webpack5从0搭建完整的react18 + TS 开发和打包环境

webpack5从0搭建完整的react18 + TS 开发和打包环境

2023-03-141,940阅读10分钟
 
专栏: 
webpack学习与总结
 
 
智能总结
复制
重新生成
 

本文详细介绍了使用 webpack5 从 0 搭建完整的 react18 + TS 开发和打包环境的过程,包括初始化项目,配置基础环境,处理各类文件(如 less、图片等),设置别名、缩小 loader 作用范围、资源懒加载、生成 gzip 文件等优化,总结了当前成果和不足,完整代码将上传至 GitHub。

关联问题:如何优化构建速度react模块热替换怎样怎样处理less文件
 
 

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,在项目中打开终端。执行:

 
shell
代码解读
复制代码
npm init - y
1、初始化好之后,可以看到多了一个package.json

微信截图_20230226192326.png

2、然后在文件夹下新增以下的目录和文件:

image-20230226193307811.png

3、接下里安装webpack依赖:
 
shell
代码解读
复制代码
npm i webpack webpack-cli -D
4、安装React依赖:
 
shell
代码解读
复制代码
npm i react react-dom -S
5、安装react类型依赖:
 
shell
代码解读
复制代码
npm i @types/react @types/react-dom -D
6、在public/index.html添加内容:

微信截图_20230226193819.png

7、在tsconfig.json添加内容:
 
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中添加内容:
 
tsx
代码解读
复制代码
import React from 'react'

export default function App() {
    return (
        <div>
            <h2>webpack-react-ts</h2>
        </div>
    )
}

9、在src/index.tsx添加内容:

 
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、配置入口文件:

 
js
代码解读
复制代码
const path = require("path")

module.exports = {
    entry:path.join(__dirname,"../src/index.tsx"),//入口文件
}

2.1.2、配置出口文件:

 
js
代码解读
复制代码
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预设:

 
shell
代码解读
复制代码
npm i babel-loader @babel/core @babel/preset-react @babel/preset-typescript -D 

webpack.base.js中添加module.rules配置:

 
js
代码解读
复制代码
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,注意要把高频出现的文件后缀放在前面:

 
js
代码解读
复制代码
module.exports = {
  // ...
  resolve: {
    extensions: ['.js', '.tsx', '.ts'],
  }
}

(这里我只配置了js、tsx、ts,其他引入的文件都要求带后缀,这样可以提升构建的速度)

2.1.5、添加html-webpack-plugin插件

webpack需要把最终构建好的静态资源都引入到HTML文件中,这样才能在浏览器中运行。

先安装依赖:

 
shell
代码解读
复制代码
npm i html-webpack-plugin -D

webapck.base.js中只用html-webpack-plugin:

 
js
代码解读
复制代码
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在开发环境下启动服务器来辅助开发

 
shell
代码解读
复制代码
npm i webpack-dev-server -D

还需要依赖webpack-merge来合并基本配置,安装依赖:

 
shell
代码解读
复制代码
npm i webpack-merge -D

修改webpack.dev.js的内容,合并公共的配置,然后添加在开发模式下的配置:

 
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中添加内容

微信截图_20230226205626.png

执行npm run dev,就能看到项目已经跑起来了,访问http://localhost:3000,就可以看到项目界面。

2.3、webpack打包环境配置

2.3.1、修改webpack.prod.js的内容:

 
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中

微信截图_20230226212021.png

然后执行npm run build,打包结果将会放在==dist==文件夹里:

微信截图_20230226212252.png

在浏览器中查看打包结果

打包后的dist文件可以在本地借助node服务器serve打开,全局安装serve

 
shell
代码解读
复制代码
npm i serve -g

然后执行serve -s dist,就可以启动打包后的项目了。

微信截图_20230226212547.png

到这里,一个最最最基础版的react+ts的webpack5就配置好啦,但是这些在真实开发项目的时候还完全不够,所以我们继续进行更详细的配置。

三、基础功能配置

3.1、 配置环境变量

环境变量按照作用分为了两种:

  • 区分是开发模式还是打包构建模式
  • 区分是项目的业务环境,是开发/测试/正式环境

第一种情况可以用process.env.NODE_ENV来判断环境变量,第二种情区分项目接口环境可以自定义一个环境变量process.env.BASE_ENV,设置环境变量可以借助cross-envwebpack-DefinePlugin来设置。

  • cross-env:用来兼容各种系统的设置环境变量的包

  • webpack-DefinePlugin:为业务代码注入环境变量,webpack内置的有

安装cross-env:

 
shell
代码解读
复制代码
npm i cross-env -D

然后修改package.json的script脚本字段,删除原来的devbuild字段,改为:

微信截图_20230226220307.png

这里解释一下,dev开头是指开发模式,build开头是指打包模式。冒号后面对应的是dev /test/prod是对应业务环境的开发/测试/正式上线环境。

process.env.NODE_ENV环境变量,webpack会自动根据设置的mode字段,来给业务代码注入对应的development和prodction。这里在命令中再次设置环境变量NODE_ENV是为了在webpack和babel的配置文件中访问到。

在webpack.base.js中打印一下设置的环境变量

微信截图_20230227091309.png

执行npm run build:dev,可以看到打印的信息:

微信截图_20230227091503.png

当前是打包模式,业务环境是开发环境。这里需要把process.env.BASE_ENV注入到业务代码里面,就可以通过该环境变量设置对应环境的接口地址和数据,要借助webpack.DefinePlugin插件。

所以我们修改一下webpack.base.js:

 
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打印一下两个环境变量:

微信截图_20230227092702.png

执行npm run dev:test,可以在浏览器控制台上看到打印的结果:

微信截图_20230227092839.png

3.2、处理css和less文件

src下新增app.css:

 
css
代码解读
复制代码
h2 {
    color: red;
    transform: translateY(100px);
}

src/App.tsx中引入app.css:

 
tsx
代码解读
复制代码
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, 安装依赖:

 
shell
代码解读
复制代码
npm i style-loader css-loader -D
  • style-loader: 把解析后的css代码从js中抽离,放到头部的style标签中(在运行时做的)
  • css-loader: 解析css文件代码

使用loader:

(因为解析css的配置开发和打包环境都会用到,所以加在公共配置webpack.base.js中)

 
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启动后在浏览器查看,可以看到样式生效了。

微信截图_20230227093731.png

让我们继续解决less文件,安装lessless-loader:

 
sh
代码解读
复制代码
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:

 
js
代码解读
复制代码
module.exports = {
  // ...
  module: {
    // ...
    rules: [
      // ...
      {
        test: /.(css|less)$/, //匹配 css和less 文件
        use: ['style-loader','css-loader', 'less-loader']
      }
    ]
  },
  // ...
}

测试一下,新增src/app.less:

 
sh
代码解读
复制代码
#root {
  h2 {
    font-size: 20px;
  }
}

在App.tsx中引入app.less,npm run build:dev打包,借助serve -s dist启动项目,可以看到less文件编写的样式编译css后也插入到style标签了。

微信截图_20230227094905.png

3.3、处理css3前缀兼容

有时候我们需要解决兼容一些低版本浏览器,就得给css3加上一些前缀。我们可以借助插件自动加前缀,postcss-loader就是来给css3加上各种浏览器前缀的。先安装依赖:

 
sh
代码解读
复制代码
npm i postcss-loader autoprefixer -D
  • postcss-loader:处理css时自动加前缀
  • autoprefixer:决定添加哪些浏览器前缀到css中

修改webpack.base.js, 在解析css和less的规则中添加配置:

 
js
代码解读
复制代码
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文件:

 
sh
代码解读
复制代码
IE 9 # 兼容IE 9
chrome 35 # 兼容chrome 35

以兼容到ie9和chrome35版本为例,配置好后,执行npm run build:dev打包,可以看到打包后的css文件已经加上了ie和谷歌内核的前缀

微信截图_20230227100407.png

上面可以看到解析css和less有很多重复配置,可以进行提取postcss-loader配置优化一下。

postcss.config.js是postcss-loader的配置文件,会自动读取配置,根目录下新建postcss.config.js:

 
js
代码解读
复制代码
module.exports = {
  plugins: ['autoprefixer']
}

