likes
comments
collection
share

webpack5 从0搭建react SPA

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

如果从0创建一个实际的单页面应用的话,还是比较推荐 vite。不过我之前接触的大部分都是基于 create-react-app 搭建的项目,就想着现在基于 webpack5 从0开始搭建一个 react 项目需要做哪些事?是不是可以用上一些比较新的 loader 比如 swc-loader?于是带着这些疑问就开始折腾了

其实 webpack 干的事就是将多个模块(例如js文件、css文件、图片等)按照指定的规则打包成一个或多个文件,然后支持多种模块化方案,比如 ES Module、CommonJS、AMD 等,并且有比较大的插件生态,可以帮助我们高效地开发项目。官网中文文档 还是很详细的。以下长文警告...

一些准备工作

  • Node 环境
    • 通过 fnm 管理 node 版本,可以参考 fnm使用
    • Node.js 基于 latest
  • 包管理用 pnpm,安装方式 npm i -g pnpm
  • React 基于 v18

初始化

新建个目录,然后通过 npm init初始化项目,之后会生成 package.json常见字段解释戳这个

接下来目标是先搭建一个能展示 html 的 demo,内容就简单输出一句 "hello my-cra"

先新建 public/index.html,后面这个 html 就作为模板了,然后建个 src/index.js 作为入口文件

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My CRA</title>
  </head>
  <body>
    <div>hello my-cra</div>
  </body>
</html>

server

那首先得有个静态服务返回 html 文档,需要安装以下库:

pnpm install webpack-dev-server webpack-cli webpack -D
  • webpack-cli是 webpack 的命令行接口,和 webpack 进行交互,借助它我们可以通过命令行来运行 webpack,或者运行 webpack-dev-server 来创建一个开发服务器
  • webpack-dev-server就是基于 express 起个静态服务,当然还提供了热模块替换(Hot Module Replacement)和实时重载(Live Reloading)的功能

新建webpack配置文件 webpack.config.js,内容如下:

const path = require("path");
const isProduction = process.env.NODE_ENV === "production";

const config = {
  devServer: {
    static: {
      directory: path.join(__dirname, "public"),
    },
    port: 8000,
  },
};

module.exports = () => {
  if (isProduction) {
    config.mode = "production";
  } else {
    config.mode = "development";
  }
  return config;
};

增加服务启动命令,修改 package.json

"scripts": {
    "serve": "webpack serve",
}

执行 pnpm run serve,访问 8000 页面,诶,第一步不就迈出来了

html注入js文件

html模板一般是空的,需要解析js文件后再渲染到root节点上,这里定义root节点的id为root,还需要一个入口文件 src/index.js,顺便把 html 模板挪到 src 下吧 src/index.html

注入模板就需要用到 HtmlWebpackPlugin ,这个插件作用是在构建时将js、css等文件插入到 html 模板中,然后生成最终的 html 文件

# 安装
pnpm install html-webpack-plugin -D

修改配置文件 webpack.config.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

const config = {
  // 入口文件
  entry: "./src/index.js",
  // 产物配置
  output: {
    path: path.resolve(__dirname, "./dist"),
    filename: "index_bundle.js",
  },
  // 插件
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
      filename: "index.html",
    }),
  ],
  // ...
};

html 模板也要修改下

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My CRA</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

ok,重新 pnpm run serve 看下效果

代码转换

有经验的同学应该知道,webpack 构建相对耗时的两个部分是代码转换和代码压缩

swc-loader

我们一般需要通过 babel 编译js,将现代 ES6+ 语法和特性转换成向后兼容的语法,确保能够运行在当前和旧版本的浏览器或其他环境中。但其实主流浏览器已经能支持ESModule和绝大部分语法了,这也是vite这些构建工具流行的主要原因

这里使用 swc-loader 替换 babel-loader

 // webpack.config.js
 const config = {
   // ...
   module: {
    rules: [
      {
        test: /.m?js$/,
        exclude: /(node_modules)/,
        use: {
          loader: "swc-loader",
          options: {
            jsc: {
              parser: {
                syntax: "ecmascript",
                tsx: false,
                decorators: true,
              },
              transform: {
                legacyDecorator: true,
              },
              // 以下配置需要先安装 @swc/helpers
              externalHelpers: true,
              target: "es5",
            },
            isModule: "unknown",
          },
        },
      },
    ],
  },

写个demo转换下试试

// src/index.js
const root = document.getElementById("root");
root.textContent = "hello my-cra";
export let a = 1;
export const map = new Map();
map.set("name", "Lucas");
const fn = () => {
  console.log("===fn", a, map, x);
};
console.log(fn);

执行 pnpm run build,生成的js文件如下

(() => {
  "use strict";
  var e = document.getElementById("root");
  e && (e.textContent = "hello my-cra");
  var o = new Map();
  o.set("name", "Lucas"),
    console.log(function () {
      console.log("===fn", 1, o, x);
    });
})();

typescript

我们可以借助 swc 编译 ts 代码,这样就不需要引入额外的ts编译器

修改 swc-loader 的配置

// ...
module: {
    rules: [
      {
        // 匹配 ts 和 js 文件
        test: /.[tj]s$/,
        exclude: /(node_modules)/,
        use: {
          loader: "swc-loader",
          options: {
            jsc: {
              parser: {
                // 修改
                syntax: 'typescript',
                // 支持装饰器
                decorators: true,
              },
              transform: {
                legacyDecorator: true,
                react: {
                  // NOTE:在转换React代码时,SWC将自动引入运行时代码,确保jsx语法能正常编译
                  runtime: 'automatic',
                },
              },
              // ...
            },
            // ...
          },
        },
      },
    ],
  },

增加 typescript 的配置文件 tsconfig.json详细参数解释戳这里

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    /* Linting */
    "strict": true,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"]
}

src/index.js 改为 src/index.ts

enum EDirection {
  None,
  Left,
  Right,
}
const root = document.getElementById("root");
root.textContent = "hello my-cra";
export let a: number = 1;
export const map = new Map();
map.set("name", "Lucas");
const fn = () => {
  console.log("===fn", a, map, EDirection.Left);
};
console.log(fn);

执行 pnpm run build 打包生成文件如下:

(() => {
  "use strict";
  var e;
  (function (e) {
    // 双向map
    (e[(e.None = 0)] = "None"),
      (e[(e.Left = 1)] = "Left"),
      (e[(e.Right = 2)] = "Right");
  })(e || (e = {})),
    (document.getElementById("root").textContent = "hello my-cra");
  var t = new Map();
  t.set("name", "Lucas"),
    console.log(function () {
      console.log("===fn", 1, t, 1);
    });
})();

React

swc 也支持编译 jsx,加下配置就行

pnpm install react react-dom
pnpm install @types/react @types/react-dom -D

修改配置

// ...
module: {
    rules: [
      {
        test: /.[tj]sx?$/,
        exclude: /(node_modules)/,
        use: {
          loader: "swc-loader",
          options: {
            jsc: {
              parser: {
                syntax: "typescript",
                decorators: true,
                // 新增
                tsx: true,
              },
              // ...
            },
            // ...
          },
        },
      },
    ],
  },

修改 index.tsx

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

新增 App.tsx

const App = () => {
  return <div>hello my-cra</div>;
};

export default App;

这里其实还有个问题,我在导入App这个组件时是这样导入的

import App from "./App.tsx";

但实际项目中我们都是直接写 "./App"。这个要怎么做?其实加个 resolve 的配置就行了

const config = {
    // ...
    resolve: {
        // 按照数组顺序解析文件
        extensions: [".tsx", ".ts", ".jsx", ".js"],
    },
    // ...
}

注意:如果你的项目依赖特定的 Babel 插件或者比较复杂,那么使用 SWC 前可能还需要经过比较详细的测试,也就需要在成本投入和性能之间做个衡量

CSS管理

接下来考虑加下样式。怎么让下面这一句生效?

import "./App.css"

这个时候就需要将样式注入html文件了,需要用到一些 loaders

  • style-loader。负责将 CSS 注入到 DOM 中,可以选择 style 标签(默认)或者通过 link 标签引入
  • css-loader。负责解析 CSS 文件中的 @importurl() 引用,并将它们转换为模块依赖
pnpm install css-loader style-loader -D

修改 webpack 配置如下:

module: {
    rules: [
      // ...
      {
        test: /.css$/i,
        use: ["style-loader", "css-loader"],
      },
    ]
 }

新增 App.css

.app {
  padding: 16px;
  font-size: 50px;
}

然后在 App.tsx 引入后刷新页面看下效果

import "./App.css";
const App = () => {
  return <div className="app">hello my-cra</div>;
};
export default App;

预处理

