解析 webpack 核心——Webpack 原理
解析 webpack 核心——Webpack 原理
Webpack 要解决的两个问题
编译 import 和 export 关键字
将多个文件打包成一个
如何编译 import 和 export 关键字
- 不同浏览器功能不同
- 现代浏览器可以通过
<script type="module">来支持 import/export
IE 8~15
不支持import/export
- 兼容策略
激进兼容策略 把代码全部放到
<script type="module">
里面缺点 不被
IE 8~15
支持;而且会导致文件请求过多平稳兼容策略 把关键字转译为普通代码(通过转译函数完成),并把所有文件打包成一个文件
缺点 需要写复杂代码来完成这件事情
- 那么怎么写这个转译函数?
@babel/core
已经帮我们做了示例
1 | // project_1/index.js |
执行node -r ts-node/register bundler_1.ts
结果如下
1 | duplicated dependency: a.js |
- 核心代码如下
1 | ; |
- 总结
import 关键字会变成 require 函数
export 关键字会变成 exports 对象
本质:ESModule 语法变成了 CommonJS 规则
但是目前我们不知道 require 函数怎么写,先不管,假设 require 已经写好了
如何将多个文件打包成一个
- 打包后的文件会是什么样子的?
肯定包含所有模块,并且能执行所有模块
预想如下
1 | var depRelation = [ |
现在有三个问题待解决
depRelation 是对象,需要变成一个数组
code 是字符串,需要变成一个函数, 函数遵循 CJS2 规范
execute 函数待完善
解决上述第一个问题
将
depRelation[key] = { deps: [], code: es5Code }
改为了
const item = { key, deps: [], code: es5Code } depRelation.push(item)
1 | // bundler_2.ts |
可以执行node -r ts-node/register bundler_2.ts
看看
解决上述第二个问题
把 code 字符串外面包一个
function(require, module, exports){...}
执行
node project_2/string_code_to_function.js
把{code: ${code2}}
写到文件project_2/result_fun.json
里最终文件里面的 code 就是函数, 代码参见
project_2
解决上述第三个问题
代码参考
dist.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105// dist.js
var depRelation = [{
key: 'index.js',
deps: ['a.js', 'b.js'],
code: function(require, module, exports) {
var _a = _interopRequireDefault(require('./a.js'))
var _b = _interopRequireDefault(require('./b.js'))
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { 'default': obj }
}
console.log(_a['default'].getB())
console.log(_b['default'].getA())
}
}, {
key: 'a.js',
deps: ['b.js'],
code: function(require, module, exports) {
Object.defineProperty(exports, '__esModule', {
value: true
})
exports['default'] = void 0
var _b = _interopRequireDefault(require('./b.js'))
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { 'default': obj }
}
var a = {
value: 'a',
getB: function getB() {
return _b['default'].value + ' from a.js'
}
}
var _default = a
exports['default'] = _default
}
}, {
key: 'b.js',
deps: ['a.js'],
code: function(require, module, exports) {
Object.defineProperty(exports, '__esModule', {
value: true
})
exports['default'] = void 0
var _a = _interopRequireDefault(require('./a.js'))
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { 'default': obj }
}
var b = {
value: 'b',
getA: function getA() {
return _a['default'].value + ' from b.js'
}
}
var _default = b
exports['default'] = _default
}
}]
var modules = {}
execute(depRelation[0].key)
function execute(key) {
// 如果已经 require 过,就直接返回上次的结果
if (modules[key]) {
return modules[key]
}
// 找到要执行的项目
var item = depRelation.find(i => i.key === key)
// 找不到就报错,中断执行
if (!item) {
throw new Error(`${item} is not found`)
}
// 把相对路径变成项目路径
var pathToKey = (path) => {
var dirname = key.substring(0, key.lastIndexOf('/') + 1)
var projectPath = (dirname + path).replace(/\.\//g, '').replace(/\/\//, '/')
return projectPath
}
// 创建 require 函数
var require = (path) => {
return execute(pathToKey(path))
}
// 初始化当前模块
modules[key] = { __esModule: true }
// 初始化 module 方便 code 往 module.exports 上添加属性
var module = { exports: modules[key] }
// 调用 code 函数,往 module.exports 上添加导出属性
// 第二个参数 module 大部分时候是无用的,主要用于兼容旧代码
item.code(require, module, module.exports)
// 返回当前模块
return modules[key]
}执行
node dist.js
得到结果
1
2b from a.js
a from b.js
如何自动生成最终文件?
模板拼接——
var dist = ""; dist += content; writeFileSync('dist.js', dist)
代码参考
bundler_3.ts
, 最终生成文件dist_auto.js
和dist.js
相差无几1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96// bundler_3.ts
// 请确保你的 Node 版本大于等于 14
// 请先运行 yarn 或 npm i 来安装依赖
// 然后使用 node -r ts-node/register 文件路径 来运行,
// 如果需要调试,可以加一个选项 --inspect-brk,再打开 Chrome 开发者工具,点击 Node 图标即可调试
import { parse } from '@babel/parser'
import traverse from '@babel/traverse'
import { writeFileSync, readFileSync } from 'fs'
import { resolve, relative, dirname } from 'path'
import * as babel from '@babel/core'
// 设置根目录
const projectRoot = resolve(__dirname, 'project_1')
// 类型声明
type DepRelation = { key: string, deps: string[], code: string }[]
// 初始化一个空的 depRelation,用于收集依赖
const depRelation: DepRelation = [] // 数组!
// 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js
collectCodeAndDeps(resolve(projectRoot, 'index.js'))
writeFileSync('dist_auto.js', generateCode())
console.log('done')
function generateCode() {
let code = ''
code += 'var depRelation = [' + depRelation.map(item => {
const { key, deps, code } = item
return `{
key: ${JSON.stringify(key)},
deps: ${JSON.stringify(deps)},
code: function(require, module, exports){
${code}
}
}`
}).join(',') + '];\n'
code += 'var modules = {};\n'
code += `execute(depRelation[0].key)\n`
code += `
function execute(key) {
if (modules[key]) { return modules[key] }
var item = depRelation.find(i => i.key === key)
if (!item) { throw new Error(\`\${item} is not found\`) }
var pathToKey = (path) => {
var dirname = key.substring(0, key.lastIndexOf('/') + 1)
var projectPath = (dirname + path).replace(\/\\.\\\/\/g, '').replace(\/\\\/\\\/\/, '/')
return projectPath
}
var require = (path) => {
return execute(pathToKey(path))
}
modules[key] = { __esModule: true }
var module = { exports: modules[key] }
item.code(require, module, module.exports)
return modules[key]
}
`
return code
}
function collectCodeAndDeps(filepath: string) {
const key = getProjectPath(filepath) // 文件的项目路径,如 index.js
if (depRelation.find(i => i.key === key)) {
// 注意,重复依赖不一定是循环依赖
return
}
// 获取文件内容,将内容放至 depRelation
const code = readFileSync(filepath).toString()
const { code: es5Code } = babel.transform(code, {
presets: ['@babel/preset-env']
})
// 初始化 depRelation[key]
const item = { key, deps: [], code: es5Code }
depRelation.push(item)
// 将代码转为 AST
const ast = parse(code, { sourceType: 'module' })
// 分析文件依赖,将内容放至 depRelation
traverse(ast, {
enter: path => {
if (path.node.type === 'ImportDeclaration') {
// path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
const depAbsolutePath = resolve(dirname(filepath), path.node.source.value)
// 然后转为项目路径
const depProjectPath = getProjectPath(depAbsolutePath)
// 把依赖写进 depRelation
item.deps.push(depProjectPath)
collectCodeAndDeps(depAbsolutePath)
}
}
})
}
// 获取文件相对于根目录的相对路径
function getProjectPath(path: string) {
return relative(projectRoot, path).replace(/\\/g, '/')
}
简易打包器
至此,我们就实现了一个简易打包器,但是存在如下问题
生成的代码中有多个重复的
_interopXXX
函数只能引入和运行 JS 文件
只能理解 import,无法理解 require
不支持插件
不支持配置入口文件和 dist 文件名