前端架构设计
一、架构分层设计
1.1 分层总览
现代前端应用架构通常采用分层设计思想,将关注点分离,使每一层职责单一、可独立演化。一个典型的前端应用可划分为以下四个层级:
| 层级 | 职责范围 | 核心技术 |
|---|---|---|
| 展示层(View Layer) | UI 渲染、组件编排、动画交互 | Vue/React 组件、CSS 框架 |
| 业务层(Business Layer) | 状态管理、业务逻辑、权限控制 | Pinia/Redux、组合式 API |
| 数据层(Data Layer) | API 请求、缓存、数据转换 | Axios/React Query、数据仓库 |
| 基础设施层(Infrastructure Layer) | 路由、国际化、主题、日志 | Vue Router/React Router、i18n |
1.2 各层职责详解
展示层是整个架构的最上层,负责将数据以可视化的方式呈现给用户。它由一组原子组件、分子组件和页面组件组成。展示层不应当包含任何业务逻辑,组件内部只处理与 UI 相关的状态,如展开/收起、动画状态等。典型的展示层技术选型包括 Vue 的单文件组件(SFC)或 React 的函数组件加 Hooks。
业务层位于展示层之下,封装了应用的业务规则和流程编排。在小型应用中,业务逻辑可以直接放在组件内;但在中大型项目中,需要将业务逻辑抽取到独立的业务模块中。例如用户注册流程涉及的字段校验、密码加密、调用注册接口、处理返回结果、跳转页面等一系列操作,应当封装在业务服务中而非散落在组件里。
数据层负责与后端 API 交互,包括 HTTP 请求的发送与响应处理、请求重试、缓存管理、数据预取等。数据层不关心业务语义,只处理数据的获取与持久化。现代前端应用中,React Query(TanStack Query)和 Vue Query 已经成为服务端状态管理的标准方案,它们提供了声明式的请求管理、自动缓存失效和后台刷新能力。
基础设施层是横切关注点的集合,包括路由管理、国际化(i18n)、主题切换、错误边界、日志上报等。这些功能贯穿应用的各个层面,为其他层级提供基础能力支撑。
1.3 层间通信
各层之间的通信遵循严格的单向依赖规则:
- 展示层依赖于业务层:组件调用业务服务获取数据或执行操作
- 业务层依赖于数据层:业务服务调用数据层接口获取或持久化数据
- 数据层依赖于基础设施层:HTTP 客户端依赖路由拦截器、Token 管理等
用户交互
↓
展示层 (View) ──→ 业务层 (Business) ──→ 数据层 (Data) ──→ 后端 API
↑ ↑ ↑
└──── 事件/回调 ──────┴──── 订阅/通知 ───────┘
基础设施层 (Infrastructure)
├── 路由管理
├── 国际化
├── 主题系统
└── 日志/监控1.4 依赖方向
依赖方向遵循从稳定到不稳定的原则。基础设施层是最稳定的,几乎不依赖其他业务模块;展示层是最不稳定的,因为 UI 变化最频繁。具体规则如下:
- 上层可以依赖下层,下层不能依赖上层
- 同层之间通过事件总线或依赖注入进行解耦
- 基础设施层的服务通过抽象接口注入到业务层和数据层
这种依赖规则确保了当 UI 发生变化时,不会影响到业务逻辑层;当业务规则变化时,不会影响到数据持久化层。
二、权限系统
2.1 RBAC 在前端的实现
基于角色的访问控制(RBAC,Role-Based Access Control)是最常用的权限模型。在前端实现中,通常包含三个核心要素:
- 用户(User):系统的使用者,可以被分配一个或多个角色
- 角色(Role):权限的集合,如管理员、编辑、访客
- 权限(Permission):具体操作的许可标识,如
post:create、user:delete
前端权限控制的粒度可分为三级:路由权限、按钮权限和数据权限。
2.2 路由权限
路由权限控制用户能否访问某个页面。实现方式通常是在路由守卫(Navigation Guard)中检查当前用户的角色或权限标识。
通用的实现方案是在路由元信息(Route Meta)中声明需要的角色或权限,然后在全局路由守卫中进行匹配校验:
路由定义时声明 roles 或 permissions 字段
↓
用户登录后获取角色/权限列表并存储
↓
路由跳转时,全局守卫检查目标路由的权限要求
↓
有权限 → 正常跳转
无权限 → 重定向到 403 页或首页对于不同角色的菜单可见性,可以采用动态路由的方式:根据用户角色动态添加可访问的路由。管理员能看到"用户管理"和"系统设置"页面,而普通编辑只能看到"文章管理"相关页面。
2.3 按钮权限
按钮权限比路由权限更细粒度,控制页面上某个按钮或操作是否可见或可用。常用的实现方式包括:
- 自定义指令:Vue 中通过
v-permission指令,React 中通过高阶组件或自定义 Hook - 条件渲染:使用
v-if包裹,传入权限标识数组
<template>
<button v-permission="'article:delete'">删除</button>
</template>权限指令内部的实现逻辑是:从全局状态中取出当前用户的权限列表,检查传入的标识是否在列表中。如果不在,则从 DOM 中移除该元素。
2.4 数据权限
数据权限控制用户能看到哪些数据。例如,编辑只能看到自己创建的文章,而管理员可以看到全部文章。数据权限的实现通常需要前后端配合:前端在请求中携带用户身份信息,后端根据用户角色和部门进行数据过滤。前端需要做的是:
- 根据当前角色配置 API 请求的参数(如
?authorId=currentUser) - 在列表渲染时根据权限过滤某些敏感字段
- 在表单提交前验证用户是否有权修改目标数据
2.5 权限校验时机
权限校验应在以下几个关键时机执行:
- 登录成功后:拉取用户角色和权限列表,存储到全局状态和 localStorage
- 路由跳转前:全局路由守卫检查路由级权限
- 渲染组件时:自定义指令或条件渲染检查按钮级权限
- API 请求发起前:请求拦截器中检查是否有权调用该接口
- API 响应返回后:如果返回 403 状态码,触发权限刷新或登出
2.6 动态路由
动态路由是权限系统的核心机制之一。它与静态路由不同,不是在应用初始化时就固定下来,而是根据用户登录后的角色动态生成。
静态路由(所有用户都能访问):
/login, /403, /404
动态路由(根据角色动态添加):
admin → [/users, /roles, /settings, /logs]
editor → [/posts, /categories, /comments]
viewer → [/dashboard]在 Vue Router 中可以使用 router.addRoute() 方法在运行时添加路由。React Router v6 也可以通过嵌套路由结合条件渲染实现类似效果。
三、国际化方案
3.1 i18n 库选型
前端国际化通常使用专门的 i18n 库。社区主流方案对比:
| 特性 | vue-i18n | react-i18next | intl (FormatJS) |
|---|---|---|---|
| 框架绑定 | Vue 专用 | React 专用 | 框架无关 |
| 编译时优化 | 支持(v9+) | 通过插件支持 | 支持 |
| 惰性加载 | 支持 | 支持 | 支持 |
| ICU MessageFormat | 支持 | 支持 | 原生支持 |
| TypeScript 支持 | 良好 | 良好 | 优秀 |
选型建议:Vue 项目首选 vue-i18n,React 项目首选 react-i18next,如果你需要框架无关的解决方案,@formatjs/intl 是一个不错的选择。
3.2 翻译文件组织
翻译文件的组织方式直接影响项目的可维护性。推荐按功能模块划分翻译文件:
locales/
├── zh-CN/
│ ├── common.json # 通用文案(按钮、提示等)
│ ├── menu.json # 菜单翻译
│ ├── login.json # 登录页
│ ├── dashboard.json # 仪表盘
│ ├── posts.json # 文章管理
│ └── users.json # 用户管理
├── en-US/
│ ├── common.json
│ ├── menu.json
│ ├── login.json
│ ├── dashboard.json
│ ├── posts.json
│ └── users.json
└── index.ts # 统一导出这种按模块组织的结构有以下好处:
- 每个翻译文件体积小,易于维护
- 支持按需加载,只在访问某个模块时才加载对应的翻译
- 多人协作时可以按模块分配翻译任务,减少冲突
3.3 动态语言切换
动态切换语言而不刷新页面的实现要点:
- 语言状态管理:将当前语言存储在全局状态(Pinia/Redux)和持久化存储(localStorage)中
- 响应式更新:i18n 库将翻译函数暴露为响应式引用,切换语言时自动更新所有绑定点
- DOM 更新:所有使用了
$t()或t()的文本节点在语言切换时自动重新渲染 - 非文本内容:对于图片、日期时间等需要本地化的内容,通过响应式计算属性处理
3.4 与路由集成
国际化路由有两种常见模式:
前缀模式:URL 中包含语言代码
/zh-CN/dashboard→ 中文仪表盘/en-US/dashboard→ English Dashboard
子域名模式:不同语言使用不同子域名
zh.example.com/dashboarden.example.com/dashboard
前缀模式实现更简单,推荐大多数项目采用。实现方式是在路由配置中为每条路由添加语言前缀,或者在路由守卫中根据当前语言动态拼接路径。
3.5 SSR 国际化
在服务端渲染(SSR,Server-Side Rendering)场景下,国际化的实现需要额外注意:
- 根据请求头的
Accept-Language或 Cookie 确定初始语言 - 在服务端预渲染时将翻译注入到 HTML 中
- 客户端激活(Hydration)时,确保服务端和客户端的翻译一致,避免内容闪烁
Nuxt(Vue)和 Next.js(React)都提供了内置的国际化模块或插件,可以大大简化 SSR 国际化的实现。
3.6 回退策略
当某个语言的翻译缺失时,需要有一个合理的回退策略。推荐的做法是链式回退:
当前语言 → 父级语言 → 默认语言 → 显示 Key例如当前语言是 zh-CN(简体中文),查找顺序为:
- 尝试
zh-CN的翻译 - 未找到,尝试
zh(中文通用)的翻译 - 未找到,尝试
en(默认语言)的翻译 - 未找到,显示原始的 Key(如
menu.dashboard)
这种策略可以避免在翻译不完全时页面出现空白或乱码。
3.7 ICU MessageFormat
ICU MessageFormat 是 Unicode 联盟定义的国际化消息格式标准,支持复数、性别、选择等高级语法。大多数现代 i18n 库都支持此格式。
ICU 格式的核心语法包括:
- 选择(Select):根据变量值选择不同的输出
- 复数(Plural):根据数量选择单复数形式,可以定义
=0、=1、one、other等规则 - 数字格式化:数字的千分位、小数点、货币符号
- 日期格式化:日期格式根据地区自动适配
需要注意的是,ICU MessageFormat 中的 {变量} 语法在 VitePress 或 Vue 模板中可能会与模板插值语法冲突。在使用时,应当在代码块中使用 <code v-pre> 包裹,或者用 ::: v-pre 容器来避免冲突。
四、主题方案
4.1 CSS 变量
CSS 自定义属性(CSS Variables)是实现主题切换的基础技术。通过在 :root 中定义一组语义化的 CSS 变量,然后在组件中使用这些变量,可以在运行时通过修改变量的值来实现主题切换。
:root {
--color-primary: #1890ff;
--color-bg: #ffffff;
--color-text: #333333;
}
[data-theme="dark"] {
--color-primary: #69c0ff;
--color-bg: #141414;
--color-text: #e8e8e8;
}使用 CSS 变量相比 CSS-in-JS 方案有以下优势:
- 零运行时开销:CSS 变量的切换由浏览器原生完成,不需要 JavaScript 重新计算样式
- 不影响渲染性能:修改 CSS 变量不会触发重新布局(Reflow),只会触发重新绘制(Repaint)
- 易于调试:变量的变化可以在浏览器开发者工具中直接观察
- 与组件库兼容:大多数现代组件库(Ant Design、Element Plus)都支持通过 CSS 变量定制主题
4.2 暗色模式
暗色模式的实现不仅仅是反转颜色,而是一套完整的设计体系转换。关键考虑点包括:
- 对比度:暗色模式下文本与背景的对比度不应低于 4.5:1(WCAG AA 标准)
- 色相偏移:亮色模式下使用冷色调(蓝色系),暗色模式下可以使用暖色调或降低饱和度的版本
- 阴影调整:暗色模式下使用更柔和、透明度更低的阴影
- 图片适配:深色背景上的图片可能需要添加白色边框或降低不透明度
4.3 多主题切换
除亮色/暗色两种模式外,部分应用还需要支持多品牌主题或自定义主题。实现多主题切换的核心是主题 Token 体系:
- 定义一组设计 Token(Design Tokens),包括颜色、字体、间距、圆角等
- 为每个主题建立 Token 值映射表
- 在运行时切换到不同的 Token 映射表,重新设置 CSS 变量
主题切换的核心代码结构:
- 用户选择主题
- 从主题 Token 映射表中查找对应的 Token 值
- 将 Token 值批量设置到
document.documentElement的 CSS 变量上 - 所有使用这些 CSS 变量的组件自动更新
4.4 主题持久化
主题偏好需要在页面刷新后保持。常用的持久化方案包括:
- localStorage:存储用户选择的主题名称
- Cookie:适合 SSR 场景,服务端可以根据 Cookie 直接渲染对应的主题
- prefers-color-scheme:CSS 媒体查询,用于检测用户的系统主题偏好
推荐的优先级策略是:用户手动选择的主题 > 系统主题偏好 > 默认主题。这意味着:
- 如果用户曾经手动切换过主题,使用用户的选择
- 如果用户没有手动选择,根据操作系统的主题设置自动选择
- 如果系统无法检测,使用预定义的默认主题
4.5 与组件库集成
现代组件库(如 Ant Design V5、Element Plus)已经全面拥抱 CSS 变量。与组件库集成的关键步骤:
- 覆盖变量:通过在全局样式中覆盖组件库的 CSS 变量来定制主题
- 保持一致性:确保组件库的变量命名与项目自定义变量命名统一,避免重复定义
- 按需变量:如果只需要修改少量变量,只覆盖需要的部分,其余使用组件库的默认值
4.6 设计 Token
设计 Token 是主题系统的原子单元。一个完整的设计 Token 体系包含以下类别:
| 类别 | 示例 Token | 说明 |
|---|---|---|
| 颜色 | color-primary、color-success、color-warning | 品牌色、语义色 |
| 字体 | font-size-base、font-family、font-weight | 字体系统 |
| 间距 | spacing-xs、spacing-md、spacing-xl | 间距体系 |
| 圆角 | border-radius-sm、border-radius-lg | 圆角规格 |
| 阴影 | shadow-sm、shadow-lg | 阴影层级 |
| 动画 | transition-duration、easing | 过渡动画 |
五、状态管理架构
5.1 状态分层
前端应用中的状态可以按照生命周期和作用域分为四个层次:
全局状态(Global State):应用级状态,多个页面和组件共享。典型示例包括用户登录信息、主题偏好、语言选择。全局状态通常使用 Pinia(Vue)或 Zustand/Redux(React)管理。
局部状态(Local State):组件内部状态,生命周期与组件绑定。典型示例包括表单输入值、下拉菜单展开/收起、Tab 切换。局部状态使用 ref(Vue)或 useState(React)管理。
服务端状态(Server State):存储在服务端但在客户端缓存的数据。典型示例包括文章列表、用户详情、配置信息。服务端状态的最佳实践是使用 React Query 或 Vue Query 管理,它们提供了缓存、后台刷新、乐观更新等能力。
URL 状态(URL State):编码在 URL 中的状态,支持分享和浏览器前进后退。典型示例包括搜索关键词、分页页码、筛选条件。URL 状态通过路由库管理。
5.2 状态分层模型
┌─────────────────────────────────────────┐
│ 全局状态 (Global) │
│ 用户信息、主题、语言、权限 Pinia/Zustand │
├─────────────────────────────────────────┤
│ 服务端状态 (Server) │
│ 数据获取、缓存、失效 TanStack Query │
├─────────────────────────────────────────┤
│ URL 状态 (URL) │
│ 路由参数、查询字符串 Vue/React Router │
├─────────────────────────────────────────┤
│ 局部状态 (Local) │
│ 表单、UI 交互状态 ref/useState/v-bind │
└─────────────────────────────────────────┘5.3 层间通信规则
各层状态之间的通信需要遵循以下规则:
- 单向数据流:数据从外层流向内层,事件从内层流向外层
- 服务端状态不复制到全局状态:避免缓存不一致,始终使用 Query 库管理
- URL 状态作为单数据源:当页面刷新时,URL 中的状态可以恢复,不需要额外存储
- 局部状态不提升到全局状态:除非确实被多个不相关组件共享
5.4 数据流图
典型的前端数据流如下:
用户操作(按钮点击、表单提交)
↓
组件 dispatch Action / 调用 Mutation
↓
业务层处理逻辑(校验、转换)
↓
数据层发起 API 请求
↓
后端返回数据
↓
数据层更新缓存
↓
响应式系统通知组件更新
↓
UI 重新渲染5.5 状态管理的选型建议
- 小型项目(< 10 个页面):只需要 Vue 的
ref/reactive或 React 的useState/useReducer,配合 React Query 管理服务端状态 - 中型项目(10-30 个页面):引入轻量级状态管理库,Vue 生态选择 Pinia,React 生态选择 Zustand
- 大型项目(30+ 个页面,多团队协作):Vue 生态继续使用 Pinia,React 生态建议使用 Zustand 或 Redux Toolkit,并配合完善的架构规范和自动化工具
六、API 层设计
6.1 请求封装
API 请求层是整个数据层的核心入口。一个完善的请求封装应当提供以下能力:
- 统一的基础配置:baseURL、超时时间、默认请求头
- 请求拦截器:自动注入 Token、添加语言头、记录请求开始时间
- 响应拦截器:统一解析响应体、处理业务状态码、触发错误处理
- 类型安全:基于 TypeScript 泛型提供完整的请求和响应类型推断
请求拦截器:
1. 从状态管理获取 Token
2. 自动添加到 Authorization 头
3. 添加 Accept-Language 头
4. 记录请求开始时间(用于性能监控)
响应拦截器:
1. 检查 HTTP 状态码
2. 解析业务状态码
3. 成功 → 返回数据
4. 未授权(401) → 触发 Token 刷新或重定向到登录页
5. 无权限(403) → 触发权限提示
6. 服务端错误(500) → 上报错误并显示友好提示6.2 错误处理
前端的错误处理需要区分可预期错误和不可预期错误。
可预期错误是业务层面的错误,例如表单校验失败、密码不正确、资源不存在。这些错误应当在业务代码中捕获并处理,给出具体的用户提示。
不可预期错误是系统层面的错误,例如网络断开、服务端 500、JSON 解析失败。这些错误应当在全局错误处理器中统一拦截,显示通用的错误提示,并上报到日志监控系统。
推荐的错误处理分层:
- 请求层:捕获网络错误、超时、HTTP 状态码错误
- 数据层:捕获业务状态码错误,转换为业务异常
- 业务层:捕获并处理具体的业务异常,决定是否重试或降级
- 展示层:显示错误状态 UI,如错误提示条、空状态占位
6.3 重试策略
网络请求不可靠,合理设置重试策略可以显著提高应用的健壮性:
- 自动重试:对于 GET 请求(幂等操作),在遇到网络错误或 5xx 状态码时自动重试
- 指数退避:重试间隔逐渐增加(1s → 2s → 4s → 8s),避免雪崩效应
- 最大重试次数:通常设置为 3 次,超过后不再重试
- 手动重试:在 UI 层提供"重新加载"按钮,让用户决定是否重试
6.4 缓存策略
合理的数据缓存可以显著减少网络请求,提升用户体验:
- 内存缓存:在内存中缓存最近请求的数据,适合不频繁变化的数据(如字典数据、配置信息)
- 持久化缓存:使用 localStorage 或 IndexedDB 缓存数据,适合离线应用或 SSR 场景
- SWR 策略:Stale While Revalidate(先展示缓存数据,同时发起请求更新缓存),由 React Query 等库提供
缓存的失效策略同样重要,常见的失效策略包括:
- 时间失效:设置缓存 TTL(Time To Live),到期后自动失效
- 事件失效:当执行了相关的 Mutation 操作(创建/更新/删除)后,手动使缓存失效
- 标签失效:为缓存数据打标签,按标签批量失效
6.5 取消重复请求
在单页应用中,用户可能频繁操作导致同一个接口被多次调用。取消重复请求能够避免资源浪费和数据竞争问题。
实现方案:
- 为每个请求生成唯一的请求 Key(由 URL、Method、参数组合生成)
- 在发起请求前,检查是否有相同 Key 的请求正在进行
- 如果有,取消前一个请求或忽略前一个请求的响应
- 使用 AbortController(Fetch API)或 CancelToken(Axios)实现请求取消
6.6 请求合并
当多个组件在同一时间段内请求相同的数据时,可以将这些请求合并为一次网络请求,所有调用方共享同一个请求结果。
实现方案:
- 创建一个请求队列,Key 为请求的唯一标识
- 当第一个请求到达时,发起实际的网络请求,并将 Promise 存入队列
- 后续相同 Key 的请求到达时,直接返回队列中的 Promise,不发起新请求
- 请求完成后,从队列中移除
这种模式在微前端架构中特别有用,多个子应用可能同时请求相同的用户信息或配置数据。
七、前端目录结构最佳实践
7.1 按功能模块组织
推荐的前端项目目录结构(以 Vue 3 为例):
src/
├── api/ # API 层:请求定义和封装
│ ├── request.ts # Axios 实例、拦截器配置
│ ├── modules/ # 按业务模块划分的 API
│ │ ├── auth.ts # 认证相关接口
│ │ ├── user.ts # 用户相关接口
│ │ └── post.ts # 文章相关接口
│ └── types/ # API 请求/响应类型定义
│
├── assets/ # 静态资源
│ ├── images/
│ ├── fonts/
│ └── styles/
│ ├── variables.css # CSS 变量/设计 Token
│ ├── reset.css # 重置样式
│ └── global.css # 全局样式
│
├── components/ # 共享组件
│ ├── common/ # 通用基础组件
│ ├── business/ # 业务组件
│ └── layout/ # 布局组件
│
├── composables/ # 组合式逻辑(Vue Hooks)
│ ├── useAuth.ts
│ ├── usePermission.ts
│ └── useTheme.ts
│
├── config/ # 应用配置
│ ├── index.ts # 全局配置
│ ├── theme.ts # 主题 Token
│ └── i18n.ts # 国际化配置
│
├── directives/ # 自定义指令
│ ├── permission.ts # 权限指令
│ └── debounce.ts # 防抖指令
│
├── layouts/ # 布局页面
│ ├── default.vue
│ └── blank.vue
│
├── locales/ # 国际化翻译文件
│ ├── zh-CN/
│ └── en-US/
│
├── router/ # 路由配置
│ ├── index.ts # 路由实例
│ ├── routes.ts # 静态路由定义
│ ├── dynamic-routes.ts # 动态路由生成
│ └── guard.ts # 路由守卫
│
├── stores/ # 全局状态
│ ├── auth.ts # 认证状态
│ ├── app.ts # 应用状态(主题、语言等)
│ └── permission.ts # 权限状态
│
├── utils/ # 工具函数
│ ├── format.ts # 格式化工具
│ ├── validate.ts # 校验工具
│ └── storage.ts # 存储工具
│
├── views/ # 页面组件
│ ├── dashboard/
│ ├── posts/
│ └── users/
│
├── App.vue # 根组件
├── main.ts # 入口文件
└── env.d.ts # 类型声明7.2 对比其他方案
除了按功能模块组织外,还有一些其他目录组织方式:
按文件类型组织(分层组织):
src/
├── components/ # 所有组件
├── pages/ # 所有页面
├── services/ # 所有服务
└── utils/ # 所有工具这种方式适合小型项目,但在大型项目中,随着文件数量增长,同名文件难以区分,维护成本会增加。
按领域组织(DDD 模式):
src/
├── modules/
│ ├── auth/ # 认证领域(含组件、API、状态、路由)
│ ├── posts/ # 文章领域
│ └── users/ # 用户领域
└── shared/ # 共享基础设施适合大型企业级应用,每个领域可以独立发展,甚至拆分为独立的微前端应用。
7.3 命名规范
- 组件文件:PascalCase,如
UserAvatar.vue、DataTable.vue - 组合式函数:camelCase 加
use前缀,如useAuth.ts、usePermission.ts - 普通工具函数:camelCase,如
formatDate.ts、validateEmail.ts - 状态管理:camelCase 加模块名,如
authStore.ts、appStore.ts - API 模块:camelCase,文件名与后端模块对应,如
authApi.ts、userApi.ts
八、架构决策记录
在实际项目的前端架构设计中,以下决策原则值得参考:
- 渐进增强:从简单架构开始,随着业务复杂度增加逐步引入新的层和工具,避免一开始就过度设计
- 按需引入:只引入当前阶段需要的库和工具,不需要为未来可能的需求预先引入
- 约定优于配置:通过团队约定和项目规范减少配置复杂度,例如统一的 API 错误码处理、统一的组件命名规范
- 可测试性:每一层都应该可以被独立测试,展示层使用组件测试,业务层使用单元测试,数据层使用 Mock 测试