Публикации и подписки

Отступление 4.5

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

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

  • Поймете, как работают публикации и подписки.
  • Узнаете, что делает пакет autopublish.
  • Увидите еще несколько примеров реализации публикаций и подписок.
  • Система публикаций и подписок - это одна из главных концепций Meteor, и с первого раза иногда бывает сложно понять ее полностью.

    Это приводит ко многим заблуждениям, таким как уверенность в том, что Meteor - небезопасен, или что Meteor-приложения не в состоянии справляться с большим количеством данных.

    Главная причина того, что людям эти вещи сперва казались непонятными - это именно то «волшебство», что Meteor делает за нас. Хотя это самое волшебство на деле крайне полезно, за ним не видно процессов, происходящих за кулисами (на то оно и волшебство). Так что давайте попробуем заглянуть по-глубже и понять, что же на самом деле происходит.

    Былые дни

    Сперва давайте вернемся в старый добрый 2011 год, когда Meteor еще не существовал. Представим, что вы решили создать простое приложение на Ruby on Rails. Когда пользователь заходит на сайт, клиент (его браузер) посылает запрос на сервер, где живет ваше приложение.

    Первое, что в этом случае должно сделать приложение - это понять, какие данные пользователь должен увидеть. Это может быть, например, 12-ая страница поиска, или страница профиля некой Маши, или последние 20 твитов Боба и т.д. Это похоже на то, как продавец книжного магазина ищет на полках книгу, которую вы попросили.

    Как только нужная информация найдена, следующее, что должно сделать приложение - это предоставить эту информацию в виде HTML (или JSON, в случае API).

    Если опять проводить параллели с книжным магазином, то наш воображаемый продавец на данном этапе оборачивает книгу, которую вы купили и кладет ее в пакет. Это и называется «View» в популярной архитектуре «Model-View-Controller».

    Наконец, приложение отправляет готовый HTML в браузер пользователя. Работа приложения выполнена, и теперь оно может, откинувшись, глотнуть пива, пока не придет следующий запрос от браузера.

    The Meteor Way

    Теперь давайте посмотрим, что же такое особенное делает Meteor, по сравнению с предыдущим подходом. Как мы уже поняли, ключевая инновация Meteor состоит в том, что, в то время, как приложение Ruby on Rails живет только на сервере, он имеет еще и клиентскую часть, которая исполняется в браузере.

    Отправляем подмножество данных клиенту.
    Отправляем подмножество данных клиенту.

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

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

    Такой подход имеет 2 больших отличия: во-первых, вместо того, чтобы отправлять уже готовый HTML, Meteor отправляет собственно сами данные, и дает коду на клиенте возможность самому распоряжаться этими данными как он пожелает (data on a wire). Во-вторых, операции с этими данными будут происходить практически мгновенно, так как вам не нужно будет постоянно слать запросы на сервер (latency compensation).

    Публикации

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

    Так что нам нужен способ, с помощью которого мы сможем отправлять на клиент только необходимые данные. Этот способ - публикации (publications).

    Давайте вернемся к Microscope. Вот все наши посты, которые есть в БД:

    Все посты в нашей базе данных.
    Все посты в нашей базе данных.

    Хотя этот функционал мы еще не реализовали, давайте все же представим, что некоторые посты были отмечены как оскорбительные. Хотя мы и не будем их удалять из самой БД, они все же не должны показываться пользователям (т.е. не нужно отправлять их на клиент).

    Первое, что мы должны сделать - это сообщить Meteor, какие данные мы хотим отправить на клиент. В данном случае - что мы хотим отправить только те посты, что не отмечены как оскорбительные.

    Исключаем помеченные посты.
    Исключаем помеченные посты.

    Вот серверный код, который отвечает за это:

    // on the server
    Meteor.publish('posts', function() {
      return Posts.find({flagged: false});
    });
    

    Этот код обезопасит нас от того, что клиент будет иметь доступ к отмеченным постам. Это именно тот способ, которым можно сделать Meteor-приложения безопасными: просто убедитесь, что вы публикуете только те данные, к которым вы хотите предоставить доступ на клиенте.

    DDP

    Базово можно рассматривать публикации и подписки как трубу, через которую данные из коллекции на сервере перетекают в коллекцию на клиенте.

    Протокол, который собственно и является такой трубой, называется DDP (Distributed Data Protocol). Чтобы узнать больше о нем, вы можете посмотреть вот эту речь одного из основателей Meteor Matt DeBergalis, либо этот скринкаст за авторством Chris Mather, который более детально раскроет вам основные концепции этого протокола.

    Подписки

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

    Любые данные, на которые вы подписываетесь, будут отражены на клиенте благодаря MiniMongo.

    К примеру, давайте предположим, что мы просматриваем профиль некоего Bob Smith, и хотим, чтобы отображались только его посты.

    Подписываясь на посты Bob'а мы реплицируем их на клиенте.
    Подписываясь на посты Bob'а мы реплицируем их на клиенте.

    Сперва, мы немного изменим нашу публикацию так, чтобы она принимала параметр:

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

    Далее мы укажем этот параметр уже в подписке:

    // on the client
    Meteor.subscribe('posts', 'bob-smith');
    

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

    Выборка

    Теперь посты Bob’а, которые мы получили, распределены по всем категориям (например: «JavaScript», «Ruby» и «Python»). Может мы и хотим загрузить все его посты в память браузера, прямо сейчас мы хотим отображать только те, что принадлежат категории «JavaScript». Как раз вот здесь и нужна выборка:

    Выделяем подмножество документов на клиенте
    Выделяем подмножество документов на клиенте

    Также как и на сервере, мы будем использовать для этого метод Posts.find().

    // on the client
    Template.posts.helpers({
      posts: function(){
        return Posts.find({author: 'bob-smith', category: 'JavaScript'});
      }
    });
    

    Теперь, когда мы имеем четкое представление роли публикаций и подписок в Meteor-приложении, давайте пойдем еще дальше и посмотрим несколько способов реализации этого механизма.

    Autopublish

    Если вы создаете Meteor-приложение с нуля (используя meteor create), оно будет автоматически включать в себя пакет autopublish. Для начала, давайте разберем, что же именно он делает.

    Главная назначение модуля autopublish - это сделать максимально простым и удобным начало процесса разработки вашего Meteor-приложения. Он автоматически отражает все серверные данные на клиенте, позаботившись за вас о публикациях и подписках.

    Autopublish
    Autopublish

    Как это работает? Если у вас есть коллекция под названием posts на сервере, то пакет autopublish автоматически отправит каждый пост, который он найдет в этой коллекции MongoDB в одноименную коллекцию на клиенте (если она конечно существует).

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

    Поэтому, использование autopublish имеет смысл только в начале процесса разработки, пока вы еще не думаете о публикациях.

    Публикация коллекций целиком

    Как только вы уберете пакет autopublish, вы увидете, что все данные исчезли с клиента. Самый простой способ вернуть их - это просто воспроизвести то же, что делает autopublish - опубликовать коллекцию целиком. Например:

    Meteor.publish('allPosts', function(){
      return Posts.find();
    });
    
    Публикация полной коллекции
    Публикация полной коллекции

    Да, все еще публикуем коллекции целиком, но по крайней мере мы можем контролировать, какие коллекции мы публикуем, а какие нет. В данном случае, мы публикуем коллекцию Posts, но не публикуем Comments.

    Публикация частей коллекции

    Следующий уровень контроля - публикация только части коллекции. К примеру, только постов, принадлежащих конкретному автору:

    Meteor.publish('somePosts', function(){
      return Posts.find({'author':'Tom'});
    });
    
    Частичная публикация коллекции
    Частичная публикация коллекции

    За кулисами

    Если вы читали документацию Meteor по этой теме, вы должно быть знаете об использовании методов added() или ready() для того, чтобы задавать атрибуты записей на клиенте и изо всех сил пытались понять, почему же во всех Meteor-приложениях, которые вы видели, они никогда не используются.

    Причина в том, что Meteor предоставляет очень полезный метод: _publishCursor(). Вы также его еще не встречали, верно? Но это именно тот метод, который Meteor использует, когда вы возвращаете курсор публикации (например: Posts.find({‘author’: ‘Tom’})).

    Когда Meteor видит, что публикация somePosts возвращает курсор, он вызывает метод _publishCursor(), чтобы (как вы и догадались) опубликовать курсор автоматически.

    Вот что делает этот метод:

    • Проверяет имя коллекции на сервере.
    • Достает все соответствующие документы из курсора и отправляет их в одноименную коллекцию на клиенте. (Он использует метод .added() для этого).
    • Каждый раз, когда документ добавлен, удален или изменен, он сообщает об этих изменениях коллекции на клиенте, используя метод .observe() на курсоре и .added(), .changed() или .removed().

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

    Публикуем свойства документов частично

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

    Как и в прошлый раз, мы используем метод .find() чтобы вернуть курсор, но на этот раз мы исключим некоторые свойства:

    Meteor.publish('allPosts', function(){
      return Posts.find({}, {fields: {
        date: false
      }});
    });
    
    Частичная публикация свойств (полей)
    Частичная публикация свойств (полей)

    Конечно, мы можем совмещать обе техники. Например, если мы хотим получить все посты от Tom’а, но при этом оставляя в стороне даты, мы напишем:

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

    Подводим итоги

    Итак, мы прошли путь от публикации всех свойств всех документов всех коллекций (используя autopublish) к публикации части свойств части документов части коллекций.

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

    Иногда вы вынуждены идти еще дальше, объединяя или совмещая публикации. Мы это затронем в следующей главе!