ES6 模块的加载机制在浏览器和 Node.js 中有不同的实现方式,理解这些差异对于正确使用模块至关重要。
浏览器中的模块#
script type=“module”#
<!-- 声明为模块脚本 --><script type="module"> import { greet } from './greet.js' greet('World')</script>
<!-- 外部模块文件 --><script type="module" src="./app.js"></script>模块脚本的特性#
<!-- 1. 自动 defer,不阻塞 HTML 解析 --><script type="module" src="./app.js"></script><!-- 等价于 --><script defer src="./app.js"></script>
<!-- 2. 多次引入只执行一次 --><script type="module" src="./app.js"></script><script type="module" src="./app.js"></script><!-- 只执行一次 -->
<!-- 3. 支持 async 实现立即执行 --><script type="module" async src="./analytics.js"></script>内联模块#
<script type="module"> // 内联模块也可以使用 import import { utils } from './utils.js'
// 但不能被其他模块导入 // 因为没有 URL</script>模块作用域#
<script type="module"> const privateVar = '模块内部' // 不会污染全局</script>
<script type="module"> console.log(typeof privateVar) // undefined</script>
<!-- 普通脚本会污染全局 --><script> var globalVar = '全局变量'</script><script> console.log(globalVar) // '全局变量'</script>CORS 限制#
<!-- 跨域模块需要服务器支持 CORS --><script type="module" src="https://other-domain.com/module.js"></script><!-- 需要响应头: Access-Control-Allow-Origin: * -->
<!-- 本地文件不能直接使用模块 --><!-- file:// 协议下 import 会失败 --><!-- 需要使用本地服务器 -->nomodule 回退#
<!-- 现代浏览器执行 module --><script type="module" src="./app.js"></script>
<!-- 旧浏览器执行 nomodule --><script nomodule src="./app-legacy.js"></script>Node.js 中的模块#
启用 ES 模块#
// 方式1:使用 .mjs 扩展名import { readFile } from 'fs/promises';
// 方式2:package.json 设置 type// package.json{ "type": "module"}
// 然后 .js 文件就是 ES 模块// app.jsimport { readFile } from 'fs/promises';.mjs 和 .cjs#
// .mjs - ES 模块import fs from 'fs'export const data = 'ES Module'
// .cjs - CommonJS 模块const fs = require('fs')module.exports = { data: 'CommonJS' }package.json type 字段#
{ "type": "module"}| type 值 | .js 文件 | .mjs 文件 | .cjs 文件 |
|---|---|---|---|
| ”module” | ES 模块 | ES 模块 | CommonJS |
| ”commonjs” 或省略 | CommonJS | ES 模块 | CommonJS |
ES 模块 vs CommonJS#
语法差异#
// ES 模块import fs from 'fs'import { readFile } from 'fs'export const name = 'ES'export default function () {}
// CommonJSconst fs = require('fs')const { readFile } = require('fs')exports.name = 'CJS'module.exports = function () {}加载时机#
// CommonJS - 运行时加载const module = condition ? require('./a') : require('./b')
// ES 模块 - 编译时确定(静态 import)// import module from condition ? './a' : './b'; // 语法错误
// ES 模块动态导入const module = await import(condition ? './a.js' : './b.js')导出的是引用还是值#
// counter.js (ES 模块)export let count = 0export function increment() { count++}
// main.jsimport { count, increment } from './counter.js'console.log(count) // 0increment()console.log(count) // 1(引用,值更新了)
// counter.cjs (CommonJS)let count = 0module.exports = { count, increment() { count++ },}
// main.cjsconst { count, increment } = require('./counter.cjs')console.log(count) // 0increment()console.log(count) // 0(值的复制,不会更新)this 的值#
// ES 模块顶层console.log(this) // undefined
// CommonJS 顶层console.log(this) // module.exports(初始为 {})互操作性#
ES 模块导入 CommonJS#
module.exports = { name: 'Library', greet() { return 'Hello' },}
// main.mjs// 默认导入获取整个 module.exportsimport lib from './lib.cjs'console.log(lib.name) // 'Library'console.log(lib.greet()) // 'Hello'
// 命名导入可能不可用(取决于 Node 版本)// import { name } from './lib.cjs'; // 可能报错CommonJS 导入 ES 模块#
export const name = 'ES Module'export default function greet() { return 'Hello'}
// main.cjs// CommonJS 不能直接 require ES 模块// const lib = require('./lib.mjs'); // 错误
// 需要使用动态 importasync function main() { const lib = await import('./lib.mjs') console.log(lib.name) // 'ES Module' console.log(lib.default()) // 'Hello'}main()创建双模式包#
{ "name": "my-package", "type": "module", "main": "./dist/index.cjs", // CommonJS 入口 "module": "./dist/index.js", // ES 模块入口(打包工具使用) "exports": { ".": { "import": "./dist/index.js", "require": "./dist/index.cjs" } }}条件导出#
exports 字段#
{ "name": "my-package", "exports": { ".": { "import": "./dist/index.mjs", "require": "./dist/index.cjs", "types": "./dist/index.d.ts" }, "./utils": { "import": "./dist/utils.mjs", "require": "./dist/utils.cjs" } }}使用#
// ES 模块import pkg from 'my-package' // 加载 ./dist/index.mjsimport utils from 'my-package/utils' // 加载 ./dist/utils.mjs
// CommonJSconst pkg = require('my-package') // 加载 ./dist/index.cjsconst utils = require('my-package/utils') // 加载 ./dist/utils.cjs条件导出优先级#
{ "exports": { ".": { "node": { "import": "./dist/node.mjs", "require": "./dist/node.cjs" }, "browser": "./dist/browser.js", "default": "./dist/index.js" } }}模块解析#
Node.js 解析算法#
// 相对路径import './foo.js' // 必须带扩展名import './foo/index.js'
// 包名import 'lodash' // node_modules/lodash
// 包的子路径import 'lodash/fp' // 受 exports 字段限制扩展名必需#
// CommonJS 可以省略扩展名const foo = require('./foo') // 自动尝试 .js, .json, .node
// ES 模块必须带扩展名import foo from './foo' // 错误import foo from './foo.js' // 正确Import Maps(浏览器)#
<script type="importmap"> { "imports": { "lodash": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.js", "react": "https://esm.sh/react@18", "@/": "./src/" } }</script>
<script type="module"> import _ from 'lodash' import React from 'react' import utils from '@/utils/index.js'</script>顶层 await#
ES2022 新特性#
const response = await fetch('/api/config')export const config = await response.json()
// main.jsimport { config } from './config.js'// 等待 config.js 完全执行后才继续console.log(config)模块依赖图#
console.log('a: start')await new Promise((r) => setTimeout(r, 1000))console.log('a: end')export const a = 'A'
// b.jsconsole.log('b: start')await new Promise((r) => setTimeout(r, 500))console.log('b: end')export const b = 'B'
// main.jsimport { a } from './a.js'import { b } from './b.js'console.log('main')
// 输出顺序:// a: start// b: start// b: end(b 先完成)// a: end// main(等所有依赖完成)模块缓存#
单例模式#
// 模块只加载一次,天然单例export const config = { api: 'https://api.example.com', debug: true,}
// a.jsimport { config } from './config.js'config.debug = false
// b.jsimport { config } from './config.js'console.log(config.debug) // false(同一个对象)清除缓存(Node.js)#
// 删除缓存(仅适用于 CommonJS)delete require.cache[require.resolve('./module.cjs')]
// ES 模块无法清除缓存// 只能通过添加查询字符串绕过const mod = await import(`./module.js?t=${Date.now()}`)调试技巧#
查看模块图#
// 浏览器 DevTools - Network 面板// 查看所有模块请求
node --experimental-loader ./loader.mjs app.js模块加载失败处理#
// 动态导入可以捕获错误try { const module = await import('./might-not-exist.js')} catch (error) { console.error('模块加载失败:', error) // 提供后备 const fallback = await import('./fallback.js')}小结#
| 特性 | 浏览器 | Node.js |
|---|---|---|
| 启用方式 | type="module" | .mjs 或 type: "module" |
| 扩展名 | 必需 | 必需 |
| 顶层 await | 支持 | 支持 |
| 动态导入 | 支持 | 支持 |
| CORS | 需要 | 不适用 |
| import maps | 支持 | 实验性 |
理解模块加载机制有助于正确配置项目和解决模块相关问题。