SAP UI5 專案架構

SAP UI5專案架構

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
├── node_modules              # 
webapp/ # 應用程式的核心根目錄

├── controller/ # 邏輯控制層 (Controller)
│ ├── App.controller.js
│ └── Main.controller.js

├── view/ # 介面顯示層 (View,通常為 XML 格式)
│ ├── App.view.xml
│ └── Main.view.xml

├── model/ # 資料模型層 (Model,處理本地或全域資料)
│ ├── formatter.js # 欄位格式化工具
│ └── models.js # 初始化模型的工廠方法

├── i18n/ # 多國語言語系檔
│ ├── i18n.properties # 預設語系
│ └── i18n_zh_TW.properties

├── css/ # 自訂樣式表 (選填)
│ └── style.css

├── Component.js # 應用的入口組件,負責初始化與路由啟動
├── manifest.json # 核心元數據描述文件(描述配置、資料源、路由)
└── index.html # 獨立運行的本地測試入口網頁
├── .gitignore git版控
├── package.json # 套件與指令設定
└── README.md # 專案說明

核心架構元件解析

  • manifest.json (App Descriptor):
  1. 核心配置中心:定義應用程式的識別碼(ID)、版本、所依賴的 UI5 核心庫,以及多國語言檔路徑。
  2. 資料源與模型宣告:在此處定義與後端溝通的 dataSources(例如 SAP OData 服務),並直接綁定至全域 models。
  3. 路由配置 (Routing):定義導頁邏輯(Routes)與對應的顯示目標(Targets),實現單頁應用(SPA)的切換。
  • Component.js
  1. 應用啟動點:繼承自 sap.ui.core.UIComponent。
  2. 自動載入:框架會引導此檔案自動讀取 manifest.json。它的主要職責是例項化應用的根視圖(Root View)並初始化路由服務。
  • View (視圖層):
  1. 宣告式 UI:強烈建議使用 XML 格式(如 Main.view.xml)。
  2. 核心職責:僅負責佈局與控制項定義(如表格、輸入框、按鈕),不包含任何業務邏輯,確保介面與邏輯分離。
  • Controller (控制層):
  1. 行為處理:使用 JavaScript 撰寫(如 Main.controller.js),負責處理視圖上觸發的事件(如點擊按鈕、輸入驗證)。
  2. 資料操作:透過控制項獲取模型,或將後端資料更新至模型中。
  • Model (模型層與資料綁定):
    雙向綁定:支援 JSONModel(本地雙向)、ResourceModel(多國語言 i18n)以及 ODataModel(後端伺服器資料同步)。

兩種主要的開發架構型態

  1. SAP UI5 Free-style (自由開發):
  2. 依循上述標準 MVC 架構,開發者擁有極高的 UI 客製化彈性。
  3. 適用於業務邏輯極度特殊、現有標準元件無法滿足的企業流程。
  4. SAP Fiori Elements (配置驅動開發):
  5. 無代碼/低代碼 UI:開發者不需要手動撰寫 View 和 Controller。
  6. 元數據驅動:完全依賴後端 OData 服務的註解 (Annotations)與元數據來自動渲染標準的 Fiori 頁面(例如:List Report、Object Page)。
  7. 適用於高標準化、講求快速交付的增刪查改(CRUD)應用。

manifest.json 解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
{
"_version": "1.12.0",
"sap.app": {
"id": "my.app", // 應用的唯一識別碼
"i18n": "i18n/i18n.properties", // 多國語言文字檔的路徑
"title": "{{appTitle}}", // 應用標題,對應 i18n 裡的 key
"description": "{{appDescription}}",
"applicationVersion": {
"version": "1.0.0"
},
"dataSources": {
"orderApi": {
"url": "https://script.google.com/macros/s/AKfycby3VIvl_TU4O_KoFUNXlvfINS0Lv1CEZuU5AoY7YU9JaFTgZ2x1f1lvStjoIuUldREFKw/exec",
"type": "JSON"
}
}
},
"sap.ui": {
"technology": "UI5",
"icons": {},
"deviceTypes": {
"desktop": true,
"tablet": true,
"phone": true
}
},
"sap.ui5": {
"rootView": {
"viewName": "my.app.view.Main", // 應用的第一個根視圖(通常只包含 sap.m.App 控制項)
"type": "XML",
"async": true,
"id": "app"
},
"resources": {
"css": [
{
"uri": "css/style.css"
}
]
},
"dependencies": {
"minUI5Version": "1.71.66",
"libs": {
"sap.ui.core": {},
"sap.m": {}, // 必備的行動與桌面端核心 UI 庫
"sap.tnt": {},
"sap.f": {}
}
},
"handleValidation": true,
"contentDensities": {
"compact": true,
"cozy": true
},
"models": {
"i18n": {. // 宣告全域多國語言模型
"type": "sap.ui.model.resource.ResourceModel",
"settings": {
"bundleName": "uxc.integration.i18n.i18n",
"async": true
}
},
"": { // 空白字串代表「預設模型」,通常綁定主要 OData 服務
"dataSource": "mainService",
"preload": true,
"settings": {
"v2Mode": true,
"defaultBindingMode": "TwoWay" // 設定雙向綁定(畫面改,資料改)
}
},
"ui": {
"type": "sap.ui.model.json.JSONModel"
}
},
"routing": { ... }
}
}

參考資料

index.html
引入 SAPUI5 核心庫
全部語法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SAPUI5 1.71 Google Sheets CRUD 系統</title>
<!-- 引入 SAPUI5 核心庫 src="https://sdk.openui5.org/resources/sap-ui-core.js" -->

<script
src="https://sdk.openui5.org/resources/sap-ui-core.js"
data-sap-ui-theme="sap_fiori_3"
data-sap-ui-resourceroots='{"my.app": "./"}'
data-sap-ui-compatVersion="edge"
data-sap-ui-oninit="module:sap/ui/core/ComponentSupport"
data-sap-ui-async="true"
>
</script>
</head>
<body class="sapUiBody" id="content">
<div data-sap-ui-component data-name="my.app"></div>
</body>
</html>

Component.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sap.ui.define([
"sap/ui/core/UIComponent"
], function (UIComponent) {
"use strict";
return UIComponent.extend("my.app.Component", {
metadata: {
manifest: "json"
},
init: function () {
UIComponent.prototype.init.apply(this, arguments);

}
});
});



Main.view.xml 如同 html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<mvc:View
controllerName="my.app.controller.Main"
xmlns:mvc="sap.ui.core.mvc"
xmlns="sap.m"
xmlns:f="sap.ui.layout.form"
xmlns:core="sap.ui.core">
<App>
<Page title="SAPUI5 1.71.66 Google Sheets RESTful CRUD 系統">
<content><!-- 使用 SimpleForm 構建表單 -->
<f:SimpleForm
editable="true"
layout="ResponsiveGridLayout"
labelSpanXL="3"
labelSpanL="3"
labelSpanM="3"
labelSpanS="12"
adjustLabelSpan="false"
emptySpanXL="4"
emptySpanL="4"
emptySpanM="4"
emptySpanS="0"
columnsXL="1"
columnsL="1"
columnsM="1"
singleContainerFullWidth="true">
<f:content>
<VBox>
<Label text="產品名稱" required="true" />
<Input
placeholder="請輸入產品名稱..."

/>

<Label text="活動區域" required="true" />
<Select
id="zoneSelect"
selectedKey="{/selectedZone}"
items="{path: '/zones'}"
change="onZoneChange"
>
<items>
<!-- key 是傳給後台的值,text 是顯示給使用者看的文字 -->
<core:Item key="{key}" text="{text}" />
</items>
</Select>

<!-- 職業 -->
<Label text="職業" required="true" />
<Select placeholder="請選擇職業"
selectedKey="{/selectedTags}"
items="{path: '/tags'}"
change="onTagsChange"
>
<core:Item key="{tagId}" text="{tagName}" />

</Select>
<Label text="活動時間" required="true" />
<!-- 運用 HBox 將日期與時間選擇器並排 -->
<HBox alignItems="Center">
<DatePicker
placeholder="Pick a date" valueFormat="yyyy-MM-dd"
displayFormat="yyyy/MM/dd"
change="onDateChange"
/>
<Text text="-" class="sapUiTinyMarginEnd" />
<TimePicker placeholder="Pick a time" />
</HBox>
<!-- 即時送達 -->
<Label text="即時送達" />
<Switch state="false" type="AcceptReject" />

<!-- 活動地點 -->
<Label text="活動地點" required="true" />
<SegmentedButton selectedKey="home">
<items>
<SegmentedButtonItem key="home" text="Home" />
<SegmentedButtonItem key="company" text="Company" />
<SegmentedButtonItem key="school" text="School" />
</items>
</SegmentedButton>
<!-- 活動類型 -->
<Label text="活動類型" required="true" />
<VBox>
<HBox>
<CheckBox text="公司" class="sapUiLargeMarginEnd" />
<CheckBox text="車站" />
</HBox>
<HBox>
<CheckBox text="辦公室" class="sapUiLargeMarginEnd" />
<CheckBox text="捷運" />
</HBox>

<!-- 資源 -->
<Label text="資源" required="true" />
<RadioButtonGroup columns="2">
<buttons>
<RadioButton text="Sponsorship" />
<RadioButton text="Venue" />
</buttons>
</RadioButtonGroup>
<!-- Activity form -->
<Label text="Activity form" required="true" />
<TextArea rows="4" growing="true" width="100%" />
</VBox>
<Button
text="送出資料"
press="onButtonClick"
type="Emphasized" />
</VBox>
</f:content>
</f:SimpleForm>
</content>
</Page>
</App>
</mvc:View>

