保持控制:Webpack 和 React 指南,Pt。 1

已发表: 2022-03-11

在开始一个新的 React 项目时,您有许多模板可供选择:创建 React App、 react-boilerplate和 React Starter Kit,仅举几例。

这些模板被成千上万的开发人员采用,能够支持非常大规模的应用程序开发。 但是它们让开发人员体验和捆绑输出背负着各种默认值,这可能并不理想。

如果您想对构建过程保持更大程度的控制,那么您可能会选择投资自定义 Webpack 配置。 正如您将从本 Webpack 教程中了解到的那样,此任务并不是很复杂,而且在排除其他人的配置问题时,这些知识甚至可能很有用。

Webpack:入门

我们今天编写 JavaScript 的方式与浏览器可以执行的代码不同。 我们经常依赖现代浏览器尚未支持的其他类型的资源、转译语言和实验性功能。 Webpack 是一个用于 JavaScript 的模块打包器,它可以弥补这一差距,并在开发人员体验方面免费生成跨浏览器兼容的代码。

在我们开始之前,您应该记住,本 Webpack 教程中提供的所有代码也可以在 GitHub 上以完整的 Webpack/React 示例配置文件的形式获得。 如果您有任何问题,请随时在此处参考并返回本文。

基本配置

从 Legato(版本 4)开始,Webpack 不需要任何配置即可运行。 选择构建模式将应用一组更适合目标环境的默认值。 本着本文的精神,我们将抛开这些默认设置,自己为每个目标环境实施合理的配置。

首先,我们需要安装webpackwebpack-cli

 npm install -D webpack webpack-cli

然后我们需要使用具有以下选项的配置填充webpack.config.js

  • devtool :在开发模式下启用源映射生成。
  • entry :我们的 React 应用程序的主文件。
  • output.path :存储输出文件的根目录。
  • output.filename :用于生成文件的文件名模式。
  • output.publicPath :文件将在 Web 服务器上部署的根目录的路径。
 const path = require("path"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; const isDevelopment = !isProduction; return { devtool: isDevelopment && "cheap-module-source-map", entry: "./src/index.js", output: { path: path.resolve(__dirname, "dist"), filename: "assets/js/[name].[contenthash:8].js", publicPath: "/" } }; };

上述配置适用于纯 JavaScript 文件。 但是在使用 Webpack 和 React 时,我们需要在将代码发送给用户之前执行额外的转换。 在下一节中,我们将使用 Babel 来改变 Webpack 加载 JavaScript 文件的方式。

JS 加载器

Babel 是一个带有许多用于代码转换的插件的 JavaScript 编译器。 在本节中,我们将把它作为加载器引入到我们的 Webpack 配置中,并将其配置为将现代 JavaScript 代码转换为普通浏览器可以理解的代码。

首先,我们需要安装babel-loader@babel/core

 npm install -D @babel/core babel-loader

然后我们将在我们的 Webpack 配置中添加一个module部分,让babel-loader负责加载 JavaScript 文件:

 @@ -11,6 +11,25 @@ module.exports = function(_env, argv) { path: path.resolve(__dirname, "dist"), filename: "assets/js/[name].[contenthash:8].js", publicPath: "/" + }, + module: { + rules: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + options: { + cacheDirectory: true, + cacheCompression: false, + envName: isProduction ? "production" : "development" + } + } + } + ] + }, + resolve: { + extensions: [".js", ".jsx"] } }; };

我们将使用单独的配置文件babel.config.js来配置 Babel。 它将使用以下功能:

  • @babel/preset-env :将现代 JavaScript 功能转换为向后兼容的代码。
  • @babel/preset-react :将 JSX 语法转换为普通的 JavaScript 函数调用。
  • @babel/plugin-transform-runtime :通过将 Babel 助手提取到共享模块中来减少代码重复。
  • @babel/plugin-syntax-dynamic-import :在缺乏原生Promise支持的浏览器中启用动态import()语法。
  • @babel/plugin-proposal-class-properties :启用对公共实例字段语法提案的支持,用于编写基于类的 React 组件。

我们还将启用一些特定于 React 的生产优化:

  • babel-plugin-transform-react-remove-prop-types -types 从生产代码中删除不必要的 prop-types。
  • @babel/plugin-transform-react-inline-elements在编译期间评估React.createElement并内联结果。
  • @babel/plugin-transform-react-constant-elements静态 React 元素提取为常量。

