前端项目里都有啥?

806 阅读28分钟

人类的赞歌是勇气的赞歌

大家好,我是柒八九。一个专注于前端开发技术/RustAI应用知识分享Coder

写在最前面

这不是快过年了吗,肯定会有发红包的环节,然后我也定制了几款寓意比较好的红包封面。然后最为回馈粉丝的新年礼物。 请大家笑纳。

由于红包性质,需要大家在指定平台领取。望周知

前言

Rust 赋能前端-开发一款属于你的前端脚手架中我们介绍过使用Rust来写一个基于前端项目的脚手架,在发文后反响也不错。然后,有些动手能力强的小伙伴,已经将其应用到实际开发中了。

如果,还有没把玩过这个小工具的同学也不用着急,反正经过一顿操作猛如虎,我们就会构建出一个拥有一个功能完备的前端项目,你只需要关心自己页面的构建。

<选择UI库,选择CSS预处理器,选择Hook,选择状态管理库,项目初始化完毕>

具体的页面结构如下:

脚手架的文章中,我们将主要的精力放在了Rust上,而没有过多介绍前端项目的功能结构。所以,今天我们来讲讲一个功能完备的前端项目(React版本)需要具备哪些东西。

快速创建一个React项目,我们可以选择Create-React-App或者Vite,下文中我们以Vite构建的项目作为底,来进行二次的配置。(当然,下面有的配置可能根据打包工具的不同而有所差别,但是思路都是一样的)

好了,天不早了,干点正事哇。

我们能所学到的知识点

  1. TypeScript
  2. Eslint + Oxlint
  3. Prettier 或 Biome
  4. Husky
  5. Css相关
  6. Browserslist
  7. axios
  8. Errorboundy
  9. 自定义hook
  10. 全局loading
  11. 路由
  12. 状态管理
  13. Vite 配置优化

1. TypeScript

有人说Ts是一把双刃剑,对于功能简单的项目而言,无端的引入Ts无疑是作茧自缚;但是呢,对于那些数据流向复杂和业务盘根错节的项目而言,从自我角度而言,引入Ts无疑是明智之选。

tsconfig.xx.json

在使用Vite构建的React+Ts项目,会在根目录下创建两个关于Ts的文件。

  1. tsconfig.json
  2. tsconfig.node.json

这是因为项目使用两个不同的环境来执行 Ts 代码:

  1. tsconfig.json

    • 作用于应用程序(src 文件夹)它在浏览器中运行
    • 用于配置 React 项目的 Ts 编译选项,包括目标版本、模块解析方式、JSX 语法支持等。
    • 定义了项目的编译规则和设置
  2. tsconfig.node.json

    • Vite 本身(包括其配置)是在 Node 内的计算机上运行的,而 Node 是完全不同的环境(与浏览器相比),具有不同的应用程序接口和限制条件。
    • 用于配置 Vite 本身的 Ts 编译选项,它包含了 Vite 配置文件的引用和一些特定于 Node 环境的编译选项。
    • 这个文件主要用于 ViteNode 环境下的编译和构建过程

针对我们来讲,要对我们项目做针对Ts的处理的话,那就只需要关心tsconfig.json中的内容就好。

其实对于Vite为我们创建的配置文件(tsconfig.json)完全够我们进行项目开发,但是我们还需要对其做额外的配置。

{
  "compilerOptions": {
    "target": "ESNext", // 指定 ECMAScript 目标版本,ESNext 表示最新版本
    "useDefineForClassFields": true, // 启用新的类字段语义
+    "lib": ["DOM", "DOM.Iterable", "WebWorker", "ESNext"], // 编译过程中包含的库文件
    "allowJs": false, // 不允许编译 JavaScript 文件
    "skipLibCheck": true, // 跳过库文件的类型检查
    "esModuleInterop": false, // 禁用 ES 模块间的互操作性
    "allowSyntheticDefaultImports": true, // 允许从没有默认导出的模块中默认导入
    "strict": false, // 禁用所有严格类型检查选项
    "forceConsistentCasingInFileNames": true, // 强制文件名大小写一致
    "module": "ESNext", // 指定生成代码的模块系统,ESNext 为最新模块标准
    "moduleResolution": "Node", // 模块解析策略,Node 用于 Node.js
+    "resolveJsonModule": true, // 允许导入 JSON 模块
    "isolatedModules": true, // 每个文件都作为单独的模块
    "noEmit": true, // 不输出文件
    "jsx": "react-jsx", // 指定 JSX 代码的编译方式
    "types": ["vite/client"], // 包含的类型声明文件
    "downlevelIteration": true, // 支持较低版本的迭代器特性
    "allowImportingTsExtensions": true, // 允许导入 `.ts` 扩展名的文件
+    "baseUrl": ".", // 解析非相对模块的基准目录
+    "paths": { // 设置路径映射
+      "@/*":["src/*"],
+      "@hooks/*": ["src/hooks/*"],
+      "@assets/*": ["src/assets/*"],
+      "@utils/*": ["src/utils/*"],
+      "@components/*": ["src/components/*"],
+      "@api/*": ["src/api/*"]
    }
  },
+  "include": ["./src", "*.d.ts"], // 包含的文件或目录
+  "files": ["index.d.ts"] // 包含的独立文件列表
}

我们讲需要额外配置的项标注在上方,然后并配有注释,就不在过多解释了。具体配置项有不明确的地方,可以参考Ts官网配置文档

vite-env.d.ts

手动操作window上的属性

虽然,我们对Ts做了配置,但是呢在开发中还是会遇到Ts的报错问题。例如,我们想在Window上挂载一个类型(x),并且在通过winodw.x进行设置和取值。但是此时,Ts就会报错。我们需要有一种方式来告知Ts这种方式是合法的。

此时,我们的vite-env.d.ts就派上用场了。

/// <reference types="vite/client" />
interface Window {
  ajaxStatus: 'pending' | 'resolved';
}

define 定义全局常量

vite项目中,我们还可以通过define来定义全局常量

