模块化
模块化是指把一个复杂的系统分解到多个模块以方便编码。
很久以前,开发网页要通过命名空间的方式来组织代码,例如: jQuery 库把它的API都放在了 window.$ 下,在加载完 jQuery 后其他模块再通过 window.$ 去使用 jQuery。 这样做有很多问题,其中包括:
命名空间冲突,两个库可能会使用同一个名称
无法合理地管理项目的依赖和版本
无法方便地控制依赖的加载顺序
当项目变大时这种方式将变得难以维护,需要用模块化的思想来组织代码。
后面发展起来了众多的前端模块化规范:
CommonJS
定义
CommonJS 是一个项目,其目标是为 JavaScript 在网页浏览器之外创建模块约定。创建这个项目的主要原因是当时缺乏普遍可接受形式的 JavaScript 脚本模块单元,模块在与运行JavaScript 脚本的常规网页浏览器所提供的不同的环境下可以重复使用。
很长一段时间 JavaScript 语言是没有模块化的概念的,直到 Node.js 的诞生,把 JavaScript 语言带到服务端后,面对文件系统、网络、操作系统等等复杂的业务场景,模块化就变得不可或缺。于是 Node.js 和 CommonJS 规范就相得益彰,共同走入开发者的视线。
CommonJS 是一种使用广泛的 JavaScript 模块化规范,核心思想是通过 require 方法来同步地加载依赖的其他模块,通过 module.exports 导出需要暴露的接口。
CommonJS 还可以细分为 CommonJS1 和 CommonJS2,区别在于 CommonJS1 只能通过 exports.XX = XX 的方式导出,CommonJS2 在 CommonJS1 的基础上加入了 module.exports = XX 的导出方式。 CommonJS 通常指 CommonJS2。
使用
采用 CommonJS 导入及导出时的代码如下:
// 导入 const moduleA = require('./moduleA'); // 导出 module.exports = moduleA.someFunc;
用 module.exports
定义当前模块对外输出的接口,用 require
加载模块。
// 定义模块 area.js function area(radius) { return Math.PI * radius * radius; } // 在这里写上需要向外暴露的函数、变量 module.exports = { area: area } // 引用自定义的模块时,参数包含路径 var math = require('./math'); math.area(2);
优点
- 代码可复用于 Node.js 环境下并运行,例如做同构应用;
- 通过 NPM 发布的很多第三方模块都采用了 CommonJS 规范。
缺点
- 代码无法直接运行在浏览器环境下,必须通过工具转换成标准的 ES5。
这种规范天生就不适用于浏览器,因为它是同步的。浏览器端每加载一个文件,要发网络请求去取,如果网速慢,就非常耗时,浏览器就要一直等 require 返回,就会一直卡在那里,阻塞后面代码的执行,从而阻塞页面渲染,使得页面出现假死状态。
AMD (Asynchronous Module Definition)
定义
AMD 是 “Asynchronous Module Definition” 的缩写,意思就是"异步模块定义"。AMD 规范主要是为了解决针对浏览器环境的模块化问题。
先介绍一下 RequireJS。
RequireJS is a JavaScript file and module loader. It is optimized for in-browser use, but it can be used in other JavaScript environments, like Rhino and Node. Using a modular script loader like RequireJS will improve the speed and quality of your code.
RequireJS 的基本思想是,通过 define 方法,将代码定义为模块。当这个模块被 require 时,它开始加载它依赖的模块,当所有依赖的模块加载完成后,开始执行回调函数,返回值是该模块导出的值。
AMD 就是 RequireJS 在推广过程中对模块定义的规范化产出。AMD 与 CommonJS 最大的不同在于它采用异步的方式去加载依赖的模块。 它解决了 CommonJS 规范不能用于浏览器端的问题。
使用
采用 AMD 导入及导出时的代码如下:
// 定义一个模块 define('module', ['dep'], function(dep) { return exports; }); // 导入和使用 require(['module'], function(module) { });
优点
- 可在不转换代码的情况下直接在浏览器中运行;
- 可异步加载依赖;
- 可并行加载多个依赖;
- 代码可运行在浏览器环境和 Node.js 环境下。
缺点
- JavaScript 运行环境没有原生支持 AMD,需要先导入实现了 AMD 的库后才能正常使用。
CMD (Common Module Definition)
定义
和 AMD 类似,CMD 是 Sea.js 在推广过程中对模块定义的规范化产出。
Sea.js 是阿里的玉伯写的。它的诞生在 RequireJS 之后,玉伯觉得 AMD 规范是异步的,模块的组织形式不够自然和直观。于是他在追求能像 CommonJS 那样的书写形式。于是就有了 CMD 。
Sea.js:追求简单、自然的代码书写和组织方式,具有以下核心特性:
简单友好的模块定义规范:Sea.js 遵循 CMD 规范,可以像 Node.js 一般书写模块代码。
自然直观的代码组织方式:依赖的自动加载、配置的简洁清晰,可以让我们更多地享受编码的乐趣。
使用
代码在运行时,首先是不知道依赖的,需要遍历所有的require关键字,找出后面的依赖。具体做法是将function toString后,用正则匹配出require关键字后面的依赖。显然,这是一种牺牲性能来换取更多开发便利的方法。
CMD 规范的实现:
<script src="sea.js"></script> <script src="a.js"></script>
首先要在 html 文件中引入 sea.js 工具库,就是这个库提供了定义模块、加载模块等功能。它提供了一个全局的 define 函数用来定义模块。所以在引入 sea.js 文件后,再引入的其它文件,都可以使用 define 来定义模块。
// 所有模块都通过 define 来定义 define(function(require, exports, module) { // 通过 require 引入依赖 var a = require('xxx') var b = require('yyy') // 通过 exports 对外提供接口 exports.doSomething = ... // 或者通过 module.exports 提供整个接口 module.exports = ... }) // a.js define(function(require, exports, module){ var name = 'morrain' var age = 18 exports.name = name exports.getAge = () => age }) // b.js define(function(require, exports, module){ var name = 'lilei' var age = 15 var a = require('a.js') console.log(a.name) // 'morrain' console.log(a.getAge()) //18 exports.name = name exports.getAge = () => age })
Sea.js 可以像 CommonsJS 那样同步的形式书写模块代码的秘诀在于:
当 b.js 模块被 require 时,b.js 加载后,Sea.js 会扫描 b.js 的代码,找到 require 这个关键字,提取所有的依赖项,然后加载,等到依赖的所有模块加载完成后,执行回调函数,此时再执行到 require(‘a.js’) 这行代码时,a.js 已经加载好在内存中了。
优点
- 同样实现了浏览器端的模块化加载。 可以按需加载,依赖就近。
缺点
- 依赖 SPM 打包,模块的加载逻辑偏重。
Sea.js 实现了对 JS 代码的模块化组织,大大提高了前端开发效率。然而在实际项目中,大量的细分模块却导致大量的脚本请求,拖慢了页面加载速度,也给服务器造成不小的压力。针对这一情况,spm(static package manager)因运而生,专门用于打包、压缩 Sea.js 模块以及 CSS 文件。
ES6 Modules
定义
ES6 模块化是欧洲计算机制造联合会 ECMA 提出的 JavaScript 模块化规范,它在语言的层面上实现了模块化。浏览器厂商和 Node.js 都宣布要原生支持该规范。它将逐渐取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
使用
采用 ES6 模块化导入及导出时的代码如下:
// 导入 import { readFile } from 'fs'; import React from 'react'; // 导出 export function hello() {}; export default { // ... };
优缺点
ES6模块虽然是终极模块化方案,但它的缺点在于目前无法直接运行在大部分 JavaScript 运行环境下,必须通过工具转换成标准的 ES5 后才能正常运行。
ES Harmony
未来的模块
TC39,负责讨论ECMAScript语法和语义定义问题和其未来迭代的标准机构,它是由许多的非常聪明的开发者组成的。这些开发者中的一些人(比如Alex Russell)对Javascript在大规模开发中的用例场景在过去几年一直保持者密切的关注,并且敏锐的意识到了人们对于能够使用其编写更加模块化JS的优良的语言特性的需求。 出于这个原因,目前已经有大量激动人心的,包括在客户端和服务器上都能起作用的弹性模块,一个模块加载器以及更多的对语言的改进提议。
更多请参考:w3cschool:ES Harmony
拓展:Sea.js 与 RequireJS 的异同?
RequireJS 和 Sea.js 都是模块加载器,倡导模块化开发理念,核心价值是让 JavaScript 的模块化开发变得简单自然。
两者的主要区别如下:
定位有差异。RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。Sea.js 则专注于 Web 浏览器端,同时通过 Node 扩展的方式可以很方便跑在 Node 环境中。
遵循的规范不同。RequireJS 遵循 AMD(异步模块定义)规范,Sea.js 遵循 CMD (通用模块定义)规范。规范的不同,导致了两者 API 不同。Sea.js 更贴近 CommonJS Modules/1.1 和 Node Modules 规范。
推广理念有差异。RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。Sea.js 不强推,采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。
对开发调试的支持有差异。Sea.js 非常关注代码的开发调试,有 nocache、debug 等用于调试的插件。RequireJS 无这方面的明显支持。
插件机制不同。RequireJS 采取的是在源码中预留接口的形式,插件类型比较单一。Sea.js 采取的是通用事件机制,插件类型更丰富。
总之,如果说 RequireJS 是 Prototype 类库的话,则 Sea.js 致力于成为 jQuery 类库。
参考资料
前端科普系列-CommonJS:不是前端却革命了前端
CommonJS、AMD/CMD、ES6 Modules 以及 webpack 原理浅析
深入浅出webpack:前端的发展
RequireJS 中文网
Sea.js 官网
Sea.js 与 RequireJS 的异同
CMD 模块定义规范
w3cschool:ES Harmony