在第一章我們提到了 Meteor 的核心功能, 那就是服務(wù)器端和客戶端的自動數(shù)據(jù)同步。
在這一章我們要仔細(xì)了解一下它是如何運(yùn)作的,以及研究那個讓它得以運(yùn)行的關(guān)鍵技術(shù): Meteor 集合(Collection)。
集合是一個特殊的數(shù)據(jù)結(jié)構(gòu),它將你的數(shù)據(jù)存儲到持久的、服務(wù)器端的 MongoDB 數(shù)據(jù)庫中,并且與每一個連接的用戶瀏覽器進(jìn)行實(shí)時地同步。
我們想讓我們的 post 永久保存并且要在用戶之間共享,所以我們一開始要新建一個叫做 Posts
的 collection 來保存它們。
我們現(xiàn)在做一個社交新聞應(yīng)用, 所以第一件事兒就是做一個人們貼上來的帖子的連接列表。 我們叫它 'post'
很自然, 我們需要把它們存起來。 Meteor 捆綁了 MongoDB 運(yùn)行在服務(wù)器上作為持久化存儲。
因此,盡管一個用戶在瀏覽器上有各種狀態(tài)(比如他們正在閱讀哪一頁, 或者正在輸入那一條評論), 而服務(wù)器上,尤其是 Mongo,保存的是永久保留的一致數(shù)據(jù)。 說到一致, 我們是指對于所有用戶來說都是一樣的數(shù)據(jù): 每個用戶也許在看不同的頁面, 但是帖子 Post 的主列表對所有用戶來說卻始終是一樣的。
這些數(shù)據(jù)在Meteor中被存儲在集合(Collection)中。 集合是一種特殊的數(shù)據(jù)結(jié)構(gòu), 通過發(fā)布(publications)和訂閱(subscriptions)機(jī)制把數(shù)據(jù)實(shí)時同步上行或者下行到連接著的各個用戶的瀏覽器或者M(jìn)ongo數(shù)據(jù)庫中。讓我們看看如何做到的。
我們希望我們的帖子Post可以持久存儲并分享給用戶們, 所以我們一開始就要建立一個叫 Posts
的集合來存儲他們。 如果你還沒有在根文件夾建立一個叫做 collections/
的文件夾, 并在里面放一個 posts.js
的文件的話,那現(xiàn)在就加上。
Posts = new Mongo.Collection('posts');
代碼所在的目錄既不是 client/
也不是 server/
所以 Posts
會共同存在運(yùn)行在服務(wù)器和客戶端。 然而,這個集合的使用在兩種環(huán)境下十分不同。
在 Meteor 中,關(guān)鍵字 var
限制對象的作用域在文件范圍內(nèi)。 我們想要 Posts
作用于整個應(yīng)用范圍內(nèi),因此我們在這里不要 Var 這個關(guān)鍵字。
網(wǎng)絡(luò)應(yīng)用有三種基本方式保存數(shù)據(jù),各種方式有不同的角色:
Meteor 使用所有三種方式,有時會從一個地方同步數(shù)據(jù)到另一個地方(我們會馬上看到)。話雖如此,數(shù)據(jù)庫仍然是包含數(shù)據(jù)主副本的“規(guī)范化的”數(shù)據(jù)源。
不在 client/
或 server/
文件夾中代碼會在客戶端和服務(wù)器端運(yùn)行。所以 Posts
集合在客戶端和服務(wù)器端都可用。但是,在各自環(huán)境下所起的作用有很大不同。
在服務(wù)器,集合有一個任務(wù)就是和 Mongo 數(shù)據(jù)庫聯(lián)絡(luò),讀取任何數(shù)據(jù)變化。 在這種情況下,它可以比對標(biāo)準(zhǔn)的數(shù)據(jù)庫。
在客戶端,集合是一個安全拷貝來自于實(shí)時一致的數(shù)據(jù)子集??蛻舳说募峡偸牵ㄍǔ#┩该鞯貙?shí)時更新數(shù)據(jù)子集。
在這一章,我們開始使用瀏覽器控制臺,不過不要和終端、Meteor Shell 或者 Mongo Shell 搞混了。 現(xiàn)在對它們做個比對。
console.log()
會輸出到這里$
提示符console.log()
會輸出到這里?
meteor shell
調(diào)用。>
。meteor mongo
或者 mrt mongo
來啟動>
注意在各種情況下你都不需要敲提示符($
?
或 >
)在命令前面。而且你可以認(rèn)定任何不是用提示符起始的行都是前一個命令的輸出結(jié)果。
在服務(wù)器端,集合可以像 API 一樣操作 Mongo 數(shù)據(jù)庫。在服務(wù)器端的代碼,你可以寫像 Posts.insert()
或 Posts.update()
這樣的 Mongo 命令,來對 Mongo 數(shù)據(jù)庫中的 posts
集合進(jìn)行操作。
如果想直接看看 MongoDB 數(shù)據(jù)庫,可以打開第二個終端窗口(這時候 Meteor 還在第一個終端窗口繼續(xù)運(yùn)行呢),在你應(yīng)用的目錄,輸入命令 meteor mongo
啟動 Mongo Shell 外殼程序?,F(xiàn)在你可以輸入標(biāo)準(zhǔn)的 Mongo 命令(如同以往,你可以敲 ctrl+c
快捷鍵退出)。比如讓我們插入一個新的 post:
meteor mongo
> db.posts.insert({title: "A new post"});
> db.posts.find();
{ "_id": ObjectId(".."), "title" : "A new post"};
注意如果你把應(yīng)用部署在 *.meteor.com 上,你一樣可以通過 meteor mongo myApp
的方式進(jìn)入你應(yīng)用的 Mongo shell 進(jìn)行操作。
而且你還可以輸入 meteor logs myApp
得到你應(yīng)用的 log 日志。
Mongo 的語法由于借鑒了 Javascript 的語法所以十分熟悉。我們現(xiàn)在在 Mongo 外殼里不做過多的數(shù)據(jù)操作,不過我們可以隨時來這里檢查數(shù)據(jù)確保他們正常存在。
客戶端的集合更加有趣。當(dāng)你在客戶端申明 Posts = new Mongo.Collection('posts');
你實(shí)際上是創(chuàng)建了一個本地的,在瀏覽器緩存中的真實(shí)的 Mongo 集合。 當(dāng)我們說客戶端集合被"緩存"是指它保存了你數(shù)據(jù)的一個子集,而且對這些數(shù)據(jù)提供了十分快速的訪問。
有一點(diǎn)我們必須要明白,因?yàn)檫@是 Meteor 工作的一個基礎(chǔ): 通常說來,客戶端的集合的數(shù)據(jù)是你 Mongo 數(shù)據(jù)庫的所有數(shù)據(jù)的一個子集(畢竟我們不會想把整個數(shù)據(jù)庫的數(shù)據(jù)全傳到客戶端來)。
第二,那些數(shù)據(jù)是被存儲在瀏覽器內(nèi)存中的,也就是說訪問這些數(shù)據(jù)幾乎不需要時間,不像去服務(wù)器訪問 Posts.find()
那樣需要等待,因?yàn)閿?shù)據(jù)事實(shí)上已經(jīng)載入了。
Meteor 的客戶端 Mongo 的技術(shù)實(shí)現(xiàn)被成為 MiniMongo。它目前還不是一個完美的實(shí)現(xiàn),而且你會發(fā)現(xiàn)偶爾 Mongo 的功能在這里不能實(shí)現(xiàn)。不過本書中涉及到的功能都是可以在 Mongo 和 MiniMongo 中實(shí)現(xiàn)的。
這一切最關(guān)鍵的是如何讓客戶端的集合數(shù)據(jù)與服務(wù)器端同名的集合數(shù)據(jù)同步(以我們現(xiàn)在這個例子來說是 posts
)。
與其現(xiàn)在就解釋細(xì)節(jié)不如讓我們先來看看發(fā)生了什么。
現(xiàn)在我們打開兩個瀏覽器窗口,分別打開他們的瀏覽器控制臺。然后在終端命令行打開 Mongo 外殼程序。
現(xiàn)在可以在這三個地方看到我們早前時候建立的那個文檔。(注意,我們應(yīng)用的用戶界面依然顯示著我們之前的三個演示 post,請忽略它們。)
> db.posts.find();
{title: "A new post", _id: ObjectId("..")};
? Posts.findOne();
{title: "A new post", _id: LocalCollection._ObjectID};
讓我們來創(chuàng)建一個帖子。在其中一個瀏覽器窗口中運(yùn)行這個插入命令:
? Posts.find().count();
1
? Posts.insert({title: "A second post"});
'xxx'
? Posts.find().count();
2
毫無疑問,這個帖子被加入到本地集合中?,F(xiàn)在讓我們查看一下 Mongo:
? db.posts.find();
{title: "A new post", _id: ObjectId("..")};
{title: "A second post", _id: 'yyy'};
如同你所看見的那樣,這個帖子一路上行一直到 Mongo 數(shù)據(jù)庫中,而我們卻沒有為這個連接客戶端和服務(wù)器的過程寫任何一行代碼。(嚴(yán)格地說,我們的確寫了_一行_代碼:new Mongo.Collection('posts')
)。但是這沒關(guān)系!
現(xiàn)在到第二個瀏覽器窗口的控制臺中輸入這個命令:
? Posts.find().count();
2
這個帖子居然也在這兒!甚至于我們連刷新都沒有在第二個瀏覽器做過,更何況我們也沒有寫任何代碼來推送更新。這一切想魔術(shù)一般 - 而且是即時的,盡管這一切以后看起來都很顯而易見。
實(shí)際情況是服務(wù)器端的集合被客戶端的集合通知說有一個新帖子,然后執(zhí)行了一個任務(wù)把這個帖子放入 Mongo 數(shù)據(jù)庫,進(jìn)而會送到所有連接著的 post
即可。
在瀏覽器的控制臺取出所有的帖子沒什么用處。我們以后會學(xué)習(xí)如何把這些數(shù)據(jù)顯示在模板中,并把這個簡單的 HTML 原型變成一個有用的實(shí)時 Web 應(yīng)用。
從瀏覽器控制臺看到集合算是一件事兒,我們更應(yīng)該關(guān)注的是能在屏幕上顯示數(shù)據(jù)和數(shù)據(jù)的變化。要做到這一點(diǎn),我們需要把我們的應(yīng)用從一個單一顯示靜態(tài)數(shù)據(jù)的頁面
變成可以實(shí)時動態(tài)數(shù)據(jù)的應(yīng)用
。
讓我們看怎么做。
首先我們先放點(diǎn)數(shù)據(jù)在數(shù)據(jù)庫里。我們要做的是讓服務(wù)器第一次初始啟動的時候從一個數(shù)據(jù)文件中讀取數(shù)據(jù)結(jié)構(gòu)存在Posts
集合中。
首先我們要確保數(shù)據(jù)庫中沒有數(shù)據(jù)。我們使用 meteor reset
命令清空數(shù)據(jù)庫初始化我們的項(xiàng)目。當(dāng)然,如果在真實(shí)的正在運(yùn)行的正式項(xiàng)目上請務(wù)必十分小心。
停止 Meteor 服務(wù)(通過鍵入 ctrl-c
) 然后在命令行輸入:
meteor reset
這個 reset 命令徹底地把 Mongo 數(shù)據(jù)庫清空了。在開發(fā)的時候這個命令很有用,尤其當(dāng)我們的數(shù)據(jù)庫發(fā)生數(shù)據(jù)混亂的時候。
現(xiàn)在重啟我們的 Meteor 應(yīng)用:
meteor
現(xiàn)在數(shù)據(jù)庫已經(jīng)清空,我們可以增加下面的代碼以便在服務(wù)器啟動時候檢查數(shù)據(jù)庫 Posts
集合,如果為空則載入三條帖子。
if (Posts.find().count() === 0) {
Posts.insert({
title: 'Introducing Telescope',
url: 'http://sachagreif.com/introducing-telescope/'
});
Posts.insert({
title: 'Meteor',
url: 'http://meteor.com'
});
Posts.insert({
title: 'The Meteor Book',
url: 'http://themeteorbook.com'
});
}
我們把這個文件放到了 server/
目錄中,因此永遠(yuǎn)不會被加載到任何用戶的瀏覽器中。這段代碼在服務(wù)器啟動的時候會立即運(yùn)行,然后調(diào)用插入
功能在數(shù)據(jù)庫的 posts
集合中插入三條簡單的帖子。因?yàn)槲覀冞€沒有加入任何數(shù)據(jù)安全功能,所以無論在服務(wù)器還是在客戶端運(yùn)行這個文件都事實(shí)上沒有區(qū)別的。
現(xiàn)在我們用 meteor
命令啟動服務(wù),這三條帖子會被裝在到數(shù)據(jù)庫中。
現(xiàn)在如果我們打開一個瀏覽器的控制臺,我們可以看到這三個帖子都被轉(zhuǎn)載到 MiniMongo 中了:
? Posts.find().fetch();
要把這些 post 渲染到 HTML 中,我們需要用模板 helper。
在第三章中,我們看到 Meteor 允許我們把 數(shù)據(jù)上下文 捆綁到我們的 Spacebars 模板上,從而用 HTML 視圖顯示這些簡單的數(shù)據(jù)結(jié)構(gòu)。 我們可以同樣把我們的集合數(shù)據(jù)捆綁起來。我們馬上就替換掉靜態(tài)的 postsData
Javascript 對象成為一個動態(tài)地集合。
現(xiàn)在請隨手刪掉postsData
代碼。下面是 posts_list.js
修改后的樣子:
Template.postsList.helpers({
posts: function() {
return Posts.find();
}
});
在 Meteor 中,find()
返回值是一個游標(biāo)。游標(biāo)是一種從動數(shù)據(jù)源。如果你想輸出內(nèi)容,你可以對游標(biāo)使用 fetch()
來把游標(biāo)轉(zhuǎn)換成數(shù)組。
Meteor 十分智能地在應(yīng)用中保持游標(biāo)狀態(tài)而避免動不動就把游標(biāo)變成數(shù)組。這就造成了你不會經(jīng)常在 Meteor 代碼中看到 fetch()
被調(diào)用(基于同樣原因,我們在上述例子中也沒有使用 fetch )。
現(xiàn)在,與其把帖子們變成靜態(tài)的數(shù)組,不如直接把游標(biāo)賦給 posts
幫助方法。但是如何做得到呢?如果我們回到瀏覽器上,我們可以看到:
我們可以清晰地看到 {{#each}}
幫助方法已經(jīng)枚舉了 Posts
中的所有帖子,而且顯示到屏幕上。服務(wù)器端的集合從 Mongo 數(shù)據(jù)庫中取出貼子數(shù)據(jù),通過網(wǎng)絡(luò)傳到客戶端的集合中,進(jìn)而 handlers 的幫助方法 把這些數(shù)據(jù)加載到模板中。
現(xiàn)在我們只需要再走一步;讓我們通過控制臺增加另一個帖子:
Posts.insert({
title: 'Meteor Docs',
author: 'Tom Coleman',
url: 'http://docs.meteor.com'
});
再看瀏覽器 - 你會看到這些:
你剛才第一次看到從動功能生效了。當(dāng)我們告訴 handlebars 去枚舉 Posts.find()
游標(biāo)的時候,它自己知道如何發(fā)現(xiàn)游標(biāo)的變動,從而用最簡單的方式將變化后的正確數(shù)據(jù)顯示到屏幕上。
在目前的情況下,最簡單的變動應(yīng)該就是增加一個 <div class="post">...</div>
。 如果你想看看是否的確如此,你可以打開 DOM 檢查器然后選擇某個已經(jīng)存在的帖子的 <div>
。
現(xiàn)在在 Javascript 控制臺,插入另外一個帖子。當(dāng)你回到檢查器,會發(fā)現(xiàn)一條新的 <div>
對應(yīng)了新增的那個帖子。同時,原先選中的那個舊的 <div>
仍然存在。這是一種判斷元素是否被重新渲染的有效方式。
到此為止,我們?nèi)匀挥弥?autopublish
這個包,這個包并不是為正式產(chǎn)品化的應(yīng)用程序準(zhǔn)備的。正如它的名字陳述的那樣,它簡單地把整個集合分享給所有連接的客戶端。這個可不是我們期望的樣子,所以讓我們?nèi)サ羲?/p>
打開一個終端窗口,輸入:
meteor remove autopublish
這個操作有了立即的反應(yīng)。當(dāng)你打開瀏覽器,你會發(fā)現(xiàn)所有的帖子都不見了!這是因?yàn)槲覀円恢币蕾囉?autopublish
來讓我們的客戶端可以鏡像般地得到數(shù)據(jù)庫中的所有帖子。
最終我們需要做得到我們僅僅把我們客戶端需要看到的帖子傳輸過來(需要考慮分頁的情況)。不過暫時我們可以先設(shè)置把 Posts
所有帖子都發(fā)布出來。
為達(dá)到這個目的,我們建立一個簡單的 Publish()
函數(shù),它僅僅返回一個反映所有帖子的游標(biāo)。
Meteor.publish('posts', function() {
return Posts.find();
});
在客戶端我們需要訂閱這個發(fā)布。我們僅僅需要增加這樣一行到 main.js
文件中:
Meteor.subscribe('posts');
如果你現(xiàn)在看一眼瀏覽器,發(fā)現(xiàn)帖子都回來了。哇!好險??!
我們都做了什么?盡管我們還沒有用戶界面,至少我們已經(jīng)有了一個能用的應(yīng)用。我們可以把這個應(yīng)用部署到網(wǎng)絡(luò)上,(使用瀏覽器的控制臺)發(fā)帖子,并看到帖子顯示在其他用戶的瀏覽器上。
更多建議: