Printable

参与贡献

为 webpack 做出贡献的群体,通常热衷于开源项目、关心用户体验和关注软件生态系统,对这些人来说,更具有意义的事情是,获得共同推动着 web 向前发展的成就感。由于我们使用 Open Collective 资金模型,公开透明地积累和管理资金,所以我们能够通过贡献人员、依赖项目和核心团队获得支持和资金。想要捐款,只需点击下面的按钮……

但是,投入资金的回报情况如何呢?

开发人员

我们希望能为开发人员提供最大程度的愉悦开发体验。开发人员可以通过如下方式为我们提供帮助,例如,贡献丰富有趣的文档、发布 pull request、帮助我们覆盖小众用例,并辅助维护 webpack。

我如何帮助 webpack?

任何人都可以通过以下方式来帮助 webpack:

  • 要求雇主在项目中使用 webpack。
  • 帮助我们编写和维护本网站上的内容(请查看作者指南)。
  • 核心仓库贡献代码。
  • 成为 open collective 的筹款人员(backer)或赞助人员(sponsor)。

要求雇主使用 webpack

可以要求你的雇主通过使用 webpack 来改进工作流程:webpack 是用于字体、图像、图像优化和 json 的 all-in-one 全特性工具。向他们解释 webpack 试图将代码和资源打包为最小文件体积,从而使网站和应用程序更加快速。

贡献代码

向 webpack 贡献代码,并不只是对专属 club 的贡献。你作为开发人员正在为下游项目的整体健康做出贡献。数百个、甚至数千个项目依赖于 webpack,你的贡献将使所有用户的生态系统更加完善。

本章节的其余部分,是专门针对那些「希望成为我们持续增长社区的一部分」的开发人员编写:

管理人员

CTO, VP 和业主也可以向我们提供帮助!

webpack 是打包代码的全特性工具。它可以在由社区驱动的 plugin 和 loader 的帮助下处理字体、图像、数据等。将你的所有资源交付一个工具进行处理,是非常有效地资源管理方案,这样你或你的团队可以花更少的时间,来确保具有许多移动部件的机器正常工作,并且有更多时间构建你的产品。

赞助

除了资金援助外,公司还可以通过以下方式支持 webpack:

  • 提供不活跃但是参与项目的开发人员。
  • 为我们改进 CI 和回归测试,提供计算能力(computing power)。

你还可以鼓励开发人员,向 webpack 生态系统贡献开源的 loader, plugin 和其他工具。最后,如上所述,非常感谢帮助我们添加 CI/CD 基础设施的那些公司。

其他人群

对于那些对我们的使命感兴趣的人 - 例如风险投资者、政府机构、数字营销公司等 - 我们真诚希望你能够与我们展开合作,通过 webpack 这个最好的 npm package,改善你的产品!所以请毫不犹豫地向我们提出你需要解决的问题。

拉取请求(pull requests)

随着 webpack 的发展,webpack 特性(features)和改动(changes)的文档会同步更新。webpack 集成了自动创建 issue 的机制,在过去几年被证明是行之有效的。 当一个特性被合并后,在我们的仓库(repository)中会创建一个文档要求的 issue ,我们希望该 issue 能及时解决。这意味着有特性,改动和重要改动(breaking changes)等着文档提交、检查、发布。尽管如此,如果 Pull Request 作者 30 天还未完成,我们会把这种 Pull Request 标记为失效(stale)。我们会接管未完成的部分,并完成该任务。 如果 Pull Request 作者把 fork 的仓库的写权限授权给 webpack 文档团队,我们将直接提交到你的分支来完成任务。否则,我们不得不自己重头开始,或者委托给有意愿的社区成员。这样你的 Pull Request 就是多余的,可能会被清理程序(cleanup process)关闭。

作者指引

以下部分包含编辑和格式化本网站内容的所有必需知识。请确保在开始编辑或添加之前,你已经进行过一些研究。有时候最困难的地方在于,确定内容应该在哪和确定它是否存在。

步骤

  1. 如果文章包含 issue 链接,先查看 issue。
  2. 点击编辑并阐述结构。
  3. 提交 PR 修改。

YAML 文件顶部信息

每篇文章的顶部都包含一小部分 YAML Frontmatter 格式的内容:

---
title: 我的文章
group: 我的小节
sort: 3
contributors:
  - [github 用户名]
related:
  - title: 相关文章的标题
    url: [相关文章的 url]
---

让我们来逐个分析:

  • title:文章的名称。
  • group:小节的名称。
  • sort:这篇文章在此类(或子类)中的顺序(如果存在同类)。
  • contributors:文章贡献者的 GitHub 用户名列表。
  • related:任何相关阅读或有用示例。