export default defineConfig({
  define: {
    __APP_VERSION__: JSON.stringify('v1.0.0'),
    //.....
  },
})

针对此种情况,我们也需要在vite-env.d.ts中进行配置处理。

declare const __APP_VERSION__: string

环境变量

在前端项目开发中,我们常常需要区分开发环境生产环境,此时就会有环境变量的出现,我们可以根据这些变量来控制项目的运行方式。

我们可以在命令行中使用--mode参数来指定运行模式。
例如,使用vite build --mode production来指定生产环境模式。Vite会根据指定的模式加载对应的环境变量文件(.env.production)。

vite中可以通过.env.xx(xxdevelopment/production)文件来管理环境变量,并使用import.meta.env来在代码中访问这些环境变量。

ES2020import命令添加了一个元属性import.meta,返回当前模块的元信息。 关于这块可以参考我们之前的文章你真的了解ESM吗?

例如,

  1. 在项目的根目录下创建.env.development文件,并在其中定义我们的环境变量 VITE_API_KEY=your-api-key VITE_BASE_URL=front789.com

  2. 使用import.meta.env:在我们的代码中,可以直接使用import.meta.env来访问这些环境变量。例如:

    const apiKey = import.meta.env.VITE_API_KEY;
    const baseUrl = import.meta.env.VITE_BASE_URL;
    

针对上面的情况,如果我们不对环境变量在vite-env.d.ts中配置的话,在访问的时候,Ts就会报错。

具体配置如下:(注意interface的名称)

// ...省略上面的配置代码

interface ImportMetaEnv {
  readonly VITE_API_KEY: string
  readonly VITE_BASE_URL: string
  // 更多环境变量...
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

2. Eslint + Oxlint

莎士比亚(Shakespeare)名言:There are a thousand Hamlets in a thousand people's eyes
翻译成中文就是我们耳熟能详的:一千个读者眼中就会有一千个哈姆雷特

如果一个团队中对代码规范没有一个合理的认知,那写出来的代码就是千人千面了。所有,我们急需一种方案在规范方面去制约这种情况的发生。

索性,我们有eslint。(其实,我们还有更好的选择-Oxlint,我们也会有涉猎)

配置Eslint

下图是eslint配置文件的文档

上面有3类信息。

  1. 配置eslint的方式有很多js/esm/yaml/json/package.json
  2. 如果多个配置文件存在,它们是有优先级的
    • .eslintrc.js优先级最高
    • package.json中优先级最低
  3. 配置方式主要有两种方式
    • .eslintrc.*
    • package.json中新增eslintConfig属性

当我们使用Vite构建React+Ts项目时候,会在根目录下为我们创建.eslintrc.cjs。但是呢,为了能复用配置文件,我们采用.eslintrc.json方式来配置eslint。(之所以采用.eslintrc.jsonOxlint有关)

配置.eslintrc.json

{
  "root": true, // 表示这是项目的根配置文件
  "env": {
    "browser": true, // 启用浏览器全局变量
    "es2022": true, // 使用 ES2022 全局变量和语法
    "node":true
  },
  "extends": [ // 指定一系列的扩展配置
    "eslint:recommended", // 使用 ESLint 推荐的规则
    "plugin:react/recommended", // 使用 React 插件推荐的规则
    "plugin:compat/recommended", // 检查浏览器兼容性
    "plugin:@typescript-eslint/recommended", // 使用 TypeScript 插件推荐的规则
    "plugin:react-hooks/recommended", // 使用 React 钩子(Hooks)推荐的规则
    "plugin:react/jsx-runtime" // 支持 React 17 新的 JSX 转换
  ],
  "settings": { // 自定义设置
    "react": { // 针对 React 的设置
      "createClass": "createReactClass", // React.createClass 的别名
      "pragma": "React", // JSX 转换时使用的 React 变量名
      "fragment": "Fragment", // React.Fragment 的别名
      "version": "detect" // 自动检测 React 版本
    }
  },
  "parser": "@typescript-eslint/parser", // 指定解析器为 TypeScript ESLint 解析器
  "parserOptions": { // 解析器选项
    "ecmaFeatures": {
      "jsx": true // 启用 JSX
    },
    "ecmaVersion": "latest", // 使用最新的 ECMAScript 标准
    "sourceType": "module" // 使用 ES6 模块
  },
  "plugins": [ // 使用的插件
    "react", // React 插件
    "compat", // 浏览器兼容性插件
    "@typescript-eslint", // TypeScript ESLint 插件
    "prettier" // Prettier 插件(代码格式化)
  ],
  "rules": { // 自定义规则
    "react/prop-types": "off", // 关闭 React 的 prop-types 规则
    "react/display-name": "warn", // 警告 React 组件缺少 display name
    "react/react-in-jsx-scope": "off", // 关闭 React 必须在作用域内的规则(对于 React 17+ 不需要)
    "react/require-default-props": "off", // 关闭 React 的默认属性规则
    "react-hooks/rules-of-hooks": "error", // 强制执行 React 钩子的规则
    "no-irregular-whitespace": "warn", // 警告不规则的空白
    "react-hooks/exhaustive-deps": ["warn", { // React 钩子依赖项的完整性检查
      "additionalHooks": "(useRecoilCallback|useRecoilTransaction_UNSTABLE)" // 额外的钩子
    }],
    "@typescript-eslint/indent": ["warn", 4, { "SwitchCase": 1 }], // TypeScript 缩进规则
    "linebreak-style": ["warn", "unix"], // 换行风格
    "quotes": ["warn", "single"], // 引号风格
    "semi": ["warn", "always"], // 分号
    "prefer-const": "warn", // 优先使用 const
    "no-empty": "warn", // 警告空块
    "no-debugger": "error", // 禁用 debugger
    "no-console": ["error", { "allow": ["warn", "error", "debug", "info"] }], // 限制 console 的使用
    "@typescript-eslint/no-empty-function": "warn", // TypeScript 空函数规则
    "@typescript-eslint/no-unused-vars": "warn", // TypeScript 未使用变量规则
    "@typescript-eslint/explicit-module-boundary-types": "warn" // TypeScript 模块边界类型规则
  },
  "ignorePatterns": ["**/*.d.ts", "**/*/dist"] // 忽略模式
}

上面的每个都有详细的注释,这里就不过多展开说明了。

Oxlint

虽然eslint能够让我们的项目更加健壮,但是呢,由于eslint的校验是很耗费时间,如果项目很大的话,针对格式校验也是一件很痛苦的事情。

是时候,拿出新的解决方案了。Oxlint-- 一款用Rust编写的针对JS格式校验工具。

它不是eslint的替代方案,而它是eslint的增强方案。

如果我们在eslint上耗费了很多时间,我们可以在项目中引入Oxlint来优化代码校验时间。

Oxlint有很多操作方式,更多的是配合husky或者ci进行代码校验。针对如何进行此处的操作,我们在介绍husky的时候来说明。

由于Oxlint刚开源不久,它的官网也很模糊,所有有些必要的信息我们是不好获取的。并且它的有些Rules也不单单针对JS,所有我们需要对其需要进行筛选。

  • 可以通过npx oxlint@latest --rules来进行rules的查看
    • 它融合了很多校验规则eslint/jsx/react/import/jest/unicorn(轻量级多体系结构 CPU 仿真器框架)
  • 可以通过npx oxlint@latest -h查看各种命令
  • 还可以通过-c ./eslintrc.json复用eslint的规则
    • 这就是我们选择用.eslintrc.json作为eslint的配置原因
    • 因为,可以和oxlint复用配置信息。
  • --fix来修复部分问题,但是这种修复方式有限,不会百分百修复发现的问题

3. Prettier 或 Biome

爱美之心,人皆有之。我们也想让我们的代码变得赏心悦目,那代码美化就必不可少。

如果,提到代码美化,那prettier在前端有着举足轻重的地位。

Prettier

配置文件

从上图中我们得到几类信息

  • Eslint类似,Prettier也有多种配置方式。图中,按照优先级由高到低排列
  • Prettier没有全局配置方式

.prettier.js

我们选择.prettier.js来配置项目

module.exports = {
    printWidth: 100, // 指定代码长度,超出换行
    tabWidth: 4, // tab 键的宽度
    useTabs: false, // 不使用tab
    semi: true, // 结尾加上分号
    singleQuote: true, // 使用单引号
    quoteProps: 'as-needed', // 要求对象字面量属性是否使用引号包裹,(‘as-needed’: 没有特殊要求,禁止使用,'consistent': 保持一致 , preserve: 不限制,想用就用)
    jsxSingleQuote: false, // jsx 语法中使用单引号
    trailingComma: 'es5', // 确保对象的最后一个属性后有逗号
    bracketSpacing: true, // 大括号有空格 { name: 'rose' }
    jsxBracketSameLine: false, // 在多行JSX元素的最后一行追加 >
    arrowParens: 'always', // 箭头函数,单个参数添加括号
    requirePragma: false, // 是否严格按照文件顶部的特殊注释格式化代码
    insertPragma: false, // 是否在格式化的文件顶部插入Pragma标记,以表明该文件被prettier格式化过了
    proseWrap: 'preserve', // 按照文件原样折行
    htmlWhitespaceSensitivity: 'ignore', // html文件的空格敏感度,控制空格是否影响布局
    endOfLine: 'lf', // 结尾是 \n \r \n\r auto
    // 使用 Unix 格式的换行符
    endOfLine: 'lf',
    // 格式化文件的范围,可以是 "all"、"none" 或 "proposed"
    rangeStart: 0,
    rangeEnd: Infinity,
};

当然,每个团队都有自己的规范,所有上面的提供的代码,不代表最优方案,需要大家见仁见智。

Biome

PrettierEslint存在相同的问题,就是性能问题。然后Prettier的创始人发起了一个优化Prettier的挑战。在高手云集的情况下,Biome杀出重围,脱颖而出。

biome也是一款用Rust编写的前端工具库。

有没有感觉到Rust在重构前端工具中,越来越重要。这里王婆卖瓜一下,前端时间,我们Rust写了一个前端脚手架,有兴趣的同学可以自行使用。

下图是Biome在美化代码和校验代码和传统工具的benchmark的结果。

从结果来看Biome是一个不错的美化代码的新方案,但是,但是,由于Biome是新项目,有些边缘case还没完全兼顾。如果对不关心这些东西的话,其实无脑使用Biome是一个不错的选择。毕竟,效率优先。


4. Husky

其实,针对eslint/prettier我们可以设置在保存文件时候,利用Vscode进行自动校验和修正,这个不在我们本文的讨论范围中。这个属于Vscode的配置项了。

但是呢,我们选择了另外一种触发eslint/prettier的方式,那就是利用husky在触发git hook时处理。

在我们脚手架中在初始化项目时,我们就会执行git init来将项目变成一个仓库。

所以,我们可以直接使用husky的配置。

下图是官网的示例代码。

新增prepare命令

执行下面命令,用于按照husky

npm pkg set scripts.prepare="husky install"
npm run prepare

上面的代码中在package.json中的scripts字段中新增了一个prepare的属性,其值为husky install

其实,npm是有生命周期这个概念的。下图中就介绍了很多内置的生命周期。(大家也可以认为这是hook

package.json 文件中,scripts 字段用于定义一些脚本命令,而 prepare 是其中一个可用的内置脚本。通常,prepare 脚本用于在包(package)被安装前执行一些准备工作。这对于确保包在安装后能够正确工作非常有用。

prepare 脚本中,我们可以定义需要在包安装前执行的一些命令。这些命令可以是任何我们认为在包安装前需要完成的任务,比如构建、编译、复制文件等。

而我们这里的意思是,husky是优先被安装的库。

新增pre-commit Hook

npx husky add .husky/pre-commit 'yarn lint-staged'

执行上面的操作后,在我们项目的根目录下就会自动构建了一个.husky的文件,并且新增了一个pre-commit的文件。

pre-commit内容如下,我们刚才的yarn lint-staged赫然在列。

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

yarn lint-staged

修改package.json

我们在package.json中新增一个lint-staged的命令

{
  "lint-staged": {
    "*.{ts,tsx}": [
      "oxlint", //oxlint 校验
      "eslint", // eslint 校验
      //"prettier --write" // 使用prettier修正代码
      "biome format"  // 使用biome 修正代码
    ]
  },
}

上面的oxlint/eslint都是用于规则的校验。

prettierbiome是二选一的。我们可以使用prettier亦或者使用biome来对代码进行修正。这都是随意的。

pre-push

npx husky add .husky/pre-push 'yarn tsc-test'

pre-push钩子是在执行git push之前运行的脚本,用于在代码push到远程仓库之前执行一些操作,比如运行测试或进行代码检查。

在这种情况下,yarn tsc-test是希望在每次push之前运行的命令。这可能是用于运行Ts编译器的测试命令,以确保在推送代码之前没有类型错误或编译问题。


5. Css相关

作为浏览器四大语言之一的CSS,处理起来也颇费功夫。(除了js/html/css,wasm也算一种内置语言,想了解更多,可以参考浏览器第四种语言-WebAssembly)

Css 预处理器(pre-processors)

CSS 预处理器对于那些希望通过变量混合数学函数运算函数嵌套语法样式表模块化来增强页面样式的前端开发人员来说是一个真正可用的工具。它们可以轻松实现重复样式的自动化、减少错误并编写可重用的代码,同时确保与各种 CSS 版本的向后兼容性。

常见的CSS预处理器有

  • Sass/Scss

    • SASSSyntropically Awesome Style Sheets):它被设计为与所有版本的 CSS 兼容。它遵循命令式样式模式,这意味着我们可以指定事情的完成方式。
    • 在某些时候,它往往感觉更像是一种编程语言,而不是一种样式语言
  • Less

    • LESS(Leaner Style Sheets)是 CSS 的向后兼容语言扩展。 LESS 本质上遵循声明式样式模式。这意味着我们指定我们想要看到的内容,而不是我们希望如何完成它。这主要是因为它与函数式编程相似,这使得它更具可读性和更容易理解。
  • Stylus

    • Stylus 提供了更多的表现力,同时保持了更简洁的语法。有Python背景的人会对其非花括号缩进语法产生强烈的共鸣。

针对这三种的优缺点,我们后期专门会有文章介绍。

Scss 语法简介

Sass/Scss由于拥有广泛的社区支持,所以我们的项目首选Sass

我们来简单介绍一下Sass的语法特性。更详细的语法可以参考sass官网

  1. 变量:允许使用变量来存储和重用值,从而增强代码的可维护性和一致性。
  2. 混入(Mixins):允许包含 CSS 属性组。它们提高了代码的可重用性,并使管理复杂的样式变得更加容易。
  3. 嵌套:支持 CSS 选择器的嵌套,提供更直观的方式来编写和组织样式。它提高了可读性并使代码结构更加透明。
  4. 部分(Partials)和模块化Modules:允许创建可以导入到其他 Sass 文件的部分 Sass 文件。此功能增强了模块化和代码组织,使开发人员能够独立处理项目的特定部分。
    • 可以创建包含 CSS 小片段的部分 Sass 文件,我们可以将这些 CSS 片段包含在其他 Sass 文件中。
    • 部分文件是一个以下划线开头命名Sass 文件。我们可以将其命名为 _partial.scss 之类的名称。下划线让 Sass 知道该文件只是一个部分文件,并且不应将其生成为 CSS 文件。
    • 部分文件@use 规则一起使用。
  5. 扩展(Extend)和继承(Inheritance):Sass 引入了占位符选择器的概念,它充当可重用的样式块。它们可以由其他选择器扩展和继承,从而减少代码重复并促进更易于维护的代码库。
  • 使用 @extend 可以让我们从一个选择器到另一个选择器共享一组 CSS 属性

PostCSS

PostCSS 是一个 JavaScript 工具,可将 CSS 代码转换为抽象语法树 (AST),然后提供 API,以便使用 JavaScript 插件对其进行分析和修改。

尽管它的名字中包含Post,有的同学就会将其与预处理器(pre-processors)进行关联,其实它既不是后处理器也不是预处理器,它只是一个将特殊的 PostCSS 插件语法转换为 CSS 的转译器

可以将其视为 CSS 界的Bable工具

PostCSS 提供了一个庞大的插件生态系统来执行不同的功能,例如我们在开发中常见的autoprefixerTailwind css

PostCSS的核心是插件。每个插件都是为特定任务而创建的。

我们可以通过官网提供的Post Plugins来搜索我们想要的插件。或者通过postcss github查找

Eslint/Prettier类似,配置PostCSS也有很多方式。

  • package.json
  • .postcssrc
  • .postcssrc.json
  • .postcssrc.yml
  • (.postcssrc|postcss.config).(js|mjs|cjs|ts|mts|cts)

我们选择最常规的方式,postcss.config.js来配置,这样更容易处理一些逻辑。

postcss.config.js

本地安装PostCss

npm i -D postcss

下面我们就配置一些,比较常用的插件。

之所以,选择xx.js这样我们通过process.env.NODE_ENV来区分开发环境生产环境

module.expors = () => {
  return {
    plugins: {
      'postcss-import': {},
      'stylelint':{
        'rules': {
          'color-no-invalid-hex': true
        }
      },
      'postcss-preset-env': {
        autoprefixer: {},
        stage: 3,
        features: {
          'custom-properties': false,
        },
      },
      autoprefixer: {
        grid: true,
        flex: true,
      },
      'postcss-combine-media-query': {},
      'postcss-combine-duplicated-selectors': {},
      'cssnano':{},// 样式压缩
      'tailwindcss':{},// 新增tailwindcss 的配置
    },
  };
};

我们来简单介绍上面的几个插件的作用。

