-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy path07s-latency-compensation.md.erb
184 lines (131 loc) · 16.9 KB
/
07s-latency-compensation.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
---
title: การชดเชยความล่าช้า
slug: latency-compensation
date: 0007/01/02
number: 7.5
sidebar: true
contents: เข้าใจเรื่องการชดเชยความล่าช้า|ทำให้แอพช้าลงเพื่อดูว่าเกิดอะไรขึ้น|เรียนรู้ว่าเมธอดของ Meteor เรียกใช้งานกันเองอย่างไร
paragraphs: 28
---
ในบทที่ผ่านมา เราได้แนะนำแนวคิดใหม่ในโลกของ Meteor นั่นก็คือ **เมธอด**
<%= diagram "latency1", "Without latency compensation", "pull-right" %>
เมธอดของ Meteor คือ วิธีการทำงานของชุดคำสั่งบนเซิร์ฟเวอร์ที่มีแบบแผน ในตัวอย่างของเรานั้น เราใช้เมธอดเพื่อต้องการมั่นใจว่า ข่าวที่ป้อนใหม่จะถูกแท็กด้วยชื่อและ id ของผู้สร้าง รวมทั้งเวลาปัจจุบันของเซิร์ฟเวอร์
อย่างไรก็ตาม ถ้า Meteor เรียกใช้เมธอดด้วยวิธีปกติ เราก็จะพบปัญหาแน่นอน ลองพิจารณาลำดับเหตุการณ์ข่างล่างนี้ดู (เวลาที่สุ่มขึ้นมาใช้เพื่อการอธิบายเท่านั้น)
- *+0ms:* ผู้ใช้คลิ๊กที่ปุ่ม submit และเบราว์เซอร์เรียกใช้เมธอด
- *+200ms:* เซิร์ฟเวอร์ปรับแก้ไขฐานข้อมูล Mongo
- *+500ms:* ไคลเอนต์รับค่าการเปลี่ยนแปลง และอัพเดทหน้าจอตามการเปลี่ยนแปลงนั้น
ถ้า Meteor ทำงานด้วยวิธีแบบนี้ มันก็จะทำให้เกิดความล่าช้าระหว่างการทำงานกับการแสดงผล (ความล่าช้าจะมากหรือน้อย ขึ้นอยู่กับว่า คุณอยู่ใกล้เซิร์ฟเวอร์แค่ไหน) ซึ่งเราปล่อยให้เกิดขึ้นกับเว็บแอพสมัยใหม่ไม่ได้
### การชดเชยความล่าช้า
<%= diagram "latency2", "With latency compensation", "pull-right" %>
เพื่อหลีกเลี่ยงปัญหานี้ Meteor ก็นำเสนอแนวคิดใหม่ที่เรียกว่า **การชดเชยความล่าช้า** เมื่อเราสร้างเมธอด `post` ขึ้นมานั้น เราใส่มันไว้ในไฟล์ที่อยู่ในโฟลเดอร์ `collections/` นั่นก็หมายความว่า มันถูกเรียกใช้ได้จากทั้งเซิร์ฟเวอร์ *และไคลเอนต์* และมันก็รันทั้งสองฝั่งในเวลาเดียวกันด้วย!
เมื่อคุณเรียกใช้เมธอด ไคลเอนต์จะส่งคำสั่งนั้นไปที่เซิร์ฟเวอร์ และในขณะเดียวกันก็จะ *จำลอง* การทำงานของเมธอดกับคอลเลคชั่นที่ไคลเอนต์ไปพร้อมๆกันด้วย ดังนั้นการทำงานของเราก็จะกลายเป็นแบบนี้
- *+0ms:* ผู้ใช้คลิ๊กที่ปุ่ม submit และเบราว์เซอร์เรียกใช้เมธอด
- *+0ms:* ไคลเอนต์จำลองการทำงานของเมธอดกับคอลเลคชันที่ไคลเอนต์ และปรับหน้าจอตามผลการทำงาน
- *+200ms:* เซิร์ฟเวอร์ปรับแก้ไขฐานข้อมูล Mongo
- *+500ms:* ไคลเอนต์รับค่าการเปลี่ยนแปลง แล้วยกเลิกการเปลี่ยนแปลงที่จำลองขึ้น และใช้การเปลี่ยนแปลงจากเซิร์ฟเวอร์แทน (ซึ่งโดยทั่วไปจะเหมือนกัน) จากนั้นอัพเดทหน้าจอตามการเปลี่ยนแปลง
โดยผลที่ได้คือ ผู้ใช้เห็นการเปลี่ยนแปลงทันที จากนั้นเมื่อเซิร์ฟเวอร์ส่งผลลัพธ์กลับมาหลังจากนั้นอีกไม่นาน ซึ่งอาจจะมีหรือไม่มีการเปลี่ยนแปลงจากเอกสารต้นทางที่เซิร์ฟเวอร์ส่งมาให้ก็ได้ สิ่งหนึ่งที่เราได้จากตรงนี้คือ เราควรจะลองจนแน่ใจว่า เราได้ใช้ข้อมูลที่ใกล้เคียงกับเอกสารจริงมากที่สุดเท่าที่จะทำได้
### สังเกตุการชดเชยความล่าช้า
เราก็แค่เปลี่ยนอะไรเล็กน้อยกับเมธอด `post` เพื่อสังเกตุการทำงานนี้ เริ่มจากการใช้ฟังก์ชัน `Meteor._sleepForMs()` เพื่อหน่วงการทำงานของเมธอดประมาณ 5 วินาที (ตรงนี้สำคัญมาก) *บนเซิร์ฟเวอร์*
เราจะใช้ `isServer` เพื่อถาม Meteor ว่าตอนนี้กำลังทำงานอยู่ที่ไคลเอนต์ (ที่เรียกว่า stub) หรือบนเซิร์ฟเวอร์ ซึ่ง [stub](http://docs.meteor.com/#methods_header) ก็คือ เมธอดจำลองการทำงานที่ Meteor รันบนไคลเอนต์ไปพร้อมๆกัน ในขณะที่เมธอด "จริง" จะรันอยู่บนเซิร์ฟเวอร์
จากนั้น เราจะบอก Meteor ว่า ถ้าโค้ดที่กำลังรันนั้นเกิดบนเซิร์ฟเวอร์ ให้หน่วงเวลาไว้ซัก 5 วินาที และเพิ่มคำว่า `(server)` ไปที่ตอนท้ายของชื่อข่าว ถ้าไม่ใช่ให้เพิ่มคำว่า `(client)` เข้าไปแทน
~~~js
Posts = new Mongo.Collection('posts');
Meteor.methods({
postInsert: function(postAttributes) {
check(this.userId, String);
check(postAttributes, {
title: String,
url: String
});
if (Meteor.isServer) {
postAttributes.title += "(server)";
// wait for 5 seconds
Meteor._sleepForMs(5000);
} else {
postAttributes.title += "(client)";
}
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 "collections/posts.js" %>
<%= highlight "11~17" %>
ถ้าเราหยุดไว้แค่นี้ เราก็ยังคงสรุปอะไรไม่ได้มากเท่าไหร่ สิ่งที่เกิดตอนนี้ ดูเหมือนว่าฟอร์มที่ใช้ submit จะหยุดการทำงานไป 5 วินาที ก่อนจะส่งคุณกลับไปที่หน้าแสดงรายการข่าว และดูเหมือนไม่มีอะไรเกิดขึ้นอีก
เพื่อให้เข้าใจว่าทำไม เราจะกลับไปที่โค้ดของตัวจัดการเหตุการณ์ submit
~~~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 alert(error.reason);
// show this result but route anyway
if (result.postExists)
alert('This link has already been posted');
Router.go('postPage', {_id: result._id});
});
}
});
~~~
<%= caption "client/templates/posts/post_submit.js" %>
ที่เราวางคำสั่ง `Router.go() ` ไว้ในฟังก์ชัน callback ก็เพื่อต้องการให้ฟอร์มหยุดรอจนกว่าเมธอดจะทำงานเสร็จ จากนั้นจึงค่อยเปลี่ยนหน้าเว็บให้เรา
ตอนนี้ดูเหมือนว่าเรากำลังมาถูกทางแล้ว คือ เราจะไม่ส่งผู้ใช้ไปที่หน้าต่อไป ก่อนที่จะรู้ว่าข้อมูลที่ป้อนเข้ามาใช้ได้หรือไม่ เพราะถ้าทำอย่างนั้นผู้ใช้อาจสับสนเนื่องจากเราส่งผู้ใช้ไปที่หน้านึง จากนั้นอีกไม่กี่วินาทีต่อมาก็ส่งกลับมาหน้าป้อนข่าวเหมือนเดิม เพื่อให้เค้าแก้ไขข้อมูลอีกครั้ง
แต่สำหรับตัวอย่างที่เรากำลังดูอยู่นี้ เราต้องการเห็นผลลัพธ์ของการทำงานทันที ดังนั้นเราจึงเปลี่ยนเส้นทางไปที่ `postsList` แทน (เราไม่สามารถเปลี่ยนเส้นทางไปที่หน้าข่าวได้ เพราะไม่รู้ค่า `_id` จากข้างนอกเมธอด) โดยเอาคำสั่งเปลี่ยนเส้นทางออกจากฟังก์ชัน callback เมื่อเรียบร้อยก็จะได้แบบนี้
~~~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 alert(error.reason);
// show this result but route anyway
if (result.postExists)
alert('This link has already been posted');
});
Router.go('postsList');
}
});
~~~
<%= caption "client/templates/posts/post_submit.js" %>
<%= highlight "20" %>
<%= scommit "7-5-1", "Demonstrate the order that posts appear using a sleep." %>
ถ้าเราสร้างข่าวใหม่ตอนนี้ เราจะเห็นการชดเชยความล่าช้าเกิดขึ้นชัดเจน เริ่มแรก ชื่อข่าวถูกต่อท้ายด้วย `(client)` (ข่าวแรกในรายการ ลิงก์ไปที่ GitHub)
<%= screenshot "s5-1", "Our post as first stored in the client collection" %>
จากนั้นอีก 5 วินาทีต่อมา มันก็ถูกแทนที่ด้วยเอกสารจริงซึ่งได้จากเซิร์ฟเวอร์
<%= screenshot "s5-2", "Our post once the client receives the update from the server collection" %>
### เมธอดกับคอลเลคชั่นที่ไคลเอนต์
จากที่ผ่านมาคุณอาจคิดว่า เมธอดนั้นค่อนข้างซับซ้อน แต่ความเป็นจริงก็คือ มันทำงานแบบง่ายๆก็ได้ และเราก็เห็นเมธอดแบบง่ายๆกันมาแล้วสามตัวที่เกียวข้องกับการจัดการคอลเลคชั่น ได้แก่ `insert`, `update` และ `remove`
โดยเมื่อคุณสร้างคอลเลคชั่นบนเซิร์ฟเวอร์ชื่อ `'posts'` คุณก็ได้สร้างเมธอดขึ้นมาสามตัวโดยไม่รู้ตัว คือ `posts/insert`, `posts/update` และ `posts/delete` หรืออีกนัยนึงก็คือ เมื่อคุณเรียกใช้ `Posts.insert()` กับคอลเลคชั่นของคุณที่ไคลเอนต์ คุณก็กำลังเรียกใช้เมธอดที่ชดเชยความล่าช้า ซึ่งทำงานสองอย่างนี้
1. ตรวจดูว่า เรามีสิทธิที่จะเปลี่ยนแปลงข้อมูลได้หรือไม่ โดยเรียกใช้ฟังก์ชัน callback ทั้ง `allow` และ `deny` (อย่างไรก็ดี สิ่งนี้ไม่จำเป็นต้องเกิดขึ้นที่ฝั่งไคลเอนต์ ในตอนที่จำลองการทำงาน)
2. ทำให้เกิดการเปลี่ยนแปลงจริงๆกับแหล่งเก็บข้อมูล
### เมธอดเรียกใช้เมธอด
ถ้าคุณตามทัน คุณอาจเพิ่งเข้าใจว่า เมธอด `post` ก็เรียกใช้อีกเมธอด (`post/insert`) เมื่อเราเพิ่มข่าวเข้าไป แต่อาจสงสัยอยู่ว่ามันทำงานได้ยังไง
เมื่อการทำงานแบบจำลองเกิดขึ้น (ที่เมธอดฝั่งไคลเอนต์) เราก็เรียกใช้ `insert` แบบจำลอง (เพิ่มข้อมูลเข้าไปที่คอลเลคชั่นฝั่งไคลเอนต์) แต่เราไม่ได้เรียกใช้ `insert` จริงๆบนฝั่งเซิร์ฟเวอร์ เพราะเรารู้ว่าเมธอด `post` เวอร์ชันบนเซิร์ฟเวอร์จะทำตรงนี้ให้
ผลก็คือ เมื่อเมธอด `post` บนฝั่งเซิร์ฟเวอร์เรียกใช้ `insert` มันก็ไม่ต้องกังวลกับการทำงานแบบจำลองนั้น และทำงานไปแบบที่เคยเป็น
และก็เหมือนกับบทแทรกก่อนๆ คุณต้องไม่ลืมที่จะเปลี่ยนโค้ดกลับมาเหมือนเดิม ก่อนที่จะอ่านบทต่อไป