请注意,related 将在页面底部生成 Further Reading 部分,contributors 将在其下生成 Contributors 部分。如果你编辑了一篇文章,并希望获得认可,请不要犹豫,将你的 GitHub 用户名添加到 contributors 列表中。

文章结构

  1. 简介 —— 一两个段落,以便你了解关于什么和为什么的基本想法。
  2. 概述剩余内容 —— 将如何呈现内容。
  3. 主要内容 —— 讲述你承诺要讲的内容。
  4. 结论 —— 讲述你讲了什么并复述要点。

排版

  • Webpack 作为句子的第一个单词时,W 可以大写。参考 source 以了解更多。
  • loader 应当用反引号包裹,并且使用 kebab-casedcss-loaderts-loader,……
  • plugin 应当用反引号包裹,并且使用 camel-cased: BannerPluginNpmInstallWebpackPlugin,……
  • 使用 "webpack 2" 指代特定的 webpack 版本 ("webpack v2")
  • 使用 ES5; ES2015, ES2016, …… 指代 ECMAScript 标准 (ES6, ES7)

译者注:中文文档排版规范不必依照于此。

格式化

代码

语法:```javascript … ```

function foo() {
  return 'bar';
}

foo();

引号

在代码片段和项目文件中使用单引号 (.jsx.scss 等):

- import webpack from "webpack";
+ import webpack from 'webpack';

行内反引号也是一样:

正确

把值设置为 'index.md'……

错误

把值设置为 "index.md"……

列表

  • Boo
  • Foo
  • Zoo

列表应该按字母顺序排序。

表格

参数解释说明输入类型默认值
--debug把 loader 切换为 debug 模式booleanfalse
--devtool为打包的资源定义 source map 类型string-
--progress打印 compilation 的百分比进度booleanfalse

表格也应该按字母顺序排序。

配置属性

配置 属性,应该按字母顺序排序:

  • devServer.compress
  • devServer.hot
  • devServer.static

引用

引用块

语法:>

这是一个引用块。

提示

语法:T>

语法:W>

语法:?>

假设与详略

写文档时,不要做假设:

- 你可能已经知道如何为生产环境优化 bundle ……
+ 我们在 [production guide](/guides/production/) 学习过……

请不要假设事情简单。避免使用词汇“只是”,“简单地”。

- 简单地运行命令……
+ 运行 `command-name` 命令……

配置的默认值和类型

总是为所有文档选项提供类型和默认值,以保持文档的可读性和良好书写。我们在选项后面加上类型和默认值:

configuration.example.option

string = 'none'

这里 = 'none' 表示指定选项的默认值是 'none'

string = 'none': 'none' | 'development' | 'production'

这里 : 'none' | 'development' | 'production' 枚举所有可能的类型值,本例接受 3 个字符串:'none', 'development''production'

在类型之间使用空格,为指定选项列举所有可用的类型:

string = 'none': 'none' | 'development' | 'production' boolean

使用方括号表示数组:

string [string]

如果 数组 允许多种类型,使用英文逗号:

string [string, RegExp, function(arg) => string]

标记为函数时,如果有参数,应同时把参数列出来:

function (compilation, module, path) => boolean

这里 (compilation, module, path) 列举指定函数接受的参数,=> boolean 表示函数返回值的类型必须是 boolean

标记 Plugin 为可用的选项值类型时,使用 Plugin 的 camel case 标题:

TerserPlugin [TerserPlugin]

这表示选项接受一个或多个 TerserPlugin 实例。

使用 number 标记为 number :

number = 15: 5, 15, 30

使用 object 标记为对象:

object = { prop1 string = 'none': 'none' | 'development' | 'production', prop2 boolean = false, prop3 function (module) => string }

当对象的 key 可以有多个类型时,用 | 列来出。如下例子,prop1 可以是字符串或字符串数组:

object = { prop1 string = 'none': 'none' | 'development' | 'production' | [string]}

这样我们可以展示默认值,枚举和其他信息。

如果对象的 key 是动态的,用户自定义的,用 <key> 来描述:

object = { <key> string }

选项 shortlist 及其类型

有时,我们希望描述对象的某些属性,以及列表中的函数。在属性列表直接添加即可:

  • madeUp (boolean = true): 简短描述
  • shortText (string = 'i am text'): 另一个简短描述

EvalSourceMapDevToolPlugin 页面的 选项 部分 有一个例子。

添加链接

