前言
webpack是目前常用的模块打包构建工具,因为之前对webpack配置优化也比较了解,最近顺着思路debug了一下源码,当然过程很痛苦不过也在看过各个大佬文章也算是对大体一知半解,所以来输出一下,建议大家熟悉webpack流程后再去尝试源码。
源码流程大体分三个阶段,初始化、编译、打包输出。
const webpack = require('../lib/index.js') // 直接使用源码中的webpack函数
const config = require('./webpack.config')//初始化
const compiler = webpack(config)//执行
compiler.run((err, stats)=>{
if(err){
console.error(err)
}else{
console.log(stats)
}
})
这是我debug的js代码,初始化阶段就是const compiler = webpack(config)这句话,然后编译 打包都在compiler.run()之后的流程,下面进行逐个分析。
初始化
源码中初始化会先进lib/index.js,随后进入到lib/webpack.js执行他的create()方法并返回compiler对象,compiler是我们在打包之后仅此一个的。
create会走到compiler = createCompiler(options)去创建compiler实例并返回
createCompiler方法就是拿到我们webpack.config中的配置的参数然后开始进行的不走就是上面所显示的,具体的compiler类上的属性跟方法就不过多赘述,后面重要的会出现。其中比较重要的是第五步,会挂载很多钩子然后在compiler的事件流里面触发,比较重要的是跟后面相关的make编辑的钩子,这里先留个悬念。至此我们拿到了compiler对象,完成了初始化阶段。
编译
下面开始进入compiler.run()方法
首先执行了this.hooks.beforeRun.callAsync()这个钩子,我拿这个方法举例说明webpack里面的事件流的具体实现。
首先看到compiler实例在初始化的时候hooks属性上的beforeRun的值是初始化一个AsyncSeriesHook的实例,这个AsyncSeriesHook其实是tabaple类的一种实现,tapaple类是基于发布订阅模式,我们看到的this.hooks.beforeRun.callAsync()就是去执行它里面收集的一些方法。然后我通过搜索看看哪里把方法订阅上去发现了lib/index.js文件中。webpack事件流是基于tapable类,建议大家也去看下它的构造。
run方法继续走,会走到compile方法里面
compile方法包含了构建的主要内容,在hooks.beforeCompile钩子执行完之后,compilation登场,这个对象是每次编译都会创建一个的,里面包含了这次编译的信息和一些打包输出的方法。
实例化compilation对象之后到达了hooks.make钩子执行,从这开始就进行了编译阶段,通过搜索我们找到了注册make方法的事件。
出现在EntryPlugin.js,根据new EntryPlugin网上找找到了EntryOptionPlugin.js,再往上找,找到了在初始化的时候留的悬念,我们在挂载很多plugin的时候在compiler.hooks.make上注册一个编译事件。如上图,EntryPlugin.js最重要的是拿到compilation对象,执行compilation.addEntry开始添加入口文件编译。
根据addEntry方法一步步往下debug,=> compilation._addEntryItem => compilation.addModuleChain => compilation.handleModuleCreation =>this.addModule=>this.buildModule=>module.build,最终会走到NormalModule对象(./lib/NormalModule.js)中,执行build方法,首先会先执行doBuild方法
const { runLoaders } = require("loader-runner");
doBuild(options, compilation, resolver, fs, callback){
// runLoaders从包'loader-runner'引入的方法
runLoaders({
resource: this.resource, // 这里的resource可能是js文件,可能是css文件,可能是img文件
loaders: this.loaders,
}, (err, result) => {
const source = result[0];
const sourceMap = result.length >= 1 ? result[1] : null;
const extraInfo = result.length >= 2 ? result[2] : null;
// ...
})
}
runLoaders把不是js的模块转换成js模块
runLoaders把非js模块转成js模块之后,从入口触发将模块变成ast语法树再去查询它依赖的模块,递归收集依赖生成result,为打包输出做准备。
输出
完成上面解析模块依赖生成module之后,接下来就是着手输出的事了,回来到compilation.seal方法
seal方法中实现了很多优化比如dependencies,module,还生成了本次编译的chunk和hash值等,最终来到了this.createChunkAssets方法
createChunkAssets方法遍历chunks生成manifest数组,每个数组元素上面都有render方法,下面遍历manifest,执行里面的render方法会拼接成我们打包输出的文件的source
之后会组装到compilation.assets里面
然后compiler中的compile方法就走到最终方法onCompiled,执行this.emitAssets根据输出路径输出每个chunk文件。
至此就生成了我们打包文件,期间因为很多发布订阅的钩子和一些循环的方法debug异常痛苦,好在参考了一些社区文章也算大体流程体会下来收获良多,撒花~