Skip to content

模块化,通往未来JavaScript库之路

I must work and study hard... edited this page Aug 2, 2014 · 20 revisions

编辑:github 原文:Modules, a Future Approach to JavaScript Libraries

JS库在过去的十年里已经发展到了顶峰,像jQuery在浏览器端兼容方面取得了非常巨大的成功,比如事件处理,Ajax和DOM操作。

当时,jQuery几乎解决了我们所有的问题,我们带着这股超强能量一路披荆斩棘。

但是web在发展,API在改进,标准在被不断的实现,web技术正在你来我往的进行快速更新,我不确定当前的巨人是否可以在将来的浏览器端找到属于自己的位置,但我知道未来生态正在被面向模块所主导。

进入模块的世界

一个模块通常是由做一件事情的很多方法组合而成。比如,一个模块负责向元素增加class样式、或者基于Ajax进行HTTP通信等等,数不胜数。

各个模块可能大小和样子都不同,但是它们有一个共同的目标,那就是可以组合到一起保证功能的运行。简而言之,运行在一起的每个模块都会有自己的逻辑实现,不论在客户端还是服务器端。

这样这些模块之间就会形成依赖,而这些依赖还是很容易管理的。沉浸在巨型库的日子正在慢慢褪去,因为它不够灵活。像jQuery这类的库已经意识到了这点,他们提供了一个在线工具可以让你只下载自己需要的部分。

现代版的API助推了模块化灵感的出现,因为当前浏览器实现已经大幅改进,我们只要创建一个很小的工具型模块就可以帮组我们做最普通的任务了。

模块化时代已经来临,准确的说是已经到来。

第一个模块的启发

从现代版API出现的时候,我就对classList产生了很大的兴趣。受到诸如jQuery库的启发,我们现在将使用一个原生的方法去为元素增加class。

classList已经出现有一段时间了,但是知道的人并不是很多,这就启发我去创建一个模块来封装这个API。

在我们深入代码之前,我们先看下jQuery是如何给一个元素增加class的。

$(elem).addClass(‘myclass’);

当这个代码被加载的时候,我们不在关心之前讨论的classList API,取而代之的是DOMTokenList对象,它存储了对应元素的className(空格隔开取值)。classList API提供了一些和DOMTokenList对象交互的方法,这和jQuery很像。下面是一个如果通过classListadd()增加class的例子:

elem.classList.add(‘myclass’);

从这里我们能学到什么呢?一个库如果能够融入一个语言的特性是一件非常棒的事情。这就是开放WEB平台的伟大,对于代码的处理,我们可以拥有自己的想法。

那下一步呢?我们知道了模块,而且我们喜欢这个classList API,但是不幸的是,当前并不是所有的浏览器都支持它,不过我们可以写个后备方法。这听起来是个不错的想法:如果支持则使用classList,否则自动转到后备方法中。

创建第一个模块:Apollo.js

大约6个月前,我写了一个非常独立而且轻量级的模块用来对一个元素增加class,而且是原生js代码,最后我将它命名为apollo.js。

这个模块的主要目的就是在不依赖其他库的情况下使用classList API去做一些简答的事情。jQuery还没有使用classList API,因此我想去试验这个新技术是非常值得的。

我当前的任务就是尽可能好的设计它、组装它。

使用classList

正如我们所见,classList是一个非常优雅的API,并且对jQuery开发者来说也很友好,上手会很快。但是有一点不是我喜欢的,就是必须每次调用classList 对象来使用它的方法。我决定移除掉这些重复部分,使用下面的API进行设计:

apollo.addClass(elem, ‘myclass’);

一个好的class操作模块应该包含 hasClass, addClass, removeClass 和 toggleClass方法,这些方法都是在apollo命名空间下。

自己看上面的addClass方法,你会发现我把元素节点传递给了它作为第一个参数。和jQuery不一样,jQuery采用的是在对象上绑定这些方法,这就很容易形成大对象。而这里我要设计的模块只是接收一个DOM节点,我们要做的就是使用开发者定义的元素、很原生的方法。第二个参数是一个字符串,就是class名字。

