使用Webpack搭建React脚手架(中篇)

上一篇中,通过配置webpack和环境变量,从而使得脚手架有了初始化的成效。

本篇,我们来继续搭建基础功能配置。

引入 less、sass(scss)、stylus

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

1. 避免全局样式冲突:使用 CSS Modules 可以确保样式只应用于特定组件,避免全局样式冲突。

2. 更好的可维护性: CSS Modules 使得样式与组件代码紧密关联,方便代码维护。

3. 提高代码可重用性: CSS Modules 可以轻松地将样式从一个组件复制到另一个组件,提高代码可 重用性。

4. 支持动态样式:使用 CSS Modules 可以轻松地生成动态样式,例如根据组件状态或属性更改样 式。

5. 更好的性能: CSS Modules 使用模块化的方式加载样式,提高了页面加载速度和性能。

基本用法

先安装相关的依赖

pnpm i less less-loader sass-loader 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,
          {
            loader: "sass-loader",
            options: {
              implementation: require("sass"), // 使用dart-sass代替node-sass
            },
          },
        ],
      },
      {
        test: stylRegex,
        use: [...styleLoadersArray, "stylus-loader"],
      },
    ],
  },
};
export default baseConfig;

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

/* src/app.less */
@color: red;
.lessBox {
.box {
color: @color;
background-color: lightblue;
transform: translateX(100);
&:before{
@arr: 'hello', 'world';
content: `@{arr}.join(' ').toUpperCase()`;
}
}
}
/* src/app.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.styl */
.stylBox
.box
color: red;
background-color: yellow;

 在 App.tsx 中引入:

import "@/App.css";
import lessStyles from "./app.less";
import scssStyles from "./app.scss";
import stylStyles from "./app.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 类名,且里面包含了我们自定义的类名,方便日后调试 用

同时在验证打包是否成功,运行 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;

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

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

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

pnpm i 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

兼容到 ie9 和 chrome35 版本为例,配置好后,在 app.module.less 中加入一些CSS3的语法,重新启 动项目,就可以在浏览器的控制台-Elements 中看到配置成功了。 执行 pnpm run build:dev 打包,也可以看到打包后的 css 文件已经加上了 ie 和谷歌内核的前缀。 

babel处理js非标准语法

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

import React, { PureComponent } from "react";
// 装饰器为,组件添加age属性
function addAge(Target: Function) {
  Target.prototype.age = 111;
}
// 使用装饰器
@addAge
class Cls extends PureComponent {
  age?: number;
  render() {
    return <h2>我是类组件---{this.age}</h2>;
  }
}
export default Cls;

需要开启一下 ts 装饰器支持,修改 tsconfig.json 文件

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

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

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

在 babel.config.js 中添加插件:

const isDEV = process.env.NODE_ENV === "development"; // 是否是开发模式
module.exports = {
  // 执行顺序由右往左,所以先处理ts,再处理jsx,最后再试一下babel转换为低版本语法
  presets: [
    [
      "@babel/preset-env",
      {
        // 设置兼容目标浏览器版本,也可以在根目录配置.browserslistrc文件,babel-loader会自动寻找上面配置好的文件.browserlistrc
        // "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
  ), // 过滤空值
};

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

webpack构建速度优化

 webpack 进度条

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

pnpm i 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) {
	// 当编译完成后调用
	},
}

 当然多数情况下,我们并不会使用这些,基本默认就足够了。

构建耗时

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

pnpm i speed-measure-webpack-plugin -D

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

import { Configuration } from "webpack";
import prodConfig from "./webpack.prod"; // 引入打包配置
import { merge } from "webpack-merge"; // 引入合并webpack配置方法
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); 
// 引入webpack打包速度分析插件
const smp = new SpeedMeasurePlugin(); // 实例化分析插件

// 使用smp.wrap方法,把生产环境配置传进去,由于后面可能会加分析配置,所以先留出合并空位

const analyConfig: Configuration = smp.wrap(merge(prodConfig, {

}));
export default analyConfig;

 修改 package.json 添加启动 webpack 打包分析脚本命令,在 scripts 新增:

{
	"scripts": {
	"build:analy": "cross-env NODE_ENV=production BASE_ENV=production webpack -cbuild/webpack.analy.ts"
	}
}

 执行 pnpm build:analy 命令

开启持久化存储缓存

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

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

module.exports = {
  cache: {
    type: "filesystem", // 使用文件缓存
  },
};

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

 开启多线程 loader

 运行在 Node.js 之上的 webpack 是单线程模式的,也就是说, webpack 打包只能逐个文件处理,当 webpack 需要打包大量文件时,打包时间就会比较漫长。 多进程/多实例构建的方案比较知名的有以下三种:

thread-loader

parallel-webpack

HappyPack

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

pnpm i thread-loader -D

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

修改 webpack.base.ts

module: {
  rules: [
    {
      test: /\.(ts|tsx)$/, // 匹配ts和tsx文件
      use: [
        // 开启多进程打包。
        // 进程启动大概为600ms,进程通信也有开销。
        // 只有工作消耗时间比较长,才需要多进程打包
        {
          loader: "thread-loader",
          options: {
            wokers: 4, // 进程数
          },
        },
        "babel-loader",
      ],
    },
  ];
}

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

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

 缩小构建目标

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

include :只解析该选项配置的模块

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

修改 webpack.base.ts

module: {
  rules: [
    {
      test: /\.(ts|tsx)$/, // 匹配ts和tsx文件
      exclude: /node_modules/,
      use: [
        // 开启多进程打包。
        // 进程启动大概为600ms,进程通信也有开销。
        // 只有工作消耗时间比较长,才需要多进程打包
        {
          loader: "thread-loader",
          options: {
            wokers: 4, // 进程数
          },
        },
        "babel-loader",
      ],
    },
  ];
}

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

devtools 配置

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

devtool 的命名规则为:

^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$

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

 webpack构建产物优化

 bundle 体积分析工具

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

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

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

样式提取 

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

pnpm i 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, // 开发环境使用stylelooader,打包模式抽离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 文件。

样式压缩

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

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

pnpm i 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 已经被压缩了

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, // 设置为false将关闭性能提示。默认情况下,Webpack会显示有关入口点和资产大小的警告和错误消息。将hints设置为false可以禁用这些消息。
    maxAssetSize: 4000000, // 设置一个整数,表示以字节为单位的单个资源文件的最大允许大小。
		//如果任何资源的大小超过这个限制,Webpack将发出性能警告。在你提供的配置中,这个值被设置为4000000字节(约4MB)。
    maxEntrypointSize: 5000000, // 设置一个整数,表示以字节为单位的入口点文件的最大允许大小。
		//入口点是Webpack构建产生的主要JS文件,通常是应用程序的主要代码。如果入口点的大小超过这个限制,Webpack将发出性能警告。在你提供的配置中,这个值被设置为5000000字节(约5MB)。
  },
});
export default prodConfig;

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

 文件指纹

项目维护的时候,一般只会修改一部分代码,可以合理配置文件缓存,来提升前端加载页面速度和减少服务器压力,而 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                                           文件所在文件夹

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: mediaRegex, // 匹配媒体文件
        generator: {
          filename: "static/media/[name].[contenthash:8][ext]", // 文件输出目录和命名
        },
      },
      {
        test: fontRegex, // 匹配字体图标文件
        generator: {
          filename: "static/json/[name].[contenthash:8][ext]", // 文件输出目录和命名
        },
      },
      {
        test: imageRegex, // 匹配图片文件
        generator: {
          filename: "static/images/[name].[contenthash:8][ext]", // 加上[contenthash:8]
        },
      },
      {
        // 匹配json文件
        test: jsonRegex,
        type: "json", // 模块资源类型为json模块
        generator: {
          filename: "static/json/[name].[hash][ext][query]", // 专门针对json文件的处理
        },
      },
    ],
  },
};

再修改 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 了。

代码分割 

一般第三方包的代码变化频率比较小,可以单独把 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 中

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

tree-shaking清理未引用js

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

在 src/components 目录下新增 Demo1.tsx 、 Demo2.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 ,把 Demo1 和 Demo2 组件引入进来再暴露出去: 

// 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 组件被 treeshaking 移除掉了。

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/purgecsswebpack-plugin
      only: ["dist"],
      safelist: {
        standard: [/^ant-/], // 过滤以ant-开头的类名,哪怕没用到也不删除
      },
    }),
  ],
});

资源懒加载 

像 react , vue 等单页应用打包默认会打包到一个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.css 和 LazyDemo 组件的资源或者写一个函数:

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;

资源预加载 

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

// 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"
    )
);
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 文章,安装依赖: 

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

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

优化并不是一蹴而就的,一般是随着项目的发展逐步针对性优化,本系列主要谈论一个项目的基本架子,故只对 webpack 做基础的优化。除了上面的配置外,可以在真正实际开发的时候逐步实践 ,以上为React中篇的内容。

  • 14
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值