Upload
spike-brehm
View
61.129
Download
0
Embed Size (px)
DESCRIPTION
Citation preview
Introducing Rendr:Run your Backbone.js apps on the client and server
Spike Brehm@spikebrehm
HTML5DevConfApril 1, 2013
2008
2013
Exciting times in the world of web apps.
<your framework here>
Client-sideMVC
+
Poor SEO; not crawlable
Performance hit to download and parse JS
Duplicated application logic
Context switching
It’s still a PITA to build fast, maintainable rich-client apps.
What if our JavaScript app could run on both sides?
We started thinking...
Client +serverMVCaka “The Holy Grail”
Provides SEO
Initial pageload is drastically faster
Consolidated application logic
Has anyone already done this?
Meteor: client/server, but no server-side rendering; owns data layer
Derby: client+server rendering, but owns data layer
Mojito: client+server rendering, but full stack, and... YUI.
Okay... how hard can it be?
+
+
Rendr.
What it is.
JavaScript MVC on client & server
Backbone & Handlebars
BaseView, BaseModel, BaseCollection, BaseApp, ClientRouter, ServerRouter...
Express middleware
Minimal glue between client & server
What it is.
What it ain’t.
Batteries-included web framework
Finished
What it ain’t.
Design goals:
• Write application logic agnostic to environment.
• Library, not a framework.
• Minimize if (server) {...} else {...}.
• Hide complexity in library.
• Talk to RESTful API.
• No server-side DOM.
• Simple Express middleware.
Classes:
- BaseApp < Backbone.Model- BaseModel < Backbone.Model- BaseCollection < Backbone.Collection- BaseView < Backbone.View- AppView < BaseView
- ClientRouter < BaseRouter- ServerRouter < BaseRouter
- ModelStore < MemoryStore- CollectionStore < MemoryStore
- Fetcher
|- client/|- shared/|- server/
Rendr directory structure
Sent to client}
|- app/|- public/|- server/
App directory structure
|- app/|--- collections/|--- controllers/|--- models/|--- templates/|--- views/|--- app.js|--- router.js|--- routes.js|- public/|- server/
App directory structure
Entire app dir gets sent to client}
var User = require(‘app/models/user’);var BaseView = require(‘rendr/shared/base/view’);
CommonJS using Stitch
On the server:
On the client:var User = require(‘app/models/user’);var BaseView = require(‘rendr/shared/base/view’);
module.exports = function(match) { match('', 'home#index'); match('users', 'users#index'); match('users/:id', 'users#show');};
app/routes.js
app/routes.js
module.exports = function(match) { match('', 'home#index'); match('users', 'users#index'); match('users/:id', 'users#show');};
controller
app/routes.js
module.exports = function(match) { match('', 'home#index'); match('users', 'users#index'); match('users/:id', 'users#show');};
action
Render lifecycle, server.
• On server startup, parse routes file and mount as Express routes
• GET /users/1337• Router matches "/users/:id" to "users#show" with
params = {"id": 1337}• Router finds controller:
require("app/controllers/users_controller")• Router executes show action with params = {"id": 1337}• The show action says to fetch User#1337 and use
UsersShowView view class• Router instantiates new UsersShowView with data• Router calls view.getHtml()• Hands HTML to Express, which decorates with layout
and serves response
Render lifecycle, client.
• On page load, Router parses routes file and mounts Backbone routes
• pushState /users/1337• Router matches "/users/:id" to "users#show" with
params = {"id": 1337}• Router finds controller:
require("app/controllers/users_controller")• Router executes show action with params = {"id": 1337}• The show action says to fetch User#1337 and use
UsersShowView view class• Router instantiates new UsersShowView with data• Router calls view.render()• Insert into DOM
• On page load, Router parses routes file and mounts Backbone routes
• pushState /users/1337• Router matches "/users/:id" to "users#show" with
params = {"id": 1337}• Router finds controller:
require("app/controllers/users_controller")• Router executes show action with params = {"id": 1337}• The show action says to fetch User#1337 and use
UsersShowView view class• Router instantiates new UsersShowView with data• Router calls view.render()• Insert into DOM
• On page load, Router parses routes file and mounts Backbone routes
• pushState /users/1337• Router matches "/users/:id" to "users#show" with
params = {"id": 1337}• Router finds controller:
require("app/controllers/users_controller")• Router executes show action with params = {"id": 1337}• The show action says to fetch User#1337 and use
UsersShowView view class• Router instantiates new UsersShowView with data• Router calls view.render()• Insert into DOM
module.exports = { show: function(params, callback) { callback(null, 'users_show_view'); }};
app/controllers/users_controller.js
app/controllers/users_controller.js
module.exports = { show: function(params, callback) { callback(null, 'users_show_view'); }};
app/controllers/users_controller.js
module.exports = { show: function(params, callback) { callback(null, 'users_show_view'); }};
app/controllers/users_controller.js
module.exports = { show: function(params, callback) { callback(null, 'users_show_view'); }};
app/controllers/users_controller.js
Most simple case: no fetching of data.
module.exports = { show: function(params, callback) { callback(null, 'users_show_view'); }};
app/controllers/users_controller.js
module.exports = { show: function(params, callback) { callback(null, 'users_show_view'); }};
But we want to fetch the user.
app/controllers/users_controller.js
module.exports = { show: function(params, callback) { var spec = { model: {model: ‘User’, params: params} }; this.app.fetch(spec, function(err, results) { callback(err, 'users_show_view', results); }); }};
app/controllers/users_controller.js
module.exports = { show: function(params, callback) { var spec = { model: {model: ‘User’, params: params} }; this.app.fetch(spec, function(err, results) { callback(err, 'users_show_view', results); }); }};
app/controllers/users_controller.js
module.exports = { show: function(params, callback) { var spec = { model: {model: ‘User’, params: params} }; this.app.fetch(spec, function(err, results) { callback(err, 'users_show_view', results); }); }};
app/controllers/users_controller.js
module.exports = { show: function(params, callback) { var spec = { model: {model: ‘User’, params: params} }; this.app.fetch(spec, function(err, results) { callback(err, 'users_show_view', results); }); }};
var BaseView = require('rendr/shared/base/view');
module.exports = BaseView.extend({ className: 'users_show_view'});module.exports.id = 'users_show_view';
app/views/users_show_view.js
var BaseView = require('rendr/shared/base/view');
module.exports = BaseView.extend({ className: 'users_show_view'});module.exports.id = 'users_show_view';
app/views/users_show_view.js
var BaseView = require('rendr/shared/base/view');
module.exports = BaseView.extend({ className: 'users_show_view'});module.exports.id = 'users_show_view';
app/views/users_show_view.js
var BaseView = require('rendr/shared/base/view');
module.exports = BaseView.extend({ className: 'users_show_view'});module.exports.id = 'users_show_view';
app/views/users_show_view.js
var BaseView = require('rendr/shared/base/view');
module.exports = BaseView.extend({ className: 'users_show_view',
events: { 'click p': 'handleClick' },
handleClick: function() {...}});module.exports.id = 'users_show_view';
app/views/users_show_view.js
<h1>User: {{name}}</h1>
<p>From {{city}}.</p>
app/templates/users_show_view.hbs
Rendered HTML
<div class="users_show_view" data-view="users_show_view" data-model_name="user" data-model_id="1337">
<h1>User: Spike</h1>
<p>From San Francisco.</p></div>
Rendered HTML
<div class="users_show_view" data-view="users_show_view" data-model_name="user" data-model_id="1337">
<h1>User: Spike</h1>
<p>From San Francisco.</p></div>
Rendered HTML
<div class="users_show_view" data-view="users_show_view" data-model_name="user" data-model_id="1337">
<h1>User: Spike</h1>
<p>From San Francisco.</p></div>
Rendered HTML
<div class="users_show_view" data-view="users_show_view" data-model_name="user" data-model_id="1337">
<h1>User: Spike</h1>
<p>From San Francisco.</p></div>
Where’d the data come from?
Where’s the render method?
How do I customize what gets passed to the template?
Sensible defaults.
var BaseView = require('rendr/shared/base/view');
module.exports = BaseView.extend({ className: 'users_show_view',
getTemplateData: function() {
}});
app/views/users_show_view.js
var BaseView = require('rendr/shared/base/view');
module.exports = BaseView.extend({ className: 'users_show_view',
getTemplateData: function() { var data = BaseView.prototype.getTemplateData \ .call(this); }});
app/views/users_show_view.js
var BaseView = require('rendr/shared/base/view');
module.exports = BaseView.extend({ className: 'users_show_view',
getTemplateData: function() { var data = BaseView.prototype.getTemplateData \ .call(this); // `data` is equivalent to this.model.toJSON()
}});
app/views/users_show_view.js
var BaseView = require('rendr/shared/base/view');
module.exports = BaseView.extend({ className: 'users_show_view',
getTemplateData: function() { var data = BaseView.prototype.getTemplateData \ .call(this); // `data` is equivalent to this.model.toJSON() return _.extend(data, { nameUppercase: data.name.toUpperCase() }); }});
app/views/users_show_view.js
<h1>User: {{nameUppercase}}</h1>
<p>From {{city}}.</p>
app/templates/users_show_view.hbs
<div class="users_show_view" data-...> <h1>User: SPIKE</h1>
<p>From San Francisco.</p></div>
Rendered HTML
<!doctype html><html lang="en"><head>...</head>
<body><div id="content"> <div class="users_show_view" data-view="users_show_view" data-model_name="user" data-model_id="1337">
<h1>User: SPIKE</h1>
<p>From San Francisco.</p> </div></div>
<script>(function() {var App = window.App = new (require('app/app'));App.bootstrapData({ "model":{"summary":{"model":"user","id":1337}, "data":{"name":"Spike","city":"San Francisco", ...}});App.start();})();</script></body></html>
Rendered HTML with layout
<!doctype html><html lang="en"><head>...</head>
<body><div id="content"> <div class="users_show_view" data-view="users_show_view" data-model_name="user" data-model_id="1337">
<h1>User: SPIKE</h1>
<p>From San Francisco.</p> </div></div>
<script>(function() {var App = window.App = new (require('app/app'));App.bootstrapData({ "model":{"summary":{"model":"user","id":"wycats"}, "data":{"name":"Spike","city":"San Francisco", ...}});App.start();})();</script></body></html>
Rendered HTML with layout
View hydration.
1. Find DOM els with data-view attribute.
var viewEls = $("[data-view]");
2. Determine model or collection based on data-model_name, data-model_id, etc.
var modelName = viewEl.data(‘model_name’), modelId = viewEl.data(‘model_id’);
console.log(modelName, modelId); => “user” 1337
3. Fetch model/collection data from ModelStore/CollectionStore.
var model = modelStore.get(modelName, modelId);
4. Find view class.
var viewName, ViewClass;viewName = viewEl.data(‘view’);ViewClass = require('app/views/' + viewName);
5. Instantiate view instance with model.
var view = new ViewClass({ model: model, ...});
6. Attach to DOM el.
view.setElement(viewEl);
7. Delegate events.
view.delegateEvents();
8. Profit!
alert(“That wasn’t so hard, right?”)
Rendr is available starting today.
github.com/airbnb/rendr
TODO
• Share routing logic between client & server.
• Lazy load views, templates, etc as needed.
• Support other templating languages.
• Break down into smaller modules.
• Rewrite in vanilla JavaScript.
• much more...
Hackers wanted.
Thanks!
@spikebrehm@rendrjs
@AirbnbNerds