webpack5+react18+ts+antd5 作弊表

2,315 阅读13分钟

webpack5+react18+ts+antd5 作弊表

webpack5发布已经两年多了,同时5版本也对很多原有的功能进行了优化,阔别两年,webpack 5 正式发布了!,react18也稳定没有发现什么问题了,接下从零开始搭建一套webpack + react18 + ts + antd5的开发环境

1.初始化项目

npm init -y

初始化之后,你的目录应该是这样的

1.jpg

安装webpackwebpack-cli

npm i webpack webpack-cli -D

安装react相关的依赖

npm i react react-dom -S

由于是涉及ts,所以需要安装对应的类型依赖

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

2.添加关于react+ts相关代码

需要在public文件夹下添加模板相关的内容

<!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>myWebpack-react-ts</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>

编写App.tsx内容

// src/App.tsx

import React from 'react'
export const App =()=>{
  return (
    <div>
      <h3>webpack5+react18+ts+antd5 作弊表</h3>
    </div>
  )
}

编写index.tsx内容

// src/index.tsx
import React from "react";
import {createRoot} from "react-dom/client"
import {App} from "./App"

const root = document.querySelector('#root') as HTMLElement
createRoot(root).render(
  <React.StrictMode>
    <App/>
  </React.StrictMode>
)

编写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", // react18这里也可以改成react-jsx
  },
  "include": ["./src"]
}

目前关于react+ts方面的代码内容已经添加好了,接下来需要配置相关的webpack代码了

3.配置webpack的公共配置

由于需要分开发环境和生成环境以及公共配置代码的抽离,所以会在build文件夹下创建三个文件; 公共配置代码文件:webpack.base.js,开发环境:webpack.dev.js,生成环境:webpack.prod.js

修改公共配置代码文件webpack.base.js

1. 配置入口文件

// webpack.base.js
const path = require('path');

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

2. 配置出口文件

// webpack.base.js
const path = require('path');

module.exports = {
  // 入口文件 
  ...
  // 出口文件
  output: {
    filename: 'static/js/[name].[chunkhash:8].js',
    path: path.join(__dirname, '../dist'),
    // 如果是 webpack4 需要配置clean-webpack-plugin来删除dist文件, webpack5则内置了
    clean: true,  
    // 打包后文件的公共前缀路径
    publicPath: '/'
  },
}

3. 配置loader解析ts和jsx webpack默认是无法识别js结尾以外的文件的,包括jsx也是不能识别的,所以需要配置一些loder和预设来让tsx(@babel/preset-typescript)转换成js,然后再通过(@babel/preset-typescript)预设来识别jsx语法

  1. 安装babel核心模块和babel预设
npm i babel-loader @babel/core @babel/preset-react @babel/preset-typescript -D
  1. 需要在webpack.base.js文件中添加
module.exports = {
  ....
  module: {
    rules: [
      {
        test: /.(ts|tsx)$/,
        use: { 
          loader: 'babel-loader', 
          options: { 
            // 预设执行顺序由右往左,所以先处理ts,再处理jsx 
            presets: [ '@babel/preset-react', '@babel/preset-typescript' ] 
          } 
        }
      {
    ]
  }
}
  1. 配置extensions extensions主要用于省略引入的后缀,例如像jststsx这些高频出现的后缀,如何配置:
// webpack.base.js 
module.exports = { 
  // ... 
  resolve: { 
    extensions: ['.js', '.tsx', '.ts']   // 注意把高频出现的文件后缀放在前面
  } 
}

配置省略后缀,不适宜配置过多,会影响你的构建速度 4. 添加html-webpack-plugin插件 想要在浏览器运行,需要有一个html文件,但是打包之后会有很多js、css等文件,这些文件也是需要引入到html文件里面才行,这时候并不希望手动引入,我们希望的是提供一个模板,使用插件来自动引入对应的静态资源

npm i html-webpack-plugin -D
module.exports = {
  // ....
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../public/index.html'),
      inject: true,  // 自动注入静态资源
    })
  ]
}

目前关于webpack公共配置代码就基本已经编写好了,接下来需要区分一些开发环境生成环境(也叫打包)

4.配置webpack开发环境的代码

开发环境需要有自己的server

  1. 安装webpack-dev-server 开发环境的代码都是写在webpack.dev.js文件里面,最后使用webpack-merge一下公共配置的代码即可
npm i webpack-dev-server webpack-merge -D
// webpack.dev.js

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, // 解决history路由404问题
    static: {
      directory: path.join(__dirname, "../public")  //托管静态资源public文件夹
    }
  }
})
  1. 需要在package.json中添加脚本命令,方便启动
// package.json

"scripts": { 
  "dev": "webpack-dev-server -c build/webpack.dev.js" ,
  "start": "npm run dev"
},

执行npm start这时候一个初步的项目就启动起来了

5.配置生成环境

  1. 生成环境代码都在webpack.prod.js里面
// webpack.prod.js 

const { merge } = require('webpack-merge') 
const baseConfig = require('./webpack.base.js') 
module.exports = merge(baseConfig, { 
  // 生产模式,会开启tree-shaking和压缩代码,以及其他优化
  mode: 'production', 
})
  1. 需要在package.json中添加脚本命令,方便打包
"scripts": { 
  "dev": "webpack-dev-server -c build/webpack.dev.js", "build": "webpack -c build/webpack.prod.js" 
},

执行npm run build,最终打包在dist文件中;打包完我们如何查看内容是否和我们本地开发的内容是一致的呢
可以借助node提供的serve打开

npm i serve -g

安装完成之后,只需要进入你的根目录执行serve -s dist,这样就可以运行你打包的内容了

目前算是完成了一个最基础的react+ts+webpack5的配置,但还有很多功能没有实现

6.基础功能的配置

一:环境配置

  1. 配置环境变量 其主要作用:
  • 区分开发模式还是生成模式
  • 区分是测试环境还是开发环境、模拟上线的环境、上线环境

可以死记process.env.NODE_ENV这串东西可以实现环境的区分,我们可以借助cross-envwebpack.DefinePlugin来自定义一个环境变量

  • cross-env:兼容各系统设置的环境变量
  • webpack.DefingPlugin:webpack内置的插件,这样可以让你在业务代码里面拿到这个环境变量
  1. 安装
npm i cross-env -D
  1. 修改package.json
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev:dev": "cross-env NODE_ENV=development BASE_ENV=development webpack-dev-server --progress -c build/webpack.dev.js",
    "dev:test": "cross-env NODE_ENV=development BASE_ENV=test webpack-dev-server --progress -c build/webpack.dev.js",
    "dev:pre": "cross-env NODE_ENV=development BASE_ENV=pre webpack-dev-server --progress -c build/webpack.dev.js",
    "dev:prod": "cross-env NODE_ENV=development BASE_ENV=production webpack-dev-server --progress -c build/webpack.dev.js",
    
    "build:dev": "cross-env NODE_ENV=production BASE_ENV=development webpack --progress -c build/webpack.prod.js",
    "build:test": "cross-env NODE_ENV=production BASE_ENV=test webpack --progress -c build/webpack.prod.js",
    "build:pre": "cross-env NODE_ENV=production BASE_ENV=pre webpack --progress -c build/webpack.prod.js",
    "build:prod": "cross-env NODE_ENV=production BASE_ENV=production webpack --progress -c build/webpack.prod.js",
    "start": "npm run dev:dev",
  },

一般正规的流程都是这样,dev是开发模式,build是打包模式;其后面对应的dev/test/pre/prod对应着不同的业务环境。

我们还可以在webpack.base.js文件里面打印一下环境变量来确认是否正确

// webpack.base.js

console.log('NODE_ENV', process.env.NODE_ENV) 
console.log('BASE_ENV', process.env.BASE_ENV)

然后执行npm run build:dev,可以在执行界面看到

NODE_ENV production
BASE_ENV development

现在说明是生成模式,但是业务环境还是开发模式,如果你业务代码里面需要用到这些环境变量做一些判断,需要用到webpack.DefinePlugin插件, 一般在你封装axios的时候会用到

// webpack.base.js

const webpack = require('webpack') 
module.export = { 
  // ... 
  plugins: [ 
    // ... 
    new webpack.DefinePlugin({ 
      'process.env.BASE_ENV': JSON.stringify(process.env.BASE_ENV) 
    }) 
  ] 
}