  1. postcss-import

    • 用于在 CSS 文件中引入其他 CSS 文件
    • postcss-import与原生CSS中的导入规则不同。
      • 原生 CSS 中的@import规则,因为它会阻止同时下载样式表,从而影响加载速度和性能。浏览器必须等待加载每个导入的文件,而不是能够一次加载所有 CSS 文件。
  2. autoprefixer

    • 它可以解析供应商前缀,如 -webkit-moz-ms,并使用来自 Can I Use 网站的值将其添加到 CSS 规则中。
    • 使用 browserslist来决定兼容哪些版本的浏览器或者Node
  3. postcss-preset-env

    • 能够在代码中使用现代 CSS(如嵌套和自定义媒体查询),将其转换为浏览器可以理解的 CSS。
    • 它有一个 stage 选项,可以根据 CSS 功能在作为 Web 标准实现的过程中的稳定性来确定要进行 Polyfill 的 CSS 功能
      • stage 可以是 0(实验)到 4(稳定)或 false。第 2 阶段是默认阶段。
    • 此外,预设环境插件默认包含 Autoprefixer 插件,并且 browsers 选项将自动传递给它。
    • 也就是说我们设置了postcss-preset-env就不需要设置Autoprefixer
  4. stylelint

    • 这是一个 CSS linter,可以帮助我们避免代码中的错误破坏我们的页面
    • 默认情况下不启用任何规则,也没有默认值。我们必须显式配置每个规则才能将其打开。
  5. cssnano

    • 这是一个压缩工具,用于尽可能减小最终 CSS 文件大小,以便我们的代码为生产环境做好准备。
    • 某些部分将被更改以尽可能减小大小,例如删除不必要的空格、换行、重命名值和变量、合并在一起的选择器等等。
  6. Tailwind CSS 是一个 CSS 框架,旨在使用户能够更快、更轻松地创建应用程序。我们可以使用实用类来控制布局、颜色、间距、排版、阴影等,以创建完全自定义的组件设计


Lightning CSS

由于PostCss是用JS写的,那势必就会有性能通病。幸运的是,我们现在有Lightning CSS

下图是对CSSNano/ESBuild/Lightning Css的压缩对比图。

从图中看到,它也是Rust重写的。

针对不同的打包工具,它有各自的配置方式。(这里我们就按vite来讲)。

但是呢,使用lightningcss速度是快,但是一些注意事项。

  1. 对于scss文件的注释,我们不能使用// xx(这不是标准的scss注释)而是需要使用/* xx */,scss文件使用//报错的具体解释
  2. 针对@keyframes等自定义属性,我们需要使用lightning csscustomAtRulesvisitor进行配置。

反正,效率是提升了,这块的学习成本比较高。


6. Browserslist

The best practice is to use .browserslistrc config or browserslist key in package.json to share target browsers with Babel, ESLint and Stylelint

在配置PostCSSBrowserslist时,会提示我们最好将Browserslist抽离出去,这样我们就可以为eslint/babel/stylelint统一配置。

那什么是Browserslist呢?

来自官网的截图

它也可以通过很多方式配置。例如

  1. .browserslistrc
  2. package.json配置browserslist字段
  3. 在项目的根路上上创建browserslist
  4. 创建BROWSERSLIST 变量

如果不特别配置,我们使用的是defaults值,而defaults是下面值的简短版本

