JavaScript-全栈教程-二-

79 阅读11分钟

JavaScript 全栈教程(二)

原文:Full Stack JavaScript

协议:CC BY-NC-SA 4.0

四、Backbone.js 简介

代码不是资产。这是一种责任。写的越多,以后要维护的就越多。—未知

本章将演示:

  • 从头开始设置 Backbone.js 应用并安装依赖项
  • 使用 Backbone.js 集合
  • Backbone.js 事件绑定
  • Backbone.js 视图和带有下划线的子视图
  • 重构 Backbone.js 代码
  • AMD 和 Require.js 进行 Backbone.js 开发
  • Backbone.js 产品需要. js
  • 一个简单的 Backbone.js 初学者工具包

Backbone.js 已经存在了一段时间,所以它非常成熟,可以被信任用于严肃的前端开发项目。这个框架绝对是极简主义和非个人化的。您可以将 Backbone.js 与许多其他库和模块一起使用。我认为 Backbone.js 是构建一个定制框架的基础,这个框架将非常适合您的特定用例。

有些人对 Backbone.js 的非个性化和极简主义感到厌恶。他们更喜欢能为他们做更多事情的框架,并执行一种特定的做事方式(例如,Angular 最佳实践(github . com/John papa/Angular-style guide))。这对我来说完全没问题,您可以继续研究更复杂的前端框架。它们都非常适合 Node.js 堆栈和生态系统。就本书的目的而言,Backbone.js 是理想的,因为它为普通的非框架 jQuery 代码提供了一些急需的理性,同时它没有陡峭的学习曲线。你需要知道的只是一些类和方法,我们在这本书里会讲到。其他的都是 JavaScript,不是特定领域的语言。

从头开始设置 Backbone.js 应用

我们将使用 Backbone.js 和模式-视图-控制器(MVC)架构构建一个典型的 starter Hello World 应用。在开始时,这听起来可能有点过头了,但是随着我们的发展,我们会增加越来越多的复杂性,包括模型、子视图和集合。

Hello World 应用的完整源代码可以在05-backbone/hello-world下和 GitHub ( https://github.com/azat-co/fullstack-javascript/tree/master/05-backbone/hello-world )上找到。

Backbone.js 依赖项

引导您完成实施并演示项目的补充视频: http://bit.ly/1O7xRCY

下载以下库:

显然,当这本书出版时,这些版本不会是最新的。我建议坚持使用本书中的版本,因为这是我用来测试所有示例和项目的版本。使用不同的较新版本可能会导致一些意外的冲突。

创建一个index.html文件,并将这些框架包含在这个文件中,如下所示:

< !DOCTYPE>

<html>

<head>

<script``src="jquery.js"

<script``src="underscore.js"

<script``src="backbone.js"

<script>

// TODO write some awesome JS code!

</script>

</head>

<body>

</body>

</html>

我们也可以将<script>标签放在文件末尾的</body>标签之后。这将改变脚本和其余 HTML 的加载顺序,并影响大文件的性能。

让我们在<script>标签中定义一个简单的 Backbone.js 路由:

...

var router = Backbone.Router.extend({

})

...

现在,为了简单起见(亲亲——保持简单),我们将把所有的 JavaScript 代码放在index.html文件中。对于真正的开发或生产代码来说,这不是一个好主意,所以我们稍后将重构它。

接下来,在一个extend调用中设置一个特殊的routes属性:

var router = Backbone.Router.extend({

routes: {

}

})

Backbone.js routes属性需要采用以下格式:’path/:param’:’action’

这将导致filename#path/param URL 触发一个名为action的函数(在Router对象中定义)。现在,我们将添加一条home路线:

var router = Backbone.Router.extend({

routes: {

’’: ’home’

}

})

这很好,但是现在我们需要添加一个home函数:

var router = Backbone.Router.extend({

routes: {

’’: ’home’

},

home: function(){

// TODO render HTML

}

})

稍后我们将回到home函数,为视图的创建和渲染添加更多的逻辑。现在我们应该定义我们的homeView:

var homeView = Backbone.View.extend({

})

看起来很眼熟吧?Backbone.js 对其所有组件使用相似的语法:extend函数和一个 JSON 对象作为它的参数。

从现在开始有多种方法可以继续,但是最佳实践是使用eltemplate属性,它们在 Backbone.js 中是特殊的:

var homeView = Backbone.View.extend({

el: ’body’,

template: _.template(’Hello World’)

})

属性el只是一个包含 jQuery 选择器的字符串(您可以使用带有“.”的类名)和带' # '的 id 名称)。模板属性被赋予了一个Underscore.js函数template,其中只有一个纯文本“Hello World”。

为了呈现我们的homeView,我们使用了this.$el,它是一个编译过的 jQuery 对象,引用了el属性中的元素,并使用 jQuery .html()函数用this.template()值替换 HTML。以下是我们的 Backbone.js 视图的完整代码:

var homeView = Backbone.View.extend({

el: ’body’,

template: _.template(’Hello World’),

render: function(){

this.$el.html(this.template({}))

}

})

现在,如果我们回到router,我们可以将这两行添加到home函数中:

var router = Backbone.Router.extend({

routes: {

’’: ’home’

},

initialize: function(){

},

home: function(){

this.homeView = new homeView

this.homeView.render()

}

})

第一行创建了homeView对象,并将其分配给路由的homeView属性。第二行将调用homeView对象中的render()方法,触发‘Hello World’输出。

最后,为了启动一个主干应用,我们在一个文档就绪包装器中调用new Router来确保文件的 DOM 被完全加载:

var app

$(document).ready(function(){

app = new router

Backbone.history.start()

})

这一次,我不会列出index.html的完整源代码,因为它相当简单。

在浏览器中打开index.html看看是否工作;也就是说,“Hello World”消息应该出现在页面上。

使用 Backbone.js 集合

引导您完成实施并演示项目的补充视频: http://bit.ly/1O7xRCY

这个例子的完整源代码在05-backbone/collections下面。它建立在从头开始设置 Backbone.js 应用练习中的“Hello World”示例的基础上,可以从 GitHub ( https://github.com/azat-co/fullstack-javascript/tree/master/05-backbone/collections )下载。

我们应该添加一些数据来玩,充实我们的观点。为此,请将此代码添加到<script>标签之后,其他代码之前:

var appleData = [

{

name: ’fuji’,

url: ’img/fuji.jpg’

},

{

name: ’gala’,

url: ’img/gala.jpg’

}

]

这是我们的苹果数据库,或者更准确地说,是我们的 REST API 端点替代品,它为我们提供了苹果的名称和图像 URL(数据模型)。

注意,通过将后端的 REST API 端点分配给 Backbone.js 集合、模型或两者中的url属性,并对它们调用fetch()方法,可以很容易地替换这个模拟数据集。

现在,为了让用户体验更好一点,我们可以在主干路由中向routes对象添加一个新路由:

...

routes: {

’’: ’home’,

’apples/:appleName’: ’loadApple’

},

...

这将允许用户去index.html#apples/SOMENAME并期望看到一些关于苹果的信息。该信息将由主干路由定义中的loadApple函数获取和呈现:

loadApple: function(appleName){

this.appleView.render(appleName)

}

你注意到一个appleName变量了吗?这与我们在route中使用的名称完全相同。这就是我们如何在 Backbone.js 中访问查询字符串参数(例如,?param=value&q=search)

现在我们需要重构一些代码来创建一个主干集合,用我们的appleData变量中的数据填充它,并将集合传递给homeViewappleView。非常方便的是,我们在路由构造器方法initialize中完成了这一切:

initialize: function(){

var apples = new Apples()

apples.reset(appleData)

this.homeView = new homeView({collection: apples})

this.appleView = new appleView({collection: apples})

},

此时,我们已经基本完成了对Router类的处理,它看起来应该是这样的:

var router = Backbone.Router.extend({

routes: {

’’: ’home’,

’apples/:appleName’: ’loadApple’

},

initialize: function(){

var apples = new Apples()

apples.reset(appleData)

this.homeView = new homeView({collection: apples})

this.appleView = new appleView({collection: apples})

},

home: function(){

this.homeView.render()

},

loadApple: function(appleName){

this.appleView.render(appleName)

}

})

让我们稍微修改一下homeView来查看整个数据库:

var homeView = Backbone.View.extend({

el: ’body’,

template: _.template(’Apple data: <%= data %>’),

render: function(){

this.$el.html(this.template({data: JSON.stringify(this.collection.models)}))

}

// TODO subviews

})

现在,我们只是在浏览器中输出 JSON 对象的字符串表示。这一点也不用户友好,但是稍后我们将通过使用列表和子视图来改进它。

我们的 apple Backbone 系列非常简洁:

var Apples = Backbone.Collection.extend({

})

当我们从其 API 中使用fetch()reset()函数时,Backbone 会自动在集合中创建模型。我发现使用这些功能非常有用。

苹果视图不再复杂;它只有两个属性:templaterender。在模板中,我们希望显示带有特定值的figureimgfigcaption标签。下划线. js 模板引擎在这项任务中非常方便:

var appleView = Backbone.View.extend({

template: _.template(

’<figure>\

<img src="<%= attributes.url %>"/>\

<figcaption><%= attributes.name %></figcaption>\

</figure>’),

...

})

为了使包含 HTML 标记的 JavaScript 字符串更具可读性,我们可以使用反斜杠换行符转义符(\),或者关闭字符串并用加号(+)将它们连接起来。这是前面的appleView的一个例子,它是用后一种方法重构的:

var appleView = Backbone.View.extend({

template: _.template(

’<figure>’+

+’<img src="<%= attributes.url %>"/>’+

+’<figcaption><%= attributes.name %></figcaption>’+

+’</figure>’),

...

})

请注意“”符号;它们是 Undescore.js 在attributes对象的属性urlname中打印值的指令。

最后,我们将render函数添加到appleView类中。

render: function(appleName){

为了获得按名称过滤的苹果列表,在Collection类上有一个where方法。我们只需要数组中的第一项,因为 JavaScript 中的数组是从零开始的(它们从 0 开始,而不是从 1 开始),按名称获取 apple 模型的语法如下:

var appleModel = this.collection.where({name: appleName})[0]

一旦我们有了自己的模型,我们需要做的就是把模型传递给模板(也叫补水模板)。结果是我们注入到<body>中的一些 HTML:

var appleHtml = this.template(appleModel)

$(’body’).html(appleHtml)

}

因此,我们通过where()方法在集合中找到一个模型,并使用[]选择第一个元素。现在,render函数负责数据的加载和渲染。稍后我们将重构函数,将这两个功能分离到不同的方法中。

为了您的方便,这里是整个应用,它在05-backbone/collections/index.html和 GitHub ( https://github.com/azat-co/fullstack-javascript/blob/master/05-backbone/collections/index.html )文件夹中:

< !DOCTYPE>

<html>

<head>

<script``src="jquery.js"

<script``src="underscore.js"

<script``src="backbone.js"

<script>

var appleData = [

{

name: ’fuji’,

url: ’img/fuji.jpg’

},

{

name: ’gala’,

url: ’img/gala.jpg’

}

]

var app

var router = Backbone.Router.extend({

routes: {

’’: ’home’,

’apples/:appleName’: ’loadApple’

},

initialize:``function

var``apples =``new

apples.reset(appleData)

this``.homeView =``new

this``.appleView =``new

},

home:``function

this .homeView.render()

},

loadApple:``function

this .appleView.render(appleName)

}

})

