Meteor-Web-应用开发秘籍-全-
Meteor Web 应用开发秘籍(全)
原文:
zh.annas-archive.org/md5/c00c8ea8c646c329d33a9f8bc4d3721e译者:飞龙
前言
停下来,思考一下你上一次感到 高兴 的时候。
我敢打赌,无论那种愉快的体验是什么,它与新的 JavaScript 框架无关。好吧,这一切即将改变。
Meteor 不仅仅是一个你几周后就会忘记名字的 JavaScript 框架。它是被一群真正的 计算机科学家 创建的,并且不断得到改进,他们被软件开发应该如何进行的愿景所驱动。他们的座右铭是“编程应该是有趣的”,随着你浏览这个 Meteor 食谱列表,你将确切地看到使用 Meteor 开发应用程序有多么有趣。
Meteor 是模块化和声明式的,支持数据在传输中,得到了一个繁荣的开发社区的广泛支持,并实现了全栈反应性。这听起来像是一堆毫无意义的时髦词汇,直到你完全理解它们对你日常开发工作的影响。
由于 Meteor 是模块化的并且得到了良好的支持,它可以轻松地与所有你喜欢的 JavaScript 框架一起工作。你可以使用整个 Meteor 堆栈,或者你可以混合搭配社区包来补充你现有的基础设施/技能集。
Meteor 通过分布式数据协议 (DDP) 支持数据在传输中。这一创新意义重大,因为它允许你创建优雅、强大的客户端应用程序,负责自己的渲染。你不再需要依赖复杂、过时的服务器技术,这些技术充满了状态和渲染问题,(坦白说) 这些问题本应该在几年前就得到解决。DDP 不仅属于 Meteor,甚至也不属于 JavaScript。DDP 客户端几乎在每种主要的编程语言中都有!这为许多优雅、强大的集成可能性打开了大门,允许你在现有的平台和基础设施上构建。
将 DDP 与全栈反应性相结合,为开发前端应用开辟了全新的方式。模板、数据库事务和视图/控制器逻辑得到了极大的简化,使你可以编写干净、简洁、声明式的代码。随着你不再担心繁琐的状态和 CRUD 操作,专注于快速、优雅的原型设计,你的前端开发工作将从 几周 减少到 几小时。
你对 Meteor 越熟悉,作为开发者的效率和创造力就会越高。使用 Meteor 框架进行编程确实是一种愉快的体验。本书中找到的食谱将帮助你顺利完成每个项目,并成为未来许多项目的不可或缺的参考资料。
本书涵盖的内容
第一章,优化你的工作流程,将带你了解 Meteor 开发工作流程的所有方面。它包括可重复、一致的项目模板的最佳实践;定制你的 Meteor 开发环境;以及部署你的完成项目。
第二章,使用包进行定制,涵盖了模块化 Meteor 打包系统的各个方面。包括搜索、安装、删除、修改、创建和部署各种包的配方。还包括有关如何使用一些更有用和流行的包的信息,包括 npm 模块和 Iron Router。
第三章,构建优秀用户界面,包含了使用 Blaze(Meteor 的反应式模板引擎)开发前端应用的必要配方。从基本模板到自定义组件和动画,一切都被涵盖。
第四章,创建模型,提供了 Meteor 提供的反应性数据/模型能力的清晰、简洁的示例。从实现简单的数据收集到高级过滤和更新插入,本章将成为你所有反应性数据需求的首选参考。
第五章,实现 DDP,涵盖了分布式数据协议的主要用途。从读取原始 DDP 流到与其他编程语言一起使用该协议,一切都被涵盖,并提供了易于实现的示例。
第六章,掌握反应性,带你了解 Meteor 前端反应性的更高级方面。这些配方带你“深入了解”,展示如何自定义和创建反应性组件,以及如何将非反应性组件(如 JQuery UI)集成到流畅、有效的用户界面中。
第七章,使用客户端方法,包含了旨在利用一些更有用的 HTML5 组件的高级 UI 配方。本章包含创建动态 SVG 图表、实现基于触摸的 Canvas 组件或使用 FileReader 上传和提供图像的信息和示例。
第八章,集成第三方库,指导你使用独立的第三方库。包括直接使用 npm 模块、实现复杂的 D3.js 图表以及使用 Polymer 构建完整 UI 的说明和解释。本章使用这些示例作为指南,你可以扩展这些指南以在 Meteor 中实现任何第三方库。
第九章,保护你的应用程序,介绍了 Meteor 的所有基本安全功能。当你的应用程序准备部署到生产环境时,本章中的配方将确保你的应用程序既安全又高效。
第十章, 处理账户,深入探讨了强大且灵活的 Meteor 账户包。您将学习如何自定义账户 UI,使用如 Twitter 等外部 OAuth 提供者,甚至执行双因素认证。
第十一章, 利用高级功能,提供了“研究生水平”的方法和示例,说明如何充分利用 Meteor 的全部功能。主要关注服务器端功能,本章涵盖了使用 EJSON 扩展/打包对象、服务器方法以及使用 Fibers 优雅地处理异步函数和回调。
第十二章,创建有用的项目,是其他章节的总结,以一个有用且完整的应用程序的形式出现。这些食谱将引导您创建 REST 服务、向现有应用程序添加社交分享功能、使用 Iron Router 构建完整的应用程序,以及部署到移动设备。
您需要为此书准备什么
本书假设您已经具备 JavaScript 和 HTML 的实际知识。熟悉 Node、npm、GitHub 以及命令行/终端将非常有帮助(但不是必需的),这对于充分利用本书中的食谱非常有帮助。
您将找到安装 Meteor 在 Mac OS X 或 Linux 上的食谱,以及使用 Meteor 在 Windows 和 Google Chromebooks 上的链接。在任何情况下,您都需要访问互联网以下载 Meteor 和社区包,并且无论操作系统如何,您都需要在您的开发机器上拥有安装权限。
对于部署到生产环境或移动设备,要求将因食谱而异。要成功完成所有食谱,您需要自己的托管服务器和 DNS 域名,以及 iOS、Android 或 Windows 移动设备和 SDK。
本书面向对象
本书旨在为所有经验水平的开发者提供帮助,他们希望使用 JavaScript 创建移动和全栈 Web 应用程序。许多简单的食谱可以很容易地由经验较少的开发者遵循,而一些高级食谱将需要广泛的现有 Web、移动和服务器技术的知识。任何希望创建基于 JavaScript 的全栈应用程序的应用程序或企业级 Web 开发者都将从本书中涵盖的食谱和概念中受益。
章节
在本书中,您会发现几个频繁出现的标题(准备工作、如何做、它是如何工作的、还有更多、以及参见)。
为了给出完成食谱的明确说明,我们按以下方式使用这些章节:
准备工作
本节告诉您在食谱中可以期待什么,并描述了如何设置任何软件或任何为食谱所需的初步设置。
如何做…
本节包含遵循食谱所需的步骤。
它是如何工作的…
这一节通常包含对上一节发生事件的详细解释。
还有更多...
这一节包含有关食谱的附加信息,以便使读者对食谱有更多的了解。
参见
这一节提供了对其他有助于食谱的有用信息的链接。
约定
在这本书中,你会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“你必须安装 curl 才能安装 Meteor。”
代码块如下设置:
<head>
<title>FileTemplate</title>
</head>
<body>
<h1>Welcome to Meteor!</h1>
{{> hello}}
</body>
当我们希望引起你对代码块中特定部分的注意时,相关的行或项目将以粗体显示:
Package.onUse(function(api) {
api.versionsFrom('1.0.3.2');
api.addFiles('testpack.js', 'client');
api.addFiles('servertestpack.js', 'server');
});
任何命令行输入或输出都如下所示:
$ sudo apt-get install curl
新术语和重要词汇以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“点击屏幕底部的标签为NEW ORGANIZATION的按钮。”
注意
警告或重要注意事项以如下框中的形式出现。
小贴士
小贴士和技巧看起来是这样的。
读者反馈
我们欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出你真正能从中获得最大价值的标题。
要发送给我们一般性的反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及本书的标题。
如果你对某个主题有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在你已经是 Packt 图书的骄傲所有者,我们有一些事情可以帮助你从购买中获得最大价值。
下载示例代码
你可以从你购买的所有 Packt 出版物的账户中下载示例代码文件www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。可选地,代码示例也可以从github.com/strack/PacktMeteorRecipes下载。
下载本书的颜色图像
我们还为你提供了一个包含本书中使用的截图/图表颜色图像的 PDF 文件。这些颜色图像将帮助你更好地理解输出的变化。你可以从www.packtpub.com/sites/default/files/downloads/MeteorCookbook_ColorImages.pdf下载此文件。
错误清单
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
盗版
在互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过链接mailto:copyright@packtpub.com与我们联系,并提供疑似盗版材料的链接。
我们感谢您在保护我们作者和我们提供有价值内容的能力方面的帮助。
问题
如果您对本书的任何方面有问题,您可以通过链接mailto:questions@packtpub.com与我们联系,我们将尽力解决问题。
第一章. 优化您的流程
在本章中,我们将涵盖以下主题:
-
安装 Meteor
-
查找 Meteor 的文档
-
获取问题的帮助
-
设置您的项目文件结构
-
设置您的开发环境
-
使用 Web 控制台
-
将测试应用部署到 Meteor
-
使用 CNAME 重定向部署到 Meteor
-
部署到自定义托管环境
-
使用 Meteor Up (MUP)进行部署
-
使用 CoffeeScript
-
使用 CSS 编译器
简介
欢迎来到 Meteor 的奇妙世界!本章将向您介绍一些优化您的 Meteor 工作流程的最佳实践。从安装 Meteor 到将您的成品应用部署到您自己的定制服务器上,这里找到的食谱将帮助您快速启动并运行,并在您开始每天使用 Meteor 时提供极佳的参考。
安装 Meteor
Meteor 团队使安装变得简单。只要您正在工作的系统(Linux 或 Mac OS X)相对较新,安装 Meteor 应该非常简单。我们将其包括为食谱,以便当您在新机器上安装 Meteor 时,您手头就有它。我们还将包括来自 GitHub 的安装说明,以防您想使用开发分支。
准备工作
您必须安装curl才能安装 Meteor。如果您在 Mac OS X 上,curl已经安装。如果您在 Linux 上,curl通常已经安装。要检查curl,请打开一个终端窗口并执行以下命令:
$ curl
如果已安装curl,您将收到以下消息:
curl: try 'curl --help' or 'curl --manual' for more information
如果没有安装curl,您将收到类似以下的消息:
-bash: /usr/bin/curl: No such file or directory
要安装curl,使用apt-get或yum(根据您的 Linux 版本)。以下是在 Ubuntu 上安装curl的命令。只需将命令中的apt-get替换为yum,即可在 Fedora、Debian 或 CentOS 上安装:
$ sudo apt-get install curl
您应该看到一个安装消息,可能还会有提示安装。完成安装过程;然后您将准备好安装 Meteor。
如何做到这一点...
打开一个终端窗口并执行以下命令:
$ curl https://install.meteor.com/ | sh
它是如何工作的...
curl https://install.meteor.com/命令用于直接从 Meteor 获取安装脚本。一旦脚本下载完成,| sh参数告诉您的系统执行该脚本,该脚本将 Meteor 安装到/usr/local/bin/meteor(它应该在您的路径中)。一旦整个脚本执行完成,您将能够从任何位置运行meteor命令。
更多...
之前的食谱涵盖了默认/稳定的 Meteor 安装。如果您想直接从源代码安装 Meteor,或者如果您想安装一个开发分支(例如,当前的夜间版本),您可以使用git来完成。
Meteor 仓库位于github.com/meteor/meteor。您可以在该位置找到各种开发分支,以及主要的 Meteor 仓库。
假设您已经安装了git并且想要安装主要的 Meteor 构建,打开一个终端窗口并执行以下命令:
$ git clone git://github.com/meteor/meteor.git
这将把最新的 Meteor 构建克隆到名为meteor的子文件夹中。请注意,这不会在您的机器上全局安装 Meteor。这意味着,为了运行meteor命令,您需要导航到(或引用)您刚刚克隆的 Meteor 仓库所在的meteor文件夹。
小贴士
根据您的具体情况,可以使用各种其他安装参数和自定义选项。要查看更全面的说明,请访问 Meteor GitHub 主README.md页面github.com/meteor/meteor。
查找 Meteor 的文档
Meteor 终于达到了 1.0 版本,因此,它的各种库和功能开始稳定下来。然而,Meteor 的一些部分仍然在变化中。因此,拥有最新的文档非常重要。本指南将向您展示在需要时您可以在哪里找到官方 Meteor 文档。
如何操作…
简单地在一个浏览器中导航到docs.meteor.com/并收藏该页面。
它是如何工作的…
就这些了。Meteor 团队会保持文档的更新,您可以确信在那里找到所有生产就绪的功能,以及大量的宝贵建议。
还有更多…
如果您想要有一个离线文档副本并确保您拥有文档的最新和最佳版本,您可以使用 Meteor 的 GitHub 仓库来提供最新文档的本地副本。
要获取文档的初始副本,请导航到您想要存储文档的位置(例如,您的Documents目录)并使用以下命令克隆最新的 Meteor 仓库:
$ git clone git://github.com/meteor/meteor.git
如果你已经完成了这个操作,并且想要确保你拥有最新和最好的版本,请导航到meteor子文件夹(在你执行初始 Git 克隆时创建)并运行以下命令:
$ git pull origin master
这将更新 Meteor 仓库到最新和最好的版本。
一旦您获得了最新的文档,您可以通过在浏览器中导航到docs目录来访问它:
$ cd docs/
然后,运行meteor命令:
$ meteor
一旦包更新,Meteor 文档项目将启动,您将在命令行中看到以下内容:
=> Meteor server running on: http://localhost:3000/
您现在可以使用浏览器导航到http://localhost:3000/并阅读最新的文档。
获取帮助解答问题
您使用 Meteor 的越多,主要包就越熟悉。然而,您可能还希望开始尝试一些不太为人所知的包的高级功能。您也可能会有一个关于 Meteor 的新应用想法。无论如何,您最终会遇到需要比文档中包含的信息更多的信息的情况。以下是一些关于您需要问题时去哪里寻找答案的快速提示。
如何做到这一点…
获取问题解答有三个主要来源:
-
对于具体的技术问题或难题,请访问
stackoverflow.com/questions/tagged/meteor。搜索其他人已经提出的问题;如果您找不到答案,请提交您自己的问题。 -
对于一般问题和评论,请访问
forums.meteor.com/上的 Meteor 论坛。 -
要与多个 Meteor 开发者(包括核心团队成员)实时工作,您可以访问
irc.freenode.net上的 IRC #meteor 聊天室。
它是如何工作的…
让我们看看如何使用之前提到的每个资源的技巧。
Stack Overflow
Stack Overflow 是一个非常活跃的开发者社区,其中许多人乐于帮助他人解决技术问题。对于 Meteor 特定的帮助,一旦您导航到 stackoverflow.com/questions/tagged/meteor,您可以使用最能描述您问题的关键词进行搜索,例如,如果您想在 PhoneGap 应用程序内运行 Meteor,您可能想搜索所有标记为 [meteor] 的问题中的 cordova,如下所示:

这将搜索所有之前提交的、带有关键词 [meteor] 并在问题中包含 cordova 的标记问题。您将想要查看结果,看看您的问题是否已经被提出。如果其中一个问题(及其答案)满足您的需求,那就太好了!如果您的提问尚未得到解答,您可以提出自己的问题。
点击标有 提问 的按钮:

填写您问题的详细信息。别忘了包括标签,以便更容易识别和分类您的问题。提交您的问题,Stack Overflow 社区很可能很快就会为您找到答案。
Meteor 论坛
Meteor 论坛同样非常活跃,每天都有许多热情的 Meteor 开发者访问该网站。您可以在许多不同的 Meteor 相关主题上提问、查看讨论和发表评论。大多数时候,如果您有一个技术问题或难题,最好使用 Stack Overflow。然而,论坛也是获取更多抽象或具体 Meteor 问题的绝佳资源。
#meteor 在 IRC 上
meteor IRC 频道是一个很好的频道,你可以加入并收听。你将能够与各种优秀的开发者互动,其中许多人愿意立即回答你的问题。请记住,你遇到的大多数问题或问题(尤其是当你刚开始接触 Meteor 时)已经在 Stack Overflow 或 Meteor 论坛上得到了解决,因此在 IRC 聊天中提问之前,先检查并确保你的问题还没有被回答是礼貌的。
还有更多…
请记住,由于 Meteor 仍然很新,有许多人在努力解决你可能能够回答的问题。此外,当你找到自己问题的答案时,可能值得将其发布在 Stack Overflow 上。这将帮助其他可能遇到相同问题的人节省时间。
在 Stack Overflow、Meteor 论坛以及甚至 #meteor IRC 上回答他人的问题是一种为社区做出贡献的好方法。
小贴士
如果你自己找到了答案,别忘了在 Stack Overflow 上回答你自己的问题!
设置你的项目文件结构
当你在 Meteor 中启动一个新项目时,默认的文件配置是为了让你立即开始运行。这很好,但如果你希望保持代码优化且易于理解,你可能需要稍微调整一下文件结构。以下食谱概述了你可以在大多数项目中使用的文件结构。
准备工作
你将想要创建一个新的基本 Meteor 项目,然后我们将对其进行修改以适应我们的需求。在终端窗口中,导航到你想要创建项目的文件夹,并输入以下命令以创建一个名为 FileTemplate 的项目:
$ meteor create FileTemplate
小贴士
下载示例代码
你可以从你购买的所有 Packt 出版物书籍的账户中下载示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给你。可选地,代码示例也可以从 github.com/strack/PacktMeteorRecipes 下载。
这将创建基本文件结构。现在,导航到你在文件资源管理器窗口中选择的文件夹。在这个练习中,我们将使用 Mac OS X Finder 窗口,这样我们就可以可视化文件夹结构:

小贴士
总是以一个新的 Meteor 项目开始(不要尝试手动创建你的文件夹结构)。尽管文件结构看起来很简单,但 meteor create 命令创建的隐藏文件和文件夹是你无法离开的!
如何做…
要设置你的项目文件结构,请按照以下步骤进行:
-
在您的根目录中,删除默认的
.css、.js和.html文件。FileTemplate现在应该看起来是空的,类似于以下截图:![如何操作…]()
小贴士
而不是删除基本的
.html、.css和.js文件,您可以使用以下步骤中讨论的代码/文件条目将它们分开。 -
在您的根文件夹中创建以下子文件夹:
-
client -
server -
both -
private -
public
-
-
导航到
client子文件夹。创建一个名为main.html的新文件。编辑此文件并添加以下代码(从FileTemplate.html获取):<head> <title>FileTemplate</title> </head> <body> <h1>Welcome to Meteor!</h1> {{> hello}} </body> <template name="hello"> <button>Click Me</button> <p>You've pressed the button {{counter}} times.</p> </template> -
在
client文件夹中创建一个新的子文件夹,命名为scripts。 -
在
scripts内部,创建一个名为main.js的新文件。编辑此文件,添加以下代码(从FileTemplate.js中的Meteor.isClient部分获取):// counter starts at 0 Session.setDefault('counter', 0); Template.hello.helpers({ counter: function () { return Session.get('counter'); } }); Template.hello.events({ 'click button': function () { // increment the counter when button is clicked Session.set('counter', Session.get('counter') + 1); } }); -
在
client文件夹中创建一个新的子文件夹,命名为lib。创建两个子文件夹,分别命名为scripts和styles。 -
在
styles内部,创建一个名为style.css的新文件。 -
导航到
server子文件夹。创建一个名为server.js的新文件。将以下代码添加到server.js文件中并保存更改:Meteor.startup(function () { // code to run on server at startup });您完成的文件结构现在应该看起来像以下截图:
![如何操作…]()
它是如何工作的…
我们将主要成对地分解这个文件结构。这是因为几乎对于每个文件夹,都有一个服务于相反目的的另一个文件夹。
client/server
client文件夹被 Meteor 解释为仅属于客户端的代码。server文件夹则正好相反,告诉 Meteor,包含在其中的任何文件仅应用于服务器端处理。
将client和server文件夹分开逻辑上分离了您的代码(使其更容易可视化),节省了不必要的处理,并防止了错误(如果由服务器处理,客户端模板和渲染代码将引发错误)。
在client文件夹内部,我们放置了一个scripts子文件夹和两个文件,它们都具有主前缀(main.html和main.js)。scripts子文件夹是为了我们的便利,将 HTML 和 JavaScript 分开,这样当我们回头查看代码时,它将更容易阅读和分段。
main/lib
main前缀用于告诉 Meteor:最后运行此代码。当 Meteor 开始处理和编译文件时,它会等待处理所有名为main的文件,直到最后。这有助于在需要在使用它们之前初始化库或脚本的情况下。
相反,lib文件夹告诉 Meteor:首先运行此代码。放置在lib文件夹内部或作为其子项的任何内容都将由 Meteor 在运行时首先运行。这是一个放置全局变量、尚未作为包包含的通用库(参见第二章,使用包进行自定义)以及当然任何样式表的好地方。这就是为什么我们包括了scripts和styles子文件夹,并创建了一个初始的通用style.css文件。
使用main前缀和lib文件夹一起可以帮助我们保持正确的顺序,并确保我们的样式尽可能快地加载。
public/private
public文件夹是一个存储需要全局访问的资产的仓库。这是一个存储非敏感信息(如图像和文本文件)非常好的地方。它也很容易记住。如果你需要将某些内容公开显示,请将其放在public文件夹中。
private文件夹正好相反。这是一个只能由服务器查看的资产存储位置。这是一个放置适度敏感信息的好地方(你将需要额外的安全措施,如数据库,来保护你想要更安全的信息)。任何放置在private文件夹中的文件都可以由服务器通过没有特殊路径或导航参数来引用。
both
both文件夹独立存在,是客户端和服务器都可以访问的文件夹。当你创建模型和集合(参见第四章,创建模型)时,你希望将声明放在both文件夹内的一个文件中。这样做的主要原因是不重复代码,并确保客户端和服务器都从相同的模型声明开始工作。
还有更多...
显然,你不必每次都从头开始创建这个文件结构。你可以使用你喜欢的脚本引擎(Grunt、Gulp、Shell Script 等)并创建一个脚本,它可以通过单个命令进行必要的更改。或者,你可以创建一个模板,然后将整个文件夹复制到新位置,然后就像它是新项目一样使用它。
如果你确实决定使用这个配方而不是自动化脚本,请确保定期将你的基础模板更新到最新的 Meteor 构建版本,每次复制时也是如此。这可以在终端窗口中非常容易地完成。导航到根文件夹(例如~/Documents/Meteor/FileTemplate)并运行以下命令:
$ meteor update
你将收到一条消息,告诉你文件夹已经是最新的,或者 Meteor 将更新项目到最新版本。
参见也
- 第二章,使用包进行自定义
设置你的开发环境
在你开发项目的同时,有几个默认包和设置可以使你的调试和开发工作变得更加容易。这个配方将为你提供一些建议,帮助你创建一个理想的发展环境。
准备工作
你需要确保已经安装了 Meteor(当然),并且你需要设置一个默认的项目文件结构。你可以使用本章早些时候的“设置你的项目文件结构”配方,或者使用meteor create [...]命令创建一个新的 Meteor 项目。
这个配方假设你已经设置了一些 Mongo 集合和/或某些类型的用户账户。如果你没有添加集合,你可能只想为了测试目的添加一个。为此,创建一个名为[项目根]/both/model.js的文件,并输入以下代码行:
testCollection = new Mongo.Collection('testCollection');
在有一个项目和至少一个要监控的 Mongo 集合的情况下,我们准备开始开发和调试了!
如何操作…
首先,我们想确保insecure和autopublish包已安装,这对于默认的 Meteor 项目通常是默认情况。
-
在终端窗口中,导航到你的项目根目录,并输入以下命令:
$ meteor list这将列出你的项目使用的所有包。你应该会收到类似于以下的内容:
autopublish 1.0.2 insecure 1.0.2 meteor-platform 1.2.1 … -
如果缺少
autopublish或insecure包,你可以通过输入以下命令手动添加它们:$ meteor add autopublish $ meteor add insecure -
接下来,安装
msavin:mongol智能包。在终端窗口中,输入以下命令:$ meteor add msavin:mongol在简短的安装过程之后,你应该会收到一条消息,表明
msavin:mongol包已正确安装。 -
通过在终端窗口中输入以下命令来启动你的
meteor项目:$ meteor -
通过导航到
http://localhost:3000在浏览器中打开你的应用程序。一旦进入,按下Ctrl + M,并查看屏幕的左下角。你会看到一个可展开的控制台,类似于以下截图:![如何操作…]()
点击任何集合名称或账户标签,你可以查看、更新、删除和插入记录,如下面的截图所示:
![如何操作…]()
当你在构建你的应用程序时,这会非常有用,因为你可以立即访问你的集合和用户账户配置文件。
它是如何工作的…
以下行是安装包所需的所有内容:
$ meteor add msavin:mongol
这告诉 Meteor 去寻找包含所有依赖的msavin:mongol包,并将其添加到你的项目中。这是一个第三方包,更多关于这个包的详细信息可以在atmospherejs.com/msavin/mongol找到。
安装完成后,您可以通过浏览器页面上的 Ctrl + M 快捷键访问它。在底层,mongol 包正在监控任何集合,特别是将用户账户相关的集合过滤到 账户 选项卡中。除此之外,所有集合都同等对待,这个界面只是简化了您在需要查看集合内部情况时的调试/开发过程。
还有更多...
Mongol 非常新,但它一直在不断改进。您可以通过访问 github.com/msavin/Mongol 了解所有当前功能并预览即将推出的功能。
参见
-
在第二章 Customizing with Packages 中的 Adding Meteor packages、Removing Meteor packages 和 Using npm modules 配方,Customizing with Packages
-
在第九章 Securing Your Application 中的 Basic safety – turning off autopublish 和 Basic safety – removing insecure 配方,Securing Your Application
使用网页控制台
有时在调试时修改代码会太慢。这个配方将向您介绍网页控制台,并给您一些使用它的提示。
准备工作
我们需要一个沙盒来玩耍。为了这个配方,我们将使用 Meteor 内置的默认示例之一。
在终端窗口中,输入以下命令:
$ meteor create --example leaderboard
创建完成后,导航到 leaderboard 文件夹并启动 Meteor:
$ cd leaderboard
$ meteor
我们现在可以使用我们选择的网页浏览器导航到我们的排行榜示例页面(http://localhost:3000)。
我们需要在各种浏览器中启用网页控制台。不分先后,以下是如何在您可能工作的浏览器上访问网页控制台的方法:
Safari
通过转到 Safari | 偏好设置 | 高级 (CMD+, 如果您更喜欢快捷键) 并确保启用 菜单栏中显示开发菜单 选项来启用 开发 菜单:

现在,在任何网页上,点击 开发 下的 显示错误控制台(或使用 CMD + Alt + C 快捷键):

这将打开网页控制台,如下面的截图所示:

Firefox
在任何网页上,点击 工具 | Web 开发者 下的 Web 控制台菜单选项(CMD + Alt + K 快捷键):

网页控制台现在如以下截图所示:

Chrome
在任何网页上,点击 查看 | 开发者 下的 开发者工具菜单项(CMD + Alt + I 快捷键):

这将打开网页控制台,如下面的截图所示:

如何做到...
为了展示网页控制台可以实现的功能,让我们创建一个新的科学家,并为 Tesla 添加一些分数。因为,我要让你完成,但尼古拉·特斯拉是有史以来最伟大的科学家。就是。所有。时间。
-
如果您还没有这样做,请导航到
http://localhost:3000/。 -
现在,如果尚未打开,请打开网页控制台。
-
在控制台中运行以下命令:
> Players.insert({name:"Stephen Hawking"}) -
斯蒂芬·霍金 的名字现在将在列表中。您可以选择他的名字并添加一些分数:
![如何操作…]()
-
现在,让我们给 Tesla 应得的关爱。首先,让我们找到 Tesla 记录的
_id值。在控制台中运行以下命令:> Players.findOne({name:"Nikola Tesla"}) -
从结果中提取
_id值并修改分数,类似于以下内容:> Players.update({_id:"bPDpqp7hgEx4d6Eui"}, {$set:{score:334823}})现在,Tesla 将被放置在排行榜的顶部:
![如何操作…]()
-
最后一次操作,让我们取消选择之前截图中所提到的任何科学家。在控制台中输入以下命令:
> Session.set("selectedPlayer",null)您会注意到,现在没有选择任何科学家。
小贴士
您可以通过按上箭头或下箭头键在网页控制台中运行之前的命令。
它是如何工作的...
网页控制台充当您代码的扩展。您能够接收日志消息,调用公开的方法和事件,或更改和实例化全局变量。您能够在实时动态地做到这一点,这在测试或调试时可以带来很多优势。
通过几行精心编写的代码,我们能够复制事件处理程序和属于应用程序的方法中的行为。换句话说,我们能够手动测试事物,而无需修改、保存和重新测试代码。这种即兴测试可以真正节省我们时间。
还有更多…
网络控制台和相关开发工具可以执行各种客户端调试和测试任务。您可以运行预写的脚本,在执行过程中暂停代码,监控变量和调用堆栈,甚至可以动态地创建自己的方法和变量。
小贴士
网页控制台在各个浏览器之间略有不同,但您可以在 developers.google.com/chrome-developer-tools/docs/console 找到适用于几乎所有网页控制台的说明。
将测试应用部署到 Meteor
如您所期待的那样,Meteor 使几乎所有事情都变得更容易做。在服务器上测试您的应用程序也不例外。这个食谱将向您展示如何将您的应用程序部署到 Meteor 服务器,在那里您可以测试(并展示!)您的新应用程序。
准备工作
这个食谱的唯一要求是您有一个可工作的应用程序。
您的应用不需要很复杂或完整,但它至少应该在屏幕上渲染一些内容,以便您可以验证它是否在 Meteor 服务器上运行。
对于这个食谱,我们将使用默认的 leaderboard 示例,该示例是在终端窗口中使用以下命令创建的:
$ meteor create --example leaderboard
如何操作...
要将测试应用程序部署到 Meteor,请按照以下步骤进行:
-
首先,你需要选择一个应用程序名称。
应用程序名称将作为你的应用程序被服务的 URL 的子域出现在 Meteor 服务器上,例如
http://myproject.meteor.com。然而,有很多人在 Meteor 服务器上测试应用程序。很可能一个通用名称,如 "myproject" 或 "leaderboard",已经被占用。所以,我们需要选择一个独特的名称。
对于这个食谱,我将使用这个应用程序名称
packtrecipe。你显然需要选择其他名称。我是第一个到这里来的!小贴士
你的项目和应用程序名称不需要匹配。应用程序名称只是一个标识符,这样 Meteor 服务器就知道如何路由到你的应用程序。
-
现在我们已经选择了应用程序名称,我们将打开一个终端窗口并导航到我们项目的根目录:
$ cd leaderboard -
一旦我们进入正确的文件夹,我们将按照以下方式发出
deploy命令:$ meteor deploy packtrecipe -
Meteor 将打包并部署你的应用程序。完成后,你应该在终端窗口中看到以下类似的内容:
Deploying to packtrecipe.meteor.com. Bundling... Uploading... Now serving at packtrecipe.meteor.com -
在浏览器中导航到 Meteor 给你的 URL(在这种情况下,
http://packtrecipe.meteor.com),你将看到你新部署的应用程序,如下所示:![如何操作…]()
它是如何工作的…
这太神奇了!Meteor 的人都是巫师!
实际上,核心开发团队已经非常努力地使部署尽可能简单。在底层,你的本地 Meteor 安装正在打包正确的文件,编译包列表和依赖项,然后向 Meteor 服务器发送请求。一旦 Meteor 服务器收到请求,就会进行所有必要的错误检查,并创建和初始化包、数据库和应用程序。添加一个虚拟主机地址,然后 voila!你的应用程序就上线并运行了。
其中涉及很多细节(和代码),但这些都应该能给你一个关于如何部署到 Meteor 测试服务器的总体概念。你不高兴自己不必编写所有这些代码吗?
还有更多…
你可能已经注意到,你必须创建一个 Meteor 开发者账户才能部署。这是应该的,因为没有登录/安全细节,有人可能会来并用自己的部署覆盖你的部署。
然而,这个要求相当灵活,允许你添加其他用户甚至组织,以便多个人可以部署或更新你的应用程序。
要设置一个组织,导航到 www.meteor.com/account-settings/organizations 并点击屏幕底部的“新建组织”按钮。然后你可以添加个人用户到组织中,类似于以下截图:

一旦部署了你的应用程序,你可以通过meteor authorized命令授权个人或组织所做的更改。使用以下语法添加授权账户:
$ meteor authorized [your-url.meteor.com] --add [user/organization]
例如,我们会使用以下命令将packtmeteor组织添加到我们的部署应用中:
$ meteor authorized packt.meteor.com --add packtmeteor
同样,如果我们想取消授权(例如,让我们取消strack2账户的授权),我们会输入类似以下的内容:
$ meteor authorized packt.meteor.com --remove strack2
小贴士
Meteor 开发者账户还有其他有用的功能。要探索你可以用你的账户做什么,请访问www.meteor.com/account-settings。
参见
- 在第二章的构建自定义包食谱中,使用包进行定制
使用 CNAME 重定向部署到 Meteor
将应用部署到 Meteor 测试服务器一切都很顺利,但如果你想使用自己的自定义域名怎么办?Meteor 团队为你提供了一个简单的方法来实现这一点。这个食谱将向你展示如何利用这个简单而强大的功能。
准备就绪
你需要在你的域名注册的主机服务上创建一个指向origin.meteor.com的CNAME重定向。具体操作方法差异很大,因此请咨询你的主机服务知识库以获取确切步骤。对于这个食谱,我们将使用packtpub.com域名的主机服务的 cPanel 界面。
在你的CNAME重定向中输入你希望使用的子域名(例如,meteor.packtpub.com),并将重定向位置设置为origin.meteor.com。点击添加 CNAME 记录以提交记录:

如何操作...
假设我们将要使用的子域名是meteor.packtpub.com。为了部署到 Meteor 环境,请执行以下步骤:
-
一旦你的CNAME重定向设置正确,打开一个终端窗口,导航到你的项目根目录,然后输入以下命令:
$ meteor deploy meteor.packtpub.com -
当捆绑、上传和提供步骤完成时,Meteor 会部署你的应用并提供反馈:
Deploying to meteor.packtpub.com. Bundling... Uploading... Now serving at meteor.packtpub.com -
要验证应用程序,请在浏览器中导航到你的应用程序 URL(例如,
http://meteor.packtpub.com)。如果一切部署正确,你将看到你的应用程序正在运行。
它是如何工作的...
这个 Meteor 部署功能几乎与默认部署相同,只是内置了一些额外的代码来解释CNAME重定向的来源。
当请求通过CNAME重定向到达origin.meteor.com时,Meteor 会使用CNAME重定向的原始目的地作为应用程序部署的唯一标识符。Meteor 还会使用来自此CNAME重定向的后续请求来提供应用程序。
在这个特定情况下,原始CNAME目标地址是meteor.packtpub.com。当请求被重定向到origin.meteor.com时,Meteor 会识别CNAME重定向并使用它来将流量导向已部署的应用程序。
参见
- 本章中关于将测试应用程序部署到 Meteor的配方
部署到自定义托管环境
当你准备将你的应用程序实际部署到生产环境时,你需要将你的 Meteor 应用程序转换为纯 Node 应用程序。有多种方法可以做到这一点,所有这些方法(目前)都是手动过程,并且不适合初学者。可能会出错很多。这个配方将向你展示如何手动将你的 Meteor 应用程序作为 Node 应用程序部署到生产服务器。
准备工作
正如提到的,有多种方法和服务器配置可以成功托管 Node 应用程序。然而,有一些共同点,这些共同点对于这个配方都是必需的。
首先,你需要一个托管MongoDB数据库。
你的 MongoDB 数据库可以托管在远程服务器上,或者在你将部署 Node 应用程序的同一台机器上。对于这个配方,我们已经将默认的 MongoDB 数据库部署到了我们将托管应用程序的同一台机器上。因此,我们的MONGO_URL值将是:
MONGO_URL=mongodb://localhost:27017
小贴士
MongoDB 的安装说明(和教程)可以在docs.mongodb.org/manual/installation/找到。
免费或便宜的专用 MongoDB 服务托管也存在。运行一个快速的互联网搜索以查找MongoDB 托管,或者访问www.mongodb.com/partners/cloud以获取提供商列表。
接下来,你的托管环境需要最新的稳定 Node 和 npm 模块。
这些程序的安装说明超出了本书的范围。有关安装说明和如何开始的建议,请访问以下链接:
-
Node (
nodejs.org) -
npm (
www.npmjs.com/)
小贴士
这些程序的最新或夜间构建在生产环境中可能会给你带来问题。请确保使用稳定版本,除非你已经彻底测试了其他版本。
最后,你需要在你的托管环境中安装forever npm 模块。从你的托管环境的一个终端窗口中,运行以下命令:
$ npm install –g forever
注意,你可能需要使用sudo命令全局安装软件包。这取决于 npm 在你的服务器上是如何安装的。前面的命令将在你的机器上安装forever,现在你就可以准备和部署你的应用程序到生产环境了。
如何做这件事...
要部署到自定义托管环境,请按照以下步骤操作:
-
在你的开发环境中打开一个终端窗口,导航到你的 Meteor 项目的根目录。在这个根目录中,执行以下命令:
$ meteor build [your-build-location] --architecture [x]将上一行代码中的占位符替换为你的构建位置(例如:
~/Documents/builds/mybuild)和一个架构(选项有os.osx.x86_64、os.linux.x86_64和os.linux.x86_32)。如果你是在将要部署应用程序的服务器上构建,--architecture选项是可选的。 -
Meteor 将捆绑、提取并重新打包你的 Meteor 项目的干净生产副本,为在托管环境中使用做好准备。在构建过程中,Meteor 将在终端窗口中更新你的状态。
-
一旦构建完成,导航到你指定的构建文件夹,例如,如果你指定
~/Documents/builds/mybuild作为你的构建位置,你需要输入以下命令:$ cd ~/Documents/builds/mybuild -
在构建文件夹中,你会看到一个名称类似于
[your-project-name].tar.gz的tarball文件。例如,如果我的项目名称是
leaderboard,那么tarball文件名称将是leaderboard.tar.gz。 -
记下名称和位置,因为当你将构建复制并提取到服务器时,你需要用到它。
-
对于这个食谱,让我们假设你正在使用 Linux 服务器托管你的生产应用程序。让我们使用以下终端命令创建一个
meteorapps文件夹:$ mkdir /home/meteorapps -
接下来,将
tarball文件从你的开发环境复制到生产托管环境中的/home/meteorapps/文件夹。如果你在一个不同的机器上构建你的应用程序,你可以通过 SCP、FTP、常见的文件服务器、Git 仓库等方式进行传输。实际上,你如何复制它并不重要,只要现在你的托管环境中有一个
tarball文件副本即可。 -
一旦复制完成,在
tarball文件上运行以下命令:$ tar –xf [your-tarball-name].tar.gz这将把文件提取到一个名为
bundle的文件夹中。如果你导航到bundle文件夹,你会找到一个README文件。本食谱的下一步操作来自这个README文件,所以你可以自由查看它们以获得更简洁的指令集。如果你检查文件夹内容,你应该会看到以下类似的内容:README main.js programs server star.json -
现在,我们将安装运行我们的应用程序所需的 npm 软件包。导航到
bundle/programs/server并执行以下命令:$ npm install注意
注意,我们没有使用
–g参数,因为我们只安装了捆绑文件夹中指定的本地 npm 软件包。 -
接下来,我们需要设置
PORT和MONGO_URL导出参数,以便 Node 知道如何托管我们的应用程序。在终端窗口中输入以下命令:$ export PORT=8080 $ export MONGO_URL=mongodb://localhost:27017这两个导出命令告诉你的 Node 服务器要监听哪个端口,以及在哪里找到
mongodb托管实例(在这个例子中我们使用的是本地实例)。你还需要配置
ROOT_URL和MAIL_URL环境变量。输入它们的语法类似于以下导出命令:$ export ROOT_URL='http://[your-hostname.com]' $ export MAIL_URL='smtp://user:password@mailhost:port/'
现在,我们可以运行我们的应用程序。记住,我们安装了 npm 的 forever 包。forever 包允许我们运行 Node 应用程序,并在遇到错误时自动重启它。在终端窗口中运行以下命令:
$ forever start main.js
此命令指示 Node 以节点应用程序的方式启动 main.js,并在出现任何问题时重启它。
小贴士
你可以通过从 bundle/ 目录发出以下命令来稍后停止应用程序:
$ forever stop main.js
现在是时候通过打开浏览器并将它指向你指定的主机环境来测试你的应用程序是否成功运行了,例如,如果我们的生产环境托管了 meteorapp.packtpub.com 子域名,并且我们指定 端口 8080,如前例所示,我们将在浏览器中导航到 http://meteorapp.packtpub.com:8080。
你的应用程序应该已经启动并开始服务页面。
它是如何工作的...
Node 是为了尽可能快地运行而构建的。为了做到这一点,它会在不同的硬件和软件配置上以不同的方式运行。这意味着你在开发环境中使用的 Node 和 npm 包(例如,在 MAC OS X 上)与生产环境中的相应 Node 和 npm 包(例如,Linux Ubuntu 12.4 LTS Precise)略有不同。这尤其适用于基础包,如 npm 的 fibers 包。
此外,尽管 Meteor 是基于 Node 构建的,但它并不是一个原生 Node 应用程序。它增加了一些抽象层和处理层,这些层可以让你作为开发者更容易地生活,但它们并不构成最漂亮的原生 Node 生产环境。
meteor build 命令会为我们处理这个问题,并创建一个不包含已安装 npm 包的构建。相反,它将任何 npm 包列为依赖项。由于具体的 npm 包没有被包含在内(Meteor 在一个包清单文件中列出了它们),因此没有兼容性问题。我们只是简单地告诉 Node 使用包清单文件作为某种清单来查找和安装特定于当前环境的包。我们是在发出 npm install 命令时这样做。
一旦 npm 读取了清单文件,检索并安装了所有需要的包,并通知我们安装已正确完成,我们就可以运行我们的新原生 Node 应用程序。
我们然后设置了一些导出变量(PORT、MONGO_URL、ROOT_URL 和 MAIL_URL),并使用 forever npm 包而不是正常的 node 命令来运行我们的应用程序。使用 forever 可以帮助我们不必每次服务器崩溃时都返回服务器。理想情况下,我们永远不会有一个应用程序崩溃,但我们生活在这个真实的世界中,当需要重启应用程序时,forever 是一个巨大的时间节省者。
更多信息...
如果我们的应用程序真正准备好投入生产,我们也希望它在服务器重启时自动启动。这取决于你的生产服务器运行的操作系统的各种方法,我们不会涵盖所有这些方法。
我们将提供一个示例脚本和如何在 Ubuntu 上执行此操作的说明,并让你根据需要修改脚本以适应其他环境。
每当 Ubuntu 服务器重启时,它都会运行在/etc/init/文件夹中找到的任何*.conf scripts脚本。我们假设我们的应用程序位于/home/meteorapps/prodapp/bundle/,我们将监听端口 8080,并且我们使用本地 MongoDB 服务(请根据需要调整这些设置)。
使用具有sudo权限的终端窗口,将以下meteorapp.conf脚本添加到生产服务器的/etc/init/:
# prodapp configuration file
# /etc/init/meteorapp.conf
start on (local-filesystems)
stop on shutdown
script
cd /home/meteorapps/prodapp/bundle/
export MONGO_URL=mongodb://localhost:27107 PORT=8080
export ROOT_URL='http://example.com'
export MAIL_URL='smtp://user:password@mailhost:port/'
exec forever start main.js
end script
让我们分解一下这个脚本做了什么。
前两行(start on和stop on)告诉操作系统何时运行此脚本。在这种情况下,我们在本地文件系统准备好后立即运行它,并在收到关机请求后立即停止它。
然后,我们有将要运行的脚本。我们使用cd /home/meteorapps/prodapp/bundle导航到我们的应用程序文件夹。
然后,我们声明我们的 MongoDB 服务位置和我们希望 Node 监听的端口。
最后,我们执行forever命令,要求它以 Node 应用程序启动main.js。
Meteor Group 目前正在开发一个名为Galaxy的项目,它将使部署到自定义服务器变得与部署到测试服务器一样简单。一旦 Galaxy 可用,这个配方就会变得过时,但这是一件好事!如前所述,这个配方只涵盖了一种部署到生产环境的方法。你只需浏览forums.meteor.com或访问其他 Meteor 信息来源,就可能找到另一种更简单的方法。
相关内容
-
本章的使用问题获取帮助配方
-
本章的使用 Meteor Up (MUP)部署配方
-
在第十二章“创建有用的项目”中的将应用部署到移动设备配方,创建有用的项目
使用 Meteor Up (MUP)部署
如果我们没有添加一种将 Meteor 应用部署到生产服务器的最终方法,那将是我们的疏忽。这种方法被称为Meteor Up(MUP)。使用一个相当简单的配置文件,MUP 极大地简化了服务器部署过程。在我们看到 Galaxy(Meteor 的部署平台)的样子之前,MUP 是部署到自定义环境的最简单方式。这个配方将向你展示如何使用 Meteor Up 将你的应用部署到自定义服务器。
准备工作
要完成这个配方,你需要一个可工作的 Meteor 应用程序。它不需要很复杂,但确实需要是功能性的,这样你就可以一眼看出它是否已经正确安装在你的服务器上。
如何做到这一点…
MUP 通过 npm 安装。它需要全局安装,因此我们将使用 -g 参数。要使用 MUP 将您的应用程序部署到自定义服务器,请执行以下步骤:
-
要安装 MUP,打开一个终端窗口并执行以下命令:
$ npm install -g mup这将需要一些时间来安装,但一旦完成,您就可以配置和使用 MUP 来部署您的应用程序。
-
要运行
mup init,在终端窗口中导航到您的项目根目录并输入以下命令:$ mup init您将收到类似以下的消息:
Empty Project Initialized!此过程将在您的项目根目录中创建两个文件。我们不需要担心第一个文件(
settings.json)。我们将使用第二个文件,命名为mup.json来配置我们的应用程序。 -
现在,我们将配置
mup.json。在编辑器中打开[项目根目录]/mup.json文件。文件的顶部将包含我们的远程服务器信息。它应该类似于以下代码行:"servers": [ { "host": "hostname", "username": "root", "password": "password" // or pem file (ssh based authentication) //"pem": "~/.ssh/id_rsa" } ],host属性将是您将通过 SSH/SCP 访问的服务器名称。如果我们的服务器名称是my-production-server.com,则主机属性将类似于以下内容:"host": "my-production-server.com",username和password属性是如果您要ssh登录远程服务器时将使用的用户/密码组合。下面的四个属性帮助我们配置服务器。如果我们想让 MUP 安装 Mongo、Node 或 PhantomJS(所有这些都是通过 MUP 部署所必需的),我们可以指定如下代码:
"setupMongo": true, "setupNode": true, "nodeVersion": "0.10.33", "setupPhantom": true,小贴士
就本书的印刷而言,最新的稳定 Node 版本是
0.10.33。您可能想检查这一点并相应地修改nodeVersion属性。如果 Mongo/Node/PhantomJS 已经安装,您可以将前面的属性更改为
false(这将加快您的部署速度)。接下来,我们指定我们想要我们的应用程序被称为什么。这是通过修改
appName属性来完成的:"appName": "[your-app-name-here]",我们需要指定我们本地机器上即将部署的应用程序所在的文件夹。这由
app属性确定:"app": "/path/to/the/app",我们需要设置的最后一个属性是
env。这指示 Node 从哪个ROOT_URL和PORT运行我们的 Meteor 应用程序。例如,如果我们正在将传入的 HTTP 流量重定向到本地的port 1337(这是通过反向代理,如nginx,或虚拟主机,如apache来完成的),我们的env配置将类似于以下代码:"env": { "ROOT_URL": "http://localhost", "PORT": 1337 }, -
现在,让我们使用
mup setup来配置我们的远程服务器。在终端窗口中,导航到您的项目根目录并输入以下命令:$ mup setup这将在远程服务器上安装 MongoDB、Node 和 PhantomJS。它还将配置我们的远程服务器环境并安装一些辅助 npm 包,例如
upstart。 -
让我们使用
mup deploy来部署我们的应用程序。一旦mup setup命令完成,我们就可以准备部署我们的应用程序。在终端窗口中执行以下命令:$ mup deploy
MUP 会将您的应用程序本地打包,上传构建,在远程服务器上配置必要的 npm 包,然后提供您的应用程序。在这个过程中,MUP 将在终端中提供状态更新。
完成后,您的应用程序应该已经启动并运行。您可以通过访问外部 URL(例如,http://my-custom-server.com)或通过 SSH 登录到远程服务器,并在本地主机上使用 curl 命令测试构建(例如,curl http://localhost:3000)来测试它。
您可能需要进行一些故障排除,以确保您的虚拟主机或反向代理配置正确,但经过一些小的调整后,您会发现,目前 MUP 无疑是部署到自定义服务器的最佳方式。
它是如何工作的…
MUP 执行我们通常需要手动实施的步骤(如本章中 部署到自定义托管环境 的食谱),并自动实现它们。成功执行此操作有三个主要部分。
首先,当我们执行 mup init 命令时,MUP 会创建一个默认配置文件。我们使用所有设置编辑新创建的文件,以便在远程服务器上安装正确的软件,配置环境变量,并上传我们的生产构建。
其次,我们使用 mup install 命令在远程服务器上安装所有需要的软件,这些软件在 mup.json 配置文件中已指定。
最后,我们的应用程序被打包、上传、解压,使用环境变量初始化,并设置为在远程服务器上运行。这一切都是通过 mup deploy 命令完成的。
MUP 根据我们编辑的配置文件执行这些任务。
小贴士
我们可以配置许多额外的设置,并且可以通过访问 MUP 仓库来探索 MUP 的许多优秀功能,MUP 仓库位于 github.com/arunoda/meteor-up。
参见
- 本章中 部署到自定义托管环境 的食谱
使用 CoffeeScript
许多人更喜欢 CoffeeScript 而不是标准的 JavaScript 语法。如果您是这些人中的一员,并且有偏好,Meteor 会满足您的需求。这个食谱将向您展示如何快速在您的开发环境中启用 CoffeeScript。
准备工作
除了安装 Meteor 和创建一个项目以便开始使用 CoffeeScript 之外,无需为这个食谱做任何准备。
如何操作…
要使用 CoffeeScript,请按照以下步骤操作:
-
打开一个终端窗口并导航到您的项目根目录。
-
输入以下命令:
$ meteor add coffeescript您应该在终端窗口中看到以下响应:
coffeescript added, version 1.0.CoffeeScript 现已安装并准备就绪!
您可以通过创建一个 .coffee 文件并添加一个脚本(例如,将 test.coffee 添加到您的 client 文件夹)来测试安装。当您使用 meteor 命令启动您的 Meteor 应用程序时,脚本应该按预期执行。
小贴士
你可以轻松地卸载 CoffeeScript。只需执行以下终端命令:
$ meteor remove coffeescript
工作原理…
在幕后,你刚刚安装的 CoffeeScript 包会寻找任何*.coffee和*.litcoffee文件,将它们编译成原生 JavaScript,然后打包供你的运行中的 Meteor 应用程序使用。
注意
就像在运行中的 Meteor 应用程序中的所有其他文件一样,你的 CoffeeScript 更改将立即被处理。
相关链接
- 在第二章的添加 Meteor 包食谱中,使用包进行自定义
使用 CSS 编译器
任何 CSS 爱好者都知道,使用标准 CSS 创建样式表可能是繁琐且重复的工作。许多设计师和开发者更喜欢使用动态样式表语言或预处理器,例如 Less、Stylus 和 SCSS/SASS。
Meteor 不仅使预处理器可用,而且将它们视为任何其他文件,以便更改能够立即反映出来。
这个食谱将向你展示如何在你的 Meteor 应用程序中启用一些更流行的 CSS 编译器。
准备工作
除了安装 Meteor 和创建一个项目以便你可以开始使用 CSS 编译器之外,无需为这个食谱做任何准备。
如何操作…
我们将介绍三种不同的预处理器,因为它们都以类似的方式工作。
使用 Stylus
-
打开终端窗口并导航到你的项目根目录。
-
输入以下命令:
$ meteor add stylus你应该在终端窗口中看到类似的响应:
stylus added, version 1.0.7Stylus 现在已安装并准备好使用。你可以通过创建一个
.styl文件并添加一个脚本(例如,将test.styl添加到你的client/styles文件夹)来测试这一点。
当你使用meteor命令启动你的 Meteor 应用程序时,Stylus 文件将被处理,并渲染适当的 CSS。
小贴士
你也可以在 Meteor 中使用nib代码。只需将@import 'nib'添加到你的.styl文件中,Meteor 就会处理其余部分。
使用 Less
-
打开终端窗口并导航到你的项目根目录。
-
输入以下命令:
$ meteor add less你应该在终端窗口中看到以下响应:
less added, version 1.0.14现在 Less 包已安装,你可以使用 Less 样式表语法来创建你的 CSS。
就像 Stylus 一样,你可以通过创建一个.less文件并添加一些样式声明(例如,将test.less添加到你的client/styles文件夹)来测试这一点。
当你使用meteor命令启动你的 Meteor 应用程序时,Less 文件将被 Meteor 编译成标准 CSS 并按常规渲染。
小贴士
如果你喜欢在你的 Less 样式表中使用@import语句,请确保使用.lessimport扩展名。否则,Meteor 将自动导入和编译它找到的任何.less文件。
使用 SCSS / SASS
-
打开终端窗口并导航到你的项目根目录。
-
输入以下命令:
$ meteor add fourseven:scss你应该在终端窗口中看到类似的响应:
fourseven:scss added, version 2.1.1
现在可以使用 SCSS 和 SASS 文件来设置你的 CSS 样式。就像之前一样,你可以通过创建一个 .scss 或 .sass 文件并添加一些样式声明(例如,将 test.sass 添加到你的 client/styles 文件夹中)来测试这一点。
当你使用 meteor 命令启动你的 Meteor 应用程序时,SCSS 或 SASS 文件将被 Meteor 编译成标准 CSS 并渲染。
它是如何工作的...
当你使用 meteor add 命令安装任何预处理程序时,它会安装相应的 npm 包,以便在 Meteor 内部工作。
就像其他文件一样,Meteor 会监控任何 *.styl、.less、.scss 和 .sass 文件的更改,将更改编译成 CSS,并立即渲染更改。
参见
- 在第二章的 添加 Meteor 包 菜单中,使用包进行自定义
第二章. 使用包进行定制
在本章中,我们将涵盖以下主题:
-
添加 Meteor 包
-
移除 Meteor 包
-
使用 Atmosphere 发现新包
-
使用 Iron Router 创建多页面应用程序
-
构建自定义包
-
使用 npm 模块
-
将自定义包发布到 Atmosphere
简介
Meteor 中的包系统使你的开发生活变得更加轻松。这也体现了 Meteor 的核心原则之一——模块化开发。如果你想使用整个默认的 Meteor 堆栈,那太好了!如果你不喜欢某个部分,并想用第三方包替换它,那也很好!这完全取决于你。Meteor 允许你快速添加和删除功能,使用其他人共享的最新代码,并创建你自己的可重用代码段。本章将为你提供充分利用 Meteor 包系统所需的食谱。
添加 Meteor 包
核心 Meteor 开发组(MDG)已经为你开发了超过 140 个包供你使用。这些包提供了从简单的显示调整到完全集成的账户管理等功能和功能。这些包不仅很有用,而且非常容易添加到你的项目中。除了核心 MDG 包之外,还有数百个第三方包可供选择,所有这些包都是免费的,并且可以同样容易地添加。这个食谱将向你展示如何将 Meteor 包添加到你的项目中。
准备工作
你需要安装 Meteor 并创建一个项目。任何项目都可以。你还应该打开一个终端窗口,导航到你的项目根目录,例如,如果你的项目名称是packagesTest,位于~/Documents/MeteorProjects文件夹中,你需要在终端窗口中输入以下命令:
$ cd ~/Documents/MeteorProjects/packagesTest
如何做...
让我们以安装fastclick包为例。这个包消除了移动/触摸屏的 300 毫秒延迟。
在你的终端窗口中,在根目录下,输入以下命令:
$ meteor add fastclick
这将在你的项目中安装 bootstrap 包,并显示类似以下的消息:

它是如何工作的...
meteor add [包名]命令告诉 Meteor 在 Meteor 包注册表中查找命名的包,并将适当的文件复制到你的项目中。此外,包名被添加到你的项目的声明文件中,这样当 Meteor 启动你的项目时,将添加并执行命名的项目文件。
还有更多...
如前所述,你可以使用相同的meteor add命令安装第三方包。核心 MDG 包和第三方包之间的区别在于,第三方包有创建者的账户 ID 作为前缀,例如,要添加优秀的 HammerJS 包到你的应用中,你需要输入以下命令:
$ meteor add chriswessels:hammer
本章中的其他食谱将指导你发现和实现第三方包,所以请保持关注!
参见
- 本章中的 使用 Atmosphere 发现新包 菜谱
移除 Meteor 包
移除 Meteor 包与添加它们一样简单。这个菜谱将向你展示如何快速移除一个 Meteor 包。
准备工作
你需要安装 Meteor 并创建一个项目。你还应该打开一个终端窗口,并导航到你的项目根目录。例如,如果你的项目名称是 packagesTest,位于 ~/Documents/MeteorProjects 文件夹中,请在终端窗口中输入以下命令:
$ cd ~/Documents/MeteorProjects/packagesTest
如何操作...
让我们移除 insecure Meteor 包。在你的终端窗口中,输入以下命令:
$ meteor remove insecure
这将移除 insecure 包。
工作原理...
meteor remove [包名] 命令将指导 Meteor 在你的项目声明文件中查找指定的包,并从你的项目中移除该包的声明和源文件。
更多...
有时候检查并查看你添加到项目中的包非常有帮助。为了查看这个列表,请在你的终端窗口中输入以下命令:
$ meteor list
这将给你一个包含在你项目中的所有包的快速列表。
使用 Atmosphere 发现新包
Meteor 是一个新兴的平台,每天都在增长其受欢迎程度。Meteor 社区几乎每天都在提出新的包和与现有 JavaScript 库的集成。由于核心 Meteor 团队没有时间测试和应用社区制作的每个新包,因此创建了一个包注册表,它具有简化的安装流程。这个包注册表被称为 Atmosphere。Atmosphere 拥有干净、简单的用户界面,允许你通过流行度搜索、评分和发现新包。这个菜谱将向你展示如何使用 Atmosphere 来查找和实现 Meteor 和第三方包。
准备工作
这里实际上没有什么可做的,但你绝对需要安装 Meteor 并创建一个项目,以便立即开始使用 Atmosphere 上列出的包!
如何操作...
要使用 Atmosphere 发现新包,请按照以下步骤操作:
-
在浏览器中,导航到
atmospherejs.com。你应该会看到一个带有居中搜索输入框的非常简单的页面。让我们查找官方的 Twitterbootstrap包。 -
输入单词
bootstrap并观察搜索结果随着你的输入出现。你应该会看到以下截图类似的内容:![如何操作…]()
你会注意到搜索结果中的每个卡片底部都有一些统计数据。向下箭头统计是下载次数,这(与包的年龄)是搜索结果排名中的主要统计指标。你会看到一个星号,代表有多少开发者将该包标记为收藏/星标。最后,你将看到一个背景中的灰色线条,表示随时间的变化趋势。
注意
第二个结果(
twbs:bootstrap)是官方的 Twitter Bootstrap 包。 -
点击
twbs:bootstrap卡片,将出现该包的概述。你将看到有关该包的更多信息。这包括使用说明、GitHub 仓库链接(如果有的话)、包的扩展历史、相关包、依赖项和依赖包。在页面顶部将会有添加包到项目的说明:![如何做…]()
-
你可以复制文本并在终端窗口中执行它,在你的根项目文件夹中,如下所示:
$ meteor add twbs:bootstrap在简短的下载过程之后,官方 Bootstrap 包将被安装到你的项目中。
如前例所述,Atmosphere 有几种额外的发现选项。有链接到包贡献者个人资料(这样你可以看到他们可能贡献的其他包),如下所示:
![如何做…]()
如以下截图所示,有相关包的链接:
![如何做…]()
如以下截图所示,还有链接到依赖项和依赖包:
![如何做…]()
任何这些链接都将帮助你发现其他可以补充原始包并可能在你的应用程序中很有用的包。
在 Atmosphere 中查找包的选项还有更多。如果你导航回主页atmospherejs.com/并向下滚动,你会看到趋势、最常用、最近和热门搜索等部分。所有这些都值得浏览,以发现一些最新和最好的可用包:

随意探索和发现。你会在 Atmosphere 中发现一些隐藏的宝藏,而且最重要的是,你会意识到几乎每样东西都有一个 Meteor 包。如果没有,你可以创建一个,就像本章后面找到的“构建自定义包”食谱一样。
它是如何工作的…
Atmosphere 网站只是简单地在 Meteor 包仓库的顶部添加了一个用户友好的包装。它做得非常好,该网站的贡献者/创建者应受到赞扬。Atmosphere 正在不断改进,如果你愿意,你可以为其成功做出贡献。你可以通过访问 Atmosphere FAQ 来查看即将推出什么内容,该 FAQ 位于atmospherejs.com/i/faq。
还有更多…
如果你急于行事或者非常喜欢命令行,你可以在项目根目录的终端窗口中执行搜索。以下是一个基于 CLI 的搜索示例:
$ meteor search bootstrap
结果将以字母顺序列出,每个结果都有简要说明:

请注意,这个结果可能相当庞大(没有结果优先级),因此使用基于 CLI 的搜索的有效性会有所不同。
参见
- 本章中的构建自定义包和将自定义包发布到 Atmosphere配方
使用 Iron Router 创建多页面应用程序
Iron Router 是一个非常有用的 Atmosphere 包。它允许你快速轻松地向你的 Meteor 项目添加多个服务器页面。这个配方将向你展示如何配置你的项目以使用 Iron Router。
准备工作
你需要安装 Meteor。你还需要一个空的 Meteor 项目(参见第一章中的设置你的项目文件结构配方,优化你的工作流程)。
最后,你需要在你的 Meteor 项目中添加 Iron Router 包。你可以参考本章前面找到的添加 Meteor 包配方,或者在项目根目录的终端窗口中输入以下命令:
$ meteor add iron:router
如何做到这一点...
我们将创建一个非常简单的多页面示例,并使用一个全新的空白项目来完成。
-
首先,设置多个页面的路由路径。在你的项目
both子文件夹中创建一个名为router的子文件夹。这可以在导航到你的项目根文件夹后,在终端窗口中执行以下命令来完成:$ mkdir both/router -
现在,在
router文件夹中创建一个名为router.js的新文件。在文本编辑器中打开router.js文件,添加以下代码,并保存你的更改:Router.configure({ layout:'routeexample' }); Router.map(function (){ this.route('thing1', { path: '/' }); this.route('thing2', { path: '/second' }); }); -
接下来,在编辑器中打开位于
client子文件夹中的main.html文件。将以下模板声明添加到文件的底部并保存更改:<template name="routeexample"> {{yield}} </template> <template name="thing1"> <div> I am the FIRST page</div> </template> <template name="thing2"> <div> I am the SECOND page</div> </template> -
现在,在编辑器中打开位于
[项目文件夹]/client/scripts/main.js的main.js文件。定位到Template.hello.events方法,并将'click button'函数体更改为以下内容:Template.hello.events({ 'click button' : function () { Router.current().route.getName()=='thing1'? Router.go('thing2'): Router.go('thing1'); } }); -
使用
meteor命令保存所有文件的更改并运行你的应用程序。当你在浏览器中导航到你的项目主页(
http://localhost:3000/)时,你应该看到以下截图类似的内容:![如何做到这一点…]()
-
点击标记为点击我的按钮,或者导航到
http://localhost:3000/second/,屏幕应该变为以下内容:![如何做到这一点…]()
-
你可以通过点击屏幕上的按钮在两个页面之间继续切换。
它是如何工作的...
Iron Router 使用你的 URL 中的路径在客户端和服务器中调用事件。在大多数情况下(除非你明确使用where参数设置 Iron Router 路径在服务器上渲染),事件和随后的渲染仅在客户端发生。
从本质上讲,Iron Router 使用路径作为参数列表,根据路径中包含的信息调用函数并渲染适当的模板。
让我们分析一下我们刚刚输入的代码,以启动 Iron Router,从 router.js 开始:
Router.configure({
layout:'routeexample'
});
这段代码片段告诉 Iron Router 使用名为 routeexample 的 <template> 标签作为页面的主要/默认模板。
我们在 main.html 中创建了 routeexample 模板,如下所示:
<template name="routeexample">
{{yield}}
</template>
在这个模板中找到的 yield 语句被称为主要 yield。它是一个占位符,用于其他模板,一旦 Iron Router 完成映射过程,这些模板将被渲染。
继续阅读,在 router.js 文件中,我们有 Router.map 函数:
Router.map(function (){
this.route('thing1', {
path: '/'
});
this.route('thing2', {
path: '/second'
});
});
这创建了两个映射的路由,使用 this.route 函数。在每个函数调用中,第一个参数指定了路由的名称,因为我们没有指定特定的模板,这也意味着它也隐含了要使用的模板名称。在这种情况下,我们使用了 thing1 和 thing2 模板,这些模板我们在 main.html 中创建:
<template name="thing1">
<div> I am the FIRST page</div>
</template>
<template name="thing2">
<div> I am the SECOND page</div>
</template>
这些模板非常简单,这也是它们在我们的例子中易于理解的原因。每个模板都会渲染一个简单的 div 元素,其中包含一个消息,指示用户当前所在的页面。
回到 router.js,每个 this.route 函数调用中都有一个路径参数。这个路径参数告诉 Iron Router 在 URL 中寻找什么以确定映射。在我们的第一个页面(渲染 thing1 模板)的情况下,path 元素被设置为 "/",即根路径。在我们的第二个页面(渲染 thing2 模板)的情况下,path 被设置为 "/second"。
到目前为止,Iron Router 已经正确映射,这意味着它将根据映射的路由渲染适当的模板。我们希望使在两个路由之间切换变得容易,因此我们修改了 main.js 脚本,如下所示:
Template.hello.events({
'click button' : function () {
Router.current().route.getName()=='thing1'? Router.go('thing2'): Router.go('thing1');
}
});
现在,当我们的按钮被点击时,我们会检查当前映射的路由名称(通过 Router.current().route.getName() 获取)是否为 thing1。如果是,我们告诉 Iron Router 将当前渲染的模板(以及 URL 路径)更改为名为 thing2 的第二个模板,其 path 值设置为 "/second"。如果当前映射的路由不是 thing1,则切换回原始映射的路由。
Router.go 语句更改了 URL 路径,从而触发了事件,以渲染适当的模板并将其插入到主要 yield 中。
还有更多…
Iron Router 是一个非常复杂且强大的包,它包含许多有用的功能,例如预渲染事件、设置数据上下文以及访问服务器端函数。如果你打算构建一个多页面的 Meteor 应用程序,你将需要仔细阅读完整的文档,该文档可在 github.com/EventedMind/iron-router 找到。
参见
- 在第十二章 创建有用的项目 的 使用 Iron Router 创建完整的应用程序 菜谱,创建有用的项目
构建自定义包
随着你对 Meteor 的熟悉程度提高,你将想要开始创建自己的自定义包,该包将整合你在多个项目中可能发现有用的代码。这个菜谱将指导你创建自己的个人 Meteor 包的基础知识。
准备工作
制作这个食谱只需要 Meteor 和一个文本编辑器。
如何操作…
我们将创建一个包,它将使我们能够轻松地在客户端网页控制台以及服务器终端控制台中写入控制台。
-
要创建一个新的基线包,打开一个终端窗口并导航到你希望你的包驻留的位置。一旦到达那里,执行以下命令:
$ meteor create --package [myMeteorID]:testpack确保将
[myMeteorID]替换为你自己的 Meteor 开发者账户 ID 或你所属的 Meteor 组织 ID,例如(在本章的其余部分),我们将使用packtmeteor组织。因此,我们的命令将如下所示:$ meteor create --package packtmeteor:testpack -
我们现在将添加我们包的代码。打开名为
testpack.js的文件,并添加以下代码:TestPack = { log: function(msg){ console.log(msg); Meteor.call('serverlog',msg); } };保存你的更改,并在同一文件夹中创建一个名为
servertestpack.js的新文件。打开此文件进行编辑,并添加以下代码:Meteor.methods({ serverlog : function(msg){ console.log(msg); } }); -
我们现在需要对
package.js进行一些修改,以确保我们的包能够正常工作。打开package.js并修改Package.onUse()方法,使其看起来如下所示:Package.onUse(function(api) { api.versionsFrom('1.0.3.2'); api.addFiles('testpack.js', 'client'); api.addFiles('servertestpack.js', 'server'); });接下来,在
Package.onUse()方法中添加以下代码,紧接在最后一个api.AddFiles()调用之后:Package.onUse(function(api) { api.addFiles('servertestpack.js', 'server'); if (api.export){ api.export('TestPack'); } });最后,通过从测试文件名中移除前缀来修改
Package.onTest()方法,如下所示:Package.onTest(function(api) { api.use('tinytest'); api.use('packtmeteor:testpack'); api.addFiles('testpack-tests.js'); }); -
保存所有更改,你的包就准备好在应用程序中使用。选择一个你正在进行的项目,或者使用
meteor create命令创建一个默认的 Meteor 项目,并在[项目根]/packages/中添加一个子文件夹。将testpack/文件夹复制并粘贴到[项目根]/packages/文件夹中。然后,在终端窗口中,执行以下命令:$ meteor add packtmeteor:testpack你的自定义包将注册用于你的项目,你现在可以启动你的应用程序:
$ meteor打开浏览器,导航到
http://localhost:3000并打开网页控制台。在网页控制台中,输入以下命令:> TestPack.log('my package is worky!')你应该在浏览器控制台中看到以下屏幕:
![如何操作…]()
你应该在服务器终端控制台中看到以下内容:
![如何操作…]()
恭喜!你刚刚创建了自己的个人包。
工作原理…
在 Meteor 中,包实际上只是有组织的文件集合,其中有一个配置文件告诉 Meteor 如何使用这些文件。
在这个例子中,我们使用 meteor create --package 命令创建了一个默认的包模板。请注意,我们遵循了 [所有者]:[包名] 的命名约定,这是 Meteor 所要求的,如果我们想在 Atmosphere 上发布此包供他人使用(有关详细信息,请参阅本章中的 将自定义包发布到 Atmosphere 菜单)。
一旦创建,我们就添加了我们的功能,这非常简单。我们只是在客户端和服务器上调用 console.log()。
然后,我们修改了配置文件,该文件总是命名为 package.js。以下表格显示了该文件包含的三个部分:
Package.describe() |
这包含了我们的包的名称、摘要和版本号。 |
|---|---|
Package.onUse() |
这列出了依赖项并公开了我们包中可用的方法。 |
Package.onTest() |
这列出了测试期间要运行的依赖项和测试方法。 |
对于这个配方,我们没有修改 Package.describe() 方法。Meteor 自动为我们创建了这一部分,并给了我们一个 版本号,这是必需的。
小贴士
包的版本号遵循 语义化版本规范(SemVer)(3 个数字+一个可选的包装,例如 1.0.2-rc_beta)。有关 SemVer 规范的更多信息,请参阅 semver.org/
我们的大部分更改发生在 Package.onUse() 中,所以我们将逐项说明这个方法。
api.versionsFrom() 方法指定了你的包中使用的核心包应该来自哪个 Meteor 版本。这很有用,因为 Meteor 更新得很频繁,而且一些运行你的包所需的依赖项可能在后续版本中发生变化。此方法允许你将你在应用程序中使用的依赖包的发布版本“冻结”,以防止兼容性问题。
api.addFiles() 方法允许我们包含在包目录中找到的文件,并指定它们是客户端特定还是服务器特定。在我们的例子中,我们使用 api.addFiles() 与 client 参数一起告诉 Meteor,testpack.js 文件是客户端特定的。然后我们使用 server 参数告诉 Meteor,servertestpack.js 文件是服务器特定的。
然后,我们使用了 api.export() 方法来声明/暴露 TestPack 对象。这使得我们能够在浏览器的网页控制台中使用 TestPack.log() 调用。
最后,我们有 Package.onTest() 方法。它包含两个 api.use() 方法调用,这些调用声明了在测试期间运行 testpack-test.js 文件所需的包依赖项。
小贴士
api.use() 方法不仅限于测试。你还可以在 Package.onUse() 方法中使用它来包含 MDG 核心或第三方包。
完成我们的修改后,我们将整个包复制到了项目中的 packages/ 子文件夹。这对于任何未部署的包都是必要的。当你使用 meteor add 命令添加包时,Meteor 会检查两个地方:在线 Meteor 包仓库(Atmosphere)和 [项目根]/packages/ 子文件夹。在这种情况下,因为我们的包没有部署到 Atmosphere,我们在 packages/ 子文件夹中有一个包的副本。
还有更多...
存在着几种其他配置包的选项(其中一些包含在本章的其他菜谱中)。这些选项的完整列表可以在 Meteor 文档中找到,文档位于 docs.meteor.com/#/full/packagejs。
相关内容
-
本章中的 Using npm modules 和 Publishing custom packages to Atmosphere 菜单
-
第十一章中的 Building custom server methods 菜单,利用高级功能
使用 npm 模块
Node 包管理器(NPM)有数千个模块可供使用。知道如何在 Meteor 项目中添加 npm 模块,你可以访问所有这些模块。本菜谱将向你展示如何做到这一点。
准备工作
你需要确保你已经安装了 Meteor 和 Node/npm。
你还需要一个使用自定义构建包的项目。我们将使用本章中 Building a custom package 菜单中的项目。
如何操作...
我们首先需要添加一个对我们要添加的 npm 包的引用,在这个例子中,将是 colors 模块。
-
在你的自定义包(应用
packages/子文件夹中的那个)的package.js文件中,将Npm.depends语句添加到文件末尾,如下例所示:Npm.depends({ "colors": "0.6.2" }); -
现在我们需要向
colors模块添加一个Npm.require引用,并将我们的console.log()命令更改为使用彩虹水果口味。将你的server-test.js文件修改如下例所示:var colors = Npm.require('colors'); Meteor.methods({ serverlog : function(msg,warn){ console.log(msg.rainbow); } }); -
保存你的更改,我们就可以测试更新后的包了。在浏览器中导航到
http://localhost:3000,打开浏览器控制台,并多次执行以下命令:> TestPack.log('rainbows and unicorns') -
如果你切换到你的控制台窗口,你应该会看到一个多彩的响应,如下面的截图所示:
![如何操作…]()
它是如何工作的...
在 package.js 文件中的 Npm.depends 声明告诉 Meteor,每次启动时,它需要安装列出的 npm 模块。在这种情况下,我们告诉 Meteor 去 npm 注册表中获取 colors 模块,版本 0.6.2。
在 server-test.js 中的 Npm.require 函数调用使 colors 模块的功能在服务器上可用。现在,每次我们进行 console.log() 调用时,传递给调用的字符串可以添加一个 .color 引用。在我们的例子中,我们选择了 .rainbow,这使得打印到控制台中的每个字母都显示为不同的颜色。
小贴士
要查看 npm colors 模块中所有可用的各种选项,请访问 npmjs.org/package/colors。
更多内容…
您可以在自定义 Meteor 包中使用任何您想要的 npm 模块。其中一些依赖于异步方法和事件,因此您必须使用特定的异步 Meteor 方法来包装这些模块。您可以在第十一章([part0083.xhtml#aid-2F4UM1 "第十一章。利用高级功能")中了解更多关于这些方法的信息,利用高级功能。
相关内容
-
第八章([part0069.xhtml#aid-21PMQ1 "第八章。集成第三方库")中关于直接使用 npm 包的配方,集成第三方库
-
第十一章([part0083.xhtml#aid-2F4UM1 "第十一章。利用高级功能")中关于处理异步方法和使用异步函数的配方,利用高级功能
将自定义包发布到 Atmosphere
当您创建了一个有用的包并希望与世界分享时,Atmosphere 是您的最佳选择。这个配方将向您展示如何上传您的自定义包到 Atmosphere,并提供了一些维护您包的最佳实践。
准备工作
要创建一个快速 Atmosphere 包,请完成本章中找到的使用 npm 模块配方。
您还需要在 GitHub 上创建一个空白的仓库。假设您有 GitHub 登录,请导航到首页(github.com)并创建一个新的仓库:

为您的仓库添加标题和描述,然后点击标记为创建仓库的按钮。
要填充您的新 Git 仓库,您需要 Git URL,可以从您新 Git 项目的首页复制。它看起来可能像github.com/strack/packt-testpack.git。
记住这个 URL,因为您将使用它来填充您新仓库的内容。
如何操作…
要注册和上传我们的包,我们需要检查我们的配置文件,将我们的代码上传到 GitHub,然后部署我们的包。
-
要配置
package.js,请打开位于您包根目录下的package.js文件。在我们的例子中,该文件位于[项目根]/packages/testpack/package.js。我们首先将修改摘要,如下例所示:// Brief, one-line summary of the package. summary: 'Demonstration of an Atmosphere Package',接下来,我们将添加 GitHub URL 到
git属性:// URL to the Git repository... git: 'https://github.com/strack/packt-testpack.git',您会注意到文档属性引用了
README.md文件,这意味着我们的 GitHub 文档将具有双重用途。打开README.md并添加以下行:Demonstration of an Atmosphere Package, deployed to Atmosphere -
要将代码推送到 GitHub,请保存所有文件,并在终端窗口中导航到您的包根目录(不是应用程序的根目录)。在我们的例子中,根目录是
[项目根]/packages/testpack/。输入以下行以填充 GitHub 仓库:$ git init $ git add -A $ git commit -m "Created color server console" $ git remote add origin https://github.com/strack/packt-testpack.git $ git pull origin master –m "initial pull" $ git push origin master -
完成后,要发布到 Atmosphere,请使用以下命令调用
meteor publish:$ meteor publish --create哇!您已将您的包添加到 Atmosphere。
小贴士
我们直接从应用程序的 packages 子文件夹中部署了所有包。最佳实践是在独立于任何应用程序的情况下开发和部署您的包。请参阅 构建自定义包 的说明。
您可以通过导航到 atmospherejs.com 并按名称搜索来在 Atmosphere 上查看您的包,如下面的截图所示:

就像在 Atmosphere 中找到的任何其他 Meteor 包一样,您可以使用终端中的 meteor add 命令将其添加到您的任何项目中。
它是如何工作的…
Atmosphere 读取需要发布的 package.js 文件,检查所有信息是否准确,然后从 GitHub 仓库发布一个链接到您的项目。在这个特定案例中,我们在文件中添加了一个摘要,在 git 属性中指定了 GitHub URL,并在创建 GitHub 仓库后调用 meteor publish 来部署包。
还有更多…
更多关于如何发布到 Atmosphere 的说明可以在 atmospherejs.com/i/publishing 找到。
参见
- 本章中的 添加 Meteor 包 和 使用 Atmosphere 发现新包 的说明
第三章. 构建出色的用户界面
在本章中,我们将涵盖以下主题:
-
使用 Spacebars 插入模板
-
使用三重花括号插入原始 HTML
-
创建动态列表
-
使用 Bootstrap 构建平滑的界面
-
创建自定义的全局辅助函数
-
创建自定义组件
-
使用反应性 HTML 属性
-
使用动态模板
-
动画 DOM 元素
简介
就像 Meteor 中的其他一切一样,UI 开发被设计成简单、优雅且强大。只需几个命令,你就能创建复杂且动态的用户界面,显著加快你的原型/线框设计工作。
本章中的食谱将带你了解最常用的 UI 和模板构建技术,这些技术将成为你未来 Meteor 应用程序的基础。
使用 Spacebars 插入模板
Spacebars是 Meteor 的模板语言。在 Meteor 中,所有文档对象模型(DOM)元素都是通过 Meteor 的默认模板引擎Blaze程序化创建的。因此,可以使用纯 JavaScript 创建元素。然而,使用 JavaScript 创建 HTML 元素并不一定快速和简单。
受到流行的 HTML 模板语言Handlebars的启发,Spacebars 使得使用模板结构化和声明 HTML 元素变得极其简单。它还为你提供了访问 Meteor 所有反应性和动态功能的机会。这个食谱将向你展示如何使用 Spacebars 语法创建模板。
准备工作
你需要安装 Meteor 并创建一个项目。我们建议你有一个简单的项目,类似于在第一章中找到的设置你的项目文件结构食谱,优化你的工作流程,但任何项目都可以。你需要找到你的<body>标签,它通常位于你的main.html文件中。
如何做...
为了使这个食谱更有用,我们将创建两个模板,并使用以下步骤将一个模板嵌套在另一个模板中:
-
首先,创建一个名为
templates.html的文件,并将其添加到你的client文件夹中:![如何做...]()
-
在编辑器中打开
templates.html,添加以下代码,并保存你的更改:<template name="firstTemplate"> <div id="title">This is my first ever template</div> <svg height="200" width="100%" id="shapes"> {{> one}} </svg> </template> <template name="one"> <circle cx="100" cy="50" r="40" stroke="#ffc348" stroke-width="3" fill="#31be4f" /> </template> -
接下来,打开你的
main.html文件(或包含你的<body>标签的.html文件)并编辑你的body标签,添加以下代码:<body> {{> firstTemplate}} </body> -
保存更改,如果 Meteor 尚未运行,请启动它,并在浏览器中导航到你的 Meteor 托管页面(通常是
http://localhost:3000):![如何做...]()
它是如何工作的...
我们创建了两个模板,第一个是一个非常简单的svg元素:
<template name="one">
<circle cx="100" cy="50" r="40" stroke="#ffc348" stroke-width="3" fill="#31be4f" />
</template>
我们给模板分配了一个name属性值为"one",并添加了具有位置和外观属性的格式良好的 HTML <circle>元素。我们还创建了一个父模板,如下所示:
<template name="firstTemplate">
<div id="title">This is my first ever template</div>
<svg height="200" width="100%">
{{> one}}
</svg>
</template>
此模板还有一个名称("firstTemplate")并且包含一个<div>元素,其中包含我们模板的标题。我们声明了一个<svg>元素,并使用{{>}}命令在<svg>和</svg>标签之间插入名为one的模板。
最后,在main.html中,我们使用{{>}}命令(也称为模板包含)在主<body>和</body>标签之间插入firstTemplate模板。
如您所见,我们模板的内容主要由 HTML 标签组成。这是我们 UI 的基本构建块,也是我们从一开始创建模板的方式。任何包含在双花括号({{…}})中的内容都是针对 Blaze 的命令,Blaze 是 Meteor 的模板引擎。在这种情况下,我们告诉 Blaze:
-
将
firstTemplate模板插入到我们的<body>元素中。 -
将
one模板插入到我们的<svg>元素中,在firstTemplate内部。
还有更多…
您可以在官方 Meteor 文档中阅读有关模板的所有信息,该文档位于docs.meteor.com/#/full/templates。
使用三重花括号插入原始 HTML
有时,您可能想在您的 Meteor 页面上插入原始 HTML。这通常发生在您有由其他进程或库生成的 HTML 时。本配方将向您展示如何在 Meteor 模板中渲染您的原始 HTML。
准备工作
您需要安装 Meteor 并创建一个至少包含一个模板的项目。我们建议您使用本章中使用 Spacebars 插入模板配方中创建的文件。
如果您的client文件夹中还没有templates.js文件,您应该创建一个来将模板helpers和events逻辑与其他 JavaScript 代码分开。
如何操作…
识别您根模板的名称并打开包含模板定义的文件。在我们的例子中,根模板命名为firstTemplate,可以在我们的templates.html文件中找到,该文件位于我们的[项目根]/client文件夹中。按照以下步骤使用三重花括号插入原始 HTML:
-
在
</template>标签之前插入以下行,并保存您的更改:<template name="firstTemplate"> ... {{{rawInsert}}} </template>现在,打开您的
templates.js文件并添加以下辅助函数:Template.firstTemplate.helpers({ rawInsert: function () { return "<div><strong>Raw HTML!</strong></div>"; } }); -
保存您的更改,如果 Meteor 尚未运行,请启动它,并在浏览器中导航到您的 Meteor 托管页面(通常是
http://localhost:3000)。您应该在屏幕底部看到渲染的 HTML,如下面的截图所示:![如何操作…]()
它是如何工作的…
我们创建并调用了我们的Template.firstTemplate.rawInsert辅助函数,通过在模板底部添加{{{rawInsert}}}标签并在我们的templates.js文件中声明rawInsert辅助函数来实现。
三重花括号标签通知 Blaze,从辅助函数返回的对象将是原始 HTML,需要以这种方式渲染。相应地,Blaze 期望一个字符串,并尝试渲染它。
要查看 Blaze 通常如何处理原始 HTML,将三重花括号标签更改为双花括号标签 ( {{rawInsert}}),保存你的文件,并在浏览器中查看结果,如下面的截图所示:

参见
- 在第七章 “使用客户端方法”的 使用 SVG 和 Ajax 创建动态图表 菜谱中,使用客户端方法
创建动态列表
Meteor Spacebars 语法支持通过使用模板块标签快速开发列表、表格和其他数据展示。在模板中使用,块标签可以快速添加动态列表,甚至可以根据每个元素的独特属性自定义其外观和感觉。本菜谱将向你展示如何使用 #each 块标签在 Blaze 模板中显示集合的多个元素。
准备工作
我们将使用本章中找到的 使用 Spacebars 插入模板 菜谱中的代码作为我们的基准项目。
如果你还没有这样做,请在你的 [project root]/client 文件夹中创建并保存一个名为 templates.js 的新文件。
在你的 [project root]/both 文件夹中创建并保存一个名为 collections.js 的新文件。
我们将想要对 templates.html 文件中的 HTML 进行一项修改。我们将把我们的 <svg> 元素的高度从 200 改为 800:
<svg height="800" width="100%" id="shapes">
最后,我们希望将 random 包添加到我们的项目中,以帮助我们生成一些随机颜色和位置。在终端窗口中,导航到你的项目根目录,并输入以下命令:
$ meteor add random
如何做到这一点…
要创建动态列表,请按照以下步骤操作:
-
首先,让我们声明一个
shapes集合。打开collections.js,添加以下代码,并保存你的更改:Shapes = new Mongo.Collection('shapes'); -
现在,让我们动态地向页面添加形状。打开你的
templates.html文件,对one模板进行以下更改,并保存你的更改:<template name="one"> {{#each svgShapes}} <circle cx="{{x}}" cy="{{y}}" r="40" stroke="{{border}}" stroke-width="3" fill="{{color}}" /> {{/each}} </template> -
我们现在将创建一个模板辅助函数,它返回我们的动态集合。打开
templates.js,添加以下辅助函数,然后关闭并保存你的更改:Template.one.helpers({ svgShapes: function () { return Shapes.find(); } }); -
我们现在可以动态地向
shape集合添加形状,它们将在我们的页面上渲染。在浏览器中打开 JavaScript 控制台,并输入以下命令:> Shapes.insert({x:200,y:50,border:'#123456',color:'#bada55'})你应该在屏幕上看到一个带有深色边框的绿色圆形,类似于以下截图:
![如何做到这一点…]()
让我们添加更多圆形来证明我们的模板正在渲染
Shapes集合中的所有形状。 -
在
templates.js中,添加以下代码:Template.firstTemplate.events({ 'click svg': function (e,t){ var xpos,ypos; if(e.offsetX==undefined) { xpos = e.pageX-$('#shapes').offset().left; ypos = e.pageY-$('#shapes').offset().top; } else { xpos = e.offsetX; ypos = e.offsetY; } choices = ['#bada55','#B43831', '783BA3', '#00AB1B', '#143275', '#FFA700'], color = Random.choice(choices), border = Random.choice(choices); Shapes.insert({x:xpos,y:ypos,border:border,color:color}); }, 'dblclick': function(e,t){ Meteor.call('resetShapes'); } }); -
我们现在想显示形状的总数,因此我们需要向
Template.firstTemplate.helpers方法调用中添加一个shapeCount函数:Template.firstTemplate.helpers({ shapeCount: function () { return Shapes.find().count(); }, rawInsert: function () {... We will also need to Modify templates.html to display a count of the shapes collection: <template name="firstTemplate"> <div id="title">shapes collection count: {{shapeCount}}</div> -
让我们添加一个
resetShapes方法。打开/创建位于你的[project root]/server文件夹中的server.js文件,并添加以下代码:Meteor.methods({ resetShapes: function(){ Shapes.remove({}); } }); -
保存所有更改,如果 Meteor 还未运行,请启动它,并在浏览器中导航到你的 Meteor 主办的页面(通常是
http://localhost:3000)。现在,当你点击浏览器中的某个位置时,新的圆圈将出现,屏幕的左上角将显示你的
shapes集合计数:![如何操作…]()
随意添加你想要的任何形状,或者通过在屏幕上的任何位置双击来重置为零。
工作原理…
我们在 templates.js 中创建了 Template.one.svgShapes 辅助函数,它返回对 shapes 集合的指针。这为我们的模板设置了数据上下文,并且可以通过添加到 templates.html 中的 one 模板中的 {{#each svgShapes}} 块标签来访问。
由于模板和模板辅助函数是响应式计算,它们会在集合更新时做出反应。这种反应导致 Blaze 重新渲染页面,使用 #each 块标签内找到的动态模板。
更多内容…
其他块辅助函数也存在,并且它们同样易于使用。有关更详细的说明,请参阅 GitHub 上的 Spacebars 文档,网址为 github.com/meteor/meteor/blob/devel/packages/spacebars/README.md。
相关内容
-
在 第四章 中实现简单集合的菜谱,创建模型
-
在 第七章 中使用 SVG 和 Ajax 创建动态图形的菜谱,使用客户端方法
使用 Bootstrap 构建平滑的界面
作为一名网页开发者,你时间消耗最大的一个方面就是页面样式设计。Bootstrap 提供了一种优雅、简洁的设计,以及足够的现成组件样式,让你可以快速启动,无需陷入编写 CSS 的困境。这个菜谱将带你了解一些 Bootstrap 组件样式,并展示它们的使用是多么简单。
准备工作
我们将使用本章中找到的 创建动态列表 菜谱作为我们代码的基准。
我们还需要将官方 Bootstrap 包添加到我们的 Meteor 项目中,使用 Meteor 的包仓库。打开一个终端窗口,导航到你的项目的根目录,并输入以下命令:
$ meteor add twbs:bootstrap
小贴士
有其他一些包与官方 Bootstrap 包相辅相成。你可以在 atmospherejs.com/?q=bootstrap 找到这些包的完整列表。
如何操作…
我们将使用 Bootstrap 内置的 btn 和 navbar 样式来使标题和集合计数看起来更美观。按照以下步骤使用 Bootstrap 构建平滑的界面:
-
首先,让我们修改我们的标题,添加一个子
<div>标签,并像 Bootstrap 按钮一样对其进行样式化。打开templates.html,进行以下代码更改,并保存你的文件:<div id="title">Shapes Collection Count: <div id="resetShapes" class="btn btn-warning"> {{shapeCount}} </div> </div> -
在此更改之后,你的标题应该看起来更加引人注目,如下面的截图所示:
![如何操作…]()
-
现在,我们将添加
navbar样式到整个标题,使其具有独特、故意的样子。再次打开templates.html,并将标题<div>标签替换为以下代码:<nav class="navbar navbar-default"> <div class="container-fluid"> <div class="navbar-header"> <div id="title" class="navbar-brand">Shapes Collection Count</div> </div> <div class="nav navbar-left"> <div id="resetShapes" class="btn btn-warning navbar-btn"> {{shapeCount}} </div> </div> </div> </nav> -
保存这些更改,你的页面应该会更新。现在,Shapes Collection Count部分应该以独特的
navbar样式出现,类似于以下截图:![如何操作…]()
-
最后,让我们将计数按钮变成一个实际的按钮。打开
templates.js,将dblclick事件更改为特定的click事件。此事件放置在Template.firstTemplate.events函数的底部:Template.firstTemplate.events({ … 'click #resetShapes': function(e,t){ Meteor.call('resetShapes'); } })我们现在已经禁用了双击作为重置我们的
shapes集合的方法,并将其转移到Shapes Collection Count总数。通过点击橙色按钮,集合将重置为零:![如何操作…]()
它是如何工作的…
使用 Bootstrap 更改页面的外观和感觉只是添加正确的 CSS class声明和适当地结构化我们的 HTML。
为了样式化shapes集合计数,我们在class属性中添加了btn和btn-warning值。这允许 Bootstrap CSS 适当地样式化<div>标签,使其看起来像一个橙色按钮。
为了样式化我们的标题,我们首先必须为navbar标签创建脚手架。我们通过在本质上用<nav>标签包裹我们的<div>标签来实现这一点。然后,通过添加nav、navbar、navbar-header和navbar-left到class属性中,适当地结构化包含标签。其他的class声明(navbar-default、navbar-brand和navbar-btn)是严格的样式声明,改变颜色/形状但不影响整体结构或布局。
还有更多…
Bootstrap 非常健壮,并附带出色的文档。你可以在getbootstrap.com/了解更多关于 Bootstrap 的信息。
参见
- 在第二章的Adding Meteor packages和Discovering new packages with Atmosphere食谱中,使用包进行自定义
创建自定义的全局助手
当你真正开始深入研究模板时,你很快会发现模板助手有多么真正出色。你可以内联访问数据,并极大地减少你需要编写的代码量。
然而,你最终会遇到一个情况,你发现自己正在多个模板助手中重复相同的逻辑。如果有一种方法可以创建任何模板都可以访问的全局助手会怎么样?嗯,Meteor 也有解决方案!这个食谱将向你展示如何使用Template.registerHelper函数创建全局模板助手。
准备工作
我们将使用本章中找到的Building a smooth interface with Bootstrap食谱中的代码库。请首先遵循该食谱或下载相应的代码库。
如何操作…
我们将创建一个全局随机颜色生成器,并通过以下步骤将随机颜色添加到屏幕上的所有对象中:
-
打开位于你的
[项目根]/client文件夹中的templates.html,并对one模板的颜色属性进行以下更改:<template name="one"> {{#each svgShapes}} <circle cx="{{x}}" cy="{{y}}" r="40" stroke="{{randColor}}" stroke-width="3" fill="{{randColor}}" />{{/each}} </template> -
我们还将在
<div>标题中使用randColor辅助函数,给它一点(随机)颜色。在firstTemplate模板中,查找并按以下方式修改<div>标题:<div class="navbar-header"> <div id="title" class="navbar-brand" style="color:{{randColor}}">Shapes Collection Count</div> -
保存你的更改,如果还没有启动,请启动你的 Meteor 应用程序。当你用浏览器查看你的 Meteor 页面时,你会注意到所有的圆都变成了黑色。这是因为尽管我们引用了全局辅助函数,但我们还没有真正创建它。
-
因此,让我们来处理这个问题。打开位于你的
[项目根]/client文件夹中的templates.js,并在底部添加以下函数:Template.registerHelper('randColor',function(){ choices = ['#bada55','#B43831', '783BA3', '#00AB1B', '#143275', '#FFA700']; return Random.choice(choices); }); -
保存更改后,你的页面将刷新,圆现在将具有随机颜色,应用程序的标题栏也是如此。为了证明它确实使用了全局的
randColor辅助函数,请打开另一个浏览器窗口,导航到你的页面(通常是http://localhost:3000),并查看结果。当你点击页面以添加圆或刷新页面时,颜色将随机变化,并且与下一个浏览器窗口不同,如以下截图所示:![如何做到这一点…]()
它是如何工作的...
Template.registerHelper 函数声明了一个可以在任何模板上下文中看到的辅助函数。把它想象成一种声明全局函数的方法,这些函数可以在 Spacebars 语法中使用。
Template.registerHelper 的第一个参数是一个字符串值,它包含我们辅助函数的名称(在模板内联引用时使用的名称)。在为全局辅助函数选择名称时,请记住它确实是全局的。因此,它应该是唯一的,以防止冲突,或者其值可能会被特定模板的辅助函数覆盖。我们的例子非常简单,所以我们可以使用 randColor;然而,随着你构建更复杂的模板,给你的辅助函数起更独特的名称是个好主意。返回的值显然是随机选择的一种颜色,使用的是我们在原始 firstTemplate 点击事件处理器中使用的相同技术。
我们随后在两个不同的模板中使用了这个全局辅助函数。首先,我们将圆的 svg 声明中的 {{color}} 和 {{border}} 替换为 {{randColor}},导致我们的圆每次都显示为随机颜色。最后,我们在 <div> 标题中添加了 style="color:{{randColor}}" 属性,这样在页面刷新时就会随机分配文本颜色。
还有更多...
使用全局辅助函数,你可以完成一些非常高级的技术,例如使用其他与模板相关的函数,如 Template.instance() 和 Template.currentData。所有这些内容都无法在本食谱中涵盖,但我们鼓励你探索 Meteor 文档,以便熟悉全局辅助函数能做什么。
小贴士
要了解可用的模板实用工具的概述,请参阅 Meteor 文档 docs.meteor.com/#/full/templates_api。
参见
- 本章中的 使用 Spacebars 插入模板 和 使用 HTML 属性进行响应式操作 食谱
创建自定义组件
在 Meteor 中添加和使用模板是加快你工作流程的绝佳方式。使用 Meteor 内置的内容块辅助函数可以进一步简化你的工作流程,通过允许嵌套和延迟模板调用。本质上,你可以组合和嵌套模板来创建自己的“组件”。本食谱将向你展示如何使用 Template.contentBlock 构建 Spacebars 自定义块辅助函数。
准备工作
我们将使用本章中 使用 Bootstrap 构建平滑界面 食谱中的代码库。请首先遵循该食谱,或下载相应的代码库。
如何做到这一点...
按照以下步骤创建自定义组件:
-
我们需要将特定于模板的
shapeCount辅助函数全局化,以便我们可以在组件内部使用它。在templates.js文件中,删除Template.firstTemplate.helpers()方法调用中找到的shapeCount函数,并将以下函数添加到页面底部:Template.registerHelper('shapeCount',function (){ return Shapes.find().count(); }); -
现在,我们将创建我们的组件。我们首先创建一个
navbutton模板,它将使用新创建的shapeCount辅助函数。在你的[project_root]/client文件夹中创建一个新的模板文件,命名为navbar.html。在文本编辑器中打开此文件,并添加以下模板代码:<template name="navbutton"> <div class="nav navbar-left"> <div id="resetShapes" class="btn btn-warning navbar-btn"> {{shapeCount}} </div> </div> </template> -
要创建自定义的
navbar块标签,将navbar模板添加到同一文件中,包括Template.contentBlock引用,如下所示:<template name="navbar"> <nav class="navbar navbar-default"> <div class="container-fluid"> <div class="navbar-header"> <div id="title" class="navbar-brand">Shapes Collection Count</div> </div> {{> Template.contentBlock}} </div> </nav> </template> -
我们组件现在已完成!让我们在我们的代码中使用它。打开
templates.html并完全删除<nav>元素块,包括firstTemplate模板中的代码。此更改后,将只剩下<svg>元素。就在<svg>元素上方,我们将使用{{#[template name]}}块标签调用我们的navbar模板,如下所示:<template name="firstTemplate"> {{#navbar}} {{> navbutton}} {{/navbar}} <svg> -
保存所有更改,如果 Meteor 实例尚未运行,请启动它,并在浏览器中查看结果。你会注意到没有任何变化。我们具有与之前相同的功能;但现在,我们已经创建了一个自定义 Spacebars 组件(正式上称为自定义块辅助工具),并且可以根据需要向
navbar标签添加更多组件或输入显式 HTML,例如,将{{> navbutton}}替换为<div>{{shapeCount}}</div>将会显示 形状集合计数,而无需使用navbutton模板,类似于以下截图(不太美观,但你能理解这个意思):![如何操作…]()
如果你检查浏览器中的元素,你会注意到手动输入的 <div>…</div> 已经被插入到占位符块中,类似于以下示例:
<nav class="navbar navbar-default">
...
<div>15</div>
...
</nav>
工作原理…
构建块辅助工具的关键在于 Template.contentBlock 函数。我们创建了一个新的 navbar 模板,并在该模板内部使用了 {{> Template.contentBlock}} 来指定一个占位符。这个占位符(称为内容 块)可以在稍后指定,而不是必须显式地在 navbar 模板内部指定。这个简单的添加内容块的操作将我们的 navbar 模板转换成了一个 组件。
一旦我们创建了块辅助工具以及要放入块辅助工具中的模板,我们就添加了一个 Spacebars 块标签,带有 {{#navbar}} 和结束的 {{/navbar}} 调用。在这个块内部,我们使用 {{> navbutton}} 调用我们的新 navbutton 模板,它被插入到 {{> Template.contentBlock}} 占位符中。在运行时,所有这些模板函数将按顺序执行,Blaze 将渲染实际的 HTML DOM 元素。
更多内容…
自定义块辅助工具也可以通过使用 UI.elseBlock 来利用 #if、#else 和 #unless 逻辑。使用这些命令,你可以创建可重用、动态的块辅助工具和模板,进一步简化你的开发过程。
小贴士
要详细了解所有选项,请参阅 Spacebars 的 GitHub 文档,位于 github.com/meteor/meteor/blob/devel/packages/spacebars/README.md。
参见
- 本章中的 使用 Spacebars 插入模板 和 创建自定义全局辅助工具 食谱
使用反应性 HTML 属性
在 Meteor 中,隐藏的宝藏之一是你对 HTML 渲染的极细粒度控制。Blaze 是这样设计的,你可以动态地修改、添加或删除 HTML 元素属性,而不会影响 DOM 中的其他任何内容!本食谱将指导你如何在元素属性中使用动态、反应性数据。
准备工作
我们将使用来自 创建自定义组件 食谱的代码库,这是本章中的前一个食谱。请首先遵循该食谱或下载相应的代码库。
如何操作…
按照以下步骤使用 HTML 属性进行响应性操作:
-
我们首先需要创建一个新的
navcolor模板。在您的[project root]/client文件夹中打开navbar.html并添加以下代码:<template name="navcolor"> <div class="nav navbar-form navbar-right"> <input type="checkbox" id='bgCheck' checked="{{useBG}}"/> <input type="text" id='bgColor' placeholder="#bgcolor" value="{{bgColor}}" /> </div> </template> -
我们现在需要将我们的新模板添加到
navbar内容块中,以便我们可以输入新的背景颜色并开启或关闭它。打开templates.html(同样在client文件夹中)并修改firstTemplate模板,在{{> navbutton}}调用下方添加一个模板调用,类似于以下内容:{{#navbar this}} {{> navbutton}} {{> navcolor}} {{/navbar}} -
在下一行,给
<svg>元素添加一个style属性,这样我们就可以更改背景颜色,如下所示:<svg height="800" width="100%" id="shapes" style="background-color:{{bgColor}}"> -
在前面的代码中,我们设置了两个辅助函数的引用:
bgColor和useBG。现在我们将创建这些全局模板辅助函数。打开templates.js并添加以下两个函数:Template.registerHelper('bgColor', function () { if (Session.equals('useBG', true)) {return Session.get('bgColor'); } else return null; }); Template.registerHelper('useBG', function(){return Session.get('useBG'); }); -
如果我们保存这些更改并打开浏览器中的网络控制台,我们现在可以以编程方式更改和切换背景颜色。在网络控制台中,执行以下两行:
> Session.set('bgColor','lig htgrey') > Session.set('useBG',true)屏幕上应该有以下三个变化:
-
现在背景应该是浅灰色
-
您
navbar标签右上角的复选框应该被勾选 -
现在右上角的文本框中应该出现lightgrey这个词
您的屏幕现在应该看起来类似于以下截图:
![如何操作…]()
-
-
剩下的就是将事件连接到复选框和文本框的
<input>元素。在您的[project_root]/client文件夹中创建一个名为navbar.js的新文件,并添加以下events处理程序声明:Template.navcolor.events({ 'click #bgCheck': function (e, c) { Session.set('useBG', e.currentTarget.checked); }, 'change #bgColor, keyup #bgColor': function (e, c) { if (!Session.equals('bgColor', e.currentTarget.value)) { Session.set('bgColor', e.currentTarget.value); } } }); -
保存您的更改,您的元素现在将影响背景。勾选/取消勾选复选框将切换背景颜色,更改文本框中的值将动态更改显示的背景。
-
我们想观察这将对 DOM 产生的影响。打开浏览器中的
开发者面板并定位到<svg>元素。当您使用复选框切换背景的开启和关闭时,观察顶部<svg>标签内的属性列表。您会注意到,根据复选框的状态,style属性会出现和消失,类似于以下截图:![如何操作…]()
如您所见,属性不仅仅是变为空。它被完全移除了。您还会注意到,在这种情况下,屏幕或元素视图都没有刷新。这是因为 DOM 没有被重建。属性是根据{{bgColor}}的状态被添加和移除的。同样,当您修改文本框中的值时,其他什么都不会受到影响(没有屏幕刷新,或状态丢失)。
它是如何工作的…
随着 Blaze 模板引擎的引入,Meteor 停止了字符串操作,并开始直接与 DOM 树/DOM 元素交互。与其它模板库相比,这种变化简化了开发过程,因为 DOM 元素不需要在 HTML 每次更改时都重新渲染。
在style属性的情况下,属性内的唯一参数是对bgColor辅助函数的引用。如果辅助函数或数据上下文中的值是null/undefined,并且该值是属性中的唯一值,Blaze 会自动移除该属性,而不是将其渲染为无值。
因此,在这种情况下,当{{bgColor}}辅助函数返回null(当Session.useBG为false或undefined时),Blaze 会从<svg>元素中移除style属性。
参考内容
-
本章中的使用 Spacebars 插入模板食谱
-
在第四章的使用 Session 对象食谱中,创建模型
使用动态模板
Meteor 还有另一项模板技巧!除了自定义代码块、响应式数据上下文以及所有其他各种模板优点外,Meteor 还允许你动态地在渲染的模板之间切换。本食谱将展示如何使用Template.dynamic辅助函数动态渲染模板。
准备工作
我们将使用本章中创建自定义组件食谱中的代码库。请首先遵循该食谱或下载相应的代码库。
如何操作...
我们将添加一个动态模板,在渲染时将我们的形状从圆形切换到正方形。按照以下步骤使用动态模板:
-
首先,我们需要创建我们的备用模板。在
templates.html中,位于你的[项目根]/client文件夹内,添加以下模板,就在现有one模板声明下方:<template name="two"> {{#each svgShapes}} <rect x="{{x}}" y="{{y}}" width="80" height="80" stroke="{{border}}" stroke-width="3" fill="{{color}}" /> {{/each}} </template> -
我们需要为这个模板提供一个数据上下文。幸运的是,我们可以重用
Shapes集合。打开templates.js,复制svgShapes函数,并将其添加到新的Template.two.helpers()方法调用中,如下所示:Template.two.helpers({ svgShapes: function () { return Shapes.find(); } });现在,我们将根据
Session.curTemplate变量在one和two模板之间切换逻辑。在templates.js的底部,添加以下helpers()方法调用:Template.firstTemplate.helpers({ curTemplate: function () { var curTempl = Session.get('curTemplate'); if (!curTempl) { curTempl = 'one'; Session.set('curTemplate', curTempl); } return curTempl; } }); -
剩下的工作就是添加动态模板声明并创建一种简单的方法来在模板之间切换。让我们先处理声明。打开
templates.html并修改<svg>元素内的双括号,如下所示:<svg height="800" width="100%" id="shapes"> {{> Template.dynamic template = curTemplate}} </svg> -
现在,我们将添加一些切换按钮并将事件连接到更改
Session.curTemplate变量的值。打开navbar.html并在底部添加以下模板:<template name="navshapes"> <div class="nav navbar-right"> <button id="btnCircles" class="btn btn-success navbar-btn">one</button> <button id="btnSquares" class="btn btn-danger navbar-btn">two</button> </div> </template> -
我们需要在我们的块助手中包含
navshapes模板。打开templates.html并在{{/navbar}}块标签之前添加以下模板包含:{{#navbar}} ... {{> navshapes}} {{/navbar}} -
最后一个添加,为了连接事件,我们就可以开始了。在您的
[项目根]/client文件夹中创建一个navbar.js文件,并添加以下代码:Template.navshapes.events({ 'click button.btn': function(e,c){ Session.set('curTemplate',e.currentTarget.textContent); } }); -
保存所有更改,如果项目尚未运行,请启动您的 Meteor 项目,并在浏览器中导航到您的项目(通常是
http://localhost:3000)。您将在屏幕右上角看到两个按钮。点击它们,来回切换,以查看屏幕上的形状在圆形和正方形之间切换,类似于以下截图:![如何做到这一点...]()
它是如何工作的…
Template.dynamic 接收一个 template 参数,并使用适当的名称渲染模板。因为我们通过使用 curTemplate 助手使模板参数动态和响应式,所以渲染的模板将随着 Session.curTemplate 的值的变化而变化。这种动态渲染模板的能力是流行的 Meteor 包 iron:router 的基础。它可以用在多个你想根据响应式数据源切换模板的情况中。
还有更多…
Iron Router 将动态模板提升到了全新的水平。Iron Router 的文档可以在 github.com/iron-meteor/iron-router/blob/devel/Guide.md 找到。
另请参阅
-
在第二章 使用包定制 的 使用 Iron Router 创建多页面应用 菜谱,使用包定制
-
本章的 创建自定义组件 菜谱
-
在第四章 创建模型 的 使用 Session 对象菜谱,创建模型
-
在第十二章 创建完整应用与 Iron Router 的 使用 Iron Router 创建完整应用 菜谱,创建有用项目
动画 DOM 元素
Meteor 有一种非常优雅的方式来渲染我们的 Web 应用中的 DOM 元素,但直到现在,这还没有包括动画。幸运的是,现在 Meteor 支持动画,包括当元素首次添加到 DOM 时发生的动画。这个菜谱将指导你如何在 Meteor 中使用标准的 CSS 动画技术。
准备工作
我们将使用本章中 使用动态模板 菜谱的代码库。请首先遵循该菜谱或下载相应的代码库。
如何做到这一点...
我们将动画化我们之前菜谱中的形状,演示在元素首次创建和渲染之前和之后的动画效果。
由于我们使用了 SVG 元素而不是 DOM 元素,我们需要修改 jQuery 以能够使用 .addClass 和 .removeClass。幸运的是,有一个自定义包可以同时添加 SVG 支持和 Meteor UI 事件支持。
-
在终端窗口中,导航到你的项目根目录并执行以下行:
$ meteor add appmill:animation-hooks这将安装
animation-hooks包,该包包括 jQuery 对 SVG 的支持。 -
我们现在将添加一个非常简单的悬停动画效果。在
[项目根目录]/client/lib/styles文件夹中打开或创建一个style.css文件,并添加以下代码:circle,rect { opacity:1; transition:all 500ms 0ms ease-in; } circle:hover, rect:hover{ opacity:0; } -
保存你的更改,如果你的 Meteor 项目尚未启动,请启动它,并在浏览器中导航到你的项目 URL(通常是
http://localhost:3000)。如果你屏幕上没有形状,请点击添加一些,然后悬停在形状上。当你悬停时,你会注意到形状会慢慢淡出,当你将鼠标移开时,形状会慢慢淡入,直到它们恢复到全不透明度。现在我们将使用 jQuery 同时移动屏幕上的所有形状。在navbar.html中,在底部添加以下模板:<template name="navoffset"> <div class="nav navbar-right"> <button id="btnNegative" class="btn btn-danger navbar-btn" data-offset="-">red</button> <button id="btnPositive" class="btn btn-info navbar-btn" data-offset="">blue</button> </div> </template> -
在
templates.html中,在navshapes声明之前添加对新navoffset模板的调用:{{#navbar this}} {{> navbutton}} {{> navoffset}} {{> navshapes}} {{/navbar}}我们现在通过修改
navbar.js来添加button事件的代码,在底部添加以下函数:Template.navoffset.events({ 'click button.btn': function(e,c){ var shapes = ($('rect').length)? $('rect'):$('circle'); if (shapes.length==0) return; var offset = e.currentTarget.dataset.offset; _.each(shapes,function(d,i){ var randVal = Math.ceil(Math.random()*200)+'px'; var randOffset = offset+randVal; var translate = ('translate('+randOffset+','+randOffset+')'); $(d).css('transform',translate); }); } }); -
我们还希望对我们的
nav按钮添加一点修饰,所以将以下 CSS 添加到style.css文件中,在底部:div.nav { margin-left: 20px; } -
保存所有这些更改,现在你的网页应该包含两个新的按钮,分别标记为红色和蓝色。当你交替点击这些按钮时,屏幕上的所有形状将随机移动其位置,在屏幕上左右对角移动。
-
我们现在将使用内置的动画钩子,称为
_uihooks,来动态地将形状添加到 DOM 中时进行动画处理。打开templates.html并对one和two模板进行以下修改:{{#each svgShapes}} {{#Animate}} <circle class="shape animate" ... {{/Animate}} {{/each} ... {{#each svgShapes}} {{#Animate}} <rect class="shape animate" ... {{/Animate}} {{/each}} -
我们有一个最后的步骤,那就是通过我们的
.animateCSS 声明将形状的初始之前状态添加到形状中。打开style.css并在底部添加以下声明:rect.animate, circle.animate { transform: translate(-300px,-100px); opacity:0; } -
保存你的更改,并通过添加新的形状到屏幕(点击任何地方)或通过将形状从圆形更改为正方形,然后再回到圆形(点击one和two按钮)来测试新的动画功能。当你点击时,你会看到形状淡入并从左侧移动到位置。
它是如何工作的…
因为 Blaze 现在根据 DOM 渲染对象,而不是根据基于文本的 HTML 标签渲染,它能够了解创建的元素的样式和条件,例如,因为我们已经在style.css中添加了opacity:0样式到我们的:hover形状元素,鼠标下方的元素会淡出并保持淡出状态,即使我们点击/添加新的形状。使用 jQuery,我们可以以编程方式添加样式和转换,就像我们在navbar.js中使用.css()函数所做的那样。Blaze 也会尊重这些更改,因为对 CSS/DOM 的编程更改会被记住,即使添加了新元素。
_uihooks声明包含三个事件处理器:
insertElement |
在 DOM 节点渲染之前触发,这样我们就可以操作其位置,映射节点动画时的位置,然后渲染节点时对其进行动画处理。 |
|---|---|
removeElement |
在 DOM 节点被移除之前触发。我们检查是否有出行的动画,如果有,则对节点进行动画处理。否则,我们直接移除它。 |
moveElement |
当 DOM 节点在 DOM 中的索引位置发生变化时触发。我们可以在索引变化之前添加任何动画,并在索引变化期间创建动画效果。 |
对于我们的示例,我们只需要关注插入和移除元素。每个元素都通过模板分配了 CSS animate类,这使得元素偏移到预期的位置(-300,-100)。我们的动画方式是通过让元素在屏幕上以偏移位置(animate类生效)出现,然后通过移除animate类来触发过渡/动画,因为元素移动到其预期位置。多亏了_uihooks和insertElement事件处理器,我们可以在元素最初渲染后立即以编程方式移除class声明,使它们进行动画。这使得形状看起来像是从左上角淡入。同样,我们可以在元素被移除之前将它们添加回来,以执行出行的动画。
我们通过将元素包裹在{{#Animate}}…{{/Animate}}块助手中来声明哪些元素会受到影响的,这会导致insertElement和removeElement处理器在包裹的元素上触发。
还有更多…
随着 Meteor 继续改进动画支持,我们预计会有一些变化,尽管主要功能已经存在。同时,您可以通过在客户端源中添加断点来了解_uihooks的工作方式,在packages/appmill:animations-hooks/animation-hooks.js文件中,类似于以下截图:

您也可以在github.com/strack/meteor-animation-hooks上获得(稍微)更详细的解释。
参见
- 本章的使用响应性 HTML 属性配方
第四章。创建模型
在本章中,我们将涵盖以下主题:
-
实现一个简单的集合
-
使用 Session 对象
-
使用 MongoDB 查询进行排序
-
使用 MongoDB 查询进行过滤
-
创建 upsert MongoDB 查询
-
实现部分集合
简介
Meteor 数据模型设计得非常易于开发。那些担心长 SQL 语句、数据库驱动程序和严格结构的数据库表的日子已经过去了。取而代之的是一种简单直接的、基于 JSON 的文档模型,让你能够专注于应用程序的功能。本章包含了与 MongoDB 和 Meteor 中的反应模型上下文交互的最常见菜谱。
实现一个简单的集合
集合是客户端和服务器之间通信的媒介,更改会推送到客户端,请求会推送到服务器。这个菜谱将向你展示如何在客户端和服务器上声明一个集合以供使用。
准备工作
首先,你需要安装 Meteor 并创建一个项目。要创建标准的项目文件结构,请参阅 第一章 中的 设置你的项目文件结构 菜谱,优化你的工作流程。
对于这个特定的练习,你还需要安装 autopublish 和 insecure 包(这些包默认已安装)。如果你需要添加它们,请参阅 第二章 中的 使用包进行自定义 菜谱,使用包进行自定义。
如何做...
要实现一个简单的集合,请按照以下步骤进行:
-
在你的
both文件夹中创建一个新文件,命名为simple.js。 -
在编辑器中打开
simple.js文件,通过输入以下内容来声明一个 MongoDB 注释集合:Comments = new Mongo.Collection('comments'); -
现在,打开客户端
scripts文件夹中的main.js文件([项目根]/client/scripts/main.js),向click button事件处理器添加一个动作,该动作将记录插入到Comments集合中:Template.hello.events({ 'click button': function () { // increment the counter when button is clicked Session.set('counter', Session.get('counter') + 1); Comments.insert({text:'This is comment #' + (Comments.find().count()+1)}); } }); -
我们还需要添加一个注释助手。定位到
Template.hello.helpers方法,并在开头添加以下助手:Template.hello.helpers({ comments: function () { return Comments.find(); }, ... }); -
保存这些更改。
-
打开你的
main.html页面([项目根]/client/main.html),按照以下示例在hello模板中添加一个{{ #each..}}模板片段,并保存你的更改:<template name="hello"> {{#each comments}} <div>{{text}}</div> {{/each}} ... </template> -
现在,打开一个终端窗口,导航到你的项目
[root]文件夹,并启动 Meteor:$ cd [project root] $ meteor -
在网页浏览器中,导航到
http://localhost:3000/并点击屏幕上的按钮几次。结果应该每次点击都添加注释,类似于这样:![如何做...]()
它是如何工作的...
simple.js 中的声明被客户端和服务器读取:
Comments = new Mongo.Collection('comments');
这将实例化模型,并以 Comments 集合的形式体现。
main.js 的更改包括在 click 事件中添加一个额外的动作:
Comments.insert({text:'This is comment #' + (Comments.find().count()+1)});
这将在客户端的 Comments 集合中添加一个评论对象,它将迅速传播到客户端的 minimongo,然后到服务器上的 MongoDB。由于 main.js 中的第二个更改,UI 立即更新:
comments: function () { return Comments.find();
}
comments 辅助函数是一个响应式计算,这意味着每当它包含的响应式上下文(观察属性)之一发生变化时,它都会重新运行。在这个例子中,Comments.find() 是一个响应式上下文,因此每当 Comments 集合发生变化时,这个 comments 辅助函数都会重新运行。
假设 MongoDB 集合(服务器端)批准了更改,UI 将保持更新。如果发生冲突或事务中存在问题,服务器将发送纠正消息,Minimongo 将使用正确的状态更新。但是,在这种情况下,因为我们没有冲突或延迟,更改被卡住,每次点击后都会添加评论。
此模板是响应式的,这意味着当在 Comments 集合中找到更改时,此函数将更新,Blaze 将重新渲染添加到 main.html 的 {{#each…}} 模板块:
{{#each comments}}
<div>{{text}}</div>
{{/each}}
相关内容
- 第三章中的 创建动态列表 和 使用响应式与 HTML 属性 食谱,构建出色的用户界面
使用会话对象
会话对象是一个全局客户端对象,因此它是客户端模型的一部分。尽管它不属于任何集合,但会话对象可以在响应式上下文中使用,这意味着您可以使用它来使响应式方法在它发生变化时重新运行。本食谱将向您展示如何使用会话对象来更新页面上的元素。
准备工作
您需要安装 Meteor 并创建一个项目。要创建标准项目文件结构,请参阅第一章中的 设置您的项目文件结构 食谱,优化您的流程。一个快速默认项目对于本食谱来说也完全可以。
如何操作…
要使用会话对象,请按照以下步骤操作:
-
在您的客户端的
scripts文件夹中的main.js文件([项目根目录]/client/scripts/main.js)中打开,并将greeting辅助函数添加到Template.hello.helpers的开头,如下所示:Template.hello.helpers({ greeting: function() { return Session.get('greeting')||'Welcome to Chapter 4'; }, ... -
打开
main.html并将问候语添加到您的hello模板中,如下所示:<template name="hello"> <h3>{{greeting}}</h3> -
如果 Meteor 尚未运行,请在终端窗口中导航到您的项目根目录并运行
meteor命令:$ cd [project root] $ meteor -
现在,打开浏览器,导航到
http://localhost:3000/,并打开您的网页控制台。 -
在网页控制台中,输入并执行以下命令:
> Session.set('greeting','I just changed the Session Object')您屏幕上的问候语应从 欢迎来到第四章 更改为 我刚刚更改了会话对象,如下面的截图所示:
![如何操作…]()
它是如何工作的…
在greeting模板辅助函数内部添加对Session.get的调用,告诉 Meteor 在Session.keys集合的greeting键中查找一个值。由于集合是 Meteor 中的响应式上下文,对Session集合所做的更改(即使通过 Web 控制台执行)会立即反映在 UI 上。
注意
Session对象字面上是一个会话对象。它只持续到下一次手动页面刷新。如果您需要更持久的,您将需要使用离线存储或 MongoDB 集合。请确保在设计/开发计划中包含这一事实。
还有更多...
截至0.9.1版本,Meteor 还提供了单个响应式变量。它们可以用作Session对象的替代品,具有更简洁的语法,可以是任何类型的对象,并且支持在触发响应式无效化之前检查更改。
要使用响应式变量,将reactive-var包添加到您的 Meteor 项目中:
$ meteor add reactive-var
您可以声明并使用一个变量而不是Sessio n对象:
greeting = ReactiveVar("Welcome to Chapter 4");
greeting: function () {
return greeting.get();
}
此变量(正确声明)可以像处理Session对象一样进行操作,具有更简洁的语法。在 Web 调试控制台中输入以下命令将相应地更改显示:
> greeting.set('I just changed the reactive variable')
上述命令将产生类似于以下截图的结果:

参见
-
在第三章的使用 HTML 属性与响应性结合食谱中,构建出色的用户界面
-
在第六章的创建和消费响应值以及不使用 Mongo 更新 Blaze 模板的食谱中,掌握响应性
使用 MongoDB 查询进行排序
有很多时候您需要排序一个集合。最近的评论、按字母顺序排列的列表和按金额排序的银行交易都是很好的例子。本食谱将向您展示如何使用find()请求中的选项对 MongoDB 集合进行排序。
准备工作
我们将使用本章中找到的实现简单集合食谱作为基准。请为此活动遵循该食谱。
如何做到这一点…
要使用 MongoDB 查询执行排序,请按照以下步骤进行:
-
在您的
main.html文件(位于[project root]/client/main.html)中,对{{#each…}}模板片段进行以下修改:{{#each comments}} <div>{{text}}{{number}}</div> {{/each}} {{greeting}} -
接下来,修改位于
[project root]/client/script/main.js中的click事件处理器内的Comments.insert操作:Template.hello.events({ 'click button' : function () { // template data, if any, is available in 'this' if (typeof console !== 'undefined') console.log("You pressed the button"); Comments.insert({text:'This int #', number:Random.hexString(3)}); } }); -
最后,在
main.js内部,修改find()参数以指定按其number降序排序评论(-1):Template.hello.helpers({ ... comments: function () { return Comments.find({},{sort:{number:-1}}); }, ... }); -
保存所有更改,如果需要的话运行 Meteor。导航到
http://localhost:3000/,点击屏幕上的按钮几次,并观察评论按评论编号排序:![如何做到这一点…]()
工作原理…
这个菜谱的核心在于find()方法参数。我们通过添加一个随机数来修改了数字的存储方式,这样如果我们不做其他任何事情,评论将无序显示。但是,通过在comments辅助函数中添加{sort:{number:-1}},我们向 Blaze 提供了按随机生成的number属性降序排序的结果。要按升序排序,请使用1作为参数。要按降序排序,请使用-1。
其余的更改是为了支持排序更改。首先,main.js被修改,以便number属性可以被随机分配并在将其插入Comments集合时添加到注释对象中:
Comments.insert({text:'This is comment #', number:Random.hexString(3)});
最后的更改是对main.html进行更改,以在 UI 中显示新的number属性:
<div>{{text}}{{number}}</div>
更多内容…
如果需要,你可以使用多个属性执行复杂的排序。要这样做,只需将额外的属性键添加到sort选项中,使用升序(1)或降序(-1)值,类似于以下:
return Collection.find({},{sort:{prop1:-1, prop2:1}});
参见
- 在第三章的使用 Spacebars 插入模板菜谱中,构建优秀的用户界面
使用 MongoDB 查询进行过滤
无论你是在执行搜索、组织记录还是缩小结果范围,迟早你都会想要过滤你收藏的结果。这个菜谱展示了如何使用 MongoDB 的find方法选项来限制集合中的记录数量。
准备工作
我们将使用本章中使用 MongoDB 查询进行排序菜谱中创建的项目。请完成该菜谱,并使用文件作为本菜谱的基线。
如何操作…
要过滤 MongoDB 查询,请按照以下步骤操作:
-
将以下更改应用到
[项目根]/client/scripts/main.js中的comments辅助函数:Template.hello.helpers({ ... comments: function () { return Comments.find({number:/[2468]/},{sort:{number:-1}}); }, ... }); -
保存
main.js并在必要时启动 Meteor。导航到http://localhost:3000/;点击屏幕上的按钮几次,并观察只显示包含偶数的评论。多次点击后的结果应该类似于以下截图:![如何操作…]()
-
将
main.js中的正则表达式更改,只显示包含奇数的评论,通过在查询中进行以下更改:return Comments.find({number:/[13579]/}, {sort:{number:-1}}); -
保存你的更改,并观察你的 UI 的变化,它应该看起来类似于以下截图:
![如何操作…]()
它是如何工作的…
通过在查询中的find()语句中添加一个选择器,集合将缩小以匹配选择器中指定的任何内容。在这种情况下,选择器是一个正则表达式,只有当number属性包含至少一个偶数时,才会返回结果,随后,如果number属性包含至少一个奇数。
注意,这并没有以任何方式改变 {sort:{number:-1}} 断言,并且它工作得完全一样,无论我们如何更改查询。
小贴士
你可以使用一系列选择器来限制/过滤你的集合。要获取完整列表,请查看位于 docs.mongodb.org/manual/reference/operator/query/ 的 MongoDB 查询运算符列表。
还有更多...
将过滤器移到服务器端提供了安全性和性能优势,因为非匹配的结果从一开始就不会发送到客户端。实际上,find() 最常在服务器上使用,利用 Meteor 的发布和订阅功能。关于在服务器上使用 find() 的示例,请参阅本章中找到的 使用 MongoDB 查询进行排序 菜谱。
相关内容
- 本章中的 实现部分集合 和 使用 MongoDB 查询进行排序 菜谱
创建 upsert MongoDB 查询
在开发应用程序时,有时你会遇到需要更新现有记录或如果不存在则插入记录的情况。这通常使用条件运算符,如 if 来完成。
通过 MongoDB,Meteor 通过允许你使用简单语法进行 upsert(更新+插入)记录来减轻检查的负担。这个菜谱将向你展示如何做到这一点。
准备工作
我们将再次使用本章中 使用 MongoDB 查询进行排序 菜谱中创建的项目。请创建这个菜谱的新副本,并使用这些文件作为本菜谱的基线。
我们还将使用官方的 momentjs 包。要添加 momentjs 包,请在终端窗口中导航到你的项目根目录。一旦到达那里,请执行以下命令:
$ meteor add momentjs:moment
这将添加 momentjs 包,现在你可以完成这个菜谱了。
如何做到...
我们首先需要增加重复记录的可能性,并确定每个评论被插入或更新的时间。按照以下步骤创建 upsert MongoDB 查询:
-
打开
[项目根目录]/client/scripts/main.js文件,并按如下方式修改Template.hello.events:'click button': function () { ... Session.set('counter', Session.get('counter') + 1); var newC = { text:'This is comment #', number:Random.hexString(1), time: moment().format('ll, hh:mm:ss') }; Meteor.call('commentUpsert',newC); } -
我们现在将为
upsert函数创建一个服务器方法。默认情况下,Meteor 只允许通过_id在客户端进行修改,由于_id值可能不存在,我们需要为我们的upsert函数创建一个服务器方法。打开[项目根目录]/server/server.js并在文件底部添加以下方法:Meteor.methods({ commentUpsert: function(newC){ Comments.upsert({number:newC.number},{$set:newC}); } }); -
最后,我们将修改
[项目根目录]/client/main.html以在comments集合中显示每个评论的时间戳:{{#each comments}} <div>{{text}}{{number}} at: {{time}}</div> {{/each}} -
保存你的更改,如果 Meteor 还未运行,请启动它,并导航到
http://localhost:3000/。 -
持续点击屏幕上的按钮,观察每次点击时,不是添加新记录,而是如果记录已存在,则更新记录的时间戳。
它是如何工作的...
在 server.js 中,我们在 Meteor.methods 内声明了 commentUpsert 方法。
此方法接收一个新评论对象 (newC) 作为参数,并在 Comments 集合上调用 upsert。第一个参数(选择器)告诉 MongoDB 在 newC 对象中查找与 number 属性匹配的任何条目。第二个参数(修改器)告诉 MongoDB 在 upsert 对象上插入/更新哪些字段。
如果找到匹配项,则更新字段。如果没有找到匹配项,则插入一个新记录,newC 对象提供值。
在 main.html 中,我们只是简单地添加了新的 time 属性到显示中:
<div>{{text}}{{number}} at: {{time}}</div>
在 main.js 中,我们首先删除了 Comments.insert() 语句。然后创建 newC 对象,使用 moment().format() 将时间戳转换为可读格式,填充一个随机数字、一些文本和一个时间戳。最后,我们通过 Meteor.call 语句调用 commentUpsert 服务器端方法。
结果确认我们的 upsert 函数正在正常工作,因为每个新的评论(带有新的 number 属性)都被添加到我们的列表中,每个现有的评论(带有已经存在的 number 属性)的时间戳都得到了更新。
还有更多…
Meteor 通过添加实际的 upsert 函数来简化事情,而不是在传统的 MongoDB update 查询中设置 {upsert:true}。在 Meteor 中,update 或 upsert 都可以使用,具体选择取决于你。
小贴士
关于在 MongoDB 集合中更新和 upsert 记录的详细文档已经存在。你可以在 docs.mongodb.org/manual/reference/method/db.collection.update/ 找到更多详细信息。
参见
- 第十一章 构建自定义服务器方法 菜谱,利用高级功能
实现部分集合
部分集合是从服务器发送下来的集合,其中只包含每个记录上可用的部分信息。这对于隐藏属性或字段,以及减少包含大量信息的记录的大小很有用。换句话说,部分集合可以帮助提高安全和性能。这个菜谱将向你展示如何在服务器上实现部分集合。
准备工作
让我们以本章中找到的 创建 upsert MongoDB 查询 菜谱作为本菜谱的基准。创建这个菜谱的新副本,然后进行其他准备工作。
让我们在做这件事的同时也稍微整理一下。在一个终端窗口中,导航到根项目文件夹,并执行以下命令:
$ meteor add twbs:bootstrap
这将添加 bootstrap CSS 框架。现在我们想利用 bootstrap,所以打开你的 main.html 文件(位于 [项目根]/client/),并对 hello 模板进行以下更改:
<template name="hello">
...
<div class="btn-group-vertical">
{{#each comments}}
<div class="btn btn-default">{{text}}{{number}}
<span class="label label-warning">
{{time}}
</span>
</div>
{{/each}}
</div>
<p><button>Click Me</button></p>
...
</template>
这些更改将使用一些默认的bootstrap组件样式来使我们的显示看起来类似于以下截图:

现在,我们需要移除autopublish默认的 Meteor 包。在终端窗口中,执行以下命令:
$ meteor remove autopublish
这将暂时中断你的应用程序;在这种情况下,Comments集合中的记录不会通过线发送到客户端,因此 UI 中不会显示任何记录。别担心,我们会修复这个问题!
你现在可以继续到下一个步骤。
如何操作…
首先,由于我们移除了autopublish,我们需要准备客户端订阅comments数据流。要实现部分集合,请按照以下步骤进行:
-
打开
[项目根]/client/scripts/main.js,并将以下代码添加到文档的顶部:Meteor.subscribe('comments'); -
当我们打开
main.js时,让我们也改变我们排序的内容,通过修改comments辅助函数并保存你的更改:comments: function () { return Comments.find({},{sort:{time:-1}}); }, -
接下来,我们需要服务器发布
Comments数据集合。我们将同时从响应流中移除文本字段,实现部分集合。打开[项目根]/server/server.js,添加以下代码,并保存你的更改:Meteor.publish ('comments',function(){ return Comments.find({} , {fields:{text:false}}); });我们现在可以立即看到文本字段不再在 UI 中显示,如下面的截图所示:
![如何操作…]()
-
让我们现在稍微清理一下 UI,使其明显显示记录正在重新排序。再次打开
main.html,进行以下更改,并保存文件:<div class="btn btn-default">{{time}} <span class="label label-warning"> {{number}} </span> </div>现在,当你点击按钮来更新
time属性时,时间戳将被更新,并且每次点击(点击几次以真正看到效果)都会改变记录的排序:![如何操作…]()
它是如何工作的…
这个菜谱的核心在server.js中,我们添加了fields字段指定符到 MongoDB 查询(在Meteor.publish(…)语句中):
Meteor.publish ('comments',function(){ return Comments.find({} , {fields:{text:false}});
})
这个字段指定符告诉 MongoDB 查询从结果中排除文本字段/属性。一旦做出这个更改,main.html仍然试图在模板中调用{{text}},但由于它是空的,所以没有显示。然后我们完全移除了显示{{text}}的调用,因为它不是必需的。
还有更多…
如前所述,fields字段指定符可以通过只列出我们不想看到的字段来设置为排除。为此,只需为每个不想看到的字段使用:false(或:0)参数,其余字段将默认包含。例如:
record.find({} , {fields:{f1:false , f2:false}});
字段指定符也可以设置为包含,这意味着只有指定的字段将通过使用:true(或:1)参数被包含。这看起来会类似于以下代码:
record.find({} , {fields:{f3:true , f4:true}});
提示
要了解更多关于fields字段指定符的信息,请查看官方 Meteor 文档,链接为docs.meteor.com/#/full/fieldspecifiers。
参见
- 第九章中的基本安全 - 关闭 autopublish配方,保护您的应用程序,章节链接为 Chapter 9。
第五章。实现 DDP
在本章中,我们将涵盖以下主题:
-
读取 DDP 流
-
使用仅客户端集合
-
实现多服务器 DDP
-
将 DDP 与其他技术集成
简介
对于 Meteor 来说,实时应用程序的概念是基础,客户端和服务器通过数据在线上的原则相互交互。Meteor 团队为此实时客户端-服务器通信开发了一种协议,称为分布式数据协议(DDP)。与 Meteor 中的其他一切一样,DDP 是一个独立的(尽管是核心)库,您可以使用它单独使用。本章中的食谱将向您展示 DDP 是如何工作的,并给您一些关于如何在 Meteor 的默认使用之外使用它的想法。
读取 DDP 流
要真正理解 DDP(以及调试目的)的情况,能够读取 DDP 流非常有用。看到 DDP 的实际操作不仅可以帮助调试过程,还可以帮助您更好地理解这个协议的灵活性和实用性。这个食谱将向您展示如何使用 Arunoda Susiripala 的出色的 DDP 分析器。
准备工作
您需要安装 Meteor、Node 和 npm。有关安装说明,请访问以下网址:
-
Node—
nodejs.org -
npm—
www.npmjs.com/
您还需要一个项目。您可以使用几乎任何您喜欢的项目。因为它提供了合理的、可控的客户-服务器通信量(换句话说,DDP 流),我们将使用todos示例应用程序。请注意,如果您愿意,您也可以使用自己的 Meteor 项目。我们之所以使用这个示例,是因为它方便,并且我们可以控制数据流。
要安装todos示例,请打开一个终端窗口,导航到您选择的目录,并输入以下命令:
$ meteor create --example todos
现在请输入以下命令启动您的 Meteor 应用程序:
$ cd todos
$ meteor
您现在已准备好在todos应用程序上安装和运行 DDP 分析器。
如何做...
-
首先,我们需要安装 DDP 分析器。在终端窗口中,输入并执行以下命令:
$ npm install -g ddp-analyzer注意,您可能需要在使用
-g操作符时使用sudo,以便从任何地方使分析器可用,并确保它具有适当的权限。经过简短的安装过程后,分析器将安装并准备好使用。 -
通过输入以下命令启动分析器代理:
$ ddp-analyzer-proxy您将收到类似以下的消息:
DDP Proxy Started on port: 3030 =============================== Export following env. variables and start your meteor app export DDP_DEFAULT_CONNECTION_URL=http://localhost:3030 meteor -
我们现在需要重新启动我们的 Meteor 实例,使用前面步骤中提到的环境变量。在您的终端窗口中,如果
meteor仍在运行,请停止它,并输入以下命令:$ export DDP_DEFAULT_CONNECTION_URL=http://localhost:3030 $ meteor -
一旦
meteor开始备份,请在浏览器中导航到http://localhost:3000,DDP 分析器将准备就绪。 -
执行多个任务,例如添加新的
todo项,完成/编辑现有项等。 -
查看你在启动 DDP 分析器时所在的终端窗口中的 DDP 流。它看起来会类似于以下截图:
![如何做...]()
你将能够看到所有针对客户端的消息。黄色数字告诉你消息是发送给哪个客户端的。以 IN 开头的消息表示消息是从服务器发送到客户端的。如果消息以 OUT 开头,这表示消息和客户端到服务器的出站请求。消息是按照颜色编码的文本之后的纯文本。
使用 DDP 分析器,你可以真正地深入了解,看到 DDP 究竟是如何工作的。你会很快意识到该协议是多么灵活和简单,并且你将更好地理解你可以在你的 Meteor(或其他)应用程序中使用它做什么。
它是如何工作的...
简单来说,DDP 是两个端点之间的一系列消息。这些消息显示了更新、添加和删除,并通过 WebSocket 传输。这意味着消息可以实时发送,而不是等待传统的客户端请求轮询。这减少了流量(嘈杂),并确保消息尽可能快地送达。
DDP 分析器使用 DDP 协议,监听你的浏览器和 Meteor 应用程序之间的流式对话。当你使用 DDP_DEFAULT_CONNECTION_URL= 命令更改 DDP 执行的端口时,你通过端口 3030 重定向了你的应用程序。DDP 分析器监听这个端口,代理连接到端口 3000,并在终端窗口中记录所有接近它的流量。
更多内容...
完成后,请确保打开一个新的终端窗口或使用以下命令将你的环境变量改回端口 3000:
$ export DDP_DEFAULT_CONNECTION_URL=http://localhost:3000
如果你不这样做,你的应用程序将停止工作,因为 DDP 仍在通过端口 3030 运行。
参见
- 在 第一章 的 使用 web 控制台 食谱中,优化你的工作流程
使用仅客户端集合
要真正理解 DDP 的工作原理,尝试自己实现与数据通过网络通信是有帮助的。这样做(并且是一个非常有用的食谱!)的最佳方式是让客户端继续利用内置的 Mongo 集合,并基于一些简单的逻辑从服务器端程序性地发送更新。这个食谱将指导你创建一个仅客户端的集合,并通过服务器端的调用来操作这个集合。
准备工作
我们将需要一个示例项目作为基准。为此基准,我们将快速创建一个书签应用程序。没有铃声和哨声,只是一个简单的书签标题和 URL 列表。
要做到这一点,我们首先需要默认的模板脚手架。请使用第一章中“设置你的项目文件结构”配方创建一个名为bookmarks的新项目,作为你的起始文件结构。
我们需要添加和移除一些 Meteor 包。首先,移除autopublish包。打开一个终端窗口,导航到你的项目根目录,并执行以下命令:
$ meteor remove autopublish
接下来,让我们添加bootstrap包。在你的终端窗口中执行以下命令:
$ meteor add twbs:bootstrap
现在我们将添加创建我们的bookmarks程序所需的 HTML 和 JavaScript。
将你的[项目根目录]/client/main.html文件的内容替换为以下内容:
<head>
<title>Chapter 05</title>
</head>
<body>
{{> urls}}
</body>
<template name="urls">
<div class="url-container">
{{#each bookmarks}}
<div class="bookmark panel {{selected}}">
<div class="panel-heading">
<button type="button" class="close"><span class="glyphicon glyphicon-remove"></span>
</button>
{{#if editing}}
<h3 class="panel-title"><input type="text" value="{{title}}"></h3>
{{else}}
<h3 class="panel-title"> {{title}}</h3>
{{/if}}
</div>
<div class="panel-body">
{{#if src}}
<a href="{{src}}">{{src}}</a>
{{else}}
<div class="input-group">
<input type="text" class="form-control" placeholder="enter URL here" />
</div>
{{/if}}
</div>
</div>
{{/each}}
<input type="button" class="btn btn-primary" id="btnNewBM" value="add" />
</div>
</template>
接下来,在你的[项目根目录]/client/scripts文件夹中,将以下内容添加到main.js中:
Meteor.subscribe('urls');
然后,在同一个scripts文件夹中创建一个templateHelpers.js文件,并添加以下helpers函数:
Template.urls.helpers({ bookmarks: function () { return URLs.find();
},
title: function () { return this.title || this.src;
},
editing: function () { return Session.equals('selMark', this._id) && Session.equals('editMark', this._id);
},
selected: function () { return Session.equals('selMark', this._id) ? "panel-warning" : "panel-success";
},
src: function () { return this.src || false;
}
});
在同一个templateHelpers.js文件中,添加以下events函数:
Template.urls.events({
'click .bookmark': function (e) {
Session.set('selMark', this._id);
},
'dblclick .bookmark': function (e) {
Session.set('editMark', this._id);
},
'blur .panel-title > input': function (e) {
if (e.currentTarget.value !== this.title) {
Meteor.call('updateTitle', this._id, e.currentTarget.value, function (err, succ) {
console.log(succ);
});
}
Session.set('editMark', null);
},
'keypress .panel-title > input': function (e) {
if (e.keyCode == 13 && e.currentTarget.value !== this.title) {
Meteor.call('updateTitle', this._id, e.currentTarget.value, function (err, succ) {
console.log(succ);
Session.set('editMark', null);
});
}
},
'click #btnNewBM': function (e) {
URLs.insert({
title: 'new bookmark'
});
},
'blur .input-group > input': function (e) {
if (e.currentTarget.value !== this.src) {
Meteor.call('updateSRC', this._id, e.currentTarget.value, function (err, succ) {
console.log(succ);
});
}
},
'keypress .input-group > input': function (e) {
if (e.keyCode == 13 && e.currentTarget.value !== this.src) {
Meteor.call('updateSRC', this._id, e.currentTarget.value, function (err, succ) {
console.log(succ);
});
}
},
'click .close': function (e) {
Meteor.call('removeBM', this._id, function (err, succ) {
});
}
});
现在,我们需要一点样式。打开位于你的[项目根目录]/libs/styles文件夹中的style.css文件,并用以下样式替换现有的样式:
/* CSS declarations go here */
body {
font-size:1.5rem;
}
.url-container{
background-color: rgb(255, 255, 255);
border-color: rgb(221, 221, 221);
border-width: 1px;
border-radius: 4px 4px 0 0;
-webkit-box-shadow: none;
box-shadow: none;
position: relative;
padding: 45px 15px 15px;
margin: 20px;
margin-left:auto;
margin-right:auto;
max-width: 90%;
border-style:solid;
}
.input-group {
width:90%;
}
我们需要声明我们的 URL 集合,因此在你的[项目根目录]/both文件夹中创建一个collections.js文件,并添加以下Mongo.Collection声明和String.prototype函数:
URLs = new Mongo.Collection("urls");
if (typeof String.prototype.startsWith != 'function') {
String.prototype.startsWith = function (str){
return this.slice(0, str.length) == str;
};
}
最后,我们添加一些服务器逻辑。打开[项目根目录]/server/server.js,并用以下代码替换任何现有的代码:
Meteor.publish('urls',function(){
return URLs.find();
});
Meteor.methods({
updateTitle: function(id,title){
var bmark = URLs.findOne(id);
if (!bmark) return;
if (title&& title!==bmark.title){
URLs.update(id,{$set:{title:title}});
return "updated";
}
return "same title";
},
updateSRC: function(id,src){
var bmark = URLs.findOne(id);
if (!bmark) return;
if (src&& src!==bmark.src){
//Adding the http if it doesn't already have it...
src = src.startsWith('http')? src: 'http://'+src;
URLs.update(id,{$set:{src:src}});
return "updated";
}
return "same src";
},
removeBM: function(id){
URLs.remove(id);
return "removed";
}
});
保存所有更改,启动你的meteor实例,并在浏览器中导航到你的项目(通常是http://localhost:3000)。如果一切顺利,你现在将有一个工作的bookmarks应用,你可以添加 URLs,并通过双击添加/更改书签的标题。以下截图是浏览器完成时的一个示例:

如何做到这一点...
我们将在“书签”页面的底部添加一个计数器。这个计数器会告诉我们有多少个安全和非安全链接(通过链接是否以https开头来决定)。
-
首先,我们将添加一个客户端只读的集合
bmcounts并订阅它。如果我们在这个clients文件夹的任何地方声明这个集合,服务器将看不到它。因此,它将是一个客户端只读的集合。在你的客户端scripts文件夹中的main.js文件([项目根目录]/client/scripts/main.js)中打开,并在现有的Meteor.subscribe('urls')命令周围添加以下两行,如下所示:BMCounts = new Mongo.Collection("bmcounts"); Meteor.subscribe('urls'); Meteor.subscribe('bmcounts', new Mongo.ObjectID()); -
当我们在
scripts文件夹中时,让我们打开templateHelpers.js文件,并添加我们需要来显示计数的反应式Template.helpers函数。将以下函数添加到文件的底部:Template.linkcount.helpers({ BMCount: function(){ return BMCounts.findOne(); } }); -
现在,让我们添加 HTML 模板。打开位于你的
client文件夹中的main.html,并在<body>标签周围添加以下代码:<body> {{> urls}} {{> linkcou nt}} </body> <template name="linkcount"> {{#with BMCount}} <div id="counts"> <button class="btn btn-lg btn-info"> <span class="glyphicon glyphicon-link"></span> {{unsecureCount}} / <span class="glyphicon glyphicon-lock"></span> {{secureCount}} </button> </div> {{/with}} </template> -
我们还会用一些 CSS 让计数器看起来更美观。打开你的
style.css文件,它位于你的lib/styles目录中,并添加以下 CSS 声明:#counts{ position:relative; float:right; margin-right:50px; } -
现在我们需要在服务器端做一些实际的工作。我们需要在添加、删除或更改书签时计数和排序。打开你的
server.js文件,它位于你的server文件夹中,并添加以下Meteor.publish函数:Meteor.publish('bmcounts', function (id) { var self = this; var count = 0; var secCount = 0; var initializing = true; var handle = URLs.find().observeChanges({ //TODO: Added //TODO: Removed //TODO: Changed }); initializing = false; self.added("bmcounts", id, { unsecureCount: count, secureCount: secCount }); self.ready(); self.onStop(function () { handle.stop(); }); }); -
在前面的代码块中,我们有三个
TODO注释。我们首先需要创建added监听器。直接在//TODO: Added注释下方,添加以下代码://TODO: Added added: function (idx, doc) { if (doc.src && doc.src.toLowerCase().startsWith('https')) { secCount++; if (!initializing) self.changed("bmcounts", id, { secureCount: secCount }); } else { count++; if (!initializing) self.changed("bmcounts", id, { unsecureCount: count }); } }, -
接下来是
removed监听器。直接在//TODO: Removed注释下方,添加以下代码://TODO: Removed removed: function (idx, doc) { //really inefficient... var bms = URLs.find().fetch(); secCount = _.filter(bms,function(bm){ return bm.src && bm.src.toLowerCase().startsWith('https'); }).length; count = bms.length - secCount; self.changed("bmcounts", id, { unsecureCount: count, secureCount: secCount }); }, -
最后,是
changed监听器。直接在//TODO: Changed注释下方添加以下代码://TODO: Changed changed: function (idx, doc) { if (doc.src && doc.src.toLowerCase().startsWith('https')) { secCount++; count--; self.changed("bmcounts", id, { unsecureCount: count, secureCount: secCount }); } } -
保存所有更改,如果你的
meteor实例尚未启动,请启动它,并在浏览器中导航到你的项目(通常是http://localhost:3000)。你应该会在屏幕的右下角看到一个包含安全和非安全链接计数的小信息按钮。添加一些新链接,删除一些链接,并观察浏览器中的计数即时变化,类似于以下截图:![如何做…]()
它是如何工作的…
我们刚刚添加的所有客户端代码与常规 Mongo 集合的代码完全相同。对于客户端来说,一切都是正常的。
然而,在服务器上,我们需要手动根据对URLs集合所做的更改更新 DDP 流中的.added和.changed消息。
为了实现这一点,我们首先使用Meteor.publish()函数参数中传递的函数:
Meteor.publish('bmcounts', function (id) {
...
});
每当客户端订阅 DDP 流时,这个函数就会重新运行。在函数内部,我们使用Mongo.Cursor.observeChanges()函数创建一个handle:
var handle = URLs.find().observeChanges({
...
});
在observeChanges函数内部,如果添加了一个文档(除了初始化时),我们会检查src属性是否以https开头,以确定是否可以增加secCount(安全计数)变量;否则,我们只会增加正常的count变量。
如果一个文档被删除,我们不得不做一些小技巧,但我们会重新计算secCount和count变量。
如果文档的src变量被更改,我们将执行与added中相同的检查,并将增量从count移动到secCount。
随着URLs集合实时变化,这三种情况将会发生。因此,在每个更改之后,我们将调用self.changed()函数,该函数通过 DDP 向客户端发送消息,模拟如果我们正常修改bmcounts集合时将会发送的自动更改消息。
你会注意到bmcounts集合在服务器上从未被使用。相反,我们通过"bmcounts"订阅通道向客户端发送消息,如下例所示:
self.changed("bmcounts", id, {
...
});
这是一个重要的区别。客户端并没有订阅实际的 MongoDB 集合。相反,它订阅了一个 DDP 消息通道。在这种情况下,我们让客户端为我们做繁重的工作,通过向 bmcount 客户端专用集合发送 "bmcounts" 订阅消息。本质上,服务器通过电线发送数据变化,而客户端由于不知情,将这些消息视为实际的数据变化。
该方法的其余部分是在客户端取消订阅时的清理工作,使用 handle.stop() 来完成初始化。它包括调用 self.added(),这与 self.changed() 调用做同样的事情(模拟数据变化并将其发送到已订阅的客户端)。初始化的最后一步是调用 self.ready(),这会让已订阅的客户端知道所有初始数据变化都已发送。
参见
-
在 第四章 的 实现部分集合 食谱中,创建模型
-
在 第九章 的 基本安全 - 关闭 autopublish 食谱中,保护您的应用程序
实现多服务器 DDP
乐趣不仅仅局限于单个客户端和服务器 DDP 连接。哦,不!实际上,Meteor 已经邀请了所有人参加 DDP 舞会,在这个食谱中,我们将看到两个 Meteor 服务器如何使用 DDP 互相通信。
准备工作
我们将使用本章中找到的 使用客户端专用集合 的代码库。请创建此项目的实例并启动你的 meteor 服务器。
对于这个项目,我们将根据链接是否安全来改变书签标题的颜色;因此,我们需要对我们的第一个项目进行一个修改,使颜色依赖于 secType 属性(我们将在本食谱中创建)。打开位于 [项目根]/client/scripts 文件夹中的 templateHelpers.js 文件。定位到 Template.urls.helpers 部分,并对 selected 函数进行以下修改:
Template.urls.helpers({
…
selected: function () {
return Session.equals('selMark', this._id) ? "panel-warning" : this.secType;
},
…
由于我们将实现另一个 Meteor 服务器实例,我们还需要创建一个新的 Meteor 项目。在终端窗口中,导航到根项目文件夹之外的位置,并在终端窗口中输入以下命令:
$ meteor create typecheck
$ cd typecheck
$ meteor remove autopublish
$ meteor --port 3030
这将正确初始化第二个服务器实例,并在端口 3030 上启动它。
现在我们已经准备好让我们的第二个 Meteor 项目与第一个项目进行通信了!
如何做到这一点...
我们将使用一个新的书签属性 secType 来改变书签标题的颜色。我们的新(第二个)项目将在书签通过 DDP 通道到来时进行检查,并在需要时添加 secType 属性。让我们开始吧。
-
首先,我们将想要利用我们从使用仅客户端集合食谱中熟悉的
String.startsWith()函数。将以下原型修改器添加到位于您第二个项目根文件夹中的typecheck.js文件底部:if (typeof String.prototype.startsWith != 'function') { String.prototype.startsWith = function (str){return this.slice(0, str.length) == str; }; } -
现在,我们想要创建一个仅服务器文件,以便我们可以专注于手头的任务。在项目根目录下创建一个
server文件夹,然后创建并打开一个名为DDPChecker.js的文件进行编辑。 -
在
DDPChecker.js文件中,添加以下代码以从我们的第一个项目连接和订阅urls集合:conn = DDP.connect("http://localhost:3000"); URLs = new Mongo.Collection("urls",conn); conn.subscribe("urls"); -
现在,我们创建一个函数来检查记录,看
secType变量是否已设置。如果没有设置,并且有一个要检查的src属性,它将使用适当的secType变量更新记录。将以下函数添加到DDPChecker.js文件底部:function checkSecType(idx,doc){ if (!doc.src || doc.secType) return; if (doc.src.toLowerCase().startsWith('https')) doc.secType = 'panel-success'; else doc.secType = 'panel-danger'; URLs.update(idx,doc); } -
最后一个拼图是添加对刚刚创建的
checkSecType函数的调用,每次记录被添加或更改时。我们通过调用observeChanges函数和URLs.find()游标来完成此操作。将以下代码添加到DDPChecker.js文件底部:URLs.find().observeChanges({ added:checkSecType, changed:checkSecType }); -
打开浏览器并导航到您的项目(通常是
http://localhost:3000)。每个书签的颜色应该对应于书签链接的网络安全类型,类似于以下截图:![如何做…]()
它是如何工作的…
在我们的第二个项目中,我们声明了URLs集合并订阅了变化,就像在正常的client应用中做的那样。这里的区别在于我们正在使用DDP.Connect()函数建立与现有 Meteor 服务器的连接:
conn = DDP.connect("http://localhost:3000");
URLs = new Mongo.Collection("urls",conn);
当我们声明URLs集合时,我们将此连接的处理程序作为名为conn的参数传递。这告诉 Meteor 我们对该集合(更准确地说,是 DDP 消息)感兴趣,这些消息来自我们的第一个服务器。
为了避免冗余代码,我们创建了checkSecType函数,该函数监听对URLs集合所做的更改,并将secType属性设置为与src属性的网络安全类型相对应。然后我们将checkSecType函数用作通过observeChanges通过 DDP 线传入的added和changed事件的处理器。
还有更多…
使用 DDP 作为客户端并不局限于 Meteor。您可以使用 DDP 与几乎所有技术一起使用。您将在本章后面的将 DDP 与其他技术集成食谱中看到一个示例。
相关内容
-
在第四章的实现部分集合食谱中,创建模型
-
本章中的将 DDP 与其他技术集成食谱
将 DDP 与其他技术集成
DDP 永远不会停止!DDP 库适用于大多数主要编程语言和平台,你可以非常快速地让你的客户端、服务器,甚至是硬件设备与你的 Meteor 服务器进行通信,而不需要太多努力。这个配方将向你展示如何从 Node.js 程序使用 DDP 连接到 Meteor 服务器。
准备工作
我们将想要使用本章中找到的 使用仅客户端集合 配方的代码库。请创建该项目的实例并启动你的 meteor 服务器。
你需要安装 Node 和 npm 来完成这个配方。它们默认与 Meteor 一起安装。但是,如果你需要有关如何安装 Node.js 和 npm 的说明,可以在以下网站上找到:
-
Node—
nodejs.org -
NPM—
www.npmjs.com/
我们还需要确保 ddp npm 模块已安装。打开终端窗口并执行以下命令:
$ npm -g install ddp
小贴士
当全局安装 npm 模块时,你可能需要使用 sudo 命令,类似于以下命令:
$ sudo npm –g install ...
如何操作...
我们将创建一个 Node 服务,该服务将检查 URLs 集合中记录的 src 属性以确保它们是安全的(HTTPS)。如果它们不安全,Node 服务将调用我们的 Meteor updateSRC 方法并将 src 属性更改为安全。要与其他技术集成 DDP,请执行以下步骤:
-
我们首先创建项目文件夹。在终端窗口中,创建一个
nodecheck文件夹,然后使用cd命令进入该目录,并通过执行以下命令添加ddpnpm模块:$ mkdir nodecheck $ cd nodecheck $ npm install ddp -
我们现在想创建我们的服务。在
nodecheck目录中创建一个名为main.js的文件。打开它进行编辑,并声明 DDP 连接,添加以下代码:var DDPClient = require("ddp"); var ddpclient = new DDPClient({ host: "localhost", port: 3000, path: "websocket", ssl: false, autoReconnect: true, autoReconnectTimer: 500, maintainCollections: true, ddpVersion: '1' }); -
我们现在想使用
.connect()函数连接到我们的 Meteor 服务器,并在控制台窗口中显示成功订阅的结果。将以下代码添加到main.js:ddpclient.connect(function (error) { if (error) { console.log(error); } else console.log('successful connection'); ddpclient.subscribe('urls', [], function () { var urls = Object.keys(ddpclient.collections.urls); console.log(urls); }) }); -
最后,我们将使用
.on()函数监听通过 DDP 传输的消息。我们将检查所有added和changed消息,并在需要时将 URL 替换为安全的 URL,使用.call()函数调用远程 Meteor 服务器updateSRC方法。将以下代码添加到main.js:ddpclient.on('message', function (msg) { var message = ddpclient.EJSON.parse(msg); switch (message.msg) { case "added": case "changed": var url = message.fields.src; if (url) { if (!url.startsWith('https')) { message.fields.src = url.replace('http:', 'https:'); ddpclient.call('updateSRC', [message.id, message.fields.src], function (err, success) { if (!err) console.log(success); }) } } break; default: break; } }); if (typeof String.prototype.startsWith != 'function') { String.prototype.startsWith = function (str) { return this.slice(0, str.length) == str; }; }好的,我们现在可以运行我们的 Node 服务了。保存所有更改,并确保你在浏览器中打开了你原始 Meteor 项目的实例(通常是
http://localhost:3000),这样你就可以看到更改。它们会很快发生,所以不要眨眼! -
在终端窗口中,导航到你的
nodecheck文件夹,并输入以下命令:$ node main.js在你的网页浏览器中,之前使用
http的每个链接现在都将使用https。如果你添加另一个链接并故意尝试将其作为http输入,正在运行的节点服务将立即将其更改为https。如果你查看运行node main.js命令的终端窗口,你将看到每个转换后的链接旁边都有单词 updated,你的网页浏览器看起来可能如下截图所示:![如何做…]()
它是如何工作的…
Meteor 服务器正在使用 DDP 监听客户端订阅请求。由于我们使用的是为 Node 构建的 DDP client 库(使用 ddp npm 模块),因此我们可以使用新的 DDPClient() 函数声明我们的连接,连接并订阅 urls 通道,并使用 .on() 函数监控通过 DDP 传递的所有消息:
var ddpclient = new DDPClient({
host: "localhost",
port: 3000,
...
});
ddpclient.connect(function (error) {
...
ddpclient.subscribe('urls', ... );
...
});
ddpclient.on('message', function (msg) {
...
});
最后,如果我们想要进行修改,可以通过调用 Meteor 服务器的远程 methods 来实现,使用 .call() 函数:
ddpclient.call('updateSRC', ... );
你会注意到,我们不需要在 Meteor 服务器端做任何事情就能使它工作。这就是 DDP 的美妙之处;对于任何兼容的 DDP 库,协议都是相同的。只要两个系统都使用 DDP 协议,系统的语言、硬件和操作系统都是无关紧要的,并且不会影响服务器之间的通信。
如你所见,DDP 协议非常强大,因为它简单且清晰。DDP 还有许多其他方面,你将需要查阅 Meteor 和相应的 GitHub 文档。
还有更多…
你可以通过访问 Meteoropedia 在 www.meteorpedia.com/read/DDP_Clients 查看可用的 DDP 客户端列表。
参见
- 本章中的 使用仅客户端集合 和 实现多服务器 DDP 菜单
第六章 掌握响应式编程
在本章中,您将学习以下主题:
-
创建和消费响应式值
-
使用 Ajax 查询结果修改 UI 元素响应式
-
使自定义库响应式
-
不使用 Mongo 更新 Blaze 模板
-
使用内联数据响应式地修改 UI 元素
-
集成 jQuery UI
简介
响应式编程是一种新兴的开发方法,其中数据的变化会自动触发整个系统的变化。这允许您,作为开发者,以声明式的方式编写代码,并让响应式元素管理任何变化。Meteor 可能是今天最好的、最全面实现的响应式编程。通过理解响应式编程的核心概念,您可以使用Tracker(以前称为Deps)库创建简单、声明式的代码,同时避免与响应式和异步 JavaScript 编程相关的常见陷阱。本章中的食谱将为您提供简单、清晰的示例,说明 Meteor 响应模型的主要组件是如何工作的。
创建和消费响应式值
Tracker,简单来说,是 Meteor 的变量跟踪系统。它用于管理响应式值、数据结构和计算(消耗响应式值的函数)。本食谱将向您展示如何创建响应式值,并使用Tracker.autorun()对这些值进行计算。换句话说,它将教会您在 Meteor 内部如何进行响应式编程。这个食谱将作为更复杂功能的基础非常有用。
准备工作
为了简化,我们将使用默认的 Meteor 项目,并添加reactive-var包。打开一个终端窗口,导航到您想要创建根项目的地方,并执行以下命令:
$ meteor create reactiverecipes
$ cd reactiverecipes
$ meteor add reactive-var
$ meteor
您现在可以开始使用响应式变量了。
如何操作...
我们将根据响应式变量修改按钮的文本;因此,我们需要创建按钮并连接响应式上下文。
-
在您最喜欢的文本编辑器中打开您的项目,并编辑
reactiverecipes.html文件,为<button>元素添加一个 ID,如下例所示:<button id='btnReact'>Click Me</button> -
现在,打开
reactiverecipes.js,并在if (Meteor.isClient)条件下方添加以下代码行:if (Meteor.isClient) { btnText = new ReactiveVar('Click Me'); Tracker.autorun(function () { $('#btnReact').text(btnText.get()); }); -
最后,在
Template.hello.events声明中添加以下行,位于'click button'函数中的Session.set()函数下方:Session.set("counter"...); btnText.set('Again!'); -
保存您的更改,并在浏览器中导航到
http://localhost:3000。一旦到达那里,点击标有点击我的按钮,并观察文本变为再次!:![如何操作...]()
您可以通过在浏览器中打开控制台窗口并使用
btnText.set()命令来手动更改按钮文本,如下例所示:> btnText.set('Pretty please...')按钮文本将立即变为您设置的任何值。
它是如何工作的...
前面的例子似乎过于简化,但关于这一点我们有两点要说。
首先,它之所以简单,是因为 Meteor 是为了使您的代码非常简单和易于理解而构建的。这使得您的开发和调试工作大大减少了时间消耗。
注意声明响应式变量的过程是如何由一行代码组成的。当我们添加 btnText = new ReactiveVar('Click Me') 语句时,我们只是声明了一个变量(并将其初始化为 'Click Me'),但我们通过声明知道它是一个响应式变量。
接下来,我们在 Tracker.autorun() 块内部封装了一个极其简单的 jQuery 语句。这个块被称为响应式计算。响应式计算最初运行一次,然后每当任何包含在其中的响应式变量发生变化时,它都会重新运行(重新计算)。因此,在这个例子中,我们告诉 Tracker 监控 btnText(一个响应式变量)的值,并在它发生变化时自动再次运行(因此得名 autorun)代码块。
注意我们不必担心任何时间条件,例如“这是第一次运行吗?”或者“好吧,当有变化时……”。我们只需简单地声明 jQuery 语句,让 Tracker 为我们确定时间。
这就是透明响应式编程这个术语的含义。有一套代码用于初始化,另一套代码用于处理变化。除了变量声明外,您的整个代码库都可以用普通的、老式的 JavaScript 编写。
其次,它在底层所做的事情绝对不简单!为了给您,程序员,提供这样的前端简单性,Meteor 实现了响应式提供者和响应式计算。
我们将牺牲一点精确度来使概念更容易理解。当调用 Tracker.autorun 时,它会做四件事:
-
它创建一个计算
-
它将
Tracker.currentComputation设置为那个计算 -
它调用传递给它的函数
-
它将
Tracker.currentComputation设置为null
计算本质上是一个事件处理函数,并包含传递给 Tracker.autorun 的函数的引用。计算所等待的事件是调用 computation.invalidate 方法。当调用 invalidate 方法时,计算会重新运行它包含的函数。
现在我们来看函数。传递给 Tracker.autorun 的函数如果包含响应式提供者,则被视为响应式函数。响应式提供者是一个具有获取和设置某些值的功能的对象,并跟踪依赖关系。当调用 get 函数时,它会做三件事:
-
它检查
Tracker.currentComputation是否有值。 -
如果它有,计算就会被添加到内部列表中。
-
它返回 getter 请求的变量的值。
这两个步骤都是通过调用depend()方法来执行的,这是一个在依赖对象上找到的方法。reactive-var库自动处理这部分,所以你不需要直接调用depend()方法。你所要做的就是使用反应性变量!
同样,当调用set函数时,它会做两件事:
-
它改变了内部变量的值
-
对于内部列表中的每个计算,都会调用
invalidate()方法
通过调用changed()方法,循环调用invalidate()方法,这是一个在依赖对象上找到的helper方法。再次强调,reactive-var为你处理了这部分。欢迎你!
当每个计算被无效化时,它会重新运行包含它的函数。整个循环从新重新运行的函数调用 getter 开始,getter 返回它们的值(比如我们的btnReact按钮的text值)并将计算添加到提供者的内部列表中,再次等待 setter 被调用。
尽管它们被极度简化(任何阅读此内容的 MDG 核心成员可能正在喷咖啡……),以下是ReactiveVar和autorun对象所做的工作:
function ReactiveVar(value) {
// create the dependency object (a helper class)
var _dependency = new Tracker.Dependency;
//set the default internal value
var _internalVal = value;
var getValue = function () {
// call depend(), which adds the computation
_dependency.depend();
// return the internal value
return _internalVal;
};
var setValue = function (newValue) {
// update the internal value
_internalVal = newValue;
// loop through computations and call invalidate()
_dependency.changed();
};
return this;
}
function autorun(func) {
// creates computation and assigns it to
// Tracker.currentComputation
var computation = new Tracker.Computation(func, Tracker.currentComputation);
// Calls the onInvalidate method the first time,
// so that func function will run
Tracker.onInvalidate({...});
return computation;
}
为了清晰起见,我们省略了两件事,但为了有一个完整的理解,这两件事很重要。首先,depend()辅助方法还设置了一个onInvalidate()监听器,它会从反应性提供者的内部列表中移除计算。其次,在将计算添加到内部列表之前,会检查计算是否已经存在于内部列表中。
当你问为什么在计算被无效化时移除计算,简短的答案是,这使得整个计算add-execute-remove循环非常优雅。它保持所有计算都是最新的,并且依赖函数只运行一次,无论在同一个函数内部调用 getter 的次数有多少。如果它们没有被移除,函数会被多次运行,这可不是什么好事。
因此,让我们回顾一下在这个配方中我们做了什么:
-
autorun方法创建一个计算并将你的函数传递给这个计算 -
计算有一个
onInvalidate方法,它除了其他事情外,还会运行你的函数 -
在
autorun创建了一个计算之后,它会使用onInvalidate方法运行你的函数一次 -
你的函数中包含反应性变量,它们有自己的待办列表
-
当你的函数运行时,会调用 getter,它们会将计算添加到待办列表中
-
也会调用 setter,执行待办列表并清除它们
-
因为待办事项列表中的函数有反应性变量,所以这个过程会重复(函数会被重新运行)
-
重复上述步骤
再次强调,这个解释被极大地简化了,因此,相当不准确;然而,从概念上讲,它可能会帮助你更好地理解底层发生了什么。
还有更多……
Meteor 还提供了 ReactiveDict 对象,它运行方式与 ReactiveVar 完全相同,但可以以键值对的形式存储一系列的响应式变量。语法与 ReactiveVar 完全相同,但您需要在 set 和 get 方法中添加一个键,如下面的代码所示:
btnText.get(); // ReactiveVar
txtFields.get('btnText'); // ReactiveDict
...
btnText.set('Click Me'); // ReactiveVar
txtFields.set('btnText','Click Me') // ReactiveDict
要使用 ReactiveDict,只需使用以下终端命令添加 reactive-dict 包:
$ meteor add reactive-dict
最后,您不必使用 ReactiveVar 或 ReactiveDict,而是可以自己实现响应式提供者。请参阅本章中找到的 制作自定义库响应式 菜谱作为示例。
参见
-
第三章 创建动态列表 菜谱,构建出色的用户界面
-
本章中的 制作自定义库响应式 菜谱
使用 Ajax 查询结果填充 ReactiveVar
无论何时我们使用 Ajax,请求(甚至接收)数据都相当简单。当我们需要用新的或更新的数据更新 UI 时,问题就出现了。借助 Meteor 的响应式编程能力,这个问题就不再是问题。在这个菜谱中,您将看到如何使用 Meteor 的 ReactiveVar 库来更新您的 UI。
准备工作
为了快速启动,我们将使用默认的 Meteor 项目,并添加了一些包。打开一个终端窗口,导航到您想要创建根项目的地方,并执行以下命令:
$ meteor create ajaxreactive
$ cd ajaxreactive
$ meteor add reactive-var
$ meteor add http
$ meteor add twbs:bootstrap
$ meteor
我们现在准备好构建一个响应式的 Ajax 查询了!
如何做到这一点…
我们将从 openweathermap.org 拉取天气数据,使用他们免费的(但仅用于测试)API。我们将从我们的 openweathermap.org 查询结果中提取数据,并将它们放入 ReactiveVar 库中,以便它们可以被我们的 Blaze 模板响应式地消费。
让我们先从修改 UI 开始:
-
打开
ajaxreactive.html并在我们的即将创建的weather模板调用下添加对现有hello模板的调用,如下面的代码所示:{{> hello}} {{> weather}} -
我们还希望在我们的
hello模板中重新使用计数器,以告诉我们起始经度将是什么。按照以下方式更改hello模板中的<p>元素的描述:<template name="hello"> <button>Click Me</button> <p>You are starting at {{counter}} longitude.</p> </template> -
接下来,添加我们的
weather模板,它只是一个简单的表格,添加了一些美观性,多亏了 bootstrap!在ajaxreactive.html的底部添加以下模板:<template name="weather"> {{#if reports}} <table class="table"> <thead> <th>name</th> <th>weather</th> <th>temp</th> <th>humidity</th> </thead> <tbody> {{#each reports}} <tr class={{severity}}> <td>{{name}}</td> <td>{{description}}</td> <td>{{temp}}</td> <td>{{humidity}}</td> </tr> {{/each}} </tbody> </table> {{/if}} </template> -
保存您的更改并导航到
http://localhost:3000。虽然除了计数器的描述外,没有什么会改变,但我们很快就会想查看我们的天气数据。 -
让我们打开
ajaxreactive.js并声明我们的ReactiveVar库。在Session.setDefault()下方添加以下代码:Session.setDefault("counter", 0); weatherlist = new ReactiveVar; -
我们现在将修改
'click button'函数以增加计数器并执行我们的 Ajax 调用。在Template.hello.events部分找到该函数,并按以下方式修改代码:// increment the counter when button is clicked if (Session.get("counter") <= 60) Session.set("counter", Session.get("counter") + 4); else Session.set("counter", 0) getWeather();注意
不要忘记将计数器增加的值改为
4;否则,您点击时weather数据不会改变太多。 -
接下来,我们需要添加
Template.weather.helpers,以便我们的 UI 能够正确填充。在Template.hello.events部分之后,添加以下内容:Template.weather.helpers({ reports: function () { if (!weatherlist) return false; return weatherlist.get(); }, severity: function () { if (this.weather && this.weather[0].main == "Clear") return "success"; else return "warning"; }, description: function () { if (this.weather && this.weather[0]) return this.weather[0].description; else return ""; }, temp: function () { if (this.main) return this.main.temp; else return ""; }, humidity: function () { if (this.main) return this.main.humidity; else return ""; } }); -
最后,我们需要添加我们的 Ajax 调用和异步回调函数,一旦结果返回。在
Template.weather.helpers部分之后,添加以下两个函数:function getWeather() { var long1 = +Session.get("counter"), long2 = long1+5; HTTP.get("http://api.openweathermap.org/data/2.5/ box/city?bbox=12,"+long1+",15,"+long2+",10&cluster=yes", harvestWeather); } function harvestWeather(error, data) { if (data && data.content) { var weather = EJSON.parse(data.content); weatherlist.set(weather.list); } } -
保存所有更改,并在浏览器中的项目页面上点击按钮。你应该会看到以下截图类似的内容:
![如何做…]()
随着你的点击,给定区域的天气结果将显示出来,向北移动。
它是如何工作的…
在这种情况下,我们没有使用自动反应的 Mongo 集合。相反,我们调用了openweather.org api,并使用set方法更新了ReactiveVar库(在这种情况下,weatherList变量)。因为模板辅助函数依赖于对该相同ReactiveVar的get方法的调用,所以当ReactiveVar更新时,它们会自动重新运行。让我们来分解一下。
我们首先在 HTML 中创建了weather模板:
<template name="weather">
{{#if reports}}
...
{{/if}}
</template>
模板使用{{#each...}}块遍历reports辅助对象,用结果填充 HTML 表格。
接下来,在我们的客户端 JavaScript 中,我们使用新的ReactiveVar库声明了我们的反应式变量weatherlist:
weatherlist = new ReactiveVar;
然后,我们在reports辅助对象中使用了weatherlist.get(),该对象位于Template.weather.helpers:
reports: function () {
...
return weatherlist.get();
}
通过在这里使用它,我们设置了一个依赖关系,以便每次调用weatherlist.set()时,模板的数据都会刷新,并且模板会更新/重新运行。
最后,我们使用HTTP.get()将我们的按钮连接到 Ajax 调用,并将harvestWeather函数作为回调(HTTP.get(url,arg,callback)):
function getWeather() {
var long1 = ...
HTTP.get("...", harvestWeather);
}
一旦回调被触发,它会整理 Ajax 调用中的数据,并使用weatherlist.set()重新填充我们的反应式变量:
function harvestWeather(error, data) {
...
weatherlist.set(weather.list);
}
如前所述,当这个set函数被调用时,它会使模板函数无效,并反应式地更新我们的 UI。
你可以在回调函数(harvestWeather)和天气模板辅助函数(reports)中非常清楚地看到,这些调用是常规的、纯 JavaScript。我们实际上只是在调用一个get或set函数。因为我们调用这些函数的对象是一个ReactiveVar,所以所有反应式依赖和 UI 更新都由我们处理。
你可以快速看到 Meteor 的反应式编程模型是多么强大。我们不是在处理事件和处理器,也不是担心回调地狱,我们使用了简单的、干净的set命令,让 Meteor 为我们处理所有细节。
参见
-
在第三章的创建动态列表食谱中,构建出色的用户界面
-
本章的创建和消费反应值食谱
创建一个自定义库的反应性
因为我们经常处理变量和数据,所以有时会忽略 Meteor 的响应性不仅适用于响应式值。任何 JavaScript 库中的任何函数都可以被转换成响应式提供者。这个配方将向你展示如何使用 Tracker.depend() 和 Tracker.changed() 命令创建自己的响应式提供者。
准备工作
为了使示例简单,我们将使用默认的 Meteor 项目,包含一个 bootstrap 包和一个随机颜色生成器。打开一个终端窗口,导航到您想要创建根项目的地方,并执行以下命令:
$ meteor create customreactive
$ cd customreactive
$ meteor add twbs:bootstrap
$ meteor add rzymek:randomcolor
$ meteor
如何操作…
让我们假设你有一个(相当酷)的库叫做 colorsaurus。你的 colorsaurus 对象喜欢咆哮。很多。主要是因为在恐龙语中,“rawr”意味着“我爱你”,而且 colorsaurus 希望尽可能多地与所有朋友分享随机颜色。每次你从这个杂色生物那里请求一个颜色时,它会立即给你一个随机颜色。这显然是有史以来最有用的库,所以让我们开始构建它!
-
打开
customreactive.js并在Template声明下方添加以下colorsaurus对象,位于Meteor.isClient条件语句内:colorsaurus = { color: function(){ return randomColor(); }, rainbowRoar: function (){ console.log('rawr'); } }; -
当我们在
customreactive.js中时,让我们添加numcolor辅助函数。定位到Template.hello.helpers方法,并在方法顶部添加以下内容:Template.hello.helpers({ numcolor: function(){ return colorsaurus.color(); }, counter: ... }); -
保存您的更改,导航到
http://localhost:3000,并在控制台窗口中输入以下命令:> colorsaurus.color() > colorsaurus.rainbowRoar() -
你应该会得到一个随机颜色和来自我们的朋友
colorsaurus函数的一个简洁的 我爱你:![如何操作…]()
-
它还不是响应式的,但我们需要先准备我们的用户界面,以便释放响应式
colorsaurus函数的所有强大功能。打开customreactive.html并对hello模板进行以下修改:<button id='btnColor'>Click Me</button> <p style="color:{{numcolor}}">You've pressed the button {{counter}} times.</p> -
现在,我们需要更改
click事件以使colorsaurus发出咆哮声。在customreactive.js中修改Template.hello.events部分,如下所示:Template.hello.events({ 'click button': function () { // increment the counter when button is clicked Session.set("counter", Session.get("counter") + 1); colorsaurus.rainbowRoar(); } }); -
剩下的工作就是让
colorsaurus具有响应性并设置一个autorun函数。再次打开customreactive.js并添加以下响应式语句,包括添加Tracker.Depenency对象:var colorDep = new Tracker.Dependency; colorsaurus = { color: function(){ colorDep.depend(); return randomColor(); }, rainbowRoar: function (){ console.log('rawr'); colorDep.changed(); } }; -
现在,在
colorsaurus代码块之后立即添加一个Tracker.autorun函数,如下面的代码所示:Tracker.autorun(function(){ $('#btnColor').css('background-color' , colorsaurus.color()); $('body').css('background-color', colorsaurus.color()); }); -
保存您的更改,转到您的浏览器,并点击按钮,直到您的小小心灵渴望为止。您将获得一些非常棒的颜色组合,如下面的截图所示:
![如何操作…]()
它是如何工作的…
注意按钮、文本和页面背景是如何变化的,并且它们都变成了随机、不同的颜色。这是因为我们使用了一个响应式库函数,每次调用时都会返回一个随机颜色,而不是返回一个固定的变量。
通过让我们的 color() 函数返回 randomColor() 的结果,我们确保每次调用 colorsaurus.color() 时,都会得到不同的结果。
我们在返回函数中添加了 colorDep.depend(),这会记录由 Tracker.autorun 或响应式模板(见本章中 创建和消费响应式值 的配方,以获取完整解释)创建的计算。
最后,我们调用了 colorDep.changed(),这会运行记录的计算。
代码的每一部分都是独立的,因此不依赖于其他代码部分或库。通过 Tracker.Dependency 对象,Meteor 会为我们跟踪一切,这样我们就可以随意添加或删除响应式依赖项。例如,尝试在您的浏览器控制台中运行以下行:
> Tracker.autorun(function(){console.log(colorsaurus.color());});
现在,每次您点击页面上的按钮时,您都会从 colorsaurus 获得另一个随机颜色,并将其打印到您的控制台:

这就是最好的响应式编程。Rawr!
参见
-
第三章 中 使用响应性与 HTML 属性 的配方,构建出色的用户界面
-
本章中 创建和消费响应式值 的配方
无 Mongo 更新 Blaze 模板
我们的用户界面中的并非所有内容都必须依赖于 Mongo 集合。实际上,我们可以在模板中使用几乎任何响应式对象,并且更改将立即显示。这个配方将快速向您展示如何使用自定义集合来填充和更新 UI 模板。
准备工作
让我们从基础 Meteor 项目开始,并添加 bootstrap 和 reactive-var 包。在终端窗口中,导航到您希望项目驻留的位置,并输入以下命令:
$ meteor create mongoless
$ cd mongoless
$ meteor add twbs:bootstrap
$ meteor add reactive-var
$ meteor
最后,打开浏览器并导航到 http://localhost:3000,以便您可以看到实时更新。
如何做到这一点...
我们将创建一个简单的按钮点击快照数组,每次页面上的按钮被点击时,都会向数组中添加一个新元素。随后,这些按钮将使用 {{#each}} 模板块添加到 UI 中。
-
首先,打开
mongoless.html并将以下块添加到hello模板中,紧随<p>元素之后,并在关闭</template>标签之前,如下例所示:{{#each buttonPresses}} <div class="btn btn-info pressed">#{{btnRank}}</div> {{/each}} </template> -
现在,我们需要添加一个响应式变量并将一些辅助函数附加到
Template.hello.helpers对象上。打开mongoless.js并添加以下高亮代码:if (Meteor.isClient) { presses = new ReactiveVar; // counter starts at 0 Session.setDefault("counter", 0); Template.hello.helpers({ counter: function () { return Session.get("counter"); }, buttonPresses: function(){ return presses.get(); }, btnRank: function(){ return this.rank; } }); -
剩下的只是每次按钮被按下时更新
presses变量。在Template.hello.events中,在'click button'事件处理程序中,在Session.set()调用之后立即添加以下代码行:Session.set("counter", Session.get("counter") + 1); var _presses = presses.get() || []; _presses.push({rank:Session.get("counter")}); presses.set(_presses); -
保存所有更改,打开你的浏览器,并开始点击标记为 点击我 的按钮。你应该会在每次点击按钮时看到一个新按钮被创建,类似于以下内容:
![如何操作…]()
它是如何工作的…
当我们将 buttonPresses 辅助函数添加到 Template.hello.helpers 对象时,我们只是用存储在 ReactiveVar 元素内的简单数组替换了我们通常会用 MongoDB 集合来做的操作:
presses = new ReactiveVar;
集合是响应式提供者,这意味着它们会根据需要跟踪和重新运行计算。presses 对象也是一个响应式提供者,因此它执行完全相同的事情。当值更新时,它会重新运行任何存储的计算/响应式函数。在这种情况下,当数组被修改并且调用 presses.set() 时,它会重新运行计算。
参见
-
第三章中的 Creating dynamic lists 菜谱,Building Great User Interfaces
-
本章中的 Creating and consuming a reactive value 菜谱
使用内联数据动态修改 UI 元素
通常,当 HTML 页面中的元素被渲染时,这种渲染并不直接与创建它们的任何数据相关联,例如,如果我们有一个对象数组,我们可能会通过遍历数组并为数组中的每个对象添加 <div> 元素来生成一些 HTML。除非我们手动将它们与对象数组相关联,否则这些新创建的元素在没有任何方式与创建它们的数据相关联。这导致开发者尝试强行加入关联数据,这些数据用于事件和其他下游函数。简而言之,仅使用现有的网络技术,很难确保所有数据与 HTML DOM 元素完全同步。Meteor 被设计用来优雅地解决这个问题,跟踪每个 DOM 元素的上下文,因此允许即时访问最初创建元素时使用的数据。这个菜谱将指导你如何检索和使用与单个 DOM 元素关联的数据。
准备工作
我们将使用本章中找到的 Updating Blaze templates without Mongo 菜谱中的代码库。请完成该菜谱,然后在项目根目录中运行以下终端命令来添加 randomcolor 包:
> meteor add rzymek:randomcolor
请确保你的浏览器也打开到 http://localhost:3000,这样我们就可以实时看到更改。
如何操作...
我们将向现有的按钮创建功能添加一些功能。首先,我们将为每个新按钮添加一个随机颜色;其次,当按钮被点击时,我们将移除该颜色;第三,我们将根据关联的内联数据恢复颜色。
那么,让我们开始吧。我们需要更新hello模板,为新按钮设置初始背景颜色。我们还需要一种随机移除这些颜色的方法。我们将通过添加一个新的控制按钮来实现这一点。
-
打开你的
.html文件(可能是mongoless.html),修改hello模板以使其看起来如下:<template name="hello"> <button id="addBtn">Click Me</button> <button id="chgColor">Or Me!</button> <p>You've pressed the button {{counter}} times.</p> {{#each buttonPresses}} <div class="btn btn-info pressed" style="background-color:{{btnColor}}"> #{{btnRank}}</div> {{/each}} </template> -
现在,我们需要将
btnColor辅助函数添加到Template.hello.helpers中,并修改存储的数据对象以为新颜色腾出空间。我们还需要细化按钮上的click事件,以区分添加新按钮和移除按钮颜色。打开你的.js文件(可能是mongoless.js),对变量声明和辅助函数进行以下更改:if (Meteor.isClient) { presses = new ReactiveVar; counter = new ReactiveVar(0); Template.hello.helpers({ counter: function () { return counter.get(); }, buttonPresses: function () { return presses.get(); }, btnRank: function () { return this.rank; }, btnColor: function () { return this.color; } }); ... -
现在,我们需要修改现有的
click事件,添加一个新的事件来移除颜色,并添加一个最终的处理程序来设置所有新按钮在点击时恢复颜色。对Template.hello.events部分进行以下更改:Template.hello.events({ 'click #addBtn': function () { // increment the counter when button is clicked counter.set(counter.get() + 1); var _presses = presses.get() || []; var newBtn = { color: randomColor(), rank: counter.get() }; _presses.push(newBtn); presses.set(_presses); }, 'click #chgColor': function () { var rndBtn = ~~(Math.random() * counter.get()); $('.pressed')[rndBtn].style.backgroundColor = ''; }, 'click .pressed': function (e, n) { e.currentTarget.style.backgroundColor = this.color; } }); -
保存所有更改并点击屏幕上的点击我按钮 5-10 次。你会注意到所有新添加的按钮都被分配了随机颜色,如下面的截图所示:
![如何操作...]()
-
现在,多次点击或我按钮。随机地,按钮会失去随机颜色并变为由
bootstrap提供的默认btn-info蓝色,如下面的截图所示:![如何操作...]()
任何失去颜色的按钮都可以通过直接点击按钮来恢复颜色。
它是如何工作的…
这个菜谱的核心在于'click .pressed'事件处理程序内部。在那里,我们将点击按钮的backgroundColor属性分配给this.color:
'click .pressed': function (e, n) {
e.currentTarget.style.backgroundColor = this.color;
}
在这种情况下,有一个对该 DOM 元素相关联的数据对象的引用。Meteor 跟踪每个元素是如何创建的。因此,当模板内部发生事件时,Meteor 将数据对象作为上下文(即this)在事件处理程序中提供。这样,Meteor 可以提供对每个渲染元素的内置数据的即时访问。
注意,即使我们手动使用'click #chgColor'事件更改了backgroundColor,Meteor 仍然保留用于渲染元素的 data 对象的引用。这变得很重要,因为我们现在不再需要将数据作为 DOM 元素的属性存储——不再需要data-color或data-whatever属性来杂乱无章地填充 UI 并可能暴露数据。虽然数据对象对 UI 是隐藏的,但它们可以立即和直接地访问。因此,为了访问必要的相关数据,不需要进行任何复杂的计算或 DOM 操作。
还有更多…
上述示例使用默认的 Meteor Template 事件处理器,因此当然数据是可用的。但是,即使您绕过 Meteor 的 Template 事件处理器并使用,例如,jQuery 事件,通过调用元素的 Blaze.getData() 函数,相关数据也将可用。
在此情况下设置事件处理器有点棘手。我们必须首先将每个按钮的填充移动到新的模板中,因为 jQuery click 事件处理器必须在 rendered() 函数回调内部运行。为此,请对您的 .html 文件(可能是 mongoless.html)进行以下更改:
<template name="hello">
<button id="addBtn">Click Me</button>
<button id="chgColor">Or Me!</button>
<p>You've pressed the button {{counter}} times.</p>
{{#each buttonPresses}}
{{> helloBtn}}
{{/each}}
</template>
<template name="helloBtn">
<div class="btn btn-info pressed"
style="background-color:{{btnColor}}">
#{{btnRank}}</div>
</template>
您还需要从 Template.hello.events 中移除 'click .pressed' 事件处理器。一旦移除事件处理器,请在 Template.btnRank.rendered 代码块内添加 jQuery.click() 事件处理器,以便在新按钮渲染时立即运行,如下面的代码所示:
Template.helloBtn.rendered = function () {
this.$('.pressed').click(function (e) {
e.currentTarget.style.backgroundColor = Blaze.getData(this).color;
});
};
最后,由于我们将 div 渲染移动到了新的 helloBtn 模板,我们需要将 btnRank 和 btnColor 辅助函数从 Template.hello.helpers 移动到新创建的 Template.helloBtn.helpers 块,如下面的代码所示:
Template.helloBtn.helpers({
btnRank: function () {
return this.rank;
},
btnColor: function () {
return this.color;
}
});
这是一项大量工作以获得相同的结果,但它有助于说明 Blaze/Meteor 关联数据功能的灵活性。我们只需稍微修改事件辅助函数,将其指向 Blaze.getData(this).color 而不是 this.color。尽管每个事件处理器中都有相同的关键字,但 jQuery 事件处理器内的 this 指的是 DOM 元素,而不是原始 Meteor 事件处理器内的关联数据对象。Blaze.getData(element) 以 DOM 元素作为参数,并检索该元素的关联内联数据。
在任何情况下,获取相关数据都非常简单,这允许您以编程方式对 UI 进行任何操作,而无需担心破坏/更改与每个渲染元素关联的数据。
参见
- 在 第三章 的 使用 Spacebars 插入模板 和 创建动态列表 菜谱中,请参阅 第三章。
集成 jQuery UI
jQuery 库非常受欢迎,这是有充分理由的。当正确使用时,它可以加快开发过程,并为我们提供可靠的方式来执行那些否则需要大量编码的工作。
jQuery 的补充是 jQuery UI,它是一组小部件、主题和动画效果。使用 jQuery UI,您可以快速创建拖放组件、可排序列表以及许多其他有用的 UI 美化功能。
本食谱将指导您在 Meteor 模板中创建一个 jQuery UI-sortable 小部件。
准备工作
对于这个配方,我们肯定希望客户端和服务器文件夹保持代码的整洁和可读性。为了实现这一点,我们将依赖我们的默认模板脚手架。请使用 第一章 中 设置你的项目文件结构 配方创建一个名为 swatches 的新项目,作为你的起始文件结构。
一旦完成脚手架搭建,我们需要将 randomcolor 包添加到我们的项目中。在终端窗口中,导航到你的项目根目录,并运行以下命令:
> meteor add rzymek:randomcolor
我们还希望有一个漂亮的主题,所以让我们使用 Google 的 Material Design 主题的修改版,以增添乐趣。在相同的终端窗口中输入以下命令:
> meteor add html5cat:bootstrap-material-design
接下来,我们想要直接从 jqueryui.com 获取 jQuery UI 的定制版本。在浏览器中导航到 jqueryui.com/,并点击右侧的 Custom Download:

在下载构建器中,进行以下选择:
-
选择最新稳定版本
-
在 Components 下取消选择 Toggle All 复选框
-
在 UI Core 下选择 Toggle All 复选框
-
在 Interactions 下选择 Sortable 复选框
-
在底部,从 Theme 下拉菜单中选择 No Theme
你的选择将类似于以下截图:

我们只需要可排序交互和 UI 核心。其他所有内容都会增加我们的文件大小,所以我们将省略它们。点击 Download,下载完成后解压文件,找到 jquery-ui.min.js 文件,并将其复制到 [project root]/client/lib/scripts 文件夹中。
我们可以简单地使用社区包引入 jQuery UI,但它体积较大,并且不能帮助我们了解如何在 Meteor 中使用未打包的第三方库。因此,我们将采用这种手动安装方式。
打开浏览器到 http://localhost:3000,以便我们可以实时查看更改。我们现在已准备好将 jQuery UI-sortable 小部件添加到我们的项目中。
如何操作...
我们将创建色卡,它们将显示它们所代表的十六进制代码,并且它们将是可排序的,这意味着我们可以通过拖放来移动它们。
-
要实现这一点,我们首先需要创建一个
Swatches集合,该集合在客户端和服务器上均可访问。在你的[project root]/both/文件夹中,创建或编辑一个名为model.js的文件,并添加以下集合声明:Swatches = new Mongo.Collection('swatches'); -
接下来,让我们使用名为
swatches的模板创建我们的用户界面。打开或创建[project root]/client/main.html文件,删除所有内容,并添加以下代码:<head> <title>Swatches</title> </head> <body> {{> colors}} </body> <template name="colors"> <h1>Yay Colors!</h1> <div id="cList"> {{#each swatches}} <div class="swatch" style="background-color:{{color}}"> {{color}}</div>{{color}}</div> {{/each}} </div> <input id="btnNew" type="button" class="btn btn-primary" value="New Color" /> </template> -
我们希望对其进行一点格式化,以便使色卡尺寸保持一致。打开或创建一个名为
[project root]/client/lib/styles/style.css的文件,并添加以下 CSS 声明:.swatch { min-height: 100px; min-width: 100px; display:inline-block; color:white; text-align: center; } #cList { width: 520px; } -
最后,我们将创建添加色卡并能够将它们拖动到屏幕上的逻辑。打开/创建
[项目根]/client/scripts/main.js文件,删除文件内的所有内容,并添加以下Template.helpers和Rankings声明:Template.colors.helpers({ swatches: function(){ return Swatches.find({},{ sort: { rank:1}}); } }) Rankings = { beforeFirst: function(first) { return first - 1;}, middle: function(before,after){ return (before+after)/2;}, afterLast: function(last){ return last + 1; } }; -
现在,有趣的部分来了!我们将创建
jQuery.sortable对象及其stop()函数,并使用常规 jQuery 将我们的button.click事件处理器连接起来。为了正确添加sortable和click事件处理器,我们需要在Template.rendered()函数内部声明它们。在main.js文件中,在Rankings声明下方,输入以下代码:Template.colors.rendered = function(){ this.$('#cList').sortable({ stop: function (e,ui){ var el = ui.item.get(0); var before = ui.item.prev().get(0); var after = ui.item.next().get(0); var newRank = null; if (!before){ newRank = Rankings.beforeFirst(Blaze.getData(after).rank); } else if (!after) { newRank = Rankings.afterLast(Blaze.getData(before).rank); } else { newRank = Rankings.middle(Blaze.getData(before).rank,Blaze.getData(after).rank); } Swatches.update(Blaze.getData(el)._id, {$set: {rank:newRank}}); } }); this.$('#btnNew').click(function(e){ var newColor = randomColor({luminosity:'random', hue:'random'}); Swatches.insert({color:newColor, rank: Swatches.find().count()+1}); }); };
保存所有更改,然后转到你的浏览器。页面应该有一个标有 NEW COLOR 的漂亮、时尚的蓝色按钮。每次你点击这个按钮,都会添加一个新的色卡,颜色是随机的。如果你将任何色卡从一个位置拖放到另一个位置,色卡将会适当地重新排序。这种重新排序不是临时的。如果你刷新页面或打开另一个浏览器窗口,你通过拖放所做的重新排序将会保持。
例如,假设你将色卡从最后一个元素移动到第一个。这个变化将会保持,并且任何打开到同一页面的其他客户端/浏览器将立即反映这个变化。
当以下紫色色卡被拖动并放置时,变化将如以下截图右侧所示:

如以下截图所示,它会在每个 UI 中立即更新:

它是如何工作的...
多亏了我们的 click 事件处理器,每当使用 Swatches.insert() 添加色卡时,都会为该色卡分配一个排名:
this.$('#btnNew').click(function(e){
...
Swatches.insert({..., rank: Swatches.find().count()+1});
});
如果我们查看 Template.colors.helpers 中的 swatches 辅助函数,我们可以看到 MongoDB 的 Collection.find() 查询是按 rank 排序的:
swatches: function(){
return Swatches.find({},{ sort: { rank:1 }});
}
这保留了 UI 中色卡的顺序,并允许我们通过拖放来操作它们的顺序。
在我们的 sortable.stop() 函数内部,我们正在确定色卡被拖动并放置到列表中的位置。一旦我们确定了色卡的位置,我们就使用 Rankings 辅助函数计算该色卡的新排名。然后我们立即使用新排名更新 swatches 集合,这将传播并使 UI 的位置变化永久化。
更多内容...
关键要点在于,虽然可以使用 jQuery(或任何其他第三方库)来执行直接的 DOM 操作,但这些操作不会超出单个用户的会话,甚至不会超出服务器下一次 DDP 变更。
为了使操作永久化并充分利用 Blaze 渲染引擎的出色响应式编程模型,我们需要修改数据源(在这种情况下,是 swatches 集合)。修改将立即处理,而且不需要我们做任何努力,通过响应式来实现。
因此,为了回顾,第三方库可以直接在 Meteor 应用程序中操作 DOM 元素,第三方代码将在Template.rendered()函数块内部执行。
要使更改“生效”,我们只需更新相应的 Mongo 集合即可。使用这种技术,我们可以几乎集成所有现有的 JavaScript 库(如果还没有人为我们完成atmospherejs.com/上的工作)。
参见
-
第一章中的设置你的项目文件结构配方,优化你的工作流程
-
本章中的使用内联数据动态修改 UI 元素配方
第七章. 使用客户端方法
在本章中,你将学习以下主题:
-
使用 SVG 和 Ajax 创建动态图表
-
使用 HTML FileReader 上传图片
-
使用 Canvas 元素创建彩色书
简介
自从 HTML 诞生以来,客户端的 Web 编程已经取得了长足的进步。直到最近,为了做一些简单的事情,比如构建图表、读取文件和在屏幕上绘制,还需要编写/使用第三方polyfill库。但那个我们所有人都知道且不喜欢的小男孩已经长大了!随着强大的、易于使用的脚本和图形 API 以及对象现在几乎在所有浏览器中都得到了支持,Web 编程终于可以独立使用了。本章中的食谱将介绍一些最受欢迎的 Web API 对象,并展示如何在 Meteor 中实现它们。
使用 SVG 和 Ajax 创建动态图表
虽然现在有很多图表库(所有这些你都可以在 Meteor 中使用,顺便提一下!)但了解如何在 HTML 页面内部实现基本形状,使用SVG 模板,是很有好处的。有时实现一个完整的图表库可能有些过度,而掌握 SVG 的相关知识可以非常有用。然而,这是一个 Meteor 食谱;我们不仅想看到一个 SVG 的例子,还想看到它在实际中的应用!这个食谱将教你如何将实时数据源流式传输到 MongoDB 集合中,然后使用 SVG 标签图形化地表示这个动态集合。
准备工作
我们将使用默认项目安装,包含client、server和both文件夹,以保持代码的整洁和可读性。在终端窗口中,导航到你希望项目所在的位置,并执行以下命令:
$ meteor create svggraph
$ cd svggraph
$ rm svggraph.*
$ mkdir client
$ mkdir server
$ mkdir both
我们还想要使用一些包,并移除autopublish包,所以现在就来做这件事。在终端窗口中运行以下命令:
$ meteor remove autopublish
$ meteor add http
$ meteor add meteorhacks:aggregate
$ meteor add rzymek:randomcolor
$ meteor
我们现在可以开始创建我们的 SVG 流数据图表了。
如何做到这一点...
我们的项目将读取来自www.meetup.com/的评论流,该流是免费提供的,并且非常容易使用。我们将记录流中的评论,并显示按州的总评论数。我们需要创建一个界面,该界面将显示州的总数作为垂直条形图,并显示最后一条评论以及它来自的 Meetup Group 的图片。
注意
注意
我们仅为了演示目的使用数据流收集代码!它没有内置冗余检查或加固。我们强烈建议不要将此代码直接用于任何类似生产的应用程序。
按以下步骤使用 SVG 和 Ajax 创建动态图表:
-
我们首先将创建一个具有
id为stateBars的 SVG 元素。然后,我们将为每个单独的状态创建一个<rect>元素,并使用stateStat模板调整高度。在[project root]/client/文件夹中,创建一个client.html文件,添加以下 HTML 代码,并保存文件:<body> {{> cPic}} </body> <template name="cPic"> <svg id="stateBars" width="800" height="600"> {{#each stateStats}} {{> stateStat}} {{/each}} </svg> <div id="cComment">{{curComment}}</div> <img id="cPic" src="img/{{curPic}}"/> </template> <template name="stateStat"> <rect width="{{width}}" height="{{stackHeight}}" y="{{stackPosition}}" style="fill:{{color}};fill-opacity:0.8;" /> <text x="5" y="{{textYPos}}" fill="black">{{state}}: {{total}}</text> </template> -
我们希望在页面上提供一些基本的样式,所以让我们先把这件事处理掉。在
[project root]/client文件夹中,创建一个style.css文件,并添加以下样式声明:body { font-family: 'Helvetica-Neue', sans-serif; font-size: 12px; } #stateBars { border:dashed 3px #ccc; } #cComment { margin-top: 10px; } #cPic { width: 200px; margin-top: 10px; } -
接下来,让我们设置
Comments和CountryTotals集合。为了在逻辑上保持事情更清晰,在客户端我们将使用一些服务器端逻辑来聚合传入的评论。我们首先需要声明我们的集合,以便在客户端和服务器端使用。在[project root]/both/文件夹中,创建一个名为model.js的新文件,并将以下两个声明添加到该文件中:Comments = new Mongo.Collection('comments'); CountryTotals = new Mongo.Collection('countryTotals'); -
接下来,让我们设置对
commentsStream数据的监控,并添加代码来统计总数。由于我们移除了autopublish包,我们需要声明我们的Comments发布,同时,我们希望在每次有新的评论到来时更新某个状态的总数。我们可以通过使用Meteor.publish()函数和cursor.observeChanges()方法来实现这一点。在你的[project root]/server/文件夹中,创建一个名为svggraph-server.js的新文件,并添加以下方法:Meteor.publish("commentsStream", function(country){ var cursor = Comments.find({country:country}); var initializing = true; cursor.observeChanges({ added:function(id,doc){ if (initializing) return; var cTots = Meteor.call('totalsByCountry', doc.country); var sTots = Meteor.call('totalsByState',doc.country); var existingTots = CountryTotals.findOne({country:doc.country}); if (!sTots || !cTots) return; sTots = _.map(sTots,function(s,i,d){ s._id.total = s.total; if (existingTots){ var existingState = _.findWhere(existingTots.states,{state:s._id.state}); if (existingState) s._id.color = existingState.color || randomColor({luminosity: 'light', hue: 'blue'}); else s._id.color = randomColor({luminosity: 'light', hue: 'blue'}); } return s._id; }); var cObj = {country:doc.country, total:cTots[0].total, states: sTots}; CountryTotals.upsert({country:cObj.country},cObj); } }); initializing = false; return cursor; }); -
我们还需要通过国家声明我们的状态总数
Meteor.publish()方法,因此让我们也这样做。将以下内容追加到svggraph-server.js文件的末尾:Meteor.publish("graphData", function(country){ return CountryTotals.find({country:country}); }); -
我们需要跟踪我们正在监控的国家,以及 Meetup Group 的最后一条评论和图片。最直接的方法是通过
Session变量来实现,因此让我们在[project root]/client/文件夹中创建一个名为svggraph-client.js的文件,并在顶部添加以下三个变量:Session.setDefault("country", "us"); Session.setDefault("msgComment","No comments yet"); Session.setDefault("msgPic", "https://d14jjfgstdxsoz.cloudfront.net/meteor-logo.png"); -
现在我们已经指定了我们将要监控的国家,我们可以添加我们的
Meteor.subscribe语句,这两个语句都接受国家作为参数。在svggraph-client.js文件中,在Tracker.autorun块内添加以下语句:Tracker.autorun(function(){ Meteor.subscribe("graphData", Session.get('country')); Meteor.subscribe("commentsStream", Session.get('country')); }); -
我们将在稍后连接数据,但仍然需要添加基于前面步骤中声明的
Session变量的Template.helpers方法来显示最新的图片和评论。在svggraph-client.js文件中,在Tracker.autorun块下方添加以下代码:Template.cPic.helpers({ curPic: function () { return Session.get('msgPic'); }, curComment: function() { return Session.get('msgComment'); } }); -
针对客户端即将到来的数据,我们需要一些
helper方法,用于发送数据和在我们的 Mongo DB 集合上执行聚合查询。我们还想有一个快速重置我们集合的方法,因为收集到的数据量对于示例应用来说可能会变得杂乱无章。因此,我们将创建以下服务器端方法:-
addMsg:这个方法用于将消息插入到我们的Comments集合中。 -
totalsByState:这个用于按状态汇总Comments的总数。 -
totalsByCountry:这个用于按国家汇总总数。 -
resetDB:这个用于重置Comments和CountryTotals集合。
打开
svggraph-server.js并添加以下代码:Meteor.methods({ addMsg : function (msg) { var upMsg = {}; try { upMsg.country = msg.group.country; upMsg.state = msg.group.state; upMsg.category = msg.group.category.name; upMsg.thumb = (msg.group.group_photo ? msg.group.group_photo.thumb_link: ""); upMsg.createdAt = Date.now(); } catch(e){ console.log(e.message); return null; } Comments.insert(upMsg); }, totalsByState: function (country){ return Comments.aggregate([ {$match:{country:country}}, {$group:{_id:{state:"$state"},total:{$sum:1}}}, {$sort:{"total":-1}} ]); }, totalsByCountry: function(country){ return Comments.aggregate([ {$match:{country:country}}, {$group:{_id:{},total:{$sum:1}}} ]); }, resetDB: function(){ Comments.remove({}); CountryTotals.remove({}); console.log('Collections have been reset'); } }); -
-
在服务器端辅助逻辑全部就绪后,是时候读取和解析数据流了。在
svggraph-client.js文件中,添加以下connect、disconnect、onopen、onclose和onmessageWebSocket 函数:function MeetupsStream() { var ms = {}; var ws; var sURL = "ws://stream.meetup.com/2/event_comments"; ms.connect = function (url) { sURL = url || sURL; ws = new WebSocket(sURL); ws.onopen = ms.onopen; ws.onmessage = ms.onmessage; ws.onclose = ms.onclose; return ms; }; ms.disconnect = function () { ws && ws.close(); }; ms.onopen = function () { console.log("Meetup stream started..."); }; ms.onmessage = function (e) { var rec_msg = EJSON.parse(e.data); if (rec_msg.group.group_photo) Session.set('msgPic', rec_msg.group.group_photo.photo_link); Session.set('msgComment', rec_msg.comment); Meteor.call('addMsg', rec_msg); // }; ms.onclose = function () { console.log("Meetup stream closed."); }; return ms; } -
数据流已经准备就绪,接下来只需添加我们的显示逻辑,并切换开关。在
svggraph-client.js文件中,定位到Template.cPic.helpers代码块,并在curComment事件下方添加以下辅助函数(别忘了逗号!):curComment: function() { return Session.get('msgComment'); }, stateStats: function () { var ct = CountryTotals.findOne({country:Session.get('country')}); if (!ct) return []; var stateTotals = ct.states; var ctotal = ct.total; var SVGWidth = 800; var SVGHeight = 600; return _.map(stateTotals, function(s,i,l){ var retObj = {}; retObj.state = s.state; retObj.index = i; retObj.total = s.total; retObj.width = ~~(SVGWidth * (s.total/ctotal)); retObj.stackHeight = ~~(SVGHeight/l.length); retObj.stackPosition = i*retObj.stackHeight; retObj.color = s.color; return retObj; }); } }); -
我们需要一些样式辅助器来正确定位我们的文本数据,并让我们知道最新评论添加的位置(以使事物更加精美!)。将以下
Template.helpers代码块追加到svggraph-client.js文件的底部:Template.stateStat.helpers({ textYPos: function () { return this.stackPosition + ~~(this.stackHeight/2); }, textXPos : function(){ return this.width - ~~(this.width*.2); }, color : function(){ if (Session.equals('lastState',this.state)) return '#2ecc71'; return this.color; } }); -
使用
Template.rendered方法块,我们将数据流打开,并在每次发布新评论时更新我们的lastState会话变量,使用简单的autorun方法。在svggraph-client.js文件的底部输入以下代码:Template.cPic.rendered = function(){ MStream = new MeetupsStream(); MStream.connect(); this.autorun(function(){ var last = Comments.findOne( {country:Session.get('country')}, {sort:{createdAt:-1}}); if (last) Session.set('lastState',last.state); }); } -
保存所有更改,并打开浏览器(如果您还没有的话)到
http://localhost:3000。稍等片刻,您应该会看到新的评论正在到来,状态总数正在更新,如下面的截图所示:![如何操作...]()
-
让它运行一段时间,以便出现更多州。如果您运行足够长的时间,最终会得到所有 50 个州(也许甚至包括波多黎各!)。如果您想重置您的集合,打开浏览器控制台并输入以下命令:
> Meteor.call('resetDB')
它是如何工作的...
如同食谱介绍中提到的,图形化动态数据有两个关键点。第一个与渲染 SVG 对象有关,主要可以在 HTML 模板代码中找到,位于 [项目根]/clie nt/client.html 文件中。我们首先使用了 {{> cPic}} 模板包含来引用我们的 cPic 模板。模板本身声明了我们的 <svg> 元素,然后对 stateStats 集合运行标准的 {{#each }} 控制结构:
<template name="cPic">
<svg ...>
{{#each stateStats}}
{{> stateStat}}
{{/each}}
</svg>
...
</template>
我们直接设置了 <svg> 元素的宽度和高度(注意,它们不是 CSS 样式属性,而是实际元素属性),并添加了一个唯一的 id 属性用于 CSS 样式,如下面的示例所示:
<svg id="stateBars" width="800" height="600">
在 stateStat 模板中,我们创建一个 <rect> 元素并动态设置几乎所有属性:
-
width:这个基于评论数量设置。 -
height:这个基于总状态数设置。 -
y:这是垂直位置,基于数据集中的位置设置。 -
fill:这是颜色值,如果它是最后更新的状态,则设置为绿色。
我们同样设置了文本 SVG 元素的位置,以确保它与相应的<rect>元素对齐:
<template name="stateStat">
<rect width="{{width}}" height="{{stackHeight}}" y="{{stackPosition}}"
style="fill:{{color}};fill-opacity:0.8;" />
<text x="5" y="{{textYPos}}" fill="black">{{state}}: {{total}}</text>
</template>
重要的是要理解 SVG 元素不是相对于同一<svg>块内的其他元素定位的。这就是为什么我们必须设置每个元素的位置。此外,一些属性(例如width和height)可以直接设置。SVG 元素实际上是真正的 DOM 元素和图形元素的混合体。
小贴士
对于 SVG 的精彩介绍,你可以阅读 MDN SVG 教程,位于developer.mozilla.org/en-US/docs/Web/SVG/Tutorial。
第二个关键部分围绕着WebSocket HTML5 Web API 对象的使用,该对象位于svggraph-client.js文件的MeetupsStream函数中。在这个函数中,我们通过设置.ondisconnect、.onopen和.onmessage处理程序来准备一个相当标准的 Ajax(实际上是WebSocket)调用:
function MeetupsStream() {
ms.onopen = ...
ms.onmessage = ...
ms.onclose = ...
}
我们最感兴趣的是.onmessage处理器,其中我们将数据(以字符串形式传入)解析为EJSON对象:
var rec_msg = EJSON.parse(e.data);
然后(条件性地)设置msgPic变量,这立即改变了cPic模板中显示的图像。我们同样更新msgComment来更改显示的评论:
if (rec_msg.group.group_photo)
Session.set('msgPic', ...);
Session.set('msgComment', rec_msg.comment);
最后,我们在服务器端的'addMsg'方法上创建了一个Meteor.call方法,这使我们的状态总数更新开始运转:
Meteor.call('addMsg', rec_msg);
小贴士
WebSocket对象也有一个很好的 MDN 教程,位于developer.mozilla.org/en-US/docs/WebSockets。
在我们使用的代码中还可以找到一些其他零散的内容,例如meteorhacks:aggregate包,它允许我们通过.aggregate方法在 MongoDB 集合上进行求和和分组(官方支持即将推出!)但这个菜谱旨在关注 SVG 和 Ajax(WebSocket)方面。
参见
-
在第三章的创建动态列表菜谱中,构建出色的用户界面
-
在第十一章的处理异步事件菜谱中,利用高级功能
使用 HTML FileReader 上传图像
通过网络应用添加文件现在是一个相当标准的功能。但这并不意味着它容易通过编程来实现。新浏览器支持 Web API 来简化我们的工作,并且存在许多高质量的库/包来帮助我们导航文件读取/上传的森林,但作为我们这样的编码伐木工,我们喜欢知道如何自己动手!在这个菜谱中,你将学习如何读取和上传图像文件到 Meteor 服务器。
准备工作
我们将使用默认的项目安装,包括 client、server 和 both 文件夹,以及用于存储图像的特殊文件夹。在终端窗口中,导航到你希望项目存放的位置,并执行以下命令:
$ meteor create imageupload
$ cd imageupload
$ rm imageupload.*
$ mkdir client
$ mkdir server
$ mkdir both
$ mkdir .images
注意 .images 文件夹中的点。这非常重要,因为我们不希望每次向服务器添加图像时,Meteor 应用程序都会自动刷新!通过将图像文件夹命名为 .images,我们将其隐藏在 Meteor 内置的类似索伦之眼的监控系统之下,因为以点开头的文件夹对 Linux 或 Unix 来说是“不可见”的。
让我们也注意一下我们需要的额外大气层(Atmosphere)包。在同一个终端窗口中,执行以下命令:
$ meteor add twbs:bootstrap
$ meteor add voodoohop:masonrify
我们现在可以开始构建我们的图像上传应用程序了。
如何操作...
我们想显示我们上传的图像,因此我们将使用布局包(voodoohop:masonrify)进行显示。我们还将通过拖放来启动上传,以减少 UI 组件。最后,我们将依赖一个 npm 模块来使文件上传变得更容易。让我们将其分解为几个步骤,从用户界面开始。
-
在
[项目根目录]/client文件夹中,创建一个名为imageupload.html的文件,并添加以下模板和template包含:<body> <h1>Images!</h1> {{> display}} {{> dropzone}} </body> <template name="display"> {{#masonryContainer columnWidth=50 transitionDuration="0.2s" id="MasonryContainer" }} {{#each imgs}} {{> img}} {{/each}} {{/masonryContainer}} </template> <template name="dropzone"> <div id="dropzone" class="{{dropcloth}}">Drag images here...</div> </template> <template name="img"> {{#masonryElement "MasonryContainer"}} <img src="img/{{src}}" class="display-image" style="width:{{calcWidth}}"/> {{/masonryElement}} </template> -
我们只想添加一点点的样式,包括为我们的拖放区域添加一个“激活”状态,这样我们就能知道何时可以安全地将文件拖放到页面上。在你的
[项目根目录]/client/文件夹中,创建一个新的style.css文件,并输入以下 CSS 样式指令:body { background-color: #f5f0e5; font-size: 2rem; } div#dropzone { position: fixed; bottom:5px; left:2%; width:96%; height:100px; margin: auto auto; line-height: 100px; text-align: center; border: 3px dashed #7f898d; color: #7f8c8d; background-color: rgba(210,200,200,0.5); } div#dropzone.active { border-color: #27ae60; color: #27ae60; background-color: rgba(39, 174, 96,0.3); } img.display-image { max-width: 400px; } -
现在我们想创建一个
Images集合来存储上传的图像文件的引用。为此,我们将依赖 EJSON。EJSON 是 Meteor 对 JSON 的扩展版本,它允许我们快速将二进制文件从客户端传输到服务器。在你的[项目根目录]/both/文件夹中,创建一个名为imgFile.js的文件,并添加以下 MongoDB 集合的代码行:Images = new Mongo.Collection('images'); -
现在我们将创建
imgFile对象,并声明一个用于客户端和服务器上的imgFileEJSON 类型。在先前的Images声明之后,输入以下代码:imgFile = function (d) { d = d || {}; this.name = d.name; this.type = d.type; this.source = d.source; this.size = d.size; }; -
为了正确初始化
imgFile作为 EJSON 类型,我们需要实现fromJSONValue()、prototype()和toJSONValue()方法。然后我们将使用EJSON.addType()方法将imgFile声明为 EJSON 类型。在imgFile函数声明下方添加以下代码:imgFile.fromJSONValue = function (d) { return new imgFile({ name: d.name, type: d.type, source: EJSON.fromJSONValue(d.source), size: d.size }); }; imgFile.prototype = { constructor: imgFile, typeName: function () { return 'imgFile' }, equals: function (comp) { return (this.name == comp.name && this.size == comp.size); }, clone: function () { return new imgFile({ name: this.name, type: this.type, source: this.source, size: this.size }); }, toJSONValue: function () { return { name: this.name, type: this.type, source: EJSON.toJSONValue(this.source), size: this.size }; } }; EJSON.addType('imgFile', imgFile.fromJSONValue);备注
本食谱中使用的 EJSON 代码深受克里斯·马瑟(Chris Mather)的事件驱动思维文件上传教程的启发。我们建议查看他的网站,并了解更多关于文件上传的信息。
www.eventedmind.com。 -
尽管通常将客户端特定和服务器特定代码放在单独的文件中会更干净,但由于代码与刚刚输入的
imgFile代码相关,我们将把它们都放在同一个文件中。在上一步中EJSON.addType()函数调用下方,添加以下Meteor.isClient和Meteor.isServer代码:if (Meteor.isClient){ _.extend(imgFile.prototype, { read: function (f, callback) { var fReader = new FileReader; var self = this; callback = callback || function () {}; fReader.onload = function() { self.source = new Uint8Array(fReader.result); callback(null,self); }; fReader.onerror = function() { callback(fReader.error); }; fReader.readAsArrayBuffer(f); } }); _.extend (imgFile, { read: function (f, callback){ return new imgFile(f).read(f,callback); } }); }; if (Meteor.isServer){ var fs = Npm.require('fs'); var path = Npm.require('path'); _.extend(imgFile.prototype, { save: function(dirPath, options){ var fPath = path.join(process.env.PWD,dirPath,this.name); var imgBuffer = new Buffer(this.source); fs.writeFileSync(fPath, imgBuffer, options); } }); }; -
接下来,我们将添加一些
Images集合的insert辅助工具。我们将提供添加图像引用(URI)或上传文件到服务器上的.images文件夹的能力。为此,我们需要一些Meteor.methods。在[project root]/server/文件夹中,创建一个imageupload-server.js文件,并输入以下代码:Meteor.methods({ addURL : function(uri){ Images.insert({src:uri}); }, uploadIMG : function(iFile){ iFile.save('.images',{}); Images.insert({src:'images/' +iFile.name}); } }); -
我们现在需要建立从
.images文件夹处理/提供图像的代码。我们需要绕过 Meteor 的正常资产服务能力,以处理在(隐藏的).images文件夹中找到的内容。为此,我们将使用fsnpm 模块,并将任何访问Images/文件夹地址的内容请求重定向到服务器上找到的实际.images文件夹。在上一步中输入的Meteor.methods块之后,添加以下WebApp.connectHandlers.use()函数代码:var fs = Npm.require('fs'); WebApp.connectHandlers.use(function(req, res, next) { var re = /^\/images\/(.*)$/.exec(req.url); if (re !== null) { var filePath = process.env.PWD + '/.images/'+ re[1]; var data = fs.readFileSync(filePath, data); res.writeHead(200, { 'Content-Type': 'image' }); res.write(data); res.end(); } else { next(); } }); -
我们的图像
display模板完全依赖于Images集合,因此我们需要在客户端添加适当的反应式Template.helpers函数。在你的[project root]/client/文件夹中,创建一个imageupload-client.js文件,并添加以下代码:Template.display.helpers({ imgs: function () { return Images.find(); } }); -
如果我们添加了不喜欢的图片并想快速删除它们,最简单的方法是在图片上双击。所以,让我们在这个文件中
Template.helpers方法下方添加执行此操作的代码:Template.display.events({ 'dblclick .display-image': function (e) { Images.remove({ _id: this._id }); } }); -
现在是时候添加一些有趣的功能了。我们将添加拖放视觉反馈提示,这样无论何时我们将任何东西拖到我们的拖放区域,拖放区域都会向用户提供视觉反馈。同样,一旦我们离开该区域,或者实际上放下项目,拖放区域应该恢复到正常状态。我们将通过一个
Session变量来实现这一点,该变量在div.dropzone元素的 CSS 类被更改时修改。在imageupload-client.js文件的底部,添加以下Template.helpers和Template.events代码块:Template.dropzone.helpers({ dropcloth: function () { return Session.get('dropcloth'); } }); Template.dropzone.events({ 'dragover #dropzone': function (e) { e.preventDefault(); Session.set('dropcloth', 'active'); }, 'dragleave #dropzone': function (e) { e.preventDefault(); Session.set('dropcloth'); } }); -
最后的任务是评估被拖放到页面拖放区域的内容。如果拖放的是简单的 URI,我们将直接将其添加到
Images集合中。如果是文件,我们将存储它,创建一个指向它的 URI,然后将它附加到Images集合中。在imageupload-client.js文件中,在Template.dropzone.events代码块最后的闭合花括号之前,添加以下事件处理逻辑:'dragleave #dropzone': function (e) { ... }, 'drop #dropzone': function (e) { e.preventDefault(); Session.set('dropcloth'); var files = e.originalEvent.dataTransfer.files; var images = $(e.originalEvent.dataTransfer.getData('text/html')).find('img'); var fragment = _.findWhere(e.originalEvent.dataTransfer.items, { type: 'text/html' }); if (files.length) { _.each(files, function (e, i, l) { imgFile.read(e, function (error, imgfile) { Meteor.call('uploadIMG', imgfile, function (e) { if (e) { console.log(e.message); } }); }) }); } else if (images.length) { _.each(images, function (e, i, l) { Meteor.call('addURL', $(e).attr('src')); }); } else if (fragment) { fragment.getAsString(function (e) { var frags = $(e); var img = _.find(frags, function (e) { return e.hasAttribute('src'); }); if (img) Meteor.call('addURL', img.src); }); } } }); -
保存所有更改并打开浏览器到
http://localhost:3000。从任何网站找到一些图片,并将它们拖放到拖放区域。当你拖放图像时,图像将立即出现在你的网页上,如下面的截图所示:![如何操作…]()
当你将恐龙图像拖放到拖放区域时,它们将按以下截图所示上传:
![如何操作…]()
同样,拖放实际文件也会迅速上传并显示图像,如下面的截图所示:
![如何操作…]()
-
当文件被拖放时,它们会被上传并保存在
.images/文件夹中:![如何操作…]()
它是如何工作的…
我们刚刚创建的代码有很多组成部分,但我们可以将它精简为四个区域。
首先,我们创建了一个新的 imgFile 对象,包括通过 Object.prototype = {…} 声明添加的内部函数。这里添加的函数(typeName、equals、clone、toJSONValue 和 fromJSONValue)主要用于允许 imgFile 对象在客户端和服务器上正确地进行 序列化 和 反序列化。通常情况下,这并不是必需的,因为我们可以直接将数据 insert 到 Mongo 集合中,但在这个例子中是必需的,因为我们想在客户端和服务器上分别使用 FileReader 和 Node fs 包来直接加载和保存图像文件,而不是将它们写入集合。
第二,客户端使用下划线 _.extend() 方法创建 read() 函数,而服务器端则用于创建 save() 函数。read 函数接收被拖放的文件,将其读取到 ArrayBuffer 中,然后调用包含的 callback,将文件上传到服务器。服务器端的 save 函数读取 ArrayBuffer,并将后续的图像文件写入服务器上的指定位置(在我们的例子中,是 .images 文件夹)。
第三,我们创建了一个 ondropped 事件处理器,使用 'drop #dropzone' 事件。此处理器确定是否实际文件被拖放并放下,或者它只是一个包含 src 属性中 URI 链接的 HTML <img> 元素。在文件的情况下(由 files.length 确定),我们调用 imgFile.read 命令,并传递一个带有立即 Meteor.call('uploadIMG'…) 方法的回调。在 <img> 标签的情况下,我们解析 src 属性中的 URI,并使用 Meteor.call('addURL') 更新 Images 集合。
第四,我们有用于更新 UI 的helper函数。这些包括Template.helpers函数、Template.events函数以及WebApp.connectedHandlers.use()函数,用于在每次上传文件时无需更新 UI 的情况下正确地服务上传的图片。记住,Meteor 会在任何文件更改时自动更新 UI。遗憾的是,这包括静态文件,例如图片。为了解决这个问题,我们将我们的图片存储在一个对 Meteor 不可见的文件中(使用.images)。为了将流量重定向到那个隐藏文件夹,我们实现了.use()方法来监听任何意图访问'/images/'文件夹的流量,并相应地重定向。
与任何复杂的配方一样,代码中还有其他部分,但这应该涵盖了文件上传的主要方面(前面章节中提到的四个区域)。
更多内容…
下一个逻辑步骤不是简单地复制远程图像文件的 URI,而是下载、保存并服务这些远程图像的本地副本。这也可以使用FileReader和 Node fs库来完成,可以通过前面章节中提到的现有客户端代码,或者直接在服务器上作为cron作业来完成。
小贴士
更多关于FileReader的信息,请参阅位于developer.mozilla.org/en-US/docs/Web/API/FileReader的 MDN FileReader 文章。
相关内容
-
第八章中的直接使用 npm 包配方,集成第三方库
-
第十一章中的创建自定义 EJSON 对象配方,利用高级功能
使用 Canvas 元素制作彩色书
现在有多种方式可以在网页上显示图形。DOM 元素、SVG、WebGL,以及可能是最用户友好的Canvas元素。使用 JavaScript,Canvas元素提供了一个灵活的图形区域,你可以随心所欲地绘画、擦除、上色、剪切和粘贴。你可以在这个配方中找到一个很好的Canvas元素的应用示例,你将学习如何从头开始构建一个彩色书应用。
准备工作
拿出你的蜡笔!好吧,可能不需要(不要在显示器上使用它们!)但我们即将创建的是最好的替代品,所以让我们先处理掉应用结构和包,这样我们就可以开始涂色了!
除了常见的文件夹外,我们还需要一些特殊文件夹来使我们的代码分组更干净、更易于管理。打开一个终端窗口,导航到你想要创建根项目的地方,并执行以下命令:
$ meteor create coloringbook
$ cd coloringbook
$ rm coloringbook.*
$ mkdir -p client/scripts
$ mkdir {both,server,public}
$ meteor
如何做到这一点…
表面上可能看起来不多,但实际上开发一本涂色书应用有很多工作要做。你必须考虑用户偏好、撤销/重做、选择颜色、擦除等等。我们将一步一步地解决这些问题,随着项目的进行添加包和功能。
-
首先,我们将添加用户管理包。Meteor 提供了一些非常出色的开箱即用的用户账户管理功能,我们将利用这些功能。在终端窗口中,导航到你的
[项目根目录]文件夹,并输入以下命令添加以下两个 Meteor 库:$ meteor add accounts-password $ meteor add accounts-ui -
我们希望在屏幕上显示这些样式,所以让我们添加必要的 UI 元素并适当地进行样式化。在你的
[项目根目录]/client/文件夹中创建一个名为cb-ui.html的文件,并添加以下代码:<body> {{> loginButtons}} </body>目前你的页面看起来非常简洁,但就像那样,我们已经有账户和账户创建功能了!在浏览器中,导航到
http://localhost:3000,你应该会在屏幕左上角看到账户对话框下拉菜单,类似于以下截图:![如何操作…]()
-
让我们通过添加一个工具栏来完成 UI 的视觉方面,在这个工具栏中我们可以看到颜色样本并调整画笔的大小;同时,我们也要添加我们的
canvas元素,以及一个背景<div>来显示我们想要着色的图片。在同一个cb-ui.html文件中,在body元素块中添加以下模板包含:<body> {{> loginButtons}} {{> toolbar}} <div id="bgpicture"></div> {{> picture}} </body> -
picture模板实际上是最简单的,所以我们将首先添加该代码。在</body>标签下方添加以下模板代码:<template name="picture"> <canvas id="picture" class="no-copy">Images go here...</canvas> </template> -
现在我们添加
toolbar和swatch模板,这些模板使用 SVG 和Masonry.js来显示我们的画笔和颜色样本。在上一步骤添加的模板代码下方,添加以下代码:<template name="toolbar"> <div class="brush-size" id="brush-size"> <svg id="brush-preview" height="70" width="70" style="display:{{eraseMode}}"> <circle cx="35" cy="35" r="{{preview.size}}" fill="{{preview.color}}" /> </svg> </div> {{#masonryContainer columnWidth=50 transitionDuration="0.2s" id="MasonryContainer" }} {{#each swatches}} {{> swatch}} {{/each}} {{/masonryContainer}} </template> <template name="swatch"> {{#masonryElement "MasonryContainer"}} <div class="swatch" style="background-color:{{color}};"> {{color}}</div> {{/masonryElement}} </template> -
为了让一切看起来都很漂亮,我们需要添加一些 Atmosphere 包,以及一点 CSS 样式。在终端窗口中,导航到你的
[项目根目录]文件夹,并运行以下命令:$ meteor add twbs:bootstrap $ meteor add voodoohop:masonrify -
接下来,在
[项目根目录]/client/文件夹中创建一个名为cb-style.css的文件,并添加以下样式声明:#picture { color: #ccc; border: 3px dashed #ccc; width:800px; height:600px; border-radius: 4px; line-height: 3em; text-align: center; left: 100px; position: absolute; } #bgpicture { pointer-events:none; touch-events:none; position:absolute; background: url('Rawr.GIF'); top:0px; left:100px; width: 800px; height:600px; z-index: 999; } .no-copy { -webkit-user-select: none; } #login-buttons { position : absolute; min-width: 220px; right: 20px; top: 10px; } .login-link-text { position:absolute; right: 0px; } .swatch { height:50px; width:50px; border-radius: 4px; border: solid #ccc 2px; line-height: 50px; font-size:0.8em; text-align: center; margin-bottom: 5px; } .masonry_container { position:absolute; top:100px; left:23px; width: 50px; } .brush-size { position:absolute; left:10px; top:10px; border: dashed 3px #ccc; border-radius: 40px; width:76px; height:76px; } -
注意到
bgpicture元素中有一个背景图片,名为'RAWR.GIF'——你可以从本菜谱的源文件中获取这张图片,或者你可以添加自己的图片进行着色。图片 必须 有透明背景,如果它是800x600像素,看起来会更好,但无论如何,你想要显示的图片应该放入[项目根目录]/public/文件夹中,并且你应该在上一步中更改background属性,以便它在屏幕上正确显示。 -
我们需要能够更改画笔大小、更改颜色,并在画布上绘画/擦除,并且我们希望在无论什么设备上都能做到这一点,因此我们将使用官方的
Hammer.js包。我们还希望包括全局快捷键来撤销(control + Z)和重做(shift + control + Z),因此我们将添加一个方便的库来实现这一点!在终端窗口中,在你的[项目根]文件夹中,输入以下两个命令:$ meteor add hammer:hammer@=2.0.4_1 $ meteor add gwendall:body-events -
在
Template.events块中使用Hammer.js事件更干净、更易于使用,因此我们希望为Hammer.js文件添加 jQuery 插件。由于 Meteor 依赖于 jQuery 事件处理器,如果插件添加到 jQuery 中,它也会添加到 Meteor 中!截至本文撰写时,添加插件最可靠的方法是将plugin脚本文件手动复制到你的[项目根]/client/scripts/文件夹中。你可以通过访问hammerjs.github.io/jquery-plugin/并遵循那里的说明来获取脚本,或者在你的scripts文件夹中创建一个名为jquery.hammer.js的文件,并添加以下代码:(function(factory) { if (typeof define === 'function' && define.amd) { define(['jquery', 'hammerjs'], factory); } else if (typeof exports === 'object') { factory(require('jquery'), require('hammerjs')); } else { factory(jQuery, Hammer); } }(function($, Hammer) { function hammerify(el, options) { var $el = $(el); if(!$el.data("hammer")) { $el.data("hammer", new Hammer($el[0], options)); } } $.fn.hammer = function(options) { return this.each(function() { hammerify(this, options); }); }; // extend the emit method to also trigger jQuery events Hammer.Manager.prototype.emit = (function(originalEmit) { return function(type, data) { originalEmit.call(this, type, data); $(this.element).trigger({ type: type, gesture: data }); }; })(Hammer.Manager.prototype.emit); })); -
现在,我们将添加
Activities、Prefs和Swatches集合。如前所述,我们需要通过记录活动来跟踪我们的撤销/重做。我们还需要跟踪颜色样本、用户对画笔大小的偏好等。为此,我们将声明三个 MongoDB 集合。在你的[项目根]/both/文件夹中,创建一个名为cb-model.js的文件,并添加以下代码:Swatches = new Mongo.Collection('swatches'); Prefs = new Mongo.Collection('prefs'); Activities = new Mongo.Collection('activities');我们稍后将在客户端和服务器端使用这些,但现在我们已经完成了所有工作。你可以关闭
cb-model.js文件,继续下一步。 -
让我们添加偏好设置和撤销/重做历史记录的服务器端逻辑。尽管这听起来可能令人难以置信,但与客户端代码相比,彩色书的服务器端代码非常轻量。因此,我们将把它放在一边。我们需要在服务器上做一些事情:
-
初始化样本和其他偏好设置
-
如果需要,允许添加新的样本
-
跟踪和回忆我们的绘画活动(撤销/重做)
-
清除一切,以便从头开始
在你的
[项目根]/server/文件夹中,创建一个名为cb-server.js的文件,并添加以下Meteor.methods声明:Meteor.methods({ initSwatches : function(userId){ // no user = nothing to do. return. if (!userId) return; // if we already have swatches, return. if (Swatches.findOne({user:userId})) return; // add initial swatches Swatches.insert({color: '#ecf0f1', user:userId}); Swatches.insert({color: '#ccc', user:userId}); Swatches.insert({color: '#f1c40f', user:userId}); Swatches.insert({color: '#e67e22', user:userId}); Swatches.insert({color: '#e74c3c', user:userId}); Swatches.insert({color: '#2ecc71', user:userId}); Swatches.insert({color: '#2980b9', user:userId}); Swatches.insert({color: '#000', user:userId}); }, addSwatch : function (color){ // no user = nothing to do. return. if (!this.userId) return; // if it doesn't already exist, add the swatch if (!Swatches.findOne({color:color})){ Swatches.insert({color:color, user:this.userId}); } }, clearActivity : function(){ // no user, return. if (!this.userId) return; // clear the undo history from Activities collection Activities.remove({user:this.userId}); }, breakHistory : function(snapShot){ // if we don't have a valid snapshot, // or user isn't logged in, return. if (!snapShot||!this.userId) return; // remove all snapshots after this one in the undo chain Activities.remove({$and: [{createdAt:{$gt:snapShot.createdAt}},{user:this.userId}]}) } }); -
-
现在在同一个文件中添加
Accounts.onLogin()事件处理器:Accounts.onLogin(function(login){ // first, confirm that we have a valid userId userId = login.user._id; if (!userId) return; // if so, and if we don't have preferences, let's initialize if (!Prefs.findOne({user:userId})){ Prefs.insert({user:userId, size:11, color:'#e74c3c'}); } // likewise, let's initialize swatches Meteor.call('initSwatches', userId); }); -
是时候构建允许我们使用
canvas元素在屏幕上绘制的主体逻辑了。在[项目根]/client/文件夹中创建一个名为cb-client.js的文件,并添加以下初始默认值,这些值控制我们要绘制的内容以及我们的画笔颜色:Session.setDefault('drawing', false); Session.setDefault('color', '#e74c3c'); -
由于它被用于多个地方,我们希望创建一个
drawLine函数,该函数将根据需要绘制/擦除屏幕的一部分。在Session.setDefault声明下方添加以下代码:// **drawLine** -- helper function to draw / erase lines drawLine = function (from, to, color,size) { if (size) ctx.lineWidth = size; if (color) ctx.strokeStyle = color; if (Session.get('erase')){ ctx.globalCompositeOperation = 'destination-out'; } else { ctx.globalCompositeOperation = 'source-over'; } ctx.beginPath(); ctx.moveTo(from.x, from.y); ctx.lineTo(to.x, to.y); ctx.closePath(); ctx.stroke(); } -
现在,添加一些辅助函数,帮助我们清理屏幕,或计算相对于页面的绘画笔迹定位:
// **getPosition** -- helper function to calculate cursor position getPosition = function (event) { return { x: parseInt(event.gesture.center.x - event.currentTarget.offsetLeft), y: parseInt(event.gesture.center.y - event.currentTarget.offsetTop) }; } // **wipe** -- function to clear the painting area wipe = function (emitAlso) { ctx.clearRect(0, 0, canvas.attr('width'), canvas.attr('height')); }当用户将鼠标/笔/手指从屏幕上抬起时,我们想要记录他们刚刚绘制的图案,因此让我们在我们的辅助函数下方添加一个快照拍摄函数:
// **addSnapshot** -- helper function to save strokes and update // undo history addSnapshot = function(){ var userId = Meteor.userId(); if (!userId) return; //Convert Canvas into a Picture ctx.globalCompositeOperation = 'source-over'; var canvasPic = canvas[0].toDataURL(); var timestamp = Date.now(); // check current history. if we are in undo-land, need to clean // up snapshots var curHist = Session.get('history'); if (curHist){ var curSnap = Session.get('currentSnapshot'); Meteor.call('breakHistory',curSnap); Session.set('history',0); } // Save it to our Activities History Activities.insert({ user:userId, canvas:canvasPic, createdAt:timestamp }); }; -
当我们首次登录,或者在我们上下翻滚撤销/重做历史记录链时,我们需要重新绘制屏幕。在
addSnapshot辅助函数下方添加以下辅助函数:// **paintActivity** -- helper function to redraw screen on undo/// redo/draw paintActivity = function(userId,idx){ var latestActs = Activities.find({user:userId}, {sort:{createdAt:-1}}).fetch(); if (!latestActs.length) { return; } if(!latestActs[idx]) idx = latestActs.length-1; wipe(); var imageObj = new Image(); imageObj.onload = function () { ctx.drawImage(this, 0, 0); }; Session.set('currentSnapshot',latestActs[idx]); imageObj.src = latestActs[idx].canvas; }; -
好的,我们已经准备好进行最后的润色,这包括启用我们的工具栏,并将所有触摸/鼠标事件连接起来以调整画笔大小、更改颜色、绘画和擦除。在您的
[project root]/client/文件夹中,创建一个名为tmpl-toolbar.js的文件,并添加以下交互事件处理器代码:Template.toolbar.rendered = function(){ // we first need to turn on hammer.js touch events... var brushSize = this.$('#brush-size').hammer(); // ...we then change the pan threshold from 10 to 2 var mgr = brushSize.data('hammer'); mgr.get('pan').set({ threshold: 2 }); }; -
在同一文件中,添加
Template.helpers函数以显示画笔大小、颜色偏好等:Template.toolbar.helpers({ swatches: function () { // Return the swatches for this user return Swatches.find({ user: Meteor.userId() }); }, preview : function(){ // gets preferences for toolbar, with one modification... var prefs = Prefs.findOne({user:Meteor.userId()}); // ...because brush is a circle, we need radius, not diameter if (prefs) prefs.size= ~~(prefs.size/2); return prefs; }, eraseMode : function(){ // if we're in erase mode, the brush circle is hidden return (Session.get('erase')? 'none':null); } }); -
让我们再添加交互逻辑,通过
Template.events辅助块:Template.toolbar.events({ 'panstart #brush-size' : function(ev){ // record our offset position, and turn on resizing Session.set('brushFrom',ev.gesture.center.x); Session.set('brushResize',true); }, 'pan #brush-size': function(ev){ // if we're not resizing, no need to continue if (!Session.equals('brushResize',true)) return; // likewise, if there are no prefs, just return var prefs = Prefs.findOne({user:Meteor.userId()}); if (!prefs) return; // calculate the delta from last we checked... var adjustment = Session.get('brushFrom'); adjustment = ev.gesture.center.x - adjustment; // ...and create a new brush size var newbrushSize = prefs.size + adjustment; // reset offset position, in case resizing continues Session.set('brushFrom', ev.gesture.center.x); // new brush size needs to be the 3rd bowl of porridge... if (newbrushSize<=70&&newbrushSize>=3){ // adjust the preferences record and update the collection prefs.size = newbrushSize; Prefs.update({_id:prefs._id}, prefs); } }, 'panstop #brush-size': function(ev){ // job's done. clean up. Session.set('brushFrom'); Session.set('brushResize',false); }, 'doubletap #brush-size': function(ev){ // turn on 'erase' mode Session.set('erase',(!Session.get('erase'))); } }); -
我们还对
swatch模板有一些逻辑/事件处理,因此让我们在[project root]/client/文件夹中创建一个名为tmpl-swatch.js的文件,并添加以下事件监听器和事件交互逻辑:// suuuuper simple turning on of touch events // using hammer.js Template.swatch.rendered = function () { this.$('.swatch').hammer(); }; Template.swatch.events({ 'tap .swatch': function (ev) { // if no preference, return; var prefs = Prefs.findOne({user:Meteor.userId()}); if (!prefs) return; // change the color to whatever swatch we tapped on prefs.color = this.color; // update Prefs collection Prefs.update({_id:prefs._id},prefs); } }); -
再添加两个文件,我们就完成了!在
picture模板(我们的canvas元素所在的位置)中,我们需要处理相当多的交互,因此让我们在[project root]/client/文件夹中创建一个名为tmpl-picture.js的文件,并通过Template.rendered方法块添加我们的初始化逻辑。将以下代码添加到tmpl-picture.js文件中:Template.picture.rendered = function () { // set the canvas we will be drawing on canvas = this.$('#picture'); // set the context for the canvas ctx = canvas[0].getContext('2d'); // need to properly size the canvas canvas.attr({ width: 800, height: 600 // ...AND set up tap listeners via hammer.js }).hammer(); // we want to change the default threshold from 10 to 2 canvas.data('hammer').get('pan').set({threshold:2}); // we now set the line and line cap style ctx.lineJoin = ctx.lineCap = 'round'; // Stops iOS from doing that bouncy, janky thing document.ontouchmove = function (event) { event.preventDefault(); }; -
在偏好更改或用户登录/注销时,我们必须执行一些响应式逻辑来清理或初始化我们的画布。在
Template.picture.rendered下方添加两个autorun方法块,如下所示:// Reactive function that reruns whenever // preference are updated this.autorun(function () { // if no prefs exist, return var prefs = Prefs.findOne({user:Meteor.userId()}); if (!prefs) return; // set stroke color and width ctx.strokeStyle = prefs.color; ctx.lineWidth = prefs.size; }); // Reactive function that reruns whenever // User logs in, or our undo history position changes this.autorun(function(){ // if we're not logged in (no userId), return var userId = Meteor.userId(); if (!userId){ wipe(); return; } // otherwise, paint the proper screen, // using the undo chain history position paintActivity(userId,Session.get('history')||0); }); }; -
picture模板的最后一件事情是将绘图事件本身连接起来。当我们开始绘图时,我们需要panstart、panmove、panend的事件处理器,并且我们还需要一个在页面上doubletap时清理一切的处理器。将以下Template.events方法块添加到tmpl-pictur``e.js文件的底部:Template.picture.events({ 'panmove #picture': function (ev) { // we must be in drawing mode... if (Session.equals('drawing', true) && Meteor.userId()) { // find our cursor position to = getPosition(ev); // physically draw the stroke drawLine(from,to); // update our from position from = to; } }, 'panstart #picture': function (ev) { // get our from position, when we start drawing from = getPosition(ev); // tell everyone that we are in drawing mode Session.set('drawing', true); }, 'panend #picture': function (ev) { // drawing mode is over! Session.set('drawing', false); // we now record the screen, add to undo chain addSnapshot(); }, 'doubletap #picture': function (ev) { // clear the screen wipe(); // wipe out our undo history Meteor.call('clearActivity'); } }); -
现在只剩下
keydown事件逻辑,这样我们才能有适当的撤销/重做。快速在[project root]/client/文件夹中创建一个名为tmpl-body.js的文件,并添加以下代码:Template.body.events({ 'keydown' : function(ev){ // if there's no undo history, no reason to continue, // so return. var histLength = Activities.find({user:Meteor.userId()}).fetch().length; if (!histLength) return; // If it's not a CTRL+Z or CMD+Z, we don't care, so // return. if ((!ev.metaKey && !ev.ctrlKey)||(ev.keyCode!==90)) return; // find the current position in the undo chain, if any. var curHist = Session.get('history')||0; // if it was SHIFT+CMD+Z, it means redo, so decrement // the history if (ev.shiftKey) curHist--; // otherwise, increment the history else curHist++; // if we're past the boundaries of TIME and SPACE we // certainly don't care about JavaScript anymore, so let's return. if(curHist<0 || curHist> histLength-1 ) return; // after all that, set the new undo chain position Session.set('history',curHist); } });呼呼!我们做到了!保存所有更改,并通过浏览器导航到您的项目,通过
http://localhost:3000。登录/创建账户后,您应该看到一个漂亮的调色板,您选择的图片,以及画笔大小/预览。以下功能可用:-
在画笔预览上双击以切换
'eraser'模式 -
在画笔预览上点击并左右拖动以调整画笔大小
-
在页面上双击以擦除并清除您的撤销历史
-
按Ctrl + Z或CMD + Z来撤销您的笔触
-
按Shift + Ctrl + Z或Shift + CMD + Z来重做您的笔触
-
使用另一个用户注销并重新登录,以允许多用户使用
请注意,即使你登出,你的撤销/重做历史链也会被保留。一旦你重新登录,你可以逐笔浏览你的绘画创作。
如果你是一个特别棒的艺术家,就像我的女儿一样,并且你正确地编写了所有代码,你的页面看起来将类似于以下杰作:
![如何做到这一点…]()
-
它是如何工作的…
哇!这代码太多了,只是为了演示canvas元素的工作原理,不是吗?我们不必一步一步地通过所有这些,让我们看看与Canvas元素相关的重要部分。
在tmpl-picture.js内部,我们初始化我们的canvas。在Template.picture.rendered()辅助块中,我们首先使用this.$('#picture')找到我们的canvas元素:
canvas = this.$('#picture');
我们然后使用名为getContext()的非常合适的函数来获取上下文(处理程序)。我们只想得到 2D 表示,而不是 3D,所以我们把'2d'作为参数传递给getContext:
ctx = canvas[0].getContext('2d');
接下来是cb-client.js,我们在全局ctx变量中传递上下文,有几种用途。在drawLine函数中,我们设置大小(lineWidth),颜色(strokeStyle),并根据我们是擦除('destination-out')还是铺上一些颜料('source-over')来设置笔触类型(globalCompositeOperation):
drawLine = function (from, to, color,size) {
...
ctx.lineWidth = size;
...
ctx.strokeStyle = color;
if (Session.get('erase')){
ctx.globalCompositeOperation = 'destination-out';
} else {
ctx.globalCompositeOperation = 'source-over';
}
...
一切设置/确定之后,我们告诉画布我们开始绘制(beginPath);我们移动我们的笔触(moveTo,lineTo),然后我们清理(closePath,stroke):
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(to.x, to.y);
ctx.closePath();
ctx.stroke();
为了节省数据库空间/事务,我们不记录每一笔。相反,我们等待一笔画完成('panend #picture'事件)并添加整个画布的快照。我们是在addSnapshot辅助函数内部这样做,通过调用canvas.toDataURL()方法:
var canvasPic = canvas[0].toDataURL();
一旦我们将canvas图形元素表示为数据,我们只需通过Activities.insert()将其保存到Activities集合中:
Activities.insert({
user:userId,
canvas:canvasPic,
createdAt:timestamp
});
从Activities集合检索和显示保存的截图同样简单。ctx处理程序为我们处理绘图。我们只需提供数据,就可以让那只猴子跳舞。因此,在paintActivity中,我们创建一个新的Image()对象,并给那个imageObj添加一个.src。然后,当.onLoad事件回调被触发时,我们调用ctx.drawImage():
var imageObj = new Image();
imageObj.onload = function () {
ctx.drawImage(this, 0, 0);
};
最后,也是最简单的,如果我们想清除屏幕,我们只需调用ctx.clearRect()并指定我们想要清除的尺寸(在这种情况下,画布的width和height):
ctx.clearRect(0, 0, canvas.attr('width'), canvas.attr('height'));
在这里,有很多 Meteor 魔法在协同工作,因此我们尽可能少地使用函数式编程,但这种协调在其他菜谱中有详细说明。
还有更多…
实际上,还有更多!我们本来可以添加自定义颜色样本(顺便说一句,你可以使用 Meteor.call('addSwatch','#yourcolorhere') 从控制台完成此操作),或者启用拖放,以及存储其他图片。如果你想要这样做,可以使用本章中找到的 使用 HTML FileReader 上传图片 菜单。
然而,核心功能将保持不变:引用一个 <canvas> 对象,获取上下文,然后绘制,宝贝,画吧!
小贴士
想要一个关于 HTML canvas 的信息丰富且详尽的教程,请访问:www.html5canvastutorials.com/。
参见
-
在 第二章 的 添加 Meteor 包 菜单,使用包进行自定义
-
在 第三章 的 使用 Spacebars 插入模板 和 创建自定义全局辅助函数 菜单,构建出色的用户界面
-
在 第六章 的 创建和消费反应性值 菜单,掌握反应性
-
本章中的 使用 HTML FileReader 上传图片 菜单
第八章。集成第三方库
在本章中,您将学习以下主题:
-
直接使用 npm 包
-
使用 D3.js 构建图表
-
使用 Polymer 创建前沿的 UI
简介
由于 Atmosphere 提供了如此多的第三方包,因此构建任何东西都非常容易!如果您有特定的开发需求——某种类型的逻辑或库——很可能其他人已经为您打包好了(如果还没有,那将是一个很好的机会!)。
话虽如此,并非所有包都是平等的。一些将拥有出色的文档,包括如何在 Meteor 中实现库的示例和教程。而另一些则不然。在任何情况下,您最终都需要自己动手做一些工作。在本章中,我们将介绍一些更受欢迎的第三方库,并展示如何在 Meteor 中实现它们。
直接使用 npm 包
在第二章中,我们通过“使用 npm 模块”配方了解了如何使用 Npm.depends() 指令将 npm 包封装到您自己的个人包中,这是在“使用包进行自定义”部分的内容。这当然很好,但如果我们只想直接使用 npm,而不创建自定义包呢?Arunoda Susiripala——最富有创造力(且才华横溢)的 Meteor 社区开发者之一,已经编写了一个包,可以帮助我们实现这一点。在本配方中,您将学习如何使用 meteorhacks:npm 包直接在您的代码中实现 npm 模块。具体来说,我们将使用 Highlight.js 模块在服务器上正确格式化 JavaScript 代码,就像您在 Markdown 或在线代码编辑器中看到的那样。
准备工作
我们将使用默认项目安装,包括 client、server 和 both 文件夹,以保持代码的整洁和可读性。我们还将使用 private 文件夹来存储一些静态内容,所以让我们在此时添加它。在终端窗口中,导航到您希望项目驻留的位置,并执行以下命令:
$ meteor create npmdirect
$ cd npmdirect
$ rm npmdirect.*
$ mkdir {client,server,private,both}
我们还希望使用 meteorhacks:npm 包,所以现在就让我们来做吧。在终端中运行以下命令:
$ meteor add meteorhacks:npm
$ meteor
最后,我们希望打开浏览器到 http://localhost:3000,这样我们就可以观看有趣的内容了!我们现在已经准备好开始创建我们的直接 npm 集成。
如何做到这一点...
我们将添加 Highlight.js Node 模块,并添加一些示例代码来利用该模块,使用内联模板作为我们的工作区域。按照以下步骤直接使用 npm 包:
-
我们首先将添加
Highlight.jsNode 模块。为了添加直接使用的 npm 模块,我们将在packages.json文件中进行简单的声明,该文件是在我们添加meteorhacks:npm时自动添加到我们的项目中的。打开[项目根目录]/packages.json并添加以下声明:{ "highlight.js" : "8.4.0" }如果您查看运行 Meteor 的终端窗口,您将看到类似以下文本的内容:
npm-container: updating npm dependencies -- highlight.js... => Meteor server restarted我们的
Highlight.js模块现在可以使用了! -
我们现在将创建使用
Highlight.js所需的服务器逻辑。在[项目根目录]/server/目录中创建一个名为server.js的文件。打开该文件进行编辑,并添加以下代码:Meteor.methods({ highlight : function(){ return setC(); } }); var setC = function(){ var hLight = Meteor.npmRequire('highlight.js'); var code = Assets.getText('code.txt'); code = hLight.highlight('javascript',code,true); return code.value; }; -
接下来,让我们在
private文件夹中创建一个示例代码文件。我们将使用一个基础文本文件来突出显示代码,以便使语法更加简单(而不是尝试在 JavaScriptvar中完成,这可能会因为转义序列而变得混乱)。Meteor 允许我们使用[项目根目录]/private/文件夹来存储仅在服务器上可见的静态文件。在private文件夹中创建一个名为code.txt的文件,并在文件中添加一些(有效的)JavaScript 代码。我们将重用前一步中的var setC = …函数,但你可以放入任何你想要的,只要它是有效的 JavaScript。一旦添加了代码,保存你的更改。 -
我们现在需要创建一个简单的代码面板模板。在
[项目根目录]/client/中创建一个名为main.html的文件。打开该文件进行编辑,并添加以下代码:<body> {{> code}} </body> <template name="code"> <pre class="hljs">{{{highlighted}}}</pre> </template> -
在模板就位后,我们可以添加
Template.code.helpers并使用Meteor.call()。在[项目根目录]/client/中创建一个名为tmpl-code.js的文件,并添加以下代码:Template.code.helpers({ highlighted : function(){ return Session.get('code'); } }); Meteor.startup(function(){ Meteor.call('highlight', function(e,d){ if (e) return; Session.set('code',d); }); });保存这些更改后,你的
code.js文件中的代码将可见,尽管几乎没有 CSS 格式化。如果你检查浏览器,你应该会看到以下类似的截图:![如何操作...]()
-
嗯,这并不那么令人印象深刻,对吧?让我们通过一些自定义 CSS 真正突出显示正在发生的事情。在浏览器中,导航到官方的
Highlights.js主题列表,位于github.com/isagalaev/highlight.js/tree/master/src/styles,并选择你喜欢的任何主题。只需单击主题名称,然后单击标有 Raw 的按钮,并复制相应的 CSS 代码。然后,在你的[项目根目录]/client/文件夹中创建一个名为style.css的文件,粘贴你复制的代码,并保存文件。你的格式化文本现在应该有一些明显的格式化和着色,类似于以下截图(这是Solarized Light主题,从我们之前提到的链接中的solarized_light.css复制而来):![如何操作...]()
工作原理...
在我们的 server.js 文件中,我们创建了一个简单的 Meteor 方法,该方法通过 Meteor.call() 在客户端可用并被调用。我们创建了一个单独的函数,即使在方法内部调用也不必要(我们本可以直接在方法调用中放置逻辑),因为我们希望保持代码的整洁和易于理解。在 setC() 函数内部,我们通过使用 Meteor.npmRequire() 直接使用一个 npm 模块:
var hLight = Meteor.npmRequire('highlight.js');
通过Assets.getText()快速引用我们的文本文件,我们将示例代码通过.highlight()方法管道输入到Highlights.js文件中,该方法以语言('javascript')作为参数,让模块知道我们希望在结果格式化文本中看到哪种语言高亮:
var code = Assets.getText('code.txt');
code = hLight.highlight('javascript',code,true);
return code.value;
我们返回格式化后的文本,随后通过Session.get('code')变量更新客户端 UI。模板渲染原始 HTML 字符串,多亏了使用{{{triple moustaches}}}而不是双大括号,我们添加的 CSS 文件则负责美化。
简而言之,这确实非常简单——通过添加meteorhacks:npm包,我们可以在packages.json文件中简单地声明我们希望使用的 npm 模块,然后在 Meteor 中通过Meteor.npmRequire()函数直接使用它们。
更多内容...
如您所知,npm 严格异步运行且非阻塞。这意味着在许多情况下,当您想在 Meteor 中使用 npm 模块,并希望同步使用时,您需要将调用包装在Async包装器中。我们将在第十一章(利用高级功能)中更深入地探讨如何做到这一点,但meteorhacks:npm包允许我们通过Async.wrap()和Async.runSync()方法快速/轻松地做到这一点。
小贴士
要了解更多关于Async.wrap和其他在meteorhacks:npm中可用的方法,请参阅atmospherejs.com/meteorhacks/npm上的介绍。
最后,尽管它没有文档说明(因此不是官方支持/可能更改),核心 Meteor 服务器库中使用了某些 npm 模块,这些模块在服务器端是公开和可用的。要使用这些模块中的任何一个,只需在您的服务器代码中使用Npm.require(),您不需要实现任何其他内容。您可以直接使用它们。截至v1.0.2的当前列表如下:
-
child_process -
crypto -
fibers -
fibers/future -
fs -
http -
os -
path -
semver -
source-map-support -
underscore -
url
参见
-
在第二章(使用包进行自定义)的使用 npm 模块食谱中,使用包进行自定义
-
在第三章(构建出色的用户界面)的使用三重大括号插入原始 HTML食谱中,构建出色的用户界面
-
在第十一章(利用高级功能)的处理异步事件和使用异步函数食谱中,利用高级功能
使用 D3.js 构建图表
在 Meteor 中使用第三方库来渲染内容非常简单且易于操作,而且市面上有很多优秀的库。我们个人非常喜欢的一个库是 D3.js 库(d3js.org/)。它有很好的文档支持,并且是其他(大多数)自包含图形渲染库实现的一个典型例子。在这个菜谱中,我们将创建一个包含动画的演示图形应用程序,使用 D3.js。
准备工作
我们将使用默认的项目安装,包括 client、server 和 both 文件夹,并且当然需要官方的 D3.js 包。在一个终端窗口中,导航到你希望项目存放的位置,并执行以下命令:
$ meteor create d3sample
$ cd d3sample
$ rm d3sample.*
$ mkdir {client,server,both}
$ meteor add d3js:d3
$ meteor
现在我们可以开始构建一些 D3 的示例图形了!
如何操作...
通常,D3.js 数据源是手动更新,或者作为某种类型的数据源的结果进行更新,调用某种类型的 update 语句。由于 Meteor 允许反应式计算(参见第六章 掌握反应性 中的 创建和消费反应性值 菜谱),我们将把 D3.js 渲染逻辑直接放入一个 Tracker.autorun() 计算中。让我们设置我们的反应性数据源,将渲染逻辑放入一个 autorun 中,并且为了增加效果,添加一些动画!
-
我们首先需要创建和初始化
Letters集合。在你的[项目根目录]/both/文件夹中创建一个名为model.js的文件,并添加以下代码:Letters = new Mongo.Collection('letters');让我们同时添加这个集合的初始化。在你的
[项目根目录]/server/文件夹中,创建一个名为server.js的文件,并添加以下Meteor.startup函数代码:Meteor.startup(function(){ if (!Letters.find().fetch().length){ Letters.insert({letter:'A',frequency:.08167}); Letters.insert({letter:'B',frequency:.01492}); Letters.insert({letter:'C',frequency:.02782}); Letters.insert({letter:'D',frequency:.04253}); Letters.insert({letter:'E',frequency:.12702}); Letters.insert({letter:'F',frequency:.02288}); Letters.insert({letter:'G',frequency:.02015}); Letters.insert({letter:'H',frequency:.06094}); Letters.insert({letter:'I',frequency:.06966}); Letters.insert({letter:'J',frequency:.00153}); Letters.insert({letter:'K',frequency:.00772}); Letters.insert({letter:'L',frequency:.04025}); Letters.insert({letter:'M',frequency:.02406}); Letters.insert({letter:'N',frequency:.06749}); Letters.insert({letter:'O',frequency:.07507}); Letters.insert({letter:'P',frequency:.01929}); Letters.insert({letter:'Q',frequency:.00095}); Letters.insert({letter:'R',frequency:.05987}); Letters.insert({letter:'S',frequency:.06327}); Letters.insert({letter:'T',frequency:.09056}); Letters.insert({letter:'U',frequency:.02758}); Letters.insert({letter:'V',frequency:.00978}); Letters.insert({letter:'W',frequency:.02360}); Letters.insert({letter:'X',frequency:.00150}); Letters.insert({letter:'Y',frequency:.01974}); Letters.insert({letter:'Z',frequency:.00074}); } }); -
当我们打开
server.js文件时,让我们快速添加一个更新频率的辅助方法,稍后我们会用到。将以下代码添加到server.js文件中:Meteor.methods({ updateFrequency : function(letter,frequency){ Letters.update({letter:letter}, {$set:{frequency:frequency}}); } }); -
现在,我们将添加一些简单的脚手架和样式。我们的 HTML 非常简单,因为
D3.js正在执行繁重的工作。尽管如此,我们仍然希望将我们的svg元素放入一个 Meteor 模板中。导航到你的[项目根目录]/client/文件夹,创建一个名为client.html的文件,并添加以下代码:<body> {{> diagram}} </body> <template name="diagram"> <svg class="chart"></svg> </template>让我们添加一些简单的样式来调整字体和颜色等。创建一个名为
[项目根目录]/client/style.css的文件,并添加以下 CSS 声明:.bar { fill: steelblue; } .bar:hover { fill: brown; } .axis { font: 10px sans-serif; } .axis path, .axis line { fill: none; stroke: #000; shape-rendering: crispEdges; } .x.axis path { display: none; }我们现在准备好在
rendered/autorun中添加D3.js渲染逻辑。以下代码几乎逐行来自 Mike Bostock 的关于D3.js的出色入门教程。最简单的事情就是从该教程的现有代码库中复制粘贴到本地文件,然后进行修改。要做到这一点,请在浏览器中导航到bl.ocks.org/mbostock/3885304,并将<script>标签内的所有内容复制到结束的</script>标签。在您的[项目根]/client/文件夹中创建一个名为client.js的文件,并将代码粘贴到新文件中(大约 60 行代码)。我们现在将以以下四种方式修改该页面的代码:
-
将所有代码包裹在
Template.rendered回调中 -
在
autorun计算中包裹和扩展d3.selectAll渲染代码 -
将轴渲染代码移出
autorun -
将数据更改为指向我们的
Letters.find()MongoDB 查询
完成的
Template.diagram.rendered回调如下所示。它主要由D3.js示例代码组成,前四个修改已突出显示。如果更容易,您可以直接从下面复制粘贴,而不是自己进行修改。然而,至少浏览一下这些更改,看看代码是如何修改的。在您的client.js文件中,对现有代码进行以下修改:Template.diagram.rendered = function(){ //we wrap everything in the Template.rendered() callback, // so that we don't interfere with Blaze var margin = {top: 20, right: 20, bottom: 30, left: 40}, width = 960 - margin.left - margin.right, height = 500 - margin.top - margin.bottom; //We are going to set the domains for x an y immediately //(assuming the alphabet isn't going to change) x = d3.scale.ordinal() .domain('ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')) .rangeRoundBands([0, width], .1); //ALSO: note that we *removed* the 'var' declarations, //so that x and y are global / accessible y = d3.scale.linear() .domain([0,0.15]) .range([height, 0]); var xAxis = d3.svg.axis() .scale(x) .orient("bottom"); var yAxis = d3.svg.axis() .scale(y) .orient("left") .ticks(10, "%"); // We are moving the axes creation (and SVG init) // to be *outside* our autorun() var svg = d3.select("body").append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .call(xAxis); svg.append("g") .attr("class", "y axis") .call(yAxis) .append("text") .attr("transform", "rotate(-90)") .attr("y", 6) .attr("dy", ".71em") .style("text-anchor", "end") .text("Frequency"); //We move D3.js rendering inside Tracker.autorun() this.autorun(function(){ // Instead of reading data from a static file, // we access the Letters collection var data = Letters.find().fetch(); if (!data.length) return; // To use D3.js's built-in update tracking, // we need access to our d3.selectAll() object.. var bars = svg .selectAll(".bar") .data(data, function(d){return d._id;}); // On new (when initializing), we append and animate bars.enter() .append("rect") .attr("class", "bar") .attr("x", function(d) { return x(d.letter); }) .attr("width", x.rangeBand()) .attr("height",0) .attr("y", height) .transition() .attr("y", function(d) { return y(d.frequency); }) .attr("height", function(d) { return height - y(d.frequency); }); // On change, we just animate to the new position bars .transition() .duration(200) .ease("sin-out") .attr("y", function(d) { return y(d.frequency); }) .attr("height", function(d) { return height - y(d.frequency); }); }); }; -
-
我们现在可以创建和调用
randomize函数。我们的页面现在应该可以正确渲染了(请在http://localhost:3000上检查并亲自验证),但它没有移动或做任何其他事情。让我们通过为我们的字母更改随机频率值来改变这一点。在client.js的底部,autorun块之后,以及渲染的callback块的结束括号之前,添加以下定时器调用:});//<--- end of autorun() // everything's set! let's randomize 5 times / second... Meteor.setInterval(randomize,200); };//<-- end of rendered()最后,在文件的底部添加
randomize()函数,在所有其他内容之后:// Our randomize function randomize = function(){ // get a random position between 0-25 var ranLetter = ~~(Math.random()*26), // and a random frequency between 0-15% ranFreq = (Math.random()*0.15); // get the actual character ranLetter = x.domain()[ranLetter]; // update the frequency using a server call, // because it's easier than tracking down the _id Meteor.call('updateFrequency',ranLetter,ranFreq); };保存所有更改,您的条形图应该会快乐地跳跃,类似于以下截图:
![如何做到这一点…]()
它是如何工作的…
在整合 D3.js(以及由此扩展的任何其他渲染引擎)时,有两个关键规则需要记住:
-
将所有您的 DOM/SVG 操作逻辑放在
Template.rendered()内 -
使数据源具有反应性,并将渲染逻辑包裹在
Tracker.autorun()内
当我们将整个 D3.js 逻辑从教程中添加到 Template.diagram.rendered() 中时,我们遵循了规则 #1。这告诉 Blaze 不要干涉我们将要做的任何事情。我们实际上是在告诉 Blaze 去抽根烟休息一下,同时我们实现一些传统的 DOM/SVG 操作(以及动画!)。
然后,我们不是依赖于非响应式数据源,在那里我们不得不定期检查更新和/或每当结果更改时调用update()函数,而是通过在autorun函数中包装D3.js逻辑来使用 Meteor 的声明式、响应式编程风格。由于var data = Letters.find().fetch()是一个响应式数据源,这个autorun函数将在Letters集合有更改时重新运行。
换句话说,我们通过创建一个D3.js友好的响应式计算来遵循规则#2,而不是使用像流浪汉一样的功能(非响应式)数据模型。Mike Bostock 会为我们感到骄傲,因为避免使用函数式编程是D3.js的核心原则之一。
由于D3.js示例代码,我们似乎写了大量的代码,而且我们省略了一些细节,例如将y和x轴渲染逻辑移出autorun函数(因为我们不希望每次都重新渲染),但实际上真的很简单。注意在randomize()函数内,我们的更新语句是多么的简单(并且松散耦合):一行。
当涉及到第三方渲染器时,使用 rendered() 进行包装,并使用 autorun() 进行响应式处理。
小贴士
使用 rendered() 进行包装,使用 autorun() 进行响应式处理。
还有更多……
我们强烈建议您深入了解D3.js,正如之前提到的,它运行在相同的声明式编程原则之上,正如它与 Meteor 的集成如此干净一样,这确实有原因,对于 SVG 图表和动画来说,它确实是顶级的。
小贴士
D3.js的教程可以在github.com/mbostock/d3/wiki/Tutorials找到。
参考以下内容
-
在第三章的创建动态列表食谱中,构建出色的用户界面。
-
在第七章的使用 SVG 和 Ajax 创建动态图表食谱中,使用客户端方法,创建前沿的 UI。
使用 Polymer 创建尖端 UI。
Polymer (www.polymer-project.org/)是雄心勃勃的。它以最好的方式雄心勃勃,如果它成功,它将使我们的开发生活变得更好。
作为网络开发者,我们经历的最大浪费时间活动之一是调整 CSS,重新定位 DOM 元素,并尝试手动编写动画。并不是说这些事情不好(我们中的一些人非常喜欢设计良好的组件);而是几乎每次,当引入新的项目或设计主题时,我们的努力都会付诸东流。
通过使用在所有现代浏览器上(至少目标是这样的——雄心勃勃,对吧?)都能正常工作的可重用、可靠的组件,Polymer 旨在将设计从开发中抽象出来,让开发者专注于开发,设计师专注于设计。
这个食谱将向您展示如何在 Meteor 应用程序中使用 Polymer 组件。
准备工作
我们将把实际的步骤分解为两部分:配置 Meteor 和 Polymer 以及将 Polymer 元素添加到我们的应用中。因此,我们将把配置和设置放在 准备工作 步骤中,以使代码和代码的解释更容易理解。
创建你的应用和文件夹
我们将需要一些我们通常不使用的文件夹和子文件夹,所以让我们先整理好整个文件结构。打开终端窗口,导航到你想要创建根项目的地方,并执行以下命令:
$ meteor create polymersample
$ cd polymersample
$ rm polymersample.*
$ mkdir -p client/templates/init
$ mkdir {both,server}
$ mkdir -p public/components
创建你的文件
为了节省以后逐个创建文件的时间,我们现在就一次性创建它们。在同一个终端窗口中,在你的 [项目根目录] 文件夹中,执行以下命令:
$ touch .bowerrc
$ touch client/imports.html
$ touch client/main.html
$ touch client/styles.css
$ touch client/templates/tmpl-authors.html
$ touch client/templates/tmpl-authors.js
$ touch client/templates/tmpl-search.html
$ touch client/templates/tmpl-search.js
$ touch client/templates/init/head.html
$ touch both/model.js
$ touch server/server.js
配置 Bower 和安装 Polymer
Bower 是安装 Polymer 库最安全、最好的方式,因为它确保我们拥有最新和最好的版本。你需要确保已经安装了 Bower。如果没有,在你的终端窗口中运行以下命令:
$ npm install -g bower
小贴士
你可能需要以 sudo 运行 npm,因为你正在全局安装。如果是这样,请运行以下命令:
sudo npm install -g bower.
安装 Bower 后,我们想要告诉 Bower 我们希望将 Polymer 组件安装在哪里。在 Meteor 中,那就是在 public/components 文件夹中。在文本编辑器中打开 [项目根目录]/.bowerrc 文件,添加以下行并保存更改:
{"directory":"public/components/"}
我们现在已准备好使用 Bower 初始化我们的项目。在终端窗口中,在你的 [项目根目录]/ 目录中,输入以下命令:
$ bower init
Bower 即将问你很多问题!好消息是大多数问题的答案是 回车键,或者 空格键然后回车键。更好的消息是,大多数答案并不真正重要。但是,你需要完成这个过程,这样 Bower 才会为你生成 bower.json 文件,该文件将跟踪我们安装了哪些 Polymer 库(关于这一点稍后详细说明),所以回答问题时可以参考以下示例:
? name: Polymer Sample
? version: 0.0.1
? description:
? main file:
? what types of modules does this package expose?:
? keywords:
? authors:
? license: none
? homepage:
? set currently installed components as dependencies?: No
? add commonly ignored files to ignore list?: Yes
? would you like to mark this package as private which prevents it from being accidentally published to the registry?: Yes
然后,你将看到你的 bower.json 文件的预览,如下面的截图所示:

假设 private:true 和 ignore:[...] 已经设置好,输入 Y 并按 Enter。你的 Bower 配置现在已完成!
我们现在已准备好安装 Polymer 库。在同一个终端窗口中,依次输入以下命令,完成一个后再进行下一个:
$ bower install polymer --save
$ bower install polymer/core-elements --save
$ bower install polymer/paper-elements --save
$ bower install polymer-github-card --save
在最后的安装中,Bower 将询问 polymer 和 core-components 的哪个版本将支持 polymer-github-card。选择其他组件所依赖的版本(截至本文撰写时,这是 [library]#⁰.5.0)。
添加辅助 Meteor 包
我们需要添加三个第三方包。其中之一(voodoohop:masonrify)将帮助我们布局,另外两个(meteorhacks:inject-initial和differential:vulcanize)将允许以最小的痛苦将 Polymer 元素添加到我们的 Meteor 项目中(我们怀疑这将在未来变得更好,但现在这是一个解决方案;而且说实话,这并不真的很糟糕)。在终端窗口中,在您的[项目根目录]/文件夹中,输入以下命令:
$ meteor add voodoohop:masonrify
$ meteor add meteorhacks:inject-initial
$ meteor add differential:vulcanize
配置 Meteor
设置几乎完成!Meteor 通常与其他框架相处得很好,但由于 Polymer 有一些预渲染 DOM 操作(这真的很酷,但会干扰 Meteor 的魔力...)我们必须能够向<body>标签注入一个属性,我们还需要确保基本polymer和webcomponents文件被正确引用。
在编辑器中打开[项目根目录]/server/server.js文件,并添加以下 HTML 注入代码:
Meteor.startup(function(){
Inject.rawModHtml('addUnresolved',function(html){
return html = html.replace('<body>', '<body unresolved>');
})
});
现在我们将处理基本文件引用。打开[项目根目录]/client/templates/init/head.html文件,并添加以下代码:
<head>
<script src="img/webcomponents.js"></script>
<title>github authors</title>
</head>
最后一步(这是为了让vulcanize能够合并所有组件引用),打开[项目根目录]/client/imports.html文件,并添加以下行:
<!-- Components -->
<link rel="import" href="/components/polymer/polymer.html">
<!-- Styles -->
<link rel="import" href="/components/font-roboto/roboto.html">
设置已完成!让我们启动 Meteor(在终端中运行meteor命令),打开浏览器到http://localhost:3000,并开始使用 Polymer 组件!
如何做到这一点...
我们即将要做的事情有两个注意事项:首先,目前 Polymer 与 Chrome 浏览器配合得最好。下面的示例在其他浏览器中也能工作,但有一些特性,如click或keypress事件,可能会引起一些挫败感。其次,由于时间问题,当您修改代码时,可能需要手动刷新浏览器(这在使用 Meteor 时是非常罕见的)。
请记住,Polymer 还远未达到1.0版本——您使用这个配方正处于前沿——所以您需要耐心等待,因为我们将在 Meteor 中使用 Polymer 的基础知识。优化所有其他浏览器的时间、事件和其他行为本身就是一本书的内容,所以让我们专注于基础部分,随着 Meteor 和 Polymer 的发展,这些问题将自行解决。
-
我们首先添加我们的 CSS 样式。Polymer 在这里做了大部分工作,所以让我们先处理我们必须要贡献的少量 CSS。打开
[项目根目录]/client/styles.css文件,并添加以下 CSS 声明:html,body { height: 100%; margin: 0; font-family: 'RobotoDraft', sans-serif; } .container { width: 80%; margin: 50px auto; } div.sText { display:inline-block; width: 20rem; } div.sBtn { display: inline-block; width: 5rem; vertical-align: text-top; } paper-button.colored { color: white); } paper-button[raised].colored { background: rgb(66, 133, 244); color: white; } -
让我们看看
polymer-github-card组件,使用静态表示,只是为了看看我们在玩什么。打开[项目根目录]/client/imports.html文件,并在Components部分的"…polymer.html"条目下方添加以下行:<link rel="import" href="/components/polymer-github-card/dist/polymer-github-card.html">现在打开与文件夹相同的
main.html文件,并添加以下代码:<body> <div class="container"> <polymer-github-card user="meteor"></polymer-github-card> </div> </body>在你的浏览器中,在
vulcanize重新编译你的<header>之后,你应该会看到以下截图类似的内容:![如何做…]()
-
既然我们已经确认 Polymer 运行正常,让我们将
polymer-github-card组件放入 Meteor 模板中,并创建基于 Mongo 集合添加多个卡片的能力。打开[项目根目录]/both/model.js并添加以下行:Authors = new Mongo.Collection('authors'); -
现在让我们创建我们的模板。你会记得我们正在使用
voodoohop:masonrify来控制布局,因此我们需要将模板声明包裹在适当的{{#masonry…}}块中。打开[项目根目录]/client/templates/tmpl-authors.html并添加以下代码:<template name="authors"> {{#masonryContainer columnWidth=265 gutter=5 transitionDuration="0.2s" id="MasonryContainer" }} {{#each authors}} {{> authorCard}} {{/each}} {{/masonryContainer}} </template> <template name="authorCard"> {{#masonryElement "masonryContainer"}} <div> <polymer-github-card user="{{userid}}"> </polymer-github-card> </div> {{/masonryElement}} </template>再次强调,前面的代码的大部分与布局有关。实际调用动态填充
polymer-github-card组件的代码位于authorCard模板中,并引用了user属性中的{{userid}}。userid是免费的,因为它是Authors集合中每条记录的属性。 -
现在让我们创建
Template.helper函数,通过Collection.find()函数传递Authors集合。打开[项目根目录]/client/templates/tmpl-authors.js并添加以下代码:Template.authors.helpers({ authors: function(){ return Authors.find().fetch(); } }); -
当我们做这件事的时候,让我们使删除记录变得非常简单。在
tmpl-authors.js中,在底部添加以下事件处理程序:Template.authors.events({ 'dblclick polymer-github-card': function(e){ Authors.remove({_id:this._id}); } }); -
我们现在需要修改我们的主模板以引用我们新的动态模板。打开
main.html并修改内容,使其看起来如下:<body> <div class="container"> {{> authors}} </div> </body> -
保存所有更改,查看你的浏览器(给
vulcanize一点时间来完成其任务),你应该会看到一个完全空白的页面。哇!这仅仅是因为我们的Authors集合是空的。让我们来修复这个问题。在你的浏览器控制台中,执行以下命令:> Authors.insert({userid:'meteor'}) > Authors.insert({userid:'glasser'})当你输入每个命令时,一个新的卡片会出现在适当的位置,带有 Meteor 标志和 David Glasser 微笑快乐的童年照片,如下截图所示:
![如何做…]()
命令行已经过时了!让我们更新我们的页面,使用一些纸张元素组件,并给我们一个简单的方法来添加更多的 GitHub 作者。
-
打开
[项目根目录]/imports.html并为我们将要使用的组件添加链接(顺便说一下,这些组件位于我们的public/components/文件夹中…)在"…polymer.html"声明下方。完成后,你的文件应该看起来如下:<!-- Components --> <link rel="import" href="/components/polymer/polymer.html"> <link rel="import" href="/components/paper-ripple/paper-ripple.html"> <link rel="import" href="/components/paper-shadow/paper-shadow.html"> <link rel="import" href="/components/paper-input/paper-input.html"> <link rel="import" href="/components/paper-button/paper-button.html"> <link rel="import" href="/components/polymer-github-card/dist/polymer-github- card.html"> <!-- Styles --> <link rel="import" href="/components/font-roboto/roboto.html">让我们创建我们的
search模板,它将包含一个paper-input组件和一个paper-button组件。打开[项目根目录]/client/templates/tmpl-search.html并添加以下代码:<template name="search"> <div class="sText"> <paper-input-decorator id="searchText" floatingLabel label="find an author"> <input is="core-input" id="sInput"> </paper-input-decorator> </div> <div class="sBtn"> <paper-button raised class="colored" role="button" tabindex="0"> search </paper-button> </div> </template>在 Meteor 方面,前面的代码没有什么特别之处。它只是直接的 Polymer 格式化和配置。
小贴士
要了解更多关于 Polymer 组件的信息,请访问:
-
我们希望在执行搜索时添加新卡片,也就是说,在我们
paper-input组件内部按下Enter键或点击我们的paper-button组件时。打开位于同一client/templates/文件夹中的tmpl-search.js,并添加以下代码:Template.search.events({ 'keypress #sInput' : function(e){ if (e.keyCode!=13) return; addAuthor(); }, 'click paper-button': function(e){ addAuthor(); } }); function addAuthor(){ var sInput = $('#sInput'), sVal = sInput.val(); sInput.blur(); if (!sVal || (Authors.findOne({userid:sVal}))) return; sInput.val(''); Authors.insert({userid:sVal}); } -
最后一步是为我们的
search模板在main.html中添加一个模板包含。打开[项目根]/client/main.html并添加以下高亮代码:<body> <div class="container"> {{> search}} </div> <div class="container"> {{> authors}} </div> </body>完成了!在浏览器中查看您的应用程序,允许刷新/重新编译,您应该能够添加您想要的任何 GitHub 作者卡片。添加一些作者(建议:
meteorhacks,arunoda,d3,mbostock,voodoohop,pazguille,polymer,addyosmani),在每次输入后都会出现新卡片,如下面的截图所示:![如何操作…]()
它是如何工作的…
我们很少需要参考“准备就绪”部分来完全理解正在发生的事情,但关于在哪里以及如何放置 Polymer 文件的环境很重要,所以让我们从这里开始。我们将 Bower 指向将所有 Polymer 文件放入属于[项目根]/public/的子文件夹。Meteor 对待public文件夹中的文件与其他文件夹不同——它将它们视为静态文件,并且不会处理其中找到的.js或.html文件。
这对我们有利,因为目前 Polymer 和 Meteor 渲染器之间存在一些相当棘手的冲突。具体来说,两者都使用{{double-stache}}符号和<template>标签,但它们的解释不同。为了解决这些冲突(诺贝尔和平奖即将到来!...)我们“隔离”每个框架,允许 Polymer 链接到public文件夹中的任何内容,并让 Meteor 忽略同一位置的所有内容。这几乎就像Meteor 开发组(MDG)有意这样设计的一样!当然,他们是这样的,预计并鼓励使用其他有偏见的框架。MDG 确实希望 Meteor 与其他人良好地协作,在这种情况下,它允许我们在应用程序中使用 Polymer。
还需要进行一些其他调整,例如在<head>块中插入链接元素,并将未解析的属性注入到<body>元素中,但这些只是小问题,只需一次性解决,一旦完成,我们就可以随心所欲地使用 Polymer 组件。
至于 Polymer 组件的使用,我们既可以直接使用它们(如步骤#2 所示),无需 Meteor 的参与,也可以将它们整合到我们的{{#each…}}模板或其他模板中。这样我们就有了访问正常的 Meteor Template.helpers和Template.events的权限,这为我们提供了对反应性数据的简单、声明式访问。
具体来说,如果我们查看tmpl-authors.html文件,我们会看到我们正在使用{{#each authors}}和{{userid}}模板助手,动态地迭代和渲染polymer-github-card组件:
<template name="authors">
...
{{#each authors}}
{{> authorCard}}
{{/each}}
...
</template>
<template name="authorCard">
...
<polymer-github-card user="{{userid}}">
</polymer-github-card>
...
</template>
在tmpl-authors.js和tmpl-search.js中,我们使用Template.events来解析keypressed、click和dblclick事件,并能够使用上下文(内联)数据来修改记录,例如当我们使用Authors.remove({_id:this._id})语句删除记录时:
Template.authors.events({
'dblclick polymer-github-card': function(e){
Authors.remove({_id:this._id});
}
});
更多内容...
Polymer 和 Meteor 之间存在大量交互和兼容性/性能的细微差别,这些内容超出了本食谱(以及本书)的范围。我们建议浏览 Stack Overflow (stackoverflow.com/questions/tagged/meteor)、气象论坛(forums.meteor.com/),或者直接打开 hood 进行实验,以获得更多关于正确集成 Polymer 和 Meteor 的经验。
参见
-
在第二章的添加气象包食谱,使用包进行自定义
-
在第三章的创建自定义组件食谱中,构建出色的用户界面
-
在第四章的实现简单集合食谱中,创建模型
第九章。保护你的应用程序
在本章中,你将学习以下主题:
-
基本安全 - 关闭
autopublish -
基本安全 - 移除
insecure -
使用
allow和deny保护数据交易 -
使用外观隐藏数据
-
使用
browser-policy保护客户端
简介
Meteor 使开发和原型设计尽可能快速和简单。为了实现这一点,有一些默认包被安装,但在生产应用程序中并不需要。当你准备你的应用程序用于生产时,你将想要移除那些使原型设计更容易的包,并用一些安全最佳实践来替换它们,以使你的应用程序更加安全。在本章中,我们将讨论为生产准备应用程序所需的基线安全机制。
基本安全 - 关闭 autopublish
快速轻松地访问你的数据在原型设计时可以节省你大量的时间!默认情况下安装在每个新创建的 Meteor 应用程序中的 autopublish 包,使你能够快速管理和访问你的数据集合,以便你能够快速编写出优秀的代码。然而,当需要的时候,向每个数据集合中的每个字段广播是不高效且不安全的。这个配方将向你展示移除 autopublish 包的基本方法,并实现你自己的 publish/subscribe 代码以保持应用程序按预期工作。
准备工作
我们将创建一个非常基础的应用程序,在屏幕上显示简单的文本卡片,然后展示 autopublish 和 subscribe/publish 对这些卡片的影响。为此,我们需要创建我们的文件夹结构,添加一些基本模板,并添加一些样式。
项目设置
在终端窗口中,通过输入以下命令创建你的根项目:
$ meteor create secure-autopublish
$ cd secure-autopublish
$ rm secure-autopublish.*
$ mkdir {client,server,both}
$ meteor
创建基本模板
在文本编辑器中,在你的 [项目根目录]/both/ 文件夹中创建一个名为 collections.js 的文件,并添加以下行:
Cards_open = new Mongo.Collection('open');
接下来,创建一个名为 [项目根目录]/client/main.html 的文件,并添加以下 <template> 和 <body> 声明:
<body>
<div class="container">
{{> open}}
</div>
</body>
<template name="open">
<h3 id="new-open">open:</h3>
{{#each opens}}
<div class="card {{shared}}">
<div class="label id">id</div>
<div class="id">{{_id}}</div>
<div class="label text">text</div>
<div class="text">{{text}}</div>
<div class="label owner">owner</div>
<div class="owner">{{owner}}</div>
</div>
{{/each}}
</template>
我们只需要添加一点逻辑来显示和创建,然后我们就可以继续到样式部分。创建一个名为 [项目根目录]/client/templatehelpers.js 的新文件,并添加以下 Template.helpers 和 Template.events 函数:
Template.open.helpers({
opens: function(){
return Cards_open.find({},{sort:{text:1}}).fetch();
},
shared: function(){
return (this.shared? 'shared':null);
}
});
Template.open.events({
'dblclick #new-open' : function(e){
e.preventDefault();
var txt = 'open card# ' + Cards_open.find({}).count();
Cards_open.insert({text:txt});
},
'click .text' : function(e){
e.preventDefault();
var shrd = (!this.shared);
Cards_open.update({_id:this._id},{$set:{shared:shrd}});
},
'dblclick .id' : function(e){
e.preventDefault();
Cards_open.remove({_id:this._id});
}
});
添加 CSS 样式
我们只需要一点 CSS 来使事物更具视觉吸引力。创建一个名为 [项目根目录]/client/styles.css 的文件,并添加以下 CSS:
body {
font-family: 'helvetica neue';
}
.card {
display: inline-block;
min-width:10rem;
height: 10rem;
border: 2px dashed #ccc;
border-radius: 0.21rem;
margin: 0.25rem 0.25rem;
padding: 0.5rem;
vertical-align: top;
}
.container {
width:90%;
margin: auto;
}
.shared {
background-color: rgba(25, 121, 36, 0.36);
}
.label {
font-weight: bold;
margin: 0.2rem 0;
padding: 0.1rem;
padding-left: 0.3rem;
}
.label:hover {
background-color: rgba(7, 180, 21, 0.76);
border-radius: 0.2rem;
}
你的应用程序现在应该已经启动并运行。在浏览器中导航到 http://localhost:3000,然后双击 open: 标签来创建一些新的卡片。点击卡片中的 text 标签来修改共享属性(卡片将变为绿色),并双击 id 标签来删除卡片。经过一番操作后,你的屏幕将类似于以下截图:

如果一切正常,我们就准备好移除 autopublish 包。
如何操作...
按照以下步骤关闭 autopublish:
-
在一个新的终端窗口(保持 Meteor 运行!)中,导航到你的
[project root]并输入以下命令:$ meteor remove autopublish你的网页屏幕现在将显示无结果,如果你对
Cards_open集合进行计数,即使你创建了多个记录,计数也会返回 0:![如何操作...]()
-
我们现在将添加
publish和subscribe。Cards_open集合仍然存在。然而,由于我们移除了autopublish包,客户端和服务器之间的通信已经被切断。为了恢复它,我们需要在服务器上添加一个publish方法,并在客户端添加一个subscribe方法。创建一个名为[project root]/server/collections-server.js的文件,并添加以下publish函数调用:Meteor.publish('open',function(){ return Cards_open.find({}); }); -
现在,创建一个名为
[project root]/client/collections-client.js的文件,并添加以下subscribe()函数调用:Meteor.subscribe('open');
完成了!你已经成功移除了 autopublish 包,并重新创建了允许客户端仍然看到 Cards_open 集合所需的 publish/subscribe 调用。现在,当你使用之前提到的点击和双击创建、修改和删除时,你的浏览器应该能够正确显示结果。
它是如何工作的...
简而言之,autopublish 会检查存在哪些集合,并自动为你编写 publish 和 subscribe 函数调用。它为它找到的每个集合都这样做,因此既不高效也不安全。
通过移除 autopublish,我们停止了 publish 和 subscribe 函数自动调用。因此,我们必须重新创建这些调用,在服务器上创建一个简单的 publish() 调用(在 'open' 通道上),在客户端上创建一个 subscribe() 调用(在相同的 'open' 通道上)。
在 publish 函数中的 find() 语句检索所有内容,这本身既不安全也不高效,但我们将在其他菜谱中修复这个问题。本菜谱的重点是如何移除 autopublish 包,而不影响我们应用程序的功能。
参见
- 在 第二章 的 使用包进行自定义 部分的 移除 Meteor 包 菜谱中
基本安全 - 移除不安全
在移除 autopublish 后,我们将想要控制数据的添加、删除和更新方式,并采取适当的措施来确保安全。为了启用这种级别的控制,我们需要移除名为 insecure 的包。在移除 insecure 包后恢复功能,我们需要使用基本的 collection.allow 声明。这个菜谱将向你展示如何做到这一点。
准备工作
我们将使用本章中找到的基本安全 - 关闭自动发布配方作为我们的基准。一旦你完成了这个配方,复制secure-autopublish文件夹(注意:你需要所有子文件夹,包括隐藏的.meteor文件夹),将其重命名为secure-rm-insecure,在终端中使用meteor命令启动你的应用程序,你就可以继续进行了。
如何做...
就像之前的autopublish配方一样,我们只需要移除insecure包,然后恢复功能。
-
在一个新的终端窗口(保持 Meteor 运行!),导航到你的项目根目录,并输入以下命令:
$ meteor remove insecure你的应用程序现在禁止对
Cards_open集合进行任何客户端更改。尝试添加一张新卡片、分享一张卡片或删除一张卡片,你将无法做到。无论是通过 UI 使用点击和双击,还是通过通过 Web 控制台编程方式,你将无法进行任何更改,如下面的截图所示:![如何做...]()
-
好的,现在我们需要恢复我们的超级能力!打开
[项目根目录]/server/collections-server.js文件,并将以下代码添加到文件底部:Cards_open.allow({ insert : function(userId,doc){ return true; }, update : function(userId,doc,fieldNames,modifier){ return true; }, remove : function(userId,doc){ return true; } });保存这些更改后,我们的
insert、update和remove功能已恢复。你现在可以添加、修改和删除你想要的任何数量的卡片,无论是通过 UI 还是通过通过 Web 控制台编程方式。
它是如何工作的...
insecure包几乎与autopublish对发布安全所做的一样,对数据集合安全进行操作 - 它找到它能够找到的所有集合,并为所有函数(insert、update和remove)自动创建一个collection.allow函数。通过移除insecure包,我们阻止了Cards_open集合允许任何客户端更改。
为了解决这个问题,并为更细粒度的安全做准备(本章后面的配方中提供详细信息),我们调用了Cards_open.allow(),并通过对每个检查函数返回true来启用所有集合修改。
因此,尽管我们应用程序的网络安全性没有改变,我们现在准备修改我们的发布和安全设置,使我们的应用程序准备好生产使用。
参见
- 在第二章的使用包进行自定义中,移除 Meteor 包配方
使用 allow 和 deny 保护数据交易
正确配置的 Meteor 集合非常安全。我们对允许和不允许的内容的细粒度控制使我们能够适当地保护我们的应用程序。在本配方中,你将学习如何使用allow和deny来保护你的集合和控制访问。
准备工作
使用本章中找到的基本安全 - 移除不安全配方,我们已经移除了autopublish和insecure包的应用程序。一旦我们添加并配置适当的用户accounts包,我们就可以继续进行了。
以基本安全 - 移除不安全配方的一个副本作为基准,打开一个终端窗口,导航到你的项目根目录,并执行以下命令:
$ meteor add accounts-ui
$ meteor add accounts-password
如果你的应用程序还没有运行,请确保使用meteor命令启动它。
我们现在需要添加loginButtons模板,并修改我们的insert语句,为每条记录添加owner属性。
打开你的[项目根]/client/main.html文件,并在<body>标签下方添加loginButtons模板包含,如下面的示例所示:
<body>
{{> loginButtons}}
<div class="container">
...
接下来,打开你的[项目根]/client/templatehelpers.js文件,并修改Template.open.events插入逻辑以添加owner,并且只有当有登录用户时才触发。你的代码更改应如下所示:
Template.open.events({
'dblclick #new-open' : function(e){
e.preventDefault();
if (!Meteor.userId()) return;
var txt = 'open card# ' + Cards_open.find({}).count();
Cards_open.insert({text:txt , owner: Meteor.userId()});
},
最后,在你的浏览器中,创建一个新用户,并确保你以该用户身份登录(用户名不重要 - 我们建议使用一个假的用户名,例如user1@test.com)。
现在,每次你创建新卡片时,所有者部分将自动填充为登录用户的唯一 ID,如下面的截图所示:

如何操作...
当前应用程序的状态不安全。任何人都可以插入、删除和更新任何卡片,即使它们属于另一个用户!我们将通过使用collection.allow()和collection.deny()声明来解决这个问题。
-
首先,我们将需要登录用户进行
insert。打开你的[项目根]/server/collections-server.js文件,找到Cards_open.allow()函数调用,并对insert函数声明进行以下修改:Cards_open.allow({ insert : function(userId,doc){ return(userId!=null); },你现在将无法在注销状态下创建新卡片(如果你愿意,可以测试一下)。
-
接下来,我们只允许记录的所有者
更新或删除卡片。在相同的collections-server.js文件中,修改update和remove函数声明如下:update : function(userId,doc,fieldNames,modifier){ return (doc.owner==userId); }, remove : function(userId,doc){ return (doc.owner==userId); } -
保存你的更改,并通过以新用户身份登录(如果需要,请创建一个),尝试更改结果,尝试在不登录的情况下添加新卡片等来测试你的新规则。有了这些规则,只有登录用户才能创建新卡片,只有卡片的所有者才能修改卡片或删除它。
它是如何工作的...
所有客户端尝试以任何方式更改集合的操作都通过两个回调函数:allow和deny。为了使集合更改被服务器接受,传入的更改必须从allow函数之一收到一个true响应(例如,如果userId!=null,我们在insert函数中返回true),并且必须从任何deny函数中收到零个true响应。
在这个特定的情况下,我们在allow回调中对insert进行简单的检查,以确保用户已登录,这相当于userId!=null。对于update和remove,检查是查看登录用户是否是卡片的拥有者/创建者,通过return(doc.owner==userId)。
你可以声明任意数量的allow或deny回调,尽管通常最好在可能的情况下将它们合并,使用“悲观”的安全模型(只允许需要的,而不是允许所有,只拒绝需要的)。
还有更多…
之前的allow规则在仅使用 UI 的情况下工作得很好。然而,有人可能会打开控制台窗口并直接进行数据操作调用,这可能会引起一些问题。
首先,我们对insert的检查只是userId!=null。任何额外的字段,甚至是恶意的insert,都可以通过命令行添加,例如,假设我拥有另一个用户的userId(这并不难得到,它在每张卡的owner字段中都可以找到)。我可以轻松地插入带有恶意的文本卡片,或者更新现有卡片的文本和所有者,使其看起来是另一个用户创建了笔记。
例如,如果我以user2@test.com登录,并且我知道user1@test.com的userId值是'8v2GGh98RrYfso92c',我可以在浏览器控制台中运行以下命令,并可能让user1陷入麻烦:
> Cards_open.insert({text:'NSFW !@##%!!!',owner:'8v2GGh98RrYfso92c'})
我们可以通过几种方式来处理这个问题。我们要么使我们的allow回调函数更复杂,使用多个if…else语句,要么使用deny回调来禁止某些行为。在[项目根目录]/server/collections-server.js中,创建一个新的deny回调,代码如下:
Cards_open.deny({
insert : function(userId,doc){
return (doc.owner!=userId);
},
update : function(userId,doc,fieldNames,modifier){
return (fieldNames.length!=1 || !(~fieldNames.indexOf('shared')));
}
});
对于insert,如果doc.owner!=userId,deny回调将返回true。对于update,如果尝试修改除共享字段外的任何字段,deny回调也将返回true。使用这两个回调函数,我们进一步增强了安全性,并消除了任何控制台操作的不当行为。
参见
要了解allow和deny可以做什么,请参阅官方的 Meteor 文档,可在docs.meteor.com/#/full/allow找到。
使用外观隐藏数据
我们的一些安全(和性能)问题可以通过限制对数据集中某些字段和记录的访问来解决,例如,如果记录的owner字段没有发送到客户端,潜在的攻击者将永远无法获取另一个用户的userId值。同样,如果只将属于特定userId的记录或标记为共享的记录传递给客户端,私人记录可以保持私密,并且只对创建它们的用户可见。这个方法将向您展示如何创建外观来限制发送到客户端的字段和记录。
准备工作
请完成本章中找到的 使用 allow 和 deny 保护数据事务 菜谱,包括在 更多内容… 部分中找到的附加 deny 回调函数。完成这些后,并且您的 Meteor 应用程序正在运行,您就可以使用这个菜谱了。
如何操作...
我们将修改服务器上的 publish 函数,使其只返回属于或与登录用户共享的记录,并且我们将停止广播 owner 字段。
-
打开
[项目根目录]/server/collections-server.js文件,定位到Cards_open.publish部分,并对Cards_open.find()方法进行以下修改:Meteor.publish('open',function(){ return Cards_open.find({$or: [ {shared:true}, {owner:this.userId} ] }, {fields:{owner:0}}); }); -
现在,由于
owner字段不再在客户端可见,我们可以从[项目根目录]/client/main.html文件中的公开模板中删除以下两行:<div class="label owner">owner</div> <div class="owner">{{owner}}</div> -
保存这些更改后,任何登录用户只能看到由该用户创建的卡片,或者共享的卡片。如果你在两个不同的浏览器中登录,使用两个不同的用户,你将能够看到共享如何使记录对另一个用户可见,反之亦然。以下截图显示了两个用户共享一些记录而未共享其他记录的示例:
![如何操作...]()
它是如何工作的...
通过修改选择器和 fields 选项,我们能够限制发布给客户端的记录集。客户端尽管尝试,但永远无法看到选择器排除的任何记录,也无法看到任何排除的 fields,因为服务器在发布时根本不会发送它们。
具体来说,我们使用了一个 {$or: [...]} 选择器来限制哪些记录被发布,只包括由当前用户创建的记录(owner:this.userId),或者已共享的记录(shared:true)。我们使用了 {fields:{owner:0}} 选项,以返回所有 fields 除了 owner。这种 黑名单 方法比 白名单 方法安全性较低,但为了使这个菜谱更简单,我们决定告诉查询要排除哪些字段(乐观)而不是包含哪些字段(悲观)。
要白名单而不是黑名单,列出您希望显示的字段,并传递一个值为 1 的值(例如:{text:1 , _id:1 , shared:1})。所有未指定的字段将自动不随查询返回。
参见
- 在第四章 创建模型 的 使用 MongoDB 查询进行过滤 菜谱中,创建模型
使用浏览器策略保护客户端
在 Meteor 中保护数据库相当直接,但客户端安全怎么办?Meteor 同样为您提供了保障,使用标准的 Content-Security-Policy 和 X-Frame-Options 安全措施。本菜谱将指导您添加 browser-policy 包,并配置基本的客户端安全。
准备工作
我们将像往常一样创建一个新的项目,但我们将保留默认文件,在创建过程中添加一些不安全的脚本功能。
框架设置
在终端窗口中,导航到你的项目根目录,并执行以下命令:
$ meteor create secure-client
$ cd secure-client
$ mkdir {client,server,both}
$ mv secure-client.* client/
$ meteor
添加 CDN 托管的 Bootstrap
访问官方 Bootstrap 的 入门 页面,位于 getbootstrap.com/getting-started/,并滚动到标记为 Bootstrap CDN 的部分。复制该部分的内容,并将其插入到 [project root]/client/secure-client.html 文件的 <head> 块中。完成后,你的更改应类似于以下代码:
<head>
<title>secure-client</title>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css">
<!-- Latest compiled and minified JavaScript -->
<script src="img/bootstrap.min.js"></script>
</head>
添加内联和 eval() 脚本
当我们打开 secure-client.html 时,让我们修改模板,添加一些指示器,一个 href 属性中的内联脚本,以及一个用于显示 eval() 结果的新部分。修改你的 hello 模板,使其看起来如下代码:
<template name="hello">
<button>Click Me</button>
<p>You've pressed the button
<div class="badge">{{counter}}</div>
times.</p>
<p>Which is 1/2 of our eval value: {{dblCounter}}</p>
<a href="javascript:alert('hax0rz!'); Meteor.call('dropTable');">
<div class="btn btn-info">Bootstrap!</div>
</a>
</template>
我们需要添加一些逻辑,以确保模板能够正确显示。首先,我们将创建一个名为 Test 的简单集合。创建一个名为 [project root]/both/model.js 的文件,并添加以下行:
Test = new Mongo.Collection('test');
现在,配置服务器方法 dropTable 以模拟有人删除数据库。创建一个名为 [project root]/server/methods.js 的文件,并添加以下代码:
Meteor.methods({
dropTable: function(){
Test.remove({});
}
});
接下来,我们需要修改 hello 模板助手和事件,使其对我们的巧妙黑客攻击“易受攻击”!打开 [project root]/client/secure-client.js,将 Template.hello.helpers 部分修改为以下内容:
Template.hello.helpers({
counter: function () {
try {
var x = Test.find().count();
Session.set('counter', eval("x*2"));
} catch (err) {
console.log('ERROR: ', err);
}
return x;
},
dblCounter: function () {
return Session.get('counter');
}
});
最后,修改 Template.hello.events 以向 Test 集合添加记录,而不是更新 counter 变量。你的代码应类似于以下内容:
Template.hello.events({
'click button': function () {
// increment the counter when button is clicked
Test.insert({action:'click'});
}
});
保存所有这些更改后,我们的应用程序已经彻底“被黑”,使用了 eval() 来加倍正常点击计数器,一个内联脚本将删除我们的 Test 集合中的所有记录,并且使用了来自备用站点(Bootstrap CDN)的脚本和样式。
导航到 http://localhost:3000/ 并稍微玩一下按钮。点击几次后,你的屏幕将类似于以下截图:

要激活内联“黑客”,点击标记为 Bootstrap! 的按钮——会出现一个通知,告诉你你已经被黑了,点击 OK 后,Test 集合将被清空。通知看起来可能如下截图:

你现在可以关闭所有这些高级黑客技术了!
如何做到这一点...
为了保护我们的应用程序,我们将添加 browser-policy 包,然后根据我们的环境适当配置它。
-
在一个新的终端窗口中,导航到你的项目根目录(保持你的应用程序运行!)并执行以下命令:
$ meteor add browser-policy您的应用程序现在将失去所有的 Bootstrap 格式化,以及那个将您的计数器翻倍和 eval()函数,它也将不再工作。当您点击点击我按钮时,您的计数器会增加,但双倍计数器不会。您的屏幕应该类似于以下截图,其中在 Web 控制台中有很多错误解释说之前的不安全“黑客”行为现在不再被允许:
![如何操作...]()
-
我们现在将使用
BrowserPolicy.content微调我们的安全设置。内联脚本漏洞仍然存在(如果您想测试它,请点击Bootstrap!),而且我们之前认为不是黑客行为的那种格式化也不再起作用。因此,我们在一个领域(内联脚本)上不够严格,而在另一个领域(拒绝从 Bootstrap CDN 获取所有内容,这是一个可信来源)上过于严格。让我们来纠正这个问题。在[项目根目录]/server/文件夹中创建一个名为policy.js的新文件。添加以下两行,并保存您的更改:BrowserPolicy.content.allowStyleOrigin( 'https://maxcdn.bootstrapcdn.com/'); BrowserPolicy.content.allowScriptOrigin( 'https://maxcdn.bootstrapcdn.com/'); -
我们已经恢复了 Bootstrap 格式化!现在,让我们禁止内联脚本,以及防止连接到任何服务器。将以下两行添加到
policy.js文件中并保存您的更改:BrowserPolicy.content.disallowInlineScripts(); BrowserPolicy.content.disallowConnect();那个正在删除我们的
Test集合的内联脚本现在将不再运行。然而,通过禁止所有连接,我们意外地破坏了与我们的服务器DDP连接。我们需要通过将我们的//:localhost:3000地址列入白名单来修复这个问题,对于 HTTP 和 websockets(对于DDP)。 -
将以下三行添加到
policy.js文件的末尾并保存您的更改:var rootUrl = __meteor_runtime_config__.ROOT_URL; BrowserPolicy.content.allowConnectOrigin(rootUrl); BrowserPolicy.content.allowConnectOrigin(rootUrl.replace('http', 'ws'));小贴士
由于我们正在处理您浏览器的安全策略,每次您对
policy.js进行更改时都需要手动刷新。最后,刷新您的浏览器一次,现在所有应该允许的内容都在工作,所有不应该的内容(内联脚本、
eval()等)都被禁止。经过几次点击后,您的屏幕应该看起来像以下截图:![如何操作...]()
它是如何工作的...
默认情况下,安装browser-policy包将禁用eval()脚本,并且只允许来自您站点的资源(图像等)。这就是为什么仅仅添加browser-policy就禁用了双倍计数器eval()脚本,并移除了所有 Bootstrap 文件。
为了允许 Bootstrap 资源,我们使用两个BrowserPolicy.content函数——allowStyleOrigin()和.allowScriptOrigin()——将 Bootstrap CDN 的样式和脚本列入白名单。
接下来,我们使用disallowInlineScripts()函数关闭内联脚本。我们还使用disallowConnect()函数防止任何 AJAX/远程服务器调用。
这样严格的设置也破坏了我们服务器的DDP连接,我们使用allowConnectOrigin()函数将其恢复,适用于 HTTP 和ws。
参见
我们有许多其他选项可供选择(这只是一个入门指南),所以如果您真的想精细调整您的安全设置,我们推荐 Arunoda Susiripala 撰写的一篇极好的文章,位于meteorhacks.com/xss-and-meteor.html,以及可在 Atmosphere 上找到的browser-policy文档:atmospherejs.com/meteor/browser-policy。
第十章。使用账户
在这个章节中,你将学习以下主题:
-
实现 OAuth 账户包
-
自定义账户登录
-
执行双因素认证
简介
几乎对我们构建的每个应用程序来说,某种类型的认证和用户识别都是至关重要的。通常,我们会花费几天甚至几周的时间来开发账户系统,而我们可以用这些时间来编写我们的应用程序。Meteor 解决了这个问题,并且做得很好。从与主要 OAuth 提供者的集成(如 Twitter、Google、Facebook 等)到简单、安全的基于密码的系统,在 Meteor 应用程序中添加账户和认证既快又简单。本章中的配方将涵盖 Meteor 的accounts包最重要的方面,使你能够轻松处理用户账户并继续其他工作。
实现 OAuth 账户包
今天,有如此多的流行认证服务可供选择,用户基数如此庞大,不利用这些服务似乎有点愚蠢。如果你使用像 Twitter 或 GitHub 这样的主要服务的账户系统,你立刻就能接触到庞大的用户群,这可以增加你应用程序的使用率。这个配方将向你展示如何在 Meteor 应用程序中实现 OAuth 账户系统,以 Twitter 账户服务为例。
准备工作
我们将几乎完全专注于我们应用程序的账户和认证部分,因此我们只需要一个非常简单的基础应用程序。
在终端窗口中,通过输入以下命令创建你的根项目:
$ meteor create twitter-login
$ cd twitter-login
$ mkdir {client,server,both}
$ mv twitter-login.* client/
$ meteor add twbs:bootstrap
$ meteor
就这些了。其他所有事情都将在我们配方内部完成,所以让我们开始吧!
如何操作...
我们将添加适当的 accounts 包,并配置我们的 Twitter 登录服务。按照以下步骤进行:
-
在一个新的终端窗口(保持你的应用程序运行),导航到你的项目根目录,并输入以下两个命令:
$ meteor add accounts-twitter $ meteor add accounts-ui这将添加几个依赖包,所以你不必这样做。Meteor 为我们做这件事是多么周到和绅士啊!
小贴士
你也可以轻松地使用其他认证服务,只需将此步骤中使用的
accounts-twitter替换为以下任何一个:accounts-facebook、accounts-github、accounts-google、accounts-meetup、account-weibo或accounts-meteor-developer。 -
打开
[项目根目录]/client/twitter-login.html并在起始<body>标签之后添加以下模板包含:<body> {{> loginButtons}} -
保存此更改,并在浏览器中导航到你的应用程序(通常是
http://localhost:3000/)。在你的屏幕左上角,你会看到一个写着配置 Twitter 登录的红色按钮,如下面的截图所示:![如何操作...]()
-
点击配置 Twitter 登录按钮,将出现配置你的 Twitter 应用程序的指令。严格按照这些指令操作,在适当的位置输入你的
consumerkey(API 密钥)和消费者secret密钥(API 密钥)。小贴士
对于这个配方,你在网站字段中输入的值将是
http://127.0.0.1:3000而不是http://localhost:3000。Twitter 不允许使用localhost。 -
配置好服务后,你就可以登录了。点击蓝色的(以前是红色的)带有使用 Twitter 登录标签的按钮,应该会弹出一个新窗口,要求你授权你的应用程序,如下面的截图所示:
![如何操作...]()
-
一旦你授权了应用程序,你应该已经登录,这可以通过登录按钮中的状态变化来表示,如下面的截图所示:
![如何操作...]()
如何工作...
这个配方相当简洁,所以涉及的代码不多。但我们可以分析一下正在发生的事情,这样我们就可以在需要时了解如何调整这个配方。
让我们深入探讨一下认证部分是如何工作的。accounts-twitter 依赖于 accounts-base、accounts-oauth 和 twitter 包。如果我们逐一打开这些包,就能看到为我们做了多少前期工作:

尽可能简单地说,accounts-ui 和 accounts-twitter 为你提供了一个响应式用户界面,并调用 accounts-base,它处理所有的账户管理。accounts-oauth 通过处理 OAuth 特定的事件和调用帮助 accounts-base。accounts-oauth 由 twitter 包配置,该包提供了使用 Twitter OAuth 服务所需的特定 URL 和参数。
这里有一个更详细的解释:
accounts-base 包是一个通用的账户包,接受不同类型的登录方法,提供账户管理的辅助方法,并帮助维护users集合。users集合是我们存储登录状态、首选项和配置文件信息的地方。
accounts-base 中暴露的一个方法是 Accounts.registerLoginHandler(),它可以被更具体的登录包(如 accounts-oauth 或 accounts-password)用来注册登录信息的处理程序:

当 accounts-base 收到登录请求时,该请求有一个特定的 type,并包含某些 service 参数。accounts-base 将信息通过所有已注册的登录处理程序运行,并让每个处理程序响应,可以是 undefined ("这不是我的登录方法")、error ("凭证错误") 或 serviceData 对象 ("登录被接受"),包括用于轻松重连的令牌。
accounts-oauth 包通过注册类型为 'oauth' 的登录处理程序并在其上公开一些自己的助手来构建在 accounts-base 之上。accounts-oauth 助手允许我们配置特定的 OAuth 服务。每个 OAuth 服务都需要定制的 URL 和参数。我们选择了 Twitter OAuth 服务,因此使用了 twitter 和 accounts-twitter 包来配置这些 URL 和参数。
accounts-oauth 包也负责通过弹出式身份验证表单处理来自 OAuth 服务的消息/回调。也就是说,当 Twitter 的弹出式身份验证完成时,它重定向到 http://localhost:3000/_oauth/twitter 并有一个 OAuth 令牌传递给我们的应用程序。accounts-oauth 包评估该特定 URL(因为我们使用 twitter 包配置了它),抓取令牌,然后尝试使用类似以下内容的 JSON 通过 Accounts.callLoginMethod() 进行登录:
{
methodArguments: [{oauth: {
credentialToken: "m3OHQUrRWU34anuq40Bx3q7JBoEmVgwKGICU1jY4H7_"
credentialSecret: "2qPoqew8m-AXiC2OVfrkWem0_M_APcdMpnz-cGsl6-k"
}},
...
]}
accounts-oauth 注册的登录处理程序接收此 JSON,并与(Twitter)OAuth 服务确认令牌是否有效。如果有效,将在 users 集合中创建/更新 user 配置文件,并通过回调函数将包含登录令牌等内容的 serviceData 传递给客户端。由于回调,并且客户端正在订阅 users 集合,客户端会看到有已登录用户,并相应地采取行动。
twitter 和 accounts-twitter 包包含在 accounts-base 和 accounts-oauth 之上工作的便捷方法。例如,twitter 包有一个服务器文件(twitter_server.js),它声明了特定的 Twitter URL,通过 OAuth.registerService() 方法注册了 twitter 服务,甚至创建了一个针对 Twitter 的特定凭证调用,如下面的代码所示:
Twitter.retrieveCredential =
function(credentialToken, credentialSecret) {
return OAuth.retrieveCredential(
credentialToken, credentialSecret);
};
accounts-twitter 包创建了 Meteor.loginWithTwitter() 方法,并使用 Accounts.addAutoPublishFields() 声明了哪些配置文件字段在客户端可见。
还有更多...
有时,查看用户和配置信息是如何存储的,有助于理解底层发生了什么。我们可以通过在终端窗口中使用 meteor mongo 命令来完成这项操作。打开一个新的终端窗口(保持应用程序运行),导航到您的项目根目录,并输入以下命令:
$ meteor mongo
现在,您将能够通过命令行访问存储用户信息和登录服务配置的集合。
要查看 Twitter 配置设置,请输入以下命令:
> db.meteor_accounts_loginServiceConfiguration.find()
您将看到您的 Twitter 登录服务的配置,如下面的代码所示:
{ "service" : "twitter",
"consumerKey" : "th2is2i2safa333kecon442sume24r433key",
"secret" : "th9isi9sa9fa87kesecr666e3t",
"loginStyle" : "popup", "_id" : "DBfakeYnnFbmidbC"
}
如果您想重新配置您的 Twitter 登录服务,可以使用以下命令删除条目:
> db.meteor_accounts_loginServiceConfiguration.remove({})
完成此操作后,您可以按照屏幕上的说明操作,并重新输入您在先前的配方中使用的 Twitter 凭据。
要查看登录用户的不同的状态,运行并重新运行以下命令:
> db.users.findOne()
尝试运行它,当用户注销时,当用户登录时,以及当用户尚未存在时。特别关注 services 部分,你将能够看到 twitter 和 resume 登录服务如何处理登录。
参见
-
本章中的 定制账户登录 菜单
-
在第十一章的 构建自定义服务器方法 和 处理异步事件 菜单中,利用高级功能,第十一章
定制账户登录
打包的账户登录很棒,但它们并不总是与页面的设计相匹配,或者它们提供了过多的功能,而我们只需要一点功能。这个菜谱将向你展示如何使用 Twitter OAuth 服务作为示例来定制 Meteor 的账户包。
准备工作
我们将基本上使用本章中找到的 实现 OAuth 账户包 菜谱作为我们的基线,但我们不会添加 accounts-ui 包,因此我们不会通过 UI 配置 Twitter 服务,所以我们需要自己动手。
在终端窗口中,通过输入以下命令创建你的根项目:
$ meteor create twitter-custom
$ cd twitter-custom
$ mkdir {client,server,both}
$ mv twitter-custom.* client/
$ meteor add twbs:bootstrap
$ meteor
打开一个新的终端窗口(保持你的应用运行)并添加 accounts-twitter 和 service-configuration 包:
$ meteor add service-configuration
$ meteor add accounts-twitter
我们现在需要手动配置我们的登录服务,使用我们现有的 Twitter 服务(使用 实现 OAuth 账户包 菜谱创建的)中的 API 密钥和 API 密钥。创建一个名为 [项目根目录]/server/auth-init.js 的文件,并添加以下代码,用你的密钥和密钥替换相应的部分:
ServiceConfiguration.configurations.upsert({
service:"twitter" },
{
$set: {
"consumerKey" : "[your API Key from apps.twitter.com]",
"secret" : "[your API secret from apps.twitter.com]"
}
}
);
提示
当你从 Twitter 应用页面复制密钥时,通常会在末尾有一个额外的空格字符。确保你删除该字符(例如,"key123 " 需要改为 "key123"),否则你的认证将失败!
我们现在准备好构建我们自己的定制登录。
如何操作...
要构建我们自己的登录系统,我们需要几个按钮和一些表示我们已登录的指示器。简单得很。
-
打开
[项目根目录]/client/twitter-custom.html并添加以下模板:<template name="customLogin"> {{#if currentUser}} <div id="logout" class="btn btn-info">Log out</div> <img src="img/{{profPic}}" alt=""> {{else}} <div id="login" class="btn btn-default">Log in</div> {{/if}} </template> -
我们现在想调用我们的模板。在
<body>标签内进行以下更改,并保存文件:<body> <div class="container"> {{>customLogin}} <h1>Welcome to Meteor!</h1> {{> hello}} </div> </body> -
创建一个名为
[项目根目录]/client/templatehelpers.js的新文件,并添加以下customLogin模板辅助函数:Template.customLogin.helpers({ profPic: function(){ var loggedin = Meteor.user(); return loggedin && loggedin.services...url; } }); -
现在,让我们连接我们的登录和注销按钮。在同一个
templatehelpers.js文件中,添加以下事件声明:Template.customLogin.events({ 'click #login' : function(e){ Meteor.loginWithTwitter(); }, 'click #logout': function(e){ Meteor.logout(); } }); -
保存你的更改,并在浏览器中导航到你的应用(通常是
http://localhost:3000)。你应该能看到一个登录按钮,如下面的截图所示:![如何操作...]()
-
如果您点击 登录 按钮,并在弹出窗口中授权 Twitter 应用程序,您将获得认证,您的 Twitter 头像将出现在 注销 按钮旁边,如下面的截图所示:
![如何操作...]()
它是如何工作的...
从 customLogin 模板开始,我们使用 {{#if currentUser}} 辅助函数,该函数检查 Meteor.user() 是否为 null。换句话说,如果用户已登录,currentUser 返回 true。
如果 currentUser 是 true,我们添加一个 注销 按钮,以及一个 <img> 标签,其 src 属性设置为用户资料上找到的属性。具体来说,如果用户已登录,profPic 返回 services.twitter.profile_image_url 属性。
对于登录和注销事件,我们只需调用由 accounts-twitter 包提供的 Meteor.loginWithTwitter() 和 Meteor.logout() 方法。Meteor 会为我们处理其余部分。
还有更多…
账户界面本身非常可定制,在 atmospherejs.com/?q=accounts-ui 提供了许多可用的包。
我们建议安装 accounts-ui-unstyled 包,并尝试使用 CSS/样式。您可以通过查看在 github.com/meteor/meteor/tree/devel/packages/accounts-ui-unstyled 可用的原始存储库来获得有关可用选项和 DOM 元素的极佳概述。
请特别注意 login_buttons.html 和 login_buttons.js 文件,因为它们会为您提供一些可能的提示。
参见
- 本章的 实现 OAuth 账户包 菜谱
执行双因素认证
通过提供双因素认证,我们可以使任何应用程序更加安全(并且更安全地免受机器人或黑客攻击)。双因素认证要求个人使用两种不同的方法来验证其身份。其中一种方法,即短信文本验证,因其便利性和难以模仿而变得相当流行。本菜谱将向您展示如何在 Meteor 应用程序中创建双因素认证,使用 Twitter OAuth 和 Twilio SMS 服务。
准备工作
使用本章中提供的 自定义账户登录 菜谱,我们已经有了一个针对 Twitter 进行认证的应用程序。我们将扩展该菜谱,并添加 Twilio SMS 服务以发送用于第二次认证挑战的 6 位验证码。为了我们可以专注于认证部分,我们将在这里设置 Twilio 服务,而不是在主菜谱中。
创建我们的基线应用程序
请遵循本章中找到的 自定义账户登录 菜谱,并重新创建该项目,将名称从 twitter-custom 更改为 two-factor。
为 Twilio SMS 服务注册
在浏览器中导航到 www.twilio.com/try-twilio,或者访问主页 www.twilio.com/,然后点击右上角的 SIGN UP 链接。
输入创建账户所需的信息,然后点击 Get Started。
通过输入你的电话号码并点击 Text Me 来验证你是一个人类。不久之后,你将收到一条短信(如果没有收到,可以重试)。将短信中的代码输入到验证部分,然后点击 Verify 按钮。
现在,将为你生成一个电话号码。接受默认号码,或者选择一个自己的号码(默认号码是免费的),然后点击 Go To Your Account 按钮。
小贴士
记下分配给你的电话号码,因为你稍后需要用到它。
恭喜,你已经通过 Twilio 设置了一个试用账户!
在 Twilio 上创建 SMS 服务
在登录到 Twilio 后,点击页面右上角的你的名字。选择 Account 选项以转到 Account Settings 页面。在那里,你会看到两组 API 密钥。你可以使用其中任何一个进行测试,但显然你希望在生产应用中使用实时凭证。你的屏幕应该看起来像以下截图:

决定你将使用哪些凭证,并记下 AccountSID 和 AuthToken。
安装 twilio-node npm 包
在终端中停止你的应用(Ctrl + C),然后输入以下命令:
$ meteor add meteorhacks:npm
再次运行你的应用,输入以下命令:
$ meteor
你将收到一条消息,告诉你 meteorhacks:npm 已经初始化,如下例所示:
-> npm support has been initialized.
-> please start your app again.
在我们再次启动应用之前,我们需要声明我们将使用 twilio-node npm 包。打开新创建的 [project root]/packages.json 文件,并添加以下声明:
{
"twilio":"1.10.0"
}
现在,在终端中再次启动你的应用,使用 meteor 命令,如前例所示。
在安装了 twilio-node npm 包之后,剩下的就是创建一个 Twilio 消息方法并测试它。
创建和测试 sendTwilio() 方法
在 [project root]/server 目录下创建一个名为 twilio.js 的新文件,并添加以下代码,根据需要替换 AccountSID、AuthToken 和 Twilio Phone Number:
sendTwilio = function (phone, message) {
return Meteor.wrapAsync(function (phone, message, callback) {
var Twilio = Meteor.npmRequire('twilio')
('[YOUR AccountSID GOES HERE]',
'[YOUR AuthToken GOES HERE]');
var phoneNum = '+1' + phone;
var twilioPhone = '[TWILIO NUMBER PATTERN: +1NUMBER]';
Twilio.sendMessage({
to: phoneNum,
from: twilioPhone,
body: message
}, function (err, msg) {
if (err) {
callback && callback(err);
} else {
callback && callback(null, msg);
}
});
})(phone, message);
};
一部分时间可以用来解释前面所有代码的功能,但简单来说,Twilio.sendMessage() 方法通过 Meteor.wrapAsync() 和回调函数被调用,因为 Twilio 是一个 npm 模块,因此需要这个包装器。你现在应该能够向你的手机发送消息了,你可以通过打开一个新的终端窗口(保持你的应用运行),导航到你的根目录,并使用 meteor shell 命令来测试。
如果你的手机号码是 555.867.5309,你应该输入以下内容:
$ meteor shell
> sendTwilio('5558675309','test',function(e,d){console.log(e,d);})
如果一切设置正确,你会在手机上收到一条文本消息,类似于以下截图:

如果出现问题,控制台将输出错误信息,你可以追踪到错误的来源。
希望一切顺利,我们现在已经准备好完成我们的双因素认证流程!
如何做到这一点...
我们将首先确保服务器端的运行顺畅,并引入一个新的登录状态称为 verified。一旦服务器端完成,我们将在我们的 UI 中构建不同的用户状态。
我们需要生成一个 6 位数的代码发送给用户,我们可以利用现有的 Meteor 包来完成这个任务。
-
在一个新的终端窗口中(保持你的应用运行),在项目根目录下,输入以下命令:
$ meteor add random -
现在,创建一个名为
[项目根目录]/both/helpers.js的文件,并添加以下代码:Random.digits = function(len){ var numArr = [0,1,2,3,4,5,6,7,8,9]; var ret = ''; while(ret.length<len){ ret+=Random.choice(numArr); } return ret; }; -
我们刚刚扩展了随机包,使其能够输出我们选择的任何长度的简单代码。如果你想测试,可以在网页控制台中输入
Random.digits(6)。 -
打开
[项目根目录]/server/auth-init.js并追加以下声明:var verifiedField = ['services.twofactor.verified', 'services.twofactor.phone']; Accounts.addAutopublishFields({ forLoggedInUser: verifiedField, forOtherUsers: [] }); -
现在,为了确保
services.twofactor.verified属性存在于每个账户上,我们将在账户创建时以及用户成功登录时初始化它。将以下代码追加到auth-init.js:Accounts.onCreateUser(function(options,user) { check(options, Object); check(user, Object); user.services.twofactor = {}; user.services.twofactor.code = Random.digits(6); user.services.twofactor.verified = false; user.profile = options.profile; return user; }); Accounts.onLogin(function(options){ if (options.type!=='resume'){ Meteor.users.update( options.user._id, {$set: {"services.twofactor.verified":false, "services.twofactor.code":Random.digits(6) } } ); } }); -
现在我们已经准备好创建短信认证挑战。创建一个名为
[项目根目录]/server/auth-methods.js的新文件,并添加以下Meteor.methods声明:Meteor.methods({ sendChallenge : function (phone){ if (!this.userId) return; var newCode = Random.digits(6); if (phone!=null){ Meteor.users.update( this.userId, {$set: {"services.twofactor.phone":phone, "services.twofactor.code":newCode}}); } else { Meteor.users.update( this.userId, {$set: {"services.twofactor.code":newCode}}); } var curUser = Meteor.users.findOne(this.userId); return sendTwilio(curUser.services.twofactor.phone, curUser.services.twofactor.code); }, verifyCode : function(code){ if (!this.userId) return; var curUser = Meteor.users.findOne(this.userId); if (!curUser) return; if (curUser.services.twofactor.code == code){ Meteor.users.update( this.userId, {$set: {"services.twofactor.verified":true}}); } } }); -
在服务器端设置好一切之后,我们现在需要更新客户端的模板和事件。让我们首先创建一种视觉信号来表示我们已经成功认证。打开
[项目根目录]/client/two-factor.html并修改<button>元素如下:<button class="btn {{btnState}}">Click Me</button> -
现在打开同一文件夹中的
two-factor.js,并在Template.hello.helpers声明中添加以下内容:counter: function () { return Session.get('counter'); }, btnState: function(){ var curUser =Meteor.user(); if (curUser && curUser.services.twofactor.verified) return 'btn-success'; return 'btn-danger'; } -
最后,对
'click button'事件处理程序进行以下修改:'click button': function () { // increment the counter when button is clicked var curUser =Meteor.user(); if (curUser && curUser.services.twofactor.verified) { Session.set('counter', Session.get('counter') + 1); } else { alert ('not authorized!'); } } -
所有的其他工作都已经完成。我们现在只需要提供一个方法来调用我们的服务器。打开
[项目根目录]/client/two-factor.html并对customLogin模板进行以下修改:<template name="customLogin"> <div class="btn-toolbar"> <div class="btn-group" role="group"> {{#if currentUser}} <div type="button" id="logout" class="btn btn-info btn-lg">Log out</div> <div id="profile" class="btn btn-default btn-lg"> <img src="img/{{profPic}}" alt=""> </div> {{else}} <div type="button" id="login" class="btn btn-default btn-lg">Log in</div> {{/if}} </div> {{#if currentUser}} {{>secondLogin}} {{/if}} </div> </template> -
我们现在需要创建一个
secondLogin模板,根据用户是否已验证来设置条件。将以下模板追加到two-factor.html的底部:<template name="secondLogin"> {{#if verified}} <div class="btn btn-success btn-lg"> <span class="glyphicon glyphicon-ok"></span> </div> {{else}} <div class="btn-group" role="group"> <div class="btn btn-primary btn-lg" id="btnChallenge"> <span class="glyphicon glyphicon-phone"></span> </div> <input type="text" id="phoneNum" class="btn btn-default btn-lg" placeholder="{{defaultPhone}}"> </div> <div class="btn-group" role="group"> <input type="text" id="verCode" class="btn btn-default btn-lg" placeholder="code..."> <div class="btn btn-primary btn-lg" id="btnVerify"> <span class="glyphicon glyphicon-check"></span> </div> </div> {{/if}} </template> -
我们需要添加一点 CSS 来使我们的个人资料图片表现正常。打开名为
[项目根目录]/client/two-factor.css的文件,并添加以下 CSS 声明:#profile img { max-height: 44px; margin: 0 0; } #profile { padding: 0 0; } -
打开
[项目根目录]/client/templatehelpers.js并添加以下辅助函数:Template.secondLogin.helpers({ verified: function(){ var curUser = Meteor.user(); return (curUser&&curUser.services.twofactor.verified); }, defaultPhone: function(){ var curUser = Meteor.user(); return curUser && curUser.services.twofactor.phone; } }); -
最后,我们需要添加按钮的事件处理程序来发送短信文本以及验证短信消息中找到的代码。将以下代码追加到
templatehelpers.js:Template.secondLogin.events({ 'click #btnChallenge' : function (e){ var phoneNum = $('#phoneNum').val(); if (!phoneNum.length) phoneNum = $('#phoneNum').attr('placeholder'); if (!phoneNum.length==10) return; Meteor.call('sendChallenge',phoneNum); }, 'click #btnVerify' : function(e){ var verCode = $('#verCode').val(); if (!verCode.length==6) return; Meteor.call('verifyCode',verCode); } }); -
保存所有更改,然后测试您的新 UI。通过 Twitter 进行认证后,您将收到两个文本提示和两个按钮,如下面的截图所示:
![如何操作...]()
左侧的按钮会将随机生成的代码发送到您指定的电话号码(如果存在已保存的电话号码,则发送到该号码)。右侧的按钮将提交验证代码。如果您输入了正确的代码(在发送到您手机的短信中找到的代码),您将得到验证,并且您的屏幕将类似于以下截图:
![如何操作...]()
恭喜你,你已经在你的 Meteor 应用中实现了双因素认证!
它是如何工作的...
为了实现双因素认证,必须完成几件事情。首先,我们需要通过一个名为 twofactor 的新服务扩展用户集合。一些 services.twofactor 属性需要暴露给客户端使用,并且每当创建新用户或用户使用 Twitter OAuth 登录时,我们必须将 services.twofactor.verified 设置为 false。在我们的 auth-init.js 文件中,我们通过调用 Accounts.addAutopublishFields() 方法,然后通过监听并更新 Accounts.onCreateUser() 和 Accounts.onLogin() 事件处理器来完成这两项任务:
Accounts.addAutopublishFields({ forLoggedInUser: verifiedField, forOtherUsers: [] });
Accounts.onCreateUser(function(options,user) {
...
user.services.twofactor.code = Random.digits(6);
user.services.twofactor.verified = false;
...
});
Accounts.onLogin(function(options){
...
{$set:
{"services.twofactor.verified":false,
"services.twofactor.code":Random.digits(6)
}
...
});
我们添加了两个服务器方法来帮助我们验证用户。第一个,sendChallenge(),生成一个新的 6 位随机代码,更新 services.twofactor.code 属性,然后通过 Twilio 服务将代码发送到指定的电话号码。第二个,verifyCode(),接收用户的手动输入,将手动输入的代码与 services.twofactor.code 属性进行比较,如果匹配则将 services.twofactor.verified 更新为 true:
Meteor.methods({
sendChallenge : function (phone){
if (!this.userId) return;
var newCode = Random.digits(6);
...
return sendTwilio(...);
},
verifyCode : function(code){
...
if (curUser.services.twofactor.code == code){
Meteor.users.update(
this.userId,
{$set:
{"services.twofactor.verified":true}});
}
}
});
通过添加 verified 属性和用于将 verified 属性从 false 更改为 true 的服务器方法,我们现在可以在我们的 UI 中使用 verified。我们创建了一个辅助方法 Template.secondLogin.verified,用于检查 services.twofactor.verified 属性是否设置为 true。然后我们在 secondLogin 模板中使用这个辅助方法来显示用户已登录并得到验证:
<template name="secondLogin">
{{#if verified}}
<div class="btn btn-success btn-lg">
<span class="glyphicon glyphicon-ok"></span>
</div>
{{else}}
...
{{/if}}
</template>
UI 上的其余事件处理器和辅助工具用于方便起见,或者用于调用之前概述的服务器方法。
简而言之:我们扩展了 users 集合,添加了一个 verified 属性。我们使用短信文本和验证来改变 verified 属性的值。我们不允许客户端 UI 进行任何活动,除非 verified==true。这个检查作为检查是否有已登录用户的补充,使我们能够在我们的 UI 中要求双因素认证。
还有更多...
这种通过添加新的 services 属性来扩展 users 集合的模型可以用于几乎所有事情,而不仅仅局限于 SMS 文本挑战。通过添加和公开新的 services 属性,你可以控制哪些功能可用,这取决于用户的状态。想象一下,能够根据用户购买的订阅计划限制一些 UI 功能。或者想象一下,根据用户保存的首选项记住布局和视图偏好。所有这些,以及更多,都可以通过扩展 users 集合来实现。
参见
-
使用 Bootstrap 构建平滑界面中的 构建平滑界面 菜单,构建出色的用户界面
-
直接使用 npm 包中的 使用 npm 包直接 菜单,集成第三方库
-
本章中 自定义账户登录 的菜谱
-
使用异步函数中的 使用异步函数 菜单,利用高级功能
第十一章。利用高级功能
在本章中,我们将介绍以下主题:
-
构建自定义服务器方法
-
创建自定义 EJSON 对象
-
处理异步事件
-
使用异步函数
简介
Meteor 提供了一些独特的功能,使我们在全栈开发环境中的生活更加轻松。本章将深入探讨这些不太为人所知的功能区域,为你提供更多底层知识,使你的应用程序更加优雅和强大。
构建自定义服务器方法
在 Meteor 中,我们与客户端和服务器之间的大部分通信都是通过 DDP 完成的。然而,有时直接调用服务器会非常有用。例如,在测试时,或者作为管理员,你可能需要创建一些“隐藏”的辅助方法来简化调试。在其他情况下,你可能希望提供非常安全或非常简单的服务方法,以减少代码的脆弱性或复杂性。无论如何,服务器方法是开发的基础,本食谱将指导你使用Meteor.methods()函数创建和使用服务器端方法。
准备工作
要创建服务器方法,我们首先需要一个基本的应用程序。我们将快速创建一个颜色样本应用程序。虽然不是什么特别复杂的应用,但一旦完成,我们就能通过良好的视觉反馈来创建服务器方法。
项目设置
你需要安装 Meteor,并创建一个项目。在终端窗口中,通过输入以下命令创建你的根项目:
$ meteor create server-calls
$ cd server-calls
$ rm server-calls.*
$ mkdir {client,server,both}
$ meteor add twbs:bootstrap
$ meteor
创建一个简单应用
创建一个名为 [project root]/both/helpers.js 的文件,并添加以下代码:
Swatches = new Mongo.Collection('swatches');
randomColor = function(){
var retCol = '#';
while (retCol.length<4) {
retCol += Random.choice('06F');
}
return retCol;
}
接下来,创建一个名为 [project root]/client/client.js 的文件,并添加以下代码:
Template.colors.helpers({
swatches: function(){
return Swatches.find().fetch();
}
})
Template.body.events({
'click #newColor' : function(e){
Swatches.insert({color:randomColor()});
}
})
通过创建一个名为 [project root]/client/styles.css 的文件并添加以下样式声明来创建一些简单的样式:
.swatch{
display:inline-block;
height:8rem;
width:8rem;
border-radius: 0.5rem;
margin-top: 1rem;
}
#newColor{
display:block;
margin-top: 0.5rem;
}
最后,通过创建 [project root]/client/main.html 并添加以下模板来创建你的模板:
<body>
<div class="container">
<div id="newColor" class="btn btn-info btn-lg">
<span class="glyphicon glyphicon-plus"></span>
</div>
{{> colors}}
</div>
</body>
<template name="colors">
{{#each swatches}}
<div class="swatch" style="background-color:{{color}}"></div>
{{/each}}
</template>
保存所有更改,在浏览器中导航到 http://localhost:3000,然后反复点击带有加号按钮以添加随机颜色样本。你应该会看到以下截图类似的内容:

现在你可以准备创建一些辅助服务器方法了。
如何操作...
我们将创建两个服务器方法。一个用于清除Swatches集合,另一个用于添加唯一的颜色。
-
首先,让我们构建
clearSwatches()函数。创建一个名为[project root]/server/methods.js的文件,并添加以下Meteor.methods声明:Meteor.methods({ clearSwatches: function(){ Swatches.remove({}); } });在你的浏览器控制台中,输入以下命令:
> Meteor.call('clearSwatches')所有的颜色样本都应该消失。不要担心,朋友,你总是可以通过点击屏幕上的按钮来创建更多样本。
-
接下来,让我们创建
addUniqueSwatches()函数。从上一步打开methods.js文件,并在clearSwatches声明之后添加以下声明:clearSwatches: function(){ Swatches.remove({}); }, addUniqueSwatch: function(newColor){ if (Swatches.findOne({color:newColor})) return null; Swatches.insert({color:newColor}); } -
现在,打开
[项目根目录]/client/client.js文件,并对Template.body.events声明进行以下修改:Template.body.events({ 'click #newColor' : function(e){ Meteor.call('addUniqueSwatch' , randomColor()); } })现在,当你点击按钮添加颜色时,你会发现最终不会添加新的色卡。这是因为独特的颜色总数为 27。如果你一开始没有色卡,你的屏幕最终会看起来类似于以下截图:
![如何操作…]()
无论你点击按钮多少次,都无法添加超过 27 种颜色。
它是如何工作的…
我们通过在 server 文件夹内创建代码在服务器上创建了两个调用。这些调用被暴露给客户端,因为我们使用 Meteor.methods() 函数声明了它们。
我们在 methods.js 文件中创建的 clearSwatches() 方法是 隐藏的,也就是说,除非你知道它的存在,否则没有简单的方法知道它存在。addUniqueSwatch() 方法用于我们按钮的 onclick 事件中,所以有人可以通过查看我们的客户端代码来发现它。
在这两种情况下,方法不是通过 Ajax/传统服务器调用调用的,而是由 Meteor 自动暴露,并且通过 Meteor.call() 方法调用,该方法接受方法名称作为第一个参数,以及任何额外的参数作为方法参数,非常容易调用。
还有更多…
如果你将回调函数作为 Meteor.call() 的最后一个参数传递,当服务器方法执行完成后,回调函数将被自动调用。你可以使用这个功能在调用完成后执行操作。以下是一个示例。
打开 [项目根目录]/server/methods.js 文件,并对 addUniqueSwatch() 函数进行以下修改:
addUniqueSwatch: function(newColor){
if (Swatches.findOne({color:newColor})) return null;
Swatches.insert({color:newColor});
return Swatches.find().count();
}
现在,在你的浏览器控制台中,快速重置 Swatches 集合:
> Meteor.call('clearSwatches')
最后,多次手动调用 addUniqueSwatch():
> Meteor.call('addUniqueSwatch',randomColor(), function(err,data){console.log(data);})
每次运行该命令时,控制台将打印出色卡的总数。Meteor 会自动运行你传递的回调函数,将 addUniqueSwitch() 方法的结果填充到数据参数中。
参见
-
在 第一章 的 使用网络控制台 菜谱中,优化你的工作流程
-
在 第二章 的 添加 Meteor 包 菜谱中,使用包进行自定义
创建自定义 EJSON 对象
使用 DDP 在客户端和服务器之间传递简单对象非常简单。但你是否知道你可以传递自定义的、命名的对象,包括方法?Meteor 的人扩展了 JSON,允许通过 DDP 传递自定义的完整对象,而不必担心序列化/反序列化。这个菜谱将教你如何创建和使用自定义 EJSON 对象,并在客户端和服务器之间传递这些自定义对象。
准备工作
我们将使用本章中找到的先前配方,即构建自定义服务器方法,作为基准。请完成该配方,然后进行以下修改。
声明 Swatch 对象
创建一个名为[项目根目录]/both/swatch.js的新文件,并将以下代码添加到该文件中:
Swatch = function (color){
this.color = color;
}
Swatch.prototype = {
constructor: Swatch,
switch: function(){
this.color = randomColor();
},
toString: function(){
return "My color is: " + this.color;
}
}
修改 Swatches.insert()
为了使用 EJSON 对象,这些对象不是 MongoDB 期望的正常、纯对象,我们在插入时需要将对象分层。在网页控制台中,执行以下行以清除Swatches集合:
> Meteor.call('clearSwatches')
现在,打开[项目根目录]/server/methods.js并修改Swatches.insert()方法如下:
addUniqueSwatch: function(newColor){
...
Swatches.insert({swatch:new Swatch(newColor)});
}
改变色板颜色
在插入时分层将稍微破坏我们的 UI,但没关系,我们可以恢复,因为我们很棒。在[项目根目录]/client/client.js文件中的Template.colors.helpers部分,并在swatches辅助函数下方添加以下辅助函数:
},
color: function(){
return this.swatch.color;
}
最后,让我们添加以下events声明,以便我们可以改变色板的颜色:
Template.colors.events({
'click .swatch' : function(e){
this.swatch.color = randomColor();
Swatches.update(this._id,this);
}
})
保存您的更改,并通过点击添加的任何色板在浏览器中测试。每次点击时,相应的色板都应该改变为随机颜色。现在,您已经准备好创建自定义 EJSON 对象了。
如何操作...
如前所述,Meteor 会为我们处理序列化和反序列化。我们只需要将我们的对象声明为 EJSON 对象。按照以下步骤创建自定义 EJSON 对象:
-
让我们添加
typeName和toJSONValue函数。打开[项目根目录]/both/swatch.js文件,并在toString()函数下方添加以下两个函数到Swatch.prototype声明中:}, typeName: function(){ return 'Swatch'; }, toJSONValue: function(){ return { color:this.color }; } -
接下来,我们需要使用
EJSON.addType声明我们的对象。在同一个swatch.js文件中,在最底部添加以下函数调用:EJSON.addType("Swatch", function fromJSONValue(value){ return new Swatch(value.color); }); -
我们现在可以使用我们的 EJSON 对象方法了。打开
[项目根目录]/client/client.js文件,并对Template.colors.events进行以下更改:'click .swatch': function (e) { this.swatch.switch(); Swatches.update(this._id, this); console.log(this.swatch.toString()); }在浏览器中,添加一些新的色板,并使用打开的浏览器控制台窗口点击它们。色板会改变颜色,并在控制台中显示它们的新颜色。您的屏幕应该类似于以下截图:
![如何操作...]()
它是如何工作的...
当已知的EJSON对象被插入到或从 MongoDB 检索时,Meteor 会自动使用EJSON库对对象进行序列化和反序列化。
要正确完成这项任务,对象本身至少需要有两个方法。首先,对象必须有一个typeName函数,它有助于将对象映射到声明的EJSON对象。其次,对象必须有一个toJSONValue()函数,这样 Meteor 才能正确地将对象序列化为字符串/标准 JSON 值。如果没有toJSONValue()函数,MongoDB 会对对象产生错误,并拒绝插入它。通过 DDP 传输对象同样不可行。
需要进行的另一个操作是声明 EJSON 对象,我们在 swatch.js 中使用 EJSON.addType() 函数完成了这一操作。这个函数接受一个 fromJSONValue() 函数作为参数,该函数(正如其名)接收通过线路传递的 JSON 值,并实例化一个适当类型的实际对象。在我们的例子中,这是一个 Swatch 对象,唯一需要创建的属性是 color 属性,它通过构造函数传入。
一旦声明了前面的方法并调用了 addType 函数,Meteor 会为我们处理其余的事情,并将我们的对象巧妙地存储在 MongoDB 中。以下是一个示例,展示了我们序列化的 Swatch 对象的原始 JSON:
{
"_id" : "tktEzxMGTGNZ8oB4R",
"swatch" : {
"EJSON$type" : "Swatch",
"EJSON$value" : {
"EJSONcolor" : "#66F"
}
}
}
还有更多...
EJSON 对象还有其他辅助函数,您可以在一个对象中声明这些函数,以帮助开发和使用 EJSON 对象。
可以在 EJSON 对象上实现 .clone() 方法,以执行具有逻辑的深度复制。如果你没有声明 .clone() 函数,Meteor 将使用 toJSONValue() 代替。
.equals() 方法接受另一个对象作为参数,并执行你选择的自定义比较。如果你没有声明 .equals() 函数,Meteor 将简单地取两个对象,对每个对象执行 toJSONValue() 转换,并比较结果。
小贴士
通过查看 Meteor 文档了解更多关于 EJSON 库的信息,文档地址为:www.meteor.com/ejson。
参见
- 本章中 构建 自定义服务器方法 的配方
处理异步事件
Meteor 是一个响应式框架。正如你可能注意到的,它建立在 Node.js 之上,但在编写或使用包时,它神奇地避免了你通常在 Node.js 中遇到的回调和事件循环的戏剧性。Meteor 允许你以声明式、类似同步的风格进行编码。这个配方将向你展示 Meteor 是如何做到这一点的,以及你如何处理来自第三方包的异步事件。在这种情况下,我们将从 npm 的 twit 包中读取 Twitter 流。
准备工作
我们需要快速在 Twitter 上设置一个测试应用并加载 npm 的 twit 模块,所以我们在这里做,以免分散对配方本身的注意力。请注意,你可以为这个配方使用任何你想要的异步事件流,包括标准的 setInterval() 方法,它可以用来模拟异步事件流。
创建基线 Meteor 应用
打开一个终端窗口,导航到你希望项目所在的位置,并输入以下命令:
$ meteor create thirdpartyevents
$ cd thirdpartyevents
$ rm thirdpartyevents.*
$ mkdir {client,server,both}
$ meteor add meteorhacks:npm
$ meteor
与通常启动 Meteor 不同,这将创建一个名为 [项目根]/packages.json 的文件。在编辑器中打开该文件,并添加以下声明:
{
"twit" : "1.1.20"
}
保存你的更改,然后在终端中再次运行 meteor 命令:
$ meteor
获取你的 Twitter 访问令牌
使用浏览器登录到twitter.com,然后导航到apps.twitter.com。按照说明创建一个新的应用,点击密钥和访问令牌标签以获取您的消费者密钥 + 秘密以及在该页面上找到的有效访问令牌 + 秘密。请保持此页面打开,因为我们稍后会需要参考它。
小贴士
关于设置 Twitter 应用的 Meteor 特定说明,请参阅第十章中的实现 OAuth 账户包配方,处理账户。
初始化 twit
创建一个名为[项目根]/server/config-twit.js的文件,并添加以下代码,用之前获得的信息替换consumer_key、consumer_secret、access_token和access_token_secret。完成后,您的文件应类似于以下代码:
Twitter = Meteor.npmRequire('twit');
Twit = new Twitter({
consumer_key: 'egrdttfakeconsumerkeyFMx42339eMR8',
consumer_secret: 'fR2r02CthisnJCDtVisMij2WjNiafakeo6QPqsecretnxztb',
access_token: 'q8thisnEkn3xMiscUhafake9I5EOAtoken3DvDZM',
access_token_secret: '7mel7Kr8fakeaccesstokensecretdzpiDuaqtRaij914'
});
simplifyTweet = function(tweet){
var retObj = {};
if (!tweet) return retObj;
retObj.created_at = tweet.created_at;
retObj.text = tweet.text;
retObj.user = '@' + tweet.user.screen_name;
return retObj;
}
创建推文集合,并构建流读取器
创建一个名为[项目根]/both/model.js的文件,并添加以下行:
Tweets = new Mongo.Collection('tweets');
现在,创建一个名为[项目根]/server/twitter-stream.js的文件,并添加以下代码:
stream = {};
Meteor.methods({
TwitterStream: function (query) {
if (query == 'off') {
if (stream.stop != null) stream.stop();
Tweets.remove({});
return;
}
stream = Twit.stream('statuses/filter', {
track: query
});
stream.on('tweet', function (tweet) {
var simpleT = simplifyTweet(tweet);
console.log(simpleT);
});
}
})
跟踪和测试更改
创建一个名为[项目根]/client/consoleTracking.js的文件,并添加以下代码:
Tracker.autorun(function(){
console.table(Tweets.find().fetch());
});
现在,在浏览器中导航到http://localhost:3000,打开控制台窗口,并输入以下命令:
> Meteor.call('TwitterStream','JavaScript')
在您使用meteor命令启动 Meteor 的终端窗口中,您应该定期看到一些 JSON 格式的推文。如果推文没有显示,可能是因为您使用的查询不够流行(这是一个实时 Twitter 流!),因此您可以选择其他内容,如lmao或lebron,如果您想看到稳定的流。
最后,您将想要测试Tweets集合的跟踪。在浏览器控制台中输入以下命令,并多次运行它:
> Tweets.insert({a:Tweets.find().count()})
如果一切设置正确,您将在控制台中看到不断增长的条目表。
要关闭 Twitter 流并清除Tweets集合,请在浏览器控制台中输入以下内容:
> Meteor.call('TwitterStream','off')
在一切清理完毕后,让我们继续到配方。
如何操作...
此配方专注于仅处理异步事件同步。因此,以下有两个非常简单的步骤:
-
打开
[项目根]/server/twitter-stream.js,并在stream.on处理器中添加以下insert语句,如下所示:stream.on('tweet', function (tweet) { var simpleT = simplifyTweet(tweet); console.log(simpleT); Tweets.insert(simpleT); });因为事件处理器是异步事件,如果您直接尝试打开流,您将得到一个非常糟糕的错误,如下所示:
Meteor code must always run within a Fiber. -
按照以下方式修改
stream.on处理器:stream.on('tweet', Meteor.bindEnvironment( function (tweet) { var simpleT = simplifyTweet(tweet); console.log(simpleT); Tweets.insert(simpleT); }));您现在可以在浏览器控制台中打开流,如下所示:
> Meteor.call('TwitterStream','JavaScript')随着从流中进入的条目,它们将在推文集合中填充,您将在浏览器控制台中看到类似以下的结果:
![如何操作…]()
它是如何工作的…
为了实现我们熟悉并喜爱的声明式、响应式环境,Meteor 服务器故意在每个请求上运行单个线程。因此,当你创建某种异步操作,例如监听 Node.js 事件流时,该异步操作的回调(处理程序)在另一个线程上操作。当回调被触发时,如果你尝试调用 Meteor 函数(例如 Tweets.insert()),Meteor 将抛出错误,因为回调和主 Meteor 服务器在不同的线程上。此外,如果你使用了任何具有值的全局变量,你无法保证在异步调用等待期间这些值保持不变。其他操作可能会改变环境!
Meteor 有一种优雅的方式来处理这种情况,使用 Meteor.bindEnvironment()。
通过使用 Meteor.bindEnvironment() 封装回调函数,创建了一个 Fiber。Fiber 跟踪变量及其值(环境),并知道迟早会调用回调。
同时,Fiber 将操作从事件循环中移除,以便操作不会阻塞其他操作。一旦回调就绪,Fiber 将操作放回事件循环,恢复环境,并完成操作。
在这个特定的情况下,当我们使用 Meteor.call('TwitterStream',…) 时,我们是在单个线程上向 Meteor 服务器发送请求。Twit.stream 服务在该线程上启动,但事件(传入的流)是异步的。通过使用 Meteor.bindEnvironment() 封装 stream.on() 的处理程序,我们指示 Meteor “拍摄”当前环境的快照。然后,Meteor 将当前操作从事件循环中取出,以便其他事情可以通过。然后,我们等待。
当流有新数据时,事件被触发,这会触发回调。Meteor(或更准确地说,Fiber)看到回调已就绪,从使用 Meteor.bindEnvironment() 创建的快照中恢复环境,并将操作放回事件循环。这个过程会重复进行,直到需要多少次就重复多少次,每次有新的推文到来并触发回调函数时。
以简化的风险,Meteor.bindEnvironment() 将一些代码封装到 Fiber 中,然后等待,而不阻塞任何其他代码操作。一旦封装的代码准备就绪/激活,Fiber 确保代码在正确的线程上以正确的数据执行。
还有更多……
我们可以非常细致地控制要封装的代码,并且可以将封装的代码分离出来,使整体代码更容易阅读。
将以下函数添加到 twitter-stream.js 文件的顶部:
wrappedInsert = Meteor.bindEnvironment(function(simpleT){
Tweets.insert(simpleT);
});
现在,通过移除 stream.on 的 Meteor.bindEnvironment() 封装,并调用 wrappedInsert() 而不是 Tweets.insert(),如下所示:
stream.on('tweet', function (tweet) {
var simpleT = simplifyTweet(tweet);
console.log(simpleT);
wrappedInsert(simpleT);
});
这将完全与封装整个回调函数的方式相同。
最后一种选择,我们可以移除整个回调,用用户友好的名称包装它,并在回调部分使用该名称,如下面的示例所示:
wrappedCallback = Meteor.bindEnvironment(
function (tweet) {
var simpleT = simplifyTweet(tweet);
console.log(simpleT);
Tweets.insert(simpleT);
}
);
...
stream.on('tweet', wrappedCallback);
小贴士
关于 Fibers、Futures 和Meteor.bindEnvironment()的最简洁解释可以在以下链接找到:bit.ly/meteor-fibers-explained。请务必仔细查看该页面的所有链接!
相关内容
- 在第十章([part0079.xhtml#aid-2BASE1 "第十章。使用账户"】)的实现 OAuth 账户包食谱中,使用账户
使用异步函数
Node.js 尽管非常出色,但有一个缺陷:回调。异步和非阻塞有许多优点,我们无法想象没有它的生活。但是,如果有一种方法可以避免“回调地狱”,通过以同步风格编写我们的代码,但仍然获得异步代码的好处,那岂不是很好?正如您可能已经猜到的,Meteor 有实现这一点的办法。这个食谱将向您展示如何使用 Meteor.wrapAsync() 以同步风格编写和处理异步函数。
准备工作
由于简洁性可以带来清晰性,我们将尽可能简化这个食谱。
打开一个终端窗口,导航到您希望项目存放的位置,并输入以下命令:
$ meteor create wrap-sample
$ cd wrap-sample
$ mkdir server
$ meteor
如何做…
我们将使用标准的 JavaScript setTimeout() 函数来模拟对异步方法的延迟调用。
-
首先,让我们为服务器调用做准备。打开
[项目根]/wrap-sample.js并修改Template.hello.events函数,如下所示:'click button': function () { var x = 0; while (x < 5) { x++; var q = "" + x + ". do work"; Meteor.call('someCall', q, function (e, d) { console.log(d); }); } } -
创建一个名为
[项目根]/server/method.js的文件,并添加以下异步函数:asyncCall = function(query,cb){ var ranLen = ~~(Math.random()*3000); setTimeout(function(){ cb && cb(null,query + " complete!"); },ranLen); }; -
现在,添加一个简单的
Meteor.methods声明,使用Meteor.wrapAsync()来编写同步风格的代码,如下所示:Meteor.methods({ someCall: function (query) { console.log('performing: '+query); this.unblock(); var syncCall = Meteor.wrapAsync(asyncCall); var result = syncCall(query); return result; } }); -
打开一个浏览器到
http://localhost:3000,打开浏览器控制台,并点击屏幕上的按钮。当您查看服务器终端时,您将立即看到五个日志条目,按顺序打印,类似于以下内容:performing: 1\. do work performing: 2\. do work performing: 3\. do work performing: 4\. do work performing: 5\. do work在浏览器窗口中,您将看到五个日志条目。然而,这些条目不会立即出现,并且可能顺序混乱,类似于以下示例:
1\. do work complete! 4\. do work complete! 2\. do work complete! 5\. do work complete! 3\. do work complete! -
再次点击按钮,注意五个服务器调用的随机顺序和完成时间。
它是如何工作的…
Meteor.wrapAsync() 是一种语法糖,用于将异步调用运行得像同步调用一样。正如本章中讨论的处理异步事件食谱所示,Fibers 和 Futures 是 Meteor 处理异步逻辑的方式。在这种情况下,我们正在处理在Node.js中会被认为是标准异步函数的函数。
Meteor 定义的标准异步函数如下:
-
将回调作为最后一个参数
-
是非阻塞的
-
在完成时执行回调
传入回调函数的签名总是相同的。有两个参数,第一个是一个 error 对象。如果有错误,这个对象将有一个值,否则,error 参数将是 null。
第二个参数是一个 data 对象。如果调用不返回一个错误对象作为其第一个参数,这个数据参数将包含可以被回调函数使用的数据。
在我们的例子中,我们声明了一个名为 asyncCall() 的标准异步函数。asyncCall() 的签名如下所示:
function(query,cb)...
在 asyncCall() 中没有任何操作会阻塞事件循环,尽管我们调用了 setInterval()。setInterval() 调用是异步的,因此是非阻塞的(非常类似于 AJAX 调用或文件 I/O 操作等)。一旦 setInterval() 完成,它将调用我们的回调函数 cb(null, query + '...')。
在 Meteor.methods 声明中,我们创建了一个名为 someCall 的方法。在 someCall 内部,我们首先通过调用 this.unblock() 确保不会阻塞事件循环。
然后,我们使用 Meteor.wrapAsync(asyncCall) 命令将我们的标准异步函数(如前所述)进行封装。
一旦封装,我们就可以像同步调用一样使用封装后的调用。实际上它不是同步的。结果会有延迟,但我们的操作将等待异步调用完成,这使我们能够更好地理解正在发生的事情。如果我们查看 Meteor.wrapAsync() 之后的代码行,我们可以轻松地理解正在发生的情况:
var result = syncCall(query);
return result;
result 变量被分配给调用结果。然后我们像在同步函数中一样返回 result。简单易懂。
还有更多…
使用 Meteor.wrapAsync() 的规则可以这样表述:任何你原本需要将逻辑嵌套在回调中的标准异步函数,你都可以将其封装,并像同步调用一样使用其结果。
在底层,Meteor 正在封装 Fiber/Future 并自动实现 .current、.yield()、.run() 和 .wait() 这些功能供你使用。但无需担心这一点。只要你坚持认为 .wrapAsync() 将带有回调的异步函数转换成不带回调的标准函数,你就能顺利地写出优雅、声明式的代码。
参见
- 本章中关于 处理异步事件 的配方
第十二章:创建有用的项目
在本章中,我们将涵盖以下主题:
-
创建 RESTful Web 服务
-
使用 Iron Router 创建完整的应用
-
将应用部署到移动设备
-
添加社交分享
简介
在您学习 Meteor 的旅途中,您将达到一个掌握基础知识的阶段。在那个阶段,您将希望开始将所有基础知识整合到实际有用的应用或包中。外面的开发世界很大,有很多机会。Meteor 的领域仍然处于起步阶段,如果您已经遵循了本书中的所有(或大多数)食谱,您可能已经准备好开始定义这个领域。最后一章将指导您了解适用于大多数项目的四个最有用的食谱。这将为您独立定义、发现和构建提供更坚实的基础!
创建 RESTful Web 服务
唉,唉,整个开发世界还没有意识到 DDP 协议有多么神奇(还没有呢!)。数据通过线传输是一个惊人的概念,我们相信它(或类似的东西)将是应用程序通信的未来。与此同时,我们有一些非常有用且已建立的协议,如果在我们应用中实现,将增加我们应用在其他平台上的覆盖范围和可访问性。
所有这些协议中,REST 是始祖。REST 已经成熟、定义明确,几乎在所有可想象的编程语言中都有实现。如果我们/当我们与外星种族接触时,第一个信号很可能是POST请求。鉴于其当前状态(以及我们最可能的选择,即和平的星际谈判),我们希望将 REST 功能添加到我们的某些应用中。本食谱将指导您使用 Iron Router 实现服务器端 REST 实现。
准备工作
由于在本食谱中我们只处理应用的 REST 部分,所以我们不需要任何花哨的东西。一个简单的、标准的 Meteor 应用就足够了,只需要几个服务器端文件。
创建基线应用
打开终端窗口,导航到您希望项目根目录所在的位置,并输入以下命令:
$ meteor create RESTSample
$ cd RESTSample
$ rm RESTSample.*
$ mkdir -p server/api
$ mkdir server/model
$ meteor add iron:router
$ meteor
安装和配置 Postman
我们需要一种手动调用我们的 REST 服务的方法,Postman 插件/应用对于 Chrome 来说是个不错的选择。如果您有自己的手动调用 REST 服务的方法,请随意使用。如果没有,以下是安装 Postman 的方法:
-
在浏览器中,导航到
getpostman.com,然后点击页面底部的立即获取链接。Postman 应用的预览将弹出,在右上角将有一个安装 Postman 的按钮。点击该按钮,按照指示操作,并打开 Postman。 -
在 Postman 的请求窗口中,在常规选项卡下,输入以下 URL:
http://localhost:3000/api/ -
然后,点击头部按钮,在头部 | 值部分下输入以下内容:
Content-Type | application/json -
完成后,您的屏幕应该看起来像以下截图:
![安装和配置 Postman]()
我们现在准备好构建我们的 REST 服务,并使用 Postman 进行测试。
如何操作...
按照以下步骤创建 RESTful Web 服务:
-
首先,我们将声明
Quotes集合。创建一个名为[项目根目录]/server/model/quotes.js的文件,并添加以下代码:Quotes = new Mongo.Collection('quotes'); -
接下来,我们将添加
writeHeaders函数。创建一个名为[项目根目录]/server/api/REST.js的文件,并将以下函数添加到文件底部:function writeHeaders(self) { self.response.statusCode = 200; self.response.setHeader('Content-type', 'application/json'); self.response.setHeader('Access-Control-Allow-Origin', '*'); self.response.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); } -
我们现在将编写处理
GET请求的代码。在REST.js文件的顶部,创建以下Router.route()方法调用:Router.route('/api', { where: 'server' }) .get(function () { //write headers writeHeaders(this); //send our response... this.response.end('GET not supported\n'); });保存所有更改,确保您的应用程序正在运行,然后在 Postman 中点击发送按钮。在结果部分,点击正文选项卡和原始按钮。您应该看到以下信息:
![如何操作...]()
恭喜你,你刚刚创建了你第一个 REST 服务!
-
现在,让我们继续到
post调用。我们将假设一个POST查询是请求查看我们Quotes集合中属于特定用户的任何引用。在REST.js中,从.get()函数调用中删除最后一个分号(我们正在链式调用命令,所以你这样做很重要),并在你删除分号的地方添加以下代码:.post(function () { writeHeaders(this); var user = this.request.body.owner; if (!user) { this.response.end('No user specified...\n'); return; } var quotes = Quotes.find({ owner: user }).fetch(); this.response.end(JSON.stringify(quotes)); }) -
在我们的集合中有数据之前,测试
POST查询并没有太多意义,对吧?让我们立即通过添加对PUT的支持来解决这个问题。在你刚刚输入的代码之后(作为函数链的延续),添加以下代码:.put(function () { writeHeaders(this) var upQuote = this.request.body.update; if (!upQuote) { this.response.end('nothing to update'); } var update = Quotes.upsert({ _id: upQuote.id }, { $set: upQuote.changes }); this.response.end('Quote accepted!...\n'); });注意
注意,我们在末尾有一个分号,这意味着我们已经完成,可以开始使用我们的完整功能服务了!
-
在 Postman 中,将服务类型从
GET更改为PUT。在输入字段中,点击标有原始的按钮,并输入以下 JSON 代码:{"update":{ "changes":{"author":"Jerry Pournelle and Larry Niven", "text":"Second guessing God is an old, old game.", "owner":"me" } }}您的屏幕应该看起来像以下截图:
![如何操作...]()
-
点击发送按钮,只需几毫秒,响应区域将显示以下信息:
Quote accepted!... -
重复前面的步骤,根据需要更改引用/作者和所有者。
-
我们现在可以测试
POST查询。将服务类型从PUT更改为POST,并在输入字段中输入以下 JSON:{"owner":"me"} -
点击发送按钮,然后在输出窗口中点击美化按钮。输出将是一些格式良好的 JSON,至少包含一个条目,类似于以下截图所示:
![如何操作...]()
你刚刚在 Meteor 中编写了一个简单但功能齐全的 REST 服务。
工作原理...
这个食谱的繁重工作由 Iron Router 完成。当我们添加iron:router包时,该包会在服务器端监听所有传入的请求。我们配置 Iron Router 监听http://[yourapp.url]/api/路由的GET、POST和PUT请求。为了使 Iron Router 监听那个特定的路由,我们使用了Router.route('/api'...)方法调用。重要的是,我们声明该路由将在服务器端使用{where: 'server'}参数进行处理。
将每种类型的请求链接到.route()调用,我们使用了.get()、.post()和.put()方法调用。在这些调用中,我们可以通过引用this.request.body.[key]来读取请求中传递的 JSON 数据。
最后,我们能够通过使用 Iron Router 的response对象来发送响应,我们使用它来设置头信息并发送消息回调用客户端。
还有更多...
如往常一样,我们尽量将功能保持到最小,以减少这个食谱的信号与噪声。然而,这里已经足够让你对使用 Iron Router 的 REST 服务能做什么有一个很好的了解。如果你想要更多关于 Iron Router 中 RESTful 函数的信息和具体细节,请访问 Iron Router 指南:github.com/iron-meteor/iron-router/blob/devel/Guide.md#server-routing。
参见
-
在第二章的使用 Iron Router 创建多页应用程序食谱中,使用包进行自定义
-
本章的使用 Iron Router 创建完整的应用程序食谱
使用 Iron Router 创建完整的应用程序
从 Meteor 的几乎一开始,开发重点就放在了“智能”客户端应用程序上。我们很久以前就离开了以服务器为主的世界,因此,单页模型在 Meteor 开发堆栈中得到了极好的支持。
对于这类应用程序来说,iron:router包(atmospherejs.com/iron/router)是必不可少的。是的,从技术上讲,你可以不使用 Iron Router 构建一个多功能的、以移动端优先的应用程序,但与使用 Iron Router 相比,这将耗费更多的时间和更复杂的操作。想象一下 Iron Router 就像使用微波炉而不是在篝火旁户外烹饪。没有任何工具。在雨中。还有携带狂犬病的浣熊在你周围捣乱。好吧,好吧,可能没有这么糟糕,但你的意思很清楚——你应该使用 Iron Router。
这个食谱将指导你构建一个功能齐全的报价跟踪服务,前端使用 Iron Router 构建的单页应用程序。
准备工作
我们通常会为食谱添加很多准备工作步骤,但在这个食谱中,我们需要一次性构建所有内容作为食谱的一部分。因此,我们在这里只添加最基本的内容。
打开一个终端窗口,导航到你希望应用程序根目录所在的位置,并输入以下命令:
$ meteor create QuotesApp
$ cd QuotesApp
$ rm QuotesApp.*
$ mkdir {client,both}
$ meteor add twbs:bootstrap
$ meteor
我们有很多工作要做,所以让我们开始吧!
如何做到这一点...
这个菜谱的主要部分如下:
-
创建顶部的
navbar -
添加用户认证
-
添加
Quotes集合 -
添加 Iron Router 路由
-
创建页面过渡动画
-
显示
Quotes集合 -
添加和编辑
Quotes -
部署应用程序
各个部分之间会有一些交叉,但我们应该能够保持事情相当整洁,所以如果你想在移动过程中切换一些功能或添加一些功能,请随时这样做。
-
首先,我们需要创建顶部的
navbar。我们将使用标准的 Bootstrap
navbar元素,因为我们不想在 CSS 样式上花费大量时间。一旦你对 CSS 更加熟悉,你可以扩展或替换 Bootstrap 样式,使其成为你自己的。首先,由于这是一个面向移动端的 app,让我们设置我们的标题,并使用 Google 字体。创建一个名为
[project root]/client/header.html的新文件,并添加以下声明:<head> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"> <link href='http://fonts.googleapis.com/css?family=Raleway' rel='stylesheet' type='text/css'> </head>创建一个名为
[project root]/client/layout.html的新文件,并添加以下模板,它将作为我们导航工具栏的基础:<template name="layout"> <div class="navbar navbar-default navbar-fixed-top" role="navigation"> <div class="navbar-header"> <a class="navbar-brand" id="btn-home" href="/"> <span class="glyphicon glyphicon-book"></span></a> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav navbar-right"> <!-- Login buttons will go here --> </ul> </div> </div> <!-- App panels will go here--> </template>我们将在之后临时添加一个
body元素,以便我们可以检查显示(当我们实现iron:router时我们将移除它):<body> {{> layout}} </body>最后,我们需要添加一些微小的 CSS,以确保即将到来的所有内容都能顺利运行。创建一个名为
[project root]/client/styles.css的文件,并添加以下 CSS 声明:body {font-family: 'Raleway', sans-serif;} .page {vertical-align: top;position: absolute;width: 100%;height: 100%;background-color: #eee;top: 50px;} .page-detail {background-color: #fff;color: #555;} .glyphicon-book {font-size: 3rem;line-height: 2rem;} .toggle-edit{margin-top: 1.4rem;font-size: 2rem; background-color: #777;border-radius: 50%; border: none;padding: 1.5rem;width: 5rem;height: 5rem;} .input-group {margin-top: 1.4rem;} .quote {font-size:2rem;} .title {font-size:3rem;} .author {margin-bottom:2rem;margin-top:1rem;} .tag {margin:0.25rem;} .tags {margin-top:1rem;} .add-quote {height:9rem;} .add-quote .btn{color:#5bc0de;font-size:4rem;} -
接下来,我们需要添加用户认证。
由于我们过去有经验,我们将启用 Twitter 认证。我们还将使用一些与我们的 Bootstrap 工具栏相匹配的样式化登录按钮。打开一个终端窗口,导航到你的项目根目录,并输入以下命令:
$ meteor add accounts-twitter $ meteor add ian:accounts-ui-bootstrap-3我们现在需要将
loginButtons模板添加到我们的工具栏中。打开[project root]/client/layout.html并定位到以下注释:<!-- Login buttons will go here -->用以下代码替换这个注释:
{{> loginButtons}}保存你的更改并导航到你的 app 在浏览器中。右上角将包含一个配置 Twitter按钮。点击此按钮并根据说明配置 Twitter,就像我们在第十章的实现 OAuth 账户包菜谱中做的那样,与账户一起工作。
配置完成后,你可能想使用你的 Twitter 账户测试登录。如果一切顺利,我们可以继续到我们应用程序的核心部分。
-
让我们添加
Quotes集合。我们肯定准备好添加我们的
Quotes集合,但我们希望根据用户是否登录来控制发送到客户端的记录。为了实现这一点,我们需要移除autopublish。在终端窗口中,在我们的项目根目录下,输入以下命令:$ meteor remove autopublish接下来,创建一个名为
[project root]/both/model.js的文件,并将以下行添加到该文件中:Quotes = new Mongo.Collection('quotes');通常,我们会在客户端文件中添加一个
subscribe命令,在服务器端文件中添加一个publish命令,但逻辑如此简单,所以我们只需直接将它们添加到本例中的model.js文件中。将以下代码添加到model.js并保存你的更改:if (Meteor.isClient) { Meteor.subscribe('quotes'); } else { Meteor.publish('quotes', function () { return Quotes.find({ owner: this.userId }); }) } -
接下来,我们需要添加我们的 Iron Router 路由。
当你添加 Iron Router 时,在运行的 Meteor 实例中需要的更改相当重大。技术上,你可以在不停止应用的情况下添加 Iron Router,但我们建议在安装和配置 Iron Router 时停止你的 Meteor 应用。
一旦你停止了你的应用,在终端窗口中输入以下命令:
$ meteor add iron:router现在我们声明默认布局模板。创建一个名为
[project root]/both/router.js的文件,并添加以下行:Router.configure({ layoutTemplate: 'layout' });现在我们的布局模板已被 Iron Router 调用,我们不再需要之前在
layout.html文件中临时放置的 body 元素。打开[project root]/client/layout.html并删除整个<body>...</body>段落。我们在
layout.html中为我们的应用页面预留了一个占位符,我们需要用传统的 Iron Router yield 指令来填充。在layout.html中找到以下行:<!-- App panels will go here-->将该行替换为以下代码:
{{> yield}}我们需要创建两个路由,现在就来做这件事。打开
[project root]/both/router.js并添加以下Router.map声明:Router.map(function(){ this.route('main', { path: '/', template:'main' }); this.route('detail', { path: '/quote/:_id', template:'detail', data: function () { return Quotes.findOne({_id: this.params._id}); } }); });我们已经创建了路由,现在我们需要为这些路由创建占位符模板。创建一个名为
[project root]/client/main.html的文件,并添加以下模板:<template name="main"> <div class="page"> <h1>this is the main page</h1> </div> </template>创建一个名为
[project root]/client/detail.html的文件,并添加以下模板:<template name="detail"> <div class="page page-detail"> <h1>this is the detail page</h1> </div> </template>现在,你可以重新启动你的应用(在你的终端中输入
meteor命令)并测试路由。当你导航到http://localhost:3000和http://localhost:3000/quote/1234时,你应该能看到以下截图中的类似页面:![如何做…]()
-
我们现在将创建页面转换动画。
在那里有一个名为
momentum的优秀 Atmosphere 包。通过 Percolate Studios 创建的 Iron Router 特定包,我们可以轻松地将momentum扩展到 Iron Router 路由转换。在终端窗口中,输入以下命令:$ meteor add percolate:momentum-iron-router创建一个名为
[project root]/client/transitions.js的文件,并将以下转换声明作为一个Template辅助函数添加:Template.layout.helpers({ transition: function () { return function (from, to, element) { if (to.template=="main") return 'left-to-right'; return 'right-to-left'; } } });我们还需要用
momentum模板辅助函数包围我们的{{> yield}}包含。打开layout.html并添加以下包装器:{{#momentum plugin='iron-router' options=transition}} {{> yield}} {{/momentum}}保存您的更改,并再次导航到
http://localhost:3000/page/1234。一旦页面加载,点击页面左上角的书籍图标。主页面应该滑动到合适的位置。如果您在浏览器中点击后退按钮,将发生反向动画。 -
我们现在准备显示
Quotes集合。我们将在主模板中为每个引用显示一些简要信息,然后将其扩展到详细模板中的更多细节。让我们从主模板开始。
打开
[project root]/client/main.html,删除<h1>占位符标签,并添加以下代码,该代码遍历Quotes集合并为每个创建分组项:<template name="main"> <div class="page"> <div class="list-group"> {{#each quotes}} <a class="list-group-item quote-title"> <span class="glyphicon glyphicon-menu-right pull-right"></span> <h4 class="list-group-item-heading">{{title}}</h4> {{#each tagsFormatted}} <div class="btn btn-xs btn-primary">{{tag}}</div> {{/each}} </a> {{/each}} </div> </div> </template>我们需要添加几个辅助器来支持之前创建的模板,因此创建一个名为
[project root]/client/main.js的文件,并添加以下代码:Template.main.helpers({ quotes: function(){ return Quotes.find().fetch(); }, tagsFormatted: function(){ var tags = this.tags; if (!tags) return []; return _.map(tags.split(','),function(d){ var retTag = '#' + d; return {tag:retTag}; }); } });我们还需要一个
click事件处理程序,它将在我们点击引用分组项时将我们带到detail页面模板。将以下代码添加到main.js文件的底部:Template.main.events({ 'click .quote-title' : function(e){ Router.go('/quote/'+this._id); } });保存您的更改。现在让我们以编程方式向
Quotes集合添加几个条目。在浏览器中导航到http://localhost:3000/。确保您已使用 Twitter 账户登录,打开浏览器控制台,并执行以下命令两三次:Quotes.insert({owner:Meteor.userId(), title:'quote title', quote:'quote body', author:'quote author', tags:'test,tags,field'})在这样做之后,您的屏幕应该看起来类似于以下截图:
![如何做…]()
如果您点击其中一个项目,通用的详细页面应该滑动到视图中。现在让我们处理
detail页面模板,以便它能够显示引用的详细信息。打开[project root]/client/detail.html文件,并用以下内容替换模板内容:<template name="detail"> <div class="page page-detail"> <div class="container"> {{#if editing}} {{else}} <h3 class="title">{{title}}</h3> <div class="container quote">{{quote}}</div> <div class="row container author"> <div class="pull-right">-{{author}}</div> </div> <div class="row container tags"> {{#each tagsFormatted}} <div class="btn btn-xs btn-primary tag">{{tag}}</div> {{/each}} </div> {{/if}} </div> </div> </template>保存您的更改,并点击其中一个占位符引用。您应该看到一个屏幕出现,看起来类似于以下截图:
![如何做…]()
标签没有显示出来,因为我们还没有添加必要的辅助器。创建一个名为
[project root]/client/detail.js的文件,并添加以下辅助器声明:Template.detail.helpers({ tagsFormatted: function () { var tags = this.tags; if (!tags) return []; return _.map(tags.split(','), function (d) { var retTag = '#' + d; return { tag: retTag }; }); } });在浏览器中回顾一下;标签现在应该出现在详细页面模板上。
-
现在我们已经处理好了显示,让我们添加编辑和插入新
quote对象的能力。首先,让我们调整
detail.html文件并添加一个编辑切换按钮。打开detail.html,在最后一个</div>标签之前添加以下div元素:</div> <div class="container"> <div class="btn btn-danger toggle-edit pull-right"> <span class="glyphicon {{editbuttonstate}}"></span> </div> </div> </div> </template>接下来,让我们添加用于编辑/插入的输入字段。在相同的
detail.html文件中,在{{#if editing}}部分添加标题输入字段。使用 Bootstrapinput-group样式,如下所示:{{#if editing}} <div class="input-group input-group-lg"> <span class="input-group-addon" id="addon-title">Title</span> <input type="text" id="input-title" class="form-control" placeholder="title..." value="{{title}}"> </div> {{else}}接下来是
quote正文的input-group元素。在上述代码片段中title分组之后添加以下内容:</div> <div class="input-group"> <span class="input-group-addon" id="addon-quote">Quote</span> <textarea name="input-quote" id="input-quote" rows="4" class="form-control">{{quote}} </textarea> </div> {{else}}我们需要一个
input-group元素用于作者。将其添加到quote分组下方,如下所示:</div> <div class="input-group"> <input type="text" id="input-author" class="form-control" placeholder="author..." value="{{author}}"> <span class="input-group-addon" id="addon-author">Author</span> </div> {{else}}最后但同样重要的是,对于模板来说,标签也很重要。在
author组下方,添加以下代码:</div> <div class="form tags"> <textarea name="input-tags" id="input-tags" class="form-control" rows="5">{{tags}}</textarea> </div> {{else}}我们需要添加一些额外的助手。打开
detail.js文件,在tagsFormatted助手块之后立即添加以下两个助手,在Template.detail.helpers()方法中(别忘了在顶部添加额外的逗号!):}, editbuttonstate: function () { return (Session.get('editing')) ? 'glyphicon-ok' : 'glyphicon-pencil'; }, editing: function () { return (Session.equals('editing', true)); } });现在我们需要添加在编辑和显示之间切换的事件处理器。我们还需要添加一个提交按钮。所以,让我们一石二鸟,给我们的切换按钮添加在状态变化时变成提交按钮的能力。在
detail.js文件的底部,添加以下 Templateevents声明:Template.detail.events({ 'click .toggle-edit': function (e) { e.preventDefault(); var editflag = Session.equals('editing', true); if (editflag && Meteor.userId()) { var upQuote = {}; upQuote.title = $('#input-title').val(); upQuote.quote = $('#input-quote').val(); upQuote.author = $('#input-author').val(); upQuote.tags = $('#input-tags').val(); upQuote.owner = Meteor.userId(); Quotes.upsert({ _id: this._id }, { $set: upQuote }); } if (!editflag && !Meteor.userId()) return; Session.set('editing', (!editflag)); } });保存你的更改,导航到显示单个引言,然后点击右下角的铅笔图标。你可以将通用引言更改为特定引言,如下面的截图所示:
![如何操作…]()
点击勾选标记,你的更改应该会保存,如下面的截图所示:
![如何操作…]()
我们已经处理了编辑;现在我们需要允许用户添加新的引言。我们将使用的逻辑将依赖于我们的编辑。当点击“新引言”按钮时,我们将在
Quotes集合中立即创建一个新的quote对象,然后导航到编辑屏幕。打开[项目根目录]/client/main.html文件,在最后一个{{/each}}标签之后立即添加以下按钮组,但仍然在list-group</div>标签内:{{/each}} <a href="#" class="list-group-item add-quote"> <div class="btn btn-lg center-block"> <span class="glyphicon glyphicon-plus"></span> </div> </a> </div> </div> </template>当点击此按钮时,我们希望在
Quotes集合中插入一个新的对象,然后导航到编辑屏幕。打开main.js文件,在Template.main.events()方法调用中添加以下事件处理器,紧接在.quote-title事件处理器之后(再次,别忘了逗号!):}, 'click .add-quote' : function(e){ if (!Meteor.userId()) return; Quotes.insert({owner:Meteor.userId()}, function(e,d){ if (!e){ Session.set('editing',true); Router.go('/quote/'+d); } }); } });保存所有更改,在浏览器中导航到
http://localhost:3000,你应该会在引言组底部看到一个大的蓝色加号。点击按钮,你将被带到类似于以下截图的新鲜、干净的编辑屏幕。在这里,你可以输入一个新的引言:![如何操作…]()
-
在所有功能就绪后,我们就可以部署我们的应用了。
我们可以在任何我们想要的地方部署我们的应用(请参阅第一章中的许多部署配方,优化你的工作流程以获取详细信息),但在这个配方中,我们将选择最简单/最快的方式来部署,即直接部署到 Meteor 服务器。
使用Ctrl + C在终端停止你的应用运行,并执行以下命令,将
[your-test-url]替换为你想要的任何内容:$ meteor deploy [your-test-url].meteor.com在我们的案例中,我们使用了以下命令:
$ meteor deploy packt-quotes.meteor.com等待你的应用构建并上传,你应该会得到一个熟悉的消息,类似于以下内容:
Deploying to packt-quotes.meteor.com. Now serving at http://packt-quotes.meteor.com导航到您新部署的应用程序,如果需要的话配置 Twitter(您需要一个指向新部署 URL 的新 Twitter 应用 ID),登录,并添加一些引语。完成之后,您的应用程序应该看起来类似于以下截图:
![如何做…]()
恭喜!您已经通过八个步骤创建并部署了一个完整的应用程序。您应该为自己感到骄傲。去拿一杯汽水或一块庆祝的披萨吧。您应得的!
它是如何工作的…
由于前面的每个步骤都涉及其他章节的食谱,我们不会过多关注代码的细节。相反,我们将概述每个步骤的作用,并在需要的地方添加细节。
我们首先添加了我们的顶部菜单/导航栏。使用 Bootstrap 内置的navbar和navbar-fixed-topCSS 类,我们几乎不费吹灰之力就将navbar定位在屏幕顶部。我们在头部文件中添加了一个网络字体(感谢,谷歌!)并设置了在移动设备上使用的参数。
对于用户身份验证,我们依赖于默认的 Meteor 账户包(特别是accounts-twitter包)。对于格式化,我们使用了由 Ian Martorell 创建的优秀设计的Bootstrap包。他为我们的重活儿做了很多工作,所以我们只需要添加这个包,并将我们的{{> loginButtons}}包含放置在正确的位置。
我们在客户端和服务器都能看到的文件中声明了Quotes集合,就像往常一样,并添加了一个publish函数,该函数只会将创建者登录用户创建的结果发送到客户端。
我们添加了 Iron Router 包,并利用该包的Router.map()和layoutTemplate助手来实现基线布局模板、主模板和详情模板。
我们接下来利用了 Percolate Studios 的一点点魔法,称为momentum。他们为momentum创建了一个插件,允许您将{{>yield}}指令包装起来,并将其指向一个模板助手,在那里我们执行一些快速逻辑,比如,“如果我们要去主页,从左到右走。如果不是,从右到左走。”这是一个设计精良的包,并且非常容易实现!
我们接下来的步骤(显示、编辑和创建引语)可以被认为是食谱的核心,但由于所有脚手架已经就位,并且因为这种类型的功能在 Meteor 中实现起来非常容易,我们只是简单地使用了{{#each…}}指令、一些响应式数据绑定和几个click事件处理器来显示和编辑Quotes集合中的记录。
事实上,整个项目中最复杂的部分是将tags属性解析成数组使用_.map()。想想看。我们整个应用中最技术性“挑战”的部分是解析一个字符串。Meteor 真的很棒,不是吗?
我们最后的步骤是部署,这也很简单,因为我们已经在第一章中做过几次了。
总体来看,从头开始构建整个应用似乎很令人畏惧。将其分解为离散的步骤不仅使其变得简单,而且实际上还很有趣。我们希望你喜欢这个过程,我们希望你会修改这个食谱,扩展它,并创建一些快速、有用的应用!
还有更多...
如果你仔细检查了这个食谱的最终产品,你可能已经注意到一些功能缺失和一些需要进一步改进的地方。例如,目前还没有删除引言的方法,我们无法按标签/主题/作者排序,而且我们实际上在部署时没有移除不安全的包。
换句话说,我们可以从前面章节中应用很多其他的食谱来改进这个应用。我们将把这个任务留给你,当你构建和扩展你自己的应用时,我们迫不及待地想看看你能想出什么!
参见
-
在第一章中,优化你的工作流程的将测试应用部署到 Meteor食谱
-
在第二章中,使用包进行自定义的添加 Meteor 包食谱
-
在第三章中,构建出色的用户界面的使用 Bootstrap 构建平滑界面食谱
-
在第十章中,使用账户的实现 OAuth 账户包食谱
将应用部署到移动设备
如果你只是将单页应用以网页的形式提供,那么构建具有各种酷炫功能的单页应用有什么好处呢?这个食谱将带你完成所有必要的步骤,以测试和部署你的应用到 iOS 设备。
准备工作
你需要一个应用来完成这个食谱。我们将使用本章前面找到的使用 Iron Router 创建完整应用食谱中创建的应用。如果你没有你想部署的应用,请先完成那个食谱,然后回到这个食谱来学习如何部署你的应用。
如何操作…
部署你的应用有两个部分。第一部分是在使用已经部署到生产 URL 的服务端代码的同时在设备上测试它。第二部分是为部署到 App Store 准备应用构建。
-
首先,让我们设置我们的移动应用构建环境。要将应用部署到 iOS 设备,你需要一个 Apple 开发者许可证,一台运行 Mac OS X 的机器,以及在该机器上安装的 Xcode。
要获取 Apple 开发者许可证,请访问
developer.apple.com/programs/并按照链接/说明加入 iOS 开发者计划。一旦您加入了该计划,请从
developer.apple.com/xcode/downloads/下载并安装 Xcode。您需要正确配置 Xcode,包括根据需要添加证书和移动配置文件。这涉及一个学习曲线,我们强烈建议您在继续之前花些时间学习开发、认证和 App Store 提交流程。打开一个终端窗口,导航到您的应用目录,并输入以下命令:
$ meteor install-sdk ios这可能需要一段时间,但请确保 SDK 已正确安装,并根据指示修复任何错误或未完成的安装项。
完成后,输入以下命令:
$ meteor add-platform ios您现在已正确配置,可以在 iOS 设备上测试/部署您的应用。
-
现在我们来测试我们的应用在 iOS 设备上的运行情况。
通过 USB 线将您的 iOS 设备连接到您的计算机。在命令行中输入以下命令:
$ meteor run ios-device --mobile-server [your-app-url.com]这里,
[you-app-url.com]被替换为您部署的服务器实例的 URL。在我们的例子中,我们使用了以下命令(当然,所有内容都在一行中):$ meteor run ios-device --mobile-server packt-quotes.meteor.com应用将被构建和打包,然后
Xcode将打开。一旦应用在Xcode中编译完成,您将在Xcode应用程序的顶部中间看到以下消息:![如何操作…]()
从下拉菜单中选择您的 iOS 设备,然后点击显示在此截图中的播放按钮。
![如何操作…]()
确保您的设备已解锁,并在进行一些下载和代码推送活动后,您的设备上会出现带有 Meteor 标志的图标。应用将快速打开;然后,经过短暂的延迟,您的应用将像在浏览器中拉起一样运行,但它是在您的设备上作为一个完整的应用程序运行的!以下是
Quotes应用在 iPhone 上运行的截图:![如何操作…]()
-
下一步是构建一个独立的移动应用文件。
一旦您测试了一切并且一切按预期工作,您将想要创建一个实际的应用文件,您可以将其提交到 Apple App Store。当您在移动设备上安装测试应用时,Meteor 会自动为您的应用创建几个资产。您可以通过导航到
[项目根目录]/.meteor/local/cordova-build/resources文件夹来查看这些资产,如下面的截图所示:![如何操作…]()
您可以使用这些资源或根据需要替换它们。一旦您放置了正确的图标和启动画面,您将需要创建一个名为
[项目根目录]/mobile-config.js的文件,并添加以下参数,至少包括以下内容:App.info({ name: '[yourappname]', description: 'Description of my app', version: '0.0.1' }); App.icons({ 'iphone': 'resources/icons/icon-60.png', 'iphone_2x': 'resources/icons/icon-60@2x.png' }); App.launchScreens({ 'iphone': 'resources/splash/Default~iphone.png', 'iphone_2x': 'resources/splash/Default@2x~iphone.png', 'iphone5': 'resources/splash/Default-568h@2x~iphone.png' });您当然可以更改此文件中的任何内容,并且您还可以添加其他几个参数来进一步调整安装。请参阅
[项目根目录]/.meteor/local/cordova-build/config.xml文件以获取一个很好的选项列表。一旦你对配置满意,导航到根应用目录,并在终端中输入以下命令(当然,所有内容都在一行中):
$ meteor build [your-output-directory] --server=[your-app-url]例如,我们的
Quotes应用的命令看起来会是这样(当然,所有内容都在一行中):$ meteor build ~/builds/ --server=packt-quotes.meteor.com一旦完成,你可以在
Xcode中打开项目目录(在我们的例子中,项目目录会是~/builds/ios/project),并随心所欲地进行配置。一旦一切正确并且按照你的期望运行,你可以签名应用并将其提交到苹果应用商店。
它是如何工作的……
Meteor 利用cordova库、Xcode和你的 Mac 操作系统来打包、编译和打包你的应用程序。重点是将其放入Xcode,在那里你可以像操作任何原生应用一样操作应用,进行测试、部署或提交到应用商店。
关于在Xcode中做什么以及在提交到苹果应用商店时的具体操作超出了本书的范围。如果你想了解更多关于 Meteor 特定应用构建的信息,请访问github.com/meteor/meteor/wiki/How-to-submit-your-iOS-app-to-App-Store。
还有更多……
第一次做任何事情都是脑力劳动。构建和部署移动应用也不例外。以下是一些帮助你在出现问题时进行故障排除的技巧:
-
进行“拨号音”测试:如果你的应用正在构建,但在设备上“启动”需要花费很长时间,或者你只看到一个空白屏幕,首先尝试确保你的所有网络和 Xcode 配置都是正确的。你可以通过使用
meteor create my-test-app创建一个新的 Meteor 项目,并将这个裸机应用部署到你的移动设备上来做这个测试。如果它能正常启动,那么你已经成功得到了“拨号音”,可以开始找出导致你加载缓慢的包或代码片段。 -
铁路路由配置必须准确:如果你使用 Iron Router 并且你的 Iron Router 路由配置不正确,应用通常会拒绝加载。如果你遇到问题,实现最基本的路由,没有花哨的代码,并看看你是否能在设备上启动路由。再次强调,从简单开始。
-
使用模拟器:你不必总是将应用推送到实际设备。使用
meteor run ios或meteor run android命令几乎可以在瞬间启动一个模拟器,在那里你可以比在实际设备上更快地测试和调试。 -
使用 Xcode 的日志:如果有问题,请检查日志,日志位于 Xcode 的右下角面板中。你还可以在应用在你的设备上运行时使用 Xcode 设置断点,这可以帮助你隔离潜在的问题。
除此之外,坚持下去。我们可以向你保证,一旦你正确配置,将内容部署到移动设备上将非常简单,成功率极高。不要放弃!相信很快就会“点击”成功,你将不会在构建移动应用程序时遇到任何问题/焦虑。实际上,当你看到你的应用程序在 App Store 或人们的设备上时,这会变得很有趣,并且极具成就感。值得学习曲线!
参见
- 本章中的 使用 Iron Router 创建完整应用程序 菜谱
添加社交分享
随着将社交媒体集成到你的应用程序中的需求不断增长,你需要了解如何直接从你的应用程序中发送/发布/分享。这个菜谱将向你展示如何将 Twitter 发布(推文)添加到你的应用程序中。
准备工作
我们将使用之前在本章中找到的 使用 Iron Router 创建完整应用程序 菜单中的 Quotes 应用程序。请在继续之前完成该菜谱。
我们还将使用 npm 的 twit 模块来完成这个菜谱。有关每个步骤的详细步骤和解释,请参阅第十一章 利用高级功能 中 处理异步事件 菜谱的第一部分,利用高级功能。
快速查看以下步骤,你需要安装和配置 twit 模块:
-
执行以下终端命令:
$ meteor add meteorhacks:npm $ meteor -
打开
packages.json文件并添加以下代码:{ "twit" : "1.1.20" } -
运行以下终端命令:
$ meteor -
创建
[项目根目录]/server/config-twit.js文件并添加以下配置:Twitter = Meteor.npmRequire('twit'); Twit = new Twitter({ consumer_key: '…', consumer_secret:'…', access_token: '…', access_token_secret: '…' });
在我们的 Twitter 密钥、令牌和秘密都到位,并且 twit 已经初始化后,我们准备开始发推文!
如何做到这一点…
我们需要创建一个服务器方法,更新我们的用户界面,并给出一个引用已被分享的提示。多亏了 twit,对 Twitter API 的实际调用相当简单。让我们开始吧。
-
首先,让我们创建
twuote服务器方法。创建一个名为
[项目根目录]/server/tweets.js的文件,并添加以下Meteor.methods声明:Meteor.methods({ twuote : function(id){ } });我们首先检查用户是否已登录,以及是否传递了有效的
quote变量的id参数。在twuote函数调用中,添加以下行:if (!id || !this.userId) return; var quote = Quotes.findOne({_id:id}); if (!quote || !quote.quote || !quote.author) return;现在,我们需要操作
quote和author字符串,确保其长度不超过140个字符。在之前插入的行下方,添加以下字符串操作逻辑:var tweet = '"'; if (quote.quote.length>138){ tweet += quote.quote.slice(0,135); tweet += '..."'; } else { tweet += quote.quote + '" --' + quote.author; } if (tweet.length>140){ tweet = tweet.slice(0,137) + '...'; }在整理完引言后,我们准备发送推文!在字符串操作逻辑下方,添加以下
Twit.post()函数和包装回调函数:Twit.post('statuses/update', { status: tweet }, Meteor.bindEnvironment(function(err, data, response) { Quotes.update({_id:id},{$set:{tweeted:true}}); }) ); return tweet;我们的方法已经完成。让我们修改我们的用户界面。
-
我们将通过添加分享按钮来开始修改我们的用户界面。
打开
[项目根目录]/client/detail.html文件,并在toggle-edit按钮下方,在父容器元素内添加以下按钮:<div class="container"> <div class="btn btn-danger toggle-edit pull-right"> ... </div> <div class="btn pull-left {{tweetColor}}" id="btn-tweet">{{tweetText}} </div> </div>我们希望给我们的按钮一个轻微的下推,所以请在
[项目根目录]/client/styles.css文件底部添加以下 CSS 指令:#btn-tweet{margin-top: 2.4rem;}我们还需要为我们的按钮添加几个额外的辅助函数。打开
[项目根目录]/client/detail.js,并将以下两个辅助函数添加到Template.detail.helpers代码块的底部(就像往常一样,别忘了在行前加上逗号):editing: function () { ... }, tweetText: function (){ return (this.tweeted)?'shared':'share...'; }, tweetColor: function (){ return (this.tweeted)?'btn-success':'btn-info'; } });剩下的只是连接点击事件。打开
[项目根目录]/client/detail.js文件,找到Template.detail.events声明,并在括号内添加以下点击事件处理器(别忘了在处理器前的那一行加上逗号):}, 'click #btn-tweet' : function (e) { if (this.tweeted) return; Meteor.call('twuote',this._id); } });保存所有更改,在浏览器中导航到你的应用(通常是
http://localhost:3000),点击一个引语以打开详情页面,然后点击以下截图所示的 分享... 按钮:![如何操作…]()
给你的服务器一点时间来发送推文。分享... 按钮将变为绿色,文本将更改为 已分享。你的推文将在 Twitter 上实时显示,如下面的截图所示:
![如何操作…]()
它是如何工作的…
重要的代码都位于 [项目根目录]/server/tweets.js 文件中的 Meteor.methods({}) 声明中。
我们调用 Twit.post(),它接受一个回调作为最后一个参数。由于 twit 是一个 Node 模块,这个回调是异步的。因为它异步,我们需要在调用期间保留环境当前状态的方法,以及当回调执行时将进程放回 事件循环 的方法(有关完整解释,请参阅第十一章 处理异步事件 的 Handling asynchronous events 菜谱,Leveraging Advanced Features)。
我们通过将回调包裹在 Meteor.bindEnvironment() 中来完成这两项任务。当回调触发时,我们更新 Quotes 集合,为相应的引语添加 {tweeted : true} 属性。
这个属性反过来导致我们的 UI 使用绿色按钮更新,并禁止额外的推文(在 [项目根目录]/client/detail.js 的 click 事件 处理器中检查)。
就这样,我们添加了社交分享功能。从这里,你可以将这个菜谱应用到其他社交平台,使其更无缝地集成到你的应用中。
还有更多…
我们必须小心,不要在生产环境中使用前面的代码!我们移除了所有的安全和错误处理。没有验证,示例是为单个服务器端用户硬编码的。如果你在生产应用中以这种方式使用此代码,会发生一些不太愉快的事情,而且这不是我们的责任(不允许责备!)。
相反,我们建议您详细阐述这些元素,特别是允许使用已登录用户的访问令牌而不是硬编码的用户令牌。
只需稍加润色,您就可以轻松使用此代码来补充您的 Meteor 应用程序,并扩展其功能以支持 Facebook、Instagram、LinkedIn、Pinterest 等等。
小贴士
有关使用 twit 模块可以做什么的完整说明,请访问 github.com/ttezel/twit。
参见
-
在第二章“使用包进行自定义”中的添加 Meteor 包配方
-
在第十一章“利用高级功能”中的处理异步事件配方
-
本章中的使用 Iron Router 创建完整应用配方































































































浙公网安备 33010602011771号