-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy path09-errors.md.erb
554 lines (414 loc) · 34.5 KB
/
09-errors.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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
---
title: รับมือกับความผิดพลาด
slug: errors
date: 0009/01/01
number: 9
contents: สร้างกลไกแสดงข้อผิดพลาดและข้อความต่างๆ|ตรวจสอบความถูกต้องของข้อมูลอย่างเข้มข้น|แสดงรายงานข้อผิดพลาดลงบนฟอร์ม
paragraphs: 31
---
การใช้แค่ไดอะล็อกพื้นฐานอย่าง `alert()` เพื่อแสดงข้อความเตือนเมื่อเกิดปัญหากับข้อมูลที่ส่งเข้ามาทางฟอร์มไม่ใช่อะไรที่น่าพอใจนัก ที่สำคัญคือ มันไม่ได้ช่วยอะไรเรื่อง UX เลย ซึ่งพวกเราน่าจะทำได้ดีกว่านั้น
โดยเราน่าจะสร้างกลไกรายงานข้อผิดพลาดขึ้นใหม่ให้ดีกว่าเดิม สามารถแจ้งเตือนผู้ใช้ได้ว่าเกิดอะไรขึ้น โดยไม่ขัดขวางการทำงานที่ดำเนินไปตามปกติ
ซึ่งสิ่งที่เราจะสร้างนี้ก็คือ ระบบง่ายๆใช้แสดงข้อผิดพลาด ที่ด้านมุมขวาบนของหน้าจอ คล้ายๆกับแอพของ Mac ที่ชื่อ [Growl](http://growl.info/)
### รู้จักกับคอลเลกชั่นแบบโลคอล (Local Collections)
ก่อนจะเริ่ม เราจำเป็นต้องสร้างคอลเลกชั่นเพื่อใช้เก็บข้อผิดพลาดซะก่อน โดยให้เก็บเฉพาะข้อผิดพลาดของเซสชั่นที่กำลังใช้งานอยู่เท่านั้น และไม่จำเป็นต้องจัดเก็บลงฐานข้อมูลด้วย โดยเราจะสร้างคอลเลกชั่นใหม่นี้ให้เป็น *คอลเลกชั่นแบบโลคอล* หมายความว่า คอลเลกชั่นนี้จะถูกเก็บไว้ที่เบราว์เซอร์เท่านั้น ไม่มีการส่งกลับไปที่เซิร์ฟเวอร์
เพื่อให้เป็นไปตามนี้ เราก็จะสร้างคอลเลกชั่นของข้อผิดพลาดไว้ในโฟลเดอร์ `client` (เพื่อให้เรียกใช้จากฝั่งไคลเอนต์เท่านั้น) โดยระบุชื่อของคอลเลกชั่น MongoDB เป็น `null` (เนื่องจากเราจะไม่เก็บข้อมูลของคอลเลกชั่นนี้ลงฐานข้อมูลที่เซิร์ฟเวอร์เลย)
~~~js
// Local (client-only) collection
Errors = new Mongo.Collection(null);
~~~
<%= caption "client/helpers/errors.js" %>
เมื่อมีคอลเลกชั่นแล้ว เราก็สร้างฟังก์ชั่น `throwError` เพื่อใส่ข้อผิดพลาดเข้าไปในนั้น โดยเราไม่ต้องกังวลเรื่อง `allow` หรือ `deny` หรือความปลอดภัยอื่นๆ เนื่องจากคอลเลกชั่นนี้เป็นของผู้ใช้คนปัจจุบันเท่านั้น
~~~js
throwError = function(message) {
Errors.insert({message: message})
}
~~~
<%= caption "client/helpers/errors.js" %>
ข้อดีของการนำคอลเลกชั่นแบบโลคอลมาใช้เก็บข้อผิดพลาด ที่เหมือนกับคอลเลกชั่นแบบอื่่นๆ ก็คือ ความเป็นรีแอคทีฟ ซึ่งหมายความว่า เราสามารถแสดงข้อความผิดพลาดได้ในแบบรีแอคทีฟเหมือนๆกับที่เราแสดงข้อมูลจากคอลเลกชั่นอื่นๆนั่นเอง
### แสดงข้อผิดพลาด
เราจะแทรกข้อผิดพลาดไว้ที่ส่วนบนของไฟล์เลย์เอาท์ของเรา ดังนี้
~~~html
<template name="layout">
<div class="container">
{{> header}}
{{> errors}}
<div id="main">
{{> yield}}
</div>
</div>
</template>
~~~
<%= caption "client/templates/application/layout.html" %>
<%= highlight "4" %>
และสร้างเทมเพลท `errors` และ `error` ในไฟล์ `errors.html`
~~~html
<template name="errors">
<div class="errors">
{{#each errors}}
{{> error}}
{{/each}}
</div>
</template>
<template name="error">
<div class="alert alert-danger" role="alert">
<button type="button" class="close" data-dismiss="alert">×</button>
{{message}}
</div>
</template>
~~~
<%= caption "client/templates/includes/errors.html" %>
<% note do %>
### เทมเพลทคู่
คุณอาจสังเกตุเห็นว่าเราได้ใส่เทมเพลทสองตัวในไฟล์เดียวกัน ที่ผ่านมาเราใช้แบบ "หนึ่งไฟล์ หนึ่งเทมเพลท" สำหรับ Meteor แล้ว การที่เราเอาเทมเพลททั้งหมดมารวมไว้ในไฟล์เดียวกันไม่ทำให้เกิดปัญหาอะไร (แต่มันอาจทำให้เราสับสนได้ ถ้าเอามารวมไว้ที่ `main.html` ไฟล์เดียว)
ในกรณีนี้ เนื่องจากเทมเพลททั้งสองค่อนข้างสั้น เราเลยขอยกเว้นและนำมันมารวมไว้ที่ไฟล์เดียวกันเพื่อให้ไฟล์ที่เก็บไว้ทั้งหมดดูโล่งขึ้นอีกนิด
<% end %>
ตอนนี้เราก็เหลือแค่สร้างตัวช่วยเทมเพลท จากนั้นเราก็พร้อมจะไปกันต่อ!
~~~js
Template.errors.helpers({
errors: function() {
return Errors.find();
}
});
~~~
<%= caption "client/templates/includes/errors.js" %>
ถึงตรงนี้ คุณก็พร้อมที่จะทดสอบการแสดงข้อผิดพลาดนี้ด้วยตัวเองแล้ว แค่เปิดคอนโซลของเบราว์เซอร์และพิมพ์
~~~js
throwError("I'm an error!");
~~~
<%= screenshot "9-1", "Testing error messages." %>
<%= commit "9-1", "Basic error reporting." %>
<% note do %>
### ข้อผิดพลาดสองรูปแบบ
เป็นเรื่องสำคัญที่เราต้องแยกแยะความแตกต่างระหว่างข้อผิดพลาดในระดับแอพ `app-level` และในระดับโค้ด `code-level`
ข้อผิดพลาดในระดับแอพ โดยทั่วไปเกิดจากการทำงานของผู้ใช้ และผู้ใช้งานก็สามารถจัดการพวกมันได้ ที่เห็นได้ชัดคือ ข้อผิดพลาดจากการตรวจสอบ ข้อผิดพลาดจากสิทธิการใช้งาน ข้อผิดพลาดจาก `not-found` และอื่นๆ ซึ่งข้อผิดพลาดเหล่านี้เป็นสิ่งที่เราต้องแสดงต่อผู้ใช้ เพื่อช่วยให้พวกเค้าแก้ไขปัญหาที่กำลังเกิดขึ้นได้
ข้อผิดพลาดในระดับโค้ดนั้น เป็นอีกเรื่องนึง ส่วนใหญ่เกิดมาจากบั๊กในโค้ดที่คุณเขียน คาดเดาไม่ได้ และคุณก็ *ไม่อยาก* จะแสดงให้ผู้ใช้เห็นโดยตรง แต่อาจจะแค่ต้องการติดตามมันด้วยบริการติดตามข้อผิดพลาดที่เปิดให้บริการอยู่ก็พอ (เช่นที่ [Kadira](http://kadira.io) )
โดยในบทนี้เราจะเน้นที่ข้อผิดพลาดในระดับแอพเท่านั้น ไม่ใช่การไล่หาบั๊กแต่อย่างใด
<% end %>
### สร้างข้อผิดพลาด
ตอนนี้เราก็รู้วิธีแสดงข้อผิดพลาดแล้ว แต่ก่อนที่เราจะเห็นมันเราก็ต้องทำให้มันเกิดขึ้นซะก่อน ซึ่งที่ผ่านมาเราได้เตรียมโค้ดรองรับเมื่อมีข้อผิดพลาดไว้ดีอยู่แล้ว เช่น การเตือนเมื่อข่าวที่โพสท์ซ้ำกัน ตอนนี้เราก็แค่เปลี่ยนฟังก์ชัน `alert` ในตัวช่วยเหตุการณ์ `postSubmit` ให้เป็นฟังก์ชัน `throwError` ดังนี้
~~~js
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) {
// display the error to the user and abort
if (error)
return throwError(error.reason);
// show this result but route anyway
if (result.postExists)
throwError('This link has already been posted');
Router.go('postPage', {_id: result._id});
});
}
});
~~~
<%= caption "client/templates/posts/post_submit.js" %>
<%= highlight "13,17" %>
จากนั้น เราก็จะทำเหมือนกันที่ตัวช่วยเหตุการณ์ `postEdit`
~~~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
throwError(error.reason);
} else {
Router.go('postPage', {_id: currentPostId});
}
});
},
//...
});
~~~
<%= caption "client/templates/posts/post_edit.js" %>
<%= highlight "15" %>
<%= commit "9-2", "Actually use the error reporting." %>
มาทดสอบกันดูหน่อย ลองสร้างข่าวใหม่โดยป้อน URL เป็น `http://meteor.com` ให้ซ้ำกับข่าวเดิมที่สร้างไว้แล้ว คุณก็จะเห็นอะไรแบบนี้
<%= screenshot "9-2", "Triggering an error" %>
### ลบข้อผิดพลาด
คุณน่าจะสังเกตุเห็นว่า ข้อความผิดพลาดต่างๆจะเลือนหายไปเองในเวลาไม่กี่วินาที ทั้งนี้ก็เพราะความมหัศจรรย์ของ CSS ที่เราใส่ไว้ในสไตล์ชีตเมื่อตอนเริ่มต้นของหนังสือเล่มนี้
~~~css
@keyframes fadeOut {
0% {opacity: 0;}
10% {opacity: 1;}
90% {opacity: 1;}
100% {opacity: 0;}
}
//...
.alert {
animation: fadeOut 2700ms ease-in 0s 1 forwards;
//...
}
~~~
<%= caption "client/stylesheets/style.css" %>
ที่เราทำคือ สร้างอนิเมชั่น `fadeOut` ใน CSS ให้มี 4 คีย์เฟรม โดยกำหนดค่าความทึบแสงให้แตกต่างกัน (ณ ตำแหน่ง 0%, 10%, 90%, และ 100% ของการเกิดอนิเมชั่น) และใช้อนิเมชั่นนี้กับคลาส `.alert`
โดยอนิเมชั่นนี้จะใช้เวลาทั้งหมด 2700 มิลลิวินาที ด้วยค่าที่กำหนดคือ ใช้สูตรเวลาแบบ `ease-in` , รันแบบหน่วง 0 วินาที , รันหนึ่งครั้ง และให้แสดงที่คีย์เฟรมสุดท้ายหลังจากรันจบ
<% note do %>
### อนิเมชั่นแบบไหนดี
คุณอาจกำลังสงสัยว่าทำไมเราใช้อนิเมชั่นแบบ CSS (ซึ่งถูกกำหนดไว้ล่วงหน้าและอยู่นอกเหนือการควบคุมของแอพ) แทนที่จะใช้อนิเมชั่นที่ควบคุมจาก Meteor เอง
ถึงแม้ Meteor จะรองรับการสร้างอนิเมชั่นได้หลากหลาย แต่เนื่องจากเราต้องการให้บทนี้เน้นที่ข้อผิดพลาด เราจึงเลือกที่จะใช้อนิเมชั่นแบบง่ายๆของ CSS และเก็บสิ่งที่น่าสนใจไว้ในบทอนิเมชั่นโดยเฉพาะ
<% end %>
ดูเหมือนว่าจะใช้การได้แล้ว แต่ถ้าคุณลองทำให้เกิดข้อผิดพลาดหลายๆครั้ง (เช่น ป้อนค่าลิงก์ที่เหมือนๆกันซักสามครั้ง) คุณก็จะเห็นข้อผิดพลาดนั้นเลื่อนตำแหน่งลงมาเรื่อยๆ
<%= screenshot "9-3", "Stack overflow." %>
ที่เป็นแบบนี้ก็เพราะในขณะที่ตัว `.alert` ดูเลือนหายไป แต่มันยังคงอยู่ใน DOM ไม่ได้หายไปไหน ซึ่งเป็นเรื่องที่เราต้องแก้ไข
สถานะการณ์แบบนี้จะเข้าทาง Meteor พอดี เนื่องจากคอลเลกชั่นข้อผิดพลาดเป็นแหล่งข้อมูลแบบรีแอคทีฟ สิ่งที่เราต้องทำเพื่อกำจัดข้อผิดพลาดเก่าก็แค่ลบมันออกจากคอลเลกชั่นเท่านั้น!
โดยเราจะใช้คำสั่ง `Meteor.setTimeout` เพื่อกำหนดให้ฟังก์ชั่น callback ทำการลบข้อผิดพลาดออกหลังจากหมดเวลาที่ตั้งไว้ (ในกรณีนี้คือ 3000 มิลลิวินาที)
~~~js
Template.errors.helpers({
errors: function() {
return Errors.find();
}
});
Template.error.onRendered(function() {
var error = this.data;
Meteor.setTimeout(function () {
Errors.remove(error._id);
}, 3000);
});
~~~
<%= caption "client/templates/includes/errors.js" %>
<%= highlight "7~12" %>
<%= commit "9-3", "Clear errors after 3 seconds." %>
ฟังก์ชัน callback ของเหตุการณ์ [`onRendered`](http://docs.meteor.com/#/full/template_onRendered) จะทำงานหลังจากที่เบราว์เซอร์ได้แสดงเทมเพลทแล้ว โดย `this` ในฟังก์ชัน callback คือ ตัวเทมเพลทที่กำลังใช้งานอยู่ และ `this.data` ก็คือข้อมูลที่ถูกแสดงนั่นเอง (ในกรณีนี้คือ ข้อผิดพลาด)
### ค้นหาความถูกต้อง
จนถึงตรงนี้เรายังไม่ได้กำหนดวิธีการตรวจสอบหน้าฟอร์มของเราไว้เลย ซึ่งอย่างน้อยที่สุดที่เราควรทำก็คือ ให้ผู้ใช้ป้อนข่าวที่มีทั้ง URL และชื่อเข้ามา ดังนั้นเราก็ต้องทำให้แน่ใจว่าพวกเค้าจะทำอย่างนั้นได้
โดยเราจะทำสองอย่างเพื่อเตือนให้ผู้ใช้รู้ว่ามีข้อมูลตรงไหนที่ขาดหายไป อย่างแรก เราจะใส่ CSS class ที่ `div` ตัวนอกของฟิลด์ที่มีปัญหา และอย่างที่สอง เราจะแสดงข้อความผิดพลาดที่มีประโยชน์ข้างใต้ฟิลด์นั้น
เราเริ่มด้วยการเตรียมเทมเพลท `postSubmit` ให้รองรับตัวช่วยใหม่ตามนี้
~~~html
<template name="postSubmit">
<form class="main form page">
<div class="form-group {{errorClass 'url'}}">
<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"/>
<span class="help-block">{{errorMessage 'url'}}</span>
</div>
</div>
<div class="form-group {{errorClass 'title'}}">
<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"/>
<span class="help-block">{{errorMessage 'title'}}</span>
</div>
</div>
<input type="submit" value="Submit" class="btn btn-primary"/>
</form>
</template>
~~~
<%= caption "client/templates/posts/post_submit.html" %>
<%= highlight "3,7,10,14" %>
สังเกตุด้วยว่าเราส่งค่าพารามิเตอร์ (`url` และ `title` ตามลำดับ) ไปที่ฟังก์ชั่นตัวช่วยแต่ละตัว โดยเรียกใช้ตัวช่วยตัวเดียวกันซ้ำสองครั้ง แต่เปลี่ยนการทำงานของมันตามค่าพารามิเตอร์ที่ส่งเข้าไป
ได้เวลาสนุกกันแล้ว ตอนนี้เราจะลองเอาตัวช่วยพวกนี้มาใช้ให้เกิดประโยชน์กันดู
เราจะใช้ **เซสชั่น** เพื่อเก็บค่าอ็อบเจกต์ `postSubmitErrors` ที่มีข้อความผิดพลาดอยู่ข้างใน เมื่อผู้ใช้เริ่มทำงานกับฟอร์ม อ็อบเจกต์นี้จะเปลี่ยนแปลงค่าไปและทำให้เกิดการอัพเดทที่หน้าจอแบบรีแอคทีฟ
แรกสุด เราจะกำหนดค่าเริ่มต้นให้อ็อบเจกต์นี้เมื่อเทมเพลท `postSubmit` ถูกสร้างขึ้น เพื่อให้แน่ใจว่าผู้ใช้จะไม่เห็นข้อความผิดพลาดเดิมจากการใช้งานก่อนหน้านั้น
จากนั้นเราจะสร้างตัวช่วยเทมเพลทขึ้นมาสองตัว ที่คอยตรวจดูค่าคุณสมบัติ `field` ของ `Session.get('postSubmitErrors')` (โดยที่ `field` เป็นได้ทั้ง `url` หรือ `title` ขึ้นอยู่กับว่าเราเรียกใช้ตัวช่วยเทมเพลทจากตรงไหน)
โดยตัวช่วย `errorMessage` จะคืนค่าข้อความผิดพลาดมาให้ แต่ `errorClass` จะตรวจดูว่า *มี* ข้อความหรือไม่ และคืนค่า `has-error` ถ้าพบว่ามีข้อความอยู่
~~~js
Template.postSubmit.onCreated(function() {
Session.set('postSubmitErrors', {});
});
Template.postSubmit.helpers({
errorMessage: function(field) {
return Session.get('postSubmitErrors')[field];
},
errorClass: function (field) {
return !!Session.get('postSubmitErrors')[field] ? 'has-error' : '';
}
});
//...
~~~
<%= caption "client/templates/posts/post_submit.js" %>
<%= highlight "1~12" %>
คุณสามารถทดสอบตัวช่วยว่าทำงานถูกต้องหรือไม่ โดยเปิดคอนโซลของเบราว์เซอร์และป้อนโค้ดต่อไปนี้
~~~js
Session.set('postSubmitErrors', {title: 'Warning! Intruder detected. Now releasing robo-dogs.'});
~~~
<%= caption "Browser console" %>
<%= screenshot "9-4", "Red alert! Red alert!" %>
ขั้นตอนต่อไปคือ ผูกค่าของเซสชั่นอ็อบเจกต์ `postSubmitErrors` เข้ากับฟอร์ม
ก่อนที่จะทำตรงนั้น ให้เราสร้างฟังก์ชันใหม่ `validatePost` ใน `posts.js` เพื่อใช้ตรวจดูอ็อบเจกต์ `post` และคืนอ็อบเจกต์ `errors` ที่ประกอบด้วยข้อความผิดพลาดที่เกิดขึ้น (โดยตั้งชื่อคีย์เป็น `title` หรือ `url` ตามชื่อฟิลด์ที่ไม่มีข้อมูล )
~~~js
//...
validatePost = function (post) {
var errors = {};
if (!post.title)
errors.title = "Please fill in a headline";
if (!post.url)
errors.url = "Please fill in a URL";
return errors;
}
//...
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "3~13" %>
ซึ่งเราจะเรียกใช้ฟังก์ชันนี้จากตัวช่วยเหตุการณ์ `postSubmit`
~~~js
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()
};
var errors = validatePost(post);
if (errors.title || errors.url)
return Session.set('postSubmitErrors', errors);
Meteor.call('postInsert', post, function(error, result) {
// display the error to the user and abort
if (error)
return throwError(error.reason);
// show this result but route anyway
if (result.postExists)
throwError('This link has already been posted');
Router.go('postPage', {_id: result._id});
});
}
});
~~~
<%= caption "client/templates/posts/post_submit.js" %>
<%= highlight "10~12" %>
ให้สังเกตุด้วยว่า ที่เราใช้ `return` ก็เพื่อยกเลิกการทำงานของตัวช่วยเมื่อเกิดความผิดพลาดขึ้น แต่ไม่ใช่เพราะเราต้องการคืนค่านี้ออกมา
<%= screenshot "9-5", "Caught red-handed." %>
### การตรวจสอบฝั่งเซิร์ฟเวอร์
ดูเหมือนว่าเรายังทำไม่เสร็จซะทีเดียว เราได้ตรวจสอบว่ามีการป้อนข้อมูล URL และชื่อข่าวที่ฝั่ง *ไคลเอนต์* แต่บนฝั่ง *เซิร์ฟเวอร์* ล่ะ อาจมีใครบางคนพยายามป้อนข่าวแบบว่างๆ ด้วยการเรียกใช้เมธอด `postInsert` ผ่านคอนโซลของเบราว์เซอร์โดยตรงก็ได้
ถึงแม้ว่าเราไม่จำเป็นต้องแสดงข้อความผิดพลาดบนเซิร์ฟเวอร์ เราก็ยังสามารถใช้ฟังก์ชัน `validatePost` ตัวเดิมได้ เว้นเสียแต่ว่า ครั้งนี้เราจะเรียกใช้มันจากในเมธอด `postInsert` ไม่ใช่แค่เรียกใช้จากตัวช่วยเหตุการณ์เท่านั้น
~~~js
Meteor.methods({
postInsert: function(postAttributes) {
check(this.userId, String);
check(postAttributes, {
title: String,
url: String
});
var errors = validatePost(postAttributes);
if (errors.title || errors.url)
throw new Meteor.Error('invalid-post', "You must set a title and URL for your post");
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
};
}
});
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "9~11" %>
ย้ำอีกครั้งว่า ผู้ใช้งานผ่านหน้าจอปกติไม่ควรต้องเห็นข้อความ "You must set a title and URL for your post" นี้ เพราะมันจะปรากฎให้เห็นเฉพาะกับผู้ที่ใช้งานโดยไม่ผ่านหน้าจอปกติ แต่ใช้งานผ่านคอนโซลโดยตรงเท่านั้น
ลองทดสอบกันดู โดยเปิดคอนโซลของเบราว์เซอร์ แล้วลองป้อนโพสท์ข่าวที่ไม่มี URL ตามนี้
~~~js
Meteor.call('postInsert', {url: '', title: 'No URL here!'});
~~~
ถ้าเราทำทุกอย่างถูกต้อง คุณจะได้รับโค้ดข้อมูลที่ค่อนข้างเยอะกลับมาพร้อมด้วยข้อความ "You must set a title and URL for your post"
<%= commit "9-4", "Validate post contents on submission." %>
### ตรวจสอบเมื่อทำการแก้ไข
ก่อนจะจบงาน เราจะใช้วิธีการตรวจสอบแบบเดียวกันนี้กับหน้าฟอร์ม *edit* ของเราเช่นกัน โดยโค้ดที่ได้จะดูคล้ายๆกัน ตัวแรกคือเทมเพลท
~~~html
<template name="postEdit">
<form class="main form page">
<div class="form-group {{errorClass 'url'}}">
<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"/>
<span class="help-block">{{errorMessage 'url'}}</span>
</div>
</div>
<div class="form-group {{errorClass 'title'}}">
<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"/>
<span class="help-block">{{errorMessage 'title'}}</span>
</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" %>
<%= highlight "3,7,10,14" %>
ต่อมาก็ตัวช่วยเทมเพลท
~~~js
Template.postEdit.onCreated(function() {
Session.set('postEditErrors', {});
});
Template.postEdit.helpers({
errorMessage: function(field) {
return Session.get('postEditErrors')[field];
},
errorClass: function (field) {
return !!Session.get('postEditErrors')[field] ? 'has-error' : '';
}
});
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()
}
var errors = validatePost(postProperties);
if (errors.title || errors.url)
return Session.set('postEditErrors', errors);
Posts.update(currentPostId, {$set: postProperties}, function(error) {
if (error) {
// display the error to the user
throwError(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" %>
<%= highlight "1~12,25~27,32" %>
ก็เหมือนกับที่เราทำกับฟอร์มสร้างข่าว เราต้องตรวจสอบความถูกต้องของข่าวที่เซิร์ฟเวอร์ด้วย ยกเว้นแต่ว่า เราไม่ได้ใช้เมธอดเพื่อแก้ไขข่าว แต่เรียกใช้ `update` โดยตรงจากไคลเอนต์ ถ้าคุณยังจำได้
นั่นหมายความว่า เราจะต้องเพิ่มฟังก์ชัน callback แบบ `deny` ตัวใหม่เข้าไปแทน
~~~js
//...
Posts.deny({
update: function(userId, post, fieldNames, modifier) {
var errors = validatePost(modifier.$set);
return errors.title || errors.url;
}
});
//...
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "3~8" %>
ให้สังเกตุว่าตัวแปร `post` ที่รับเข้ามาคือ ข่าว *เดิม* ซึ่งในกรณีนี้เราต้องการตรวจสอบความถูกต้องของการ *อัพเดท* เราถึงเรียกใช้ `validatePost` กับค่าคุณสมบัติ `$set` ของตัว `modifier` (เหมือนที่ใช้ใน `Posts.update({$set: {title: ..., url: ...}})`)
ที่ใช้แบบนี้ได้ก็เพราะว่า ใน `modifier.$set` ประกอบด้วย`title` และ `url` เหมือนกับที่อ็อบเจกต์ `post` ทั้งตัวมี และยังหมายความได้อีกว่า การอัพเดทแค่ `title` หรือ `url` ตัวใดตัวหนึ่งเพียงตัวเดียว จะไม่สามารถเกิดขึ้นได้แน่นอน ซึ่งในการใช้งานจริงไม่น่ามีปัญหา
คุณอาจสังเกตุเห็นว่า ฟังก์ชัน callback แบบ `deny` นี้เป็นตัวที่สอง เมื่อเราเพิ่มฟังก์ชัน callback แบบ `deny`เข้าไปหลายตัว การทำงานจะถูกยกเลิกเมื่อตัวใดตัวหนึ่งมีค่าเป็น `true` ซึ่งในกรณีนี้หมายความว่า `update` จะเกิดขึ้นได้กับฟิลด์ `title` และ `url` เท่านั้น โดยตัวใดตัวหนึ่งต้องไม่มีค่าว่างด้วย
<%= commit "9-5", "Validate post contents when editing." %>