五、可寻址能力和导航
生活在 web 上的应用依赖于可寻址资源。 URI 是一项重要的互联网技术。 它消除了整个类的复杂性,因为我们可以将有关资源的信息编码到 URI 字符串中。 这就是政策部分。 机制部分取决于浏览器,或者我们的 JavaScript 代码——查找请求的资源并显示它。
过去,处理 uri 是在后端进行的。 当用户向浏览器传递一个 URI 时,浏览器的职责是将这个请求发送到后端并显示响应。 对于大规模的 JavaScript 应用,这一责任主要转移到前端。 我们有在浏览器中实现复杂路由的工具,有了它,对后端技术的依赖就少了。
然而,一旦我们的软件包上了特性,前端路由的好处是要付出代价的。 本章深入研究了随着应用架构的发展和成熟,我们可能会遇到的路由场景。 大多数框架中路由组件的底层实现细节并不重要。 我们更关心的是我们的路由组件如何适应规模影响者。
路由方法
在 JavaScript 中有两种路由方法。 第一种是使用基于散列的 uri。 这些是以#
字符开头的 uri,这是更流行的方法。 另一种不太流行的方法是使用浏览器的历史 API 来生成 web 用户习惯使用的更传统的 uri。 这种技术更复杂,直到最近才获得足够的浏览器支持,使其可行。
Hash uri
URI 的散列部分最初用于指向文档中的特定位置。 所以浏览器会查看#
字符的到的所有内容,并将这些信息发送到后端,请求一些页面内容。 只有当页面到达并呈现时,#
字符的右侧边才变得相关。 此时浏览器使用 URI 的散列部分来查找页面中本地相关的点。
现在,URI 的散列部分的使用有所不同。 它仍然用于避免在 URI 更改时将不相关的数据传递到后端。 主要的区别在于,今天我们处理的是应用和特性,而不是网站和静态内容。 因为当地址改变时,大部分应用已经加载到浏览器中,所以向后端发送不必要的请求是没有意义的。 我们只需要新的 URI 所需的数据,而这通常是通过在后台的 API 请求来完成的。
当我们讨论在 JavaScript 应用中对 URI 使用散列方法并更改 URI 时,通常只会更改散列部分。 这意味着相关的浏览器事件将被触发,通知我们的代码 URI 发生了变化。 但它不会自动向后端发出新的页面内容请求,这是关键。 实际上,我们可以像这样从前端路由中获得大量的性能和效率收益,这也是我们使用这种方法的原因之一。
它不仅工作得好,而且很容易实现。 在实现哈希更改事件侦听器(执行逻辑来获取相关数据,然后用相关内容更新页面)时,没有太多移动部件。 此外,浏览器历史的变化会自动为我们处理。
传统 uri
对于一些用户和开发人员来说,散列方法就像是一种黑客攻击。 更不用说在公共互联网环境中出现的 SEO 挑战了。 他们更喜欢更传统的斜杠分隔的资源名称格式。 由于对历史 API 的改进,现在所有的现代浏览器都可以实现这一点。 从本质上说,路由机制可以侦听被推入历史堆栈的状态,当这种情况发生时,它阻止请求被发送到后端,而是在本地处理它。
显然,这种方法需要更多的代码才能工作,需要考虑更多的边缘情况。 例如,后端需要支持前端路由所做的所有 URI,因为用户可以向应用提供任何有效的 URI。 处理这个问题的一种技术是在服务器上使用重写规则,将 404 错误重定向回应用索引页,这里是我们实际路由处理的位置。
也就是说,在大多数 JavaScript 应用框架中发现的路由组件抽象了方法上的差异,并提供了一种无缝地朝某个方向前进的方法。 无论是为了增强功能,还是为了提高可伸缩性,使用哪种方法有关系吗? 不是真的。 但就可伸缩性而言,我们必须承认实际上有两种主要的方法,我们不希望完全依赖于其中一种方法。
路由的工作原理
现在是时候让我们更深入地挖掘路由了。 我们想知道路由的职责,以及当 URI 改变时它的生命周期是什么样子。 本质上,这相当于路由获取新的 URI 并确定它是否感兴趣。 如果是,那么它将使用解析的 URI 数据作为参数触发相应的路由事件。
理解路由在底层的角色对于扩展应用非常重要,因为我们拥有的 uri 越多,响应这些路由事件的组件越多,就越有可能出现扩展问题。 当我们知道路由生命周期中发生的事情时,我们可以做出适当的伸缩权衡,以响应伸缩影响因素。
路由职责
路由的简单视图只是一个映射——有路由、字符串或正则表达式模式定义,它们映射到回调函数。 重要的是,这个过程是快速、可预测和稳定的。 这是一个挑战,特别是当我们的应用中的 uri 数量增加时。 以下是任何路由组件都需要处理的大致情况:
- 存储路由模式到相应事件名称的映射
- 监听 URI 变化事件-hash changeorpop state
- 执行路由模式查找,将新 URI 与每个映射模式进行比较
- 当找到匹配时,根据模式解析新的 URI
-
Triggering the mapped route event, passing any parsed data
注释
路由查找过程涉及到通过路由地图进行线性搜索以找到匹配。 当定义了大量路由时,这可能意味着显著的性能下降。 当路由映射是一个对象数组时,也会导致路由性能不一致。 例如,如果一个路由在数组的末尾,这意味着它是最后检查的,并且执行缓慢。 如果它位于数组的开头,则性能更好。
为了避免频繁访问 uri 的性能下降,我们可以扩展路由,以便它根据优先级属性对路由映射数组进行排序。 另一种方法是使用try结构,以避免线性查找。 当然,只有在路由太多,路由性能很差的情况下,才需要考虑这样的优化。
当 URI 改变时,路由有很多事情要做,这就是为什么理解给定路由的生命周期很重要,从地址栏中的 URI 改变到它所有的事件处理函数的完成。 从性能的角度来看,许多路由会对我们的应用产生负面影响。 从合成的角度来看,跟踪哪些组件创建哪些路由并对哪些路由作出反应是一项挑战。 当我们知道任何给定路由的生命周期时,这就更容易处理了。
路由事件
一旦路由找到了改变的 URI 的匹配,并且根据匹配模式解析了 URI,它的最终任务就是触发路由事件。 被触发的事件作为映射的一部分提供。 URI 可以对变量进行编码,这些变量将被解析并作为数据传递给每个路由事件处理程序。
路由事件提供了一个抽象层,这意味着不是路由的组件可以触发路由事件
大多数框架都提供了可以直接调用函数来响应路由变化的路由组件,而不是触发路由事件。 这实际上更简单,而且是一种更直接的方法,对于较小的应用来说是有意义的。 我们通过事件触发机制从路由触发事件得到的间接影响是,我们的组件与路由是松散耦合的。
这是有益的,因为不了解彼此的不同组件可以侦听相同的路由事件。 随着我们的扩展,已经存在了一段时间的相同路线将需要承担新的责任,添加新的处理程序比在相同的功能代码上继续构建更容易。 还有一个抽象的好处——侦听路由事件的组件并不关心该事件实际上是由路由实例触发的。 当我们需要一个组件来触发类似路由的行为时,这是非常有用的,而不需要实际依赖路由。
URI 部件和模式
在大规模的 JavaScript 应用中,很多想法都涉及到路由组件。 我们还需要在 uri 本身投入大量的思考。 它们是由什么组成的? 它们在整个应用中是否一致? 什么是糟糕的 URI? 如果在上述任何考虑上偏离了正确的方向,就会使应用的可寻址性难以扩展。
编码信息
URI 的要点在于,客户端可以将它传递给我们的应用,它包含足够的信息,可以用它来做一些有用的事情。 最简单的 URI 只是指向一个资源类型,或应用中的一个静态位置——/users
或/home
是这些类型 URI 的例子。 使用这个信息,我们的路由可以触发一个路由事件,并触发一个回调函数。 这些回调甚至不需要任何参数—它们只知道要做什么,因为没有可变性。
另一方面,路由回调函数可能需要一些上下文。 这时 URI 中的编码信息就变得很重要了。 最常见的用法是当客户端使用唯一标识符请求某个资源的特定实例时。 例如,users/31729
。 在这里,路由需要找到一个匹配这个字符串的模式,这个模式还将指定如何提取31729
变量。 然后将它传递给回调函数,该函数现在有足够的上下文信息来执行它的任务。
如果我们试图在 uri 中编码大量信息,uri 可能会变得庞大和复杂。 这方面的一个例子是为显示资源网格的页面编码查询参数。 试图指定路径模式中的所有可能性是困难的,而且容易出错。 在变量的组合中,必然会有一些变化和意料之外的边缘情况。 有些可能是可选的。
当给定的 URI 具有如此大的复杂性时,最好将编码选项排除在传递给路由的 URI 模式之外。 相反,让回调函数查看 URI 并执行进一步解析以确定上下文。 这样可以保持路由规范的整洁和整洁,并将奇怪的复杂处理程序与其他所有东西隔离开来。
对于常见的查询,我们可能希望为用户提供一个简单的 URI,特别是当它以链接的形式呈现时。 例如,最近的帖子会链接到/posts/recent
。 这个 URI 的处理程序有一些东西需要计算出来,否则就需要在 URI 中进行编码,例如排序和要获取的资源数量。 有时这些东西不需要包含在 URI 中,这样的决策既有利于用户体验,也有利于代码的可伸缩性。
设计 uri
资源名是我们创建 uri 的一个很好的灵感来源。 如果 URI 链接到一个显示事件的页面,它可能应该以events
开始。 然而,有时,后端所公开的资源没有直观的名称。 或者,作为一个组织或行业,我们喜欢缩写某些术语。 这些也应该避免,除非应用的上下文提供了意义。
反之亦然——如果 URI 太冗长,那么在 URI 中添加太多的含义实际上会导致混淆。 从单个单词的角度来看,或者从 URI 组件的数量来看,这可能过于冗长。 为了帮助传达结构并使人眼更容易解析,uri 通常被分成几个部分。 例如,事物的类型,后面跟着事物的标识符。 在 uri 中编码分类信息或其他切线信息对用户并没有真正的帮助——尽管它可以在 UI 中显示。
在可以做到的地方,我们应该保持一致。 如果我们要限制资源名的字符数量,那么它们都应该遵循相同的限制。 如果我们使用斜杠分隔 URI 部分,那么在任何地方都应该这样做。 这背后的整个想法是,当我们的用户有很多 URI 时,它可以很好地扩展,因为他们最终可以猜出某个东西的 URI 是什么,而不需要单击链接。
在保持一致的同时,我们有时希望某些类型的 uri 能够脱颖而出。 例如,当我们访问一个将资源置于不同状态的页面时,或者需要用户输入时,我们应该用不同的符号作为动作的前缀。 假设我们正在编辑一个任务——URI 可能是/tasks/131:edit
。 我们在应用中处处保持一致,用斜杠分隔 URI 组件。 所以我们可以做/tasks/131/edit
。 然而,这使得它看起来像是不同的资源,而实际上,它是与tasks/131
相同的资源。 只是现在,UI 控件处于不同的状态。
下面是一个示例,展示了一些用于测试路由的正则表达式:
// Wildcards are used to match against parameters in URIs...
console.log('second', (/^user\/(.*)/i).exec('user/123'));
// [ 'user/123', '123' ]
// Matches against the same URI, only more restrictively...
console.log('third', (/^user\/(\d+)/i).exec('user/123'));
// [ 'user/123', '123' ]
// Doesn't match, looking for characters and we got numbers...
console.log('fourth', (/^user\/([a-z])/i).test('user/123'));
// false
// Matches, we got a range of characters...
console.log('fifth', (/^user\/([a-z]+)/i).exec('user/abc'));
// [ 'user/abc', 'abc' ]
将资源映射到 uri
现在是时候看看中的 uri 了。 uri 最常见的形式是应用中的链接。 至少,这是一个想法; 拥有一个连接良好的应用。 虽然路由知道如何处理 uri,但我们还没有查看需要生成这些链接并将其插入到 DOM 中的所有位置。
有两种方法可以生成链接。 第一个过程有点手工,需要模板引擎和实用程序函数的帮助。 第二种方法采用更自动化的方法,试图扩展许多 uri 的可管理性。
手动构建 uri
如果一个组件在 DOM 中呈现内容,它可能会构建 URI 字符串并将它们添加到链接元素中。 当只有少量页面和 uri 时,这是非常容易做到的。 这里的可伸缩性问题是,JavaScript 应用中的页面数和 URI 数是相互补充的—大量 URI 意味着大量页面,反之亦然。
在实现视图时,我们可以使用路由模式映射配置作为参考,该结构指定了 uri 的样子以及当它们被激活时会发生什么。 在模板引擎的帮助下(大多数框架都以这种或那种形式使用模板引擎),我们可以使用模板特性根据需要动态呈现链接。 或者,由于缺少模板复杂性,我们需要一个独立的工具来为我们生成这些 URI 字符串。
当有很多 uri 要链接,有很多模板时,这就变得很有挑战性。 我们至少可以从模板语法中得到一些帮助,这使得构建这些链接不那么痛苦。 但是它仍然很耗时而且容易出错。 此外,由于在模板中构建链接的静态特性,我们将开始看到重复的模板内容。 我们至少需要硬编码我们所链接的资源的类型。
自动化资源 uri
我们链接到的绝大多数资源是来自 API 的实际资源,并在我们的代码中通过模型或集合表示。 在这种情况下,如果我们可以在每个模型或集合上使用相同的函数来构建 URI,而不是利用模板工具来为这些资源构建 URI,那就更好了。 这样,与构建 uri 相关的模板中的任何重复都会消失,因为我们只关心抽象的uri()
函数。
这种方法在简化模板的同时,引入了一个挑战,即使模型与路由同步。 例如,模型生成的 URI 字符串需要与路由期望看到的模式匹配。 因此,要么实现者需要遵守足够的纪律,以保持模型的 URI 生成与路由同步,要么模型需要以某种方式基于模式生成 URI 字符串。
如果路由使用某种简化的正则表达式语法来构建 URI 模式,则可以通过路由定义来保持模型实现的uri()
功能的自动同步。 这里的挑战是,模型需要知道路由——这可能会带来依赖扩展问题——我们有时想要模型,而不一定是路由。 如果我们的模型存储了向路由注册的 URI 模式呢? 然后它可以使用这个模式来生成 URI 字符串,并且它仍然只在一个地方更改。 然后,另一个组件将向路由注册模式,因此与模型没有紧密耦合。
下面是一个例子,展示了如何将 URI 字符串封装在模型中,远离其他组件:
// router.js
import events from 'events.js';
// The router is also an event broker...
export default class Router {
constructor() {
this.routes = [];
}
// Adds a given "pattern" and triggers event "name"
// when activated.
add(pattern, name) {
this.routes.push({
pattern: new RegExp('^' +
pattern.replace(/:\w+/g, '(.*)')),
name: name
});
}
// Adds any configured routes, and starts listening
// for navigation events.
start() {
var onHashChange = () => {
for (let route of this.routes) {
let result = route.pattern.exec(
location.hash.substr(1));
if (result) {
events.trigger('route:' + route.name, {
values: result.splice(1)
});
break;
}
}
};
window.addEventListener('hashchange', onHashChange);
onHashChange();
}
}
// model.js
export default class Model {
constructor(pattern, id) {
this.pattern = pattern;
this.id = id;
}
// Generates the URI string for this model. The pattern is
// passed in as a constructor argument. This means that code
// that needs to generate URI strings, like DOM manipulation
// code, can just ask the model for the URI.
get uri() {
return '#' + this.pattern.replace(/:\w+/, this.id);
}
}
// user.js
import Model from 'model.js';
export default class User extends Model {
// The URI pattern for instances of this model is
// encapsulated in this static method.
static pattern() {
return 'user/:id';
}
constructor(id) {
super(User.pattern(), id);
}
}
// group.js
import Model from 'model.js';
export default class Group extends Model {
// The "pattern()" method is static because
// all instances of "Group" models will use the
// same route pattern.
static pattern() {
return 'group/:id';
}
constructor(id) {
super(Group.pattern(), id);
}
}
// main.js
import Router from 'router.js';
import events from 'events.js';
import User from 'user.js';
import Group from 'group.js';
var router = new Router()
// Add routes using the "pattern()" static method. There's
// no need to hard-code any routes here.
router.add(User.pattern(), 'user');
router.add(Group.pattern(), 'group');
// Setup functions that respond to routes...
events.listen('route:user', (data) => {
console.log(`User ${data.values[0]} activated`);
});
events.listen('route:group', (data) => {
console.log(`Group ${data.values[0]} activated`);
});
// Construct new models, and user their "uri" property
// in the DOM. Again, nothing related to routing patterns
// need to be hard-coded here.
var user = new User(1);
document.querySelector('.user').href = user.uri;
var group = new Group(1);
document.querySelector('.group').href = group.uri;
router.start();
触发路由
最常见的路由触发器是用户单击应用中的链接。 正如上一节所讨论的,我们需要装备链接生成机制来处理多个页面和多个 uri。 这种影响者的另一个维度是实际触发行动本身。 例如,对于较小的应用,链接显然更少。 这也意味着用户的点击事件更少,更多的导航选择意味着更高的事件触发频率。
考虑不太为人所知的导航角色也很重要。 这包括重定向用户以响应某个后端任务的完成,或者只是一个直接的解决方案,从 a 点到 B 点。
用户动作
当用户在我们的应用中单击链接时,浏览器将获取该链接并更改 URI。 这包括进入应用的入口点——可能来自另一个网站或书签。 这就是使得链接和 uri 如此灵活,它们可以来自任何地方,指向任何东西。 在可能的地方利用链接是有意义的,因为这意味着我们的应用连接良好,而处理 URI 更改是我们的路由擅长的,并且可以轻松处理。
但是还有其他方法可以触发 URI 更改和后续的路由工作流。 例如,假设我们在一个create
事件表单上。 我们提交表单,响应返回成功-我们想让用户在create
事件页面? 或者我们想把他们带到显示事件列表的页面,这样他们就可以看到他们刚刚添加的事件? 在后一种情况下,手动更改 URI 是有意义的,而且非常容易实现。
我们的应用可以改变地址栏的不同方式
重定向用户
通过成功的 API 响应将用户重定向到新路由是手动触发路由的一个很好的例子。 还有一些其他的情况,我们会想重定向用户从他们当前所在的地方到一个新的页面,与他们正在执行的活动相符,或确保他们只是观察正确的信息。
并不是所有繁重的处理都需要在后端进行——我们可能会遇到运行进程的本地 JavaScript 组件,完成后,我们希望将用户带到应用中的另一个页面。
这里的关键思想是,结果比原因更重要——我们不太关心是什么原因导致 URI 更改。 真正重要的是能够以不可预见的方式使用路由。 随着应用的扩展,我们将面临这样的情况:解决办法通常是通过快速而简单的路由攻击。 完全控制应用的导航可以让我们更好地控制应用的扩展方式。
路由配置
我们的路由到它们的事件的映射通常比路由实现本身要大。 这是因为随着应用的增长和获取更多的路由模式,可能的列表也会变大。 很多时候,这是应用在满足伸缩需求时不可避免的结果。 诀窍是不要让大量的路由声明在它们自己的权重下崩溃,这可以通过多种方式发生。
有多种方法来配置给定路由实例响应的路由。 根据我们使用的框架,路由组件在如何配置方面可能比其他组件更灵活。 一般来说,有静态路由方法,或事件注册方法。 我们还需要考虑路由在任何给定时间禁用给定路由的能力。
静态路由声明
简单的应用通常使用静态声明来配置路由。 这通常意味着路由模式到回调函数的映射,都是在路由创建时完成的。 这种方法的优点在于所有路由模式的相对位置性。 一眼就能看出我们的路线配置发生了什么,我们不必去寻找特定的路线。 然而,当有很多路径时,这是不起作用的,因为我们必须搜索它们。 同时,也不存在关注点的分离,这对于试图独立完成工作的开发者来说并不适用。
注册事件
当有很多路由需要定义时,重点应该放在封装的路由上——哪些组件需要这些路由,它们如何告诉路由这些路由? 好吧,大多数路由将允许我们简单地调用一个方法,让我们添加新的路由配置。 然后我们只需要包含路由并添加来自组件的路由。
这绝对是朝正确方向迈出的一步; 它允许我们将路由声明保存在需要它们的组件中,而不是将整个应用的路由配置拼凑到一个对象中。 但是,我们可以进一步扩展这种可伸缩性。
与其让我们的组件直接依赖于一个路由实例,为什么不触发一个添加路由事件呢? 这将被任何正在监听该事件的路由接收。 也许我们的应用正在使用多个路由实例,每个实例都有自己的专门化(比如日志记录),它们都可以根据特定的标准侦听添加的路由。 关键是,我们的组件不需要关心路由实例,只需要关心当给定的模式与 URI 变化相匹配时,会有什么东西触发路由事件。
如何使用事件使组件与路由隔离
关闭路由
在我们配置了给定的路由之后,我们是否假定它在整个会话期间都是可行的路由? 或者,路由是否应该有某种方法使给定的路由失效? 这取决于我们如何从责任的角度看待具体案件。
例如,如果发生了事件,并且某些路由不再是可访问的——尝试它只会导致用户友好的错误——路由处理函数可以检查路由是否可访问。 然而,这增加了回调函数本身的复杂性,这种复杂性将散布在整个应用的回调中,而不是在一个地方自包含。
另一种方法是使用一些健全检查组件,当组件进入需要这样做的状态时,该组件会使路由失效。 当状态变为路由可以处理的状态时,这个组件也可以启用路由。
第三种方法是在第一次注册路由时添加一个保护函数作为选项。 当路由被匹配时,它就会运行这个函数,如果它通过了保护,它就会被正常激活,否则,它就会失败。 这种方法的伸缩性最好,因为所检查的状态; 与相关路由紧密耦合,不需要在路由的启用/禁用状态之间切换。 可以将保护函数看作是路由匹配条件的一部分。
下面是一个示例,展示了一个接受保护条件函数的路由。 如果这个保护函数存在并返回 false,则不会触发路由事件:
// router.js
import events from 'events.js';
// The router triggers events in response to
// route changes.
export default class Router {
constructor() {
this.routes = [];
}
// Adds a new route, with an optional
// guard function.
add(pattern, name, guard) {
this.routes.push({
pattern: new RegExp('^' +
pattern.replace(/:\w+/g, '(.*)')),
name: name,
guard: guard
});
}
start() {
var onHashChange = () => {
for (let route of this.routes) {
let guard = route.guard;
let result = route.pattern.exec(
location.hash.substr(1));
// If a match is found, and there's a guard
// condition, evaluate it. The event is only
// triggered if this passes.
if (result) {
if (typeof guard === 'function' && guard()) {
events.trigger('route:' + route.name, {
values: result.splice(1)
});
}
break;
}
}
};
window.addEventListener('hashchange', onHashChange);
onHashChange();
}
}
// main.js
import Router from 'router.js';
import events from 'events.js';
var router = new Router()
// Function that can be used as a guard condition
// with any route we declare. It's returning a random
// value to demonstrate the various outcomes, but this
// could be anything that we want applied to all our routes.
function isAuthorized() {
return !!Math.round(Math.random());
}
// The first route doesn't have a guard condition,
// and will always trigger a route event. The second
// route will only trigger a route event if the given
// callback function returns true.
router.add('open', 'open');
router.add('guarded', 'guarded', isAuthorized);
events.listen('route:open', () => {
console.log('open route is always accessible');
});
events.listen('route:guarded', (data) => {
console.log('made it past the guard function!');
});
router.start();
排除路由故障
一旦我们的路由增长到足够大的尺寸,我们将不得不排除复杂的情况。 如果我们事先知道可能出现的问题,我们就能更好地应对它们。 我们还可以在我们的路由实例中构建故障排除工具来帮助这个过程。 扩展我们架构的可寻址性意味着快速和可预测地响应问题。
冲突的路线
相互冲突的路线会引起极大的麻烦,因为它们很难追踪。 冲突的模式是后来添加到路由上的更具体模式的一般或类似版本。 更一般的模式冲突,因为它匹配的是最具体的 uri,而这些 uri 应该由更具体的模式匹配。 但是,它们从来没有被测试过,因为一般的路由是先执行的。
当发生这种情况时,可能根本看不出路由有什么问题,因为不正确的路由处理程序将运行得很好,在 UI 中,一切看起来都很正常——除了一件事有点不对头。 如果路由是按照先进先出顺序处理的,则特殊性很重要。 也就是说,如果首先添加更通用的路由模式,那么它们将总是在激活时匹配更具体的 URI 字符串。
当有很多 uri 时,像这样对它们进行排序的挑战是,这是一项耗时的工作。 我们必须比较我们可能添加到现有路线模式中的任何新路线的顺序。 如果所有的开发人员都被添加到同一个地方,那么他们之间的承诺也有可能发生冲突。 这是按组件分离路由的另一个优点。 它使潜在的冲突路由更容易发现和处理,因为组件可能有少量类似的 URI 模式。
下面的例子显示了一个路由组件有两条冲突的路由:
// Finds the first matching route in "routes" - tested
// against "uri".
function match() {
for (let route of routes) {
if (route.route.test(uri)) {
console.log('match', route.name);
break;
}
}
}
var uri = 'users/abc';
var routes = [
{ route: /^users/, name: 'users' },
{ route: /^users\/(\w+)/, name: 'user' }
];
match();
// match users
// Note that this probably isn't expected behavior
// if we look closely at the "uri". This illustrates
// the importance of order, when testing against a
// collection of URIs specs.
routes.reverse();
match();
// match user
记录初始配置
在配置了所有相关路由之前,路由不应该开始侦听 URI 更改事件。 例如,如果单个组件使用该组件所需的路由配置路由,那么在组件有机会配置其路由之前,我们不会希望路由开始侦听 URI 更改事件。
初始化其从属组件的主应用组件可能会引导这个进程,当完成时,告诉路由启动。 当每个组件都封装了自己的路由时,在开发过程中很难把握整个路由的配置。 为此,我们在路由中需要一个选项来记录它的整个配置——模式以及它们触发的事件。 这有助于我们扩大规模,因为我们不必牺牲模块化路线来获得大局。
记录路由事件
除了记录初始路由配置之外,如果路由能够记录 URI 更改事件触发时发生的生命周期,将会很有帮助。 这与我们在前一章中讨论的事件机制日志记录不同——这些事件将在路由触发路由事件后记录。
如果我们正在构建一个包含大量路由的大型 JavaScript 架构,我们会想要知道关于路由的一切,以及它在运行时的行为。 路由对于我们的应用的可扩展性是如此的基础,所以我们想在这里投资于微小的细节。
例如,当路由通过可用路由查找匹配时,了解它在做什么是很有用的。 查看由路由解析出的 URI 字符串的结果也很有用,这样我们就可以将其与路由事件处理程序所看到的结果进行比较。 并不是所有的路由组件都支持这种级别的日志记录。 如果我们确实需要它,一些框架将提供足够的进入组件的入口点,以及良好的扩展机制。
处理无效的资源状态
有时,我们会忘记路由是无状态的; 它接受一个 URI 字符串作为输入,并基于模式匹配标准触发事件。 与可寻址性相关的伸缩问题与路由状态无关,而是与侦听路由的组件状态有关。
例如,假设我们从一个资源导航到另一个资源。 当我们访问这个新资源时,第一个资源可以发生很多事情。 它很容易改变,让这个特定的用户访问它是非法的,同时,它在他们的历史中,他们需要做的就是按下后退按钮。
路由和可寻址性可以引入到我们的应用中。 然而,路由并没有责任处理这些边缘情况。 它们的发生是由于大量 uri、大量组件和将它们联系在一起的复杂业务规则的组合。 路由只是一种帮助我们处理大规模策略的机制,而不是一个实现策略的地方。
小结
这一章详细讨论了可寻址性,以及如何在应用扩展时实现这个架构属性。
我们首先讨论了路由和可寻址性的不同方法——散列更改事件和利用现代浏览器中可用的历史 API。 大多数框架为我们抽象了这些差异。 接下来,我们讨论了路由的职责,以及如何通过触发事件将它们与其他组件解耦。
uri 本身的设计在我们的软件的可伸缩性中也扮演着重要的角色,因为它们需要保持一致和可预测。 甚至用户也可以使用这种可预测性来帮助自己扩展我们软件的使用。 uri 编码信息,然后传递给响应路由的处理程序; 这一点也需要考虑。
然后我们研究了各种触发路径的方式。 这里的标准方法是单击链接。 如果我们的应用连接良好,它将到处都有链接。 为了帮助我们扩展大量链接,我们需要一种自动生成 URI 字符串的方法。 接下来,我们将查看组件运行所需的元数据。 这些是我们组件的用户首选项和默认值。
版权属于:月萌API www.moonapi.com,转载请注明出处