1. 全链路真相:一行注解如何从前端“穿透”到后端
先说结论:@ObjectModel.readOnly并不是前端 UI 层面的“禁用输入框”,而是从 CDS 数据模型层发起、经由 OData 服务序列化、最终影响 Fiori Elements 渲染机制的一条完整链路。只有在模型层标记了只读语义,前端才能拿到“这个字段不可编辑”的元数据并据此渲染出正确形态。反过来说,如果只看前端代码去控制 readonly,往往是治标不治本——遇到 Fiori Elements 的字段只读覆盖规则,甚至会被框架自动覆盖回来。
这个注解的实际使用场景非常广泛:主键字段、创建时间、创建人、审计字段、外部系统回填的状态码、计算字段、聚合字段、导航属性对应外键……凡是用户不该改的值,都该在 CDS 上把语义钉死。适合正在用 ABAP Development Tools(ADT)做 CDS 开发、或者维护 Fiori Elements 应用的同事阅读;对刚接触这套技术栈的初学者来说,这篇文章也可以帮你建立起一条清晰的链路认知。
我最初接触这个注解时也在<KEY>字段上踩过不少坑——以为标记了只读就万事大吉,结果 OData 返回的元数据里根本没有预期的标记,前端的 SmartField 照样显示成可输入状态。后来把整条链路捋顺了,才发现问题往往不是出在注解本身,而是出在对注解“位置”和“层级”的理解上。本文就按这条链路逐步拆开讲。
2. 先看懂 CDS 注解的层次:写在哪儿,决定管到哪儿
2.1 三种粒度:属性级别、实体级别、Association 级别
@ObjectModel.readOnly最常见的写法是标记在属性上:
define view view_carrier as select from /DMO/Carrier as C { @ObjectModel.readOnly : true key C.carrier_id as CarrierId, @ObjectModel.readOnly : true C.carrier_name as CarrierName, C.carrier_contact as CarrierContact }这种写法最直观,读代码的人一眼就能看出哪几个字段不该被编辑。但也有一种容易被忽略的写法,是直接标在实体(Entity)级别:
@ObjectModel.readOnly : true @EndUserText.label: 'Carrier view' define view view_carrier as select from /DMO/Carrier as C { key C.carrier_id as CarrierId, C.carrier_name as CarrierName }实体级别的readOnly在 OData 服务里表现成什么?整体读取权限下的不可写语义——所有字段都不允许单独新增/修改/删除。这种写法通常只用于“纯展示型”的视图,例如只读的报表、聚合查询结果、跨系统接口视角。它粒度粗,但你不会希望有人在明细页里新增一条报表记录吧。
还有第三种位置:Association 级别。比如一个视图通过 association 关联了另一张表,你希望在 Fiori Elements 的对象页中导航过去后,目标视图整个都不可编辑,可以在 association 上标注:
@ObjectModel.readOnly : true association _to_Agency with foreign key AgencyID as _AgencyID这种方式管的是“通过该 association 发起的一段交互语义”,实际渲染时 Fiori Elements 会据此决定该导航块展示模式。它和实体级别掉头的作用范围不同,但思路一致:缓存只读语义,抑制写入请求。
这里有一个很关键的点:属性级别标注影响的是字段本身,实体级别标注影响的是这个视图对应那套 create / update / delete 操作。如果实体级别和属性级别同时出现,两个语义是叠加的,不会互相覆盖。
2.2 和其他“只读”注解的分工
很多人会问一个问题:@ObjectModel.readOnly和@UI.readOnly到底有什么区别?一言以蔽之,前者是“服务层语义”,后者是“UI 表现层语义”,两者管的范围不在一个维度上。
| 注解 | 生效层 | 影响范围 | 典型场景 |
|---|---|---|---|
@ObjectModel.readOnly | CDS -> OData 服务语义 | OData 元数据中 Capabilities 的只读标记、写操作限制 | 后端不允许写入的字段、聚合计算字段、审计字段 |
@UI.readOnly | Fiori Elements 渲染层 | 前端 SmartField 的 editable 控制 | 根据 UI 状态动态禁用输入框 |
@ObjectModel.mandatory | 服务层必填 | 写入时校验必填性 | 和只读搭配使用,避免必填校验和只读冲突 |
@ObjectModel.creationEnabled/@updateEnabled等 | 服务层操作开关 | 实体级写入能力控制 | 整表只读、禁止新增/修改/删除 |
实际项目里这两者经常会一起出现。一个字段在后端语义上是只读的(比如创建时间),同时在前端交互上也不希望用户看到输入框(字段都不显示),那就两个注解都标注。但如果你只标了@UI.readOnly,那 OData 交互协议层面仍然认为该字段可写,某些直接调用服务端 Capabilities 解析逻辑的前端自定义控件,仍然可能尝试发起对该字段的修改操作,然后被后端拒绝——这种“前后不一致”的坑,在集成第三方前端控件的项目里特别常见。
我的建议很简单:凡是具备数据不可变性或服务端确定值的字段,一律用@ObjectModel.readOnly钉死;UI 层面的动态控制再用@UI.readOnly补充。两者是配合关系,不是替代关系。
2.3 为什么位置选错会“悄悄失效”
ADT 里写 CDS 时,注解和元素之间是同一行,还是上一行,还是用括号包围,风格上不同但语义上等价。真正影响语义的是“写在哪个元素上”。
最常见的误用是:只在关联外键字段上标了 readOnly,可是 UI 上对应的是值帮助(Value Help)控件;值帮助控件渲染的时候,Fiori Elements 会读取 property 的 HelpRequest 信息和编辑状态,如果这个外键既有值帮助又有只读,有些版本会表现为输入框可点开值帮助却无法输入——看起来像只读了,但实际焦点仍然触发 EditFlow。你传值过去后,操作列表里可能已经附带了一个Update请求。这样的情况就很隐蔽:界面看着是灰的,数据却有可能被外部工具改掉。
所以只读字段最好也同时在 CDS 层面把“值帮助”语义关掉,比如不要为只读外键注册额外的@Consumption.valueHelpDefinition。否则前后端状态不一致,Fiori Elements 渲染线程会进入一种“禁用和值帮助并存”的处理分支,不同版本行为还有差异。
3. 中间层发生了什么:OData 服务的序列化与注解语义映射
3.1 readOnly 如何进入 OData 元数据
CDS 视图经过 OData 服务发布后(典型路径是让你建的 CDS 视图作为@OData.publish: true暴露,或者在服务定义里引用这个视图),注解并不会原封不动地出现在$metadata里。@ObjectModel.readOnly会被 OData 项目底层引擎转换成对应的 OData 语义终结符,具体表现有几处:
- 该 Property 上出现
Core.Computed或Core.Immutable标记(取决于你用的是哪种 OData 服务暴露方式)。 - 如果实体被标记了只读,
Org.OData.Capabilities.V1.InsertRestrictions、UpdateRestrictions、DeleteRestrictions中的Insertable/Updatable/Deletable会被置为false。 - 支持
Org.OData.Capabilities.V1.ReadByKeyRestrictions的服务还会出现相应的只读能力声明。
用浏览器或 Postman 打开服务的$metadata,你在 Property 上会看到类似片段:
<Property Name="CarrierId" Type="Edm.String" Nullable="false" MaxLength="3"> <Annotation Term="Core.Computed"/> </Property>有些服务实现还会输出Org.OData.Core.V1.Computed之外的Core.Released标记,或者自定义注解com.sap.vocabularies.Common.v1.ReadOnly。但无论如何,本质都是同一件事:把 CDS 层的注解语义变成前端可解析的元数据。Fiori Elements 的前端框架读取的就是这一层信息,而不是直接连回后端的 CDS 源文件。
这里值得强调一个排查链路的方法论:当你在前端看不到只读效果时,第一件事不是去调前端代码,而是先打开 OData 服务的$metadata搜这个字段,看看里面有没有Core.Computed或等价术语。元数据里有,问题在前端渲染层;元数据里没有,说明 CDS / 服务发布层就没把这个注解带出来,前端再怎么折腾都白搭。
3.2 序列化行为差异:只读字段真的不会被写入吗
一个常见的偏见是:只要标记了@ObjectModel.readOnly,前端传过来的写入请求里这个字段就会被后端的框架自动忽略。实际上并不完全是这样。
OData 服务对只读语义的强制程度,取决于服务实现采用的校验策略:
- 对于 RAP 架构(Restful ABAP Programming Model)发布的服务,
@ObjectModel.readOnly映射的只读标记会在 draft 激活和写入校验时生效,框架会拒绝针对该字段的写入修改。 - 对于经典 ABAP 编程模型发布的纯网关服务,如果业务代码在
CHANGE方法里不主动检查该字段是否被修改,只读字段仍然可能被写入——注解只影响元数据,不会自动改变服务实现的写入逻辑。
这意味着什么?意味着你在 CDS 里标了 readOnly,只能说明“模型语义上不允许写”,但服务实现是否严格遵守这一语义,取决于运行时框架。RAP 环境下你可以信任框架,经典环境里还是建议在业务代码里加上防御性检查。这不是一句劝退,而是我在一个遗留系统升级项目里实际操作后得出的结论:当时系统里几个逻辑字段在 CDS 标注了只读,但 OData 服务的UPDATE方法实现里没有过滤这些字段,结果有外部系统直接通过服务调用把维护值覆盖了。后来我们在服务实现里补了白名单校验,问题才彻底解决。
3.3 聚合与计算字段的特殊性
再补充一个容易被忽略的细节:CDS 聚合表达式(SUM、COUNT、MAX、MIN、AVG)计算出来的字段,以及表达式拼接出来的字段(比如字符串拼接、算数运算),它们的可写性在 OData 语义里本来就是受限的。此时标注@ObjectModel.readOnly更多是“显式声明”,而非“赋予只读属性”。
但有一种情况值得警惕:当你在聚合视图中使用cast或表达式重建了一个字段,并且这个字段在 OData 服务里被自动识别为“计算字段”时,如果服务版本比较老,可能不会自动把该字段标记为只读。这个时候你不显式标注 readOnly,前端拿到的就是可编辑状态。所以建议在所有聚合视图和表达式计算字段上都主动加上@ObjectModel.readOnly,不要依赖框架的自动判定。
4. Fiori Elements 如何拿到语义并渲染出只读 UI
4.1 渲染链路的四个关键环节
当 OData 服务的$metadata正确携带了只读标记后,Fiori Elements 的渲染才真正启动。这条链路大家平时看不太到,但理解它对排查问题很有帮助。
首先,应用启动时 Fiori Elements 会通过ODataModel读取$metadata,解析出每个EntityType的属性元数据,包括Core.Computed/Capabilities限制等。这一步完成之后,框架把元数据注册到内部的FieldControl解析器。
接着,当页面初始化 List Report 或 Object Page 时,框架会按 UI 注解(比如@UI.LineItem、@UI.FieldGroup)渲染表格列和表单字段。每个字段的“可编辑状态”不是直接取 CDS 注解,而是取上一步注册的元数据。简单说:如果$metadata里没有只读信息,框架默认字段可编辑。
第三步,字段渲染由控件级别完成。表格的Column和表单的SmartField/Input会根据字段的editable状态决定表现。比较新版本的 Fiori Elements 还支持在 manifest 里配置flexible行的按条件只读;此时前端的只读状态由FieldControl的取值决定,而这个取值可能来自元数据,也可能来自后端返回的行内属性。这一步非常容易制造“又读又写”的奇怪组合。
最后,用户点击“编辑”按钮时,框架进入编辑模式:它会遍历当前页面中所有字段的editable标志,来判断哪些控件进入可编辑状态。很多新手在这里发现,字段在元数据里已经标了只读,但表单里对应控件仍然显示为灰白可输入状态——原因通常是该字段绑定到了某个自定义控件或者模板,绕过了框架默认的SmartField路径。
4.2 列表报表 vs 对象页的渲染差异
同一个@ObjectModel.readOnly在不同 Fiori Elements 页面类型上的表现不完全一样:
在 List Report 的表格列里,只读字段的列头不会有任何特殊图标,“行内编辑”模式下该列保持不可编辑(前提是启用了行内编辑且元数据里有标记)。而如果你没有启用行内编辑,表格列本来就是纯展示,此时只读标记对渲染结果影响不大——这也是很多人误以为“注解没生效”的原因之一,其实字段从头到尾都不可编辑,只是表格模式本身就没有编辑状态。
在 Object Page 的详情表单里,只读字段的默认渲染是Text或非激活状态的SmartField,点击编辑按钮时不会变成可输入的Input控件。这里有一个细节:Object Page 上同一个字段在“标题区域”“表单区域”“表格区域”可能被渲染多次(比如建单页还有 header 和 body 之分),如果某个区域绑定了DataFieldForAnnotation,而这个注解本身没有继承字段级别的只读信息,那该区域的控件依然会进入可编辑状态。
我查过的项目中,最典型的坑就在这:表单中同一个字段出现了两次,一次在 Form 区域,一次在侧边栏,后者覆盖了前者的只读状态。排查时不要把眼光只停在一处。
4.3 FieldControl 机制与只读优先级
Fiori Elements 渲染引擎内部维护了一个“字段控制位”的概念。一个字段的最终可编辑状态,由以下因素按优先级组合决定:
- OData 元数据中显式标记(
Core.Computed/Core.Immutable):最高优先级。 - 行内数据属性(比如从数据源返回的
SAP__fieldControl值):中优先级,常用在动态只读场景(如根据状态码禁用字段)。 - UI 注解(如
@UI.readOnly)和页面配置(manifest 里的自定义属性):低优先级。 - 用户操作状态(比如草稿、编辑模式、审批模式):作为上下文参与最终判断。
所以你在前端看到的“字段不可编辑”往往不是单一注解决定的,而是这几个因素叠加的结果。明白了这个优先级,你就能解释很多怪异现象:为什么后端$metadata里只读字段在行内编辑时仍然可点选?因为这个字段的 UI 注解里可能有一个相反的@UI.lineItem配置覆盖了编辑状态;又或者因为表格行内编辑压根不看元数据而只看fieldControl行内值。
提示:排查只读问题时,不要只搜
readOnly关键字,要找fieldControl和“属性可编辑性解析”相关的逻辑。后者才是前端真正的决策中心。
5. 实操记录:一个买赠场景的只读配置全过程
5.1 需求背景与字段梳理
某模拟项目 X 中,需要做一个销售订单类型的 Fiori Elements 应用,其中有几个字段的写权限要求完全反过来:主键、创建时间、订单状态属于“系统控制字段”,不允许用户改;行项目里“折扣类型”这个字段由后端基于客户等级自动计算,也不允许用户在界面上修改;但“折扣金额”在某些业务场景下允许用户微调,普通场景下不允许改。
这个需求单靠在 CDS 里全部打上@ObjectModel.readOnly是解决不了的,因为“折扣金额”在不同场景下的只读性不同。于是我们采用了“基础只读 + 动态覆盖”的方案:基础语义字段全部钉死只读,动态字段通过后端数据属性下发前端控制信息。
那时我们遇到的问题和你们大概率也一样:后端字段很多,一个一个找太暴露;有些字段在 CDS 表达式里生成了别名,注解写在别名字段上,看起来没问题,可用起来却没有语义反馈。所以我们在实际建模型之前梳理了一张表:
| 字段 | 来源 | 写权限策略 | CDS 注解方案 |
|---|---|---|---|
| OrderId | 主键 | 永远只读 | @ObjectModel.readOnly : true |
| CreatedAt | 系统默认 | 永远只读 | @ObjectModel.readOnly : true |
| Status | 状态机控制 | 服务层锁定 | @ObjectModel.readOnly : true |
| DiscountType | 后端计算 | 永远只读 | @ObjectModel.readOnly : true |
| DiscountAmount | 手动/自动 | 动态控制 | 不标注只读,由服务数据返回 fieldControl |
| CustomerRating | 关联字段 | 只在草稿态可编辑 | 不标注只读,UI 层按状态动态控制 |
这个表格看起来平平无奇,但它帮我建立了一个分类习惯:判断一个字段的只读性,先问它归属于“数据固有属性”还是“业务状态相关”。前者用语义注解,后者用动态机制。混用是常态,但要区分开。
5.2 CDS 视图定义的实际片段
基于上面的分析,我们写出的 CDS 视图大概是下面这个样子:
define root view /X/Order as select from /X/OrderDb as O { @ObjectModel.readOnly : true key O.order_id as OrderId, @ObjectModel.readOnly : true @ObjectModel.estimatedTotalNumberOfRows: 1000 O.created_at as CreatedAt, @ObjectModel.readOnly : true O.status as Status, @ObjectModel.readOnly : true O.discount_type as DiscountType, O.discount_amount as DiscountAmount, @ObjectModel.association.inverse : /X/OrderItem association [0..*] to /X/OrderItem as _Items }这里有几个细节值得展开:
第一,@ObjectModel.readOnly写在key字段上没有任何问题,主键天然不该被改,显式标注只是为了让服务发布时元数据更完整。第二,Status虽然在语义上只读,但如果你希望在 UI 上仍然展示状态颜色徽章,将状态字段标记只读完全不影响值显示,只是没有输入框而已。第三,动态字段DiscountAmount刻意不标注只读,是为了把控制权交给运行时数据。
第三点要特别注意:如果你在 CDS 上就标注了只读,即便后端返回了fieldControl信息,前端也会按“元数据最高优先级”直接忽略动态控制。你必须在语义层放弃对它“不可写”的声明,才能把状态让给行内的数据属性。
5.3 动态只读的落地:后端 fieldControl 下发小技巧
动态字段的处理有两种常见路径:
一种是使用 BOPF 或 RAP 的“确定/计算”机制,在读取时把FieldControl代码直接设置到行为定义里。这种方法最正统,代码啰嗦但可靠。
另一种轻量方案,是在 CDS 视图中添加一个额外的控制字段(比如SAP__fieldControl),服务端根据业务规则把该字段的 JSON 结构化字符串传给前端。Fiori Elements 的列表表格和表单控件原生支持读取fieldControl格式,不需要写大量前端自定义逻辑。
当时我们在项目里采用的正是轻量方案:CDS 视图里预留了一个额外的字段FieldControlX1,服务实现里根据订单状态动态生成 JSON:
{ "DiscountAmount": "ReadOnly" }前端 manifest 里只需要对这一列做绑定声明,SmartField 就能识别这个值是ReadOnly还是Editable。
这个做法的优点是迁移成本低,对已有 CDS 模型改动小;缺点是扩展字段多了以后,控制内容会变得很复杂,而且这种“元数据之外的字段控制信息”不具备服务语义上的校验能力。所以它只能用于 UI 展示,不能替代@ObjectModel.readOnly在服务层的安全意义。
5.4 配置后的前端表现实测
配置完成后,我实际登录 Fiori Elements 界面验证了几种场景:
- 列表报表行内编辑模式:折扣类型列没有出现输入框,点击该单元格时没有任何编辑响应;主键和创建时间列在行内编辑时呈现纯文本。
- 对象页编辑模式:点击 Edit 后,折扣金额字段正常变为可输入状态;但后端在特定客户等级下返回了 fieldControl=ReadOnly 后,该字段直接呈现禁用样式,此时 Edit 按钮依然可以点击,其他字段正常编辑,只有这个字段保持不变。
- 状态字段在“审批中”状态下,通过后端返回的 fieldControl 强制为 ReadOnly,此时证书 UI 和控制面都正确显示。
一句话总结实测结果:静态部分靠 CDS 注解,动态部分靠 fieldControl,两者配合使用才能覆盖现实的业务复杂度。
注意:动态 fieldControl 的优先级低于元数据但要高于 UI 注解。如果你手写的
@UI.readOnly和行内 fieldControl 冲突,最终以行内值呈现为准;但如果你写了@ObjectModel.readOnly,那 fieldControl 就会被元数据压制,动态控制失效。
6. 常见问题与排查技巧实录
6.1 为什么 OData 元数据里看不到只读标记
这是在服务发布阶段最常遇到的问题。可能的原因按出现频率排序如下:
| 排查点 | 可能原因 | 解决办法 |
|---|---|---|
$metadata的 Property 上没有 Output | CDS 注解没写在该属性上,而是写在实体级别 | 确认注解级联语义,实体级只读不会生成属性级标记 |
| Property 上有 Core.Computed 但不影响编辑 | 服务实现是经典 GW,模型前后端分离 | 在服务实现方法里补充校验 |
| 某些字段名带下划线或命名空间变化 | CDS 别名转 OData 时字段名被改 | 检查 EDMX 中字段的原始名称,确认注解绑定的是别名还是源字段 |
| 使用了聚合/表达式字段却没显式标注 | 框架不会自动为所有计算字段补标记 | 显式补齐@ObjectModel.readOnly |
| 服务缓存了旧版本模型 | OData 服务元数据缓存过期 | 刷新服务的模型缓存或重启服务提供实例 |
这里特别要提一下别名问题。CDS 视图里如果你用了 SQL 表达式字段,比如:
concat( O.discount_type, O.discount_amount ) as DiscountInfo,这个DiscountInfo字段的注解需要写在新字段名上,写回O.discount_type上是没有任何语义的。ADT 在保存时通常不会给出语法错误,但模型加载时注解位置不正确,只会表现为“谁也没有错误,就是不生效”,极其坑人。
6.2 前端仍然显示可编辑,如何定位
当$metadata里已经能看到Core.Computed之类的标记,但 Fiori Elements 上仍然可编辑时,从前端方向继续查:
- 检查字段是否被自定义扩展覆盖。最容易中招的是
custom扩展的Action或自定义列,它们如果直接绑定了Input控件,就不会走框架的只读逻辑。 - 检查该字段是不是“值帮助支持编辑”的特殊控件。Value Help 字段如果绑定了
InputWithValueHelp,而代码里手动设了setEditable(true),会绕过只读语义。优先去掉这种手写控制,让框架走默认逻辑。 - 检查页面是否处于“编辑模式”的例外分支。Fiori Elements 在“草稿编辑”状态下会把某些注释为“CreateOnly”的字段临时开放给用户编辑,直到保存才戴锁。这不是 bug,是框架特性,注意区分。
- 检查 manifest.json 中页面的属性覆盖。
manifest的sap.ui.generic.app配置里可以针对 page 设置editable = true强行覆盖全页只读状态。这个开关优先级极高,会覆盖字段级语义。
定位手段也有讲究。打开浏览器开发者工具,在控制台里查看this.getBindingContext()或oField.getEditable()的状态,可以判断控件当前的可编辑来源。不过更实用的方法是直接搜索fieldControl相关的字段值,因为 OData 返回的行数据里如果有这个控制位,它的优先级足够说明问题。
6.3 草稿机制的只读陷阱
最后不得不说的是草稿(Draft)机制对只读语义的影响。SAP Fiori Elements 很多应用启用草稿后,字段可编辑性的判定会复杂一层:草稿创建时用户可以编辑字段,提交激活后某些字段自动进入只读。但如果你在 CDS 上标记了@ObjectModel.readOnly,那么从草稿一诞生,这个字段就没法编辑。
这两者之间会产生一个体验上的冲突:假设你有一个“客户类别”字段,新建草稿时可选择,激活后不可改。你就不应该给这个字段加@ObjectModel.readOnly,而应该依靠草稿激活后的状态机去控制只读。反过来,如果字段是主键或者创建时间,加上@ObjectModel.readOnly是对的,因为草稿创建时你也不应该改它。
所以我经常建议团队:先画清楚字段生命周期,再决定用哪种只读方案。无脑一键全员只读,会让草稿流程出现大量“不可输入却不可跳过”的校验死锁;不标记真正不可变字段,又会留下数据被篡改的后门。好的模型设计,在字段可变性上是有节奏的。
7. 经验沉淀:我常用的三条判断准则
把整条链路捋完,最后分享几条我实际项目中沉淀下来的判断准则,算不上方法论,但能帮你快速决策。
第一,静态不可变性大于 UI 状态。字段只要在数据语义上不可变(主键、创建时间、系统计算值),一律@ObjectModel.readOnly钉死,然后忘了它,不要再花精力去做前端动态控制。
第二,动态业务规则优先走 fieldControl,而不是在 CDS 和前端同时写死。凡是“某些状态下不可编辑、某些状态下可编辑”的字段,都不该在 CDS 上标记只读,而是把控制点放到服务端行数据下发上。这样改业务规则时只需要动服务逻辑,不需要重新发布 CDS 模型。
第三,无论使用哪种方案,验证时一定要看$metadata和前端实际表现两层。只看前端表现,可能误判为“注解没用”;只看元数据,可能漏掉前端自定义控件覆盖只读的情况。按“后端语义 -> OData 元数据 -> 前端渲染”的顺序逐层验证,九十步的问题都能定位到具体某一层。
另外补充一个小技巧:在 CDS 视图用@ObjectModel.readOnly的时候,可以在@UI组里同时给只读字段加@UI.hidden(如果这字段连显示都不需要),避免表单渲染时出现一个“不可编辑但可见”的冗余组件。比如内部审计代码字段,通常连展示都不该展示,只读加隐藏组合效果最好。
如果你还想继续往深处探索,可以从这几个方向入手:RAP 行为定义里determine和validation对只读字段的处理、Fiori Elements 的fieldControl完整 JSON 格式支持、以及 OData V4 的Core.Computed与 draft 机制的交互细节。这篇文章只到前端渲染为止,这三块内容展开来都是一篇单独的深入文章了。