Voor dit blog en als oefening heb ik een klein beetje tijd gestoken in het maken van een javascript library waarmee ik mijn "piwik" analytics platform kan aanspreken, zonder gebruik te moeten maken van de standaard meegeleverde library.
Daar had ik twee redenen voor:
- Ik vroeg mij af of ik de 23 KB javascript van piwik nodig had gewoon om te kijken of ik 5 of 10 hits heb op dit blog
- Ik wou wel eens weten hoe ik een mooi afgelijnde javascript library moet maken.
Het resultaat kan je vinden op github (uiteraard): github: piwik-light.js.
Hier geef ik wat meer uitleg over de interessante lessen die ik daarbij heb geleerd.
Javascript Lirbary Structuur
De basisstructuur van een hedendaagse javascript library is een anonieme functie die op haar beurt andere functies bevat en de externe API terug geeft aan de clients (gebruikers van de library).
De basis ziet er een beetje als volgt uit:
var friendlyLib = (function() {
var SayHello = function() {
console.log('Hello from friendlyLib!');
};
return {
SayHello: SayHello
};
}());
Als je dan basic_library.js importeert krijg je een globale variable "friendlyLib" ter beschikking (zoals je bv ook jQuery of $ ter beschikking krijgt). Het gebruik is dan eenvoudig:
friendlyLib.SayHello();
Het voordeel is dat je meerdere functies kan aanmaken binnen jouw library zonder dat die in conflict kunnen komen met functies in andere libraries. Je voorkomt vooral dat jouw library allerlei publieke functies gaat definiëren. Er wordt slechts één globale variabele aangemaakt die toegang verleent tot je volledige library.
Je kan bovendien "private" en "public" functies en velden aanmaken.
In het voorbeeld hier boven wordt in de return een object terug gegeven met slechts één element, de SayHello functie zelf. Maar je zou bijvoorbeeld een tweede functie kunnen aanmaken voor intern gebruik, die geen deel uitmaakt van je publieke API:
var friendlyLib = (function() {
var SayHello = function() {
// Let's see what I'm thinking ...
var tought = ReadMind();
// But let's not say what we're thinking to the public.
console.log('Hello from friendlyLib!');
},
ReadMind = function() {
return 'Man, I really do not want to talk to this guy again!';
};
return {
SayHello: SayHello
};
}());
In het voorbeeld hierboven heeft onze library twee functies: SayHello en ReadMind. Maar in de return gaan we enkel SayHello naar buiten brengen.
Wat dus niet zal lukken is:
friendlyLib.ReadMind();
Maar de SayHello zelf functie heeft wel toegang tot de interne "ReadMind" functie.
Dit is de basis voor een javascript library, en voor mijn piwik library heb ik eigenlijk niet veel extra's nog echt gebruikt.
Dependencies
Soms wil je een library die verder bouwt op functionaliteit van andere libraries, bijvoorbeeld een library die toegang heeft tot jQuery. Omdat we een functie gebruiken (een closure eigenlijk) kunnen we dat mooi inpassen:
var friendlyLib = (function(internalJquery) {
var SayHello = function() {
internalJquery('div').text('Hello from friendlyLib!');
};
// Rest of the code exclude for clarity
}(jQuery));
Jouw library neemt nu één input parameter aan, het jQuery object. We geven als input de globale jQuery mee, terwijl we intern eventueel een andere naam kunnen gebruiken hiervoor. Op die manier blijven we nog meer geïsoleerd van de omgeving waarin onze library wordt gebruikt.
Dit principe wordt nog intensiever gebruikt bij libraries zoals "require.js", waar de javascript application in modules wordt opgedeeld en de onderlinge afhankelijkheid van de modules op deze manier wordt uitgedrukt.
Piwik
Dus, met dit geraamte kon ik verder bouwen om mijn piwik-light te maken. In tegenstelling tot de standaard piwik module had ik slechts nood aan een beperkt aantal parameters om te loggen:
- Url van het huidige bezoek
- De "referrer" (dus vanaf welke site de gebruiker op die pagina is terecht gekomen)
- Een random "visitor id"
De andere opties die de volledige piwik client mee geeft had ik niet nodig.
De details van de piwik API staan mooi beschreven op de website, dus hier ga ik niet verder op ingaan. Maar bij het ontwikkelen heb ik enkele interessante zaken mogen doen.
Random visitor id
Bij tracking wil je meestal wel een idee hebben van de unieke bezoekers, in tegenstelling tot louter het aantal "hits" op de pagina. Maar je wilt ook niet het IP adres van elke bezoeker gaan bij houden in je database (dat kan, maar laat ons toch zuinig zijn met het opslaan van dergelijke informatie).
Je kan een "tracking id" in een cookie bewaren, maar nog beter vond ik het om de SessionStore te gebruiken. Eén van de "nieuwigheden" in HTML 5 zijn de SessionStore en LocalStore. Beiden toegankelijk via window.Storage.
Alle moderne browsers ondersteunen dit. Beiden zijn lokale opslagruimtes in de browser van de bezoeker. Het voordeel hiervan is wel dat de inhoud niet bij elke request opnieuw over het netwerk wordt gestuurd, zoals wel het geval is bij cookies.
Er zijn dus twee types:
- SessionStorage: slaat data op voor de huidige sessie. Sluit de gebruiker de tab of het venster, dan verdwijnt ook de data
- LocalStorage: deze data blijft aanwezig tot de gebruiker die effectief leeg maakt
Ik heb ervoor gekozen om een unieke ID in de SessionStorage te plaatsen. Zo kan ik natuurlijk niet zien of eenzelfde gebruiker elke dag op mijn site komt (elke sessie krijgt een andere ID), maar dat vond ik niet zo erg. Mijn doel was om een algemeen overzicht te krijgen van enige activiteit op dit blog. Het idee dat je na weken nog altijd éénzelfde bezoeker zou kunnen herkennen vond ik een beetje ver gaan.
Random Hex Number
Om een willekeurig hexadecimal getal van 16 digits te maken gebruik ik de volgende functie in javascript. Let wel, pseudo-random en ook niet "cryptographically strong" (gebruik dit dus niet om een willekeurig wachtwoord of dergelijks te genereren):
/**
* Creates 16 random hexadecimal digits
*/
randomHex = (function () {
/* We want numbers from 0 - (2^32 - 1) */
var maxInt32 = 4294967295,
/* Pads a string with leading zero's so that it is at least 8 digits long */
pad8 = function (text) {
var result = '00000000' + text;
return result.substring(result.length - 8);
},
/* Creates a "random" 32-bit integer and converts it to hexadecimal */
rand32hex = function () {
/* Gets a random number from 0 to 2^32 - 1 and converts it to hex */
var randomNumber = Math.floor((maxInt32 * Math.random()) + 1);
return randomNumber.toString(16);
};
return function () {
return pad8(rand32hex()) + pad8(rand32hex());
};
}())
Het gebruik is dan eenvoudig:
var randomNumber = randomHex();
Cross Domain Call
Omdat het een beetje tricky is om met Ajax requests te doen naar een ander domein dat die van de huidige website, gaat de library geen ajax request doen naar de Piwik API maar inplaats daarvan een "img" object in de DOM inladen. Een beetje vies, maar het werkt wel. We doen eigenlijk een request van een image, maar in de URL steken we ook de parameters die we eigenlijk willen registreren in piwik.
Aangenomen dat de url al is opgesteld, is de code eenvoudig:
/**
* Send a request, currently using "GET" with an image ...
*/
sendRequest = function (url) {
// Might support POST later ...
// For GET using an image (based on the real piwik.js library)
var image = new Image(1, 1);
image.src = url;
}
Dit veroorzaakt een GET request.
Do-not-track
Moderne browser bieden de optie aan de gebruiker aan om een do-not-track indicatie mee te sturen bij elke request naar webpagina's. Dit om de wens over te brengen aan websites dat de gebruiker niet getracked wenst te worden (dus dat er niet wordt geregistreerd op welke sites hij zoal komt).
Iedere website kan uiteraard die vlag negeren, dus meer dan een indicatie van de wensen van de gebruiker is het niet.
Maar aangezien de piwik code technisch gezien ook tracked wou ik die vlag wél respecteren. Er zijn echter een aantal tricky zaken aan die vlag verbonden. Je kan ze inschakelen, uitschakelen of niet mee geven. Bovendien is het inschakelen bijvoorbeeld niet ondersteund in Firefox en is de waarde van de vlag, als je ze met javascript ophaalt, (nog) niet bij alle browsers gelijk.
Hier heb ik een cross-browser functie voor geschreven:
/**
* Tries to find the value of the doNotTrack flag.
* If no flag is found or not defined, will return undefined.
* If the flag is OFF, it will return false.
* If the flag is ON, it will return true
*/
getDoNotTrack = function () {
var value, dnt;
if (window.navigator.doNotTrack !== undefined) {
value = window.navigator.doNotTrack;
} else if (window.navigator.msDoNotTrack !== undefined) {
value = window.navigator.msDoNotTrack;
}
if (value) {
if (value === 'yes' || value === '1') {
dnt = true;
} else if (value === 'no' || value === '0') {
dnt = false;
}
}
return dnt;
},
Bij de meeste browsers vind je de vlag in "window.navigator.doNotTrack", maar bij IE vind je ze in window.navigator.msDoNotTrack. Sommige browsers gebruiken "yes" en anderen "1" als waarde voor een ingeschakelde doNotTrack vlag.
Een mooi overzicht vind je bij Mozilla: MDN: Navigator.doNotTrack.
Inladen van de library
Dit was zo mogelijk nog het meest interessante van de hele ervaring: hoe laadt iemand de library in zonder daarmee het inladen van de rest van de pagina te vertragen.
Google heeft hier voor hun analytics een mooie uiteenzetting over neergeschreven: Introduction to Analytics.js.
Het komt erop neer dat je niet zomaar in header of zelfs niet onderaan de body een <script src="piwik.js"></script> kunt plaatsen. Omdat dit het weergeven van de pagina kan vertragen. De kunst is om pas na het weergeven van de pagina het inladen van het script te forceren. Mijn library gaat zijn functionaliteit trouwens automatisch uitvoeren eens de library is ingeladen.
Uiteindelijk kwam het neer op dit stukje code, onderaan de body:
<script>
(function () {
var siteid = 1,
host = 'yourwebsite.com',
options = { useHttps: true, sendReferrer: true };
// Loading for the first time
_piwika = {
id: siteid,
host: host,
options: options
};
// create a new script element
var script = document.createElement('script');
script.src = '/javascripts/piwik-light.js';
script.async = true;
// insert the script element into the document
var firstScript = document.getElementsByTagName('script')[0];
firstScript.parentNode.insertBefore(script, firstScript);
}());
</script>
De versie van google was minimized. Mijn versie is explicieter, maar dat maakt ze ook iets makkelijker te volgen.
Eerst stel ik een aantal variabelen in en maak ik een globaal object aan: piwika.
Daar zitten de id van de website (zodat piwik weet bij welke database hij die moet plaatsen). De hostnaam en twee opties in.
Mijn library gaat (momenteel hard-coded) deze waarden ophalen uit dat globaal object.
Daarna maak ik dynamisch een script element aan en steek ik er piwik-light.js in (in mijn geval van de lokale server).
Daarna zoek ik het de eerste script tag in mijn document en steek ik mijn nieuwe script tag er tussen. Op dat moment zal de browser het script effectief inladen.
JSLint Will Hurt Your Feelings
Ik heb JSLint gebruikt om mijn code op kwaliteit te controleren. Het is waar wat ze zeggen: JSLint will hurt your feelings.
Maar niet getreurd. Op één opmerking na is ook JSLint tevreden met mijn library.
En ik persoonlijk ook. Komende van 23KB zit ik nu met een javascript file van 1,67 KB.
Conclusie
Deze piwik library was een ideaal leerscenario: een duidelijk doel maar niet te complex.
En hij werkt perfect voor mijn doeleinden. Misschien heeft iemand anders er ook iets aan, je weet maar nooit!