前端开发规范文档
前端开发规范文档
链接跳转可能会跳到 notion,原文由 notion 写成。在 cnblogs 中可以通过鼠标移动到标题查看目录跳转。
规范文档
持续更新的前端开发规范文档,文档中描述了前端开发的各种规范。如果有更好的建议,欢迎提交。
目录
文档规范
如何编写一个好的文档,工程介绍 Readme 和技术文档 Docmentation.
约定规范
描述工程中的常见约定规范,包括:
项目结构
规范哲学
- 所有的功能和逻辑,只跟模块相关的,都应该放在该模块下,模块之间保持独立性,互相独立,互不关心,完全解耦。
- 不要把所有的相关功能行为都放进一个全局的文件维护,这是反模式的设计。所有的设计模式都是为了高内聚,低耦合。
例如:把所有业务模块的 api service 耦合到一个大的 allApi.js 是并不明智的做法,这样做变成了高内聚,高耦合。限制代码的可重用性,降低了代码的可维护性和可扩展性。:
- 高耦合:意味着如果某个模块的 Service 发生变化,可能会影响到其他模块。这违背了模块化设计的原则,应该尽量减少模块之间的依赖关系。
- 维护成本:一个大的全局文件会变得非常庞大,难以管理和维护。每次需要添加或修改 Service 时,都需要修改这个文件,这可能会引发各种问题,比如冲突、漏洞等。
- 测试成本:将所有的 API Service 都放在一个文件中,会使得测试变得困难。每次需要测试某个 API Service 时,都需要加载整个文件,这可能会导致测试时间过长
最佳实践
- 全局性功能 - 放置到全局文件目录,使用
@/{moduleName}.{featName}.{ts|js}
引入。意义即:全局公有,全局共有,对外暴露,全局使用- 全局组件 - /components
- 全局常量 - /constants
- 全局服务 - /services
- 全局工具 - /utils
- 全局接口 - /interfaces
- 全局钩子 - /hooks
- 全局主题 - /themes
- 全局验证 - /validations
- 全局样式 - /styles
- 全局翻译 - /locales
- 全局类型 - /types
- 全局三方库 - /libs
- 全局 Context - /contexts
- 全局模版 - /templates
- 全局包 - /packages
- Monorepo 的使用场景
- ...
- 业务性功能 - 放置到模块内部,并使用 _{moduleName} 声明。意义即:模块内部私有,不对外暴露,非本模块内部的外部最好不要使用
- 模块组件 -
_AuthRoleSelector.tsx
仅服务于当前业务的组件,不对外暴露,不对外使用 - 模块服务 -
_AuthRoleApi.service.ts
仅服务于当前业务的服务,可以是 API,也可以是其他功能定义 - 模块钩子 -
_AuthRole.hook.ts
仅服务于当前业务的钩子 - 模块 Context -
_AuthRole.context.ts
仅服务于当前业务的数据 - ...
- 模块组件 -
内部约定
- 所有的 table 的操作列,使用 fixed=“right” 固定到右侧,方便用户操作
- 所有的请求函数,使用 async/await + try catch finally 格式,为了更好的维护体验
- 在 try 中的 await 之后判断 code 状态码是否正常
- 在 finally 中处理 loading = false
- 在 catch 中处理 error (通常拦截器会统一处理异常,但某些特例 code 的情况下需要手动处理)
内部 SDK
- 日期操作:使用 dayjs,其它功能扩展再通过插件引入即可。
- 工具方法:使用 lodash-es,支持 ESM 的 Tree-shaking 和无需配置按需加载。
表单校验:使用 zod, 围绕尽可能友好的开发体验而设计。
命名规范
命名方式 | 小驼峰 | 大驼峰 | 短横线 | 下划线 | 全大写 |
---|---|---|---|---|---|
命名缩写 | camelCase | PascalCase | kebab-case | snake_case | UPPER_CASE |
约定写法 | 单词之间用大写字母分隔,第一个单词的首字母小写。 | 单词之间用大写字母分隔,每个单词的首字母都大写。 | 单词之间用短横线 - 分隔,所有字母小写。 | 单词之间用下划线 _ 分隔,所有字母小写。 | 单词之间用下划线 _ 分隔,所有字母大写。 |
常用场景 | 变量名 | 类名、接口名、构造函数、组件名 | URL、文件名、WebComponent | 变量、函数、数据库字段 | 常量、枚举 |
示例 1 | firstName | Person | page-title | first_name | MAX_SIZE |
示例 2 | loginButton | LoginForm | user-avatar | user_profile | ERROR_CODE |
示例 3 | userProfile | IPlugin | test-demo | main | HTTP_STATUS |
规范哲学
- 不修改的字段即常量使用
UPPER_CASE
- 不常修改的字段即变量使用
PascalCase
,即不是常量,也不是变量,介于两者之间 - 经常修改的字段即变量使用
camelCase
- 数据库或接口字段使用
snake_case
目录命名
参见上文中的 内部规则 - 全局性功能 - 全局文件目录:
-
config - 配置文件集合目录,使用
PascalCase
命名- global.config.ts - 全局配置文件
- app.config.ts - 应用配置文件
- database.config.ts - 数据库配置文件
-
constants - 放置常量、枚举,使用
PascalCase
命名-
配置常量:SystemConfig.const.ts
import packageJson from '../../package.json' export const APP_NAME = packageJson.name export const APP_VERSION = packageJson.version
-
枚举常量:UserStatusEnum.const.js - 用户状态
-
枚举定义:是一组值的集合。类似于 Python 中的 Dictionary + Tuple,具名 Key + 不可变 Value.
-
枚举名称:使用
PascalCase
,枚举字段同使用PascalCase
声明。比如说 TypeScript 中的枚举定义:/** * 用户状态 枚举定义 */ export const UserStatusEnum = { /** 0 - 正常 */ Normal: 0, /** 1 - 禁用 */ Banned: 1 }
enum Color { Red, Green, Yellow }
enum UserStatusEnum { Normal = 0, Banned = 1 }
-
-
枚举对象:是对枚举定义的补充,是对枚举值的意义的解释的补充,比如:
/** * 用户状态 枚举对象 */ export const UserStatus = { Normal: { label: '正常', value: UserStatusEnum.Normal, }, Banned: { label: '禁用', value: UserStatusEnum.Banned, }, }
-
在代码中获取对应枚举值的
label
或value
用于if
或swith
等相关的条件判断 -
在代码中通过编写
UserStatus.Normal.value
这样的代码提升代码的可读性和可维护性if (response.userStatus === UserStatus.Normal.value) {}
-
-
枚举列表,很多业态需要遍历枚举对象的值。比如:Select, Checkbox, RadioGroup:
const UserStatusList = Object.values(UserStatus)
-
-
-
services - 放置公共服务,使用
PascalCase
,通常暴露出来一个 Service class,用以被实例化或上下文注入。用例及场景:- 作为一个职责单一的纯函数方法被其他逻辑方法依赖使用,便于代码细粒度的拆分和组合。
- UserApi.service.js - 用户相关 API 接口的服务,其他任意模块都可以通过调用
UserApi.fetchUserById(userId: number)
获取用户信息数据。
- UserApi.service.js - 用户相关 API 接口的服务,其他任意模块都可以通过调用
- 作为一个职责单一的高内聚的功能实现被其他模块耦合,其他模块依赖该 Service 的抽象,便于代码的依赖。
- Plugin.service.js - 插件可以有各种各样的实现,但它们必须实现
IPlugin.interface.ts
定义的基础接口。 - CacheStorage.service.ts - 支持特定缓存时间后过期的 storage 的服务,暴露出去一个实例。
- MemoryStorage.service.ts - 运行时内存 storage 服务,暴露出去一个实例。
- ClientStorage.service.ts - 客户端本地存储的 storage 服务,暴露出去两个实例。
-
clientLocalStorage
export const clientLocalStorage = new ClientStorage( typeof localStorage !== 'undefined' ? localStorage : ({} as any as Storage) )
-
clientSessionStorage
export const clientSessionStorage = new ClientStorage( typeof sessionStorage !== 'undefined' ? sessionStorage : ({} as any as Storage) )
-
- Plugin.service.js - 插件可以有各种各样的实现,但它们必须实现
- 作为一个职责单一的纯函数方法被其他逻辑方法依赖使用,便于代码细粒度的拆分和组合。
-
components - 公共组件,用于放置全局使用的公共组件。使用
PascalCase
命名- CSR/SSR/SSG/ISR:
- CashConverter/index.ts - 金钱转换组件
- CoinConverter/index.ts - 金币转换组件
- Next.js RSC:
- layouts/DefaultLayout.server.ts - 服务端组件
- sidebar/SidebarMenu.client.ts - 客户端组件
- CSR/SSR/SSG/ISR:
-
utils - 公共工具方法,使用
camelCase
命名- formatter.util.ts - 格式化工具
- logger.util.ts - 日志打印工具
- converter.util.ts - 转换工具库
-
themes - 全局主题,使用
camelCase
命名- light.theme.ts - 白天主题
- dark.theme.ts - 黑夜主题
- gold.theme.ts - 土豪金?!
-
interfaces - 定义公共接口 interface,设计为先。在编写代码之前,先声明 interface 定义 API 接口,是很好的习惯。PS:常规约定是在接口声明前使用 I 单词开头,如下:
- IStorage.interface.ts - 储存接口定义,对应不同的实现是:
class SessionStorageImpl implements IStorage {}
,ClientStorage 的 sessionStorage 实现class MemoryStorageImpl implements IStorage {}
,MemoryStorage 的内存版本实现
- IPlugin.interface.ts - 插件接口定义,对应实现同上
- IStorage.interface.ts - 储存接口定义,对应不同的实现是:
-
hooks - 公共钩子 hook 集合目录,使用
camelCase
命名- install.hook.ts - 安装相关 hook
- pwa.hook.ts - PWAs 相关 hook
-
contexts - React context 集合目录,使用
PascalCase
命名- User.context.ts - 用于存放用户相关的数据,使用 Provider 提供给所有的嵌套子组件消费 Consumer。 (生产消费模式)
- Config.context.ts - 用于存放系统配置相关的数据。
-
locales - 语言集合目录,不同的国际化对目录或文件对命名要求不一致,取决于上下文。通常是
locales
或者langs
- zh.{ts|js|json} - 中文
- zh-CN:大陆简体中文
- zh-HK:香港繁体中文
- zh-TW:台湾繁体中文
- vi - 越南语
- id - 印尼语
- br - 巴西语
- mx - 墨西哥语
- th - 泰语
- zh.{ts|js|json} - 中文
-
*.setup.ts - 启动或实例化配置的文件,使用
camelCase
命名- db.setup.ts - 读取配置并启动 DB 的代码文件
- router.setup.ts - 读取 router 配置并实例化 router 的代码文件
-
main.ts/index.ts - 程序入口文件
文件命名
对一个文件的描述出了父级文件夹的命名作为功能集合之外,还能通过扩展后缀名。这最初是 Java 社区的命名规范,后迁移到了 TypeScript 社区。
特别是在 Nest.js 之后开始大行其道。后缀名是对文件功能的补充说明,当编辑器打开了几个同名文件的时候可以补充说明当前文件对应的职责。如果没有后缀名,当在开发 User 模块时,打开几个 User.ts 文件将很令人困惑。比如:
user
├── models 用户模块对数据模型对定义和声明
│ └── model/User.interace.ts
│ └── model/User.dto.ts
│ └── model/User.dao.ts
├── User.controller.ts 用户模块的 Controller class 对 endpoints 定义和实现
├── User.service.ts 用户模块对 DB 的 CURD 模块的 Service 实现
└── ...
因此在定义文件名时,可以补充文件职责 featName:
-
文件名:{moduleName}.{featName}.{extension},视具体情况使用
PascalCase
命名或者camelCase
命名。- moduleName - 当前文件的主要功能或模块
- featName - 当前文件的功能/角色/定位
- extension - 文件扩展名
例子:
- UserStatusEnum.const.js
- formatter.util.ts
- Config.context.ts
-
组件名:使用
PascalCase
命名。Web component 和 Vue 推荐 kebab-case,React社区推荐 PascalCase/index.tsx。命名规则同 **Class 类名,**使用 noun 名词 + desc 描述。本质上,一个组件,就是一个能被实例化复用的一组 UI 和 Function 的集合,功能同 Class.- GooglePage.server.tsx - 对应 nextjs 中的服务端组件
- AppSupport.client.tsx - 对应 nextjs 中的客户端组件
- CashConverter/index.tsx - 推荐统一使用目录放置 index.tsx。不推荐 CashConverter.tsx 这种单文件组件,不利于扩展、新增文件和组件。
程序命名
- Class 类名:使用
PascalCase
命名,命名规范同变量名 noun 名词 + desc 描述。在早期的 ES 规范中,区分构造函数和普通函数就已经使用这样的命名规范。export class CacheStorage extends ClientStorage {}
- 类继承export default class StatisticsService {}
- 默认导出class AdjustGenerator {}
- Adjust 生成器 Class
- Interface 接口名:使用
PascalCase
命名,约定是大写字母 I + InterfaceName 用于让人一眼理解当前的用法是 interface, 而非 class.IContentBlockProps
- 最常见的 React 组件的 props interface 声明IStorageInterface
- Storage.interface.ts 储存接口的设计定义
- Method 方法名:使用
camelCase
命名,verb 动词 + noun 名词 + desc 描述,提升可读性,增强可维护性。- handleClickMenu - 处理点击菜单
- convertIPAddress - 转换 IP 地址
- getGoogleConfigByPkg - 通过 pkg 获取 Google 配置
- genServiceUrlByVisitorId - 通过 VisitorId 生成 service 地址
- Variable 变量名:使用
camelCase
命名,最常见的命名,通常格式 noun 名词 + desc 描述。变量名应该尽可能的体现它的功能/角色/意义。nodeEnv
- 名如其义options.map(**option** => **option**.value)
- 尽量使用有意义的命名,不要使用items.map(**x** => **x**.label)
这种命名,编译时工程会自动 Minification,不用节约变量命名的字符串长度物理空间。
- Constant 常量名:常量的编写,使用
UPPER_CASE
命名,使用 noun 名词 + desc 描述。- APP_VERSION - 应用版本
- SYSTEM_TITLE - 系统标题
- Enum 枚举名:因此枚举区别于常量,使用
PascalCase
命名。枚举跟常量类似,不同在于枚举会新增和删除 option。- RechargeType.enum.ts - 动态枚举
- UserStatusEnum.const.ts - 常量枚举
- Bool 布尔值名:使用
camelCase
命名,常见的命名格式即 isXXxx, shouldXXxx, hasXXxx 用于表示状态、行为、存在。- isInstalledPWA - 是否已安装过 PWA 应用,is + verb/ed (动词或动词过去式) + noun(名词)表示 noun 是否已执行 verb/ed 动作。
- isSupportsPWA - 是否支持 PWA 模式,is + verb + noun 表示是否 noun 是否存在 verb 状态 。
- shouldInstallPWA - 是否应该安装 PWA 应用, should + verb + noun 表示 noun 是否应该执行 verb 行为。
- hasCheckedStatus - 是否已检查过 status,has + verb/ed + noun 表示是否已经执行过 verb 行为。
模块规范
我们最常用的模块划分,即是按功能区分。前文中的目录和文件名都是,除此之外,前端开发中最常用的就是按逻辑区分。
- 功能:按照功能来划分模块,根据不同的功能将代码放入不同的模块中。这样可以提高代码的可读性和可维护性。
- 逻辑分层:根据项目的业务逻辑来划分模块,将相关的业务逻辑放在同一个模块中。这样可以更好地划分职责和降低模块之间的耦合度。
- 数据类型:根据数据类型来划分模块,将处理相同数据类型的代码放在同一个模块中。这样可以提高代码的一致性和可维护性。
- 可重用性:将可重用的代码提取出来作为独立的模块,以便在其他地方重用。这样可以提高代码的复用性,减少重复编码。
- 性能:根据性能考虑来划分模块,将对性能有影响的代码放在独立的模块中,以便进行优化和管理。
- 安全性:根据安全性考虑来划分模块,将涉及安全性的代码放在独立的模块中,以便进行安全性检查和管理。
- 测试:根据可测试性考虑来划分模块,将需要测试的代码放在独立的模块中,以便更好地进行单元测试和集成测试。
组件规范
组件分类
- 按功能区分
- 基础组件:基本的 UI 元素,例如:按钮 Button、输入框 Input、标签 Tag 等。
- 容器组件:包装和组织其他组件,例如:布局容器 Layout、导航栏 NavigationBar、卡片 Card 等。
- 展示组件:展示数据,例如:表格 Table、图表 Chart、列表 List 等。
- 表单组件:收集和验证用户输入,例如:表单输入框 Input、选择器 Picker、复选框 Checkbox 等。
- 导航组件:导航和导航跳转,例如:导航栏 Breadcrumb、菜单 Menu、分页 Pagination ****等。
- 弹出层组件:创建弹出层或对话框,例如:模态框 Modal、浮层抽屉 Drawer ****等。
- 图标组件:展示图标,例如:图标 Icon、图标库 Iconfont 等。
- 工具组件:一些常用的工具功能,例如:时间选择器 TimePicker、日期选择器 DatePicker 等。
- 按职责区分
- UI 组件
- UI 组件通常只关注展示和用户交互,并不涉及业务逻辑。
- UI 的展示和交互,例如按钮、输入框、标签、表格等。
- 逻辑组件
- 逻辑组件与 UI 组件进行交互,并提供数据和功能给 UI 组件使用。
- 负责处理业务逻辑和数据流动,例如数据获取、数据处理、数据展示等。
- 业务组件
- 数据获取/处理/展示:从后端接口获取的业务数据,对数据进行处理和转换,将数据传递给 UI 组件进行展示,提供一些可复用的数据展示组件。
- 业务逻辑处理:业务组件可以处理与业务逻辑相关的功能,例如功能验证、数据校验、数据提交、逻辑处理等。
- 事件处理:业务组件可以处理用户交互事件,例如点击事件、输入事件等,并根据事件触发相应的业务逻辑。
- UI 组件
组件拆分
在前端开发中,组件是构建用户界面的基本单元。当设计组件时,考虑从以下几个维度进行划分:
- 功能:按照功能来划分组件,将具有相似功能的元素组合成一个组件。提高代码的可重用性和可维护性。
- 单一职责原则:遵循单一职责原则,即一个组件应该只负责一种功能或展示一种数据。使组件更加灵活和易于维护。
- 拆分复杂度:将复杂的UI或功能拆分成多个简单的组件,以便更好地管理和维护。降低每个组件的复杂度,提高代码的可读性。
- 可复用性:将可复用的 UI 元素提取为独立的组件,以便在整个应用程序中重复使用。有助于减少重复编码和提高开发效率。
- UI/UX:根据用户界面和用户体验来划分组件,将具有相似 UI 和 UX 需求的元素放在同一个组件中。提高用户体验的一致性。
- 数据流:根据数据流来划分组件,将数据流相似的元素放在同一个组件中。有助于管理数据流和状态管理。
- 可测试性:将需要测试的功能放在独立的组件中,以便更好地进行单元测试和集成测试。提高代码的质量和稳定性。
通过从这些维度进行组件的划分,可以使代码更加模块化、可维护和可扩展。合理的组件划分降低代码的耦合度,提高代码的复用性和可测试性。
组件测试
使用 Storybook 管理工程内部公共组件,包括组件 DEMO,API 描述,自动生成基于组件 DEMO 的 API 接口文档,并可通过 Web GUI 直接运行自动化测试,或者通过 Vitest 编写组件测试用例运行测试即可。
- Why Storybook?
- 📝 Develop UIs that are more durable
- ✅ Test UIs with less effort and no flakes
- 📚 Document UI for your team to reuse
- 📤 Share how the UI actually works
- 🚦Automate UI workflows
代码规范
代码组织
- 组件化(见组件拆分)和模块化,做最小细粒度拆分,提高可读性和可维护性
- 文件名与模块功能相关联,以便更好地理解代码的作用
- 避免将太多的代码放在单个文件中,减少加载时间和内存占用
- 公共依赖提取,更利于查找、定位和维护
- 公共枚举
- 公共配置
- 动态导入,在应用初始化加载入口时做懒加载设计,减少加载入口的 Bundle 代码大小,优化用户体验
- locale 多语言代码:
- 根据系统语言,加载默认的语言包
- 用户 1. 点击切换后,2. 再动态导入 3. 导入成功后 4. 切换系统语言
- theme 主题代码,同上
- page 页面适配代码,初始化时加载了两套代码:
- Mobile 和 PC,通过判断只动态导入一套代码即可
- locale 多语言代码:
- 路由懒加载:入口文件由 lazy, import 加 Suspense 统一包装,全部懒加载/按需加载。
- PC 和 mobile 组件在 CSR 使用声明式组件渲染条件加载
- 在 SSR 使用 request 的 UA 拦截渲染。
- 工程配置:Bundler 使用 vitePluginImp 插件配置 antd/lodash 等流行库的按需加载
- vite-plugin-imp - A vite plugin for import library component style automatic.
- vite-plugin-chunk-split - 一个简单易用的 Vite 拆包插件
- Rollup output.manualchunks
拼写问题
- 安装 Code Spell Checker 插件,提示单词拼写错误
写法约定
-
可继承,但不可重写 JavaScript 原生类型的 Class,比如:String, Number, Boolean, Symbol 等
// bad class String {} // good class StrImpl extends String {}
-
异步操作:使用
try/catch
和async/await
,不要使用Promise
// bad function query() { return fetch().then().catch() } // good async function query() { try { const data = await fetch() } catch (error) { // code ... } }
-
魔法数字:使用枚举声明,不要使用具体的无法直接理解的无意义的值
// bad if (value === 0) {} else (value === 1) {} // good if (value === SelectEnum.PROVINCE) {} else if (value === SelectEnum.CITY) {}
-
代码语义化:抽象的值先声明具体词义
// bad new Date().getTime() - (30 * 24 * 3600 * 1000) // good const ONE_SECOND = 1000 const ONE_MINUTE = ONE_SECOND * 60 const ONE_HOUR = ONE_MINUTE * 60 const ONE_DAY = ONE_HOUR * 24 const ONE_MONTH = ONE_DAY * 30 new Date().getTime() - ONE_MONTH
-
代码健壮性:使用
??
可选操作符或者逻辑操作符&&
或||
兼容// bad const { code, data } = awai request() if (code === 200) { this.data = data.map(item => ({ ...item, xx: 'xx', })) } // good const { code, data } = awai request() if (code === 200) { this.data = data?.map(item => ({ ...(item ?? {}), xx: 'xx', })) ?? [] }
-
Early Return:提前返回,提升执行速度与效率
// bad if (type) { res = (num / str) } else { res = (num * str) } return res // good if(type) return num / str return num * str
格式规范
-
VSCode 推荐插件
- 安装 Trailing Spaces 插件 - Highlight trailing spaces and delete them in a flash! ,提示和自动修复尾空格
- 安装 ESLint, Prettier, EditorConfig, DotENV 等插件,自动格式化代码
- 安装 Better Comments, change-case, Auto Rename Tag, Auto Close Tag 等插件自动优化代码编写和格式化体验
-
Linter 自定义配置
-
空间感和呼吸感:增加声明之间的空行,让代码空间有呼吸感。了解更多见 ESLint padding-line-between-statements 规则:
{ "rules": { "padding-line-between-statements": [ "error", { "blankLine": "always", "prev": ["block", "block-like"], "next": "*" }, { "blankLine": "always", "prev": "import", "next": "*" }, { "blankLine": "any", "prev": "import", "next": "import" }, { "blankLine": "always", "prev": "*", "next": ["export", "return", "block", "block-like"] }, { "blankLine": "any", "prev": "export", "next": "export" }, { "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" }, { "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"] } ] }, }
-
-
Linter 工具默认配置
- 使用脚手架默认 lint 配置
- 参见项目的模版代码 eslint 规则
- 参考 Airbnb’s Style Guide commit lint 规则
其他规范
Vue.js 编写规范
规范和 lint 工具约定 props, components, computed, data, watcher, beforeMount, mounted 等 options api 的编写顺序。
React.js 编写规范
因为 hooks 在框架级的强约定(不能放入条件表达式、强制先后顺序),所以不用考虑声明等顺序问题。
- Props 中的函数,将往下传递的函数使用 useMemoizedFn 或者 useCallback 包裹,可优化渲染性能。
- Request 请求,使用 useRequest/useSWR/useQuery 等 react 流行的请求 hook,已实现更现代化的应用状态管理和更新,使代码更加结构化,数据更新更及时,用户体验和 DX 更好。
- Loading 状态使用最佳实践 Suspense
- Icon 图标使用 iconfont 统一管理
- 收益:
- 节省开发资源:iconfont 有十几年来业界积淀的大量图标,我们可以节省大量寻找图标、导入/导出图标到 SVG 再到项目的时间。
- 降低开发成本:antd 支持通过 iconfont url 一键生成图标组件,不用为图标引入 SVG 和编写单独代码、重新构建和部署。
- 降低维护成本:不需要对样式、大小做修改,增删图标不需要修改代码仓库,只需要修改配置的 CDN URL。
- 增加 DX / UX:增删图标后刷新依赖立即可用,将 ICON_CDN_URL 改成类似 apoll 这种支持运行时更改配置的工具库,可以做到无缝更新图标 UI。
- 零构建成本:无需为图标做构建,更新图标后 iconfont 一键生成新的 CDN URL 地址,更新到配置中心即可。
- 风险
- CDN 稳定性:iconfont 是阿里系服务,已稳定运行近 10 年。稳定性有保障,前司使用了 7 年未出过问题。我们的服务也部署在阿里云服务上,所以唯一的风险就是阿里云挂了 ?!
- 解决办法:将 CDN 在本地 OSS/CDN 备份一份作为容错。—— 2024-06-28 补充
- 收益:
测试规范
- 端到端测试(E2E Test):
- 端到端测试是从用户的角度出发,模拟真实的用户场景和业务流程,验证整个系统的端到端行为是否符合预期。
- 它测试的是整个应用程序的完整流程,包括UI交互、数据交互、第三方系统集成等。
- 端到端测试通常使用自动化测试工具(如Selenium、Cypress等)来模拟用户操作,并验证最终结果。
- 端到端测试的目的是确保整个应用程序在各个组件协同工作时,能够正常运行并满足业务需求。
- 端到端测试通常运行速度较慢,因为它需要启动整个应用程序,并模拟完整的用户场景。
- 单元测试(Unit Test):
- 单元测试是针对最小可测试单元(如函数、方法或模块)进行验证,确保它们的行为符合预期。
- 它测试的是独立的代码单元,与其他单元没有耦合关系。
- 单元测试通常使用测试框架(如Jest、Mocha等)来编写和运行测试用例。
- 单元测试的目的是确保每个代码单元的内部实现是正确的,并满足设计要求。
- 单元测试通常运行速度较快,因为它只关注单个代码单元,不需要启动整个应用程序。
应用规范
- 应用生命周期设计 Lifecycle Hooks
- 多语言 & 国际化 i18n languages
- 应用配置与枚举 Configurations and Enums
- 身份认证与权限 ****Auth & Permission
- 应用储存与缓存 Application Storage & Cache
- 表单字段与校验 From Field & Validator
- 常见副作用 Common side effects
- 资源处理 Resource processing
- 提效工具 Tools
- 工作流工具 CI/CD
应用生命周期设计 Lifecycle Hooks
- 应用初始化阶段 - beforeCreate/create:
- 应用初始化:在应用启动时进行一些全局的初始化设置,例如创建根组件、设置路由等。
- 模块加载:根据需要异步加载应用所需的模块、组件和资源。
- 应用路由导航:
- 路由导航守卫:在进行路由导航之前或之后执行一些操作,例如身份验证、权限检查等。
- 路由解析:解析路由参数、查询参数等,准备要渲染的组件所需的数据。
- 组件生命周期:
- 组件创建:在组件创建时执行一些初始化操作,例如设置初始状态、订阅事件等。
- 组件渲染:将组件的模板渲染到页面上,显示相应的视图。
- 组件更新:在组件状态或属性变化时,重新渲染组件。
- 组件销毁:在组件被销毁之前执行一些清理操作,例如取消订阅、清除定时器等。
- 数据管理:
- 数据获取:从后端或其他数据源获取数据,并进行相应的处理和转换。
- 数据更新:在数据发生变化时,通知相关的组件进行更新。
- 事件处理:
- 事件绑定:将事件处理函数与相应的 DOM 元素或组件进行绑定。
- 事件触发:在用户交互或其他触发条件下,触发相应的事件处理函数。
- 错误处理:
- 异常捕获:捕获应用中的异常或错误,并进行相应的处理和报告。
- 错误 SDK:Sentry
- 应用状态销毁 - beforeUnmount/unmount:
- 取消订阅和事件解绑:在应用销毁前,确保取消所有的订阅和解绑所有绑定的事件处理函数,以避免潜在的内存泄漏问题。
- 断开连接和清理资源:如果应用与后端服务建立了连接或使用了其他资源,应在销毁阶段断开连接并清理相应的资源,以释放占用的资源和确保正确的资源管理。
- 清理定时器和计时器:如果应用中使用了定时器或计时器,应在销毁阶段清理它们,以防止在应用销毁后继续运行并导致潜在的问题。
- 清理缓存和状态:根据应用的需求,可以在销毁阶段清除应用使用的缓存数据或状态,以确保下次启动应用时处于初始状态。
多语言 & 国际化 Multi-languages & Internationalization
国际化插件 i18n Plugins
请安装 lokalise.i18n-ally 插件,以实现更好的 i18n 国际化编写体验:
- 支持一键新增翻译
- 支持自动机器翻译
- 支持编辑器文案自动替换
- 其他实用功能
国际化主键 i18n Key
不同的国际化框架可能对于区别语言和地区的标识符使用不同的约定,为了保持代码共识的一致性,定义统一的 i18n key 规则如下:
- 语言代码:语言代码通常是由两个字母组成,表示语言的 ISO 639-1 代码。例如,
zh
表示中文,en
表示英语。 - 地区代码:地区代码通常是由两个字母组成,表示地区的 ISO 3166-1 代码。例如,
CN
表示中国,US
表示美国。 - 语言和地区的组合:使用语言代码和地区代码的组合来表示不同的语言和地区。例如,
zh-CN
表示中文(中国),zh-TW
表示中文(台湾),en-US
表示英语(美国),es-MX
表示西班牙语(墨西哥)。-
如果遇到重复声明,则引用同一份文件即可,比如一些适用繁体中文的地区:
import zh_CN from './zh-CN.locale.json' // 简体中文,适用地区:中国大陆 import zh_HK from './zh-HK.locale.json' // 繁体中文,适用地区:中国香港,中国台湾 const i18nMap: <I18nCode, Translations> = { 'zh_CN': zh_CN, 'zh-HK': zh_HK, 'zh-TW': zh_TW }
-
国际化声明 i18n Declaration
文件格式
i18n 有多种编写格式,不同的编程语言支持不同的格式。比如:.yaml, .json, .js, .ts, .properties, .xml 等。
- 在前端工程中,约定使用以下格式:
.json
>.ts
>.mjs
>.js
数据形态
约定使用 KeyPath 结构,而不是 KeyValue 结构。因为 KeyPath 允许更细粒度和模块化,更简洁,可读性更高,可维护性更好。很多 i18n 插件同时支持两种数据格式,但 KeyPath
更适合现代前端工程。
-
Nested/KeyPath:
KeyPath
是 key 对应命名空间关键字,可以无限衍生,value 对应文本内容。-
写法格式
{ "welcome": { "title": "Welcome to our website!" } }
{ "welcome": { "title": "欢迎访问我们的网站!" } }
-
用法示例
const { t } = useTranslation('welcome') const title = t("title") // 对应语言文案
-
-
Flat/KeyValue:KeyValue
是 key 的 value 对应的文本内容。-
写法格式
{ "message.common.code.askDecryptionAgain": "Do you want to decrypt again?" }
{ "message.common.code.askDecryptionAgain": "是否重新解密?" }
-
用法示例
const confirmMessage = i18n.t('message.common.code.askDecryptionAgain')
-
应用配置与枚举 Configurations and Enums
所有的异步资源,在设计接口时使用 async/await 异步 + Memory Cache 的模式。如果数据未请求过,则发起 Http 请求。若请求过,则从缓存中读取。
系统配置 System configurations
系统设置 ,是初始化系统时的前置依赖,不同的环境有不同的配置。通常的设计是后端架构师使用 Apollo 定义配置后通过接口 /api/system/config
返回给前端:
interface SystemConfig {
/* CDN 静态资源地址 */
CDN_URL: string
/* 资源上传地址 */
uploadURL: string
/* 资源下载地址 */
downloadURL: string
/* 图标下载地址 - 系统 icons,通常是维护的 iconfont url */
iconfontURL: string
/* API 请求前缀 */
API_BASE_URL: string
/* 当前版本 */
version: string | number
/* 其他系统三方依赖的配置 */
systemTitle: string
systemLogo: string
systemEnumAPI: string
/* 等等其他配置项 */
}
系统枚举 System enums
在开发业务系统时,会定义大量的枚举。枚举的定义,多数由后端同学在开发后端逻辑时定义。比如 Java 的 EnumClass
,TypeScript 的 Enum
对象等。这些枚举在我们的系统中被大量使用,而被循环渲染的过程中有多种业态,比如:
- select 下拉框
- radio 单选框
- checkbox 多选框
因此,保证枚举的及时性和一致性至关重要。枚举的一致性影响我们的开发体验,枚举的及时性影响我们的用户体验。常见的场景是:
- 后端维护后端枚举
- 前端维护前端枚举
- 共同枚举的管理 ⚠️
- 前端改了,跟后端接口返回不一致
- 后端改了,前端无感知
- 后端枚举更新了
- 前端必须 follow 更改
- 前端必须发版本
- 才能触达用户
因此枚举不适合也不应该在前端维护。枚举应该通过接口统一返回,此时可以根据业务的使用密度来划分枚举接口的密度。比如,在初始化业务系统时,被即时使用到枚举,可以在系统初始化时通过接口 /api/system/enums 返回:
interface Enum {
/* 枚举名 */
label: string
/* 枚举值 */
value: string | number
/* 摘自 @ant-design/pro-components */
status: 'Success' | 'Error' | 'Processing' | 'Warning' | 'Default';
}
// 推荐使用 键值对 的方式,方便前端做判断
type EnumMap = Record<string, Enum>
// 使用 List 的方式,方便前端做 for 循环渲染
type EnumList = Enum[]
使用 EnumMap 的好处在于,可以直接通过 EnumMap[key] 定位到具体的 option。举例 GET /api/system/enum/userStatus:
{
code: 0,
success: true,
data: {
UserStatus: {
Online: { label: '在线', value: 1, status: 'Success' },
Offline: { label: '离线', value: 0, status: 'Warning' },
Deleted: { label: '已删除', value: -1, status: 'Error' },
},
// other enums
}
}
在代码中判断很好使用:
// 如果用户状态为在线
if (row.userStatus === UserStatus.Online.value) {
// code here
}
当作为 select 筛选时:
const userStatusList = Object.values(UserStatus)
枚举的国际化 i18n for Enums
- 后端实现,后端在返回
/api/enum/*
时根据 request-headers 中的 accept-language 传回对应的翻译后的语言版本。 - 前端实现,不推荐这么做。前端实现,则带来了开发体验上的断层。
身份认证与权限 Permission & Auth
"Auth"(身份验证)和 "Permission"(权限)是在软件开发中常用的两个概念,它们在用户认证和授权方面起着不同的作用:
-
Auth(身份验证):
身份验证(Auth)是确认用户身份的过程。它用于验证用户是否是系统中的合法用户,并且具有适当的凭据来访问系统资源。身份验证通常涉及用户提供凭据(如用户名和密码)以验证其身份。认证成功后,系统会授予用户一个身份标识(如令牌或会话),以便在后续的请求中验证用户的身份。
身份验证的目的是确保只有经过验证的用户可以访问系统的受保护资源。它通常用于用户登录和访问控制的过程中。例如,用户在网站上登录时需要进行身份验证,以便系统可以验证其身份并授予相应的访问权限。
-
Permission(权限):
权限(Permission)是指授予用户或用户组对系统资源执行特定操作的权力。权限控制决定了哪些用户可以执行哪些操作,并限制了他们对资源的访问。权限通常与用户的角色、组织结构或其他属性相关联。
权限控制的目的是确保用户仅能访问其被授权的资源,并限制他们对系统中敏感或保密信息的访问。例如,一个系统管理员可能具有更高级的权限,可以执行敏感操作,而普通用户只能执行有限的操作,受到访问限制。
总结来说,身份验证(Auth)用于验证用户的身份,并为其颁发身份标识,而权限(Permission)用于控制用户对系统资源的访问和操作。身份验证确保用户是合法用户,而权限控制决定了用户可以做什么以及对哪些资源具有访问权限。
用户校验 Auth
-
用户登录 User login - 在系统初始化的生命周期中判断用户是否登录,并根据用户登录后的角色与权限执行后续逻辑。若未登录,则重定向到登录页面。
/login?redirect=prevPage
type Role = string | number type FunctionKey = string | number interface BaseEntity { createdTime: Date createdUser: User['id'] updatedTime: Date updatedUser: User['id'] }
import React, { useEffect, PropsWithChildren } from 'react' // user.context.ts interface User extends BaseEntity { id: string name: string /* 角色 */ roles: Role[] /* 权限点 */ functionKeys: FunctionKey[] } const UserContext = createContext<User>(null | null) const UserProvider: React.FC<PropsWithChildren> = ({ children }) => { const [user, setUser] = useState<User | null>(null); const history = useHistory(); useEffect(() => { const checkUserLogin = async () => { const isLoggedIn = checkIfUserIsLoggedIn(); // 自定义函数,用于判断用户是否登录 if (isLoggedIn) { const user = await getUserData(); // 自定义函数,用于获取用户数据 setUser(user); } else { const redirectUrl = window.location.href; // 获取当前页面 URL history.push(`/login?redirect=${encodeURIComponent(redirectUrl)}`); } }; checkUserLogin(); }, [history]); return <UserContext.Provider value={user}>{children}</UserContext.Provider>; }; export { UserContext, UserProvider };
鉴权 Permission
-
用户身份角色 User Roles - 通常的设计是,路由
Route['meta']
需要指定权限点,再判断用户的权限点中是否拥有该权限,如果拥有则放入有效路由表中,并动态装载到路由表中。interface Route { path: string name: string component: any children?: Route[] meta: { title: string roles: Role[] cache: boolean } }
- 路由菜单权限 Page Route Menu - 根据用户角色权限渲染指定路由,仅渲染有权限的路由,否则跳转 404。
const geneNewRoutes = (allRoutes: Route[]) => { const newRoutes = [] allRoutes.forEach((route) => { if (user.roles.includes(route.name)) { if (route?.children?.length) { route.children = geneNewRoutes(route.children) } newRoutes.push(route) } }) return newRoutes }
-
用户权限点 Function Keys - 判断用户的权限点是否拥有该权限,拥有则渲染,反之不渲染。
type FunctionKeys = string[]
- 按钮模块功能权限 Button/Module Feature - 根据用户权限点判断用户是否有某个指定权限,并根据条件渲染特定功能模块。
import React, { PropsWithChildren } from 'react' import UserContext from '@/contexts/user.context' interface IAccessProps { functionKeys: string[] fallback?: React.ReactNode } const Access: React.FC<PropsWithChildren<IAccessProps>> = ({ functionKeys = [], fallback = null, children }) => { const user = useContext(UserContext) const functions = user.functionKeys const accessible = functionKeys.every(key => functions.includes(key)) return accessible ? children : fallback }
应用缓存 Application Storage Cache
- Memory(内存)存储:
- 将数据存储在内存中,例如使用变量、数组或对象等数据结构。
- 当页面刷新或关闭时,内存中的数据会丢失。
- 对于临时数据或者在单个页面会话中传递数据的场景很有用。
- 不受存储空间限制,但是占用内存可能会影响性能。
- Session(会话)存储:
- 使用
sessionStorage
对象存储数据,数据存储在浏览器的会话期间。 - 当浏览器或标签页关闭时,存储的数据会丢失。
- 用于在页面会话期间存储临时数据,例如表单输入或页面设置。
- 存储空间一般在 5-10MB 之间,因浏览器而异。
- 使用
- Local(本地)存储:
- 使用
localStorage
对象存储数据,数据存储在浏览器的持久存储中。 - 即使浏览器关闭或刷新,存储的数据依然存在,除非用户清除浏览器缓存或主动删除。
- 用于存储持久性数据,例如用户设置、应用配置或缓存数据。
- 存储空间一般在 5-10MB 之间,因浏览器而异。
- 使用
- IndexedDB 存储:
- 客户端非关系型数据库技术,提供结构化数据存储和事务处理。
- 支持键值对和对象存储,可以处理复杂查询和大量数据。
- 适用于需要本地处理大量数据的 Web 应用程序。
- 存储空间受用户设备和浏览器设置限制,可达数百兆甚至更多。
- 是现代 Web 开发中推荐的客户端数据库技术。
- 其他储存
- CookieStorage - 略
- CacheStorage - 增加 expireTime 在读写时处理过期的数据
表单原子性 From & Field 与表单校验 Validator
表单组件
- Form - 表单控件,中后台 CRUD 高频控件。
- Field - 字段控件,其内置了各种数据类型的渲染。
- currency
- number
- text
- percent
- progress
- date
- datetime
- time
- 等常见类型,全部类型参见 ProField 的 ValueType 枚举。
验证库
- async-validator - 阿里前端开发的异步验证库,是 ant design 和 element ui 的验证库。
- joi - The most powerful data validation library for JS.
- zod - 围绕尽可能友好的开发体验而设计。
- …
常见副作用 Common side effects
事件监听
- Window 全局级事件 https://developer.mozilla.org/en-US/docs/Web/API/Window
- DOMLoaded
- load
- unload
- unhandledRejection
- BOM 事件
- Mouse event
- Keyboard event
- focus/blur/scroll
- ...
异步回调
- 事件回调
- 网络请求
- 定时器
- setTimeout
- setInterval
- process.nextTick => only for Nodejs
- 文件 I/O
- ...
资源处理 Resource processing
加载方式
- 动态资源
- XHR
-
请求功能:
- 类型适配 - adapter(适配器模式)
- 数据处理 - transformData(代理模式)
- 数据拦截 - interceptors(拦截器模式)
- 异常处理 - errorHandler
请求库对比:
- axios - 经典 XHR 请求框架,据说作者面试 Google 翻车后去了 Apple。
- useRquest - 阿里出品的 ahooks 库中的 request hook,同下面的 useSWR,但更简单好用?!能快速配合 react + ant design 生态。
- useSWR - React 社区很流行的一个 request library。
- react-query 相比swr feature更多,但是打包体积也更大。
-
- JSONP
- XHR
- 静态资源
- JS/CSS/HTML
- Image/Video/Audio
- ...
- 加载状态
- 加载中 - loading
- 加载完成 - success
- 加载失败 - failed
数据类型
- JSON
- XML
- Text
- HTML
- FormData
- Binary
- GraphQL
- ...
加载方式
- 同步加载
- 异步加载 - 懒加载/按需加载
提效工具 Tools
- VSCode Plugins
- Github Copilot
- Codeium
- VSCode Snippets
工作流工具 CI/CD
开发阶段:
- Stylelint for css
- Prettier for IDE
- Eslint for typescript
- Commitlint
- Changeset
- .editorconfig
部署阶段:
- Github Action
- Gitlab pipeline
- Jenkins web-hook
- Docker deploy
- Vercel