In een recent project kregen we de opdracht om de mobiele versie van een bestaande website te bouwen. De bestaande website was eigenlijk geen goede kandidaat voor een responsive ontwerp, dus we besloten een compleet nieuwe versie van de site te maken voor mobiele apparaten.

_(foto door Johan Larsson)
Eerst dachten we aan het m.example.com-patroon, waarbij de sites volledig aparte domeinen zijn. Echter, aangezien we de infrastructuur voor deze site niet beheren, wilden we een aanpak kiezen die zo min mogelijk infrastructuurwijzigingen vereist. Het ondersteunen van een ander domein op dezelfde servers is niet erg moeilijk, maar het is nog een taak die makkelijk mis kan gaan en in ons geval niet nodig is.
Ik heb deze techniek ook vrijgegeven voor iedereen die hem gemakkelijk in een rails-app wil gebruiken via een rails-engine:
- https://github.com/milesmatthias/cdddn
- http://rubygems.org/gems/cdddn
Inleiding tot CloudFront
Een groot voordeel van deze website is dat het CloudFront gebruikt. CloudFront is een content delivery network (ook wel CDN genoemd), waar statische webpagina's worden gehost op 52 datacenters over de hele wereld. Dit maakt het mogelijk om de pagina zo snel mogelijk aan de browser te leveren, omdat je ongeacht waar je in de wereld bent, niet ver weg bent van een van de knooppunten van CloudFront, zodat het minder tijd kost om de website aan je apparaat te leveren. CloudFront is onderdeel van het AWS-aanbod van Amazon en garandeert naast snelheid ook beschikbaarheid. (In tegenstelling tot S3, waar alleen beschikbaarheid wordt gegarandeerd.)
Als je CloudFront gebruikt, vertel je het waar je webserver zich bevindt, zodat CloudFront de nieuwste versie van de webpagina's kan ophalen die het voor je serveert. CloudFront vraagt je webserver ook alleen om nieuwe versies van je webpagina's om de paar minuten, of welke tijdinterval je configureert in je CloudFront-dashboard. Normaal gesproken is dat ergens rond de 24 uur. Dit maakt het mogelijk voor CloudFront om zich te concentreren op beschikbaarheid en snelheid, terwijl de belasting op je webserver aanzienlijk wordt verminderd. (Het duurt ook tijd voor CloudFront om een nieuwe versie van een pagina op te halen en alle 52 datacenters bij te werken.)
Echter, dit brengt het probleem van het bijhouden van de status met zich mee. Bijvoorbeeld, als je site volledig door CloudFront wordt geleverd, hoe ondersteun je dat gebruikers inloggen op je site? Als ik een gebruiker ben die Bob heet en de pagina op /mijn/account
zegt "Hallo Bob!", zal CloudFront denken dat iedereen die de pagina op /mijn/account
aanvraagt, de versie van de pagina moet zien die "Hallo Bob!" zegt. Gebruikers (behalve misschien mensen die Bob heten...) zullen erg verward zijn. Niet alleen dat, maar wat als een gebruiker een waarde in hun account bijwerkt en verwacht dat die verandering onmiddellijk wordt weergegeven? CloudFront haalt niet bij elke aanvraag een nieuwe versie van de pagina op (dat zou het doel van CloudFront erg tegenspreken), het wacht elke paar minuten op een nieuwe versie. CloudFront heeft verschillende van deze vragen aangepakt met nieuwe functies, maar je moet je bewust zijn van deze implicaties bij het gebruik van CloudFront.
In ons geval heeft de site waar we aan werken geen concept van "gebruikers" en kan niemand in het systeem inloggen. Toch willen we dat CloudFront verschillende versies van de site toont afhankelijk van het apparaat waarmee je de site aanvraagt (mobiel vs. desktop).
CloudFront kan voor je apparatuurdetectie uitvoeren en vervolgens een speciale header versturen naar je webserver om te zeggen "Geef me de versie van de pagina voor een telefoon", maar we wilden controle over de methode van apparatuurdetectie (het beantwoorden van de vraag - is dit een telefoon of een desktop?) en we wilden de mogelijkheid voor eindgebruikers en ontwikkelaars om het apparaat dat voor hen is gekozen te wijzigen. Bijvoorbeeld, eindgebruikers op een tablet willen misschien gewoon de desktopversie zien, ook al tonen we hun aanvankelijk de mobiele versie. Onze ontwikkelaars zullen het ook lastig vinden om elke keer van apparaat te wisselen wanneer ze tussen de versies willen wisselen bij het bouwen van de site.
Onze oplossing
Dus hier is de methode die we hebben bedacht om zowel een desktop- als een mobiele versie van een site op hetzelfde domein te hebben en geleverd door CloudFront. Een vereiste opmerking is dat CloudFront normaal gesproken alle cookies voor je domein uitschakelt, maar je kunt het configureren om cookies die je noemt toe te staan. In ons geval vertelden we CloudFront om een cookie genaamd 'apparaat' toe te staan. Let ook op dat we in de context van een rails 4-app zijn, maar deze architectuur kan in elke stack worden gerepliceerd. Het belangrijkste punt hier is dat de client moet uitzoeken welk apparaat de gebruiker gebruikt, hen in staat stellen om dat te wijzigen, en de server op de hoogte houden van het type apparaat zodat de server de juiste pagina's kan leveren (dit gebeurt via cookies).
Op elke pagina moeten we iets zoals dit doen (snippet van app/views/layout/application.html.erb)
<%= javascript_include_tag "mobile_detection" %>
<script>
if (!window.location.href.match(/get_device|set_device/)){
console.log('logging the window.location.href = ' + window.location.href);
localStorage.setItem('referrer_href', window.location.href);
}
var device_cookie = cookie.get('device'),
cookiesEnabled = cookie.enabled();
if (cookiesEnabled && device_cookie == undefined) {
window.location = '/get_device.html'
}
</script>
De bovenstaande code onthoudt eenvoudig welke pagina je bent beland (meestal de homepage, maar we willen dat de client in staat is om links te delen en dat het ook op mobiel werkt), en vervolgens leidt je door naar de pagina /get_device
om je apparaat te detecteren als we dat nog niet hebben gedaan. Let op het gebruik van localStorage
, wat betekent dat dit alleen werkt op IE8+ en niet werkt op Opera Mini.
Let ook op dat de bovenstaande code zich in de <head>
van de lay-out bevindt zodat het zo snel mogelijk wordt uitgevoerd en voordat enige scripts die de desktopversie mogelijk heeft. In dit geval maakt de desktopversie zwaar gebruik van javascript om veel inhoud te laden, dus we wilden echt niet dat al dat gebeurde op een telefoon voordat de site erkende dat de gebruiker de mobiele versie van de site moest zien.
Op elke mobiele pagina moeten we dit doen (van `app/views/layout/application.mobile.erb):
<script>
var device_cookie = cookie.get('device'),
cookiesEnabled = cookie.enabled();
if (cookiesEnabled && device_cookie == 'desktop') {
window.location.reload(true);
}
</script>
Dit komt omdat mobiele apparaten de pagina's zwaar cachen (wat zinvol is) dus als de gebruiker het apparaat heeft gewijzigd, moeten we ervoor zorgen dat we die bijgewerkte cookie naar de server sturen, zelfs al is de url niet gewijzigd (en dus denkt de cache dat het dezelfde pagina is die geleverd moet worden). Het true
in de reload
-functieaanroep vertelt de browser om te negeren wat er in de cache staat.
Let op dat we exact hetzelfde doen op elke desktop-pagina, behalve dat we controleren of de gebruiker hun apparaat naar 'mobiel'
heeft gewijzigd.
Apparaatdetectie op /get_device
(app/views/home/get_device.html)
<script>
function isMobile(){
var MOBILE_USER_AGENTS = 'palm|blackberry|nokia|phone|midp|mobi|symbian|chtml|ericsson|minimo|' +
'audiovox|motorola|samsung|telit|upg1|windows ce|ucweb|astel|plucker|' +
'x320|x240|j2me|sgh|portable|sprint|docomo|kddi|softbank|android|mmp|' +
'pdxgw|netfront|xiino|vodafone|portalmmm|sagem|mot-|sie-|ipod|up\\.b|' +
'webos|amoi|novarra|cdm|alcatel|pocket|ipad|iphone|mobileexplorer|' +
'mobile|zune',
mobile_regex = new RegExp(MOBILE_USER_AGENTS, "i");
return navigator.userAgent.match(mobile_regex);
}
// de apparaatcookie instellen op wat we detecteren
var detected_device = isMobile() ? 'mobiel' : 'desktop';
cookie.set('device', detected_device, {
domain: '<%= Rails.env.development? ? '' : Settings.host %>',
path : '/'
});
// window.location naar waar je vandaan kwam
window.location.assign(localStorage.getItem('referrer_href'