五、Meteor 风格的数据

我们几乎已经完成了借阅图书馆应用,无需过多担心数据的存储方式。 Meteor 的数据缓存和同步方法是有意构建的,使构建应用的这一部分尽可能简单,这样你就不用花大量时间在数据库连接、查询和缓存上,而可以专注于编写一个伟大的程序。

但是,我们确实想回顾一下方法论,确保我们对 Meteor 如何处理数据有一个坚实的理解,这样我们就可以执行一些常见的优化,并更快地构建我们的应用。

在本章中,您将学习以下主题:

  • MongoDB 和面向文档的存储
  • 广播变化-Meteor 如何使您的 web 应用反应
  • 配置发布者-如何精简和保护您的数据

文档存储

Meteor 使用 MongoDB (minimongo)的一个版本来存储模型中的所有数据。 它能够使用任何其他的 NoSQL/面向文档的数据库,但 MongoDB 默认带有 Meteor 安装。 这个功能使您的程序更简单,更容易编写,并且在快速、轻量级的数据存储方面工作得非常好。

但是为什么不使用关系数据库呢

传统上,数据是使用关系模型存储的。 关系模型及其所有相关规则、关系、逻辑和语法是现代计算不可或缺的重要组成部分。 关系数据库的严格结构,以及对每个记录、关系和关联的精确要求,为我们提供了快速搜索、可伸缩性和深度分析的可能性。

然而,这种精确并不总是必要的。 例如,在我们的借阅库中,使用一个成熟的关系数据库就有点过分了。 事实上,在某些情况下,拥有一个灵活的数据存储系统更有效,您可以快速扩展该系统,而无需进行重大的编码。

举个例子,如果你想添加一个新属性list对象,这将是更简单的数据库添加新的属性,让担心,而不必重构数据库代码,添加一个新列,更新你所有的 SQL 语句和触发器,并确保所有先前的记录新财产。

这就是面向文档存储发挥作用的地方。 在面向文档的存储系统中,数据存储由一组键值对的文档组成。 那份文件的结构是什么? 数据存储并不真正关心。 它可以包含几乎任何东西,只要每条记录有一个唯一的键,以便它可以被检索。

在一个文档条目中,你可以有一个非常简单的文档。 也许是键值对。

{name:phone_number}

然后,在另一个文档条目中(在同一个数据存储中),您可以有一个复杂的对象,具有嵌套数组、嵌套对象等等。

{ people: [
  {firstname:"STEVE", lastname:"Scuba", phones :[
    {type:cell, number:8888675309},
    {type:home, number:8005322002}]
  },
  {firstname:...
    ...
  }]
}

见鬼,它可能是威廉·莎士比亚的未删节作品。 这并不重要。 只要数据存储能够获取该文档并为其分配唯一的键,就可以存储该文档。

您可能已经猜到,结构的缺乏可能会降低查询、排序和操作这些文档的效率。 但这没关系,因为我们主要关心的是编码的便捷性和开发速度,而不是效率。

此外,因为我们的应用只有几个核心功能,所以我们可以快速确定最常使用的查询,并围绕这些查询优化文档模式。 在某些情况下,这使得面向文档的数据库实际上比传统的关系数据库执行更好

提示

有一些非常复杂的面向文档的存储解决方案,有些人可能会认为它们与标准关系数据库一样有效,甚至更有效,但这些讨论超出了本书的范围。

考虑到面向文档存储系统的灵活性,它非常适合快速更改,Meteor 提供的基础库使我们不必担心连接或结构。 我们所要做的就是对如何检索、添加和修改这些文档有一个高层次的了解,我们可以把剩下的留给 Meteor。

MongoDB

mongodb 是一个开源的 NoSQL(不仅仅是 SQL)数据库。 它提供了复杂的特性,如索引、链接和原子操作,但其核心是面向文档的存储解决方案。

提示

如需了解更多 MongoDB 信息,请访问官方网站http://www.mongodb.org

