八、创作教程和实时编程

这是本书针对 Webpack 5.0 的最后一章。到目前为止,你可能觉得自己是一个专家,但你精通的证明来自于你拥有代码本身和定制平台的能力,甚至是黑进去的能力。纯粹主义者可能会回避像黑客攻击这样的术语,而倾向于变通方法或补丁,但我们本质上是在谈论同一个主题(除了向外行解释你在直播制作过程中黑客攻击了 Webpack,这肯定会让最狂热的怀疑者对你的编码超能力印象深刻)。

也就是说,这一章汇集了在网络包中最难做的事情和最容易完成的方法。我们将从创作库开始,然后转向定制加载器,这在本指南中已经讨论过了,特别是使用巴别尔的应用编程接口 ( API )。然而,本章将讨论一种不需要 API 的更原生的定制方法。

当然,我们将涵盖本指南中已经详细讨论过的一些主题,例如测试和闪烁,但是本指南将提供一点突出性和与定制的相关性。

从那里,我们将涵盖一些非常有趣的黑客行为,特别是在支持热模块更换 ( HMR )的环境中使用实时发布时。

最后一章涉及的一些主题如下:

  • 创作库
  • 自定义加载程序
  • 实时编码黑客

创作库

本章的这一部分对任何希望简化捆绑策略的人都非常有用。还不太清楚 Webpack 是否可以用于捆绑库和应用。

我们将从一个假设的定制库项目开始,我们称之为numbers-to-text。它的工作原理是将数字从例如1转换为5,再转换为数字的文本表示,例如3three

让我们详细讨论每个任务,并解释代码中发生了什么,用例子帮助我们更清楚地理解到底发生了什么以及代码是如何工作的。我们将按如下方式进行:

  1. 我们将从调整我们的项目结构开始。基本项目结构应该如下所示:
|- webpack.config.js
  |- package.json
  |- /src
  |- index.js
  |- ref.json

请注意,该结构可能与之前的教程不同,即ref.json文件的存在。在我们继续之前,如果您没有这些额外的文件,您将需要创建它们。

  1. 接下来,我们回到命令行界面 ( CLI ),我们首先需要初始化npm,然后确定我们已经安装了 Webpack 和lodash。如果您已经按照前几章中的示例安装了它,那么不要担心——重复的安装尝试只会覆盖最后一次,不会造成任何伤害。运行以下代码:
npm init -y
npm install --save-dev webpack lodash
  1. 完成后,我们将注意力转移到新建的src/ref.json JSON 文件上。这是自定义库的基本数据。它应该类似于以下示例:
[
  {
    "num": 1,
    "word": "One"
  },
  {
    "num": 2,
    "word": "Two"
  },
  {
    "num": 3,
    "word": "Three"
  },
  {
    "num": 4,
    "word": "Four"
  },
  {
    "num": 5,
    "word": "Five"
  },
  {
    "num": 0,
    "word": "Zero"
  }
]

如您所见,这是一个简单的选项列表,代表一个数字,以及该数字的相应书面版本。这将形成我们非常简单的库结构的主干,从原理上证明这个概念。教程完成后,您应该很自然地看到如何使库适应您的需求,不管它有多复杂。

  1. 现在,我们需要制作一个索引文件(如src/index.js)。您应该遵循此块中显示的编码:
import _ from 'lodash';
import numRef from './ref.json';
export function numToWord(num) {
  return _.reduce(numRef, (accum, ref) => {
  return ref.num === num ? ref.word : accum;
 }, '');
}
export function wordToNum(word) {
  return _.reduce(numRef, (accum, ref) => {
  return ref.word === word && word.toLowerCase() ? ref.num : accum;
  }, -1);
}

从前面的代码可以看出,索引文件本质上包含一系列与 JSON 文件内容相关的exportreturn函数。

  1. 现在,我们需要定义使用规范。具体如下:
 import * as numbersToText from 'numbers-to-text';
// ...
 numbersToText.wordToNum('Two'); 
 const numbersToText = require('numbersToText');
// ...
  1. 使用AMD时,在同一个文件中使用以下代码片段来设置功能:
numbersToText.wordToNum('Two');
AMD module requires:
require(['numbersToText'], function (numbersToText) {
 numbersToText.wordToNum('Two');
 });

用户也可以通过script标签加载来使用该库,如下所示:

<!doctype html>
 <html>
   ...
   <script src="https://unpkg.com/webpack-numbers"></script>
   <script>
    // Global variable
    numbersToText.wordToNum('Five')
    // Property in the window object
    window.numbersToText.wordToNum('Five')
   </script>
 </html>

这是加载库的最常见方式,作为一名 JavaScript 开发人员,这应该是第二天性。也就是说,它可以被配置为在 Node.js 的全局对象中公开一个属性,或者在this对象中公开一个属性。

这将我们带到库的基本配置。授权这个库的配置不止一个级别,所以这只是第一步。

基本配置

与任何 Webpack 项目一样,我们需要对其进行配置。在处理自定义库时,这需要一些额外的注意。与典型配置相比,这种配置必须实现几个不同寻常的功能。现在让我们考虑这些目标。应该以实现以下目标的方式捆绑库:

  • 使用外部来避免捆绑lodash所以要求用户加载它
  • 指定库的外部限制
  • 将库公开为名为numbersToText的变量
  • 将库名设置为numbers-to-text
  • 允许独立访问节点库

此外,请注意,用户必须能够以下列方式访问和使用库:

  • 通过从numbers-to-text导入numbersToText作为ECMAScript 2015(ES 2015)模块
  • 通过 CommonJS 模块,如使用require('webpack-numbers')方法
  • 当通过脚本标记等方法包含时,通过全局变量

考虑到所有这些,首先要做的是通过我们用于这个webpack.config.js文件的正常文件来设置 Webpack 配置,如下所示:

const path = require('path');
module.exports = {
  entry: './src/index.js',
  output: {
   path: path.resolve(__dirname, 'dist'),
   filename: 'numbers-to-text.js'
  }
};

这是基本的配置,但是我们现在将进入下一个目标:外部化lodash

使用外部材料,避免捆绑石棉

如果您现在执行构建,您将看到创建了一个相当大的包。查看文件发现lodash被捆绑在旁边。就本教程而言,lodash最好被视为对等依赖。这实质上意味着用户将安装lodash,有效地将这个外部库的控制权交给这个库的用户。

这可以通过外部的配置来完成,如在webpack.config.js、中那样如下:

const path = require('path');
module.exports = {
  entry: './src/index.js',
  output: {
   path: path.resolve(__dirname, 'dist'),
   filename: 'numbers-to-text.js'
   }
  },
   externals: {
     lodash: {
      commonjs: 'lodash',
      commonjs2: 'lodash',
      amd: 'lodash',
      root: '_'
    }
  }
};

前面的代码意味着库期望名为lodash的依赖项在用户的环境中可用。

请注意,如果计划将库用作并行 Webpack 包中的依赖项,则可以将外部对象指定为数组。

指定外部限制也很重要,我们现在将讨论这一点。

指定外部限制

您可以使用使用依赖项中多个文件的库,例如在以下描述性块中:

  • import A from 'library/one';
  • import B from 'library/two';

在这种情况下,不能通过在外部指定库来将它们从包中排除。需要一次排除一个,或者通过使用正则表达式 s 来排除,如下例所示:

module.exports = {
 externals: [
   'library/one',
   'library/two',
   // Everything that starts with "library/"
   /^library\/.+$/
  ]
};

一旦完成,我们需要公开库或者允许它加载到我们的前端。这将在下一小节中介绍。

展示图书馆

公开一个库是本指南中已经讨论过的事情,但是如果你跳到这一章,你可能会感到困惑。我们只是允许我们的应用从外部源加载库,就像任何外部加载到网页中的库一样。库应该兼容不同的环境,如 CommonJSMDNode.js ,以保证库的广泛可用性。为确保这一点,请按照以下步骤操作:

  1. library属性应该添加到webpack.config.js配置文件的输出中,如下所示:
const path = require('path');
module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'numbers-to-text.js'
    filename: 'numbers-to-text.js',
 library: 'numbersToText'
 },
 externals: {
 lodash: {
 commonjs: 'lodash',
 commonjs2: 'lodash',
 amd: 'lodash',
 root: '_'
 }
 }
};

库设置与配置相关。在大多数情况下,指定一个入口点就足够了。多零件库是可能的;但是,通过索引脚本(作为入口点)公开部分导出更简单。

It is unwise to attempt to use an array to a library entry point, as it won't compile very well.

这实现了库捆绑包作为全局变量的公开。

  1. 通过在配置文件中添加libraryTarget属性,并使用webpack.config.js添加不同的库公开选项,可以使您的库与其他环境兼容,如下所示:
