Fiori Launchpad 这个入口,很多人第一眼觉得它就是个“应用菜单聚合页”,点磁贴进应用、返回、再点另一个磁贴。真到自己上手做开发或实施的时候,才发现导航这件事并没这么简单:为什么有些应用跳转要靠#SalesOrder-display?SalesOrderID=1000这样的 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 Mapping |
| Cross-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-client=100#SalesOrder-display?SalesOrderID=1000001#号后面的部分才是关键,它由三段拼成:
- Semantic Object(语义对象):
SalesOrder。它代表“销售订单”这个业务对象,是一个业务语义层面的分类。 - Action(动作):
display。表示用户想对这个对象做什么,常见的还有create、edit、approve、maintain。 - Parameters(参数):
SalesOrderID=1000001。用于定位具体某条业务数据。
把它们拼在一起,就构成了一条 Intent:SalesOrder-display。拆开读就是“我想查看一条销售订单,它的编号是 1000001”。
应用内部的路由 Hash 可以继续追加在后头。比如#SalesOrder-display?SalesOrderID=1000001&/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,或者外部 URL |
| openMode | 打开方式:当前窗口或新窗口 |
| 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?SalesOrderID=1000001&/LineItems 或带更多层级 #SalesOrder-display?SalesOrderID=1000001&/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 里传的是
Salesorderid,Target 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 配置和参数解析是否正常,比反复点磁贴来回切换快得多。