每次聊 SAP Fiori 开发,我都喜欢问一个问题:你的应用在 launchpad 里点开之后,用户能不能顺畅地做完一整条业务?很多人觉得这是个伪命题——导航不就是点一下按钮跳个页面嘛。但真到了 Fiori launchpad 这个环境下,应用内层级导航和跨应用跳转交织在一起,事情就完全不是这么简单了。
我在维护一个内部采购相关项目时,就吃过导航的亏。用户从工作台一个待办事项直接跳进采购订单详情,在里面层层下钻到行项目,又通过一个语义链接跳到供应商管理应用查看资质,看完之后还得原路返回到采购订单继续操作。这中间任何一环断掉,用户就只能滚回启动板重新进,业务连续性直接被砍断。
这篇内容我不想从官方文档翻译,而是站在一个实操开发者的角度,把这套导航机制拆开:层级路由怎么设计、跨应用跳转用哪些姿势、参数传递有哪些坑、怎么在真正部署前把链路验证明白。如果你是第一次接触 Fiori 开发,或者已经写了几个应用但对导航心里没底,这篇文章应该能帮上忙。
1. 导航这件事,值得花心思去设计吗
先说个前提:Fiori launchpad 不是一个网址导航页,它是用户每天打开系统之后的工作入口。应用里面的导航链路,本质上是用户完成一项业务任务的路径。你把这条路径设计得合理,用户会很自然地完成工作;设计得别扭,哪怕每个页面都漂亮,最后也会被吐槽“难用”。
1.1 launchpad 不是网址收藏夹,是流转中心
很多刚接触 Fiori 的人,会把每个应用理解成一个独立的网页,点击图标打开一个 URL,然后在这个网页里折腾。这么理解没有大错,但漏掉了最关键的一点:在 launchpad 环境中,应用和应用之间是有任务的。比如一个采购员在审批应用里看到一张采购申请,他需要跳到采购订单应用去查这条申请对应的订单,再跳回审批流继续审批。这个闭环如果用一个又一个的硬编码 URL 去拼,会非常脆弱,因为应用升级、位置变化、权限不同都会让链接失效。
这正是 Fiori launchpad 的导航架构比其他 Web 应用复杂的地方:它引入了“意图(Intent)”这个概念。一个应用不再通过“页面地址”来标识,而是通过“语义对象(Semantic Object)+ 行为(Action)”来标识。比如你要展示一张采购订单,目标不是某个具体的页面 URL,而是 semanticObject=PurchaseOrder、action=display 这个意图。具体由哪个应用来响应,由 launchpad 根据用户权限和配置去决定。这就相当于你在餐厅点一份“干煸四季豆”,后厨哪位师傅做、在哪个灶台做,你不需要关心,你只需要保证你的点单方式稳定。
这个设计有一个很大的好处:如果展示订单功能的应用被替换掉了,跳转方的代码完全不需要改。我在之前某个集成项目里就利用过这一点,供应商主数据应用升级重构之后,采购订单界面里的跳转按钮一行没动,继续指向同一个意图,用户体验完全不受影响。
1.2 三种导航形态:应用内路由、跨应用跳转、外部链接
做导航设计时,我习惯先把需求落到三种形态里,避免混为一谈:
| 导航形态 | 核心机制 | 典型场景 | 常用技术 |
|---|---|---|---|
| 应用内层级导航 | UI5 Router / hash 变化 | 列表 -> 详情 -> 编辑 | sap.ui.core.routing.Router、navBack |
| 跨应用跳转 | Intent 语义导航 | 订单应用跳到供应商主数据应用 | CrossApplicationNavigator 服务、语义链接 |
| 外部链接 / 新窗口 | 普通 URL、系统间跳转 | 跳到企业门户、第三方系统 | window.open、CrossApplicationNavigator.toExternal |
判断用哪一种,核心看两点:新页面是否属于同一个业务闭环,以及是否需要把业务上下文带过去。如果只是同一个应用里查看更深层的信息,走应用内路由;如果需要另一个应用来消费当前业务对象,那就走跨应用 Intent;如果是查参考资料、帮助文档这类完全平行的信息,新开标签页更合适。三者的边界有时候是模糊的,但先分清楚,后面实现才不容易拧巴。
1.3 设计导航前先想清楚的问题
不要在写代码时才想“这里该往哪跳”,而是先回答下面这几个问题:
- 这条业务链路的终点在哪里?用户完成后是回到列表还是回启动板?
- 哪些参数需要在页面间传递?其中哪些是业务主键,哪些只是界面筛选条件?
- 跨应用时,目标应用有没有权限?没权限怎么处理?
- 返回行为应该是什么?是从详情回到列表,还是回到上一个应用?
- 每个操作是打开新标签页更合适,还是原地跳转?
这些问题看似简单,但每一个都直接影响实现方式。比如“返回行为”,如果你没想清楚用户要从详情返回列表还是返回启动板,等写 navBack 的时候就会犹豫;再比如“目标应用没权限”,如果在跳转前不做任何校验,用户点击后就是一片空白,体验非常差。这些在前面设计阶段定下来,后面每个环节都能少走弯路。
2. 应用内层级导航:路由设计决定了用户能不能“丝滑”
应用内的“丝滑”,说白了就是用户从列表到详情、从详情到更深层级,再一层层退回来,整个过程不需要思考,也不会迷路。这一节我拆开讲路由配置、返回机制和深层页面的处理。
2.1 UI5 路由的基本模型
Fiori 应用内导航的主力是 UI5 的 Router。它的工作方式可以理解为:浏览器地址栏的 hash 是遥控器,Router 监听 hash 变化,根据变化找到对应的视图并渲染。举个例子,一个订单应用有三个页面:订单列表、订单详情、行项目详情,对应的 hash 可以设计成:
#订单列表#Order/4500012345订单详情#Order/4500012345/Item/10行项目详情
在 manifest.json 里,路由的配置大概是这样的:
{ "routing": { "config": { "routerClass": "sap.m.routing.Router", "viewType": "XML", "controlId": "app", "controlAggregation": "pages", "transition": "slide", "bypassed": { "name": "NotFound", "target": "NotFound" } }, "routes": [ { "name": "OrderList", "pattern": "", "target": ["OrderList"] }, { "name": "OrderDetail", "pattern": "Order/{orderId}", "target": ["OrderDetail"] }, { "name": "ItemDetail", "pattern": "Order/{orderId}/Item/{itemId}", "target": ["ItemDetail"] } ], "targets": { "OrderList": { "viewName": "OrderList", "controlAggregation": "pages" }, "OrderDetail": { "viewName": "OrderDetail" }, "ItemDetail": { "viewName": "ItemDetail" } } } }注意 pattern 里的{orderId}是占位符,导航到该路由时必须提供这个参数,否则 Router 会匹配不上。我在项目里见过有人为了省事,把复杂参数全部放在 query 参数里,pattern 留空,结果页面确实打开了,但深层刷新、返回、书签全部失灵。所以能放进 path 的业务主键,尽量放进 path。
2.2 从列表到详情的实测路径:Master、Detail 与 Object Page
最常见的层级导航是列表到详情。如果详情包含复杂业务对象,通常用 Object Page 类型的视图。点击列表行的时候,我推荐用路由参数的映射来生成跳转目标,而不是手拼字符串。原因很简单:手拼容易遗漏参数,也容易把变量名写错。
下面是列表控制器里点击一项之后的典型写法:
sap.ui.define([ "sap/ui/core/mvc/Controller" ], function (Controller) { "use strict"; return Controller.extend("demo.controller.OrderList", { onPressOrder: function (oEvent) { var oItem = oEvent.getSource().getBindingContext(); var oCtx = oItem.getModel().getProperty(oItem.getPath()); this.getRouter().navTo("OrderDetail", { orderId: oCtx.OrderId }); } }); });navTo 的第二个参数就是 pattern 里的占位符值。如果列表和详情是同一个应用内的两个视图,这种写法足够;但如果你做的是一个特别大的应用,一个团队维护列表、一个团队维护详情,我更建议用嵌套组件加>sap.ui.define([ "sap/ui/core/mvc/Controller", "sap/ui/core/routing/History" ], function (Controller, History) { "use strict"; return Controller.extend("demo.controller.BaseController", { onNavBack: function () { var oHistory = History.getInstance(); var sPreviousHash = oHistory.getPreviousHash(); if (sPreviousHash !== undefined) { window.history.go(-1); } else { this.getRouter().navTo("OrderList", {}, true); } } }); });
这里有一个非常典型的坑:如果用户从启动板通过 deep link 直接进入详情页(比如从工作台待办列表点了过来),应用内没有任何“前一条历史”,这时直接调 history.go(-1) 是没有反应的。上面的 else 分支就是对此做的兜底:回到应用默认列表页。我第一次做这个功能时漏掉了 else,结果测试反馈“详情页返回按钮点了没反应”,查了半天才发现是 deep link 进入导致的。后来我把这段逻辑定位为基础控制器的标准动作,所有需要返回的页面都继承它,问题再没复现过。
2.4 层级太深的时候怎么办
层级其实不是越深越好。我见过列表页点了进详情,详情里再进历史版本,历史版本里再进附件预览,用户已经忘了自己在哪。遇到这种场景,我会优先考虑三种处理:
- 把附件的预览放到一个非全屏的 Dialog 或侧滑页里,不进页面栈,返回逻辑就不会被打乱。
- 在详情页上用 Tab 或者面包屑来展开同一业务对象的不同视角,而不是不断新增页面层级。
- 如果确实要跳转多个业务对象,触发跨应用导航而不是继续叠层级,让下一个应用负责自己的内部导航。
另外,面包屑在层级固定的场景下很好用,比如“模块区 > 订单列表 > 订单详情”;但如果是动态嵌套结构,面包屑很容易生成一串很长的路径,反而不好维护。我的建议是:三级以内的导航靠返回按钮就够,超过三级先想想是不是页面设计出了问题,而不是继续往里叠。
3. 跨应用跳转:语义对象不是玄学,是意图的翻译
如果说应用内层级导航是“在自家房子里走动”,那跨应用跳转就是“从你家出门,去别人家办事,办完还要回来”。这一节讲清楚语义对象、CrossApplicationNavigator 和跨应用返回。
3.1 为什么我们要绕开硬编码 URL
在 Fiori launchpad 里面,跨应用跳转最原始的做法是什么?是知道目标应用的 URL,然后 window.open。这个做法在原型阶段不会有问题,但在生产环境就是一个雷:目标应用的入口 URL 可能会带版本号,权限校验逻辑也不会被正确处理,更别说用户从外部直接访问时 launchpad 的会话还没建立。
SAP 给出的解法叫 Intent-based navigation。它本质上是一次翻译:你把业务意图告诉 launchpad,由它去查这个意图应该挂在哪个应用上,再由它决定用哪个入口来打开那个应用。整个流程类似酒店前台:你说要洗衣服,前台不会直接把洗衣房钥匙给你,而是安排保洁人员去处理,你不需要知道具体是哪一个保洁员。这样做的好处是,应用之间不会因为 URL 结构变化而互相打断。
3.2 CrossApplicationNavigator:最常用的跨应用跳转姿势
在 UI5 应用里,最标准的获取方式是通过 ushell 容器拿服务。代码如下:
sap.ui.require([ "sap/ushell/Container" ], function (Container) { var oCrossAppNavigator = Container.getService("CrossApplicationNavigator"); oCrossAppNavigator.toExternal({ target: { semanticObject: "PurchaseOrder", action: "display" }, params: { "PurchaseOrderId": "4500012345" } }); });这里要注意两个点。一个是 semanticObject 和 action 必须和 FLP 应用配置保持一致,如果目标应用没有注册对应的 intent,跳转会没有反应。另一个是 params 里的参数并不会全部传给目标应用,真正生效的是目标应用 manifest 里声明的参数映射。你可以在目标应用的 manifest.json 的sap.ui5段里配置 intent 声明,告诉 launchpad 我接收哪些参数:
"sap.ui5": { "config": { "intent": { "semanticObject": "PurchaseOrder", "action": "display", "parameters": { "PurchaseOrderId": { "type": "string", "required": true } } } } }一旦目标应用声明了 required 参数,而跳转方没有传,FLP 可能会拒绝打开或者给出错误提示。这一点在设计参数契约时要特别注意。
3.3 目标应用解析参数:入口在哪
目标应用被跳转后,怎么拿到参数?常见的有两种方式:一是通过 URL 参数来接收,应用启动时读一下 location.hash;二是如果应用用了 UI5 Router,路由的 pattern 可以把 intent 参数映射到 view 参数。不管哪种,都建议应用一启动就完成参数消费,然后再决定显示哪些数据。
如果目标应用没有用 Router 来解析参数,可以用一个简单工具函数来处理:
function getIntentParams() { var sHash = window.location.hash || ""; var iIdx = sHash.indexOf("?"); if (iIdx < 0) { return {}; } var sQuery = sHash.substring(iIdx + 1); var oParams = {}; sQuery.split("&").forEach(function (sPair) { var aParts = sPair.split("="); oParams[decodeURIComponent(aParts[0])] = decodeURIComponent(aParts[1] || ""); }); return oParams; }这段代码不复杂,但要注意它只是解析工具。真正使用时,我会先在 console 里把结果打出来确认参数名和值都符合预期,再继续做后续的业务查询。很多时候页面“看起来没收到参数”,其实是参数名大小写不一致,或编码后的特殊字符被解析错了,打印出来一眼就能发现。
3.4 跨应用返回:把用户再带回来
跨应用跳转之后,如果业务还没做完,很多时候需要回到原应用。最简单的方式是让原应用继续保留在页面栈中,新应用用新标签打开。但有些场景必须同页跳转(比如移动端的体验),这时候就需要显式地让目标应用在完成动作后跳回源应用。
在 SAP Fiori 环境里,有专门的跨应用返回服务可以用于这类场景。目标应用完成操作后调用它,就能回到来源应用,而不是简单的历史回退,代码大概是这样的:
sap.ui.require([ "sap/ushell/Container" ], function (Container) { var oService = Container.getService("CrossApplicationNavigationBack"); oService.backToPreviousApp({ backParams: { "Status": "RELEASED" } }); });注意,这个服务只有在确实由另一个应用跳转进来的场景下才有意义。如果用户从 launchpad 直接点开目标应用,它可能不会工作,所以要做能力判断后再展示“返回原应用”的按钮。我一般在目标应用启动时把来源信息记录到本地状态,只有检测到“来源是某个应用”时才显示返回入口。
3.5 新标签页、同标签页与外部系统:怎么选
每次跨应用跳转,都存在“原地跳转还是新开标签页”的选择。我的选择原则大致是:
| 场景 | 建议方式 | 原因 |
|---|---|---|
| 同一业务闭环,查完要回来确认 | 同标签页 + 显式返回 | 上下文不丢,流程不断 |
| 跨系统但只是参考信息 | 新标签页 | 原页面状态不丢失 |
| 第三方外部系统 | 外部链接 + 新标签 | 避免权限上下文互相污染 |
| 移动端窄屏设备 | 同标签页 | 标签切换成本高 |
同标签页跳转最怕的是原应用状态被浏览器前进后退误伤,所以如果采用的是同标签页方案,业务数据尽量在应用内维护一份本地状态,而不是完全依赖浏览器历史。新标签页方案则要注意弹窗拦截,用户点击异步事件触发 window.open 时很容易被浏览器拦截,建议把跳转调用放在用户点击的同步链路里。
4. 参数传递的暗坑:字符串、对象、时间格式与逗号陷阱
导航机制理顺之后,真正让人头疼的往往是参数传递。这一节把我踩过、见过的问题集中说一下。
4.1 intent 参数本质是字符串
写跨应用跳转的时候,最容易犯的错误是把参数当成普通 JavaScript 对象来理解。但不管前端怎么包装,最终 FLP 的 intent 参数就是一个字符串化的键值集合。数字还好,但布尔值、日期、数组,如果你没有约定好格式,目标应用拿到的会和你预期完全不一样。比如传递0会被某些解析逻辑当成空值,传递null可能会变成字符串"null"。
在实际项目里,我见过最典型的例子是:源应用传递了一个布尔值isUrgent=true,目标应用用if (oParams["isUrgent"])判断,看起来没问题;但源应用后来改成了isUrgent=1,目标应用没同步升级,判断就失效了。所以跨应用参数最好定义一个“只允许字符串和数字”的简单契约,不要传对象、数组和布尔值。
4.2 复杂对象怎么传:编码、JSON 与大小限制
有些场景想省事,直接把一个 JSON 对象塞给参数。我个人不建议这样做,一方面 URL 有长度限制,另一方面特殊字符会增加解析出错概率。如果确实要传结构化数据,有几种相对稳的做法:
- 简单查询条件:拆成多个命名参数,例如
OrderId、Year、Status,目标应用各自解析。 - 复杂筛选条件:在目标应用内重新通过后台接口查询,不要在导航参数里背一堆数据。
- 临时会话级数据:写入 sessionStorage 或后端暂存服务,导航参数只传一个暂存 ID。
另外,如果用了 URL 编码的 JSON,一定要在目标应用里 parse 之前 try-catch。被截断的半截 JSON 会让页面直接白屏。我印象很深的一次线上事故,就是跳转参数里包含了带逗号的备注文本,没有严格编码,目标应用按逗号切分参数,结果整条文本被切成四段,业务数据就对不上了。排查起来特别费劲,因为问题出在源应用,报错现象却在目标应用。
4.3 时间与数字格式的坑
还是围绕“字符串”这个核心问题。日期格式在不同语言环境和 launchpad 配置下会不一样,如果源应用把日期转成2024-01-15传过去,目标应用是德语的浏览器设置,直接new Date()很可能得到NaN。我的建议统一用 ISO 8601 格式传时间,显示格式交给界面层的 Formatter 去处理;数字不要带千分位,长数字主键直接当字符串处理,避免精度丢失。很多内部项目把订单号、合同号直接当成 number,传过去超过 16 位时精度就丢了,这种问题一旦发生很难追溯,因为肉眼看到的值和实际存储的值可能只差最后几位。
4.4 目标应用拿不到参数?先检查这三处
遇到跳转成功但目标应用没拿到参数,我的排查顺序是固定的:
第一,检查 intent 参数名。目标应用配置的 intake parameter 如果叫PurchaseOrderId,你在 toExternal 里传POId,就算值是同一个东西也接不上。大小写也严格敏感。
第二,检查 manifest 里是否真的声明了参数。没有声明,FLP 不会把参数传过去。有时候配置在测试环境是对的,生产环境版本没更新,也会导致这种差异。
第三,查看是不是上次跳转留下的缓存。有些浏览器会有 hash 历史恢复,目标应用启动时读取的是上次的参数。开发调试时容易踩,建议把 URL 参数打印到调试台确认当前实际 hash。
| 现象 | 大概率原因 |
|---|---|
| 跳转没反应,连空白页都没有 | semanticObject/action 未注册,或当前用户无权限 |
| 页面打开但报业务数据错误 | 参数名、参数格式不符 |
| 先看到错误,刷一下正常 | 应用缓存了旧 hash |
| 返回后原应用列表刷新丢失 | 未做返回上下文处理 |
5. 实测经验:如何在 launchpad 里验证导航链路
代码写得再漂亮,没有在 launchpad 环境里验证过,都只是“理论上可用”。这一节分享我的实测流程和踩过的坑。
5.1 本地开发与 FLP 环境的差异
很多导航问题只在 FLP 环境出现,因为本地独立跑应用的时候没有 ushell 容器,拿不到 CrossApplicationNavigator。团队内部我一般会区分两套模式:本地用 standalone 模式测试应用内路由,集成环境下用 FLP 模式测试跨应用跳转。如果本地非要模拟跨应用跳转,可以引入 SAPUI5 的沙盒环境,但对大多数项目来说,直接在测试系统的 FLP 里建两个 tile,用真实环境验证更可靠,也更贴近用户实际体验。
5.2 调试:把导航链路看到底
按 F12 打开开发者工具,我的固定动作是看重启后地址栏的 hash,它会忠实记录 launchpad 的 intent 解析结果。比如你从应用 A 跳转 B,最终 hash 可能会包含 B 应用的语义对象和行动参数。这条 hash 就是整个导航的实际路径,问题基本都能从这条链路反推。
几个常用小技巧:
- 在代码里临时加一行
console.log(window.location.hash),确认启动瞬间的 hash 是什么。 - 给 SAPUI5 加载参数加上
sap-ui-debug=true,能看到更完整的组件加载日志。 - 在 Network 面板里过滤
.js请求,观察目标应用是否真的被加载了;页面空白有可能是 JS 加载失败,而不是导航逻辑问题。
5.3 最小可复现 Demo:两个应用跳转
如果怀疑跨应用跳转有问题,我会建一个最小 Demo:两个带 intent 的简单应用。应用 A 只有一个按钮,调用 toExternal 去触发 PurchaseOrder-display;应用 B 就是 PurchaseOrder-display,收到参数后把参数渲染在页面上。通过最小 Demo,可以快速排除大项目里路由、权限、插件等干扰因素。这个 Demo 不需要真实后端,参数渲染用 mock 数据就够,关键是验证 FLP 的 intent 解析链路是不是通的。
实际操作顺序是:先在 FLP 里给两个应用各建一个 tile,打开应用 A,点按钮,看能否跳到 B;再给 A 的跳转按钮加参数,看 B 能否正确读取并展示。跑通之后,再把 Demo 代码迁移到真实项目里。这种“先最小闭环,再逐步复杂化”的方式,帮我解决过不少看起来玄学的跳转问题,其实最后都只是参数名或大小写的小问题。
5.4 回归测试清单:上线前至少跑一遍
每次上线前,我会拿着下面这份清单过一遍导航:
- 从启动板点击应用 tile,应用正常打开。
- 列表进入详情再返回,回到列表且滚动位置在可接受范围内。
- 从应用 A 跳 B,B 能打开并正确显示参数。
- B 内返回 A,A 保持之前的界面状态。
- 无权限用户点击跳转按钮,有友好提示而不是白屏。
- 外部 deep link 直接打开详情页,返回按钮回到应用默认页。
这份清单不复杂,但覆盖了导航最容易出问题的几条路径。实际项目里,花在这上面的时间通常不会超过半天,却能把上线后最频繁的“用户反馈跳转有问题”挡在门外。
6. 第一次做导航设计时我会怎么开始
如果你正要做一个新应用,或者准备重构一个老应用的导航,这一节是我会建议你照着做的顺序。
6.1 把导航链路画成一张流程图再写代码
我的习惯是先把要走的链路用最简单的文字图画一遍。比如:
FLP -> 我的订单列表 -> 订单详情 -> 行项目明细 -> 供应商应用详情 -> 返回订单详情 -> 返回订单列表 -> 返回 FLP把这条链条贴在代码旁边,之后每一个 navTo 都对着它检查一遍,很少会出现走到一半回不来的情况。这不是让你画复杂的 UML,就是让业务链路可视化,谁都能看懂,评审时也方便别人提意见。
6.2 参数契约先行
建议在项目里定义一个简单的参数契约表格,包含参数名、来源、目标、类型、是否必填、缺失时怎么办。这个表不用很正规,但能在联调阶段省掉大量扯皮时间。双方开发对照这个表各自实现,跨应用跳转很少出错。
| 参数名 | 来源应用 | 目标应用 | 类型 | 必填 | 缺失行为 |
|---|---|---|---|---|---|
| PurchaseOrderId | 审批应用 | 订单详情 | 字符串 | 是 | 提示无法显示 |
| VendorId | 订单详情 | 供应商应用 | 字符串 | 是 | 提示无法显示 |
| StatusFilter | 审批应用 | 订单列表 | 字符串 | 否 | 进入默认列表 |
这个表格里的内容,其实就是前面 4.1 里说的参数契约。先约定好再写代码,能让两边的开发各自推进,不用等对方完成。项目里凡是导航问题频发的,基本都是因为参数契约只靠口头沟通,没有落到文档上。
6.3 把目标应用当公共接口看待
跨应用跳转一旦发布,别的应用就可能依赖你。所以目标应用要像对待接口一样对待入口:参数缺了不能白屏;加载失败要给用户明确的错误提示;内部接口改造不能随便砍掉旧 intent 的兼容。我见过一个内部应用升级时把 semanticObject 大小写改了一个字母,结果所有外部跳转全部失效,排查花了大半天,最后发现问题出在配置上而不是代码逻辑。所以,如果目标是给别人用的,就一定要保证意图声明的稳定性,尽量做到“参数优雅降级,入口始终可用”。
6.4 小优化:丝滑感是这些细节叠加出来的
导航顺畅不只是“点一下能跳过去”,还包含很多细节:
- 跳转加载过程加一个 BusyDialog 或路由过渡,避免用户觉得页面卡死。
- 长列表返回时尽量保留滚动位置,减少用户翻找的成本。
- 返回前如果有未保存数据,给用户明确提示,防止误触丢失。
- 跨应用返回时带一个 backParams,让原应用能刷新对应行并给出反馈。
- 如果业务链路特别长,可以在关键节点保存用户进度,下次进入时直接恢复到之前的位置。
这些优化单个看都不起眼,但叠加在一起,用户就会觉得“这个系统用起来很顺”。导航设计永远不只是技术实现,它本质上是在帮用户维持一条不中断的思考线。
最后说一点个人感受。最早我把跨应用跳转做成一个个硬编码 URL 的时候,项目也跑得好好的,直到一次目标应用整体换掉入口,我才发现跳转方有多被动。后来全部改成 Intent 之后,这类问题就再也没找过我。所以如果你正在建一个新应用,我强烈建议别从最省事的 window.open 起步,直接按语义导航来做,越早习惯,后面的坑越少。要是你已经在老代码里踩过返回按钮失灵、参数传丢的坑,也别急着推翻重来,先对照这篇里的排查顺序抓一次现行,问题往往就在那三个小地方。