愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

前端模块化总结

随着Web前端工程的日益庞大和复杂,前端模块化开始成为前端领域所关注的热点问题

一、前端模块化现状

模块化首先由NodeJS引入(require),继而得到大规模发展和推广,并且延伸到了浏览器端,目前,前端模块化主要形成了以下的布局:

二、模块化的途径

1、函数封装

使用函数进行模块化的封装,如:

function fn1() {
    // ...
}

function fn2() {
    // ...
}

然后,在需要使用的时候,加载所需函数所在的文件,然后调用即可。这种方式实现起来很简单,但是缺点也很明显:
1)污染了全局变量,无法保证不与其他模块发生命名冲突
2)无法体现模块之间的依赖关系

2、对象

可以使用JavaScript的对象来解决变量全局污染的问题。使用如:

var ModuleA = {
    var1: 1,
    var2: 2,
    fn1 : function() {
        // ...
    },
    fn2 : function() {
        // ...
    }
}

这样子做的好处是,只要保证模块的名称唯一即可,模块内部的变量和方法有了个命名空间,可以避免全局污染,而且同一个模块内的成员也能够彼此关联。但是由于JavaScript的对象并没有办法控制对对象的访问,所以,外部是可以随意修改模块的内部成员的,如:

ModuleA.var1 = 3

如此一来,便会产生一些问题

3、立即执行函数

在ES6之前,只有函数能够创建作用域。而立即执行函数实现模块化的原理则是,在函数外部,是无法修改函数内部的变量和方法的,所以立即执行函数对外是隔绝的,除非我们暴露出了对外的接口。如:

const ModuleA = (function() {
    var var1 = 1
    var var2 = 2

    function fn1() {
        // ...
    }

    function fn2() {
        // ...
    }

    return {
        fn1: fn1,
        fn2: fn2
    }
})()

而这种方式,也是目前大多数前端模块化方案的实现原理

三、CommonJS

CommonJSNodeJS采用的模块化方案。CommonJS主要推崇:一个单独的文件就是一个模块,每一个模块都是一个单独的作用域。其他模块是无法得到该模块的内部变量或者函数的,除非该模块暴露出了变量或者函数给外部调用。如:

var str = 'Hello, world'
function greet() {
    console.log(str)
}

module.exports = greet

module.exports对象是提供外部调用的接口,是模块外部与内部通信的桥梁。使用的时候,可以用require()引用,require()的返回值便是module.exports对象,如:

const greet = require('./greet')
greet('This is a str')

然而,CommonJS这种方案,并不适合浏览器端。因为我们都知道Node是服务端的JavaScript,服务端的模块是在本地磁盘上的,加载很快,使用CommonJS完全没有问题。而浏览器中的模块,则是通过网络下载的,如果使用CommonJS的方案,那么就得同步加载,如此一来,如果一个模块很长时间没法下载下来,就会导致浏览器失去响应。因此,就出现了AMD

四、AMD

AMD是Asynchronous Module Definition的缩写,即异步模块定义。AMD实际上是RequireJS在推广过程中对模块定义的规范化的产出。而AMD规范,主要解决的问题是:
1)多个js文件之间有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器中
2)js加载的时候,浏览器会停止渲染,加载越多,页面响应时间就越长

语法

定义一个模块,采用define函数,即:

define(id?, dependencies?, factory)

对参数的说明如下:
1)id,模块的标识(默认为脚本文件名去掉扩展名)
2)dependencies,当前模块依赖的模块名称数组(默认值为['require', 'exports', 'module']
3)factory,工厂方法。模块初始化时要执行函数或者对象。如果为函数,则只执行一次,如果是对象,此对象为模块的输出值
其中,id名称的进一步说明为:

  • id名称是用/分隔的有意义单词的字符串,且单词要为camel格式,或者...
  • 模块名称不能包含扩展名(如.js
  • 模块名称可以是相对的(首字符为.或者..,相对于require书写和调用的模块解析),或者顶级的(顶级的从根命名空间开始解析)

使用范例:

define('someModule', ['d1', 'd2', 'd3'], function(d1, d2, d3) {
    // ...
})

如果要使用一个模块,则可以使用require(),其格式如:

require([dependencies], function(){})

参数说明:

  • [dependencies],是一个数组,表示使用时依赖的模块
  • 第二个参数是一个回调函数,在指定的依赖模块都加载成功后,回调函数便可以得以执行(但是依赖模块的加载仍然是异步的,只是只有当它们都加载完毕后,回调函数才会运行,因而解决了依赖性的问题)

使用范例:
1)依赖单个模块

require(['someModule'], function(someModule) {
    // ...
})

2)依赖多个模块

require(['jquery', './math.js'], function($, math) {
    // ...
})

3)模块输出

define(['jquery'], function($) {
    var someFn = function() {
        // ...
    }
    return someFn
})

4)模块定义内部引用依赖

define(function(require) {
    var $ = require('jquery')
    // ...
})


五、CMD

CMD是Common Module Definition的缩写,即通用模块定义。它是国内发展出来的,是SeaJS在推广过程中的产物。
CMD推崇:
1)一个模块一个文件,所以通常用文件名作为模块id
2)依赖就近

语法

定义一个模块如:

define(factory)
define(id?, dependencies?, factory)

参数的含义和AMD类似,但是factory方法用法则不同,AMD中factory方法传入的是加载后的模块。而CMD的factory函数的原型则为:

function(require, exports, module) {
    // ...
}

参数说明:
1)factory参数:在factory函数内部,可以使用require(id)来引用加载后的模块,如:

define(function(require, exports) {
    var a = require('./a')
    // ...
})

由于require()是同步往下执行的,如果需要异步执行,可以使用require.async(id, callback),如:

define(function(require, exports, module) {
    require.async('./a', function(a) {
        // ...
    })
})

此外,还可以使用require.resolve(id)来返回模块的路径,而不会加载模块
2)exports参数:用来向外提供模块接口
使用如:

define(function(require, exports) {
    // ...
    exports.foo = 'some str'
    exports.doSomething = doSomething
    // 不能是 exports = function() { } 的形式,要用 modules.export
    // 这和NodeJS中的做法一致
})

当然,如果不使用exports,而直接return,也是可以的,即如:

define(function(require, exports) {
    // ...
    return {
        foo: 'some str',
        doSomething: doSomething
    }
})

所以我们可以发现,使用CMD可以让我们在浏览器端如同使用NodeJS般使用模块

使用seaJS加载模块,形式如:

// 加载一个模块
seajs.use('./a')
// 加载一个模块后执行回调
seajs.use('./a', function(a) {
    // ...
})
// 加载多个模块后执行回调
seajs.use(['./a', './b'], function(a, b) {
    // ...
})


六、AMD和CMD的区别

我们可以发现,AMD和CMD是比较像的。它们的区别主要有:
1)AMD推崇依赖前置(定义模块的时候,就需要声明其依赖的模块),而CMD推崇依赖就近(定义模块的时候,可以在需要的时候再去require)
2)AMD和CMD对依赖模块的执行时机是不同的(不是加载时机)。在模块加载时机方面,AMD和CMD都是异步加载的。但是AMD在依赖模块加载完成后就会立即执行,它只能保证主逻辑能够在依赖模块全部加载完毕后得以执行(而依赖的加载顺序却和书写顺序不一定一样)。CMD加载完某个模块后,并不执行,而是只有在所有的模块都加载完成后,进入主逻辑中,碰到require()语句的时候,就执行对应的模块(所以模块的执行顺序,和书写顺序是一样的)
3)AMD中,有全局require(使用模块)和局部require(定义模块的factory方法内部使用)。而CMD中,推崇职责单一,因此CMD中只有局部require,全局的使用则使用seajs.use()