玩转 webpack 工作原理,手写一个 my-webpack
项目搭建
新建文件夹 my-webpack ,使用开发工具打开这个项目目录。
-
执行
npm init -y
,生成 package.json -
新建 bin 目录
bin 目录代表着可执行文件,在 bin 下新建主程序执行入口 my-webpack.js
#!/usr/bin/env node // 上面代码用来声明 执行环境 console.log("学习好嘞 我好困");
-
在 backage.json 配置 bin 字段
{ "bin": { // 声明指令 以及指令执行的文件 "my-webpack": "./bin/my-webpack.js" }, }
-
执行 npm link 将当前项目链接到全局包中
我们要想像 webpack 一样 全局使用
webpack
指令,必须要将包链接到全局中 -
命令行执行
my-webpack
,发现程序成功执行
分析Bundle
新建一个项目完成一次简单的打包,对打包后得bundle文件进行分析
- 搭建webpack项目
- 新建 demo 目录,并在 demo 目录下执行
yarn init -y
- 安装 webpack
yarn add webpack webpack-cli -D
- 新建业务模块 src/index.js、src/moduleA.js、src/moduleB.js
// src/index.js const moduleA = require("./moduleA.js") console.log("index.js,成功导入" + moduleA.content); // src/moduleA.js const moduleB = require("./moduleB.js") console.log("moduleA模块,成功导入" + moduleB.content); module.exports = { content: "moduleA模块" } // src/moduleB.js module.exports = { content: "moduleB模块" }
- 新建 webpack.config.js 配置打包参数
const path = require("path") module.exports = { entry: "./src/index.js", output: { filename: "bundle.js", path: path.resolve("./build") }, mode: "development" }
- 新建 build-script 脚本,并执行
npm run build
// package.json { "scripts": { "build": "webpack" }, }
- 新建 demo 目录,并在 demo 目录下执行
- 对打包后的 build/bundle.js 进行分析
(() => { /** * 所有模块 * * 所有模块在 __webpack_modules__ 以键值对的形式等待加载,模块对象键为模块ID(路径) 值为模块的内容。 * 模块内通过webpack封装的 __webpack_require__ 函数进行加载其他模块 */ var __webpack_modules__ = ({ "./src/index.js": (function (__unused_webpack_module, __unused_webpack_exports, __webpack_require__) { eval("const moduleA = __webpack_require__(/*! ./moduleA */ \"./src/moduleA.js\")\r\nconsole.log(\"this\", this);\r\nconsole.log(\"index.js,成功导入\" + moduleA.content);\n\n//# sourceURL=webpack://demo/./src/index.js?"); }), "./src/moduleA.js": ((module, __unused_webpack_exports, __webpack_require__) => { eval("const moduleB = __webpack_require__( \"./src/moduleB.js\")\r\nconsole.log(\"moduleA模块,成功导入\" + moduleB.content);\r\nmodule.exports = {\r\n content: \"moduleA模块\"\r\n}\n\n//# sourceURL=webpack://demo/./src/moduleA.js?"); }), "./src/moduleB.js": ((module) => { eval("module.exports = {\r\n content: \"moduleB模块\"\r\n}\n\n//# sourceURL=webpack://demo/./src/moduleB.js?"); }) }); /** * 模块缓存 * * 每次加载新模块后会添加缓存,下次加载相同模块前会直接使用缓存,避免重复加载。 */ var __webpack_module_cache__ = {}; /** * webpack封装的加载函数 * * 该函数根据模块ID加载模块,加载前先判断 是否模块缓存中是否有缓存,有的话直接使用缓存, * 没有则添加缓存并从所有模块(__webpack_modules__)中,加载这个模块 */ 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].call(module.exports, module, module.exports, __webpack_require__); // 返回模块最终导出的数据 return module.exports; } /** * 这是bundle.js执行起点,通过 __webpack_require__ 导入配置入口文件的模块 * * 入口文件模块如果有依赖通过__webpack_require__ 继续导入,然后执行入口文件模块中的代码 * "./src/index.js" 通过__webpack_exports__ 导入 "./src/moduleA.js","./src/moduleA.js" 通过__webpack_exports__ 导入"./src/moduleB.js" */ var __webpack_exports__ = __webpack_require__("./src/index.js"); /** * 执行流程 * __webpack_require__ 会加载 __webpack_modules__中的某个模块,这个模块的执行方法会 递归调用 __webpack_require__ 加载下个模块知道所有模块加载完成 */ })();
对 _webpack_require_ 进行分析
_webpack_require_ 可以说是bundle中非关键的狗工具函数,通过递归调用这个函数导入所有依赖var __webpack_modules__ = ({ "./src/index.js": (function (__unused_webpack_module, __unused_webpack_exports, __webpack_require__) { eval("const moduleA = __webpack_require__(/*! ./moduleA */ \"./src/moduleA.js\")\r\nconsole.log(\"this\", this);\r\nconsole.log(\"index.js,成功导入\" + moduleA.content);\n\n//# sourceURL=webpack://demo/./src/index.js?"); }), ...... }) function __webpack_require__(moduleId) { // 缓存处理 var cachedModule = __webpack_module_cache__[moduleId]; if (cachedModule !== undefined) {return cachedModule.exports;} // 新建module对象并添加缓存 var module = __webpack_module_cache__[moduleId] = {exports: {}}; /** * webpack基于node环境 * 执行__webpack_modules__的模块,传入module、module.exports并改变this指向, *这样就可以快乐的在模块中使用 this、module等变量了 */ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__); // 返回模块最终导出的数据 return module.exports; }
依赖模块分析
_webpack_modules_ 保存了所有模块,最复杂的是所有模块的依赖关系,这个涉及js、nod的基础以及抽象语法树的概念。 webpack的工作就是模块分析、然后通过各种plugin、loader对文件进行处理最终生成 _webpack_modules_,实现webpack最大的难点就是模块分析。
这里为入口文件 "./src/index.js" 为例,分析模块依赖生成 _webpack_modules_
开始解析
回到之前的项目 my-webpack 主程序执行文件 bin/my-webpack.js
-
读取打包配置文件、获取打包配置参数(入口、出口等)
// my-webpack/bin/my-webpack.js #!/usr/bin/env node const path = require("path") // 1.导入打包配置文件,获取打包配置 // 使用my-webpack工具对其他项目打包时,需要获取项目打包配置文件绝对路径 const config = require(path.resolve("webpack.config.js")) console.log("config", config);
-
回到 DEMO 项目,终端中输入指令 my-webpack 使用自己打包工具
成功获取打包配置参数
代码解析器
使用解析器根据配置参数 解析项目代码
-
在工具 my-webpack 目录下新建 lib/Compiler.js
使用面对对象思想,新建 Compiler 类
// lib/Compiler.js const path = require("path") const fs = require("fs") class Compiler { constructor(config) { this.config = config this.entry = config.entry // process.cwd可以获取node执行的文件绝对路径 // 获取被打包项目的文件路径 this.root = process.cwd() } // 根据传入的文件路径,解析文件模块 depAnalyse(modulePath) { const file = this.getSource(modulePath) console.log("file", file); } // 传入文件路径,读取文件并返回 getSource(path) { // 以“utf-8”编码格式 读取文件并返回 return fs.readFileSync(path, "utf-8") } // 执行解析器 start() { // 传入入口文件绝对路径 开始解析依赖 // 注意:此处路径不能使用 __dirname,__dirname代表 工具库"my-webpack"根目录的绝 对路径 而不是要被打包项目的根目录路径 this.depAnalyse(path.resolve(this.root, this.entry)) } } module.exports = Compiler // bin/my-webpack.js // 2. 导入解析器并新建实例 并执行构解析器 const Compiler = require("../lib/Compiler") new Compiler(config).start()
-
在被打包项目 DEMO 中,重新执行
my-webpack
成功读取入口文件
抽象语法树
顺利读取到模块代码,可将模块代码转换成抽象语法树(ast),将 require 语法替换为自己封装加载函数 _webpack_require_
代码在线转为抽象语法树: astexplorer.net/
在打包项目要进行抽象语法树的生成和语法装换需要用到两个包,分别是 @babel/parser、@babel/traverse
-
安装
npm i @babel/parser @babel/traverse -S
-
生成ast,并转换语法
// my-webpack/lib/Compiler.js // 导入解析器 const parser = require("@babel/parser") // 导入转换器 es6导出需要.defult const traverse = require("@babel/traverse").default class Compiler { depAnalyse(modulePath) { const code = this.getSource(modulePath) // 将代码解析为ast抽象语法树 const ast = parser.parse(code) /** * traverse用来转换语法,它接收两个参数 * - ast:转换前的抽象语法树节点树 * - options: 配置对象,里面包含各种钩子回调,traverse遍历语法树节点当节点满足某个钩子条件时 该钩子会被触发 * - CallExpression: */ traverse(ast, { // 当某个抽象语法树节点类型为 CallExpression(表达式调用),会触发该钩子 CallExpression(p) { console.log("该类型语法节点的 名称", p.node.callee.name); }, }) } }
-
回到 DEMO 项目,执行
my-webpack
使用自己的工具库重新打包 -
替换代码中的关键字
// my-webpack/lib/Compiler.js traverse(ast, { // 当某个抽象语法树节点类型为 CallExpression(表达式调用),会触发该钩子 CallExpression(p) { console.log("该类型语法节点的 名称", p.node.callee.name); if (p.node.callee.name === 'require') { // 修改require p.node.callee.name = "__webpack_require__" // 修改当前模块 依赖模块的路径 使用node访问资源必须是 "./src/XX" 的形式 let oldValue = p.node.arguments[0].value // 将"./xxx" 路径 改为 "./src/xxx" oldValue = "./" + path.join("src", oldValue) // 避免window的路径出现 "\" p.node.arguments[0].value = oldValue.replace(/\\/g, "/") console.log("路径", p.node.arguments[0].value); } }, })
-
回到 DEMO ,执行
my-webpack
重新打包查看控制台输出
生成源码
对AST进行处理后,可以通过 @babel/generator 生成代码
- 安装
npm i @babel/generator -S
- 解析完AST后 构建代码
// my-webpack/lib/Compiler.js // 导入生成器 const generator = require("@babel/generator").default class Compiler { depAnalyse(modulePath) { traverse(ast, { ...... }) // 将抽象语法树生成代码 const sourceCode = generator(ast).code console.log("源码", sourceCode); } }
- 回到 DEMO, 执行
my-webpack
重新构建
递归构建依赖
前面在 Compiler 中,我们通过 depAnalyse 方法 传入模块路径将 ./src/index.js 模块中的语法进行转换、依赖模块的路径进行转换。这只解析了 "./src/index.js" 这一层,它的依赖模块并没有解析构建,所以我们要递归执行 depAnalyse 解析所有模块
class Compiler {
depAnalyse(modulePath) {
// 当前模块依赖数组,存放当前模块所以依赖的路径
let dependencies = []
traverse(ast, {
CallExpression(p) {
if (p.node.callee.name === 'require') {
p.node.callee.name = "__webpack_require__"
let oldValue = p.node.arguments[0].value
oldValue = "./" + path.join("src", oldValue)
p.node.arguments[0].value = oldValue.replace(/\\+/g, "/")
// 每解析require,就将依赖模块的路径放入 dependencies 中
dependencies.push(p.node.arguments[0].value)
}
},
})
const sourceCode = generator(ast).code
console.log("sourceCode", sourceCode);
// 如果当前模块 有其他依赖的模块就 递归调用 depAnalyse 继续向下解析代码 直到没有任何依赖为止
dependencies.forEach(e => {
// 传入模块的绝对路径
this.depAnalyse(path.resolve(this.root, depPath))
})
}
}
获取所有模块
webpack打包的结果是将所有模块 以模块ID+模块执行函数的形式放一起的
class Compiler {
constructor(config) {
......
// 存放打包后的所有模块
this.modules = {}
}
depAnalyse(modulePath) {
traverse(ast, {.....})
const sourceCode = generator(ast).code
// 模块得ID处理为相对路径
let modulePathRelative = "./" + path.relative(this.root, modulePath)
// 将路径中 "\" 替换为 "/"
modulePathRelative = modulePathRelative.replace(/\\+/g, "/")
// 当前模块解析完毕 将其添加进modules
this.modules[modulePathRelative] = sourceCode
dependencies.forEach(depPath => {......})
}
start() {
this.depAnalyse(path.resolve(this.root, this.entry))
// 获取最终解析结果
console.log("module", this.modules);
}
}
在 DEMO 执行 my-webpack
重新查看打包结果
到此为止我们就通过 Compiler类 ,递归的调用 depAnalyse 方法解析项目所有模块,获取了 modules 成功构建了整个项目。
生成 _webpack_modules_
使用 webpack 打包生成 _webpack_modules_,所以模块是模块ID+模块执行函数存在的。我们可以使用 模板引擎(这是以ejs示例)将模块代码处理成模块执行函数
-
安装
npm i ejs -S
-
新建渲染模板 my-webpack/template/output.ejs
(() => { var __webpack_modules__ = ({ // 使用模板语法进行遍历 k就是模块ID <% for (let k in modules) {%> "<%- k %>": (function (module, exports, __webpack_require__) { eval(`<%- modules[k]%>`); }), <%}%> }); 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].call(module.exports, module, module.exports, __webpack_require__); return module.exports; } var __webpack_exports__ = __webpack_require__("<%-entry%>"); }) ();
-
使用模板渲染code,并写入到出口文件中
// my-webpack/lib/Compiler.js // 导入ejs const ejs = require("ejs") class Compiler { constructor(config) {...} depAnalyse(modulePath) {....} getSource(path) {...} start() { this.depAnalyse(path.resolve(this.root, this.entry)) this.emitFile() } // 生成文件 emitFile() { // 读取代码渲染依据的模板 const template = this.getSource(path.resolve(__dirname, "../template/output.ejs")) // 传入渲染模板、模板中用到的变量 let result = ejs.render(template, { entry: this.entry, modules: this.modules }) // 获取输出路径 let outputPath = path.join(this.config.output.path,this.config.output.filename) // 生成bundle文件 fs.writeFileSync(outputPath,result) } }
-
在 DEMO 项目中,重新执行
my-webpack
注意 DEMO 项目中 webpack.config.js 得输出路径为 DEMO/build/bundle.js。你必须确保 DEMO 有 build 文件夹,否则使用 my-webpack 写入文件时可能会报错。
自定义loader
前面我们实现了一个打包工具 my-webpack,它现在只能处理js文件。如果要处理其他文件或者对js代码进行操作需要借助 loader . loader 主要的功能就是将一段匹配规则的代码,进行加工处理生成最终代码进行输出。
loader的使用步骤
- 装 loader 包
- 在webpack配置文件,配置到 module 的 rules中即可
- 某些 loader 还需要配置一些其他参数(可选)
什么是loader
loader 模块就是一个函数,webpack 会向 loader 函数传入资源,loader 对这些资源进行处理后再将其返回出去。
我们在 webpack 项目中实现一个 loader
-
对 src/moduleB.js 导出内容进行处理
将导出内容含有 "moduleB" 替换为 "MODULE-B"
-
在 DEMO 项目中新建 loader/index.js
// loader本质就是一个函数 module.exports = function (resource) { // 对匹配到的资源进行处理并返回出去 return resource.replace(/moduleB/g, "MODULE-B") }
-
在 DEMO 的 webpack.config.js 中,配置自定义 loader
// DEMO/webpack.config.js module.exports = { module: { rules: [ { test: /.js$/g, use: "./loader/index.js" } ] } }
-
执行
npm run build
,使用 webpack 打包项目 并执行打包后的代码
loader执行顺序
使用loader处理某个资源会有两种情况:单个匹配规则多个loader、多个匹配规则单个loader
单个匹配规则多个loader:
当单个匹配规则多个loader,loader执行顺序是从右侧到左侧执行
多个匹配规则单个loader:
当多个匹配规则单个loader,loader执行顺序是从下到上执行
loader种类
loader种类分为 前置、内联、正常、后置
这四种loader执行顺序分别为: 前置 > 内联 > 正常 > 后置
前置loader、后置loader
前置loader、后置loader 由 loader 的 enforce 字段控制,前置 loader 会在所有 loader 执行前执行,后置 loader 会在所有 loader 执行完成后执行
内联loader
使用内联 loader 解析文件,必须在 require 、 import 引入的资源前面加上 loader ,多个 loader 要用 !
隔开 例:
import Styles from 'style-loader!css-loader?modules!./styles.css'
获取options配置
大部分 loader 只需要在 rules 里注册一下就可以使用,也有一些涉及复杂功能的 loader 需要配置 options 参数。例如,生成图片资源的 url-loader 设置生成base64 文件大小的阀值。
在 loader 中,this可以获取到上下文及webpack中的配置,通过 this.getOptions 可以获取 options 参数
-
传入参数
module: { rules: [ { test: /.js$/g, use: { loader: "./loader/index.js", options: { target: /moduleB/g, replaceContent: "MD_B" } }, }, ] },
-
自定义 loader 中使用参数
// demo/loader/index.js const loaderUtils = require("loader-utils") // loader本质就是一个函数 module.exports = function (resource) { const { target, replaceContent } = this.getOptions() // 对匹配到的资源进行处理并返回出去 return resource.replace(target, replaceContent) }
-
执行
npm run build
,使用 webpack 打包项目 并执行打包后的代码
my-webpack添加loader功能
通过前面配置 loader 和手写 loader,可以发现 my-webpack添加 loader 功能主要是这几大步骤:
- 读取 webpack 配置文件的 module.rules 配置项,进行倒叙迭代(rules每项匹配规则按倒序匹配)
- 根据正则匹配到对应的文件类型,同时再批量导入 loader 函数
- 倒序迭代调用所有 loader 函数
- 最后返回处理的代码
代码部分(my-webpack/lib/Compiler.js)
- 获取配置文件中 配置的所有 loader
class Compiler { constructor(config) { ... // 获取所有loader this.rules = config.module && config.module.rules } ... }
- 声明 useLoader 方法,在解析依赖时调用,传入解析的源码和模块路径
class Compiler { ... depAnalyse(modulePath) { let code = this.getSource(modulePath) // 使用loader处理源码 code = this.useLoader(code, modulePath) const ast = parser.parse(code) ... } // 使用loader useLoader(code, modulePath) { // 未配置rules 直接返回源码 if (!this.rules) return code // 获取源码后 输出给loader ,倒序遍历loader for (let index = this.rules.length - 1; index >= 0; index--) { const { test, use } = this.rules[index] // 如果当前模块满足 loader正则匹配 if (test.test(modulePath)) { /** * 如果单个匹配规则,多个loader,use字段为数组。单个loader,use字段为字符串或对象 */ if (use instanceof Array) { // 倒序遍历loader 传入源码进行处理 for (let i = use.length - 1; i >= 0; i--) { let loader = use[i]; /** * loader 为字符串或对象 * 字符串形式: * use:["loader1","loader2"] * 对象形式: * use:[ * { * loader:"loader1", * options:.... * } * ] */ let loaderPath = typeof loader === 'string' ? loader : loader.loader // 获取loader的绝对路径 loaderPath = path.resolve(this.root, loaderPath) // loader 上下文 const options = loader.options const loaderContext = { getOptions() { return options } } // 导入loader loader = require(loaderPath) // 传入上下文 执行loader 处理源码 code = loader.call(loaderContext, code) } } else { let loaderPath = typeof loader === 'string' ? use : use.loader // 获取loader的绝对路径 loaderPath = path.resolve(this.root, loaderPath) // loader 上下文 const loaderContext = { getOptions() { return use.options } } // 导入loader let loader = require(loaderPath) // 传入上下文 执行loader 处理源码 code = loader.call(loaderContext, code) } } } return code } }
- 执行
my-webpack
,使用 my-webpack 打包项目 并执行打包后的代码
自定义plugin
webpack 的插件接口可以让使用者触及到编译过程,插件可以将处理函数注册到编译过程的不同事件节点生命周期钩子上,当执行每个钩子时,插件可以访问到编译的当前状态。
简而言之,自定义插件可以在 webpack 编译过程的声明周期钩子中,对源码进行处理,实现一些功能。
插件生命周期钩子
钩子 | 作用 | 参数 |
---|---|---|
entryOption | 在处理webpack选项的entry配置后调用 | context,entry |
afterPlugins | 在初始化内部插件列表后调用 | compiler |
beforeRun | 在运行Compiler之前调用 | compiler |
run | 在Compiler开始工作时调用 | compiler |
emit | 向asstes目录发射asstes时调用 | compilation |
done | 编译完成后调用 | stats |
... | ... | ... |
webpack插件的组成
- 一个js命名函数
- 在插件的 prototype 上定义一个 apply 方法
- 指定一个绑定到 webpack 自身的事件钩子
- webpack 内部那么多事件钩子都是通过 tabable(专注自定义事件触发与处理) 库实现的
- 处理 webpack 内部实例的特定数据
- 功能完成后调用 webpack 提供的回调
实现一个简单plugin
- demo 项目新建 demo/plugin/helloWorldPlugin.js
// 1.声明一个命名函数
module.exports = class HelloWorldPlugin {
// 2.函数的prototype上 必须要有apply方法
apply(compiler) {
// 3.通过hooks注册钩子回调
// 4.在done事件时,触发后面回调
compiler.hooks.done.tap("HelloWorldPlugin", (stats) => {
console.log("整个webpack打包结束");
})
// 5.在done事件时,触发后面回调
compiler.hooks.emit.tap("HelloWorldPlugin", (stats) => {
console.log("文件发射结束了");
})
}
}
- webpack.config.js 引入自定义插件
const path = require("path") // 导入HelloWorldPlugin const HelloWorldPlugin = require("./plugin/helloWorldPlugin") module.exports = { entry: "./src/index.js", output: { filename: "bundle.js", path: path.resolve("./build") }, module: { rules: [ { test: /.js$/g, use: [{ loader: "./loader/index.js", options: { target: /moduleB/g, replaceContent: "MD_B" } }] }, ] }, // 配置自定义插件 plugins: [ new HelloWorldPlugin() ], mode: "development" }
-
npm run build
重新打包 demo
-
实现 html-webpack-plugin
html-webpack-plugin 的作用就是复制指定的html模板,同时自动引入 bundle.js
如何实现? 1、编写一个自定义插件,注册 afterEmit 钩子 2、根据创建插件实例时传入的 template属性 来读取 html 模板 3、使用 cheerio插件 分析HTML,可以使用 jQuery 语法操作 html 4、遍历 webpack 打包生成的资源列表,如果有多个 bundle.js ,依次引入到 html 中。 5、输出生成的 html字符串 到 dist 目录中
- demo工程新建 src/index.html、新建 plugin/HTMLPlugin.js
// demo/plugin/HTMLPlugin.js const fs = require("fs") const cheerio = require("cheerio") module.exports = class HTMLPlugin { constructor(options) { /** * 1、通过配置对象获取HTML模板 * options.filename:模板输出的文件名 * options.template: 目标模板输入路径 */ this.options = options } // 2、plugin中,必须有apply方法 apply(compiler) { // 3、在afterEmit在资源发射结束时、获取bundle等资源 compiler.hooks.afterEmit.tap("HTMLPlugin", (compilation) => { // 4、读取传入的HTML目标模板,获取DOM结构字符串 const template = fs.readFileSync(this.options.template, "utf-8") /** * 5、`yarn add cheerio` 安装cheerio并引入进来 * 通过cheerio操作 html的DOM结构,引入bundle等资源 */ let $ = cheerio.load(template) console.log(Object.keys(compilation.assets)); // 6、遍历所有资源 一次引入到html中 Object.keys(compilation.assets).forEach(e => $('body').append(`<script src="./${e}"></script>`)) // 7、将新的HTML字符串输出到 dist目录中 fs.writeFileSync("./build/" + this.options.filename, $.html()) }) } }
- 配置插件
// demo/webpack.config.js // 导入HTMLPlugin const HTMLPlugin = require("./plugin/HTMLPlugin") module.exports = { // 配置自定义插件 plugins: [ new HTMLPlugin({ filename: "index.html", template: "./src/index.html" }), new HelloWorldPlugin(), ], }
npm run build
打包项目查看生成的 build/index.html compiler和compilation的区别- compiler对象 是值 webpack 在打包项目文件时,使用的工具
- compilation对象 是指 webpack 每次打包时,每个每个阶段打包的产物
my-webpack添加plugin功能
tapable
webpack内部通过各种事件流串联插件,打包处理项目文件。而这些事件流机制的核心就是通过 tapable 实现的,tapable 就类似与 node 的 events 库,核心原理也是发布订阅。
添加plugin功能
我们这里只做简单的演示,只在关键节点设置几个钩子函数。webpack内部的实现远比我们的复杂的多,毕竟光钩子函数就几十个。
-
安装
yarn add tapable
-
加载 plugin、声明钩子、执行钩子
// my-webpack/lib/Compiler.js // 导入 tabable const { SyncHook } = require("tapable") class Compiler { constructor(config) { ...... // 1、声明钩子 this.hooks = { compile: new SyncHook(), afterCompile: new SyncHook(), emit: new SyncHook(), afterEmit: new SyncHook(['modules']), done: new SyncHook() } // 2、获取所有插件对象,执行插件apply方法 if (Array.isArray(this.config.plugins)) { this.config.plugins.forEach(e => { // 传入compiler 实例,插件可以在apply方法中注册钩子 e.apply(this) }) } } start() { // 开始解析前,执行 compile 钩子 this.hooks.compile.call() this.depAnalyse(path.resolve(this.root, this.entry)) // 分析结束后,执行 afterCompile 钩子 this.hooks.afterCompile.call() // 资源发射前,执行 emit 钩子 this.hooks.emit.call() this.emitFile() // 资源发射完毕,执行 afterEmit 钩子 this.hooks.afterEmit.call() // 解析结束,执行 done 钩子 this.hooks.done.call() } }
-
在 helloWorldPlugin 注册钩子
// demo/plugin/helloWorldPlugin.js module.exports = class HelloWorldPlugin { apply(compiler) { compiler.hooks.done.tap("HelloWorldPlugin", (stats) => { console.log("整个webpack打包结束"); }) compiler.hooks.emit.tap("HelloWorldPlugin", (stats) => { console.log("文件发射了"); }) } }
-
执行
my-webpack
, 重新进行打包
最后
本文目的是通过实现webpack的基本功能,来了解webpack究竟 “是什么”以及它是怎么工作的,它的每一部分、每个功能可能会远比我们的更复杂。最后 贴上compiler 的代码
const path = require("path")
const fs = require("fs")
// 导入解析器
const parser = require("@babel/parser")
// 导入转换器 es6导出需要.defult
const traverse = require("@babel/traverse").default
// 导入生成器
const generator = require("@babel/generator").default
// 导入ejs
const ejs = require("ejs")
// 导入 tabable
const { SyncHook } = require("tapable")
class Compiler {
constructor(config) {
this.config = config
this.entry = config.entry
// process.cwd可以获取node执行的文件绝对路径
// 获取被打包项目的文件路径
this.root = process.cwd()
// 存放打包后的所有模块
this.modules = {}
// 获取所有loader
this.rules = config.module && config.module.rules
// 1、声明钩子
this.hooks = {
compile: new SyncHook(),
afterCompile: new SyncHook(),
emit: new SyncHook(),
afterEmit: new SyncHook(['modules']),
done: new SyncHook()
}
// 2、获取所有插件对象,执行插件apply方法
if (Array.isArray(this.config.plugins)) {
this.config.plugins.forEach(e => {
// 传入compiler 实例,插件可以在apply方法中注册钩子
e.apply(this)
})
}
}
// 根据传入的文件路径,解析文件模块
depAnalyse(modulePath) {
let code = this.getSource(modulePath)
// 使用loader处理源码
code = this.useLoader(code, modulePath)
// 将代码解析为ast抽象语法树
const ast = parser.parse(code)
// 当前模块依赖数组,存放当前模块所以依赖的路径
let dependencies = []
/**
* traverse用来转换语法,它接收两个参数
* - ast:转换前的抽象语法树节点树
* - options: 配置对象,里面包含各种钩子回调,traverse遍历语法树节点当节点满足某个钩子条件时 该钩子会被触发
* - CallExpression:
*/
traverse(ast, {
// 当某个抽象语法树节点类型为 CallExpression(表达式调用),会触发该钩子
CallExpression(p) {
if (p.node.callee.name === 'require') {
// 修改require
p.node.callee.name = "__webpack_require__"
// 修改当前模块 依赖模块的路径 使用node访问资源必须是 "./src/XX" 的形式
let oldValue = p.node.arguments[0].value
// 将"./xxx" 路径 改为 "./src/xxx"
oldValue = "./" + path.join("src", oldValue)
// 避免window的路径出现 "\"
p.node.arguments[0].value = oldValue.replace(/\\+/g, "/")
// 每解析require,就将依赖模块的路径放入 dependencies 中
dependencies.push(p.node.arguments[0].value)
}
},
})
const sourceCode = generator(ast).code
// 模块得ID处理为相对路径
let modulePathRelative = "./" + path.relative(this.root, modulePath)
// 将路径中 "\" 替换为 "/"
modulePathRelative = modulePathRelative.replace(/\\+/g, "/")
// 当前模块解析完毕 将其添加进modules
this.modules[modulePathRelative] = sourceCode
// 如果当前模块 有其他依赖的模块就 递归调用 depAnalyse 继续向下解析代码 直到没有任何依赖为止
dependencies.forEach(depPath => {
// 传入模块的绝对路径
this.depAnalyse(path.resolve(this.root, depPath))
})
}
// 传入文件路径,读取文件
getSource(path) {
// 以“utf-8”编码格式 读取文件并返回
return fs.readFileSync(path, "utf-8")
}
// 执行解析器
start() {
// 开始解析前,执行 compile 钩子
this.hooks.compile.call()
// 传入入口文件绝对路径 开始解析依赖
// 注意:此处路径不能使用 __dirname,__dirname代表 工具库"my-webpack"根目录的绝对路径 而不是要被打包项目的根目录路径
this.depAnalyse(path.resolve(this.root, this.entry))
// 分析结束后,执行 afterCompile 钩子
this.hooks.afterCompile.call()
// 资源发射前,执行 emit 钩子
this.hooks.emit.call()
this.emitFile()
// 资源发射完毕,执行 afterEmit 钩子
this.hooks.afterEmit.call()
// 解析结束,执行 done 钩子
this.hooks.done.call()
}
// 生成文件
emitFile() {
// 读取代码渲染依据的模板
const template = this.getSource(path.resolve(__dirname, "../template/output.ejs"))
// 传入渲染模板、模板中用到的变量
let result = ejs.render(template, {
entry: this.entry,
modules: this.modules
})
// 获取输出路径
let outputPath = path.join(this.config.output.path, this.config.output.filename)
// 生成bundle文件
fs.writeFileSync(outputPath, result)
}
// 使用loader
useLoader(code, modulePath) {
// 未配置rules 直接返回源码
if (!this.rules) return code
// 获取源码后 输出给loader ,倒序遍历loader
for (let index = this.rules.length - 1; index >= 0; index--) {
const { test, use } = this.rules[index]
// 如果当前模块满足 loader正则匹配
if (test.test(modulePath)) {
/**
* 如果单个匹配规则,多个loader,use字段为数组。单个loader,use字段为字符串或对象
*/
if (use instanceof Array) {
// 倒序遍历loader 传入源码进行处理
for (let i = use.length - 1; i >= 0; i--) {
let loader = use[i];
/**
* loader 为字符串或对象
* 字符串形式:
* use:["loader1","loader2"]
* 对象形式:
* use:[
* {
* loader:"loader1",
* options:....
* }
* ]
*/
let loaderPath = typeof loader === 'string' ? loader : loader.loader
// 获取loader的绝对路径
loaderPath = path.resolve(this.root, loaderPath)
// loader 上下文
const options = loader.options
const loaderContext = {
getOptions() {
return options
}
}
// 导入loader
loader = require(loaderPath)
// 传入上下文 执行loader 处理源码
code = loader.call(loaderContext, code)
}
} else {
let loaderPath = typeof loader === 'string' ? use : use.loader
// 获取loader的绝对路径
loaderPath = path.resolve(this.root, loaderPath)
// loader 上下文
const loaderContext = {
getOptions() {
return use.options
}
}
// 导入loader
let loader = require(loaderPath)
// 传入上下文 执行loader 处理源码
code = loader.call(loaderContext, code)
}
}
}
return code
}
}
module.exports = Compiler
转载自:https://juejin.cn/post/7033354994430853156