六、生产、集成和联合模块

在本章中,我们将介绍生产、集成和联邦模块。我们将概述正确的部署过程、快捷方式和替代方案。尽管本章中的一些内容将讨论已经更详细讨论过的主题,但最好再复习一遍,这样你就能更清楚地了解到目前为止所学的内容。

到目前为止,我们已经讨论并实现了开发构建,并提到了投入生产,但是适当的出版物级生产的过程有点不同,涉及交叉检查和遵循最佳实践。

本书的这一部分将探讨我们可以用来部署带有各种网络实用程序的 Webpack 5 的各种选项。这将为您提供此类 web 实用程序的概述,并解释哪些更适合特定的情况和平台,包括使用 Babel 进行部署。

所有这些主题都与我们关于生产捆绑的开头部分相关,该部分还涵盖了出于生产和发布目的的部署主题。

在本章中,我们将涵盖以下主题:

  • 生产设置
  • 填隙
  • 渐进式网络应用
  • 整合任务执行者
  • 开源代码库
  • 提取样板文件
  • 模块联盟

生产设置

现在我们已经了解了基础知识,我们可以继续学习如何实际部署生产捆绑包。开发模式和生产模式下的项目建设目标差异很大。在生产模式下,目标转移到使用轻量级源映射和优化资产来缩小构建,以缩短加载时间。在开发模式中,强大的源映射是至关重要的,拥有一个带有实时重装或热模块替换的 localhost 服务器也是如此。由于它们的目的不同,通常建议为每种模式编写单独的 Webpack 配置。

尽管模式之间存在差异,但应保持通用配置。要合并这些配置,可以使用名为webpack-merge的实用程序。这种常见的配置过程意味着代码不需要在每种模式下重复。让我们开始吧:

  1. 首先打开命令行实用程序,安装webpack-merge,并将其保存在开发模式下,如下所示:
npm install --save-dev *webpack-merge*
  1. 现在,我们来检查一下项目目录。其内容应类似于以下内容:
 webpack5-demo
  |- package.json
  |- webpack.config.js
  |- webpack.common.js
  |- webpack.dev.js
  |- webpack.prod.js
  |- /dist
  |- /src
    |- index.js
    |- math.js
  |- /node_modules

请注意,前面的输出显示在这个特定示例中存在额外的文件。例如,我们在这里包括webpack.common.js文件。

  1. 让我们仔细看看webpack.common.js文件:
  const path = require('path');
  const { CleanWebpackPlugin } = require('clean-webpack-plugin');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: {
      app: './src/index.js'
    },
    plugins: [
      // new CleanWebpackPlugin(['dist/*']) for < v2 versions of 
      // CleanWebpackPlugin
      new CleanWebpackPlugin(),
      new HtmlWebpackPlugin({
        title: 'Production'
      })
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };

webpack.common.js文件处理普通请求。它做的事情类似于 ECMAScript,但格式不同。让我们确保在 ECMA 环境中工作的webpack.config.js文件与 CommonJS 配置文件执行相同的操作。注意入口点和捆绑包名称,以及 title 选项。后一个选项是模式的对应物,因此您必须确保项目中两个文件之间是对等的。

  1. 在这里,我们查看webpack.dev.js文件内部:
  const merge = require('webpack-merge');
  const common = require('./webpack.common.js');

  module.exports = merge(common, {
    mode: 'development',
    devtool: 'inline-source-map',
    devServer: {
      contentBase: './dist'
    }
  });

如您所见,前面的代码提供了如何在开发模式中使用webpack.common.js的说明。这只是最终产品的交叉检查,以确保您的工作内容格式正确,并且在编译过程中会出现错误。

  1. 如果您在生产模式下工作,将调用以下文件webpack.prod.js:
  const merge = require('webpack-merge');
  const common = require('./webpack.common.js');

  module.exports = merge(common, {
    mode: 'production',
  });

使用webpack.common.js,设置输入和输出配置,包括两种环境模式所需的任何插件。使用webpack.dev.js时,模式应设置为开发模式。另外,在该环境中添加推荐的开发工具,以及简单的devServer配置。在webpack.prod.js中,模式当然设置为生产,加载TerserPlugin

