36

编辑:这个问题、一些答案和一些评论包含很多错误信息。了解Meteor 集合、发布和订阅如何工作,以准确了解发布和订阅同一服务器集合的多个子集。


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

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

items服务器上的采集

假设我items在服务器上有一个包含数百万条记录的集合。我们还假设:

  1. 50 条记录的enabled属性设置为true, 和;
  2. 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 条记录的游标。在这个虚构的服务器端数据库中有超过 4.58 亿条记录,客户端不需要知道所有这些(实际上,在这个示例中,将它们全部发送下来可能需要几个小时):

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在客户端声明一个集合。缺陷在哪里应该很明显:如何区分Itemsforenabled_itemsItemsfor 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 线程中回答了与我类似的问题,但请记住我最初对 Meteor 的 pub-sub 的误解:

希望这可以帮助那些遇到这个线程并且比任何事情都更加困惑的人!

4

3 回答 3

34

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

在 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();
};

如果您考虑一下,最好以这种方式插入(本地)一个同时启用和处理的项目,它可以出现在两个列表中(与如果您有两个单独的集合相反)。

笔记

我意识到我有点不清楚,所以我已经扩展了一点,希望它有所帮助。

于 2012-09-28T07:11:58.530 回答
6

您可以像这样制作两个单独的出版物..

服务器出版物

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");
于 2012-09-30T18:16:02.297 回答
1

$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"); };
});

参数作为对象给出,因为它是一个单一的发布/订阅 $or'd 在一起,我需要一种方法来获取正确的参数,因为它们混合在一起。

并在模板中实际使用它:

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

简而言之,我利用在服务器和客户端中运行相同的代码的优势,如果服务器没有做某事,客户端会做,反之亦然。

最重要的是,只有您感兴趣的记录才会发送给客户。这一切都可以在没有抽象层的情况下通过简单地自己执行 $or 来实现,但是随着更多子集的添加,$or 会变得非常难看。这只是帮助用最少的代码管理它。

我很快写了这个来测试它,对篇幅和缺乏文档表示歉意:

测试.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, "\\$&");
    };
}

测试.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>
于 2012-10-02T03:35:19.223 回答