Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Bridging the Client-side Server-side Divide

Bridging the Client-side Server-side Divide

Slides for my talk for the 2012 O'Reilly Fluent JavaScript Conference. This talk covers how Typekit built our new font browsing UI, taking advantage of MVVM (Model-View-ViewModel), declarative bindings, and client/server rendering to come up with a system that organized code across client and server and reduced duplication. Even though the server is in Ruby and the client code in JS, we were able to build an app-like client UI that degrades gracefully to a standard static webpages experience for clients that don't support JS.

Sean McBride

May 30, 2012
Tweet

More Decks by Sean McBride

Other Decks in Programming

Transcript

  1. Backbone.js SproutCore Sammy.js Spine.js Cappuccino Knockout.js Javascript MVC Google Web

    Toolkit Google Closure Ember.js Angular.js Batman.js Meteor Derby JAVASCRIPT LIBRARIES
  2. MVC Model + View + Controller MVP Model + View

    + Presenter MVVM Model + View + ViewModel
  3. MVC Model + View + Controller MVP Model + View

    + Presenter MVVM Model + View + ViewModel
  4. CLIENT-SIDE SERVER-SIDE Model (ActiveRecord) ViewModel (JS + Knockout) Template (HTML

    + Bindings) ViewModel Adapter Controller (ActionController) (JSON)
  5. Template (HTML + Bindings) CLIENT-SIDE SERVER-SIDE Model (ActiveRecord) ViewModel (JS

    + Knockout) ViewModel Adapter Controller (ActionController) (JSON)
  6. Ferrying data from server to client VIEWMODEL ADAPTERS class User

    < ActiveRecord::Base validates_presence_of :billing_name end Server-side
  7. Ferrying data from server to client VIEWMODEL ADAPTERS class User

    < ActiveRecord::Base validates_presence_of :billing_name end module ViewModel::Person < ViewModel::Base def self.from_user(user) self.new( :name => user.billing_name ) end end Server-side
  8. Ferrying data from server to client VIEWMODEL ADAPTERS class User

    < ActiveRecord::Base validates_presence_of :billing_name end module ViewModel::Person < ViewModel::Base def self.from_user(user) self.new( :name => user.billing_name ) end end Server-side self.from_twitter self.from_vendor self.from_admin
  9. Ferrying data from server to client VIEWMODEL ADAPTERS user =

    User.find(1) person = ViewModel::Person.from_user(user) person.to_json # => {"name": "Sean McBride"} Server-side
  10. function Person(name) { this.name = ko.observable(name); } Person.prototype.update = function(data)

    { this.name(data["name"]); } Client-side Ferrying data from server to client VIEWMODEL ADAPTERS
  11. Backbone.js SproutCore Sammy.js Spine.js Cappuccino Knockout.js Javascript MVC Google Web

    Toolkit Google Closure Ember.js Angular.js Batman.js Meteor Derby DECLARATIVE BINDINGS
  12. Backbone.js SproutCore Sammy.js Spine.js Cappuccino Knockout.js Javascript MVC Google Web

    Toolkit Google Closure Ember.js Angular.js Batman.js Meteor Derby DECLARATIVE BINDINGS
  13. function Person(name) { this.name = ko.observable(name); } <dl id="person"> <dt>Full

    name:</dt> <dd data-bind="text: name"></dd> </dl> Client-side Template
  14. function Person(name) { this.name = ko.observable(name); } <dl id="person"> <dt>Full

    name:</dt> <dd data-bind="text: name"></dd> </dl> Full name: Client-side Template
  15. function Person(name) { this.name = ko.observable(name); } <dl id="person"> <dt>Full

    name:</dt> <dd data-bind="text: name"></dd> </dl> var me = new Person("Sean McBride"); ko.applyBindings(me, document.getElementById("person")); Full name: Client-side Template Client-side
  16. function Person(name) { this.name = ko.observable(name); } <dl id="person"> <dt>Full

    name:</dt> <dd data-bind="text: name"></dd> </dl> var me = new Person("Sean McBride"); ko.applyBindings(me, document.getElementById("person")); Full name: Sean McBride Client-side Template Client-side
  17. function Person(name) { this.name = ko.observable(name); } <dl id="person"> <dt>Full

    name:</dt> <dd data-bind="text: name"></dd> </dl> var me = new Person("Sean McBride"); ko.applyBindings(me, document.getElementById("person")); Full name: Sean McBride me.name("Oscar the Grouch"); Client-side Template Client-side
  18. function Person(name) { this.name = ko.observable(name); } <dl id="person"> <dt>Full

    name:</dt> <dd data-bind="text: name"></dd> </dl> var me = new Person("Sean McBride"); ko.applyBindings(me, document.getElementById("person")); Full name: Oscar the Grouch me.name("Oscar the Grouch"); Client-side Template Client-side
  19. Full name: Verify name: Sean McBride <dl id="person"> <dt>Full name:</dt>

    <dd><input type="text" data-bind="value: name"></dd> <dt>Verify name:</dt> <dd data-bind="text: name"></dd> </dl> Sean McBride Template
  20. Full name: Verify name: <dl id="person"> <dt>Full name:</dt> <dd><input type="text"

    data-bind="value: name"></dd> <dt>Verify name:</dt> <dd data-bind="text: name"></dd> </dl> Template
  21. Full name: Verify name: <dl id="person"> <dt>Full name:</dt> <dd><input type="text"

    data-bind="value: name"></dd> <dt>Verify name:</dt> <dd data-bind="text: name"></dd> </dl> Oscar the Grouch Oscar the Grouch Template
  22. CLIENT-SIDE SERVER-SIDE Model (ActiveRecord) ViewModel (JS + Knockout) Template (HTML

    + Bindings) ViewModel Adapter Controller (ActionController) (JSON)
  23. Template (HTML + Bindings) CLIENT-SIDE SERVER-SIDE Model (ActiveRecord) ViewModel (JS

    + Knockout) ViewModel Adapter Controller (ActionController) (JSON)
  24. class ViewModel::Person < ViewModel::Base def self.from_user(user) self.new( :name => user.billing_name

    ) end end person = ViewModel::Person.from_user(User.get(1)) Server-side
  25. class ViewModel::Person < ViewModel::Base def self.from_user(user) self.new( :name => user.billing_name

    ) end end person = ViewModel::Person.from_user(User.get(1)) render_client_side_template "person", :object => person Server-side
  26. class ViewModel::Person < ViewModel::Base def self.from_user(user) self.new( :name => user.billing_name

    ) end end person = ViewModel::Person.from_user(User.get(1)) render_client_side_template "person", :object => person Server-side Our helper Template name ViewModel adapter
  27. 1. Get the template source 2. Evaluate all bindings with

    ViewModel data 3. Turn template into DOM fragment
  28. 1. Get the template source 2. Evaluate all bindings with

    ViewModel data 3. Turn template into DOM fragment 4. Execute each binding with values to modify DOM
  29. 1. Get the template source 2. Evaluate all bindings with

    ViewModel data 3. Turn template into DOM fragment 4. Execute each binding with values to modify DOM 5. Output HTML
  30. Using Johnson server-side to EVALUATE BINDINGS Server-side Johnson.evaluate <<JS (function()

    { // Data from viewmodel adapter var name = "Sean McBride"; // Binding string from template return { text: name }; })(); JS # => { "text" => "Sean McBride" }
  31. Using Nokogiri server-side to APPLY BINDINGS Client-side ko.bindingHandlers['text'] = {

    'init': function(element, valueAccessor) { // Set text initially }, 'update': function (element, valueAccessor) { // Update text when value changes } }; <span data-bind="text: name"></span>
  32. Using Nokogiri server-side to APPLY BINDINGS Client-side ko.bindingHandlers['text'] = {

    'init': function(element, valueAccessor) { // Set text initially }, 'update': function (element, valueAccessor) { // Update text when value changes } }; Update is used on the client, but we don’t need it on the server! <span data-bind="text: name"></span>
  33. Using Nokogiri server-side to APPLY BINDINGS Server-side bindings.register "text" do

    |element, value| element.content = value.to_s end <span data-bind="text: name"></span>
  34. Template (HTML + Bindings) CLIENT-SIDE SERVER-SIDE Model (ActiveRecord) ViewModel (JS

    + Knockout) ViewModel Adapter Controller (ActionController) (JSON)
  35. CLIENT-SIDE SERVER-SIDE (JSON) u = User.find(1) p = ViewModel::Person .from_user(u)

    p.to_json var p = new Person(); p.update(jsonData); render "person", :object => p
  36. CLIENT-SIDE SERVER-SIDE (JSON) u = User.find(1) p = ViewModel::Person .from_user(u)

    p.to_json var p = new Person(); p.update(jsonData); ko.applyBindings(p, el); render "person", :object => p