mcpField
M
modernize-ui5-app

modernize-ui5-app

github.com/arc-mcp开发239236 次安装2 天前更新
代码仓库

关于 modernize-ui5-app

此工作流技能将旧版 UI5 自由式 JavaScript 应用转换为 UI5 TypeScript 应用,同时保留原应用。它提供现代化做法,并使用 UI5 MCP 指南和验证工具。

主要功能

  1. 01将 JavaScript 控制器转换为 TypeScript
  2. 02配置异步加载和 ES 模块
  3. 03采用清单驱动配置
  4. 04使用类型化事件处理程序
  5. 05运行 UI5 lint 和 TypeScript 检查

适用场景

  1. 01现代化旧版自由式 UI5 应用
  2. 02将 UI5 JavaScript 项目迁移到 TypeScript

MCP 相关

该技能使用 UI5 MCP 获取转换指南、脚手架、API 查询、lint 和清单验证;必要时还使用 sap-docs MCP 查询 UI5 与 OData V4 文档。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/arc-mcp/arc-1 --skill modernize-ui5-app

安装后在 Claude Code 中输入/modernize-ui5-app确认已加载。

详细介绍

将 UI5 自由式 JS 应用现代化 ➜ UI5 TypeScript 应用

将旧版 UI5 自由式 JavaScript 应用(典型的 2018–2021 年代应用——同步引导、JS 控制器、jQuery.sap.*、全局格式化器、ES5 模式、无类型、sap_belize)转换为近期 1.x 版本上的现代 UI5 TypeScript 应用,支持异步加载、由 manifest 驱动的配置、规范的 BaseController、sap_horizon 主题、ES 模块、带类型的事件处理程序,并且 ui5-linter 和 tsc --noEmit 输出干净。两个应用并行运行:旧版应用保持不变,位于 <source_app>/;现代应用位于 <modern_app>/。

在 RAP 后端完成后,这是两条并行 UI 路径中的一条。如果目标架构是自由式 TypeScript应用(自定义控制器、手动绑定、显式 i18n),请选择此路径。如果目标是 Fiori Elements V4 应用(由注解驱动;自定义代码最少),请改用 convert-ui5-to-fiori-elements.md。两条路径都从同一个旧版 JS 应用和 migrate-segw-to-rap 生成的同一个 V4 RAP 服务开始。

                  migrate-segw-to-rap.md  (backend: SEGW V2 → RAP V4)
                            │
              ┌─────────────┴─────────────┐
              ▼                           ▼
    modernize-ui5-app.md      convert-ui5-to-fiori-elements.md
        (freestyle TS)               (Fiori Elements V4)

路径/命名空间占位符。 <source_app>/、<modern_app>/、<source_namespace>、 <modern_namespace> 由用户提供。默认值:源应用为 legacy-*-app/(与目标应用同级); 现代应用的命名空间由源命名空间追加 .modern 得出。


此技能使用(和不使用)的 MCP

MCP用途时机
UI5 MCP(mcp__SAPUI5_MCP_Server__*)权威的 TS 转换指南、通用 UI5 指南、应用脚手架、API 参考查询、linter、manifest 验证器、版本信息全程使用——这是此技能的主要 MCP
sap-docs MCP(mcp__sap-docs__*)OData V4 绑定模式、草稿处理、控件文档V2→V4 绑定行为不明确时(例如,草稿中的复合键、$expand=_Tasks、操作调用)
arc-1 MCP可选——服务绑定 URL 查询、状态码语义仅在无法轻易获取 V4 URL 时使用(例如,无法读取 FE 应用的 manifest);否则跳过
fiori-mcp不使用这是自由式 TS 应用,不是 Fiori Elements——无需生成注解

UI5 MCP 中没有“转换为 TS”工具。转换是机械式的,并由 mcp__SAPUI5_MCP_Server__get_typescript_conversion_guidelines 驱动,该工具会返回权威的操作指南。开始时调用一次,并严格遵循其内容。

运行期间还应使用 mcp__sap-docs__search,在 UI5 最佳实践不明确或存在争议时查询(FCL 路由、V4 绑定语义、草稿处理、无障碍、主题)。其中索引的 SAP 帮助门户内容具有权威性,并且与版本相关——请引用该内容,而不是自行猜测。


关键陷阱——编写任何代码前请先阅读

以往运行此技能时曾遇到三个问题,需要多次迭代才能诊断出来。如果提前了解,就很容易避免;否则会非常棘手。即使是能力很强的大模型,也曾因陷阱 1 和 2 而花费约 15–20 分钟排查“页面空白,控制台没有错误”。

陷阱 1:FCL 保持在“OneColumn”——只显示一列

**症状:**两个视图都正确路由,控制台没有错误,无障碍树中也能看到两列的内容,但视觉上只有左侧列渲染出来。

根本原因:只有在匹配的路由中明确声明 layout 时,sap.f.routing.Router 才会更改 FCL 的 layout 属性。没有 layout 的路由会使 FCL 保持其初始值(OneColumn),因此即使路由器已将目标放入中列/末列,这些列仍会隐藏。

**修复方法:**每条 FCL 路由都必须有 layout 属性。不仅是详情/下钻路由,主页/主路由/未找到路由也必须设置;任何目标不只是 beginColumnPages 聚合的路由也必须设置。

"routes": [
  {
    "pattern": "",
    "name": "main",
    "target": ["main", "welcome"],
    "layout": "TwoColumnsMidExpanded"   // ← REQUIRED — Welcome lives in mid column
  },
  {
    "pattern": "Project/{projectId}",
    "name": "detail",
    "target": ["main", "detail"],
    "layout": "TwoColumnsMidExpanded"   // ← REQUIRED
  }
]
陷阱 2:空白页面 — DOM 已填充,但每个元素的高度都为 0

**症状:**控制台没有错误,无障碍树显示了所有内容,但页面视觉上是空白的。DevTools 确认每个嵌套元素的高度都为 0,这个值从 body 的组件 div 层层传递下来。

根本原因:UI5 的 ComponentSupport 模块在处理过程中会移除 body 容器 div 上的 data-sap-ui-component 属性。匹配该属性的 CSS 选择器(body > div[data-sap-ui-component] { height: 100% })在 ComponentSupport 运行后不再匹配,因此包装 div 没有高度,data-height="100%" 会解析为 0 的 100%。

修复方法:在 index.html 的组件 div 上直接内联设置 style="height: 100%"。依赖 data-sap-ui-component 的 CSS 选择器在这里并不可靠。

<body class="sapUiBody" id="content">
    <div
        data-sap-ui-component
        data-name="<source_namespace>.modern"
        data-id="container"
        data-height="100%"
        style="height: 100%"></div>     
</body>
陷阱 3:import Event from "sap/ui/base/Event" 是一种代码异味

**症状:**控制器中出现了诸如 (event.getParameters() as { listItem?: ListItemBase }).listItem 这样的奇怪类型断言。TypeScript 无法识别事件参数;你只能对 getParameter 或 getParameters 的返回值使用 as 类型断言来弥补。

**根本原因:**从 sap/ui/base/Event 导入通用的 Event 类型,会放弃 UI5 ≥ 1.115 提供的强类型支持。UI5 会为每个控件事件生成 <Control>$<Event>Event 和 <Control>$<Event>EventParameters 类型——应该导入这些类型。

**修复方法:**如果你发现自己在写 import Event from "sap/ui/base/Event",请停下来,找到正确的特定类型。此技能涉及的常见类型:

事件来源特定类型模块
sap.m.List / sap.m.Table selectionChangeListBase$SelectionChangeEventsap/m/ListBase
sap.m.List / sap.m.Table itemPressListBase$ItemPressEventsap/m/ListBase
sap.m.List / sap.m.Table updateFinishedListBase$UpdateFinishedEventsap/m/ListBase
sap.m.List / sap.m.Table deleteListBase$DeleteEventsap/m/ListBase
sap.m.ListItemBase pressListItemBase$PressEventsap/m/ListItemBase
sap.m.SearchField liveChangeSearchField$LiveChangeEventsap/m/SearchField
sap.m.Button pressButton$PressEventsap/m/Button
sap.ui.core.routing.Route patternMatchedRoute$PatternMatchedEventsap/ui/core/routing/Route

重要 — ListBase 继承关系:sap.m.List 和 sap.m.Table 继承自 sap.m.ListBase 的 selection / item-press / update / delete / swipe 事件。TS 事件类型定义在 sap/m/ListBase 中,而不是 sap/m/List 或 sap/m/Table 中。SAP Help 门户已确认:“sap.m.List 和 sap.m.Table 提供相同的事件,这些事件继承自 sap.m.ListBase。”不要去查找 Table$SelectionChangeEvent——它并不存在;请使用 sap/m/ListBase 中的 ListBase$SelectionChangeEvent,TypeScript 接受它用于这两个控件的 selectionChange 事件,无需类型断言。

如果找不到 UI5 ≥ 1.115 控件的特定事件类型,请调用 mcp__SAPUI5_MCP_Server__get_api_reference(query="sap.m.<Control>#<eventName>") 确认是否存在对应的类型名称。不要退回使用通用的 Event。

陷阱 4:onApprove(event: Button$PressEvent) — 声明时不带参数

症状: ESLint 将 _event(或 event)标记为 no-unused-vars。你删掉参数后, ESLint 又会将此时无用的 import { Button$PressEvent } 标记为 no-unused-imports。改两处, 重新运行两次 lint,白白浪费一分钟。

根本原因: UI5 按钮按下处理程序通常不会使用事件负载——oCtx、 projectId 等都来自 this.getView(),或来自路由匹配时填充的类字段。声明 event: Button$PressEvent 是对类型化事件规则的条件反射,但这条规则并不适用于此处。

修复: 如果不读取事件,就声明不带参数的按下处理程序:

// CORRECT:
public async onApprove(): Promise<void> {
    const projectId = this.projectId;
    // ...
}

// WRONG — generates two lint errors that need two edits to fix:
public async onApprove(_event: Button$PressEvent): Promise<void> {
    void _event;  // dead code
    const projectId = this.projectId;
}

同样适用于 onNavBack、onItemPress(当你从 this.byId 而不是事件中获取来源时),以及其他未使用事件负载的处理程序。

陷阱 5:Manifest v2 + 缺少 "type": "View" — 路由匹配成功,但不渲染任何内容

症状: 页面空白。FCL 列栏存在,宽度也正确,但每个 NavContainer 都是 空的。除此之外,控制台没有异常,只会在每次路由匹配后一毫秒出现一条很容易忽略的警告:

page stack is empty but should have been initialized -
application failed to provide a page to display

路由日志显示路由匹配正确("The route named 'main' did match"),但没有任何 视图被放入任何列中。

根本原因: 当 "_version": "2.0.0" 或更高版本(在 UI5 1.136 中引入——也是此技能 面向的版本)时,较旧的路由键 viewName、viewPath、 viewLevel 已被移除,并替换为 name、path、level;而路由 目标不再默认隐式使用 "type": "View"。如果没有显式指定类型,目标 解析会静默地不产生任何结果。

来源:SAP Help,《"Migration Information for Upgrading the Manifest File"》中 2.0.0 (1.136)的“Deprecated Manifest Entries”一行:"The routing properties ViewId, viewName, viewPath and viewLevel can no longer be used. Please use the documented alternatives by replacing them with the properties id, name, path and level, respectively along with adding the type: "view"."

修复: 在 manifest.json 的 routing 块中:

  1. 在 routing.config 中添加 "type": "View"(将其设为块中所有目标的默认值, 省去重复添加)。
  2. 使用 path(而不是 viewPath)指定视图文件夹的命名空间。
  3. 在每个目标上使用 name(而不是 viewName)——并且每个目标也添加 "type": "View" (双重保险;某些 UI5 版本无法可靠地将 config 默认值传递下去)。
  4. 在每个目标上使用 level(而不是 viewLevel)。
"routing": {
    "config": {
        "routerClass": "sap.f.routing.Router",
        "type": "View",                 // ← REQUIRED in manifest v2
        "viewType": "XML",
        "path": "<ns>.view",            // ← NOT "viewPath"
        "async": true,
        "controlId": "flexibleColumnLayout",
        "controlAggregation": "beginColumnPages",
        "bypassed": { "target": "notFound" }
    },
    "routes": [ /* ... layout property per Trap 1 ... */ ],
    "targets": {
        "main": {
            "type": "View",             // ← REQUIRED per target
            "id": "main",
            "name": "Main",             // ← NOT "viewName"
            "level": 1,                 // ← NOT "viewLevel"
            "controlAggregation": "beginColumnPages"
        },
        // ... other targets
    }
}

这种结构同时适用于 manifest v1.x 和 v2.x——添加 type: "View" 向后兼容 (从 1.14.0 / UI5 1.62 开始提供)。新键名(name/path/level)也向后兼容。 因此始终输出 v2 结构,即使你不确定项目最终使用 v1 还是 v2——这样没有任何坏处。

看到这种情况时的快速诊断: 打开浏览器控制台,搜索 "page stack is empty"。如果找到了,就是陷阱 5。如果没有,你遇到的可能是陷阱 1 (缺少 layout)或陷阱 2(高度级联)。

为什么验证器不会捕获此问题(根据对 @ui5/[email protected] 和 @ui5/[email protected] 的实测验证;UI5 MCP 封装的正是这两个版本):

工具能捕获陷阱 5?原因
run_ui5_linter(@ui5/linter)不能存在 no-removed-manifest-property 规则,但它只检查 resources/js、rootView/async、routing/config/async。不会检查路由目标中的 viewName/viewPath/viewLevel。Linter 源码读取 target.name ?? target.viewName——它会妥善接受其中任一属性,不会发出警告。
run_manifest_validation(@ui5/manifest 架构 + Ajv)不能架构将 viewName/viewPath/viewLevel 标记为 "deprecated": true,并在描述中指向 v2 替代项,但 Ajv 使用 MCP 的 strict: false 配置时会忽略 deprecated 标志。架构还将 legacyTargetAddition 和 actualTargetAdditionStandard 并列列为备选项(没有基于 _version 的条件式 if/then 分支),因此带有 viewName 的清单会验证为 isValid: true。
npm run ts-typecheck不能清单是 JSON,TypeScript 看不到它。
eslint webapp不能ESLint 不会对 UI5 清单语义建模。
浏览器运行时间接"page stack is empty but should have been initialized" 控制台警告是唯一的自动化信号,而且只会在运行时触发,不会在 lint 时触发。

对技能的影响:对于带有已弃用路由键的 v2 清单,第 7 阶段的验收门槛(linter/清单验证/ts-typecheck)都会报告通过。对此陷阱来说,唯一诚实的门槛是第 8d 阶段的浏览器渲染验证——这就是为什么第 8d 阶段是必需的,而非可选项。不要因为“lint 干净 + 验证通过”的报告,就认为路由配置正确。

如果你有时间提交上游问题,这显然是 @ui5/linter 的 no-removed-manifest-property 规则存在的缺口——将 routing.targets[].viewName/viewPath/viewLevel 添加到检查属性列表中即可弥补。


自助指南:技能找不到答案时

该技能涵盖了常见模式,但每个项目都有自己的特殊情况。遇到技能未涵盖的问题时,先调查再猜测。UI5 的中间件生态系统很分散,类型系统也很复杂;凭经验猜测往往会弄错键名或继承分支。下面的调查只需花 30 秒,却能减少反复尝试。

模式 A — 中间件/代理/构建工具配置选项

**触发条件:**你正在为 UI5 中间件(ui5-middleware-simpleproxy、ui5-middleware-livereload、fiori-tools-proxy、ui5-tooling-transpile,或 ui5.yaml 中 customMiddleware 或 customTasks 下的任何项目)设置配置键。

**调查原因:**UI5 中间件会静默忽略未知配置键——既不会报错,也不会警告,但会执行错误操作。不同软件包的命名约定各不相同:在 ui5-middleware-simpleproxy 中,跳过 TLS 验证的选项是 strictSSL: false;在 fiori-tools-proxy 中是 ignoreCertErrors: true;其他软件包则使用不同名称。不要从一种情况推断另一种。

操作步骤:

Bash: cat <target>/node_modules/<package-name>/README.md
# or:
WebFetch: https://www.npmjs.com/package/<package-name>
WebFetch: https://github.com/<owner>/<package-name>

如果 README 尚未下载到本地(尚未运行 npm install),请通过 WebFetch 或 gh api 访问 GitHub 镜像。

模式 B — UI5 控件事件的 TypeScript 类型

**触发条件:**你正在为事件处理程序参数指定类型,但不确定具体的 <Control>$<Event>Event 类型叫什么,或它位于何处。

**调查原因:**事件会被继承——它们定义在父类上,并由子类复用。搜索 Table$SelectionChangeEvent 会一无所获,因为 selectionChange 定义在 ListBase 上,而不是 Table 上。陷阱 3 的表格涵盖了此技能常遇到的事件;其他情况请询问 UI5 MCP。

操作步骤:

mcp__SAPUI5_MCP_Server__get_api_reference(
  projectDir="<absolute target>",
  query="sap.m.<Control>#<eventName>"
)

查看结果,确认:(a) 实际定义该事件的类(也就是要从哪个模块导入);(b) 规范的事件类型名称。如果 query 没有返回结果,请扩大搜索范围(query="sap.m.<Control>")并检查事件列表;类型名称格式为 <DefiningClass>$<EventName>Event。

模式 C — OData V4 绑定/草稿/操作语义

**触发条件:**你正在编写 V4 专属代码(复合键、$expand=_X 导航、操作调用、草稿处理、批量更新),但不确定规范模式是什么。

**调查原因:**V4 与 V2 的差异并不显而易见,而且这些差异与版本相关。靠猜通常会多花一次 tsc 检查周期,或遇到运行时“no metadata”错误。

操作方法:

mcp__sap-docs__search(
  query="<feature> OData V4 model UI5",
  sources=["sapui5","sap-help"],
  includeOnline=true
)
mcp__sap-docs__fetch(id="<best result id>")

可供入门的主题(请原样搜索以下术语):

  • "Draft Handling with the OData V4 Model"
  • "Operations"(V4 操作/函数)
  • "Auto-$expand / $select"
  • "Reducing the Number of Requests Required to Get the Properties of a Single Entity"
  • "Reducing Roundtrips"
  • "Filtering" + "Sorting"
模式 D — FCL 路由行为异常

**触发条件:**FCL 渲染的列数错误、列闪烁、深层链接无法还原布局,或缺少“关闭”按钮。

调查原因:sap.f.routing.Router 由布局驱动。几乎所有 FCL 异常都是路由中缺少或设置了错误的 layout 值,而不是视图 XML 错误。

**操作方法:**打开 manifest.json,检查 routing.routes[] 下的每个条目是否有 layout 属性。主页/主路由也需要设置(陷阱 1)。如果不确定该使用哪个 LayoutType 枚举值,请搜索:

mcp__sap-docs__search(query="sap.f.LayoutType FCL three-column")
模式 E — 页面渲染空白,但 DOM 已填充内容

**触发条件:**无障碍树中显示有内容,控制台没有错误,视口却是空的。

**调查原因:**高度级联。某个祖先元素的 height 解析为 0,导致所有后代元素折叠。通常是 data- 属性选择器未能匹配,因为 ComponentSupport 将其剥离了(陷阱 2);但也存在其他高度级联情况(例如 <body> 没有 height: 100%,或 Page 有隐式容器宽度但没有高度)。

**操作方法:**在浏览器 DevTools 中点击 body,在 Elements 面板中逐层检查后代树,并查看计算后的 height。第一个高度为 0 的元素就是链条中断的位置。内联添加 style="height: 100%"(首选),或使用基于未被剥离属性的 CSS 选择器(id、class — 切勿使用 data-sap-ui-*)。

模式 F — TypeScript 编译通过,但 ui5-linter 检查失败

触发条件:npm run ts-typecheck 检查通过,但 ui5lint 报出问题,而 tsc 无法帮助诊断。

**调查原因:**ts-typecheck 检查类型;ui5-linter 检查 UI5 运行时相关问题(弃用的 API、框架约定、清单交叉引用、XML 视图绑定正确性)。两者检查的方面互不相同。正确顺序是先运行 eslint --fix(清理机械性的 TS 级噪声),再运行 ui5-linter(捕获 UI5 专属问题),最后运行 tsc(检查任何剩余的类型错误)。

如果 ui5-linter 报出的发现让你看不懂,请请求上下文:

mcp__SAPUI5_MCP_Server__run_ui5_linter(
  projectDir="<absolute target>",
  filePatterns=["<the file>"],
  provideContextInformation=true
)

provideContextInformation: true 标志会返回 API 参考摘录和文档链接,解释每项发现。

模式 G — 不确定 UI5 最佳实践

触发条件:“应该用 Form 还是 SimpleForm?”、“应该用 JSONModel 还是 path?”、“加载这个 formatter 时,core:require 是正确方式吗?”、“清单是否应该声明特定主题?”

**调查原因:**SAPUI5 有与版本相关的指南。MCP 会返回适用于项目版本的权威清单。

操作方法 — 从两个固定的指南工具开始:

mcp__SAPUI5_MCP_Server__get_guidelines              # general UI5 dev rules
mcp__SAPUI5_MCP_Server__get_typescript_conversion_guidelines   # TS-specific rules

如果其中没有答案,请转向 sap-docs:

mcp__sap-docs__search(query="<your question>")
模式 H — 本次运行中同一个问题已调试两次

**触发条件:**你编写了代码,遇到错误并修复,但在另一个文件或步骤中再次遇到类似错误。这表明该技能缺少一种普遍适用的模式。

**操作:**将其记录为一条 Run-Notes 条目,包含:症状、根本原因、修复方法,以及提炼出的一行“通用规则”。运行结束后,如果该问题可能在未来项目中再次出现,请提议将其添加到 Critical Traps 部分。


此项目的命名覆盖规则(偏离旧版 SAPUI5 JS 约定)

这两项约定优先于旧版 SAPUI5 JS 编码指南。请直接应用,不要在转换时保留旧版风格。

约定旧版 SAPUI5 JS 编码指南本技能的规则
变量前缀建议使用匈牙利命名法(oModel、sQuery、iTotal、aFilters、mArgs、fHours、bFlag)不使用匈牙利命名法。 使用普通名称:model、query、total、filters、args、hours、flag。TypeScript 类型使前缀变得多余。
“Main” 实体名称“master”(例如 Master.view.xml、i18n 键 masterTitle)所有位置均使用 “main”——控制器 / 视图 / 路由 / 目标 / 文件 / i18n 键。与 SAP 自己的包容性语言指南(“master branch → main branch”)一致。

典型 SEGW-to-RAP 演示涉及的重命名映射:

旧版现代版
Master.controller.jsMain.controller.ts
Master.view.xmlMain.view.xml
controllerName="...controller.Master"controllerName="...modern.controller.Main"
route "name": "master"route "name": "main"
target "master"target "main"
i18n key masterTitlemainTitle
i18n key masterSearchPlaceholdermainSearchPlaceholder
i18n key masterCountmainCount
var oModel = ...const model = ...
var sQuery = ...const query = ...
var iTotal = ...const total = ...
var aFilters = ...const filters = ...
var oCtx = ...const ctx = ...(如果 context 更清晰,也可以用 const context = ...)
var oList = ...const list = ...
var oRouter = ...const router = ...
var oEvent = ...(参数)event
var that = this;完全删除;改用箭头函数

例外: 只有在移除前缀会与保留字、同名导入或 UI5 控件名称冲突时,才保留前缀(例如,本地变量 const event = ... 不会与常见的 UI5 导入冲突,因此可以使用;但 const Date = ... 会遮蔽全局对象,应改用 oDate 或 workDate)。

在 Phase 2 计划输出中加入一行“命名覆盖规则:”,以便用户确认本次运行采用这些规则。

原子重命名提示: 将 master 改为 main 至少涉及 7 处:文件名、视图 controllerName 属性、manifest 路由名称、manifest 目标名称 + 键 + viewName、i18n 键 × 2 个语言文件、BaseController.onNavBack 回退逻辑,以及所有控制器中的 navTo("master", ...) 调用。为避免重复编辑,请先 grep 列出所有命中项,然后一次性编辑:

Bash: grep -rn -E "master|Master" <target>/webapp

检查输出,判断哪些命中项需要重命名(例如,旧版德语注释句子中的 “master” 一词并非重命名对象——这些注释本就应该删除)。一次性完成所有编辑,然后再次 grep 验证,确认没有结果。


智能默认设置(静默应用——研究前不要询问)

设置默认值原因
源应用<source_app>/工作区下的自由编写 JS 应用
目标应用<modern_app>/为此技能的输出预留的空文件夹
目标 UI5 版本1.147.2撰写时最新的 1.x 版本;与演示中的 FE 应用匹配;符合 2.0 API
框架SAPUI5与旧版应用的 ui5.yaml 和 FE 应用的运行时匹配
语言TypeScript根据 get_typescript_conversion_guidelines
类型包@sapui5/[email protected]UI5 MCP TypeScript 指南要求使用(不是较旧的拼写错误 sap-ui5-types)
应用命名空间源命名空间 + .modern(例如 <source_namespace>.modern)在路由中与旧版区分;保持 grep 连贯性
主题sap_horizonUI5 1.108+ 的默认主题;旧版 sap_belize 已弃用
布局将 sap.m.SplitApp ➜ sap.f.FlexibleColumnLayout(FCL)主视图 + 详情视图的现代响应式默认布局
命名不使用匈牙利命名法前缀;使用“main”而非“master”——参见上文“命名覆盖”部分符合 TypeScript 惯用命名和 SAP 包容性语言规范
引导data-sap-ui-async="true" + data-sap-ui-on-init="module:sap/ui/core/ComponentSupport"根据 UI5 指南;同步模式已弃用,且会破坏 2.x
Manifest 版本_version: 1.60.0 或更高sap.app.dataSources + 声明式模型所必需
OData 模型通过开发服务器代理使用 migrate-segw-to-rap 生成的 V4 服务(或用户指定的任何 V4 服务)。SRVD 直连模式的格式:/sap/opu/odata4/sap/<service_binding>/srvd/sap/<service_binding>/0001/;如果存在同级参考应用,则检查其 $metadataV4 展示了现代模式;与已构建的任何 FE 应用使用相同后端
路由将 sap.m.routing.Router ➜ sap.f.routing.Router,并使用各 FCL 列的目标FCL 需要 f-router
BaseController必需统一提供 getRouter、getModel、getResourceBundle、getOwnerComponent
事件类型使用 <Control>$<Event>Event(例如 Button$PressEvent)UI5 ≥ 1.115 支持;UI5 指南规定必须使用
格式化器优先使用 OData 类型(sap.ui.model.odata.type.*);仅在独特业务逻辑需要时使用自定义格式化器根据 UI5 指南 §1
表单如需表单,使用 sap.ui.layout.form.Form + ColumnLayout切勿使用 SimpleForm(UI5 指南 §4)
类型断言使用实际控件类型(如 as Button),绝不使用 as any / as unknown as ...根据 TypeScript 转换 §通用规则
测试首轮暂不包含 OPA5 + QUnit如果第 1 轮顺利通过,则列为后续事项
Lintermcp__SAPUI5_MCP_Server__run_ui5_linter → 0 个问题硬性验收标准
Manifest 验证mcp__SAPUI5_MCP_Server__run_manifest_validation → 0 个错误硬性验收标准
类型检查npm run ts-typecheck(在 package.json 中添加脚本)→ 0 个错误硬性验收标准

输入

用户提供以下其中一项:

  • 旧版应用的相对路径,例如 <source_app>/
  • 目标文件夹名称,例如 <modern_app>/(技能会推断源文件夹为同级的 legacy-*)
  • 不提供任何内容——使用默认值(<source_app>/ ➜ <modern_app>/)

如果两个文件夹都存在,且目标文件夹非空,请询问:“<target>/ 已有内容。 要清空并重新开始,还是迁移到现有结构中?” 演示默认选择清空并重写。


阶段 0 — 预检

0a. 旧版应用可读
Bash: cat <legacy>/webapp/manifest.json
Bash: ls <legacy>/webapp/{controller,view,model,fragment,i18n}

断言:manifest.json 可解析;webapp/controller/ 和 webapp/view/ 均存在;至少有一个 *.controller.js 和一个 *.view.xml。如果任一条件不满足,则停止并提示 “<legacy> 看起来不像 UI5 应用——请检查路径。”

0b. UI5 MCP 服务可访问
mcp__SAPUI5_MCP_Server__get_version_info(frameworkName="SAPUI5")

断言:版本映射中至少包含 1.147.x。如果工具报错,则停止并提示 “UI5 MCP 服务未配置;运行此技能前,请在 .cursor/mcp.json 中配置它。”

0c. 获取权威指南(在编写任何代码之前执行)
mcp__SAPUI5_MCP_Server__get_typescript_conversion_guidelines
mcp__SAPUI5_MCP_Server__get_guidelines

完整阅读两条响应。它们是以下内容的权威依据:

  • 开发依赖项版本(@ui5/cli、typescript、ui5-tooling-transpile、ui5-middleware-livereload、typescript-eslint)
  • tsconfig.json 的结构(target: es2023、module: es2022、types: ["@sapui5/types"]、paths 映射)
  • ui5.yaml 的结构(ui5-tooling-transpile-task + ui5-tooling-transpile-middleware)
  • 5 步代码转换顺序:(1) 类语法,(2) ES 模块,(3) 类型注解,(4) 为泛型 getter 添加类型断言,(5) 处理剩余问题
  • @namespace JSDoc 规则:每个导出类前面都必须紧挨着添加该注释,以便反向转换时重新添加 UI5 类名
  • 应避免的类型断言:绝不使用 any,绝不使用 unknown as ...——使用实际的控件类型
  • 控件事件类型规则:从 sap/m/Button 导入 Button$PressEvent,不要从 sap/ui/base/Event 导入 Event

这些工具响应篇幅较长,而且针对特定版本;不要凭记忆转述,必要时请引用原文。

0d. 可用的 npm + node
Bash: node --version && npm --version

确认:Node 22+(CLAUDE.md 要求)和 npm 10+。


阶段 1 — 探查旧版应用

阅读 <legacy>/webapp/ 中所有相关文件。将发现的问题分为 阻塞项(现代 UI5 必须修复)、清理项(不符合最佳实践,但仍可运行)或 外观项(样式)。

1a. Manifest 扫描

提取 _version、sap.ui5.dependencies.minUI5Version、sap.ui5.rootView、sap.ui5.dependencies.libs、sap.ui5.routing、sap.ui5.models、sap.ui5.contentDensities、sap.ui5.resources 和主题引用。

