解析 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) 表示如下

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

处理差异

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

import 的引用问题

比如将 c.jse.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 没有这个语法

所以 webpackcommonjs 所有的导出 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 来说,会生成如下的代码, 将默认导出写在 exportsdefault 字段里面

  "./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表示当前的 modesm 转换来的, 这样逻辑就跟 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 引入 commonjsesm 的写法,也写明了如何对应进行转换,总的代码如下所示

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

(0)
上一篇 2024年3月28日 下午4:47
下一篇 2024年3月28日 下午4:58

相关推荐

发表回复

登录后才能评论