深入了解Webpack 5

2021-11-26 10:51:38 浏览数 (1)

Webpack的开发和生产构建

本质上,有两种构建JavaScript应用程序的模式:开发和生产。以前,您已使用开发模式在本地开发环境中开始使用Webpack Dev Server。您可以更改源代码,Webpack再次将其捆绑,Webpack Dev Server会在浏览器中向您显示最新的开发版本。

但是,最终您希望拥有在Web服务器上的生产环境中部署Web应用程序所需的所有构建文件。由于Webpack将所有JavaScript源代码捆绑到一个 dist / index.html 文件中链接的 bundle.js 文件中,因此从 本质上讲 ,您只需要Web服务器上的这两个文件即可向任何人显示Web应用程序。让我们看看如何为您创建两个文件。

首先,您已经有了 dist / index.html 文件。如果打开它,您已经看到它使用了 webpack 创建的bundle.js文件,该文件是 src / 文件夹中所有JavaScript源代码文件中的文件。

代码语言:javascript复制
<!DOCTYPE html>
<html>
  <head>
    <title>Hello Webpack bundled JavaScript Project</title>
  </head>
  <body>
    <div>
      <h1>Hello Webpack bundled JavaScript Project</h1>
    </div>
    <script src="./bundle.js"></script>
  </body>
</html>

其次,如果键入npm start,则Webpack将动态创建此 bundle.js 文件,该文件将用于Webpack Dev Server在开发模式下启动您的应用程序。您从未真正看到过 bundle.js 文件。

代码语言:javascript复制
{
  ...
  "scripts": {
    "start": "webpack serve --config ./webpack.config.js --mode development",
    "test": "echo "Error: no test specified" && exit 0"
  },
  ...
}

现在,让我们介绍第二个npm脚本,以实际构建用于生产的应用程序。我们将显式使用Webpack而不是Webpack Dev Server来捆绑所有JavaScript文件,重用以前的相同Webpack配置,还介绍了生产模式:

代码语言:javascript复制
{
  ...
  "scripts": {
    "start": "webpack serve --config ./webpack.config.js --mode development",
    "build": "webpack --config ./webpack.config.js --mode production",
    "test": "echo "Error: no test specified" && exit 0"
  },
  ...
}

如果运行npm run build,您将看到Webpack如何为您捆绑所有文件。一旦脚本经历了成功,你可以看到 DIST / bundle.js 在飞行中不生成的文件,但在你真正的创建 DIST / 文件夹。

剩下的唯一事情就是现在将 dist / 文件夹上传到Web服务器。但是,为了在本地检查 dist / 文件夹是否具有在远程Web服务器上运行应用程序所需的一切,请使用本地Web服务器亲自进行尝试:

代码语言:javascript复制
npx http-server dist

它应该输出一个URL,您可以在浏览器中访问它。如果一切正常,您可以将 dist / 文件夹及其内容上载到Web服务器。

另请注意,Webpack开发和生产模式具有其自己的默认配置。开发模式在创建源代码文件时会考虑改善的开发人员体验,而生产版本会对源代码进行所有优化。

如何管理您的Webpack构建文件夹

每次运行npm run build,您都会看到Webpack使用 dist / bundle.js 文件创建新版本的bundle JavaScript源代码。最终,您的Webpack构建管道将变得更加复杂,并且最终在 dist / 文件夹中包含两个以上的文件。突然,文件夹变得一团糟,因为您不知道哪些文件属于最新版本。最好的办法是,在每个Webpack构建中都从一个空的 dist / 文件夹开始。

假设我们要在每个Webpack构建中擦除 dist / 文件夹。这将意味着我们自动生成的 dist / bundle.js 文件将被删除(好),而我们手动实现的 dist / index.html 文件将被删除(不好)。我们不想为每个Webpack构建都手动重新创建此文件。为了自动生成 dist / index.html 文件,我们可以使用Webpack插件。首先,从项目的根目录安装html-webpack- plugin插件作为dev依赖项:

代码语言:javascript复制
npm install --save-dev html-webpack-plugin

成功安装后,在Webpack webpack.config.js 文件中引入Webpack插件:

代码语言:javascript复制
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
 
module.exports = {
  entry: path.resolve(__dirname, './src/index.js'),
  module: {
    rules: [
      {
        test: /.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader'],
      },
    ],
  },
  resolve: {
    extensions: ['*', '.js'],
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'bundle.js',
  },
  plugins: [new HtmlWebpackPlugin()],
  devServer: {
    contentBase: path.resolve(__dirname, './dist'),
  },
};

现在,npm run build再次运行,看看它如何自动生成一个新的 dist / index.html 文件。它带有一个默认模板,用于说明文件的结构方式和文件中应包含的内容。但是,如果要为 dist / index.html 文件提供自定义内容,则可以自己指定模板:

代码语言:javascript复制
const HtmlWebpackPlugin = require('html-webpack-plugin')
 
module.exports = {
  ...
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Hello Webpack bundled JavaScript Project',
      template: path.resolve(__dirname, './src/index.html'),
    })
  ],
  ...
};

然后,在您的源代码文件夹中创建一个新的 src / index.html 模板文件,并为其提供以下内容:

代码语言:javascript复制
<!DOCTYPE html>
<html>
  <head>
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div>
      <h1><%= htmlWebpackPlugin.options.title %></h1>
 
      <div id="app">
    </div>
  </body>
</html>

请注意,您不再需要使用 bundle.js 文件指定script标记,因为Webpack会自动为您引入脚本标记。还要注意,您不一定需要id属性和div容器,但在上一教程中我们已使用它在其上执行一些JavaScript。

现在,npm run build再次运行,查看新的自动生成的 dist / index.html是否src / index.html 中的模板匹配。最后,我们已经能够使用Webpack自动创建 dist / bundle.jsdist / index.html 这两个文件。这意味着我们可以在每个Webpack版本中删除 dist / 文件夹中的内容。为此,请引入clean-webpack- plugin插件:

代码语言:javascript复制
npm install --save-dev clean-webpack-plugin

然后在您的 webpack.config.js 文件中引入它:

代码语言:javascript复制
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
 
module.exports = {
  ...
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Hello Webpack bundled JavaScript Project',
      template: path.resolve(__dirname, './src/index.html'),
    }),
  ],
  ...
};

现在,每个Webpack构建都将擦除 dist / 文件夹的内容,然后从头开始创建新的 dist / index.htmldist / bundle.js 文件。通过这种方式进行设置,您将永远不会在 dist / 文件夹中找到来自较旧Webpack构建的文件,这非常适合仅将整个 dist / 文件夹投入生产。

注意:如果使用的是GitHub之类的版本控制系统,则可以将构建文件夹(dist /)放入.gitignore文件中,因为无论如何,所有内容都是自动生成的。某人获得您的项目的副本后,该人可以执行npm run build以生成文件。

Webpack source map

Webpack捆绑了所有JavaScript源代码文件。但这很完美,但是它给我们作为开发人员带来了一个陷阱。一旦引入了错误并在浏览器的开发人员工具中看到了该错误,通常很难跟踪该错误发生的文件,因为Webpack将所有内容捆绑到一个JavaScript文件中。例如,假设我们的 src / index.js 文件从另一个文件导入了一个函数并使用了它:

代码语言:javascript复制
import sum from './sum.js';
 
console.log(sum(2, 5));

在我们的 src / sum.js中 ,我们导出了此JavaScript函数,但不幸的是其中引入了一个错字:

代码语言:javascript复制
export default function (a, b) {
  return a   c;
};

如果npm start在浏览器中运行并打开该应用程序,则应该在开发人员工具中看到发生的错误:

代码语言:javascript复制
sum.js:3 Uncaught ReferenceError: c is not defined
    at eval (sum.js:3)
    at eval (index.js:4)
    at Module../src/index.js (bundle.js:457)
    at __webpack_require__ (bundle.js:20)
    at eval (webpack:///multi_(:8080/webpack)-dev-server/client?:2:18)
    at Object.0 (bundle.js:480)
    at __webpack_require__ (bundle.js:20)
    at bundle.js:84
    at bundle.js:87

如果单击发生错误的 sum.js 文件,则只会看到Webpack的捆绑输出。在此示例的情况下,它仍然可读,但是请想象输出一个更复杂的问题:

代码语言:javascript复制
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = (function (a, b) {
  return a   c;
});
;

进一步执行此步骤,并在您的Webpack构建中引入该错误以进行生产。运行,npm run build然后npx http-server dist再次在浏览器中看到错误:

代码语言:javascript复制
bundle.js:1 Uncaught ReferenceError: c is not defined
    at Module.<anonymous> (bundle.js:1)
    at t (bundle.js:1)
    at bundle.js:1
    at bundle.js:1

这次,它隐藏在 bundle.js 文件中,而没有让您知道导致它的实际文件。此外,一旦单击 bundle.js 文件,您只会看到Webpack捆绑的JavaScript生产版本,其格式不是可读的。

总之,这不是一个很好的开发人员体验,因为Webpack捆绑的JavaScript文件查找错误变得更加困难。对于开发模式,这是正确的,但对于生产模式,则更是如此。

为了克服此问题,可以引入 source map,以为Webpack提供对原始源代码的引用。通过使用 source map,Webpack可以将所有捆绑的源代码映射回原始源。在您的 webpack.config.js 文件中,为 source map引入一种常见配置:

代码语言:javascript复制
module.exports = {
  ...
  devtool: 'source-map',
};

之后,与仍然在源代码中的bug,运行npm run buildnpx http-server dist试。在浏览器中,请注意如何将错误跟踪到导致文件 sum.js

代码语言:javascript复制
sum.js:2 Uncaught ReferenceError: c is not defined
    at Module.<anonymous> (sum.js:2)
    at t (bootstrap:19)
    at bootstrap:83
    at bootstrap:83

单击文件将为您提供实际的源代码和错误的位置,即使Webpack捆绑了所有JavaScript源代码也是如此。还要注意,有一个名为 dist / bundle.js.map 的新文件,该文件用于执行 src /中的 实际源代码和 dist / bundle.js中 的捆绑JavaScript的 映射

Webpack开发/构建配置

到目前为止,我们已经使用一种通用的Webpack配置进行开发和生产。但是,我们也可以为每种模式介绍一个配置。在 package.json中 ,将启动脚本和构建脚本更改为以下内容:

代码语言:javascript复制
{
  ...
  "scripts": {
    "start": "webpack serve --config ./webpack.dev.js",
    "build": "webpack --config ./webpack.prod.js",
    "test": "echo "Error: no test specified" && exit 0"
  },
  ...
}

现在创建这两个新文件,将旧的 webpack.config.js 配置复制并粘贴到两个文件中,然后删除旧的 webpack.config.js 文件。接下来,由于我们在npm脚本中省略了Webpack模式,因此请为每个Webpack配置文件再次介绍它们。首先, webpack.dev.js 文件:

代码语言:javascript复制
module.exports = {
  mode: 'development',
  ...
};

其次, webpack.prod.js 文件:

代码语言:javascript复制
...
 
module.exports = {
  mode: 'production',
  ...
};

您用于启动和构建应用程序的npm脚本应该可以再次工作。但是您可能会想:现在有什么区别?除了我们之前动态传递的Webpack模式外,Webpack的配置对于开发和生产都是相同的。我们甚至引入了不必要的重复。稍后再详细介绍后者。

在不断增长的Webpack配置中,您将介绍在开发和生产中行为应有所不同的内容(例如,插件,规则, source map)。例如,让我们来看一下我们先前实现的 source map。为大型代码库创建 source map文件是一项性能沉重的过程。为了使开发构建快速有效地运行,以提供出色的开发人员体验,您希望开发中的 source map不像生产构建中的 source map那样100%有效。为开发模式创建它们应该更快。因此,您可以对 webpack.dev.js 文件进行首次更改,而该更改不会反映在生产配置中:

代码语言:javascript复制
...
 
module.exports = {
  mode: 'development',
  ...
  devtool: 'eval-source-map',
};

现在,对于您的开发和生产模式, source map的生成方式有所不同,因为在两个Webpack配置文件中以不同的方式定义了 source map。这只是在开发和生产中为Webpack配置不同配置的一个实例。

Webpack合并配置

目前,用于开发和生产的Webpack配置文件共享许多常用配置。如果我们能够将通用配置提取到一个单独的但常用的文件中,而仅根据开发和生产选择额外的特定配置,该怎么办?让我们通过调整 package.json 文件来做到这一点:

代码语言:javascript复制
{
  ...
  "scripts": {
    "start": "webpack serve --config build-utils/webpack.config.js --env env=dev",
    "build": "webpack --config build-utils/webpack.config.js --env env=prod",
    "test": "echo "Error: no test specified" && exit 0"
  },
  ...
}

如您所见,我们为两个npm脚本引用了一个新的共享 webpack.config.js 。该文件位于新的 build-utils 文件夹中。为了稍后在Webpack配置中区分正在运行的脚本,我们还向配置传递了一个环境标志(dev,prod)。

现在,再次创建共享的 build-utils / webpack.config.js 文件,但这一次是在新的专用 build-utils 文件夹中,并为其进行以下配置:

代码语言:javascript复制
const { merge } = require('webpack-merge');
 
const commonConfig = require('./webpack.common.js');
 
module.exports = ({ env }) => {
  const envConfig = require(`./webpack.${env}.js`);
 
  return merge(commonConfig, envConfig);
};

您可以看到该函数env从npm脚本接收了我们的环境标志。这样,我们可以动态地需要一个具有JavaScript模板文字的特定于环境的Webpack配置文件,并将其与通用的Webpack配置合并。为了合并它,让我们安装一个小助手库:

代码语言:javascript复制
npm install --save-dev webpack-merge

接下来,我们现在必须在 build-utils 文件夹中实现三个文件:

  • webpack.common.js:用于开发和构建模式的共享Webpack配置。
  • webpack.dev.js:Webpack配置仅由开发模式使用。
  • webpack.prod.js:Webpack配置仅由生产模式使用。

让我们从新的 build-utils / webpack.common.js 文件中的共享Webpack配置开始:

代码语言:javascript复制
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin')
 
module.exports = {
  entry: path.resolve(__dirname, '..', './src/index.js'),
  module: {
    rules: [
      {
        test: /.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      }
    ]
  },
  resolve: {
    extensions: ['*', '.js']
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Hello Webpack bundled JavaScript Project',
      template: path.resolve(__dirname, '..', './src/index.html'),
    })
  ],
  output: {
    path: path.resolve(__dirname, '..', './dist'),
    filename: 'bundle.js'
  },
  devServer: {
    contentBase: path.resolve(__dirname, '..', './dist'),
  },
};

