webpack

工作原理

基本概念

  • Entry: 入口,执行构建的输入
  • Module: 模块,在Webpack里一切皆模块,Webpack会从Entry开始,递归找出所有依赖的模块
  • Chunk: 代码块,一个Chunk由多个模块合成,用于代码合并和分割
  • Loader: 模块转换器,用于将模块的源内容按照需求转换成新内容
  • Plugin: 扩展插件,在构建流程中的特定时机会广播对应的事件,插件可以监听这些事件,做出对应的事情

流程

Webpack的运行流程是一个串行的过程,从启动开始依次是:

  1. 初始化参数:从配置文件和Shell语句中读取与合并参数,得出最终参数
  2. 开始编译:基于最终的参数初始化Compiler对象,加载所有配置的插件,通过执行对象的run方法开始编译
  3. 确定入口:根据配置中的Entry找出所有入口文件
  4. 编译模块:从入口文件出发,调用所有配置的Loader对模块递归的进行编译
  5. 完成模块编译:得到每个模块编译后的内容及之间的依赖关系
  6. 输出资源:根据入口及模块之前的依赖关系,组装成一个个包含多个模块的Chunk,再将每个Chunk转换成一个单独的文件加入输出列表,这是修改输出内容的最后机会
  7. 输出完成:根据配置的输出路径和文件名,将文件内容写入文件系统中

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

整体构建流程可以分为以下三大阶段:

  • 初始化,启动构建,处理配置参数,加载Plugin,实例化Compiler
  • 编译,从Entry开始,针对每个Module串行调用对应Loader去编译内容,并依据Module之间的依赖关系,递归的进行编译处理
  • 输出,将编译后的Module组合成Chunk,然后转换成文件,输出到文件系统

build时,只执行上述阶段一次。监听模式下,将不断的执行编译,输出阶段

模块Module

在模块化编程中,开发者将程序分解成离散功能块(discrete chunks of functionality),并称之为模块。

Webpack模块能够以各种方式表达他们的依赖关系,例如:

  • ES6``import语句
  • CommonJS``require()语句
  • AMD``definerequire语句
  • css/sass/less文件中的@import语句
  • 样式(url(...))或HTML文件(<img src=...>)中的图片链接(image url)

Loader

以处理SCSS文件为例:

1module.exports = {
2    module: {
3        rules: [{
4            test: /\.scss/,
5            use: [
6                'style-loader',
7                {
8                    loader: 'css-loader',
9                    options: {
10                        minimize: true,
11                    }
12                },
13                'sass-loader',
14            ]
15        }]
16    },
17}

一个Loader的职责是单一的,只负责一种转换。一个源文件需要多次转换,需要配置多个Loader。在调用多个Loader时,执行顺序是从后往前依次执行

基本写法

Loader就是一个函数,获取处理前的内容,返回处理后的内容

1module.exports = function(source){
2    // 转换逻辑省略
3
4    return source
5}

可调用的Webpack API

获取Loaderoptions

1const loaderUtils = require('loader-utils')
2
3module.exports = function(source){
4    const options = loaderUtils.getOptions(this)
5
6    // 转换逻辑省略
7
8    return source
9}

返回其他结果

直接return可以返回原内容转换后的内容,返回其他内容需要用到this.callback函数:

1this.callback(
2    // 当无法转换原内容时,为Webpack返回一个Error
3    error: Error | null,
4    // 原内容转换后的内容
5    content: String | Buffer,
6    // 用于通过转换后的内容得出原内容的Source Map,以方便测试
7    sourceMap?: SourceMap,
8    // 如果本次转换为原内容生产了AST语法树,则可以将这个AST返回,以方便之后需要AST的Loader复用
9    abstractSyntaxTree?: AST
10)

此外,由于Source Map生产很耗时,通常在开发环境下才生成。因此可以通过this.sourceMapAPI来配置是否生成Source Map

同步与异步

异步转换流程如下:

1module.exports = function(source){
2    const callback = this.async()
3
4    // 转换逻辑省略
5    someAsyncOperation(source).then( (err, result, sourceMap, ast) => {
6        callback(err, result, sourceMap, ast)
7    })
8
9    return source
10}

