本篇翻译自 Ext JS 6.7.0 官方文档,原文地址如下:
https://docs.sencha.com/extjs/6.7.0/guides/application_architecture/router.html
文档主要讲解了 Ext 提供的路由功能,之所以把它翻译出来,是因为大多数前端路由都采用了相近的模式,通过本文可以更好地理解单页面应用路由功能的原理及其使用方法。
在一个正常的网页中,用户通过点击链接或填写表单跳转至不同的页面。然而,在单页面应用中,用户的交互操作并不会导致加载新的页面,而是被一个页面和一些组件响应。那么,该如何让用户使用浏览器的前进和后退按钮?答案是使用 Ext JS 提供的路由功能记录 URL 的 hash 值变化。
路由可以通过浏览器的历史记录(history stack)追踪应用的状态,同时也可以实现深度链接到你的应用的某个部分。用户可以把这个部分的地址添加到书签,并直接分享给他人。
这里的历史记录是指浏览器所记录的一些信息,而我们通常说的历史记录是对这种信息的封装展示。为了表示区分,下文将使用 history stack 来进行讨论。
路由不可以被用来存储数据或会话。数据应该被存储在持久化的数据源,比如 cookie 或 localstorage 中。路由只是一种一种追踪应用状态的方式。
浏览器通过 URI 在互联网上导航。URI 由多个部分组成,让我们看一个例子:
https://www.example.com/apps/users#user=1234
这一定看起来很熟悉。然而,你可能不认识这个 #user=1234
。这个部分叫做 “hash” 或者 “片段标识符” 。想了解更多关于 hash 的信息,请阅读以下资源:
http://en.wikipedia.org/wiki/Fragment_identifier
hash 为应用提供了一种不必重新加载页面的控制浏览器 history stack 的方法。当 hash 变化时,浏览器会把整个 URI 加入到 history stack 中,这样就可以利用浏览器的 前进/后退 按钮在包括 hash 值改变的 URI 中跳转。
举个例子,当你把上面的 URI 更新如下后会发生什么?
https://www.example.com/apps/users#user/5678
浏览器触发了一个名为 hashchange
的事件,我们可以在应用中利用它。用户可以点击返回按钮返回 #user=1234
的 hash,同样会触发事件,你可以在你的应用中响应它。重要的一点是,hash 不会被送至服务器端。hash 通常只用于记录客户端的 URI 表现。Ext JS 的路由功能性地依靠浏览器的 hash 去实现应用状态追踪和深度链接。
Router 类的目的是使一个 MVC 应用中的 hash 改变更容易理解。你可以使用 Ext.util.History 去响应 hash 变化。并且,Ext.util.History
提供了更手动的过程,需要一个恰当的 Router 类。该类提供了一个简单的整合到 Ext JS 5 MVC 应用的方法,只需要在 view controller 中定义几个路由即可。一条路由是指一个字符串,对应 hash 值。下面是一个通过 controller 实现路由的简单例子:
Ext.define('MyApp.view.main.MainController', {
extend : 'Ext.app.ViewController',
routes : {
'users' : 'onUsers'
},
onUsers : function () {
//...
}
});
这个路由会响应 #users
这个 hash并执行 onUsers
方法(仅在该 controller 的作用范围内有效),可以看到,routes
是作为 config
对象而不是根出现的,所以其作用域可以被很好的控制。
为了更新 hash,controller 提供了 redirectTo
方法:
this.redirectTo('user/1234');
这将会把 hash 变为 #user/1234
,可以识别这个 hash 的 routes 配置将会被执行。
redirectTo
方法还可以接收一个可选参数(options)。选择如下:
注意: 为了向下兼容,该 options 可以是一个 boolean,在这种情况下设置的是 force 参数。
当一个应用启动时,可能被配置了一个默认的 hash 值。比如,你可能需要在没有 hash 存在时默认使用 #home
,可以在 /app/view/Application.js
中使用 defaultToken
配置:
Ext.define('MyApp.Application', {
extend : 'Ext.app.Application',
//...
defaultToken : 'home'
});
当应用启动时,它会检测当前 URI 的 hash,如果不存在,则会自动添加 #ome
并执行相应的 handlers。
应用可能会在 hash 中附带参数。比如 userID,在教程中我们提到过用 #uesr/1234
作为 hash,在这种情况下,我们可能想要 1234
作为 id 参数。你需要设置你的 controller 去响应 #user/1234
hash:
Ext.define('MyApp.view.main.MainController', {
extend : 'Ext.app.ViewController',
routes : {
'user/:id' : 'onUser'
},
onUser : function (id) {
//...
}
});
我们配置的 route 是 #user/:id
,这个冒号,代表这里有一个参数,你的应用将会把这个参数传递给 onUser 方法。该方法可以按照与 route 中定义的相同的顺序接收到相同数量的参数。
应用可能想要为 user ID 强制执行某种格式。在之前的探索中 ID 一直是数字,我们可以在 route 中把它配置为一个对象:
Ext.define('MyApp.view.main.MainController', {
extend : 'Ext.app.ViewController',
routes : {
'user/:id' : {
action : 'onUser',
conditions : {
':id' : '([0-9]+)'
}
}
},
onUser : function (id) {
//...
}
});
接着看这个例子,首先 onUser
方法被移动到了 action
中,其效果和之前相同。接着我们通过 conditions
这个配置提供对象参数。我们想控制的是带着冒号的那个参数,同时我们使用了正则表达式(字符串)。比如对 :id
,我们使用了 ([0-9])+
,意思是允许 0 到 9 之间的所有数字组成任意长度。我们使用正则表达式字符串是因为正则表达式对象会匹配整个 hash。如果存在多参数我们必须把字符串结合为一个正则表达式对象。如果你没有为一个参数提供 condition,它默认为:
([%a-zA-Z0-9\\-\\_\\s,]+)
这里的 Route 是指 controller 中的 config,而使用中文“路由”表述的是指 Ext 提供的路由功能,或者说 Ext 的路由器,即 Router。
有时应用会需要阻止路由的处理。比如我们想检查当前用户是否具有访问应用某一部分的权限。可以使用 before
来停止当前路由、停止所有路由或继续执行路由操作。
下面的例子会继续执行路由操作:
Ext.define('MyApp.view.main.MainController', {
extend : 'Ext.app.ViewController',
routes : {
'user/:id' : {
before : 'onBeforeUser',
action : 'onUser'
}
},
onBeforeUser : function (id, action) {
Ext.Ajax.request({
url : '/security/user/' + id,
success : function() {
action.resume();
}
});
},
onUser : function (id) {
//...
}
});
在 onBeforeUser
方法中,:id
作为一个实参传递,而第二个参数是 action。如果执行了 action
的 resume
方法,路由会继续执行, onUser
方法会被调用。注意我们可以等待 AJAX 请求完成后再继续执行路由操作。
我们可以通过执行 stop
方法来让应用停止执行当前的路由:
Ext.define('MyApp.view.main.MainController', {
extend : 'Ext.app.ViewController',
routes : {
'user/:id' : {
before : 'onBeforeUser',
action : 'onUser'
}
},
onBeforeUser : function (id, action) {
Ext.Ajax.request({
url : '/security/user/' + id,
success : function () {
action.resume();
},
failure : function () {
action.stop();
}
});
},
onUser : function (id) {
//...
}
});
before
本身同样支持许可。除了执行 action 的 resume
或 stop
方法外,你也可以使用 resolve
或 reject
达到同样的效果:
Ext.define('MyApp.view.main.MainController', {
extend : 'Ext.app.ViewController',
routes : {
'user/:id' : {
before : 'onBeforeUser',
action : 'onUser'
}
},
onBeforeUser : function (id) {
return new Ext.Promise(function (resolve, reject) {
Ext.Ajax.request({
url : '/security/user/' + id,
success : function () {
resolve()
},
failure : function () {
reject();
}
});
});
},
onUser : function (id) {
//...
}
});
注意: 你可以选择是否使用 action
作为参数,但问题必须要被解决。要么执行 action 的 resume 或 stop,要么 promise 被 resolved 或 rejected。
如果 hash 被改变但没有匹配的 route,路由将什么都不做,只是触发 unmatchedroute
事件,可以在 Ext.application
中监听:
Ext.application({
name : 'MyApp',
listen : {
controller : {
'#' : {
unmatchedroute : 'onUnmatchedRoute'
}
}
},
onUnmatchedRoute : function (hash) {
//...
}
});
路由还会触发一个全局事件:
Ext.application({
name : 'MyApp',
listen : {
global : {
unmatchedroute : 'onUnmatchedRoute'
}
},
onUnmatchedRoute : function (hash) {
//...
}
});
全局事件也可以直接在 Ext
命名空间中监听:
Ext.on('unmatchedroute', function (hash) {
//...
});
Ext JS 应用可能比较复杂,有时我们需要对一个 hash 进行多条 route 配置。路由提供了这种功能所以我们不需要在 controller 中进行额外的配置,你只需要在 hash 中使用 |
来分割,比如:
#user/1234|messages
在这种情况下,我们想要展示 id 为 1234 的用户的详细信息,并且附带一些 message。hash 中对应的两个 route 配置都会被执行。他们相互之间会被隔离(sandboxed),也就是说,如果停止了 user/1234
的 route,message
route 仍会继续执行。一个比较重要的点是每个 route 的执行顺序和 hash 中所写的顺序一致。user/1234
的 route 会比 message
的 route 先执行。
你可以通过改变 Ext.route.Router.multipleToken
控制分割符。
为了处理多个 route,redirectTo
方法也可以接收一个对象作为参数。这样你就可以操作(更新、移除) hash 的某一部分。假设我们有 3 个不同的 route(可以在不同的 controller 中):
routes: {
'bar' : 'onBar',
'baz' : {
action : 'onBaz',
name : 'bazRoute'
},
'foo' : 'onFoo'
}
baz
route 有一个新配置叫 name
。通过这个配置我们可以更好地引用 route。默认地,name 属性是 url 传递给 routes 的 key;bar
和 foo
是其它 route 的名称,我们现在有 bar
、baz
、foo
作为三个 route 的名称。
可以通过如下方法传递一个字符串初始化 hash:
this.redirectTo('foo');
现在 hash 变成了 #foo
,我们可以使用 multipleToken
属性传递被分割开的 token 所组成的字符串,就像我们过去做的那样。或者可以传递一个对象:
this.redirectTo({
bar : 'bar'
});
现在 hash 变成了 #foo|bar
。我们以 #foo
作为最初的 hash,然后向 redirectTo
传递了一个对象,并且传递了 bar
作为 key,这个 key 作为 route 的 name 被我们引用。对于复杂的 url 来说,使用 name 属性是一个很好的选择。
如果该 key 对应的 route 在当前 hash 中存在,但跳转的目标,即 value 是虚值,则该 route 会被移除。如果不是虚值,则会跳转到新的目标。如过 key 对应的 route 不存在,则它会被添加到 hash 中。如果我们想要替换掉 foo
,可以传递一个 value 给它:
this.redirectTo({
foo : 'foober'
});
现在 hash 编程了 #foober|bar
,注意 hash 中各个 token 的顺序被保留了。下面是一个更复杂的例子:
this.redirectTo({
bar : 'barber',
bazRoute : 'baz',
foo : null
});
在这个例子中,我们移除了一个、更新了一个、添加了一个,hash 现在变成了 #barber|baz
。通过传递对象,你可以更好的控制 hash 的变化,不用担心目前的 hash 是什么样子,Ext JS 会帮你解决。
如果你的应用需要 hashbang 而非常规 hash,Ext JS 现在可以为你自动化处理。Ext.util.History
是用于从浏览器中获取或设置 hash 的类,利用它可以自动地将 hash 变为 hashbang,只需要配置一个简单的属性:
Ext.util.History.hashbang = true;
这样就可以了,你的 routes 配置依然如下:
routes : {
'foo' : 'onFoo'
}
redirectTo
的使用方法也没变:
this.redirectTo('foobar');
this.redirectTo({
foo : 'foobar'
});
但 Ext.util.History
自动地把 hash 变成了 #!foobar
。你也可以通过在 application
中使用一个新的配置,即 router
来实现这个功能:
Ext.application({
name : 'MyApp',
router : {
hashbang : true
}
});
也可以对路由使用 setter 方法:
Ext.route.Router.setHashbang(true);
注意: hashbang 也是 hash,只是在 #
后面加了一个 !
,也就是说它们不能混合或匹配,只能二选其一。
如果你需要让路由等待一些进程的完成,而不去响应 hash 的变化,你可以暂停(suspend)路由。一旦暂停,hash 的变化会被加入到队列,并在继续(resume)后执行。一个常见的场景是在应用启动时延迟路由,以等待 store 或用户 session 的检查。下面是一个例子:
Ext.application({
name : 'MyApp',
defaultToken : 'home',
launch : function () {
var me = this;
Ext.route.Router.suspend();
me
.checkUserSession()
.then(me.onUser.bind(me), me.onUserError.bind(me));
},
onUser : function (user) {
MyApp.$user = user;
Ext.route.Router.resume();
},
onUserError : function (error) {
// handle error
// do not execute queued hash changes
Ext.route.Router.resume(true);
this.redirectTo('login');
}
});
如果你确定不想把 hash 的变化加入队列中以待后续处理,可以把 suspend
附带的参数设置为 false:
Ext.route.Router.suspend(false);
如果你想在所有配置的 route 前处理一个 route,你可以使用通配符(wildcard) route。
routes : {
'*' : 'onRoute',
'foo' : 'onFoo'
}
routes : {
'*' : {
before : 'onBeforeRoute',
action : 'onRoute'
},
'foo' : 'onFoo'
}
通配符 route 会先于 foo
执行。
Ext JS 提供了几个全局事件。这些事件被 Ext
命名空间触发并且可以在 Ext
或全局事件域中监听。下面是这些事件:
unmatchedroutes
在 route 未被 token 匹配(即输入了假地址)时触发。之前提到,有两种方法可以监听这些事件,推荐的方法是通过 controller/viewcontroller 中的全局事件域监听:
listen : {
global : {
beforeroutes : 'onBeforeRoutes'
}
},
onBeforeRoutes : function (action, tokens) {
return tokens.length === 1;
}
或者可以通过 Ext
命名空间:
Ext.on('unmatchedroute', function (token) {
Ext.Msg.alert('Unmatched Route', '"' + token + '" was not matched!');
});
在 before
action 中,你看见了 action
参数。它是 Ext.route.Action
的实例,并且不仅仅提供继续或停止动作的功能。这个类实际上可以通过使用 before
和 action
动作变得更加动态,这两个动作甚至可以在动作执行期间使用:
onBeforeUser : function (id, action) {
return new Ext.Promise(function (resolve, reject) {
action.before(function (id, action) {
action.action(function (id) {
//...
});
action.resume();
});
Ext.Ajax.request({
url : '/security/user/' + id,
success : function () {
resolve()
},
failure : function () {
reject();
}
});
});
}
在这个例子中,我们往执行中的 before
动作列表中添加了 before
动作(它会被最后执行)。在这个 before
动作中,我们同样往 action
动作列表中添加了一个 action
动作。注意 id
参数仍然要传递给新的 action。
如果你想知道 action 什么时候被完全执行或拒绝(所有的 before
和所有的 action
),action
类有一个 then
方法:
onBeforeUser : function (id, action) {
var me = this;
return new Ext.Promise(function (resolve, reject) {
action.then(Ext.bind(me.onFinish, me), Ext.bind(me.onRejection, me));
//...
});
}
如果你想停止 before
或 action
的执行,这里同样有一个 stop
方法:
onBeforeUser : function (id, action) {
try {
this.foo();
} catch (e) {
action.stop();
}
}
Ext JS 6.5.0 创建了一个新的 mixin,这样 routes 就可以在所有类中被配置了。
Ext.define('MyClass', {
mixins : [
'Ext.route.Mixin'
],
routes : {
'foo' : 'onFoo'
},
constructor : function (config) {
this.initConfig(config);
},
doSomething : function () {
this.redirectTo('foo');
},
onFoo : function () {
//...
}
});