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

в 4:50, , рубрики: client-side, cordova, geolocation api, html, javascript, mobile development, phonegap, метки: , , , ,

Вступление

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

Очевидно, что верстать и смотреть в броузере, в окне монитора, затруднительно. Разрешение андроид приложение, скажем 320x480, а какие размеры экрана у вашего монитора? На помощь приходят эмуляторы. Самый простой эмулятор уже есть в вашем броузере! Если вы загрузите сверстанные страницы в 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, а я надеюсь вы уже это сделали, и начнет запускать приложение уже на аппарате.

Автор: vovs

Источник

Поделиться

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