第七章:模块-灵析社区

懒人学前端

谈谈模块化的发展历程

随着我们的应用越来越大,我们想要将其拆分成多个文件,即所谓的“模块”(module)。一个模块可以包含用于特定目的的类或函数库。

模块化的定义

一个模块(module)就是一个文件。一个脚本就是一个模块。

模块可以相互加载,并可以使用特殊的指令 export import 来交换功能,从另一个模块调用一个模块的函数:

  • export 关键字标记了可以从当前模块外部访问的变量和函数。
  • import 关键字允许从其他模块导入功能。

模块化的历程

大致可以分为三个阶段:

  • 早期模块化方案
  • 规范标准时代
  • ES6 模块

早期模块化方案

命名空间

对象可以有属性,对象的属性通过对象名字来访问,相当于设定了一个命名空间。

var myModule = {
	name: "Joe",
	getName: function () {
		console.log(this.name); 
	}
};

myModule.getName(); // "Joe"

然而缺点是,对象内部属性全部会暴露出来,内部状态可以被外部更改,如下:

var myModule = {
	name: "Joe",
	getName: function () {
		console.log(this.name); 
	}
};
myModule.name = "Lucy";
myModule.getName(); // "Lucy"

立即执行函数 IIFE

立即执行函数(简称 IIFE)是利用函数闭包的特性来实现私有数据和共享方法,如下:

var myModule = (function () {
	var name = "Joe";
	function getName() {
		console.log(name);  
	}

	return { getName };
})();

myModule.name = "哈哈哈";
myModule.getName(); // "Joe"

我们可以看到,这种方法很好实现了数据的封装和私有化。如果依赖另外一个模块呢?我们只需要将其他模块作为参数传入:

// otherModule.js 模块文件
var otherModule = (function () {
	return {
		a: 1,
		b: 2
	};
})();

// myModule.js模块文件 - 依赖 otherModule 模块
var myModule = (function (other) {
	var name = "Joe";

	function getName() {
		console.log(name);
		console.log(other.a, other.b);
	}

	return { getName };
})(otherModule);

myModule.name = "哈哈哈";
myModule.getName(); // 依次输出:"Joe" 1 2

规范标准时代

主要有四种规范:

  • CommonJS —— 为 Node.js 创建的模块系统。
  • AMD —— 最古老的模块系统之一,最初由 require.js 库实现。
  • CMD —— 它汲取了 CommonJSAMD 规范的优点,也是专门用于浏览器的异步模块加载。
  • UMD —— 另外一个模块系统,建议作为通用的模块系统,它与 AMD CommonJS 都兼容。

CommonJS

Node.js 中,每一个文件就是一个模块,具有单独的作用域,对其他文件是不可见的。关于 CommonJS 的规范有几个需要注意的特点:

  • 文件即模块,文件内所有代码都运行在独立的作用域,因此不会污染全局空间。
  • 模块可以被多次引用、加载。在第一次被加载时,会被缓存,之后都从缓存中直接读取结果。
  • 加载某个模块,就是引入该模块的 module.exports 属性。
  • module.exports 属性输出的是值的拷贝,一旦这个值被输出,模块内再发生变化不会影响到输出的值。
  • 模块加载顺序按照代码引入的顺序。
  • 注意 module.exports exports 的区别

CommonJS 规范用代码如何在浏览器端实现呢?其实就是实现 module.exportsrequire 方法。

实现思路:根据 require 的文件路径,加载文件内容并执行,同时将对外接口进行缓存。因此我们需要定义module.exports 后,借助立即执行函数,将 modulemodule.exports 对象进行赋值:

let module = {}
module.exports = {}

(function(module, exports) {
    // ...  
}(module, module.exports))

AMD

Asynchronous module definition (AMD) 是专门为浏览器环境设计的,它定义了一套异步加载标准来解决同步的问题。它规定了如何定义模块,如何对外输出,如何引入依赖。这一切都需要代码去实现,因此一个著名的库 —— require.js 应运而生,require.js 实现很简单:通过 define 方法,将代码定义为模块;通过 require 方法,实现代码的模块加载。

CMD

CMD 规范整合了 CommonJS AMD 规范的特点。它的全称为:Common Module Definition,类似 require.js,

CMD 规范的实现为 sea.js。

AMDCMD 的两个主要区别如下。

  • AMD 需要异步加载模块,而 CMD require 依赖的时候,可以通过同步的形式(require),也可以通过异步的形式(require.async)。
  • CMD 遵循依赖就近原则,AMD 遵循依赖前置原则。也就是说,在 AMD 中,我们需要把模块所需要的依赖都提前在依赖数组中声明。而在 CMD 中,我们只需要在具体代码逻辑内,使用依赖前,把依赖的模块 require 进来。

具体到代码实现,sea.js 与 require.js 并没有本质差别,这里不再另做分析。

UMD

UMD(Universal Module Definition),即通用模块定义。它随着大前端的趋势所诞生,可以通过运行时或者编译时让同一个代码模块在使用 CommonJSCMD 甚至是 AMD 的项目中运行,也就是说同一个 JavaScript 包运行在浏览器端、服务区端甚至是 APP 端都只需要遵守同一个写法就行了,那它是怎样实现的呢?

我们来看看这样一段代码:

((root, factory) => {
	if (typeof define === "function" && define.amd) {
		// AMD
		define(factory);
	} else if (typeof exports === "object") {
		// CommonJS
		module.exports = factory();
	} else if (typeof define === "function" && define.cmd) {
		// CMD
		define(function (require, exports, module) {
			module.exports = factory();
		});
	} else {
		// 都不是
		root.umdModule = factory();
	}
})(this, () => {
	console.log("我是UMD"); // "我是UMD"
	// todo...
});

可以看到,define AMD/CMD 语法,而 exports 只在 CommonJS 中存在,你会发现它在定义模块的时候会检测当前使用环境和模块的定义方式,如果匹配就使用其规范语法,全部不匹配则挂载在全局对象上,我们看到传入的是一个 this,它在浏览器中指的就是 window,在服务端环境中指的就是 global,使用这样的方式将各种模块化定义都兼容。

ES6 模块

ES6 模块(或称为 ESM),ES6 模块不是对象,import 命令被 JavaScript 引擎静态分析,在编译的时候就引入模块代码。而不是在代码运行时加载,所以无法实现条件加载。也就使得静态分析成为可能。

export

export 可以导出对象中多个属性、方法,export default 只能导出一个可以不具名的函数。我们可以使用 import 引入。

import

import { fn } from './xxx' // export 导出的方式
import fn from 'xx' // export default 方式

ES6 模块运行机制与 CommonJS 运行机制不一样。JavaScript 引擎对脚本静态分析的时候,遇到模块加载指令后会生成一个只读引用。等到脚本真正执行的时候。才会通过引用模块获取值,在引用到执行的过程中,模块中的值发生变化,导入的这里也会跟着发生变化。ES6 模块是动态引入的。并不会缓存值。模块里总是绑定其所在的模块。

小练习

ES6 模块有什么特点?

答案:见上文。

阅读量:1967

点赞量:0

收藏量:0