我們曾經輕松地通過控制臺去使用 Posts.insert 來創(chuàng)建帖子并插入到數據庫。但我們不可能指望用戶去打開控制臺來創(chuàng)建一個新的帖子吧?
所以我們需要在用戶界面上創(chuàng)建一些表單控件,讓用戶在我們的 App 上發(fā)布一些新的帖子。
我們首先為新帖子的提交頁面定義一個路線:
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'});
定義了這條路線后,現在我們可以在頭部模板(Header)中添加一個訪問我們提交頁面的鏈接:
<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>
設置了這個路線就意味著如果用戶瀏覽 /submit 的 URL 路徑, Meteor 會顯示 postSubmit 模板。 下面讓我們來寫這個模板吧:
<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>
注意:這里有大量的標簽樣式,只不過都來自于 Twitter Bootstrap。只有表單元素是必不可少的,樣式的設置只是讓我們的 App 更好看一點。在瀏覽器中顯示:
http://wiki.jikexueyuan.com/project/discover-meteor/images/7-1.png" alt="" />帖子提交頁面
這是一個簡單的表單頁面,不需要擔心它的提交事件,因為我們會通過 JavaScript 攔截表單的提交事件并更新數據。(但如果你考慮到一旦禁用了 JavaScript 的話, Meteor App 就會完全失效)。
讓我們將一個事件處理綁定到表單的 submit 事件。最好使用 submit 事件(而不是按鈕的 click 事件),因為這會覆蓋所有可能的提交方式(比如敲擊回車鍵)。
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);
}
});
這個函數使用 jQuery 去獲取我們表單字段的值,并填充到一個新的帖子對象。我們需要調用 event 的 preventDefault 方法來確保瀏覽器不會再繼續(xù)嘗試提交表單。
最后,我們要跳轉到新的帖子頁面。 insert() 方法把這個對象插入到數據庫并返回插入對象的 _id 值,路由器的 go() 方法將構建一個帖子頁面的 URL 提供我們訪問。
最終的結果是用戶點擊提交時,創(chuàng)建一個帖子,然后用戶瀏覽器將立即跳到帖子創(chuàng)建頁面。
創(chuàng)建帖子這功能看起來都很好,但我們不想讓隨機瀏覽的游客都可以這樣做:我們希望他們必須登錄。首先可以對登出的用戶隱藏帖子創(chuàng)建頁面的鏈接。不過,沒有登錄的用戶仍然可以在瀏覽器控制臺中創(chuàng)建一個帖子,這是我們不能允許的。
值得慶幸的是數據安全已經集成在 Meteor 的集合中,只是在默認情況下它是關閉的。這樣的設置可以使你在剛開始構建 App 的時候更加輕松。
我們的 App 不再需要這些輔助了,果斷扔掉吧!我們去刪除 insecure 包(恢復數據安全):
meteor remove insecure
Terminal 終端
執(zhí)行以后你會注意到,帖子的提交頁面不可用了。這是因為沒有了 insecure 包,從客戶端插入帖子集合已經不再被允許了。
我們需要給出一些明確的規(guī)則告訴 Meteor ,什么時候才能允許客戶插入帖子,否則我們只能從服務端插入。
首先,為了讓我們的提交頁面再次可用,我們先展示如何允許從客戶端插入數據。事實上,我們最終還會用不同的技術去解決這個問題,但是現在,先做一些簡單的處理吧:
Posts = new Mongo.Collection('posts');
Posts.allow({
insert: function(userId, doc) {
// 只允許登錄用戶添加帖子
return !! userId;
}
});
Posts.allow 是告訴 Meteor:這是一些允許客戶端去修改帖子集合的條件。上面的代碼,等于說“只要客戶擁有 userId 就允許去插入帖子”。
這個擁有 userId 用戶的修改會傳遞到 allow 和 deny 的方法(如果沒有用戶登錄就返回 null),這個判斷通常都是準確的。因為用戶帳戶是綁定到 Meteor 核心里面的,我們可以依靠 userId 去判斷。
然而,我們仍然需要處理一些問題:
讓我們來解決這些問題吧!
讓我們首先阻止已登出的用戶看到帖子創(chuàng)建頁面。我們會在路由器中,通過定義一個 路由 Hook 。
Hook 在路由過程中進行攔截并可能改變路由器的跳轉。你可以把它當作一個保安,檢查你的憑據才能讓你通過(或者把你帶走)。
我們需要做的是檢查用戶是否登錄,如果他們沒有登錄,呈現出來的是 accessDenied 模板而不是 postSubmit 模板。 讓我們去修改 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'});
我們還要創(chuàng)建拒絕訪問模板:
<template name="accessDenied">
<div class="access-denied jumbotron">
<h2>Access Denied</h2>
<p>You can't get here! Please log in.</p>
</div>
</template>
如果你不登錄去訪問 http://localhost:3000/submit/ ,你將會看到:拒絕訪問模板
路由器 Hooks 的好處是響應式。這意味著當用戶登錄的時候,我們不需要考慮回調或者其他類似的方法,它就可以馬上知道。當用戶狀態(tài)變?yōu)橐训卿浀臅r候,路由器的頁面模板立即從 accessDenied 變?yōu)?postSubmit,而我們無需編寫任何代碼來去控制它。
登錄,然后嘗試刷新頁面。你可能注意到,拒絕訪問的頁面會短暫地出現在帖子創(chuàng)建頁面。這是因為在服務器去檢測當前用戶之前,Meteor 會盡可能快的去渲染模板。
為了避免這個問題(這是一種常見的問題,你將會看到更多去處理客戶端和服務器之間錯綜復雜的延遲),我們將短暫顯示一個加載的畫面,騰出足夠時間讓我們去判斷用戶是否有權訪問。
畢竟在這之前,我們不知道用戶是否有正確的登錄憑證,而我們也不能直接顯示 accessDenied 或 postSubmit 模板。
所以修改 Hook 去使用我們的加載模板,同時判斷 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'});
當他們注銷登錄之后就隱藏這個鏈接,這是最簡單的方法去防止用戶試圖訪問不被授權的頁面。我們做到這一點很簡單:
//...
<ul class="nav navbar-nav">
{{#if currentUser}}<li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>{{/if}}
</ul>
//...
currentUser 的 Helper 是通過 accounts 包提供給我們的,它相當于是 Meteor.user() 的調用。因為它是響應式的,鏈接將會在你登入或者登出的的時候出現或者消失。
我們讓沒有登錄的用戶無法訪問帖子創(chuàng)建頁面,并且不允許這樣的用戶通過使用控制臺去創(chuàng)建帖子。然而,仍有一些更多的事情我們需要考慮:
你可能會想我們可以把這些事情放到我們的 submit 事件中去處理。但是實際上,我們很快就會遇到一系列的問題。
因為這些問題,最好是保持我們的事件處理方法里面足夠簡單,如果我們所做的事情超過最基本的插入或更新數據集合,那么可以使用 Meteor 的內置方法 。
Meteor 內置方法是一種服務器端方法提供給客戶端調用。其實我們對它并不陌生--事實上在后臺, Collection 的 insert、update 和 remove 都屬于 Meteor 內置方法。下面看看我們如何自己來創(chuàng)建。
讓我們回到 post_submit.js 文件,不再是直接插入到 Posts 集合,我們將調用一個名為 postInsert 的內置方法:
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) {
// 顯示錯誤信息并退出
if (error)
return alert(error.reason);
Router.go('postPage', {_id: result._id});
});
}
});
<%= caption "client/templates/posts/post_submit.js" %> <%= highlight "10~16" %>
Meteor.call 方法通過第一個參數來調用其方法。你可以為調用方法提供參數(在這種情況下,由表單數據來構建的 post 對象),最后加上一個回調,它將在服務器的方法完成后執(zhí)行。
Meteor 方法回調總會有兩個參數,error 和 result。如果 error 參數由于某種原因存在的話,我們會警告用戶(使用 return 來終止回調)。如果正常運行的話,我們將用戶轉到剛剛創(chuàng)建好的帖子評論頁面。
我們趁現在這個機會添加 audit-argument-checks 包來增加一些安全性。
這個包讓你根據預定義的模式檢查任何 JavaScript 對象。對于我們來說,我們使用它來檢查調用方法的用戶是否登陸(通過確認 Meteor.userId() 是否是個 String 字符串),然后 postAttributes 對象是否包含 title 和url 字符串,來保證我們不會添加任意數據到數據庫中。
首先讓我們定義 postInsert 方法,在我們的 collections/post.js 文件中。從 posts.js 文件中刪除 allow() 代碼塊,因為 Meteor 方法會繞過它們。
然后我們 extend postAttributes 對象另外三個屬性:用戶的 _id 和 username,還有帖子的 submitted 時間戳,在將整個數據插入數據庫之前,并返回給用戶 _id 值(換句話說,在 JavaScript 對象里的 原始 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() 方法來自于 Underscore 庫,作用是將一個對象的屬性傳遞給另一個對象。
因為 Meteor Methods 是在服務器上執(zhí)行,所以 Meteor 假設它們是可信任的。這樣的話,Meteor 方法就會繞過任何 allow/deny 回調。
如果你真的想在服務器端每次 insert、update 或 remove 之前,運行一些代碼的話,我們建議你查看 collection-hooks 代碼包的相關信息。
我們在完成這個方法之前還要在添加一項檢查。如果之前的帖子擁有了同樣的 URL,我們不會再次添加這個鏈接,反而會引導用戶到已存在的帖子上。
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
};
}
});
我們在數據庫中搜尋是否存在相同的 URL。如果找到,我們 return 返回那帖子的 _id 和 postExists: true 來讓用戶知道這個特別的情況。
由于我們調用了一個 return,方法就會到此停止,而不會執(zhí)行 insert 聲明,因此優(yōu)雅地防止了任何重復。
剩下的就是用 postExists 信息通過我們客戶端的事件 helper 來顯示警告信息:
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) {
// 向用戶顯示錯誤信息并終止
if (error)
return alert(error.reason);
// 顯示結果,跳轉頁面
if (result.postExists)
alert('This link has already been posted(該鏈接已經存在)');
Router.go('postPage', {_id: result._id});
});
}
});
現在我們已經在所有的帖子中添加了日期,確保可以使用這個屬性去進行帖子的分類。這樣,我們就可以使用 Mongo 數據庫的 sort 運算方法,根據這個字段去把對象進行排序,并且標識它們是升序還是降序。
Template.postsList.helpers({
posts: function() {
return Posts.find({}, {sort: {submitted: -1}});
}
});
花了一點功夫,我們終于有了一個用戶界面,讓用戶安全地在我們的 App 中輸入內容!
但任何一個 App 如果允許用戶去創(chuàng)建內容,同時也需要給他們一個方式來編輯或刪除它。這就是下一章將會說到的。