人类的赞歌是勇气的赞歌
大家好,我是柒八九。一个专注于前端开发技术/Rust
及AI
应用知识分享的Coder
。
写在最前面
这不是快过年了吗,肯定会有发红包的环节,然后我也定制了几款寓意比较好的红包封面。然后最为回馈粉丝的新年礼物。 请大家笑纳。
由于红包性质,需要大家在指定平台领取。望周知
前言
在Rust 赋能前端-开发一款属于你的前端脚手架中我们介绍过使用Rust
来写一个基于前端项目的脚手架,在发文后反响也不错。然后,有些动手能力强的小伙伴,已经将其应用到实际开发中了。
如果,还有没把玩过这个小工具的同学也不用着急,反正经过一顿操作猛如虎,我们就会构建出一个拥有一个功能完备的前端项目,你只需要关心自己页面的构建。
<,,,,>
具体的页面结构如下:
在脚手架
的文章中,我们将主要的精力放在了Rust
上,而没有过多介绍前端项目的功能结构。所以,今天我们来讲讲一个功能完备的前端项目(React版本
)需要具备哪些东西。
快速创建一个
React
项目,我们可以选择Create-React-App或者Vite,下文中我们以Vite
构建的项目作为底,来进行二次的配置。(当然,下面有的配置可能根据打包工具的不同而有所差别,但是思路都是一样的)
好了,天不早了,干点正事哇。
我们能所学到的知识点
- TypeScript
- Eslint + Oxlint
- Prettier 或 Biome
- Husky
- Css相关
- Browserslist
- axios
- Errorboundy
- 自定义hook
- 全局loading
- 路由
- 状态管理
- Vite 配置优化
1. TypeScript
有人说Ts
是一把双刃剑,对于功能简单的项目而言,无端的引入Ts
无疑是作茧自缚;但是呢,对于那些数据流向复杂和业务盘根错节的项目而言,从自我角度而言,引入Ts
无疑是明智之选。
tsconfig.xx.json
在使用
Vite
构建的React+Ts
项目,会在根目录下创建两个关于Ts
的文件。
tsconfig.json
tsconfig.node.json
这是因为项目使用两个不同的环境来执行 Ts
代码:
-
tsconfig.json
- 作用于应用程序(src 文件夹)它在浏览器中运行
- 用于配置
React
项目的Ts
编译选项,包括目标版本
、模块解析方式、JSX
语法支持等。 - 它定义了项目的编译规则和设置。
-
tsconfig.node.json
Vite
本身(包括其配置)是在Node
内的计算机上运行的,而Node
是完全不同的环境(与浏览器相比),具有不同的应用程序接口和限制条件。- 用于配置
Vite
本身的Ts
编译选项,它包含了Vite
配置文件的引用和一些特定于 Node 环境的编译选项。 - 这个文件主要用于
Vite
在Node
环境下的编译和构建过程。
针对我们来讲,要对我们项目做针对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
(xx
为development
/production
)文件来管理环境变量,并使用import.meta.env
来在代码中访问这些环境变量。
ES2020
为import
命令添加了一个元属性import.meta
,返回当前模块的元信息。 关于这块可以参考我们之前的文章你真的了解ESM吗?
例如,
-
在项目的根目录下创建
.env.development
文件,并在其中定义我们的环境变量 VITE_API_KEY=your-api-key VITE_BASE_URL=front789.com -
使用
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
上面有3类信息。
- 配置
eslint
的方式有很多js/esm/yaml/json/package.json
- 如果多个配置文件存在,它们是有优先级的
- .eslintrc.js优先级最高
- 在
package.json
中优先级最低
- 配置方式主要有两种方式
.eslintrc.*
package.json
中新增eslintConfig
属性
当我们使用Vite
构建React+Ts
项目时候,会在根目录下为我们创建.eslintrc.cjs
。但是呢,为了能复用配置文件,我们采用.eslintrc.json
方式来配置eslint
。(之所以采用.eslintrc.json
和Oxlint
有关)
配置.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
Prettier
和Eslint
存在相同的问题,就是性能问题。然后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
都是用于规则的校验。
而prettier
和biome
是二选一的。我们可以使用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
(Syntropically Awesome Style Sheets
):它被设计为与所有版本的CSS
兼容。它遵循命令式样式模式,这意味着我们可以指定事情的完成方式。- 在某些时候,它往往感觉更像是一种编程语言,而不是一种样式语言。
-
- LESS(
Leaner Style Sheets
)是CSS
的向后兼容语言扩展。LESS
本质上遵循声明式样式模式。这意味着我们指定我们想要看到的内容,而不是我们希望如何完成它。这主要是因为它与函数式编程相似
,这使得它更具可读性和更容易理解。
- LESS(
-
Stylus
提供了更多的表现力,同时保持了更简洁的语法。有Python背景的人会对其非花括号缩进语法产生强烈的共鸣。
针对这三种的优缺点,我们后期专门会有文章介绍。
Scss 语法简介
Sass/Scss
由于拥有广泛的社区支持,所以我们的项目首选Sass
。
我们来简单介绍一下Sass
的语法特性。更详细的语法可以参考sass官网
- 变量:允许使用变量来存储和重用值,从而增强代码的可维护性和一致性。
- 混入(
Mixins
):允许包含CSS 属性组
。它们提高了代码的可重用性,并使管理复杂的样式变得更加容易。 - 嵌套:支持 CSS 选择器的嵌套,提供更直观的方式来编写和组织样式。它提高了可读性并使代码结构更加透明。
- 部分(
Partials
)和模块化Modules
:允许创建可以导入到其他Sass
文件的部分Sass
文件。此功能增强了模块化和代码组织,使开发人员能够独立处理项目的特定部分。- 可以创建包含
CSS
小片段的部分Sass
文件,我们可以将这些 CSS 片段包含在其他 Sass 文件中。 部分文件
是一个以下划线开头命名的Sass
文件。我们可以将其命名为_partial.scss
之类的名称。下划线让Sass
知道该文件只是一个部分文件,并且不应将其生成为CSS
文件。部分文件
与@use
规则一起使用。
- 可以创建包含
- 扩展(
Extend
)和继承(Inheritance
):Sass
引入了占位符选择器
的概念,它充当可重用的样式块。它们可以由其他选择器扩展和继承,从而减少代码重复并促进更易于维护的代码库。
- 使用
@extend
可以让我们从一个选择器到另一个选择器共享一组 CSS 属性
PostCSS
PostCSS 是一个
JavaScript
工具,可将CSS
代码转换为抽象语法树 (AST
),然后提供API
,以便使用JavaScript
插件对其进行分析和修改。
尽管它的名字中包含Post
,有的同学就会将其与预处理器(pre-processors
)进行关联,其实它既不是后处理器也不是预处理器,它只是一个将特殊的 PostCSS 插件语法转换为 CSS 的转译器。
可以将其视为 CSS 界的Bable工具
PostCSS
提供了一个庞大的插件生态系统来执行不同的功能,例如我们在开发中常见的autoprefixer和Tailwind 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 的配置
},
};
};
我们来简单介绍上面的几个插件的作用。
-
- 用于在 CSS 文件中引入其他 CSS 文件
postcss-import
与原生CSS中的导入规则不同。- 原生 CSS 中的
@import
规则,因为它会阻止同时下载样式表,从而影响加载速度和性能。浏览器必须等待加载每个导入的文件,而不是能够一次加载所有 CSS 文件。
- 原生 CSS 中的
-
- 它可以解析供应商前缀,如
-webkit
、-moz
和-ms
,并使用来自 Can I Use 网站的值将其添加到 CSS 规则中。 - 使用 browserslist来决定兼容哪些版本的浏览器或者
Node
。
- 它可以解析供应商前缀,如
-
- 能够在代码中使用现代 CSS(如嵌套和自定义媒体查询),将其转换为浏览器可以理解的 CSS。
- 它有一个
stage
选项,可以根据CSS
功能在作为 Web 标准实现的过程中的稳定性来确定要进行Polyfill
的 CSS 功能- stage 可以是 0(实验)到 4(稳定)或
false
。第 2 阶段是默认阶段。
- stage 可以是 0(实验)到 4(稳定)或
- 此外,预设环境插件默认包含
Autoprefixer
插件,并且browsers
选项将自动传递给它。 - 也就是说我们设置了
postcss-preset-env
就不需要设置Autoprefixer
了
-
- 这是一个 CSS linter,可以帮助我们避免代码中的错误破坏我们的页面
- 默认情况下不启用任何规则,也没有默认值。我们必须显式配置每个规则才能将其打开。
-
- 这是一个压缩工具,用于尽可能减小最终 CSS 文件大小,以便我们的代码为生产环境做好准备。
- 某些部分将被更改以尽可能减小大小,例如删除不必要的空格、换行、重命名值和变量、合并在一起的选择器等等。
-
Tailwind CSS 是一个 CSS 框架,旨在使用户能够更快、更轻松地创建应用程序。我们可以使用
实用类
来控制布局、颜色、间距、排版、阴影等,以创建完全自定义的组件设计- 之前我们在Tailwind CSS那些事儿就有过介绍。这里就不再过多介绍了。
Lightning CSS
由于PostCss
是用JS
写的,那势必就会有性能通病。幸运的是,我们现在有Lightning CSS
下图是对CSSNano/ESBuild
/Lightning Css
的压缩对比图。
从图中看到,它也是Rust
重写的。
针对不同的打包工具,它有各自的配置方式。(这里我们就按vite
来讲)。
但是呢,使用lightningcss
速度是快,但是一些注意事项。
- 对于
scss
文件的注释,我们不能使用// xx
(这不是标准的scss注释)而是需要使用/* xx */
,scss文件使用//报错的具体解释 - 针对
@keyframes
等自定义属性,我们需要使用lightning css
的customAtRules
和visitor
进行配置。
反正,效率是提升了,这块的学习成本比较高。
6. Browserslist
The best practice is to use
.browserslistrc
config orbrowserslist
key inpackage.json
to share target browsers with Babel, ESLint and Stylelint
在配置PostCSS
的Browserslist
时,会提示我们最好将Browserslist
抽离出去,这样我们就可以为eslint/babel/stylelint
统一配置。
那什么是Browserslist呢?
它也可以通过很多方式配置。例如
.browserslistrc
- 在
package.json
配置browserslist
字段 - 在项目的根路上上创建
browserslist
- 创建
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
因为使用它太繁琐。基本上都是在fetch
和axios
二选一。
我们来简单对比一下fetch
和axios
fetch
- 提供了在
window
对象上定义的fetch()
方法。它还提供了一个 JavaScript 接口,用于访问和操作 HTTP 管道的各个部分(请求和响应)。 fetch
方法有一个必传参数——要获取的资源的 URL。此方法返回一个 Promise,可用于检索对请求的响应。
- 提供了在
axios
- 它是一个 Javascript 库,用于从浏览器的 Node.js 或 XMLHttpRequest 发出 HTTP 请求,它支持 JS ES6 原生的 Promise API。
- 它可用于拦截 HTTP 请求和响应,并启用客户端针对 XSRF 的保护。
- 它还具有取消请求的能力。
fetch
vs axios
特性 | Axios | Fetch |
---|---|---|
请求对象中的 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 中的
Errorboundy
是React
应用程序中错误处理的一个重要方面。
- 它们是
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
时使用的是类组件。并不是说类组件不好,但是现在的React
是Hook
开发模式的天下。 并且,上面的构建的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
配置,然后也能为我们提供和后端进行异步接口的操作。
对于,精益求精的我们,是不是可以在发起异步请求时候,进行一个loading
的UI
交互逻辑。
当然,我们可以在每个ajaxXX
触发的前后,使用代码侵入业务的方式。在每个异步接口触发的时候,使用变量loading:boolean
来进行<Loading />
组件的渲染。
上述的方式可行吗,必须可行。但是不够优雅。这里我们提供一种方式。基于全局属性ajaxStatus
(这个全局属性可以放到window
下,也可以放置到全局状态中redux/recoil
等)。他们的处理思路都类似的。
存储异步状态
这里我们选择将其放置到window
下。
由于我们项目使用了ts
所以,我们需要在vite-env.d.ts
对window
配置相关属性。
/// <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
状态管理库可以分为三类:
- 基于
Reducer
:需要分发(dispatch
)操作来更新一个被称为单一数据源的中央状态。在这一类中,我们有Redux和Zustand。- 优点:老牌状态管理库,社区完善
- 缺点: 样板代码太多
- 基于
Atom
:将状态分割成称为原子(atom
)的小数据片段,可以使用React hooks
进行读写。在这一类中,我们有Recoil和Jotai。- 优点:简单且可扩展,能够从更小粒度去控制状态
- 缺点:不能在组件外部使用状态
- 基于
Mutable
:利用Proxy
创建可直接写入或以响应方式读取的可变数据源。这一类中的候选者有MobX和Valtio。- 优点:依赖项在状态更改时会自动更新
- 缺点:异步更新中的竞态条件可能导致应用程序状态混乱
既然,有这么多状态管理库,我们该如何选择呢。
最适合你项目的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
分为几部分
vite.plugin.config.ts
vite.server.config.ts
vite.build.config.ts
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
属性,并将其转换为vite
的alias
属性。
例如我们在tsconfig.json
中配置了如下的paths
信息。
{
"paths": { // 设置路径映射
"@/*":["src/*"],
"@hooks/*": ["src/hooks/*"],
"@assets/*": ["src/assets/*"],
"@utils/*": ["src/utils/*"],
"@components/*": ["src/components/*"],
"@api/*": ["src/api/*"]
}
}
通过vite-config-paths
的处理,最后的vite
的config
中就会有
{
resolve: {
alias: {
'@': '/src',
'@hook':'/src/hooks/',
'@asset': '/src/hooks/',
//.....
},
},
}
也就是我们可以不用在vite
配置alias
然后,还可以在代码中进行@hook/@asset
的别名访问。
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
来处理development
和production
,这样就可以将开发模式和生产模式区分开。
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中有过介绍的。
后记
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。