工匠若水可能会迟到,但是从来不会缺席,最终还是觉得将自己的云笔记分享出来吧 ~

背景

JavaScript 的模块系统主要是在解决大中型前端应用脚本管理与加载问题。在前端模块化进程中主要经历了下面几种 JavaScript 模块化解决方案:

  • 石器时代
  • IIFE
  • CJS,CommonJS 模块规范
  • AMD,CMD,异步模块定义
  • UMD
  • ESM,ECMAScript 模块系统(ES6)

石器时代

HTML <script>元素用于嵌入或引用可执行脚本。互联网早期,页面比较简单,使用内嵌的方式或者引用单个 JavaScript 文件就可以满足业务需求。如果功能变得复杂,单个 JavaScript 文件行数变得很多,那么可以将 JavaScript 分为多个文件,并且需要小心翼翼地处理各个<script>标签的书写顺序。

这个时期针对 JavaScript 源码的组织,谈不上模块化。即便采用了文件拼接(concat)这样的处理技术,其先后顺序也需要人工维护。典型的例子是 Backbone.js 在官网上提供的代码示例:

<head>
  <title>Backbone.js Todos</title>
  <link rel="stylesheet" href="todos.css"/>
</head>
<body>
  <div>HTML代码...</div>
  <script src="../../test/vendor/json2.js"></script>
  <script src="../../test/vendor/jquery.js"></script>
  <script src="../../test/vendor/underscore.js"></script>
  <script src="../../backbone.js"></script>
  <script src="../backbone.localStorage.js"></script>
  <script src="todos.js"></script>
  <!-- 其他代码 -->
</body>

IIFE

IIFE 其实在大家都用过,该标准简单说就是执行一个立即匿名函数,模块代码放入匿名函数中,隔离变量作用域,内存释放等问题。样例使用如下:

(function(global){
    // code
})(this);

CJS,CommonJS 模块规范

CommonJS 是 nodejs 广泛使用的模块化机制。该规范的主要内容是,模块必须通过 module.exports 导出对外的变量或接口,通过 require() 来导入其他模块的输出到当前模块作用域中。

  • Nodejs 基本上按照 CommonJS 规范实现了模块系统,不过还是略有差异,因此是一种变体。
  • CommonJs 规定每个 js 文件都能被看作是一个模块,其内部定义的变量是私有的,不会对外暴露,不会污染全局。
  • CommonJs 采用同步(一个接着一个)的加载模块,适用于服务器,不适合在浏览器端使用。
  • CommonJs 不被浏览器端支持,因为浏览器没有 module, exports, require, global 四个环境变量。如果要在浏览器使用 CommonJs,需要使用工具转换(browserify)。

CommonJS 主要有以下方式对模块进行导入导出:

module.exports = { a: 12, foo: function(){ console.log(this.a) } }
//导出为一个对象
var obj = require('./m1.js')
obj.a = 34
obj.foo()
module.exports = function(){ return {a: 12} }
//导出是一个函数, 可以直接执行
var foo = require('./m2.js')
foo()
exports.foo = function(){ return {a: 12}}
//导出为一个对象, 对象里面的属性为foo
var obj = require('./m3.js')
obj.foo()

注意:不要使用 import,import 是 ES6 的语法。想要在浏览器环境中使用,需要在 index.js 中将模块导入,然后 node 使用如下命令:

browserify index.js -o bundle.js

前提是安装了 browserify 这个包,然后在 html 里面引入 bundle.js 即可。

AMD,CMD,异步模块定义

AMD

不同于 CommonJs,AMD 是异步加载,名称就是 Asynchronous Module Difinition,允许指定回调函数,等异步加载完成之后即可调用回调函数。AMD 的核心思想就是通过 define 来定义模块,然后通过 require 来加载模块,它依赖于 require.js,在 html 文件里面引入 script:

<script src="require.js" data-main="./index.js"></script>

定义模块 m1

define([模块名称], [依赖模块], function(){
  name: 'vey-module-1',
  getName: function(){
    return name
  }
  return {getName: getName}
})

上面的模块名称定义此模块的名称,不写则默认为文件名去掉后缀。依赖模块指定此模块依赖的模块,类型为数组。此模块的内容定义在 function 里面。

再定义一个模块 m2

define(['m1'], function(m1){
  name: 'vey-module-2',
  function show() {
    console.log(name, m1.getName())
  }
  return { show }
})

定义 index.js

(function(){
  // 配置模块的路径
  require.config({
    paths: {
      m1: './m1'
      m2: './m2'
    }
  })
  require(['m2'], function(m2){
    m2.show()
  })
})()

然后就可以看到执行结果了。

AMD是依赖前置的,即不管你用没用到,只要你设置了依赖就会去加载,不是按需加载的。

上面的代码在浏览器里运行,会先后发生这些事情:

  • 它首先根据指定的require.js加载require.js
  • 加载完成后,require.js会在所有的 script 标签里寻找 data-main 属性指定的路径文件作为下一个将要加载的脚本,在这里,就是./index.js
  • 开始加载./index.js,加载后,解析并执行它,如果发现它有其他依赖,那么先加载所有的依赖(递归)并完成后,再执行./index.js里面 require 的回调函数;

页面就绪后,我们可以在渲染后的 DOM 树中看到<head>标签里多了几个<script>元素,require.js正是通过这些标签来加载相应的 AMD 模块文件的。

CMD

