发布于 

前端项目工程化配置指南

Webpack5 + React18 + Ts 开发和打包环境

图源自原博客作者
图源自原博客作者

初始化项目

初始化package.json

初始化package.json文件

创建新项目的空目录文件webpack-react-ts,
在目录文件下打开cmd控制台,
执行初始化命令:

1
npm init -y

npm init命令介绍

初始化目录结构

手动创建目录结构
1
2
3
4
5
6
7
8
9
10
11
├── build 
| ├── webpack.base.js # 公共配置
| ├── webpack.dev.js # 开发环境配置
| └── webpack.prod.js # 打包环境配置
├── public
└── index.html # html模板
├── src
| ├── App.tsx # 根组件
└── index.tsx # react应用入口页面
├── tsconfig.json # ts配置
└── package.json

安装依赖

安装依赖

webpack依赖

1
npm i webpack webpack-cli -D

react依赖

1
npm i react react-dom -S

react类型依赖

1
npm i @types/react @types/react-dom -D

添加网页根html模版

添加网页根html模版 public/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>webpack-react-ts</title>
</head>
<body>
<!-- 容器节点 -->
<div id="root"></div>
</body>
</html>

配置tsconfig.json文件

配置tsconfig.json文件

配置指南

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
},
"include": ["./src"]
}

初始化src/App.tsx和src/index.tsx内容

初始化src/App.tsx和src/index.tsx内容

src/index.tsx是入口文件

1
2
3
4
5
6
7
8
import React from 'react'
import {createRoot} from 'react-dom/client'
import App from './App'

const root = document.getElementById('root')
if(root){
createRoot(root).render(<App/>)
}

src/App.tsx是根组件

1
2
3
4
5
6
7
import React from 'react';

function App(){
return <h2>Webpack React Ts</h2>
}

export default App

配置基础版React+ts环境

webpack公共环境

build/webpack.base.js
  • 配置入口文件 entry
  • 配置出口文件 output
  • 配置loader解析ts和jsx,需要安装依赖:
    • babel-loader
    • @babel/core
    • @babel/preset-react jsx解析器
    • @babel/preset-typescript ts解析器
1
npm i babel-loader @babel/core @babel/preset-react @babel/preset-typescript -D
  • 配置文件后缀 resolve
  • 将静态资源引入html文件,需要安装依赖html-webpack-plugin
1
npm i html-webpack-plugin -D
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
const path = require('path')
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// 1. 入口文件
entry: path.join(__dirname,'../src/index.tsx'),

// 2. 打包文件出口
output: {
filename: 'static/js/[name].js', // 每个输出js的名称
path: path.join(__dirname, '../dist'), // 打包结果
clean: true,
publicPath: '/', // 打包后文件的公共前缀路径
},

// 3. loader解析ts和jsx
module:{
rules:[
{
test: /.(ts|tsx)$/, // 匹配ts、tsx文件
use:{
loader: 'babel-loader',
options:{
// 预设执行从右向左
presets:[
'@babel/preset-react',
'@babel/preset-typescript'
]
}
}
}
]
},

// 4. 文件引用后缀配置
resolve: {
extensions: [".js", ".tsx", ",ts"]
},

// 5. 添加html插件
plugins: [
new HtmlWebpackPlugin({
template:path.resolve(__dirname, '../public/index.html'), // 设置模版
inject:true, // 是否自动注入
}),

]
}

webpack开发环境配置

build/webpack.dev.js

配置开发环境需要额外安装2个库:

  • webpack-dev-server 开发环境中启动服务器
  • webpack-merge 合并webpack.base.js基本配置
1
npm i webpack-dev-server webpack-merge -D

修改webpack.dev.js文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const path = require('path');
const {merge} = require('webpack-merge')
const baseConfig = require('./webpack.base')

// 合并公共配置,添加开发环境配置
module.exports = merge(baseConfig, {
mode: 'development', // 开发模式,加速打包
devtool: 'eval-cheap-module-source-map', // 源码调试结构
devServer:{ // 服务器配置
port: 3000, // 服务端口号
compress: false, // gzip压缩
hot: true, // 开启热更新
historyApiFallback: true, //
static:{
directory: path.join(__dirname, "../public") // 托管静态资源public文件夹
}
}
})

在package.json中添加dev脚本:

1
2
3
4
5
{
"scripts": {
"dev": "webpack-dev-server -c build/webpack.dev.js"
}
}

控制台执行dev启动命令:

1
npm run dev

webpack打包环境配置

build/webpack.prod.js

修改webpack.prod.js配置文件:

1
2
3
4
5
6
const {merge} = require('webpack-merge')
const baseConfig = require('./webpack.base')

module.exports = merge(baseConfig, {
mode:'production',// 开启生产环境
})

添加build脚本:

1
2
3
4
5
{
"scripts": {
"build": "webpack -c build/webpack.prod.js"
}
}

执行打包命令

1
npm run build

可以通过serve启动打包好的项目:

1
2
npm i serve -g
serve -s dist

基础功能配置

配置环境变量

环境变量分类
  • 按照模式
    • 开发环境
    • 生产环境
    • 区分方式:process.env.NODE_ENV
  • 按照项目业务环境
    • 开发环境
    • 测试环境
    • 预测环境
    • 正式环境
区分项目接口环境方式

设置一个环境变量 process.env.BASE_ENV

设置方式:

  • cross-env 兼容各系统设置环境变量的包
  • webpack.DefinePlugin
    • webpack内置,为业务代码注入环境变量

安装cross-env:

1
npm i cross-env -D

修改package.json里的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"scripts": {
"dev:dev": "cross-env NODE_ENV=development BASE_ENV=development webpack-dev-server -c build/webpack.dev.js",
"dev:test": "cross-env NODE_ENV=development BASE_ENV=test webpack-dev-server -c build/webpack.dev.js",
"dev:pre": "cross-env NODE_ENV=development BASE_ENV=pre webpack-dev-server -c build/webpack.dev.js",
"dev:prod": "cross-env NODE_ENV=development BASE_ENV=production webpack-dev-server -c build/webpack.dev.js",

"build:dev": "cross-env NODE_ENV=production BASE_ENV=development webpack -c build/webpack.prod.js",
"build:test": "cross-env NODE_ENV=production BASE_ENV=test webpack -c build/webpack.prod.js",
"build:pre": "cross-env NODE_ENV=production BASE_ENV=pre webpack -c build/webpack.prod.js",
"build:prod": "cross-env NODE_ENV=production BASE_ENV=production webpack -c build/webpack.prod.js"
}
}

在执行脚本时,会将用于判断环境的参数传入webpack的配置文件中,
webpack可以根据参数进行不同的配置:

1
2
process.env.NODE_ENV  // 模式参数
process.env.BASE_ENV // 业务参数

直到这一步,传入的参数只能在webpack配置文件中获取到,
要想在业务代码中用到参数,需要使用webpack.DefinePlugin,
将参数注入到业务代码中去:

1
2
3
4
5
6
7
8
const webpack = require('webpack');
module.exports = {
plugins:[
new webpack.DefinePlugin({
"process.env.BASE_ENV":JSON.stringify(progress.env.BASE_ENV)
})
]
}

css/less配置

如何解析css

对css的解析需要用到loader:

  • style-loader 把解析后的css代码抽离放到头部的style标签
  • css-loader 解析css文件
1
npm i style-loader css-loader -D

将loader配置到webpack.base.js中:

1
2
3
4
5
6
7
8
9
10
module.exports = {
module:{
rules:[
{ // 配置css文件
test: /\.css$/,
use:['style-loader', 'css-loader']
}
]
}
}

注意,loader会从右向左引入,这里的顺序不能改变,
因为需要先编译css代码,再将css部分嵌入到style里

less

需要用到2个包:

  • less-loader 把less编译为css
  • less 核心
1
npm i less less-loader -D

把less-loader加入到之前的css配置部分:

1
2
3
4
5
6
7
8
9
10
module.exports = {
module:{
rules:[
{ // 配置css/less文件
test: /\.(css|less)$/,
use:['style-loader', 'css-loader','less-loader']
}
]
}
}

处理css3前缀兼容性

在css3还没有普及的时候,
各家浏览器通过“实验属性”的方式对特性进行实现,
并在自家实现的属性前挂上浏览器的标识,
导致为了实现同一个属性,要写好几个版本

postcss就是为了解决这个问题而实现的,

虽然现在大部分的主流浏览器都没有这个问题,
但还是存在需要兼容低版本浏览器的情况存在,
这里就可以使用postcss-loader,

需要引入2个库:

  • postcss-loader 处理css的前缀
  • autoprefixer 决定需要兼容哪些浏览器
1
npm i postcss-loader autoprefixer -D

可以将postcss单独放到根目录里配置:
在根目录中添加postcss.config.js文件

1
2
3
module.exports = {
plugins: ['autoprefixer']
}

再添加autoprefixer配置文件.browserslistrc

1
2
IE 9 # 兼容IE 9
chrome 35 # 兼容chrome 35

然后将postcss-loader配置到webpack中

1
2
3
4
5
6
7
8
9
10
module.exports = {
module:{
rules:[
{ // 配置css/less文件
test: /\.(css|less)$/,
use:['style-loader', 'css-loader', 'postcss-loader', 'less-loader']
}
]
}
}

babel处理js兼容

babel可以用来转换js的版本,也能将不标准的js转换为标准的js,
需要安装下面的库:

  • babel-loader
    • 使用babel加载最新js并转换为ES5
  • @babel/core
    • babel编译的核心包
  • @babel/preset-env
    • 编译预设,转换目前最新的js标准语法
  • core-js
    • 使用低版本js语法模拟高版本的库,垫片
1
npm i babel-loader @babel/core @babel/preset-env core-js -D

在根目录添加babel.config.js,进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module.exports = {
// 处理顺序:
// 1. 处理ts
// 2. 处理jsx
// 3. babel转换
"presets":[
[
"@babel/preset-env",
{
//设置浏览器版本,优先使用.browserslistrc,
// "targets":{
// "ie": 9,
// "chrome": 35,
// },
"useBuiltIns": "usage", // 按需添加
"corejs": 3 // 版本
}
],
"@babel/preset-react",
"@babel/preset-typescript"
]
}

将babel添加到webpack配置中:

1
2
3
4
5
6
7
8
9
10
module.exports = {
module:{
rules:[
{
test: /.(ts|tsx)$/, // 匹配ts、tsx文件
use:"babel-loader"
}
]
}
}

复制public文件夹

public文件夹下一般放一些静态资源,比如图片、全局css等,
在dev模式下,可以通过托管配置,直接通过绝对路径访问到public文件下的资源:

1
2
3
4
5
6
7
module.exports = merge(baseConfig,{
devServer:{
static:{
directory: path.join(__dirname, "../public")
}
}
})

但在build模式下,需要将public文件内容直接复制到构建出口文件夹中

需要用到copy-webpack-plugin插件:

1
npm i copy-webpack-plugin -D

在webpack.prod.js中进行文件源、复制目标、过滤器等配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const path = require('path');
const {merge} = require('webpack-merge')
const baseConfig = require('./webpack.base')
const CopyPlugin = require('copy-webpack-plugin')
module.exports = merge(baseConfig, {
mode:'production',// 开启生产环境
plugins:[
// 复制文件插件
new CopyPlugin({
patterns:[
{
from: path.resolve(__dirname, '../public'),
to: path.resolve(__dirname, '../dist/public'),
filter: source => {
return !source.includes('index.html')
}
}
]
})
]
})

处理图片文件

使用asset-module进行处理,
在webpack.base.js中进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.exports = {
module:{
rules:[
{ // 配置图片文件
test: /.(png|jpg|jpeg|gif|svg)$/,
type: "asset",
parser:{
dataUrlCondition:{ // 小于10kb,转base64位
maxSize: 10 * 1024,
}
},
generator:{
filename:'static/images/[name][ext]' // 文件输出目录和命名
}
}
]
}
}

字体和媒体文件

字体和媒体文件与图片的配置基本相似:

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
module.exports = {
module:{
rules:[
{
test:'/.(woff2?|eot|ttf|otf)$/',
type:'asset',
parser:{
dataUrlCondition: {
maxSize: 10 * 1024,
}
},
generator:{
filename:'static/fonts/[name][ext]',
}
},
{
test:'/.(mp4|webm|ogg|mp3|wav|flac|aac)$/',
type:'asset',
parser:{
dataUrlCondition: {
maxSize: 10 * 1024,
}
},
generator:{
filename:'static/media/[name][ext]',
}
},
]
}
}

react热更新

使用webpack中,devServer.hot开启的热更新方式是直接刷新浏览器,
因此会导致数据状态的丢失,

要想不刷新浏览器,进行模块热更新,可以借助插件:

  • @pmmmwh/react-refresh-webpack-plugin
  • react-refresh
