通常在项目中有两种运行webpack的方式,基于命令行的方式以及基于代码的方式,如下所示:
// 命令
webpack --config webpack.config.js
//代码
const webpack = require('webpack')
const config = require('./webpack.config.js')
webpack(config,(err)=>{})
...
命令行的方式,就是我们比较熟悉的,我们在npm script中编写脚本去执行,我们可以把它e的config指向一个配置文件,这是命令行的方式。
基于代码的方式呢,就是我们去写一个nodejs文件,在JS文件里面去require webpack的依赖包,以及去require相关 配置文件,然后去通过执行的方式去调用。
需要注意的是,如果是以代码的方式去执行的话,这个执行函数要不就单独赋值给一个compile的变量,然后去执行这个compile的获取的流程,要不就像这个示例代码中那样,在执行外派的方法时去传入一个回调函数。只有这样他才会去真正的去执行到运行run的那个阶段。如果单单是传入config,实际上是不会去往下走。
const webpack = (options,callback)=>{
options =... // 处理相关配置
let compiler = new Complier(options.context)
WebpackOptionsApply().process(options,compiler);
// 分析参数,加载插件
if(callback){
...
compiler.run(callback)
}
return compiler
}
无论使用哪种方式去运行外派,它本质上都是webpackJS中的webpack函数。
这个函数的核心逻辑是根据配置来生成编译器的实例compiler,然后去处理参数,执行WebpackOptionsApply.process(),然后根据参数来加载不同的插件, 然后根据参数来加载不同的插件,在有回调函数的情况下,根据是否是watch模式来决定是执行compiler.watch()还是compiler.run()。
运行还是compiler.run()的执行逻辑也会比较复杂,我们把它按照流程抽象一下,抽象后的执行流程是下面这样:
首先我们去readRecords读取构建记录,而且可以用于分包缓存优化,在未设置recordsPath时直接返回。
第二步是compil了的主要构建流程,涉及到以下几个环节:
- 首先是newCompilationParams来创建这个构建过程的参数,创建NormalModules和ContextModule的工厂实力,用于创建后续的模块实例。
- 第二个是new compilation创建编译过程的实例,传入上一步的两个工厂实例作为参数。
- 第三步是执行compiler.hooks.make.callAsync, 它会触发make的这个hook,所有监听了make的插件都会在这个阶段进行执行处理,他会在具体的这个插件中去触发compilation的方法。
- 第四步,当这个make的这个过程执行完成之后,我们进入到compilation.finish,表示编译过程结束,会在finish触发后,调用相应的钩子方法并报告构建模块的错误警告。
- 第五步是compilation.seal(),它里面主要包含编译之后的所有优化过程。
在整个这个构建过程完成之后,下一步是emitAssets会调用第五步是compilation.getAssets()获取产物内容,然后去写入到输出文件中。
最后是emitRecords用于写入构建记录。在未设置recordsPath时直接返回。
在编译器运行流程中,核心的过程是第二步编译的过程。具体流程是在生成的compilation的实例中进行。
其中在编译执行过程中,主要从外部调用的是这个编码文件中的两个方法:
- 第一个是addEntry这个方法,这个方法会从这个配置文件中的entry开始递归的添加和构建模块。
- 第二个是在编译过程执行完成之后走到seal()方法,这个方法它的作用是冻结模块,表示在这个阶段开始上面的编译过程都结束了,他会对已经编译过的模块进行一系列优化,然后触发个优化阶段的Hooks。
以上就是执行webpack构建时的基本流程。
首先,第一步创建编译器compiler的实例。
第二步,根据webpack的参数加载相关的插件,不管参数中插件还是程序内置的插件,都在这个阶段进行加载。
第三步,执行编译流程,创建编译过程的compilation的实例,从入口递归添加和构建模块。构建完成之后,共建模块,然后进行相关的优化。
第四步,构建优化过程结束后,提交产物,将产物内容写入输入文件中。
除了了解上面的基本工作流程外,还有两个相关的概念需要理解,那就是webpack的生命周期和插件系统。
webpack工作流程中核心的模块,Compiler和compilation都扩展自Tapple这个类。
用于实现工作流程中的生命周期划分,以便在不同的生命周期节点上注册和调用插件,所暴露出来的生命周期节点,我们就称为hooks组成的钩子。
webpack中的插件又怎么回事儿呢?
webpack引擎是基于插件系统搭建而成的,不同的插件各司其职,在webpack的工作流中的某一个或者多个时间点上,对构建流程的某一个方面进行处理。
class MyWebPackPlugin{
apply(compiler){
compiler.hooks.run.tap('MyWebPackPlugin',complation=>{
console.log('MyWebPackPlugin--->')
})
}
}
webpack就是通过这样的工作方式,在各生命周期中经过一系列插件,将源代码逐步的变为最后的产物。
一个外派的插件是一个包含apply方法的js对象,它定义的这个插件是一个类,一般来说我们会new这个类,然后生成一个这个实例对象。
这个Apply方法的执行逻辑通常是注册webpack工作流程中的某一个生命周期的hook,然后添加对应hook,用该插件的实际处理函数。
Hooks的使用我们可以分为四步:
- 第一步是在各函数定义hook的类型和参数来生成这个hook的对象。
- 第二个是在插件中注册对应的hook添加对应后触发时的具体执行函数。
- 第三步是在webpack运行时去生成这个插件实例。
- 第四步是在运行到对应生命周期节点时,去调用hook,执行注册过插件的回调函数。
// compiler.js
this.hooks = {
...
make: new SyncHook(['compilation','params']),// 定义 HOOKS
...
}
this.hooks.compilation.call(compilation,params)// 调用 hook
// commonjsPlugin.js
// 插件中注册 hook
compiler.hooks.compilation.tap('commonjsPlugin',(compilation,{contextModuleFactory,})=>{...})
// webpackOptionsApply.js
// 生成插件实例,执行 apply方法
new CommonjsPlugin(options.module).apply(compiler)
webpack中hook一般使用方式,就是通过这种方式,webpack能够将编译器和编译过程的生命周期节点提供给外部插件,从而搭建起一个弹性化的工作引擎。
hook的类型按照同步或者异步,是否接收上插件的返回值等情况一共可以分为九种不同类型,不同类型的hook接收注册的方法也不相同,具体信息可以参考官方文档。
(完)
没有关注公众号的朋友,觉得文章对您有启发的话,记得点赞、关注、评论、转发一下。