if 我是前端 Leader 系列已经好久没更新了,我这两三年都去哪了?
😂 有可能掉进了一个黑洞。不是 Byte Dance,现在国内大小公司都卷,整体行业的已经被带偏了,还有向其他行业蔓延的趋势… 真是好的不学
那我现在怎么又开始写文章了?
因为现在我不卷工作了,公司也开始的考勤打卡,我觉得挺好了,一切按规矩办事,到点就弹射下班。
工作只是生活的一部分而已,工作的目的本来就是为了生活过得更好不是吗?这才应该是正常的人生形态,你说是不是? 2023 年了,梦也该醒了
💥 另外,我这边也想挪坑了,Base 珠海、远程也可以,有坑位推荐的可以私信我,感激不尽。
回到正题,做业务前端开发要不要做设计呢?我觉得大部分情况不需要,简单的增删改查业务,没有必要浪费时间去做这些,只要在产品侧描述清楚就行了。
如果业务比较复杂、涉及到多人分工和共识建立、而且项目预留的充裕的时间给开发者做预研和设计,那么做一下设计还是有必要的。
那怎么做呢?本文就介绍一下我在这方面的探索,希望能给读者提供一些借鉴。
设计的第一步是梳理业务。这个不是产品的责任吗?产品会提供 PRD、原型、用户故事等需求输入,但是下游的开发、测试还需要进一步消化,因为职责的不同,立场和关注点也是有差异的。
因此,笔者设计了一套适合前端的业务流程图绘制规范。关注点
在于:
交互流程
拆分
数据流
状态流转
。图例:
要点:
闭环
,而不是前端的局部交互
。用户的交互流程
。业务语言
,而不是技术语言
。容器
来承载。技术实现细节
。通过流程图可以提供什么信息?
无法提供什么信息?
→ 这部分由概要设计来弥补
统一使用 draw.io 来绘制流程图。将流程图保存在项目根目录下的 docs 下,跟随代码一起存储和更新。
推荐 VSCode draw.io 插件
要点:
其他领域
或者 外部域
, 这些不是该业务域的核心问题。通常也不是由该业务域来实现。团长
和团员的不同角色的业务要点:
业务流程图可以梳理待开发的业务流程、业务主体状态、依赖关系等等。这里并没有包含太多前端技术设计细节,概要设计
就是为了弥补这块的空白。
我在 if 我是前端团队 Leader,怎么做好概要设计 讲过类似的话题,可以结合一起看吧。
根据业务需求
以及产品原型
对业务域内的页面进行拆分。页面拆分是前端设计中最简单的一个环节,主要涉及:
页面路由定义。
页面通信协议设计。
传递大量数据
或者引用类型值
, 则需要用到内存通信
。
目录规划。原则是按业务聚合而不是职能聚合。我们推荐将同一个业务域下的组件、API、模型、页面都聚在一起,而不是按照功能分散在程序多处。
# ❌ 按职能聚合
/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 # 通用注册路由,引用业务域的页面
输出案例:
# 优惠券
## 页面设计
所属分包: member
页面 | 路径 | 命名 | params | data | backMessage |
---|---|---|---|---|---|
数据模型用于放置业务逻辑和业务状态。
通过上面的业务流程图,我们可以发现很多业务可以抽象为有限状态机
,而前端页面无非在不同的状态下,支持不同的呈现和操作。
例如拼团详情页状态机:
我们可以从上图抽象出三个状态(等待拼团、拼团过期、拼团成功、拼团取消),以及挂靠在不同状态下的不同动作。
最简单的实现是用一个状态枚举来表达它:
enum GroupStatus {
Pending = '等待',
OutDated = '过期',
Success = '成功',
Cancelled = '取消',
}
在视图层,我们可以给这些状态区分不同的呈现:
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
💡 如果不同状态下视图有较大差异,可以将每个状态抽离成单独的组件。
模型层对应的行为
触发时,也可以对状态进行断言检查
(assert, 或者转换守卫 guard):
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}`)
}
}
}
当然,对于复杂的页面,状态不会像上述的那么单一, 比如:
业务主体
(可以理解为业务的参与角色,比如拼图有团长、团员),且不同业务主体有不同状态和转换逻辑嵌套子状态
(复合状态 Compound states)、并行状态(Parallel states)就拿发起拼团这个例子来说:
多个嵌套状态,可以由多个状态变量来控制。
如上所示,一个复杂业务流程会涉及很多子状态,在设计阶段我们需要将 不同的主体的状态 识别出来。后期就围绕着这些状态进行开发。
好在我们在梳理业务流程图时,已经将相关规则梳理清楚了。识别这些状态并不难。更重要的是,这是一种业务建模思维的转变。
如果你想要深入学习和理解状态机, 或者在项目中严谨应用状态机,不妨试一下更专业的 XState。
💡 状态机学习资料: - 产品之术:一目了然的状态机图 - 如何绘画状态机来描述业务的变化 - XState
模型(Model) 是一个核心对象,它承载了核心的业务逻辑。模型类中应该包含哪些内容呢?
以登录 SDK 为例:
组件的拆分和设计是前端设计的重头戏,合理拆分组件,可以提高代码复用率和后期的可维护性。关于如何拆分和设计组件见 组件设计指南 、以及 React 组件设计实践相关文章
案例:
NoticeBar 滚动公告栏
属性
属性 | 说明 | 类型 | 默认值 | |
---|---|---|---|---|
mode | 通知栏模式,可选值为 'closeable' / 'link' | string | ‘’ | |
text | 通知文本内容 | string | ‘’ | |
color | 通知文本颜色 | string | #f60 | |
background | 滚动条背景 | string | #fff7cc | |
leftIcon | 左侧图标名称或图片链接 | string | - | |
rightIcon | 右侧图标名称或图片链接 | string | - | |
delay | 动画延迟时间 (s) | number | string | 1 |
speed | 滚动速率 (px/s) | number | string | 60 |
scrollable | 是否开启滚动播放,内容长度溢出时默认开启 | boolean | - | |
wrap | 是否开启文本换行,只在禁用滚动时生效 | boolean | false |
事件
事件名 | 说明 |
---|---|
onClick | 单击通告栏时触发 |
onClose | 关闭通告栏时触发 |
插槽
名称 | 说明 |
---|---|
children | 通知栏内显示内容 |
leftIcon | 自定义左边图标内容 |
rightIcon | 自定义右侧图标内容 |
如果你开发的是 SDK (即面向其他开发者),那就需要考虑扩展性问题,你的程序需要考虑各种场景的使用,比如对于 ToB 行业, 需要考虑二开、项目交付时,对你的程序进行各种粒度的定制。我在 2B or not 2B: 多业态下的前端大泥球 这篇文章也讨论过相关的背景。。
扩展点实现方式:
案例:
登录 SDK 扩展点
## 暴露的扩展点
| 名称 | 说明 | 单例 |
| ------------------------------------------------- | -------------------------------------------------------------- | ---- |
| '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; | 鉴权失败 |
当汽车到达一定的速度时,大部分的能耗用在了克服空气阻力(比如当到达 120km/h 时,大于 60%,随着速度的提升,这个比例会越来越高)。
这个适用于软件开发,随着团队规模的扩大,我们会花费大量时间用于“达成共识”。
这包括面对面的会议、电子邮件、即时消息、编写和阅读文档等各种形式。这是因为软件开发不仅仅是编写代码,更是需要理解业务需求、解决问题、协调任务、分享知识等。
软件开发中有很多工具 和方法论,可以帮助提升“达成共识”的效率,近些年最为出名的应该是 DDD 了,比如它强调引入领域专家来指导软件的设计、划分边界上下文、统一语言等等。
我们进行软件设计,也是出于此目的。因此一定要有评审,在这个过程中进行碰撞、纠错、最后达成共识。
上文给做前端业务开发怎么做设计打了个样,主要脉络是:
这些规范和观点可能并不完全适合你们的团队。为此,你们需要找出自身所面临的问题,然后采取行动,来构建出符合你们需求的设计规范。接着,在不断的迭代过程中,逐步完善和优化这些规范。