ES Module 模块化知识有些琐碎,最近有点空闲时间,做个整理,方便随时复习。
1. 模块化开发的历史
模块化问题实际上是 JavaScript 这门语言在设计之初没有考虑全面的产物。从历史演进上来看,大概可以分成以下几个阶段:
- file 阶段:在 HTML 中,我们通过单个
<script>
标签引入脚本文件,这是最基本的以文件为单位的模块化。但它存在无法处理各个 script 脚本之间变量污染的问题。 - file-namespace 阶段:对 JS 文件的内容做出了初步规范,一个 JS 文件只导出一个对象,这在一定程度上避免了污染问题,但并未彻底解决。
- IIFE 阶段:使用立即执行函数表达式(IIFE)完成 JS 变量的隔离,同时以函数形参的方式完成各个模块之间的数据传递。这种方式需要开发者自己确保加载顺序,且是同步的,对性能不友好且难以维护。
- 模块化标准+模块化加载器:
- CommonJS 模块化:一个文件就是一个模块,每个模块都有自己的作用域,通过
module.exports
导出成员,require
导入模块。CommonJS 规范在 Node.js 中应用广泛,但不适用于浏览器环境,因为它是一种同步模块的加载方式。 - AMD 模块规范:Asynchronous Module Definition,依赖第三方库如 Require.js 实现。但它存在语法复杂、JS 文件请求频繁的问题。
- ES Module 规范:ES 6 以后,浏览器在语法层面实现了 ES Module 规范。至此,在浏览器中使用 ES Module 规范,在 Node 环境下使用 CommonJS 规范。随着 ES Module 的普及,Node(8.5 版本及以后)也开始推崇使用 ES Module 模块化规范。
- CommonJS 模块化:一个文件就是一个模块,每个模块都有自己的作用域,通过
2. ES Module 的特点
-
实现 ES Module 的方式简单,只需使用
<script type="module">
标签。对于不支持 ES Module 的浏览器,可以使用如下方法加载补丁:<script type="nomodule" src="https://unpkg.com/promise-polyfill/dist/polyfill.min.js"></script> <script type="nomodule" src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script> <script type="nomodule" src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-es-loader.js"></script>
其中
nomodule
表示在不支持 ES Module 的浏览器中才会执行,以防止重复加载补丁。补丁使用 Babel 对 JS 代码进行编译。 -
ES Module 的特点包括:自动严格模式、每个模块都有私有作用域、脚本异步加载、ES Module 请求的脚本需要服务器支持 CORS。
3. 快速调试技巧
使用 BrowserSync 进行实时调试:
browser-sync . --files **/*.js
4. ES Module 的使用规范
4.1 default 的导入导出
// 导出
export { name as default, hello as fooHello };
// 导入
import { default as fooName } from './module.js';
4.2 export 和 import 的语法
export
和import
语句中的{}
并不是对象的字面量,相关语法也不是解构或简写。- 暴露出去的内容是只读的,无论是使用
var
、let
还是const
定义的。
4.3 导入的四种形态
import { name } from 'module.js'; // 第三方库
import { name } from './module.js'; // 相对路径
import { name } from '/module.js'; // 绝对路径
import { name } from 'http://localhost:9000/module.js'; // 网络地址
4.4 默认导出和定点导出联合使用
// 导出
export const name;
export const age;
export default title;
// 导入
import { name, age, default as title } from './module.js';
import title, { name, age } from './module.js'; // 等价于上面的导入方式
4.5 直接导出
export { default as Button } from './button.js';
export * from './button.js';
export { a } from './button.js';
5. 在 Node 环境下同时使用两种规范
5.1 文件扩展名
- 最简单的方式是将 JS 文件改成
.mjs
(ES Module)或.cjs
(CommonJS)。
5.2 兼容性
- ES Module 可以导入 CommonJS 模块,但反过来不可以。
- CommonJS 始终只导出一个默认成员,使用解构赋值时才是对象的解构;而 ES Module 中的
import
语句的花括号不是对象的解构。
5.3 Node.js 中的特殊变量
-
在 CommonJS 中,
require
、module
、exports
、__filename
、__dirname
可以直接使用。 -
在 ES Module 中,需要使用特殊方式来获取这些变量:
import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename);
6. 解决 CommonJS 无法导入 ES Module 的问题
- 通过构建工具和 Babel 转换来解决。
- 示例项目结构和配置:
-
创建项目并初始化:
mkdir test && cd $_ && npm init -y && touch index.cjs && touch esm.mjs
-
index.cjs
内容:const { a } = require('./esm.mjs'); console.log('a: ', a);
-
esm.mjs
内容:export const a = 20;
-
安装 Babel 相关插件并配置
package.json
:{ "scripts": { "start": "babel-node ./index.cjs" }, "babel": { "presets": ["@babel/preset-env"] } }
-
执行
npm start
即可正常输出。
-
7. 同一份 HTML 文件中不同 Module 之间的数据传递
- 解决方案一般是通过全局的
window
对象传递数据,或者使用自定义事件,或者使用第三方库如 PubSub 库。 export
和import
的方式由于模块间地址隔离,无法直接用于不同模块之间的数据传递。