Webpack原理
# Webpack原理
- loader 和 plugin 作用及编写
- 实现 mini-webpack,加深对webpack的理解
- 完整项目地址:mini-webpack (opens new window)
# webpack 的 bundle.js 文件分析
# 文件准备
- src/index.js
const vars = require('./variable')
const calcs = require('./calc')
console.log('结果:' + calcs.add(1, 2, 3, 4, 5))
console.log('i am ' + vars.name)
2
3
4
5
- src/variable.js
const name = 'coderly'
module.exports = {
name
}
2
3
4
5
- src/calc.js
module.exports = {
add(...nums) {
return nums.reduce((p, c) => p + c, 0)
}
}
2
3
4
5
打包之后的文件(删除无关注释)
(() => {
var __webpack_modules__ = ({
"./src/calc.js":
((module) => {
// 做了优化,未使用 __webpack_require__ 将不再作为变量传入
// 在 module.exports 上添加变量,最终是在 __webpack_module_cache__ 对象中的模块中添加了代码
eval("module.exports = {\r\n add(...nums) {\r\n return nums.reduce((p, c) => p + c, 0)\r\n }\r\n}\n\n//# sourceURL=webpack://webpack-test/./src/calc.js?");
}),
"./src/index.js":
((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
// 递归加载 variable.js 和 calc.js
eval("const vars = __webpack_require__(/*! ./variable */ \"./src/variable.js\")\r\nconst calcs = __webpack_require__(/*! ./calc */ \"./src/calc.js\")\r\n\r\nconsole.log('结果:' + calcs.add(1, 2, 3, 4, 5))\r\nconsole.log('i am ' + vars.name)\n\n//# sourceURL=webpack://webpack-test/./src/index.js?");
}),
"./src/variable.js":
((module) => {
eval("const name = 'coderly'\r\n\r\nmodule.exports = {\r\n name\r\n}\n\n//# sourceURL=webpack://webpack-test/./src/variable.js?");
})
});
// 缓存 - 用来判断文件是否已加载
var __webpack_module_cache__ = {};
function __webpack_require__ (moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 构建 模块并添加进缓存
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};
// 执行 模块对应的代码
// module 变量传入的是一个引用,做的修改将会直接作用在 __webpack_module_cache__ 对应模块中
// __webpack_require__ 递归加载文件
// require 无法在 浏览器环境下使用,所以命名 __webpack_require__
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
// 开始加载 入口文件
var __webpack_exports__ = __webpack_require__("./src/index.js");
})();
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
说明
__webpack_require__
其实就是require
(因为require 无法在浏览器环境下使用,所以命名 webpack_require)- 从入口文件开始加载文件执行
- 遇到需要
require
的,会先递归执行__webpack_require__
函数,并return module.exports
- 以上例分析
- 先执行 "./src/index.js" 对应值的代码
- 第一行遇到 const vars = webpack_require(/*! ./variable */ "./src/variable.js"),开始执行 "./src/variable.js" 对应值的代码
- "./src/variable.js" 对应的代码中 在执行完
__webpack_require__
函数后,return 了 module.exports,"./src/index.js" 对应代码中获取到了 "./src/variable.js" 导出的函数 - 第二行遇到 const calcs = webpack_require(/*! ./calc */ "./src/calc.js")
- TODO
# webpack 的 loader
执行顺序: 从下至上,从右至左
# this 上的属性说明
属性 | 作用 | 参数 | 返回值 |
---|---|---|---|
addContextDependency | 添加一个目录作为加载程序结果的依赖项 | directory:string | —— |
addDependency | 添加文件作为加载程序结果的依赖项,以使其可观察 | file:string | —— |
async | 告诉加载程序运行程序加载程序打算异步回调。返回this.callback | —— | this.callback |
cacheable | 设置可缓存标志的函数 | flag:boolean | —— |
callback | 可以同步或异步调用以返回多个结果的函数 | (err: Error/null, content: string | Buffer, sourceMap?: SourceMap, meta?: any) |
clearDependencies | 删除加载程序结果的所有依赖关系,甚至包括初始依赖关系和其他加载程序的依赖关系 | —— | —— |
context | 模块所在的目录。可以用作解析其他模块路径的上下文 | —— | —— |
data | 在 pitch 阶段和正常阶段之间共享的 data 对象 | —— | —— |
emitError | 发出一个错误,该错误也可以显示在输出中 | error: Error | —— |
emitFile | 产生一个文件。这是 webpack 特有的 | (name: string, content: Buffer/string, sourceMap: {...}) | —— |
emitWarning | 发出警告,该警告将在输出中显示 | warning: Error | —— |
fs | 用于访问 compilation 的 inputFileSystem 属性 | —— | —— |
getOptions | 提取给定的加载程序选项 | schema:JSON模式参数 | —— |
getResolve | 创建类似于的解析函数this.resolve | 函数重载 | —— |
hot | loaders 热模块替换的相关信息 | —— | —— |
importModule | 子编译器在构建时编译和执行请求的另一种轻量级解决方案 | (request, options, [callback]): Promise | —— |
loaderIndex | 当前loader 在 loader 数组中的索引 | (request: string, callback: function(err, source, sourceMap, module)) | —— |
loadModule | 将给定的请求解析为模块,应用所有已配置的 loader,并使用生成的source,sourceMap和模块实例(通常是的实例NormalModule)进行回调 | (request: string, callback: function(err, source, sourceMap, module)) | —— |
loaders | loader 数组,可以在 pitch 阶段写入 | —— | —— |
mode | webpack 在 哪个模式下运行 | —— | production/development/none |
query | 如果这个 loader 配置了 options 对象的话,this.query 就指向这个 option 对象; 如果 loader 中没有 options,而是以 query 字符串作为参数调用时,this.query 就是一个以 ? 开头的字符串 | —— | production/development/none |
request | 解析的请求字符串 | —— | production/development/none |
resolve | 解析操作的所有依赖项将作为依赖项自动添加到当前模块 | (context: string, request: string, callback: function(err, result: string)) | —— |
resource | request 中的资源部分,包括 query 参数 | —— | —— |
resourcePath | 资源文件的路径 | —— | —— |
resourceQuery | 资源的 query 参数 | —— | —— |
rootContext | 从webpack 4开始,以前 this.options.context 替换为 this.rootContext | —— | —— |
sourceMap | 应该生成一个 source map。因为生成 source map 可能会非常耗时,你应该确认 source map 确实有必要请求 | —— | —— |
target | 编译的目标。从配置选项中传递过来的 | —— | web/node |
utils | 访问 contextify 和 absolutify 实用工具 | —— | —— |
version | loader API 版本 | —— | —— |
webpack | 如果是由 webpack 编译的,这个布尔值会被设置为真 | —— | —— |
_compilation | 访问webpack的当前Compilation对象 | —— | —— |
_compiler | 访问webpack的当前Compiler对象 | —— | —— |
# 如何编写 loader
实现 vars-usage-times-loader
- 统计某个变量在文件中使用到的次数
- 使用 loader-utils 插件来提取 query
- 确定 use loader 传参字段
- webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
entry: './src/index.js',
resolveLoader: {
// 自定义模块解析
modules: [path.resolve(__dirname, './loader'), 'node_modules'],
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'vars-usage-times-loader',
options: {
varname: 'works'
}
}
}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- loader/vars-usage-times-loader.js
const parser = require('@babel/parser')
const generator = require('@babel/generator').default
const traverse = require('@babel/traverse').default
const loaderUtils = require('loader-utils')
module.exports = function(source) {
// 获取在 webpack.config.js 中传入的参数
const options = loaderUtils.getOptions(this)
// 将文件转换成 ast
let ast = parser.parse(source)
// 遍历 ast
traverse(ast, {
// 如果是函数调用
CallExpression(p) {
// 获取当前函数调用的名字
let name = p.node.callee.name
// 只匹配 require 的变量
if (name === 'require') {
// 获取 require 文件起的变量名
let varName = p.parent.id.name
// 如果该变量名和 传入的相匹配
if (varName && varName === options.varname) {
// 开始获取该变量名在当前文件中使用的次数
let currentBinding = p.scope.getBinding(p.parent.id.name)
console.log(`${options.varname} 变量在当前文件中变量使用了:${currentBinding.references} 次`)
}
}
}
})
// 重新将 ast 生成代码
source = generator(ast).code
return source
}
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
说明:
- 获取在 webpack.config.js 中传入的参数
- 将文件转换成 ast
- 遍历 ast,获取当前函数调用的名字只匹配 require 的变量
- 获取 require 文件起的变量名,如果该变量名和 传入的相匹配,开始获取该变量名在当前文件中使用的次数
- 重新将 ast 生成代码
至此一个简单的 loader 插件就完成了
# webpack 的 plugin
- 一个 JavaScript 命名函数
- 在插件函数的 prototype 上定义一个 apply 方法
- 指定一个绑定到 webpack 自身的事件钩子
- 处理 webpack 内部实例的特定数据
- 功能完成后调用 webpack 提供的回调
# webpack 的生命周期钩子
钩子 | 作用 | 参数 | 类型 |
---|---|---|---|
environment | 在初始化配置文件中的插件之后,在准备编译器环境时调用 | —— | SyncHook |
afterEnvironment | environment编译器环境设置完成后,在挂钩之后立即调用 | —— | SyncHook |
entryOption | 在处理完webpack选项中的entry配置后调用 | context, entry | SyncBailHook |
afterPlugins | 在设置初始内部插件集之后调用 | compiler | SyncHook |
afterResolvers | 解析器设置完成后触发 | compiler | SyncHook |
initialize | 在初始化编译器对象时调用 | —— | SyncHook |
beforeRun | 在运行编译器之前添加一个挂钩 | compiler | AsyncSeriesHook |
run | 开始阅读之前,请先钩住编译器records | compiler | AsyncSeriesHook |
watchRun | 在触发新的编译之后但实际开始编译之前,在监视模式下执行插件 | compiler | AsyncSeriesHook |
normalModuleFactory | 在创建NormalModuleFactory之后调用 | normalModuleFactory | SyncHook |
contextModuleFactory | 创建ContextModuleFactory后运行插件 | contextModuleFactory | SyncHook |
beforeCompile | 创建编译参数后执行插件 | compilationParams | AsyncSeriesHook |
compile | beforeCompile在创建新编译之前,在之后立即调用 | compilationParams | SyncHook |
thisCompilation | 在初始化编译时执行,恰好在发出compilation事件之前执行 | compilation, compilationParams | SyncHook |
compilation | 创建编译后运行插件 | compilation, compilationParams | SyncHook |
make | 在完成编译之前执行 | compilation | AsyncParallelHook |
afterCompile | 在完成并密封编译后调用 | compilation | AsyncSeriesHook |
shouldEmit | 在释放资产之前调用。应该返回一个布尔值,告诉是否发出 | compilation | SyncBailHook |
emit | 在将资产释放到输出目录之前立即执行 | compilation | AsyncSeriesHook |
afterEmit | 在将资产释放到输出目录后调用 | compilation | AsyncSeriesHook |
assetEmitted | 当资产被放出时执行。提供对有关发出的资产的信息的访问,例如其输出路径和字节内容 | file, info | AsyncSeriesHook |
done | 编译完成后执行 | stats | AsyncSeriesHook |
additionalPass | 该挂钩使您可以再进行一次构建 | —— | AsyncSeriesHook |
failed | 如果编译失败则调用 | error | SyncHook |
invalid | 当监视编译无效时执行 | fileName, changeTime | SyncHook |
watchClose | 监视编译停止时调用 | —— | SyncHook |
infrastructureLog | 当通过infrastructureLoggingoption在配置中启用时,允许使用基础结构日志记录 | name, type, args | SyncBailHook |
log | 允许登录到统计启用时,看到stats.logging,stats.loggingDebug和stats.loggingTrace选项。 | origin, logEntry | SyncBailHook |
# tapable 类型说明
基本的钩子:这个钩子只会简单的调用每个 tap 进去的函数(钩子类名没有waterfall, Bail, 或者 Loop 的)
Waterfall:一个 waterfall 钩子,也会调用每个tap进去的函数;不同的是:它会将前一个函数的返回的值作为形式参数传递到下一个函数
Bail:Bail 钩子允许提前退出,当任何一个 tap 进去的函数返回任意值时,bail 钩子会停止执行其他 tap 的函数(类似 Promise.race())
Loop:当 loop 钩子中的插件有一个返回一个未定义( non-undefined )的值时,钩子将从第一个插件重新启动。直到所有插件返回 undefined
Sync:一个同步钩子只能 tap 同步函数,不然会报错
AsyncSeries:一个 async-series 钩子,可以 tap 同步钩子, 基于回调的钩子和一个基于promise的钩子,它会按顺序的调用每个方法
AsyncParallel:一个 async-parallel 钩子跟上面的 async-series 一样,不同的是它会把异步钩子并行执行(并行执行就是把异步钩子全部一起开启,不按顺序执行)
# webpack 回调参数说明
参数类型 | 说明 |
---|---|
context | 基本目录,一个绝对路径,用于从配置中解析入口点和加载器(默认情况下,使用当前目录) |
entry | 打包入口:每个HTML页面一个入口点;SPA:一个入口点;MPA:多个入口点 |
compiler | webpack 编译对象(只有一个),可在该对象上添加 事件钩子 |
compilation | webpack 在各个生命周期编译构建 bundle 的产物 |
stats | —— |
# 如何编写 plugin
实现 html-webpack-plugin
- 确定需要操作的 webpack 生命周期,可读取打包生成的 chunk(emit/afterEmit)
- 确定需要传入的参数(template/filename ...)
- 取一个名字(HtmlInjectPlugin)
开始编写
- webpack.config.js 中
const HtmlInjectPlugin = require('./HtmlInjectPlugin.js')
module.exports = {
mode: 'development',
entry: './src/index.js',
plugins: [
new HtmlInjectPlugin({
template: './index.html',
filename: 'index.html'
})
]
}
2
3
4
5
6
7
8
9
10
11
12
- HtmlInjectPlugin.js
cheerio
cheerio 是 jquery 核心功能的一个快速灵活而又简洁的实现,主要是为了用在服务器端需要对 DOM 进行操作的地方
const cheerio = require('cheerio')
const fs = require('fs')
const path = require('path')
module.exports = class HtmlInjectPlugin {
constructor(options) {
// 保存传入的配置
// 在 webpack.config.js 中 new 对象时传入的参数
this.options = options
}
apply (compiler) {
// 在 afterEmit 钩子中注册事件
compiler.hooks.afterEmit.tap('HtmlInjectPlugin', (compilation) => {
// 获取整个打包输入的目录
let outputPath = compilation.options.output.path
// 读取作为模板的 index.html
let template = fs.readFileSync(path.resolve(process.cwd(), this.options.template), 'utf-8')
// 将 html 模板 转为 DOM
let $ = cheerio.load(template)
// 遍历最终生成的 chunk 文件名
Object.keys(compilation.assets).forEach((file) => {
// 根据 chunk 名动态创建 script 标签,并插入到 body 下
$('body').append($(`<script src="./${file}"></script>`))
})
// 将 DOM 转换成 文本并写入 webpack 输入目录中
fs.writeFileSync(path.join(outputPath, this.options.filename), $.html())
})
}
}
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
整个流程可概括为:
- 保存传入的配置
- 在 afterEmit 钩子中注册事件
- 获取整个打包输入的目录
- 读取作为模板的 index.html,并转为 DOM
- 遍历最终生成的 chunk 文件名,根据 chunk 名动态创建 script 标签,并插入到 body 下
- 将修改后的 DOM 转换成 文本,并写入 webpack 输入目录中
至此一个简单的 webpack plugin 就完成了
# 实现一个 mini-webpack
# package.json 注册全局命令
- package.json文件中新增
acli 为使用的命令,将会执行 ./bin/acli.js 文件
"bin": {
"acli": "./bin/acli.js"
},
2
3
在package.json 当前目录下新建 bin/acli.js 文件
- 第一行 #!/usr/bin/env node是必须的
- 改行的作用是告知使用的环境是 node
#!/usr/bin/env node
console.log('hello world')
2
注册全局命令
- 方式一:发布包之后使用
npm install xxx -g
- 方式二:使用 npm link(将一个任意位置的npm包链接到全局执行环境)
- 方式一:发布包之后使用
此时就可以在全局使用命令
acli
# 修改 acli.js
- 读取 webpack.config.js 配置文件
- 引入 自定义 编辑器
- 新建对象并开始执行打包任务
#!/usr/bin/env node
const path = require('path')
const fs = require('fs')
// 引入 webpack.config.js 配置文件,执行 acli 命令就是所在工作目录
let config = require(path.resolve('webpack.config.js'))
// 引入自定义 打包工具,等会将会实现
let Compiler = require('../lib/Compiler')
let comipler = new Compiler(config)
comipler.start() // 开始打包构建
2
3
4
5
6
7
8
9
10
11
# 实现 Compiler
- 新建 lib/Compiler.js
保存传入的配置
const path = require('path')
module.exports = class Compiler {
constructor(config) {
this.config = config
this.entry = config.entry || './src/index.js'
this.root = config.root || process.cwd()
this.rules = config.module.rules || []
this.output = config.output || { filename: 'bundle.js', path: path.resolve(this.root, 'dist') }
}
start () {
// TODO
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
添加 babel,从入口文件开始分析
递归读取依赖的文件
将 require 修改更名为 __webpack_require__
const path = require('path')
const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
module.exports = class Compiler {
constructor(config) {
this.config = config
this.entry = config.entry || './src/index.js'
this.root = config.root || process.cwd()
this.rules = config.module.rules || []
this.output = config.output || { filename: 'bundle.js', path: path.resolve(this.root, 'dist') }
this.modules = {} // 保存以读取的模块
}
getSource (filePath) {
return fs.readFileSync(filePath, 'utf-8')
}
start () {
this.depAnalyse(this.entry) // 从入口开始分析
}
depAnalyse (entry) {
let filePath = path.resolve(this.root, entry)
// 读取文件
let source = this.getSource(filePath)
// 转换成 ast,方便分析结构
let ast = parser.parse(source)
let root = this.root
let dependenes = []
// 遍历 ast 节点
traverse(ast, {
CallExpression (p) {
const { name } = p.node.callee
if (name === 'require') { // 获取 require 的路径
let oldPath = p.node.arguments[0].value
// 将 加载路径修改为从工作目录开始的相对路径
oldPath = `./${path.relative(root, path.resolve(path.dirname(path.resolve(root, entry)), oldPath))}`.replace(/\\+/g, '/')
// 将改文件中的依赖保存
dependenes.push(oldPath)
p.node.arguments[0].value = oldPath
// 将 require 更名为 __webpack_require__
p.node.callee.name = '__webpack_require__'
}
}
})
// 递归解析依赖
dependenes.forEach((path) => {
this.depAnalyse(path)
})
// 将 ast 还原为代码
let code = generator(ast).code
// 保存已加载的代码
this.modules[entry] = code
}
}
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
文件解析完成后打包
使用 ejs 模板来拼接 bundle.js
const path = require('path')
const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const ejs = require('ejs')
module.exports = class Compiler {
constructor(config) {
this.config = config
this.entry = config.entry || './src/index.js'
this.root = config.root || process.cwd()
this.rules = config.module.rules || []
this.output = config.output || { filename: 'bundle.js', path: path.resolve(this.root, 'dist') }
this.modules = {} // 保存以读取的模块
}
getSource (filePath) {
return fs.readFileSync(filePath, 'utf-8')
}
start () {
this.depAnalyse(this.entry) // 从入口开始分析
}
depAnalyse (entry) {
let filePath = path.resolve(this.root, entry)
// 读取文件
let source = this.getSource(filePath)
// 转换成 ast,方便分析结构
let ast = parser.parse(source)
let root = this.root
let dependenes = []
// 遍历 ast 节点
traverse(ast, {
CallExpression (p) {
const { name } = p.node.callee
if (name === 'require') { // 获取 require 的路径
let oldPath = p.node.arguments[0].value
// 将 加载路径修改为从工作目录开始的相对路径
oldPath = `./${path.relative(root, path.resolve(path.dirname(path.resolve(root, entry)), oldPath))}`.replace(/\\+/g, '/')
// 将改文件中的依赖保存
dependenes.push(oldPath)
p.node.arguments[0].value = oldPath
// 将 require 更名为 __webpack_require__
p.node.callee.name = '__webpack_require__'
}
}
})
// 递归解析依赖
dependenes.forEach((path) => {
this.depAnalyse(path)
})
// 将 ast 还原为代码
let code = generator(ast).code
// 保存已加载的代码
this.modules[entry] = code
// TODO emit之前应该有 loader 处理
this.emit() // 依赖遍历完成,开始生成 bundle.js 文件
},
emit () {
let template = fs.readFileSync(path.resolve(__dirname, '../template/index.ejs'), 'utf-8') // 加载的 index.ejs 文件内容见下方
let result = ejs.render(template, {
modules: this.modules,
entryPath: this.entry
})
fs.writeFileSync(path.join(this.output.path, this.output.filename), result)
}
}
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
index.ejs 模板内容
(() => { // webpackBootstrap
var __webpack_modules__ = ({
<%for (let key in modules) {%>
"<%- key%>":
((module, exports, __webpack_require__) => {
eval(`<%- modules[key]%>`);
}),
<%}%>
});
var __webpack_module_cache__ = {};
function __webpack_require__ (moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
var __webpack_exports__ = __webpack_require__(`<%- entryPath%>`);
})();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
至此,从入口遍历依赖并且生成一个 bundle.js
文件的简单 webpack
功能已经实现了
何时使用loader
- 根据我们对 loader 功能的认识:加载文件、转换文件
- 加载文件 主要是对非
js
文件的处理,这里我们不做讲解 - 转换文件 功能是我们这次将要在
mini-webpack
上实现的 rules
匹配规则顺序是从前向后匹配,如果匹配到了,loader
执行顺序从后往前。
const path = require('path')
const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const ejs = require('ejs')
module.exports = class Compiler {
constructor(config) {
this.config = config
this.entry = config.entry || './src/index.js'
this.root = config.root || process.cwd()
this.rules = config.module.rules || []
this.output = config.output || { filename: 'bundle.js', path: path.resolve(this.root, 'dist') }
this.modules = {} // 保存以读取的模块
this.cacheLoaders = {} // 缓存加载的 loader
}
getSource (filePath) {
return fs.readFileSync(filePath, 'utf-8')
}
start () {
this.depAnalyse(this.entry) // 从入口开始分析
}
depAnalyse (entry) {
let filePath = path.resolve(this.root, entry)
// 读取文件
let source = this.getSource(filePath)
// 转换成 ast,方便分析结构
let ast = parser.parse(source)
let root = this.root
let dependenes = []
// 遍历 ast 节点
traverse(ast, {
CallExpression (p) {
const { name } = p.node.callee
if (name === 'require') { // 获取 require 的路径
let oldPath = p.node.arguments[0].value
// 将 加载路径修改为从工作目录开始的相对路径
oldPath = `./${path.relative(root, path.resolve(path.dirname(path.resolve(root, entry)), oldPath))}`.replace(/\\+/g, '/')
// 将改文件中的依赖保存
dependenes.push(oldPath)
p.node.arguments[0].value = oldPath
// 将 require 更名为 __webpack_require__
p.node.callee.name = '__webpack_require__'
}
}
})
// 递归解析依赖
dependenes.forEach((path) => {
this.depAnalyse(path)
})
// 将 ast 还原为代码
let code = generator(ast).code
// 保存已加载的代码
this.modules[entry] = this.loadAndRunLoaders(code, filePath) // 将源代码交给匹配上的 loader 处理
this.emit() // 依赖遍历完成,开始生成 bundle.js 文件
},
emit () {
let template = fs.readFileSync(path.resolve(__dirname, '../template/index.ejs'), 'utf-8')
let result = ejs.render(template, {
modules: this.modules,
entryPath: this.entry
})
fs.writeFileSync(path.join(this.output.path, this.output.filename), result)
}
loadAndRunLoaders (code, filePath) {
for (let i = 0; i < this.rules.length; i++) {
let { test, use } = this.rules[i]
if (test.test(filePath)) { // 如果规则匹配上了
if (Array.isArray(use)) { // 数组
for (let j = use.length - 1; j >= 0; j--) { // 从后往前执行
let loader = use[j]
code = this.cacheLoader(loader, code)
}
} else if (typeof use === 'string') {
code = this.cacheLoader(use, code)
} else if (typeof use === 'object') {
code = this.cacheLoader(use.loader, code, use.options)
}
}
}
return code
}
cacheLoader (loader, code, options = undefined) {
if (!this.cacheLoaders[loader]) {
this.cacheLoaders[loader] = require(path.resolve(this.root, loader))
}
code = this.cacheLoaders[loader].call({ query: options }, code)
return code
}
}
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
何时使用plugin
- 在
Webpack
运行的生命周期中会广播出许多事件,Plugin
可以监听这些事件,在合适的时机通过Webpack
提供的API
改变输出结果 - 根据 plugin 的功能,在
mini-webpack
中我们需要在不用处理阶段抛出事件 - 这里主要用
tapable
来实现
const path = require('path')
const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const ejs = require('ejs')
const { SyncHook } = require('tapable')
module.exports = class Compiler {
constructor(config) {
this.config = config
this.entry = config.entry || './src/index.js'
this.root = config.root || process.cwd()
this.rules = config.module.rules || []
this.output = config.output || { filename: 'bundle.js', path: path.resolve(this.root, 'dist') }
this.modules = {} // 保存以读取的模块
this.cacheLoaders = {} // 缓存加载的 loader
// 以同步钩子举例
this.hooks = {
entryOption: new SyncHook(), // options 合并时触发
run: new SyncHook(), // 开始从入口文件分析前触发
compile: new SyncHook(), // 开始编译
make: new SyncHook(), // 编译时触发
emit: new SyncHook(), // 全部文件解析完成
done: new SyncHook(), // 整个项目构建完成
}
if (Array.isArray(config.plugins)) {
config.plugins.forEach(plusin => plusin.apply(this))
}
this.hooks.entryOption.call()
}
getSource (filePath) {
return fs.readFileSync(filePath, 'utf-8')
}
start () {
this.hooks.run.call()
this.depAnalyse(this.entry) // 从入口开始分析
}
depAnalyse (entry) {
let filePath = path.resolve(this.root, entry)
// 读取文件
let source = this.getSource(filePath)
// 转换成 ast,方便分析结构
let ast = parser.parse(source)
let root = this.root
let dependenes = []
// 遍历 ast 节点
traverse(ast, {
CallExpression (p) {
const { name } = p.node.callee
if (name === 'require') { // 获取 require 的路径
let oldPath = p.node.arguments[0].value
// 将 加载路径修改为从工作目录开始的相对路径
oldPath = `./${path.relative(root, path.resolve(path.dirname(path.resolve(root, entry)), oldPath))}`.replace(/\\+/g, '/')
// 将改文件中的依赖保存
dependenes.push(oldPath)
p.node.arguments[0].value = oldPath
// 将 require 更名为 __webpack_require__
p.node.callee.name = '__webpack_require__'
}
}
})
// 递归解析依赖
dependenes.forEach((path) => {
this.depAnalyse(path)
})
// 将 ast 还原为代码
let code = generator(ast).code
// 保存已加载的代码
this.modules[entry] = this.loadAndRunLoaders(code, filePath) // 将源代码交给匹配上的 loader 处理
this.hooks.compile.call()
this.emit() // 依赖遍历完成,开始生成 bundle.js 文件
},
emit () {
this.hooks.emit.call()
let template = fs.readFileSync(path.resolve(__dirname, '../template/index.ejs'), 'utf-8')
let result = ejs.render(template, {
modules: this.modules,
entryPath: this.entry
})
fs.writeFileSync(path.join(this.output.path, this.output.filename), result)
this.hooks.done.call()
}
loadAndRunLoaders (code, filePath) {
for (let i = 0; i < this.rules.length; i++) {
let { test, use } = this.rules[i]
if (test.test(filePath)) { // 如果规则匹配上了
if (Array.isArray(use)) { // 数组
for (let j = use.length - 1; j >= 0; j--) { // 从后往前执行
let loader = use[j]
code = this.cacheLoader(loader, code)
}
} else if (typeof use === 'string') {
code = this.cacheLoader(use, code)
} else if (typeof use === 'object') {
code = this.cacheLoader(use.loader, code, use.options)
}
}
}
return code
}
cacheLoader (loader, code, options = undefined) {
if (!this.cacheLoaders[loader]) {
this.cacheLoaders[loader] = require(path.resolve(this.root, loader))
}
code = this.cacheLoaders[loader].call({ query: options }, code)
return code
}
}
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