const path = require('path');
module.exports = {
  entry: './src/index.js',
  output: {
   path: path.resolve(__dirname, 'dist'),
   filename: 'numbers-to-text.js',
   library: 'numbersToText'
   library: 'numbersToText',
   libraryTarget: 'umd'
 },
 externals: {
   lodash: {
    commonjs: 'lodash',
    commonjs2: 'lodash',
    amd: 'lodash',
    root: '_'
   }
  }
};

有了配置集,我们现在需要公开这个库。请注意,这可以通过以下方式实现:

  • 作为一个变量——作为一个由脚本标签提供的全局变量,例如libraryTarget:'var'
  • 作为对象-可通过this对象获得,如libraryTarget:'this'
  • 窗口-这可通过window对象获得,如libraryTarget:'window'
  • 通用模块定义(UMD)—在 CommonJS 或 AMD require语句后可用,如libraryTarget:'umd'

如果您为libraryTarget函数设置了库,但没有这样做,后者将默认为一个变量,如输出配置中所指定的。

命名库并使用 Node.js

如上所述,我们现在已经进入了创作的最后阶段。在我们进行的过程中,应该按照指南进行输出的优化。当我们这样做的时候,我们将在包输出的路径中添加package.json文件作为包的主字段,如下所示:

{
 ...
 "main": "dist/numbers-to-text.js",
 ...
 }
 Or, to add as standard module as per this guide:
{
 ...
 "module": "src/index.js",
 ...
 }

前面代码块中名为"main"的选项键是指我们从package.json文件中检索到的标准。"module"键指的是允许 JavaScript 环境升级的提议,以便能够在不损害任何向后兼容能力的情况下使用 ES2015 模块。

"module"属性的情况下,这应该指向一个始终使用 ES2015 模块语法的脚本,但没有浏览器或 Node.js 不支持的其他语法。这将使 Webpack 能够解析模块语法,并通过树摇动允许更轻的包,因为用户可能只消费任何给定库的特定部分。

一旦完成,该捆绑包可以作为npm包发布。

您已经学习了如何使用带有相应数字和文本的 JSON 文件来设置和配置您的第一个自定义库,包括在前端公开新库和指定库的范围限制。

在一个相关的领域中,现在让我们来谈谈定制加载器。

自定义加载程序

本指南的前几章已经详细讨论了加载器。然而,我们只提到了它们的定制或创作。这对于至少证明你对 Webpack 的掌握将越来越重要,所以我们现在应该讨论它。

以下教程的结构如下:

  • 设置
  • 简单用法
  • 复杂用法
  • 指导方针

指南部分本身将被细分,但是现在,让我们从设置开始。

设置

开始这一部分的最好方法是看看我们如何在本地开发和测试一个加载器。这是一个很好且令人愉快的开始方式,我们将按如下方式进行:

  1. 测试单个加载程序时,您可以简单地使用路径解析到webpack.config.js中的规则对象内的本地文件,如下所示:
module.exports = {
 //...
 module: {
 rules: [
 {
   test: /\.js$/,
   use: [
 {
    loader: path.resolve('path/to/loader.js'),
    options: {/* ... */}
    }
   ]
  }
 ]
 }
};
  1. 要测试多个加载器,您可以使用resolveLoader.modules配置,网络包将在webpack.config.js中搜索加载器。例如,如果您的项目中有一个本地目录,其中有一个加载程序,那么代码如下所示:
module.exports = {
  resolveLoader: {
    modules: [
     'node_modules',
     path.resolve(__dirname, 'loaders')
   ]
  }
};

这应该是你需要开始的全部。但是,如果您已经为您的加载程序创建了一个单独的存储库,那么您可以使用npm链接到您想要在其中运行测试的项目。

依赖于加载器的使用有不止一种方法,所以——自然地——我们将从简单的使用开始。

简单用法

简单加载器的概念已经被提到过,也就是说,当一个加载器执行一个非常简单和具体的任务时,它会更有用。这将使测试变得更容易,并且,由于有这么多测试,它们可以以更复杂的用法被链接到其他测试,以执行更多种类的任务。

当单个加载程序应用于资源时,只使用一个参数调用加载程序。这是一个包含正在加载的资源内容的字符串。

同步加载器可以返回一个代表转换模块的值。在更复杂的情况下,加载程序可以使用以下函数返回任意数量的值:this.callback(err, values...)

然后,错误要么传递给函数,要么在同步加载器中抛出。

在这种情况下,加载程序应该返回一两个值。第一个值是一些字符串形式的 JavaScript 代码。第二个值是可选的,会产生一个SourceMap和 JavaScript 对象。

装载器在被链接的情况下会变得更加复杂。当讨论自定义加载器的复杂用法时,这将是一个很好的起点,所以我们现在就开始吧。

复杂用法

如前一小节简单用法中所述,复杂用法通常是指一个加载器与另一个或一组加载器以链式模式在上下文中的使用。

当多个加载器被链接时,重要的是要记住它们是以相反的顺序执行的!

这将是从右到左或从下到上,这取决于您使用的数组格式。例如,以下内容将适用:

  • 脚本首先调用的最后一个加载程序将被传递原始资源的内容(加载程序正在运行的数据或脚本)。
  • 第一个加载器被称为 last,预计将返回 JavaScript 和一个可选的源映射。
  • 中间的加载器将与链中前一个加载器的结果一起执行。

因此,在下面的常见示例中,foo-loader将被传递原始资源,bar-loader将接收foo-loader的输出,并返回最终的转换模块和一个源地图(如果需要)。

要以这种方式链接装载机,请从webpack.config.js配置文件开始,如下所示:

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

这就是配置,但是由于链式装载机本质上是复杂的,所以遵循一个标准是很好的。接下来是一套指导方针,将帮助你的项目走到一起,而不会在边缘磨损。

指导方针

编写加载程序时,应遵循以下准则。它们按重要性排序,有些仅适用于特定场景:

  • 简化装载机的用途
  • 利用链接
  • 模块化输出
  • 确保无国籍状态
  • 使用装载实用程序
  • 标记加载程序依赖项
  • 解析模块依赖关系
  • 提取公共代码
  • 不惜一切代价避免绝对路径!
  • 使用对等依赖关系

让我们从简化开始,更详细地讨论每一个问题。

简化装载机的用途

当装载机执行简单明了的任务时,工作效率最高。这可以使维护每个加载器的工作变得更简单,也允许在更复杂的任务中使用链接。这是因为针对不同的任务可能有许多不同的加载器;因此,为了实现通用性,它们经常按顺序使用。

因此,与 Webpack 捆绑包一样,它们应该是模块化的。因此,它们执行的特定任务可以被隔离和细化,这也将允许在与其他加载器的链中使用时更普遍的应用。这就引出了下一个概念:加载器的链接。

利用链接

利用装载机可以链接在一起的事实。不要编写一个单独的加载器来处理许多任务,而是编写多个加载器。隔离它们不仅可以使每个加载器保持简单,还可以使它们用于更多样的用途。

例如,当呈现具有通过加载器选项或查询参数指定的数据的模板文件时,它可以被编写为单个加载器,该加载器从源编译模板,执行它,并返回导出包含 HTML 代码的字符串的模块。但是,在下面的指南中,有一个简单的apply-loader可以与其他开源加载器链接:

  • jade-loader:这将模板转换为导出函数的模块。
  • apply-loader:执行带有加载器选项的函数,返回基本 HTML 代码。
  • e idea HTML-loader:这个接受 HTML 代码,输出一个有效的 JavaScript 模块。
  • 加载器可以被链接的事实也意味着它们不一定需要输出 JavaScript,只要链中的下一个加载器可以处理它的输出。

Webpack 总是模块化的,所以让我们看看在使用链式装载器时的建议。

模块化输出

在最好的情况下,您应该始终保持输出模块化。加载器生成的模块应该遵循与普通模块相同的设计启发。

这可能是显而易见的原因,但是与现有项目的兼容性意味着应该遵循这个标准。如前所述,它对于装载器的链接也越来越重要。加载器通常是按顺序使用的,许多 Webpack 项目需要安装和使用它们。

因此,遵守模块化输出惯例将防止项目变得过于复杂,事实上,可能会导致 Webpack 捆绑的目的发生逆转,即创建一个更小、更简洁和优化的应用。

对于大多数开发人员来说,这种传统的坚持或标准格式是第二天性,但是当使用 Webpack 时,可能会有一些兼容性考虑被您忽略了,因为它们对于捆绑是如此特殊。其中一个考虑因素是加载程序的“状态”。

确保无国籍状态

确保加载器不会在模块转换之间保留状态。每次运行应该总是独立于其他编译模块。

在编译过程中,当您微调您的包时,您可能最终会运行几个构建,并且您不想纠正每个运行,而是将每个构建保持在其原始状态。

如果出现问题,这将使跟踪变得更加容易,因为您总是可以从源文件重新开始,而不必在命令行会话的最开始就开始。

考虑与其他加载程序的兼容性也很重要。由于这是加载器之间的约定,您应该保持相同的约定,除非您的加载器从根本上有必要执行其特定的任务,如果是这样的话,应该向开发人员说明这一点,这样就不会出错。

如果状态的传递对于加载器的功能是必要的,那么有一个方便的解决方案来提供对约定的遵守:加载器实用程序包。

使用装载实用程序

为什么不利用loader-utils套餐呢?它提供了各种有用的工具,但其中最常见的工具之一是检索传递给任何正在使用的加载程序的选项的能力。与loader-utils一起,schema-utils包应该用于一致的基于 JSON 模式的验证。下面的代码块显示了一个使用两个包的例子,使用loader.js:

import { getOptions } from 'loader-utils';
 import validateOptions from 'schema-utils';
const schema = {
  type: 'object',
  properties: {
    test: {
     type: 'string'
    }
  }
 };
export default function(source) {
  const options = getOptions(this);
  validateOptions(schema, options, 'Example Loader');

现在,我们可以对源代码进行一些转换,如下所示:

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

这种转换将使代码变得字符串化——本质上,将内容输出到一行代码中,这种代码很难被人类阅读,但却是计算机的理想选择。如果有人希望手动复制代码,这通常也有助于解决隐私问题。了解了这一点之后,让我们继续讨论加载器依赖性准则。

标记加载程序依赖项

如果加载程序使用外部资源,例如从文件系统读取时,加载程序必须指出这一点。此信息用于使“可缓存”加载程序无效,并在监视模式下重新编译它们。下面是如何使用loader.js内部的addDependency方法实现这一点的一个简单例子:

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);
  });
 }

加载程序和模块化依赖项之间有一些区别。现在我们来讨论后者。

解析模块依赖关系

根据您使用的模块类型,可能有不同的模式用于指定任何依赖关系。例如在层叠样式表 ( CSS 中,使用了@importURL(...)语句。这样做时,这些依赖关系应该由模块系统来解决。

这可以通过以下两种方式之一来实现:

  • 通过将语句转换为require语句
  • 使用this.resolve功能解析路径

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

对于 LESS 加载器,每个@import语句都不能转换成require语句,因为更少的文件必须在一次迭代中编译。因此,LESS 加载器将使用自定义路径解析逻辑来扩展 LESS 编译器。然后它将使用this.resolve方法来解决依赖性。

如果您使用的语言只接受相对的统一资源定位器 ( 网址), ~ 波浪号惯例可用于指定安装模块的参考。一个例子就是url('~some-library/image.png')

提取公共代码

作为最佳实践的一部分,应该避免在加载器进程的每个模块中生成公共代码。更好的方法是在加载器中使用一个运行时文件,并为任何共享模块生成一个require语句进程。这更适合 Webpack 解析代码的方式。

Webpack 的基本目的是编译一个项目,这样代码就不会重复,所以这可能不用说,但是加载器本身应该这样做,而不是让 Webpack 核心处理。否则,应用将不必要地庞大,或者编译时间不必要地长。

如果你有编程插件的经验,你可能会忽略这个非常明显的规则,但是这里值得一提,因为它对 Webpack 的操作和过程非常重要。

避免绝对路径

如前所述,不应该在任何与模块相关的代码中插入绝对路径,因为如果根目录被移动,散列将会中断。此外,请注意在loader-utils加载器中有一个stringifyRequest方法,它可以用于相对路径的绝对路径,以帮助您的过程自动化。

Refer to Chapter 2Working with Modules and Code Splitting, to get a refresher on absolute paths if you think you need it.

和普通代码一样,这是 Webpack 工作方式的基础,如果您在创作过程中没有考虑到这一点,您可能会忽略它,所以它当然值得一提。相对路径才是正道。

关于标准的最后一点涉及到对等依赖。现在让我们看看这些。

使用对等依赖关系

如果开发一个简单包装器的加载器(本质上,代码充当更多操作代码的外壳),操作代码——或包——应该作为peerDependency包含在内。这是因为它将允许您使用package.json文件指定包的确切版本。

在以下示例中,sass-loadernode-sass指定为对等依赖项。看一下代码:

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

这对于兼容性问题来说是非常宝贵的,尤其是在复杂的编程项目中。

单元测试

到目前为止,我们已经编写了一个定制的加载器,遵循了指导方针,甚至让它在本地运行。下一步是测试。下面的例子是一个简单的单元测试过程。它利用babel-jest Jest 框架和一些其他预置来允许使用import/exportasync/await方法:

  1. 我们将从安装这些并将其保存为名为devDependencies的东西开始,如下所示:
npm install --save-dev jest babel-jest babel-preset-env 

之前的命令行条目在开发模式下安装了 Jest 框架和巴别塔Jest

  1. 接下来,我们必须看一下webpack.config.js中使用的关于这个特定单元测试程序的配置,如下所示:
.babelrc
{
 "presets": [[
 "env",
 {
 "targets": {
 "node": "4"
 }
 }
 ]]
 }
  1. 示例中加载器的功能是处理一个文本文件,并用给加载器的选项替换[name]的任何实例。然后,它输出一个有效的 JavaScript 模块,该模块包含作为其默认导出的文本,如下例所示 src/loader.js :
import { getOptions } from 'loader-utils';
export default function loader(source) {
 const options = getOptions(this);
source = source.replace(/\[name\]/g, options.name);
return `export default ${ JSON.stringify(source) }`;
 }
  1. 该加载器将用于处理以下文本文件,称为test/example.txt:
Hi Reader!
  1. 下一步有点复杂。它使用 Node.js API 和memory-fs来执行 Webpack。这将避免内容被输出到本地硬盘驱动器(知道起来非常方便),并使我们能够访问统计数据,这些数据可用于控制我们的转换模块。它从以下命令行开始:
npm install --save-dev webpack memory-fs
  1. 一旦它被安装,我们需要对它的相关编译器脚本做一些工作。使用以下 test/compiler.js 文件:
import path from 'path';
 import webpack from 'webpack';
 import memoryfs from 'memory-fs';
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: {
     name: 'Alice'
    }
   }
 }]
 }
});
compiler.outputFileSystem = new memoryfs();
return new Promise((resolve, reject) => {
  compiler.run((err, stats) => {
 if (err) reject(err);
 if (stats.hasErrors()) reject(new Error(stats.toJson().errors));
resolve(stats);
 });
});
};

在前面的例子中,我们内联了我们的配置,但是作为export函数参数的配置也是可以接受的。这允许使用同一个编译器模块测试多个设置。

  1. 完成这些后,我们现在可以编写测试并添加一个npm脚本来运行它。让我们从将以下代码添加到我们的test/loader.test.js文件开始:
import compiler from './compiler.js';
test('Inserts name and outputs JavaScript', async () => {
 const stats = await compiler('example.txt');
 const output = stats.toJson().modules[0].source;
expect(output).toBe('export default "Hi Reader!\\n"');
 });
 package.json
{
 "scripts": {
  "test": "jest"
 }
 }

如果程序正常工作,前面的代码块显示了加载到示例文本中的测试函数。

现在一切都应该就绪了。

  1. 代码现在可以运行了,我们将通过在命令行中运行一个npm构建并查看命令行窗口来检查新的加载器是否已经通过测试,如下所示:
 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 社区的其他人分享你的创作,并帮助扩展各地开发人员的能力,同时留下你的印记。

在本指南中,我们已经介绍了大量内容,我想您会觉得自己是一名专家,已经构建了定制库和加载器,但是进行实时编码黑客攻击会让您的技能集更加令人印象深刻。了解这些非常有用,尤其是在定制工作中,变通方法和临时方法比屡试不爽的代码更有可能出现,所以让我们来深入了解一下!

实时编码黑客

在这一部分,我们将看到一些非常有趣的东西,这些东西会让专家感觉像超级英雄,如果他或她曾经陷入困境或只是想炫耀的话。我们将讨论与 HMR 配合良好的装载机,如monkey-hot-loaderreact-hot-loader,以及eval__Eval的各种用途。

首先,你应该注意到 HMR 有副作用。它总是在更新时再次评估整个模块。这包括依赖链,它会更新以指向新模块。然而,我们可能只想让原始模块评估新代码,而不是整个模块。谢天谢地,这周围有一个黑客:使用monkey-hot-loader

猴子热装载机

