Мобильная веб-разработка: HTML5 приложение для Android

от автора

Вступление

К счастью, есть более чем один способ написать приложение для мобильного телефона. Можно сделать сайт, упаковать его специальным образом, и вуаля, вот вам и приложение! Именно такой подход предлагает нам проект phonegap.com/ именно об этом методе и пойдет речь в этой статье.

Уверен что ни стоит обсуждать экономическую целесообразность данного подхода. Она на лицо. Да, знаний нужно больше чем у среднестатистического веб разработчика, но все же, это сайт! Это понятно! Это тот же HTML, это тот же броузер, тот же Javascript. Найти разработчика ни так сложно, как скажем “нативного”. А уж если умножить на кроссплатформенность данного решения, так и вообще может показаться что это панацея. Конечно, мы то с вами знаем, что ни какой “пилюли” не существует, но в ряде случае, это действительно best practic


Итак, мое рабочее задание звучало так: Разработать клиентское приложение, под ОС Android. Приложение — игра. Квест. Суть игры заключается в следующем: группа людей, желающих интересно отдохнуть, делятся на команды. Каждой команде дается по смартфону. В смартфоне приложение. Открываем приложение. Приложение соединяется с сервером и оттуда приходят вопросы. Для каждой команды они свои. Вопросы могут выглядеть как обычные вопросы с вариантами ответов, ну скажем Сколько лет городу Санкт-Петербург?, так и вопросы локации. Найдите парадный вход в инженерный замок. Команда двигается, находит вход, нажимает Мы на месте и координаты уходят на сервер. От сервера ответ, верно или нет. Есть также вопросы фотографии. Например Сфотографируйте себя на фоне инженерного замка. В сумме, все ответы оцениваются и в итоге одна из команд выигрывает, набирая больше очков. Вкратце все.

Шаг 1 — протитипы

В общем задание нам понятно. Предположим что техническое задание уже составлено. Что еще? Нужны прототипы. Вот они:

image

Шаг 2 — макеты

Следующий шаг. Нужно их от рисовать. Беремся за работу, получается следующее.

image

Шаг 3 — выбираем фреймворк

По сути, их две:

1. Sencha Touch
http://www.sencha.com/products/touch

2. Jquerymobile
http://jquerymobile.com/

Возьмем Sencha Touch. Фреймворк сделан на подобие ExtJS. Большое количество классов. Компонуем их, настраиваем — получаем приложение. Доступ к HTML элементам есть, но на уровне фреймворка управлять элементами крайне не разумно. Грубо говоря, поменять стандартное визуальное отображение элементов крайне затруднительно. Зато данные от сервера получать в формате JSON одно удовольствие.

И наоборот. Jquerymobile это доступ к элементам, по сути расширенный Jquery. Добавляются теги к элементам. После загрузки фреймворк по этим тегам дополняет элементы стилями и другими элементами. Вот только подружить фрейморк с JSON данными от сервера у меня не получилось. Jquerymobile ждет от сервера html код. Безусловно можно получать JSON и его на стороне клиента преобразовывать в html код, что собственно и делает Sencha. Но это ни есть хорошая практика. Это идет в разрез с идеологией фреймворка. Возникает огромное количество проблем, решить которые крайне сложно.

Стоп. А зачем нам фреймворк? Что первый, что второй, по сути, это, так сказать, готовая элементная база, готовые решения, цель которых помочь вам сделать приложение (сайт) визуально похожим на нативное приложение. А нужно нам это? Нет. А как же PhoneGap? А что он, ему все равно, что вы используете. Ни где ни каких ограничений нет. Ну тогда давайте просто сверстаем приложение, как обычный сайт и дело с концом!

Шаг 4 — верстаем

Сам процесс верстки ни чем ни отличается от стандартного. Есть безусловно нюансы, вот о них и поговорим. Первым таким нюансом являются метатеги.

<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />

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

В отличии от десктоп броузера, броузер мобильного телефона (вероятно ни всех) добавляет рамку к элементам, на которых установлен фокус. Подобная рамка, при наведении фокуса, есть по умолчанию в Google Chrome, в момент когда мы вводим данные в очередное поле . Лечится это аналогично.

input:focus { 	outline: 0 none; }  textarea:focus { 	outline: 0 none; } .Button:focus { 	outline: 0 none; } 

И самый последний нюанс это position:fixed. И это действительно проблема, ибо универсальных решений тут нет. Все упирается в сами мобильные броузеры, они просто не поддерживают, или поддерживают но не полностью, такой функционал. Ни получается закрепить панели управления одним решением для всех случаев. К примеру, jquerymobile, до версии 1.1, в случае если броузер не поддерживает position: fixed, эмулировал скроллирование и динамически менял позицию закреплённых элементов, что в общем то не придавала реалистичности и парой выглядело “ни айс”.

Вот по этой ссылке есть описание мобильных броузеров, которые поддерживают position: fixed
bradfrostweb.com/blog/mobile/fixed-position/
а также есть ссылки на Javascript библиотеки, которые эмулируют работу position: fixed и процесса скроллирования. К сожалению работу ни одного из них удовлетворительной назвать нельзя.

В моем конкретном случае, мобильная платформа была указана как Android 2.3, а она поддерживает position: fixed, но при этом пользовательский zoom работать не будет, что по сути в приложении ни к чему. Указываем в заголовке viewport

<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />

И прописываем стили

 .Header { 	 background-color: white; 	 background-image: none; 	 border: none; 	 text-shadow: none; 	 border-bottom: white solid 3px; 	 font-weight: bold; 	 position: fixed; 	 width: 100%; 	 height: 62px; 	 top: 0; 	 left: 0; 	 z-index: 100  } 

На этом все.

Шаг 5 — эмуляторы

Очевидно, что верстать и смотреть в броузере, в окне монитора, затруднительно. Разрешение андроид приложение, скажем 320×480, а какие размеры экрана у вашего монитора? На помощь приходят эмуляторы. Самый простой эмулятор уже есть в вашем броузере! Если вы загрузите сверстанные страницы в Google Chrome и нажмете Ctrl+Shift+I, броузер покажет вам инструменты разработчика. В правом нижнем углу вы можете найти иконку с шестеренкой, нажимайте на ней. Далее выбираем вкладку Override и вот он, ваш эмулятор. Выбираем User Agent и ставим галочку Device Metric. На первом этапе этого будет достаточно.

image

А еще есть эмулятор от самого PhoneGap! emulate.phonegap.com/
Называется Ripple. Ставится в виде дополнений к Google Chrome. Ура! Наши возможности резко увеличились. В случае, если в своем приложении вы используете библиотеку cordova для расширения функционала приложения, скажем для работы с камерой телефона или компасом, то Ripple даст вам возможность симулировать данные процессы.

Ну и раз пошла речь про эмуляторы, нельзя ни сказать и про эмулятор, который ставиться вместе с Eclipse, если следовать инструкции от Phonegap
docs.phonegap.com/en/2.2.0/guide_getting-started_android_index.md.html#Getting%20Started%20with%20Android
Этот эмулятор уже ведет себя совсем как настоящее устройство. Все ошибки, какие были найдены на этом эмуляторе, все аналогичным образом были найдены и на устройстве. Ну и конечно нужно сказать, что пользоваться этим эмулятором оперативно сложно. Долго грузится, трудно текст набирать и т.д. Подходит он для самой последней стадии. Когда ваше приложение уже работает прекрасно на всех других ранее перечисленных эмуляторах.

Шаг 6 — программируем

Хоть статья и для программистов, размешать весь код тут просто глупо. Опишу в общем. Программирование веб приложение, по сути, ни отличается от программирование небольшого сайта. Тут те же методы и подходы, но выполнены на Javascript. Тот же MVC, те же паттерны: синглетон, компановщик и т.д.

Вот фронт контроллер

var App = { 	Init: function() { 		this.model = new Model(this.url); 		this.view = new View(); 		this.controller = new Controller({ 			model: this.model, 			view: this.view 		}); 		return this; 	}, 	Run: function(task, params) { 		if (typeof task == 'undefined') { 			this.controller.Login(); 		} else if (typeof this.controller[task] == 'undefined') { 			this.controller.Login(); 		} else { 			this.controller[task](params); 		} 		return this; 	}, 	Done: function() { 		return this; 	} } $(document).ready(function() {	     	App.Init(); 	App.Run();	 	App.Done(); });  

* В javascript нет магических методов. Если скажем в PHP мы можем использовать __call, и вызывать App.SomeSome(‘<параметры>’), то тут нужно будем писать App.Run(‘SomeSome’, ‘<параметры>’)

Вот пример контроллера:

var Controller = function(params) { 	this.view = params.view; 	this.model = params.model; } Controller.prototype = { 	Login: function() {		 		this.view.Login(); 	}, 	LoginSubmit: function() { 		var that = this, 			value = this.model.GetLoginFormValue(), 			errors = this.model.GetLoginFormErrors();  		if (errors !== false) { 			this.view.Login(value, errors); 		} else { 			this.model.SendToServer('teamLogin', value, function(err, data) { 				if (!err) {                     					that.model.SetTeam(data); 					that.model.ListenServer(data.lastMessageId); 					that.Welcome(); 				} else { 					that.view.ShowPopup('error', data) 				} 			});			 		} 	}, 	Welcome: function() { 		var that = this; 		 		this.model.GetWelcomeContent(function(err, data) { 			if (!err) { 				that.view.Welcome(data); 			} else { 				that.view.ShowPopup('error', data); 			}			 		}); 	} 

Вот небольшой пример модели

var Model = function(url) { 	this.url = url; } Model.prototype = { 	GetHelpChat: function(callback) { 		var url = 'helpChat?team='+this.team.teamId+'&hash='+this.team.hash; 		 		this.ReciveFromServer(url, function(err, data) { 			if (err) { 				callback(true, data); 			} else { 				callback(false, data); 			}			 		});		 	}, 

Вот пример представления

var View = function() { 	this.page = $('.Page'); } View.prototype = { 	TaskIndex: function(status, time, tasks) { 		var num = Util.GetRndNumber(); 		 		this.Show( 			Html.Header( 				Html.IconPanel(status), 				Html.TimePanel(time) 			), 			Html.Content( 				Html.TaskPanel(tasks) 			), 			Html.Footer( 				Html.ButtonPanelBottom('task') 			) 		);         setInterval(Timer.Total, 1000);         setInterval(Timer.Current, 1000);             	        Util.SetScrollToTop();		 	}, 

По сути, тут тоже самое, что и в случае, если бы сайт писался на PHP. За исключением фундаментального принципа, Javascript — асинхронный язык и без callback тут ни как (если не использовать специальные библиотеки конечно же)

Отдельно хочется остановится на нюансах, а именно работа с фотокамерой смартфона. Из коробки javascript не умеет этого делать. На помощь приходит библиотека Cordova, которую предлагает подключить PhoneGap. А вот ссылка, на описание работы с камерой телефона

http://docs.phonegap.com/en/2.2.0/cordova_camera_camera.md.html#Camera

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

navigator.camera.getPicture(OnSuccess, OnFail, { 	quality: 75,             allowEdit: true,             targetWidth: 280,             targetHeight: 280, 	destinationType: destinationType.DATA_URL }); 

Но и это оказалось еще не все. Метод getPicture возращает base64 закодированную картинку, а вот данные между сервером и клиентом передаются в виде запросов JSONP.
Очевидно что передать такое количество данных через GET запрос невозможно. Серверная часть, кстати, не помню говорил я или нет, на PHP. Да, не самое лучшее решение, про WebSocket можно забыть. Проксирование тоже не сделать. Вероятно, решение данной проблемы была одна из самых сложных. А решение нашлось следующее. Время идет и стандартные классы расширяются, добавляются новые методы. Так вот класс XMLHttpRequest обзавелся новыми событиями. Кроме стандартного onreadystatechange появилось также событие onload. Если обработчик ответа от сервера “повешать” на него, и в заголовке Content-Type указать application/x-form-urlencoded, то броузер будет делать кроссдоменный запрос методом POST, что, собственно нам и нужно. Вот пример

var xhr = new XMLHttpRequest();             xhr.open('POST', url, true);             xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');             xhr.onload = function(e) {                 if (this.readyState == 4) {                     if (this.status == 200) {                         var r = JSON.parse(this.responseText);                         if (r.success) {                             callback(false, r.data);                         } else {                             callback(true, r.message);                         }                     } else {                         that.view.ShowPopupWindow('Error', msg.ERROR_CONNECTION);                     }                 }             } 

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

Столкнулся я также и с проблемой Same Origin Policy. Решение этой проблемы лежит на серверной стороне. В конфигурационных файлах прописывается разрешение на кросс доменный запрос и дело с концом.

Пробовал я также и FormData API
developer.mozilla.org/en-US/docs/Web/API/FormData?redirectlocale=en-US&redirectslug=Web%2FAPI%2FXMLHttpRequest%2FFormData
Но, к сожалению, этот API, броузер мобильного телефона не поддерживает.

Хочется также отметить, что в случае, если вам не нужны расширенные функции работы с телефоном: акселерометр, компас, камера, медиа и т.д. подключать библиотеку cordova не обязательно (а это примерно 300 килобайт). Геолокация, кстати, доступна и без нее.

Шаг 7 — отлаживаем

Вот наше приложение готово. Сверстано и прекрасно работает на эмуляторе Ripple (см. раздел про эмуляторы). Начинается самое интересное, а именно отладка на телефоне. Но сначала, попробуем запустить приложение на эмуляторе, в eclipse. Перед каждым запуском приложения на эмуляторе, система просит отчистить проект. Project -> Clean. Не забываем это делать. Нажимаем Run — поехали!

После загрузки эмулятора, в панели LogCat Eclipse будет огромное количество сообщений. Первым вопрос который возникает — какие наши? Для того, чтобы видеть только свои ошибки, и в частности, видеть сообщения которые приложение выводит в консоль console.log, нужно настроить фильтр. В панели LogCat, слева, есть отдельный блок, Saved Filters. Открыв ее, вы конечно увидите пустой список, ибо фильтров у нас пока нет. Нажимаем на плюсик и видим окно

image

Вводим в Log Tag web console, как на картинке и теперь Log консоль будет показывать сообщения от вашего веб приложения.

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

image

JSCallback Error: Request failed with status 0 at :1180915830

Начинаем изучать ошибку. Очевидно что ошибка вызывается в момент получения данных с сервером. Ошибка говорит что приходит статус 0. Начинаем искать решение в Google, и вот что находим
simonmacdonald.blogspot.ru/2011/12/on-third-day-of-phonegapping-getting.html
stackoverflow.com/questions/11230685/phonegap-android-status-0-returned-from-webservice

Делаем вывод: вероятно нужно добавить статус 0, как верный статус, для продолжения обработки ответа сервера. Ищем, где же это сообщения JSCallback и находим его в файле cordova.js на строке 3740 (cordova-2.1.0.js)

function startXhr() {     // cordova/exec depends on this module, so we can't require cordova/exec on the module level.     var exec = require('cordova/exec'),     xmlhttp = new XMLHttpRequest();      // Callback function when XMLHttpRequest is ready     xmlhttp.onreadystatechange=function(){         if (!xmlhttp) {             return;         }         if (xmlhttp.readyState === 4){             // If callback has JavaScript statement to execute             if (xmlhttp.status === 200) {                  // Need to url decode the response                 var msg = decodeURIComponent(xmlhttp.responseText);                 setTimeout(function() {                     try {                         var t = eval(msg);                     }                     catch (e) {                         // If we're getting an error here, seeing the message will help in debugging                         console.log("JSCallback: Message from Server: " + msg);                         console.log("JSCallback Error: "+e);                     }                 }, 1);                 setTimeout(startXhr, 1);             }              // If callback ping (used to keep XHR request from timing out)             else if (xmlhttp.status === 404) {                 setTimeout(startXhr, 10);             }              // 0 == Page is unloading.             // 400 == Bad request.             // 403 == invalid token.             // 503 == server stopped.             else {                 console.log("JSCallback Error: Request failed with status " + xmlhttp.status);                 exec.setNativeToJsBridgeMode(exec.nativeToJsModes.POLLING);             }         }     }; 

Пробуем заменить if (xmlhttp.status === 200) на if (xmlhttp.status === 200 || xmlhttp.status === 0) и вуаля — ни какого эффекта!

Дальше не буду рассказывать как я потратил целый день, кружа вокруг этой ошибки. Скажу только, что был готов отчается, ибо ни что не могло мне помочь. Приложение все равно падало, пока я просто не решил закомментировать часть кода. И о чудо! Ошибка исчезла! Возвращая, по частям, свой код, я нашел его часть, которая приводила к ошибке.

var Util = { 	SetNewHash: function(hash) { 		/** 		 * Это не работает в Android 2.3!! 		 */ 		//location.href = 'http://'+location.host+location.pathname+'#'+hash;		 	}, 

Почему смена Хеша, приводила к такой ошибке, для меня осталось загадкой. Если у кого какие будут мысли на этот счет — велком.

Шаг 8 — запускаем

Чтобы запустить приложение уже не посредственно на телефоне, достаточно войти в решим настройки, выбрать раздел Разработка и там взвести галочку напротив пункта Отладка USB. Далее, нажимая RUN в eclipse, среда определит что у вас подключен телефон к USB, а я надеюсь вы уже это сделали, и начнет запускать приложение уже на аппарате.

ссылка на оригинал статьи http://habrahabr.ru/post/183458/


Комментарии

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

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