ES Module 模块化

71 阅读4分钟

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 模块化规范。

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 的语法

  • exportimport 语句中的 {} 并不是对象的字面量,相关语法也不是解构或简写。
  • 暴露出去的内容是只读的,无论是使用 varlet 还是 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 中,requiremoduleexports__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 库。
  • exportimport 的方式由于模块间地址隔离,无法直接用于不同模块之间的数据传递。