日期:2021年11月2日
Javascript模块化编程(一):模块的写法——阮一峰
Javascript模块化编程,已经成为一个迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。
但是,Javascript不是一种模块化编程语言,它不支持"类"(class),更遑论"模块"(module)了。(正在制定中的ECMAScript标准第六版,将正式支持"类"和"模块",但还需要很长时间才能投入实用。)
——2012/10/26
JS 模块化编年简史
CommonJS ➡️ NodeJs ➡️ RequireJs(AMD) ➡️ SeaJs(CMD) ➡️ ES6 module
CommonJS、AMD、CMD、ES6区别
| 名称 | CommonJs | AMD | CMD | ES6 Module |
|---|---|---|---|---|
| 全称 | CommonJs | Asynchronous Module Definition | Common Module Definition | ECMAScript 6.0 / 2015 |
| 同步/异步 | 同步 | 异步 | 异步 | |
| 实现实例 | NodeJS | RequireJS | SeaJS(淘宝) | JavaScript |
| 运行环境 | 服务端 | 浏览器 | 浏览器 | 前后端 |
| 特点 | 1. 所有代码都运行在模块作用域,不会污染全局作用域; 2. 模块是同步加载的,即只有加载完成,才能执行后面的操作; 3. 模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓存; 4. CommonJS输出是值的拷贝(即,require返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值)。 | 依赖前置,提前执行。在define方法里传入的依赖模块(数组),会在一开始就下载并执行。 | 依赖就近,延迟执行。只有到require时依赖模块才执行。❓牺牲性能换取开发便利? | (对比CommonJS) 1. CommonJS模块是运行时加载,ES6 Module是编译时输出接口; 2. CommonJS加载的是整个模块,将所有的接口全部加载进来,ES6 Module可以单独加载其中的某个接口; 3. CommonJS输出是值的拷贝,ES6 Module输出的是值的引用,被输出模块的内部的改变会影响引用的改变; 4. CommonJS this指向当前模块,ES6 Module this指向undefined; 5. 目前浏览器对ES6 Module兼容还不太好,我们平时在webpack中使用的export/import,会被打包为exports/require。 |
CommonJS
概述
每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
如果想在多个文件分享变量,必须定义为global对象的属性。
global.warning = true;
上面代码的warning变量,可以被所有文件读取。当然,这样写法是不推荐的。
每个模块内部,
module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
var x = 5;
var addX = function (value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
上面代码通过module.exports输出变量x和函数addX。
require方法用于加载模块。
var example = require('./example.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6
exports变量
为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。
var exports = module.exports;
造成的结果是,在对外输出模块接口时,可以向exports对象添加方法。
exports.area = function (r) {
return Math.PI * r * r;
};
exports.circumference = function (r) {
return 2 * Math.PI * r;
};
注意,不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。
exports = function(x) {console.log(x)};
上面这样的写法是无效的,因为exports不再指向module.exports了。
下面的写法也是无效的。
exports.hello = function() {
return 'hello';
};
module.exports = 'Hello world';
上面代码中,hello函数是无法对外输出的,因为module.exports被重新赋值了。
这意味着,如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出。
module.exports = function (x){ console.log(x);};
如果你觉得,exports与module.exports之间的区别很难分清,一个简单的处理方法,就是放弃使用exports,只使用module.exports。
AMD规范与CommonJS规范的兼容性
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。
AMD规范则是非同步加载模块,允许指定回调函数。
由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。
但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。
AMD规范使用define方法定义模块,下面就是一个例子:
define(['package/lib'], function(lib){
function foo(){
lib.log('hello world!');
}
return {
foo: foo
};
});
AMD规范允许输出的模块兼容CommonJS规范,这时define方法需要写成下面这样:
define(function (require, exports, module){
var someModule = require("someModule");
var anotherModule = require("anotherModule");
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
exports.asplode = function (){
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
};
});
require命令
require的是被导出的值的拷贝。也就是说,一旦导出一个值,模块内部的变化就影响不到这个值 。
AMD (Asynchronous Module Definition)
AMD 规范的实现:
<script src="require.js"></script>
<script src="a.js"></script>
所以在引入 require.js 文件后,再引入的其它文件,都可以使用 define 来定义模块。
define(id?, dependencies?, factory)
id:可选参数,用来定义模块的标识,如果没有提供该参数,就使用 js 文件名(去掉拓展名)对于一个 js 文件只定义了一个模块时,这个参数是可以省略的。
dependencies:可选参数,是一个数组,表示当前模块的依赖,如果没有依赖可以不传。
factory:工厂方法,模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次,返回值便是模块要导出的值。如果是对象,此对象应该为模块的输出值。
模块A可以这么定义:
// a.js
define(function(){
var name = 'morrain'
var age = 18
return {
name,
getAge: () => age
}
})
// b.js
define(['a.js'], function(a){
var name = 'lilei'
var age = 15
console.log(a.name) // 'morrain'
console.log(a.getAge()) // 18
return {
name,
getAge: () => age
}
})
它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在回调函数中,等到加载完成之后,这个回调函数才会运行。
RequireJS 的基本思想是,通过 define 方法,将代码定义为模块。当这个模块被 require 时,它开始加载它依赖的模块,当所有依赖的模块加载完成后,开始执行回调函数,返回值是该模块导出的值。AMD 是 "Asynchronous Module Definition" 的缩写,意思就是"异步模块定义"。
CMD (Common Module Definition)
和 AMD 类似,CMD 是 Sea.js 在推广过程中对模块定义的规范化产出。
Sea.js 是阿里的玉伯写的。它的诞生在 RequireJS 之后,玉伯觉得 AMD 规范是异步的,模块的组织形式不够自然和直观。于是他在追求能像 CommonJS 那样的书写形式。于是就有了 CMD 。
CMD 规范的实现:
<script src="sea.js"></script>
<script src="a.js"></script>
在引入 sea.js 文件后,再引入的其它文件,都可以使用 define 来定义模块。
// 所有模块都通过 define 来定义
define(function(require, exports, module) {
// 通过 require 引入依赖
var a = require('xxx')
var b = require('yyy')
// 通过 exports 对外提供接口
exports.doSomething = ...
// 或者通过 module.exports 提供整个接口
module.exports = ...
})
// a.js
define(function(require, exports, module){
var name = 'morrain'
var age = 18
exports.name = name
exports.getAge = () => age
})
// b.js
define(function(require, exports, module){
var name = 'lilei'
var age = 15
var a = require('a.js')
console.log(a.name) // 'morrain'
console.log(a.getAge()) //18
exports.name = name
exports.getAge = () => age
})
Sea.js 可以像 CommonsJS 那样同步的形式书写模块代码的秘诀在于: 当 b.js 模块被 require 时,b.js 加载后,Sea.js 会扫描 b.js 的代码,找到 require 这个关键字,提取所有的依赖项,然后加载,等到依赖的所有模块加载完成后,执行回调函数,此时再执行到 require('a.js') 这行代码时,a.js 已经加载好在内存中了。
ES6 Module
任何模块化,都必须考虑的两个问题就是导入依赖和导出接口。ES6 Module 也是如此,模块功能主要由两个命令构成:
export和import。export命令用于导出模块的对外接口,import命令用于导入其他模块导出的内容。
具体语法讲解请参考阮一峰老师的教程,示例如下:
// a.js
export const name = 'morrain'
const age = 18
export function getAge () {
return age
}
//等价于
const name = 'morrain'
const age = 18
function getAge (){
return age
}
export {
name,
getAge
}
使用 export 命令定义了模块的对外接口以后,其他 JavaScript 文件就可以通过 import 命令加载这个模块。
// b.js
import { name, getAge } from 'a.js'
export const name = 'lilei'
console.log(name) // 'morrain'
const age = getAge()
console.log(age) // 18
// 等价于
import * as a from 'a.js'
export const name = 'lilei'
console.log(a.name) // 'morrin'
const age = a.getAge()
console.log(age) // 18
除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。
从上面的例子可以看到,使用 import 命令的时候,用户需要知道所要导入的变量名,这有时候比较麻烦,于是 ES6 Module 规定了一种方便的用法,使用 export default 命令,为模块指定默认输出。
// a.js
const name = 'morrain'
const age = 18
function getAge () {
return age
}
export default {
name,
getAge
}
// b.js
import a from 'a.js'
console.log(a.name) // 'morrin'
const age = a.getAge()
console.log(age) // 18
显然,一个模块只能有一个默认输出,因此 export default 命令只能使用一次。同时可以看到,这时 import 命令后面,不需要再使用大括号了。
除了基础的语法外,还有 as 的用法、export 和 import 复合写法、export * from 'a'、import()动态加载 等内容,可以自行学习。
📝❓前面提到的 Node.js 已经默认支持 ES6 Module ,浏览器也已经全面支持 ES6 Module。至于 Node.js 和 浏览器 如何使用 ES6 Module,可以自行学习。
ES6 Module 和 CommonJS 的区别
太长了,不说了 看这里🔎
动态和静态
CommonJS和ES6 Module最本质的区别在于前者对模块依赖的解决是“动态的”,而后者是“静态的”。
这里“动态”的含义是, 模块依赖关系的建立发生在代码运行阶段;而“静态”则是模块依赖关系的建立发生在代码编译阶段。
值拷贝和动态映射
在导入一个模块时,对于CommonJS来说获取的是一份导出值的拷贝;
而在ES6 Module中则是值的动态映射,并且这个映射是只读的。
参考: