SAPUI5 (22) - Routing 實現多頁面導航

前面我們實現了基于組件的多頁面程序,這個程序還有兩個主要的缺點:

1)存在全局變量oApp (sap.m.App);

2)多個頁面全部在程序啟動時加載,多個頁面也是在同一個 URL 下由
Openui5 框架進行管理,這種模式滿足不了多頁面的需求。解決的辦法是:路由 ( routing ) 。通過 routing 實現多頁面的導航,異步實現按需加載。

本篇仍然基于之前 Master-detail 供應商顯示這支程序進行重構,使用 routing 實現頁面間導航,將 Component 的元數據(metadata),移到專門的文件中。

OpenUi5 的 Routing

Openui5 的 routing 基于模式 ( pattern ),使用 # 符號表示不同的路徑 ( route ),導航通過路徑的改變來實現。

OpenUI5 Routing: 圖片來源:https://openui5.hana.ondemand.com/#docs/guide/3d18f20bd2294228acb6910d8e8a5fb5.html

首先我們用 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.jsmetadata 部分配置,也可以在
    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 中的項目文件結構:

來源: https://blogs.sap.com/2014/05/04/get-started-with-sapui5-and-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 modelbundleName 后面是根據
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 是程序啟動的重要設置,啟動的流程如下:

  1. index.html 的 ComponentContainer 根據 namecomponent 屬性實例化 Component
  2. Component 的 metadata 指向設定的 manifest.json 文件
  3. manifest.json 文件的 sap.ui5>rootView 設定了啟動時候加載并顯示
    的 root view 為 App.view.xml
  4. 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 主要負責兩件事:
  1. 獲取 Master view 傳遞的路徑,根據此路徑完成 element binding。比如當 Master view 傳過來 /Suppliers/0,則與第一條數據綁定;

  2. 根據頁面之間的關系,當點擊 返回 按鈕時,返回到上一個頁面。

  • 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);
    }
}

源代碼

22_zui5_routing_master_detail

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容