webpack
和 vite
的流程大致一样,都需要分析抽象语法树获取模块的导入导出, 对每个模块进行编译转换,但是他们两个在开发模式下对于模块的处理是正好相反的
webpack
在打包的过程中需要将所有的 js
模块合并成一个文件, 这些 js
模块有些是 CommonJs
的,有些是 ESM
,webpack
会统一将所有的模块转换成 类commonjs
, 然后打包成一个文件
而 vite
在开发模式下恰恰相反,因为深度依赖 esm
, 所以vite
需要将所有模块都转换成esm
,然后借助浏览器发起网络请求并加载运行
本文的重点在于解释 webpack
和vite
是如何兼容不同模块类型并加载的。
首先看下 webpack
是如何兼容commonjs
和esm
的
最简单的导出和导入
先用一个简单的例子解释一下 webpack
的处理过程
转换
假设有两个不同模块类型的文件都导出了变量desc
分别是文件c.js
(Commonjs
) , 文件e.js
(ESM
) 表示如下
// c.js
exports.desc = "commonjs"
// e.js
export const desc = "esm"
在另外一个模块引入
// index.js
import { desc as desc1 } from "./c.js"
import { desc as desc2 } from "./e.js"
如何兼容这两种模块呢?webpack
使用 类似commonjs
的形式,将所有的 esm
都转换成类commonjs
的形式,然后打包
首先将 e.js
转换成 commonjs
, 如下所示
exports.desc = "esm"
打包
然后将所有的模块装进一个表里面,如下面所示
下面是这是究极简化的版本,理解其含义
const modules = {
"./c.js"(exports) { // c.js 的代码
exports.desc = "commonjs"
},
"./e.js"(exports) { // e.js 的代码
exports.desc = "esm"
},
}
// 自定义的 require
function customRequire(key) {
const exports = {}
modules[key](exports)
return exports
}
// 导入的模块被翻译成如下所示
const module1 = customRequire("./c.js")
const module2 = customRequire("./e.js")
const desc1 = module1.desc
const desc2 = module2.desc
上面的代码自定义了一个 customRequire
的函数,该函数将 c.js
和e.js
的导出都放入exports
对象里面返回,这样就解决了两种模块兼容的问题。
处理差异
仅仅是上面的代码还不能正确的处理这两种模块的差异,还需要处理几个问题
import 的引用问题
比如将 c.js
和 e.js
改一下
// c.js
let desc = "commonjs"
exports.desc = desc
setTimeout(() => {
desc = "commonjs-modify" // 改掉该变量
}, 500)
// e.js
let desc = "esm"
export { desc }
setTimeout(() => {
desc = "esm-modify" // 改掉该变量
}, 500)
引入的模块
import { desc as desc1 } from "./c.js"
import { desc as desc2 } from "./e.js"
setTimeout(() => {
console.log(desc1)
console.log(desc2)
}, 1000)
这时候输出是什么?
commonjs
esm-modify
可以看到 esm
导出的值被改掉了
这是因为 commonjs
导出的是一个值的拷贝,但是esm
导出的是一个引用,所以 esm export 的变量是会变的!
解决这个问题也很简单, 将ESM
导出的所有属性都改成getter
, 这样每次使用 desc
的时候,都会重新读一次最新的变量
"./e.js"(exports) {
var desc = "esm"
Object.defineProperty(exports, "desc", {
enumerable: true,
get() {
return desc
},
})
},
export default
esm
支持 export default
, 但是commonjs
没有这个语法
所以 webpack
将 commonjs
所有的导出 exports
当成default export
维持 c.js
不变,将e.js
改成这样
// e.js
var desc = "esm"
export { desc }
export default desc
将入口的代码改一下
import c from "./c.js"
import e from "./e.js"
对于commonjs
不需要改动,默认就把所有的输出exports
当成default export
就行了
对于esm
来说,会生成如下的代码, 将默认导出写在 exports
的 default
字段里面
"./e.js"(exports) {
var desc = "esm"
Object.defineProperty(exports, "desc", {
enumerable: true,
get() {
return desc
},
})
Object.defineProperty(exports, "default", {
enumerable: true,
get() {
return desc
},
})
},
入口的代码会转换成下面的代码
const module1 = customRequire("./c.js")
const module2 = customRequire("./e.js")
const defaultExport1 = module1
const defaultExport2 = module2["default"]
对于 commonjs
,导入 exports
对象
对于 esm
, 导入 exports['default']
对象
tsc 在这一块不会默认引入 commonjs 的 所有对象作为 exports 对象,要你开启
esModuleInterop
才行
关于 tsc 在 import default 的处理
tsc
在处理导入 commonjs
的时候,不会把把所有的 exports
作为默认的导入,还是会访问 exports['default']
如果你开启了 esModuleInterop: true
, 会生成一个辅助函数, 用__esModule
进行判断,__esModule
表示当前的 mod
是 esm
转换来的, 这样逻辑就跟 webpack
一样了
// 简化版本
var __importDefault = function (mod) {
return mod?.__esModule ? mod : { "default": mod };
};
const x = __importDefault(customRequire('xxx'))['default'] // import x from 'xxx'
import * as
假设入口模块是这样的
import * as c from "./c.js"
import * as e from "./e.js"
对于 esm
, 只要将 exports 对象直接
返回就行了, 里面包含了所有字段和 default
字段
而对于 commonjs
, 要将exports
复制一份,然后放在 default
字段里面,用于兼容esm
标准
定义 importStar
function importStar(exports) {
const result = {
...exports,
default: exports,
}
return result
}
最后入口模块被转换成下面的形式
const module1 = customRequire("./c.js")
const module2 = customRequire("./e.js")
const allExport1 = importStar(module1)
const allExport2 = module2
小结
上面涵盖了大部分 esm
引入 commonjs
和esm
的写法,也写明了如何对应进行转换,总的代码如下所示
const modules = {
"./c.js"(exports) {
// c.js 的代码
exports.desc = "commonjs"
},
"./e.js"(exports) {
var desc = "esm"
Object.defineProperty(exports, "desc", {
enumerable: true,
get() {
return desc
},
})
Object.defineProperty(exports, "default", {
enumerable: true,
get() {
return desc
},
})
},
}
// 自定义的 require
function customRequire(key) {
const exports = {}
modules[key](exports)
return exports
}
// import * as
function importStar(exports) {
const result = {
...exports,
default: exports,
}
return result
}
对应转换的过程如下
// import commonjs
const c = custom_require("./c.js") // <= import c from './c.js'
const c = importStar(custom_require("./c.js")) // <= import * as c from './c.js'
const {desc} = custom_require("./c.js") // <= import {desc} from './c.js'
// import esm
const e = custom_require("./e.js")['default'] // <= import e from './e.js'
const e = custom_require("./e.js") // <= import * as e from './e.js'
const {desc} = custom_require("./e.js") // <= import {desc} from './e.js'
__esModule
一般打包软件都会给 esm
的导出定义一个新的字段__esModule
,标记这个模块原本是 esModule
, 然后在各种导入里面做判断
比如
"./e.js"(exports) {
Object.defineProperty(exports, "__esModule", { value: true })
}
这个时候 importStar
可以改成
function importStar(exports) {
if (exports.__esModule) { // 如果是 `esm` 直接返回`exports`
return exports
}
const result = {
...exports,
default: exports,
}
return result
}
vite
vite
在开发阶段跟 webpack
刚好是反过来的,需要将所有模块转换成 esm
, 浏览器可以识别并运行这种模块机制,对于esm
导入 esm
,根本不需要处理,直接丢给浏览器就行了
重点在于如何在 esm
中导入 类commonjs
的模块
定义两个 类commonjs
文件
c.js
文件本身就是 commonjs
, 没啥特别的
// c.js
exports.desc = "commonjs"
e.js
原本是 esm
,但是被 webpack
之类的工具转换成了 类commonjs
模块
// e.js
// 标记它原本是 esm
Object.defineProperty(exports, "__esModule", { value: true })
// export default
Object.defineProperty(exports, "default", {
enumerable: true,
get() {
return 10
},
})
// 一个导出的属性
Object.defineProperty(exports, "desc", {
enumerable: true,
get() {
return "esm"
},
})
// 其实简化了就是下面的代码
// exports.desc = "esm"
// exports.__esModule = true
// exports.default = "defaultExport"
vite
对于上面的文件的处理非常简单粗暴,直接 export default exports
vite
会定义一个 _commonJs
函数, 该函数简化后为
function _commonJs(cb) {
var exports
return function require() {
if (exports) return exports
exports = {}
cb[Object.getOwnPropertyNames(cb)[0]](exports)
return exports
}
}
然后c.js
会转换成如下的代码, 说白了就是 export default exports
const require_module1 = _commonJs({
"/c.js"(exports) {
exports.desc = "esm"
},
})
export default require_module1() // 等价于 export default exports
e.js
会被转换成
const require_module1 = _commonJs({
"/e.js"(exports) {
Object.defineProperty(exports, "__esModule", { value: true })
// export default
Object.defineProperty(exports, "default", {
enumerable: true,
get() {
return 10
},
})
// 一个导出的属性
Object.defineProperty(exports, "desc", {
enumerable: true,
get() {
return "esm"
},
})
},
})
export default require_module1() // 等价于 export default exports
导出的部分都差不多,重点在于导入的部分
import *
import * as all from 'xxx'
被转换成
import moduleExports from "xxx"
function importStar(exports) {
if (exports.__esModule) {
return exports
} else {
return { default: { ...exports }, exports }
}
}
const all = importStar(moduleExports)
import default
import defaultExport from 'xxx'
被转换成
import moduleExports from "xxx"
function importDefault(exports) {
if (exports.__esModule) {
return exports["default"]
} else {
return exports
}
}
const defaultExport = importDefault(moduleExports)
你看懂了 webpack
是如何处理的,vite
这里就很简单了
总结
本文简单介绍了一下打包软件是如何进行模块兼容的,将各种场景和对应的处理都大致说明了一下,读者可以按照这个思路自己写一个打包软件
原文链接:https://juejin.cn/post/7349551248516333605 作者:asyncrustacean