building a single-page app: backbone, node.js, and beyond

Post on 15-Jan-2015

21.447 Views

Category:

Documents

1 Downloads

Preview:

Click to see full reader

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

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

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

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