1
npm i @pmmmwh/react-refresh-webpack-plugin react-refresh -D

在webpack.dev.js中配置热更新插件:

1
2
3
4
5
6
7
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')

module.exports = merge(baseConfig,{
plugins:[
new ReactRefreshWebpackPlugin(), // 添加热更新插件
]
})

需要在babel.config.js中更新插件,
并且需要对环境进行判断,尽在开发环境下开启:

1
2
3
4
5
6
7
const isDEV = process.env.NODE_ENV === 'development';
module.exports = {
'plugins':[
isDEV && require.resolve('react-refresh/babel'),
].filter(Boolean), // 过滤空值
}

优化构建速度

构件耗时分析

需要借助到一个插件:

1
npm i speed-measure-webpack-plugin -D

构建分析配置文件webpack.analy.js:

1
2
3
4
5
6
7
8
9
10
const prodConfig = require('./webpack.prod')
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin() // 实例化分析插件
const {merge} = require('webpack-merge')

// 加入生产环境配置,和分析配置
module.exports = smp.wrap(merge(prodConfig,{

}))

添加package.json脚本并执行:

1
2
3
4
5
{
"scripts": {
"build:analy": "cross-env NODE_ENV=production BASE_ENV=production webpack -c build/webpack.analy.js"
}
}

控制台输出结果:

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
PS D:\webpack-react-ts> npm run build:analy
Active code page: 65001

> webpack-react-ts@1.0.0 build:analy
> cross-env NODE_ENV=production BASE_ENV=production webpack -c build/webpack.analy.js

(node:6120) [DEP_WEBPACK_COMPILATION_NORMAL_MODULE_LOADER_HOOK] DeprecationWarning: Compilation.hooks.normalModuleLoader was moved to NormalModule.getCompilationHooks(compilation).loader
(Use `node --trace-deprecation ...` to show where the warning was created)


SMP ⏱ # 总运行时间
General output time took 6.063 secs

SMP ⏱ Plugins # 插件运行时间
HtmlWebpackPlugin took 0.259 secs
CopyPlugin took 0.189 secs
DefinePlugin took 0.007 secs

SMP ⏱ Loaders # 解析器运行时间
babel-loader took 1.52 secs
module count = 2
css-loader, and
postcss-loader, and
less-loader took 1.27 secs
module count = 1
modules with no loaders took 0.885 secs
module count = 156
html-webpack-plugin took 0.014 secs
module count = 1
style-loader, and
css-loader, and
postcss-loader, and
less-loader took 0.012 secs
module count = 1



assets by path static/ 195 KiB
asset static/js/main.js 177 KiB [emitted] [minimized] (name: main) 1 related asset
asset static/images/toby_dog.jpg 18.5 KiB [compared for emit] [from: src/assets/imgs/toby_dog.jpg] (auxiliary name: main)
asset public/toby_dog.jpg 18.5 KiB [compared for emit] [from: public/toby_dog.jpg] [copied]
asset index.html 314 bytes [compared for emit]
runtime modules 1000 bytes 6 modules
orphan modules 5.59 KiB [orphan] 3 modules
modules by path ./node_modules/core-js/internals/*.js 80.4 KiB 121 modules
modules by path ./node_modules/core-js/modules/*.js 30 KiB 17 modules
modules by path ./node_modules/style-loader/dist/runtime/*.js 5.84 KiB 6 modules
modules by path ./src/ 6.37 KiB (javascript) 18.5 KiB (asset) 3 modules
modules by path ./node_modules/react-dom/ 131 KiB 3 modules
modules by path ./node_modules/react/ 6.95 KiB 2 modules
modules by path ./node_modules/scheduler/ 4.33 KiB
./node_modules/scheduler/index.js 198 bytes [built] [code generated]
./node_modules/scheduler/cjs/scheduler.production.min.js 4.14 KiB [built] [code generated]
modules by path ./node_modules/css-loader/dist/runtime/*.js 2.31 KiB
./node_modules/css-loader/dist/runtime/noSourceMaps.js 64 bytes [built] [code generated]
./node_modules/css-loader/dist/runtime/api.js 2.25 KiB [built] [code generated]
webpack 5.94.0 compiled successfully in 6066 ms

开启持久化存储缓存

webpack4中文件缓存:

  • 使用babel-loader缓存js解析结果
  • cache-loader缓存css等资源解析结果
  • 模块缓存插件hard-source-webpack-plugin
    • 二次打包时会对文件做哈希对比验证文件前后是否一致

webpack5对缓存策略进行优化:

  • 增加了持久化缓存
  • 改进了缓存算法

在webpack.base.js中配置开启缓存策略

1
2
3
4
5
module.exports = {
cache:{
type:'filesystem', // 使用文件缓存
}
}

缓存位置:node_modules/.cache/webpack

开启多线程loader

开启多线程loader

开启多线程loader可以借助多核cpu开启多线程loader解析,
需要安装依赖:

1
npm i thread-loader -D

使用时,需要将thread-loader放置在其他loader之前,
在它之后的loader会在一个独立的worker池中运行

开启多线程需要启动时间,适合规模较大的项目

配置alias别名

配置别名可以降低项目开发中资源引用的复杂度

webpack.base.js配置:

1
2
3
4
5
6
7
module.exports = {
resolve:{
alias:{
'@':path.join(__dirname, '../src')
}
}
}

tsconfig.json配置:

1
2
3
4
5
6
7
8
{
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
}
}

资源的路径调用:

1
import logo from '@/assets/imgs/logo.png'

缩小loader作用范围

include/exclude

可以通过2个属性,来对loader解析的范围进行限制:

  • include 只解析改配置的模块
  • exclude 不解析的模块

精确使用loader

loader会在webpack构建模块依赖关系引入新文件时执行,
通过test对文件名称进行正则匹配,
如果匹配到了,就从右向左依次执行use中的loader,

可以更精确的根据文件的后缀,区分需要执行的loader,
比如,可以将css和less拆分,
以减少css收到less-loader处理的这一步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
module:{
rules:[
{ // 配置css文件
include: [path.resolve(__dirname, '../src')],
test: /\.css$/,
use:['style-loader', 'css-loader', 'postcss-loader']
},
{ // 配置less文件
include: [path.resolve(__dirname, '../src')],
test: /\.less$/,
use:['style-loader', 'css-loader', 'postcss-loader', 'less-loader']
},
]
}
}

缩小模块搜索范围

Node中存在3中模块:

  • node核心模块
  • node_modules模块
  • 自定义文件模块

在项目中如果没有指定准确的路径,引入模块时,查找顺序如下:

  • 优先查询node核心模块
  • 当前目录下node_modules
  • 祖辈目录的node_modules
  • node全局模块

如果是追溯到了node全局模块的情况,开发环境和生产环境的node全局可能不一样,
因此要避免这种情况,将查找的范围局限在项目目录下,
在webpack.base.js中配置:

1
2
3
4
5
module.exports = {
resolve:{
modules: [path.resolve(__dirname, '../node_modules')],
}
}

devtool配置

开发调试时需要看到源代码,
生产模式下则是编译后的代码,
这就需要SourceMap进行转换,
devtool就是用来控制SourceMap的生成的:

webpack.dev.js

1
2
3
module.exports = merge(baseConfig, {
devtool: 'eval-cheap-module-source-map', // 源码调试结构
})

优化构建结果文件

webpack包分析工具

webpack-bundle-analyzer

需要借助库:

1
npm install webpack-bundle-analyzer -D

在webpack.analy.js中加入插件相关的配置:

1
2
3
4
5
6
7
8
9
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer')

// 加入生产环境配置,和分析配置
module.exports = smp.wrap(merge(prodConfig,{
plugins:[
new BundleAnalyzerPlugin()
]
}))

抽取css样式文件

css文件在不同的环境下有不同的优化策略:

  • 开发环境下,使用style标签嵌入方便热更新替换
  • 生产环境下,将css文件单独打包方便缓存策略

这里需要用到插件:

1
npm i mini-css-extract-plugin -D

在开发模式下使用style-loader,抽取css,
webpack.base.js:

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
const isDev = process.env.NODE_ENV === 'development'
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module:{
rules:[
{ // 配置css文件
include: [path.resolve(__dirname, '../src')],
test: /\.css$/,
use:[
// 开发环境使用style-loader, 打包模式抽取css
isDev?'style-loader':MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
},
{ // 配置less文件
include: [path.resolve(__dirname, '../src')],
test: /\.less$/,
use:[
isDev?'style-loader':MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'less-loader'
]
}
]
}
}

打包时,使用MiniCssExtractPlugin进行抽离:

1
2
3
4
5
6
7
8
9
10
11
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = merge(baseConfig, {
mode:'production',// 开启生产环境
plugins:[
// 抽离css插件
new MiniCssExtractPlugin({
filename: 'static/css/[name].css'
})
]
})

压缩css文件

css-minimizer-webpack-plugin

下载css压缩插件

1
npm i css-minimizer-webpack-plugin -D

在webpack.prod.js中进行配置:

1
2
3
4
5
6
7
8
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = merge(baseConfig, {
optimization:{
minimizer:[
new CssMinimizerPlugin(), // 压缩css
]
}
})

压缩js文件

安装terser-webpack-plugin插件:

1
npm i terser-webpack-plugin -D

修改webpack.prod.js配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const TerserPlugin = require('terser-webpack-plugin')
module.exports = merge(baseConfig, {
optimization:{
minimizer:[
new TerserPlugin({ // 压缩js
parallel: true, // 开启多线程压缩
terserOptions: {
compress:{
pure_funcs: ["console.log"] // 删除console.log
}
}
})
]
}
})

合理配置打包文件hash

hash是浏览器缓存策略中重要的一部分,
webpack提供三种hash:

  • hash
    • 全部文件共用相同的hash值
    • 项目文件只要修改,项目构建的hash值就会改变
  • chunkhash
    • 对不同的入口文件进行依赖解析,构建对应chunk,生成相应哈希值
    • 文件本身修改或者依赖文件修改,chunkhash会变化
  • contenthash
    • 每个文件都有自己的哈希值

配置hash值的格式如下:

1
filename: "[name].[chunkhash:8][ext]"
  • ext 文件后缀
  • name 文件名
  • path 文件相对路径
  • folder 文件夹
  • hash 每次构建生成的唯一hash值
  • chunkhash 根据chunk生成hash值
  • contenthash 根据文件内容生成hash值
    • 一般用于图片、css等资源文件的哈希值生成

在webpack.base.js中对生成的文件名称进行hash映射配置:

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
module.exports = {
// 出口文件
output: {
filename: 'static/js/[name].[chunkhash:8].js', // 每个输出js的名称
},
module:{
rules:[
{ // 配置图片文件
test: /.(png|jpg|jpeg|gif|svg)$/,
generator:{
filename:'static/images/[name].[contenthash:8][ext]' // 文件输出目录和命名
}
},
{
test:'/.(woff2?|eot|ttf|otf)$/',
generator:{
filename:'static/fonts/[name].[contenthash:8][ext]',
}
},
{
test:'/.(mp4|webm|ogg|mp3|wav|flac|aac)$/',
generator:{
filename:'static/media/[name].[contenthash:8][ext]',
}
},
]
}
}

在构建模式中,对被抽离出来css文件也进行hash映射:

1
2
3
4
5
6
7
module.exports = merge(baseConfig, {
plugins:[
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:8].css'
})
]
})

代码分割第三方包和公共模块

node_modules中的代码变化频率一般比较小,
可以单独打包,对应的chunkhash值很少变化。

同样的,还有一些公共模块,
由于很少变化,也可以单独分割出来。

这需要在webpack.prod.js中进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.exports = merge(baseConfig, {
optimization:{
// 分隔代码
splitChunks:{
cacheGroups:{
vendors:{ // 提取node_modules代码
test: /node_modules/, // 只匹配node_modules中的模块
name: 'vendors', // 提取文件命名为vendors, js后缀和chunkhash会自动加
minChunks: 1, // 只要使用一次就提取出来
chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
minSize: 0, // 目标代码体积大于0就提取出来
priority: 1, // 提取优先级为1
},
commons: { // 提取页面公共代码
name: 'commons', // 提取文件命名为commons
minChunks: 2, // 只要使用两次就提取出来
chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
minSize:0, // 目标代码体积大于0就提取出来
}
}
}
}
})

tree-shaking清理未使用的js/css

webpack内置tree-shaking,在进行prod打包时将没有使用的js清除掉

对于没有用到的css的清理,则需要额外安装库:

  • purgecss-webpack-plugin
  • glob-all 选择要检测那些文件里面的类型、id还有标签名称
1
npm i purgecss-webpack-plugin glob-all -D

在webpack.prod.js中进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const {PurgeCSSPlugin} = require('purgecss-webpack-plugin')
const globAll = require('glob-all')
module.exports = merge(baseConfig, {
plugins:[
// 清理无用css
new PurgeCSSPlugin({
// 检测src下所有tsx文件和public下index.html中使用到的类名、id、标签
//只打包这些文件中用到的样式
paths: globAll.sync([
`${path.join(__dirname,'../src')}/**/*.tsx`,
path.join(__dirname,'../public/index.html')
]),

// 自定义白名单
safelist:{
standard:[/^ant-/], // 过滤以ant-开头的类名
}
})
]
})

资源懒加载

react提供懒加载组件,
webpack默认支持资源懒加载,
资源在需要使用的时候进行动态加载

组件的懒加载:

1
2
3
4
5
6
7
8
9
import React, {lazy, Suspense, useState} from 'react';
const LazyDemo = lazy(()=>import('@/components/LazyDemo'))
function App(){
return (
<>
<Suspense fallback={null}><LazyDemo/></Suspense>
</>
)
}

资源预加载

关于link标签的ref属性:

  • preload
    • 资源必要,浏览器一定会加载
  • prefetch
    • 资源可能需要,浏览器空闲时加载

webpack中支持在import时,
使用注释进行预获取/预加载配置:

1
2
3
4
5
6
7
8
// 单个目标
import(
/* webpackChunkName: "my-chunk-name" */ // 资源打包后的文件chunkname
/* webpackPrefetch: true */ // 开启prefetch预获取
/* webpackPreload: true */ // 开启preload预加载

'./module'
)

打包时生成gzip文件

nginx可以通过配置gzip:on开启压缩,
但是会消耗服务器的资源,
因此可以在前端构建生产项目时,就对内容进行gzip压缩。

需要用到插件:

1
npm i compression-webpack-plugin -D

在webpack.prod.js中添加配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
const CompressionPlugin = require("compression-webpack-plugin");
module.exports = merge(baseCofig,{
plugins:[
// 打包压缩配置
new CompressionPlugin({
test: /.(js|css)$/, // 只生成css、js的压缩文件
filename:'[path][base].gz', // 文件命名
algorithm: 'gzip', // 压缩格式
threshold: 10240, // 最小压缩尺寸
minRatio: 0.8, // 压缩率
})
]
})

React + Ts 项目开发代码规范

参考博客

editorconfig统一编辑器配置

在VSCode编辑器里,添加editorconfig需要安装EditorConfig for VSCode插件

在项目根目录里添加.editorconfig配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
root = true # 控制配置文件 .editorconfig 是否生效的字段

[**] # 匹配全部文件
indent_style = space # 缩进风格,可选space|tab
indent_size = 2 # 缩进的空格数
charset = utf-8 # 设置字符集
trim_trailing_whitespace = true # 删除一行中的前后空格
insert_final_newline = true # 设为true表示使文件以一个空白行结尾
end_of_line = lf

[**.md] # 匹配md文件
trim_trailing_whitespace = false

editorconfig一般用于规范项目中的缩进风格

Prettier

在IDE插件中安装Prettier

在项目根目录中添加.prettierrc.js配置文件,
用于对代码进行格式化处理

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
printWidth: 100, // 一行的字符数,如果超过会进行换行
tabWidth: 2, // 一个tab代表几个空格数,默认就是2
useTabs: false, // 是否启用tab取代空格符缩进,.editorconfig设置空格缩进,所以设置为false
semi: false, // 行尾是否使用分号,默认为true
singleQuote: true, // 字符串是否使用单引号
trailingComma: 'none', // 对象或数组末尾是否添加逗号 none| es5| all
jsxSingleQuote: true, // 在jsx里是否使用单引号,你看着办
bracketSpacing: true, // 对象大括号直接是否有空格,默认为true,效果:{ foo: bar }
arrowParens: "avoid", // 箭头函数如果只有一个参数则省略括号
}

在webstorm中,需要首先安装prettier包:

1
npm install --save-dev --save-exact prettier

在设置/prettier中,将prettier指向本地npm包里的prettier,开启run on save,
这样在保存项目时就会自动执行prettier格式化代码

eslint + lint-staged监测代码

项目安装eslint依赖

1
npm i eslint -D

配置.eslintrc.js文件,使用快捷命令:

1
npm init @eslint/config

根据控制台问题的回答情况,最终会生成一个eslint.config.js文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";


export default [
{files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"]},
{languageOptions: { globals: globals.browser }},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
];