岁月如歌

用开放的心态,打造专业的人生。

jQuery 模块介绍与 jQuery 插件的深度模块化

with 40 comments

jQuery 模块

大名鼎鼎的 jQuery 就不多介绍了,详细介绍推荐官网:jquery.com
阮一峰最近整理的文章也不错,推荐:jQuery 设计思想, jQuery 最佳实践

几点感悟:

  1. jQuery 是 DOM 操作类库,其核心功能是找到 DOM 元素并对其进行操作。
  2. 拿 jQuery 与 YUI, Dojo 等框架相比是不公平的,就如拿轮胎和汽车相比一样。jQuery 只是一个轮胎,功能很单一,YUI 和 Dojo 等则是相对完整的汽车,除了轮胎,还有引擎、外壳等等。
  3. 说 jQuery 不适合构建大型应用,就如说轮胎不适合参加赛车比赛一样不合逻辑。你可以用 jQuery 做轮胎,然后选择其他部件组合起来去 DIY 一辆赛车。能否胜出,得看赛车手的 DIY 水准。
  4. jQuery 的困局在于 DIY 高手不多,经常是一个好轮胎挂上一堆破破烂烂的外壳就上前线了。jQuery 的破局也在于 DIY. DIY 意味着灵活、可替换性,意味着可快速前行和高性能。
  5. jQuery 灵活性带来的缺陷,比如有可能由选择器和链式风格导致的低效 DOM 操作,目前在提供了同类功能的 YUI3 等类库中同样存在。这不是类库的问题,更多是因为使用者的经验欠缺导致的。就如一把优秀的菜刀,到了一个拙劣的厨子手中,依旧切不好菜一样。工具很重要,但更重要的是我们得提升自己的刀工。
  6. 最后,回到第一点:jQuery 是 DOM 操作类库。非 DOM 操作,都是 jQuery 的辅助功能,不是 jQuery 的强项,就如菜刀不能当斧头用一样。

我们可以通过简单封装,让 jQuery 成为 CommonJS 的模块。这样,调用时只要 require 即可:

test.html:

<script src="http://modules.seajs.com/libs/seajs/1.0.1/sea.js"></script>
<script>
seajs.use('./init');
</script>

init.js:

seajs.config({
  alias: {
    'juery': 'jquery/1.6.1/jquery'
  }
});

define(function(require, exports, module) {
  var $ = require('jquery');
  // do something with jQuery
});

jQuery 插件的模块化

jQuery 提供了 DOM 操作功能,在实际应用中,我们还需要 cookie, template, storage 等等一系列功能。这时可以从 jQuery 社区中寻找各种插件来完成。大部分插件通过 jQuery 插件的模块化 一文中提供的方法封装就好。

之前的封装方法,总结成一句话是:“jQuery 穿肠过,插件身上留”。正如 Kidwind 反馈的一样,每次“穿肠过”的时候都要运行一次插件代码,频繁调用某些插件时,会存在 CPU 浪费,还可能带来隐患:

假设有以下jquery插件a, b, c, d,它们之间的关系如下
b 依赖于 a
c 依赖于 a
d 依赖于 b c

假设页面使用到d插件,那么插件a将进行两次初始化,也就是会调用两次
var $ = require(‘jquery’);
require(‘a’)($);
进行插件a的注册,当系统复杂时,重复的插件注册会不会影响系统的性能,同时会不会存在隐患?如插件b对引用的插件a进行了部分功能扩展,当引入插件c的时候又重新注册了插件a,那么插件b对插件a的扩展将不存在了,当然改写插件功能的实际情况也许不会存在,此处只是举个例子,说明隐患的存在。
如何避免重复的插件注册,可以避免隐患,同时获得更好的性能(避免了多次插件注册的运算耗时)。

面对这种情况,我们究竟应该如何做好 jQuery 插件的模块化?

jQuery 插件的形式

jQuery 插件一般可以总结为以下模板

(function($) {  
  // Main plugin function
  $.fn.PLUGIN = function(options) {
    // snip...
  };

  // Public plugin function
  $.fn.PLUGIN.FUNCT = function() {
    // Cool JS action
  };

  // Default settings for the plugin
  $.fn.PLUGIN.defaults = { /* snip... */ };

  // Private function that is used within the plugin
  // snip...
})(jQuery);

