Skip to content

前端设计文档规范

为什么需要设计?

  • 敏捷 Story DoD 有个设计环节,这个阶段前端应该有什么产出?
  • 前端专业性体现在哪里呢?
  • 和DDD 一样。 在开始开发之前,把设计工作做好,开发就是照葫芦画瓢,我们的工作更容易预测,没有惊喜。很多问题在 Code Review 阶段发现有可能已经晚了,何况我们 Code Review 还没做好?
  • 设计不是一个人的事情,我们要利用集体智慧,把事情做好。 以后前端也会有一个技术评审会议。这是除 Code Review 之外,难得一次技术上的沟通和知识互换。
  • 写完代码之后呢?文档呢?听说过文档驱动开发吗?设计阶段的产出就是我们的文档。

看一下这个项目开发流程图,当项目启动之后,产品的同事会先来一轮需求宣讲,告知大家本期大致做什么,产品的同事会整合各方的发言在完善一轮功能。

全部功能确定之后,会进入需求讨论,这个时候,开发会明确这一期需求中,到底要做哪些功能。也就进入了今天的正题,如何做开发设计。

为什么推荐写设计文档

我们先来回想一下最近的一次需求,下面几个问题,能回忆起几个:

  • 哪些功能是相似的?相似功能有没有做通用处理?
  • 哪个功能实现起来有难度?最后是怎么解决困难点的?
  • 主业务流程是什么?有几条不同的业务流程?业务代码设计的可扩展性怎么样?
  • 新增了几个公共组件?用途分别是什么?
  • 老代码有改动吗?新老代码的兼容性是怎么处理的?
  • 有没有全新的、以前从没接触过的业务概念和体系,是如何应对全新业务的?
  • 有没有连带功能?连带功能的覆盖范围是哪些?

产品视角的需求文档,需要在哪些地方做什么样的功能提供给用户。

开发视角的需求设计,不但要梳理清楚具体功能、功能所在的位置,还要考虑代码的质量。程序开发,不是单纯的代码堆砌。所以技术论坛了,我们看到很多优秀的怎么实现某类功能的文章。

设计文档是帮开发梳理业务功能,呈现优质的开发思维的载体。另外,当开发思路逐渐丰富,开发速度也就提上来了。

设计文档写什么?

上面的几个问题,也侧面的描述了设计文档中要写哪些内容。主要包括业务流程梳理,功能点梳理,功能的封装、通用、可扩展、兼容性设计,重点和难点归纳和实现方案,新业务概念和体系的应对方案,连带功能的覆盖范围总结。

1. 业务流程图

1.1 图例和要点

图例:

  • 泳道

泳道是业务承载的容器。比如页面、模块、业务主体、某些特殊终端(比如短信、消息通知)、组件。

  • 子视图

泳道里也可以进一步拆分子视图,用于分离面向不同目标的业务。比如一个页面有多个角色参与,就可以为这些角色单独创建一个子视图。

  • 条件

业务分支

  • 动作

可以是业务流程的某个环节,或者由用户、外部触发的事件。

动作也可以由状态关联,表示该状态下支持的动作。例如:

状态关联的动作,表示业务主体处于该状态时,支持这些动作

  • 状态

业务主体当前的状态。

状态通常也作为流程的结束节点

  • 数据流

用于表示不同业务环节之间流转、不同业务主体之间交互,所需要的数据。

  • 角色

参与业务的角色

  • 起始节点

起始节点通常是角色发起的动作。如果是泳道内部的流程其实节点,可以使用这个图例。

  • 注释

用于详细描述业务环节、规则和状态

要点:

  • 流程图要表示业务流程的闭环,而不是前端的局部交互
  • 和传统业务流程图不太一样的是,我们的业务流程图也会关注用户的交互流程
  • 尽量使用业务语言,而不是技术语言。
  • 使用泳道来表示业务环节由什么容器来承载。
  • 梳理流程图时不要过渡关注技术实现细节

通过流程图可以提供什么信息?

  • 熟悉业务规则,比如业务的边界条件、业务主体状态的流转规则、流程数据(通信规则)
  • 分析模块之间的依赖关系。
  • 页面的状态

