前端模块化
# CommonJS
由社区提出的前端模块化方案,目前主要用于服务端。通过exports module.exports
和require
实现模块的导出和导入。例子:
// module-a.js
var data = "hello world";
function getData() {
return data;
}
module.exports = {
getData,
};
// index.js
const { getData } = require("./module-a.js");
console.log(getData());
CommonJS是同步加载导入的模块的,这与其大部分用在服务端的场景相符合,因为资源大多都存储在服务端,加载时不会产生大量的网络IO。如果用在客户端,模块的加载很可能导致浏览器解析JS阻塞,影响页面渲染速度。
# AMD/CMD
AMD是一种异步加载模块的方案,CMD也是同时期出现的另一个方案,目前requireJS对AMD和CMD进行了实现。例子:
// main.js 导入模块 首先声明要导入的模块,然后定义当前模块
define(["./print"], function (printModule) {
printModule.print("main");
});
// print.js 模块导出了一个print函数
define(function () {
//返回一个对象,对象中包含模块需要导出的内容
return {
print: function (msg) {
console.log("print " + msg);
},
};
});
UMD可能经常和上面两个方案一起被提及,不过它严格来说并不算一种新的模块化规范。它是一种兼容AMD和CommonJS的模块化方案,能够同时支持在Node和浏览器环境运行。
# ES Module
ECMAScript官方提供的模块化规范。
//foo.js
const name = 'Jack'
const age = 20
const sayHello = function (){
console.log('hello world')
}
//或者可以直接在定义标识符的位置导出
export {
name,
age,
sayHello
}
//main.js
import {name, age, sayHello} from "./foo.js"; //这里的.js后缀不能省略,否则会找不到文件
console.log(name)
console.log(age)
sayHello()
ES Module也是采用异步的方式加载模块,且import
语句必须写在文件的最前面。
# ES Module和CommonJS的区别
- CommonJS基于运行时同步加载模块,ES Module则是基于编译时异步加载模块。
- ES Module在编译时就能知道具体的依赖模块,因此支持Tree Shaking。CommonJS则必须在运行时才能知道模块依赖。
- CommonJS输出值的拷贝,ES Module输出值的只读引用。
- CommonJS中的顶层this指向模块本身,而ESM中的this输出为undefined。
第一点和第二点是相互关联的。因为CommonJS引入模块的逻辑可以写在条件判断语句中的:
// 这样的写法是支持的
if (true) {
require('xxx')
}
这样写法只有在代码运行时才能知晓是否要加载模块,也即所谓的运行时。而ESM的模块加载逻辑必须写在模块的最前面,按照上面的写法会报错,这种处于最顶层的方式让代码在被编译的时候就知道需要加载哪些模块了,也就是所谓的编译时。Tree Shaking是用于擦除死代码的优化,ESM在编译时就能知道哪些模块没有被使用所以支持;而CommonJS必须在运行的时候才能知道模块是否被使用,所以不能提前进行Tree Shaking否则可能导致代码运行报错。
最后这一点解释一下:CommonJS规范模块导出一个值,另一个模块导入这个值,如果导出模块的值发生变化,不会影响到导入这个值的模块。ESM模块则是动态读取,导出值的模块中值发生变化,会影响导入模块中的值。导入模块中的值引用是只读的,不能进行引用的修改。