Discover Meteor

Building Real-Time JavaScript Web Apps

Wprowadzenie

1

Wyobraź sobie otwarcie tego samego folderu w dwóch oknach na twoim komputerze.

Teraz kliknij wewnątrz jednego z okien i usuń plik. Czy plik zniknął także w drugim oknie?

Nie musisz w zasadzie wykonywać tych kroków żeby wiedzieć, że plik został usunięty. Kiedy modyfikujemy lokalny system plików, zmiana jest wykonywana wszędzie bez potrzeby odświeżania czy wykonywania funkcji zwrotnej (ang. callback). To po prostu działa.

Teraz wyobraź sobie jak ten sam scenariusz zadziałałby w środowisku web'owym. Dla przykładu, powiedzmy, że otworzyłeś to samo okno administracyjnej WordPress'a w dwóch oknach przeglądarki i dodałeś nowych wpis w jednym z nich. W przeciwieństwie do folderów, nie ważne jak długo będziesz czekał, drugie okno nie odzwierciedli zmian dopóki go nie odświeżysz.

Przez lata przyzwyczailiśmy się do myśli, że strona internetowa jest czymś, z czym można komunikować się tylko w krótkich, odrębnych seriach.

Jednak Meteor jest częścią nowej fali framework'ów i technologii, które zamierzają stawić czoła status quo, sprawiając, że strony internetowe staną się reaktywne i będą działały w czasie rzeczywistym.

Czym jest Meteor?

Meteor jest platformą zbudowaną na bazie Node.js służącą do tworzenia internetowych aplikacji działających w czasie rzeczywistym. Jest tym co znajduje się pomiędzy bazą danych twojej aplikacji a interface'em użytkownika i zapewnia ich synchronizację.

Jako, że Meteor oparty jest na Node.js, używa on JavaScript'u zarówno po stronie klienta jak i serwera. Ponad to, Meteor umożliwia także współdzielenie kodu pomiędzy obydwoma środowiskami.

Otrzymujemy w ten sposób potężną platformę, która jest jednocześnie bardzo prosta w użyciu dzięki odseparowaniu programisty od wielu częstych problemów i zagrożeń wynikających z tworzenia aplikacji internetowych.

Dlaczego Meteor?

Zatem dlaczego powinieneś spędzić czas na nauce Meteor'a zamiast innego framework'a webowego? Zostawiając na boku przeróżne cechy Meteor'a, wszystko sprowadza się do jednej rzeczy: Meteor jest łatwy do opanowania.

Meteor umożliwia, łatwiej niż jakikolwiek inny framework, stworzenie aplikacji internetowej czasu rzeczywistego i uruchomienie jej w ciągu kilku godzin. A jeśli kiedykolwiek zajmowałeś się programowaniem front-end'owym, to zaznajomiony jesteś już z JavaScript'em, a pracując w Meteor'ze nie będziesz potrzebował uczyć się żadnego innego języka.

Meteor może ale nie musi być idealnym framework'iem dla ciebie. Ale skoro można zapoznać się z nim w ciągu kilku wieczorów lub przez weekend to czemu nie spróbować i przekonać się na własnej skórze?

Dlaczego ta książka?

Przez ostatnie 6 miesięcy, pracowaliśmy nad Telescope, open-source'ową applikacją napisaną w Meteor'ze, która umożliwia każdemu stworzenie własnego serwisu społecznościowego z aktualnościami (na podobę Reddit'a lub Hacker news), gdzie ludzie mogą zamieszczać linki i głosować na nie.

Wiele się nauczyliśmy budując tę aplikację, jednak nie zawsze łatwo było znaleźć odpowiedź na nasze pytania. Musieliśmy szukać rozwiązań w wielu różnych miejscach, a w wielu przypadkach wymyślać własne. Dlatego też w tej książce chcemy podzielić się zdobytą wiedzą i stworzyć prostą instrukcję jak krok po kroku, od zera zbudować pełno prawną aplikację w Meteor'ze.

Aplikacja, która będziemy budować jest nieznacznie uproszczoną wersją Telescope, którą nazwaliśmy Microscope. Podczas pisania aplikacji, poruszymy wiele różnych kwestii składających się na tworzenie aplikacji w Meteor'ze, takich jak konta użytkowników, kolekcje, trasowanie (ang. Routing) i wiele innych.

Jeśli po przeczytaniu książki będziesz chciał dalej zgłębiać wiedzę, będziesz w stanie z łatwością zrozumieć kod Telescope, gdyż została napisana zgodnie z tym samym schematem.

O autorach

Jeśli jesteś ciekaw kim jesteśmy i dlaczego możesz nam zaufać, poniżej znajduje się krótka informacja o naszej działalności.

Tom Coleman jest członkiem Percolate Studio, sklepu dla web developerów z nastawieniem na jakość i user experience. Jest również współtwórcą aplikacji Meteorite i repozytorium pakietów Atmosphere. Stoi także za wieloma projektami open-source dla Meteor'a (takimi jak Router).

Sacha Greif pracował z takimi startup'ami jak Hipmunk i RubyMotion jak designer webowy i produktu. Jest twórcą aplikacji Telescope i Sidebar (która bazuje na Telescope), jest także założycielem Folyo.

Rozdziały i sekcja uzupełniająca

Chcieliśmy aby ta książka była przydatna zarówno dla początkującego użytkownika Meteor'a jak i dla zaawansowanego programisty. Dlatego rozdziały zostały podzielone na dwie kategorie: zwykłe rozdziały (ponumerowane od 1 do 14) i rozdziały poboczne (numer połówkowe .5).

Zwykłe rozdziały poprowadzą cię przez proces budowania aplikacji , abyś jak najszybciej mógł sam rozpocząć pisanie aplikacji, wyjaśniając najważniejsze kwestie bez zaprzątania ci głowy zbytnimi szczegółami.

Z drugiej strony, rozdziały poboczne zgłębiać będą zawiłości Meteor'a i pomogą ci lepiej zrozumieć co tak naprawdę dzieje się pod maską.

Więc jeśli jesteś początkujący, możesz spokojnie pominąć rozdziały poboczne przy pierwszym czytaniu książki i powrócić do nich później gdy zaznajomisz się już trochę z Meteor'em.

Commits (pot. komity) i działające przykłady

Nie ma nic gorszego, niż kiedy czytając książkę o programowaniu nagle uświadomimy sobie, że nasz kod nie jest zgodny z tym z przykładów i nic nie działa tak jak powinno.

Aby tego uniknąć, musimy pobrać repozytorium Microscope z GitHub'a. Będziemy zamieszczać także bezpośrednie linki do komitów po każdych kilku zmianach w kodzie. Ponadto, przy każdym komicie zamieszczamy link do działającej instancji aplikacji dla danego komita, abyś mógł(a) porównać go ze swoją lokalną wersją. Oto przykład:

Zatwierdź 11-2

Display notifications in the header.

Pamiętaj jednak, że nie powinieneś przechodzić od jednego do drugiego git checkout tylko dlatego, że zamieściliśmy odwołania do komitów. Nauczysz się o wiele więcej, jeśli poświęcisz trochę czasu na ręczne napisanie kodu swojej aplikacji.

Kilka dodatkowych zasobów

Gdy kiedykolwiek będziesz potrzebować więcej informacji o konkretnej funkcji Meteor'a, oficjalna dokumentacja jest najlepszym miejscem do ropoczęcia poszukiwania.

Polecamy także serwis Stack Overflow w razie problemów i pytań, oraz kanał IRC #meteor jeśli potrzebujesz szybkiej odpowiedzi.

Czy potrzebuję Git'a?

Pomimo, że wiedza na temat działania systemu kontroli wersji Git nie jest wymagana aby podążać za przykładami w książce, jest jednak zalecana.

Jeśli chcesz szybko opanować Git'a, polecamy artykuł Nick'a Farina Git Is Simpler Than You Think.

Jeśli jesteś początkujący użytkownikiem Git'a, polecamy również GitHub dla Mac'a, narzędzie które umożliwia klonowanie i zarządzanie repozytorium bez korzystania z linii poleceń.

Kontakt

Jak zacząć

2

Pierwsze wrażenie jest ważne - dlatego proces instalacji Meteora powinien być stosunkowo łatwy. W większości przypadków Meteor będzie gotowy do działania w mniej niż pięć minut.

Aby zacząć, możemy go zainstalować otwierając okno terminala i wpisując:

$ curl https://install.meteor.com | sh

To zainstaluje konsolowe narzędzie meteor w twoim systemie. Od tej pory będziesz mógł zacząć używać Meteora.

Nie instalowanie Meteora

Jeżeli nie możesz (albo nie chcesz) instalować Meteor'a lokalnie, polecamy spróbować Nitrous.io.

Nitrous.io to serwis pozwalający Ci uruchamiać aplikacje i edytować ich kod w twojej przeglądarce, napisaliśmy krótki poradnik aby pomóc ci w konfiguracji.

Możesz także wykonwać tę instrukcję aż do sekcji “Instalacja Meteor'a i Meteorite” (włącznie), i następnie przejść przez książkę ponownie rozpoczynając od sekcji “Tworzenie prostej aplikacji” z tego rozdziału.

Meteorite

Zważając na fakt, iż Meteor nie wspiera jeszcze pakietów firm trzecich, Tom Coleman (jeden z autorów tej książki) wraz z kilkoma innymi członkami społeczności stworzyli Meteorite, wrapper dla Meteor'a. Meteorite zajmuje się także instalacją Meteor'a i podłączeniem dowolnego pakietu ktory możesz znaleźć.

Jako że będziemy polegać na pakietach firm trzecich dla niektórych funkcjonalności Microscope'a, zainstalujmy Meteorite.

Instalacja Meteorite

Musisz się upewnić że node i git są zainstalowane na twoim komputerze. Zainstaluj je w standardowy sposób dla twojego systemu operacyjnego, lub sprawdź te łącza:

Następnie, zainstalujmy Meteorite. Jest to pakiet npm (ang Node Package Module, standarowy format modułów Node'a), instalujemy go za pomocą:

$ npm install -g meteorite

Błedy uprawnień?

Na niektórych komputerach możesz potrzebować uprawnień root'a aby zainstalować Meteorite. Aby uniknąć problemów, upewnij się że używasz sudo -H:

$ sudo -H npm install -g meteorite

Możesz poczytać więcej o tym problemie w dokumentacji Meteorite.

To wszystko! Od tego momentu Meteorite będzie się wszystkim zajmował.

Notka: nie ma jeszcze obsługie Meteorite dla Windows, ale możesz spojrzeć na nasz przewodnik dla Windows.

### mrt vs meteor

Meteorite zainstaluje komendę mrt, którą będziemy używać do isntalacji pakietów dla naszej aplikacji. Jednakże kiedy chcemy uruchomić nasz serwer, nadal używamy komendy meteor.

Tworzenie prostej aplikacji

Teraz, kiedy mam zainstalowany Meteorite, stwórzmy aplikację. Aby to zrobić, używamy komendy wiersza poleceń Meteorite'a mrt:

$ mrt create microscope

Ta komenda ściągnie Meteor'a i dokona podstawowej konfiguracji, gotowego do użycia projektu Meteor'a. Kiedy skończy, powinieneś widzieć katalog microscope/, zawierający następujące pliki:

microscope.css  
microscope.html 
microscope.js   
smart.json 

Aplikacja którą Meteor dla ciebie stworzył jest to prosty kod, który prezentuje kilka prostych wzorców.

Mimo, iż nasza aplikacja nie robi zbyt wiele, możemy ją uruchomić. Aby uruchomić aplikację, wejdź do terminala i napisz:

$ cd microscope
$ meteor

Teraz wpisz w swojeje przeglądarce adres http://localhost:3000/ (lub odpowiednik http://0.0.0.0:3000/) i powinieneś zobaczyć mniej więcej coś takiego:

Meteor's Hello World.
Meteor’s Hello World.

Zatwierdź 2-1

Created basic microscope project.

Gratulacje! Twoja pierwsza aplikacja w Meteor'ze działa. Przy okazji, aby ją zatrzymać wszystko co musisz zrobić to w oknie terminala i w zakładce gdzie aplikacja jest uruchomiona nacisnąć ctrl+c.

Dodawanie pakietu

Użyejmy teraz Meteorite, aby dodać smart package, który pozwoli na dołączenie =Bootstrap do projektu:

$ mrt add bootstrap

Zatwierdź 2-2

Added bootstrap package.

Notka o Pakietach

Kiedy mówimy o pakietach w kontekście Meteor'a, warto pomówić o szczegółach. Meteor używa pięciu typów pakietów:

  • Rdzeń Meteor'a jako takie jest podzielone na rdzenne pakiety. Są one zawarte w każdej aplikacji Meteor'a i pradopobonie nigdy nie będziesz musiał się o nie martwić.
  • Meteor pakiety smart (ang. smart packages) są grupą około 37 pakietów (możesz zobaczyć całą listę za pomocą meteor list), które są dostarczane razem z Meteor'em i możesz je opcjonalnie zaimportować do swojej aplikacji. Możesz dodać je nawet, gdy nie używasz Meteorite, za pomocą meteor add packagename.
  • Pakiety lokalne to pakeity które możesz swtorzyć sam i włożyć je do katalogu /packages. Także nie potrzebujesz Meteorite by z nich korzystać.
  • pakiety smart Atmosphere to pakiety do Meteor'a trzecich firmy znajdujące się na Atmosphere. By je importować i używać potrzebny jest Meteorite.
  • pakiety NPM (Node Packaged Modules) to pakiety Node.js. Mimo, iż nie współpracują one od razu z Meteorem w stanie w jakim są dostarczane, mogą być używane przez wyżej wymienione typy pakietów.

Struktura plików aplikacji Meteor

Zanim zaczniemy pisać kod, musimy skonfigurować prawidłowo nasz projekt. Aby wyczyścić naszą strukturę, otwórz katalog microscope i usuń microscope.html, microscope.js, and microscope.css.

Następnie, stwórz pięc katalogów w /microscope: /client, /server, /public, /lib, and /collections oraz puste pliki main.html and main.js w katalogu /client. Nie przejmuj się jeżeli zepsuje to chwilowo aplikację, uzupełnimy te pliki w następnym rozdziale.

Powinniśmy wspomnieć, że niektóre z tych katalogów ma specjalne właściwości. Jeżeli chodzi o pliki, Meteor posiada następujące reguły:

  • Kod zawarty w katalogu /server uruchamiany jest tylko po stronie serwera.
  • Kod zawarty w katalogu /client uruchamiany jest tylko po stronie klienta.
  • Wszystko inne uruchamiane jest zarówno po stronie serwera jak i klienta.
  • Pliki w katalogu /lib są ładowane jako pierwsze.
  • Pliki main.* są ładowane na końcu.
  • Twoje zasoby statyczne (czcionki, obrazki, itd.) powinny być umieszczone w katalogu /public.

Warto zauważyć, że mimo, iż Meteor posiada te reguły, nie zmusza cię do używania jakiejkolwiek predefiniowanej struktury katalogów dla Twojej aplikacji. Tak więc struktura którą proponujemy jest naszym sposobem pisania aplikacji, a nie sztywnymi regułami.

Jeżeli chcesz poznać więcej szczegółów, zachęcamy by zapoznać się z oficjalną dokumentacją Meteor'a.

Czy Meteor jest frameworkiem MVC?

Jeżeli używałeś wcześniej innych frameworków, takich jak Ruby on Rails, możesz się zastanawiać czy Meteor zaadoptował wzorzec MVC

Krótka odpowiedź to: nie. W przeciwieństwie do Rails'ów, Meteor nie narzuca żadnej predefiniowanej struktury dla Twojej aplikacji. Także w tej książce poprostu układamy kod w sposób, który ma dla nas największy sens, nie martwiąc się przy tym za bardzo o akronimy.

Nie ma katalogu public?

Ok, skłamaliśmy. Nie potrzebujemy katalogu public/ z tego prostego powodu że Microscope nie potrzebuje żadnych statycznych zasobów. Ale odkąd większość aplikacji napisanych w Meteor'ze zawiera co najmniej kilka obrazków, uznaliśmy że warto poruszyć ten temat.

Przy okazji, mogłeś już zauważyć ukryty katalog .meteor. To jest miejsce w którym Meteor przechowuje swój własny kod - modyfikacja rzeczy znajdujących się tam to zazwyczaj bardzo zły pomysł. Prawdę ówiąc nie potrzebujesz nigdy zaglądać do tego katalogu. Jedynym wyjątkiem są pliki .meteor/packages and .meteor/release, które odpowiednio są używane do wylistowania twoich pakietów i wersji Meteor'a którą używasz. Kiedy dodajesz pakiet albo zmieniasz wersję Meteor'a, może być pomocne sprawdzenie zmian w tych plikach.

Podkreślenia vs CamelCase

Jedyne co mamy do powiedzenia na temat debaty pomiędzy podkreśleniami (my_variable) kontra camelCase (myVariable) jest to że naprawdę nie ma znaczenia którą konwencję przyjmiesz, jeżeli tylko się do niej konsekwentnie stosujesz.

W tej książce używamy camelCase, ponieważ jest to zwyczajowy zapis JavaScript'u (koniec końców, jest to JavaScript, a nie java_script!).

Jedynym wyjątkiem do tej reguły są nazwy plików, które będą używały podkreśleń (moj_plik.js) i klas CSS, które używają myślników (.moja-klasa). Powodem tego jest to, że podkreślniki są powszechnie spotykane w systemie plików, a składnia CSS używa myślników (font-family, text-align, itd.).

Zatroszcz się o CSS

To nie jest książka o CSS. Aby nie zwalniać zajmując się szczegółami styli, zdecydowaliśmy zrobić cały zestaw styli dostępny od początku, tak żebyś nie musiał się nim przejmować od początku.

CSS automatycznie jest zminimalizowany i załadowany przez Meteor'a, i w odróżnieniu do innych statycznych zasobów idzie do katalogu /client, a nie /public, także włóż do pliku style.css:

.grid-block, .main, .post, .comments li, .comment-form {
    background: #fff;
    border-radius: 3px;
    padding: 10px;
    margin-bottom: 10px;
    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
}
body {
    background: #eee;
    color: #666666;
}
.navbar { margin-bottom: 10px }
.navbar .navbar-inner {
    border-radius: 0px 0px 3px 3px;
}
#spinner { height: 300px }
.post {
    *zoom: 1;
    -webkit-transition: all 300ms 0ms;
    -webkit-transition-delay: ease-in;
    -moz-transition: all 300ms 0ms ease-in;
    -o-transition: all 300ms 0ms ease-in;
    transition: all 300ms 0ms ease-in;
    position: relative;
    opacity: 1;
}
.post:before, .post:after {
    content: "";
    display: table;
}
.post:after { clear: both }
.post.invisible { opacity: 0 }
.post .upvote {
    display: block;
    margin: 7px 12px 0 0;
    float: left;
}
.post .post-content { float: left }
.post .post-content h3 {
    margin: 0;
    line-height: 1.4;
    font-size: 18px;
}
.post .post-content h3 a {
    display: inline-block;
    margin-right: 5px;
}
.post .post-content h3 span {
    font-weight: normal;
    font-size: 14px;
    display: inline-block;
    color: #aaaaaa;
}
.post .post-content p { margin: 0 }
.post .discuss {
    display: block;
    float: right;
    margin-top: 7px;
}
.comments {
    list-style-type: none;
    margin: 0;
}
.comments li h4 {
    font-size: 16px;
    margin: 0;
}
.comments li h4 .date {
    font-size: 12px;
    font-weight: normal;
}
.comments li h4 a { font-size: 12px }
.comments li p:last-child { margin-bottom: 0 }
.dropdown-menu span {
    display: block;
    padding: 3px 20px;
    clear: both;
    line-height: 20px;
    color: #bbb;
    white-space: nowrap;
}
.load-more {
    display: block;
    border-radius: 3px;
    background: rgba(0, 0, 0, 0.05);
    text-align: center;
    height: 60px;
    line-height: 60px;
    margin-bottom: 10px;
}
.load-more:hover {
    text-decoration: none;
    background: rgba(0, 0, 0, 0.1);
}
client/stylesheets/style.css

Zatwierdź 2-3

Re-arranged file structure.

Informacja CoffeeScript

W tej książce będziemy pisać w czystym JavaScript. Jeżeli preferujesz CoffeeScript, Meteor umożliwia użycie go. Po prostu dodaj pakiet CoffeeScript:

mrt add coffeescript

Umieszczanie aplikacji na zewnętrznym serwerze

Sidebar 2.5

Część osób lubi pracować nad projektem, aż do momentu gdy doprowadzą go do perfekcji, natomiast reszta nie może doczekać się opublikowania go i pokazania wszystkim tak szybko, jak to tylko możliwe.

Jeżeli zaliczasz się do pierwszego grona i chciałbyś na razie rozwijać aplikację lokalnie, możesz przeskoczyć ten rozdział i przejść do następnego. Z drugiej strony, jeżeli wolisz poznać sposób, w jaki można opublikować aplikację online, zapraszamy do przeczytania tego rozdziału.

Nauczymy się jak opublikować aplikację Meteora na kilka różnych sposobów. Możesz wybrać dowolne rozwiązanie w jakimkolwiek stopniu zaawansowania projektu się znajdujesz, czy pracujesz nad Microscope czy nad inną aplikacją Meteora. Pora zacząć!

Wprowadzenie do Sidebars

Jest to rozdział poświęcony Sidebars. Bardziej szczegółowo jest opisany w dalszej części książki.

Jeżeli wolisz kontynuwać pracę nad Microscope, możesz przeskoczyć ten rozdział i wrócić do niego w dowolnym momencie.

Publikacja aplikacji na serwerach Meteora

Publikacja aplikacji w domenie Meteora (np. http://myapp.meteor.com) jest najłatwiejszym rozwiązaniem i będzie pierwszym, które wypróbujemy. Może być to użyteczne do pokazania aplikacji w fazie rozwoju, lub szybkiego ustawienia serwera pre-produkcyjnego.

Publikacja na serwerach Meteora jest całkiem prosta. Otwórz terminal, przejdź do folderu aplikacji Meteora i wpisz:

$ meteor deploy myapp.meteor.com

Oczywiście musisz się zatroszczyć o zmianę “myapp” dowolnie wybraną nazwą aplikacji, która nie jest jeszcze zajęta. Jeżeli pierwszy raz publikujesz aplikację, zostaniesz poproszony o dodanie konta Meteora. Jeżeli wszystko zostanie wykonane poprawnie, po kilku sekundach będziesz miał dostęp do swojej aplikacji na http://myapp.meteor.com.

Możesz skorzystać z oficjalnej dokumentacji, aby uzyskać więcej informacji na tematy takie jak bezpośredni dostęp do bazy danych opublikowanej aplikacji czy konfiguracja innej domeny dla twojej aplikacji.

Deploying On Modulus

Modulus jest świetnym wyborem, jeżeli chodzi o hosting aplikacji NodeJS. Jest to jeden z niewielu firm PaaS (platform-as-a-service), która wspiera oficjalnie Meteora i jest już całkiem pokaźna liczna osób posiadająch produkcyjne aplikacje na ich serwerach.

Demeteorizer

Narzędzie open source Modulusa demeteorizer konwertuje aplikację Meteora w standardową aplikację NodeJS.

Zacznij od utworzenia konta. Aby opublikować aplikację na serwerach Modulusa, musisz zainstalować konsolowe narzędzie ‘modulus’:

$ npm install -g modulus

A następnie zalogować się za pomocą:

$ modulus login

Utworzymy teraz projekt Modulusa (uwaga: można to także wykonać na ich stronie www przez przeglądarkę):

$ modulus project create

Następnym krokiem jest utworzenie bazy danych MongoDB dla aplikacji. Możemy ją utworzyć także w Modulus, MongoHQ lub jakimkolwiek innym hostingu MongoDB.

Po utworzeniu bazy danych MongoDB, możemy otrzymać adres MONGO_URL bazy z webowego interfejsu Modulusa (Dashboard > Databases > Select your database > Administration) i następnie użyć go do konfiguracji aplikacji jak poniżej:

$ modulus env set MONGO_URL "mongodb://<user>:<pass>@mongo.onmodulus.net:27017/<database_name>"

Jesteśmy gotowi opublikować aplikację. Jest bardzo proste:

$ modulus deploy

Właśnie opublikowaliśmy aplikację na serwerach Modulusa. Aby uzyskać więcej informacji o dostępie do logów, ustawieniu dowolnej domeny czy SSL, przejdź do dokumentacji Modulusa.

Meteor Up

Mimo tego, że nowe firmy hostingowe pojawiają się jak grzyby po deszczu, często napotkasz problemy związane z konkretnym hostingiem i jego ograniczeniami. Na dzień dzisiejszy publikacja aplikacji na własnym serwerze jest najlepszym sposobem na umieszczenie aplikacji Meteora w internecie. Jedyną przeszkodą może być to, że publikacja na własnym serwerze nie jest prosta, szczególnie gdy szukasz środowiska produkcyjnego o wysokiej jakości.

Meteor Up (lub w skrócie mup) jest kolejną próbą poradzenia sobie z tym problemem. Jest to narzędzie konsolowe, które zajmuje się ustawieniem i publikacją aplikacji za ciebie. Zobaczmy zatem jak apublikować Microscope używając Meteor Up.

Zanim będziemy mogli kontynuować, potrzebujemy serwer na który możemy opublikować aplikację. Polecamy Digital Ocean, gdzie hosting zaczyna się od 5$ miesięcznie lub AWS, który ma darmowe instancje Micro (szybko napotkasz problemy związane ze skalowaniem aplikacji, ale jeżeli chcesz się pobawić, na początek powinno wystarczyć).

Niezależenie, który serwis wybierzesz, powinieneś skończyć mając dostępne trzy rzeczy: adres IP twojego serwera, nazwę użytkownika umożliwiającego zalogowanie (zwykle root lub ubuntu) i hasło. Trzymaj je w bezpiecznym miejscu, wkrótce będziesz go potrzebował!

Inicjalizacja Meteor Up

Aby rozpocząć zainstalujemy Meteor Up za pomocą npm:

$ npm install -g mup

Następnie utworzymy specjalny folder w którym będą przechowywane ustawienia Meteor Up dla danego serwera. Użyjemy oddzielnego folderu z dwóch powodów: po pierwsze najlepiej unikać wstawiania prywatnych danych w repozytorium Git, szczególnie gdy pracujesz nad kodem dostępnym publicznie.

Po drugie, używając osobnych folderów, możesz równocześnie zarządzać kilkoma konfiguracjami Meteor Up. Staje się to na przykład przydatne przy publikacji na serwerze produkcyjnym i pre-produkcyjnym (ang. staging).

Utwórzmy zatem ten nowy folder i zainicjalizujmy w nim nowy projekt Meteor Up:

$ mkdir ~/microscope-deploy
$ cd ~/microscope-deploy
$ mup init

Wspóldzielenie ustawień za pomocą Dropbox

Świetnym sposobem na współdzielenie ustawień przez Ciebie i cały Twój zespół jest stworzenie folderu z konfiguracjami w twoim DropBoxie lub podobnym serwisie.

Konfiguracja Meteor Up

Podczas inicjalizacji nowego projektu Meteor Up stworzy dwa nowe pliki: mup.json oraz settings.json.

W mup.json znajdziesz wszystkie ustawienia dotyczące publikacji aplikacji, a w settings.json, wszystkie ustawienia związane z aplikacją (tokeny OAuth, Google Analytics itp.).

Następnym krokiem jest konfiguracja pliku mup.json. Poniżej znajdziesz domyślny plik mup.json wygenerowany przez mup init i wszystko co należy zrobić, to uzupełnić w nim puste pola:

{
  //informacje logowania na serwer
  "servers": [{
    "host": "hostname",
    "username": "root",
    "password": "password"
    //or pem file (ssh based authentication)
    //"pem": "~/.ssh/id_rsa"
  }],

  //instaluj MongoDB na serwerze
  "setupMongo": true,

  //ścieżka do aplikacji (lokalny folder)
  "app": "/path/to/the/app",

  //konfiguracja zmiennych środowiskowych
  "env": {
    "ROOT_URL": "http://supersite.com"
  }
}
mup.json

Zapoznajmu się po kolei z każdym z ustawień.

Server Authentication

Zauważysz, że Meteor Up wspiera logowanie za pomocą haseł i prywatnych kluczy (PEM), zatem może być używany z większością firm hostingowych w chmurze.

Ważna uwaga: jeżeli zdecydujesz się na użycie logowania za pomocą hasła, zainstaluj najpierw pakiet sshpass (zgodnie z tym tutorialem).

Konfiguracja MongoDB

Kolejnym krokiem jest konfiguracja bazy danych MongoDB dla Twojej aplikacji. Polecamy MongoHQ lub jakąkolwiek inną firmę hostingową dla MongoDB, ponieważ oferują profesjonalne wsparcie i lepsze narzędzia do zarządzania bazą.

Jeżeli zdecydowałeś się użyć MongoHQ, ustaw pole setupMongo na false i dodaj zmienną środowiskową MONGO_URL w bloku env w pliku mup.json. Jeżeli zdecydowałeś się na hosting Meteor Up, ustaw pole setupMongo na true, a Meteor Up zatroszczy się o resztę.

Ścieżka Aplikacji Meteora

Ponieważ nasza konfiguracja Meteor Up jest umieszczona w innym folerze, musimy wskazać Meteorowi z powrotem na naszą aplikację używając pola app. Wpisz lokalną ścieżkę, którą możesz otrzymać po wpisaniu pwd w konsoli w głównym folderze danej aplikacji.

Zmienne Środowiskowe

Możesz określić wszystkie zmienne środowiskowe aplikacji (takie jak ROOT_URL, MAIL_URL, MONGO_URL, itd.) w środku bloku env.

Ustawienia i publikacja aplikacji

Zanim będziemy mogli opublikować aplikację, musimy przygotować serwer, aby był gotowy na hosting aplikacji Meteora. Na szczęście ten skomplikowany proces został uproszczony do zawołania pojedyńczej komendy!

$ mup setup

W zależności od szybkości serwa i łącza może to zająć do kilku minut. Jeżeli wszystko zostało ustawione prawidłowo, możemy opublikować aplikację za pomocą:

$ mup deploy

To polecenie przygotuje i opublikuje aplikację Meteora na serwer, który właśnie przygotowaliśmy.

Wyświetlanie logów.

Logi są bardzo istotne i Meteor Up umożliwia łatwy dostęp do nich przez emulację polecenia tail -f. Wpisz:

$ mup logs -f

To podsumowuje nasz przegląd możliwości Meteor Up. Aby uzyskać więcej informacji sugerujemy zaznajomienie się z repozytorium Meteor Up na GitHub.

Powyższe trzy sposoby publikacji aplikacji powinny wystarczyć w większości przypadków. Wiemy, że wielu czytelników wolałoby mieć pełną kontrolę i ustawić serwer Meteora od podstaw. Jest to jednak temat, który może być poruszony innym razem…lub w innej książce!

Szablony

3

Aby ułatwić wprowadzenie do Meteora, zaimplementujemy ogólny szkielet aplikacji i później przejdziemy do coraz bardziej szczegółowych zagadnień. Innymi słowy na początek zbudujemy szkielet HTML/JavaScript, następnie będziemy go podłączać etapami do kolejnych części Meteora.

Oznacza to, że w bieżącym rozdziale skoncentrujemy się wyłącznie na tym, co dzieje się w folderze /client.

Stwórzmy nowy plik o nazwie main.html w folderze /client i wstawmy do niego następujący kod:

<head>
  <title>Microscope</title>
</head>
<body>
  <div class="container">
    <header class="navbar">
      <div class="navbar-inner">
        <a class="brand" href="/">Microscope</a>
      </div>
    </header>
    <div id="main" class="row-fluid">
      {{> postsList}}
    </div>
  </div>
</body>
client/main.html

Będzie to nasz główny szablon aplikacji. Jak widać jest to HTML z wyjątkiem taga {{> postsList}}, który jest miejscem wstawienia szablonu postsList jak wkrótce zobaczysz. Na teraz stwórzmy jeszcze kilka szablonów.

Szablony Meteora

Jądrem serwisu wiadomości społecznościowych jest lista postów. Dokładnie w taki sposób zorganizujemy szablony.

Stwórzmy folder /views w folderze /client. Będzie to miejsce wstawienia wszystkich szablonów. Aby trzymać porządek utworzymy również folder /posts w folderze /views przechowujący szablony mające związek z postami.

Znajdowanie plików

Meteor doskonale radzi sobie ze znajdowaniem plików. Nieważne gdzie wstawisz kod w folderz /client, Meteor go znajdzie i odpowiednio skompiluje. Oznacza to, że nigdy nie będziesz musiał wstawiał ścieżek ‘include’ do plików JavaScript czy CSS.

Oznacza to również, że mógłbyś równie dobrze wstawić wszystkie pliki w jeden folder, czy nawet cały kod w jeden plik. Ponieważ Meteor i tak skompiluje wszystko w jeden pojedyńczy skompresowany plik, chcielibyśmy raczej trzymać porządek i dobrze zorganizować strukturę plików.

W końcu jesteśmy gotowi utworzyć kolejny szablon. Utwórz plik posts_list.html w folderze client/views/posts:

<template name="postsList">
  <div class="posts">
    {{#each posts}}
      {{> postItem}}
    {{/each}}
  </div>
</template>
client/views/posts/posts_list.html

Oraz post_item.html:

<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
    </div>
  </div>
</template>
client/views/posts/post_item.html

Zwróć uwagę na atrybut name="postsList" elementu szablonu. Jest to nazwa, którą Meteor wykorzysta do śledzenia miejsca, w który wstawić odpowiedni szablon.

Nadszedł czas wprowadzić system szablonów Meteora, Handlebars. Handlebars jest po prostu HTML z dodatkiem trzech funkcji: partials, expressions (ang. wyrażeń) i block helpers (ang. helperów bloków kodu).

Partials używają składni {{> nazwaSzablonu}} i po prostu nakazują za zamienienie tego taga szablonem o tej samej nazwie (w naszym przypadku postItem).

Expressions, takie jak {{title}} mają dwojakie znaczenie: zwracają pole danego obiektu o nazwie title lub wartość zwracaną z helpera szablonu, który został zdefiniowany w bieżącym managerze szablonu (więcej o tym później).

Ostatecznie, block helpers to specjalne tagi, które kontrolują przebieg wykonania szablonu, np. {{#each}}…{{/each}} czy {{#if}}…{{/if}}.

Więcej szczegółów na temat Handlebars

Jeżeli chciałbyś się nauczyć więcej o Handlebars, przejdź na oficjalny serwis Handlebars lub przeczytaj ten przydatny tutorial

Zaopatrzeni w tą wiedzę, możemy łatwo zrozumieć co dzieje się podczas wykonywania naszej aplikacji.

Najpierw, w szablonie postsList iterujemy po obiekcie posts za pomocą helpera bloku {{#each}}…{{/each}}. Następnie w każdej iteracji wstawiamy szablon postItem.

Skąd pochodzi obiekt posts? DObre pytanie. Jest to helper szablonu, który zdefiniujemy podczas zapoznawania się z managerami szablonu.

Szablon postItem jest w miarę prosty do opisania. Używa tylko trzech wyrażeń: {{url}} i {{title}} zwracają pola dokumentu, a {{domain}} woła helper szablonu.

Wiele razy wspomnieliśmy o “helperach szablonu” bez wyjaśniania jak działają. Aby do tego przejść, musimy najpierw wyjaśnić zasadę działania managera szablonu.

Manager szablonu

DO tej pory mieliśmy styczność z Handlebars, które jest tak naprawdę HTML z kilkoma dodatkami w postaci tagów. W przeciwieństwie do innych języków takich jak PHP (czy nawet zwykłych stron HTML, które mogą zawierać JavaScript), Meteor oddziela szablony i ich logikę, a same szablony nie robią nic same z siebie.

Aby je ożywić, szablon potrzebuje managera. Możesz wyobrazić sobie analogię do szefa kuchni, który biorąc składniki potraw przygotowuje je przed podaniem kelnerowi (szablonowi), który go Tobie prezentuje.

Mówiąc inaczej, rola szablonów jest ograniczona do wyświetlania lub iterowania przez zmienne, manager wykonuje trudniejsze zadanie przez przypisanie wartości każdej zmiennej szablonu.

Manager?

Gdy zapytaliśmy programistów, jakie nazwy nadają managerom szablonu, połowa nazwała je “kontrolery”, a druga połowa “te pliki, w których umieszczam kod JavaScript”.

Manager nie jest tak naprawdę kontrolerem (przynajmniej w sensie kontrolera MVC) a termin “TFWIPMJSC” (przyp. tlum. ang. akronim od 'te pliki,…’) nie dawał się łatwo zapamiętać, więc odrzuciliśmy obie propozycje.

Nadal potrzebowaliśmy nadać nazwę czemuś, nad czym pracujemy i wymyśliśmy nazwę “manager”, która wcześniej nie miała swojego odpowiednika w innych frameworkach webowych.

Aby uprościć sytuację, zaadoptujemy konwencję nazywania managera tak samo, jak szablon, z wyjątkiem rozszerzenia .js. Stwórzmy zatem posts_list.js w folderze /client/views/posts i zacznijmy budowę pierwszego managera:

var postsData = [
  {
    title: 'Introducing Telescope',
    author: 'Sacha Greif',
    url: 'http://sachagreif.com/introducing-telescope/'
  }, 
  {
    title: 'Meteor',
    author: 'Tom Coleman',
    url: 'http://meteor.com'
  }, 
  {
    title: 'The Meteor Book',
    author: 'Tom Coleman',
    url: 'http://themeteorbook.com'
  }
];
Template.postsList.helpers({
  posts: postsData
});
client/views/posts/posts_list.js

Jeżeli wszystko zostało wpisane poprawnie, powinieneś zobaczyć w przeglądarce:

Pierwszy szablon zawierający dane statyczne
Pierwszy szablon zawierający dane statyczne

Zatwierdź 3-1

Dodany szablon listy postów i statyczne dane szablonu.

Osiągneliśmy tutaj dwie rzeczy. Po pierwsze ustawiliśmy dane początkowe prototypu aplikacji w talbice postsData. Dane te normalnie zostałyby pobrane z bazy danych, ale ponieważ jeszcze się tego nie nauczyliśmy (poczekaj do kolejnego rozdziału), “oszukujemy” przez wprowadzenie statycznych danych.

Po drugie, użyliśmy funkcję Template.myTemplate.helpers() Meteora, aby zdefiniować helper szablonu o nazwie posts który zwraca tablicę postsData.

Zdefiniowanie helpera posts oznacza, że jest on dostępny do użycia w obrębie szablonu:

<template name="postsList">
  <div class="posts">
    {{#each posts}}
      {{> postItem}}
    {{/each}}
  </div>
</template>
client/views/posts/posts_list.html

Zatem nasz szablon będzie w stanie iterować po tablicy postsData i wysyłać każdy obiekt po kolei do szablonu postItem:

Zawartość “this”

Stwórzmy manager post_item.js:

Template.postItem.helpers({
  domain: function() {
    var a = document.createElement('a');
    a.href = this.url;
    return a.hostname;
  }
});
client/views/posts/post_item.js

Zatwierdź 3-2

Ustawiony helper `domain` (domena) na `postItem`.

Tym razem wartością helpera domain nie jest tablica, ale funkcja anonimowa. Ten wzorzec jest jest częściej spotykany (i bardziej użyteczny) niż przedstawiony poprzednio przykład ze wstawieniem danych początkowych.

Wyświetlenie domen dla każdego linku.
Wyświetlenie domen dla każdego linku.

Helper domain ma URL jako paramter i zwraca jego domenę używając kilku sztuczek JavaScript. Ale skąd bierze się sam url?

Aby odpowiedzieć na to pytanie, musimy wrócić do szablonu posts_list.html. Block helper {{#each}} nie tylko iteruje po tablicy, are również ustawia wartość this w środku bloku odpowiadającego iterowanemu obiektowi.

Oznacza to, że pomiędzy tagami {{#each}}, każdy post po kolei jest równoznaczny z this i jest to zastosowane w każdym wstawioym managerze szablonu (post_item.js).

Rozumiemy teraz dlaczego this.url() zwraca URL bieżącego posta. Co więcej, jeżeli użyjemy {{title}} i {{url}} w środku szablonu post_item.html, Meteor rozpozna co oznacza this.title i this.url i zwróci poprawne wartości.

Sztuczki JavaScript

Mimo to, że nie jest to wyłącznie domena Meteora, przedstawimy szybko kilka “sztuczek JavaScript”. Na początek stworzymy pusty link HTML (a) i zapiszemy go w pamięci.

Ustawimy następnie jego atrybut href, aby wskazywał na URL bieżącego posta (jak przed chwilą zobaczyliśmy, this jest bieżącym obiektem do którego szablon ma dostęp).

Na końcu wykorzystujemy fakt, że specjalne pole hostname elementu a można wykorzystać do zwrócenia nazwy domeny danego linka bez zwracania całego URL/

Jeżeli uważnie przeczytałeś bieżący rozdział i zastosowałeś się do instrukcji, powinieneś zobaczyć w przeglądarce listę postów. Ta lista to dane statyczne i nie wykorzystujemy jeszcze żadnych funkcji Meteora pozwalających na pracę z danymi zmieniającymi się w czasie rzeczywistym. W kolejnymi rozdziale pokażemy, jak to zmienić!

Przeładowanie kodu podczas pracy aplikacji (ang. Hot Code Reload)

Mogłeś zauważyć, że nie było konieczne odświeżenia okna przeglądarki po każdej zmianie pliku.

Dzieje się tak, ponieważ Meteor śledzi wszystkie pliki dostępne w folderze danego projektu i automatycznie odświeża przeglądarkę za każdym razem, gdy znajdzie zmianę w jakimkolwiek pliku.

Przeładowanie kodu jest bardzo sprytne, zachowuje również stan aplikacji między odświeżeniami strony!

Używanie Git & GitHub

Sidebar 3.5

GitHub jest społecznościowym repozytorium dla projektów open-source bazujących na systemie kontroli wersji Git i jego podstawową funkcją jest ułatwienie współdzielenia kodu i współpracy przy pracy nad projektami.

Ten rozdział zakłada, że nie jesteś zaznajomiony z Git i GitHub. Jeżeli znasz oba, możesz przeskoczyć do następnego rozdziału!

Praca z commitami

Podstawowym narzędziem, zmianą części pliku jest commit. Możesz myślić o commit jak o zrzucie stanu bazy plików w danym momencie czasu.

Zamiast dostarczenia całego kodu Microcope, robiliśmy takie zrzuty w trakcie pisania książki i możesz je zobaczyć online na GitHub.

Przykładowo, tak wygląda ostatni commit poprzedniego rozdziału:

Commit Git wyświetlany na GitHub.
Commit Git wyświetlany na GitHub.

Możesz tutaj zobaczć “diff” (skrót z ang. difference) pliku post_item.js, innymi słowy zmiany, jakie zostały dokonane w danym commicie. W tym przypadku, stworzyliśmy od postaw plik posts_item.js, zatem cała jego zawartość jest podświetlona na zielono.

Porównajmy ten przykład z przykładem z dalszej części książki:

Wprowadzanie zmian kodu.
Wprowadzanie zmian kodu.

Tym razem wyłącznie zmienione linie zostały podświetlone na zielono.

Oczywiście czasami ani nie dodajesz ani zmieniasz linie kody, ale je usuwasz:

Usuwanie kodu.
Usuwanie kodu.

Pierwszy raz użyliśmy GitHub: zmiany są widoczne natychmiastowo.

Przeglądanie kodu commita

Okno przeglądania commita Gita pokazuje zmiany zawarte w danym commicie, ale czasem chciałbyś spojrzeć na pliki, które nie uległy zmianie, aby się upewnić, że ich zawartość jest poprawna w danym momencie czasu.

Ponownie GitHub okazuje się pomocny. Gdy jesteś na stronie commita, kliknij na przycisk Browse code:

Przycisk `Browse code`.
Przycisk `Browse code`.

Będziesz miał teraz dostęp do całego repozytorium w momencie danego commita.

Repozytorium podczas commita 3-2.
Repozytorium podczas commita 3-2.

GitHub nie pokazuje wielu wizualizacji tego, co obserwujemy patrząc na commit, ale możesz go porównać z “"normalnym” widokiem brancha master i zerknąć na różnice w strukturze pliku:

Repozytorium podczas commita 14-2.
Repozytorium podczas commita 14-2.

Lokalny dostęp do commita

Zobaczyliśmy przed chwilą, jak przeglądać kod danego commita online na GitHub. Ale co jeśli chciałbyć osiągnąć to samo lokalnie? Przykładowo, mógłbyś mieć ochotę na uruchomienie aplikacji lokalnie na danym commicie, aby zobaczyć jak ma się zachowywać w danym momencie czasu.

Aby to osiągnąć, przejdziemy przez pierwsze kroki (przynajmniej w tej książce) z konsolową aplikacją git. Dla początkujących, upewnij się, że zainstalowałeś Git. Następnie sklonuj (inaczej mówiąc, pobierz z serwera kopię) repozytorium Microscope za pomocą:

$ git clone git@github.com:DiscoverMeteor/Microscope.git github_microscope

Ostatni parametr github_microscope jest lokalną nazwą repozytorium, do którego będziesz klonował aplikację. Zakładając, że już posiadasz folder microscope, wybierz dowolną nazwę (nie musi być taka sama, jak repozytorium na GitHub).

Następnie przejdźmy do tego folderu używając cd i zacznijmy używać konsolową aplikację git:

$ cd github_microscope

Teraz gdy sklonowaliśmy respozytorium z GitHub, pobraliśmy cały kod aplikacji, co oznacza że mamy dostęp do najnowszego commita.

Na szczęście jest możliwość wrócenia do dowolnego miejsca w czasie i pobranie (ang. “check out”) danego commita bez wpływania na pozostałe. Wypróbujmy to teraz:

$ git checkout chapter3-1
Note: checking out 'chapter3-1'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b new_branch_name

HEAD is now at a004b56... Added basic posts list template and static data.

Git poinformował, że jesteśmy obecnie w stanie “detached HEAD”, co oznacza, że możemy obserwować poprzednie commity, ale nie możemy ich modyfikować. Możesz wyobrazić sobie w tym momencie wróżkę patrzącą w szklaną kulę.

(Uwaga: Git posiada w swoim arsenale również komendy pozwalające na zmianę poprzednich commitów. Tu mógłbyś wyobrazić sobie podróżnika w czasie zmieniającego przeszłość, ale jest to poza zakresem tego krótkiego wstępu do Gita).

Powodem, dla którego mogłeś wpisać chapter3-1 jest to, że otagowaliśmy wszystkie commity Microscope poprawnym znacznikiem rozdziału. Gdybyśmy tego nie zrobili, konieczne byłoby odnalezienie hasha lub mówiąc inaczej unikalnego idetyikatora danego commita.

Ponownie w takim przypadku GitHub ułatwia pracę. Można znaleźć tag commita w dolnym prawym rogu niebieskiego przycisku nagłówka, jak pokazano poniżej:

Znajdowanie hasha commita.
Znajdowanie hasha commita.

Wypróbujmy zatem użyć hash zamiast taga:

$ git checkout c7af59e425cd4e17c20cf99e51c8cd78f82c9932
Previous HEAD position was a004b56... Added basic posts list template and static data.
HEAD is now at c7af59e... Augmented the postsList route to take a limit

A co, jeżeli znudzi nam się patrzenie w szklaną kulę i chcemy wrócić do stanu początkowego? W takim przypadku nakazujemy Git, aby pobrał branch master:

$ git checkout master

Zanotuj, że możesz również uruchomić aplikację za pomocą komendy meteor w dowolnym momencie tego procesu, nawet w stanie “detached HEAD”. Może zaistnieć konieczność zawołania mrt update jeżeli Meteor wyświetli komunikat błędu o brakującym pakiecie, ponieważ kod pakietów nie jest dołączony do repozytorium Git dla Microscope.

Oglądanie historii zmian

Często napotkasz następujący scenariusz: patrzysz na plik i zauważasz zmiany, których wcześniej nie widziałeś. Nie pamiętasz kiedy plik uległ zmianie. Możesz sprawdzić każdy commit po kolei, aż nie znajdziesz prawidłowego, ale istnieje łatwiejszy sposób aby to odnaleźć dzięki funkcji Historia (ang. History) w GitHub.

Po pierwsze, znajdź dany plik w repozytorium w GitHub, następnie odszukaj przycisk “History.

Przycisk History w GitHub.
Przycisk History w GitHub.

Widzimy teraz uporządkowaną listę commitów, które zmieniły dany plik:

Wyświetlenie historii plików.
Wyświetlenie historii plików.

Znajdowanie osoby odpowiedzialnej za dokonane zmiany

Aby zebrać wszystkie informacje, przyjrzymy się bliżej Blame.

Przycisk Blame w GitHub.
Przycisk Blame w GitHub.

Uporządkowany widok informuje kto i w jakim commicie modyfikował dany plik (innymi słowo, kogo winić za popełnione czyny, jeżeli coś nie chce działać).

Widok Blame w GitHub.
Widok Blame w GitHub.

Ponieważ Git oraz GitHub są całkiem skomplikowanymi narzędziami, nie mamy zamiaru przedstawić wszystkich informacji na ich temat w pojedyńczym rozdziale. Tak naprawdę dotknęliśmy tylko czubka góry lodowej, ale mamy nadzieję, że przedstawiona wiedza okaże się przydatna podczas dalszego czytania tej książki.

Kolekcje

4

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:

Posts = new Meteor.Collection('posts');
collections/posts.js

Zatwierdź 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.

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.

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.

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

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

The 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

Konsola Mongo
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.

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:

> db.posts.insert({title: "A new post"});

> db.posts.find();
{ "_id": ObjectId(".."), "title" : "A new post"};
Konsola Mongo

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.

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.

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.

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.

> db.posts.find();
{title: "A new post", _id: ObjectId("..")};
Konsola Mongo
 Posts.findOne();
{title: "A new post", _id: LocalCollection._ObjectID};
Konsola pierwszej przeglądarki

Utwórzmy nowy post. W jednym z okien przeglądarki wykonaj komendę insert:

 Posts.find().count();
1
 Posts.insert({title: "A second post"});
'xxx'
 Posts.find().count();
2
Konsola pierwszej przeglądarki

Bez większych niespodzianek post został dodany do lokalnej kolekcji. Sprawdźmy teraz bazę Mongo:

❯ db.posts.find();
{title: "A new post", _id: ObjectId("..")};
{title: "A second post", _id: 'yyy'};
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:

 Posts.find().count();
2
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:

$ 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:

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'
  });
}
server/fixtures.js

Zatwierdź 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:

 Posts.find().fetch();
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ć:

Template.postsList.helpers({
  posts: function() {
    return Posts.find();
  }
});
client/views/posts/posts_list.js

Zatwierdź 4-3

Połączenie kolekcji z szablonem `postsList`.

Znajdowanie i pobieranie danych z bazy

Funkcja find() w Meteorze zwraca kursor, który jest reaktywnym źródłem danych. 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).

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:

Użycie zmiennych danych
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:

 Posts.insert({
  title: 'Meteor Docs', 
  author: 'Tom Coleman', 
  url: 'http://docs.meteor.com'
});
Konsola przeglądarki

Spójrz na przeglądarkę – powinieneś zobaczyć:

Dodawanie postów przez konsolę
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.

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.

Łą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:

$ 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:

Meteor.publish('posts', function() {
  return Posts.find();
});
server/publications.js

Po stronie klienta musimy zasubskrybować publikację. Dodamy poniższą linię do main.js:

Meteor.subscribe('posts');
client/main.js

Zatwierdź 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.

Publikacje i subskrypcje

Sidebar 4.5

Publikacje i subskrypcje należą do fundamentalnych konceptów Meteora, ale ciężko je zrozumieć jeżeli dopiero zaczynasz przygodę z Meteorem.

Prowadzi to do wielu nieporozumień, tak jak plotka, że Meteor nie jest bezpieczny, lub że Meteor nie jest w stanie poradzić sobie z dużą liczbą danych.

W dużym stopniu powodem, dla którego ludzie są zdezorientowani, jest “magia”, którą Meteor za nas wykonuje. Mimo to, że ta “magia” jest bardzo użyteczna, może ukryć wiele kwestii, które są ukryte za kulisami implementacji (jak to zwykle bywa, gdy wiele rzeczy jest robione za nas). Zobaczmy zatem pod maskę i spróbujmy zrozumieć, co naprawdę się dzieje.

Po staremu

Najpierw wróćmy do roku 2011, gdy nie było jeszcze Meteora. Przypuśćmy, że budujesz prostą aplikację Rails. W momencie, gdy użytkownik wejdzie na Twoją stronę, klient (czyli przeglądarka) wysyła żądania do aplikacji, która jest uruchomiona na serwerze.

Pierwszym zadaniem aplikacji jest sprawdzenie, jakie dane udostępnić użytkownikowi. Może to być strona 12 rezultatów wyszukiwania, informacja o profilu Marysi, ostatnie 20 tweetów Bartka itd. Można o tym myśleć jako o sprzedawcy pracującemu w księgarni, który przegląda półki na twoje żądanie.

Gdy dane zostały znalezione, kolejnym zadaniem aplikacji jest przetłumaczenie ich w format HTML zrozumiały dla człowieka (lub JSON w przypadku API).

Używając metafory księgarni, można to skojarzyć z opakowaniem właśnie zakupionej książki i wrzucenie jej do torby. Jest to część nazywana “View” w znanym modelu MVC (Model-View-Controller).

Ostatecznie, aplikacja wysyła wygenerowany kod HTML do przeglądarki. Praca aplikacji jest zakończona i kontrola zostaje przekazana z dala od jej wirtualnych rąk, oczekująć na kolejny dostęp.

Jak to robi Meteor

Zaznajomimy się z tym, co czyni Meteor tak specjalnym, w porównaniu do wcześniejszych rozwiązań. Jak już zdążyliśmy zauważyć, główną innowacją Meteora jest to, że aplikacje Rails są uruchamiane wyłącznie po stronie serwera, aplikacje Meteora zawierają również komponent po stronie klienta, który jest uruchamiany po stronie klienta (czyli w przeglądarce).

Przesyłanie podzbioru bazy danych do klienta.
Przesyłanie podzbioru bazy danych do klienta.

Odpowiada to sprzedawcy, który nie tylko znajduje dla Ciebie książki, ale również idzie z Tobą do domu i czyta je dla Ciebie w nocy (przyznajmy, że przyprawia to trochę o gęsią skórkę).

Taka architektura pozwala na osiągnięcie Meteorowi wielu ciekawych rzeczy, między innymi bardzo znane śą metody erwerowym (ang. Meteor calls) database everywhere. Mowiąc prostym językiem, Meteor kopiuje część bazy danych i przesyła ją klientowi.

Ma to dwie duże konsekwencje: po pierwsze, zamiast wysyłać klientowi kod HTML, aplikacja Meteora wyśle prawdziwe, surowe dane i pozwoli klientowi nimi zarządzać i je wyświetlać (data on the wire). Po drugie, będziesz miał natychmiastowy dostęp do danych* bez konieczności na oczekiwanie na odpowiedź serwera (kompensacja lagów).

Publikowanie kolekcji

Baza danych aplikacji może zawierać dziesiątki tysięcy dokumentów, niektóre z nich mogą być prywatne lub utajnione. Z tego względu nie powinniśmy robić wiernej kopii całej bazy po stronie klienta, z uwagi na prywatność jak i skalowanie aplikacji.

Potrzebujemy zatem znaleźć sposób na przekazanie przez Meteora jedynie podzbioru danych, które mogą zostać wysłane do klienta. Osiągniemy to za pomocą publikacji.

Wróćmy do Microscope. Poniżej znajdziesz wszystkie posty znajdujące się w bazie danych aplikacji:

Wszystkie posty znajdujące się w bazie danych.
Wszystkie posty znajdujące się w bazie danych.

Co prawda taka funkcjonalność nie istnieje w aplikacji Microscope, ale wyobraźmy sobie, że niektóre z postów zostały zaznaczone jako posiadające obraźliwe słownictwo. Mimo to, że chcemy je dalej przechowywać w bazie danych, nie powinny być udostępniane użytkownikom (tj. wysyłane do klienta).

Naszym pierwszym zadaniem będzie przekazanie Meteorowi które dane chcemy wysłać klientowi. Bedą publikowane wyłącznie te posty, które nie zostały oznaczone flagą jako obraźliwe.

Wyłączenie zaznaczonych postów.
Wyłączenie zaznaczonych postów.

Oto kod odpowiadający tej funkcjonalności po stronie serwera:

// na serwerze
Meteor.publish('posts', function() {
  return Posts.find({flagged: false}); 
});

Upewnia się to, że nie ma możliwości tego, że klient będzie miał dostęp do oflagowanych postów. Jest to dokładnie sposób, w jaki zabezpieczysz aplikację: przez upewnienie się, że publikujesz tylko te dane, do których bieżący użytkownik ma dostęp.

DDP

Zasadniczo możesz wyobrazić sobie system publikacji/subskrypcji jako rurę, która transferuje dane ze strony serwera (źródłowej kolekcji) do klienta (docelowej kolekcji).

Protokół, przez który odbywa się komunikacja nazywa się DDP (ang. Distributed Data Protocol). Aby dowiedzieć się więcej o protokole DDP, możesz obejrzeć wystąpienie Matta DeBergalisa z The Real-time Conference (jednego z założycieli Meteora), lub ten screencast autorstwa Chrisa Mathera, który wprowadza w ten protokół bardziej szczegółowo.

Subskrybowanie kolekcji

Mimo tego, że chcemy udostępnić klientowi wszystkie nieoflagowane posty, nie możemy tak po prostu wysłać tysięcy postów jednocześnie. Potrzebujemy znaleźć sposób na przekazanie klientowi jaki podzbiór danych potrzebują w danej chwili i właśnie to zapewniają subskrypcje.

Jakiekolwiek dane, które będziesz subskrybował, będą miały w całości kopiowane klientowi dzięki MiniMongo, implementacji bazy MongoDB po stronie klienta.

Przykładowo, załóżmy, że przeglądamy stronę z profilem Boba Smitha i chcemy wyświetlić wyłącznie jego posty.

Subskrybowanie postów Boba skopiuje je po stronie klienta.
Subskrybowanie postów Boba skopiuje je po stronie klienta.

Na początek zmodyfikujemy naszą publikację i dodamy do niej parametr:

// na serwerze
Meteor.publish('posts', function(author) {
  return Posts.find({flagged: false, author: author});
});

A następnie zdefiniujemy ten parametr, gdy subskrybujemy tą publikację w kodzie aplikacji po stronie klienta:

// po stronie klienta
Meteor.subscribe('posts', 'bob-smith');

Właśnie w ten sposób zapewniasz skalowalność aplikacji po stronie klienta: zamiast subskrybować wszystkie dostępne dane, po prostu wybierasz te, które bieżąco potrzebujesz. W ten sposób unikasz zapełnienia pamięci przeglądarki niezależnie od wielkości bazy danych po stronie serwera.

Szukanie w bazie danych

Okazuje się, że post Boba jest przyporządkowany do kliku kategorii (np. “JavaScript”, ”Ruby” i ”Python”). Możliwe jest, że nadal chcemy wczytać wszystkie posty Boba do pamięci, ale chcemy wyświetlić tylko te z kategorii “JavaScript”. Pomocne w tym będzie tworzenie zapytań w bazie za pomocą find.

Zaznaczanie podzbioru dokumentów po stronie klienta.
Zaznaczanie podzbioru dokumentów po stronie klienta.

Tak, jak po stronie serwera, użyjemy funkcję Posts.find() do zaznaczenia pozdbioru danych.

// po stronie klienta
Template.posts.helpers({
  posts: function(){
    return Posts.find(author: 'bob-smith', category: 'JavaScript');
  }
});

Teraz gdy mamy ogólne pojęcie na temat roli, jaką odgrywają publikacje i subskrypcje, spójrzmy na te pojęcia bardziej szczegółowo i poznajmy kilka powszechnych wzorców implementacji.

Autopublikacja

Jeżeli tworzysz projekt Meteora od podstaw, (tj. używając meteor create), projekt będzie automatycznie wyposażony w pakiet autopublish. Aby zacząć porozmawiajmy o tym, jak ten pakiet dokładnie działa.

Celem autopublish jest ułatwienie implementacji aplikacji Meteora poprzez automatyczne kopiowanie wszystkich danych przechowywanych na serwerze do klienta, obsługując za Ciebie wszystkie publikacje i subskrypcje za jednym zamachem.

Autopublikacja
Autopublikacja

Jak to działa? Przyjmijmy, że posiadasz aplikację 'posts' po stronie serwera. W takim przypadku autopublish wyśle wszystkie posty, które znajdzie w kolekcji posts po stronie serwera do kolekcji 'posts' po stronie klienta (zakładając, że istnieje).

Jeżeli zatem używasz pakietu autopublish, nie musisz się troszczyć o publikacje. Dane są wszechobecne i wszystko jest proste. Oczywiście możesz napotkać oczywiste problemy związane z utrzymywaniem kompletnej kopii bazy danych po stronie klienta na każdej maszynie po stronie klienta.

Z tego powodu użycie autopublish jest odpowiednie wyłącznie wtedy gdy zaczynasz implementację i nie uzwględniłeś publikacji.

Publikowanie całych kolekcji

Zaraz po tym, jak usuniesz autopublish, szybko zdasz sobie sprawę, że zniknęły dane po stronie klienta. Łatwym sposobem na ich odzyskanie jest zduplikowanie funkcjonalności pakietu autopublish przez publikowanie całej kolekcji. Przykładowo:

Meteor.publish('allPosts', function(){
  return Posts.find();
});
Publishing a full collection
Publishing a full collection

W dalszym ciągu publikujemy całą kolekcję, ale przynajmniej mamy kontrolę nad tym, które kolekcje publikować, a które nie. W tym przypadku, publikujemy kolekcję Posts, ale nie publikujemy Comments.

Częściowe publikowanie kolekcji

Następnym poziomem kontroli publikacji danych jest publikacja części kolekcji. Na przykład publikacja postów należących do danego autora:

Meteor.publish('somePosts', function(){
  return Posts.find({'author':'Tom'});
});
Częściowe publikowanie kolekcji
Częściowe publikowanie kolekcji

Za kulisami

Jeżeli czytałeś dokumentację publikacji Meteora, mogłeś być przytłoczony fragmentem nakazującym używania funkcji added() i ready() do ustawiania atrybutów rekordów po stronie klienta i mogłeś być zaskoczony tym, że nie widziałeś nigdy użycia powyższych metod w żadnej widzianej aplikacji Meteora.

Jest to spowodowane bardzo ważnym ułatwieniem, które dostarcza Meteor: metodą _publishCursor(). Nie widziałeś również tej metody? Może nie bezpośrednio, ale jeżeli zwracasz kursor (np. Posts.find({'author':'Tom'})) w funkcji dokonującej publikacji, to Meteor używa właśnie tej funkcji.

Gdy Meteor rozpoznaje, że publikacjia somePosts zwraca kursor, woła metodę _publishCursor() aby – tak, zgadłeś – automatycznie publikować powyższy kursor.

Pokrótce opiszmy sposób działania _publishCursor():

  • Sprawdza nazwę kolekcji po stronie serwera.
  • Wczytuje z bazy danych wszystkie pasujące do danego kursora dokumenty i wysyła je do kolekcji po stronie klienta która ma tą samą nazwę, co na serwerze. (Wewnętrznie używa metodę .added() aby to osiągnąć).
  • Za każdym razem, gdy dodano, zmieniono lub usunięto dokument, wysyła zmiany do kolekcji po stronie klienta. (Wewnętrznie używa metodę .observe() na kursorze i .added(), .changed() oraz removed()).

Zatem w powyższym przykładzie byliśmy w stanie upewnić się, że użytkownik otrzymał wyłącznie te posty, które go interesowały (te napisane przez Toma), dostępne w cache'u po stronie klienta.

Publikowanie wyłącznie niektórych pól kolekcji

Poznaliśmy jak publikować niektóre posty, ale możemy w większym stopniu ograniczyć liczbę przesyłanych danych! Zobaczmy jak publikować wyłącznie wybrane pola kolekcji.

Tak, jak poprzednio, użyjemy metody find() , aby zwróciła kursor, ale tym razem wyłączymy z zapytania niektóre pola:

Meteor.publish('allPosts', function(){
  return Posts.find({}, {fields: {
    date: false
  }});
});
Publikowanie niektórych pól
Publikowanie niektórych pól

Oczywiście możemy połączyć oba rozwiązania. Na przykład, jeżeli chcemy zwrócić wszystkie posty napisane przez Toma, ale nie publikować daty napisania postów, napisalibyśmy:

Meteor.publish('allPosts', function(){
  return Posts.find({'author':'Tom'}, {fields: {
    date: false
  }});
});

Podsumowanie

Przeszliśmy drogę od opublikowania wszystkich pól wszystkich dokumentów danej kolekcji (za pomocą autopublish) do publikowania wyłącznie niektórych pól niektórych dokumentów wybranych kolekcji.

To podsumowuje podstawę pracy z publikacjami Meteora i techniki poznane powyżej są w stanie pokryć znaczną większość powszechnych zastosowań.

Czasami jednak będziesz musiał wyjść poza te ramy przez łączenie, linkowanie lub mergowanie publikacji. Opiszemy to w kolejnym rozdziale!

Routing

5

Teraz skoro mamy listę postów (która w końcu będzie wysłana) potrzebujemy stronę dla każdego posta, na której użytkownicy będą mogli prowadzić dyskuję.

Chcielibyśmy, żeby te strony były dostępne za pomocą permanentnego linka, URLa w formie http://myapp.com/posts/xyz (gdzie xyz jest identyfikatorem MongoDB _id), który jest unikalny dla każdego posta.

Oznacza to, że potrzebujemy pewnego rodzaju routera, który sprawdza, jaki URL został wpisany w przeglądarce i przekierowuje do właściwej podstrony i wyświetla pożądaną treść.

Dodanie pakietu Iron Router

Iron Router jest routerem przeznaczonym i zaprojektowanym specjalnie dla aplikacji Meteora.

Nie tylko pomaga ze zorganizowaniem routingu (odpowiednich ścieżek), ale również dba o filtry (przypisaniem akcji do niektórych ścieżek) jak również zarządza subskrypcjami (kontroluje, która ścieżka ma dostęp do konretnych danych). (Uwaga: Iron Router został częściowo zaprojektowany przez autora książki Discover Meteor, którą czytasz , Toma Colemana.)

Na początek zainstalujmy pakiet z Atmosphere:

$ mrt add iron-router
Terminal

Powyższa komenda pobiera i instaluje pakiet iron-router w Twojej aplikacji, który jest od razu gotowy do użycia. Zauważ, że będziesz musiał czasem zrestartować aplikacji (za pomocą ctrl+c aby zabić proces i mrt aby go ponownie wystartować) aby pakiet mógł być użyty.

Zauważ, że Iron Router jest pakietem niedostępnym w standardowych pakietach Meteora, pochodzi z trzeciego źródła i konieczna jest instalacja Meteorite (meteor add iron-router nie zadziała).

Słowniczek Routera

Poruszymy wiele różnych tematów routera w tym rozdziale. Jeżeli masz doświadczenie z frameworkiem takim jak Rails, będziesz już zaznajomiony z większością tych konceptów. Jeżeli nie jesteś, oto kilka pojęć, które przyspieszą pracę (zachowujemy oryginalne nazewnictwo, ponieważ jest stosowane w kodzie źródłowym, niektórych się z reguły nie tłumaczy. Podajemy tłumaczenie w nawiasie - przyp. tłum.):

  • Routes (trasy): Trasa jest fundamentalnym pojęciem routera. Jest to zbiór instrukcji, który wskaże aplikacji gdzie się kierować i co robić po napotkaniu danego URL.
  • Paths (ścieżki): Ścieżka jest dowolnym URLem dostępnym w Twojej aplikacji. Może być statyczna (/terms_of_service) lub dynamiczna (/posts/xyz) i może nawet zawierać parametry zapytań (/search?keyword=meteor).
  • Segments (segmenty): części ścieżki rozdzielone slashem (/).
  • Hooks (haki): Hak jest akcją, którą chciałbyć wykonać przed, po, lub podczas wykonywania procesu routowania. Typowym przykładem może być sprawdzenie, czy użytkownik ma wystarczające prawa, aby obejrzeć daną stronę.
  • Filters (filtry): Filtry to globalne haki, które definiuje się dla jednej lub więcej tras.
  • Route Templates (szablony trasy): Każda trasa musi wskazywać na szablon. Jeżeli nie określisz konkretnego szablonu, router będzie domyślnie szukał szablonu o tej samej nazwie, co trasa.
  • Layouts: (układy) Możesz pomyśleć o układach jak o ramce na zdjęcia. Zawierają cały kod HTML, który opakowuje bieżący szablon i pozostanie nienaruszony po zmianach szablonu.
  • Controllers (kontrolery): Czasami zdasz sobie sprawę, że wiele Twoich szablonów używa tych samych parametrów. Zamiast duplikować kod, możesz sprawic aby wszystkie takie trasy dziedziczyły z jednego kontrolera trasy, który będzie zawierał logikę routingu.

Aby dowiedzieć się więcej o Iron Router, sprawdź pełną dokumentację na GitHub.

Routing: Mapowanie URL na odpowiednie szablony

Jak do tej pory budowaliśmy layout za pomocą zahardcode'owanych dołączeń szablonu (takich jak {{>postsList}}). Zatem mimo to, że zawartość strony może ulec zmianom, struktura strony pozostanie taka sama: nagłówek z listą postów poniżej.

Iron Router pozwala na przerwanie tego schematu przez przejęcie kontroli nad zawartością renderowaną w tagu HTML <body>. Nie definiujemy zatem zawartości nagłówka sami, jak by to było w przypadku normalnej strony HTML. Zamiast tego wskazujemy routerowi specjalny szablon layoutu zawierający helper szablonu zawierający {{> yield}}.

Powyższy helper {{> yield}} zdefiniuje specjalny dynamiczny obszar, który zostanie automatycznie przerenderowany za każdym razem, gdy szablon będzie odpowiadał bieżącej ścieżce (przyjmijmy, że ten specjalny szablon będzie się nazywał od tej pory “szablonem trasy” ang. “route templates”):

Layouts and templates.
Layouts and templates.

Rozpoczniemy od utworzenia naszego szablonu i dodania helpera {{> yield}}. Najpierw usuniemy tag HTML <body> z main.html i przeniesiemy jego zawartość do odrębnego szablonu layout.html.

Nasz odchudzony main.html wygląda teraz następująco:

<head>
  <title>Microscope</title>
</head>
client/main.html

A nowo utworzony layout.html będzie zawierał zewnętrzny layout aplikacji:

<template name="layout">
  <div class="container">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="brand" href="/">Microscope</a>
    </div>
  </header>
  <div id="main" class="row-fluid">
    {{> yield}}
  </div>
  </div>
</template>
client/views/application/layout.html

Zauważyłeś zapewne, że zamieniliśmy włączenie szablonu postsList wywołaniem helpera yield. Po zastosowaniu tej zmiany nie zobaczysz nic na ekranie. Dzieje się tak, ponieważ nie wskazaliśmy routerowi jak stosować się do URL / i pokazuje nam domyślnie pusty szablon.

Aby rozpocząć, możemy przywrócić poprzednią funkcjonalność przez przemapowanie głównego URL / do szablonu postList. Utworzymy folder /lib w głównym folderze projektu i plik router.js w tym folderze.

Router.configure({
  layoutTemplate: 'layout'
});

Router.map(function() {
  this.route('postsList', {path: '/'});
});
lib/router.js

Osiągneliśmy dwie ważne sprawy. Po pierwsze, wskazaliśmy routerowi, aby używał layout, który właśnie utworzyliśmy jako domyślny layout dla wszystkich tras. Po drugie, zdefiniowaliśmy nową trasę o nazwie postLists i powiązaliśmy ją ze ścieżką /.

The /lib folder

Cokolwiek wstawisz do folderu lib będzie załadowane przed załadowaniem pozostałych części aplikacji (z wyjątkiem smart packages). Jest to świetne miejsce do wstawiania jakiegokolwiek kodu pomocniczego, który ma być cały czas dostępny.

Uwaga: zauważ, że skoro folder /lib nie jest umieszczony w /client ani /server, oznacza to, że jego zawartość będzie dostępna w obu środowiskach.

Nazwane trasy (ang named routes)

Rozwiejmy pewne wątpliwości. Nazwaliśmy naszą trasę postsList, a również mamy szablon o nazwie postsList. Jak można to w prosty sposób wyjaśnić?

Domyślnie Iron Router szuka szablonu o tej samej nazwie, co trasa. Tak naprawdę również szuka ścieżki o tej samej nazwie, co nazwa trasy. Oznacza to, że jeżeli nie zdefiniowaliśmy odrębnej ścieżki (a zrobiliśmy to przez dodanie opcji path w definicji trasy), nasz szablon nie byłby dostępny domyślnie przez URL postsList.

Możesz się zastanawiać, dlaczego w ogóle potrzebujemy nadawać nazwy trasom. Otóż pozwala to na użycie kilku funkcji Iron Routera, które ułatwiają na tworzenie linków w aplikacji. Najbardziej przydatną jest helper Handlebars {{pathFor}}, który zwraca ścieżkę URL dla każdej trasy.

Chcemy, aby nasz link do strony domowej wskazywał na listę postów, zatem zamiast precyzować statyczny URL /, możemy również użyć helpera Handlebars. Końcowy wynik będzie identyczny, ale da to nam więcej elastyczności, ponieważ helper zwróci zawsze prawidłowy URL nawet jeżeli zmienimy ścieżką trasy w routerze.

<header class="navbar">
  <div class="navbar-inner">
    <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
  </div>
</header>

//...
client/views/application/layout.html

Zatwierdź 5-1

Podstawowy routing.

Oczekiwanie na dane

Jeżeli uruchomisz bieżącą wersję aplikacji w środowisku produkcyjnym przez zawołanie meteor deploy (lub uruchomisz instancję dostępną w linku powyżej) zauważysz, że lista przez moment jest pusta, zanim posty pojawią się na ekranie. Dzieje się tak, ponieważ podczas ładowania strony nie ma żadnych postów do wyświetlenia, aż do momentu gdy subskrypcja posts zakończy pobieranie danych z serwera.

O wiele lepszym rozwiązaniem byłoby przedstawienie w sposób wizualny użytkownikowi, że coś się w tym czasie dzieje i że powinien przez chwilę poczekać.

Na szczęście Iron Router umożliwia osiągnięcie tego w łatwy sposób – używając waitOn na subskrypcję:

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { return Meteor.subscribe('posts'); }
});

Router.map(function() {
  this.route('postsList', {path: '/'});
});
lib/router.js

Opiszmy po kolei. Najpierw zmieniliśmy blok Router.configure() aby dostarczyć routerowi nazwę szablonu wyświetlanego podczas ładowania danych (który zaimplementujemy wkrótce).

Następnie dodaliśmy funkcję waitOn, która zwraca subskrypcję posts. Ostatecznie podłączyliśmy się pod wbudowaną funkcję loading. Oznacza to, że router upewni się, że subskrypcja posts zostanie załadowana zanim użytkownik zostanie przekierowany na ścieżkę, którą wybrał.

Zauważ, że skoro definiujemy globalnie funkcję waitOn() na poziomie routera, taka kolej rzeczy będzie miała miejsce tylko przy pierwszym dostępie użytkownika do aplikacji. Kolejnym razem dane będą już załadowane w pamięci przeglądarki i router nie będzie zmuszony na ponowne oczekiwanie.

Ponieważ pozwalamy routerowi na zarządzanie subskrypcjami, możemy ją bezpiecznie usunąć z pliku main.js (który powinien być teraz pusty).

Zwykle dobrym pomysłem jest czekanie na subskrypcje nie tylko ze względu na poprawienie obsługi użytkownika, ale również warto upewnić się, że dane będą zawsze dostępne z poziomu szablonu. Eliminuje to konieczność obsługi sytuacji wyjątkowych, gdy szablon jest renderowany zanim dane są dostępne.

Ostatnim elementem układanki jest ładowanie szablonu. Użyjemy pakietu spin aby utworzyć ładnie wyglądający animowany spinner. Dodaj go za pomocą mrt add spin i utwórz szablon jak poniżej:

<template name="loading">
  {{>spinner}}
</template>
client/views/includes/loading.html

Zauważ, że {{>spinner}}, jest funkcją typu “partial” zawartą w pakiecie spin. Mimo to, że funkcja ta pochodzi z “zewnątrz” aplikacji, możemy ją stosować jak każdy inny dowolny szablon.

Zatwierdź 5-2

Czekanie na subskrypcję post.

Pierwszy rzut oka na reaktywność

Reaktywność jest jednym z głównych fundamentów Meteora. Pomimo tego, że jeszcze nie zajęliśmy się tym tematem, ładowanie naszego szablonu daje pierwsze wrażenie tego konceptu.

Przekierowanie do szablonu ładowania danych, jeżeli dane nie zostały jeszcze załadowane jest bardzo dobrym rozwiązaniem, ale skąd router wie kiedy przekierować użytkownika z powrotem na żądaną stronę po załadowaniu danych?

Na razie nie zastanawiajmy się nad tym. Ale nie martw się, dowiesz się o tym bardzo szybko!

Przekierowanie do konkretnego posta

Teraz po tym, jak dowiedzieliśmy się jak przekierowywać na szablon postsList, zaimplementujmy ścieżkę, która umożliwi wyświetlenie szczegółów pojedynczego posta.

Jest tutaj jeden haczyk: nie możemy kontynuować i definiować osobnych tras dla każdego nowego posta, ponieważ może być ich setki. Potrzebujemy zatem ustawić jedną dynamiczną trasę i sprawić, aby ta trasa wyświetlała żądany post.

Aby rozpocząć, utworzymy nowy szablon, który po prostu renderuje ten sam szablon posta, którego użyliśmy wcześniej w liście postów.

<template name="postPage">
  {{> postItem}}
</template>
client/views/posts/post_page.html

Dodamy później więcej elementów do tego szablonu (np. komentarze), ale na teraz będzie on służył jako szkielet dla naszego {{> postItem}}.

Zamierzamy utworzyć nową nazwaną trasę, tym razem kojarząc ścieżkę URL o formie /posts/<ID> z szablonem postPage:

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id'
  });
});

lib/router.js

Specjalna składnia :_id informuje router o dwóch sprawach: po pierwsze aby skojarzyć jakąkolwiek trasę o formacie /posts/xyz/, gdzie “xyz” może być dowolne. Po drugie, aby wstawił cokolwiek znajdzie w “xyz” do własności _id tablicy parametrów routera (params).

Zwróć uwagę, że używamy _id tylko ze względu na łatwość i wygodę implementacji. Router nie ma możliwości, aby dowiedzieć się że przekazujemy właściwy _id, czy dowolny losowy ciąg znaków.

Przekierowujemy teraz na właściwy szablon, ale ciągle czegoś brakuje: router zna _id posta, który chcemy wyświetlić, ale szablon nie ma o tym żadnego pojęcia. Jak więc rozwiązać ten problem?

Na szczęście router ma wbudowane sprytne rozwiązanie tego problemu: pozwala na określenie kontekstu danych dla szablonu. Możesz skojarzyć kontekst danych z marmoladą w abstrakcyjnym pączku, pączku stworzonym z szablonów i layoutów. Aby uprościć tok rozumowania, są to dane, z których korzysta szablon:

Kontekst danych.
Kontekst danych.

W naszym przypadku otrzymamy prawidłowy kontekst danych przez szukanie posta na postawie _id otrzymanego z URL:

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });
});

lib/router.js

Zatem za każdym razem, gdy użytkownik uzyska dostęp do tej trasy, odpowiedni post zostanie znaleziony i przekazany do szablonu. Pamiętaj, że findOne zwraca pojedynczy post, który jest wynikiem zapytania i że przekazanie id jako parametru funkcji jest skrótem dla {_id: id}.

W ciele funkcji data dla danej trasy, this odpowiada bieżącej trasie i możemy użyć this.params aby uzyskać dostęp do nazwanych części trasy (które oznaczyliśmy za pomocą przedrostka : w naszej ścieżce).

Więcej o kontekstach danych

Możesz kontrolować wartość this w helperach szablonów przez ustawienie kontekstu danych szablonu.

Dzieje się to zwykle pośrednio przy pomocy iteratora {{#each}}, który automatycznie ustawia kontekst danych każdej iteracji do bieżącego elementu będącego wynikiem iteracji.

{{#each widgets}}
  {{> widgetItem}}
{{/each}}

Możemy również jawnie użyć {{#with}}, który oznacza: “użyj powyższy obiekt dla danego szablonu”. Przykładowo możemy napisać:

{{#with myWidget}}
  {{> widgetPage}}
{{/with}}

Okazuje się, że możemy to również osiągnąć jawnie przez przesłanie kontekstu jako parametru do wywołania szablonu. W takim przypadku poprzedni fragment kodu może być przepisany następująco:

{{> widgetPage myWidget}}

Używanie dynamicznego helpera nazwanej trasy

Na końcu musimy się upewnić, że wskazujemy na właściwe miejsce, gdy chcemy linkować pojedynczy post. Moglibyśmy to osiągnąć również przez <a href="/posts/{{_id}}">, ale użycie helpera trasy jest po prostu bardziej solidne.

Nazwaliśmy ścieżkę posta postPage, zatem możemy użyć helpera {{pathFor 'postPage'}}:

<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
    </div>
    <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
  </div>
</template>
client/views/posts/post_item.html

Zatwierdź 5-3

Przekierowanie na pojedyńczą stronę posta.

Ale chwila, skąd dokładnie router wie, skąd wziąć część xyz z /posts/xyz? W końcu nie przekazujemy tu żadnego _id.

Okazuje się, że Iron Router jest na tyle sprytny, że uzyskuje tą informację bez naszej pomocy. Gdy prosimy o użycie trasy postPage router wie, że ta trasa potrzebuje _id (ponieważ zdefiniowaliśmy to w naszej ścieżce).

Zatem router będzie szukał tego _id w najbardziej logicznym dla tego miejscu: w kontekście danych helpera {{pathFor 'postPage'}}, innymi słowy this. I tak się składa, że this odpowiada postowi, który (o dziwo!) posiada własność _id.

Ewentualnie, możesz również jawnie nakazać routerowi miejsce szukania własności _id, przez przekazanie drugiego parametru helperowi (np. {{pathFor 'postPage' someOtherPost}}). Praktycznym tego przykładem byłoby uzyskanie linku do poprzedniego lub następnego posta z listy.

Aby sprawdzić, czy działa to poprawnie, przejdź do listy postów i kliknij na jeden z linków Discuss. Powinieneś zobaczyć coś podobnego do:

Strona z pojedyńczym postem.
Strona z pojedyńczym postem.

HTML5 pushState

Należy zdać sobie sprawę, że powyższe zmiany URL zachodzą przy użyciu HTML5 pushState.

Router przechwytuje kliknięcie na URL, które są wewnętrzne dla serwisu i zapobiega przeglądarce na przejście do zewnętrznego linku dla aplikacji. Zamiast tego uaktualnia tylko potrzebne zmiany stanu aplikacji.

Jeżeli wszystko działa prawidłowo, strona powinna zmieniać się natychmiastowo. Tak naprawdę, czasami dzieje się to zbyt szybko i może być potrzebny sposób na łagodne przejście z jednej strony na drugą. Jest to poza zakresem tego rozdziału, ale również jest to interesujący temat do przemyśleń.

Sesja

Sidebar 5.5

Meteor jest reaktywnym frameworkiem. Oznacza to, że po zmianie danych twoja aplikacja reaguje na zmiany i nie musisz jawnie robić nic dodatkowo.

Widzieliśmy to już w akcji w momencie gdy nasz szablon zmieniał się po zmianie danych i ścieżki URL.

Spojrzymy na to dokładniej w późniejszych rozdziałach, ale teraz chciałbym wprowadzić kilka podstawowych cech reaktywności, które są bardzo przydatne w ogólnych aplikacjach.

Sesja Meteora

Obecnie w Microscope, bieżący stan aplikacji użytkownika jest całkowicie zawarty w URL, który jest odwiedzany (oraz w bazie danych).

W wielu przypadkach potrzebne jest jednak przechowywanie ulotnego stanu aplikacji, który jest istotny tylko dla bieżącej wersji aplikacji użytkownika (na przykład czy dany element jest właśnie pokazywany czy ukryty). Wygodnym sposobem na osiągniecie tego jest użycie Sesji.

Sesja jest globalnym, reaktywnym magazynem danych. Jest globalny w sensie obiektu typu singleton: istnieje jedna sesja i dostęp do niej jest możliwy z dowolnego miejsca. Globalne zmienne są zazwyczaj uważane za coś złego, ale w tym przypadku sesja jest używana jako centralna magistrala komunikacyjna między różnymi częściami aplikacji.

Zmiana stanu Sesji

Sesja jest dostępna wszędzie jako Session. Aby ustawić wartość sesji, możesz wywołać:

 Session.set('pageTitle', 'A different title');
Konsola przeglądarki

Możesz ponownie wczytać dane za pomocą Session.get('mySessionProperty');. Jest to reaktywne źródło danych, co oznacza, że jeżeli zaimplementujesz helper, zobaczysz że dane wyjściowe helpera będą się zmieniały reaktywnie gdy zmienna Session ulegnie zmianie.

Aby to wypróbować, dodaj następujący kod do szablonu layout:

<header class="navbar">
  <div class="navbar-inner">
    <a class="brand" href="{{pathFor 'postsList'}}">{{pageTitle}}</a>
  </div>
</header>
client/views/application/layout.html
Template.layout.helpers({
  pageTitle: function() { return Session.get('pageTitle'); }
});
client/views/application/layout.js

Automatyczne przeładowanie kodu przez Meteora (znane jako “hot code reload” lub w skrócie HCR), zachowuje zmienne Session, zatem powinniśmy teraz zobaczyć napis “A different title” wyświetlony w pasku nawigacji. Jeżeli tak się nie stało, wpisz ponownie poprzednią komendę Session.set().

Co więcej, jeżeli ponownie zmienimy wartość zmiennej (znowu w konsoli przeglądarki), zobaczymy znowu inny tytuł:

 Session.set('pageTitle', 'A brand new title');
Konsola przeglądarki

Sesja jest globalnie dostępna, zatem zmiany te mogą być przeprowadzane w dowolnym miejscu aplikacji. Daje to wiele możliwości, ale może także wprowadzić w pułapkę, jeżeli jest zbyt często używane.

Identyczne zmiany

Jeżeli zmodyfikujesz zmienną Session za pomocą Session.set(), ale ustawisz daną zmienną na tą samą wartość, Meteor będzie na tyle sprytny, żeby przerwać łańcuch reaktywności i uniknąć niepotrzebnych wywołań metod.

Wprowadzenie do Autorun

Zaznajomiliśmy się z przykładem reaktywnego źródła danych i obserwowaliśmy je w akcji w środku helpera szablonu. Ale mimo to, że niektóre konteksty Meteora (takie jak helpery szablonów) są z natury reaktywne, większość kodu Meteora jest nadal zwykłym niereaktywnym kodem JavaScript.

Przypuśćmy, że w kodzie aplikacji mamy poniższy fragment kodu:

helloWorld = function() {
  alert(Session.get('message'));
}

Mimo to, że mamy tu dostęp do zmiennej Session, kontekst w którym została zawołana nie jest reaktywny. Oznacza to, że nie będziemy otrzymywali nowych alertów za każdym razem po zmianie zmiennej.

Tutaj właśnie przychodzi z pomocą Autorun. Jak nazwa wskazuje, kod znajdujący się w środku bloku autorun będzie automatycznie uruchomiony i będzie uruchomiony za każdym razem, gdy reaktywne źródło danych, z którego korzysta, ulegnie zmianie.

Spróbuj napisać w konsoli przeglądarki:

 Deps.autorun( function() { console.log('Value is: ' + Session.get('pageTitle')); } );
Value is: A brand new title
Browser console

Jak mogłeś oczekiwać, blok kodu dostarczany do autorun zostaje uruchomiony raz, wypisując dane na konsolę. Spróbujmy teraz zmienić tytuł:

 Session.set('pageTitle', 'Yet another value');
Value is: Yet another value
Konsola przeglądarki

Magia! Po zmianie wartości sesji autorun wiedziało o tym, żeby ponownie uruchomić kod i wypisać nową wartość na konsolę.

Wracając zatem do naszego poprzedniego przykładu, jeżeli chcemy pokazać nowy alert po każdej zmianie zmiennej Session, wszystko co trzeba zrobić, to wstawić fragment kodu do bloku autorun:

Deps.autorun(function() {
  alert(Session.get('message'));
});

Jak właśnie widzieliśmy, autorun może być bardzo użyteczny w śledzeniu reaktywnych źródeł danych i jawnej reakcji na nie.

Natychmiastowe przeładowanie kodu

Podczas rozwoju Microscope korzystaliśmy z jednej z wielu funkcji Meteora pozwalającej oszczędzić czas: hot code reload (HCR). Za każdym razem o zapisaniu dowolnego pliku kodu źródłowego aplikacji, Meteor rozpoznaje zmiany i automatycznie restartuje serwer informując każdego klienta o konieczności przeładowania strony.

Jest to podobne to automatycznego przeładowania strony z jedną istotną różnicą.

Aby dowiedzieć się, co to za różnica, rozpocznij resetując zmienną Session, którą wcześniej używaliśmy:

 Session.set('pageTitle', 'A brand new title');
 Session.get('pageTitle');
'A brand new title'
Konsola przeglądarki

Jeżeli mielibyśmy ręcznie przeładować stronę, nasze zmienne Session naturalnie byłyby stracone (ponieważ utworzyłoby to nową sesję). Z drugiej strony, jeżeli wyzwolimy hot code reload (na przykład przez zapisanie jednego z plików źródłowych) strona się przeładuje, ale zmienna Session będzie nadal ustawiona. Wypróbuj to teraz!

 Session.get('pageTitle');
'A brand new title'
Konsola przeglądarki

Jeżeli zatem używamy zmiennych sesji do śledzenia tego, co użytkownik właśnie robi, HCR powinien być prawie transparentny dla użytkownika, ponieważ zachowa wartość wszystkich zmiennych sesji. Pozwala to na instalację nowej wersji produkcyjnej naszej aplikacji Meteora mając pewność, że nasi użytkownicy będą w minimalnym stopniu zdezorganizowani.

Rozważ to przez chwilę. Jeżeli możemy trzymać cały stan w URL i sesji, możemy transparetnie zmieniać uruchomiony kod źródłowy każdej aplikacji klienta nie przeszkadzając im zbytnio.

Sprawdźmy co dzieje się, gdy ręcznie odświeżymy stronę:

 Session.get('pageTitle');
null
Konsola przeglądarki

Gdy przeładowaliśmy stronę, straciliśmy sesję. W HCR Meteor zapisuje sesję do lokalnego magazynu danych i wczytuje ją ponownie po przeładowaniu strony. Jednakże to pierwsze zachowanie się aplikacji po ręcznym przeładowaniu strony ma sens: jeżeli użytkownik przeładowuje stronę, to tak jakby ponownie odwiedził ten sam URL i powinien być zresetowany do stanu początkowego, takiego jaki zobaczyłby jakikolwiek użytkownik odwiedzający tą stronę.

Ważne wnioski, które należy z tego wyciągnąć to:

  1. Zawsze zapisuj stan użytkownika w sesji lub URL, tak aby użytkownikom jak najmniej przeszkadzało hot code reload.
  2. Zapisuj jakikolwiek stan, który ma być dzielony między użytkownikami w URL.

Dodawanie użytkowników

6

Do tej pory zdołaliśmy w miarę sensownie utworzyć i wyświetlić trochę statycznych danych początkowych i połączyć wszystko razem w prosty prototyp aplikacji.

Widzieliśmy nawet jak nasz UI reaguje na żywo na zmiany danych i wstawione czy zmienione dane pojawiają się natychmiastowo. Nadal jednak nasza strona jest sparaliżowana przez fakt, że nie możemy wstawiać danych. Faktycznie, nawet nie mamy żadnych użytkowników!

Sprawdźmy jak można to naprawić.

Konta: proste podejście do użytkowników

W większości frameworków webowych dodawanie kont użytkowników jest znajomą przeszkodą. Na pewno trzeba to robić prawie w każdym projekcie, ale nigdy nie jest to tak łatwe jak powinno być. Co więcej, kiedy tylko musisz pracować z OAuth lub jakimkolwiek innym schematem logowania, sprawy szybko się komplikują.

Na szczęście Metor troszczy się o to wszystko. Dzięki temu, że pakiety Meteora mogą dodać kod zarówno po stronie serwera (JavaScript) i klienta (JavaScript, HTML i CSS), możemy dostać system logowania prawie za darmo.

Mógłbyś po prostu użyć wbudowanego w Meteorze UI dla kont użytkowników (za pomocą mrt add accounts-ui), ale ponieważ zbudowaliśmy całą aplikację za pomocą Bootstrap, użyjemy zamiast tego pakiet accounts-ui-bootstrap-dropdown (nie martw się, jedyna różnica to styl UI). Wpisz w linii komend:

$ mrt add accounts-ui-bootstrap-dropdown
$ mrt add accounts-password
Terminal

Te dwie komendy dają nam dostęp do specjalnych szablonów dla kont użytkowników; możemy dołączyć je do naszej strony za pomocą helpera {{> loginButtons}}. Przydatna wskazówka: możesz kontrolować, po której stronie ekranu pokaże się przyciski do logowania używając atrybutu align (na przykład {{> loginButtons align="right"}}).

Dodamy przyciski do naszego nagłówka. Ponieważ nasz nagłówek zaczyna się rozrastać, dajmy mu trochę więcej miejsca we własnym szablonie (wstawimy go do client/views/includes/). Użyjemy także trochę markup'u i klas Bootstrap aby upewnić się, że wszystko ładnie wygląda:

<template name="layout">
  <div class="container">
    {{> header}}
    <div id="main" class="row-fluid">
      {{> yield}}
    </div>
  </div>
</template>
client/views/application/layout.html
<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav pull-right">
          <li>{{> loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/includes/header.html

Teraz, gdy otwieramy naszą aplikację w oknie przeglądarki, widzimy przyciski do logowania w górnym prawym rogu naszej strony.

Meteor's built-in accounts UI
Meteor’s built-in accounts UI

Możemy ich użyć do dodania konta, zalogowania się, zapytania o zmianę hasła i wszystkiego potrzebnego tej prostej stronie dla kont użytkowników opartych na hasłach.

Aby przekazać naszemu systemowi kont, że chcemy aby użytkownik logował się za pomocą nazwy użytkownika, dodajemy po prostu blok konfiguracji Accounts.ui w nowym pliku config.js w folderze client/helpers/:

Accounts.ui.config({
  passwordSignupFields: 'USERNAME_ONLY'
});
client/helpers/config.js

Zatwierdź 6-1

Added accounts and added template to the header

Dodanie pierwszego użytkownika

Spróbuj dodać konto użytkownika: przycisk “Sign in” zmieni się i pokaże twoją nazwę użytkownika. Potwierdza to, że konto użytkownika zostało dla Ciebie utworzone. Ale skąd pochodzą dane konta użytkownika?

Przez dodanie pakietu accounts Meteor utworzył specjalną kolekcję, do której można mieć dostęp przez Meteor.users. Aby ją zobaczyć, otwórz konsolę przeglądarki i wpisz:

 Meteor.users.findOne();
Konsola przeglądarki

Konsola powinna zwrócić obiekt reprezentujący twojego użytkownika; jeżeli przyjrzysz mu się bliżej, zauważysz że jest tam obecna twoja nazwa użytkownika jak i _id, które jest twoim unikatowym identyfikatorem. Zauważ, że możesz również otrzymać akutalnie zalogowanego użytkownika wołająć Meteor.user().

Teraz wyloguj się i ponownie utwórz konto z inną nazwą użytkownika. Meteor.user() powinno zwrócić teraz drugiego użytkownika. Ale poczekaj, zawołajmy:

 Meteor.users.find().count();
1
Konsola przeglądarkiu

Konsola zwraca wartość 1. Czy nie powinna mieć wartości 2? Czy pierwszy użytkownik został usunięty? Jeżeli spróbujesz zalogować się jako pierwszy użytkownik, zobaczysz, że tak się nie stało.

Upewnijmy się i sprawdźmy w kanonicznym magazynie danych, czyli bazie danych Mongo. Zalogujemy się do Mongo (meteor mongo w terminalu) i sprawdzimy:

> db.users.count()
2
Konsola Mongo

Zdecydowanie jest dwóch użytkowników w bazie. Dlaczego więc widzimy jednego w przeglądarce?

Tajemnicza publikacja!

Jeżeli wrócisz w myślach do rozdziału 4, możesz pamiętać, że przez wyłączenie autopublish zablokowaliśmy automatyczne wysyłanie wszystkich danych przez kolekcje z serwera do każdego podłączonego klienta i jego lokalnej wersji kolekcji. Potrzebowaliśmy stworzyć parę, publikację i subskrypcję, aby przesyłać dane przez kanał klient-serwer.

Nie ustawialiśmy jednak żadnej publikacji dla użytkownika. Dlaczego zatem widzimy jakiekolwiek dane użytkownika?

Odpowiedź jest taka, że pakiet accounts automatycznie przeprowadza auto-publish aktualnie zalogowanego użytkownika, bez względu na inne ustawienia. Jeżeli by tego nie robił, użytkownik nie mógłby się nigdy zalogować na stronę!

Pakiet accounts publikuje wyłącznie bieżącego użytkownika. Wyjaśnia to, dlaczego użytkownik nie może widzieć szczegółów kont innych użytkowników.

Publikacja zatem publikuje wyłącznie jeden obiekt użytkownika dla każdego zalogowanego użytkownika (i nie publikuje żadnego, gdy nie jesteś zalogowany).

Co więcej, dokumenty w naszej kolekcji użytkownika nie zawierają tych samych pól po stronie klienta i serwera. W Mongo, użytkownik posiada wiele danych. Aby to zobaczyć, wróc do terminala Mongo i wpisz:

> db.users.findOne()
{
  "createdAt" : 1365649830922,
  "_id" : "kYdBd9hr3fWPGPcii",
  "services" : {
    "password" : {
      "srp" : {
        "identity" : "qyFCnw4MmRbmGyBdN",
        "salt" : "YcBjRa7ArXn5tdCdE",
        "verifier" : "df2c001edadf4e475e703fa8cd093abd4b63afccbca48fad1d2a0986ff2bcfba920d3f122d358c4af0c287f8eaf9690a2c7e376d701ab2fe1acd53a5bc3e843905d5dcaf2f1c47c25bf5dd87764d1f58c8c01e4539872a9765d2b27c700dcdedadf5ac82521467356d3f91dbeaf9848158987c6d359c5423e6b9cabf34fa0b45"
      }
    },
    "resume" : {
      "loginTokens" : [
        {
          "token" : "BMHipQqjfLoPz7gru",
          "when" : 1365649830922
        }
      ]
    }
  },
  "username" : "tmeasday"
}
Konsola Mongo

Z drugiej strony, w przeglądarce obiekt użytkownika o wiele bardziej zredukowany, co możesz sprawdzić przez wpisanie równorzędnej komendy:

 Meteor.users.findOne();
Object {_id: "kYdBd9hr3fWPGPcii", username: "tmeasday"}
Konsola przeglądarki

Powyższy przykład pokazuje jak lokalna kolekcja może być bezpiecznym podzbiorem prawdziwej bazy danych. Zalogowany użytkownik widzi wyłącznie tyle prawdziwego zbioru danych, ile potrzebuje (w tym przypadku do zalogowania). Jest to użyteczny wzorzec do zapamiętania, jak zobaczymy później.

Nie oznacza to, że nie możesz sprawić, aby większa część danych użytkownika była publiczna, jeżeli tylko chcesz. Możesz odnieść się do Dokumentacji Meteora aby sprawdzić jak opcjonalnie publikować więcej pól w kolekcji Meteor.users().

Reaktywność

Sidebar 6.5

Jeżeli kolekcje są jądrem Metora, to reaktywność jest warstwą, która robi to jądro użytecznym.

Kolekcje radykalnie zmieniają sposób w jaki Twoja aplikacja radzi sobie ze zmianą danych. Zamiast ręcznie sprawdzać zmiany danych (np. przez wołanie AJAX) i następnie wstawianie zmian do HTMLa, zmiany danych zamiast tego mogą przyjść w dowolnym momencie i być nieprzerwanie aplikowane do interfejsu użytkownika przez Meteora.

Poświęć chwilę na przemyślenie tego: za kurtyną, Meteor może zmieniać jakąkolwiek część twojego interfejsu użytkownika gdy zależna od niego kolekcja ulega zmianie.

Imperatywnym sposobem na osiągniecie tego byłoby użycie .observe(), funkcji kursora, która wywołuje callbacki gdy dokumenty pasujące do kursora ulegają zmianie. Moglibyśmy wtedy aplikować zmiany w DOM (renderowanym HTML naszej strony) za pomocą tych callbacków. Kod wynikowy wyglądałby mniej więcej tak:

Posts.find().observe({
  added: function(post) {
    // when 'added' callback fires, add HTML element
    $('ul').append('<li id="' + post._id + '">' + post.title + '</li>');
  },
  changed: function(post) {
    // when 'changed' callback fires, modify HTML element's text
    $('ul li#' + post._id).text(post.title);
  },
  removed: function(post) {
    // when 'removed' callback fires, remove HTML element
    $('ul li#' + post._id).remove();
  }
});

Prawdopodobnie zorientowałeś się już, jak szybko zwiększy się poziom skomplikowania takiego kodu. Wyobraź sobie radzenie sobie ze zmianami każdego atrybutu posta i koniecznością zmiany skomplikowanego HTML w elementach <li> posta. Nie wspominając o szczególnych przypadkach, które mogą się pojawić gdy zaczniemy polegać na wielu źródłach informacji, które mogą zmieniać się w czasie rzeczywistym.

Kiedy powinniśmy używać observe()?

Używanie powyższego schematu jest czasami konieczne, szczególnie przy pracy z widgetami ze źródeł trzecich. Na przykład wyobraź sobie, że chcemy dodać lub usuwać znaczniki na mapie w czasie rzeczywistym bazując na danych znajdujących się w Kolekcji (powiedzmy, aby pokazać lokalizacje bieżąco zalogowanych użytkowników).

W takich przypadkach będziesz musiał używać callbacków observe() aby zmusić mapę do “rozmowy” z kolekcją Meteora i wiedzieć jak reagować na zmianę danych. Na przykład, polegalibyśmy na callbackach added i removed aby wołać funkcje API mapy dropPin() czy removePin().

Podejście deklaratywne

Meteor daje nam lepsze rozwiązanie: reaktywność, która jest samym jądrem podejścia deklaratywnego. Bycie deklaratywnym pozwala na zdefiniowanie związku między obiektami raz i poźniej poleganie na tym, że będą automatycznie synchronizowane zamiast ustawiać akcje reagujące na każdą możliwą zmianę.

Jest to bardzo mocny koncept, ponieważ system reagujący w czasie rzeczywistym ma wiele wejść, które mogą zmieniać się w nieoczekiwanych momentach. Przez deklaratywne określenie jak renderujemy bazowane na HTML lub jakiekolwiek inne źródła danych o które się troszczymy, Meteor może przejąć pracę monitorowania źródeł danych i w sposób niewidoczny uaktualniać interfejs użytkownika.

Podsumowując to wszystko, zamiast myśleć o kolejnych callbackach observe, Meteor pozwala nam napisać:

<template name="postsList">
  <ul>
    {{#each posts}}
      <li>{{title}}</li>
    {{/each}}
  </ul>
</template>

I następnie otrzymać listę postów za pomocą:

Template.postsList.helpers({
  posts: function() {
    return Posts.find();
  }
});

Za kulisami Meteor podłącza callbacki observe() za nas przerenderowując odpowiednie sekcje HTML po zmianie reaktywnego źródła danych.

Śledzenie zależności w Meteorze: Komputacje

Podczas gdy Meteor jest reaktywnym frameworkiem, działającym w trybie rzeczywistym, tylko częściowo kod samego Meteora jest reaktywny. Jeżeli byłby w całości reaktywny, cała aplikacja uruchamiała by się ponownie po każdej zmianie danych. Zamiast tego, reaktywność jest ograniczona do konkretnych obszarów kodu i nazywamy te obszary komputacjami.

Innymi słowy, komputacja jest blokiem kodu wykonywanym po każdej zmianie reaktywnego źródła danych. Jeżeli masz reaktywne źródło danych (na przykład zmienną Session) i chciałbyś reaktywnie na nią reagować, potrzebujesz ustawić odpowiednią komputację, która się tym zajmie.

Zauważ, że zwykle nie musisz tego robić jawnie, ponieważ Meteor daje domyślnie osobną specjalną komputację każdemu szablonowi, który renderuje (oznacza to, że kod w helperach szablonów i callbackach jest domyślnie reaktywny).

Każde reaktywne źródło danych śledzi wszystkie komputacje, które go używają, tak aby mogło same dać im znać, gdy jego wartość ulegnie zmianie. Aby to osiągnąć, woła funkcję invalidate() komputacji.

Komputacje są z reguły ustawiane po to, aby przeliczyć zawartość danych podczas weryfikacji i dzieje się tak również w komputacjach szablonu (dodatkowo komputacje szablonu próbują kilku sztuczek, aby bardziej widajnie przerysować stronę). Mimo, że możesz mieć większą kontrolę nad komputacją podczas wykonywania invalidate, to większość czasu będziesz potrzebował domyślnych ustawień.

Ustawianie komputacji

Teraz skoro rozumiemy teorię komputacji, implementacja ich będzie nieproporcjonalnie łatwiejsza niż teoria. Używamy po prostu funkcji Deps.autorun aby umieścić blok kodu w komputacji i uczynić go reaktywnym:

Deps.autorun(function() {
  console.log('There are ' + Posts.find().count() + ' posts');
});

Za kulisami, autorun tworzy komputację i podłącza ją w odpowiednie miejsce, aby była wykonywana za każdym razem, gdy zmienia się źródło danych na którym zależy. Ustawiliśmy właśnie bardzo prostą komputację, która wypisuje w konsoli liczbę postów. Ponieważ Posts.find() jest reaktywnym źródłem danych, zatroszczy się o przekazanie komputacji, aby została wykonana za każdym razem, gdy liczba postów ulegnie zmianie.

> Posts.insert({title: 'New Post'});
There are 4 posts.

Końcowym rezultatem tego jest to, że możemy pisać kod, który używa reaktywnych źródeł danych w bardzo naturalny sposób, wiedząc, że za kulisami system zależności we właściwym momencie zatroszczy się o uruchomienie kodu.

Tworzenie postów

7

Widzieliśmy jak łatwo utworzyć nowy post z konsoli przeglądarki bezpośrednio używając funkcji bazy danych Posts.insert, ale nie możemy oczekiwać, że nasi użytkownicy będą otwierali konsolę przy każdym tworzeniu nowego posta.

W końcu będziemy musieli zbudować pewien interfejs użytkownika, aby pozwolić użytkownikom dodawać nowe posty z poziomu aplikacji.

Budowanie strony Nowy Post

Zaczniemy od zdefiniowania ścieżki dla naszej nowej strony:

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { return Meteor.subscribe('posts'); }
});

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postSubmit', {
    path: '/submit'
  });
});
lib/router.js

Użyjemy funkcji routera data, aby ustawić kontekst danych szablonu postPage. Pamiętaj, że cokolwiek wstawiliśmy do kontekstu danych, będzie dostępne jako this w obrębie helperów szablonu.

Dodawanie linka do nagłówka.

Po zdefiniowaniu ścieżki, możemy dodać link do strony umożliwiającej dodanie posta w nagłówku:

<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav">
          <li><a href="{{pathFor 'postSubmit'}}">New</a></li>
        </ul>
        <ul class="nav pull-right">
          <li>{{loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/includes/header.html

Ustawienie ścieżki oznacza, że jeżeli użytkownik przejdzie do URL /submit, Meteor wyświetli szablon postSubmit. Napiszmy zatem ten szablon:

<template name="postSubmit">
  <form class="main">
    <div class="control-group">
        <label class="control-label" for="url">URL</label>
        <div class="controls">
            <input name="url" type="text" value="" placeholder="Your URL"/>
        </div>
    </div>

    <div class="control-group">
        <label class="control-label" for="title">Title</label>
        <div class="controls">
            <input name="title" type="text" value="" placeholder="Name your post"/>
        </div>
    </div>

    <div class="control-group">
        <label class="control-label" for="message">Message</label>
        <div class="controls">
            <textarea name="message" type="text" value=""/>
        </div>
    </div> 

    <div class="control-group">
        <div class="controls">
            <input type="submit" value="Submit" class="btn btn-primary"/>
        </div>
    </div>
  </form>
</template>

client/views/posts/post_submit.html

Uwaga: jest tu wiele znaczników pochodzących z Bootstrap Twittera. Podczas gdy tylko elementy formularza są istotne, dodatkowe znaczniki pomogą w upiększeniu naszej aplikacji. Powinna wyglądać teraz podobnie do:

Formularz dodawania posta
Formularz dodawania posta

Jest to prosty formularz. Nie musimy się martwić o wykonanie konkretnej akcji, ponieważ będziemy przechwytywali zdarzenie wyślij na forumlarzu i uaktualniali dane za pomocą JavaScript. (Nie ma sensu dostarczać na wszelki wypadek rozwiązania nie-JavaScript, gdy weźmiesz pod uwagę, że aplikacja Meteora jest całkowicie niefunkcjonalna po wyłączeniu obsługi JavaScript).

Tworzenie postów

Połączmy handler zdarzeń (ang. event handler) ze zdarzeniem submit formularza. Lepiej użyć zdarzenie submit, niż click na przycisku, jako że to pokryje wszystkie możliwe sposoby wysyłania postów (jak na przykład wciśnięcie przycisku Enter w polu URL).

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(),
      message: $(e.target).find('[name=message]').val()
    }

    post._id = Posts.insert(post);
    Router.go('postPage', post);
  }
});
client/views/posts/post_submit.js

Zatwierdź 7-1

Dodana strona do dodawania postów podlinkowana w nagłówku…

Powyższa funkcja używa jQuery do parsowania wartości różnych pól formularza i tworzenia nowego obiektu posta z wyników parsowania. Musimy się upewnić, że używamy preventDefault na parametrze event naszego handlera aby upewnić się, że przeglądarka nie będzie kontynuowała i próbowała wysłać formularza.

Ostatecznie, możemy przekierować uzytkownika to strony nowego posta. Funkcja kolekcji insert zwraca wygenerowany id dla obiektu, który został wstawiony do bazy danych, który użyje funkcja Routera go() aby utworzyć docelowy URL.

Końcowym rezultatem jest to, że po wciśnieciu przycisku wyślij przez użytkownika, tworzony jest post i użytkownik jest natychmiastowo zabierany do strony z dyskusją na temat tego nowego posta.

Dodanie kilku środków bezpieczeństwa

Tworzenie postów ma się dobrze, ale nie chcemy na to pozwolić dowolnemu użytkownikowi: chcemy, aby się zalogowali przed dodaniem posta. Oczywiście możemy zacząć od ukrycia formularza wprowadzania nowego posta dla niezalogowanych użytkowników. Mimo to, użytkownik mógłby ukrycie utworzyć post w konsoli przeglądarki bez zalogowania się, a nie możemy sobie na to pozwolić.

Na szczęście bezpieczeństwo danych jest wbudowane w podstawy kolekcji Meteora: jest to po prostu domyślnie wyłączone, gdy tworzysz nowy projekt. Pozwala to na łatwy start i budowanie własnej aplikacji zostawiając nudne sprawy na sam koniec.

Nasza aplikacja już nie potrzebuje tej pomocy, zatem usuńmy pakiet insecure:

$ meteor remove insecure
Terminal

Po wykonaniu tej komendy zauważyszm że formularz posta przestanie działać. Dzieje się to, ponieważ bez pakietu insecurewstawianie danych po stronie klienta przestaje być dozwolone. Musimy dodać Metorowi wyraźne zasady pozwalające na wstawianie postów przez klienta lub wstawiać posty po stronie serwera.

Zezwolenie na wstawianie postów

Aby rozpocząć, pokażemy jak zezwolić na wstawianie nowych postów po stronie klienta, aby formularz zaczął znowu działać poprawnie. Jak się okaże później, wybierzemy jeszcze inny sposób, ale na teraz poniższy kod wystarczy, aby naprawić działanie aplikacji:

Posts = new Meteor.Collection('posts');

Posts.allow({
  insert: function(userId, doc) {
    // only allow posting if you are logged in
    return !! userId;
  }
});
collections/posts.js

Zatwierdź 7-2

Usunieto insecure i zezwolono na zapisy do kolekcji posts.

Wołamy Posts.allow, które przekazuje Meteorowi “jest to zbiór okoliczności dla których klient ma zezwolenie różnych akcji na kolekcji Posts”. W tym przypadku, mówimy “klient może wstawiać posty pod warunkiem posiadania userId”.

userId użytkownika przeprowadzającego zmianę jest przekazywany do allow i deny (lub zwraca null, jeżeli żaden użytkownik nie jest zalogowany), co jest prawie zawsze pożyteczne. I skoro konta użytkowników są powiązane z głównym modułem Meteora możemy zawsze polegać na prawidłowości userId.

Zdołaliśmy upewnić się, że musisz być zalogowany, aby móc tworzyć posty. Spróbuj się wylogować i utworzyć posta. Powinieneś zobaczyć w konsoli przeglądarki jak poniżej:

Insert failed: Access denied
Insert failed: Access denied

Jednakże, wciąż musimy dać sobie radę z kilkoma problemami:

  • Wylogowani użytkownicy nadal mają dostę do formularza tworzenia posta
  • Post nie jest związany z użytkownikiem w żaden sposób (i nie ma żadnego kodu po stronie serwera, który by to obsługiwał).
  • Można utworzyć wiele postów, które będą wskazywały na ten sam URL.

Naprawmy te problemy.

Zabezpieczanie dostępu do formularza nowego posta

Zacznijmy od zabezpieczenia przed zobaczeniem formularza dla niezalogowanych użytkowników. Zrobimy to na poziomie routera przez zdefiniowanie funkcji podłączonej do ścieżki (ang. hook).

Funkcja taka przechwyca proces przekierowania i potencjalnie może zmienić akcję, którą podejmuje router. Możesz myśleć o tym jako o strażniku, który sprawdza twoje dane przez wpuszczeniem Ciebie do środka (lub odmowie dostępu).

Co potrzebujemy, to sprawdzenie czy użytkownik jest zalogowany, a jeżeli nie jest, wyrenderowanie szablonu accessDenied zamiast oczekiwanego szablonu postSubmit (następnie zatrzymujemy wykonywanie kolejnych funkcji przez router). Zmieńmy zatem router.js tak, jak poniżej:

Router.configure({
  layoutTemplate: 'layout'
});

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postSubmit', {
    path: '/submit'
  });
});

var requireLogin = function() {
  if (! Meteor.user()) {
    this.render('accessDenied');
    this.stop();
  }
}

Router.before(requireLogin, {only: 'postSubmit'});
lib/router.js

Tworzymy również szablon dla strony z odmową dostępu:

<template name="accessDenied">
  <div class="alert alert-error">Nie masz tu dostępu! Proszę się zalogować.</div>
</template>
client/views/includes/access_denied.html

Zatwierdź 7-3

Odmowa dostępu do strony nowych postów dla niezalogowanyc…

Jeżeli skierujesz się teraz do http://localhost:3000/submit/ nie będąc zalogowanym, powinieneś zobaczyć:

Szablon odmowy dostępu
Szablon odmowy dostępu

Pozytywnym aspektem funkcji podpiętych pod router jest to, że są reaktywne. Oznacza to, że możemy działać deklaratywnie i nie musimy martwić się o callbacki czy cokolwiek podobnego, gdy użytkownik jest zalogowany. Gdy status zalogowania się użytkownika ulega zmianie, szablony strony routera natychmiastowo zmienia się z accessDenied na postSubmit bez konieczności pisania dodatkowego kodu.

Zaloguj się, następnie spróbuj odświeżyć stronę. Możesz czasami zobaczyć krótkie mignięcie szablonu odmowy dostępu przed pojawieniem się strony pozwalającej na dodanie posta. Przyczyna tego jest taka, że Meteor rozpoczyna renderowania tak szybko, jak to jest tylko możliwe, zanim skontaktuje się z serwerem i sprawdzi, czy bieżący użytkownik (zapisany w lokalnej bazie danych) nawet istnieje.

Aby uniknąć tego problemu (który jest częstą klasą problemów, z którymi będziesz musiał się zmierzyć poznając subtelności związanych z opóźnieniem przy komunikacji klient-serwer), wyświetlimy po prostu na ekranie szablon ładowania na krótką chwilę podczas sprawdzania, czy użytkownik ma dostęp.

W końcu w tym momencie nie wiemy czy użytkownik ma prawidłowe dane do zalogowania i nie możemy pokazać ani accessDenied ani postSubmit aż tego nie sprawdzimy.

Modyfikujemy zatem naszą funkcję podpiętą do routera aby używała szablonu do ładowania podczas gdy Meteor.logginIn() zwraca true:

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postSubmit', {
    path: '/submit'
  });
});

var requireLogin = function() {
  if (! Meteor.user()) {
    if (Meteor.loggingIn())
      this.render(this.loadingTemplate);
    else
      this.render('accessDenied');

    this.stop();
  }
}

Router.before(requireLogin, {only: 'postSubmit'});
lib/router.js

Zatwierdź 7-4

Show a loading screen while waiting to login.

Ukrywanie linka

Najłatwiejszym sposobem aby zapobiec przypadkowy dostęp użytkownikom do tej strony jest ukryć link. Osiągniemy to całkiem łatwo przez:

<ul class="nav">
  {{#if currentUser}}<li><a href="{{pathFor 'postSubmit'}}">Dodaj post</a></li>{{/if}}
</ul>
client/views/includes/header.html

Zatwierdź 7-5

Pokazuj link Dodaj post jeżeli uzytkownik jest zalogowany.

Helper currentUser jest dostarczony przez pakiet accounts i jest odpowiednikiem Meteor.user() dostarczanego przez Handlebars. Ponieważ jest reaktywny, link pojawi się lub nie w zależności od tego, czy się wylogujesz, czy zalogujesz do aplikacji.

Serwerowe metody Meteora: Większy stopień bezpieczeństwa i abstrakcji

Zdołaliśmy zabezpieczyć dostęp do strony nowych postów dla niezalogowanych użytkowników, i odmówić takim użytkownim możliwość dodawania postów nawet jeżeli oszukują i używają konsoli przeglądarki. Jest jednak jeszcze kilka rzeczy, o które musimy się zatroszczyć:

  • Dodanie znacznika czasu do posta.
  • Upewnienie się, że ten sam URL nie można dodać więcej, niż jeden raz.
  • Dodanie szczegółów o autorze posta (ID, nazwa użytkownika, itd.).

Możesz sobie pomyśleć, że wszystko to możemy osiągnąć w funkcji obsługującej zdarzenia submit. Gdybyśmy to zrobili, napotkalibyśmy nową grupę problemów:

  • Jeżeli chodzi o znacznik czasu, musielibyśmy polegać na prawidłowo ustawionym czasie po stronie klienta, co nie zawsze nastąpi
  • Klient nie będzie wiedział o wszystkich URL, które zostały opublikowane na stronie. Będą jedynie wiedzieli o postach, które bieżąco widzą (później dokładnie opiszemy jak to działa), więc nie ma pewnego sposobu na wymuszenie unikalnych adresów URL po stronie klienta.
  • Ostatecznie, chociaż teoretycznie możemy dodawać szczegóły użytkownika po stronie klienta, nie wymuszalibyśmy ich dokładności i zgodności z prawdą i prowadziłoby to do nadużyć związanych z używaniem konsoli przeglądarki.

Z powodów wymienionych powyżej lepiej jest utrzymywać proste funkcje obsługujące zdarzenia i jeżeli robimy coś więcej niż tylko podstawowa wstawianie lub modyfikacje kolekcji, używać Metod Meteora (metod przez duże ’M’).

Metoda Meteora jest funkcją serwerową wykonywaną po stronie serwera, ale wywoływaną po stronie klienta. Nie jest to dla nas całkowicie obce – w szczególności funkcje kolekcji insert, update i remove są wszystkie Metodami. Sprawdźmy jak zaimplementować własną.

Wróćmy do post_submit.js. Zamiast wstawiania bezpośrednio do kolekcji Posts, zawołamy Metodę o nazwie post:

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(),
      message: $(e.target).find('[name=message]').val()
    }

    Meteor.call('post', post, function(error, id) {
      if (error)
        return alert(error.reason);

      Router.go('postPage', {_id: id});
    });
  }
});
client/views/posts/post_submit.js

Funkcja Meteor.call woła Metodę o nazwie określonej w pierwszym parametrze funkcji. Możesz dodać parametry do wywołania funkcji (w tym przypadku obiekt post skonstruowany na podstawie formularza) i ostatecznie dodać callback, który zostanie uruchomiony po zakończeniu Metody. Tutaj po prostu ostrzegamy użytkownika czy wystąpił jakiś problem lub przekierowujemy go nowoutworzonej strony dyskusji konkretnego posta.

Następnie zdefiniujemy Metodą w pliku collections/posts.js. Usuniemy blok allow() z posts.js, ponieważ Metoda Meteora i tak go ominie. Pamiętaj, że Metody są wykonywane po stronie serwera, więc Meteor uważa, że można im ufać.

Posts = new Meteor.Collection('posts');

Meteor.methods({
  post: function(postAttributes) {
    var user = Meteor.user(),
      postWithSameLink = Posts.findOne({url: postAttributes.url});

    // ensure the user is logged in
    if (!user)
      throw new Meteor.Error(401, "You need to login to post new stories");

    // ensure the post has a title
    if (!postAttributes.title)
      throw new Meteor.Error(422, 'Please fill in a headline');

    // check that there are no previous posts with the same link
    if (postAttributes.url && postWithSameLink) {
      throw new Meteor.Error(302, 
        'This link has already been posted', 
        postWithSameLink._id);
    }

    // pick out the whitelisted keys
    var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
      userId: user._id, 
      author: user.username, 
      submitted: new Date().getTime()
    });

    var postId = Posts.insert(post);

    return postId;
  }
});
collections/posts.js

Zatwierdź 7-6

Uzycie Metody przy dodawaniu posta.

Metoda ta jest trochę skomplikowana, ale mamy nadzieje że możesz nadążyć.

Na początkek definiujemy zmienną user i sprawdzamy, czy post z tym samym linkiem już nie istnieje. Następnie sprawdzamy, czy użytkownik jest zalogowany, rzucając błąd (który końcowo będzie wyświetlony w przeglądarce), jeżeli nie jest. Przeprowadzamy także prostą walidację obiektu posta, aby upewnić się, że post zawiera tytuł.

Następnie, jeżeli istnieje już post z tym samym URL, rzucamy błąd 302 (który oznacza przekierowanie), przekazaując użytkownikowi, że powinien sprawdzić poprzednio tworzony post.

Klasa Error Meteora ma trzy parametry. Pierwszy, (error) będzie numerycznym kodem 302, drugi (reason) zawiera wyjaśnienie zrozumiałe dla człowieka i trzeci (details) może być jakąkolwiek użyteczną informacją.

W naszym przypadku, użyjemy trzeci argument aby przekazać ID posta, który właśnie znaleźliśmy. Uwaga: użyjemy tego później aby przekierować użytkownika do jeszcze nieistniejącego posta.

Jeżeli wszystkie kroki są sprawdzone i przechodzą poprawnie, bierzemy pola, które chcemy wstawić (upewniając się, że użytkownik wołający tą Metodę w konsoli przeglądarki nie może dodać fałszywych danych do bazy) i dodajemy pewne informacje o uzytkowniku dodającym posta – jak również bieżący czas – do posta.

Ostatecznie, wstawiamy post do bazy danych i zwracamy nowy id użytkownikowi.

Sortowanie postów

Teraz skoro mamy dostęp do czasu wstawienia wszystkich postów, ma sens upewnienie się, że są posortowane ze względu na ten atrybut. Aby to uczynić, możemy użyć operatora Mongo sort, który oczekuje obiektu składającego się z kluczy po których posortować i znaku oznaczającego czy sortowanie ma być wykonane rosnąco czy malejąco.

Template.postsList.helpers({
  posts: function() {
    return Posts.find({}, {sort: {submitted: -1}});
  }
});
client/views/posts/posts_list.js

Zatwierdź 7-7

Sortowanie postów po znaczniku czasu.

Zabrało to trochę pracy, ale wreszcie mamy interfejs użytkownika, który pozwala bezpiecznie wstawiać nowe dane do naszej aplikacji!

Jakakolwiek aplikacja, która pozwala użytkownikom na tworzenie nowych treści musi dać im także sposób na ich późniejszą edycję lub usuwania. Poświęcony jest temu rozdział Edytowanie Postów.

Kompensacja lagów

Sidebar 7.5

W ostatnim rozdziale wprowadziliśmy nowy koncept w świecie Metora: Metody,

Bez kompensacji lagów
Bez kompensacji lagów

Metoda w Meteorze umożliwia uruchomienie serii komand na serwerze w uporządkowany sposób. W naszym przykładzie, użytliśmy Metody ponieważ chcieliśmy upewnić się, że nowe posty zostaną otagowane imieniem autora i jego id oraz bieżącym czasem serwera.

Gdyby jednak Meteor uruchamiał Metody w sposób najbardziej podstawowy, mielibyśmy problem. Rozważ następującą sekwencję zdarzeń (uwaga: znaczniki czasu są losowymi wartościami, wybranymi wyłącznie dla celów demonstracyjnych):

  • +0ms: Użytkownik klika na przycisk wyślij i przeglądarka wywołuje Metodę po stronie serwera.
  • +200ms: Serwer wprowadza zmiany w bazie danych Mongo
  • +500ms: Klient odbiera powyższe zmiany i uaktualnia interfejs użytkownika aby je odzwierciedlić.

Jeżeli tak działałby Meteor, występowałby bardzo krótki lag (przerwa) między wykonywaniem powyższych akcji i zobaczeniem wyników na ekranie (opóźnienie byłoby mniej lub bardziej widoczne w zależności od odległości do serwera). Nie możemy tak funkcjonować w nowoczesnej aplikacji webowej!

Kompensacja Lagów

Z kompensacją lagów
Z kompensacją lagów

Aby obejść ten problem, Meteor wprowadza koncept Kompensacji lagów. Gdy zdefiniowaliśmy metodę posts, umieściliśmy ją w pliku znajdującym się w folderze collections/. Oznacza to, że jest dostępna zarówno dla serwera oraz klienta – oraz to, że będzie uruchomiona na obu w tym samym czasie!

Gdy wywołujesz metodę serwerową, klient wysyła wywołanie do serwera i jednocześnie symuluje wykonanie akcji metody na kolekcjach znajdujących się u klienta. Nasza sekwencja zdarzeń wygląda teraz następująco:

  • +0ms: Użytkownik klika na przycisk wyślij i przeglądarka wywołuje metodę po stronie serwera
  • +0ms: Klient symuluje akcję wywołania metody na kolekcji klienta i zmienia UI odzwierciedlając zmiany
  • +200ms: Serwer wprowadza zmiany w bazie danych Mongo
  • +500ms: Klient otrzymuje zmiany od serwera i cofa symulowane zmiany, zmieniając je zmianami ze strony serwera (które ogólnie są takie same). UI zmienia się odzwierciedlając nowe zmiany.

W wyniku tego użytkownik natychmiastowo widzi zmiany. Gdy odpowiedź serwera jest zwracana po kilku chwilach, może, ale nie musi być widoczna zmiana podczas transferu autentycznych dokumentów. Można się z tego nauczyć tego, że powinniśmy upewnić się, że symulujemy prawdziwe dokumenty tak bardzo jak to możliwe.

Obserwacja kompensacji lagów

Możemy dodać małą zmianę do wywołania metody post aby zobaczyć to w trakcie działania. Aby tego dokonać, napiszemy w pewnym sensie zaawansowaną implementację kodu używając pakietu npm futures, aby opóźnić wstawianie obiektów w ciele metody po stronie serwera.

Sprawdzimy zmienną isSimulation aby zapytać Meteora czy metoda jest aktualnie wykonywana jako stub. Stub jest symulacją Metody, którą Meteor wykonuje równolegle po stronie klienta, podczas gdy “prawdziwa” metoda jest uruchomiona na serwerze.

Zapytamy zatem Metora czy kod jest wykonywany po stronie klienta. W przypadku odpowiedzi twierdzącej, dodamy słowo (client) do tytułu posta. W przypadku odpowiedzi przeczącej dodamy słowo (server):

Meteor.methods({
  post: function(postAttributes) {
    // […]

    // pick out the whitelisted keys
    var post = _.extend(_.pick(postAttributes, 'url', 'message'), {
      title: postAttributes.title + (this.isSimulation ? '(client)' : '(server)'),
      userId: user._id, 
      author: user.username, 
      submitted: new Date().getTime()
    });

    // wait for 5 seconds
    if (! this.isSimulation) {
      var Future = Npm.require('fibers/future');
      var future = new Future();
      Meteor.setTimeout(function() {
        future.return();
      }, 5 * 1000);
      future.wait();
    }

    var postId = Posts.insert(post);

    return postId;
  }
});
collections/posts.js

Uwaga: gdybyś się zastanawiał, this w this.isSimulation jest obiektem wywołanej metody który dostarcza dostęp do wielu przydatnych zmiennych.

Dokładny sposób działania Futures jest poza zakresem tej książki, wspomnimy tylko, że nakazaliśmy, aby Meteor poczekał 5 sekund przed wstawieniem danych do kolekcji po stronie serwera.

Dodamy także przekierowanie wysłania posta bezpośrednio do listy postów:

Template.postSubmit.events({
  'submit form': function(event) {
    event.preventDefault();

    var post = {
      url: $(event.target).find('[name=url]').val(),
      title: $(event.target).find('[name=title]').val(),
      message: $(event.target).find('[name=message]').val()
    }

    Meteor.call('post', post, function(error, id) {
      if (error)
        return alert(error.reason);
    });
    Router.go('postsList');
  }
});
client/views/posts/post_submit.js

Zatwierdź 7-5-1

Pokazanie porządku pojawiania się postów za pomocą sleep.

Jeżeli utworzymy teraz posta, wyraźnie zobaczymy kompensację lagów w czasie działania. Na początku tytuł posta ma przyrostek (client) (pierwszy post z listy, podlinkowany do GitHub):

Post zapisany po stronie klienta
Post zapisany po stronie klienta

Następnie pięć sekund później, jest on zamieniany prawdziwą zawartością dokumentu przysłaną przez serwer:

Post po aktualizacji ze strony serwera
Post po aktualizacji ze strony serwera

Metody do pracy z kolekcją po stronie klienta

Widząc to możesz pomyśleć, że metody wykonywane po stronie serwera są skomplikowane, ale tak naprawdę mogą być całkiem proste. Widzieliśmy już trzy proste motedy: metody zmiany kolekcji, insert, update i remove.

Gdy definujesz kolekcję po stronie serwera o nazwie posts, definiujesz pośrednio trzy metody: posts/insert, posts/update i posts/delete. Innymi słowy, gdy wołasz Posts.insert() na kolekcji po stronie klienta, tworzysz metodę która jest kompensowana serwerem i która wykonuje dwie rzeczy:

  1. Sprawdza, czy można wykonać zmianę przez zawołanie callbacków allow i deny (nie jest to wykonywane w przypadku symulacji)
  2. Przeprowadza prawdziwą zmianę w bazie danych.

Metody do pracy z kolekcją po stronie serwera

Jeżeli nadążasz, mogłeś zdać sobie sprawę, że nasza metoda post podczas wstawiania postów wywołuje inną metodę (posts/insert). Jak to działa?

Gdy uruchamiana jest symulacja (wersja metody po stronie klienta), wykonujemy symulację insert (zatem wprowadzamy dane do koelekcji po stronie klienta), ale nie wołamy prawdziwej metody insert po stronie serwera, ponieważ oczekujemy, że wykona to serwerowa wersja metody post.

Co za tym idzie, gdy serwerowa metoda post woła insert, nie ma potrzeby o martwienie się o symulację i wprowadzenie danych przebiega płynnie.

Edytowanie postów

8

Teraz skoro możemy tworzyć posty, następnym krokiem będzie umożliwienie edycji i usuwanie postóœ. Kod UI który to umożliwi jest releatywnie prosty i jest to dobry moment na podjęcie tematu zarządzania praw użytkowników.

Zacznijmy od podłączenia routera. Dodamy ścieżkę, która umożliwi dostęp do strony modyfikacji posta i ustawi jej kontekst danych:

Router.configure({
  layoutTemplate: 'layout'
});

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postEdit', {
    path: '/posts/:_id/edit',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postSubmit', {
    path: '/submit'
  });
});

var requireLogin = function() {
  if (! Meteor.user()) {
    if (Meteor.loggingIn())
      this.render('loading')
    else
      this.render('accessDenied');

    this.stop();
  }
}

Router.before(requireLogin, {only: 'postSubmit'});
lib/router.js

Szablon edytowania posta

Możemy teraz skoncetrować się na szablonie. Nasz szablon postEdit będzie miał całkiem standardową formę:

<template name="postEdit">
  <form class="main">
    <div class="control-group">
        <label class="control-label" for="url">URL</label>
        <div class="controls">
            <input name="url" type="text" value="{{url}}" placeholder="Your URL"/>
        </div>
    </div>

    <div class="control-group">
        <label class="control-label" for="title">Title</label>
        <div class="controls">
            <input name="title" type="text" value="{{title}}" placeholder="Name your post"/>
        </div>
    </div>

    <div class="control-group">
        <div class="controls">
            <input type="submit" value="Submit" class="btn btn-primary submit"/>
        </div>
    </div>
    <hr/>
    <div class="control-group">
        <div class="controls">
            <a class="btn btn-danger delete" href="#">Delete post</a>
        </div>
    </div>
  </form>
</template>
client/views/posts/post_edit.html

A poniżej znajdziesz manager post_edit.js który z nim współgra:

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');
    }
  }
});
client/views/posts/post_edit.js

Jak do tej pory większość kodu powinna się Tobie wydać znajoma. Na początku mamy helper szablonu, który pobiera bieżący post z bazy danych i przekazuje go do szablonu.

Następnie mamy dwo callbacki zdarzeń szablonu: jeden dla zdarzenia submit formularza, drugi dla usunięcia zdarzenia ‘click’ dla linka.

Callback usuwający zdarzenie click jest bardzo prosty: omija domyślne zdarzenie click i pyta o potwierdzenie. Jeżeli je dostanie, korzysta z post ID obecnego posta otrzymanego z kontekstu danych szablonu, usuwa go i ostatecznie przekierowuje użytkownika do strony domowej.

Callback uaktualniający dane 'update’ jest nieco obszerniejszy, ale niewiele bardziej skomplikowany. Po ominięciu domyślnego zdarzenia i otrzymaniu bieżącego posta, otrzymujemy nowe dane formularza ze strony i zapisujemy je w obiekcie postProperties.

Następnie przekazujemy ten obiekt to metody Meteora Collection.update() i używamy callbacka który wyświetla błąd gdy uaktualnienie się nie powiedzie lub wysyła użytkownika z powrotem na stronę posta, gdy uaktualnienie przebiegnie pomyślnie.

Dodawanie linków

Powinniśmy również edytować linki do naszych postów, aby użytkownicy mieli dostęp do strony edytowania posta:

<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">Discuss</a>
  </div>
</template>
client/views/posts/post_item.html

Oczywiście nie chcemy pokazywać linku do edytowania posta osobom postronnym. Pomaga w tym helper ownPost:

Template.postItem.helpers({
  ownPost: function() {
    return this.userId == Meteor.userId();
  },
  domain: function() {
    var a = document.createElement('a');
    a.href = this.url;
    return a.hostname;
  }
});
client/views/posts/post_item.js
Formularz edytowania posta.
Formularz edytowania posta.

Zatwierdź 8-1

Dodany formularz edytowania posta.

Nasz formularz edytowania posta wygląda dobrze, ale właściwie nie możemy niczego teraz edytować. Co się dzieje?

Ustawianie praw dostępu

Ponieważ poprzednio usunęliśmy pakiet insecure, wszystkie zmiany po stronie klienta są obecnie odrzucane.

Aby to naprawić, ustawimy zasady praw dostępu. Najpierw utwórz nowy plik permissions.js w folderze lib. Załaduje to logikę ustawiania praw dostępu jako pierwszą (i będzie dostępna w obu środowiskach, po stronie klienta i serwera).

// sprawdz czy userId jest wlascicielem dokumentu
ownsDocument = function(userId, doc) {
  return doc && doc.userId === userId;
}
lib/permissions.js

W rozdziale Tworzenie postów pozbyliśmy się metod allow(), ponieważ wstawialiśmy nowe posty wyłącznie za pomocą metody serwerowej (która i tak omija allow()).

Teraz jednak edytujemy i usuwamy posty po stronie klienta, wróćmy zatem do pliku posts.js i dodajmy blok allow():

Posts = new Meteor.Collection('posts');

Posts.allow({
  update: ownsDocument,
  remove: ownsDocument
});

Meteor.methods({
  ...
collections/posts.js

Zatwierdź 8-2

Dodanie podstawowych praw dostępu do sprawdzenia właścici…

Ograniczanie edycji

Fakt, że możesz edytować własne posty nie oznacza, że powinieneś edytować każde pole. Na przykład, nie chcemy aby użytkownicy mogli tworzyć posta i następnie przypisać go innej osobie.

Używamy callbacka Meteora deny() aby upewnić się, że użytkownicy mogę edytować wyłącznie niektóre pola:

Posts = new Meteor.Collection('posts');

Posts.allow({
  update: ownsDocument,
  remove: ownsDocument
});

Posts.deny({
  update: function(userId, post, fieldNames) {
    // may only edit the following two fields:
    return (_.without(fieldNames, 'url', 'title').length > 0);
  }
});
collections/posts.js

Zatwierdź 8-3

Pozwól na zmianę tylko niektórych pól posta.

Używamy tablicy fieldNames, która zawiera listę zmienianych pól i używamy metody without() Underscore, aby zwrócić tablicę, która zawiera częściową tablicę zawierającą pola, te które nie są polami url czy title.

Jeżeli wszystko przebiegnie prawidłowo, ta tablica powinna być pusta i jej długość powinna być równa 0. Jeżeli ktoś stara się dokonać czegoś nieoczekiwanego, długość tablicy będzie równa 1 lub więcej i callback zwrócie true (odrzucając w ten sposób uaktualnienie).

Zawołania metod vs manipulacja danych po stronie klienta

Używamy metody post aby utworzyć nowe posty, natomiast do edycji i usuwania ich wołamy update i remove bezpośrednio po stronie klienta i ograniczamy dostęp przez allow i deny.

Kiedy odpowiednie jest używanie każdego z nich?

Gdy sytuacja jest relatywnie jasna i można konkretnie wyrazić zasady za pomocą allow i deny, zwykle prościej jest wykonywać je po stronie klienta.

Bezpośrednia manipulacja bazy danych ze strony klienta tworzy wyobrażenie natychmiastowej zmiany i służy lepszemu postrzeganiu aplikacji przez użytkownika, tak długo, jak pamiętasz o prawidłowym obchodzeniu się z przypadkami błędów (tj. wtedy gdy serwer zwraca informację, że zmiana się nie powiodła).

Jednakże, gdy masz potrzebę wykonania czynności, które powinny być poza kontrolą użytkownika (tak jak ustawianie znacznika czasu posta czy przypisywanie go do konkretnego użytkownika), lepiej jest użyć Metodę po stronie serwera.

Metody serwerowe są także odpowiednie w kliku innych przypadkach:

  • Gdy musisz znać lub zwracać wartości za pomocą callbacka raczej niż czekać na synchronizację i propagację danych przez serwer.
  • Dla funkcji obciążających bazę danych, dla których jest zbyt kosztowne przesyłanie całej kolekcji do klienta.
  • Do sumowania lub zbierania danych (np. zliczanie, obliczanie średniej lub sumy).

Callbacki allow i deny

Sidebar 8.5

System bezpieczeństwa Meteora pozwala na kontrolę modyfikacji bazy danych bez konieczności definiowania metod za każdym razem, gdy chcemy przeprowadzić zmianę.

Ponieważ potrzebowaliśmy wykonywać dodatkowe czynności, takie jak dekorowanie posta dodatkowymi polami i reakcję na opublikowanie posta pod konkretnym URL, użycie szczególnej metody post podczas tworzenia posta miało duży sens.

Z drugiej strony, nie potrzebowaliśmy tak naprawdę tworzyć nowych metod do uaktualniania i usuwania postów. Potrzebowaliśmy tylko sprawdzić czy użytkownik miał prawa do wykonywania tych akcji, a było to łatwe przy wykorzystaniu callbacków allow i deny`.

Używanie tych callbacków pozwala na bardziej deklaratywne modyfikowanie bazy danych i mówienie o tym jakie zmiany można przeprowadzić. Fakt integracji z systemem kont użytkowników jest dodatkowym bonusem.

Wiele callbacków

Możemy zdefiniować tyle callbacków allow, ile tylko potrzeba. Potrzebujemy, aby przynajmniej jeden z nich zwracał wartość true dla bieżącej zmiany. Zatem gdy Posts.insert jest wołany w przeglądarce (nieważne, czy z kodu aplikacji po stronie klienta, czy z konsoli przeglądarki), serwer z kolei zawoła cokolwiek jest zezwolone-insert sprawdza wzystkie po kolei, aż znajdzie jeden, który zwróci true. Jeżeli nie znajdzie żadnego, nie pozwoli na wprowadzenie danych i zwróci klientowi błąd 403.

Podobnie możemy zdefiniować jeden lub więcej callbacków deny. Jeżeli jakikolwiek z tych callbacków zwróci wartość true, zmiana będzie odwołana i zostanie zwrócony błąd 403. Logika tego jest taka, że dla udanego insert zostanie zawołany jeden lub więcej callbacków insert oraz kazdy callback deny.

Uwaga: n/e oznacza Not Executed (niewykonane)
Uwaga: n/e oznacza Not Executed (niewykonane)

Innymi słowy, Meteor przesuwa listę callbacków zaczynając najpierw od deny, następnie przechodzi do allow i woła każdy po kolei, aż do momentu gdy jeden z nich zwróci true.

Praktycznym przykładem tego wzorca jest posiadanie dwóch callbacków allow(), jeden sprawdza, czy post należy do konkretnego użytkownika, a drugi sprawdza, czy użytkownik ma prawa administratora. Jeżeli bieżącuy użytkownik jest administratorem, zapewnia to możliwość edytowania dowolnego posta, ponieważ przynajmniej jeden z tych callbacków zwróci true.

Kompensacja lagów

Pamiętaj, że metody wprowadzające zmiany w bazie (takie jak .update()) mają kompensację lagów, tak jak każda inna metoda serwerowa. Zatem jeżeli przykładowo próbujesz usunąć posta, który nie należy do Ciebie przez konsolę przeglądarki, zobaczysz chwilowe zniknięcie posta w momencie gdy lokalna kolekcja traci dokument, ale post pojawi się ponownie w mgnieniu oka, ponieważ serwer informuje, że w rzeczywistości dokument nie został usunięty.

Oczywiście to zachowanie nie jest problemem gdy jest uruchomione z konsoli przeglądarki (wszakże jeżeli użytkownik próbuje modyfikować dane z poziomu konsoli, nie jest to tak naprawdę Twoim problem co dzieje się w ich przeglądarce). Jednak, musisz się upewnić, czy nie dzieje się to z poziomu interfejsu użytkownika. Na przykład, musisz się boleśnie upewnić, że nie pokazujesz przycisków pozwalających na usunięcie posta użytkownikom, którzy nie są upoważnieni do jego usuwania.

Szczęśliwie ponieważ dzielisz kod przechowujący uprawnienia pomiędzy klientem i serwerem (na przykład możesz napisać funkcję biblioteczną canDeletePost(user, post) i wstawić ją we współdzielonym folderze /lib), nie nastręcza to konieczności pisania dużej ilości kodu.

Zasady dostępu po stronie serwera

Pamiętaj, że system ustalania praw stosuje się wyłącznie do zmian w bazie danych inicjowanych po stronie klienta. Meteor zakłada, że po stronie serwera wszystkie operacje są dozwolone.

Oznacza to, że jeżeli miałbyś napisać metodę serwerową deletePost, która mogłaby być wołana przez klienta, każdy mógłby usunąć dowolny post. Prawdopodobnie nie chcesz tego, chyba że sprawdziłeś już prawa użytkownika dla tej metody.

Używanie funkcji deny (odmawianie dostępu) jako callbacka

Ostatecznie możesz użyć deny jako callbacka “onX” (jeżeli coś zajdzie). Na przykład, możesz osiągnąć dodanie znacznika czasu modyfikacji posta lastModified za pomocą następującego kodu:

Posts.deny({
  update: function(userId, doc, fields, modifier) {
    doc.lastModified = +(new Date());
    return false;
  },
  transform: null
});

Ponieważ callbacki deny są uruchamiane dla każdej udanej modyfikacji za pomocą update, wiemy, że ten callback będzie uruchomiony i jest w stanie zmienić dokument w uporządkowany sposób.

Trzeba przyznać, że ten sposób jest trochę hackowaniem systemu, więc możesz rozważyć dokonywanie zmian za pomocą metod po stronie serwera. Mimo wszystko, wiedza ta jest przydatna i można mieć nadzieję, że w przyszłości będzie dostępny callback beforeUpdate.

Błędy

9

Używanie standardowego mechanizmu przeglądarki, czyli okna dialogowego alert() aby ostrzegać użytkownika o problemie z dodaniem danych wydaje się nieco niezadowalające i z pewnością nie tworzy świetnego interfejsu użytkownika. Możemy to udoskonalić.

Zamiast tego rozwiązania, zbudujemy wszechstronny mechanizm raportowania błędów, który będzie wykonywał lepiej zadanie przekazywania błędów bez zatrzymywania pracy użytkownika.

Wprowadzenie do lokalnych kolekcji

Zaimplementujemy prosty system, który śledzi, które błędy użytkownik zobaczył i wyświetla nowe błędy w odpowienim miejscu “migoczących wiadomości” na stronie. Ten wzorzec UX jest użyteczny, gdy chcemy poinformować użytkownika, że coś się stało bez przerywania ich pracy.

Utworzymy coś podobnego do migoczących powiadomień znanych z aplikakcji Ruby on Rails, ale jest bardziej subtelne, bo zaimplementowane po stronie klienta i rozpoznające kiedy użytkownik zobaczył wiadomość.

Aby rozpocząć, utworzymy kolekcję, w której będziemy zapamiętywać błędy. Pod warunkiem, że błędy są przeznaczone tylko do bieżącej sesji i nie muszą być w żaden sposób zapisywane, zrobimy coś nowego i stworzymy lokalną kolekcję. Oznacza to, że kolekcja Errors będzie istniała tylko w przeglądarce po stronie klienta i nie będzie starała się synchronizować z serwerem.

Aby tego dokonać, stworzymy komunikat błędu w pliku znajdującym się tylko po stronie klienta, z nazwą kolekcji ustawioną na null. Tworzymy funkcję throwError która po prostu wstawia błąd w nowo utworzoną lokalną kolekcję:

// Local (client-only) collection
Errors = new Meteor.Collection(null);
client/helpers/errors.js

Teraz gdy kolekcja została utworzona, możemy dodać funkcję throwError, którą będziemy wołali aby dodać do niej błędy. Nie musimy się martwić o allow czy deny czy cokolwiek podobnego, ponieważ jest to kolekcja lokalna, która nie będzie zapisywana w bazie danych Mongo.

throwError = function(message) {
  Errors.insert({message: message})
}
client/helpers/errors.js

Zaletą używania lokalnej kolekcji do przechowywania błędów jest to, że jak wszystkie kolekcje, jest reaktywna – oznacza to, że możemy wyświetlać błędy w ten sam sposób, co jakiekolwiek inne dane z kolekcji.

Wyświetlanie błędów

Zamierzamy wyświetlać błędy na samej górze strony:

<template name="layout">
  <div class="container">
    {{> header}}
    {{> errors}}
    <div id="main" class="row-fluid">
      {{yield}}
    </div>
  </div>
</template>
client/views/application/layout.html

Utwórzmy teraz szablony errors i error w errors.html:

<template name="errors">
  <div class="errors row-fluid">
    {{#each errors}}
      {{> error}}
    {{/each}}
  </div>
</template>

<template name="error">
  <div class="alert alert-error">
    <button type="button" class="close" data-dismiss="alert">&times;</button>
    {{message}}
  </div>
</template>
client/views/includes/errors.html

Bliźniacze szablony

Zauważysz, że wrzucamy dwa szablony do jednego pliku. Do tej pory staraliśmy się stosować zasadę “jeden plik, jeden szablon”, ale jeżeli chodzi o Meteora, wrzucenie wszystkich szablonów do jednego pliku działa równie dobrze (chociaż stworzyłoby to bardzo zagmatwany main.html !)

W tym przypadku, ponieważ oba szablony błędów są relatywnie krótkie, zrobimy wyjątek i wrzucimy je do tego samego pliku aby uporządkować repozytorium.

Potrzebujemy tylko zintegrować nagłówek szablony i można zaczynać!

Template.errors.helpers({
  errors: function() {
    return Errors.find();
  }
});
client/views/includes/errors.js

Zatwierdź 9-1

Podstawowe raportowanie błędów.

Tworzenie błędów

wiemy jak wyświetlać błędy, ale potrzebujemy utworzyć kilka zanim będzie można cokolwiek zobaczyć. Błędy są najczęściej uaktywniane przez użytkowników wproadzających nowe dane, zatem będziemy sprawdzali błędy w callbacku tworzenia postów i wyświetlać wiadomość dla jakichkolwiek błędów, które powstają.

Dodatkowo, jeżeli otrzymamy błąd 302 (który oznacza, że post o tym samym URL już istnieje), przekierujemy użytkownika to bieżącego posta. Otrzymamy _id bieżącego posta z error.details (pamiętaj, że przekazaliśmy _id posta jako trzeci parametr do details naszej klasy Errors w rozdziale 7).

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(),
      message: $(e.target).find('[name=message]').val()
    }

    Meteor.call('post', post, function(error, id) {
      if (error) {
        // display the error to the user
        throwError(error.reason);

        if (error.error === 302)
          Router.go('postPage', {_id: error.details})
      } else {
        Router.go('postPage', {_id: id});
      }
    });
  }
});
client/views/posts/post_submit.js

Zatwierdź 9-2

Używanie raportowania błędów.

Sprawdź to: spróbuj stworzyć posta i przejść do URL http://meteor.com. Ponieważ ten URL jest już dołączony do posta w danych początkowych (fixtures), powinieneś zobaczyć:

Wyzwalanie błędu
Wyzwalanie błędu

Usuwanie błędów

Możliwe, że kliknąłeś przycisk ‘zamknij’ po zobaczeniu błędu. Jeżeli to zrobiłeś, błąd powinien zniknąć i pojawić się zaraz po tym, jak wczytasz kolejną stronę do przeglądarki. O co chodzi?

Przycisk 'zamknij’ wyzwala kod JavaScript w Boostrap Twittera: nie ma nic wspólnego z Meteorem! Co się dzieje: Bootstrap usuwa element <div> z DOM zawierający błąd, ale nie z kolekcji Meteora. Oznacza to, że błąd pojawi się ponownie po ponownym przerenderowaniu strony.

Z tego względu jeżeli nie chcesz powstrzymać nieustającego pojawiania się błędu za każdym razem i wprawić użytkownika w obłęd, zaimplementujmy rozwiązanie umożliwiające usuwanie błędów także z kolekcji.

Na początek zmienimy funkcję throwError aby wziąć pod uwagę właściwość seen (ang. widziany). Będzie to przydatne później do śledzenia faktu, czy błąd został już widziany przez użytkownika.

Gdy jest to gotowe, możemy zaimplementować funkcję clearErrors, która czyści te widziane błędy:

// Local (client-only) collection
Errors = new Meteor.Collection(null);

throwError = function(message) {
  Errors.insert({message: message, seen: false})
}

clearErrors = function() {
  Errors.remove({seen: true});
}
client/helpers/errors.js

Następnie, wyczyścimy błędy w routerze aby nawigacja do następnej strony spowoduje usunięcie tych błędów na zawsze:

// ...

Router.before(requireLogin, {only: 'postSubmit'})
Router.before(function() { clearErrors() });
lib/router.js

Aby funkcja clearErrors() poprawnie wykonała swoją pracę, błędy muszą być zaznaczane jako widziane. Aby przebiegało to prawidłowo, musimy zatroszczyć się o obsługę jednego przypadku wyjątkowego: gdy wyrzucamy błąd i przekierowujemy użytkownika do innego miejsca (tak jak przy próbie utworzenia posta ze zduplikowanym linkiem), przekierowanie jest natychmiastowe. Oznacza to, że użytkownik nigdy nie dostanie szansy na zobaczenie błędu zanim zostanie on usunięty.

Jest to miejsce, w którym przyda się zmienna seen (widziany). Musimy się upewnić, że jest ustawiana na true wyłącznie wtedy, gdy użytkownik zobaczył błąd.

Aby tego dokonać, użyjemy funkcji Meteor.defer(). Funkcja ta przekazuje Meteorowi, aby uruchomić callback “zaraz po tym” co dzieje się w tej chwili. Jeżeli pomoże to zrozumieć, możesz pomyśleć, że defer() to jak przekazanie przeglądarce aby poczekać 1 milisekundę przed kontynuowaniem pracy.

Osiągneliśmy to, że Meteor ustawia seen na true 1 milisekundę po renderowaniu szablonu template. Ale czy pamiętasz jak wspomnieliśmy, że przekierowanie dzieje się natychmiastowo? Oznacza to, że mechanizm przekierowania zostanie uruchomiony przed callbackiem defer, który nigdy nie będzie miał możliwości na wykonanie.

Jest to dokładnie to, co chcieliśmy osiągnąć: jeżeli nie został wykonany, nasz błąd nie zostanie zaznaczony jako widziany, co oznacza, że nie będzie usunięty, co również oznacza, że pojawi się na stronie na którą użytkownik został przekierowany, dokładnie tak, jak chcieliśmy!

Template.errors.helpers({
  errors: function() {
    return Errors.find();
  }
});

Template.error.rendered = function() {
  var error = this.data;
  Meteor.defer(function() {
    Errors.update(error._id, {$set: {seen: true}});
  });
};
client/views/includes/errors.js

Zatwierdź 9-3

Monitorowanie które błędy były wyświetlone i czyszczenie …

Callback rendered jest wywołany ponownie po renderowaniu szablonu w przeglądarce. W środku callback'a this odnosi się do bieżącej instancji szablonu, a this.data pozwala na dostęp do danych aktualnie renderowanego obiektu (w naszym przypadku, komunikatu błędu).

Świetnie! Dużo pracy było wykonane, aby zaimplementować coś, czego użytkownicy miejmy nadzieję nigdy nie zobaczą!

Callback rendered

Callback rendered szablonu jest wywoływany za każdym razem po renderowaniu szablonu w przeglądarce. Zawiera to oczywiście pierwsze renderowanie, gdy pojawia się na ekranie, ale warto zapamiętać, że callback będzie także wywoływany za każdym razem, gdy szablon jest renderowany ponownie tj. za każdym razem, gdy jego dane ulegną zmianie.

Callback rendered jest ogolnie wywoływany przynajmniej dwukrotnie: za pierwszym razem, gdy aplikakcja zostaje załadowana i za drugim razem gdy dane kolekcji zostają wczytane. Powinieneś być zatem ostrożny przy wstawianiu kodu, który nie powinien zostać uruchomiony dwukrotnie (np. alert, czy kod śledzący zdarzenia na stronie).

Publikacja pakietu na meteorite

Sidebar 9.5

Napisaliśmy kod obsługi błędów umożliwiając jego wielokrotne wykorzystanie. Dlaczego więc nie spakować go i nie stworzyć z niego inteligentnego pakietu i nie podzielić się z nim z resztą społeczności Meteor'a?

Najpierw musimy stworzyć strukturę plików dla naszego pakietu, w którym będzie rezydować. Umieszczamy nasz pakiet w katalogu packages/errors/. Tworzy to niestandardowy pakiet, który jest automatycznie przygotowany do użycia. (Mogłeś zauważyć, że Meteorite instaluje pakiety poprzez tworzenie linków symbolicznych w katalogu packages/).

Następnie tworzymy w tym katalogu plik package.js. Plik ten informuje Meteor'a o tym jak pakiet powinien być używany oraz dostarcza informacji o zmiennych, które eksportuje.

Package.describe({
  summary: "Szablon do wyświetlania użytkownikowi błędów aplikacji."
});

Package.on_use(function (api, where) {
  api.use(['minimongo', 'mongo-livedata', 'templating'], 'client');

  api.add_files(['errors.js', 'errors_list.html', 'errors_list.js'], 'client');

  if (api.export) 
    api.export('Errors');
});
packages/errors/package.js

Dodajmy trzy pliki do nowego pakietu. Możemy je skopiować z Microscope bez większych zmian z wyjątkiem konieczności zmiany przestrzeni nazw i bardziej uporządkowanego API:

Errors = {
  // Kolekcja lokalna (tylko po stronie klienta)
  collection: new Meteor.Collection(null),

  throw: function(message) {
    Errors.collection.insert({message: message, seen: false})
  },
  clearSeen: function() {
    Errors.collection.remove({seen: true});
  }
};

packages/errors/errors.js
<template name="meteorErrors">
  {{#each errors}}
    {{> meteorError}}
  {{/each}}
</template>

<template name="meteorError">
  <div class="alert alert-error">
    <button type="button" class="close" data-dismiss="alert">&times;</button>
    {{message}}
  </div>
</template>
packages/errors/errors_list.html
Template.meteorErrors.helpers({
  errors: function() {
    return Errors.collection.find();
  }
});

Template.meteorError.rendered = function() {
  var error = this.data;
  Meteor.defer(function() {
    Errors.collection.update(error._id, {$set: {seen: true}});
  });
};
packages/errors/errors_list.js

Testowanie pakietu z Microscope

Będziemy teraz testowali pakiet lokalnie z Microscope aby upewnić się, że zmiany w kodzie działają prawidłowo. Aby zlinkować pakiet z projektem, wykonujemy polecenie meteor add errors. Następnie musimy usunąć istniejące pliki, które zostały zastąpione przez nowy pakiet:

$ rm client/helpers/errors.js
$ rm client/views/includes/errors.html
$ rm client/views/includes/errors.js
usuwanie starych plików z konsoli

Kolejną rzeczą, którą należy zrobić, są małe zmiany konieczne do użycia prawidłowego API:

Router.before(function() { Errors.clearSeen(); });
lib/router.js
  {{> header}}
  {{> meteorErrors}}
client/views/application/layout.html
Meteor.call('post', post, function(error, id) {
  if (error) {
    // wyświetl błąd użytkownikowi
    Errors.throw(error.reason);

client/views/posts/post_submit.js
Posts.update(currentPostId, {$set: postProperties}, function(error) {
  if (error) {
    // wyświetl błąd użytkownikowi
    Errors.throw(error.reason);
client/views/posts/post_edit.js

Zatwierdź 9-5-1

Utworzona podstawowa wersja pakietu i zlinkowanie go z ap…

Po zastosowaniu powyższych zmian, powinniśmy zaobserwować aplikację działającą w ten sam sposób, co przed wprowadzeniem pakietu.

Pisanie testów

Pierwszym krokiem podczas tworzenia pakietu jest testowanie go z aplikacją, a kolejnym jest napisanie zbioru testów, które odpowiednio testują sposób działania pakietu. Meteor pracuje z Tinytest (wbudowanym testerem pakietów), który ułatwia uruchamianie testów i zapewnia spokojny sen po udostępnieniu pakietu innym użytkownikom.

Stwórzmy plik z testami, który będzie używany przez Tinytest z kolekcją przechowującą błędy:

Tinytest.add("Kolecja Errors funkcjonuje poprawnie.", function(test) {
  test.equal(Errors.collection.find({}).count(), 0);

  Errors.throw('Nowy błąd!');
  test.equal(Errors.collection.find({}).count(), 1);

  Errors.collection.remove({});
});

Tinytest.addAsync("Errors template works", function(test, done) {  
  Errors.throw('A new error!');
  test.equal(Errors.collection.find({seen: false}).count(), 1);

  // renderowanie szablonu
  OnscreenDiv(Spark.render(function() {
    return Template.meteorErrors();
  }));

  // poczekaj kilka milisekund
  Meteor.setTimeout(function() {
    test.equal(Errors.collection.find({seen: false}).count(), 0);
    test.equal(Errors.collection.find({}).count(), 1);
    Errors.clearSeen();

    test.equal(Errors.collection.find({seen: true}).count(), 0);
    done();
  }, 500);
});
packages/errors/errors_tests.js

W powyższych testach sprawdzamy, że podstawowe funkcje Meteor.Errors działają poprawnie i upewniamy się, że kod rendered w szablonie nadal działa.

Nie opiszemy tu w pełni dokładnie jak pisać testy pakietów Meteora (ponieważ API nie jest jeszcze finalne i często ulega zmianom), ale mamy nadzieję, że kod jest zrozumiały.

Aby uruchomić testy w pacakge.js, użyj poniższy kod:

Package.on_test(function(api) {
  api.use('errors', 'client');
  api.use(['tinytest', 'test-helpers'], 'client');  

  api.add_files('errors_tests.js', 'client');
});
packages/errors/package.js

Zatwierdź 9-5-2

Dodanie testów do pakietu.

Nstępnie można uruchomić testy za pomocą:

$ meteor test-packages errors
Terminal
Wszystkie testy przechodzą poprawnie.
Wszystkie testy przechodzą poprawnie.

Publikacja pakietu

Teraz chcemy opublikować pakiet dla wszystkich użytkowników Meteora. Osiągniemy to przez opublikowanie go w Atmoshpere.

Po pierwsze, musimy dodać plik smart.json, aby przekazać ważne szczegóły dotyczące pakietu dla Meteora i Atmosphere:

{
  "name": "errors",
  "description": "Szablon do wyświetlania błędów aplikacji uźytkownikowi.",
  "homepage": "https://github.com/tmeasday/meteor-errors",
  "author": "Tom Coleman <tom@thesnail.org>",
  "version": "0.1.0",
  "git": "https://github.com/tmeasday/meteor-errors.git",
  "packages": {
  }
}
packages/errors/smart.json

Zatwierdź 9-5-3

Dodanie pliku smart.json

Wstawiliśmy powyżej podstawowe metadane, które informują o pakiecie, opisują co robi, jego lokalizację git i początkowy numer wersji. Jeżeli nasz pakiet polegał na innych pakietach dostępnych w Atmosphere, możemy dodać sekcję "packages" aby opisać zależności.

Gdy wszystko to zostało dodane, publikacja pakietu jest łatwa. Musimy utworzyć repozytorium Git, następnie za pomocą git push opublikować kod na zdalnym serwerze i dodać link do niego w smart.json.

Proces, który należy wykonać na GitHub, to utworzenie nowego repozytorium, a następnie podążanie za standardowymi krokami pozwalającymi umieścić kod pakietu w danym repozytorium. Następnie należy użyć polecenie mrt release aby go opublikować:

$ git init
$ git add -A
$ git commit -m "Utworzony Pakiet Errors"
$ git remote add origin https://github.com/tmeasday/meteor-errors.git
$ git push origin master
$ mrt release .
Zrobione!
Terminal (run from within `packages/errors`)

Uwaga: nazwy pakietów muszą być unikatowe. Jeżeli utworzysz nazwę pakietu, który już istnieje nastąpi konflikt. W przyszłości Atmosphere będzie miało własną przestrzeń nazw dla każdego użytkownika, zatem weź po uwagę, że może się to zmienić.

Kolejna uwaga: Musisz się zalogować do http://atmosphere.meteor.com/accounts i stworzyć nowego użytkownika z hasłem, które będziesz wpisywał po wydaniu polecenia mrt release.

Teraz skoro pakiet został opublikowany, możemy usunąć go z projektu i dodać go bezpośrednio za pomocą Meteorite:

$ rm -r packages/errors
$ mrt add errors
Terminal (run from the top level of the app)

Zatwierdź 9-5-4

Usunięto pakiet z aplikacji.

Powinieneś zobaczyć teraz w konsoli pierwsze pobieranie pakietu. Dobra robota!

Komentarze

10

Głównym celem serwisu wiadomości społecznościowych jest utworzenie społeczności użytkowników i będzie ciężko to osiągnąć bez dostarczenia sposobu komunikacji między ludźmi. Zatem dodajmy komentarze w tym rozdziale!

Zaczniemy od utworzenia nowej kolekcji do przechowywania komentarzy i dodanie podstawowych danych początkowych (fixture) do tej kolekcji.

Comments = new Meteor.Collection('comments');
collections/comments.js
// Dane początkowe
if (Posts.find().count() === 0) {
  var now = new Date().getTime();

  // utwórz dwóch użytkowników
  var tomId = Meteor.users.insert({
    profile: { name: 'Tom Coleman' }
  });
  var tom = Meteor.users.findOne(tomId);
  var sachaId = Meteor.users.insert({
    profile: { name: 'Sacha Greif' }
  });
  var sacha = Meteor.users.findOne(sachaId);

  var telescopeId = Posts.insert({
    title: 'Introducing Telescope',
    userId: sacha._id,
    author: sacha.profile.name,
    url: 'http://sachagreif.com/introducing-telescope/',
    submitted: now - 7 * 3600 * 1000
  });

  Comments.insert({
    postId: telescopeId,
    userId: tom._id,
    author: tom.profile.name,
    submitted: now - 5 * 3600 * 1000,
    body: 'Interesting project Sacha, can I get involved?'
  });

  Comments.insert({
    postId: telescopeId,
    userId: sacha._id,
    author: sacha.profile.name,
    submitted: now - 3 * 3600 * 1000,
    body: 'You sure can Tom!'
  });

  Posts.insert({
    title: 'Meteor',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://meteor.com',
    submitted: now - 10 * 3600 * 1000
  });

  Posts.insert({
    title: 'The Meteor Book',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://themeteorbook.com',
    submitted: now - 12 * 3600 * 1000
  });
}
server/fixtures.js

Nie zapomnijmy opublikować i subskrybować nowo utworzoną kolekcję:

Meteor.publish('posts', function() {
  return Posts.find();
});

Meteor.publish('comments', function() {
  return Comments.find();
});
server/publications.js
Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { 
    return [Meteor.subscribe('posts'), Meteor.subscribe('comments')];
  }
});
lib/router.js

Zatwierdź 10-1

Dodana kolekcja komentarzy, publikacja/subskrypcja i dane…

Zauważ, że aby uruchomić powyższy kod ustawiający dane początkowe, będziesz musiał wykonać meteor reset aby wyczyścić bazę danych. Po resecie nie zapomnij utworzyć nowego konta i się do niego zalogować!

Na początek stworzyliśmy kilku (całkowicie wymyślonych) użytowników, wstawiliśmy ich dane do bazy danych i użyliśmy ich id aby później pobrać ich z bazy. Następnie dodaliśmy komentarz każdemu użytkownikowi do pierwszego posta, łącząc komentarz do posta (z postId) i użytkownika (z userId). Dodaliśmy także datę dodania posta i jego treść, jak również zdenormalizowane pole author wskazujące na autora komentarza.

Również zmieniliśmy router, aby obsługiwał zarówno posty jak i komentarze.

Wyświetlanie komentarzy

Dobrze, że teraz wszystkie komentarze mamy w bazie danych, ale również potrzebujemy pokazać je na stronie zawierającą dyskusję. Mam nadzieję, że ten proces jest już tobie znajomy i masz pojęcie o kolejnych krokach:

<template name="postPage">
  {{> postItem}}

  <ul class="comments">
    {{#each comments}}
      {{> comment}}
    {{/each}}
  </ul>
</template>
client/views/posts/post_page.html
Template.postPage.helpers({
  comments: function() {
    return Comments.find({postId: this._id});
  }
});
client/views/posts/post_page.js

Wstawiamy blok {{#each comments}} w szablonie posta, zatem this jest postem w helperze comments. Aby znaleźć odpowiednie komentarze, sprawdzamy te, które są połączone z danym postem przez atrybut postId.

Biorąc po uwagę, że poznaliśmy już Handlebars i funkcje pomocnicze szablonu (helper), renderowanie komentarza nie powinno nastręczać większych kłopotów. Utworzymy nowy katalog comments w views aby zapisać całą informację o komentarzu:

<template name="comment">
  <li>
    <h4>
      <span class="author">{{author}}</span>
      <span class="date">on {{submittedText}}</span>
    </h4>
    <p>{{body}}</p>
  </li>
</template>
client/views/comments/comment.html

Zaimplementujmy szybko helper szablonu, aby wyświetlić naszą datę publikacji submitted w czytelnym formacie (chyba że jesteś jednym z ludzi, którzy płynnie odczytują timestamp UNIX i kody hexadecymalne?)

Template.comment.helpers({
  submittedText: function() {
    return new Date(this.submitted).toString();
  }
});
client/views/comments/comment.js

Następnie, wyświetlimy liczbę komentarzy do każdego posta:

<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
      <p>
        submitted by {{author}},
        <a href="{{pathFor 'postPage'}}">{{commentsCount}} comments</a>
        {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
      </p>
    </div>
    <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
  </div>
</template>
client/views/posts/post_item.html

I dodamy helper commentsCount do naszego managera postItem:

Template.postItem.helpers({
  ownPost: function() {
    return this.userId == Meteor.userId();
  },
  domain: function() {
    var a = document.createElement('a');
    a.href = this.url;
    return a.hostname;
  },
  commentsCount: function() {
    return Comments.find({postId: this._id}).count();
  }
});
client/views/posts/post_item.js

Zatwierdź 10-2

Wyświetlanie komentarzy na `postPage`.

Powinniśmy teraz być w stanie wyświetlić komentarze z danych początkowych i zobaczyć coś podobnego do:

Wyświetlanie komentarzy
Wyświetlanie komentarzy

Wysyłanie komentarzy

Dodajmy teraz możliwość dodawania komentarzy dla użytkowników. Proces, przez który przejdziemy będzie bardzo podobny do tego, w jakim pozwoliliśmy użytkownikom tworzyć nowe posty.

Zaczniemy od dodania przycisku wstawiania pod każdym postem:

<template name="postPage">
  {{> postItem}}

  <ul class="comments">
    {{#each comments}}
      {{> comment}}
    {{/each}}
  </ul>

  {{#if currentUser}}
    {{> commentSubmit}}
  {{else}}
    <p>Komentować mogą wyłącznie zalogowani użytkownicy. Proszę się zalogować.</p>
  {{/if}}
</template>
client/views/posts/post_page.html

I następnie utworzenia formularza szablonu komentarza:

<template name="commentSubmit">
  <form name="comment" class="comment-form">
    <div class="control-group">
        <div class="controls">
            <label for="body">Skomentuj</label>
            <textarea name="body"></textarea>
        </div>
    </div>
    <div class="control-group">
        <div class="controls">
            <button type="submit" class="btn">Dodaj komentarz</button>
        </div>
    </div>
  </form>
</template>
client/views/comments/comment_submit.html
Formularz do dodawania komentarza
Formularz do dodawania komentarza

Aby opublikować komentarz, wołamy metodę comment w managerze commentSubmit, który działa podobnie do managera postSubmit:

Template.commentSubmit.events({
  'submit form': function(e, template) {
    e.preventDefault();

    var $body = $(e.target).find('[name=body]');
    var comment = {
      body: $body.val(),
      postId: template.data._id
    };

    Meteor.call('comment', comment, function(error, commentId) {
      if (error){
        throwError(error.reason);
      } else {
        $body.val('');
      }
    });
  }
});
client/views/comments/comment_submit.js

Zupełnie tak samo, jak zaimplementowaliśmy metodę post po stronie serwera, zaimplementujemy metodę comment, która utworzy komentarz, sprawdzi, że wszystko przebiegło prawidłowo i wstawi nowy komentarz do kolekcji comments.

Comments = new Meteor.Collection('comments');

Meteor.methods({
  comment: function(commentAttributes) {
    var user = Meteor.user();
    var post = Posts.findOne(commentAttributes.postId);
    // ensure the user is logged in
    if (!user)
      throw new Meteor.Error(401, "Musisz się zalogować aby dodawać komentarze");

    if (!commentAttributes.body)
      throw new Meteor.Error(422, 'Proszę wpisać treść komentarza. ');

    if (!post)
      throw new Meteor.Error(422, 'Musisz komentować określony post');

    comment = _.extend(_.pick(commentAttributes, 'postId', 'body'), {
      userId: user._id,
      author: user.username,
      submitted: new Date().getTime()
    });

    return Comments.insert(comment);
  }
});
collections/comments.js

Zatwierdź 10-3

Utworzony formularz do dodawania koentarzy.

Nie dzieje się tutaj nic szczególnego, jest tu proste sprawdzenie, że użytkownik jest zalogowany, komentarz ma treść i że jest połączony z postem.

Kontrola subskrypcji komentarzy

Obecna sytuacja jest taka, że publikujemy wszystkie komentarze wszystkich postów do wszystkich podłączonych klientów. Wydaję się to trochę nieekonomiczne. Używamy w końcu tylko małego podzbioru danych w określonym momencie. Polepszmy zatem publikacje i subskrypcje aby dokładnie kontrolować, które komentarze są publikowane.

Jeżeli pomyślimy o tym więcej, jedynym momentem, w którym potrzebujemy subskrybować publikację comments, jest chwila w której użytkownik wchodzi na stronę konkretnego posta i potrzebujemy wczytać jedynie podzbiór komentarzy do niego.

Pierwszym krokiem będzie zmiana sposobu subskrypcji komentarzy. Aż do teraz, subskrypcje były tworzone na poziomie routera, co oznaczało, że wczytywaliśmy wszystkie dane raz podczas inicjalizacji routera.

Chcemy teraz, żeby nasza subskrypcja zależała od parametru ścieżki i że parametr może się oczywiście zmienić w dowolnym momencie. Z tego względu musimy przenieść kod subskrypcji z poziomu routera do poziomu scieżki.

Ma to jeszcze inne konsekwencje: zamiast wczytywać dane gdy inicjalizujemy aplikację, od teraz będziemy je wczytywali za każdym razem po wejściu na konkretną ścieżkę (inaczej URL). Oznacza to, że będziesz teraz doświadczał doczytywania danych przy przeglądaniu aplikacji. Jest to niemożliwe do uniknięcia, chyba że zamierzasz początkowo wczytać wszystkie dostępne dane.

Oto jak wygląda nowa funkcja waitOn na poziomie ścieżki:

Router.map(function() {

  //...

  this.route('postPage', {
    path: '/posts/:_id',
    waitOn: function() {
      return Meteor.subscribe('comments', this.params._id);
    },
    data: function() { return Posts.findOne(this.params._id); }
  });

  //...

});
lib/router.js

Zauważysz, że przekazujemy this.params._id jako argument do subskrypcji. Użyjmy teraz tej nowej informacji do upewnienia się, że ograniczyliśmy zbiór danych do komentarzy należących do bieżącego posta:

Meteor.publish('posts', function() {
  return Posts.find();
});

Meteor.publish('comments', function(postId) {
  return Comments.find({postId: postId});
});
server/publications.js

Zatwierdź 10-4

Utworzona prosta publikacja/subskrypcja komentarzy.

Jest tylko jeden problem: gdy powrócimy do strony domowej, twierdzi że wszystkie posty mają 0 komentarzy:

Wszystkie komentarze zniknęły!
Wszystkie komentarze zniknęły!

Liczenie komentarzy

Przyczyna takiego stanu rzeczy wyda się wkrótce jasna: w jednym momencie możemy mieć tylko wczytane komentarze jednego posta, zatem gdy wołamy Comments.find({postId: this._id}) w helperze commentsCount w managerze postItem, Meteor nie odnajduje potrzebnych danych do obliczenia liczby komentarzy po stronie klienta.

Najlepszym sposobem na poradzenie sobie z tym, jest zdenormalizowanie liczby komentarzy do posta (nie przejmuj się, jeżeli nie wiesz o co chodzi, następny rozdział tłumaczy to szczegółowo). Zobaczysz, że powoduje to w małym stopniu zwiększenie złożoności kodu, ale wzrost wydajności jaki uzyskujemy przez uniknięcie publikowania wszystkich komentarzy jest tego warty.

Osiągniemy to przez dodanie zmiennej commentsCount do struktury danych post. Aby rozpocząć, uaktualnimy nasze dane początkowe (ang. fixtures) (i użyjemy meteor reset aby je przeładować – nie zapomnij ponownie utworzyć konta użytkownika po dokonaniu tej operacji):

var telescopeId = Posts.insert({
  title: 'Introducing Telescope',
  ..
  commentsCount: 2
});

Posts.insert({
  title: 'Meteor',
  ...
  commentsCount: 0
});

Posts.insert({
  title: 'The Meteor Book',
  ...
  commentsCount: 0
});
server/fixtures.js

Następnie upewnimy się, że wszystkie nowe posty mają 0 komentarzy:

// pick out the whitelisted keys
var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
  userId: user._id, 
  author: user.username, 
  submitted: new Date().getTime(),
  commentsCount: 0
});

var postId = Posts.insert(post);
collections/posts.js

I następnie uaktualniamy odpowiedni licznik commentsCount gdy tworzymy nowy komentarz za pomocą operatora $inc Mongo (który zwiększa pole numeryczne o 1):

// uaktualnienie posta o liczbę komentarzy
Posts.update(comment.postId, {$inc: {commentsCount: 1}});

return Comments.insert(comment);
collections/comments.js

Na koniec, możemy usunąć helper commentsCount z client/views/posts/post_item.js, skoro to pole jest dostępne teraz bezpośrednio w poście.

Zatwierdź 10-5

Zdenormalizowana liczba komentarzy do posta.

Teraz skoro użytkownicy mogą ze sobą rozmawiać, byłoby szkoda, gdyby przegapili nowe komentarze na ich posty. I proszę, następny rozdział właśnie pokaże, jak zaimplementować powiadomienia, aby tego uniknąć!

Denormalizacja

Sidebar 10.5

Denormalizacja danych oznacza przechowywanie danych w formie innej niż “normalna”. Innymi słowy, denormalizacja oznacza posiadanie kilku kopii tych samych danych.

W ostatnim rozdziale, zdenormalizowaliśmy licznik przechowujący liczbę komentarzy do obiektu posta aby zapobiec wczytywaniu za każdym razem wszystkich komentarzy. Dla przypadku modelowania danych jest to zbędne, ponieważ równie dobrze moglibyśmy policzyć zbiór komentarzy w dowolnej chwili, aby dostać tą wartość (pomijając wpływ na wydajność tego rozwiązania).

Denormalizacja często oznacz dodatkową pracę dla developera. W naszym przypadku, za każdym razem gdy dodajemy lub usuwamy komentarz, musimy także pamiętać o uaktualnieniu odpowiedniego posta, aby upewnić się, że pole commentsCount ma prawidłową wartość. Jest to dokładnie przyczyna dlaczego relacyjne bazy danych, takie jak MySQL nie są przychylne takim praktykom.

Jednakże normalne podejście do tego problemu ma także swoje minusy: bez pola commentsCount, potrzebowalibyśmy wysyłać wszystkie komentarze do klienta za każdym razem, tylko po to, aby je policzyć, czyli to co robiliśmy na początku. Denormalizacja pozwala uniknąć tego całkowicie.

Specjalna publikacja

Byłoby również możliwe utworzenie specjalnej publikacji, która wysyła wyłącznie liczniki komenterzy, którymi jesteśmy zainteresowani (tj. liczników komentarzy które aktualnie widzimy, za pomocą zapytań aggregate po stronie serwera).

Jest to jednak warte rozważenia, jeżeli stopień skomplikowania kodu takiej publikacji byłby mniejszy, niż wysiłek włożony w denormalizację…

Oczywiście takie rozważania są specyficzne dla konkretnych zastosowań: jeżeli piszesz kod, gdzie integralność danych jest najważniejsza, unikanie niezgodności danych ma większy priorytet niż strata na wydajności.

Wstawianie dokumentów lub używanie wielu kolekcji

Jeżeli jesteś zaawansowanym użytkownikiem Mongo, mogłeś się zdziwić, że utworzyliśmy drugą kolekcję tylko dla komentarzy: dlaczego po prostu nie wstawiać komentarzy do listy znajdującej się w dokumencie posta?

Okazuje się, że dla wielu narzędzi które oferuje Meteor, pracowuje się lepiej gdy operujemy na poziomie kolekcji. Na przykład:

  1. Helper {{#each}} jest bardzo wydajny, jeżeli chodzi o iterację po kursorze (wyniku collection.find()). Nie jest to prawdą, gdy iteruje po tablicy objektów w środku większego obiektu.
  2. allow i deny operują na poziomie dokumentu, ułatwia to potwierdzenie, że jakiekolwiek modyfikacje pojedyńczych komentarzy są poprawne i obsługa byłaby bardziej skomplikowana na poziomie posta.
  3. DDP operuje na poziomie atrybutów dokumentu najwyższego poziomu – oznacza to, że jeżeli comments byłyby własnością post, za każdym razem, gdy komentarz byłby dodawany do posta, serwer musiałby wysyłać całą uaktualnioną listę komentarzy tego posta do wszystkich podłączonych klientów.
  4. Publikacje i subskrypcje można o wiele łatwiej kontrolować na poziomie dokumentów. Na przykład, jeżeli chcielibyśmy dzielić na strony komentarze do posta, byłoby to trudne do zrealizowania gdyby komentarze nie były umieszczone w odrębnej kolekcji.

Mongo proponuje zagnieżdżanie dokumentów aby zredukować liczbę kosztownych zapytań o pobranie dokumentów. Jednakże nie należy się tym zbytnio przejmować biorąc pod uwagę architekturę Meteora: większość czasu pytamy o dokumenty po stronie klienta, gdzie dostęp do bazy danych jest właściwie darmowy.

Wady denormalizacji

Istnieje dobry argument na to, że nie należy denormalizować danych. Polecamy przeczytać Why You Should Never Use MongoDB autorstwa Sarah Mei, która przedstawia dobry przykład przeciwko denormalizacji.

Powiadomienia

11

Teraz kiedy użykownicy mogą komentować na posty innych użytkowników, byłoby dobrze móc powiadamiać o rozpoczętej konwersacji.

Aby tego dokonać, damy znać właścicielowi posta, że dodano komentarz i dostarczymy mu link, aby mógł go przeczytać.

Jest to funkcjonalność, w której Meteor naprawdę błyszczy: ponieważ wszystko w Meteorze domyślnie dzieje się w czasie rzeczywistym, będziemy wyświetlali te notyfikacje natychmiastowo. Nie chcemy czekać na użytownika, aby odświeżył stronę czy sprawdzał to w jakikolwiek sposób, możemy po prostu pokazać nową notyfikację bez potrzeby pisania żadnego dodatkowego kodu.

Tworzenie powiadomień

Utworzymy powiadomienie gdy ktoś skomentuje Twój post. W przyszłości powiadomienia mogą być rozszerzone, aby wziąć pod uwagę wiele innych scenariuszy, ale teraz wystarczy to aby na bieżąco informować użytkowników o tym, co się dzieje.

Stwórzmy kolekcję Notifications, oraz funkcję createCommentNotification, która wstawi pasujące powiadomienie do jednego z Twoich postów:

Notifications = new Meteor.Collection('notifications');

Notifications.allow({
  update: ownsDocument
});

createCommentNotification = function(comment) {
  var post = Posts.findOne(comment.postId);
  if (comment.userId !== post.userId) {
    Notifications.insert({
      userId: post.userId,
      postId: post._id,
      commentId: comment._id,
      commenterName: comment.author,
      read: false
    });
  }
};
collections/notifications.js

Tak jak posty lub komentarze, kolekcja Notifications będzie współdzielona po stronie klienta i serwera. Ponieważ potrzebujemy uaktualniać powiadomienia po tym, kiedy użytkownik je zobaczył, włączamy również uaktualnienia, jak zwykle upewniając się, że ograniczamy uprawnienia do uaktualnień własnych danych użytkownika.

Utworzyliśmy także prostą funkcję, która sprawdza post który jest aktualnie komentowany i buduje listę użytkowników, którzy mają być powiadomieni i wstawia nową notyfikację.

Już tworzymy komentarze po stronie serwera, zatem możemy rozwinąć funkcję je tworzącą, aby wywoływała naszą funkcję. Zamienimy return Comments.insert(comment); przez comment._id = Comments.insert(comment) aby zaoszczędzić _id nowo utworzonego komentarza w zmiennej, a następnie zawołać naszą funkcję createCommentNotification:

Comments = new Meteor.Collection('comments');

Meteor.methods({
  comment: function(commentAttributes) {

    // [...]

    // utwórz komentarz, zachowaj jego id
    comment._id = Comments.insert(comment);

    // teraz utwórz powiadomienie informujące użytkownika o nowym komentarzu
    createCommentNotification(comment);

    return comment._id;
  }
});
collections/comments.js

Opublikujemy także powiadomienia i utworzymy dla nich subskrypcje po stronie klienta.

// [...]

Meteor.publish('notifications', function() {
  return Notifications.find();
});
server/publications.js
Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { 
    return [Meteor.subscribe('posts'), Meteor.subscribe('notifications')]
  }
});
lib/router.js

Zatwierdź 11-1

Added basic notifications collection.

Wyświetlanie powiadomień

Możemy teraz kontynuować i dodać listę powiadomień do nagłówka.

<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav">
          {{#if currentUser}}
            <li>
              <a href="{{pathFor 'postSubmit'}}">Submit Post</a>
            </li>
            <li class="dropdown">
              {{> notifications}}
            </li>
          {{/if}}
        </ul>
        <ul class="nav pull-right">
          <li>{{loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/includes/header.html

I utworzyć szablony notifications i notification (będą dzieliły wspólny plik notifications.html):

<template name="notifications">
  <a href="#" class="dropdown-toggle" data-toggle="dropdown">
    Notifications
    {{#if notificationCount}}
      <span class="badge badge-inverse">{{notificationCount}}</span>
    {{/if}}
    <b class="caret"></b>
  </a>
  <ul class="notification dropdown-menu">
    {{#if notificationCount}}
      {{#each notifications}}
        {{> notification}}
      {{/each}}
    {{else}}
      <li><span>No Notifications</span></li>
    {{/if}}
  </ul>
</template>

<template name="notification">
  <li>
    <a href="{{notificationPostPath}}">
      <strong>{{commenterName}}</strong> commented on your post
    </a>
  </li>
</template>
client/views/notifications/notifications.html

Jak widać, zaplanowaliśmy, że każde powiadomienie ma zawierać link do posta, do którego dodano komentarz i nazwę użytkownika, który dodał komentarz.

Następnie, musimy się upewnić, że zaznaczyliśmy prawidłową listę powiadomień w naszym managerze i uaktualnić powiadomienie jako czytaj gdy użytkownik kliknie na link, na który wzkazują.

Template.notifications.helpers({
  notifications: function() {
    return Notifications.find({userId: Meteor.userId(), read: false});
  },
  notificationCount: function(){
    return Notifications.find({userId: Meteor.userId(), read: false}).count();
  }
});

Template.notification.helpers({
  notificationPostPath: function() {
    return Router.routes.postPage.path({_id: this.postId});
  }
})

Template.notification.events({
  'click a': function() {
    Notifications.update(this._id, {$set: {read: true}});
  }
})
client/views/notifications/notifications.js

Zatwierdź 11-2

Display notifications in the header.

Możesz myśleć, że powiadomienia nie różnią się zbytnio od błedów i masz rację. Ich struktura jest podobna. Jest jednak jedna duża różnica między nimi: utworzyliśmy odpowiednią synchronizowaną kolekcję miedzy klientem i serwerem. Oznacza to, że nasze powiadomienia są zapamiętywane i tak długo jak używamy tego samego konta, będzie istniało po odświeżeniach przeglądarki i między różnymi urządzeniami, które przeglądają stronę.

Wypróbuj to: otwórz drugą przeglądarkę (powiedzmy Firefox), utwórz nowe konto użytkownika, i skomentuj posta, którego utworzyłeś za pomocą głównego konta (które zostawiłeś otwarte w Chrome). Powinieneś zobaczyć mniej więcej to:

Displaying notifications.
Displaying notifications.

Kontrola dostępu do powiadomień

Powiadomienia działają dobrze. Jest jednak jeden problem: nasze powiadomienia są dostępne dla wszystkich.

Jeżeli masz nadal otwartą drugą przeglądarkę, spróbuj uruchomić następujący kod w konsoli przeglądarki:

 Notifications.find().count();
1
Browser console

Nowy użytkownik (ten, który skomentował) nie powinien otrzymać żadnego powiadomienia. Powiadomienie, które widzą w kolekcji Notifications należy do niego.

Poza potencjalnymi problemami związanymi z zachowaniem prywatności, nie stać nas na to, aby załadować wszystkie powiadomienia w każdej przeglądarce innych użytkowników. Na wystarczająco dużym serwisie, mogłoby to zapełnić dostępną pamięć przeglądarki i spowodować poważne problemy związane ze spadkiem wydajności.

Rozwiązaliśmy ten problem za pomocą publikacji. Możemy użyć publikacji aby bardzo szczegółowo określić, którą część kolekcji chcemy dzielić z każdą przeglądarką.

Aby to osiągnąć, musimy zwrócić inny kursor niż Notifications.find() w naszej publikacji. Mianowicie, chcemy zwrócić kursor, która odpowiada bieżącemu powiadomieniu użytkownika.

Osiągnięcie tego jest proste, ponieważ funkcja publish ma dostęp do _id bieżącego użytkownika dostępną jako this.userId:

Meteor.publish('notifications', function() {
  return Notifications.find({userId: this.userId});
});
server/publications.js

Zatwierdź 11-3

Synchronizowanie wyłącznie tych powiadomień, które są prz…

Teraz, gdy sprawdzimy dwie niezależnie uruchomione przeglądarki, powinniśmy zobaczyć dwie różne kolekcje powiadomień:

 Notifications.find().count();
1
Browser console (user 1)
 Notifications.find().count();
0
Browser console (user 2)

Tak naprawdę, lista publikacji powinna zmieniać się nawet po wylogowaniu i ponownym zalogowaniu się do aplikacji. Dzieje się tak dlatego, ponieważ publikacje po każdej zmianie konta użytkownika są publikowane ponownie.

Nasza aplikacja staje się coraz bardziej funkcjonalna i im więcej użytkowników przyłącza się i zaczyna publikować linki, tym większe ryzyko nieskończonie ładującej się stronie głównej. Zajmiemy się tym w następnym rozdziale przez zaimplementowanie dzielenia dokumentu na strony.

Zaawansowana reaktywność

Sidebar 11.5

Rzadko zachodzi potrzeba napisania samemu kodu do śledzenia zależności, ale z pewnością warto zrozumieć ten mechanizm aby później móc śledzić sposób jego działania.

Wyobraź sobie, że chcemy śledzić liczbę przyjaciół danego użytkownika Facebooka, którzy ‘polubili’ każdy post na Microscope. Przyjmijmy, że już zaimplementowaliśmy logowanie użytkownika za pomocą Facebooka, wywołaliśmy odpowiednie funkcje API i parsowaliśmy odpowienie dane. Mamy asynchroniczną funkcję po stronie klienta, która zwraca liczbę polubień posta, getFacebookLikeCount(user, url, callback).

Ważną sprawą wartą zapamiętania jest, w jakim stopniu taka funkcja jest niereaktywna i w jakim stopniu nie jest czasu rzeczywistego. Funkcja wywołuje żądanie HTTP do Facebooka, odczyta trochę danych i udostępni je aplikacji w asynchronicznym callbacku. Nie wywoła się ponownie sama gdy liczba polubień ulegnie zmianie i nasze UI nie uaktualni się podczas zmiany danych.

Aby to naprawić, możemy zacząć przez ustawienie odpowiedniego czasu setInterval, aby wołać funkcję co kilka sekund.

currentLikeCount = 0;
Meteor.setInterval(function() {
  var postId;
  if (Meteor.user() && postId = Session.get('currentPostId')) {
    getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
      function(err, count) {
        if (!err)
          currentLikeCount = count;
      });
  }
}, 5 * 1000);

Za każdym razem, gdy sprawdzamy zmienną currentLikeCount, możemy oczekiwać na zwrócenie prawidłowej liczby z pięciosekundowym marginesem błędu. Możemy użyć tą zmienną w helperze jak poniżej:

Template.postItem.likeCount = function() {
  return currentLikeCount;
}

Jednak nic nie wywołuje renderowania szablonu po zmianie currentLikeCount. Pomimo tego, że zmienna ta jest pseudo- czasu rzeczywistego, nie jest jeszcze reaktywna i nie może się w odpowiedni sposób komunikować z resztą ekosystemu Meteora.

Śledzenie reaktywności: Obliczenia

Reaktywność Meteora działa za pośrednictwem zależności, struktur danych, które śledzą zbiór komputacji.

Jak widzieliśmy we wcześniejszym pasku bocznym reaktywności, komputacja jest blokiem kodu, który używa reaktywnych danych. W naszym przypadku istnieje komputacja pośrednio utworzona dla szablonu postItem. Każdy helper managera tego szablonu pracuję z tą komputacją.

Możesz myśleć o komputacji, jako o sekcji kodu, która “troszczy się” o reaktywne dane. Gdy dane zmieniają się, to ta komputacja zostaje o tym poinformowana (przez invalidate()) i to ta komputacja decyduje o tym, czy wykonać jakąś akcję.

Przemiana zwykłej zmiennej w funkcję reaktywną

Aby zmienić naszą zmienną currentLikeCount w reaktywne źródło danych, musimy śledziź wszystkie komputacje, które używają jej w zależności. Wymaga to zmiany ze zmiennej na funkcję (która zwróci wartość):

var _currentLikeCount = 0;
var _currentLikeCountListeners = new Deps.Dependency();

currentLikeCount = function() {
  _currentLikeCountListeners.depend();
  return _currentLikeCount;
}

Meteor.setInterval(function() {
  var postId;
  if (Meteor.user() && postId = Session.get('currentPostId')) {
    getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
      function(err, count) {
        if (!err && count !== _currentLikeCount) {
          _currentLikeCount = count;
          _currentLikeCountListeners.changed();
        }
      });
  }
}, 5 * 1000);

Co osiągneliśmy to ustawienie zależności _currentLikeCountListeners, która śledzi wszystkie komputacje w której użyty był currentLikeCount(). Gdy wartość _currentLikeCountListeners ulega zmianie, wołamy funkcję changed() na tej zależności, która unieważnia wszystkie śledzone komputacje.

Wtedy komputacje mogą kontynuować i reagować na zmiany odpowiednio do każdego przypadku. W przypadku komputacji szablonu oznacza to, że szablon się renderuje.

Obliczenie szablonów i kontrola ich renderowania

Główną przyczyną, dla której każdy szablon posiada własną komputację jest kontrola liczby odświeżania, która zachodzi na ekranie.

Gdy wołamy szablon z innego szablonu, druga komputacja zostaje wstawiona w pierwszą. Zatem jeżeli reaktywne dane z wewnętrznego szablonu zmieniają się, ten wewnętrzny szablon jest odświeżony, natomiast zewnętrzny szablon pozostaje nienaruszony. W ten sposób komputacje kontrolują zakres zmian na reaktywne dane.

Meteor wspomaga także trochę podstawowy mechanizm zagnieżdżania szablonów.

Po pierwsze, helper {{#constant}} wyłącza reaktywność. Jakiekolwiek dane, które są zbierane przez helpery szablonu są używane tylko raz. Nawet jeżeli szablon go zawierający jest odświeżony, dane HTML stałego bloku zostają nienaruszone. Umożliwia to wykorzystanie stałych regionów do zarządzania widgetami pochodzących od osób trzecich, które nie spodziewają się odświeżenia DOM pod nimi.

Po drugie, helper {{#isolate}} ustawia nową komputację w środku szablonu. Innymi słowy ma taki sam efekt jak zagnieżdżony szablon.

Zatem jeżeli jeden z reaktywnych źródeł danych będący w obrębie bloku isolate zmieni się, izolowany obszer ulegnie odświeżeniu, ale reszta zawierającego go szablonu nie. Jednakże gdy szablon zewnętrzny się odświeży, odświeży się także izolowany obszar.

Porównianie zależności do Angular

Angular jest reaktywną biblioteką działającą po stronie klienta, rozwijaną przez dobrych ludzi z Google. Zilustrujemy podejście zastosowane w Meteorze do śledzenia zależności w Angular, jako że dwa rozwiązania bardzo różnią się od siebie.

Widzieliśmy, że model Meteora używa bloków kodu nazywanych komputacjami. Komputaje te są śledzone przez specjalne reaktywne źródła danych (funkcje), które troszczą się o unieważnienie ich gdy zachodzi taka potrzeba. Zatem źródło danych jasno informuje wszystkie swoje zależności, gdy potrzebują zawołać invalidate(). Zauważ, że ogólnie dzieje się to, gdy dane uległy zmianie, ale można być także wywołać unieważnienie z innych powodów.

Dodatkowo, pomimo tego, że komputacje są ponownie uruchamiane podczas unieważnienia, można ustawić je tak, aby zachowywały się w dowolny sposób. Daje to wysoki poziom kontroli nad reaktywnością.

W Angular, reaktywność jest zarządzana przez obiekt scope. Można o nim myśleć jako o zwykłym obiekcie JavaScript z kilkoma specjalnymi metodami.

Jeżeli chcesz reaktywnie zależeć na zmiennej w scope, wywołujesz scope.$watch, dostarczając wyrażenia które chcesz monitorować (t.j. na której części scope Ci zależy) i funkcję obserwująca (listener), która ma być wywoływana po każdej zmianie wyrażenia. Możesz zatem otwarcie zdefiniować co zrobić za każdą zmianą wartości wyrażenia.

Wracając do przykładu Facebooka, napisalibyśmy:

$rootScope.$watch('currentLikeCount', function(likeCount) {
  console.log('Current like count is ' + likeCount);
});

Oczywiście tak rzadko, jak ustawia się komputacje w Meteorze, zarówno w Angular nie woła się często $watch ponieważ instrukcje ng-model i {{expressions}} automatycznie ustawiają obserwatorów, którzy troszczą się o odświeżanie po każdej zmianie.

Po zmianie takiej reaktywnej zmiennej, musi być zawołana scope.$apply(). Funkcja ta przechodzi przez każdego obserwatora scope ale wywołuje wyłącznie funkcje obserwujące obserwatorów, których zmieniła się wartość wyrażenia.

Zatem scope.$apply() jest podobne do dependency.changed(), z tym wyjątkiem, że działa z poziomu scope raczej niż daje pełną kontrolę nad tym, które funkcje oberwujące mają być przeliczone. Mówiąc o tym, ten lekki brak całkowitej kontroli daje Angular możliwość bycia sprytnym i wydajnym w sposobie w jakim ustalone zostaje które funkcje obserwujące mają być przeliczone.

Z Angular, kod naszej funkcjigetFacebookLikeCount() wyglądałby mniej więcej tak:

Meteor.setInterval(function() {
  getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
    function(err, count) {
      if (!err) {
        $rootScope.currentLikeCount = count;
        $rootScope.$apply();
      }
    });
}, 5 * 1000);

Trzeba przyznać, że Meteor troszczy się o szczegóły za nas i pozwala na czerpanie pełnych korzyści z reaktywności bez wkładania w to większego wysiłku. Mamy nadzieję, że nauka o tym okaże się pomocna, gdy zechcesz rozwinąć aplikację i pchnąć ją poza granice obecnej implementacji.

Dzielenie dokumentu na strony

12

Sprawy z Microscope mają się świetnie i możemy oczekiwać, że aplikacja stanie się hitem po opublikowaniu jej na świat.

Z tego względu powinniśmy pomyśleć o wpływie jaką ma liczba postów renderowanych na stronie na wydajność serwisu po starcie!

Wcześniej rozmawialiśmy o tym, jak kolekcja po stronie klienta powinna zawierać podzbiór danych obecny na serwerze i nawet udało się nam to osiągnąć dla kolekcji zawierających powiadomienia i komentarze.

Obecnie jednak nadal publikujemy wszystkie posty za jednym zamachem, do wszystkich połączonych użytkowników. Aby rozwiązać ten problem, potrzebujemy podzielić nasze posty na strony.

Dodawanie postów

Po pierwsze, w danych początkowych, załadujmy wystarczającą liczbę postów, tak aby dzielenie na strony miało sens.

// Dane początkowe
if (Posts.find().count() === 0) {

  //...

  Posts.insert({
    title: 'The Meteor Book',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://themeteorbook.com',
    submitted: now - 12 * 3600 * 1000,
    commentsCount: 0
  });

  for (var i = 0; i < 10; i++) {
    Posts.insert({
      title: 'Test post #' + i,
      author: sacha.profile.name,
      userId: sacha._id,
      url: 'http://google.com/?q=test-' + i,
      submitted: now - i * 3600 * 1000,
      commentsCount: 0
    });
  }
}
server/fixtures.js

Po wywołaniu komendy meteor reset powinieneś zobaczyć coś podobnego do:

Wyświetlanie wymyślonych danych .
Wyświetlanie wymyślonych danych .

Zatwierdź 12-1

Dodane wystarczająco postów aby potrzebne było dzielenie …

Nieskończone dzielenie dokumentu na strony.

Zaimplementujemy proste “nieskończone” dzielenie dokumentu na strony. Oznacza to, że wyświetlimy, przyjmijmy 10 postów na ekranie z linkem âpokaż więcejâ umieszczonym pod spodem. Kliknięcie na ten link doda kolejne 10 postów do listy i tak dalej w nieskończoność. Oznacza to, że możemy kontrolować nasz cały system dzielenia na strony przez pojedyńczy parametr zawierający liczbę postów do wyświetlenia na ekranie.

Teraz będziemy musieli znaleźć sposób na przekazanie serwerowi o tym parametrze, tak aby wiedział ile postów wysłać klientowi. Tak się składa, że subskrybujemy już publikację posts w routerze, więc wykorzystamy to i pozwolimy routerowi na kontrolę nad dzieleniem na strony.

Najłatwiejszym sposobem na ustawienie tego jest po prostu umieszczenie parametru oznaczającego granicę postów w ścieżce dającej URL w formie http://localhost:3000/25. Dodatkowym bonusem używania URL w porównaniu do innych metod jest to, że jeżeli wyświetlasz obecnie 25 postów i odświeżysz stronę przez pomyłkę, nadal będziesz widział 25 postów na stronie po przeładowaniu strony.

Aby zrobić to dobrze, musimy zmienić sposób, w jaki subskrybujemy na posty. Tak, jak poprzednio w rozdziale Komentarze, będziemy musieli przenieść nasz kod odpowiedzialny za subskrypcje z poziomu routera na poziom ścieżki routera.

Możliwe, że wyda się to być zbyt dużą liczbą nowych informacji do przyswojenia, ale stanie się to zrozumiałe po przeczytaniu kodu.

Po pierwsze, przestaniemy subskrybować do publikacji posts w bloku Router.configure(). Po prostu usuń Meteor.subscribe('posts') pozostawiając jedynie subskrypcję notifications:

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { 
    return [Meteor.subscribe('notifications')]
  }
});
lib/router.js

Dodamy następnie parametr postsLimit to ścieżki routingu. Dodanie ? po nazwie parametru oznacza, że jest opcjonalny. Tak więc ścieżka nie tylko będzie pasowała do http://localhost:3000/50, ale również do zwykłego http://localhost:3000.

Router.map(function() {
  //...

  this.route('postsList', {
    path: '/:postsLimit?'
  });
});
lib/router.js

Istotne jest zanotowanie, że ścieżka w formie /:parametr? będzie pasowała do każdej możliwej ścieżki. Ponieważ każda trasa będzie parsowana kolejno i sprawdzana czy pasuje do bieżącej ścieżki musimy się upewnić, że ścieżki są ułożone w kolejności od najbardziej specyficznej do najbardziej ogólnej.

Inaczej mówiąc, ścieżki routingu, które mają być dopasowane do najbardziej szczegółowych tras takich jak /posts/:_id powinny być pierwsze, a ścieżka postsList powinna być przesunięta na ostatnią pozycję, ponieważ pasuje w sumie do wszystkiego.

Teraz czas na zmierzenie się z ciężkim problem subskrypcji i znajdowania właściwych danych. Musimy obsłużyć przypadek, gdzie parametr postsLimit nie jest obecny, więc przypiszemy mu wartość domyślną. Użyjemy â5â aby zapewnić dostatecznie dużo miejsca na dzielenie na strony.

Router.map(function() {
  //..

  this.route('postsList', {
    path: '/:postsLimit?',
    waitOn: function() {
      var postsLimit = parseInt(this.params.postsLimit) || 5; 
      return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: postsLimit});
    }
  });
});
lib/router.js

Zauważysz, że przkazujemy teraz obiekt JavaScript ({limit: postsLimit}) jak i nazwę publikacji posts. Ten obiekt będzie służył jako parametr opcje dla Posts.find() po stronie serwera. Przejdźmy zatem do kodu wykonywanego po stronie serwera i zaimplementujmy:

Meteor.publish('posts', function(options) {
  return Posts.find({}, options);
});

Meteor.publish('comments', function(postId) {
  return Comments.find({postId: postId});
});

Meteor.publish('notifications', function() {
  return Notifications.find({userId: this.userId});
});
server/publications.js

Przekazywanie parametrów

Nasz kod publikacji w rzeczywistości przekazuje serwerowi, że może zaufać dowolnemu obiektowi JavaScript wysyłanego przez klienta (w naszym przypadku {limit: postsLimit}) który służy jako opcje dla polecenia find(). Umożliwia to użytkownikom przesłanie wybranych opcji przez konsolę przeglądarki.

W naszym przypadku, jest to stosunkowo nieszkodliwe, ponieważ wszystko, co użytkownik mógłby zrobić, to zmienić kolejność postów lub zmienić liczbę wyświetlanych (co chcemy zrobić na początek).

Nie powinno się jednak używać tego wzorca podczas zapisywania prywatnych danych na niepublikowanych polach, ponieważ użytkownik odpowiednio modyfikując opcję fields mógłby mieć do nich dostęp i powinieneś również unikać używania go dla argumentu selektora find() dla tych samych powodów bezpieczeństwa.

Bardziej bezpieczne byłoby przekazywanie pojedyńczych parametrów zamiast całego obiektu, aby upewnić się że masz pełną kontrolę nad dostępem do danych:

Meteor.publish('posts', function(sort, limit) {
  return Posts.find({}, {sort: sort, limit: limit});
});

Teraz kiedy subskrybujemy posty na poziomie konkretnej ścieżki URL, miałoby sens ustawić kontekst danych w tym samym miejscu. Odejdźmy trochę od poprzedniego schematu i zmieńmy funkcję data w ten sposób, aby zwracała obiekt JavaScript zamiast zwracania kursora. To pozwala na utworzenia nazwanego kontekstu danych, który nazwiemy posts.

Oznacza to, że zamiast pośredniego dostępu jako this w środku szabklonu, nasz kontekst danych będzie dostępny jako posts. Oprócz tej małej zmiany, kod powinien wyglądać znajomo:

Router.map(function() {
  this.route('postsList', {
    path: '/:postsLimit?',
    waitOn: function() {
      var limit = parseInt(this.params.postsLimit) || 5; 
      return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
    },
    data: function() {
      var limit = parseInt(this.params.postsLimit) || 5; 
      return {
        posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
      };
    }
  });

  //..
});
lib/router.js

Teraz skoro ustawiamy kontekst danych na poziomie routera możemy bezpiecznie pozbyć się nagłówka szablonu posts w pliku posts_lists.js. Skoro nazwaliśmy kontekst danych posts (tak samo jak helper), nie podrzebujemy nawet dotykać szablonu postList!

Zbierzmy wszystko razem. Oto jak powinien wyglądać nowy i poprawiony kod router.js:

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { 
    return [Meteor.subscribe('notifications')]
  }
});

Router.map(function() {
  //...

  this.route('postsList', {
    path: '/:postsLimit?',
    waitOn: function() {
      var limit = parseInt(this.params.postsLimit) || 5; 
      return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
    },
    data: function() {
      var limit = parseInt(this.params.postsLimit) || 5; 
      return {
        posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
      };
    }
  });
});
lib/router.js

Zatwierdź 12-2

Zmieniona trasa postsList pozwalająca na ograniczenie lic…

Wypróbujmy nowy system dzielenia postów. Mamy teraz możliwość wyświetlania dowolnej liczby postów na stronie głównej za pomocą zmiany parametru URL. Przykładowo, sþróbuj odwiedzić http://localhost:3000/3. Powinieneś ujrzeć coś podobnego do:

Kontrola liczby postów na stronie głównej.
Kontrola liczby postów na stronie głównej.

Dlaczego nie osobne strony?

Dlaczego używamy nieskończonego dzielenia na strony zamiast pokazywać kolejne strony zawierające po 10 postów każda, jak Google pokazujące wynik zapytania? Robimy to ze względu na reakcje Meteora w czasie rzeczywistym.

Wyobraźmy sobie, że dzielimy naszą kolekcję Posts używając systemu używanego przez Google i jesteśmy na stronie 2, która pokazuje posty od 10 do 20. Co dzieje się, gdy użytkownik usunie nagle pierwsze 10 postów?

Ponieważ nasza aplikacja działa w czasie rzeczywistym, nasz zbiór danych uległby zmianie. Post 10 stałby się postem 9 i wypadł z widoku, a post 11 byłby widoczny, Końcowy rezultat byłby taki, że użytkownik zobaczyłby nagle zmieniające się posty bez widocznej przyczyny!

Nawet jeżeli tolerowalibyśmy tą niedogodność UX, tradycyjne dzielenie dokumentu jest również trudne do zrealizowania z przyczyn technicznych.

Wróćmy do poprzedniego przykładu. Publikujemy posty od 10 do 20 z kolekcji Posts, ale jak znaleźć te posty po stronie klienta? Nie możesz wybrać postów 10 do 20, skoro po stronie klienta jest tylko 10 postów.

Jednym rozwiązaniem byłoby publikowanie 10 postów po stronie serwera i następnie wywoływanie Posts.find() po stronie klienta, aby przechwycić wszystkie publikowane posty.

Działa to, gdy masz tylko jedną subskrypcję. Ale co się stanie jeżeli będziesz miał więcej niż jedną subskrypcję postów, a stanie się to wrótce?

Powiedzmy, że jedna subskrypcja pyta o posty od 10 do 20, a inna o 30 do 40. Masz teraz 20 postów załadowane po stronie klenta, nie mając pojęcia które należą do odpowiedniej subskrypcji.

Ze względu na te wszytkie kwestie, tradycyjne dzielenie na dokumetu na strony nie ma większego sensu przy korzystaniu z Meteora.

Implementacja kontrolera Routera

Mogłeś zauważyć, że powtórzyliśmy dwukrotnie linię var limit = parseInt(this.params.postsLimit) || 5;. Dodatkowo, numer zakodowany numer “5†nie jest idealny. To nie koniec świata, ale ponieważ zawsze warto stosować się do zasady DRY (Don’t Repeat Yourself), sprawdźmy jak możemy zrobić refaktoryzację kodu.

Wprowadźmy nową funkcję Iron Routera, Kontrolerow ścieżki. Kontroler ścieżki umożliwia na zebranie razem funkcjonalności w jeden spójny pakiet, z którego może dziedziczyć jakakolwiek ścieżka. Teraz użyjemy kontrolera do pojedyńczej ścieżki, a w następnym rozdziale dowiesz się jak pomocna będzie ta funkcjonalność.

PostsListController = RouteController.extend({
  template: 'postsList',
  increment: 5, 
  limit: function() { 
    return parseInt(this.params.postsLimit) || this.increment; 
  },
  findOptions: function() {
    return {sort: {submitted: -1}, limit: this.limit()};
  },
  waitOn: function() {
    return Meteor.subscribe('posts', this.findOptions());
  },
  data: function() {
    return {posts: Posts.find({}, this.findOptions())};
  }
});

Router.map(function() {
  //...

  this.route('postsList', {
    path: '/:postsLimit?',
    controller: PostsListController
  });
});
lib/router.js

Przejdźmy przez ten krok. Początkowo tworzymy kontroler przez rozszerzenie RouteController‘a. Następnie używamy właściwości Template jak poprzednio, a następnie nowej właściwości increment.

Następnie definiujemy nową funkcję linit, która zwróci obecny limit i funkcję findOptions która zwróci obiekt z opcjami. Może się wydawać dodatkowym niepotrzebnym krokiem, ale zrobimy z niego później użytek.

Następnie, zdefiniujemy nowe funkcje waitOn i data jak poprzednio, przy czym używają one obecnie funkcji findOptions.

Ostatnią rzeczą do zrobienia jest zmiana ścieżki postsList prowadzącą do naszego nowego kontrolera z własnością controller.

Zatwierdź 12-3

Zrefaktoryzowana ścieżka postsLists do RouteController'a’.

Dodawanie linku 'Pokaż więcej’

Mamy teraz działające dzielenie dokumentu na strony i kod wygląda dobrze. Jest tylko jeden problem: nie ma innego sposobu na używanie tego dzielenia na strony niż ręczna zmiana URL. Zdecydowanie nie polepsza to używania go, więc weźmy się do pracym aby go polepszyć.

To, co chcemy osiągnąć jest wystarczająco proste. Dodamy przycisk wczytaj więcej na dole listy postów, co zwiększy o 5 liczbę postów wyświetlanych po każdym kliknięciu. Więc jeżeli jesteśmy na URL http://localhost:3000/5, kliknięcie na wczytaj więcej powinno przenieść na http://localhost:3000/10. Jeżeli zdołałeś przeczytać do te pory tą książkę, powinieneś dać sobie radę z prostą arytmetyką!

Tak, jak poprzednio, dodamy logikę dzielenia na strony do naszej ścieżki. Pamietasz, gdy jawnie nazwaliśmy kontekst danych zamiast używać anonimowego kursora? Nie ma zasady, która mówiłaby, że funkcja data może przekazywać jedynie kursory, więc użyjemy tej samej techniki aby wygenerować przycisk wczytaj więcej.

PostsListController = RouteController.extend({
  template: 'postsList',
  increment: 5, 
  limit: function() { 
    return parseInt(this.params.postsLimit) || this.increment; 
  },
  findOptions: function() {
    return {sort: {submitted: -1}, limit: this.limit()};
  },
  waitOn: function() {
    return Meteor.subscribe('posts', this.findOptions());
  },
  posts: function() {
    return Posts.find({}, this.findOptions());
  },
  data: function() {
    var hasMore = this.posts().fetch().length === this.limit();
    var nextPath = this.route.path({postsLimit: this.limit() + this.increment});
    return {
      posts: this.posts(),
      nextPath: hasMore ? nextPath : null
    };
  }
});
lib/router.js

Przyjrzyjmy się głębiej na trick wykorzystywany przez routera. Pamiętaj, że ścieżka postList (która dziedziczy z kontrolera PostListController nad którym obecnie pracujemy, bierze parametr postLimit.

Z tego względu, gdy wprowadzamy {postsLimit: this.limit() + this.increment} do this.route.path(), nakazujemy ścieżce postsList zbudować własną ścieżkę używając powyższego obiektu JavaScript jako kontekstu danych.

Innymi słowy, jest to dokładnie to samo, co użycie helpera Handlebars {{pathFor 'postsList'}}, z tym, że zamieniamy pośrednio dostępne this przez własny stworzony kontekst danych.

Wykorzystujemy tą ścieżkę i dodajemy ją do kontekstu danych dla naszego szablonu, ale wyłącznie wtedy, gdy jest więcej postów do wyświetlenia. Sposób, w jaki to robimy, jest trochę podchwytliwy.

Wiemy, e this.limit() zwraca bieżącą liczbę postów, którą chcielibyśmy pokazać, która może być zawarta w bieżącym URL, lub jest wartością domyślną (5), jeżeli URL nie zawiera żadnego parametru.

Z drugiej strony, this.posts() odnosi się do bieżącego kursora, zatem this.posts.fetch().length odnosi się do liczby postów znajdujących się w obrębie danego kursora.

Zatem, jeżeli pytamy o n postów i dostajemy n, będziemy pokazywali przycisk wczytaj więcej. Jeżeli zapytamy o n i dostaniemy mniej niż n, oznacza to, że osiągneliśmy limit i nie powinniśmy wyświetlać tego przycisku.

Niestety nie ma prostych rozwiązań tego problemu, z tego względu zostaniemy na razie z tą niezbyt perfekcyjną implementacją.

Wszystko, co pozostało do zrobienia, to dodanie linka wczytaj więcej pod listą postów, upewniając się, żeby pokazać go gdy jest więcej postów dostępnych do wczytania.

<template name="postsList">
  <div class="posts">
    {{#each posts}}
      {{> postItem}}
    {{/each}}

    {{#if nextPath}}
      <a class="load-more" href="{{nextPath}}">Load more</a>
    {{/if}}
  </div>
</template>
client/views/posts/posts_list.html

Oto jak powinna wyglądać teraz lista postów:

Przycisk “wczytaj więcej”.
Przycisk “wczytaj więcej”.

Zatwierdź 12-4

Dodany nextPath() do kontrolera i użycie go do przechodze…

Liczba postów a ich długość

Możesz się zastanawiać, dlaczego użyliśmy this.posts.fetch().length zamiast this.posts.count(). Jest to tymczasowe obejście buga (https://github.com/meteor/meteor/issues/654) obecnego w Metorze podczas pisania tej książki. Miejmy nadzieję, że zostanie poprawiony szybko!

Lepszy pasek postępu

Dzielenie liczby postów na strony działa teraz prawidłowo, ale ma wadę: za każdym razem po wciśnięciu przycisku wczytaj więcej router wysyła zapytanie do bazy o posty, zostajemy przekierowani do szablony loading podczas oczekiwania na nowe dane. W wyniku tego zostajemy odesłani na samą górę strony i musimy przesuwać ją za każdym razem z powrotem.

Byłoby o wiele lepiej, gdybyśmy mogli zostać na tej samej stronie podczas wykonywania całej operacji i wyświetlali wskaźnik wczytywania danych. Dokładnie do tego służy pakiet iron-router-progress.

Podobnie to Safari działającej w iOS lub stron takich jak Medium czy YouTube, iron-router-progress dodaje cienki pasek postępu na górze strony. Do zaimplementowanie tego wystarczy dodanie pakietu do Twojej aplikacji:

mrt add iron-router-progress
bash console

Dzięki pracy wykonywanej przez smart packages, nasz wskaźnik wygląda od razu perfekcyjnie! Będzie aktywowany dla każdej ścieżki i automatycznie zakończony gdy dane skończą się ładować.

Zrobimy tylko jedną zmianę. Wyłączymy pakiet iron-router-progress dla ścieżki postSubmit, ponieważ nie musi ona czekać na żadne dane (jest to po prostu pusty formularz):

Router.map(function() {

  //...

  this.route('postSubmit', {
    path: '/submit',
    disableProgress: true
  });
});
lib/router.js

Zatwierdź 12-5

Użycie pakietu iron-router-progress aby polepszyć widok p…

Dostęp do dowolnego postu

Obecnie domyślnie wczytujemy pięć najnowszych postów. Co stanie się, jeżeli ktoś przejdzie bezpośrednio do strony posta?

Pusty szablon.
Pusty szablon.

Jeżeli to sprawdzisz, zobaczysz pusty szablon. To ma sens: nakazaliśmy routerowi subskrypcję do publikacji posts podczas wczytywania ścieżki postsList, ale nie wskazaliśmy żadnych instrukcji dla ścieżki postPage.

Wszystko, co poznaliśmy do tej pory, to sposób subskrypcji listy n najnowszych postów. Jak zapytać serwer o konkretny post? Poznasz teraz mały sekret: można mieć więcej niż jedną publikację każdej kolekcji!

Aby zatem odzyskać nasze zagubione posty, utworzymy nową, oddzielną publikację singlePost, która publikuje tylko jeden post znaleziony po _id.

Meteor.publish('posts', function(options) {
  return Posts.find({}, options);
});

Meteor.publish('singlePost', function(id) {
  return id && Posts.find(id);
});
server/publications.js

Zasubskrybujemy teraz odpowiednie posty po stronie klienta. Subskrybujemy już publikację comments w funkcji waitOn ścieżki postPage, zatem możemy w prosty sposób dodać w tym samym miejscu subskrypcję do ścieżki postEdit, ponieważ wymaga ona tych samych danych.

Router.map(function() {

  //...

  this.route('postPage', {
    path: '/posts/:_id',
    waitOn: function() {
      return [
        Meteor.subscribe('singlePost', this.params._id),
        Meteor.subscribe('comments', this.params._id)
      ];
    },
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postEdit', {
    path: '/posts/:_id/edit',
    waitOn: function() { 
      return Meteor.subscribe('singlePost', this.params._id);
    },
    data: function() { return Posts.findOne(this.params._id); }    
  });

  /...

});
lib/router.js

Zatwierdź 12-6

Użycie pojedyńczej subskrypcji posta, aby upewnić siœ, że…

Mając ukończoną implementację dzielenia postów na strony, nasza aplikacja pozbyła się problemów związanych ze skalowaniem. Użytkownicy są pewni dodawać większą liczby linków, niż poprzednio. Czy nie było by dobrze znaleźć sposób na ustalenie rankingu linków? Jest to dokładnie temat następnego rozdziału Głosowanie.

Głosowanie

13

Poniewaź nasza strona staje się coraz bardziej popularna, znajdowanie najlepszych linków stanie się szybko coraz bardziej skomplikowane. Z tego względu potrzebujemy zbudować system oceniania postów.

Moglibyśmy zbudować skomplikowany system oceniania używając karmy, zanikającej w miarę upływu czasu punktacji i wielu różnych zasad (większość z nich została zaimplementowana w Telescope](http://telesc.pe), większym bracie Microscope). Dla potrzeb naszej aplikacji użyjemy jednak czegoś prostszego i będziemy oceniać posty przez liczbę głosów jakie otrzymały.

Zacznijmy od umożliwieniu użytkownikom głosowania na posty.

Model Danych

Będziemy zapisywali listę głosów na każdego posta, co umożliwi decyzję pokazania przycisku do głosowania użytkownikom jak również uniemożliwi pokazanie przycisku dwa razy tej samej osobie.

Prywatność Danych i Publikacji

Zamierzamy publikować listę głosów dla wszystkich użytkowników, co również umożliwi automatyczny dostęp do danych przez konsolę przeglądarki.

Jest to pewien rodzaju problem prywatności danych, który może pojawić się przez sposób, w jaki działają kolekcje. Na przykład, czy chcemy aby ludzie wiedzieli kto głosował na ich posty? W naszym przypadku upublicznienie tej informacji nie będzie miało żadnych konsekwencji, ale jest ważne aby zdawać sobie z tego sprawę.

Również zauważ, że gdybyśmy chcieli ograniczyć pewną część tej informacji, musielibyśmy się upewnić, że klient nie może zmieniać z pól fields naszej publikacji, zarówno przez usunięcie tej własności po stronie serwera jak i przez przekazywanie całego obiektu z opcjami z serwera do klienta.

Zdenormalizujemy również całkowitą liczbę głosów na posta aby ułatwić odczytywanie tej liczby. Dodamy więc dwa atrybuty do posta: ‘głosujący’ (upvoters) i 'głosy’ (votes). Zacznijmy przez dodanie ich do pliku z danymi początkowymi (fixtures).

// dane początkowe
if (Posts.find().count() === 0) {
  var now = new Date().getTime();

  // utwórz dwóch użytkowników
  var tomId = Meteor.users.insert({
    profile: { name: 'Tom Coleman' }
  });
  var tom = Meteor.users.findOne(tomId);
  var sachaId = Meteor.users.insert({
    profile: { name: 'Sacha Greif' }
  });
  var sacha = Meteor.users.findOne(sachaId);

  var telescopeId = Posts.insert({
    title: 'Introducing Telescope',
    userId: sacha._id,
    author: sacha.profile.name,
    url: 'http://sachagreif.com/introducing-telescope/',
    submitted: now - 7 * 3600 * 1000,
    commentsCount: 2,
    upvoters: [], votes: 0
  });

  Comments.insert({
    postId: telescopeId,
    userId: tom._id,
    author: tom.profile.name,
    submitted: now - 5 * 3600 * 1000,
    body: 'Interesting project Sacha, can I get involved?'
  });

  Comments.insert({
    postId: telescopeId,
    userId: sacha._id,
    author: sacha.profile.name,
    submitted: now - 3 * 3600 * 1000,
    body: 'You sure can Tom!'
  });

  Posts.insert({
    title: 'Meteor',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://meteor.com',
    submitted: now - 10 * 3600 * 1000,
    commentsCount: 0,
    upvoters: [], votes: 0
  });

  Posts.insert({
    title: 'The Meteor Book',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://themeteorbook.com',
    submitted: now - 12 * 3600 * 1000,
    commentsCount: 0,
    upvoters: [], votes: 0
  });

  for (var i = 0; i < 10; i++) {
    Posts.insert({
      title: 'Test post #' + i,
      author: sacha.profile.name,
      userId: sacha._id,
      url: 'http://google.com/?q=test-' + i,
      submitted: now - i * 3600 * 1000,
      commentsCount: 0,
      upvoters: [], votes: 0
    });
  }
}
server/fixtures.js

Jak zwykle zatrzymaj aplikację, uruchom meteor reset, zrestartuj swoją aplikację i utwórz nowe konto użytkownika. Następnie upewnijmy się, że powyższe dwie opcje są zainicjalizowane gdy posty są tworzone.

//...

// upewnij się, że nie ma postów z takim samym odnośnikiem
if (postAttributes.url && postWithSameLink) {
  throw new Meteor.Error(302, 
    'This link has already been posted', 
    postWithSameLink._id);
}

// wybierz klucze z whitelisty
var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
  userId: user._id, 
  author: user.username, 
  submitted: new Date().getTime(),
  commentsCount: 0,
  upvoters: [], 
  votes: 0
});

var postId = Posts.insert(post);

return postId;

//...
collections/posts.js

Budowa szablonów wykorzystywanych do głosowania

Najpierw dodajmy przycisk umożliwiający głosowanie na posta:

<template name="postItem">
  <div class="post">
    <a href="#" class="upvote btn"></a>
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
      <p>
        {{votes}} Votes,
        submitted by {{author}},
        <a href="{{pathFor 'postPage'}}">{{commentsCount}} comments</a>
        {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
      </p>
    </div>
    <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
  </div>
</template>
client/views/posts/post_item.html
The upvote button
The upvote button

Następnie, zawołajmy metodę serwera upvote, gdy użytkownik kliknie na przycisk:

//...

Template.postItem.events({
  'click .upvote': function(e) {
    e.preventDefault();
    Meteor.call('upvote', this._id);
  }
});
client/views/posts/post_item.js

Na końcu, wrócimy do pliku collections/posts.js i następnie dodajmy funkcję po stronie serwera, która doda głos do danego posta.

Meteor.methods({
  post: function(postAttributes) {
    //...
  },

  upvote: function(postId) {
    var user = Meteor.user();
    // ensure the user is logged in
    if (!user)
      throw new Meteor.Error(401, "You need to login to upvote");

    var post = Posts.findOne(postId);
    if (!post)
      throw new Meteor.Error(422, 'Post not found');

    if (_.include(post.upvoters, user._id))
      throw new Meteor.Error(422, 'Already upvoted this post');

    Posts.update(post._id, {
      $addToSet: {upvoters: user._id},
      $inc: {votes: 1}
    });
  }
});
collections/posts.js

Zatwierdź 13-1

Added basic upvoting algorithm.

Ta funkcja jest całkiem prosta. Sprawdzamy prewencyjnie, czy użytkownik jest zalogowany i czy post naprawdę istnieje. Następnie upewniamy się, że użytkownik jeszcze nie zagłosował na posta, i inkrementujemy całkowitą liczbę postów i dodajemy użytkownika do listy użytkowników, którzy zagłosowali.

Ostatni krok jest interesujący, ponieważ użyliśmy kilka specjalnych operatorów Mongo. Jest o wiele więcej do poznania, ale dwa powyżej są bardzo pomocne: $addToSet dodaje element do tablicy jeżeli jeszcze w niej nie istnieje a #inc inkrementuje liczbę całkowitą.

Poprawki interfejsu użytkownika

Jeżeli użytkownik nie jest zalogowany, lub jeżeli już zagłosował na posta, nie będzie mógł zagłosować. Aby odzwierciedlić to w interfejsie użytkownika, użyjemy metody pomocniczej szablonu (helpera) aby warunkowo dodać wyłączoną klasę CSS do przycisku umożliwiającego głosowanie.

<template name="postItem">
  <div class="post">
    <a href="#" class="upvote btn {{upvotedClass}}"></a>
    <div class="post-content">
      //...
  </div>
</template>
client/views/posts/post_item.html
Template.postItem.helpers({
  ownPost: function() {
    //...
  },
  domain: function() {
    //...
  },
  upvotedClass: function() {
    var userId = Meteor.userId();
    if (userId && !_.include(this.upvoters, userId)) {
      return 'btn-primary upvotable';
    } else {
      return 'disabled';
    }
  }
});

Template.postItem.events({
  'click .upvotable': function(e) {
    e.preventDefault();
    Meteor.call('upvote', this._id);
  }
});
client/views/posts/post_item.js

Zmieniamy klasę z .upvote do .upvotable, więc nie zapomnij również zmienić również funcję obsługującą klikanie na element.

ściemnianie przycisków do głosowania.
ściemnianie przycisków do głosowania.

Zatwierdź 13-2

Ściemnienie przycisku do głosowania gdy nie zalogowany / …

Następnie, możesz zauważyć, że posty z jednym głosem są oznaczone jako “1 votes” (jeden głos) więc spędźmy trochę czasu na poprawnej odmianie tych etykiet. Odmiana w liczbie mnogiej może być skomplikowana, ale na teraz zróbmy to w sposóp uproszczony. Zaimplementujemy ogólną funkcję pomocniczą szablonu (helper) , który będzie można użyć w dowolnym miejscu:

Handlebars.registerHelper('pluralize', function(n, thing) {
  // fairly stupid pluralizer
  if (n === 1) {
    return '1 ' + thing;
  } else {
    return n + ' ' + thing + 's';
  }
});
client/helpers/handlebars.js

Funkcje pomocnicze szablonów, które stworzyliśmy poprzednio, były związane z managerem i szablonem do którego miały zastosowanie. Przez użycie Handlebars.registerHelper utworzyliśmy globalny helper, który może być używany z dowolnego szablonu:

<template name="postItem">
//...
<p>
  {{pluralize votes "Vote"}},
  submitted by {{author}},
  <a href="{{pathFor 'postPage'}}">{{pluralize commentsCount "comment"}}</a>
  {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
</p>
//...
</template>
client/views/posts/post_item.html
Dopracowanie liczby mnogiej do perfekcji (ang. Perfecting Proper Pluralization, potrenuj angielski i powiedz to 10 razy)
Dopracowanie liczby mnogiej do perfekcji (ang. Perfecting Proper Pluralization, potrenuj angielski i powiedz to 10 razy)

Zatwierdź 13-3

Dodano helper do stosowania liczby mnogiej aby poprawić f…

Powinniśmy teraz zobaczyć “1 vote” (1 głos). Jako ćwiczenie zmień helpera aby stosował język polski.

Mądrzejszy algorytm głosowania

Nasz kod odpowiedzialny za głosowanie wygląda dobrze, ale może być jeszcze lepszy. W metodzie upvote dwukrotnie odpytujemy bazę Mongo: pierwszy raz, aby pobrać post i drugi aby go uaktualnić.

Prowadzi to do dwóch problemów. Pierwszy jest taki, że jest stosunkowo nieefektywne odpytywać bazę dwukrotnie. Drugi, ważniejszy, że może dojść do wyścigu wykonania kodu (ang. race condition). Prześledź następujący algorytm:

  1. Pobierz posty z bazy danych.
  2. Sprawdź, czy użytkownik już zagłosował.
  3. Jeżeli nie, zagłosuj za pomocą użytkownika.

Co się stanie, jeżeli ten sam użytkownik zagłosuje na ten sam post podczas wykonywania tego algortymu? W obecnej postaci umożliwia to zagłosowanie na ten sam post dwukrotnie. Na szczęście Mongo pozwala na połączenie kroków 1-3 w pojedyńczą komendę:

Meteor.methods({
  post: function(postAttributes) {
    //...
  },

  upvote: function(postId) {
    var user = Meteor.user();
    // upewnij się, że użytkownik jest zalogowany
    if (!user)
      throw new Meteor.Error(401, "Musisz się zalogować, aby móc głosować");

    Posts.update({
      _id: postId, 
      upvoters: {$ne: user._id}
    }, {
      $addToSet: {upvoters: user._id},
      $inc: {votes: 1}
    });
  }
});
collections/posts.js

Zatwierdź 13-4

Ulepszony algorytm głosowania.

Powyżej wykonujemy polecenie “znajdź wszystkie posty z danym id na które ten użytkownik jeszcze nie zagłosował i uaktualnij je w ten sposób”. Jeżeli użytkownik jeszcze nie zagłosował, wyszukane zostaną posty z danym id. Z drugiej strony, jeżeli użykownił zagłosował, wtedy zapytanie nie znajdzie żadnych dokumentów i w konsekwencji nic się nie wydarzy.

Jedynym minusem jest to, że nie możemy powiedzieć użytkownikowi, czy już zagłosował na dany post (ponieważ pozbyliśmy się zapytania do bazy danych, które to sprawdzało). Ale mimo wszystko powinien to zauważyć patrząc na wyłączony przycisk do głosowania w interfejsie użytkownika.

Kompensacja lagów

Przyjmijmy, że starałeś się oszukiwać i wysłać jeden z twoich postów na samą górę listy, przez bezpośrednią zmianę liczby głosów:

> Posts.update(postId, {$set: {votes: 10000}});
Konsola przeglądarki

(Gdzie postId jest identyfikatorem jednego z twoich postów)

Ta bezczelna próba oszukania systemu zostałaby przechwycona przez nasz callback deny() (w collections/posts.js, pamiętasz?) i natychmiast zanegowana.

Ale jeżeli przyjrzysz się temu dokładnie, mógłbyś zauważyć kompensację lagów w akcji. Może to nastąpić tylko na bardzo krótki moment, ale post na chwilę skoczy na pierwszą pozycję listy i wróci na swoją bieżącą pozycję.

Co się stało? W twojej lokalnej kolekcji Posts, update zostało zawołane bez żadnego problemu. Dzieje się to natychmiastowo i post szybko przemieszcza się na górę listy. W międzyczasie po stronie serwera update jest zanegowane, więc chwilę później (kilka milisekund, jeżeli serwer Meteora jest uruchomiony lokalnie), serwer zwraca błąd nakazując kolekcji na przywrócenie do poprzedniej postaci.

Wynik końcowy: podczas czekania na odpowiedź serwera, interfejs użytkownika nie może zrobić nic jak tylko zaufać lokalnej kolekcji. Gdy tylko serwer zwraca odpowiedź błędu i neguje modyfikację, interfejs użytkownika dostosowuje się do tego.

Ocenianie postów na frontowej stronie

Teraz skoro mamy ranking dla każdego posta bazujący na liczbie głosów, wyświetlmy listę najlepszych postów. Aby to osiągnąć, zobaczymy jak zarządzać dwiema osobnymi subskrypcjami kolekcji postów i uogólnić szablon postsList.

Aby rozpocząć, chcemy mieć dwie subskrypcje, każdą dla innego porządku sortowania. Trick polega na tym, że obie subskrypcje zasubskrybują tą samą publikację posts z różnymi parametrami!

Stworzymy także dwie nowe trasy routingu (ang. routes) newPosts oraz bestPosts, dostępne odpowiednio jako /new i /best (jak również /new/5 i /best/5 aby podzielić listę na kilka stron).

Aby tego dokonać rozszerzymy nasz PostListController w dwa osobne kontrolery NewPostsListsController oraz BestPostsListController. Pozwoli to na ponowne użycie tych samych parametrów routowania zarówno dla home i newPosts, przez udostępnienie kontrolera NewPostsListCOntroller z którego można dziedziczyć. Dodatkowo jest to dobry przykład pokazujący jak elastyczny jest Iron Router.

PostsListController = RouteController.extend({
  template: 'postsList',
  increment: 5, 
  limit: function() { 
    return parseInt(this.params.postsLimit) || this.increment; 
  },
  findOptions: function() {
    return {sort: this.sort, limit: this.limit()};
  },
  waitOn: function() {
    return Meteor.subscribe('posts', this.findOptions());
  },
  posts: function() {
    return Posts.find({}, this.findOptions());
  },
  data: function() {
    var hasMore = this.posts().fetch().length === this.limit();
    return {
      posts: this.posts(),
      nextPath: hasMore ? this.nextPath() : null
    };
  }
});

NewPostsListController = PostsListController.extend({
  sort: {submitted: -1, _id: -1},
  nextPath: function() {
    return Router.routes.newPosts.path({postsLimit: this.limit() + this.increment})
  }
});

BestPostsListController = PostsListController.extend({
  sort: {votes: -1, submitted: -1, _id: -1},
  nextPath: function() {
    return Router.routes.bestPosts.path({postsLimit: this.limit() + this.increment})
  }
});

Router.map(function() {
  this.route('home', {
    path: '/',
    controller: NewPostsListController
  });

  this.route('newPosts', {
    path: '/new/:postsLimit?',
    controller: NewPostsListController
  });

  this.route('bestPosts', {
    path: '/best/:postsLimit?',
    controller: BestPostsListController
  });
  // ..
});
lib/router.js

Zauważ, że mamy teraz więcej niż jedną trasę, wyjęliśmy logikę nextPath z kontrolera PostListController w NewPostsListController i BestPostsListController, ponieważ scieżka routingu będzie się różniła w obu przypadkach.

Dodatkowo, gdy sortujemy po liczbie głosów, mamy drugie sortowanie po czasie publikacji postów aby upewnić się, że sortowanie jest poprawne.

Dodamy także linki w nagłówku strony:

<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'home'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav">
          <li>
            <a href="{{pathFor 'newPosts'}}">New</a>
          </li>
          <li>
            <a href="{{pathFor 'bestPosts'}}">Best</a>
          </li>
          {{#if currentUser}}
            <li>
              <a href="{{pathFor 'postSubmit'}}">Submit Post</a>
            </li>
            <li class="dropdown">
              {{> notifications}}
            </li>
          {{/if}}
        </ul>
        <ul class="nav pull-right">
          <li>{{loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/include/header.html

Mając tą implementację, możemy uzyskać listę najlepszych postów:

Ranking po liczbie punktów
Ranking po liczbie punktów

Zatwierdź 13-5

Dodano trasy dla list postów i stron do ich wyświetlania.

Lepszy nagłówek

Teraz skoro mamy dwie strony z listą postów, ciężko wiedzieć którą listę w tej chwili przeglądasz. Zmieńmy więc nagłówek strony, żeby ta informacja była bardziej oczywista. Stworzymy menadżera header.js oraz funkcję pomocniczą, która używając aktualną ścieżkę routingu i jedną lub więcej tras ustawi aktywną klasę na naszych elementach nawigacyjnych:

Przyczyną, dla której chcemy obsługiwać wielokrotne nazwy tras jest to, że zarówno trasy home jako i newPosts (które odpowiadają URL odpowiednio / i /new) wskazują na ten sam szablon. Oznacza to, że nasz klasa activeRouteClass powinna być na tyle sprytna, aby uaktywnić tag <li> w obu przypadkach.

<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'home'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav">
          <li class="{{activeRouteClass 'home' 'newPosts'}}">
            <a href="{{pathFor 'newPosts'}}">New</a>
          </li>
          <li class="{{activeRouteClass 'bestPosts'}}">
            <a href="{{pathFor 'bestPosts'}}">Best</a>
          </li>
          {{#if currentUser}}
            <li class="{{activeRouteClass 'postSubmit'}}">
              <a href="{{pathFor 'postSubmit'}}">Submit Post</a>
            </li>
            <li class="dropdown">
              {{> notifications}}
            </li>
          {{/if}}
        </ul>
        <ul class="nav pull-right">
          <li>{{loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/includes/header.html
Template.header.helpers({
  activeRouteClass: function(/* route names */) {
    var args = Array.prototype.slice.call(arguments, 0);
    args.pop();

    var active = _.any(args, function(name) {
      return Router.current().route.name === name
    });

    return active && 'active';
  }
});
client/views/includes/header.js
Pokazywanie aktywnej strony
Pokazywanie aktywnej strony

Parametry helpera

Aż do teraz nie używaliśmy tego konkretnego wzorca, ale zarówno jak każdy inny tag Handlebars, helper szablonu może także mieć parametry.

I podczas, gdy oczywiście możesz przekazywać kontretne nazwane parametry do swojej funkcji, możesz także przekazać nieokreśloną liczbę anonimowych parametrów i mieć do nich dostęp w ciele funkcji za pomocą obiektu arguments.

W tym ostatnim przypadku, prawdopodobnie będziesz chciał przekształcić obiekt arguments w zwykłą tablicę JavaScript i następnie wywołać pop() aby usunąć hash dodany na końcu przez Handlebars.

Dla każdego elementu nawigacji, helper activeRouteClass przyjmuje listę nazw tras i następnie używa helpera any() biblioteki Underscore aby sprawdzić, czy jakakolwiek trasa przechodzi test (tj. czy odpowiadający jej URL jest równy aktualnej ścieżce).

Jeżeli jakiekolwiek trasy są dopasowane do aktualnej ścieżki, any() zwróci true. Na samym końcu wykorzytamy wzorzec JavaScript o nazwie boolean && string, gdzie false && myString zwraca false ale true && myString zwraca myString.

Zatwierdź 13-6

Dodane aktywne klasy do nagłówka strony.

Teraz gdy użytkownicy mogą głosować na posty w czasie rzeczywistym, zobaczysz, że elementy skaczą w górę i w dół strony podczas zmiany rankingu. Czy nie było by miło, gdyby istniał sposób na wygładzenie tego za pomocą dobrze dopasowanych animacji?

Zaawansowane publikacje

Sidebar 13.5

Do tej pory powinieneś się zaznajomić ze sposobem działania publikacji i subskrypcji. Czas zatem przejść do bardziej zaawansowanych scenariuszy.

Wielokrotne publikowanie kolekcji

W rozdziale wprowadzającym publikacje i subskrypcje poznałeś niektóre ze wzorów stosowania publikacji i subskrypcji i nauczyłeś jak funkcja _publishCursor ułatwia ich implementacje podczas tworzenia własnych stron.

Przypomnijmy sobie, co funkcja publishCursor faktycznie wykonuje: zbiera wszystkie dokumenty, które pasują do kursora i wysyła je do kolecji po stronie klienta która ma tą samą nazwę. Zauważ, że nazwa publication nie jest w żaden sposób wymieniona.

Oznacza to, że możemy mieć więcej niż jedną pulikację łączącą wersję jakichkolwiek kolekcji po stronie klienta i serwera.

Spotkaliśmy się już z tym wzorcem w rozdziale Dzielenie dokumentu na strony, gdzie publikowaliśmy dzielony podzbiór wszystkich postów w dołączonych do bieżącego posta.

Innym podobnym zastosowaniem publikacji jest publikacja przeglądu większego zbioru dokumentów, jak również szczegółów pojedyńczego elementu.

Dwukrotne publikowanie kolekcji
Dwukrotne publikowanie kolekcji
Meteor.publish('allPosts', function() {
  return Posts.find({}, {fields: {title: true, author: true}});
});

Meteor.publish('postDetail', function(postId) {
  return Posts.find(postId);
});

W momencie gdy klient subskrybuje powyższe dwie publikacje (używając autorun aby upewnić się, że właściwy postId jest wysyłany do subskrypcji postDetail), jego kolekcja posts jest uaktualniana danymi pochodzącymi z dwóch źródeł: listy tytułów i nazwisk autorów z pierwszej subskrypcji i pełnymi szczegółami posta z drugiej.

Możesz zdać sobie sprawę, że post publikowany przez postDetail jest również publikowany przez allPosts (pomimo tego, że jedynie z podzbiorem jego danych). Jednakże, Meteor troszczey się o łączeniu pól i upewnienia się, żeby żaden post nie był duplikowany.

Jest to bardzo duże ułatwienie, ponieważ w tym przypdaku gdy renderujemy listę streszczeń postów, mamy do czynienia z obiektami danych które mają tylko tyle danych do pokazania, ile potrzeba. Jednakże podczas renderowania strony dla pojedyńczego posta, mamy wszystkie potrzebne dane. Oczywiście musimy wziąć pod uwagę, żeby nie oczekiwać po stronie klenta dostępności wszystkich pól we wszystkich postach - jest to częste źródło nieporozumień i błędów.

Należy odnotować, że nie jesteś ograniczony do zmieniających się właściwości dokumentu. Mógłbyś równie dobrze publikować te same własności w obu publikacjach ale uporządkować je w inny sposób.

Meteor.publish('newPosts', function(limit) {
  return Posts.find({}, {sort: {submitted: -1}, limit: limit});
});

Meteor.publish('bestPosts', function(limit) {
  return Posts.find({}, {sort: {votes: -1, submitted: -1}, limit: limit});
});
server/publications.js

Wielokrotna subskrypcja publikacji

Powyżej przeczytałeś, że można publikować tą samą kolekcję wielokrotnie. Okazuje się, że można osiągnąć bardzo podobny rezultat za pomocą innego wzorca: stworzenie jednej publikacji i jej wielokrotną subskrypcję.

W Microscope subskrybujemy publikację posts wielokrotnie, ale Iron Router zajmuje się tworzeniem i usuwaniem każdej z nich. Nie ma jednak przeciwskazań, aby subskrybować ją wiele razy.

Przykładowo, załóżmy, że chcemy wczytać zarówno najnowsze i najlepsze posty do pamięci równocześnie:

Dwukrotna subskrypcja tej samej publikacji
Dwukrotna subskrypcja tej samej publikacji

Ustawiamy pojedyńczą publikację:

Meteor.publish('posts', function(options) {
  return Posts.find({}, options);
});

A następnie subskrybujemy ją wielokrotnie. Tak naprawdę mniej więcej to samo robimy w Microscope:

Meteor.subscribe('posts', {submitted: -1, limit: 10});
Meteor.subscribe('posts', {baseScore: -1, submitted: -1, limit: 10});

Co dokładnie tu się dzieje? Każda przeglądarka otwiera dwie różne subskrypcje, każda łączy się do tej samej publikacji po stronie serwera.

Każda subskrypcja dostarcza różne argumenty dla tej publikacji, ale zasadniczo za każdym razem (różny) zbiór dokumentów zostaje wydzielony z kolekcji posts i wysłany do kolekcji po stronie klienta.

Wiele kolekcji w pojedyńczej subskrypcji

W przeciwieństwie do tradycyjnych relacyjnych baz danych takich jak MySQL, która korzysta z łączenia tabel joins, bazy danych NoSQL takie jak Mongo koncentrują się wyłącznie na denormalizacji i wstawianiu. Zobaczmy jak to działa w kontekście Meteora.

Wybierzmy konkretny przykład. Dodaliśmy komentarze do postów i jak do tej pory byliśmy zadowoleni z zamieszczania komentarzy do pojedyńczych postów czytanych przez użytkownika.

Jednakże, przyjmijmy, że chcielibyśmy pokazać komentarze do wszystkich postów na stronie początkowej (miejąc na uwadze, że te posty będą się zmieniały gdy będziemy je wyświetlali strona po stronie). Jest to dobry przykład dla którego powodem jest załączanie komentarzy w postach i tak naprawdę przyczyniło się to do tego, żebyśmy zdenormalizowali liczniki komentarzy.

Moglibyśmy oczywiście zawsze wstawiać komentarze do postów, pozbywając się kolekcji przechowującej komentarze (Comments). Ale jak widzieliśmy w rozdziale Denormalizacja, decydując się na to, pozbylibyśmy się dodatkowych korzyści pracy z osobnymi kolekcjami.

Okazuje się, że istnieje sztuczka, za pomocą której można wstawiać komentarze do postów, zostawiając osobne kolekcje.

Przyjmijmy, że zarówno z naszą pierwszą stroną pokazującą listę postów, chcemy zasubkrybować listę najlepszych 2 komentarzy do każdego posta.

Byłoby to trudne do realizacji z niezależną publikacją komentarzy, szczególnie gdyby lista postów była w jakiś sposób ograniczona (przymijmy, że do 10 ostatnich postów). Musielibyśmy napisać publikację, która wyglądała by mniej więcej w taki sposób:

Dwie kolekcje w jednej subskrypcji
Dwie kolekcje w jednej subskrypcji
Meteor.publish('topComments', function(topPostIds) {
  return Comments.find({postId: topPostIds});
});

Byłby to problem z punktu widzenia wydajności, ponieważ publikacja musiałaby być usuwana i ponownie tworzona za każdym razym, gdy zmienia się lista topPostsIds.

Jest sposób na obejście tego. Możemy wykorzystać fakt, że możemy mieć nie tylko więcej niż jedną publikację dla pojedyńczej kolekcji, ale także więcej niż jedną kolekcję dla jednej publikacji:

Meteor.publish('topPosts', function(limit) {
  var sub = this, commentHandles = [], postHandle = null;

  // prześlij dwa pierwsze komentarze załączone do postu
  function publishPostComments(postId) {
    var commentsCursor = Comments.find({postId: postId}, {limit: 2});
    commentHandles[post._id] = 
      Meteor.Collection._publishCursor(commentsCursor, sub, 'comments');
  }

  postHandle = Posts.find({}, {limit: limit}).observeChanges({
    added: function(id, post) {
      publishPostComments(post._id);
      sub.added('posts', id, post);
    },
    changed: function(id, fields) {
      sub.changed('posts', id, fields);
    },
    removed: function(id) {
      // przestań obserwować zmiany komentarzy do postu
      commentHandles[id] && commentHandles[id].stop();
      // usuń post
      sub.removed('posts', id);
    }
  });

  sub.ready();

  // upewnij się, że wszystko zostało posprzątane (uwaga: `_publishCursor`
  // robi to za nas za pomocą obserwatora komentarzy)
  sub.onStop(function() { postsHandle.stop(); });
});

Zwróć uwagę, że nie zwracami niczego w tej publikacjim ponieważ wysyłamy ręcznie wiadomości do pod kolekcji nas samych (korzystając z funkcji .added() i jej pochodnych). Z tego powodu nie musimy prosić _publishCursor o zrobienie tego dla nas przez zwrócenie kursora.

Od teraz za każdym razem gdy publikujemy posta, równocześnie publikujemy dwa najlepsze komentarze załączone do niego. I to wszystko za pomocą jednego wywołania subskrypcji!

Mimo tego, że do tej pory Meteor nie umożliwia tego podejścia do publikacji w prosty sposób, możesz zerknąć na pakiet publish-with-relations na Atmosphere, która stara się ułatwić stosowanie tego wzorca.

Łączenie różnych kolekcji

Do czego możemy jeszcze wykorzystać nowo przyswojoną wiedzę? A więc, jeżeli nie używamy publishCursor nie musimy stosować się do ograniczenia, że kolekcja źródłowa na serwerze musi mieć tą samą nazwę, o kolekcja docelowa po stronie klienta.

Jedna kolekcja dla dwóch subskrypcji
Jedna kolekcja dla dwóch subskrypcji

Jednym z powodów, dla którego chcielibyśmy to wykorzystać, jest Dziedziczenie Pojedyńczej Tabeli.

Przyjmijmy, że chcielibyśmy się odnieść do różnych typów obiektów z naszego posta. Każdy z nich przechowuje niektóre wspólne pola, ale różni się trochę treścią. Przykładowo, moglibyśmy budować silnik blogowy przypominający Tumblr, gdzie każdy post ma ID, datę utworzenia i tytuł; ale dodatkowo może także zawierać rysunek, video, link czy po prostu tekst.

Moglibyśmy zapisywać wszystkie te obiekty w pojedyńczej kolekcji 'Resources' po stronie serwera, używając atrybutu type aby określić jakim są typem obiektu (video, ’image, link itd.).

Pomimo tego, że po stronie serwera istnieje pojedyńcza kolekcja Resources, moglibyśmy przekształcić ją w wiele kolekcji Videos, Images, itd. po stronie klienta używając sztuczki jak poniżej.

  Meteor.publish('videos', function() {
    var sub = this;

    var videosCursor = Resources.find({type: 'video'});
    Meteor.Collection._publishCursor(videosCursor, sub, 'videos');

    // _publishCursor nie woła tego za nas w przypadku, gdy wywołujemy to więcej niż raz.
    sub.ready();
  });

Nakazujemy publishCursor opublikować wszystkie nasze filmy na wzór tego, jak zrobił by to kursor zwracany normalnie, ale zamiast publikować kolekcję resources po stronie klienta, publikujemy część resources do videos.

Czy to jest aby na pewno dobre rozwiązanie? Nie jesteśmy tu po to aby to osądzać. Tak, czy inaczej, dobrze wiedzieć jak używać Meteora w pełni jego możliwości!

Animacje

14

Opanowaliśmy już głosowanie w czasie rzeczywistym, punktację i ranking. Niestety te usprawnienia doprowadziły do nieprzyjemnego interfejsu użytkownika, ponieważ posty skaczą po stronie głównej. Użyjemy animacji aby pozbyć się tego efektu i wygładzić interfejs.

Meteor i DOM

Zanim dojdziemy do prawdziwej zabawy (przemieszczania elementów na stronie), należy zrozumieć jak Meteor współpracuje z DOM (ang. Document Object Model – kolekcją elementów HTML które składają się na zawartość strony).

Główną zasadą o której należy pamiętać jest to, że elementy DOM nie mogą się przemieszczać. Mogą być wyłącznie usuwane i tworzone (miej na uwadze, że jest to ograniczenie DOM, a nie Meteora). Aby więc spowodawać iluzję zamiany elementów A i B Meteor usunie element B i wstawi całkiem nową kopię (B’) przed elementem A.

Nie ułatwia to animacji, ponieważ nie można po prostu animować elementu B na nową pozycję, ponieważ B zniknie zaraz po przerenderowaniu strony (co jak wiemy dzieje się natychmiastowo dzięki reaktywności). Zamiast tego należy animować nowo stworzony B’, który przemieszcza się z początkowej pozycji B’ do nowej pozycji przed elementem A.

Aby zamienić miejscami posty A i B (umieszczone w miejscach odpowiednio p1 i p2) przejdziemy przez kolejne kroki:

  1. Usunięcie B
  2. Utworzenie B’ przed A w DOM
  3. Przesunięcie B’ na pozycję p2
  4. Przesunięcie A na pozycję p1
  5. Animowanie A na pozycję p2
  6. Animowanie B’ na pozycję p1

Poniższy diagram wyjaśnia szczegółowo te kroki:

Zamiana dwóch postów
Zamiana dwóch postów

Miej na uwadze, że dla kroków 3 i 4 nie animujemy A i B’ do ich pozycji ale “teleportujemy” je natychmiastowo. Ponieważ dzieje się to natychmiastowo, daje to iluzję tego, że element B nie został usunięty i odpowiednio umieści oba elementy do annimacji na nowe pozycje.

Na szczęście Meteor zatroszczy się za nas o kroki 1 & 2, zatem musimy martwić się wyłącznie o kroki od 3 do 6.

Co więcej, w krokach 5 i 6 wszystko co robimy, to przesuwanie elementów do ich docelowych miejsc. W związku z tym musimy się martwić wyłącznie o kroki 3 i 4 tj. wysłanie elementów do startowych punktów animacji.

Dopasowanie czasu animacji

Do tej pory mówiliśmy o tym jak animować posty ale nie kiedy je animować.

Dla kroków 3 i 4 odpowiedź brzmi: podczas renderowania callback'a rendered szablonu w środku managera post_item.js, który jest wywoływany przy każdej zmianie własności posta (w naszym przypadku rankingu).

Kroki 5 i 6 są bardziej podchwytliwe. Pomyśl o nich w następujący sposób: jeżeli każesz perfekcyjnie myślącemu robotowi poruszać się na północ przez 5 minut, a następnie po zakończeniu poruszać się na południe przez 5 minut, prawdopodobnie wydedukował by, że skończy w tej samej pozycji i zamiast martwić swoją energię wcale nie zmieniłby pozycji.

Jeżeli chcesz się upewnić, że robot będzie się poruszał przez całe 10 minut, należy poczekać aż przebył pierwsze 5 minut, i wtedy kazać mu wrócić.

Przeglądarka działa w podobny sposób: jeżeli damy jej 2 instrukcje jednocześnie, nowe współrzędne zajmą miejsce starych i nic by się nie wydarzyło. Inaczej mówiąc, nie bylibyśmy w stanie ich animować.

Meteor nie zapewnia callbacka justAfterRendered (ang. zaraz po renderowaniu), ale możemy go symulować używając Meteor.defer() który bierze funkcję i odracza jej wykonanie aż do momentu zarejestrowania jej jako inne zdarzenie.

Pozycjonowanie CSS

Aby animować posty, które są zamieniane kolejnością na stronie, musimy wejść w terytorium CSS. W związku z tym szybko przedstawimy umieszczanie elementów za pomocą CSS.

Elementy na stronie domyślnie używają statycznego pozycjonowania. Statycznie pozycjonowane elementy naturalnie wpasowują się w stronę i ich współrzędne nie mogą być zmieniane lub animowane.

Względne pozycjonowanie oznacza z innej strony, że element także jest naturanie wpasowany w stronę, ale może być umieszczony z przesunięcięm względnym do pozycji początkowej.

Bezwzględne pozycjonowanie idzie o jeden krok dalej i pozwala nadać określony współrzędne x/y dla danego elementu względnie do punktu początkowego dokumentu lub pierwszego bezwzględnego lub względnie przesuniętego elementu nadrzędnego.

Użyjemy pozycjonowania względnego aby animować posty. Zatroszczyliśmy się za Ciebie o kod CSS, ale jeżeli chcesz to zrobić sam, wystarczy dodać poniższy kod do arkusza stylów (ang. stylesheet):

.post{
  position:relative;
  transition:all 300ms 0ms ease-in;
}
client/stylesheets/style.css

Ułatwia to znacznie kroki 5 i 6: wszystko co należy zrobić, to ustawić top na 0px (wartość domyślną) i nasze posty automatycznie przesuną się na ich “normalną” pozycję.

Oznacza to, że jedynym wyzwaniem jest znalezienie miejsca z którego je animować (kroki 3 i 4) względnie do ich nowej pozycji. Mówiąc inaczej jak bardzo je przesunąć. Nie jest to również trudne: prawidłowy offset to po prostu poprzednia pozycja postu minus jego nowa pozycja.

Pozycja bezwględna (Position:absolute)

Moglibyśmy również użyć position:absolute z względnym elementem nadrzędnym do pozycjonowania elementów. Wielkim minusem bezwzględnie pozycjonowanych elementów jest to, że są całkowicie usuwane z przepływu strony, powodując zwinięcie nadrzędnego kontenera tak jak gdyby byłby pusty.

To w konsekwencji oznacza, że potrzebowalibyśmy sztucznie ustawić wysokość kontenera za pomocą JavaScript zamiast postawić przeglądarce na naturalne rozmieszczenie elementów. W związku z tym, gdzie tylko można najlepiej używać pozycjonowania względnego.

Pamięć absolutna.

Pozostaje jeszcze jeden problem. Podczas gdy element A zostaje w DOM i może dzięki temu “pamiętać” poprzednią pozcyję, element B przeżywa reinkarnację jako element B’ i ma wyczyszczoną pamięć o poprzedniej pozycji.

Na szczęście Meteor radzi sobie z tym problemem przez udostępnienie objektu instancji szablonu w callbacku rendered. Za oficjalną dokumentacją Meteora:

W callbacku this odności się do obiektu instancji szablonu, który jest unikatowy do tego wystąpienia szablonu i zachowany przy renderowaniu.

Zatem co zrobimy, to odnajdziemy obecną pozycję posta na stronie i następnie zapamiętamy tą pozycję w obiekcie instancji szablonu. W ten sposób, nawet wtedy gdy post jest usuwany i ponownie tworzony jesteśmy w stanie określić skąd go animować.

Instancje szablonu pozwalają również na dostęp do danych kolekcji przez własność data. Okaże się to pomocne do ustalenia rankingu posta.

Ranking Postów

Mówiliśmy poprzednio o rankingu postów ale “ranking” właściwie nie istnieje jako właściwość posta, ponieważ jest jedynie konsekwencją ułożenia postów w kolekcji. Jeżeli chcemy być w stanie animować posty zgodnie z ich rankingiem, będziemy musieli w jakiś sposób wyczarować tą własność znikąd.

Zauważ, że nie możemy wstawić tej własności “ranking” bezpośrednio do bazy danych, ponieważ ranking jest względną własnością zależną od sposobu sortowania postów (np. post może być pierwszy w rankingu jeżeli jest sortowany po dacie publikacji lub trzeci gdy jest posortowany względem punktów).

Idealnie wstawilibyśmy tą własność w kolekcje newPosts i topPosts ale Meteor jeszcze nie dostarcza wygodnego mechanizmu pozwalającego to osiągnąć.

Zatem wstawimy “ranking” jako ostatni możliwy krok managera szabloku postList:

Template.postsList.helpers({
  postsWithRank: function() {
    this.posts.rewind();
    return this.posts.map(function(post, index, cursor) {
      post._rank = index;
      return post;
    });
  }
});
/client/views/posts/posts_list.js

Zamiast prostego zwracania kursora Posts.find({}, {sort: {submitted: -1}, limit: postsHandle.limit()}) jak w poprzednim helperze posts, helper postsWithRank używa dostępnego kursora i dodaje własność _rank do każdego z jego dokumentów.

Nie zapomnij uaktualnić szablonu postList:

<template name="postsList">
  <div class="posts">
    {{#each postsWithRank}}
      {{> postItem}}
    {{/each}}

    {{#if nextPath}}
      <a class="load-more" href="{{nextPath}}">Load more</a>
    {{/if}}
  </div>
</template>
/client/views/posts/posts_list.html

A teraz bądź tak miły i przewiń wstecz

Meteor jest jednym z najbardziej przemyślanych i najnowocześniejszych z dostępnych frameworków webowych, ale jedna z jego funcji przywodzi na myśl lata 90te i kasety magnetowidowe: funkcja rewind().

Za każdym razem, gdy używasz kursora z forEach(), map(), czy fetch() będziesz zmuszony przewinąć kursor do tyłu zanim będzie mógł być ponownie użyty.

W niektórych przypadkach jest lepiej prewencyjnie przewinąć kursor niż ryzykować pojawienie się buga.

Składając wszystko razem

Można teraz złożyć wszystkie elementy razem używając callbacka szablonu rendered managera post_item.js do uruchomienia naszej animacji:

Template.postItem.helpers({
  //...
});

Template.postItem.rendered = function(){
  // animacja postu na nową pozycję
  var instance = this;
  var rank = instance.data._rank;
  var $this = $(this.firstNode);
  var postHeight = 80;
  var newPosition = rank * postHeight;

  // jeżeli element posiada currentPosition (tzn. nie jest pierwszy raz renderowany)
  if (typeof(instance.currentPosition) !== 'undefined') {
    var previousPosition = instance.currentPosition;
    // oblicz różnicę między starą i nową pozycją i wyślij tam element
    var delta = previousPosition - newPosition;
    $this.css("top", delta + "px");
  }

  // rysuj w starej pozycji..
  Meteor.defer(function() {
    instance.currentPosition = newPosition;
    // przenieś element na pozycję początkową
    $this.css("top",  "0px");
  }); 
};

Template.postItem.events({
  //...
});
/client/views/posts/post_item.js

Zatwierdź 14-1

Dodano animację uporządkowania postów.

Nie powinno być trudne rozumienie tego, jeżeli spojrzysz na nasz poprzedni diagram.

Zwróć uwagę, że skoro ustawiamy własność currentPosition instancji szablonu w callbacku defer, oznacza to że ta własność nie będzie istniała podczas pierwszego renderowania fragmentu szablonu. Nie jest to jednak problemem, ponieważ i tak nie jesteśmy zainteresowani animowaniem pierwszego renderowania.

Teraz otwórz swoją stronę w przeglądarce i zacznij dodawać głosy. Powinieneś zaobserować delikatne przemieszczanie się postów niczym podczas baletu!

Animacja Nowych Postów

Nasze posty teraz odpowiednio się przemieszczają, ale nie mieliśmy jeszcze żadnej animacji nowego posta. Zamiast prostego wstawiania nowego posta, użyjmy efektu ściemniania podczas wstawiania.

Jest to bardziej skomplikowane, niż wydaje się na pierwszy rzut oka. Problem polega na tym, że callback Meteora rendered jest wołany w dwóch osobnych przypadkach:

  1. Kiedy nowy szablon jest wstawiany w DOM
  2. Za każdym razem gdy zmianie ulegają dane, z których korzysta szablon.

Wyłącznie przypadek 1 powinien być animowany, chyba że masz na celu zmiany interfejsu użytkownia w choinkę przy każdej zmianie danych.

Upewnijmy się, że tylko animujemy posty kiedy są nowe a nie kiedy są ponownie renderowane z powodu zmiany danych, z których korzystają. Już sprawdzamy istnienie zmiennej instancji (która jest ustawiana tylko po pierszym renderowaniu szablonu), zarem wystarczy wrócić do naszego callbacka rendered i dodać blok else:

Template.postItem.helpers({
  //...
});

Template.postItem.rendered = function(){
  // animacja postu na nową pozycję
  var instance = this;
  var rank = instance.data._rank;
  var $this = $(this.firstNode);
  var postHeight = 80;
  var newPosition = rank * postHeight;

  // jeżeli element posiada currentPosition (tzn. nie jest pierwszy raz renderowany)
  if (typeof(instance.currentPosition) !== 'undefined') {
    var previousPosition = instance.currentPosition;
    // oblicz różnicę między starą i nową pozycją i wyślij tam element
    var delta = previousPosition - newPosition;
    $this.css("top", delta + "px");
  } else {
    // jest to pierwsze renderowanie, więc ukryj element
    $this.addClass("invisible");
  }

  // rysuj w starej pozycji..
  Meteor.defer(function() {
    instance.currentPosition = newPosition;
    // przenieś element na pozycję początkową
    $this.css("top",  "0px").removeClass("invisible");
  }); 
};

Template.postItem.events({
  //...
});
/client/views/posts/post_item.js

Zatwierdź 14-2

Ściemnianie elementów podczas ich rysowania.

Zauważ, że że removeClass("invisible") którą dodaliśmy w funkcji defer() będzie uruchamiana podczas każdego renderowania. Ale będzie robiła cokolwiek wyłącznie gdy klasa .invisible jest obecna dla elementu, a będzie to prawdziwe wyłącznie podczas pierwszego renderowania elementu.

CSS i JavaScript

Jak już zapewne zauważyłeś, używamy klasy CSS .invisible aby wyzwolić animację zamiast bezpośrednio animować własność CSS opacity jak robiliśmy w przypadku top. Jest to spowodowane tym, że dla top potrzebna była animacja własności do określonej wartości, która zależała na danych instancji.

Z innej strony, chcemy wyłącznie pokazać element niezależnie od jego danych. Ponieważ przechowywanie CSS z dala od kodu JavaScript jest dobrą praktyką, dodamy wyłącznie tutaj klasę tutaj a ustalimy szczegóły animacji w akruszu stylów (kodzie CSS).

W końcowym efekcie powinniśmy mieć taką animację, jaką chcieliśmy. Teraz uruchom własną aplikację i spróbuj jak to działa! Możesz również zmieniać klasy .posts i posts.invisible aby sprawdzić, czy istnieją inne ciekawe przejścia animacji. Wskazówka: CSS easing functions jest dobrym miejscem na start!

Słownik Meteor'a

Sidebar 14.5

W tej książce poznasz kilka nowych terminów. Znajdziesz również takie, które znasz, ale są one używane w nowy sposób w kontekście Meteora. W tym rozdziale zajmiemy się ich przedstawieniem.

Klient

Kiedy mówimy o Kliencie, odnosimy się do kodu, który uruchamiany jest po stronie użytkownika. Może to być tradycyjna przeglądarka jak Firefox lub Safari, lub coś bardziej skomplikowanego jak np. UIWebView dla natywnej aplikacji iPhone'a.

Kolekcja

Kolekcje w Meteorze są zbiorami danych automatycznie synchronizowanymi pomiędzy klientem i serwerem. Kolekcje posiadają nazwę (jak np. posty) i zazwyczaj istnieją zarówno na po stronie klienta jak i serwera. Pomimo tego, że zachowują się inaczej, mają wspólne API oparte na API bazy danych Mongo.

Komputacja (Obliczenie)

Komputacja jest blokiem kodu, który jest uruchamiany za każdym razem, kiedy reaktywne źródło danych, na którym jest oparta, zmieni się. Mając reaktywne źródło danych (na przykład zmienna Session) i chcąc reagować na jego zmiany, będziesz musiał ustawić komputacje dla niego.

Kursor

Kursor jest rezultatem wykonania zapytania na kolekcji Mongo. Po stronie klienta, kursor nie jest zwykłą tablicą wyników ale reaktywnym obiektem, na którym można obserwować dodawanie, usuwanie i aktualizowanie obiektów.

DDP

DDP jest w Meteorze Rozproszonym Protokołem Danych (ang. Distributed Data Protocol), który używany jest do synchronizacji kolekcji i wykonywania metod Meteora. Głównym zadaniem DDP jest zastąpienie ociężałego protopkołu HTTP protokołem czasu rzeczystistego.

Zależności (ang. Deps)

Zależności to część systemu reaktywności Meteora. Zależności działając pod maską zapewniają automatyczną synchronizację kodu HTML z danymi modelu.

Dokument

Mongo jest bazą danych opartą o dokumenty. W związku z tym obiekty wyciągane z kolekcji są nazywane “dokumentami”. Dokumenty są prostymi obiektami JavaScript (jednak nie mogą zawierać funkcji) z jedną specjalną właściwością _id, którą Meteor wykorzystuje do śledzenia właściwości obiektu w protokole DDP.

Funkcje pomocnicze (ang. Helpers)

Kiedy system szablon potrzebuje wyrenderować coś bardziej złożonego niż właściwość dokumentu wtedy z pomocą przychodzą funkcje pomocnicze (potocznie zwane helperami). Funkcje te ułatwiają renderowanie danych.

Kompensacja opóźnienia

Jest techniką, która umożliwia symulację wykonania metody po stronie klienta. Pozwala to uniknąć opóźnienia (pot. lagu) spowodowanego czekaniem na odpowiedź serwera.

Metoda

Metody w Meteorze są zdalnymi wywołaniami funkcji zainicjowanymi po stronie klienta, a wykonywanymi na serwerze. Są zaopatrzone w mechanizmy, które umożliwiają śledzenie zmian w kolekcjach oraz kompensację opóźnienia.

MiniMongo

Implementacja kolekcji po stronie klienta. Jest magazynem danych rezydującym w pamięci operacyjnej, posiadającym API na wzór Mongo. Biblioteka dostarczająca wspomniane funkcjonalności nazywana jest “MiniMongo” aby zaznaczyć, że jest okrojoną wersją bazy Mongo, która w całości działa w pamięci operacyjnej.

Pakiet

Pakiet w Meteorze może składać się z

  1. Kodu JavaScript działającego po stronie serwera.
  2. Kodu JavaScript działającego po stronie klienta.
  3. Instrukcji na temat sposobu przetwarzania zasobów (np. z SASS do CSS).
  4. Zasobów do przetworzenia.

Pakiet można traktować jak bibliotekę o super mocy. Meteor posiada obszerny zestaw pakietów. Dostępne jest także repozytorium Atmosphere, które jest kolekcją pakietów pisanych przez społeczność użytkowników Meteora.

Publikacja

A publication is a named set of data that is customized for each user that subscribes to it. You set up a publication on the server.

Publikacja jest opatrzonym nazwą zbiorem danych, który jest indywidualnie dostosowywany do każdego użytkownika, który tę publikację subskrybuje. Publikacja jest umieszczana na serwerze.

Serwer

Serwer Meteora to serwer HTTP i DDP działającymi w oparciu o node.js. Jest złożony z wszystkich bibliotek wchodzących w skład Meteor'a oraz twojego kodu JavaScript rezydującego po stronie serwera. Podczas uruchomiania serwera Meteor łączy się z bazą danych Mongo (która jest uruchomiana automatycznie w trybie deweloperskim).

Sesja

Sesja w Meteorze odnosi się do działającego po stronie klienta reaktywnego źródła danych, używanego przez twoją aplikację do śledzenia stanu w jakim jest użytkownik.

Subskrybcja

Subskrybcja jest odwołaniem do publikacji dla konkretnego klienta. Subskrybcja składa się z kodu, który uruchamiany jest w przeglądarce i komunikuje się z publikacją na serwerze, oraz zapewnia synchronizację danych.

Szablon

Szablony pozwalają na generowanie kodu HTML w JavaScript'cie. Meteor domyślnie wspiera bibliotekę Handlebars, bibliotekę nie opartą na funkcjach logicznych. Planowane jest wsparcie dla innych systemów szablonów w przyszłości.

Kontekst danych szablonu

Podczas renderowania szablonu wykorzystywany jest obiekt JavaScript, który posiada konkretne dane przeznaczone dla tego szablonu. Obiekty te zazwyczaj są prostymi obiektami JavaScript (ang. plain-old-JavaScript-objects, POJOs), często są to dokumenty z kolekcji, jednak mogą być bardziej skomplikowane i posiadać funkcje.