5

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

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

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

在渲染任何东西之前等待模型加载感觉没有响应。重新渲染会导致闪烁,并且到处添加“正在加载...请稍候”或微调器会使视图模板非常复杂(尤其是如果模型获取失败,因为模型不存在,或者用户无权查看这页纸)。

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

我是否需要远离主题标签视图并使用pushState?服务器可以给我推送吗?我全是耳朵。

从已经加载的页面加载:

我觉得当已经加载了一个页面而不是直接登陆页面时,你可以做更多的事情Product

如果应用程序呈现一个产品页面的链接,比如呈现一个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>

我处理这种链接到详细信息页面模式的自然方法是定义一个路线,该路线沿着这些路线做一些事情:

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

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

现在,在哪里/何时最好进行提取?

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

  2. 如果我在ProductView's中加载模型initializethis.model.fetch()并监听模型事件,我将被迫渲染两次,一次之前使用空占位符(因为否则你必须盯着白页),然后一次。如果在加载过程中发生错误,那么我必须擦除视图(出现闪烁/故障)并显示一些错误。

是否有另一个我没有看到的选项,可能涉及可以在视图之间重用的过渡加载页面?或者总是第一次调用render()显示一些微调器/加载指示器是一种好习惯?

编辑:通过加载collection.fetch()

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

然而,Collection用于生成列表的可能不是ProductCollection。它可能是一个ProductOrderCollection或其他东西,它只是对产品 ID 的引用(或一些足够数量的产品信息来呈现指向它的链接)。

如果模型很大,尤其是,Product通过 a获取所有内容collection.fetch()也可能令人望而却步。Product万一某个产品链接被点击。

鸡还是鸡蛋?该collection.fetch()方法也不能真正解决直接导航到产品页面的用户的问题......在这种情况下,我们仍然需要渲染一个ProductView页面,该页面需要Product仅从一个 id(或产品页面中的任何内容)获取模型网址)。

4

2 回答 2

3

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

我并不完全反对 T J 的回答。如果您在网站加载时继续对所有产品执行 collection.fetch() (用户通常希望涉及一些加载时间),那么您拥有所有数据,您可以像传递该数据一样他提到。他的建议和我通常做的唯一区别是我通常在所有观点中都提到了应用程序。所以,例如在我的app.js中的初始化函数中,我会做这样的事情。

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 的渲染中进行获取。

现在,我知道你在想什么。我是不是失去理智了。如果您在渲染函数中进行 fetch,那么在我们点击实际渲染 productsViewList 模板的行之前,调用将没有时间返回。好吧,幸运的是,有几种方法可以解决这个问题。一种方法是使用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;
}

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

虽然我们在这里有一个渲染功能,但我想介绍另一个我在任何地方都使用的概念。我称之为 postRender。这是一个函数,一旦模板完成加载,我将在其中执行依赖于 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;
}

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

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

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


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


解决此问题的另一种方法是,如果您只想转到特定的产品页面,那么只需在单个 productView 上实现我上面概述的渲染/后渲染模式。但是请注意,您的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 中使用。

==================================================== ======================= 总之,我希望这会有所帮助。我不确定我是否回答了你所有的问题,但我认为我在这些问题上做得很好。为了让这个变得太长,我现在要在这里停下来,让你消化一下,问你有什么问题。我想我可能需要对这篇文章进行至少 1 次更新才能进一步清除它。

于 2015-12-02T18:18:08.030 回答
2

你开始说:

我在一个集合视图中有一个项目列表

那么集合有什么..?楷模..!

当您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
});

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

如果您的集合端点返回大量数据怎么办..?

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


我用给定的 ID 构造了一个 Product 模型

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

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

于 2015-11-17T16:42:18.773 回答