使用简单的命令,我们可以看到哪些记录(哪些文档)是可用的,将这些记录转换为 JavaScript 对象,然后保存那些更改过的对象。 把 MongoDB 记录想象成一个实际的文本文档:

  1. 找到文档,并打开它进行编辑(Meteor 等效:lists.find (...))。
  2. 对文档进行更改(Meteor 等效:lists.update({...}))。
  3. 保存文件(使用.update()功能自动完成)。

它不是那么简单,你需要有很多的语法学习如果你要考虑自己的专家 MongoDB,但是你可以清楚地看到一个简单,干净的面向文档的方法:查找/创建一个记录,修改,保存/更新记录到数据存储。

我们需要讨论最后一个概念,以帮助您更好地理解 MongoDB 如何工作。 它被称为数据库,但更容易将其概念化为文档的集合。 集合被索引并且可以快速访问,但它仍然是一个集合,而不是一组关系表/实体。 就像你会想到一个文件夹在你的硬盘上,你保存所有的文本文件,想想 MongoDB 作为一个文件的集合,所有的文件都是可访问的,可以“打开”,改变,和保存。

使用直接命令

为了更好地理解 MongoDB 的工作方式,让我们在命令行中玩一些有趣的。

  1. 首先,确保您的应用已经启动并正在运行(打开一个终端窗口,cd~/Documents/Meteor/LendLib目录,并执行meteor命令)。 接下来,打开浏览器到http://localhost:3000
  2. Now, you will want to open an additional terminal window, cd to the ~/Documents/Meteor/LendLib directory, and run the following command:

    ``` meteor mongo

    ```

    你应该会看到类似如下截图的消息:

    Using direct commands

现在,您已经为您的 Lending Library 应用连接到了正在运行的 MongoDB 数据库。 让我们来看看几个命令。

  1. 首先,让我们打开帮助屏幕。 输入以下命令,点击,输入:

    ```

    help

    ```

  2. You'll get a list of commands, and a brief explanation of what each one does. One in particular will show us even more commands we can use: db.help(). This will give us a list of database-related commands. Type the following into your terminal window, and press Enter:

    ```

    db.help()

    ```

    不要被可能的命令数量所压倒。 你不需要知道所有这些,除非你想成为 MongoDB 的专家。 你只需要知道一些,但四处看看不会伤害任何人,所以让我们继续。

  3. 如前所述,文档在 MongoDB 中存储在一个称为集合的逻辑分组中。 我们可以直接看到这一点,并在终端窗口直接查看我们的列表集合。 要查看所有可用集合的列表,请输入以下命令:

  4. 在响应中,您将找到您的借阅图书馆收藏的名称:lists。 让我们来看看lists系列。 输入如下:

    ```

    db.getCollection('lists')

    ```

  5. Well, that wasn't very exciting. All we got back was meteor.lists. We want to be able to perform some queries on that collection. So this time, lets assign the collection to a variable.

    ```

    myLists = db.getCollection('lists')

    ```

    看起来我们得到了和上次一样的结果,但我们得到的比上次多得多。 我们现在已经将lists集合赋值给变量myLists。 因此,我们可以在终端窗口中运行命令,就像在 Meteor 代码中一样。

  6. Let's get the Clothes list, which currently doesn't have any items in it, but still exists. Enter the following command:

    ```

    Clothes = myLists.findOne({Category:"Clothes"})

    ```

    这将返回一些非常基本的 JSON。 如果您仔细查看,您将能够看到以"items" : [ ]表示的空项目数组。 你还会注意到一个_id键值,旁边有一个很长的数字,类似如下:

    "_id" : "520e4f45-8469-47b9-8621-b41e60723de0",

    我们没有添加_id。 MongoDB 为我们创建了它。 这是一个唯一的密钥,所以如果我们知道它,我们就可以在不影响其他文档的情况下对该文档进行更改。 我们实际上在我们的借阅库应用中在多个位置使用这个。

如果你看看里面的~/Documents/Meteor/LendLib/LendLib.js,你会看到下面的函数添加一个项目到一个列表:

function addItem(list_id,item_name){
  if (!item_name&&!list_id)
    return;
  lists.update({_id:list_id},
  {$addToSet:{items:{Name:item_name}}}); 
}

注意,当我们调用lists.update()函数时,我们通过_id确定要更新哪个文档。 这确保了我们不会意外地更新多个文档。 例如,如果您要给两个列表提供相同的类别名称(例如,"DVDs"),并使用类别作为选择器({Category:"DVDs"}),那么您将对两个类别列表采取操作。 相反,如果您使用_id,它只会用匹配的_id更新惟一的文档。

回到终端,我们现在将变量myLists赋给了我们的lists集合,并将Clothes变量赋给了代表Clothes列表的lists集合中的文档。

请注意当前浏览器中的Clothes列表。

Using direct commands

让我们继续,并将我们最喜欢的衬衫添加到Clothes列表。 我们将直接在终端窗口中进行操作。 输入以下命令:

>myLists.update({_id:Clothes._id},{$addToSet:{items:{Name:"Favorite Shirt"}}})

该命令使用Clothes._id作为选择器更新myLists,并调用$addToSet,使用Name:"Favorite Shirt"添加项。 Meteor 更新需要几秒钟,但你很快就会看到你最喜欢的衬衫现在添加到列表中。

Using direct commands

如果您重新运行Clothes赋值命令Clothes = myLists.findOne({Category:"Clothes"}),您现在将看到items数组中有您喜欢的衬衫的条目。

我们可以使用带有不同参数的.update()函数($pull用于删除,$set用于更新)来轻松地更新或删除一个项目。

提示

对于代码示例,请查看LendLib.js中的removeItem()updateLendee()函数。

更多关于 MongoDB 命令的深入教程,请访问http://mongodb.org,并点击TRY IT OUT

现在,我们已经介绍了一些可以直接实现的命令,让我们回顾一些LendLib.js代码,并讨论跟踪集合更改的响应式代码。

广播改变

通过使用发布/订阅模型,Meteor 一直在寻找集合和Session变量的变化。 当发生更改时,将广播(或发布)更改事件。 回调函数正在监听(或订阅)正在广播的事件,当它订阅的特定事件被发布时,函数中的代码被激活。 或者,数据模型可以直接绑定到 HTML/Handlebars 模板的部分,这样当发生更改时,HTML 就会重新呈现。

已发布事件

那么,什么时候发布事件呢? 正如前面提到的,当模型中有变化时,事件被广播。 换句话说,当一个集合或变量被修改时,Meteor 发布适当的更改事件。 如果将文档添加到集合中,则会触发事件。 如果已经在集合中的文档被修改,然后又被保存回集合中,则会触发一个事件。 最后,如果改变了Session变量,则触发一个事件。 函数和模板正在侦听(订阅)特定事件,并将适当地处理数据中的变化。

第三章为什么是陨石! ,这是模型视图视图模型模式在工作。 在响应式上下文中,函数和模板对模型中的更改作出反应。 反过来,视图中的操作将通过视图-模型逻辑创建对模型的更改:

Published events

MeteorMVVM 是一种干净、简洁的开发模式:

  1. 为模型更改设置订阅者(模型=集合、文档和Session变量)。
  2. 创建处理视图事件的逻辑(视图事件=按钮单击、文本输入,等等)。
  3. 在逻辑调用模型时更改模型(更改=已发布的事件)。

它一遍又一遍,点击一个按钮导致模型更改,然后触发一个事件,由模板侦听。 这将基于模型更改更新视图。 泡沫,冲洗,重复。

配置发布者

到目前为止,我们一直在使用autopublish。 这意味着,我们不必为任何事件或集合编写特定的发布事件。 这对于测试来说很好,但是我们想对发布的事件和文档有更多的控制,这样我们就可以提高性能和安全性。

如果我们有一个大的数据集,我们可能不希望每次都返回整个集合。 如果使用了autopublish,则整个集合将返回,这可能会减慢速度,或者会暴露我们不想暴露的数据。

