Angular היא מסגרת MVC, ולכן עלינו להגדיר את האפליקציה כך שהמודל, התצוגה והבקר יושמטו באופן לוגי. למרבה המזל, זה דבר טריוויאלי כשמשתמשים ב-Angular.
התצוגה היא הפשוטה ביותר, אז נתחיל בה.
בסופו של דבר אנחנו רוצים להציג את רשימת הקבצים של המשתמש. לשם כך,
לכל מסמך במודל הנתונים שלנו 'Docs'. כל פריט מכיל סמל של קובץ, קישור לפתיחת הקובץ באינטרנט ותאריך העדכון האחרון.
הערה: כדי שהתבנית תהיה HTML חוקית, אנחנו משתמשים במאפייני data-*
לאיטרטור ngRepeat של Angular, אבל זה לא חובה. אפשר לכתוב בקלות את המשחזר בתור
<li ng-repeat="doc in docs">
.
בשלב הבא עלינו לומר ל-Angular איזה בקר יפקח על הרינדור של התבנית הזו. לשם כך, אנחנו משתמשים בהוראה ngController כדי לומר ל-DocsController
שיש שליטה על התבנית:
<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>
חשוב לזכור שאי אפשר לראות כאן פונקציות event listener או נכסים לקישור נתונים. Angular עושה את העבודה הקשה בשבילנו!
השלב האחרון הוא לגרום ל-Agular להאיר את התבניות שלנו. הדרך הטיפוסית לעשות זאת היא לכלול את הוראת ngApp עד הסוף ב :
<html data-ng-app="gDriveApp">
אם רוצים, אפשר גם לצמצם את היקף האפליקציה לחלק קטן יותר בדף. באפליקציה הזו יש רק נאמן מידע אחד, אבל אם נוסיף עוד פקדים בשלב מאוחר יותר, כדאי למקם את ngApp ברכיב העליון כדי להפוך את הדף כולו למוכן ל-Angular.
המוצר הסופי עבור main.html
נראה כך:
<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>
כמה מילים על Content Security Policy
בניגוד למסגרות אחרות של JS MVC, ב-Angular v1.1.0+ לא נדרשים שינויים כדי לעבוד ב-CSP מחמירה. זה פשוט עובד, מהרגע הראשון!
עם זאת, אם אתם משתמשים בגרסה ישנה יותר של Angular בגרסאות 1.0.1 ו-v1.1.0, תצטרכו להגדיר ל-Angular לפעול ב"מצב אבטחת תוכן". כדי לעשות זאת, כוללים את ההוראה ngCsp לצד ngApp :
<html data-ng-app data-ng-csp>
טיפול בהרשאה
מודל הנתונים לא נוצר על ידי האפליקציה עצמה. במקום זאת, הוא מאוכלס מ-API חיצוני (Google Drive API). לכן נדרשת קצת עבודה כדי לאכלס את נתוני האפליקציה.
לפני שנוכל להגיש בקשת API, עלינו לאחזר אסימון OAuth בשביל חשבון Google של המשתמש.
לשם כך יצרנו שיטה לארוז את הקריאה ל-chrome.identity.getAuthToken()
ולאחסן את הקובץ accessToken
, שבו נוכל להשתמש שוב בקריאות עתידיות ל-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);
}
};
הערה: העברת הקריאה החוזרת האופציונלית מאפשרת לנו לדעת מתי אסימון ה-OAuth מוכן.
הערה: כדי לפשט את התהליך, יצרנו ספרייה בשם gdocs.js שתטפל במשימות ה-API.
אחרי שנקבל את האסימון, הגיע הזמן להגיש בקשות לממשק ה-API של Drive ולאכלס את המודל.
שלד של שלד
ה "מודל" של מעלה הסרטונים הוא מערך פשוט (נקרא מסמכים) של אובייקטים שיוצגו
כאלה
s בתבנית:
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();
});
}
לתשומת ליבך, gdocs.auth()
נקרא כחלק מה-constructor של DocsController. כשהפנימיים ב-Angular יוצרים את הבקר, יש לוודא שלמשתמש ממתין אסימון OAuth חדש.
אחזור נתונים
תבנית מוכנה. פיגומים לבקרים. אסימון OAuth בהישג יד. מה עכשיו?
הגיע הזמן להגדיר את שיטת הבקר הראשית, fetchDocs()
. הוא זה שאחראי על הבקר, שאחראי על בקשות הקבצים של המשתמש ועל מילוי מערך המסמכים עם נתונים מתגובות API.
$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()
משתמשת בשירות $http
של Angular כדי לאחזר את הפיד הראשי באמצעות XHR. אסימון הגישה ל-oauth נכלל בכותרת Authorization
, יחד עם כותרות ופרמטרים מותאמים אישית אחרים.
השדה successCallback
מעבד את תגובת ה-API ויוצר אובייקט Docs חדש לכל רשומה בפיד.
אם מפעילים את fetchDocs()
כרגע, הכול עובד ורשימת הקבצים מופיעה:
מחיאות כפיים!
רגע, חסרים לנו הסמלים של הקבצים המגניבים. מה נותן? בבדיקה מהירה של המסוף יש כמה שגיאות שקשורות ל-CSP:
הסיבה לכך היא שאנחנו מנסים להגדיר את הסמלים img.src
לכתובות URL חיצוניות. זו הפרה של מדיניות CSP. למשל: https://ssl.gstatic.com/docs/doclist/images/icon_10_document_list.png
. כדי לפתור את הבעיה, אנחנו צריכים להעביר את הנכסים המרוחקים האלה באופן מקומי לאפליקציה.
ייבוא נכסי תמונות מרוחקים
כדי ש-CSP יפסיק לצעוק לנו, אנחנו משתמשים ב-XHR2 כדי "לייבא" את סמלי הקבצים כ-Blobs, ואז מגדירים את img.src
לערך blob: URL
שנוצר על ידי האפליקציה.
הנה successCallback
המעודכן עם קוד ה-XHR שנוסף:
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);
}
});
});
};
עכשיו, כש-CSP מרוצה מאיתנו שוב, אנחנו מקבלים סמלים נחמדים של קבצים:
מעבר למצב אופליין: שמירת משאבים חיצוניים במטמון
האופטימיזציה הברורה שצריך לעשות: היא לא לבצע מאות בקשות XHR לכל סמל של קובץ בכל קריאה ל-fetchDocs()
. ניתן לבדוק זאת במסוף הכלים למפתחים על ידי לחיצה על הלחצן 'רענן' כמה פעמים. בכל פעם, n תמונות מאוחזרות:
בואו נשנה את successCallback
כדי להוסיף שכבת שמירה במטמון. התוספות מודגשות בגופן מודגש:
$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);
};
שימו לב שבהתקשרות החוזרת (callback) של webkitResolveLocalFileSystemURL()
אנחנו מבצעים קריאה ל-$scope.$apply()
כשמופיעה הרשומה האחרונה. בדרך כלל אין צורך להתקשר אל $apply()
. Angular מזהה באופן אוטומטי שינויים במודלים של נתונים. עם זאת, במקרה שלנו יש שכבת הוספה של קריאה חוזרת אסינכרונית ש-Angular לא מודעת לה. אנחנו חייבים להודיע ל-Angular באופן מפורש מתי המודל שלנו מתעדכן.
בהפעלה הראשונה, הסמלים לא יופיעו במערכת הקבצים של HTML5 והקריאות אל window.webkitResolveLocalFileSystemURL()
יגרמו להפעלה של הקריאה החוזרת (callback) מסוג שגיאת שגיאה. במקרה כזה, אפשר להשתמש שוב בשיטה הקודמת ולאחזר את התמונות. ההבדל היחיד במקרה הזה הוא שכל blob נכתב במערכת הקבצים (פרטים נוספים בקטע writeFile() ). המסוף מאמת את ההתנהגות הזו:
בהפעלה הבאה (או בלחיצה על הלחצן 'רענן'), כתובת ה-URL שהועברה אל webkitResolveLocalFileSystemURL()
קיימת כי הקובץ כבר נשמר במטמון. האפליקציה מגדירה את הערך filesystem: URL
של הקובץ בשדה doc.icon
, ומונעת יצירה של XHR יקר של הסמל.
העלאה באמצעות גרירה ושחרור
אפליקציה של מעלה התוכן מפרסמת באופן שגוי אם לא ניתן להעלות קבצים!
app.js מטפל בתכונה הזו על ידי הטמעת ספרייה קטנה בנושא גרירה ושחרור של HTML5 בשם DnDFileController
. הוא מאפשר לגרור קבצים משולחן העבודה ולהעלות אותם ל-Google Drive.
הוספת הקוד לשירות gdocs עושה את העבודה:
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;
});