Мобильное приложение HTML5: ошибка или успех. Попытка №0

3 июля 2013 г., 9:06:29
Упомянутые аппараты

За несколько лет, читая новости и события в мире Web разработки, у меня нарисовалась розовая мечта: написал один раз — работает везде и всегда. При этом очень часто встречаю негативные отзывы о разработке мобильных приложений на HTML5 ( тут и комментарии на статьи 1 и 2 ). Основные доводы бастующих: несоответствие родному интерфейсу, глючность и тормознутость, проблемы с хранением данных и тд и тп. Ни в коем случае не хочу запустить очередные холи вары на эту тему. Но мечта живет и ее можно подтвердить или отвергнуть только после собственного наступления на грабли. Итак, цель – написать на HTML5 мобильное приложения для сбора заказов торговым агентом в торговых точках. Я сталкивался с данными решениями разных компаний, поэтому знаком с предметной областью, и эта тема идеально подходит для мечты. К основным требованиям я добавлю несколько заметок из собственного опыта:

  • Программа должна работать на многих устройствах и на разных платформах. Обычно у компаний, особенно больших, уже есть парк мобильный устройств. Некоторые компании-дистрибьюторы даже заставляют использовать собственные телефоны (так сказать добровольно принудительный BYOD).
  • Поддержка офлайн работы. К сожалению интернет покрытие оставляет желать лучшего. Нативные решения хорошо справляются с данной проблемой.
  • Программа должна легко расширяться. Почему-то у поставщиков таких решений возникает проблема нормального обновления версий
  • Использование железа ( камера, GPS).

Маленькая заметка: Статья написана с целью закрепления пройденного материала по изучению новой технологии. В связи с полным отсутствием реального опыта создания приложений такого рода, заранее прошу прощения за возможные огрехи.

Предварительная архитектура: Backend — .net MVC with OData. Глобально не важно, что я буду использовать в этой роли, главное, чтобы соответствовало новым стандартам WEB API. Frontend – тут все сложно для меня. При отсутствии опыта выбрать что-то очень сложно. После некоторого просматривания остановился на PhoneJS. Меня подкупило то, что это полноценный фреймворк для SPA приложения, так что не требуется связывать насколько библиотек в кучу, а также использование knockoutjs. Для работы с данными решил использовать breeze. Уверен, что список будет меняться в процессе разработки. Все это потом запаковать при помощи PhoneGap и получить подобие приложения. В этой статье построим что-то простенькое для начала: просмотр данных торговой точки на определённом маршруте торгового агента.

Создание проекта. Создаем новый проект ASP.NET MVC 4 Web Application и назовем «MSales». В диалоге New ASP.NET MVC 4 Project выбираем шаблон Web API. Обновляем пакеты: Update-Package knockoutjs и Update-Package jQuery, и устанавливаем: Install-Package Breeze.WebApi и Install-Package datajs. К сожалению, для PhoneJS нет пакета, поэтому ручками добавляем все необходимые css и js в проект. На выбор есть нескольто типов layout, я использовал NavbarLayout, поменяв файл _Layout.cshtml:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    @Styles.Render("~/Content/css")
    @Styles.Render("~/Content/dx")
    @Styles.Render("~/Content/layouts")
    @Scripts.Render("~/bundles/modernizr")
</head>
<body>
    @Html.Partial("NavbarLayout")
    @RenderBody()

    @Scripts.Render("~/bundles/jquery")
    @RenderSection("scripts", required: false)
</body>
</html>

В файле BundleConfig прописываем весь контент и скрипты. У меня получилось вот так:

BundleConfig
// Сокращено для упрощения
            bundles.Add(new ScriptBundle("~/bundles/knockout").Include(
           "~/Scripts/knockout-{version}.js"));


            bundles.Add(new ScriptBundle("~/bundles/breeze").Include(
           "~/Scripts/q.js",
           "~/Scripts/datajs-{version}.js",
           "~/Scripts/breeze.debug.js"
           ));
            bundles.Add(new ScriptBundle("~/bundles/dx").Include(
                       "~/Scripts/dx.phonejs.js",
                       "~/Scripts/globalize"
                       ));

            bundles.Add(new ScriptBundle("~/bundles/app").Include(
                       "~/Scripts/App/app.init.js",
                       "~/Scripts/App/app.viewmodel.js",
                       "~/Scripts/App/NavbarLayout.js"
                       ));

            bundles.Add(new StyleBundle("~/Content/dx").Include("~/Content/dx/dx.*"));
            bundles.Add(new StyleBundle("~/Content/layouts").Include("~/Content/layouts/NavbarLayout.css"));

Модель и контролеры В модель на данный момент включим два файла: классы для маршрутов (по этим маршрутам ходит торговый агент) и торговых точек (магазинов):

Модель
public class Route
    {
        public int RouteID { get; set; }
        [Required]
        [StringLength(30)]
        public string RouteName { get; set; }

    }
   public class Customer
    {
        public int CustomerID { get; set; }
        [Required]
        [StringLength(50)]
        public string CustomerName { get; set; }
        [StringLength(150)]
        public string Address { get; set; }
        public string Comment { get; set; }
        [ForeignKey("Route")]
        public int RouteID { get; set; }
        virtual public Route Route { get; set; }
    }

Контроллеры будут очень простые (более детально про OData можно почитать тут ):

Контроллеры
public class RoutesController : EntitySetController<Route, int>
    {
        private MSalesContext db = new MSalesContext();
        public override IQueryable<Route> Get()
        {

            return db.Routes; ;
        }

        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }
    }
    public class CustomersController : EntitySetController<Customer, int>

    {
        private MSalesContext db = new MSalesContext();
        public override IQueryable<Customer> Get()
        {

            return db.Customers; ;
        }
        protected override Customer GetEntityByKey(int key)
        {
            return db.Customers.FirstOrDefault(p => p.CustomerID == key);
        }


        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }

    }
Маленький штрих в файле WebApiConfig:
public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {

            config.Routes.MapODataRoute("odata", "odata", GetEdmModel());
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            config.EnableQuerySupport();

            config.EnableSystemDiagnosticsTracing();
        }
        public static IEdmModel GetEdmModel()
        {
            ODataModelBuilder builder = new ODataConventionModelBuilder();
            builder.EntitySet<Route>("Routes");
            builder.EntitySet<Customer>("Customers");
            builder.Namespace = "MSales.Models";
            return builder.GetEdmModel();
        }
    }

При регистрации маршрута для протокола OData необходимо указать строку builder.Namespace = "MSales.Models";, необходимую для работы библиотек breeze и datajs.

Frontend. В папке Scripts/app создадим файл скрипта app.init.js для инициализации библиотек:

window.MyApp = {};

 $(function () {
    MyApp.app = new DevExpress.framework.html.HtmlApplication({
        namespace: MyApp,

        defaultLayout: "navbar",
        navigation: [
          {
              title: "Routes",
              action: "#route",
              icon: "home"
          },
          {
              title: "About",
              action: "#about",
              icon: "info"
          }
        ]
    });
    MyApp.app.router.register(":view/:id", { view: "route", id: 0 });
    MyApp.app.navigate();
    var serverAddress = "/odata/";
    breeze.config.initializeAdapterInstances({ dataService: "OData" });
    MyApp.manager = new breeze.EntityManager(serverAddress);
});

Создаем HTML приложение, в котором указываем layout и параметры навигации, которая состоит из двух пунктов: маршруты и about; а также инициализируем библиотеку breeze. В файле Index.cshtml необходимо разместить dxView и специальную область с именем “content”, в котором выводится обычный список:

<div data-options="dxView : { name: 'route', title: 'Routes' } " >
  <div class="route-view"  data-options="dxContent : { targetPlaceholder: 'content' } " >
    <div data-bind="dxList: { dataSource: dataSource }">
     <div data-options="dxTemplate : { name: 'item' }"  data-bind="text: RouteName, dxAction: '#customers/{RouteID}'"/>
    </div>
  </div>
</div>

Для того, чтобы эти пару строк заработали необходимо, создать Viewmodel, поэтому в папке Scripts/app создадим файл app.viewmodel.js:

MyApp.route = function (params) {
	var viewModel = {
		dataSource: {
			load: function (loadOptions) {
				if (loadOptions.refresh) {
				    var deferred = new $.Deferred();
				    var query = breeze.EntityQuery.from("Routes").orderBy("RouteID");
				    MyApp.manager.executeQuery(query, function (result) {
				        deferred.resolve(result.results);
					  });
					return deferred;
				}
			}
		}
	}
	return viewModel;
}; 

Хочу обратить внимание что имя Viewmodel совпадает с именем dxView, и содержит только объект dataSource, в которой мы определяем один метод load для загрузки данных. Параметр refresh определяет должны ли данные виджета обновлены полностью. В методе строим запрос, сортируя по полю RouteID и выполняем его. Добавим еще одну View – About:

    <div data-options="dxView : { name: 'about', title: 'About' } ">
        <div data-options="dxContent : { targetPlaceholder: 'content' } ">
            <div data-bind="dxScrollView: {}">
                <p style="padding: 5px">This is my first SPA application.</p>
            </div>
        </div>
    </div>

