使用-Meteor-构建单页-Web-应用-全-

使用 Meteor 构建单页 Web 应用(全)

原文:zh.annas-archive.org/md5/54FF21F0AC5E9648A2B99A8900626FC1

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

感谢您购买这本书。您为前端和 JavaScript 技术的新一步做出了明智的选择。Meteor 框架不仅仅是为了简化事情而出现的另一个库。它为 Web 服务器、客户端逻辑和模板提供了一个完整的解决方案。此外,它还包含了一个完整的构建过程,这将使通过块状方式为 Web 工作变得更快。多亏了 Meteor,链接您的脚本和样式已经成为过去,因为自动构建过程会为您处理所有事情。这确实是一个很大的改变,但您很快就会喜欢上它,因为它使扩展应用程序的速度与创建新文件一样快。

Meteor 旨在创建单页应用程序,其中实时是默认值。它负责数据同步和 DOM 的更新。如果数据发生变化,您的屏幕将进行更新。这两个基本概念构成了我们作为网页开发者所做的很多工作,而 Meteor 则无需编写任何额外的代码即可实现。

在我看来,Meteor 在现代网页开发中是一个完整的游戏改变者。它将以下模式作为默认值引入:

  • 胖客户端:所有的逻辑都存在于客户端。HTML 仅在初始页面加载时发送

  • 在客户端和服务器上使用相同的 JavaScript 和 API

  • 实时:数据自动同步到所有客户端

  • 一种“无处不在的数据库”方法,允许在客户端进行数据库查询

  • 作为 Web 服务器通信默认的发布/订阅模式

一旦你使用了我所介绍的所有这些新概念,你很难回到过去那种只花费时间准备应用程序结构,而链接文件或将它们封装为 Require.js 模块,编写端点以及编写请求和发送数据上下的代码的老方法。

在阅读这本书的过程中,您将逐步介绍这些概念以及它们是如何相互连接的。我们将建立一个带有后端编辑帖子的博客。博客是一个很好的例子,因为它使用了帖子列表、每个帖子的不同路由以及一个管理界面来添加新帖子,为我们提供了全面理解 Meteor 所需的所有内容。

本书涵盖内容

第一章,Meteor 入门,描述了安装和运行 Meteor 所需的步骤,同时还详细介绍了 Meteor 项目的文件结构,特别是我们将要构建的 Meteor 项目。

第二章,构建 HTML 模板,展示了如何使用 handlebar 这样的语法构建反应式模板,以及如何在其中显示数据是多么简单。

第三章,存储数据和处理集合,涵盖了服务器和客户端的数据库使用。

第四章, 数据流控制, 介绍了 Meteor 的发布/订阅模式,该模式用于在服务器和客户端之间同步数据。

第五章, 使用路由使我们的应用具有多样性, 教我们如何设置路由,以及如何让我们的应用表现得像一个真正的网站。

第六章, 使用会话保持状态, 讨论了响应式会话对象及其使用方法。

第七章, 用户和权限, 描述了用户的创建以及登录过程是如何工作的。此时,我们将为我们的博客创建后端部分。

第八章, 使用 Allow 和 Deny 规则进行安全控制, 介绍了如何限制数据流仅对某些用户开放,以防止所有人对我们的数据库进行更改。

第九章, 高级响应性, 展示了如何构建我们自己的自定义响应式对象,该对象可以根据时间间隔重新运行一个函数。

第十章, 部署我们的应用, 介绍了如何使用 Meteor 自己的部署服务以及在自己的基础设施上部署应用。

第十一章, 构建我们自己的包, 描述了如何编写一个包并将其发布到 Atmosphere,供所有人使用。

第十二章, Meteor 中的测试, 展示了如何使用 Meteor 自带的 tinytest 包进行包测试,以及如何使用第三方工具测试 Meteor 应用程序本身。

附录, 包含 Meteor 命令列表以及 iron:router 钩子及其描述。

本书需要的软件

为了跟随章节中的示例,你需要一个文本编辑器来编写代码。我强烈推荐 Sublime Text 作为你的集成开发环境,因为它有几乎涵盖每个任务的可扩展插件。

你还需要一个现代浏览器来查看你的结果。由于许多示例使用浏览器控制台来更改数据库以及查看代码片段的结果,我推荐使用 Google Chrome。其开发者工具网络检查器拥有一个 web 开发者需要的所有工具,以便轻松地工作和服务器调试网站。

此外,你可以使用 Git 和 GitHub 来存储你每一步的成功,以及为了回到代码的先前版本。

每个章节的代码示例也将发布在 GitHub 上,地址为github.com/frozeman/book-building-single-page-web-apps-with-meteor,该仓库中的每个提交都与书中的一个章节相对应,为你提供了一种直观的方式来查看在每个步骤中添加和移除了哪些内容。

本书适合对象

这本书适合希望进入单页、实时应用新范式的 Web 开发者。你不需要成为 JavaScript 专业人士就能跟随书中的内容,但扎实的基本知识会让你发现这本书是个宝贵的伴侣。

如果你听说过 Meteor 但还没有使用过,这本书绝对适合你。它会教你所有你需要理解并成功使用 Meteor 的知识。如果你之前使用过 Meteor 但想要更深入的了解,那么最后一章将帮助你提高对自定义反应式对象和编写包的理解。目前 Meteor 社区中涉及最少的主题可能是测试,因此通过阅读最后一章,你将很容易理解如何使用自动化测试使你的应用更加健壮。

约定

在这本书中,你会发现多种用于区分不同信息类型的文本样式。以下是这些样式的几个示例及其含义解释。

文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、假 URL、用户输入和 Twitter 处理显示如下:"With Meteor, we never have to link files with the <script> tags in HTML."

一段代码如下所示:

<head>
  <title>My Meteor Blog</title>
</head>
<body>
  Hello World
</body>

当我们希望引起你对代码块中特定部分的关注时,相关行或项目以粗体显示:

<div class="footer">
  <time datetime="{{formatTime timeCreated "iso"}}">Posted {{formatTime timeCreated "fromNow"}} by {{author}}</time>
</div>

任何命令行输入或输出如下所示:

$ cd my/developer/folder
$ meteor create my-meteor-blog

新术语重要词汇以粗体显示。例如,你在屏幕上看到的、在菜单或对话框中出现的词汇,在文本中显示为这样:"However, now when we go to our browser, we will still see Hello World."

注意

警告或重要说明以这样的盒子形式出现。

提示

技巧和建议以这样的形式出现。

读者反馈

我们的读者的反馈总是受欢迎的。告诉我们你对这本书的看法——你喜欢或可能不喜欢的地方。读者反馈对我们开发您真正能从中获得最大收益的书很重要。

发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在消息主题中提到书名。

如果你需要我们出版某本书,并希望看到它,请在www.packtpub.com上的建议书名表单中给我们留言,或者发送电子邮件至<suggest@packtpub.com>

如果您在某个主题上有专业知识,并且您有兴趣撰写或为书籍做出贡献,请查看我们在www.packtpub.com/authors上的作者指南。

客户支持

既然您已经成为 Packt 书籍的自豪拥有者,我们有很多东西可以帮助您充分利用您的购买。

下载示例代码

您可以通过您在www.packtpub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便我们将文件直接通过电子邮件发送给您。

错误更正

尽管我们已经尽一切努力确保我们的内容的准确性,但错误确实会发生。如果您在我们的书中发现一个错误——也许是在文本或代码中——我们将非常感谢您能向我们报告。这样做可以节省其他读者的挫折感,并帮助我们改进本书的后续版本。如果您发现任何错误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击错误提交表单链接,并输入您错误的详细信息。一旦您的错误得到验证,您的提交将被接受,错误将被上传到我们的网站,或添加到该标题的错误部分现有的错误列表中。

要查看之前提交的错误更正,请前往www.packtpub.com/books/content/support并在搜索字段中输入书籍的名称。所需信息将在错误更正部分下出现。

盗版

互联网上的版权材料盗版是一个持续存在的问题,所有媒体都受到影响。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上以任何形式发现我们作品的非法副本,请立即提供给我们地址或网站名称,以便我们可以寻求解决方案。

请通过<copyright@packtpub.com>联系我们,并提供疑似被盗材料的链接。

我们感谢您在保护我们的作者和我们提供有价值内容的能力方面所提供的帮助。

问题

如果您在这本书的任何一个方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们会尽力解决问题。

第一章:开始使用 Meteor

欢迎来到关于 Meteor 的这本书。Meteor 是一个令人兴奋的新 JavaScript 框架,我们将很快看到如何用更少的代码实现真实且令人印象深刻的结果。

在本章中,我们将学习系统要求以及我们开始需要使用哪些额外的工具。我们将了解如何轻松地运行我们的第一个 Meteor 应用程序,以及一个 Meteor 应用程序可能的良好基本文件夹结构。我们还将了解 Meteor 的自动构建过程及其特定的文件加载方式。

我们还将了解如何使用 Meteor 官方的包管理系统添加包。在本章末尾,我们将简要查看 Meteor 的命令行工具及其一些功能。

为了总结,我们将涵盖以下主题:

  • Meteor 的全栈框架

  • Meteor 的系统要求

  • 安装 Meteor

  • 添加基本包

  • Meteor 的文件夹约定和加载顺序

  • Meteor 的命令行工具

Meteor 的全栈框架

Meteor 不仅仅是一个像 jQuery 或 AngularJS 这样的 JavaScript 库。它是一个包含前端库、基于 Node.js 的服务器和命令行工具的全栈解决方案。所有这些加在一起让我们可以用 JavaScript 编写大规模的网络应用程序,无论是在服务器端还是客户端,都可以使用一致的 API。

尽管 Meteor 还相当年轻,但已经有几家公司,如lookback.iorespond.lymadeye.io,在其生产环境中使用 Meteor。

如果你想亲自看看用 Meteor 制作的东西,请查看madewith.meteor.com

Meteor 使我们能够快速构建网络应用程序,并处理诸如文件链接、文件压缩和文件合并等无聊的过程。

以下是在 Meteor 下可以实现的一些亮点:

  • 我们可以使用模板来构建复杂的网络应用程序,这些模板在数据更改时会自动更新,从而大大提高速度。

  • 在我们应用程序运行的同时,我们可以将新代码推送到所有客户端。

  • Meteor 的核心包带有一个完整的账户解决方案,允许与 Facebook、Twitter 等无缝集成。

  • 数据将自动在客户端之间同步,几乎实时地保持每个客户端在相同的状态。

  • 延迟补偿将使我们的界面在服务器响应后台进行时看起来超级快速。

使用 Meteor 时,我们永远不需要在 HTML 的<script>标签中链接文件。Meteor 的命令行工具会自动收集我们应用程序文件夹中的 JavaScript 或 CSS 文件,并在初始页面加载时将它们链接到index.html文件中。这使得将我们的代码结构化到单独的文件中变得像创建它们一样简单。

Meteor 的命令行工具还会监控我们应用程序文件夹内的所有文件,如有更改,就会在文件更改时实时重建它们。

此外,它还会启动一个 Meteor 服务器,为客户端提供应用文件。当文件发生变化时,Meteor 会重新加载每个客户端的网站,同时保留其状态。这被称为热代码重载

在生产环境中,构建过程还会对我们的 CSS 和 JavaScript 文件进行合并和压缩。

仅仅通过添加lesscoffee核心包,我们甚至可以不费吹灰之力地用 LESS 写所有样式和用 CoffeeScript 写代码。

命令行工具也是用于部署和捆绑我们的应用的工具,这样我们就可以在远程服务器上运行它。

听起来很棒吗?让我们看看使用 Meteor 需要什么。

Meteor 的要求

Meteor 不仅仅是 JavaScript 框架和服务器。正如我们之前所看到的,它也是一个命令行工具,为我们整个构建过程做好准备。

目前,官方支持的操作系统如下:

本书和所有示例都使用Meteor 1.0

使用 Chrome 的开发者工具

我们还需要安装了 Firebug 插件的 Google Chrome 或 Firefox 来跟随需要控制台的示例。本书中的示例、屏幕截图和解释将使用 Google Chrome 的开发者工具。

使用 Git 和 GitHub

我强烈推荐在使用我们将在本书中工作的网页项目时使用GitHub。Git 和 GitHub 帮助我们备份我们的进度,并让我们总能回到之前的阶段,同时看到我们的更改。

Git 是一个版本控制系统,由 Linux 的发明者、Linus Torvalds 于 2005 年创建。

使用 Git,我们可以提交我们代码的任何状态,并稍后回到那个确切的状态。它还允许多个开发者在同一代码库上工作,并通过自动化过程合并他们的结果。如果在合并过程中出现冲突,合并开发者可以通过删除不需要的代码行来解决这些合并冲突

我还建议在github.com注册一个账户,这是浏览我们代码历史的最简单方式。他们有一个易于使用的界面,以及一个很棒的 Windows 和 Mac 应用。

要跟随本书中的代码示例,你可以从本书的网页www.packtpub.com/books/content/support/17713下载每个章节的全部代码示例。

此外,您将能够从github.com/frozeman/book-building-single-page-web-apps-with-meteor克隆本书的代码。这个仓库中的每个标签等于书中的一个章节,提交历史将帮助您查看每个章节所做的更改。

安装 Meteor

安装 Meteor 就像在终端中运行以下命令一样简单:

$ curl https://install.meteor.com/ | sh

就这样!这将安装 Meteor 命令行工具($ meteor),Meteor 服务器,MongoDB 数据库和 Meteor 核心包(库)。

注意

所有命令行示例都在 Mac OS X 上运行和测试,可能会在 Linux 或 Windows 系统上有所不同。

安装 Git

要安装 Git,我建议从mac.github.comwindows.github.com安装 GitHub 应用程序。然后我们只需进入应用程序,点击首选项,并在高级选项卡内点击安装命令行工具按钮。

如果我们想手动安装 Git 并通过命令行进行设置,我们可以从git-scm.com下载 Git 安装程序,并遵循help.github.com/articles/set-up-git这个很好的指南。

现在,我们可以通过打开终端并运行以下命令来检查一切是否成功安装:

$ git

提示

下载示例代码

您可以从您在www.packtpub.com账户中购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

这将返回 Git 选项的列表。如果我们得到command not found: git,我们需要检查git二进制文件是否已正确添加到我们的PATH环境变量中。

如果一切顺利,我们就可以准备创建我们的第一个 Meteor 应用了。

创建我们的第一个应用

为了创建我们的第一个应用程序,我们打开终端,前往我们希望创建新项目的文件夹,并输入以下命令:

$ cd my/developer/folder
$ meteor create my-meteor-blog

Meteor 现在将创建一个名为my-meteor-blog的文件夹。Meteor 为我们在这个文件夹内创建的 HTML、CSS 和 JavaScript 文件已经是一个完整的 Meteor 应用程序。为了看到它的实际效果,运行以下命令:

$ cd my-meteor-blog
$ meteor

Meteor 现在将在端口3000上为我们启动一个本地服务器。现在,我们可以打开我们的网页浏览器,导航到http://localhost:3000。我们将看到应用程序正在运行。

这个应用程序除了显示一个简单的反应式示例外,没有什么作用。如果你点击点击我按钮,它会增加计数器:

创建我们的第一个应用

对于后面的示例,我们将需要 Google Chrome 的开发者工具。要打开控制台,我们可以在 Mac OS X 上按Alt + command + I,或者在 Chrome 的右上角点击菜单按钮,选择更多工具,然后选择开发者工具

开发者工具允许我们查看我们网站的 DOM 和 CSS,以及有一个控制台,我们可以在其中与我们的网站的 JavaScript 进行交互。

创建一个好的文件夹结构

对于这本书,我们将从头开始构建自己的应用程序。这也意味着我们必须建立一个可持续的文件夹结构,这有助于我们保持代码的整洁。

在使用 Meteor 时,我们对文件夹结构非常灵活。这意味着我们可以把我们的文件放在任何我们想要的地方,只要它们在应用程序的文件夹内。Meteor 以不同的方式处理特定的文件夹,允许我们只在外部客户端、服务器或两者上都暴露文件。我们稍后会看看这些特定的文件夹。

但是,首先让我们通过删除我们新创建的应用程序文件夹中所有的预添加文件,并创建以下的文件夹结构:

- my-meteor-blog
  - server
  - client
    - styles
    - templates

预添加样式文件

为了能完全专注于 Meteor 代码但仍然拥有一个漂亮的博客,我强烈建议从书籍的网页上下载本章伴随的代码,网址为packtpub.com/books/content/support/17713。它们将包含两个已经可以替换的样式文件(lesshat.import.lessstyles.less),这将使你在接下来的章节中的示例博客看起来很漂亮。

你也可以直接从 GitHub 下载这些文件,网址为github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter1/my-meteor-blog/client/styles,然后手动将它们复制到my-meteor-blog/client/styles文件夹中。

接下来,我们需要添加一些基本包,这样我们就可以开始构建我们的应用程序了。

添加基本包

Meteor 中的包是可以在我们的项目中添加的库。Meteor 包的好处是它们是开箱即用的自包含单元。它们主要提供一些模板功能,或者在项目的全局命名空间中提供额外的对象。

包还可以为 Meteor 的构建过程添加功能,比如stylus包,它让我们可以使用stylus预处理器语法来编写我们应用程序的样式文件。

对于我们的博客,我们首先需要两个包:

less:这是一个 Meteor 核心包,它将我们的样式文件实时编译成 CSS。

jeeeyul:moment-with-langs:这是一个用于日期解析和格式化的第三方库。

添加一个核心包

要添加less包,我们只需打开终端,前往我们的项目文件夹,并输入以下命令:

$ meteor add less

现在,我们可以在我们的项目中使用任何*.less文件,Meteor 将在其构建过程中自动将它们编译为我们。

添加第三方包

要添加第三方包,我们可以在atmospherejs.com上搜索包,这是 Meteor 打包系统的前端,或者使用命令行工具$ meteor search <package name>

对于我们的博客,我们将需要jeeeyul:moment-with-langs包,它允许我们稍后简单地操作和格式化日期。

包使用作者名加上冒号进行命名空间。

要添加moment包,我们只需输入以下命令:

$ meteor add jeeeyul:moment-with-langs

进程完成后,我们使用$ meteor重新启动应用程序,我们将在应用程序的全局命名空间中拥有moment对象,我们可以在接下来的章节中使用它。

如果我们想要添加某个包的特定版本,我们可以使用以下命令:

$ meteor add jeeeyul:moment-with-langs@=2.8.2

如果您想要 1.0.0 范围内的版本(而不是 2.0.0),请使用以下命令:

$ meteor add jeeeyul:moment-with-langs@1.0.0

要仅更新包,我们可以简单地运行以下命令:

$ meteor update –-packages-only

此外,我们可以使用以下命令仅更新特定的包:

$ meteor update jeeeyul:moment-with-langs

就是这样!现在我们完全准备好开始创建我们的第一个模板。您可以直接进入下一章,但请确保您回来阅读,因为我们将详细讨论 Meteor 的构建过程。

变量作用域

为了理解 Meteor 的构建过程及其文件夹约定,我们需要快速了解一下变量作用域。

Meteor 在提供代码之前,将每个代码文件包裹在匿名函数中。因此,使用var关键字声明的变量将仅在该文件的作用域内可用,这意味着这些变量无法被您应用程序中的其他任何文件访问。然而,当我们不使用这个关键字声明一个变量时,我们将其变成了一个全局可用的变量,这意味着它可以从我们应用程序中的任何文件访问。为了理解这一点,我们可以看一下以下示例:

// The following files content
var myLocalVariable = 'test';
myGlobalVariable = 'test';

在 Meteor 的构建过程之后,前面的代码行将如下所示:

(function(){
  var myLocalVariable = 'test';
  myGlobalVariable = 'test';
})();

这样,使用var创建的变量是匿名函数的局部变量,而另一个变量可以全局访问,因为它可能是在此之前在其他地方创建的。

Meteor 的文件夹约定和加载顺序

虽然 Meteor 没有对我们的文件夹名称或结构施加限制,但是有一些命名约定可以帮助 Meteor 的构建过程确定文件需要加载的顺序。

以下表格描述了文件夹及其特定的加载顺序:

文件夹名称 加载行为
client 此文件仅在客户端加载。
client/compatibility 此文件不会被包裹在匿名函数中。这是为使用var声明顶级变量的库设计的。此外,这个文件夹中的文件将在客户端上的其他文件之前加载。
server 此文件夹中的文件仅在服务器上提供。
public 这个文件夹可以包含在客户端上使用的资产,例如图片、favicon.icorobots.txt。公共文件夹内的文件夹和文件可以从根目录 / 在客户端上直接访问。
private 这个文件夹可以包含只有服务器上可用的资产。这些文件可以通过 Assets API 访问。
lib lib 文件夹内的文件和子文件夹将在其他文件之前加载,其中更深层次的 lib 文件夹将在其父文件夹的 lib 文件夹之前加载。
tests 此文件夹内的文件将完全不被 Meteor 触摸或加载。
packages 当我们想要使用本地包时,我们可以将它们添加到这个文件夹中,Meteor 将使用这些包,即使有一个与之一样的名字存在于 Meteor 的官方包系统中。(然而,我们仍然需要使用 $ meteor add .... 添加包)

下面的表格描述了创建特定加载顺序的文件名:

文件名 加载行为
main.* 具有此名称的文件最后加载,而更深层次的文件夹则在它们的父文件夹的文件之前加载
*.* 表中提到的前面文件夹之外的文件将在客户端和服务器上一起加载

因此,我们看到 Meteor 收集了所有文件,除了 publicprivatetests 中的文件。

此外,文件总是按照字母顺序加载,子文件夹中的文件会在父文件夹中的文件之前加载。

如果我们有位于 clientserver 文件夹之外的文件,并希望确定代码应该在哪里执行,我们可以使用以下变量:

if(Meteor.isClient) {
  // Some code executed on the client
}

if(Meteor.isServer) {
  // Some code executed on the server. 
}

我们还看到,main.* 文件中的代码是最后加载的。为了确保特定代码只在所有文件加载完毕且客户端的 DOM 准备就绪后加载,我们可以使用 Meteor 的 startup() 函数:

Meteor.startup(function(){
  /*
  This code runs on the client when the DOM is ready,
  and on the server when the server process is finished starting.
  */
});

服务器上加载资产

要从服务器上的 private 文件夹加载文件,我们可以如下使用 Assets API:

Assets.getText(assetPath, [asyncCallback]);
// or
Assets.getBinary(assetPath, [asyncCallback])

在这里,assetPath 是相对于 private 文件夹的文件路径,例如,'subfolder/data.txt'

如果我们提供一个回调函数作为第二个参数,Assets() 方法将异步运行。因此,我们有两种获取资产文件内容的方法:

// Synchronously
var myData = Assets.getText('data.txt');

// Or asynchronously
Assets.getText('data.txt', function(error, result){
  // Do somthing with the result.
  // If the error parameter is not NULL, something went wrong
});

注意

如果第一个例子返回一个错误,我们当前的服务器代码将会失败。在第二个例子中,我们的代码仍然可以工作,因为错误包含在 error 参数中。

既然我们已经了解了 Meteor 的基本文件夹结构,那么现在让我们简要地看看 Meteor 的命令行工具。

Meteor 的命令行工具

既然我们已经了解了 Meteor 的构建过程和文件夹结构,我们将更详细地看看 Meteor 提供命令行工具能做什么。

正如我们在使用 meteor 命令时所见,我们需要在 Meteor 项目中才能执行所有操作。例如,当我们运行 meteor add xxx,我们就会向当前所在的项目中添加一个包。

更新 Meteor

如果 Meteor 发布了一个新版本,我们可以通过运行以下命令简单地更新我们的项目:

$ meteor update

如果我们想要回到之前的版本,我们可以通过运行以下命令来实现:

$ meteor update –-release 0.9.1

这将使我们的项目回退到发布版本 0.9.1。

部署 Meteor

将我们的 Meteor 应用程序部署到公共服务器,只需运行以下命令即可:

$ meteor deploy my-app-name

