webpack-bundler-demo

Webpack 简单实现

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/**
* 读取内容
*
* yarn add babylon babel-core babel-traverse babel-preset-env
* 首先我们传入一个文件路径参数,然后通过 fs 将文件中的内容读取出来
* 接下来我们通过 babylon 解析代码获取 AST,目的是为了分析代码中是否还引入了别的文件
* 通过 dependencies 来存储文件中的依赖,然后再将 AST 转换为 ES5 代码
* 最后函数返回了一个对象,对象中包含了当前文件路径、当前文件依赖和当前文件转换后的代码
*/
function readCode(filePath) {
// 读取文件内容(以字符串的形式)
const content = fs.readFileSync(filePath, 'utf-8')

// 生成 AST(转换字符串为 AST 抽象语法树)
const ast = babylon.parse(content, {
sourceType: 'module'
})

// 寻找当前文件的依赖关系
const dependencies = []
// 遍历抽象语法树
traverse(ast, {
// 每当遍历到 import 语法的时候
ImportDeclaration: ({ node }) => {
// 依赖文件的相对路径
// 把依赖的模块加入到数组中
dependencies.push(node.source.value)
}
})

// 转换为浏览器可运行的代码
// 通过 AST 将代码转为 ES5
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})

return {
filePath,
dependencies,
code
}
}

/**
* 提取依赖关系
*
* 调用 readCode 函数,传入入口文件
* 分析入口文件的依赖
* 识别 JS 和 CSS 文件
* 首先我们读取入口文件,然后创建一个数组,该数组的目的是存储代码中涉及到的所有文件
* 接下来我们遍历这个数组,一开始这个数组中只有入口文件,在遍历的过程中,如果入口文件有依赖其他的文件,那么就会被 push 到这个数组中
* 在遍历的过程中,我们先获得该文件对应的目录,然后遍历当前文件的依赖关系
* 在遍历当前文件依赖关系的过程中,首先生成依赖文件的绝对路径,然后判断当前文件是 CSS 文件还是 JS 文件
* 如果是 CSS 文件的话,我们就不能用 Babel 去编译了,只需要读取 CSS 文件中的代码,然后创建一个 style 标签,将代码插入进标签并且放入 head 中即可
* 如果是 JS 文件的话,我们还需要分析 JS 文件是否还有别的依赖关系
* 最后将读取文件后的对象 push 进数组中
*/
function getDependencies(entry) {
// 读取入口文件
// 从入口开始,分析所有依赖项,形成依赖图,采用深度优先遍历
const entryObject = readCode(entry)
// 定义一个保存依赖项的数组
const dependencies = [entryObject]

// 遍历所有文件依赖关系
for (const asset of dependencies) {
// 获得文件目录
const dirname = path.dirname(asset.filePath)

// 遍历当前文件依赖关系
asset.dependencies.forEach(relativePath => {
// 获得绝对路径
const absolutePath = path.join(dirname, relativePath)
// CSS 文件逻辑就是将代码插入到 `style` 标签中
if (/\.css$/.test(absolutePath)) {
const content = fs.readFileSync(absolutePath, 'utf-8')
const code = `
const style = document.createElement('style')
style.innerText = ${JSON.stringify(content).replace(/\\r\\n/g, '')}
document.head.appendChild(style)
`
dependencies.push({
filePath: absolutePath,
relativePath,
dependencies: [],
code
})
} else {
// JS 代码需要继续查找是否有依赖关系
const child = readCode(absolutePath)
// 给子依赖项赋值
child.relativePath = relativePath
// 将子依赖也加入队列中,循环处理
dependencies.push(child)
}
})
}

return dependencies
}

/**
* 实现打包的功能,生成浏览器可执行文件
*
*(Babel 将我们 ES6 的模块化代码转换为了 CommonJS,浏览器不支持,所以自己实现 CommonJS 相关的代码)
* 首先遍历所有依赖文件,构建出一个函数参数对象
* 对象的属性就是当前文件的相对路径,属性值是一个函数,函数体是当前文件下的代码,函数接受三个参数 module、exports、require
* module 参数对应 CommonJS 中的 module
* exports 参数对应 CommonJS 中的 module.export
* require 参数对应我们自己创建的 require 函数
* 接下来就是构造一个使用参数的函数了,函数做的事情很简单,就是内部创建一个 require 函数,然后调用 require(entry),也就是 require('./entry.js'),这样就会从函数参数中找到 ./entry.js 对应的函数并执行,最后将导出的内容通过 module.export 的方式让外部获取到
* 最后再将打包出来的内容写入到单独的文件中
*/
function bundle(dependencies, entry) {
let modules = ''
// 构建函数参数,生成的结构为
// { './entry.js': function(module, exports, require) { 代码 } }

dependencies.forEach(dep => {
const filePath = dep.relativePath || entry
modules += `'${filePath}': (
function (module, exports, require) { ${dep.code} }
),`
})

// 构建 require 函数,目的是为了获取模块暴露出来的内容
// module, exports, requir 不能直接在浏览器中使用,这里模拟了模块加载、执行、导出操作
const result = `
(function(modules) {
// 创建一个 require 函数: 它接受一个 模块ID 并在我们之前构建的模块对象查找它
function require(id) {
const module = { exports : {} }
//
modules[id](module, module.exports, require)

return module.exports
}

// 执行入口文件
require('${entry}')

})({${modules}})
`

// 当生成的内容写入到文件中
fs.writeFileSync('./bundle.js', result)
}
坚持原创技术分享,您的支持将鼓励我继续创作!