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

от автора

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/

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

ссылка на оригинал статьи http://habrahabr.ru/company/surfo/blog/218819/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *