SlideShare uma empresa Scribd logo
1 de 184
Baixar para ler offline
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
Past: Why Single-Page Apps
                        Present: How we built Wish Lists
                        Future: In pursuit of the Holy Grail




Thursday, September 13, 12
Past
                             Why 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 Way
                               aka “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 Way
             aka “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.com
                    http://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
Present
                             How 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
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
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
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
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
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
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
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
Bootstrapping the app

                    • Each action bootstraps whatever data
                             needed on first pageload




Thursday, September 13, 12
Bootstrapping the app

                    • Each action bootstraps whatever data
                             needed on first pageload

                    • Subsequent data is requested on-
                             demand




Thursday, September 13, 12
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
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
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 Router
    class 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 Router
    class 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 Router
    class 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 Router
    class 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.BaseView
           class AIR.Views.BaseView extends Backbone.View
                 postInitialize: ->
                 postRender: ->
                 getRenderData: ->
                 cleanup: ->
                 ...




Thursday, September 13, 12
Before
      class WishlistIndexView extends Backbone.View

           template: 'wishlists/wishlist_index_view'



           render: ->

                @$el.html JST[@template](@model.toJSON())

                @renderSomeThing()

                @



           renderSomeThing: -> ...




Thursday, September 13, 12
Before
      class WishlistIndexView extends Backbone.View

           template: 'wishlists/wishlist_index_view'



           render: ->

                @$el.html JST[@template](@model.toJSON())

                @renderSomeThing()

                @



           renderSomeThing: -> ...




Thursday, September 13, 12
Before
      class WishlistIndexView extends Backbone.View

           template: 'wishlists/wishlist_index_view'



           render: ->

                @$el.html JST[@template](@model.toJSON())

                @renderSomeThing()

                @



           renderSomeThing: -> ...




Thursday, September 13, 12
Before
      class WishlistIndexView extends Backbone.View

           template: 'wishlists/wishlist_index_view'



           render: ->

                @$el.html JST[@template](@model.toJSON())

                @renderSomeThing()

                @



           renderSomeThing: -> ...




Thursday, September 13, 12
Before
      class WishlistIndexView extends Backbone.View

           template: 'wishlists/wishlist_index_view'



           render: ->

                @$el.html JST[@template](@model.toJSON())

                @renderSomeThing()

                @



           renderSomeThing: -> ...




Thursday, September 13, 12
Before
      class WishlistIndexView extends Backbone.View

           template: 'wishlists/wishlist_index_view'



           render: ->

                @$el.html JST[@template](@model.toJSON())

                @renderSomeThing()

                @



           renderSomeThing: -> ...




Thursday, September 13, 12
After
      class WishlistIndexView extends AIR.Views.BaseView

           template: 'wishlists/wishlist_index_view'



           postRender: ->

                @renderSomeThing()



           renderSomeThing: -> ...




Thursday, September 13, 12
After
      class WishlistIndexView extends AIR.Views.BaseView

           template: 'wishlists/wishlist_index_view'



           postRender: ->

                @renderSomeThing()



           renderSomeThing: -> ...




Thursday, September 13, 12
After
      class WishlistIndexView extends AIR.Views.BaseView

           template: 'wishlists/wishlist_index_view'



           postRender: ->

                @renderSomeThing()



           renderSomeThing: -> ...




Thursday, September 13, 12
Before, Part II
      class WishlistIndexView extends Backbone.View

           ...



           render: ->

                @$el.html JST[@template](@model.toJSON())

                @




Thursday, September 13, 12
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
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
After, Part II
      class WishlistIndexView extends AIR.Views.BaseView

           ...



           getRenderData: ->

                _.extend super,

                     show_share_button: @options.show_share_button




Thursday, September 13, 12
After, Part II
      class 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 initialization
             class 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 initialization
             class 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 initialization
             class 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 initialization
             class 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 initialization
             class 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 Framework
                    Justin Tulloss, @justin_tulloss
                    http://www.youtube.com/watch?v=TB-l2nF67iU




Thursday, September 13, 12
Modular, DRY views


                             • (screenshot)



Thursday, September 13, 12
http://airbnb.github.com/infinity

                                    Infinity.js


                             • (screenshot)



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');

                   "Edit Wish List"



                   <h1>{{t "edit_wish_list"}}</h1>




Thursday, September 13, 12
I18n.t()
                   I18n.t('edit_wish_list');

                   "Edit Wish List"



                   <h1>{{t "edit_wish_list"}}</h1>

                   <h1>Edit Wish List</h1>




Thursday, September 13, 12
Interpolation
      I18n.t('owners_wish_list', {name: name});




Thursday, September 13, 12
Interpolation
      I18n.t('owners_wish_list', {name: name});

      "Spike’s Wish List"




Thursday, September 13, 12
Interpolation
      I18n.t('owners_wish_list', {name: name});

      "Spike’s Wish List"



      <h1>{{t "owners_wish_list" name=name}}</h1>




Thursday, September 13, 12
Interpolation
      I18n.t('owners_wish_list', {name: name});

      "Spike’s Wish List"



      <h1>{{t "owners_wish_list" name=name}}</h1>

      <h1>Spike’s Wish List</h1>




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()
    I18n.pluralize("Listing", listings);

     3 Listings




    <span>{{t_pluralize "Listing" count=listings}}</span>




Thursday, September 13, 12
I18n.pluralize()
    I18n.pluralize("Listing", listings);

     3 Listings




    <span>{{t_pluralize "Listing" count=listings}}</span>

   <span>3 Listings</span>




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
PhraseBundle
   I18n.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
PhraseBundle
   I18n.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
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
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
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
CDN Asset URLs
             • Image paths need to go through Sprockets




Thursday, September 13, 12
CDN Asset URLs
             • Image paths need to go through Sprockets
           Development:        https://localhost.airbnb.com:3001
                               /static/icons/facebook.png




Thursday, September 13, 12
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
CDN Asset URLs




Thursday, September 13, 12
CDN Asset URLs
         window.ImagePaths = <%= map_image_paths([
           'icons/facebook.png',
           ...
         ]).to_json.html_safe %>;




Thursday, September 13, 12
CDN Asset URLs
         window.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 URLs
         window.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”



         <img src=”{{image_path “icons/facebook.png”}}” ...>



Thursday, September 13, 12
Future
                             In 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 Waters
        We are refactoring m.airbnb.com with a
        Node backend instead of Rails.




Thursday, September 13, 12
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
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
Testing the Node.js Waters




Thursday, September 13, 12
Node Frameworks




Thursday, September 13, 12
Node Frameworks
        Geddy, Tower
        Rails-inspired. Not utilizing Node’s strengths.




Thursday, September 13, 12
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
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
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
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
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
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
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
res.end()



Thursday, September 13, 12
Let’s chat
                               @spikebrehm




Thursday, September 13, 12

Mais conteúdo relacionado

Semelhante a Building a Single-Page App: Backbone, Node.js, and Beyond

Aloha on-rails-2009
Aloha on-rails-2009Aloha on-rails-2009
Aloha on-rails-2009John Woodell
 
A Modern Framework for Amazon Elastic MapReduce (BDT309) | AWS re:Invent 2013
A Modern Framework for Amazon Elastic MapReduce (BDT309) | AWS re:Invent 2013A Modern Framework for Amazon Elastic MapReduce (BDT309) | AWS re:Invent 2013
A Modern Framework for Amazon Elastic MapReduce (BDT309) | AWS re:Invent 2013Amazon Web Services
 
Hyves: Mobile app development with HTML5 and Javascript
Hyves: Mobile app development with HTML5 and JavascriptHyves: Mobile app development with HTML5 and Javascript
Hyves: Mobile app development with HTML5 and Javascriptnlwebperf
 
Performance & Responsive Web Design
Performance & Responsive Web DesignPerformance & Responsive Web Design
Performance & Responsive Web DesignZach Leatherman
 
One Page, One App -or- How to Write a Crawlable Single Page Web App
One Page, One App -or- How to Write a Crawlable Single Page Web AppOne Page, One App -or- How to Write a Crawlable Single Page Web App
One Page, One App -or- How to Write a Crawlable Single Page Web Apptechnicolorenvy
 
SmugMug's Zero-Downtime Migration to AWS (ARC312) | AWS re:Invent 2013
SmugMug's Zero-Downtime Migration to AWS (ARC312) | AWS re:Invent 2013SmugMug's Zero-Downtime Migration to AWS (ARC312) | AWS re:Invent 2013
SmugMug's Zero-Downtime Migration to AWS (ARC312) | AWS re:Invent 2013Amazon Web Services
 
Step by Step Mobile Optimization
Step by Step Mobile OptimizationStep by Step Mobile Optimization
Step by Step Mobile OptimizationGuy Podjarny
 
