webpck深入浅出教程webpack源码分析
Posted fullstack_lth
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了webpck深入浅出教程webpack源码分析相关的知识,希望对你有一定的参考价值。
webpack入口文件:
执行npm命令后,查看node_modules\\.bin目录下是否存在webpack.sh或者webpack.cmd文件,如果存在就执行,如果不存在就抛出错误。
webpack实际入口文件是:
node_modules\\webpack\\bin\\webpack.js
一、分析入口文件webpack.js
#!/usr/bin/env node
// @ts-ignore
process.exitCode = 0;//1、状态码
const runCommand = (command, args) => ;//2、运行某个命令
const isInstalled = packageName => ;//3、判断是否安装某个包
const CLIs = ["webpack-cli","webpack-command"]//webpack的cli工具webpack-cli或者webpack-command
const installedClis = CLIs.filter(cli => cli.installed);//过滤两个cli工具是否安装
if (installedClis.length === 0) //根据安装的cli数量处理
else if (installedClis.length === 1)
else
process.exitCode = 1;
二、进入webpack-cli(或者webpack-command处理)
webpack-cli/bin/cli.js文件分析,其实就是执行webpack-cli
1、解析命令,看命令是否需要经过编译,如果是NON_COMPILATION_CMD数组中的以下命令
const NON_COMPILATION_ARGS = ["init", "migrate", "serve", "generate-loader", "generate-plugin", "info"];
判断是否有serve,并做处理后return返回
const NON_COMPILATION_CMD = process.argv.find(arg =>
if (arg === "serve")
global.process.argv = global.process.argv.filter(a => a !== "serve");
process.argv = global.process.argv;
return NON_COMPILATION_ARGS.find(a => a === arg);
);
如果不是NON_COMPILATION_CMD数组中的这些命令则引入yargs,对命令行定制处理
2、分析命令行参数及webpack配置文件中的参数,对各个参数做转换,组成webpack编译需要的配置项
3、进入webpack,根据options配置项编译和构建
processOptions的过程中,判断命令行中的参数和配置文件的参数,对outputOptions增加对应的option。
处理完options后,将参数传给webpack对象,并实例化生成全局的编译对象compiler = webpack(options);
三、进入webpack/lib/webpack.js
1、如果传入options是个数组,递归执行,否则就调用WebpackOptionsDefaulter.js中的process对options处理
2、接着实例化compiler对象,并执行环境相关的hooks,最后WebpackOptionsApply调用根据options.target类型设置webpack对应的默认插件。
3、最后执行compiler的run方法
四、进入compiler.js中run方法
1、compiler中的run方法核心就这句this.compile(onCompiled),由于compiler类中没有hooks,而且由于compiler和compilation类都继承了Tapable类,并且compiler和compilation类中this.hooks中定义了很多个Hook,所以按照逻辑推测(很早就知道。。。)是Tapable中的hooks方法。所以我们待会再来看下面这几句代码,先看Tapable实现。
this.hooks.beforeRun.callAsync(this, err =>
if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err =>
if (err) return finalCallback(err);
this.readRecords(err =>
if (err) return finalCallback(err);
this.compile(onCompiled);
);
);
);
2、Tapable是一个类似与Nodejs的EventEmitter的库,主要是控制钩子函数的发布订阅,控制着webpack的plugin系统。
Tapable中暴漏了很多Hook类,主要是为插件提供挂载的钩子,入口文件中暴漏了以下Hooks:
exports.SyncHook = require("./SyncHook");//同步钩子
exports.SyncBailHook = require("./SyncBailHook");//同步熔断钩子
exports.SyncWaterfallHook = require("./SyncWaterfallHook");//同步流水钩子
exports.SyncLoopHook = require("./SyncLoopHook");//同步循环钩子
exports.AsyncParallelHook = require("./AsyncParallelHook");//异步并发钩子
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");//异步并发熔断钩子
exports.AsyncSeriesHook = require("./AsyncSeriesHook");//异步串行钩子
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");//异步串行熔断钩子
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");//异步串行流水钩子
webapck中所有插件都监听Hooks,一旦Hooks触发后,对应插件也执行,并且每个插件都会实现一个apply方法,传参为compiler对象。
序号 | 钩子名称 | 执行方式 | 使用要点 |
---|---|---|---|
1 | SyncHook | 同步串行 | 不关心监听函数的返回值 |
2 | SyncBailHook | 同步串行 | 只要监听函数中有一个函数的返回值不为 null ,则跳过剩下所有的逻辑 |
3 | SyncWaterfallHook | 同步串行 | 上一个监听函数的返回值可以传给下一个监听函数 |
4 | SyncLoopHook | 同步循环 | 当监听函数被触发的时候,如果该监听函数返回true 时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环 |
5 | AsyncParallelHook | 异步并发 | 不关心监听函数的返回值 |
6 | AsyncParallelBailHook | 异步并发 | 只要监听函数的返回值不为 null ,就会忽略后面的监听函数执行,直接跳跃到callAsync 等触发函数绑定的回调函数,然后执行这个被绑定的回调函数 |
7 | AsyncSeriesHook | 异步串行 | 不关系callback() 的参数 |
8 | AsyncSeriesBailHook | 异步串行 | callback() 的参数不为null ,就会直接执行callAsync 等触发函数绑定的回调函数 |
9 | AsyncSeriesWaterfallHook | 异步串行 | 上一个监听函数的中的callback(err, data) 的第二个参数,可以作为下一个监听函数的参数 |
Tapable的使用-钩子的绑定与执行,类似于发布订阅的on和emit
Async | Sync |
tapAsync/tapPromise/tap | tap |
callAsync/promise | call |
3、这句代码是关键:
this.compile(onCompiled)
先看compile实现:
webpack编译的执行顺序为以下这些hooks的执行顺序:
beforerun->beforeCompile->compile->aftercompile->make(从entry开始递归分析依赖)->compile调用compilation中的finish(上报模块错误)->compile调用compilation中的seal(优化)->afterCompile
以上这些hooks为一些关键流程的hooks,实际compiler和compilation上各自挂载了很多hooks,有入口相关,模块构建相关,优化相关,输出相关,watch相关,环境相关等大约一百左右。
compile(callback) //编译
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => //编译前的准备工作
if (err) return callback(err);
this.hooks.compile.call(params);
//生成编译实例compilation对象
const compilation = this.newCompilation(params);
//对entry开始递归分析依赖,对每个依赖模块build
this.hooks.make.callAsync(compilation, err =>
if (err) return callback(err);
compilation.finish(err => //每个模块编译完成
if (err) return callback(err);
compilation.seal(err =>
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err =>
if (err) return callback(err);
//执行回调onCompiled
return callback(null, compilation);
);
);
);
);
);
4、期间生成了compilation对象,挂载了一堆属性,并执行了两个hooks
createCompilation()
return new Compilation(this);
newCompilation(params)
const compilation = this.createCompilation();
compilation.fileTimestamps = this.fileTimestamps;
compilation.contextTimestamps = this.contextTimestamps;
compilation.name = this.name;
compilation.records = this.records;
compilation.compilationDependencies = params.compilationDependencies;
this.hooks.thisCompilation.call(compilation, params);
this.hooks.compilation.call(compilation, params);
return compilation;
五、输出到文件
执行完compile函数就执行回调-onCompiled函数
const onCompiled = (err, compilation) =>
if (err) return finalCallback(err);
if (this.hooks.shouldEmit.call(compilation) === false)
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err =>
if (err) return finalCallback(err);
return finalCallback(null, stats);
);
return;
this.emitAssets(compilation, err =>
if (err) return finalCallback(err);
if (compilation.hooks.needAdditionalPass.call())
compilation.needAdditionalPass = true;
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err =>
if (err) return finalCallback(err);
this.hooks.additionalPass.callAsync(err =>
if (err) return finalCallback(err);
this.compile(onCompiled);
);
);
return;
this.emitRecords(err =>
if (err) return finalCallback(err);
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err =>
if (err) return finalCallback(err);
return finalCallback(null, stats);
);
);
);
;
从中我们看到执行的hooks为输出文件到磁盘的任务
shouldEmit(判断是否应该输出文件)-> emit -> needAdditionalPass->done
六,其他一些核心概念
ModuleFactory
模块工厂就是负责构造模块的实例,介绍两种NormalModuleFactory
和ContextModuleFactory
。两者不同的地方在于后者用于解析动态import()
. 模块工厂主要是用于将Resolver
解析成功的请求里的源码从文件中拿出,在内存中创建一个模块对象(NormalModule)
Resolver
请求一个模块的时,将模块名或者相对地址发给模块解析器,它会去解析出绝对地址去寻找那个模块,看是否存在,如果存在则返回相应的模块信息,包括上下文等。这里的请求可以类似网络请求一样携带上查询参数之类的,Resolver
将会返回额外信息。webpack4里将Resolver
这个实例抽出来单独发了一个包enhanced-resolve
, 抽象出来可以便于用户实现自己的Resolver
七、webpack流程总结
借用腾讯@程柳锋老师的webpack中hooks的编译流程图:
上面是hooks调用顺序,下面我们总结webpack总的工作流程,包含加载 - 编译 - 输出三个步骤:
1.初始化。 从webpck的配置文件(webpack.config.js或其它)中读取配置信息,或者从shell脚本的输入参数中读取配置信息,初始化本次的执行环节。
2.加载插件,准备编译。 根据配置信息,加载本次执行所需要的所有相关插件。
3.读取入口文件。 根据配置信息的entry属性依次读取要编译入的文件。
4.编译。 对第3步中读取到的入口文件内容进行编译,根据配置信息匹配相对于的Loader进行编译,同时递归地对该文件所依赖的的文件/资源匹配相对于的Loader进行编译。
5.完成编译。 第四步中,得到每个模块被编译后的内容,以及模块之间的依赖关系。
6.准备输出。 根据第5步中的编译内容和模块的依赖关系,将每一个主入口文件和其所依赖的所有模块组成一个chunk,根据配置的entry得到一个chunk列表。
7.输出到文件。 根据第6步的结果结合webpack配置信息中的output参数按照指定的格式,对每一个输出chunk进行命名,chunk内容转换(主要是指输出的模块类型,比如指定输出amd,umd等)并输出到指定的路径中。
八、参考资料:
1、https://medium.com/webpack/the-contributors-guide-to-webpack-part-2-9fd5e658e08c
2、https://juejin.im/post/5abf33f16fb9a028e46ec352
3、https://frontendmasters.com/courses/webpack-plugins/ 强烈推荐,webpack核心开发者Sean Larkin视频讲解webpack
4、极客时间《玩转webpack4》课程
以上是关于webpck深入浅出教程webpack源码分析的主要内容,如果未能解决你的问题,请参考以下文章
[js高手之路]深入浅出webpack教程系列6-插件使用之html-webpack-plugin配置(下)