背景:上周发现公司项目里面使用的 import('./xxx.js') (dynamic import 此方法为动态倒入并且返回一个 promise),有的同学在这个 dynamic import 的 then里面直接使用,也有同学使用 data.default 属性,也可以正常运行,并且里面打印的信息也差不多,今天一探究竟。
前置: 对项目进行一个抽象,首先查看 webpack打包之后的产物。产出一个 IIFE, 此IIFE的传入参数为一个由多个 js bundle 组成的对象,类似下面的抽象
(fucntion(module){
// 实现__webpack_require__
})
({
'/src/index': function(module, __webpack_exports__, __webpack_require__){
var xx = __webpack_require__('/src/test')
console.log('xx', xx)
},
'/src/test': function(module, __webpack_exports__, __webpack_require__){
console.log('title')
},
})
本质上 webpack 是一个打包器,可以打包多种规范的js文件,webpack在打包之后,自己实现了一套自己的 commonjs 规范,核心就是 webpack_require 和 webpack_exports, 此处不是今天的重点。 请记住: 每一个需要加载的模块(对于webpack 编译之后来说,就是每一个 module 都有自己的 webpack_exports、webpack_require), 我们的属性和方法通常都是挂载在__webpack_exports__ 上面。
问题1: 使用 dynamic import 无论导入 commonjs 模块或者导入 esmodule 模块,上面都有一个 default 属性(都使用了默认的导出的情况下)
问题2: 使用 dynamic import 无论导入 commonjs 模块或者导入 esmodule 模块,打印结果都有 {__esmodule: true, Sysmbol.toStringTag: 'Module' }
问题3: 使用 dynamic import, webpack 如何进行切割的 (下次写)
问题4: 使用 dynamic import, 此 js bundle 如何被加载的 (下次写)
case1: 当我们使用 dynamic import的时候,我们动态倒入一个 esmodule 的js 文件。
// test.js
export default 'test'
export const name = 'test1'
//index.js
import('./test.js').then((data) => {
console.log(data)
// 打包结果: { name: 'test1', default: 'test', __esmodule: true, Sysmbol.toStringTag: 'Module' }
})
test.js 的打包结果 如下所示:(由于使用的 动态导入,所以test.js打包结果是一个单独的 js bundle)
能产生此打印的结果原因是:
- webpack_require.r 方法 包装了 test.js 的 webpack_exports (因为 test.js 是一个 esmodule 模块,在 webpack 里面 所有的 esmodules 模块 都需要被__webpack_require__.r 包装一下)
通过此方法包装之后,当前模块对象就会存在2个属性 (__esmodule: true 和 Sysmbol.toStringTag: 'Module' ), 主要就是标识 test.js 是一个 esmodule 模块,注意 如果是 commonjs 模块是不会被 webpack_require.r 包装的。
- webpack_require.d 方法本质上就是 Object.defineProperty,通过 Object.defineProperty 对一个对象进行定义属性
对于 test.js 打包结果,我们看到首先 通过 webpack_require.d 在 webpack_exports 上面定义了一个 age 属性 和一个 getter 函数。(相当于 Object.defineProperty(webpack_exports, 'age', { enumerable: true, get: getter }))
当用户访问 webpack_exports.age 就会触发 getter 方法执行。
- 后面对 __webpack_exports__定义一个 default 属性 (webpack_exports["default"] = "11") 通过以上,webpack 就完成了 对 test.js 的编译。 本质上相当于在 export 上面定义了 4个属性 (name、default、__esmodule、Sysmbol.toStringTag), 所以我们在 dynamic import的 then(...) 里面 可以通过 data.default 拿到值 (最主要我们在此esmodule 模块使用了 默认导出)
case2: 当我们使用 dynamic import的时候,我们动态倒入一个 commonjs 的js文件。
// test.js
module.export.test = 'test'
export.name = 'test1'
//index.js
import('./test.js').then((data) => {
console.log(data)
// 打包结果: { test: 'test', name: 'test1', default: { test: 'test', name: 'test1' }, __esmodule: true, Sysmbol.toStringTag: 'Module' }
})
test.js 的打包结果 如下所示:(由于使用的 动态导入,所以test.js打包结果是一个 js bundle)
能产生此打印的结果原因是:
- 在 test.js 的打包结果看到,只针对 module 这个对象进行了赋值 (module.exports.test = 'test'; exports.name = 'test1')
- 此时就会产生疑问,明明 test.js 的打包结果只有上面两行代码,为什么会产生 __esmodule: true, Sysmbol.toStringTag: 'Module' 这两个属性呢?
- 此时我们查看加载 test.js 的代码就会发现原因
- 通过 webpack_require.e 实现异步加载了一个 js bundle,本质上就是发出一个请求,然后接收此对于的js (后续抽空详细写一个此过程)
- webpack_require.t 加载一个 commonjs 模块 (今天的重点)
- 通过 webpack_require.t.bind(null) 绑定this 并且返回一个函数
- 在bind 之后这个函数 接收2个参数(path, mode)
- 查看 webpack_require.t 方法,此方法内部用到了二进制的位运算 (位运算的规律: 使用 & 时,对应的位置都是1 结果才是1,否则结果是0; 使用 | 时,对应的位置有1个是1,结果就是1,当两个位都是0时,结果才是 0)
- 进入条件: 可以看到 首先 让 mode & 1 取一次值,此时 mode 为 7 (0b0111 & 0b0001, 结果是1), 所以触发了 webpack_require, 直接加载此模块(其实就是加载 test.js)
- 进入条件: mode & 8 取一次值,此时 mode 为 7 (0b0111 & 0b1000, 结果是0),接着向下执行
- 进入条件: mode & 4 取一次值,此时 mode 为 7 (0b0111 & 0b0100, 结果是4) 但是 value.__esmodule 不是 true(因为此时的 value 是被加载的 test.js 这个 commonjs 模块,__esmodule 只会标识 esmodule的模块)
- 接着向下执行: var ns = Object.create(null) 创造一个新的对象
- 接着向下执行: webpack_require.r(ns) 通过__webpack_require__.r 包装一下这个刚刚创建的新对象 (通过此方法包装之后,当前模块对象就会存在2个属性 (__esmodule: true 和 Sysmbol.toStringTag: 'Module' ), 所以当我们加载 commonjs 的时候,也会答应 __esmodule 和 Sysmbol.toStringTag 属性)
14. 通过 Object.defineProperty(ns, 'default', {enumerable: true, value: value}) 给 ns 上面 挂载了 default 属性,并且这个属性的 value 是 test.js 打包的结果, 此结果: {test: 'test', name: 'test1' }
15. 进入条件: mode & 2 取一次值,此时 mode 为 7 (0b0111 & 0b0010, 结果是2),并且 value 不是一个 string (value 是正在被加载的 test.js 打包的结果, 此结果: {test: 'test', name: 'test1' } )
16. 此时循环 这个 test.js 打包的结果(上面有两个属性test、name), 通过__webpack_require__.d 给已经定义的 ns 这个对象分别赋值这两个key(name、test), 相当于 Object.defineProperty(ns, 'name', function(){ return 'test1'}; Object.defineProperty(ns, 'test', function(){ return 'test'})
17. 最终返回 ns 这个对象。 回头再看, ns 对象首先被__webpack_require__.r 处理一下,后面被 Object.defineProperty(ns, 'default', {enumerable: true, value: value}) 处理一下,最后被使用 for in 循环拿到 加载的 test.js 打包的结果,进行遍历赋值给到 ns 对象。
- 经上述处理, ns 也是一个 esmodule 模块,只是被webpack 特殊处理的,所以打印结果也有__esmodule: true 和 Sysmbol.toStringTag: 'Module'
- 经过上述,使用 webpack 打包项目时,当尝试加载一个非 esmodule 规范的js bundle时,webpack 会自己对这个模块进行修饰 (添加__esmodule、Sysmbol.toStringTag,default 这三个key)。至此,回到最初的问题,我们可以在 default 里面拿到我们需要的值,也可以不在 default 里面拿到值(webpack给我们做了处理,还要看我们是否使用了默认导出)
- webpack_require.t 主要就是加载非 esmodule 规范的js bundle,并且为其包装成为一个看似编译结果是 esmodule的模块。在非动态导入也会遇到类似的加载过程,webpack_require.t 主要目的就是加载该模块 并且给该模块加上__esmodule、Sysmbol.toStringTag 属性