解析 webpack , vite 处理 commonjs 和 esm 的原理

webpackvite 的流程大致一样,都需要分析抽象语法树获取模块的导入导出, 对每个模块进行编译转换,但是他们两个在开发模式下 对于模块的处理是正好相反

webpack在打包的过程中需要将所有的 js 模块合并成一个文件, 这些 js 模块有些是 CommonJs 的,有些是 ESMwebpack 会统一将所有的模块转换成 类commonjs , 然后打包成一个文件

vite开发模式下 恰恰相反,因为深度依赖 esm , 所以vite需要将所有模块都转换成esm,然后借助浏览器发起网络请求并加载运行

本文的重点在于解释 webpackvite是如何兼容不同模块类型并加载的。

首先看下 webpack 是如何兼容commonjsesm

最简单的导出和导入

先用一个简单的例子解释一下 webpack 的处理过程

转换

假设有两个不同模块类型的文件都导出了变量desc

分别是文件c.js(Commonjs) , 文件e.js(ESM) 表示如下

js 复制代码
// c.js
exports.desc = "commonjs"
js 复制代码
// e.js
export const desc = "esm"

在另外一个模块引入

js 复制代码
// index.js
import { desc as desc1 } from "./c.js"
import { desc as desc2 } from "./e.js"

如何兼容这两种模块呢?webpack 使用 类似commonjs 的形式,将所有的 esm 都转换成类commonjs 的形式,然后打包

首先将 e.js 转换成 commonjs, 如下所示

js 复制代码
exports.desc = "esm"

打包

然后将所有的模块装进一个表里面,如下面所示

下面是这是究极简化的版本,理解其含义

js 复制代码
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.jse.js 的导出都放入exports 对象里面返回,这样就解决了两种模块兼容的问题。

处理差异

仅仅是上面的代码还不能正确的处理这两种模块的差异,还需要处理几个问题

import 的引用问题

比如将 c.jse.js 改一下

js 复制代码
// c.js
let desc = "commonjs"

exports.desc = desc

setTimeout(() => {
  desc = "commonjs-modify" // 改掉该变量
}, 500)
js 复制代码
// e.js
let desc = "esm"
export { desc }
setTimeout(() => {
  desc = "esm-modify" // 改掉该变量
}, 500)

引入的模块

js 复制代码
import { desc as desc1 } from "./c.js"
import { desc as desc2 } from "./e.js"

setTimeout(() => {
  console.log(desc1)
  console.log(desc2)
}, 1000)

这时候输出是什么?

bash 复制代码
commonjs
esm-modify 

可以看到 esm 导出的值被改掉了

这是因为 commonjs 导出的是一个值的拷贝,但是esm导出的是一个引用,所以 esm export 的变量是会变的!

解决这个问题也很简单, 将ESM导出的所有属性都改成getter, 这样每次使用 desc 的时候,都会重新读一次最新的变量

js 复制代码
 "./e.js"(exports) {
    var desc = "esm"

    Object.defineProperty(exports, "desc", {
      enumerable: true,
      get() {
        return desc
      },
    })
  },

export default

esm 支持 export default, 但是commonjs 没有这个语法

所以 webpackcommonjs 所有的导出 exports 当成default export

维持 c.js 不变,将e.js 改成这样

js 复制代码
// e.js
var desc = "esm"
export { desc }
export default desc

将入口的代码改一下

js 复制代码
import c from "./c.js"
import e from "./e.js"

对于commonjs不需要改动,默认就把所有的输出exports当成default export 就行了

对于esm 来说,会生成如下的代码, 将默认导出写在 exportsdefault 字段里面

js 复制代码
  "./e.js"(exports) {
    var desc = "esm"

    Object.defineProperty(exports, "desc", {
      enumerable: true,
      get() {
        return desc
      },
    })

    Object.defineProperty(exports, "default", {
      enumerable: true,
      get() {
        return desc
      },
    })
  },

入口的代码会转换成下面的代码

js 复制代码
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表示当前的 modesm 转换来的, 这样逻辑就跟 webpack 一样了

js 复制代码
// 简化版本
var __importDefault = function (mod) {
    return mod?.__esModule ? mod : { "default": mod };
};

const x = __importDefault(customRequire('xxx'))['default'] // import x from 'xxx'

import * as

假设入口模块是这样的

js 复制代码
import * as c from "./c.js"
import * as e from "./e.js"

对于 esm, 只要将 exports 对象直接 返回就行了, 里面包含了所有字段和 default 字段

而对于 commonjs, 要将exports 复制一份,然后放在 default 字段里面,用于兼容esm标准

定义 importStar

js 复制代码
function importStar(exports) {
  const result = {
    ...exports,
    default: exports,
  }
  return result
}

最后入口模块被转换成下面的形式

js 复制代码
const module1 = customRequire("./c.js")
const module2 = customRequire("./e.js")

const allExport1 = importStar(module1)
const allExport2 = module2

小结

上面涵盖了大部分 esm 引入 commonjsesm 的写法,也写明了如何对应进行转换,总的代码如下所示

js 复制代码
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
}

对应转换的过程如下

js 复制代码
// 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, 然后在各种导入里面做判断

比如

js 复制代码
  "./e.js"(exports) {
    Object.defineProperty(exports, "__esModule", { value: true })
  }

这个时候 importStar 可以改成

js 复制代码
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, 没啥特别的

js 复制代码
// c.js
exports.desc = "commonjs"

e.js 原本是 esm,但是被 webpack之类的工具转换成了 类commonjs 模块

js 复制代码
// 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 函数, 该函数简化后为

js 复制代码
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

js 复制代码
const require_module1 = _commonJs({
  "/c.js"(exports) {
    exports.desc = "esm"
  },
})

export default require_module1() // 等价于 export default exports

e.js 会被转换成

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 *

js 复制代码
import * as all from 'xxx'

被转换成

js 复制代码
import moduleExports from "xxx"

function importStar(exports) {
  if (exports.__esModule) {
    return exports
  } else {
    return { default: { ...exports }, exports }
  }
}

const all = importStar(moduleExports)

import default

js 复制代码
import defaultExport from 'xxx'

被转换成

js 复制代码
import moduleExports from "xxx"

function importDefault(exports) {
  if (exports.__esModule) {
    return exports["default"]
  } else {
    return exports
  }
}

const defaultExport = importDefault(moduleExports)

你看懂了 webpack 是如何处理的,vite这里就很简单了

总结

本文简单介绍了一下打包软件是如何进行模块兼容的,将各种场景和对应的处理都大致说明了一下,读者可以按照这个思路自己写一个打包软件

相关推荐
她似晚风般温柔7894 小时前
Uniapp + Vue3 + Vite +Uview + Pinia 分商家实现购物车功能(最新附源码保姆级)
开发语言·javascript·uni-app
Jiaberrr5 小时前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
everyStudy6 小时前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
Ylucius6 小时前
动态语言? 静态语言? ------区别何在?java,js,c,c++,python分给是静态or动态语言?
java·c语言·javascript·c++·python·学习
tabzzz6 小时前
Webpack 概念速通:从入门到掌握构建工具的精髓
前端·webpack
200不是二百6 小时前
Vuex详解
前端·javascript·vue.js
LvManBa6 小时前
Vue学习记录之三(ref全家桶)
javascript·vue.js·学习
深情废杨杨7 小时前
前端vue-父传子
前端·javascript·vue.js
司篂篂8 小时前
axios二次封装
前端·javascript·vue.js
姚*鸿的博客8 小时前
pinia在vue3中的使用
前端·javascript·vue.js