Tech Talk: RocksDB Slides by Dhruba Borthakur & Haobo Xu of Facebook
Tech Talk: RocksDB Slides by Dhruba Borthakur & Haobo Xu of FacebookTech Talk: RocksDB Slides by Dhruba Borthakur & Haobo Xu of Facebook
Tech Talk: RocksDB Slides by Dhruba Borthakur & Haobo Xu of FacebookThe Hive
 
Cloud4all Architecture Overview
Cloud4all Architecture OverviewCloud4all Architecture Overview
Cloud4all Architecture Overviewicchp2012
 
Building businesspost.ie using Node.js
Building businesspost.ie using Node.jsBuilding businesspost.ie using Node.js
Building businesspost.ie using Node.jsRichard Rodger
 
Optimizing web performance (Fronteers edition)
Optimizing web performance (Fronteers edition)Optimizing web performance (Fronteers edition)
Optimizing web performance (Fronteers edition)Dave Olsen
 
Measuring Web Performance
Measuring Web Performance Measuring Web Performance
Measuring Web Performance Dave Olsen
 
Proud to be polyglot!
Proud to be polyglot!Proud to be polyglot!
Proud to be polyglot!NLJUG
 
An Introduction to AngularJS
An Introduction to AngularJSAn Introduction to AngularJS
An Introduction to AngularJSFalk Hartmann
 

Semelhante a Building a Single-Page App: Backbone, Node.js, and Beyond (20)

Aloha on-rails-2009
Aloha on-rails-2009Aloha on-rails-2009
Aloha on-rails-2009
 
Play
PlayPlay
Play
 
NATO IST Symposium 2013
NATO IST Symposium 2013NATO IST Symposium 2013
NATO IST Symposium 2013
 
A Modern Framework for Amazon Elastic MapReduce (BDT309) | AWS re:Invent 2013
A Modern Framework for Amazon Elastic MapReduce (BDT309) | AWS re:Invent 2013A Modern Framework for Amazon Elastic MapReduce (BDT309) | AWS re:Invent 2013
A Modern Framework for Amazon Elastic MapReduce (BDT309) | AWS re:Invent 2013
 
Hyves: Mobile app development with HTML5 and Javascript
Hyves: Mobile app development with HTML5 and JavascriptHyves: Mobile app development with HTML5 and Javascript
Hyves: Mobile app development with HTML5 and Javascript
 
Performance & Responsive Web Design
Performance & Responsive Web DesignPerformance & Responsive Web Design
Performance & Responsive Web Design
 
Rubypalooza 2009
Rubypalooza 2009Rubypalooza 2009
Rubypalooza 2009
 
One Page, One App -or- How to Write a Crawlable Single Page Web App
One Page, One App -or- How to Write a Crawlable Single Page Web AppOne Page, One App -or- How to Write a Crawlable Single Page Web App
One Page, One App -or- How to Write a Crawlable Single Page Web App
 
Apache performance
Apache performanceApache performance
Apache performance
 
SmugMug's Zero-Downtime Migration to AWS (ARC312) | AWS re:Invent 2013
SmugMug's Zero-Downtime Migration to AWS (ARC312) | AWS re:Invent 2013SmugMug's Zero-Downtime Migration to AWS (ARC312) | AWS re:Invent 2013
SmugMug's Zero-Downtime Migration to AWS (ARC312) | AWS re:Invent 2013
 
The PRPL Pattern
The PRPL PatternThe PRPL Pattern
The PRPL Pattern
 
Html5 tx - preso
Html5 tx - presoHtml5 tx - preso
Html5 tx - preso
 
Step by Step Mobile Optimization
Step by Step Mobile OptimizationStep by Step Mobile Optimization
Step by Step Mobile Optimization
 
Tech Talk: RocksDB Slides by Dhruba Borthakur & Haobo Xu of Facebook
Tech Talk: RocksDB Slides by Dhruba Borthakur & Haobo Xu of FacebookTech Talk: RocksDB Slides by Dhruba Borthakur & Haobo Xu of Facebook
Tech Talk: RocksDB Slides by Dhruba Borthakur & Haobo Xu of Facebook
 
Cloud4all Architecture Overview
Cloud4all Architecture OverviewCloud4all Architecture Overview
Cloud4all Architecture Overview
 
Building businesspost.ie using Node.js
Building businesspost.ie using Node.jsBuilding businesspost.ie using Node.js
Building businesspost.ie using Node.js
 
Optimizing web performance (Fronteers edition)
Optimizing web performance (Fronteers edition)Optimizing web performance (Fronteers edition)
Optimizing web performance (Fronteers edition)
 
Measuring Web Performance
Measuring Web Performance Measuring Web Performance
Measuring Web Performance
 
Proud to be polyglot!
Proud to be polyglot!Proud to be polyglot!
Proud to be polyglot!
 
An Introduction to AngularJS
An Introduction to AngularJSAn Introduction to AngularJS
An Introduction to AngularJS
 

Mais de Spike Brehm

Managing Through Chaos (w/ presenter notes)
Managing Through Chaos (w/ presenter notes)Managing Through Chaos (w/ presenter notes)
Managing Through Chaos (w/ presenter notes)Spike Brehm
 
Managing Through Chaos
Managing Through ChaosManaging Through Chaos
Managing Through ChaosSpike Brehm
 
The Evolution of Airbnb's Frontend
The Evolution of Airbnb's FrontendThe Evolution of Airbnb's Frontend
The Evolution of Airbnb's FrontendSpike Brehm
 
Integrating Browserify with Sprockets
Integrating Browserify with SprocketsIntegrating Browserify with Sprockets
Integrating Browserify with SprocketsSpike Brehm
 
Building Isomorphic Apps (JSConf.Asia 2014)
Building Isomorphic Apps (JSConf.Asia 2014)Building Isomorphic Apps (JSConf.Asia 2014)
Building Isomorphic Apps (JSConf.Asia 2014)Spike Brehm
 
JSConf US 2014: Building Isomorphic Apps
JSConf US 2014: Building Isomorphic AppsJSConf US 2014: Building Isomorphic Apps
JSConf US 2014: Building Isomorphic AppsSpike Brehm
 
In Pursuit of the Holy Grail: Building Isomorphic JavaScript Apps
In Pursuit of the Holy Grail: Building Isomorphic JavaScript AppsIn Pursuit of the Holy Grail: Building Isomorphic JavaScript Apps
In Pursuit of the Holy Grail: Building Isomorphic JavaScript AppsSpike Brehm
 
General Assembly Workshop: Advanced JavaScript
General Assembly Workshop: Advanced JavaScriptGeneral Assembly Workshop: Advanced JavaScript
General Assembly Workshop: Advanced JavaScriptSpike Brehm
 
Isomorphic JavaScript: #DevBeat Master Class
Isomorphic JavaScript: #DevBeat Master ClassIsomorphic JavaScript: #DevBeat Master Class
Isomorphic JavaScript: #DevBeat Master ClassSpike Brehm
 
Introducing Rendr: Run your Backbone.js apps on the client and server
Introducing Rendr: Run your Backbone.js apps on the client and serverIntroducing Rendr: Run your Backbone.js apps on the client and server
Introducing Rendr: Run your Backbone.js apps on the client and serverSpike Brehm
 
Extending Apostrophe to build a variable-based CMS for rendering PDF brochures
Extending Apostrophe to build a variable-based CMS for rendering PDF brochuresExtending Apostrophe to build a variable-based CMS for rendering PDF brochures
Extending Apostrophe to build a variable-based CMS for rendering PDF brochuresSpike Brehm
 

Mais de Spike Brehm (11)

Managing Through Chaos (w/ presenter notes)
Managing Through Chaos (w/ presenter notes)Managing Through Chaos (w/ presenter notes)
Managing Through Chaos (w/ presenter notes)
 
Managing Through Chaos
Managing Through ChaosManaging Through Chaos
Managing Through Chaos
 
The Evolution of Airbnb's Frontend
The Evolution of Airbnb's FrontendThe Evolution of Airbnb's Frontend
The Evolution of Airbnb's Frontend
 
Integrating Browserify with Sprockets
Integrating Browserify with SprocketsIntegrating Browserify with Sprockets
Integrating Browserify with Sprockets
 
Building Isomorphic Apps (JSConf.Asia 2014)
Building Isomorphic Apps (JSConf.Asia 2014)Building Isomorphic Apps (JSConf.Asia 2014)
Building Isomorphic Apps (JSConf.Asia 2014)
 
JSConf US 2014: Building Isomorphic Apps
JSConf US 2014: Building Isomorphic AppsJSConf US 2014: Building Isomorphic Apps
JSConf US 2014: Building Isomorphic Apps
 
