-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy path08-editing-posts.md.erb
262 lines (199 loc) · 11 KB
/
08-editing-posts.md.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
---
title: Biên Tập Bài Viết
slug: editing-posts
date: 0008/01/01
number: 8
points: 5
photoUrl: http://www.flickr.com/photos/ikewinski/9473337133/
photoAuthor: Mike Lewinski
contents: Thêm form để biên tập bài viết của bạn.|Thiết lập quyền hạn biên tập.|Hạn chế thuộc tính nào có thể biên tập được.
paragraphs: 29
---
Bây giờ chúng ta đã có thể tạo bài viết, bước tiếp theo sẽ là biên tập và xoá chúng. Trong khi code UI để làm việc đó khá là dễ dàng, đây là thời điểm thích hợp để nói về việc làm thế nào Meteor quản lý quyền hạn.
Ban đầu hãy lắp ráp router trước. Chúng ta sẽ thêm vào route để truy cập trang biên tập bài viết và thiết lập ngữ cảnh dữ liệu cho nó.
~~~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" %>
### Template biên tập bài viết
Chúng ta bây giờ có thể tập trung vào template. Template `postEdit` của chúng ta sẽ khá là tiêu chuẩn:
~~~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" %>
Và sau đây là file `post_edit.js` đi kèm với nó:
~~~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) {
// display the error to the user
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" %>
Bây giờ thì phần lớn đoạn code có lẽ đã trở lên thân thuộc với bạn.
Chúng ta có hai hàm callback cho sự kiện của template: một cho sự kiện `submit` và một cho sự kiện `click` vào đường dẫn xoá.
Callback xoá khá là đơn giản: xoá bỏ sự kiện click mặc định, sau đó hỏi người dùng xác nhận. Nếu bạn nhận nó, thu được ID bài viết hiện tại từ ngữ cảnh dữ liệu của template, xoá nó, và cuối cùng đổi hướng người dùng về trang chủ (homepage).
Callback cập nhật thì dài hơn một chút, nhưng cũng không quá phức tạp hơn. Sau khi xoá bỏ sự kiện mặc định và lấy ra bài viết hiện tại, chúng ta lấy giá trị từ trường của form từ trang và lưu trữ chúng trong object `postProperties`.
Chúng ta sau đó gửi object tới Method `Collection.update()` sử dụng toán tử [`$set`](http://docs.mongodb.org/manual/reference/operator/update/set/) (thứ sẽ thay đổi một bộ các trường được đặc tả trong khi giữ nguyên những trường còn lại), và dùng callbac để hiển thị hoặc lỗi nếu việc update thất bại, hoặc đưa người dùng trở lại trang bài viết nếu như việc update thành công.
### Thêm đường dẫn
Chúng ta cũng thêm vào đường dẫn vào bài viết để người dùng có thể truy cập được vào trang biên tập bài viết.
~~~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" %>
Dĩ nhiên, chúng ta không muốn hiển thị đường dẫn biên tập tới bất kỳ ai khác. Đây là lúc mà helper `ownPost` xuất hiện:
~~~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", "Post edit form." %>
<%= commit "8-1", "Added edit posts form." %>
Trang biên tập form của chúng ta trông khá ổn, nhưng chúng ta không thực sự biên tập bất kỳ thứ gì bây giờ. Điều gì đang diễn ra?
### Thiết lập quyền hạn
Vì chúng ta đã xoá bỏ gói `insecure`, tất cả thay đổi từ client đều bị từ chối.
Để thay đổi điều này, chúng ta sẽ thiết lập một vài luật về quyền hạn. Đầu tiên, tạo một file `permissions.js` bên trong `lib`. Điều này đảm bảo rằng logic quyền hạn được nạp đầu tiên (và hữu dụng đối với cả hai môi trường):
~~~js
// check that the userId specified owns the documents
ownsDocument = function(userId, doc) {
return doc && doc.userId === userId;
}
~~~
<%= caption "lib/permissions.js" %>
Trong chương về [Tạo bài viết](/chapter/creating-posts), chúng ta đã loại bỏ Method `allow()` bởi vì chúng ta chỉ muốn chèn thêm bài viết mới từ Method phía server (thứ bỏ qua `allow()`).
Nhưng bây giờ vì chúng ta biên tập và xoá bài viết từ phía client, hãy quay trở lại `posts.js` và thêm khối `allow()` vào:
~~~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", "Added basic permission to check the post's owner." %>
### Hạn chế biên tập
Chỉ vì bạn có thể biên tập bài viết của mình, điều đó cũng không có nghĩa là bạn nên biên tập *mọi* thuộc tính. Ví dụ, chúng ta không muốn người dùng có thể tạo bài viết và gán cho một ai đó khác.
Vì vậy chúng ta sẽ dùng callback `deny()` của Meteor để chắc chắn rằng người dùng chỉ có thể biên tập một số trường nhất định:
~~~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) {
// may only edit the following two fields:
return (_.without(fieldNames, 'url', 'title').length > 0);
}
});
//...
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "8~13" %>
<%= commit "8-3", "Only allow changing certain fields of posts." %>
Chúng ta đang nói về mảng `fieldNames` mà có chứa danh cách những trường đang được thay đổi, và sử dụng Method `without` của [Underscore](http://underscorejs.org/) để trả về một mảng con chứa những trường mà *không phải* `url` hoặc `title`.
Nếu mọi việc bình thường, mảng đó sẽ trống không và độ dài của nó sẽ là 0. Nếu ai đó cố làm điều gì đó gượng ép, độ dài của mảng sẽ trở thành 1 hoặc lớn hơn, và callback sẽ trả về `true` (do đó từ chối việc cập nhật).
Bạn có thể đã nhận ra rằng không nơi nào trong đoạn code biên tập bài viết kiểm tra đường dẫn có bị trùng lặp hay . Điều này có nghĩa là một người dùng có thể submit đường dẫn và sau đó thay đổi đường dẫn của nó để vượt qua đoạn kiểm tra đó. Giải pháp cho vấn đề này có thể cũng là sử dụng Meteor method cho việc biên tập bài viết, nhưng chúng tôi sẽ dành công việc đó như một bài tập cho độc giả.
<% note do %>
### Method Calls vs điều khiển dữ liệu phía client
Để tạo bài viết, chúng ta đang sử dụng Meteor method `postInsert`, nhưng ngược lại khi biên tập và xoá chúng, chúng ta đang gọi `update` và `remove` trực tiếp ở phía client và hạn chế truy cập thông qua `allow` và `deny`.
Khi nào thì phù hợp để làm theo cách này hay cách kia?
Khi mà mọi thứ khá rõ ràng và bạn có thể diễn đạt một cách thoả đáng các luật thông qua `allow` và `deny`, thường thì sẽ đơn giản hơn khi mà làm trực tiếp trên client.
Tuy nhiên, ngay khi bạn bắt đầu cần phải làm những thứ bên ngoài tầm kiểm soát của người dùng (ví dụ như gắn tem thời gian cho bài viết hoặc chỉ định nó cho người dùng đúng), thường thì tốt hơn dùng Method.
Gọi Method cũng thường hợp lý hơn trong một vài hoàn cảnh:
- Khi mà bạn muốn biết hoặc trả về giá trị dựa vào callback hơn là đợi cho tương tác ngược và đồng bộ truyền lại.
- Cho hàm với cơ sở dữ liệu nặng, mà việc gửi dữ liệu lớn như vậy đắt giá.
- Khi tổng hợp và tập hợp dữ liệu (ví dụ như đếm, tính trung bình, tính tổng).
[Kiểm tra blog của chúng tôi](https://www.discovermeteor.com/blog/meteor-methods-client-side-operations/) cho việc khảo sát sâu hơn chủ đề này.
<% end %>