-
Notifications
You must be signed in to change notification settings - Fork 4
/
04-collections.md.erb
334 lines (215 loc) · 17.4 KB
/
04-collections.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
---
title: Kolekcje
slug: collections
date: 0004/01/01
number: 4
contents: O reaktywnych kolekcjach, fundamentalnej funkcjonalności Meteora.|Zrozumiesz jak działa synchronizacja danych w Meteorze.|Zintegrujesz kolekcje z szablonami.|Jak przekształcić nasz podstawowy prototyp w prawdziwą aplikację czasu rzeczywistego!
paragraphs: 72
---
W pierwszym rozdziale rozmawialiśmy o podstawowej funkcjonalności Meteorą jaką jest synchronizacja danych między klientem i serwerem.
W bieżącym rozdziale przyjrzymy się bliżej tej funkcjonalności i sposobie działania kluczowego elementu technologii, która to umożliwia, kolekcji Meteora.
Budujemy aplikację społeczonościową wyświetlającą wiadomości, zatem pierwszą rzeczą, którą chcemy osiągnąć, jest stworzenie listy listy linków do postów. Nazwiemy każdy z tych linków "postem".
Oczywiście potrzebujemy te posty gdzieś przechowywać. Meteor domyślnie pracuje z bazą danych MongoDB, która jest uruchomiona po stronie serwera i jest twoim stałym magazynem danych.
Z tego względu, mimo to, że przeglądarka może przechowywać pewien stan aplikacji (na przykład bieżącą stronę, lub aktualnie wpisywany komentarz), serwer, a szczególniej Mongo zawiera stały, kanoniczny magazyn danych. Przez *kanoniczny* rozumiemy, że jest taki sam dla wszystkich użytkowników: każdy użytkownik może być na innej stronie, ale główna lista postów jest taka sama dla wszystkich użykowników.
Te dane są przechowane w **Kolekcji** Meteora. Kolekcja jest specjalną strukturą danych, która poprzez publikacje i subskrypcje troszczy się o synchronizację danych w czasie rzeczywistym między każdą podłączoną przeglądarką po stronie klienta i bazą danych Mongo. Przyjrzyjmy się temu bliżej.
Chcemy, aby nasze posty były stałe i aby były współdzielone między wszystkich użytkowników, zatem zaczniemy od utworzenia kolekcji `Posts`, w której będziemy je przechowywali. Jeżeli jeszcze tego nie zrobiłeś, utwórz folder `collections/` w głównym folderze aplikacji i plik `posts.js` w tym folderze. Następnie dodaj:
~~~js
Posts = new Meteor.Collection('posts');
~~~
<%= caption "collections/posts.js" %>
<%= commit "4-1", "Dodano kolekcję posts." %>
Kod, który nie znajduje się ani w folderze `client/` ani w `server/` będzie uruchamiany po *obu* stronach. Zatem kolekcja `Posts` jest dostępna zarówno po stronie klienta i serwera. Jednakże, sposób działania kolekcji po stronach znacznie różni się od siebie.
<% note do %>
### Używać Var czy nie?
W Meteorze słowo kluczowe `var` ogranicza zagres obiektu do bieżącego pliku. Ponieważ chcemy udostępnić kolekcję `Posts` całej aplikacji, *nie* używamy słowa kluczowego `var`.
<% end %>
Po stronie serwera kolekcja kontaktuje się z bazą danych Mongo, czyta i zapisuje dowolne zmiany. W ten sposób może być porównana do standardowej biblioteki bazy danych. Po stroni klienta kolekcja jest *bezpieczną* kopią *pozdbioru* prawdziwej, kanonicznej kolekcji. Kolekcja po stronie klienta jest na bieżąco i (w większości) niewidowicznie uaktualniana w czasie rzeczywistym z oznaczonym podzbiorem danych.
<% note do %>
### Konsola vs Konsola vs Konsola
W niniejzym rozdziale zaczniemy korzystać z **konsoli przeglądarki**, której nie należy mylić z **terminalem** czy **konsolą Mongo**. Poniżej znajdziesz szybkie wprowadzenie do każdej z nich.
#### Terminal
<%= screenshot "terminal", "Terminal" %>
- Wołany na poziomie systemu operacyjnego
- **Serwerowe** funkcje `console.log()` wypisują tutaj informacje.
- Tzw. znak zachęty to: `$`. (Linux, Mac OSX)
- Znany również jako: Shell, Bash
#### Konsola przeglądarki
<%= screenshot "browser-console", "The Browser Console" %>
- Wołana z poziomu przeglądarki, uruchamia kod JavaScript
- **Klienckie** funkcje `console.log()` wypisują tutaj informacje.
- Znak zachęty to: `❯`.
- Znana również jako: Konsola JavaScript, Konsola DevTools
#### Konsola Mongo
<%= screenshot "mongo-shell", "Konsola Mongo" %>
- Uruchamiana z terminala za pomocą `meteor mongo` lub `mrt mongo`.
- Daje bezpośredni dostęp do bazy danych aplikacji.
- Znak zachęty: `>`.
- Również znana jako: Mongo Shell
Zauważ, że w każdym przypadku, nie jest wymagane wpisywanie znaku zachęty (`$`, `❯`, czy `>`), jako części komendy. Możesz także przyjąć, że jakakolwiek linia *nie* zaczynająca się od znaku zachęty jest wynikiem wywołania poprzedniej komendy.
<% end %>
### Kolekcje po stronie serwera
Po stronie serwera kolekcja działa jako API dla bazy danych Mongo. Pozwala to na pisanie w Twoim kodzie wykonywanym na serwerze poleceń takich jak `Posts.insert()` czy `Posts.update()`, które zmienią dane w kolekcji `posts` zapisanej w bazie danych Mongo.
Aby zajrzeć bezpośrednio do bazy danych Mongo, otwórz drugie okno terminala (podczas gdy `meteor` jest uruchomiony w pierwszym oknie) i przejdź do głównego folderu aplikacji. Następnie wykonaj polecenie `meteor mongo` aby uruchomić konsolę Mongo, w której możesz wpisywać standardowe polecenia Mongo (jak zwykle możesz opuścić terminal korzystając z kombinacji klawiszy `ctrl+c`. Przykładowo wstawmy nowy post do bazy danych:
~~~bash
> db.posts.insert({title: "A new post"});
> db.posts.find();
{ "_id": ObjectId(".."), "title" : "A new post"};
~~~
<%= caption "Konsola Mongo" %>
<% note do %>
### Mongo na Meteor.com
Zauważ, że gdy umieszczasz swoją aplikację na *.meteor.com, możesz mieć również dostęp do produkcyjnej bazy danych za pomocą polecenia `meteor mongo mojaAplikacja`.
Możesz również przeczytać logi aplikacji przez wpisanie polecenia `meteor logs mojaAplikacja`.
<% end %>
Składnia Mongo wydaje się być znajoma, pownieważ używa interfejsu Javascript. Nie będziemy przeprowadzali kolejnych zmian w konsoli Mongo, ale od czasu do czasu sprawdzimy czy oczekiwane dane faktycznie znajdują się w bazie.
### Kolekcje po stronie klienta
Kolekcje stają się bardziej interesujące po stronie klienta. Gdy deklarujesz `Posts = new Meteor.Collection('posts');` tworzysz po stronie klienta _lokalny, dostępny z poziomu przeglądarki, cache_ prawdziwej kolekcji znajdującej się w bazie danych Mongo. Gdy wspominamy, że kolekcje po stronie klienta działają jako cache, mamy na myśli, że zawierają *podzbiór* danych oraz *bardzo* szybki dostęp do tych danych.
Ważne jest, aby zrozumieć to w tym miejscu, ponieważ jest to fundamentalna zasada działania Meteora. Ogólnie rzecz biorąc, kolekcje po stronie klienta składają się z podzbioru wszystkich dokumentów zapisanych w kolekcji Mongo (nie chcemy przecież wysyłać całej bazy danych klientowi).
Po drugie, te dokumenty są przechowywane *w pamięci przeglądarki*, co oznacza że dostęp do nich jest praktycznie natychmiastowy. Nie ma zatem powolnego oczekiwania na odpowiedź serwera lub bazy danych na pobranie danych po zawołaniu `Posts.find()` po stronie klienta, ponieważ dane są już załadowane i dostępne.
<% note do %>
### Wprowadzenie do MiniMongo
Implementacja bazy danych Mongo po stronie klienta ma nazwę MiniMongo. Nie jest to jeszcze doskonała implementacja i można napotkać pewne ograniczenia, które nie działają jeszcze w Minimongo. Mimo wszystko, wszystkie pojęcia opisane w tej książce działają podobnie w Mongo i MiniMongo.
<% end %>
### Komunikacja między Klientem i Serwerem
Kluczowym zagadnieniem jest sposób, w jaki kolekcja po stronie klienta synchronizuje dane z kolekcją serwerową o tej samej nazwie (w naszym przypadku `'posts'`).
Zamiast wyjaśniać to szczegółowo zobaczmy co dzieje się podczas synchronizacji.
Otwórz dwa okna przeglądarki i uruchom konsolę przeglądarki w każdej z nich. Następnie uruchom konsolę Mongo z linii komend. W tym momencie, powinieneś zobaczyć w każdym z trzech miejsc pojedyńczy dokument, który utworzyliśmy wcześniej.
~~~bash
> db.posts.find();
{title: "A new post", _id: ObjectId("..")};
~~~
<%= caption "Konsola Mongo" %>
~~~js
❯ Posts.findOne();
{title: "A new post", _id: LocalCollection._ObjectID};
~~~
<%= caption "Konsola pierwszej przeglądarki" %>
Utwórzmy nowy post. W jednym z okien przeglądarki wykonaj komendę insert:
~~~js
❯ Posts.find().count();
1
❯ Posts.insert({title: "A second post"});
'xxx'
❯ Posts.find().count();
2
~~~
<%= caption "Konsola pierwszej przeglądarki" %>
Bez większych niespodzianek post został dodany do lokalnej kolekcji. Sprawdźmy teraz bazę Mongo:
~~~bash
❯ db.posts.find();
{title: "A new post", _id: ObjectId("..")};
{title: "A second post", _id: 'yyy'};
~~~
<%= caption "Konsola Mongo" %>
Jak widać, post został przeniesiony z powrotem do bazy danych Mongo po stronie serwera bez potrzeby pisania pojedyńczej linii kodu (tak naprawdę to napisaliśmy _jedną_ linię kodu: `new Meteor.Collection('posts')`). Ale to nie wszystko!
Przejdź do drugiej instancji przeglądarki i wpisz w konsoli:
~~~js
❯ Posts.find().count();
2
~~~
<%= caption "Druga konsola przeglądarki" %>
Post również jest tutaj dostępny! Pomimo tego, że nie odświeżyliśmy strony czy w jakimkolwiek stopniu działaliśmy z drugą przeglądarką, jak również nie napisaliśmy nowego kodu, który miałby przesłać uaktualnione dane. Wszystko to stało się automagicznie -- oraz natychmiastowo, stanie się to oczywiste później.
Co dokładnie się stało - nasza kolekcja po stronie serwera została poinformowana przez klienta o nowym poście i przejęła zadanie dystrybucji powyższego posta do bazy danych Mongo i do wszystkich podłączonych kolekcji `post`.
Czytanie postów z poziomu konsoli przeglądarki nie jest za bardzo użyteczne. Nauczymy się, jak przekazać dane szablonom i konsekwentnie przekształcić nasz pierwszy prototyp HTML w aplikację webową działającą w czasie rzeczywistym.
### Działanie w czasie rzeczywistym
Obserwacja zawartości kolekcji w konsoli przeglądarki to jedno, a wyświetlanie i zmienianie danych na ekranie, to całkiem co innego. Aby to osiągnąć przekształcimy naszą aplikację z prostej *strony* wyświetlającej statyczne dane w *aplikację* webową czasu rzeczywistego z dynamicznymi, zmieniającymi się danymi.
Spójrzmy poniżej, jak to osiągnąć.
### Zapełnienie bazy danych
Pierwszą rzeczą, którą zrobimy, będzie wstawienie danych do bazy danych. Osiągniemy to za pomocą pliku zawierającego dane początkowe (ang. fixture). Plik zawiera uporządkowany zbiór danych, który zostanie wstawiony do bazy podczas pierwszego uruchomienia serwera.
Upewnijmy się najpierw, że baza danych jest pusta. Użyjemy polecenia `meteor reset`, które opróżnia bazę danych i resetuje cały projekt. Oczywiście musisz być bardzo ostrożny z wykonywaniem tej komendy gdy zaczniesz pracę z prawdziwymi projektami.
Zatrzymaj serwer Meteora (za pomocą `ctrl-c`), a następnie wykonaj z linii komend polecenie:
~~~bash
$ meteor reset
~~~
Polecenie reset całkowicie usuwa bazę danych Mongo. Jest to użyteczne polecenie podczas implementacji aplikacji, gdzie istnieje duże prawdopodobieństwo wprowadzenia bazy danych w niespójny stan.
Teraz, skoro baza danych jest pusta, możemy dodać poniższy kod, który wczyta trzy posty po każdym starcie serwerea gdy kolekcja `Posts` jest pusta:
~~~js
if (Posts.find().count() === 0) {
Posts.insert({
title: 'Introducing Telescope',
author: 'Sacha Greif',
url: 'http://sachagreif.com/introducing-telescope/'
});
Posts.insert({
title: 'Meteor',
author: 'Tom Coleman',
url: 'http://meteor.com'
});
Posts.insert({
title: 'The Meteor Book',
author: 'Tom Coleman',
url: 'http://themeteorbook.com'
});
}
~~~
<%= caption "server/fixtures.js" %>
<%= commit "4-2", "Dodane dane do kloekcji posts." %>
Umieściliśmy powyższy plik w folderze `server/`, zatem nie zostanie on nigdy załadowany do przeglądarki użytkownika. Kod zostanie natychmiastowo uruchomiony po starcie serwera i wykona funkcje `insert`, które dodadzą trzy proste posty do kolekcji `Posts`. Ponieważ nie zatroszczyliśmy się jeszcze o bezpieczeństwo danych, nie ma żadnej różnicy między uruchomieniem tych poleceń po stronie serwera i klienta.
Teraz uruchom ponownie serwer za pomocą polecenia `meteor` i powyższe trzy posty zostaną wprowadzone do bazy danych.
### Przekazanie danych z bazy do HTML za pomocą helperów szablonu
Teraz, gdy otworzymy konsolę przeglądarki, zobaczymy wszystkie trzy posty wczytane do MiniMongo:
~~~js
❯ Posts.find().fetch();
~~~
<%= caption "Konsola przeglądarki" %>
Aby przekształcić te dane w renderowany HTML, możemy użyć funkcji pomocniczych szablonu (helper). W rozdziale 3 zobaczyliśmy jak Meteor pozwala na połączenie *kontekstu danych* z szablonami Handlebards aby budować strony zawierające proste struktury danych. W ten sam sposób możemy je połączyć z danymi z kolekcji. Po prostu zamienimy statyczny obiekt `postsData` przez dynamiczną kolekcję.
Skoro o tym mowa, możesz usunąć kod `postsData`. Oto jak `posts_list.js` powinien teraz wyglądać:
~~~js
Template.postsList.helpers({
posts: function() {
return Posts.find();
}
});
~~~
<%= caption "client/views/posts/posts_list.js" %>
<%= highlight "2~4" %>
<%= commit "4-3", "Połączenie kolekcji z szablonem `postsList`." %>
<% note do %>
### Znajdowanie i pobieranie danych z bazy
Funkcja `find()` w Meteorze zwraca *kursor*, który jest [reaktywnym źródłem danych](http://docs.meteor.com/#find). Jeżeli chemy pobrać jego zawartość, możemy użyć funkcję `fetch()` na tym kursorze, która przekształci go w tablicę z danymi.
W obrębie aplikacji Meteor jest sprytny na tyle, aby iterować po kursorze bez potrzeby jawnego przekształcania go w tablicę. Z tego powodu nie zobaczysz pojawiającej się często w kodzie Meteora funkcji `fetch()` (i jest to powód, dla którego nie użyliśmy jej w powyższym przykładzie).
<% end %>
Teraz, zamiast wczytywać statyczną tablicę z listą postów za pomocą zmiennej, zwracamy kursor do helpera `posts`. Ale co tym sposobem osiągneliśmy? Jeżeli wrócimy do przeglądarki, zobaczymy:
<%= screenshot "4-3", "Użycie zmiennych danych" %>
Widzimy wyraźnie, że helper `{{#each}}` przeiterował wszystkie dostępne dane z kolekcji `Posts` i wyświetlił je na ekranie. Kolekcja po stronie serwera wczytała posty z bazy danych Mongo, przesłała je do kolekcji klienta, a helper Handlebars przekazał je do szablonu.
Pójdziemy jeden krok naprzód; dodajmy kolejny post w konsoli:
~~~js
❯ Posts.insert({
title: 'Meteor Docs',
author: 'Tom Coleman',
url: 'http://docs.meteor.com'
});
~~~
<%= caption "Konsola przeglądarki" %>
Spójrz na przeglądarkę -- powinieneś zobaczyć:
<%= screenshot "4-4", "Dodawanie postów przez konsolę" %>
Właśnie pierwszy raz byłeś naocznym świadkiem działania reaktywności. Gdy nakazaliśmy Handlebars iterować po kursorze `Posts.find()`, framework wiedział jak obserwować kursor na zachodzące zmiany i patchować wynikowy HTML w najprostszy sposób, aby wyświetlać prawidłowe dane na ekranie.
<% note do %>
### Inspekcja zmian w DOM
W tym przypadku, najprostszą zmianą było dodanie kolejnego `<div class="post">...</div>`. Jeżeli chcesz się upewnić, że to się na pewno wydarzyło, otwórz DOM inspector i zaznacz element `<div>` odpowiadający jednemu z istniejących postów.
Teraz, w konsoli JavaScript, wstaw kolejny post. Jeżeli przejdziesz z powrotem do inspectora DOM, zobaczysz dodatkowy element `<div>` odpowiadający nowemu postowi, ale nadal będzie zaznaczony *ten sam* element `<div>` co poprzednio. Jest to użyteczny sposób na sprawdzenie, kiedy elementy zostały przerenderowane i kiedy nie ulegają zmianie.
<% end %>
### Łączenie kolekcji: Publikacje i Subsrkrypcje
Do tej pory używaliśmy pakietu `autopublish`, który nie jest zamierzony do użycia w środowisku produkcyjnym. Jak nazwa wskazuje, pakiet ten automatycznie publikuje w całości wszystkie kolekcje każdemu podłączonemu klientowi. Nie jest to, co chcemy osiągnąć, więc wyłączmy to.
Otwórz nowe okno terminala i wpisz:
~~~bash
$ meteor remove autopublish
~~~
Ma to efekt natychmiastowy. Jeżeli będziesz obserwował przeglądarkę, zobaczysz, że wszystkie posty zniknęły! Dzieje się tak, poniewaź polegaliśmy na pakiecie `autopublish`, który tworzył lustrzane odbicie wszystkich postów z bazy danych w kolekcji po stronie klienta.
W końcu będzieli musieli się upewnić, że przesyłamy wyłącznie te posty, które użytkownik będzie miał zobaczyć (biorąc również po uwagę np. dzielenie dokumentu na strony). Na teraz, ustawimy publikację kolekcji `Posts` w całości.
Aby to osiągnąć, utworzymy prostą funkcję `publish()`, która zwraca kursor posiadający referencję do wszystkich postów:
~~~js
Meteor.publish('posts', function() {
return Posts.find();
});
~~~
<%= caption "server/publications.js" %>
Po stronie klienta musimy *zasubskrybować* publikację. Dodamy poniższą linię do `main.js`:
~~~js
Meteor.subscribe('posts');
~~~
<%= caption "client/main.js" %>
<%= commit "4-4", "Usunięto `autopublish` i ustawiono podstawową publikację." %>
Jeżeli ponownie sprawdzimy przeglądarkę, zobaczymy ponownie nasze posty. Świetnie!
### Konluzja
Co zatem osiągnęliśmy? Pomimo tego, że jeszcze nie posiadamy interfejsu użytkownika, mamy funkcjonalną aplikację webową. Moglibyśmy wystawić aplikację na zewnętrznym internetowym serwerze i (używając konsolę przeglądarki) zacząć dodawać posty, które byłyby widziane w każdej podłączonej do strony przeglądarki na całym świecie.