前端模块化
模块化的理解
引入
在早期的 JavaScript 开发中存在全局变量污染和依赖管理等问题,在多人开发前端应用时这是一个棘手的问题。
全局变量污染:比如:A 开发的 a.js 文件中创建了一个名字为 name 的全局变量,B开发的 b.js 文件中也有一个 name 全局变量,这样就会导致全局变量命名冲突的问题。
依赖管理:在一个 html 文件中可能会用到多个 <script>
标签引入多个 js 文件,多个文件之间可能会有相互依赖的关系,那么多个 <script>
标签的排列顺序也是一个难以处理的问题。
为了解决这些问题,就需要使用到模块化编程的思想。
什么是模块
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
- 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信
模块化的好处
- 避免命名冲突
- 更好地分离,按需加载
- 高复用性
- 高可维护性
模块化规范
模块化的规范:CommonJS、AMD、CMD、ES6 Modules
CommonJS
概述
Node 应用由模块组成,采用 CommonJS 模块规范。在 CommonJS 模块规范中,每个文件就是一个模块,有自己的作用域,文件中的属性和方法都是私有的,在模块内部可以通过 exports
或 module.exports
把这些变量和方法暴露出来。外部在使用时,需要通过 require
引入这个模块,就可以使用这个模块中的属性和方法了。
特点
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
- 模块加载的顺序,按照其在代码中出现的顺序。
基本语法
- 暴露模块:
module.exports = value(推荐)
或exports.xxx = value
- 引入模块:
require(xxx)
,如果是第三方模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径
实例演示
// lib.js 使用 module.exports 导出模块
var count = 3;
function countIncrement() {
count++;
}
module.exports = {
count: count,
countIncrement: countIncrement
};
// main.js 使用 require 导入模块
const mod = require('./lib');
console.log(mod.count); // 3
mod.countIncrement();
console.log(mod.count); // 3
从上面的例子可以看到:CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
上面代码说明,lib.js
模块加载以后,它的内部变化就影响不到输出的 mod.counter
了。这是因为 mod.counter
是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
// lib.js
var count = 3;
function countIncrement() {
count++;
}
module.exports = {
get count() {
return count;
},
countIncrement: countIncrement
};
上面代码中,输出的 counter
属性实际上是一个取值器函数。现在再执行 main.js
,就可以正确读取内部变量 counter
的变动了。
// main.js
const mod = require('./lib');
console.log(mod.count); // 3
mod.countIncrement();
console.log(mod.count); // 4
ES6 Module
概述
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
ES6 模块不是对象,而是通过 export
命令显式指定输出的代码,再通过 import
命令输入。
基本语法
模块功能主要由两个命令构成:export
和 import
。export
命令用于规定模块的对外接口,export
可以输出变量、函数或类,import
命令用于输入其他模块提供的功能。
实例演示
// profile.js
// 导出方式一
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
// 导出方式二(推荐)
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export { firstName, lastName, year } // 注意:有大括号
// 导出函数
export function multiply(x, y) {
return x * y;
};
// 导出时还可以重命名
function f1(x, y) {
return x + y;
}
export { f1 as add } // 注意:有大括号
// main.js
// 导入,大括号中的变量名必须和导出的接口的名字相同
import { firstName, lastName, year, multiply, add } from './profile.js' // 注意:有大括号
// 导入时也可以重命名
import { lastName as myLastName} from './profile.js'
// 执行所加载的模块,但是不输入任何值
import 'lodash'
使用 export default
命令,为模块提供默认输出。
其他模块加载该模块时,import
命令可以为该函数指定任意名字。需要注意的是,这时import
命令后面,不使用大括号。
// export-default.js
export default function() {
console.log('hello world!');
}
// import-default.js 在导入时可以使用任意名字
import sayHello from './import-default.js'
sayHello();
在加载模块的脚本中,不允许修改接口。下面代码中,脚本加载了变量 a
,对其重新赋值就会报错。
import { a } from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
如果 a
是一个对象,改写 a
的属性是允许的。但是,建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。
AMD
概述
AMD 规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。浏览器端一般采用 AMD 规范,RequireJS 是一个实现了 AMD 规范的工具库。
基本语法
require.config
指定引用路径、define()
定义模块、require()
引用模块
定义暴露模块:
// 定义没有依赖的模块
define(function() {
return 模块
})
// 定义有依赖的模块
define(['module1', 'module2'], function(m1, m2) {
return 模块
})
引入使用模块:
require(['module1', 'module2'], function(m1, m2) {
使用 m1/m2
})
CMD
概述
CMD 是另一种 JS 模块化方案,它与 AMD 很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD 推崇依赖就近、延迟执行。此规范其实是在 Sea.js 推广过程中产生的。
AMD 在定义模块的时候要先声明其依赖的模块,CMD 只要依赖的模块在附近就可以了。AMD 推崇依赖前置,因此,JS 可以及其轻巧地知道某个模块依赖的模块是哪一个,因此可以立即加载那个模块;而 CMD 是就近依赖,它要等到所有的模块变为字符串,解析一遍之后才知道他们之间的依赖关系。
AMD 在依赖模块加载完成后就直接执行依赖模块,依赖模块的执行顺序和我们书写的顺序不一定一致。CMD 在依赖模块加载完成后并不执行,只是下载而已,等到所有的依赖模块都加载好后,进入回调函数逻辑,遇到 require 语句 的时候才执行对应的模块,这样模块的执行顺序就和我们书写的顺序保持一致了。
基本语法
定义暴露模块:
//定义没有依赖的模块
define(function (require, exports, module){
exports.xxx = value
module.exports = value
})
//定义有依赖的模块
define(function (require, exports, module){
//引入依赖模块(同步)
var module2 = require('./module2')
//引入依赖模块(异步)
require.async('./module3', function (m3) {
})
//暴露模块
exports.xxx = value
})
引入使用模块:
define(function (require) {
var m1 = require('./module1')
var m4 = require('./module4')
m1.show()
m4.show()
})
ES6 Module 与 CommonJS 的差异
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
- CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
- ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令
import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import
有点像 Unix 系统的“符号连接”,原始值变了,import
加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
- 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
- 编译时加载: ES6 模块不是对象,而是通过
export
命令显式指定输出的代码,import
时采用静态命令的形式。即在import
时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。模块内部引用的变化,会反应在外部。
CommonJS 加载的是一个对象(即module.exports
属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
- CommonJS 模块的
require()
是同步加载模块,ES6 模块的import
命令是异步加载,有一个独立的模块依赖的解析阶段。
参考资料: