-
Notifications
You must be signed in to change notification settings - Fork 12
/
04-collections.md.erb
359 lines (227 loc) · 18.1 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
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
---
title: Collections
slug: collections
date: 0004/01/01
number: 4
contents: Meteor's Kernfeature kennenlernen, Realtime Collections.|Lernen, wie Meteors Datensynchronisation funktioniert.|Lernen, wie du Collections mit deinen Templates integrierst.|Unseren einfachen Prototypen in eine funktionierende Realtime Application verwandeln!
paragraphs: 72
---
Im ersten Kapitel haben wir über Meteor's Kernfeature gesprochen, die automatische Synchronisierung von Daten zwischen Client und Server.
In diesem Kapitel werfen wir einen genaueren Blick darauf, wie das funktioniert und betrachten die Arbeitsweise der wichtigsten Technologie, die dies ermöglicht, der Meteor **Collection**.
Eine Collection ist eine spezielle Datenstruktur, die sich um das Speichern von Daten in der permanenten, serverseitigen MongoDB Datenbank, sowie deren Synchronisation mit dem Browser jedes verbundenen Users in Echtzeit kümmert.
Wir wollen, dass unsere Posts dauerhaft gepsichert sind und zwischen Usern geteilt werden, also beginnen wir damit eine Collection namens `Posts` zu erstellen um sie dort zu speichern.
Collections sind ein zentraler Bestandteil jeder App, um also sicherzustellen, dass sie immer zu Beginn definiert werden, erstellen wir sie in der lib directory. Sofern noch nicht geschehen, erstelle im Ordner `lib` einen Unterordner `collections/` und darin eine Datei posts.js. Dann füge dieser Datei folgende Zeile hinzu:
~~~js
Posts = new Meteor.Collection('posts');
~~~
<%= caption "lib/collections/posts.js" %>
<%= commit "4-1", "Added a posts collection" %>
////
<% note do %>
### To Var Or Not To Var?
In Meteor wird das Schlüsselwort `var` benutzt um den Geltungsbereich eines Objekts auf die aktuelle Datei zu beschränken. In unserem Beispiel, wollen wir die Collection `Posts` aber in unserer ganzen App verfügbar machen, weshalb wir `var` *nicht* benutzen.
<% end %>
### Daten speichern
Webanwendungen verfügen über drei grundsätzliche Möglichkeiten ihre Daten zu speichern, jede hat dabei eine andere Aufgabe:
- **Browser (Memory):** Dinge wie JavaScript Variablen werden im temporären Speicher (Memory) des Browsers gespeichert, diese Daten sind *nicht* permanent: sie sind lokal im aktuellen Browser-Tab und gehen verloren, sobald man es schließt
- **Browser (Storage):** Browser können Daten auch dauerhaft speichern indem sie z.B.Cookies oder den permanenten Speicher ([Local Storage](http://diveintohtml5.info/storage.html)) benutzen. Auch wenn diese Daten von Session zu Session bestehen bleiben, sind sie *lokal* beim jeweiligen Benutzer (aber über alle Tabs verfügbar) und können deshalb nur schwer mit anderen Nutzern geteilt werden.
- **Serverseitige Datenbank:** der beste Ort um Daten dauerhaft zu speichern, die man mehr als einem Benutzer zugänglich machen möchte, ist eine gute alte Datenbank. (MongoDB ist die Standardlösung bei Meteor)
Meteor benutzt alle drei Varianten und synchronisiert zeitweise von einem zum anderen Speicherort (wie wir bald sehen werden). Davon abgesehen, bleibt die Datenbank die "kanonische" Quelle, die den Stamm der Daten enthält.
### Client & Server
Code, der sich in anderen Ordnern als `clients/` oder `server/` befindet, wird in beiden Kontexten ausgeführt. Damit ist beispielsweise die Collection `Posts` für Client und Server verfügbar. Nichtsdestotrotz kann das was die Collection tut, in beiden Umgebungen sehr unterschiedlich sein.
Auf dem Server ist es die Aufgabe der Collection mit der MongoDB Datenbank zu kommunizieren und Änderungen zu lesen oder zu schreiben. In dieser Hinsicht kann sie mit einer Standard Datenbank-Bibliothek verglichen werden.
Beim Client hingegen ist die Collection eine Kopie einer Teilmenge der realen originalen Stammdaten. Die clientseitige Collection wird permanent und (meist) transparent in echtzeit mit der Teilmenge synchronisiert.
<% note do %>
### Console vs Console vs Console
In diesem Kapitel beginnen wir die **Browser Konsole** zu benutzen, die man nicht mit dem **terminal**, der **Meteor Shell** oder der **Mongo Shell** verwechseln darf. Hier ist eine kurze Einführung zu jedem davon:
#### Terminal
<%= screenshot "terminal", "Das Terminal" %>
- wird im Betriebssystem aufgerufen
- **serverseitiges** `console.log()` wird hier ausgegeben
- Prompt: `$`
- auch bekannt als: Shell, Bash
#### Browser Console
<%= screenshot "browser-console", "Die Browser Konsole" %>
- wird innerhalb des Browsers aufgerufen, führt JavaScript Code aus
- **clientseitiges** console.log() wird hier ausgegeben
- Prompt: `❯`
- auch bekannt als: JavaScript Konsole, Dev-Tools Konsole
#### Meteor Shell
<%= screenshot "meteor-shell", "Die Meteor Shell" %>
- wird im Terminal mit `meteor shell` aufgerufen
- gewährt direkten Zugang zum serverseitigen Code der Anwendung
- Prompt: `>`
#### Mongo Shell
<%= screenshot "mongo-shell", "Die Mongo Shell" %>
- wird im Terminal mit `meteor mongo` aufgerufen
- gewährt direkten Zugang zur Datenbank der Anwendung
- Prompt: `>`
- auch bekannt als: Mongo Konsole
Beachte, dass man in keinem Fall als Teil eines Befehls das Prompt Zeichen (`$`, `❯`, oder `>`) eingibt. Auch kann man davon ausgehen, dass jede Zeile, die nicht mit dem Prompt beginnt, eine Ausgabe des vorangegangen Befehls ist.
<% end %>
### Server-Side Collections
Kommen wir nun zurück zum Server, wo sich die Collection wie eine API in die Mongo Datenbank verhält. Im serverseitigen code, ermöglicht uns das, Mongo Befehle wie `Posts.insert()` oder `Posts.update()` zu schreiben, die dann Änderungen an der `posts` Collection in Mongo bewirken.
Um einen Blick in die Mongo Datenbank zu werfen, öffne ein zweites Terminal Fenster (während `meteor` noch im ersten Fenster läuft) und begib dich in das Verzeichnis deiner App. Führe dann den Befehl `meteor mongo` aus um eine Mongo shell zu starten in der man standard Mongo Befehle ausführen (und wie gewohnt mit `strg+c` beenden) kann. Lass uns z.B. einen neuen Post einfügen:
~~~bash
> db.posts.insert({title: "A new post"});
> db.posts.find();
{ "_id": ObjectId(".."), "title" : "A new post"};
~~~
<%= caption "Die Mongo Shell" %>
<% note do %>
### Mongo auf Meteor.com
Beachte, dass bei Apps, die auf *.meteor.com gehostet werden, ein Zugriff auf die Mongo shell der installierten Anwendung mit dem Befehl `meteor mongo myApp` möglich ist.
Wo wir schon dabei sind, mit folgendem Befehl kannst du die Logs deiner Anwendung einsehen: `meteor logs myApp`.
<% end %>
Mongos Syntax wirkt vertraut, da er eine JavaScript Schnittstelle nutzt. Wir werden zwar keine weiteren Daten über die Mongo Shell verändern, aber wir werden ab und zu mal einen Blick hinein werfen, um zu sehen was sich darin befindet.
### Client-Side Collections
Clientseitig werden die Collections interessanter. Deklariert man `Posts = new Mongo.Collection('posts')`; auf der Clientseite, erzeugt man eine *lokale*, *im Browsercache* liegende, Kopie der realen Mongo Collection. Wenn wir von einer clientseitigen Collection als "cache" sprechen, meinen wir damit, dass sie eine *Teilmenge* der Daten enhält und einen sehr *schnellen* Zugriff bietet.
Es ist wichtig diese Punkte zu verstehen, da sie die Grundlage dafür bilden, wie Meteor funktioniert. Im Allgemeinen besteht eine clientseitige Collection aus einer Teilmenge aller Dokumente aus einer mongo Collection (schließlich wollen wir normalerweise nicht unsere *gesamte* Datenbank an den Client schicken.).
Zum Zweiten befinden sich diese Dokumente dann im *Browser Speicher* und es kann praktisch ohne Verzögerung darauf zugegriffen werden. Es gibt also keine langsames hin- und her Senden mit dem Server oder der Datenbank um Daten zu erhalten, wenn man `Posts.find()` auf der Clientseite aufruft, da die Daten bereits vorher geladen wurden.
<% note do %>
### Introducing MiniMongo
Meteors clientseitige Mongo Implementierung heißt MiniMongo. Es ist noch keine perfekte Implementierung und es kann vorkommen, dass bestimmte Mongo Features nicht in MiniMongo funktionieren. Nichtsdestotrotz funktionieren alle Funktionen, die in diesem Buch erklärt werden sowohl in Mongo als auch in MiniMongo
<% end %>
### Client-Server Communication
Der wichtigste Teil von alledem ist wie die clientseitige Collection ihre Daten mit der serverseitigen Collection gleichen Namens (`posts` in unserem Fall) synchronisiert. Anstatt das im Detail zu erklären, schauen wir uns einfach an was passiert.
Öffne dazu zwei Browser Fenster und jeweils die Browser Konsole. Öffne die Mongo shell in der Komandozeile.
Jetzt sollten wir in der Lage sein, das einzelne Dokument zu finden, das wir vorhin in allen drei Kontexten erstellt haben (beachte, dass das User Interface unserer App immernoch die drei statischen Datensätze anzeigt. Wir ignorieren das erst einmal).
~~~bash
> db.posts.find();
{title: "A new post", _id: ObjectId("..")};
~~~
<%= caption "Die Mongo Shell" %>
~~~js
❯ Posts.findOne();
{title: "A new post", _id: LocalCollection._ObjectID};
~~~
<%= caption "Erste Browser Konsole" %>
Erstellen wir nun einen neuen Post, indem wir in einem der Browserfenster den Insert Befehl ausführen:
~~~js
❯ Posts.find().count();
1
❯ Posts.insert({title: "A second post"});
'xxx'
❯ Posts.find().count();
2
~~~
<%= caption "Erste Browser Konsole" %>
Wie nicht anders zu erwarten, ist der Post in der lokalen Collection gelandet. Nun sehen wir uns die Mongo Datenbank an.
~~~bash
❯ db.posts.find();
{title: "A new post", _id: ObjectId("..")};
{title: "A second post", _id: 'yyy'};
~~~
<%= caption "Die Mongo Shell" %>
Wie man sehen kann, ist der Post bis in die Mongo Datenbank gelangt, ohne dass wir auch nur eine Zeile Code geschrieben haben um unseren Clienten mit dem Server zu verbinden. (streng genommen, haben wir *eine* Zeile Code geschrieben: `new Mongo.Collection('posts')`). Doch das ist noch nicht alles!
Gib nun im zweiten Browserfenster folgendes in die Konsole ein:
~~~js
❯ Posts.find().count();
2
~~~
<%= caption "Zweite Browser Konsole" %>
Der Post ist auch hier! Obwohl wir das zweite Fenster nie neu geladen oder überhaupt irgendwie damit in Berührung gekommen sind und neuen Code, der Updates betrifft haben wir auch nicht geschrieben. Es ist alles magisch passiert - und unverzüglich, aber das wird später noch klarer werden.
Was passiert ist, ist, dass unsere serverseitige Collection von einer clientseitigen Collection über einen neuen Post informiert wurde, diesen in die Mongo Datenbank geschrieben und an alle anderen verbundenen `posts` Collections geschickt hat.
Doch Posts in der Browser Konsole abzufragen ist nicht wirklich hilfreich. Bald schon werden wir lernen, wie man diese Daten mit unseren Templates verbindet und dabei unseren einfachen HTML Prototypen in eine funktionierende echtzeit Webanwendung verwandeln.
### Die Datenbank befüllen
Sich den Inhalt unserer Collections in der Browser Konsole anzusehen ist die eine Sache, aber was wir wirklich machen wollen ist, die Daten und die Änderungen daran auf dem Bildschirm anzuzeigen. Auf diese Weise werden wir unsere App von einer einfachen Web-*Seite*, die statische Daten anzeigt, zu einer echtzeit Web-*Anwendung* mit dynamischen, wechselnden Daten weiterentwickeln.
Zunächst einmal befüllen wir die Datenbank mit einigen Daten. Wir machen das über eine fixture Datei, die eine Sammlung von strukturierten Daten in die `Posts` Collection läd, wenn der Server das erste Mal startet.
Erst einmal sollte sichergestellt sein, dass die Datenbank leer ist. Wir benutzen `meteor reset` um die Datenbank zu löschen und das Projekt zurückzusetzen. Natürlich sollte man mit diesem Befehl sehr vorsichtig sein, wenn man beginnt an realen Projekten zu arbeiten.
Stoppe jetzt den Meteor Server mit `strg+c` und führe in der Komandozeile folgenden Befehl aus:
~~~bash
$ meteor reset
~~~
Der Reset-Befehl leert die Mongo Datenbank komplett. Er stellt einen nützlichen Befehl bei der Entwicklung dar, denn das Risiko ist groß, dass die Datenbank sonst inkonsistent wird.
Starten wir unsere Meteor App nun wieder:
~~~bash
$ meteor
~~~
Jetzt wo die Datenbank leer ist, können wird folgenden Code hinzufügen, der drei Posts einfügt, sobald der Server gestartet wird und die `Posts` Collection leer ist:
~~~js
if (Posts.find().count() === 0) {
Posts.insert({
title: 'Introducing Telescope',
url: 'http://sachagreif.com/introducing-telescope/'
});
Posts.insert({
title: 'Meteor',
url: 'http://meteor.com'
});
Posts.insert({
title: 'The Meteor Book',
url: 'http://themeteorbook.com'
});
}
~~~
<%= caption "server/fixtures.js" %>
<%= commit "4-2", "Added data to the posts collection." %>
Wir haben diese Datei im Server Verzeichnis abgelegt, damit sie niemals in einem Client-Browser geladen wird. Der Code wird sofort ausgeführt, wenn der Server startet und ruft `insert` auf um die drei einfachen Datensätze in unsere `posts` Collection zu speichern.
Starte jetzt den Server wieder mit dem Befehl `meteor` und die drei Posts werden in die Datenbank geladen.
### Dynamische Daten
Wenn wir eine Browser Konsole öffnen, sehen wir wie alle drei Posts in MiniMongo geladen werden:
~~~js
❯ Posts.find().fetch();
~~~
<%= caption "Browser Konsole" %>
Um diese drei Posts in HTML gerendert zu bekommen, benutzen wir unseren Freund den Template Helper.
In Kapitel 3 haben wir bereits gesehen wie Meteor uns ermöglicht Daten mit unseren Spacebars Templates zu verknüpfen um HTML Ansichten einfacher Datenstrukturen zu erzeugen. Wir können nun Daten aus unserer Collection analog dazu einbinden. Dazu ersetzen wir einfach unser statisches JavaScript Objekt `postsData`durch eine dynamische Collection.
Wo wir gerade davon sprechen, du kannst nun den Code von `postsData`löschen. So sollte die `posts_list.js`jetzt aussehen:
~~~js
Template.postsList.helpers({
posts: function() {
return Posts.find();
}
});
~~~
<%= caption "client/templates/posts/posts_list.js" %>
<%= commit "4-3", "Wired collection into `postsList` template." %>
<% note do %>
### Find & Fetch
In Meteor gibt `find()`einen Cursor zurück, was eine [reaktive Datenquelle](http://docs.meteor.com/#/full/find) darstellt. Wenn wir dessen Inhalte darstellen wollen, können wir `fetch` auf den Cursor anwenden um ihn in ein Array umzuwandeln.
Innerhalb einer App ist Meteor intelligent genug zu wissen wie man über einen Cursor iteriert ohne dass man jedes mal explizit eine Umwandlung in ein Array vornehmen muss. Das ist der Grund warum man `fetch` nur selten im Meteor Code findet (und warum wir es im Beispiel oben nicht benutzt haben).
<% end %>
Anstatt eine Liste von Posts als ein statisches Array aus einer variable zu laden, geben wir nun einen Cursor an unseren Helper `posts` zurück (obwohl alles sehr ähnlich aussehen wird, da wir immernoch die gleichen Daten nutzen):
////
<%= screenshot "4-3", "Mit 'live' Daten" %>
Unser Helper `{{#each}}` hat über all unsere `Posts` iteriert und zeigt sie auf dem Bildschirm an. Die serverseitige Collection hat die Posts von Mongo bezogen, an unsere clientseitige Collection übergeben und unsere Spacebars Helper haben sie ins Template übermittelt.
Jetzt gehen wir noch einen Schritt weiter; wir fügen einen weiteren Post über die Konsole hinzu:
~~~js
❯ Posts.insert({
title: 'Meteor Docs',
author: 'Tom Coleman',
url: 'http://docs.meteor.com'
});
~~~
<%= caption "Browser Konsole" %>
Zurück im Browser sollte das nun etwa so aussehen:
<%= screenshot "4-4", "Posts über die Konsole hinzufügen" %>
Du hast gerade zum ersten mal die Reaktivität in Aktion gesehen. Da wir Spacebars befohlen haben über den Cursor `Posts.find()` zu iterieren, hat es gewusst wie der Cursor auf Veränderungen überwacht werden muss und wie das HTML möglichst einfach anzupassen ist um die korrekten Daten darzustellen.
<% note do %>
### DOM Veränderungen untersuchen
In diesem Fall war die einfachste mögliche Veränderung ein weiteres `<div class="post">...</div>` hinzuzufügen. Wenn du sichergehen möchtest, dass dies wirklich die einzige Veränderung ist, öffne den DOM Inspector und wähle ein `<div>` eines bestehenden Posts.
In der JavaScript Konsole fügst du nun einen weiteren Post hinzu. Wenn du zurück zum Inspector wechselst, siehst du ein weiteres `<div>` für den neuen Post, doch das ausgewählte ´<div>` ist unverändert. Dies ist eine gute Methode um festzustellen, ob Elemente neu gerendert wurden oder unverändert geblieben sind.
<% end %>
### Collections verbinden: Publications und Subscriptions
Bis jetzt hatten wir das `autopublish` Paket aktiviert, das nicht für den Einsatz bei Produktiv-Apps gedacht ist. Wie der Name schon sagt, bewirkt dieses Paket, dass jede Collection komplett mit jedem Client verbunden wird. Da dies nicht wirklich das ist, was wir wollen, entfernen wir es.
Öffne ein neues Terminal Fenster und führe folgenden Befehl aus:
~~~bash
$ meteor remove autopublish
~~~
Dies hat eine sofortige Wirkung auf unser Projekt. Wenn du nun in den Browser schaust, wirst du sehen, dass alle Posts verschwunden sind! Das liegt daran, dass wir uns auf `autopublish` verlassen haben, das sichergestellt hat, dass unsere Client Collection der Posts eine 1:1 Kopie aller Posts in der Datenbank war. Schlussendlich müssen wir sicherstellen, dass wir nur die Posts übertragen, die der Benutzer auch wirklich sehen muss (Dinge wie Pagination berücksichtigt). Doch für den Moment richten wir `Posts` so ein, dass es komplett übergeben wird.
Um dies zu erreichen, erstellen wir eine Funktion `publish()` die einen Cursor zurückgibt, der alle Posts referenziert:
~~~js
Meteor.publish('posts', function() {
return Posts.find();
});
~~~
<%= caption "server/publications.js" %>
Beim Client müssen wir ein *subscribe* für die Publication einrichten. Dazu fügen wir einfach folgende Zeile in die `main.js` Datei ein:
~~~js
Meteor.subscribe('posts');
~~~
<%= caption "client/main.js" %>
<%= commit "4-4", "Removed `autopublish` and set up a basic publication." %>
Wenn wir nun in den Browser zurückkehren, sind unsere Posts wieder da.
Puh!
### Zusammenfassung
Was haben wir also erreicht? Obwohl wir noch keine Benutzeroberfläche haben, verfügen wir über eine funktionierende Web-Anwendung. Wir könnten die App im Internet veröffentlichen und (über die Browser Konsole) anfangen Geschichten zu posten, welche dann in den Browsern der Benutzer in der ganzen Welt erscheinen würden.