Разработка приложения для Firefox OS на реальном примере

в 13:07, , рубрики: Без рубрики

imageМобильные приложения созданные с помощью веб-технологий понемногу захватывают мир. Но создание таких приложений, под популярные платформы, связанны с кучей проблем — от неизвестных истории багов, зоопарка размеров экрана, до проблем с производительностью, которые не решаются просто переписыванием тонких мест.

Но к счастью, этот топик не будет наполнен обыденной трагедией разработки, подобных приложений. Поскольку сегодня, я покажу на реальном примере, как разрабатывать приложения под Firefox OS, которая поддерживает большую часть современных веб-технологий, и вообще говоря создана для них и благодаря им.

Рецепт

Для приготовления нам понадобиться:
0) Апи какого-нибудь сервиса, в нашем случае это ФотоФания (не сочтите за рекламу).
1) Angular js — в качестве основы
2) buildingfirefoxos.com/building-blocks/ — заготовки ui-блоков
3) jQuery и прочие либы по вкусу

0) Апи

Поскольку хочется показать реальное приложение, то и апи надо использовать реальное. ФотоФания это сайт с помощью которого из своих скучных сэлфи можно создать веселые фоточки. Так что нам, немного, придется работать с изображениями на клиенте (хотя основная работа будет происходить на сервере, разумеется).

1) Angular js

Angular хорош. Конечно у меня есть притенении к нему, с точки зрения производительности на мобильных девайсах, но он позволяет отстранится от рутинного кода, обновлений элементов и прочего, занимаясь только данными. Кроме того, приложение на Angular без особых усилий получается отлично структурированным.

2) Building Blocks

Building Blocks — это готовые элементы дизайна, взятые (частично) из исходников Gaia — UI прослойки Firefox OS. Эти заготовки не используют js, вся логика, которая там есть, реализована через псевдо-селекторы. При этом, весь код написан с помощью html5 элементов, и использует data-* атрибуты для обозначения роли блока.

Еще я заметил важную вещь — код Building Blocks работает быстрее и плавнее чем код, который я написал сам, как бы я не старался. По крайней мере, это касается списков. Возможно определённые элементы ускоряются нативно — не уверен насколько это правда. Так что по возможности пользуйтесь заготовками — они ускоряют разработку, делают приложение похожим на нативное и делают его плавнее.

3) jQuery и прочие

Вообще говоря, я бы с радостью избавился от jQuery, но он является зависимостью плагина для пинч-зума github.com/segdeha/jquery-pan-zoom. Подсадить плагин на Zepto не получилось. К счастью jQuery можно билдить из тех кусков которые вам нужны. Поэтому я беспощадно избавился от SIzzle и прочих ненужных в быту вещей.

Немного о структуре

Структуру приложения каждый использует какую хочет. В моем случае она похожа на

/build/
   /build.js
   /build.css
/scripts/
   /vendors/
   /controllers/
   /services/
   /.../
   /app.js
   /dictionary.js
   /config.js
/styles/
   /helpers/
      /variables.less
      /mixins.less
   /main.less
   /header.less
   /...
/images/
index.html

Скрипты и стили билдятся с помощью grunt в реальном времени. А grunt release уже все минифицирует. Так же я использую grunt-angular-templates который все шаблоны превращает в js код, который так же добавляется в build.js

Первоначальная разметка страницы

<!doctype html>
<html ng-app="PhotoFunia" ng-csp>

<head>
   <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
   <meta charset="UTF-8">
   <title>PhotoFunia</title>
   <link rel="stylesheet" href="build/build.css"/>
</head>

<body role="application">
   <!-- Прелоадер, который показывается при старте. Я не выношу его в отдельный файл, в надежде что так будет быстрее-->
   <section ng-controller="PreloaderController">
      <div id="preloader" ng-hide="preloaderHide">
         <div class="preloader-content">
            <div class="logo"></div>
         </div>
      </div>
   </section>

   <!-- Это наш дроуэр (меню) которое будет выдвигаться слева -->
   <section data-type="sidebar"
            ng-include="VIEWS.DRAWER"></section>

   <!-- Это наш контент. Не знаю почему у него id="drower" так нам сказал building blocks -->
   <section id="drawer" role="region" ng-view
            ng-class="{'menu-opened': drawerOpened}"></section>

   <!-- Это своеобраный toast для FF OS. http://buildingfirefoxos.com/building-blocks/status.html -->
   <section role="status" ng-controller="ToastController">
      <p ng-if="text" ng-bind="text"></p>
   </section>

   <!-- Тут скрывается универсальный код для попапов -->
   <section ng-include="VIEWS.POPUP"></section>

   <script src="build/build.js"></script>
</body>
</html>
Важно

Думаю некоторые заметили ng-csp в элементе. Эта директива включает поддержку Content Security Policy — и это является обязательным, для создания приложения для Firefox OS. Кроме того, нам надо немного пропатчить сам angular!
Ищем в коде angular строчку window.XMLHttpRequest(); и заменяем на window.XMLHttpRequest({mozSystem: true});
А так же нам надо добавить руками в наши стили — вспомогательные стили angular.js. А именно — code.angularjs.org/1.2.16/angular-csp.css

Немного о js

Надо заметить, что я изначально пишу код на coffeescript, но буду вам показывать его javascript аналог. Возможно где-то всплывут неточности — тогда мы вместе их исправим.
Еще я надеюсь, что вы знакомы с angular и js достаточно хорошо, чтобы мне не останавливаться на каждом шагу. Иначе эту статью никто никогда в жизни не дочитает.

Мультиязычность

Наше приложение будет мультиязычным. Поэтому нам требуется словарь, его интерпретатор и какая-нибудь директива или фильтр, чтобы вставлять слова в код. Часто когда речь заходит об мультиязычности с помощью angular — всплывают какие-то фильтры, которые выглядят не очень приятно. Например нам предлагают писать так <div ng-bind="'header_search_title' | l10n"></div> меня это немного корежит — зачем вызывать каждый раз лишнюю функцию и писать такой неудобный код, если можно просто использовать словарь в глобал-скоупе, и обращаться к нему? Например так <div ng-bind="m.header.search.title"></div>. За всё время моей работы, такой способ меня не подводил. Давайте же его и реализуем.

Хочется чтобы он был любой вложенностью, поддерживал любое количество языков. Без проблем!

var DICTIONARY = {
   header: {
      search: {
         title: {
            ru: 'Поиск',
            en: 'Search'
         },
         favorites: {
            ru: 'Избранное',
            en: 'Favorites'
         }
      }
   },
   
   cancel: {
      ru: 'Отмена',
      en: 'Cancel'
   }
};

Структура есть, теперь, чтобы не обращаться напрямую переменной (а не языку), надо написать небольшой парсер.

App.run(['$rootScope', function ($rootScope) {
   var lang = 'ru'; // тут должна быть какая-то логика :)
   
   $rootScope.m = (function() {
      var parse = function(obj, result) {
         result = result || {};
         for (var key in obj) {
            var value = obj[key];
            if (typeof value !== "object")
               return obj[lang];
            result[key] = parse(value);
         }
         return result;
      };
      return parse(DICTIONARY);
   })();
}]);

То есть мы просто рекурсивно доходим, до того момента, когда значение переменной становится не объектом, и возвращаем значение с нужным языком. Все просто, и теперь у нас есть переменная m в рут-скоупе, к которой мы можем смело обращаться.
Кто-то наверняка засомневается в производительности такого решения, но могу вас уверить, что это капля в море. И вообще вам никто не мешает написать директиву, которая не будет вешать ватчер на переменную. Так что все в порядке.

Вернемся к разметке

Давайте доделаем drawer (Меню). Допустим у нас есть уже основа в виде js. Есть контроллер который обслуживает наше меню, и мы уже получили с помощью апи список категорий, которые будем выводить в меню. Тогда код меню получается очень простым.

Как видно, в своей разметке index.html мы уже использовали часть кода из Building Blocks. Сейчас мы возьмем код Drawer'a и немного изменим под себя. buildingfirefoxos.com/building-blocks/drawer.html


<nav ng-controller="DrawerController">
   <div class="empty-space"></div>
   <ul>
      <li>
         <a ng-click="go('/favorites'); closeDrawer()">
            <span class="text" ng-bind="m.menu.favorite"></span>
            <span class="counter" ng-bind="favorite.get().length"></span>
         </a>
      </li>
   </ul>
   <h2 ng-bind="m.category.title"></h2>
   <ul>
      <li ng-repeat="cat in categories">
         <a ng-click="openCategory(cat.key)">
            <span class="text" ng-bind="cat.title"></span>
            <span class="counter" ng-bind="cat.count"></span>
            <span class="new-counter" ng-if="cat.new_count" ng-bind="'+'+cat.new_count"></span>
         </a>
      </li>
   </ul>
</nav>

Как видно у нас два списка в меню, в самом верхнем лежит только одна ссылка на избранное (заметили что мы уже используем нашу переменную m?), там же выводится кол-во избранных фотоэффектов, сбоку. Ниже у нас, собственно, сам список категорий.

И для полноты картины сделаем еще одну страницу — список эффектов, то есть страница категории.
buildingfirefoxos.com/building-blocks/headers.html — тут берем хедеры
buildingfirefoxos.com/building-blocks/lists.html — тут списки
buildingfirefoxos.com/building-blocks/filters.html — тут фильтры (табы)
buildingfirefoxos.com/building-blocks/buttons.html — а тут кнопочи

И получаем, упрощенно, что-то такое:

<!-- Наш хедер, собсвенной персоной -->
<header>
   <menu type="toolbar">
      <!-- Кнопочка для поиска -->
      <a ng-click="go('/search')">
         <span class="icon action-icon search"></span>
      </a>
   </menu>
   <!-- Две кнопки для открытия / закрытия меню (drawer) -->
   <a ng-click="closeDrawer()"><span class="icon icon-menu"></span></a>
   <a ng-click="openDrawer()"><span class="icon icon-menu"></span></a>
   <h1 ng-bind="category.title"></h1>
</header>

<div role="main" data-type="list" id="category">
   <!-- Табы, которые устанавливают сортировку эффектов -->
   <ul role="tablist" data-type="filter" data-items="2">
      <li role="tab" aria-selected="{{ sorting === 'new'}}">
         <a ng-click="setSorting('new')" text="m.category.new"></a>
      </li>
      <li role="tab" aria-selected="{{ sorting === 'popular'}}">
         <a ng-click="setSorting('popular')" text="m.category.popular"></a>
      </li>
   </ul>

   <!-- И наши эффекты собственной персоной -->
   <ul class="effects">
      <li ng-repeat="effect in effects"
          ng-click="openEffect(effect.key)">
         <aside class="pack-end">
            <img ng-src="{{CONFIG.DOMAIN + effect.icon}}">
         </aside>
         <a>
            <p ng-bind="effect.title"></p>
            <p ng-if="effect.labels">
               <span ng-repeat="label in effect.labels"
                     ng-class="label" class="label"></span>
            </p>
         </a>
      </li>
   </ul>

   <!-- Кнопочка для подгрузки эффектов -->
   <div class="load-more" ng-if="isNeedMore && effects.length">
      <button ng-click="showMore()" text="m.category.show_more"></button>
   </div>
</div>

Надеюсь все понятно. А понять надо следущее — мы берем готовые блоки и вырезаем из них самое нужное нам. Ими можно крутить и вертеть как угодно.
Таким же образом делаются все остальные страницы.

Об API Firefox OS

В приложениях Firefox OS версии >= 1.3 input[file] работать не будет, в замен его, нам предлагают пользоваться MozActivity. Что даже упрощает работу.

Вот так выглядит получение картинки:

var pick = new MozActivity({
  name: "pick",
  data: {
    type: ["image/*"],
    nocrop: true
  }
});

pick.onsuccess = function() {
  var blob = pick.result.blob;
  // что-то делаем с блобом
};

Вот так шеринг:

new MozActivity({
  name: "share",
  data: {
    type: "image/*",
    number: 1,
    blobs: [blob]
  }
});

И примерно так — сохранение:

var sdcard = navigator.getDeviceStorage("sdcard");
var request = sdcard.addNamed(blob, name);
request.onsuccess = function() {};
request.onerror = function() {};

После того, как приложение готово, его надо сбилдить и протестировать.

Чтобы билдить — можно использовать эмуляторы.
Я вам предлагаю использовать Менеджер приложений, который встроен по умолчанию в новые версии firefox. developer.mozilla.org/ru/docs/Mozilla/Firefox_OS/Using_the_App_Manager. Тут же можно устанавливать разные версии эмуляторов, подключать девайсы и дебажить! Что очень удобно.

Так же нам нужен манифест приложения. Выглядит и пишется он очень просто.
Приведу пример:

{
   "name": "PhotoFunia",
   "description": "PhotoFunia is the best way to add a spark to your photos, make them special and more original.",
   "launch_path": "/index.html",
   "version" : "1",
   "type": "privileged",
   "developer": {
      "name": "",
      "email" : ""
   },
   "default_locale": "en",
   "icons": {
      "256": "/images/app_icon_256.png"
   },
   "permissions": {
      "device-storage:pictures": {
         "description": "Save result images",
         "access": "readwrite"
      },
      "device-storage:sdcard": {
         "description": "Save result images",
         "access": "readwrite"
      },
      "mobilenetwork": {
         "description": "Check for available connection"
      },
      "systemXHR": {
         "description": "Need for internet connection"
      }
   }
}

Собственно говоря, засовываем все нужные файлы в .zip архив и отправляем в marketplace.firefox.com на проверку. Через пару дней (а может и к вечеру) приложение появится в маркете!

Что это было?

Я хотел донести до вас, что разработка под Firefox OS это очень просто, интересно и весело! Конечно, пока что, на этом денег не заработать. Но можно подумать, мы здесь ради денег собрались. Так что, берите IDE в руки, и создавайте приложения под Firefox OS!

Тем, лучшим из нас, у кого есть девайс на Firefox OS — прошу к столу marketplace.firefox.com/app/photofunia/

Все пожелания, а так же ошибки и недочеты принимаю в личку.

Автор: Evgeny42

Источник

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


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