Angular는 MVC 프레임워크이므로 모델, 뷰, 컨트롤러가 논리적으로 분리되도록 앱을 정의해야 합니다. 다행히 Angular를 사용하는 경우 이 작업이 간단해집니다.
뷰가 가장 쉬우므로 지금부터 시작하겠습니다.
궁극적으로는 사용자의 파일 목록을 표시하려고 합니다. 이를 위해 간단한
여기 보이는 것과 같이 쓰여 있습니다.
모든 문서에 대한 설명을
확인할 수 있습니다 각 항목에는 파일 아이콘, 웹에서 파일을 열기 위한 링크, 최종 업데이트 날짜가 포함됩니다.
참고: 템플릿을 유효한 HTML로 만들기 위해 Angular의 ngRepeat 반복자에 data-*
속성을 사용하지만 반드시 그럴 필요는 없습니다. 반복자를 쉽게 <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 프레임워크와 달리 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);
}
};
참고: 선택적 콜백을 전달하면 OAuth 토큰이 준비되었는지 유연하게 알 수 있습니다.
참고: 간단하게 API 작업을 처리할 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()
는 DocsController 생성자의 일부로 호출됩니다. Angular의 내부 기능이 컨트롤러를 생성할 때 Google은 사용자를 위해 새로운 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()
는 Angular의 $http
서비스를 사용하여 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를 사용하여 파일 아이콘을 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()
를 호출할 때마다 파일 아이콘마다 100번씩 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()
에 전달된 URL이 존재합니다. 파일이 이전에 캐시되었기 때문입니다. 앱은 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;
});