使用less作为 css 预处理语言,需要借助less-loader

pnpm install less less-loader -D

修改 webpack 配置如下:

module: {
    rules: [
      // ...
      {
        test: /.(less|css)$/i,
        use: ["style-loader", "css-loader", "less-loader"],
      },
    ]
 }

改为 App.less

@textColor: red;

.app {
  padding: 16px;

  .text {
    color: @textColor;
    font-size: 50px;
  }
}

然后在 App.tsx 引入后刷新页面看下效果

import "./App.less";

const App = () => {
  return (
    <div className="app">
      <div className="text">hello my-cra</div>
    </div>
  );
};

export default App;

css module

css-loader 默认开启这个配置。但是实际开发过程,我们往往需要留下类名作为前缀便于调试,这里设置为 类名+随机hash值取5位,所以需要改下 css-loader 的配置

module: {
    rules: [
      // ...
      {
        test: /(.module)?.less$/i,
        use: [
          "style-loader",
          {
            loader: require.resolve("css-loader"),
            options: {
              modules: {
                auto: true,
                localIdentName: "[local]_[hash:base64:5]",
              },
            },
          },
          "less-loader",
        ],
      },
      {
        test: /(.module)?.css$/i,
        use: [
          "style-loader",
          {
            loader: require.resolve("css-loader"),
            options: {
              modules: {
                auto: true,
                localIdentName: "[local]_[hash:base64:5]",
              },
            },
          },
        ],
      },
    ]
 }

修改 App.tsx

import styles from "./App.module.less";

const App = () => {
  return (
    <div className={styles["app"]}>
      <div className={styles["text"]}>hello my-cra</div>
    </div>
  );
};

export default App;

postcss

需要借助 postcss-loader。有比较多好用的插件,比如 postcss-preset-envpostcss-px2remstylelint等。postcss-preset-env可以根据指定的目标浏览器或运行环境(在 package.json 中配置 browserslist),自动将现代的css特性转换为大多数浏览器能够理解的css代码,这其中就包括自动添加所需的浏览器前缀(这个插件内置了 autoprefixer

pnpm install postcss postcss-loader postcss-preset-env -D

修改 webpack 配置如下:

 // ...
module: {
    rules: [
      // ...    
      {
        test: /(.module)?.less$/i,
        use: [
          "style-loader",
          {
            loader: require.resolve("css-loader"),
            options: {
              modules: {
                auto: true,
                localIdentName: "[local]_[hash:base64:5]",
              },
            },
          },
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: ["postcss-preset-env"],
              },
            },
          },
          "less-loader",
        ],
      },
      {
        test: /(.module)?.css$/i,
        use: [
          "style-loader",
          {
            loader: require.resolve("css-loader"),
            options: {
              modules: {
                auto: true,
                localIdentName: "[local]_[hash:base64:5]",
              },
            },
          },
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: ["postcss-preset-env"],
              },
            },
          },
        ],
      },
    ]
 }

然后测试下

::placeholder {
  color: #ccc;
}

正常的话,应该会输出以下内容:

::-moz-placeholder {
  color: #ccc;
}
::placeholder {
  color: #ccc;
}

分离css

依赖 mini-css-extract-plugin 插件,一般在生产环境中使用,开发环境还是通过 style-loader 将样式注入 js 文件

pnpm i -D mini-css-extract-plugin@1.3.6

细心的同学应该发现了,这里固定了版本,因为大于这个版本就会和后面提到的耗时分析插件 speed-measure-webpack-plugin 有冲突了,详细内容参考 github.com/stephencook…

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// ...
{
    test: /(.module)?.less$/i,
    use: [
      isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
      {
        loader: require.resolve('css-loader'),
        options: {
          modules: {
            auto: true,
            localIdentName: '[local]_[hash:base64:5]',
          },
        },
      },
      {
        loader: 'postcss-loader',
        options: {
          postcssOptions: {
            plugins: ['postcss-preset-env'],
          },
        },
      },
      'less-loader',
    ],
  },
  {
    test: /(.module)?.css$/i,
    use: [
      isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
      {
        loader: require.resolve('css-loader'),
        options: {
          modules: {
            auto: true,
            localIdentName: '[local]_[hash:base64:5]',
          },
        },
      },
      {
        loader: 'postcss-loader',
        options: {
          postcssOptions: {
            plugins: ['postcss-preset-env'],
          },
        },
      },
    ],
  },