请使用相对 URL (如 /concepts/mode/) 来链接到自有内容,不要用绝对 URL (如 https://webpack.js.org/concepts/mode/)。

编写 loader

loader 是导出为一个函数的 node 模块。该函数在 loader 转换资源的时候调用。给定的函数将调用 Loader API,并通过 this 上下文访问。

设置

在深入研究不同 loader 以及他们的用法和例子之前,我们先看三种本地开发测试的方法。

匹配(test)单个 loader,你可以通过在 rule 对象使用 path.resolve 指定一个本地文件:

webpack.config.js

const path = require('path');

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: path.resolve('path/to/loader.js'),
            options: {
              /* ... */
            },
          },
        ],
      },
    ],
  },
};

匹配(test)多个 loaders,你可以使用 resolveLoader.modules 配置,webpack 将会从这些目录中搜索这些 loaders。例如,如果你的项目中有一个 /loaders 本地目录:

webpack.config.js

const path = require('path');

module.exports = {
  //...
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'loaders')],
  },
};

顺便提一下,如果你已经为 loader 创建了独立的库和包,你可以使用 npm link 来将其链接到你要测试的项目。

简单用法

当一个 loader 在资源中使用,这个 loader 只能传入一个参数 - 一个包含资源文件内容的字符串。

同步 loader 可以 return 一个代表已转换模块(transformed module)的单一值。在更复杂的情况下,loader 也可以通过使用 this.callback(err, values...) 函数,返回任意数量的值。错误要么传递给这个 this.callback 函数,要么抛给(thrown in)同步 loader 。

loader 会返回一个或者两个值。第一个值的类型是 JavaScript 代码的字符串或者 buffer。第二个可选值是 SourceMap,它是个 JavaScript 对象。

复杂用法

当链式调用多个 loader 的时候,请记住它们是反方向执行的。取决于数组写法格式,从右向左或者从下向上执行。

  • 最后的 loader 最早调用,将会传入原始资源(raw resource)内容。
  • 第一个 loader 最后调用,期望值是传出 JavaScript 和 source map(可选)。
  • 中间的 loader 执行时,会传入前一个 loader 的结果。

在下例中,foo-loader 被传入原始资源,bar-loader 将接收 foo-loader 的产出,返回最终转化后的模块和一个 source map(可选)

webpack.config.js

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.js/,
        use: ['bar-loader', 'foo-loader'],
      },
    ],
  },
};

用法准则(Guidelines)

编写 loader 时应该遵循以下准则。它们按重要程度排序,有些仅适用于某些场景,请阅读下面详细的章节以获得更多信息。

  • 保持 简单
  • 使用 链式 传递。
  • 模块化 的输出。
  • 确保 无状态
  • 使用 loader utilities
  • 记录 loader 的依赖
  • 解析 模块依赖关系
  • 提取 通用代码
  • 避免 绝对路径
  • 使用 peer dependencies

简单(simple)

loaders 应该只做单一任务。这不仅使每个 loader 易维护,也可以在更多场景链式调用。

链式(chaining)

利用 loader 可以链式调用的优势。写五个简单的 loader 实现五项任务,而不是一个 loader 实现五项任务。功能隔离不仅使 loader 更简单,可能还可以将它们用于你原先没有想到的功能。

以通过 loader 选项或者查询参数得到的数据渲染模板为例。可以把源代码编译为模板,执行并输出包含 HTML 代码的字符串写到一个 loader 中。但是根据用法准则,已经存在这样一个 apply-loader,可以将它和其他开源 loader 串联在一起调用。

  • pug-loader: 导出一个函数,把模板转换为模块。
  • apply-loader: 根据 loader 选项执行函数,返回原生 HTML。
  • html-loader: 接收 HTML,输出一个合法的 JS 模块。

模块化(modular)

保证输出模块化。loader 生成的模块与普通模块遵循相同的设计原则。

无状态(stateless)

确保 loader 在不同模块转换之间不保存状态。每次运行都应该独立于其他编译模块以及相同模块之前的编译结果。

loader 工具库(Loader Utilities)

充分利用 loader-utils 包。它提供了许多有用的工具,但最常用的一种工具是获取传递给 loader 的选项。schema-utils 包配合 loader-utils,用于保证 loader 选项,进行与 JSON Schema 结构一致的校验。这里有一个简单使用两者的例子:

loader.js

import { urlToRequest } from 'loader-utils';
import { validate } from 'schema-utils';

const schema = {
  type: 'object',
  properties: {
    test: {
      type: 'string',
    },
  },
};

export default function (source) {
  const options = this.getOptions();

  validate(schema, options, {
    name: 'Example Loader',
    baseDataPath: 'options',
  });
  
  console.log('The request path', urlToRequest(this.resourcePath));

  // 对资源应用一些转换……

  return `export default ${JSON.stringify(source)}`;
}

loader 依赖(loader dependencies)

如果一个 loader 使用外部资源(例如,从文件系统读取),必须声明它。这些信息用于使缓存 loaders 无效,以及在观察模式(watch mode)下重编译。下面是一个简单示例,说明如何使用 addDependency 方法实现上述声明:

