
每次聊 SAP Fiori 开发我都喜欢问一个问题你的应用在 launchpad 里点开之后用户能不能顺畅地做完一整条业务很多人觉得这是个伪命题——导航不就是点一下按钮跳个页面嘛。但真到了 Fiori launchpad 这个环境下应用内层级导航和跨应用跳转交织在一起事情就完全不是这么简单了。我在维护一个内部采购相关项目时就吃过导航的亏。用户从工作台一个待办事项直接跳进采购订单详情在里面层层下钻到行项目又通过一个语义链接跳到供应商管理应用查看资质看完之后还得原路返回到采购订单继续操作。这中间任何一环断掉用户就只能滚回启动板重新进业务连续性直接被砍断。这篇内容我不想从官方文档翻译而是站在一个实操开发者的角度把这套导航机制拆开层级路由怎么设计、跨应用跳转用哪些姿势、参数传递有哪些坑、怎么在真正部署前把链路验证明白。如果你是第一次接触 Fiori 开发或者已经写了几个应用但对导航心里没底这篇文章应该能帮上忙。1. 导航这件事值得花心思去设计吗先说个前提Fiori launchpad 不是一个网址导航页它是用户每天打开系统之后的工作入口。应用里面的导航链路本质上是用户完成一项业务任务的路径。你把这条路径设计得合理用户会很自然地完成工作设计得别扭哪怕每个页面都漂亮最后也会被吐槽“难用”。1.1 launchpad 不是网址收藏夹是流转中心很多刚接触 Fiori 的人会把每个应用理解成一个独立的网页点击图标打开一个 URL然后在这个网页里折腾。这么理解没有大错但漏掉了最关键的一点在 launchpad 环境中应用和应用之间是有任务的。比如一个采购员在审批应用里看到一张采购申请他需要跳到采购订单应用去查这条申请对应的订单再跳回审批流继续审批。这个闭环如果用一个又一个的硬编码 URL 去拼会非常脆弱因为应用升级、位置变化、权限不同都会让链接失效。这正是 Fiori launchpad 的导航架构比其他 Web 应用复杂的地方它引入了“意图Intent”这个概念。一个应用不再通过“页面地址”来标识而是通过“语义对象Semantic Object 行为Action”来标识。比如你要展示一张采购订单目标不是某个具体的页面 URL而是 semanticObjectPurchaseOrder、actiondisplay 这个意图。具体由哪个应用来响应由 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。在实际项目里我见过最典型的例子是源应用传递了一个布尔值isUrgenttrue目标应用用if (oParams[isUrgent])判断看起来没问题但源应用后来改成了isUrgent1目标应用没同步升级判断就失效了。所以跨应用参数最好定义一个“只允许字符串和数字”的简单契约不要传对象、数组和布尔值。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-debugtrue能看到更完整的组件加载日志。在 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 跳 BB 能打开并正确显示参数。B 内返回 AA 保持之前的界面状态。无权限用户点击跳转按钮有友好提示而不是白屏。外部 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 起步直接按语义导航来做越早习惯后面的坑越少。要是你已经在老代码里踩过返回按钮失灵、参数传丢的坑也别急着推翻重来先对照这篇里的排查顺序抓一次现行问题往往就在那三个小地方。