webpack工作流程

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了webpack工作流程相关的知识,希望对你有一定的参考价值。

参考技术A Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程 :

1.初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
2.开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
3.确定入口:根据配置中的 entry 找出所有的入口文件。
4.编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
5.完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
6.输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
7.输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

前端工程化-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 对象传到了下一个流程的控制对象中。
  1. 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")
  ...
  1. 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 完成输出
  1. 核心对象 Compilation:
    compiler.run 后首先会触发 compile ,构建出 Compilation 对象:

    Compilation 对象负责组织整个打包过程,包含了每个构建环节及输出环节所对应的方法,addEntry() , _addModuleChain() ,buildModule() , seal() , createChunkAssets() (在每一个节点都会触发 webpack 事件去调用各插件);该对象内部存放着所有 module ,chunk,生成的 asset 以及用来生成最后打包文件的 template 的信息。

  2. 编译与构建主流程:
    创建 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));
;
  1. 生成最终 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 )。
  1. 输出:
    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工作流程的主要内容,如果未能解决你的问题,请参考以下文章

webpack执行机制流程是怎么样的

前端工程化-webpack工作流程

前端工程化-webpack工作流程

前端工程化9:Webpack构建流程分析,Webpack5源码解读

无法使 webpack HMR 工作

Webpack 2 开发工具不工作