Pull to refresh

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

Reading time8 min
Views4.4K
Продолжаю развивать воплощение моей мечты о разработке приложений на HTML5 для мобильных платформ: написал один раз — работает везде и всегда. В прошлой статье я получил не совсем мобильное приложение, скорее мобильный сайт, так как не была предусмотрена работа без сети. Постараюсь это исправить, а так же каждый (почти) сможет попробовать как все это работает на своих личных устройствах.

Итак, мы получим небольшое мобильное веб приложение, которое сможем запускать под несколькими мобильными платформами, кликнув при этом только несколько кнопок мыши.

image


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


архитектура


Реализуем что-нибудь простое:
  • маршруты торгового агента (человек, который ходит по магазинам и заказывает товар). Маршрут — день недели.
  • список торговых точек(магазинов), привязанных к маршруту.
  • заказы торговых точек


Получим с сервера необходимые данные и сохраним локально.
Серверная часть — отдельное приложение .net MVC with OData. В этой статье рассматривать реализацию не вижу смысла, так как сервер просто должен отдавать данные по протоколу OData.
Клиентская часть — phonegap приложение с библиотеками: PhoneJS и BreezeJS.

Итак, приступим…

для ускорения будем использовать шаблон приложения PhoneJS с такой структурой:
  • css
  • js
  • layouts
  • views
  • index.css
  • index.html
  • index.js

Вроде все понятно что-для чего. В файл index.html добавляем ссылки на необходимые библиотеки для BreezeJS.

Запуск приложения


В файле index.js создается само приложение, описывается навигация и другие сервисные функции. PhoneJS предлагает нам несколько layouts. я выбрал slideout. Итак создадим приложение:
 $(function () {
        app = ms.app = new DevExpress.framework.html.HtmlApplication(APP_SETTINGS);
        app.router.register(":view/:item", { view: "Home", item: undefined });
        ms.app.viewShown.add(onViewShown);
        ms.app.navigationManager.navigating.add(onNavigate);
        startApp(!ms.dataservice.initUserData());
        setTimeout(function () {
            document.addEventListener("deviceready", onDeviceReady, false);
            console.log("delay");
            if (device.platform == "tizen") {
                document.addEventListener("tizenhwkey", function (e) {
                    if (e.keyName === "back")
                        onBackButton();
                });
            }
        }, 1000);
    });


APP_SETTINGS — парамерты приложения, в котором указываем навигацию и Layout:
Скрытый текст
APP_SETTINGS = {
        namespace: ms,
        navigationType: "slideout",
        navigation: [
            {
                "id": "Home",
                "title": "Home",
                "action": "#Home",
                "icon": "home"
            },
             {
                 "id": "Settings",
                 "title": "Settings",
                 "action": "#Settings",
                 "icon": "card"
             },
            {
                "id": "About",
                "title": "About",
                "action": "#about",
                "icon": "info"
            }
        ]
    };

Сначала настраиваю “роутинг” и вид(view) по-умолчанию, после чего запускаем приложение, при чем проверяется есть ли уже данные в локальном хранилище функцией ms.dataservice.initUserData()(рассмотрим чуть позже). Функция возвращает истину в случае наличия данных. Т.е. если запускаем первый раз, то переходим на вид Settings для получения данных.
 function startApp(needToSynchronize) {
        if (needToSynchronize)
            ms.app.navigate("Settings/1");
        else
            ms.app.navigate();
    }

Также перед запуском приложения ввожу небольшую задержку — в будущем сделаю заставку.

Работа с данными


Для работы с данными создадим отдельный файл dataservice.js:
Скрытый текст
MobileSales.dataservice =function ($, DX, app, undefined) {
    var DATA_VERSION_KEY = "mobilesales-version",
    DATA_KEY = "mobilesales-data",
    logger = app.logger;
    serviceName = "http://mobsalessrv.azurewebsites.net/odata/";
    breeze.config.initializeAdapterInstances({ dataService: "OData" });
    var manager = new breeze.EntityManager(serviceName);
    var store = manager.metadataStore;

    var queries = {
       Routes: {
            name: "Routes",
            query: breeze.EntityQuery.from("Routes").orderBy("RouteID"),
        },
       Customers: {
            name: "Customers",
            query: breeze.EntityQuery.from("Customers").orderBy("CustomerName"),
         },
       ProductTypes: {
           name: "ProductTypes",
           query: breeze.EntityQuery.from("ProductTypes").orderBy("ProductTypeName"),
       },
       Products: {
           name: "Products",
           query: breeze.EntityQuery.from("Products").orderBy("ProductName"),
       },
       Orders: {
           name: "Orders",
           query: breeze.EntityQuery.from("Orders").orderBy("Date"),
       },
       OrderDetails: {
           name: "OrderDetails",
           query: breeze.EntityQuery.from("OrderDetails"),
       },
    };
    
    function initUserData() {
        var dataFromStorage = localStorage.getItem(DATA_KEY);
        if (dataFromStorage) {
            manager.importEntities(dataFromStorage);
            return true;
        } else {
            return false;
        }
    }

    
    function loadData(query) {
        return manager.executeQuery(query);
    }
    function  getRoutes(){
        return manager.executeQueryLocally(queries.Routes.query);
    };
    function getCustomers() {
        return manager.executeQueryLocally(queries.Customers.query);
    };
    function getProduct(productID) {
        var query = queries.Products.query.where("ProductID", "==", productID);
        return manager.executeQueryLocally(query)[0];
    };

    function getOrders(customerID) {
        var query = queries.Orders.query;
    
        if (typeof customerID != "undefined" && customerID > 0)
            query= query.where("CustomerID", "==", customerID);

        return manager.executeQueryLocally(query);
    };
    function getOrderDetails(orderID) {
        var query = queries.OrderDetails.query;
        if (typeof orderID != "undefined" && orderID > 0)
            query = query.where("OrderID", "==", orderID);
        var result = manager.executeQueryLocally(query);
        result.forEach(function (item) {
                item.ProductName = getProduct(item.ProductID()).ProductName;
        });
        return result;
    };

    function saveDataLocally() {
        var exportData = manager.exportEntities();
        localStorage.setItem(DATA_KEY, exportData);
    }
    var dataservice =  {
        manager: manager,
        metadataStore: manager.metadataStore,
        initUserData: initUserData,
        queries: queries,
        loadData: loadData,
        getRoutes: getRoutes,
        getCustomers: getCustomers,
        saveDataLocally: saveDataLocally,
        getOrders: getOrders,
        getOrderDetails: getOrderDetails,
    };
    return dataservice;
}(jQuery, DevExpress, MobileSales);

Как работать с breeze читайте в прошлой статье. Остановлюсь только на нескольких моментах. Объект queries содержит список запросов для получения данных, планирую его расширить в будущем различными параметрами, а также вообще получать данный список с сервера, что позволить управлять динамически наполнением данных.

У breeze есть замечательная возможность сохранять и восстанавливать все данные локально используя manager.exportEntities() и manager.importEntities(dataFromStorage) соответственно. Функция initUserData проверяет наличие данных в локальном хранилище по ключу DATA_KEY и достает их. Все остальные функции вида getXXX выборка необходимых сущностей локально и используются в нужных видах.

Для загрузки всех данных с сервера используем отдельный вид settings, в котором есть кнопка для закачки данных, панель загрузки и список сущностей:
<div data-options="dxView : { name: 'Settings', title: 'Settings' } ">
    <div data-options="dxContent : { targetPlaceholder: 'content' } ">
        <div class="actions">
            <div data-bind="dxButton: { text: 'Synchronize', clickAction: synchData }"></div>
        </div>
        <div data-bind="dxLoadPanel: { message: message, visible: loading().length>0 }"></div>
        <div data-bind="dxList: { items: entityList }">
            <div data-options="dxTemplate : { name: 'item' }">
                <div data-bind="text: $data.name" class="entity-name"> </div>
                <div data-bind="text: $data.status" class="entity-status"></div>

            </div>
        </div>
    </div>
</div>

Совсем забыл рассказать, что и breeze и Phonejs используют замечательную библиотеку knockout, по-этому очень удобно все это скомпоновать в кучу.
settings.js
MobileSales.Settings = function (params) {
    var app = MobileSales,
        needToSynchonize = params.item==="1",
        self = this;

    var vm = {
        entityList: ko.observableArray([]),
        loading: ko.observableArray(),
        viewShowing: function () {
            if (needToSynchonize)
                getEntities();
        },
        viewShown: function () {
            $(".dx-active-view .dx-scrollable").data("dxScrollView").scrollTo(0);
        },
        synchData: getEntities,

    };

    vm.message = ko.computed(function () {
        return "Loading ...(left:" + this.loading().length + ")"
    }, vm);

    function getEntities() {
        var mapped = $.map(app.dataservice.queries, function (item) {
            item.status = ko.observable("Loading");
            vm.loading.push(true);
            app.dataservice.loadData(item.query).then(function (data) {
                app.logger.log("Loaded data: " + item.query.resourceName);
                item.status("Succeded");
                app.logger.log(app.dataservice.getRoutes());
                vm.loading.pop();
                if (vm.loading().length === 0)
                    app.dataservice.saveDataLocally();
            }).fail(function (error) {
                item.status("Error");
                app.logger.error("Error Loading data");
                app.logger.log(error);
                vm.loading.pop();
            });
            return item;
        });
        vm.entityList(mapped);
    };
    return vm;
};