这将要求我们注册一个 Meteor 开发者账户,并在部署我们的应用程序。

要了解如何部署一个 Meteor 应用程序的完整介绍,请参考第十章,部署我们的应用程序

在附录中,你可以找到 Meteor 命令及其解释的完整列表。

总结

在本章中,我们学习了 Meteor 运行所需要的内容、如何创建一个 Meteor 应用程序,以及构建过程是如何工作的。

我们知道 Meteor 的文件结构相当灵活,但有一些特殊的文件夹,如clientserverlib文件夹,它们在不同的位置和顺序被加载。我们还了解了如何添加包以及如何使用 Meteor 命令行工具。

如果你想更深入地了解我们迄今为止学到的内容,请查看 Meteor 文档的以下部分:

你可以在找到本章的代码示例,或者在 GitHub 上找到

现在我们已经设置了我们项目的基本文件夹结构,我们准备开始 Meteor 的有趣部分——模板。

第二章: 构建 HTML 模板

在我们成功安装 Meteor 并设置好我们的文件夹结构之后,我们现在可以开始为我们的博客构建基本模板了。

在本章中,我们将学习如何构建模板。我们将了解如何显示数据以及如何使用助手函数更改某些部分。我们将查看如何添加事件、使用条件以及理解数据上下文,都在模板中。

以下是对本章将涵盖内容的概述:

在 Meteor 中编写模板

通常当我们构建网站时,我们在服务器端构建完整的 HTML。这很简单;每个页面都是在服务器上构建的,然后发送到客户端,最后 JavaScript 添加了一些额外的动画或动态行为。

这在单页应用中不是这样,因为在单页应用中,每个页面都需要已经存在于客户端浏览器中,以便可以随时显示。Meteor 通过提供存在于 JavaScript 中的模板来解决这个问题,可以在某个时刻将它们放置在 DOM 中。这些模板可以包含嵌套模板,使得轻松重用和结构化应用的 HTML 布局变得容易。

由于 Meteor 在文件和文件夹结构方面非常灵活,任何*.html页面都可以包含一个模板,并在 Meteor 的构建过程中进行解析。这允许我们将所有模板放在我们在第第一章Meteor 入门中创建的my-meteor-blog/client/templates文件夹中,这种文件夹结构的选择是因为它帮助我们组织模板,当应用增长时。

Meteor 的模板引擎称为Spacebars,它是 handlebars 模板引擎的派生。Spacebars 建立在Blaze之上,后者是 Meteor 的响应式 DOM 更新引擎。

注意

Blaze 可以使用其 API 直接生成反应式 HTML,尽管使用 Meteor 的 Spacebars 或建立在 Blaze 之上的第三方模板语言(如为 Meteor 设计的 Jade)更为方便。

有关 Blaze 的更多详细信息,请访问docs.meteor.com/#/full/blazegithub.com/mquandalle/meteor-jade

使 Spacebars 如此激动人心的是它的简单性和反应性。反应式模板意味着模板的某些部分可以在底层数据变化时自动更改。无需手动操作 DOM,不一致的界面已成为过去。为了更好地了解 Meteor,我们将从为我们的应用创建的基本 HTML 文件开始:

  1. 让我们在我们my-meteor-blog/client文件夹中创建一个index.html文件,并输入以下代码行:

    <head>
      <title>My Meteor Blog</title>
    </head>
    <body>
      Hello World
    </body>
    

    注意

    请注意,我们的index.html文件不包含<html>...</html>标签,因为 Meteor 会收集任何文件中的<head><body>标签,并构建自己的index.html文件,该文件将交付给用户。实际上,我们还可以将此文件命名为myapp.html

  2. 接下来,我们通过在命令行中输入以下命令来运行我们的 Meteor 应用:

    $ cd my-meteor-blog
    $ meteor
    
    

    这将启动一个带有我们应用的 Meteor 服务器。

  3. 就这样!现在我们可以打开浏览器,导航到http://localhost:3000,我们应该能看到Hello World

这里发生的是,Meteor 将查看我们应用文件夹中可用的所有 HTML 文件,合并所有找到的<head><body>标签的内容,并将其作为索引文件提供给客户端。

如果我们查看我们应用的源代码,我们会看到<body>标签是空的。这是因为 Meteor 将<body>标签的内容视为自己的模板,在 DOM 加载时,将与相应的 JavaScript 模板一起注入。

注意

要查看源代码,不要使用开发者工具的元素面板,因为这将显示 JavaScript 执行后的源代码。在 Chrome 中,右键单击网站,而选择查看页面源代码

我们还会看到 Meteor 已经在我们的<head>标签中链接了各种各样的 JavaScript 文件。这些都是 Meteor 的核心包和我们的第三方包。在生产环境中,这些文件将被合并成一体。为了看到这个效果,打开终端,使用Ctrl + C退出我们运行中的 Meteor 服务器,并运行以下命令:

$ meteor --production

如果我们现在查看源代码,我们会看到只有一个神秘的 JavaScript 文件被链接。

接下来,最好是通过简单地退出 Meteor 并再次运行meteor命令回到我们的开发者模式,因为这样在文件发生变化时可以更快地重新加载应用。

构建基本模板

