Skip to main content

webpack5从0搭建react SPA

如果从 0 创建一个实际的单页面应用的话,还是比较推荐 vite。不过我之前接触的大部分都是基于 create-react-app 搭建的项目,就想着现在基于 webpack5 从 0 开始搭建一个 react 项目需要做哪些事?是不是可以用上一些比较新的 loader 比如 swc-loader?于是带着这些疑问就开始折腾了

其实 webpack 干的事就是将多个模块(例如 js 文件、css 文件、图片等)按照指定的规则打包成一个或多个文件,然后支持多种模块化方案,比如 ES Module、CommonJS、AMD 等,并且有比较大的插件生态,可以帮助我们高效地开发项目。官网中文文档 还是很详细的。以下长文警告...

一些准备工作

  • Node 环境
    • 通过 fnm 管理 node 版本,可以参考 fnm 使用
    • Node.js 基于 latest
  • 包管理用 pnpm,安装方式 npm i -g pnpm
  • React 基于 v18

初始化

新建个目录,然后通过 npm init初始化项目,之后会生成 package.json常见字段解释戳这个

接下来目标是先搭建一个能展示 html 的 demo,内容就简单输出一句 "hello my-cra"

先新建 public/index.html,后面这个 html 就作为模板了,然后建个 src/index.js 作为入口文件

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My CRA</title>
</head>
<body>
<div>hello my-cra</div>
</body>
</html>

server

那首先得有个静态服务返回 html 文档,需要安装以下库:

pnpm install webpack-dev-server webpack-cli webpack -D
  • webpack-cli是 webpack 的命令行接口,和 webpack 进行交互,借助它我们可以通过命令行来运行 webpack,或者运行 webpack-dev-server 来创建一个开发服务器
  • webpack-dev-server就是基于 express 起个静态服务,当然还提供了热模块替换(Hot Module Replacement)和实时重载(Live Reloading)的功能

新建 webpack 配置文件 webpack.config.js,内容如下:

const path = require("path");
const isProduction = process.env.NODE_ENV === "production";

const config = {
devServer: {
static: {
directory: path.join(__dirname, "public"),
},
port: 8000,
},
};

module.exports = () => {
if (isProduction) {
config.mode = "production";
} else {
config.mode = "development";
}
return config;
};

增加服务启动命令,修改 package.json

"scripts": {
"serve": "webpack serve",
}

执行 pnpm run serve,访问 8000 页面,诶,第一步不就迈出来了

html 注入 js 文件

html 模板一般是空的,需要解析 js 文件后再渲染到 root 节点上,这里定义 root 节点的 id 为 root,还需要一个入口文件 src/index.js,顺便把 html 模板挪到 src 下吧 src/index.html

注入模板就需要用到 HtmlWebpackPlugin ,这个插件作用是在构建时将 js、css 等文件插入到 html 模板中,然后生成最终的 html 文件

# 安装
pnpm install html-webpack-plugin -D

修改配置文件 webpack.config.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

const config = {
// 入口文件
entry: "./src/index.js",
// 产物配置
output: {
path: path.resolve(__dirname, "./dist"),
filename: "index_bundle.js",
},
// 插件
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
filename: "index.html",
}),
],
// ...
};

html 模板也要修改下

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My CRA</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

ok,重新 pnpm run serve 看下效果

代码转换

有经验的同学应该知道,webpack 构建相对耗时的两个部分是代码转换和代码压缩

swc-loader

我们一般需要通过 babel 编译 js,将现代 ES6+ 语法和特性转换成向后兼容的语法,确保能够运行在当前和旧版本的浏览器或其他环境中。但其实主流浏览器已经能支持 ESModule 和绝大部分语法了,这也是 vite 这些构建工具流行的主要原因

这里使用 swc-loader 替换 babel-loader

 // webpack.config.js
const config = {
// ...
module: {
rules: [
{
test: /.m?js$/,
exclude: /(node_modules)/,
use: {
loader: "swc-loader",
options: {
jsc: {
parser: {
syntax: "ecmascript",
tsx: false,
decorators: true,
},
transform: {
legacyDecorator: true,
},
// 以下配置需要先安装 @swc/helpers
externalHelpers: true,
target: "es5",
},
isModule: "unknown",
},
},
},
],
},

写个 demo 转换下试试

// src/index.js
const root = document.getElementById("root");
root.textContent = "hello my-cra";
export let a = 1;
export const map = new Map();
map.set("name", "Lucas");
const fn = () => {
console.log("===fn", a, map, x);
};
console.log(fn);

执行 pnpm run build,生成的 js 文件如下

(() => {
"use strict";
var e = document.getElementById("root");
e && (e.textContent = "hello my-cra");
var o = new Map();
o.set("name", "Lucas"),
console.log(function () {
console.log("===fn", 1, o, x);
});
})();

typescript

我们可以借助 swc 编译 ts 代码,这样就不需要引入额外的 ts 编译器

修改 swc-loader 的配置

// ...
module: {
rules: [
{
// 匹配 ts 和 js 文件
test: /.[tj]s$/,
exclude: /(node_modules)/,
use: {
loader: "swc-loader",
options: {
jsc: {
parser: {
// 修改
syntax: 'typescript',
// 支持装饰器
decorators: true,
},
transform: {
legacyDecorator: true,
react: {
// NOTE:在转换React代码时,SWC将自动引入运行时代码,确保jsx语法能正常编译
runtime: 'automatic',
},
},
// ...
},
// ...
},
},
},
],
},

增加 typescript 的配置文件 tsconfig.json详细参数解释戳这里

// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

src/index.js 改为 src/index.ts

enum EDirection {
None,
Left,
Right,
}
const root = document.getElementById("root");
root.textContent = "hello my-cra";
export let a: number = 1;
export const map = new Map();
map.set("name", "Lucas");
const fn = () => {
console.log("===fn", a, map, EDirection.Left);
};
console.log(fn);

执行 pnpm run build 打包生成文件如下:

(() => {
"use strict";
var e;
(function (e) {
// 双向map
(e[(e.None = 0)] = "None"),
(e[(e.Left = 1)] = "Left"),
(e[(e.Right = 2)] = "Right");
})(e || (e = {})),
(document.getElementById("root").textContent = "hello my-cra");
var t = new Map();
t.set("name", "Lucas"),
console.log(function () {
console.log("===fn", 1, t, 1);
});
})();

React

swc 也支持编译 jsx,加下配置就行

pnpm install react react-dom
pnpm install @types/react @types/react-dom -D

修改配置

// ...
module: {
rules: [
{
test: /.[tj]sx?$/,
exclude: /(node_modules)/,
use: {
loader: "swc-loader",
options: {
jsc: {
parser: {
syntax: "typescript",
decorators: true,
// 新增
tsx: true,
},
// ...
},
// ...
},
},
},
],
},

修改 index.tsx

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

新增 App.tsx

const App = () => {
return <div>hello my-cra</div>;
};

export default App;

这里其实还有个问题,我在导入 App 这个组件时是这样导入的

import App from "./App.tsx";

但实际项目中我们都是直接写 "./App"。这个要怎么做?其实加个 resolve 的配置就行了

const config = {
// ...
resolve: {
// 按照数组顺序解析文件
extensions: [".tsx", ".ts", ".jsx", ".js"],
},
// ...
};

注意:如果你的项目依赖特定的 Babel 插件或者比较复杂,那么使用 SWC 前可能还需要经过比较详细的测试,也就需要在成本投入和性能之间做个衡量

CSS 管理

接下来考虑加下样式。怎么让下面这一句生效?

import "./App.css";

这个时候就需要将样式注入 html 文件了,需要用到一些 loaders

  • style-loader。负责将 CSS 注入到 DOM 中,可以选择 style 标签(默认)或者通过 link 标签引入
  • css-loader。负责解析 CSS 文件中的 @importurl() 引用,并将它们转换为模块依赖
pnpm install css-loader style-loader -D

修改 webpack 配置如下:

module: {
rules: [
// ...
{
test: /.css$/i,
use: ["style-loader", "css-loader"],
},
];
}

新增 App.css

.app {
padding: 16px;
font-size: 50px;
}

然后在 App.tsx 引入后刷新页面看下效果

import "./App.css";
const App = () => {
return <div className="app">hello my-cra</div>;
};
export default App;

预处理

使用less作为 css 预处理语言,需要借助less-loader

pnpm install less less-loader -D

修改 webpack 配置如下:

module: {
rules: [
// ...
{
test: /.(less|css)$/i,
use: ["style-loader", "css-loader", "less-loader"],
},
];
}

改为 App.less

@textColor: red;

.app {
padding: 16px;

.text {
color: @textColor;
font-size: 50px;
}
}

然后在 App.tsx 引入后刷新页面看下效果

import "./App.less";

const App = () => {
return (
<div className="app">
<div className="text">hello my-cra</div>
</div>
);
};

export default App;

css module

css-loader 默认开启这个配置。但是实际开发过程,我们往往需要留下类名作为前缀便于调试,这里设置为 类名+随机hash值取5位,所以需要改下 css-loader 的配置

module: {
rules: [
// ...
{
test: /(.module)?.less$/i,
use: [
"style-loader",
{
loader: require.resolve("css-loader"),
options: {
modules: {
auto: true,
localIdentName: "[local]_[hash:base64:5]",
},
},
},
"less-loader",
],
},
{
test: /(.module)?.css$/i,
use: [
"style-loader",
{
loader: require.resolve("css-loader"),
options: {
modules: {
auto: true,
localIdentName: "[local]_[hash:base64:5]",
},
},
},
],
},
];
}

修改 App.tsx

import styles from "./App.module.less";

const App = () => {
return (
<div className={styles["app"]}>
<div className={styles["text"]}>hello my-cra</div>
</div>
);
};

export default App;

postcss

需要借助 postcss-loader。有比较多好用的插件,比如 postcss-preset-envpostcss-px2remstylelint等。postcss-preset-env可以根据指定的目标浏览器或运行环境(在 package.json 中配置 browserslist),自动将现代的 css 特性转换为大多数浏览器能够理解的 css 代码,这其中就包括自动添加所需的浏览器前缀(这个插件内置了 autoprefixer

pnpm install postcss postcss-loader postcss-preset-env -D

修改 webpack 配置如下:

// ...
module: {
rules: [
// ...
{
test: /(.module)?.less$/i,
use: [
"style-loader",
{
loader: require.resolve("css-loader"),
options: {
modules: {
auto: true,
localIdentName: "[local]_[hash:base64:5]",
},
},
},
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: ["postcss-preset-env"],
},
},
},
"less-loader",
],
},
{
test: /(.module)?.css$/i,
use: [
"style-loader",
{
loader: require.resolve("css-loader"),
options: {
modules: {
auto: true,
localIdentName: "[local]_[hash:base64:5]",
},
},
},
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: ["postcss-preset-env"],
},
},
},
],
},
];
}

然后测试下

::placeholder {
color: #ccc;
}

正常的话,应该会输出以下内容:

::-moz-placeholder {
color: #ccc;
}
::placeholder {
color: #ccc;
}

分离 css

依赖 mini-css-extract-plugin 插件,一般在生产环境中使用,开发环境还是通过 style-loader 将样式注入 js 文件

pnpm i -D mini-css-extract-plugin@1.3.6

细心的同学应该发现了,这里固定了版本,因为大于这个版本就会和后面提到的耗时分析插件 speed-measure-webpack-plugin 有冲突了,详细内容参考 https://github.com/stephencookdev/speed-measure-webpack-plugin/issues/167。修改配置如下:

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// ...
{
test: /(.module)?.less$/i,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
{
loader: require.resolve('css-loader'),
options: {
modules: {
auto: true,
localIdentName: '[local]_[hash:base64:5]',
},
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['postcss-preset-env'],
},
},
},
'less-loader',
],
},
{
test: /(.module)?.css$/i,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
{
loader: require.resolve('css-loader'),
options: {
modules: {
auto: true,
localIdentName: '[local]_[hash:base64:5]',
},
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['postcss-preset-env'],
},
},
},
],
},
// ...
plugins: [
new MiniCssExtractPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
})
],

静态资源处理

webpack5 自带的资源处理器,和 webpack4 的处理方式有明显区别,可以参考 webpack5 升级小结

  • asset/resource 导出文件 URL(file-loader)
  • asset/inline 导出一个资源的 dataURI(url-loader)
  • asset 在导出一个 dataURI 和导出文件 URL 之间自动选择
  • asset/source 导出资源的源代码

处理图像

可以设定一个阈值,不超过这个阈值时就将图像转成 base64 内联到产物中,减少请求数

module: {
rules: [
// ...
{
test: /.(png|svg|jpg|jpeg|gif)$/i,
type: "asset",
parser: {
dataurlCondition: {
maxSize: 1024, // 单位是B
},
},
},
],
},

其他资源

下面是字体资源的例子,其他资源还有音频、视频、3d 模型等等,其实也是类似的处理

module: {
rules: [
// ...
{
test: /.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
],
},

devServer

热更新

webpack-dev-serverwebpack-dev-middleware 里默认开启 watch 模式,会自动重新编译文件,这个过程其实包括了整个应用的重新打包和加载,所以相对来说速度较慢。而热更新 HMR 是一种更高效的重新编译的方式,只替换局部模块,当然,webpack5 默认开启,配置方式如下:

// ...
devServer: {
static: {
directory: path.join(__dirname, "public"),
},
// 热更新
hot: true,
// 开启gzip压缩
compress: true,
// 指定静态服务的端口
port: 8000,
// 在默认浏览器自动打开页面
open: true
},

可以将 hot 设置为 false,前后对比一下,感受下局部刷新和全局刷新的区别(打开控制台 Elements 可以看到刷新效果)

代码规则约束

vscode 配置

新增 .vscode/settings.json,下面是设置了保存时自动格式化代码,然后格式化程序默认使用 prettier

{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}

eslint

记得给 vscode 安装 eslint 插件

pnpm install eslint -D
# 初始化,生成 .eslintrc.js
npx eslint --init

参考配置文件如下:

# .eslintrc.js
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
jest: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
],
overrides: [
{
env: {
node: true,
},
files: ['.eslintrc.{js,cjs}'],
parserOptions: {
sourceType: 'script',
},
},
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['@typescript-eslint', 'react'],
rules: {
// 允许使用require
'@typescript-eslint/no-var-requires': 0,
// 关掉react需要默认导入React的问题,在react>=17时需要
'react/react-in-jsx-scope': 'off',
'react/jsx-uses-react': 'off',
},
// 防止出现warning:React version not specified in eslint-plugin-react settings
settings: {
react: {
version: 'detect',
},
},
};

package.json 增加 script 命令,用于全局检查项目代码

"scripts": {
// ...
"eslint": "eslint --fix "src/**/*.{js,ts,jsx,tsx}"",
}

prettier

记得给 vscode 安装 prettier 插件

pnpm install prettier -D

新增配置文件 .prettierrc

{
"tabWidth": 2,
"singleQuote": true,
"semi": true,
"trailingComma": "all"
}

package.json 增加 script 命令,用于全局检查项目代码

"scripts": {
// ...
"prettier": "prettier --write "src/**/*.{js,ts,jsx,tsx,less,css}"",
}

stylelint

这个其实是 postcss 的插件,类似于 eslint,用于检查样式文件的语法,包括 css、less、sass 等。记得在 vscode 中安装 stylelint 插件

pnpm install stylelint stylelint-config-standard postcss-less -D

项目根目录下新增 .stylelintrc.js,配置参考如下:

module.exports = {
extends: "stylelint-config-standard",
customSyntax: "postcss-less",
};

增加 script 命令,便于全局检查样式文件:

"scripts": {
// ...
"stylelint": "stylelint --fix "src/**/*.{css,less}"",
}

但这个时候你会发现,保存 css 文件时并没有自动修复一些问题,需要开启 vscode 的配置项,在.vscode/settings.json中增加配置如下:

{
"editor.formatOnSave": true,
// 新增
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": true
},
"editor.defaultFormatter": "esbenp.prettier-vscode"
}

代码提交检查

需要以下几种插件的配合:

  • commitlint 插件可以校验提交的 commit 信息是否符合规范,参考官方文档,不符合的话可以限制不能提交
  • husky。操作 git 生命周期钩子的工具
  • lint-staged。本地暂存代码检查工具,可以让 husky 只检验 git 工作区的文件
pnpm install husky lint-staged @commitlint/cli @commitlint/config-conventional -D

还需要以下步骤:

  1. package.json 追加以下配置。prepare 其实在 pnpm install 前会自动执行
"scripts": {
// ...
"prepare": "husky",
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"
],
"src/**/*.{css,less}": [
"stylelint --fix"
],
},
  1. 执行 pnpm run prepare 初始化 husky
  2. 在根目录下创建 .commitlintrc.js
module.exports = {
extends: ["@commitlint/config-conventional"],
};
  1. 修改初始化生成的commit-msgpre-commit文件

commit-msg

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

echo "========= 校验 commit-msg ======="
pnpm commitlint --edit $1

pre-commit

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

echo "========= 执行 lint-staged ======="
pnpm lint-staged

ok,这样的话,在 git commit 之前会进入工作区文件的扫描,执行 prettier 脚本,修改 eslint 问题,校验 commit msg,通过后再提交到工作区。尝试提交下代码试试吧

editorConfig

使用不同编辑器打开同一份文件,如果编辑器配置不统一,显示效果和输入内容很有可能不一致。EditorConfig 就主要用于统一代码编辑器编码风格

.editorConfig 配置参考

# https://editorconfig.org

# 已经是顶层配置文件,不必继续向上搜索
root = true

[*]
# 编码字符集
charset = utf-8
# 缩进风格是空格
indent_style = space
# 一个缩进占用两个空格,因没有设置tab_with,一个Tab占用2列
indent_size = 2
# 换行符 lf
end_of_line = lf
# 文件以一个空白行结尾
insert_final_newline = true
# 去除行首的任意空白字符
trim_trailing_whitespace = true

[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

Biome(不建议)

基于 Rust 的代码检查和格式化工具,目前还不推荐实际项目中使用,不过有望替代 prettier 和 eslint,下面是使用方法,会和 eslint、prettier 冲突,慎重尝试。以下 lint 和 prettier 主要都是用推荐的配置

pnpm install -D -E @biomejs/biome
# 初始化配置文件
npx @biomejs/biome init

修改配置文件

{
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
"organizeImports": {
// 启用 import 语句排序优化
"enabled": true
},
"linter": {
"enabled": true,
// 启用规则校验
"rules": {
// recommended 表示使用推荐的规则
"recommended": true
}
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "tab",
"indentWidth": 2,
"lineWidth": 120
}
}

安装 vscode 插件 biome,然后将 Biome 设置为默认格式化程序(右键选择使用...格式化,可以配置默认值)

接着增加 .vscode/settings.json 文件

{
"editor.codeActionsOnSave": {
// 保存时自动调整 import 语句顺序
"source.organizeImports.biome": "explicit"
}
}

然后调整 import 语句顺序,保存后查看是否优化排序了,也就是 biome 是否生效了。也可以改引号、缩进、分号确认 formatter 是否生效

Biome 目前还不支持校验和格式化 css/vue 代码,所以这一块还需要借助 eslint 和 Prettier 来做,希望后面更完善后再考虑替换 eslint 和 prettier(包括对 ts 代码的规则完善)

构建分析

增加编译进度

一些中大型项目可能需要进度条来直观地查看进度,这里用到 progress-bar-webpack-plugin 插件

pnpm i -D progress-bar-webpack-plugin

修改 webpack 配置

// webpack.config.js
// chalk注意使用v4版本,否则会报错
const chalk = require('chalk');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
// ...
plugins: [
new ProgressBarPlugin({
format: ` :msg [:bar] ${chalk.green.bold(':percent')} (:elapsed s)`,
}),
// ...
],

progress-bar-webpack-plugin效果截图

耗时分析

主要是分析 loader 和 plugin 处理耗时,这里要借助 speed-measure-webpack-plugin 插件。不过这个插件好久没更新了,不太确定是否完整支持 webpack5

pnpm i -D speed-measure-webpack-plugin

修改配置

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();

const config = smp.wrap({
// ...webpack config
});

smp插件效果截图

产物体积分析

需要用到 webpack-bundle-analyzer

pnpm i -D webpack-bundle-analyzer

修改配置

plugins: [
// ...
new BundleAnalyzerPlugin({
// 设置为false,禁止自动打开分析页面
openAnalyzer: false,
}),
],

执行 npm run serve 后,可以在 localhost:8888 查看

构建优化

编译提速

  • 开启持久化缓存 cache,对二次构建有显著提速
const config = smp.wrap({
cache: {
type: "filesystem",
},
// ...
});

体积优化

js 代码压缩

借助 terser-webpack-plugin压缩 js 代码,可以开启 swc 压缩

pnpm i -D terser-webpack-plugin

修改 webpack 配置

const TerserPlugin = require("terser-webpack-plugin");

const config = smp.wrap({
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
minify: TerserPlugin.swcMinify,
}),
],
},
// ...
});

css 代码压缩

前提是分离 css 代码为单独的文件,然后可以借助 css-minimizer-webpack-plugin压缩代码

pnpm i -D css-minimizer-webpack-plugin

修改配置

optimization: {
minimize: true,
minimizer: [
new CssMinimizerPlugin(),
// ...
],
},

tree-shaking

只要使用 ES6 模块语法和将 mode 设置为 production,webpack5 就会自动启用 tree-shaking 功能

CSS 代码也可以做类似的操作,借助 purgecss-webpack-plugin 插件

pnpm i -D purgecss-webpack-plugin

修改配置

const { PurgeCSSPlugin } = require("purgecss-webpack-plugin");
const PATHS = {
src: path.join(__dirname, "src"),
};
// ...
plugins: [
new PurgeCSSPlugin({
paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
}),
// ...
];

code spliting

可以将多页面共用的代码单独抽成一个 chunk,这样可以减小一定的体积,然后可以按需加载或并行加载这些模块

const config = smp.wrap({
// ...
splitChunks: {
chunks: "all",
cacheGroups: {
vendors: {
test: /[\/]node_modules[\/]/,
chunks: "all",
priority: 10,
enforce: true,
},
},
},
});

其他

alias

修改 webpack resolve 配置

const config = smp.wrap({
// ...
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
extensions: [".tsx", ".ts", ".jsx", ".js"],
},
});

tsconfig 需要追加配置,确保路径提示正常

"compilerOptions": {
// ...
"paths": {
"@/*": ["./src/*"]
}
}

环境区分

借助 dotenv来区分不同环境,它能将环境变量中的变量从 .env 文件挂载到 process.env 对象上。大部分项目需要配置不同环境,按需加载不同的环境变量,使用 dotenv 就可以解决这一问题

pnpm install dotenv-cli -D

新建 config/env/.env.devconfig/env/.env.testconfig/env/.env.prod

# .env.dev
NAME = development
# .env.test
NAME = test
# .env.prod
NAME = production

修改 package.json 的 scripts 命令

"scripts": {
"serve": "dotenv -e ./config/env/.env.dev webpack serve",
"build": "dotenv -e ./config/env/.env.prod webpack",
"build:dev": "dotenv -e ./config/env/.env.dev webpack",
"build:test": "dotenv -e ./config/env/.env.test webpack",
"build:prod": "dotenv -e ./config/env/.env.prod webpack",
},

然后通过以下方式就可以读取了

// webpack.config.js
const envName = process.env.NAME;
console.log("===envName", envName);

sourceMap

参考 https://webpack.docschina.org/configuration/devtool/ 来选择更适合自己项目的,多数情况下,选择 eval-cheap-module-source-map就可以,线上环境其实也可以不用 sourcemap

const config = smp.wrap({
devtool: "eval-cheap-module-source-map",
// ...
});

最后

基于 webpack5 从 0 搭建 react 单页面应用的尝试,对比 create-react-app 应该是差蛮多的,还有些细节可以完善,后面有空研究对比看看,然后争取补个续集 ~