URLs和路由

通过阅读本章,你会了解到:

  1. URLs在客户端-渲染的应用中所扮演的角色,以及其和传统的服务器-渲染的应用之间的区别。
  2. 如何使用Flow路由器定义应用中客户端和服务器端的路由。
  3. 如何根据不同的URL在你的应用中显示不同的内容。
  4. 如何链接到路由以及通过编程进行路由。

客户端路由

在web应用程序中,路由 是指使用URLs来驱动用户界面的过程。URLs是每个web浏览器显而易见的功能,从用户的角度来看,它有几个重要的功能:

  1. 书签 - 用户能够在它们的浏览器中设置URLs的书签,用来稍后回来访问URLs指向的内容。
  2. 分享 - 用户可以通过发送一个指向某个特定页面的链接来共享内容。
  3. 导航 - URLs用来驱动web浏览器的前进/后退功能。

在传统的web应用程序里,服务器一次渲染一个页面,URL是用户访问应用程序的基础入口。用户是通过点击URLs在应用程序中进行导航的,这些URLs是服务器通过HTTP发送的,并且服务器能够很好地通过服务器端的路由器进行响应。

相反,流星的操作原则是 数据连线,服务器不考虑URLs或HTML页面。客户端应用和服务器之间的通信是通过DDP。通常当一个应用程序装载时,它会初始化一系列的 订阅 用来获取需要渲染应用的数据。由于用户和应用程序之间的交互,不同的订阅会被装载,但这方面URL不需要在技术上的任何参与 - 你可以很容易地让一个流星应用的URL不做任何的改变。

然而,上面列出的多数面向用户的URLs功能仍然和典型的流星应用程序有关。由于服务器不是URL-驱动的,URL只是变为用户在客户端查看时的客户端状态的表示。和一个服务器端-渲染的应用程序不同,它不需要描述用户的现有状态的整体性;它只需要简单地包含你想要链接的部分。例如,URL会有此页面上应用的任意的搜索过滤器,但不会有下拉菜单或弹出框的状态。

使用Flow路由器

要在你的应用中添加路由,可以安装kadira:flow-router包:

meteor add kadira:flow-router

Flow路由器是一个为流星设计的、由社区开发支持的路由包。在撰写本指南的时候,它的版本是2.x。要获取更详细的关于Flow路由器提供的所有的功能,请参见Kadira Meteor routing guide

定义一个简单的路由

一个路由器的基本目标就是用来匹配特定的URLs和执行相应的动作作为结果。这都是发生在客户端应用中用户的浏览器或移动应用容器中的。让我们来看一个来自于Todos示例应用的例子:

FlowRouter.route('/lists/:_id', {
  name: 'Lists.show',
  action(params, queryParams) {
    console.log("Looking at a list?");
  }
});

路由处理器会在两种状况下运行:如果某个URL匹配URL模式相应的页面进行初始化装载,或者如果URL改变到一个符合页面打开的模式。注意和服务器端-渲染的应用不同,URL能够在不向服务器发送请求的前提下就发生变化。

当路由被匹配时,action方法得到执行,并且你能执行任意你想要的操作。路由的name属性是可选的,但可以让我们在后续操作时更容易找到它。

URL模式匹配

考虑以下的URL模式使用在下面的代码片段中:

'/lists/:_id'

上面的模式会匹配特定的URLs。你可能注意到被:分隔的部分 - 就是说这是一个url参数,并且会匹配任意出现在路径部分的字符串。Flow路由器会让URL的那部分作为现有路由的params属性部分。

另外URL能够特定一个HTTP查询字符串(?之后的那部分)。如果这样,Flow路由器也会将它分割成为命名的参数,称之为queryParams

以下是一些URLs,params结果和queryParams的例子:

URL 是否匹配模式? params queryParams
/ no
/about no
/lists/ no
/lists/eMtGij5AFESbTKfkT yes { _id: "eMtGij5AFESbTKfkT"} { }
/lists/1 yes { _id: "1"} { }
/lists/1?todoSort=top yes { _id: "1"} { todoSort: "top" }

注意所有这些在paramsqueryParams中的值都是字符串,因为URLs并没有任何进行编码数据类型的方法。例如,如你想要一个用来表示数字的参数,可能在你访问它的时候使用parseInt(value, 10)来进行转换。

访问路由信息

另外作为参数值传递到路由的action方法的参数,Flow路由器会通过全局单例FlowRouter的(响应式的和不是响应式的)方法让这些信息成为可用。作为用户在应用中进行导航,这些方法的值会相应地改变(在一些情况下是响应式的)。

和在应用程序中的其他全局单例一样(参见数据装载一章中的关于存储一节的信息),最好限制对于FlowRouter的访问。这样可以让你的应用保持模块化和更多的独立性。这种情况下的FlowRouter,最好仅从你的顶层组件层级上对其进行访问,要么是在“页面”组件中,要么是包裹它们的排版组件。要阅读更多访问数据的信息,参见用户界面一章

现有的路由

