این راهنما به شما کمک میکند تا شروع به ساخت برنامههای Chrome با چارچوب AngularJS MVC کنید. برای نشان دادن Angular در عمل، ما به یک برنامه واقعی که با استفاده از این چارچوب ساخته شده است، یعنی Google Drive Uploader اشاره می کنیم. کد منبع در GitHub موجود است.
درباره برنامه
آپلودکننده Google Drive به کاربران اجازه میدهد تا فایلهای ذخیرهشده در حساب Google Drive خود را به سرعت مشاهده کرده و با آنها تعامل داشته باشند و همچنین فایلهای جدید را با استفاده از HTML Drag and Drop API آپلود کنند. این یک مثال عالی از ساختن یک برنامه است که با یکی از APIهای Google صحبت می کند. در این مورد، Google Drive API.
Uploader از OAuth2 برای دسترسی به داده های کاربر استفاده می کند. API chrome.identity واکشی یک نشانه OAuth برای کاربر وارد شده را انجام می دهد، بنابراین کار سخت برای ما انجام می شود! هنگامی که یک نشانه دسترسی طولانی مدت داشته باشیم، برنامه ها از Google Drive API برای دسترسی به داده های کاربر استفاده می کنند.
ویژگی های کلیدی که این برنامه استفاده می کند:
- تشخیص خودکار AngularJS برای CSP
- فهرستی از فایلهای واکشی شده از Google Drive API را ارائه دهید
- HTML5 Filesystem API برای ذخیره آیکون های فایل به صورت آفلاین
- HTML5 کشیدن و رها کردن برای وارد کردن/آپلود فایل های جدید از دسکتاپ
- XHR2 برای بارگذاری تصاویر، متقابل دامنه
- chrome.identity API برای مجوز OAuth
- قابهای بدون کروم برای تعریف ظاهر و احساس نوار ناوبری خود برنامه
ایجاد مانیفست
همه برنامههای Chrome به یک فایل manifest.json
نیاز دارند که حاوی اطلاعاتی باشد که Chrome برای راهاندازی برنامه نیاز دارد. مانیفست حاوی ابرداده مربوطه است و هر گونه مجوز ویژه ای را که برنامه برای اجرا نیاز دارد فهرست می کند.
یک نسخه حذف شده از مانیفست آپلودکننده به این صورت است:
{
"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/"
]
}
مهمترین بخشهای این مانیفست بخشهای «oauth2» و «مجوزها» هستند.
بخش "oauth2" پارامترهای مورد نیاز را توسط OAuth2 برای انجام جادوی خود تعریف می کند. برای ایجاد «client_id»، دستورالعملهای موجود در دریافت شناسه مشتری را دنبال کنید. "scopes" محدوده های مجوزی را فهرست می کند که نشانه OAuth برای آنها معتبر خواهد بود (به عنوان مثال، API هایی که برنامه می خواهد به آنها دسترسی داشته باشد).
بخش «مجوزها» شامل URLهایی است که برنامه از طریق XHR2 به آنها دسترسی خواهد داشت. پیشوندهای URL لازم است تا Chrome بداند کدام درخواستهای بین دامنهای را مجاز میکند.
ایجاد صفحه رویداد
همه برنامههای Chrome برای راهاندازی برنامه و پاسخ به رویدادهای سیستم به یک اسکریپت/صفحه پسزمینه نیاز دارند.
در اسکریپت background.js خود، Drive Uploader یک پنجره 500x600 پیکسلی را به صفحه اصلی باز می کند. همچنین حداقل ارتفاع و عرض را برای پنجره مشخص می کند تا محتوا خیلی خرد نشود:
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'
});
});
پنجره به عنوان یک پنجره بدون کروم (قاب: "هیچ") ایجاد می شود. به طور پیشفرض، پنجرهها با نوار پیشفرض بستن/بسط/به حداقل رساندن سیستم عامل رندر میشوند:
آپلودکننده از frame: 'none'
برای رندر کردن پنجره به عنوان "تصاویر خالی" استفاده می کند و یک دکمه بسته سفارشی در main.html
ایجاد می کند:
کل منطقه ناوبری در یک پیچیده شده است
<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>
در app.js ، این دکمه به window.close()
متصل است.
طراحی اپلیکیشن به روش Angular
Angular یک فریم ورک MVC است، بنابراین باید برنامه را به گونه ای تعریف کنیم که یک مدل، نمای و کنترل کننده به طور منطقی از آن خارج شود. خوشبختانه، هنگام استفاده از Angular، این امر بی اهمیت است.
View ساده ترین است، بنابراین بیایید از آنجا شروع کنیم.
ایجاد دیدگاه
main.html "V" در MVC است. جایی که ما قالب های HTML را برای رندر داده ها تعریف می کنیم. در Angular، قالب ها بلوک های ساده HTML با مقداری سس خاص هستند.
در نهایت می خواهیم لیست فایل های کاربر را نمایش دهیم. برای آن، ساده است
- فهرست منطقی است بیت های Angular به صورت پررنگ برجسته می شوند:
- برای هر سند در مدل داده ما "اسناد". هر مورد شامل یک نماد فایل، پیوند برای باز کردن فایل در وب و آخرین به روز رسانی تاریخ است.
در مرحله بعد، باید به 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 قالب های ما را روشن کند. راه معمولی برای انجام این کار این است که دستورالعمل ngApp را تا آخر اضافه کنید :
<html data-ng-app="gDriveApp">
همچنین در صورت تمایل میتوانید برنامه را به بخش کوچکتری از صفحه محدود کنید. ما فقط یک کنترلر در این برنامه داریم، اما اگر بخواهیم بعداً موارد بیشتری اضافه کنیم، قرار دادن ngApp در بالاترین عنصر، کل صفحه را Angular-ready می کند.
محصول نهایی
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، Angular v1.1.0+ برای کار در یک CSP سختگیرانه، به هیچ گونه تغییری نیاز ندارد. این فقط کار می کند، خارج از جعبه!
با این حال، اگر از نسخه قدیمیتر Angular بین نسخه 1.0.1 و 1.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); } };
هنگامی که توکن را در اختیار داریم، زمان آن است که درخواست هایی را علیه 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()
به عنوان بخشی از سازنده 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 دوباره از ما راضی است، آیکون های فایل خوبی دریافت می کنیم:
آفلاین شدن: ذخیره منابع خارجی
بهینه سازی واضحی که باید انجام شود: در هر تماس با
fetchDocs()
100 درخواست XHR برای هر نماد فایل ایجاد نکنید. با چندین بار فشار دادن دکمه "Refresh" این مورد را در کنسول Developer Tools تأیید کنید. هر بار، 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() مراجعه کنید. کنسول این رفتار را تأیید می کند:پس از اجرای بعدی (یا فشار دادن دکمه "Refresh")، URL ارسال شده به
webkitResolveLocalFileSystemURL()
وجود دارد زیرا فایل قبلاً در حافظه پنهان بوده است. این برنامهdoc.icon
رویfilesystem: URL
و از ایجاد XHR پرهزینه برای نماد جلوگیری میکند.بارگذاری را بکشید و رها کنید
یک اپلیکیشن آپلودگر اگر نتواند فایلها را آپلود کند، تبلیغات نادرستی است!
app.js این ویژگی را با پیاده سازی یک کتابخانه کوچک در اطراف HTML5 Drag and Drop به نام
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; });
<ul>
<li data-ng-repeat="doc in docs">
<img data-ng-src=""> <a href=""></a>
<span class="date"></span>
</li>
</ul>
این دقیقاً همانطور که به نظر می رسد خوانده می شود: an