下面的命令将安装所有必要的依赖项:

 npm install -D @babel/preset-env @babel/preset-react @babel/runtime @babel/plugin-transform-runtime @babel/plugin-syntax-dynamic-import @babel/plugin-proposal-class-properties babel-plugin-transform-react-remove-prop-types @babel/plugin-transform-react-inline-elements @babel/plugin-transform-react-constant-elements

然后我们将使用这些设置填充babel.config.js

 module.exports = { presets: [ [ "@babel/preset-env", { modules: false } ], "@babel/preset-react" ], plugins: [ "@babel/plugin-transform-runtime", "@babel/plugin-syntax-dynamic-import", "@babel/plugin-proposal-class-properties" ], env: { production: { only: ["src"], plugins: [ [ "transform-react-remove-prop-types", { removeImport: true } ], "@babel/plugin-transform-react-inline-elements", "@babel/plugin-transform-react-constant-elements" ] } } };

这种配置允许我们以与所有相关浏览器兼容的方式编写现代 JavaScript。 在 React 应用程序中我们可能需要其他类型的资源,我们将在以下部分中介绍。

CSS 加载器

在设计 React 应用程序的样式时,我们至少需要能够包含纯 CSS 文件。 我们将使用以下加载器在 Webpack 中执行此操作:

  • css-loader :解析 CSS 文件,解析外部资源,例如图像、字体和其他样式导入。
  • style-loader :在开发过程中,在运行时将加载的样式注入到文档中。
  • mini-css-extract-plugin :将加载的样式提取到单独的文件中以供生产使用,以利用浏览器缓存。

让我们安装上面的 CSS 加载器:

 npm install -D css-loader style-loader mini-css-extract-plugin

然后我们将在 webpack 配置的module.rules部分添加一个新规则:

 @@ -1,4 +1,5 @@ const path = require("path"); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; @@ -25,6 +26,13 @@ module.exports = function(_env, argv) { envName: isProduction ? "production" : "development" } } + }, + { + test: /\.css$/, + use: [ + isProduction ? MiniCssExtractPlugin.loader : "style-loader", + "css-loader" + ] } ] },

我们还将MiniCssExtractPlugin添加到plugins部分,我们将仅在生产模式下启用:

 @@ -38,6 +38,13 @@ module.exports = function(_env, argv) { }, resolve: { extensions: [".js", ".jsx"] - } + }, + plugins: [ + isProduction && + new MiniCssExtractPlugin({ + filename: "assets/css/[name].[contenthash:8].css", + chunkFilename: "assets/css/[name].[contenthash:8].chunk.css" + }) + ].filter(Boolean) }; };

此配置适用于纯 CSS 文件,并且可以扩展为与各种 CSS 处理器一起使用,例如 Sass 和 PostCSS,我们将在下一篇文章中讨论。

图像加载器

Webpack 还可以用于加载静态资源,例如图像、视频和其他二进制文件。 处理此类文件的最通用方法是使用file-loaderurl-loader ,它将为其消费者提供所需资源的 URL 引用。

在本节中,我们将添加url-loader来处理常见的图像格式。 url-loaderfile-loader的不同之处在于,如果原始文件的大小小于给定的阈值,它将把整个文件作为 base64 编码的内容嵌入到 URL 中,从而无需额外的请求。

首先我们安装url-loader

 npm install -D url-loader

然后我们在 Webpack 配置的module.rules部分添加一个新规则:

 @@ -34,6 +34,16 @@ module.exports = function(_env, argv) { isProduction ? MiniCssExtractPlugin.loader : "style-loader", "css-loader" ] + }, + { + test: /\.(png|jpg|gif)$/i, + use: { + loader: "url-loader", + options: { + limit: 8192, + name: "static/media/[name].[hash:8].[ext]" + } + } } ] },

SVG

对于 SVG 图像,我们将使用@svgr/webpack加载器,它将导入的文件转换为 React 组件。

我们安装@svgr/webpack

 npm install -D @svgr/webpack