简言之就是往 $.fn 上添加新成员,有部分插件还会往 $ 上添加成员。

之前的“穿肠过”模块化方式,可以表示为:

define(function() { return function($) {
  $.fn.PLUGIN = ...
}});

调用方式:

define(function(require, exports) {
  var $ = require('jquery');
  require('some-jquery-plugin')($);

  $(sth).PLUGIN(...);
});

不是很直观,不够方便,还有前面提到的隐患。

深度模块化

为了更好的模块化,意味着我们要添加更多代码:

some-jquery-plugin.js:

define(function(require, exports, module) {
  var $ = require('jquery').sub();

  // Main plugin function
  $.fn.PLUGIN = function(options) {
    // snip...
  };

  // Public plugin function
  $.fn.PLUGIN.FUNCT = function() {
    // Cool JS action
  };

  // Default settings for the plugin
  $.fn.PLUGIN.defaults = { /* snip... */ };

  // Private function that is used within the plugin
  // snip...

  module.exports = $;
});

这样封装后,调用变成:

define(function(require, exports) {
  var $ = require('jquery');
  var PLUGIN = require('some-jquery-plugin');

  PLUGIN(sth).PLUGIN(...);
});

这样能解决之前提到的重复初始化问题,但是 PLUGIN(sth).PLUGIN(...) 的使用方式怪怪的。比如这个非常帅的 chosen 插件,按照上面的方式模块化后,调用方式为:

chosen('#some-id').chosen();

虽然可用,但怎么看怎么别扭。这是因为 jQuery 是以 DOM 为中心的,代码的默认流程是找到要操作的 DOM 元素,然后对其进行操作。这种代码书写方式,对于模块后的插件来说,很别扭。更好的期待中的调用方式是:

define(function(require, exports) {
  var $ = require('jquery');
  var Chosen = require('chosen');

  var chosen = new Chosen(selector, options);
  chosen.doSth(...);
});

理论上,我们甚至可以不知道 chosen 依赖 jQuery, 我们需要关心的只是 chosen 的 API. 上面这种理想的调用方式,需要我们对插件进行“深度”模块化:

some-jquery-plugin.js:

define(function(require, exports, module) {
  var $ = require('jquery');

  // Main plugin function
  function PLUGIN(selector, options) {
    var els = $(selector);
    // snip...
  };

  // Public plugin function
  PLUGIN.FUNCT = function() {
    // Cool JS action
  };

  // Default settings for the plugin
  PLUGIN.defaults = { /* snip... */ };

  // Private function that is used within the plugin
  // snip...

  module.exports = PLUGIN;
});

也就是说,在 plugin 的代码里,我们并不对 $.fn 或 $ 进行扩展,只用 $ 来进行 DOM 操作而已,返回的是独立的 PLUGIN 对象,就和我们写普通的业务模块一样。这样,就实现预期中更优雅的调用方式。

jQuery 的插件机制,在模块化面前很鸡肋。jQuery 一直被冠以“不适合大型项目”,也和 jQuery 的这种插件机制有关系。这会导致大家都去污染 $.fn, 这就和污染全局变量一样。项目一大,冲突的概率,和调试的成本都会变大,很悲剧。

因此,推荐大家利用模块的机制去重构一部分好用的 jQuery 插件,目前 dew 项目里已经重新实现了 cookie 等部分模块。强烈推荐大家都参与进来,将自己喜欢的,常用的 jQuery 等插件迁移过来。或者推进插件作者直接修改源码,增加对 CommonJS 的支持。路漫漫,但众人拾柴火焰高,星火可燎原,期待大家的参与。

建议大家直接 fork dew 项目,可以将自己重构的模块 pull request 过来,邮件给 seajs(at)googlegroups.com 群组。讨论和 code review 后,就可以转成 dew 的正式模块。

等模块丰富起来,我们就可以有更多时间去做更意思的事情了。

Advertisements

Written by lifesinger

August 19, 2011 at 22:51

Posted in Articles

40 Responses

