前端模块化的前世今生
凡是开发大型应用程序,模块块必然是不可或缺的一部分。那么什么是模块化呢?其实模块化就是将一个复杂的系统分解成多个独立的模块的代码组织方式。在很长的一段时间里,前端只能通过一系列的<script>
标签来维护我们的代码关系,但是一旦我们的项目复杂度提高的时候,这种简陋的代码组织方式便是如噩梦般使得我们的代码变得混乱不堪。所以,在开发大型Javascript应用程序的时候,就必须引入模块化机制。由于早期官方并没有提供统一的模块化解决方案,所以在群雄争霸的年代,各种前端模块化方案层出不穷。本文将从最早期的IFEE闭包方案到现在的ES6 Modules, 追根溯源,带你详细了解前端模块化的前世今生。
IIFE
模块化的一大作用就是用来隔离作用域,避免变量冲突。而Javascript没有语言层面的命名空间概念,只能将代码暴露到全局作用域下。在刀耕火种的年代,作为脚本语言的Javascript为了避免全局变量污染,只能使用闭包来实现模块化。好在我们可以利用自执行函数(IIFE)来执行代码,从而避免变量名泄漏到全局作用域中:
1 | (function(window) { |
虽然IIFE可以有效解决命名冲突的问题,但是对于依赖管理,还是束手无策。由于浏览器是从上至下执行脚本,因此为了维持脚本间的依赖关系,就必须手动维护好script
标签的相对顺序。
AMD
AMD (Asynchronous Module Definition)也是一种 JavaScript模块化规范。从名字上可以看出,它主要提供了异步加载的功能。对于多个JS模块之间的依赖问题,如果使用原生的方式加载代码,随着加载文件的增多,浏览器会长时间地失去响应,而AMD能够保证被依赖的模块尽早地加载到浏览器中,从而提高页面响应速度。由于该规范原生Javascript无法支持,所以必须使用相应的库来实现对应的模块化。RequireJS就是实现了该规范的类库,实际上AMD也是其在推广过程中的产物。
利用RequireJS来编写模块,所有的依赖项必须提前声明好。在导入模块的时候,也会先加载对应的依赖模块,然后再执行接下来的代码,同时AMD模块可以并行加载所有依赖模块,从而很好地提高页面加载性能:
1 | define('./index.js',function(code){ |
CMD
CMD(Common Module Definition)最初是由阿里的玉伯提出的,同AMD类似,使用CMD模块也需要使用对应的库SeaJS。SeaJS所要解决的问题和requireJS一样,但是在使用方式和加载时机上有所不同:
1 | define(function(require) { |
CMD加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的。如果使用require.async()方法,可以实现模块的懒加载。
CommonJS
随着Javasript应用进军服务器端,业界急需一种标准的模块化解决方案,于是,CommonJS(www.commonjs.org)应运而生。它最初是由Kevin Dangoor在他的这篇博文中首次提出。
这是一种被广泛使用的Javascript模块化规范,大家最熟悉的Node.js应用中就是采用这个规范。在Node.js中,内置了module对象用来定义模块, require函数用来加载模块文件,代码如下:
1 | // utils.js 模块定义 |
此种模块化方案特点就是:同步阻塞式加载,无法实现按需异步加载。另外,如果想要在浏览器中使用CommonJS模块就需要使用Browserify
进行解析:
1 | npm install browserify -g |
当然,你也可以使用gulp, webpack等工具进行解析打包后引入到浏览器页面中去。
UMD
上面介绍的CommonJS和AMD等模块化方案都是针对特定的平台,如果想要实现跨平台的模块化,就得引入UMD的模块化方式。UMD是通用模块定义(Universal Module Definition)的缩写,使用该中模块化方案,可以很好地兼容AMD, CommonJS等模块化语法。
接下来,让我们通过一个简单地例子看一下如何使用和定义UMD模块:
1 | (function(root, factory) { |
这种模块定义方法,可以看做是IIFE的变体。不同的是它倒置了代码的运行顺序,需要你将所需运行的函数作为第二个参数传入。由于这种通用模块的适用性强,很多JS框架和类库都会打包成这种形式的代码。
ES6 Modules
对于ES6来说,不必再使用闭包和封装函数等方式进行模块化支持了。在ES6中,从语法层面就提供了模块化的功能。然而受限于浏览器的实现程度,如果想要在浏览器中运行,还是需要通过Babel等转译工具进行编译。ES6提供了import
和export
命令,分别对应模块的导入和导出功能。具体实例如下:
1 | // demo-export.js 模块定义 |
对于具体的语法细节,想必大家在日常使用过程中都已经轻车熟路了。但对于ES6模块化来说,有以下几点特性是需要记住的:
- ES6使用的是基于文件的模块。所以必须一个文件一个模块,不能将多个模块合并到单个文件中去。
- ES6模块API是静态的,一旦导入模块后,无法再在程序运行过程中增添方法。
- ES6模块采用引用绑定(可以理解为指针)。这点和CommonJS中的值绑定不同,如果你的模块在运行过程中修改了导出的变量值,就会反映到使用模块的代码中去。所以,不推荐在模块中修改导出值,导出的变量应该是静态的。
- ES6模块采用的是单例模式,每次对同一个模块的导入其实都指向同一个实例。
Webpack中的模块化方案
作为现代化的前端构建工具,Webpack还提供了丰富的功能能够使我们更加轻易地实现模块化。利用Webpack,你不仅可以将Javascript文件进行模块化,同时还能针对图片,css等静态资源进行模块化。你可以在代码里使用CommonJS, ES6等模块化语法,打包的时候你也可以根据需求选择打包类型,如UMD, AMD等:
1 | module.exports = { |
另外,ES6模块好处很多,但是并不支持按需加载的功能, 而按需加载又是Web性能优化中重要的一个环节。好在我们可以借助Webpack来弥补这一缺陷。Webpack v1版本提供了require.ensure
API, 而2.x之后使用了import()
函数来实现异步加载。具体的代码示例可以查看我之前所写的前端性能优化之加载技术 这篇文章。
总结
模块化方案解决了代码之间错综复杂的依赖关系,不仅降低了开发难度同时也让开发者将精力更多地集中在业务开发中。随着ES6标准的推出,模块化直接成为了Javascript语言规范中的一部分。这也意味着CommonJS, AMD, CMD等模块化方案终将退出历史的舞台。当然,要实现完全ES6模块化的使用,还需要一段长时间的等待。那么,在这段过渡的时间里,我们可能仍然需要维护旧有的代码,使用传统的模块化方案来构建应用。对于前端工程师来说,系统地了解主流的模块化方案就显得十分必要了。最后,让我们再一次回顾一下各种模块化方式的特点:
模块化方案 | 加载 | 同步/异步 | 浏览器 | 服务端 | 模块定义 | 模块引入 |
---|---|---|---|---|---|---|
IFEE | 取决于代码 | 取决于代码 | 支持 | 支持 | IFEE | 命名空间 |
AMD | 提前预加载 | 异步 | 支持 | 构建工具r.js | define | require |
CMD | 按需加载 | 延迟执行 | 支持 | 构建工具spm | define | define |
Common | 值拷贝,运行时加载 | 同步 | 原生不支持,需要使用browserify提前打包编译 | 原生支持 | module.exports | require |
UMD | 取决于代码 | 取决于代码 | 支持 | 支持 | IFEE | 命名空间 |
ES Modules(ES6) | 实时绑定,动态绑定,编译时输出 | 同步 | 需用babel转译 | 需用babel转译 | export | import |
参考资料
http://javascript.ruanyifeng.com/nodejs/module.html
《你不知道的Javascript》
——本文首发于个人公众号,转载请注明出处———
最后,欢迎大家关注我的公众号,一起学习交流。