ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RAP Side Effects实战:让Fiori Elements局部刷新成为常态

RAP Side Effects实战:让Fiori Elements局部刷新成为常态 做RAP开发这几年客户问得最多的问题不是“怎么写CRUD”而是“为什么我点了审批界面上的状态还是老样子”。后端数据明明已经改了Fiori界面就是不为所动用户只能手动点刷新或者干脆退出重进。这个体验放在桌面端还能忍拿到移动端就完全没法用了。其实从 RAP 诞生开始模型层面就考虑了这个问题对应的能力叫 Side Effects副作用建模。它让“局部刷新”不必靠前端代码一个个去维护而是直接在行为定义里声明“哪个动作会改变哪些数据”。框架识别声明后自动生成 OData 注解Fiori Elements 按注解刷新字段、按钮状态乃至整个实体卡片。这篇文章不打算堆概念直接按我自己项目里的做法把建模思路、代码落地、调试记录和常见坑一次性讲清楚。适合谁看正在用 RAP Fiori Elements 开发 Fiori 应用的后端开发尤其是被“刷新”问题折磨过的人以及打算把老 UI5 控制器里的刷新逻辑收敛回模型层的团队。1. 为什么局部刷新这么难先看没有 Side Effects 的日子1.1 三种“能用但很痛苦”的旧方案接触 Side Effects 之前我处理界面刷新基本靠三种土办法。第一种整页刷新。按钮回调里直接调oModel.refresh()或者把视图控制器里的bindingContext.refresh()一摆数据确实回来了代价是整个页面的数据全部重拉。列表页还好主从视图带行项目、带附件、带状态历史的时候一次刷新就是几十个请求用户肉眼可见地卡顿。移动端更明显页面转圈能转两三秒。第二种事件总线 控制器手动更新。后端 action 请求完成后在onActionFinished回调里手动setProperty更新界面字段或者调用byId(...).getBinding(...).refresh()。这种写法灵活是真灵活问题也真实——刷新逻辑散落在各个控制器里换一个人接手根本不知道哪里刷了哪里没刷。更麻烦的是时序有时候 action 还没返回完界面就先刷了拿到的是旧数据。第三种后端把联动结果直接写在 action 里更新数据库前端依赖字段变化自动刷新。听起来聪明实际上前端并不知道该什么时候重新读数据还得靠控制器人为触发。等于问题没解决只是换了位置。这三种方案本质上都是命令式也就是“在代码里一步一步告诉界面去干什么”。只要逻辑一多就必然出现漏刷、错刷、重复刷。1.2 Side Effects 带来的本质变化从按钮逻辑到模型声明RAP Side Effects 最核心的变化是把“刷不刷新”这个决策从控制器代码里拿出来放到了后端模型定义里。你在行为定义里写一个side effects块框架就知道某个字段变化会影响另一些字段某个 action 执行后需要重新读取某个实体。Fiori Elements 拿到这个声明后自动决定发什么请求、刷新哪些绑定前端控制器一行刷新代码都不用写。这样局部刷新就从“运动式开发任务”变成了“模型默认能力”。这也是标题里“成为常态”的意思——不是让每个人记得在控制器里补刷新逻辑而是让模型本身告诉界面该怎么表现。我越来越觉得这个转变的真正价值不是少写几行代码。它把“变化关系”这种业务知识固化在了唯一的位置前端不管换多少版只要消费同样的 OData 服务局部刷新的行为就是一致的。1.3 哪些场景才真正值得用 Side EffectsSide Effects 不是银弹我用下来觉得它的适用边界大致是这样。特别适合的场景主表状态字段变化后需要联动刷新其他字段action 执行完后需要重新拉取实体数据按钮可用性跟随状态变化。这些都是典型的声明式联动模型说清楚就够了。不太适合的场景跨系统异步业务流程比如发往外部系统的审批、等待第三方回调的状态这种应该靠后台任务加推送而不是前端联动纯粹的前端交互比如 Tab 切换、弹窗开关没必要动用后端模型需要在刷新前弹二次确认或做前端校验的逻辑还是手动刷新更可控。简单说Side Effects 解决的是“后端数据变化了前端需要同步反映”的问题。如果你要的只是“前端看起来不一样”那用 UI5 本身的响应式绑定反而更合适。2. Side Effects 建模思路行为定义里写清楚“影响关系”2.1 一次看懂 side effects 块的完整语法RAP 的 Side Effects 声明不是在 CDS 视图里写的而是写在同一套行为定义define behavior的side effects块里。我拿最近一个差旅审批项目的简化版做例子define behavior for ZRAP_TRAVEL_I alias Travel persistent table zrap_travel lock master authorization master ( instance ) late numbering { field ( numbering : skip ) TravelID; field ( readonly : update ) TravelID, CustomerID; action ( features : instance ) acceptTravel result [1] $self; action ( features : instance ) rejectTravel result [1] $self; action setStatusToOpen result [1] $self; side effects { field OverallStatus affects field StatusText; field OverallStatus affects action acceptTravel; field OverallStatus affects action rejectTravel; action acceptTravel affects entity zrap_travel_i; action rejectTravel affects entity zrap_travel_i; action setStatusToOpen affects entity zrap_travel_i; } }逐条说。field OverallStatus affects field StatusText状态值一变界面上的状态描述文本也要重新读取。这是字段级联动刷新范围最小。field OverallStatus affects action acceptTravel状态变了之后“批准”按钮的可用性需要重新评估。配合action ( features : instance )用前端会重新调 features 来判断按钮是否置灰。action acceptTravel affects entity zrap_travel_i执行“批准”动作后头实体整体刷新。这里的entity后面跟的是 CDS 视图名不是别名。Fiori Elements 收到这个声明后会针对这套实体数据发起局部重新读取。这些语句看着简单但每一个都对应前端的一次绑定刷新动作。写的时候要想清楚“最小影响范围”能刷新一个字段就别刷新整个实体否则请求量会悄悄涨上去。2.2 四种联动关系的选择指南我用一张表把常见的声明形式收一下方便大家对照。声明形式含义典型场景field A affects field B字段 A 变化重新读取字段 B状态码变化后刷状态文本、金额、日期等展示字段field A affects action X字段 A 变化重新评估 action X 可用性状态变成已批准后禁用“批准”按钮action X affects action Y执行 X 后重新评估 Y 的可用性先“审批”后“释放”审批后释放按钮才可用action X / function F / read affects entity E动作执行后重新读取实体 E点“批准”后整个页面局部刷最新数据field A affects function F字段 A 变化后重跑函数输入数量后重新计算预估金额实际建模时最考验判断力的是选字段级还是实体级。字段级请求轻但你得把每个受影响字段都列全实体级覆盖面广省心但代价是刷新请求更重。我自己的习惯是一个实体字段小于 10 个、变化逻辑集中在少数字段时优先字段级实体结构复杂、行项目也依赖头字段变化时直接实体级更稳妥。2.3 eager 模式字段一改就要刷新默认情况下字段级的 Side Effects 是在修改请求真正执行后才触发。什么意思就是用户改了状态下拉框但还没保存界面上的联动字段可能纹丝不动。如果你希望“下拉一变关联字段立刻跟着变”需要加eager修饰side effects ( eager ) { field OverallStatus affects field StatusText; field OverallStatus affects action acceptTravel; }加了 eager 之后字段值改变后立即触发副作用不需要等保存。这个选项在录入类页面特别有用比如选了客户马上联动出信用额度填了数量马上刷新总价。但要注意成本。eager 意味着字段每次变化都会多出额外的 OData 请求如果表单字段多、联动关系密页面会变得很“啰嗦”。我开始用的时候贪省事把一个带 20 多个字段的页面全加了 eager结果用户反馈打字都一顿一顿的。后来只给真正需要即时联动的字段加了 eager其他保留默认。这里还有个小技巧前端配合showLoadingIndicator: true刷新过程中给用户明确反馈避免误以为界面卡死。2.4 后端怎么把“声明”变成前端“动作”SideEffects 链路很多同事问我我后端写了 Side Effects前端是怎么知道的答案藏在 OData V4 的 metadata 里。RAP 行为定义里的side effects块会由运行时自动转换成 OData V4 的Common.SideEffects注解。打开服务 metadata你会看到类似这样的内容Annotation TermCommon.SideEffects Record PropertyValue PropertySourceProperties Collection PropertyPathOverallStatus/PropertyPath /Collection /PropertyValue PropertyValue PropertyTargetProperties Collection PropertyPathTotalPrice/PropertyPath PropertyPathStatusText/PropertyPath /Collection /PropertyValue /Record /AnnotationFiori Elements 启动时会读取这份注解针对SourceProperties的变化刷新对应的TargetProperties或实体导航。这就是整个局部刷新链路的闭环后端模型声明OData 注解传输前端自动响应。以前我在 UI5 控制器里做不到这点因为前端根本拿不到“业务影响关系”这种语义信息。RAP 把这个能力下沉到模型层之后连图表、表格、表单这些标准控件都能平等地享受局部刷新不需要为每种控件单独适配。3. 落地实践差旅审批如何做到动作后局部刷新3.1 业务场景与数据模型准备理论说再多不如跑一个完整例子。我用的场景是一个简化版差旅审批应用RAP 经典模型根视图ZRAP_TRAVEL_I差旅申请头子实体ZRAP_BOOKING_I行项目。目标动作有三个acceptTravel批准申请、rejectTravel拒绝申请、setStatusToOpen把已关闭的单子重新打开。界面要求是点击“批准”后状态字段从 Open 变为 Accepted无需手动刷新头实体的总金额如果因后端计算而变化界面自动更新“批准”按钮在 Accepted 状态下置灰“重新打开”按钮出现行项目数据能跟随实体刷新一并更新。先给 CDS 视图一个简化版本AccessControl.authorizationCheck: #CHECK EndUserText.label: Travel Metadata.layer: #CORE define root view entity ZRAP_TRAVEL_I as select from zrap_travel { key travel_id as TravelID, customer_id as CustomerID, begin_date as BeginDate, end_date as EndDate, overall_status as OverallStatus, total_price as TotalPrice, currency_code as CurrencyCode, description as Description, Semantics.systemDate.createdAt: true local_created_at as CreatedAt }3.2 行为定义的完整写法行为定义里有两个关键点action 声明要带features : instance让按钮能按当前状态动态置灰side effects块把状态字段、动作、实体的影响关系完整声明出来。define behavior for ZRAP_TRAVEL_I alias Travel persistent table zrap_travel lock master authorization master ( instance ) late numbering { field ( numbering : skip ) TravelID; field ( readonly : update ) TravelID, CustomerID; action ( features : instance ) acceptTravel result [1] $self; action ( features : instance ) rejectTravel result [1] $self; action setStatusToOpen result [1] $self; side effects { field OverallStatus affects field StatusText; field OverallStatus affects action acceptTravel; field OverallStatus affects action rejectTravel; action acceptTravel affects entity zrap_travel_i; action rejectTravel affects entity zrap_travel_i; action setStatusToOpen affects entity zrap_travel_i; } }这里有个细节值得注意result [1] $self表示动作执行后返回整个实体实例。这个声明和affects entity看起来有点重叠但实际分工不同。result $self负责把 action 执行后的最新实体数据带回给发起请求的控件affects entity负责通知前端其他绑定同一实体的控件重新读取。两者配合才能做到“点一个按钮整个界面相关区域都更新”。3.3 行为实现类里的关键动作代码行为定义写得再漂亮实现类写错一样白搭。以acceptTravel为例完整的实现类代码长这样METHOD acceptTravel. 1. 更新状态字段 MODIFY ENTITIES OF zrap_travel_i ENTITY Travel UPDATE SET FIELDS WITH VALUE #( ( %key travels[ 1 ]-%key OverallStatus A ) ) FAILED failed REPORTED reported. 2. 读取最新数据并返回 READ ENTITIES OF zrap_travel_i ENTITY Travel RESULT FAILED failed REPORTED reported. 3. 将实体结果包装为 $self 返回结构 result VALUE #( FOR travel IN result ( %tky travel-%tky %param travel ) ). ENDMETHOD.第 2 步的READ ENTITIES ... RESULT很多人会漏掉。如果你只做 MODIFY 不 READ前端拿到的返回结果可能是空的界面刷新就无从谈起。我的习惯是 action 实现里永远做“先改后读”确保返回给前端的是数据库层面的最新值而不是内存里改了一半的中间值。get_features方法控制按钮可用性这也是 Side Effects 能联动按钮的关键METHOD get_features. READ ENTITIES OF zrap_travel_i ENTITY Travel FIELDS ( OverallStatus ) WITH VALUE #( FOR key IN travels ( %key key-%key ) ) RESULT DATA(travels_to_check) FAILED failed REPORTED reported. result VALUE #( FOR travel IN travels_to_check ( %tky travel-%tky %action-acceptTravel COND #( WHEN travel-OverallStatus A THEN if_abap_behvfc-o_disabled ELSE if_abap_behvfc-o_enabled ) ) ). ENDMETHOD.fc-o_disabled常量在不同 ABAP 版本里写法略有差异以当前环境 ADT 自动补全为准。重点是features 方法必须从数据库或传入实体读取最新状态不能依赖方法内的临时变量。我见过一个同事把状态判据写在另一个方法里做了缓存结果按钮状态永远停留在第一次加载时的值排查了半天。3.4 Fiori Elements 端的验证效果与配置后端做完前端零代码。打开 Fiori Elements List Report点“批准”按钮在浏览器 F12 Network 面板里能看到清晰的请求序列先是 action 调用带来的更新请求紧接着是针对ZRAP_TRAVEL_I实体集的局部 GET。页面不会整屏转圈只有对应的状态字段、金额字段和按钮区域刷新。如果觉得刷新过程没有可视化反馈可以在manifest.json对应页面设置里加sideEffects: { showLoadingIndicator: true }这里还有两个前端配置项值得知道。skipDeepMergeOnEntityRefresh: true的作用是在 Side Effects 触发刷新时跳过前端对临时编辑内容的深度合并。默认情况下实体刷新会尽量保留用户尚未保存的输入但某些复杂表单会出现刷新后旧值和新值混在一起的情况这时候打开这个开关让服务器数据直接覆盖反而干净。ignore则用来排除某些实体让 Side Effects 刷新不作用于指定实体集。项目里如果有个超大附件表跟着主实体联动刷新一次慢得要命就可以把这个表加进 ignore改用手动按需加载。4. 常见问题与排查技巧实录这个问题清单是我在真实项目里一条条踩出来的比官方文档有用得多。4.1 “声明了副作用界面就是没反应”遇到这种情况我的排查顺序固定三步。第一步确认行为定义真的激活了。Side Effects 语法写错、行为定义激活报错整个side effects块都会被忽略。激活后可以打开行为定义的预览检查生成的 annotation或者用服务 URL 的 metadata 直接搜Common.SideEffects搜不到说明后端根本没产出注解。第二步检查拼写。实体名、字段名、方法名任何一个名字对不上编译期可能不报错但运行时就是没有副作用。我踩过一次affects action rejectTravel写成了affects action rejectTrave编译过了IDE 也没提示废了半天时间。第三步看前端有没有被自定义扩展覆盖。Fiori Elements 里如果用了自定义列的 Flex 扩展某些列可能不走标准注解刷新。这种情况不是 Side Effects 失效而是你的扩展把“常规路径”切断了。最好的处理是尽量用标准绑定少对列做深度自定义。4.2 总金额/关联字段不更新的排查路径这是“声明了、刷新了、但值还是旧的”类问题。我遇到过一个典型场景总金额由 determination 在更新时重新计算。但 action 方法里先 UPDATE 了状态字段再 READ 实体此时 determination 还没触发读出来的总金额还是旧值界面自然刷新成旧值。排查到最后发现是“读得太早”。解决思路有两种在 action 实现里显式触发计算逻辑或者调整 determination 的触发字段。RAP 支持在 CDS 行为定义里写determination calculateTotalPrice on modify { field TotalPrice; }让更新金额相关字段时自动触发重算然后把触发条件覆盖到 action 里更新的字段。另外一个容易翻船的坑是虚拟字段。CDS 视图里的$virtual元素不会从数据库读取它的值靠行为实现或 determination 填充。Side Effects 刷新实体时虚拟字段如果没走计算逻辑返回给前端就是空值。遇到这种情况别在虚拟字段上做 Side Effects 的依赖项要么改成持久化存储要么确保每次 action 后都显式重算虚拟字段。4.3 按钮可用性没联动问题大多在 features声明里写了field OverallStatus affects action acceptTravel按钮还是不变灰。多数情况是get_features方法的问题。优先检查 features 方法是否用的最新状态。RAP 的 features 方法会在每次界面刷新、字段变更、动作执行后被重新调用但它拿到的数据源由你决定。如果从缓存或实例变量判断结果大概率是旧值。其次检查返回的%action结构是否覆盖了所有 action。只给%action-acceptTravel赋值不给%action-rejectTravel赋值后者就维持框架默认值默认可能是可用。我的写法是把所有 action 的 features 在同一个方法里一次性算完避免遗漏。最后别忘了field ( readonly : update )会直接影响前端控件的可编辑性这种声明层面的禁用和 features 返回的禁用是两套机制。如果一个字段本身只读按钮也会跟着不可用先排除这个再查 features。4.4 无限刷新与性能问题Side Effects 最常见的性能问题不是逻辑错而是“刷新范围过大 eager 用得太多”。我接手过一个订单页面状态字段的field A affects field B写了五条同时还affects entity而实体本身有子表、附件和几十个字段。结果用户每改一次状态F12 里哗啦啦拉回五六个请求。改法是能缩小到字段级就缩小必要的时候加skipDeepMergeOnEntityRefresh: true避免刷新时做额外深度合并。还有一种是副作用链套链。A 变化触发 B 刷新B 刷新又触发 C 刷新多个实体互相影响。RAP 框架对同一个实体的重复刷新有一层去重机制但跨实体链条很难收敛。我建模时会把所有副作用关系画成一张矩阵图出现环状依赖就先处理掉否则运行期请求量会失控。4.5 问题速查表症状优先检查项处理思路声明了但无刷新请求行为定义是否激活、metadata 是否有Common.SideEffects重新激活比对字段名和服务元数据有刷新但值未更新READ 是否发生在 determination 之后调整 determination 触发字段或显式重算按钮可用性不联动features 方法数据源是否最新改为从数据库读取最新状态请求过多、页面卡顿是否 entity 级刷新、eager 是否过多缩小刷新范围关闭多余 eagerDraft 模式下不刷新active 与 draft 实体映射是否正确检查with draft下 action 操作的实体版本刷新后未保存内容丢失前端深度合并行为按业务需要配置skipDeepMergeOnEntityRefreshDraft 模式我多说一句。启用草稿后用户编辑的对象是 draft 版本action 有可能作用于 active 版本。Side Effects 刷新时前端如果绑定的是 draft 数据源收到的 active 更新就不会落到 draft 上。这个坑排查起来特别隐蔽我当时的解决方法是让 action 同时处理 draft 和 active 版本或者在行为定义里明确 draft 相关配置避免两边数据不一致。5. 工程化落地让 Side Effects 在团队里成为规范5.1 建模阶段先画“副作用影响矩阵”Side Effects 关系一多光靠代码 review 根本看不过来。我们团队现在的做法是设计阶段同步出一张“副作用影响矩阵”字段和动作作为行受影响对象作为列每个格子标注刷新级别字段级、动作级、实体级以及是否 eager。这张表的产出物直接就是行为定义里side effects块的草稿。评审的时候一张表比一屏代码直观得多前端同事能提前知道哪些界面会“自己动”测试同事也能照着表补用例。实际开发时照着矩阵填代码基本不会漏声明。5.2 评审与测试如何验证局部刷新没做过头Side Effects 是“做过了头”比“没做”更难发现的问题。界面刷新了看起来一切正常但请求数量翻了倍用户没感知服务器却在默默承压。我们组定了个验收标准一个操作触发一次局部刷新对应的 OData 请求不超过 3 个。测试的时候用 F12 Network 做基准先记录不加 Side Effects 时的请求量再加声明后对比增量。增量集中在受影响的字段或实体上才算合格。自动化层面OPA5 测试里可以监听网络请求和模型刷新事件断言特定字段更新时没有触发整页刷新。我们最早没有这层测试后来一次重构把一个副作用的刷新范围从字段级改成了实体级界面上谁都看不出来直到压测才发现请求量翻倍。所以这个断言值得补。5.3 我的个人经验总结用 Side Effects 这几年最大的体会是“模型声明”比“代码控制”可靠得多。控制器里的刷新逻辑写一万行换个界面又得重写模型里的side effects声明写一次Fiori Elements、自定义 UI5、甚至未来新的前端框架都能自动享受。我现在接手一个 RAP 项目第一件事不是翻前端代码而是打开行为定义看side effects块。这个块基本就是整个应用的“界面联动地图”看完它哪些界面会自动变、哪些按钮会自动灰、哪些数据改动会牵一发动全身心里就有数了。最后分享一个小习惯每次给side effects块增删声明我都会顺手在影响的实体上搜一下所有引用它的前端绑定项。有些界面虽然消费同一个 OData 服务但用的是自定义控件不一定响应标准注解。确认一遍至少能少一半“为什么他那边没刷新”的跨组扯皮。Side Effects 不是黑魔法它只是把“数据变化了界面该知道”这件事显式化了。显式带来可控可控才能成为常态。希望这篇能帮你少走几步弯路。
返回列表