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