【脚手架】从0到1搭建React18+TS4.x+Webpack5项目(二)基础功能配置

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 【脚手架】从0到1搭建React18+TS4.x+Webpack5项目(二)基础功能配置

微信图片_20230612190305.pngflag:每月至少产出三篇高质量文章!

持续学习、更新,出啥新玩意儿就玩啥!同类型的技术栈,会通过使用做横向测评。

同时,欢迎关注我的另外几篇文章:

微信图片_20230612190400.png可能想要玩的技术栈:

  • 按需加载
  • dark theme
  • I18n
  • ......
  • 静态资源:图片、fontsMedia、数据资源(JSONcsvtsvexcel)......
  • React-Admin、可视化大屏、响应式、代码生成、低代码
  • 权限:React-Admin(RBAC) + GoReact-Admin(RBAC) + Node
  • 接口 mock
  • 性能优化
  • 工具层面:热更新、资源压缩、代码分离(动态导入、懒加载、预加载等)、缓存……
  • 代码层面:大组件拆分、全局状态管理、组件封装、re-render

上篇文章,我们做了项目的一些初始化工作,本篇将加入一些基本的配置。

1、引入 less、sass(scss)、stylus

lesssass(scss)stylus是三个比较流行的 CSS Modules 预处理库。在 React 中,使用CSS Modules 的好处在于:

  1. 避免全局样式冲突:使用 CSS Modules 可以确保样式只应用于特定组件,避免全局样式冲突。
  2. 更好的可维护性:CSS Modules 使得样式与组件代码紧密关联,方便代码维护。
  3. 提高代码可重用性:CSS Modules 可以轻松地将样式从一个组件复制到另一个组件,提高代码可重用性。
  4. 支持动态样式:使用 CSS Modules 可以轻松地生成动态样式,例如根据组件状态或属性更改样式。
  5. 更好的性能:CSS Modules 使用模块化的方式加载样式,提高了页面加载速度和性能。

1.1 基本用法

先安装相关的依赖:

pnpm add less less-loader sass-loader node-sass stylus stylus-loader -D

webpack.base.ts添加相关的loader

// ...
const cssRegex = /\.css$/;
const sassRegex = /\.(scss|sass)$/;
const lessRegex = /\.less$/;
const stylRegex = /\.styl$/;
const styleLoadersArray = [
  "style-loader",
  {
    loader: "css-loader",
    options: {
      modules: {
        localIdentName: "[path][name]__[local]--[hash:5]",
      },
    },
  },
];
const baseConfig: Configuration = {
  // ...
  module: {
    rules: [
      // ...
       {
        test: cssRegex, //匹配 css 文件
        use: styleLoadersArray,
      },
      {
        test: lessRegex,
        use: [
          ...styleLoadersArray,
          {
            loader: "less-loader",
            options: {
              lessOptions: {
                // 如果要在less中写类型js的语法,需要加这一个配置
                javascriptEnabled: true
              },
            },
          },
        ],
      },
      {
        test: sassRegex,
        use: [
          ...styleLoadersArray,
          "sass-loader",
        ],
      },
      {
        test: stylRegex,
        use: [
          ...styleLoadersArray,
          "stylus-loader",
        ],
      },
    ],
  },
  // ...
};
export default baseConfig;

webpack配置说明

  1. localIdentName:配置生成的css类名组成(path路径,name文件名,local原来的css类名, hash: base64:5拼接生成hash值5位,具体位数可根据需要设置。
  2. 如下的配置(localIdentName: '[local]__[hash:base64:5]'):生成的css类名类似 class="edit__275ih"这种,既能达到scoped的效果,又保留原来的css类名(edit)。

推荐阅读:【Webpack进阶】less-loader、css-loader、style-loader实现原理

然后就可以在业务中使用了:

/* src/app.module.less */
@color: red;
.lessBox {
    .box {
        color: @color;
        background-color: lightblue;
        transform: translateX(100);
        &:before{
            @arr: 'hello', 'world';
            content: `@{arr}.join(' ').toUpperCase()`;
          }
    }
}
/* src/app.module.scss */
$blue: #1875e7;
$side: left;
.scssBox {
  margin: 20px 0;
  .box {
    color: $blue;
    background-color: rgb(226, 223, 223);
    border: 1px solid grey;
    margin-#{$side}: 20px;
    padding: (20px/2);
  }
}
/* src/app.module.styl */
.stylBox 
  .box 
    color: red;
    background-color: yellow;

App.tsx中引入:

import '@/App.css'
import lessStyles from './app.less'
import scssStyles from './app.module.scss'
import stylStyles from './app.module.styl'
function App() {
  return <div>
    <h2>webpack5-react-ts</h2>
    <div className={lessStyles['lessBox']}>
      <div className={lessStyles['box']}>lessBox</div>
    </div>
    <div className={scssStyles['scssBox']}>
      <div className={scssStyles['box']}>scssBox</div>
    </div>
    <div className={stylStyles['stylBox']}>
      <div className={stylStyles['box']}>stylBox</div>
    </div>
  </div>
}
export default App

重启项目,就会发现生成了带有hash值的class类名,且里面包含了我们自定义的类名,方便日后调试用:

微信图片_20230612190614.png
同时在验证打包是否成功,运行 pnpm run build:dev,然后通过serve -S dist查看。 当然,如果你不希望每次写less的时候,都在文件名上加一个.module,可以在less-loader中添加如下配置:

// ...
const baseConfig: Configuration = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: lessRegex,
        use: [
          ...styleLoadersArray,
          {
            loader: "less-loader",
            options: {
              lessOptions: {
                importLoaders: 2,
                // 可以加入modules: true,这样就不需要在less文件名加module了
                modules: true,
                // 如果要在less中写类型js的语法,需要加这一个配置
                javascriptEnabled: true
              },
            },
          },
        ],
      },
      // ...
    ],
  },
  // ...
};
export default baseConfig;

至此,我们就完成了lesssass(scss)stylus的引入。

Tips:虽然我们在样式文件名上加一个 .module 的后缀,可以明确这是 css modules,但也带来了额外的码字开销。可以在 global.d.ts 加入样式文件的声明,就可以避免写 .module 后缀。

// src/typings/global.d.ts
/* CSS MODULES */
declare module '*.css' {
  const classes: { [key: string]: string };
  export default classes;
}
declare module '*.scss' {
  const classes: { [key: string]: string };
  export default classes;
}
declare module '*.sass' {
  const classes: { [key: string]: string };
  export default classes;
}
declare module '*.less' {
  const classes: { [key: string]: string };
  export default classes;
}
declare module '*.styl' {
  const classes: { [key: string]: string };
  export default classes;
}

然后就可以删掉样式文件中的 .module 后缀了。微信图片_20230612190726.png

1.2 处理CSS3前缀在浏览器中的兼容

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

pnpm add postcss-loader autoprefixer -D

为了避免webpack.base.ts文件过于庞大,我们将一些loader配置提取成单独的文件来进行管理,根目录新建postcss.config.js,作为postcss-loader的配置文件,会自动读取配置:

module.exports = {
  ident: "postcss",
  plugins: [require("autoprefixer")],
};

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

// ...
const styleLoadersArray = [
  "style-loader",
  {
    loader: "css-loader",
    options: {
      modules: {
        localIdentName: "[path][name]__[local]--[hash:5]",
      },
    },
  },
  // 添加 postcss-loader
  'postcss-loader'
];

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

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

以兼容到ie9chrome35版本为例,配置好后,在app.module.less中加入一些CSS3的语法,重新启动项目,就可以在浏览器的控制台-Elements 中看到配置成功了。

执行pnpm run build:dev打包,也可以看到打包后的css文件已经加上了ie和谷歌内核的前缀。

2、处理其他常用资源

2.1 处理图片

对于图片文件,webpack4使用file-loaderurl-loader来处理的,但webpack5不使用这两个loader了,而是采用自带的 asset-module 来处理,修改webpack.base.ts,添加图片解析配置

{
  output: {
    // ... 这里自定义输出文件名的方式是,将某些资源发送到指定目录
    assetModuleFilename: 'images/[hash][ext][query]'
  },
  module: {
    rules: [
      // ...
      {
        test: /\.(png|jpe?g|gif|svg)$/i, // 匹配图片文件
        type: "asset", // type选择asset
        parser: {
          dataUrlCondition: {
            maxSize: 20 * 1024, // 小于10kb转base64
          }
        },
        generator:{ 
          filename:'static/images/[hash][ext][query]', // 文件输出目录和命名
        },
      },
    ]
  }
}
资源模块类型 描述
asset/resource 发送一个单独的文件,并导出 URL,替代 file-loader,相当于file-loader, 将文件转化成Webpack能识别的资源,其他不做处理。
asset/inline 导出一个资源的 data URI,以前使用 url-loader 实现。
asset/source 导出资源的源代码 ,以前是使用 raw-loader 实现。
asset 相当于自动选择 asset/resourceasset/inline,替换 url-loader 中的 limit,相当于url-loader将文件转化成Webpack能识别的资源,同时小于某个大小的资源会处理成data URI形式。

将文件编译为 Data URI 使用,可以节省 HTTP 请求,是一个性能优化的点。但是将图片文件经过 base64 编码转为 Data URI,体积会增加大约33%。所以,我们一般只针对小图片做Base64的处理,对于一些比较大的文件来说,转为 Data URI 会明显增加打包后文件的体积,反而会加大对带宽资源和流量的需求。

由于我们希望通过 ES6 的新语法 ESModule 的方式导入资源,为了使 TypeScript 可以识别图片模块,需要在 src/typings/global.d.ts 中加入声明:

// ...
/* IMAGES */
declare module '*.svg' {
  const ref: string;
  export default ref;
}
declare module '*.bmp' {
  const ref: string;
  export default ref;
}
declare module '*.gif' {
  const ref: string;
  export default ref;
}
declare module '*.jpg' {
  const ref: string;
  export default ref;
}
declare module '*.jpeg' {
  const ref: string;
  export default ref;
}
declare module '*.png' {
  const ref: string;
  export default ref;
}

然后在 App.tsx 中引入图片资源:

import '@/App.css'
import lessStyles from '@/app.less'
import scssStyles from '@/app.scss'
import stylStyles from '@/app.styl'
import smallImg from '@/assets/imgs/5kb_img.jpeg'
import bigImg from '@/assets/imgs/10kb_img.png'
function App() {
  return <div>
    <h2>webpack5-react-ts</h2>
    <div className={lessStyles['lessBox']}>
      <div className={lessStyles['box']}>lessBox
      <img src={smallImg} alt="小于10kb的图片" />
      <img src={bigImg} alt="大于于10kb的图片" />
      <div className={lessStyles['smallImg']}>小图片背景</div> 
      <div className={lessStyles['bigImg']}>大图片背景</div> 
      </div>
    </div>
    <div className={scssStyles['scssBox']}>
      <div className={scssStyles['box']}>scssBox</div>
    </div>
    <div className={stylStyles['stylBox']}>
      <div className={stylStyles['box']}>stylBox</div>
    </div>
  </div>
}
export default App