loader.js

import path from 'path';

export default function (source) {
  var callback = this.async();
  var headerPath = path.resolve('header.js');

  this.addDependency(headerPath);

  fs.readFile(headerPath, 'utf-8', function (err, header) {
    if (err) return callback(err);
    callback(null, header + '\n' + source);
  });
}

模块依赖(module dependencies)

根据模块类型,可能会有不同的模式指定依赖关系。例如在 CSS 中,使用 @importurl(...) 语句来声明依赖。这些依赖关系应该由模块系统解析。

可以通过以下两种方式中的一种来实现:

  • 通过把它们转化成 require 语句。
  • 使用 this.resolve 函数解析路径。

css-loader 是第一种方式的一个例子。它将 @import 语句替换为 require 其他样式文件,将 url(...) 替换为 require 引用文件,从而实现将依赖关系转化为 require 声明。

对于 less-loader,无法将每个 @import 转化为 require,因为所有 .less 的文件中的变量和混合跟踪必须一次编译。因此,less-loaderless 编译器进行了扩展,自定义路径解析逻辑。然后,利用第二种方式,通过 webpack 的 this.resolve 解析依赖。

通用代码(common code)

避免在 loader 处理的每个模块中生成通用代码。相反,你应该在 loader 中创建一个运行时文件,并生成 require 语句以引用该共享模块:

src/loader-runtime.js

const { someOtherModule } = require('./some-other-module');

module.exports = function runtime(params) {
  const x = params.y * 2;

  return someOtherModule(params, x);
};

src/loader.js

import runtime from './loader-runtime.js';

export default function loader(source) {
  // 自定义的 loader 逻辑

  return `${runtime({
    source,
    y: Math.random(),
  })}`;
}

绝对路径(absolute paths)

不要在模块代码中插入绝对路径,因为当项目根路径变化时,文件绝对路径也会变化。loader-utils 中的 stringifyRequest 方法,可以将绝对路径转化为相对路径。

同等依赖(peer dependencies)

如果你的 loader 简单包裹另外一个包,你应该把这个包作为一个 peerDependency 引入。这种方式允许应用程序开发者在必要情况下,在 package.json 中指定所需的确定版本。

例如,sass-loader 指定 node-sass 作为同等依赖,引用如下:

{
  "peerDependencies": {
    "node-sass": "^4.0.0"
  }
}

测试

当你遵循上面的用法准则编写了一个 loader,并且可以在本地运行。下一步该做什么呢?让我们用一个简单的单元测试,来保证 loader 能够按照我们预期的方式正确运行。我们将使用 Jest 框架。然后还需要安装 babel-jest 和允许我们使用 import / exportasync / await 的一些预设环境(presets)。让我们开始安装,并且将这些依赖保存为 devDependencies

npm install --save-dev jest babel-jest @babel/core @babel/preset-env

babel.config.js

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          node: 'current',
        },
      },
    ],
  ],
};

我们的 loader 将会处理 .txt 文件,并且将任何实例中的 [name] 直接替换为 loader 选项中设置的 name。然后返回包含默认导出文本的 JavaScript 模块:

src/loader.js

export default function loader(source) {
  const options = this.getOptions();

  source = source.replace(/\[name\]/g, options.name);

  return `export default ${JSON.stringify(source)}`;
}

我们将会使用这个 loader 处理以下文件:

test/example.txt

Hey [name]!

请注意留心接下来的步骤,我们将会使用 Node.js APImemfs 去执行 webpack。这让我们避免向磁盘产生 输出文件,并允许我们访问获取转换模块的统计数据 stats

npm install --save-dev webpack memfs

test/compiler.js

import path from 'path';
import webpack from 'webpack';
import { createFsFromVolume, Volume } from 'memfs';

export default (fixture, options = {}) => {
  const compiler = webpack({
    context: __dirname,
    entry: `./${fixture}`,
    output: {
      path: path.resolve(__dirname),
      filename: 'bundle.js',
    },
    module: {
      rules: [
        {
          test: /\.txt$/,
          use: {
            loader: path.resolve(__dirname, '../src/loader.js'),
            options,
          },
        },
      ],
    },
  });

  compiler.outputFileSystem = createFsFromVolume(new Volume());
  compiler.outputFileSystem.join = path.join.bind(path);

  return new Promise((resolve, reject) => {
    compiler.run((err, stats) => {
      if (err) reject(err);
      if (stats.hasErrors()) reject(stats.toJson().errors);

      resolve(stats);
    });
  });
};

最后,我们来编写测试,并且添加 npm script 运行它:

test/loader.test.js

/**
 * @jest-environment node
 */