  • > 0.5%: 全球使用率至少为 0.5% 的浏览器
  • last 2 versions: 每个浏览器的最后 2 个版本
  • Firefox ESR :最新的 Firefox 扩展支持版本
  • not dead: 在过去 24 个月内获得官方支持或更新的浏览器

而在f_cli中我们使用的是.browserslistrc

[production]
> 1%
not dead 
not op_mini all

[modern]
last 1 chrome version
last 1 firefox version
last 1 safari version

它和package.json中配置等同

{
  "browserslist":{
    "production": [
      ">1%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

7. axios

不进行后端数据交互的前端项目都是耍流氓。

前端项目中有很多方式能够发起异步请求。例如XMLHttpRequest/Fetch等浏览器原生API,还有axios。一般项目中,首先不会选择XMLHttpRequest因为使用它太繁琐。基本上都是在fetchaxios二选一。

我们来简单对比一下fetchaxios

  1. fetch
    • 提供了在window对象上定义的 fetch() 方法。它还提供了一个 JavaScript 接口,用于访问和操作 HTTP 管道的各个部分(请求和响应)。
    • fetch 方法有一个必传参数——要获取的资源的 URL。此方法返回一个 Promise,可用于检索对请求的响应。
  2. axios
    • 它是一个 Javascript 库,用于从浏览器的 Node.js 或 XMLHttpRequest 发出 HTTP 请求,它支持 JS ES6 原生的 Promise API。
    • 它可用于拦截 HTTP 请求和响应,并启用客户端针对 XSRF 的保护。
    • 它还具有取消请求的能力。

fetch vs axios

特性AxiosFetch
请求对象中的 URL
安装方式独立的第三方包,易于安装内置于大多数现代浏览器,无需安装
XSRF 保护内置
数据属性使用 data 属性使用 body 属性
数据内容包含对象需要进行字符串化
请求成功判断状态码为 200 且状态文本为 'OK'响应对象包含 ok 属性
JSON 数据自动转换支持需要两步过程:首先发起请求,其次调用响应的 .json() 方法
请求取消和超时支持不支持
拦截 HTTP 请求支持默认情况下不提供拦截请求的方式
下载进度支持 内置支持不支持
上传进度支持不支持不支持
浏览器兼容性广泛支持仅支持 Chrome 42+、Firefox 39+、Edge 14+ 和 Safari 10.1+(向后兼容性)
GET 请求时处理数据内容忽略 data 内容可以包含请求体内容

从对比中看,针对异步能力要求不高的项目来讲,我们可以无脑选择fetch,毕竟它是原生支持,不需要额外下载依赖。但是,如果我们需要用到更高级的异步操作,那无疑就是axios

所以,我们项目中也首选axios

配置axios(ts)

文件目录,在项目的根目录下request文件下。

import axios, { AxiosRequestConfig, isAxiosError } from 'axios';

interface JsonResponse<T> {
  code: number;
  msg: string;
  data: T;
}

const apiPrefix = '/xxx';

// 获取用户授权信息
export const getAuth = () => {
  const token = localStorage.getItem('token');
  return token ? `Bearer ${token}` : '';
};

const axiosInstance = axios.create({
  baseURL: `${apiPrefix}/`,
  timeoutErrorMessage: 'request timeout',
  timeout: 12000,
  headers: {
    Authorization: getAuth(),
  },
});

export const request = async <T = unknown>(config: AxiosRequestConfig) => {
  try {
    const { data } = await axiosInstance.request<JsonResponse<T>>(config);
    if (data.code === 0) {
      return data.data;
    } else {
      throw new Error(data.msg);
    }
  } catch (error) {
    if (isAxiosError(error)) throw new Error('网络异常');
    throw error;
  }
};

上面代码中配置了axios

然后我们就可以在api文件夹中进行调用。

import { request } from '@/request';


// 进行接口信息的注册
export const ajaxPostXX = (params: { p1: string; p2: number }) => {
  return request({
    url: '/path/action',
    method: 'POST', 
    data: params,
  });
};

在组件中进行接口的调用

const asyncAction = async () => {
   const asyncData = await ajaxPostXX({
      p1: front,
      p2: 789,
    });
    // 在此处就可以处理异步数据
}

当然,我们还可以使用axios的接口拦截功能。

const axiosInstance = axios.create({
  baseURL: `${apiPrefix}/`,
  timeoutErrorMessage: 'request timeout',
  // timeout: 120000,
  headers: {
    Authorization: getAuth(),
  },
});
// 配置请求
axiosInstance.interceptors.request.use();
// 配置接口返回
axiosInstance.interceptors.response.use()

8. Errorboundy

有错不可怕,可怕的是,知道错了,不及时修正。

React 中的ErrorboundyReact 应用程序中错误处理的一个重要方面。

  • 它们是 React 组件,可以在其子组件树中的任何位置捕获 JavaScript 错误,记录这些错误,并显示回退 UI,而不是崩溃的组件树。
  • 它们就像一个 JavaScript catch {} 块,但用于组件。

React 原生API

React v16 中引入了Errorboundy,要使用它们,我们需要使用以下一种或两种生命周期方法定义类组件:getDerivedStateFromError()componentDidCatch()

  • getDerivedStateFromError():此生命周期方法在引发错误后呈现回退 UI。它是在渲染阶段调用的,因此不允许产生副作用
  • componentDidCatch():此方法用于记录错误信息。它是在提交阶段调用的,因此允许产生副作用

我们可以使用getDerivedStateFromError()/componentDidCatch()构建我们错误处理机制。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新状态,以便下一次渲染将显示备用用户界面。
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 还可以将错误记录到后台,存储起来
    console.log(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 可以渲染任何自定义备用用户界面
      return <h1>页面发生错误</h1>;
    }

    return this.props.children; 
  }
}

使用ErrorBoundary包裹我们需要处理的组件

class App extends React.Component {
  render() {
    return (
      <ErrorBoundary>
        <MyComponent />
      </ErrorBoundary>
    );
  }
}

使用第三方库(react-error-boundary)

我们使用原生的方式来构建ErrorBoundary时使用的是类组件。并不是说类组件不好,但是现在的ReactHook开发模式的天下。 并且,上面的构建的ErrorBoundary的扩展性不是很高。

所以,我们这里选择第三方库react-error-boundary

它使用一个名为 ErrorBoundary 的简单组件,我们可以使用它来包装可能容易出错的代码。

react-error-boundary的优点在于它消除了手动编写类组件和处理状态的需要。它在幕后完成所有繁重的工作,使我们能够专注于构建应用程序。

关于,如何使用react-error-boundary我们后期在详细讲。(这里就不再过多解释)


9. 自定义Hook

不要重复做那些无关紧要的事情

就像上面说的那样,现在是Hook的天下。我们可以基于React内置Hook做排列组合,形成符合我们特定业务逻辑的自定义Hook。

在之前美丽的公主和它的27个React 自定义 Hook中,我们介绍了在项目开发中比较常用的自定义hook。并且,在我们的f_cli中也有此项的配置。

如果,有些同学感觉自己构建自定义Hook比较麻烦,那么可以选择aHook,它提供了很多有用的自定义Hook


10. 全局loading

在讲axios时,我们就提供了一套简单的axios配置,然后也能为我们提供和后端进行异步接口的操作。

对于,精益求精的我们,是不是可以在发起异步请求时候,进行一个loadingUI交互逻辑。

当然,我们可以在每个ajaxXX触发的前后,使用代码侵入业务的方式。在每个异步接口触发的时候,使用变量loading:boolean来进行<Loading />组件的渲染。

上述的方式可行吗,必须可行。但是不够优雅。这里我们提供一种方式。基于全局属性ajaxStatus(这个全局属性可以放到window下,也可以放置到全局状态中redux/recoil等)。他们的处理思路都类似的。

存储异步状态

这里我们选择将其放置到window下。

由于我们项目使用了ts所以,我们需要在vite-env.d.tswindow配置相关属性。

/// <reference types="vite/client" />
interface Window {
  ajaxStatus?: 'pending' | 'resolved';
}

修改异步状态

然后,我们在每次发起异步时对ajaxStatus进行配置。在之前的axios的配置上进行处理。

export const request = async <T = unknown>(config: AxiosRequestConfig,noLoading?:boolean) => {
  try {
+    if (noLoading) window.ajaxStatus = 'resolved';
+    else window.ajaxStatus = 'pending';
    const { data } = await axiosInstance.request<JsonResponse<T>>(config);
    if (data.code === 0) {
+      window.ajaxStatus = 'resolved';
      return data.data;
    } else {
      throw new Error(data.msg);
    }
  } catch (error) {
+    window.ajaxStatus = 'resolved';
    if (isAxiosError(error)) throw new Error('网络异常');
    throw error;
  }
};

上面代码中,我们还可以通过noLoading来控制,某个接口是否拥有全局Loading 的交互处理。

监听异步状态

我们可以在顶层组件中,使用Object.defineProperty(window, 'ajaxStatus',{}ajaxStatus的值进行监听。然后触发本地的setLoading的,然后进行对应的Loading组件的渲染。

const App = () => {
  const [loading, setLoading] = useState<boolean>(true);
 
  useEffect(() => {
    Object.defineProperty(window, 'ajaxStatus', {
      // getter 方法
      get: function () {
        return this._ajaxStatus; // 返回真实值
      },
      // setter 方法
      set: function (value) {
        this._ajaxStatus = value; // 设置真实值
        setLoading(value === 'resolved' ? false : true);
      },
    });
  }, []);

  
  return (
    <div className="main">
      {loading && <Loading />}
      <div className="main-body">
        // 页面组件或者路由配置
      </div>
    </div>
  );
};


11. 路由

React Router仍然是处理 React 应用中路由的第一选择。凭借其丰富的文档和积极的社区,它继续是我们应用中声明性路由的可靠选择。


12. 状态管理

React状态管理库可以分为三类:

  1. 基于Reducer:需要分发(dispatch)操作来更新一个被称为单一数据源的中央状态。在这一类中,我们有ReduxZustand
    • 优点:老牌状态管理库,社区完善
    • 缺点: 样板代码太多
  2. 基于Atom:将状态分割成称为原子(atom)的小数据片段,可以使用React hooks进行读写。在这一类中,我们有RecoilJotai
    • 优点:简单且可扩展,能够从更小粒度去控制状态
    • 缺点:不能在组件外部使用状态
  3. 基于Mutable:利用Proxy创建可直接写入或以响应方式读取的可变数据源。这一类中的候选者有MobXValtio
    • 优点:依赖项在状态更改时会自动更新
    • 缺点:异步更新中的竞态条件可能导致应用程序状态混乱

既然,有这么多状态管理库,我们该如何选择呢。

最适合你项目的React状态管理库取决于你和你团队的具体需求和专业知识

请不要:仅基于项目大小复杂性选择库。因为我们可能在某处听说过X更适合大型项目,而Y更适合较小的项目。库的作者在设计其库时考虑了可扩展性,而项目的可扩展性取决于我们如何编写代码和使用库,而不是我们选择使用哪些库。


13. Vite 配置优化

由于用f_cli构建的React应用,我们是用Vite做项目管理。那么我们就来讲讲针对Vite的配置优化。

如果对Vite的打包流程还不了解的同学,可以参考我们之前写的浅聊Vite

如果,大家的项目是CRA构建的,那就是大概率是Webpack进行项目管理。如果想了解这方面的知识,可以参考前端工程化之Webpack优化

使用vite构建的前端项目,它会为我们内置很多默认插件,让我们可以无脑进行前端应用开发。下面是最基本的vite配置(vite.config.js)

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

const target = 'http://path:port/';

export default defineConfig(() => {
  return {
    plugins: [
      react(),
    ],
    server: {
      port: 7890,
      proxy: {
        '/api/': {
          target,
          changeOrigin: true,
          autoRewrite: true,
          followRedirects: false,
        },
      },
    }
  };
});

但是呢,它提供的默认配置,有时候不满足我们的使用情况,所以我们就需要做二次开发。

这里多说一句,除了官网的文档,如果大家想了解更多关于vite的配置可以直接看源码node_modules/vite/dist/node/index.d.ts里面有很多我们比较关系的部分,例如mode/plugins/css/esbuild/server/build

下面我们按照功能将vite分为几部分

  1. vite.plugin.config.ts
  2. vite.server.config.ts
  3. vite.build.config.ts
  4. vite.config.ts

vite.plugin.config.ts

下面是我们f_cli中关于vite.plugin.config.ts的配置信息。

import { PluginOption,splitVendorChunkPlugin } from "vite";
import react from '@vitejs/plugin-react';
import svgSprite from 'vite-plugin-svg-sprite';
import vitePluginImp from 'vite-plugin-imp';
import tsconfigPaths from 'vite-tsconfig-paths';
import { visualizer } from "rollup-plugin-visualizer";
import commonjs from '@rollup/plugin-commonjs';
import viteImagemin from 'vite-plugin-imagemin';
import compression from 'vite-plugin-compression2';

const plugins = (mode: string): PluginOption[] => {
    const prodPlugins = mode === 'production' ? [
        visualizer(),
        commonjs(),
        splitVendorChunkPlugin(),
    ] : [];
    return [
        react(),
        tsconfigPaths(),
        svgSprite({ symbolId: 'icon-[name]-[hash]' }),
        vitePluginImp({
            libList: [
                { libName: 'lodash', libDirectory: '', camel2DashComponentName: false },
            ],
        }),
        viteImagemin({
            gifsicle: {
                optimizationLevel: 7, // 设置GIF图片的优化等级为7
                interlaced: false // 不启用交错扫描
            },
            optipng: {
                optimizationLevel: 7 // 设置PNG图片的优化等级为7
            },
            mozjpeg: {
                quality: 20 // 设置JPEG图片的质量为20
            },
            pngquant: {
                quality: [0.8, 0.9], // 设置PNG图片的质量范围为0.8到0.9之间
                speed: 4 // 设置PNG图片的优化速度为4
            },
            svgo: {
            plugins: [
                {
                    name: 'removeViewBox' // 启用移除SVG视图框的插件
                },
                {
                    name: 'removeEmptyAttrs',
                    active: false // 不启用移除空属性的插件
                }
            ]
            }
        }), 
        compression({
            algorithm: "gzip", // 指定压缩算法为gzip,[ 'gzip' , 'brotliCompress' ,'deflate' , 'deflateRaw']
            threshold: 10240, // 仅对文件大小大于threshold的文件进行压缩,默认为10KB
            deleteOriginalAssets: false, // 是否删除原始文件,默认为false
            include: /\.(js|css|json|html|ico|svg)(\?.*)?$/i, // 匹配要压缩的文件的正则表达式,默认为匹配.js、.css、.json、.html、.ico和.svg文件
            compressionOptions: { level: 9 }, // 指定gzip压缩级别,默认为9(最高级别)
            // verbose: true, //是否在控制台输出压缩结果
            // disable: false, //是否禁用插件
        }),
        // 针对特殊资源,采用brotli压缩
        // compression({ algorithm: 'brotliCompress', exclude: [/\.(br)$/, /\.(gz)$/], deleteOriginalAssets: true }),
        [...prodPlugins]
    ];
};

export default plugins;        

上面的插件,想必大家都比较熟悉,我就挑几个有用但是不常见的来简单说一下。

vite-tsconfig-paths

vite-tsconfig-paths可以识别我们在tsconfig.json中的paths属性,并将其转换为vitealias属性。

例如我们在tsconfig.json中配置了如下的paths信息。

{
  "paths": { // 设置路径映射
      "@/*":["src/*"],
      "@hooks/*": ["src/hooks/*"],
      "@assets/*": ["src/assets/*"],
      "@utils/*": ["src/utils/*"],
      "@components/*": ["src/components/*"],
      "@api/*": ["src/api/*"]
  }
}

通过vite-config-paths的处理,最后的viteconfig中就会有

{
  resolve: {
      alias: {
        '@': '/src',
        '@hook':'/src/hooks/',
        '@asset': '/src/hooks/',
        //.....
      },
  },
}

也就是我们可以不用在vite配置alias然后,还可以在代码中进行@hook/@asset的别名访问。

vite-plugin-imagemin

vite-plugin-imagemin

该插件用于对项目中的各种图片资源进行压缩处理。毕竟,在前端项目中图片是一个很耗费网络资源的数据。

上面的注释也很清晰,我们不做使用方式的介绍,其实使用vite-plugin-imagemin时,最麻烦的是,刚开始的安装过程。如果不做特殊处理,它是一直在控制台卡着下载,随后报一个网络超时的问题。

为了解决这个,我们需要在package.json新增一个resolutions属性。

{
  "resolutions": {
    "bin-wrapper": "npm:bin-wrapper-china"
  }
}

package.json文件中,resolutions字段用于定义自定义包版本或范围,以解决依赖关系中的问题。这允许我们覆盖依赖项的版本范围,而无需手动编辑yarn.lock文件

想了解关于resolutions 可以看yarn_resolutions


rollup-plugin-visualizer

我们可以利用rollup-plugin-visualizer在打包时,生成项目的各个资源的占比图,然后根据这些占比可以很容易看到哪些资源过大,为我们提供优化的思路。

利用mode处理开发环境和生成环境

从上面的代码中,我们可以看到我们使用mode来处理developmentproduction,这样就可以将开发模式和生产模式区分开。


vite.server.config.ts

import { loadEnv } from 'vite';
const server = (mode: string) => {
  const env = loadEnv(mode, process.cwd(), 'VITE_');
  return ({
          open: true,
          host: '0.0.0.0',
          port: 3005,
          hmr: {
              overlay: false,
          },
          proxy: {
              // env 中指定地址,则优先从env中获取
              '/api/': env.VITE_PROXY_URL ?? 'https://xx.dev.com/',
          },
          watch: {
              // ignored: ['!**/node_modules/@kx/database/dist/**'],
          },
      })
};

export default server;         

该文件用于启动一个前端服务,然后我们依据env来区分代理地址。

而这个VITE_PROXY_URL我们可以在package.json中的scripts中配置。

{
  "scripts": {
    "build": "tsc && vite build",
    "dev": "vite",
    "dev:prod": "cross-env VITE_PROXY_URL=https://xx.yy.com vite --mode=production",
    "dev:test": "cross-env VITE_PROXY_URL=https://xx.test.com vite --mode=test",
  },
}

通过,上面的方式我们可以通过dev:prod在本地访问线上环境的数据。


vite.build.config.ts


const build = () => {
    return ({
            chunkSizeWarningLimit: 500,
            minify: 'esbuild',
            sourcemap: false,
            manifest: false,
            cssCodeSplit: true,
            rollupOptions: {
                maxParallelFileOps: 40,
                output: {
                    // 设置chunk的文件名格式
                    chunkFileNames: (chunkInfo) => {
                        const facadeModuleId = chunkInfo.facadeModuleId
                            ? chunkInfo.facadeModuleId.split("/")
                            : [];
                        const fileName1 =
                            facadeModuleId[facadeModuleId.length - 2] || "[name]";
                        // 根据chunk的facadeModuleId(入口模块的相对路径)生成chunk的文件名
                        return `js/${fileName1}/[name].[hash].js`;
                    },
                    // 设置入口文件的文件名格式
                    entryFileNames: "js/[name].[hash].js",
                    // 设置静态资源文件的文件名格式
                    assetFileNames: "[ext]/[name].[hash:4].[ext]",
                },
            },
        })
};

export default build;

由于vite的开发模式和生成模式不一致,所以我们需要配置打包工具esbuild/teaser,还有rollupOptions来处理打包后的资源名称和位置。


vite.config.ts

我们通过不同的文件将vite的功能进行拆分配置,这样我们能够在修改指定的配置时,能够轻松的查看到。

然后,我们在vite.config.ts中引入并配置到相关的属性中。

import { defineConfig } from 'vite';
import plugins from "./vite.plugin.config";
import build from './vite.build.config';
import server from './vite.server.config';

export default defineConfig(({ mode }) => {

    return {
        server: server(mode),
        plugins:plugins(mode),
        build: build
   };
    
});

其实,针对vite的配置还有很多,这点我们在浅聊Vite中有过介绍的。


后记

分享是一种态度

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。