app.less 文件中加入背景图片:

@color: red;
.lessBox {
    .box {
        color: @color;
        background-color: lightblue;
        transform: translateX(100);
        &:before{
            @arr: 'hello', 'world';
            content: `@{arr}.join(' ').toUpperCase()`;
          }
    }
    .smallImg {
        width: 69px;
        height: 75px;
        background: url('@/assets/imgs/5kb_img.jpeg') no-repeat;
      }
      .bigImg {
        width: 232px;
        height: 154px;
        background: url('@/assets/imgs/10kb_img.png') no-repeat;
      }
}

重启项目,可以看到图片被正确地展示出来了:

微信图片_20230612191037.png
然后测试打包:

微信图片_20230612191110.png图片被打包进了我们指定的 static/images 文件里面。

2.2 处理字体和媒体

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

module: {
  rules: [
     // ...
     {
        test:/.(woff2?|eot|ttf|otf)$/, // 匹配字体图标文件
        type: "asset", // type选择asset
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024, // 小于10kb转base64
          }
        },
        generator:{ 
          filename:'static/fonts/[hash][ext][query]', // 文件输出目录和命名
        },
      },
      {
        test:/.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件
        type: "asset", // type选择asset
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024, // 小于10kb转base64
          }
        },
        generator:{ 
          filename:'static/media/[hash][ext][query]', // 文件输出目录和命名
        },
      },
  ]
}

测试一下,我们可以去阿里的 iconfont 下载几个图标:

微信图片_20230612191154.png
解压下载的zip包,将其放入到项目的src下,打开demo_index.html,官方推荐了三种使用方式,可以根据自己的需求来选择。微信图片_20230612191235.png当然也可以直接当做图片下载使用:
微信图片_20230612191301.png
引入就和上述处理图片的一样。

对于fant-family,可以去 google font 找一个字体,放入 assets/fonts,然后通过下面的方式引入:

@color: red;
@font-face {
    font-family: "GoldmanBold";
    src: local("GoldmanBold"),
     url("@/assets/fonts/Phudu-Bold.ttf") format("truetype");
    font-weight: bold;
}
.lessBox {
    .box {
        color: @color;
        background-color: lightblue;
        transform: translateX(100);
        font-family: "GoldmanBold";
        &:before{
            @arr: 'hello', 'world';
            content: `@{arr}.join(' ').toUpperCase()`;
          }
    }
}

就可以发现,页面的字体变成了我们设置的字体样式:
微信图片_20230612191341.png
其实还有很多国内外开源的图标库,也很好用,使用方式跟常用的UI库很类似,这里推荐以下几个:

2.3 处理json资源

// ...
{
    // 匹配json文件
    test: /\.json$/,
    type: "asset/resource", // 将json文件视为文件类型
    generator: {
      // 这里专门针对json文件的处理
      filename: "static/json/[name].[hash][ext][query]",
    },
},
// ...

在src下增加一个json文件test.json:

[
    {
        "name": "ian1",
        "age": 18
    },
    {
        "name": "ian2",
        "age": 22
    },
    {
        "name": "ian3",
        "age": 23
    }
]

然后在App.tsx中引入:

import memberList from './test.json'

这里可能会出现一个报错:

Cannot find module './test.json'. Consider using '--resolveJsonModule' to import module with '.json' extension.

需要在tsconfig.json中加入一个配置:

{
  "compilerOptions": {
    // ...
    "resolveJsonModule": true,
    // ...
  },
  // ...
}

就可以在控制台打印出我们通过模块导入的json文件了:

微信图片_20230612191528.png

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

pnpm add @babel/plugin-proposal-decorators -D

babel.config.js 中添加插件:

const isDEV = process.env.NODE_ENV === "development"; // 是否是开发模式
module.exports = {
  // 执行顺序由右往左,所以先处理ts,再处理jsx,最后再试一下babel转换为低版本语法
  presets: [
    [
      "@babel/preset-env",
      {
        // 设置兼容目标浏览器版本,这里可以不写,babel-loader会自动寻找上面配置好的文件.browserslistrc
        // "targets": {
        //  "chrome": 35,
        //  "ie": 9
        // },
        targets: { browsers: ["> 1%", "last 2 versions", "not ie <= 8"] },
        useBuiltIns: "usage", // 根据配置的浏览器兼容,以及代码中使用到的api进行引入polyfill按需添加
        corejs: 3, // 配置使用core-js使用的版本
        loose: true,
      },
    ],
    // 如果您使用的是 Babel 和 React 17,您可能需要将 "runtime": "automatic" 添加到配置中。
    // 否则可能会出现错误:Uncaught ReferenceError: React is not defined
    ["@babel/preset-react", { runtime: "automatic" }],
    "@babel/preset-typescript",
  ],
  plugins: [
    ["@babel/plugin-proposal-decorators", { legacy: true }],
  ].filter(Boolean), // 过滤空值
};

现在项目就支持装饰器了。重启项目:

微信图片_20230612191706.png

4、热更新

在之前的章节我们已经在devServer中配置hottrue,在webpack4中,还需要在插件中添加了HotModuleReplacementPlugin,在webpack5中,只要devServer.hottrue了,该插件就已经内置了。

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

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

pnpm add @pmmmwh/react-refresh-webpack-plugin react-refresh -D

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

