为数据模型“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 框架不同,Angular v1.1.0 及更高版本无需进行任何调整即可在严格的 CSP 内运行。只需开箱即可使用!
但是,如果您使用的是介于 v1.0.1 到 v1.1.0 之间的旧版 Angular,则需要让 Angular 在“内容安全模式”下运行。将 ngCsp 指令与 ngApp 一起使用即可实现这一点:
<html data-ng-app data-ng-csp>
处理授权
数据模型不是由应用本身生成的,而是通过外部 API (Google Drive API) 填充。因此,为了填充应用的数据,需要进行一些工作。
在发出 API 请求之前,我们需要获取用户的 Google 帐号的 OAuth 令牌。
为此,我们创建了一个方法来封装对 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 发出请求并填充模型了。
骨架控制器
上传器的“模型”是一个简单的对象数组(称为文档),这些对象将渲染为
:
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 响应中的数据归档 docs 数组。
$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()
使用 Angular 的 $http
服务通过 XHR 检索主 Feed。OAuth 访问令牌以及其他自定义标头和参数包含在 Authorization
标头中。
successCallback
会处理 API 响应,并为 Feed 中的每个条目创建一个新的文档对象。
如果您现在运行 fetchDocs()
,则一切正常,并显示文件列表:
耶!
等等,我们找不到那些漂亮的文件图标了。What gives? 快速检查一下控制台会显示许多与 CSP 相关的错误:
这是因为我们正在尝试将图标 img.src
设置为外部网址,而这违反了 CSP。例如:https://ssl.gstatic.com/docs/doclist/images/icon_10_document_list.png
。为了解决此问题,我们需要将这些远程资源在本地提取到应用。
导入远程图片素材资源
为了让 CSP 不要再向我们大喊大叫,我们使用 XHR2 将文件图标作为 Blob “导入”,然后将 img.src
设置为应用创建的 blob: URL
。
以下是添加了 XHR 代码的更新后的 successCallback
:
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()
时针对每个文件图标发出数百个 XHR 请求。请在开发者工具控制台中按多次“刷新”按钮进行验证。每次提取 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())。控制台会验证此行为:
下次运行(或按“刷新”按钮)时,传递给 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;
});