Met deze handleiding kunt u aan de slag met het bouwen van Chrome-apps met het AngularJS MVC-framework. Om Angular in actie te illustreren, verwijzen we naar een daadwerkelijke app die is gebouwd met behulp van het raamwerk, de Google Drive Uploader. De broncode is beschikbaar op GitHub.
Over de app
Met de Google Drive Uploader kunnen gebruikers snel bestanden bekijken en gebruiken die zijn opgeslagen in hun Google Drive-account, en nieuwe bestanden uploaden met behulp van de HTML Drag and Drop API's . Het is een goed voorbeeld van het bouwen van een app die met een van de API's van Google praat; in dit geval de Google Drive API.
De Uploader gebruikt OAuth2 om toegang te krijgen tot de gegevens van de gebruiker. De chrome.identity API zorgt voor het ophalen van een OAuth-token voor de ingelogde gebruiker, dus het harde werk is voor ons gedaan! Zodra we een toegangstoken met een lange levensduur hebben, gebruiken de apps de Google Drive API om toegang te krijgen tot de gegevens van de gebruiker.
Belangrijkste functies die deze app gebruikt:
- AngularJS's autodetectie voor CSP
- Geef een lijst weer met bestanden die zijn opgehaald uit de Google Drive API
- HTML5-bestandssysteem-API om bestandspictogrammen offline op te slaan
- HTML5 Drag and Drop voor het importeren/uploaden van nieuwe bestanden vanaf het bureaublad
- XHR2 om afbeeldingen te laden, cross-domein
- chrome.identity API voor OAuth-autorisatie
- Chromeloze frames om het uiterlijk van de navigatiebalk van de app te definiëren
Het manifest maken
Alle Chrome-apps vereisen een manifest.json
bestand dat de informatie bevat die Chrome nodig heeft om de app te starten. Het manifest bevat relevante metagegevens en vermeldt eventuele speciale machtigingen die de app moet uitvoeren.
Een uitgeklede versie van het manifest van de Uploader ziet er als volgt uit:
{
"name": "Google Drive Uploader",
"version": "0.0.1",
"manifest_version": 2,
"oauth2": {
"client_id": "665859454684.apps.googleusercontent.com",
"scopes": [
"https://www.googleapis.com/auth/drive"
]
},
...
"permissions": [
"https://docs.google.com/feeds/",
"https://docs.googleusercontent.com/",
"https://spreadsheets.google.com/feeds/",
"https://ssl.gstatic.com/",
"https://www.googleapis.com/"
]
}
De belangrijkste onderdelen van dit manifest zijn de secties 'oauth2' en 'permissions'.
De sectie "oauth2" definieert de vereiste parameters die OAuth2 nodig heeft om zijn magie te doen. Om een "client_id" aan te maken, volgt u de instructies in Uw client-ID ophalen . De 'scopes' vermelden de autorisatiebereiken waarvoor het OAuth-token geldig is (bijvoorbeeld de API's waartoe de app toegang wil hebben).
Het gedeelte 'machtigingen' bevat URL's waartoe de app toegang heeft via XHR2. De URL-voorvoegsels zijn vereist zodat Chrome weet welke cross-domein verzoeken moeten worden toegestaan.
Het maken van de evenementenpagina
Alle Chrome-apps vereisen een achtergrondscript/-pagina om de app te starten en te reageren op systeemgebeurtenissen.
In het background.js- script opent Drive Uploader een venster van 500x600px naar de hoofdpagina. Het specificeert ook een minimale hoogte en breedte voor het venster, zodat de inhoud niet te krap wordt:
chrome.app.runtime.onLaunched.addListener(function(launchData) {
chrome.app.window.create('../main.html', {
id: "GDriveExample",
bounds: {
width: 500,
height: 600
},
minWidth: 500,
minHeight: 600,
frame: 'none'
});
});
Het raam is gemaakt als chroomloos raam (frame: 'geen'). Standaard worden vensters weergegeven met de standaard sluit-/uitbreidings-/minimalisatiebalk van het besturingssysteem:
De Uploader gebruikt frame: 'none'
om het venster als een "lege lei" weer te geven en maakt een aangepaste sluitknop in main.html
:
Het gehele navigatiegebied is verpakt in een
<style>
nav:hover #close-button {
opacity: 1;
}
#close-button {
float: right;
padding: 0 5px 2px 5px;
font-weight: bold;
opacity: 0;
transition: all 0.3s ease-in-out;
}
</style>
<button class="btn" id="close-button" title="Close">x</button>
In app.js is deze knop gekoppeld aan window.close()
.
Het ontwerpen van de app op de Angular-manier
Angular is een MVC-framework, dus we moeten de app zo definiëren dat een model, weergave en controller er logischerwijs uit vallen. Gelukkig is dit triviaal bij het gebruik van Angular.
De weergave is het gemakkelijkst, dus laten we daar beginnen.
Het creëren van de weergave
main.html is de "V" in MVC; waar we HTML-sjablonen definiëren om gegevens in weer te geven. In Angular zijn sjablonen eenvoudige HTML-blokken met een speciaal sausje.
Uiteindelijk willen we de lijst met bestanden van de gebruiker weergeven. Daarvoor een simpel
- lijst is logisch. De hoekbits zijn vetgedrukt:
<ul>
<li data-ng-repeat="doc in docs">
<img data-ng-src=""> <a href=""></a>
<span class="date"></span>
</li>
</ul>
Dit leest precies zoals het eruit ziet: stamp een uit
Vervolgens moeten we Angular vertellen welke controller toezicht zal houden op de weergave van deze sjabloon. Daarvoor gebruiken we de ngController- richtlijn om de DocsController
te vertellen dat hij controle heeft over de sjabloon
<body data-ng-controller="DocsController">
<section id="main">
<ul>
<li data-ng-repeat="doc in docs">
<img data-ng-src=""> <a href=""></a>
<span class="date"></span>
</li>
</ul>
</section>
</body>
Houd er rekening mee dat wat u hier niet ziet, is dat we gebeurtenislisteners of eigenschappen aansluiten voor gegevensbinding. Angular doet het zware werk voor ons!
De laatste stap is om Angular onze sjablonen te laten verlichten. De typische manier om dat te doen is door de ngApp- richtlijn helemaal bovenaan op te nemen :
<html data-ng-app="gDriveApp">
U kunt de app ook tot een kleiner deel van de pagina beperken als u dat wilt. We hebben maar één controller in deze app, maar als we er later meer zouden toevoegen, maakt het plaatsen van ngApp op het bovenste element de hele pagina Angular-ready.
Het eindproduct voor main.html
ziet er ongeveer zo uit:
<html data-ng-app="gDriveApp">
<head>
…
<base target="_blank">
</head>
<body data-ng-controller="DocsController">
<section id="main">
<nav>
<h2>Google Drive Uploader</h2>
<button class="btn" data-ng-click="fetchDocs()">Refresh</button>
<button class="btn" id="close-button" title="Close"></button>
</nav>
<ul>
<li data-ng-repeat="doc in docs">
<img data-ng-src=""> <a href=""></a>
<span class="date"></span>
</li>
</ul>
</section>
Een woord over het inhoudsbeveiligingsbeleid
In tegenstelling tot veel andere JS MVC-frameworks vereist Angular v1.1.0+ geen aanpassingen om binnen een strikte CSP te werken. Het werkt gewoon, out-of-the-box!
Als u echter een oudere versie van Angular tussen v1.0.1 en v1.1.0 gebruikt, moet u Angular vertellen dat deze in een "inhoudsbeveiligingsmodus" moet draaien. Dit wordt gedaan door naast ngApp de ngCsp- richtlijn op te nemen:
<html data-ng-app data-ng-csp>
Autorisatie afhandelen
Het gegevensmodel wordt niet door de app zelf gegenereerd. In plaats daarvan wordt het ingevuld vanuit een externe API (de Google Drive API). Er is dus wat werk nodig om de gegevens van de app te vullen.
Voordat we een API-verzoek kunnen doen, moeten we een OAuth-token ophalen voor het Google-account van de gebruiker. Daarvoor hebben we een methode gemaakt om de aanroep van chrome.identity.getAuthToken()
in te pakken en de accessToken
op te slaan, die we kunnen hergebruiken voor toekomstige aanroepen van de Drive API.
GDocs.prototype.auth = function(opt_callback) {
try {
chrome.identity.getAuthToken({interactive: false}, function(token) {
if (token) {
this.accessToken = token;
opt_callback && opt_callback();
}
}.bind(this));
} catch(e) {
console.log(e);
}
};
Zodra we het token hebben, is het tijd om verzoeken in te dienen tegen de Drive API en het model te vullen.
Skeletcontroleur
Het "model" voor de Uploader is een eenvoudige array (docs genoemd) van objecten die als zodanig worden weergegeven
var gDriveApp = angular.module('gDriveApp', []);
gDriveApp.factory('gdocs', function() {
var gdocs = new GDocs();
return gdocs;
});
function DocsController($scope, $http, gdocs) {
$scope.docs = [];
$scope.fetchDocs = function() {
...
};
// Invoke on ctor call. Fetch docs after we have the oauth token.
gdocs.auth(function() {
$scope.fetchDocs();
});
}
Merk op dat gdocs.auth()
wordt aangeroepen als onderdeel van de DocsController-constructor. Wanneer de interne functies van Angular de controller maken, zijn we er zeker van dat er een nieuw OAuth-token op de gebruiker wacht.
Gegevens ophalen
Sjabloon opgemaakt. Controller in de steigers. OAuth-token in de hand. Wat nu?
Het is tijd om de hoofdcontrollermethode fetchDocs()
te definiëren. Het is het werkpaard van de controller, verantwoordelijk voor het opvragen van de bestanden van de gebruiker en het archiveren van de documentenarray met gegevens uit API-reacties.
$scope.fetchDocs = function() {
$scope.docs = []; // First, clear out any old results
// Response handler that doesn't cache file icons.
var successCallback = function(resp, status, headers, config) {
var docs = [];
var totalEntries = resp.feed.entry.length;
resp.feed.entry.forEach(function(entry, i) {
var doc = {
title: entry.title.$t,
updatedDate: Util.formatDate(entry.updated.$t),
updatedDateFull: entry.updated.$t,
icon: gdocs.getLink(entry.link,
'http://schemas.google.com/docs/2007#icon').href,
alternateLink: gdocs.getLink(entry.link, 'alternate').href,
size: entry.docs$size ? '( ' + entry.docs$size.$t + ' bytes)' : null
};
$scope.docs.push(doc);
// Only sort when last entry is seen.
if (totalEntries - 1 == i) {
$scope.docs.sort(Util.sortByDate);
}
});
};
var config = {
params: {'alt': 'json'},
headers: {
'Authorization': 'Bearer ' + gdocs.accessToken,
'GData-Version': '3.0'
}
};
$http.get(gdocs.DOCLIST_FEED, config).success(successCallback);
};
fetchDocs()
gebruikt $http
service van Angular om de hoofdfeed via XHR op te halen. Het oauth-toegangstoken is opgenomen in de Authorization
header, samen met andere aangepaste headers en parameters.
De successCallback
verwerkt het API-antwoord en creëert een nieuw doc-object voor elk item in de feed.
Als je nu fetchDocs()
uitvoert, werkt alles en verschijnt de lijst met bestanden:
Woot!
Wacht,...we missen die handige bestandspictogrammen. Wat geeft? Een snelle controle van de console toont een aantal CSP-gerelateerde fouten:
De reden is dat we proberen de pictogrammen img.src
in te stellen op externe URL's. Dit is in strijd met het CSP. Bijvoorbeeld: https://ssl.gstatic.com/docs/doclist/images/icon_10_document_list.png
. Om dit op te lossen, moeten we deze externe middelen lokaal naar de app halen.
Externe afbeeldingsmiddelen importeren
Om CSP te laten stoppen met tegen ons te schreeuwen, gebruiken we XHR2 om de bestandspictogrammen te "importeren" als Blobs en stellen we vervolgens img.src
in op een blob: URL
gemaakt door de app.
Hier is de bijgewerkte successCallback
met de toegevoegde XHR-code:
var successCallback = function(resp, status, headers, config) {
var docs = [];
var totalEntries = resp.feed.entry.length;
resp.feed.entry.forEach(function(entry, i) {
var doc = {
...
};
$http.get(doc.icon, {responseType: 'blob'}).success(function(blob) {
console.log('Fetched icon via XHR');
blob.name = doc.iconFilename; // Add icon filename to blob.
writeFile(blob); // Write is async, but that's ok.
doc.icon = window.URL.createObjectURL(blob);
$scope.docs.push(doc);
// Only sort when last entry is seen.
if (totalEntries - 1 == i) {
$scope.docs.sort(Util.sortByDate);
}
});
});
};
Nu CSP weer blij met ons is, krijgen we mooie bestandsiconen:
Offline gaan: externe bronnen in de cache opslaan
De voor de hand liggende optimalisatie die moet worden doorgevoerd: voer geen honderden XHR-verzoeken uit voor elk bestandspictogram bij elke aanroep van fetchDocs()
. Controleer dit in de Developer Tools-console door meerdere keren op de knop "Vernieuwen" te drukken. Elke keer worden n afbeeldingen opgehaald:
Laten we successCallback
aanpassen om een cachinglaag toe te voegen. De toevoegingen zijn vetgedrukt:
$scope.fetchDocs = function() {
...
// Response handler that caches file icons in the filesystem API.
var successCallbackWithFsCaching = function(resp, status, headers, config) {
var docs = [];
var totalEntries = resp.feed.entry.length;
resp.feed.entry.forEach(function(entry, i) {
var doc = {
...
};
// 'https://ssl.gstatic.com/doc_icon_128.png' -> 'doc_icon_128.png'
doc.iconFilename = doc.icon.substring(doc.icon.lastIndexOf('/') + 1);
// If file exists, it we'll get back a FileEntry for the filesystem URL.
// Otherwise, the error callback will fire and we need to XHR it in and
// write it to the FS.
var fsURL = fs.root.toURL() + FOLDERNAME + '/' + doc.iconFilename;
window.webkitResolveLocalFileSystemURL(fsURL, function(entry) {
doc.icon = entry.toURL(); // should be === to fsURL, but whatevs.
$scope.docs.push(doc); // add doc to model.
// Only want to sort and call $apply() when we have all entries.
if (totalEntries - 1 == i) {
$scope.docs.sort(Util.sortByDate);
$scope.$apply(function($scope) {}); // Inform angular that we made changes.
}
}, function(e) {
// Error: file doesn't exist yet. XHR it in and write it to the FS.
$http.get(doc.icon, {responseType: 'blob'}).success(function(blob) {
console.log('Fetched icon via XHR');
blob.name = doc.iconFilename; // Add icon filename to blob.
writeFile(blob); // Write is async, but that's ok.
doc.icon = window.URL.createObjectURL(blob);
$scope.docs.push(doc);
// Only sort when last entry is seen.
if (totalEntries - 1 == i) {
$scope.docs.sort(Util.sortByDate);
}
});
});
});
};
var config = {
...
};
$http.get(gdocs.DOCLIST_FEED, config).success(successCallbackWithFsCaching);
};
Merk op dat we in de webkitResolveLocalFileSystemURL()
callback $scope.$apply()
aanroepen wanneer het laatste item wordt gezien. Normaal gesproken is het aanroepen van $apply()
niet nodig. Angular detecteert automatisch wijzigingen in datamodellen. In ons geval hebben we echter een extra laag van asynchrone callback waarvan Angular zich niet bewust is. We moeten Angular expliciet vertellen wanneer ons model is bijgewerkt.
Bij de eerste keer opstarten bevinden de pictogrammen zich niet in het HTML5-bestandssysteem en zullen de aanroepen van window.webkitResolveLocalFileSystemURL()
ertoe leiden dat de foutcallback wordt aangeroepen. In dat geval kunnen we de techniek van vroeger hergebruiken en de afbeeldingen ophalen. Het enige verschil deze keer is dat elke blob naar het bestandssysteem wordt geschreven (zie writeFile() ). De console verifieert dit gedrag:
Bij de volgende uitvoering (of druk op de knop "Vernieuwen") bestaat de URL die is doorgegeven aan webkitResolveLocalFileSystemURL()
omdat het bestand eerder in de cache is opgeslagen. De app stelt doc.icon
in op het filesystem: URL
het bestand en vermijdt het maken van de dure XHR voor het pictogram.
Uploaden via slepen en neerzetten
Een uploader-app is valse reclame als deze geen bestanden kan uploaden!
app.js verwerkt deze functie door een kleine bibliotheek rond HTML5 Drag and Drop te implementeren, genaamd DnDFileController
. Het geeft de mogelijkheid om bestanden vanaf het bureaublad te slepen en naar Google Drive te uploaden.
Door dit simpelweg aan de gdocs-service toe te voegen, is het werk gedaan:
gDriveApp.factory('gdocs', function() {
var gdocs = new GDocs();
var dnd = new DnDFileController('body', function(files) {
var $scope = angular.element(this).scope();
Util.toArray(files).forEach(function(file, i) {
gdocs.upload(file, function() {
$scope.fetchDocs();
});
});
});
return gdocs;
});