一句话观点先放这CustomDialog 看上去是为弹窗这种场景设计的实际上它的生命周期假设和真实的业务场景对不上。我承认一开始是被官方文档带偏的——他们给的所有示例都很简洁、漂亮。但真实项目里你不可能只弹一个静态文字框对吧等你需要带数据、带异步、带按钮回调的时候它就开始给你表演翻车三连了。下面三个坑是我分别在三个不同场景里遇到的。坑一和 Navigation 路由的 onBackPress 互相打架我之前写过一个删除确认弹窗长这样CustomDialogstruct DeleteConfirmDialog{controller:CustomDialogController caseTitle:stringonConfirm:()void(){}build(){Column(){Text(确定删除「${this.caseTitle}」).fontSize(16)Row(){Button(取消).onClick(()this.controller.close())Button(删除).onClick((){this.onConfirm()this.controller.close()})}}}}页面用 Navigation 包着弹窗在页面里。问题来了用户按系统返回键时是关弹窗还是退页面我当时期待是关弹窗结果它直接退了页面弹窗还在。回来一看弹窗是显示的但 controller 已经销毁了点任何按钮都报 undefined。更恶心的是当你用router.back()主动退页面时如果页面里挂着一个没关的 CustomDialogLog 里会冒出一堆CustomDialogController has been destroyed的红字然后整个 App 一卡一卡的。我后来翻了下官方 issue 区发现这个问题从 API 8 就有人提到现在还是 open 状态——这已经两年了。坑二弹窗内部 async 数据回填关闭时序会错乱这个坑最坑。场景是这样的用户点编辑标签弹窗起来弹窗里面要发请求拿可选标签列表。请求回来之前按钮 disabledloading 转圈。代码大概长这样CustomDialogstruct TagEditDialog{controller:CustomDialogControllerStatetags:string[][]Stateloading:booleantrueaboutToAppear(){fetchTags().then((res){this.tagsres.datathis.loadingfalse})}build(){Column(){if(this.loading){LoadingProgress()}else{ForEach(this.tags,(tag){Text(tag)})}}}}看起来没毛病是吧实际跑起来网络慢的时候弹窗会先关掉数据才回来。不是没关好那种残留是 controller.close() 已经调用了then 里 this.tags res.data 还在执行。为什么因为 controller.close() 是异步的它立刻就返回了。我一开始以为是 await 没用对后来打日志才发现——即使我 await 了this.controller.close()弹窗消失的动画和 then 回调的执行顺序在不同设备上还不一样。更惨的是this 指向也会丢。我那段代码里then((res) { this.tags res.data })在鸿蒙 PC 上有一半概率 this 是 undefined在鸿蒙手机标准版上倒是稳的。这种PC 端和手机端行为不一致的问题我个人最讨厌——你根本没法复现用户反馈过来你也只能猜。但CustomDialog的问题更直接它就是没把异步这件事纳入设计考量。控制器一调假设你立刻能拿到结果。整个生命周期都按同步模型设计结果一遇到 await、then、网络请求就开始拧巴。为什么我纠结这一点因为我在另一个项目里用 promptAction.showToast 处理过类似场景——人家就老老实实按 promise 模型设计的从来不出错。CustomDialog 抄了一部分 UI又自作主张加了 controller.close() 这种立刻返回的 API两套模型混在一起怎么用怎么别扭。坑三多次调用弹窗叠在一起我有个批量操作功能逻辑是选完案例后弹一个选择操作类型的弹窗选完操作类型后再弹一个确认执行的弹窗。连环弹。我第一次这么写this.step1DialogController.open()在 step1 的确定回调里this.step1DialogController.close()this.step2DialogController.open()你猜怎么着两个弹窗同时显示。step2 直接叠在 step1 上面背景的 step1 还能看见。用户点 step2 外面想关掉 step2结果 step1 也被关了。官方对这个的解释是CustomDialog 不是模态的文档里轻飘飘一句带过。但问题是——它的样式和行为就是模态的啊挡住下面的操作谁会想到它是非模态的我后来加了一堆 setTimeout 来错开this.step1DialogController.close()setTimeout((){this.step2DialogController.open()},250)看起来能跑了。但 250ms 是魔数换个设备可能 200ms 都不够。而且这种魔法数字早晚要翻车。我现在已经不敢这么写了。替代方案普通组件 visibility我后来是用这个方案重写的全项目跑了两个月没再出过问题Componentexportstruct BaseDialog{Propvisibility:VisibilityVisibility.NoneBuilderParamcontent:()voidthis.defaultContentBuilderParamactions:()voidthis.defaultActionsBuilderdefaultContent(){Text(默认内容)}BuilderdefaultActions(){Button(关闭).onClick((){this.visibilityVisibility.None})}build(){Stack(){// 背景遮罩Column().width(100%).height(100%).backgroundColor(#00000080).visibility(this.visibility).onClick((){this.visibilityVisibility.None})// 弹窗本体Column(){this.content()this.actions()}.width(80%).backgroundColor(Color.White).borderRadius(12).padding(16).visibility(this.visibility)}}}页面里用EntryComponentstruct CaseListPage{StatedialogVisible:VisibilityVisibility.Nonebuild(){Column(){Button(打开弹窗).onClick((){this.dialogVisibleVisibility.Visible})BaseDialog({visibility:this.dialogVisible}){// 自定义内容Text(这是要删除的案例)}}}}好处和 Navigation 路由完全无冲突因为它是普通组件关闭就是改个状态异步数据回填时序清晰多次弹窗不存在这个问题每次都重新 mount坏处动画得自己写其实也不难animateTo 一把梭全屏遮罩要自己处理点击穿透没办法像 CustomDialog 那样全局调起但这三条我都能接受至少不会再PC 端和手机端行为不一致。顺便说说 promptAction 的取舍你可能会问那系统级的弹窗怎么办比如 Toast、AlertDialog 这种全局提示我的答案是能用 promptAction 的就用 promptAction。鸿蒙官方给 promptAction 设计的就是 promise 风格的 APIawait 一把梭没那么多破事。// 全局提示用这个就完事promptAction.showToast({message:保存成功})// 需要用户确认的轻量弹窗promptAction.showDialog({title:提示,message:确定删除吗,buttons:[{text:取消,color:#999},{text:确定,color:#FF0000}]}).then((result){if(result.index1){// 确认删除逻辑}})我现在的策略是这样的所有系统级提示走 promptAction所有页面内弹窗走普通 Component。这样边界很清晰代码也很好维护。唯一要注意的是promptAction.showDialog 在某些鸿蒙版本里样式很丑几乎没法定制样式。但对功能来说够用了——丑就丑吧又不是不能用。我现在的态度我承认我花了三天才决定把 CustomDialog 删干净。中间也犹豫过——毕竟官方 API说不定下个版本就修了呢但转念一想真用上 CustomDialog 的项目都已经 1.0 了没人会等你下个版本。绕开它我反而更自由——我可以在弹窗里塞任何逻辑不用担心 controller 在那边抽风。重写那一刻我甚至有点兴奋。说白了删 CustomDialog 那行代码的时候有种终于把病根切了的快感。我估计每个被这个 API 折磨过的开发者看到自己项目里彻底没有它都会下意识去刷新一下 build 目录——不是有什么问题是想确认它真的没了。你要是也在被 CustomDialog 折磨直接换成普通组件试试别再花时间研究为什么 onBackPress 不触发这种问题——这问题短期内不会修的。要不要等官方修我个人是不会等的。等不下去是一方面另一方面我新项目干脆连导入都不会有它了——架构上把这条路堵死就不用纠结这个弹窗到底该不该用 CustomDialog。写完这篇我打算去把那三处历史代码也清理一下controller: CustomDialogController 这个字段留着实在碍眼。关于我10 年软件开发的老人软件设计师 人工智能应用工程师专注鸿蒙 ArkTS 北向和 Web 前端不定期在 CSDN 写点鸿蒙和 AI 的东西。本文遵循 MIT 协议转载请注明出处。