1、初始化项目
首先手动新建一个项目文件夹 webpack-react-ts,然后在项目文件夹下执行
npm init -y
初始化 package.json 后,在项目目录下新增如下目录结构和文件
├── config
| ├── webpack.common.js # 公共配置
| ├── webpack.dev.js # 开发环境配置
| └── webpack.prod.js # 打包环境配置
├── public
│ └── index.html # html模板
├── src
| ├── App.tsx
│ └── index.tsx # react应用入口页面
├── tsconfig.json # ts配置
├── .browserslistrc # 浏览器适配信息
├── .prettierrc.js # 代码格式化配置
├── typing.d.ts # 定义ts文件
└── package.json
安装 webpack 依赖
npm i webpack webpack-cli -D
安装 react 依赖
npm i react react-dom -S
安装 react 类型依赖
npm i @types/react @types/react-dom -D
添加 public/index.html 内容
<!DOCTYPE html>
<html lang="">
<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" />
<!--htmlWebpackPlugin.options.title 这暂时不用管,后面会讲到 -->
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
</body>
</html>
添加 tsconfig.json 内容
{
"compilerOptions": {
"experimentalDecorators": true,
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["typing.d.ts", "./src",]
}
添加 .browserslistrc 内容
Browserslist是一个在不同的前端工具之间,共享目标浏览器和Node.js版本的配置
可以在项目根目录下新建 .borwserslistrc 文件,然后Browserslist工具就会根据这个条件查询需要适配的浏览器, 需要做适配的都会更具这个查出来的浏览器版本进行适配,比如postcss等等
>1%
last 2 version
not dead
ie 11
添加 .prettierrc.js 内容
module.exports = {
printWidth: 130, // 一行的字符数,如果超过会进行换行
tabWidth: 2, // 一个tab代表几个空格数,默认就是2
useTabs: false, // 是否启用tab取代空格符缩进,.editorconfig设置空格缩进,所以设置为false
semi: true, // 行尾是否使用分号,默认为true
// singleQuote: true, // 字符串是否使用单引号
trailingComma: "all", // 对象或数组末尾是否添加逗号 none| es5| all
// jsxSingleQuote: true, // 在jsx里是否使用单引号,你看着办
bracketSpacing: true, // 对象大括号直接是否有空格,默认为true,效果:{ foo: bar }
arrowParens: "avoid", // 箭头函数如果只有一个参数则省略括号
};
添加 src/App.tsx 内容
import React from 'react'
function App() {
return <h2>webpack5-react-ts</h2>
}
export default App
添加 src/index.tsx 内容
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 />)
}
2、配置 React + TS 环境
修改 webpack.common.js 文件 ,添加如下内容。
这里是 webpack 打包执行的入口文件,开发环境和生产环境的配置都会在这个文件内合并。这里也包含了一些公共配置
const { merge } = require('webpack-merge');
const prodConfig = require('./webpack.prod');
const devConfig = require('./webpack.dev');
const commonConfig = isProduction => {
return {};
};
module.exports = function (env) {
const isProduction = env.production;
process.env.NODE_ENV = isProduction ? 'production' : 'development';
const config = isProduction ? prodConfig : devConfig;
const mergeConfig = merge(commonConfig(isProduction), config);
return mergeConfig;
};
需要安装 webpack-merge
npm i webpack-merge -D
给 webpack.dev.js添加如下内容
const path = require('path');
module.exports = {}
给 webpack.prod.js 添加如下内容
const path = require('path');
module.exports = {}
2.1 webpack 公共配置
配置入口文件
修改 webpack.common.js 文件
const commonConfig = isProduction => {
return {
entry: path.resolve(process.cwd(), './src/index.tsx'), // ,
};
};
配置出口文件
修改 webpack.common.js 文件 ,添加 output 选项内容
const commonConfig = isProduction => {
return {
// ...
output: {
filename: 'static/js/[name].[chunkhash:8].build.js', // 每个输出js的名称
path: path.resolve(process.cwd(), './dist'), // 打包结果输出路径
clean: true, // webpack4需要配置clean-webpack-plugin来删除dist文件,webpack5内置了
publicPath: '/' // 打包后文件的公共前缀路径
},
};
};
配置 loader 解析 ts 和 tsx
由于 webpack 默认只能识别 js 文件,不能识别 jsx 语法,需要配置 loader 解析。
@babel/preset-typescript来先ts语法转换为 js 语法 ,再借助预设 @babel/preset-react来识别jsx语法
安装 babel 核心模块 和 babel 预设
npm i @babel/preset-react @babel/preset-typescript -D
修改 webpack.common.js文件
const commonConfig = isProduction => {
return {
// ...
module: {
rules: [
{
test: /\.(js|ts)x?$/,
include: [path.resolve(process.cwd(), "./src")], // 只对项目src文件的ts,tsx进行loader解析
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
],
},
};
};
在项目根目录下新建 babel.config.js 文件,将 babel 配置放在这里统一管理,写上如下预设代码
const presets = [
["@babel/preset-react"],
["@babel/preset-typescript"],
];
module.exports = {
presets,
};
配置 resolve
extensions 配置文件名简写
extensions 是 webpack 的 resolve 解析配置下的选项,在引入模块时不带文件后缀时,会来该配置数组里面依次添加后缀查找文件,因为ts不支持引入以 .ts, tsx为后缀的文件,所以要在 extensions 中配置,而第三方库里面很多引入js文件没有带后缀,所以也要配置下js
修改 webpack.common.js文件,注意把高频出现的文件后缀放在前面
const commonConfig = isProduction => {
return {
// ...
resolve: {
extensions: [".tsx", ".jsx", ".ts", ".js", ".json", ".wasm", ".mjs"]
}
};
};
alias 配置别名
webpack 支持设置别名,设置别名的可以让引用的地方减少路径复杂度
修改 webpack.common.js文件
const commonConfig = isProduction => {
return {
// ...
resolve: {
alias: {
"@": path.resolve(process.cwd(), "./src"),
},
}
};
修改 tsconfig.json, 添加baseUrl paths
{
"compilerOptions": {
// ...
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
}
添加 html-webpack-plugin插件
webpack 需要把最终构建好的静态资源都引入到 html 中,这样才能在浏览器中运行。html-webpack-plugin 就是做这件事的,自动将打包好的文件引入html 文件中
安装依赖
npm i html-webpack-plugin -D
修改 webpack.common.js文件
const HtmlWebpackPlugin = require('html-webpack-plugin'); // html模板的plugin
const commonConfig = isProduction => {
return {
// ...
plugins: [
new HtmlWebpackPlugin({
title: "webpack-react-ts", // 模板文件里有用到title这个变量,将这里的变量名字注入到模板文件中
template: path.resolve(process.cwd(), "./public/index.html"), // 模板文件的地址
inject: true, // 自动注入静态资源
}),
],
};
};
2.2 webpack 开发环境配置
安装 webpack-dev-server
开发环境代码配置在 webpck-dev.js 中,需要借助 webpack-dev-server 在开发环境中启动服务器辅助开发。
安装依赖
npm i webpack-dev-server -D
修改 webpack.dev.js 文件
const path = require('path');
module.exports = {
mode: 'development',
devtool: 'inline-source-map',
devServer: {
hot: true, // 热更新HMR 后面会讲到react热替换的具体配置
//hotOnly是当代码编译失败时,是否刷新整个页面,如果不希望重新刷新整个页面,可以设置hotOnly为true
// hotOnly: true,
host: '0.0.0.0', // 设置成0.0.0.0可以通过ip4地址访问到
port: 8000, //port设置监听的端口,默认情况下是8080
open: true, // 自动打开浏览器
compress: false, // 是否开启压缩 gzip压缩,开发环境不开启,提升热更新速度
proxy: {
'/api': {
// 表示的是代理到的目标地址
target: 'http://123.207.32.32:8000',
//默认情况下,我们的 /api 也会被写入到URL中,如果希望删除,可以使用pathRewrite
pathRewrite: {
'^/api': '',
},
//默认情况下不接收转发到https的服务器上,如果希望支持,可以设置为false
secure: false,
// 它表示是否更新代理后请求的headers中host地址【不太明白】
changeOrigin: true,
},
},
//historyApiFallback是开发中一个非常常见的属性,它主要的作用是解决SPA页面在路由跳转之后,进行页面刷新时,返回404的错误
historyApiFallback: true,
static: {
directory: path.resolve(process.cwd(), './public'), //托管静态资源public文件夹
},
}
};
package.json 添加 dev 脚本
// package.json
"scripts": {
"start": "webpack server --config ./config/webpack.common.js --env development"
},
运行 以下命令可以在浏览器看到页面了
npm run start
2.3 配置打包环境
修改 webpack.prod.js 的代码
const path = require("path");
module.exports = {
mode: "production",// 生产模式,会开启tree-shaking和压缩代码,以及其他优化
};
package.json 添加 build 打包命令脚本
"scripts": {
"build": "webpack --config ./config/webpack.common.js --env production"
},
执行 npm run build ,最终打包在 dist 文件夹中
浏览器查看打包结果
安装 http-server
npm install --global http-server
然后在项目根目录下执行以下命令就可以启动打包后的项目了
http-server ./dist
到现在一个基础的支持react和ts的webpack5就配置好了,但只有这些功能是远远不够的,还需要继续添加其他配置。
3、基础功能配置
3.1 配置环境变量
环境变量按照作用来分为两种
- 区分是开发模式还是打包构建模式,可以用
process.env.NODE_ENV,因为很多第三方包都用这个 - 区分项目业务环境开发、预发布、正式环境。可以用
process.env.BASE_ENV
设置环境变量可以用 webpack 自带的--env 指令设置环境变量,然后用webpack.DefinePlugin这个内置插件为业务代码注入环境变量
修改 package.json 的 scripts 脚本为
"scripts": {
"dev:dev": "webpack server --config ./config/webpack.common.js --env development BASE_ENV=dev",
"dev:pre": "webpack server --config ./config/webpack.common.js --env development BASE_ENV=pre",
"dev:prod": "webpack server --config ./config/webpack.common.js --env development BASE_ENV=prod",
"start": "webpack server --config ./config/webpack.common.js --env development BASE_ENV=dev",
"build:dev": "webpack --config ./config/webpack.common.js --env production BASE_ENV=dev",
"build:pre": "webpack --config ./config/webpack.common.js --env production BASE_ENV=pre",
"build:prod": "webpack --config ./config/webpack.common.js --env production BASE_ENV=prod"
},
- developent/production 区分是开发模式还是打包构建模式
- BASE_ENV=dev/BASE_ENV=pre/BASE_ENV=prod 区分项目业务环境开发、预发布、正式环境
修改 webpack.common.js 文件,如下
module.exports = function (env) {
const isProduction = env.production;
// 设置区分开发模式还是构建模式
process.env.NODE_ENV = isProduction ? "production" : "development";
// 设置区分业务环境
process.env.BASE_ENV = env.BASE_ENV;
const config = isProduction ? prodConfig : devConfig;
const mergeConfig = merge(commonConfig(isProduction), config);
console.log({BASE_ENV:process.env.BASE_ENV,NODE_ENV:process.env.NODE_ENV})
return mergeConfig;
};
将当前的环境变量注入到业务代码,修改 webpack.common.js 文件,如下
const commonConfig = isProduction => {
return {
// ...
plugins: [
new DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.BASE_ENV': JSON.stringify(process.env.BASE_ENV)
})
]
}
}
在 src/index.tsx 下打印下这两个环境变量,检测是否注入到业务代码
// src/index.tsx
// ...
console.log('NODE_ENV', process.env.NODE_ENV)
console.log('BASE_ENV', process.env.BASE_ENV)
以上代码在 ts 环境中会报错,需要安装 @types/node 类型包
npm i @types/node -D
运行 npm run dev:dev 命令,然后在控制台查看是否有这两个变量
3.2 处理 css 和 less 文件
在 src 下新增 app.css
h2 {
color: red;
transform: translateY(100px);
}
在 src/App.tsx 中引入 app.css
import React from 'react'
import './app.css'
function App() {
return <h2>webpack5-rea11ct-ts</h2>
}
export default App
执行打包命令npm run build:dev,会发现有报错, 因为webpack默认只认识js,是不识别css文件的,需要使用loader来解析css, 安装依赖
npm i style-loader css-loader -D
- style-loader 把解析后的 css 从 js 中抽离,放到头部的 style 标签中
- css-loader 解析 css 文件
因为解析 css 的配置在开发和打包构建都需要用到,所以修改 webpack.common.js 文件
const commonConfig = isProduction => {
return {
// ...
module: {
rules: [
{
test: /\.css/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
// 如果在css文件中映入了其他css文件,则需要用这个属性表示引入的该文件需要回退两个loader处理,即用less-loader处理
importLoaders: 2,
},
}
],
}
]
}
}
}
loader执行顺序是从右往左,从下往上的,匹配到css文件后先用css-loader解析css, 最后借助style-loader把css插入到头部style标签中。
执行 npm run dev:dev 就可以看到样式生效了
3.3 支持 less 文件
项目开发中,为了更好的提升开发体验,一般会使用css超集less或者scss。对于这些超集也需要对应的loader来识别解析。这里选用 less 作为示范。需要安装依赖
npm i less-loader less -D
- less-loader: 解析 less 文件代码,把 less 编译成css
- less: less 的核心代码
修改 webpack.common.js 文件匹配 less 代码文件
const commonConfig = isProduction => {
return {
// ...
module: {
rules: [
{
test: /\.less/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
// 如果在css文件中映入了其他css文件,则需要用这个属性表示引入的该文件需要回退两个loader处理,即用less-loader处理
importLoaders: 1,
}
},
{
loader: 'less-loader'
}
]
}
]
}
};
};
新增 src/app.less 测试以下
#root {
h2 {
font-size: 20px;
margin-left: 100px;
}
}
在 App.tsx 中引入 app.less
import React from "react";
import './app.css'
import "./app.less"
function App() {
return <h2>webpack5-react-ts</h2>;
}
export default App;
运行 npm run dev:dev 看页面效果
3.4 postcss 处理css兼容
这个工具可以帮助我们进行一些CSS的转换和适配,比如自动添加浏览器前缀、css 样式的重置,但是实现这些工具,我们需要借助于 PostCSS 对应的插件,在 webpack 中使用 postcss 就是使用 postcss-loader 来处理的,然后使用 postcss-preset-env 这个插件处理 css,该插件默认内置了很多插件,比如:autoprefixer--添加前缀的插件
安装插件
npm install postcss-loader postcss-preset-env -D
在项目根目录下新建postcss.config.js文件,postcss-loader会读取这里的配置
module.exports = {
plugins: [
'postcss-preset-env'
]
}
在 webpack.common.js 中修改css less 兼容 css 处理,修改后如下
const commonConfig = isProduction => {
return {
// ...
module: {
rules: [
{
test: /\.css/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
// 如果在css文件中映入了其他css文件,则需要用这个属性表示引入的该文件需要回退两个loader处理,即用less-loader处理
importLoaders: 1,
},
},
{
loader: 'postcss-loader',
},
],
},
{
test: /\.less/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
// 如果在css文件中映入了其他css文件,则需要用这个属性表示引入的该文件需要回退两个loader处理,即用less-loader处理
importLoaders: 2,
}
},
{
loader: 'postcss-loader',
},
{
loader: 'less-loader'
}
]
}
]
}
};
};
配置完成后,需要有一份要兼容浏览器的清单,让postcss-loader知道要加哪些浏览器的前缀,之前新建的 .browserslistrc 文件就是这个作用
>1%
last 2 version
not dead
ie 11
运行 npm run dev:dev 查看 css 前缀是否加上
3.5 babel 预设处理 js 兼容
现在js不断新增很多方便好用的标准语法来方便开发,甚至还有非标准语法比如装饰器,都极大的提升了代码可读性和开发效率。但前者标准语法很多低版本浏览器不支持,后者非标准语法所有的浏览器都不支持。需要把最新的标准语法转换为低版本语法,把非标准语法转换为标准语法才能让浏览器识别解析,而babel就是来做这件事的
安装依赖
npm i babel-loader @babel/core @babel/preset-env core-js -D
- babel-loader: 使用 babel 加载最新js代码并将其转换为 ES5(上面已经安装过)
- @babel/corer: babel 编译的核心包
- @babel/preset-env: babel 编译的预设,可以转换目前最新的js标准语法
- core-js: 使用低版本js语法模拟高版本的库,也就是垫片
修改 babel.config.js 文件如下
const presets = [
[
"@babel/preset-env",
{
// 设置兼容目标浏览器版本,这里可以不写,babel-loader会自动寻找上面配置好的文件.browserslistrc
// "targets": {
// "chrome": 35,
// "ie": 9
// },
useBuiltIns: "usage", // 代码中需要哪些polyfill, 就引用相关的api
corejs: 3, // 还需要配置corejs的
},
],
["@babel/preset-react"],
["@babel/preset-typescript"],
];
module.exports = {
presets,
};
3.6 复制 public 文件夹
一般public文件夹都会放一些静态资源,可以直接根据绝对路径引入,比如图片,css,js文件等,不需要webpack进行解析,只需要打包的时候把public下内容复制到构建出口文件夹中,可以借助copy-webpack-plugin插件,安装依赖
npm i copy-webpack-plugin -D
开发环境已经在 devServer 中配置了 static 托管了 public 文件夹,在开发环境使用绝对路径可以访问到public 下的文件,但打包构建时不做处理会访问不到,所以现在需要在打包配置文件 webpack.prod.js 中新增 copy 插件配置
修改 webpack.prod.js文件如下
const CopyWebpackPlugin = require("copy-webpack-plugin"); // 复制文件的plugin
module.exports = {
// ...
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(process.cwd(), "./public"), // 复制public下文件
to: path.resolve(process.cwd(), "./dist"), // 复制到dist目录中
globOptions: {
// 表示需要忽略的文件,不用复制
ignore: ["**/index.html"],
},
},
],
}),
],
};
在 public 文件中新增一个 favicon.ico 图标文件,在 index.html 中引入测试
<!DOCTYPE html>
<html lang="">
<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" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="root"></div>
<!-- built files will be auto injected -->
</body>
</html>
执行 npm run build:dev打包,就可以看到favicon.ico 已经被复制到 dist 目录下了
然后 再运行 http-server ./dist查看 favicon.ico 是否显示正确
3.7 处理图片文件
对于图片文件, webpack4 使用 file-loader 和 url-loade r来处理的,但 webpack5 不使用这两个 loader了,而是采用自带的 asset-module 来处理
修改 webpack.common.js, 添加图片解析配置
module.exports = {
module: {
rules: [
// ...
{
test: /.(png|jpe?g|gif|svg)$/,
// type: "asset/resource", file-loader的效果直接复制
// type: "asset/inline", url-loader的效果直接转化为base64
// type: "asset/source", 导出资源的源代码。之前通过使用 raw-loader 实现
type: "asset", // 根据大小自动判断,例如一下配置了parser则根据这个大小是否转换成base64
generator: {
filename: "img/[name].[contenthash:6][ext]", // 自定义文件输出的名字和输出目录
},
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 小于10kb的图片转化为base64
},
},
}
]
}
}
测试一下,准备一张小于 10kb 和一张大于 10kb 的图片,放在 src/assets/imgs 目录下,修改App.tsx
import React from "react";
import smallImg from './assets/imgs/5kb.png'
import bigImg from './assets/imgs/22kb.png'
import './app.css'
import "./app.less"
function App() {
return <>
<h2>webpack5-react-ts</h2>;
<img src={smallImg} alt="小于10kb的图片" />
<img src={bigImg} alt="大于于10kb的图片" />
</>
}
export default App;
这时候会报错,找不到图片相应的类型声明,需要到 typing.d.ts 中增加图片类型声明
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';
declare module '*.less';
declare module '*.css';
修改 app.less
#root {
h2 {
font-size: 20px;
margin-left: 100px;
}
.smallImg {
width: 69px;
height: 75px;
background: url("@/assets/imgs/5kb.png") no-repeat;
}
.bigImg {
width: 232px;
height: 154px;
background: url("./assets/imgs/22kb.png") no-repeat;
}
}
运行 npm run start 和 npm run build:dev http-server ./dist分别查看效果
3.8 处理字体和媒体文件
字体文件和媒体文件这两种资源处理方式和处理图片是一样的,只需要把匹配的路径和打包后放置的路径修改一下就可以了。修改 webpack.common.js 文件:
// webpack.common.js
const commonConfig = isProduction =>{
module: {
rules: [
// ...
{
test:/.(woff2?|eot|ttf|otf)$/, // 匹配字体图标文件
type: "asset", // type选择asset
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 小于10kb转base64位
}
},
generator:{
filename:'static/fonts/[name].[contenthash:6][ext]', // 文件输出目录和命名
},
},
{
test:/.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件
type: "asset", // type选择asset
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 小于10kb转base64位
}
},
generator:{
filename:'static/media/[name].[contenthash:6][ext]', // 文件输出目录和命名
},
},
]
}
}
4、配置 react 模块热更新
借助 @pmmmwh/react-refresh-webpack-plugin 插件来实现,该插件又依赖于 react-refresh, 安装依赖
npm i @pmmmwh/react-refresh-webpack-plugin react-refresh -D
配置 react 热更新插件,修改 webpack.dev.js 文件
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
// ...
plugins: [new ReactRefreshWebpackPlugin()],
};
修改 babel.config.js 配置
const presets = [
[
"@babel/preset-env",
{
useBuiltIns: "usage", // 代码中需要哪些polyfill, 就引用相关的api
corejs: 3, // 还需要配置corejs的
},
],
["@babel/preset-react"],
["@babel/preset-typescript"],
];
const plugins = [];
const isProduction = process.env.NODE_ENV === "production";
// React HMR -> 模块的热替换 必然是在开发时才有效果
if (!isProduction) {
// console.log(isProduction, typeof isProduction, "---------");
plugins.push(["react-refresh/babel"]);
} else {
}
module.exports = {
presets,
plugins
};
修改 App.tsx 的内容
import React, { useState } from "react";
import smallImg from "./assets/imgs/5kb.png";
import bigImg from "./assets/imgs/22kb.png";
import "./app.css";
import "./app.less";
function App() {
const [count, setCounts] = useState("");
const onChange = (e: any) => {
setCounts(e.target.value);
};
return (
<>
<h2>webpack5-react-ts2</h2>
<img src={smallImg} alt="小于10kb的图片" />
<img src={bigImg} alt="大于于10kb的图片" />
<div className="smallImg"></div>
<div className="bigImg"></div>
<p>受控组件</p>
<input type="text" value={count} onChange={onChange} />
<p>非受控组件</p>
<input type="text" />
</>
);
}
export default App;
运行 npm run start 查看是否无刷新更新页面
5、优化构建速度
5.1 构建耗时分析
当进行优化的时候,肯定要先知道时间都花费在哪些步骤上了,而 speed-measure-webpack-plugin 插件可以帮我们做到,安装依赖
npm i speed-measure-webpack-plugin -D
为了不影响正常的开发/打包,新建一个 webpack.analy.js 分析耗时
const path = require("path");
const { merge } = require("webpack-merge");
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); // 引入webpack打包速度分析插件
const smp = new SpeedMeasurePlugin(); // 实例化分析插件
const { commonConfig } = require("./webpack.common");
const prodConfig = require("./webpack.prod");
const devConfig = require("./webpack.dev");
module.exports = function (env) {
const isProduction = env.production;
process.env.NODE_ENV = isProduction ? "production" : "development";
const config = isProduction ? prodConfig : devConfig;
const mergeConfig = merge(commonConfig(isProduction), config);
/**
* 分析各个loader、plugin的打包耗时,与MiniCssExtractPlugin有冲突
*/
return smp.wrap(mergeConfig);
// return mergeConfig;
};
同时需要修改 webpack.common.js 文件,在文件最末尾导出公共配置
module.exports.commonConfig = commonConfig;
在package.json 添加 scripts 脚本
"scripts": {
"dev:analy": "webpack --config ./config/webpack.analy.js --env development BASE_ENV=dev",
"build:analy": "webpack --config ./config/webpack.analy.js --env production BASE_ENV=prod"
},
执行 npm run build:analy 分析耗时
5.2 开启持久化缓存
webpack5 较于 webpack4,新增了持久化缓存、改进缓存算法等优化,通过配置 webpack 持久化缓存,来缓存生成的 webpack 模块和 chunk,改善下一次打包的构建速度,可提速 90% 左右,配置也简单,修改webpack.common.js
// webpack.common.js
// ...
const commonConfig = isProduction => {
// ...
cache: {
type: 'filesystem', // 使用文件缓存
},
}
通过开启持久化缓存,打包速度提神 80%
缓存的存储位置在node_modules/.cache/webpack
5.3 开启多线程loader
webpack 的 loader 默认在单线程执行,现代电脑一般都有多核 cpu,可以借助多核 cpu 开启多线程 loader解析,可以极大地提升 loader 解析的速度, thread-loader 就是用来开启多进程解析loader的,安装依赖
npm i thread-loader -D
使用时,需将此 loader 放置在其他 loader 之前。放置在此 loader 之后的 loader 会在一个独立的 worker 池中运行。
const commonConfig = isProduction => {
return {
module: {
rules: [
{
test: /\.(js|ts)x?$/,
include: [path.resolve(process.cwd(), "./src")], // 只对项目src文件的ts,tsx进行loader解析
exclude: /node_modules/,
use: [
{
loader: "thread-loader",
},
{
loader: "babel-loader"
}
]
}
]
}
}
};
5.4 缩小 loader 作用范围
一般第三库都是已经处理好的,不需要再次使用loader去解析,可以按照实际情况合理配置loader的作用范围,来减少不必要的loader解析,节省时间,通过使用 include和exclude 两个配置项,可以实现这个功能,常见的例如:
- include:只解析该选项配置的模块
- exclude:不解该选项配置的模块,优先级更高
const commonConfig = isProduction => {
return {
module: {
rules: [
{
test: /\.(js|ts)x?$/,
include: [path.resolve(process.cwd(), "./src")], // 只对项目src文件的ts,tsx进行loader解析
exclude: /node_modules/,
use: [
{
loader: "thread-loader",
},
{
loader: "babel-loader"
}
]
}
]
}
}
};
其他loader也是相同的配置方式,如果除src文件外也还有需要解析的,就把对应的目录地址加上就可以了,比如需要引入antd的css,可以把antd的文件目录路径添加解析css规则到include里面
5.5 缩小模块搜索范围
node 里面有三种模块
- node 核心模块
- node_modules 模块
- 自定义文件模块
使用require和import引入模块时如果有准确的相对或者绝对路径,就会去按路径查询,如果引入的模块没有路径。会按照以下顺序查找
- 优先查询node核心模块
- 如果没有找到会去当前目录下node_modules中寻找,
- 如果没有找到会查从父级文件夹查找node_modules,一直查到系统node全局模块
这样会有两个问题,一个是当前项目没有安装某个依赖,但是上一级目录下node_modules或者全局模块有安装,就也会引入成功,但是部署到服务器时可能就会找不到造成报错,另一个问题就是一级一级查询比较消耗时间。可以告诉webpack搜索目录范围,来规避这两个问题
const commonConfig = isProduction => {
return {
resolve: {
extensions: [".tsx", ".jsx", ".ts", ".js", ".json", ".wasm", ".mjs"],
alias: {
"@": path.resolve(process.cwd(), "./src"),
},
// 查找第三方模块只在本项目的node_modules中查找,提升模块查找的速度,提升构建速度
modules: [path.resolve(process.cwd(), "./node_modules")],
}
}
}
5.6 devtool 配置
开发过程中或者打包后的代码都是webpack处理后的代码,如果进行调试肯定希望看到源代码,而不是编译后的代码, source map就是用来做源码映射的,不同的映射模式会明显影响到构建和重新构建的速度, devtool选项就是webpack提供的选择源码映射方式的配置。
devtool的命名规则为 (inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map
- inline 代码内通过 dataUrl 形式引入 SourceMap
- hidden 生成 SourceMap 文件,但不使用
- eval eval(...) 形式执行代码,通过 dataUrl 形式引入 SourceMap
- nosources 不生成 SourceMap
- cheap 只需要定位到行信息,不需要列信息
- module 展示源代码中的错误位置
开发环境推荐 eval-cheap-module-source-map
修改 webpack.dev.js
module.exports = {
devtool: "eval-cheap-module-source-map",
};
打包环境推荐:none(就是不配置devtool选项了,不是配置devtool: 'none')
5.7 显示构建速度
webpackbar 插件可以在打包的时候显示编译进度,安装依赖
npm i webpackbar -D
修改 webpack.common.js 文件
const WebpackBar = require("webpackbar"); // 显示编译进度
const commonConfig = isProduction => {
return {
plugins: [
new WebpackBar({
name: isProduction ? "正在打包" : "正在启动",
})
};
};
运行 npm run start 可以在控制台看见编译进度了
6、优化构建结果文件
6.1 webpack 包分析工具
webpack-bundle-analyzer是分析webpack打包后文件的插件。通过该插件可以对打包后的文件进行观察和分析,可以方便我们对不完美的地方针对性的优化,安装依赖
npm install webpack-bundle-analyzer -D
修改 webpack.prod.js
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); // 打包分析
module.exports = {
plugins: [
new BundleAnalyzerPlugin(), // 打包分析
],
};
6.2 抽取css样式文件
在开发环境我们希望 css 嵌入在 style 标签里面,方便样式热替换,但打包时我们希望把 css 单独抽离出来,方便配置缓存策略。而插件 mini-css-extract-plugin 就是来帮我们做这件事的,安装依赖:
npm i mini-css-extract-plugin -D
修改 webpack.common.js ,根据环境变量设置开发环境使用 style-loader,打包环境抽离 css
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); // 将css单独提取到独立的文件中
const commonConfig = isProduction => {
return {
// ...
module: {
rules: [
{
test: /\.less/,
use: [
isProduction ? { loader: MiniCssExtractPlugin.loader } : { loader: "style-loader" },
{
loader: 'css-loader',
options: {
// 如果在css文件中映入了其他css文件,则需要用这个属性表示引入的该文件需要回退两个loader处理,即用less-loader处理
importLoaders: 1,
}
},
{
loader: 'less-loader'
}
]
}
]
}
};
};
再修改 webpack.prod.js 打包时候抽取css
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); // 将css提取到单独的文件
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash:8].css",
}),
],
};
配置完成后,开发模式会嵌入到 style 方便热气换,打包时候会抽离成单独的 css 文件
运行 npm run build:dev 验证 css 是否分离成单独文件
6.3 压缩 css 文件
上面配置了打包时把css抽离为单独css文件的配置,打开打包后的文件查看,可以看到默认css是没有压缩的,需要手动配置一下压缩css的插件。
可以借助css-minimizer-webpack-plugin来压缩css,安装依赖
npm i css-minimizer-webpack-plugin -D
修改 webpack.prod.js 文件,
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); // 对css代码进行压缩
module.exports = {
plugins: [
new CssMinimizerPlugin(), // 压缩css
],
};
再次执行 npm run build:prod 可以看到css 已经被压缩了
6.4 压缩 js 文件
项目维护的时候,一般只会修改一部分代码,可以合理配置文件缓存,来提升前端加载页面速度和减少服务器压力,而hash就是浏览器缓存策略很重要的一部分。webpack 打包的 hash 分三种:
- hash:跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的 hash 值
- chunkhash:不同的入口文件进行依赖文件解析、构建对应的chunk,生成对应的哈希值,文件本身修改或者依赖文件修改, chunkhash 值会变化
- contenthash:每个文件自己单独的 hash 值,文件的改动只会影响自身的 hash 值
hash是在输出文件时配置的,格式是 filename: "[name].[chunkhash:8][ext]",[xx] 格式是 webpack 提供的占位符, :8是生成hash的长度。
- ext 文件后缀名
- name 文件名
- path 文件相对路径
- folder 文件所在文件夹
- hash 每次构建生成的唯一 hash 值
- chunkhash 根据 chunk 生成 hash 值
- contenthash 根据文件内容生成hash 值
因为js我们在生产环境里会把一些公共库和程序入口文件区分开,单独打包构建,采用 chunkhash 的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响,可以继续使用浏览器缓存,所以 js 适合使用 chunkhash 。
css 和图片资源媒体资源一般都是单独存在的,可以采用 contenthash ,只有文件本身变化后会生成新 hash值。
之前的文件已经按照上面的 规则加了,这里不做过多示例,详情请查看上面的内容
6.5 代码分割第三方包和公共模块
一般第三方包的代码变化频率比较小,可以单独把 node_modules 中的代码单独打包, 当第三包代码没变化时,对应 chunkhash 值也不会变化,可以有效利用浏览器缓存,还有公共的模块也可以提取出来,避免重复打包加大代码整体体积, webpack 提供了代码分隔功能, 需要我们手动在优化项 optimization 中手动配置下代码分隔 splitChunks 规则。
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendors", // 提取文件命名为vendors,js后缀和chunkhash会自动加
minChunks: 1, // 只要使用一次就提取出来
chunks: "initial", // 只提取初始化就能获取到的模块,不管异步的
minSize: 0, // 提取代码体积大于0就提取出来
priority: 1, // 提取优先级为1
},
// react: {
// test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
// filename: "js/[id]_react.js",
// chunks: "all",
// },
commons: {
// 提取页面公共代码
name: "commons", // 提取文件命名为commons
minChunks: 2, // 只要使用两次就提取出来
chunks: "initial", // 只提取初始化就能获取到的模块,不管异步的
minSize: 0, // 提取代码体积大于0就提取出来
},
},
},
},
};
配置完成后,可以看到 node_modules 的模块已经被抽离到 vendors.90e0c56b.build.js了,而业务代码则被抽离到 main.714f6a4c.build.js
修改业务代码再打包,此时 vendors.90e0c56b.build.js 没有变化,变化的只是 main 文件,这样发版后,浏览器就可以继续使用缓存中的 vendors.90e0c56b.build.js ,只需要重新请求 main.js 就可以了。
6.6 tree-shaking清理未引用js
Tree Shaking 的意思就是摇树,伴随着摇树这个动作,树上的枯叶都会被摇晃下来,这里的 tree-shaking 在代码中摇掉的是未使用到的代码,也就是未引用的代码。模式 mode 为production时就会默认开启tree-shaking 功能以此来标记未引入代码然后移除掉,测试一下。
6.7 tree-shaking清理未使用css
js 中会有未使用到的代码, css 中也会有未被页面使用到的样式,可以通过 purgecss-webpack-plugin 插件打包的时候移除未使用到的 css 样式,这个插件是和 mini-css-extract-plugin 插件配合使用的,在上面已经安装过,还需要glob来选择要检测哪些文件里面的类名和id还有标签名称, 安装依赖:
npm i purgecss-webpack-plugin glob-all -D
修改 webpack.prod.js
插件本身也提供了一些白名单 safelist 属性,符合配置规则选择器都不会被删除掉,比如使用了组件库 antd, purgecss-webpack-plugin 插件检测src文件下tsx文件中使用的类名和id时,是检测不到在 src 中使用 antd 组件的类名的,打包的时候就会把 antd 的类名都给过滤掉,可以配置一下安全选择列表,避免删除 antd组件库的前缀ant
const globAll = require("glob-all");
const PurgecssPlugin = require("purgecss-webpack-plugin"); // 消除css未使用的代码
module.exports = {
plugins: [
// 清理无用css(打包发现暂时有些没有用到的class样式没清理掉,原因待查找)
new PurgecssPlugin({
//表示要检测哪些目录下的内容需要被分析,这里我们可以使用glob;
paths: globAll.sync([`${path.join(__dirname, "../src")}/**/*.tsx`, path.join(__dirname, "../public/index.html")]),
//默认情况下,Purgecss会将我们的html标签的样式移除掉,如果我们希望保留,可以添加一个safelist的属性;
safelist: function () {
return {
standard: ["body", "html",/^ant-/],
};
},
}),
],
};
6.8 资源懒加载
webpack默认支持资源懒加载,只需要引入资源使用import语法来引入资源,webpack打包的时候就会自动打包为单独的资源文件,等使用到的时候动态加载。
以 react 为例,配合 lazy 和 Suspense 使用
6.9 资源预加载
上面配置了资源懒加载后,虽然提升了首屏渲染速度,但是加载到资源的时候会有一个去请求资源的延时,如果资源比较大会出现延迟卡顿现象,可以借助 link 标签的 rel 属性 prefetch 与 preload , link 标签除了加载 css 之外也可以加载 js 资源,设置 rel 属性可以规定 link 提前加载资源,但是加载资源后不执行,等用到了再执行。
rel的属性值
- preload是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源。
- prefetch是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源,会在空闲时加载。
对于当前页面很有必要的资源使用 preload ,对于可能在将来的页面中使用的资源使用 prefetch。
webpack v4.6.0+ 增加了对预获取和预加载的支持,使用方式也比较简单,在 import 引入动态资源时使用webpack 的魔法注释
// 单个目标
import(
/* webpackChunkName: "my-chunk-name" */ // 资源打包后的文件chunkname
/* webpackPrefetch: true */ // 开启prefetch预获取
/* webpackPreload: true */ // 开启preload预获取
'./module'
);
测试一下,在src/components目录下新建PreloadDemo.tsx, PreFetchDemo.tsx
// src/components/PreloadDemo.tsx
import React from "react";
function PreloadDemo() {
return <h3>我是PreloadDemo组件</h3>
}
export default PreloadDemo
// src/components/PreFetchDemo.tsx
import React from "react";
function PreFetchDemo() {
return <h3>我是PreFetchDemo组件</h3>
}
export default PreFetchDemo
修改 App.tsx
import React, { lazy, Suspense, useState } from "react";
import smallImg from "./assets/imgs/5kb.png";
import bigImg from "./assets/imgs/22kb.png";
// prefetch
const PreFetchDemo = lazy(
() =>
import(
/* webpackChunkName: "PreFetchDemo" */
/*webpackPrefetch: true*/
"@/components/PreFetchDemo"
),
);
// preload
const PreloadDemo = lazy(
() =>
import(
/* webpackChunkName: "PreloadDemo" */
/*webpackPreload: true*/
"@/components/PreloadDemo"
),
);
import "./app.css";
import "./app.less";
function App() {
const [count, setCounts] = useState("");
const [show, setShow] = useState(false);
const onChange = (e: any) => {
setCounts(e.target.value);
};
const onClick = () => {
setShow(true);
};
return (
<>
<h2>webpack5-react-ts</h2>
<img src={smallImg} alt="小于10kb的图片" />
<img src={bigImg} alt="大于于10kb的图片" />
<div className="smallImg"></div>
<div className="bigImg"></div>
<p>受控组件</p>
<input type="text" value={count} onChange={onChange} />
<p>非受控组件</p>
<input type="text" />
<button onClick={onClick}>展示</button>
{show && (
<>
<Suspense fallback={null}>
<PreloadDemo />
</Suspense>
<Suspense fallback={null}>
<PreFetchDemo />
</Suspense>
</>
)}
</>
);
}
export default App;
测试发现只有js资源设置prefetch模式才能触发资源预加载
使用pnpm 安装包,因为pnpm解决了幽灵依赖问题,如果用的pnpm的话,需要手动再安装以下依赖
pnpm add ansi-html-community html-entities events core-js-pure error-stack-parser
6.10 打包时生成 gzip 文件
前端代码在浏览器运行,需要从服务器把html,css,js资源下载执行,下载的资源体积越小,页面加载速度就会越快。一般会采用gzip压缩,现在大部分浏览器和服务器都支持gzip,可以有效减少静态资源文件大小,压缩率在 70% 左右。
webpack可以借助compression-webpack-plugin 插件在打包时生成 gzip 文章,安装依赖
npm i compression-webpack-plugin -D
修改 webpack.prod.js
const CompressionPlugin = require("compression-webpack-plugin"); // webpack对文件的压缩
module.exports = {
plugins: [
new CompressionPlugin({
test: /\.(css|js)$/i, // 匹配哪些文件需要压缩
threshold: 0, // 设置文件从多大开始压缩
minRatio: 0.8, // 至少的压缩比例
algorithm: "gzip", // 采用的压缩算法
}),
],
};
配置完成后再打包,可以看到打包后js的目录下多了一个 .gz 结尾的文件
7、总结
到目前为止已经使用webpack5把react18+ts的开发环境配置完成,并且配置比较完善的保留组件状态的热更新,以及常见的优化构建速度和构建结果的配置,完整代码已上传到 webpack-react-ts