webpack构建过程
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
- webpack会读取你在命令行传入的配置以及项目里的 webpack.config.js 文件,初始化本次构建的配置参数,并且执行配置文件中的插件实例化语句,生成Compiler传入plugin的apply方法,为webpack事件流挂上自定义钩子。
- 接下来到了entryOption阶段,webpack开始读取配置的Entries,递归遍历所有的入口文件
- Webpack接下来就开始了compilation过程。会依次进入其中每一个入口文件(entry),先使用用户配置好的loader对文件内容进行编译(buildModule),我们可以从传入事件回调的compilation上拿到module的resource(资源路径)、loaders(经过的loaders)等信息;之后,再将编译好的文件内容使用acorn解析生成AST静态语法树(normalModuleLoader),分析文件的依赖关系逐个拉取依赖模块并重复上述过程,最后将所有模块中的require语法替换成__webpack_require__来模拟模块化操作。
- emit阶段,所有文件的编译及转化都已经完成,包含了最终输出的资源,我们可以在传入事件回调的compilation.assets 上拿到所需数据,其中包括即将输出的资源、代码块Chunk等等信息
compilation:一个 Compilation
对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation
对象也提供了很多事件回调供插件做扩展。
Compiler:Compiler
负责文件监听和启动编译。Compiler
实例中包含了完整的 Webpack
配置,全局只有一个 Compiler
实例。
开始
npm初始化项目,根目录新建文件
webpack.config.js
1 | const path = require('path'); |
index.js
1 | import a from './index1.js'; |
index1.js
1 | import b from './index2.js'; |
index2.js
1 | const b = 2; |
修改package.json文件
1 | ... |
执行sctipts命令
1 | npm run build |
看下构建完的代码
1 | /******/ (function(modules) { // webpackBootstrap |
webpack_require实现了自己的模块化,并且把代码都缓存在了installedModules里,代码文件以对象形式传入,key是路径,value是包裹的代码字符串。
如何断点调试打包过程
修改package.json文件
1 | ... |
因为可以知道构建的时候调用的是webpack.js文件,所以添加--inspect-brk
参数,开启调试。
上面这个例子,调用webpack.js文件的时候,最后是调用到这
也就是引入这个模块
所以进入这个文件打个debugger
初始化参数
首先调用yargs.parse
方法,传入命令行参数与回调函数。
1 | self.parse = function parse (args, shortCircuit, _parseFn) { |
在这里做参数合并,之后调用传入的回调函数
走进options = require("./utils/convert-argv")(argv);
,在这里面获取配置文件位置,并且把配置项push
到options
数组里
requireConfig
方法,就是引入配置文件模块
1 | const requireConfig = function requireConfig(configPath) { |
最后得到的配置信息
之后把配置信息传入processOptions
函数
1 | function processOptions(options) { |
先引入webpack
,文件目录在node_modules/webpack/lib/webpack.js
开始编译
初始化 Compiler
对象 compiler = webpack(options);
1 | const webpack = (options, callback) => { |
compiler是Compiler的实例
1 | compiler = new Compiler(options.context); |
在创建过程中,hooks属性里创建了很多钩子实例
实例化compiler之后,遍历了plugins
数组
判断是否是function
类型,不是就调用apply
方法(我们这边只使用了CleanWebpackPlugin
插件,所以调用的是它实例的apply
方法)
1 | apply(compiler: Compiler) { |
- 先判断
compiler.options.output.path
,输出路径是否存在,不存在就报提醒 - 之后判断
cleanOnceBeforeBuildPatterns
的长度,它是在实例化的时候创建的
,再判断是否有hooks
,之后调用下面这个方法
1 | hooks.emit.tap('clean-webpack-plugin', (compilation) => { |
把名字、类型、回调存到数组里,挂载在hook.emit
上
- 之后又在
hooks.done
上的taps
属性存入,这里的区别是回调函数不一样
compiler
最后的样子
接着执行run方法开始执行编译
1 | run(callback) { |
进入this.compile(onCompiled);
1 | compile(callback) { |
在这里构建compilation对象
1 | const compilation = this.newCompilation(params); |
进入newCompilation
1 | newCompilation(params) { |
进入createCompilation,实例化Compilation
1 | createCompilation() { |
确认入口
之后进入this.hooks.make.callAsync
,目的是确认入口
进入this._addModuleChain
创建NormalModule实例
1 | (err, result) => { |
之后对module进行build
进入doBuild
方法
1 | this.doBuild(options, compilation, resolver, fs, err => { |
loader先对文件处理转成buffer再转成字符串,挂载在_source上
1 | const asString = buf => { |
之后在这里解析入口文件转成ast,解析依赖关系。
之后进入handleParseResult
解析出的依赖会继续走到parser.parse
的地方
输出资源
webpack 会监听 seal
事件调用各插件对构建后的结果进行封装,要逐次对每个 module 和 chunk 进行整理,生成编译后的源码,合并,拆分,生成 hash
进入compilation.seal
进入createChunkAssets方法,遍历chunks
先获得manifest,遍历它,之后进入fileManifest.render()
1 | render(hash, chunk, moduleTemplate, dependencyTemplates) { |
生成source的方法在这里,所有的requirefn被替换成了__webpack_require__
1 | renderBootstrap(hash, chunk, moduleTemplate, dependencyTemplates) { |
最终得到source
生成好的js保存在compilation.assets里
之后走进onCompiled方法,又走进emitAssets方法,调用进入this.hooks.emit.callAsync
中的回调方法
最后走到writeOut方法,先得到content
进入source.source方法,其实就是循环然后拼接
1 | source() { |
最后将文件写入到目标目录
补充
其中的广播事件,调用clean-webpack-plugin插件的时候
1 | emitAssets(compilation, callback) { |
进入this.hooks.emit.callAsync
1 | tap: (context, tap) => { |
进入handler(0.95, "emitting", tap.name);
1 | const defaultHandler = (percentage, msg, ...args) => { |
输出进度,和插件名
之后调用回调
进入_fn0(compilation)
进入handleInitial
方法
1 | handleInitial(compilation: Compilation) { |
进入removeFiles
方法
1 | removeFiles(patterns: string[]) { |
进入delSync
方法,在这里会对文件做删除处理
1 | module.exports.sync = (patterns, options) => { |
之后打印出被删除的文件名
hash值生成
使用crypto.js
进入this.createHash();
方法调用
hash值是fullHash截取前20位
生成逻辑在createHash方法里
1 | createHash() { |
loader
1 | npm install @babel/core @babel/preset-env babel-loader -D |
webpack.config.js
1 | const path = require('path'); |
目录
1 | ├── package-lock.json |
钩子调用顺序
1 | Compiler:beforeRun 清除缓存 |
参考
https://segmentfault.com/a/1190000015088834
https://segmentfault.com/a/1190000015917768
https://juejin.im/entry/57f4ac35a0bb9f0058168491