На вход принимаем параметр, который передаем в случае принудительной синхронизации. Все запросы к серверу breeze выполняет асинхронно, что позволяет сделать элегантное решение закачки сущностей в entityList. Для этого нам понадобится всего лишь один наблюдаемый массив (observableArray) loading.
Разберем функцию getEntities: проходим по всем элементам dataservice.queries, добавляя к каждому поле status и добавляем любое значение в массив vm.loading.push(true). В обработчиках успешного выполнения или выдачи ошибки обновляем статус элемента и уменьшаем массив loading. В итоге мы очень просто получаем асинхронную загрузку нескольких сущностей с контролем завершения. Вот так это все выглядит.
image

Остальное в программе не очень интересно, просто отображает закачанные в хранилище данные, и дублирует прошлую статью. Если интересно, то посмотрите исходный код.
В виде Home можно выбрать торговою точку на маршруте маршрута, а также просмотреть заказы этой точки. Вот так это выглядит:
image


Осталось получить мобильное приложение в несколько кликов:
● Заходим на http://build.phonegap.com.
● логинимся через GitHub.
● Добавляем новое приложение, выбрав нужный репозиторий
● Нажимаем волшебную кнопку “Ready to build”
● И получаем приложение на 5 из 6 платформ (к сожалению у меня нет ключа на IOS):
image

Небольшая специфика: снимок выше сделан на PhoneGap версии 2.9, кстати, WP тут версии 7. При выборе версии 3.+ получим вот такое сообщение: Blackberry, Symbian, and WebOS are no longer supported as of PhoneGap 3, но за то есть поддержка WP8.

Осталось добавить немного дегтя (может и много): производительность. На старых или слабых устройствах работать просто невозможно — тормоз полнейший. Но как только брал устройство с более-менее нормальными параметрами, все получалось. Согласен, это не нативное приложение, но я даже сравнивать не хочу — это разные вещи. И я уверен у такого подхода (Ну в смысле HTML5 вообще) есть будущее, и ниша будет найдена.

Ссылки на приложение:

● Исходники https://github.com/gfhfk/MobSales
● Протестировать в браузере (я пробовал Chrome и IE11) http://mobsales.azurewebsites.net/.
● Кому интересно серверная часть: http://mobsalessrv.azurewebsites.net/odata/
● сама программа:
Версия 3.4 https://build.phonegap.com/apps/727149
image

Кому интересно версия 2.9 https://build.phonegap.com/apps/733422/
image


Примечание 1: Есть проблема с установкой приложения на Windows Phone через qr-код, возникает ошибка. Нужно заходить по ссылке с компьютера, качать xap и заливать его через Application Deployment Tool. Наверно глюк Phonegap Build.
Примечание 2: Иногда, особенно при первом запуске или при запуске на эмуляторе, выдается вот такая ошибка:
image


Решение этой проблемы очень простое:
1. Переименуйте файл index.html на main.html.
2. Создайте новый index.html и вставьте кусок:
<!doctype html>
<html>
  <head>
   <title>tittle</title>
   <script>
     window.location='./main.html';
   </script>
  <body>
  </body>
</html>

3. Все — больше ошибок нет (Хотя с версией 2.9 на некоторых устройствах все таки выдавало).
Примечание 3: В эмуляторе WP8 приложение запускается не с первого раза, пока еще не разобрался почему. На реальных устройствах не пробовал. В WP8.1 вообще не запустилось(разбираться пока не буду).
Примечание 4: Сайты находятся на бесплатном хостинге Windows Azhure WebSites, где ресурсы ограничены. Учитывая хабра эффект сайты могут лечь, тогда дайте знать — переведу на платный на некоторое время.
Итог писать не буду, предлагаю всем желающим самому протестировать и ответить на опрос, по которому решу что дальше делать:
Only registered users can participate in poll. Log in, please.
что дальше делать
71.43% Интересно — продолжать дальше развивать приложение.30
16.67% Не совсем понятно — нужно увеличить функционал.7
2.38% Интересно — только нужно изменить библиотеки (укажите какие в комментариях)1
19.05% HTML5 для мобильных приложений есть зло — использовать другой подход для мультиплатформенной разработки (Xamarin, Delphi)8
2.38% мультиплатформенные мобильные приложения есть зло — только нативные приложения1
11.9% Все бросить — начинать писать реальные проекты.5
42 users voted. 25 users abstained.
Tags:
Hubs:
Total votes 6: ↑4 and ↓2+2
Comments2

Articles