- PVSM.RU - https://www.pvsm.ru -

Создание простого Chrome приложения

В прошлом топике [1] я постарался рассказать, что такое Chrome app, и зачем их писать. В этом, как обещал, я опишу процесс создания простого Chrome-приложения. В качестве примера будет использован текстовый редактор. Во-первых, его можно написать очень коротко, так чтобы практически весь код поместился в статью. Во-вторых, в текстовом редакторе будут использоваться несколько характерных для Chrome (и других основанных на Chromium браузеров) программных интерфейсов. В-третьих, да, я уже писал текстовый редактор для Chrome [2].

Создание простого Chrome приложения

Полный код редактора доступен на гитхабе [3]. Готовый редактор можно установить из магазина приложений Chrome [4].

Подготовка

Для тестирования приложения, которое вы разрабатываете, необходимо будет добавить его в свой браузер. Для этого на странице chrome://extensions нужно отметить чекбокс «Режим разработчика» («Developer mode»). После этого станет возможным добавить ваше расширение или приложение.

manifest.json

Код любого приложения для Chrome, как и любого расширения, начинается с файла manifest.json [5]. В нём описывается вся мета-информация приложения. Приведу целиком манифест редактора [6]:

{
  "name": "Simple Text",
  "description": "An extremely simple text editor (sample Chrome app)",
  "version": "0.1",
  "icons": {
    "48": "icon/48.png",
    "128": "icon/128.png"
  },
  "manifest_version": 2,
  "minimum_chrome_version": "31.0",
  "offline_enabled": true,
  "app": {
    "background": {
      "scripts": ["js/background.js"]
    }
  },
  "permissions": [
    {"fileSystem": ["write"]}
  ],
  "file_handlers": {
    "text": {
      "title": "Simple Text",
      "types": ["application/javascript",
                "application/json",
                "application/xml",
                "text/*"],
      "extensions": ["c", "cc", "cpp", "css", "h", "hs", "html", "js", "json", "md", "py", "textile", "txt", "xml", "yaml"]
    }
  }
}

Разберём поля, которые тут встретились. С названием и описанием всё ясно. Версия является обязательным полем — Chrome Web Store будет требовать, чтобы она менялась, когда вы загружаете обновление вашего приложения.

Стандарные размеры иконок [7], требующихся для приложения — 48×48 и 128×128 пикселов. Также в некоторых случаях используется иконка размера 16×16. Кроме этого, другие размеры иконки могут потребоваться в случаях, когда она будет показываться на дисплеях высокого разрешения, как на Chromebook Pixel и новых MacbookPro.

"manifest_version" — версия формата файла manifest. В данный момент следует использовать значение 2.

"offline_enabled" — как можно ожидать, данный флаг установлен для приложений, работающих вне зависимости от наличия доступа к интернету.

Следующая конструкция — главная в файле:

  "app": {
    "background": {
      "scripts": ["js/background.js"]
    }
  },

Тут браузеру сообщается, как запускать приложение. В отличие от расширений, для которых background page является необязательным атрибутом, в приложении он всегда есть. Логика работы такова: при запуске приложения сначала загружается код background page. Он может регистрировать обработчики тех или иных событий, в частности, события onLaunched, который затем стартует, когда пользователь тем или иным способом открывает приложение.

В разделе "permissions" описываются настройки доступа для приложения. В нашем случае добавлена возможность сохранять файлы.

Наконец, в разделе "file_handlers" описаны типы файлов, открываемых приложением. Для разных типов файлов в файловом менеджере Chrome OS могут показывать разные строчки в меню. Например, для одних файлов пункт в меню может выглядеть «Смотреть изображение в СуперПрограмме», а для других — «Редактировать текст в СуперПрограмме».

Назначение Chrome-приложения программой для открытия того или иного типа файлов работает пока только в Chrome OS.

Background page

Весь код, реализующий background page находится в файле js/background.js [8]. Вот он:

var entryToLoad = null;

function init(launchData) {
  var fileEntry = null
  if (launchData && launchData['items'] && launchData['items'].length > 0) {
    entryToLoad = launchData['items'][0]['entry']
  }

  var options = {
    frame: 'chrome',
    minWidth: 400,
    minHeight: 400,
    width: 700,
    height: 700
  };

  chrome.app.window.create('index.html', options);
}

chrome.app.runtime.onLaunched.addListener(init);

Background page работает в фоновом режиме независимо от окон приложения. Большую часть времени он не загружен в память. При запуске системы его код исполняется и может установить обработчики тех или иных событий, самое распространённое из которых — onLaunched. Когда обработчики установлены, background page, как правило, выгружается из памяти и запускается обратно только если произошло одно из событий, на которые он подписан.