这也意味着如果你的模块有副作用,比如启动一个服务器,那么monkey-hot-loader用起来就不会那么好用了。如果有一个全局状态,情况就不会是这样,但如果编码正确,就真的不应该有全局状态。

此外,请注意,当我们通过取monkey-patch来更改模块,并用更新来修补原始模块时。

在本节中,我们将详细探讨修补顶级函数的工作方式。

monkey-hot-loader是一个 Webpack 加载器,它解析 JavaScript 文件并提取文件中所有顶级函数的名称:

  1. 例如,看看下面的代码,我们将它放在app.js文件中,但是可以放在任何地方,因为代码是全局工作的,并且会影响顶级函数:
function foo() {
  return 5;
}
function bar() {
  return function() {
  // ...
 }
}
module.exports = function() {
 // ...
}

关于前面的例子,monkey-hot-loader的最新版本目前只提取了函数名foobar,因为只有这些函数可以打补丁。一些其他类型的函数可以被修改,但是为了便于解释,现在让我们保持简单。

foobar功能可以通过设置为新功能来打补丁。这是因为它们是顶级函数。更新后,这些函数将在同一范围内创建。然后很容易在那里注入新的代码。

关于导出函数,只有一个真正的问题:使用导出函数的模块仍然会引用旧的变体。要解决这个问题,还需要做很多工作,我们现在就来回顾一下。

这些函数的名称被赋予monkey-hot-loader附加到每个模块的运行时代码。

  1. 当模块最初运行时,它将迭代这些名称,并使每个函数都是可修补的,我们通过用以下代码替换它来实现:
var patched = function() {
 if(patchedBindings[binding]) {
  return patchedBindings[binding].apply(this, arguments);
 }
 else {
  return f.apply(this, arguments);
 }
};
 patched.prototype = f.prototype;

在这里,f变量将引用名称foo如果我们修补它。注意patched的语义应该与f变量相同。对patched变量的任何调用都应该产生与对f的调用相同的结果。

foo的初始语义保持不变的情况下,我们安装了一个“钩子”来检查是否有新版本的函数需要调用。在所有的顶级函数都被这个变体替换之后,我们可以简单地通过加载一个函数到patchedBindings来覆盖它们中的任何一个。甚至导出的函数也会调用新的变体。

目前monkey-hot-loader实现这个顶层功能打补丁作为初始实验。您可以考虑使用backend-with-webpack项目来玩它,看看如何将其与您的应用集成。

根据上下文,修补可能需要采用不同的启发式方法。例如,如果你的前端使用 React ,你的大部分代码将存在于 React 库组件中。react-hot-loader在这方面非常有效。然而,关于你的后端代码,大部分可能被归类为方法,在这种情况下,在原型上修补方法最适合。

反应热修补

react-hot-loader通过将原始模块绑定到任何新代码来工作,无论是函数、类还是方法。它将修补反应组件的所有方法以使用新方法。

这留下的问题是如何修补原始模块。如果你试图接受更新,事情会变得极其复杂。例如,如果您更改了闭包内部的代码,就很难在不丢失现有状态的情况下修补闭包。使用 React 引擎的调试器应用编程接口可能是可以实现的,但是从头到尾进行这种更改可能是困难的:有多困难将在很大程度上取决于您的特定上下文。

请注意,当修补闭包时,只允许非常基本的修补是最好的,因为直观上很容易跟踪一切是如何工作的。

当它是一个复杂的定制项目时,我们经常需要修补代码。Webpack 中非常常见的一个工具是一个名为eval的先天修补工具。我们现在将仔细看看。

evaluate 评价

无论是 React Hot Patching 还是 Monkey Hot Loader,我们都可以使用eval在整个模块范围内安装所有这些补丁版本。之后,我们保存模块的范围。这只会在模块首次运行时发生:

  1. 如果我们需要在这个特定范围内修改代码的能力,我们可以通过创建一个eval代理来维护app.js文件中的任何状态,如下所示:
var moduleEval = function(some code) {
 return eval(some code);
 }

该函数稍后通过 dispose 处理程序传递给该模块的未来变体。

虽然上述所有情况都发生在所述模块的初始运行中,但是通过不同的路径连续更新。在这种情况下,除了迭代每个顶层绑定之外,整个模块都被再次评估,然后调用func.toString()返回函数代码,然后使用moduleEval在原始模块的范围内重新评估代码,以引用原始状态。

  1. 然后,这个eval功能被安装在patchedBindings中,以便在系统以后进行的任何调用中使用,如下所示:
bindings.forEach(function(binding) {
 // Get the updated function instance
 var f = eval(binding);
// We need to rectify the function in the original module so
 // it references any of the original state. Strip the name
 // and simply eval it.
 var funcCode = (
 '(' + f.toString().replace(/^function \w+\(/, 'function (') + ')'
 );
 patchedBindings[binding] = module.hot.data.moduleEval(funcCode);
 });

在理想的情况下,我们可能会更新模块的源代码,并避免运行模块,因为我们无论如何都希望代码是字符串形式的。

碰巧的是,有可能避开整个func.toString()moduleEval过程,干脆不支持任何全局状态,虽然全局状态对于调试操作非常有用。对于简单到 REPL ( 读取-评估-打印循环)的交互尤其如此。然而,类没有这个问题,因为它们的所有状态都是实例的一部分,这就是为什么react-hot-loader没有这个黑客也能正常工作。

对于外行人来说,REPL 也被称为交互式顶层或语言外壳,它接受单个用户的输入并对其进行评估。

Be aware that in Webpack 5, there are currently known problems with eval() that relate to optimization.innerGraph when in production mode.

_eval有个黑客可用,知道了很有用。让我们现在进入那个部分。

__Eval 黑客

最后一次黑客攻击的时间到了,这可能是整本书最大的诡计!_eval()函数将字符串计算为函数的表达式。这可以与热加载结合使用,以允许在整个项目中立即评估您的代码。这就是_Eval黑客的本质。现在让我们再深入探讨一下。

如果我们想要一个在模块内部评估代码的 REPL,并且能够打开模块来选择在哪个上下文中进行评估,那么我们不能用这个基础设施来做到这一点,但是我们可以通过app.js文件中的以下示例来实现这一点:

function __eval() {
 var user = getLastUser();
 console.log(findAllDataOn(user));
 }

一旦这样做了,并且您定义了一个名为__eval的函数,monkey-hot-loader将在每次模块更新时执行它。这对于即时反馈非常有用。使用这种方法,您可以调用一些 API 并记录结果,然后动态处理这些 API,直到看到您想要的结果。这样,你所要做的就是做一些编码修改,保存文件,然后立即看到更新的输出。

此外,您可以使用代码表单__eval作为全局使用的脚本,并允许典型的 HMR 系统在每次更新模块时运行该模块。也就是说,任何有副作用的模块都需要专业代码。此外,您可以跨评估构建一个状态来玩或调试。

与老的 Lisp 做事风格(即选择代码并按下 Ctrl + E 运行)不同,这种技术是按模块完成的,您可以选择运行代码的上下文。

一个考虑是不能引入新的变量,例如改变变量的值,在strict模式下使用_eval函数。这也适用于__eval,所以在你开始之前值得记住。

摘要

本章已经带您了解了 Webpack 可能提供的一些更高级的功能,例如库创作和实时编码黑客,使用热加载的_Eval技术来实现项目中的即时反馈。这包括如何定制加载器,甚至补丁顶层功能的详细解释和示例。

您现在应该对手动捆绑和实时编码有了足够深的理解,可以与任何专家相媲美。为什么不通过参加本章末尾的测验向自己展示这种诀窍呢?如果你曾经在求职面试甚至对大客户的陈述中被安排在现场,能够快速表达你的专业知识,这对你会有好处。

这本书作为一个整体已经给出了关于如何胜任使用网络包和将应用开发提升到全新水平的广泛而全面的细节。随着您的网络包捆绑的发展,这一章将变得越来越重要,您可以确信这一章将成为未来许多项目的书签。

一旦你尝试了练习测试题,你可能想翻到书的开头,在每一章测试自己。您将在本指南后面的单独章节中找到评估答案。成功完成会让你的专业知识毋庸置疑,所以试一试吧。

问题

  1. Webpack 可以用来捆绑库和应用吗?
  2. 创作库时,如何将外部库排除在捆绑包之外?
  3. Webpack 提供了四种公开自定义库的方法。它们是什么?
  4. 为什么在构建自定义模块时不应该使用绝对路径?
  5. __eval的功能前缀如何帮助即时反馈?
  6. 为什么开发人员必须指示加载程序读取外部资源,如文件系统?
  7. 加载程序被链接时是如何执行的?**