❝人类的赞歌是勇气的赞歌 ❞
大家好,我是「柒八九」。一个「专注于前端开发技术/Rust
及AI
应用知识分享」的Coder
。
这不是快过年了吗,肯定会有发红包的环节,然后我也定制了几款寓意比较好的红包封面。然后最为回馈粉丝的新年礼物。 请大家笑纳。
在Rust 赋能前端-开发一款属于你的前端脚手架中我们介绍过使用Rust
来写一个基于前端项目的脚手架,在发文后反响也不错。然后,有些动手能力强的小伙伴,已经将其应用到实际开发中了。
如果,还有没「把玩」过这个小工具的同学也不用着急,反正经过一顿操作猛如虎,我们就会构建出一个拥有一个功能完备的前端项目,你只需要关心自己页面的构建。
<<< 左右滑动见更多 >>>
具体的页面结构如下:
在脚手架
的文章中,我们将主要的精力放在了Rust
上,而没有过多介绍前端项目的功能结构。所以,今天我们来讲讲「一个功能完备的前端项目」(React版本
)需要具备哪些东西。
❝快速创建一个
React
项目,我们可以选择Create-React-App[1]或者Vite[2],下文中我们以Vite
构建的项目作为底,来进行二次的配置。(当然,下面有的配置可能根据打包工具的不同而有所差别,但是思路都是一样的) ❞
好了,天不早了,干点正事哇。
❝
❞
有人说Ts
是一把双刃剑,对于功能简单的项目而言,无端的引入Ts
无疑是作茧自缚;但是呢,对于那些「数据流向复杂」和业务盘根错节的项目而言,从自我角度而言,引入Ts
无疑是明智之选。
❝在使用
Vite
构建的React+Ts
项目,会在根目录下创建两个关于Ts
的文件。
tsconfig.json
tsconfig.node.json
❞
这是因为项目使用「两个不同的环境」来执行 Ts
代码:
tsconfig.json
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官网配置文档[3]
虽然,我们对Ts
做了配置,但是呢在开发中还是会遇到Ts
的报错问题。例如,我们想在Window
上挂载一个类型(x
),并且在通过winodw.x
进行设置和取值。但是此时,Ts
就会报错。我们需要有一种方式来告知Ts
这种方式是合法的。
此时,我们的vite-env.d.ts
就派上用场了。
/// <reference types="vite/client" />
interface Window {
ajaxStatus: 'pending' | 'resolved';
}
在vite
项目中,我们还可以通过define[4]来定义「全局常量」。
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=https://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
}
❝莎士比亚(
Shakespeare
)名言:There are a thousand Hamlets in a thousand people's eyes
翻译成中文就是我们耳熟能详的:「一千个读者眼中就会有一千个哈姆雷特」 ❞
如果一个团队中对代码规范
没有一个合理的认知,那写出来的代码就是「千人千面」了。所有,我们急需一种方案在规范方面去制约这种情况的发生。
索性,我们有eslint
。(其实,我们还有更好的选择-Oxlint
,我们也会有涉猎)
下图是eslint配置文件的文档[5]
上面有3类信息。
eslint
的方式有很多js/esm/yaml/json/package.json
package.json
中优先级最低」.eslintrc.*
package.json
中新增eslintConfig
属性当我们使用Vite
构建React+Ts
项目时候,会在根目录下为我们创建.eslintrc.cjs
。但是呢,为了能复用配置文件,我们采用.eslintrc.json
方式来配置eslint
。(之所以采用.eslintrc.json
和Oxlint
有关)
{
"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"] // 忽略模式
}
上面的每个都有详细的注释,这里就不过多展开说明了。
虽然eslint
能够让我们的项目更加健壮,但是呢,由于eslint
的校验是很耗费时间,如果项目很大的话,针对格式校验也是一件很痛苦的事情。
是时候,拿出新的解决方案了。Oxlint[6]-- 一款用Rust
编写的针对JS
格式校验工具。
❝它不是
eslint
的替代方案,而它是eslint
的增强方案。 ❞
如果我们在eslint
上耗费了很多时间,我们可以在项目中引入Oxlint
来优化代码校验时间。
Oxlint
有很多操作方式,更多的是配合husky
或者ci
进行代码校验。针对如何进行此处的操作,我们在介绍husky
的时候来说明。
由于Oxlint
刚开源不久,它的官网也很模糊,所有有些必要的信息我们是不好获取的。并且它的有些Rules
也不单单针对JS
,所有我们需要对其需要进行筛选。
npx oxlint@latest --rules
来进行rules
的查看eslint/jsx/react/import/jest
/unicorn[7](轻量级多体系结构 CPU 仿真器框架)npx oxlint@latest -h
查看各种命令-c ./eslintrc.json
复用eslint
的规则.eslintrc.json
作为eslint
的配置原因」oxlint
复用配置信息。--fix
来修复部分问题,但是这种修复方式有限,不会百分百修复发现的问题「爱美之心,人皆有之」。我们也想让我们的代码变得赏心悦目,那代码美化就必不可少。
如果,提到代码美化,那prettier[8]在前端有着举足轻重的地位。
从上图中我们得到几类信息
Eslint
类似,Prettier
也有多种配置方式。图中,按照「优先级由高到低排列」。Prettier
没有全局配置方式我们选择.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,
};
❝当然,每个团队都有自己的规范,所有上面的提供的代码,不代表最优方案,需要大家见仁见智。 ❞
Prettier
和Eslint
存在相同的问题,就是「性能问题」。然后Prettier
的创始人发起了一个优化Prettier的挑战[9]。在高手云集的情况下,Biome[10]杀出重围,脱颖而出。
biome
也是一款用Rust
编写的前端工具库。
❝有没有感觉到
Rust
在重构前端工具中,越来越重要。这里王婆卖瓜一下,前端时间,我们用Rust
写了一个前端脚手架,有兴趣的同学可以自行使用。 ❞
下图是Biome
在美化代码和校验代码和传统工具的benchmark
的结果。
从结果来看Biome
是一个不错的美化代码的新方案,但是,但是,由于Biome
是新项目,有些边缘case
还没完全兼顾。如果对不关心这些东西的话,其实无脑使用Biome
是一个不错的选择。毕竟,效率优先。
其实,针对eslint/prettier
我们可以设置在保存文件时候,利用Vscode
进行自动校验和修正,这个不在我们本文的讨论范围中。这个属于Vscode
的配置项了。
但是呢,我们选择了另外一种触发eslint/prettier
的方式,那就是利用husky[11]在触发git hook
时处理。
在我们脚手架中在初始化项目时,我们就会执行git init
来将项目变成一个仓库。
所以,我们可以直接使用husky
的配置。
下图是官网的示例代码。
执行下面命令,用于按照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
是优先被安装的库。
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
中新增一个lint-staged
的命令
{
"lint-staged": {
"*.{ts,tsx}": [
"oxlint", //oxlint 校验
"eslint", // eslint 校验
//"prettier --write" // 使用prettier修正代码
"biome format" // 使用biome 修正代码
]
},
}
上面的oxlint/eslint
都是用于规则的校验。
而prettier
和biome
是二选一的。我们可以使用prettier
亦或者使用biome
来对代码进行修正。这都是随意的。
npx husky add .husky/pre-push 'yarn tsc-test'
pre-push
钩子是在执行git push
之前运行的脚本,用于在代码push
到远程仓库之前执行一些操作,比如运行测试或进行代码检查。
在这种情况下,yarn tsc-test
是希望在每次push
之前运行的命令。这可能是用于运行Ts
编译器的测试命令,以确保在推送代码之前没有类型错误或编译问题。
作为浏览器四大语言之一的CSS
,处理起来也颇费功夫。(除了js/html/css
,wasm
也算一种内置语言,想了解更多,可以参考浏览器第四种语言-WebAssembly)
pre-processors
)❝CSS 预处理器对于那些希望通过
变量
、混合
、数学函数
、运算函数
、嵌套语法
和样式表模块化
来增强页面样式的前端开发人员来说是一个真正可用的工具。它们可以轻松实现重复样式的自动化、减少错误并编写可重用的代码,同时确保与各种 CSS 版本的向后兼容性。 ❞
常见的CSS预处理器有
SASS
(Syntropically Awesome Style Sheets
):它被设计为与所有版本的 CSS
兼容。它遵循命令式样式模式,这意味着我们可以指定事情的完成方式。Leaner Style Sheets
)是 CSS
的向后兼容语言扩展。 LESS
本质上遵循声明式样式模式。这意味着我们指定我们想要看到的内容,而不是我们希望如何完成它。这主要是因为它与函数式编程相似
,这使得它更具可读性和更容易理解。Stylus
提供了更多的表现力,同时保持了更简洁的语法。有Python背景的人会对其非花括号缩进语法产生强烈的共鸣。针对这三种的优缺点,我们后期专门会有文章介绍。
Sass/Scss
由于拥有广泛的社区支持,所以我们的项目首选Sass
。
我们来简单介绍一下Sass
的语法特性。更详细的语法可以参考sass官网[15]
Mixins
):允许包含 CSS 属性组
。它们提高了代码的可重用性,并使管理复杂的样式变得更加容易。Partials
)和模块化Modules
:允许创建可以导入到其他 Sass
文件的部分 Sass
文件。此功能增强了模块化和代码组织,使开发人员能够独立处理项目的特定部分。CSS
小片段的部分 Sass
文件,我们可以将这些 CSS 片段包含在其他 Sass 文件中。部分文件
是一个以「下划线开头命名」的 Sass
文件。我们可以将其命名为 _partial.scss
之类的名称。下划线让 Sass
知道该文件只是一个部分文件,并且不应将其生成为 CSS
文件。部分文件
与 @use
规则一起使用。Extend
)和继承(Inheritance
):Sass
引入了占位符选择器
的概念,它充当可重用的样式块。它们可以由其他选择器扩展和继承,从而减少代码重复并促进更易于维护的代码库。@extend
可以让我们从一个选择器到另一个选择器共享一组 CSS 属性❝PostCSS[16] 是一个
JavaScript
工具,可将CSS
代码转换为抽象语法树 (AST
),然后提供API
,以便使用JavaScript
插件对其进行分析和修改。 ❞
尽管它的名字中包含Post
,有的同学就会将其与预处理器(pre-processors
)进行关联,其实它既不是后处理器也不是预处理器,它「只是一个将特殊的 PostCSS 插件语法转换为 CSS 的转译器」。
❝可以将其视为 CSS 界的Bable[17]工具 ❞
PostCSS
提供了一个庞大的插件生态系统来执行不同的功能,例如我们在开发中常见的autoprefixer[18]和Tailwind css[19]
❝
PostCSS
的核心是「插件」。每个插件都是为特定任务而创建的。 ❞
我们可以通过官网提供的Post Plugins[20]来搜索我们想要的插件。或者通过postcss github[21]查找
和Eslint/Prettier
类似,配置PostCSS
也有很多方式。
package.json
.postcssrc
.postcssrc.json
.postcssrc.yml
(.postcssrc|postcss.config).(js|mjs|cjs|ts|mts|cts)
我们选择最常规的方式,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 的配置
},
};
};
我们来简单介绍上面的几个插件的作用。
@import
规则,因为它会阻止同时下载样式表,从而影响加载速度和性能。浏览器必须等待加载每个导入的文件,而不是能够一次加载所有 CSS 文件。postcss-import
与原生CSS中的导入规则不同。 -webkit
、-moz
和 -ms
,并使用来自 Can I Use 网站[24]的值将其添加到 CSS 规则中。Node
。false
。第 2 阶段是默认阶段。stage
选项,可以根据 CSS
功能在作为 Web 标准实现的过程中的稳定性来确定要进行 Polyfill
的 CSS 功能Autoprefixer
插件,并且 browsers
选项将自动传递给它。postcss-preset-env
就不需要设置Autoprefixer
了实用类
来控制布局、颜色、间距、排版、阴影等,以创建完全自定义的组件设计由于PostCss
是用JS
写的,那势必就会有性能通病。幸运的是,我们现在有Lightning CSS[29]
下图是对CSSNano[30]/ESBuild
/Lightning Css
的压缩对比图。
从图中看到,它也是Rust
重写的。
针对不同的打包工具,它有各自的配置方式。(这里我们就按vite
来讲)。
但是呢,使用lightningcss
速度是快,但是一些注意事项。
scss
文件的注释,我们不能使用// xx
(这不是标准的scss注释)而是需要使用/* xx */
,scss文件使用//报错的具体解释[31]@keyframes
等自定义属性,我们需要使用lightning css
的customAtRules
和visitor
进行配置。❝反正,效率是提升了,这块的学习成本比较高。 ❞
❝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[32]呢?
来自官网的截图
它也可以通过很多方式配置。例如
.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"
]
}
}
❝不进行后端数据交互的前端项目都是耍流氓。 ❞
前端项目中有很多方式能够发起异步请求。例如XMLHttpRequest[33]/Fetch[34]等浏览器原生API,还有axios[35]。一般项目中,首先不会选择XMLHttpRequest
因为使用它太繁琐。基本上都是在fetch
和axios
二选一。
我们来简单对比一下fetch
和axios
fetch
window
对象上定义的 fetch()
方法。它还提供了一个 JavaScript 接口,用于访问和操作 HTTP 管道的各个部分(请求和响应)。fetch
方法有一个必传参数——要获取的资源的 URL。此方法返回一个 Promise,可用于检索对请求的响应。axios
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
。
文件目录,在项目的根目录下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()
有错不可怕,可怕的是,知道错了,不及时修正。
❝React 中的
Errorboundy
是React
应用程序中错误处理的一个重要方面。
React
组件,可以在其子组件树中的任何位置捕获 JavaScript
错误,记录这些错误,并显示「回退 UI」,而不是崩溃的组件树。catch {}
块,但用于组件。❞
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[36]
它使用一个名为 ErrorBoundary
的简单组件,我们可以使用它来包装可能容易出错的代码。
react-error-boundary
的优点在于它消除了手动编写类组件和处理状态的需要。它在幕后完成所有繁重的工作,使我们能够专注于构建应用程序。
关于,如何使用react-error-boundary
我们后期在详细讲。(这里就不再过多解释)
❝不要重复做那些无关紧要的事情 ❞
就像上面说的那样,现在是Hook
的天下。我们可以基于React内置Hook[37]做排列组合,形成符合我们特定业务逻辑的自定义Hook。
在之前美丽的公主和它的27个React 自定义 Hook中,我们介绍了在项目开发中比较常用的自定义hook。并且,在我们的f_cli
中也有此项的配置。
如果,有些同学感觉自己构建自定义Hook
比较麻烦,那么可以选择aHook[38],它提供了很多有用的自定义Hook
。
在讲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>
);
};
❝React Router[39]仍然是处理 React 应用中路由的「第一选择」。凭借其丰富的文档和积极的社区,它继续是我们应用中声明性路由的可靠选择。 ❞
React
状态管理库可以分为三类:
Reducer
:需要分发(dispatch
)操作来更新一个被称为「单一数据源」的中央状态。在这一类中,我们有Redux[40]和Zustand[41]。Atom
:将状态分割成称为原子(atom
)的小数据片段,可以使用React hooks
进行读写。在这一类中,我们有Recoil[42]和Jotai[43]。Mutable
:利用Proxy
创建可直接写入或以响应方式读取的可变数据源。这一类中的候选者有MobX[44]和Valtio[45]。既然,有这么多状态管理库,我们该如何选择呢。
❝最适合你项目的React状态管理库取决于你和你团队的具体需求和专业知识 ❞
请不要:仅基于项目大小
和复杂性
选择库。因为我们可能在某处听说过X更适合大型项目,而Y更适合较小的项目。库的作者在设计其库时考虑了可扩展性,而项目的可扩展性取决于我们如何编写代码和使用库,而不是我们选择使用哪些库。
由于用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
下面是我们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[46]可以识别我们在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[47]
该插件用于对项目中的各种图片资源进行压缩处理。毕竟,在前端项目中图片是一个很耗费网络资源的数据。
上面的注释也很清晰,我们不做使用方式的介绍,其实使用vite-plugin-imagemin
时,最麻烦的是,刚开始的安装过程。如果不做特殊处理,它是一直在控制台卡着下载,随后报一个网络超时的问题。
为了解决这个,我们需要在package.json
新增一个resolutions
属性。
{
"resolutions": {
"bin-wrapper": "npm:bin-wrapper-china"
}
}
❝在
package.json
文件中,resolutions
字段用于定义自定义包版本或范围,以解决依赖关系中的问题。这允许我们覆盖依赖项的版本范围,而无需手动编辑yarn.lock
文件 ❞
想了解关于resolutions
可以看yarn_resolutions[48]
我们可以利用rollup-plugin-visualizer[49]在打包时,生成项目的各个资源的占比图,然后根据这些占比可以很容易看到哪些资源过大,为我们提供优化的思路。
从上面的代码中,我们可以看到我们使用mode
来处理development
和production
,这样就可以将开发模式和生产模式区分开。
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
在本地访问线上环境的数据。
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
的功能进行拆分配置,这样我们能够在修改指定的配置时,能够轻松的查看到。
然后,我们在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中有过介绍的。