在该系列之前的文章使用 jQuery:UI 项目中,我介绍了使用 jQuery 代码中的插件来提高 web 应用程序的效率。
但必须知道,这些插件不是自己凭空产生的,它们是由开发人员编写、测试并完善的,这些人员为 jQuery 社区奉献了自己的业余时间。我们做这些都是免费的,是出于对自己代码的热爱。本文主要关注您如何回报这个伟大的社区,即如何编写自己的插件并上传到 jQuery 的插件页面。这可以让所有人使用您创建的插件,可以让整个 jQuery 开发社区变得更好。今年您也做出自己的贡献吧。
在编写本文中的插件时,我发现插件的创建过程以及用来创建它的框架非常简单明了。困难的地方在于想一些其他人还没有做过的事情,并编写一些能真正完成某些操作的 JavaScript 代码。由于插件结构简单明了,对于新手它简单易学,对于高手它很灵活,因此插件的数量急速上升。
当然,在研究本文所涉及的内容时,我还发现每个作者编写插件的风格都不同,jQuery 允许好几种不同的插件编写风格。在本文中,我集中介绍最简单的一种风格,以及 jQuery 本身推荐的一种风格,插件弹出后您就会看到差别或不同的选项。
描述插件
创建插件的第 1 步当然是想一个好点子。像大部分点子一样,其他人总会给您创造机会。以我在本文中开发的插件为例,它不是什么新颖的概念,但在我撰写文本的时候,jQuery 插件社区还找不到该插件。我知道我个人会从该插件中受益良多。
我的插件是一个 NumberFormatter 插件。处理服务器端代码(比如 Java™ 或 PHP)和国际化的用户应该很熟悉数字格式化。众所周知,并非每个人都用相同的方式格式化数字。例如,并非每个人都使用 “里” 来度量距离。在美国,数字的写法可能是 “1,250,500.75”(这个数字是从我的税收表上抄来的),但在其他国家的写法可能完全不同:德国是 “1.250.500,75”,法国是 “1 250 500,75”,瑞士是 “1'250'500.75”,日本是 “125,0500.75”。数字完全相同,但是在向 web 应用程序用户展示时使用不同的格式。
因此,问题归结到,当编写一个国际化应用程序时,如何向不同国家的人展示这些数字?当然,解决方案是使用服务器端格式化,这种解决办法非常常见。Java 有一个健壮的格式化库,使数字的格式化变得非常简单。当使用数字在服务器上设置页面时,服务器负责处理这些数字。但是,很多时候数字可能不在服务器上,因此您需要一种方法在客户机上格式化数字,而不需要与服务器会话。
我在这里描述的典型用例如下。您的 web 应用程序中有一个输入字段,要求用户输入他们的薪水。在美国,用户可能以各种格式输入 “$65000”、“65,000”、“65000” 和 “65,000.00”。所有这些数字都是相同的,但是您需要控制这些数字在屏幕上的显示方式,这样才能提供更好的用户体验。您可以在输入数字之后调用服务器,但是如果有许多使用不同格式的数字字段就太麻烦了。此外,如果您可以在客户端处理该问题,并向用户提供即时反馈,那么就不需要这样做了。
因此,我建立了一个空缺,之后我将尝试使用 JavaScript/jQuery 功能填补这一空缺。我的插件将在客户机上提供数字格式化,为其他人提供一种国际化 web 应用程序的方式,且无需与服务器会话。作为额外的功能,我的插件还可以提供反向操作;该插件使开发人员能够解析数字,从格式化的文本字符串中获取数字。这还可以应用于客户机上的数字操作。此外,我将模拟 JavaDecimalFormatter 类中的功能,以维护执行数字格式化的客户端代码和标准服务器端方法之间的通用性。
第 1 步结果:我发现了一个插件需求,然后定义了对于该需求我可以填补的空缺。
插件规则
jQuery 团队建立了许多希望插件作者都能遵守的通用规则,为插件用户创建一个通用而可信的环境。考虑到 jQuery 团队比我聪明多了,我没有理由违背这些规则,对不对?出于该原因,我在此列出这些规则,并且在插件的每一步都尽量遵守这些规则。
- 文件命名为 “
jquery.<your plug-in name>.js ” 这是有道理的,因为您希望用户查看文件时立即知道这是一个 jQuery 插件以及这是哪个插件。
检查完毕。我的插件将命名为 “jquery.numberformatter.js ”。
- 所有新方法都附加到 jQuery.fn 对象,所有新功能都附加到 jQuery 对象
现阶段这可能有点难以理解,在下一节我将讨论更多内容,因为这是实际编码过程中最重要的规则。
检查完毕。我的方法/函数将仅附加到这两个对象。
- “
this ” 用于引用 jQuery 对象 这有利于插件作者的编写,它让所有插件作者在引用 “this ” 时都知道将从 jQuery 收到哪个对象。
检查完毕。我将仅使用 “this ” 引用 jQuery 对象。
- 插件中定义的所有方法/函数的末尾都必须带有一个 “;”(分号),否则将不利于代码的最小化。
因为这是最小化 JavaScript 文件的最佳实践,大于最小值会很糟糕,您的插件有可能很快就被抛弃。
检查完毕。所有的方法/函数都将以 “;” 结尾。
- 除有特别注明外,所有方法都必须返回 jQuery 对象
jQuery 方法的顺序链 (daisy-chaining) 非常著名,如果您编写打破链条的插件,它就一定会 “打破链条”。
检查完毕。我的format() 方法将返回 jQuery 对象,虽然我的parse() 方法没有返回 jQuery 对象,但我在很多地方都注明该函数打破了链条。(毕竟,它不可能返回一个 Number 对象而不打破链条)。
- 您应该总是使用
this.each() 迭代匹配的元素,这是一种可靠而有效地迭代对象的方式。 出于性能和稳定性考虑,他们推荐所有的方法都使用它迭代匹配的元素。
检查完毕。我的方法都将只使用该方法迭代匹配的元素。
- 总是在插件代码中使用 “jQuery” 而不是 “$”
这很重要,它使与 “$” 有冲突的用户(那些使用另一个 JavaScript 库的用户)能够使用 “var JQ = jQuery.noConflict(); ” 函数更改他们的 jQuery 别名(pseudonym)。但是,在我查看许多插件时,我发现该规则常常得不到遵守,这太不幸了。如果开发人员需要更改 jQuery 别名,那么很可能意味着该插件要被弃用了。
检查完毕。在我的插件中,我将仅使用 jQuery 而不是它的别名 “$”。
好了,这些就是在插件代码中必须遵守的规则和建议。真正的问题在于,它们实际上是强制性的,因为如果您不遵守这些插件规则,那么您的插件就得不到广泛应用,而且还会得到不好的反馈。结果该插件很快就没有人使用了,您所花费的时间都将白费。因此,遵守这些规则非常重要。这不仅能帮助您鹤立鸡群,保证您代码的统一性,还能增加插件的成功几率。
第 2 步结果:我将遵守创建 jQuery 插件的所有规则
编写插件
现在可以开始编写代码了!开始编写插件的第一步是确定如何组织您的插件。开始有两种选择:您希望它是一个方法还是一个函数?“它们有区别吗?”您可能会这样问。
正如我上面提到的,方法需要附加到 jQuery.fn 对象,函数需要附加到 jQuery 对象。这样一切都清楚了,不是吗?如果您对 jQuery 相对不太了解,那么可能还不是很清楚。您可以这样考虑。方法使代码能够迭代所有传入插件的选定元素。因此,插件可以接收任何类型的 HTML 元素,由插件决定如何处理每个元素。因此,插件方法可以接收任何 jQuery 选择器,所有从 “p” 到 “#mySpecificPageElement” 的内容。如果您希望插件更加灵活,允许用户传入任何类型的页面元素,那么最好使用方法。插件开发人员应该负责正确地处理所有内容。相比之下,函数不使用任何选定元素作为参数。函数可以简单地应用于整个页面。这也由插件开发人员负责处理,开发人员必须定义他们希望与插件交互的页面元素,并忽略其他元素。让我们在代码中看看不同之处。
清单 1. jQuery 插件方法/函数
// This is a method because you can pass any type of selector to the method and it // will take some action on the results of the selector. In this case, it will // doSomething() on the page element with an ID of myExample $("#myExample").doSomething();
// This is also a method, even though you are passing the entire page body to // the method, because you are still passing a selector $("body").doSomethingElse();
// This is a function, because you are NOT passing any selector to the function // The plug-in developer must determine what page elements they want to take action on. // This is usually accomplished by the plug-in developer requiring the page elements // to contain a certain class name.
<div class="anotherThing">
// This hypothetical plug-in developer would document that his plug-in only works // on elements with the class "anotherThing" $.anotherThing(); |
从这些描述中判断,插件使用的似乎是方法,因为您需要让用户告诉您他们希望格式化哪些页面元素。清单 2 展示了现在插件的代码。
清单 2. 方法定义
jQuery.fn.format = function();
// You would call your plug-in like this (at this point) $("#myText").format(); |
当然,您的函数不可能是放之四海而皆准的插件,因为您处理的是国际化情况,无法自动指出希望格式化文本的国家或者需要的格式。因此,您必须稍微修改插件以接收某些选项。格式化方法中需要两个选项:数字应该使用的格式(例如,#,### 以及 #,###.00)和本地语言环境(本地语言环境是一个简单的 2 字符国家代码,用于确定要使用的国际数字格式)。
您还需要让插件尽可能的易于使用,因为您必须提高插件的成功几率。这意味着您应该继续定义一些默认的选项,使用户在不想传入选项时不需要这样做。我编写插件的所在地是美国,这里使用的是世界上最常见的数字格式,我的默认语言环境是 “us”,格式默认为 “#,###.00”,因此货币自然要使用该默认值。
清单 3. 允许在插件中使用选项
jQuery.fn.format = function(options) {
// the jQuery.extend function takes an unlimited number of arguments, and each // successive argument can overwrite the values of the previous ones. // This setup is beneficial for defining default values, because you define // them first, and then use the options passed into the method as the // second argument. This allows the user to override any default values with their // own in an easy-to-use setup. var options = jQuery.extend( {
format: "#,###.00", locale: "us"
}, options); |
创建插件框架的最后一步是正确处理传入方法的选定元素。回想一下上例您会发现,选定元素可以是单页面元素,或者是多页面元素。您必须等效地处理它们。同样,回想一下 jQuery 插件规则,"this" 对象只能引用 jQuery 对象。因此,您有一个对传入方法的 jQuery 选定元素的引用,现在需要迭代它们。同样,回顾规则让我们知道,每个插件方法都应该返回 jQuery 对象。当然,您知道 jQuery 对象就是 "this" ,因此在方法中返回 this 完全没有问题。让我们看看如何在代码片段中实现迭代每个选定元素并返回 jQuery 对象。
清单 4. 处理 jQuery 对象
jQuery.fn.format = function(options) {
var options = jQuery.extend( {
format: "#,###.00", locale: "us"
}, options);
// this code snippet will loop through the selected elements and return the jQuery object // when complete return this.each(function(){ // inside each iteration, you can reference the current element by using the standard // jQuery(this) notation // the rest of the plug-in code goes here }); |
由于实际插件本身不是本文的重点,我不对此进行详细阐述,但是您可以在本文的插件代码附件中看到全部内容(请参见 下载)。如果您决定编写函数而不是方法,我还将向您展示一个样例,介绍如何设置插件架构。
清单 5. 使用函数的示例插件
jQuery.exampleFunction = function(options) {
var options = jQuery.extend( {
// your defaults
}, options);
jQuery(".exampleSelector").each(function(){
});
}); |
调优插件
网上关于初级插件的大部分文章都到此为止了,这时它们会让您采用基本的插件格式并运行。但是,这种基本架构也太 “基本” 了。在编写插件时还必须考虑另一件重要的事情,给您插件增色所需要的内容远不止一个初级插件那么简单。再多增加两个步骤,您就能将初级插件转换为中级插件。
调优 #1 - 让内部方法私有化
在任何面向对象的编程语言中,您会发现创建运行重复代码的外部函数非常方便。在我创建的 NumberFormatter 插件中,有一个这种代码的样例 —— 该代码决定向函数传递哪个地理位置,以及要使用哪些字符作为小数点和分组符。format() 方法和 parse() 方法中都需要该代码,任何一个初级程序员都会告诉您这属于它自己的方法。但是,这会出现一个问题,因为您处理的是 jQuery 插件:如果您使用 JavaScript 中的定义将它作为自己的函数,那么任何人都可以为任何目的使用脚本调用该方法。这不是该函数的目的,我更倾向于不调用它,因为它仅用于内部工作。那么,让我们看看如何将该函数私有化。
这种私有方法问题的解决方案称为 Closure,它可以有效地从外部调用关闭整个插件代码,附加到 jQuery 对象的除外(那些是公共方法)。通过这种设计,您可以将任何代码放入插件中,不用担心被外部脚本调用。通过将插件方法附加到 jQuery 对象,您可以有效地将它们变为公共方法,而让其他的函数/类私有化。清单 6 展示了实现该操作所需的代码。
清单 6. 私有化函数
// this code creates the Closure structure (function(jQuery) {
// this function is "private" function formatCodes(locale) { // plug-in specific code here }; // don't forget the semi-colon
// this method is "public" because it's attached to the jQuery object jQuery.fn.format = function(options) {
var options = jQuery.extend( {
format: "#,###.00", locale: "us"
},options);
return this.each(function(){ var text = new String(jQuery(this).text()); if (jQuery(this).is(":input")) text = new String(jQuery(this).val());
// you can call the private function like any other function var formatData = formatCodes(options.locale.toLowerCase());
// plug-in-specific code here }); }; // don't forget the semi-colon to close the method
// this code ends the Closure structure })(jQuery);
调优 #2 - 让插件的默认值可覆盖
调优插件的最后一步是让它可以覆盖默认值。毕竟,如果德国的开发人员下载了该插件,而且了解他的所有 web 应用程序用户希望使用德文版本,那么您应该让他能够使用一行代码修改默认语言环境,而不是要他在每个方法调用中都修改一遍。这样您的插件才会非常方便,因为一个 web 应用程序不太可能使用不同的国际化格式向用户展示数字。您在网页上看一下就知道,所有数字都是使用同一个语言环境的格式。
该步骤要求您修改某处代码,因此您将看到让插件最为耀眼的一步。 清单 7. 覆盖默认值
jQuery.fn.format = function(options) { // Change how you load your options in to take advantage of your overridable defaults // You change how your extend() function works, because the defaults // are globally defined, rather than within the method. If you didn't use the // {} as the first argument, you'd copy the options passed in over the defaults, which is // undesirable. This {} creates a new temporary object to store the options // You can simply call the defaults as an object within your plug-in var options = jQuery.extend({},jQuery.fn.format.defaults, options);
return this.each(function(){
// rest of the plug-in code here
// define the defaults here as an object in the plug-in jQuery.fn.format.defaults = { format: "#,###.00", locale: "us" }; // don't forget the semi-colon |
这是创建插件的最后一个步骤!这样您就有了一个不错的插件,可以进行最后的测试了。清单 8 展示了您可以放入本文的完整插件,以便您查看这些部分是如何变为一个整体的。此外还包含了 parse() 函数,到目前为止我还没有讨论过该函数,但是它包含在插件中(我没有详细介绍插件处理格式化的部分,因为它们不在本文讨论之列。样例中包含了该部分,插件本身当然也有)。 清单 8. NumberFormatter 插件
(function(jQuery) {
function FormatData(valid, dec, group, neg) { this.valid = valid; this.dec = dec; this.group = group; this.neg = neg; };
function formatCodes(locale) { // format logic goes here return new FormatData(valid, dec, group, neg); };
jQuery.fn.parse = function(options) {
var options = jQuery.extend({},jQuery.fn.parse.defaults, options);
var formatData = formatCodes(options.locale.toLowerCase());
var valid = formatData.valid; var dec = formatData.dec; var group = formatData.group; var neg = formatData.neg;
var array = []; this.each(function(){
var text = new String(jQuery(this).text()); if (jQuery(this).is(":input")) text = new String(jQuery(this).val());
// now we need to convert it into a number var number = new Number(text.replace(group,'').replace(dec,".").replace(neg,"-")); array.push(number); });
return array; };
jQuery.fn.format = function(options) {
var options = jQuery.extend({},jQuery.fn.format.defaults, options);
var formatData = formatCodes(options.locale.toLowerCase());
var valid = formatData.valid; var dec = formatData.dec; var group = formatData.group; var neg = formatData.neg;
return this.each(function(){ var text = new String(jQuery(this).text()); if (jQuery(this).is(":input")) text = new String(jQuery(this).val());
// formatting logic goes here
if (jQuery(this).is(":input")) jQuery(this).val(returnString); else jQuery(this).text(returnString); }); };
jQuery.fn.parse.defaults = { locale: "us" };
jQuery.fn.format.defaults = { format: "#,###.00", locale: "us" };
})(jQuery); |
测试插件
创建插件的最后一步是全面测试它。用户在插件中发现 bug 会让他们很恼火。用户不会去修复它,他们会快速放弃使用它。如果有几个这类用户再加上一些糟糕的评论,您的插件很快就会石沉大海。此外,这是一个很好的互惠行为 —— 您希望自己使用的插件都经过了很好的测试,那么您也应该提供经过良好测试的插件。
我创建了一个快速测试结构来测试我的插件(不需要单元测试库),该结构创建了许多跨区,跨区中是一些数字,紧接数字后面的是该数字的正确格式。JavaScript 测试对数字调用格式,然后比较格式化的数字与想要的结果,如果失败则显示为红色。通过该测试,我可以设置不同的测试用例,测试所有可能的格式(我已经这样做了)。我将测试页面附加到样例 下载 中,以便您为测试自己插件找到一个可能的解决方案,利用 jQuery 进行测试。
查看完成的插件
让我们看看运行中的新 NumberFormatter。我已经创建了一个简单的 web 应用程序,您可以查看 NumberFormatter 插件如何满足您的应用程序。
图 1. 运行中的 NumberFormatter
这个 Web 应用程序很简单,也很直接。当用户离开文本字段时(输入了薪水、住宅、子女信息之后),NumberFormatter 插件将相应地格式化其输入的信息。该插件使 Web 应用程序能够向用户展示统一格式的数字。还要注意,该 web 应用程序是为德国用户格式化的,因此小数点和分组符号与美国用户不一样(关于这一点,请让我展示如何更改默认值)。 清单 9. 运行中的 NumberFormatter
$(document).ready(function() {
// use the AlphaNumeric plug-in to limit the input $(".decimal").decimal(); $(".numeric").numeric();
// you want to change the defaults to use the German locale // this will change the default for every method call on the // entire page, so you won't have to pass in the "locale" // argument to any function $.fn.format.defaults.locale = "de"; $.fn.parse.defaults.locale = "de";
// when the salary field loses focus, format it properly $("#salary").blur(function(){ $(this).format({format:"#,###.00"}); });
// when the house field loses focus, format it properly $("#houseWorth").blur(function(){ $(this).format({format:"#,###"}); });
// when the kids field loses focus, format it properly $("#kids").blur(function(){ $(this).format({format:"#"}); });
// calculate the tax $("#calculate").click(function(){ // parse all the numbers from the fields var salary = $("#salary").parse(); var house = $("#houseWorth").parse(); var kids = $("#kids").parse(); // make some imaginary tax formula var tax = Math.max(0,(0.22*salary) + (0.03*house) - (4000*kids)); // place the result in the tax field, and then format the resulting number // you need one intermediate step though, and that's the "formatNumber" function // because all numbers in JavaScript use a US locale when made into a String // you need to convert this Number into a German locale String before // calling format on it. // So, the steps are: // 1) the tax is a Number that looks like 9200.54 (US locale) // 2) formatNumber converts this to a String of 9200,54 (German locale) // 3) put this String in the #tax field // 4) Call format() on this field $("#tax").text($.formatNumber(tax)).format({format:"#,###"}); });
}); |
在结束之前,关于 NumberFormatter 插件还有几件事情需要指出。首先,该插件是第一个 1.0.0 发行版,因此我希望将来进行扩展,包含更多 Java DecimalFormatter 中的格式化功能。包括支持货币、科学计数法和百分比。它还对负数和正数包含不同的格式化规则,负数不是简单的 “-”(例如,对负数使用 (5,000),在会计中是这样做的)。最后,一个好的格式器应该支持格式中的任何字符,而仅它忽略不属于保留字符的部分。这都是我近期想添加的功能,希望该插件变得更加健壮。
获取用户的语言环境
最后一个问题与 jQuery 无关,但是使用该插件时可能会出现 —— 如何获取用户的语言环境?这个问题提得很好,因为目前使用 JavaScript 没有办法获取该信息。您需要创建一个 JavaScript Bridge 来实现该目的。什么是 JavaScript Bridge?我的意思是您可以建立一个简单的设计模式将值从服务器端代码传入 JavaScript 代码。清单 10 展示了您可以使用 Java 在 JSP 页面做到这一点。
清单 10. 获取用户的语言环境
<%
// the request object is built into JSPs // unfortunately, it's not any easier // tested on FF, IE, Safari, Chrome String locale = "us"; // or your default locale String accLang = request.getHeader("Accept-Language"); if (accLang.length() > 5) { accLang = accLang.substring(0,5); locale = accLang.substring(accLang.indexOf("-")+1); }
%>
$(document).ready(function() {
// take advantage of the ability to override defaults by using the JavaScript // Bridge here. Then your page can use the format() and parse() functions // elsewhere in the page without modifying them for a user's locale. $.fn.format.defaults.locale = "<%=locale%>"; $.fn.parse.defaults.locale = "<%=locale%>";
}); |
共享插件
最后,编写和测试插件都做完了。最后一步是与他人共享该插件,并将它上传到 jQuery 网站的插件存储库。
- 转到 jQuery 网站的插件页面,在左导航栏,单击 Login/Register 然后单击 Create New Account。如果已经有一个帐户,请登录;如果没有,则创建一个新帐户。
- 验证通过后,左导航将出现一些选项。其中有一个 “Add plug-in”。
- 导航插件创建页面。因为您只能使用 jQuery 1.2 测试该插件,您应该将其作为一个兼容版本包含在内。花一些时间为插件写一个好标题以及一个好的描述。毕竟,现在是向其他用户推销该插件的时候了,您需要让自己与众不同。尽量讲出该插件的好处。
- 该插件需要您提供插件主页。虽然您创建了插件,但很可能没有插件主页。幸运的是,如果您没有自己的服务器保存插件,Google 很乐于为开源项目提供空间。我选择在 Google Code 中放置该插件。要建立自己的 Google Code 项目,只需要访问 code.google.com 然后按照注册流程注册即可。
- 按下 Submit 之后您的插件就创建好了!
恭喜,您的插件现在是 jQuery 插件社区的一部分了,您现在正式成为开源项目的贡献者之一。给自己一个 5 星评级以奖励自己吧!因为您值得!
结束语
在本文中,我主要介绍如何为 jQuery JavaScript 框架创建插件。我从头开始,构思了一个想法并将其付诸实现,介绍了创建该插件要使用的几个步骤。然后,我阅读了 jQuery “戒律”,它是为确保插件的一致性而设置的插件规则。我还在文章中提醒这些规则,因为我看到许多规则在插件中都没有得到遵守,尤其是只能在插件中使用 “jQuery” 而不是 “$”(当然,我遵守了该规则)。
介绍了插件的背景,以及编写插件的规则之后,您了解了基本的插件框架以及在插件中编写方法与编写函数的不同。方法应该在采用选定元素作为参数并对其执行某些操作时使用。提供页面元素的任务由调用方法的人负责。另一方面,函数应该在对选定元素不感兴趣时使用,因为您已经知道要执行操作的页面元素。提供页面元素的任务由编写该函数的开发人员负责。这两种形式的插件都是有效的,仅在在插件需求上有所不同。最后,查看设置默认选项的基本方式以及如何让用户提供自己的选项。
本文的下一步是为插件提供一些亮点,以增加它的先进性。该步骤给整个插件画上了句号,有效地创建了私有函数和公共函数。我创建了一些在插件内部调用的函数,以便插件外部的人无法调用它们。您还可以看到如何向插件用户展示默认值,让用户定义他们自己的默认值,轻松实现编码。
最后,在样例 web 应用程序中使用插件展示它的行为。本文的最后一部分是这些努力工作的最终成果 —— 您需要将插件上传到 jQuery 插件社区站点,让它成为 JavaScript 库的一部分。 |