URL 和路由
阅读完本指南后,您将了解
- URL 在客户端渲染应用中扮演的角色,以及它与传统服务器端渲染应用的不同之处。
- 如何使用 Flow Router 为您的应用定义客户端和服务器路由。
- 如何根据 URL 显示不同的内容。
- 如何根据 URL 动态加载应用程序模块。
- 如何构造指向路由的链接并以编程方式跳转到路由。
客户端路由
在 Web 应用中,路由是使用 URL 来驱动用户界面 (UI) 的过程。URL 是每个 Web 浏览器中的一个突出功能,从用户的角度来看,它具有几个主要功能
- 书签 - 用户可以在其 Web 浏览器中为 URL 添加书签,以保存他们希望稍后返回的内容。
- 分享 - 用户可以通过发送指向特定页面的链接与他人共享内容。
- 导航 - URL 用于驱动 Web 浏览器的后退/前进功能。
在传统的 Web 应用堆栈中,服务器每次渲染一个 HTML 页面,URL 是用户访问应用的基本入口点。用户通过点击 URL 导航应用,这些 URL 通过 HTTP 发送到服务器,服务器通过服务器端路由器做出相应的响应。
相反,Meteor 遵循数据在线传输的原则,服务器不以 URL 或 HTML 页面的形式进行思考。客户端应用通过 DDP 与服务器通信。通常,当应用加载时,它会初始化一系列订阅,以获取渲染应用所需的数据。当用户与应用交互时,可能会加载不同的订阅,但在此过程中技术上不需要涉及 URL——您可以拥有一个 Meteor 应用,其中 URL 从不改变。
但是,上面列出的 URL 的大多数面向用户的特性对于典型的 Meteor 应用仍然相关。由于服务器不是 URL 驱动的,因此 URL 成为用户当前查看的客户端状态的有用表示。但是,与服务器端渲染的应用不同,它不需要描述用户当前状态的全部内容;它只需要包含您希望可链接的部分。例如,URL 应该包含应用于页面的任何搜索过滤器,但不一定包含下拉菜单或弹出窗口的状态。
使用 Flow Router
要向您的应用添加路由,请安装 ostrio:flow-router-extra
包
meteor add ostrio:flow-router-extra
Flow Router 是 Meteor 的一个社区路由包。
使用 Flow Router Extra
Flow Router Extra 由 kadira
仔细扩展了 flow-router
包,并增加了 waitOn 和模板上下文。Flow Router Extra 共享原始“Flow Router”API,包括为额外功能(如 waitOn
、模板上下文和内置 .render()
)提供的风格。注意:arillo:flow-router-helpers
和 zimme:active-route
已经内置到 Flow Router Extra 中,并且已更新以支持最新的 Meteor 版本。
要向您的应用添加路由,请安装 ostrio:flow-router-extra
包
meteor add ostrio:flow-router-extra
定义一个简单的路由
路由器的基本目的是匹配某些 URL 并作为结果执行操作。所有这些都在客户端发生,在应用用户的浏览器或移动应用容器中。让我们从 Todos 示例应用中举一个例子
FlowRouter.route('/lists/:_id', {
name: 'Lists.show',
action(params, queryParams) {
console.log("Looking at a list?");
}
});
此路由处理程序将在两种情况下运行:如果页面最初加载到与 URL 模式匹配的 URL,或者如果页面打开时 URL 更改为与模式匹配的 URL。请注意,与服务器端渲染的应用不同,URL 可以更改而无需向服务器发出任何其他请求。
当路由匹配时,action
方法执行,您可以执行任何需要的操作。路由的 name
属性是可选的,但它让我们以后可以更方便地引用此路由。
URL 模式匹配
考虑以下 URL 模式,在上面的代码片段中使用
'/lists/:_id'
以上模式将匹配某些 URL。您可能会注意到 URL 的一部分以 :
为前缀 - 这意味着它是一个URL 参数,并且将匹配该路径段中存在的任何字符串。Flow Router 将使 URL 的该部分在当前路由的 params
属性上可用。
此外,URL 可能包含 HTTP 查询字符串(可选的 ?
之后的部分)。如果是这样,Flow Router 也会将其拆分为命名参数,它称之为 queryParams
。
以下是一些 URL 示例以及生成的 params
和 queryParams
URL | 匹配模式? | params | queryParams |
---|---|---|---|
/ | 否 | ||
/about | 否 | ||
/lists/ | 否 | ||
/lists/eMtGij5AFESbTKfkT | 是 | { _id: “eMtGij5AFESbTKfkT”} | { } |
/lists/1 | 是 | { _id: “1”} | { } |
/lists/1?todoSort=top | 是 | { _id: “1”} | { todoSort: “top” } |
请注意,params
和 queryParams
中的所有值始终都是字符串,因为 URL 无法以任何方式对数据类型进行编码。例如,如果您希望参数表示一个数字,则可能需要在访问它时使用 parseInt(value, 10)
进行转换。
访问路由信息
除了将参数作为参数传递给路由上的 action
函数之外,Flow Router 还通过全局单例 FlowRouter
上的(反应式和非反应式)函数提供了各种信息。当用户在您的应用中导航时,这些函数的值将相应地发生变化(在某些情况下是反应式的)。
与应用中的任何其他全局单例一样(有关存储的信息,请参阅 数据加载),最好限制对 FlowRouter
的访问。这样,应用的不同部分将保持模块化和更独立。对于 FlowRouter
,最好仅从组件层次结构的顶部访问它,无论是在“页面”组件中还是在包装它的布局中。有关访问数据的更多信息,请阅读 UI 文章。
当前路由
在代码中访问有关当前路由的信息非常有用。以下是一些您可以调用的反应式函数
FlowRouter.getRouteName()
获取路由的名称FlowRouter.getParam(paramName)
返回单个 URL 参数的值FlowRouter.getQueryParam(paramName)
返回单个 URL 查询参数的值
在 Todos 应用中列表页面的示例中,我们使用 FlowRouter.getParam('_id')
访问当前列表的 ID(我们将在下面详细介绍)。
突出显示活动路由
在组件层次结构更深层次的地方访问全局 FlowRouter
单例以访问当前路由的信息是合理的,这种情况是在通过导航组件渲染链接时。通常需要以某种方式突出显示“活动”路由(这是用户当前正在查看的路由或网站部分)。
在 Todos 示例应用中,我们在 App_body
模板中链接到用户知道的每个列表
{{#each list in lists}}
<a class="list-todo {{activeListClass list}}">
...
{{list.name}}
</a>
{{/each}}
我们可以使用 activeListClass
助手来确定用户当前是否正在查看该列表
Template.App_body.helpers({
activeListClass(list) {
const active = ActiveRoute.name('Lists.show')
&& FlowRouter.getParam('_id') === list._id;
return active && 'active';
}
});
基于路由进行渲染
现在我们了解了如何定义路由以及如何访问有关当前路由的信息,我们就可以开始做通常在用户访问路由时想要做的事情——渲染一个代表它的用户界面到屏幕上。
在本节中,我们将讨论如何使用 Blaze 作为 UI 引擎来渲染路由。如果您使用 React 或 Angular 构建您的应用,您最终会得到类似的概念,但代码会略有不同。
使用 Flow Router 时,在不同 URL 上在页面上显示不同视图的最简单方法是使用补充的 Blaze Layout 包。首先,确保您已安装 Blaze Layout 包
meteor add kadira:blaze-layout
要使用此包,我们需要定义一个“布局”组件。在 Todos 示例应用中,该组件称为 App_body
<template name="App_body">
...
{{> Template.dynamic template=main}}
...
</template>
(这不是整个 App_body
组件,但我们在这里重点介绍最重要的部分)。在这里,我们使用 Blaze 的一个称为 Template.dynamic
的功能来渲染一个附加到数据上下文 main
属性的模板。使用 Blaze Layout,我们可以在访问路由时更改该 main
属性。
我们在 Lists.show
路由定义的 action
函数中执行此操作
FlowRouter.route('/lists/:_id', {
name: 'Lists.show',
action() {
BlazeLayout.render('App_body', {main: 'Lists_show_page'});
}
});
这意味着,每当用户访问表单为 /lists/X
的 URL 时,Lists.show
路由将启动,触发 BlazeLayout
调用以设置 App_body
组件的 main
属性。
组件作为页面
请注意,我们调用要渲染的组件为 Lists_show_page
(而不是 Lists_show
)。这表示此模板由 Flow Router 操作直接渲染,并形成此 URL 渲染层次结构的“顶部”。
Lists_show_page
模板无需参数即可渲染——此模板负责从当前路由收集信息,然后将其传递到其子模板中。相应地,Lists_show_page
模板与渲染它的路由紧密相关,因此它需要是一个智能组件。有关智能组件和可重用组件的更多信息,请参阅有关 UI/UX 的文章。
对于像 Lists_show_page
这样的“页面”智能组件来说,这样做是有意义的
- 收集路由信息,
- 订阅相关订阅,
- 从这些订阅中获取数据,以及
- 将数据传递到子组件中。
在这种情况下,Lists_show_page
的 HTML 模板看起来非常简单,大部分逻辑都在 JavaScript 代码中
<template name="Lists_show_page">
{{#each listId in listIdArray}}
{{> Lists_show (listArgs listId)}}
{{else}}
{{> App_notFound}}
{{/each}}
</template>
({{#each listId in listIdArray}}}
是 页面到页面过渡 的动画技术)。
Template.Lists_show_page.helpers({
// We use #each on an array of one item so that the "list" template is
// removed and a new copy is added when changing lists, which is
// important for animation purposes.
listIdArray() {
const instance = Template.instance();
const listId = instance.getListId();
return Lists.findOne(listId) ? [listId] : [];
},
listArgs(listId) {
const instance = Template.instance();
return {
todosReady: instance.subscriptionsReady(),
// We pass `list` (which contains the full list, with all fields, as a function
// because we want to control reactivity. When you check a todo item, the
// `list.incompleteCount` changes. If we didn't do this the entire list would
// re-render whenever you checked an item. By isolating the reactiviy on the list
// to the area that cares about it, we stop it from happening.
list() {
return Lists.findOne(listId);
},
// By finding the list with only the `_id` field set, we don't create a dependency on the
// `list.incompleteCount`, and avoid re-rendering the todos when it changes
todos: Lists.findOne(listId, {fields: {_id: true}}).todos()
};
}
});
实际上是 listShow
组件(一个可重用组件)负责渲染页面的内容。由于页面组件将参数传递到可重用组件,因此它能够非常机械地工作,并且与路由通信和渲染页面的关注点已分离。
注销时更改页面
有一些渲染逻辑类型看起来与路由相关,但也似乎与用户界面渲染相关。一个经典的例子是授权;例如,如果用户尚未登录,您可能希望为某些页面子集渲染登录表单。
最好将所有关于渲染什么的逻辑都保存在组件层次结构中(即渲染组件的树)。因此,此授权应该在组件内部发生。假设我们想将其添加到上面我们看到的Lists_show_page
中。我们可以做类似的事情
<template name="Lists_show_page">
{{#if currentUser}}
{{#each listId in listIdArray}}
{{> Lists_show (listArgs listId)}}
{{else}}
{{> App_notFound}}
{{/each}}
{{else}}
Please log in to edit posts.
{{/if}}
</template>
当然,我们可能会发现我们需要在应用程序的多个需要访问控制的页面之间共享此功能。我们可以通过将模板包装在包含我们所需行为的包装器“布局”组件中来在模板之间共享功能。
您可以通过使用 Blaze 的“模板作为块助手”功能来创建包装器组件(请参阅Blaze 文章)。以下是如何编写授权模板
<template name="App_forceLoggedIn">
{{#if currentUser}}
{{> Template.contentBlock}}
{{else}}
Please log in see this page.
{{/if}}
</template>
一旦该模板存在,我们就可以包装我们的Lists_show_page
<template name="Lists_show_page">
{{#App_forceLoggedIn}}
{{#each listId in listIdArray}}
{{> Lists_show (listArgs listId)}}
{{else}}
{{> App_notFound}}
{{/each}}
{{/App_forceLoggedIn}}
</template>
这种方法的主要优点是,在查看Lists_show_page
时,可以立即清楚用户访问页面时会发生什么行为。
可以通过将模板包装在多个包装器中或创建一个组合多个包装器模板的元包装器来组合这种类型的多个行为。
更改路由
当用户到达新路由时渲染更新的 UI 如果不为用户提供到达新路由的方法,则没有多大用处!最简单的方法是使用可靠的<a>
标签和 URL。您可以使用诸如FlowRouter.pathFor
之类的助手自己生成 URL,以显示指向特定路由的链接。例如,在 Todos 示例应用程序中,我们的导航链接如下所示
<a href="{{pathFor 'Lists.show' _id=list._id}}" title="{{list.name}}"
class="list-todo {{activeListClass list}}">
以编程方式路由
在某些情况下,您希望根据用户操作更改路由,而不是他们点击链接。例如,在示例应用程序中,当用户创建新列表时,我们希望将其路由到他们刚刚创建的列表。我们通过在知道新列表的 ID 后调用FlowRouter.go()
来做到这一点
import { insert } from '../../api/lists/methods.js';
Template.App_body.events({
'click .js-new-list'() {
const listId = insert.call();
FlowRouter.go('Lists.show', { _id: listId });
}
});
如果您想更改 URL 的一部分,也可以使用FlowRouter.setParams()
和FlowRouter.setQueryParams()
。例如,如果我们正在查看一个列表并想转到另一个列表,我们可以编写
FlowRouter.setParams({_id: newList._id});
当然,调用FlowRouter.go()
始终有效,因此除非您尝试针对特定情况进行优化,否则最好使用它。
在 URL 中存储数据
正如我们在引言中讨论的那样,URL 实际上是用户正在查看的客户端状态某些部分的序列化。尽管参数只能是字符串,但可以通过序列化将任何类型的数据转换为字符串。
通常,如果您想在 URL 参数中存储任意可序列化的数据,您可以使用EJSON.stringify()
将其转换为字符串。您需要使用encodeURIComponent
对字符串进行 URL 编码,以删除 URL 中有意义的任何字符
FlowRouter.setQueryParams({data: encodeURIComponent(EJSON.stringify(data))});
然后,您可以使用EJSON.parse()
从 Flow Router 中获取数据。请注意,Flow Router 会自动为您执行 URL 解码
const data = EJSON.parse(FlowRouter.getQueryParam('data'));
重定向
有时,您的用户最终会到达不适合他们的页面。也许他们正在寻找的数据已移动,也许他们在管理员面板页面上并注销了,或者也许他们刚刚创建了一个新对象,并且您希望他们最终到达他们刚刚创建的对象的页面。
通常,我们可以响应用户的操作进行重定向,方法是调用FlowRouter.go()
及其朋友,就像我们在上面的列表创建示例中一样,但如果用户直接浏览到不存在的 URL,则了解如何立即重定向很有用。
如果 URL 已过时(有时您可能会更改应用程序的 URL 方案),您可以在路由的action
函数内进行重定向
FlowRouter.route('/old-list-route/:_id', {
action(params) {
FlowRouter.go('Lists.show', params);
}
});
动态重定向
上述方法仅适用于静态重定向。但是,有时您需要加载一些数据才能确定重定向到的位置。在这种情况下,您需要渲染组件层次结构的一部分以订阅您需要的数据。例如,在 Todos 示例应用程序中,我们希望使根 (/
) 路由重定向到第一个已知列表。为了实现这一点,我们需要渲染一个特殊的App_rootRedirector
路由
FlowRouter.route('/', {
name: 'App.home',
action() {
BlazeLayout.render('App_body', {main: 'App_rootRedirector'});
}
});
App_rootRedirector
组件渲染在App_body
布局内,该布局负责在渲染其子组件之前订阅用户知道的列表集,并且我们保证至少有一个这样的列表。这意味着如果App_rootRedirector
最终被创建,将加载一个列表,因此我们可以执行
Template.App_rootRedirector.onCreated(function rootRedirectorOnCreated() {
// We need to set a timeout here so that we don't redirect from inside a redirection
// which is a limitation of the current version of FR.
Meteor.setTimeout(() => {
FlowRouter.go('Lists.show', Lists.findOne());
});
});
如果您需要等待在创建时尚未订阅的特定数据,则可以使用autorun
和subscriptionsReady()
来等待该订阅
Template.App_rootRedirector.onCreated(function rootRedirectorOnCreated() {
// If we needed to open this subscription here
this.subscribe('lists.public');
// Now we need to wait for the above subscription. We'll need the template to
// render some kind of loading state while we wait, too.
this.autorun(() => {
if (this.subscriptionsReady()) {
FlowRouter.go('Lists.show', Lists.findOne());
}
});
});
用户操作后重定向
通常,您只想在用户完成特定操作时以编程方式转到新路由。上面我们看到了一种情况(创建新列表),我们希望乐观地执行它——即在我们收到服务器关于方法成功的回复之前。我们可以这样做,因为我们合理地预期该方法在几乎所有情况下都会成功(有关此的进一步讨论,请参阅UI/UX 文章)。
但是,如果我们想等待方法从服务器返回,我们可以将重定向放在方法的回调中
Template.App_body.events({
'click .js-new-list'() {
lists.insert.call((err, listId) => {
if (!err) {
FlowRouter.go('Lists.show', { _id: listId });
}
});
}
});
您还希望显示某种指示,表明该方法在他们单击按钮和重定向完成之间正在工作。如果方法返回错误,请不要忘记提供反馈。
高级路由
动态加载模块
动态导入 首次出现在Meteor 1.5中。此技术可以显着减少客户端的捆绑包大小,并根据请求动态加载模块和依赖项,在本例中,基于当前 URI。
假设我们有index.html
和index.js
,其中包含index
模板的代码,并且这是应用程序中唯一依赖于大型moment
包的地方。这意味着moment
包在应用程序的其他部分不需要,它只会浪费带宽并减慢加载时间。
<!-- /imports/client/index.html -->
<template name="index">
<h1>Current time is: {{time}}</h1>
</template>
// /imports/client/index.js
import moment from 'moment';
import { Template } from 'meteor/templating';
import './index.html';
Template.index.helpers({
time() {
return moment().format('LTS');
}
});
// /imports/lib/routes.js
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
FlowRouter.route('/', {
name: 'index',
waitOn() {
// Wait for index.js load over the wire
return import('/imports/client/index.js');
},
action() {
BlazeLayout.render('App_body', {main: 'index'});
}
};
有关更多信息和示例,请参阅此线程
页面丢失
如果用户输入了错误的 URL,则您可能希望向他们显示一些有趣的未找到页面。实际上,未找到页面有两类。第一种是输入的 URL 与您的任何路由定义都不匹配。您可以使用FlowRouter.notFound
来处理此问题
// the App_notFound template is used for unknown routes and missing lists
FlowRouter.notFound = {
action() {
BlazeLayout.render('App_body', {main: 'App_notFound'});
}
};
第二种情况是 URL 有效,但实际上与任何数据都不匹配。在这种情况下,URL 与路由匹配,但一旦路由成功订阅,它就会发现没有数据。在这种情况下,页面组件(订阅并获取数据)通常会渲染未找到模板而不是页面的常用模板
<template name="Lists_show_page">
{{#each listId in listIdArray}}
{{> Lists_show (listArgs listId)}}
{{else}}
{{> App_notFound}}
{{/each}}
<template>
分析
通常,您希望了解应用程序的哪些页面访问次数最多,以及用户来自哪里。您可以在部署指南中阅读有关如何设置基于 Flow Router 的分析的信息。
服务器端路由
正如我们所讨论的,Meteor 是一个用于客户端渲染应用程序的框架,但这并不总是消除对服务器渲染路由的需求。服务器端路由有三个主要用例。
用于 API 访问的服务器路由
尽管 Meteor 允许您编写低级连接处理程序以在服务器端创建任何类型的 API,但如果您只想创建方法和发布的 RESTful 版本,您通常可以使用simple:rest
包来执行此操作。有关更多信息,请参阅数据加载和方法文章。
如果您需要更多控制权,可以使用全面的nimble:restivus
包来创建或多或少您需要的任何内容,以及您需要的任何本体。
服务器渲染
Blaze UI 库不支持服务器端渲染,因此如果您使用 Blaze,则无法在服务器上渲染页面。但是,React UI 库确实支持。这意味着如果您使用 React 作为渲染框架,则可以在服务器上渲染 HTML。
尽管 Flow Router 可用于或多或少地像我们上面为 Blaze 描述的那样渲染 React 组件,但在撰写本文时,Flow Router 对 SSR 的支持仍处于实验阶段。但是,如果您想将 SSR 用于 Meteor,这可能是目前最好的方法。
用于其他资源的服务器路由
您可能希望在服务器上提供其他资源或接收 Webhook。如果您需要处理 URL 动态部分的任何更复杂的事情,您可能希望实现Picker,它是一个简单的服务器端路由器,用于处理动态路由。
如果您需要在提供其他服务器端资源(如 PDF 文档或 XLSX 电子表格)时对用户进行身份验证,则可以使用mhagmajer:server-router
包轻松地执行此操作。有一篇博文更详细地描述了这一点。