// ...
plugins: [
    new MiniCssExtractPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html',
    })
],

静态资源处理

webpack5 自带的资源处理器,和 webpack4 的处理方式有明显区别,可以参考 webpack5升级小结

  • asset/resource 导出文件 URL(file-loader)
  • asset/inline 导出一个资源的 dataURI(url-loader)
  • asset 在导出一个 dataURI 和导出文件 URL 之间自动选择
  • asset/source 导出资源的源代码

处理图像

可以设定一个阈值,不超过这个阈值时就将图像转成base64内联到产物中,减少请求数

module: {
    rules: [
        // ...
        {
            test: /.(png|svg|jpg|jpeg|gif)$/i,
            type: "asset",
            parser: {
              dataurlCondition: {
                maxSize: 1024, // 单位是B
              },
            },
          },
    ],
},

其他资源

下面是字体资源的例子,其他资源还有音频、视频、3d模型等等,其实也是类似的处理

module: {
    rules: [
        // ...
        {
          test: /.(woff|woff2|eot|ttf|otf)$/i,
          type: 'asset/resource',
       },
    ],
},

devServer

热更新

webpack-dev-serverwebpack-dev-middleware 里默认开启 watch 模式,会自动重新编译文件,这个过程其实包括了整个应用的重新打包和加载,所以相对来说速度较慢。而热更新 HMR 是一种更高效的重新编译的方式,只替换局部模块,当然,webpack5默认开启,配置方式如下:

// ...
devServer: {
    static: {
      directory: path.join(__dirname, "public"),
    },
    // 热更新
    hot: true,
    // 开启gzip压缩
    compress: true,
    // 指定静态服务的端口
    port: 8000,
    // 在默认浏览器自动打开页面
    open: true
},

可以将 hot 设置为 false,前后对比一下,感受下局部刷新和全局刷新的区别(打开控制台 Elements 可以看到刷新效果)

代码规则约束

vscode 配置

新增 .vscode/settings.json,下面是设置了保存时自动格式化代码,然后格式化程序默认使用 prettier

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode"
}

eslint

记得给vscode安装eslint插件

pnpm install eslint -D
# 初始化,生成 .eslintrc.js
npx eslint --init

参考配置文件如下:

