去年复盘一个协作类WebApp的失败案例时,团队把三周时间都花在了返工上:原型验收现场,产品和开发对一个“任务”何时进入“已完成”栏争执不下——开发说后端根本没有这个状态,设计说线框图上就是这么画的。问题不在执行力,而是一开始就没人把“数据长什么样、数据怎么组织、用户如何与数据互动”这三件事单独建模,整个方案直接跳进了画页面。那次之后,内容模型(Content Model)、数据树(Data Tree)和交互模型(Interaction Model)成了我设计WebApp时的固定前置步骤,今天把这三个方法完整拆开讲讲。
这篇文章适合产品经理、交互/UI设计师、即将开工的WebApp项目负责人,也适合想搞懂“信息架构到底怎么落地”的新人。文中我会用一个贯穿全文的例子——面向小团队的任务协作WebApp,带你把三个模型从零到一建一遍,最后聊一聊建模过程中反复出现的坑与排查经验。
1. 内容模型:先回答“产品里到底有什么”
很多团队做WebApp是从画线框图开始的,第一批页面出来之后,开发问的第一个问题往往是:一个任务到底包含哪些字段?负责人是必填还是选填?任务和评论是什么关系?页面原型回答不了这些问题,因为它只表达了“看起来怎么样”,没有表达“本质上是什么”。
1.1 页面原型是效果图,内容模型才是施工图
我经常用一个装修类比:页面原型相当于装修效果图,好看、直观、能向老板汇报;内容模型则是户型图加水电走向图,不浪漫,但施工队真正照做的是它。效果图告诉你要在墙上挂一幅画,施工图告诉你那面墙是不是承重墙、电线怎么走、插座装多高。WebApp也是一个道理——卡片样式可以随便调,但“任务”这个对象有多少个字段、哪些必填、哪些由系统自动生成,这些如果不在设计阶段定清楚,开发过程中就会出现程序员自由发挥的灾难。
内容模型的定义很简单:它是对产品中所有内容对象的结构化描述。以任务协作应用为例,内容对象可能包括用户、项目、任务、评论、附件、标签、通知、操作日志。每一个对象都有自己的属性集合,对象之间还有关系。它的价值在于,把隐藏在产品经理头脑中、散落在PRD各处的“这个页面有这些字段”,集中成一份一看就能核对的数据清单。
内容模型关注的四个问题是:
- 产品里有哪些类型的内容对象?
- 每个对象有哪些属性(字段),各自是什么类型?
- 哪些属性是必填的,哪些有默认值或约束条件?
- 对象之间的关联关系是什么,是一对一、一对多还是多对多?
这四个问题,是所有后续页面设计和接口设计的事实基础。页面可以设计得漂亮,但如果字段错了、关系乱了,开发一实现就会立刻暴露。
1.2 构建内容模型的三步法
第一步是列实体。把产品涉及的所有“名词”摆出来。这里有一个非常重要的判断标准:实体必须是独立存在、有多个属性、会被多处复用的东西,而不是某个页面上的视觉区块。比如“任务卡片”不是实体,“任务”才是;“看板列”不是实体,“任务状态”是它的一个字段。
以任务协作WebApp为例,我通常会收到这样一份实体初稿:
| 实体 | 初步判定 | 说明 |
|---|---|---|
| 用户 | 是 | 有账号、姓名、头像、邮箱等 |
| 项目 | 是 | 有名称、描述、成员列表、截止日期 |
| 任务 | 是 | 有标题、描述、状态、负责人、优先级等 |
| 评论 | 是 | 有内容、作者、创建时间,挂在任务下 |
| 附件 | 是 | 有文件名、路径、上传人、大小 |
| 标签 | 是 | 有名称、颜色,且可复用到多个任务 |
| 看板列 | 否 | 看板列只是任务状态的可视化映射,用枚举字段表示即可 |
| 通知 | 是 | 有接收人、触发事件、是否已读 |
第二步是定字段。每个实体列出它的字段名、字段类型、是否必填、约束条件。这一步骤看起来机械,但实际上最见功力,因为遗漏字段是WebApp开发中返工率最高的问题。比如“任务”实体,草稿时可能只写标题和负责人,但实际开发时还涉及截止时间、优先级、创建人、创建时间、最后修改时间、状态、归档标记、看板列位置、排序权重。排序权重这种东西,如果不提前想清楚,到了做拖拽排序的时候,前后端又要各改一遍。
第三步是理关系。这一步决定接口嵌套结构和数据流转方向。任务协作应用里最常见的几类关系:
- 用户和项目:多对多(用户可以加入多个项目,一个项目有多个成员)
- 项目和任务:一对多(通常还可以归类成:任务挂在项目下)
- 任务和评论:一对多
- 任务和标签:多对多
- 任务和附件:一对多
- 任务和子任务:自引用的一对多(通过parent_id实现)
我最开始做内容模型时犯过一个典型错误:把所有东西都设计成一对一,觉得这样最简单。结果做到后面发现,一个任务需要多个附件、多个标签、多个评论,只能推翻重来。所以关系这一步宁可在建模初期多想一步,也不要等开发阶段再改。
1.3 区分“内容对象”和“页面元素”是新手最常见的坎
判断一个东西是内容对象还是页面元素,可以问自己三个问题:
- 它是不是有独立的生命周期?比如“任务”从创建到完成到归档,“看板列”则随着任务状态变化而变化,没有自己的生命周期。
- 它是否被多个页面或组件复用?用户头像被任务卡片、评论列表、成员管理页共同引用,所以“用户”是实体;某个页面特有的装饰性区块就不是。
- 它是否有多个字段?只有一个名称、没有其他属性的东西,先不要急着当实体,很可能只是一个普通字段。
掌握了这个判断方法,后面建数据树的时候会顺畅很多,因为树上的每个节点几乎都对应着内容模型里的实体或实体集合。
2. 数据树:让内容模型长成可导航的层级结构
内容模型解决的是“有哪些东西”,数据树解决的是“这些东西怎么组织”。我倾向于把数据树理解为内容模型的结构化视图——它按照包含关系、从属关系,把内容对象排成一棵有层次的树。
2.1 为什么是“树”,而不是“网”或者“平铺”
理论上,内容对象之间的关系可以形成任意有向图,但WebApp的主导航和数据组织几乎都用树。原因有几个:
第一,人的工作记忆和视觉导航能力是有限的。树形结构自上而下的路径清晰,用户知道自己在哪一层、能去哪一层,不会迷路。平铺结构适合极少数场景(比如标签云、时间流),但在功能复杂的协作工具里,一旦对象超过几十个,平铺页面会变成灾难。
第二,树天然适合做权限继承。父级节点上的权限可以平滑地传递给子级,如果某个项目设置了对某个成员可见,那么项目下的所有任务天然对这个成员可见。用图的遍历做权限传递,每一次都要重新计算,代价太高。
第三,树的深度和面包屑导航天然匹配。WebApp的返回逻辑、上一级入口、侧边栏的展开收起,都能直接复用树的路径信息。
本质上说,树是“包含/从属”关系的最佳载体,它回答的是:哪个内容挂在哪个内容下面,用户怎么逐层下钻。
2.2 从内容模型推导数据树的可操作路径
建树不是凭感觉把导航菜单画出来,而是有推导步骤的。我通常按这个顺序操作:
第一步,把内容模型中所有关系过一遍,找出符合“包含或从属”语义的关系。在任务协作应用里,项目包含任务,任务包含评论和附件,这些都是天然的树边。但“任务多对多标签”就不适合做成树,它更适合做成“索引”或“筛选器”,树的某个节点下挂一个“全部标签”入口已经足够了。
第二步,判断关系连成树以后,树的深度是否可控。Web端的树深度我建议最多不超过五层,移动端两到三层就得打住。如果超过这个深度,通常说明要么实体划分太碎,要么某些“详情”和“列表”被错误地割裂成了多层。
第三步,为树上的每个节点设计“聚合数据”。我习惯把这个动作叫“蓄水池设计”——树上的节点不只是静态的名称,还要携带下级内容的摘要信息。比如项目节点上要显示任务总数、未完成任务数、近7天活跃度;任务节点上要显示评论数、附件数、子任务完成比例。这些聚合数据决定了列表页和卡片组件的信息密度。
一个任务协作WebApp的数据树大致长这样(我习惯用文本缩进表达树的层级):
- 工作台
- 我的任务(按负责人筛选的任务集合)
- 我评论过的(按评论作者筛选)
- 待办提醒(按截止时间和状态筛选)
- 项目A
- 任务
- 任务1
- 评论
- 附件
- 子任务
- 任务2
- 任务1
- 成员
- 标签
- 任务
- 项目B
- 任务
- 成员
注意“我的任务”并没有在内容模型中单独定义实体,它本质上是任务集合的一种筛选视图。这种“筛选视图”在数据树里非常常见,它们不是真正的父级节点,而是虚拟节点,但导航上需要这样的入口。
2.3 导航树是数据树的裁剪和权限过滤
数据树和最终页面导航树并不完全等同。数据树表达的内容组织和权限边界,导航树是数据树在特定用户身份下可见部分的投影。同一棵树,普通成员看不到“项目设置”节点;访客只能看到被分享的任务节点,看不到整个项目分支。这个投影逻辑在建模时就要想清楚,否则到权限设计时会打回重来。
我在实际项目里会把数据树图贴在需求文档第一页,然后给每个节点标注“可见角色”和“导航形态”——有的节点对应侧边栏菜单,有的节点只作为面包屑路径存在,不出现导航菜单中。这些标注后续会成为权限用例的生成基础。
3. 交互模型:把状态变化当成一等公民
内容模型和数据树把静态结构定义完了,接下来要解决的是动态行为:用户能做什么操作,操作之后界面怎么变,数据怎么流转。这就是交互模型(Interaction Model)要覆盖的范围。
3.1 页面流程图为什么不够用
传统的页面流程图把用户路径画成“页面A → 页面B → 页面C”,这对简单网站够用,但WebApp的核心是状态。任务协作系统里一个最典型的例子:同一个任务卡片,在“待处理”状态下显示“开始处理”按钮,在“进行中”状态下显示“标记完成”按钮,在“已完成”状态下按钮消失、显示完成时间和完成人。如果只画页面跳转,这些同一页面内的状态变化需要散落在三四张线稿里,既难读,也容易被遗漏。
交互模型的思路正好反过来:它以内容对象的生命周期为出发点,把每一个状态、每一个可触发状态迁移的事件、每一次事件后的反馈都做成显式的设计对象。页面只是状态的一个承载视图。
3.2 状态、事件、反馈三件套
我建交互模型时,始终围绕三个核心要素:
第一个是状态(State)。状态既包括视图状态,也包括数据状态。视图状态好理解,比如弹窗的开与关、卡片的展开与收起;数据状态指的则是内容对象自身的生命周期,比如任务有“待处理、进行中、已完成、已归档”四个状态。这两类状态需要分开定义,因为它们变化的节奏不同:视图状态由用户操作触发、变化迅速;数据状态往往要经过服务端确认、变化较慢。
第二个是事件(Event)。事件是让系统发生变化的触发器。用户点击、表单提交、拖拽、键盘快捷键,这些是用户事件;服务端回调、WebSocket推送、定时任务扫描,这些是系统事件。建交互模型时,要把每个状态下允许的合法事件列全,同时也要把非法事件处理想清楚——比如任务已经归档了,用户还想修改标题,这时候是禁止编辑还是弹出提示?
第三个是反馈(Feedback)。反馈是事件发生之后,系统给用户的可感知回应。即时反馈包括按钮的加载态、乐观更新(先改界面再发请求)、表单校验错误提示;后置反馈包括站内通知、邮件提醒、推送通知。很多WebApp交互做得“闷”,就是因为只设计了操作,没设计反馈。
把三件套综合起来,一个任务状态迁移的交互模型可以抽象成下面这组规则:
| 当前状态 | 用户动作 | 触发事件 | 目标状态 | 即时反馈 | 服务端调用 |
|---|---|---|---|---|---|
| 待处理 | 点击“开始处理” | 状态变更 | 进行中 | 按钮变为“标记完成”,卡片移动 | PATCH task.status |
| 进行中 | 点击“标记完成” | 状态变更 | 已完成 | 卡片置灰,显示完成时间 | PATCH task.status |
| 已完成 | 点击“重新打开” | 状态变更 | 进行中 | 卡片恢复编辑态 | PATCH task.status |
| 所有状态 | 点击“删除” | 删除 | 已删除 | 显示二次确认弹窗 | DELETE task |
这张表比任何一段文字描述都精确。开发拿到之后,接口路径、状态枚举、前端交互反馈基本都能直接实现。
3.3 异常状态是交互模型的分水岭
新手建模时容易只画“快乐路径”:点击保存就保存成功,打开列表就有数据。实际WebApp中大量交互复杂度来自异常状态:列表正在加载、加载失败、空数据、筛选结果为空、权限不足、网络断开、服务端保存失败。这些状态在设计评审时往往被忽略,却在开发后期占用一半的联调时间。
一个合格的任务列表页交互模型,至少要覆盖以下状态:
- 首次加载:显示骨架屏或加载态,而不是空白的页面
- 加载失败:显示错误提示和“重试”按钮
- 空数据:区分“项目下确实没任务”和“筛选条件过窄没匹配结果”两种情况,给不同的空态文案
- 保存失败:保留用户输入的内容,弹出错误提示,不直接清空表单
这些状态在交互模型里必须和正常状态并列设计。没有覆盖异常态的交互模型,本质上是没有建模,只是在画图。
3.4 交互模型和内容模型的对应关系
交互模型看起来是行为的堆叠,实际上背后全部对应着内容模型字段的变更。用户点了“标记完成”,本质上是任务的status字段从“进行中”改为“已完成”,同时要写入completed_at和completed_by字段;任务要在“已完成”列表中出现,本质上是列表查询条件中status等于“已完成”,且archived等于false。
把交互模型和内容模型放在同一张表里对照,能发现很多隐藏问题。比如:页面设计上支持“暂存任务”,但内容模型里根本没有draft这个状态字段;界面上显示“最后编辑时间”,但内容模型没定义updated_at的更新规则。这种对照检查最好在原型评审之前做,因为改一行模型字段比改一套交互流程便宜太多了。
4. 一次完整建模实战:任务协作WebApp从结构到交互
前面三章都在讲概念,这一章我把它们串起来,完整走一遍建模过程。需求背景:面向15人以下小团队的任务协作WebApp,核心功能为项目管理、任务创建与分配、任务看板、评论区、状态流转、通知提醒。
4.1 内容模型定稿
经过实体梳理,定义出七个核心内容对象:用户、项目、项目成员、任务、评论、附件、通知。下面给出关键字段表。
用户实体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | string | 是 | 用户唯一标识 |
| name | string | 是 | 显示名称 |
| string | 是 | 登录账号 | |
| avatar_url | string | 否 | 头像URL |
| created_at | datetime | 是 | 注册时间 |
项目实体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | string | 是 | 项目唯一标识 |
| name | string | 是 | 项目名称 |
| description | longtext | 否 | 项目描述 |
| owner_id | string | 是 | 创建者,外键到用户 |
| created_at | datetime | 是 | 创建时间 |
| archived | boolean | 是 | 归档标记,默认false |
任务实体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | string | 是 | 任务唯一标识 |
| project_id | string | 是 | 所属项目,外键 |
| parent_id | string | 否 | 父任务ID,支持子任务 |
| title | string | 是 | 标题 |
| description | longtext | 否 | 详细描述 |
| status | enum | 是 | 待处理/进行中/已完成/已归档 |
| priority | enum | 是 | 低/中/高/紧急 |
| assignee_id | string | 否 | 负责人,外键到用户 |
| creator_id | string | 是 | 创建人,外键到用户 |
| due_date | datetime | 否 | 截止时间 |
| estimated_hours | number | 否 | 预估工时 |
| completed_at | datetime | 否 | 完成时间 |
| completed_by | string | 否 | 完成人 |
| archived | boolean | 是 | 归档标记,默认false |
评论和附件实体挂到任务之下。通知实体挂到用户之下,包含触发事件类型、关联对象、已读标记。
关系图我在文档里用文字描述:用户与项目为多对多(通过项目成员表),项目与任务为一对多,任务与评论为一对多,任务与附件为一对多,任务通过parent_id实现自关联的子任务结构。标签被设计成任务的高频筛选维度,但因为早期版本标签的复杂度过高,MVP阶段暂时不做,用优先级字段替代筛选。
4.2 数据树落位
基于内容模型的关系,最自然的项目数据树如下:
- 用户
- 通知(按接收人聚合)
- 项目(按成员可见性过滤)
- 任务
- 子任务
- 评论
- 附件
- 子任务
- 成员
- 用户信息
- 任务
导航树则尽量控制深度,保持在四级以内:
- 工作台
- 我负责的
- 我创建的
- 我评论过的
- 项目详情
- 任务列表(按状态分组的看板视图)
- 任务详情
- 评论区
- 附件区
- 任务详情
- 成员管理
- 任务列表(按状态分组的看板视图)
项目节点上需要做的聚合数据包括任务总数、未完成数、本周到期数;任务节点上需要显示的聚合数据包括评论数、附件数、子任务完成比。这些聚合数据直接决定了卡片组件的信息密度,也是在定原型交互稿之前必须敲定的数据项。
4.3 交互模型落地
三个重点交互场景逐一建模。
场景一:任务状态流转。任务的合法状态迁移是:待处理 → 进行中 → 已完成 →(归档)→ 已归档。已归档任务可以取消归档回到已完成。从已完成回到进行中需要二次确认,因为已经写入的completed_at字段要继续保留原值,只有“重新打开”这一动作会清空该字段。这个规则如果不提前写清楚,开发八成会直接更新completed_at。
场景二:分配负责人。给任务指派负责人时,用户可以通过输入框搜索团队成员,选择后立即执行乐观更新。界面上头像先换成新负责人,同时向服务端发出PATCH请求。如果请求失败,头像回滚为原负责人,并弹出“保存失败,请重试”。为什么用乐观更新?因为团队成员的搜索列表本来就在本地有缓存,分配操作是高频操作,等待请求返回的体验滞后感很明显。
场景三:删除项目。删除项目是整个系统中最敏感的交互之一。我设计的是:第一层点击“删除”弹出确认弹窗,要求输入“删除”两个字;第二层校验该项目下是否有未完成任务,如果有则提示需要先处理这些任务或确认级联删除;第三层用软删除,保留30天可恢复期,管理员在管理后台可以撤销。这组交互所对应的字段变更包括项目表archived标记和任务表archived批量更新,交互模型里必须写清楚。
此外还设计了通用异常规则:任务列表接口加载失败时显示统一错误页,空筛选结果时页面不铺满空白,而是给出“清除筛选条件”快捷操作。“保存中”按钮统一禁用防止重复提交。待办通知超过100条时自动折叠为“查看更多”。
4.4 从模型到原型与接口的一次过
模型建完之后,原型页面其实可以被推导出来。页面 = 数据树中一个节点 + 该节点承载的内容对象在当前状态下的展示。任务详情页本质上是数据树中“任务”节点的完整展开:上半部分展示任务字段,下半部分是评论和附件的列表,右上角的操作区根据任务当前状态动态渲染按钮。
接口设计同样可以从模型直接推导。REST资源路径其实是数据树的投影:/projects/{projectId}/tasks/{taskId}/comments,对应树上“项目 → 任务 → 评论”的三级路径;/users/{userId}/notifications对应“用户 → 通知”的二级路径。模型评审时,我要求前端把页面上每个可编辑字段标出对应实体字段名,后端把每个接口的绑定的资源路径写出来,两张表一对照,遗漏无所遁形。
5. 建模过程中我踩过的坑与排查经验
最后分享几个真实项目中的高频坑。这些坑不是从文档里读来的,是熬夜联调换来的。
5.1 把页面元素当成了内容类型
有一版设计里,团队把“看板列”建成了实体,每个列有名称、顺序、卡片数量。后来开发质疑:看板列的数量是固定的(四个状态),每列卡片其实就是按状态筛选的任务,为什么要多一张表?这个设计白白增加了一次数据库迁移和前后端联调。
判断标准重新说一遍:如果它的所有属性都能由某个已有实体的字段推导出来,它就是一个视图概念,不是内容对象。看板列的所有信息,都能从“任务状态”这一枚举字段推导出来,所以它不该出现在内容模型里。
5.2 数据树层级失控
有次给一个电商后台建模,为了追求分类严谨,建出来的树深达七层:工作台 → 商品管理 → 分类管理 → 三级分类 → 商品列表 → 商品详情 → 编辑历史。用户要修改一个商品的价格,要经历六次点击加两次页面跳转。后来我们用两个手段解决:一是把“编辑历史”从树里拆出去,改成商品详情页内的一个折叠面板;二是把“三级分类”从导航树里拿掉,改成筛选器放在列表页顶部。这两刀直接把路径缩短到三次点击。
建模时给自己定一条规则:任何一条到一个具体操作对象的路径,如果超过四次点击,说明树上有节点不适合出现在导航路径中。把“数据树”和“导航树”分开看待,是解决层级过深的钥匙。数据树表达数据之间的实际从属关系,可以完整准确;导航树必须做裁剪,只保留用户高频访问的节点。
5.3 交互状态与服务端状态脱节
踩过最痛的一个坑:设计稿里画了“草稿”状态,但后端的状态枚举没有draft选项。过了两个迭代,数据里根本没有草稿数据,前端却为它写了一套完整逻辑,包括草稿列表页、草稿卡片、草稿恢复提示。到验收时才发现,整个“草稿”模块是前端自嗨、后端无感。
从那次起,我在交互模型评审时增加了一个固定环节:前端把所有视图状态梳理成一张枚举表,后端把服务端的状态枚举也列出来,双方并排列出对照表,一一核对映射关系。有映射不清的当场定方案,不允许出现“前端有但后端无”的状态。这个方法几乎为零成本,但能避免掉一半的联调返工。
5.4 模型文档变成一次性文档
建模最大的敌人是“建完就丢”。很多团队在项目启动时认真建模,等开发进入编码阶段,模型文档就被遗忘,后续所有变更都发生在各自的脑海里,到测试阶段又出现大量理解和实现偏差。
我的建议是把模型文档做成团队的“活文档”:用一个在线共享文档维护内容对象字典,每个实体一张表,任何新增字段、调整状态枚举、修改关系,都要在文档中同步更新,并在周会上过一遍变更。测试用例也建议直接从模型文档导出:每个实体对应一套接口测试,每个状态迁移对应一组交互用例。当模型文档成为测试用例的来源时,它就自然地被每个角色维护起来了,不会再变成墙纸。
想要快速判断一个项目有没有建模习惯,只要看它项目中期之后还在不在文档里讨论“字段到底叫什么名字”就知道了。页面上改文案是小事,字段名和状态枚举改起来才是伤筋动骨。
建模不一定能让项目变快。前三天你会觉得慢,因为大家都在画表格、写字段、对状态,而不是画页面。但到了联调阶段,你会发现接口文档不用临时编,测试用例不用从零写,前后端对状态的争吵几乎绝迹。省下的那些时间,是以周为单位计算的。