在什么时候获取视图所依赖的 Backbone 模型?

At which point does one fetch a Backbone Model on which a view depends?

我似乎经常遇到这样的情况:我正在渲染一个视图,但该视图所依赖的模型尚未加载。大多数情况下,我只有从 URL 中获取的模型 ID,例如对于假设的市场应用程序,用户登陆该应用程序 URL:

http://example.org/#/products/product0

在我的 ProductView 中,我创建了一个 ProductModel 并设置了它的 ID,product0 然后我 fetch()。我用占位符渲染一次,当获取完成时,我重新渲染。但我希望有更好的方法。

在渲染任何东西之前等待模型加载感觉没有反应。重新渲染会导致闪烁,并且在各处添加 "loading... please wait" 或微调器会使视图模板变得非常复杂(特别是如果由于模型不存在而导致模型获取失败,或者用户无权查看页面) .

那么,当您还没有模型时,渲染视图的正确方法是什么?

我需要离开吗 来自主题标签视图并使用 pushState?服务器可以给我推送吗?我洗耳恭听。

正在从已加载的页面加载:

我觉得与直接登陆 Product 页面相比,当已经加载了一个页面时,您可以做更多的事情。

如果应用程序将 link 呈现到产品页面,比如呈现 ProductOrder 集合,还有什么可以做的吗?

<ul id="product-order-list">
  <li>Ordered 5 days ago. Product 0 <a href="#/products/product0">(see details)</a></li>
  <li>Ordered 1 month ago. Product 1 <a href="#/products/product1">(see details)</a></li>
</ul>

我处理这种 link-to-details-page 模式的自然方法是定义一个路由,它按照这些行做一些事情:

routes: {
  'products/:productid': 'showProduct'
  ...
}

showProduct: function (productid) {
  var model = new Product({_id: productid});
  var view = new ProductView({model: model});
  //just jam it in there -- for brevity
  $("#main").html(view.render().el);
}

然后我倾向于在视图的 initialize 函数中调用 fetch(),并从 this.listenTo('change', ...) 事件侦听器调用 this.render()。这会导致复杂的 render() 情况,以及对象在视图中出现和消失。例如,我的 Product 视图模板可能会为用户评论保留一些屏幕空间,但当且仅当产品上的评论是 present/enabled 时——这在模型之前通常是未知的完全从服务器获取。

现在,where/when 最好进行提取吗?

  1. 如果我在页面转换之前加载模型,它会导致直接查看代码,但会引入用户可察觉的延迟。用户将单击列表中的一个项目,并且必须等待(没有页面更改)返回模型。响应时间很重要,我还没有对此进行可用性研究,但我认为用户习惯于在单击 link.

  2. 后立即看到页面变化
  3. 如果我使用 this.model.fetch()ProductViewinitialize 中加载模型并监听模型事件,我将被迫渲染两次,- - 一次之前使用空占位符(因为否则你必须盯着白页),一次之后。如果在加载过程中发生错误,那么我必须擦除视图(显示 flickery/glitchy)并显示一些错误。

是否还有其他我没有看到的选项,可能涉及可以在视图之间重复使用的过渡加载页面?或者总是第一次调用 render() 显示一些 spinners/loading 指标是好的做法吗?

编辑:通过 collection.fetch()

加载

有人可能会建议,因为这些项目已经是列出的集合的一部分(用于呈现 link 列表的集合),所以可以在单击 link 之前获取它们, collection.fetch()。如果集合确实是Product的集合,那么渲染产品视图就很容易了。

用于生成列表的 Collection 可能不是 ProductCollection。它可能是 ProductOrderCollection 或仅具有对产品 ID 的引用的其他内容(或足够数量的产品信息以向其呈现 link)。

如果 Product 模型很大,通过 collection.fetch() 获取所有 Product 也可能会让人望而却步,尤其是。万一产品 links 之一被点击。

先有鸡还是先有蛋? collection.fetch() 方法也没有真正解决直接导航到产品页面的用户的问题...在这种情况下,我们仍然需要呈现需要 ProductProductView 页面仅从一个 ID(或产品页面 URL 中的任何内容)获取模型。

你开始说:

I have a listing of items in one Collection view

那么集合有什么..?型号..!

当您执行 collection.fetch() 时,您会检索所有模型。

当用户选择一个项目时,只需将相应的模型传递给项目视图,例如:

this.currentView = new ItemView({
  model: this.collection.find(id); // where this points to collection view
                            // and id is the id of clicked model
});

这样就不会出现任何延迟/不正确的渲染。

如果您的收集终点 returns 大量数据..怎么办?

然后实施分页、延迟加载等常见做法


I construct a Product model with the given ID

对我来说这听起来不对。如果您有一系列产品,则不应手动构建此类模型。

让集合在呈现列表视图之前获取您的模型。这样就可以避免你提到的所有问题了。

好吧,我认为有很多方法可以解决这个问题。我将列出我想到的所有内容,希望有人能与您合作,或者至少会激发您找到最佳解决方案。

我并不完全反对T J的回答。如果您只是继续在网站加载时对所有产品执行 collection.fetch() (用户通常希望涉及一些加载时间),那么您拥有所有数据,您可以只传递该数据像他提到的那样圆。他的建议与我通常所做的唯一区别是,我通常在所有视图中都会引用应用程序。因此,例如在 app.js 中的 initialize 函数中,我将执行类似的操作。

initialize: function() {
    var options = {app: this}

    var views = {
        productsView: new ProductsView(options)
    };

    this.collections = {
        products: new Products()
    }

    // This session model is just a sandbox object that I use
    // to store information about the user's session. I would
    // typically store things like currentlySelectedProductId
    // or lastViewedProduct or things like that. Then, I just
    // hang it off the app for ease of access.
    this.models = {
        session: new Session()
    }
}

然后在我的 productsView.js 初始化 函数中我会这样做:

initialize: function(options) {
    this.app = options.app;

    this.views = {
        headerView: new HeaderView(options),
        productsListView: new ProductsListView(options),
        footerView: new FooterView(options)
    }; 
}

我在productsView.js初始化时创建的子视图是任意的。我主要只是想证明我也继续将该选项对象传递给视图的子视图。

这样做是允许每个视图,无论是顶级视图还是深度嵌套的子视图,每个视图都知道其他每个视图,并且每个视图都引用应用程序数据。

这两个代码示例还介绍了尽可能精确地确定功能范围的概念。不要试图拥有一个可以完成所有事情的视图。将功能传递给其他视图,以便每个视图都有一个特定的用途。这也将促进视图的重用。特别复杂的模态。

现在回到手头的实际主题。如果您打算继续并预先加载所有产品,您应该在哪里取货?因为就像您说的那样,您不希望只是坐在用户面前的空白页面。所以,我的建议是欺骗他们。尽可能多地加载页面,并且只阻止需要加载数据的部分。这样对于用户来说,页面看起来像是在加载,而您实际上是在幕后工作。如果您可以诱使用户认为页面正在稳定加载,那么他们就不太可能对页面加载感到不耐烦。

因此,参考 productsView.js 中的初始化,您可以继续让 headerView 和 footerView 呈现。然后,您可以在 productsListView 的渲染中进行提取。

现在,我知道你在想什么。我是不是疯了。如果您在呈现函数中进行提取,那么在我们点击实际呈现 productsViewList 模板的行之前,调用将没有时间到达 return。好吧,幸运的是,有几种方法可以解决这个问题。一种方法是使用 Promises。然而,我通常这样做的方式是只使用渲染函数作为它自己的回调。让我告诉你。

render: function(everythingLoaded) {
    var _this = this;

    if(!!everythingLoaded) {
        this.$el.html(_.template(this.template(this)));
    }
    else {
        // load a spinner template here if you want a spinner

        this.app.collection.products.fetch()
            .done(function(data) {
                _this.render(true);
            })
            .fail(function(data) {
                console.warn('Error: ' + data.status);
            });
    }

    return this;
}

现在,通过以这种方式构建我们的渲染,在数据完全加载之前不会加载实际模板。

虽然我们这里有一个渲染函数,但我想介绍另一个我在任何地方都用到的概念。我称之为 post渲染。这是一个函数,在模板加载完成后,我执行任何依赖 DOM 元素的代码。如果您只是编写一个普通的 .html 页面,那么这是传统上位于 $(document).ready(function() {}); 中的代码。可能值得注意的是,我的模板没有使用 .html 文件。我使用 嵌入式 javascript 文件 (.ejs)。继续,postRender 函数是我基本上添加到样板代码中的函数。因此,每当我在代码库中为视图调用 render 时,我都会立即将 postRender 链接到它上面。我还使用 postRender 作为对自身的回调,就像我对渲染所做的那样。所以,基本上,前面的代码示例在我的代码库中看起来像这样。

render: function(everythingLoaded) {
    var _this = this;

    if(!!everythingLoaded) {
        this.$el.html(_.template(this.template(this)));
    }
    else {
        // load a spinner template here if you want a spinner

        this.app.collection.products.fetch()
            .done(function(data) {
                _this.render(true).postRender(true);
            })
            .fail(function(data) {
                console.warn('Error: ' + data.status);
            });
    }

    return this;
},

postRender: function(everythingLoaded) {
    if(!!everythingLoaded) {
        // do any DOM manipulation to the products list after 
        // it's loaded and rendered
    }
    else {
        // put code to start spinner
    }

    return this;
}

通过像这样链接这些函数,我们保证它们会 运行 顺序。

============================================= ============================

所以,这是解决问题的一种方法。但是,您提到您不必预先加载所有产品,因为担心请求会花费太长时间。


旁注:您真的应该考虑删除与产品调用相关的任何信息,这些信息可能会导致调用花费大量时间,并制作更大的片段信息是一个单独的请求。我有一种感觉,如果您可以真正快速地为他们提供核心信息,并且如果与每个产品相关的缩略图需要更长的时间来加载,那么用户将更容忍需要一段时间才能加载的数据,那么它不应该结束世界。这只是我的意见。


解决这个问题的另一种方法是,如果您只想转到特定的产品页面,那么只需实施我上面概述的 render/postRender 模式 个别产品视图。但是请注意,您的 productView.js 可能看起来像这样:

initialize: function(options) {
    this.app = options.app;
    this.productId = options.productId;

    this.views = {
        headerView: new HeaderView(options),
        productsListView: new ProductsListView(options),
        footerView: new FooterView(options)
    }; 
}

render: function(everythingLoaded) {
    var _this = this;

    if(!!everythingLoaded) {
        this.$el.html(_.template(this.template(this)));
    }
    else {
        // load a spinner template here if you want a spinner

        this.app.collection.products.get(this.productId).fetch()
            .done(function(data) {
                _this.render(true).postRender(true);
            })
            .fail(function(data) {
                console.warn('Error: ' + data.status);
            });
    }

    return this;
},

postRender: function(everythingLoaded) {
    if(!!everythingLoaded) {
        // do any DOM manipulation to the product after it's 
        // loaded and rendered
    }
    else {
        // put code to start spinner
    }

    return this;
}

这里唯一的区别是 productId 在选项对象中传递给初始化,然后被拉出并在渲染函数的 .fetch 中使用。

============================================= ============================ 总之,我希望这会有所帮助。我不确定我是否已经回答了您所有的问题,但我认为我已经很好地解决了这些问题。由于时间太长,我暂时停在这里,让您消化一下并提出您的任何问题。我想我可能必须对此 post 进行至少 1 次更新才能进一步清除它。