var homeView = Backbone.View.extend({

el: ’body’,

template: _.template(’Apple data: <%= data %>’),

render:``function

this .$el.html( this .template({data: JSON.stringify( this

}

})

var Apples = Backbone.Collection.extend({

})

var appleView = Backbone.View.extend({

template: _.template(’<figure>\

<img src="<%= attributes.url%>"/>\

<figcaption><%= attributes.name %></figcaption>\

</figure>’),

render: function(appleName){

var appleModel = this.collection.where({name: appleName})[0]

var appleHtml = this.template(appleModel)

$(’body’).html(appleHtml)

}

})

$(document).ready(function(){

app = new router

Backbone.history.start()

})

</script>

</head>

<body>

<div></div>

</body>

</html>

在浏览器中打开collections/index.html文件。你应该看到我们数据库中的数据;也就是Apple data: [{"name":"fuji","url":"img/fuji.jpg"},{"name":"gala","url":"img/gala.jpg"}]

现在,让我们在浏览器中转到collections/index.html#apples/fujicollections/index.html#apples/gala。我们希望看到一个带标题的图像。这是一个项目的详细视图,在本例中是一个苹果。干得好!

Backbone.js 事件绑定

引导您完成实施并演示项目的补充视频: http://bit.ly/1k0ZnUB

在现实生活中,获取数据并不是瞬间发生的,所以让我们重构代码来模拟它。为了更好的用户体验(UX),我们还必须向用户显示一个加载图标(spinner 或 ajax-loader ),通知他们信息正在被加载。

在 Backbone 中有事件绑定是一件好事。如果没有它,我们将不得不传递一个呈现 HTML 的函数作为对数据加载函数的回调,以确保在我们有实际数据要显示之前不执行呈现函数。

因此,当用户进入详细视图(apples/:id)时,我们只调用加载数据的函数。然后,有了合适的事件监听器,当有新数据时(或者当数据改变时,我们的视图将自动地(这不是一个错误)更新自己;Backbone.js 支持多个甚至自定义事件)。

供您参考,如果您不想输入代码(这是我推荐的),它在05-backbone/binding和 GitHub ( https://github.com/azat-co/fullstack-javascript/blob/master/05-backbone/binding/index.html )中。

让我们更改路由中的代码:

...

loadApple: function(appleName){

this.appleView.loadApple(appleName)

}

...

其他一切都保持不变,直到我们到达appleView类。我们需要添加一个构造函数或一个initialize方法,这是 Backbone.js 框架中的一个特殊单词或属性。每次我们创建一个对象的实例,比如var someObj = new SomeObject(),它都会被调用。我们还可以向initialize函数传递额外的参数,就像我们对视图所做的那样(我们传递了一个带有键collection和值apples主干集合的对象)。在 backbonejs.org/#View-constructor 阅读更多关于 Backbone.js 构造函数的内容。

...

var appleView = Backbone.View.extend({

initialize: function(){

// TODO: create and setup model (aka an apple)

},

...

我们有我们的initialize函数;现在我们需要创建一个代表单个苹果的模型,并在模型上设置适当的事件监听器。我们将使用两种类型的事件,change和一个名为spinner的自定义事件。为此,我们将使用on()函数,它接受这些属性:on(event, actions, context)。你可以在 backbonejs.org/#Events-on 了解更多。

...

var appleView = Backbone.View.extend({

initialize: function(){

this.model = new (Backbone.Model.extend({}))

this.model.bind(’change’, this.render, this)

this.bind(’spinner’, this.showSpinner, this)

},

...

})

...

前面的代码基本上可以归结为两件简单的事情:

Call the render() function of the appleView object when the model has changed.   Call the showSpinner() method of the appleView object when event spinner has been fired.  

到目前为止,一切顺利,对吧?但是 GIF 图标 spinner 又是怎么回事呢?让我们在appleView中创建新的属性:

...

templateSpinner: ’<img src="img/spinner.gif" width="30"/>’,

...

还记得路由里的loadApple调用吗?这就是我们如何实现appleView中的功能:

...

loadApple:function(appleName){

要显示 spinner GIF 图像,使用this.trigger让 Backbone 调用showSpinner:

this.trigger(’spinner’)

接下来,我们需要访问闭包内部的上下文。有时候我喜欢用一个有意义的名字来代替_this或者self,所以:

var view = this

接下来,您将对服务器进行 XHR 调用(例如,$.ajax())来获取数据。我们将模拟从远程服务器获取数据时的实时延迟:

setTimeout(function(){

view.model.set(view.collection.where({

name:appleName

})[0].attributes)

}, 1000)

},

...

attributes是一个 Backbone.js 模型属性,它为一个普通的 JavaScript 对象提供了模型属性。总的来说,第一行将触发spinner事件(我们仍然需要为其编写函数)。第二行只是用于范围问题(所以我们可以在闭包里使用appleView)。

setTimeout函数模拟真实远程服务器响应的时间延迟。在其中,我们通过使用一个model.set()函数和一个model.attributes属性(返回模型的属性)将一个选定模型的属性分配给视图的模型。

现在我们可以从render方法中移除一段额外的代码,并实现showSpinner函数:

render: function(appleName){

var appleHtml = this.template(this.model)

$(’body’).html(appleHtml)

},

showSpinner: function(){

$(’body’).html(this.templateSpinner)

}

...

仅此而已!在浏览器中打开index.html#apples/galaindex.html#apples/fuji,在等待苹果图像加载的同时欣赏加载动画。

下面是index.html文件的完整代码(也在05-backbone/binding/index.htmlhttps://github.com/azat-co/fullstack-javascript/blob/master/5-backbone/binding/index.html ):

< !DOCTYPE>

<html>

<head>

<script``src="jquery.js"

<script``src="underscore.js"

<script``src="backbone.js"

<script>

var appleData = [

{

name: ’fuji’,

url: ’img/fuji.jpg’

},

{

name: ’gala’,

url: ’img/gala.jpg’

}

]

var app

var router = Backbone.Router.extend({

routes: {

’’: ’home’,

’apples/:appleName’: ’loadApple’

},

initialize:``function

var``apples =``new

apples.reset(appleData)

this``.homeView =``new

this``.appleView =``new

},

home:``function

this .homeView.render()

},

loadApple:``function

this .appleView.loadApple(appleName)

}

})

var homeView = Backbone.View.extend({

el: ’body’,

template: _.template(’Apple data: <%= data %>’),

render:``function

this .$el.html( this .template({data: JSON.stringify( this

}

})

var Apples = Backbone.Collection.extend({

})

var appleView = Backbone.View.extend({

initialize:``function

this``.model =``new

this .model.on(’change’, this .render, this

this .on(’spinner’, this .showSpinner, this

},

template: _.template(’<figure>\

<img src="<%= attributes.url%>"/>\

<figcaption><%= attributes.name %></figcaption>\

</figure>’),

templateSpinner: ’<img src="img/spinner.gif" width="30"/>’,

loadApple:``function

this .trigger(’spinner’)

var``view =

setTimeout(``function

view.model.set(view.collection.where({name: appleName})[0].attributes)

}, 1000)

},

render:``function

var appleHtml = this .template( this

$(’body’).html(appleHtml)

},

showSpinner:``function

$(’body’).html(``this

}

})

$(document).ready(function(){

app =``new

Backbone.history.start()

})

</script>

</head>

<body>

<div></div>

</body>

</html>

Backbone.js 视图和带有下划线的子视图

引导您完成实施并演示项目的补充视频: http://bit.ly/1k0ZnUB 。而这个例子在 https://github.com/azat-co/fullstack-javascript/tree/master/05-backbone/subview 都有。

子视图是在另一个主干视图中创建和使用的主干视图。子视图概念是抽象(分离)UI 事件(例如,点击)和类似结构元素(例如,苹果)的模板的好方法。

子视图的用例可能包括表格中的一行、列表中的一项、一个段落或一个新行。

我们将重构我们的主页来显示一个漂亮的苹果列表。每个列表项都有一个苹果名称和一个带有onClick事件的购买链接。让我们首先用我们的标准主干extend()函数为单个苹果创建一个子视图:

...

var appleItemView = Backbone.View.extend({

tagName: ’li’,

template: _.template(’’

+’<a href="#apples/<%=name%>" target="_blank">’

+’<%=name%>’

+’</a> <a class="add-to-cart" href="#">buy</a>’),

events: {

’click .add-to-cart’: ’addToCart’

},

render: function() {

this.$el.html(this.template(this.model.attributes))

},

addToCart: function(){

this.model.collection.trigger(’addToCart’, this.model)

}

})

...

现在我们可以用tagNametemplateeventsrenderaddToCart属性和方法填充对象。

...

tagName: ’li’,

...

tagName自动允许 Backbone.js 创建一个带有指定标签名的 HTML 元素,在本例中是列表项的<li>。这将是单个苹果的表示,我们列表中的一行。

...

template: _.template(’’

+’<a href="#apples/<%=name%>" target="_blank">’

+’<%=name%>’

+’</a> <a class="add-to-cart" href="#">buy</a>’),

...

模板只是一个带有下划线. js 指令的字符串。它们被包裹在<%%>符号中。<%=简单来说就是打印一个值。同样的代码可以用反斜杠转义来编写:

...

template: _.template(’\

<a href="#apples/<%=name%>" target="_blank">\

<%=name%>\

</a> <a class="add-to-cart" href="#">buy</a>\

’),

...

每个<li>将有两个锚元素(<a>),链接到一个详细的苹果视图(#apples/:appleName,和一个购买按钮。现在我们要将一个事件监听器附加到购买按钮上:

...

events: {

’click .add-to-cart’: ’addToCart’

},

...

语法遵循以下规则:

event + jQuery element selector: function name

键和值(由冒号分隔的左右部分)都是字符串。例如:

’click .add-to-cart’: ’addToCart’

或者

’click #load-more’: ’loadMoreData’

为了呈现列表中的每一项,我们将对this.$el jQuery 对象使用 jQuery html()函数,该对象是基于我们的tagName属性的<li> HTML 元素:

...

render: function() {

this.$el.html(this.template(this.model.attributes))

},

...

addToCart将使用trigger()函数通知集合,该特定型号(apple)可供用户购买:

...

addToCart: function(){

this.model.collection.trigger(’addToCart’, this.model)

}

...

下面是appleItemView主干视图类的完整代码:

...

var appleItemView = Backbone.View.extend({

tagName: ’li’,

template: _.template(’’

+ ’<a href="#apples/<%=name%>" target="_blank">’

+ ’<%=name%>’

+ ’</a> <a class="add-to-cart" href="#">buy</a>’),

events: {

’click .add-to-cart’: ’addToCart’

},

render: function() {

this.$el.html(this.template(this.model.attributes))

},

addToCart: function(){

this.model.collection.trigger(’addToCart’, this.model)

}

})

...

很简单!但是主视图呢,它应该呈现我们所有的项目(苹果)并为 HTML 元素提供一个包装器<ul>容器?我们需要修改和增强我们的homeView

首先,我们可以添加 jQuery 可以理解的额外的string类型的属性作为homeView的选择器:

...

el: ’body’,

listEl: ’.apples-list’,

cartEl: ’.cart-box’,

...

我们可以在模板中使用前面的属性,或者只是在homeView中硬编码它们(我们稍后将重构我们的代码):

...

template: _.template(’Apple data: \

<ul class="apples-list">\

</ul>\

<div class="cart-box"></div>’),

...

创建homeView(new homeView())时会调用initialize函数。在这里,我们呈现我们的模板(使用我们最喜欢的html()函数),并将一个事件监听器附加到集合,集合是一组苹果模型:

...

initialize: function() {

this.$el.html(this.template)

this.collection.on(’addToCart’, this.showCart, this)

},

...

绑定事件的语法在上一节中介绍过。本质上是在调用homeViewshowCart()函数。在这个函数中,我们将appleName添加到购物车中(连同一个换行符,一个<br/>元素):

...

showCart: function(appleModel) {

$(this.cartEl).append(appleModel.attributes.name + ’<br/>’)

},

...

最后,这是我们期待已久的render()方法,在该方法中,我们遍历集合中的每个模型(每个苹果),为每个苹果创建一个appleItemView,为每个苹果创建一个<li>元素,并将该元素附加到 DOM 中带有类apples-listview.listEl<ul>元素:

...

render: function(){

view = this

// So we can use view inside of closure

this.collection.each(function(apple){

var appleSubView = new appleItemView({model:apple})

// Creates subview with model apple

appleSubView.render()

// Compiles template and single apple data

$(view.listEl).append(appleSubView.$el)

// Append jQuery object from single

// Apple to apples-list DOM element

})

}

...

让我们确保在homeView主干视图中没有遗漏任何东西。以下是没有内联注释的完整代码:

...

var homeView = Backbone.View.extend({

el: ’body’,

listEl: ’.apples-list’,

cartEl: ’.cart-box’,

template: _.template(’Apple data: \

<ul class="apples-list">\

</ul>\

<div class="cart-box"></div>’),

initialize: function() {

this.$el.html(this.template)

this.collection.on(’addToCart’, this.showCart, this)

},

showCart: function(appleModel) {

$(this.cartEl).append(appleModel.attributes.name + ’<br/>’)

},

render: function(){

view = this

this.collection.each(function(apple){

var appleSubView = new appleItemView({model: apple})

appleSubView.render()

$(view.listEl).append(appleSubView.$el)

})

}

})

...

您应该能够点击购买按钮,并在购物车中放入您选择的苹果。查看单个苹果不再需要在浏览器的 URL 地址栏中键入其名称。我们可以单击该名称来打开一个新窗口,其中包含详细视图。

通过使用子视图,我们为所有项目(苹果)重用了模板,并为每个项目附加了一个特定的事件(见图 4-1 )。这些事件足够智能,可以将关于模型的信息传递给其他对象:视图和集合。

A978-1-4842-1751-1_4_Fig1_HTML.jpg

图 4-1。

The list of apples rendered by subviews

为了以防万一,下面是子视图示例的完整代码,也可以在 https://github.com/azat-co/fullstack-javascript/blob/master/05-backbone/subview/index.html 获得:

<!DOCTYPE>

<html>

<head>

<script src="jquery.js"></script>

<script src="underscore.js"></script>

<script src="backbone.js"></script>

<script>

var appleData = [

{

name: ’fuji’,

url: ’img/fuji.jpg’

},

{

name: ’gala’,

url: ’img/gala.jpg’

}

]

var app

var router = Backbone.Router.extend({

routes: {

’’: ’home’,

’apples/:appleName’: ’loadApple’

},

initialize: function(){

var apples = new Apples()

apples.reset(appleData)

this.homeView = new homeView({collection: apples})

this.appleView = new appleView({collection: apples})

},

home: function(){

this.homeView.render()

},

loadApple: function(appleName){

this.appleView.loadApple(appleName)

}

})

var appleItemView = Backbone.View.extend({

tagName: ’li’,

template: _.template(’\

<a href="#apples/<%=name%>" target="_blank">\

<%=name%>\

</a> <a class="add-to-cart" href="#">buy</a>\

’),

events: {

’click .add-to-cart’: ’addToCart’

},

render: function() {

this.$el.html(this.template(this.model.attributes))

},

addToCart: function(){

this.model.collection.trigger(’addToCart’, this.model)

}

})

var homeView = Backbone.View.extend({

el: ’body’,

listEl: ’.apples-list’,

cartEl: ’.cart-box’,

template: _.template(’Apple data: \

<ul class="apples-list">\

</ul>\

<div class="cart-box"></div>’),

initialize: function() {

this.$el.html(this.template)

this.collection.on(’addToCart’, this.showCart, this)

},

showCart: function(appleModel) {

$(this.cartEl).append(appleModel.attributes.name + ’<br/>’)

},

render: function(){

view = this

this.collection.each(function(apple){

var appleSubView = new appleItemView({model: apple})

appleSubView.render()

$(view.listEl).append(appleSubView.$el)

})

}

})

var Apples = Backbone.Collection.extend({

})

var appleView = Backbone.View.extend({

initialize: function(){

this.model = new (Backbone.Model.extend({}))

this.model.on(’change’, this.render, this)

this.on(’spinner’, this.showSpinner, this)

},

template: _.template(’<figure>\

<img src="<%= attributes.url%>"/>\

<figcaption><%= attributes.name %></figcaption>\

</figure>’),

templateSpinner: ’<img src="img/spinner.gif" width="30"/>’,

loadApple:function(appleName){

this.trigger(’spinner’)

var view = this

setTimeout(function(){

view.model.set(view.collection.where({name: appleName})[0].attributes)

}, 1000)

},

render: function(appleName){

var appleHtml = this.template(this.model)

$(’body’).html(appleHtml)

},

showSpinner: function(){

$(’body’).html(this.templateSpinner)

}

})

$(document).ready(function(){

app = new router

Backbone.history.start()

})

</script>

</head>

<body>

<div></div>

</body>

</html>

到单个项目的链接,例如collections/index.html#apples/fuji,也应该通过在浏览器地址栏中键入它而独立工作。

重构 Backbone.js 代码

引导您完成实施并演示项目的补充视频: http://bit.ly/1k0ZnUB

此时,您可能想知道使用框架并在一个文件中拥有多个具有不同功能的类、对象和元素有什么好处。这样做是为了坚持让事情简单的想法。

你的应用越大,无组织的代码库就越痛苦。让我们将应用分解成多个文件,每个文件都是以下类型之一:

  • 视角
  • 模板
  • 路由
  • 募捐
  • 模型

让我们编写这些脚本,将标签包含到我们的index.html头或主体中,如前所述:

<script``src="apple-item.view.js"

<script``src="apple-home.view.js"

<script``src="apple.view.js"

<script``src="apples.js"

<script``src="apple-app.js"

名称不必遵循破折号和点号的约定,只要能容易地说出每个文件应该做什么。

现在,让我们将对象和类复制到相应的文件中。

我们的主index.html文件应该看起来非常简洁:

< !DOCTYPE>

<html>

<head>

<script``src="jquery.js"

<script``src="underscore.js"

<script``src="backbone.js"

<script``src="apple-item.view.js"

<script``src="apple-home.view.js"

<script``src="apple.view.js"

<script``src="apples.js"

<script``src="apple-app.js"

</head>

<body>

<div></div>

</body>

</html>

其他文件只有与其文件名对应的代码。

apple-item.view.js的内容将拥有appleView对象:

var appleView = Backbone.View.extend({

initialize: function(){

this.model = new (Backbone.Model.extend({}))

this.model.on(’change’, this.render, this)

this.on(’spinner’, this.showSpinner, this)

},

template: _.template(’<figure>\

<img src="<%= attributes.url %>"/>\

<figcaption><%= attributes.name %></figcaption>\

</figure>’),

templateSpinner: ’<img src="img/spinner.gif" width="30"/>’,

loadApple:function(appleName){

this.trigger(’spinner’)

var view = this

// We’ll need to access that inside of a closure

setTimeout(function(){

// Simulates real time lag when fetching

// data from the remote server

view.model.set(view.collection.where({

name: appleName

})[0].attributes)

}, 1000)

},

render: function(appleName){

var appleHtml = this.template(this.model)

$(’body’).html(appleHtml)

},

showSpinner: function(){

$(’body’).html(this.templateSpinner)

}

})

apple-home.view.js文件具有homeView对象:

var homeView = Backbone.View.extend({

el: ’body’,

listEl: ’.apples-list’,

cartEl: ’.cart-box’,

template: _.template(’Apple data: \

<ul class="apples-list">\

</ul>\

<div class="cart-box"></div>’),

initialize: function() {

this.$el.html(this.template)

this.collection.on(’addToCart’, this.showCart, this)

},

showCart: function(appleModel) {

$(this.cartEl).append(appleModel.attributes.name + ’<br/>’)

},

render: function(){

view = this // So we can use view inside of closure

this.collection.each(function(apple){

var appleSubView = new appleItemView({model:apple})

// Create subview with model apple

appleSubView.render()

// Compiles template and single apple data

$(view.listEl).append(appleSubView.$el)

// Append jQuery object from

// single apple to apples-list DOM element

})

}

})

apple.view.js文件包含主苹果列表:

var appleView = Backbone.View.extend({

initialize: function(){

this.model = new (Backbone.Model.extend({}))

this.model.on(’change’, this.render, this)

this.on(’spinner’,this.showSpinner, this)

},

template: _.template(’<figure>\

<img src="<%= attributes.url %>"/>\

<figcaption><%= attributes.name %></figcaption>\

</figure>’),

templateSpinner: ’<img src="img/spinner.gif" width="30"/>’,

loadApple:function(appleName){

this.trigger(’spinner’)

var view = this

// We’ll need to access that inside of a closure

setTimeout(function(){

// Simulates real time lag when

// fetching data from the remote server

view.model.set(view.collection.where({

name:appleName

})[0].attributes)

}, 1000)

},

render: function(appleName){

var appleHtml = this.template(this.model)

$(’body’).html(appleHtml)

},

showSpinner: function(){

$(’body’).html(this.templateSpinner)

}

})

apples.js是一个空集合:

var Apples = Backbone.Collection.extend({

})

apple-app.js是包含数据、路由和启动命令的主应用文件:

var appleData = [

{

name: ’fuji’,

url: ’img/fuji.jpg’

},

{

name: ’gala’,

url: ’img/gala.jpg’

}

]

var app

var router = Backbone.Router.extend({

routes: {

’’: ’home’,

’apples/:appleName’: ’loadApple’

},

initialize: function(){

var apples = new Apples()

apples.reset(appleData)

this.homeView = new homeView({collection: apples})

this.appleView = new appleView({collection: apples})

},

home: function(){

this.homeView.render()

},

loadApple: function(appleName){

this.appleView.loadApple(appleName)

}

})

$(document).ready(function(){

app = new router

Backbone.history.start()

})

现在让我们尝试打开应用。它应该与前面的子视图示例完全一样。

这是一个好得多的代码组织,但它仍然远非完美,因为我们仍然在 JavaScript 代码中直接使用 HTML 模板。问题是设计人员和开发人员不能处理同一个文件,对表示的任何更改都需要更改主要代码库。

我们可以在我们的index.html文件中再添加几个 JS 文件:

<script``src="apple-item.tpl.js"

<script``src="apple-home.tpl.js"

<script``src="apple-spinner.tpl.js"

<script``src="apple.tpl.js"

通常,一个主干视图有一个模板,但是在我们的appleView——一个单独窗口中的苹果的详细视图——中,我们还有一个旋转器,一个“加载”GIF 动画。

文件的内容只是被赋予一些字符串值的全局变量。稍后,当我们调用下划线. js 辅助方法_.template()时,我们可以在视图中使用这些变量。

下面是apple-item.tpl.js文件:

var appleItemTpl = ’\

<a href="#apples/<%=name%>" target="_blank">\

<%=name%>\

</a> <a class="add-to-cart" href="#">buy</a>\

这是apple-home.tpl.js文件:

var appleHomeTpl = ’Apple data: \

<ul class="apples-list">\

</ul>\

<div class="cart-box"></div>’

下面是apple-spinner.tpl.js文件:

var appleSpinnerTpl = ’<img src="img/spinner.gif" width="30"/>’

这是apple.tpl.js文件:

var appleTpl = ’<figure>\

<img src="<%= attributes.url %>"/>\

<figcaption><%= attributes.name %></figcaption>\

</figure>’

现在尝试启动应用。完整代码在 https://github.com/azat-co/fullstack-javascript/tree/master/05-backbone/refactor

正如你在前面的例子中看到的,我们使用了全局范围的变量(没有关键字window)。

在全局名称空间中引入大量变量时要小心(window关键字)。可能会有冲突和其他不可预测的后果。例如,如果您编写了一个开源库,而其他开发人员开始直接使用这些方法和属性,而不是使用接口,那么当您最终决定删除或反对这些全局泄漏时,会发生什么呢?为了防止这种情况,正确编写的库和应用使用了 JavaScript 闭包 ( https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures )。

下面是一个使用闭包和全局变量模块定义的例子:

;(function() {

var apple= function() {

...// Do something useful like return apple object

}

window.Apple = apple

}())

如果我们需要访问 app 对象(这会创建对该对象的依赖):

;(function() {

var app = this.app

// Equivalent of window.appliation

// in case we need a dependency (app)

this.apple = function() {

...

// Return apple object/class

// Use app variable

}

// Equivalent of window.apple = function(){...}

}())

如您所见,我们已经创建了函数并立即调用了它,同时还将所有内容括在括号()中。

AMD 和 Require.js 进行 Backbone.js 开发

引导您完成实施并演示项目的补充视频: http://bit.ly/1k0ZnUB

AMD 允许我们将开发代码组织成模块,管理依赖关系,并异步加载它们。这篇文章很好地解释了为什么 AMD 是个好东西:为什么是 AMD?

启动您的本地 HTTP 服务器,例如 MAMP(www . mamp . info/en)或 node-static ( https://github.com/cloudhead/node-static )。

让我们通过使用 Require.js 库来增强我们的代码。

我们的index.html会缩水更多:

< !DOCTYPE>

<html>

<head>

<script``src="jquery.js"

<script``src="underscore.js"

<script``src="backbone.js"

<script``src="require.js"

<script``src="apple-app.js"

</head>

<body>

<div></div>

</body>

</html>

我们在应用中只包含了库和一个 JavaScript 文件。该文件具有以下结构:

require([...],function(...){...})

以一种更具解释性的方式:

require([

’name-of-the-module’,

...

’name-of-the-other-module’

],function(referenceToModule, ..., referenceToOtherModule){

...// Some useful code

referenceToModule.someMethod()

})

基本上,我们告诉浏览器从文件名数组中加载文件——require()函数的第一个参数——然后将这些文件中的模块作为变量传递给匿名回调函数(第二个参数)。在 main 函数(匿名回调)内部,我们可以通过引用那些变量来使用我们的模块。因此,我们的apple-app.js倏然变成:

require([

’apple-item.tpl’, // Can use shim plug-in

’apple-home.tpl’,

’apple-spinner.tpl’,

’apple.tpl’,

’apple-item.view’,

’apple-home.view’,

’apple.view’,

’apples’

],function(

appleItemTpl,

appleHomeTpl,

appleSpinnerTpl,

appleTpl,

appelItemView,

homeView,

appleView,

Apples

){

var appleData = [

{

name: ’fuji’,

url: ’img/fuji.jpg’

},

{

name: ’gala’,

url: ’img/gala.jpg’

}

]

var app

var router = Backbone.Router.extend({

// Check if need to be required

routes: {

’’: ’home’,

’apples/:appleName’: ’loadApple’

},

initialize: function(){

var apples = new Apples()

apples.reset(appleData)

this.homeView = new homeView({collection: apples})

this.appleView = new appleView({collection: apples})

},

home: function(){

this.homeView.render()

},

loadApple: function(appleName){

this.appleView.loadApple(appleName)

}

})

$(document).ready(function(){

app = new router

Backbone.history.start()

})

})

我们将所有代码放在函数中,该函数是require()的第二个参数,通过文件名提到模块,并通过相应的参数使用依赖关系。现在我们应该定义模块本身。这就是我们如何使用define()方法实现的:

define([...],function(...){...})

其含义类似于require()函数:依赖项是数组中作为第一个参数传递的文件名(和路径)的字符串。第二个参数是 main 函数,它接受其他库作为参数(数组中参数和模块的顺序很重要):

define([’name-of-the-module’],function(nameOfModule){

var b = nameOfModule.render()

return b

})

注意,不需要在文件名后添加.js。Require.js 会自动完成。Shim 插件用于导入 HTML 模板等文本文件。

让我们从模板开始,将它们转换成 Require.js 模块。

下面是新的apple-item.tpl.js文件:

define(function() {

return ’\

<a href="#apples/<%=name%>" target="_blank">\

<%=name%>\

</a> <a class="add-to-cart" href="#">buy</a>\

})

这是apple-home.tpl文件:

define(function(){

return ’Apple data: \

<ul class="apples-list">\

</ul>\

<div class="cart-box"></div>’

})

下面是apple-spinner.tpl.js文件:

define(function(){

return ’<img src="img/spinner.gif" width="30"/>’

})

这是apple.tpl.js文件:

define(function(){

return ’<figure>\

<img src="<%= attributes.url %>"/>\

<figcaption><%= attributes.name %></figcaption>\

</figure>’

})

下面是apple-item.view.js文件:

define(function() {

return ’\

<a href="#apples/<%=name%>" target="_blank">\

<%=name%>\

</a> <a class="add-to-cart" href="#">buy</a>\

})

apple-home.view.js文件中,我们需要声明对apple-home.tplapple-item.view.js文件的依赖:

define([’apple-home.tpl’, ’apple-item.view’], function(

appleHomeTpl,

appleItemView){

return  Backbone.View.extend({

el: ’body’,

listEl: ’.apples-list’,

cartEl: ’.cart-box’,

template: _.template(appleHomeTpl),

initialize: function() {

this.$el.html(this.template)

this.collection.on(’addToCart’, this.showCart, this)

},

showCart: function(appleModel) {

$(this.cartEl).append(appleModel.attributes.name + ’<br/>’)

},

render: function(){

view = this // So we can use view inside of closure

this.collection.each(function(apple){

var appleSubView = new appleItemView({model:apple})

// Create subview with model apple

appleSubView.render()

// Compiles template and single apple data

$(view.listEl).append(appleSubView.$el)

// Append jQuery object from

// a single apple to apples-list DOM element

})

}

})

})

apple.view.js文件依赖于两个模板:

define([

’apple.tpl’,

’apple-spinner.tpl’

], function(appleTpl,appleSpinnerTpl){

return  Backbone.View.extend({

initialize: function(){

this.model = new (Backbone.Model.extend({}))

this.model.on(’change’, this.render, this)

this.on(’spinner’,this.showSpinner, this)

},

template: _.template(appleTpl),

templateSpinner: appleSpinnerTpl,

loadApple:function(appleName){

this.trigger(’spinner’)

var view = this

// We’ll need to access that inside of a closure

setTimeout(function(){

// Simulates real time lag when

// fetching data from the remote server

view.model.set(view.collection.where({

name:appleName

})[0].attributes)

}, 1000)

},

render: function(appleName){

var appleHtml = this.template(this.model)

$(’body’).html(appleHtml)

},

showSpinner: function(){

$(’body’).html(this.templateSpinner)

}

})

})

这是apples.js文件:

define(function(){

return Backbone.Collection.extend({})

})

我希望你现在能看到这个模式。我们所有的代码都根据逻辑(例如,视图类、集合类、模板)被分割成单独的文件。主文件用require()函数加载所有的依赖项。如果我们需要非主文件中的某个模块,那么我们可以在define()方法中请求它。通常,在模块中我们想要返回一个对象;例如,在模板中我们返回字符串,在视图中我们返回主干视图类和对象。

尝试启动位于 https://github.com/azat-co/fullstack-javascript/blob/master/05-backbone/amd/ 的示例。目测应该不会有什么变化。如果您在开发工具中打开“网络”选项卡,您可以看到文件加载方式的不同。

图 4-2 ( https://github.com/azat-co/fullstack-javascript/tree/master/05-backbone/refactor/index.html )所示的旧文件以串行方式加载我们的 JavaScript 脚本,而图 4-3 ( https://github.com/azat-co/fullstack-javascript/blob/master/05-backbone/amd/index.html )所示的新文件以并行方式加载它们。

A978-1-4842-1751-1_4_Fig3_HTML.jpg

图 4-3。

The new 05-backbone/amd/index.html file

A978-1-4842-1751-1_4_Fig2_HTML.jpg

图 4-2。

The old 05-backbone/refactor/index.html file

Require.js 有很多配置选项,这些选项是通过 HTML 页面顶层的requirejs.config()调用定义的。更多信息请访问 requirejs.org/docs/api.html#config

让我们在示例中添加一个半身像参数。bust 参数将被附加到每个文件的 URL,防止浏览器缓存文件。这对开发来说是完美的,对生产来说是可怕的。

将此内容添加到其他内容之前的apple-app.js文件中:

requirejs.config({

urlArgs: ’bust=’ +  (new Date()).getTime()

})

require(

...

注意在图 [4-4 中,每个文件请求现在的状态是 200 而不是 304(未修改)。

A978-1-4842-1751-1_4_Fig4_HTML.jpg

图 4-4。

Network tab with bust parameter added

Backbone.js 产品需要. js

我们将使用 Node 包管理器(NPM)来安装requirejs库(这不是打字错误;名称中没有句号)。在项目文件夹中,在终端中运行以下命令:

$ npm init

那就跑

$ npm install requirejs

或添加-g进行全球安装:

$ npm install -g requirejs

创建一个名为app.build.js的文件:

({

appDir: "./js",

baseUrl: "./",

dir: "build",

modules: [

{

name: "apple-app"

}

]

})

将脚本文件移动到js文件夹(appDir属性)。生成的文件将被放在build文件夹中(dir参数)。有关构建文件的更多信息,请查看 https://github.com/jrburke/r.js/blob/master/build/example.build.js 提供的带有注释的广泛示例。

现在,构建一个巨大的 JavaScript 文件的一切都应该准备好了,这个文件将包含我们所有的依赖项和模块:

$ r.js -o app.build.js

或者

$ node_modules/requirejs/bin/r.js -o app.build.js

您应该会得到一个 r.js 已处理文件的列表,如图 4-5 所示。

A978-1-4842-1751-1_4_Fig5_HTML.jpg

图 4-5。

A list of the r.js processed files

从浏览器窗口的构建文件夹中打开index.html,检查网络选项卡是否显示任何改进,现在只需加载一个请求或文件(图 4-6 )。

A978-1-4842-1751-1_4_Fig6_HTML.jpg

图 4-6。

Performance improvement with one request or file to load

如需了解更多信息,请访问 requirejs.org/docs/optimization.html 查看 r.js 官方文档。

示例代码可从 https://github.com/azat-co/fullstack-javascript/tree/master/05-backbone/r 获得

还有 https://github.com/azat-co/fullstack-javascript/tree/master/05-backbone/r/build

对于 JS 文件的丑化(减小文件大小),我们可以使用丑化 2 模块。要使用 NPM 安装它,请使用:

$ npm install uglify-js

然后用optimize: "uglify2"属性更新app.build.js文件:

({

appDir: "./js",

baseUrl: "./",

dir: "build",

optimize: "uglify2",

modules: [

{

name: "apple-app"

}

]

})

使用以下命令运行 r.js:

$ node_modules/requirejs/bin/r.js -o app.build.js

您应该得到这样的结果:

define("apple-item.tpl",[],function(){return’ <a href="#apples/<%=name%>" target="_blank"> <%=name%> </a> <a class="add-to-cart" href="#">buy</a>’}),define("apple-home.tpl",[],function(){return’Apple data:<ulclass="apples-list"></ul><div class="cart-box"></div>’}),define("apple-spinner.tpl",[],function(){return’<img src="img/spinner.gif" width="30"/>’}),define("apple.tpl",[],function(){return’<figure><img src="<%= attributes.url %>"/><figcaption><%= attributes.name %></figcaption></figure>’}),define("apple-item.view",["apple-item.tpl"],function(e){return Backbone.View.extend({tagName:"li",template:_.template(e),events:{"click .add-to-cart":"addToCart"},render:function(){this.$el.html(this.template(this.model.attributes))},addToCart:function(){this.model.collection.trigger("addToCart",this.model)}})}),define("apple-home.view",["apple-home.tpl","apple-item.view"],function(e,t){return Backbone.View.extend({el:"body",listEl:".apples-list",cartEl:".cart-box",template:_.template(e),initialize:function(){this.$el.html(this.template),this.collection.on("addToCart",this.showCart,this)},showCart:function(e){$(this.cartEl).append(e.attributes.name+"<br/>")},render:function(){view=this,this.collection.each(function(e){var i=new t({model:e});i.render(),$(view.listEl).append(i.$el)})}})}),define("apple.view",["apple.tpl","apple-spinner.tpl"],function(e,t){return Backbone.View.extend({initialize:function(){this.model=new(Backbone.Model.extend({})),this.model.on("change",this.render,this),this.on("spinner",this.showSpinner,this)},template:_.template(e),templateSpinner:t,loadApple:function(e){this.trigger("spinner");var t=this;setTimeout(function(){t.model.set(t.collection.where({name:e})[0].attributes)},1e3)},render:function(){var e=this.template(this.model);$("body").html(e)},showSpinner:function(){$("body").html(this.templateSpinner)}})}),define("apples",[],function(){return Backbone.Collection.extend({})}),requirejs.config({urlArgs:"bust="+(new Date).getTime()}),require(["apple-item.tpl","apple-home.tpl","apple-spinner.tpl","apple.tpl","apple-item.view","apple-home.view","apple.view","apples"],function(e,t,i,n,a,l,p,o){var r,s=[{name:"fuji",url:"img/fuji.jpg"},{name:"gala",url:"img/gala.jpg"}],c=Backbone.Router.extend({routes:{"":"home","apples/:appleName":"loadApple"},initialize:function(){var e=new o;e.reset(s),this.homeView=new l({collection:e}),this.appleView=new p({collection:e})},home:function(){this.homeView.render()},loadApple:function(e){this.appleView.loadApple(e)}});$(document).ready(function(){r=new c,Backbone.history.start()})}),define("apple-app",function(){});

该文件故意没有格式化以显示 uglify 2(github . com/mishoo/uglifyjs 2)是如何工作的。如果没有换行符,代码就在一行上。还要注意变量和对象的名字被缩短了。

超级简单的 Backbone.js 初学者工具包

为了快速启动您的 Backbone.js 开发,可以考虑使用超级简单的 Backbone 初学者工具包 ( https://github.com/azat-co/super-simple-backbone-starter-kit )或类似的项目:

摘要

到目前为止,我们已经讲述了如何:

  • 从头开始构建一个 Backbone.js 应用。
  • 使用视图、集合、子视图、模型和事件绑定。
  • 在苹果数据库应用的例子中使用 AMD 和 Require.js。

在这一章中,你已经对 Backbone.js 有了足够的了解,可以开始在你的网络或移动应用中使用它。如果没有像 Backbone 这样的框架,随着代码的增长,它将会变得更加复杂。另一方面,使用 Backbone 或类似的 MVC,您可以更好地扩展代码。