# .eslintrc.js
module.exports = {
  root: true,
  env: {
    browser: true,
    es2021: true,
    node: true,
    jest: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
  ],
  overrides: [
    {
      env: {
        node: true,
      },
      files: ['.eslintrc.{js,cjs}'],
      parserOptions: {
        sourceType: 'script',
      },
    },
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  plugins: ['@typescript-eslint', 'react'],
  rules: {
    // 允许使用require
    '@typescript-eslint/no-var-requires': 0,
    // 关掉react需要默认导入React的问题,在react>=17时需要
    'react/react-in-jsx-scope': 'off',
    'react/jsx-uses-react': 'off',
  },
  // 防止出现warning:React version not specified in eslint-plugin-react settings
  settings: {
    react: {
      version: 'detect',
    },
  },
};

package.json 增加 script 命令,用于全局检查项目代码

"scripts": {
    // ...
    "eslint": "eslint --fix "src/**/*.{js,ts,jsx,tsx}"",
}

prettier

记得给vscode安装prettier插件

pnpm install prettier -D

新增配置文件 .prettierrc

{
  "tabWidth": 2,
  "singleQuote": true,
  "semi": true,
  "trailingComma": "all"
}

package.json 增加 script 命令,用于全局检查项目代码

"scripts": {
    // ...
    "prettier": "prettier --write "src/**/*.{js,ts,jsx,tsx,less,css}"",
}

stylelint

这个其实是postcss的插件,类似于 eslint,用于检查样式文件的语法,包括css、less、sass等。记得在 vscode 中安装 stylelint 插件

pnpm install stylelint stylelint-config-standard postcss-less -D

项目根目录下新增 .stylelintrc.js,配置参考如下:

module.exports = {
  extends: "stylelint-config-standard",
  customSyntax: "postcss-less",
};

增加 script 命令,便于全局检查样式文件:

"scripts": {
    // ...
    "stylelint": "stylelint --fix "src/**/*.{css,less}"",
}

但这个时候你会发现,保存css文件时并没有自动修复一些问题,需要开启vscode的配置项,在.vscode/settings.json中增加配置如下:

{
  "editor.formatOnSave": true,
  // 新增
  "editor.codeActionsOnSave": {
    "source.fixAll.stylelint": true
  },
  "editor.defaultFormatter": "esbenp.prettier-vscode"
}

代码提交检查

需要以下几种插件的配合:

  • commitlint 插件可以校验提交的 commit 信息是否符合规范,参考官方文档,不符合的话可以限制不能提交
  • husky。操作 git 生命周期钩子的工具
  • lint-staged。本地暂存代码检查工具,可以让 husky 只检验 git 工作区的文件
pnpm install husky lint-staged @commitlint/cli @commitlint/config-conventional -D

还需要以下步骤:

  1. package.json 追加以下配置。prepare 其实在 pnpm install 前会自动执行
"scripts": {
  // ...
  "prepare": "husky",
},
"lint-staged": {
  "src/**/*.{js,jsx,ts,tsx}": [
    "prettier --write",
    "eslint --cache --fix"
  ],
  "src/**/*.{css,less}": [
    "stylelint --fix"
  ],
},
  1. 执行 pnpm run prepare 初始化 husky
  2. 在根目录下创建 .commitlintrc.js
module.exports = {
  extends: ["@commitlint/config-conventional"],
};
  1. 修改初始化生成的commit-msgpre-commit文件

commit-msg

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

echo "========= 校验 commit-msg ======="
pnpm commitlint --edit $1

pre-commit

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

echo "========= 执行 lint-staged ======="
pnpm lint-staged

ok,这样的话,在 git commit 之前会进入工作区文件的扫描,执行 prettier 脚本,修改 eslint 问题,校验 commit msg,通过后再提交到工作区。尝试提交下代码试试吧

editorConfig

使用不同编辑器打开同一份文件,如果编辑器配置不统一,显示效果和输入内容很有可能不一致。EditorConfig 就主要用于统一代码编辑器编码风格

.editorConfig 配置参考

# https://editorconfig.org

# 已经是顶层配置文件,不必继续向上搜索
root = true

[*]
# 编码字符集
charset = utf-8
# 缩进风格是空格
indent_style = space
# 一个缩进占用两个空格,因没有设置tab_with,一个Tab占用2列
indent_size = 2
# 换行符 lf
end_of_line = lf
# 文件以一个空白行结尾
insert_final_newline = true
# 去除行首的任意空白字符
trim_trailing_whitespace = true

[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

Biome(不建议)

基于 Rust 的代码检查和格式化工具,目前还不推荐实际项目中使用,不过有望替代 prettier 和 eslint,下面是使用方法,会和eslint、prettier冲突,慎重尝试。以下 lint 和 prettier 主要都是用推荐的配置

pnpm install -D -E @biomejs/biome
# 初始化配置文件
npx @biomejs/biome init

修改配置文件

{
  "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
  "organizeImports": {
    // 启用 import 语句排序优化
    "enabled": true
  },
  "linter": {
    "enabled": true,
    // 启用规则校验
    "rules": {
      // recommended 表示使用推荐的规则
      "recommended": true
    }
  },
  "formatter": {
    "enabled": true,
    "formatWithErrors": false,
    "indentStyle": "tab",
    "indentWidth": 2,
    "lineWidth": 120
  }
}

安装 vscode 插件 biome,然后将 Biome 设置为默认格式化程序(右键选择使用...格式化,可以配置默认值)

接着增加 .vscode/settings.json 文件

{
  "editor.codeActionsOnSave":{
    // 保存时自动调整 import 语句顺序
    "source.organizeImports.biome": "explicit"
  }
}

然后调整import语句顺序,保存后查看是否优化排序了,也就是 biome 是否生效了。也可以改引号、缩进、分号确认 formatter 是否生效

Biome 目前还不支持校验和格式化 css/vue 代码,所以这一块还需要借助 eslint 和 Prettier 来做,希望后面更完善后再考虑替换 eslint 和 prettier(包括对ts代码的规则完善)

构建分析

增加编译进度

一些中大型项目可能需要进度条来直观地查看进度,这里用到 progress-bar-webpack-plugin 插件

pnpm i -D progress-bar-webpack-plugin

修改 webpack 配置

// webpack.config.js
// chalk注意使用v4版本,否则会报错
const chalk = require('chalk');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
// ...
  plugins: [
    new ProgressBarPlugin({
      format: `  :msg [:bar] ${chalk.green.bold(':percent')} (:elapsed s)`,
    }),
    // ...
  ],

webpack5 从0搭建react SPA

耗时分析

主要是分析 loader 和 plugin 处理耗时,这里要借助 speed-measure-webpack-plugin 插件。不过这个插件好久没更新了,不太确定是否完整支持 webpack5

pnpm i -D speed-measure-webpack-plugin

修改配置

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();

const config = smp.wrap({
  // ...webpack config
});

webpack5 从0搭建react SPA

产物体积分析

需要用到 webpack-bundle-analyzer

pnpm i -D webpack-bundle-analyzer

修改配置

plugins: [
    // ...
    new BundleAnalyzerPlugin({
      // 设置为false,禁止自动打开分析页面
      openAnalyzer: false,
    }),
],

执行 npm run serve 后,可以在 localhost:8888 查看

构建优化

编译提速

  • 开启持久化缓存 cache,对二次构建有显著提速
const config = smp.wrap({
  cache: {
    type: 'filesystem',
  },
  // ...
})

体积优化

js代码压缩

借助 terser-webpack-plugin压缩js代码,可以开启swc压缩

pnpm i -D terser-webpack-plugin

修改 webpack 配置

const TerserPlugin = require('terser-webpack-plugin');

const config = smp.wrap({
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin({
      minify: TerserPlugin.swcMinify,
    })],
  },
  // ...
})

