building a single-page app: backbone, node.js, and beyond
Post on 15-Jan-2015
21.447 Views
Preview:
DESCRIPTION
TRANSCRIPT
Building a Single-Page App: Backbone, Node.js, and Beyond
Spike Brehm, Front End Engineerspike@airbnb.com
@spikebrehmSeptember 12, 2012
Thursday, September 13, 12
Past: Why Single-Page Apps
Present: How we built Wish Lists
Future: In pursuit of the Holy Grail
Thursday, September 13, 12
PastWhy Single-Page Apps
Thursday, September 13, 12
Thursday, September 13, 12
Airbedandbreakfast.com
Thursday, September 13, 12
Airbedandbreakfast.com
• Started in 2008 as a Rails 2.x app
Thursday, September 13, 12
Airbedandbreakfast.com
• Started in 2008 as a Rails 2.x app
• Now Rails 3.0
Thursday, September 13, 12
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
What is a Single-Page App?
Thursday, September 13, 12
What is a Single-Page App?
Thursday, September 13, 12
What is a Single-Page App?
Thursday, September 13, 12
What is a Single-Page App?
• Navigate in the app without page refresh
Thursday, September 13, 12
What is a Single-Page App?
• Navigate in the app without page refresh
• Application logic in the client
Thursday, September 13, 12
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
Why Single-Page Apps?
Thursday, September 13, 12
Why Single-Page Apps?
• Faster JavaScript runtimes
Thursday, September 13, 12
Why Single-Page Apps?
• Faster JavaScript runtimes
• New browser features (pushState, localStorage, etc.)
Thursday, September 13, 12
Why Single-Page Apps?
• Faster JavaScript runtimes
• New browser features (pushState, localStorage, etc.)
• Heightened user expectations
Thursday, September 13, 12
Two Approaches
The Easy Way
The Hard Wayaka “The Holy Grail”
Thursday, September 13, 12
The Easy Way
Thursday, September 13, 12
The Easy Way
Thursday, September 13, 12
The Easy Way
• JavaScript app runs entirely in client
Thursday, September 13, 12
The Easy Way
• JavaScript app runs entirely in client
• Server technology agnostic
Thursday, September 13, 12
The Easy Way
• JavaScript app runs entirely in client
• Server technology agnostic
• Can use Backbone to structure app
Thursday, September 13, 12
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
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
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
The Hard Wayaka “The Holy Grail”
Thursday, September 13, 12
The Hard Way
Thursday, September 13, 12
The Hard Way• Routing, templating, application logic, utilities run on
client and server
Thursday, September 13, 12
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
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
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
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
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
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
Performance
Improving performance on Twitter.comhttp://engineering.twitter.com/2012/05/improving-performance-on-twittercom.html
“Time to first tweet”
Thursday, September 13, 12
Stops and Starts
Thursday, September 13, 12
Stops and Starts
• mustache.rb: code duplication
Thursday, September 13, 12
Stops and Starts
• mustache.rb: code duplication
• therubyracer: performance, stability
Thursday, September 13, 12
Stops and Starts
• mustache.rb: code duplication
• therubyracer: performance, stability
• PhantomJS: slow, overly complicated
Thursday, September 13, 12
PresentHow we built Wish Lists
Thursday, September 13, 12
Thursday, September 13, 12
Technologies
Thursday, September 13, 12
Technologies• MV*: Backbone.js
Thursday, September 13, 12
Technologies• MV*: Backbone.js
• Templating: Handlebars
Thursday, September 13, 12
Technologies• MV*: Backbone.js
• Templating: Handlebars
• UI & Layout: Oxygen (Airbnb’s Bootstrap)
Thursday, September 13, 12
Technologies• MV*: Backbone.js
• Templating: Handlebars
• UI & Layout: Oxygen (Airbnb’s Bootstrap)
• CoffeeScript
Thursday, September 13, 12
Technologies• MV*: Backbone.js
• Templating: Handlebars
• UI & Layout: Oxygen (Airbnb’s Bootstrap)
• CoffeeScript
• HTML5 pushState
Thursday, September 13, 12
Technologies• MV*: Backbone.js
• Templating: Handlebars
• UI & Layout: Oxygen (Airbnb’s Bootstrap)
• CoffeeScript
• HTML5 pushState
• api.airbnb.com
Thursday, September 13, 12
Rails-Backbone interface: index.html.erb
<div class=”app_view”></div>
<script>!function(){ I18n.extend(<%= @phrases.to_json.html_safe %>); Airbnb.Api.config(<%= @api_config.to_json.html_safe %>);
window.WishlistsApp = new AIR.Apps.Wishlists( <%= @init_data.to_json.html_safe %> );}();</script>
Thursday, September 13, 12
<div class=”app_view”></div>
<script>!function(){ I18n.extend(<%= @phrases.to_json.html_safe %>); Airbnb.Api.config(<%= @api_config.to_json.html_safe %>);
window.WishlistsApp = new AIR.Apps.Wishlists( <%= @init_data.to_json.html_safe %> );}();</script>
Rails-Backbone interface: index.html.erb
Thursday, September 13, 12
<div class=”app_view”></div>
<script>!function(){ I18n.extend(<%= @phrases.to_json.html_safe %>); Airbnb.Api.config(<%= @api_config.to_json.html_safe %>);
window.WishlistsApp = new AIR.Apps.Wishlists( <%= @init_data.to_json.html_safe %> );}();</script>
Rails-Backbone interface: index.html.erb
Thursday, September 13, 12
<div class=”app_view”></div>
<script>!function(){ I18n.extend(<%= @phrases.to_json.html_safe %>); Airbnb.Api.config(<%= @api_config.to_json.html_safe %>);
window.WishlistsApp = new AIR.Apps.Wishlists( <%= @init_data.to_json.html_safe %> );}();</script>
Rails-Backbone interface: index.html.erb
Thursday, September 13, 12
<div class=”app_view”></div>
<script>!function(){ I18n.extend(<%= @phrases.to_json.html_safe %>); Airbnb.Api.config(<%= @api_config.to_json.html_safe %>);
window.WishlistsApp = new AIR.Apps.Wishlists( <%= @init_data.to_json.html_safe %> );}();</script>
Rails-Backbone interface: index.html.erb
Thursday, September 13, 12
<div class=”app_view”></div>
<script>!function(){ I18n.extend(<%= @phrases.to_json.html_safe %>); Airbnb.Api.config(<%= @api_config.to_json.html_safe %>);
window.WishlistsApp = new AIR.Apps.Wishlists( <%= @init_data.to_json.html_safe %> );}();</script>
Rails-Backbone interface: index.html.erb
Thursday, September 13, 12
<div class=”app_view”></div>
<script>!function(){ I18n.extend(<%= @phrases.to_json.html_safe %>); Airbnb.Api.config(<%= @api_config.to_json.html_safe %>);
window.WishlistsApp = new AIR.Apps.Wishlists( <%= @init_data.to_json.html_safe %> );}();</script>
Rails-Backbone interface: index.html.erb
Thursday, September 13, 12
Bootstrapping the app window.WishlistsApp = new AIR.Apps.Wishlists({ “listings”: [...], “wishlists”: [...], ... });
Thursday, September 13, 12
Bootstrapping the app window.WishlistsApp = new AIR.Apps.Wishlists({ “listings”: [...], “wishlists”: [...], ... });
WishlistsApp.get(‘wishlists’)
=> [Object, Object, Object, ...]
Thursday, September 13, 12
Bootstrapping the app
Thursday, September 13, 12
• Each action bootstraps whatever data needed on first pageload
Bootstrapping the app
Thursday, September 13, 12
• Each action bootstraps whatever data needed on first pageload
• Subsequent data is requested on-demand
Bootstrapping the app
Thursday, September 13, 12
App Initializeclass 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
App Initializeclass 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
Backbone Router
Thursday, September 13, 12
Backbone Router• Translates URL changes to method
calls
Thursday, September 13, 12
Backbone Router• Translates URL changes to method
calls
• Source of global app state
Thursday, September 13, 12
Backbone Router• Translates URL changes to method
calls
• Source of global app state
• Keep state out of views
Thursday, September 13, 12
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
Backbone Routerclass AIR.Routers.Wishlists extends Backbone.Router routes: 'wishlists/:id' : 'show' 'wishlists/:id/edit' : 'edit'
...
show: (id) -> @app.fetchWishlist id, (model) => view = new AIR.Views.Wishlists.ShowView {@app, model} @updateContent(view)
Thursday, September 13, 12
Backbone Routerclass AIR.Routers.Wishlists extends Backbone.Router routes: 'wishlists/:id' : 'show' 'wishlists/:id/edit' : 'edit'
...
show: (id) -> @app.fetchWishlist id, (model) => view = new AIR.Views.Wishlists.ShowView {@app, model} @updateContent(view)
Thursday, September 13, 12
Backbone Routerclass AIR.Routers.Wishlists extends Backbone.Router routes: 'wishlists/:id' : 'show' 'wishlists/:id/edit' : 'edit'
...
show: (id) -> @app.fetchWishlist id, (model) => view = new AIR.Views.Wishlists.ShowView {@app, model} @updateContent(view)
Thursday, September 13, 12
Backbone Routerclass AIR.Routers.Wishlists extends Backbone.Router routes: 'wishlists/:id' : 'show' 'wishlists/:id/edit' : 'edit'
...
show: (id) -> @app.fetchWishlist id, (model) => view = new AIR.Views.Wishlists.ShowView {@app, model} @updateContent(view)
Thursday, September 13, 12
Data-on-demand
Thursday, September 13, 12
Data-on-demand # AIR.Apps.Wishlists
fetchWishlist: (id, callback) -> model = @wishlists.get(id) if model? callback(model) else @appView.setLoading(true) @wishlists.fetchById id, (model) => @appView.setLoading(false) callback(model)
Thursday, September 13, 12
Data-on-demand # AIR.Apps.Wishlists
fetchWishlist: (id, callback) -> model = @wishlists.get(id) if model? callback(model) else @appView.setLoading(true) @wishlists.fetchById id, (model) => @appView.setLoading(false) callback(model)
Thursday, September 13, 12
Data-on-demand # AIR.Apps.Wishlists
fetchWishlist: (id, callback) -> model = @wishlists.get(id) if model? callback(model) else @appView.setLoading(true) @wishlists.fetchById id, (model) => @appView.setLoading(false) callback(model)
Thursday, September 13, 12
Data-on-demand # AIR.Apps.Wishlists
fetchWishlist: (id, callback) -> model = @wishlists.get(id) if model? callback(model) else @appView.setLoading(true) @wishlists.fetchById id, (model) => @appView.setLoading(false) callback(model)
Thursday, September 13, 12
Data-on-demand # AIR.Apps.Wishlists
fetchWishlist: (id, callback) -> model = @wishlists.get(id) if model? callback(model) else @appView.setLoading(true) @wishlists.fetchById id, (model) => @appView.setLoading(false) callback(model)
Thursday, September 13, 12
Data-on-demand # AIR.Apps.Wishlists
fetchWishlist: (id, callback) -> model = @wishlists.get(id) if model? callback(model) else @appView.setLoading(true) @wishlists.fetchById id, (model) => @appView.setLoading(false) callback(model)
Thursday, September 13, 12
api.airbnb.com
Thursday, September 13, 12
api.airbnb.com• Used by iOS, Android, Mobile Web clients
Thursday, September 13, 12
api.airbnb.com• Used by iOS, Android, Mobile Web clients
• No Cross-Domain XHR
Thursday, September 13, 12
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
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
Accessing API from Backbone
Airbnb.Api.getUrl(‘/v1/users/1234’)
Thursday, September 13, 12
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
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
Accessing API from Backbone
AIR.Mixins.ApiResource = url: (options = {}) -> apiPath = options.apiPath || @apiPath if _.isFunction(apiPath)
apiPath = apiPath.call(@) Airbnb.Api.getUrl(apiPath)
sync: (method, model, options) -> options = _.defaults options, url: @url(options) Backbone.sync method, model, options
Thursday, September 13, 12
Accessing API from Backbone
AIR.Mixins.ApiResource = url: (options = {}) -> apiPath = options.apiPath || @apiPath if _.isFunction(apiPath)
apiPath = apiPath.call(@) Airbnb.Api.getUrl(apiPath)
sync: (method, model, options) -> options = _.defaults options, url: @url(options) Backbone.sync method, model, options
Thursday, September 13, 12
Accessing API from Backbone
AIR.Mixins.ApiResource = url: (options = {}) -> apiPath = options.apiPath || @apiPath if _.isFunction(apiPath)
apiPath = apiPath.call(@) Airbnb.Api.getUrl(apiPath)
sync: (method, model, options) -> options = _.defaults options, url: @url(options) Backbone.sync method, model, options
Thursday, September 13, 12
Accessing API from Backbone
AIR.Mixins.ApiResource = url: (options = {}) -> apiPath = options.apiPath || @apiPath if _.isFunction(apiPath)
apiPath = apiPath.call(@) Airbnb.Api.getUrl(apiPath)
sync: (method, model, options) -> options = _.defaults options, url: @url(options) Backbone.sync method, model, options
Thursday, September 13, 12
AIR.Views.BaseViewclass AIR.Views.BaseView extends Backbone.View
postInitialize: ->
postRender: ->
getRenderData: ->
cleanup: ->
...
Thursday, September 13, 12
Beforeclass WishlistIndexView extends Backbone.View
template: 'wishlists/wishlist_index_view'
render: ->
@$el.html JST[@template](@model.toJSON())
@renderSomeThing()
@
renderSomeThing: -> ...
Thursday, September 13, 12
Beforeclass WishlistIndexView extends Backbone.View
template: 'wishlists/wishlist_index_view'
render: ->
@$el.html JST[@template](@model.toJSON())
@renderSomeThing()
@
renderSomeThing: -> ...
Thursday, September 13, 12
Beforeclass WishlistIndexView extends Backbone.View
template: 'wishlists/wishlist_index_view'
render: ->
@$el.html JST[@template](@model.toJSON())
@renderSomeThing()
@
renderSomeThing: -> ...
Thursday, September 13, 12
Beforeclass WishlistIndexView extends Backbone.View
template: 'wishlists/wishlist_index_view'
render: ->
@$el.html JST[@template](@model.toJSON())
@renderSomeThing()
@
renderSomeThing: -> ...
Thursday, September 13, 12
Beforeclass WishlistIndexView extends Backbone.View
template: 'wishlists/wishlist_index_view'
render: ->
@$el.html JST[@template](@model.toJSON())
@renderSomeThing()
@
renderSomeThing: -> ...
Thursday, September 13, 12
Beforeclass WishlistIndexView extends Backbone.View
template: 'wishlists/wishlist_index_view'
render: ->
@$el.html JST[@template](@model.toJSON())
@renderSomeThing()
@
renderSomeThing: -> ...
Thursday, September 13, 12
Afterclass WishlistIndexView extends AIR.Views.BaseView
template: 'wishlists/wishlist_index_view'
postRender: ->
@renderSomeThing()
renderSomeThing: -> ...
Thursday, September 13, 12
Afterclass WishlistIndexView extends AIR.Views.BaseView
template: 'wishlists/wishlist_index_view'
postRender: ->
@renderSomeThing()
renderSomeThing: -> ...
Thursday, September 13, 12
Afterclass WishlistIndexView extends AIR.Views.BaseView
template: 'wishlists/wishlist_index_view'
postRender: ->
@renderSomeThing()
renderSomeThing: -> ...
Thursday, September 13, 12
Before, Part IIclass WishlistIndexView extends Backbone.View
...
render: ->
@$el.html JST[@template](@model.toJSON())
@
Thursday, September 13, 12
Before, Part IIclass 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
Before, Part IIclass 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
After, Part IIclass WishlistIndexView extends AIR.Views.BaseView
...
getRenderData: ->
_.extend super,
show_share_button: @options.show_share_button
Thursday, September 13, 12
After, Part IIclass WishlistIndexView extends AIR.Views.BaseView
...
getRenderData: ->
_.extend super,
show_share_button: @options.show_share_button
Thursday, September 13, 12
cleanup()class AIR.Views.BaseView extends Backbone.View
...
cleanup: ->
@undelegateEvents()
@model?.off(null, null, @)
@remove()
Thursday, September 13, 12
cleanup()class WishlistIndexView extends AIR.Views.BaseView
...
cleanup: ->
super
@someChildView.cleanup()
clearInterval(@interval)
Thursday, September 13, 12
cleanup()
Backbone 0.9.2 adds new method: Backbone.View.prototype.dispose()
Thursday, September 13, 12
Modular, DRY views
Thursday, September 13, 12
Modular, DRY views
• Re-usable bits of markup and behavior
Thursday, September 13, 12
Modular, DRY views
• (screenshot)
Thursday, September 13, 12
Modular, DRY views
• (screenshot)
Thursday, September 13, 12
Subview initializationclass EditView extends AIR.Views.BaseView
...
postRender: ->
@renderPrivacyDropdown()
renderPrivacyDropdown: ->
view = new AIR.Views.Shared.PrivacyDropdownView
'private': @model.get('private')
@$('data-privacy-dropdown').replaceWith view.render().el
Thursday, September 13, 12
Subview initializationclass EditView extends AIR.Views.BaseView
...
postRender: ->
@renderPrivacyDropdown()
renderPrivacyDropdown: ->
view = new AIR.Views.Shared.PrivacyDropdownView
'private': @model.get('private')
@$('data-privacy-dropdown').replaceWith view.render().el
Thursday, September 13, 12
Subview initializationclass EditView extends AIR.Views.BaseView
...
postRender: ->
@renderPrivacyDropdown()
renderPrivacyDropdown: ->
view = new AIR.Views.Shared.PrivacyDropdownView
'private': @model.get('private')
@$('data-privacy-dropdown').replaceWith view.render().el
Thursday, September 13, 12
Subview initializationclass EditView extends AIR.Views.BaseView
...
postRender: ->
@renderPrivacyDropdown()
renderPrivacyDropdown: ->
view = new AIR.Views.Shared.PrivacyDropdownView
'private': @model.get('private')
@$('data-privacy-dropdown').replaceWith view.render().el
Thursday, September 13, 12
Subview initializationclass EditView extends AIR.Views.BaseView
...
postRender: ->
@renderPrivacyDropdown()
renderPrivacyDropdown: ->
view = new AIR.Views.Shared.PrivacyDropdownView
'private': @model.get('private')
view.on ‘private-changed’, (isPrivate) =>
# do something
console.log(isPrivate)
@$('data-privacy-dropdown').replaceWith view.render().el
Thursday, September 13, 12
What goes into a view?
Thursday, September 13, 12
What goes into a view?
• app/assets/coffeescripts/views/shared/privacy_dropdown_view.coffee
Thursday, September 13, 12
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
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
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
Rdio’s Backbone-based View Component FrameworkJustin Tulloss, @justin_tullosshttp://www.youtube.com/watch?v=TB-l2nF67iU
Thursday, September 13, 12
Modular, DRY views
• (screenshot)
Thursday, September 13, 12
Infinity.js
• (screenshot)
http://airbnb.github.com/infinity
Thursday, September 13, 12
I18n.js
Thursday, September 13, 12
I18n.js
• 192 countries
Thursday, September 13, 12
I18n.js
• 192 countries
• 31 locales
Thursday, September 13, 12
I18n.js
• 192 countries
• 31 locales
• Client-slide translation library
Thursday, September 13, 12
I18n.t()I18n.t('edit_wish_list');
Thursday, September 13, 12
I18n.t()I18n.t('edit_wish_list');
"Edit Wish List"
Thursday, September 13, 12
I18n.t()I18n.t('edit_wish_list');
<h1>{{t "edit_wish_list"}}</h1>
"Edit Wish List"
Thursday, September 13, 12
I18n.t()I18n.t('edit_wish_list');
<h1>Edit Wish List</h1>
<h1>{{t "edit_wish_list"}}</h1>
"Edit Wish List"
Thursday, September 13, 12
InterpolationI18n.t('owners_wish_list', {name: name});
Thursday, September 13, 12
InterpolationI18n.t('owners_wish_list', {name: name});
"Spike’s Wish List"
Thursday, September 13, 12
Interpolation
<h1>{{t "owners_wish_list" name=name}}</h1>
I18n.t('owners_wish_list', {name: name});
"Spike’s Wish List"
Thursday, September 13, 12
Interpolation
<h1>{{t "owners_wish_list" name=name}}</h1>
<h1>Spike’s Wish List</h1>
I18n.t('owners_wish_list', {name: name});
"Spike’s Wish List"
Thursday, September 13, 12
I18n.extend()
I18n.extend({ "edit_wish_list": "Edit Wish List", "owners_wish_list": "%{name}’s Wish List", ...});
Thursday, September 13, 12
I18n.pluralize()I18n.pluralize("Listing", listings);
Thursday, September 13, 12
I18n.pluralize()I18n.pluralize("Listing", listings);
3 Listings
Thursday, September 13, 12
I18n.pluralize()
<span>{{t_pluralize "Listing" count=listings}}</span>
I18n.pluralize("Listing", listings);
3 Listings
Thursday, September 13, 12
I18n.pluralize()
<span>{{t_pluralize "Listing" count=listings}}</span>
<span>3 Listings</span>
I18n.pluralize("Listing", listings);
3 Listings
Thursday, September 13, 12
pluralize() just calls t(){
"pluralize.Listing.zero": "%{count} Listings",
"pluralize.Listing.one": "%{count} Listing",
"pluralize.Listing.many": "%{count} Listings"
}
Thursday, September 13, 12
PhraseBundle
Thursday, September 13, 12
PhraseBundle• Composable bundles of I18n phrases
Thursday, September 13, 12
PhraseBundle• Composable bundles of I18n phrases
• Keep phrases DRY
Thursday, September 13, 12
PhraseBundle• Composable bundles of I18n phrases
• Keep phrases DRY
• Separation of concerns: treat phrases as data source
Thursday, September 13, 12
PhraseBundleI18n.extend(<%= { 'map_view' => t('wishlists.Map View', :default => 'Map View'), 'list_view' => t('wishlists.List View', :default => 'List View'), ...}.to_json.html_safe %>);
Thursday, September 13, 12
PhraseBundleI18n.extend(<%= { 'map_view' => t('wishlists.Map View', :default => 'Map View'), 'list_view' => t('wishlists.List View', :default => 'List View'), ...}.to_json.html_safe %>);
I18n.extend(<%= PhraseBundles::Wishlists.new.to_json.html_safe %>);
Thursday, September 13, 12
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 endend
PhraseBundle
Thursday, September 13, 12
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 endend
PhraseBundle
Thursday, September 13, 12
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 endend
PhraseBundle
Thursday, September 13, 12
CDN Asset URLs• Image paths need to go through Sprockets
Thursday, September 13, 12
CDN Asset URLs
https://localhost.airbnb.com:3001/static/icons/facebook.png
• Image paths need to go through Sprockets
Development:
Thursday, September 13, 12
CDN Asset URLs
https://localhost.airbnb.com:3001/static/icons/facebook.png
https://a0.muscache.com/airbnb/static/icons/facebook-e04e8c0c43e40ff7a277a3a7a734ed52.png
• Image paths need to go through Sprockets
Development:
Production:
Thursday, September 13, 12
CDN Asset URLs
Thursday, September 13, 12
CDN Asset URLswindow.ImagePaths = <%= map_image_paths([ 'icons/facebook.png', ...]).to_json.html_safe %>;
Thursday, September 13, 12
CDN Asset URLswindow.ImagePaths = <%= map_image_paths([ 'icons/facebook.png', ...]).to_json.html_safe %>;
ImagePaths['icons/facebook.png'];=> “https://a0.muscache.com/airbnb
/static/icons/facebook-e04e8c0c43e40ff7a277a3a7a734ed52.png”
Thursday, September 13, 12
CDN Asset URLswindow.ImagePaths = <%= map_image_paths([ 'icons/facebook.png', ...]).to_json.html_safe %>;
<img src=”{{image_path “icons/facebook.png”}}” ...>
ImagePaths['icons/facebook.png'];=> “https://a0.muscache.com/airbnb
/static/icons/facebook-e04e8c0c43e40ff7a277a3a7a734ed52.png”
Thursday, September 13, 12
FutureIn pursuit of the Holy Grail
Thursday, September 13, 12
Backbone.js is just a stopgap
Thursday, September 13, 12
Backbone.js is just a stopgap
• Backbone.View is DOM-centric
Thursday, September 13, 12
Backbone.js is just a stopgap
• Backbone.View is DOM-centric
• Backbone.History is window-centric
Thursday, September 13, 12
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
It’s a great time to be a JavaScript hacker.
Thursday, September 13, 12
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
Testing the Node.js Waters
Thursday, September 13, 12
Testing the Node.js WatersWe are refactoring m.airbnb.com with a Node backend instead of Rails.
Thursday, September 13, 12
Testing the Node.js WatersWe 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
Testing the Node.js WatersWe 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
Testing the Node.js Waters
Thursday, September 13, 12
Node Frameworks
Thursday, September 13, 12
Node FrameworksGeddy, TowerRails-inspired. Not utilizing Node’s strengths.
Thursday, September 13, 12
Node FrameworksGeddy, TowerRails-inspired. Not utilizing Node’s strengths.
SocketStreamModular, real-time, but optimized for The Easy Way.
Thursday, September 13, 12
Node FrameworksGeddy, TowerRails-inspired. Not utilizing Node’s strengths.
SocketStreamModular, real-time, but optimized for The Easy Way.
MeteorSolves for The Hard Way, but all-or-nothing. Alpha.
Thursday, September 13, 12
Node FrameworksGeddy, TowerRails-inspired. Not utilizing Node’s strengths.
SocketStreamModular, real-time, but optimized for The Easy Way.
MeteorSolves for The Hard Way, but all-or-nothing. Alpha.
DerbySolves for The Hard Way, but not very modular. Alpha.
Thursday, September 13, 12
Node Frameworks
Solves for The Hard Way, but not very modular. Alpha.Derby
Active authors.Active mailing list.Small, if messy, codebase.
Thursday, September 13, 12
Node Frameworks
Solves for The Hard Way, but not very modular. Alpha.
DerbyActive authors.Active mailing list.Small, if messy, codebase.
Thursday, September 13, 12
Node Frameworks
Solves for The Hard Way, but not very modular. Alpha.
DerbyActive authors.Active mailing list.Small, if messy, codebase.
Thursday, September 13, 12
Other ResourcesSingle Page App Book, by Mikito Takadahttp://singlepageappbook.com/
view.json, by Mikito Takadahttp://mixu.net/view.json/
Building The Next SoundCloudhttp://backstage.soundcloud.com/2012/06/building-the-next-soundcloud/
Sean McBride, Bridging the Client-Server Dividehttp://seanmcb.com/client-server-divide/
NodeUp Podcasthttp://nodeup.com/
Thursday, September 13, 12
res.end()
Thursday, September 13, 12
Let’s chat@spikebrehm
Thursday, September 13, 12
top related