我們?cè)?jīng)輕松地通過(guò)控制臺(tái)去使用 Posts.insert
來(lái)創(chuàng)建帖子并插入到數(shù)據(jù)庫(kù)。但我們不可能指望用戶(hù)去打開(kāi)控制臺(tái)來(lái)創(chuàng)建一個(gè)新的帖子吧?
所以我們需要在用戶(hù)界面上創(chuàng)建一些表單控件,讓用戶(hù)在我們的 App 上發(fā)布一些新的帖子。
我們首先為新帖子的提交頁(yè)面定義一個(gè)路線(xiàn):
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
name: 'postPage',
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/submit', {name: 'postSubmit'});
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
定義了這條路線(xiàn)后,現(xiàn)在我們可以在頭部模板(Header)中添加一個(gè)訪(fǎng)問(wèn)我們提交頁(yè)面的鏈接:
<template name="header">
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navigation">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
</div>
<div class="collapse navbar-collapse" id="navigation">
<ul class="nav navbar-nav">
<li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{{> loginButtons}}
</ul>
</div>
</div>
</nav>
</template>
設(shè)置了這個(gè)路線(xiàn)就意味著如果用戶(hù)瀏覽 /submit
的 URL 路徑, Meteor 會(huì)顯示 postSubmit
模板。 下面讓我們來(lái)寫(xiě)這個(gè)模板吧:
<template name="postSubmit">
<form class="main form">
<div class="form-group">
<label class="control-label" for="url">URL</label>
<div class="controls">
<input name="url" id="url" type="text" value="" placeholder="Your URL" class="form-control"/>
</div>
</div>
<div class="form-group">
<label class="control-label" for="title">Title</label>
<div class="controls">
<input name="title" id="title" type="text" value="" placeholder="Name your post" class="form-control"/>
</div>
</div>
<input type="submit" value="Submit" class="btn btn-primary"/>
</form>
</template>
注意:這里有大量的標(biāo)簽樣式,只不過(guò)都來(lái)自于 Twitter Bootstrap。只有表單元素是必不可少的,樣式的設(shè)置只是讓我們的 App 更好看一點(diǎn)。在瀏覽器中顯示:
帖子提交頁(yè)面
這是一個(gè)簡(jiǎn)單的表單頁(yè)面,不需要擔(dān)心它的提交事件,因?yàn)槲覀儠?huì)通過(guò) JavaScript 攔截表單的提交事件并更新數(shù)據(jù)。(但如果你考慮到一旦禁用了 JavaScript 的話(huà), Meteor App 就會(huì)完全失效)。
讓我們將一個(gè)事件處理綁定到表單的 submit
事件。最好使用 submit
事件(而不是按鈕的 click
事件),因?yàn)檫@會(huì)覆蓋所有可能的提交方式(比如敲擊回車(chē)鍵)。
Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
post._id = Posts.insert(post);
Router.go('postPage', post);
}
});
添加一個(gè)帖子提交頁(yè)面并把鏈接放到頭部(Header)
這個(gè)函數(shù)使用 jQuery 去獲取我們表單字段的值,并填充到一個(gè)新的帖子對(duì)象。我們需要調(diào)用 event
的 preventDefault
方法來(lái)確保瀏覽器不會(huì)再繼續(xù)嘗試提交表單。
最后,我們要跳轉(zhuǎn)到新的帖子頁(yè)面。 insert()
方法把這個(gè)對(duì)象插入到數(shù)據(jù)庫(kù)并返回插入對(duì)象的 _id
值,路由器的 go()
方法將構(gòu)建一個(gè)帖子頁(yè)面的 URL 提供我們?cè)L問(wèn)。
最終的結(jié)果是用戶(hù)點(diǎn)擊提交時(shí),創(chuàng)建一個(gè)帖子,然后用戶(hù)瀏覽器將立即跳到帖子創(chuàng)建頁(yè)面。
創(chuàng)建帖子這功能看起來(lái)都很好,但我們不想讓隨機(jī)瀏覽的游客都可以這樣做:我們希望他們必須登錄。首先可以對(duì)登出的用戶(hù)隱藏帖子創(chuàng)建頁(yè)面的鏈接。不過(guò),沒(méi)有登錄的用戶(hù)仍然可以在瀏覽器控制臺(tái)中創(chuàng)建一個(gè)帖子,這是我們不能允許的。
值得慶幸的是數(shù)據(jù)安全已經(jīng)集成在 Meteor 的集合中,只是在默認(rèn)情況下它是關(guān)閉的。這樣的設(shè)置可以使你在剛開(kāi)始構(gòu)建 App 的時(shí)候更加輕松。
我們的 App 不再需要這些輔助了,果斷扔掉吧!我們?nèi)h除 insecure
包(恢復(fù)數(shù)據(jù)安全):
meteor remove insecure
Terminal 終端
執(zhí)行以后你會(huì)注意到,帖子的提交頁(yè)面不可用了。這是因?yàn)闆](méi)有了 insecure
包,從客戶(hù)端插入帖子集合已經(jīng)不再被允許了。
我們需要給出一些明確的規(guī)則告訴 Meteor ,什么時(shí)候才能允許客戶(hù)插入帖子,否則我們只能從服務(wù)端插入。
首先,為了讓我們的提交頁(yè)面再次可用,我們先展示如何允許從客戶(hù)端插入數(shù)據(jù)。事實(shí)上,我們最終還會(huì)用不同的技術(shù)去解決這個(gè)問(wèn)題,但是現(xiàn)在,先做一些簡(jiǎn)單的處理吧:
Posts = new Mongo.Collection('posts');
Posts.allow({
insert: function(userId, doc) {
// 只允許登錄用戶(hù)添加帖子
return !! userId;
}
});
Posts.allow
是告訴 Meteor:這是一些允許客戶(hù)端去修改帖子集合的條件。上面的代碼,等于說(shuō)“只要客戶(hù)擁有 userId
就允許去插入帖子”。
這個(gè)擁有 userId
用戶(hù)的修改會(huì)傳遞到 allow
和 deny
的方法(如果沒(méi)有用戶(hù)登錄就返回 null
),這個(gè)判斷通常都是準(zhǔn)確的。因?yàn)橛脩?hù)帳戶(hù)是綁定到 Meteor 核心里面的,我們可以依靠 userId
去判斷。
然而,我們?nèi)匀恍枰幚硪恍﹩?wèn)題:
讓我們來(lái)解決這些問(wèn)題吧!
讓我們首先阻止已登出的用戶(hù)看到帖子創(chuàng)建頁(yè)面。我們會(huì)在路由器中,通過(guò)定義一個(gè) 路由 Hook 。
Hook 在路由過(guò)程中進(jìn)行攔截并可能改變路由器的跳轉(zhuǎn)。你可以把它當(dāng)作一個(gè)保安,檢查你的憑據(jù)才能讓你通過(guò)(或者把你帶走)。
我們需要做的是檢查用戶(hù)是否登錄,如果他們沒(méi)有登錄,呈現(xiàn)出來(lái)的是 accessDenied
模板而不是 postSubmit
模板。 讓我們?nèi)バ薷?router.js 文件:
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
name: 'postPage',
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/submit', {name: 'postSubmit'});
var requireLogin = function() {
if (! Meteor.user()) {
this.render('accessDenied');
} else {
this.next();
}
}
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
我們還要?jiǎng)?chuàng)建拒絕訪(fǎng)問(wèn)模板:
<template name="accessDenied">
<div class="access-denied jumbotron">
<h2>Access Denied</h2>
<p>You can't get here! Please log in.</p>
</div>
</template>
當(dāng)沒(méi)有登錄的時(shí)候拒絕訪(fǎng)問(wèn)帖子創(chuàng)建頁(yè)面
如果你不登錄去訪(fǎng)問(wèn) http://localhost:3000/submit/ ,你將會(huì)看到:拒絕訪(fǎng)問(wèn)模板
路由器 Hooks 的好處是響應(yīng)式。這意味著當(dāng)用戶(hù)登錄的時(shí)候,我們不需要考慮回調(diào)或者其他類(lèi)似的方法,它就可以馬上知道。當(dāng)用戶(hù)狀態(tài)變?yōu)橐训卿浀臅r(shí)候,路由器的頁(yè)面模板立即從 accessDenied
變?yōu)?postSubmit
,而我們無(wú)需編寫(xiě)任何代碼來(lái)去控制它。
登錄,然后嘗試刷新頁(yè)面。你可能注意到,拒絕訪(fǎng)問(wèn)的頁(yè)面會(huì)短暫地出現(xiàn)在帖子創(chuàng)建頁(yè)面。這是因?yàn)樵诜?wù)器去檢測(cè)當(dāng)前用戶(hù)之前,Meteor 會(huì)盡可能快的去渲染模板。
為了避免這個(gè)問(wèn)題(這是一種常見(jiàn)的問(wèn)題,你將會(huì)看到更多去處理客戶(hù)端和服務(wù)器之間錯(cuò)綜復(fù)雜的延遲),我們將短暫顯示一個(gè)加載的畫(huà)面,騰出足夠時(shí)間讓我們?nèi)ヅ袛嘤脩?hù)是否有權(quán)訪(fǎng)問(wèn)。
畢竟在這之前,我們不知道用戶(hù)是否有正確的登錄憑證,而我們也不能直接顯示 accessDenied
或 postSubmit
模板。
所以修改 Hook 去使用我們的加載模板,同時(shí)判斷 Meteor.loggingIn()
是否為真:
//...
var requireLogin = function() {
if (! Meteor.user()) {
if (Meteor.loggingIn()) {
this.render(this.loadingTemplate);
} else {
this.render('accessDenied');
}
} else {
this.next();
}
}
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
在等待登錄的時(shí)候顯示一個(gè)加載畫(huà)面
當(dāng)他們注銷(xiāo)登錄之后就隱藏這個(gè)鏈接,這是最簡(jiǎn)單的方法去防止用戶(hù)試圖訪(fǎng)問(wèn)不被授權(quán)的頁(yè)面。我們做到這一點(diǎn)很簡(jiǎn)單:
//...
<ul class="nav navbar-nav">
{{#if currentUser}}<li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>{{/if}}
</ul>
//...
currentUser
的 Helper 是通過(guò) accounts
包提供給我們的,它相當(dāng)于是 Meteor.user()
的調(diào)用。因?yàn)樗琼憫?yīng)式的,鏈接將會(huì)在你登入或者登出的的時(shí)候出現(xiàn)或者消失。
我們讓沒(méi)有登錄的用戶(hù)無(wú)法訪(fǎng)問(wèn)帖子創(chuàng)建頁(yè)面,并且不允許這樣的用戶(hù)通過(guò)使用控制臺(tái)去創(chuàng)建帖子。然而,仍有一些更多的事情我們需要考慮:
你可能會(huì)想我們可以把這些事情放到我們的 submit
事件中去處理。但是實(shí)際上,我們很快就會(huì)遇到一系列的問(wèn)題。
因?yàn)檫@些問(wèn)題,最好是保持我們的事件處理方法里面足夠簡(jiǎn)單,如果我們所做的事情超過(guò)最基本的插入或更新數(shù)據(jù)集合,那么可以使用 Meteor 的內(nèi)置方法 。
Meteor 內(nèi)置方法是一種服務(wù)器端方法提供給客戶(hù)端調(diào)用。其實(shí)我們對(duì)它并不陌生--事實(shí)上在后臺(tái), Collection
的 insert
、update
和 remove
都屬于 Meteor 內(nèi)置方法。下面看看我們?nèi)绾巫约簛?lái)創(chuàng)建。
讓我們回到 post_submit.js
文件,不再是直接插入到 Posts
集合,我們將調(diào)用一個(gè)名為 postInsert
的內(nèi)置方法:
Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
Meteor.call('postInsert', post, function(error, result) {
// 顯示錯(cuò)誤信息并退出
if (error)
return alert(error.reason);
Router.go('postPage', {_id: result._id});
});
}
});
<%= caption "client/templates/posts/post_submit.js" %><%= highlight "10~16" %>
Meteor.call
方法通過(guò)第一個(gè)參數(shù)來(lái)調(diào)用其方法。你可以為調(diào)用方法提供參數(shù)(在這種情況下,由表單數(shù)據(jù)來(lái)構(gòu)建的 post
對(duì)象),最后加上一個(gè)回調(diào),它將在服務(wù)器的方法完成后執(zhí)行。
Meteor 方法回調(diào)總會(huì)有兩個(gè)參數(shù),error
和 result
。如果 error
參數(shù)由于某種原因存在的話(huà),我們會(huì)警告用戶(hù)(使用 return
來(lái)終止回調(diào))。如果正常運(yùn)行的話(huà),我們將用戶(hù)轉(zhuǎn)到剛剛創(chuàng)建好的帖子評(píng)論頁(yè)面。
我們趁現(xiàn)在這個(gè)機(jī)會(huì)添加 audit-argument-checks
包來(lái)增加一些安全性。
這個(gè)包讓你根據(jù)預(yù)定義的模式檢查任何 JavaScript 對(duì)象。對(duì)于我們來(lái)說(shuō),我們使用它來(lái)檢查調(diào)用方法的用戶(hù)是否登陸(通過(guò)確認(rèn) Meteor.userId()
是否是個(gè) String
字符串),然后 postAttributes
對(duì)象是否包含 title
和url
字符串,來(lái)保證我們不會(huì)添加任意數(shù)據(jù)到數(shù)據(jù)庫(kù)中。
首先讓我們定義 postInsert
方法,在我們的 collections/post.js
文件中。從 posts.js
文件中刪除 allow()
代碼塊,因?yàn)?Meteor 方法會(huì)繞過(guò)它們。
然后我們 extend
postAttributes
對(duì)象另外三個(gè)屬性:用戶(hù)的 _id
和 username
,還有帖子的 submitted
時(shí)間戳,在將整個(gè)數(shù)據(jù)插入數(shù)據(jù)庫(kù)之前,并返回給用戶(hù) _id
值(換句話(huà)說(shuō),在 JavaScript 對(duì)象里的 原始 caller 方法)
Posts = new Mongo.Collection('posts');
Meteor.methods({
postInsert: function(postAttributes) {
check(Meteor.userId(), String);
check(postAttributes, {
title: String,
url: String
});
var user = Meteor.user();
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
var postId = Posts.insert(post);
return {
_id: postId
};
}
});
注意的是 _.extend()
方法來(lái)自于 Underscore 庫(kù),作用是將一個(gè)對(duì)象的屬性傳遞給另一個(gè)對(duì)象。
因?yàn)?Meteor Methods 是在服務(wù)器上執(zhí)行,所以 Meteor 假設(shè)它們是可信任的。這樣的話(huà),Meteor 方法就會(huì)繞過(guò)任何 allow/deny 回調(diào)。
如果你真的想在服務(wù)器端每次 insert
、update
或 remove
之前,運(yùn)行一些代碼的話(huà),我們建議你查看 collection-hooks 代碼包的相關(guān)信息。
我們?cè)谕瓿蛇@個(gè)方法之前還要在添加一項(xiàng)檢查。如果之前的帖子擁有了同樣的 URL,我們不會(huì)再次添加這個(gè)鏈接,反而會(huì)引導(dǎo)用戶(hù)到已存在的帖子上。
Meteor.methods({
postInsert: function(postAttributes) {
check(this.userId, String);
check(postAttributes, {
title: String,
url: String
});
var postWithSameLink = Posts.findOne({url: postAttributes.url});
if (postWithSameLink) {
return {
postExists: true,
_id: postWithSameLink._id
}
}
var user = Meteor.user();
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
var postId = Posts.insert(post);
return {
_id: postId
};
}
});
我們?cè)跀?shù)據(jù)庫(kù)中搜尋是否存在相同的 URL。如果找到,我們 return
返回那帖子的 _id
和 postExists: true
來(lái)讓用戶(hù)知道這個(gè)特別的情況。
由于我們調(diào)用了一個(gè) return
,方法就會(huì)到此停止,而不會(huì)執(zhí)行 insert
聲明,因此優(yōu)雅地防止了任何重復(fù)。
剩下的就是用 postExists
信息通過(guò)我們客戶(hù)端的事件 helper 來(lái)顯示警告信息:
Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
Meteor.call('postInsert', post, function(error, result) {
// 向用戶(hù)顯示錯(cuò)誤信息并終止
if (error)
return alert(error.reason);
// 顯示結(jié)果,跳轉(zhuǎn)頁(yè)面
if (result.postExists)
alert('This link has already been posted(該鏈接已經(jīng)存在)');
Router.go('postPage', {_id: result._id});
});
}
});
現(xiàn)在我們已經(jīng)在所有的帖子中添加了日期,確保可以使用這個(gè)屬性去進(jìn)行帖子的分類(lèi)。這樣,我們就可以使用 Mongo 數(shù)據(jù)庫(kù)的 sort
運(yùn)算方法,根據(jù)這個(gè)字段去把對(duì)象進(jìn)行排序,并且標(biāo)識(shí)它們是升序還是降序。
Template.postsList.helpers({
posts: function() {
return Posts.find({}, {sort: {submitted: -1}});
}
});
花了一點(diǎn)功夫,我們終于有了一個(gè)用戶(hù)界面,讓用戶(hù)安全地在我們的 App 中輸入內(nèi)容!
但任何一個(gè) App 如果允許用戶(hù)去創(chuàng)建內(nèi)容,同時(shí)也需要給他們一個(gè)方式來(lái)編輯或刪除它。這就是下一章將會(huì)說(shuō)到的。
更多建議: