Extending Apostrophe to build a variable-based CMS for rendering PDF brochures
Building a Single-Page App: Backbone, Node.js, and Beyond
1. Building a Single-Page App:
Backbone, Node.js, and Beyond
Spike Brehm, Front End Engineer
spike@airbnb.com
@spikebrehm
September 12, 2012
Thursday, September 13, 12
2. Past: Why Single-Page Apps
Present: How we built Wish Lists
Future: In pursuit of the Holy Grail
Thursday, September 13, 12
3. Past
Why Single-Page Apps
Thursday, September 13, 12
7. Airbedandbreakfast.com
• Started in 2008 as a Rails 2.x app
• Now Rails 3.0
Thursday, September 13, 12
8. Airbedandbreakfast.com
• Started in 2008 as a Rails 2.x app
• Now Rails 3.0
• Still stuck in old, page-based paradigm
Thursday, September 13, 12
9. What is a Single-Page App?
Thursday, September 13, 12
10. What is a Single-Page App?
Thursday, September 13, 12
11. What is a Single-Page App?
Thursday, September 13, 12
12. What is a Single-Page App?
• Navigate in the app without page refresh
Thursday, September 13, 12
13. What is a Single-Page App?
• Navigate in the app without page refresh
• Application logic in the client
Thursday, September 13, 12
14. What is a Single-Page App?
• Navigate in the app without page refresh
• Application logic in the client
• Fetch data on demand
Thursday, September 13, 12
22. The Easy Way
• JavaScript app runs entirely in client
Thursday, September 13, 12
23. The Easy Way
• JavaScript app runs entirely in client
• Server technology agnostic
Thursday, September 13, 12
24. The Easy Way
• JavaScript app runs entirely in client
• Server technology agnostic
• Can use Backbone to structure app
Thursday, September 13, 12
25. The Easy Way
• JavaScript app runs entirely in client
• Server technology agnostic
• Can use Backbone to structure app
• Poor SEO -- not crawlable
Thursday, September 13, 12
26. The Easy Way
• JavaScript app runs entirely in client
• Server technology agnostic
• Can use Backbone to structure app
• Poor SEO -- not crawlable
• Performance hit to download & evaluate JS before
rendering
Thursday, September 13, 12
27. The Easy Way
• JavaScript app runs entirely in client
• Server technology agnostic
• Can use Backbone to structure app
• Poor SEO -- not crawlable
• Performance hit to download & evaluate JS before
rendering
• Good for apps behind login, or tools
Thursday, September 13, 12
28. The Hard Way
aka “The Holy Grail”
Thursday, September 13, 12
30. The Hard Way
• Routing, templating, application logic, utilities run on
client and server
Thursday, September 13, 12
31. The Hard Way
• Routing, templating, application logic, utilities run on
client and server
• Navigate to any page, HTML rendered in client -- hit
refresh, serves up HTML
Thursday, September 13, 12
32. The Hard Way
• Routing, templating, application logic, utilities run on
client and server
• Navigate to any page, HTML rendered in client -- hit
refresh, serves up HTML
• Must render full page of HTML without access to DOM
(or find a faster DOM implementation)
Thursday, September 13, 12
33. The Hard Way
• Routing, templating, application logic, utilities run on
client and server
• Navigate to any page, HTML rendered in client -- hit
refresh, serves up HTML
• Must render full page of HTML without access to DOM
(or find a faster DOM implementation)
• Requires JavaScript runtime on the server (or DSL that
compiles down to JavaScript -- think GWT)
Thursday, September 13, 12
34. The Hard Way
• Routing, templating, application logic, utilities run on
client and server
• Navigate to any page, HTML rendered in client -- hit
refresh, serves up HTML
• Must render full page of HTML without access to DOM
(or find a faster DOM implementation)
• Requires JavaScript runtime on the server (or DSL that
compiles down to JavaScript -- think GWT)
• Backbone not a good fit
Thursday, September 13, 12
35. The Hard Way
• Routing, templating, application logic, utilities run on
client and server
• Navigate to any page, HTML rendered in client -- hit
refresh, serves up HTML
• Must render full page of HTML without access to DOM
(or find a faster DOM implementation)
• Requires JavaScript runtime on the server (or DSL that
compiles down to JavaScript -- think GWT)
• Backbone not a good fit
• Provides good SEO
Thursday, September 13, 12
36. The Hard Way
• Routing, templating, application logic, utilities run on
client and server
• Navigate to any page, HTML rendered in client -- hit
refresh, serves up HTML
• Must render full page of HTML without access to DOM
(or find a faster DOM implementation)
• Requires JavaScript runtime on the server (or DSL that
compiles down to JavaScript -- think GWT)
• Backbone not a good fit
• Provides good SEO
• Better performance
Thursday, September 13, 12
37. Performance
Improving performance on Twitter.com
http://engineering.twitter.com/2012/05/improving-performance-on-
twittercom.html
“Time to first tweet”
Thursday, September 13, 12
61. Bootstrapping the app
• Each action bootstraps whatever data
needed on first pageload
Thursday, September 13, 12
62. Bootstrapping the app
• Each action bootstraps whatever data
needed on first pageload
• Subsequent data is requested on-
demand
Thursday, September 13, 12
63. App Initialize
class AIR.Apps.Wishlists extends Backbone.Model
initialize: =>
@wishlists = new AIR.Collections.Wishlists @get('wishlists')
@listings = new AIR.Collections.Listings @get('listings')
...
new AIR.Routers.Wishlists({app: @})
Thursday, September 13, 12
64. App Initialize
class AIR.Apps.Wishlists extends Backbone.Model
initialize: =>
@wishlists = new AIR.Collections.Wishlists @get('wishlists')
@listings = new AIR.Collections.Listings @get('listings')
...
new AIR.Routers.Wishlists({app: @})
WishlistsApp.wishlists
=> Wishlists
_byCid: Object
_byId: Object
length: 11
models: Array[11]
__proto__: ctor
Thursday, September 13, 12
66. Backbone Router
• Translates URL changes to method
calls
Thursday, September 13, 12
67. Backbone Router
• Translates URL changes to method
calls
• Source of global app state
Thursday, September 13, 12
68. Backbone Router
• Translates URL changes to method
calls
• Source of global app state
• Keep state out of views
Thursday, September 13, 12
69. Backbone Router
• Translates URL changes to method
calls
• Source of global app state
• Keep state out of views
• Idempotent view rendering
Thursday, September 13, 12
82. api.airbnb.com
• Used by iOS, Android, Mobile Web clients
Thursday, September 13, 12
83. api.airbnb.com
• Used by iOS, Android, Mobile Web clients
• No Cross-Domain XHR
Thursday, September 13, 12
84. api.airbnb.com
• Used by iOS, Android, Mobile Web clients
• No Cross-Domain XHR
• JSONP for GET; but no POST, PUT, DELETE
Thursday, September 13, 12
85. api.airbnb.com
• Used by iOS, Android, Mobile Web clients
• No Cross-Domain XHR
• JSONP for GET; but no POST, PUT, DELETE
• Added CORS support in API to allow
requests coming from valid Airbnb domain
(*.airbnb.com, *.airbnb.co.uk, *.airbnb.de...)
Thursday, September 13, 12
86. Accessing API from
Backbone
Airbnb.Api.getUrl(‘/v1/users/1234’)
Thursday, September 13, 12
87. Accessing API from
Backbone
Airbnb.Api.getUrl(‘/v1/users/1234’)
=> "https://api.airbnb.com/v1/users/1234?currency=USD&locale=en&
key=...&oauth_token=..."
Thursday, September 13, 12
88. Accessing API from
Backbone
class AIR.Models.WishlistUser extends Backbone.Model
jsonKey: 'user'
apiPath: -> "/v1/users/#{@id}"
...
_.extend AIR.Models.WishlistUser.prototype, AIR.Mixins.ApiResource
Thursday, September 13, 12
93. AIR.Views.BaseView
class AIR.Views.BaseView extends Backbone.View
postInitialize: ->
postRender: ->
getRenderData: ->
cleanup: ->
...
Thursday, September 13, 12
94. Before
class WishlistIndexView extends Backbone.View
template: 'wishlists/wishlist_index_view'
render: ->
@$el.html JST[@template](@model.toJSON())
@renderSomeThing()
@
renderSomeThing: -> ...
Thursday, September 13, 12
95. Before
class WishlistIndexView extends Backbone.View
template: 'wishlists/wishlist_index_view'
render: ->
@$el.html JST[@template](@model.toJSON())
@renderSomeThing()
@
renderSomeThing: -> ...
Thursday, September 13, 12
96. Before
class WishlistIndexView extends Backbone.View
template: 'wishlists/wishlist_index_view'
render: ->
@$el.html JST[@template](@model.toJSON())
@renderSomeThing()
@
renderSomeThing: -> ...
Thursday, September 13, 12
97. Before
class WishlistIndexView extends Backbone.View
template: 'wishlists/wishlist_index_view'
render: ->
@$el.html JST[@template](@model.toJSON())
@renderSomeThing()
@
renderSomeThing: -> ...
Thursday, September 13, 12
98. Before
class WishlistIndexView extends Backbone.View
template: 'wishlists/wishlist_index_view'
render: ->
@$el.html JST[@template](@model.toJSON())
@renderSomeThing()
@
renderSomeThing: -> ...
Thursday, September 13, 12
99. Before
class WishlistIndexView extends Backbone.View
template: 'wishlists/wishlist_index_view'
render: ->
@$el.html JST[@template](@model.toJSON())
@renderSomeThing()
@
renderSomeThing: -> ...
Thursday, September 13, 12
100. After
class WishlistIndexView extends AIR.Views.BaseView
template: 'wishlists/wishlist_index_view'
postRender: ->
@renderSomeThing()
renderSomeThing: -> ...
Thursday, September 13, 12
101. After
class WishlistIndexView extends AIR.Views.BaseView
template: 'wishlists/wishlist_index_view'
postRender: ->
@renderSomeThing()
renderSomeThing: -> ...
Thursday, September 13, 12
102. After
class WishlistIndexView extends AIR.Views.BaseView
template: 'wishlists/wishlist_index_view'
postRender: ->
@renderSomeThing()
renderSomeThing: -> ...
Thursday, September 13, 12
103. Before, Part II
class WishlistIndexView extends Backbone.View
...
render: ->
@$el.html JST[@template](@model.toJSON())
@
Thursday, September 13, 12
104. Before, Part II
class WishlistIndexView extends Backbone.View
...
render: ->
data = _.extend @model.toJSON(),
show_share_button: @options.show_share_button
@$el.html JST[@template](data)
@
Thursday, September 13, 12
105. Before, Part II
class WishlistIndexView extends Backbone.View
...
render: ->
@$el.html JST[@template](@getRenderData())
@
getRenderData: ->
_.extend @model.toJSON(),
show_share_button: @options.show_share_button
Thursday, September 13, 12
106. After, Part II
class WishlistIndexView extends AIR.Views.BaseView
...
getRenderData: ->
_.extend super,
show_share_button: @options.show_share_button
Thursday, September 13, 12
107. After, Part II
class WishlistIndexView extends AIR.Views.BaseView
...
getRenderData: ->
_.extend super,
show_share_button: @options.show_share_button
Thursday, September 13, 12
108. cleanup()
class AIR.Views.BaseView extends Backbone.View
...
cleanup: ->
@undelegateEvents()
@model?.off(null, null, @)
@remove()
Thursday, September 13, 12
109. cleanup()
class WishlistIndexView extends AIR.Views.BaseView
...
cleanup: ->
super
@someChildView.cleanup()
clearInterval(@interval)
Thursday, September 13, 12
110. cleanup()
Backbone 0.9.2 adds new method:
Backbone.View.prototype.dispose()
Thursday, September 13, 12
121. What goes into a
view?
• app/assets/coffeescripts/views/shared/privacy_dropdown_view.coffee
Thursday, September 13, 12
122. What goes into a
view?
• app/assets/coffeescripts/views/shared/privacy_dropdown_view.coffee
• app/assets/templates/views/shared/privacy_dropdown_view.hbs
Thursday, September 13, 12
123. What goes into a
view?
• app/assets/coffeescripts/views/shared/privacy_dropdown_view.coffee
• app/assets/templates/views/shared/privacy_dropdown_view.hbs
• app/assets/stylesheets/partials/_ privacy_dropdown_view.scss
Thursday, September 13, 12
124. What goes into a
view?
• app/assets/coffeescripts/views/shared/privacy_dropdown_view.coffee
• app/assets/templates/views/shared/privacy_dropdown_view.hbs
• app/assets/stylesheets/partials/_ privacy_dropdown_view.scss
• lib/phrase_bundles/privacy_dropdown_view.rb
Thursday, September 13, 12
147. PhraseBundle
• Composable bundles of I18n phrases
Thursday, September 13, 12
148. PhraseBundle
• Composable bundles of I18n phrases
• Keep phrases DRY
Thursday, September 13, 12
149. PhraseBundle
• Composable bundles of I18n phrases
• Keep phrases DRY
• Separation of concerns: treat phrases
as data source
Thursday, September 13, 12
152. PhraseBundle
module PhraseBundles
class Wishlists < PhraseBundle
includes :privacy_dropdown, :share_dropdown, :wishlists_modal
def phrases
{
'map_view' => t('wishlists.Map View', :default => 'Map View'),
'list_view' => t('wishlists.List View', :default => 'List View'),
...
}
end
end
end
Thursday, September 13, 12
153. PhraseBundle
module PhraseBundles
class Wishlists < PhraseBundle
includes :privacy_dropdown, :share_dropdown, :wishlists_modal
def phrases
{
'map_view' => t('wishlists.Map View', :default => 'Map View'),
'list_view' => t('wishlists.List View', :default => 'List View'),
...
}
end
end
end
Thursday, September 13, 12
154. PhraseBundle
module PhraseBundles
class Wishlists < PhraseBundle
includes :privacy_dropdown, :share_dropdown, :wishlists_modal
def phrases
{
'map_view' => t('wishlists.Map View', :default => 'Map View'),
'list_view' => t('wishlists.List View', :default => 'List View'),
...
}
end
end
end
Thursday, September 13, 12
155. CDN Asset URLs
• Image paths need to go through Sprockets
Thursday, September 13, 12
156. CDN Asset URLs
• Image paths need to go through Sprockets
Development: https://localhost.airbnb.com:3001
/static/icons/facebook.png
Thursday, September 13, 12
157. CDN Asset URLs
• Image paths need to go through Sprockets
Development: https://localhost.airbnb.com:3001
/static/icons/facebook.png
https://a0.muscache.com/airbnb
Production:
/static/icons/facebook-
e04e8c0c43e40ff7a277a3a7a734ed52.png
Thursday, September 13, 12
164. Backbone.js is just a
stopgap
• Backbone.View is DOM-centric
Thursday, September 13, 12
165. Backbone.js is just a
stopgap
• Backbone.View is DOM-centric
• Backbone.History is window-centric
Thursday, September 13, 12
166. Backbone.js is just a
stopgap
• Backbone.View is DOM-centric
• Backbone.History is window-centric
• Backbone.Model and
Backbone.Collection are more
portable (with override of
Backbone.sync)
Thursday, September 13, 12
167. It’s a great time to be a JavaScript hacker.
Thursday, September 13, 12
168. It’s a great time to be a JavaScript hacker.
But not a great time to build modern,
plug-and-play web apps.
Thursday, September 13, 12
170. Testing the Node.js Waters
We are refactoring m.airbnb.com with a
Node backend instead of Rails.
Thursday, September 13, 12
171. Testing the Node.js Waters
We are refactoring m.airbnb.com with a
Node backend instead of Rails.
Primary goal is to learn how to
productionize a Node app.
Thursday, September 13, 12
172. Testing the Node.js Waters
We are refactoring m.airbnb.com with a
Node backend instead of Rails.
Primary goal is to learn how to
productionize a Node app.
Secondary goal is to prototype a new way
of building web apps.
Thursday, September 13, 12
175. Node Frameworks
Geddy, Tower
Rails-inspired. Not utilizing Node’s strengths.
Thursday, September 13, 12
176. Node Frameworks
Geddy, Tower
Rails-inspired. Not utilizing Node’s strengths.
SocketStream
Modular, real-time, but optimized for The Easy Way.
Thursday, September 13, 12
177. Node Frameworks
Geddy, Tower
Rails-inspired. Not utilizing Node’s strengths.
SocketStream
Modular, real-time, but optimized for The Easy Way.
Meteor
Solves for The Hard Way, but all-or-nothing. Alpha.
Thursday, September 13, 12
178. Node Frameworks
Geddy, Tower
Rails-inspired. Not utilizing Node’s strengths.
SocketStream
Modular, real-time, but optimized for The Easy Way.
Meteor
Solves for The Hard Way, but all-or-nothing. Alpha.
Derby
Solves for The Hard Way, but not very modular. Alpha.
Thursday, September 13, 12
179. Node Frameworks
Active authors.
Active mailing list.
Small, if messy, codebase.
Derby
Solves for The Hard Way, but not very modular. Alpha.
Thursday, September 13, 12
180. Node Frameworks
Derby
Active authors.
Active mailing list.
Small, if messy, codebase.
Solves for The Hard Way, but not very modular. Alpha.
Thursday, September 13, 12
181. Node Frameworks
Derby
Active authors.
Active mailing list.
Small, if messy, codebase.
Solves for The Hard Way, but not very modular. Alpha.
Thursday, September 13, 12
182. Other Resources
Single Page App Book, by Mikito Takada
http://singlepageappbook.com/
view.json, by Mikito Takada
http://mixu.net/view.json/
Building The Next SoundCloud
http://backstage.soundcloud.com/2012/06/building-the-next-
soundcloud/
Sean McBride, Bridging the Client-Server Divide
http://seanmcb.com/client-server-divide/
NodeUp Podcast
http://nodeup.com/
Thursday, September 13, 12