无法提供什么信息?

  • 无法体现技术设计细节
  • 无法体现视图的呈现细节

→ 这部分由概要设计来弥补

1.2 案例

要点:

  • 使用不同的泳道来表示页面
  • 不是该领域的流程放在 其他领域 或者 外部域 , 这些不是该业务域的核心问题。通常也不是由该业务域来实现。
  • 使用子视图分离团长和团员的不同角色的业务
  • 使用黄色标记跨泳道之间的流程,用蓝色标记角色的业务发起点。

2. 概要设计

业务流程图可以梳理待开发的业务流程图、业务主体状态、依赖关系等等。这里并没有包含太多前端技术设计细节,概要设计就是为了弥补这块的空白。

2.1 页面/模块拆分

根据业务需求以及产品原型对业务域内的页面进行拆分。页面拆分是前端设计中最简单的一个环节,主要涉及:

  • 页面路由定义

    • 页面命名。我们推荐使用别名导航,而不是路径导航。因为路径的可读性较差、变动的频率也更高。
    • 页面路径。
    • 分包规划。在小程序中,分包直接影响页面路径,以及后期发布。能不放在主包的就不放在主包。
  • 页面通信协议设计

    • 路由参数(params)。设计携带在页面 URL 上的关键参数(查询字符串)。例如商品详情页面,id 表示商品 id。
    • 通信协议。如果路由参数无法满足需要,需要在页面之间传递大量数据或者引用类型值, 则需要用到内存通信
      • 输入(data)。
      • 输出(backMessage)。页面返回参数。

⚠️ 大部分场景我们不推荐使用内存通信,因为这会造成页面之间的耦合、丧失独立运行能力、且无法分享到外部。因此在审核设计时要考虑有没有必要用内存通信。

目录规划。原则是按业务聚合而不是职能聚合。我们推荐将同一个业务域下的组件、API、模型、页面都聚在一起,而不是按照功能分散在程序多处。

js
# ❌ 按职能聚合
/components
  /a
  /b
  /c
/pages
  /page-a
  /page-c
/api
/utils


# ✅ 按业务域聚合
/modules
   domain-a/     # 业务模块
     components/
       /a
       /b
     page-a.tsx
     api.ts
     utils.ts
   domain-b/     # 业务模块 
     components/
       /c
     page-b.tsx
     api.ts  
routes.ts # 通用注册路由,引用业务域的页面

2.2 模型拆分和设计

模型用于放置业务逻辑和业务状态。

2.2.1 业务状态机/业务主体生命周期

通过上面的流程图,我们可以发现很多业务就是一个状态机,而前端页面无非在不同的状态下,支部不同的呈现和操作。

例如拼团详情页状态机:

我们可以从上图抽象出三个状态(等待拼团、拼团过期、拼团成功、拼团取消),以及挂靠在不同状态下的不同动作。

最简单的实现是用一个状态枚举来表达它:

js
enum GroupStatus {
  Pending = '等待', 
  OutDated = '过期', 
  Success = '成功',
  Cancelled = '取消'
}

在视图层,我们可以给这些状态区分不同的呈现:

js
status === GroupStatus.Pending ?
  <ButtonGroup>
    <Button>取消拼团</Button>
    <Button>分享拼团</Button>
  </ButtonGroup>
: status === GroupStatus.OutDated || status === GroupStatus.Cancelled ?
  <ButtonGroup>
    <Button>再次拼团</Button>
  </ButtonGroup>
: status === GroupStatus.Success ?
  <ButtonGroup>
    <Button>查看订单</Button>
    <Button>再次拼团</Button>
  </ButtonGroup>
: null

如果不同状态下视图有较大差异,可以将每个状态抽离成单独的组件。

模型层对应的行为触发时,也可以对状态进行检查:

js
class GroupModel {
  status: GroupStatus
  // ...

  /**
   * 取消拼团
   */
  cancel() {
    // 状态检查
    this.assertStatus(GroupStatus.Pending, '取消拼团')
    await this.repo.cancel(this.id)

    // 状态流转
    this.status = GroupStatus.Cancelled
	}

  /**
   * 状态检查
   */
  assertStatus(status: GroupStatus, message: string) {
    if (this.status !== status) {
      throw new Error(`程序异常:只能在 ${status} 状态下,才能 ${message}`)
    }
  }
}

对于复杂的页面,状态不会像上述的那么单一,有可能存在多个业务主体,且不同业务主体有不同状态,甚至状态还可以嵌套子状态、状态流转规则复杂等等。

就拿发起拼团这个例子来说:

如上所示,一个复杂业务流程会涉及很多状态,在设计阶段我们要将 不同的主体的状态 识别出来。后期就围绕着这些状态进行开发。

状态机学习资料:

  • 产品之术:一目了然的状态机图
  • 如何绘画状态机来描述业务的变化

2.2.2 模型设计

模型(Model) 是一个核心对象,它承载了核心的业务逻辑。模型类中应该包含哪些内容呢?

  • 业务状态。即我们在上一节中识别出来的业务状态。在模型层中会为不同’主体‘创建一个状态变量,用于存放当前的状态。

  • 业务数据。例如活动详情、当前选中数据、活动列表等等。

  • 计算数据/衍生数据。在业务数据的基础上计算出来。我们建议你不要去直接修改业务数据,而是优先基于业务数据去推断、计算你想要的数据。

  • 行为。模型就是是 数据+行为。通常行为可以总结为以下集中

    • 状态变更、流转。比如下单、发起拼团,触发业务状态之间的流转。
    • 业务数据变更。比如修改选中的商品、删除列表项。
    • 数据持久化。调用持久化层相关接口,对业务数据/状态进行持久化。
  • 事件。事件是模块解耦、实现扩展的一种重要手段。通常模型会抛出下列事件:

    • 业务状态变更。
    • 异常情况。
    • 扩展点

TIP

不过不是所有业务状态变更事件都应该抛出来,因为:

  • 不是所有业务状态变更事件都能在前端捕获到。前端只是业务流程的局部,能被前端捕获的往往是由页面在界面触发的。
  • 不是所有事件抛出去都有意义。结合实际场景来看,比如需要在这个事件触发时进行埋点。
  • 模型生命周期。使用依赖注入框架之后,需要关心这个问题,决定单例还是非单例

    原则是如果你的模型需要在整个应用生命周期中存在,则使用单例,例如登录、会员信息这些。大部分场景都应该使用非单例,跟随页面释放而释放。

2.2.3 输出案例

以登录 SDK 为例:

  • 业务状态:

  • 登录状态

    • 初始化:创建会话
    • 登录中
    • 登录成功:重新登录、更新用户信息、退出登录
    • 登录失败:再次登录
  • 业务数据:

    • 会话信息
    • 失败信息
    • 重新登录的次数
  • **衍生数据:**这些信息都从会话信息中提取出来

    • 已登录?
    • 已注册?
    • 会话 id
    • 用户信息
  • 行为:

    • 创建会话
    • 重新登录
    • 退出登录
    • 等待登录成功
    • 更新用户信息
  • 事件:

    • 缓存会话恢复
    • 登录前
    • 初次登录成功
    • 登录成功
    • 登录失败
    • 会话刷新
    • 退出登录
    • 用户信息更新
  • 模型生命周期:单例

2.3 组件拆分和设计

组件的拆分和设计是前端设计的重头戏,合理拆分组件,可以提高代码复用率和后期的可维护性。

输出案例:

NoticeBar 滚动公告栏

属性

属性说明类型默认值
mode通知栏模式string-
text通知文本内容string""
color通知文本颜色string
background滚动条背景string

事件

事件名说明
onClick单击通告栏时触发
onClose关闭通告栏时触发

插槽

名称说明
children通知栏内显示内容
leftIcon自定义左边图标内容
rightIcon自定义右侧图标内容

2.4 扩展点设计(SDK 开发)

如果你开发的是 SDK,那就需要考虑扩展性问题,你的程序需要考虑各种场景的使用,考虑行业团团、项目交付时,对你的程序进行各种粒度的定制。

扩展点实现方式:

  • 使用依赖注入形式。依赖注入点可以由外部进行重新定义
  • 事件。

输入案例:

登录 SDK 扩展点

