Angular คือเฟรมเวิร์ก MVC เราจึงต้องกําหนดแอปในลักษณะที่โมเดล มุมมอง และการควบคุมใช้อย่างสมเหตุสมผล โชคดีที่ไม่สำคัญเมื่อใช้ Angular
ท้ายที่สุดแล้ว เราต้องการแสดงรายการไฟล์ของผู้ใช้ สำหรับเรื่องนั้น วิธีที่เรียบง่าย
สำหรับเอกสารทุกฉบับในโมเดลข้อมูล "เอกสาร" ของเรา แต่ละรายการจะมีไอคอนไฟล์ ลิงก์สำหรับเปิดไฟล์ในเว็บ และ last updatedDate
หมายเหตุ: หากต้องการทำให้เทมเพลตเป็น 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>
โปรดทราบว่าสิ่งที่คุณไม่เห็นที่นี่คือเรานำ Listener เหตุการณ์หรือพร็อพเพอร์ตี้สำหรับการเชื่อมโยงข้อมูลมาใช้ Angular ทำให้พวกเรายกน้ำหนัก
ขั้นตอนสุดท้ายคือการทำให้ Angular สว่างไสวในเทมเพลตของเรา วิธีการโดยทั่วไปก็คือการใส่คำสั่ง ngApp ตลอดเส้นทางใน URL ต่อไปนี้
<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>
คำศัพท์เกี่ยวกับนโยบายรักษาความปลอดภัยเนื้อหา
Angular v1.1.0+ ต่างจากเฟรมเวิร์ก JS MVC อื่นๆ ตรงที่คุณไม่ต้องปรับเปลี่ยนเพื่อทำงานภายใน CSP ที่เข้มงวด ใช้งานได้เลยทันทีที่แกะกล่อง
อย่างไรก็ตาม หากคุณกำลังใช้ Angular รุ่นเก่าระหว่าง v1.0.1 และ v1.1.0 คุณจะต้องบอกให้ Angular ให้เรียกใช้ใน "โหมดรักษาความปลอดภัยเนื้อหา" ซึ่งทำได้โดยใส่คำสั่ง ngCsp ไว้ข้าง ngApp
<html data-ng-app data-ng-csp>
การอนุมัติการจัดการ
ทั้งนี้แอปไม่ได้สร้างโมเดลข้อมูลขึ้นเอง แต่สร้างขึ้นจาก API ภายนอก (API Google ไดรฟ์) ดังนั้นจึงต้องมีงานเล็กน้อยเพื่อเติมข้อมูลของแอป
ก่อนที่จะส่งคำขอ 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 ของไดรฟ์และป้อนข้อมูลโมเดล
ตัวควบคุมโครงกระดูก
"model" สำหรับผู้อัปโหลดคืออาร์เรย์อย่างง่าย (เรียกว่า docs) ของออบเจ็กต์ที่จะแสดงผลเป็น
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()
ตอนนี้ ทุกอย่างจะทำงานได้และรายการไฟล์จะปรากฏขึ้น ดังนี้
ไชโย!
เดี๋ยวก่อน... เราพลาดไอคอนไฟล์ที่สวยงามพวกนั้นไปแล้ว What gives? การตรวจสอบคอนโซลอย่างรวดเร็วจะแสดง
ข้อผิดพลาดเกี่ยวกับ CSP หลายรายการ ดังนี้
เหตุผลคือเราพยายามตั้งค่าไอคอน img.src
เป็น URL ภายนอก ซึ่งเป็นการละเมิด CSP เช่น https://ssl.gstatic.com/docs/doclist/images/icon_10_document_list.png
ในการแก้ปัญหานี้ เราต้องดึงเนื้อหาระยะไกล
เหล่านี้มาใส่ไว้ในแอป
การนําเข้าชิ้นงานรูปภาพระยะไกล
เพื่อให้ CSP หยุดตะโกนใส่เราได้ เราใช้ XHR2 เพื่อ "นำเข้า" ไอคอนไฟล์เป็น Blob จากนั้นตั้งค่า 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 จำนวน 100 ครั้งสำหรับไอคอนไฟล์แต่ละไอคอนในการเรียก 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()
จะทำให้ระบบเรียกใช้ข้อผิดพลาด สำหรับกรณีดังกล่าว เราสามารถใช้เทคนิคจากก่อนหน้าและดึงรูปภาพได้ ความแตกต่างเพียงอย่างเดียวในครั้งนี้คือ มีการเขียนแต่ละ BLOB ลงในระบบไฟล์ (ดู writeFile() ) คอนโซลจะยืนยันลักษณะการทำงานนี้
เมื่อเรียกใช้ครั้งถัดไป (หรือกดปุ่ม "รีเฟรช") URL ที่ส่งไปยัง webkitResolveLocalFileSystemURL()
จะมีอยู่เนื่องจากมีการแคชไฟล์ไว้ก่อนหน้านี้ แอปจะตั้งค่า doc.icon
เป็น filesystem: URL
ของไฟล์และหลีกเลี่ยงการสร้าง XHR ที่มีราคาแพงสำหรับไอคอน
การอัปโหลดแบบลากและวาง
แอปเครื่องมืออัปโหลดจะเป็นการโฆษณาเท็จหากอัปโหลดไฟล์ไม่ได้
app.js จัดการฟีเจอร์นี้โดยใช้ไลบรารีขนาดเล็กรอบๆ การลากและวางของ HTML5 ที่เรียกว่า DnDFileController
เครื่องมือนี้ช่วยให้ลากไฟล์จากเดสก์ท็อปและอัปโหลดไปยัง Google ไดรฟ์ได้
เพียงเพิ่มค่านี้ในบริการ 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;
});