import path from "path";
import webpack, { Configuration as WebpackConfiguration } from "webpack";
import { Configuration as WebpackDevServerConfiguration } from "webpack-dev-server";
import WebpackDevServer from "webpack-dev-server";
import ReactRefreshWebpackPlugin from "@pmmmwh/react-refresh-webpack-plugin";
import { merge } from "webpack-merge";
import baseConfig from "./webpack.base";
// 运行命令的时候重启一次打开一个tab 页很烦,所以呢优化一下
// 参考:create-react-app 的启动方式
// https://github.com/facebook/create-react-app/blob/main/packages/react-dev-utils/openChrome.applescript
// 记得关闭webpack-dev-server的配置中的自动打开 open: false 或者注释
const openBrowser = require("./util/openBrowser");
interface Configuration extends WebpackConfiguration {
  devServer?: WebpackDevServerConfiguration;
}
const host = "127.0.0.1";
const port = "8082";
// 合并公共配置,并添加开发环境配置
const devConfig: Configuration = merge(baseConfig, {
  mode: "development", // 开发模式,打包更加快速,省了代码优化步骤
  /**
    开发环境推荐:eval-cheap-module-source-map
    ● 本地开发首次打包慢点没关系,因为 eval 缓存的原因, 热更新会很快
    ● 开发中,我们每行代码不会写的太长,只需要定位到行就行,所以加上 cheap
    ● 我们希望能够找到源代码的错误,而不是打包后的,所以需要加上 module
   */
  devtool: "eval-cheap-module-source-map",
  plugins: [
    new ReactRefreshWebpackPlugin(), // 添加热更新插件
  ],
});
const devServer = new WebpackDevServer(
  {
    host, // 地址
    port, // 端口
    open: false, // 是否自动打开,关闭
    setupExitSignals: true, // 允许在 SIGINT 和 SIGTERM 信号时关闭开发服务器和退出进程。
    compress: false, // gzip压缩,开发环境不开启,提升热更新速度
    hot: true, // 开启热更新,后面会讲react模块热替换具体配置
    historyApiFallback: true, // 解决history路由404问题
    static: {
      directory: path.join(__dirname, "../public"), // 托管静态资源public文件夹
    },
    headers: { "Access-Control-Allow-Origin": "*" },
  },
  webpack(devConfig)
);
devServer.start().then(() => {
  // 启动界面
  openBrowser(`http://${host}:${port}`);
});
export default devConfig;

babel-loader配置react-refesh刷新插件,修改babel.config.js文件

const isDEV = process.env.NODE_ENV === "development"; // 是否是开发模式
module.exports = {
  // 执行顺序由右往左,所以先处理ts,再处理jsx,最后再试一下babel转换为低版本语法
  presets: [
    [
      "@babel/preset-env",
      {
        // 设置兼容目标浏览器版本,这里可以不写,babel-loader会自动寻找上面配置好的文件.browserslistrc
        // "targets": {
        //  "chrome": 35,
        //  "ie": 9
        // },
        targets: { browsers: ["> 1%", "last 2 versions", "not ie <= 8"] },
        useBuiltIns: "usage", // 根据配置的浏览器兼容,以及代码中使用到的api进行引入polyfill按需添加
        corejs: 3, // 配置使用core-js使用的版本
        loose: true,
      },
    ],
    // 如果您使用的是 Babel 和 React 17,您可能需要将 "runtime": "automatic" 添加到配置中。
    // 否则可能会出现错误:Uncaught ReferenceError: React is not defined
    ["@babel/preset-react", { runtime: "automatic" }],
    "@babel/preset-typescript",
  ],
  plugins: [
    ["@babel/plugin-proposal-decorators", { legacy: true }],
    isDEV && require.resolve("react-refresh/babel"), // 如果是开发模式,就启动react热更新插件
  ].filter(Boolean), // 过滤空值
};

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

import { useState } from 'react'
import '@/App.css'
import lessStyles from '@/app.less'
import scssStyles from '@/app.scss'
import stylStyles from '@/app.styl'
import smallImg from '@/assets/imgs/5kb_img.jpeg'
import bigImg from '@/assets/imgs/10kb_img.png'
import chengzi from '@/assets/imgs/chengzi.png'
import memberList from './test.json'
import ClassComp from '@/components/Class'
function App() {
  const [ count, setCounts ] = useState('')
  const onChange = (e: any) => {
    setCounts(e.target.value)
  }
  console.log('memberList', memberList)
  return <div>
    <h2>webpack5-react-ts</h2>
    <div className={lessStyles['lessBox']}>
      <div className={lessStyles['box']}>lessBox(天下无敌)
      <img src={smallImg} alt="小于10kb的图片" />
      <img src={bigImg} alt="大于于10kb的图片" />
      <img src={chengzi} alt="橙子font" />
      <div className={lessStyles['smallImg']}>小图片背景</div> 
      <div className={lessStyles['bigImg']}>大图片背景</div> 
      </div>
    </div>
    <div className={scssStyles['scssBox']}>
      <div className={scssStyles['box']}>scssBox</div>
    </div>
    <div className={stylStyles['stylBox']}>
      <div className={stylStyles['box']}>stylBox</div>
    </div>
    <ClassComp />
    <div>
        <p>受控组件</p>
        <input type="text" value={count} onChange={onChange} />
        <br />
        <p>非受控组件</p>
        <input type="text" />
    </div>
  </div>
}
export default App

在两个输入框分别输入内容后,修改App.tsxp标签的文本,会发现在不刷新浏览器的情况下,页面内容进行了热更新,并且react组件状态也会保留。

微信图片_20230612192011.png
新增或者删除页面hooks时,热更新时组件状态不会保留。

5、webpack构建速度优化

5.1 webpack 进度条

webpackbar 这是一款个人感觉是个十分美观优雅的进度条,很多成名框架都用过他。而且使用起来也极其方便,也可以支持多个并发构建是个十分强大的进度插件。

pnpm add webpackbar -D

最常用的属性配置其实就是这些,注释里也写的很清楚了,我们在 webpack.base.ts 中引入

// ...
import WebpackBar from 'webpackbar';
// ...
const baseConfig: Configuration = {
  // ...
  // plugins 的配置
  plugins: [
    // ...
    new WebpackBar({
      color: "#85d",  // 默认green,进度条颜色支持HEX
      basic: false,   // 默认true,启用一个简单的日志报告器
      profile:false,  // 默认false,启用探查器。
    })
  ],
};
export default baseConfig;

当然里面还有一个属性就是 reporters 还没有写上,可以在里面注册事件,也可以理解为各种钩子函数。如下:

{   // 注册一个自定义记者数组
    start(context) {
      // 在(重新)编译开始时调用
      const { start, progress, message, details, request, hasErrors } = context
    },
    change(context) {
      // 在 watch 模式下文件更改时调用
    },
    update(context) {
      // 在每次进度更新后调用
    },
    done(context) {
      // 编译完成时调用
    },
    progress(context) {
      // 构建进度更新时调用
    },
    allDone(context) {
      // 当编译完成时调用
    },
    beforeAllDone(context) {
      // 当编译完成前调用
    },
    afterAllDone(context) {
      // 当编译完成后调用
    },
}

当然多数情况下,我们并不会使用这些,基本默认就足够了。最后,刚才的代码我们的输出表现为:

微信图片_20230612192140.png其他的工具可看:聊聊webpack的打包进度展示及美化

5.2 构建耗时

Never, ever, ever, ever work on performance improvements or optimization without monitoring! 永远,永远,永远,永远不要在没有监控的情况下进行性能改进或优化!

意思是,如果我们想要去优化webpack,一定要通过评估、测试之后,针对影响性能的点进行优化,而不是盲目地为了优化而优化。

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

pnpm add speed-measure-webpack-plugin -D

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

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.ts"
  }
  // ...
}

执行npm run build:analy命令

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

5.3 开启持久化存储缓存

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

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


// webpack.base.ts
// ...
module.exports = {
  // ...
  cache: {
    type: 'filesystem', // 使用文件缓存
  },
}

当前代码的测试结果:

模式 第一次耗时 第二次耗时
开发模式 4151毫秒 1310毫秒
打包模式 4945毫秒 590毫秒

通过开启webpack5持久化存储缓存,极大缩短了启动和打包的时间。缓存的存储位置在node_modules/.cache/webpack,里面又区分了developmentproduction缓存。

微信图片_20230612192538.png

5.4 开启多线程 loader

运行在 Node.js 之上的 webpack 是单线程模式的,也就是说,webpack 打包只能逐个文件处理,当 webpack 需要打包大量文件时,打包时间就会比较漫长。

多进程/多实例构建的方案比较知名的有以下三种:

  • thread-loader
  • parallel-webpack
  • HappyPack

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

pnpm add thread-loader -D

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

修改webpack.base.ts

module: {
  rules: [
    {
      test: tsxRegex, // 匹配.ts, tsx文件
      use: ['thread-loader', 'babel-loader']
    }
  ]
}

由于thread-loader不支持抽离css插件MiniCssExtractPlugin.loader(下面会讲),所以这里只配置了多进程解析 ts

值得注意的是,开启多线程也是需要启动时间,thread-loader 会将你的 loader 放置在一个 worker 池里面运行,每个 worker 都是一个单独的有 600ms 限制的 Node.js 进程。同时跨进程的数据交换也会被限制,所以最好是项目变大到一定程度之时再采用,否则效果反而不好。

5.5 缩小构建目标

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

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

修改webpack.base.ts

module: {
  rules: [
    {
      test: tsxRegex, // 匹配.ts, tsx文件
      exclude: /node_modules/,
      use: ['thread-loader', 'babel-loader']
    }
  ]
}

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

5.6 devtools 配置

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

devtool的命名规则为:

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

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

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

修改webpack.dev.ts

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

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

// webpack.prod.ts
module.exports = {
  // ...
  // devtool: '', // 不用配置devtool此项
}

none配置在调试的时候,只能看到编译后的代码,也不会泄露源代码,打包速度也会比较快。只是不方便线上排查问题,但一般都可以根据报错信息在本地环境很快找出问题所在。

6、webpack 构建产物优化

6.1 bundle 体积分析工具

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

pnpm add webpack-bundle-analyzer -D

修改 webpack.analy.ts

import { Configuration } from "webpack";
import { merge } from "webpack-merge";
import prodConfig from "./webpack.prod";
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
// 引入webpack打包速度分析插件
const smp = new SpeedMeasurePlugin();
// 使用smp.wrap方法,把生产环境配置传进去,由于后面可能会加分析配置,所以先留出合并空位
const analyConfig: Configuration = smp.wrap(merge(prodConfig, {
    plugins: [
      new BundleAnalyzerPlugin() // 配置分析打包结果插件
    ]
}))
export default analyConfig;

配置好后,执行 pnpm run build:analy 命令,打包完成后浏览器会自动打开窗口,可以看到打包文件的分析结果页面,可以看到各个文件所占的资源大小:

image.png
然后,我们就可以根据这个图上给出的信息,来针对性优化产物体积。

