使用webpack搭建一个React-Cli

基础配置

在配置webpack之前,我们需要将一些其他的必须配置准备好

babel

在根目录下创建.babelrc.js或者babel.config.js配置react预置环境

1
2
3
4
module.exports = {
// 使用react官方规则
presets: ["react-app"],
};

Eslint

在根目录下创建.eslintrc.js或者eslint.config.js配置react预置环境

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
extends: ["react-app"], // 继承 react 官方规则
parserOptions: {
babelOptions: {
presets: [
// 解决页面报错问题
["babel-preset-react-app", false],
"babel-preset-react-app/prod",
],
},
},
};

package.json初步配置

在控制台中输入npm init -y快速生成项目信息,并配置好支持浏览器列表,便于postcssbabel编译

1
2
3
4
5
"browserslist": [
"last 2 version",
"> 1%",
"not dead"
]

开发模式配置

在项目中创建config文件夹,创建webpack.dev.js配置开发模式

项目的入口和导出配置

1
2
3
4
5
6
7
entry: "../src/main.js",
output: {
path: undefined,
filename: "static/js/[name].js",
chunkFilename: "static/js/[name].chunk.js",
assetModuleFilename: "static/js/[hash:10][ext][query]",
},

module配置

由于样式处理中都用到了css-loaderstyle-loader所以将公共部分提取出来封装成一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const getStyleLoaders = (preProcessor) => {
return [
"style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env", // 能解决大多数样式兼容性问题
],
},
},
},
preProcessor,
].filter(Boolean);
};

配置模块,由于是react的脚手架搭建,所以jsx也要通过babel处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
module: {
rules: [
{
oneOf: [
{
test: /\.css$/,
use: getStyleLoaders(),
},
{
test: /\.less$/,
use: getStyleLoaders("less-loader"),
},
{
test: /\.s[ac]ss$/,
use: getStyleLoaders("sass-loader"),
},
{
test: /\.styl$/,
use: getStyleLoaders("stylus-loader"),
},
{
test: /\.jpe?g|png|gif|svg|webp$/,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 图片大小小于10kb会被base64处
},
},
},
{
test: /\.(ttf|woff2?)$/,
type: "asset/resource",
},
{
test: /\.jsx?$/,
include: path.resolve(__dirname, "../src"),
loader: "babel-loader",
options: {
cacheDirectory: true,
cacheCompression: false,
},
},
],
},
],
},

插件配置

开发模式中插件主要是用来配置HTML和eslint的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
plugins: [
new ESLintWebpackPlugin({
context: path.resolve(__dirname, "../src"),
exclude: "node_modules",
cache: true,
cacheLocation: path.resolve(
__dirname,
"../node_modules/.cache/.eslintcache"
),
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../public/index.html"),
}),
],

压缩配置

在开发模式中,不需要进行css和图片等资源的压缩,所以只配置js模块的代码分离以及runtime的hash标识就好

1
2
3
4
5
6
7
8
optimization: {
splitChunks: {
chunks: "all",
},
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`,
},
},

其他配置

剩下的就只需要配置好开发服务器、模式以及source-map就好

1
2
3
4
5
6
7
8
devServer: {
open: true,
host: "localhost",
port: 3000,
hot: true,
},
mode: "development",
devtool: "cheap-module-source-map",

测试脚手架

下载依赖

我这里通过yarn来下载脚手架中所需的依赖

1
yarn add @babel/core @pmmmwh/react-refresh-webpack-plugin babel-loader babel-preset-react-app css-loader eslint-config-react-app eslint-webpack-plugin html-webpack-plugin less-loader postcss-loader postcss-preset-env react-refresh sass-loader style-loader stylus-loader webpack webpack-cli webpack-dev-server -D

以上都是开发需要的依赖,由于我们是react脚手架当然也需要下载react

yarn add react react-dom

运行一个react项目

首先在根目录下创建src目录并创建main.js文件

1
2
3
4
5
6
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

然后在同样的目录下创建App.jsx文件

1
2
3
4
5
6
import React from "react";

function App() {
return <h1>App</h1>;
}
export default App;

再在根目录下创建public文件夹下创建index.html文件并创建app节点,后运行项目

我们会发现出现了这样一个错误

这是因为babel需要变量来辨别是开发环境还是生产环境,如果没有读取到变量的话就会报出错误妨碍程序正常运行,我们需要安上一个名为cross-env的包然后再package.json中配置出该变量。

yarn add cross-env -D

然后再package.json中修改命令

1
2
3
4
5
"scripts": {
"start": "npm run dev",
"dev": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack --config ./config/webpack.prod.js"
},

再次运行就配置好babel所需的环境变量了。但是我们运行后发现又出现了一个错误

image-20220620214711845

这是由于引入的App组件是.jsx的格式,webpack无法识别并打包,所以我们需要在webpack.dev.js中配置解析的格式。

1
2
3
resolve: {
extensions: [".jsx", ".js", ".json"],
},

重新启动项目,我们就会发现程序正常的运行起来了。

image-20220620215039455

其他配置

然后我们在引入几个组件和样式测试一下,创建pages文件夹下创建Home.jsx组件,以及响应的样式

Home/index.less

1
2
3
.home {
color:deeppink
}

Home/Home.jsx

1
2
3
4
5
6
7
import React from "react";
import "./index.less";

function Home() {
return <h1 className="home">Home</h1>;
}
export default Home;

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
import React from "react";
import Home from "./pages/Home/Home";

function App() {
return (
<div>
<h1>App</h1>
<Home />
</div>
);
}
export default App;

运行后结果正常,样式也能实现热模替换

image-20220620220106137

但是修改Home.jsx无法实现热模替换,在react中实现热模替换是需要下载插件以及配置的,插件在上面已经下载好了,直接引用配置即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 引入插件
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
// 修改配置module中
{
test: /\.jsx?$/,
include: path.resolve(__dirname, "../src"),
loader: "babel-loader",
options: {
cacheDirectory: true,
cacheCompression: false,
plugins: [
// "@babel/plugin-transform-runtime", // presets中包含了
"react-refresh/babel", // 开启js的HMR功能
],
},
},
// 修改配置plugin中
plugins: [
new ESLintWebpackPlugin({
context: path.resolve(__dirname, "../src"),
exclude: "node_modules",
cache: true,
cacheLocation: path.resolve(
__dirname,
"../node_modules/.cache/.eslintcache"
),
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../public/index.html"),
}),
new ReactRefreshWebpackPlugin(),
],

这样就能实现热模替换了

然后我们下载react-router设置路由查看是否正常运行

`yarn add react-router-donm

main.js

1
2
3
4
5
6
7
8
9
10
11
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("app"));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React from "react";
import { Suspense, lazy } from "react";
import { Routes, Route, Link } from "react-router-dom";

const Home = lazy(() => import("./pages/Home/Home"));
const About = lazy(() => import("./pages/About/About"));

function App() {
return (
<div>
<h1>App</h1>
<ul>
<li>
<Link to="/home"></Link>
</li>
<li>
<Link to="/about"></Link>
</li>
</ul>
<Suspense>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</div>
);
}
export default App;

在pages下创建About组件

About/About.jsx

1
2
3
4
5
6
import React from "react";

function About() {
return <h1>About</h1>;
}
export default About;

运行代码

image-20220620224312769

当我们刷新页面后,就会发现出现了问题

image-20220620224347907

由于刷新后,浏览器就会直接根据地址来去文件夹中寻找home,由于没有找到home所以就会这样。我们可以通过配置webpack重新将入口定向到index然后匹配路由就可以了

webpack.dev.js

1
2
3
4
5
6
7
8
devServer: {
open: true,
host: "localhost",
port: 3000,
hot: true,
compress: true,
historyApiFallback: true, // 解决react-router刷新404问题
},

这样这个问题就解决了,然后我们检查一下按需引入的情况

image-20220620224800556

按需引入也正常的完成了,这样我们的开发环境配置就结束了。

生产模式配置

我们直接在开发模式的基础上修改配置就好了

首先要修改样式的公共方法,在开发模式中使用的loader为style-loader,在生产模式中使用的则是MiniCssExtractPlugin.loader,只需要替换一下即可。

入口导出配置

因为生产模式要打包出去了,所以我们的path也不能是undefined了

1
2
3
4
5
6
7
8
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "../dist"),
filename: "static/js/[name].[contenthash:10].js",
chunkFilename: "static/js/[name].[contenthash:10].chunk.js",
assetModuleFilename: "static/js/[hash:10][ext][query]",
clean: true,
},

module配置

在生产模式中不需要用到HMR了,所以可以直接将babel中的HMR相关的配置删除

插件配置

同样的插件中的和HMR相关的配置也可以删除掉

控制css输出的配置

1
2
3
4
5
6
7
// 引入插件
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
// 插件配置
new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash:10].css",
chunkFilename: "static/css/[name].[contenthash:10].chunk.css",
}),

压缩配置

引入插件

1
2
3
const TerserWebpackPlugin = require("terser-webpack-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");

配置压缩的插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
minimizer: [
new CssMinimizerPlugin(), // 压缩css
new TerserWebpackPlugin(), // 压缩js
new ImageMinimizerPlugin({ // 压缩图片
minimizer: {
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: [
["gifsicle", { interlaced: true }],
["jpegtran", { progressive: true }],
["optipng", { optimizationLevel: 5 }],
[
"svgo",
{
plugins: [
"preset-default",
"prefixIds",
{
name: "sortAttrs",
params: {
xmlnsOrder: "alphabetical",
},
},
],
},
],
],
},
},
}),
],

其他配置

将mode调整为production,devtool调整为source-map,删除掉服务器配置

下载依赖

由于图片压缩的依赖包下载问题较大,所以将它单独下载

yarn add mini-css-extract-plugin css-minimizer-webpack-plugin image-minimizer-webpack-plugin imagemin -D

yarn add imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D

如果下载不了的话建议使用cnpm下载

图标等在public下的文件的处理

进行打包操作后,如果你为index.html配置了网页图标那么就会发现,在打包后的文件夹内并没有图标,这是因为webpack打包时不会打包public文件夹下的文件,所以需要用到一个插件来解决这个问题

下载插件:yarn add copy-webpack-plugin -D

使用插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 引入插件
const CopyPlugin = require("copy-webpack-plugin");
// 使用插件
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, "../public"),
to: path.resolve(__dirname, "../dist"),
toType: "dir",
noErrorOnMissing: true, // 不生成错误
globOptions: {
// 忽略文件
ignore: ["**/index.html"],
},
info: {
// 跳过terser压缩js
minimized: true,
},
},
],
}),

运行打包后图标就可以正常显示了

合并配置

在开发模式和生产模式中有许多配置相同的地方,如果使用两个文件作为配置复用率太低,所以可以合并配置来提高代码复用性

在配置开发模式运行react的一开始我们就配置了cross-env这个插件,所以我们也可以在webpack.config.js中通过环境变量来判断环境生成配置

1
const isProduction = process.env.NODE_ENV === "production";

合并后的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");

// 需要通过 cross-env 定义环境变量
const isProduction = process.env.NODE_ENV === "production";

const getStyleLoaders = (preProcessor) => {
return [
isProduction ? MiniCssExtractPlugin.loader : "style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env", // 能解决大多数样式兼容性问题
],
},
},
},
preProcessor,
].filter(Boolean);
};

