Offline Mobile Web
App Architecture & Design
Boaz Sender / @boazsender
* Go'oood morning TORONTO - honor to be in canada - ya'll very nice ppl
* Here to talk about _Offline Mobile Web Application Architecture & Design_
* Not going to talk about jQuery Mobile
* Going to talk about challenges on mobile, and a good architectural design approach for solving them
* Applies to JQM, but I am going to use Backbone, the jaaaavascript library to illustrate how you might implement
# My frontend stack
* Backbone
* jQuery
* lodash
* LayoutManager - for view management
* Require.js - deps management
* QUnit - testing
* Node.js with npm, and Grunt (jshint, csslint, qunit, concat, uglify, htmlmin, cssmin, imgmin, require.js, jst, appcache, connect, watch)
* gonna speak about grunt and backbone in this talk
* "Scaling Backbone.js Applications with Marionette.js" by Derick Bailey
* "Developing with Grunt" by Dan Heberden
* eh, hoser
## Boaz Sender
* Work at [Bocoup](http://bocoup.com)
* @boazsender on [twitter](http://twitter.com/boazsender) & [github](http://github.com/boazsender)
* [boazsender.com](http://boazsender.com) on the www
* boaz on [freenode](http://webchat.freenode.net/)
* My name is Boaz Sender.
* That's B.O.A Zed
* I work at Bocoup, the Open Web technology company.
* You can find me at @boazsender on twitter, github, pinterest, facebook, and pretty much everything else.
* boazsender dot com on the dub dub dub (writing, and more about me).
* find me on freenode (and sometimes on mozilla irc) where my nick is boaz– raise your hand if you use IRC.
* Gathered here today to talk about offline mobile web application development.
### I'm going to cover intro stuff:
- single-page minified and gzipped approaches to asset and content delivery.
### Real interesting part
* exploration of application flow and design,
* share my oppinionated approach to building mobile web exeriences.
# Why?
### (630 Million smarthones bought in 2012)
* Android (453,070,000)
* iOS (126,220,000)
* Blackberry (33,640,000)
* Windows mobile (18,410,000)
But, before I get into the technology here, I'd like to take a moment to reflect on why this is important.
* new 630 million person audience using modern mobile web browsers
* Mozilla just released their first Open Web hand set with telefonica and deutcsh telecom this past week
* FFOS targets developing countries w/ inexpensive low power devices where most people do not have regular access to the web.
* Moz underdog
* all manufacturers & carrier are vieing for a piece of this market,
* the only chance they have of unseating Apple is to all support the same open platform, and they are all choosing the Open Web.
* future of mobile platforms is __without question__ the Open Web
cite: http://en.wikipedia.org/wiki/Smartphone#Historical_sales_figures (figures from Gartner)
cite: http://en.wikipedia.org/wiki/History_of_video_game_consoles_(seventh_generation)#Sales_standings
Battery
### So,
* can anyone guess what this is?
* SPACE bar advance frag
* funny thing about these 630 million phones: when you open them they all have a battery.
* So I did some quick research: as it turns out, these batteries die quickly.
## Radio
* Running the radio kills these batteries
* Every separate request that you make for a new resource runs the radio, and kills battery.
* The larger your resource is, the longer the radio runs.
* The more requests you make, the longer the radio runs.
* minimize these requests & make them small!
* don't jam them all into one single request, which we could do, b/c
* that would cause a slow initial page load
* would make our applications updates cause the user to have to do a full redownload.
# Connectivity
I also did some research into network connectivity, and discovered some shocking results.
These mobile phones have, on average, less reliable network connections than most desktops.
* dektop network reliability on the bottom in red,
* smartphone network reliability on top in green.
* Desktop clearly blazes ahead of smartphone network reliability.
* on desktop we might be ok with making our users redownload content everytime they visit a page
* But, this research shows that we probably shouldn't do this on mobile.
### We should
* store our content and behavior directives in local storage
* and cache our HTML, CSS, JavaSript & image assets using appcache
* for offline consumption. Yum.
## How do we solve these problems?
So how do we design Open Web software to have a small payload, and be cacheable for offline use?
We...
### optimize our assets
### design a single page experience
### Declare content and behavior as json
### Deliver json in traunches
Optimized Assets
boaz@gallina:~/git/app $ grunt
### optimized assets
* concatenated minified and gzipped html/css/js
* sprited images
Running "clean:0" (clean) task
Cleaning "dist/"...OK
Running "lint:files" (lint) task
Lint free.
Running "csslint:base_theme" (csslint) task
Lint free files: 1
Running "jst:dist/debug/templates.js" (jst) task
File 'dist/debug/templates.js' created.
Running "requirejs" task
/home/boaz/git/bcc/dist/debug/require.js
----------------
/home/boaz/git/bcc/assets/js/libs/jquery.js
/home/boaz/git/bcc/assets/js/libs/lodash.js
/home/boaz/git/bcc/assets/js/libs/backbone.js
/home/boaz/git/bcc/assets/js/plugins/backbone.layoutmanager.js
/home/boaz/git/bcc/assets/js/plugins/backbone.search.js
/home/boaz/git/bcc/assets/js/plugins/backbone.routefilter.js
/home/boaz/git/bcc/app/app.js
/home/boaz/git/bcc/app/modules/page.js
/home/boaz/git/bcc/app/modules/navigation.js
/home/boaz/git/bcc/app/modules/appstate.js
/home/boaz/git/bcc/app/modules/notifications.js
/home/boaz/git/bcc/app/modules/search.js
/home/boaz/git/bcc/app/modules/overlay.js
/home/boaz/git/bcc/app/modules/header.js
/home/boaz/git/bcc/app/modules/util.js
/home/boaz/git/bcc/app/modules/pagetypes/list.js
/home/boaz/git/bcc/app/modules/pagetypes/content.js
/home/boaz/git/bcc/app/modules/pagetypes/contentlist.js
/home/boaz/git/bcc/app/modules/pagetypes/event.js
/home/boaz/git/bcc/assets/js/libs/leaflet.js
/home/boaz/git/bcc/app/modules/pagetypes/map.js
/home/boaz/git/bcc/app/modules/pagetypes/mapconfig.js
/home/boaz/git/bcc/app/modules/pagetypes/notfound.js
/home/boaz/git/bcc/app/router.js
/home/boaz/git/bcc/assets/js/plugins/orientationchange.js
/home/boaz/git/bcc/app/main.js
/home/boaz/git/bcc/app/config.js
Running "concat:dist" (concat) task
File "dist/debug/require.js" created.
Running "stylus:compile" (stylus) task
File dist/debug/index.css created.
Running "appcache:dist" (appcache) task
Running "min:dist/release/require.js" (min) task
File "dist/release/require.js" created.
Uncompressed size: 733085 bytes.
Compressed size: 82898 bytes gzipped (269063 bytes minified).
Running "mincss:dist/release/index.css" (mincss) task
File dist/release/index.css created.
Uncompressed size: 32182 bytes.
Compressed size: 5041 bytes gzipped (19684 bytes minified).
Running "appcache:dist" (appcache) task
Done, without errors.
Single Page
template = _.template('<header><h1><%= pageObj.title %></h1></header><section><p><%= pageObj.text %></p></section>');
var pageObj = {
title: "jQUery Toronto",
text: "Although many legends exist about the..."
};
template( pageObj );
### single page experience
* not html
* Very little html ever goes over the wire because html is much bigger than JSON and templates.
* Renders all html on the client with micro templates from server data
Declaration
http://app.com/api/full-state
{
"name": "jQuery Toronto",
"lang": { "a bunch of layout text"},
"revision": "1",
"pages": [ pageObj, pageObj, pageObj ],
"navigation": [ navObj, navObj, navObj ]
}
### Declare content and behavior
* the content and behaviors for each "page" are declared and delivered to the client as JSON
* so that it can be rendered by client side code
* full payload of app data at http://app.com/api/full-state
Page Object
{
"name": "home",
"route": "",
"type": "list",
"content": {
"header": "jQueryTo",
"children": [
"sponsors",
"about",
"venue",
"schedule",
"speakers"
]
}
}
Navigation Object
{
"text": "Home",
"route": "/",
"icon": "iconsprite.png",
"coordinates": [ x,y ]
}
* text
* sprited icon
* sprite coords
* where it should go
Flat Page Schema
{
"name": "jQuery Toronto",
"lang": { "a bunch of layout text"},
"revision": "1",
"pages": [ pageObj, pageObj, pageObj ],
"navigation": [ navObj, navObj, navObj ]
}
* __NOTE__: use a flat schema
Not nested
{
"name": "jQuery Toronto",
"lang": { "a bunch of layout text"},
"revision": "1",
"pages": [{
name: "foo",
children: [ pageObj, pageObj, pageObj ],
},
{
name: "bar",
children: [ pageObj, pageObj, pageObj ],
}]
"navigation": [ navObj, navObj, navObj ]
}
Traunching
http://app.com/api/initial-state
{
"name": "jQuery Toronto",
"lang": { "a bunch of layout text"},
"revision": "1",
"pages": [ {...}, {...} {...} ],
"navigation": [ {...}, {...} {...} ]
}
### Deliver data in traunches
* client can request initial content for a snappy initial load
* subsequant payloads ad hoc for subsequant pages.
http://app.com/api/pages
{
"revision": "2",
"pages": [ pageObj, pageObj, pageObj ]
}
Other endpoints to consider
full-data
initial-data
pages
pages?first=[:id]&last=[:id]
changes?since=[epochdate]
page/[:id]
Defalt GET Query Params
?revision=[revisionnumber]
×tamp=[epochdate]
## Consuming the data
* Appstate model
* viewmap
* router
* pause for questions
Appstate
appstate = Backbone.Model.extend({
url: "http://app.com/api/full-state",
initialize: function() {
// Initialize self from localstorage
// Bind to reserialize on change
},
parse: function( resp ) {
// Construct server response into a into a backbone model
}
});
### URL
### initialize: function() { ... }
* (invoked when an instance of the appstate Contructor is created)
* check local storage and initialize self from localstorage if state exists
* bind to change event on self and reserialize on change
### parse: function(resp) { ... }
* (called whenever appstate's data is returned by the server)
* Construct models from each object property
* Construct collections from each of the arrays in the responses
View map
var pagetypes = {
list: Backbone.View.extend({
template: templateFunc,
tagName: "ul",
serialize: function() { ... }
}),
content: Backbone.View.extend({ ... }),
event: Backbone.View.extend({ ... }),
map: Backbone.View.extend({ ... })
};
{
"name": "home",
"route": "",
"type": "list",
"content": { ... }
}
Routing
var Router = Backbone.Router.extend({
routes: {
"*path": "page"
},
page: function( route ){
route = decodeURI(route);
var page = appstate.get("pages").where({ route: route })[0];
var pagetype = page.get( "type" );
var currentView = new pagetypes[ pagetype ]({ model: page });
currentView.render();
}
});
And here is where the magic happens.
Maps (heavy behavior)
{
"name": "Map of Toronto",
"route": "map-of-toronto",
"type": "map",
"content": {
"tileUrlTmpl": "path-to-tile-server-endpoint",
"mapcenter": [43.40, -74.24],
"zoom": 15,
"optionspage": "map-of-toronto-opts"
}
* Google Maps
* Bing Maps
* JqueryGeo
* Leaflet ← the best one
* Mapquest
* Polymaps
manifest.appcache
(MIME type text/cache-manifest)
CACHE MANIFEST
# Wed Feb 20 2013 01:19:00 GMT-0500 (EST)
CACHE:
/index.html
/assets/js/libs/dist.js
/assets/css/index.css
/assets/img/sprite.png
/assets/img/loading.png
NETWORK:
http://app.com/api/
* use a script loader and concat/min your script
* concat/min all your css
* concat all your images into one use spritesheet
* consider an intial loading image
### JUST IN CASE
CACHE
* lists all resources that should be downloaded and stored locally. The browser will begin downloading these in the background as soon as the page has loaded. If any are already in the browser's cache, they will not be downloaded again separately.
NETWORK
* lists all URLs that may be loaded over the Internet, eg: API calls or other frequently updated resources.
FALLBACK
* lists replacements for network URLs to be used when the browser is offline or the remote server is unavailable.
SETTINGS
* specifies settings for appcache behaviour. Currently, the only available setting is cache mode.
prefer-online, which indicates that cached data should not be used when the browser has an active internet connection (supported in Firefox 14+ and Opera 12+). The default is fast, which uses the cache even when the browser has an active internet conncetion.
## Dont copy native
m.lanyrd.com uses top nav
## Questions
[@boazsender](http://twitter.com/boazsender) / [boazsender.com](http://boazsender.com)
* Spoke about grunt and backbone in this talk
* "Scaling Backbone.js Applications with Marionette.js" by Derick Bailey
* "Developing with Grunt" by Dan Heberden