likes
comments
collection
share

Webpack 5.0 从零开始搭建 React 项目

作者站长头像
站长
· 阅读数 52

前言

使用最新的 webpack5 一步一步从零搭建一个完整的 react18 + ts 开发和打包环境,配置完善的模块热替换以及构建速度构建结果的优化。

初始化项目

先手动初始化一个基本的 react + ts 项目

mkdir webpack5-react-ts
npm init -y

初始化好 package.json 后,在项目下新增以下目录结构和文件

├── build
|   ├── webpack.base.js # 公共配置
|   ├── webpack.dev.js  # 开发环境配置
|   └── webpack.prod.js # 打包环境配置
├── public
   └── index.html # html模板
├── src
|   ├── App.tsx 
   └── index.tsx # react应用入口页面
├── tsconfig.json  # ts配置
└── package.json

安装 webpack 依赖

npm i webpack webpack-cli -D

安装 react 依赖

npm i react react-dom

安装 react 类型依赖

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

添加 public/index.html 内容

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>webpack-react-ts</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>

添加 tsconfig.json 内容

{
  "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"]
}

如果你不想直接复制也不想全局安装 typescript ,可以使用自动生成

npx -p typescript tsc --init

添加 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 />)
}

添加 webpack 配置

webpack 公共配置

修改 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].js', // 每个输出js的名称
    path: path.join(__dirname, '../dist'), // 打包结果输出路径
    clean: true, // webpack4需要配置clean-webpack-plugin来删除dist文件,webpack5内置了
    publicPath: '/' // 打包后文件的公共前缀路径
  },
}

3.配置 loader 解析 ts 和 jsx

babel-loader 的主要作用是将 JavaScript 文件传递给 Babel 编译器进行转译(编译工作主要由 @babel/core 来处理),并将编译后的结果返回给 Webpack 进行后续处理。 另由于 webpack 默认只能够识别 js 文件,不能识别 ts、jsx 语法,所以需要配置 loader 的预设  @babel/preset-typescript 来将 ts 语法转换为 js 语法;预设  @babel/preset-react 来将 jsx 转换为 js。

安装babel核心模块和babel预设

npm i babel-loader @babel/core @babel/preset-react @babel/preset-typescript -D

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

// webpack.base.js
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.(ts|tsx)$/, // 匹配.ts, tsx文件
        use: {
          loader: 'babel-loader',
          options: {
            // 预设执行顺序由右往左,所以先处理ts,再处理jsx
            presets: [
              '@babel/preset-react',
              '@babel/preset-typescript'
            ]
          }
        }
      }
    ]
  }
}

4.配置 extensions

extensionswebpackresolve 解析配置下的选项,如果引入模块时不带文件后缀,会到该配置数组里面按照从左到右的顺序依次添加后缀查找文件,因为 webpack 不支持引入以 .ts, tsx为后缀的文件,所以要在 extensions 中配置,而第三方库里面很多引入 js 文件没有带后缀,所以也要配置下 js。

5.添加 html-webpack-plugin 插件

webpack 需要把最终构建好的静态资源都引入到一个 html 文件中,这样才能在浏览器中运行,html-webpack-plugin 就是用来做这件事情的

npm i html-webpack-plugin -D

因为该插件在开发模式和构建打包模式都会用到,所以放在公共配置 webpack.base.js 里面

// webpack.base.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, // 自动注入静态资源
    })
  ]
}

现在一个最基础的 webpack 基本公共配置就已经配置好了,接下来需要在此基础上分别配置 dev(开发环境)build(打包环境)

webpack 开发环境配置

1.安装 webpack-dev-server

开发环境配置需要写在 webpack.dev.js 文件中,并且需要借助 webpack-dev-server 在开发环境启动服务器来辅助开发,还需要依赖 webpack-merge 来合并基本配置,安装依赖:

npm i webpack-dev-server webpack-merge -D

修改 webpack.dev.js 代码,合并公共配置,并添加开发模式配置

// 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, // 服务端口号
    compress: false, // gzip压缩,开发环境不开启,提升热更新速度
    hot: true, // 开启热更新,后面会讲react模块热替换具体配置
    historyApiFallback: true, // 解决history路由404问题
    static: {
      directory: path.join(__dirname, "../public"), //托管静态资源public文件夹
    }
  }
})

