Webpack
webpack 流程
- webpack 使用 Tapable 作为事件中心,Tapable 可以定义、监听、触发事件
- webpack 将打包分成了 initialize -> run -> compiler -> compilation -> make -> afterCompile -> emit -> done 阶段
- webpack 在开始的时候就创建 Compiler 实例 compiler,将 webpack.config.js 的 options 赋值给 compiler.options
- 之后经历从 environment 到 initialize 阶段,之后调用 compiler 的 run 方法
- 在 compiler.compile 方法会创建 NormalModuleFactory + ContextModuleFactory 实例
- 在 compiler.compile 阶段创建 Compilation 实例
- EntryPlugin 监听了 make 事件,获取到入口文件路径,调用 compilation.addEntry,将路径传递给 compilation
- ------- compilation 拿到入口文件之后开始工作 ----------
- 广播 compilation addEntry 事件
- compilation 调用 NormalModuleFactory.create 方法,创建 module
- module 是 NormalModule 的实例,调用 module.build 方法,开始 build
- ------- module 是 NormalModule 的实例,多态,调用到 NormalModule.build 方法 ----------
- ------- parse ----------
- NormalModule.build 方法调用了 runLoaders,使用 loader 去解析文件,获取到 Buffer 数组,处理之后就可以拿到文件内容,赋值给 normal._source
- 使用 parser 去解析文件内容 => NormalModule 中的 parser 是 JavaScriptParser 的实例,所以调用 JavaScriptParser.parse 解析
- JavaScriptParser 使用了 acorn 第三方库去解析 code 到 ast
- traverse ast,得到所有的 import 语句,一旦发现 importDeclaration 则触发 JavaScriptParser.hooks.import 钩子,对应的监听函数就会处理依赖
- ------- 得到了 _source(源代码) 和 ast -----------
- ------- DFS process dependencies => module chain(compilation.modules) -------------
- 之后 DFS 处理 module 的 dependencies,所有的 module 形成一个链,此时 module 全部创建完成
- module chain 是一个数组,挂载在 compilation.modules 属性上,是一个 Set
- 广播 compilation succeedEntry 事件
- ------ compilation 完成事情 => compilation 根据入口文件获取到所有的 module ----------
- ------ EntryPlugin 调用的方法 compilation.addEntry 完成 -----------------
- ------ 进入下一个阶段 ------------------
- 广播 compiler.finishMake 事件
- ------ code generator => compilation.assets ----------
- 调用 compilation.seal 方法,将 compilation.modules 遍历生成一个 CacheSource
- 将所有的要输出的代码放置在 CacheSource._children 中
- 此时的 cacheSource 是一个数组,内容就是打包后的 main.js
- 将上述的 CacheSource 实例挂载到 compilation.assets['main.js']._source 上
- 广播 compiler.afterCompile 事件
- ----- 此时完成了 compiler.compile 方法执行 ------------
- 调用 compiler.emitAssets 方法触发 compiler.emit 事件
- 在 compiler.emit 事件回调中创建 dist 文件夹
- 之后调用 emitFiles 创建文件,文件来自于 compilation.assets
- 之后调用 doWrite 进行具体地写文件操作,之后触发 compiler.assetEmitted 事件
- ----- 完成了 compiler.emitAssets 方法,进入回调 ----
- 调用 compiler.emitRecords 方法
- compiler.emitRecords 方法回调中触发 compiler.done 事件
概述
AST
- AST => Abstract Syntax Tree => 抽象语法树
Babel
babel 原理
- parse => 把代码 code 变成 AST
- traverse => 遍历 AST 进行修改
- generate => 把 AST 变成代码 code2
- code --1--> ast --2--> ast2 --3--> code2
- 使用 @babel/core 和 @babel/preset-env 可以将代码转为 ES5
将转化后的代码输出到一个文件中
读取文件(test.js),并将文件中的代码转化(file_to_es5.ts)之后写入另一个文件(test.es5.js)
工具
- babel 可以把高级代码翻译成 ES5
- @babel/parser
- @babel/traverse
- @babel/generator
- @babel/core 包含前三者
- @babel/preset-env 内置很多规则
依赖
分析 JS 文件的依赖关系
- deps_1.ts + project_1
node -4 ts-node/register deps_1.ts
递归的分析嵌套依赖
- 依赖关系
- DFS
- deps_2.ts + project_2
静态分析循环依赖
- deps_3.ts + project_3
- node -r ts-node/register deps_3.ts
- Error: 栈溢出
解决循环依赖
- deps_4.ts + project_4
- 如果分析过了,就直接返回
- 静态分析 => 只分析依赖,不执行代码
- node project_4/index.js => 执行代码 => ReferenceError: Cannot access 'a' before initialization
总结
- 模块间可以循环依赖
- 代码不能有逻辑漏洞
- 没有逻辑漏洞的循环依赖 => project_5
- 最佳实践 => 不要使用循环依赖
打包 bundle
- 浏览器不能直接运行 ES6+ 代码
- 浏览器功能不同
- 现代浏览器可以通过 <script type=module> 来支持 import/export
- IE 8-15 不支持 import export,所以不能运行
- 把关键字转译为普通代码,并把所有文件打包成一个文件 => 需要编写复杂的代码来完成这件事情
把关键字转译成普通代码
- @babel/core 可以实现
- bundler_1.ts
- node -r ts-node/register bundler_1.ts
// a.js => ES5
// 将 import/export 转化成函数
- import 关键字会变成 require 函数
- export 关键字会变成 exports 对象
- 本质 => ESModule 语法变成了 CommonJS 规则
将所有文件打包成一个文件
- 包含了所有模块,并可以执行所有模块 => 执行入口文件
depRelation 转化成数组
- bundler_2.ts
code 转化成函数
- 在 code 字符串外面包一个 function(require, module, exports) {...}
- 把 code 写到文件里,引号不会出现在文件中
- case:
code = `var b = require('./b.js'); exports.default = 'a'`; code2 = ` function(require, module, exports){ ${code} } ` // 此时 code2 还是字符串 // 然后把 ` {code: ${code2}} ` 写入最终文件中 // 最终文件里的 code 就是函数了
完成 execute
- 执行入口文件
const modules = {} // modules 用于缓存所有模块
function execute(key) {
// key => index.js/a.js
if (modules[key]) {
return modules[key];
}
var item = depRelation.find(dep => dep.key === key);
var require = (path) => {
// 可能导入了其他的文件,所以需要将 execute 传递出去
// 输入一个文件路径,拿到路径去执行这个文件
return execute(pathToKey(path));
}
modules[key] = {__esModule: true}; // 标识这个模块是 ES 模块 => 自己有 default,不需要添加 default
var module = {exports: modules[key]};
// 执行一个文件的代码,传入导入其他模块的方法(require) + 如何导出自身模块(导出的东西放置在 module.exports 对象中)
item.code(require, module, module.exports); // 调用 code 转化的函数
return modules[key]
}
如何得到最终文件
拼凑出字符串,然后写入文件
- var dist = "";
- dist += content;
- writeFileSync("dist.js", dist);
自动创建最终文件
- bundler_3.ts => writeFileSync('dist_2.js', generateCode())
- node -r ts-node/register bundler_3.ts
- node dist_2.js
- bundler_3.ts 就是一个简易打包器
目前还存在的问题
- 生成的代码中有多个重复的 _interopXXX 函数
- 只能引入和运行 JS 文件
- 只能理解 import,无法理解 require
- 不支持插件
- 不支持配置入口文件和 dist 文件名
Loader
- bundler 目前只能加载 JS
- 要加载 CSS
- 把 CSS 变为 JS,就可以加载 CSS 了
将 css 转化为字符串
- 校验是否是 css
- css => body {color: red}
- => const str = "body {color: red}"; export default str;
- 使用 JS 生成 style 标签,将 str 写入标签中
- 将 style 标签 append 到 head 里
loader
- 一个 loader 可以是一个普通函数
function transform(code) {
const code2 = doSomething(code);
return code2;
}
module.exports = transform // 兼容 Node.js
- 一个 loader 也可以是一个异步函数
async function transform(code) {
const code2 = await doSomething(code);
return code2;
}
module.exports = transform // 兼容 Node.js
- 使用 require 不用 import
- 方便动态加载 => loader 名字是从配置中读取的
- 旧版 Node 不支持 import
优化 => 单一职责原则
- webpack 里面每个 loader 只做了一件事情
- 上面的 css-loader 做了两件事情
- 把 CSS 变为 JS 字符串 => css-loader => 将 css 转译为 js
- 把 JS 字符串放到 style 标签中 => style-loader => 不是转译
row-loader
- webpack 提供 loader-utils 和 schema-utils 作为辅助工具
- webpack 通过 this 来传递上下文
- getOptions(this) 可以获取 options
- validate 可以验证 options 是否合法
css-loader
- this.async() 用于获取回调
Webpack 的 loader 是什么
- webpack 自带的打包器只能支持 JS 文件
- 如果要加载其他文件,就需要使用 loader
- loader 的原理就是把文件内容包装成能运行的 JS
- case:加载 css 时需要用到 style-loader 和 css-loader
- css-loader 把代码从 css 代码变成 export default str 形式的 js 代码
- style-loader 把代码挂载到 head 里的 style 标签里面
Webpack 源码
webpack-cli 是如何调用 webpack 的
问:在 demo 目录运行 webpack-cli,会自动把 src/index.js 打包为 dist/main.js,webpack-cli 是如何调用 webpack 的?
答:1. webpack = require('webpack') 2. compiler = webpack(options, callback)
webpack-cli 通过上面两个语句来调用 webpack
webpack 是如何分析 index.js 的
- 打包器需要先分析并收集依赖,然后打包成一个文件,那么 webpack 是怎么做这件事情的
- 创建了 compiler 对象
- 之后调用了 hooks.xxx.call => 卡死了
Tapable
- webpack 团队为了写 webpack 而写的一个事件/钩子库
- 定义一个事件/钩子 => this.hooks.eventName = new SyncHook(["arg1", "arg2"])
- 监听一个事件/钩子 => this.hooks.eventName.tap('监听理由', fn)
- 触发一个事件/钩子 => this.hooks.eventName.call("arg1", "arg2")
webpack 的流程是怎样的
- webpack 把打包分为了哪几个阶段(事件/钩子)
- 至少有 env -> init -> run -> beforeCompile -> compile -> compilation -> make -> finishMake -> afterCompile -> emit 这几个钩子
webpack 读取 index.js 并分析和收集依赖是在哪个阶段
factory.create(nmf.create)做了什么
nmf.create 得到了一个 module 对象(路径对应的文件的内容),之后 addModule 和 buildModule
addModule 做了什么?
把 module 添加到 compilation.modules 里面,并且还通过检查 id 防止重复添加
buildModule 做了什么?
- 调用了 module.build()
- NormalModule.js 查看 build 源码 => 调用了 runLoaders
- 然后来到了 processResult(),发现了 _source 和 _ast
webpack 是如何分析 index.js 的
- runLoaders 读取源代码
- 之后将源代码变成 _ast
webpack 如何知道 index.js 依赖了哪些文件的
- webpack 会对 index.js 进行 parse 得到 ast,那么接下来 webpack 应该会 traverse 这个 ast,寻找 import 语句,那么相关代码在哪里?
- 其中 blockPreWalkStatement() 对 importDeclaration 进行检查
- 一旦发现 import 'xxx' 就会触发 import 钩子,对应的监听函数会处理依赖
- 其中 walkStatements() 对 ImportExpression 进行了检查
- 一旦发现 import('xxx') 就会触发 importCall 钩子,对应的监听函数会处理依赖
webpack 拿到了所以的依赖关系以及对应的文件,如果把这面 modules 合并成一个文件
- compilation.seal() => 该函数会创建 chunks,为每个 chunk 进行 codeGeneration,然后为每个 chunk 创建 asset
- seal() 之后,emitAssets()、emitFiles() 会创建文件
- 最终得到 dist/main.js 和其他 chunk 文件
总结
- webpack 使用 hooks 把主要阶段固定下来
- 插件可以绑定到任意的阶段
- 入口插件(EntryPlugin.js)处理入口文件,默认入口 ./src
- make -> compiler -> compilation -> entry -> dep -> module
- webpack 怎么分析依赖和打包的?
- 使用 JavascriptParser 对 index.js 进行 parse 得到 ast,然后遍历 ast
- 发现依赖声明就将其添加到 module 的 dependencies 或 blocks 中
- seal 阶段,webpack 将 module 转为 chunk,可能会把多个 module 通过 codeGeneration 合并为一个 chunk
- seal 之后,为每个 chunk 创建文件,并写到硬盘上
面试
- webpack 使用 Tapable 作为事件中心,将打包分为 env、compile、make、seal、emit 几个阶段
- 在 make 阶段借助 acorn 对源代码进行了 parse
webpack 流程
init -> run -> compile -> compilation -> make -> afterCompile -> seal(对代码进行一些封装) -> codeGenerate(生成最终的文件) -> emit(emit
一个文件) -> done
Plugin
imagemin-webpack-plugin
- 监听 emit 事件
- 回调函数会对 compilation.assets 进行检测,如果是图片就优化,之后替换图片
clean-webpack-plugin
- 监听 emit 事件,清空 dist
- 监听 done 事件,删除 assets 之外的文件
DefinePlugin
- 监听 compilation 事件
- 获取 normalModuleFactory,监听 parser 事件,
- parser 遍历 ast 的时候,如果发现有使用定义的全局变量,那么就将全局变量添加进 dependency 中
知识点
- Node 运行 TS 代码 =>
node -r ts-node/register <file>
- Node 运行 TS 代码并运行在 Chrome 中 =>
node -r ts-node/register --inspect-brk <file>
- 图片相关 loader 工作原理思路
- 如果发现是图片,则把图片放入一个 public/assets 文件中,之后导出相对路径
- 如果发现是图片,并且文件很小,转化成 base64 编码