Когда пользователь кликает на иконку приложения, или открывает в нём какой-то файл, в background page запускается событие onLaunched [9]. В него передаются параметры вызова, в частности, файл(ы), которые приложение должно открыть. Код entryToLoad = launchData['items'][0]['entry'] сохраняет переданный в приложение файл в локальной переменной, откуда его потом возьмёт код редактора. Событие onLaunched может прийти и тогда, когда приложение уже открыто. В этом случае код в background page может сам решить, открывать ли новое окно, или совершить какие-то действия в уже открытом окне.

Метод chrome.app.window.create [10] создаёт новое окно приложения. Первый параметр — путь к открываемому в нём html-файлу (относительно директории приложения). Второй — параметры окна. Остановлюсь на одном из них. frame: 'chrome' создаёт окно с обычным для текущей операционной системы оформлением. Другой вариант здесь — frame: 'none'. В этом случае приложение запускается в «голом» окне, и разработчик должен будет сам позаботиться о добавлении кнопок для закрытия, свёртывания и развёртывания окна, а также области, за которую окно можно будет таскать по экрану.

index.html

В HTML и CSS файлах, входящих в состав приложений Chrome, нет ничего специфического. Единственная особенность, которую можно отметить — это отсутствие необходимости заботиться о межбраузерной соместимости.

<!DOCTYPE html>
<html>
<head>
  <title>Simple Text</title>
  <link href="main.css" rel="stylesheet">
  <script src="js/jquery-2.1.0.min.js" type="text/javascript"></script>
  <script src="js/main.js" type="text/javascript"></script>
</head>
<body>
  <header>
    <button id="open">Open</button>
    <button id="save">Save</button>
    <button id="saveas">Save as</button>
  </header>
  <textarea></textarea>
</body>
</html>

Мы воспользуемся jQuery [11], чтобы немного упростить код. Для редактирования мы будем использовать поле <textarea>. В настоящем редакторе вместо это будет использоваться более интеллектуальный модуль редактирования. Наиболее распространённые варианты: CodeMirror [12] и Ace [13].

Для полноты картины приведу CSS:

body {
  margin: 0;
}

header {
  background-color: #CCC;
  border-bottom: 1px solid #777;
  -webkit-box-align: center;
  -webkit-box-orient: horizontal;
  -webkit-box-pack: left;
  display: -webkit-box;
  height: 48px;
  padding: 0px 12px 0px 12px;
}

button {
  margin: 8px;
}

textarea {
  border: none;
  -webkit-box-sizing: border-box;
  font-family: monospace;
  padding: 4px;
  position: absolute;
  top: 48px;
  bottom: 0px;
  left: 0px;
  right: 0px;
  width: 100%;
}

textarea:focus {
  outline: none !important;
}

Основной код: работа с файлами

Так как в нашем примере мы для простоты ограничимся минимальным набором возможностей, то основной код редактора будет посвящён почти исключительно работе с файлами. Для этого используется несколько API, часть из которых уже находится на пути к стандартизации W3C. File API и сопутствующие интерфейсы — большая тема, заслуживающая отдельной статьи. В качестве хорошего введения рекомендую эту статью на html5rocks.com [14].

Итак, разберём код в js/main.js. Я буду приводить его фрагментами, полный код — на Гитхабе [15].

function init(entry) {
  $('#open').click(open);
  $('#save').click(save);
  $('#saveas').click(saveAs);
  chrome.runtime.getBackgroundPage(function(bg) {
    if (bg.entryToLoad)
      loadEntry(bg.entryToLoad);
  });
}

$(document).ready(init);

Задача функции инициализации — добавить обработчики к кнопкам и получить из background page файл для открытия. Контекст background page получается из основного окна асинхронно с помощью chrome.runtime.getBackgroundPage [16].

Обработчики нажатий на кнопки:

var currentEntry = null;

function open() {
  chrome.fileSystem.chooseEntry({'type': 'openWritableFile'}, loadEntry);
}

function save() {
  if (currentEntry) {
    saveToEntry(currentEntry);
  } else {
    saveAs();
  }
}

function saveAs() {
  chrome.fileSystem.chooseEntry({'type': 'saveFile'}, saveToEntry);
}

Текущий FileEntry мы будем хранить в глобальной переменной currentEntry.

Единственная специфичная особенность в приведённом выше коде — это метод chrome.fileSystem.chooseEntry [17]. С помощью этого метода открывается окно выбора файлов (своё на каждой системе). Как и все прочие функции для работы с файловой системой, этот метод асинхронный и получает callback для продолжения работы (в нашем случае функции loadEntry и saveToEntry, описанные ниже).

Чтение файла:

function setTitle() {
  chrome.fileSystem.getDisplayPath(
      currentEntry,
      function(path) {
        document.title = path + ' - Simple Text';
      });
}

function loadEntry(entry) {
  currentEntry = entry;
  setTitle();
  entry.file(readFile);
}

function readFile(file) {
  var reader = new FileReader();
  reader.onloadend = function(e) {
    $('textarea').val(this.result);
  };
  reader.readAsText(file);
}

