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