2.package.json 添加 dev 命令脚本

package.jsonscripts 中添加

// package.json
"scripts": {
  "dev": "webpack-dev-server -c build/webpack.dev.js"
},

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

webpack 打包环境配置

1.修改 webpack.prod.js 代码

// webpack.prod.js

const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base.js')
module.exports = merge(baseConfig, {
  mode: 'production', // 生产模式,会开启tree-shaking和压缩代码,以及其他优化
})

2.package.json 添加 build 打包命令脚本

package.jsonscripts 中添加

"scripts": {
    "dev": "webpack-dev-server -c build/webpack.dev.js",
    "build": "webpack -c build/webpack.prod.js"
},

执行 npm run build,最终打包在dist文件中,打包结果:

dist                    
├── static
|   ├── js
|     ├── main.js
├── index.html

3.浏览器查看打包结果

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

npx serve -s dist      

现在一个基础的支持 react + tswebpack5 配置项目就搭建好了,但只有这些功能是远远不够的,下面还需要继续添加其他配置。

项目基础功能配置

配置环境变量

环境变量按作用可以分为两种

  1. 区分是开发模式还是打包模式
  2. 区分项目业务环境,开发、测试、预发布、正式环境

区分开发模式还是打包模式可以用 process.env.NODE_ENV ,因为很多第三方包里面判断都是采用的这个环境变量。

区分项目接口环境可以自定义一个环境变量 process.env.BASE_ENV,设置环境变量可以借助 cross-envwebpack.DefinePlugin 来设置。

  • cross-env:支持跨平台设置环境变量
  • webpack.DefinePluginwebpack 内置的插件,可以为业务代码注入环境变量

安装 cross-env

npm i cross-env -D

修改 package.jsonscripts 脚本字段,删除原先的 devbuild ,改为

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

dev 开头是开发模式,build 开头是打包模式,冒号后面对应的dev/test/pre/prod是对应的业务环境的开发/测试/预发布/正式环境。

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

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

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

修改 webpack.base.js

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

配置后 webpack 会把环境变量值注入到业务代码里面去,webpack 解析代码匹配到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)

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

// 当前是开发环境,业务环境是测试环境
// NODE_ENV development
// BASE_ENV test

处理 css 和 sass 文件

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-react-ts</h2>
}
export default App

执行打包命令 npm run build:dev,会发现有报错,因为 webpack 默认只认识 js,是不识别 css 文件的,需要使用 loader 来解析 css,安装依赖

npm i style-loader css-loader -D
  • css-loadercss-loader 用于处理 CSS 文件,将 CSS 文件转换为 JavaScript 模块。它会分析 CSS 文件的依赖关系,并将其转换为一个包含 CSS 样式的 JavaScript 对象。这样,CSS 样式就可以在 JavaScript 代码中被引用和使用。css-loader 的主要作用是解析 CSS 文件,并处理其中的 @importurl() 等语法,以及处理 CSS 模块化、压缩等功能。在 Webpack 配置文件中,通常将 css-loaderstyle-loader 进行链式配置,以实现将 CSS 样式注入到最终的 HTML 页面中。
  • style-loaderstyle-loader 用于将 CSS 样式注入到 HTML 页面中。它接收由 css-loader 转换后的 CSS 样式对象,并将其以内联样式的形式添加到 HTML 页面的 <style> 标签中或通过添加 <link> 标签引入样式文件。style-loader 的主要作用是将 CSS 样式应用到页面上,使其生效并展示。

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

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

因为 loader 执行顺序是从右往左从下往上的,匹配到 css 文件后先用 css-loader解析 css, 最后借助 style-loadercss 插入到头部 style 标签中。

配置完成后再 npm run build:dev 打包,借助 npx serve -s dist 启动后在浏览器查看,可以看到样式生效了。

支持 less 或 scss

项目开发中,为了更好的提升开发体验,一般会使用 css 超集 less 或者 sass,对于这些超集也需要对应的 loader 来识别解析。以 sass 为例,需要安装依赖:

npm i sass-loader sass -D

只需要在 rules 中添加 scss 文件解析,遇到 scss 文件,使用 sass-loader 解析为 css,再进行 css 解析流程,修改 webpack.base.js

// webpack.base.js
module.exports = {
  // ...
  module: {
    // ...
    rules: [
      // ...
      {
        test: /.(css|scss)$/, //匹配 css, scss 文件
        use: ['style-loader','css-loader', 'sass-loader']
      }
    ]
  },
  // ...
}

这样就也可以在项目中使用 scss 文件了

处理 css3 前缀兼容

虽然 css3 现在浏览器支持率已经很高了,但有时候需要兼容一些低版本浏览器,需要给 css3 加前缀,可以借助插件来自动加前缀,postcss-loader 就是来给 css3 加浏览器前缀的,安装依赖:

npm i postcss-loader autoprefixer -D
  • postcss-loader:处理 css 时自动加前缀
  • autoprefixer:决定添加哪些浏览器前缀到 css

修改 webpack.base.js,在解析 cssscss 的规则中添加配置

module.exports = {
  // ...
  module: { 
    rules: [
      // ...
      {
        test: /.(css|scss)$/, //匹配 css, scss 文件
        use: [
          'style-loader',
          'css-loader',
          // 新增
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: ['autoprefixer']
              }
            }
          },
          'sass-loader'
        ]
      }
    ]
  },
  // ...
}

配置完成后,需要有一份要兼容浏览器的清单,让 postcss-loader 知道要加哪些浏览器的前缀,在根目录创建 .browserslistrc文件

IE 9 # 兼容IE 9
chrome 35 # 兼容chrome 35

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

babel 预设处理 js 兼容

JavaScript 新增很多方便好用的标准语法来方便开发,甚至还有非标准语法比如装饰器等,都极大的提升了代码可读性和开发效率,但前者标准语法在很多低版本浏览器上不被支持,后者非标准语法所有的浏览器都还暂不支持。所以需要借助 Babel 把最新的标准语法和非标准语法转换为浏览器兼容性更好的低版本语法,这里只讲配置,更详细的可以看 Babel 那些事儿。安装依赖:

npm i @babel/preset-env core-js -D

必要的几个模块中 babel-loader@babel/core 上面已经安装过了

  • @babel/preset-env:它可以配合 core-js ,根据目标环境自动确定需要进行的转译和 polyfill 的配置,将现代的 JavaScript 语法转换为目标环境所支持的较旧的语法。
  • core-js:是一个 JavaScript 标准库,它提供了一组用于填充缺失功能的 Polyfill。

修改 webpack.base.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.jsbabel-loaderoptions 配置

// webpack.base.js
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.(ts|tsx)$/,
        use: 'babel-loader'
      },
      // 如果node_moduels中也有要处理的语法,可以把js|jsx文件配置加上
      // {
      //  test: /.(js|jsx)$/,
      //  use: 'babel-loader'
      // }
      // ...
    ]
  }
}

babel 处理 js 非标准语法

现在 react 主流开发都是函数组件和 react-hooks,但有时也会用类组件,可以用装饰器简化代码。新增 src/components/Class.tsx 组件,在 App.tsx 中引入该组件使用

import React, { PureComponent } from "react";

// 装饰器为,组件添加age属性
function addAge(Target: Function) {
  Target.prototype.age = 111
}
// 使用装饰圈
@addAge
class Class extends PureComponent {

  age?: number

  render() {
    return (
      <h2>我是类组件---{this.age}</h2>
    )
  }
}

export default Class

这里需要开启一下 ts 装饰器语法支持,修改 tsconfig.json 配置

// tsconfig.json
{
  "compilerOptions": {
    // ...
    // 开启装饰器使用
    "experimentalDecorators": true
  }
}

上面 Class 组件代码中使用了装饰器,但是目前 js 标准语法是不支持的,现在运行或者打包会报错,不识别装饰器语法,需要借助 babel-loader 插件,安装依赖:

npm i @babel/plugin-proposal-decorators -D

babel.config.js 中配置插件

module.exports = { 
  // ...
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }]
  ]
}

现在项目就支持装饰器语法了。

复制 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.htmldist/ 目录下,所以不需要再复制该文件了。

测试一下,在 public 中新增一个 favicon.ico 图标文件,在index.html中引入

处理图片文件

对于图片文件,webpack4 使用 file-loaderurl-loader 来处理的,但 webpack5 不使用这两个 loader 了,而是采用自带的 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/images/5kb.png'
import bigImg from './assets/images/22kb.png'
import './app.scss'

function App() {
  return (
    <>
      <img src={smallImg} alt="小于10kb的图片" />
      <img src={bigImg} alt="大于于10kb的图片" />
    </>
  )
}
export default App

这个时候在引入图片的地方会报:找不到模块“./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 打包,借助 npx serve -s dist 查看效果,可以看到可以正常解析图片了,并且小于 10kb 的图片被转成了 base64 位格式的。

Webpack 5.0 从零开始搭建 React 项目

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

import React from 'react'
import smallImg from './assets/imgs/5kb.png'
import bigImg from './assets/imgs/22kb.png'
import './app.scss'

function App() {
  return (
    <>
      <img src={smallImg} alt="小于10kb的图片" />
      <img src={bigImg} alt="大于于10kb的图片" />
      <div className='smallImg'></div> {/* 小图片背景容器 */}
      <div className='bigImg'></div> {/* 大图片背景容器 */}
    </>
  )
}
export default App

修改 app.scss

// app.scss
#root {
  .smallImg {
    width: 69px;
    height: 75px;
    background: url('./assets/images/5kb.png') no-repeat;
  }
  .bigImg {
    width: 232px;
    height: 154px;
    background: url('./assets/images/22kb.png') no-repeat;
  }
}

可以看到背景图片也一样可以识别,小于 10kb 转为 base64 位。

Webpack 5.0 从零开始搭建 React 项目

Webpack 5.0 从零开始搭建 React 项目

处理字体和媒体文件

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

// webpack.base.js
module.exports = {
  module: {
    rules: [
      // ...
      {
        test:/.(woff2?|eot|ttf|otf)$/, // 匹配字体图标文件
        type: "asset", // type选择asset
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024, // 小于10kb转base64位
          }
        },
        generator:{ 
          filename:'static/fonts/[name][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][ext]', // 文件输出目录和命名
        },
      }
    ]
  }
}

配置 webpack 模块热更新

热更新上面已经在 devServer 中配置 hottrue,在 webpack4 中,还需要在插件中添加了 HotModuleReplacementPlugin ,在 webpack5 中,只需要将 devServer.hot 设置为true 就可以了,因为该插件已经内置了。

现在开发模式下修改 csssass 文件,页面样式可以在不刷新浏览器的情况实时生效,因为此时样式都在 style 标签里面,style-loader 做了替换样式的热更新功能。但是修改 App.tsx ,浏览器会自动刷新后再显示修改后的内容,但我们想要的不是刷新浏览器,而是在不需要刷新浏览器的前提下模块热更新,并且能够保留react组件的状态。

可以借助 @pmmmwh/react-refresh-webpack-plugin 插件来实现,该插件又依赖于 react-refresh , 安装依赖:

优化构建速度

当进行优化的时候,肯定要先知道时间都花费在哪些步骤上了,而 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 命令

Webpack 5.0 从零开始搭建 React 项目

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

开启持久化存储缓存

webpack5 之前做缓存是通过使用 babel-loader 缓存 js 的解析结果,使用 cache-loader 缓存 css 等资源的解析结果。另外还可以使用模块缓存插件 hard-source-webpack-plugin ,配置好缓存后,第二次打包通过对文件做哈希对比来验证文件前后是否一致,如果一致则采用上一次的缓存,这样可以极大地节省时间。

webpack5 较于 webpack4,新增了持久化缓存、改进缓存算法等优化,通过配置 webpack 持久化缓存,来缓存生成的 webpack 模块和 chunk,改善下一次打包的构建速度,可提速 90% 左右,配置也简单,修改 webpack.base.js

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

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

Webpack 5.0 从零开始搭建 React 项目

Webpack 5.0 从零开始搭建 React 项目

当前文章代码的测试结果