现在,让我们通过在我们my-meteor-blog/client/templates文件夹中创建一个名为layout.html的文件,将基本模板添加到我们的博客中。这个模板将作为我们博客布局的包装模板。要构建基本模板,请执行以下步骤:

  1. 在刚刚创建的layout.html中添加以下代码行:

    <template name="layout">
      <header>
        <div class="container">
          <h1>My Meteor Single Page App</h1>
          <ul>
            <li>
              <a href="/">Home</a>
            </li>
            <li>
              <a href="/about">About</a>
            </li>
          </ul>
        </div>
      </header>
    
      <div class="container">
        <main>
        </main>
      </div>
    </template>
    
  2. 接下来,我们将创建主页模板,稍后列出我们所有的博客文章。在layout.html相同的模板文件夹中,我们将创建一个名为home.html的文件,并包含以下代码行:

    <template name="home">
    {{#markdown}}
    ## Welcome to my Blog
    Here I'm talking about my latest discoveries from the world of JavaScript.
    {{/markdown}}
    </template>
    
  3. 下一个文件将是一个简单的关于页面,我们将其保存为about.html,并包含以下代码片段:

    <template name="about">
    {{#markdown}}
    ## About me
    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
    tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
    quis nostrud **exercitation ullamco** laboris nisi ut aliquip ex ea commodo
    consequat.
    
    Link to my facebook: [facebook.com][1]
    
    [1]: http://facebook.com
    {{/markdown}}
    </template>
    

    正如您所见,我们使用了一个{{#markdown}}块助手来包装我们的文本。大括号是 Blaze 用来将逻辑带到 HTML 的处理程序语法。{{#markdown}}...{{/markdown}}块在模板渲染时将所有的 Markdown 语法转换成 HTML。

    注意

    由于 Markdown 语法将缩进解释为代码,因此 Markdown 文本不能像我们对 HTML 标签那样进行缩进。

  4. 为了能够使用{{#markdown}}块助手,我们首先需要将markdown核心包添加到我们的应用程序中。为此,我们使用Ctrl + C在终端中停止正在运行的应用程序,并输入以下命令:

    $ meteor add markdown
    
    
  5. 现在我们可以再次运行meteor命令来启动我们的服务器。

然而,当我们现在打开浏览器时,我们仍然会看到Hello World。那么我们如何使我们的模板现在变得可见呢?

添加模板和部分

为了在应用程序中显示主页模板,我们需要打开之前创建的index.html,并执行以下步骤:

  1. 我们将Hello World替换为以下模板包含助手:

    {{> layout}}
    
  2. 如果我们现在回到浏览器,我们会看到文本消失了,而我们之前创建的layout模板以及其标题和菜单出现了。

  3. 为了完成页面,我们需要在layout模板中显示home模板。我们只需在layout模板的main部分添加另一个模板包含助手,如下所示:

    <main>
      {{> home}}
    </main>
    
  4. 如果我们回到浏览器,我们应该看到以下截图:Adding templates and partials

如果我们现在将{{> home}}替换为{{> about}},我们将会看到我们的about模板。

使用模板助手显示数据

每个模板都可以有函数,这些函数被称为template助手,它们可以在模板及其子模板中使用。

除了我们自定义的助手函数外,还有三个回调函数在模板创建、渲染和销毁时被调用。要使用模板助手显示数据,请执行以下步骤:

  1. 为了看到这三个回调函数的作用,让我们创建一个名为home.js的文件,并将其保存到我们的my-meteor-blog/client/templates/文件夹中,并包含以下代码片段:

    Template.home.created = function(){
      console.log('Created the home template');
    };
    Template.home.rendered = function(){
      console.log('Rendered the home template');
    };
    
    Template.home.destroyed = function(){
      console.log('Destroyed the home template');
    };
    

    如果我们现在打开浏览器的控制台,我们会看到前两个回调被触发。最后一个只有在动态移除模板时才会触发。

  2. 为了在home模板中显示数据,我们将创建一个助手函数,该函数将返回一个简单的字符串,如下所示:

    Template.home.helpers({
      exampleHelper: function(){
        return 'This text came from a helper with some <strong>HTML</strong>.';
      }
    });
    
  3. 现在如果我们去我们的home.html文件,在{{markdown}}块助手之后添加{{exampleHelper}}助手,并保存文件,我们将在浏览器中看到出现的字符串,但我们注意到 HTML 被转义了。

  4. 为了使 Meteor 正确渲染 HTML,我们可以简单地将双花括号替换为三花括号,如下代码行所示,Blaze 不会让 HTML 转义:

    {{{exampleHelper}}}
    

    注意

    注意,在我们的大多数模板助手中,我们不应该使用三花括号{{{...}}},因为这将打开 XSS 和其他攻击的大门。只有当返回的 HTML 安全可渲染时才使用它。

  5. 此外,我们可以使用双花括号返回未转义的 HTML,但我们需要返回通过SpaceBars.SafeString函数传递的字符串,如下例所示:

    Template.home.helpers({
      exampleHelper: function(){
        return new Spacebars.SafeString('This text came from a helper with some <strong>HTML</strong>.');
      }
    });
    

为模板设置数据上下文

  • 现在我们已经有了contextExample模板,我们可以通过传递一些数据将其添加到我们的home模板中,如下所示:

    {{> contextExample someText="I was set in the parent template's helper, as an argument."}}
    

    这将在contextExample模板中显示文本,因为我们使用{{someText}}来显示它。

    提示

    记住,文件名实际上并不重要,因为 Meteor 会无论如何收集并连接它们;然而,模板名称很重要,因为我们用这个来引用模板。

    在 HTML 中设置上下文不是非常动态,因为它是有硬编码的。为了能够动态地改变上下文,最好使用template助手函数来设置它。

    • 为此,我们必须首先将助手添加到我们的home模板助手中,该助手返回数据上下文,如下所示:
    Template.home.helpers({
      // other helpers ...
      dataContextHelper: function(){
        return {
          someText: 'This text was set using a helper of the parent template.',
          someNested: {
            text: 'That comes from "someNested.text"'
          }
        };
      }
    });
    
    • 现在我们可以将此助手作为数据上下文添加到我们的contextExample模板包含助手中,如下所示:
    {{> contextExample dataContextHelper}}
    
    • 另外,为了显示我们返回的嵌套数据对象,我们可以在contextExample模板中使用 Blaze 点语法,通过在模板中添加以下代码行来实现:
    <p>{{someNested.text}}</p>
    

这现在将显示someTextsomeNested.text,后者是由我们的助手函数返回的。

使用{{#with}}块助手

设置数据上下文的一种另一种方法是使用{{#with}}块助手。以下代码片段与之前使用助手函数的包含助手具有相同的结果:

{{#with dataContextHelper}}
  {{> contextExample}}
{{/with}}

我们甚至在浏览器中得到同样的结果,当我们不使用子模板,只是将contextExample模板的内容添加到{{#with}}块助手中,如下所示:

{{#with dataContextHelper}}
  <p>{{someText}}</p>
  <p>{{someNested.text}}</p>
{{/with}}

模板助手和模板回调中的"this"

在 Meteor 中,模板助手中的this在模板回调(如created()rendered()destroyed())中的使用方式不同。

如前所述,模板有三个回调函数,在模板的不同状态下触发:

  • created:当模板初始化但尚未插入 DOM 时触发

  • rendered:当模板及其所有子模板附加到 DOM 时触发

  • destroyed:当模板从 DOM 中移除并在模板实例被销毁之前触发

在这些回调函数中,this 指的是当前模板实例。实例对象可以访问模板的 DOM 并带有以下方法:

  • this.$(selectorString):这个方法找到所有匹配 selectorString 的元素,并返回这些元素的 jQuery 对象。

  • this.findAll(selectorString):这个方法找到所有匹配 selectorString 的元素,但返回普通的 DOM 元素。

  • this.find(selectorString):这个方法找到匹配 selectorString 的第一个元素,并返回一个普通的 DOM 元素。

  • this.firstNode:这个对象包含模板中的第一个元素。

  • this.lastNode:这个对象包含模板中的最后一个元素。

  • this.data:这个对象包含模板的数据上下文

  • this.autorun(runFunc):一个在模板实例被销毁时停止的反应式 Tracker.autorun() 函数。

  • this.view:这个对象包含这个模板的 Blaze.View 实例。Blaze.View 是反应式模板的构建块。

在辅助函数内部,this 仅指向当前的数据上下文。

为了使这些不同的行为变得可见,我们将查看一些示例:

  • 当我们想要访问模板的 DOM 时,我们必须在渲染回调中进行,因为只有在这一点上,模板元素才会出现在 DOM 中。为了看到它的工作原理,我们按照以下方式编辑我们的 home.js 文件:

    Template.home.rendered = function(){
      console.log('Rendered the home template');
    
     this.$('p').html('We just replaced that text!');
    };
    

    这将用我们设置的字符串替换由 {{#markdown}} 块辅助函数创建的第一个 <p> 标签。现在当我们检查浏览器时,我们会发现包含我们博客介绍文本的第一个 <p> 标签已经被替换。

  • 对于下一个示例,我们需要为我们的 contextExample 模板创建一个额外的模板 JavaScript 文件。为此,我们在 templates 文件夹中创建一个名为 examples.js 的新文件,并使用以下代码片段保存它:

    Template.contextExample.rendered = function(){
      console.log('Rendered Context Example', this.data);
    };
    
    Template.contextExample.helpers({
      logContext: function(){
        console.log('Context Log Helper', this);
      }
    });
    

    这将把渲染回调以及一个名为 logContext 的辅助函数添加到我们的 contextExample 模板辅助函数中。为了使这个辅助函数运行,我们还需要将其添加到我们的 contextExample 模板中,如下所示:

    <p>{{logContext}}</p>
    

当我们现在回到浏览器的控制台时,我们会发现数据上下文对象已经被返回给所有我们的已渲染的 contextTemplates 模板的 rendered 回调和辅助函数。我们还可以看到辅助函数将在渲染回调之前运行。

注意

如果您需要从模板辅助函数内部访问模板的实例,您可以使用 Template.instance() 来获取它。

现在让我们使用事件使我们的模板变得交互式。

添加事件

为了使我们的模板更具动态性,我们将添加一个简单的事件,这将使之前创建的 logContext 辅助函数重新反应式地运行。

首先,然而,我们需要在我们的 contextExample 模板中添加一个按钮:

<button>Get some random number</button>

为了捕获点击事件,打开 examples.js 并添加以下 event 函数:

Template.contextExample.events({
  'click button': function(e, template){
    Session.set('randomNumber', Math.random(0,99));
  }
});

这将设置一个名为 randomNumber 的会话变量到一个随机数。

注意

在下一章中,我们将深入讨论会话。现在,我们只需要知道当会话变量发生变化时,所有使用Session.get('myVariable')获取该会话变量的函数将重新运行。

为了看到这个效果,我们将向logContext助手添加一个Session.get()调用,并像以下方式返回先前设置的随机数:

Template.contextExample.helpers({
  logContext: function(){
    console.log('Context Log Helper',this);

    return Session.get('randomNumber');
  }
});

如果我们打开浏览器,我们会看到获取一些随机数按钮。当我们点击它时,我们会看到一个随机数出现在按钮上方。

注意

当我们在我们home模板中多次使用contextTemplates模板时,我们会发现该模板助手每次都会显示相同的随机数。这是因为会话对象将重新运行其所有依赖项,其中所有依赖项都是logHelper助手的实例。

既然我们已经介绍了模板助手,那么让我们创建一个自定义的块助手。

块助手

example.html file:
<template name="blockHelperExample">
  <div>
    <h1>My Block Helper</h1>
    {{#if this}}
      <p>Content goes here: {{> Template.contentBlock}}</p>
    {{else}}
      <p>Else content here: {{> Template.elseBlock}}</p>
    {{/if}}
  </div>
</template>

{{> Template.contentBlock}}是为块内容预定义的占位符。同样适用于{{> Template.elseBlock}}

this(在这个例子中,我们使用模板的上下文作为一个简单的布尔值)为true时,它将显示给定的Template.contentBlock。否则,它将显示Template.elseBlock的内容。

为了看到我们可以如何将最近创建的模板作为块助手使用,请查看以下示例,我们可以将其添加到home模板中:

{{#blockHelperExample true}}
  <span>Some Content</span>
{{else}}
  <span>Some Warning</span>
{{/blockHelperExample}}

现在我们应该看到以下截图:

块助手

现在我们将true更改为false,我们传递给{{#blockHelperExample}},我们应该看到{{else}}之后的内容。

我们还可以使用助手函数来替换布尔值,这样我们就可以动态地切换块助手。此外,我们可以传递键值对参数,并通过它们的键在块助手模板内部访问它们,如下面的代码示例所示:

{{#blockHelperExample myValue=true}}
...
{{/blockHelperExample}}

我们还可以按照以下方式通过其名称访问给定参数:

<template name="blockHelperExample">
  <div>
    <h1>My Block Helper</h1>
    {{#if myValue}}
    ...
    {{/if}}
  </div>
</template>

注意

请注意,块内容的上下文将是出现块的模板的上下文,而不是块助手模板本身的上下文。

块助手是一种强大的工具,因为它们允许我们编写自包含组件,当打包成包时,其他可以使用它们作为即插即用的功能。这个特性有潜力允许一个充满活力的市场,就像我们在 jQuery 插件市场中看到的那样。

列出帖子

此模板将用于在主页上显示每个帖子。

  • 为了使其出现,我们需要在home模板中添加一个{{#each}}助手,如下所示:

    {{#each postsList}}
      {{> postInList}}
    {{/each}}
    

    当我们传递给{{#each}}块助手时,如果postsList助手返回一个数组,{{#each}}的内容将针对数组中的每个项目重复,将数组项目设置为数据上下文。

    • 为了看到这个效果,我们在home.js文件中添加了postsList助手,如下所示:
    Template.home.helpers({
      // other helpers ...
      postsList: function(){
        return [
          {
            title: 'My Second entry',
            description: 'Borem sodum color sit amet, consetetur sadipscing elitr.',
            author: 'Fabian Vogelsteller',
            timeCreated: moment().subtract(3, 'days').unix()
          },
          {
            title: 'My First entry',
            description: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr.',
            author: 'Fabian Vogelsteller',
            timeCreated: moment().subtract(7, 'days').unix()
          }
        ];
      }
    });
    
    • 正如我们可以看到的,我们返回一个数组,每个项目都是一个包含我们文章数据上下文的对象。对于 timeCreated,我们使用我们之前添加的第三方包的 moment 函数。这将生成过去几天的时间戳。如果我们现在去浏览器,我们会看到列出的两篇文章,如下截图所示:列出文章* 为了以正确的格式显示我们的文章项中的 timeCreated,我们需要创建一个助手函数来格式化时间戳。然而,因为我们想要在后面的其他模板中使用这个助手,我们需要让它成为一个全局助手,任何模板都可以访问。为此,我们创建一个名为 template-helpers.js 的文件,并将其保存到我们的 my-meteor-blog/client 文件夹中,因为它不属于任何特定的模板.* 为了注册一个全局助手,我们可以使用 Meteor 的 Template.registerHelper 函数:
    Template.registerHelper('formatTime', function(time, type){
      switch(type){
        case 'fromNow': 
          return moment.unix(time).fromNow();
        case 'iso':
          return moment.unix(time).toISOString();
        default:
          return moment.unix(time).format('LLLL');
      }
    });
    
    • 现在,我们只需通过用以下代码段替换 postInList 模板的底部内容来添加助手:
    <div class="footer">
      <time datetime="{{formatTime timeCreated "iso"}}">Posted {{formatTime timeCreated "fromNow"}} by {{author}}</time>
    </div>
    

现在,如果我们保存这两个文件并回到浏览器,我们会看到博客文章底部添加了一个相对日期。这之所以有效,是因为我们把时间和一个类型字符串传递给助手,如下所示:

{{formatTime timeCreated "fromNow"}}

助手然后使用一个 moment 函数返回格式化的日期。

有了这个全局助手,我们现在可以格式化任何 Unix 时间戳,在任何模板中将时间转换为相对时间、ISO 时间字符串和标准日期格式(使用 LLLL 格式,转换为 1986 年 9 月 4 日星期四晚上 8:30)。

既然我们已经使用了 {{#with}}{{#each}} 块助手,让我们来看看 Blaze 使用的其他默认助手和语法。

Spacebars 语法

来总结一下 Spacebars 的语法:

助手 描述
{{myProperty}} 模板助手可以是模板数据上下文中的属性或模板助手函数。如果存在具有相同名称的助手函数和属性,模板助手将使用助手函数。
{{> myTemplate}} 包含助手用于模板,并且总是期待一个模板对象或者 null。
{{> Template.dynamic template=templateName [data=dataContext]}} 使用 {{> Template.dynamic ...}} 助手,你可以通过提供返回模板名称的模板助手来动态渲染模板。当助手重新运行并返回不同的模板名称时,它将用新模板替换此位置的模板。
{{#myBlockHelper}}...{{/myBlockHelper}} 包含 HTML 和 Spacebars 语法的块助手。

默认情况下,Spacebars 带有以下四个默认块助手:

  • {{#if}}..{{/if}}

  • {{#unless}}..{{/unless}}

  • {{#with}}..{{/with}}

  • {{#each}}..{{/each}}

{{#if}} 块助手允许我们创建简单的条件,如下所示:

{{#if myHelperWhichReturnsABoolean}}
  <h1>Show me this</h1>
{{else}}
  <strong>If not<strong> show this.
{{/if}}

{{#unless}} 块助手的工作方式与 {{#if}} 相同,但逻辑相反。

如前所见,{{#with}}块将为其内容和包含的模板设置新的数据上下文,而{{#each}}块帮助器将多次渲染,为每次迭代设置不同的数据上下文。

访问父数据上下文

为了完成对 Spacebars 语法的探索,让我们更仔细地看看我们用来显示数据的模板帮助器语法。正如我们已经在前面看到的,我们可以使用双花括号语法显示数据,如下所示:

{{myData}}

在此帮助器内部,我们可以使用点语法访问对象属性:

{{myObject.myString}}

我们还可以使用路径样式的语法访问父数据上下文:

{{../myParentsTemplateProperty}}

此外,我们可以移动更多的上下文:

{{../../someParentProperty}}

这一特性使我们能够非常灵活地设置数据上下文。

注意

如果我们想从一个模板帮助器内部做同样的事情,我们可以使用模板 API 的Template.parentData(n),其中n是要访问父模板数据上下文所需的步骤数。

Template.parentData(0)Template.currentData()相同,或者如果我们处于模板帮助器中,则为this

向帮助器传递数据

向帮助器传递数据可以通过两种不同的方式完成。我们可以如下向帮助器传递参数:

{{myHelper "A String" aContextProperty}}

然后,我们可以在帮助器中按照以下方式访问它:

Template.myTemplate.helpers({
   myHelper: function(myString, myObject){
     // And we get:
     // myString = 'aString'
     // myObject = aContextProperty
   }
});

除了这个,我们还可以以键值的形式传递数据:

{{myHelper myString="A String" myObject=aDataProperty}}

然而,这次我们需要按照以下方式访问它们:

Template.myTemplate.helpers({
   myHelper: function(Parameters){
     // And we can access them:
     // Parameters.hash.myString = 'aString'
     // Parameters.hash.myObject = aDataProperty
   }
});

请注意,块帮助器和包含帮助器的行为不同,因为它们总是期望对象或键值作为参数:

{{> myTemplate someString="I will be available inside the template"}}

// Or

{{> myTemplate objectWithData}}

如果我们想在帮助器函数中使用它,那么我们需要对传递的参数进行类型转换,如下所示:

Template.myBlock.helpers({
   doSomethingWithTheString: function(){
     // Use String(this), to get the string
     return this;
   }
});

此外,我们还可以在我们的块帮助器模板中简单地显示字符串,使用{{Template.contentBlock}}如下所示:

<template name="myBlock">
  <h1>{{this}}</h1>
  {{Template.contentBlock}}
</template>

我们还可以将另一个模板帮助器作为参数传递给包含或块帮助器,如下例所示:

{{> myTemplate myHelperWhichReturnsAnObject "we pass a string and a number" 300}}

尽管向模板帮助器传递数据和向包含/块帮助器传递数据略有不同,但在生成帮助器时参数可以非常灵活。

总结

反应式模板是 Meteor 最令人印象深刻的功能之一,一旦我们习惯了它们,我们可能就不会再回到手动操作 DOM 了。

阅读这一章之后,我们应该知道如何在 Meteor 中编写和使用模板。我们还应该理解其基本语法以及如何添加模板。

我们看到了如何在模板中访问和设置数据,以及如何使用帮助器。我们学习了不同类型的帮助器,例如包含帮助器和块帮助器。我们还构建了我们自己的自定义块帮助器并使用了 Meteor 的默认帮助器。

我们了解到模板有三种不同的回调,分别用于模板创建、渲染和销毁时。

我们学习了如何向帮助器传递数据,以及这在普通帮助器和块帮助器之间的区别。

为了深入了解,请查看以下文档:

你可以在这个章节找到代码示例,网址为www.packtpub.com/books/content/support/17713,或者在 GitHub 上查看github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter2

关于模板的新知识让我们准备好向我们的数据库添加数据,并看看我们如何在主页上显示它。

第三章:存储数据和处理集合

在上一章中,我们学习了如何构建模板并在其中显示数据。我们建立了我们应用程序的基本布局并在首页列出了一些后续示例。

在本章中,我们将持续向服务器上的数据库添加后续示例。我们将学习如何稍后在客户端访问这些数据,以及 Meteor 如何在客户端和服务器之间同步数据。

在本章中,我们将涵盖以下主题:

Meteor 和数据库

Meteor 目前默认使用 MongoDB 在服务器上存储数据,尽管还计划有用于关系型数据库的驱动程序。

注意

如果你有冒险精神,可以尝试一下社区构建的 SQL 驱动程序,例如来自 atmospherejs.com/numtel/mysqlnumtel:mysql 包。

MongoDB 是一个NoSQL 数据库。这意味着它基于平面文档结构,而不是关系表结构。它对文档的处理方式使它成为 JavaScript 的理想选择,因为文档是用 BJSON 编写的,这与 JSON 格式非常相似。

Meteor 采用了一种无处不在的数据库的方法,这意味着我们有一个相同的 API 来在客户端和服务器上查询数据库。然而,当我们在客户端查询数据库时,我们只能访问我们发布给客户端的数据。

MongoDB 使用一种称为集合的数据结构,这在 SQL 数据库中相当于一个表。集合包含文档,每个文档都有自己的唯一 ID。这些文档是类似 JSON 的结构,可以包含具有值的属性,甚至是多维属性,如下所示:

{
  "_id": "W7sBzpBbov48rR7jW",
  "myName": "My Document Name",
  "someProperty": 123456,
  "aNestedProperty": {
    "anotherOne": "With another string"
  }
}

这些集合用于在服务器上的 MongoDB 以及客户端的minimongo集合中存储数据,后者是一个模仿真实 MongoDB 行为的内存数据库。

注意

我们将在本章末尾更多地讨论minimongo

MongoDB API 允许我们使用简单的基于 JSON 的查询语言从集合中获取文档。我们可以传递其他选项,只询问特定字段对返回的文档进行排序。这些功能在客户端尤其强大,可以以各种方式显示数据。

设置集合

为了亲眼看到这一切,让我们通过创建我们的第一个集合来开始。

我们在my-meteor-blog文件夹内创建一个名为collections.js的文件。我们需要在根目录中创建它,这样它才能在客户端和服务器上都可用。现在让我们将以下代码行添加到collections.js文件中:

Posts = new Mongo.Collection('posts');

这将使Posts变量在全球范围内可用,因为我们没有使用var关键字,这会将它们限制为该文件的范围。

Mongo.Collection是查询数据库的 API,它带有以下基本方法:

  • insert:此方法用于将文档插入数据库

  • update:此方法用于更新文档或它们的部分内容

  • upsert:此方法用于插入或更新文档或它们的部分内容

  • remove:此方法用于从数据库中删除文档

  • find:此方法用于查询数据库中的文档

  • findOne:此方法用于只返回第一个匹配的文档

添加帖子示例

要查询数据库中的帖子,我们需要添加一些帖子示例。这必须在服务器上完成,因为我们希望它们持久存在。要添加一个示例帖子,请执行以下步骤:

  1. 我们在my-meteor-blog/server文件夹内创建一个名为main.js的文件。在这个文件中,我们将使用Meteor.startup()函数在服务器启动时执行代码。

  2. 我们然后添加帖子示例,但只有在集合为空时。为了防止这种情况,我们每次重启服务器时都添加它们,如下所示:

    Meteor.startup(function(){
    
      console.log('Server started');
    
      // #Storing Data -> Adding post examples
      if(Posts.find().count() === 0) {
    
        console.log('Adding dummy posts');
        var dummyPosts = [
          {
            title: 'My First entry',
            slug: 'my-first-entry',
            description: 'Lorem ipsum dolor sit amet.',
            text: 'Lorem ipsum dolor sit amet...',
            timeCreated: moment().subtract(7,'days').unix(),
            author: 'John Doe'
          },
          {
            title: 'My Second entry',
            slug: 'my-second-entry',
            description: 'Borem ipsum dolor sit.',
            text: 'Lorem ipsum dolor sit amet...',
            timeCreated: moment().subtract(5,'days').unix(),
            author: 'John Doe'
          },
          {
            title: 'My Third entry',
            slug: 'my-third-entry',
            description: 'Dorem ipsum dolor sit amet.',
            text: 'Lorem ipsum dolor sit amet...',
            timeCreated: moment().subtract(3,'days').unix(),
            author: 'John Doe'
          },
          {
            title: 'My Fourth entry',
            slug: 'my-fourth-entry',
            description: 'Sorem ipsum dolor sit amet.',
            text: 'Lorem ipsum dolor sit amet...',
            timeCreated: moment().subtract(2,'days').unix(),
            author: 'John Doe'
          },
          {
            title: 'My Fifth entry',
            slug: 'my-fifth-entry',
            description: 'Korem ipsum dolor sit amet.',
            text: 'Lorem ipsum dolor sit amet...',
            timeCreated: moment().subtract(1,'days').unix(),
            author: 'John Doe'
          }
        ];
        // we add the dummyPosts to our database
        _.each(dummyPosts, function(post){
          Posts.insert(post);
        });
      }
    });
    

现在,当我们检查终端时,我们应该看到与以下屏幕截图类似的某些内容:

添加帖子示例

注意

我们还可以使用 Mongo 控制台添加虚拟数据,而不是在代码中编写它们。

要使用 Mongo 控制台,我们首先使用$ meteor启动 Meteor 服务器,然后在第二个终端运行$ meteor mongo,这将我们带到 Mongo shell。

在这里,我们可以简单地使用 MongoDB 的语法添加文档:

db.posts.insert({title: 'My First entry',
 slug: 'my-first-entry',
 description: 'Lorem ipsum dolor sit amet.',
 text: 'Lorem ipsum dolor sit amet...',
 timeCreated: 1405065868,
 author: 'John Doe'
}
)

查询集合

当我们保存我们的更改时,服务器确实重新启动了。在此阶段,Meteor 在我们的数据库中添加了五个帖子示例。

注意

如果服务器没有重新启动,这意味着我们在代码中的某个地方犯了语法错误。当我们手动重新加载浏览器或检查终端时,我们会看到 Meteor 给出的错误,然后我们可以进行修复。

如果我们数据库中出了什么问题,我们总是可以使用终端中的$ meteor reset命令来重置它。

我们只需在浏览器中打开控制台并输入以下命令即可查看这些帖子:

Posts.find().fetch();

这将返回一个包含五个项目的数组,每个项目都是我们的示例帖子之一。

为了在我们前端页面上列出这些新插入的帖子,我们需要在 home.js 文件中替换我们 postsList 帮助器的內容,如下面的代码行所示:

Template.home.helpers({
  postsList: function(){
    return Posts.find({}, {sort: {timeCreated: -1}});
  }
});

正如我们所看到的,我们直接在帮助器中返回了集合游标。这个返回值然后传递到我们的 home 模板中的 {{#each}} 块帮助器,该帮助器将在渲染 postInList 模板时遍历每个帖子。

注意

请注意,Posts.find() 返回一个游标,在 {{#each}} 块帮助器中使用时效率更高,而 Posts.find().fetch() 将返回一个包含文档对象的数组。使用 fetch(),我们可以在返回之前操纵文档。

我们将一个选项对象作为 find() 函数的第二个参数。我们传递的选项将根据 timeCreated 进行排序,并使用 -1-1 的值意味着它将按降序排序(1 表示升序)。

现在,当我们查看我们的浏览器时,我们会看到我们的五篇帖子全部列出,如下面的截图所示:

查询集合

更新集合

现在我们已经知道如何插入和获取数据,让我们来看看如何在我们的数据库中更新数据。

正如我们之前所见,我们可以使用浏览器的光标来玩转数据库。对于我们接下来的例子,我们将只使用控制台来了解当我们在数据更改时,Meteor 如何反应性地改变模板。

为了能够在我们的数据库中编辑一篇帖子,我们首先需要知道其条目的 _id 字段。为了找出这个,我们需要输入以下命令:

Posts.find().fetch();

这将返回 Posts 集合中的所有文档,因为我们没有传递任何特定的查询对象。

在返回的数组中,我们需要查看最后一个项目,标题为 My Fifth entry 的项目,并使用 Cmd + C(或者如果我们在 Windows 或 Linux 上,使用 Ctrl + C)将 _id 字段复制到剪贴板。

注意

我们也可以简单地使用 Posts.findOne(),这将给我们找到的第一个文档。

现在我们已经有了 _id,我们可以通过输入以下命令简单地更新我们第五篇帖子的标题:

Posts.update('theCopied_Id', {$set: {title: 'Wow the title changed!'}});

一旦我们执行这个命令,我们就会注意到第五篇帖子的标题已经变成了我们新的标题,如果我们现在重新加载页面,我们会看到标题保持不变。这意味着更改已经持久地保存到了数据库中。

为了看到 Meteor 的响应性跨客户端,打开另一个浏览器窗口,导航到 http://localhost:3000。现在我们再次通过执行以下命令更改我们的标题,我们会看到所有客户端实时更新:

Posts.update('theCopied_Id', {$set: {title: 'Changed the title again'}});

数据库无处不在

在 Meteor 中,我们可以使用浏览器的控制台来更新数据,这意味着我们可以从客户端更新数据库。这之所以有效,是因为 Meteor 会自动将这些更改同步到服务器,并相应地更新数据库。

这之所以发生,是因为我们的项目默认添加了 autopublishinsecure 核心包。autopublish 包会自动将所有文档发布给每个客户端,而 insecure 包允许每个客户端通过其 _id 字段更新数据库记录。显然,这对于原型设计来说很好,但对于生产环境来说是不切实际的,因为每个客户端都可以操作我们的数据库。

如果我们移除了 insecure 包,我们将需要添加“允许和拒绝”规则来确定客户端可以更新哪些内容以及不可以更新哪些内容;否则,所有更新都将被拒绝。我们将在后面的章节中查看这些规则的设置,但现在这个包对我们很有用,因为我们可以立即操作数据库。

在下一章中,我们将了解如何手动将某些文档发布给客户端。我们将从移除 autopublish 包开始。

客户端与服务器集合之间的差异

Meteor 采用了一种无处不在的数据库方法。这意味着它为客户端和服务器端提供了相同的 API。数据流动是通过发布订阅模型来控制的。

服务器上运行着真正的 MongoDB 数据库,它负责持久化存储数据。在客户端,Meteor 包含一个名为 minimongo 的包,它是一个纯内存数据库,模仿了 MongoDB 的大部分查询和更新功能。

每次客户端连接到其 Meteor 服务器时,Meteor 都会下载客户端订阅的文档并将它们存储在其本地的 minimongo 数据库中。从这里,它们可以在模板中显示,或者由函数处理。

当客户端更新一个文档时,Meteor 会将其同步回服务器,在那里它将穿过任何允许/拒绝函数,然后被永久存储在数据库中。这也适用于反向操作;当服务器端数据库中的文档发生变化时,它将自动同步到所有订阅它的客户端,使每个连接的客户端保持最新。

概要

在本章中,我们学习了如何在 Meteor 的 MongoDB 数据库中持久化存储数据。我们还看到了如何查询集合和更新文档。我们理解了“无处不在的数据库”方法意味着什么,以及 Meteor 如何使每个客户端保持最新。

为了更深入地了解 MongoDB 以及如何查询和更新集合,请查看以下资源:

你可以在这个章节找到代码示例,网址为www.packtpub.com/books/content/support/17713,或者在 GitHub 上查看github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter3

在下一章中,我们将了解如何使用发布和订阅控制数据流,从而只将必要的文档发送给客户端。

第四章:控制数据流

在前一章节中,我们学习了如何将数据持久化地存储在我们的数据库中。在本章中,我们将了解如何告诉 Meteor 应该向客户端发送什么数据。

到目前为止,所有这些都是因为使用了autopublish包而神奇地工作的,该包将与每个客户端同步所有数据。现在,我们将手动控制这个流程,只向客户端发送必要的数据。

在本章中,我们将介绍以下主题:

数据同步 – 当前的 Web 与新的 Web

在当前的 Web 中,大多数页面要么是托管在服务器上的静态文件,要么是由服务器在请求时生成的动态页面。这对于大多数服务器端渲染的网站来说是真的,例如用 PHP、Rails 或 Django 编写的网站。这两种技术除了被客户端显示外不需要任何努力;因此,它们被称为客户端。

在现代网络应用程序中,浏览器的概念已经从薄客户端转移到客户端。这意味着网站的大部分逻辑都存在于客户端,并且客户端请求它需要的数据。

目前,这主要是通过调用 API 服务器实现的。这个 API 服务器然后返回数据,通常以 JSON 格式返回,给客户端一个轻松处理和使用数据的方式。

大多数现代网站都是薄客户端和厚客户端的混合体。普通页面是服务器端渲染的,只有如聊天框或新闻提要等功能通过 API 调用进行更新。

Meteor,然而,建立在这样一个理念上,即使用所有客户端的计算能力比使用一个单一服务器的计算能力要好。一个纯厚客户端或者一个单页应用包含了一个网站前端的所有逻辑,在初始页面加载时发送下来。

服务器随后仅仅作为数据源,只向客户端发送数据。这可以通过连接到 API 并利用 AJAX 调用实现,或者像 Meteor 一样,使用一种名为发布/订阅的模型。在这个模型中,服务器提供一系列发布物,每个客户端决定它想订阅哪个数据集。

与 AJAX 调用相比,开发者无需处理任何下载或上传逻辑。Meteor 客户端在订阅特定数据集后自动后台同步所有数据。当服务器上的数据发生变化时,服务器将更新后的文档发送给客户端,反之亦然,如下面的图表所示:

同步数据 - 当前的网络与新的网络

注意

如果这听起来确实不安全,请放心,我们可以设置规则,在服务器端过滤更改。我们将在第八章,使用允许和拒绝规则进行安全设置中查看这些可能性。

移除 autopublish 包

为了使用 Meteor 的发布/订阅,我们需要移除autopublish包,这个包是我们项目默认添加的。

这个包适用于快速原型设计,但在生产环境中不可行,因为我们的数据库中的所有数据都将同步到所有客户端。这不仅不安全,而且还会减慢数据加载过程。

我们只需在我们my-meteor-blog文件夹内的终端上运行以下命令:

$ meteor remove autopublish

现在我们可以再次运行meteor来启动我们的服务器。当我们检查网站时,我们会发现我们上一章的所有帖子都消失了。

然而,它们实际上并没有消失。当前的服务器只是还没有发布任何内容,客户端也只是没有订阅任何内容;因此,我们看不到它们。

发布数据

为了在客户端再次访问帖子,我们需要告诉服务器将其发布给订阅的客户端。

为此,我们将在my-meteor-blog/server文件夹中创建一个名为publications.js的文件,并添加以下代码行:

Meteor.publish('all-posts', function () {
  return Posts.find();
});

Meteor.publish函数将创建一个名为all-posts的发布,并返回一个包含Post集合中所有帖子的游标。

现在,我们只需告诉客户端订阅这个发布,我们就会再次看到我们的帖子。

我们在my-meteor-blog/client文件夹中创建一个名为subscriptions.js的文件,内容如下:

Meteor.subscribe('all-posts');

现在,当我们检查我们的网站时,我们可以看到我们的博客文章已经重新出现。

这是因为当执行subsciptions.js文件时,客户端会订阅all-posts发布,这发生在页面完全加载之前,因为 Meteor 自动将subsciptions.js文件添加到文档的头部为我们。

这意味着 Meteor 服务器首先发送网站,然后 JavaScript 在客户端构建 HTML;随后,所有订阅都会同步,填充客户端的集合,并且模板引擎Blaze能够显示帖子。

现在我们已经恢复了我们的帖子,让我们看看我们如何告诉 Meteor 只发送集合中的一部分文档。

只发布数据的一部分

为了使我们的首页更具未来感,我们需要限制在上面显示的文章数量,因为随着时间的推移,我们可能会添加很多文章。

为此,我们将创建一个名为limited-posts的新发布,其中我们可以向文章的find()函数传递一个limit选项,并将其添加到我们的publications.js文件中,如下所示:

Meteor.publish('limited-posts', function () {
  return Posts.find({}, {
    limit: 2,
    sort: {timeCreated: -1}
  });
});

我们添加一个sort选项,通过它按timeCreated字段降序排列文章。这是必要的,以确保我们获取最新的文章并然后限制输出。如果我们只在客户端上对数据进行排序,可能会发生我们省略了较新的文章,因为服务器发布只会发送它找到的第一个文档,不管它们是否是最新的。

现在我们只需去到subscriptions.js文件,将订阅更改为以下代码行:

Meteor.subscribe('limited-posts');

如果我们现在查看我们的浏览器,我们会看到只有最后两篇文章出现在我们的首页上,因为我们只订阅了两个,如下面的屏幕截图所示:

只发布数据的部分

注意

我们必须意识到,如果我们保留旧订阅的代码并与新订阅的代码并列,我们将同时订阅两个。这意味着 Meteor 合并了两个订阅,因此在我们客户端集合中保留了所有订阅的文档。

在添加新订阅之前,我们必须注释掉旧的订阅或删除它。

发布特定字段

为了优化发布,我们还可以确定要从文档中发布哪些字段。例如,我们只要求titletext属性,而不是其他所有属性。

这样做可以加快我们订阅的同步速度,因为我们不需要整个文章,只需要在首页上列出文章时必要的数据和简短描述。

让我们在publications.js文件中添加另一个发布:

Meteor.publish('specificfields-posts', function () {
  return Posts.find({}, {
    fields: {
      title: 1
    }
  });
});

由于这只是一个示例,我们传递一个空对象作为一个查询来查找所有文档,作为find()的第二个参数,我们传递一个包含fields对象的选项对象。

我们给每个字段一个值为1的属性,该属性将被包含在返回的文档中。如果我们想通过排除字段来工作,我们可以使用字段名称并将值设置为0。然而,我们不能同时包含和排除字段,因此我们需要根据文档大小选择哪个更适合。

现在我们可以在subscriptions.js文件中简单地将订阅更改为以下代码行:

Meteor.subscribe('specificfields-posts');

现在,当我们打开浏览器时,它将向我们展示一个文章列表。只有标题存在,而描述、时间和作者字段为空:

发布特定字段

懒加载文章

既然我们已经浏览了这些简单的示例,那么现在让我们将它们结合起来,并为首页上的文章列表添加一个优美的懒加载功能。

懒加载是一种技术,只有在用户需要或滚动到末尾时才加载附加数据。这可以用来增加页面加载,因为要加载的数据是有限的。为此,让我们执行以下步骤:

  1. 我们需要向首页文章列表的底部添加一个懒加载按钮。我们打开我们的home.html文件,在home模板的末尾,在我们{{#each postsList}}块助手下面添加以下按钮:

    <button class="lazyload">Load more</button>
    
  2. 接下来,我们将向我们的publications.js文件中添加一个发布,以发送灵活数量的文章,如下所示:

    Meteor.publish('lazyload-posts', function (limit) {
      return Posts.find({}, {
        limit: limit,
        fields: {
          text: 0
        },
        sort: {timeCreated: -1}
      });
    });
    

基本上,这是我们之前学到的内容的组合。

  • 我们使用了limit选项,但不是设置一个固定的数字,而是使用了limit参数,我们稍后将其传递给这个发布函数。

  • 以前,我们使用了fields选项并排除了text字段。

  • 我们可以只包含fields来获得相同的结果。这将更安全,因为它确保我们在文档扩展时不会获取任何额外的字段:

    fields: {
      title: 1,
      slug: 1,
      timeCreated: 1,
      description: 1,
      author: 1
    }
    
  • 我们对输出进行了排序,以确保我们总是返回最新的文章。

现在我们已经设置了我们的发布,让我们添加一个订阅,这样我们就可以接收其数据。

注意

请注意,我们需要先删除任何其他订阅,这样我们就不会订阅任何其他发布。

为此,我们需要利用 Meteor 的session对象。这个对象可以在客户端用来设置反应性的变量。这意味着每次我们改变这个会话变量时,它都会再次运行使用它的每个函数。在下面的示例中,我们将使用会话来在点击懒加载按钮时增加文章列表的数量:

  1. 首先,在subscription.js文件中,我们添加以下代码行:

    Session.setDefault('lazyloadLimit', 2);
    Tracker.autorun(function(){
    Meteor.subscribe('lazyload-posts', Session.get('lazyloadLimit'));
    });
    
  2. 然后我们将lazyloadLimit会话变量设置为2,这将是我们前端页面最初显示的文章数量。

  3. 接下来,我们创建一个Tracker.autorun()函数。这个函数将在开始时运行,后来在我们改变lazyloadLimit会话变量到另一个值时随时运行。

  4. 在这个函数内部,我们订阅了lazyload-posts,将lazyloadLimit值作为第二个参数。这样,每次会话变量改变时,我们都用一个新的值改变我们的订阅。

  5. 现在,我们只需要通过点击懒加载按钮来增加会话值,订阅就会改变,发送给我们额外的文章。为此,我们在home.js文件的末尾添加以下代码行:

    Template.home.events({
      'click button.lazyload': function(e, template){
      var currentLimit = Session.get('lazyloadLimit');
    
      Session.set('lazyloadLimit', currentLimit + 2);
      }
    });
    

    这段代码将为懒加载按钮附加一个click事件。每次我们点击这个按钮时,我们都会获取lazyloadLimit会话,并增加两倍。

  6. 当我们检查浏览器时,我们应该能够点击文章列表底部的懒加载按钮,它应该再添加两篇文章。每次我们点击按钮时,都应该发生这种情况,直到我们达到五个示例文章。

当我们只有五篇文章时,这看起来并不太有意义,但当文章超过 50 篇时,将最初显示的文章限制为 10 篇将显著提高页面加载时间。

然后我们只需要将会话的默认值更改为 10 并增加 10,就可以实现一个很好的懒加载效果。

切换订阅

现在我们已经有了很好的懒加载逻辑,让我们来看看这里的底层发生了什么。

我们之前创建的.autorun()函数将在代码首次执行时运行,订阅lazyload-posts发布。Meteor 然后发送Posts集合的最初两个文档,因为我们的第一个limit值是2

下次我们更改lazyloadLimit会话时,它通过更改发布函数中的限制值来更改订阅。

Meteor 然后在后台检查我们客户端数据库中存在的文档,并请求下载缺失的文档。

当我们减少会话值时,这个方法也会起作用。Meteor 会删除与当前订阅/订阅不匹配的文档。

因此,我们可以尝试这样做;我们打开浏览器控制台,将会话限制设置为5

Session.set('lazyloadLimit', 5);

这将立即在我们的列表中显示所有五个示例文章。现在如果我们将其设置为更小的值,我们将看到它们是如何被移除的:

Session.set('lazyloadLimit', 2);

为了确保它们已经消失,我们可以查询我们本地数据库,如下所示:

Posts.find().fetch();

这将返回一个包含两个项目的数组,显示 Meteor 已经删除了我们不再订阅的文章,如下图所示:

切换订阅

关于数据发布的一些说明

Posts collection changes:
Meteor.publish('comments', function (postId) {
    var post = Posts.find({_id: postId});

    return Comments.find({_id: {$in: post.comments}});
});

为了解决这个问题,你可以将文章和评论分开发布并在客户端连接它们,或者使用第三方包,如在atmospherejs.com/reywood/publish-composite提供的允许有反应性发布的reywood:publish-composite包。

注意

请注意,Meteor.publish()函数重新运行的唯一情况是当前用户发生变化,使得this.userId在函数中可访问。

总结

在本章中,我们创建了几篇发布文章并订阅了它们。我们使用了fieldslimit选项来修改发布的文档数量,并为博客首页实现了一个简单的懒加载逻辑。

为了更深入地了解我们学到的内容,我们可以查看第三章, 存储数据和处理集合。以下 Meteor 文档将详细介绍我们可以在集合find()函数中使用的选项:

你可以在这个章节代码示例的www.packtpub.com/books/content/support/17713找到,或者在 GitHub 上找到github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter4

在下一章节,我们将给我们的应用添加一个真正应用的元素——不同的页面和路由。

第五章:使用路由使我们的应用具有灵活性

既然我们已经到了这一章节,我们应该已经对 Meteor 的模板系统有一个很好的理解,并且了解服务器与客户端之间数据同步的工作原理。在消化了这些知识后,让我们回到有趣的部分,把我们的博客变成一个具有不同页面的真正网站。

你可能会问,“在单页应用中页面做什么?” “单页”这个术语有点令人困惑,因为它并不意味着我们的应用只由一个页面组成。它更是一个从当前做事方式衍生出来的术语,因为只有一页是从服务器发送下来的。在那之后,所有的路由和分页都在浏览器中完成。再也不需要从服务器本身请求任何页面了。在这里更好的术语应该是“客户端 web 应用程序”,尽管单页是目前使用的名称。

在本章中,我们将涵盖以下主题:

  • 为我们的静态和动态页面编写路由。

  • 根据路由更改订阅

  • 为每个页面更改网站的标题。

那么,我们不要浪费时间,先添加iron:router包。

注意

如果你直接跳到这一章节并且想跟随示例,从以下网址下载前一章节的代码示例:书的网页www.packtpub.com/books/content/support/17713 或 GitHub 仓库github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter4

这些代码示例还将包含所有样式文件,因此我们不必担心在过程中添加 CSS 代码。

添加 iron:router 包

路由是应用中特定页面的 URL。在服务器端渲染的应用中,路由要么由服务器的/框架配置定义,要么由服务器上的文件夹结构定义。

在客户端应用中,路由仅仅是应用将用来确定要渲染哪些页面的路径。

客户端内要执行的步骤如下:

  1. 网站被发送到客户端。

  2. JavaScript 文件(或文件)被加载并解析。

  3. 路由器代码将检查当前它是哪个 URL,并运行正确的路由函数,然后渲染正确的模板。

    提示

    为了在我们的应用中使用路由,我们将使用iron:router包,这是一个为 Meteor 编写的路由器,它使得设置路由和将它们与订阅结合变得容易。

  4. 要添加包,我们取消任何正在运行的 Meteor 实例,前往我们的my-meteor-blog文件夹,并输入以下命令:

    $ meteor add iron:router
    
    
  5. 如果我们完成了这些,我们可以通过运行$ meteor命令再次启动 Meteor。

当我们回到浏览器的控制台时,我们会看到一个错误,说:Error: Oh no! No route found for path: "/"。不用担心;我们将在下一节处理这个问题。

设置路由器

为了使用路由器,我们需要对其进行设置。为了保持我们的代码组织有序,我们将在my-meteor-blog文件夹的根目录下创建一个名为routes.js的文件,并输入以下代码:

Router.configure({
    layoutTemplate: 'layout'
});

路由配置允许您定义以下默认模板:

` layoutTemplate` 布局模板将作为主包装器。在这里,子模板将在{{> yield}}占位符中渲染,该占位符必须放在模板的某个位置。
` notFoundTemplate` 如果当前 URL 没有定义路由,将渲染此模板。
` loadingTemplate` 当当前路由的订阅正在加载时,将显示此模板。

对于我们的博客,我们现在只需定义layoutTemplate属性。

执行以下步骤以设置路由器:

  1. 要创建我们的第一个路由,我们需要在route.js文件中添加以下代码行:

    Router.map(function() {
    
        this.route('Home', {
            path: '/',
            template: 'home'
        });
    
    });
    

    注意

    您还可以将Home路由命名为home(小写)。然后我们可以省略手动模板定义,因为iron:router将自动查找名为home的模板。

    为了简单起见,我们手动定义模板,以保持全书中的所有路由一致。

  2. 如果我们现在保存这个文件并回到浏览器,我们将看到layout模板被渲染两次。这并不是因为iron:router默认将layoutTemplate添加到我们应用程序的正文中,而是因为我们手动添加了它,以及在index.html中使用了{{> layout}},所以它被渲染了两次。

为了防止layout模板的重复出现,我们需要从index.html文件中的<body>标签中删除{{> layout}}助手。

当我们检查浏览器时,现在只会看到layout模板被渲染一次。

切换到布局模板

尽管我们通过template: home向我们的Home路由传递了一个模板,但我们并没有动态地渲染这个模板;我们只是显示了带有其硬编码子模板的布局模板。

为了改变这一点,我们需要将布局模板内的{{> home}}包含助手替换为{{> yield}}

{{> yield}}助手是iron:router提供的占位符助手,在此处渲染路由模板。

完成此操作后,当我们检查浏览器时,我们不应该看到任何变化,因为我们仍然在渲染home模板,但这次是动态的。然后我们按照以下步骤进行操作:

  1. 为了验证这一点,我们将向我们的应用程序添加一个未找到的模板,通过在layout.html文件中的布局模板之后添加以下模板:

    <template name="notFound">
      <div class="center">
        <h1>Nothing here</h1><br>
        <h2>You hit a page which doesn't exist!</h2>
      </div>
    </template>
    
  2. 现在我们需要向route.js中的Router.configure()函数添加notFoundTemplate属性:

    Router.configure({
        layoutTemplate: 'layout',
        notFoundTemplate: 'notFound'
    });
    

现在,当我们导航到http://localhost:3000/doesntexist时,我们将看到notFound模板被渲染,而不是我们的home模板:

切换到布局模板

如果我们点击主菜单中的首页链接,我们会回到我们的首页,因为此链接导航到"/"。我们已经成功添加了我们的第一个路由。现在让我们继续创建第二个路由。

添加另一个路由

拥有一个首页并不意味着是一个真正的网站。让我们添加一个到我们的关于页面的链接,该页面自从第二章 构建 HTML 模板以来就在我们的抽屉里。

要这样做,只需复制Home路由,并将值更改为创建一个About路由,如下所示:

Router.map(function() {

    this.route('Home', {
        path: '/',
        template: 'home'
    });
    this.route('About', {
        path: '/about',
        template: 'about'
    });
});

完成!

现在,当我们回到浏览器时,我们可以点击主菜单中的两个链接来切换我们的首页关于页面,甚至输入http://localhost:3000/about也会直接带我们到相应的页面,如下截图所示:

添加另一个路由

将帖子订阅移动到首页路由

为了为每个页面加载正确的数据,我们需要在路由中拥有订阅,而不是将其保存在单独的subscriptions.js文件中。

iron:router有一个特殊的函数叫做subscriptions(),这正是我们需要的。使用这个函数,我们可以反应性地更新特定路由的订阅。

为了看到它的实际应用,将subscriptions()函数添加到我们的Home路由中:

this.route('Home', {
    path: '/',
    template: 'home',
    subscriptions
: function(){
 return Meteor.subscribe("lazyload-posts", Session.get('lazyloadLimit'));
 }
});

subscriptions.js文件中的Session.setDefault('lazyloadLimit', 2)行需要在routes.js文件的开头,并在Router.configure()函数之前:

if(Meteor.isClient) {
    Session.setDefault('lazyloadLimit', 2);
}

这必须包裹在if(Meteor.isClient){}条件内,因为会话对象仅在客户端可用。

subscriptions()函数和之前使用的Tracker.autorun()函数一样是响应式的。这意味着当lazyloadLimit会话变量发生变化时,它会重新运行并更改订阅。

为了看到它的工作情况,我们需要删除my-meteor-blog/client/subscriptions.js文件,这样我们就不会有两个订阅相同发布物的点。

当我们现在检查浏览器并刷新页面时,我们会看到home模板仍然显示所有示例帖子。点击懒加载按钮会增加列出的帖子数量,但这次一切都是在我们的反应式subscriptions()函数中完成的。

注意

iron:router带有更多的钩子,您可以在附录中找到简短的列表。

为了完成我们的路由,我们只需要添加帖子路由,这样我们就可以点击一个帖子并详细阅读。

设置帖子路由

为了能够显示一个完整的帖子页面,我们需要创建一个帖子模板,当用户点击一个帖子时可以加载。

我们在my-meteor-blog/client/templates文件夹中创建一个名为post.html的文件,并使用以下模板代码:

<template name="post">
  <h1>{{title}}</h1>
  <h2>{{description}}</h2>

  <small>
    Posted {{formatTime timeCreated "fromNow"}} by {{author}}
  </small>

  <div class="postContent">
    {{#markdown}}
{{text}}
    {{/markdown}}
  </div>
</template>

这个简单的模板显示了博客文章的所有信息,甚至重用了我们在这本书中早些时候从template-helper.js文件创建的{{formatTime}}助手。我们用这个助手来格式化文章创建的时间。

我们暂时还看不到这个模板,因为我们必须先为这个页面创建发布和路由。

创建一个单篇博文发布

为了在这个模板中显示完整文章的数据,我们需要创建另一个发布,该发布将完整的文章文档发送到客户端。

为了实现这一点,我们打开my-meteor-blog/server/publication.js文件,并添加以下发布内容:

Meteor.publish("single-post", function(slug) {
  return Posts.find({slug: slug});
});

这里使用的slug参数将在稍后的订阅方法中提供,以便我们可以使用slug参数来引用正确的文章。

注意

缩略词是文档标题,以一种适合 URL 使用的方式格式化。缩略词比简单地在 URL 后附加文档 ID 更好,因为它们可读性强,易于访问者理解,也是良好 SEO 的重要组成部分。

为了使用缩略词,每个缩略词都必须是唯一的。我们在创建文章时会照顾到这一点。

假设我们传递了正确的斜杠,比如my-first-entry,这个发布将发送包含此斜杠的文章。

添加博文路由

为了让这个路由工作,它必须是动态的,因为每个链接的 URL 对于每篇文章都必须是不同的。

我们还将渲染一个加载模板,直到文章被加载。首先,我们在my-meteor-blog/client/templates/layout.html中添加以下模板:

<template name="loading">
  <div class="center">
    <h1>Loading</h1>
  </div>
</template>

此外,我们还需要将此模板作为默认加载模板添加到routes.js中的Router.configure()调用中:

Router.configure({
    layoutTemplate: 'layout',
    notFoundTemplate: 'notFound',
    loadingTemplate: 'loading',
    ...

然后,我们将以下代码行添加到我们的Router.map()函数中,以创建一个动态路由:

this.route('Post', {
    path: '/posts/:slug',
    template: 'post',

    waitOn: function() {
        return Meteor.subscribe('single-post', this.params.slug);
    },
    data: function() {
        return Posts.findOne({slug: this.params.slug});
    }
});

'/posts/:slug'路径是一个动态路由,其中:slug可以是任何内容,并将传递给路由函数作为this.params.slug。这样我们只需将给定的 slug 传递给single-post订阅,并检索与这个 slug 匹配的文章的正确文档。

waitOn()函数的工作方式类似于subscriptions()函数,不过它会自动渲染我们在Router.configure()中设置的loadingTemplate,直到订阅准备好。

这个路由的data()函数将设置post模板的数据上下文。我们基本上在我们的本地数据库中查找包含来自 URL 的给定 slug 的文章。

注意

Posts集合的findOne()方法与find()方法类似,但只返回找到的第一个结果作为 JavaScript 对象。

让我们总结一下这里发生的事情:

  1. 路由被调用(通过点击链接或页面重新加载)。

  2. 然后waitOn()函数将订阅由给定的slug参数标识的正确文章,该参数是 URL 的一部分。

  3. 由于waitOn()函数,loadingTemplate将在订阅准备好之前渲染。由于这在我们的本地机器上会非常快,所以我们可能根本看不到加载模板。

  4. 一旦订阅同步,模板就会渲染。

  5. 然后data()函数将重新运行,设置模板的数据上下文为当前文章文档。

现在发布和路由都准备好了,我们只需导航到http://localhost:3000/posts/my-first-entry,我们应该看到post模板出现。

文章链接

虽然我们已经设置了路由和订阅,但我们看不到它工作,因为我们需要正确的文章链接。由于我们之前添加的每个示例文章都包含一个slug属性,所以我们只需将它们添加到postInList模板中的文章链接。打开my-meteor-blog/client/templates/postInList.html文件,按照以下方式更改链接:

<h2><a href="posts/{{slug}}">{{title}}</a></h2>

最后,当我们打开浏览器并点击博客文章的标题时,我们会被重定向到一个显示完整文章条目的页面,如下面的屏幕截图所示:

文章链接

更改网站标题

如今我们的文章路由已经运行,我们只缺少为每个页面显示正确的标题。

遗憾的是,<head></head>在 Meteor 中不是一个响应式模板,我们本可以让 Meteor 自动更改标题和元标签。

计划将head标签变成一个响应式模板,但可能在 1.0 版本之前不会实现。

为了更改文档标题,我们需要找到一种基于当前路由来更改它的不同方法。

幸运的是,iron:router有一个onAfterAction()函数,也可以在Router.configure()函数中用于每个路由之前运行。在这个函数中,我们有权访问当前路由的数据上下文,所以我们可以简单地使用原生 JavaScript 设置标题:

Router.configure({
    layoutTemplate: 'layout',
    notFoundTemplate: 'notFound',

    onAfterAction: function() {
 var data = Posts.findOne({slug: this.params.slug});

 if(_.isObject(data) && !_.isArray(data))
 document.title = 'My Meteor Blog - '+ data.title;
 else
 document.title = 'My Meteor Blog - '+ this.route.getName();
 }
});

使用Posts.findOne({slug: this.params.slug}),我们获取当前路由的文章。然后我们检查它是否是一个对象;如果是,我们将文章标题添加到title元标签。否则,我们只取路由名称。

Router.configure()中这样做将为每个路由调用onAfterAction

现在如果我们看看我们浏览器的标签页,我们会发现当我们浏览网站时,我们网站的标题会发生变化:

更改网站标题

提示

如果我们想要让我们的博客更酷,我们可以添加mrt:iron-router-progress包。这将在切换路由时在页面的顶部添加一个进度条。我们只需从我们的应用程序文件夹中运行以下命令:

$ meteor add mrt:iron-router-progress

摘要

就这样!现在我们的应用程序是一个功能完整的网站,有不同的页面和 URL。

在本章中,我们学习了如何设置静态和动态路由。我们将我们的订阅移到了路由中,这样它们就可以根据路由的需要自动更改。我们还使用了 slugs 来订阅正确的文章,并在post模板中显示它们。最后,我们更改了网站的标题,使其与当前路由相匹配。

要了解更多关于iron:router的信息,请查看其文档在github.com/EventedMind/iron-router

你可以在这个章节的代码示例在www.packtpub.com/books/content/support/17713找到,或者在 GitHub 上找到github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter5

在下一章中,我们将深入探讨 Meteor 的会话对象。

第六章:使用会话保持状态

我们在之前的章节中实现懒加载技术时已经使用了 Meteor 的 session 对象。在本章中,我们想要更深入地了解它,并学习如何使用它来创建特定模板的反应式函数。

本章将涵盖以下主题:

Meteor 的 session 对象

Meteor 提供的Session对象是一个反应式数据源,主要用于在热代码重载过程中维护全局状态,尽管它不会在页面手动重载时保存其数据,这使得它与 PHP 会话不同。

注意

当我们上传新代码时,服务器会将这些更新推送给所有客户端,这时就会发生热代码重载。

Session对象是一个反应式数据源。这意味着无论这个 session 变量在反应式函数中如何使用,当它的值发生变化时,它都会重新运行那个函数。

session 变量的一个用途可以是维护我们应用的全局状态,例如,检查用户是否显示侧边栏。

session 对象对于模板和其他应用部分之间的简单数据通信并不有用,因为维护这会很快变得令人痛苦,并且可能发生命名冲突。

实现简单反应性的更好方法

如果我们想要用于应用内通信,最好使用 Meteor 的reactive-var包,它带有一个类似于ReactiveVar对象的Session

使用它时,我们可以简单地通过$ meteor add reactive-var来添加它。

然后需要实例化这个对象,并带有反应式的get()set()函数,类似于session对象:

Var myReactiveVar = new ReactiveVar('my initial value');

// now we can get it in any reactive function
myReactiveVar.get();

// and set it, to rerun depending functions
myReactiveVar.set('my new value');

为了实现更自定义的反应性,我们可以使用 Meteor 的Tracker包构建我们自己的自定义反应式对象。有关更多信息,请参阅第九章,高级反应性

提示

对于与特定模板实例绑定的反应式变量,请查看我的frozeman:template-var包在atmospherejs.com/frozeman/template-var

在模板助手使用 session

由于所有模板助手函数都是反应式函数,因此在这样的助手内部使用 session 对象是一个好地方。

反应式意味着当我们在这个函数内部使用反应式对象时,该函数会在反应式对象发生变化时重新运行,同时重新渲染模板的这部分。

注意

模板助手不是唯一的反应式函数;我们还可以使用Tracker.autorun(function(){…})创建自己的,正如我们早先章节中看到的那样。

为了展示在模板助手中美使用会话的方法,请执行以下步骤:

  1. 打开我们的my-meteor-blog/client/templates/home.js文件,并在文件中的任何位置添加以下助手代码:

    Template.home.helpers({
      //...
      sessionExample: function(){
        return Session.get('mySessionExample');
      }
    });
    

    这创建了sessionExample助手,它返回mySessionExample会话变量的值。

  2. 接下来,我们需要把我们这个助手添加到我们的home模板本身,通过打开my-metepr-blog/client/templates/home.html文件,在我们{{#each postsList}}块助手上面加上助手:

    <h2>This comes from our Session: <strong>{{sessionExample}}</strong></h2>
    
  3. 现在,打开浏览器窗口,输入http://localhost:3000。我们会看到我们添加的静态文本出现在博客的主页上。然而,为了看到 Meteor 的反应式会话在起作用,我们需要打开浏览器的控制台并输入以下代码行:

    Session.set('mySessionExample', 'I just set this.');
    

    以下屏幕截图说明了这一点:

    在模板助手中美使用会话

在我们按下Enter键的那一刻,我们就看到了文字被添加到了我们的模板中。这是因为当我们调用Session.set('mySessionExample', ...)时,Meteor 会在我们之前调用Session.get('mySessionExample')的每个反应式函数中重新运行。对于模板助手,这只会重新运行这个特定的模板助手,只重新渲染模板的这部分。

我们可以通过为mySessionExample会话变量设置不同的值来尝试,这样我们就可以看到文字如何随时变化。

会话和热代码推送

热代码推送是指当我们更改文件时,Meteor 服务器将这些更改推送到客户端。Meteor 足够智能,可以重新加载页面,而不会丢失 HTML 表单或会话的值。因此,会话可以用来在热代码推送过程中保持用户状态的一致性。

为了看到这一点,我们将mySessionExample的值设置为我们想要的任何东西,并看到网站更新为此值。

现在,我们打开我们的home.html文件,进行一点小修改,例如移除{{sessionExample}}助手周围的<strong>标签并保存文件,我们会发现尽管页面随着新更改的模板重新加载,我们的会话状态仍然保持。这在以下屏幕截图中得到证明:

会话和热代码推送

注意

如果我们手动使用浏览器的刷新按钮重新加载页面,会话将无法保持更改,文字将消失。

为了克服这个限制,Meteor 的包仓库中有许多包,它们反应式地将数据存储在浏览器的本地存储中,以在页面重新加载时保持数据。其中一个包叫做persistent-session,可以在atmospherejs.com/package/persistent-session找到。

反应性地重新运行函数

为了根据会话更改重新运行函数,Meteor 提供了Tracker.autorun()函数,我们之前用它来改变懒加载订阅。

Tracker.autorun()函数将使传递给它的每个函数都具有反应性。为了看到一个简单的例子,我们将创建一个函数,每次函数重新运行时都会警告一个文本。

注意

Tracker包是会话在幕后使用的东西,以使反应性工作。在第九章,高级反应性,我们将深入研究这个包。

执行以下步骤以反应性地重新运行函数:

  1. 让我们创建一个名为main.js的新文件,但这次在my-meteor-blog目录的根目录中,内容如下:

    if(Meteor.isClient) {
    
        Tracker.autorun(function(){
            var example = Session.get('mySessionExample'); 
            alert(example);
        });
    }
    

    注意

    在后面的章节中我们将会需要main.js文件。因此,我们在根目录中创建了它,使其可以在客户端和服务器上访问。

    然而,由于 Meteor 的 session 对象只存在于客户端,我们将使用if(Meteor.isClient)条件,以便只在客户端执行代码。

    现在当我们查看浏览器时,我们会看到一个显示undefined的警告。这是因为传递给Tracker.autorun()的函数在代码执行时也会运行,在这个时候我们还没有设置我们的会话。

  2. 要设置会话变量的默认值,我们可以使用Session.setDefault('mySessionExample', 'My Text')。这将在不运行任何反应性函数的情况下设置会话,当会话值未定义时。如果会话变量的值已经设置,setDefault将根本不会更改变量。

  3. 在我们的示例中,当页面加载时我们可能不希望出现一个警告窗口。为了防止这种情况,我们可以使用Tracker.Computation对象,它作为我们函数的第一个参数传递给我们,并为我们提供了一个名为firstRun的属性。这个属性将在函数的第一次运行时设置为true。当我们使用这个属性时,我们可以在开始时防止显示警告:

    Tracker.autorun(function(c){
        var example = Session.get('mySessionExample'); 
    
        if(!c.firstRun) {
            alert(example);
        }
    });
    
  4. 现在让我们打开浏览器的控制台,将会话设置为任何值以查看警告窗口出现:

    Session.set('mySessionExample','Hi there!');
    

此代码的输出在下方的屏幕截图中展示:

反应性地重新运行函数

注意

当我们再次运行相同的命令时,我们不会看到警告窗口出现,因为 Meteor 足够智能,可以防止在会话值不变时重新运行。如果我们将其设置为另一个值,警告将再次出现。

停止反应式函数

作为第一个参数传递的Tracker.Computation对象还为我们提供了一种完全停止函数反应性的方法。为了尝试这个,我们将更改函数,使其在我们传递stop字符串给会话时停止其反应性:

Tracker.autorun(function(c){
    var example = Session.get('mySessionExample'); 

    if(!c.firstRun) {
        if(Session.equals('mySessionExample', 'stop')) {
            alert('We stopped our reactive Function');
            c.stop();
        } else {
            alert(example);
        }
    }
});

现在,当我们进入浏览器的控制台并运行Session.set('mySessionExample', 'stop')时,响应式函数将停止变得响应式。为了测试这一点,我们可以尝试运行Session.set('mySessionExample', 'Another text'),我们会发现警告窗口不会出现。

注意

如果我们对代码进行更改并且发生了热代码重载,响应式函数将再次变为响应式,因为代码被执行了 again。

前面的示例还使用了一个名为Session.equals()的函数。这个函数可以比较两个标量值,同时防止不必要的重新计算,与使用Session.get('mySessionExample) === 'stop'相比。使用Session.equals()只有在会话变量改变那个值时才会重新运行这个函数。

注意

在我们的示例中,然而,这个函数并没有什么区别,因为我们之前也调用了Session.get()

在模板中使用 autorun

虽然在某些情况下在我们的应用程序中全局使用Tracker.autorun()可能很有用,但随着我们应用程序的增长,这些全局响应式函数很快变得难以维护。

因此,将响应式函数绑定到它们执行操作的模板是一个好的实践。

幸运的是,Meteor 提供了一个特殊的Tracker.autorun()版本,它与模板实例相关联,并在模板被销毁时自动停止。

为了利用这一点,我们可以在created()或渲染回调中启动响应式函数。首先,让我们注释掉main.js文件中的上一个示例,这样我们就不会得到两个警告窗口。

打开我们的home.js文件,添加以下代码行:

Template.home.created = function(){

    this.autorun(function(){
        alert(Session.get('mySessionExample'));
    });
};

这将在主页模板创建时创建响应式函数。当我们进入浏览器的控制台并设置mySessionExample会话为新值时,我们会看到警告窗口出现,如下面的屏幕截图所示:

在模板中使用 autorun

现在,当我们通过点击菜单中的关于链接切换模板,并将mySessionExample会话变量再次设置为另一个值时,我们不会看到警告窗口出现,因为当模板被销毁时,响应式的this.autorun()已经停止。

注意

注意所有的Tracker.autorun()函数都返回一个Tracker.Computation对象,可以使用Tracker.Computation.stop()随时停止 autorun 的响应性:

Var myReactiveFunction = Tracker.autorun(function(){...});
// Do something which needs to stop the autorun
myReactiveFunction.stop();

响应式的会话对象

我们看到了会话对象可以在其值改变时重新运行函数。这和集合的find()findOne()函数的行为一样,这些函数在集合的底层数据改变时会重新运行函数。

我们可以使用会话来在热代码推送之间保持用户状态,比如下拉菜单或弹出的状态。但是,请注意,如果没有明确的命名约定,这些会话变量很快就会变得难以维护。

为了实现更具体的反应式行为,最好使用 Meteor 的Tracker核心包构建一个自定义的反应式对象,这将在第九章,高级反应性中介绍。

总结

在本章中,我们了解了 Meteor 的反应式会话对象能做什么。我们用它来重新运行模板助手和我们自己的自定义函数,并且我们通过created()destroyed()回调创建了一个特定的反应式函数模板。

要深入了解,请查看 Meteor 关于会话和反应性的文档,具体资源如下:

你可以在www.packtpub.com/books/content/support/17713找到本章的代码示例,或者在 GitHub 上查看github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter6

在下一章中,我们将为我们的博客创建管理员用户和后端,为创建和编辑帖子打下基础。

第七章:用户和权限

通过对前一章的内容进行操作,我们应该现在有一个运行中的博客了。我们可以点击所有的链接和帖子,甚至可以延迟加载更多的帖子。

在本章中,我们将添加我们的后端登录并创建管理员用户。我们还将创建一个编辑帖子的模板,并使管理员用户能够看到编辑按钮,以便他们可以编辑和添加新内容。

在本章中,我们将学习以下概念:

  • Meteor 的 accounts

  • 创建用户和登录

  • 如何限制某些路由仅供已登录用户使用

    注意

    你可以删除前一章中的所有会话示例,因为我们在推进应用时不需要它们。从 my-meteor-blog/main.jsmy-meteor-blog/client/templates/home.jsmy-meteor-blog/client/templates/home.html 中删除会话的代码,或者下载前一章代码的新副本。

    如果你直接跳到这一章并且想跟随示例,可以从以下网址下载前一章的代码示例:www.packtpub.com/books/content/support/17713 或从 GitHub 仓库 github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter6 下载。

    这些代码示例还将包含所有的样式文件,所以我们不需要在过程中添加 CSS 代码。

Meteor 的 accounts 包

Meteor 使得通过其 accounts 包向我们的网络应用添加身份验证变得非常容易。accounts 包是一个与 Meteor 的核心紧密相连的完整的登录解决方案。创建的用户可以在许多 Meteor 的服务器端函数中通过 ID 进行识别,例如,在一个出版物中:

Meteor.publish("examplePublication", function () {
  // the current loggedin user id can be accessed via
  this.userId;
}

此外,我们还可以通过简单地添加一个或多个 accounts-* 核心包来添加通过 Facebook、GitHub、Google、Twitter、Meetup 和 Weibo 登录的支持。

Meteor 还提供了一个简单的登录界面,一个可以通过使用 {{> loginButtons}} 助手添加的额外模板。

所有注册的用户资料都将存储在一个名为 Users 的集合中,Meteor 为我们创建了这个集合。所有的认证过程和通信过程都使用 Secure Remote Password (SRP) 协议,大多数外部服务都使用 OAuth。

对于我们的博客,我们只需创建一个管理员用户,当登录后,他们可以创建和编辑帖子。

注意

如果我们想要使用第三方服务之一进行登录,我们可以先完成本章的内容,然后添加前面提到的其中一个包。

添加额外包后,我们可以打开 登录 表单。我们将看到一个按钮,我们可以配置第三方服务以供我们的应用使用。

添加 accounts 包

要开始使用登录系统,我们需要将 accounts-uiaccounts-password 包添加到我们的应用中,如下所示:

  1. 为了做到这一点,我们打开终端,导航到我们的my-meteor-blog文件夹,并输入以下命令:

    $ meteor add accounts-ui accounts-password
    
    
  2. 在我们成功添加包之后,我们可以使用meteor命令再次运行我们的应用程序。

  3. 因为我们想要阻止我们的访客创建额外的用户账户,所以我们需要在我们的accounts包中禁止这个功能。首先,我们需要打开我们在前一章节中创建的my-meteor-blog/main.js文件,并删除所有代码,因为我们不再需要会话示例。

  4. 然后在这个文件中添加以下代码行,但一定要确保不要使用if(Meteor.isClient),因为这次我们希望在客户端和服务器上都执行代码:

    Accounts.config({
        forbidClientAccountCreation: true
    });
    

    这将禁止在客户端调用Accounts.createUser(),并且accounts-ui包将不会向我们的访客显示注册按钮。

    注意

    这个选项似乎对第三方服务不起作用。所以,当使用第三方服务时,每个人都可以注册并编辑文章。为了防止这种情况,我们将在服务器端创建“拒绝”规则以禁止用户创建,这超出了本章节的范围。

为我们的模板添加管理功能

允许编辑我们文章的最佳方式是在我们文章的页面上添加一个编辑文章链接,这个链接只有在登录后才能看到。这样,我们节省了为另一个后端重建类似基础设施的工作,并且使用起来很方便,因为前端和后端之间没有严格的分离。

首先,我们将向我们的home模板添加一个创建新文章链接,然后将编辑文章链接添加到文章的pages模板中,最后在主菜单中添加登录按钮和表单。

添加新文章的链接

让我们先添加一个创建新文章链接。打开my-meteor-blog/clients/templates/home.html中的home模板,并在{{#each postsList}}块助手之上添加以下代码行:

{{#if currentUser}}
    <a href="/create-post" class="createNewPost">Create new post</a>
{{/if}}

{{currentUser}}助手随accounts-base包一起提供,当我们安装我们的accounts包时安装了它。它会返回当前登录的用户,如果没有用户登录,则返回 null。将其用于{{#if}}块助手内部允许我们只向登录用户显示内容。

添加编辑文章的链接

要编辑文章,我们只需在我们的post模板中添加一个编辑文章链接。打开同一文件夹中的post.html,并在{{author}}之后添加{{#if currentUser}}..{{/if}},如下所示:

<small>
    Posted {{formatTime timeCreated "fromNow"}} by {{author}}

    {{#if currentUser}}
        | <a href="/edit-post/{{slug}}">Edit post</a>
    {{/if}}
</small>

添加登录表单

现在我们已经有了添加和编辑文章的链接,让我们添加登录表单。我们可以创建自己的表单,但 Meteor 已经包含了一个简单的登录表单,我们可以将其样式修改以符合我们的设计。

由于我们之前添加了accounts-ui包,Meteor 为我们提供了{{> loginButtons}}模板助手,它作为一个即插即用的模板工作。为了添加这个功能,我们将打开我们的layout.html模板,并在菜单的<ul></ul>标签内添加以下助手,如下所示:

<h1>My Meteor Single Page App</h1>
<ul>
    <li>
        <a href="/">Home</a>
    </li>
    <li>
        <a href="/about">About</a>
    </li>

</ul>

{{> loginButtons}}

创建编辑文章的模板

现在我们只缺少编辑帖子的模板。为了添加这个模板,我们将在my-meteor-blog/client/templates文件夹中创建一个名为editPost.html的文件,并填入以下代码行:

<template name="editPost">
  <div class="editPost">
     <form>
        <label>
          Title
          <input type="text" name="title" placeholder="Awesome title" value="{{title}}">
        </label>

        <label>
          Description
          <textarea name="description" placeholder="Short description displayed in posts list" rows="3">{{description}}</textarea>
        </label>

        <label>
          Content
          <textarea name="text" rows="10" placeholder="Brilliant content">{{text}}</textarea>
        </label>

        <button type="submit" class="save">Save Post</button>
    </form>
  </div>
</template>

正如我们所看到的,我们添加了{{title}}{{description}}{{text}}帮助器,这些将从帖子数据中稍后获取。这个简单的模板,带有它的三个文本字段,将允许我们以后编辑和创建新帖子。

如果我们现在查看浏览器,我们会注意到我们看不到到目前为止所做的任何更改,除了网站角落里的登录链接。为了能够登录,我们首先需要添加我们的管理员用户。

创建管理员用户

由于我们已禁用客户端创建用户,作为一种安全措施,我们将在服务器上以创建示例帖子的方式创建管理员用户。

打开my-meteor-blog/server/main.js文件,在Meteor.startup(function(){...})内的某个位置添加以下代码行:

if(Meteor.users.find().count() === 0) {

    console.log('Created Admin user');

    Accounts.createUser({
        username: 'johndoe',
        email: 'johndoe@example.com',
        password: '1234',
        profile: {
            name: 'John Doe'
        }
    });
}

如果我们现在打开浏览器,我们应该能够使用我们刚才创建的用户登录,我们会立即看到所有编辑链接出现。

然而,当我们点击任何编辑链接时,我们会看到notFound模板出现,因为我们还没有创建任何管理员路由。

添加权限

Meteor 的account包默认并不带有对用户可配置权限的支持。

为了添加权限控制,我们可以添加第三方包,比如deepwell:authorization包,可以在 Atmosphere 上找到,网址为atmospherejs.com/deepwell/authorization,它带有复杂的角色模型。

如果我们想手动完成,我们可以在创建用户时向用户文档添加简单的roles属性,然后在创建或更新帖子时在允许/拒绝角色中检查这些角色。我们将在下一章学习允许/拒绝规则。

如果我们使用Accounts.createUser()函数创建用户,我们就不能添加自定义属性,因此我们需要在创建用户后更新用户文档,如下所示:

var userId = Accounts.createUser({
  username: 'johndoe',
  email: 'johndoe@example.com',
  password: '1234',
  profile: {
    name: 'John Doe'
  }
});
// add the roles to our user
Meteor.users.update(userId, {$set: {
  roles: {admin: true},
}})

默认情况下,Meteor 会发布当前登录用户usernameemailsprofile属性。要添加其他属性,比如我们的自定义roles属性,我们需要添加一个发布功能,以便在客户端访问roles属性,如下所示:

  1. 打开my-meteor/blog/server/publications.js文件,添加以下发布功能:

    Meteor.publish("userRoles", function () {
     if (this.userId) {
      return Meteor.users.find({_id: this.userId}, {fields: {roles: 1}});
     } else {
      this.ready();
     }
    });
    
  2. my-meteor-blog/main.js文件中,我们像下面这样添加订阅:

    if(Meteor.isClient){
      Meteor.subscribe("userRoles");
    }
    
  3. 现在既然我们在客户端已经有了roles属性,我们可以把homepost模板中的{{#if currentUser}}..{{/if}}改为{{#if currentUser.roles.admin}}..{{/if}},这样只有管理员才能看到按钮。

有关安全性的说明

用户只能使用以下命令更新自己的profile属性:

Meteor.users.update(ownUserId, {$set: {profiles:{myProperty: 'xyz'}}})

如果我们想要更新roles属性,我们将失败。为了看到这一点,我们可以打开浏览器的控制台并输入以下命令:

Meteor.users.update(Meteor.user()._id, {$set:{ roles: {admin: false}}});

这将给我们一个错误,指出:更新失败:拒绝访问,如下面的屏幕截图所示:

关于安全性的说明

注意

如果我们想要允许用户编辑其他属性,例如他们的roles属性,我们需要为此添加一个Meteor.users.allow()规则。

为管理员创建路由

现在我们已经有了一个管理员用户,我们可以添加那些指向editPost模板的路由。尽管从理论上讲editPost模板对每个客户端都是可用的,但它不会造成任何风险,因为允许和拒绝规则才是真正的安全层,我们将在下一章中查看这些规则。

要添加创建文章的路由,让我们打开我们的my-meteor-blog/routes.js文件,并向Router.map()函数添加以下路由:

this.route('Create Post', {
    path: '/create-post',
    template: 'editPost'
});

这将在我们点击主页上的创建新文章链接后立即显示editPost模板,如下面的屏幕截图所示:

为管理员创建路由

我们发现表单是空的,因为我们没有为模板设置任何数据上下文,因此模板中显示的{{title}}{{description}}{{text}}占位符都是空的。

为了使编辑文章的路由工作,我们需要添加类似于为Post路由本身所做的订阅。为了保持事物的DRY(这意味着不要重复自己),我们可以创建一个自定义控制器,这个控制器将被两个路由使用,如下所示:

  1. Router.configure(...);调用之后添加以下代码行:

    PostController = RouteController.extend({
        waitOn: function() {
            return Meteor.subscribe('single-post', this.params.slug);
        },
    
        data: function() {
            return Posts.findOne({slug: this.params.slug});
        }
    });
    
  2. 现在我们可以简单地编辑Post路由,删除waitOn()data()函数,并添加PostController

    this.route('Post', {
        path: '/posts/:slug',
        template: 'post',
        controller: 'PostController'
    });
    
  3. 现在我们还可以通过简单地更改pathtemplate属性来添加编辑文章路由:

    this.route('Edit Post', {
        path: '/edit-post/:slug',
        template: 'editPost',
        controller: 'PostController'
    });
    
  4. 这就完成了!现在当我们打开浏览器时,我们将能够访问任何文章并点击编辑按钮,然后我们将被引导到editPost模板。

如果您想知道为什么表单会填充文章数据,请查看我们刚刚创建的PostController。在这里,我们在data()函数中返回文章文档,将模板的数据上下文设置为文章的数据。

现在我们已经设置了这些路由,我们应该完成了。难道不是吗?

还不是,因为任何知道/create-post/edit-post/my-title路由的人都可以简单地看到editPost模板,即使他或她不是管理员。

防止访客看到管理路由

routes.js file:
var requiresLogin = function(){
    if (!Meteor.user() ||
        !Meteor.user().roles ||
        !Meteor.user().roles.admin) {
        this.render('notFound');

    } else {
        this.next();
    }
}; 

Router.onBeforeAction(requiresLogin, {only: ['Create Post','Edit Post']});

在这里,首先我们创建了requiresLogin()函数,它将在创建文章编辑文章路由之前执行,因为我们将其作为第二个参数传递给Router.onBeforeAction()函数。

requiresLogin()内部,我们检查用户是否已登录,当调用Meteor.user()时,这将返回用户文档,并且检查他们是否有admin角色。如果没有,我们简单地渲染notFound模板,并不再继续路由。否则,我们运行this.next(),这将继续渲染当前路由。

就这样!如果我们现在登出并导航到/create-post路由,我们将看到notfound模板。

如果我们登录,模板将切换并立即显示editPost模板。

这是因为一旦我们将requiresLogin()函数传递给Router.onBeforeAction(),它就会变得具有反应性,而Meteor.user()是一个反应式对象,所以用户状态的任何变化都会重新运行这个函数。

现在我们已经创建了管理员用户和所需的模板,我们可以继续实际创建和编辑帖子。

总结

在本章中,我们学习了如何创建和登录用户,如何仅向已登录用户显示内容和模板,以及如何根据登录状态更改路由。

要了解更多,请查看以下链接:

您可以在www.packtpub.com/books/content/support/17713或 GitHub 上的github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter7找到本章的代码示例。

在下一章中,我们将学习如何创建和更新帖子以及如何从客户端控制数据库的更新。

第八章:使用允许和拒绝规则进行安全设置

在前一章中,我们创建了我们的管理员用户并准备了editPost模板。在本章中,我们将使这个模板工作,以便我们可以使用它创建和编辑帖子。

为了使插入和更新数据库中的文档成为可能,我们需要设置约束,使不是每个人都可以更改我们的数据库。在 Meteor 中,这是使用允许和拒绝规则完成的。这些函数将在文档被插入数据库前检查它们。

在本章中,您将涵盖以下主题:

添加一个生成 slug 的函数

为了从我们的帖子标题生成 slugs,我们将使用带有简单slugify()函数的underscore-string库。幸运的是,这个库的一个包装包已经在 Meteor 包服务器上存在。要添加它,我们请在终端中运行以下命令,位于我们的my-meteor-blog文件夹中:

$ meteor add wizonesolutions:underscore-string

这将使用默认在 Meteor 中使用的underscore扩展一些额外的字符串函数,如_.slugify(),从字符串生成一个 slug。

创建新帖子

现在我们已经可以为每个创建的页面生成 slugs,我们可以继续将保存过程添加到editPost模板中。

为此,我们需要为我们的editPost模板创建一个 JavaScript 文件,通过将一个名为editPost.js的文件保存到my-meteor-blog/client/templates文件夹中来实现。在这个文件中,我们将为模板的保存按钮添加一个事件:

Template.editPost.events({
  'submit form': function(e, template){
    e.preventDefault();
    console.log('Post saved');
  }
});

现在,如果我们前往/create-post路由并点击保存帖子按钮,帖子已保存日志应该在浏览器控制台中出现。

保存帖子

为了保存帖子,我们只需取表单的内容并将其存储在数据库中。稍后,我们将重定向到新创建的帖子页面。为此,我们将我们的点击事件扩展为以下几行代码:

Template.editPost.events({
  'submit form': function(e, tmpl){
    e.preventDefault();
    var form = e.target,
        user = Meteor.user();

我们获取当前用户,以便稍后将其作为帖子的作者添加。然后使用我们的slugify()函数从帖子标题生成一个 slug:

        var slug = _.slugify(form.title.value);

接着,我们使用所有其他表单字段将帖子文档插入到Posts集合中。对于timeCreated属性,我们使用在第一章,Meteor 入门中添加的moment包获取当前的 Unix 时间戳。

owner字段将帮助我们确定是由哪个用户创建了此帖子:

Posts.insert({
            title:          form.title.value,
            slug:           slug,
            description:    form.description.value,
            text:           form.text.value,
            timeCreated:    moment().unix(),
            author:         user.profile.name,
            owner:          user._id

        }, function(error) {
            if(error) {
                // display the error to the user
                alert(error.reason);
            } else {
                // Redirect to the post
                Router.go('Post', {slug: slug});
            }
        });
    }
});

我们传递给insert()函数的第二个参数是一个由 Meteor 提供的回调函数,如果出错,它将接收到一个错误参数。如果发生错误,我们警告它,如果一切顺利,我们使用生成的 slug 将用户重定向到新插入的帖子。

由于我们的路由控制器将会订阅这个 slug 的帖子,它将能够加载我们新创建的帖子并在帖子模板中显示它。

现在,如果我们打开浏览器,填写表单,并点击保存按钮,我们应该已经创建了我们的第一个帖子!

编辑帖子

所以保存是可行的。编辑呢?

当我们点击帖子中的编辑按钮时,我们将再次显示editPost模板。这次,表单字段填充了帖子的数据。到目前为止还不错,但如果我们现在点击保存按钮,我们将创建另一个帖子,而不是更新当前帖子。

更新当前帖子

由于我们设置了editPost模板的数据上下文,我们可以简单地使用帖子_id字段的存在作为更新的指示器,而不是插入帖子数据:

Template.editPost.events({
    'submit form': function(e, tmpl){
        e.preventDefault();
        var form = e.target,
            user = Meteor.user(),
            _this = this; // we need this to reference the slug in the callback

        // Edit the post
        if(this._id) {

            Posts.update(this._id, {$set: {
                title:          form.title.value,
                description:    form.description.value,
                text:           form.text.value

            }}, function(error) {
                if(error) {
                    // display the error to the user
                    alert(error.reason);
                } else {
                    // Redirect to the post
                    Router.go('Post', {slug: _this.slug});
                }
            });

        // SAVE
        } else {

            // The insertion process ...

        }
    }
});

知道了_id,我们可以简单地使用$set属性来更新当前文档。使用$set只会覆盖titledescriptiontext字段。其他字段将保持原样。

请注意,我们现在还需要在函数顶部创建_this变量,以便在回调 later 中访问当前数据上下文的slug属性。这样,我们稍后可以将用户重定向到我们编辑的帖子页面。

现在,如果我们保存文件并回到浏览器,我们可以编辑帖子并点击保存,所有更改都将如预期般保存到我们的数据库中。

现在,我们可以创建和编辑帖子。在下一节中,我们将学习如何通过添加允许和拒绝规则来限制对数据库的更新。

限制数据库更新

到目前为止,我们只是将插入和更新功能添加到了我们的editPost模板中。然而,如果有人在他们浏览器的控制台输入一个insert语句,任何人都可以插入和更新数据。

为了防止这种情况,我们需要在服务器端正确检查插入和更新权限,然后再更新数据库。

Meteor 的集合带有允许和拒绝函数,这些函数在每次插入或更新之前运行,以确定该操作是否被允许。

允许规则让我们允许某些文档或字段被更新,而拒绝规则覆盖任何允许规则,并肯定地拒绝对其集合的任何操作。

为了使这更加明显,让我们想象一个例子,我们定义了两个允许规则;其中一个将允许某些文档的title字段被更改,另一个只允许编辑description字段,但还有一个额外的拒绝规则可以防止某个特定文档在任何情况下被编辑。

删除不安全的包

为了开始使用允许和拒绝规则,我们需要从我们的应用程序中删除insecure包,这样客户端就不能简单地不通过我们的允许和拒绝规则就对我们的数据库进行更改。

使用终端中的Ctrl + C 停止运行中的meteor实例,并运行以下命令:

$ meteor remove insecure

成功删除包后,我们可以使用meteor命令再次运行 Meteor。

当我们现在打开浏览器尝试编辑任何帖子时,我们将看到一个提示窗口,显示访问被拒绝。记得我们之前在更新或插入操作失败时添加了这个alert()调用吗?

添加我们的第一个允许规则

为了使我们的帖子再次可编辑,我们需要添加允许规则以重新启用数据库更新。

为此,我们将在我们的my-meteor-blog/collections.js文件中添加以下允许规则,但在这个例子中,我们通过检查 Meteor 的isServer变量,使它们只在服务器端执行:

if(Meteor.isServer) {

    Posts.allow({
        insert: function (userId, doc) {
            // The user must be logged in, and the document must be owned by the user
            return userId && doc.owner === userId && Meteor.user().roles.admin;
        },

在插入允许规则中,我们只会在帖子所有者与当前用户匹配时插入文档,如果用户是管理员,我们可以在上一章中添加的roles.admin属性来确定。

如果允许规则返回false,将拒绝文档的插入。否则,我们将成功添加一个新帖子。更新也是一样,只是我们只检查当前用户是否是管理员:

        update: function (userId, doc, fields, modifier) {
            // User must be an admin
            return Meteor.user().roles.admin;
        },
        // make sure we only get this field from the documents
        fetch: ['owner']
    });
}

传递给update函数的参数如下表所示:

Field 描述
--- ---
userId 执行update操作的当前登录用户的用户 ID
doc 数据库中的文档,不包括拟议的更改
fields 包含将要更新的字段参数的数组
modifier 用户传递给update函数的修改器,例如{$set: {'name.first': "Alice"}, $inc: {score: 1}}

我们最后在允许规则的对象中指定的fetch属性,决定了当前文档的哪些字段应该传递给更新规则。在我们这个例子中,我们只需要owner属性用于我们的更新规则。fetch属性存在是为了性能原因,以防止不必要的巨大文档被传递到规则函数中。

注意

此外,我们可以指定remove()规则和transform()函数。remove()规则将获得与insert()规则相同的参数,并允许或阻止文档的删除。

transform()函数可以用来在传递给允许或拒绝规则之前转换文档,例如,使其规范化。然而,要注意的是,这不会改变插入数据库的文档。

现在如果我们尝试在我们的网站上编辑一个帖子,我们应该能够编辑所有帖子以及创建新的帖子。

添加拒绝规则

为了提高安全性,我们可以修复帖子的所有者和创建时间。我们可以通过向我们的Posts集合中添加一个额外的拒绝规则来防止对所有者以及timeCreatedslug字段的更改,如下所示:

if(Meteor.isServer) {

  // Allow rules

  Posts.deny({
    update: function (userId, docs, fields, modifier) {
      // Can't change owners, timeCreated and slug
      return _.contains(fields, 'owner') || _.contains(fields, 'timeCreated') || _.contains(fields, 'slug');
    }
  });
}

这个规则将简单地检查fields参数是否包含受限制的字段之一。如果包含,我们就拒绝更新这篇帖子。所以,即使我们之前的允许规则已经通过,我们的拒绝规则也确保了文档不会发生变化。

我们可以在浏览器的控制台中尝试拒绝规则,当我们处于一个帖子页面时,输入以下命令:

Posts.update(Posts.findOne()._id, {$set: {'slug':'test'}}); 

这应该会给你一个错误,提示更新失败:访问被拒绝,如下面的截图所示:

添加拒绝规则

虽然我们现在可以添加和更新帖子,但还有一种比简单地将它们从客户端插入到我们的Posts集合中更好的添加新帖子的方法。

使用方法调用来添加帖子

方法是可以在客户端调用并在服务器上执行的函数。

方法存根和延迟补偿

方法的优势在于它们可以在服务器上执行代码,同时拥有完整的数据库和客户端上的存根方法。

例如,我们可以有一个方法在服务器上执行某些操作,并在客户端的存根方法中模拟预期的结果。这样,用户不必等待服务器的响应。存根还可以调用界面更改,例如添加一个加载指示器。

一个原生方法调用的例子是 Meteor 的Collection.insert()函数,它将执行客户端侧的函数,立即将文档插入到本地minimongo数据库中,同时发送一个请求在服务器上执行真正的insert方法。如果插入成功,客户端已经有了插入的文档。如果出现错误,服务器将响应并从客户端再次移除插入的文档。

在 Meteor 中,这个概念被称为延迟补偿,因为界面会立即对用户的响应做出反应,从而补偿延迟,而服务器的往返将在后台发生。

使用方法调用来插入帖子,使我们能够简单地检查我们想要为帖子使用的 slug 是否已经在另一篇帖子中存在。此外,我们还可以使用服务器的时间来为timeCreated属性确保我们没有使用错误的用户时间戳。

更改按钮

在我们的示例中,我们将简单地使用方法存根功能,在服务器上运行方法时将保存按钮的文本更改为Saving…。为此,执行以下步骤:

  1. 首先,让我们通过模板助手更改保存按钮的静态文本,以便我们可以动态地更改它。打开my-meteor-blog/client/templates/editPost.html,用以下代码替换保存按钮的代码:

    <button type="submit" class="save">{{saveButtonText}}</button>
    
  2. 现在打开my-meteor-blog/client/templates/editPost.js,在文件开头添加以下模板助手函数:

    Session.setDefault('saveButton', 'Save Post');
    Template.editPost.helpers({
      saveButtonText: function(){
        return Session.get('saveButton');
      }
    });
    

    在这里,我们返回名为saveButton的会话变量,我们之前将其设置为默认值Save Post

更改会话将允许我们在保存文档的同时稍后更改保存按钮的文本。

添加方法

现在我们有了一个动态的保存按钮,让我们在我们的应用中添加实际的方法。为此,我们将创建一个名为methods.js的新文件,直接位于我们的my-meteor-blog文件夹中。这样,它的代码将在服务器和客户端加载,这是在客户端作为存根执行方法所必需的。

添加以下代码以添加方法:

Meteor.methods({
    insertPost: function(postDocument) {

        if(this.isSimulation) {
            Session.set('saveButton', 'Saving...');
        }
    }
});

这将添加一个名为insertPost的方法。在这个方法内部,存根功能已经通过使用isSimulation属性添加,该属性是通过 Meteor 在函数的this对象中提供的。

this对象还具有以下属性:

  • unblock():当调用此函数时,将防止该方法阻塞其他方法调用

  • userId:这包含当前用户的 ID

  • setUserId():这个函数用于将当前客户端连接到某个用户

  • connection:这是通过该方法在服务器上调用的连接

如果isSimulation设置为true,该方法不会在服务器端运行,而是作为存根在客户端运行。在这个条件下,我们简单地将saveButton会话变量设置为Saving…,以便按钮文本会更改:

Meteor.methods({
  insertPost: function(postDocument) {

    if(this.isSimulation) {

      Session.set('saveButton', 'Saving...');

    } else {

为了完成方法,我们将添加帖子插入的服务器端代码:

       var user = Meteor.user();

       // ensure the user is logged in
       if (!user)
       throw new Meteor.Error(401, "You need to login to write a post");

在这里,我们获取当前用户以添加作者名称和所有者 ID。

如果用户没有登录,我们就抛出异常,用new Meteor.Error。这将阻止方法的执行并返回我们定义的错误信息。

我们还查找具有给定 slug 的帖子。如果我们找到一个,我们在 slug 前添加一个随机字符串,以防止重复。这确保了每个 slug 都是唯一的,我们可以成功路由到我们新创建的帖子:

      if(Posts.findOne({slug: postDocument.slug}))
      postDocument.slug = postDocument.slug +'-'+ Math.random().toString(36).substring(3);

在我们插入新创建的帖子之前,我们使用moment库和authorowner属性添加timeCreated

      // add properties on the serverside
      postDocument.timeCreated = moment().unix();
      postDocument.author      = user.profile.name;
      postDocument.owner       = user._id;

      Posts.insert(postDocument);

在我们插入文档之后,我们返回修正后的 slug,然后在该方法调用的回调中作为第二个参数接收:

       // this will be received as the second argument of the method callback
       return postDocument.slug;
    }
  }
});

调用方法

现在我们已经创建了insertPost方法,我们可以改变在editPost.js文件中之前插入帖子时的提交事件代码,用我们的方法进行调用:

var slug = _.slugify(form.title.value);

Meteor.call('insertPost', {
  title:          form.title.value
  slug:           slug,
  description:    form.description.value
  text:           form.text.value,

}, function(error, slug) {
  Session.set('saveButton', 'Save Post');

  if(error) {
    return alert(error.reason);
  }

  // Here we use the (probably changed) slug from the server side method
  Router.go('Post', {slug: slug});
});

正如我们在方法调用的回调中看到的那样,我们使用在回调中作为第二个参数接收到的slug变量路由到新创建的帖子。这确保了如果slug变量在服务器端被修改,我们使用修改后的版本来路由到帖子。此外,我们将saveButton会话变量重置为将文本更改为Save Post

就这样!现在,我们可以使用我们新创建的insertPost方法创建并保存新的帖子。然而,编辑仍然会在客户端使用Posts.update()进行,因为我们现在有了允许和拒绝规则,以确保只有允许的数据被修改。

总结

在本章中,我们学习了如何允许和拒绝数据库的更新。我们设置了自己的允许和拒绝规则,并了解了方法如何通过将敏感过程移动到服务器端来提高安全性。我们还通过检查 slug 是否已存在并在其中添加了一个简单的进度指示器来改进发帖过程。

如果您想更深入地了解允许和拒绝规则或方法,请查看以下 Meteor 文档:

您可以在www.packtpub.com/books/content/support/17713找到本章的代码示例,或者在 GitHub 上找到github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter8

在下一章中,我们将通过不断更新帖子的时间戳来使我们的界面实现实时更新。

第九章:高级响应式

现在我们的博客基本上已经完成了,因为我们能够创建和编辑文章。在本章中,我们将利用 Meteor 的响应式模板来使我们的界面时间戳自动更新。我们将构建一个响应式对象,该对象将重新运行模板助手,显示博客文章创建的时间。这样,它们总是显示正确的相对时间。

在本章中,我们将介绍以下内容:

响应式编程

如我们已经在全书中看到的,Meteor 使用某种称为响应性的东西。

开发者在构建软件应用程序时必须解决的一个问题是指界面中表示数据的的一致性。大多数现代应用程序使用某种称为模型-视图-控制器MVC)的东西,其中视图的控制器确保它始终表示模型的当前状态。模型通常是服务器 API 或浏览器内存中的 JSON 对象。

保持界面一致的最常见方法如下(来源:manual.meteor.com):

  • 轮询和差异:定期(例如,每秒一次)获取事物的当前值,看看它是否发生变化,如果是,执行更新。

  • 事件:可以变化的事物在变化时发出事件。程序的另一部分(通常称为控制器)安排监听这个事件,获取当前值,并在事件触发时执行更新。

  • 绑定:值由实现某些接口的对象表示,例如BindableValue。然后,使用“绑定”方法将两个BindableValues连接在一起,这样当一个值发生变化时,另一个值会自动更新。有时,作为设置绑定的一部分,可以指定一个转换函数。例如,可以将FooBar绑定,并使用toUpperCase转换函数。

这些模式很好,但它们仍然需要大量的代码来维护所表示数据的的一致性。

另一种模式,尽管还不是那么常用,那就是响应式编程。这种模式是一种声明式的数据绑定方式。这意味着当我们使用一个响应式数据源,如一个Session变量或Mongo.Collection时,我们可以确信,一旦其值发生变化,使用这些值的响应式函数或模板助手将重新运行,总是保持基于这些值的用户界面或计算更新。

米托尔手册为我们提供了一个响应式编程用法的实例:

响应式编程非常适合构建用户界面,因为它不是试图用一段统一的代码来模拟所有的交互,而是让程序员表达在特定变化发生时应该发生的事情。响应变化的范式比显式地建模哪些变化会影响程序状态更容易理解。

例如,假设我们正在编写一个 HTML5 应用程序,有一个项目表,用户可以点击一个项目来选择它,或者按 Ctrl 点击来选择多个项目。我们可能有一个<h1>标签,并希望该标签的内容等于当前选定项目的大写名称,如果有多个项目被选中,则为“Multiple selection”。而且,我们可能有一组<tr>标签,并希望每个<tr>标签的 CSS 类为“selected”,如果该项目对应的行在选定项目的集合中,否则为空字符串。

为了使这个例子在上述模式中实现,我们可以很快地看到,与响应式编程相比,它变得多么复杂(来源:manual.meteor.com):

  • 如果我们使用轮询和差分,UI 将会变得不可接受地卡顿。用户点击后,屏幕实际上直到下一次轮询周期才会更新。此外,我们必须存储旧的选定集合,并与新的选定集合进行差分,这有点麻烦。

  • 如果我们使用事件,我们就必须编写一些相当复杂的控制器代码,手动将选择的变化或选定项目的名称映射到 UI 的更新。例如,当选择发生变化时,我们必须记住更新<h1>标签和(通常)两个受影响的<tr>标签。更重要的是,当选择发生变化时,我们必须自动在新生成的选定项目上注册一个事件处理程序,以便我们记住要更新<h1>。尤其是当 UI 被扩展和重新设计时,很难构建干净的代码并维护它。

  • 如果我们使用绑定,我们就必须使用一个复杂的领域特定语言DSL)来表达变量之间复杂的 relationships。这个 DSL 必须包括间接性(将<h1>的内容绑定到当前选择的任何固定项目的名称,而是绑定到由当前选择指示的项目)、转换(将名称首字母大写)和条件(如果有多个项目被选择,显示一个占位符字符串)。

使用米托尔的反应式模板引擎 Blaze,我们可以简单地使用{{#each}}块助手来遍历一个元素列表,并根据用户交互或根据项目的属性添加一些条件以添加一个选中类。

如果用户现在更改数据或从服务器接收的数据发生变化,界面将自动更新以表示相应的数据,节省我们大量时间并避免不必要的复杂代码。

无效化周期

理解反应式依赖的关键部分是无效化周期。

当我们在一个反应式函数中使用反应式数据源,例如Tracker.autorun(function(){…}),反应式数据源本身看到它在一个反应式函数中,并将当前函数作为依赖项添加到其依赖存储中。

然后,当数据源的值发生变化时,它会无效化(重新运行)所有依赖的函数,并将它们从其依赖存储中移除。

在反应式函数的重新运行中,它会将反应式函数重新添加到其依赖存储中,这样在下次无效化(值变化)时它们会再次运行。

这是理解反应性的关键,正如我们在以下示例中所看到的。

想象我们有三个Session变量设置为false

Session.set('first', false);
Session.set('second', false);

此外,我们还有Tracker.autorun()函数,它使用了这两个变量:

Tracker.autorun(function(){
    console.log('Reactive function re-run');
    if(Session.get('first')){
        Session.get('second');
    }
});

现在我们可以调用Session.set('second', true),但是反应式函数不会重新运行,因为在第一次运行中它从未被调用,因为first会话变量被设置为false

如果我们现在调用Session.set(first, true),该函数将重新运行。

此外,如果我们现在设置Session.set('second', false),它也会重新运行,因为在第二次重新运行中,Session.get('second')可以添加这个反应式函数作为依赖项。

由于反应式数据源在每次无效化时都会从其存储中移除所有依赖项,并在反应式函数的重新运行中重新添加它们,因此我们可以设置Session.set(first, false)并尝试将其更改为Session.set('second', true)。函数将不再重新运行,因为在这个运行中从未调用过Session.get('second')

一旦我们理解了这一点,我们就可以实现更细粒度的反应性,将反应式更新保持在最小。解释的控制台输出与以下屏幕截图类似:

无效化周期

构建一个简单的反应式对象

正如我们所看到的,反应式对象是一个在反应式函数中使用的对象,当它的值发生变化时,它会重新运行函数。米托尔的Session对象是反应式对象的一个例子。

在本章中,我们将构建一个简单的反应式对象,它将在时间间隔内重新运行我们的{{formatTime}}模板助手,以便所有相对时间都能正确更新。

米托尔的反应性是通过Tracker包实现的。这个包是所有反应性的核心,允许我们跟踪依赖项并在需要时重新运行它们。

执行以下步骤以构建简单的反应式对象:

  1. 让我们开始吧,让我们将以下代码添加到my-meteor-blog/main.js文件中:

    if(Meteor.isClient) {
        ReactiveTimer = new Tracker.Dependency;
    }
    

    这将在客户端创建一个名为ReactiveTimer的变量,带有Tracker.Dependency的新实例。

  2. ReactiveTimer变量下方,但仍在if(Meteor.isClient)条件下,我们将添加以下代码,每 10 秒重新运行一次我们ReactiveTimer对象的的所有依赖项:

    Meteor.setInterval(function(){
        // re-run dependencies every 10s
        ReactiveTimer.changed();
    }, 10000);
    

    Meteor.setInterval将每 10 秒运行一次函数。

    注意

    Meteor 自带了setIntervalsetTimeout的实现。尽管它们与原生 JavaScript 等效,但 Meteor 需要这些来引用服务器端特定用户的确切超时/间隔。

Meteor 自带了setIntervalsetTimeout的实现。尽管它们与原生 JavaScript 等效,但 Meteor 需要这些来引用服务器端特定用户的确切超时/间隔。

在这个区间内,我们调用ReactiveTimer.changed()。这将使每个依赖函数失效,并重新运行。

重新运行函数

到目前为止,我们还没有创建依赖项,所以让我们这样做。在Meteor.setInterval下方添加以下代码:

Tracker.autorun(function(){
    ReactiveTimer.depend();
    console.log('Function re-run');
});

如果我们现在回到浏览器控制台,我们应该会看到每 10 秒函数重新运行一次,因为我们的反应式对象重新运行了函数。

我们甚至可以在浏览器控制台中调用ReactiveTimer.changed(),函数也会重新运行。

这些例子很好,但不会自动更新我们的时间戳。

为此,我们需要打开my-meteor-blog/client/template-helpers.js并在我们的formatTime助手函数顶部添加以下行:

ReactiveTimer.depend();

这样,我们应用中的每个{{formatTime}}助手每 10 秒就会重新运行一次,更新流逝时的相对时间。要看到这一点,请打开浏览器,创建一篇新博客文章。现在保存博客文章,并观察创建时间文本,你会发现过了一会儿它会发生变化:

重新运行函数

创建高级计时器对象

之前的示例是一个自定义反应式对象的简单演示。为了使其更有用,最好创建一个单独的对象,隐藏Tracker.Dependency函数并添加其他功能。

Meteor 的反应性和依赖跟踪允许我们从另一个函数内部调用depend()函数时创建依赖项。这种依赖链允许更复杂的反应式对象。

在下一个示例中,我们将取我们的timer对象并为其添加startstop函数。此外,我们还将使其能够选择一个时间间隔,在该时间间隔内计时器将重新运行:

  1. 首先,让我们从main.jstemplate-helpers.js文件中删除之前添加的代码示例,并在my-meteor-blog/client内创建一个名为ReactiveTimer.js的新文件,内容如下:

    ReactiveTimer = (function () {
    
        // Constructor
        function ReactiveTimer() {
            this._dependency = new Tracker.Dependency;
            this._intervalId = null;
        };
    
        return ReactiveTimer;
    })();
    

    这创建了一个经典的 JavaScript 原型类,我们可以使用new ReactiveTimer()来实例化它。在其构造函数中,我们实例化了一个new Tracker.Dependency并将其附加到该函数。

  2. 现在,我们将创建一个start()函数,它将启动一个自选的间隔:

    ReactiveTimer = (function () {
    
        // Constructor
        function ReactiveTimer() {
            this._dependency = new Tracker.Dependency;
            this._intervalId = null;
        };
        ReactiveTimer.prototype.start = function(interval){
            var _this = this;
            this._intervalId = Meteor.setInterval(function(){
                // rerun every "interval"
                _this._dependency.changed();
            }, 1000 * interval);
        };
    
        return ReactiveTimer;
    })();
    

    这是我们之前使用的相同代码,不同之处在于我们将间隔 ID 存储在this._intervalId中,这样我们可以在stop()函数中稍后停止它。传递给start()函数的间隔必须是秒;

  3. 接下来,我们在类中添加了stop()函数,它将简单地清除间隔:

    ReactiveTimer.prototype.stop = function(){
        Meteor.clearInterval(this._intervalId);
    };
    
  4. 现在我们只需要一个函数来创建依赖关系:

    ReactiveTimer.prototype.tick = function(){
        this._dependency.depend();
    };
    

    我们的反应式定时器准备好了!

  5. 现在,要实例化timer并使用我们喜欢的间隔启动它,请在文件末尾的ReactiveTimer类后添加以下代码:

    timer = new ReactiveTimer();
    timer.start(10);
    
  6. 最后,我们需要回到template-helper.js文件中的{{formatTime}}助手,并添加``time.tick()函数,界面上所有的相对时间都会随着时间流逝而更新。

  7. 要看到反应式定时器的动作,可以在浏览器的控制台中运行以下代码片段:

    Tracker.autorun(function(){
        timer.tick();
        console.log('Timer ticked!');
    });
    
  8. 我们应该现在每 10 秒看到一次Timer ticked!的日志。如果我们现在运行time.stop(),定时器将停止运行其依赖函数。如果我们再次调用time.start(2),我们将看到Timer ticked!现在每两秒出现一次,因为我们设置了间隔为2创建一个高级定时器对象

正如我们所看到的,我们的timer对象现在相当灵活,我们可以在整个应用程序中创建任意数量的时间间隔。

反应式计算

Meteor 的反应性和Tracker包是一个非常强大的特性,因为它允许将事件行为附加到每个函数和每个模板助手。这种反应性正是保持我们界面一致性的原因。

虽然到目前为止我们只接触了Tracker包,但它还有几个我们应该查看的属性。

我们已经学习了如何实例化一个反应式对象。我们可以调用new Tracker.Dependency,它可以通过depend()changed()创建和重新运行依赖关系。

停止反应式函数

当我们在一个反应式函数内部时,我们也能够访问到当前的计算对象,我们可以用它来停止进一步的反应式行为。

为了看到这个效果,我们可以在浏览器的控制台中使用我们已经在运行的timer,并使用Tracker.autorun()创建以下反应式函数:

var count = 0;
var someInnerFunction = function(count){
    console.log('Running for the '+ count +' time');

    if(count === 10)
        Tracker.currentComputation.stop();
};
Tracker.autorun(function(c){
    timer.tick();

    someInnerFunction(count);

    count++;
});

timer.stop();
timer.start(2);

在这里,我们创建了someInnerFunction()来展示我们如何从嵌套函数中访问当前计算。在这个内部函数中,我们使用Tracker.currentComputation获取计算,它给了我们当前的Tracker.Computation对象。

我们使用之前在Tracker.autorun()函数中创建的count变量进行计数。当我们达到 10 时,我们调用Tracker.currentComputation.stop(),这将停止内部依赖和Tracker.autorun()函数的依赖,使它们失去反应性。

为了更快地看到结果,我们在示例的末尾以两秒的间隔停止和开始timer对象。

如果我们把前面的代码片段复制并粘贴到浏览器的控制台并运行它,我们应该看到Running for the xx time出现 10 次:

停止响应式函数

当前计算对象对于从依赖函数内部控制响应式依赖项很有用。

防止在启动时运行

Tracker``.Computation对象还带有firstRun属性,我们在前一章中使用过。

例如,当使用Tracker.autorun()创建响应式函数时,它们在首次被 JavaScript 解析时也会运行。如果我们想要防止这种情况,我们可以在检查firstRun是否为true时简单地停止函数,在执行任何代码之前:

Tracker.autorun(function(c){
    timer.tick();

    if(c.firstRun)
        return;

    // Do some other stuff
});

注意

我们在这里不需要使用Tracker.currentComputation来获取当前计算,因为Tracker.autorun()已经将其作为第一个参数。

同样,当我们停止Tracker.autorun()函数时,如以下代码所述,它将永远不会为会话变量创建依赖关系,因为第一次运行时从未调用Session.get()

Tracker.autorun(function(c){
  if(c.firstRun)
    return;

  Session.get('myValue');
}):

为了确保我们使函数依赖于myValue会话变量,我们需要将它放在return语句之前。

高级响应式对象

Tracker包还有一些更高级的属性和函数,允许您控制何时无效化依赖项(Tracker.flush()Tracker.Computation.invalidate())以及允许您在它上面注册额外的回调(Tracker.onInvalidate())。

这些属性允许您构建复杂的响应式对象,这超出了本书的范围。如果您想要更深入地了解Tracker包,我建议您查看 Meteor 手册中的manual.meteor.com/#tracker

总结

在本章中,我们学习了如何构建我们自己的自定义响应式对象。我们了解了Tracker.Dependency.depend()Tracker.Dependency.changed(),并看到了响应式依赖项具有自己的计算对象,可以用来停止其响应式行为并防止在启动时运行。

为了更深入地了解,请查看Tracker包的文档,并查看以下资源的Tracker.Computation对象的详细属性描述:

你可以在本章的代码示例在www.packtpub.com/books/content/support/17713或者在 GitHub 上找到github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter9

现在我们已经完成了我们的博客,我们将在下一章看看如何将我们的应用程序部署到服务器上。

第十章:部署我们的应用程序

我们的应用程序现在已准备好部署。在本章中,我们将了解如何将我们的应用程序部署到不同的服务器上,使其公开并向世界展示我们所构建的内容。

Meteor 使得在自身的服务器基础设施上部署应用程序变得非常容易。操作免费且迅速,但可能不适合生产环境。因此,我们将探讨手动部署以及一些为在任何 Node.js 服务器上部署而构建的优秀工具。

在本章中,我们将涵盖以下主题:

在 meteor.com 上部署

Meteor 提供了自己的托管环境,其中每个人都可以用一个命令免费部署应用程序。为了部署应用程序,Meteor 会为我们创建一个开发者账户,以便我们稍后管理和部署应用程序。首先,让我们执行以下步骤,在 meteor.com 上部署我们的应用程序:

  1. 在 meteor.com 的子域上部署就像在我们的应用程序文件夹中的终端运行以下命令那么简单:

    $ meteor deploy myCoolNewBlog
    
    

    我们可以自由选择要部署的子域。如果 myCoolNewBlog.meteor.com 已经被占用,Meteor 会要求我们登录所有者的账户以覆盖当前部署的应用程序,或者我们必须选择另一个名字。

  2. 如果域名可用,Meteor 会要求我们提供一个电子邮件地址,以便它为我们创建一个开发者账户。输入电子邮件地址后,我们将收到一封电子邮件,其中有一个链接设置我们的 Meteor 开发者账户,如下面的屏幕截图所示:在 meteor.com 上部署

  3. 为了创建我们的账户,我们需要遵循 Meteor 给出的链接,以便我们通过添加用户名和密码完全设置我们的账户,如下面的屏幕截图所示:在 meteor.com 上部署

  4. 完成这些操作后,我们将访问我们的开发者账户页面,在那里我们可以添加电子邮件地址,检查我们的最后登录,以及授权其他 Meteor 开发者登录到我们的应用程序(尽管我们首先必须添加 accounts-meteor-developer 包)。

  5. 最后,要在终端中使用 $ meteor login 登录我们的 Meteor 开发者账户,输入我们的凭据,并再次运行 deploy 命令来最终部署我们的应用程序:

    $ meteor deploy myCoolNewBlog
    
    
  6. 使用$ meteor authorized –add <username>命令,我们可以允许其他 Meteor 开发者将应用程序部署到我们应用程序的子域,如下所示屏幕截图:在 meteor.com 上部署

  7. 如果我们想更新我们部署的应用程序,我们只需在我们应用程序的文件夹内运行$ meteor deploy。 Meteor 将要求我们提供凭据,然后我们可以部署我们的应用程序。

如果我们正在朋友的计算机上,并且想使用我们的 Meteor 账户,可以使用$ meteor login。 Meteor 将保持我们登录状态,并且每个人都可以重新部署我们的任何应用程序。 我们需要确保在完成时使用$ meteor logout

使用域名在 meteor.com 上部署

我们还可以将应用程序托管在meteor.com,但可以定义我们自己的域名。

要这样做,我们只需使用我们的域名进行部署,如下所示:

$ meteor deploy mydomain.com

这将使应用程序托管在 meteor.com 上,但没有类似于myapp.meteor.com的直接 URL。

要将我们的域名指向 Meteor 服务器上的应用程序,我们需要将域名的A 记录更改为origin.meteor.com的 IP 地址(在撰写本书时为107.22.210.133),或CNAME 记录更改为origin.meteor.com。 您可以在注册域名的 DNS 配置中提供商处进行此操作。

Meteor 然后从我们的域名获取请求并在内部将其重定向到托管我们应用程序的服务器。

备份并恢复托管在 meteor.com 上的数据库

如果您需要备份数据库或将它移动到另一个服务器,您可以使用以下命令获取部署数据库的临时 Mongo 数据库凭据:

$ meteor mongo myapp.meteor.com –url

这将获取类似于以下凭据:

mongodb://client-ID:xyz@production-db-b1.meteor.io:27017/yourapp_meteor_com

然后,您可以使用前面输出的凭据使用mongodump备份您的数据库:

$ mongodump -h production-db-b1.meteor.io --port 27017 --username client-ID --password xyz --db yourapp_meteor_com

这将在您所在位置创建一个名为dump/yourapp_meteor_com的文件夹,并将数据库的转储文件放在里面。

要恢复到另一个服务器,请使用mongorestore,最后一个参数是你放置数据库转储的文件夹:

$ mongorestore -h mymongoserver.com --port 27017 --username myuser --password xyz --db my_new_database dump/yourapp_meteor_com

如果你只想将数据放入您本地的 Meteor 应用程序数据库中,请使用$ meteor启动 Meteor 服务器并运行以下命令:

$ mongorestore --port 3001

在其他服务器上部署

Meteor 的免费托管很棒,但当涉及到在生产中使用应用程序时,我们希望能够控制我们正在使用的服务器。

Meteor 允许我们将应用程序捆绑在一起,这样我们就可以在任何 Node.js 服务器上部署它。唯一的缺点是我们需要自己安装某些依赖项。此外,还有两个使部署应用程序几乎像 Meteor 本身一样简单的包,尽管它们的配置仍然需要。

捆绑我们的应用程序

为了在我们的服务器上部署应用,我们需要一个安装了最新版本的 Node.js 和 NPM 的 Linux 服务器。服务器应该和我们将要创建捆绑包的本地机器是同一平台。如果你想在另一个平台上部署你的应用,查看下一节。现在让我们通过以下步骤构建应用:

  1. 如果我们的服务器符合上述要求,我们可以在本地机器上的应用文件夹中运行以下命令:

    $ meteor build myAppBuildFolder
    
    
  2. 这将创建一个名为myAppBuildFolder的文件夹,里面有一个*.tar.gz文件。然后我们可以将这个文件上传到我们的服务器,并在例如~/Sites/myApp下提取它。然后我们进入提取的文件夹并运行以下命令:

    $ cd programs/server
    $ npm install
    
    
  3. 这将安装所有的 NPM 依赖。安装完成后,我们设置必要的环境变量:

    $ export MONGO_URL='mongodb://user:password@host:port/databasename'
    $ export ROOT_URL='http://example.com'
    $ export MAIL_URL='smtp://user:password@mailhost:port/'
    $ export PORT=8080
    
    

    export命令将设置MONGO_URLROOT_URLMAIL_URL环境变量。

  4. 由于这种手动部署没有预装 MongoDB,我们需要在我们的机器上安装它,或者使用像 Compose 这样的托管服务(mongohq.com)。如果我们更愿意自己在服务器上安装 MongoDB,我们可以遵循在docs.mongodb.org/manual/installation的指南。

  5. ROOT_URL变量应该是指向我们服务器的域的 URL。如果我们的应用发送电子邮件,我们还可以设置自己的 SMTP 服务器,或使用像 Mailgun 这样的服务(mailgun.com)并更改MAIL_URL变量中的 SMTP 主机。

    我们也可以指定我们希望应用运行的端口,使用PORT环境变量。如果我们没有设置PORT变量,它将默认使用端口80

  6. 设置这些变量后,我们转到应用的根目录,并使用以下命令启动服务器:

    $ node main.js
    
    

    提示

    如果你想确保你的应用在崩溃或服务器重启时能够重新启动,可以查看forever NPM 包,具体解释请参阅github.com/nodejitsu/forever

如果一切顺利,我们的应用应该可以通过<your server's ip>:8080访问。

如果我们手动部署应用时遇到麻烦,我们可以使用接下来的方法。

使用 Demeteorizer 部署

使用$ meteor build的缺点是,大多数 node 模块已经被编译,因此在服务器环境中可能会造成问题。因此出现了 Demeteorizer,它与$ meteor build非常相似,但还会额外解压捆绑包,并创建一个包含所有 node 依赖项和项目正确 node 版本的package.json文件。以下是使用 Demeteorizer 部署的方法:

  1. Demeteorizer 作为一个 NPM 包提供,我们可以使用以下命令安装:

    $ npm install -g demeteorizer
    
    

    注意

    如果npm文件夹没有正确的权限,请在命令前使用sudo

  2. 现在我们可以去应用文件夹并输入以下命令:

    $ demeteorizer -o ~/my-meteor-blog-converted
    
    
  3. 这将把准备分发的应用程序输出到my-meteor-blog-converted文件夹。我们只需将这个文件夹复制到我们的服务器上,设置与之前描述相同的环境变量,并运行以下命令:

    $ cd /my/server/my-meteor-blog-converted
    $ npm install
    $ node main.js
    
    

这应该会在我们指定的端口上启动我们的应用程序。

使用 Meteor Up 部署

前面的步骤可以帮助我们在自己的服务器上部署应用程序,但这种方法仍然需要我们构建、上传和设置环境变量。

Meteor Upmup)旨在使部署像运行$ meteor deploy一样简单。然而,如果我们想要使用 Meteor Up,我们需要在服务器上拥有完全的管理权限。

此外,这允许我们在应用程序崩溃时自动重新启动它,使用forever NPM 包,以及在服务器重新启动时启动应用程序,使用upstart NPM 包。我们还可以恢复先前的部署版本,这为我们提供了在生产环境部署的良好基础。

注意

接下来的步骤是针对更高级的开发人员,因为它们需要在服务器机器上设置sudo权限。因此,如果您在部署方面没有经验,可以考虑使用像 Modulus 这样的服务(modulus.io),它提供在线 Meteor 部署,使用自己的命令行工具,可在modulus.io/codex/meteor_apps找到。

Meteor Up 将按照以下方式设置服务器并部署我们的应用程序:

  1. 要在我们的本地机器上安装mup,我们输入以下命令:

    $ npm install -g mup
    
    
  2. 现在我们需要创建一个用于部署配置的文件夹,这个文件夹可以位于我们的应用程序所在的同一个文件夹中:

    $ mkdir ~/my-meteor-blog-deployment
    $ cd ~/my-meteor-blog-deployment
    $ mup init
    
    
  3. Meteor Up 为我们创建了一个配置文件,它看起来像以下这样:

    {
      "servers": [
        {
          "host": "hostname",
          "username": "root",
          "password": "password"
          // or pem file (ssh based authentication)
          //"pem": "~/.ssh/id_rsa"
        }
      ],
      "setupMongo": true,
      "setupNode": true,
      "nodeVersion": "0.10.26",
      "setupPhantom": true,
      "appName": "meteor",
      "app": "/Users/arunoda/Meteor/my-app",
      "env": {
        "PORT": 80,
        "ROOT_URL": "http://myapp.com",
        "MONGO_URL": "mongodb://arunoda:fd8dsjsfh7@hanso.mongohq.com:10023/MyApp",
        "MAIL_URL": "smtp://postmaster%40myapp.mailgun.org:adj87sjhd7s@smtp.mailgun.org:587/"
      },
      "deployCheckWaitTime": 15
    }
    
  4. 现在我们可以编辑这个文件以适应我们的服务器环境。

  5. 首先,我们将添加 SSH 服务器认证。我们可以提供我们的 RSA 密钥文件,或者提供一个用户名和密码。如果我们想要使用后者,我们需要安装sshpass,一个用于在不使用命令行的前提下提供 SSH 密码的工具:

    "servers": [
        {
          "host": "myServer.com",
          "username": "johndoe",
          "password": "xyz"
          // or pem file (ssh based authentication)
          //"pem": "~/.ssh/id_rsa"
        }
    ],
    

    注意

    要为我们的环境安装sshpass,我们可以按照gist.github.com/arunoda/7790979的步骤进行,或者如果您在 Mac OS X 上,可以查看www.hashbangcode.com/blog/installing-sshpass-osx-mavericks

  6. 接下来,我们可以设置一些选项,例如选择在服务器上安装 MongoDB。如果我们使用像 Compose 这样的服务,我们将将其设置为false

    "setupMongo": false,
    

    如果我们已经在我们的服务器上安装了 Node.js,我们还将将下一个选项设置为false

    "setupNode": false,
    

    如果我们想要指定一个特定的 Node.js 版本,我们可以如下设置:

    "nodeVersion": "0.10.25",
    

    Meteor Up 还可以为我们安装 PhantomJS,这对于我们使用 Meteor 的 spiderable 包是必要的,这个包可以使我们的应用程序被搜索引擎爬取:

    "setupPhantom": true,
    

    在下一个选项中,我们将设置我们应用程序的名称,它可以与我们的应用程序文件夹名称相同:

    "appName": "my-meteor-blog",
    

    最后,我们需要指向我们的本地应用程序文件夹,以便 Meteor Up 知道要部署什么:

    "app": "~/my-meteor-blog",
    
  7. Meteor Up 还允许我们预设所有必要的环境变量,例如正确的MONGO_URL变量:

    "env": {
        "ROOT_URL": "http://myServer.com",
        "MONGO_URL": "mongodb://user:password@host:port/databasename",
        "PORT": 8080
    },
    
  8. 最后一个选项设置了 Meteor Up 在检查应用是否成功启动前会等待的时间:

    "deployCheckWaitTime": 15
    

设置服务器

为了使用 Meteor Up 设置服务器,我们需要对sudo进行无密码访问。按照以下步骤设置服务器:

  1. 为了启用无密码访问,我们需要将当前用户添加到服务器的sudo组中:

    $ sudo adduser <username> sudo
    
    
  2. 然后在sudoers文件中添加NOPASSWD

    $ sudo visudo
    
    
  3. 现在用以下这行替换%sudo ALL=(ALL) ALL行:

    %sudo ALL=(ALL) NOPASSWD:ALL
    
    

使用 mup 部署

如果一切顺利,我们可以设置我们的服务器。以下步骤解释了如何使用mup进行部署:

  1. 从本地my-meteor-blog-deployment目录中运行以下命令:

    $ mup setup
    
    

    这将配置我们的服务器并安装配置文件中选择的全部要求。

    一旦这个过程完成,我们随时可以通过在同一目录下运行以下命令来部署我们的应用:

    $ mup deploy
    
    

通过创建两个具有不同应用名称的 Meteor Up 配置,我们还可以创建生产和演示环境,并将它们部署到同一服务器上。

前景

目前,Meteor 将原生部署限制在其自己的服务器上,对环境控制有限。计划推出一款企业级服务器基础设施,名为Galaxy,它将使部署和扩展 Meteor 应用像 Meteor 本身一样简单。

尽管如此,凭借 Meteor 的简洁性和强大的社区,我们已经拥有部署到任何基于 Node.js 的托管和 PaaS 环境的丰富工具集。

注意

例如,如果我们想在 Heroku 上部署,我们可以查看 Jordan Sissel 在github.com/jordansissel/heroku-buildpack-meteor上的构建包。

总结

在本章中,我们学习了如何部署 Meteor,以及在 Meteor 自己的服务器架构上部署可以有多么简单。我们还使用了 Demegorizer 和 Meteor Up 这样的工具来部署我们自己的服务器架构。

要了解更多具体的部署方法,请查看以下资源:

您可以在这个应用的完整示例代码中找到准备部署的版本,或者在 GitHub 上查看github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter10

在下一章中,我们将创建一个包含我们之前创建的ReactiveTimer对象的包,并将其发布到 Meteor 的官方包仓库。

第十一章:构建我们自己的包

在本章中,我们将学习如何构建自己的包。编写包允许我们创建可以共享在许多应用中的闭合功能组件。在本章的后半部分,我们将把我们的应用发布到 Atmosphere,Meteor 的第三方包仓库,地址为atmospherejs.com

在本章中,我们将涵盖以下主题:

包的结构

包是一个包含特定变量暴露给 Meteor 应用的 JavaScript 文件集合。除了在 Meteor 应用中,包文件将按我们指定的加载顺序加载。

每个包都需要一个package.js文件,该文件包含该包的配置。在这样的文件中,我们可以添加一个名称、描述和版本,设置加载顺序,并确定哪些变量应该暴露给应用。此外,我们还可以为我们的包指定单元测试来测试它们。

package.js文件的一个例子可能看起来像这样:

Package.describe({
  name: "mrt:moment",
  summary: "Moment.js, a JavaScript date library.",
  version: "0.0.1",
  git: "https://..."
});

Package.onUse(function (api, where) {
  api.export('moment');

  api.addFiles('lib/moment-with-langs.min.js', 'client');
});

Package.onTest(function(api){
  api.use(["mrt:moment", "tinytest"], ["client", "server"]);
  api.addFiles("test/tests.js", ["client", "server"]);
});

我们可以按照自己的意愿结构包中的文件和文件夹,但以下安排是一个好的基础:

包的结构

  • tests:包含包的单元测试和tests.js文件

  • lib:包含包使用的第三方库

  • README.md:包含使用包的简单说明

  • package.js: 此文件包含包的元数据

  • myPackage.js:这些是包含包代码的一个或多个文件

要测试一个包,我们可以使用 Meteor 的tinytest包,它是一个简单的单元测试包。如果我们有测试,我们可以使用以下命令运行它们:

$ meteor test-packages <my package name>

这将启动一个 Meteor 应用,地址为http://localhost:3000,它运行我们的包测试。要了解如何编写一个包,请查看下一章。

创建自己的包

要创建自己的包,我们将使用我们在第九章,高级反应性中构建的ReactiveTimer对象:

  1. 我们来到终端,在我们的应用文件夹中运行以下命令:

    $ meteor create --package reactive-timer
    
    
  2. 这将创建一个名为packages的文件夹,其中有一个reactive-timer文件夹。在reactive-timer文件夹内,Meteor 已经创建了一个package.js文件和一些示例包文件。

  3. 现在我们可以删除reactive-timer文件夹内的所有文件,除了package.js文件。

  4. 然后我们将我们在第九章 高级反应性中创建的my-meteor-blog/client/ReactiveTimer.js文件移动到我们新创建的reactive-timer包文件夹中。

  5. 最后,我们打开复制的ReactiveTimer.js文件,并删除以下行:

    timer = new ReactiveTimer();
    timer.start(10);
    

    稍后,我们在应用本身内部实例化timer对象,而不是在包文件中。

现在我们应该有一个简单的文件夹,带有默认的package.js文件和我们的ReactiveTimer.js文件。这几乎就是全部了!我们只需要配置我们的包,就可以在应用中使用它了。

添加包元数据

要添加包的元数据,我们打开名为package.js的文件,并添加以下代码行:

Package.describe({
  name: "meteor-book:reactive-timer",
  summary: "A simple timer object, which can re-run reactive functions based on an interval",
  version: "0.0.1",
  // optional
  git: "https://github.com/frozeman/meteor-reactive-timer"
});

这为包添加了一个名称、一个描述和一个版本。

请注意,包名称与作者的名称命名空间。这样做的目的是,通过它们的作者名称,可以使具有相同名称的包区分开来。在我们这个案例中,我们选择meteor-book,这并不是一个真实的用户名。要发布包,我们需要使用我们真实的 Meteor 开发者用户名。

Package.describe()函数之后是实际的包依赖关系:

Package.onUse(function (api) {
  // requires Meteor core packages 1.0
  api.versionsFrom('METEOR@1.0');

  // we require the Meteor core tracker package
  api.use('tracker', 'client');

  // and export the ReactiveTimer variable
  api.export('ReactiveTimer');

  // which we find in this file
  api.addFiles('ReactiveTimer.js', 'client');
});

在这里,我们定义了这个包应该使用的 Meteor 核心包的版本:

  • 使用api.use(),我们定义了这个包依赖的额外包(或包)。请注意,这些依赖不会被使用这个包的应用本身访问到。

    注意

    另外,还存在api.imply(),它不仅使另一个包在包的文件中可用,而且还将它添加到 Meteor 应用本身,使其可以被应用的代码访问。

  • 如果我们使用第三方包,我们必须指定最低的包版本,如下所示:

    api.use('author:somePackage@1.0.0', 'server');
    
    

    注意

    我们还可以传入第三个参数,{weak: true},以指定只有在开发者已经将依赖包添加到应用中时,才会使用该依赖包。这可以用来在有其他包存在时增强一个包。

  • api.use()函数的第二个参数中,我们可以指定是否在客户端、服务器或两者上都加载它,使用数组:

    api.use('tracker', ['client', 'server']);
    
    

    提示

    我们实际上不需要导入Tracker包,因为它已经是 Meteor 核心meteor-platform包的一部分(默认添加到任何 Meteor 应用中);我们在这里这样做是为了示例。

  • 然后我们使用api.export('ReactiveTimer')来定义包中应该向使用此包的 Meteor 应用公开哪个变量。记住,我们在ReactiveTimer.js文件中使用以下代码行创建了ReactiveTimer对象:

    ReactiveTimer = (function () {
      ...
    })();
    

    注意

    请注意,我们没有使用var来创建变量。这样,它在包的所有其他文件中都可以访问,也可以暴露给应用本身。

  • 最后,我们使用api.addFiles()告诉包系统哪些文件属于这个包。我们可以有api.addFiles()的多个调用,一个接一个。这个顺序将指定文件的加载顺序。

    在这里,我们再次告诉 Meteor 将文件加载到哪个地方——客户端、服务器还是两者都加载——使用['client', 'server']

    在这个例子中,我们只在客户端提供了ReactiveTimer对象,因为 Meteor 的反应式函数只存在于客户端。

    注意

    如果你想要查看api对象的所有方法,请查看 Meteor 的文档docs.meteor.com/#packagejs

添加包

将包文件夹复制到my-meteor-blog/packages文件夹中并不足以让 Meteor 使用这个包。我们需要遵循额外的步骤:

  1. 为了添加包,我们需要从终端前往我们的应用文件夹,停止任何正在运行的meteor实例,并运行以下命令:

    $ meteor add meteor-book:reactive-timer
    
    
  2. 然后,我们需要在我们的应用中实例化ReactiveTimer对象。为此,我们需将以下代码行添加到我们的my-meteor-blog/main.js文件中:

    if(Meteor.isClient) {
        timer = new ReactiveTimer();
        timer.start(10);
    }
    
  3. 现在我们可以再次使用$ meteor启动 Meteor 应用,并在http://localhost:3000打开我们的浏览器。

我们应该看不到任何区别,因为我们只是用我们meteor-book:reactive-timer包中的ReactiveTimer对象替换了应用中原本的ReactiveTimer对象。

为了看到计时器运行,我们可以打开浏览器的控制台并运行以下的代码片段:

Tracker.autorun(function(){
    timer.tick();
    console.log('timer run');
});

这应该会每 10 秒记录一次timer run,显示我们的包实际上是在工作的。

发布我们的包给公众

向世界发布一个包是非常容易的,但为了让人们使用我们的包,我们应该添加一个 readme 文件,这样他们就可以知道如何使用我们的包。

在我们之前创建的包文件夹中创建一个名为README.md的文件,并添加以下的代码片段:

# ReactiveTimer

This package can run reactive functions in a given interval.
## Installation

    $ meteor add meteor-book:reactive-timer

## Usage

To use the timer, instantiate a new interval:

    var myTimer = new ReactiveTimer();

Then you can start an interval of 10 seconds using:

    myTimer.start(10);

To use the timer just call the following in any reactive function:

    myTimer.tick();

To stop the timer use:

    myTimer.stop();

正如我们所见,这个文件使用了 Markdown 语法。这样,它将在 GitHub 和atmospherejs.com上看起来很不错,这是一个你可以浏览所有可用 Meteor 包的网站。

通过这个 readme 文件,我们将使其他人更容易使用我们的包并欣赏我们的工作。

在线发布我们的包

在我们保存了readme文件之后,我们可以将这个包推送到 GitHub 或其他的在线 Git 仓库,并将仓库的 URL 添加到package.js文件的Package.describe({git: …})变量中。将代码托管在 GitHub 上可以保证它的安全性,并允许他人进行分叉和改进。下面让我们来进行将我们的包推送到线上的步骤:

  1. 发布我们的包,我们可以在终端的pages文件夹内简单地运行以下命令:

    $ meteor publish --create
    
    

    这会构建并捆绑包,然后上传到 Meteor 的包服务器上。

  2. 如果一切顺利,我们应该能够通过输入以下命令找到我们的包:

    $ meteor search reactive-timer
    
    

    这在下面的截图中有所说明:

    在线发布我们的包

  3. 然后,我们可以使用以下命令显示找到的包的所有信息:

    $ meteor show meteor-book:reactive-timer
    
    

    这在上面的截图中说明:

    在线发布我们的包

  4. 要使用来自 Meteor 服务器的包版本,我们只需将packages/reactive-timer文件夹移到别处,删除package文件夹,然后运行$ meteor来启动应用程序。

    现在 Meteor 在packages文件夹中找不到具有该名称的包,并将在线查找该包。既然我们发布了它,它将被下载并用于我们的应用程序。

  5. 如果我们想在我们的应用程序中使用我们包的特定版本,我们可以在终端中从我们应用程序的文件夹内运行以下命令:

    $ meteor add meteor-book:reactive-timer@=0.0.1
    
    

现在我们的包已经发布,我们可以在http://atmospherejs.com/meteor-book/reactive-timer看到它,如下所示:

在线发布我们的包

注意

请注意,这只是一个包的示例,并且从未实际发布过。然而,在atmospherejs.com/frozeman/reactive-timer以我的名义发布的这个包的版本可以找到。

更新我们的包

如果我们想发布我们包的新版本,我们只需在package.js文件中增加版本号,然后从packages文件夹内使用以下命令发布新版本:

$ meteor publish

要使我们的应用程序使用我们包的最新版本(只要我们没有指定固定版本),我们只需在终端内从我们的应用程序文件夹中运行以下命令:

$ meteor update meteor-book:reactive-timer

如果我们想更新所有包,我们可以运行以下命令:

$ meteor update –-packages-only

总结

在本章中,我们从我们的ReactiveTimer对象创建了自己的包。我们还了解到,在 Meteor 的官方打包系统中发布包是多么简单。

要深入了解,请阅读以下资源中的文档:

您可以在www.packtpub.com/books/content/support/17713或 GitHub 上github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter11找到本章的代码示例。

这个代码示例只包含包,所以为了将其添加到应用程序中,请使用前一章的代码示例。

在下一章中,我们将查看测试我们的应用程序和包。

第十二章:Meteor 中的测试

在这个最后的章节中,我们将讨论我们如何测试一个 Meteor 应用。

测试是一个广泛的话题,超出了本章的范围。为了简化,我们将简要介绍两种可用的工具,因为它们确实不同,并为每种工具提供一个简单的示例。

在本章中,我们将介绍以下主题:

测试类型

测试是用来验证其他代码或应用功能的代码片段。

我们可以将测试分为四个主要组:

  • 单元测试:在这个测试中,我们只测试我们代码的小单元。这可以,例如,是一个函数或一段代码。单元测试不应该调用其他函数,向硬盘或数据库写入,或访问网络。如果需要这样的功能,应该编写桩函数,这些函数返回期望的值而不调用真正的函数。

  • 集成测试:在这个测试中,我们将多个测试结合起来,在不同的环境中运行它们,以确保它们仍然有效。与单元测试相比,这个测试的不同之处在于,我们实际上是在运行连接的功能,比如调用数据库。

  • 功能测试:这可以是单元测试或界面测试,但只测试功能特性/函数的功能,而不检查副作用,例如是否适当地清理了变量。

  • 验收测试:这个测试在完整的系统上运行,例如,一个网络浏览器。想法是尽可能地模仿实际用户。这些测试与定义功能的用户故事非常相似。这种测试的缺点是,它使得追踪错误变得困难,因为测试发生在较高的层次。

在下面的示例中,我们主要会为了简化而编写功能测试。

测试包

在上一章中,我们基于 ReactiveTimer 对象构建了一个包。一个好的包应该总是包含单元测试,这样人们就可以运行它们,并确信对该包所做的更改不会破坏其功能。

Meteor 为包提供了一个简单的单元测试工具,称为 TinyTest,我们将使用它来测试我们的包:

  1. 要添加测试,我们需要将我们在上一章中构建的meteor-book:reactive-timer包复制到我们应用的my-meteor-blog/packages文件夹中。这样,我们可以修改包,因为 Meteor 将优先选择packages文件夹中的包而不是其包服务器中的包。如果你移除了包,只需使用以下命令将其重新添加:

    $ meteor add meteor-book:reactive-timer
    
    

    注意

    此外,我们需要确保我们删除my-meteor-blog/client/ReactiveTimer.js文件,如果我们使用了来自第十章 部署我们的应用 的代码示例作为基础的话,我们应该有的。

  2. 然后我们打开我们packages文件夹中的package.js文件,并在文件的末尾添加以下几行代码:

    Package.onTest(function (api) {
      api.use('meteor-book:reactive-timer', 'client');
      api.use('tinytest', 'client');
    
      api.addFiles('tests/tests.js', 'client');
    });
    

    这将包括我们的meteor-book:reactive-timer包和tinytest,在运行测试时。然后它将运行tests.js文件,其中将包含我们的单元测试。

  3. 现在,我们可以通过在我们的包文件夹中添加一个名为tests的文件夹,并在其中创建一个名为tests.js的文件来创建测试。

    目前,tinytest包没有被 Meteor 文档化,但它很小,这意味着它非常简单。

    基本上,有两个函数,Tinytest.add(test)Tinytest.addAsync(test, expect)。它们都运行一个简单的测试函数,我们可以使用test.equal(x, y)test.isTrue(x),或test.isUndefined(x)来通过或失败这个函数。

    对于我们的包测试,我们将简单地测试在启动计时器后ReactiveTimer._intervalId是否不再为 null,这样我们就可以知道计时器是否运行了。

添加包测试

测试首先描述将要测试的内容。

要测试_intervalId,我们在我们的tests.js文件中添加以下几行代码:

Tinytest.add('The timer set the _intervalId property', function (test) {
    var timer = new ReactiveTimer();
    timer.start(1);

    test.isTrue(timer._intervalId !== null);

    timer.stop();
});

然后我们启动一个计时器,并测试其_intervalId属性是否不再为 null。最后,我们再次停止计时器以清理测试。

接下来,我们将把我们tests.js文件中要添加的下一个测试设置为异步,因为我们需要等待计时器至少运行一次:

Tinytest.addAsync('The timer run', function (test, expect) {
    var run = false,
        timer = new ReactiveTimer();
    timer.start(1);

    Tracker.autorun(function(c){
        timer.tick();

        if(!c.firstRun)
            run = true;
    });

    Meteor.setTimeout(function(){
        test.equal(run, true);
        timer.stop();

        expect();
    }, 1010);
});

让我们来看看这个异步测试中发生了什么:

  • 首先,我们再次以 1 秒的间隔启动计时器,并创建了一个名为run的变量。我们只在我们的反应式Tracker.autorun()函数运行时将这个变量切换为true。请注意,我们使用了if(!c.firstRun)来防止在函数第一次执行时设置run变量,因为我们只希望在 1 秒后的“滴答”计数。

  • 然后我们使用Meteor.setTimeout()函数检查run是否被更改为trueexpect()告诉Tinytest.addAsync()测试已经结束并输出结果。请注意,我们还停止了计时器,因为我们需要在每个测试后清理。

运行包测试

要最终运行测试,我们可以从我们应用的根目录运行以下命令:

$ meteor test-packages meteor-book:reactive-timer

这将启动一个 Meteor 应用并运行我们的包测试。要查看它们,我们导航到http://localhost:3000

运行包测试

提示

我们也可以通过命名由空格分隔的多个包来同时运行一个以上的包测试:

$ meteor test-packages meteor-book:reactive-timer iron:router

为了看看测试是否有效,我们将通过注释掉 my-meteor-book/packages/reactive-timer/ReactiveTimer.js 文件中的 Meteor.setInterval() 来故意使它失败,如下所示:

运行包测试

我们应该始终尝试使我们的测试失败,因为一个测试也可能是编写成永远不会成功或失败的方式(例如,当 expect() 从未被调用时)。这将阻止其他测试的执行,因为当前的测试可能永远不会完成。

一个好的经验法则是,测试功能时要好像我们正在看一个黑箱。如果我们根据函数是如何编写的来过度定制我们的测试,那么在我们改进函数时修复测试会比较困难。

测试我们的 Meteor 应用

为了测试应用本身,我们可以使用 Velocity Meteor 的官方测试框架。

Velocity 本身不包含测试工具,而是为诸如 Jasmine 或 Mocha 等测试包提供了一种统一的方式来测试 Meteor 应用,并使用 velocity:html-reporter 包在控制台或应用界面本身报告它们的输出。

让我们引用他们自己的话:

Velocity 监控您的 tests/ 目录,并将测试文件发送到正确的测试插件。测试插件执行测试,并在完成后将每个测试的结果发送回 Velocity。然后 Velocity 结合所有测试插件的结果,并通过一个或多个报告插件输出它们。当应用或测试发生变化时,Velocity 将重新运行您的测试并反应性地更新结果。

这段内容来自 velocity.meteor.com。此外,Velocity 还增加了诸如 Meteor 存根和自动存根等功能。它能够为隔离测试创建镜像应用,并运行设置代码(测试数据)。

现在,我们将查看使用 Jasmine 的单元测试和使用 Nightwatch 的验收测试。

使用 Jasmine 测试

为了使用 Jasmine 和 Velocity,我们需要安装 sanjo:jasmine 包以及 velocity:html-reporter 包。

为此,我们将从我们的 apps 文件夹内运行以下命令:

$ meteor add velocity:html-reporter

然后,我们使用以下命令为 Meteor 安装 Jasmine:

$ meteor add sanjo:jasmine

为了让 Velocity 能够找到测试,我们需要创建以下文件结构:

- my-meteor-blog
  - tests
    - jasmine
    - client
      - unit
      - integration
    - server
      - unit

现在,当我们使用 $ meteor 启动 Meteor 服务器时,我们会发现 Jasmine 包已经在 /my-meteor-blog/tests/jasmine/server/unit 文件夹中创建了两个文件,其中包含我们包的存根。

向服务器添加单元测试

现在我们可以向客户端和服务器添加单元测试。在这本书中,我们将只向服务器添加一个单元测试,稍后向客户端添加集成测试,以保持在本书章节的范围内。这样做步骤如下:

  1. 首先,我们在 /my-meteor-blog/tests/jasmine/server/unit 文件夹中创建一个名为 postSpecs.js 的文件,并添加以下命令:

    describe('Post', function () {
    

    这将创建一个描述测试内部将涉及什么的测试框架。

  2. 在测试框架内,我们调用beforeEach()afterEach()函数,这两个函数分别在每个测试之前和之后运行。在其中,我们将使用MeteorStubs.install()为所有的 Meteor 函数创建桩,并使用MeteorStubs.uninstall()之后清理它们:

    beforeEach(function () {
        MeteorStubs.install();
    });
    
    afterEach(function () {
        MeteorStubs.uninstall();
    });
    

    注意

    桩是一个模仿其原始函数或对象的功能或对象,但不会运行实际代码。相反,桩可以用来返回函数我们测试依赖的特定值。

    桩确保单元测试只测试特定的代码单元,而不是它的依赖。否则,依赖函数或对象的一个断裂会导致其他测试链失败,使得找到实际问题变得困难。

  3. 现在我们可以编写实际的测试。在这个例子中,我们将测试我们之前在书中创建的insertPost方法是否插入了帖子,并确保不会插入重复的 slug:

    it('should be correctly inserted', function() {
    
        spyOn(Posts, 'findOne').and.callFake(function() {
            // simulate return a found document;
            return {title: 'Some Tite'};
        });
    
        spyOn(Posts, 'insert');
    
        spyOn(Meteor, 'user').and.returnValue({_id: 4321, profile: {name: 'John'}});
    
        spyOn(global, 'moment').and.callFake(function() {
            // simulate return the moment object;
            return {unix: function(){
                return 1234;
            }};
        });
    

    首先,我们为insertPost方法中使用的所有函数创建桩,以确保它们返回我们想要的结果。

    特别是,看看spyOn(Posts, "findOne")调用。正如我们可以看到的,我们调用了一个假函数,并返回了一个只有标题的假文档。实际上,我们可以返回任何东西,因为insertPost方法只检查是否找到了具有相同 slug 的文档。

  4. 接下来,我们实际上调用该方法并给它一些帖子数据:

        Meteor.call('insertPost', {
            title: 'My Title',
            description: 'Lorem ipsum',
            text: 'Lorem ipsum',
            slug: 'my-title'
        }, function(error, result){
    
  5. 在方法的回调内,我们添加了实际的测试:

            expect(error).toBe(null);
    
            // we check that the slug is returned
            expect(result).toContain('my-title');
            expect(result.length).toBeGreaterThan(8);
    
            // we check that the post is correctly inserted
            expect(Posts.insert).toHaveBeenCalledWith({
                title: 'My Title',
                description: 'Lorem ipsum',
                text: 'Lorem ipsum',
                slug: result,
                timeCreated: 1234,
                owner: 4321,
                author: 'John'
            });
        });
    });
    

    首先,我们检查错误对象是否为 null。然后我们检查方法生成的 slug 是否包含'my-title'字符串。因为我们在较早的Posts.findOne()函数中返回了一个假文档,所以我们期望我们的方法会给 slug 添加一些随机数,比如'my-title-fotvadydf4rt3xr'。因此,我们检查其长度是否大于原始'my-title'字符串的八个字符。

    最后,我们检查Post.insert()函数是否被调用了期望的值。

    注意

    为了完全理解如何测试 Jasmine,请查看文档jasmine.github.io/2.0/introduction.html

    你也可以在www.cheatography.com/citguy/cheat-sheets/jasmine-js-testing找到一个很好的 Jasmine 函数速查表。

  6. 最后,我们关闭开始时的describe(...函数:

    });
    

如果我们现在再次使用$ meteor启动我们的 Meteor 应用,过一会儿我们会在右上角看到一个绿色点。

点击这个点可以让我们访问 Velocity 的html-reporter,它应该能显示我们的测试已经通过:

向服务器添加单元测试

为了使我们的测试失败,让我们去到我们的my-meteor-blog/methods.js文件,并将以下行注释掉:

if(Posts.findOne({slug: postDocument.slug}))
    postDocument.slug = postDocument.slug +'-'+ Math.random().toString(36).substring(3);

这将防止 slug 被更改,即使已经存在具有相同 slug 的文档,也会使我们的测试失败。如果我们回头在浏览器里检查,我们应该会看到测试失败:

向服务器添加单元测试

我们只需通过添加新的it('应该是什么', function() {...});函数来添加更多测试。

向客户端添加集成测试

添加集成测试与添加单元测试一样简单。区别在于所有的测试规格文件都放到my-meteor-blog/tests/jasmine/client/integration文件夹里。

与单元测试不同,集成测试在实际应用环境中运行。

为访客添加测试

在我们第一个示例测试中,我们将测试确保访客看不到创建文章按钮。在第二个测试中,我们将以管理员身份登录,检查我们是否能看到它。

  1. 让我们在我们my-meteor-blog/tests/jasmine/client/integration文件夹里创建一个名为postButtonSpecs.js的文件。

  2. 现在我们向文件添加以下代码片段并保存它:

    describe('Vistors', function() {
        it('should not see the create posts link', function () {
            var div = document.createElement('DIV');
            Blaze.render(Template.home, div);
    
            expect($(div).find('a.createNewPost')[0]).not.toBeDefined();
        });
    });
    
postButtonSpecs.js file as the one we used before:
describe('The Admin', function() {
    afterEach(function (done) {
        Meteor.logout(done);
    })

    it('should be able to login and see the create post link', function (done) {
        var div = document.createElement('DIV');
        Blaze.render(Template.home, div);

        Meteor.loginWithPassword('johndoe@example.com', '1234', function (err) {

            Tracker.afterFlush(function(){

              expect($(div).find('a.createNewPost')[0]).toBeDefined();
                expect(err).toBeUndefined();

                done();
            });

        });
    });
});

这里我们再次向一个div中添加home模板,但这次我们使用管理员凭据以管理员身份登录。登录后,我们调用Tracker.afterFlush()给 Meteor 时间重新渲染模板,然后检查按钮是否现在出现。

因为这个测试是异步运行的,我们需要调用done()函数,这个函数是作为it()函数的参数传递的,告诉 Jasmine 测试结束了。

注意

由于 Meteor 不会把文件捆绑在tests目录里,我们测试文件里的凭据是安全的。

如果我们现在回到浏览器,我们应该会看到两个集成测试通过了:

为管理员添加测试

创建测试后,我们总是应该确保尝试失败测试以查看它是否真的工作。为此,我们只需在my-meteor-blog/client/templates/home.html中注释掉a.createNewPost链接。

注意

你可以使用 PhantomJS 如下运行 Velocity 测试:

$ meteor run --test

首先需要全局安装 PhantomJS,使用$ npm install -g phantomjs。请注意,撰写此书时此特性是实验性的,可能运行不了你的所有测试。

验收测试

尽管我们可以用这些测试分别测试客户端和服务器代码,但我们不能测试两者之间的交互。为此,我们需要验收测试,如果详细解释,将超出本章节的范围。

在撰写本文的时候,还没有使用 Velocity 实施的验收测试框架,尽管有两个你可以使用。

Nightwatch

clinical:nightwatch包让你能简单地运行验收测试,如下所示:

"Hello World" : function (client) {
     client
        .url("http://127.0.0.1:3000")
        .waitForElementVisible("body", 1000)
        .assert.title("Hello World")
        .end();
}

尽管安装过程不像安装 Meteor 包那样直接,但在运行测试之前,你自己需要安装并运行 MongoDB 和 PhantomJS。

如果你想尝试一下,请查看 atmosphere-javascript 网站上的包:atmospherejs.com/clinical/nightwatch

Laika

如果你想测试服务器与客户端之间的通信,可以使用 Laika。它的安装过程与 Nightwatch 相似,因为它需要单独安装 MongoDB 和 PhantomJS。

Laika 启动一个服务器实例并连接多个客户端。然后你可以设置订阅或插入并修改文档。你还可以测试它们在客户端的外观。

要安装 Laika,请访问arunoda.github.io/laika/

注意

在撰写本文时,Laika 与 Velocity 不兼容,后者试图在 Laika 的环境中运行测试文件夹中的所有文件,导致错误。

总结

在这最后一章中,我们学习了如何使用 Meteor 官方测试框架 Velocity 的sanjo:jasmine包编写简单的单元测试。我们还简要介绍了可能的验收测试框架。

如果你想更深入地了解测试,可以查看以下资源:

你可以在这本书的代码文件在www.packtpub.com/books/content/support/17713或者在 GitHub 上github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter12找到。

既然你已经读完了整本书,我假设你对 Meteor 的了解比以前多了很多,对这个框架也和我一样兴奋!

关于 Meteor 的任何问题,你都可以在stackoverflow.com上提问,那里有一个很棒的 Meteor 社区。

我还建议阅读www.meteor.com/projects上的所有 Meteor 子项目,并研究docs.meteor.com上的文档。

希望你能享受阅读这本书的过程,现在你已经准备好使用 Meteor 框架来制作伟大的应用程序了!

附录 A. 附录

附录中包含 Meteor 命令行工具的命令列表和对iron:router钩子的简要描述。

命令行工具命令列表

选项 描述
run 使用meteor run与使用meteor相同。这将为我们应用启动一个 Meteor 服务器并监控文件更改。
create <名称> 这将通过创建一个同名的文件夹来初始化一个 Meteor 项目,并有一些初始文件。
update 这将更新我们当前的 Meteor 应用到最新版本。我们还可以使用meteor update --release xyz来将我们的 Meteor 应用修复到一个特定的版本。
deploy <站点名称> 这将把我们的 Meteor 应用部署到<站点名称>.meteor.com。我们可以传递--delete选项来删除一个已部署的应用
build <文件夹名称> 这将创建一个文件夹,其中包含我们捆绑的应用代码,准备部署到我们自己的服务器。
add/remove <包名称> 这将向/从我们的项目中添加或删除一个 Meteor 核心包。
list 这将列出我们的应用正在使用的所有 Meteor 包。
mongo 这会让我们访问本地 MongoDB shell。我们同时还需要启动我们的应用meteor run。如果我们需要访问部署在meteor.com上的应用的 mongo 数据库,使用$ meteor mongo yourapp.meteor.com --url但要小心,这些凭据仅有效 1 分钟。
reset 这将把我们的本地开发数据库重置为空白状态。当我们的应用运行时此操作将无效。注意这将删除我们存储在本地数据库中的所有数据。
logs <站点名称> 这将下载并显示我们在<站点名称>.meteor.com部署的应用的日志。
search 这会搜索包含指定正则表达式的 Meteor 包和发布版本。
show 这会显示有关特定包或版本的更多信息:名称、摘要、其维护者的用户名,以及(如果指定)其主页和 Git URL。
publish 这会发布我们的包。我们之前必须使用 cd 命令进入包文件夹,使用$ meteor login登录到我们的 Meteor 账户。要第一次发布一个包,我们使用$ meteor publish --create
publish-for-arch 这会从不同的架构发布一个现有包版本的构建。我们的机器必须有正确的架构才能为特定架构发布。目前,Meteor 支持的架构有 32 位 Linux、64 位 Linux 和 Mac OS。Meteor deploy运行的服务器使用 64 位 Linux。
publish-release 这会发布 Meteor 的一个版本。这需要一个 JSON 配置文件。更多详细信息,请访问docs.meteor.com/#/full/meteorpublishrelease
claim 这会将使用旧 Meteor 版本的站点通过我们的 Meteor 开发者账户进行认领。
login 这会将我们登录到 Meteor 开发者账户。
logout 这会将我们登出 Meteor 开发者账户。
whoami 这会打印我们 Meteor 开发者账户的用户名。
test-packages 这将运行一个或多个包的测试。有关更多信息,请参阅第十二章, 使用 Meteor 进行测试
admin 此部分用于捕获需要授权才能使用的各种命令。Meteor admin的一些示例用途包括添加和删除包维护者以及为包设置主页。它还包括用于管理 Meteor 版本的各种帮助函数。

铁轨:路由钩子 |

以下表格包含路由控制器钩子的列表: |

action 这个函数可以覆盖路由的默认行为。如果我们定义这个函数,我们必须手动使用this.render()渲染模板。
onBeforeAction 这个函数在路由渲染前运行。在这里,我们可以放置额外的自定义操作。
onAfterAction 这个函数在路由渲染后运行。在这里,我们可以放置额外的自定义操作。
onRun 当路由第一次加载时,此函数运行一次。在热代码重载或再次导航相同的 URL 时,此函数不会再次运行。
onRerun 每次调用此路由时,此函数将被调用。
onStop 当离开当前路由到新路由时,此函数运行一次。
subscriptions 这个函数可以返回影响this.ready()在动作钩子中的订阅。
waitOn 这个函数可以返回订阅,但在那些准备好之前会自动渲染loadingTemplate
data 此函数的返回值将设置为此路由模板的数据上下文。

这些钩子的完整解释可以在以下资源中找到: |

posted @ 2024-05-23 14:42  绝不原创的飞龙  阅读(35)  评论(0编辑  收藏  举报