在什么时候获取视图所依赖的 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 最好进行提取吗?
如果我在页面转换之前加载模型,它会导致直接查看代码,但会引入用户可察觉的延迟。用户将单击列表中的一个项目,并且必须等待(没有页面更改)返回模型。响应时间很重要,我还没有对此进行可用性研究,但我认为用户习惯于在单击 link.
后立即看到页面变化
如果我使用 this.model.fetch()
在 ProductView
的 initialize
中加载模型并监听模型事件,我将被迫渲染两次,- - 一次之前使用空占位符(因为否则你必须盯着白页),一次之后。如果在加载过程中发生错误,那么我必须擦除视图(显示 flickery/glitchy)并显示一些错误。
是否还有其他我没有看到的选项,可能涉及可以在视图之间重复使用的过渡加载页面?或者总是第一次调用 render()
显示一些 spinners/loading 指标是好的做法吗?
编辑:通过 collection.fetch()
加载
有人可能会建议,因为这些项目已经是列出的集合的一部分(用于呈现 link 列表的集合),所以可以在单击 link 之前获取它们, collection.fetch()
。如果集合确实是Product
的集合,那么渲染产品视图就很容易了。
用于生成列表的 Collection
可能不是 ProductCollection
。它可能是 ProductOrderCollection
或仅具有对产品 ID 的引用的其他内容(或足够数量的产品信息以向其呈现 link)。
如果 Product
模型很大,通过 collection.fetch()
获取所有 Product
也可能会让人望而却步,尤其是。万一产品 links 之一被点击。
先有鸡还是先有蛋? collection.fetch()
方法也没有真正解决直接导航到产品页面的用户的问题...在这种情况下,我们仍然需要呈现需要 Product
的 ProductView
页面仅从一个 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 次更新才能进一步清除它。
我似乎经常遇到这样的情况:我正在渲染一个视图,但该视图所依赖的模型尚未加载。大多数情况下,我只有从 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 最好进行提取吗?
如果我在页面转换之前加载模型,它会导致直接查看代码,但会引入用户可察觉的延迟。用户将单击列表中的一个项目,并且必须等待(没有页面更改)返回模型。响应时间很重要,我还没有对此进行可用性研究,但我认为用户习惯于在单击 link.
后立即看到页面变化
如果我使用
this.model.fetch()
在ProductView
的initialize
中加载模型并监听模型事件,我将被迫渲染两次,- - 一次之前使用空占位符(因为否则你必须盯着白页),一次之后。如果在加载过程中发生错误,那么我必须擦除视图(显示 flickery/glitchy)并显示一些错误。
是否还有其他我没有看到的选项,可能涉及可以在视图之间重复使用的过渡加载页面?或者总是第一次调用 render()
显示一些 spinners/loading 指标是好的做法吗?
编辑:通过 collection.fetch()
有人可能会建议,因为这些项目已经是列出的集合的一部分(用于呈现 link 列表的集合),所以可以在单击 link 之前获取它们, collection.fetch()
。如果集合确实是Product
的集合,那么渲染产品视图就很容易了。
用于生成列表的 Collection
可能不是 ProductCollection
。它可能是 ProductOrderCollection
或仅具有对产品 ID 的引用的其他内容(或足够数量的产品信息以向其呈现 link)。
如果 Product
模型很大,通过 collection.fetch()
获取所有 Product
也可能会让人望而却步,尤其是。万一产品 links 之一被点击。
先有鸡还是先有蛋? collection.fetch()
方法也没有真正解决直接导航到产品页面的用户的问题...在这种情况下,我们仍然需要呈现需要 Product
的 ProductView
页面仅从一个 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 次更新才能进一步清除它。