CommonJS 最初是服务于服务端的,所以说 CommonJS 不是前端,但它的载体是前端语言 JavaScript,为后面前端模块化的盛行产生了深远的影响,奠定了结实的基础。CommonJS:不是前端却革命了前端!
前端模块化方案 - CommonJS,AMD,CMD,ES6 - 图1
CommonJS 是一个项目,其目标是为 JavaScript 在网页浏览器之外创建模块约定。创建这个项目的主要原因是当时缺乏普遍可接受形式的 JavaScript 脚本模块单元,模块在与运行JavaScript 脚本的常规网页浏览器所提供的不同的环境下可以重复使用。

前端之所以需要模块化有三个原因:

  1. 模块化可以处理全局变量污染的问题
  2. 模块化可以对数据进行保护
  3. 解决模块之间的依赖关系

Node.js 应用由模块组成,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。

CommonJS 规范还规定,每个模块内部有两个变量可以使用,require 和 module。

require 用来加载某个模块

module 代表当前模块,是一个对象,保存了当前模块的信息。exports 是 module 上的一个属性,保存了当前模块要导出的接口或者变量,使用 require 加载的某个模块获取到的值就是那个模块使用 exports 导出的值**

  1. // a.js
  2. var name = 'morrain'
  3. var age = 18
  4. module.exports.name = name
  5. module.exports.getAge = function(){
  6. return age
  7. }
  8. //b.js
  9. var a = require('a.js')
  10. console.log(a.name) // 'morrain'
  11. console.log(a.getAge())// 18

exports

为了方便,Node.js 在实现 CommonJS 规范时,为每个模块提供一个 exports的私有变量,指向 module.exports。你可以理解为 Node.js 在每个模块开始的地方,添加了如下这行代码。**

  1. var exports = module.exports

于是可以改写上面的代码如下:

  1. // a.js
  2. var name = 'morrain'
  3. var age = 18
  4. exports.name = name
  5. exports.getAge = function(){
  6. return age
  7. }

如果一个模块的对外接口,就是一个单一的值,可以使用 module.exports 导出

  1. // a.js
  2. var name = 'morrain'
  3. var age = 18
  4. module.exports = name

require

require 命令的基本功能是,读入并执行一个 js 文件,然后返回该模块的 exports 对象。如果没有发现指定模块,会报错。

CommonJS 的实现

了解 CommonJS 的规范后,不难发现我们在写符合 CommonJS 规范的模块时,无外乎就是使用了 require 、 exports 、 module 三个东西,然后一个 js 文件就是一个模块。如下所示:

  1. // a.js
  2. var name = 'morrain'
  3. var age = 18
  4. exports.name = name
  5. exports.getAge = function () {
  6. return age
  7. }
  8. // b.js
  9. var a = require('a.js')
  10. console.log('a.name=', a.name)
  11. console.log('a.age=', a.getAge())
  12. var name = 'lilei'
  13. var age = 15
  14. exports.name = name
  15. exports.getAge = function () {
  16. return age
  17. }
  18. // index.js
  19. var b = require('b.js')
  20. console.log('b.name=',b.name)

如果我们向一个立即执行函数提供 require 、 exports 、 module 三个参数,模块代码放在这个立即执行函数里面。模块的导出值放在 module.exports 中,这样就实现了模块的加载。如下所示:

  1. (function(module, exports, require) {
  2. // b.js
  3. var a = require("a.js")
  4. console.log('a.name=', a.name)
  5. console.log('a.age=', a.getAge())
  6. var name = 'lilei'
  7. var age = 15
  8. exports.name = name
  9. exports.getAge = function () {
  10. return age
  11. }
  12. })(module, module.exports, require)

知道这个原理后,就很容易把符合 CommonJS 模块规范的项目代码,转化为浏览器支持的代码。很多工具都是这么实现的,从入口模块开始,把所有依赖的模块都放到各自的函数中,把所有模块打包成一个能在浏览器中运行的 js 文件。譬如 Browserify 、webpack 等等。

我们以 webpack 为例,看看如何实现对 CommonJS 规范的支持。我们使用 webpack 构建时,把各个模块的文件内容按照如下格式打包到一个 js 文件中,因为它是一个立即执行的匿名函数,所以可以在浏览器直接运行。

  1. // bundle.js
  2. (function (modules) {
  3. // 模块管理的实现
  4. })({
  5. 'a.js': function (module, exports, require) {
  6. // a.js 文件内容
  7. },
  8. 'b.js': function (module, exports, require) {
  9. // b.js 文件内容
  10. },
  11. 'index.js': function (module, exports, require) {
  12. // index.js 文件内容
  13. }
  14. })

其他前端模块化得方案

我们对 CommonJS 的规范已经非常熟悉了,require 命令的基本功能是,读入并执行一个 js 文件,然后返回该模块的 exports 对象,这在服务端是可行的,因为服务端加载并执行一个文件的时间消费是可以忽略的,模块的加载是运行时同步加载的,require 命令执行完后,文件就执行完了,并且成功拿到了模块导出的值。

这种规范天生就不适用于浏览器,因为它是同步的。可想而知,浏览器端每加载一个文件,要发网络请求去取,如果网速慢,就非常耗时,浏览器就要一直等 require 返回,就会一直卡在那里,阻塞后面代码的执行,从而阻塞页面渲染,使得页面出现假死状态。

为了解决这个问题,后面发展起来了众多的前端模块化规范,包括 CommonJS 大致有如下几种:
前端模块化方案 - CommonJS,AMD,CMD,ES6 - 图2

AMD (Asynchronous Module Definition)

它解决了 CommonJS 规范不能用于浏览器端的问题,而 AMD 就是 RequireJS 在推广过程中对模块定义的规范化产出。

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

RequireJS 的基本思想是,通过 define 方法,将代码定义为模块。当这个模块被 require 时,它开始加载它依赖的模块,当所有依赖的模块加载完成后,开始执行回调函数,返回值是该模块导出的值。AMD 是 “Asynchronous Module Definition” 的缩写,意思就是”异步模块定义”。

CMD (Common Module Definition)

和 AMD 类似,CMD 是 Sea.js 在推广过程中对模块定义的规范化产出。Sea.js 是阿里的玉伯写的。它的诞生在 RequireJS 之后,玉伯觉得 AMD 规范是异步的,模块的组织形式不够自然和直观。于是他在追求能像 CommonJS 那样的书写形式。于是就有了 CMD 。

  1. // 所有模块都通过 define 来定义
  2. define(function(require, exports, module) {
  3. // 通过 require 引入依赖
  4. var a = require('xxx')
  5. var b = require('yyy')
  6. // 通过 exports 对外提供接口
  7. exports.doSomething = ...
  8. // 或者通过 module.exports 提供整个接口
  9. module.exports = ...
  10. })
  11. // a.js
  12. define(function(require, exports, module){
  13. var name = 'morrain'
  14. var age = 18
  15. exports.name = name
  16. exports.getAge = () => age
  17. })
  18. // b.js
  19. define(function(require, exports, module){
  20. var name = 'lilei'
  21. var age = 15
  22. var a = require('a.js')
  23. console.log(a.name) // 'morrain'
  24. console.log(a.getAge()) //18
  25. exports.name = name
  26. exports.getAge = () => age
  27. })

Sea.js 可以像 CommonsJS 那样同步的形式书写模块代码的秘诀在于:当 b.js 模块被 require 时,b.js 加载后,Sea.js 会扫描 b.js 的代码,找到 require 这个关键字,提取所有的依赖项,然后加载,等到依赖的所有模块加载完成后,执行回调函数,此时再执行到 require(‘a.js’) 这行代码时,a.js 已经加载好在内存中了
**

ES6 Module

前面提到的 CommonJS 是服务于服务端的,而 AMD、CMD 是服务于浏览器端的,但它们都有一个共同点:都在代码运行后才能确定导出的内容,CommonJS 实现中可以看到。

还有一点需要注意,AMD 和 CMD 是社区的开发者们制定的模块加载方案,并不是语言层面的标准。从 ES6 开始,在语言标准的层面上,实现了模块化功能,而且实现得相当简单,完全可以取代 CommonJS 和 CMD、AMD 规范,成为浏览器和服务器通用的模块解决方案。
**

ES6 Module 语法

任何模块化,都必须考虑的两个问题就是导入依赖和导出接口。ES6 Module 也是如此,模块功能主要由两个命令构成:export 和 import。export 命令用于导出模块的对外接口,import 命令用于导入其他模块导出的内容。

  1. // a.js
  2. export const name = 'morrain'
  3. const age = 18
  4. export function getAge () {
  5. return age
  6. }
  7. //等价于
  8. const name = 'morrain'
  9. const age = 18
  10. function getAge (){
  11. return age
  12. }
  13. export {
  14. name,
  15. getAge
  16. }

使用 export 命令定义了模块的对外接口以后,其他 JavaScript 文件就可以通过 import 命令加载这个模块。

  1. // b.js
  2. import { name as aName, getAge } from 'a.js'
  3. export const name = 'lilei'
  4. console.log(aName) // 'morrain'
  5. const age = getAge()
  6. console.log(age) // 18
  7. // 等价于
  8. import * as a from 'a.js'
  9. export const name = 'lilei'
  10. console.log(a.name) // 'morrin'
  11. const age = a.getAge()
  12. console.log(age) // 18

除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。
从上面的例子可以看到,使用 import 命令的时候,用户需要知道所要导入的变量名,这有时候比较麻烦,于是 ES6 Module 规定了一种方便的用法,使用 export default命令,为模块指定默认输出。

  1. // a.js
  2. const name = 'morrain'
  3. const age = 18
  4. function getAge () {
  5. return age
  6. }
  7. export default {
  8. name,
  9. getAge
  10. }
  11. // b.js
  12. import a from 'a.js'
  13. console.log(a.name) // 'morrin'
  14. const age = a.getAge()
  15. console.log(age) // 18

ES6 与CommonJS的区别

CommonJS 只能在运行时确定导出的接口,实际导出的就是一个对象。而 ES6 Module 的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及导入和导出的变量,也就是所谓的”编译时加载”。

正因为如此,import 命令具有提升效果,会提升到整个模块的头部,首先执行。下面的代码是合法的,因为 import 的执行早于 getAge 的调用。

  1. // a.js
  2. export const name = 'morrain'
  3. const age = 18
  4. export function getAge () {
  5. return age
  6. }
  7. // b.js
  8. const age = getAge()
  9. console.log(age) // 18
  10. import { getAge } from 'a.js'

也正因为 ES6 Module 是编译时加载, 所以不能使用表达式和变量,因为这些是只有在运行时才能得到结果的语法结构。