天天看点

细说 webpack 之流程篇

细说 webpack 之流程篇

目前,几乎所有业务的开发构建都会用到 webpack 。的确,作为模块加载和打包神器,只需配置几个文件,加载各种 loader 就可以享受无痛流程化开发。但对于 webpack 这样一个复杂度较高的插件集合,它的整体流程及思想对我们来说还是很透明的。那么接下来我会带你了解 webpack 这样一个构建黑盒,首先来谈谈它的流程。

在开始了解之前,必须要能对 webpack 整个流程进行 debug ,配置过程比较简单。

估计大家对 webpack.config.js 的配置也尝试过不少次了,这里就大致对这个配置文件进行个分析。

除此之外再大致介绍下 webpack 的一些核心概念:

loader:能转换各类资源,并处理成对应模块的加载器。loader 间可以串行使用。

chunk:code splitting 后的产物,也就是按需加载的分块,装载了不同的 module。

对于 module 和 chunk 的关系可以参照 webpack 官方的这张图:

细说 webpack 之流程篇

plugin:webpack 的插件实体,这里以 uglifyjsplugin 为例。

在 webpack 中你经常可以看到 compilation.plugin(‘xxx’, callback) ,你可以把它当作是一个事件的绑定,这些事件在打包时由 webpack 来触发。

细说 webpack 之流程篇

每次在命令行输入 webpack 后,操作系统都会去调用 <code>./node_modules/.bin/webpack</code> 这个 shell 脚本。这个脚本会去调用 <code>./node_modules/webpack/bin/webpack.js</code> 并追加输入的参数,如 -p , -w 。(图中 webpack.js 是 webpack 的启动文件,而 $@ 是后缀参数)

细说 webpack 之流程篇

在 webpack.js 这个文件中 webpack 通过 optimist 将用户配置的 webpack.config.js 和 shell 脚本传过来的参数整合成 options 对象传到了下一个流程的控制对象中。

获取到后缀参数后,optimist 分析参数并以键值对的形式把参数对象保存在 optimist.argv 中,来看看 argv 究竟有什么?

在加载插件之前,webpack 将 webpack.config.js 中的各个配置项拷贝到 options 对象中,并加载用户配置在 webpack.config.js 的 plugins 。接着 optimist.argv 会被传入到<code>./node_modules/webpack/bin/convert-argv.js</code> 中,通过判断 argv 中参数的值决定是否去加载对应插件。(至于 webpack 插件运行机制,在之后的运行机制篇会提到)

<code>options</code> 作为最后返回结果,包含了之后构建阶段所需的重要信息。

这和 webpack.config.js 的配置非常相似,只是多了一些经 shell 传入的插件对象。插件对象一初始化完毕, options 也就传入到了下个流程中。

在加载配置文件和 shell 后缀参数申明的插件,并传入构建信息 options 对象后,开始整个 webpack 打包最漫长的一步。而这个时候,真正的 webpack 对象才刚被初始化,具体的初始化逻辑在 <code>lib/webpack.js</code> 中,如下:

webpack 的实际入口是 compiler 中的 run 方法,run 一旦执行后,就开始了编译和构建流程 ,其中有几个比较关键的 webpack 事件节点。

<code>compile</code> 开始编译

<code>make</code> 从入口点分析模块及其依赖的模块,创建这些模块对象

<code>build-module</code> 构建模块

<code>after-compile</code> 完成构建

<code>seal</code> 封装构建结果

<code>emit</code> 把各个chunk输出到结果文件

<code>after-emit</code> 完成输出

compiler.run 后首先会触发 compile ,这一步会构建出 compilation 对象:

细说 webpack 之流程篇

这个对象有两个作用,一是负责组织整个打包过程,包含了每个构建环节及输出环节所对应的方法,可以从图中看到比较关键的步骤,如 <code>addentry()</code> , <code>_addmodulechain()</code> ,<code>buildmodule()</code> , <code>seal()</code> , <code>createchunkassets()</code> (在每一个节点都会触发 webpack 事件去调用各插件)。二是该对象内部存放着所有 module ,chunk,生成的 asset 以及用来生成最后打包文件的 template 的信息。

在创建 module 之前,compiler 会触发 make,并调用 <code>compilation.addentry</code> 方法,通过 options 对象的 entry 字段找到我们的入口js文件。之后,在 addentry 中调用私有方法<code>_addmodulechain</code> ,这个方法主要做了两件事情。一是根据模块的类型获取对应的模块工厂并创建模块,二是构建模块。

