Продвинутые Публикации

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

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

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

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

    Многократная публикация одной коллекции

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

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

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

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

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

    Двойная публикация коллекции
    Двойная публикация коллекции
    Meteor.publish('allPosts', function() {
      return Posts.find({}, {fields: {title: true, author: true}});
    });
    
    Meteor.publish('postDetail', function(postId) {
      return Posts.find(postId);
    });
    

    Теперь, когда клиент подписан на обе публикации (мы используем autorun чтобы быть уверенными, что подписка postDetail получает верное значение параметра postId), его коллекция 'posts' пополняется одновременно из двух источников - список заголовков и имена авторов из первой подписки, а также подробные детали поста из второй.

    Вы наверное уже догадались, что один и тот же пост публикуется через обе публикации. Первая - allPosts - опубликует только часть полей поста, в то время как вторая - postDetail - опубликует его целиком. К счастью, Meteor умеет совмещать результаты таких публикаций в один и тот же документ на клиенте, просто объединяя поля и не создавая копий.

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

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

    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

    Многократная подписка на одну публикацию

    Мы только что наблюдали, как можно опубликовать коллекцию более одного раза. Оказывается, похожего результата можно добиться и другим способом: создать одну публикацию, но подписаться на нее несколько раз.

    В Microscope мы подписываемся на публикацию posts несколько раз, но Iron Router заботится об этих подписках за нас. Не смотря на это, нам ничто не мешает подписаться несколько раз одновременно.

    Скажем, нам нужно загрузить одновременно самые последние и самые лучшие посты:

    Подписываемся дважды на одну публикацию
    Подписываемся дважды на одну публикацию

    Мы создаем одну публикацию:

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

    Затем мы подписываемся на эту публикацию несколько раз. Это примерно то же самое, что мы делаем в Microscope:

    Meteor.subscribe('posts', {submitted: -1, limit: 10});
    Meteor.subscribe('posts', {baseScore: -1, submitted: -1, limit: 10});
    

    Что же тут происходит? Каждый браузер открывает две разных подписки, каждая из которых присоединяется к одной и той же публикации на сервере.

    Каждая подписка задает разные аргументы, и для каждой подписки свой список документов собирается на сервере и отправляется в коллекцию клиента.

    Несколько коллекций в одной подписке

    В отличие от традиционных реляционных баз данных как MySQL, которые используют объединения данных (joins), базы данных типа NoSQL вроде Mongo используют денормализацию и внедрение. Давайте рассмотрим, как это работает в случае с Meteor.

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

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

    Конечно, мы могли бы просто внедрить комментарии в посты, и избавиться от коллекции Comments. Однако, как мы уже помним из главы Денормализация, это лишило бы нас многих приятных фишек работы с отдельной коллекцией.

    Оказывается, есть один трюк с подпиской, который позволяет внедрить комментарии в посты, одновременно сохраняя свою коллекцию с комментариями.

    Предположим что одновременно со списком постов на главной странице мы хотели бы подписаться на список с 2 последними комментариями к каждому посту.

    Если бы комментарии были отдельной публикацией, добиться подобной функциональности было бы сложно. Особенно, если список постов был бы как-то ограничен (например, до 10 последних постов). Нам пришлось бы написать публикацию вроде такой:

    Две коллекции в одной подписке
    Две коллекции в одной подписке
    Meteor.publish('topComments', function(topPostIds) {
      return Comments.find({postId: topPostIds});
    });
    

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

    Эту проблему можно обойти, если принять тот факт, что можно не только иметь несколько публикаций для одной коллекции, но и несколько коллекций на одну публикацию:

    Meteor.publish('topPosts', function(limit) {
      var sub = this, commentHandles = [], postHandle = null;
    
      // посылаем два последних комментария одного поста
      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) {
          // прекращаем следить за изменениями к коментариям поста
          commentHandles[id] && commentHandles[id].stop();
          // удаляем пост
          sub.removed('posts', id);
        }
      });
    
      sub.ready();
    
      // проверяем что мы все почистили (внимание: `_publishCursor`
      // делает это за нас с помощью наблюдателей за комментариями)
      sub.onStop(function() { postsHandle.stop(); });
    });
    

    Обратите внимание, мы ничего не возвращаем в этой публикации. Вместо этого посылается сообщение в sub с помощью функции .added() и ее друзей. Так что нам не нужно просить _publishCursor сделать это за нас с помощью возвращаемого курсора.

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

    Не смотря на то, что Meteor мало рекламирует подобный подход, вы можете также ознакомиться с пакетом publish-with-relations на Atmosphere, который позволяет легко использовать подобный сценарий.

    Объединяем разные коллекции

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

    Одна коллекция для двух подписок
    Одна коллекция для двух подписок

    Одна из причин по которой стоит воспользоваться этой фишкой это паттерн Single Table Inheritance - Наследование в одной таблице.

    Предположим, мы хотели бы ссылаться на различные типы объектов из наших постов. У каждого из типов были бы общие поля, в добавок к своим уникальным полям. Например, мы могли бы создать движок для блога в стиле Tumblr, где у каждого поста может быть ID, время создания, заголовок, но также может быть картинка, видео, линк или просто текст.

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

      Meteor.publish('videos', function() {
        var sub = this;
    
        var videosCursor = Resources.find({type: 'video'});
        Meteor.Collection._publishCursor(videosCursor, sub, 'videos');
    
        // _publishCursor не вызывает следующую функцию за нас в случае если мы вызываем данный код несколько раз
        sub.ready();
      });
    

    Мы сообщаем _publishCursor публиковать наши видео, точно так же, как это делал бы курсор - но вместо публикации в коллекцию resources на клиенте, мы публикуем их в коллекцию 'videos'.

    Хорошая ли это идея? Не будем высказывать здесь мнений. В любом случае неплохо знать то, на что способен Meteor.