import compiler from './compiler.js';

test('Inserts name and outputs JavaScript', async () => {
  const stats = await compiler('example.txt', { name: 'Alice' });
  const output = stats.toJson({ source: true }).modules[0].source;

  expect(output).toBe('export default "Hey Alice!\\n"');
});

package.json

{
  "scripts": {
    "test": "jest"
  },
  "jest": {
    "testEnvironment": "node"
  }
}

准备就绪后,我们可以运行它,然后看新的 loader 是否能通过测试:

 PASS  test/loader.test.js
  ✓ Inserts name and outputs JavaScript (229ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.853s, estimated 2s
Ran all test suites.

一切正常!现在,你应该准备开始开发、测试、部署你的 loaders 了。我们希望你可以在社区分享你的 loader!

自定义插件

插件向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以在 webpack 构建流程中引入自定义的行为。创建插件比创建 loader 更加高级,因为你需要理解 webpack 底层的特性来处理相应的钩子,所以请做好阅读源码的准备!

创建插件

webpack 插件由以下组成:

  • 一个 JavaScript 命名函数或 JavaScript 类。
  • 在插件函数的 prototype 上定义一个 apply 方法。
  • 指定一个绑定到 webpack 自身的事件钩子
  • 处理 webpack 内部实例的特定数据。
  • 功能完成后调用 webpack 提供的回调。
// 一个 JavaScript 类
class MyExampleWebpackPlugin {
  // 在插件函数的 prototype 上定义一个 `apply` 方法,以 compiler 为参数。
  apply(compiler) {
    // 指定一个挂载到 webpack 自身的事件钩子。
    compiler.hooks.emit.tapAsync(
      'MyExampleWebpackPlugin',
      (compilation, callback) => {
        console.log('这是一个示例插件!');
        console.log(
          '这里表示了资源的单次构建的 `compilation` 对象:',
          compilation
        );

        // 用 webpack 提供的插件 API 处理构建过程
        compilation.addModule(/* ... */);

        callback();
      }
    );
  }
}

基本插件架构

插件是由「具有 apply 方法的 prototype 对象」所实例化出来的。这个 apply 方法在安装插件时,会被 webpack compiler 调用一次。apply 方法可以接收一个 webpack compiler 对象的引用,从而可以在回调函数中访问到 compiler 对象。一个插件结构如下:

class HelloWorldPlugin {
  apply(compiler) {
    compiler.hooks.done.tap(
      'Hello World Plugin',
      (
        stats /* 绑定 done 钩子后,stats 会作为参数传入。 */
      ) => {
        console.log('Hello World!');
      }
    );
  }
}

module.exports = HelloWorldPlugin;

然后,要安装这个插件,只需要在你的 webpack 配置的 plugin 数组中添加一个实例:

// webpack.config.js
var HelloWorldPlugin = require('hello-world');

module.exports = {
  // ... 这里是其他配置 ...
  plugins: [new HelloWorldPlugin({ options: true })],
};

使用 schema-utils 来校验传入插件的选项。这里是个例子:

import { validate } from 'schema-utils';

// 选项对象的 schema
const schema = {
  type: 'object',
  properties: {
    test: {
      type: 'string',
    },
  },
};

export default class HelloWorldPlugin {
  constructor(options = {}) {
    validate(schema, options, {
      name: 'Hello World Plugin',
      baseDataPath: 'options',
    });
  }

  apply(compiler) {}
}

Compiler 和 Compilation

在插件开发中最重要的两个资源就是 compilercompilation 对象。理解它们的角色是扩展 webpack 引擎重要的第一步。

class HelloCompilationPlugin {
  apply(compiler) {
    // 指定一个挂载到 compilation 的钩子,回调函数的参数为 compilation 。
    compiler.hooks.compilation.tap('HelloCompilationPlugin', (compilation) => {
      // 现在可以通过 compilation 对象绑定各种钩子
      compilation.hooks.optimize.tap('HelloCompilationPlugin', () => {
        console.log('资源已经优化完毕。');
      });
    });
  }
}

module.exports = HelloCompilationPlugin;

compilercompilation 以及其他重要对象提供的钩子清单,请查看 plugins API 文档。

异步编译插件

有些插件钩子是异步的。我们可以像同步方式一样用 tap 方法来绑定,也可以用 tapAsynctapPromise 这两个异步方法来绑定。

tapAsync

当我们用 tapAsync 方法来绑定插件时,_必须_调用函数的最后一个参数 callback 指定的回调函数。

class HelloAsyncPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync(
      'HelloAsyncPlugin',
      (compilation, callback) => {
        // 执行某些异步操作...
        setTimeout(function () {
          console.log('异步任务完成...');
          callback();
        }, 1000);
      }
    );
  }
}

module.exports = HelloAsyncPlugin;

tapPromise

当我们用 tapPromise 方法来绑定插件时,_必须_返回一个 pormise ,异步任务完成后 resolve 。

class HelloAsyncPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapPromise('HelloAsyncPlugin', (compilation) => {
      // 返回一个 pormise ,异步任务完成后 resolve
      return new Promise((resolve, reject) => {
        setTimeout(function () {
          console.log('异步任务完成...');
          resolve();
        }, 1000);
      });
    });
  }
}

module.exports = HelloAsyncPlugin;

示例

一旦我们可以深入理解 webpack compiler 和每个独立的 compilation,我们依赖 webpack 引擎将有无限多的事可以做。我们可以重新格式化已有的文件,创建衍生的文件,或者制作全新的生成文件。

让我们来写一个简单的示例插件,生成一个叫做 assets.md 的新文件;文件内容是所有构建生成的文件的列表。这个插件大概像下面这样:

class FileListPlugin {
  static defaultOptions = {
    outputFile: 'assets.md',
  };

  // 需要传入自定义插件构造函数的任意选项
  //(这是自定义插件的公开API)
  constructor(options = {}) {
    // 在应用默认选项前,先应用用户指定选项
    // 合并后的选项暴露给插件方法
    // 记得在这里校验所有选项
    this.options = { ...FileListPlugin.defaultOptions, ...options };
  }

  apply(compiler) {
    const pluginName = FileListPlugin.name;

    // webpack 模块实例,可以通过 compiler 对象访问,
    // 这样确保使用的是模块的正确版本
    // (不要直接 require/import webpack)
    const { webpack } = compiler;

    // Compilation 对象提供了对一些有用常量的访问。
    const { Compilation } = webpack;

    // RawSource 是其中一种 “源码”("sources") 类型,
    // 用来在 compilation 中表示资源的源码
    const { RawSource } = webpack.sources;

    // 绑定到 “thisCompilation” 钩子,
    // 以便进一步绑定到 compilation 过程更早期的阶段
    compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
      // 绑定到资源处理流水线(assets processing pipeline)
      compilation.hooks.processAssets.tap(
        {
          name: pluginName,

          // 用某个靠后的资源处理阶段,
          // 确保所有资源已被插件添加到 compilation
          stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
        },
        (assets) => {
          // "assets" 是一个包含 compilation 中所有资源(assets)的对象。
          // 该对象的键是资源的路径,
          // 值是文件的源码

          // 遍历所有资源,
          // 生成 Markdown 文件的内容
          const content =
            '# In this build:\n\n' +
            Object.keys(assets)
              .map((filename) => `- ${filename}`)
              .join('\n');

          // 向 compilation 添加新的资源,
          // 这样 webpack 就会自动生成并输出到 output 目录
          compilation.emitAsset(
            this.options.outputFile,
            new RawSource(content)
          );
        }
      );
    });
  }
}

module.exports = { FileListPlugin };

webpack.config.js

const { FileListPlugin } = require('./file-list-plugin.js');

// 在 webpack 配置中使用自定义的插件:
module.exports = {
  // …

  plugins: [
    // 添加插件,使用默认选项
    new FileListPlugin(),

    // 或者:

    // 使用任意支持的选项
    new FileListPlugin({
      outputFile: 'my-assets.md',
    }),
  ],
};

这样就生成了一个制定名称的 markdown 文件:

# In this build:

- main.css
- main.js
- index.html

插件的不同类型

webpack 插件可以按照它所注册的事件分成不同的类型。每一个事件钩子都预先定义为同步、异步、瀑布或并行钩子,钩子在内部用 call/callAsync 方法调用。支持的钩子清单或可被绑定的钩子清单,通常在 this.hooks 属性指定。

例如:

this.hooks = {
  shouldEmit: new SyncBailHook(['compilation']),
};

表示唯一支持的钩子是 shouldEmit ,这是一个 SyncBailHook 类型的钩子,传入插件的唯一参数是 compilation

支持的各类型钩子:

Synchronous(同步) Hooks

  • SyncHook

    • 通过 new SyncHook([params]) 定义。
    • tap 方法绑定。
    • call(...params) 方法调用。
  • Bail(保险) Hooks

    • 通过 new SyncBailHook([params]) 定义。
    • tap 方法绑定。
    • call(...params) 方法调用。

    Bail类型的钩子,其插件回调函数是串行调用,任意一个插件回调函数返回非 undefined 值,则停止执行插件,该值作为钩子的返回值。optimizeChunksoptimizeChunkModules 等很有用的事件都是 SyncBailHooks 。

  • Waterfall(瀑布) Hooks

    • 通过 new SyncWaterfallHook([params]) 定义。
    • tap 方法绑定。
    • call(...params) 方法调用。

    该类型的插件是一个接一个串行执行,前一个的返回值作为后一个的入参。插件需要考虑执行的顺序,因为后一个插件必须接受前一个插件执行的结果作为入参。第一个插件的参数为 init 。 所以 waterfall 钩子至少要制定一个参数。这种模式用于和 ModuleTemplateChunkTemplate 等 webpack 模板相关的 Tapable 实例。

