Angular ist ein MVC-Framework. Daher müssen wir die Anwendung so definieren, dass Modell, Ansicht und Controller logisch herausfallen. Glücklicherweise ist dies bei der Verwendung von Angular einfach.
Am einfachsten ist die Ansicht. Fangen wir also an.
Letzten Endes soll nur die Dateiliste des Nutzers angezeigt werden. Dafür gibt es eine einfache
für jedes Dokument in unserem Datenmodell „Dokumente“. Jedes Element enthält ein Dateisymbol, einen Link zum Öffnen der Datei im Web und das Datum der letzten Aktualisierung.
Hinweis: Damit die Vorlage gültigen HTML-Code erhält, verwenden wir data-*
-Attribute für den ngRepeat -Iterator von Angular, was aber nicht unbedingt erforderlich ist. Sie können den Repeater einfach als <li ng-repeat="doc in docs">
schreiben.
Als Nächstes müssen wir Angular mitteilen, welcher Controller das Rendering dieser Vorlage überwacht. Dazu verwenden wir die Anweisung ngController , um der DocsController
mitzuteilen, dass sie die Vorlage verwenden soll:
<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>
Beachten Sie, dass Sie hier nicht sehen, dass wir Event-Listener oder Attribute für die Datenbindung verknüpfen. Angular übernimmt diese schwierige Arbeit für uns.
Der letzte Schritt besteht darin, Angular unsere Vorlagen zum Leuchten zu bringen. In der Regel fügen Sie dazu die Anweisung ngApp bis ganz nach oben in ein :
<html data-ng-app="gDriveApp">
Sie können die App auch auf einen kleineren Teil der Seite beschränken. Wir haben in dieser Anwendung nur einen Controller. Wenn wir später weitere hinzufügen würden, wird die gesamte Seite angular-fähig, wenn wir ngApp auf das oberste Element einfügen.
Das Endprodukt für main.html
sieht in etwa so aus:
<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>
Eine Anmerkung zur Content Security Policy
Im Gegensatz zu vielen anderen JS MVC-Frameworks sind bei Angular Version 1.1.0 und höher keine Änderungen erforderlich, um innerhalb eines strengen CSP zu funktionieren. Es funktioniert sofort nach dem Auspacken!
Wenn Sie jedoch eine ältere Angular-Version zwischen 1.0.1 und 1.1.0 verwenden, müssen Sie Angular so konfigurieren, dass es im Inhaltssicherheitsmodus ausgeführt wird. Dazu wird die Anweisung ngCsp zusammen mit ngApp eingefügt:
<html data-ng-app data-ng-csp>
Autorisierung handhaben
Das Datenmodell wird nicht von der App selbst generiert. Stattdessen wird sie von einer externen API (Google Drive API) ausgefüllt. Daher ist ein gewisser Aufwand erforderlich, um die Daten der App zu befüllen.
Bevor wir eine API-Anfrage durchführen können, müssen wir ein OAuth-Token für das Google-Konto des Nutzers abrufen.
Dazu haben wir eine Methode erstellt, um den Aufruf von chrome.identity.getAuthToken()
zu kapseln und den accessToken
zu speichern, den wir für zukünftige Aufrufe der Drive API wiederverwenden können.
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);
}
};
Hinweis :Durch das Übergeben des optionalen Callbacks können wir flexibel feststellen, wann das OAuth-Token bereit ist.
Hinweis :Zur Vereinfachung haben wir für API-Aufgaben eine Bibliothek namens gdocs.js erstellt.
Sobald wir das Token haben, können Sie Anfragen an die Drive API senden und das Modell ausfüllen.
Skelett-Controller
Das Modell für den Uploader ist ein einfaches Array (Dokumente genannt) von Objekten, die wie folgt gerendert werden:
in der Vorlage verwenden:
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();
});
}
Beachten Sie, dass gdocs.auth()
als Teil des DocsController-Konstruktors aufgerufen wird. Wenn der Controller von den internen Angular-Instanzen erstellt wird, ist versichert, dass ein neues OAuth-Token auf den Nutzer wartet.
Daten abrufen
Die Vorlage. Controller-Scaffold. OAuth-Token vorliegt. Was heißt das für die Zukunft?
Jetzt definieren Sie die Hauptcontrollermethode fetchDocs()
. Er ist das Arbeitstier des Controllers. Er ist dafür verantwortlich, die Dateien des Nutzers anzufordern und das Array „docs“ mit Daten aus API-Antworten zu füllen.
$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()
verwendet den Dienst $http
von Angular, um den Hauptfeed über XHR abzurufen. Das OAuth-Zugriffstoken ist zusammen mit anderen benutzerdefinierten Headern und Parametern im Header Authorization
enthalten.
Das successCallback
verarbeitet die API-Antwort und erstellt für jeden Eintrag im Feed ein neues DOC-Objekt.
Wenn Sie fetchDocs()
jetzt ausführen, funktioniert alles und die Liste der Dateien wird angezeigt:
Super!
Moment, wir vermissen diese netten Dateisymbole. What gives? Eine kurze Prüfung der Konsole zeigt eine Reihe von CSP-bezogenen Fehlern an:
Wir versuchen, die Symbole img.src
auf externe URLs zu beschränken. Dies stellt einen Verstoß gegen die CSP dar. Beispiel: https://ssl.gstatic.com/docs/doclist/images/icon_10_document_list.png
Um dieses Problem zu beheben, müssen wir diese
Remote-Assets lokal in die App laden.
Remote-Bild-Assets importieren
Damit die CSP nicht mehr anschreit, verwenden wir XHR2, um die Dateisymbole als Blobs zu "importieren" und dann img.src
auf einen von der Anwendung erstellten blob: URL
zu setzen.
Hier ist das aktualisierte successCallback
mit dem hinzugefügten 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);
}
});
});
};
Da die CSP nun wieder mit uns zufrieden ist, bekommen wir schöne Dateisymbole:
Offline gehen: Externe Ressourcen im Cache speichern
Die offensichtliche Optimierung, die vorgenommen werden muss: Senden Sie nicht Hunderte von XHR-Anfragen für jedes Dateisymbol bei jedem Aufruf von fetchDocs()
. Überprüfen Sie dies in der Entwicklertools-Konsole, indem Sie mehrmals auf die Schaltfläche "Aktualisieren" klicken. Jedes Mal werden n Bilder abgerufen:
Ändern wir successCallback
, um eine Caching-Ebene hinzuzufügen. Die Ergänzungen sind fett hervorgehoben:
$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);
};
Im webkitResolveLocalFileSystemURL()
-Callback rufen wir $scope.$apply()
auf, wenn der letzte Eintrag zu sehen ist. Normalerweise ist es nicht erforderlich, $apply()
aufzurufen. Angular erkennt Änderungen an Datenmodellen automatisch. In unserem Fall gibt es jedoch eine zusätzliche Schicht des asynchronen Callbacks, die Angular nicht erkennt. Wir müssen Angular explizit mitteilen, wenn unser Modell aktualisiert wurde.
Bei der ersten Ausführung sind die Symbole nicht im HTML5-Dateisystem vorhanden und die Aufrufe von window.webkitResolveLocalFileSystemURL()
führen dazu, dass der zugehörige Fehler-Callback aufgerufen wird. In diesem Fall können wir die bisherige Technik wiederverwenden und die Bilder abrufen. Der einzige Unterschied zu diesem Zeitpunkt besteht darin, dass jedes Blob in das Dateisystem geschrieben wird (siehe writeFile() ). Die Console überprüft dieses Verhalten:
Bei der nächsten Ausführung (oder durch Klicken auf die Schaltfläche „Aktualisieren“) existiert die an webkitResolveLocalFileSystemURL()
übergebene URL, weil die Datei zuvor im Cache gespeichert wurde. Die Anwendung legt doc.icon
auf den filesystem: URL
der Datei fest und vermeidet die kostspielige XHR für das Symbol.
Upload per Drag-and-drop
Eine Uploader-App verwendet falsche Werbung, wenn sie keine Dateien hochladen kann.
app.js implementiert diese Funktion durch Implementierung einer kleinen Bibliothek mit dem Namen DnDFileController
um HTML5 Drag-and-drop. Sie können damit Dateien vom Desktop per Drag-and-drop in Google Drive hochladen.
Dazu fügen Sie einfach Folgendes zu gdocs hinzu:
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;
});