凡是开发大型应用程序, 模块块必然是不可或缺的一部分. 那么什么是模块化呢? 其实模块化就是将一个复杂的系统分解成多个独立的模块的代码组织方式. 在很长的一段时间里, 前端只能通过一系列的 < script > 标签来维护我们的代码关系, 但是一旦我们的项目复杂度提高的时候, 这种简陋的代码组织方式便是如噩梦般使得我们的代码变得混乱不堪. 所以, 在开发大型 JavaScript 应用程序的时候, 就必须引入模块化机制. 由于早期官方并没有提供统一的模块化解决方案, 所以在群雄争霸的年代, 各种前端模块化方案层出不穷. 本文将从最早期的 IFEE 闭包方案到现在的 ES6 Modules, 追根溯源, 带你详细了解前端模块化的前世今生.
IIFE
模块化的一大作用就是用来隔离作用域, 避免变量冲突. 而 JavaScript 没有语言层面的命名空间概念, 只能将代码暴露到全局作用域下. 在刀耕火种的年代, 作为脚本语言的 JavaScript 为了避免全局变量污染, 只能使用闭包来实现模块化. 好在我们可以利用自执行函数 (IIFE) 来执行代码, 从而避免变量名泄漏到全局作用域中:
- (function(Windows) {
- Windows.jQuery = {
- // 这里是代码
- };
- })(Windows);
虽然 IIFE 可以有效解决命名冲突的问题, 但是对于依赖管理, 还是束手无策. 由于浏览器是从上至下执行脚本, 因此为了维持脚本间的依赖关系, 就必须手动维护好 script 标签的相对顺序.
AMD
AMD (Asynchronous Module Definition)也是一种 JavaScript 模块化规范. 从名字上可以看出, 它主要提供了异步加载的功能. 对于多个 JS 模块之间的依赖问题, 如果使用原生的方式加载代码, 随着加载文件的增多, 浏览器会长时间地失去响应, 而 AMD 能够保证被依赖的模块尽早地加载到浏览器中, 从而提高页面响应速度. 由于该规范原生 JavaScript 无法支持, 所以必须使用相应的库来实现对应的模块化. RequireJS 就是实现了该规范的类库, 实际上 AMD 也是其在推广过程中的产物.
利用 RequireJS 来编写模块, 所有的依赖项必须提前声明好. 在导入模块的时候, 也会先加载对应的依赖模块, 然后再执行接下来的代码, 同时 AMD 模块可以并行加载所有依赖模块, 从而很好地提高页面加载性能:
- define('./index.js',function(code){
- // code 就是 index.JS 返回的内容
- return {
- sayHello: function(name) {
- return "Hello," + name;
- }
- }
- });
- CMD
CMD(Common Module Definition)最初是由阿里的玉伯提出的, 同 AMD 类似, 使用 CMD 模块也需要使用对应的库 SeaJS.SeaJS 所要解决的问题和 RequireJS 一样, 但是在使用方式和加载时机上有所不同:
- define(function(require) {
- // 通过 require 引用模块
- var path=require.resolve('./cmdDefine');
- alert(path);
- });
CMD 加载完某个依赖模块后并不执行, 只是下载而已, 在所有依赖模块加载完成后进入主逻辑, 遇到 require 语句的时候才执行对应的模块, 这样模块的执行顺序和书写顺序是完全一致的. 如果使用 require.async()方法, 可以实现模块的懒加载.
CommonJS
随着 Javasript 应用进军服务器端, 业界急需一种标准的模块化解决方案, 于是, CommonJS(www.commonjs.org)应运而生. 它最初是由 Kevin Dangoor 在他的这篇博文中首次提出.
这是一种被广泛使用的 JavaScript 模块化规范, 大家最熟悉的 Node.JS 应用中就是采用这个规范. 在 Node.JS 中, 内置了 module 对象用来定义模块, require 函数用来加载模块文件, 代码如下:
- // utils.JS 模块定义
- var add = function(a, b) {
- return a + b;
- };
- module.exports = {
- add: add
- };
- // 加载模块
- var utils = require('./utils');
- console.log(utils.add(1, 2));
此种模块化方案特点就是: 同步阻塞式加载, 无法实现按需异步加载. 另外, 如果想要在浏览器中使用 CommonJS 模块就需要使用 Browserify 进行解析:
NPM install browserify -g
browserify utils.JS> bundle.JS
当然, 你也可以使用 gulp, webpack 等工具进行解析打包后引入到浏览器页面中去.
UMD
上面介绍的 CommonJS 和 AMD 等模块化方案都是针对特定的平台, 如果想要实现跨平台的模块化, 就得引入 UMD 的模块化方式. UMD 是通用模块定义 (Universal Module Definition) 的缩写, 使用该中模块化方案, 可以很好地兼容 AMD, CommonJS 等模块化语法.
接下来, 让我们通过一个简单地例子看一下如何使用和定义 UMD 模块:
- (function(root, factory) {
- if(typeof define === 'function' && define.amd) {
- define(['jquery'], factory);
- } else if(typeof module === 'object' &&
- typeof module.exports === 'object') {
- var jQuery = require('jquery');
- module.exports = factory(jQuery);
- } else {
- root.UmdModule = factory(root.jQuery);
- }
- }(this, function(jQuery) {
- // 现在你可以利用 jQuery 做你想做的事了
- }));
这种模块定义方法, 可以看做是 IIFE 的变体. 不同的是它倒置了代码的运行顺序, 需要你将所需运行的函数作为第二个参数传入. 由于这种通用模块的适用性强, 很多 JS 框架和类库都会打包成这种形式的代码.
ES6 Modules
对于 ES6 来说, 不必再使用闭包和封装函数等方式进行模块化支持了. 在 ES6 中, 从语法层面就提供了模块化的功能. 然而受限于浏览器的实现程度, 如果想要在浏览器中运行, 还是需要通过 Babel 等转译工具进行编译. ES6 提供了 import 和 export 命令, 分别对应模块的导入和导出功能. 具体实例如下:
- // demo-export.JS 模块定义
- var name = "scq000"
- var sayHello = (name) => {
- console.log("Hi," + name);
- }
- export {name, sayHello};
- // demo-import.JS 使用模块
- import {sayHello} from "./demo-export";
- sayHello("scq000");
对于具体的语法细节, 想必大家在日常使用过程中都已经轻车熟路了. 但对于 ES6 模块化来说, 有以下几点特性是需要记住的:
ES6 使用的是基于文件的模块. 所以必须一个文件一个模块, 不能将多个模块合并到单个文件中去.
ES6 模块 API 是静态的, 一旦导入模块后, 无法再在程序运行过程中增添方法.
ES6 模块采用引用绑定(可以理解为指针). 这点和 CommonJS 中的值绑定不同, 如果你的模块在运行过程中修改了导出的变量值, 就会反映到使用模块的代码中去. 所以, 不推荐在模块中修改导出值, 导出的变量应该是静态的.
ES6 模块采用的是单例模式, 每次对同一个模块的导入其实都指向同一个实例.
Webpack 中的模块化方案
作为现代化的前端构建工具, Webpack 还提供了丰富的功能能够使我们更加轻易地实现模块化. 利用 Webpack, 你不仅可以将 JavaScript 文件进行模块化, 同时还能针对图片, CSS 等静态资源进行模块化. 你可以在代码里使用 CommonJS, ES6 等模块化语法, 打包的时候你也可以根据需求选择打包类型, 如 UMD, AMD 等:
- module.exports = {
- //...
- output: {
- library: 'librayName',
- libraryTarget: 'umd', // 配置输出格式
- filename: 'bundle.js'
- }
- };
另外, ES6 模块好处很多, 但是并不支持按需加载的功能, 而按需加载又是 Web 性能优化中重要的一个环节. 好在我们可以借助 Webpack 来弥补这一缺陷. Webpack v1 版本提供了 require.ensureAPI, 而 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》
https://www.infoq.cn/article/es6-in-depth-modules?utm_source=articles_about_ES6-In-Depth&utm_medium=link&utm_campaign=ES6-In-Depth
来源: http://www.jianshu.com/p/ccc9875c2d80