前面我們實現了基于組件的多頁面程序,這個程序還有兩個主要的缺點:
1)存在全局變量oApp (sap.m.App)
;
2)多個頁面全部在程序啟動時加載,多個頁面也是在同一個 URL 下由
Openui5 框架進行管理,這種模式滿足不了多頁面的需求。解決的辦法是:路由 ( routing ) 。通過 routing 實現多頁面的導航,異步實現按需加載。
本篇仍然基于之前 Master-detail 供應商顯示這支程序進行重構,使用 routing 實現頁面間導航,將 Component 的元數據(metadata),移到專門的文件中。
OpenUi5 的 Routing
Openui5 的 routing 基于模式 ( pattern ),使用 #
符號表示不同的路徑 ( route ),導航通過路徑的改變來實現。
首先我們用 SAP 官方教程的圖片來說明路由的實現方式。
-
路由器 ( router ) 負責路由的管理。當 URL 的路徑改變時,Router 的偵聽機制可以得知這個改變,根據新的路徑在配置文件中查找路徑表達式所對應的目標值 ( target ),目標值的設定中有要加載的視圖 ( View),所以 Router 就加載相應的視圖來展現界面。
- 比如上圖中,當路徑為
#/object/ID_5
,Router 在配置文件中查找到,路徑為object/{objectId}
時,應該調用Object View
展現數據。由 Router 通知 app 進行相應處理。
- 比如上圖中,當路徑為
路徑 ( route ) 用于通知 application 路徑的變化。比如
index.html#
i表示起始頁,index.html#/detail/0
表示明細頁面的第一條記錄。這些都是不同的路徑。對于每一個路徑,都是一個不同的 模式 ( pattern ) ** 。模式確定了路徑和目標 ( target ) 的匹配關系**。比如
index.html#/detail/0
這個路徑,模式為detail/{supplierPath}
,這個模式對應的 target 為detail
( target 下面進行說明)。**目標 ( target ) **定義需要加載的 view, 確定 view 的級別。級別可以增加顯示的效果。比如上面 detail 這個 target,應該加載的 view 為
Detail
。路由可以在
Component.js
的metadata
部分配置,也可以在
manifest.json
這個文件中配置,這個文件被稱作 Application Descriptor 。也可以在代碼中調用類的構造器來設置。一般是在manifest.json
文件中配置。
總結為一句話:路由器負責管理,對于不同的路徑,在 pattern 中找匹配,在 target 中找視圖 ( view )。
Pattern 表達式
Openui5 一共有 5 種 pattern表達式
:
硬編碼模式:頁面之間根據模式導航,沒有參數傳遞,比如
product/settings
表示導航到產品配置。路徑含有必輸參數模式:模式中 大括號({}) 包含的部分表示參數必須輸入。比如
product/{id}
表示導航到產品某一 id,比如product/5
表示 id 為 5 的產品,id 為必輸。路徑含有可選參數模式:模式中 冒號 包含的部分為必輸參數。比如
product/{id}/detail/:detailId:
,detailId
為可選參數。product/5/detail
以及product/3/detail/2
都能與此模式匹配。路徑含有查詢參數模式:查詢參數 ( query parameter ) 在問號之后。比如
product{?query}
,query 這個參數為必輸項。product:?query:
中的 query 這個參數為可選參數。通配參數模式:以星號結尾的參數是通配參數,通配參數將根據模式盡可能匹配。
Routing 實現 Master-detail 界面
下圖來自網絡,很好地說明了 routing 中的項目文件結構:
示例項目的文件結構如下:
Application Descriptor
manifest.json
文件配置應用程序的很多信息,被稱為 Application Descriptor 。先給出文件的全部內容:
{
"_version": "1.1.0",
"sap.app": {
"_version": "1.1.0",
"id": "stone.sapui5.test",
"type": "application",
"i18n": "i18n/i18n.properties",
"applicationVersion": {
"version": "1.0.0"
},
"title": "{{appTitle}}",
"description": "{{appDescription}}",
"dataSources": {
"mainService": {
"uri": "./service/data.json",
"type": "JSON"
}
}
},
"sap.ui": {
"_version": "1.1.0",
"technology": "UI5",
"deviceTypes": {
"desktop": true,
"tablet": true,
"phone": true
},
"supportedThemes": [
"sap_bluecrystal"
]
},
"sap.ui5": {
"_version": "1.1.0",
"rootView": {
"viewName": "webapp.view.App",
"type": "XML"
},
"dependencies": {
"minUI5Version": "1.30.0",
"libs": {
"sap.m": {}
}
},
"contentDensities": {
"compact": true,
"cozy": true
},
"models": {
"": {
"dataSource": "mainService"
},
"i18n": {
"type": "sap.ui.model.resource.ResourceModel",
"settings": {
"bundleName": "webapp.i18n.i18n"
}
}
},
"routing": {
"config": {
"routerClass": "sap.m.routing.Router",
"viewType": "XML",
"viewPath": "webapp.view",
"controlId": "app",
"controlAggregation": "pages",
"bypassed": {
"target": "notFound"
}
},
"routes": [{
"pattern": "",
"name": "master",
"target": "master"
},
{
"pattern": "detail/{supplierPath}",
"name": "detail",
"target": "detail"
}],
"targets": {
"master": {
"viewName": "Master",
"viewLevel": 1
},
"detail": {
"viewName": "Detail",
"viewLevel": 2
},
"notFound": {
"viewName": "NotFound",
"viewId": "notFound"
}
}
}
}
}
解釋一些重要的配置:
1. 資源包文件
資源包文件的設置有兩個地方:
"sap.app": {
"_version": "1.1.0",
"id": "stone.sapui5.test",
"type": "application",
"i18n": "i18n/i18n.properties",
...
},
這里設置的是資源包文件的路徑和文件名。使用的相對于 manifest.json
文件的相對路徑。
另外一個地方在 sapui5.models
:
"sap.ui5": {
...
"models": {
...
"i18n": {
"type": "sap.ui.model.resource.ResourceModel",
"settings": {
"bundleName": "webapp.i18n.i18n"
}
}
}
設置名稱為 i18n 的 resource model, bundleName
后面是根據
index.html
文件的 resource roots 設置的相對路徑。然后在代碼中添加對 ResourceBundle
的依賴后,通過 {i18n>xxx}
實現綁定。
2. Models
manifest.json
文件共設置了兩個 model:
"sap.ui5": {
...
"models": {
"": {
"dataSource": "mainService"
},
"i18n": {
"type": "sap.ui.model.resource.ResourceModel",
"settings": {
"bundleName": "webapp.i18n.i18n"
}
}
}
一個是沒有指定名稱的 model,當 view 中數據綁定時,沒有給出前綴的時候,就參照到這個 model。比如 <Text text="{/Suppliers/0/id}" />
就參照到 model 所加載的數據中第一個 Supplier id。這個 model 的
dataSource
是在 sap.app
部分設置的 dataSource:
"sap.app": {
...
"dataSources": {
"mainService": {
"uri": "./service/data.json",
"type": "JSON"
}
}
dataSource 為./service
文件夾下面的 data.json
文件。
第二個是剛才提到的 Resource Model: i18n。
3. Root View
"sap.ui5": {
"_version": "1.1.0",
"rootView": {
"viewName": "webapp.view.App",
"type": "XML"
},
Root view (啟動即顯示的 view):類型為 xml,名稱為 App。OpenUI5 在相應文件夾下面查找名為 App.view.xml
文件并加載。通過這種方式,實現了 root view 的配置化。
Root view 是程序啟動的重要設置,啟動的流程如下:
- index.html 的 ComponentContainer 根據
name
或component
屬性實例化 Component - Component 的 metadata 指向設定的
manifest.json
文件 -
manifest.json
文件的sap.ui5>rootView
設定了啟動時候加載并顯示
的 root view 為App.view.xml
- App view 并不需要像之前文章介紹的內嵌 master view 和 detail view,而是由路由器根據路徑在 pattern 中找匹配的模式,在 target 中找對應的
view 加載。
4. Routing 設置
"sap.ui5": {
...
"routing": {
"config": {
"routerClass": "sap.m.routing.Router",
"viewType": "XML",
"viewPath": "webapp.view",
"controlId": "app",
"controlAggregation": "pages",
"bypassed": {
"target": "notFound"
}
},
"routes": [{
"pattern": "",
"name": "master",
"target": "master"
},
{
"pattern": "detail/{supplierPath}",
"name": "detail",
"target": "detail"
}],
"targets": {
"master": {
"viewName": "Master",
"viewLevel": 1
},
"detail": {
"viewName": "Detail",
"viewLevel": 2
},
"notFound": {
"viewName": "NotFound",
"viewId": "notFound"
}
}
}
比較直觀,不懂的地方可以參照 Routing Configuration 。
Componet.js 文件
這個文件主要的變化是將 metadata 的設置、resource model 的設置、root view 的設置都轉移到 manifest.json
中,所以 Component 中 一條語句完成初始化:
this.getRouter().initialize();
manifest.json
文件的全部代碼:
sap.ui.define([
"sap/ui/core/UIComponent",
"sap/ui/model/resource/ResourceModel",
"sap/ui/model/json/JSONModel"
], function (UIComponent, ResourceModel, JSONModel) {
"use strict";
return UIComponent.extend("webapp.Component", {
metadata: {
manifest: "json"
},
init : function () {
// call the base component's init function
UIComponent.prototype.init.apply(this, arguments);
// create the views based on the url/hash
this.getRouter().initialize();
}
});
});
Root View
<core:View xmlns:core="sap.ui.core"
xmlns:mvc="sap.ui.core.mvc"
xmlns="sap.m"
displayBlock="true"
xmlns:html="http://www.w3.org/1999/xhtml">
<App id="app" />
</core:View>
根據 manifest.json
的 root view 設置,App.view.xml
是 root view,在
view 中只需要申明 sap.m.App
,id 為 app
。Master view 和 Detail view不申明,由 routing 根據路徑自動加載。
Master Controller 和 Detail Controller
Master view 和 Detail view 代碼沒有改變。但 Master controller 和 Detail controller 的代碼需要改變。前一篇是通過 oApp(sap.m.Agg
) 這個全局變量來導航,通過 oApp 管理頁面。回顧一下 Master controller 中onListPess
事件處理程序的代碼:
onListPress: function(oEvent){
// 跳轉到detail view
var sPageId = oApp.getPages()[1].getId();
oApp.to(sPageId);
// 設置detail page的bindingContext
var oContext = oEvent.getSource().getBindingContext();
var oDetailPage = oApp.getPage(sPageId);
oDetailPage.setBindingContext(oContext);
}
變更后 Master controller 的代碼如下:
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/core/UIComponent"
],
function(Controller, UIComponent){
"use strict";
return Controller.extend("webapp.controller.Master", {
onListPress: function(oEvent){
var oRouter = UIComponent.getRouterFor(this);
var oItem = oEvent.getSource();
var sPath = oItem.getBindingContext().getPath();
oRouter.navTo("detail", {
supplierPath: encodeURIComponent(sPath)
});;
}
});
}
);
Master controller 在行項目被點擊之后,要完成兩個任務:
- 跳轉到 Detail view
- 向 Detail view 傳遞一個參數,這個參數是當前點擊的路徑,Detail view
獲取這個路徑,完成數據的綁定。
所有這些,都通過 router 來完成。
-
var oRouter = UIComponent.getRouterFor(this);
獲取當前的 router -
var oItem = oEvent.getSource()
獲取點擊所在的行,然后
oItem.getBindingContext().getPath()
獲取點擊行的路徑 (string類型) 。比如,當用戶點擊第一行,sPath 為/Suppliers/0
。這個路徑需要傳遞到
detail view。 -
oRouter.navTo()
方法不能包含/
(這是一個特殊的字符),否則提示如下錯誤。
Uncaught Error: Invalid value "/Suppliers/0" for segment "{supplierPath}".
...
所以使用 encodeURIComponent()
函數編碼,在Detail controller 中用decodeURIComponen()t
函數解碼。
Detail.controller.js 的代碼:
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/core/UIComponent",
"sap/ui/core/routing/History"
],
function(Controller, UIComponent, History){
"use strict";
return Controller.extend("webapp.controller.Detail", {
onInit: function(){
var oRouter = UIComponent.getRouterFor(this);
oRouter.getRoute("detail")
.attachPatternMatched(this._onObjectMatched, this);
},
onNavPress: function() {
var oHistory = History.getInstance();
var sPreviousHash = oHistory.getPreviousHash();
if (sPreviousHash != undefined){
window.history.go(-1);
}else{
var oRouter = UIComponent.getRouterFor(this);
oRouter.navTo("master",{}, true);
}
},
_onObjectMatched: function (oEvent) {
var sPath = decodeURIComponent(
oEvent.getParameter("arguments").supplierPath);
this.getView().bindElement({ path: sPath});
}
});
}
);
代碼說明:
- Detail view 主要負責兩件事:
獲取 Master view 傳遞的路徑,根據此路徑完成 element binding。比如當 Master view 傳過來
/Suppliers/0
,則與第一條數據綁定;根據頁面之間的關系,當點擊 返回 按鈕時,返回到上一個頁面。
-
onInit()
event handler中:oRouter.getRoute("detail").attachPatternMatched(this._onObjectMatched, this);
,當模式匹配時,附加事件處理器為_onObjectMatched
。然后在_onObjectMatched
中獲取 Master view 傳遞的路徑并綁定數據。
_onObjectMatched: function (oEvent) {
var sPath = decodeURIComponent(
oEvent.getParameter("arguments").supplierPath);
this.getView().bindElement({path: sPath});
}
- 當用戶點擊導航按鈕,判斷是否有上一個路徑 ( previous hash ),如果有就返回上一個路徑,否則跳轉到 Master view:
onNavPress: function() {
var oHistory = History.getInstance();
var sPreviousHash = oHistory.getPreviousHash();
if (sPreviousHash != undefined){
window.history.go(-1);
}else{
var oRouter = UIComponent.getRouterFor(this);
oRouter.navTo("master",{}, true);
}
}