可打印

贡献

为 webpack 贡献的人是出于对开源、用户和生态系统的热爱,最重要的是,为了共同推动 Web 的发展。由于我们的 Open Collective 资金和透明度模型,我们能够通过贡献者、依赖项目以及贡献者和核心团队来提供支持和资金。要捐款,请点击下面的按钮...

但投资的回报是什么?

开发者

我们希望提供的最大核心功能是愉快的开发体验。像您这样的开发者可以通过贡献丰富而充满活力的文档、提交拉取请求来帮助我们覆盖小众用例,并帮助维护您所喜爱的 webpack。

我能如何提供帮助?

任何人都可以通过以下任何一种方式提供帮助:

鼓励雇主

您可以要求您的雇主利用 webpack 来改善您的工作流程:它是一个用于字体、图像和图像优化以及 JSON 的一体化工具。向他们解释 webpack 将如何尽力捆绑您的代码和资源以实现最小的文件大小,从而带来更快的网站和应用程序。

您的贡献

为 webpack 贡献并非加入一个排他性俱乐部。作为开发者,您正在为下游项目的整体健康做出贡献。成百上千的项目依赖于 webpack,贡献将使整个生态系统对所有用户都更好。

本网站的其余部分致力于像您这样希望成为我们不断壮大的社区一员的开发者。

高管

CTO、副总裁和所有者也能提供帮助!

Webpack 是一个用于捆绑代码的一体化工具。在社区驱动的插件和加载器的帮助下,它可以处理字体、图像、数据等等。所有资源由一个工具处理会非常有帮助,因为您或您的团队可以花费更少的时间确保一个包含许多运动部件的机器正常工作,而将更多时间用于构建您的产品。

赞助

除了经济援助,公司还可以通过以下方式支持 webpack:

  • 提供未积极从事项目的开发者。
  • 为改进持续集成(CI)和回归测试提供计算能力。

您还可以鼓励您的开发者通过开源 webpack 加载器、插件和其他实用工具来为生态系统做出贡献。而且,如上所述,我们非常感谢您在增加 CI/CD 基础设施方面的任何帮助。

其他人

对于任何其他有兴趣帮助我们完成使命的人——例如风险投资家、政府实体、数字机构等——我们非常乐意与您合作,作为顶级的 npm 包之一,共同改进您的产品!请随时提出问题。

社区

正在寻找帮助,有问题,或者想与其他 webpack 贡献者联系?

加入我们的 Discord 服务器,获取实时支持、讨论和协作。

拉取请求

webpack 功能和变更的文档现在正随着 webpack 的演进而更新。自动化问题创建集成已建立,并在近年证明有效。当一个功能合并后,我们仓库中会创建一个带有文档请求的问题,我们期望及时解决它。这意味着有一些功能、变更和破坏性变更正在等待文档、审查和发布。话虽如此,如果拉取请求的作者在30天以上放弃它,我们可能会将此拉取请求标记为过时。我们可能会接管完成它所需的工作。如果拉取请求作者授予 webpack 文档团队对其 fork 的写入权限,我们将直接提交到您的分支并完成工作。在其他情况下,我们可能需要自行重新开始或委托给愿意的社区成员。这可能会使您的拉取请求变得多余,并可能在清理过程中被关闭。

编写者指南

以下部分包含您编辑和格式化本网站内容所需了解的一切。在开始编辑或添加之前,请务必进行一些研究。有时最困难的部分是找到内容应该放在哪里,并确定它是否已经存在。

流程

  1. 如果文章链接到相关问题,请检查该问题。
  2. 点击 编辑 并扩展结构。
  3. 提交拉取请求。

YAML Frontmatter

每篇文章顶部都包含一小段用 YAML Frontmatter 编写的配置

---
title: My Article
group: My Sub-Section
sort: 3
contributors:
  - [github username]
related:
  - title: Title of Related Article
    url: [url of related article]
---