Asynchronous(异步) Hooks

  • Async Series(异步串联) Hook

    • 通过 new AsyncSeriesHook([params]) 定义。
    • tap/tapAsync/tapPromise 方法绑定。
    • callAsync(...params) 方法调用。

    插件处理函数(handler functions)的参数为所有参数,以及一个签名为 (err?: Error) -> void 的 callback 函数,callback 函数的处理函数按注册顺序执行。callback 在所有处理函数执行完后调用。 这是 emitrun 事件的常见使用模式。

  • Async waterfall(异步瀑布) 插件会用 waterfall 方式异步应用

    • 通过 new AsyncWaterfallHook([params]) 定义。
    • tap/tapAsync/tapPromise 方法绑定。
    • callAsync(...params) 方法调用。

    插件处理函数的参数为当前值,以及一个签名为 (err: Error, nextValue: any) -> void 的 callback 函数。调用时 nextValue 是下一个处理函数的当前值。第一个处理函数的当前是只 init 。当所有 handler 都执行后,callback执行,参数为最后一个值。任一个 handler 传入 err 值,停止调用 handler 且 callback 被调用。 这种模式在 before-resolveafter-resolve 事件中常见。

  • Async Series Bail

    • 通过 new AsyncSeriesBailHook([params]) 定义。
    • tap/tapAsync/tapPromise 方法绑定。
    • callAsync(...params) 方法调用。
  • Async Parallel

    • 通过 new AsyncParallelHook([params]) 定义。
    • tap/tapAsync/tapPromise 方法绑定。
    • callAsync(...params) 方法调用。

默认配置

Webpack 先应用插件的默认配置,再应用自身的默认配置。这样可以让插件拥有自己的默认配置,同时支持实现创建预设配置的插件。

插件模式

在 webpack 构建系统中,能够通过插件进行定制,这赋予了无限的可能性。这使你可以创建自定义资源类型,执行唯一的构建修改,甚至可以使用中间件来增强 webpack 运行时。下面是在编写插件时非常有用一些 webpack 的功能。

检索遍历资源、chunk、模块和依赖

在执行完成编译的封存阶段后,编译的所有结构都可以遍历。

class MyPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
      // 检索每个(构建输出的)chunk:
      compilation.chunks.forEach((chunk) => {
        // 检索 chunk 中(内置输入的)的每个模块:
        chunk.getModules().forEach((module) => {
          // 检索模块中包含的每个源文件路径:
          module.buildInfo &&
            module.buildInfo.fileDependencies &&
            module.buildInfo.fileDependencies.forEach((filepath) => {
              // 我们现在已经对源结构有不少了解……
            });
        });

        // 检索由 chunk 生成的每个资源文件名:
        chunk.files.forEach((filename) => {
          // 获取 chunk 生成的每个文件的资源源代码:
          var source = compilation.assets[filename].source();
        });
      });

      callback();
    });
  }
}
module.exports = MyPlugin;
  • compilation.modules:编译后的(内置输入的)模块数组。每个模块管理控制来源代码库中的原始文件的构建。
  • module.fileDependencies:模块中引入的源文件路径构成的数组。这包括源 JavaScript 文件本身(例如:index.js)以及它所需的所有依赖资源文件(样式表、图像等)。审查依赖,可以用于查看一个模块有哪些从属的源文件。
  • compilation.chunks:编译后的(构建输出的)chunk 集合。每个 chunk 所管理控制的最终渲染资源的组合。
  • chunk.getModules():chunk 中引入的模块构成的数组。通过扩展可以审查每个模块的依赖,来查看哪些原始源文件被注入到 chunk 中。
  • chunk.files:chunk 生成的输出文件名构成的集合。你可以从 compilation.assets 表中访问这些资源来源。

监听观察图的修改

运行 webpack 中间件时,每个编译包括一个 fileDependencies Set(正在观察哪些文件)和一个 fileTimestamps Map,它将被观察的文件路径映射到时间戳。这可以用于检测编译中哪些文件已经修改:

class MyPlugin {
  constructor() {
    this.startTime = Date.now();
    this.prevTimestamps = new Map();
  }
  apply(compiler) {
    compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
      const changedFiles = Array.from(compilation.fileTimestamps.keys()).filter(
        (watchfile) => {
          return (
            (this.prevTimestamps.get(watchfile) || this.startTime) <
            (compilation.fileTimestamps.get(watchfile) || Infinity)
          );
        }
      );

      this.prevTimestamps = compilation.fileTimestamps;
      callback();
    });
  }
}

module.exports = MyPlugin;

还可以将新文件路径添加到观察图中,以在这些文件修改时,接收消息和重新触发编译。只要将有效的文件路径推送到 compilation.fileDependencies Set 中,就可以将其添加到观察图中。

监听 chunk 的修改

类似于观察图,监听 chunk(或者模块,就当前情况而言)的修改也很简单,通过在编译时跟踪它们的哈希来实现。

class MyPlugin {
  constructor() {
    this.chunkVersions = {};
  }
  apply(compiler) {
    compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
      var changedChunks = compilation.chunks.filter((chunk) => {
        var oldVersion = this.chunkVersions[chunk.name];
        this.chunkVersions[chunk.name] = chunk.hash;
        return chunk.hash !== oldVersion;
      });
      callback();
    });
  }
}

module.exports = MyPlugin;

发布流程

部署 webpack 的发布流程实际上非常简单。请阅读以下步骤,以了解清楚这些是如何完成的。

拉取请求(pull request)

当向 main 分支上提交 pull request 时,选择 Create Merge Commit 选项。

发布

npm version patch && git push --follow-tags && npm publish
npm version minor && git push --follow-tags && npm publish
npm version major && git push --follow-tags && npm publish

这样将递增包版本号,提交变更,然后创建一个本地标签(tag),并推送到 GitHub,然后发布到 npm package

之后就可以到 GitHub 的 发布页面 上为新的标签(tag)编写 Changelog。

调试

在为核心仓库贡献代码,编写 loader/plugin,又或是处理复杂的项目时,调试工具将会成为工作流程的重心。无论问题是大型项目的性能下降还是无用追溯,以下工具都可以使这些问题变得不那么痛苦。

  • NodeCLI 提供的 stats 数据
  • node-nightly 和 Node.js 最新版本提供的 Chrome DevTools

Stats

无论你是想手动还是使用工具来筛选 这些数据stats 数据在调试构建的问题时都非常有用。我们不会在此处深入介绍,请参阅 此页面 以了解详细内容。但应该知道可以使用它来查找以下信息:

  • 每个模块的内容。
  • 每个 chunk 中包含的模块。
  • 每个模块编译(compilation)和解析的 stats。
  • 构建错误和警告。
  • 模块之间的关系。
  • 其他更多……

最重要的是,官方分析工具各种其他分析工具 会将这些数据展示为多种形式的可视化图表。

DevTools

虽然在简单场景中,可能 console 语句会表现良好,然而有时还需要更加强大的解决方案。正如大多数前端开发人员已经知道的,将 Chrome DevTools 用在调试 web 应用程序,是一个能够解救我们的实用工具,但它并没有局限于调试 web 应用程序。从 Node v6.3.0+ 开始,开发人员可以使用内置的 --inspect 标记以使用 DevTools 调试 Node.js 应用程序。

这可以帮助轻松创建断点、调试内存使用情况、在控制台中暴露和检查对象等。在这个简短的演示中,我们将利用 node-nightly 包,它提供最新和强大的检测能力。

首先在全局安装:

npm install --global node-nightly

现在,需要运行一次以结束安装:

node-nightly

现在,我们可以直接使用带有 --inspect 标记的 node-nightly,在任何基于 webpack 的项目中开始构建。注意,我们不应该运行 NPM scripts,例如 npm run build,所以需要指定完整的 node_modules 路径:

node-nightly --inspect ./node_modules/webpack/bin/webpack.js

应该输出类似如下内容:

Debugger listening on ws://127.0.0.1:9229/c624201a-250f-416e-a018-300bbec7be2c
For help see https://nodejs.org/en/docs/inspector

现在,在浏览器中访问 chrome://inspect,你会看到在 Remote Target 标题下可以进行审查(inspect)的活动脚本。单击每个脚本下自动连接会话的“inspect”链接,会打开一个专门用于 debugger 或 Open dedicated DevTools for Node 链接。除此之外还可以看到 NiM 扩展程序,这是一个方便的 Chrome 插件。每当通过 --inspect 调试某个脚本时,都会自动打开 DevTools 标签页。

我们推荐使用 --inspect-brk 标记,此标记将在脚本的第一条语句处断开,以便你可以在源代码中设置断点,并根据需要启动/停止构建。此外不要忘记仍然可以向脚本传递参数。例如,如果你有多个配置文件,你可以通过 --config webpack.prod.js 指定想要调试的配置。

1 位贡献者

webpack