現(xiàn)在,我們已經(jīng)創(chuàng)建了一個(gè)帖子列表頁(yè)面(最終是由用戶提交的),我們還需要添加一個(gè)單獨(dú)的帖子頁(yè)面,提供給用戶評(píng)論對(duì)應(yīng)的帖子。
我們希望可以通過固定鏈接訪問到每個(gè)單獨(dú)的帖子頁(yè)面,URL 形式是 http://myapp.com/posts/xyz
(這里的 xyz
是 MongoDB 的 _id
標(biāo)識(shí)符),對(duì)于每個(gè)帖子來說是唯一的。
這意味著我們需要某些路由來看看瀏覽器的地址欄里面的路徑是什么,并相應(yīng)地顯示正確的內(nèi)容。
Iron Router 是特別為了 Meteor Apps 開發(fā)的路由包。
它不僅能幫助路由(設(shè)置路徑),還能幫助過濾(為這些路徑分配跳轉(zhuǎn)),甚至能管理訂閱(控制路徑可以訪問哪些數(shù)據(jù))。(注意:Iron Router 是由本書《Discover Meteor》的其中一名作者 Tom Coleman 參與開發(fā)的。)
首先,讓我們從 Atmosphere 中安裝這個(gè)包:
meteor add iron:router
Terminal 終端
這個(gè)命令是下載并安裝 Iron Router 包到我們的 App,這樣我們就可以使用了。請(qǐng)注意,在能夠順利使用這個(gè)包之前,你可能需要重啟你的 Meteor 應(yīng)用(通過按 ctrl + c
就能停止進(jìn)程,然后輸入 meteor
再次啟動(dòng)它)。
在本章我們會(huì)接觸很多路由器的不同功能。如果你對(duì)類似 Rails 的框架有一定實(shí)踐經(jīng)驗(yàn)的話,你可能已經(jīng)很熟悉大部分的這些詞匯概念了。但是如果沒有的話,這里有一個(gè)快速詞匯表讓你來了解一下:
/terms_of_service
)或者動(dòng)態(tài)的(/posts/xyz
),甚至還可以包含查詢參數(shù)(/search?keyword=meteor
)。/
)進(jìn)行分隔。關(guān)于更多 Iron Router 的信息,請(qǐng)查看 GitHub上面的完整文檔.
到目前為止,我們已經(jīng)使用了一些固定模板(比如 {{> postsList}}
)來為我們布局。因此,盡管我們 App 的內(nèi)容還可以更改,但是頁(yè)面的基本結(jié)構(gòu)都已經(jīng)不變了:一個(gè)頭(header),它下面是帖子列表。
Iron Router 負(fù)責(zé)處理在 HTML <body>
標(biāo)簽里面該呈現(xiàn)什么,讓我們擺脫了這個(gè)枷鎖。所以我們不會(huì)再自己去定義標(biāo)簽里面的內(nèi)容,取而代之的是,我們將路由器指定到一個(gè)包含 {{> yield}}
標(biāo)簽的布局模板。
這個(gè) {{> yield}}
標(biāo)簽將會(huì)定義一個(gè)動(dòng)態(tài)區(qū)域,它會(huì)自動(dòng)呈現(xiàn)對(duì)應(yīng)于當(dāng)前線路的相應(yīng)模板(從現(xiàn)在起,我們將指定這個(gè)特殊的模板叫 “route templates”):
布局和模板
我們將開始構(gòu)建我們的布局和添加 {{> yield}}
標(biāo)簽。首先,我們先從 main.html
文件里面刪除 <body>
標(biāo)簽,并把它的內(nèi)容放到它們共同的模板 layout.html
里面(保存在新的 client/templates/application
文件夾中)。
我們把 main.html
刪減內(nèi)容之后應(yīng)該是這樣的:
<head>
<title>Microscope</title>
</head>
client/main.html
而新創(chuàng)建的 layout.html
現(xiàn)在將會(huì)包含 App 的外層布局:
<template name="layout">
<div class="container">
<header class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<a class="navbar-brand" href="/">Microscope</a>
</div>
</header>
<div id="main" class="row-fluid">
{{> yield}}
</div>
</div>
</template>
client/templates/application/layout.html
你會(huì)注意到我們已經(jīng)把 yield
helper 取代了 postsList
模板。
完成之后,我們?yōu)g覽器標(biāo)簽會(huì)顯示 Iron Router 默認(rèn)的幫助頁(yè)面。這是因?yàn)槲覀冞€沒有告訴路由怎樣處理 /
URL,所以它僅僅呈現(xiàn)一個(gè)空的模板。
接下來,我們可以恢復(fù)之前的根路徑 /
URL 映射到 postsList
模板。然后我們?cè)诟夸泟?chuàng)建一個(gè) /lib
目錄,并在里面創(chuàng)建 router.js
文件:
Router.configure({
layoutTemplate: 'layout'
});
Router.route('/', {name: 'postsList'});
lib/router.js
我們已經(jīng)完成了兩件重要的事情。第一,我們已經(jīng)告訴路由器使用我們剛剛創(chuàng)建的 layout
模板作為所有路由的默認(rèn)布局。
第二,我們已經(jīng)定義了一個(gè)名為 postsList
的路由規(guī)則,并映射到 /
路徑。
你放在 /lib
文件夾里面的所有文件都會(huì)在你的 App 運(yùn)行的時(shí)候確保首先被加載(可能除了 smart 包)。這是放置需要隨時(shí)準(zhǔn)備使用的輔助代碼的好地方。
不過有一點(diǎn)注意的是:因?yàn)?/lib
文件夾并不是放在 /client
或 /server
文件夾里面,這意味著它的代碼將會(huì)同時(shí)存在于客戶端和服務(wù)器。
在這里我們先清除一些歧義。我們有一個(gè)路由規(guī)則,叫做叫 postsList
,同時(shí)我們也有一個(gè)名字叫 postsList
的模板。這里是怎么回事?
默認(rèn)情況下,Iron Router 會(huì)為這個(gè)路由規(guī)則,指定相同名字的模板。而如果路徑(path
參數(shù))沒有指定,它也會(huì)根據(jù)路由規(guī)則的名字,去指定同樣名字的路徑。舉個(gè)例子,在上面的設(shè)置中,如果我們不提供 path
參數(shù),那么訪問 /postsList
將會(huì)自動(dòng)獲取到 postList
模板。
你可能想知道為什么我們需要在一開始去制定路由規(guī)則。這是因?yàn)?Iron Router 的部分功能需要使用路由規(guī)則去生成 App 的鏈接信息。其中最常見的一個(gè)是 {{pathFor}}
的 Spacebars helper,它需要返回路由規(guī)則的 URL 路徑。
我們希望主頁(yè)鏈接到帖子列表頁(yè)面,所以除了指定靜態(tài)的 /
URL ,我們還可以使用 Spacebars helper。雖然它們的效果是一樣的,不過這給了我們更多的靈活性,如果我們更改了路由規(guī)則的映射路徑,helper 仍然可以輸出正確的 URL 。
<header class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
</div>
</header>
//...
client/templates/application/layout.html
如果你要部署當(dāng)前版本的 App(或啟動(dòng)起來去使用上面的鏈接),你會(huì)注意到在所有帖子完全出現(xiàn)之前,列表里面會(huì)空了一段時(shí)間。這是因?yàn)樵诘谝淮渭虞d頁(yè)面的時(shí)候,要等到 posts
訂閱完成后,即從服務(wù)器抓取完帖子的數(shù)據(jù),才能有帖子顯示在頁(yè)面上。
這應(yīng)該要有一個(gè)更好的用戶體驗(yàn),比如提供一些視覺上的反饋?zhàn)層脩糁勒谧x取數(shù)據(jù),這樣用戶才會(huì)去繼續(xù)等待。
幸好 Iron Router 給了我們一個(gè)簡(jiǎn)單的方法去實(shí)現(xiàn)它。我們把訂閱放到 waitOn
的返回上。
我們把 posts
訂閱從 main.js
移到路由文件中:
Router.configure({
layoutTemplate: 'layout',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
lib/router.js
我們這里所談?wù)摰氖菍?duì)于網(wǎng)站的每個(gè)路由(我們現(xiàn)在只有一個(gè),但是我們馬上會(huì)添加更多!)我們都訂閱了 posts
訂閱。
這和我們之前做的(訂閱原來被放在了 main.js
文件中,這文件現(xiàn)在應(yīng)該是空的了,可以刪除)關(guān)鍵區(qū)別在于 Iron Router 現(xiàn)在可以得知路由什么時(shí)候準(zhǔn)備好——即當(dāng)路由得到它需要渲染的數(shù)據(jù)時(shí)。
如果我們只是顯示一個(gè)空的模板的話,得知 postsList
路由已準(zhǔn)備好也做不了什么事情。幸好 Iron Router 自帶了一個(gè)延緩顯示模板的方法,在路由調(diào)用模板準(zhǔn)備好前,顯示一個(gè) loding
加載模板:
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
lib/router.js
注意,因?yàn)槲覀冊(cè)诼酚善骷?jí)別上全局定義了 waitOn
方法,所以這個(gè)只會(huì)在用戶第一次訪問你的 App 的時(shí)候發(fā)生一次。在那之后,數(shù)據(jù)已經(jīng)被加載到了瀏覽器的內(nèi)存,路由器不需要再次去等待它。
最后一塊拼圖是加載模板。我們將會(huì)使用 spin
包去創(chuàng)建一個(gè)帥氣的動(dòng)畫加載畫面。通過 meteor add sacha:spin
去添加它,然后在 client/templates/includes
文件夾內(nèi)創(chuàng)建 loading
模板:
<template name="loading">
{{>spinner}}
</template>
client/templates/includes/loading.html
注意 {{> spinner}}
是 spin
包中的一個(gè)模板標(biāo)簽。盡管這部分是來自我們的 App 之外,不過我們就像其他模板一樣去使用它就可以了。
這是一個(gè)好辦法去等待你的訂閱,不僅為了用戶體驗(yàn),還因?yàn)樗梢皂樌卮_保數(shù)據(jù)可以馬上體現(xiàn)在模板上。這消除了需要處理的模板被呈現(xiàn)之前,底層數(shù)據(jù)必須可用的問題,這往往需要復(fù)雜的解決方案。
響應(yīng)性是 Meteor 的一個(gè)核心部分,雖然我們沒有真正的接觸到,但我們的加載模板給了我們?nèi)ソ佑|這個(gè)概念的機(jī)會(huì)。
如果數(shù)據(jù)還沒有加載完成的時(shí)候重定向去一個(gè)加載模板是很好,不過路由器如何知道在什么時(shí)候數(shù)據(jù)加載完,然后用戶應(yīng)該要重定向回到原本的頁(yè)面呢?
剛剛我們說的這個(gè)就是響應(yīng)性的體現(xiàn),不過別擔(dān)心,很快你會(huì)了解到關(guān)于它的更多東西。
既然我們已經(jīng)看到了如何路由到 postsList
模板上,現(xiàn)在讓我們建立一個(gè)路由來顯示一個(gè)帖子的詳細(xì)信息吧。
這里有一個(gè)問題:我們不能繼續(xù)單獨(dú)定義路由規(guī)則與路徑的映射,因?yàn)榭赡苡谐汕先f個(gè)。所以我們需要建立一個(gè)動(dòng)態(tài)的路由規(guī)則,并讓路由規(guī)則去顯示我們要查看的帖子。
首先,我們將創(chuàng)建一個(gè)新的模板,簡(jiǎn)單地呈現(xiàn)相同的我們使用在帖子列表的模板。
<template name="postPage">
{{> postItem}}
</template>
client/templates/posts/post_page.html
我們以后還會(huì)添加更多的元素在這個(gè)模板上(如注釋),但現(xiàn)在它將僅僅作為放置 {{> postItem}}
的外殼。
我們準(zhǔn)備創(chuàng)建另一個(gè)路由規(guī)則,這次 URL 路徑 /posts/<ID>
映射到 postPage
模板:
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
name: 'postPage'
});
lib/router.js
這個(gè)特殊的 :_id
標(biāo)記告訴路由器兩件事:第一,去匹配任何符合 /posts/xyz/
(“xyz”可以是任意字符)格式的路線。第二,無論“xyz”里面是什么,都會(huì)把它放到路由器的 params
數(shù)組中的 _id
屬性里面去。
請(qǐng)注意,我們這里只使用 _id
只是為了方便起見。路由器是沒有辦法知道你是通過一個(gè)實(shí)際的 _id
,還是僅僅通過一些隨機(jī)的字符去訪問。
我們現(xiàn)在路由到正確的模板了,但是我們?nèi)匀宦┝艘粋€(gè)事情:路由器通過這個(gè)帖子的 _id
可以知道我們想顯示哪個(gè)帖子,但模板還沒有線索。那么,我們要如果解決這個(gè)問題呢?
值得慶幸的是,路由器有一個(gè)聰明的內(nèi)置解決方案:它允許你指定一個(gè)數(shù)據(jù)源。你可以把數(shù)據(jù)源想象成填充的一個(gè)美味的蛋糕去填充模板和布局。簡(jiǎn)單的說,就是你的模板要填上:
在我們的例子中,我們可以從 URL 上獲取 _id
,并通過它找到我們的帖子從而獲得正確的數(shù)據(jù)源:
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
name: 'postPage',
data: function() { return Posts.findOne(this.params._id); }
});
lib/router.js
所以每次用戶訪問這條路由規(guī)則,我們會(huì)找到合適的帖子并將其傳遞給模板。記住,findOne
返回的是一個(gè)與查詢相匹配的帖子,而僅僅需要提供一個(gè) id
作為參數(shù),它可以簡(jiǎn)寫成 {_id: id}
。
在路由規(guī)則的 data
方法里面,this
對(duì)應(yīng)于當(dāng)前匹配的路由對(duì)象,我們可以使用 this.params
去訪問一個(gè)比配項(xiàng)(在 path
中通過 :
前綴去表示它們)。
通過設(shè)置模板的數(shù)據(jù)源,你可以在模板 helper 里面控制 this
的值。
這個(gè)工作通常會(huì)隱式地被 {{#each}}
迭代器完成,它會(huì)自動(dòng)設(shè)置對(duì)應(yīng)的數(shù)據(jù)源到每個(gè)正在迭代的當(dāng)前項(xiàng)中:
{{#each widgets}}
{{> widgetItem}}
{{/each}}
當(dāng)然我們也可以使用 {{#with}} 去顯式地操作,它就像簡(jiǎn)單地說“拿這個(gè)對(duì)象,提供給下面的模板應(yīng)用”。例如,我們可以這樣寫:
{{#with myWidget}}
{{> widgetPage}}
{{/with}}
因此通過傳遞數(shù)據(jù)源作為參數(shù)給模板調(diào)用也可以實(shí)現(xiàn)相同的效果,所以前面的代碼塊可以重寫為:
{{> widgetPage myWidget}}
想深入了解數(shù)據(jù)源,建議閱讀我們的博客帖子。
最后,我們 要?jiǎng)?chuàng)建一個(gè)新的“評(píng)論”按鈕,并指向正確的帖子頁(yè)面。我們可以做一些像 <a href="/posts/{{_id}}">
這種動(dòng)態(tài)模式,不過使用路由 Helper 會(huì)更可靠一點(diǎn)。
我們已經(jīng)把帖子路由規(guī)則命名為 postPage
,所以我們可以使用 {{pathFor 'postPage'}}
helper :
<template name="postItem">
<div class="post">
<div class="post-content">
<h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
</div>
<a href="{{pathFor 'postPage'}}" class="discuss btn btn-default">Discuss</a>
</div>
</template>
client/templates/posts/post_item.html
不過等等,路由器到底如何準(zhǔn)確地知道從 /posts/xyz
中的哪個(gè)位置去獲得 xyz
路徑?畢竟,我們沒有傳遞任何的 _id
給它。
事實(shí)證明,Iron Router 是足夠聰明地自己去發(fā)現(xiàn)它。我們告訴路由器使用 postPage
路由規(guī)則,而路由器知道這條規(guī)則的某些地方需要使用 _id
(因?yàn)檫@是我們定義 path
的辦法)。
因此,路由器將會(huì)在 {{pathFor 'postPage'}}
的上下文環(huán)境(即 this
對(duì)象)中尋找這個(gè) _id
。而在這個(gè)例子中,this
對(duì)象對(duì)應(yīng)著一個(gè)帖子,它就是我們要尋找的擁有 _id
屬性的地方。
又或者,你可以通過傳遞 Helper 的第二個(gè)參數(shù),來明確指定需要找的 _id
在哪里。例如,{{pathFor 'postPage' someOtherPost}}
。實(shí)際情況下,如果要獲取帖子列表中前一個(gè)或者后一個(gè)的鏈接,我們就會(huì)使用這種模式。
為了看看它是否已經(jīng)正常運(yùn)作,我們?nèi)g覽帖子列表頁(yè)面并點(diǎn)擊其中一個(gè)“Discuss”的鏈接。你應(yīng)該看到類似這樣的:
一個(gè)單獨(dú)的帖子頁(yè)面
這里我們需要知道的是,這些 URL 變化的產(chǎn)生原因是正在使用 HTML5 pushState.
路由器通過處理 URLs 的點(diǎn)擊去訪問網(wǎng)站的內(nèi)部,這樣可以防止瀏覽器跳出我們的 App ,而不只是為了必要的改變 App 的狀態(tài)。
如果一切運(yùn)作正常的話,頁(yè)面應(yīng)該會(huì)瞬間改變。事實(shí)上,有時(shí)候事情變化得過快,可能需要某種類型的過渡頁(yè)面。這是本章的范圍之外的,但卻是一個(gè)有趣的話題。
讓我們別忘了路由工作兩種方式:改變我們?cè)L問的頁(yè)面 URL,也能顯示我們改變 URL 的新頁(yè)面。所以我們需要解決當(dāng)某用戶輸入錯(cuò)誤的 URL 時(shí)的情況。
幸好,Iron Rounter 可以通過 notFoundTemplate
選項(xiàng)來為我們解決這個(gè)問題。
首先,我們?cè)O(shè)置一個(gè)新模板來顯示簡(jiǎn)單的 404 錯(cuò)誤 信息:
<template name="notFound">
<div class="not-found jumbotron">
<h2>404</h2>
<p>Sorry, we couldn't find a page at this address. 抱歉,我們無法找到該頁(yè)面。</p>
</div>
</template>
client/templates/application/not_found.html
然后,我們將 Iron Rounter 指向這個(gè)模板:
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() { return Meteor.subscribe('posts'); }
});
//...
lib/router.js
為了驗(yàn)證這個(gè)錯(cuò)誤頁(yè)面,你可以嘗試隨機(jī)輸入 URL 像 http://localhost:3000/nothing-here
。
但是稍等,如果有人輸入了像 http://localhost:3000/posts/xyz
這種格式的 URL,xyz
不是一個(gè)合法的帖子 _id
怎么辦?雖然是合法的路由,但是沒有指向任何數(shù)據(jù)。
幸好,如果我們?cè)?route.js
結(jié)尾添加了特別的 dataNotFound
hook,Iron Rounter 就能足夠智能地解決這個(gè)問題。
//...
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
lib/router.js
這會(huì)告訴 Iron Router 不僅在非法路由情況下,而且在 postPage
路由,每當(dāng) data
函數(shù)返回“falsy”(比如 null
、false
、undefined
或 空)對(duì)象時(shí),顯示“無法找到”的頁(yè)面。
你也許會(huì)想知道命名“Iron Router”背后的故事。根據(jù) Iron Router 的作者 Chris Mather,因?yàn)榱餍牵╩eteor)主要由鐵(iron)元素構(gòu)成的事實(shí)。
更多建議: