前端工程化-webpack工作流程
Posted natsu-cc
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端工程化-webpack工作流程相关的知识,希望对你有一定的参考价值。
前言:
目前,webpack作为前端最流行的项目构建工具,然而webpack内部的执行过程却是黑盒的,作为模块加载和打包神器,只需配置几个文件,加载各种 loader 就可以享受无痛流程化开发,对于 webpack 这样一个复杂度较高的插件集合,我们应该对webpack内部的执行流程进行一定了解,便于我们更好的掌握项目的构建和运行。
工作原理:
基本概念:
- Entry: 入口,webpack构建第一步从entry开始。
- Module:模块,在webpack中一个模块对应一个文件。webpack会从entry开始,递归找出所有依赖的模块。
- Chunk:代码块,一个chunk由多个模块组合而成,用于代码合并与分割。
- Loader: 模块转换器,用于将模块的原内容按照需求转换成新内容。
- Plugin:拓展插件,在webpack构建流程中的特定时机会广播对应的事件,插件可以监听这些事件的发生,在特定的时机做对应的事情。
- Output:输出结果,源码在Webpack中经过一系列处理后而得出的最终结果。
Webpack构建流程:
- 初始化参数
解析 webpack 配置参数,合并 shell 传入和 webpack.config.js 文件配置的参数,形成最后的配置结果。 - 开始编译
上一步得到的参数初始化 compiler 对象,注册所有配置的插件,插件监听 webpack 构建生命周期的事件节点,做出相应的反应,执行对象的 run 方法开始执行编译。 - 确定入口
从配置文件( webpack.config.js )中指定的 entry 入口,开始解析文件构建 AST 语法树,找出依赖,递归下去。 - 编译模块
递归中根据文件类型和 loader 配置,调用所有配置的 loader 对文件进行转换,再找出该模块依赖的模块,再递归本步骤,直到所有入口依赖的文件都经过了本步骤的处理。 - 完成模块编译并输出
递归完后,得到每个文件结果,包含每个模块以及他们之间的依赖关系,根据 entry 配置生成代码块 chunk 。 - 输出完成
输出所有的 chunk 到文件系统。
注意:在构建生命周期中有一系列插件在做合适的时机做合适事情,比如 UglifyPlugin 会在 loader 转换递归完对结果使用 UglifyJs 压缩覆盖之前的结果。
- createAssets : 收集和处理文件的代码;
- createGraph :根据入口文件,返回所有文件依赖图;
- bundle : 根据依赖图整个代码并输出;
webpack.config.js配置:
module.exports =
entry: '',
output:
path: '',
filename: '',
publicPath: '',
,
module:
rules: [
test: /\\.css?$/,
use: [
'style-loader',
loader: 'css-loader',
options:
// 参数传递
],
include: [],
exclude: []
],
noParse: []
,
plugins: [],
rosolve:
modules: [],
extensions: [],
alias: []
,
performance:
hints: '',
maxAssetSize: 20000,
maxEntrypointSize: 40000
,
devTool: '',
context: '',
target: '',
externals: ,
stats: ,
devServer:
proxy: '',
contentBase: '',
compress: '',
hot: '',
https: '',
cache: '',
watch: '',
profile: true
,
watchOptions:
ignored: '',
aggregateTimeout: 300,
poll: 1000
- entry:指定了模块的入口,它让源文件加入构建流程中被webpack控制。
- output:配置如何输出最终的代码结果。
- module:配置各种类型的模块的处理规则和解析策略。
- rosolve:配置webpack寻找模块的规则。
- plugin:配置扩展插件,扩展webpack的更多功能。
- devServer:配置devServer,实现本地http服务、模块热替换、source map调试等。
module 和 chunk 的关系:
流程总览:
- shell 与 config 解析:
命令行输入 webpack 后,操作系统都会去调用 ./node_modules/.bin/webpack 这个 shell 脚本。这个脚本会去调用 ./node_modules/webpack/bin/webpack.js 并追加输入的参数。webpack.js 这个文件中 webpack 通过 optimist 将用户配置的 webpack.config.js 和 shell 脚本传过来的参数整合成 options 对象传到了下一个流程的控制对象中。
- optimist:
与commander 一样,optimist 实现了 node 命令行的解析,分析参数并以键值对的形式把参数对象保存在 optimist.argv 中。
var optimist = require("optimist");
optimist
.boolean("json").alias("json", "j").describe("json")
.boolean("colors").alias("colors", "c").describe("colors")
.boolean("watch").alias("watch", "w").describe("watch")
...
- config 合并与插件加载:
加载插件之前,webpack 将 webpack.config.js 中的各个配置项拷贝到 options 对象中,并加载用户配置在 webpack.config.js 的 plugins 。接着 optimist.argv 会被传入到./node_modules/webpack/bin/convert-argv.js 中,通过判断 argv 中参数的值决定是否去加载对应插件。
options 作为最后返回结果,包含了之后构建阶段所需的重要信息,插件对象一初始化完毕, options 也就传入到了下个流程中。
entry: ,//入口配置
output: , //输出配置
plugins: [], //插件集合(配置文件 + shell指令)
module: loaders: [ [Object] ] , //模块配置
context: //工程路径
...
- 编译与构建流程:
加载配置文件和 shell 后缀参数申明的插件,并传入构建信息 options 对象后,webpack 对象开始初始化。
function webpack(options)
var compiler = new Compiler();
...// 检查options,若watch字段为true,则开启watch线程
return compiler;
...
webpack 的实际入口是 compiler 中的 run 方法,run 一旦执行后,开始编译和构建流程 ,其中有几个比较关键的 webpack 事件节点:
- compile 开始编译
- make 从入口点分析模块及其依赖的模块,创建这些模块对象
- build-module 构建模块
- after-compile 完成构建
- seal 封装构建结果
- emit 把各个chunk输出到结果文件
- after-emit 完成输出
-
核心对象 Compilation:
compiler.run 后首先会触发 compile ,构建出 Compilation 对象:
Compilation 对象负责组织整个打包过程,包含了每个构建环节及输出环节所对应的方法,addEntry() , _addModuleChain() ,buildModule() , seal() , createChunkAssets() (在每一个节点都会触发 webpack 事件去调用各插件);该对象内部存放着所有 module ,chunk,生成的 asset 以及用来生成最后打包文件的 template 的信息。 -
编译与构建主流程:
创建 module 之前,Compiler 会触发 make,并调用 Compilation.addEntry 方法,通过 options 对象的 entry 字段找到入口js文件。之后,在 addEntry 中调用私有方法_addModuleChain ,这个方法主要做了两件事情:一是根据模块的类型获取对应的模块工厂并创建模块,二是构建模块。
- 调用各 loader 处理模块之间的依赖;
webpack 能将所有资源都整合成模块,不仅仅是 js 文件。所以需要一些 loader ,比如 url-loader , jsx-loader , css-loader 等等来让我们可以直接在源文件中引用各类资源。webpack 调用 doBuild() ,对每一个 require() 用对应的 loader 进行加工,最后生成一个 js module。 - 调用 acorn 解析经 loader 处理后的源文件生成抽象语法树 AST;
- 遍历 AST,构建该模块所依赖的模块;
对于当前模块,或许存在着多个依赖模块。当前模块会开辟一个依赖模块的数组,在遍历 AST 时,将 require() 中的模块通过 addDependency() 添加到数组中。当前模块构建完成后,webpack 调用 processModuleDependencies 开始递归处理依赖的 module,接着就会重复之前的构建步骤。
打包输出:
所有模块及其依赖模块 build 完成后,webpack 会监听 seal 事件调用各插件对构建后的结果进行封装,要逐次对每个 module 和 chunk 进行整理,生成编译后的源码,合并,拆分,生成 hash 。 同时这是我们在开发时进行代码优化和功能添加的关键环节。
Compilation.prototype.seal = function seal(callback)
this.applyPlugins("seal"); // 触发插件的seal事件
this.preparedChunks.sort(function(a, b)
if (a.name < b.name)
return -1;
if (a.name > b.name)
return 1;
return 0;
);
this.preparedChunks.forEach(function(preparedChunk)
var module = preparedChunk.module;
var chunk = this.addChunk(preparedChunk.name, module);
chunk.initial = chunk.entry = true;
// 整理每个Module和chunk,每个chunk对应一个输出文件。
chunk.addModule(module);
module.addChunk(chunk);
, this);
this.applyPluginsAsync("optimize-tree", this.chunks, this.modules, function(err)
if (err)
return callback(err);
... // 触发插件的事件
this.createChunkAssets(); // 生成最终assets
... // 触发插件的事件
.bind(this));
;
- 生成最终 assets:
封装过程中,webpack 会调用 Compilation 中的 createChunkAssets 方法进行打包后代码的生成。
- 不同的 Template
四个 Template 的子类,分别是 MainTemplate.js , ChunkTemplate.js,ModuleTemplate.js , HotUpdateChunkTemplate.js。 - 模块封装
模块在封装的时候和它在构建时一样,都是调用各模块类中的方法。封装通过调用module.source() 来进行各操作,比如说 require() 的替换。 - 生成 assets
各模块进行 doBlock 后,把 module 的最终代码循环添加到 source 中。一个 source 对应着一个 asset 对象,该对象保存了单个文件的文件名( name )和最终代码( value )。
- 输出:
webpack 调用 Compiler 中的 emitAssets() ,按照 output 中的配置项将文件输出到了对应的 path 中,从而 webpack 整个打包过程结束。要注意的是,若想对结果进行处理,则需要在 emit 触发后对自定义插件进行扩展。
tapable:
tapable库简介:
tapable这个库提供了一系列hooks类,这些hooks类可以用来为插件创建钩子,在指定的时候执行对应钩子中注册的函数。
const
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
= require("tapable");
总体上分为三种类型的hooks类:
- 普通型:会在钩子触发时依次执行每一个被注册的函数。
- 流水型:会在钩子触发时执行每一个被注册的函数,但是上一个函数的返回值会被传递到下一个函数中。
- 熔断型:会在钩子触发时执行被注册的函数,一旦某一个函数有返回值,则立即返回,后续的回调不再处理。
每种类型由同步,异步又可以分为三种类型:
- Sync: 同步执行。
- AsyncSeries:异步顺序执行。
- AsyncParallel: 异步并行执行。
compiler对象创建和plugins注入:
// 将传入的options和default Options合并, 获取到最终的配置项
options = new WebpackOptionsDefaulter().process(options);
// 生成compiler对象
compiler = new Compiler(options.context);
compiler.options = options;
// compiler文件处理相关方法挂载, NodeEnvironmentPlugin事件注册
new NodeEnvironmentPlugin(
infrastructureLogging: options.infrastructureLogging
).apply(compiler);
// webpack插件在这里注册相应的hooks事件
if (options.plugins && Array.isArray(options.plugins))
for (const plugin of options.plugins)
if (typeof plugin === "function")
plugin.call(compiler, compiler);
else
plugin.apply(compiler);
// environment准备好了,相关hooks执行
compiler.hooks.environment.call();
// environment准备完毕,相关hooks执行
compiler.hooks.afterEnvironment.call();
// 这一行的主要工作依然还是注册plugin
// webpack的整个构建过程就是通过在不同的时机执行hooks内注册的plugin来完成的
compiler.options = new WebpackOptionsApply().process(options, compiler);
webpack会把我们传入的options和webpack内部自定义的options进行合并,并创建一个compiler对象。compiler对象内部有很多hooks,之后遍历options.plugins,为每一个plugin注册执行时机。
如下图:
compile流程:
创建完毕compiler对象后,我们会通过compiler.run()方法启动编译流程。
run方法内部会依次触发beforeRun钩子和run钩子,在run钩子的回调中执行compile方法,并传入onCompiled回调。
make这个钩子是很重要的一个钩子,在这个钩子中我们会调用Single/MultiEntryPlugin,从入口文件开始遍历打包。
总结:
webpack 的整体流程主要还是依赖于 compilation 和 module 这两个对象,本质是个插件集合,并且由 tapable 控制各插件在 webpack 事件流上运行。
以上是关于前端工程化-webpack工作流程的主要内容,如果未能解决你的问题,请参考以下文章