让我们过下剩下的class操作方法:

  • apollo.hasClass(elem, ‘myclass’);
  • apollo.addClass(elem, ‘myclass’);
  • apollo.removeClass(elem, ‘myclass’);
  • apollo.toggleClass(elem, ‘myclass’);

那我们从哪里开始呢?首先我们需要一个添加这些方法的对象。使用IIFE,我包装了一个对象apollo来创建我们的模块定义。

(function () {

var apollo = {};

apollo.hasClass = function (elem, className) {
    return elem.classList.contains(className);
};

apollo.addClass = function (elem, className) {
    elem.classList.add(className);
};

apollo.removeClass = function (elem, className) {
    elem.classList.remove(className);
};

apollo.toggleClass = function (elem, className) {
    elem.classList.toggle(className);
};

window.apollo = apollo;

})();

apollo.addClass(document.body, 'test');

现在classList可以工作了,我们该思考浏览器兼容问题了。apollo模块的目标就是提供一个独立小巧的API,而不管是啥浏览器。

测试classList特性的最简单方法就是下面这样:

if ('classList' in document.documentElement) {
    // you’ve got support
}

我们使用in操作符来判断classList是否存在,如果存在,则使用它来提供给用户:

(function () {

var apollo = {};
var hasClass, addClass, removeClass, toggleClass;

if ('classList' in document.documentElement) {
    hasClass = function () {
        return elem.classList.contains(className);
    }
    addClass = function (elem, className) {
        elem.classList.add(className);
    }
    removeClass = function (elem, className) {
        elem.classList.remove(className);
    }
    toggleClass = function (elem, className) {
        elem.classList.toggle(className);
    }
}

apollo.hasClass = hasClass;
apollo.addClass = addClass;
apollo.removeClass = removeClass;
apollo.toggleClass = toggleClass;

window.apollo = apollo;

})();

判断class是否存在有不少方法,可以读取className然后循环遍历所有的name,然后可以替换它或者新增它。jQuery经常这样做,使用长循环或者复杂的结构,但是我不想弄坏我这个轻量级模块的味道,因此使用正则表达式去匹配和替换,这样能达到同样的效果。

下面是我想到的最简洁的实现了:

function hasClass (elem, className) {
    return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className);
}

function addClass (elem, className) {
    if (!hasClass(elem, className)) {
        elem.className += (elem.className ? ' ' : '') + className;
    }
}

function removeClass (elem, className) {
    if (hasClass(elem, className)) {
        elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'), '');
    }
}

function toggleClass (elem, className) {
    (hasClass(elem, className) ? removeClass : addClass)(elem, className);
}

现在让我们把这些集成到模块中,同时把浏览器兼容性代码也加进去:

(function () {

    var apollo = {};
    var hasClass, addClass, removeClass, toggleClass;

    if ('classList' in document.documentElement) {
        hasClass = function () {
            return elem.classList.contains(className);
        };
        addClass = function (elem, className) {
            elem.classList.add(className);
        };
        removeClass = function (elem, className) {
            elem.classList.remove(className);
        };
        toggleClass = function (elem, className) {
            elem.classList.toggle(className);
        };
    } else {
        hasClass = function (elem, className) {
            return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className);
        };
        addClass = function (elem, className) {
            if (!hasClass(elem, className)) {
                elem.className += (elem.className ? ' ' : '') + className;
            }
        };
        removeClass = function (elem, className) {
            if (hasClass(elem, className)) {
                elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'), '');
            }
        };
        toggleClass = function (elem, className) {
            (hasClass(elem, className) ? removeClass : addClass)(elem, className);
        };
    }

    apollo.hasClass = hasClass;
    apollo.addClass = addClass;
    apollo.removeClass = removeClass;
    apollo.toggleClass = toggleClass;

    window.apollo = apollo;

})();

好,功能我们就谈到这里。apollo模块还有一些其他特性,比如一次可以增加多个class,如果你有兴趣你可以试下。

那我们到现在为止都做了哪些事情呢?我们创建了多个函数的包装,这个包装用来去做一件事,而且一件事就够了。这个模块非常简单易读,而且很容易做单元测试。后面我们可以把apollo引入到项目中,它的小巧远胜jQuery。

依赖管理:AMD和CommonJS

