作者: frank 前端迷
转发链接:https://mp.weixin.qq.com/s/di5WDvGyMZ5qQ2YPr3w0lQ
目录webpack4主流程源码解说以及动手实现一个简单的webpack(上)本篇
前言简单概述 Webpack 整体运行流程
- 读取参数
- 实例化 Compiler
- entryOption 阶段,读取入口文件
- Loader 编译对应文件,解析成 AST
- 找到对应依赖,递归编译处理,生成 chunk
- 输出到 dist
通过打断点的方式阅读源码,来看一下命令行输入 webpack 的时候都发生了什么?P.S. 以下的源码流程分析都基于 webpack4
先附上一张自己绘制的执行流程图
- 初始化参数(webpack.config.js shell options)
webpack 的几种启动方式
- 通过 webpack-cli执行 会走到 ./node_modules/.bin/webpack-cli(执行)
- 通过 shell 执行webpack ,会走到 ./bin/webpack.js
- 通过 require("webpack")执行 会走到 ./node_modules/webpack/lib/webpack.js
追加 shell 命令的参数,如-p , -w,通过 yargs 解析命令行参数convert-yargs 把命令行参数转换成 Webpack 的配置选项对象 同时实例化插件 new Plugin()
- 实例化 Compiler
阅读完整源码点击这里:webpack.js
//webpack入口
constwebpack=(options,callback)=>{
letcompiler
//实例Compiler
if(Array.isArray(options)){
//...
compiler=createMultiCompiler(options)
}else{
compiler=createCompiler(options)
}
//...
//若options.watch===true&&callback则开启watch线程
if(watch){
compiler.watch(watchOptions,callback)
}else{
compiler.run((err,stats)=>{
compiler.close((err2)=>{
callback(err||err2,stats)
})
})
}
returncompiler
}
webpack 的入口文件其实就实例了 Compiler 并调用了 run 方法开启了编译,
- 注册 NodeEnvironmentPlugin 插件,挂载 plugin 插件,使用 WebpackOptionsApply 初始化基础插件
在此期间会 apply 所有 webpack 内置的插件,为 webpack 事件流挂上自定义钩子
源码仍然在webpack.js文件
constcreateCompiler=(rawOptions)=>{
//...省略代码
constcompiler=newCompiler(options.context)
compiler.options=options
//应用Node的文件系统到compiler对象,方便后续的文件查找和读取
newNodeEnvironmentPlugin({
infrastructureLogging:options.infrastructureLogging,
}).apply(compiler)
//加载插件
if(Array.isArray(options.plugins)){
for(constpluginofoptions.plugins){
//依次调用插件的apply方法(默认每个插件对象实例都需要提供一个apply)若为函数则直接调用,将compiler实例作为参数传入,方便插件调用此次构建提供的Webpack API并监听后续的所有事件Hook。
if(typeofplugin==='function'){
plugin.call(compiler,compiler)
}else{
plugin.apply(compiler)
}
}
}
//应用默认的Webpack配置
applyWebpackOptionsDefaults(options)
//随即之后,触发一些Hook
compiler.hooks.environment.call()
compiler.hooks.afterEnvironment.call()
//内置的Plugin的引入,对webpackoptions进行初始化
newWebpackOptionsApply().process(options,compiler)
compiler.hooks.initialize.call()
returncompiler
}
编译阶段
- 启动编译(run/watch 阶段)
这里有个小逻辑区分是否是 watch,如果是非 watch,则会正常执行一次 compiler.run()。
如果是监听文件(如:--watch)的模式,则会传递监听的 watchOptions,生成 Watching 实例,每次变化都重新触发回调。
如果不是监视模式就调用 Compiler 对象的 run 方法,befornRun->beforeCompile->compile->thisCompilation->compilation开始构建整个应用。
const{SyncHook,SyncBailHook,AsyncSeriesHook}=require('tapable')
classCompiler{
constructor(){
//1.定义生命周期钩子
this.hooks=Object.freeze({
//...只列举几个常用的常见钩子,更多hook就不列举了,有兴趣看源码
done:newAsyncSeriesHook(['stats']),//一次编译完成后执行,回调参数:stats
beforeRun:newAsyncSeriesHook(['compiler']),
run:newAsyncSeriesHook(['compiler']),//在编译器开始读取记录前执行
emit:newAsyncSeriesHook(['compilation']),//在生成文件到output目录之前执行,回调参数:compilation
afterEmit:newAsyncSeriesHook(['compilation']),//在生成文件到output目录之后执行
compilation:newSyncHook(['compilation','params']),//在一次compilation创建后执行插件
beforeCompile:newAsyncSeriesHook(['params']),
compile:newSyncHook(['params']),//在一个新的compilation创建之前执行
make:newAsyncParallelHook(['compilation']),//完成一次编译之前执行
afterCompile:newAsyncSeriesHook(['compilation']),
watchRun:newAsyncSeriesHook(['compiler']),
failed:newSyncHook(['error']),
watchClose:newSyncHook([]),
afterPlugins:newSyncHook(['compiler']),
entryOption:newSyncBailHook(['context','entry']),
})
//...省略代码
}
newCompilation(){
//创建Compilation对象回调compilation相关钩子
constcompilation=newCompilation(this)
//...一系列操作
this.hooks.compilation.call(compilation,params)//compilation对象创建完成
returncompilation
}
watch(){
//如果运行在watch模式则执行watch方法,否则执行run方法
if(this.running){
returnhandler(newConcurrentCompilationError())
}
this.running=true
this.watchMode=true
returnnewWatching(this,watchOptions,handler)
}
run(callback){
if(this.running){
returncallback(newConcurrentCompilationError())
}
this.running=true
process.nextTick(()=>{
this.emitAssets(compilation,(err)=>{
if(err){
//在编译和输出的流程中遇到异常时,会触发failed事件
this.hooks.failed.call(err)
}
if(compilation.hooks.needAdditionalPass.call()){
//...
// done:完成编译
this.hooks.done.callAsync(stats,(err)=>{
//创建compilation对象之前
this.compile(onCompiled)
})
}
this.emitRecords((err)=>{
this.hooks.done.callAsync(stats,(err)=>{})
})
})
})
this.hooks.beforeRun.callAsync(this,(err)=>{
this.hooks.run.callAsync(this,(err)=>{
this.readRecords((err)=>{
this.compile(onCompiled)
})
})
})
}
compile(callback){
constparams=this.newCompilationParams()
this.hooks.beforeCompile.callAsync(params,(err)=>{
this.hooks.compile.call(params)
//已完成complication的实例化
constcompilation=this.newCompilation(params)
//触发make事件并调用addEntry,找到入口js,进行下一步
// make:表示一个新的complication创建完毕
this.hooks.make.callAsync(compilation,(err)=>{
process.nextTick(()=>{
compilation.finish((err)=>{
//封装构建结果(seal),逐次对每个module和chunk进行整理,每个chunk对应一个入口文件
compilation.seal((err)=>{
this.hooks.afterCompile.callAsync(compilation,(err)=>{
//异步的事件需要在插件处理完任务时调用回调函数通知Webpack进入下一个流程,
//不然运行流程将会一直卡在这不往下执行
returncallback(null,compilation)
})
})
})
})
})
})
}
emitAssets(){}
}
- 编译模块:(make 阶段)
- 从 entry 入口配置文件出发, 调用所有配置的 Loader 对模块进行处理,
- 再找出该模块依赖的模块, 通过 acorn 库生成模块代码的 AST 语法树,形成依赖关系树(每个模块被处理后的最终内容以及它们之间的依赖关系),
- 根据语法树分析这个模块是否还有依赖的模块,如果有则继续循环每个依赖;再递归本步骤直到所有入口依赖的文件都经过了对应的 loader 处理。
- 解析结束后,webpack 会把所有模块封装在一个函数里,并放入一个名为 modules 的数组里。
- 将 modules 传入一个自执行函数中,自执行函数包含一个 installedModules对象,已经执行的代码模块会保存在此对象中。
- 最后自执行函数中加载函数(webpack__require)载入模块。
classCompilationextendsTapable{
constructor(compiler){
super();
this.hooks={};
//...
this.compiler=compiler;
//...
//构建生成的资源
this.chunks=[];
this.chunkGroups=[];
this.modules=[];
this.additionalChunkAssets=[];
this.assets={};
this.children=[];
//...
}
//
buildModule(module,optional,origin,dependencies,thisCallback){
//...
//调用module.build方法进行编译代码,build中其实是利用acorn编译生成AST
this.hooks.buildModule.call(module);
module.build(/**param*/);
}
//将模块添加到列表中,并编译模块
_addModuleChain(context,dependency,onModule,callback){
//...
//moduleFactory.create创建模块,这里会先利用loader处理文件,然后生成模块对象
moduleFactory.create({
contextInfo:{
issuer:"",
compiler:this.compiler.name
},
context:context,
dependencies:[dependency]
},(err,module)=>{
constaddModuleResult=this.addModule(module);
module=addModuleResult.module;
onModule(module);
dependency.module=module;
//...
//调用buildModule编译模块
this.buildModule(module,false,null,null,err=>{});
});
}
//添加入口模块,开始编译&构建
addEntry(context,entry,name,callback){
//...
this._addModuleChain(//调用_addModuleChain添加模块
context,entry,module=>{
this.entries.push(module);
},
//...
);
}
seal(callback){
this.hooks.seal.call();
//...
//完成了Chunk的构建和依赖、Chunk、module等各方面的优化
constchunk=this.addChunk(name);
constentrypoint=newEntrypoint(name);
entrypoint.setRuntimeChunk(chunk);
entrypoint.addOrigin(null,name,preparedEntrypoint.request);
this.namedChunkGroups.set(name,entrypoint);
this.entrypoints.set(name,entrypoint);
this.chunkGroups.push(entrypoint);
GraphHelpers.connectChunkGroupAndChunk(entrypoint,chunk);
GraphHelpers.connectChunkAndModule(chunk,module);
chunk.entryModule=module;
chunk.name=name;
//...
this.hooks.beforeHash.call();
this.createHash();
this.hooks.afterHash.call();
this.hooks.beforeModuleAssets.call();
this.createModuleAssets();
if(this.hooks.shouldGenerateChunkAssets.call()!==false){
this.hooks.beforeChunkAssets.call();
this.createChunkAssets();
}
//...
}
createHash(){
//...
}
//生成assets资源并保存到Compilation.assets中给webpack写插件的时候会用到
createModuleAssets(){
for(leti=0;i<this.modules.length;i ){
constmodule=this.modules[i];
if(module.buildInfo.assets){
for(constassetNameofObject.keys(module.buildInfo.assets)){
constfileName=this.getPath(assetName);
this.assets[fileName]=module.buildInfo.assets[assetName];
this.hooks.moduleAsset.call(module,fileName);
}
}
}
}
createChunkAssets(){
asyncLib.forEach(
this.chunks,(chunk,callback)=>{
// manifest是数组结构,每个manifest元素都提供了`render`方法,提供后续的源码字符串生成服务。至于render方法何时初始化的,在`./lib/MainTemplate.js`中
letmanifest=this.getRenderManifest()
asyncLib.forEach(
manifest,(fileManifest,callback)=>{...
source=fileManifest.render()
this.emitAsset(file,source,assetInfo)
},callback)
},callback)
}
}
classSingleEntryPlugin{
apply(compiler){
compiler.hooks.compilation.tap(
'SingleEntryPlugin',
(compilation,{normalModuleFactory})=>{
compilation.dependencyFactories.set(
SingleEntryDependency,
normalModuleFactory
)
}
)
compiler.hooks.make.tapAsync(
'SingleEntryPlugin',
(compilation,callback)=>{
const{entry,name,context}=this
constdep=SingleEntryPlugin.createDependency(entry,name)
compilation.addEntry(context,dep,name,callback)
}
)
}
staticcreateDependency(entry,name){
constdep=newSingleEntryDependency(entry)
dep.loc={name}
returndep
}
}
概括一下 make 阶段单入口打包的流程,大致为 4 步骤
- 执行 SingleEntryPlugin(单入口调用 SingleEntryPlugin,多入口调用 MultiEntryPlugin,异步调用 DynamicEntryPlugin),EntryPlugin 方法中调用了 Compilation.addEntry 方法,添加入口模块,开始编译&构建
- addEntry 中调用 _addModuleChain,将模块添加到依赖列表中,并编译模块
- 然后在 buildModule 方法中,调用了 NormalModule.build,创建模块之时,会调用 runLoaders,执行 Loader,利用 acorn 编译生成 AST
- 分析文件的依赖关系逐个拉取依赖模块并重复上述过程,最后将所有模块中的 require 语法替换成 webpack_require 来模拟模块化操作。
runLoaders 方法调用 iteratePitchingLoaders 去递归查找执行有 pich 属性的 loader ;若存在多个 pitch 属性的 loader 则依次执行所有贷款pitch 属性的 loader ,执行完后逆向执行所有贷款pitch 属性的 normal 的 normal loader 后返回 result,没有 pitch 属性的 loader 就不会再执行;若 loaders 中没有 pitch 属性的 loader 则逆向执行 loader;执行正常 loader 是在 iterateNormalLoaders 方法完成的,处理完所有 loader 后返回 result。
出自文章你真的掌握了 loader 么?- loader 十问(https://juejin.im/post/5bc1a73df265da0a8d36b74f)
Compiler 和 Compilation 的区别webpack 打包离不开 Compiler 和 Compilation,它们两个分工明确,理解它们是我们理清 webpack 构建流程重要的一步。
Compiler 负责监听文件和启动编译 它可以读取到 webpack 的 config 信息,整个 Webpack 从启动到关闭的生命周期,一般只有一个 Compiler 实例,整个生命周期里暴露了很多方法,常见的 run,make,compile,finish,seal,emit 等,我们写的插件就是作用在这些暴露方法的 hook 上
Compilation 负责构建编译。每一次编译(文件只要发生变化,)就会生成一个 Compilation 实例,Compilation 可以读取到当前的模块资源,编译生成资源,变化的文件,以及依赖跟踪等状态信息。同时也提供很多事件回调给插件进行拓展。
完成编译- 输出资源:(seal 阶段)
在编译完成后,调用 compilation.seal 方法封闭,生成资源,这些资源保存在 compilation.assets, compilation.chunk,然后便会调用 emit钩子,根据 webpack config 文件的 output 配置的 path 属性,将文件输出到指定的 path.
- 输出完成:done/failed 阶段
done 成功完成一次完成的编译和输出流程。failed 编译失败,可以在本事件中获取到具体的错误原因 在确定好输出内容后, 根据配置确定输出的路径和文件名, 把文件内容写入到文件系统。
emitAssets(compilation,callback){
constemitFiles=(err)=>{
//...省略一系列代码
// afterEmit:文件已经写入磁盘完成
this.hooks.afterEmit.callAsync(compilation,(err)=>{
if(err)returncallback(err)
returncallback()
})
}
//emit事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(这是最后一次修改最终文件的机会)
this.hooks.emit.callAsync(compilation,(err)=>{
if(err)returncallback(err)
outputPath=compilation.getPath(this.outputPath,{})
mkdirp(this.outputFileSystem,outputPath,emitFiles)
})
}
然后,我们来看一下 webpack 打包好的代码是什么样子的。
推荐JavaScript学习相关文章《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
《》
,