В функции setTitle() мы меняем заголовок окна, чтобы показать путь к текущему файлу. То, как будет отображаться этот заголовок, зависит от системы. На Chrome OS он вообще не показывается. chrome.fileSystem.getDisplayPath [18] — наиболее корректный способ получить путь файлу, подходящий, чтобы показывать его пользователю. Другое представление пути доступно через entry.fullPath.

В File API есть два различных объекта, описывающих файл: FileEntry и File. Грубо говоря, FileEntry олицетворяет путь к файлу, а File — данные, в нём содержащиеся. Следовательно, для того, чтобы прочитать файл, необходимо по Entry получить объект File. Это достигается с помощью асинхронного метода entry.file().

FileReader [19] — отдельный объект, предназначеный для чтения файлов. Он позволяет достаточно гибко управлять процессом чтения, но нам от него в данном случае нужно просто прочесть всё содержимое файла.

Запись файла, как и чтение, не содержит специфичного для Chrome кода:

function saveToEntry(entry) {
  currentEntry = entry;
  setTitle();

  var blob = new Blob([$('textarea').val()], {type: 'text/plain'});
  entry.createWriter(function(writer) {
    writer.onwrite = function() {
      writer.onwrite = null;
      writer.write(blob);
    }
    writer.truncate(blob.size);
  });
}

Прежде чем писать данные, их необходимо привести к виду Blob [20]. Один дополнительный шаг, который понабится нам при записи — это обрезание файла на случай, если он уже существует и имеет большую длину. Если бы мы были точно уверены, что это новый файл, код записи упростился бы до:

  entry.createWriter(function(writer) {
      writer.write(blob);
  });

Заключение

На этом код нашего приложения закончен. К сожалению, управление файлами в JavaScript устроено несколько неинтуитивно, и, вероятно, является наиболее сложной частью приложения. Но, как я уже писал выше, эти API не специфичны для Chrome, а реализованы во всех современных браузерах [21]

Код этого примера сделан максимально коротким, чтобы уместить его в формат статьи. Если вы хотите посмотреть на более развёрнутые примеры того, как используются те или иные возможности Chrome API, на Гитхабе опубликован большой набор примеров Chrome apps [22]. Официальная документация по всем программным интерфейсам — на developer.chrome.com [23]. Основное место, где можно получить ответы на конкретные вопросы по программированию Chrome-приложений — тэг google-chrome-app на StackOverflow [24].

Автор: eterevsky

Источник [25]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/news/54745

Ссылки в тексте:

[1] прошлом топике: http://habrahabr.ru/company/google/blog/211346/

[2] текстовый редактор для Chrome: https://github.com/GoogleChrome/text-app/

[3] доступен на гитхабе: https://github.com/eterevsky/simple-text

[4] установить из магазина приложений Chrome: https://chrome.google.com/webstore/detail/eemaijgmlecljjagojapeeaohllhobke

[5] файла manifest.json: http://developer.chrome.com/apps/manifest.html

[6] манифест редактора: https://github.com/eterevsky/simple-text/blob/master/manifest.json

[7] иконок: http://developer.chrome.com/apps/manifest/icons.html

[8] js/background.js: https://github.com/eterevsky/simple-text/blob/master/js/background.js

[9] onLaunched: https://developer.chrome.com/apps/app_runtime.html#event-onLaunched

[10] chrome.app.window.create: http://developer.chrome.com/apps/app_window.html#method-create

[11] jQuery: http://jquery.com/

[12] CodeMirror: http://codemirror.net/

[13] Ace: http://ace.c9.io/

[14] эту статью на html5rocks.com: http://www.html5rocks.com/en/tutorials/file/filesystem/

[15] на Гитхабе: https://github.com/eterevsky/simple-text/blob/master/js/main.js

[16] chrome.runtime.getBackgroundPage: https://developer.chrome.com/apps/runtime.html#method-getBackgroundPage

[17] chrome.fileSystem.chooseEntry: http://developer.chrome.com/apps/fileSystem.html#method-chooseEntry

[18] chrome.fileSystem.getDisplayPath: http://developer.chrome.com/apps/fileSystem.html#method-getDisplayPath

[19] FileReader: https://developer.mozilla.org/en-US/docs/Web/API/FileReader

[20] Blob: https://developer.mozilla.org/en-US/docs/Web/API/Blob

[21] во всех современных браузерах: http://www.html5rocks.com/en/features/file_access

[22] большой набор примеров Chrome apps: https://github.com/GoogleChrome/chrome-app-samples

[23] на developer.chrome.com: http://developer.chrome.com/apps/about_apps.html

[24] тэг google-chrome-app на StackOverflow: http://stackoverflow.com/tags/google-chrome-app/info

[25] Источник: http://habrahabr.ru/post/212107/