css代码压缩

前提是分离 css 代码为单独的文件,然后可以借助 css-minimizer-webpack-plugin压缩代码

pnpm i -D css-minimizer-webpack-plugin

修改配置

optimization: {
    minimize: true,
    minimizer: [
      new CssMinimizerPlugin(),
      // ...
    ],
  },

tree-shaking

只要使用 ES6 模块语法和将 mode 设置为 production,webpack5 就会自动启用 tree-shaking 功能

CSS 代码也可以做类似的操作,借助 purgecss-webpack-plugin 插件

pnpm i -D purgecss-webpack-plugin

修改配置

  const { PurgeCSSPlugin } = require('purgecss-webpack-plugin');
  const PATHS = {
     src: path.join(__dirname, 'src'),
  };
  // ...
  plugins: [ 
      new PurgeCSSPlugin({
        paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
      }),
      // ...
  ]

code spliting

可以将多页面共用的代码单独抽成一个 chunk,这样可以减小一定的体积,然后可以按需加载或并行加载这些模块

const config = smp.wrap({
  // ...
  splitChunks: {
    chunks: "all",
    cacheGroups: {
      vendors: {
        test: /[\/]node_modules[\/]/,
        chunks: "all",
        priority: 10,
        enforce: true,
      },
    },
  },
});

其他

alias

修改 webpack resolve 配置

const config = smp.wrap({
  // ...
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
    extensions: ['.tsx', '.ts', '.jsx', '.js'],
  },
})

tsconfig 需要追加配置,确保路径提示正常

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

环境区分

借助 dotenv来区分不同环境,它能将环境变量中的变量从 .env 文件挂载到 process.env 对象上。大部分项目需要配置不同环境,按需加载不同的环境变量,使用 dotenv 就可以解决这一问题

pnpm install dotenv-cli -D

新建 config/env/.env.devconfig/env/.env.testconfig/env/.env.prod

# .env.dev
NAME = development
# .env.test
NAME = test
# .env.prod
NAME = production

修改 package.json 的 scripts 命令

"scripts": {
    "serve": "dotenv -e ./config/env/.env.dev webpack serve",
    "build": "dotenv -e ./config/env/.env.prod webpack",
    "build:dev": "dotenv -e ./config/env/.env.dev webpack",
    "build:test": "dotenv -e ./config/env/.env.test webpack",
    "build:prod": "dotenv -e ./config/env/.env.prod webpack",
},

然后通过以下方式就可以读取了

// webpack.config.js
const envName = process.env.NAME;
console . log ( '===envName' , envName); 

sourceMap

参考 webpack.docschina.org/configurati… 来选择更适合自己项目的,多数情况下,选择 eval-cheap-module-source-map就可以,线上环境其实也可以不用 sourcemap

const config = smp.wrap({
  devtool: 'eval-cheap-module-source-map',
  // ...
})

最后

基于 webpack5 从 0 搭建 react 单页面应用的尝试,对比 create-react-app 应该是差蛮多的,还有些细节可以完善,后面有空研究对比看看,然后争取补个续集 ~