请注意,merge()可以用于特定于环境的配置中,以便您可以轻松地在开发和生产模式中包含通用配置。还值得注意的是,使用**webpack-merge**工具时,有多种高级功能可用。

  1. 让我们在 webpack.common.js 里面做那些开发配置:
  { 
   "name": "development", 
   "version": "1.0.0", 
   "description": "", 
   "main": "src/index.js",
   "scripts": { 
   "start": "webpack-dev-server --open --config webpack.dev.js", 
   "build": "webpack --config webpack.prod.js" 
  }, 
    "keywords": [], 
    "author": "", 
    "license": "ISC", 
    "devDependencies": { 
      "clean-webpack-plugin": "^0.1.17", 
      "css-loader": "^0.28.4", 
      "csv-loader": "^2.1.1", 
      "express": "^4.15.3",
      "file-loader": "^0.11.2", 
      "html-webpack-plugin": "^2.29.0", 
      "style-loader": "^0.18.2", 
      "webpack": "^5.0.0",
      "webpack-dev-middleware": "^1.12.0",
      "webpack-dev-server": "^2.9.1", 
      "webpack-merge": "^4.1.0", 
      "xml-loader": "^1.2.1"
   } 
 }

前面的例子只是显示了 CommonJS 的完整配置。请注意插件依赖项及其版本的列表,它们是通过devDependancies选项加载的。

  1. 现在,运行这些脚本,看看输出如何变化。
  2. 以下代码显示了如何继续添加到生产配置中:
 document.body.appendChild(component());

请注意,在生产模式下,Webpack 5 默认情况下会缩小项目代码。

TerserPlugin是开始缩小的好地方,应该作为默认选项。然而,有几个选择,如BabelMinifyPluginclosureWebpackPlugin

当尝试一个不同的缩小插件时,确保选择也将删除任何死代码,类似于我们在本书前面描述的树摇动。与摇树有关的东西正在闪烁,我们接下来将讨论它。

填隙

闪烁,或者更具体地说,shim-loaders。现在是详细探讨这个概念的好时机,因为在你继续前进之前,你需要对它有一个坚定的把握。

Webpack 使用的编译器可以理解用ECMAScript2015CommonJSAMD 编写的模块。需要注意的是,一些第三方库可能需要全局依赖,例如在使用 jQuery 时。在这种情况下,这些库可能需要导出全局变量。这个模块几乎被打破的特性就是摆振的作用所在。

匀场可以让我们将一种语言规范转换成另一种语言规范。在 Webpack 中,这通常是通过特定于您的环境的专用加载器来完成的。

Webpack 背后的主要概念是模块化开发的使用——独立的模块被安全地包含,不依赖于隐藏的或全局的依赖关系——因此重要的是要注意这种依赖关系的使用应该是稀疏的。

当您希望多填充浏览器以便支持多个用户时,填充会很有用。在这种情况下,polyfill 只需要在需要的地方打补丁并按需加载。

匀场与修补有关,但往往发生在浏览器中,这使得它与渐进式 web 应用高度相关。

在下一节中,我们将更详细地了解渐进式网络应用——它们是 Webpack 5 的关键。

渐进式网络应用

有时称为 PWAs,渐进式网络应用在线提供原生应用体验。它们有许多促成因素,其中最值得注意的是应用在离线和在线时运行的能力。为此,需要使用服务人员。

允许您的 web 应用脱机工作意味着您可以提供推送通知等功能。这些丰富的体验也将通过服务人员等设备提供给基于 web 的应用。这个脚本将在浏览器的后台工作,不管用户是否在正确的页面上,并且允许这些相同的通知甚至后台同步。

与桌面应用类似,pwa 提供网络覆盖,但快速可靠。它们也可以像移动应用一样令人着迷,并可以提供同样的沉浸式体验。这展示了应用质量的新水平。它们还可以在跨平台兼容性方面发挥作用。

响应式设计是网络在这个方向上的第一次推动,但推动互联网更加普及的努力让我们走向了 PWA。为了利用应用的潜力,您应该使用渐进的方法。有关更多信息,请参见谷歌关于该主题的资源:https://developers.google.com/web/progressive-web-apps/

使用 Webpack 时,需要注册服务人员,以便您可以开始将桌面功能集成到基于网络的 PWA 中。pwa 不是为用户本地安装而设计的;它们通过网络浏览器进行本地工作。

