-
Notifications
You must be signed in to change notification settings - Fork 39
/
Copy path13s-advanced-publications.md.erb
181 lines (122 loc) · 15.8 KB
/
13s-advanced-publications.md.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
---
title: Продвинутые Публикации
slug: advanced-publications
date: 0013/01/02
number: 13.5
sidebar: true
contents: Узнаете продвинутые способы работы с публикациями.|Увидите, насколько гибкими могут быть публикации и подписки.
paragraphs: 36
---
К этому моменту у вас должно быть хорошее представление о том, как между собой работают публикации и подписки. Давайте отбросим в сторону все ограничения и рассмотрим несколько продвинутых примеров.
### Многократная публикация одной коллекции
В [нашей первой главе про публикации](/chapters/publications-and-subscriptions/) мы познакомились с наиболее распространенными способами работы с публикациями и подписками. Также мы узнали, как функция `_publishCursor` позволяет легко создавать их для наших сайтов.
Давайте вспомним, как именно работает `_publishCursor`. Эта функция находит все документы совпадающие с текущим курсором, и отправляет их в коллекцию на клиенте с *тем же именем*. Обратите внимание, что имя _публикации_ здесь не играет никакой роли.
Это означает что мы можем иметь _больше одной публикации_, соединяющей коллекции на клиенте и сервере.
Мы уже видели этот подход в действии в [главе разбиения постов на страницы](/chapters/pagination/), когда публиковалось подмножество постов одновременно с отдельно выбранным постом.
Другим похожим сценарием является публикация *краткого содержания* большого списка документов, одновременно с подробными деталями отдельно выбранного документа.
<%= diagram "doublecollection", "Двойная публикация коллекции", "pull-center" %>
~~~js
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 умеет совмещать результаты таких публикаций в один и тот же документ на клиенте, просто объединяя поля и не создавая копий.
Отлично, теперь при отрисовке списка с кратким содержанием постов у нас будет ровно столько данных, сколько требуется. В то же время, когда мы будем загружать страницу с одним конкретным постом, у нас снова будет все, что нужно для ее отображения. Конечно же, нам нужно позаботиться о том, чтобы клиент не ожидал мгновенной доступности всех полей у всех постов - это довольно распространенная загвоздка.
Стоит упомянуть, что вы не ограничены в вариациях параметров у публикуемых документов. Вы можете опубликовать одни и те же поля в обоих публикациях, но упорядочить сам список публикуемых документов по-разному.
~~~js
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});
});
~~~
<%= caption "server/publications.js" %>
### Многократная подписка на одну публикацию
Мы только что наблюдали, как можно опубликовать коллекцию более одного раза. Оказывается, похожего результата можно добиться и другим способом: создать одну публикацию, но подписаться на нее *несколько* раз.
В Microscope мы подписываемся на публикацию `posts` несколько раз, но Iron Router заботится об этих подписках за нас. Не смотря на это, нам ничто не мешает подписаться несколько раз *одновременно*.
Скажем, нам нужно загрузить одновременно самые последние и самые лучшие посты:
<%= diagram "subscribetwice", "Подписываемся дважды на одну публикацию", "pull-center" %>
Мы создаем одну публикацию:
~~~js
Meteor.publish('posts', function(options) {
return Posts.find({}, options);
});
~~~
Затем мы подписываемся на эту публикацию несколько раз. Это примерно то же самое, что мы делаем в Microscope:
~~~js
Meteor.subscribe('posts', {submitted: -1, limit: 10});
Meteor.subscribe('posts', {baseScore: -1, submitted: -1, limit: 10});
~~~
Что же тут происходит? Каждый браузер открывает *две* разных подписки, каждая из которых присоединяется к *одной и той же* публикации на сервере.
Каждая подписка задает разные аргументы, и для каждой подписки свой список документов собирается на сервере и отправляется в коллекцию клиента.
### Несколько коллекций в одной подписке
В отличие от традиционных реляционных баз данных как MySQL, которые используют *объединения данных* (*joins*), базы данных типа NoSQL вроде Mongo используют *денормализацию* и *внедрение*. Давайте рассмотрим, как это работает в случае с Meteor.
Взглянем на конкретный пример. Мы добавили комментарии к постам, и пока что были вполне довольны тем, как комментарии публикуются только к одному конкретному посту, открытому в данный момент пользователем.
Предположим, мы хотели бы отобразить *все* комментарии ко *всем* постам на главной странице (эти посты будут меняться, когда мы будем пролистывать страницы списка). Этот случай дает хороший повод внедрить комментарии в посты. По этой же причине мы когда-то денормализовали значение количества постов в документ с постом.
Конечно, мы могли бы просто внедрить комментарии в посты, и избавиться от коллекции `Comments`. Однако, как мы уже помним из главы *Денормализация*, это лишило бы нас многих приятных фишек работы с отдельной коллекцией.
Оказывается, есть один трюк с подпиской, который позволяет внедрить комментарии в посты, одновременно сохраняя свою коллекцию с комментариями.
Предположим что одновременно со списком постов на главной странице мы хотели бы подписаться на список с 2 последними комментариями к каждому посту.
Если бы комментарии были отдельной публикацией, добиться подобной функциональности было бы сложно. Особенно, если список постов был бы как-то ограничен (например, до 10 последних постов). Нам пришлось бы написать публикацию вроде такой:
<%= diagram "multiplecollections", "Две коллекции в одной подписке", "pull-center" %>
~~~js
Meteor.publish('topComments', function(topPostIds) {
return Comments.find({postId: topPostIds});
});
~~~
Это было бы проблемой с точки зрения быстродействия, ведь публикацию пришлось бы разрывать и создавать заново каждый раз, когда меняется список `topPostIds`.
Эту проблему можно обойти, если принять тот факт, что можно не только иметь несколько *публикаций* для одной *коллекции*, но и несколько *коллекций* на одну *публикацию*:
~~~js
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`, на нас больше не распространяется ограничение, что коллекция на сервере должна иметь то же самое имя и на клиенте.
<%= diagram "linkedcollections", "Одна коллекция для двух подписок", "pull-center" %>
Одна из причин по которой стоит воспользоваться этой фишкой это паттерн *Single Table Inheritance* - *Наследование в одной таблице*.
Предположим, мы хотели бы ссылаться на различные типы объектов из наших постов. У каждого из типов были бы общие поля, в добавок к своим уникальным полям. Например, мы могли бы создать движок для блога в стиле Tumblr, где у каждого поста может быть ID, время создания, заголовок, но также может быть картинка, видео, линк или просто текст.
Не смотря на то, что у нас одна единственная коллекция `Resources` на сервере, мы можем превратить ее в несколько коллекций на клиенте типа `Видео`, `Картинки` и так далее. В этом нам помогут несколько волшебных строк кода:
~~~js
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.