然后我们在 Webpack 配置的module.rules部分添加一个新规则:

 @@ -44,6 +44,10 @@ module.exports = function(_env, argv) { name: "static/media/[name].[hash:8].[ext]" } } + }, + { + test: /\.svg$/, + use: ["@svgr/webpack"] } ] },

SVG 图像作为 React 组件可以很方便, @svgr/webpack使用 SVGO 进行优化。

注意:对于某些动画甚至鼠标悬停效果,您可能需要使用 JavaScript 操作 SVG。 幸运的是, @svgr/webpack将 SVG 内容嵌入到 JavaScript 包中,允许您绕过为此所需的安全限制。

文件加载器

当我们需要引用任何其他类型的文件时,通用file-loader将完成这项工作。 它的工作方式类似于url-loader ,为需要它的代码提供一个资产 URL,但它没有尝试对其进行优化。

与往常一样,首先我们安装 Node.js 模块。 在这种情况下, file-loader

 npm install -D file-loader

然后我们将新规则添加到 Webpack 配置的module.rules部分。 例如:

 @@ -48,6 +48,13 @@ module.exports = function(_env, argv) { { test: /\.svg$/, use: ["@svgr/webpack"] + }, + { + test: /\.(eot|otf|ttf|woff|woff2)$/, + loader: require.resolve("file-loader"), + options: { + name: "static/media/[name].[hash:8].[ext]" + } } ] },

在这里,我们添加了用于加载字体file-loader ,您可以从 CSS 文件中引用它。 您可以扩展此示例以加载您需要的任何其他类型的文件。

环境插件

我们可以使用 Webpack 的DefinePlugin()将环境变量从构建环境暴露给我们的应用程序代码。 例如:

 @@ -1,5 +1,6 @@ const path = require("path"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const webpack = require("webpack"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; @@ -65,7 +66,12 @@ module.exports = function(_env, argv) { new MiniCssExtractPlugin({ filename: "assets/css/[name].[contenthash:8].css", chunkFilename: "assets/css/[name].[contenthash:8].chunk.css" - }) + }), + new webpack.DefinePlugin({ + "process.env.NODE_ENV": JSON.stringify( + isProduction ? "production" : "development" + ) + }) ].filter(Boolean) }; };

在这里,我们将process.env.NODE_ENV替换为表示构建模式的字符串: "development""production"

HTML 插件

在没有index.html文件的情况下,我们的 JavaScript 包毫无用处,只是坐在那里没有人能找到它。 在本节中,我们将介绍html-webpack-plugin为我们生成一个 HTML 文件。

我们安装html-webpack-plugin

 npm install -D html-webpack-plugin

然后我们将html-webpack-plugin添加到 Webpack 配置的plugins部分:

 @@ -1,6 +1,7 @@ const path = require("path"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const webpack = require("webpack"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; @@ -71,6 +72,10 @@ module.exports = function(_env, argv) { "process.env.NODE_ENV": JSON.stringify( isProduction ? "production" : "development" ) + }), + new HtmlWebpackPlugin({ + template: path.resolve(__dirname, "public/index.html"), + inject: true }) ].filter(Boolean) };

生成的public/index.html文件将加载我们的包并引导我们的应用程序。

优化

我们可以在构建过程中使用多种优化技术。 我们将从代码缩小开始,通过这个过程,我们可以在不牺牲功能的情况下减小包的大小。 我们将使用两个插件来最小化我们的代码:用于 JavaScript 代码的terser-webpack-plugin和用于 CSS optimize-css-assets-webpack-plugin

让我们安装它们:

 npm install -D terser-webpack-plugin optimize-css-assets-webpack-plugin

然后我们将在我们的配置中添加一个optimization部分:

 @@ -2,6 +2,8 @@ const path = require("path"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const webpack = require("webpack"); const HtmlWebpackPlugin = require("html-webpack-plugin"); +const TerserWebpackPlugin = require("terser-webpack-plugin"); +const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; @@ -75,6 +77,27 @@ module.exports = function(_env, argv) { isProduction ? "production" : "development" ) }) - ].filter(Boolean) + ].filter(Boolean), + optimization: { + minimize: isProduction, + minimizer: [ + new TerserWebpackPlugin({ + terserOptions: { + compress: { + comparisons: false + }, + mangle: { + safari10: true + }, + output: { + comments: false, + ascii_only: true + }, + warnings: false + } + }), + new OptimizeCssAssetsPlugin() + ] + } }; };

