Fiori Launchpad 这个入口很多人第一眼觉得它就是个“应用菜单聚合页”点磁贴进应用、返回、再点另一个磁贴。真到自己上手做开发或实施的时候才发现导航这件事并没这么简单为什么有些应用跳转要靠#SalesOrder-display?SalesOrderID1000这样的 hash为什么跨应用跳转不能直接window.location.href硬跳为什么从 A 应用跳到 B 应用之后浏览器返回键一按直接退出了整个 Launchpad这一连串问题背后的答案都指向同一个核心——Fiori Launchpad 的导航体系不是散装链接而是一套从 Intent-Based Navigation 出发细分出 Cross-App 与 Inner-App 两个层次的完整设计逻辑。这篇文章适合正在做 Fiori 应用开发、实施或运维的读者。我会把这三层导航拆开讲Intent-Based Navigation 的语法与配置、Cross-App 跨应用跳转的代码写法和传参细节、Inner-App 应用内路由与 FLP 的边界最后再分享几个实际项目里踩过的导航坑和排查思路。读完你应该能理清“什么时候该走哪一层导航、参数怎么传、返回按钮行为怎么保证符合用户预期”。1. 先想清楚Fiori Launchpad 在导航链路上到底扮演什么角色1.1 点击磁贴不是“打开链接”而是一次意图路由很多 Fiori 里跑的业务应用本身也是 SAPUI5 应用有自己的 Router。于是新手很容易产生一个朴素的误解Launchpad 就是个 iframe 容器磁贴点击等同于往 iframe 里塞一个应用 URL 罢了。这个理解在工程上是错的而且会带来不少后患。实际上当用户点击 Launchpad 上的磁贴FLP 做的是这样一串处理读取磁贴关联的 Intent 配置。用 URLParsing 服务解析当前 Shell Hash拆分出 Intent 部分和应用内部 hash 部分。根据 Intent 去匹配 Target Mapping确认用户有权限、目标应用存在。决定打开方式当前窗口还是新窗口。把 Intent 参数注入应用应用完成内部路由进入对应页面。也就是说Launchpad 并不是“转发器”而是“导航中枢”。它把每一次跳转都变成了一次“意图匹配”而不是简单的 URL 跳转。这个设计带来的收益非常大应用之间互相跳转时不关心对方的技术路由只关心业务语义权限校验和日志审计在导航层就完成了不需要每个应用自己实现管理员还可以在不改代码的情况下替换某个磁贴背后的目标应用。1.2 三层导航模型的边界在哪里Fiori 的导航体系可以粗略分成三层导航层级发起方式典型场景谁来管基于意图的导航FLP 导航磁贴点击、Shell Hash 直接输入、应用代码发起从 Launchpad 进入任意应用Fiori Launchpad / Target MappingCross-App 导航应用内代码调用导航服务从订单应用跳转到合同应用并携带业务上下文sap.ushell容器服务Inner-App 导航应用内部 Router / NavContainer列表页进入详情页、详情页进入子对象页SAPUI5 Router这三层里最容易被忽略的边界是什么时候该用 Cross-App什么时候该用 Inner-App。我的判断标准一直很朴素——用户当前的操作是否仍然属于同一个业务对象的处理过程。比如在销售订单应用里点开行项目明细这是同一个订单业务过程的延续属于 Inner-App但如果用户从订单详情跳到“关联合同”这个独立应用业务对象已经从订单切换到了合同此时应当走 Cross-App 导航让 FLP 去解析合同应用的 Intent而不是在订单应用里硬生生嵌入一个合同页面。搞清这层边界之后再回头看 Intent-Based Navigation 的语法和配置思路会清晰很多。2. Intent-Based Navigation把“打开某条订单”变成一条语义契约2.1 一段 URL 里藏着整个设计哲学在 Fiori Launchpad 里一个典型的导航 URL 长这样https://host:port/sap/bc/ui2/flp?sap-client100#SalesOrder-display?SalesOrderID1000001#号后面的部分才是关键它由三段拼成Semantic Object语义对象SalesOrder。它代表“销售订单”这个业务对象是一个业务语义层面的分类。Action动作display。表示用户想对这个对象做什么常见的还有create、edit、approve、maintain。Parameters参数SalesOrderID1000001。用于定位具体某条业务数据。把它们拼在一起就构成了一条 IntentSalesOrder-display。拆开读就是“我想查看一条销售订单它的编号是 1000001”。应用内部的路由 Hash 可以继续追加在后头。比如#SalesOrder-display?SalesOrderID1000001/LineItems其中/LineItems就是应用内部页面路由。这种“Intent inner hash”的组合方式让同一个 URL 既能表达业务意图又能记录应用内部页面状态用户刷新浏览器之后还能恢复现场。2.2 为什么不直接用普通 URL 而是用语义对象你可以会问既然是跳转我直接在代码里写window.location.href /sap/bc/ui2/flp#xxx页面不也能到吗能到但维护成本完全不同。用 Intent 而不是硬编码 URL核心好处是解耦。登录 Launchpad 的调用方只关心“我要打开一个销售订单的显示页面”至于这个意图由哪个 Fiori 应用实现调用方不关心。管理员在后台维护 Target Mapping 时可以把 Intent 指向 A 应用过段时间再改成 B 应用所有入口都不用改代码。这就好比 Java 开发里面向接口编程——调用方依赖的是接口定义而不是某个具体实现类。这种设计也简化了跨系统集成。Shell Hash 里的 Intent 本身是纯文本参数可以被外部系统拼接出来嵌入到企业门户、第三方 OA 或即时通讯里不需要对方了解目标应用的内部页面结构。对方只需要知道“语义对象 动作 业务主键”就够了。2.3 Semantic Object 与 Action 的命名约定命名这件事看起来小实际偏差会害死人。我在项目里见过因为语义对象大小写不统一导致磁贴点击后无法匹配 Target Mapping 的案例排查起来非常费劲。建议团队从一开始就定下规范Semantic Object 统一使用业务对象的英文名称首字母大写驼峰比如SalesOrder、Customer、Material。Action 统一使用小写动词比如display、create、edit、approve。参数名统一使用业务字段名通常就是 OData 服务里的主键字段名比如SalesOrderID、CustomerID。不要使用技术组件名作为语义对象。比如把“Z_ORDER_REPORT”这种程序名塞进语义对象等于把业务语义和技术实现绑定在一起违背了这套设计的初衷。命名规范一旦定好后续的 Cross-App 跳转写起来会非常顺。调用方看到Contract-display?ContractID...不用查文档就知道这是在打开合同展示页。3. 从磁贴点击到应用启动Target Mapping 与参数的完整链路3.1 Target Mapping 是导航配置的心脏Intent 只是“愿望”Target Mapping 才是“实现”。在 Fiori 的实施术语里Target Mapping 承担着把 Intent 绑定到具体应用实例的责任。简单理解就是一张映射表配置项含义semanticObject语义对象和 Intent 中的对象一致semanticObjectAction动作和 Intent 中的动作一致application要打开的具体 Fiori 应用 ID或者外部 URLopenMode打开方式当前窗口或新窗口parameters参数定义支持必填参数和可选参数用户能看到的磁贴Tile和 Target Mapping 是两个层面的东西磁贴负责“展示”Target Mapping 负责“路由”。一个 Target Mapping 可以被多个磁贴引用也可以被应用代码直接触发不需要经过磁贴。3.2 参数的配置与传递细节参数的传递有两种典型方式。第一种是调用方携带参数磁贴配置里定义默认参数或者应用代码跳转时显式传入参数最终拼在 Intent 的 query string 里。第二种是 Target Mapping 里配置参数映射将外部的参数名映射到应用内部的参数名。这里要特别注意“必填参数”的配置。如果 Target Mapping 里把一个参数标记为必填而调用方没传FLP 会在导航时就报错或者中断跳转。我在项目里遇到过一个场景某个应用的展示页需要同时接收订单号和公司代码但公司代码只在某些入口会传过来结果用户在几个特定磁贴上点击时频繁报参数缺失。后来排查才发现管理员在配置时把参数严格设成了必填实际上部分业务场景是不需要按公司代码过滤的。最终把参数改为可选再在应用里做容错处理才解决。参数在 URL 传输过程中全部是字符串数字、日期类型最后都要在应用侧自行转换。后面我会专门讲这个坑。3.3 接收端如何拿到 Intent 参数应用接收到 Intent 后参数获取方式分两种情况。如果是 SAP Fiori Elements 应用框架会在 manifest.json 的 routing 配置里自动把 Intent 参数映射到路由参数和页面绑定上下文开发时只需要在注解或 manifest 里配置对应字段即可。如果是自由式 SAPUI5 应用常见做法是监听 Shell Hash 变化通过sap.ushell.Container.getService(URLParsing)解析出 Intent 参数。老项目里也有直接window.location.hash手动切割字符串的做法。代码大致长这样var oURLParsing sap.ushell.Container.getService(URLParsing); var oShellHash oURLParsing.parseShellHash(window.location.hash); if (oShellHash oShellHash.parameters) { this._sSalesOrderId oShellHash.parameters.SalesOrderID; }这里有个重要细节Shell Hash 和应用内部 hash 是两段URLParsing 会把 Intent 参数单独解析出来不会把内部路由的参数混进来。所以如果应用内部 Router 也用了同名参数才会产生覆盖问题否则两个层级的参数是隔离的。4. Cross-App 导航跨应用跳转的代码套路与传参边界4.1 发送端用导航服务而不是硬编码跨应用跳转的标准做法是调用sap.ushell.Container提供的CrossApplicationNavigation服务。写起来并不复杂onOpenContract: function () { var oCrossAppNav sap.ushell.Container.getService(CrossApplicationNavigation); oCrossAppNav.toExternal({ target: { semanticObject: Contract, action: display }, params: { ContractID: this._sContractId } }); }调用toExternal之后FLP 会根据Contract-display这个 Intent 去匹配 Target Mapping匹配成功后带参打开目标应用。调用方完全不关心目标应用是 SAPUI5、Fiori Elements 还是一个外部网页只要对方配置好了导航就能通。这里我强烈建议加一个环境判断。本地开发或独立运行应用时sap.ushell很可能不存在直接调用会报错。加上判断会让代码更健壮if (sap.ushell sap.ushell.Container sap.ushell.Container.getService) { // FLP 环境走跨应用导航 } else { // 非 FLP 环境回退到应用内部逻辑 }这个细节在我自己维护的多个应用里都用上了。理由很简单本地调试时不需要起整个 FLP应用也能通过内部路由跑起来一旦误触发跳转也不至于整个页面直接报错。4.2 接收端与外部 URL 场景目标应用在接收端不需要额外写“接收 Cross-App 导航请求”的代码因为 Cross-App 导航最终也是拼成一条 Intent由 FLP 把应用重新拉起。也就是说接收应用感知到的和用户从磁贴进来是一样的都是拿到一个 Intent 参数集合。但如果 Target Mapping 里配置的是外部 URL接收端就不是 SAPUI5 应用了。此时 FLP 会把 Intent 参数拼到外部 URL 的 query string 后面。外部系统如果支持按参数渲染详情页就能实现“从 Fiori 跳转外部系统并携带上下文”的集成效果。这种模式在对接自研系统或第三方 Web 应用时非常常见。4.3 传参规则哪些数据走 URL哪些不该走 URL跨应用导航的参数会经过 URL 拼接所以它天然适合轻量、幂等、短小的业务主键数据。比如订单号、客户编号、合同编号。这类数据传过去之后目标应用通常还要回源查询业务数据不需要在 URL 里塞一大坨完整对象。反过来复杂的业务上下文、用户选择的多条记录、临时计算的中间结果就不适合硬塞在 URL 里。URL 长度本身有限制而且长期暴露在浏览器历史和服务器日志里敏感数据不适合这样传。我在项目里的经验是凡是能通过一次 OData 查询拿回的数据都只传主键凡是一次请求拿不回、且临时性很强的中间态优先走 sessionStorage 或后端暂存返回时清掉。比如从一个报价应用跳转到订单创建应用需要带一整份选中的报价行项目明细我就先把这份明细写入一个临时存储区域只把临时存储的标识符通过 URL 传过去。目标应用根据标识符读取明细用完之后清理。这样 URL 简洁也不会有数据长度爆炸和编码转义的问题。5. Inner-App 导航应用内部路由和 FLP 的边界该划在哪5.1 UI5 Router 在 FLP 环境下的真实形态每次从磁贴或者 Cross-App 跳转进入一个应用之后应用就进入了自己的路由世界。SAPUI5 的 Router 通过 hash change 驱动页面切换这是标准的 Inner-App 导航。在 FLP 环境里应用内部的 hash 其实是以“内嵌段”的形式存在的。整体浏览器 hash 大致长这样#SalesOrder-display?SalesOrderID1000001/LineItems 或带更多层级 #SalesOrder-display?SalesOrderID1000001/Items/10086/之后的部分属于应用内部路由由 UI5 Router 负责。它在启动时从 Shell Hash 中剥离出内部 hash再交给路由配置解析从而恢复应用内部页面栈。这种嵌套模式带来一个很实际的好处用户把一个带内部页面状态的 URL 复制给别人对方打开后不仅能回到同一个业务对象页面还能落在具体的内部子页面。不过这也带来一个教训——内部路由参数不应包含完整业务对象关键字否则会让整个 URL 变得冗长且难以阅读。5.2 返回按钮为什么常常“回不去”这是 Cross-App 导航最常被吐槽的一个问题。很多业务用户反馈从应用 A 跳到应用 B处理完之后点浏览器返回结果直接退出了 Launchpad而不是回到应用 A。原因不复杂跨应用跳转本质上是一次“重新导航”浏览器历史栈里并没有保留“应用 A 中的某个页面”这样的完整记录。应用 A 的页面状态在前一次跳转后可能已经结束Launchpad 只是通过 Intent 把应用 B 拉了出来。这时按浏览器返回历史栈退无可退自然就离开了 Launchpad。要解决这个问题不能寄希望于浏览器返回键而应该在产品设计上做补偿。一种是目标应用里自己放一个“返回”按钮点击后用 Cross-App 导航显式跳回源应用的 Intent参数也从之前缓存里取回。另一种是使用 FLP 提供的导航回退能力让 Launchpad 自身维护一层业务回退历史。具体 API 在不同版本里名字略有差异但核心思路是一致的回退行为要由业务层面显式控制不能依赖浏览器原生历史栈。5.3 怎么判断这段页面流应该走 Inner 还是 Cross这个判断直接决定了导航代码放在哪、参数走哪一层、返回行为如何设计。我的划分标准有三个业务对象是否变化。同一个业务对象内部的不同层级页面如订单头和订单行项目走 Inner-App。不同业务对象之间如从订单到合同、从合同到发票考虑 Cross-App。应用归属是否变化。哪怕业务对象相近如果物理上属于不同的 Fiori 应用那就要走 Cross-App由 FLP 解析对方应用的 Intent。用户心智是否是“另一个任务”。用户进入详情页继续看详情是一个任务用户需要去另一个页面做一件事再回来是两个任务导航层级就该分开。遵循这三条规则应用与应用的边界会清晰很多也不会出现一个应用越做越大、塞进一整套无关功能的情况。6. 实际项目里的导航问题排查与处理6.1 磁贴点了没反应先看网络请求再查配置这是我碰到的最高频问题之一。一个磁贴配好之后点击毫无响应既不进应用也不报错。排查链路我梳理成四步第一步打开浏览器开发者工具切到 Network 面板再点磁贴。观察有没有发起对口应用的 manifest 请求。如果完全没请求说明 FLP 在配置解析阶段就中断了。第二步看 Console 有没有和 intent 相关的报错比如提示找不到匹配的 Target Mapping。第三步到 Launchpad 管理界面检查 Target Mapping 里的 semanticObject 和 semanticObjectAction 是否和磁贴配置完全一致尤其注意大小写和拼写。第四步检查当前用户是否被分配了对应角色和目录组。没有权限时磁贴可能压根不渲染或者点击后被权限校验拦下。有一次我排查了很久最后发现是语义对象名里多了一个空格肉眼几乎看不出来。复制粘贴配置的时候最容易出这种问题建议这类字段一定要从服务端配置里统一复制不要手敲。6.2 参数变成 undefined类型、大小写和编码参数丢失也是高频坑。好不容易跳转成功目标应用读参数时却是 undefined。我总结下来主要是三类原因参数名大小写不一致。Intent 里传的是SalesorderidTarget Mapping 和接收代码里用的是SalesOrderID匹配失败。这个最隐蔽因为 URL 本身对大小写校验有时候并不严格。必填参数没传。导航当时能通但应用内部依赖某个参数做 OData 查询没拿到参数渲染出异常页面。类型被 URL 字符串化。参数到应用侧永远是字符串拿去做数值比较或日期格式化之前必须先转换。我写接收逻辑的习惯是var iSalesOrderId parseInt(oShellHash.parameters.SalesOrderID, 10); var oDate new Date(oShellHash.parameters.PostDate);顺便提醒一句涉及中文、特殊符号、长文本的参数跳转前要做 URL 编码处理否则某个字符被截断会导致接收侧解析错乱。我一般用encodeURIComponent统一处理目标应用侧再decodeURIComponent还原。6.3 跨应用返回设计一个不丢上下文的后退跨应用返回的问题前面讲过原理这里给一个可落地的处理方案。源应用在跳转前把当前业务上下文记录到sessionStorage里键名约定好比如srcIntent和srcParams。目标应用页面上提供“返回”按钮点击后用 Cross-App 导航服务读取sessionStorage构造 Intent 跳回源应用。// 目标应用内返回 var oCrossAppNav sap.ushell.Container.getService(CrossApplicationNavigation); var sSrcIntent sessionStorage.getItem(srcIntent); var sSrcParams JSON.parse(sessionStorage.getItem(srcParams) || {}); if (sSrcIntent) { var aParts sSrcIntent.split(-); var oTarget { semanticObject: aParts[0], action: aParts[1] || display }; oCrossAppNav.toExternal({ target: oTarget, params: sSrcParams }); }这样设计之后用户从应用 A 跳去应用 B可以从 B 里主动返回 A并且 A 的上下文还在体验上就是一个完整的任务闭环。跳转成功后把sessionStorage里的临时记录清理掉避免下次串上下文。6.4 判断 FLP 环境是否存在本地调试与生产通吃最后一个实用技巧也是我自己一直遵守的。凡是和导航相关的代码都要先判断当前是否运行在 FLP 环境里。方法很简单function isRunningInFLP() { return !!(window[sap-ushell] sap.ushell sap.ushell.Container); }本地独立运行应用时代码还能走内部路由和普通跳转不至于一加载就报错。这个判断也能规避很多“本地可以、上 FLP 就挂”的诡异问题因为本地没有 Shell 容器服务直接调用跨应用导航 API 一定会抛异常。我见过不少项目把本地调试和应用内导航耦合在一起最后每次调试都要硬起一个完整的 Launchpad 环境效率非常低。这让我在每次启动 Fiori 项目时都习惯先做一件额外的事把业务顾问梳理好的业务对象清单拿到手和开发团队一起给每个业务对象定义标准化的语义对象名和动作名再对照这份清单确定哪个页面流属于应用内部、哪个跳转需要走跨应用。导航设计这件事如果等到页面都做完了再补基本就是重新返工反过来前期把导航地图画明白后面新增应用、替换功能都会轻松很多。调试真出问题时也可以直接在浏览器地址栏里改 Shell Hash——把#SalesOrder-display手工改成#Contract-display再回车几秒钟就能判断 Intent 配置和参数解析是否正常比反复点磁贴来回切换快得多。