而构建模块作为最耗时的一步,又可细化为三步:

调用各 loader 处理模块之间的依赖

webpack 提供的一个很大的便利就是能将所有资源都整合成模块,不仅仅是 js 文件。所以需要一些 loader ,比如 <code>url-loader</code> , <code>jsx-loader</code> , <code>css-loader</code> 等等来让我们可以直接在源文件中引用各类资源。webpack 调用 <code>dobuild()</code> ,对每一个 require() 用对应的 loader 进行加工,最后生成一个 js module。

遍历 ast,构建该模块所依赖的模块

对于当前模块,或许存在着多个依赖模块。当前模块会开辟一个依赖模块的数组,在遍历 ast 时,将 require() 中的模块通过 <code>adddependency()</code> 添加到数组中。当前模块构建完成后,webpack 调用 <code>processmoduledependencies</code> 开始递归处理依赖的 module,接着就会重复之前的构建步骤。

module 是 webpack 构建的核心实体,也是所有 module 的 父类,它有几种不同子类:<code>normalmodule</code> , <code>multimodule</code> , <code>contextmodule</code> , <code>delegatedmodule</code> 等。但这些核心实体都是在构建中都会去调用对应方法,也就是 <code>build()</code> 。来看看其中具体做了什么:

对于每一个 module ,它都会有这样一个构建方法。当然,它还包括了从构建到输出的一系列的有关 module 生命周期的函数,我们通过 module 父类类图其子类类图(这里以 normalmodule 为例)来观察其真实形态:

细说 webpack 之流程篇

可以看到无论是构建流程,处理依赖流程,包括后面的封装流程都是与 module 密切相关的。

在所有模块及其依赖模块 build 完成后,webpack 会监听 <code>seal</code> 事件调用各插件对构建后的结果进行封装,要逐次对每个 module 和 chunk 进行整理,生成编译后的源码,合并,拆分,生成 hash 。 同时这是我们在开发时进行代码优化和功能添加的关键环节。

在封装过程中,webpack 会调用 compilation 中的 <code>createchunkassets</code> 方法进行打包后代码的生成。 createchunkassets 流程如下:

细说 webpack 之流程篇

不同的 template

从上图可以看出通过判断是入口 js 还是需要异步加载的 js 来选择不同的模板对象进行封装,入口 js 会采用 webpack 事件流的 render 事件来触发 <code>template类</code> 中的<code>renderchunkmodules()</code> (异步加载的 js 会调用 chunktemplate 中的 render 方法)。

在 webpack 中有四个 template 的子类,分别是 <code>maintemplate.js</code> , <code>chunktemplate.js</code>,<code>moduletemplate.js</code> , <code>hotupdatechunktemplate.js</code> ,前两者先前已大致有介绍,而 moduletemplate 是对所有模块进行一个代码生成,hotupdatechunktemplate 是对热替换模块的一个处理。

模块封装

模块在封装的时候和它在构建时一样,都是调用各模块类中的方法。封装通过调用<code>module.source()</code> 来进行各操作,比如说 require() 的替换。

生成 assets

各模块进行 doblock 后,把 module 的最终代码循环添加到 source 中。一个 source 对应着一个 asset 对象,该对象保存了单个文件的文件名( name )和最终代码( value )。

最后一步,webpack 调用 compiler 中的 <code>emitassets()</code> ,按照 output 中的配置项将文件输出到了对应的 path 中,从而 webpack 整个打包过程结束。要注意的是,若想对结果进行处理,则需要在 <code>emit</code> 触发后对自定义插件进行扩展。

webpack 的整体流程主要还是依赖于 <code>compilation</code> 和 <code>module</code> 这两个对象,但其思想远不止这么简单。最开始也说过,webpack 本质是个插件集合,并且由 <code>tapable</code> 控制各插件在 webpack 事件流上运行,至于具体的思想和细节,将会在后一篇文章中提到。同时,在业务开发中,无论是为了提升构建效率,或是减小打包文件大小,我们都可以通过编写 webpack 插件来进行流程上的控制,这个也会在之后提到。

转载自:http://taobaofed.org/blog/2016/08/24/react-key/

作者:叶斋

继续阅读