模式第一次耗时第二次耗时
开发模式1045ms212ms
打包模式1868ms236ms

缓存的存储位置在 node_modules/.cache/webpack,里面又区分了 developmentproduction 缓存

Webpack 5.0 从零开始搭建 React 项目

开启多线程 loader

webpackloader 默认单线程执行,现代电脑一般都有多核 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左右,所以适合规模比较大的项目。

配置 alias 别名

webpack 支持设置别名 alias ,设置别名可以让后续引用的地方减少路径的复杂度。

修改webpack.base.js

module.export = {
  // ...
   resolve: {
    // ...
    alias: {
      '@': path.join(__dirname, '../src')
    }
  }
}

修改 tsconfig.json,添加 baseUrlpaths

{
  "compilerOptions": {
    // ...
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "src/*"
      ]
    }
  }
}

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

src/App.tsx 可以修改为

import React from 'react'
import smallImg from '@/assets/images/5kb.png'
import bigImg from '@/assets/images/22kb.png'
import '@/app.scss'

function App() {
  return (
    <>
      <img src={smallImg} alt="小于10kb的图片" />
      <img src={bigImg} alt="大于于10kb的图片" />
      <div className='smallImg'></div> {/* 小图片背景容器 */}
      <div className='bigImg'></div> {/* 大图片背景容器 */}
    </>
  )
}
export default App

src/app.scss 可以修改为

// app.scss
#root {
  .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;
  }
}

缩小 loader 作用范围

一般第三方库都是已经处理好的,不需要再次使用 loader 去解析,项目可以按照实际情况合理配置 loader 的作用范围,来减少不必要的 loader 解析,节省构建时间,通过使用 includeexclude 两个配置项,可以实现这个功能,常见的例如:

  • include:只解析该选项配置的模块
  • exclude:不解析该选项配置的模块,优先级更高

修改 webpack.base.js

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

其他 loader 也是相同的配置方式,如果除 src 目录下文件之外还有需要解析的,就把对应的目录地址加上就可以了,比如需要引入 antdcss,可以把 antd 的文件目录路径添加解析 css 规则到 include 里面。

精确使用loader

loaderwebpack 构建过程中使用的时机是在 webpack 构建模块依赖关系引入新文件时,webpack 会根据文件后缀来倒序遍历 rules 数组,如果文件后缀和 test 正则匹配到了,就会使用该 rule 中配置的 loader 依次对文件源代码进行处理,最终拿到处理后的 sourceCode 结果,可以通过配置避免使用无用的 loader 解析来提升构建速度,例如:使用 sass-loader 去解析 css 文件这种情况。

可以拆分上面配置的 scsscss,避免让 sass-loader 再去解析 css 文件

// webpack.base.js
// ...
module.exports = {
  module: {
    // ...
    rules: [
      // ...
      {
        test: /.css$/, //匹配所有的 css 文件
        include: [path.resolve(__dirname, '../src')],
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader'
        ]
      },
      {
        test: /.less$/, //匹配所有的 less 文件
        include: [path.resolve(__dirname, '../src')],
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader',
          'less-loader'
        ]
      },
    ]
  }
}

tstsx 也是如此,ts 里面是不能写 jsx 语法的,所以可以尽可能避免使用 @babel/preset-reactts 语法文件做处理。

缩小模块搜索范围

node 里面模块有三种

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

使用 requireimport 引入模块时如果有准确的相对或者绝对路径,就会去按路径查询,如果引入的模块没有路径,会优先查询 node 核心模块,如果没有找到会去当前目录下 node_modules 中寻找,如果没有找到会到父级文件夹继续查找 node_modules,一直查到系统 node 全局模块。

这样会有两个问题,一个是当前项目没有安装某个依赖,但是上一级目录下 node_modules 或者全局模块有安装,就也会引入成功,但是部署到服务器时可能就会找不到造成报错,另一个问题就是一级一级查询比较消耗时间。所以可以告诉 webpack 搜索目录范围,来规避这两个问题。

修改 webpack.base.js

// webpack.base.js
const path = require('path')
module.exports = {
  // ...
  resolve: {
     // ...
     // 如果用的是 pnpm 就暂时不要配置这个,会有幽灵依赖的问题,访问不到很多模块。
     modules: [path.resolve(__dirname, '../node_modules')], // 查找第三方模块只在本项目的node_modules中查找
  },
}

devtool 配置

开发过程中或者打包后的代码都是 webpack 处理后的代码,开发者如果进行调试肯定希望看到源代码,而不是编译后的代码,source map 就是用来做源码映射的,不同的映射模式会明显影响到构建和重新构建的速度,devtool 选项就是 webpack 提供的选择源码映射方式的配置。

devtool 的命名规则为 ^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$

关键字描述
inline代码内通过 dataUrl 形式引入 SourceMap
hidden生成 SourceMap 文件,但不使用
evaleval(...) 形式执行代码,通过 dataUrl 形式引入 SourceMap
nosources不生成 SourceMap
cheap只需要定位到行信息,不需要列信息
module展示源代码中的错误位置

开发环境推荐:eval-cheap-module-source-map

  • 本地开发首次打包慢点没关系,因为 eval 缓存的原因, 热更新会很快
  • 开发中,我们每行代码不会写的太长,只需要定位到行就行,所以加上 cheap
  • 我们希望能够找到源代码的错误,而不是打包后的,所以需要加上 module

修改 webpack.dev.js

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

打包环境推荐:none(是不推荐配置 devtool 选项了,不是指配置 devtool: 'none')

 // webpack.prod.js
module.exports = {
  // ...
  // devtool: '', // 不用配置devtool此项
}
  • 打包环境不配置 devtool 的话调试只能看到编译后的代码,但是这样也就不会泄露源代码,打包速度也会比较快。
  • 只是不方便线上排查问题,但一般都可以根据报错信息在本地环境很快找出问题所在。

其他优化配置

除了上面的配置外,webpack还提供了其他的一些优化方式,本次搭建没有使用到,所以只简单罗列下

  • externals:外包拓展,打包时会忽略配置的依赖,会从上下文中寻找对应变量
  • module.noParse:匹配到设置的模块,将不进行依赖解析,适合jquery,boostrap这类不依赖外部模块的包
  • ignorePlugin:可以使用正则忽略一部分文件,常在使用多语言的包时可以把非中文语言包过滤掉

优化构建结果文件

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

Webpack 5.0 从零开始搭建 React 项目

抽取css样式文件

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

npm i mini-css-extract-plugin -D

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

// 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: [
          isDev ? 'style-loader' : MiniCssExtractPlugin.loader, // 开发环境使用style-loader,打包模式抽离css
          'css-loader',
          'postcss-loader'
        ]
      },
      {
        test: /.less$/, //匹配所有的 less 文件
        include: [path.resolve(__dirname, '../src')],
        use: [
          isDev ? 'style-loader' : MiniCssExtractPlugin.loader, // 开发环境使用style-loader,打包模式抽离css
          '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({
      filename: 'static/css/[name].css' // 抽离css的输出目录和名称
    }),
  ]
})

配置完成后,在开发模式 css 会嵌入到 style 标签里面,方便样式热更新,打包时会把 css 抽离成单独的 css 文件。

合理配置打包文件hash

项目维护的时候,一般只会修改一部分代码,可以通过合理配置文件缓存来提升前端加载页面速度和减少服务器压力,而 hash 就是浏览器缓存策略很重要的一部分。webpack 打包的 hash 分三种:

  • hash:跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash
  • chunkhash:不同的入口文件进行依赖文件解析、构建对应的 chunk ,生成对应的哈希值,文件本身修改或者依赖文件修改,chunkhash 值会变化
  • contenthash:每个文件自己单独的 hash 值,文件的改动只会影响自身的 hash

hash 是在输出文件时配置的,格式是 filename: "[name].[chunkhash:8][ext]"[xxx]格式是webpack 提供的占位符,:8是生成 hash 的长度。

占位符解释
ext文件后缀名
name文件名
path文件相对路径
folder文件所在文件夹
hash每次构建生成的唯一 hash 值
chunkhash根据 chunk 生成 hash 值
contenthash根据文件内容生成hash 值

因为 js 我们在生产环境里会把一些公共库和程序入口文件区分开,单独打包构建,采用 chunkhash 的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响,可以继续使用浏览器缓存,所以 js 适合使用 chunkhash

css 和图片资源媒体资源一般都是单独存在的,可以采用 contenthash,只有文件本身变化后会生成新 hash 值。

修改 webpack.base.js,把 js 输出的文件名称格式加上 chunkhash,把 css 和图片媒体资源输出格式加上 contenthash

// webpack.base.js
// ...
module.exports = {
  // 打包文件出口
  output: {
    filename: 'static/js/[name].[chunkhash:8].js', // 加上[chunkhash:8]
    // ...
  },
  module: {
    rules: [
      {
        test:/.(png|jpg|jpeg|gif|svg)$/, // 匹配图片文件
        // ...
        generator:{ 
          filename:'static/images/[name].[contenthash:8][ext]' // 加上[contenthash:8]
        },
      },
      {
        test:/.(woff2?|eot|ttf|otf)$/, // 匹配字体文件
        // ...
        generator:{ 
          filename:'static/fonts/[name].[contenthash:8][ext]', // 加上[contenthash:8]
        },
      },
      {
        test:/.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件
        // ...
        generator:{ 
          filename:'static/media/[name].[contenthash:8][ext]', // 加上[contenthash:8]
        },
      },
    ]
  },
  // ...
}

再修改 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].[contenthash:8].css' // 加上[contenthash:8]
    }),
    // ...
  ],
  // ...
})

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

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

一般第三方包的代码变化频率比较小,可以单独把 node_modules 中的代码单独打包,当第三包代码没变化时,对应 chunkhash 值也不会变化,这样可以有效利用浏览器缓存,另外项目公共的模块也可以提取出来,可以有效避免重复打包加大代码整体体积,webpack 提供了代码分割功能,需要我们手动在优化项 optimization 中手动配置下代码分隔 splitChunks 规则。

修改 webpack.prod.js

module.exports = {
  // ...
  optimization: {
    // ...
    splitChunks: { // 分隔代码
      cacheGroups: {
        vendors: { // 提取node_modules代码
          test: /node_modules/, // 只匹配node_modules里面的模块
          name: 'vendors', // 提取文件命名为vendors,js后缀和chunkhash会自动加
          minChunks: 1, // 只要使用一次就提取出来
          chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
          minSize: 0, // 提取代码体积大于0就提取出来
          priority: 1, // 提取优先级为1
        },
        commons: { // 提取页面公共代码
          name: 'commons', // 提取文件命名为commons
          minChunks: 2, // 只要使用两次就提取出来
          chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
          minSize: 0, // 提取代码体积大于0就提取出来
        }
      }
    }
  }
}

tree-shaking 清理未引用 js

Tree Shaking 的意思就是摇树,伴随着摇树这个动作,树上的枯叶都会被摇晃下来,这里的 tree-shaking 在代码中摇掉的是未使用到的代码,也就是未引用的代码,tree shaking 的概念最早是在 rollup 库中出现的,webpackv2 版本之后也开始支持。模式 modeproduction 时就会默认开启 tree-shaking 功能以此来标记未引入代码然后移除掉,下面测试一下:

src/components 目录下新增 Demo1Demo2 两个组件

// src/components/Demo1.tsx
import React from "react";
function Demo1() {
  return <h3>我是Demo1组件</h3>
}
export default Demo1

// src/components/Demo2.tsx
import React from "react";
function Demo2() {
  return <h3>我是Demo2组件</h3>
}
export default Demo2

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

// src/components/index.ts
export { default as Demo1 } from './Demo1'
export { default as Demo2 } from './Demo2'

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

// ...
import { Demo1, Demo2 } from '@/components'

function App() {
  return <Demo1 />
}
export default App

执行打包,可以看到在 main.js 中搜索 Demo,只搜索到了Demo1,代表 Demo2 组件被tree-shaking 移除掉了。

tree-shaking 清理未使用 css

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

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

本文版本是4版本最新的5版本导入方式需要改为 const { PurgeCSSPlugin } = require('purgecss-webpack-plugin')

修改 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