In Pursuit of the Holy Grail: Building Isomorphic JavaScript Apps
In Pursuit of the Holy Grail: Building Isomorphic JavaScript AppsIn Pursuit of the Holy Grail: Building Isomorphic JavaScript Apps
In Pursuit of the Holy Grail: Building Isomorphic JavaScript Apps
 
General Assembly Workshop: Advanced JavaScript
General Assembly Workshop: Advanced JavaScriptGeneral Assembly Workshop: Advanced JavaScript
General Assembly Workshop: Advanced JavaScript
 
Isomorphic JavaScript: #DevBeat Master Class
Isomorphic JavaScript: #DevBeat Master ClassIsomorphic JavaScript: #DevBeat Master Class
Isomorphic JavaScript: #DevBeat Master Class
 
Introducing Rendr: Run your Backbone.js apps on the client and server
Introducing Rendr: Run your Backbone.js apps on the client and serverIntroducing Rendr: Run your Backbone.js apps on the client and server
Introducing Rendr: Run your Backbone.js apps on the client and server
 
Extending Apostrophe to build a variable-based CMS for rendering PDF brochures
Extending Apostrophe to build a variable-based CMS for rendering PDF brochuresExtending Apostrophe to build a variable-based CMS for rendering PDF brochures
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
  • 6. Airbedandbreakfast.com • Started in 2008 as a Rails 2.x app 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
  • 16. Why Single-Page Apps? • Faster JavaScript runtimes Thursday, September 13, 12
  • 17. Why Single-Page Apps? • Faster JavaScript runtimes • New browser features (pushState, localStorage, etc.) Thursday, September 13, 12
  • 18. Why Single-Page Apps? • Faster JavaScript runtimes • New browser features (pushState, localStorage, etc.) • Heightened user expectations Thursday, September 13, 12
  • 19. Two Approaches The Easy Way The Hard Way aka “The Holy Grail” Thursday, September 13, 12
  • 20. The Easy Way Thursday, September 13, 12
  • 21. The Easy Way 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
  • 29. The Hard Way 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
  • 38. Stops and Starts Thursday, September 13, 12
  • 39. Stops and Starts • mustache.rb: code duplication Thursday, September 13, 12
  • 40. Stops and Starts • mustache.rb: code duplication • therubyracer: performance, stability Thursday, September 13, 12
  • 41. Stops and Starts • mustache.rb: code duplication • therubyracer: performance, stability • PhantomJS: slow, overly complicated Thursday, September 13, 12
  • 42. Present How we built Wish Lists Thursday, September 13, 12
  • 45. Technologies • MV*: Backbone.js Thursday, September 13, 12
  • 46. Technologies • MV*: Backbone.js • Templating: Handlebars Thursday, September 13, 12
  • 47. Technologies • MV*: Backbone.js • Templating: Handlebars • UI & Layout: Oxygen (Airbnb’s Bootstrap) Thursday, September 13, 12
  • 48. Technologies • MV*: Backbone.js • Templating: Handlebars • UI & Layout: Oxygen (Airbnb’s Bootstrap) • CoffeeScript Thursday, September 13, 12
  • 49. Technologies • MV*: Backbone.js • Templating: Handlebars • UI & Layout: Oxygen (Airbnb’s Bootstrap) • CoffeeScript • HTML5 pushState Thursday, September 13, 12
  • 50. Technologies • MV*: Backbone.js • Templating: Handlebars • UI & Layout: Oxygen (Airbnb’s Bootstrap) • CoffeeScript • HTML5 pushState • api.airbnb.com Thursday, September 13, 12
  • 51. 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
  • 52. 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
  • 53. 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
  • 54. 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
  • 55. 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
  • 56. 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
  • 57. 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
  • 58. Bootstrapping the app window.WishlistsApp = new AIR.Apps.Wishlists({ “listings”: [...], “wishlists”: [...], ... }); Thursday, September 13, 12
  • 59. Bootstrapping the app window.WishlistsApp = new AIR.Apps.Wishlists({ “listings”: [...], “wishlists”: [...], ... }); WishlistsApp.get(‘wishlists’) => [Object, Object, Object, ...] 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
  • 70. Backbone Router class 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
  • 71. Backbone Router class 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
  • 72. Backbone Router class 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
  • 73. Backbone Router class 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
  • 75. 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
  • 76. 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
  • 77. 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
  • 78. 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
  • 79. 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
  • 80. 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
  • 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
  • 89. 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
  • 90. 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
  • 91. 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
  • 92. 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
  • 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
  • 111. Modular, DRY views Thursday, September 13, 12
  • 112. Modular, DRY views • Re-usable bits of markup and behavior Thursday, September 13, 12
  • 113. Modular, DRY views • (screenshot) Thursday, September 13, 12
  • 114. Modular, DRY views • (screenshot) Thursday, September 13, 12
  • 115. Subview initialization class 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
  • 116. Subview initialization class 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
  • 117. Subview initialization class 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
  • 118. Subview initialization class 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
  • 119. Subview initialization class 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
  • 120. What goes into a view? 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
  • 125. Rdio’s Backbone-based View Component Framework Justin Tulloss, @justin_tulloss http://www.youtube.com/watch?v=TB-l2nF67iU Thursday, September 13, 12
  • 126. Modular, DRY views • (screenshot) Thursday, September 13, 12
  • 127. http://airbnb.github.com/infinity Infinity.js • (screenshot) Thursday, September 13, 12
  • 129. I18n.js • 192 countries Thursday, September 13, 12
  • 130. I18n.js • 192 countries • 31 locales Thursday, September 13, 12
  • 131. I18n.js • 192 countries • 31 locales • Client-slide translation library Thursday, September 13, 12
  • 132. I18n.t() I18n.t('edit_wish_list'); Thursday, September 13, 12
  • 133. I18n.t() I18n.t('edit_wish_list'); "Edit Wish List" Thursday, September 13, 12
  • 134. I18n.t() I18n.t('edit_wish_list'); "Edit Wish List" <h1>{{t "edit_wish_list"}}</h1> Thursday, September 13, 12
  • 135. I18n.t() I18n.t('edit_wish_list'); "Edit Wish List" <h1>{{t "edit_wish_list"}}</h1> <h1>Edit Wish List</h1> Thursday, September 13, 12
  • 136. Interpolation I18n.t('owners_wish_list', {name: name}); Thursday, September 13, 12
  • 137. Interpolation I18n.t('owners_wish_list', {name: name}); "Spike’s Wish List" Thursday, September 13, 12
  • 138. Interpolation I18n.t('owners_wish_list', {name: name}); "Spike’s Wish List" <h1>{{t "owners_wish_list" name=name}}</h1> Thursday, September 13, 12
  • 139. Interpolation I18n.t('owners_wish_list', {name: name}); "Spike’s Wish List" <h1>{{t "owners_wish_list" name=name}}</h1> <h1>Spike’s Wish List</h1> Thursday, September 13, 12
  • 140. I18n.extend() I18n.extend({ "edit_wish_list": "Edit Wish List", "owners_wish_list": "%{name}’s Wish List", ... }); Thursday, September 13, 12
  • 141. I18n.pluralize() I18n.pluralize("Listing", listings); Thursday, September 13, 12
  • 142. I18n.pluralize() I18n.pluralize("Listing", listings); 3 Listings Thursday, September 13, 12
  • 143. I18n.pluralize() I18n.pluralize("Listing", listings); 3 Listings <span>{{t_pluralize "Listing" count=listings}}</span> Thursday, September 13, 12
  • 144. I18n.pluralize() I18n.pluralize("Listing", listings); 3 Listings <span>{{t_pluralize "Listing" count=listings}}</span> <span>3 Listings</span> Thursday, September 13, 12
  • 145. pluralize() just calls t() { "pluralize.Listing.zero": "%{count} Listings", "pluralize.Listing.one": "%{count} Listing", "pluralize.Listing.many": "%{count} Listings" } 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
  • 150. PhraseBundle I18n.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
  • 151. PhraseBundle I18n.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
  • 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
  • 158. CDN Asset URLs Thursday, September 13, 12
  • 159. CDN Asset URLs window.ImagePaths = <%= map_image_paths([ 'icons/facebook.png', ... ]).to_json.html_safe %>; Thursday, September 13, 12
  • 160. CDN Asset URLs window.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
  • 161. CDN Asset URLs window.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” <img src=”{{image_path “icons/facebook.png”}}” ...> Thursday, September 13, 12
  • 162. Future In pursuit of the Holy Grail Thursday, September 13, 12
  • 163. Backbone.js is just a stopgap 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
  • 169. Testing the Node.js Waters 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
  • 173. Testing the Node.js Waters 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
  • 184. Let’s chat @spikebrehm Thursday, September 13, 12