Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Node Powered Mobile

Tim Caswell
December 21, 2011

Node Powered Mobile

This is my SWDC 2009 talk in Stockholm Sweden. It talks about Connect, nodejs, and making real-time mobile apps.

Tim Caswell

December 21, 2011
Tweet

More Decks by Tim Caswell

Other Decks in Programming

Transcript

  1. What is needed • Simple Interface • Light Code •

    Networked Data • Real-Time Data • Free Deployment • Open Workflow Saturday, June 5, 2010
  2. 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
  3. Connect We’ll use a new node framework that “connects” the

    mobile browser to data on the server. Saturday, June 5, 2010
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. • 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
  15. • Static • Rest • Router • PubSub • Cache

    Manifest • Direct • JSON-RPC • More... Built-in Data Providers Saturday, June 5, 2010
  16. 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
  17. 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
  18. 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
  19. 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
  20. • 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
  21. 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
  22. 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
  23. index.html <!DOCTYPE html> <html lang="en" manifest="cache.manifest"> <head> <meta charset="utf-8" />

    <meta name="apple-mobile-web-app-capable" content="yes"> <title>Node + Raphaël</title> <link rel="stylesheet" href="style.css" type="text/css" /> <script src="raphael.js"></script> <script src="client.js"></script> </head> <body> <div id="holder"></div> </body> </html> Saturday, June 5, 2010