Dzielenie dokumentu na strony

12

procent przetłumaczone

W tym rozdziale dowiesz się:

  • Dowiedz się wiecej o subskrypcjach w Meteorze i wykorzystaniu ich do kontroli danych.
  • Zaimplementuj nieskończone dzielenie dokumentu na strony.
  • Użyj pakietu `iron-router-progress` aby zaimplementować pasek postępu w stylu iOS.
  • Utwórz specjalną subskrypcję dla bezpośrednich linków do strony z postem.
  • 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.