webpack5从0搭建完整的react18 + TS 开发和打包环境
webpack5从0搭建完整的react18 + TS 开发和打包环境
webpack5从零搭建完整的react18+ts开发和打包环境
本文将使用webpack5一步一步从0搭建一个完整的React 18 + TS 开发和打包环境,配置完善的模块热替换
以及构建速度
和构建结果
的优化,完整代码将同步至GitHub,文章取经自掘金大佬Ausra无忧
的文章,大家可以前往看一看,很不错!
目录:
1、初始化项目
2、配置基础版 react + Ts 环境
3、常用功能配置
4、配置react模块热替换
5、优化构建速度
6、优化构建结果文件
7、总结
一、初始化项目
先手动初始化一个react + ts 项目。新建文件夹webpack-react-18,在项目中打开终端。执行:
npm init - y
1、初始化好之后,可以看到多了一个package.json
2、然后在文件夹下新增以下的目录和文件:
3、接下里安装webpack依赖:
npm i webpack webpack-cli -D
4、安装React依赖:
npm i react react-dom -S
5、安装react类型依赖:
npm i @types/react @types/react-dom -D
6、在public/index.html
添加内容:
7、在tsconfig.json
添加内容:
{
"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"]
}
8、在src/App.tsx
中添加内容:
import React from 'react'
export default function App() {
return (
<div>
<h2>webpack-react-ts</h2>
</div>
)
}
9、在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/>)
}
接下来就可以配置webpack的代码啦~
二、配置基础版React + TS环境
2.1、修改webpack公共配置 —— webpack.base.js
2.1.1、配置入口文件:
const path = require("path")
module.exports = {
entry:path.join(__dirname,"../src/index.tsx"),//入口文件
}
2.1.2、配置出口文件:
const path = require("path")
module.exports = {
// ...
//打包文件出口
output:{
filename:"static/js/[name].js",//每个输出的js文件的名称
path:path.join(__dirname,"../dist"),//打包结果输出的路径
clean:true,//webapck5内置的,webpack4中需要配置clean-webpack-plugin来删除之前的dist
publicPath:"/"//打包后文件的公共前缀路径
}
}
2.1.3、配置loader解析TS和JSX
webpack默认只能识别js文件,不能识别jsx和ts语法,需要配置loader的预设@babel/preset-typescript
来先将TS语法转化成JS语法,再通过@babel/preset-react
来识别jsx语法。
安装babel
核心模块和preset
预设:
npm i babel-loader @babel/core @babel/preset-react @babel/preset-typescript -D
在webpack.base.js
中添加module.rules
配置:
const path = require("path")
module.exports = {
// ...
module:{
rules:[
{
test:/.(ts|tsx)$/,//匹配ts、tsx文件
use:{
loader:"babel-loader",
options:{
//预设执行顺序由右往左,所以这里是先处理ts再处理jsx
presets:[
"@babel/preset-react",
"@babel/preset-typescript"
]
}
}
}
]
}
}
2.1.4、配置extensions
extebsions
是webpack的resolve
解析配置下的选项,在==引入模块时不带入文件后缀==的时候,会在该配置数组中依次添加后缀查找文件。因为ts不支持引入以.ts
、.tsx
为后缀的文件,所以要在extensions
中要配置,在很多第三方库中里面很多引入js文件且没有带后缀,所以也要配置下js。
修改webpack.base.js
,注意要把高频出现的文件后缀放在前面:
module.exports = {
// ...
resolve: {
extensions: ['.js', '.tsx', '.ts'],
}
}
(这里我只配置了js、tsx、ts,其他引入的文件都要求带后缀,这样可以提升构建的速度)
2.1.5、添加html-webpack-plugin
插件
webpack需要把最终构建好的静态资源都引入到HTML文件中,这样才能在浏览器中运行。
先安装依赖:
npm i html-webpack-plugin -D
在webapck.base.js
中只用html-webpack-plugin
:
const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")
module.exports = {
// ...
plugins:[
new HtmlWebpackPlugin({
template:path.resolve(__dirname,"../public/index.html"),//模板用定义root节点的模板
inject:true//自动注入静态资源
})
],
}
在这里,一个基本的react公共配置就差不多了,然后我们在此基础上分别进行配置开发环境和打包环境。
2.2、 webpack开发环境下的配置
2.2.1、安装webpack-dev-server
开发环境的配置代码在webpack.dec.js
中配置,需要借助webpack-dev-server
在开发环境下启动服务器来辅助开发
npm i webpack-dev-server -D
还需要依赖webpack-merge
来合并基本配置,安装依赖:
npm i webpack-merge -D
修改webpack.dev.js
的内容,合并公共的配置,然后添加在开发模式下的配置:
const path = require("path")
const {merge} = require("webpack-merge")
const baseConfig = require("./webpack.base.js")
//合并公共配置,并添加开发环境配置
module.exports = merge(baseConfig,{
mode:"development",//开发模式下打包速度更快,省去了一些代码优化步骤
devtool:"eval-cheap-module-source-map",//源码调试的模式,后面单独会说
devServer:{
port:3000,//服务端口号
hot:true,//开启热模块替换功能,后面会有对react模块热替换的具体配置
compress:false,//gzip压缩,开发环境下不用开启,提升热更新的速度
historyApiFallback:true,//解决history路由一刷新变404的问题
static:{
directory:path.join(__dirname,"../public"),//托管静态资源public文件夹
}
}
})
package.json
添加dev
脚本:
2.2.2、在package.json的script
中添加内容
执行npm run dev
,就能看到项目已经跑起来了,访问http://localhost:3000
,就可以看到项目界面。
2.3、webpack打包环境配置
2.3.1、修改webpack.prod.js
的内容:
const {merge} = require("webpack-merge")
const baseConfig = require("./webpack.base.js")
module.exports = merge(baseConfig,{
mode:"production",//生产模式下,会开启tree-shaking和压缩代码,以及其他优化
})
2.3.2、在package.json中添加build
打包指令
还是在script中
:
然后执行npm run build
,打包结果将会放在==dist==文件夹里:
在浏览器中查看打包结果
打包后的dist文件可以在本地借助node服务器serve打开,全局安装serve
npm i serve -g
然后执行serve -s dist
,就可以启动打包后的项目了。
到这里,一个最最最基础版的react+ts的webpack5就配置好啦,但是这些在真实开发项目的时候还完全不够,所以我们继续进行更详细的配置。
三、基础功能配置
3.1、 配置环境变量
环境变量按照作用分为了两种:
- 区分是开发模式还是打包构建模式
- 区分是项目的业务环境,是开发/测试/正式环境
第一种情况可以用process.env.NODE_ENV
来判断环境变量,第二种情区分项目接口环境可以自定义一个环境变量process.env.BASE_ENV
,设置环境变量可以借助cross-env
和webpack-DefinePlugin
来设置。
-
cross-env:用来兼容各种系统的
设置环境变量的包
-
webpack-DefinePlugin:为业务代码注入环境变量,webpack内置的有
安装cross-env:
npm i cross-env -D
然后修改package.json的script
脚本字段,删除原来的dev
和build
字段,改为:
这里解释一下,dev
开头是指开发模式,build
开头是指打包模式。冒号后面对应的是dev
/test
/prod
是对应业务环境的开发/测试/正式上线环境。
process.env.NODE_ENV环境变量,webpack会自动根据设置的mode字段,来给业务代码注入对应的development和prodction。这里在命令中再次设置环境变量NODE_ENV是为了在webpack和babel的配置文件中访问到。
在webpack.base.js中打印一下设置的环境变量
执行npm run build:dev
,可以看到打印的信息:
当前是打包模式,业务环境是开发环境。这里需要把process.env.BASE_ENV注入到业务代码里面,就可以通过该环境变量设置对应环境的接口地址和数据,要借助webpack.DefinePlugin
插件。
所以我们修改一下webpack.base.js
:
const webpack = require("webpack")
module.exports = {
//...
plugins:[
new webpack.DefinePlugin({
"process.env.BASE_ENV":JSON.stringify(process.env.BASE_ENV)
})
]
//...
}
配置后,它会把值注入到业务代码里面去,webpack解析代码匹配到process.env.BASE_ENV
,就会设置到相应的值。测一下,在src/index.tsx
打印一下两个环境变量:
执行npm run dev:test
,可以在浏览器控制台上看到打印的结果:
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"
export default function App() {
return (
<div>
<h2>webpack-react-ts</h2>
</div>
)
}
执行打包命令npm run build:dev,会发现有报错, 因为webpack默认只认识js,是不识别css文件的,需要使用loader来解析css, 安装依赖:
npm i style-loader css-loader -D
- style-loader: 把解析后的css代码从js中抽离,放到头部的style标签中(在运行时做的)
- css-loader: 解析css文件代码
使用loader:
(因为解析css的配置开发和打包环境都会用到,所以加在公共配置webpack.base.js中)
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /.css$/, //匹配 css 文件
use: ['style-loader','css-loader']
}
]
},
// ...
}
loader执行顺序是从右往左,从下往上的,匹配到css文件后先用css-loader解析css, 最后借助style-loader把css插入到头部style标签中。
配置完成后再npm run build:dev打包,借助serve -s dist启动后在浏览器查看,可以看到样式生效了。
让我们继续解决less文件,安装less
和less-loader
:
npm i less-loader less -D
- less-loader: 解析less文件代码,把less编译为css
- less: less的核心
使用less-loader
:其实步骤也简单,就是在rules中添加less规则,遇到less文件,就使用less-loader解析为css,再进行css的解析过程。让我们修改下webpack.base.js
:
module.exports = {
// ...
module: {
// ...
rules: [
// ...
{
test: /.(css|less)$/, //匹配 css和less 文件
use: ['style-loader','css-loader', 'less-loader']
}
]
},
// ...
}
测试一下,新增src/app.less:
#root {
h2 {
font-size: 20px;
}
}
在App.tsx中引入app.less,npm run build:dev
打包,借助serve -s dist
启动项目,可以看到less文件编写的样式编译css后也插入到style标签了。
3.3、处理css3前缀兼容
有时候我们需要解决兼容一些低版本浏览器,就得给css3加上一些前缀。我们可以借助插件自动加前缀,postcss-loader
就是来给css3加上各种浏览器前缀的。先安装依赖:
npm i postcss-loader autoprefixer -D
- postcss-loader:处理css时自动加前缀
- autoprefixer:决定添加哪些浏览器前缀到css中
修改webpack.base.js, 在解析css和less的规则中添加配置:
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /.(css|less)$/, //匹配 css和less 文件
use: [
'style-loader',
'css-loader',
// 新增
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['autoprefixer']
}
}
},
'less-loader'
]
}
]
},
// ...
}
配置完成后,需要有一份要兼容浏览器的清单,让postcss-loader知道要加哪些浏览器的前缀,在根目录下创建 .browserslistrc文件:
IE 9 # 兼容IE 9
chrome 35 # 兼容chrome 35
以兼容到ie9和chrome35版本为例,配置好后,执行npm run build:dev打包,可以看到打包后的css文件已经加上了ie和谷歌内核的前缀
上面可以看到解析css和less有很多重复配置,可以进行提取postcss-loader配置优化一下。
postcss.config.js是postcss-loader的配置文件,会自动读取配置,根目录下新建postcss.config.js:
module.exports = {
plugins: ['autoprefixer']
}
修改webpack.base.js, 取消postcss-loader的options配置
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /.(css|less)$/, //匹配 css和less 文件
use: [
'style-loader',
'css-loader',
'postcss-loader',
'less-loader'
]
},
]
},
// ...
}
3.4、babel预设处理js兼容
现在js不断新增了很多方便好用的标准语法来方便开发,甚至还有非标准语法比如装饰器,都极大的提升了代码可读性和开发效率。但前者标准语法很多低版本浏览器不支持,后者非标准语法所有的浏览器都不支持。
所以我们需要把最新的标准语法转换为低版本的语法,把非标准语法转换为标准语法,这样才能让浏览器识别解析,而babel就是来做这件事的,这里只讲下简单配置。
安装依赖:
npm i babel-loader @babel/core @babel/preset-env core-js -D
-
babel-loader: 使用 babel 加载最新js代码并将其转换为 ES5(上面已经安装过)
-
@babel/core: babel 编译的核心包
-
@babel/preset-env: babel 编译的预设,可以转换目前最新的js标准语法
-
core-js: 使用低版本js语法模拟高版本的库,也就是垫片
修改webpack.base.js:
module.exports = {
// ...
module: {
rules: [
{
test: /.(ts|tsx)$/,
use: {
loader: 'babel-loader',
options: {
// 执行顺序由右往左,所以先处理ts,再处理jsx,最后再试一下babel转换为低版本语法
presets: [
[
"@babel/preset-env",
{
// 设置兼容目标浏览器版本,这里可以不写,babel-loader会自动寻找上面配置好的文件.browserslistrc
// "targets": {
// "chrome": 35,
// "ie": 9
// },
"useBuiltIns": "usage", // 根据配置的浏览器兼容,以及代码中使用到的api进行引入polyfill按需添加
"corejs": 3, // 配置使用core-js低版本
}
],
'@babel/preset-react',
'@babel/preset-typescript'
]
}
}
}
]
}
}
(这里格式不太好看,建议大家在复制在代码编辑器中打开)
此时再打包就会把语法转换为对应浏览器兼容的语法了。
为了避免webpack配置文件过于庞大,可以把babel-loader的配置抽离出来, 新建babel.config.js文件,使用js作为配置文件,是因为可以访问到process.env.NODE_ENV环境变量来区分是开发还是打包模式。
移除webpack.base.js中babel-loader的options配置:
3.5、复制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 path = require('path')
const CopyPlugin = require('copy-webpack-plugin');
module.exports = merge(baseConfig, {
mode: 'production',
plugins: [
// 复制文件插件
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, '../public'), // 复制public下文件
to: path.resolve(__dirname, '../dist'), // 复制到dist目录中
filter: source => {
return !source.includes('index.html') // 忽略index.html
}
},
],
}),
]
})
在上面的配置中,忽略了index.html,因为html-webpack-plugin会以public下的index.html为模板生成一个index.html到dist文件下,所以不需要再复制该文件了。
测试一下,在public中新增一个favicon.ico图标文件,并在index.html中引入:
再执行npm run build:dev打包,就可以看到public下的favicon.ico图标文件被复制到dist文件中了。
3.5、处理图片文件
对于图片文件,webpack5采用自带的asset-module来处理
修改webpack.base.js,添加图片解析配置
module.exports = {
module: {
rules: [
// ...
{
test:/.(png|jpg|jpeg|gif|svg)$/, // 匹配图片文件
type: "asset", // type选择asset
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 小于10kb转base64位
}
},
generator:{
filename:'static/images/[name][ext]', // 文件输出目录和命名
},
},
]
}
}
测试一下,准备一张小于10kb的图片 和 大于10kb的图片,放在src/assets/imgs目录下, 修改App.tsx:
import React from 'react'
import smallImg from './assets/imgs/dog.jpg'
import bigImg from './assets/imgs/壁纸.jpeg'
import "./app.css"
import "./app.less"
export default function App() {
return (
<div>
<h2>webpack-react-ts</h2>
<img src={smallImg} alt="小于10kb的图片" />
<img src={bigImg} alt="大于于10kb的图片" />
</div>
)
}
注意:这个时候在引入图片的地方会报:找不到模块“./assets/imgs/22kb.png”或其相应的类型声明,需要添加一个图片的声明文件
新增src/images.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'
添加图片声明文件后,就可以正常引入图片了, 然后执行npm run build:dev打包,借助serve -s dist查看效果,可以看到可以正常解析图片了,并且小于10kb的图片被转成了base64位格式的。
css中的背景图片一样也可以解析,修改app.tsx。
import React from 'react'
import smallImg from './assets/imgs/dog.jpg'
import bigImg from './assets/imgs/壁纸.jpeg'
import "./app.css"
import "./app.less"
export default function App() {
return (
<div>
<h2>webpack-react-ts</h2>
<img src={smallImg} alt="小于10kb的图片" />
<img src={bigImg} alt="大于于10kb的图片" />
<div className='smallImg'></div> {/* 小图片背景容器 */}
<div className='bigImg'></div> {/* 大图片背景容器 */}
</div>
)
}
修改app.less:
#root {
.smallImg {
width: 69px;
height: 75px;
background: url('./assets/imgs/dog.jpg') no-repeat;
}
.bigImg {
width: 232px;
height: 154px;
background: url('./assets/imgs/壁纸.jpeg') no-repeat;
}
}
可以看到背景图片也一样可以识别,小于10kb转为base64位:
3.6、处理字体和媒体文件
字体文件和媒体文件这两种资源处理方式和处理图片是一样的,只需要把匹配的路径和打包后放置的路径修改一下就可以了。修改webpack.base.js文件:
四、配置react热模块替换(HMR)
热更新上面已经在devServer中配置hot为true, 在webpack5中,只要devServer.hot为true了就可以了,该插件就已经内置了。
先在开发模式下修改css和less文件,页面样式可以在不刷新浏览器的情况实时生效,因为此时样式都在style标签里面,style-loader做了替换样式的热替换功能。但是修改App.tsx,浏览器会自动刷新后再显示修改后的内容,但我们想要的不是刷新浏览器,而是在不需要刷新浏览器的前提下模块热更新,并且能够保留react组件的状态。
我们可以借助@pmmmwh/react-refresh-webpack-plugin插件来实现,该插件又依赖于react-refresh,
1、安装依赖:
npm i @pmmmwh/react-refresh-webpack-plugin react-refresh -D
配置react热更新插件,修改webpack.dev.js
// webpack.dev.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = merge(baseConfig, {
// ...
plugins: [
new ReactRefreshWebpackPlugin(), // 添加热更新插件
]
})
2、为babel-loader配置react-refesh刷新插件,修改babel.config.js文件
const isDEV = process.env.NODE_ENV === 'development' // 是否是开发模式
module.exports = {
// ...
"plugins": [
isDEV && require.resolve('react-refresh/babel'), // 如果是开发模式,就启动react热更新插件
// ...
].filter(Boolean) // 过滤空值
}
3、测试一下,修改App.tsx代码:
import React,{useState} from 'react'
// import smallImg from './assets/imgs/dog.jpg'
// import bigImg from './assets/imgs/壁纸.jpeg'
import "./app.css"
import "./app.less"
export default function App() {
const [ count, setCounts ] = useState('')
const onChange = (e: any) => {
setCounts(e.target.value)
}
return (
<div>
<h2>webpack-react-ts</h2>
<p>受控组件</p>
<input type="text" value={count} onChange={onChange} />
<br />
<p>非受控组件</p>
<input type="text" />
</div>
)
}
在两个输入框分别输入内容后,修改App.tsx中h2标签的文本,会发现在不刷新浏览器的情况下,页面内容进行了热更新,并且react组件状态也会保留。
注意:新增或者删除页面hooks时,热更新时组件状态不会保留。
五、优化构建速度
5.1、构建耗时分析
当进行优化的时候,肯定要先知道时间都花费在哪些步骤上了,而speed-measure-webpack-plugin插件可以帮我们做到,安装依赖:
npm i speed-measure-webpack-plugin -D
使用的时候为了不影响到正常的开发/打包模式,我们选择新建一个配置文件,新增webpack构建分析配置文件build/webpack.analy.js
const prodConfig = require('./webpack.prod.js') // 引入打包配置
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); // 引入webpack打包速度分析插件
const smp = new SpeedMeasurePlugin(); // 实例化分析插件
const { merge } = require('webpack-merge') // 引入合并webpack配置方法
// 使用smp.wrap方法,把生产环境配置传进去,由于后面可能会加分析配置,所以先留出合并空位
module.exports = smp.wrap(merge(prodConfig, {
}))
修改package.json添加启动webpack打包分析脚本命令,在scripts新增:
{
// ...
"scripts": {
// ...
"build:analy": "cross-env NODE_ENV=production BASE_ENV=production webpack -c build/webpack.analy.js"
}
// ...
}
执行npm run build:analy命令:
可以在图中看到各plugin和loader的耗时时间,现在因为项目内容比较少,所以耗时都比较少,在真正的项目中可以通过这个来分析打包时间花费在什么地方,然后来针对性的优化。
5.2、开启持久化存储缓存
webpack5通过缓存生成的webpack模块和chunk,改善了下一次打包的构建速度,可提速90%左右,配置如下,修改webpack.base.js
:
// webpack.base.js
// ...
module.exports = {
// ...
cache: {
type: 'filesystem', // 使用文件缓存
},
}
当前文章代码的测试结果:
模式 | 第一次耗时 | 第二次耗时 |
---|---|---|
开发模式 | 3381ms | 257ms |
打包模式 | 4126ms | 479ms |
通过开启webpack5持久化存储缓存,再次打包的时间提升了92%。
缓存的存储位置在node_modules/.cache/webpack,里面又区分了development和production缓存:
5.3、开启多线程loader
webpack的loader默认在单线程执行,现代电脑一般都有多核cpu,可以借助多核cpu开启多线程loader解析,可以极大地提升loader解析的速度,thread-loader就是用来开启多进程解析loader的,安装依赖:
npm i thread-loader -D
注意:使用时,需将此 loader 放置在其他 loader 之前。放置在此 loader 之后的 loader 会在一个独立的 worker 池中运行。
启用多线程打包,修改webpack.base.js:
// webpack.base.js
module.exports = {
// ...
module: {
rules: [
{
test: /.(ts|tsx)$/,
use: ['thread-loader', 'babel-loader']
}
]
}
}
由于thread-loader不支持抽离css插件MiniCssExtractPlugin.loader(下面会讲),所以这里只配置了多进程解析js,开启多线程也是需要启动时间,大约600ms左右,所以适合规模比较大的项目。
5.4、配置alias别名
webpack支持设置别名,设置别名后可以让后续写引用地址的时候减少路径的复杂度。
修改webpack.base.js
;
module.export = {
// ...
resolve: {
// ...
alias: {
'@': path.join(__dirname, '../src')
}
}
}
修改tsconfig.json,添加baseUrl和paths:
{
"compilerOptions": {
// ...
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
}
}
}
配置完成后,在项目中使用@/xxx,就会指向项目中src/xxx
,在js/ts/css文件中都可以用。
在src/App.tsx
中修改内容:
import React,{useState} from 'react'
import smallImg from '@/assets/imgs/dog.jpg'
import bigImg from '@/assets/imgs/壁纸.jpeg'
import "./app.css"
import "./app.less"
export default function App() {
// const [ count, setCounts ] = useState('')
// const onChange = (e: any) => {
// setCounts(e.target.value)
// }
return (
<div>
<h2>webpack-react-ts</h2>
<img src={smallImg} alt="小于10kb的图片" />
<img src={bigImg} alt="大于于10kb的图片" />
<div className='smallImg'></div> {/* 小图片背景容器 */}
<div className='bigImg'></div> {/* 大图片背景容器 */}
</div>
)
}
在app.less
中修改引用路径:
#root {
.smallImg {
width: 69px;
height: 75px;
background: url('@/assets/imgs/dog.jpg') no-repeat;
}
.bigImg {
width: 232px;
height: 154px;
background: url('@/assets/imgs/壁纸.jpeg') no-repeat;
}
}
5.5、缩小loader作用范围
按照实际情况合理配置loader的作用范围,来减少不必要的loader解析,可以节省时间,通过使用 include和exclude 两个配置项来实现这个功能
- exclude:不解析该选项配置的模块
- include:只解析该选项配置的模块
修改webpack.base.js
:
const path = require('path')
module.exports = {
// ...
module: {
rules: [
{
include: [path.resolve(__dirname, '../src')], 只对项目src文件的ts,tsx进行loader解析
test: /.(ts|tsx)$/,
use: ['thread-loader', 'babel-loader']
}
]
}
}
如果除src文件外也还有需要解析的,就把对应的目录地址加上就可以了,比如需要引入antd的css,可以把antd的文件目录路径添加解析css规则到include里面
5.5、精确使用loader
loader在构建过程中的使用是根据文件后缀来倒序遍历rules数组
,如果文件后缀和test
正则匹配到了,就会使用该rule中的loader依次对文件源代码进行处理,最终拿到结果,我们可以通过减少无用的loader解析来提升构建的速度,比如避免使用less-loader来解析css文件。
我们可以拆分上面的配置,避免让less-loader再去解析css
原来的配置:
注意:ts和tsx也是如此,ts里面不能写jsx语法,,所以要避免@babel/preset-react
对.ts
文件语法做处理。
5.5、缩小模块的搜索范围
node里面模块分三种:
- node核心模块
- node_modules模块
- 自定义的文件模块
使用require
或者import
来引入模块时,如果有准确的路径,那么就会按照路径来查找。如果没有准确的路径时,它就会优先查询node
模块,若没有找到,就去当前目录下的node_modules
中找,如果没有找到,就会从父级文件夹中查找node_modules
,一直查到系统node全局模块
。
这样的话就会有些问题,比如一级一级地查询比较消耗时间。我们可以告诉webpack搜索目录范围,来规避这个问题。
修改webpack.base.js
:
const path = require('path')
module.exports = {
// ...
resolve: {
// ...
modules: [path.resolve(__dirname, '../node_modules')], // 查找第三方模块只在本项目的node_modules中查找
},
}
5.6、devtool配置
开发过程中或者打包后的代码都是webpack处理后的代码,如果进行调试肯定希望看到源代码,而不是编译后的代码, source map就是用来做源码映射的,不同的映射模式会明显影响到构建和重新构建的速度
关键字 | 描述 |
---|---|
eval | eval(...) 形式执行代码 |
inline | 代码内通过 dataUrl 形式引入 SourceMap |
cheap | 只需要定位到行信息,不需要列信息 |
module | 展示源代码中的错误位置 |
hidden | 生成 SourceMap 文件,但不使用 |
nosources | 不生成 SourceMap |
开发环境推荐使用:eval-cheap-module-source-map
原因:
- 开发模式下,第一次打包会有点慢,但是加了eval会有缓存,所以热更新会很快
- 一般每行的代码不会很多,所以我们只用定位到行就行,所以用cheap
- 我们需要定位到源码中的错误,所以用module
打包环境下:就不使用devtool了,就是说不用配置devtool了。
devtool为none的话(不是说devtool:“none”),我们只能看到编译后的代码,不会泄露源码,打包速度会更加快
六、优化构建结果
1、webpack包分析工具
webpack-bundle-analyzer是分析webpack打包后文件的插件,使用交互式可缩放树形图可视化 webpack 输出文件的大小。通过该插件可以对打包后的文件进行观察和分析,可以方便我们对不完美的地方针对性的优化,安装依赖:
npm install webpack-bundle-analyzer -D
修改webpack.analy.js:
// webpack.analy.js
const prodConfig = require('./webpack.prod.js')
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
const { merge } = require('webpack-merge')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') // 引入分析打包结果插件
module.exports = smp.wrap(merge(prodConfig, {
plugins: [
new BundleAnalyzerPlugin() // 配置分析打包结果插件
]
}))
配置好后,执行npm run build:analy
命令,打包完成后浏览器会自动打开窗口,可以看到打包文件的分析结果页面,可以看到各个文件所占的资源大小。
2、抽取css样式文件
在开发环境我们希望css嵌入在style标签里面,方便样式热替换,但在打包时我们希望把css单独抽离出来,方便配置缓存策略。而插件mini-css-extract-plugin就是来帮我们做这件事的,安装依赖:
npm i mini-css-extract-plugin -D
修改webpack.base.js, 根据环境变量设置开发环境使用style-loader,打包模式抽离css:
再修改webpack.prod.js, 打包时添加抽离css插件
// webpack.prod.js
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = merge(baseConfig, {
mode: 'production',
plugins: [
// ...
// 抽离css插件
new MiniCssExtractPlugin({
filename: 'static/css/[name].css' // 抽离css的输出目录和名称
}),
]
})
配置完成后,在开发模式下css会嵌入到style标签中,方便样式热替换,打包模式下会把css文件抽离成单独的css文件。
3、压缩css文件
上面我们配置了css抽离成单独的文件。默认情况下css是没有压缩的,然后我们可以看一下:
我们需要手动压缩css,继续配置css的插件,可以借助
css-minimizer-webpack-plugin
来压缩css,安装依赖:
npm i css-minimizer-webpack-plugin -D
修改webpack.prod.js文件, 需要在优化项optimization下的minimizer属性中配置:
// webpack.prod.js
// ...
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
// ...
optimization: {
minimizer: [
new CssMinimizerPlugin(), // 压缩css
],
},
}
再次执行打包就可以看到css已经被压缩了。
4、压缩js文件
设置mode为production时,webpack会使用内置插件terser-webpack-plugin压缩js文件,该插件默认支持多线程压缩,但是上面配置optimization.minimizer压缩css后,js压缩就失效了,需要手动再添加一下,webpack内部安装了该插件
安装依赖:
npm i terser-webpack-plugin -D
使用插件:
// ...
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
// ...
optimization: {
minimizer: [
// ...
new TerserPlugin({ // 压缩js
parallel: true, // 开启多线程压缩
terserOptions: {
compress: {
pure_funcs: ["console.log"] // 删除console.log
}
}
}),
],
},
}
配置完成后再打包,css和js就都可以被压缩了。
5、合理配置打包文件hash
合理配置文件缓存,可以提升前端加载页面速度。webpack打包的hash分三种:
- hash:跟整个项目的构建有关,只要项目里面有文件修改,那么整个项目构建的hash都会改变
- chunkhash:文件本身修改或者依赖的文件修改,chunkhash值会改变
- contenthash:每个文件有一个单独的hash值,文件的改动只会影响自身的hash
js:我们在生产环境里会把一些公共库和程序入口文件区分开,单独打包构建。采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响,可以继续使用浏览器缓存,所以js适合使用chunkhash。
css、媒体、图片资源:一般都是单独存在的,可以采用contenthash,只有文件本身变化后会生成新hash值。
修改webpack.base.js,把js输出的文件名称格式加上chunkhash,把css和图片媒体资源输出格式加上contenthash:
再修改webpack.prod.js,修改抽离css文件名称格式:
再次打包就可以看到文件后面的hash了
6、代码分隔第三方包和公共的模块
一般情况下第三方包不会改动,我们可以对node_modules
里的代码单独打包,当第三方包代码没有改变时,它的chunkhash
也不会改变,我们可以有效利用浏览器的缓存。webpack提供了代码分隔功能,需要在optimization
中手动配置splitChunk
规则
修改webpack.prod.js
:
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就提取出来
}
}
}
配置完成后,执行打包命令,我们可以看到node_modules
里面的代码被抽离到vendors.50da5d2a.js
中,业务代码被抽离到main.dd0c4a70.js
中。
测试一下,此时verdors.js的chunkhash是50da5d2a,main.js文件的chunkhash是dd0c4a70,改动一下App.tsx,再次打包,可以看到main.js的chunkhash值变化了,但是vendors.js的chunkhash还是原先的,这样浏览器就可以继续使用缓存中的verdors.50da5d2a.js,只需要重新请求main.js就可以了。
7、tree-shaking清理未引用的js
模式mode为production时就会默认开启tree-shaking功能以此来标记未引入代码然后移除掉,测试一下。
在src/components目录下新增Demo1,Demo2两个组件
import React from 'react'
export default function Demo1() {
return (
<div>Demo1</div>
)
}
import React from 'react'
export default function Demo2() {
return (
<div>Demo2</div>
)
}
再在src/components目录下新增index.ts, 把Demo1和Demo2组件引入进来再暴露出去
// src/components/index.ts
export { default as Demo1 } from './Demo1'
export { default as Demo2 } from './Demo2'
在App.tsx中引入两个组件,但只使用Demo1组件
import React,{useState} from 'react'
// import smallImg from '@/assets/imgs/dog.jpg'
// import bigImg from '@/assets/imgs/壁纸.jpeg'
import {Demo1,Demo2} from "@/components"
import "./app.css"
import "./app.less"
export default function App() {
// const [ count, setCounts ] = useState('')
// const onChange = (e: any) => {
// setCounts(e.target.value)
// }
return (
<div>
<Demo1 />
<h2>webpack-react-ts</h2>
{/* <img src={smallImg} alt="小于10kb的图片" />
<img src={bigImg} alt="大于于10kb的图片" /> */}
{/* 小图片背景容器 */}
{/* <div className='smallImg'></div> */}
{/* 大图片背景容器 */}
{/* <div className='bigImg'></div> */}
</div>
)
}
执行打包,可以看到在main.js中搜索Demo,只搜索到了Demo1, 代表Demo2组件被tree-shaking移除掉了。
7、清理未使用的css
css中也会有未被页面使用到的样式,可以通过purgecss-webpack-plugin插件打包的时候移除未使用到的css样式,这个插件是和mini-css-extract-plugin插件配合使用的,在上面已经安装过,还需要glob-all来选择要检测哪些文件里面的类名和id还有标签名称, 安装依赖:
npm i css-minimizer-webpack-plugin -D
修改webpack.prod.js文件:
/ webpack.prod.js
// ...
const globAll = require('glob-all')
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
// ...
plugins: [
// 抽离css插件
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:8].css'
}),
// 清理无用css
new PurgeCSSPlugin({
// 检测src下所有tsx文件和public下index.html中使用的类名和id和标签名称
// 只打包这些文件中用到的样式
paths: globAll.sync([
`${path.join(__dirname, '../src')}/**/*.tsx`,
path.join(__dirname, '../public/index.html')
]),
}),
]
}
测试一下,修改App.tsx:
App.tsx中有两个div,类名分别是smallImg和bigImg,当前app.less代码为:
#root {
.smallImg {
width: 69px;
height: 75px;
background: url('./assets/imgs/dog.jpg') no-repeat;
}
.bigImg {
width: 232px;
height: 154px;
background: url('./assets/imgs/壁纸.jpeg') no-repeat;
}
}
此时打包一下,看看打包后的main.css
文件:
因为页面中有h2标签、smallImg和bigImg类名,所以打包后的css也有。若此时我们修改一下app.less
中的类名,将smallImg
改为small
后,.small
就是没有用的类名了。因为页面中没有类名为small
的节点,我们再次打包看看结果:
可以看到main.css已经没有 .small类名的样式了,做到了删除无用css**的功能。nice
8、资源懒加载
react,vue等单页应用打包默认会打包到一个js文件中,虽然使用代码分割可以把node_modules模块和公共模块分离,但页面初始加载还是会把整个项目的代码下载下来,其实只需要公共资源和当前页面的资源就可以了,其他页面资源可以等使用到的时候再加载,可以有效提升首屏加载速度。
webpack默认支持资源懒加载,只需要引入资源使用import语法来引入资源,webpack打包的时候就会自动打包为单独的资源文件,等使用到的时候动态加载。
import React from "react";
function LazyDemo() {
return <h3>我是懒加载组件组件</h3>
}
export default LazyDemo
修改App.tsx
:
import React, { lazy, Suspense, useState } from 'react'
const LazyDemo = lazy(() => import('@/components/LazyDemo')) // 使用import语法配合react的Lazy动态引入资源
function App() {
const [ show, setShow ] = useState(false)
// 点击事件中动态引入css, 设置show为true
const onClick = () => {
import('./app.css')
setShow(true)
}
return (
<>
<h2 onClick={onClick}>展示</h2>
{/* show为true时加载LazyDemo组件 */}
{ show && <Suspense fallback={null}><LazyDemo /></Suspense> }
</>
)
}
export default App
点击展示文字时,才会动态加载app.css和LazyDemo组件的资源。
9、打包时生成gzip文件
前端代码在浏览器运行,需要从服务器把html,css,js资源下载执行,下载的资源体积越小,页面加载速度就会越快。
webpack可以借助compression-webpack-plugin 插件在打包时生成 gzip 文件,安装依赖:
npm i compression-webpack-plugin -D
添加配置,修改webpack.prod.js:
const glob = require('glob')
const CompressionPlugin = require('compression-webpack-plugin')
module.exports = {
// ...
plugins: [
// ...
new CompressionPlugin({
test: /.(js|css)$/, // 只生成css,js压缩文件
filename: '[path][base].gz', // 文件命名
algorithm: 'gzip', // 压缩格式,默认是gzip
test: /.(js|css)$/, // 只生成css,js压缩文件
threshold: 10240, // 只有大小大于该值的资源会被处理。默认值是 10k
minRatio: 0.8 // 压缩率,默认值是 0.8
})
]
}
配置完进行打包,可以看到js的目录下多了一个==.gz==文件:
因为只有node_modules
的大小是超过10K的,所以只有它生成gzip文件。我们可以看到文件大小减少了近66%:
总结:
到目前为止,已经使用webpack5完成了react18+Ts的环境配置,也配置了比较完善的HMR功能,以及一些常见的优化构建速度和优化构建结果,完整代码将上传至github。当然这还远远不够,还有很多细节没有做到位,比如oneOf、eslint部分、babel的辅助代码、图片压缩、preload/prefetch预加载等等……
本文是总结自己在学习中使用webpack5搭建react+ts构建环境中使用到的配置, 肯定也很多没有做好的地方,后续有更好的配置也会继续更新记录的……
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
2022-02-26 Windows2012R2配置SQLSERVER2012集群Alwayson配置高可用性
2022-02-26 【H5】十款构建 React.js 应用程序的UI框架
2022-02-26 Sql Server2008-读写分离
2022-02-26 Aforge.net之旅——开篇:从识别验证码开始
2022-02-26 AForge.NET
2022-02-26 移动端网页特效
2022-02-26 PC 端网页特效