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

в 10:44, , рубрики: Без рубрики

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

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

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

Подготовка

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

manifest.json

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

{
  "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 будет требовать, чтобы она менялась, когда вы загружаете обновление вашего приложения.

Стандарные размеры иконок, требующихся для приложения — 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. Вот он:

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

Метод chrome.app.window.create создаёт новое окно приложения. Первый параметр — путь к открываемому в нём 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, чтобы немного упростить код. Для редактирования мы будем использовать поле <textarea>. В настоящем редакторе вместо это будет использоваться более интеллектуальный модуль редактирования. Наиболее распространённые варианты: CodeMirror и Ace.

Для полноты картины приведу 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.

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

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.

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

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. С помощью этого метода открывается окно выбора файлов (своё на каждой системе). Как и все прочие функции для работы с файловой системой, этот метод асинхронный и получает 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 — наиболее корректный способ получить путь файлу, подходящий, чтобы показывать его пользователю. Другое представление пути доступно через entry.fullPath.

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

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

Запись файла, как и чтение, не содержит специфичного для 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. Один дополнительный шаг, который понабится нам при записи — это обрезание файла на случай, если он уже существует и имеет большую длину. Если бы мы были точно уверены, что это новый файл, код записи упростился бы до:

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

Заключение

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

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

Автор: eterevsky

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js