这样你就可以在你组件里面打印了

二:处理样式文件配置

如果你组件里面写有样式相关的,打包会报错,因为webpack默认不支持css文件,报错信息也会告诉你需要一些loader来解析css

  1. 安装依赖(这里主要是以less来举例)
npm i style-loader css-loader -D
  1. webpack.base.js中加入配置项
// webpack.base.js 

// ... 
module.exports = { 
  // ... 
  module: { 
    rules: [ 
      // ... 
      { 
        test: /.css$/, //匹配 css 文件 
        use: ['style-loader','css-loader'] 
      } 
    ] 
  }, 
  // ... 
}

注意:loader解析是从右往左的,style-loader主要是把解析后的css代码从js中抽离,放到头部的style标签中;css-loader主要用于解析css文件代码;经历这些配置之后,打包会发现不报错了,而且通过serve -s dist启动,可以在浏览器中看到样式生效了

  1. 让webpack支持less
npm i less-loader less -D
  • less-loader用于解析less文件,把less编译为css
  • less即less的核心

只需要在rules中的use中添加一下即可

// webpack.base.js 

module.exports = { 
  // ... 
  module: { 
    rules: [ 
      // ... 
      { 
        test: /.css$/, //匹配 css 文件 
        use: ['style-loader','css-loader', 'less-loader'] 
      } 
    ] 
  }, 
  // ... 
}
  1. 处理css3前缀兼容问题 有的时候,需要兼容一些低版本的浏览器,此时就需要给css加前缀了,因此我们可以借助postcss-loader来给css3添加浏览器私有前缀
npm i postcss-loader autoprefixer -D
  • postcss-loader:用于处理css时自动添加浏览器私有前缀
  • autoprefixer:决定添加哪些浏览器前缀到css中

需要在webpack.base.js中加入规则

// webpack.base.js 

module.exports = { 
  // ... 
  module: { 
    rules: [ 
      // ... 
      { 
        test: /.css$/, //匹配 css 文件 
        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版本,这时候你打包,可以在浏览器中看到私有前缀已经加上了

上面的postcssOptions还可以去掉,只不过需要在根目录中创建一个postcss.config.js文件,二者是等价了,里面的内容是

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

你的webpack.base.js就可以变成

// webpack.base.js 

module.exports = { 
  // ... 
  module: { 
    rules: [ 
      // ... 
      { 
        test: /.css$/, //匹配 css 文件 
        use: [ 
          'style-loader', 
          'css-loader', 
          'postcss-loader',
          'less-loader' 
        ]
      } 
    ] 
  }, 
  // ... 
}

三:babel配置

为什么需要babel,因为目前浏览器不能够识别高版本的js语法,具体用法和配置如下:

  1. 安装依赖
npm i babel-loader @babel/core @babel/preset-env core-js -D
  • babel-loader:将高版本的js(指的是es6及以上)代码转换为es5
  • @babel/core:babel核心编译包
  • @babel/preset-env:babel预设
  • core-js:使用低版本js语法模拟高版本
  1. 配置
// webpack.base.js

module.exports = {
  module: {
    rules: [
      {
        test: /.(ts|tsx)$/,
        use: 'babel-loader',
        options: {
          // 执行顺序由右往左,所以先处理ts,再处理jsx,最后再试一下babel转换为低版本语法
          presets: [
            "@babel/preset-env",
            {
              // 根据配置的浏览器兼容,以及代码中使用到的api进行引入polyfill按需添加
              "useBuiltIns": "usage",
              // 配置使用core-js低版本
              "corejs": 3,
            }
          ],
          '@babel/preset-react', 
          '@babel/preset-typescript'
        }
      }
    ]
  }
}

此时你打包会发现语法会转换成浏览器兼容的语法了

同时上面的配置项也可以抽离出来的,需要在根目录下新建一个babel.config.js文件

// babel.config.js

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      },
    ],
    // 预设执行顺序由右往左,所以先处理ts,再处理jsx
    '@babel/preset-react',
    '@babel/preset-typescript'
  ]
}