6.2 样式提取

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

pnpm add mini-css-extract-plugin -D

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

// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const isDev = process.env.NODE_ENV === 'development' // 是否是开发模式
const styleLoadersArray = [
  isDev ? "style-loader" : MiniCssExtractPlugin.loader, // 开发环境使用style-looader,打包模式抽离css
  {
    loader: "css-loader",
    options: {
      modules: {
        localIdentName: "[path][name]__[local]--[hash:5]",
      },
    },
  },
  'postcss-loader'
];

再修改webpack.prod.ts,打包时添加抽离css插件:

// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const prodConfig: Configuration = merge(baseConfig, {
  // ...
  plugins: [
    // ...
    new MiniCssExtractPlugin({
      filename: 'static/css/[name].css' // 抽离css的输出目录和名称
    }),
  ],
});
export default prodConfig;

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

image.png

6.3 样式压缩

可以看到,上面配置了打包时把css抽离为单独css文件的配置,打开打包后的文件查看,可以看到默认css是没有压缩的,需要手动配置一下压缩css的插件。

可以借助 css-minimizer-webpack-plugin 来压缩css,安装依赖:

pnpm add css-minimizer-webpack-plugin -D

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

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

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

微信图片_20230612193111.png

6.4 js 压缩

依赖 说明
terser-webpack-plugin 用于处理 js 的压缩和混淆
css-minimizer-webpack-plugin 压缩css文件
compression-webpack-plugin 预先准备的资源压缩版本,使用 Content-Encoding 提供访问服务


设置mode为production时,webpack会使用内置插件terser-webpack-plugin压缩js文件,该插件默认支持多线程压缩,但是上面配置optimization.minimizer压缩css后,js压缩就失效了,需要手动再添加一下,webpack内部安装了该插件,由于pnpm解决了幽灵依赖问题,如果用的pnpm的话,需要手动再安装一下依赖。

pnpm i terser-webpack-plugin compression-webpack-plugin -D

修改 webpack.prod.ts 文件:

import path from "path";
import { Configuration } from "webpack";
import { merge } from "webpack-merge";
import CopyPlugin from "copy-webpack-plugin";
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
import TerserPlugin from 'terser-webpack-plugin'
import CompressionPlugin from 'compression-webpack-plugin'
import baseConfig from "./webpack.base";
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const prodConfig: Configuration = merge(baseConfig, {
  mode: "production", // 生产模式,会开启tree-shaking和压缩代码,以及其他优化
  /**
   * 打包环境推荐:none(就是不配置devtool选项了,不是配置devtool: 'none')
   * ● none话调试只能看到编译后的代码,也不会泄露源代码,打包速度也会比较快。
   * ● 只是不方便线上排查问题, 但一般都可以根据报错信息在本地环境很快找出问题所在。
   */
  plugins: [
    new CopyPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, "../public"), // 复制public下文件
          to: path.resolve(__dirname, "../dist"), // 复制到dist目录中
          filter: (source) => !source.includes("index.html"), // 忽略index.html
        },
      ],
    }),
    new MiniCssExtractPlugin({
      filename: 'static/css/[name].css' // 抽离css的输出目录和名称
    }),
    // 打包时生成gzip文件
    new CompressionPlugin({
      test: /\.(js|css)$/, // 只生成css,js压缩文件
      filename: '[path][base].gz', // 文件命名
      algorithm: 'gzip', // 压缩格式,默认是gzip
      threshold: 10240, // 只有大小大于该值的资源会被处理。默认值是 10k
      minRatio: 0.8 // 压缩率,默认值是 0.8
    })
  ],
  optimization: {
    // splitChunks: {
    //   chunks: "all",
    // },
    runtimeChunk: {
      name: 'mainifels'
    },
    minimize: true,
    minimizer: [
      new CssMinimizerPlugin(), // 压缩css
      new TerserPlugin({
        parallel: true, // 开启多线程压缩
        terserOptions: {
          compress: {
            pure_funcs: ['console.log'] // 删除console.log
          }
        }
      })
    ],
  },
  performance: {
    hints: false,
    maxAssetSize: 4000000, // 整数类型(以字节为单位)
    maxEntrypointSize: 5000000 // 整数类型(以字节为单位)
  }
});
export default prodConfig;

配置完成后再打包,css和js就都可以被压缩了:

微信图片_20230612193223.png

6.5 文件指纹

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

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

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

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

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

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

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

// webpack.base.ts
// ...
const baseConfig: Configuration = {
  // 打包文件出口
  output: {
    filename: 'static/js/[name].[chunkhash:8].js', // // 加上[chunkhash:8]
    // ...
  },
  module: {
    rules: [
      {
        test: imageRegex, // 匹配图片文件
        // ...
        generator:{ 
          filename:'static/images/[name].[contenthash:8][ext]' // 加上[contenthash:8]
        },
      },
      {
        test: fontRegex, // 匹配字体文件
        // ...
        generator:{ 
          filename:'static/fonts/[name].[contenthash:8][ext]', // 加上[contenthash:8]
        },
      },
      {
        test: mediaRegex, // 匹配媒体文件
        // ...
        generator:{ 
          filename:'static/media/[name].[contenthash:8][ext]', // 加上[contenthash:8]
        },
      },
    ]
  },
  // ...
}

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

// webpack.prod.ts
// ...
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 了:
微信图片_20230612193347.png

6.6 代码分割

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

修改 webpack.prod.ts

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

配置完成后执行打包,可以看到 node_modules 里面的模块被抽离到 verdors.xxx.js 中,业务代码在 main.xxx.js 中:

微信图片_20230612193434.png
测试一下,此时 verdors.jschunkhash0d771b2fmain.js 文件的 chunkhash56a4dd60,改动一下 App.tsx,再次打包,可以看到下图 main.jschunkhash 值变化了,但是 vendors.jschunkhash 还是原先的,这样发版后,浏览器就可以继续使用缓存中的 verdors.0d771b2f.js,只需要重新请求 main.js 就可以了。

6.7 tree-shaking清理未引用js

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

src/components 目录下新增 Demo1.tsxDemo2.tsx 两个组件

// 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 移除掉了:

微信图片_20230612193553.png

6.8 tree-shaking清理未使用css

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

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

修改 webpack.prod.ts

// ...
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
const globAll = require('glob-all')
const { PurgeCSSPlugin } = require('purgecss-webpack-plugin')
const prodConfig: Configuration = merge(baseConfig, {
  // ...
  plugins: [
    // 抽离css插件
    new MiniCssExtractPlugin({
      filename: 'static/css/[name].[contenthash:8].css'
    }),
    // 清理无用css,检测src下所有tsx文件和public下index.html中使用的类名和id和标签名称
    // 只打包这些文件中用到的样式
    new PurgeCSSPlugin({
      paths: globAll.sync(
        [`${path.join(__dirname, '../src')}/**/*`, path.join(__dirname, '../public/index.html')],
        {
          nodir: true
        }
      ),
      // 用 only 来指定 purgecss-webpack-plugin 的入口
      // https://github.com/FullHuman/purgecss/tree/main/packages/purgecss-webpack-plugin
      only: ["dist"],
      safelist: {
        standard: [/^ant-/] // 过滤以ant-开头的类名,哪怕没用到也不删除
      }
    }),
  ]
})

6.9 资源懒加载

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 handleOnClick = () => {
    import('@/App.css')
    setShow(true)
  }
  return (
    <>
      <h2 onClick={handleOnClick}>展示</h2>
      {/* show为true时加载LazyDemo组件 */}
      { show && <Suspense fallback={null}><LazyDemo /></Suspense> }
    </>
  )
}
export default App

点击展示文字时,才会动态加载app.cssLazyDemo组件的资源。

微信图片_20230612193736.png
或者写一个函数:

import React from "react";
import { FC, lazy, Suspense } from "react";
interface LazyWrapperProps {
  /** 组件路径: 在 src/pages 目录下的页面路径,eg: /home => src/pages/home/index.tsx */
  path: string;
}
/**
 * 懒加载组件包装器
 */
const LazyWrapper: FC<LazyWrapperProps> = ({ path }) => {
  const LazyComponent = lazy(() => import(`@/components/${path}`));
  return (
    <Suspense fallback={<div>loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
};
export default LazyWrapper;

使用:

import { Suspense, lazy, useState } from 'react'
// import '@/App.css'
import lessStyles from '@/app.less'
import scssStyles from '@/app.scss'
import stylStyles from '@/app.styl'
import smallImg from '@/assets/imgs/5kb_img.jpeg'
import bigImg from '@/assets/imgs/10kb_img.png'
import chengzi from '@/assets/imgs/chengzi.png'
import memberList from './test.json'
import ClassComp from '@/components/Class'
import { Demo1, Demo2 } from '@/components'
import {watchEnv, add} from '@/utils/watch'
import LazyWrapper from '@/components/LazyWrapper'
const LazyDemo = lazy(() => import('@/components/LazyDemo')) 
function App() {
  const [ count, setCounts ] = useState('')
  const [ show, setShow ] = useState(false)
  const onChange = (e: any) => {
    setCounts(e.target.value)
  }
  console.log('memberList', memberList)
  // 点击事件中动态引入css, 设置show为true
  const handleOnClick = () => {
    import('@/App.css')
    setShow(true)
  }
  return <div>
    <h2>webpack5-react-ts</h2>
    <div className={lessStyles['lessBox']}>
      <div className={lessStyles['box']}>lessBox(east_white)
      <img src={smallImg} alt="小于10kb的图片" />
      <img src={bigImg} alt="大于于10kb的图片" />
      <img src={chengzi} alt="橙子font" />
      <div className={lessStyles['smallImg']}>小图片背景</div> 
      <div className={lessStyles['bigImg']}>大图片背景</div> 
      </div>
    </div>
    <div className={scssStyles['scssBox']}>
      <div className={scssStyles['box']}>scssBox</div>
    </div>
    <div className={stylStyles['stylBox']}>
      <div className={stylStyles['box']}>stylBox</div>
    </div>
    <ClassComp />
    <div>
        <p>受控组件</p>
        <input type="text" value={count} onChange={onChange} />
        <br />
        <p>非受控组件</p>
        <input type="text" />
    </div>
    <Demo1 />
    <div>{add(1, 2)}</div>
    <>
      <h2 onClick={handleOnClick}>展示</h2>
      {/* show为true时加载LazyDemo组件 */}
      { show && <Suspense fallback={null}><LazyWrapper path='LazyDemo' /></Suspense> }
    </>
  </div>
}
export default App

6.10 资源预加载

上面配置了资源懒加载后,虽然提升了首屏渲染速度,但是加载到资源的时候会有一个去请求资源的延时,如果资源比较大会出现延迟卡顿现象,可以借助link标签的rel属性prefetch与preload,link标签除了加载css之外也可以加载js资源,设置rel属性可以规定link提前加载资源,但是加载资源后不执行,等用到了再执行。

rel的属性值

  • preload是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源。
  • prefetch是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源,会在空闲时加载。

对于当前页面很有必要的资源使用 preload,对于可能在将来的页面中使用的资源使用 prefetch

webpack v4.6.0+ 增加了对 预获取和预加载 的支持,使用方式也比较简单,在 import 引入动态资源时使用 webpack 的魔法注释:

// 单个目标
import(
  /* webpackChunkName: "my-chunk-name" */ // 资源打包后的文件chunkname
  /* webpackPrefetch: true */ // 开启prefetch预获取
  /* webpackPreload: true */ // 开启preload预获取
  './module'
);

测试一下,在 src/components 目录下新建 PreloadDemo.tsxPreFetchDemo.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组件资源,但是不执行里面的代码,等点击展示按钮后从预加载的资源中直接取出来执行,不用再从服务器请求,节省了很多时间。

微信图片_20230612193935.png
在测试时发现只有js资源设置 prefetch 模式才能触发资源预加载,preload 模式触发不了,css 和图片等资源不管设置 prefetch 还是 preload 都不能触发,不知道是哪里没配置好。

6.11 gzip 压缩

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

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

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

pnpm i compression-webpack-plugin -D

添加配置,修改 webpack.prod.ts

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
      threshold: 10240, // 只有大小大于该值的资源会被处理。默认值是 10k
      minRatio: 0.8 // 压缩率,默认值是 0.8
    })
  ]
}

