Sencha Touch 2的访问历史和路由支持堪称独有特色,也让它的用户体验向Native应用更加靠近了一步。通俗的讲,访问历史和路由这两个功能(深链接只是路由功能的一个应用)就是在浏览器环境中,把单页面应用程序模拟成多页面交互的效果,而且还无需刷新页面。
这篇文章的英文原址是http://docs.sencha.com/touch/2-0/#!/guide/history_support
原文标题是:Routing, Deep Linking and the Back Button(路由、深链接以及后退按钮)。
Sencha Touch 交流QQ群213119459欢迎您的加入。
Routing, Deep Linking and the Back Button
路由、深链接和后退按钮
注:为方便起见,文中所有出现 Sencha Touch的地方均以 ST简写替代。
Sencha Touch 2 comes with fully history and deep-linking support. This gives your web applications 2 massive benefits:
ST2带来了全面的访问历史和深链接支持,这将赋予我们的应用程序两个好处:
l The back button works inside your apps, navigating correctly and quickly between screens without refreshing the page
(浏览器的)后退按钮将会在你的应用程序内部发挥作用,从而实现在(应用程序内部的)界面屏幕之间快速正确的导航,而不会刷新整个页面。
l Deep-linking enables your users to send a link to any part of the app and have it load the right screen
深链接功能使得用户可以把应用程序的任意部分作为链接发送出去,当这个链接被点击时页面会加载到正确的内容(而不是应用程序的初始页面)
The result is an application that feels much more in tune with what users expect from native apps, especially on Android devices with the built-in back button fully supported.
结果就是你的web应用程序也可以给人带来如同本地应用一般的舒畅体验,尤其是对Android设备来说,后退按钮可以得到最完整的功能支持。
Setting up routes
设置路由
Setting up history support for your apps is pretty straightforward and is centered around the concept of routes. Routes are a simple mapping between urls and controller actions - whenever a certain type of url is detected in the address bar the corresponding Controller action is called automatically. Let's take a look at a simple Controller:
以路由功能为基础,为应用程序设置访问历史功能支持变得很容易。路由其实实现的就是url和控制器动作之间的映射,当地址栏中的一个特定类型url被检测到的时候,相应的控制其动作就会被自动调用,我们来看一个简单的控制器例子:
Ext.define('MyApp.controller.Products', {
extend: 'Ext.app.Controller',
config: {
routes: {
'products/:id': 'showProduct'
}
},
showProduct: function(id) {
console.log('showing product ' + id);
}
});
By specifying the routes above, the Main controller will be notified whenever the browser url looks like "#products/123". For example, if your application is deployed onto http://myapp.com, any url that looks like http://myapp.com/#products/123, http://myapp.com/#products/456 or http://myapp.com/#products/abc will automatically cause your showProduct function to be called.
通过上述路由的定义,当浏览器url是类似#products/123的时候,Main控制器(译者注:怀疑作者写错了,应该是Products控制器)就会被通知。例如,你的应用程序部署在http://myapp.com这个路径,那么凡是类似http://myapp.com/#products/123,http://myapp.com/#products/456,http://myapp.com/#products/abc这样的url都会自动调用你的showProduct方法。
When the showProduct function is called this way, it is passed the 'id' token that was parsed out of the url. This happens because we used ':id' in the route - whenever a route contains a ':' it will attempt to pull that information out of the url and pass it into your function. Note that these parsed tokens are always strings (because urls are always strings themselves), so hitting a route like 'http://myapp.com/#products/456' is the same as calling showProduct('456').
当showProduct方法这样被调用的时候,其实它被传递了id这个参数,这是因为我们在路由中使用了“:id”,当一个路由包含了“:”的时候,他会被视作参数名然后把对应的值从url中获取出来并传给你的函数。注意这些解析出来的参数总是以string类型出现的,因为url本身就是个string类型。所以访问一个类似http://myapp.com/#products/456这样的路由其实就等于调用了showProduct('456')方法。
You can specify any number of routes and your routes can each have any number of tokens - for example:
你可以定义多个路由,每个路由也可以包含多个参数,比如:
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');
}
});
The second route accepts urls like #products/123/pdf, which will route through to the showProductInFormat function and console log 'showing product 123 in pdf format'. Notice that the arguments are passed into the function in the order they appear in the route definition.
第二个路由接受像#products/123/pdf这样的url,它将被指向showProductInFormat函数并且输出'showing product 123 in pdf format'结果。注意传入函数的参数顺序与路由中定义的顺序保持一致。
Of course, your Controller function probably won't actually just log a message to the console, it can do anything needed by your app - whether it's fetching data, updating the UI or anything else.
当然你的控制器函数不会只是输出一段调试信息,你可以根据需要做任何事情,获取数据、更新界面或者其他什么的都行。
Advanced Routes
路由进阶
By default, wildcards in routes match any sequence of letters and numbers. This means that a route for "products/:id/edit" would match the url "#products/123/edit" but not "#products/a ,fd.sd/edit" - the second contains a number of letters that don't qualify (space, comma, dot).
默认情况下,路由中的通配符可以匹配任意连续的字母和数字字符串。比如"products/:id/edit"这个路由可以匹配"#products/123/edit",但是却不能匹配"#products/a ,fd.sd/edit",后面这个url包含了一些非法字符(空格,逗号,点号)
Sometimes though we want the route to be able to match urls like this, for example if a url contains a file name we may want to be able to pull that out into a single token. To achieve this we can pass a configuration object into our Route:
但是有时候我们还是需要路由能够匹配类似的url,比如我们的url包含一个文件名,我们想把它取出来。这时可以传递一个配置对象到路由中去:
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);
}
});
So instead of an action string we now have a configuration object that contains an 'action' property. In addition, we added a conditions configuration which tells the :filename token to match any sequence of numbers and letters, along with a period ('.'). This means our route will now match urls like http://myapp.com/#file/someFile.jpg, passing 'someFile.jpg' in as the argument to the Controller's showFile function.
也就是说可以使用一个包含了action属性的配置对象来取代action字符串。除此之外,我们给这这个对象添加一个conditions属性,这个属性定义了:filename这个参数可以匹配英文数字、阿拉伯数字外加一个英文点号。这样我们的路由现在就可以匹配像http://myapp.com/#file/someFile.jpg这样的url了,它会把someFile.jpg作为参数传递给控制器的showFile函数。
Restoring State
还原状态
One challenge that comes with supporting history and deep linking is that you need to be able to restore the full UI state of the app to make it as if the user navigated to the deep-linked page him or herself. This can sometimes be tricky but is the price we pay for making life better for the user.
伴随访问历史支持和深链接而来的挑战就是你得想办法还原整个UI界面的状态,来使得整个界面看起来像是用户自己一步一步导航过去似的。这的确很棘手,但却是我们为了提高用户体验而不得不付出的代价。
Let's take the simple example of loading a product based on a url like http://myapp.com/#products/123. Let's update our Products Controller from above:
我们来看一个小例子,怎样加载http://myapp.com/#products/123这个链接指向的页面内容,代码是从上面Products控制器改动而来:
Ext.define('MyApp.controller.Products', {
extend: 'Ext.app.Controller',
config: {
refs: {
main: '#mainView'
},
routes: {
'products/:id': 'showProduct'
}
},
/**
* 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);
}
});
}
});
Here our 'products/:id' url endpoint results in the immediate addition of a view into our app's main view (which could be a TabPanel or other Container), then uses our product model (MyApp.model.Product) to fetch the Product from the server. We added a callback that then populates the product detail view with the freshly loaded Product. We render the UI immediately (as opposed to only rendering it when the Product has been loaded) so that we give the user visual feedback as soon as possible.
'products/:id'这个路由映射的目标应该是立刻把一个detail视图加入到应用程序的main视图(可能是一个tabpanel或者其他container)里面去,之后通过product数据模型(MyApp.model.Product)从服务器取回product信息,并在回调里把最新获取到的数据发布到product的detail视图上,然后立刻渲染UI界面(与之相对照的是在Product被加载之后才渲染)这样我们就以最快速度给了用户他们想要的数据。
Each app will need different logic when it comes to restoring state for a deeply-linked view. For example, the Kitchen Sink needs to restore the state of its NestedList navigation as well as rendering the correct view for the given url. To see how this is accomplished in both Phone and Tablet profiles check out the showView functions in the Kitchen Sink's app/controller/phone/Main.js and app/controller/tablet/Main.js files.
每个应用程序在为深链接还原状态的时候都有不同的逻辑。比如Kitchen Sink需要还原他的级联列表导航栏并渲染url指定的视图。想知道在Phone和Tablet两个Profile中是怎么实现的,请参考Kitchen Sink的 app/controller/phone/Main.js和app/controller/tablet/Main.js文件中showView函数。
Sharing urls across Device Profiles
跨设备配置共享url
In most cases you'll want to share the exact same route structure between your Device Profiles. This way a user using your Phone version can send their current url to a friend using a Tablet and expect that their friend will be taken to the right place in the Tablet app. This generally means it's best to define your route configurations in the superclass of the Phone and Tablet-specific Controllers:
大部分情况下,我们都希望在不同设备配置之间共享同样的路由架构。这样的话一个使用手机界面的用户可以把自己当前的url发送给使用平板电脑界面的朋友,并且还能保证朋友在平板电脑上看到的也是同样内容。这意味着我们最好在一个父类中定义route配置,然后由手机和平板的控制器分别继承该父类的配置:
Ext.define('MyApp.controller.Products', {
extend: 'Ext.app.Controller',
config: {
routes: {
'products/:id': 'showProduct'
}
}
});
Now in your Phone-specific subclass you can just implement the showProduct function to give a Phone-specific view for the given product:
现在phone专用的子类当中你只需要继承这个showProduct函数并显示一个手机下专用的视图即可:
Ext.define('MyApp.controller.phone.Products', {
extend: 'MyApp.controller.Products',
showProduct: function(id) {
console.log('showing a phone-specific Product page for ' + id);
}
});
And in your Tablet-specific subclass just do the same thing, this time showing a tablet-specific view:
在平台专用的子类当中也要做同样的事情,只不过这次调用的是一个平台电脑下专用的视图:
Ext.define('MyApp.controller.tablet.Products', {
extend: 'MyApp.controller.Products',
showProduct: function(id) {
console.log('showing a tablet-specific Product page for ' + id);
}
});
There are some exceptions to this rule, usually to do with linking to navigation states. The Kitchen Sink example has phone and tablet specific views - on both profiles we use a NestedList for navigation but whereas on the Tablet NestedList only takes up the left hand edge of the screen, one the Phone it fills the screen. In order to make the back button work as expected on phones, each time we navigate in the NestedList we push the new url into the history, which means that the Phone-specific controller has one additional route. Check out the app/controller/phone/Main.js file for an example of this.
也有一些例外情况,比如Kitchen Sink例子当中有phone和tablet两个专用视图,两个profile中都有一个级联列表作为导航栏,但是平板电脑上的导航栏是一直固定在左侧不动,而手机上的则是充满整个屏幕。为了使手机上的后退按钮可以正常工作,每次我们在手机级联列表中导航的时候都要设定一个新的url到访问历史当中去,这样手机专用的控制器就会多一个额外的路由,具体可以看一下app/controller/phone/Main.js文件当中的代码。