这时候你的webpack.base.js就可以写成

// webpack.base.js

module.exports = {
  module: {
    rules: [
      {
        test: /.(ts|tsx)$/,
        use: 'babel-loader',
      }
    ]
  }
}

由于是react开发,可能会用到装饰器,默认是不支持的,想要支持,你需要这样做

  1. tsconfig.json文件中开启一下
// tsconfig.json 

{ 
  "compilerOptions": { 
  // ... 
  // 开启装饰器使用 
  "experimentalDecorators": true 
  } 
}
  1. 需要安装插件
npm i @babel/plugin-proposal-decorators -D
  1. babel.config.js中添加插件
module.exports = {
  // ...
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }]
  ]
}

这时候你的项目就可以支持装饰器了

四:复制静态资源文件

很多时候,public文件夹中都会放一些静态资源文件,例如图片,字体图标,icon等,通常这些也是需要复制到你的出口文件中(dist)才能保证你打包出来的内容能正常展示,这时候我们可以利用插件实现

  1. 安装
npm i copy-webpack-plugin -D
  1. 配置,打包的时候才需要此插件,所以需要在webpack.prod.js文件中配置
// webpack.prod.js

// ...
  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
          }
        }
      ]
    }),
  ]

五:图片文件配置处理

相对于webpack4 处理图片文件需要用到file-loaderurl-loader,webpack5采用的是asset-module

  1. webpack.base.js中添加解析图片的配置
// webpack.base.js

module.exports = {
  module: {
    rules: [
      // ...
      {
        // 匹配以这些结尾的图片文件
        test: /.(png|jpg|jpeg|gif|svg)$/,  
        type: "asset",
        parser: {
          // 小于10k就会转成base64
          dataUrlCondition: {
            maxSize: 10 * 1024
          }
        },
        generator: {
          // 文件输出目录和命名
          filename:'static/img/[name][ext]' 
        }
      },
    ]
  }
}

注意:如果你使用了图片测试,ts项目会报找不到模块或相应的类似声明错误,需要添加一个关于图片的声明文件

可以创建一个img.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'

六:字体和媒体文件的配置处理

配置和图片文件的处理的配置差不多

webpack.base.js中添加配置

// webpack.base.js

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]'  
        }
      }
    ]
  }
}

7.react模块热更新的配置

webpack5开启热更新比webpack4要简单多了,webpack4还需要借助HotModuleReplacementPlugin插件才能实现,而webpack5只需要在devServer对象中写上hot:true即可

但是样式修改目前并不是热更新,而是整体刷新了浏览器,我们可以这样测试,写一个组件,里面有一个输入框,然后再修改样式,保存之后看输入框的值有没有重置即可

2.jpg 经过测试发现是重置的,这很明显不是我们想要的;

可以借助@pmmmwh/react-refresh-webpack-plugin来实现样式的热更新,此插件依赖于react-refresh

  1. 安装
npm i @pmmmwh/react-refresh-webpack-plugin react-refresh -D
  1. 配置,开发环境才需要热更新,所以需要在webpack.dev.js中配置
// webpack.dev.js 

const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); module.exports = merge(baseConfig, { 
  // ... 
  plugins: [ 
    // 添加热更新插件 
    new ReactRefreshWebpackPlugin(), 
  ] 
})
  1. 还需要在babel.config.js文件中为babel-loader配置一下
// babel.config.js

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

这次修改样式再测试则会发现输入框的中并没有重置了,也证明样式的热更新成功

3.jpg

8.构建速度的优化

一般在优化之前我们需要知道时间花费在了哪些步骤上面,这时候我们需要依赖speed-measure-webpack-plugin插件来进行分析

npm i speed-measure-webpack-plugin -D
  1. 在build文件夹中新增一个webpack.analy.js文件
const prodConfig = require('./webpack.prod')
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin') // 引入webpack打包速度分析插件

const smp = new SpeedMeasurePlugin()
const {merge} = require('webpack-merge')

// 使用smp.wrap方法,把生产环境配置传进去
module.exports = smp.wrap(merge(prodConfig, {
}))
  1. 在package.json中新增一条命令用于分析打包
{ 
  // ... 
  "scripts": { 
  // ... 
  "build:analy": "cross-env NODE_ENV=production BASE_ENV=production webpack -c build/webpack.analy.js" 
  } 
  // ... 
}
  1. 执行npm run build:analy可以看到

