前端开发规范文档
规范文档
持续更新的前端开发规范文档,文档中描述了前端开发的各种规范。如果有更好的建议,欢迎提交。
目录
- 文档规范
- 约定规范
- 文案规范 (2025-04-18 新增)
- 命名规范
- 规范哲学
- 目录命名
- 文件命名
- 程序命名
- 路由命名(2025-10-27 新增)
- 图标规范
- 模块规范
- 组件规范
- 代码规范
- 格式规范
- GitOps 规范 (2025-04-03 更新)
- 测试规范
- 应用规范
- 应用生命周期设计 Lifecycle Hooks
- 多语言 & 国际化 Multi-languages & Internationalization
- 应用配置与枚举 Configurations and Enums
- 身份认证与权限 Auth & Permission
- 应用储存与缓存 Application Storage & Cache
- 表单字段与校验 From Field & Validator
- 常见副作用 Common side effects
- 资源处理 Resource processing
- 提效工具 Tools
- 工作流工具 CI/CD
- 页面视觉规范 (2025-03-18 新增)
- 其他规范
文档规范
如何编写一个好的文档,工程介绍 Readme 和技术文档 Docmentation.
- 工程介绍: How to write a good README file
- 技术文档: Documentation standards
约定规范
描述工程中的常见约定规范,包括:
项目结构
规范哲学
- 所有的功能和逻辑,只跟模块相关的,都应该放在该模块下,模块之间保持独立性,互相独立,互不关心,完全解耦。
- 不要把所有的相关功能行为都放进一个全局的文件维护,这是反模式的设计。所有的设计模式都是为了高内聚,低耦合。
例如:把所有业务模块的 api service 耦合到一个大的 allApi.js 是并不明智的做法,这样做变成了高内聚,高耦合。限制代码的可重用性,降低了代码的可维护性和可扩展性。
- 高耦合:意味着如果某个模块的 Service 发生变化,可能会影响到其他模块。这违背了模块化设计的原则,应该尽量减少模块之间的依赖关系。
- 维护成本:一个大的全局文件会变得非常庞大,难以管理和维护。每次需要添加或修改 Service 时,都需要修改这个文件,这可能会引发各种问题,比如冲突、漏洞等。
- 测试成本:将所有的 API Service 都放在一个文件中,会使得测试变得困难。每次需要测试某个 API Service 时,都需要加载整个文件,这可能会导致测试时间过长。
最佳实践
- 全局性功能 - 放置到全局文件目录,使用
@/{moduleName}.{featName}.{ts|js} 引入。意义即:全局公有,全局共有,对外暴露,全局使用
- 全局组件 - src/components
- 全局常量 - src/constants
- 全局服务 - src/services
- 全局工具 - src/utils
- 全局接口 - src/interfaces
- 全局钩子 - src/hooks
- 全局主题 - src/themes
- 全局验证 - src/validations
- 全局样式 - src/styles
- 全局翻译 - src/locales
- 全局类型 - src/types
- 全局三方库 - src/libs
- 全局 Context - src/contexts
- 全局模版 - src/templates
- 全局包 - src/packages
- Others ...
- 业务性功能 - 放置到模块内部,并使用
modules/{moduleName} 声明。意义即:模块内部私有,不对外暴露,非本模块内部的外部最好不要使用
- 现在使用约定式路由可以把
_ 前缀去掉,因为不再需要通过 _ 区分 public/private 的 page/component (2025 年 3 月 4 日更新)
- 模块组件 -
auth/components/AuthRoleSelector/index.tsx 仅服务于当前业务的组件,不对外暴露,不对外使用
- 模块服务 -
auth/services/AuthRoleApi.service.ts 仅服务于当前业务的服务,可以是 API,也可以是其他功能定义
- 模块钩子 -
auth/hooks/AuthRole.hook.ts 仅服务于当前业务的钩子
- 模块 Context -
auth/contexts/AuthRole.context.ts 仅服务于当前业务的数据
- ...
内部约定
- 所有的 table 的实体名,使用
fixed=”left” 固定到左侧,方便用户使用
- 所有的 table 的操作列,使用
fixed=“right” 固定到右侧,方便用户操作
- 所有的请求函数,使用
async/await + try catch finally 格式,为了更好的维护体验
- 在 try 中的 await 之后判断 code 状态码是否正常,使用
isRightSystemResponse
- 在 finally 中处理 loading = false
- 在 catch 中处理 error (通常拦截器会统一处理异常,但某些特例 code 的情况下需要手动处理)
内部 SDK
- 日期操作:使用 dayjs,其它功能扩展再通过插件引入即可。
- 工具方法:使用 lodash-es,支持 ESM 的 Tree-shaking 和无需配置按需加载。
- 表单校验:使用 zod, 围绕尽可能友好的开发体验而设计。
- 状态管理:使用 zustand,可能是 React 目前最好用的 State Management Library
文案规范
文案是产品与用户最直接的沟通方式,输出正确、一致的文案,是产品设计中不可或缺的一部分。 —— 摘自 SemiDesign 文案规范
文案与代码分离
-
使用外部资源文件:将文案存储在独立文件(如 JSON、YAML)或国际化库(如 i18next、Vue I18n)中,避免硬编码。
// en.json
{
"button": {
"submit": "Submit",
"cancel": "Cancel"
}
}
-
按模块/功能分类:通过层级结构组织文案(如 login.title, error.invalidEmail),提升可维护性。
文案术语词典
用户体验优化
-
清晰简洁:避免技术术语(如“404错误” → “页面未找到”)。
| 错误场景 |
标准文案模板 |
技术映射 |
| 网络异常 |
网络连接不可用,请检查后重试 |
network_error |
| 权限不足 |
当前账号无权访问此功能 |
permission_denied |
| 数据加载失败 |
信息获取失败,点击重新加载 |
data_fetch_failed |
| 表单校验错误 |
请完善标 * 的必填项 |
form_validation_failed |
| 会话过期 |
登录状态已过期,请重新登录 |
session_expired |
-
错误提示友好化:明确问题并提供解决方案(如“密码需至少8位,包含字母和数字”)。通用错误模板:
**{错误类型}**
{问题描述}
**解决方案**:
1. {操作步骤1}
2. {操作步骤2}
[需要帮助?联系支持团队](mailto:support@company.com)
-
统一语气与风格:根据产品定位选择正式或亲切的语气,并全局保持一致。
命名规范
| 命名方式 |
小驼峰 |
大驼峰 |
短横线 |
下划线 |
全大写 |
| 命名缩写 |
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) 获取用户信息数据。
-
作为一个封装了所有跟服务端通信的服务集合,暴露出去 Service 的命名空间,增加代码可读性和可维护性。且通常一个前端的 Service class 对应一个后端的 Controller class。
// 上方放置用户服务 UserService 的类型声明
export declare namespace UserService {
type CurrentUser = {
token: string;
user_info: {
avatar: string;
login_name: string;
real_name: string;
email: string;
work_no: string;
};
permissions: string[];
roles: string[];
};
type LoginResult = {
status?: string;
type?: string;
currentAuthority?: string;
};
type FakeCaptcha = {
code?: number;
status?: string;
};
type LoginParams = {
username?: string;
password?: string;
autoLogin?: boolean;
type?: string;
};
}
// 下方放置用户服务 UserService 的服务方法实现
export class UserService {
/**
* 获取当前的用户 POST /api/currentUser
* @see {@link https://fox-yapi.kuainiujinke.com/project/36/interface/api/739}
*/
async function currentUser() {
const res = await request.post<API.Response<UserService.CurrentUser>>(
'/api/fox-user/user/getUserInfo',
{},
);
return res.data.data;
}
/** 登录接口 POST /api/login/account */
async function login(data: UserService.LoginParams) {
const res = await request.post<API.Response<UserService.CurrentUser>>(
'/api/fox-user/user/login',
data,
);
return res.data;
}
}
前端编写的 Service 通常对应后端的接口文档链接地址,可以通过注释加上该地址以方便后续同学维护和查询接口信息,这一点长期来看将非常实用。能够减少大量的沟通成本,从而降低许多维护成本。(2025 年 5 月 12 日新增)
export class ResultStatisticsService {
/**
* 查询质检结果统计列表
*** @see {@link [接口地址](https://app.apifox.com/link/project/5957596/apis/api-287268287)}**
*
* @param params
* @returns
*/
static async getResultStatisticsList(params: ResultStatisticsParams) {
const res = await request.post<API.Page<ResultStatisticsItem>>(
`${API_PREFIX}/api/report/inspection-result`,
params,
);
return res.data;
}
}
-
作为一个职责单一的高内聚的功能实现被其他模块耦合,其他模块依赖该 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)
)
-
components - 公共组件,用于放置全局使用的公共组件。使用 PascalCase 命名
- CSR/SSR/SSG/ISR:
- CashConverter/index.ts - 金钱转换组件
- CoinConverter/index.ts - 金币转换组件
- Next.js RSC:
- layouts/DefaultLayout.server.tsx - 服务端组件
- sidebar/SidebarMenu.client.tsx - 客户端组件
-
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 - 插件接口定义,对应实现同上
- 当 interface 的命名中已经中带有特定意义则无需再通过
I 声明,否则显得冗余了(2025 年 3 月 4 日更新):
- 比如 React 组件 Props interface:
ButtonProps, AvatarProps, SelectProps
-
hooks - 公共钩子 hook 集合目录,使用 camelCase 命名。2025 年 5 月 15 日更新,使用 kebab-case 命名更符合当前 hook 主流的命名习惯。
- install.hook.ts - 安装相关 hook
- machine-status.hook.ts - 使用机器状态的 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 - 泰语
-
*.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 地址
searchEntityByParam - 通过 param 参数查询 noun 实体 —— 适用于根据某个参数查询数据的场景,通过清晰准确的函数名定义,使代码一目了然。可读性和可维护性明显优于 searchUser 这种写法。例如:
searchUserByName - 通过 name 查询用户
getGoogleConfigByPkg - 通过 pkg 获取 Google 配置
genServiceUrlByVisitorId - 通过 VisitorId 生成 service 地址
- Variable 变量名:使用
camelCase 命名,最常见的命名,通常格式 noun 名词 + desc 描述。变量名应该尽可能的体现它的功能/角色/意义。
nodeEnv - 名如其义
options.map(**option** => **option**.value) - 尽量使用有意义的命名,不要使用 items.map(**x** => **x**.label) 这种命名,编译时工程会自动 Minification,不用节约变量命名的字符串长度物理空间。
- 不要使用什么
form1, form2, parentNode2 这种命名,非常不专业!而且令人困惑。
- 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 动作。
- isSupportedPWA - 是否支持 PWA 模式,is + verb + noun 表示是否 noun 是否存在 verb 状态 。
- shouldInstallPWA - 是否应该安装 PWA 应用, should + verb + noun 表示 noun 是否应该执行 verb 行为。
- hasCheckedStatus - 是否已检查过 status,has + verb/ed + noun 表示是否已经执行过 verb 行为。
路由命名
对于路由中的多个单词,目前存在 2 种形式的路由定义:
- 一种是驼峰 CamelCase
- 一种是中划线 kebab-case。
我们约定一下,链接中都使用中划线,kebab-case。也就是说,在约定式路由中,对应的(路由)文件夹都用 kebab-case 命名,这也符合 Web Standard:
// bad case
https://fox.weidu.co/admin/tools/decode/batchEncrypt
// good case
https://fox.weidu.co/admin/strategy/flow/collect/record-list
图标规范
尽量保证系统内图标的一致性。包括风格一致性,大小一致性,色彩一致性。
推荐图标库
在统一了 Render 库为 React 的基础上配合统一的 UI 库 Ant Design 使用:
- 第一优先级:https://ant-design.antgroup.com/components/icon-cn
- 第二优先级:https://react-icons.github.io/react-icons/ or https://lucide.dev/ - 作为补充,能满足 90% 的场景了。(2024-04-16 更新)
- 第三优先级:https://iconfont.cn/ - 用来弥补就是剩下的 10% 的场景。
- 最后的选择:自定义 SVG。
IconFont
- 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 补充
模块规范
我们最常用的模块划分,即是按功能区分。前文中的目录和文件名都是,除此之外,前端开发中最常用的就是按逻辑区分。
- 功能:按照功能来划分模块,根据不同的功能将代码放入不同的模块中。这样可以提高代码的可读性和可维护性。
- 逻辑分层:根据项目的业务逻辑来划分模块,将相关的业务逻辑放在同一个模块中。这样可以更好地划分职责和降低模块之间的耦合度。
- 数据类型:根据数据类型来划分模块,将处理相同数据类型的代码放在同一个模块中。这样可以提高代码的一致性和可维护性。
- 可重用性:将可重用的代码提取出来作为独立的模块,以便在其他地方重用。这样可以提高代码的复用性,减少重复编码。
- 性能:根据性能考虑来划分模块,将对性能有影响的代码放在独立的模块中,以便进行优化和管理。
- 安全性:根据安全性考虑来划分模块,将涉及安全性的代码放在独立的模块中,以便进行安全性检查和管理。
- 测试:根据可测试性考虑来划分模块,将需要测试的代码放在独立的模块中,以便更好地进行单元测试和集成测试。
组件规范
组件分类
这里有一个非常简单的通俗易懂的方案,摘自 Dan 的说法就是 “聪明” 组件与 “愚蠢” 组件。顾名思义:
- 快速记忆(2025 年 5 月 12 日更新):
- 聪明组件:里面有 数据,副作用,功能等提供丰富能力的组件。
- 愚蠢组件:里面啥都没有,只能用来渲染 UI。
- 按功能区分
- 基础组件:基本的 UI 元素,例如:按钮 Button、输入框 Input、标签 Tag 等。
- 容器组件:包装和组织其他组件,例如:布局容器 Layout、导航栏 NavigationBar、卡片 Card 等。
- 展示组件:展示数据,例如:表格 Table、图表 Chart、列表 List 等。
- 表单组件:收集和验证用户输入,例如:表单输入框 Input、选择器 Picker、复选框 Checkbox 等。
- 导航组件:导航和导航跳转,例如:导航栏 Breadcrumb、菜单 Menu、分页 Pagination ****等。
- 弹出层组件:创建弹出层或对话框,例如:模态框 Modal、弹层 Popover、浮层抽屉 Drawer ****等。
- 图标组件:展示图标,例如:图标 Icon、图标库 Iconfont 等。
- 工具组件:一些常用的工具功能,例如:时间选择器 TimePicker、日期选择器 DatePicker 等。
- 按职责区分
- 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,通过判断只动态导入一套代码即可
- 路由懒加载:入口文件由 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 {}
-
能用枚举就用枚举,千万不要使用字面量。例如:如果以后 zh 改成了 zh-CN/zh-HK/zh-TW,就不得不重新修改代码了。这增加了维护成本,而且从可读性上来讲,枚举的可读性更强。
// bad
locale === 'zh' ? 'zh_CN' : 'en_US'
// good
import { LanguageEnum } from '@fox-ui/runtime';
locale === LanguageEnum.Chinese ? 'zh_CN' : 'en_US'
-
异步操作:使用 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: it 缩写不常见,且无实际据名意义
const menus = menuItems.map(it => preProcessMenuItem(it));
// good: 名词复数 -> 名词单数
const menus = menus.map(menu => menu.visible)
-
代码健壮性:使用 ?? 可选操作符或者逻辑操作符 && 或 || 兼容
// 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
-
多重判断:使用策略模式,而非多重 if 或者 三元表达式。(2025-04-01 新增)
// bad
{businessType === BusinessType.Collection
? t('strategy.addCollectionStrategy')
: businessType === BusinessType.Market
? t('strategy.addMarketingStrategy')
: businessType === BusinessType.CollectUnAssign
? t('strategy.addCollectUnAssignStrategy')
: t('strategy.addMarketUnAssignStrategy')}
// good
const getStrategyButtonText = useCallback(
(businessType: BusinessType) => {
const strategyTextMap: Record<BusinessType, string> = {
[BusinessType.Collection]: t('strategy.addCollectionStrategy'),
[BusinessType.Market]: t('strategy.addMarketingStrategy'),
[BusinessType.CollectUnAssign]: t(
'strategy.addCollectUnAssignStrategy',
),
[BusinessType.MarketUnAssign]: t('strategy.addMarketUnAssignStrategy'),
};
return strategyTextMap[businessType] || t('strategy.addStrategy');
},
[t],
);
-
禁止导入任何私有类型,Why? 以后依赖升级了这个声明被改了,那代码编译就过不了了。(2025-04-08 新增)
// bad
import type { DataNode } from 'antd/es/tree';
// good
import { type TreeDataNode } from 'antd'
格式规范
-
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 规则
GitOps 规范
因为最近上线总是出现各种版本不一致的问题,所以找了一圈 GitOps 的最佳实践。这是一个容易忽略的环节,却也是一个程序员生涯中高频出现的场景。我们每天都在使用 Git,而 Git 的使用规范,直接关系到了产品本身的稳定性。
以前我也不理解,为什么一个简单的合并要经过这么多步骤。现在我的理解是增加这些冗余过程的原因,是为了增加安全垫。越是牵一发动全身的事情,越应该谨慎。人性是懒惰的,不能依靠人性,而应该依靠规范和流程。
- Workflow 工作流
- Branches 分支管理
- Commits 提交规范
- Quality 质量管理
- Version 版本管理
- ChangeLog 修改日志
- CheckList 检查清单
- Publish 发布管理
WorkFlow 规范
在线预览流程图
gitGraph
commit id: "Initial commit"
commit tag: "v1.0.0"
branch test
commit id: "chore: CI/CD初始化"
checkout main
branch feat-login
commit id: "feat: 登录模块"
branch feat-login-mountain
commit id: "feat: 增加权限验证"
branch feat-login-echo
commit id: "feat: 增加自动跳转"
checkout feat-login
merge feat-login-mountain
merge feat-login-echo
checkout test
merge feat-login id: "MR!123" tag: "test-20230820"
checkout main
merge test id: "Release v1.1.0" tag: "v1.1.0"
checkout main
branch hotfix-401-mountain
commit id: "fix:紧急修复鉴权漏洞"
checkout main
merge hotfix-401-mountain tag: "v1.1.1"
checkout test
流程执行要点:
-
三环境四阶段:
- 开发环境(Feature 分支):开发者本地验证
- 测试环境(Test 分支):自动化流水线 + Tester 人工验收
- 预发环境(Release / Pre 候选):生产配置验证,If have
- 生产环境(Main / Master 分支):蓝绿发布
-
紧急通道设计:
# Hotfix 特批流程
git checkout -b hotfix-xxx-{name}-{YYYYMMDD} main
git commit -m "fix(#{taskID}): 紧急修复 XX 漏洞 [HOTFIX]"
git push origin hotfix-xxx--{name}-{YYYYMMDD} # 提交 MR - 需 Tech Lead 审批
Branch 规范
分支命名习惯
| Instance |
Branch |
Description, Instructions, Notes |
| Stable 稳定环境 |
master / main |
Accepts merges from Features and Hotfixes |
| Test 测试环境 |
test |
Accepts merges from Features and Hotfixes |
| Working Features/Issues |
|
|
| 开发环境 |
feature-{taskTitle}-{developerName}- |
Always branch off HEAD of Working |
| Hotfix 紧急修复 |
hotfix-* |
Always branch off Stable |
分支管理规则
| 分支类型 |
命名规则 |
保护规则 |
合并策略 |
| Main / Master 稳定环境分支 |
main |
|
|
master |
1. 禁止直接 push |
|
|
- MR 需要 CodeReview | 1. 仅接受来自 release 分支的合并
- 仅接收测试通过的 feature 分支的合并 |
| Release / Pre 预发布环境分支 | tag-YYYYMMDD
release/YYYYMMDD
pre | 1. 自动触发预发环境部署
- 禁止 force | 1. 接收 test 分支合并,保留7天
- 接收测试通过的 feature 分支的合并 |
| Test 测试环境分支 | test | 1. 禁止 force | 1. 给测试同学使用
- 直接收合入,通常不会合出 |
| Feature 开发分支 | feature-telesales-moutnain | 1. 每日未活跃分支提醒? | 1. 开发完成需发起 MR 到 test 分支,给测试同学提测
- 验证后提交 MR 走 Code Review 流程
- 合并后到 master 后删除分支
- 走正常发布流程 |
| Hotfix 紧急修复 | hotfix-{JIRA-ID}-woods-{YYYYMMDD}
hotfix-{TASK-NAME}-olf-{YYYYMMDD} | 1. 只能从 master 上 checkout | 1. 修复后直接合并到 test 验证
- 验证后直接合并到 master
- 走紧急发布流程 |
分支清理策略
# .gitlab-ci.yml 分支清理任务
cleanup_branches:
stage: cleanup
script: |
# 删除 7 天前的 feature 分支
git branch -r --merged | grep 'origin/feature-' |
while read branch; do
if [ $(git log -1 --since='7 days' $branch | wc -l) -eq 0 ]; then
git push origin --delete ${branch#origin/}
fi
done
rules:
- if: $CI_PIPELINE_SOURCE == "schedule" # 每日凌晨执行
Commit 规范
- 目前走的自动化流程,使用的
@commitlint 工具。Why?见如下文章:
- https://www.ruanyifeng.com/blog/2016/01/commit_message_change_log.html
- https://commitlint.js.org/concepts/commit-conventions.html - 目前我们已使用该自动化工具管理
ChangeLog 规范
https://changesets-docs.vercel.app/en - 自动化工具,目前只有 KN 官网使用了自动化管理 ChangeLogs
https://github.com/changesets/changesets
# 安装配置
npm install @changesets/cli -D
npx changeset init
# 添加变更说明
npx changeset # 交互式生成变更文件
# 发布时自动生成日志
npx changeset version
npx changeset publish
Quality 规范
-
Code 代码质量
| 关卡 |
检查项 |
阈值 |
工具链 |
| 提交前 |
1. Biome - FOX |
|
|
- ESLint/Prettier - OA | 0 error | husky + lint-staged |
| MR 合并前 | 1. Tech Lead Review
- SonarQube 扫描
- AI Code Review | 1. Review Comments
- A级(0 blocker)
- Automation flow | 1. Follow 前端开发规范文档
- GitLab SAST模板 |
| 发布前 | 依赖漏洞扫描 | 无高危漏洞 | Trivy + Dependency-Check |
-
UI / UE 质量
- Developer 验收
- Leader 验收
- Product Manager 验收
- Director 验收
-
标准库 —— 测试覆盖率标准
# vitest.config.js
module.exports = {
coverageThreshold: {
global: {
branches: 80,
functions: 85,
lines: 90,
statements: 90
}
}
};
-
安全红线,绝对禁止项
- 硬编码密钥(检测到立即阻断流水线)-
.npmrc
- 高危依赖(CVE 评分 ≥ 9.0)
- 未授权 API(/admin 等接口未鉴权)
Version 版本管理
- Fox 目前的版本管理使用的是 Tag: V
- OA 目前的版本管理使用的是? 暂无
- KN 官网 目前使用的版本管理是 Tag v1.0.0
CheckList 检查清单
-
自动化检查项(CI/CD 内嵌)- 这是个理想的方案,但目前还不具足,需要依赖上下游共同推动
# .gitlab/checklist.yaml
checks:
- id: api_schema_validation
desc: "OpenAPI 契约校验通过"
script: npx swagger-cli validate api-spec.yaml
- id: i18n_coverage
desc: "国际化文案覆盖率 ≥ 95%"
script: npm run check-i18n -- --min=95
- id: error_boundary
desc: "所有路由组件已添加错误边界 / Loading 效果"
script: grep -rL 'withErrorBoundary' src/routes/ | xargs -I{} echo "缺失错误边界: {}" && exit 1
-
人工检查项 -(从 feature 合并到 master 的 MR 模板)
- Default https://git.kuainiujinke.com/loan_after/v2/fox-admin-ui/-/blob/master/.gitlab/merge_request_templates/Default.md
- Test https://git.kuainiujinke.com/loan_after/v2/fox-admin-ui/-/blob/master/.gitlab/merge_request_templates/Test.md
Publish 发布规范
- 每次合并代码 / 发版本 应该有 责任人,轮班制,下班前需要合完
- 经过前面流程后打上线日期的 TAG,目前 Pipline 会自己打
- Gitlab 监听 Tag 变动,如果有新增,执行应用打包 - 会加上 CI / CD 自动化
其他规范
- React.js 编写规范
- Vue.js 编写规范
React.js 编写规范
State
摘自 [React 最佳实践:每位开发者都应该了解的技巧](2025-06-13 更新)
在使用 useState 之前,考虑以下几点:
- 这个值是否可以在渲染时简单派生?
- 有库已经管理这个状态了吗?
- 这个值是否需要触发重渲染?
如果这些问题的答案都是“否”,那么你可能不需要 useState。
从已有状态中派生值
- 派生值不需要存储在状态中。如果你的数据可以从已有状态或属性中计算出来,直接在渲染时计算它们。
const formattedDate = new Date(date).toLocaleDateString();
以上代码在无需存储的情况下从给定的 date 输入派生出格式化的日期字符串。这种方法避免了不必要的状态管理,简化了渲染逻辑,并保持组件高效。
不要用 useEffect 来计算值
- 停止使用 useEffect 进行简单的计算!如果你的值可以直接从状态或属性计算出来且不涉及副作用,那就直接在渲染时计算。
- 对于高耗时计算,可以用 useMemo 优化性能:
const expensiveValue = useMemo(() => computeExpensiveValue(data), [data]);
这一做法将使用 useMemo 钩子在 data 变化时重新计算 expensiveValue,避免每次渲染时不必要的重新计算,提升了性能。
Hooks
因为 hooks 在框架级的强约定(不能放入条件表达式、强制先后顺序),所以不用考虑声明等顺序问题。
- Props 中的函数,将往下传递的函数使用 useMemoizedFn 或者 useCallback 包裹,可优化渲染性能。
- Request 请求,使用 useRequest/useSWR/useQuery 等 React 流行的请求 hook,已实现更现代化的应用状态管理和更新,使代码更加结构化,数据更新更及时,用户体验和 DX 更好。
- Loading 状态使用最佳实践 Suspense
2025-01-08 补充:尽量将独立的逻辑都分解抽离成独立的 React Hooks,参考代码 optimize: 使用 react-hooks 重构了 Calendar 状态,Why?
- 移动端页面的逻辑跟 PC 基本一致,且更简单
- 原来的写法需要写两份代码,抽成逻辑独立的 hooks 方便复用
Why React Hooks?:
- 抽离成 hooks 将逻辑 logic 跟视图 view 分离,便于组合
- 编写前端代码,组合优于继承。从 HTML 时代开始,标签 Markup 的设计就是组合。再到 MVC/MVVM 框架时代的组件,都是为了便于拆分和组合
PS: Hooks 对前端开发具有划时代的分水岭一般的意义。推荐所有能拆分的逻辑都拆成各个 hooks,方便复用,也更利于维护。
Vue.js 编写规范
由于我们现在并不适用 Vue,略。但大概是:
- lint 工具约定 props, components, computed, data, watcher, beforeMount, mounted 等 options api 的编写顺序。
- AI Code Review
测试规范
- 端到端测试(E2E Test):
- 端到端测试是从用户的角度出发,模拟真实的用户场景和业务流程,验证整个系统的端到端行为是否符合预期。
- 它测试的是整个应用程序的完整流程,包括UI交互、数据交互、第三方系统集成等。
- 端到端测试通常使用自动化测试工具(如Selenium、Cypress等)来模拟用户操作,并验证最终结果。
- 端到端测试的目的是确保整个应用程序在各个组件协同工作时,能够正常运行并满足业务需求。
- 端到端测试通常运行速度较慢,因为它需要启动整个应用程序,并模拟完整的用户场景。
- 单元测试(Unit Test):
- 单元测试是针对最小可测试单元(如函数、方法或模块)进行验证,确保它们的行为符合预期。
- 它测试的是独立的代码单元,与其他单元没有耦合关系。
- 单元测试通常使用测试框架(如Jest、Mocha等)来编写和运行测试用例。
- 单元测试的目的是确保每个代码单元的内部实现是正确的,并满足设计要求。
- 单元测试通常运行速度较快,因为它只关注单个代码单元,不需要启动整个应用程序。
- 集成测试(Integration Test)
应用规范
- 应用生命周期设计 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 or others
- 应用状态销毁 - beforeUnmount/unmount:
- 取消订阅和事件解绑:在应用销毁前,确保取消所有的订阅和解绑所有绑定的事件处理函数,以避免潜在的内存泄漏问题。
- 断开连接和清理资源:如果应用与后端服务建立了连接或使用了其他资源,应在销毁阶段断开连接并清理相应的资源,以释放占用的资源和确保正确的资源管理。
- 清理定时器和计时器:如果应用中使用了定时器或计时器,应在销毁阶段清理它们,以防止在应用销毁后继续运行并导致潜在的问题。
- 清理缓存和状态:根据应用的需求,可以在销毁阶段清除应用使用的缓存数据或状态,以确保下次启动应用时处于初始状态。
多语言 & 国际化 Multi-languages & Internationalization
- 国际化主键 i18n Plugins
- 国际化主键 i18n Key
- 国际化声明 i18n Declaration
- 国际化大小写约定
国际化插件 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 更适合现代前端工程。
国际化大小写约定
- 菜单 / 列头:每个单词的首字母大写(介词除外 at, on, for, with etc.)
- 操作 / 功能:仅首字母的单词大写,专有名词除外
同 ant-design pro 的做法保持一致。
应用配置与枚举 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 ? typeof children === 'function' ? children(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
- 静态资源
- JS/CSS/HTML
- Image/Video/Audio
- ...
- 加载状态
- 加载中 - loading
- 加载完成 - success
- 加载失败 - failed
数据类型
- JSON
- XML
- Text
- HTML
- FormData
- Binary
- GraphQL
- ...
加载方式
- VSCode Plugins
- 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
页面视觉规范
规范哲学
- 设计为目的服务 —— 完美不在于无以复加,而在于无可删减,万事莫不如此。—— 摘自 AntDesign
- 技术为产品服务 —— 发现、学习、整理和洞察市场的需求,再设计满足该需求的功能产品与业务流程。—— 摘自 AntDesign
- 产品为用户服务 —— 产品的增长依赖于用户的群体扩大和深度使用,而用户的成长又依赖于产品功能的完善。—— 摘自 AntDesign
- 好用才是核心 —— 如无必要,勿增实体,不分散用户注意力,让用户专注于任务达成,而非界面。通过情感化设计,适度安抚用户负面情感,强化用户正面情感。—— 摘自 AntDesign
设计价值观
参考 Ant-Design 设计价值观(2025-03-28 新增),默认 UI 的设计风格完全跟 Ant-Design Pro 靠齐,间距、表单、图表、列表都是。以保持设计语言与价值观的一致性与连贯性。(2025-03-26 新增)
- BTW,老板的意志 > 总监的意志 > 保持设计语言的一致性与连贯性 > 产品经理的意志 > 业务 leader 的意志
视觉效果
现代社会信息过载,所有展示在界面上的信息,我们需要保证它(2025-09-12 新增):
- 排列工整,可以看看《写给大家看的设计书》
- 有意义,无论是只读 read 还是有交互 action。如果这个信息没有意义,就删掉它
- 对于数据细节(比如单据号),默认带有具体的交互细节,比如能直接跳过去 or 可复制
视觉体验
在开发的时候,需要带入一些用户体验的换位思考。如果我们作为自己开发的系统页面的用户,会是怎样的感受 ?!(2025-09-12 新增)
OA 系统
OA 表格的展示规范,跟猫叔协商后,我们采取跟 Fox 一样的策略 “效率优先,美观其次”。规范用法请参考按照如下步骤(2025-10-13 新增):
- “重要信息” 不要使用 ellipsis 省略号,需要全部展示并自动换行。怎么定义“重要信息”的边界?比如第一列内容(除开序号),通常是最重要的内容。
- 表格宽度上,保证 column 列之间的 “视觉间距一致”,不要有 “大段留白的突兀感”。
- 因为信息的密度不同(比如说“中文名”和“英文名”的字符数量),但是它们通常都有一些共性
- 比如中文名通常 2 到 4 个字符串,英文名比较长,但是我们可以默认支持 8 个英文字符的换行,从而达到 “视觉上的宽度和间距一致性”。
- 针对内容宽度不固定无法取到折中值的场景,通过设置 column 列的 minWidth 和 maxWidth 属性让内容自适应宽度。
- ant-design Table 中默认不支持 max-width 属性,我们可以通过编写 hook 去包装所有 render,通过在 render 中渲染一个容器元素,并给容器元素设置 maxWidth 样式实现
- 且该 render 是支持 ant-design/pro-components 中的 valueType 属性的无需再手写各种 column 的 formatters,可以通过
render: (dom,entity,index, action, schema) => React.ReactNode 回调函数的第一个参数 dom 拿到。
- 默认使用如上展示策略,除开产品要求的特殊场景。
Fox 系统
- PageContainer 不用显示页面标题 title,因为 InnerTabs 站内多标签页已经展示了 title
- Card 不需要边框,默认全部使用无边框的 Card
- ProTable 的 FilterForm 的 label 宽度需要右对齐,即固定 width 而非 auto,且默认收起
- 要最小化的缩减用户的交互,比如:表单 form 默认 focus 第一项,支持键盘完成所有操作
- 新增的交互使用弹窗,而且默认选中第一个 input 输入框,目的是尽可能减少用户的操作(点击 or 筛选)
- 客户的电脑屏幕并不大,以及色彩跟字体都没有 Mac 精细,需要考虑空间性价比,以及 Windows 低分辨率下美观
大多数中台的页面都是如下几种类型:
- 组件结构(Components)
- 列表页(List)
- 表单页(Form)
- 详情页(Detail)
- 过渡效果 (Loading Effect)
组件结构
在目前系统中主要的组件结构如下(2025-04-16 新增):
- Page 页面组件,即 PageContainer 包裹的页面组件
- EntityTable 实体列表组件,即 ProTable 列表组件,渲染模型列表数据
- ModelForm 模型表单组件,即 Form 表单组件,编写模型字段的值
- ModelDescription 模型描述组件,即 Description 描述组件,描述实体字段意义
- EditPage 编辑页组件,即 PageContainer + ModelForm
- DetailPage 详情页组件,即 PageContainer + ModelDescription
Why?(2025-04-16 新增)
- 关注点分离:「Page 页面组件」 只负责布局结构,表格逻辑被封装到 「EntityTable 实体列表组件」 中
- 代码复用:「EntityTable 实体列表组件」 可在其他地方重用,「ModelForm」, 「ModelDescription」 同理
- 可维护性提高:更改组件逻辑时只需修改特定组件,不会影响其他组件
- 测试更容易:独立组件更易于单元测试
列表页
编写细节
-
在列表页或者详情页的渲染中,通常有数据转换的场景。比如最常见的日期范围选择,前端使用 DateRangePicker 渲染选择后的值是一个数组 [start, end],但后端接受的是两个字段 startTime & endTime。(2025 年 5 月 12 日新增)
Before: 在 request 的函数里面处理,一大堆的 converter。
After: ant-design pro-components 包括其他 pro-table, pro-fields 中处理数据的格式化都有这个 transform API,不用再转来转去了。
{
title: t('Check.QualityResult.callTime'),
dataIndex: 'callTime',
key: 'callTime',
width: 160,
valueType: 'dateRange',
initialValue: [
dayjs()
.subtract(1, 'day')
.startOf('day')
.format('YYYY-MM-DD HH:mm:ss'),
dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'),
],
fieldProps: {
allowClear: false,
},
search: {
transform: values => {
const callTimeRange = convertDateRangeToStartAndEnd(values);
return {
startCallTime: callTimeRange[0],
endCallTime: callTimeRange[1],
};
},
},
order: 1,
tooltip: t('Check.ResultStatistics.callDateTooltip'),
render: (_, record) => record.callTime,
}
-
列表中的枚举如果在语义上带有明显的 “积极” 或 “消极” 意味,则需要添加状态 status 或者颜色 color 标识以提升用户体验,方便用户快速识别(2025-04-17 新增)
2025-06-06 新增
-
列表上 item 请使用 react-router 自带的 Link 渲染,以保持浏览器对 a 标签渲染的默认行为。
-
列表页的日期默认选择当天 today 的日期,且 FilterForm 中有 dateRange 选项时候,设置 DatePicker 的 clearable 为 false,禁止用户清空。(2025-03-28 新增,背景:Fox 的数据太大,不带日期的查询很消耗性能)
-
列表中如果存在日期 / 时间类文案字段这种具有固定长度的表格,设置一个直接可见数据全貌的款度,不要鼠标移上去再展示全部。鼠标移上去展示全部,适用于那种场景?(2025-06-06 新增)
- 这个数据本身就是人类不友好的,不可读的,不可以理解的,比如 ID,编号 等系统字段
- 人类可读的,友好的,都应该尽量显示全貌,避免多余的步骤
- 除了备注或者长文本那种场景,实在太多显示不了,又不重要的,可以显示省略号然后鼠标移上去展示

表单页
表单分成两种情况:
- ModalForm - 弹窗表单,这种直接使用 ProComponents 提供的组件
ModalForm > ModelForm 即可。
- ModelForm - 模型表单(即数据表单),通常被独立放在 1 个页面中存在。这种方式的区别是:
- 需要新开路由
/list/edit/:id? ,有 id 参数即是编辑,无 id 参数即是新增
- 需要解耦成独立组件
PageContainer > Card > ModelForm > ProForm,便于未来可能向 ModalForm 方式迁移
详情页
左右分栏页,记得吸顶或者吸底。
列表中 item 的详情页的 PageContainer 的 title 需要使用该 item 的业务标题,这是一个被忽视的细节,真实用户在使用的使用确实更在乎的是业务标题,而不是什么页面名。
过渡效果
不要写这种 loading... 感觉非常奇怪。 loading for what? Why needs loading indicator here? 突兀,奇怪,不自然。
Loading 的基本特征就是,尽可能保证过渡效果的“自然”。
- 如果组件本身自带尺寸 size(width * height),使用一些组件自带的 loading,比如 Card, List 的 loading 属性。
- 使用 Skeleton 骨架屏,或者Spin + spinning 把组件包裹起来也可以
- 明确弃用 ProCard 的 loading,因为太丑了。改为使用 ant-design 的 Card 组件的 loading。
Service 层编写规范
Service 层负责封装 API 调用和数据转换逻辑。本文档规范适用于 packages/components/src/services/ 下的协议服务实现。
基本结构
使用 Class 而非函数:所有 Service 必须使用 Class 声明,通过 constructor 接收配置。
export class UlinkPhoneService {
constructor(private readonly config: FoxChatConfig) {}
async fetchPhoneInfo(caseId: string | number): Promise<PhoneInfoResponse> {
// implementation
}
}
命名规范:
- Service 类名:
{Protocol}{Feature}Service 格式,如 UlinkPhoneService、CollectConversationService
- 文件名:与类名一致,如
phone.service.ts
文件位置:按协议/功能分目录存放
services/
├── ulink/
│ ├── conversation.service.ts
│ ├── template.service.ts
│ ├── message.service.ts
│ └── phone.service.ts
├── collect/
│ └── ...
└── shared/
└── ...
代码组织顺序
// 1. 类型导入(外部 -> 内部)
import type { FoxChatConfig } from '@/types/fox-chat.type';
import { toOptionalString, toRecord } from '@/utils/converter.util';
import { request, type API } from '@feoe/shared';
// 2. 常量定义
const API_PATHS = {
PHONE_INFO: '/im/sms/phone/info',
} as const;
// 3. Helper 函数(如有)
function isNotFoundError(error: unknown): boolean {
// ...
}
// 4. Service Class 定义
export class UlinkPhoneService {
constructor(private readonly config: FoxChatConfig) {}
// 公开方法
async fetchPhoneInfo(): Promise<PhoneInfoResponse> {}
// 私有方法
private getCaseId(): string | number | undefined {}
}
必需元素
1. API_PATHS 常量
所有 API 路径必须定义为常量,使用 as const 确保类型安全:
const API_PATHS = {
CONVERSATION_LIST: '/bifrost-hermod/chat/list',
CONVERSATION_UNREAD: '/bifrost-hermod/chat/unread',
SESSION_INFO: '/im/session/info',
} as const;
2. 错误处理
必须检查 businessEndpoint 是否配置:
async fetchPhoneInfo(caseId: string | number): Promise<PhoneInfoResponse> {
if (!this.config.businessEndpoint) {
throw new Error('businessEndpoint 未配置,无法调用号码信息接口');
}
const url = `${this.config.businessEndpoint}${API_PATHS.PHONE_INFO}`;
// ...
}
3. 响应验证
const response = await request.get<API.Response<PhoneInfoResponse>>(url, {
params: { caseId },
});
if (response.data.code !== 0) {
throw new Error(String(result.message ?? '获取号码信息失败'));
}
4. 类型安全
使用 typescript 声明实体 Entity 的接口和字段
注释规范
Class 注释:说明协议、端点、与同类 Service 的区别
/**
* Ulink 协议 - 号码信息服务
*
* 获取案件可下发模板短信的号码(注册手机号或联系手机号)
*
* @remarks
* 端点:GET /im/sms/phone/info?caseId={caseId}
*/
export class UlinkPhoneService {
// ...
}
公开方法注释:必须有 JSDoc 注释
/**
* 获取案件号码信息
*
* @param caseId - 案件 ID
* @returns 号码信息,包含注册手机号和联系手机号
*/
async fetchPhoneInfo(caseId: string | number): Promise<PhoneInfoResponse> {
// ...
}
导出规范
在对应的 index.ts 中:
// 导出 Service 类
export { UlinkConversationService } from './conversation.service';
export { UlinkTemplateService } from './template.service';
export { UlinkMessageService } from './message.service';
export { UlinkPhoneService } from './phone.service';
// 导出公共类型(不导出内部辅助类型)
export type {
CreateConversationByCaseParams,
FoxChatConversation,
FoxChatConversationCreateParams,
FoxChatConversationIdentity,
FoxChatConversationInfo,
FoxChatConversationListParams,
FoxChatConversationMetadata,
FoxChatConversationQueryParams,
FoxChatSupportedChannelSession,
SupportedChannelSession,
} from './conversation.service';
export type { PhoneInfo, PhoneInfoResponse } from './phone.service';
参考示例
参考以下文件实现:
packages/components/src/services/ulink/template.service.ts
packages/components/src/services/ulink/conversation.service.ts
packages/components/src/services/ulink/phone.service.ts
与应用层 Service 的区别
| 类型 |
位置 |
用途 |
示例 |
| 协议 Service |
packages/components/src/services/ |
封装协议通信,跨应用复用 |
UlinkPhoneService |
| 应用 Service |
apps/{name}/services/ |
应用特定业务逻辑 |
UserService |
协议 Service 应保持协议无关性,不包含特定业务的逻辑。
来源:https://www.cnblogs.com/givingwu/p/18293975 |