在代码中访问现有路由的信息是很有用的。你可以调用一些响应式的方法:

  • FlowRouter.getRouteName() 会得到路由的名字
  • FlowRouter.getParam(paramName) 返回某个URL的参数值
  • FlowRouter.getQueryParam(paramName) 返回某个URL查询参数的值

在Todos应用的list页面中,访问当前的list的id是通过FlowRouter.getParam('_id’)(我们在下面会讨论更多相关信息)。

高亮激活路由

一种情况,在深层次的组件层级中访问全局 FlowRouter 单例以获得现有的路由信息是有意义的,那就是当渲染的链接是通过一个导航组件的时候。这种情况经常要求高亮“激活”路由 - 通过一些方式(这是用户现在正在查看的该路由或站点的某个部分)。

对此有个非常方便的包zimme:active-route

meteor add zimme:active-route

在Todos应用程序中,我们在App_body模版中链接着用户知道的每个list:

{{#each list in lists}}
  <a class="list-todo {{activeListClass list}}">
    ...

    {{list.name}}
  </a>
{{/each}}

我们可以通过 activeListClass 帮助器 来确定用户现在查看的list:

Template.App_body.helpers({
  activeListClass(list) {
    const active = ActiveRoute.name('Lists.show')
      && FlowRouter.getParam('_id') === list._id;

    return active && 'active';
  }
});

基于路由进行渲染

我们理解了如何定义路由和如何访问关于当前路由的信息,我们现在要做的就是通常要做的 - 当一个用户访问一个路由时,渲染一个用户界面到屏幕上并表示它。

在本节中,我们将讨论如何通过Blaze作为用户界面引擎来渲染路由。如果你的应用是使用React或Angular,你将会具有相似的概念但是代码会有所不同。

当使用Flow路由器时,为不同的URLs,在页面上显示不同的视图的最简单方式是 - 使用配套的Blaze排版包。首先,确保你安装了Blaze排版包:

meteor add kadira:blaze-layout

要使用这个包,我们需要定义一个“排版”组件。在Todos示例程序中,这个组件叫做App_body

<template name="App_body">
  ...
  {{> Template.dynamic template=main}}
  ...
</template>

(这不是整个App_body组件的代码,但我们标记了最重要的部分)。 这里我们使用一个Blaze特征,叫做 Template.dynamic 来渲染一个附加在数据上下文上的 main 属性的模版。通过使用Blaze排版,当一个路由被访问时,我们可以改变 main 属性。

我们在Lists.show路由定义的action函数中可以这么做:

FlowRouter.route('/lists/:_id', {
  name: 'Lists.show',
  action() {
    BlazeLayout.render('App_body', {main: 'Lists_show_page'});
  }
});

就是说,无论何时一个用户访问 /lists/X 形式的URL,List.show 路由都会介入,触发BlazeLayout调用,来设置 App_body 组件的 main 属性。

将组件作为页面

注意到我们调用的组件被渲染为Lists_show_page(而不是Lists_show)。这表示这个模版已经直接地由一个Flow路由器的动作进行了渲染,并且已经为这个URL形成了 ’顶层’ 的渲染层级。

List_show_page 模版中,并 不通过 参数进行渲染 — 这是该模版的职责,即用来收集从当前路由而来的信息,并且将这些信息传递到它的子模版中。相应地 Lists_show_page 模版绑定到渲染它的路由,由此它需要成为一个聪明的组件。参见UI/UX一文,以获取关于聪明的和重用的组件的信息。

将像 Lists_show_page 的“页面” 作为聪明的组件是有必要的,因为可以:

  1. 收集路由信息,
  2. 订阅相关的 订阅 ,
  3. 从这些订阅中访问数据,并且
  4. 将数据传递到子-组件中。

这种情况下,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 我们使用 #each 在每项的数组上,为让 “list” 模版可以被移除
  // removed and a new copy is added when changing lists, which is 并且,当list们发生变化时,一个新的拷贝可以被加入到里面,这对于
  // 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 我们传递 `list` ()
      // 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产生URLs,但使用arillo:flow-router-helpers 则更容易:

meteor add arillo:flow-router-helpers

现在你有了这个包,你可以在应用中使用帮助器来显示一个到特定路由的链接。例如,在Todos示例程序中,我们的导航链接看上去会是:

<a href="{{pathFor 'Lists.show' _id=list._id}}" title="{{list.name}}"
    class="list-todo {{activeListClass list}}">

编程路由

在一些情况下你需要基于用户行为(在他们点击一个链接之外)改变路由。例如,在示例应用中,当用户创建一个新的list,我们需要路由用户到他们刚创建的那个list。我们通过调用 FlowRouter.go() 一旦我们知道新的list的id,来达到目的:

Template.App_body.events({
  'click .js-new-list'() {
    const listId = Lists.methods.insert.call();
    FlowRouter.go('Lists.show', { _id: listId });
  }
});

你也可以只改变URL的一部分,如果你想要着么做的话,可以使用FlowRouter.setParams()FlowRouter.setQueryParams()。例如,如果我们查看一个list并且想要到另一个去,可以这么写:

FlowRouter.setParams({_id: newList._id});

当然,调用 FlowRouter.go() 总是会工作的,所以除非你想要优化一个特定的情况,否则就请使用它。

在URL中存储数据

如我们在简介中讨论的,URL其实只是一个用户正在查看的客户端状态的部分序列化。虽然参数只能是字符串,但是将任意类型的数据通过序列化都可以转化为字符串。

总的来说如果你想要在一个URL参数中存储任意序列化数据,你可以使用 EJSON.stringify() 来将数据转化为一个字符串。你会需要 encodeURIComponent 来移除对于URL不兼容的字符:

FlowRouter.setQueryParams({data: encodeURIComponent(EJSON.stringify(data))});

接着使用 EJSON.parse()从Flow路由器中获取回数据。注意Flow路由器为你自动使用了URL解码:

const data = EJSON.parse(FlowRouter.getQueryParam('data'));

重定向

有时,你的用户会出现在他们不应该访问到的页面。也许是由于他们查找的数据已经被移除了,也许他们已经登出但还停留在管理员看板页面上,或者是他们创建了一个新的对象而你想要他们出现在他们刚创建的对象的页面上。

通常我们在对用户行为的响应中重定向,这是通过FlowRouter.go()和它的朋友一起完成的,像在我们创建list的例子中,但如果某个用户直接浏览到一个并不存在的URL,那么立即重定向就很有用了。

如果一个URL只是简单地过期(有时你可能改变了应用程序的URL架构),可能需要在路由的action函数中进行重定向:

FlowRouter.route('/old-list-route/:_id', {
  action(params) {
    FlowRouter.go('Lists.show', params);
  }
});

动态地重定向

上面的方式只会在对静态重定向时工作。而有时你需要装载一些数据才能搞清楚需要重定向到什么地方。这种情况下你需要渲染组件层级中的一部分来订阅你需要的数据。如,在Todos示例应用中,我们想要将根(/)路由重定向到第一个知道的list。为了达成这样的目的,我们需要渲染一个特殊的App_rootRedirector路由:

FlowRouter.route('/', {
  name: 'App.home',
  action() {
    BlazeLayout.render('App_body', {main: 'App_rootRedirector'});
  }
});

App_rootRedirector 组件是在 App_body 排版中进行渲染的,它会订阅一组属于某个用户的列表,在渲染它的子组件 之前 ,并且我们保证至少有一个这样的list。这就是说如果 App_rootredirector 在list创建后才会出现,至少有一个list会被装载,所以我们可以简单的这么做:

Template.App_rootRedirector.onCreated(() => {
  // 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());
  });
});

如果你需要在特定的时刻等待特定的数据,而这数据并不是你已经订阅好的,你需要使用一个autorunsubscriptionsReady()来等待该订阅:

Template.App_rootRedirector.onCreated(() => {
  // 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());
    }
  });
});

