随着前端渲染这种开发模式变得越来越广泛,对于前端代码的自动打包以及资源的整合的需求变得日益强烈,Webpack 作为自动打包工具的必要之选,除了提供必备的打包功能,还提供了多种官方组件以及开发者自己定义组件的能力,已经成了前端工程化中不可缺少的一部分。在本文中,笔者会对 Webpack 源代码进行分析,让我们了解 Webpack 的执行流程,也对一些常用的插件代码进行解析,在大家阅读完本文 后,可以模仿这些代码,编写自己的插件。
在我们使用 Webpack 的时候,一般有两种使用方式,第一种是定义好 webpack.config.js
然后在命令行调用 webpack
来进行打包;第二种方式是使用自动化构建工具,如 Gulp
、Grunt
等,通过代码编写构建流程。在使用第二种方法的时候,需要使用 const webpack = require('webpack');
来引入 Weboack
模块,再引入本地 webpack.config.js
文件 const config = require('./webpack.config');
,最后在初始化的时候,将 config
作为 webpack
的初始化参数传入:const compiler = webpack(webpackConfig);
无论是第一种方式还是第二种方式,最后在生成 webpack
对象的时候都是统一的入口。在本次源码解析的时候,我们选择分析通过命令行的形式调用 Webpack
的方法,同时也学习一下 Webpack
对命令行解析的处理逻辑。
本文的 Webpack 版本为 2.2.1
Webpack 命令行解析
在看其他的代码的时候,比较好的流程是先看 package.json
,可以了解本项目的入口文件,依赖的模块等,我们可以看看本项目的 package.json
,寻找相关的线索:
在 package.json
中,我们可以找到 bin
中对应的代码位置:
1 | "bin": { |
表示的是,在命令行中使用 webpack
配合一些参数来执行打包任务的时候,就会自动调用该文件中的代码,我们可以看一看这部分代码的内容。
这部分代码很长,我们分拆开来看看:
1 | var path = require("path"); |
首先,查找本地版本的 Webpack,然后使用本地版本来代替全局版本。
1 | var yargs = require("yargs") |
如何实现 Webpack 和命令行进行交互呢?在 Webpack 中使用的是 yargs
这个模块来解决如何处理命令行参数。
其中,在 ./config-yargs
中,会设置 webpack --help
后在命令行中返回的提示信息,其中包括了所有的 Webpack 可以接受的参数。
在设置完 webpack
可以接受的参数以后,然后调用 yargs.parse
来处理用户在命令行的输入:
1 | yargs.parse(process.argv.slice(2), (err, argv, output) => { |
其中,我们来看一看 yargs.parse
接受的参数。当我们输入 webpack --help
的时候,我们在 process.argv.slice(2)
就可以获得 --help
这个参数,至于 process.argv
中有什么,我们可以打印一下:
1 | [ '/Users/yuchen/.nvm/versions/node/v7.10.0/bin/node', |
我们可以看到,在 argv
中的参数,第一个是 node 的应用程序位置,第二个是 webpack.js
的位置,从第三个位置起,就是我们在输入 webpack
的时候附带的参数。
在 yargs.parse
中,接收的第二个参数是一个回调函数:
1 | (err, argv, output) => { |
同样,打印出回调函数的各个参数,我们可以得到:
err
:表示执行该交互命令解析时报的错,如传入的参数不存在等
argv
:是一个对象,对象中的键值对表示用户输入的参数。如,当用户输入:webpack --help
的时候,argv
中的内容为:
1 | { _: [], |
对应 help
和 h
都为 true
。同理,如果 webpack
命令附带了其他的参数,都可以在 argv
中查到。
output
作为最后的参数,表示命令行最终显示给用户的结果。
在知道了输入的参数分别代表了什么以后,我们来看一看 yargs.parse
对参数的处理流程:
首先,对 argv
中的参数进行处理,在原来 argv
中的参数,是命令行形式的参数,现在将用户的输入,以及 webpack.config.js
中的配置合成在一起,作为用户最终需要的参数:
1 | var options = require("./convert-argv")(yargs, argv); |
我找了一个之前的项目,输入 webpack --watch
,打印了一下 options
中的内容:
1 | { entry: |
我们可以看到,这里的参数,混合了 webpack.config.js
中的内容,也包含了 watch: true
的内容,表示最终的配置。
获得了 options
之后,接下来运行 processOptions (options)
对 opetions
进行处理,我们可以看一下 processOptions
中的内容,对 processOptions
整体流程的解析,参考了这篇文章 [webpack]源码解读:命令行输入webpack的时候都发生了什么?。
1 | function processOptions (options) { |
我们可以看到,主要的流程是使用 webpack
,通过传入 opetions
来得到 compiler
。如果设置了 watch
,就调用 compiler.watch
,如果没有设置 watch
,则调用 compiler.run
。
下面,我们可以查看 "../lib/webpack.js
中 webpack
的定义,以及其中的流程处理。
lib/webpack.js 处理流程
1 | // lib/webpack.js |
在 lib/webpack.js
中,做了以下事情:
- 定义了对外提供的
webpack
函数 - 在
webpack
对象上设置了一些常用属性和插件,这些插件也是我们在对打包过程进行个性化处理的时候经常使用的插件
首先,我们来分析一下对外提供的 webpack
函数中的内容。
webpack 函数分析
我们来看一下 webpack
函数中的内容:
1 | function webpack(options, callback) { |
其中 class Compiler extends Tapable
说明 Compiler
继承自 Tapable
,Tapable
是一个小型库,能够让我们为javascript模块添加并应用插件。可以参考 Webpacl 中 Tapable 的介绍
在这里的重点,是 WebpackOptionsApply().process(options, compiler)
这一句,通过 WebpackOptionsApply
来逐个编译 webpack
编译对象,下面,我们来看看 WebpackOptionsApply
的处理流程
lib/WebpackOptionsApply.js 处理流程
本文中,处理流程分析参考 [webpack]源码解读:命令行输入webpack的时候都发生了什么?
在这里的调用方法是 WebpackOptionsApply().process(options, compiler)
,所以,我们主要关心 process
方法的流程:
1 | process(options, compiler) { |
我们可以在上面的代码中看到 webpack 文档中 Configuration 中介绍的各个属性,同时看到了这些属性对应的处理插件都是谁。
UglifyJsPlugin 处理流程
plugin是一个具有 apply
方法的 js对象。 apply
方法会被 Webpack的 compiler
(编译器)对象调用,并且 compiler
对象可在整个 compilation
(编译)生命周期内访问。
webpack插件的组成:
- 一个JavaScript函数或者class(ES6语法)。
- 在它的原型上定义一个apply方法。
- 指定挂载的webpack事件钩子。
- 处理webpack内部实例的特定数据。
- 功能完成后调用webpack提供的回调。
我们可以通过 UglifyJsPlugin.js
来理解 Webpack 的处理流程,具体解释可以参考 [webpack]源码解读:命令行输入webpack的时候都发生了什么?
1 | // 引入一些依赖,主要是与 sourceMap 相关 |
从这个插件的源码分析,我们可以基本看到 webpack 编译时的读写过程大致是怎么样的:实例化插件(如 UglifyJsPlugin )–> 读取源文件 –> 编译并输出
下面是非常简洁的代码逻辑:
1 | class UglifyJsPlugin { |
在webpack插件开发中最重要的两个核心概念就是 compiler 和 compilation 。理解他们是扩展webpack功能的关键。
webpack之plugin内部运行机制 这篇文章中有 compiler 和 compilation 的详细介绍。
·compiler
对象代表的是配置完备的Webpack环境。 compiler
对象只在Webpack启动时构建一次,由Webpack组合所有的配置项构建生成。
compilation
对象代表了一次单一的版本构建和生成资源。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,一次新的编译将被创建,从而生成一组新的编译资源。一个编译对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。编译对象也提供了很多关键点事件回调供插件做自定义处理时选择使用。
总结
现在我们回过头来再看看整体流程,当我们在命令行输入 webpack 命令,按下回车时都发生了什么:
- 执行 bin 目录下的 webpack.js 脚本,解析命令行参数以及开始执行编译。
- 调用 lib 目录下的 webpack.js 文件的核心函数 webpack ,实例化一个 Compiler,继承 Tapable 插件框架,实现注册和调用一系列插件。
- 调用 lib 目录下的 /WebpackOptionsApply.js 模块的 process 方法,使用各种各样的插件来逐一编译 webpack 编译对象的各项。
- 在3中调用的各种插件编译并输出新文件。