module.exports = {
entry: "./src/main.js",
output: {
path: isProduction ? path.resolve(__dirname, "../dist") : undefined,
filename: isProduction
? "static/js/[name].[contenthash:10].js"
: "static/js/[name].js",
chunkFilename: isProduction
? "static/js/[name].[contenthash:10].chunk.js"
: "static/js/[name].chunk.js",
assetModuleFilename: "static/js/[hash:10][ext][query]",
clean: true,
},
module: {
rules: [
{
oneOf: [
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左
use: getStyleLoaders(),
},
{
test: /\.less$/,
use: getStyleLoaders("less-loader"),
},
{
test: /\.s[ac]ss$/,
use: getStyleLoaders("sass-loader"),
},
{
test: /\.styl$/,
use: getStyleLoaders("stylus-loader"),
},
{
test: /\.(png|jpe?g|gif|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 小于10kb的图片会被base64处理
},
},
},
{
test: /\.(ttf|woff2?)$/,
type: "asset/resource",
},
{
test: /\.(jsx|js)$/,
include: path.resolve(__dirname, "../src"),
loader: "babel-loader",
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩
plugins: [
// "@babel/plugin-transform-runtime", // presets中包含了
!isProduction && "react-refresh/babel",
].filter(Boolean),
},
},
],
},
],
},
plugins: [
new ESLintWebpackPlugin({
extensions: [".js", ".jsx"],
context: path.resolve(__dirname, "../src"),
exclude: "node_modules",
cache: true,
cacheLocation: path.resolve(
__dirname,
"../node_modules/.cache/.eslintcache"
),
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../public/index.html"),
}),
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, "../public"),
to: path.resolve(__dirname, "../dist"),
toType: "dir",
noErrorOnMissing: true, // 不生成错误
globOptions: {
// 忽略文件
ignore: ["**/index.html"],
},
info: {
// 跳过terser压缩js
minimized: true,
},
},
],
}),
isProduction &&
new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash:10].css",
chunkFilename: "static/css/[name].[contenthash:10].chunk.css",
}),
!isProduction && new ReactRefreshWebpackPlugin(),
].filter(Boolean),
optimization: {
minimize: isProduction,
// 压缩的操作
minimizer: [
// 压缩css
new CssMinimizerPlugin(),
// 压缩js
new TerserWebpackPlugin(),
// 压缩图片
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: [
["gifsicle", { interlaced: true }],
["jpegtran", { progressive: true }],
["optipng", { optimizationLevel: 5 }],
[
"svgo",
{
plugins: [
"preset-default",
"prefixIds",
{
name: "sortAttrs",
params: {
xmlnsOrder: "alphabetical",
},
},
],
},
],
],
},
},
}),
],
// 代码分割配置
splitChunks: {
chunks: "all",
// 其他都用默认值
},
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`,
},
},
resolve: {
extensions: [".jsx", ".js", ".json"],
},
devServer: {
open: true,
host: "localhost",
port: 3000,
hot: true,
compress: true,
historyApiFallback: true,
},
mode: isProduction ? "production" : "development",
devtool: isProduction ? "source-map" : "cheap-module-source-map",
};

devServer不需要受环境变量影响,因为在执行webpack serve时才会读取它的配置。

一些小优化

在react项目中使用的较多的组件库就是ant-design,而在使用antd时配置自定义主题需要webpack的配置,所以需要在获取样式loader的公共方法中判断是否为less-loader(antd中为less),然后配置自定义主题色即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const getStyleLoaders = (preProcessor) => {
return [
isProduction ? MiniCssExtractPlugin.loader : "style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env",
],
},
},
},
preProcessor && {
loader: preProcessor,
options:
preProcessor === "less-loader"
? {
// antd的自定义主题
lessOptions: {
modifyVars: {
// 其他主题色:https://ant.design/docs/react/customize-theme-cn
"@primary-color": "#1DA57A", // 全局主色
},
javascriptEnabled: true,
},
}
: {},
},
].filter(Boolean);
};

在打包之后,由node_modules打包出来的单个js文件体积过于庞大,请求时耗时。所以我们要将node_modules中的依赖包按照react、antd和其他库的方式分割开,减小单个的js文件的大小。

在optimization配置项中加入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
splitChunks: {
chunks: "all",
cacheGroups: {
// layouts通常是admin项目的主体布局组件,所有路由组件
// 可以单独打包,从而复用
// 如果项目中没有,请删除
layouts: {
name: "layouts",
test: path.resolve(__dirname, "../src/layouts"),
priority: 40,
},
// 如果项目中使用antd,此时将所有node_modules打包在一
// 所以我们将node_modules中比较大的模块单独打包,从而
// 如果项目中没有,请删除
antd: {
name: "chunk-antd",
test: /[\\/]node_modules[\\/]antd(.*)/,
priority: 30,
},
// 将react相关的库单独打包,减少node_modules的chunk体
react: {
name: "react",
test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
chunks: "initial",
priority: 20,
},
libs: {
name: "chunk-libs",
test: /[\\/]node_modules[\\/]/,
priority: 10, // 权重最低,优先考虑前面内容
chunks: "initial",
},
},
},

这样一个简单的React脚手架就搭建完毕了