修改webpack.base.js, 取消postcss-loader的options配置

 
js
代码解读
复制代码
module.exports = {
  // ...
  module: { 
    rules: [
      // ...
      {
        test: /.(css|less)$/, //匹配 css和less 文件
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader',
          'less-loader'
        ]
      },
    ]
  },
  // ...
}

3.4、babel预设处理js兼容

现在js不断新增了很多方便好用的标准语法来方便开发,甚至还有非标准语法比如装饰器,都极大的提升了代码可读性和开发效率。但前者标准语法很多低版本浏览器不支持,后者非标准语法所有的浏览器都不支持。

所以我们需要把最新的标准语法转换为低版本的语法,把非标准语法转换为标准语法,这样才能让浏览器识别解析,而babel就是来做这件事的,这里只讲下简单配置。

安装依赖:

 
sh
代码解读
复制代码
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:

 
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插件,安装依赖:

 
sh
代码解读
复制代码
npm i copy-webpack-plugin -D

开发环境已经在devServer中配置了static托管了public文件夹,所以在开发环境使用绝对路径可以访问到public下的文件。但打包构建时不做处理的话会访问不到,所以现在需要在打包配置文件webpack.prod.js中新增copy插件配置:

 
js
代码解读
复制代码
// 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中引入:

微信截图_20230227103221.png

再执行npm run build:dev打包,就可以看到public下的favicon.ico图标文件被复制到dist文件中了。

微信截图_20230227103347.png

3.5、处理图片文件

对于图片文件,webpack5采用自带的asset-module来处理

修改webpack.base.js,添加图片解析配置

 
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:

 
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文件,添加内容:

 
typescript
代码解读
复制代码
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位格式的。

微信截图_20230227110721.png

css中的背景图片一样也可以解析,修改app.tsx。

 
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:

 
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位:

微信截图_20230227111218.png

3.6、处理字体和媒体文件

字体文件和媒体文件这两种资源处理方式和处理图片是一样的,只需要把匹配的路径和打包后放置的路径修改一下就可以了。修改webpack.base.js文件:

微信截图_20230227111816.png

四、配置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、安装依赖:

 
sh
代码解读
复制代码
npm i @pmmmwh/react-refresh-webpack-plugin react-refresh -D

配置react热更新插件,修改webpack.dev.js

 
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文件

 
js
代码解读
复制代码
const isDEV = process.env.NODE_ENV === 'development' // 是否是开发模式
module.exports = {
  // ...
  "plugins": [
    isDEV && require.resolve('react-refresh/babel'), // 如果是开发模式,就启动react热更新插件
    // ...
  ].filter(Boolean) // 过滤空值
}

3、测试一下,修改App.tsx代码:

 
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组件状态也会保留。

微信截图_20230227113232.png

微信截图_20230227113258.png

注意:新增或者删除页面hooks时,热更新时组件状态不会保留。

五、优化构建速度

5.1、构建耗时分析

当进行优化的时候,肯定要先知道时间都花费在哪些步骤上了,而speed-measure-webpack-plugin插件可以帮我们做到,安装依赖:

 
shell
代码解读
复制代码
npm i speed-measure-webpack-plugin -D

使用的时候为了不影响到正常的开发/打包模式,我们选择新建一个配置文件,新增webpack构建分析配置文件build/webpack.analy.js

 
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新增:

 
shell
代码解读
复制代码
{
  // ...
  "scripts": {
    // ...
    "build:analy": "cross-env NODE_ENV=production BASE_ENV=production webpack -c build/webpack.analy.js"
  }
  // ...
}

执行npm run build:analy命令:

微信截图_20230227114551.png 可以在图中看到各plugin和loader的耗时时间,现在因为项目内容比较少,所以耗时都比较少,在真正的项目中可以通过这个来分析打包时间花费在什么地方,然后来针对性的优化。

5.2、开启持久化存储缓存

webpack5通过缓存生成的webpack模块和chunk,改善了下一次打包的构建速度,可提速90%左右,配置如下,修改webpack.base.js:

 
js
代码解读
复制代码
// webpack.base.js
// ...
module.exports = {
  // ...
  cache: {
    type: 'filesystem', // 使用文件缓存
  },
}

当前文章代码的测试结果:

模式第一次耗时第二次耗时
开发模式 3381ms 257ms
打包模式 4126ms 479ms

通过开启webpack5持久化存储缓存,再次打包的时间提升了92%。

缓存的存储位置在node_modules/.cache/webpack,里面又区分了development和production缓存:

微信截图_20230227115555.png

5.3、开启多线程loader

webpack的loader默认在单线程执行,现代电脑一般都有多核cpu,可以借助多核cpu开启多线程loader解析,可以极大地提升loader解析的速度,thread-loader就是用来开启多进程解析loader的,安装依赖:

 
shell
代码解读
复制代码
npm i thread-loader -D

注意:使用时,需将此 loader 放置在其他 loader 之前。放置在此 loader 之后的 loader 会在一个独立的 worker 池中运行。

启用多线程打包,修改webpack.base.js:

 
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

 
js
代码解读
复制代码
module.export = {
  // ...
   resolve: {
    // ...
    alias: {
      '@': path.join(__dirname, '../src')
    }
  }
}

修改tsconfig.json,添加baseUrl和paths:

 
json
代码解读
复制代码
{
  "compilerOptions": {
    // ...
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "src/*"
      ]
    }
  }
}

配置完成后,在项目中使用@/xxx,就会指向项目中src/xxx,在js/ts/css文件中都可以用。

src/App.tsx中修改内容:

 
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中修改引用路径:

 
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:

 
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

原来的配置:

微信截图_20230227151618.png

注意: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:

 
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

微信截图_20230227154024.png

打包环境下:就不使用devtool了,就是说不用配置devtool了。

devtool为none的话(不是说devtool:“none”),我们只能看到编译后的代码,不会泄露源码,打包速度会更加快

六、优化构建结果

1、webpack包分析工具

webpack-bundle-analyzer是分析webpack打包后文件的插件,使用交互式可缩放树形图可视化 webpack 输出文件的大小。通过该插件可以对打包后的文件进行观察和分析,可以方便我们对不完美的地方针对性的优化,安装依赖:

 
shell
代码解读
复制代码
npm install webpack-bundle-analyzer -D

修改webpack.analy.js:

 
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命令,打包完成后浏览器会自动打开窗口,可以看到打包文件的分析结果页面,可以看到各个文件所占的资源大小。

微信截图_20230227162007.png

2、抽取css样式文件

在开发环境我们希望css嵌入在style标签里面,方便样式热替换,但在打包时我们希望把css单独抽离出来,方便配置缓存策略。而插件mini-css-extract-plugin就是来帮我们做这件事的,安装依赖:

 
shell
代码解读
复制代码
npm i mini-css-extract-plugin -D

修改webpack.base.js, 根据环境变量设置开发环境使用style-loader,打包模式抽离css:

微信截图_20230227163616.png 再修改webpack.prod.js, 打包时添加抽离css插件

 
js
代码解读
复制代码
// 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是没有压缩的,然后我们可以看一下:

微信截图_20230227164331.png 我们需要手动压缩css,继续配置css的插件,可以借助css-minimizer-webpack-plugin来压缩css,安装依赖:

 
shell
代码解读
复制代码
npm i css-minimizer-webpack-plugin -D

修改webpack.prod.js文件, 需要在优化项optimization下的minimizer属性中配置:

 
js
代码解读
复制代码
// webpack.prod.js
// ...
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
  // ...
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(), // 压缩css
    ],
  },
}

再次执行打包就可以看到css已经被压缩了。

微信截图_20230227164951.png

4、压缩js文件

设置mode为production时,webpack会使用内置插件terser-webpack-plugin压缩js文件,该插件默认支持多线程压缩,但是上面配置optimization.minimizer压缩css后,js压缩就失效了,需要手动再添加一下,webpack内部安装了该插件

安装依赖:

 
shell
代码解读
复制代码
npm i terser-webpack-plugin -D

使用插件:

 
js
代码解读
复制代码
// ...
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:

微信截图_20230227170650.png

微信截图_20230227170721.png

微信截图_20230227170807.png

再修改webpack.prod.js,修改抽离css文件名称格式:

微信截图_20230227171017.png

再次打包就可以看到文件后面的hash了

6、代码分隔第三方包和公共的模块

一般情况下第三方包不会改动,我们可以对node_modules里的代码单独打包,当第三方包代码没有改变时,它的chunkhash也不会改变,我们可以有效利用浏览器的缓存。webpack提供了代码分隔功能,需要在optimization中手动配置splitChunk规则

修改webpack.prod.js:

 
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两个组件

 
tsx
代码解读
复制代码
import React from 'react'

export default function Demo1() {
    return (
        <div>Demo1</div>
    )
}
 
tsx
代码解读
复制代码
import React from 'react'

export default function Demo2() {
    return (
        <div>Demo2</div>
    )
}

再在src/components目录下新增index.ts, 把Demo1和Demo2组件引入进来再暴露出去

 
tsx
代码解读
复制代码
// src/components/index.ts
export { default as Demo1 } from './Demo1'
export { default as Demo2 } from './Demo2'

在App.tsx中引入两个组件,但只使用Demo1组件

 
tsx
代码解读
复制代码
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移除掉了。

微信截图_20230227173643.png

7、清理未使用的css

css中也会有未被页面使用到的样式,可以通过purgecss-webpack-plugin插件打包的时候移除未使用到的css样式,这个插件是和mini-css-extract-plugin插件配合使用的,在上面已经安装过,还需要glob-all来选择要检测哪些文件里面的类名和id还有标签名称, 安装依赖:

 
shell
代码解读
复制代码
npm i css-minimizer-webpack-plugin -D

修改webpack.prod.js文件:

 
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:

微信截图_20230227174523.png

App.tsx中有两个div,类名分别是smallImg和bigImg,当前app.less代码为:

 
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文件:

微信截图_20230227193757.png

因为页面中有h2标签、smallImg和bigImg类名,所以打包后的css也有。若此时我们修改一下app.less中的类名,将smallImg改为small后,.small就是没有用的类名了。因为页面中没有类名为small的节点,我们再次打包看看结果:

微信截图_20230227194231.png

可以看到main.css已经没有 .small类名的样式了,做到了删除无用css**的功能。nice

8、资源懒加载

react,vue等单页应用打包默认会打包到一个js文件中,虽然使用代码分割可以把node_modules模块和公共模块分离,但页面初始加载还是会把整个项目的代码下载下来,其实只需要公共资源和当前页面的资源就可以了,其他页面资源可以等使用到的时候再加载,可以有效提升首屏加载速度。

webpack默认支持资源懒加载,只需要引入资源使用import语法来引入资源,webpack打包的时候就会自动打包为单独的资源文件,等使用到的时候动态加载。

 
tsx
代码解读
复制代码
import React from "react";
function LazyDemo() {
  return <h3>我是懒加载组件组件</h3>
}
export default LazyDemo

修改App.tsx:

 
js
代码解读
复制代码
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组件的资源。

微信截图_20230227181727.png

9、打包时生成gzip文件

前端代码在浏览器运行,需要从服务器把html,css,js资源下载执行,下载的资源体积越小,页面加载速度就会越快。

webpack可以借助compression-webpack-plugin 插件在打包时生成 gzip 文件,安装依赖:

 
shell
代码解读
复制代码
npm i compression-webpack-plugin -D

添加配置,修改webpack.prod.js:

 
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==文件:

微信截图_20230227191447.png

因为只有node_modules的大小是超过10K的,所以只有它生成gzip文件。我们可以看到文件大小减少了近66%:

微信截图_20230227191837.png

总结:

到目前为止,已经使用webpack5完成了react18+Ts的环境配置,也配置了比较完善的HMR功能,以及一些常见的优化构建速度和优化构建结果,完整代码将上传至github。当然这还远远不够,还有很多细节没有做到位,比如oneOf、eslint部分、babel的辅助代码、图片压缩、preload/prefetch预加载等等……

本文是总结自己在学习中使用webpack5搭建react+ts构建环境中使用到的配置, 肯定也很多没有做好的地方,后续有更好的配置也会继续更新记录的……

posted on   漫思  阅读(8)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· 分享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 端网页特效

导航

< 2025年3月 >
23 24 25 26 27 28 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 1 2 3 4 5
点击右上角即可分享
微信分享提示