发布/订阅相同服务器集合的多个子集

编辑:这个问题,一些答案,和一些评论,包含了大量的错误信息。 了解Meteor集合,出版物和订阅如何正确理解发布和订阅同一服务器集合的多个子集。


如何在服务器上将单个集合的不同子集(或“视图”)作为客户端上的多个集合发布?

下面是一些伪代码来帮助说明我的问题:

items集合在服务器上

假设我在服务器上拥有数百万条logging的items集合。 我们还假设:

  1. 50条logging的enabled属性设置为true
  2. 100条logging的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个logging的光标,另一个发送100个logging的光标。 在这个虚拟的服务器端数据库中有超过4.58亿条logging,客户端不需要知道所有这些(事实上,发送它们全部可能在这个例子中需要几个小时):

 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 。 它应该变得明显的缺点是:如何区分enabled_items Itemsprocessed_items 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()); }); 

我目前的解决scheme涉及猴子补丁_publishCursor允许使用订阅名称,而不是集合名称。 但是这不会做任何延迟补偿。 每个写都必须往返服务器:

 // On the client: var EnabledItems = new Meteor.Collection("enabled_items"); var ProcessedItems = new Meteor.Collection("processed_items"); 

随着猴子补丁,这将工作。 但是进入离线模式,更改不会立即显示在客户端 – 我们需要连接到服务器才能看到更改。

什么是正确的方法?


编辑:我只是重新审视这个线程,我意识到,现在,我的问题和答案和过多的评论带有大量的错误信息。

结果是我误解了发布 – 订阅关系。 我认为,当你发布一个游标时,它将作为与源自相同服务器集合的其他已发布游标的单独集合登陆到客户端。 这根本不是它是如何工作的。 这个想法是,客户端和服务器都有相同的集合,但集合中的内容是不同的。 发布 – 订阅合同谈判哪些文件最终在客户端。 汤姆的答案在技术上是正确的,但是错过了一些细节来扭转我的假设。 根据Tom的解释,我在另一个SO线程中回答了类似的问题,但是请记住我最初对Meteor的pub-sub的误解: 独特客户端集合的meteor发布/订阅策略

希望这有助于那些跑过这个线程的人们,而且比任何事情都更加困惑!

你能不能只使用相同的查询客户端,当你想看的项目?

在一个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的包装,允许你添加“视图”,这基本上是命名游标。 但是真正发生的事情是这些游标不是单独运行的……它们的select器被提取出来,或者一起运行,并作为单个查询运行到一个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"); }; }); 

参数是以对象的forms给出的,因为它是一个发布/订阅$或者一起,我需要一种方法来获得正确的参数,因为它们混合在一起。

而要真正在模板中使用它:

 Template.main.enabledUsers = function () { return Users.get("enabledUsers"); }; Template.main.filteredUsers = function () { return Users.get("filteredUsers"); }; 

总之,我利用在服务器和客户端都运行相同的代码,如果服务器没有做什么,客户端将会或者反过来。

而最重要的是,只有你感兴趣的logging才会被发送到客户端。 没有一个抽象层,只要做$或者你自己就可以实现,但是如果有更多的子集被添加,$或者会变得非常难看。 这只是帮助用最less的代码来pipe理它。

我很快就写了这个文件来testing它,并对文件的长度和缺乏表示歉意。

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>