# 4. 深入浅出Webpack

# 第一章 入门

# 1.1. 模块化

  1. CommonJS(http://www.commonjs.org)是一种被广泛使用的 JavaScript 模块化规范,其核心思想是通过require方法来同步加载依赖的其他模块,通过module.exports导出需要暴露的接口。

CommonJS优点:

  • 代码可复用于Node.js环境并运行
  • 通过NPM发布很多第三方模块都采用CommonJS规范 CommonJS 的缺点在于,这样的代码无法直接运行在浏览器环境下,必须通过工具转换成标准的ES5
  1. AMD也是一种JavaScript模块化规范,与CommonJS最大的不同在于,它采用了异步的方式去加载依赖的模块。AMD规范主要用于解决针对浏览器环境的模块化问题,最具代表性的实现是 requirejs(http://requirejs.org)。

AMD的优点在于:

  • 可在不转换代码的情况下直接在浏览器中运行
  • 可异步加载依赖;
  • 可并行加载多个依赖;
  • 代码可运行在浏览器环境和Node.js环境下。 AMD的缺点在于:
  • JavaScript运行环境没有原生支持AMD,需要先导入实现了AMD的库后才能正常使用。

# 1.3 安装Webpack

  1. Webpack是一个打包模块化JavaScript的工具,它会从入口文件main.js出发,识别出源码中的模块化导入语句,递归地找出入口文件的所有依赖,将入口和其所有依赖打包到一个单独的文件中。

# 1.4 使用Loader

  1. 每个 Loader 都可以通过 URL querystring 的方式传入参数,例如 css-loader?minimize中的minimize告诉css-loader要开启CSS压缩。
  2. Loader传入属性的方式除了可以通过querystring实现,还可以通过Object实现,配置文件中的options属性

# 1.6 使用DevServer

  1. 通过DevServer启动的Webpack会开启监听模式,当发生变化时重新执行构建,然后通知DevServerDevServer会让Webpack在构建出的JavaScript代码里注入一个代理客户端用于控制网页,网页和DevServer之间通过WebSocket协议通信,以方便DevServer主动向客户端发送命令。DevServer在收到来自Webpack的文件变化通知时,通过注入的客户端控制网页刷新。
  2. 如果尝试修改index.html文件并保存,则我们会发现这并不会触发以上机制,导致这个问题的原因是Webpack在启动时会以配置里的entry为入口去递归解析出entry所依赖的文件,只有entry本身和依赖的文件才会被Webpack添加到监听列表里。而index.html文件是脱离了JavaScript模块化系统的,所以Webpack不知道它的存在。
  3. Webpack支持生成Source Map,只需在启动时带上--devtool source-map参数。重启 DevServer后刷新页面,再打开 Chrome浏览器的开发者工具,就可以在 Sources栏中看到可调试的源代码了。

# 1.7 核心概念

  1. Module:模块,在Webpack里一切皆模块,一个模块对应一个文件。Webpack会从配置的Entry开始递归找出所有依赖的模块。
  2. Chunk:代码块,一个Chunk由多个模块组合而成,用于代码合并与分割。

Webpack在启动后会从Entry里配置的Module开始,递归解析Entry依赖的所有Module。每找到一个Module,就会根据配置的Loader去找出对应的转换规则,对Module进行转换后,再解析出当前Module依赖的Module。这些模块会以Entry为单位进行分组,一个Entry及其所有依赖的Module被分到一个组也就是一个Chunk。最后,Webpack会将所有Chunk转换成文件输出。在整个流程中,Webpack会在恰当的时机执行Plugin里定义的逻辑。

# 第五章 原理

# 5.4 plugin

Webpack通过Plugin机制让其更灵活,以适应各种应用场景。在Webpack运行的生命周期中会广播许多事件,Plugin可以监听这些事件,在合适的时机通过Webpack提供的API改变输出结果。

一个最基础的plugin代码:

class BasicPlugin{
  // 在构造函数中获取用户为该插件传入的配置
  constructor(options) {

  }
  // webpack会调用BasicPlugin实例的apply方法为插件实例传入compiler对象
  apply(compiler) {
    compiler.plugin('compilation', function(compilation) {

    })
  }
}

// 导出plugin
module.exports = BasicPlugin;

// 使用时相关配置
const BasicPlugin = require('./BasicPlugin.js');
module.exports = {
  plugins: [
    new BasicPlugin(options),
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

Webpack启动后,在读取配置的过程中会先执行new BasicPlugin(options),初始化一个 BasicPlugin 并获得其实例。在初始化 compiler 对象后,再调用basicPlugin.apply(compiler)为插件实例传入 compiler 对象。插件实例在获取到compiler 对象后,就可以通过 compiler.plugin(事件名称,回调函数)监听到Webpack 广播的事件,并且可以通过compiler 对象去操作Webpack

# 5.4.1 Compiler和Compilation

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

TIP

Compiler对象包含了Webpack环境的所有配置信息,包含optionsloadersplugins等信息。这个对象在 Webpack 启动时被实例化,它是全局唯一的,可以简单地将它理解为Webpack实例。

Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。当Webpack以开发模式运行时,每当检测到一个文件发生变化,便有一次新的Compilation被创建。Compilation对象也提供了很多事件回调供插件进行扩展。通过Compilation也能读取到Compiler对象。

CompilerCompilation的区别在于:Compiler代表了整个Webpack从启动到关闭的生命周期,而Compilation只代表一次新的编译。

# 5.4.2事件流

Webpack就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。这条生产线上的每个处理流程的职责都是单一的,多个流程之间存在依赖关系,只有在完成当前处理后才能提交给下一个流程去处理。插件就像插入生产线中的某个功能,在特定的时机对生产线上的资源进行处理。

Webpack通过Tapable来组织这条生产线。Webpack在运行的过程中会广播事件,插件只需要监听它所关心的事件,就能加入这条生产线中,去改变生产线的运作。Webpack的事件流机制保证了插件的有序性,使得整个系统的扩展性良好。

Webpack的事件流机制应用了观察者模式,和Node.js中的EventEmitter非常相似。CompilerCompilation都继承自Tapable,可以直接在CompilerCompilation对象上广播和监听事件。

/ **
* 广播事件
* event-name为事件名称,注意不要和现有事件重名
* params为附带的参数
/
compiler.apply('event-name', params);

/ **
* 监听名称为event-name的事件,当event-name事件发生时,函数就会被执行。
* 同时函数中的params 参数为广播事件时附带的参数。
/
compiler.plugin('event-name', function(params) {

})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

TIP

在开发插件时,还需要注意以下两点:只要能拿到CompilerCompilation对象,就能广播新的事件,所以在新开发的插件中也能广播事件,为其他插件监听使用。传给每个插件的CompilerCompilation对象都是同一个引用。也就是说,若在一个插件中修改了CompilerCompilation对象上的属性,就会影响到后面的插件。有些事件是异步的,这些异步的事件就会附带两个参数,第2个参数为回调函数,在插件处理完任务时需要调用回调函数通知Webpack,才会进入下一个处理流程。

compiler.plugin('emit', function(compilation, callback) {
  // 支持处理逻辑
  // 处理完毕后执行callback已通知Webpack
  // 如果不执行callback,运行流程会一直卡在这里不往后执行
  callback();
})
1
2
3
4
5
6

# 5.4.3 最常用的API

  1. 读取输出资源,代码块,模块及其依赖 emit事件发生时,代表源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容。
compiler.plugin('emit', function(compilation, callback) {
  // compilation.chunks存放所有代码块,是一个数组
  compilation.chunks.forEach(function(chunk) {
    // chunk代表一个代码块
    // 代码块由多个模块组成,通过chunk.forEachModule能读取代码块的每个模块
    chunk.forEachModule(function(module) {
      // module代表一个模块
      // module.fileDependencies存放当前模块的所有依赖的文件路径,是一个数组
      module.fileDependencies.forEach(function(filepath){

      })
    })

    // webpack会根据chunk生成输出的文件资源,每个chunk都对应一个及以上的输出文件
    // 例如在chunk中包含css模块并且使用了ExtractTextPlugin时,
    // 该chunk就会生成.js和.css两个文件
    chunk.files.forEach(function(filename) {
      // compilation.assets存放当前即将输出的所有资源
      // 调用一个输出资源的source()方法能获取输出资源的内容
      let source = compilation.assets[filename].source();
    })
  })
  // 这是一个异步事件,要记得调用callback来通知Webpack本次事件监听处理结束
  // 如果忘记了调用callback,则webpack将一直卡在这里而不会往后执行
  callback()
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  1. 监听文件的变化 在开发插件时经常需要知道是哪个文件发生的变化导致了新的Compilation,为此可以使用如下代码:
// 当依赖的文件发生变化时会触发watch-run事件
compiler.plugin('watch-run', (watching, callback) => {
  // 获取发生变化时的文件列表
  const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
  // changedFiles格式为键值对,键为发生变化的文件路径。
  if (changedFiles[filePath] !== undefined) {
    // filePath对应的文件发生了变化
  }
  callback()
})
1
2
3
4
5
6
7
8
9
10

在默认情况下,webpack只会监视入口和其依赖的模块是否发生了变化,在某些情况下项目可能需要引入新的文件,例如引入一个HTML文件。由于JS文件不会导入HTML文件,所以webpack不会监听HTML文件的变化,编辑HTML文件时就不会重新触发新的Compilation。为了监听HTML文件变化,我们需要将HTML文件加入依赖列表中:

compiler.plugin('after-compile', (compilation, callback) => {
  // 将HTML文件添加到文件依赖列表中,好让Webpack监听HTML模版文件,在HTML模版文件变化时重新启动一次编译。
  compilation.fileDependencies.push(filePath);
  callback()
})
1
2
3
4
5
  1. 修改输出资源 在某些场景下插件需要修改,增加,删除输出的资源,要做到这点则需要监听emit事件,因为发生emit事件时所有模块的转换和代码块对应的文件已经生成好,需要输出的资源即将输出,因此emit事件时修改webpack输出资源的最后的时机。所有需要输出的资源都会被存放在compilation.assets中,compilation.assets是一个键值对,键为需要输出的文件名称,值为文件对应的内容。
// 设置compilation.assets的代码如下:
compiler.plugin('emit', (compilation, callback) => {
  // 设置名称为fileName的输出资源
  compilation.assets[fileName] = {
    // 返回文件内容
    source: () => {
      // fileContent既可以是代表文本文件的字符串,也可以是代表二进制文件的Buffer
      return fileContent;
    },
    size: () => {
      return Buffer.byteLength(fileContent, 'utf8');
    }
  }
  callback()
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

读取compilation.assets的代码如下:

compiler.plugin('emit', (compilation, callback) => {
  // 读取名称为fileName的输出资源
  const assets = compilation.assets[fileName];
  // 获取输出资源的内容
  assets.source()
  // 获取输出资源的文件大小
  asset.size()
  callback()
})
1
2
3
4
5
6
7
8
9
  1. 判断Webpack使用了哪些插件 在开发一个插件时,我们可能需要根据当前配置是否使用了其他插件来做下一步决定,因此需要读取Webpack当前的插件配置情况。比如,若想判断当前是否使用了ExtractTextPlugin,则可以使用如下代码:
// 判断当前配置是否使用了ExtractTextPlugin
// compiler参数为webpack在apply(compiler)中传入的参数
function hasExtractTextPlugin(compiler) {
  // 当前配置使用的所有插件列表
  const plugins = compiler.options.plugins;
  // 去plugins中寻找有没有ExtractTextplugin的实例
  return plugins.find(plugin => plugin.__proto__.constructor === ExtractTextPlugin) != null;
}
1
2
3
4
5
6
7
8

TIP

ExtractTextPlugin插件它会将所有的入口 chunk(entry chunks)中引用的 *.css,移动到独立分离的 CSS 文件。因此,你的样式将不再内嵌到 JS bundle 中,而是会放到一个单独的 CSS 文件(即 styles.css)当中。 如果你的样式文件大小较大,这会做更快提前加载,因为 CSS bundle 会跟 JS bundle 并行加载。

Webpack 4.0以后将使用mini-css-extract-plugin代替