关闭自动发布

是时候关掉autopublish了。 通过打开运行meteor命令的终端窗口,暂时停止 Meteor 应用(如果它仍在运行)。 按Ctrl+C即可停止。 一旦停止,输入以下命令:

> meteor remove autopublish

这删除了autopublish库,该库负责自动发布 Meteor 内部的所有事件。

提示

从项目中删除autopublish被认为是最佳实践。 autopublish用于开发和调试,当你准备开始认真使用你的应用时,应该关闭它。

通过关闭此选项,您实际上使应用什么也不做! 恭喜你! 再次启动 Meteor 服务(输入meteor命令,按enter),打开/导航至http://localhost:3000,即可看到您的惊人进展。 你会看到下面的截图:

Turning off autopublish

分类/列表消失了! 如果你愿意,你甚至可以在控制台进行检查。 输入以下命令:

> lists.find().count()

您应该看到一个6的计数,但您将看到一个0的计数:

Turning off autopublish

到底发生了什么事? 其实很简单。 因为我们删除了autopublish库,服务器不再向我们的模型发布任何更改。

我们为什么要这么做? 破坏应用的目的是什么? 啊! 因为我们想让应用更高效。 不是自动获取每条记录,而是只获取我们需要的记录,以及这些记录的最小数据字段集。

上市类别

LendLib.js中,if(Meteor.isServer)块内部,创建以下Meteor.publish函数:

Meteor.publish("Categories", function() {
 return lists.find({},{fields:{Category:1}}); 
});

这告诉服务器发布一个"Categories"事件。 只要对函数内部的变量做了更改,它就会发布这个。 在本例中,它是lists.find()。 当一个改变会影响lists.find()的结果时,Meteor 将触发/发布一个事件。

如果您注意到,lists.find()调用不是空的。 有一个选择器:{fields:{Category:1}}。 这个选择器告诉lists.find()调用只返回指定的fields:。 并且只指定了一个字段——{Category:1}

这个 JSON 片段告诉选择器我们想要获得Category字段(1= true,0= false)。 因为这是唯一提到的字段,它被设置为1(true),Meteor 假设你想要排除所有其他属性。 如果你有任何字段设置为0(false),Meteor 会假设你想包括所有其他字段,你没有提到。

提示

更多关于find()功能的信息,请参考 MongoDB 文档http://www.mongodb.org/display/DOCS/Advanced+Queries

所以,如果你保存这个更改,你的浏览器会刷新… 显示器没有任何变化!

为什么? 正如您可能已经猜到的,删除autopublish库所做的不仅仅是消除publish事件。 它也摆脱了听众/订阅者。 我们没有设置任何用户收听Categories事件频道。 因此,我们需要添加一个用户事件,将其调到Categories频道。

if (Meteor.isClient)函数内部,在最上面,就在左括号内,输入以下代码行:

Meteor.subscribe("Categories");

保存此更改,现在您将看到您的Categories回到它们所属的位置。

Listing categories

在我们庆祝之前,点击服装类别。

Listing categories

我们最喜欢的衬衫不见了! 您现在可能已经知道,这是因为我们设置的发布事件非常具体。 Categories频道中唯一发布的字段是Category字段。 所有其他领域,包括我们的items(以及我们最喜欢的球衣)都没有被播出。

我们仔细检查这个。 点击浏览器中服装类别内的+按钮,输入Red Hooded Sweatshirt,按输入。 新条目将出现一秒钟,然后它将消失。 这是因为本地缓存和服务器同步。

当您输入新的item时,本地缓存包含一个副本。 这是你的客户暂时可见的。 但是,当与服务器进行同步时,服务器更新只发布Category字段,因此当服务器模型更新本地模型时,item不再包含在内。

还有一个测试,只是为了好玩。 在终端窗口中,停止 Meteor 服务(Ctrl+C)。 现在,在浏览器中,在Clothes类别中输入另一个item(我们将使用Pumped Up Kicks)。 因为服务停止了,没有与服务器的同步,所以你使用的是本地缓存,而这是你的item

Listing categories

现在启动服务器备份。 您的客户端将与服务器同步,然后噗的一声! 你的 T0 又不见了。

Listing categories

列出项目

这是不好的,因为我们想看到我们的items。 因此,让我们将添加回items,并在Category被选中时抓取适当的items列表。 在LenLib.cs中,就在if(Meteor.isServer)块内的第一个Meteor.publish()函数下面,添加以下函数:

Meteor.publish("listdetails", function(category_id){
 return lists.find({_id:category_id}); 
});

publish功能将在"listdetails"频道上发布。 任何侦听器/订阅器都将提供变量category_id,因此find()函数将返回一个精简的记录集。

注意,我们的客户端还没有任何改变(您的items仍然不可见)。 这是因为我们需要创建subscribe函数。

就在我们的第一个Meteor.subscribe()函数下面,添加以下函数:

Meteor.subscribe("Categories");

Meteor.autosubscribe(function() {
 Meteor.subscribe("listdetails", Session.get('current_list')); 
});

保存您的更改,并检查您的 swag衣服集合!

Listing items

让我们看一分钟,看看刚才发生了什么。 注意,订阅使用了Session.get('current_list')。 这是传递给 publish 函数的变量。 换句话说,Session变量current_list中的值将被用作find()函数的选择器中的category_id

如果你还记得第 4 章模板,我们设置了一个点击事件处理程序来监听Category的变化。 例如,当你点击Clothes时,会触发一个事件,LendLib.js中的selectCategory()函数会处理该事件,并更改Session变量。

function selectCategory(e,t){
  Session.set('current_list',this._id); 
}

Session.set()触发发布事件。 我们将Meteor.subscribe()函数包裹在"listdetails"通道内的Meteor.autosubscribe()函数中。 我们这样做是因为Session.set()事件会触发Meteor.autosubscribe(),而且我们有一个Meteor.subscribe()功能,特别针对"listdetails"通道。

换句话说:

  1. Session.set()触发事件。
  2. Meteor.subscribe()监听该事件,因为它使用了Session变量。
  3. Meteor 在"listdetails"频道上重置订阅侦听器(因为它被包装在Meteor.autosubscribe()内)。
  4. Meteor 看到新的订阅侦听器并触发一个初始事件。
  5. Meteor.subscribe()函数获取该事件,传入category_id变量,UI 会因为模型更改而刷新。

检查您的流线数据

现在的显示与我们开始本章时没有什么不同。 但在显示屏之下,这款机型要瘦得多。 选择Clothes类别后,在浏览器控制台中执行如下命令:

> lists.findOne({Category:"DVDs"})

展开对象,您将看到没有列出任何项。

Checking your streamlined data

没有项的原因是我们的Session变量current_list被设置为Clothes,而不是DVDsfind()功能只获取current_list的完整记录。

现在在浏览器控制台中输入以下命令,按,输入:

> lists.findOne({Category:"Clothes"})

展开该对象,您将看到数组中的三个项。

Checking your streamlined data

单击四周,向类别中添加项,添加新类别,并检查在客户机上可见的基础数据模型。 你会发现你的清单现在不那么显眼了,因此更加安全和谨慎。 对于您自己的个人借阅库应用来说,这可能不是问题,但随着我们在下一章展开这一内容,以便多人可以使用它,精简和谨慎的数据将真正提高性能。

小结

在本章中,您已经了解了 MongoDB 是什么,面向文档的数据库是如何工作的,并且您已经在命令行中执行了直接查询,熟悉了 Meteor 的默认数据存储库系统。 您还通过删除autopublish简化了应用,并对 Meteor 内置的发布/订阅设计模式有了牢固的理解。

在下一章中,您将真正加强应用的安全性,允许多个用户跟踪和控制他们自己的项目列表,您将看到如何通过使用文件夹进一步简化您的客户端和服务器代码。