Ext JS 6.7 中文文档:路由的使用

原文链接: https://docs.sencha.com/extjs/6.7.0/guides/application_architecture/router.html

本篇目录

  • 前言
  • 正文
    • 通过路由控制应用
    • 路由可以做什么
    • 路由不可以做什么
    • 什么是 Hash?
    • 在你的应用中实现路由
    • 更新 Hash
    • 默认 Token
    • 带参数的 Hash
    • Hash 参数格式化
    • Route 处理
    • 处理不匹配的 Route
    • 对一个 Hash 使用多条 route
    • 自动 Hashbang 支持
    • 暂停和继续路由
    • 使用通配符
    • 全局事件
    • Action 类
    • 无 MVC/MVVM 情况下的路由

前言

本篇翻译自 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 中。路由只是一种一种追踪应用状态的方式。

什么是 Hash?

浏览器通过 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

为了更新 hash,controller 提供了 redirectTo 方法:

this.redirectTo('user/1234');

这将会把 hash 变为 #user/1234,可以识别这个 hash 的 routes 配置将会被执行。

redirectTo 方法还可以接收一个可选参数(options)。选择如下:

  • force ——该参数如果为 true,路由配置将强制被执行,即使地址栏中的 hash 与传递给 redirectTo 的 hash 相同。
  • replace——默认地,hash 会被改变,而浏览器的历史记录会新增一条,这意味着浏览器的后退按钮可以返回之前的 hash。如果该参数为 true,当前的浏览器历史记录会被传递给 redirectTo 的 hash 取代。(也就是无法返回)。

注意: 为了向下兼容,该 options 可以是一个 boolean,在这种情况下设置的是 force 参数。

默认 Token

当一个应用启动时,可能被配置了一个默认的 hash 值。比如,你可能需要在没有 hash 存在时默认使用 #home,可以在 /app/view/Application.js 中使用 defaultToken 配置:

Ext.define('MyApp.Application', {
    extend : 'Ext.app.Application',

    //...

    defaultToken : 'home'
});

当应用启动时,它会检测当前 URI 的 hash,如果不存在,则会自动添加 #ome 并执行相应的 handlers。

带参数的 Hash

应用可能会在 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 中定义的相同的顺序接收到相同数量的参数。

Hash 参数格式化

应用可能想要为 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 处理

这里的 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。如果执行了 actionresume 方法,路由会继续执行, 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 的 resumestop 方法外,你也可以使用 resolvereject 达到同样的效果:

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。

处理不匹配的 Route

如果 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) {
    //...
});

对一个 Hash 使用多条 route

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;barfoo 是其它 route 的名称,我们现在有 barbazfoo 作为三个 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 支持

如果你的应用需要 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 或全局事件域中监听。下面是这些事件:

  • beforeroutes 在所有 route 被执行前触发。返回 false 可以取消 route 的执行。
  • beforeroute 在某个 route 被执行前触发。返回 false 可以取消该 route 的执行。
  • 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!');
 });

Action 类

before action 中,你看见了 action 参数。它是 Ext.route.Action 的实例,并且不仅仅提供继续或停止动作的功能。这个类实际上可以通过使用 beforeaction 动作变得更加动态,这两个动作甚至可以在动作执行期间使用:

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

        //...
    });
}

如果你想停止 beforeaction 的执行,这里同样有一个 stop 方法:

onBeforeUser : function (id, action) {
    try {
        this.foo();
    } catch (e) {
        action.stop();
    }
}

无 MVC/MVVM 情况下的路由

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 () {
        //...
    }
});

你可能感兴趣的:(前端)