本文概述
当启动一个新的React项目时, 你有很多模板可供选择:创建React App, react-boilerplate和React Starter Kit, 仅举几例。
这些模板被成千上万的开发人员所采用, 能够大规模支持应用程序开发。但是它们留下了开发人员的经验, 并且捆绑了各种默认值的输出, 这可能不是理想的选择。
如果要对构建过程保持更大程度的控制, 则可以选择投资自定义Webpack配置。正如你将从本Webpack教程中学到的那样, 该任务并不是很复杂, 并且在对其他人的配置进行故障排除时, 该知识甚至可能会很有用。
Webpack:入门
我们今天编写JavaScript的方式与浏览器可执行的代码不同。我们经常依赖其他类型的资源, 转译语言和实验性功能, 而现代浏览器尚不支持这些功能。 Webpack是JavaScript的模块捆绑器, 可以消除这种差距, 并在开发人员体验方面免费提供跨浏览器兼容的代码。
在开始之前, 你应该记住, 本Webpack教程中介绍的所有代码也都可以在GitHub上以完整的Webpack / React示例配置文件的形式获得。如果你有任何疑问, 请随时在此进行引用, 并返回本文。
基本配置
从Legato(版本4)开始, Webpack不需要任何配置即可运行。选择构建模式将应用一组更适合目标环境的默认值。本着本文的精神, 我们将忽略这些默认值, 并为每个目标环境自己实现一个明智的配置。
首先, 我们需要安装webpack和webpack-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配置中, 使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从生产代码中删除了不必要的prop-type。
- @ 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文件, 解析外部资源, 例如图像, 字体和其他样式导入。
- 样式加载器:开发期间, 在运行时将加载的样式注入文档中。
- 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添加到”插件”部分, 仅在生产模式下启用:
@@ -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还可以用于加载静态资源, 例如图像, 视频和其他二进制文件。处理此类文件的最通用方法是使用文件加载器或url加载器, 这将为其使用方提供所需资源的URL引用。
在本节中, 我们将添加url-loader以处理常见的图像格式。使url-loader与file-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捆绑包, 从而使你可以绕过此操作所需的安全限制。
文件加载器
当我们需要引用任何其他类型的文件时, 通用文件加载器将完成此工作。它的工作方式与url-loader相似, 它为需要它的代码提供资产URL, 但没有尝试对其进行优化。
与往常一样, 首先我们安装Node.js模块。在这种情况下, 文件加载器:
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]"
+ }
}
]
},
在这里, 我们添加了用于加载字体的文件加载器, 你可以从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
然后, 我们将优化部分添加到配置中:
@@ -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()
+ ]
+ }
};
};
上面的设置将确保代码与所有现代浏览器兼容。
代码分割
代码拆分是我们可以用来提高应用程序性能的另一种技术。代码拆分可以引用两种不同的方法:
- 使用动态import()语句, 我们可以提取应用程序中占我们包大小很大一部分的部分, 并按需加载它们。
- 我们可以提取更改频率较低的代码, 以便利用浏览器缓存并提高重复访问者的性能。
我们将使用将第三方依赖项和常见块提取到单独文件中的设置来填充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:20和maxAsyncChunks:20:这些设置分别增加了可为入口点导入和拆分点导入并行加载的源文件的最大数量。
此外, 我们指定以下cacheGroups配置:
- 供应商:配置第三方模块的提取。
- 测试:/ [\\ /] node_modules [\\ /] /:用于匹配第三方依赖项的文件名模式。
- 名称(模块, 块, cacheGroupKey):通过给它们一个通用名称, 将来自同一模块的各个块组合在一起。
- common:配置从应用程序代码提取公共块。
- minChunks:2:如果从至少两个模块中引用了块, 则将其视为公共块。
- 优先级:-10:为公共缓存组分配负优先级, 以便首先考虑供应商缓存组的块。
我们还通过指定runtimeChunk:” single”, 将Webpack运行时代码提取到可以在多个入口点之间共享的单个块中。
开发服务器
到目前为止, 我们专注于创建和优化应用程序的生产版本, 但是Webpack还拥有自己的带有实时重新加载和错误报告的Web服务器, 这将对我们的开发过程有所帮助。它称为webpack-dev-server, 我们需要单独安装:
npm install -D webpack-dev-server
在此代码段中, 我们将devServer部分引入到Webpack配置中:
@@ -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的高级优化技术。请继续学习有关将Web应用程序投入生产所需的Webpack知识。