javascript模块化编程思想、实现与规范
随着BS架构的发展,网站逐渐变成了互联网应用程序,嵌入网络的JavaScript代码越来越庞大,越来越复杂(业务逻辑处理或用户交互很多写在前端)。网页越来越像桌面程序,需要一个团队分工协作、进度管理、单元测试等。。开发者不得不使用软件工程的方法,管理网页的业务逻辑。因此JavaScript模块化编程已经成了一个迫切的需求,理想的情况下是开发者只需要实现核心的业务逻辑,其他业务处理都可以加载别人已经写好的模块,做到明确分工而不会相互影响。
但是,JavaScript却不是一种模块化编程语言,它不支持类(class),更别说模块(module)了。虽然ECMAScipt正在谋划支持和推广类和模块的概念,但要实际投入生产还是遥遥无期,只能自己另外想办法。为此JavaSript社区做了很多努力,努力在现有的运行环境中,利用现有的资源,实现模块化的效果。
模块化的原始写法
模块的定义就是实现特定功能的一组方法,只要把不同的函数(以及记录状态的变量)简单地放在一起,就算是一个模块了。
function f1() { // doSomething } function f2() { // doSomething }
上面的函数f1()和f2()共同组成了一个模块,使用的时候直接通过函数名调用就行了。这种做法的缺点很明显,既污染了全局变量(f1和f2处在全局的上下文栈中,属于window的属性),也无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接的关系。
模块化的对象写法
为了解决上面的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面。
var module1 = { status: 233, f1: function() { // doSomething }, f2: function() { // doSomething } }
上面的函数f1()和函数f2()都封装在了module1对象里,使用的时候就是通过访问module1对象的属性。
module1.f1();
但是,这样的写法会暴露所有的模块成员,且内部的状态可以被外部改写。
module1.status = 666;
模块化的立即执行函数写法
使用立即执行函数(Immediately-Invoked Function Expression,IIFE),可以达到不暴露私有成员的目的。
var module1 = (function() { var status = 233; var f1 = function() { // doSomething }; var f2 = function() { // doSomething }; })();
使用这样的写法,外部的代码就无法读取到内部的变量。
console.log(module1.status); // undefined
这种写法,就是JavaScript模块化的基本写法,后面的实现基本上都是依照这个思路。
模块化的放大模式
如果一个模块很大,就会要拆分成几个小的模块,或者一个模块需要继承另一个模块,这个时候就要采用放大模式(Augmentation)。
var module1 = (function(mod) { mod.f3 = function() { // doSomething }; return mod; })(module1);
这里为module1模块添加了一个新函数f3(),然后返回新的module1模块。也就是把旧的对象传进来,给这个对象添加属性,然后返回添加了属性后的对象,相当于扩展。
模块化的宽放大模式
在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上一节的写法,第一个执行的部分有可能加载一个不存在的空对象,这时就要采用宽放大模式(Loose Augmentation)。
var module1 = (function(mod) { // doSomething return mod; })(window.module1 || {});
与放大模式相比,宽放大模式就是立即执行函数的参数可以是空对象。
模块化的全局变量输入
独立性是模块化的重要特点,模块内部最好不与程序的其他部分直接交互。
为了在模块内部调用全局变量,必须显式地将其他变量输入模块。
var module1 = (function($, YAHOO) { // doSomething })(jQuery, YAHOO);
这里的module1模块中需要使用jQuery库和YUI库,于是就把这两个库(其实是两个模块)当作参数输入module1。这样做除了保证模块的独立性,也使得模块之间的依赖关系变得明显。
模块化的几种规范
模块化的前提是要遵循同一套规范,否则模块之间的调用会十分困难。
JavaScript官方没有模块化的规范,目前通用的民间规范主要有CommonJS(服务端js模块化的规范,NodeJS是这种规范的实现)、AMD(Asynchronous Module Definition异步模块定义,RequireJS遵循此规范)和CMD(Common Module Definition,通用模块定义,SeaJS遵循此规范)。
模块化在服务端的规范:CommonJS
2009年,美国程序员Ryan Dahl创造了node.js项目,将JavaScipt语言用于服务器端编程(后端)。这标志着JavaScipt模块化编程正式诞生。因为老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。
node.js的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有一个全局性的方法require(),用于加载模块。假定有一个数学模块math.js,就可以像下面这样加载并调用模块中提供的方法:
var math = require('math'); math.add(2, 3); // 5
更多的用法这里就不说了,只需要知道CommonJS是使用require()函数加载模块就行了。
模块化规范从服务端到客户端的发展
有了服务端的模块化之后,大家就想要客户端的模块化了。而且最好两者能兼容,一个模块不用修改,在服务器和浏览器都可以运行。但是由于一个重大局限,使得CommonJS规范不适用于浏览器环境。这是因为,在上面的代码中,调用math的方法必须要在math.js加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。
这对于服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是对于浏览器来说,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,可能会导致浏览器处于假死状态。
因此浏览器端的模块化不能使用同步加载(Synchronous),只能使用异步加载(Asynchronous)。这就是AMD规范诞生的背景。
模块化在客户端的规范:AMD
AMD(Asynchronous Module Definition,异步模块定义)采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,直到加载完成之后,这个回调函数才会运行。
AMD也采用require()语句加载模块,不同于CommonJS的是,它要求两个参数:
require([module], callback);
第一个参数[moudle],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是这样:
require(['math'], function(math) { math.add(2, 3); });
这样,math.add()与math模块的加载就不是同步的,浏览器也不会发生假死的状况。所以很显然地是AMD比较适合浏览器环境。应用AMD规范的主要有require.js。
模块化在客户端的规范:CMD
CMD是SeaJS在推广过程中对模块定义的规范化产出。
AMD和CMD的区别:
1.对于依赖的模块,AMD是提前执行,CMD是延迟执行。CMD推崇的是as lazy as possible,即尽可能得懒加载(延迟加载),即在需要得时候才加载。
2.CMD推崇依赖就近,AMD推崇依赖前置。
// CMD define(function(require, exports, module) { var a = require('./a'); a.doSomething(); var b = require('./b'); // 依赖可以就近书写 b.doSomething(); }) // AMD 默认推荐的是 define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好 a.doSomething(); b.doSomething(); })
3.AMD的API默认是一个当多个用,CMD的API则是严格区分,推崇职责单一。比如在AMD里,require分全局和局部,而在CMD里则没有全局require,而是根据模块系统的完备性,提供seajs.use来实现模块系统的加载启动。CMD里,每个API都简单存粹。
模块化的优点总结
1.解决了命名的冲突问题。多人开发的场景下,容易出现命名冲突,模块化通过内部封装与外部隔离能有效防止命名冲突的问题。
2.解决了文件的依赖问题,使文件易于管理。如果有很多js文件相互依赖,依赖关系和加载顺序都是让人头冷的问题。使用模块化就可以很好地实现依赖管理(使用依赖都要提前声明)。
3.提高代码的可读性。各个模块各自完成自己的功能,专司其职,除了问题也会便于维护。
4.提高代码的复用性。可以抽提特定的通用功能作为一个通用的模块。
"可是怎么办,想起你的时候,心还是会疼。"