可以通过在代码中添加以下内容来注册服务人员——在本例中,这是一个index.js文件:

  if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-
      worker.js').then(registration => {
       console.log('SW registered: ', registration);
    }).catch(registrationError => {
     console.log('SW registration failed: ', registrationError);
    });
  });
 }

完成后,运行npm build–您应该会在命令行窗口中看到以下输出:

SW registered

现在,应用可以在命令行界面中使用npm start服务。

PWA 应该有一个清单,一个服务人员,可能还有一个工作箱来包装和保护服务人员。有关清单的更多信息,请参见第 3 章使用配置和选项。Workbox 是一个插件,可以使用以下命令在命令行中安装:

npm install workbox-webpack-plugin --save-dev

在一个假设的webpack.config.js文件中可以看到工作盒插件的配置示例:

const WorkboxPlugin = require('workbox-webpack-plugin');
new WorkboxPlugin.GenerateSW({
 clientsClaim: true,
 skipWaiting: true,
 }),

{支架内的选项将鼓励服务人员快速到达那个点,并且不允许他们掐死以前的服务人员。

随着您的项目变得更加复杂,您可以考虑使用相关的任务管理器。让我们更详细地看看这个。

整合任务执行者

任务管理器处理自动化任务,如代码林挺。Webpack 不是为此目的而设计的,bundler 也不是。然而,使用 Webpack,我们可以受益于任务运行者提供的高度专注,同时仍然具有高性能捆绑。

虽然任务跑者和捆绑者之间有一些重叠,但如果方法正确,他们可以很好地整合。在这一节中,我们将探索我们可以用于一些最受欢迎的任务执行者的集成技术。

Bundlers 的工作方式是为部署准备 JavaScript 和其他前端代码,转换它们的格式,使它们适合浏览器交付。例如,它允许我们将 JavaScript 分成块进行延迟加载,这提高了性能。

捆绑销售可能具有挑战性,但其结果将消除该过程中的大量艰苦工作。

在本节中,我们将向您展示如何集成以下内容:

  • 吞咽
  • 摩卡
  • 因果报应

大口可能是最著名的任务跑者,所以让我们先从它开始。它是通过使用一个专用文件来使用的,很像其他两个任务运行器。在这里,gulpfile.js文件将处理 Webpack 如何与 grave 相互作用。让我们来看看如何整合所有这些任务执行者:

  1. 首先,我们来看看gulpfile.js文件:
const gulp = require('gulp'); 
const webpack = require('webpack-stream'); 
gulp.task('default', function() { 
   return gulp.src('src/entry.js') 
    .pipe(webpack({ 
 // Any configuration options... 
    })) 
 .pipe(gulp.dest('dist/')); 
});

这就是我们使用大口所要做的。注意使用gulp.task功能、return入口点和.pipe(**gulp**.dest('dist/'));输出位置。

  1. 以下是可以用来安装摩卡的命令行代码:
npm install --save-dev *webpack mocha mocha-webpack*
*mocha-webpack* 'test/**/*.js'

有关更多信息,请访问 Webpack 社区存储库。

  1. 以下是您需要对karma.config.js文件进行的配置文件修改,以便在网络包中使用因果报应:
module.exports = function(config) {
  config.set({
    files: [
      { pattern: 'test/*_test.js', watched: false },
      { pattern: 'test/**/*_test.js', watched: false }
    ],
    preprocessors: {
      'test/*_test.js': [ 'webpack' ],
      'test/**/*_test.js': [ 'webpack' ]
    },
    webpack: {
      // Any custom webpack configuration...
    },
    webpackMiddleware: {
      // Any custom webpack-dev-middleware configuration...
    }
  });
};

webpackwebpackMiddleware选项留空,以便您可以用项目的特定配置填充此内容。如果您不使用这些选项,可以将其完全删除。为了这个例子,我们不会。

如果您希望在您的开发环境中使用这些安装过程,它们对您来说会有一些用处,但是 GitHub 是一个越来越重要的工具。在下一节中,我们将了解它如何在开发方面发挥关键作用。

开源代码库

您可能已经知道,GitHub 是一个命令行代码托管平台,可以很好地与 Webpack 配合使用。使用网络包时,您将使用的许多代码和项目将通过 GitHub 托管。

GitHub 基于 Git 版本控制系统。将 GitHub 与 Webpack 5 结合使用,可以在线使用一些命令行操作。

Git 命令行指令通常在每个新条目之前和每个命令之前使用git命令。Webpack 的大部分内容文件都可以通过 GitHub 获得,GitHub Webpack 页面可以在这里找到:https://github.com/webpack/webpack。Webpack 5 的开发可以从进展阶段的角度来看,如果您需要升级项目,这可能会很有趣,并允许您更好地预测它的到来。这个网址是https://github.com/webpack/webpack/projects/5.

作为一名开发人员,您可能经常使用 GitHub,但是如果您是一名专门的 JavaScript 开发人员,您的经验可能有限。当从事网络包项目时,GitHub 平台提供了大量的实时和协作机会。由于提供了版本控制和命令行功能,因此不太需要在本地执行软件开发。这就是为什么 GitHub 在开发人员社区中如此受欢迎的主要原因,也是为什么它作为开发人员工作的证明变得如此重要的主要原因。

GitHub 允许开发人员在项目上合作。当使用捆绑项目时,这更加有用,因为一些命令行操作可以在线运行。GitHub 还允许敏捷工作流或项目管理界面。敏捷方法允许团队协作,而个人通过专用的数字平台进行自我组织。

使用 GitHub 时,您可能正在使用其他人的代码。这可能包括团队开发的代码框架。即使对于经验最丰富的开发人员来说,如果他们不熟悉所使用的逻辑,这也会变得非常困难。这就把我们带到了样板文件的主题上,样板文件通常是标准的或有良好文档记录的代码,但是尽管如此,您可能希望从您希望利用的项目部分中提取这一点。这就是这个提取过程开始变得非常有用的地方。

提取样板文件

样板代码是需要包含在不同地方的代码段,但是对它们很少或没有进行修改。当使用被认为冗长的语言时,经常需要编写大量的代码。这一大段代码被称为样板。它与框架或库有着本质上相同的目的,术语经常被合并或相互接受。

Webpack 社区提供样板安装,例如多个常见依赖项(如先决条件和加载程序)的组合安装。这些样板文件有多个版本,使用它们可以加速构建。请搜索网络包社区页面(https://webpack.js.org/contribute/)或网络包的 GitHub 页面(https://github.com/webpack-contrib)获取示例。

也就是说,有时只需要部分样板文件。为此,可能需要提取 Webpack 的样板功能。

Webpack 在使用其缩小方法时,允许提取样板文件;也就是说,包中只包含您需要的样板文件的元素。这是一个在编译过程中完成的自动化过程。

缩小是 Webpack 5 提供这种提取过程的关键方式,也是可以使用这种类型的 bundler 的更显著的方式之一。还有一个非常有用的关键过程,它是 Webpack 5 的原生过程。它把我们带到了一个关于如何构建包的不同方向,但是毫无疑问,它是从一个复杂的或者定制的构建开始的,比如一个从提取样板文件开始的项目。这个过程被称为模块联合。

模块联盟

模块联盟被描述为 JavaScript 架构中的游戏规则改变者。它本质上允许应用从服务器之间远程存储的模块运行代码,同时也是捆绑应用的一部分。

一些开发人员可能知道一种叫做 GraphQL 的技术。它本质上是一个在应用之间共享代码的解决方案,由一家名为 Apollo 的公司开发。联合模块是网络包 5 的一个特性,它允许这种情况在捆绑的应用之间发生。

长期以来,最好的折衷是使用DllPlugin的外部文件,它依赖于一个集中的外部依赖文件,然而,这对有机开发、便利性或大规模项目来说并不太好。

通过模块联合,JavaScript 应用可以在应用之间动态加载代码并共享依赖关系。如果应用使用联邦模块作为其构建的一部分,但需要依赖项来服务于联邦代码,则 Webpack 也可以从联邦构建的起点下载该依赖项。因此,联邦将有效地提供一个地图,说明 Webpack 5 可以在哪里找到所需的依赖代码。

说到联合,有一些术语需要考虑:远程和主机。术语“远程”指的是加载到用户应用中的应用或模块,而主机指的是用户在运行时通过浏览器访问的应用。

联合方法是为独立构建设计的,可以独立部署,也可以部署在您自己的存储库中。从这个意义上说,它们可以双向托管,有效地充当远程内容的宿主。这意味着单个项目可能会在用户的整个旅程中在托管方向之间切换。

构建我们的第一个联合应用

让我们从三个独立的应用开始,分别被标识为第一、第二和第三个应用。

我们系统中的第一个应用

让我们从配置第一个应用开始:

  1. 我们将在 HTML 中使用<app>容器。第一个应用是联合系统中的远程应用,因此将由其他应用使用。

  2. 要公开应用,我们将使用AppContainer方法:

const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin =
   require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  plugins: [
   new ModuleFederationPlugin({
    name: "app_one_remote",
    remotes: {
      app_two: "app_two_remote",
      app_three: "app_three_remote"
 },
 exposes: {
   'AppContainer':'./src/App'
 },
 shared: ["react", "react-dom","react-router-dom"]
 }),
 new HtmlWebpackPlugin({
   template: "./public/index.html",
   chunks: ["main"]
  })
 ]
} 

第一个应用还将使用系统中另外两个联邦应用的组件。

  1. 为了允许这个应用使用组件,需要指定远程范围。
  2. 所有这些步骤都应该按照前面代码块中的说明执行。
  3. 现在,让我们看看构建的 HTML 部分:
<head>
  <script src="http://localhost:3002/app_one_remote.js"></script>
  <script src="http://localhost:3003/app_two_remote.js"></script>
</head>
<body>
  <div id="root"></div>
</body>

前面的代码显示了 HTML 的<head>元素。app_one_remote.js在运行时连接运行时和临时编排层。它是专门为入口点设计的。这些是示例网址,你可以使用自己的位置。需要注意的是,这个例子是一个内存非常低的例子,您的构建可能要大得多,但是理解这个原则已经足够了。

  1. 要使用远程应用的代码,第一个应用有一个网页,该网页使用第二个应用的对话框组件,如下所示:
const Dialog = React.lazy(() => import("app_two_remote/Dialogue")); 
const Page1 = () => { 
  return ( 
    <div> 
      <h1>Page 1</h1> 
        <React.Suspense fallback="Loading User Dialogue..."> 
          <Dialog /> 
        </React.Suspense> 
    </div> 
  ); 
}
  1. 让我们从导出我们正在使用的默认 HTML 页面开始,并设置路由器,如下所示:
import { Route, Switch } from "react-router-dom";
import Page1 from "./pages/page1";
import Page2 from "./pages/page2";
import React from "react";
   const Routes = () => (
     <Switch>
       <Route path="/page1">
        <Page1 />
       </Route>
       <Route path="/page2">
        <Page2 />
       </Route>
     </Switch>
  );

前面的代码块显示了代码是如何工作的;它将从我们正在开发的系统中的每个页面导出默认路线。

这个系统是三个应用的一部分,我们现在来看第二个。

第二种应用

我们正在构建的系统由三个应用组成。这个应用将公开对话,使这个序列中的第一个应用能够使用它。然而,第二个应用将使用第一个应用的<app>元素标识符。让我们来看看:

  1. 我们将从配置第二个应用开始。这意味着我们需要将app-one指定为远程应用,同时演示双向托管:
 const HtmlWebpackPlugin = require("html-webpack-plugin");
 const ModuleFederationPlugin =
   require("webpack/lib/container/ModuleFederationPlugin");
 module.exports = {
   plugins: [
     new ModuleFederationPlugin({
     name: "app_two_remote",
     library: { type: "var", name: "app_two_remote" },
     filename: "remoteEntry.js",
     exposes: {
       Dialog: "./src/Dialogue"
 },
   remotes: {
     app_one: "app_one_remote",
 },
   shared: ["react", "react-dom","react-router-dom"]
 }),
 new HtmlWebpackPlugin({
   template: "./public/index.html",
   chunks: ["main"]
  })
 ]
};
  1. 为了便于使用,下面是根应用的样子:
import React from "react";
import Routes from './Routes'
const AppContainer = React.lazy(() =>
  import("app_one_remote/AppContainer"));

const App = () => {
  return (
   <div>
     <React.Suspense fallback="Loading App Container from Host">
       <AppContainer routes={Routes}/>
     </React.Suspense>
   </div>
  );
}
  1. 接下来,我们需要设置代码,以便导出默认应用。以下是使用对话框时默认页面的外观示例:
import React from 'react'
import {ThemeProvider} from "@material-ui/core";
import {theme} from "./theme";
import Dialog from "./Dialogue";

function MainPage() {
   return (
     <ThemeProvider theme={theme}>
       <div>
         <h1>Material User Iinterface App</h1>
         <Dialog />
      </div>
     </ThemeProvider>
  );
}
  1. 现在,我们需要导出默认MainPage。这是使用我们系统中的第三个应用完成的。

第三种应用

让我们来看看我们的第三个也是最后一个应用:

  1. 我们系统中的第三个应用将导出一个默认值MainPage。这是通过以下脚本完成的:
new ModuleFederationPlugin({
   name: "app_three_remote",
   library: { type: "var", name: "app_three_remote" },
   filename: "remoteEntry.js",
   exposes: {
     Button: "./src/Button"
 },
 shared: ["react", "react-dom"]
}),

不出所料,第三个应用看起来与前面的应用相似,只是它不消耗第一个应用的<app>。该应用是独立的,没有导航功能,因此不指定任何远程联邦组件。

在浏览器中查看系统时,应密切注意网络选项卡。代码可以跨三个不同的服务器(潜在地)和三个不同的捆绑包(自然地)进行联合。

这个组件允许在您的构建中有很大的动态性,但是您可能希望避免联合整个应用容器,除非您希望利用服务器端呈现(SSR) 或渐进式加载,否则加载时间会受到严重影响。

加载问题是一个自然的问题,但是一个通常会导致更大项目规模的问题是重复代码的潜在重复,这是使用多个并行包的结果。让我们看看 Webpack 5 是如何处理这个问题的。

重复问题

Webpack 的一个关键特性是删除重复的代码。在联合环境中,主机应用通过依赖关系为远程应用服务。在没有可共享的依赖项的情况下,远程应用可以自动下载自己的依赖项。这是一种内置的冗余故障安全机制。

手动添加供应商代码在规模上可能是乏味的,但是联合特性允许我们创建自动化脚本。这是开发人员的选择,但这可能是一个很好的机会,让你能够测试你的知识。

我们已经提到了 SSR。您应该知道,服务器构建需要一个commonjs库目标,以便它们可以与 Webpack 联合使用。这可以通过 S3 流媒体、ESI 或通过自动化 npm 发布来完成,这样您就可以使用服务器变体。以下代码显示了包含commonjs的示例:

module.exports = {
 plugins: [
  new ModuleFederationPlugin({
    name: "container",
    library: { type: "commonjs-module" },
    filename: "container.js",
    remotes: {
      containerB: "../1-container-full/container.js"
 },
   shared: ["react"]
  })
 ]
};

您可能希望使用target: "node"方法来避免使用文件路径。这将允许在为 Node.js 构建时使用相同的代码库但配置不同的 SSR。这也意味着单独的构建将成为单独的部署。

作为一家公司,Webpack 愿意展示您作为开发人员社区的一员可能已经制作的 SSR 示例。他们会很乐意通过他们的 GitHub 页面接受拉取请求,因为他们有足够的带宽,并从这个过程中受益。

摘要

在本章中,您将了解在线部署项目可能使用的流程。我们检查了安装和设置过程,以及树摇动。

首先,我们研究了生产和开发模式、每个环境的性质以及它们如何利用 Webpack。然后,我们研究了 shimming,使用它的最佳方式,它是如何工作的,以便我们可以修补代码,它与任务运行器的关系,以及它们与 bundlers(如 Webpack)的集成。

现在,您应该能够提取样板文件,集成各种任务运行器,并知道如何使用 GitHub。

在下一章中,我们将讨论热模块替换和实时编码,并掌握一些严肃的教程。

问题

  1. 样板文件是什么意思?
  2. 摇树做什么?
  3. 微光这个术语是什么意思?
  4. 渐进式 web 应用的目的是什么?
  5. 任务跑者做什么?
  6. 本章中提到了哪三个任务跑者?
  7. Webpack 的编译器可以理解用哪三种规范编写的模块?

进一步阅读