相比 AMD,CMD 也同样是异步加载,AMD 是依赖前置,CMD 是就近加载,按需加载的。主要产物就是sea.js

SeaJS 最初以就近加载以及支持加载 CSS 等特点吸引了很多用户,并提出了 CMD 规范。但在 RequireJS 也支持这些特性之后渐渐淡出,虽然 SeaJS 没有明显的缺点,但统一标准未尝不是一件好事。

sea.js核心思想与 AMD 高度类似,通过 define 定义模块,require 加载模块,如下:

<script src="sea.js"></script>
<script> seajs.use('./index.js') </script>

定义 m1

define(function (require, exports, module) {
    var msg = 'm1'
    function foo() {
        console.log(msg);
    }
    module.exports = {
        foo
    }
})

定义 m2

define(function (require, exports, module) {
    var msg = 'm2'
    function bar() {
        console.log(msg);
    }
    module.exports = bar
})

定义 m3

define(function (require, exports, module) {
    var msg = 'm3'
    function fun() {
        console.log(msg);
    }
    exports.fun = fun
})

定义 m4

define(function (require, exports, module) {
    var msg = 'm4'
    var m2 = require('./m2')
    m2()
    require.async('./m3', function(m3){
        m3.fun()
    })
    function func () {
        console.log(msg)
    }
    exports.func = func
})

定义 index.js

define(function (require, exports, module) {
  var m1 = require('./m1')
  m1.foo()
  var m4 = require('./m4')
  m4.func()
})

UMD

鉴于存在 CommonJS、AMD 等不同的模块系统,为了让代码能够同时支持它们,社区提出了一种统一模块定义(Universal Module Definition,UMD)来解决不兼容的问题。所以,UMD 其实不是什么标准,可以看成是 IIFE amd + cjs 的兼容版。也就是一个 js 文件,可以用 script 标签引用加载,用 RequireJS 加载,也可以在 node 当成 CommonJS 模块加载。

一个常见的 UMD 模块声明实际上是一个立即执行函数表达式。模块的主体在一个工厂方法里面,其返回值作为模块最终暴露的对象。例如下面的模块暴露了一个构造函数 Time:

(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
    typeof define === 'function' && define.amd ? define(factory) :
    global.Time = factory();
}(this, function () {
  // 模块工厂方法开始
  'use strict';
  var _private = '';

  function Time(param) {
    this._date = new Date(param);
  }

  return Time;
  // 模块工厂方法结束
}));

以上原理其实很简单,因为通常如果一个变量在取右值时未定义,会发生引用错误(Reference Error),例如:

// 标识符 an_undefined_token 不在当前作用域链上
console.log(an_undefined_token);

// 会报如下错误
// Uncaught ReferenceError: an_undefined_token is not defined

但是 typeof 运算符有所不同,typeof an_undefined_token 并不会报任何错,而是输出 undefined。利用 JS 的这个运算符,我们可以在脚本加载后立即执行模块头部代码,利用特性检测来判断环境中存在的是哪种模块系统。所以:

  • 如果 exports 是个对象,而且 module 也存在,那么运行工程函数,拿到其返回值,然后像任何一个 NodeJS 模块一样,将返回值赋给 module.exports。
  • 如果 define 是个函数,而且 define 上面存在 amd 属性(RequireJS 的特性,表示这是一个 AMD 加载器),那么使用 define 函数将模块工厂函数加到队列里。
  • 如果上面两个特性检测都失败,就在全局对象上面挂载该模块的返回值。

UMD 模块试图对当前最流行的那些脚本加载器(例如 RequireJS)提供足够好的兼容性。很多情况下,它使用 AMD 为基础,并对特殊情况处理以提供 CommonJS 兼容性。

ESM,ECMAScript 模块系统(ES6)

所谓 ESM,即 ECMAScript2015 Module,也就是 ES6 中的模块化。工作原理是模块文件只加载、执行一次。ES6 自带模块化,从语言的标准上实现了模块化,使用 export 导出,import导入。

let a = 10
let b = 20
let c = 'aaa'
export function myFun(){}
export default foo(){}

export default 默认导出,不管是否导出其他,这个后面跟的东西一定会被导出,而且只能写一个默认导出。

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。现在许多打包工具利用 ES6 的特点, 按需提取代码, 打出的包更加精简,如下:

//按需加载依赖
import { stat, exists, readFile } from 'fs';

//暴露接口
export default obj; // 默认对外接口
export var foo = 'str'; // 名称为`foo`的对外接口

总结

综上可以看见,IIFE 的方便会让其长期存在,而 AMD 在浏览器端还是霸主阶段,在服务端 ES6 标准化取代 CJS 是一种趋势,但 CJS 标准的庞大类库会让它暂时难以取代。

标准变量问题依赖动态加载静态分析
iife
amd
cmd
cjs
es6

以上就是关于模块的所有的总结。

参考文章说明
-------------------------------------------------------------------------
前端模块化标准对比 iife amd cmd cjs umd es6
https://coding.whyoop/2018/08/01/js-modules/

CommonJs & AMD & CMD & ESM
https://www.jianshu/p/906aa802bf98

模块系统概述
https://borninsummer/Practice-in-Front-End-Engineering-and-Components-Development/

【工匠若水 未经允许严禁转载,请尊重作者劳动成果。+微信 yanbo373131686 联系我】

更多推荐

WEB 前端模块化方案完全总结