聊到 SAP Fiori 启动时加载数据,几乎每个做到第N个页面的开发者都会卡在同一类问题上:代码里明明没写多少请求,应用打开却半天白屏;本地沙盒预览跑得飞快,接到真实 Gateway 就报 403 CSRF;想优化首屏,却连启动阶段到底是谁先发了请求、哪些请求该等、哪些请求不该等都没理清。这篇文章我想把这件事拆开讲透,同时把沙盒启动、真实环境差异、启动期排错顺序这些经常被忽略的细节一起过一遍,适合刚接手 Fiori 页面开发、或者正在做既有应用启动优化的朋友参考。
1. 把“启动”拆开看:从点击磁贴到页面可操作,到底经过了几层
很多开发者的第一反应是直接打开 controller 的 onInit,觉得“启动时加载数据”就是在初始化方法里写一次 read。这种理解没有错,但远远不够完整。SAP Fiori 应用从用户点击 Launchpad 磁贴,到页面真正呈现可用,中间至少叠加了资源加载、框架初始化、OData 服务解析、视图创建与绑定解析、业务代码主动取数这几层。每一层都可能产生网络请求,也都可能成为“启动慢”的直接原因。
1.1 启动不是一个瞬间动作,而是一条请求链路
一次典型的应用启动,可以粗略分成下面几个阶段。
首先是静态资源加载。Launchpad 打开应用时,不管是通过 Component 方式加载,还是独立 HTML 页面启动,都需要先载入 UI5 框架相关的 JavaScript、CSS,以及你项目的 Component-preload.js。这一个阶段如果白屏,通常和真实网速、CDN 缓存、preload 打包是否完整有关,和 OData 后端关系不大。
其次是框架初始化。UI5 会读取 manifest.json,解析 models、dataSources、routing、i18n 等配置。这里有个很关键的机制:manifest.json 里如果配置了 OData 数据源,框架并不会立刻把业务数据全部拉下来,但很多情况下模型被实例化后,会触发对 OData 服务元数据也就是 $metadata 的解析请求,尤其是 OData V2 模型在第一次执行请求前,往往需要先拿到 metadata 才能构造正确的请求。这个请求如果服务端响应慢,页面几乎不会有任何反馈,用户看到的就是持续转圈。
第三个阶段是视图创建。路由或懒加载策略决定了启动时真正实例化哪几个视图。每次视图实例化都会经过 controller 的 onInit,同时界面上的绑定会自动触发对应的 OData 读请求。注意,这里有两种请求并发出现:一种是你在 onInit 里手动写的 read,另一种是界面控件绑定在视图渲染时自动触发的请求。后者经常被忽略,但它往往是启动时请求数量爆炸的元凶。
最后才是业务代码里那些看起来像“启动加载”的逻辑。比如在 Component 或者 controller 初始化时读取用户偏好、根据登录用户身份判断功能权限、访问一次配置表准备下拉项。这些请求如果放在启动链路里,会变成所有页面都必须等待的一部分。
四个阶段叠加在一起,启动慢的原因就变得很复杂了。只盯着 onInit 优化,就好比堵车时只检查自己车门有没有关好,方向不太对。
1.2 框架自动请求和业务代码请求要分清楚
建议拿到任何一个 Fiori 应用,第一步都是打开浏览器开发者工具,在 Network 面板看从应用启动到首屏渲染完成这段时间,到底发出了哪些请求。在 SAP 场景里,OData 服务请求通常以 /sap/opu/odata/sap/ 开头,也可以按 /sap/bc/、/sap/opu/odata4/ 等路径过滤。
下面这个表格是启动阶段最常见的几类请求,它们的触发来源、出现时机和控制方式差别很大。
| 请求类型 | 谁触发 | 典型出现时机 | 主要影响 |
|---|---|---|---|
| OData metadata | OData 模型框架 | 模型实例化后,第一次真正需要解析服务结构时 | 决定首屏是否长时间无响应 |
| annotation 注解 | Fiori Elements 框架 | 应用打开时,框架尝试读取 UI 注解 | annotation 过多会明显拖慢启动 |
| CSRF token 获取 | OData 模型内部 | 第一个写请求发送前 | 通常不会在纯查询应用出现 |
| 业务列表绑定 | 视图中的 table/list 控件 | 视图首次渲染 | 数据量越大、没设分页时越严重 |
| 手动 read | controller 的 onInit | 控制器初始化 | 代码里能直接看到,定位容易 |
区分自动请求和手动请求的价值在于:框架自动请求往往被误认为是“网络太慢”,但事实上它是由于模型设置、视图绑定、注解文件加载等问题引起的,通过代码层面调整往往能立竿见影。比如 OData V2 模型在请求数据前需要 metadata,如果每个页面视图或每个组件都创建了一个新的 ODataModel 实例,可能导致同一份 metadata 被反复请求。我在很多项目里发现,同一应用里多次实例化相同数据源,是启动请求数增多的常见原因之一。
解决方向也很简单:优先把 OData 模型注册在组件级,通过 manifest.json 的 models 配置定义一个共享模型,而不是在每个 controller 里 new ODataModel。共享模型从机制上避免了多条 metadata 请求,也让视图绑定能复用同一个模型实例和缓存。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 启动时该拿和不该拿的数据:先给首屏做一个数据取舍
“启动时加载数据”这个需求,背后真正的问题通常不是“怎么加载数据”,而是“启动时应该加载哪些数据”。把全部可用数据一股脑塞进启动链路,是很多 Fiori 项目后期性能失控的开始。启动数据应该满足一个原则:没有它,首屏就无法给用户一个可用的状态;有了它,用户可以立即开始主要工作。其余数据能后置就后置。
2.1 一条数据该不该进启动链路,用三个问题判断
我判断一条数据是否属于真正的启动数据时,一般问自己三个问题。
第一,如果这个请求不回来,首屏界面能不能正常渲染出来并且可用?比如一张总览列表的数据,如果不回来列表区域可能是空的,用户无法开始任何操作,那它是启动数据。但如果只是页面右上角一条公告、一个倒计时,或者一个页脚统计信息,那它完全可以等几毫秒再加载。
第二,这个数据是不是只会在用户执行某个后续动作时用到?比如点击一行之后才需要查看的明细关联信息、按下某个筛选按钮后才会拿到的统计口径,这些数据放进启动时加载,只会延长首屏等待,还会额外占用宝贵的服务端资源。正确做法是把它放在对应交互事件里,需要时再读。
第三,这个数据是用户会话级、配置级,还是业务级、海量级?用户信息、权限配置通常会很小,可以先拿;而像物料、库存余额这类业务明细数据,如果无脑全量读,启动基本都会卡死。业务级数据必须配合默认查询条件、分页、过滤,而不是在启动时就把整张表搬下来。
2.2 Fiori Elements 和自由页面,处理启动数据的方式完全不同
自由开发的 Fiori 页面,你对数据读取有完全的控制权,onInit 里不写 read,视图不绑定数据源,就不会有请求。但在 Fiori Elements 页面里,启动数据的加载路径没那么直白。
以 List Report 为例,应用的数据展示字段、默认排序、过滤条件大多由注解文件定义,比如 UI.LineItem 决定列表展示哪些列,UI.SelectionFields 决定筛选区出现哪些字段。当你打开一个 List Report,框架本身会在解释注解后触发对应实体集的读请求。如果想让首屏启动更快,不是简单改 onInit,而是要看注解配置是否合理:展示字段是否过于宽泛、默认筛选是否能缩小查询范围、页面是否会因注解设计而对某个关联对象做深层次解析。
很多做传统 SAP 实施的朋友,第一次接触 Fiori Elements 时会觉得框架“不可控”,本质就是因为这里的启动加载从“写代码”变成了“配置数据”。你调整的不是程序执行顺序,而是数据模型、注解和筛选条件这些描述文件,框架根据这些描述自动决定何时、如何加载业务数据。
2.3 那些用事务代码做得很重、搬到 Fiori 后必须重新设计的场景
举个很现实的例子:把 MD07 这类库存汇总事务搬上 Fiori 时,很多顾问的需求描述是“打开页面就看到所有工厂的库存汇总”。这个需求如果直接翻译成技术实现,就是启动时加载全部工厂、全部物料的库存数据,结果基本可想而知——数据库压力爆炸,页面白屏好几分钟,然后用户开始抱怨 Fiori 不如 GUI 快。
我习惯的处理方式是先做数据取舍,把需求改造成“打开页面时,按默认工厂 + 默认日期范围加载今天的 Top 50 条库存记录”,让用户看到首屏后,再自行修改工厂选择或物料筛选。这不是把功能做少了,而是把功能做可用。最终用户感受到的“启动速度”会好很多,真实查询压力也大幅下降。
如果你的 Fiori 页面确实需要启动时带一些默认条件,可以考虑利用 Launchpad 磁贴或 URL 参数把用户上次选择的工厂、物料、日期范围传进应用,再在 Component.js 或 controller 初始化时读取这些参数作为默认查询条件。这样既满足了“启动就带数据”,又没有真的去全量加载。
3. 项目里我常用的加载策略:首屏聚合、视图懒加载和 OData V4 的默认行为
聊完该拿什么数据,再来聊落地手段。启动加载数据这件事,最终会落在几个常规动作上:把请求放在正确的时机、把非首屏数据拆出去、利用框架特性减少并发和重复请求。
3.1 首屏如果必须读多个数据,尽量让它们聚合在合理时机
假设一个页面启动时必须先展示用户名、用户可用的工厂列表,以及默认工厂对应的库存,这时候就会同时出现三个 OData 请求。自由页面中,这三个请求如果用三个并行 read 各发各的,启动请求数直接变成三份。OData V4 模型通常会把同一应用模型下面的多个读请求自动合并到 $batch 中,因此首屏多个绑定请求在浏览器里的表现往往是一批聚合请求。
但要注意另一个极端:如果放在一个 $batch 里的子请求太多,某个子请求异常会导致整批返回时间变长,后端也可能限制单批大小。我见过部分后台系统对 $batch 请求大小有约束,超过后请求直接失败。这类情况下,宁可把最影响首屏的请求单独放,把不那么关键的请求拆到另一个 group,或者延后到用户操作时再发起。
在 OData V2 自由页面里,$batch 并不总是默认开启。如果你发现一个应用启动时发出大量独立 HTTP 请求,没有批量聚合,而且你无法很快升级到 V4,可以考虑服务端是否支持 OData batch,再决定是否在代码里手动合并读取。不要为了合并而强行套一个自定义 batch 封装,那样经常引入新的序列化问题。
3.2 基础数据尽量用组件级模型和启动参数注册,而不是每个页面手动维护
有个很常见的现象:同一个工厂下拉值,在物料页、采购页、库存页里各自被读取一遍。如果三个页面都由同一组件路由控制,每次页面切换还会重复请求一次,用户感知就会变成“启动一次慢一次”。
我的做法是先把这类“用户会话级别”的数据,比如工厂列表、权限范围、用户偏好,放到组件级统一管理。应用启动时,在 Component.init 或第一个视图加载前把数据放进 JSONModel,页面需要时直接取本地 JSONModel。这样做的好处不只是快,更重要的是保证多个页面看到同一份基础数据,不会因为请求先后不同而出现下拉选项不一致。
对于需要在启动阶段明确读取少量服务参数的情况,也可以在 Component 的 init 中先做一次轻量读取,避免把这份请求延迟到第一个 controller 的 onInit 之后。尤其当后续页面的控件初始化依赖这个参数时,提前读取可以避免视图先渲染、数据后回来导致的“空一下又跳出来”的体验问题。
3.3 非首屏内容用懒加载,不只是省流量,更是给启动减少阻塞
从路由配置层面,SAPUI5 允许把部分视图设置成懒加载。很多 Fiori 项目默认把 Master、Detail、Create 页面都放进启动链路的 target 列表里,导致应用打开时连那些用户可能根本不会看的页面都被实例化,对应的数据绑定和 controller 初始化代码也会执行。正确做法是明确哪个页面是启动后第一时间展示的,其他 target 设置 lazyLoading,让框架只在首次导航到该视图时才实例化它。
在自由页面里,如果一个页面中某个区块是用户点击 Tab 或展开折叠面板才会看到的,可以采用“需要时再初始化控件或绑定数据”的策略。最直接的方式是在展示该区块时才给容器设置模型或调用 read,而不是在父页面启动时就把所有区块的数据都准备好。
提示:懒加载能减少启动时的初始化范围,但真正让用户有感的是“首屏主任务”所需请求保持较小数量。不要为了懒加载而把一个正常的表单拆得过于细碎,否则会从启动慢变成交互时频繁加载,整体体验不一定更好。
3.4 OData V4 模型下,启动加载的策略已经不一样了
如果你的 Fiori 页面运行在 S/4HANA 或 BTP ABAP 环境,并且服务是基于 OData V4 的 CDS View,启动加载体验和传统 OData V2 差距非常明显。V4 模型在绑定层面做了大量优化,默认会在批次请求中打包多个读取,还会利用上下文缓存避免同一个实体的重复请求。启动时如果能看到某个 /odata4/ 开头的 $batch 聚合请求,其中包含多个 GET,就是 V4 模型在自动工作。
但这不意味着 V4 不需要人去思考和干预。V4 模型中,如果页面里的绑定设计得不好,比如一个列表 Item 中嵌套显示多个关联属性,框架可能自动生成 $expand,把一段很深的数据链在启动时全部取回,请求体量会急剧膨胀。遇到这种情况,我在设计 CDS View 时就会注意控制嵌套深度,同时审核注解里展示字段对应的关联对象。业务数据模型的嵌套深度最终一定会反映到启动请求的效率上。
4. 本地沙盒一切正常、连真实网关就 403 CSRF:启动期接口报错的根源与定位
很多 Fiori 开发都在“沙盒启动”状态下工作,模板自带 mock 数据,本地跑起来页面秒开,一切岁月静好。等到部署到 Fiori Launchpad、接过真实 Gateway 服务后,第一次打开页面可能就直接报 HTTP 403,错误信息里出现 CSRF 字样,开发者的第一反应往往是“后端配置有问题”,但实际情况比这稍微复杂一点。
4.1 沙盒预览和 FLP 磁贴启动,本质上是两套不同的运行环境
本地沙盒预览时,最常见的形式是用 UI5 Tooling 直接打开应用的 index.html,使用 mock server 拦截 OData 请求。这种模式下,网络请求没有真正走到 S/4HANA Gateway、没有跨域问题、没有标准登录会话,也不存在后端服务激活状态的问题。如果你在沙盒里的操作路径是“直接双击本地页面”而不是通过 Launchpad 标签页启动,那你测试的其实是一个剥离了 FLP 外层框架、脱离真实网络环境的简化版本。
真实环境里,应用托管在 Fiori Launchpad,请求的是网关真实服务。打开页面时先请求 /sap/bc/ui2/ 或者 /sap/opu/odata/ 等路径,中间会经过 SSO、反向代理、IAG、网段安全策略等多层。很多本地看不出来的问题,其实在请求真正到达 OData 服务之前就已经发生了。
因此,排错的第一步,是不要在沙盒里反复调 mock 数据,而是去真实环境确认:
- 服务在 ICF 中是否激活;
- 登录用户是否有对应 OData 服务角色权限;
- 应用和网关是否在同一域名体系下,Session Cookie 是否能够正常传递;
- 是否存在反向代理对 HTTP 方法或请求头做了限制。
4.2 为什么启动加载会和 403 CSRF 扯上关系
CSRF 防护机制通常和写请求相关。OData 模型的写请求前,会先发送一个带 X-CSRF-Token: Fetch 头的 GET 请求去后端领取 Token,再用这个 Token 作为后续 POST/PUT/DELETE 请求的请求头,用于证明请求来自当前登录会话。
很多 Fiori 应用在启动阶段会做的事情,其实不是查询,而是写操作。比如某些系统为了记录操作日志,会在页面启动时向 OData 服务发送一条创建日志的请求;或者应用打开时自动为草稿数据建立会话;再比如有些场景需要在初始化阶段为当前用户创建一条上下文数据。这些写请求一旦出现在启动链路,就天然进入 CSRF 防护逻辑。如果 Token 领取失败、跨会话使用、或者请求头没有正确携带,
