Публикация / подписка нескольких подмножеств одной и той же коллекции серверов
РЕДАКТИРОВАТЬ: этот вопрос, некоторые ответы и некоторые комментарии содержат много дезинформации. Посмотрите, как работают коллекции Meteor, публикации и подписки для точного понимания публикации и подписки на несколько подмножеств одной и той же коллекции серверов.
Как можно публиковать разные подмножества (или "представления") одной коллекции на сервере как несколько коллекций на клиенте?
Вот некоторый псевдокод, чтобы проиллюстрировать мой вопрос:
items
коллекция на сервере
Предположим, что у меня есть items
Коллекция на сервере с миллионами записей. Давайте также предположим, что:
- 50 записей имеют
enabled
свойство установлено вtrue
, а также; - 100 записей имеют
processed
свойство установлено вtrue
,
Все остальные настроены на false
,
items:
{
"_id": "uniqueid1",
"title": "item #1",
"enabled": false,
"processed": false
},
{
"_id": "uniqueid2",
"title": "item #2",
"enabled": false,
"processed": true
},
...
{
"_id": "uniqueid458734958",
"title": "item #458734958",
"enabled": true,
"processed": true
}
Код сервера
Давайте опубликуем два "представления" одной и той же коллекции серверов. Один отправит курсор с 50 записями, а другой отправит курсор с 100 записями. В этой фиктивной серверной базе данных содержится более 458 миллионов записей, и клиенту не нужно знать обо всех этих фактах (фактически, отправка их всех в этом примере, вероятно, займет несколько часов):
var Items = new Meteor.Collection("items");
Meteor.publish("enabled_items", function () {
// Only 50 "Items" have enabled set to true
return Items.find({enabled: true});
});
Meteor.publish("processed_items", function () {
// Only 100 "Items" have processed set to true
return Items.find({processed: true});
});
Код клиента
Чтобы поддержать метод компенсации задержки, мы вынуждены объявить единый сбор Items
на клиенте. Должно стать очевидным, где недостаток: как можно различить Items
за enabled_items
а также Items
за processed_items
?
var Items = new Meteor.Collection("items");
Meteor.subscribe("enabled_items", function () {
// This will output 50, fine
console.log(Items.find().count());
});
Meteor.subscribe("processed_items", function () {
// This will also output 50, since we have no choice but to use
// the same "Items" collection.
console.log(Items.find().count());
});
Мое текущее решение включает в себя создание обезьяньего патча _publishCursor, позволяющего использовать имя подписки вместо имени коллекции. Но это не сделает никакой компенсации задержки. Каждая запись должна идти в оба конца на сервер:
// On the client:
var EnabledItems = new Meteor.Collection("enabled_items");
var ProcessedItems = new Meteor.Collection("processed_items");
С обезьяньим патчем на месте, это будет работать. Но перейдите в автономный режим, и изменения не появятся на клиенте сразу - нам нужно будет подключиться к серверу, чтобы увидеть изменения.
Какой правильный подход?
РЕДАКТИРОВАТЬ: Я только что вернулся к этой теме, и я понимаю, что в моем нынешнем виде мой вопрос и ответы и множество комментариев несут много дезинформации.
То, что сводится к тому, что я неправильно понял отношения публикации и подписки. Я думал, что когда вы публикуете курсор, он попадает на клиент как отдельную коллекцию от других опубликованных курсоров, созданных из той же серверной коллекции. Это просто не то, как это работает. Идея состоит в том, что и клиент, и сервер имеют одинаковые коллекции, но то, что в коллекциях, отличается. Контракты pub-sub договариваются о том, какие документы попадают на клиента. Технически правильный ответ Тома, но он упустил несколько деталей, чтобы перевернуть мои предположения. Я ответил на тот же вопрос, что и на мой, в другой ветке SO, основываясь на объяснениях Тома, но, имея в виду мое первоначальное недопонимание стратегии pub-sub в Meteor: стратегии публикации / подписки Meteor для уникальных коллекций на стороне клиента
Надеюсь, что это поможет тем, кто пересекает эту тему и уходит в замешательстве больше всего на свете!
3 ответа
Не могли бы вы просто использовать тот же запрос на стороне клиента, когда вы хотите посмотреть на элементы?
В каталоге lib:
enabledItems = function() {
return Items.find({enabled: true});
}
processedItems = function() {
return Items.find({processed: true});
}
На сервере:
Meteor.publish('enabled_items', function() {
return enabledItems();
});
Meteor.publish('processed_items', function() {
return processedItems();
});
На клиенте
Meteor.subscribe('enabled_items');
Meteor.subscribe('processed_items');
Template.enabledItems.items = function() {
return enabledItems();
};
Template.processedItems.items = function() {
return processedItems();
};
Если вы думаете об этом, то лучше так, как будто вы вставляете (локально) элемент, который включен и обрабатывается, он может появиться в обоих списках (в отличие от, если у вас было две отдельные коллекции).
НОТА
Я понял, что был немного неясен, поэтому немного расширил это, надеюсь, это поможет.
Вы могли бы сделать две отдельные публикации, как это..
Серверные публикации
Meteor.publish("enabled_items", function(){
var self = this;
var handle = Items.find({enabled: true}).observe({
added: function(item){
self.set("enabled_items", item._id, item);
self.flush();
},
changed: function(item){
self.set("enabled_items", item._id, item);
self.flush();
}
});
this.onStop(function() {
handle.stop();
});
});
Meteor.publish("disabled_items", function(){
var self = this;
var handle = Items.find({enabled: false}).observe({
added: function(item){
self.set("disabled_items", item._id, item);
self.flush();
},
changed: function(item){
self.set("disabled_items", item._id, item);
self.flush();
}
});
this.onStop(function() {
handle.stop();
});
});
Клиентские подписки
var EnabledItems = new Meteor.Collection("enabled_items"),
DisabledItems = new Meteor.Collection("disabled_items");
Meteor.subscribe("enabled_items");
Meteor.subscribe("disabled_items");
Мне удалось достичь некоторых многообещающих предварительных результатов, подойдя к проблеме с одной публикацией / подпиской на коллекцию и используя $or
в find
запрос.
Идея состоит в том, чтобы обеспечить обертку вокруг Meteor.Collection
это позволяет вам добавлять "представления", которые в основном называются курсорами. Но на самом деле происходит то, что эти курсоры не запускаются по отдельности... их селекторы извлекаются, $or'd вместе и запускаются как один запрос и к одному pub-sub.
Он не идеален, так как смещение / лимит не будут работать с этой техникой, но в данный момент minimongo все равно его не поддерживает.
Но, в конечном счете, он позволяет вам объявлять то, что выглядит как разные подмножества одной и той же коллекции, но под капотом они являются одним и тем же подмножеством. Впереди немного абстракции, чтобы они чувствовали себя чистыми.
Пример:
// Place this code in a file read by both client and server:
var Users = new Collection("users");
Users.view("enabledUsers", function (collection) {
return collection.find({ enabled: true }, { sort: { name: 1 } });
});
Или, если вы хотите передать параметры:
Users.view("filteredUsers", function (collection) {
return collection.find({ enabled: true, name: this.search, { sort: { name: 1 } });
}, function () {
return { search: Session.get("searchterms"); };
});
Параметры задаются как объекты, потому что это одна публикация / подписка $ или вместе, мне нужен был способ получить правильные параметры, так как они смешаны вместе.
И фактически использовать это в шаблоне:
Template.main.enabledUsers = function () {
return Users.get("enabledUsers");
};
Template.main.filteredUsers = function () {
return Users.get("filteredUsers");
};
Короче говоря, я использую один и тот же код на сервере и на клиенте, и если сервер ничего не делает, клиент будет работать, или наоборот.
И самое главное, только те записи, которые вас интересуют, отправляются клиенту. Это все достижимо без уровня абстракции, просто используя $ или себя, но этот $ или станет довольно уродливым, когда будет добавлено больше подмножеств. Это просто помогает управлять этим с минимальным кодом.
Я написал это быстро, чтобы проверить это, извиняюсь за длину и отсутствие документации:
test.js
// Shared (client and server)
var Collection = function () {
var SimulatedCollection = function () {
var collections = {};
return function (name) {
var captured = {
find: [],
findOne: []
};
collections[name] = {
find: function () {
captured.find.push(([]).slice.call(arguments));
return collections[name];
},
findOne: function () {
captured.findOne.push(([]).slice.call(arguments));
return collections[name];
},
captured: function () {
return captured;
}
};
return collections[name];
};
}();
return function (collectionName) {
var collection = new Meteor.Collection(collectionName);
var views = {};
Meteor.startup(function () {
var viewName, view, pubName, viewNames = [];
for (viewName in views) {
view = views[viewName];
viewNames.push(viewName);
}
pubName = viewNames.join("__");
if (Meteor.publish) {
Meteor.publish(pubName, function (params) {
var viewName, view, selectors = [], simulated, captured;
for (viewName in views) {
view = views[viewName];
// Run the query callback but provide a SimulatedCollection
// to capture what is attempted on the collection. Also provide
// the parameters we would be passing as the context:
if (_.isFunction(view.query)) {
simulated = view.query.call(params, SimulatedCollection(collectionName));
}
if (simulated) {
captured = simulated.captured();
if (captured.find) {
selectors.push(captured.find[0][0]);
}
}
}
if (selectors.length > 0) {
return collection.find({ $or: selectors });
}
});
}
if (Meteor.subscribe) {
Meteor.autosubscribe(function () {
var viewName, view, params = {};
for (viewName in views) {
view = views[viewName];
params = _.extend(params, view.params.call(this, viewName));
}
Meteor.subscribe.call(this, pubName, params);
});
}
});
collection.view = function (viewName, query, params) {
// Store in views object -- we will iterate over it on startup
views[viewName] = {
collectionName: collectionName,
query: query,
params: params
};
return views[viewName];
};
collection.get = function (viewName, optQuery) {
var query = views[viewName].query;
var params = views[viewName].params.call(this, viewName);
if (_.isFunction(optQuery)) {
// Optional alternate query provided, use it instead
return optQuery.call(params, collection);
} else {
if (_.isFunction(query)) {
// In most cases, run default query
return query.call(params, collection);
}
}
};
return collection;
};
}();
var Items = new Collection("items");
if (Meteor.isServer) {
// Bootstrap data -- server only
Meteor.startup(function () {
if (Items.find().count() === 0) {
Items.insert({title: "item #01", enabled: true, processed: true});
Items.insert({title: "item #02", enabled: false, processed: false});
Items.insert({title: "item #03", enabled: false, processed: false});
Items.insert({title: "item #04", enabled: false, processed: false});
Items.insert({title: "item #05", enabled: false, processed: true});
Items.insert({title: "item #06", enabled: true, processed: true});
Items.insert({title: "item #07", enabled: false, processed: true});
Items.insert({title: "item #08", enabled: true, processed: false});
Items.insert({title: "item #09", enabled: false, processed: true});
Items.insert({title: "item #10", enabled: true, processed: true});
Items.insert({title: "item #11", enabled: true, processed: true});
Items.insert({title: "item #12", enabled: true, processed: false});
Items.insert({title: "item #13", enabled: false, processed: true});
Items.insert({title: "item #14", enabled: true, processed: true});
Items.insert({title: "item #15", enabled: false, processed: false});
}
});
}
Items.view("enabledItems", function (collection) {
return collection.find({
enabled: true,
title: new RegExp(RegExp.escape(this.search1 || ""), "i")
}, {
sort: { title: 1 }
});
}, function () {
return {
search1: Session.get("search1")
};
});
Items.view("processedItems", function (collection) {
return collection.find({
processed: true,
title: new RegExp(RegExp.escape(this.search2 || ""), "i")
}, {
sort: { title: 1 }
});
}, function () {
return {
search2: Session.get("search2")
};
});
if (Meteor.isClient) {
// Client-only templating code
Template.main.enabledItems = function () {
return Items.get("enabledItems");
};
Template.main.processedItems = function () {
return Items.get("processedItems");
};
// Basic search filtering
Session.get("search1", "");
Session.get("search2", "");
Template.main.search1 = function () {
return Session.get("search1");
};
Template.main.search2 = function () {
return Session.get("search2");
};
Template.main.events({
"keyup [name='search1']": function (event, template) {
Session.set("search1", $(template.find("[name='search1']")).val());
},
"keyup [name='search2']": function (event, template) {
Session.set("search2", $(template.find("[name='search2']")).val());
}
});
Template.main.preserve([
"[name='search1']",
"[name='search2']"
]);
}
// Utility, shared across client/server, used for search
if (!RegExp.escape) {
RegExp.escape = function (text) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
};
}
test.html
<head>
<title>Collection View Test</title>
</head>
<body>
{{> main}}
</body>
<template name="main">
<h1>Collection View Test</h1>
<div style="float: left; border-right: 3px double #000; margin-right: 10px; padding-right: 10px;">
<h2>Enabled Items</h2>
<input type="text" name="search1" value="{{search1}}" placeholder="search this column" />
<ul>
{{#each enabledItems}}
<li>{{title}}</li>
{{/each}}
</ul>
</div>
<div style="float: left;">
<h2>Processed Items</h2>
<input type="text" name="search2" value="{{search2}}" placeholder="search this column" />
<ul>
{{#each processedItems}}
<li>{{title}}</li>
{{/each}}
</ul>
</div>
</template>