请注意,与以前的Webpack配置相比,某些文件路径已更改,因为我们现在在专用文件夹中拥有此文件。还要注意,没有Webpack模式,也没有 source map。这两个选项将成为其专用Webpack配置文件中特定的环境(例如,开发,生产)。

通过创建 build-utils / webpack.dev.js 文件 继续进行 操作,并为其提供以下内容:

代码语言:javascript复制
module.exports = {
  mode: 'development',
  devtool: 'eval-source-map',
}

最后但并非最不重要的是,新的 build-utils / webpack.prod.js 文件接收以下内容:

代码语言:javascript复制
module.exports = {
  mode: 'production',
  devtool: 'source-map',
};

您的文件夹结构现在应该类似于以下内容。请注意,先前各节的 build-utils / 文件夹之外没有Webpack配置:

代码语言:javascript复制
- build-utils/
-- webpack.common.js
-- webpack.config.js
-- webpack.dev.js
-- webpack.prod.js
- dist/
-- bundle.js
-- bundle.js.map
-- index.html
- src/
-- index.html
-- index.js
- package.json
- .babelrc

您的npm startnpm run build脚本现在应该可以工作了。相对于其 build-utils / webpack.dev.jsbuild-utils / webpack.prod.js 配置文件,两者都针对Webpack模式和 source map使用不同的配置。但是它们也共享来自 build-utils / webpack.common.js 的通用Webpack配置。一切都动态合并在 build-utils / webpack.config.js 文件中,该文件根据 package.json中 npm脚本中的传入标志进行动态合并。

