☰
CDS注解@ObjectModel.readOnly:从模型到前端只读渲染的完整链路
2026/10/16 8:20:49 网站建设 项目流程

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.readOnlyCDS -> OData 服务语义OData 元数据中 Capabilities 的只读标记、写操作限制后端不允许写入的字段、聚合计算字段、审计字段
@UI.readOnlyFiori 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 渲染引擎内部维护了一个“字段控制位”的概念。一个字段的最终可编辑状态,由以下因素按优先级组合决定:

  1. OData 元数据中显式标记(Core.Computed/Core.Immutable):最高优先级。
  2. 行内数据属性(比如从数据源返回的SAP__fieldControl值):中优先级,常用在动态只读场景(如根据状态码禁用字段)。
  3. UI 注解(如@UI.readOnly)和页面配置(manifest 里的自定义属性):低优先级。
  4. 用户操作状态(比如草稿、编辑模式、审批模式):作为上下文参与最终判断。

所以你在前端看到的“字段不可编辑”往往不是单一注解决定的,而是这几个因素叠加的结果。明白了这个优先级,你就能解释很多怪异现象:为什么后端$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 上没有 OutputCDS 注解没写在该属性上,而是写在实体级别确认注解级联语义,实体级只读不会生成属性级标记
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 上仍然可编辑时,从前端方向继续查:

  1. 检查字段是否被自定义扩展覆盖。最容易中招的是custom扩展的Action或自定义列,它们如果直接绑定了Input控件,就不会走框架的只读逻辑。
  2. 检查该字段是不是“值帮助支持编辑”的特殊控件。Value Help 字段如果绑定了InputWithValueHelp,而代码里手动设了setEditable(true),会绕过只读语义。优先去掉这种手写控制,让框架走默认逻辑。
  3. 检查页面是否处于“编辑模式”的例外分支。Fiori Elements 在“草稿编辑”状态下会把某些注释为“CreateOnly”的字段临时开放给用户编辑,直到保存才戴锁。这不是 bug,是框架特性,注意区分。
  4. 检查 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 机制的交互细节。这篇文章只到前端渲染为止,这三块内容展开来都是一篇单独的深入文章了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询