Slide 1

Slide 1 text

Node Powered Mobile By Tim Caswell Saturday, June 5, 2010

Slide 2

Slide 2 text

Node Powered Mobile By Tim Caswell Saturday, June 5, 2010

Slide 3

Slide 3 text

Simple but Different Saturday, June 5, 2010

Slide 4

Slide 4 text

What is needed Saturday, June 5, 2010

Slide 5

Slide 5 text

What is needed • Simple Interface • Light Code • Networked Data • Real-Time Data • Free Deployment • Open Workflow Saturday, June 5, 2010

Slide 6

Slide 6 text

What is needed • Simple Interface • Light Code • Networked Data • Real-Time Data • Free Deployment • Open Workflow • HTML, SVG, CSS • JavaScript • HTTP Services • PubSub • Browser Apps • It’s just text! Saturday, June 5, 2010

Slide 7

Slide 7 text

Connect We’ll use a new node framework that “connects” the mobile browser to data on the server. Saturday, June 5, 2010

Slide 8

Slide 8 text

It’s like Japanese Legos Saturday, June 5, 2010

Slide 9

Slide 9 text

Connect.createServer([ {filter: "log"}, {filter: "body-decoder"}, {filter: "conditional-get"}, {filter: "cache"}, {filter: "gzip"}, {provider: "cache-manifest", root: root}, {provider: "static", root: root} ]); Pre-Built Blocks Saturday, June 5, 2010

Slide 10

Slide 10 text

And easy too! Saturday, June 5, 2010

Slide 11

Slide 11 text

method-override.js var key; // Initialize any state (on server startup) exports.setup = function (env) { key = this.key || "_method"; }; // Modify the request stream (on request) exports.handle = function(err, req, res, next){ if (key in req.body) { req.method = req.body[key].toUpperCase(); } next(); }; Saturday, June 5, 2010

Slide 12

Slide 12 text

response-time.js exports.handle = function(err, req, res, next){ var start = new Date, writeHead = res.writeHead; res.writeHead = function(code, headers){ res.writeHead = writeHead; headers['X-Response-Time'] = (new Date - start) + "ms"; res.writeHead(code, headers); }; next(); }; Saturday, June 5, 2010

Slide 13

Slide 13 text

Well, actually, it’s not always easy. Saturday, June 5, 2010

Slide 14

Slide 14 text

static.js Saturday, June 5, 2010

Slide 15

Slide 15 text

static.js var fs = require('fs'), Url = require('url'), Path = require('path'); var lifetime = 1000 * 60 * 60; // 1 hour browser cache lifetime var DEFAULT_MIME = 'application/octet-stream'; module.exports = { setup: function (env) { this.root = this.root || process.cwd(); }, handle: function (err, req, res, next) { // Skip on error if (err) { next(); return; } var url = Url.parse(req.url); var pathname = url.pathname.replace(/\.\.+/g, '.'), filename = Path.join(this.root, pathname); if (filename[filename.length - 1] === "/") { filename += "index.html"; } Saturday, June 5, 2010

Slide 16

Slide 16 text

static.js // Buffer any events that fire while waiting on the stat. var events = []; function onData() { events.push(["data"].concat(Array.prototype.slice.call(arguments))); } function onEnd() { events.push(["end"].concat(Array.prototype.slice.call(arguments))); } req.addListener("data", onData); req.addListener("end", onEnd); fs.stat(filename, function (err, stat) { // Stop buffering events req.removeListener("data", onData); req.removeListener("end", onEnd); // Fall through for missing files, thow error for other problems if (err) { if (err.errno === process.ENOENT) { next(); // Refire the buffered events events.forEach(function (args) { req.emit.apply(req, args); }); return; var fs = require('fs'), Url = require('url'), Path = require('path'); var lifetime = 1000 * 60 * 60; // 1 hour browser cache lifetime var DEFAULT_MIME = 'application/octet-stream'; module.exports = { setup: function (env) { this.root = this.root || process.cwd(); }, handle: function (err, req, res, next) { // Skip on error if (err) { next(); return; } var url = Url.parse(req.url); var pathname = url.pathname.replace(/\.\.+/g, '.'), filename = Path.join(this.root, pathname); if (filename[filename.length - 1] === "/") { filename += "index.html"; } Saturday, June 5, 2010

Slide 17

Slide 17 text

static.js // Buffer any events that fire while waiting on the stat. var events = []; function onData() { events.push(["data"].concat(Array.prototype.slice.call(arguments))); } function onEnd() { events.push(["end"].concat(Array.prototype.slice.call(arguments))); } req.addListener("data", onData); req.addListener("end", onEnd); fs.stat(filename, function (err, stat) { // Stop buffering events req.removeListener("data", onData); req.removeListener("end", onEnd); // Fall through for missing files, thow error for other problems if (err) { if (err.errno === process.ENOENT) { next(); // Refire the buffered events events.forEach(function (args) { req.emit.apply(req, args); }); return; (err); rn; the file directly using buffers ile(filename, function (err, data) { err) { next(err); return; writeHead(200, { ontent-Type": Mime.type(filename), ontent-Length": data.length, ast-Modified": stat.mtime.toUTCString(), / Cache in browser for 1 year ache-Control": "public max-age=" + 31536000 end(data); var fs = require('fs'), Url = require('url'), Path = require('path'); var lifetime = 1000 * 60 * 60; // 1 hour browser cache lifetime var DEFAULT_MIME = 'application/octet-stream'; module.exports = { setup: function (env) { this.root = this.root || process.cwd(); }, handle: function (err, req, res, next) { // Skip on error if (err) { next(); return; } var url = Url.parse(req.url); var pathname = url.pathname.replace(/\.\.+/g, '.'), filename = Path.join(this.root, pathname); if (filename[filename.length - 1] === "/") { filename += "index.html"; } Saturday, June 5, 2010

Slide 18

Slide 18 text

static.js // Buffer any events that fire while waiting on the stat. var events = []; function onData() { events.push(["data"].concat(Array.prototype.slice.call(arguments))); } function onEnd() { events.push(["end"].concat(Array.prototype.slice.call(arguments))); } req.addListener("data", onData); req.addListener("end", onEnd); fs.stat(filename, function (err, stat) { // Stop buffering events req.removeListener("data", onData); req.removeListener("end", onEnd); // Fall through for missing files, thow error for other problems if (err) { if (err.errno === process.ENOENT) { next(); // Refire the buffered events events.forEach(function (args) { req.emit.apply(req, args); }); return; (err); rn; the file directly using buffers ile(filename, function (err, data) { err) { next(err); return; writeHead(200, { ontent-Type": Mime.type(filename), ontent-Length": data.length, ast-Modified": stat.mtime.toUTCString(), / Cache in browser for 1 year ache-Control": "public max-age=" + 31536000 end(data); }; // Mini mime module for static file serving var Mime = { type: function getMime(path) { var index = path.lastIndexOf("."); if (index < 0) { return DEFAULT_MIME; } var type = Mime.TYPES[path.substring(index).toLowerCase()] || DEFAULT_MIME; return (/(text|javascript)/).test(type) ? type + "; charset=utf-8" : type; }, TYPES : { ".3gp" : "video/3gpp", ".a" : "application/octet-stream", ".ai" : "application/postscript", ".aif" : "audio/x-aiff", ".aiff" : "audio/x-aiff", ".asc" : "application/pgp-signature", ".asf" : "video/x-ms-asf", ".asm" : "text/x-asm", ".asx" : "video/x-ms-asf", ".atom" : "application/atom+xml", ".au" : "audio/basic", ".avi" : "video/x-msvideo", var fs = require('fs'), Url = require('url'), Path = require('path'); var lifetime = 1000 * 60 * 60; // 1 hour browser cache lifetime var DEFAULT_MIME = 'application/octet-stream'; module.exports = { setup: function (env) { this.root = this.root || process.cwd(); }, handle: function (err, req, res, next) { // Skip on error if (err) { next(); return; } var url = Url.parse(req.url); var pathname = url.pathname.replace(/\.\.+/g, '.'), filename = Path.join(this.root, pathname); if (filename[filename.length - 1] === "/") { filename += "index.html"; } Saturday, June 5, 2010

Slide 19

Slide 19 text

static.js ".flv" : "video/x-flv", ".for" : "text/x-fortran", ".gem" : "application/octet-stream", ".gemspec" : "text/x-script.ruby", ".gif" : "image/gif", ".gz" : "application/x-gzip", ".h" : "text/x-c", ".hh" : "text/x-c", ".htm" : "text/html", ".html" : "text/html", ".ico" : "image/vnd.microsoft.icon", ".ics" : "text/calendar", ".ifb" : "text/calendar", ".iso" : "application/octet-stream", ".jar" : "application/java-archive", ".java" : "text/x-java-source", ".jnlp" : "application/x-java-jnlp-file", ".jpeg" : "image/jpeg", ".jpg" : "image/jpeg", ".js" : "application/javascript", ".json" : "application/json", ".log" : "text/plain", ".m3u" : "audio/x-mpegurl", ".m4v" : "video/mp4", ".man" : "text/troff", ".mathml" : "application/mathml+xml", ".mbox" : "application/mbox", ".mdoc" : "text/troff", ".me" : "text/troff", ".mid" : "audio/midi", // Buffer any events that fire while waiting on the stat. var events = []; function onData() { events.push(["data"].concat(Array.prototype.slice.call(arguments))); } function onEnd() { events.push(["end"].concat(Array.prototype.slice.call(arguments))); } req.addListener("data", onData); req.addListener("end", onEnd); fs.stat(filename, function (err, stat) { // Stop buffering events req.removeListener("data", onData); req.removeListener("end", onEnd); // Fall through for missing files, thow error for other problems if (err) { if (err.errno === process.ENOENT) { next(); // Refire the buffered events events.forEach(function (args) { req.emit.apply(req, args); }); return; (err); rn; the file directly using buffers ile(filename, function (err, data) { err) { next(err); return; writeHead(200, { ontent-Type": Mime.type(filename), ontent-Length": data.length, ast-Modified": stat.mtime.toUTCString(), / Cache in browser for 1 year ache-Control": "public max-age=" + 31536000 end(data); }; // Mini mime module for static file serving var Mime = { type: function getMime(path) { var index = path.lastIndexOf("."); if (index < 0) { return DEFAULT_MIME; } var type = Mime.TYPES[path.substring(index).toLowerCase()] || DEFAULT_MIME; return (/(text|javascript)/).test(type) ? type + "; charset=utf-8" : type; }, TYPES : { ".3gp" : "video/3gpp", ".a" : "application/octet-stream", ".ai" : "application/postscript", ".aif" : "audio/x-aiff", ".aiff" : "audio/x-aiff", ".asc" : "application/pgp-signature", ".asf" : "video/x-ms-asf", ".asm" : "text/x-asm", ".asx" : "video/x-ms-asf", ".atom" : "application/atom+xml", ".au" : "audio/basic", ".avi" : "video/x-msvideo", var fs = require('fs'), Url = require('url'), Path = require('path'); var lifetime = 1000 * 60 * 60; // 1 hour browser cache lifetime var DEFAULT_MIME = 'application/octet-stream'; module.exports = { setup: function (env) { this.root = this.root || process.cwd(); }, handle: function (err, req, res, next) { // Skip on error if (err) { next(); return; } var url = Url.parse(req.url); var pathname = url.pathname.replace(/\.\.+/g, '.'), filename = Path.join(this.root, pathname); if (filename[filename.length - 1] === "/") { filename += "index.html"; } Saturday, June 5, 2010

Slide 20

Slide 20 text

