目前你應(yīng)該對發(fā)布和訂閱交互模式有一個不錯的掌握了。因此,我們廢話少說,來看幾個更高級的情景。
在我們第一個關(guān)于發(fā)布的附錄中,我們看到了一些更普遍的發(fā)布和訂閱模式,同時我們學(xué)習(xí)了 _publishCursor
函數(shù),如何讓它們非常容易地實現(xiàn)在我們的站點上。
首先,讓我們回憶 _publishCursor
到底為我們做了什么:它將整理所有的文檔以匹配一個給定的游標(biāo)(cursor),并將它們推送至同名的客戶端集合中。注意這與 publication 的名字是不關(guān)聯(lián)的。
這意味著我們可以用不止一個 publicaton 去連接任何集合的客戶端與服務(wù)端版本。
我們已經(jīng)在分頁章節(jié)用過這個模式,當(dāng)我們在當(dāng)前顯示的帖子之外,再發(fā)布一個所有帖子的分頁后的子集。
另一個相似的用例是發(fā)布一大組文檔的預(yù)覽,和單個文檔的全部信息:
Meteor.publish('allPosts', function() {
return Posts.find({}, {fields: {title: true, author: true}});
});
Meteor.publish('postDetail', function(postId) {
return Posts.find(postId);
});
現(xiàn)在客戶端訂閱這兩個發(fā)布,這 'posts'
集合來自于兩個源渠道:來自第一個訂閱的標(biāo)題和作者姓名列表,和來自第二個訂閱的單個帖子全部信息。
你也許意識到了 postDetail
發(fā)布的帖子也被 allPosts
發(fā)布了(盡管只有它的部分屬性)。但是,Meteor 會合并字段及確認(rèn)沒有重復(fù)的帖子,來處理數(shù)據(jù)重疊的問題。
這是很棒的,因為現(xiàn)在當(dāng)我們呈現(xiàn)帖子摘要列表時,我們正在處理的數(shù)據(jù)對象正好擁有我們需要顯示的足夠數(shù)據(jù)。但是,當(dāng)我們呈現(xiàn)單個帖子時,我們有一切需要展示的數(shù)據(jù)。當(dāng)然,在這種情況下,我們需要讓客戶端不要去期待所有帖子的所有字段都能都顯示出來————這是一個常見的問題!
注意你并沒有改變文檔屬性的任何限制。你可以很好地在這兩個發(fā)布中發(fā)布同樣的屬性,但是先后排序不同。
Meteor.publish('newPosts', function(limit) {
return Posts.find({}, {sort: {submitted: -1}, limit: limit});
});
Meteor.publish('bestPosts', function(limit) {
return Posts.find({}, {sort: {votes: -1, submitted: -1}, limit: limit});
});
我們已經(jīng)看了如何多次發(fā)布同一個集合。事實證明你可以通過另一個模式來完成非常相近的結(jié)果:建立一個單一發(fā)布,卻多次訂閱它。
在 Microscope 中,我們多次重復(fù)訂閱 posts
發(fā)布,但 Iron Router 為我們設(shè)置并拆開每次的訂閱。然而,沒有理由我們不能同時進(jìn)行多次訂閱。
舉個例子,我們想要將最新的和最好的帖子同時載入內(nèi)存:
我們設(shè)定一個單一發(fā)布:
Meteor.publish('posts', function(options) {
return Posts.find({}, options);
});
并且我們多次訂閱這個發(fā)布。事實上或多或少我們在 Microscope 里這樣做了:
Meteor.subscribe('posts', {submitted: -1, limit: 10});
Meteor.subscribe('posts', {baseScore: -1, submitted: -1, limit: 10});
接下來到底發(fā)生什么了?每個瀏覽器開啟了兩個不同的訂閱,每個訂閱連接到同個服務(wù)端的發(fā)布。
每個訂閱提供了不同的發(fā)布參數(shù),但從根本上,每次一個(不同)文檔子集從 posts
集合提取出來,并通過連接機(jī)制發(fā)送到客戶端集合。
你甚至可以用同樣的參數(shù)訂閱兩次相同的發(fā)布。這個對很多處場景來說很難說有用,但這種彈性機(jī)制總有一天會有用的。
不像傳統(tǒng)關(guān)系型數(shù)據(jù)庫像 MySQL 使用 joins,NoSQL 數(shù)據(jù)庫類似 Mongo 都是關(guān)于去規(guī)范化和嵌入。讓我們看看它們是怎樣在 Meteor 環(huán)境下工作的。
讓我們看一個具體的例子。我們已經(jīng)對我們的帖子添加了評論,到目前為止,我們一直很愉快地只發(fā)布用戶看的單個帖子的評論。
但是,假設(shè)我們希望在首頁中顯示全部帖子的回復(fù)(記著這些帖子會在分頁時被改變)。這個用例展示了一個很好的理由把評論嵌入帖子中,事實上這是促使我們來非規(guī)范化評論數(shù)量。
當(dāng)然我們可以總是嵌入評論到帖子中,并完全摒除 Comments
集合。但如同我們前面在去規(guī)范化章節(jié)看到的,我們將在分離的集合的操作中也會失去一些額外的好處。
但是事實證明有一個涉及訂閱的技巧,在保持分離集合的同時再嵌入我們的評論。
讓我們假定除了首頁帖子列表之外,我們希望再訂閱每個帖子的兩個最新評論。
使用獨立的評論發(fā)布會很難完成這個要求,尤其在帖子列表受到某些限制時(比如說,最近的10個)。我們必須寫一個發(fā)布,看起來像下面的代碼:
Meteor.publish('topComments', function(topPostIds) {
return Comments.find({postId: topPostIds});
});
從性能角度來看這是個問題,因為這個發(fā)布將需要每次在 topPostIds
改變時消除及重新建立。
有一個途徑來解決這個問題。我們可以應(yīng)用這個事實:就是我們不僅可以在每個集合上擁有多次發(fā)布,而且我們也可以在每個發(fā)布上擁有多個集合。
Meteor.publish('topPosts', function(limit) {
var sub = this, commentHandles = [], postHandle = null;
// send over the top two comments attached to a single post
function publishPostComments(postId) {
var commentsCursor = Comments.find({postId: postId}, {limit: 2});
commentHandles[postId] =
Mongo.Collection._publishCursor(commentsCursor, sub, 'comments');
}
postHandle = Posts.find({}, {limit: limit}).observeChanges({
added: function(id, post) {
publishPostComments(id);
sub.added('posts', id, post);
},
changed: function(id, fields) {
sub.changed('posts', id, fields);
},
removed: function(id) {
// stop observing changes on the post's comments
commentHandles[id] && commentHandles[id].stop();
// delete the post
sub.removed('posts', id);
}
});
sub.ready();
// make sure we clean everything up (note `_publishCursor`
// does this for us with the comment observers)
sub.onStop(function() { postHandle.stop(); });
});
注意我們在這個發(fā)布中沒有返回任何東西,因為我們自己手動給 sub
發(fā)送信息(通過 .added()
等方式)。所以我們不必通過返回一個游標(biāo)來請求 _publishCursor
給我們來做這個動作。
現(xiàn)在,每次我們發(fā)布一個帖子時,我們也自動發(fā)布其2個最新評論。而且所有都在一個訂閱調(diào)用中!
雖然 Meteor 還未直接實現(xiàn)這個方法,但是你也可以參考在 Atomsphere 里的 publish-with-relations
包,它的目標(biāo)就是讓這個模式更容易使用。
這樣的訂閱彈性機(jī)制還能給我們更多新知識么?當(dāng)然,如果我們不使用 _publishCursor
,我們不必跟著此項約束,就是在服務(wù)端的源集合需要與客戶端的目標(biāo)集合有同樣的名稱。
為什么我們想這么做的一個原因就是單表繼承。
假設(shè)我們需要從我們的帖子中引用多種類型的對象,每一個對象存儲在相同字段中但又顯然是不同內(nèi)容。例如,我們建立一個類似 Tumblr 的博客引擎,每個帖子具有常見的 ID、時間戳,以及標(biāo)題;但是額外也有如圖像、視頻、鏈接,或者只是文字。
我們可以將這些對象存儲在一個單獨的 'resources'
集合中,使用 type
屬性來標(biāo)記他們是什么類型的對象(video
、image
、link
等等)。
同時,雖然我們有一個服務(wù)端的單一的 Resources
集合,我們也能夠?qū)我患限D(zhuǎn)換到多個的 Videos
、`Images',等等集合中??蛻舳说募先缦碌拇a:
Meteor.publish('videos', function() {
var sub = this;
var videosCursor = Resources.find({type: 'video'});
Mongo.Collection._publishCursor(videosCursor, sub, 'videos');
// _publishCursor doesn't call this for us in case we do this more than once.
sub.ready();
});
我們告訴 _publishCursor
(就像返回)游標(biāo)會做的一樣發(fā)布我們的視頻,但不是將 resources
集合發(fā)布到客戶端,而是我們從 resources
發(fā)布到 videos
。
另一個類似的主意是:發(fā)布到客戶端的集合,卻根本沒有服務(wù)端集合!舉例,你也許從一個第三方服務(wù)中抓取數(shù)據(jù),并發(fā)布它們到一個客戶端集合。
由于發(fā)布 API 的靈活性,可能性是無限的。
更多建議: