Skip to content

Module 加载

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 模块#

app.mjs
// 方式1:使用 .mjs 扩展名
import { readFile } from 'fs/promises';
// 方式2:package.json 设置 type
// package.json
{
"type": "module"
}
// 然后 .js 文件就是 ES 模块
// app.js
import { 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” 或省略CommonJSES 模块CommonJS

ES 模块 vs CommonJS#

语法差异#

// ES 模块
import fs from 'fs'
import { readFile } from 'fs'
export const name = 'ES'
export default function () {}
// CommonJS
const 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 = 0
export function increment() {
count++
}
// main.js
import { count, increment } from './counter.js'
console.log(count) // 0
increment()
console.log(count) // 1(引用,值更新了)
// counter.cjs (CommonJS)
let count = 0
module.exports = {
count,
increment() {
count++
},
}
// main.cjs
const { count, increment } = require('./counter.cjs')
console.log(count) // 0
increment()
console.log(count) // 0(值的复制,不会更新)

this 的值#

// ES 模块顶层
console.log(this) // undefined
// CommonJS 顶层
console.log(this) // module.exports(初始为 {})

互操作性#

ES 模块导入 CommonJS#

lib.cjs
module.exports = {
name: 'Library',
greet() {
return 'Hello'
},
}
// main.mjs
// 默认导入获取整个 module.exports
import lib from './lib.cjs'
console.log(lib.name) // 'Library'
console.log(lib.greet()) // 'Hello'
// 命名导入可能不可用(取决于 Node 版本)
// import { name } from './lib.cjs'; // 可能报错

CommonJS 导入 ES 模块#

lib.mjs
export const name = 'ES Module'
export default function greet() {
return 'Hello'
}
// main.cjs
// CommonJS 不能直接 require ES 模块
// const lib = require('./lib.mjs'); // 错误
// 需要使用动态 import
async function main() {
const lib = await import('./lib.mjs')
console.log(lib.name) // 'ES Module'
console.log(lib.default()) // 'Hello'
}
main()

创建双模式包#

package.json
{
"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.mjs
import utils from 'my-package/utils' // 加载 ./dist/utils.mjs
// CommonJS
const pkg = require('my-package') // 加载 ./dist/index.cjs
const 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 新特性#

config.js
const response = await fetch('/api/config')
export const config = await response.json()
// main.js
import { config } from './config.js'
// 等待 config.js 完全执行后才继续
console.log(config)

模块依赖图#

a.js
console.log('a: start')
await new Promise((r) => setTimeout(r, 1000))
console.log('a: end')
export const a = 'A'
// b.js
console.log('b: start')
await new Promise((r) => setTimeout(r, 500))
console.log('b: end')
export const b = 'B'
// main.js
import { a } from './a.js'
import { b } from './b.js'
console.log('main')
// 输出顺序:
// a: start
// b: start
// b: end(b 先完成)
// a: end
// main(等所有依赖完成)

模块缓存#

单例模式#

config.js
// 模块只加载一次,天然单例
export const config = {
api: 'https://api.example.com',
debug: true,
}
// a.js
import { config } from './config.js'
config.debug = false
// b.js
import { 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()}`)

调试技巧#

查看模块图#

Node.js
// 浏览器 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".mjstype: "module"
扩展名必需必需
顶层 await支持支持
动态导入支持支持
CORS需要不适用
import maps支持实验性

理解模块加载机制有助于正确配置项目和解决模块相关问题。