第29篇|表单返回就丢内容:用草稿快照和离开守卫保住输入
摘要:表单返回就丢内容,本质是把输入态当成页面临时变量。用户填了一半跳去选图片、看协议或接电话,页面重建后输入全没了。更稳的做法是把表单拆成输入态、草稿快照和提交结果:输入变化时更新脏状态,关键节点保存草稿,返回时用离开守卫确认,提交成功后再清理草稿。
我见过一个课程发布表单:标题、简介、封面都填完后,用户进入图片选择页再返回,页面重新创建,之前输入全部丢了。临时把字段塞进全局变量能救一条路径,但切账号、提交失败和重新进入又会出现旧草稿回潮。表单要按草稿链路设计,而不是靠页面实例活着。
这篇文章解决四个实际问题:
- 如何区分页面输入态、草稿态和已提交事实。
- 如何保存草稿快照而不把半成品当正式数据。
- 如何在返回时提示用户,避免误丢内容。
- 如何验证重进、提交失败、提交成功和清理路径。
先分清三种状态
| 状态 | 说明 | 生命周期 |
|---|---|---|
| 输入态 | 当前页面输入框里的值 | 页面内高频变化 |
| 草稿态 | 可恢复的未提交快照 | 跨页面、可过期 |
| 提交态 | 已保存到业务数据 | 长期事实 |
表单丢内容,通常是输入态没有及时变成草稿态;旧草稿回潮,则是提交成功后没有清理草稿态。
定义草稿快照
exportinterfaceCourseFormDraft{draftId:stringtitle:stringsummary:stringcoverUri:stringupdatedAt:number}exportfunctionisEmptyDraft(draft:CourseFormDraft):boolean{returndraft.title.trim().length===0&&draft.summary.trim().length===0&&draft.coverUri.length===0}草稿只保存用户输入,不保存提交状态、Toast 文案和页面 loading。这样恢复时不会把旧交互状态也带回来。
DraftStore 只负责读写草稿
exportinterfaceDraftStore{load(draftId:string):Promise<CourseFormDraft|null>save(draft:CourseFormDraft):Promise<void>remove(draftId:string):Promise<void>}exportclassCourseDraftService{constructor(privatereadonlystore:DraftStore){}asyncsaveDraft(draft:CourseFormDraft):Promise<void>{if(isEmptyDraft(draft)){awaitthis.store.remove(draft.draftId)return}awaitthis.store.save({...draft,updatedAt:Date.now()})}}空草稿直接清理,避免用户每次进入都看到没有意义的恢复提示。
页面输入变化只更新当前草稿
@Componentstruct CourseEditorPage{@Statedraft:CourseFormDraft=createEmptyCourseDraft()@Statedirty:boolean=falseprivatedraftService:CourseDraftService=createCourseDraftService()privateasyncupdateTitle(value:string):Promise<void>{this.draft={...this.draft,title:value}this.dirty=trueawaitthis.draftService.saveDraft(this.draft)}privatehasUnsavedInput():boolean{returnthis.dirty&&!isEmptyDraft(this.draft)}}高频输入可以加防抖,但 owner 不变:页面持有输入态,DraftService 保存可恢复快照。
返回前用离开守卫
exporttypeLeaveDecision='leave'|'stay'|'save_then_leave'exportclassFormLeaveGuard{decide(dirty:boolean,draft:CourseFormDraft):LeaveDecision{if(!dirty||isEmptyDraft(draft)){return'leave'}return'stay'}}守卫只做判断,不弹窗、不保存。页面根据结果决定是否展示确认弹窗或直接返回。
提交成功后清草稿
exportclassCoursePublishService{constructor(privatereadonlyrepository:CourseRepository,privatereadonlydraftStore:DraftStore){}asyncpublish(draft:CourseFormDraft):Promise<void>{awaitthis.repository.save({title:draft.title.trim(),summary:draft.summary.trim(),coverUri:draft.coverUri})awaitthis.draftStore.remove(draft.draftId)}}提交失败时不要清草稿。用户修正后还能继续提交,这比让用户重新填写更符合真实使用路径。
验证清单
- 填一半进入图片选择页,返回后内容仍在。
- 关闭应用再打开编辑页,能恢复未提交草稿。
- 提交失败后草稿仍在,错误可见。
- 提交成功后再次进入,不出现旧草稿。
- 空表单返回,不弹无意义确认。
- 切账号后不会恢复另一个账号的草稿。
草稿要有过期和归属
草稿不是永久缓存。一个账号的草稿不能被另一个账号看到,太久以前的草稿也不一定适合自动恢复。
| 字段 | 用途 |
|---|---|
ownerId | 区分账号或本地用户域 |
draftId | 区分新增、编辑、复制等入口 |
updatedAt | 判断是否过期 |
source | 区分手动编辑、导入恢复等来源 |
如果没有 owner,切账号后容易恢复旧草稿;如果没有 updatedAt,半年以前的半成品也可能突然弹出来。恢复草稿前要先判断它是否属于当前用户和当前入口。
恢复草稿前先做判断
exportinterfaceDraftRestorePolicy{shouldRestore(draft:CourseFormDraft,ownerId:string,now:number):boolean}exportclassDefaultDraftRestorePolicyimplementsDraftRestorePolicy{shouldRestore(draft:CourseFormDraft,ownerId:string,now:number):boolean{constexpired=now-draft.updatedAt>7*24*60*60*1000if(expired){returnfalse}returndraft.draftId.startsWith(ownerId)}}恢复策略不要写在 UI 弹窗里。页面可以问“是否有可恢复草稿”,但判断草稿是否属于当前入口,应该交给策略或 Service。
发布前回归路径
- 新增页草稿和编辑页草稿互不覆盖。
- 账号 A 的草稿不会在账号 B 下恢复。
- 过期草稿不自动弹出,但可以在草稿箱中手动找回。
- 提交成功后草稿删除,提交失败后草稿保留。
- 从图片选择页、协议页、系统后台返回,输入内容都不丢。
真实复现路径要包括跨页面回来
表单丢内容最容易在跨页面回来时出现。测试时不要只填标题然后按返回,要覆盖几个真实路径:填完标题去选封面图,回来后标题还在;填完简介去看协议页,回来后简介还在;输入一半接电话或切后台,再回到应用后草稿仍然能恢复。很多页面在普通返回时没问题,但跳转图片选择、系统权限页、协议页后会重新创建,这才是草稿链路真正要兜住的地方。
复现时按这三组入口分别走:
- 普通返回:编辑页直接按返回键。
- 跨页面返回:进入图片选择、协议页后再回来。
- 系统返回:切后台、锁屏、恢复应用后继续编辑。
草稿恢复也要给用户选择
自动恢复草稿不一定总是好事。用户可能只是想新建一条课程,却被上次半成品直接填满。更稳的交互是:发现可恢复草稿后给出提示,用户选择“继续编辑”再恢复,选择“丢弃”则清理草稿。这样既不会丢内容,也不会让旧草稿打扰新任务。
exporttypeDraftRestoreAction='restore'|'discard'|'ignore'exportinterfaceDraftRestorePrompt{title:stringupdatedText:stringaction:DraftRestoreAction}这段模型把恢复草稿变成一次明确选择,而不是页面启动时偷偷覆盖输入框。
交付前验收表
| 验收项 | 通过标准 |
|---|---|
| 草稿恢复 | 只恢复当前用户、当前入口的未过期草稿 |
| 离开确认 | 有内容才提示,空表单不打扰 |
| 提交失败 | 草稿保留,错误可见 |
| 提交成功 | 草稿删除,再进页面不回潮 |
这张表适合放到提测说明里。表单问题不是单个按钮能证明的,必须用跨页面返回和失败重试来确认。
小结:表单输入不是长期事实,但也不能只在页面里
表单要把输入态、草稿态和提交态分开。页面负责输入,DraftStore 负责可恢复快照,提交成功后再清草稿。这样返回、重进、失败重试和成功清理都有明确边界。
有内容才提示,空表单不打扰 |
| 提交失败 | 草稿保留,错误可见 |
| 提交成功 | 草稿删除,再进页面不回潮 |
这张表适合放到提测说明里。表单问题不是单个按钮能证明的,必须用跨页面返回和失败重试来确认。
小结:表单输入不是长期事实,但也不能只在页面里
表单要把输入态、草稿态和提交态分开。页面负责输入,DraftStore 负责可恢复快照,提交成功后再清草稿。这样返回、重进、失败重试和成功清理都有明确边界。