路由、深链接以及后退按钮 Routing, Deep Linking and the Back Button
Sencha Touch 2 提供完整的历史与深链接的支持,使得我们的 Web 应用拥有以下两点大的好处:
- 浏览器的“后退”按钮在你的应用中有效了,也就是说,虽然按下了后退键,但浏览器并不会立刻刷新,而是仍停留在当前的页面中。
- 可以使用“深链接”了,也就是说,可以分配的一个 url,直达制定的页面(在同一个页面中)。
上述功能综述之,就是为了更好地与原生程序相贴近,务求达到无差别的用户体验——这一点,尤其体现在能够提供“返回键”的 Android 机器上面。
设置路由器 Setting up routes
为你的应用安排的历史记录可以说一点都不困难,主要集中在如何理解的路由器(routes)的概念之上。路由器,简言之,用于定义某一 url 与用户控制器动作之间的映射关系——所谓映射关系,只要在浏览器的地址栏中输入一个地址,对应的控制器便会自动执行某组逻辑动作。咱们看看一个简单的控制器:
Ext.define('MyApp.controller.Products', { extend: 'Ext.app.Controller', config: { routes: { 'products/:id': 'showProduct' } }, showProduct: function(id) { console.log('showing product ' + id); } });
通过指定 {@link Ext.app.Controller#routes routes},当诸如“#products/123” 这样的 url 输入到浏览器时就会通知 Main 控制器。详细点说,就是你的域名是 http://myapp.cpm,然后任何的 http://myapp.com/#products/123、 http://myapp.com/#products/456 或 http://myapp.com/#products/abc 这般的链接,其实终归自动进入到 showProduct 函数的执行环境中去。
当 showProduct 被执行时,其参数就是那个 url 解析而致的。有否注意到路由器中的“:id”部分,因为我们约定,有冒号“:”即表示后面跟的是为一个参数,路由器内部解析好 url 就会得到这个参数然后送入到你所写的函数中去。值得注意的是,该参数总是字符类型(无需多言,url 本身亦是字符类型),于是浏览器得到的“http://myapp.com/#products/456 or http://myapp.com/#products/abc”亦即等于调用 showProduct('456')。
路由器合法的格式多种多样,构成了不同参数的映射关系,例如:
Ext.define('MyApp.controller.Products', { extend: 'Ext.app.Controller', config: { routes: { 'products/:id': 'showProduct', 'products/:id/:format': 'showProductInFormat' } }, showProduct: function(id) { console.log('showing product ' + id); }, showProductInFormat: function(id, format) { console.log('showing product ' + id + ' in ' + format + ' format'); } });
第一个路由很简单,前面已经说了说;第二个路由接纳 #products/123/pdf 这样的输入,转到 showProductInFormat 函数,便在在控制台中记录了 showing product 123 in pdf format 这样的输出。另外,id、format 也是按照路由定义中的顺序送入函数中。
当然了,实际中你写的并不像我们这里教学例子的那么简单。大家想该怎么做了吗?就是在控制器中写你的业务逻辑,获取数据啊、更新UI啊的任务……
高级路由器 Advanced Routes
路由器支持通贝符(wildcards)。默认下,通贝符表示不分字符或数字。假设“products/:id/edit”可以接纳“#products/123/edit”但不接纳“#products/a ,fd.sd/edit”,因为后者包含的空格、逗号、句号均不在通贝符涵盖的类型之列。
不过有时我们想自定义复杂一些的规制,——这没问题,我们分配一个配置项对象给 routes 对象而不是字符串,好比如下:
Ext.define('MyApp.controller.Products', { extend: 'Ext.app.Controller', config: { routes: { 'file/:filename': { action: 'showFile', conditions: { ':filename': "[0-9a-zA-Z\.]+" } } } }, //opens a new window to show the file showFile: function(filename) { window.open(filename); } });
当然不直接分配方法名称的名称还需要说明你要调用哪个函数,就写在配置项对象的 action 属性中。此外关键的是,条件配置项 {@link Ext.app.Route#conditions conditions} 其 key 为对应的 token,然后 value 为期望的规制。结果,假设一 url 系 http://myapp.com/#file/someFile.jpg,路由器解析后执行 showFile 函数,并送入“someFile.jpg”的参数。
复原状态 Restoring State
前面为大家介绍了定义路由器映射规制的相关知识后,接下来,就是详细讲讲具体的应用了。因为既然是“单页面的应用程序”的缘故,所以我们不轻易地产生一个 url 对应一张页面——如果我们进入首页后,一步步查找分类然后再找到那笔记录,——岂不是很累?所以说,由框架设定一套 url 规制便很有必要了。我们看看下面一个简单的例子,比如还是进入 http://myapp.com/#products/123,我们修改一下 Product 控制器如下:
Ext.define('MyApp.controller.Products', { extend: 'Ext.app.Controller', config: { refs: { main: '#mainView' }, routes: { 'products/:id': 'showProduct' } }, /** * products/:id 路由器的终点。获取一个货物的详细信息然后 push 的新视图。Endpoint for 'products/:id' routes. Adds a product details view (xtype = productview) * into the main view of the app then loads the Product into the view * */ showProduct: function(id) { var view = this.getMain().add({ xtype: 'productview' }); MyApp.model.Product.load(id, { success: function(product) { view.setRecord(product); }, failure: function() { Ext.Msg.alert('Could not load Product ' + id); } }); } });
这里的 products/:id 终点就会立刻添加一新视图到主视图中(如非导航类可以是其他的 TabPanel 控件),进而由 model 类(MyApp.model.Product)向服务器获取货物的详细信息。这一过程是异步的过程,所以我们分配了两个回调函数,分别对应成功(渲染 货物 UI)与失败的情形。
不同的应用有不同的复原状体逻辑,一些深入链接的情形。官方例子 Kitchen Sink 就是一个很好的例子,尤其它采用了 NestedList 方式的导航很能说明问题,并且有手机和平板的两种适应场景。具体怎么办到的?请到 Kitchen Sink 例子 app/controller/phone/Main.js and app/controller/tablet/Main.js 的 showView() 源码看看吧。
Sharing urls across Device Profiles
无论你有多少套<a href="#!/guide/profiles">设备描述 Device Profiles</a>,但一般来说你都会只使用路由结构,也就是说,你在 iPhone 上看到一页面,发觉不错,然后想共享该 url 给别人的话,那么直接把 url 复制给别人即可,无须担心人家的是否手机抑或平板。因此,建议分别在手机子类上写一套路由的配置,然后在平板子类上写一套路由的配置:
Ext.define('MyApp.controller.Products', { extend: 'Ext.app.Controller', config: { routes: { 'products/:id': 'showProduct' } } });
手机版的显示方式有所不同,于是,我们写出 showProduct 函数的手机版实现:
Ext.define('MyApp.controller.phone.Products', { extend: 'MyApp.controller.Products', showProduct: function(id) { console.log('showing a phone-specific Product page for ' + id); } });
平板电脑的话,在其子类中写出特定的实现:
Ext.define('MyApp.controller.tablet.Products', { extend: 'MyApp.controller.Products', showProduct: function(id) { console.log('showing a tablet-specific Product page for ' + id); } });