Webpack环境变量:定义

有时您可能想在源代码中知道您是处于开发还是生产模式。对于这些情况,您可以通过Webpack指定动态环境变量。由于每个环境都有一个Webpack配置文件(开发,生产),因此可以为它们定义专用的环境变量。在您的 build-utils / webpack.dev.js中 ,通过以下方式定义环境变量:

代码语言:javascript复制
const { DefinePlugin } = require('webpack');
 
module.exports = {
  mode: 'development',
  plugins: [
    new DefinePlugin({
      'process.env': {
        'NODE_ENV': JSON.stringify('development'),
      }
    }),
  ],
  devtool: 'eval-source-map',
};

这同样适用于您的 build-utils / webpack.prod.js 文件,但具有不同的环境变量:

代码语言:javascript复制
const { DefinePlugin } = require('webpack');
 
module.exports = {
  mode: 'production',
  plugins: [
    new DefinePlugin({
      'process.env': {
        'NODE_ENV': JSON.stringify('production'),
      }
    }),
  ],
  devtool: 'source-map',
};

现在,您可以使用 src / index.js 文件中console.log(process.env.NODE_ENV);的环境变量或 src / 文件夹中的任何其他JavaScript来基于它进行决策。在这种情况下,您已经创建了两个不同的环境变量- 每个都针对Webpack模式。但是,将来您可能会为某些情况引入更多的环境变量。

Webpack环境变量:.ENV

以前,您开始在Webpack配置文件中定义环境变量。但是,这不是敏感信息的最佳实践。例如,假设您要根据开发或生产环境使用API密钥/秘密(凭证)来访问数据库。您不想在您的Webpack配置中公开这些敏感信息,而这些信息可能会与其他人共享。相反,您想为环境文件引入专用文件,这些文件可以与其他文件和版本控制系统(如Git或SVN)保持距离。

让我们从为开发和生产模式创建两个环境变量文件开始。第一个用于开发模式,称为 .env.development 。将其放入具有以下内容的项目的根目录中:

代码语言:javascript复制
NODE_ENV=development

第二个称为 .env.production 并具有其他内容。它还放置在项目的根目录中:

代码语言:javascript复制
NODE_ENV=production

通过使用dotenv- webpack插件,您可以将这些环境变量复制到Webpack配置文件中。首先,安装插件:

代码语言:javascript复制
npm install dotenv-webpack --save-dev

其次,在开发模式的 build-utils / webpack.dev.js 文件中使用它:

代码语言:javascript复制
const path = require('path');
const Dotenv = require('dotenv-webpack');
 
module.exports = {
  mode: 'development',
  plugins: [
    new Dotenv({
      path: path.resolve(__dirname, '..', './.env.development'),
    })
  ],
  devtool: 'eval-source-map',
};

第三,在生产模式的 build-utils / webpack.prod.js 文件中使用它:

代码语言:javascript复制
const path = require('path');
const Dotenv = require('dotenv-webpack');
 
