# 模块化

在 ES 6 模块规范出现之前,虽然浏览器原生不支持模块的行为,但迫切需要这样的行为。ES 同样不支持模块,因此希望使用模块模式的库或代码库必须基于 JavaScript 的语法和词法特性“伪造”出类似模块的行为。

# 1、理解模块

将代码拆分成独立的块,然后再把这些块连接起来可以通过模块模式来实现。这种模式背后的思想很简单:把逻辑分块,各自封装,相互独立,每个块自行决定对外暴露什么,同时自行决定引入执行哪些外部代码。不同的实现和特性让这些基本的概念变得有点复杂,但这个基本的思想是所有 JavaScript 模块系统的基础。

# 1.1 函数封装

函数一个功能就是实现特定逻辑的一组语句打包,而且JavaScript的作用域就是基于函数的,所以把函数作为模块化的第一步是很自然的事情,在一个文件里面编写几个相关函数就是最开始的模块了。

function fn1() {
  statement
}

function fn2() {
  statement
}
1
2
3
4
5
6
7

这样在需要的地方调用相关的函数就可以了。

但是这样做有明显的缺陷:污染了全局变量,而且无法保证不与其他模块发生命名冲突,而且模块成员之间也没有什么关系。

# 1.2 对象封装

为了解决上面的问题,对象封装的写法应运而生,可以将有关系的函数封装到一个对象中。

const myModule = {
  var1: 1,
  var2: 2,
  fn1: function () {
    statement
  },
  fn2: function () {
    statement
  }
}
1
2
3
4
5
6
7
8
9
10

这样的话,我们在需要调用的时候引入对应的模块,然后 myModule.fn1();这样避免了全局污染,只要保证模块名称唯一,而且统一模块间的成员也有了关系。但是也有缺陷,因为我们可以随意修改模块内的变量,例如:myModule.var1 = 2;这样可能会产生意外的问题。

# 1.3 立即执行函数

立即执行函数(IIFE,Immediately Invoked Function Expression)将模块定义封装在匿名闭包中。模块定义是立即执行的,如下:

const myModule = (function() {
  var var1 = 1;
  var var2 = 2;

  function fn1 () {
    statement
  }

  function fn2 () {
    statement
  }

  return {
    fn1: fn1,
    fn2: fn2
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 2、ES6 之前的模块加载器

# 2.1 CommonJs

CommonJS 规范概述了同步声明依赖的模块定义。这个规范主要用于在服务器端实现模块化代码组织,但也可用于定义在浏览器中使用的模块依赖。CommonJS 模块语法不能在浏览器中直接运行。

一般认为,Node.js 的模块系统使用了 CommonJS 规范,实际上并不完全正确。Node.js 使用了轻微修改版本的 CommonJS,因为 Node.js 主要在服务器环境下使用,所以不需要考虑网络延迟问题。考虑到一致性,本节使用 Node.js 风格的模块定义语法。

Node.js 是 commonJS 规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。实际使用时,用 module.exports 定义当前模块对外输出的接口(不推荐直接用exports),用 require 加载模块。

// 定义math.js
function add(a, b) {
  return a + b;
}
module.exports = {
  add: add
}

// index.js
const math = require('./math');
console.log(math.add(2, 3)); // 5
1
2
3
4
5
6
7
8
9
10
11

在输入时先加载整个模块,生成一个对象,然后再从这个对象上面读取方法。

  • CommonsJs 是运行时加载
  • CommonJs 的拷贝是浅拷贝

# 2.2 AMD

CommonJS 以服务器端为目标环境,能够一次性把所有模块都加载到内存,而异步模块定义(AMD,Asynchronous Module Definition)的模块定义系统则以浏览器为目标执行环境,这需要考虑网络延迟的问题。AMD 的一般策略是让模块声明自己的依赖,而运行在浏览器中的模块系统会按需获取依赖,并在依赖加载完成后立即执行依赖它们的模块。

AMD 模块实现的核心是用函数包装模块定义。这样可以防止声明全局变量,并允许加载器库控制何时加载模块。包装函数也便于模块代码的移植,因为包装函数内部的所有模块代码使用的都是原生 JavaScript 结构。包装模块的函数是全局 define 的参数,它是由 AMD 加载器库的实现定义的。包装模块的函数是全局 define 的参数,它是由 AMD 加载器库的实现定义的。

AMD 模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

// ID 为'moduleA'的模块定义。moduleA 依赖 moduleB,
// moduleB 会异步加载
define('moduleA', ['moduleB'], function(moduleB) { 
  return { 
    stuff: moduleB.doStuff(); 
  }; 
}); 
1
2
3
4
5
6
7

# 2.3 UMD

为了统一 CommonJS 和 AMD 生态系统,通用模块定义(UMD,Universal Module Definition)规范应运而生。UMD 可用于创建这两个系统都可以使用的模块代码。本质上,UMD 定义的模块会在启动时检测要使用哪个模块系统,然后进行适当配置,并把所有逻辑包装在一个立即调用的函数表达式(IIFE)中。

下面是只包含一个依赖的 UMD 模块定义的示例:

(function (root, factory) { 
  if (typeof define === 'function' && define.amd) { 
    // AMD。注册为匿名模块
    define(['moduleB'], factory); 
  } else if (typeof module === 'object' && module.exports) { 
    // Node。不支持严格 CommonJS 
    // 但可以在 Node 这样支持 module.exports 的
    // 类 CommonJS 环境下使用
    module.exports = factory(require(' moduleB ')); 
  } else { 
    // 浏览器全局上下文(root 是 window)
    root.returnExports = factory(root. moduleB); 
  } 
  }(this, function (moduleB) { 
    // 以某种方式使用 moduleB 
    // 将返回值作为模块的导出
    // 这个例子返回了一个对象
    // 但是模块也可以返回函数作为导出值
    return {}; 
})); 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

此模式有支持严格 CommonJS 和浏览器全局上下文的变体。不应该期望手写这个包装函数,它应该由构建工具自动生成。开发者只需专注于模块的内由容,而不必关心这些样板代码。

# 2.4 CMD

CMD 即Common Module Definition通用模块定义。

CMD 是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置提前执行,CMD推崇依赖就近延迟执行

/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
  // 等于在最前面声明并初始化了要用到的所有模块
  a.doSomething();
  if (false) {
  // 即便没用到某个模块 b,但 b 还是提前执行了
    b.doSomething()
  }
});

/** CMD写法 **/
define(function(require, exports, module) {
  var a = require('./a'); //在需要时申明
    a.doSomething();
  if (false) {
    var b = require('./b');
    b.doSomething();
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 3、ES6 模块

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export 和 import。export 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。

/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
  return a + b;
};
export { basicNum, add };

/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
  ele.textContent = add(99 + basicNum);
}
1
2
3
4
5
6
7
8
9
10
11
12

# 4、区别

# 4.1 AMD和CMD的区别

最明显的区别就是在模块定义时对依赖的处理不同。

  • 1、AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块
  • 2、CMD推崇就近依赖,只有在用到某个模块的时候再去require

这种区别各有优劣,只是语法上的差距。AMD 和 CMD 最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同。

# 4.2 ES6 模块与 CommonJS 模块的差异

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值(只是浅拷贝)。
  • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的 import 有点像 Unix 系统的“符号连接”,原始值变了,import 加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
  1. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
  • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import 时采用静态命令的形式。即在 import 时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成

# 5、工作者模块

ES 6 模块与 Worker 实例完全兼容。在实例化时,可以给工作者传入一个指向模块文件的路径,与传入常规脚本文件一样。Worker 构造函数接收第二个参数,用于说明传入的是模块文件。

// 第二个参数默认为{ type: 'classic' } 
const scriptWorker = new Worker('scriptWorker.js'); 
const moduleWorker = new Worker('moduleWorker.js', { type: 'module' });
1
2
3
上次更新: 3/9/2022, 6:56:39 PM