Маршрутизация (Routing)

5

На данный момент переведено

В этой главе вы:

  • Изучите маршрутизацию в Meteor.
  • Создадите страницы обсуждения постов с уникальными URL-ми.
  • Изучите как делать ссылки с такими URL-ми правильно.
  • Теперь, когда мы имеем список постов (которые в конце концов отправят пользователи), нам нужна отдельная страница для каждого поста, где наши пользователи смогут обсудить его.

    Мы бы хотели, чтобы эти страницы были доступны через постоянную ссылку (permalink) – URL вида http://myapp.com/posts/xyz (где xyz – идентификатор _id в MongoDB), которая уникальна для каждого поста.

    Для этого нам понадобится какая-нибудь маршрутизация (routing), чтобы адресная строка в браузере правильно соответствовала отображаемому контенту.

    Добавление пакета Iron Router

    Iron Router – это пакет маршрутизации, который был задуман специально для Meteor приложений.

    Он не только помогает в маршрутизации (настройке путей), но может и позаботиться о фильтрации (сопоставлении действий и некоторых путей), и даже управлять подписками (контролировать, какой путь имеет доступ к этим данным). (Примечание: Iron Router частично был разработан Tom Coleman – соавтором книги Discover Meteor.)

    Во-первых, установим пакет из Atmosphere:

    $ meteor add iron:router
    
    Terminal

    Эта команда скачает и установит пакет Iron Router для использования вашим приложением. Заметьте, что вам иногда нужно будет перезапустить Meteor-приложение (с помощью ctrl+c убить процесс, затем meteor чтобы запустить его снова) перед тем, как пакет может быть использован.

    Термины маршрутизации

    В этой главе мы коснёмся большинства особенностей маршрутизации. Если вы уже имеете некоторый опыт с фреймворками, такими как Rails, вам будет знакомо большинство из этих концепций. На случай, если это не так, здесь приводится краткий словарик для быстрого ознакомления:

    • Маршруты (Routes): Основной строительный блок маршрутизации. Это в основном набор инструкций, которые говорят куда идти и что делать при встрече с данным URL-ом.
    • Пути (Paths): URL внутри вашего приложения. Он может быть статичным (/terms_of_service) или динамичным (/posts/xyz), и даже включать параметры запроса (/search?keyword=meteor).
    • Сегменты (Segments): Различные части пути, разделенные прямым слэшем (/).
    • Обработчики (Hooks): Действия, которые вы захотите произвести перед, после, или даже во время процесса маршрутизации. Типичным примером может быть проверка прав пользователя перед отображением страницы.
    • Фильтры (Filters): Простые обработчики, которые вы глобально определяете для одного или нескольких маршрутов.
    • Шаблоны маршрутов (Route Templates): Каждому маршруту нужно указать шаблон. Если вы его не укажете, маршрутизатор будет искать шаблон с таким же именем, как у маршрута по умолчанию.
    • Макеты (Layouts): Вы можете думать о макетах, как о цифровых фоторамках. Они содержат в себе весь html-код, который оборачивается текущим шаблоном, и будут оставаться ими же, даже если шаблон изменится.
    • Контроллеры (Controllers): Иногда вы будете понимать, что многие ваши маршруты повторно используют одни и те же параметры. Вместо дублирования кода вы можете наследовать такие маршруты от одного маршрутного контроллера (routing controller), который будет содержать всю логику маршрутизации.

    Для более подробной информации об Iron Router смотрите полную документацию на GitHub.

    Маршрутизация: сопоставление URL-ов и шаблонов

    До сих пор мы делали сборку нашего макета, используя жёстко заданные вставки шаблонов (такие как {{>postsList}}). Таким образом, контент нашего приложения может меняться, но основная структура страницы всегда одинакова: заголовок со списком постов ниже.

    Iron Router позволяет нам уйти от этой замшелости взятием на себя отрисовки содержимого html-тега <body>. Поэтому мы не будем определять содержимое <body> сами, как мы делали это с обычной html-страницей. Вместо этого мы укажем маршрут к специальному макету, который содержит метод шаблона {{> yield}}.

    Метод {{> yield}} определит специальную динамическую зону, которая автоматически будет отрисовывать шаблон, соответствующий текущему маршруту (договоримся, что с этого места такие специальные шаблоны мы будем называть «маршрутными шаблонами» – «route templates»):

    Layouts and templates.
    Layouts and templates.

    Начнем с создания нашего макета и добавления метода {{> yield}}. Прежде всего удалим наш html-тег <body> из main.html и переместим его контент в шаблон layout.html.

    Итак, наш похудевший main.html теперь выглядит так:

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

    В то же время вновь созданный layout.html теперь будет содержать внешний макет приложения:

    <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

    Как вы можете заметить, мы заменили включение шаблона postsList вызовом метода yield. Вы заметите, что после этого изменения мы ничего не увидим на экране. Это потому что мы еще не сказали маршрутизатору, что делать с URL /, поэтому он просто выдаёт пустой шаблон.

    Для начала мы можем вернуть наше старое поведение, сделав соответствие корневого URL / шаблону postsList. В корне нашего проекта создадим каталог /lib, а внутри него - файл router.js:

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

    Мы сделали две важные вещи. Во-первых, указали маршрутизатору, какой макет использовать по умолчанию. Во-вторых, определили новый маршрут postsList, соответствующий пути /.

    Папка /lib

    Всё, что находится в папке /lib, гарантированно загрузится первым - прежде, чем всё остальное в вашем приложении (за возможным исключением умных пакетов). Это делает папку /lib отличным местом для любого вспомогательного кода, который должен быть доступен всё время.

    Небольшое предупреждение: заметим, что поскольку папки /lib нет внутри папок /client или /server это означает, что её контент будет доступен для обоих окружений.

    Именованные маршруты

    Проясним здесь некоторые моменты. Мы назвали наш маршрут postsList, но мы также дали имя postsList и шаблону. Что же происходит в таком случае?

    По умолчанию Iron Router ищет шаблон с таким же именем как и маршрут. По факту он будет даже искать путь (path), основанный на имени маршрута, – это означает, что мы, не определив свой путь (который мы указываем опцией path в нашем описании маршрута), сделали наш шаблон по умолчанию доступным по URL /postsList.

    Вам может быть интересно, почему вообще маршрутам нужно давать имена. Именованные маршруты позволяют использовать несколько особенностей Iron Router, чтобы облегчить создание ссылок внутри приложения. Один из самых полезных методов в Handlebars – это {{pathFor}}, который возвращает компонент URL путь любого маршрута.

    Мы хотим, чтобы наша ссылка на главную страницу указывала на список постов. Вместо того чтобы определить статический URL /, мы можем использовать метод в Handlebars. Конечный результат будет такой же, но это даст нам больше гибкости, так как этот метод будет всегда выводить правильный URL, даже если мы изменим путь маршрута в маршрутизаторе.

    <header class="navbar">
      <div class="navbar-inner">
        <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
      </div>
    </header>
    
    //...
    
    client/views/application/layout.html

    В ожидании данных

    Если провести развертывание текущей версии приложения (или запустите экземпляр, используя ссылку выше), можно заметить, что при загрузке страницы список постов некоторое время отображается пустым. Это происходит, потому что пока подписчик posts не закончит забирать данные с сервера, постов для отображения на странице попросту нет.

    Было бы намного лучше в таких случаях обеспечить пользователю визуальную обратную связь с происходящим.

    К счастью, Iron Router даёт простой способ сделать это – мы можем воспользоваться подписчиком waitOn:

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      waitOn: function() { return Meteor.subscribe('posts'); }
    });
    
    Router.map(function() {
      this.route('postsList', {path: '/'});
    });
    
    Router.onBeforeAction('loading');
    
    lib/router.js

    Разберемся с этим кодом. Во-первых, мы изменили блок Router.configure(), обеспечив маршрутизатор именем загрузочного шаблона (который мы скоро создадим) для перенаправления на него, пока наше приложение ожидает данные.

    Во-вторых, мы добавили функцию waitOn, которая возвращает нашу posts-подписку. Это означает, что подписка posts гарантированно подгрузится перед отправкой пользователя по запрошенному маршруту.

    Заметим, что поскольку мы определяем нашу функцию waitOn глобально на уровне маршрутизации, она сработает, когда пользователь впервые зайдёт в ваше приложение. После этого данные уже будут загружены в память браузера, и маршрутизатору не нужно будет ждать их снова.

    Так как мы сейчас позволили маршрутизатору обрабатывать нашу подписку, её можно безопасно удалить из main.js (который теперь будет пустым).

    Как правило, это хорошая идея – ожидать ваши подписки – не только для взаимодействия с пользователем, но и потому что так вы можете быть уверены, что данные всегда будут доступны в шаблоне. Это устраняет необходимость иметь дело с шаблонами, начинающими отрисовку перед тем, как их данные будут доступны.

    Также мы добавим фильтр onBeforeAction, чтобы запустить встроенный loading триггер Iron Router'а и показать шаблон загрузки, пока мы ждем.

    Финальным кусочком головоломки будет сам шаблон процесса загрузки. Для создания прекрасного анимированного индикатора загрузки мы воспользуемся пакетом sacha:spin. Добавим его командой meteor add sacha:spin и создадим шаблон loading:

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

    Заметьте, что {{>spinner}} частично содержится в spin пакете. Даже если эта часть приходит “извне” нашего приложения, мы можем вставлять его также, как и любой другой шаблон.

    Коммит 5-2

    Wait on the post subscription.

    Первый взгляд на реактивность

    Реактивность – это базовая часть Meteor, и хотя мы еще по настоящему не касались её, наш шаблон загрузки даёт первый взгляд на эту концепцию.

    Перенаправление на шаблон загрузки, пока данные загружаются, это, конечно же, хорошо. Но как маршрутизатор узнал, что нужно перенаправить пользователя обратно на правильную страницу, как только данные были получены?

    На данный момент просто скажем, что здесь однозначно работает реактивность. Но не беспокойтесь, вы скоро изучите её.

    Маршрутизация к указанному посту

    Теперь, когда мы увидели как указывать маршрут к шаблону postsList, давайте построим маршрут для отображения подробного вида одного поста.

    Есть только одна загвоздка: мы не можем определить свой отдельный маршрут для каждого поста – ведь их будет огромное множество. Вместо этого нам стоит создать один динамический маршрут, и заставить его показывать любой пост.

    Для начала мы создадим новый шаблон, который просто отрисовывает такой же шаблон поста, что мы использовали ранее в списке постов.

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

    Мы добавим больше элементов (таких, как комментарии) к этому шаблону позже, но на данный момент он будет служить простой оболочкой для нашей вставки {{> postItem}}.

    Теперь создадим другой именованный маршрут – на этот раз отображение URL-ов вида /posts/<ID> к шаблону postPage:

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

    Специальный синтаксис :_id сообщает маршрутизатору две вещи: 1) совпадает любой маршрут формы /posts/xyz/, где “xyz” может принимать любое значение; 2) положить всё что найдено на месте “xyz” внутрь свойства _id массива params маршрутизатора.

    Обратите внимание, что мы используем _id здесь только ради удобства. Маршрутизатор не имеет возможности узнать, отправляем ли мы актуальный _id или просто некоторую случайную строку символов.

    Теперь мы имеем маршрут к правильному шаблону, но нам чего-то не хватает. Маршрутизатор знает _id поста, который мы бы хотели показать, но шаблон не имеет о нём никакого представления.

    К счастью, маршрутизатор имеет неплохое встроенное решение. Он позволяет указать контекст данных (data context) для шаблона. Вы можете думать о данных контекста как о начинке в торте, который сделан из шаблонов и макетов. Проще говоря, это то, чем вы заполняете шаблон:

    The data context.
    The data context.

    В нашем случае мы получим правильный контекст данных, отыскав наш пост по _id, который мы получили из 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

    Итак, каждый раз, когда пользователь обращается по этому маршруту, мы находим соответствующий пост и отправляем его в шаблон. Напомним, что findOne возвращает единственный пост, совпавший с запросом, и ему можно передать только один аргумент id для сокращения записи {_id: id}.

    Внутри функции data для маршрута, this соответствует текущему совпавшему маршруту, и мы можем использовать this.params для доступа к именованным частям маршрута (которые мы обозначили с помощью префиксов : внутри нашего path).

    Подробнее о контекстах с данными

    Передавая контекст данных шаблону, вы можете контролировать значение this внутри методов шаблона.

    Это обычно делается неявно в итераторе {{#each}}, который автоматически устанавливает контекст данных каждой итерации к текущему элементу итерации:

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

    Но мы можем также сделать это явно, используя {{#with}}, который просто говорит: “возьми этот объект, и примени следующий шаблон к нему”. Например, мы можем написать:

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

    Оказывается, вы можете добиться того же результата отправкой контекста в качестве аргумента в месте вызова шаблона. Таким образом, предыдущий блок кода может быть переписан так:

    {{> widgetPage myWidget}}
    

    Использование методов шаблона для динамических поименованных маршрутов

    Наконец, мы должны убедиться, что правильно указываем на индивидуальный пост. Снова мы могли бы сделать как-то так <a href="/posts/{{_id}}">, но вместо этого используем метод шаблона для маршрутизации – он более надёжен.

    Мы поименовали маршрут к посту как postPage, таким образом, мы можем использовать метод шаблона {{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 btn-default">Discuss</a>
      </div>
    </template>
    
    client/views/posts/post_item.html

    Коммит 5-3

    Routing to a single post page.

    Но подождите, как именно маршрутизатор узнает, где взять часть xyz в /posts/xyz? В конце концов, мы не передаем ему любой _id.

    Оказывается, Iron Router достаточно умён, чтобы понять это самому. Мы говорим маршрутизатору использовать маршрут postPage, и маршрутизатор узнаёт, что этот маршрут требует некий параметр _id (ведь он прописан нами в определении маршрута path).

    Таким образом, маршрутизатор ищет этот _id в наиболее логичном из возможных мест: в контексте данных метода {{pathFor 'postPage'}}, другими словами в this. Выходит наш this будет соответствовать посту, который (сюрприз!) действительно обладает свойством _id.

    В качестве альтернативы вы также можете явно указать маршрутизатору, где бы вы хотели найти свойство _id, отправив второй аргумент методу шаблона (т.е. {{pathFor 'postPage' someOtherPost}}). На практике этот паттерн можно использовать, например, для получения предыдущих или следующих постов в списке.

    Чтобы убедиться, что всё работает правильно, перейдите в браузере к списку постов и кликните по какой-нибудь ссылке ‘Discuss’. Вы должны увидеть что-то подобное этому:

    A single post page.
    A single post page.

    HTML5 pushState

    Стоит понимать, что изменения адреса URL происходят с помощью технологии HTML5 pushState.

    Маршрутизатор умеет отлавливать клики на локальных линках, одновременно не давая браузеру покинуть приложение. Вместо этого маршрутизатор просто вносит изменения в состояние приложения.

    Если все работает корректно, страница должна измениться мгновенно. По факту, иногда всё меняется так быстро, что может понадобиться какая-нибудь страница перехода. Это выходит за рамки этой главы, но тем не менее эта тема интересна.