module.exports = {
  mode: 'development',
  plugins: [
    new Dotenv({
      path: path.resolve(__dirname, '..', './.env.production'),
    })
  ],
  devtool: 'eval-source-map',
};

现在,您可以通过 .env.development.env.production 文件在环境变量中引入敏感信息(例如IP地址,帐户凭据和API密钥/秘密)。您的Webpack配置将复制它们,以使其在您的源代码中可访问(请参阅上一节)。如果您使用的是版本控制系统(例如Git),请不要忘记将这些新的 .env 文件添加到您的 .gitignore中 ,以向第三方隐藏您的敏感信息。

Webpack插件

Webpack具有庞大的插件生态系统。通过使用Webpack开发或生产模式已经隐式使用了其中的几个。但是,还有其他Webpack插件可以改善您的Webpack捆绑包体验。例如,让我们介绍可用于分析和可视化Webpack捆绑包的加载项。在 package.json中 ,为您的构建过程引入一个新的npm脚本,但是这次使用Webpack插件:

代码语言:javascript复制
{
  ...
  "scripts": {
    "start": "webpack serve --config build-utils/webpack.config.js --env env=dev",
    "build": "webpack --config build-utils/webpack.config.js --env env=prod",
    "build:analyze": "npm run build -- --env addon=bundleanalyze",
    "test": "echo "Error: no test specified" && exit 0"
  },
  ...
}

请注意,这个新的npm脚本如何运行另一个npm脚本,但是具有附加配置(此处是Webpack插件)。但是,Webpack插件不会神奇地运行。在这种情况下,它们仅作为标志传递给我们的Webpack配置。让我们看看如何在 build-utils / webpack.config.js 文件中使用它们:

代码语言:javascript复制
const { merge } = require('webpack-merge');
 
const commonConfig = require('./webpack.common.js');
 
const getAddons = (addonsArgs) => {
  const addons = Array.isArray(addonsArgs)
    ? addonsArgs
    : [addonsArgs];
 
  return addons
    .filter(Boolean)
    .map((name) => require(`./addons/webpack.${name}.js`));
};
 
module.exports = ({ env, addon }) => {
  const envConfig = require(`./webpack.${env}.js`);
 
  return merge(commonConfig, envConfig, ...getAddons(addon));
};

现在,不仅合并了通用的和特定于环境的Webpack配置,而且还合并了可选的附加组件,我们将它们放入专用的 build-utils / addons 文件夹中。让我们从 build-utils / addons / webpack.bundleanalyze.js 文件开始:

代码语言:javascript复制
const path = require('path');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
 
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: path.resolve(
        __dirname,
        '..',
        '..',
        './dist/report.html'
      ),
      openAnalyzer: false,
    }),
  ],
};

接下来,在命令行上通过npm安装Webpack插件:

代码语言:javascript复制
npm install --save-dev webpack-bundle-analyzer

如您所见,您已经在新的 build-utils / addons / 文件夹中引入了一个特定的Webpack插件,可以选择添加该Webpack插件。插件文件的命名与 package.json中 npm脚本传递的标志匹配。您的Webpack合并确保将所有传递的插件标记添加为Webpack配置中的实际插件。

现在,请自己尝试用于Webpack分析和可视化的可选工具。在命令行上,键入npm run build:analyze。然后,检查您的 dist / 文件夹中是否有新文件。您应该找到一种可以通过以下方式打开的方式:

  • Webpack的bundleanalyze: dist / report.html
  • 通过打开npx http-server dist,访问URL,然后附加 /report.html

您将看到具有两种不同可视化效果的构建优化的Webpack捆绑包。您的应用程序中没有很多代码,但是一旦您在节点包管理器中引入了更多的源代码和更多的外部库(依赖项),您将看到Webpack包的大小将如何增长。最终,您将偶然地引入一个大型库,这会使您的应用程序变得太大。然后,分析工具和可视化工具都可以帮助您找到问题的根源。

0 人点赞