לכל מסמך ב'מסמכים' של מודל הנתונים שלנו. כל פריט
מכיל סמל של קובץ, קישור לפתיחת הקובץ באינטרנט ותאריך העדכון האחרון.
הערה: כדי שהתבנית תהיה חוקית ב-HTML, אנחנו משתמשים במאפייני data-*
בשביל Angular
איטרטור ngRepeat , אבל אין בכך צורך. אפשר לכתוב בקלות את החוזר כ:
<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 בחלק העליון
הופך את הדף כולו למוכן ב-Agular.
המוצר הסופי של 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
בניגוד להרבה מסגרות MVC אחרות של JS, לא נדרשים תיקונים בגרסה 1.1.0 ואילך של Angular כדי לפעול בסביבה מחמירה
CSP . זה פשוט עובד, בלי שצריך.
עם זאת, אם אתם משתמשים בגרסה ישנה יותר של Angular בין גרסה 1.0.1 לבין 1.1.0, תצטרכו לדעת זאת
זוויתי להפעלה ב'מצב אבטחת תוכן'. כדי לעשות את זה, כוללים את ההוראה ngCsp
לצד ngApp :
<html data-ng-app data-ng-csp>
טיפול בהרשאות
מודל הנתונים לא נוצר על ידי האפליקציה עצמה. במקום זאת, הוא מאוכלס מ-API חיצוני (
ממשק ה-API של Google Drive). לכן, נדרשת קצת עבודה כדי לאכלס את נתוני האפליקציה.
כדי שנוכל לשלוח בקשת 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 );
}
};
הערה: העברת הקריאה החוזרת (callback) האופציונלית מאפשרת לנו לדעת מתי אסימון OAuth
מוכנה.
הערה: כדי לפשט קצת את התהליך, יצרנו ספרייה, gdocs.js שאפשר להשתמש בה במשימות API.
לאחר שנקבל את האסימון, יגיע הזמן להגיש בקשות מול Drive API ולאכלס את המודל.
שלד שלט רחוק
את ה"מודל" לכלי להעלאת סרטונים הוא מערך פשוט (שנקרא 'מסמכים') של אובייקטים שיעובדו
האלה
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 ויוצר אובייקט מסמך חדש לכל רשומה ב
שבפיד.
אם מריצים את 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 );
};
שימו לב שבקריאה החוזרת של webkitResolveLocalFileSystemURL()
אנחנו מתקשרים למספר $scope.$apply()
כאשר
הרשומה האחרונה נראית. בדרך כלל, אין צורך להתקשר למספר $apply()
. Angular מזהה שינויים בנתונים
באופן אוטומטי. אבל במקרה שלנו, יש לנו עוד שכבת קריאה אסינכרונית של קריאה חוזרת (callback)
חברת Angular לא מודעת לכך. צריך להודיע ל-Agular באופן מפורש מתי המודל שלנו מתעדכן.
בהפעלה הראשונה, הסמלים לא יופיעו במערכת הקבצים של HTML5 והקריאות
התוצאה window.webkitResolveLocalFileSystemURL()
תוביל להפעלת קריאה חוזרת (callback) של השגיאה. בשביל זה
אנחנו יכולים לעשות שימוש חוזר בשיטה הקודמת, ולאחזר את התמונות. ההבדל היחיד הפעם הוא
שכל blob נכתב במערכת הקבצים (ראו writeFile() ). המסוף מאמת את זה
התנהגות:
בהפעלה הבאה (או ללחוץ על הלחצן "רענן"), כתובת ה-URL תועבר אל
webkitResolveLocalFileSystemURL()
קיים כי הקובץ כבר נשמר במטמון. האפליקציה מגדירה
doc.icon
ל-filesystem: URL
של הקובץ ומבטל את תהליך ה-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 ;
});