Результат для iPhone: image
Вы, наверно, обратили внимание, что на элемент списка повешено событие dxAction: '#customers/{RouteID}', где, согласно заданной навигации, '#customers – это вызываемое View, а RouteID – параметр, передаваемый в это View:

<div data-options="dxView : { name: 'customers', title: 'Customers' } " >
<div data-bind="dxCommand: { title: 'Search', placeholder: 'Search...', location: 'create', icon: 'find', action: find }" ></div>
  <div data-options="dxContent : { targetPlaceholder: 'content' } " >
      <div data-bind="dxTextbox: { mode: 'search', value: searchString, visible: showSearch, valueUpdateEvent: 'search change keyup' }"></div>
    <div data-bind="dxList: { dataSource: dataSource }">
      <div data-options="dxTemplate : { name: 'item' } " data-bind="text: name, dxAction: '#customer-details/{id}'"/>
    </div>
  </div>
</div>

В связи с тем, что покупателей может быть много, добавил возможность поиска: добавил dxCommand — кнопка поиска, которая вызывает функцию find, и поле ввода перед списком. Viewmodel:

MyApp.customers = function (params) {
	var skip = 0;
	var PAGE_SIZE = 10;
	var viewModel = {
		routeId: params.id,
		searchString: ko.observable(''),
		showSearch: ko.observable(false),
		find: function () {
			viewModel.showSearch(!viewModel.showSearch());
			viewModel.searchString('');
		},
		dataSource: {
			changed: new $.Callbacks(),
			load: function (loadOptions) {
				if (loadOptions.refresh) {
					skip = 0;
				}
				var deferred = new $.Deferred();
				var query = breeze.EntityQuery.from("Customers")
                    .where("CustomerName", "substringof", viewModel.searchString())
                    .where("RouteID", "eq", viewModel.routeId)
                    .skip(skip)
                    .take(PAGE_SIZE)
                    .orderBy("CustomerID");
				MyApp.manager.executeQuery(query, function (result) {
				    skip += PAGE_SIZE;
				    console.log(result);
				    var mapped = $.map(result.results, function (data) {

				        return {
				            name: data.CustomerName,
				            id: data.CustomerID
				        }
				    });
				  	deferred.resolve(mapped);
				  });
				return deferred;
			}
		}
	};
	ko.computed(function () {
		return viewModel.searchString();
	}).extend({
		throttle: 500
	}).subscribe(function () {
		viewModel.dataSource.changed.fire();
	});
	return viewModel;
};

Переменные skip и PAGE_SIZE необходимы для загрузки части данных (в данном случае 10 записей), а дозагрузка будет идти по мере необходимости. Переменные searchString и showSearch для поиска, при чем поиск срабатывает с пол секундной задержкой после ввода символа. Результат: image
Ну и напоследок, выведем информацию о выбранном покупателе: View:

<div data-options="dxView : { name: 'customer-details', title: 'Product' } " >
   <div data-options="dxContent : { targetPlaceholder: 'content' } " >
    <div class="dx-fieldset">
      <div class="dx-field">
        <div class="dx-field-label">Id: </div>
        <div class="dx-field-value" data-bind="text: id"></div>
      </div>
      <div class="dx-field">
        <div class="dx-field-label">Name: </div>
        <div class="dx-field-value" data-bind="text: name"></div>
      </div>
      <div class="dx-field">
        <div class="dx-field-label">Address: </div>
        <div class="dx-field-value" data-bind="text: address"></div>
      </div>
      <div class="dx-field">
        <div class="dx-field-label">Comment: </div>
        <div class="dx-field-value" data-bind="text: comment"></div>
      </div>
    </div>
  </div>
</div>

ViewModel:

MyApp['customer-details'] = function (params) {
	var viewModel = {
		id: parseInt(params.id),
		name: ko.observable(''),
		address: ko.observable(''),
        comment:ko.observable('')
	};
	var data = MyApp.manager.getEntityByKey("Customer", viewModel.id);
	console.log(data);
	viewModel.name(data.CustomerName());
	viewModel.address(data.Address());
	viewModel.comment(data.Comment());

	
	return viewModel;
};

image
Примечание: скриншоты сделаны с эмулятора Ripple Emulator (Beta).

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



Обзор смартфона Apple IPhone 5s
Обзор смартфона Apple IPhone 5s Цена и наличие: http://rozetka.com.ua/apple_iphone_5s_32gb_space_gray/p331005/ Видеообзор смартфона Apple iPhone 5s Смотреть обзоры других смартфонов: http://www.youtube.com/playlist?list=PLDF13DB9F6AC560F2 Все смартфоны Apple:http://rozetka.com.ua/mobile-phones/c80003/filter/preset=smartfon;producer=apple/ Присоединяйтесь к нам в соцсетях! ВКонтакте: http://vk.com/club20877147 Facebook: http://facebook.com/rozetka.ua Google+: https://plus.google.com/+rozetka/ Twitter: https://twitter.com/rozetka_news Читайте наши новости и советы по выбору! http://rozetka.com.ua/news-articles-promotions/ PT11M4S

Похожие новости

14 февраля 2016 г., 21:06:25 Satan Note 5

Небогатые или просто разумные фанаты iPhone обновляют смартфоны по двум линейкам: номерным (3,4,5,6) и с приставкой «S». Любители флагманов Samsung практикуют похожий подход, но в отношении линеек Galaxy S и Note. Устройства последней выходят в конце года, мощнее и доработаннее флагмана. В 2016 году,...

12 февраля 2016 г., 19:48:03

Согласно докладам многих пользователей из разных стран, изменение даты на 1 января 1970 года может сделать из вашего iPhone, iPad или iPod touch под управлением iOS 9 или выше нерабочий “кирпич”. Баг в мобильной операционной системе позволяет превратить устройство с процессором A7, A8, A8X, A9 и A9X...

9 февраля 2016 г., 13:32:35 Apple выпустила третью бета-версию iOS 9.3 для разработчиков

Apple недавно выпустила третью бета-версию iOS 9.3 для разработчиков. Это, похоже, предпоследняя сборка для девелоперов, выпущенная до предполагаемого дебюта iOS 9.3 вместе с новым 4-дюймовым iPhone 5SE, iPad Air 3 и обновленными часами Apple Watch в марте. Список улучшений и усовершенствований включает...

9 февраля 2016 г., 3:21:58 image

Приветствуем читателей блога iCover! Как сообщает интернет-издание The Verge, уже в ближайшие дни компания Apple планирует запустить новую программу Trade-in в рамках которой владельцы неисправных iPhone получат возможность обменять свои неработающие гаджеты на новые с фиксированной доплатой. Стоимость...

8 февраля 2016 г., 17:17:43 iPhone 5s

Apple все-таки убедилась в том, что экран флагманского смартфона должен быть большим. iPhone 6 и iPhone 6s бьют рекорды продаж, а iPhone 6 Plus стал королем фаблетов. Несмотря на это, кажется, многие все еще хотят использовать компактное устройство. Apple ведь не просто так решила снова выпустить 4-дюймовый...


Похожие обзоры

25 января 2016 г., 12:35:18

Сегодня у нас в гостях Xiaomi Mi Pad 2 – планшет в металлическом корпусе, оснащенный новым процессором Intel Atom X5-Z8500. Внешне он очень похож на Apple iPad mini, обладает таким же дисплеем с высоким разрешением, но при этом стоит дешевле. В обзоре мы подробно расскажем о его возможностях. ЛинейкаXiaomi...

21 января 2016 г., 0:23:00 Обзор смартфона Meizu MX4 Ubuntu Edition

Слухи о том, что китайская компания Meizu планирует выпустить коммуникатор на базе Ubuntu Touch, появились еще в начале прошлого года. В течении 2014 г. компания периодически демонстрирует прототип устройства на выставках и осенью объявляет, что Meizu MX4 на Ubuntu Touch выйдет в декабре, сообщает некоторые...

20 января 2016 г., 19:45:31 Хабраобзор Highscreen Explosion или недорогого аналога Samsung Galaxy 3

Большинство Android-телефонов, с которыми я имел дело, отвечали простому требованию – максимум функциональности за минимум денег. Поэтому, когда появилась возможность пощупать вроде как аналог Samsung Galaxy S3, при этом стоимостью всего 13 тысяч (на самом деле 12990, но это маркетинговые уловки), я...

20 января 2016 г., 18:15:58

Обзор BlackBerry P'9981 Porsche Design и PlayBook BlackBerry PlayBookможно приобрести в магазине iCult: http://icult.ru/list/431/ BlackBerry P'9981 Porsche Design можно приобрести в магазине iCult: http://icult.ru/item/438/223/317/8088/ Обзор BlackBerry...

16 января 2016 г., 21:16:12 Планшет Amazon Fire 7 (2015)

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


Похожие отзывы

7 ноября 2015 г., 20:15:56 Blackberry-Priv-G02

Вчера, BlackBerry открыла продажи нового смартфона BlackBerry Priv, первого смартфона, работающего на базе операционной системы Android. Мы опубликовали перевод обзора от наших коллег из CrackBerry, а теперь, вместе с блогом INSIDE BlackBerry предлагаем вам взглянуть на первые отзывы в прессе о запуске...

12 июня 2015 г., 16:26:05 Apple айфон 5 с отзывы, его характеристики, плюсы и минусы

Apple айфон 5 с отзывы, его характеристики, плюсы и минусы от Артур · Июн 12, 2015 12:12 В данной статье рассмотрим Apple айфон 5 с отзывы, его характеристики, плюсы и минусы. Дизайн, материалы корпуса. Айфон 5 c выглядит красивым и аккуратным. Пластик...

10 июня 2015 г., 19:17:12 otzyivyi-ob-ios-9

В распоряжении пользователей было пару дней, чтобы потестить новую прошивку iOS 9. Кто-то уже сделал это — скачал и установил iOS 9, а кто-то до сих пор сидит в замешательстве и читает отзывы об iOS 9, принимая решение — качать или нет. Вот специально для таких мы и приготовили обзорную статью, в которой...

10 июня 2015 г., 9:55:21

Всего через несколько недель после того как 12-дюймовый MacBook появился в онлайн-магазине Apple в Сети было опубликовано огромное количество неоднозначных отзывов о новом ноутбуке. Одна часть пользователей находилась в восторге от тончайшего корпуса MacBook, вторая жаловалась на недоступность адаптеров...

4 июня 2015 г., 22:11:36 Внешний вид колонок

Apple предлагает пользователям вернуть колонки Beats Pill XL. Компания не только обещает компенсировать их цену, но также и добавит небольшой бонус. Причиной подобного стала обнаруженная неисправность, в связи с которой аккумулятор устройства может перегреваться и, в редких случаях, даже самовозгораться....


Похожие инструкции

12 февраля 2016 г., 14:01:02 ustanovka-datyi-1-yanvarya-1970-goda-prevratit-iphone-v-kirpich

Мобильная платформа iOS имеет ряд недоработок. Среди них есть видимые, а также скрытые. Еще одна уязвимость была обнаружена несколькими пользователями ресурса Reddit. Она касается изменения даты на iOS-устройстве, которое может превратить ваш смартфон в кирпич без возможности восстановления. ВНИМАНИЕ!...

7 января 2016 г., 23:31:54 Cydia

Время ожидания непривязанного джейлбрейка iOS 9.1, к сожалению, затянулось. Хакеры до сих пор не обновили эксплоит под версию прошивки iOS 9.1 и выше. Более того, неизвестно, будет ли вообще анонсирован обновленный инструмент. Сомнений, что последние версии мобильного программного обеспечения можно взломать,...

10 декабря 2015 г., 20:33:06 fgcmVm1OjbygEqwA

Буквально позавчера Apple представила фирменный чехол с аккумулятором для iPhone 6/6S, а «разрушители электроники» из iFixit уже получили в свои руки данный аксессуар и, конечно же, разобрали его. На внутренней подкладке из микрофибры указан модельный номер чехла-батареи (A1585) и технические характеристики...

7 декабря 2015 г., 19:16:16 skachat-kozhanyie-oboi-dlya-iphone-

Сегодня мы предлагаем владельцам iPhone необычную идею — установить обои на свой iPhone, которые будут в точности соответствовать оригинальному кожаному чехлу для смартфона от Apple. Коллекция обоев состоит из шести фонов, которые в точности воспроизводят текстуры и цвета «яблочного» аксессуара. Подборку...

28 октября 2015 г., 17:55:35 Команда Pangu обновила утилиту для джейлбрейка iOS 9-iOS 9.0.2, добавила ряд улучшений стабильности

Команда Pangu выпустила обновление для своей утилиты для джейлбрейка, способной взламывать совместимые iPhone, iPad и iPod touch. Всем тем, кто уже сделал джейл, не нужно повторять эту процедуру снова. Достаточно просто обновить "Pangu 9.0.x Untether" и "Patcyh" в Cydia. По словам разработчиков полезной...

Упомянутые аппараты
vk.com/analogindex_ru
"Analog Index" © 2014 - 2015