js
## 暴露的扩展点

| 名称                                              | 说明                                                           | 单例 |
| ------------------------------------------------- | -------------------------------------------------------------- | ---- |
| 'DI.login.SUPPORT_QUICK_PHONE_AUTH': boolean;     | 是否支持快捷手机号码授权, 默认 true                            |
| 'DI.login.SUPPORT_QUICK_USER_INFO_AUTH': boolean; | 是否支持快捷用户授权,默认 true                                |
| 'DI.login.QUICK_PHONE_AUTH_TEXT': string;         | 手机号码快捷登录文案, 默认为手机号码快捷登录                  |
| 'DI.login.QUICK_USER_INFO_TEXT': string;          | 快捷用户信息获取, 默认为 允许授权                              |
| 'DI.login.ROUTE_PROTOCOL_DETAIL': string;         | 服务协议详情页面, 默认为 protocolDetail(命名路由)              |
| 'DI.login.MAX_RELOGIN_COUNT': number;             | 最大重新登录次数, 默认为 10                                    |
| 'DI.login.VERIFY_TIMEOUT': number;                | 发送验证码超时时间, 默认 60|
| 'DI.login.LOGIN_API': string;                     | 登录接口路径, 默认 /login_v3/login_v3                         |
| 'DI.login.USER_RULE_API': string;                 | 用户服务协议列表接口路径, 默认 /wk-base/c/agreement/queryList |
| 'DI.login.REGISTER_API': string;                  | 注册用户接口路径, 默认 /cs/auth/user/register/v3              |
| 'DI.login.UPDATE_USER_API': string;               | 更新用户信息接口路径, 默认 /cs/auth/vip/user/update_user      |
| 'DI.login.SEND_PHONE_VERIFICATION_API': string;   | 发送验证码接口路径, 默认 /cs/auth/user/send_register_code     |
| 'DI.login.PLATFORM': PlatformType;                | 当前平台                                                       |
| 'DI.login.Implement': ImplementProtocol;          | 平台适配实现                                                   | yes  |
| 'DI.login.LoginRepo': LoginRepo;                  | 登录接口实现                                                   | yes  |
| 'DI.login.LoginModel': LoginModel;                | 登录模型                                                       | yes  |
| 'DI.login.RegisterModel': RegisterModel;          | 注册模型                                                       | yes  |
| 'DI.login.PhoneVerifyModel': PhoneVerifyModel;    | 手机验证码模型                                                 |

<br>
<br>

## 暴露的事件

| 标识符                                                                | 描述                         |
| --------------------------------------------------------------------- | ---------------------------- |
| 'Event.login.onRecover': SessionInfo;                                 | 从缓存中恢复                 |
| 'Event.login.onBeforeLogin': undefined;                               | 登录前                       |
| 'Event.login.onSetup': SessionInfo;                                   | 首次登录完成                 |
| 'Event.login.onLogined': SessionInfo;                                 | 已鉴权,鉴权成功             |
| 'Event.login.onLoginFailed': Error;                                   | 鉴权失败                     |
| 'Event.login.onLoginComplete': { info?: SessionInfo; error?: Error }; | 登录完成,可能成功,可能失败 |
| 'Event.login.onRefreshed': SessionInfo;                               | 会话刷新成功                 |
| 'Event.login.onRefreshFailed': Error;                                 | 会话刷新失败                 |
| 'Event.login.onLogout': never;                                        | 退出登录                     |
| 'Event.login.onUpdateInfo': SessionInfo;                              | 更新信息成功                 |
| 'Event.login.onUpdatedUser': UserInfo;                                | 更新用户信息                 |
| 'Event.login.onUpdatedUserFailed': Error;                             | 更新用户信息失败             |
| 'Event.login.onBeforeRegister': RegisterOptions;                      | 注册前                       |
| 'Event.login.onRegistered': UserInfo;                                 | 注册成功                     |
| 'Event.login.onRegisterFailed': Error;                                | 鉴权失败                     |

3. 设计输出模板

业务流程图

页面拆分

业务状态

模型设计

组件拆分

扩展点

本站总访问量次,本站总访客数人次
Released under the MIT License.