处理二进制数据

对于二进制数据,需要将exports.raw = ture:

1module.exports = function(source){
2    return source
3}
4
5// 通过exports.raw属性告诉Webpack该Loader是否需要二进制数据
6module.exports.raw = true

缓存加速

可以通过this.cacheable()方法设置是否缓存计算结果,默认是开启的

1module.exports = function(source){
2
3    // 关闭缓存
4    this.cacheable(false)
5
6    return source
7}

其他Loader API

API简介
this.context当前处理的文件所在目录,以/src/main.js为例,为/src
this.resoure当前处理的完整请求路径,包括query string,例如/src/main.js?name=1
this.resourePath当前处理文件的路径,例如/src/main.js
this.resoureQuery当前处理文件的query string
this.targetWebpack中配置的Target
this.loadModule(request: string, callback: function(err, source, sourceMap, ast))Loader在处理一个文件时,需要加入依赖时,用于获取对应文件的处理结果
this.addDependency(file: string)为当前处理的文件添加依赖。其依赖文件发生变化时,会重新调用loader处理该文件
this.addContextDependency(directory: string)将整个目录加入到正在处理的文件依赖中
this.clearDependencies()清除当前处理文件的多有依赖
`this.emitFile(name: string, content: Bufferstring, sourceMap)`

Plugin

Compiler和Compilation

开发Plugin最常用的两个对象就是CompilerCompilation,他们是PluginWebpack之间的桥梁。

  • Compiler包含所有配置信息(optionsloadersplugins等),在Webpack启动时被实例化,全局唯一。代表了整个Webpack从启动到关闭的生命周期
  • Compilation包含当前的模块资源、编译生成资源和变化的文件等。在开发模式下,每当有文件变化时,就有一次新的Compilation被创建。代表了一次编译过程

Tapable

Webpack通过Tapable来组织构建的事件流。Webpack中许多对象(包括CompilerCompilation)都继承自Tapable

Tapable暴露了tap, tapAsynctapPromise方法。可以使用这些方法,注入自定义的构建步骤,这些步骤将在整个编译过程中不同时机触发。

都自Tapable,可以广播和监听事件:

1/**
2 * eventName 事件名,不要跟现有事件重名
3 * params    附带参数
4 */
5compiler.apply(eventName, params)
6
7// 监听事件
8compiler.plugin(eventName, callback)

每个插件的CompilerCompilation对象都是同一个引用。修改CompilerCompilation对象上的属性就会影响后面的插件。有些事件是异步的,插件处理完成时调用回调函数通知Webpack,才能进入下一个流程。

1compiler.plugin('emit', function handle(compilation, callback) {
2    // 处理逻辑省略
3
4    // 调用callback通知Webpack进入下一个流程
5    callback()
6})

基本写法

Plugin主要通过监听Webpack在运行的生命周期中广播的特定事件,在合适的时机通过Webpack提供的API做出对应的处理,改变输出结果

一个Plugin就是一个类,具体写法如下:

1class TestPlugin{
2
3    // 在构造函数中可以获取用户对Plugin的配置
4    constructor(options){
5
6    }
7
8    // Webpack会调用实例的apply方法,并传入compiler对象
9    apply(compiler){
10        compiler.plugin('compilation', function handle(compilation) {
11            
12        })
13    }
14}
15
16module.exports = TestPlugin
17
18// 配置代码如下:
19
20const TestPlugin = require('./TestPlugin.js')
21
22module.exports = {
23    plugins: [
24        new TestPlugin(options)
25    ]
26}

Webpack启动后,读取配置过程中会初始化插件实例。在初始化compiler对象之后,在调用插件的apply方法,为实例传入compiler对象。插件实例在获取到compiler对象后,通过 compiler.plugin(event: string, callback: function(compilation))监听Webpack广播的事件,通过compiler对象去操作Webpack

常用API

插件可以用来修改输出文件和增加输出文件,甚至提升Webpack性能

读取输出资源、代码块、模块及依赖

emit事件发生时,代表源文件的转换和组装已经完成,可以读取到最终输出的资源、代码块、模块及其依赖,并修改输出资源的内容

热更新原理

SSE