Main.controller.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/model/json/JSONModel",
"sap/m/MessageBox",
"sap/m/MessageToast",
"sap/m/Dialog",
"sap/m/Button",
"sap/ui/layout/form/SimpleForm",
"sap/m/Label",
"sap/m/Input",
], function (Controller, JSONModel, MessageBox, MessageToast, Dialog, Button, SimpleForm, Label, Input)
{
"use strict";

return Controller.extend("my.app.controller.Main", {
// 初始化
onInit: function ()
{
var oDataSource = this.getOwnerComponent().getMetadata().getManifestEntry("sap.app").dataSources.orderApi;
this._sBaseUrl = oDataSource.url;
console.log('this._sBaseUrl', this._sBaseUrl)
this.onReadData()
// 1. 準備下拉選單的資料來源
var oData = {
// Select 的資料
zones: [
{ key: "Z1", text: "台北總部" },
{ key: "Z2", text: "台中分部" },
{ key: "Z3", text: "高雄辦事處" }
],
// 預設選中台北
selectedZone: "Z1",

// ComboBox 的資料
owners: [
{ id: "EMP001", name: "王大同" },
{ id: "EMP002", name: "李小美" },
{ id: "EMP003", name: "張三豐" }
],
selectedOwner: "",

// MultiComboBox 的資料
tags: [
{ tagId: "T1", tagName: "科技" },
{ tagId: "T2", tagName: "金融" },
{ tagId: "T3", tagName: "教育" }
],
selectedTags: ["T1"] // 預設多選勾選科技與金融
};

// 2. 建立 JSON 模型並設置到 View 上
var oModel = new JSONModel(oData);
this.getView().setModel(oModel);
},

onReadData: function ()
{
jQuery.ajax({
url: this._sBaseUrl ,
method: "GET",
dataType: "json",
success: function (aData) {
console.log('aData',aData)
// 將拿到的陣列塞入 JSONModel
this._oQuizModel.setProperty("", aData);
oTable.setBusy(false);
MessageToast.show("題庫加載完畢!");
}.bind(this),
error: function (oError) {
oTable.setBusy(false);
MessageBox.error("加載失敗,原因:" + oError.responseText);
}
});
},
// 下拉選單數值改變時的事件監聽
onZoneChange: function (oEvent)
{
console.log('onZoneChange',oEvent)
var oSelectedItem = oEvent.getParameter("selectedItem");
var sSelectedKey = oSelectedItem.getKey();
var sSelectedText = oSelectedItem.getText();

console.log("使用者選擇了 Key: " + sSelectedKey + " , Text: " + sSelectedText);
},
onTagsChange: function (oEvent)
{
console.log('onTagsChange',oEvent)
var oSelectedItem = oEvent.getParameter("selectedItem");
var sSelectedKey = oSelectedItem.getKey();
var sSelectedText = oSelectedItem.getText();
console.log("使用者選擇了 Key: " + sSelectedKey + " , Text: " + sSelectedText);
},
onDateChange: function (oEvent) {
// === 方法 A:取得格式化後的字串(例如 "2026-07-13")===
// 這是最常用的做法,可以直接拿來傳給後端 API
var sValue = oEvent.getParameter("value");
var bValid = oEvent.getParameter("valid"); // 檢查使用者輸入的日期是否合法

if (!bValid) {
MessageToast.show("請輸入正確的日期格式!");
return;
}

// === 方法 B:取得原生 JS Date 物件 ===
// 如果你需要拿這個日期去做計算(例如計算天數、判斷是否大於今天)
var oDatePicker = oEvent.getSource();
var oDateObject = oDatePicker.getDateValue(); // 得到標準的 Date 物件

// 測試輸出
console.log("字串格式 (value):", sValue);
console.log("JS 物件 (dateValue):", oDateObject);

MessageToast.show("您選擇了:" + sValue);
},
onCreatePress: function () {
MessageToast.show("表單已成功創建!");
},

onResetPress: function () {
MessageToast.show("表單已重置。");
// 在此處加入清除各控制項欄位值的邏輯
}
})
})

使用node 來啟動

package.json 套件與指令設定
scripts 使用ui5 serve –port 8080 -o index.html

1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "sapui5-google-sheets-crud-app",
"version": "1.0.0",
"description": "SAPUI5 1.71 Google Sheets App with UI5 Tooling Local Proxy",
"scripts": {
"start": "ui5 serve --port 8080 -o index.html"
},
"devDependencies": {
"@ui5/cli": "^3.0.0",
"ui5-middleware-simpleproxy": "^3.1.2"
}
}

npm 安裝會產生一個node_modules資料夾

1
npm install 

啟動

1
node run start