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