4.jpg

这里可以看到各loader和插件的耗时

由于目前项目较小,看到的耗时都是比较小的,等项目变大之后可以针对性的分析和优化

下面介绍一些比较通用的优化手段

1. 开启持久化缓存

webpack5之前的js缓存是在babel-loader中开启的,css方面的缓存是利用cache-loader来进行,模块的缓存是利用hard-source-webpack-plugin,配置好之后,第一次时间上没看到差别,第二次通过文件的哈希对比,如果一致则采用上一次缓存的结果,这样能极大的节省时间

webpack5相对于webpack4新增了持久化缓存,同时也改进缓存算法的优化,可以通过配置来缓存生成的webpack模块和chunk来改善下一次打包的构建速度,提升了约90%的速度,具体的配置操作也较为简单

// webpack.base.js
module.exports = {
  // ...
  // 开启webpack持久化存储缓存
  cache: {
    type: 'filesystem', // 使用文件缓存
  },
}

经测试发现

开发模式下第一次耗时 11142s, 第二次耗时: 692s

5.jpg 打包的耗时,第一次耗时 2227 ms, 第二次耗时: 704s

2. 开启多线程

webpack的loader默认是单线程执行的,我们可以借助cpu开启多线程loader,这样可以极大的提升loader解析的速度,thread-loader是用来开启多线程的loader

安装依赖

npm i thread-loader -D

需要注意,thread-loader需要放置在其它loader的前面

加入配置

// webpack.base.js 

module.exports = { 
  // ... 
  module: { 
    rules: [ 
      { 
        test: /.(ts|tsx)$/, 
        use: [
          'thread-loader', 'babel-loader'
        ] 
      } 
    ] 
  } 
}

但是此时你看构建时间却比没有开启多线程之前要慢,这是因为项目比较小,多线程适合较大的项目;等你的项目构建时间将近一分钟的时候,此时你开启多线程,会看到明显的提速

3. 缩小loader作用范围

通常情况下像node_modules和一些第三方库,这些都是已经处理好了的,不需要loader去解析它们,因此我们可以通过includeexclude这两个配置项目去控制解析,从而节省解析时间

  • include:只解析选择的配置项
  • exclude:不解析选择的配置模块

如何操作?

// webpack.base.js 

const path = require('path') 
module.exports = { 
  // ... 
  module: { 
    rules: [ 
      { 
        // 只对项目src文件的ts,tsx进行loader解析 
        include: [path.resolve(__dirname, '../src')], 
        test: /.(ts|tsx)$/, 
        use: ['thread-loader', 'babel-loader'] 
      } 
    ] 
  } 
}

4. 缩小模块搜索范围

通常情况下node模块分为三种

  • node核心模块
  • node_modules模块
  • 自定义文件模块
// webpack.base.js 

const path = require('path') 
module.exports = { 
  // ... 
  resolve: { 
    // ... 
    modules: [
      // 查找第三方模块只在本项目的node_modules中查找
      path.resolve(__dirname, '../node_modules')
    ],  
  }
}

5. devtool 配置

在开发中和打包后的代码都是经过webpack处理后的代码,开发中肯定是希望看到源代码的,方便调试;这时候我们就需要选择不同的source map,意思是映射源代码,而且选择不同的映射会明显的影响构建速度,devtool配置项就是webpack提供给我们选择不同的映射源码的配置

官网推荐开发环境使用的是:eval-cheap-module-source-map

  • 但是本地开发首次打包会慢些,主要原因是eval做了缓存,但是热更新会很快
  • 开发中,定位到行即可,所以加上cheap
  • 我们希望能够找到源代码的错误,而不是打包后的,所以需要加上 module
// webpack.dev.js 

module.exports = { 
  // ... 
  devtool: 'eval-cheap-module-source-map' 
}

打包环境则可以注释掉这个配置项,或者写成none;但是none看到的是编译后的代码,好处是不会泄露源代码,打包速度较快;坏处是不方便排查线上问题。

9. 构建结果文件优化

构建之前我们可以通过webpack-bundle-analyzer插件分析,该插件会以图形化的方式去展示webpack打包后各个文件的大小

6.jpg 怎么实现?

const prodConfig = require('./webpack.prod')
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin') // 引入webpack打包速度分析插件

const smp = new SpeedMeasurePlugin()
const {merge} = require('webpack-merge')

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') // 引入分析打包结果插件

// 使用smp.wrap方法,把生产环境配置传进去
module.exports = smp.wrap(merge(prodConfig, {
  plugins: [
    // 配置分析打包结果插件
    new BundleAnalyzerPlugin() 
  ]
}))

然后再执行npm run build:analy命令,打包完成之后,它会自动打开浏览器窗口,以图形化方式展示出来

1. 抽离css样式文件

由于开发环境css是嵌入在style标签里面的,好处是方便样式热替换,但是在打包的时候,是希望把css单独抽离出来,从而方便实现缓存。此时mini-css-extract-plugin插件就可以实现

安装依赖

npm i mini-css-extract-plugin -D

添加配置项

// webpack.base.js 

// ... 
const MiniCssExtractPlugin = require('mini-css-extract-plugin') 
// 是否是开发模式 
const isDev = process.env.NODE_ENV === 'development'
module.exports = { 
  // ... 
  module: { 
    rules: [ 
      // ... 
      { 
        test: /.css$/, //匹配所有的 css 文件 
        include: [
          path.resolve(__dirname, '../src')], 
          use: [ 
            // 开发环境使用style-looader,打包模式抽离css 
            isDev ? 'style-loader' : MiniCssExtractPlugin.loader, 
            'css-loader', 
            'postcss-loader' 
          ] 
      }, 
      { 
        test: /.less$/, //匹配所有的 less 文件 
        include: [
          path.resolve(__dirname, '../src')], 
          use: [ 
            // 开发环境使用style-looader,打包模式抽离css 
            isDev ? 'style-loader' : MiniCssExtractPlugin.loader, 
            'css-loader', 
            'postcss-loader', 
            'less-loader' 
          ] 
        }, 
      ] 
    }, 
  // ... 
}

还需修改webpack.prod.js,在打包时候抽离css

// webpack.prod.js 

// ... 
const MiniCssExtractPlugin = require('mini-css-extract-plugin') 
module.exports = merge(baseConfig, { 
  mode: 'production', 
  plugins: [ 
    // ... 
    // 抽离css插件 
    new MiniCssExtractPlugin({ 
      // 抽离css的输出目录和名称
      filename: 'static/css/[name].css'  
    }), 
  ] 
})

这样打包时候css会抽离到单独的css文件,开发模式下仍然保留css嵌入到style标签的方式

2. 压缩css文件

我们经过上面的配置之后,重新打包,会发现css没有压缩了,此时我们还需要利用css-minimizer-webpack-plugin插件来实现压缩css

npm i css-minimizer-webpack-plugin -D
// webpack.prod.js 

// ... 
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') 
module.exports = { 
  // ... 
  optimization: { 
    minimizer: [ 
      // 压缩css
      new CssMinimizerPlugin(),  
    ], 
  }, 
}

再次执行打包命令,你就可以看到css已经被压缩了

3. 压缩js文件

这时候会有疑问为什么还需要压缩js呢,如果此时打开你打包的js文件看,会发现,js并没有压缩;这是为什么呢?原因是当你配置了optimization.minimizer压缩css后,js压缩就会失效;如何解决?需要安装一个插件

npm i terser-webpack-plugin -D

修改webpack.prod.js文件

// 压缩js
new TerserPlugin({
  parallel: true, // 开启多线程压缩
  terserOptions: {
    compress: {
      // 删除console.log
      pure_funcs: ["console.log"]
    }
  }
}),

这样你的css和js都能得到压缩了

4. 合理使用hash

我们在开发的时候,修改一部分代码,我们是希望这一部分代码更新,而不是整体更新,这时候我们可以配置文件缓存来提升页面的加载速度从而减少服务器的压力,而hash刚好是作为浏览器缓存很重要的一部分。webpack打包的hash分为三种

  • hash:只要你的项目文件有修改,构建出来的hash的值都会发生改变,而且所有文件都是共用这个hash值的
  • chunkhash:主要是针对不同入口文件来进行的文件依赖解析,从而构建出对应的chunk,并生成对应的hash值,只有你文件本身或者依赖的文件有修改,chunkhash值才会发生变化
  • contenthash:每个文件都会自己单独的hash值,只有文件改动才会影响自身的hash值

格式:filename: "[name].[chunkhash:8][ext]"

通常情况下我们在生产环境的时候,会把一些公共库和代码入口文件区分,单独打包构建,并采用chunkhash的方式生成hash值,只要不去修改这些公共库的代码,其hash值就不会发生变化,这样就可以命中浏览器缓存;js比较适用chunkhash,而css和图片比较适用contenthash

如何配置:

// webpack.base.js

module.exports = {
  output: {
    filename: 'static/js/[name].[chunkhash:8].js',
  },
  module: {
    rules: [
      { 
        test:/.(png|jpg|jpeg|gif|svg)$/, // 匹配图片文件 
        // ... 
        generator:{ 
          // 加上[contenthash:8] 
          filename:'static/images/[name].[contenthash:8][ext]' 
        }, 
      },
    ]
  }
}

css也是同理加上,然后再次打包的时候就可以看到文件后面的hash值了

5. 代码分割第三方报和公共模块资源

通常情况下第三方报的代码的变化频率是极小的,node_modules可以单独打包,第三方报的代码没有变化时,其对应的chunkhash值是不会发生变化的,这样可以有效的命中浏览器缓存,同时第三方模块也可以提取出来,避免重复打包加大代码包的体积;而这些功能webpack都是提供了的

如何配置:

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就提取出来
      }
    }
  }
},

7. tree-shaking清理未引用js

tree-shaking 顾名思义就是摇树,在代码中的意思就是把无引用的代码清理掉,模式mode为production时会自动开启tree-shaking

tree-shaking也能清理未使用的css

不过需要安装插件

npm i purgecss-webpack-plugin glob-all -D

如何配置:

// webpack.prod.js

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-开头的类名,哪怕没用到也不删除
  }
}),

注意:safelist相当于白名单,你想那些类名不被过滤就在此加入即可

8. 懒加载

想单页面应用,打包的时候是会默认全部js资源都打包到一个文件中,这些包体积就会极大,从而影响首屏加载速度

懒加载可以实现每一个组件都打包成一个js,webpack是默认支持懒加载的

以react为例:

import React, { lazy, Suspense, useState } from 'react' 
const A = lazy(() => import('@/components/A'))

function App(){
  return (
    <>
      <Suspense fallback={<div>加载中</div>}><A /></Suspense>
    </>
  )
}
export default App

这样就是懒加载了

9.打包生成gizp

为什么需要gzip?

由于代码是运行在浏览器的,浏览器需要从服务中把html、css、js(称为静态资源文件)等资源进行下载,而下载的体积越小,页面的加载速度就会越快。通常采用的手段就是gzip

gzip压缩可以做到大大减小静态资源文件,压缩率在70%左右

nginx可以配置gzip,但是这是服务器层面实现的,在每次请求的资源的时候都会对资源进行压缩,这样也是需要时间的,目前更好的方式是前端在打包的时候就直接生成了gzip,服务器一旦收到请求的时候,就可以直接把压缩好的gzip文件返回给浏览器了,从而节省时间

怎么做?

npm i compression-webpack-plugin -D
// webpack.prod.js

const CompressionPlugin  = require('compression-webpack-plugin')
module.exports = {
  plugins: [ 
    // ... 
    new CompressionPlugin({
      test: /\.(js|css)$/, // 只生成css,js压缩文件
      filename: '[path][base].gz', // 文件命名
      algorithm: 'gzip', // 压缩格式,默认是gzip
      threshold: 10240, // 只有大小大于该值的资源会被处理。默认值是 10k
      minRatio: 0.8 // 压缩率,默认值是 0.8
    })
  ]
}

配置完成之后,打包你就可以看到目录下会多出一个.gz结尾的文件了,这个就是gzip文件