36

編集: この質問、回答の一部、およびコメントの一部には、多くの誤った情報が含まれています。Meteor のコレクション、パブリケーション、およびサブスクリプションがどのように機能するかを確認して、同じサーバー コレクションの複数のサブセットに対するパブリッシュとサブスクライブを正確に理解してください。


サーバー上の 1 つのコレクションのさまざまなサブセット (または「ビュー」) を、クライアント上の複数のコレクションとして公開するにはどうすればよいでしょうか?

私の質問を説明するのに役立つ疑似コードを次に示します。

itemsサーバー上のコレクション

itemsサーバー上に何百万ものレコードを含むコレクションがあるとします。また、次のように仮定します。

  1. 50 レコードのenabledプロパティがtrue, and に設定されています。
  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
}

サーバーコード

同じサーバー コレクションの 2 つの「ビュー」を公開してみましょう。1 つは 50 レコードのカーソルを送信し、もう 1 つは 100 レコードのカーソルを送信します。この架空のサーバー側データベースには 4 億 5,800 万件を超えるレコードがあり、クライアントはそれらすべてについて知る必要はありません (実際、この例では、それらすべてを送信するにはおそらく数時間かかります)。

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に、クライアントで 1 つのコレクションを宣言する必要があります。Items欠陥がどこにあるかが明らかになるはずです: forenabled_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 コントラクトは、最終的にクライアントに送信されるドキュメントをネゴシエートします。トムの答えは技術的には正しいですが、私の仮定を変えるためにいくつかの詳細が欠けていました。Tom の説明に基づいて、別の 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();
};

考えてみると、有効化と処理の両方が行われているアイテムを(ローカルで)挿入する場合は、両方のリストに表示される可能性があります(2つの別々のコレクションがある場合とは対照的です)。

ノート

少し不明瞭だと気づいたので、少し拡大してみました。お役に立てば幸いです。

于 2012-09-28T07:11:58.530 に答える
6

このように2つの別々の出版物を作ることができます..

サーバーの出版物

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

コレクションごとに 1 つのパブリッシュ/サブスクライブで問題に取り組み、$orクエリを活用するfindことで、いくつかの有望な予備結果を得ることができました。

Meteor.Collection基本的にはカーソルという名前の「ビュー」を追加できるラッパーを提供するという考え方です。しかし、実際に起こっているのは、これらのカーソルが個別に実行されないということです...それらのセレクターが抽出され、一緒に $or され、単一のクエリとして単一の 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 はかなり醜くなります。これは、最小限のコードで管理するのに役立ちます。

私はこれをテストするために簡単に書きました。長さとドキュメントの欠如をお詫びします。

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>
于 2012-10-02T03:35:19.223 に答える