---
title: 編輯帖子
slug: editing-posts
date: 0008/01/01
number: 8
points: 5
photoUrl: http://www.flickr.com/photos/ikewinski/9473337133/
photoAuthor: Mike Lewinski
contents: 添加一個帖子編輯表單。|設置可編輯權限。|限制哪些屬性可以編輯。
paragraphs: 29
---
上一章,我們已經學會了創建帖子,下面來學習編輯和刪除它們。頁面的代碼非常簡單,讓我們在這個時候來談論一下 Meteor 是如何管理用戶權限。
讓我們先設置我們的路由器,添加一個可以訪問帖子編輯頁的路徑,并設置它的數據上下文:
~~~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('/posts/:_id/edit', {
name: 'postEdit',
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/submit', {name: 'postSubmit'});
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'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "15~18" %>
### 帖子編輯模板
我們可以現在專注模板了。我們的 `postEdit` 模板就包含一個相當標準的表單:
~~~html
<template name="postEdit">
<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="{{url}}" 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="{{title}}" placeholder="Name your post" class="form-control"/>
</div>
</div>
<input type="submit" value="Submit" class="btn btn-primary submit"/>
<hr/>
<a class="btn btn-danger delete" href="#">Delete post</a>
</form>
</template>
~~~
<%= caption "client/templates/posts/post_edit.html" %>
用 `post_edit.js` 來配合這個的模板:
~~~js
Template.postEdit.events({
'submit form': function(e) {
e.preventDefault();
var currentPostId = this._id;
var postProperties = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
}
Posts.update(currentPostId, {$set: postProperties}, function(error) {
if (error) {
// 向用戶顯示錯誤信息
alert(error.reason);
} else {
Router.go('postPage', {_id: currentPostId});
}
});
},
'click .delete': function(e) {
e.preventDefault();
if (confirm("Delete this post?")) {
var currentPostId = this._id;
Posts.remove(currentPostId);
Router.go('postsList');
}
}
});
~~~
<%= caption "client/templates/posts/post_edit.js" %>
相信你現在已經對這些代碼都相當的熟悉了。
我們有兩個事件回調函數:一個用于表單的 `submit` 事件,一個用于刪除鏈接的 `click` 事件。
刪除鏈接的回調函數是非常簡單的:先防止默認點擊事件,然后提示確認窗口。如果確認刪除,它將從模板的數據上下文中獲得當前帖子的 ID ,然后刪除它,最后把用戶重定向到主頁。
更新的回調函數需要長一點時間,但并不復雜。在防止默認提交事件然后獲取了當前帖子之后,我們將從表單中獲取相關字段的值,并將它們存儲在一個 `postProperties` 的對象中。
然后,我們把該對象通過 [`$set`](http://docs.mongodb.org/manual/reference/operator/update/set/) 操作符(只更新指定字段的值,保留其他字段的值)傳遞給 Meteor 的 `Collection.update()` 方法,并通過回調函數去判斷如果更新失敗就顯示錯誤信息;如果更新成功了,將自動返回到該帖子的頁面。
### 添加鏈接
我們還應該添加一個編輯帖子的鏈接,以便用戶可以訪問到帖子編輯頁面:
~~~html
<template name="postItem">
<div class="post">
<div class="post-content">
<h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
<p>
submitted by {{author}}
{{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
</p>
</div>
<a href="{{pathFor 'postPage'}}" class="discuss btn btn-default">Discuss</a>
</div>
</template>
~~~
<%= caption "client/templates/posts/post_item.html" %>
<%= highlight "5~8" %>
當然,我們不能讓你的帖子提供給其他用戶去編輯。這就要通過 `ownPost` helper 來幫忙:
~~~js
Template.postItem.helpers({
ownPost: function() {
return this.userId === Meteor.userId();
},
domain: function() {
var a = document.createElement('a');
a.href = this.url;
return a.hostname;
}
});
~~~
<%= caption "client/templates/posts/post_item.js" %>
<%= highlight "2~4" %>
<%= screenshot "8-1", "帖子編輯表單。" %>
<%= commit "8-1", "添加帖子編輯表單。" %>
我們的帖子編輯表單看起來很好,但是目前還不能夠進行任何的編輯,這是為什么?
### 設置權限
自從我們移除了 `insecure` 包,現在所有客戶端的修改都會被拒絕。
為了解決這個問題,我們需要建立一些權限規則。首先,在 `lib` 目錄下創建一個新的 `permissions.js` 文件。這樣做將會首先加載我們權限文件(它在服務端和客戶端都可以被加載到):
~~~js
// check that the userId specified owns the documents
ownsDocument = function(userId, doc) {
return doc && doc.userId === userId;
}
~~~
<%= caption "lib/permissions.js" %>
在[創建帖子](/chapter/creating-posts)這個章節,我們拋棄了 `allow()` 方法,因為我們只通過服務端方法去插入新的帖子(繞過了 `allow()` 方法)。
但是現在我們要在客戶端編輯和刪除帖子!我們回到 `posts.js` 文件并添加 `allow()` :
~~~js
Posts = new Mongo.Collection('posts');
Posts.allow({
update: function(userId, post) { return ownsDocument(userId, post); },
remove: function(userId, post) { return ownsDocument(userId, post); }
});
//...
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "3~6" %>
<%= commit "8-2", "添加基本權限來檢查帖子的擁有者。" %>
### 限制編輯
盡管你可以編輯自己的帖子,但并不意味著你可以允許去編輯帖子的**每個**屬性。例如,我們不允許用戶創建一個帖子之后,再將其分配給其他用戶。
我們用 Meteor 的 `deny()` 方法,以確保用戶只能編輯特定的字段:
~~~js
Posts = new Mongo.Collection('posts');
Posts.allow({
update: function(userId, post) { return ownsDocument(userId, post); },
remove: function(userId, post) { return ownsDocument(userId, post); }
});
Posts.deny({
update: function(userId, post, fieldNames) {
// 只能更改如下兩個字段:
return (_.without(fieldNames, 'url', 'title').length > 0);
}
});
//...
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "8~13" %>
<%= commit "8-3", "只允許更改帖子的某些屬性。" %>
代碼中的 `fieldNames` 數組,它包含了需要被修改的字段,并使用 [Underscore](http://underscorejs.org/) 的 `without()` 方法返回一個*不包含* `url` 和 `title` 字段的子數組。
正常情況下,這個數組應該是空的,它的長度應該是0。如果有人采取其他操作,這個數組的長度將變為1或更多,回調函數將返回 `true` (因此禁止更新)。
你也許注意到了在我們的代碼中沒有檢查鏈接是否重復的代碼。這就意味著用戶成功添加一個鏈接后,再編輯時就會繞過檢查。這個問題同樣可以通過為編輯帖子表單使用 Meteor 內置方法來解決,但是我們將它作為練習留給讀者。
<% note do %>
### 內置方法的回調 vs 客戶端數據操作
創建帖子,我們使用的是 `postInsert` 的內置方法,而編輯和刪除帖子,我們直接在客戶端調用 `update` 和 `remove`,并通過 `allow` 和 `deny` 去限制使用權限。
我們該如何去選擇使用呢?
當操作相對比較直觀,你可以通過 `allow` 和 `deny` 去設置你的規則的時候,直接在客戶端進行操作通常會更簡單。
然而,一旦你需要做一些在用戶控制以外的事情(比如設置一個新帖子的時間戳,或者把帖子分配到正確的用戶),這種情況使用內置方法會更好。
內置方法也適用在其他的一些情景:
- 當你需要通過內置方法的回調函數去獲得返回值的時候,而不是等待響應和同步才傳遞的數據。
- 對于一些繁重的數據庫操作,比如要提取大量的數據集合。
- 計算或者合計數據的時候(比如:計數、平均值、求和)。
[請閱讀我們的 blog](https://www.discovermeteor.com/blog/meteor-methods-client-side-operations/) 來深入了解這個話題。
<% end %>