webpack源码分析二:模块化语法
# 0.前言
本篇博客为webpack_deom02 (opens new window)项目仓库笔记,主要解决两个问题:
- 如何把
es6
模块化语法转为commonJS
模块语法。 - 如何将所有依赖打包并执行,即
bundle.js
文件。
# 1. 前期知识回顾
# EMAScript规范
// 导入语法
import math from "./math" // default 导出
math.basicNum
math.add
import { basicNum, add } from "./math" // 非 default 导出
// 导出语法
// 1. 统一导出
export default { add,basicNum }
export {add,basicNum}
// 2. 各自导出
export const add = () => {}
export const basicNum = 0
2
3
4
5
6
7
8
9
10
11
12
13
14
# CommonJS
// 导入
const math = require("./math")
// 导出语法
// 1.统一导出
module.exports = {
add: add
basicNum : basicNum
}
// 以上写法等价于下面(可以简化为写法二)
module.exports.add = add
// 2.使用 exports 导出
exports.add = add
// 注:使用简化写法导出时,切记不能整体挂载,如:exports = { add, basicNum}
// 这种写法是错误的,因为 CommonJS 会在头部自动加上 exports = module.exports
// 重新赋值后,exports 将会失去 modele.exports 的引用地址。
// 结论:
// 若在 CommonJS 规范中,我们只使用 `module.expots` 导出语法,`exports`(会存在被误覆盖的情况)。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
在转化过程中,我们着重关注导出过程,CommonJS
规范默认相当于 EMAScript
语法的default
导出,如果想要直接获取内部的变量属性或者方法,可以这么写:
const add = require("./math").add
const basicNum = require("./math").basicNum
2
转译目标:
- 将
es6
的导入import
commonJS
的导入require
- 将
es6
的两种导出语法:default
导出 与各自导出语法export {x}
commonJS
的导出语法module.exports
# 2.ESModule
转译到CommonJS
在【案例1】中的compare
文件夹中已经列出了经@babel/preset-env
转换前后的代码,共可以发现3
处不同:
# 转译解析1
// 转译后,在头部多出以下字段
Object.defineProperty(exports, "__esModule", {value: true});
exports["default"] = void 0; // void 0 等价于 undefined
2
3
可以将 Object.defineProperty
语法简化为以下:
exports["__esModule"] = true
exports["default"] = undefined
2
以上两个片段的代码是等价的,从简化的代码可以发现主要做了如下处理:
exports
字段上添加了__esModule
属性。- 将
exports
字段上的default
属性清空。
# 转译解析2
// 转译前:
import b from "./b.js"
// 转译后:
var _b = _interopRequireDefault(require("./b.js"));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { "default": obj };
}
2
3
4
5
6
7
8
如果没有_interopRequireDefault
函数,则就只是将 import
转为 require
语法。
区别在于 b.js
模块本身的内容,分为ES6
模块还是 CommonJS
模块:
- 如果
b.js
是ESMoudle
模块,则等价于var _b = require("./b.js")
- 如果
b.js
是commonJS
规范,则类等价于var _b.default = require("./b.js")
。
# 转译解析3
// 转译前:
export default a
// 转译后:
var _default = a;
exports["default"] = _default;
// 以上等价于
exports["default"] = a
2
3
4
5
6
7
8
目的:CommonJS
规范和 EMAScript
规范在默认导出时的语法差异:绑定在 default
属性后挂载到 exports
。
# 转译解析总结
- 通过
exports.__esModule
标识当前模块是否为EMAScript
模块。 - 统一
ESModule
规范和CommonJS
规范导入导出的规范,保证在代码中所有的导入导出的所有模块都是携带default
字段的。- 如果是
ESModule
规范,导出时exports[default]=...
- 如果是
CommonJS
规范,导入时使用_interopRequireDefault
帮你添加default
字段。
- 如果是
# 3.应用:构建建简易的Webpack打包器
有了上面的基础后,开始搭建核心目标:手动构建一个 webpack
打包器,打包器需要达到以下功能:
减少文件请求次数:现代浏览器在处理
import
语法时,会生成多个请求,而使用Webpack
打包器则可以将多个文件合并为一个bundle.js
文件。宿主环境的降级:将
import
语法转变为CommmonJs
语法,主要可以看bundle.js
中depRelation
中的code:function(){ es5语法 }
。这里的
es5Code
是直接通过@babel/preset-env
转化得到的。使用
modules
缓存各个模块的计算结果,代码片段如下:if (modules[key]) { return modules[key] }
如果已经计算过,则直接弹出。其中,
key
是文件名,如a.js
、b.css
这种。
在《webpack
源码分析1》中,已构建 depRelation
数组,结构如下:
var depRelation = {
{key: 'index.js', deps: ['a.js', 'b.js'], code: function... },
{key: 'a.js', deps: ['b.js'], code: function... },
{key: 'b.js', deps: ['a.js'], code: function... }
}
2
3
4
5
其中,code
内容是通过 @babel/core
将代码转译为ESCode
,且不可执行。 构建打包器的步骤如下:
# 改造1:通过在 code
函数外部再包一层函数
目的:提供 es5Code
中缺少的 require
和 exports
。
code: `function(require, module, exports) {
${ es5Code }
}`
2
3
而具体的 require
和 exports
函数由下面的 execut
函数提供。
# 改造2:构造 execut
函数
目的:执行 require
函数,并将结果存入缓存对象(modules
)中,核心代码逻辑如下:
var modules = {}
function execute(key) {
// 如果已经 require 过,就直接返回上次的结果
if (modules[key]) { return modules[key] }
// 找到要执行的项目
var item = depRelation.find(i => i.key === key)
var pathToKey = (path) => ...... // 把相路径变成项目路径
// 创建 require 函数
var require = (path) => {
return execute(pathToKey(path))
}
// 初始化当前模块
modules[key] = { __esModule: true }
// 初始化 module 方便 code 往 module.exports 上添加属性
var module = { exports: modules[key] }
// 调用 code 函数,往 module.exports 上添加导出属性
item.code(require, module, module.exports)
// 返回当前模块
return modules[key]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 总览打包器代码:
var depRelation = [
{key: 'index.js', deps: ['a.js', 'b.js'], code: function... },
{key: 'a.js', deps: ['b.js'], code: function... },
{key: 'b.js', deps: ['a.js'], code: function... }
]
var modules = {} // modules 用于缓存所有模块
execute(depRelation[0].key)
function execute(key){
var require = ...
var module = ...
item.code(require, module, module.exports)
...
}
// 详见 dist.js
2
3
4
5
6
7
8
9
10
11
12
13
14
# 4.总结
本篇博客主要讨论了以下内容:
webpack
在ESModule
CommonJS
模块转译的三个差异。- 通过提供
require
和exports
,完成一个简易的webpack
打包器。