---
title: 發布與訂閱
slug: publications-and-subscriptions
date: 0004/01/02
number: 4.5
points: 5
sidebar: true
contents: 理解發布和閱讀是如何工作的。|學習默認的 Autopublish 包的作用。|更多的發布模式。
paragraphs: 52
---
發布(Publication)和訂閱(Subscription)是 Meteor 的最基本最重要的概念之一,但是如果你是剛剛開始接觸 Meteor 的話,也是有些難度的。
這已經導致不少誤解,比如認為 Meteor 是不安全的,或者說 Meteor 應用無法處理大量數據等等。
人們起初會感覺這些概念很迷惑很大程度上是因為 Meteor 像變魔法一樣替你做了很多事兒。盡管這些魔法最終看起來很有效,但是它們掩蓋了后臺真正做的工作(好像魔術一樣)。所以讓我們剝去魔法的外衣來看看究竟發生了什么。
### 過去的日子
首先,讓我們回顧一下2011年之前,當 Meteor 還沒有誕生的時候的老日子。比如說我們要建立一個簡單的 Rails app。當用戶來我們的站點,客戶端(舉例說瀏覽器)向我們的服務器端的 app 發送請求。
App 的第一個任務就是搞清楚這個客戶請求什么數據。這個可能是搜索結果的第12頁、瑪麗的用戶信息、鮑勃的最新20條微博,等等等等。 你可以想想成為一個書店的伙計在書架之間幫你尋找你要的書。
當正確的數據被找到,這個 App 的下一個任務就是把數據轉換成好看的,人類可讀的 HTML 格式(對于 API 而言是 JSON 串)。
用書店來舉例,那就相當于是把你剛買的書包好,然后裝入一個漂亮的袋子。這就是著名的 MVC(模型-視圖-控制器)模式中的視圖部分。
最終,App 把 HTML 代碼送到客戶端。這個 App 的任務也就交差了。它可以去買瓶啤酒然后等著下一個請求。
### Meteor 的方式
讓我們看看 Meteor 相對之下是多么的特別。正如我們看到的,Meteor 的關鍵性創新在于 Rails 程序只跑在**服務器**上,而一個 Meteor App 還包括在**客戶端**(瀏覽器)上運行的客戶端組件。
<%= diagram "client-server", "推送數據庫子集到客戶端", "pull-right" %>
這就相當于書店的伙計不僅僅在書店里幫你找書,還跟你回家,每天晚上讀給你聽(這聽起來怪怪的)。
這種架構可以讓 Meteor 做更多很酷的事情,其中一件主要的就是 Metoer 變得[數據庫無處不在](http://docs.meteor.com/#sevenprinciples)。簡單說,Meteor 把你的數據拿出一部分子集*復制到客戶端*。
這樣后兩個主要結果:第一,服務器不再發送 HTML 代碼到客戶端,而是發送**真實的原始數據**,讓客戶端決定如何處理[線傳數據](http://docs.meteor.com/#sevenprinciples)。第二,你可以不必等待服務器傳回數據,而是**立即訪問甚至修改數據**([延遲補償 latency compensation](http://docs.meteor.com/#sevenprinciples))。
### 發布
一個 App 的數據庫可能用上萬條數據,其中一些還可能是私用和保密敏感數據。顯而易見我們不能簡單地把數據庫鏡像到客戶端去,無論是安全原因還是擴展性原因。
所以我們需要告訴 Meteor 那些數據**子集**是需要送到客戶端,我們將用**發布**功能來做這個事兒。
讓我們來回到 Microscope。這里是我們 App 數據庫中的所有帖子:
<%= diagram "collections-1", "數據庫中的所有帖子數據", "pull-center" %>
盡管實際上不存在但是我們還是假設我們的帖子中有幾條因為言語不當被打了特殊標記的。我們需要把他們留在數據庫中但是不希望讓用戶看到(發送去客戶端)。
我們第一個任務就是告訴 Meteor 那些數據我們*要*發送去客戶端。我們告訴 Meteor 我們只**發布**沒有打標記的帖子。
<%= diagram "collections-2", "排除做過標記的帖子", "pull-center" %>
這里是對應的代碼,在服務器端代碼中。
~~~js
// 在服務器端
Meteor.publish('posts', function() {
return Posts.find({flagged: false});
});
~~~
這就保證客戶端**無論如何**也無法看到打了標記的帖子了。這就是 Meteor App 如何做到安全性的:保證只發布你讓這個當前用戶看到的數據。
<% note do %>
### DDP
基本上我們可以把發布/訂閱模式想象成為一個漏斗,從服務器端(數據源)過濾數據傳送到客戶端(目標)。
這個漏斗的專屬協議叫做 **DDP**(分布式數據協議 Distributed Data Protocol 的縮寫)。如果想了解 DDP 的更多細節,可以通過看 Matt DeBergalis(Meteor 創始人之一)[在 Real-time 大會上的講演視頻](http://2012.realtimeconf.com/video/matt-debergalis),或者來自 Chris Mather 的這個[截屏視頻](http://www.eventedmind.com/posts/meteor-subscriptions-and-ddp),來學習關于這個概念更多的細節。
<% end %>
### 訂閱
就算是我們想把打了標記的帖子也發送給客戶端,我們也不能把成千上萬的帖子一股腦都發出去。我們需要一個機制讓客戶端來確定那些子集是他們在某個特別時候特別需要的,這就是**訂閱**這個功能的用途。
通過 MiniMongo,客戶端 MongoDB 的應用,你訂閱的數據會被**鏡像**到客戶端。
舉個例子,讓我們現在瀏覽一下 Bob Smith 的個人頁面,這里只會顯示*他的*帖子。
<%= diagram "collections-3", "訂閱 Bob 的帖子鏡像到客戶端。", "pull-center" %>
首先,我們給發布功能加一個參數:
~~~js
// 在服務器端
Meteor.publish('posts', function(author) {
return Posts.find({flagged: false, author: author});
});
~~~
然后我們在客戶端*訂閱*這個發布時定義同一個參數。
~~~js
// 在客戶端
Meteor.subscribe('posts', 'bob-smith');
~~~
這就是我們讓 Meteor 程序在客戶端能夠具有可伸縮性:不去訂閱*全部*數據,而是指選擇你現在需要的數據去訂閱。這樣的話,你就可以避免消耗大量的客戶端內存,無論服務器端的總數據量有多大。
### 查找
現在 Bob 的帖子恰巧涵蓋了多個類別(比如:“JavaScript”、“Ruby”和“Python”)。也許我們仍然需要把 Bob 的所有帖子都裝入內存,但是我們現在只想顯示屬于“JavaScript”類別的帖子。這就是“查找”的用途。
<%= diagram "collections-4", "在客戶端選擇一個數據子集。", "pull-center" %>
正如我們在服務器上做的一樣,我們用了 `Posts.find()` 函數來選擇數據的子集:
~~~js
// 在客戶端
Template.posts.helpers({
posts: function(){
return Posts.find({author: 'bob-smith', category: 'JavaScript'});
}
});
~~~
現在我們應該明白訂閱和發布機制了,讓我們在深入了解一些常見的應用模式。
### 自動發布(Autopublish)
如果你從頭開始建立一個 Meteor 項目(比如,使用 `meteor create` 命令),系統會自動包含并啟用一個叫做 `autopublish` 的包。讓我們說說這個包是干什么的。
`autopublish` 的目的是讓 Meteor 應用有個簡單的起步階段,它簡單地直接把服務器上的_全部數據_鏡像到客戶端,因此你就不用管發布和訂閱了。
<%= diagram "autopublish", "自動發布", "pull-center"%>
那么這究竟是如何工作的呢?假設在服務器端我們有一個集合叫做 `posts`。自動發布包就會自動地把 Mongo 數據庫中這個集合的所有的數據(帖子)發送到客戶端的名為 `‘posts’` 的集合中(假設客戶端的確有這樣一個集合)。
因此,如果你使用自動發布,你就不需要考慮發布。數據一致,而且事情變得十分簡單。當然,這樣的話會有一個明顯的問題,就是你的所有數據都被緩存到所有用戶的電腦中。
基于這個原因,自動發布只在你起步階段且還未考慮發布之前時使用。
### 發布全部集合
一旦你刪除掉 `autopublish` 這個包,你馬上就會發現在瀏覽器上沒有數據了。一個簡單的解決方法就是重復自動發布所做的工作, 那就是發布所有數據。比如:
~~~js
Meteor.publish('allPosts', function(){
return Posts.find();
});
~~~
<%= diagram "fullcollection", "發布所有集合", "pull-center" %>
我們還是發布了所有集合,但是至少我們現在可以自己控制哪個集合我們發布哪個不發布。比如現在這個例子,我們發布了 `Posts` 集合但是并沒有發布 `Comments`。
### 發布部分集合
下一步我們要做的是發布集合中的_部分_記錄。比如我們只發布來自于某個作者的帖子:
~~~js
Meteor.publish('somePosts', function(){
return Posts.find({'author': 'Tom'});
});
~~~
<%= diagram "partialcollection", "發布集合的一部分", "pull-center" %>
<% note do %>
### 幕后真相
如果你已經閱讀了 [Meteor 發布文檔](http://docs.meteor.com/#publishandsubscribe),你可能被諸如 `added()` 和 `ready()` 之類的用來設置客戶端記錄屬性的函數搞暈了,而且還糾結于似乎我們從來沒有使用過這些方法。
原因在于 Meteor 提供了十分重要的簡化:`_publishCursor()` 方法。你也沒有看到我們用這個方法對吧?也許我們沒有直接用,但是如果你在發布函數中返回了一個[游標](/chapter/meteor-vocabulary/)(比如,`Posts.find({'author':'Tom'})`),那個就是 Meteor 使用這個方法的時候。
當 Meteor 看到 `somePosts` 發布函數返回了一個游標,它會調用 `_publishCursor()` 去 —— 你猜猜看 —— 自動發布這個游標。
下面就是 `_publishCursor()` 做的工作:
- 它檢查服務器端的集合的名稱
- 它從游標中找到所有的符合要求的文檔,然后發送到客戶端的*同名*集合中。(它使用了`.added()` 函數來完成的)
- 當新的文檔加入集合了,或者刪除或者改變了,它會把這些改動發送到客戶端的集合。(它使用 `.observe()` 來監控游標,使用 `.added()`, `.changed()` 和 `removed()` 來增刪改)。
所以在上述的例子中,我們可以保證用戶只會在客戶端緩存中得到他們感興趣的帖子(在這里例子中是 Tom 發的帖子)。
<% end %>
### 發布部分字段
我們已經看到如何發布部分帖子,但是我們還需要再精簡!讓我們看看如何只發布指定的部分*字段*。
如同以前我們使用 `find()` 返回一個游標,現在我們來去掉一些字段。
~~~js
Meteor.publish('allPosts', function(){
return Posts.find({}, {fields: {
date: false
}});
});
~~~
<%= diagram "partialproperties", "發布部分字段", "pull-center" %>
實際上,我們可以同時使用上述兩種技術,只發布作者是 Tom 的帖子,并且隱藏 date 日期字段:
~~~js
Meteor.publish('allPosts', function(){
return Posts.find({'author': 'Tom'}, {fields: {
date: false
}});
});
~~~
### 總結
我們已經從發布所有集合的所有文檔的所有字段(通過 `autopublish`),到發布_個別_集合的_個別_文檔的_個別_字段。
這已經覆蓋了 Meteor 的發布的基本內容,而且這些基本技巧已經足夠涵蓋大部分的用例了。
有時,你需要進一步來組合、連接或融合發布。我們在以后的章節中講到這些內容!