常见旧版模式:

  • _version: 1.40.0 ➜ 阻塞项(目标版本为 1.60+)
  • minUI5Version < 1.108 ➜ 阻塞项(无法保证异步行为)
  • FCL 目标配合 routerClass: "sap.m.routing.Router" ➜ 阻塞项
  • sap.ui5.models 中的硬编码服务 URL ➜ 清理项(移至 sap.app.dataSources)
  • contentDensities: { compact: true, cozy: true } ➜ 清理项(仍受支持,只需移至 manifest)
  • supportedThemes: ["sap_belize"] ➜ 清理项(删除;默认主题为 sap_horizon)
1b. Component.js 扫描

阅读 <legacy>/webapp/Component.js。查找:

  • jQuery.sap.require("...") ➜ 阻塞项(删除;改用 ES 导入)
  • 硬编码服务 URL 常量 + init 中的 new sap.ui.model.odata.v2.ODataModel(...) ➜ 阻塞项(将模型移至 manifest 的 sap.ui5.models[""])
  • 手动创建 device 模型 ➜ 保留(这是常规做法,但在 TS 中要添加类型)
  • OData V4 使用 sap.ui.model.BindingMode.TwoWay ➜ 阻塞项(V4 读取时优先使用 OneWay,并显式编辑)
  • useBatch: false ➜ 不适用(V4 的批处理方式始终不同)
  • 混合关注点(在 Component 中通过 jQuery.sap.require 加载 formatter)➜ 阻塞项
1c. Controller 扫描(逐个 Controller)

针对每个 <legacy>/webapp/controller/<X>.controller.js:

Read: <legacy>/webapp/controller/<X>.controller.js

