building isomorphic apps (jsconf.asia 2014)
DESCRIPTION
Over the past year or so, we’ve seen the emergence of a new way of building JavaScript web apps that share code between the web browser and the server, using Node.js — a technique that has come to be known as "isomorphic JavaScript.” There are a variety of use cases for isomorphic JavaScript; some apps render HTML on both the server and the client, some apps share just a few small bits of application logic, while others share the entire application runtime between client and server to provide advanced offline and realtime features. Why go isomorphic? The main benefits are performance, maintainability, reusability, and SEO. This talk shares examples of isomorphic JavaScript apps running in the wild, explore the exploding ecosystem of asset building tools, such as Browserify, Webpack, and Gulp, that allow developers to build their own isomorphic JavaScript apps with open-source libraries, demonstrate how to build an isomorphic JavaScript module from scratch, and explore how libraries like React and Flux can be used to build a single-page app that renders on the server.TRANSCRIPT
Building Isomorphic Apps @spikebrehm
Spike Brehm ____________________________
@spikebrehm
@AirbnbNerds
1. Why Isomorphic JavaScript 2. Isomorphic in the Wild 3. How to Isomorph
Why Isomorphic JavaScript
What is Isomorphic JavaScript?
JavaScript code that can be shared between environments.
i·so·mor·phicformsame
http://blog.nodejitsu.com/scaling-isomorphic-javascript-code/
Persistence
Client JavaScriptDOM manipulation UX
View layerApplication
logic Routing
Backend Ruby
Python Java PHP
Ye Olde Web App
Backend Ruby
Python Java PHP
Persistence
Client JavaScriptDOM manipulation UX
View layerApplication
logic Routing
Fat-Client, circa 2011
Persistence
Client JavaScript
Shared JavaScript
View layerApplication
logic Routing
DOM manipulation UX
Backend Ruby
Python Java PHP
Isomorphic, circa 2013
Shared View Layer
Persistence
Client JavaScript
Shared JavaScript
View layerApplication
logic Routing
DOM manipulation UX
Backend Ruby
Python Java PHP
Isomorphic, circa 2013
View layer
Markup
Data presentation
URL formatting
Date, Currency formatting
I18n
Isomorphic Use Cases
1. “Single Page Apps” that can be fully rendered on the server.
2. Ambitious apps that share logic between client & server to accomplish novel things.
Why go to the trouble?
PerformanceInitial pageload speed.
SEO*Crawlable single-page apps.
FlexibilityRun code anywhere.
MaintainabilityReduce code duplication.
Performance
Client-rendered appDownload skeleton HTML
User sees content
Download JavaScript
Fetch data from API
Evaluate JavaScript
Exacerbated on mobile: high latency, low bandwidth
Server-rendered appDownload
full HTML
Download JavaScript
User sees content
Evaluate JavaScript
Isomorphic JavaScript in the Wild
! Yahoo’s Modown libraries (successor to Mojito).
Flickr
! Facebook’s React library in a Django app.
Instagram*
! Airbnb’s Rendr library, built on Backbone and Express.
Airbnb Mobile Web
! Entire App runtime synced between client & server.
Asana
! Realtime app framework. Meteor
Isomorphic JavaScript is a spectrum
Entire view layer and app
logic shared
Small bits of view layer or logic shared
Many abstractions
Few abstractions
How to Isomorph
Isomorphic JavaScript can beenvironment agnostic
orshimmed per environment.
Environment agnostic
Does not depend on browser-specific properties (window) or server-specific properties (process.env, req.cookies).
E X A M P L E
Handlebars.js
var template = ! '<ul>' \! '{{#each posts}}' \! ' <li>{{title}}</li>' \! '{{/each}}' \! '</ul>';! !var templateFn = Handlebars.compile(template);!var html = templateFn({posts: posts});
Shimmed per environment
Provide shims for accessing environment-specific properties so module can expose a single API.
window.location.pathnamevs req.path
E X A M P L E
Superagent
superagent! .get('/api/posts.json')! .end(function(res) {! console.log(res.status, res.body, res.headers);! });
Abstractions
A B S T R A C T I O N
Cookies
Client document.cookie =! 'myCookie=1; Domain=.example.org';
Server res.setHeader(! 'Set-Cookie: myCookie=1; ' +! 'Domain=.example.org'! );
A B S T R A C T I O N
Redirects
Clientdocument.location.href = '/login';!
!window.pushState({}, '', '/login');
Server res.redirect('/login');
Let’s write a module that abstracts the setting of cookies, providing the same API for client & server.
H A C K T I M E
H A C K T I M E
setCookie('myCookie', 'the value');
document.cookie = 'myCookie=the%20value';
or
res.setHeader('Set-Cookie: myCookie=the%20value;');
H A C K T I M E
setCookie('myCookie', 'the value', {! path: '/',! domain: '.example.org',! expires: new Date(2014, 12, 31)!});
document.cookie =! 'myCookie=the%20value; Domain=.example.org; ' +! 'Path=/; Expires=Sat, 31 Jan 2015 05:00:00 GMT';
That looks kinda hard…
NPM & Browserify* to the rescue
Browserify* Use CommonJS to require() modules in the browser.
Browserify* Package dependencies from node_modules into our bundle.
*or Webpack.
Webpack is like Browserify, but with more bells-and-whistles included by default.
Used by Instagram, Facebook, Yahoo!.
H A C K T I M E
Caveat: It’s Different on the Server
! setCookie('myCookie', 'the value', {res: res});!!!!
app.use(function(req, res, next) {! setCookie('myCookie', 'the value', {res: res});! ...!! next();!});
How do we make a shimmed-per-environment module?
Utilize package.json’s “browser” field.
{! "name": "set-cookie",! "dependencies": {...}!}!!!!
Swap out the entire implementation.
{! "name": "set-cookie",! "dependencies": {...},! "browser": "./lib/client.js"!}!!!
Swap out specific files.
{! "name": "set-cookie",! "dependencies": {...},! "browser": {! "./lib/node.js": "./lib/client.js"! }!}!
Swap out dependencies.
{! "name": "set-cookie",! "dependencies": {...},! "browser": {! "./lib/node.js": "./lib/client.js",! "cookie": "cookie-browser"! }!}
Let’s build `set-cookie`.
https://github.com/spikebrehm/set-cookie
Module structure
.!"## index.js!"## lib!$ %## setter!$ "## index.js!$ %## client.js!"## node_modules!$ %## cookie
// ./index.js!!var cookie = require('cookie');!var setter = require('./lib/setter');!!module.exports = function(name, value, options) {! var cookieStr = cookie.serialize(name, value, options);! setter(cookieStr, options);!};
// ./lib/setter/index.js!!module.exports = function setter(cookieStr, options) {! var res = options && options.res;!! if (!res)! throw new Error('Must specify `res` ' +! 'when setting cookie.’);!! res.setHeader('Set-Cookie', cookieStr);!};
// ./lib/setter/client.js!!module.exports = function setter(cookieStr) {! document.cookie = cookieStr;!};
// ./package.json!!{! "name": "set-cookie",! "dependencies": {! "cookie": "^0.1.2"! },! "browser": {! "./lib/setter/index.js": "./lib/setter/client.js"! }!}
// ./index.js!!var cookie = require('cookie');!var setter = require('./lib/setter');!!module.exports = function(name, value, options) {! var cookieStr = cookie.serialize(name, value, options);! setter(cookieStr, options);!};
Projects of Note
React Reactive UI component library from Facebook. Designed from the ground up to support isomorphic rendering. http://facebook.github.io/react/
var UserProfile = React.createClass({ render: function() { return <div> <img src={this.props.user.thumbnailUrl} /> <h3>{this.props.user.name}</h3> </div>; } }); React.render(<UserProfile user={user} />, mountNode);
var UserProfile = React.createClass({ render: function() { return <div> <img src={this.props.user.thumbnailUrl} /> <h3>{this.props.user.name}</h3> </div>; } }); React.render(<UserProfile user={user} />, mountNode);
React Reactive UI component library from Facebook. Designed from the ground up to support isomorphic rendering. http://facebook.github.io/react/
var UserProfile = React.createClass({ render: function() { return <div> <img src={this.props.user.thumbnailUrl} /> <h3>{this.props.user.name}</h3> </div>; } }); var html = React.renderToString(<UserProfile user={user} />);
React Reactive UI component library from Facebook. Designed from the ground up to support isomorphic rendering. http://facebook.github.io/react/
Fluxible Yahoo’s isomorphic Flux implementation: Dispatchr, Fetchr, Routr. Provides a way to “dehydrate” server state and “rehydrate” on client. https://github.com/yahoo/flux-examples
Isobuild Meteor’s build system for isomorphic apps. Like Browserify & Webpack, uses static analysis to compute dependencies. Can target client, server, Android, or iOS. https://www.meteor.com/isobuild
if (Meteor.isClient) { // counter starts at 0 Session.setDefault("counter", 0); Template.hello.events({ 'click button': function () { // increment the counter when button is clicked Session.set("counter", Session.get("counter") + 1); } }); } if (Meteor.isServer) { Meteor.startup(function () { // code to run on server at startup }); }
Isobuild Meteor’s build system for isomorphic apps. Like Browserify & Webpack, uses static analysis to compute dependencies. Can target client, server, Android, or iOS. https://www.meteor.com/isobuild
if (Meteor.isClient) { // counter starts at 0 Session.setDefault("counter", 0); Template.hello.events({ 'click button': function () { // increment the counter when button is clicked Session.set("counter", Session.get("counter") + 1); } }); } if (Meteor.isServer) { Meteor.startup(function () { // code to run on server at startup }); }
isomorphic-tutorial Small sample isomorphic app, written from scratch using Handlebars/React, Director (routing), and Superagent (API requests). https://github.com/spikebrehm/isomorphic-tutorial
// app/routes.js var apiClient = require('./api_client'); module.exports = function(match) { match('/posts', function(callback) { apiClient.get('/posts.json', function(err, res) { if (err) return callback(err); var posts = res.body; callback(null, 'posts', {posts: posts}); }); }); };
isomorphic-tutorial Small sample isomorphic app, written from scratch using Handlebars/React, Director (routing), and Superagent (API requests). https://github.com/spikebrehm/isomorphic-tutorial
<h1>Posts</h1> <ul> {{#each posts}} <li><a href="/posts/{{id}}">{{title}}</a></li> {{/each}} </ul>
Thanks! More resources available at
http://spike.technology !
@spikebrehm @AirbnbNerds