上述设置将确保代码与所有现代浏览器兼容。

代码拆分

代码拆分是我们可以用来提高应用程序性能的另一种技术。 代码拆分可以指两种不同的方法:

  1. 使用动态import()语句,我们可以提取应用程序中占我们包大小的重要部分的部分,并按需加载它们。
  2. 我们可以提取更改频率较低的代码,以利用浏览器缓存并提高重复访问者的性能。

我们将使用将第三方依赖项和公共块提取到单独文件中的设置填充 Webpack 配置的optimization.splitChunks部分:

 @@ -99,7 +99,29 @@ module.exports = function(_env, argv) { sourceMap: true }), new OptimizeCssAssetsPlugin() - ] + ], + splitChunks: { + chunks: "all", + minSize: 0, + maxInitialRequests: 20, + maxAsyncRequests: 20, + cacheGroups: { + vendors: { + test: /[\\/]node_modules[\\/]/, + name(module, chunks, cacheGroupKey) { + const packageName = module.context.match( + /[\\/]node_modules[\\/](.*?)([\\/]|$)/ + )[1]; + return `${cacheGroupKey}.${packageName.replace("@", "")}`; + } + }, + common: { + minChunks: 2, + priority: -10 + } + } + }, + runtimeChunk: "single" } }; };

让我们更深入地看看我们在这里使用的选项:

  • chunks: "all" :默认情况下,普通块提取仅影响使用动态import()加载的模块。 此设置还可以优化入口点加载。
  • minSize: 0 :默认情况下,只有超过一定大小阈值的块才有资格被提取。 此设置可以优化所有常见代码,无论其大小。
  • maxInitialRequests: 20maxAsyncChunks: 20 :这些设置分别增加了入口点导入和分割点导入可以并行加载的源文件的最大数量。

此外,我们指定以下cacheGroups配置:

  • vendors :配置第三方模块的提取。
    • test: /[\\/]node_modules[\\/]/ :用于匹配第三方依赖项的文件名模式。
    • name(module, chunks, cacheGroupKey) :通过给它们一个通用名称,将来自同一模块的不同块组合在一起。
  • common :配置从应用程序代码中提取公共块。
    • minChunks: 2 :如果从至少两个模块中引用,则认为一个块是通用的。
    • priority: -10 :为common缓存组分配负优先级,以便首先考虑vendors缓存组的块。

我们还通过指定runtimeChunk: "single"将 Webpack 运行时代码提取到可以在多个入口点之间共享的单个块中。

开发服务器

到目前为止,我们一直专注于创建和优化应用程序的生产构建,但 Webpack 也有自己的 Web 服务器,具有实时重新加载和错误报告功能,这将有助于我们的开发过程。 它叫做webpack-dev-server ,我们需要单独安装它:

 npm install -D webpack-dev-server

在这个片段中,我们在 Webpack 配置中引入了一个devServer部分:

 @@ -120,6 +120,12 @@ module.exports = function(_env, argv) { } }, runtimeChunk: "single" + }, + devServer: { + compress: true, + historyApiFallback: true, + open: true, + overlay: true } }; };

在这里,我们使用了以下选项:

  • compress: true : 启用资产压缩以更快地重新加载。
  • historyApiFallback: true : 为基于历史的路由启用对index.html的回退。
  • open: true : 启动开发服务器后打开浏览器。
  • overlay: true : 在浏览器窗口中显示 Webpack 错误。

您可能还需要配置代理设置以将 API 请求转发到后端服务器。

Webpack 和 React:性能优化并准备就绪!

我们刚刚学习了如何使用 Webpack 加载各种资源类型,如何在开发环境中使用 Webpack,以及优化生产构建的几种技术。 如果需要,您可以随时查看完整的配置文件以获取您自己的 React/Webpack 设置的灵感。 对于任何提供 React 开发服务的人来说,提高这些技能是标准的。

在本系列的下一部分中,我们将使用针对更具体用例的说明扩展此配置,包括 TypeScript 使用、CSS 预处理器以及涉及服务器端渲染和 ServiceWorkers 的高级优化技术。 请继续关注以了解您需要了解的有关 Webpack 的所有信息,以便将您的 React 应用程序投入生产。