标记:

  • var that = this; 后接闭包 ➜ 清理项(改用箭头函数)
  • oCtx.getPath() 正则解析(.replace(/^\/Set\('/, ""))➜ 阻塞项(使用 oCtx.getProperty("Key"))
  • sap.ui.getCore().byId(...) ➜ 清理项(改用 this.byId(...))
  • sap.ui.getCore().getModel(...) ➜ 阻塞项(使用带类型断言的 this.getOwnerComponent()!.getModel(...))
  • sap.ui.core.UIComponent.getRouterFor(this) ➜ 清理项(移至 BaseController)
  • 将 sap.m.MessageBox 作为全局对象访问 ➜ 阻塞项(从 sap/m/MessageBox 导入 MessageBox)
  • jQuery.sap.require(...) ➜ 阻塞项(使用 ES 导入)
  • 全局 formatter 调用(window.com.demo.formatter.X)➜ 阻塞项(合并到 formatter 模块,并在视图中通过 core:require 导入)
  • oModel.callFunction("/X", {method:"POST", urlParameters: {...}})(OData V2 函数导入)➜ 阻塞项(转换为 V4 action:model.bindContext("/Action(...)").execute())
  • 使用 setTimeout(..., 500) 实现“数据到达后刷新”➜ 阻塞项(使用绑定事件,例如 dataReceived)
  • 参数没有 JSDoc ➜ 外观项(TS 重写时会处理)
1d. 视图扫描(逐个视图)

对于每个 <legacy>/webapp/view/<X>.view.xml:

Read: <legacy>/webapp/view/<X>.view.xml

标记:

  • 引用不存在方法的内联事件处理程序(press="onSomething")➜ 阻断项(会抛出异常)
  • 未启用异步的 <core:Fragment fragmentName="..."> ➜ 清理项
  • 已弃用的控件(sap.ui.commons.*)➜ 阻断项
  • 以 sap.m.SplitApp 为根控件 ➜ 阻断项(按照 smart-defaults 转换为 FCL)
  • 使用硬编码字符串,而非 i18n 键 ➜ 清理项
  • 基于路径的格式化器(formatter: 'window.com.demo.X.statusText')➜ 阻断项(转换为使用 core:require 引入格式化器模块,并使用 formatter: '.formatter.statusText')
  • enabled="{= ${Status} === 'D' }"(表达式绑定)➜ 保留,但适配为 V4 键(根据 V4 模型,不使用 Status,而使用 OverallStatus 等)
1e. 格式化器扫描

对于 <legacy>/webapp/model/formatter.js:

  • jQuery.sap.declare(...) + window.com.demo... 全局命名空间 ➜ 阻断项(重写为带有 export function 的 ES 模块)
  • 每次调用都实例化 sap.ui.core.format.DateFormat.getDateInstance(...) ➜ 清理项(在模块顶层只缓存一次)
  • 每次调用都实例化格式化对象 ➜ 清理项(只实例化一次)
  • 状态/优先级映射的 switch ➜ 保留,但类型改为 string | undefined ➜ string
1f. 生成检查结果报告

向用户输出结构化摘要:

Discovery — legacy app: <legacy>
  Manifest version: 1.40.0 (target 1.60+)
  UI5 version: 1.84.x (target 1.147.2)
  Controllers: 3 (App, Master, Detail)
  Views: 5 (App, Master, Detail, Welcome, NotFound)
  Layout: sap.m.SplitApp (target sap.f.FlexibleColumnLayout)
  Theme: sap_belize (target sap_horizon)
  OData: V2 hardcoded in Component.init (target V4 via manifest dataSources)

Blockers (<n>):
  - Component.js: hardcoded service URL + manual ODataModel construction
  - Component.js: jQuery.sap.require(...)
  - formatter.js: window.com.* global namespace
  - Master.controller.js: getPath() regex parsing of V2 entity key
  - Detail.controller.js: oModel.callFunction("/ApproveProject", ...) for V2 function-import
  - Detail.controller.js: setTimeout(..., 500) for counter refresh
  - *.view.xml: window.com.* formatter paths
  - App.view.xml: SplitApp root
  - ...

Cleanups (<n>):
  - Master.controller.js: `var that = this;` pattern (4×)
  - All controllers: `sap.ui.core.UIComponent.getRouterFor(this)` direct calls
  - ...

Cosmetic (<n>):
  - Missing JSDoc across controllers
  - ...

阶段 2 — 设计计划 + 用户批准

严格按此格式打印迁移计划,然后停止并等待用户回复 ok/edit/问题:

Plan — modernize <legacy> ➜ <target>:

UI5 version:       1.147.2 SAPUI5 (TypeScript)
Types:             @sapui5/[email protected]
Namespace:         <source_namespace>.modern
Theme:             sap_horizon
Layout:            sap.f.FlexibleColumnLayout (translated from SplitApp)
OData:             V4 — <v4_service_url>
Proxy:             ui5-middleware-simpleproxy → <sap_baseuri>
                   (auth via .env: UI5_MIDDLEWARE_SIMPLE_PROXY_{USERNAME,PASSWORD})

Naming overrides:  - No Hungarian prefixes (TS types replace the hint)
                   - "main" instead of "master" for controller / view / route / i18n keys
                   (legacy Master.controller.js ➜ Main.controller.ts, etc.)

Files to generate in <target>/webapp:
  Component.ts
  manifest.json (v1.60.0)
  index.html (ComponentSupport bootstrap, async, inline height fix)
  controller/BaseController.ts
  controller/App.controller.ts
  controller/Main.controller.ts        ← renamed from Master per naming overrides
  controller/Detail.controller.ts
  view/App.view.xml (FCL root)
  view/Main.view.xml                   ← renamed from Master
  view/Detail.view.xml
  view/Welcome.view.xml
  view/NotFound.view.xml
  i18n/i18n.properties (translated keys: masterX → mainX) + i18n_en.properties
  css/style.css (copied from legacy if non-empty)
  model/models.ts (device-model helper only)
  model/formatter.ts (consolidated; ES module export)

Files at <target>/ root:
  package.json (with @ui5/cli, typescript, ui5-tooling-transpile, ui5-middleware-livereload,
                ui5-middleware-simpleproxy, @sapui5/types)
  ui5.yaml (specVersion 4.0; transpile + livereload + simpleproxy middleware)
  tsconfig.json (target es2023, module es2022, strict, allowJs)
  .env.example
  .gitignore

V2 ➜ V4 binding migrations (generic — adapt to your service's entity names):
  /<EntitySet>                  ➜ /<Entity>             (drop "Set" suffix)
  /<EntitySet>('K')             ➜ /<Entity>(<Key>='K')  (named-key style)
  expand: '<Nav>'               ➜ $expand=_<Nav>        (V4 RAP prefixes assocs with _)
  oModel.callFunction(...)      ➜ model.bindContext("/<Action>(...)").execute()
  fieldName remapping           ➜ inspect $metadata; RAP often renames fields between V2/V4

Blockers being fixed: <count>
Cleanups applied:     <count>
Cosmetic skipped:     <count> (separate prettier pass if desired)

Tests in this skill: none (OPA5/QUnit follow-up)
Acceptance:          ui5-linter clean + manifest validation clean + tsc clean + npm start
                     renders Master list

Type `ok` to proceed, `edit` to revise, or ask any question.

在用户回复 ok 之前,不要修改 <target>/ 中的任何内容。


阶段 3 — 搭建现代 TypeScript 应用

3a. 目标目录非空时清空

如果用户确认清空并重写:

Bash: rm -rf <target>/* <target>/.[!.]*  # safely empty <target>/ while keeping the folder
3b. 通过 UI5 MCP 搭建脚手架

直接在 <target>/ 中调用 create_ui5_app(不要放入子文件夹):

mcp__SAPUI5_MCP_Server__create_ui5_app(
  appNamespace = "<source_namespace>.modern",
  basePath = "<absolute path to target>",
  createAppDirectory = false,
  framework = "SAPUI5",
  frameworkVersion = "1.147.2",
  typescript = true,
  initializeGitRepository = false,
  runNpmInstall = true
)

此处有意省略 oDataV4Url——V4 服务位于需要凭证的代理之后,因此 URL 验证会失败。数据源将在阶段 4 中手动添加。

3c. 验证结构
Bash: ls -la <target>/ && ls <target>/webapp/

根目录中预期包含:package.json、ui5.yaml、tsconfig.json、webapp/,其中至少包含 Component.ts、manifest.json、view/App.view.xml、controller/App.controller.ts、 index.html、i18n/i18n.properties。

如果 create_ui5_app 生成的是 JS 文件而不是 TS 文件,则明确报错——不要悄悄改为手动搭建脚手架。

3d. 确认依赖符合 UI5 MCP 指南
Read: <target>/package.json

对照 get_typescript_conversion_guidelines 的输出进行检查。预期的开发依赖至少包括:

  • @ui5/cli
  • typescript
  • ui5-tooling-transpile
  • ui5-middleware-livereload
  • 与框架版本匹配的 @sapui5/types

补齐缺失项(例如,为 OData 代理添加 ui5-middleware-simpleproxy)。仅当版本低于指南规定的最低版本时才更新;绝不要降级。

然后,如果依赖列表有变更,再次运行 npm install。

同时确认 scripts 中存在 "ts-typecheck": "tsc --noEmit"(UI5 MCP 指南要求)。如果缺失,则添加。

3e. 警告——脚手架本身使用匈牙利命名法

UI5 MCP 脚手架模板(Component.ts、BaseController.ts、默认的 *.controller.ts、models.ts)使用匈牙利式前缀命名参数和局部变量(sName、oModel、oParameters 等)。上文的命名覆盖规则同样适用于脚手架——阅读每个脚手架生成的文件时就重命名,而不只是移植旧控制器时才重命名。

最快的工作流程:先生成脚手架,然后在编写任何新的视图控制器之前,对生成的 webapp/ 执行一次批量 grep+重命名,处理常见的几个前缀(oModel、sName、oEvent、 oParameters 等)。这样,你准备扩展的 BaseController 已经采用现代风格。


阶段 4 — 配置 manifest.json + ui5.yaml + 代理

4a. 翻译 manifest.json

读取 <target>/webapp/manifest.json,然后合并旧版特有的配置:

  1. 设置 sap.app.id = <source_namespace>.modern。

  2. 根据 i18n 设置 sap.app.title / sap.app.description(已模板化为 {{appTitle}})。

  3. 如果旧版的 sap.app.icons 非空,则复制过来。

  4. 添加 sap.app.dataSources,使用用户指定的 V4 服务 URL(如果不确定,则运行 mcp__sap-docs__search,查询后端使用的 V4 绑定模式;ABAP 本地部署上的 SRVD 直连通常为 /sap/opu/odata4/sap/<service_binding>/srvd/sap/<service_binding>/0001/):

    "dataSources": {
      "mainService": {
        "uri": "<v4_service_url>",
        "type": "OData",
        "settings": {
          "odataVersion": "4.0",
          "localUri": "localService/metadata.xml"
        }
      }
    }
    
  5. 设置 sap.ui5.dependencies.minUI5Version = "1.147.0";库:sap.ui.core、sap.m、sap.f、 sap.ui.layout。

  6. 设置 sap.ui5.models[""]:

    "": {
      "type": "sap.ui.model.odata.v4.ODataModel",
      "dataSource": "mainService",
      "settings": {
        "operationMode": "Server",
        "autoExpandSelect": true,
        "earlyRequests": true
      }
    }
    
  7. 将 sap.ui5.rootView 设置为 { "viewName": "<ns>.view.App", "type": "XML", "id": "app" }。

  8. 路由 — 每个路由都必须有 layout 属性(陷阱 1),且每个目标都必须 有 type: "View",并使用 name/path/level(而非 viewName/viewPath/viewLevel) (陷阱 5):

    "routing": {
      "config": {
        "routerClass": "sap.f.routing.Router",
        "type": "View",                       // ← REQUIRED in manifest v2 (Trap 5)
        "viewType": "XML",
        "path": "<ns>.view",                  // ← NOT "viewPath"
        "async": true,
        "controlId": "flexibleColumnLayout",
        "controlAggregation": "beginColumnPages",
        "bypassed": { "target": "notFound" }
      },
      "routes": [
        {
          "pattern": "",
          "name": "main",
          "target": ["main", "welcome"],
          "layout": "TwoColumnsMidExpanded"   // ← REQUIRED (Trap 1)
        },
        {
          "pattern": "<EntityRoute>/{<keyParam>}",
          "name": "detail",
          "target": ["main", "detail"],
          "layout": "TwoColumnsMidExpanded"   // ← REQUIRED (Trap 1)
        }
      ],
      "targets": {
        "main":     { "type": "View", "id": "main",     "name": "Main",     "level": 1, "controlAggregation": "beginColumnPages" },
        "welcome":  { "type": "View", "id": "welcome",  "name": "Welcome",  "level": 2, "controlAggregation": "midColumnPages" },
        "detail":   { "type": "View", "id": "detail",   "name": "Detail",   "level": 2, "controlAggregation": "midColumnPages" },
        "notFound": { "type": "View", "id": "notFound", "name": "NotFound", "level": 3, "controlAggregation": "midColumnPages" }
      }
    }
    

    注意:type: "View" 既要出现在 config 上,也要出现在每个目标上。config 级别的默认值 应会向下传递,但在每个目标上重复声明是双保险,可避免不同 UI5 版本解析默认值的方式不一致。

  9. sap.ui5.contentDensities = { "compact": true, "cozy": true }(声明式配置,可用)。

  10. 删除 supportedThemes(sap_horizon 是默认主题,无需固定指定)。

写入合并后的 manifest:

Write: <target>/webapp/manifest.json

验证:

mcp__SAPUI5_MCP_Server__run_manifest_validation(manifestPath="<absolute path>/webapp/manifest.json")

继续之前,修复验证发现的所有问题。

4b. ui5.yaml — 添加代理并确保包含转译中间件

打开 <target>/ui5.yaml。Phase 3 之后,文件中已包含 ui5-tooling-transpile-task 和 ui5-tooling-transpile-middleware(请确认)。添加 simpleproxy 和 livereload:

specVersion: "4.0"
metadata:
  name: <source_namespace>.modern
type: application
framework:
  name: SAPUI5
  version: "1.147.2"
  libraries:
    - name: sap.m
    - name: sap.ui.core
    - name: sap.f
    - name: sap.ui.layout
    - name: themelib_sap_horizon
builder:
  customTasks:
    - name: ui5-tooling-transpile-task
      afterTask: replaceVersion
server:
  customMiddleware:
    - name: ui5-tooling-transpile-middleware
      afterMiddleware: compression
    - name: ui5-middleware-livereload
      afterMiddleware: compression
    - name: ui5-middleware-simpleproxy
      afterMiddleware: compression
      mountPath: /sap
      configuration:
        baseUri: "<sap_baseuri>/sap"   # e.g. https://abap-host:50001/sap or https://my-cf-host
        strictSSL: false               # only if upstream uses a self-signed cert (dev trials)
        query:
          sap-client: "<client>"       # e.g. "001"; omit if your service doesn't need it

**配置键陷阱:**键是 strictSSL: false,而不是 skipCertificateCheck 或 ignoreCertErrors。其他代理中间件使用这些名称(fiori-tools-proxy 使用 ignoreCertErrors),但 ui5-middleware-simpleproxy 会静默忽略未知键,因此 使用错误的键会导致 HTTPS 握手失败,且不会显示任何错误消息。已对照该 中间件的 README 验证,并确认在演示工作区中可正常使用。

将 ui5-middleware-simpleproxy 添加到 package.json 的 ui5.dependencies 中,让 UI5 工具 自动加载它:

"ui5": { "dependencies": ["ui5-middleware-simpleproxy"] }
4c. .env 脚手架配置
Write: <target>/.env.example
UI5_MIDDLEWARE_SIMPLE_PROXY_USERNAME=
UI5_MIDDLEWARE_SIMPLE_PROXY_PASSWORD=

只向用户询问一次(交互式),让其将值复制到 <target>/.env。不要提交 .env。

4d. index.html — ComponentSupport 引导 + 内联高度(脚手架生成后验证)

脚手架生成的 index.html 应已使用 ComponentSupport。根据上文的 陷阱 2 修正高度处理 — 组件 <div> 必须内联设置 style="height: 100%":

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{appTitle}}</title>
    <style>html,body,#content { height: 100%; margin: 0; }</style>
    <script
        id="sap-ui-bootstrap"
        src="resources/sap-ui-core.js"
        data-sap-ui-theme="sap_horizon"
        data-sap-ui-resource-roots='{ "<source_namespace>.modern": "./" }'
        data-sap-ui-on-init="module:sap/ui/core/ComponentSupport"
        data-sap-ui-compat-version="edge"
        data-sap-ui-async="true"
        data-sap-ui-frame-options="trusted">
    </script>
</head>
<body class="sapUiBody" id="content">
    <div
        data-sap-ui-component
        data-name="<source_namespace>.modern"
        data-id="container"
        data-height="100%"
        style="height: 100%"></div>
</body>
</html>

注意:

  • style="height: 100%" 是必需的 — 使用针对 [data-sap-ui-component] 的 CSS 选择器 看似更简洁,但行不通,因为 ComponentSupport 会在处理过程中移除该属性(陷阱 2)。
  • 单独设置 data-height="100%" 不够 — 它是 ComponentContainer 的设置,会根据父级高度解析, 因此我们还需要在外层 div 上添加内联样式。
  • 除引导脚本外,不要添加内联 <script>(CSP 规则、UI5 指南 §1)。

阶段 5 — BaseController + Component.ts + App.view.xml

5a. BaseController.ts
Write: <target>/webapp/controller/BaseController.ts
import Controller from "sap/ui/core/mvc/Controller";
import UIComponent from "sap/ui/core/UIComponent";
import Router from "sap/f/routing/Router";
import Model from "sap/ui/model/Model";
import ResourceModel from "sap/ui/model/resource/ResourceModel";
import ResourceBundle from "sap/base/i18n/ResourceBundle";
import History from "sap/ui/core/routing/History";

/**
 * @namespace <source_namespace>.modern.controller
 */
export default class BaseController extends Controller {
    public getRouter(): Router {
        return UIComponent.getRouterFor(this) as Router;
    }

    public getModel<T extends Model = Model>(name?: string): T {
        return this.getView()!.getModel(name) as T;
    }

    public setModel(model: Model, name?: string): void {
        this.getView()!.setModel(model, name);
    }

    public async getResourceBundle(): Promise<ResourceBundle> {
        const i18n = this.getOwnerComponent()!.getModel("i18n") as ResourceModel;
        return (await i18n.getResourceBundle()) as ResourceBundle;
    }

    public onNavBack(): void {
        const previousHash = History.getInstance().getPreviousHash();
        if (previousHash !== undefined) {
            window.history.go(-1);
        } else {
            this.getRouter().navTo("master", {}, true);
        }
    }
}
5b. Component.ts

模板生成的内容大致如下;请确保:

import UIComponent from "sap/ui/core/UIComponent";
import Device from "sap/ui/Device";
import JSONModel from "sap/ui/model/json/JSONModel";

/**
 * @namespace <source_namespace>.modern
 */
export default class Component extends UIComponent {
    public static metadata = {
        manifest: "json",
        interfaces: ["sap.ui.core.IAsyncContentCreation"]
    };

    public init(): void {
        super.init();
        const deviceModel = new JSONModel(Device);
        deviceModel.setDefaultBindingMode("OneWay");
        this.setModel(deviceModel, "device");
        this.getRouter().initialize();
    }

    public getContentDensityClass(): string {
        return Device.support.touch ? "sapUiSizeCozy" : "sapUiSizeCompact";
    }
}

(从旧版组件中沿用 getContentDensityClass,因为 App.controller 会使用它。)

5c. App.view.xml(FCL 根视图)
<mvc:View
    xmlns:mvc="sap.ui.core.mvc"
    xmlns="sap.f"
    controllerName="<source_namespace>.modern.controller.App"
    displayBlock="true"
    height="100%">
    <FlexibleColumnLayout id="flexibleColumnLayout"
        backgroundDesign="Solid"
        layout="OneColumn"/>
</mvc:View>
5d. App.controller.ts
import BaseController from "./BaseController";
import Component from "../Component";

/**
 * @namespace <source_namespace>.modern.controller
 */
export default class App extends BaseController {
    public onInit(): void {
        const oOwner = this.getOwnerComponent() as Component;
        this.getView()!.addStyleClass(oOwner.getContentDensityClass());
    }
}

阶段 6 — 逐个视图转换

为每个旧版 <View>.view.xml + <View>.controller.js 生成现代版本。 按以下顺序处理:Main(原为 Master)➜ Detail ➜ Welcome ➜ NotFound。

**编写任何控制器之前请记住:**应用本技能开头的命名覆盖规则——不要使用匈牙利命名法前缀(使用 const list = ...,而不是 const oList = ...);将 Master.controller.js ➜ Main.controller.ts;使用具体事件类型(陷阱 3),绝不要使用 sap/ui/base/Event 中的 Event。

对于每个控制器,请遵循 get_typescript_conversion_guidelines 中的 5 步 TS 转换:

  1. 将 Class.extend() ➜ 转换为 class extends ...,并在其正前方添加 @namespace JSDoc。
  2. 将 sap.ui.define([...], function(...)) ➜ 转换为 ES import + export default class。
  3. 为方法参数/类属性添加类型注解;使用具体事件类型,例如 Button$PressEvent,绝不要使用 sap/ui/base/Event 中的裸 Event。
  4. 为泛型 getter 的返回值添加类型断言:this.byId("x") as Table、this.getView()!.getModel() as ODataModel、event.getSource() as ColumnListItem。
  5. 修复剩余的类型错误。优先使用实际类型,而不是 any / unknown as ...。
6a. 视图转换模式
  • <SplitApp> 根节点 ➜ 替换为 App.view.xml(FCL)——已在 5c 中完成。

  • Master 通过路由放入 FCL 的 beginColumnPages;Detail 放入 midColumnPages。

  • 每个视图中保留 <Page> 包装器——这样没问题。

  • 将 controllerName="<source_namespace>.<X>" ➜ 替换为 controllerName="<source_namespace>.modern.controller.<X>"。

  • 对于格式化器:旧版使用 formatter: 'window.com.demo.../statusText'(全局路径)。 现代版本使用 core:require + 相对格式化器:

    <List
        core:require="{ formatter: '<source_namespace>/modern/model/formatter' }"
        ...>
        <ObjectListItem
            title="{Title}"
            number="{ path: 'OverallStatus', formatter: 'formatter.statusText' }"
            numberState="{ path: 'OverallStatus', formatter: 'formatter.statusState' }"
            .../>
    </List>
    

    请注意,core:require 中的命名空间路径使用 /(斜杠表示法)。旧版的“点号” 属性名在格式化器绑定中仍然有效,因为 formatter.statusText 是相对于 core:require 映射解析的。

  • 关于 V2 ➜ V4 实体/路径转换,请参阅下文 6c。

  • 删除 sap.ui.commons.* 引用。

  • 如果 linter 提示 numberUnit、firstStatus、responsive、condensed、backgroundDesign 已弃用,则删除它们(视控件而定,这些属性可能已弃用,也可能没有)。

6b. 控制器转换模式(5 步详解)

对于每个 <X>.controller.js:

步骤 1:类语法

转换前:

sap.ui.define(["sap/ui/core/mvc/Controller"], function (Controller) {
    "use strict";
    return Controller.extend("com.demo.X.controller.Master", {
        onInit: function() { /* ... */ }
    });
});

转换后(请注意 @namespace 注解——这是必需的):

import Controller from "sap/ui/core/mvc/Controller";

/**
 * @namespace com.demo.X.modern.controller
 */
export default class Master extends Controller {
    public onInit(): void { /* ... */ }
}

(实际操作中应继承 BaseController,而不是直接继承 Controller——参见 6d。)

步骤 2:ES 导入

将所有 sap.ui.define/sap.ui.require 替换为 ES import。将动态的 sap.ui.require(["sap/m/MessageBox"], cb) 替换为 import("sap/m/MessageBox").then(...)——绝不要使用 jQuery.sap.require。

步骤 3:带类型的事件处理器(再说一次,陷阱 3——请阅读)

转换前:

onPress: function (oEvent) {
    var oItem = oEvent.getParameter("listItem");
    var oCtx = oItem.getBindingContext();
}

转换后(请注意:没有匈牙利命名法;使用具体事件类型):

import { List$SelectionChangeEvent } from "sap/m/List";
import ColumnListItem from "sap/m/ColumnListItem";

public onPress(event: List$SelectionChangeEvent): void {
    const item = event.getParameter("listItem") as ColumnListItem;
    const ctx = item.getBindingContext();
}

不要退而使用 import Event from "sap/ui/base/Event",因为这种通用类型会导致每次调用 getParameter/getParameters 时都需要使用 as 断言。如果看起来不存在具体事件类型,请先搜索:

mcp__SAPUI5_MCP_Server__get_api_reference(
  projectDir="<target>",
  query="sap.m.SearchField#liveChange"
)

对于 OData V4 感知上下文的事件,请使用 sap.ui.model.odata.v4.Context:

import V4Context from "sap/ui/model/odata/v4/Context";
const ctx = item.getBindingContext() as V4Context;
步骤 4:为泛型 getter 添加类型断言
import ODataModel from "sap/ui/model/odata/v4/ODataModel";
const model = this.getOwnerComponent()!.getModel() as ODataModel;
import Table from "sap/m/Table";
const tasksTable = this.byId("tasksTable") as Table;

(请注意:变量名为 model/tasksTable,而不是 oModel/oTable。)

步骤 5:V2 ➜ V4 绑定迁移

这是最大的非机械性部分。需要了解以下三种模式:

(i)路径中的实体键

V2 路径:/<EntitySet>('K') — 使用单引号括起的字符串键,带 Set 后缀。 V4 路径:/<Entity>(<KeyField>='K') — 不带 Set 后缀,使用命名键样式。

如果 V4 服务启用了 草稿(大多数由 RAP 管理的场景都是如此),键就是复合键:/<Entity>(<KeyField>='K',IsActiveEntity=true)。对于只读的自由风格 TS 应用,默认读取活动行,因此通常应使用 IsActiveEntity=true。

从绑定上下文反向推导——读取属性,不要解析路径:

// V2: var sKey = oCtx.getPath().replace(/^\/Set\('/, "").replace(/'\)$/, "");
// V4: just read the property
const key = ctx.getProperty("<KeyField>") as string;

如果不确定服务是否启用了草稿,请搜索:

mcp__sap-docs__search(query="Draft Handling with the OData V4 Model")
(ii)Expand

V2:parameters: { expand: "<Nav>" }(在 bindElement 中)。 V4:通过 parameters: { $expand: "_<Nav>" } 传递(注意下划线——RAP V4 服务通常会在关联名称前加上 _),或者在 manifest 模型配置中使用 autoExpandSelect: true,让 UI5 根据视图绑定自行处理。

**建议:**使用 autoExpandSelect: true 和基于路径的绑定(items="{_<Nav>}")。 除非有特定原因,否则不要手动编写 $expand。若要确认服务中的导航名称,请获取其 $metadata 并查找 <NavigationProperty Name="_X">。

(iii)Function-import ➜ action

V2:

oModel.callFunction("/<Function>", {
    method: "POST",
    urlParameters: { <Key>: sKey },
    success: function(oData) { /* ... */ },
    error: function(oErr) { /* ... */ }
});

V4(绑定操作——RAP 的典型模式):

import V4Context from "sap/ui/model/odata/v4/Context";
import ODataModel from "sap/ui/model/odata/v4/ODataModel";

const ctx = this.getView()!.getBindingContext() as V4Context;
const model = ctx.getModel() as ODataModel;
// Bound action: invoke relative to the entity context
const operation = model.bindContext("<Action.FQN>(...)", ctx);
try {
    await operation.invoke("$auto");          // newer V4 API; older: .execute()
    MessageToast.show("Action succeeded.");
    ctx.refresh();                             // pick up state changes
} catch (err) {
    MessageBox.error((err as Error).message);
}

完全限定的操作名称(<Action.FQN>)因服务而异。对于 RAP V4,其格式类似于 com.sap.gateway.srvd.<service_name>.v0001.<action_name>。要查找服务对应的名称,请检查 $metadata 并查找 <Action Name="X" IsBound="true">——父级 <Schema Namespace> 属性加上 .X 即为 FQN。如果不确定 API 的形式,请使用 mcp__sap-docs__search(query="OData V4 Operations action invocation UI5")。

步骤 6:将私有字段移至类属性

之前(旧版):

onInit: function () {
    this._sProjectId = undefined;  // implicit, later
}

之后:

export default class Detail extends BaseController {
    private projectId?: string;     // no Hungarian; plain field name

    public onInit(): void { /* ... */ }
}
6b.1 V4 相对路径绑定应放在 XML 视图中,而不是控制器中

当选择父行后需要刷新子表绑定时(例如“选择一个任务 → 重新加载时间条目”),V4 惯用模式是:

  1. 在 XML 视图中声明 items 绑定,并相对于父上下文的导航关联(_<Nav>):

    <Table id="<childTable>"
        items="{
            path: '_<Nav>',
            parameters: { $filter: 'IsActiveEntity eq true' }
        }">
        <columns>
            <Column><Text text="{i18n>colA}" /></Column>
            
        </columns>
        <items>
            <ColumnListItem>
                <cells>
                    <Text text="{<FieldA>}" />
                    
                </cells>
            </ColumnListItem>
        </items>
    </Table>
    
  2. 在控制器中选择父行时设置绑定上下文——UI5 会根据新上下文解析相对的 _<Nav> 关联:

    const childTable = this.byId("<childTable>") as Table;
    childTable.setBindingContext(parentContext);
    

不要在控制器中通过 new ColumnListItem({ cells: [...] }) 构造 ColumnListItem 模板行,然后调用 bindItems({ path, template, templateShareable })。虽然这样可行,但这是命令式的 V2 模式;在 V4 中,XML 加上下文的方式更简洁、声明性更强,也更符合 UI5 的预期。任何会分配模板行的控制器方法都是一种异味。

6c. 整合 Formatter

将旧版全局变量(window.com.demo...formatter.statusText 等)提取到一个 ES 模块中:

Write: <target>/webapp/model/formatter.ts
import DateFormat from "sap/ui/core/format/DateFormat";

// instantiate once — not per call
const oDateMedium = DateFormat.getDateInstance({ style: "medium" });

/**
 * @namespace <source_namespace>.modern.model
 */
const formatter = {
    statusText(sStatus?: string): string {
        switch (sStatus) {
            case "A": return "Approved";
            case "D": return "Draft";
            case "X": return "Cancelled";
            default:  return sStatus ?? "";
        }
    },
    statusState(sStatus?: string): "Success" | "Warning" | "Error" | "None" {
        if (sStatus === "A") return "Success";
        if (sStatus === "X") return "Error";
        if (sStatus === "D") return "Warning";
        return "None";
    },
    // ...taskStatusText, taskStatusState, priorityText, priorityState, dateShort, hoursDecimal
    dateShort(oDate?: Date): string {
        return oDate ? oDateMedium.format(oDate) : "";
    },
};

export default formatter;

视图通过 core:require="{ formatter: '<source_namespace>/modern/model/formatter' }" 引用,然后在绑定中使用 formatter: 'formatter.statusText'(或者在相对于控制器时使用 formatter: '.formatter.statusText'——两种方式都可行;core:require 形式更简洁,且无需控制器字段)。

6d. 逐对编写
Write: <target>/webapp/view/<X>.view.xml
Write: <target>/webapp/controller/<X>.controller.ts

每完成一对文件,就只针对新文件运行 linter:

mcp__SAPUI5_MCP_Server__run_ui5_linter(
  projectDir="<absolute target>",
  filePatterns=["webapp/controller/<X>.controller.ts", "webapp/view/<X>.view.xml"],
  provideContextInformation=true
)

修复所有问题后再继续。不要让各个视图累积 lint 债务。

6e. i18n 迁移(重命名:masterX → mainX)

将 <legacy>/webapp/i18n/i18n.properties 复制到 <target>/webapp/i18n/i18n.properties,但 应用命名覆盖重命名:

旧键新键
masterTitlemainTitle
masterSearchPlaceholdermainSearchPlaceholder
masterCountmainCount

更新现代 XML 视图中对应的 {i18n>X} 引用。

另请使用相同内容编写 <target>/webapp/i18n/i18n_en.properties(UI5 指南: 添加键时,要同步更新所有语言环境文件)。如果旧应用提供了德语或 其他语言环境文件,也一并复制,并应用相同的重命名。

6f. Welcome + NotFound 视图

这些视图很简单——只包含 MessagePage。按 1:1 翻译 XML;如果未使用 xmlns:core,则移除;确保 复用旧应用中的 i18n 键。


第 7 阶段 — 验证

按以下顺序运行全部四项检查。全部通过后才能宣布转换完成。

**重要 — 这些检查无法捕获的问题:**检查报告干净并不意味着应用 能正常渲染。尤其是陷阱 5(manifest v2 + 已弃用的路由键 / 缺少 type: "View")会通过以下所有检查,但运行时会显示空白页面。 第 8d 阶段的浏览器渲染检查才是唯一可靠的验收关卡。陷阱 5 中的表格 列出了实际工具覆盖情况。

7a. ESLint 自动修复(首先执行 — 在人工审查前)
Bash: cd <target> && npx eslint webapp --fix

此步骤会自动解决大部分 @typescript-eslint/no-unnecessary-type-assertion、 no-unused-vars 和 no-unused-imports 问题。在人工审查前执行此步骤, 可以省去一个完整的迭代周期(否则:tsc 通过 → 人工检查 lint → 修复机械性问题 → 重新 lint)。

自动修复后,重新检查仍有人工处理问题的文件(通常为 1–2 个)。

7b. UI5 检查工具
mcp__SAPUI5_MCP_Server__run_ui5_linter(
  projectDir="<absolute target>",
  provideContextInformation=false   # avoid bloating output on the full-project run
)

预期结果:零条发现。常见误报很少;大多数发现都有工具输出中建议的实际修复方法。 只有在用户确认建议的修复看起来正确后,才使用 fix=true 参数。

7c. Manifest 验证
mcp__SAPUI5_MCP_Server__run_manifest_validation(
  manifestPath="<absolute target>/webapp/manifest.json"
)

预期结果:零错误。有关未使用 i18n 键的警告可以接受。

7d. TypeScript 类型检查
Bash: cd <target> && npm run ts-typecheck

预期结果:零错误。宣布完成前修复所有错误——不要使用 // @ts-ignore 抑制错误。


第 8 阶段 — 冒烟测试

8a. 安装并启动服务
Bash: cd <target> && npm install
Bash: cd <target> && (npm start &)

等待约 5 秒,让开发服务器启动。

8b. HTTP 探测(必要但不充分)
Bash: curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/index.html
Bash: curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/manifest.json

预期结果:两者均返回 200。

服务器注意事项(UI5 指南 §2):UI5 开发服务器不会提供默认的 index 文件。始终探测 http://localhost:8080/index.html,不要探测 http://localhost:8080/。

**重要:**HTTP 200 并不能证明 UI 已渲染。陷阱 1(FCL 卡在 OneColumn) 和陷阱 2(高度为零的空白页面)都会返回 200 OK。第 8d 节中的浏览器渲染检查才是 唯一可靠的验收关卡。

8c. 通过代理探测 OData
Bash: curl -s "http://localhost:8080<v4_service_url>\$metadata" | head -5
# where <v4_service_url> is the path from sap.app.dataSources.mainService.uri

预期结果:有效的 <edmx:Edmx ...> XML。如果返回 401,则 .env 中缺少代理凭据或凭据错误。如果 返回 404,则 manifest 中的服务绑定名称错误(重新检查 FE 应用 manifest 中的 规范 URL)。

8d. 浏览器渲染验证(关键——这是实际验收关卡)

根据可用的 MCP,运行以下其中一项。如果都不可用,请让用户 手动验证。

选项 A — Chrome MCP(首选):

mcp__Claude_in_Chrome__navigate(url="http://localhost:8080/index.html")
mcp__Claude_in_Chrome__get_page_text       # must include project titles from V4
mcp__Claude_in_Chrome__javascript_tool(
  code="document.querySelector('.sapFFCL').clientHeight"
)
# Expected: a number > 400 (a normal dev viewport is ~600-1000 tall)

选项 B — Claude_Preview MCP:

mcp__Claude_Preview__preview_start(url="http://localhost:8080/index.html")
mcp__Claude_Preview__preview_screenshot    # confirm two columns visible
mcp__Claude_Preview__preview_eval(
  expression="document.querySelector('.sapFFCL').clientHeight"
)

选项 C — 手动(两种 MCP 均不可用时):请用户打开 URL,并确认 双列渲染后再宣布技能已完成。

验证目标:

  • FCL 布局为双列,而非单列。如果看到单列,则触发了陷阱 1。
  • .sapFFCL 元素的 clientHeight > 400 px。如果为 0 或接近 0,则触发了陷阱 2。
  • 左列:显示 V4 后端数据的主列表(数量和字段与旧版应用针对相同数据所显示的内容一致)。
  • 右列:Welcome IllustratedMessage 占位内容。
  • 点击某一行后,FCL 切换为双列(或保持双列,具体取决于初始布局), 并在中间列显示 Detail 视图。
  • Detail 视图显示实体标题,以及旧版应用中的所有标签页/区域。
  • 操作按钮(例如批准/提交/取消按钮)的行为遵循旧版应用的表达式绑定规则——仅当实体处于相应状态时才启用。
8e. 最终报告

打印:

Modernization complete — <target>

UI5 version:    1.147.2 SAPUI5 (TypeScript)
Theme:          sap_horizon
Layout:         sap.f.FlexibleColumnLayout
OData:          V4 — <v4_service_url>
Views:          <count> (all under FCL columns)
Controllers:    <count> (all extend BaseController)
Linter:         clean
Manifest:       valid
TypeCheck:      clean
Smoke:          200 OK on index.html + manifest.json + $metadata; UI renders Master list

UI5 MCP calls:  <count> (get_typescript_conversion_guidelines, get_guidelines,
                create_ui5_app, run_ui5_linter ×N, run_manifest_validation,
                get_api_reference ×M where needed)
sap-docs:       <count> (only when V2 ➜ V4 patterns needed lookup)
arc-1:          <0 unless you needed to look up a V4 service binding URL>

What's next:
  - Open http://localhost:8080/index.html. Master list should render with projects.
  - Compare side-by-side with legacy app on its port and FE app on its port — three UIs,
    one backend.
  - Optional follow-up: add OPA5 page-object journey + QUnit formatter tests (see TS-conversion
    guidelines §Test Conversion for the modern OPA pattern).
  - Optional follow-up: add German i18n locale (i18n_de.properties).

错误处理——已知模式

症状原因修复方法
页面渲染为空白;控制台无错误;DOM 已填充,但所有元素的高度均为 0UI5 ComponentSupport 移除了 data-sap-ui-component 属性,因此任何依赖该属性的 CSS 高度选择器都无法匹配(陷阱 2)直接在 index.html 中组件 <div> 上添加内联 style="height: 100%"。CSS 选择器不起作用。
页面空白,列为空,控制台警告 "page stack is empty but should have been initialized"Manifest _version 为 2.0.0 或更高版本(UI5 1.136+),且路由目标缺少显式的 "type": "View",和/或仍使用已弃用的 viewName/viewPath/viewLevel 键(陷阱 5)。@ui5/linter 和 run_manifest_validation 都不会标记此问题——两者均报告无问题。在 routing.config 和每个目标中添加 "type": "View";将 viewName 改为 name、viewPath 改为 path、viewLevel 改为 level。通过浏览器渲染验证(阶段 8d),不要依赖 lint/验证关卡。
仅显示一个 FCL 列;路由将内容放入中间/末尾列,但该列被隐藏匹配的路由缺少 layout 属性(陷阱 1)为每条路由(包括首页/主页路由)添加 "layout": "TwoColumnsMidExpanded"(或适当的值)。路由器只会根据匹配路由的 layout 值更新 FCL 布局。
TypeScript 强制使用类似 (event.getParameters() as {listItem: ListItem}).listItem 的类型转换导入的是 sap/ui/base/Event 中的泛型 Event,而不是特定的 <Control>$<Event>Event(陷阱 3)替换为特定事件类型,例如 import { List$SelectionChangeEvent } from "sap/m/List"。如不确定,请使用 get_api_reference。
create_ui5_app 未返回错误,但 <target>/webapp/Component.ts 是一个 .js 文件UI5 MCP 回退到了 JS 模板明确使用 typescript: true 重新调用;确认 frameworkVersion 受支持(必须 >= 1.96)
run_ui5_linter 在全新脚手架项目中发现数百个问题模板语言不匹配(生成的是 JS 脚手架,但预期为 TS)同上——重新生成脚手架
tsc --noEmit 报错,提示缺少 sap/m/... 类型未安装 @sapui5/types 或版本不正确cd <target> && npm install --save-dev @sapui5/[email protected]
tsc --noEmit 因 event.getParameter("X") 返回 unknown 而报错使用了 sap/ui/base/Event 中的泛型 Event 类型替换为控件专用事件类型:import { Button$PressEvent } from "sap/m/Button"。需要 UI5 ≥ 1.115(指南 §1 子项目符号)
npm start 失败,提示 "port 8080 in use"旧版应用的 npm start 已在 8080 端口运行先停止旧版应用,或在其他端口运行新版应用:ui5 serve -p 8081
浏览器显示空白页;控制台提示:"failed to load Component.js"data-sap-ui-resource-roots 与 sap.app.id 不匹配两者必须使用相同的完整限定命名空间(<source_namespace>.modern)
OData 代理返回 401.env 缺失或凭据错误cp .env.example .env,设置 UI5_MIDDLEWARE_SIMPLE_PROXY_USERNAME 和 ..._PASSWORD
OData 代理返回 502/证书错误与使用自签名证书的上游服务 TLS 握手失败对于 ui5-middleware-simpleproxy,设置 strictSSL: false(不要使用 skipCertificateCheck——该键会被静默忽略)。对于 fiori-tools-proxy,键为 ignoreCertErrors: true。请阅读中间件的 README——参见自助排查模式 A。
FCL 在导航时不切换布局缺少每条路由的 layout 属性,或 controlAggregation 设置错误Master ➜ beginColumnPages,Detail ➜ midColumnPages;路由应设置 "layout": "TwoColumnsMidExpanded"
MessageBox.show(...) 运行时错误:"not a function"导入路径错误import MessageBox from "sap/m/MessageBox";——旧版 sap.ui.commons 路径已不存在
Lint 将格式化函数标记为 "unused"TS 中导入了格式化函数,但视图使用 core:require(linter 并不总会交叉检查)在导出项上添加 JSDoc @public 以抑制警告,或者添加控制器级引用(public formatter = formatter;)
V4 bindContext("/Action").execute() 因 "no metadata" 被拒绝服务元数据仍在加载首次执行操作前运行一次 await oModel.requestObject("/");或者运行 oModel.attachOnce("metadataLoaded", ...),并据此控制操作按钮
V4 绑定静默返回空列表关联名称错误(V2 <Nav> 与 V4 _<Nav> 不同)RAP V4 服务会在关联名称前加 _。检查 $metadata 中的 <NavigationProperty Name=...>,确认规范名称。
绑定到 /<EntitySet> 的 V4 列表返回 404沿用了 V2 集合名称去掉 Set 后缀:/<Entity>(以实体类型而非实体集命名)
npx tsc --noEmit 报错,提示 JQuery 类型缺失缺少 @types/jquerynpm install --save-dev @types/jquery;确认 tsconfig.types 包含 @types/jquery

此技能明确不涵盖的内容

  • 添加新功能。 这是一项翻译技能——仅实现与旧版应用功能一致。如果旧版应用没有搜索/筛选功能,现代应用也不会添加。
  • 测试。 OPA5 + QUnit 脚手架属于后续工作——这样可以缩小第 1 轮的范围。准备好后,可参阅 TS 转换指南 §测试转换,了解现代 OPA5 模式。
  • 自定义控件。 旧版应用没有自定义控件。如果你的应用有,请参阅 TS 转换指南 §UI5 控件 TypeScript 转换(这是额外步骤,需要使用 @ui5/ts-interface-generator 并手动复制构造函数签名)。
  • 无障碍审计。 UI5 1.147 控件默认符合无障碍要求,但 axe + 手动屏幕阅读器测试属于独立交付项。
  • Fiori Launchpad 集成。 现代应用独立运行。FLP 磁贴相关工作应归入 FE 技能或单独的 FLP 技能。
  • 超出 sap_horizon 范围的主题自定义。 自定义主题需要构建 themelib_*。
  • 超出 ui5 build 范围的构建时打包。 不使用 Webpack/Vite。

运行此技能的 LLM 注意事项

  • 编写任何代码前,请先阅读上文的「关键陷阱」部分。 过去的运行中,大多数调试时间都耗在三个问题上:FCL 路由缺少 layout、缺少内联 style="height: 100%",以及导入通用的 Event。即使技能的其他部分都完美无缺,跳过这些检查也会让你多花 15-30 分钟。
  • 静默应用命名覆盖规则。 使用「main」,不要用「master」;不要使用匈牙利命名法前缀。不要为了保持「一致性」而保留旧命名——旧命名本身就是我们要现代化的内容之一。
  • 始终先调用 get_typescript_conversion_guidelines 和 get_guidelines。 其中的内容量大且与版本相关;不要凭记忆转述。
  • 运行过程中,使用 sap-docs MCP 临时查询最佳实践。 如果不确定路由配置、V4 绑定行为、草稿语义、FCL 布局值等,请搜索 mcp__sap-docs__search 并获取具体主题。不要猜测。
  • 每个视图的粒度很重要: 完整完成一个视图(XML + 控制器 + 格式化器 + lint)后,再处理下一个。不要让技术债务在多个视图间累积。
  • 旧 API 名称在现代目标中消失时(例如控件重命名或方法被替换),请查阅 get_api_reference。 不要猜测。
  • 绝不要擅自降低指南指定的依赖版本——只能升级。
  • 不要使用 any 或 unknown as ... 类型断言。 使用正确的控件类型。any 类型断言会掩盖 V2 与 V4 绑定问题,这些问题会在运行时表现为「undefined.replace is not a function」错误。
  • OData V4 的路径语义与 V2 不同。 不要沿用 getPath().replace(/^\/Set/, "") 这类模式——正确做法是 ctx.getProperty("Key")。
  • 最终 lint 检查前,请删除 console.log 调试语句。 旧版应用中有许多 console.log("[X] ...") 行——它们适合用于展示旧版最差实践,但不应出现在现代化后的版本中。
  • 彻底删除「var that = this;」和后续的 .bind(that) 模式——箭头函数能在 TS 中正确处理 this。
  • 原样删除旧版非英文 JSDoc 注释(如「Worst-practice 2020-Mix...」、「manuell, weil immer schon so gemacht」等)。这些注释描述的是旧版状态,与现代化后的代码相矛盾。请改用简洁的英文描述说明现代控制器的用途。
  • 在 UI5 linter 运行前,先执行 npx eslint webapp --fix。 它会自动解决 no-unnecessary-type-assertion、no-unused-vars 和 no-unused-imports——这三类问题占转换后 lint 技术债的大部分。跳过这一步会多出一个完整的迭代周期(tsc 通过 → ui5lint 失败 → 修复机械性问题)。
  • 按下事件处理程序通常不带参数。 onApprove、onNavBack、onItemPress(使用 this.byId 而不是事件源时)应声明为 (),而不是 _event: Button$PressEvent。后者会导致导入未使用,需要再编辑一次。参见上文陷阱 4。
  • ARC-1 对此技能是可选的。 仅在需要查询 V4 服务绑定 URL(例如 SAPRead type=SRVB name=<service_binding>),或确认后端 CDS/行为定义中的状态码/枚举映射时使用;否则跳过。
  • 这里不使用 fiori-mcp——它适用于带注解的 Fiori Elements,而不是使用手动控件的自由布局 TS 应用。
  • 如果存在使用同一后端的同级参考应用(例如你已基于同一 V4 服务构建了 Fiori Elements 应用),请先阅读其 manifest.json 和 ui5.yaml。这些文件是经过验证可用的参考,其中包含 V4 URL、代理配置、导航关联以及任何身份验证方面的特殊情况——复制这些值比重新摸索更快。
  • 服务器注意事项: UI5 开发服务器没有默认索引页。始终检查 /index.html。
  • UI5 指南 §4 禁止使用 SimpleForm。 如需表单,请使用 Form + ColumnLayout(此处的旧版应用没有表单,因此无需处理)。
  • 对于内置格式化(数字、日期、货币),请优先使用 OData 类型(sap.ui.model.odata.type.*),再考虑自定义格式化器。自定义格式化器仅用于独特的业务逻辑(例如没有内置等效实现的状态码 → 文本映射)。

评论

支持 Markdown