Subscribe to comments with RSS.

  1. 从jQuery插件书写方式讲到jQuery深度模块化,最后得出的结论是…. 不要写 jQuery 插件?

    ktmud

    August 19, 2011 at 23:21

  2. 赞成,只把jquery当轮胎用,在轮胎上挂太多东西会越来越笨重,支持dew项目

    hpf1908

    August 19, 2011 at 23:33

  3. 1 楼真相帝

    用 jQuery 的 Plugin 机制写出来的项目就像一堆铁钉里扔进一块磁铁的感觉:
    1. 很扎手
    2. 一碰就坏
    3. 离了 jQuery 活不了

    pw

    August 20, 2011 at 01:22

  4. 感觉jquery插件还是很方便的,没有遇到太大的问题,还将继续使用

    ivy

    August 20, 2011 at 13:54

  5. 最近重构代码时,确实遇到了玉伯说的 jQuery 的这些问题。
    深切地感受到了 jQuery 在模块化方面的缺陷以及对 $.fn 扩展的危害啊。

    糖伴西红柿

    August 20, 2011 at 23:12

  6. init.js示例代码上的jquery拼错了

    wo_is神仙

    August 22, 2011 at 11:02

  7. 个人权衡了一下,觉得“深度”模块化工程量比较大,而且在版本更新时会比较麻烦,插件一多那维护起来困难,打算还是采用“丑陋”的办法,这和我当初所想到的解决方案不谋而合,当初也是觉得丑陋,不过现在看来,丑陋好过高维护量,可以想成jquer插件的模块反回的仍然是jquery对象心理就稍平衡些了。感谢玉伯兄这么快对我的疑问作出了答复!!!!

    Kidwind

    August 22, 2011 at 12:07

  8. […] jQuery 模块介绍与 jQuery 插件的深度模块化 […]

  9. 深度模块化需要修改源码的工作量不是一点半点,更新维护的成本太高,不切实际,相信大多数人都不会采用,其实为什么不换个角度思考下,传肠过方式的问题其实是本身seajs具有的问题,重复注册插件的问题可以通过记录依赖链的方式来解决,一旦依赖链中存在此插件,不执行此句require就可以了

    TerryLee

    August 23, 2011 at 11:26

    • 最近几天又想了下,深度模块化也可以不修改源码,采用外层 adapter 的方式封装。等有时间了我再写篇博客说明。

      lifesinger

      August 23, 2011 at 12:29

      • adapter 方式封装能有例子演示下么。

        invinca

        November 2, 2011 at 15:28

  10. […] seajs 越来越热门,相关的模块化讨论也比较多,玉伯最近一篇文章(jQuery 模块介绍与 jQuery 插件的深度模块化)讨论了如何修改 jQuery 模块使之成为 commonjs […]

  11. 玉伯,你好啊,我就一个疑问,在这里讲的“深度模块化”到底是什么意思?难道就是将每一个插件都要重新修改一下吗,都要利用这种模块化方法进行模块化吗?

    lookforstudy

    August 23, 2011 at 20:30

  12. 一直有个问题.望解答.
    如我做一个遮罩层组件M,在一个页面中有多个模块(A,B,C,D)都在用M,按你的说法,那每个模块在运行时就要分别实例化多个M.但实际运用中只需要一个M实例就够了.A,B,C,D只调用其show,hide方法即可..这种情况应如何处理.

    qfljm

    August 24, 2011 at 14:32

    • 这个取决与遮罩层本身的 api. 比如遮罩层的实现就是单例模式,提供的 api 直接就是:Mask.config(…), Mask.show/hide, 这样就可以做到多模块调用同一个 Mask.

      lifesinger

      August 24, 2011 at 14:48

    • M 模块

      define(function(require, exports) {
          var M = function(){...};
          // ....
      
          var instance = null;
          exports.getInstance = function() {
              if (instance === null) {
                 instance = new M();
              }
              return instance;
          };
      });
      

      用法

      define(function(require) {
          var M = require('M');
      
          var m = M.getInstance(); // M 只会被 new 一次
      });
      

      perfectworks

      August 24, 2011 at 14:51

  13. 同意玉伯的观点,用jQuery三年了,我基本上不用jQuery的插件机制,都是独立封装。
    写了个ie6fixed的小模块,同时支持CommonJS。
    代码:https://github.com/wangjianjun/ie6fixed
    文档:http://wangjianjun.github.com/ie6fixed/index.html

    lifehacker007

    August 26, 2011 at 23:19

  14. 有a、b、c三个模块,
    b依赖a,
    c依赖a,
    页面A用到b模块,
    页面B用到c模块,
    seajs是如何管理这3个模块的代码的?a模块的代码有没有被下载两次到用户的浏览器?
    如果没有,是怎样解决的?
    盼解答

    jason

    September 1, 2011 at 16:47

  15. […] seajs 越来越热门,相关的模块化讨论也比较多,玉伯最近一篇文章(jQuery 模块介绍与 jQuery 插件的深度模块化)讨论了如何修改 jQuery 模块使之成为 commonjs […]

  16. […] seajs 越来越热门,相关的模块化讨论也比较多,玉伯最近一篇文章(jQuery 模块介绍与 jQuery 插件的深度模块化)讨论了如何修改 jQuery 模块使之成为 commonjs […]

  17. 技术文章学习一下

    仁心博客

    September 6, 2011 at 20:32

  18. 请教博主,seajs的优点毋庸置疑。但如果想用到JQ的:$(document).ready的话,怎么办?用sea引入了JQ,window.onload先于dom ready执行。这样到是有个好处,就是不需要使用到dom ready了。
    但是如果需要dom ready时就执行一些动作的话,这时用JQ的domready已经晚了,用preload也同样在onload后执行。
    想了下办法,把jq和init.js合并在一起貌似可以。。

    cloudend

    September 8, 2011 at 16:16

    • $(document).ready(fn) 还是可以用的,呵呵。
      其实 js 文件都异步化加载后,domready 可以逐步废弃掉了,意义不大。最开始 domready 的价值在于,让 js 的执行不阻塞页面渲染,同时避免 ie 下的报错。异步化后,默认就不阻塞,一般也不会导致 ie 报错,因此 domready 可以“被忘记”了,呵呵。

      lifesinger

      September 8, 2011 at 17:07

  19. 大湿你好,我想问问kissy 现在还更新吗?

    http://docs.kissyui.com/kcs/core.html

    是用什么生成的?

    fengyin

    September 12, 2011 at 09:09

    • kissy 还在维护的,目前主要维护人是承玉。cheat sheet 是手工维护的,不是自动生成的。

      lifesinger

      September 12, 2011 at 10:29

      • 把你们的 cheat sheet 拿下来去做其他东西方便查询的,要做什么授权吗?还是直接用就好?

        fengyin

        September 12, 2011 at 23:35

    • @fengyi: 直接用就好,无需授权。

      lifesinger

      September 13, 2011 at 09:12

  20. 还有大师,你有没有考虑过为 yui 和closure 写一个图形界面。加入一大堆功能不是挺好的吗?

    fengyin

    September 12, 2011 at 23:44

    • 我不是大师哦。没考虑过图形界面,都自动化了,普通开发者不需要关心,图形化意义不大。

      lifesinger

      September 13, 2011 at 09:22

  21. 请问玉伯在mac下是用什么编辑器写代码的?

    雕刻

    September 14, 2011 at 17:11

    • IntelliJ IDEA

      lifesinger

      September 15, 2011 at 09:25

      • 为什么你不用 textmate ? 那个不是很好吗?

        还有大师我用你的 界面做了点东西

        https://ganquan.info/standard-c/

        别找我索取版权费。。。我只有穷得卖裤衩了。。

        fengyin

        September 22, 2011 at 08:55

    • textmate 是文本编辑器,IDEA 是 IDE,就如手枪和机关枪一样,各有所长。

      cheat sheet 的样式随便用,没关系。

      lifesinger

      September 22, 2011 at 09:26

  22. 玉伯能再讲讲 seajs 与 kissy 的深度结合吗? 看见 item.taobao.com 的页面已经用上 seajs 了。

    无口

    September 21, 2011 at 17:15

  23. 有问题啊,,这样做的话在一个链式操作里要用到两个插件怎么办?

    zicjin

    September 26, 2011 at 11:09

    • 链式操作的最佳实践,我觉得是用在 jQuery 默认提供的 api 上。对于插件而言,追求链式未必好,适当打断,有利于代码的清晰化、提升可读性。

      lifesinger

      April 5, 2012 at 14:23

  24. […] jQuery 模块介绍与 jQuery 插件的深度模块化 […]

  25. 为什么我按上面的方式,封装jquery.cookie.js 总是报 jQuery is undefined ?

    ky

    January 25, 2013 at 17:08

  26. 传统插件类似直接继承jQuery 而深度模块是组合jQuery??

    Nightink

    March 22, 2013 at 09:14


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s