ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

RAP Side Effects 实战:从行为定义根治 Fiori 局部刷新难题

2026/10/2 3:07:39 拓冰建站 浏览量
RAP Side Effects 实战:从行为定义根治 Fiori 局部刷新难题 做 Fiori 开发的同行应该都有过这种经历用户在详情页改了一个下拉框页面上另一个关键字段死活不更新业务顾问一口咬定“系统坏了”你跑到前端 Fiori 调试器里翻 controller找到一处手写的oModel.refresh()战战兢兢塞进某个事件回调里。问题看似解决了但下一个页面、下一个字段、下一个项目同样的戏码还要重演。直到我真正把 RAP Side Effects 建模思路吃透之后才意识到局部刷新这件事本来就不该靠前端打补丁而应该在 ABAP RESTful Application Programming ModelRAP的行为定义层面把它当成一等公民去建模。这篇文章就把我在这方面的踩坑过程、建模思路和落地套路完整写出来给正在做 Fiori 开发、被局部刷新反复折磨的朋友一个可复制的参考。1. 为什么局部刷新总是被当成“前端杂活”来做三个根因1.1 根因一OData 是无状态的前端根本没有“字段依赖”概念Fiori 应用本质上是在和 OData 服务对话。OData 这个协议的设计初衷是无状态的前端发一个GET请求后端返回一份数据前端发一个PATCH请求后端把数据改掉再返回被修改的实体。两边没有任何“连接”后端也不会主动告诉你“喂你改了 A 字段之后B 字段其实已经变了”。这就像你打电话问客服一个问题客服答完就挂了电话。你想知道另一个关联信息只能再打一次电话。传统 SEGW 时代尤其明显所有联动逻辑都要靠前端自己猜先判断用户改了哪个字段再手动去读依赖字段最后把返回值塞进页面模型里。猜对了界面正常猜错了页面数据就是旧的。RAP 出来之后后端的建模能力大大增强但很多人写代码的思路还停留在 SEGW 时代。行为定义里的determination和validation都在后端把逻辑算好了可结果没同步给前端用户界面上看到的还是旧值。然后前端同事就被拉去“修 bug”实际上问题出在缺一个“通知机制”。1.2 根因二SAPUI5 controller 里塞满了“手工 refresh 补丁”我以前接手过一个老项目打开一个 Fiori 列表报表的 controller前二十行几乎全是刷新补丁onAmountChange: function (oEvent) { var oModel this.getView().getModel(); oModel.setProperty(/TotalAmount, newTotal); // 或者更粗暴的全量刷新 oModel.refresh(); }这种代码有四个非常现实的问题。第一oModel.refresh()会把整个实体集合从头读一遍Fiori 界面瞬间冒出一个全屏 loading 遮罩用户连续操作时页面会闪跳体验非常糟糕。第二手写setProperty直接把前端计算出来的值塞进模型这个值未必是后端算出来的。业务逻辑一变比如审批规则从“总金额大于 5000 走二级审批”改成“大于 8000 走二级审批”前端代码要跟着改后端逻辑也要改改完两边还得对得齐。第三这种代码完全无法复用。每个页面、每个 controller、每个事件回调都是独立的一份项目里出现了“搜索 Refresh 代码找联动逻辑”的荒诞操作。第四也是最容易被忽略的手写刷新会绕过 OData 的并发检查和校验逻辑。你setProperty塞进去一个值后端可能根本没认可这个值等到保存时才发现报错用户早就对着一堆“看起来正确”的假数据操作了半天。1.3 根因三一个联动需求跨了“谁改、谁算、谁显示”三层局部刷新这件事拆开来看其实涉及三层。第一层是“谁改了”——前端用户改了某个字段。第二层是“谁算”——后端需要基于这个改动重新计算某些字段或状态。第三层是“谁显示”——前端要把计算结果拿回来重新渲染。传统开发的问题是这三层分别由前端和后端两批人、两套代码实现中间没有任何契约。后端改了 determination前端不知道前端改了刷新逻辑后端也不关注。两边各自为政最后联调的时候才发现对不上。更麻烦的是很多项目的“第三层”根本没有统一的实现方案。有人用afterSave回调有人在onChange里读有人在视图事件里监听。一个应用里同时存在三种刷新风格新同事接手后根本不知道该按哪种风格继续写。所以我说局部刷新不是“前端杂活”它的本质是建模问题。只要把“谁改了字段之后哪些字段需要被重新读取”这件事明确地声明在 RAP 的行为定义里三层自然就打通了。2. Side Effects 的设计定位它不是计算器是“信号灯”2.1 一句话理解告诉前端“数据变了去重读这些字段”RAP 里的 Side Effects直译过来是“副作用”这个翻译多少有点吓人其实它的设计意图非常朴素在行为定义里声明一张“依赖清单”——当某个字段被修改、某个 action 被执行之后哪些字段需要重新读取。它本身不负责计算。比如你改了费用明细的金额期望“总金额”字段跟着变。真正算出“总金额”新值的是后端的 determination真正负责把人放进画面的是前端框架。Side Effects 在这里扮演的角色就相当于红绿灯它不生产车辆也不指挥司机它只告诉你“现在可以走了”或者“该停了”。用个更生活化的类比你在一家餐厅点菜服务员记下你换了一道菜字段变更后厨重新算总价determination但服务员不会主动跑回来告诉你新总价除非你喊一声“帮我看看现在多少钱”。Side Effects 就是那个“自动通知服务员来报价格”的机制——你改了菜单服务员马上过来把新总价念给你听。有了这个信号灯前端 Fiori Elements 框架就会在字段 change 时自动发起一次针对依赖字段的读取请求拿到后端处置后的最新值完成局部刷新。2.2 它和 determination / validation 的分工很多 RAP 新手分不清这三个概念我直接给结论determination负责“算”。某个字段被修改或动作被执行时后端自动运行一段逻辑重算相关字段。validation负责“拦”。数据要进入某种状态比如保存、提交之前校验是否合法不合法就报错。side effects负责“通知”。它不计算也不校验只告诉前端“字段 A 变了你应该重新去读字段 B”。三者通常是配合出现的。一个完整的字段联动场景往往是用户改了amount字段determination 里重算totalamount和approvallevelvalidation 在保存时校验新算出来的审批等级是否合理side effects 声明amount - totalamount、amount - approvallevel让前端把后端算好的结果拿回来显示。如果只写 determination不写 side effects那后端确实算出新值了但 Fiori 界面纹丝不动——这就是很多项目里“数据保存后再打开才正确页面上实时看不到联动”的根本原因。下面这张表把三个概念的分工总结清楚了我通常会让团队成员贴在自己工位上机制动词触发时机典型用途缺了会怎样Determination算字段修改、action 执行、保存重算金额、状态、默认值后端数据不更新Validation拦保存、提交、特定字段变更校验必填、校验范围脏数据直接落库Side Effect通知字段修改、action 执行后通知前端重新读取依赖字段界面不刷新用户以为系统坏了2.3 它声明在哪里行为定义是一个集中的“依赖契约”Side Effects 不是写在 ABAP 类里的也不是写在 CDS 视图注解里的而是写在 RAP 的行为定义Behavior Definition里。这个位置选得非常精妙行为定义本来就是描述“这个业务对象能做什么、在什么条件下做”的地方Side Effects 就是描述“做完之后要通知谁”。我见过有人在 CDS 视图的注解里试图加类似的刷新逻辑那是行不通的。CDS 视图负责的是数据模型和投影它不管理行为而行为定义直接和 RAP 运行时绑定Fiori Elements 在运行时能直接读到这份元数据把它转换成前端的自动刷新行为。换句话说Side Effects 实际上是一个集中的依赖契约。它把散落在前端 controller 里的各种 refresh 补丁全部收编到后端形成一份一眼就能看清楚的依赖清单。代码评审的时候不用再去翻十来个 JS 文件找“某字段联动某字段”直接打开行为定义所有联动关系列得明明白白。3. 一次真实落地费用申请单的金额联动和审批人刷新3.1 业务场景与建模目标说这么多不如直接看一次真实落地。我前阵子给一家客户做内部报销系统的 Fiori 应用业务对象是“差旅费用申请单”用 RAP 建模前端是 Fiori Elements 列表报表加对象页。业务顾问提的需求非常典型用户在明细里填写单笔费用金额amount后申请单主表的totalamount要立刻刷新总金额跨过不同阈值审批等级approvallevel也要变化比如超过 5000 变二级审批审批等级变化后对应的审批人approver字段要跟着刷新成该等级的实际审批人用户点击“提交”action 后整体状态overallstatus要从“草稿”变成“待审批”。如果按老套路这些联动至少要在 SAPUI5 页面里写三处model.refresh()或oModel.read()的调用而且还得知道后端到底改了哪些字段。用 RAP Side Effects我只需要在行为定义里做声明。3.2 行为定义里的 Side Effects 声明行为定义的骨架大致如下不同 RAP Release 关键字略有差异以你当前系统帮助为准但主结构一致define behavior for ZR_TRAVEL_APP alias TravelApp persistent table ztravel_app draft table ztravel_app_d lock master authorization master ( global ) etag master lastchangedat { field ( readonly ) travelid, createdat, lastchangedat; field ( mandatory ) employeeid, appreason; action ( features: instance ) submitApp; action ( features: instance ) approveApp; determination calculateTotalAmount on modify { field amount; } validation validateApprovalLevel on save { field approvallevel; } side effects { field amount - { field totalamount; } field amount - { field approvallevel; } field approvallevel - { field approver; } action submitApp - { field overallstatus; } } }注意看side effects块里的写法field amount - { field totalamount; }表示“当 amount 被修改后前端应该重新读取 totalamount”。action submitApp - { field overallstatus; }则表示“当 submitApp 这个 action 执行完之后前端应该重新读取 overallstatus”。这里有个非常容易理解错的地方Side Effects 声明的左侧可以是字段也可以是 action但右侧通常是一组字段。左侧的字段或 action 是“触发器”右侧的字段是“被通知方”。RAP 运行时会处理好这层关系当左侧事件发生时自动让前端发起针对右侧字段的读取。3.3 后端逻辑该怎么配合determination 写在正确的事件上Side Effects 本身不产生数据所以配套的 determination 一定不能漏。我们这个例子里有两个计算点第一是calculateTotalAmount。它需要在一个即时的事件上触发比如字段amount被 modify 之后立刻重算总金额和审批等级。典型写法是在行为实现类里实现一个 determination 方法方法内部循环读取明细行金额累加并更新主表totalamount。第二是validateApprovalLevel。它负责在保存前拦截不合理的审批等级比如某些费用类型不允许二级审批。这里要和approvallevel的默认计算配合好不能出现“界面上显示二级审批但保存时校验规则又把它否了”的矛盾。我在第一次落地时踩过一个很典型的坑只在行为定义里写了side effects没写 determination结果前端确实发起了重新读取请求但后端根本没有新值可读读回来的还是旧值。排查了半天才发现totalamount的计算逻辑只写在了一个老式的SAVE增强里根本没有注册为 RAP 的 determination所以 modify 之后那个时间点数据库和后端内存里都没有新值。正确的配合方式很简单Side Effects 负责“何时通知前端”Determination 负责“何时算好新值”两者触发事件必须对齐。3.4 验证是否生效Fiori Elements 预览和网络面板声明写完之后怎么确认它真的生效我推荐两条路径。第一条用 ADT 里的 Service Binding 打开 Fiori Elements 预览。在对象页里修改amount字段然后盯着“总金额”字段看。如果 side effects 生效totalamount 会在一两秒内自动变化不需要按回车也不需要保存。第二条打开浏览器的开发者工具网络面板观察 OData 请求。修改 amount 后除了正常的PATCH请求你应该能看到后续有一个针对totalamount/approvallevel的读取请求。如果只有 PATCH 没有读请求说明 side effects 声明没被前端消费到如果有读请求但界面上没变化说明后端 determination 没算对或者返回结构对不上。还要强调一点如果启用了 draft 机制我们这个场景是有的大部分联动发生在draft 实体上而不是 active 实体上。这意味着你在行为实现类里操作数据时要区分 draft table 和 active table别把草稿状态的计算结果写到正式表里否则保存前也会出现奇怪的前后不一致。4. 边界与踩坑Side Effects 不是银弹这四个场景要绕行4.1 列表页行内字段的刷新有局限我说的“局部刷新成为常态”在对象页和详情页上很顺但在列表页的行内编辑场景下Side Effects 并没有那么万能。Fiori 列表报表通常展示多个实体实例每行是一个独立的实体。如果你在行内修改了 A 字段希望当前行的 B 字段刷新行为定义里的 field 级 side effects 可以覆盖但如果 A 字段的变化会让当前行相关的其它行字段也变化——比如费用明细某个字段改了导致列表页另一个申请单的汇总也变了——这种跨行的依赖就不是单一的 side effects 能优雅表达的了。实际项目中我遇到最多的情况是行内编辑时依赖字段的刷新请求发出来了但因为该行还处于未保存的编辑状态前端必须把未提交的修改先处理好否则刷新会把用户输入冲掉。Fiori Elements 内部有一套机制在协调这件事但总有一些边界场景会出问题。我的绕行方案是列表页尽量只承载轻量联动重联动放到对象页如果必须做复杂行间联动优先考虑在后端把逻辑沉淀成自定义 action前端触发 action 后做一次受控的读取而不是依赖纯粹的 side effects 自动刷新。4.2 跨 BO、跨实体的依赖不能直接写Side Effects 的作用范围是单个行为定义之内。如果你的依赖关系跨越了两个业务对象——举例来说费用申请单的总金额要刷新同时还要刷新主数据里的“部门预算剩余额度”——这就没法在这个行为定义的 side effects 里直接声明因为“部门预算剩余额度”属于另一个业务对象的字段。这种场景下我建议做三件事在职责上明确“哪个 BO 是刷新动作的发起方”把跨 BO 的读取逻辑封装成一个 function 或 action让这个 function/action 在内部读取关联 BO并把需要展示的字段一起返回在发起方 BO 的行为定义里用 action 级别的 side effects 去刷新那些“能直接显示的返回字段”。跨实体不是不能做而是不要硬把两个 BO 的字段塞进同一张 side effects 依赖清单里。曾经有个同事试图把关联 BO 的字段直接写进 side effects 右侧编译倒是过了但运行时前端请求路径完全不对折腾了两天才发现这个边界。4.3 自定义 SAPUI5 页面不会自动消费 Side Effects这是最容易让团队产生误解的一点。Fiori Elements 是原生消费 side effects 声明的但如果你用的是自定义开发的 SAPUI5 自由应用前端框架不会自动读取行为定义里的 side effects 元数据然后发起刷新请求。换句话说Side Effects 的“自动化”红利主要属于 Fiori Elements。对自由页面你仍然需要在 controller 里监听字段 change 事件自己发起读取。那么问题来了RAP Side Effects 对自由页面就完全没有意义吗也不是。它的声明仍然是一份清晰的依赖契约你可以让前端程序在启动时读取 OData 元数据里的 side effect 描述或者至少让前后端团队对照同一份契约开发避免各搞一套。但从成本和收益来看我的实际建议是复杂业务对象的新页面优先选 Fiori Elements确实需要自由页面时把刷新逻辑集中封装不要散落在每个 controller 的细节里。4.4 并发与 ETag频繁刷新会让 412 找上门RAP 默认使用 etag 做并发控制。我们可以从行为定义里看到etag master lastchangedat这样的声明。当 side effects 触发了重新读取请求而读取请求内部又带着旧 etag 去执行某些修改操作时如果数据已经被其他用户更新过OData 服务就会返回 412 状态码Precondition Failed。我刚开始推广 Side Effects 时遇到过测试人员报告“我改一个字段界面弹提示说数据已被其他用户修改”。排查后发现是测试人员在一个对象页里快速连续修改字段每个字段的 change 都触发了 side effect 刷新而前一个刷新请求还没结束后一个带着旧 etag 的操作就撞了上去。这类问题的处理思路降低 side effects 触发的读取粒度只刷新真正相关的字段不要大范围读取整个实体在 Fiori Elements 页面里确保你用的是框架标准的UI.DataField和字段组展示框架会协调 etag 处理对于自由页面要在前端做请求合并或防抖避免连续 change 事件引发并发刷新。下表总结了四个边界场景和应对策略我每次给团队培训都会发这张表场景表现推荐应对列表行内复杂联动刷新会打乱编辑状态轻量联动用 field 级重联动走自定义 action跨 BO 字段依赖side effects 作用范围不够封装跨 BO 读取逻辑返回字段统一暴露自由 SAPUI5 页面不自动消费声明契约驱动开发或收敛到 Fiori Elements并发 etag 冲突412 错误、刷新覆盖防抖、合并请求、细化刷新粒度5. 让 Side Effects 成为团队常态评审清单和验收标准5.1 建模阶段就该做的事Side Effects 真正要“成为常态”不能只靠一个人会用而是要在团队流程里把它固化下来。我现在的做法是在需求评审环节业务顾问讲完一个交互需求后我额外追加一个问题——“这个页面上改了哪些字段之后必须连带刷新哪些字段”如果业务顾问答不出来就拉着关键用户现场逐一过一遍。这一步看似耗时其实是在为后端的依赖清单攒素材。等这张“字段联动表”整理出来写行为定义里的 side effects 就是照抄工作几乎不会错。同时要避免“只做一侧”开发人员拿到需求后很容易只写 determination、忘了 side effects或者反过来。如果你也在带团队可以在任务拆解时明确加上一条独立 checklist“新增字段变更联动时必须同步确认 determination 触发点和 side effects 声明。”这样从一开始就不会漏。5.2 代码评审清单我把代码评审里的 Side Effects 检查项固定成了五条简单但极为管用联动字段是否都在 side effects 右侧只要 determination 里改了某个字段的值而这个字段需要在 UI 上实时更新就必须出现在 side effects 右侧。触发事件是否对齐determination 里用的是 modify 触发还是 save 触发side effects 里的声明要与之匹配。save 才触发的话就不能指望界面上实时联动。前端是否还有手写 refresh一旦行为定义里有了 side effects 声明前端 controller 里对应的oModel.refresh()/oModel.read()应该可以删除。如果删不掉一定要搞清楚为什么多半是场景超出了 side effects 的边界。action 是否声明了输出刷新新增 action 后如果它改变了任何状态字段要在 side effects 里给这个 action 配上对应的字段刷新。draft 场景是否验证过申请单这类有草稿机制的 BO必须验证 draft 状态下 side effects 工作正常否则用户看到的“实时刷新”可能是假象。5.3 验收时怎么测最后是验收标准。我通常要求测试用例覆盖四类场景字段级联动修改 A 字段B 字段自动刷新且不需要保存。action 级联动点击提交、审批等 action状态字段自动刷新。保存断电验证保存之后重新打开应用联动结果仍然正确排除“只是前端临时算了笔账”的假联动。并发场景两个用户同时改同一条数据刷新后不会互相覆盖也不会冒出莫名其妙的 412。这四类跑完side effects 的落地基本就是达标的。我在实际项目里的一个体会是side effects 用好的关键在于克制——不要每个小字段都声明一遍而是把真正需要“用户改完立刻看到结果”的依赖抽出来。过度声明会让前端产生大量无意义的刷新请求页面反而变卡。真要给个标准我会说凡是用户改了之后需要立刻确认结果的字段用 side effects凡是保存后自然刷新即可的字段别硬塞进依赖清单。最后再分享一个小技巧把行为定义里的 side effects 块当作文档来写每个依赖加一行 ABAP 注释说明是哪个业务规则驱动的。这样半年后业务规则变了任何人打开这段代码都能立刻判断该改哪一行不会动错地方。局部刷新这件事做到这个程度才真正算得上“常态”。