模块的概念并不新颖,我们经常使用他们。你可能知道JS可不仅仅只用于浏览器端,它可以运行于服务端甚至是TV。

对于这个新创建的模块我们应该采用哪个模式呢?我们能在什么地方使用它呢?当前有两个概念:AMD和CommonJS,下面让我们深入剖析下。

AMD

异步模块定义(AMD)是一个用来定义异步加载模块的JS API,它一般用于浏览器端因为在调试和跨域访问中需要异步加载。AMD可以在开发阶段把各个模块分散在各个不同的文件中。

AMD使用了define方法,用来定义一个模块并导出对象。使用AMD,我们可以引用任何依赖。下面是一个简单的例子:

define([‘alpha’], function (alpha) {
  return {
    verb: function () {
      return alpha.verb() + 2;
    }
  };
});

如果你想用AMD的话你也可以这样做:

define([‘apollo’], function (alpha) {
  var apollo = {};
    var hasClass, addClass, removeClass, toggleClass;

    if ('classList' in document.documentElement) {
    hasClass = function () {
        return elem.classList.contains(className);
    };
    addClass = function (elem, className) {
        elem.classList.add(className);
    };
    removeClass = function (elem, className) {
        elem.classList.remove(className);
    };
    toggleClass = function (elem, className) {
        elem.classList.toggle(className);
    };
    } else {
    hasClass = function (elem, className) {
        return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className);
    };
    addClass = function (elem, className) {
        if (!hasClass(elem, className)) {
            elem.className += (elem.className ? ' ' : '') + className;
        }
    };
    removeClass = function (elem, className) {
        if (hasClass(elem, className)) {
            elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'),       '');
        }
    };
    toggleClass = function (elem, className) {
        (hasClass(elem, className) ? removeClass : addClass)(elem, className);
    };
}

apollo.hasClass = hasClass;
apollo.addClass = addClass;
apollo.removeClass = removeClass;
apollo.toggleClass = toggleClass;

window.apollo = apollo;
});

CommonJS

这几年依赖管理工具盒模式已经伴随着Nodejs迅速发展起来了。NodeJS使用了CommonJS,后者通过一个esports对象来定义一个模块的内容。下面看一个最简单的CommonJS实现:

// someModule.js
exports.someModule = function () {
  return "foo";
};

上面的代码我把它放在了someModele.js文件中。在其他地方只要引用它就可以使用它了,按照CommonJS的描述,我们只要调用require就可以了:

// do something with `myModule`
var myModule = require(‘someModule’);

如果你使用过Grunt或者Gulp,那么你可能会习惯这种模式了。

为了在apollo中使用这个模式,我们不在使用window对象来引用他,而是使用了exports对象(看最后一行exports.apollo = apollo)。

(function () {

var apollo = {};
var hasClass, addClass, removeClass, toggleClass;

if ('classList' in document.documentElement) {
    hasClass = function () {
        return elem.classList.contains(className);
    };
    addClass = function (elem, className) {
        elem.classList.add(className);
    };
    removeClass = function (elem, className) {
        elem.classList.remove(className);
    };
    toggleClass = function (elem, className) {
        elem.classList.toggle(className);
    };
} else {
    hasClass = function (elem, className) {
        return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className);
    };
    addClass = function (elem, className) {
        if (!hasClass(elem, className)) {
            elem.className += (elem.className ? ' ' : '') + className;
        }
    };
    removeClass = function (elem, className) {
        if (hasClass(elem, className)) {
            elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'), '');
        }
    };
    toggleClass = function (elem, className) {
        (hasClass(elem, className) ? removeClass : addClass)(elem, className);
    };
}

apollo.hasClass = hasClass;
apollo.addClass = addClass;
apollo.removeClass = removeClass;
apollo.toggleClass = toggleClass;

exports.apollo = apollo;

})();

统一模块定义(UMD)

AMD和CommonJS都不是非常完美的解决方案,因为我我们想创建一个可以运行于任何地方的模块。

不错,我们可以通过If else来判断当前哪个方案可用,如果支持AMD或者CommonJS,那就可以直接使用它,这个解决方案被称为UMD。UMD包装了If else分支,我们只要传入一个函数给它就可以了,下面是一个例子:

(function (root, factory) {
if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define(['b'], factory);
} else {
    // Browser globals
    root.amdWeb = factory(root.b);
}
}(this, function (b) {
//use b in some fashion.

// Just return a value to define the module export.
// This example returns an object, but the module
// can return a function as the exported value.
return {};

}));

哇!这里还差了东东。我们把一个函数传递给了IIFE风格的函数第二个参数factory,这个factory被设定在AMD和全局变量中。注意,这里不支持CommonJS,但是我们可以让它支持下:

(function (root, factory) {
if (typeof define === 'function' && define.amd) {
    define(['b'], factory);
} else if (typeof exports === 'object') {
    module.exports = factory;
} else {
    root.amdWeb = factory(root.b);
}
}(this, function (b) {
return {};
}));

这里巧妙的module.exports = factory这行代码,使用它可以支持CommonJS。

让我们把apollo填充到这个UMD结构中,这样它就可以不仅运行于CommonJS环境中,同时也可以运行于AMD和浏览器端。

/*! apollo.js v1.7.0 | (c) 2014 @toddmotto | https://github.com/toddmotto/apollo */
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    define(factory);
  } else if (typeof exports === 'object') {
    module.exports = factory;
  } else {
    root.apollo = factory();
  }
})(this, function () {

  'use strict';

  var apollo = {};

  var hasClass, addClass, removeClass, toggleClass;

  var forEach = function (items, fn) {
    if (Object.prototype.toString.call(items) !== '[object Array]') {
      items = items.split(' ');
    }
    for (var i = 0; i < items.length; i++) {
      fn(items[i], i);
    }
  };

  if ('classList' in document.documentElement) {
    hasClass = function (elem, className) {
      return elem.classList.contains(className);
    };
    addClass = function (elem, className) {
      elem.classList.add(className);
    };
    removeClass = function (elem, className) {
      elem.classList.remove(className);
    };
    toggleClass = function (elem, className) {
      elem.classList.toggle(className);
    };
  } else {
    hasClass = function (elem, className) {
      return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className);
    };
    addClass = function (elem, className) {
      if (!hasClass(elem, className)) {
        elem.className += (elem.className ? ' ' : '') + className;
      }
    };
    removeClass = function (elem, className) {
      if (hasClass(elem, className)) {
        elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'), '');
      }
    };
    toggleClass = function (elem, className) {
      (hasClass(elem, className) ? removeClass : addClass)(elem, className);
    };
  }

  apollo.hasClass = function (elem, className) {
    return hasClass(elem, className);
  };

  apollo.addClass = function (elem, classes) {
    forEach(classes, function (className) {
      addClass(elem, className);
    });
  };

  apollo.removeClass = function (elem, classes) {
    forEach(classes, function (className) {
      removeClass(elem, className);
    });
  };

  apollo.toggleClass = function (elem, classes) {
    forEach(classes, function (className) {
      toggleClass(elem, className);
    });
  };

  return apollo;

});

我们已经创建了一个跨多个环境的模块,这对于项目中增加一个新特性到我们项目中就非常灵活-我们不需要分成好几块来分别支持一个功能。

测试

很明显,我们的模块是支持单元测试的,而且只需要很少的测试用例,这样开发者就能很容易的把它集成到项目中了,并做功能增强。和巨型库需要花好长时间去实现新特性和修复bug相比,小模块可以很快速的更新。

包装

我们自己创建我们自己的模块并且可以支持多个开发环境,这是一件很棒的事情。这是的开发更加可控、有趣,同时我们可以更加了解我们的工具,从而用的更好。模块都附有说明文档,我们可以很快的把它应用到我们的项目中。如果一个模块不可用,我们可以可以找个其他的模块,当然也可以自己写。我们最好不要直接依赖一个巨型库。

惊喜:ES6模块

最后,一个可喜的结果就是,JS库已经影响了原生JS语言了,比如类管理。下一代JS语言ES6会支持import和export。

看下export代码:

/// myModule.js
function myModule () {
  // module content
}
export myModule;

import代码:

import {myModule} from ‘myModule’;

你可以点击这里读更懂关于ES6和模块化的说明。

Clone this wiki locally