一、JS 中的模块化规范
要明白我们的打包工具究竟做了什么,首先必须明白的一点就是 JS 中的模块化。
在 ES6 规范之前,我们有 CommonJS、AMD 等主流的模块化规范。
1、CommonJS(node.js 原生 & 同步加载 & 多次加载一次执行)
Node.js 是一个基于 V8 引擎,事件驱动 I/O 的服务端 JS 运行环境。
在 2009 年刚推出时,它就实现了一套名为 CommonJS 的模块化规范。
在 CommonJS 规范里:
- 每个 JS 文件都是一个模块(module),每个模块内部都可以使用
require
函数和module.exports
对象,来对模块进行导入和导出。
示例代码:
javascript
// index.js
require('./moduleA');
const str = require('./moduleB');
console.log('index', str);
// moduleA.js
const timestamp = require('./moduleB');
setTimeout(() => console.log('moduleA', timestamp), 1000);
// moduleB.js
module.exports = new Date().getTime();
执行说明:
index.js
代表的模块通过执行require
函数,分别加载了相对路径为./moduleA
和./moduleB
的两个模块,同时输出moduleB
模块的结果。moduleA.js
文件内也通过require
函数加载了moduleB.js
模块,在 1s 后也输出了加载进来的结果。moduleB.js
文件内部定义了一个时间戳,使用module.exports
对象导出。
运行结果(执行 node index.js
):
diff
index 1738743973675
moduleA 1738743973675
模块特性:
- 每个模块都是单例的,当一个模块被多次加载的时候,最终执行实际上只会被执行一次。
2、AMD(浏览器端 & 异步 & 多次加载一次执行)
另一个为 WEB 开发者所熟知的 JS 运行环境就是浏览器了。浏览器并没有提供像 Node.js 里一样的 require
方法。
不过,受到 CommonJS 模块化规范的启发,WEB 端还是逐渐发展起来了 AMD、SystemJS 规范等适合浏览器端运行的 JS 模块化开发规范。
AMD 全称 Asynchronous module definition,意为异步的模块定义,不同于 CommonJS 规范的同步加载,AMD 正如其名所有模块默认都是异步加载,这也是早期为了满足 web 开发的需要,因为如果在 web 端也使用同步加载,那么页面在解析脚本文件的过程中可能使页面暂停响应。
示例代码:
javascript
// index.js
require(['moduleA', 'moduleB'], function(moduleA, moduleB) {
console.log('index', moduleB);
});
// moduleA.js
define(function(require) {
const timestamp = require('moduleB');
setTimeout(() => console.log('moduleA', timestamp), 1000);
});
// moduleB.js
define(function(require) {
return new Date().getTime();
});
使用说明 :
如果想要使用 AMD 规范,我们还需要添加一个符合 AMD 规范的加载器脚本在页面中,符合 AMD 规范实现的库很多,比较有名的就是 require.js
。
3、ESModule
前面我们说到的 CommonJS 规范和 AMD 规范有这么几个特点:
- 语言上层的运行环境实现的模块化规范,模块化规范由环境自己定义。
- 相互之间不能兼容使用。例如不能在 Node.js 运行 AMD 模块,不能直接在浏览器运行 CommonJS 模块。
在 EcmaScript 2015(也就是我们常说的 ES6)之后,JS 有了语言层面的模块化导入导出语法以及与之匹配的 ESModule 规范。
使用 ESModule 规范,我们可以通过 import
和 export
两个关键词来对模块进行导入与导出。
示例代码(基于 ESModule 规范改写之前的例子):
javascript
// index.js
import './moduleA';
import str from './moduleB';
console.log(str);
// moduleA.js
import timestamp from './moduleB';
setTimeout(() => console.log('moduleA', timestamp), 1000);
// moduleB.js
export default new Date().getTime();
Webpack 配置(webpack.config.js
):
javascript
const path = require('path');
module.exports = {
mode: 'none',
entry: './index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.bundle.js', // 注意 filename 是小写不是 fileNane
publicPath: 'dist/'
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader', // 所有的 js 文件进 webpack 处理的过程中都使用 babel 进行编译
options: {
presets: ['@babel/preset-env'] // 支持使用环境参数来处理不同的内容
}
}
}
]
}
};
依赖安装与运行:
bash
# 安装依赖
yarn add webpack webpack-cli @babel/core babel-loader @babel/preset-env
# 编译打包
./node_modules/.bin/webpack
# 启动静态服务器(可使用 Python 简单启动)
python3 -m http.server
关于 JS 运行环境与解析器
每个 JS 的运行环境都有一个解析器,否则这个环境也不会认识 JS 语法。
它的作用就是用 ECMAScript 的规范去解释 JS 语法,也就是处理和执行语言本身的内容。
例如按照逻辑正确执行 var a = "123"; function func() { console.log("hahaha"); }
之类的内容。
在解析器的上层,每个运行环境都会在解释器的基础上封装一些环境相关的 API。
例如 Node.js 中的 global
对象、process
对象,浏览器中的 window
对象、document
对象等等。
这些运行环境的 API 规范各自规范的影响,例如浏览器端的 W3C 规范,它们规定了 window
对象和 document
对象上的 API 内容,以使得我们能让 document.getElementById
这样的 API 在所有浏览器上运行正常。
以上就是对 JS 模块化相关规范(CommonJS、AMD、ESModule )以及 JS 运行环境、解析器等相关内容的梳理 ,通过不同规范的对比,能更清晰理解在不同场景(Node.js 服务端、浏览器端等 )下模块化的实现与应用 。