static.js ".flv" : "video/x-flv", ".for" : "text/x-fortran", ".gem" : "application/octet-stream", ".gemspec" : "text/x-script.ruby", ".gif" : "image/gif", ".gz" : "application/x-gzip", ".h" : "text/x-c", ".hh" : "text/x-c", ".htm" : "text/html", ".html" : "text/html", ".ico" : "image/vnd.microsoft.icon", ".ics" : "text/calendar", ".ifb" : "text/calendar", ".iso" : "application/octet-stream", ".jar" : "application/java-archive", ".java" : "text/x-java-source", ".jnlp" : "application/x-java-jnlp-file", ".jpeg" : "image/jpeg", ".jpg" : "image/jpeg", ".js" : "application/javascript", ".json" : "application/json", ".log" : "text/plain", ".m3u" : "audio/x-mpegurl", ".m4v" : "video/mp4", ".man" : "text/troff", ".mathml" : "application/mathml+xml", ".mbox" : "application/mbox", ".mdoc" : "text/troff", ".me" : "text/troff", ".mid" : "audio/midi", // Buffer any events that fire while waiting on the stat. var events = []; function onData() { events.push(["data"].concat(Array.prototype.slice.call(arguments))); } function onEnd() { events.push(["end"].concat(Array.prototype.slice.call(arguments))); } req.addListener("data", onData); req.addListener("end", onEnd); fs.stat(filename, function (err, stat) { // Stop buffering events req.removeListener("data", onData); req.removeListener("end", onEnd); // Fall through for missing files, thow error for other problems if (err) { if (err.errno === process.ENOENT) { next(); // Refire the buffered events events.forEach(function (args) { req.emit.apply(req, args); }); return; (err); rn; the file directly using buffers ile(filename, function (err, data) { err) { next(err); return; writeHead(200, { ontent-Type": Mime.type(filename), ontent-Length": data.length, ast-Modified": stat.mtime.toUTCString(), / Cache in browser for 1 year ache-Control": "public max-age=" + 31536000 end(data); ".cc" : "text/x-c", ".chm" : "application/vnd.ms-htmlhelp", ".class" : "application/octet-stream", ".com" : "application/x-msdownload", ".conf" : "text/plain", ".cpp" : "text/x-c", ".crt" : "application/x-x509-ca-cert", ".css" : "text/css", ".csv" : "text/csv", ".cxx" : "text/x-c", ".deb" : "application/x-debian-package", ".der" : "application/x-x509-ca-cert", ".diff" : "text/x-diff", ".djv" : "image/vnd.djvu", ".djvu" : "image/vnd.djvu", ".dll" : "application/x-msdownload", ".dmg" : "application/octet-stream", ".doc" : "application/msword", ".dot" : "application/msword", ".dtd" : "application/xml-dtd", ".dvi" : "application/x-dvi", ".ear" : "application/java-archive", ".eml" : "message/rfc822", ".eps" : "application/postscript", ".exe" : "application/x-msdownload", ".f" : "text/x-fortran", ".f77" : "text/x-fortran", ".f90" : "text/x-fortran", }; // Mini mime module for static file serving var Mime = { type: function getMime(path) { var index = path.lastIndexOf("."); if (index < 0) { return DEFAULT_MIME; } var type = Mime.TYPES[path.substring(index).toLowerCase()] || DEFAULT_MIME; return (/(text|javascript)/).test(type) ? type + "; charset=utf-8" : type; }, TYPES : { ".3gp" : "video/3gpp", ".a" : "application/octet-stream", ".ai" : "application/postscript", ".aif" : "audio/x-aiff", ".aiff" : "audio/x-aiff", ".asc" : "application/pgp-signature", ".asf" : "video/x-ms-asf", ".asm" : "text/x-asm", ".asx" : "video/x-ms-asf", ".atom" : "application/atom+xml", ".au" : "audio/basic", ".avi" : "video/x-msvideo", var fs = require('fs'), Url = require('url'), Path = require('path'); var lifetime = 1000 * 60 * 60; // 1 hour browser cache lifetime var DEFAULT_MIME = 'application/octet-stream'; module.exports = { setup: function (env) { this.root = this.root || process.cwd(); }, handle: function (err, req, res, next) { // Skip on error if (err) { next(); return; } var url = Url.parse(req.url); var pathname = url.pathname.replace(/\.\.+/g, '.'), filename = Path.join(this.root, pathname); if (filename[filename.length - 1] === "/") { filename += "index.html"; } Saturday, June 5, 2010

Slide 21

Slide 21 text

static.js ".flv" : "video/x-flv", ".for" : "text/x-fortran", ".gem" : "application/octet-stream", ".gemspec" : "text/x-script.ruby", ".gif" : "image/gif", ".gz" : "application/x-gzip", ".h" : "text/x-c", ".hh" : "text/x-c", ".htm" : "text/html", ".html" : "text/html", ".ico" : "image/vnd.microsoft.icon", ".ics" : "text/calendar", ".ifb" : "text/calendar", ".iso" : "application/octet-stream", ".jar" : "application/java-archive", ".java" : "text/x-java-source", ".jnlp" : "application/x-java-jnlp-file", ".jpeg" : "image/jpeg", ".jpg" : "image/jpeg", ".js" : "application/javascript", ".json" : "application/json", ".log" : "text/plain", ".m3u" : "audio/x-mpegurl", ".m4v" : "video/mp4", ".man" : "text/troff", ".mathml" : "application/mathml+xml", ".mbox" : "application/mbox", ".mdoc" : "text/troff", ".me" : "text/troff", ".mid" : "audio/midi", // Buffer any events that fire while waiting on the stat. var events = []; function onData() { events.push(["data"].concat(Array.prototype.slice.call(arguments))); } function onEnd() { events.push(["end"].concat(Array.prototype.slice.call(arguments))); } req.addListener("data", onData); req.addListener("end", onEnd); fs.stat(filename, function (err, stat) { // Stop buffering events req.removeListener("data", onData); req.removeListener("end", onEnd); // Fall through for missing files, thow error for other problems if (err) { if (err.errno === process.ENOENT) { next(); // Refire the buffered events events.forEach(function (args) { req.emit.apply(req, args); }); return; (err); rn; the file directly using buffers ile(filename, function (err, data) { err) { next(err); return; writeHead(200, { ontent-Type": Mime.type(filename), ontent-Length": data.length, ast-Modified": stat.mtime.toUTCString(), / Cache in browser for 1 year ache-Control": "public max-age=" + 31536000 end(data); ".bmp" : "image/bmp", ".bz2" : "application/x-bzip2", ".c" : "text/x-c", ".cab" : "application/vnd.ms-cab-compressed", ".cc" : "text/x-c", ".chm" : "application/vnd.ms-htmlhelp", ".class" : "application/octet-stream", ".com" : "application/x-msdownload", ".conf" : "text/plain", ".cpp" : "text/x-c", ".crt" : "application/x-x509-ca-cert", ".css" : "text/css", ".csv" : "text/csv", ".cxx" : "text/x-c", ".deb" : "application/x-debian-package", ".der" : "application/x-x509-ca-cert", ".diff" : "text/x-diff", ".djv" : "image/vnd.djvu", ".djvu" : "image/vnd.djvu", ".dll" : "application/x-msdownload", ".dmg" : "application/octet-stream", ".doc" : "application/msword", ".dot" : "application/msword", ".dtd" : "application/xml-dtd", ".dvi" : "application/x-dvi", ".ear" : "application/java-archive", ".eml" : "message/rfc822", ".eps" : "application/postscript", ".exe" : "application/x-msdownload", ".f" : "text/x-fortran", ".f77" : "text/x-fortran", ".f90" : "text/x-fortran", }; // Mini mime module for static file serving var Mime = { type: function getMime(path) { var index = path.lastIndexOf("."); if (index < 0) { return DEFAULT_MIME; } var type = Mime.TYPES[path.substring(index).toLowerCase()] || DEFAULT_MIME; return (/(text|javascript)/).test(type) ? type + "; charset=utf-8" : type; }, TYPES : { ".3gp" : "video/3gpp", ".a" : "application/octet-stream", ".ai" : "application/postscript", ".aif" : "audio/x-aiff", ".aiff" : "audio/x-aiff", ".asc" : "application/pgp-signature", ".asf" : "video/x-ms-asf", ".asm" : "text/x-asm", ".asx" : "video/x-ms-asf", ".atom" : "application/atom+xml", ".au" : "audio/basic", ".avi" : "video/x-msvideo", var fs = require('fs'), Url = require('url'), Path = require('path'); var lifetime = 1000 * 60 * 60; // 1 hour browser cache lifetime var DEFAULT_MIME = 'application/octet-stream'; module.exports = { setup: function (env) { this.root = this.root || process.cwd(); }, handle: function (err, req, res, next) { // Skip on error if (err) { next(); return; } var url = Url.parse(req.url); var pathname = url.pathname.replace(/\.\.+/g, '.'), filename = Path.join(this.root, pathname); if (filename[filename.length - 1] === "/") { filename += "index.html"; } Saturday, June 5, 2010

Slide 22

Slide 22 text

• Authentication • Authorization • Body Decoder • Cache • Conditional Get • Debug • Error Handler • Gzip • Log • Method Override • Response Time • Session Built-in Filter Modules Saturday, June 5, 2010

Slide 23

Slide 23 text

• Static • Rest • Router • PubSub • Cache Manifest • Direct • JSON-RPC • More... Built-in Data Providers Saturday, June 5, 2010

Slide 24

Slide 24 text

Raphaël JS Raphaël is a small JavaScript library that should simplify your work with vector graphics on the web. Saturday, June 5, 2010

Slide 25

Slide 25 text

Multi-Touch Vectors http://vimeo.com/11610421 Saturday, June 5, 2010

Slide 26

Slide 26 text

multitouch-demo.js window.onload = function () { var R = Raphael(0, 0, "100%", "100%"), r = R.circle(100, 100, 50), g = R.circle(210, 100, 50), b = R.circle(320, 100, 50), p = R.circle(430, 100, 50); var start = function () { this.ox = this.attr("cx"); this.oy = this.attr("cy"); this.animate({r: 70, opacity: .25}, 500, ">"); }, move = function (dx, dy) { this.attr({cx: this.ox + dx, cy: this.oy + dy}); }, up = function () { this.animate({r: 50, opacity: .5}, 500, ">"); }; R.set(r, g, b, p).drag(move, start, up); }; Saturday, June 5, 2010

Slide 27

Slide 27 text

Creating Shapes var R = Raphael(0, 0, "100%", "100%"), r = R.circle(100, 100, 50) .attr({fill: "hsb(0, 1, 1)"}), g = R.circle(210, 100, 50) .attr({fill: "hsb(.3, 1, 1)"}), b = R.circle(320, 100, 50) .attr({fill: "hsb(.6, 1, 1)"}), p = R.circle(430, 100, 50) .attr({fill: "hsb(.8, 1, 1)"}); Saturday, June 5, 2010

Slide 28

Slide 28 text

Attaching Events function start() { this.ox = this.attr("cx"); this.oy = this.attr("cy"); this.animate({r: 70, opacity: .25}, 500, ">"); } function move(dx, dy) { this.attr({cx: this.ox + dx, cy: this.oy + dy}); } function up() { this.animate({r: 50, opacity: .5}, 500, ">"); } R.set(r, g, b, p).drag(move, start, up); Saturday, June 5, 2010

Slide 29

Slide 29 text

Let’s combine them! Saturday, June 5, 2010

Slide 30

Slide 30 text

• Serving static assets (HTML, CSS, JS) • Live Interaction (Pub Sub) • Performance Tweaks (Cache, Gzip) • Offline Mode (Cache Manifest) • HTTP Request Logging Saturday, June 5, 2010

Slide 31

Slide 31 text

app.js (stack) require.paths.unshift("./lib"); var Connect = require('connect'); var root = __dirname + "/public"; module.exports = Connect.createServer([ {filter: "log"}, {filter: "body-decoder"}, {provider: "pubsub", route: "/stream", logic: Backend}, {filter: "conditional-get"}, {filter: "cache"}, {filter: "gzip"}, {provider: "cache-manifest", root: root}, {provider: "static", root: root} ]); Saturday, June 5, 2010

Slide 32

Slide 32 text

app.js (Backend) var Backend = { subscribe: function (subscriber) { if (subscribers.indexOf(subscriber) < 0) { subscribers.push(subscriber); } }, unsubscribe: function (subscriber) { var pos = subscribers.indexOf(subscriber); if (pos >= 0) { subscribers.slice(pos); } }, publish: function (message, callback) { subscribers.forEach(function (subscriber) { subscriber.send(message); }); callback(); } }; Saturday, June 5, 2010

Slide 33

Slide 33 text

index.html Node + Raphaël
Saturday, June 5, 2010

Slide 34

Slide 34 text

Demo Time! Saturday, June 5, 2010

Slide 35

Slide 35 text

http://github.com/extjs/connect http://twitter.com/creationix http://raphaeljs.com http://nodejs.org Saturday, June 5, 2010

Slide 36

Slide 36 text

Any Questions ? Saturday, June 5, 2010