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

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,200评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,526评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,321评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,601评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,446评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,345评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,753评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,405评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,712评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,743评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,529评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,369评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,770评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,026评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,301评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,732评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,927评论 2 336

推荐阅读更多精彩内容