import React from 'react'
import './app.css'
import './app.less'

function App() {
  return (
    <>
      <div className='smallImg'></div>
      <div className='bigImg'></div>
    </>
  )
}
export default App

App.tsx 中有两个 div,类名分别是smallImgbigImg,当前app.scss代码为

#root {
  .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;
  }
}

此时先执行一下打包,查看 main.css

但是purgecss-webpack-plugin插件不是全能的,由于项目业务代码的复杂,插件不能百分百识别哪些样式用到了,哪些没用到,所以请不要寄希望于它能够百分百完美解决你的问题,这个是不现实的。

插件本身也提供了一些白名单safelist属性,符合配置规则选择器都不会被删除掉,比如使用了组件库antd, purgecss-webpack-plugin插件检测src文件下tsx文件中使用的类名和id时,是检测不到在src中使用antd组件的类名的,打包的时候就会把antd的类名都给过滤掉,可以配置一下安全选择列表,避免删除antd组件库的前缀ant

new PurgeCSSPlugin({
  // ...
  safelist: {
    standard: [/^ant-/], // 过滤以ant-开头的类名,哪怕没用到也不删除
  }
})

资源懒加载

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

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

以懒加载组件和 css 为例,新建懒加载组件 src/components/LazyDemo.tsx

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.cssLazyDemo 组件的资源。

资源预加载

上面配置了资源懒加载后,虽然提升了首屏渲染速度,但是加载到资源的时候会有一个去请求资源的延时,如果资源比较大会出现延迟卡顿现象,可以借助link标签的rel属性prefetchpreload,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'

// prefetch
const PreFetchDemo = lazy(() => import(
  /* webpackChunkName: "PreFetchDemo" */
  /*webpackPrefetch: true*/
  '@/components/PreFetchDemo'
))
// preload
const PreloadDemo = lazy(() => import(
  /* webpackChunkName: "PreloadDemo" */
  /*webpackPreload: true*/
  '@/components/PreloadDemo'
 ))

function App() {
  const [ show, setShow ] = useState(false)

  const onClick = () => {
    setShow(true)
  }
  return (
    <>
      <h2 onClick={onClick}>展示</h2>
      {/* show为true时加载组件 */}
      { show && (
        <>
          <Suspense fallback={null}><PreloadDemo /></Suspense>
          <Suspense fallback={null}><PreFetchDemo /></Suspense>
        </>
      ) }
    </>
  )
}
export default App

然后打包后查看效果,页面初始化时预加载了PreFetchDemo.js组件资源,但是不执行里面的代码,等点击展示按钮后从预加载的资源中直接取出来执行,不用再从服务器请求,节省了很多时间。

gzip 压缩

前端代码在浏览器运行,需要从服务器把html,css,js资源下载执行,下载的资源体积越小,页面加载速度就会越快。一般会采用gzip压缩,现在大部分浏览器和服务器都支持gzip,可以有效减少静态资源文件大小,压缩率在 70% 左右。

nginx可以配置gzip: on来开启压缩,但是只在nginx层面开启,会在每次请求资源时都对资源进行压缩,压缩文件会需要时间和占用服务器cpu资源,更好的方式是前端在打包的时候直接生成gzip资源,服务器接收到请求,可以直接把对应压缩好的gzip文件返回给浏览器,节省时间和cpu

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 结尾的文件

总结

到目前为止已经使用 webpack5react18+ts 的开发环境配置完成,并且配置比较完善的保留组件状态的热更新,以及常见的优化构建速度构建结果的配置。但还有细节需要优化,比如把容易改变的配置单独写个config.js来配置,输出文件路径封装。这篇文章只是配置,如果想学好webpack,还需要学习webpack的构建原理以及loaderplugin的实现机制。

参考

  1. webpack官网
  2. babel官网
  3. 【万字】透过分析 webpack 面试题,构建 webpack5.x 知识体系
  4. Babel 那些事儿
  5. 阔别两年,webpack 5 正式发布了!
  6. webpack5从零搭建完整的react18+ts开发和打包环境
  7. webpack从入门到进阶
转载自:https://juejin.cn/post/7243337546826170425
评论
请登录