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>
ضع في اعتبارك أن ما لا تراه هنا هو أننا نربط أدوات معالجة الأحداث أو المواقع
لربط البيانات. يقوم Angular بهذا رفع الأثقال من أجلنا!
الخطوة الأخيرة هي جعل Angular تضيء نماذجنا. تتمثل الطريقة المعتادة لإجراء ذلك في تضمين توجيه 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>
نبذة عن سياسة أمان المحتوى
على عكس العديد من أطر عمل JS MVC الأخرى، لا يتطلب الإصدار 1.1.0 والإصدارات الأحدث من Angular أي تعديلات على الملف للعمل باستخدام CSP صارم. إنها تعمل فقط بمزايا جديدة!
ومع ذلك، إذا كنت تستخدم إصدارًا قديمًا من Angular، بين الإصدارين 1.0.1 وv1.1.0، يجب تفعيل
تطبيق Angular لتشغيله في "وضع أمان المحتوى". ويتم ذلك من خلال تضمين التوجيه ngCsp إلى جانب ngApp :
<html data-ng-app data-ng-csp>
معالجة التفويض
لا يتم إنشاء نموذج البيانات بواسطة التطبيق نفسه. بدلاً من ذلك، تتم تعبئته من واجهة برمجة تطبيقات خارجية (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);
}
};
ملاحظة: يمنحنا تمرير معاودة الاتصال الاختيارية مرونة في معرفة الوقت الذي يكون فيه رمز OAuth
جاهزًا.
ملاحظة: لتبسيط الأمور، أنشأنا مكتبة، gdocs.js ، لمعالجة مهام واجهة برمجة التطبيقات.
بعد حصولنا على الرمز المميّز، يحين وقت تقديم طلبات مقابل Drive API وملء النموذج.
وحدة التحكّم في الهيكل العظمي
"نموذج" القائم بالتحميل هو مصفوفة بسيطة (تسمى مستندات) من الكائنات التي سيتم عرضها
في القالب:
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()
كجزء من الدالة الإنشائية لوحدة التحكم في محرّر المستندات. عندما تنشئ العناصر الداخلية في Angular وحدة التحكم،
نضمن الحصول على رمز OAuth مميز جديد في انتظار المستخدم.
جارٍ استرجاع البيانات
تم تخطيط النموذج. تم تثبيت وحدة التحكّم. رمز OAuth المميز في متناول اليد. ما الخطوة التالية التي يجب اتخاذها؟
حان الوقت لتحديد طريقة وحدة التحكّم الرئيسية، fetchDocs()
. إنه عمود عمل وحدة التحكم، وهو المسئول عن
طلب ملفات المستخدم وملء صفيف المستندات بالبيانات من ردود واجهة برمجة التطبيقات.
$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
استجابة واجهة برمجة التطبيقات وتنشئ كائن مستند جديدًا لكل إدخال في الخلاصة.
في حال تشغيل fetchDocs()
الآن، كل شيء على ما يرام وستظهر قائمة الملفات:
رائع.
انتظر،...فقد نفتقد رموز الملفات الأنيقة هذه. What gives? يعرض التحقق السريع من وحدة التحكم مجموعة من
الأخطاء المتعلقة بـ CSP:
السبب هو أنّنا نحاول ضبط الرموز img.src
على عناوين URL خارجية، ما يخالف سياسة CSP. على سبيل المثال: https://ssl.gstatic.com/docs/doclist/images/icon_10_document_list.png
. لإصلاح ذلك، نحتاج إلى سحب
الأصول البعيدة هذه محليًا إلى التطبيق.
استيراد أصول الصور عن بُعد
لكي يتوقف CSP عن الصراخ علينا، نستخدم XHR2 "لاستيراد" رموز الملف كوحدات تخزين كبيرة، ثم نضبط 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 راضٍ عن خدماتنا مرة أخرى، نحصل على رموز جيدة للملفات:
العمل بلا اتصال بالإنترنت: تخزين الموارد الخارجية في ذاكرة التخزين المؤقت
التحسين الواضح الذي يجب إجراؤه: عدم إجراء 100 ثانية من طلبات 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 التغييرات على
نماذج البيانات تلقائيًا. ومع ذلك، في حالتنا هذه، لدينا طبقة إضافية من معاودة الاتصال غير المتزامنة التي لا يكون Angular على دراية بها. يجب أن نخبر Angular مباشرةً عندما يتم تحديث نموذجنا.
عند التشغيل لأول مرة، لن تكون الرموز في نظام ملفات HTML5 وسيؤدي الاستدعاءات إلى
window.webkitResolveLocalFileSystemURL()
إلى استدعاء استدعاء الخطأ الخاص به. بالنسبة لهذه الحالة، يمكننا إعادة استخدام التقنية من قبل وجلب الصور. الاختلاف الوحيد هذه المرة هو أنّ كل كائن ثنائي كبير تتم كتابته في نظام الملفات (راجع 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;
});