在用户动作之后进行重定向

经常你只是需要通过编程的方式到达一个新的路由,当某个用户已经完成了某个特定的动作时。上面我们看到的例子(创建一个新的list)当我们想要 乐观地 处理它 — 如,在我们从服务器获得Method成功执行的消息之前。我们可以这么做是因为我们可以有理由地相信在多数情况下Method将会成功(参见 UI/UX一章 以获得更近一步的详细情况)。

然而,如果想要等待该方法从服务器端返回,我们可以在该方法的回调中放置重定向:

Template.App_body.events({
  'click .js-new-list'() {
    Lists.methods.insert.call((err, listId) => {
      if (!err) {
        FlowRouter.go('Lists.show', { _id: listId });  
      }
    });
  }
});

你也会想要在方法工作的时候(在它们的按钮点击和重定向完成之间)显示某种指示。当然,不要忘记提供反馈当方法返回错误的时候。

高级路由

Missing pages

如果用户打了错误的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路由器,这在部署指南中进行了描述。

服务器端路由

如我们已经讨论的,流星是一个为客户端渲染应用程序的框架,但这不代表没有服务器端渲染的路由需要。

为API访问的服务器端路由

虽然流星允许你 撰写底层的链接处理器 来创建任意种类的在服务器端的API,如果你只需要对于你的Methods和 发布 创建一个REST的版本,你可以使用simple:rest 包很轻松地达到目的。参见 数据装载Methods 来获取更多的信息。

如果你需要更多的控制,则可以使用更复杂的 nimble:restivus 包来创建任意的需要的(REST API)。

服务器渲染

Blaze UI库不支持服务器端的渲染,所以如果你使用Blaze那么就没有办法在服务器端渲染页面。然而,React UI库却可以这么做。就是说,如果你使用React作为你的渲染框架,在服务器上进行渲染HTML是可行的。

虽然Flow路由器可以被用做渲染React组件就如我们用Blaze一样,撰写本文时Flow路由器支持的SSR仍然在实验中。然而,这可能是现在最好的在流星中使用SSR的方式了。