配置完成后再打包,可以看到打包后js的目录下多了一个 .gz 结尾的文件:

微信图片_20230612194057.png
因为只有verdors.js的大小超过了10k, 所以只有它生成了gzip压缩文件,借助serve -s dist启动dist,查看verdors.js加载情况

微信图片_20230612194124.png
可以看到 verdors.js 的原始大小是 182kb,使用 gzip 压缩后的文件只剩下了 60.4kb,减少了 70% 的大小,可以极大提升页面加载速度。

7、webpack 其他优化

优化并不是一蹴而就的,一般是随着项目的发展逐步针对性优化,本系列主要谈论一个项目的基本架子,故只对 webpack 做基础的优化。除了上面的配置外,webpack还提供了其他的一些优化方式,可以在真正实际开发的时候逐步实践,网上已经有大量的资源来对这个方面多深入的实践,可以参考如下:

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

下一篇,我们将引入一些代码规范和代码风格相关的工具来规范我们的工程~


相关文章
|
3月前
|
移动开发 前端开发 JavaScript
React DnD:实现拖拽功能的终极方案?
本文首发于微信公众号“前端徐徐”,介绍了一个强大的 React 拖拽库——React DnD。React DnD 帮助开发者轻松创建复杂的拖拽界面,适用于 Trello 风格的应用、列表重排序、可拖拽的 UI 组件等场景。文章详细介绍了 React DnD 的基本信息、主要特点、使用场景及快速上手指南。
298 3
React DnD:实现拖拽功能的终极方案?
|
4月前
|
移动开发 前端开发
react项目配合diff实现文件对比差异功能
在React项目中,可以使用`diff`库实现文件内容对比差异功能。首先安装`diff`库,然后在组件中引入并使用`Diff.diffChars`或`Diff.diffLines`方法比较文本差异。通过循环遍历`diff`结果,可以生成不同样式的HTML元素来高亮显示文本差异。
193 1
react项目配合diff实现文件对比差异功能
|
4月前
|
前端开发
react配置proxy代理的两种方式
本文介绍了在React项目中配置代理的两种方式:通过在package.json中添加代理配置,以及通过创建setupProxy.js文件来实现更复杂的代理规则。
199 2
|
4月前
|
前端开发
React 中购物车功能实现(全选多选功能实现)
React 中购物车功能实现(全选多选功能实现)
49 2
|
3月前
|
前端开发 JavaScript Shell
深入解析前端构建利器:webpack核心概念与基本功能全览
深入解析前端构建利器:webpack核心概念与基本功能全览—
41 0
|
4月前
|
前端开发 开发者
在前端开发中,webpack 作为一个强大的模块打包工具,为我们提供了丰富的功能和扩展性
【9月更文挑战第1天】在前端开发中,Webpack 作为强大的模块打包工具,提供了丰富的功能和扩展性。本文重点介绍 DefinePlugin 插件,详细探讨其原理、功能及实际应用。DefinePlugin 可在编译过程中动态定义全局变量,适用于环境变量配置、动态加载资源、接口地址配置等场景,有助于提升代码质量和开发效率。通过具体配置示例和注意事项,帮助开发者更好地利用此插件优化项目。
94 13
|
4月前
|
存储 移动开发 前端开发
初探react,用react实现一个todoList功能
该文章通过创建一个TodoList应用来介绍React的基础知识,包括环境搭建、组件创建、状态管理和事件处理,并演示了如何使用React Hooks来优化组件逻辑。
|
5月前
|
前端开发
|
5月前
|
存储 监控 前端开发
|
5月前
|
存储 JavaScript 前端开发
探索React状态管理:Redux的严格与功能、MobX的简洁与直观、Context API的原生与易用——详细对比及应用案例分析
【8月更文挑战第31天】在React开发中,状态管理对于构建大型应用至关重要。本文将探讨三种主流状态管理方案:Redux、MobX和Context API。Redux采用单一存储模型,提供预测性状态更新;MobX利用装饰器语法,使状态修改更直观;Context API则允许跨组件状态共享,无需第三方库。每种方案各具特色,适用于不同场景,选择合适的工具能让React应用更加高效有序。
112 0