让我们来分解这些:

  • title: 文章的名称。
  • group: 子部分的名称
  • sort: 文章在其所在部分(或)子部分(如果存在)中的顺序。
  • contributors: 为本文做出贡献的 GitHub 用户名列表。
  • related: 任何相关阅读或有用示例。

请注意,related 将在页面底部生成一个**进一步阅读**部分,而 contributors 将在其下方生成一个**贡献者**部分。如果您编辑了一篇文章并希望获得认可,请不要犹豫,将您的 GitHub 用户名添加到 contributors 列表中。

文章结构

  1. 简要介绍 - 一两段文字,让您对内容和原因有个基本了解。
  2. 内容大纲 – 内容将如何呈现。
  3. 主要内容 - 讲述您承诺讲述的内容。
  4. 结论 - 讲述您所说的内容并总结要点。

排版

  • 在句首时,Webpack 可以大写 W。(来源
  • 加载器用反引号括起来,并使用烤串命名法css-loader, ts-loader, …
  • 插件用反引号括起来,并使用驼峰命名法BannerPlugin, NpmInstallWebpackPlugin, …
  • 使用 "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将加载器切换到调试模式booleanfalse
--devtool为捆绑资源定义源映射类型string-
--progress以百分比显示编译进度booleanfalse

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

配置属性

配置属性也应按字母顺序排列

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

引用

块引用

语法:>

这是一个块引用。

提示

语法:T>

语法:W>

语法:?>

假设与简洁

编写文档时不要做假设。

- You might already know how to optimize bundle for production...
+ As we've learned in [production guide](/guides/production/)...

请不要假设事情很简单。避免使用“仅仅”、“简单地”之类的词语。

- Simply run command...
+ Run the `command-name` command...

配置默认值和类型

始终为所有文档选项提供类型和默认值,以保持文档的可访问性和良好的编写质量。我们在指定文档选项后添加类型和默认值

configuration.example.option

string = 'none'

其中 = 'none' 表示给定选项的默认值为 'none'

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

其中 : 'none' | 'development' | 'production' 列举了可能的类型值,在这种情况下,接受三种字符串:'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 的驼峰命名法标题

TerserPlugin [TerserPlugin]

这意味着该选项期望一个或几个 TerserPlugin 实例。

要标记一个数字,请使用 number

number = 15: 5, 15, 30

要标记一个对象,请使用 object

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

当对象的键可以有多种类型时,使用 | 来列出它们。以下是一个示例,其中 prop1 可以是字符串,也可以是字符串数组

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

这使我们能够显示默认值、枚举和其他信息。

如果对象的键是动态的、用户定义的,请使用 <key> 来描述它

object = { <key> string }

选项摘要及其类型

有时,我们希望在列表中描述对象和函数的某些属性。在适用情况下,直接将类型添加到列出属性的列表中

  • madeUpboolean = true):简短描述
  • shortTextstring = 'i am text'):另一个简短描述

一个示例可以在 EvalSourceMapDevToolPlugin 页面的 options 部分找到。

添加链接

请使用相对 URL(例如 /concepts/mode/)链接我们自己的内容,而不是绝对 URL(例如 https://webpack.js.cn/concepts/mode/)。

编写加载器

加载器是一个导出函数的 Node 模块。当资源应由该加载器转换时,会调用此函数。给定函数将能够使用提供给它的 this 上下文访问 加载器 API

设置

在我们深入了解不同类型的加载器、它们的用法和示例之前,让我们先看看在本地开发和测试加载器的三种方式。

要测试单个加载器,您可以使用 path 在规则对象中 resolve 一个本地文件

webpack.config.js

const path = require('path');

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

要测试多个,您可以利用 resolveLoader.modules 配置来更新 webpack 搜索加载器的位置。例如,如果您的项目中有本地的 /loaders 目录

webpack.config.js

const path = require('path');

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

顺便说一句,如果您已经为您的加载器创建了一个单独的仓库和包,您可以将它 npm link 到您想要测试的项目中。

简单用法

当单个加载器应用于资源时,加载器只带有一个参数被调用——一个包含资源文件内容的字符串。

同步加载器可以 返回 一个表示转换后模块的单一值。在更复杂的情况下,加载器可以使用 this.callback(err, values...) 函数返回任意数量的值。错误要么传递给 this.callback 函数,要么在同步加载器中抛出。

加载器预期返回一个或两个值。第一个值是作为字符串或缓冲区的生成的 JavaScript 代码。第二个可选值是作为 JavaScript 对象的 SourceMap。

复杂用法

当多个加载器串联时,重要的是要记住它们是逆序执行的——根据数组格式,要么从右到左,要么从下到上。

  • 最后调用的加载器(第一个被调用)将接收原始资源的内容。
  • 第一个加载器(最后被调用)预期返回 JavaScript 和一个可选的源映射。
  • 中间的加载器将使用链中前一个加载器的结果执行。

在以下示例中,foo-loader 将接收原始资源,而 bar-loader 将接收 foo-loader 的输出,并在必要时返回最终转换后的模块和源映射。

webpack.config.js

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

指南

编写加载器时应遵循以下指南。它们按重要性排序,有些仅适用于特定场景,请阅读后续详细部分以获取更多信息。

  • 保持它们**简单**。
  • 利用**链式调用**。
  • 发出**模块化**输出。
  • 确保它们是**无状态的**。
  • 使用**加载器实用工具**。
  • 标记**加载器依赖**。
  • 解析**模块依赖**。
  • 提取**通用代码**。
  • 避免**绝对路径**。
  • 使用**对等依赖**。

简单

加载器应该只执行一个任务。这不仅使维护每个加载器的工作更容易,而且还允许它们在更多场景中进行链式调用。

链式调用

利用加载器可以串联在一起的事实。不要编写一个处理五个任务的单个加载器,而是编写五个更简单的加载器来分担这项工作。将它们隔离不仅可以使每个单独的加载器保持简单,而且可能允许它们用于您最初没有想到的用途。

以通过加载器选项或查询参数指定数据来渲染模板文件为例。它可以编写为一个单独的加载器,从源编译模板,执行它并返回一个导出包含 HTML 代码字符串的模块。然而,根据指南,存在一个 apply-loader 可以与其他开源加载器串联使用

  • pug-loader: 将模板转换为导出函数的模块。
  • apply-loader: 使用加载器选项执行函数并返回原始 HTML。
  • html-loader: 接受 HTML 并输出有效的 JavaScript 模块。

模块化

保持输出模块化。加载器生成的模块应遵循与普通模块相同的设计原则。

无状态

确保加载器在模块转换之间不保留状态。每次运行都应始终独立于其他编译模块以及同一模块的先前编译。

加载器实用工具

利用 loader-utils 包,它提供了各种有用的工具。除了 loader-utils,还应该使用 schema-utils 包来进行基于 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));

  // Apply some transformations to the source...

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

数据共享

在 webpack 中,加载器可以链式调用并与链中的后续加载器共享数据。为此,您可以使用原始加载器中的 this.callback 方法随内容(源代码)传递数据。在原始加载器的默认导出函数中,您可以使用 this.callback 的第四个参数传递数据。

export default function (source) {
  const options = getOptions(this);
  // Pass data using the fourth argument of this.callback
  this.callback(null, `export default ${JSON.stringify(source)}`, null, {
    some: data,
  });
}

在上面的示例中,this.callback 第四个参数中的 some 属性用于将数据传递给下一个链式加载器。

加载器依赖

如果加载器使用外部资源(即通过从文件系统读取),它们**必须**指明。此信息用于在观察模式下使可缓存加载器失效并重新编译。这是一个如何使用 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);
  });
}

模块依赖

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

这可以通过两种方式完成:

  • 通过将它们转换为 require 语句。
  • 使用 this.resolve 函数解析路径。

css-loader 是第一种方法的一个很好的例子。它通过将 @import 语句替换为对其他样式表的 require,并将 url(...) 替换为对引用文件的 require,从而将依赖转换为 require

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

通用代码

避免在加载器处理的每个模块中生成通用代码。相反,在加载器中创建一个运行时文件,并生成对该共享模块的 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) {
  // Custom loader logic

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

绝对路径

不要将绝对路径插入到模块代码中,因为当项目根目录移动时,它们会破坏哈希。您可以使用以下代码将绝对路径转换为相对路径。

// `loaderContext` is same as `this` inside loader function
JSON.stringify(
  loaderContext.utils.contextify(
    loaderContext.context || loaderContext.rootContext,
    request
  )
);

对等依赖

如果您正在开发的加载器只是另一个包的简单包装,那么您应该将该包作为 peerDependency 包含进来。这种方法允许应用程序的开发者根据需要指定 package.json 中的确切版本。

例如,sass-loader node-sass 指定为对等依赖,如下所示

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

测试

所以您已经编写了一个加载器,遵循了上面的指南,并将其设置为在本地运行。接下来是什么?我们来看一个单元测试示例,以确保我们的加载器按预期工作。我们将使用 Jest 框架来完成此操作。我们还将安装 babel-jest 和一些预设,这将允许我们使用 import / exportasync / await。让我们首先安装并将它们保存为 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',
        },
      },
    ],
  ],
};

我们的加载器将处理 .txt 文件,并将 [name] 的任何实例替换为提供给加载器的 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)}`;
}

我们将使用此加载器处理以下文件

test/example.txt

Hey [name]!

请密切注意下一步,因为我们将使用 Node.js APImemfs 来执行 webpack。这使我们避免将 output 发送到磁盘,并且将使我们能够访问 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 脚本来运行它

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"
  }
}

一切就绪后,我们可以运行它,看看我们的新加载器是否通过了测试

 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.

它成功了!至此,您应该已经准备好开始开发、测试和部署您自己的加载器。我们希望您能与社区的其他人分享您的创作!

编写插件

插件将 webpack 引擎的全部潜力暴露给第三方开发者。通过使用分阶段构建回调,开发者可以将自己的行为引入 webpack 构建过程。构建插件比构建加载器更高级一些,因为您需要了解一些 webpack 的底层内部机制才能进行钩入。准备好阅读一些源代码吧!

创建插件

一个 webpack 插件由以下部分组成:

  • 一个命名 JavaScript 函数或一个 JavaScript 类。
  • 在其原型中定义 apply 方法。
  • 指定要钩入的事件钩子
  • 操作 webpack 内部实例特定的数据。
  • 功能完成后调用 webpack 提供的回调。
// A JavaScript class.
class MyExampleWebpackPlugin {
  // Define `apply` as its prototype method which is supplied with compiler as its argument
  apply(compiler) {
    // Specify the event hook to attach to
    compiler.hooks.emit.tapAsync(
      'MyExampleWebpackPlugin',
      (compilation, callback) => {
        console.log('This is an example plugin!');
        console.log(
          'Here’s the `compilation` object which represents a single build of assets:',
          compilation
        );

        // Manipulate the build using the plugin API provided by webpack
        compilation.addModule(/* ... */);

        callback();
      }
    );
  }
}

基本插件架构

插件是具有 apply 方法的实例化对象,该方法在其原型上。在安装插件时,webpack 编译器会调用此 apply 方法一次。apply 方法被赋予底层 webpack 编译器的引用,从而可以访问编译器回调。插件的结构如下:

class HelloWorldPlugin {
  apply(compiler) {
    compiler.hooks.done.tap(
      'Hello World Plugin',
      (
        stats /* stats is passed as an argument when done hook is tapped.  */
      ) => {
        console.log('Hello World!');
      }
    );
  }
}

module.exports = HelloWorldPlugin;

然后要使用插件,请在您的 webpack 配置 plugins 数组中包含一个实例

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

module.exports = {
  // ... configuration settings here ...
  plugins: [new HelloWorldPlugin({ options: true })],
};

使用 schema-utils 来验证通过插件选项传递的选项。这是一个例子

import { validate } from 'schema-utils';

// schema for options object
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) {}
}

编译器与编译

开发插件时最重要的两个资源是 compilercompilation 对象。理解它们的作用是扩展 webpack 引擎的重要第一步。

class HelloCompilationPlugin {
  apply(compiler) {
    // Tap into compilation hook which gives compilation as argument to the callback function
    compiler.hooks.compilation.tap('HelloCompilationPlugin', (compilation) => {
      // Now we can tap into various hooks available through compilation
      compilation.hooks.optimize.tap('HelloCompilationPlugin', () => {
        console.log('Assets are being optimized.');
      });
    });
  }
}

module.exports = HelloCompilationPlugin;

有关 compilercompilation 和其他重要对象上可用的钩子列表,请参阅插件 API 文档。

异步事件钩子

一些插件钩子是异步的。要钩入它们,我们可以使用 tap 方法,它将以同步方式运行,或者使用 tapAsync 方法或 tapPromise 方法,它们是异步方法。

tapAsync

当我们使用 tapAsync 方法钩入插件时,我们需要调用作为函数最后一个参数提供的回调函数。

class HelloAsyncPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync(
      'HelloAsyncPlugin',
      (compilation, callback) => {
        // Do something async...
        setTimeout(function () {
          console.log('Done with async work...');
          callback();
        }, 1000);
      }
    );
  }
}

module.exports = HelloAsyncPlugin;

tapPromise

当我们使用 tapPromise 方法钩入插件时,我们需要返回一个 Promise,该 Promise 在异步任务完成后解决。

class HelloAsyncPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapPromise('HelloAsyncPlugin', (compilation) => {
      // return a Promise that resolves when we are done...
      return new Promise((resolve, reject) => {
        setTimeout(function () {
          console.log('Done with async work...');
          resolve();
        }, 1000);
      });
    });
  }
}

module.exports = HelloAsyncPlugin;

示例

一旦我们能够连接到 webpack 编译器和每个单独的编译,我们就可以用引擎本身做无限的事情。我们可以重新格式化现有文件,创建派生文件,或者完全创建新的资源。

让我们编写一个示例插件,它生成一个名为 assets.md 的新构建文件,其内容将列出我们构建中的所有资源文件。这个插件可能看起来像这样

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

  // Any options should be passed in the constructor of your plugin,
  // (this is a public API of your plugin).
  constructor(options = {}) {
    // Applying user-specified options over the default options
    // and making merged options further available to the plugin methods.
    // You should probably validate all the options here as well.
    this.options = { ...FileListPlugin.defaultOptions, ...options };
  }

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

    // webpack module instance can be accessed from the compiler object,
    // this ensures that correct version of the module is used
    // (do not require/import the webpack or any symbols from it directly).
    const { webpack } = compiler;

    // Compilation object gives us reference to some useful constants.
    const { Compilation } = webpack;

    // RawSource is one of the "sources" classes that should be used
    // to represent asset sources in compilation.
    const { RawSource } = webpack.sources;

    // Tapping to the "thisCompilation" hook in order to further tap
    // to the compilation process on an earlier stage.
    compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
      // Tapping to the assets processing pipeline on a specific stage.
      compilation.hooks.processAssets.tap(
        {
          name: pluginName,

          // Using one of the later asset processing stages to ensure
          // that all assets were already added to the compilation by other plugins.
          stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
        },
        (assets) => {
          // "assets" is an object that contains all assets
          // in the compilation, the keys of the object are pathnames of the assets
          // and the values are file sources.

          // Iterating over all the assets and
          // generating content for our Markdown file.
          const content =
            '# In this build:\n\n' +
            Object.keys(assets)
              .map((filename) => `- ${filename}`)
              .join('\n');

          // Adding new asset to the compilation, so it would be automatically
          // generated by the webpack in the output directory.
          compilation.emitAsset(
            this.options.outputFile,
            new RawSource(content)
          );
        }
      );
    });
  }
}

module.exports = { FileListPlugin };

webpack.config.js

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

// Use the plugin in your webpack configuration:
module.exports = {
  // …

  plugins: [
    // Adding the plugin with the default options
    new FileListPlugin(),

    // OR:

    // You can choose to pass any supported options to it:
    new FileListPlugin({
      outputFile: 'my-assets.md',
    }),
  ],
};

这将生成一个选定名称的 markdown 文件,内容如下所示

# In this build:

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

不同插件形态

插件可以根据其钩入的事件钩子进行分类。每个事件钩子都被预定义为同步或异步,或瀑布式或并行钩子,并使用 call/callAsync 方法在内部调用钩子。支持或可以钩入的钩子列表通常在 this.hooks 属性中指定。

例如

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

它表示唯一支持的钩子是 shouldEmit,它是一个 SyncBailHook 类型钩子,并且将传递给钩入 shouldEmit 钩子的任何插件的唯一参数是 compilation

支持的各种钩子类型有:

同步钩子

  • SyncHook

    • 定义为 new SyncHook([params])
    • 使用 tap 方法钩入。
    • 使用 call(...params) 方法调用。
  • Bail Hooks

    • 使用 SyncBailHook[params] 定义
    • 使用 tap 方法钩入。
    • 使用 call(...params) 方法调用。

    在这种类型的钩子中,每个插件回调将按特定 args 依次调用。如果任何插件返回除 undefined 之外的任何值,则该值将由钩子返回,并且不再调用后续插件回调。许多有用的事件,如 optimizeChunksoptimizeChunkModules 都是 SyncBailHooks。

  • Waterfall Hooks

    • 使用 SyncWaterfallHook[params] 定义
    • 使用 tap 方法钩入。
    • 使用 call(...params) 方法调用

    这里每个插件都会依次被调用,并带有前一个插件返回值的参数。插件必须考虑其执行顺序。它必须接受来自前一个执行插件的参数。第一个插件的值是 init。因此,瀑布钩子至少必须提供 1 个参数。此模式用于与 webpack 模板(如 ModuleTemplateChunkTemplate 等)相关的 Tapable 实例。

异步钩子

  • Async Series Hook

    • 使用 AsyncSeriesHook[params] 定义
    • 使用 tap/tapAsync/tapPromise 方法钩入。
    • 使用 callAsync(...params) 方法调用

    插件处理函数被调用,带有所有参数和签名 (err?: Error) -> void 的回调函数。处理函数按注册顺序调用。callback 在所有处理函数都被调用后调用。这也是 emitrun 等事件常用的模式。

  • 异步瀑布 插件将以异步瀑布方式应用。

    • 使用 AsyncWaterfallHook[params] 定义
    • 使用 tap/tapAsync/tapPromise 方法钩入。
    • 使用 callAsync(...params) 方法调用

    插件处理函数被调用,带有当前值和签名 (err: Error, nextValue: any) -> void. 的回调函数。当被调用时,nextValue 是下一个处理函数的当前值。第一个处理函数的当前值是 init。所有处理函数应用后,回调函数将使用最后一个值被调用。如果任何处理函数为 err 传递了值,则回调函数将使用此错误被调用,并且不再调用其他处理函数。此插件模式预期用于 before-resolveafter-resolve 等事件。

  • Async Series Bail

    • 使用 AsyncSeriesBailHook[params] 定义
    • 使用 tap/tapAsync/tapPromise 方法钩入。
    • 使用 callAsync(...params) 方法调用
  • Async Parallel

    • 使用 AsyncParallelHook[params] 定义
    • 使用 tap/tapAsync/tapPromise 方法钩入。
    • 使用 callAsync(...params) 方法调用

配置默认值

webpack 在插件默认值应用后应用配置默认值。这允许插件拥有自己的默认值,并提供了一种创建配置预设插件的方式。

插件模式

插件提供了在 webpack 构建系统中执行自定义的无限机会。这使您可以创建自定义资源类型、执行独特的构建修改,甚至在使用中间件时增强 webpack 运行时。以下是编写插件时变得有用的一些 webpack 功能。

探索资源、代码块、模块和依赖

编译密封后,可以遍历编译中的所有结构。

class MyPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
      // Explore each chunk (build output):
      compilation.chunks.forEach((chunk) => {
        // Explore each module within the chunk (built inputs):
        chunk.getModules().forEach((module) => {
          // Explore each source file path that was included into the module:
          module.buildInfo &&
            module.buildInfo.fileDependencies &&
            module.buildInfo.fileDependencies.forEach((filepath) => {
              // we've learned a lot about the source structure now...
            });
        });

        // Explore each asset filename generated by the chunk:
        chunk.files.forEach((filename) => {
          // Get the asset source for each file generated by the chunk:
          var source = compilation.assets[filename].source();
        });
      });

      callback();
    });
  }
}
module.exports = MyPlugin;
  • compilation.modules: 编译中模块(构建输入)的集合。每个模块管理源库中原始文件的构建。
  • module.fileDependencies: 包含在模块中的源文件路径数组。这包括源 JavaScript 文件本身(例如:index.js),以及它所要求的全部依赖资源文件(样式表、图像等)。查看依赖关系有助于查看哪些源文件属于一个模块。
  • compilation.chunks: 编译中代码块(构建输出)的集合。每个代码块管理最终渲染资源的组成。
  • chunk.getModules(): 包含在代码块中的模块数组。通过扩展,您可以查看每个模块的依赖项,以了解哪些原始源文件流入了代码块。
  • chunk.files: 由代码块生成的文件名集合(Set)。您可以从 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 中,以将它们添加到监听文件。

已更改的代码块

与监听图类似,您可以通过跟踪代码块(或模块)的哈希值来监控编译中已更改的代码块(或模块)。

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 的发布过程实际上相当简单。请通读以下步骤,以便您清楚了解它是如何完成的。

拉取请求

将拉取请求合并到 main 分支时,请选择**创建合并提交**选项。

发布

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

这将增加包版本,提交更改,创建一个**本地标签**,推送到 GitHub 并发布 npm 包。

之后,前往 GitHub 发布页面,为新标签编写更新日志。

调试

在为核心仓库贡献、编写加载器/插件,甚至处理复杂项目时,调试工具可能成为您工作流程的核心。无论问题是大型项目上的性能缓慢还是无用的堆栈跟踪,以下实用工具都可以使解决问题变得不那么痛苦。

统计信息

无论您是想手动筛选这些数据,还是使用工具进行处理,stats 数据在调试构建问题时都非常有用。我们在此不深入探讨,因为有一个专门的页面详细介绍了其内容,但请注意,您可以使用它来查找以下信息

  • 每个模块的内容。
  • 每个代码块中包含的模块。
  • 每个模块的编译和解析统计信息。
  • 构建错误和警告。
  • 模块之间的关系。
  • 以及更多...

最重要的是,官方的 分析工具其他各种工具 将接受这些数据并以各种方式将其可视化。

DevTools

虽然 console 语句在简单场景下可能效果很好,但有时需要更强大的解决方案。正如大多数前端开发者已经知道的那样,Chrome DevTools 在调试 Web 应用程序时是救星,但它们不应该止步于此。从 Node v6.3.0+ 开始,开发者可以使用内置的 --inspect 标志在 DevTools 中调试 Node 程序。

让我们首先使用 node --inspect 调用 webpack。

请注意,我们不能运行 npm scripts,例如 npm run build,所以我们必须指定完整的 node_modules 路径

node --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://node.org.cn/en/docs/inspector

现在跳转到浏览器中的 chrome://inspect,您应该在**远程目标**标题下看到您已检查的任何活动脚本。单击每个脚本下的“检查”链接以打开专用调试器,或单击**为 Node 打开专用 DevTools**链接以连接自动连接的会话。您还可以查看 NiM 扩展,这是一个方便的 Chrome 插件,每次您使用 --inspect 脚本时都会自动打开一个 DevTools 标签页。

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

1 贡献者

webpack