Это руководство поможет вам начать создавать приложения Chrome с помощью платформы AngularJS MVC. Чтобы проиллюстрировать Angular в действии, мы будем ссылаться на реальное приложение, созданное с использованием этой платформы — Google Drive Uploader. Исходный код доступен на GitHub.
О приложении
Google Drive Uploader позволяет пользователям быстро просматривать файлы, хранящиеся в их учетной записи Google Drive, и взаимодействовать с ними, а также загружать новые файлы с помощью API-интерфейсов HTML Drag and Drop . Это отличный пример создания приложения, взаимодействующего с одним из API Google ; в данном случае API Google Диска.
Программа загрузки использует OAuth2 для доступа к данным пользователя. API chrome.identity обрабатывает получение токена OAuth для вошедшего в систему пользователя, поэтому вся тяжелая работа сделана за нас! Как только у нас появится долгосрочный токен доступа, приложения будут использовать API Google Диска для доступа к данным пользователя.
Основные функции, которые использует это приложение:
- Автоопределение AngularJS для CSP
- Отображение списка файлов, полученных из API Google Диска.
- API файловой системы HTML5 для хранения значков файлов в автономном режиме
- HTML5 Drag and Drop для импорта/загрузки новых файлов с рабочего стола
- XHR2 для загрузки изображений, междоменный
- chrome.identity API для авторизации OAuth
- Безрамочные рамки для определения внешнего вида навигационной панели приложения.
Создание манифеста
Для всех приложений Chrome требуется файл manifest.json
, содержащий информацию, необходимую Chrome для запуска приложения. Манифест содержит соответствующие метаданные и перечисляет все специальные разрешения, необходимые приложению для запуска.
Урезанная версия манифеста Uploader выглядит так:
{
"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» и «permissions».
Раздел «oauth2» определяет необходимые параметры OAuth2 для творения чудес. Чтобы создать «client_id», следуйте инструкциям в разделе «Получение идентификатора клиента» . В «областях» перечислены области авторизации, для которых будет действителен токен 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 это тривиально.
Вид — самый простой, поэтому начнем с него.
Создание представления
main.html — это буква «V» в MVC; где мы определяем HTML-шаблоны для рендеринга данных. В Angular шаблоны представляют собой простые блоки HTML с особым соусом.
В конечном итоге мы хотим отобразить список файлов пользователя. Для этого простой
- список имеет смысл. Биты Angular выделены жирным шрифтом:
<ul>
<li data-ng-repeat="doc in docs">
<img data-ng-src=""> <a href=""></a>
<span class="date"></span>
</li>
</ul>
Это читается именно так, как выглядит: искоренить
Далее нам нужно указать 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 . Это просто работает, из коробки!
Однако, если вы используете более старую версию 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
, который мы можем повторно использовать для будущих вызовов API Drive.
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.
$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 для «импортирования» значков файлов в виде 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 для каждого значка файла при каждом вызове 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 Drag and Drop под названием 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;
});