Text
                    КАФЕДРЕ ИНФОРМАЦИОННЫХ СИСТЕМ
И ТЕХНОЛОГИЙ СПБГЛТУ 50 ЛЕТ

Н. П. ВАСИЛЬЕВ,
А. М. ЗАЯЦ

ВВЕДЕНИЕ
В ГИБРИДНЫЕ
ТЕХНОЛОГИИ
РАЗРАБОТКИ
МОБИЛЬНЫХ
ПРИЛОЖЕНИЙ
Учебное пособие

САНКТПЕТЕРБУРГ
МОСКВА
КРАСНОДАР
2020


УДК 004.45(075.8) ББК 32.973.3я73 В 19 Васильев Н. П. Введение в гибридные технологии разра ботки мобильных приложений : учебное пособие для ВО / Н. П. Васильев, А. М. Заяц. — СанктПетербург : Лань, 2020. — 160 с. : ил. — (Учебники для вузов. Специальная литература). — Текст : непосредственный. ISBN 978"5"8114"5029"9 В пособии после изложения сущности гибридных приложений на основе Apache Cordova (ранее PhoneGap) и NativeScript, их особенностей, достоинств и недостатков, обсуждается базовая технология Cordova, а в качестве примера строится простое приложение, использующее данные геолокации. Дан краткий обзор ряда наиболее известных библиотек JavaScript и фреймворков на их основе. Объяснено, как устанавливать и использовать инструменты командной строки для управления жизненным циклом при ложения Sencha Ext JS Modern и интеграции его с Cordova. Полные листинги кодов представлены в приложении. Особое внимание уделено вопросам разработки гибридных приложений под iOS. Подробно рассматривается так называемое Ad Hocраспространение приложений через защищённое соединение с webсервером. УДК 004.45(075.8) ББК 32.973.3я73 Îáëîæêà П. И. ПОЛЯКОВА © Издательство «Лань», 2020 © Н. П. Васильев, А. М. Заяц, 2020 © Издательство «Лань», художественное оформление, 2020
ВВЕДЕНИЕ Обсуждая вопросы, связанные с разработкой мобильных приложений, придётся оперировать рядом модных терминов. Так, например, среди разработчиков часто употребляется термин «фреймворк» (framework), который в какойто мере подменяет термин «технология» и более привычный — software development kit. Фреймфорки определяются как сложные среды разработки программного обеспечения. Они включают в себя множество разноплановых компонентов. В их состав входят отладчики, компиляторы, библиотеки c описаниями интерфейсов, редакторы и другие компоненты. Все они предназначены для упрощения разработки приложений. Фреймворки составляют основу процесса разработки, и их использование делает этот процесс более простым и интересным. Без фреймворков каждое приложение пришлось бы писать с нуля. Другим популярным термином является термин «экосистема». Экосистема объединяет сообщество разработчиков. Система предусматривает возможность использования открытых и общих ресурсов каждым членом сообщества (фактически каждым желающим), а с другой стороны, предусматривает механизмы пожертвования своих разработок сообществу. Современные тенденции развития технологий разработки таковы, что можно явно выделить так называемые нативные — «родные», можно сказать, официальные технологии разработки, предлагаемые «держателями» мобильных платформ, и иные технологии от сторонних фирм. Официальные, или нативные (от англ. native — родной), технологии предлагаются непосредственно разработчиками мобильных операционных систем (или мобильных платформ). Наиболее популярные платформы — это iOS и Android, и их разработчики — это Apple и Google. Что касается Android, то это открытая операционная система, и Google поэтому является скорее инициатором или организатором этой платформы. Для разработки нативных iOS-приложений Apple предлагает свою среду разработки Xcode и языки программирования swift или objective C. Для Android соответствующей нативной средой разработки является Android Studio (https://developer.android.com/studio/index.html), в которой используется язык программирования Java. Для Android также возможен вариант нативной разработки на языке C++ в среде Android NDK. Аналогичное сопоставление можно провести и для других популярных мобильных платформ, которыми являются Windows Phone (Microsoft), Bada-Tizen (Samsung) или BlackBerry OS и др. Разнообразие всевозможных проектов, стартапов и уже устоявшихся и признанных фреймворков и экосистем от сторонних фирм поражает. Неофициальные технологии могут привлечь разработчика, например, языками программирования, которыми владеет разработчик, и, как следствие, более низким порогом вхождения в разработку, более короткой кривой обучения, а также универсальностью и простотой. Разработка мобильных приложений в настоящее время возможна на языке C#, а огромная армия web-разработчиков вполне успешно может проявить своё искусство на языках JavaScript, HTML и CSS. 3
Язык JavaScript в последнее время набирает всё большую силу — становится более популярным и уже давно вышел за рамки средства для «оживления» HTML-страниц. Наряду с JavaScript ряд фреймворков ориентирован на использование языка TypeScript от Microsoft, который отличается большей строгостью и находит своих пользователей среди сторонников традиционного объектно-ориентированного подхода, характерного для языков C++ или Java. Однако отметим, что TypeScript транслируется в JavaScript, который в конечном счёте и реализуется интерпретатором. В настоящее время большинство фреймворков, ориентированных под web-разработчиков, используют одну из двух фундаментальных технологий, позволяющих разрабатывать или гибридные приложения, или приложения, аналогичные нативным (или и то и другое). 4
1. РАЗРАБОТКА МОБИЛЬНЫХ ПРИЛОЖЕНИЙ ДЛЯ WEB-РАЗРАБОТЧИКОВ 1.1. Гибридные приложения 1.1.1. Происхождение термина Термин «гибридная разработка» отражает суть этого подхода — он сочетает в себе native- и web-разработку. Гибридное мобильное приложение представляет собой web-код (то есть код JavaScript, HTML и CSS), который выполняется компонентом webview (дословно web-просмотр), интегрированным в native-приложение. Гибридные приложения имеют web-интерфейс и производительность браузера. Однако, в отличие от web-приложений, гибридные приложения могут получить доступ к функциональности устройства, к таким его частям, как датчики акселерометра и компаса, камера, геолокация и т. д. Наиболее популярной платформой для разработки гибридных мобильных приложений в настоящее время является Apache Cordova. История появления этой технологии такова: в 2008–2009 гг. в рамках канадского стартапа Nitobi был создан проект PhoneGap как среда с открытым исходным кодом, которая позволяла получить доступ к нативным функциям мобильного устройства из встроенного webview. Целью проекта было обеспечить возможность построения мобильных приложений исключительно на web-технологиях (HTML/CSS и JavaScript), но с возможностью вызова нативного кода. В 2011 г. Adobe приобрела Nitobi и все права на PhoneGap. Исходный код ядра был передан Apache Foundation. Этот исходный код остался открытым, но ему понадобилось новое имя. После пары неудачных попыток в конце концов было выбрано название «Cordova» — по названию улицы, на которой был расположен офис Nitobi. Cordova внедряет web-код в webview и предусматривает интерфейс (Application Programm Interface) для доступа к собственным ресурсам устройства (к файловой системе и другой аппаратуре) из кода JavaScript через базовые плагины. Дополнительные плагины, разработанные экосообществом, значительно расширяют возможности гибридных приложений. На рисунках, представленных далее, наглядно отражена архитектура взаимодействия гибридных приложений с мобильной платформой. Далее наглядно представлен порядок взаимодействия гибридного приложения с мобильной платформой. Web-часть гибридного приложения (Web portion of App) взаимодействует с операционной системой (OS-Specific APIs) опосредованно — через движок webview (Rendering Engine). Нативная часть приложения, которая обычно представлена плагинами, работает с сервисами операционной системы непосредственно. Стрелки от HTML, CSS, JS к сервисам OS-Specific APIs показывают запрещённое взаимодействие. 5
Рис. 1 Три различных варианта взаимодействия с операционной системой и устройством: нативное приложение, гибридное и web-приложение (https://myshadesofgray.wordpress.com/2014/04/15/hybrid-applications-and-android-nativebrowser/) Рис. 2 Архитектура гибридного приложения 1.1.2. Достоинства и недостатки гибридных приложений Достоинства: • Популярные и универсальные для всех платформ языки программирования: JavaScript, HTML, CSS. 6
• Кроссплатформенный подход: один и тот же код можно использовать для iOS, Android, Windows Phone и других мобильных платформ. • Сокращение времени и стоимости разработки за счёт универсальности кода и сокращения сроков обучения. • Мощная экосистема разработки с множеством ресурсов. Apache Cordova — это платформа с открытым исходным кодом. Ядро платформы обеспечивает доступ к основным возможностям мобильной операционной системы и функциям устройства, а сообщество разработало широкий спектр подключаемых модулей, позволяющих задействовать дополнительные функциональные возможности устройств. Недостатки: • Поскольку webview разные для разных платформ (даже для разных версий), то могут потребоваться дополнительные настройки и оптимизация кода, чтобы приложение работало, как ожидается, на всех устройствах. • Проблемы с производительностью, особенно для задач с быстрой графикой: у webview есть некоторые проблемы при обработке графики, типичной для игр или приложений с динамичным графическим интерфейсом. • Для использования уникальных функций платформы потребуется написать дополнительный нативный код (если не удаётся найти готовый подключаемый модуль — плагин). • Часто плагин, реализующий требуемую функцию, оказывается устаревшим и может потребовать доработки. • Медленное внедрение новых версий платформ. Можно ждать несколько месяцев, прежде чем Apache Cordova представит проверенную поддержку новой версии мобильной платформы и ее функций. 1.2. NativeScript 1.2.1. Происхождение термина Название NativeScript отражает сущность технологии — создавать приложения с помощью JavaScript, которые выполняются на мобильной платформе с подобающей (нативной) производительностью, используют нативный интерфейс — API платформы — и получают непосредственный доступ к функциональности устройств (https://docs.nativescript.org). Основой мобильного приложения являются модули NativeScript и так называемые runtimes. Модули предоставляют доступ к функциям устройства и компонентам пользовательского интерфейса из кода JavaScript, а runtimes налету (дословно — во время выполнения) интерпретируют, компилируют и выполняют JavaScript-код и таким образом позволяют использовать уникальные API-интерфейсы платформы. Некоторые дополнительные функции доступны через пользовательские плагины NativeScript — модули платформы node.js [1, 16]. Архитектуру приложения NativeScript наглядно представляют следующие рисунки. 7
Рис. 3 Взаимодействие приложения NativeScript с мобильной платформой (https://myshadesofgray.wordpress.com/tag/mobile/) Рис. 4 Порядок взаимодействия NativeScript-кода с мобильной платформой (https://docs.nativescript.org/core-concepts/technical-overview) Последний рисунок представляет наиболее важные части технологии NativeScript: • Runtimes, как уже было сказано, являются своеобразными интерпретаторами кода TypeScript/JavaScript, а точнее, базовых модулей — NativeScript Core Modules. 8
• Core Modules, в свою очередь, предназначены для предоставления абстракций, необходимых для доступа к функциям нативных платформ через Runtimes. Они фактически устанавливают определённую дисциплину работы с API мобильной платформы с помощью языка TypeScript/JavaScript. • Сборка, установка и запуск приложений на iOS или Android через CLI NativeScript (Command Line Interface — интерфейс командной строки) команды NativeScript. 1.2.2. Достоинства и недостатки NativeScript Достоинства технологии: • Кроссплатформенный подход. Один код можно использовать для iOS или Android. • Знания JavaScript, XML и CSS и некоторое понимание нативного программирования в iOS и Android позволяют углубиться в NativeScript. • Нативный пользовательский интерфейс и производительность на всех платформах и устройствах, поскольку приложения непосредственно используют компоненты iOS или Android. • Экосистема разработки с открытым исходным кодом. • Поддержка one day для новых версий платформ: когда новая версия мобильной платформы становится доступной, NativeScript быстро обеспечивает поддержку новой версии и ее функций. NativeScript — это довольно «молодая» технология, и это её главный недостаток. Сообщество быстро разрастается, однако ресурсы экосистемы — дополнительные плагины ещё не наработаны в требуемом объёме. Поддерживаются только iOS и Android, хотя со временем список поддерживаемых платформ, вероятно, расширится. 1.2.3. Сравнение гибридной и NativeScript-разработки Представленная ниже таблица даёт сравнительную оценку двух основных технологий, позволяющих использовать web-код, — гибридной и NativeScript: Apache Cordova NativeScript JavaScript, HTML, CSS JavaScript, XML, CSS, основы натив-ного программирования в iOS и And-roid Поддерживаемые устройства и платформы Android 2.3.3 и старше iOS 5.0 и старше Windows Phone 8.0 и 8.1 Android 4.2 и старше iOS 7.1 и старше Пользовательский интерфейс web-интерфейс — одинако- оригинальный вый для всех платформ платформы Требуемые и знания умения Производительность Браузера Нативных приложений 9 интерфейс
Продолжение табл. Apache Cordova NativeScript Поддерживаемые фреймворки Ionic (https://ionicframework.com) Sencha Ext JS modern https://www.sencha.com/ Kendo UI https://www.progress.com/ Vue.js (https://vuejs.org) Angular 2 NativeScript https://www.progress.com/nativescript React React-Native https://facebook.github.io/react-native Библиотеки для Android Библиотеки для iOS и CocoaPods Поддерживаемые менеджеры управления модулями (пакетами) bower npm Модель расширения Apache Cordova plugins NativeScript plugins Отметим, что bower — это модуль node.js, который позволяет управлять установкой библиотек JavaScript, а npm — менеджер модулей node.js. Наглядное представление о соотношении универсальности, производительности и совместимости, которые имеют различные технологии разработки мобильных приложений, даёт представленный ниже рисунок. Рис. 5 Соотношение универсальности, производительности и совместимости (https://myshadesofgray.wordpress.com/tag/mobile/) 1.3. Заключение Гибридная разработка Apache Cordova и кроссплатформенная разработка NativeScript имеют много общего. 10
Оба фреймворка: • используют один и тот же исходный код приложения для различных мобильных платформ; • допускают интеграцию с JavaScript-библиотеками и менеджерами пакетов; • допускают расширение с помощью плагинов (подключаемых модулей) от разработчиков эко-сообщества; • имеют открытый код. Гибридный подход является лучшим выбором в следующих случаях: • разработка ведётся на JavaScript, HTML и CSS; • Для получения наибольшей выгоды от мощной экосистемы; • пользовательский интерфейс разрабатывается с помощью Ionic, Kendo UI, jQuery Mobile, Sencha Ext JS или других JavaScript-библиотек; • используются пакеты Bower; • возможность заимствования кода аналогичного web-приложения; • кратчайшие сроки разработки и обучения; • приложение должно выполняться для Android, iOS, Windows Phone и других платформах; • приложение должно выполняться одинаково на старых и новых устройствах (то есть на старых и новых версиях мобильных операционных систем); • приложение не использует активно встроенную функциональность устройства; • от приложения не требуется максимальная производительность; • от приложения не требуется специфичный для каждой платформы интерфейс. NativeScript является лучшим выбором в следующих случаях: • разработка требует знания JavaScript, XML и CSS и понимания основ нативного программирования под iOS и Android; • готовность принять недостатки начальной стадии развития экосистемы сообщества; • используются модули npm; • достаточно времени для разработки и обучения; • приложение должно выполняться на Android и iOS; • приложение должно выполняться на современных устройствах с последними версиями операционных систем; • приложение активно использует нативную функциональность устройства; • требуется пиковая производительность, например для игр; • требуются специфичные для платформы интерфейс и функциональность. 11
2. РАЗРАБОТКА ГИБРИДНЫХ ПРИЛОЖЕНИЙ НА ОСНОВЕ CORDOVA 2.1. Установка Cordova Cordova — это модуль популярной платформы node.js [1, 7, 8, 9, 15]. Поэтому скачиваем предлагаемую на официальном сайте https://nodejs.org версию node (из двух вариантов рекомендуется выбирать не последнюю, а рекомендуемую для большинства пользователей — Recommended For Most Users). Платформа node.js работает в любой операционной системе, и её установка обычно не вызывает проблем. Также будет установлен менеджер управления модулями npm. Устанавливаем модуль Cordova глобально — выполняем в терминале с правами администратора следующую команду: npm install —g сordova Иногда возникает необходимость определить — где в файловой системе находится установленный модуль — сделать это можно простой командой: which cordova 2.2. Обновление Cordova Как уже было сказано, Cordova — это модуль node.js, и поэтому он обновляется согласно общим правилам, установленным для этой платформы. Модулями node.js управляет менеджер npm, который устанавливается совместно с node.js. Обновление глобального модуля Cordova выполняется командой: npm update —g cordova Кстати, удалить модуль также достаточно просто: npm uninstall -g cordova После обновления Cordova, очевидно, следует обновить мобильные платформы, которые были созданы с помощью прежней версии. Сделать это можно следующим образом: сначала удалить, а затем вновь добавить платформу. Удаляем, например, платформу android: cordova platform remove android Затем восстанавливаем платформу с обновлённой версией Cordova: cordova platform add android Другой вариант обновления платформы — команда: cordova platform update android 12
Также следует упомянуть команду, которая позволяет «очистить» сборку (build) приложения под ту или иную платформу от всех артефактов: cordova clean android ios Здесь мы очистили сборку под Android и iOS. 2.3. Что потребуется дополнительно для разработки под iOS Потребуются: • компьютер от Apple; • учётная запись разработчика (Apple ID); • установленный Xcode. Apple ID. У владельца устройства от Apple, скорее всего, уже есть Apple ID. В любом случае желательно перейти по ссылке https://developer.apple. com/account, а затем авторизоваться (здесь же можно создать Apple ID в случае его отсутствия): Рис. 6 Форма авторизации разработчика на сайте Apple (https://developer.apple.com/account) Следует установить среду разработки Xcode. Это можно сделать через App Store или скачать пакет установки (dmg) с портала разработчика (загрузка будет доступна после авторизации). Не следует выбирать бета-версию. Собственно, для Cordova требуется только интерфейс командной строки — Xcode CLI (Command Line Interface) — это часть Xcode. После установки Xcode убедиться в том, что CLI установлен, можно, выполнив в терминале команду: xcode-select —install Эта команда позволит установить Xcode CLI, если по каким-то причинам он оказался всё-таки не установленным. 13
Процесс развертывания, то есть загрузка и установка (deploing) на мобильное устройство iOS приложений, тем более их централизованное распространение (distributing), — довольно сложный и не бесплатный процесс. Следует, однако, подчеркнуть, что с появлением Xcode 7-й версии стала возможна установка приложений без участия в одной из платных программ Apple (Apple Developer Program или Apple Developer Enterprise Program), то есть бесплатно, правда, с рядом существенных ограничений. Эту возможность иногда называют Free Provisioning. Пожалуй, самое существенное ограничение состоит в том, что приложение будет запускаться на мобильном устройстве только в течение недели. Однако если пересобрать приложение и вновь развернуть на мобильном устройстве, то оно снова просуществует очередную неделю. В остальных случаях потребуется приобщиться (зачислиться — Enroll) к Apple Developer Program или Apple Developer Enterprise Program на портале developer.apple.com. Различие этих двух программ в основном состоит в том, что участие в первой дает право на централизованное распространение приложений через App Store. Участие во второй ориентировано на распространение приложений только внутри фирмы (организации). Это так называемое In-House распространение (за пределами App Store). Участие в первой программе обойдется в 99$, участие во второй — 299$ в год. В рамках любой программы допускается так называемое Ad Hoc распространение — можно устанавливать приложения на 100 устройствах в год. Более подробно эти вопросы обсуждаются далее [2, 3, 4, 5, 6]. 2.4. Что потребуется дополнительно для разработки под Android Понадобится Android Studio — интегрированная среда разработки под Android, созданная на базе популярной (для java-разработчиков) среды IntelliJ IDEA. Однако в первую очередь нужно убедиться, что установлен JDK (Java Development Kit). Это обязательный компонент для разработки на Java, а поскольку нативная разработка под Android ведется на Java — то и для разработки под Android тоже. Скачать JDK можно бесплатно с официального сайта https://www.oracle.com/technetwork/java/javase/downloads/index.html. Следует выбирать одну из последних версий JDK, и именно JDK, а не JRE! Если есть сомнение — установлен ли этот пакет на компьютере, то можно попытаться в окне терминала выполнить команду java, а в Windows поискать JDK среди установленных программ в соответствующей оснастке. Скачивать Android Studio следует здесь: https://developer.android.com/ studio. В одном установщике будет все необходимое — среда разработки IDE, Android Emulator, Android SDK. Наиболее объёмным является последний компонент, под который следует предусмотреть больше места на диске. В комплект SDK должна входить хотя бы одна из платформ — набор библиотек, позволяющих взаимодействовать с системой Android. Такая платформа называется целевой (target platform) и за14
даётся во время создания проекта через параметр — target. Платформа также указывается при создании виртуального Android-устройства (AVD — Android Virtual Device). Список установленных в SDK платформ можно получить командой: android list targets Ответ — список с описанием установленных платформ: Available Android targets: ---------id: 1 or "android-19" Name: Android 4.4.2 Type: Platform API level: 19 Revision: 4 Skins: HVGA, QVGA, WQVGA400, WQVGA432, WSVGA, WVGA800 (default), WVGA854, WXGA720, WXGA800, WXGA800-7in Tag/ABIs : default/armeabi-v7a, default/x86 ---------id: 2 or "android-20" Name: Android 4.4W.2 Type: Platform API level: 20 Revision: 2 Skins: HVGA, QVGA, WQVGA400, WQVGA432, WSVGA, WVGA800 (default), WVGA854, WXGA720, WXGA800, WXGA800-7in, AndroidWearRound, AndroidWearSquare, AndroidWearRo und, AndroidWearSquare Tag/ABIs : android-wear/armeabi-v7a, android-wear/x86 ---------id: 3 or "android-21" Name: Android 5.0.1 Type: Platform API level: 21 Revision: 2 Skins: HVGA, QVGA, WQVGA400, WQVGA432, WSVGA, WVGA800 (default), WVGA854, WXGA720, WXGA800, WXGA800-7in Tag/ABIs : android-tv/armeabi-v7a, android-tv/x86, default/armeabi-v7a, default /x86, default/x86_64 ---------- Первая строка каждого описания — идентификатор, однозначно определяющий конкретную платформу в SDK. Его числовая часть определяется порядком, в котором платформы устанавливались в SDK. Текстовое же значение является уникальным и указывает версию платформы, независимо от компьютера, и порядка, в котором платформы были установлены. В поле Name содержится имя цели — версии Android. Каждой версии Android однозначно соответствует уровень API (API level). API (Application Program Interface) — интерфейс для приложений, который обеспечивает операционная система: 15
Версия платформы Уровень API Код версии Android 5.1 22 LOLLIPOP_MR1 Android 5.0 21 LOLLIPOP Android 4.4W 20 KITKAT_WATCH Android 4.4 19 KITKAT Android 4.3 18 JELLY_BEAN_MR2 Android 4.2, 4.2.2 17 JELLY_BEAN_MR1 Android 4.1, 4.1.1 16 JELLY_BEAN Android 4.0.3, 4.0.4 15 ICE_CREAM_SANDWICH_MR1 Android 4.0, 4.0.1, 4.0.2 14 ICE_CREAM_SANDWICH Android 3.2 13 HONEYCOMB_MR2 Android 3.1.x 12 HONEYCOMB_MR1 Android 3.0.x 11 HONEYCOMB Android 2.3.4 Android 2.3.3 10 GINGERBREAD_MR1 Android 2.3.2 Android 2.3.1 Android 2.3 9 GINGERBREAD Android 2.2.x 8 FROYO Android 2.1.x 7 ECLAIR_MR1 Android 2.0.1 6 ECLAIR_0_1 Android 2.0 5 ECLAIR Android 1.6 4 DONUT Android 1.5 3 CUPCAKE Android 1.1 2 BASE_1_1 Android 1.0 1 BASE После установки Android Studio будет доступен менеджер sdkmanager в каталоге %ANDROID_HOME%\tools\bin, который позволяет проверить факт установки требуемых компонент, а в случае отсутствия — установить требуемые компоненты. Здесь ANDROID_HOME — переменная окружения, которая создаётся в результате установки и содержит путь к месту расположения Android Studio. Желательно, чтобы были установлены: Android SDK Platform 25, Android SDK Build-Tools 25.0.2 (или новее), Android Support Repository, Google Repository. Для этого можно выполнить следующую команду в терминале Windows: "%ANDROID_HOME%\tools\bin\sdkmanager" "tools" "platform-tools" "platforms;android-25" "build-tools;25.0.2" "extras;android;m2repository" "extras;google;m2repository" 16 Powered by TCPDF (www.tcpdf.org)
Наконец, можно создать эмуляторы — виртуальные устройства — Android virtual devices (AVDs) для тестов приложений на этих устройствах. Сделать это можно из Android Studio, выбрав Tools-Android-AVD Manager. При этом Android Studio может отказать в создании эмуляторов, например, по причине того, что процессор компьютера не поддерживает технологию vt-x (может быть, просто не сделана соответствующая установка в BIOS или EFI). В настоящее время существует богатый арсенал всевозможных эмуляторов от сторонних фирм, которые могут оказаться более удобными и быстрыми по сравнению с официальными. Например, genymotion, который доступен на официальном сайте https://www.genymotion.com/download/ (для скачивания программы придётся предварительно зарегистрироваться). Эмулятор основан на известной программе виртуализации virtualbox, которую можно скачать с официального сайта https://www.virtualbox.org/ и установить отдельно от genymotion или воспользоваться инсталлятором, который уже включает virtualbox (то есть genymotion предлагается в двух вариантах — с virtualbox и без). Далее потребуется интегрировать genymotion и Android Studio. Во-первых, следует указать genymotion, где расположен ADB Tool (Android Debug Bridge Tool), установленный вместе с Android Studio. По умолчанию genymotion использует свой собственный ADB Tool, но можно определить специфику последнего в Android SDK от Android Studio. Чтобы специфицировать заказной ADB Tool: 1. Открыть Genymotion — Settings — ADB. 2. Включить Use custom Android SDK tools. 3. Специфицировать маршрут к Android SDK, кликнув Browse (если пользователь Aministrator, то путь в Windows может быть таким: C:/Users/Administrator/AppData/local/Android/sdk). 4. Кликнуть OK. Теперь в genymotion следует создать требуемые эмуляторы. Во-вторых, в Android Studio потребуется установить плагин для связи с genymotion: 1. Перейти в раздел Файл — Настройки (для Windows и Linux) или в Android Studio — Preferences (для Mac OS X). 2. Выбрать Плагины и Обзор репозиториев. 3. Найти в появившемся списке плагинов Genymotion и в контекстном меню выбрать Загрузить и установить. После перезагрузки Android Studio появится иконка плагина в линейке инструментов. При первой активации этой иконки, конечно, потребуется указать, где установлен genymotion, но затем созданные там эмуляторы будут доступны в Android Studio. 2.5. Создание приложения Cordova 2.5.1. Создание стартового (шаблонного) приложения Рекомендуется создать каталог — рабочее пространство для размещения разработок (и не только Cordova). Не обязательно этот каталог называть work17
space. Желательно, но не обязательно, чтобы workspace был доступен для webсервера. Следует перейти в каталог рабочего пространства. В рамках этого каталога стартовать проект Cordova позволяет команда: сordova create app_path com.mycompany.myteam.myapp MyAppName Здесь com.mycompany.myteam.myapp — это ID приложения (AppID в iOS), которое строится по таким же принципам, как и доменные имена серверов (DNS), только в обратном порядке и с учётом регистра. Следует тщательно продумывать этот идентификатор, особенно для iOS-приложений, которые реально планируются развивать вплоть до загрузки в App Store. Например, учебное приложение, демонстрирующее доступ к геолокации, можно назвать так: learn.CordovaTest.geolocation. Наконец, последний параметр — это название приложения. Последние два параметра (App ID и название приложения MyAppName, необязательны). Перейдём в каталог рабочего пространства и выполним команду: сordova create CordovaGeolocation learn.CordovaTest.geolocation CordovaGeolocationApp В результате будет создан каталог CordovaGeolocation со следующей структурой: Рис. 7 Структура каталога приложения Cordova 18
На рисунке присутствует каталог плагина cordova-plugin-geolocation, который будет добавлен далее. Разработка приложения ведётся в каталоге www, который готов для загрузки обозревателем сразу после генерации приложения, вот результат такой загрузки: Рис. 8 Так выглядит загрузка приложения Cordova в браузере Этот результат (index.html): соответствует содержимому индексного файла <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *; img-src 'self' data: content:;"> <meta name="format-detection" content="telephone=no"> <meta name="msapplication-tap-highlight" content="no"> <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width"> <link rel="stylesheet" type="text/css" href="css/index.css"> <title>Hello World</title> </head> <body> <div class="app"> <h1>Apache Cordova</h1> <div id="deviceready" class="blink"> <p class="event listening">Connecting to Device</p> <p class="event received">Device is Ready</p> </div> </div> <script type="text/javascript" src="сordova.js"></script> <script type="text/javascript" src="js/index.js"></script> </body> </html> В этом файле прежде всего привлекают внимание мета-теги. 19
2.5.2. Назначение мета-тегов Первый мета-тег определяет политику безопасности содержимого (Content Security Policy). Можно разрешать или запрещать те или иные сетевые запросы, выполняемые через webview напрямую, используя различные критерии, например: <!— Разрешить загрузку содержимого любого типа, расположенного там же, где и этот файл — self-расположение, используя тот же протокол, который использовался для загрузки этого файла, а также из foo.com --> <meta http-equiv="Content-Security-Policy" content="default-src 'self' foo.com"> <!— Разрешить загрузку любого содержимого (например, CSS, AJAX, frame, media и т. д.) с учётом следующих требований * CSS только из self-расположения или inline, * скрипты из self-расположения или inline, разрешить eval()--> <meta http-equiv="Content-Security-Policy" content="default-src *; stylesrc 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafeeval'"> <!-- Разрешает загрузку только из self-расположения через протокол https --> <meta http-equiv="Content-Security-Policy" content="default-src 'self' https:"> <!-- Разрешить iframe с содержимым из self-расположения и https://cordova.apache.org/ --> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; frame-src 'self' https://cordova.apache.org"> Политика безопасности делает приложение безопасным от Cross-Site Scripting (XSS) атак. В стартовом проекте Cordova предлагает следующий вариант: <meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *; img-src 'self' data: content:;"> Любой скрипт, который злоумышленник попытается добавить в страницу, просто будет отклонен благодаря данному мета-тегу. Мета-тег format-detection — запрещаем распознавать номера телефонов, а также выделять их. Пользователь не сможет нажать на ссылку, чтобы сделать звонок на этот номер телефона: <meta name="format-detection" content="telephone=no"> Мета-тег msapplication-tap-highlight отключает серую подсветку нажатия (tap) в Windows Phone версии 8 и старше. Если не планируется разворачивать приложение на Windows Phone устройство, можно удалить данный тег: <meta name="msapplication-tap-highlight" content="no"> 20
Следующий мета-тег viewport. Первый атрибут (слева направо) — userscalable=no запрещает пользователю масштабировать страницу (увеличивать или уменьшать содержимое). Далее initial-scale со значением 1 означает, что содержимое изначально загружается на 100% — без масштабирования, как есть. Атрибуты maximum-scale и minimum-scale установлены в 1. Это минимальные и максимальные значения, допустимые для масштабирования. Атрибут width — максимальная ширина содержания страницы, на экране устройства: <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width"> 2.5.3. Индикация состояния Cordova После мета-тегов в индексном файле (index.html) подключаются стили (index.css) для форматирования содержимого тела документа, основное назначение которого — индикация состояния Cordova. Элемент p с классом listening отображает состояние готовности API (Application Program Interface) устройства. Он будет скрыт, когда API устройства будет полностью загружено — готово к работе. После загрузки отображаться будет следующий элемент p: <div class="app"> <h1>Apache Cordova</h1> <div id="deviceready" class="blink"> <p class="event listening">Connecting to Device</p> <p class="event received">Device is Ready</p> </div> </div> Далее идут два тега script. Первый подключает файл cordova.js. Код в этом файле обеспечивает единый для разных мобильных платформ API для доступа к нативным функциям устройства. Данный файл отсутствует в исходной папке www, так как он по очевидным причинам добавляется в приложение на стадии выбора платформы, то есть это разные файлы для разных платформ. Второй script подключает файл index.js со следующим содержимым: var app = { // Application Constructor initialize: function() { document.addEventListener('deviceready', this.onDeviceReady.bind(this), false); }, // deviceready Event Handler // // Bind any Cordova events here. Common events are: // 'pause', 'resume', etc. onDeviceReady: function() { this.receivedEvent('deviceready'); }, // Update DOM on a Received Event receivedEvent: function(id) { var parentElement = document.getElementById(id); 21
var listeningElement = parentElement.querySelector('.listening'); var receivedElement = parentElement.querySelector('.received'); listeningElement.setAttribute('style', 'display:none;'); receivedElement.setAttribute('style', 'display:block;'); console.log('Received Event: ' + id); } }; app.initialize(); Здесь дожидаемся специфичного для работы с мобильным устройством события deviceready, которое соответствует полной готовности API Cordova к работе. Реакция на это событие фактически реализуется в функции receivedEvent(). В изначальном варианте эта функция отвечает за скрытие и отображение элементов p, которые описаны выше. 2.5.4. Добавление платформы Прежде всего заметим, что команды Cordova для работы с проектом после его создания должны выполняться из каталога этого проекта. Под платформой следует понимать операционную систему мобильного устройства. Это также может быть и обозреватель browser. Узнать о добавленных платформах позволяет команда: cordova platform list В ответ получим не только добавленные платформы, но и доступные для добавления (Available platforms): Installed platforms: browser 5.0.1 ios 4.5.3 Available platforms: android ~6.3.0 blackberry10 ~3.8.0 (deprecated) osx ~4.0.1 ubuntu ~4.3.4 (deprecated) webos ~3.7.0 windows ~5.0.0 www ^3.12.0 Добавим платформу browser: cordova platform add browser В ответ Cordova прокомментирует выполненные действия: Using cordova-fetch for cordova-browser@~5.0.0 Adding browser project... Creating Cordova project for cordova-browser: Path: /WWW/CordovaGeolocation/platforms/browser 22
Name: CordovaGeolocationApp Discovered plugin "cordova-plugin-whitelist" in config.xml. Adding it to the project Installing "сordova-plugin-whitelist" for browser Adding cordova-plugin-whitelist to package.json Saved plugin info for "сordova-plugin-whitelist" to config.xml --save flag or autosave detected Saving browser@~5.0.1 into config.xml file ... В результате будет добавлен подкаталог browser в каталоге platforms. Кроме этого, автоматически будет добавлен плагин cordova-plugin-whitelist, о котором речь пойдёт далее. Аналогично можно добавить платформу iOS: cordova platform add ios или Android: cordova platform add android Можно указать несколько платформ одновременно. После добавления платформы можно подготовить приложение к сборке с помощью команды: cordova prepare ios Эта команда скопирует файлы из исходного каталога (www) и дополнительно создаст все необходимые файлы и подкаталоги в соответствующем каталоге с платформой, в том числе и файлы проекта, которые могут быть подхвачены оригинальными средствами программирования под эту платформу — native SDK. Так, для iOS — это Xcode и его файл проекта с расширением xcodeproj: Рис. 9 Структура каталога проекта iOS-приложения 23
Проект, таким образом, можно подгрузить в Xcode и уже с его помощью собрать приложение и загрузить его непосредственно на мобильное устройство или эмулятор. Этот процесс требует отдельного описания. В рамках же командной строки можно скомпилировать приложение: cordova compile ios А команда cordova build ios позволяет совместить обе команды prepare и compile. Если платформа не указана, то команды выполняются для всех добавленных платформ. Следует подчеркнуть, что если речь идёт о платформе iOS, то, конечно, окончательную сборку приложения (релиз) и установку его на мобильное устройство, которая даже в самом простом варианте (free provisioning) требует подписи кода и создания free provisioning profile, удобнее и надёжнее выполнять в рамках Xcode, который делает это всё автоматически. Также заметим, что перед загрузкой проекта в Xcode рекомендуется сделать доступными все файлы проекта, сгенерированные Cordova. Сделать это можно с помощью следующей команды: chmod —R a+rwX path_to_project Эта команда изменит права доступа ко всему содержимому каталога path_to_project (включая подкаталоги) — сделает его доступным по чтению, записи и выполнению для всех групп пользователей. Не стоит также выполнять команду build, если мобильное устройство подключено к компьютеру, поскольку в этом случае Cordova будет пытаться установить приложение на это устройство, а для этого ей потребуются дополнительные модули. Конечно, эти модули можно установить — но это несколько иной путь. Наконец, отметим, что не следует без особой необходимости корректировать файлы, собранные Cordova в каталоге платформы, вся разработка ведётся в исходном каталоге: path_to_project/www Таким образом, после любых доработок в исходнике, то есть в каталоге www, собираем проект под платформу browser, смотрим, что получилось в обозревателе по адресу: http://path_to_project/platforms/browser/www/ Если в системе отсутствует web-сервер, то вполне можно воспользоваться командой: cordova serve 24
Эта команда запустит встроенный web-сервер и сообщит об этом: Static file server running on: http://localhost:8000 (CTRL + C to shut down) То есть результат можно наблюдать по адресу: http://localhost:8000 При этом будет предложено перейти в www-каталог для каждой установленной платформы (iOS и browser): Рис. 10 Результат команды cordova serve 2.5.5. Плагин cordova-plugin-whitelist Основное назначение плагинов Cordova — это дополнительная функциональность приложения, обычно связанная с наличием тех или иных возможностей мобильного устройства и установленного программного обеспечения. Например, плагин может обеспечить доступ к геолокации, камере (фото/видео) или списку контактов. Плагин cordova-plugin-whitelist будет автоматически добавлен при добавлении любой платформы. Этот плагин регламентирует загрузку разрешённого содержимого — «белый» список (whitelist). Прежде уже шла речь о политике Content Security Policy, которая позволяет конфигурировать загрузку кроссдоменного содержимого. Однако это не единственный способ определиться с «белым» списком. Есть и другие возможности, которые связаны с редактированием файла config.xml. 25
Navigation Whitelist — дословно «белый список навигации» — контролирует URL, к которым может обращаться навигатор webview. Такой переход может быть инициирован не только ссылками или кодом JavaScript (window.open()), но и плагинами. По умолчанию доступ разрешён только к локальным файлам: file://URLs. Чтобы разрешить навигацию к иным URL, следует использовать тег <allow-navigation> в файле config.xml: <!—- Разрешить ссылки к любому содержимому в домене example.com --> <allow-navigation href="http://example.com/*" /> <!—- Звёздочки допускаются также при указании протокола и в домене --> <allow-navigation href="*://*.example.com/*" /> <!—- А вот так делать не рекомендуется, поскольку будут допустимы любые запросы в сети через протоколы http and https --> <allow-navigation href="*" /> <!-- Предыдущая декларация эквивалентна следующим трём --> <allow-navigation href="http://*/*" /> <allow-navigation href="https://*/*" /> <allow-navigation href="data:*" /> Intent Whitelist. В отличии от предыдущего, этот белый список не применяется к плагинам, а только к гиперссылкам и вызовам window.open(). По умолчанию внешние URL-адреса не разрешены. Для разрешения следует добавить тег <allow-intent> в config.xml: <!-- Допустимы любые ссылки --> <allow-intent href="http://*/*" /> <allow-intent href="https://*/*" /> <!-- Только к example.com --> <allow-intent href="http://example.com/*" /> <!-- - Звёздочки допустимы в любом месте url --> <allow-intent href="*://*.example.com/*" /> <!-- Разрешить SMS ссылки --> <allow-intent href="sms:*" /> <!-- Разрешить телефонные вызовы --> <allow-intent href="tel:*" /> <!-- Разрешить geo: ссылки --> <allow-intent href="geo:*" /> <!-- Разрешить все url для вызова любых установленных приложений не рекомендуется --> <allow-intent href="*" /> Network Request Whitelist. Это устаревший способ контроля сетевых запросов, вместо которого следует пользоваться возможностями конфигурации CSP. В отличии от CSP этот вариант менее надёжен: например, не блокирует запрещённые URL при редиректе. Этот способ может быть полезен для уста26
ревших webview, которые не поддерживают CSP. Тем не менее приведём примеры конфигурации в config.xml — добавляем теги access: <!-- Любые запросы к google.com --> <access origin="http://google.com" /> <access origin="https://google.com" /> <!—Доступ к maps.google.com --> <access origin="http://maps.google.com" /> <!—Доступ к любому поддомену google.com --> <access origin="http://*.google.com" /> <!-- Разрешить content: URLs --> <access origin="content:///*" /> <!-- Допустимы любые запросы --> <access origin="*" /> 2.6. Базовые плагины Cordova Как уже было сказано, плагины являются неотъемлемой частью экосистемы Cordova. Cordova опосредованно, через плагины общается c компонентами мобильной платформы, то есть с предусмотренными для нативных приложений интерфейсами APIs (Application Programm Interfaces) устройства. Это позволяет использовать JavaScript для работы с нативным кодом. Фреймворк Apache Cordova поддерживает набор базовых плагинов — Core Plugins. Эти плагины предоставляют гибридному приложению доступ к основным возможностям устройства, таким как аккумулятор, камера, контакты и т. д. В дополнение к базовым плагинам существует достаточно большое количество плагинов от экосообщества, которые обеспечивают дополнительную функциональность. Поиск плагинов осуществляется здесь: http://cordova.apache. org/plugins/, или с помощью npm. Создавать свои собственные плагины может любой участник экосистемы согласно руководству от Apache Cordova. В таблице, представленной ниже, перечислены базовые плагины и их реализация для различных мобильных платформ. Android Cordova CLI Mac, Windows, Linux BlackBerry 10 Mac, Windows, Linux Windows 8.1, Windows iOS Windows Phone 8 Phone 8.1, Windows 10 Mac Windows + Embedded webview + — + — — Plugin Interface + + + + + 27
Продолжение табл. Android BlackBerry 10 Windows 8.1, Windows iOS Windows Phone 8 Phone 8.1, Windows 10 Core Plugin APIs Только Windows Phone 8.1 BatteryStatus + + + + Camera + + + + + Capture + + + + + Connection + + + + + Contacts + + + + Device + + + + + Events + + + + + File + + + + + File Transfer + + + + + Geolocation + + + + + Globalization + + + + + InAppBrowser + + + + Media + + + + + Notification + + + + + Splashscreen + + + + + Status Bar + — + + Storage + + + + Vibration + + + + частично через iframe Только Windows Phone 8.1 + Только Windows Phone 8.1 Любые плагины, включая базовые, должны быть явно добавлены, например для добавления плагина камеры следует выполнить команду: cordova plugin add cordova-plugin-camera Или удалить ненужный плагин: cordova plugin add cordova-plugin-camera 28
2.7. Пример приложения, демонстрирующего работу с геолокацией Обсудим простой пример гибридного приложения [7, 8, 9, 10], демонстрирующий доступ к геолокации (которая есть практически у каждого современного мобильного устройства). В качестве заготовки можно взять приложение CordovaGeolocationApp, сгенерированное в предыдущих разделах. Для работы с геолокацией, конечно, потребуется соответствующий плагин, найти и скачать который можно здесь: http://cordova.apache.org/plugins/, или воспользоваться командной строкой, как это делается ниже (если название плагина уже известно). Однако прежде всего обсудим общие вопросы, связанные со стилевым оформлением, характерным для мобильных устройств, тем более что в предыдущем разделе не рассматривались стилевые правила, которые Cordova предлагает в стартовом проекте, и которые расположены в файле index.css. Здесь же мы восполним это упущение с избытком и рассмотрим специфические стили, которые необходимы для оформления содержимого экранов мобильных устройств, и которые будут полезными для любого приложения. 2.7.1. Touch-экраны Запрет выделения При использовании touch-экранов пользователи, как правило, используют не точечные нажатия (клики), а жесты. В результате текст на странице, а также элементы управления могут выделиться. Если это не желательно, то следует запретить выделение: *, body { -moz-user-select: -moz-none; -o-user-select: none; -khtml-user-select: none; -webkit-user-select: none; user-select: none; } Если приложение содержит поля ввода, то для них выделение можно разрешить, например, для последующего копирования: input, textarea { -moz-user-select: text; -o-user-select: text; -khtml-user-select: text; -webkit-user-select: text; user-select: text; } Запрет изменения размеров полей ввода Следует отключить resize у textarea и input — они не должны менять свой размер: input, textarea { resize: none; } 29
Запрет изменения размеров шрифтов Для полного контроля дизайна приложения следует отключить автоматическое изменение размеров шрифтов: *, body { -webkit-text-size-adjust: none; text-size-adjust: none; } Если браузеру «покажется», что шрифты нужно увеличить, то при отсутствии данного стиля iPhone может самопроизвольно изменить размер шрифта при изменении ориентации на альбомную. Перехват touch-событий Если использовать только click-события, забывая про touch, то мобильное устройство может «тормозить» c отрисовкой страницы (особенно при анимации с использованием DOM-элементов), ожидая, пока обработка touch завершится. Чтобы убрать задержку, необходимо вместо click-события «повесить» аналогичное touch-событие, а также сопутствующие события, такие как touchmove и touchend, и заблокировать их распространение (stopPropagation()) и реакцию по умолчанию (preventDefault()). Выделение при тапе При тапе (клике) на каком-либо элементе страницы Android-устройства стремятся выделить элемент жёлтой или синей заливкой, а iOS-устройства — серой. Нежелательная заливка убирается путем использования в CSS следующего кода: *, body { -webkit-tap-highlight-color: transparent; -webkit-focus-ring-color: rgba(0,0,0,0); outline: none; } В крайнем случае помогут перехват и блокирование всех touch-событий. Блокирование контекстного меню на картинках При удержании пальца на картинке практически все современные мобильные webview предлагают сохранить её. Если это не желательно, то блокировать контекстное меню можно при помощи CSS: img { -webkit-touch-callout: none; } Если это не поможет, то исключить подобные ситуации позволяет блокирование всех touch-событий, приходящихся на картинку. Блокирование нежелательных действий с сенсорным экраном Иногда некоторые действия с touch-экраном могут оказаться нежелательными, например скроллинг по горизонтали или уменьшение (увеличение) содержимого. Управлять доступностью подобных жестов позволяет стиль touchaction. Например, разрешить только скроллинг по вертикали можно так: 30
touch-action: pan-y; Следующее css-свойство добавит плавный скролл в блоках с overflow: scroll. Желательно добавлять это свойство везде, где внутри блока может возникать прокрутка, к примеру в мобильном меню. -webkit-overflow-scrolling: touch 2.7.2. Добавление плагина геолокации Добавим требуемый плагин для поддержки геолокации. Для этого из каталога проекта выполним команду: cordova plugin add cordova-plugin-geolocation В ответ Cordova сообщит об успешной установке плагина для всех установленных платформ (ios и browser): Installing "cordova-plugin-geolocation" for browser Installing "cordova-plugin-geolocation" for ios Adding cordova-plugin-geolocation to package.json Saved plugin info for "cordova-plugin-geolocation" to config.xml В результате будет сформирована следующая файловая структура в каталоге проекта, которая уже была представлена на рисунке 7 выше. Начинать работу с геолокацией можно только после возникновения события deviceready (это характерно для всех плагинов). Событие deviceready уже обсуждалось выше — это стартовая точка для работы с Cordova. Геолокация доступна через объект navigator.geolocation: document.addEventListener("deviceready", onDeviceReady, false); function onDeviceReady() { console.log("Объект navigator.geolocation готов к работе"); } У этого объекта доступны три метода. Метод getCurrentPosition() принимает три параметра. Первый (обязательный) параметр — функция, которая вызывается в случае успешного определения позиции, и которой передаётся объект position, инкапсулирующий всю информацию — широту, долготу и т. д. Второй (необязательный) параметр — функция обратного вызова на случай ошибки. Наконец, третий (необязательный) параметр содержит ряд опций, определяющих работу метода: function onSuccess(position) { alert( 'Широта: ' + position.coords.latitude + '\n' + 'Долгота: ' + position.coords.longitude + '\n' + 'Высота: '+ position.coords.altitude + '\n' + 'Точность: ' + position.coords.accuracy + '\n' + 'Точность по высоте: ' + position.coords.altitudeAccuracy + '\n' + 'Направление: ' + position.coords.heading + '\n' + 'Скорость: ' + position.coords.speed + '\n' + 'Время: ' + position.timestamp + '\n'); 31
}; function onError(error) { alert('code: ' + error.code + '\n' + 'message: ' + error.message + '\n'); } var opts = { enableHighAccuracy: true }; navigator.geolocation.getCurrentPosition(onSuccess, onError, opts); Параметр enableHighAccuracy — определяет точность вычисления позиции, если true, то позиция должна быть вычислена с помощью спутников (GPS), иначе могут использоваться менее точные сетевые методы. Другие два метода объекта navigator.geolocation работают в паре. Метод watchPosition() принимает те же самые параметры, что и метод getCurrentPosition(), однако функция onSuccess() будет вызываться с некоторой периодичностью, устанавливаемой в параметре frequency: var watchID = navigator.geolocation.watchPosition(onSuccess, onError, { maximumAge: 3000, frequency: 1000, timeout: 5000, enableHighAccuracy: true }); Параметр timeout определяет время ожидания ответа. Значения временных параметров задаются в миллисекундах. Параметр maximumAge — время устаревания значений геопозиции, сохранённых в кэше (в миллисекундах). Указание параметров enableHighAccuracy и maximumAge — необязательно. Параметр frequency, очевидно, следует указывать, если требуется периодичность вызова onSuccess(). Метод clearWatch() позволяет прекратить наблюдение: navigator.geolocation.clearWatch(watchID); 2.7.3. Отображение данных геолокации Сформируем HTML-файл, в котором предусмотрим заголовок, подзаголовок и содержательную часть — div-элементы с классами: header, subheader и content. В подзаголовочной части предусмотрим две кнопки — для старта (остановки) наблюдений за позицией и обновления показаний. Содержательная часть — ряд строк (div-элементы с классом row), каждая из которых предназначена для отображения одного из параметров геопозиции (широта, долгота и т. д.) и состоит из двух колонок (div-элементы с классом col) — для названия параметра и для его значения. Исключением является первая — статусная — строка. Описанный HTML-код — тело индексного файла (index.html) — имеет следующий вид: <div class="header"> <h1>Геолокация</h1> </div> <div class="subheader"> <div class="row"> <div class="col"> 32 Powered by TCPDF (www.tcpdf.org)
<button class="button center" id="watchButton"> Начать отслеживать геопозицию </button> </div> <div class="col"> <button class="button center" id="refreshButton"> Обновить показания </button> </div> </div> </div> <div class="content"> <div class="row"> <div class="col center"> <span id="status" class="color-accent bold"></span> </div> </div> <div id="results" class="hidden"> <div class="row"> <div class="col right"> <label class="bold">Широта: </label> </div> <div class="col left"> <span id="latitude" class="color-accent"> </span> </div> </div> <div class="row"> <div class="col right"> <label class="bold">Долгота: </label> </div> <div class="col left"> <span id="longitude" class="color-accent"> </span> </div> </div> <div class="row"> <div class="col right"> <label class="bold">Высота: </label> </div> <div class="col left"> <span id="altitude" class="color-accent"> </span> </div> </div> <div class="row"> <div class="col right"> <label class="bold">Точность: </label> </div> <div class="col left"> <span id="accuracy" class="color-accent"> </span> </div> </div> <div class="row"> <div class="col right"> <label class="bold">Точность по высоте: </label> </div> 33
<div class="col left"> <span id="altitudeAccuracy" class="color-accent"> </span> </div> </div> <div class="row"> <div class="col right"> <label class="bold">Направление: </label> </div> <div class="col left"> <span id="heading" class="color-accent"> </span> </div> </div> <div class="row"> <div class="col right"> <label class="bold">Скорость: </label> </div> <div class="col left"> <span id="speed" class="color-accent"></span> </div> </div> <div class="row"> <div class="col right"> <label class="bold">Время: </label> </div> <div class="col left"> <span id="timestamp" class="color-accent"> </span> </div> </div> </div> </div> Теперь требуется стилизовать должным образом представленные структурные элементы и их классы. Во-первых, позаботимся о том, чтобы тело HTML-документа занимало все доступное пространство: html, body { /* уберём отступы и поля */ margin: 0px; padding: 0px; /* размеры включают рамку */ box-sizing: border-box; height: 100%; overflow: hidden; } Также исключим ненужные для мобильных экранов эффекты, которые уже обсуждались выше: body { /* выделение при тапе */ -webkit-tap-highlight-color: transparent; -webkit-focus-ring-color: rgba(0,0,0,0); outline: none; -webkit-user-drag: none; 34
/* запрет выделения */ -moz-user-select: -moz-none; -o-user-select: none; -khtml-user-select: none; -webkit-user-select: none; user-select: none; /* запрет изменения размеров шрифтов */ -webkit-text-size-adjust: none; text-size-adjust: none; /* блокирование контекстного меню на картинках */ -webkit-touch-callout: none; /* среди всех жестов разрешается только скроллинг по вертикали */ -ms-touch-action: pan-y; touch-action: pan-y; } Это общие css-настройки, применимые для большинства мобильных платформ. Зададим также специфические для нашего приложения настройки: цвет текста и заднего плана, шрифт, а также flex-разметку для содержимого тела документа: body { display: -webkit-flex; display: flex; -webkit-flex-direction: column; flex-direction: column; color: #2d3e50; background-color: #f4f6f7; font-size: 14px; font-family: "Open Sans", sans-serif; } Стили для классов элементов первого уровня тела документа (это заголовок, подзаголовок и контент): .header { height: 74px; background-color: #2d3e50; } .header > h1 { margin: 27px 20px; font-size: 21px; overflow: hidden; text-align: left; text-overflow: ellipsis; white-space: nowrap; color: #ffffff; } .subheader { border-bottom: 1px solid rgba(45, 62, 80, 0.3); background-color: #ffffff; } 35
.content { -webkit-flex: 1; flex: 1; overflow-x: hidden; overflow-y: auto; /* скроллинг */ -webkit-overflow-scrolling: touch; } Для того чтобы div-элементы с классами row и col действительно выполняли роль строк и колонок, опять же воспользуемся flex-разметкой: .row { display: -webkit-flex; display: flex; padding: 5px; } .col { /* колонки одинаковой ширины */ -webkit-flex: 1; flex: 1; padding: 5px; } Для выравнивания содержимого колонок (влево, вправо, по центру) и выделения названий и значений параметров зададим стили: /* выравниваем */ .left { text-align: left; } .right { text-align: right; white-space: nowrap; text-overflow: ellipsis; } .center { text-align: center; } /* названия параметров */ .bold { font-weight: bold; } /* значения параметров */ .color-accent { color: #3598db; } Управляющие кнопки стилизуем следующим образом: .button { background-color: #ffffff; color: #2d3e50; margin: 0px; padding: 0px 12px; width: 100%; min-width: 50px; border: 1px solid rgba(45, 62, 80, 0.3); 36
border-radius: 2px; text-overflow: ellipsis; font-size: 14px; cursor: pointer; } Наконец, чтобы изначально скрыть блок с результатами наблюдений (id="results"), определим соответствующий класс: .hidden { display: none; } Результат описанного форматирования представлен на рисунке ниже: Рис. 11 Результат работы приложения на эмуляторе iPhone 5s 2.7.4. Функционирование приложения (index.js) Приложение реализовано в виде объекта app, в котором инкапсулирована вся функциональность. Во-первых, предусмотрен метод initialize(), основная задача которого — инициализация реакции на событие deviceready: var app = { watchID: null, // результат watchPosition() initialize: function() { document.addEventListener('deviceready', this.onDeviceReady.bind(this), false); }, onDeviceReady: function() { … }, … 37
}; app.initialize(); Таким образом, после возникновения события deviceready управление будет передано методу onDeviceReady(). Основная задача этого метода — предусмотреть реакции на нажатия управляющих кнопок: onDeviceReady: function() { // здесь можно подключить обработчики событий cordova, // например: 'pause', 'resume', и др. // 'pause' – приложение переведено в фоновый режим // document.addEventListener('pause', onPause, false); // 'resume' – приложение вышло из фонового режима // document.addEventListener('resume', onResume, false); var that = this; document.getElementById("watchButton").addEventListener("click", function() { that.handleWatch.apply(that, arguments); }, false); document.getElementById("refreshButton").addEventListener("click", function() { that.handleRefresh.apply(that, arguments); }, false); } Следует обратить внимание на то, что все методы вызываются в контексте объекта app, то есть this внутри методов ссылается на этот объект. Реализуется это стандартными приёмами: с помощью метода bind и замыканий. Метод onDeviceReady() привязывается к контексту app с помощью bind, а реакции handleWatch() и handleRefresh() «замыкаются» на переменную that. Обработка нажатия на кнопку «Начать отслеживать геопозицию»: handleWatch: function() { var that = this, button = document.getElementById("watchButton"); // если наблюдение уже ведётся if( that.watchID != null ) { that.setStatus();// очистка статусной строки // скроем блок с результатами наблюдений document.getElementById("results").classList.add("hidden"); // прерываем наблюдение navigator.geolocation.clearWatch(that.watchID); that.watchID = null; button.innerHTML = "Начать отслеживать геопозицию"; } else { this.setStatus("Ожидание данных геолокации..."); // Обновляем данные с периодичностью одна секунда var options = { frequency: 1000, timeout: 20000, enableHighAccuracy: true }; 38
that.watchID = navigator.geolocation.watchPosition(function() { that.onSuccess.apply(that, arguments); }, function() { that.onError.apply(that, arguments); }, options); button.innerHTML = "Прекратить отслеживать геопозицию"; } } Обработка нажатия на кнопку «Обновить показания»: handleRefresh: function() { var that = this, options = { enableHighAccuracy: true, timeout: 20000 }; that.setStatus("Ожидание данных геолокации ..."); navigator.geolocation.getCurrentPosition(function() { that.onSuccess.apply(that, arguments); }, function() { that.onError.apply(that, arguments); }, options); } Заметим, что в обоих обработчиках в случае успешного считывания данных геолокации управление передаётся функции onSuccess(), которая вызывается в контексте объекта app, и которой передаются аргументы как есть, без изменений, то есть объект position (с данными геолокации): var app = { watchID: null, // Конструктор приложения initialize: function() { document.addEventListener('deviceready', this.onDeviceReady.bind(this), false); }, onDeviceReady: function() { … }, handleRefresh: function() { … }, handleWatch: function() { … }, onSuccess:function(position) { // Выводим геолокацию. for (key in position.coords) document.getElementById(key).innerText = position.coords[key]; document.getElementById("timestamp").innerText = new Date(position.timestamp).toLocaleTimeString().split(" ")[0]; 39
this.setStatus(); document.getElementById("results").classList.remove("hidden"); }, onError: function (error) { this.setStatus('code: ' + error.code + '<br/>' + 'message: ' + error.message + '<br/>'); document.getElementById("results").classList.add("hidden"); }, setStatus: function (value) { if (!value) document.getElementById("status").innerHTML = " "; else document.getElementById("status").innerHTML = value; } }; app.initialize(); 2.7.5. Настройка приложения под iOS Начиная с десятой версии, iOS запрашивает у пользователя мобильного устройства разрешение на использование приложением данных геолокации, которые считаются конфиденциальными. При этом приложение должно предусмотреть объяснение необходимости доступа к конфиденциальным данным — строку, которая будет являться составной частью запроса на разрешение. Если соответствующая строка не предусмотрена, то в доступе к геолокации приложению будет отказано. Для определения объяснения в файле CordovaGeolocationApp-Info.plist приложения CordovaGeolocation должен быть задан параметр с именем NSLocationWhenInUseUsageDescription. Более предпочтительный путь — воспользоваться тегом edit-config в файле config.xml: <edit-config file="*-Info.plist" mode="merge" target="NSLocationWhenInUseUsageDescription"> <string>требуется доступ к гелокации для …</string> </edit-config> Кроме этого, для предотвращения сохранения данных WebStorage в облаках iClouds в файле config.xml также следует задать следующий параметр: <preference name="BackupWebStorage" value="local" /> 2.7.6. Окончательная сборка и установка приложения Предполагается, что уже выполнена и без ошибок сборка приложения для установленных платформ с помощью команд Cordova, которые обсуждались выше. В результате будут созданы подкаталоги проектов с именами платформ — platforms/название_платформы (например, platforms/ios или platforms/android). Каждый такой каталог содержит всё необходимое, чтобы открыть проект с помощью нативных средств разработки, например Xcode или Android Studio, и дальнейшую работу вести уже в рамках этих программ. 40
Для iOS достаточно дважды кликнуть файл с расширением xcodeproj, а для Android — запустить Android Studio и при открытии проекта указать соответствующий подкаталог — platforms/android. Для установки на устройство или симулятор существует альтернативный путь — команды Cordova. Для запуска на физическом устройстве (его следует подключить с помощью кабеля) выполнить команды: cordova run android —device cordova run ios --device Для запуска на симуляторе: cordova emulate android cordova emulate ios Предпочтительным, однако, является первый путь — использование нативных средств разработки, особенно для iOS, поскольку для этой платформы использование приведённых команд потребует установки дополнительных модулей ios-deploy и ios-sim: npm install -g ios-deploy npm install -g ios-sim Кроме этого, потребуется «ручная» настройка этих модулей: создание «вручную» профилей, сертификатов и т. д. Это довольно объёмная тема и требует отдельного обсуждения. 41
3. ИСПОЛЬЗОВАНИЕ ФРЕЙМВОРКОВ JAVASCRIPT 3.1. Обзор популярных web-фреймворков для мобильных платформ Разработка приложений на JavaScript без использования одного из многочисленных современных фреймворков возможна, но малоэффективна. Это в полной мере касается и гибридной разработки под webview и нативной под API той или иной мобильной платформы. Без привлечения подобных средств пришлось бы с нуля разрабатывать стандартные виджеты — элементы пользовательского интерфейса: списки, меню, гриды, тулбары и т. д., привязывать их к данным (которые могут храниться локально или на внешних серверах), реализовывать механизмы drag and drop или концепцию MVC (Model View Controller). Скорее всего, результат такой «монтёрской» разработки оказался бы не лучшим — не отвечающим современному уровню, особенно если речь идёт о приложениях с насыщенным интерфейсом (RIA — Rich Interface Application). Подчеркнём, что количество всевозможных JavaScript-фреймворков, известных и малоизвестных, популярных и менее популярных, достаточно велико, чтобы в них можно было легко потеряться разработчику, а их развитие настолько динамично, что делать подобные обзоры — занятие неблагодарное — ситуация может измениться прежде, чем читатель прочтёт эти строки. Некоторые из фреймворков ориентированы исключительно на разработку под браузер и малопригодны для мобильных устройств, отличающихся малыми размерами touch-экранов и использованием жестов. Эти отличия характерны не только для браузеров, но и для компонентов webview, используемых гибридными приложениями. С появлением технологий native-разработки на языках JavaScript и TypeScript появились и соответствующие фреймворки. Известны следующие лидеры, использующие указанную технологию — это компания Progress Sofware (Telerik) (https://www.progress.com/) и её NativeScript (https://www.nativescript.org), компания Appcelerator и её технология Titanium — Hyperloop (https://www.appcelerator.com/), а также компания Facebook и её технология React Native (https://facebook.github.io/reactnative/). Отметим, что NativeScript от Progress Sofware используется в известном фреймворке Angular NativeScript (https://angular.io/), а компания Progress приобрела Telerik, поэтому многие программные продукты, известные под брэндом Telerik, теперь идут под брэндом Progress. Фреймворки, как правило, включают некоторый базовый бесплатный набор инструментов и сервисов и дополнительные компоненты за дополнительную плату. Например, Appcelerator Inc. предоставляет: • IDE Titanium — интегрированную среду разработки, включающую: on-device debugger, live UI Editor, code analyzer, performance profiler; • App Designer для визуального формирования внешнего вида приложения, с богатой палитрой элементов интерфейса, которые легко перетянуть на рабочее поле приложения; 42
• API Builder — для сборки приложения под API платформы; • сервисы Push и Analytics. Упомянутый выше NativeScript — это модуль популярной платформы Node.js. Это бесплатный и открытый продукт. То же касается React Native — это также модуль node, бесплатный и открытый продукт от эко-сообщества node, который является своеобразным «мобильным» продолжением известного web-фреймворка React.js. Отметим, что для развёртывания приложения на реальном устройстве, в конечном счёте, потребуется нативная среда программирования под iOS и Android. Следует также обратить внимание, что нативная разработка в отличие от гибридной не опирается на HTML и модель DOM (Document Object Model) — на стадии выполнения обращение происходит к оригинальному API платформы. Как это работает, подробно обсуждалось в п. 1.2, посвящённом Native Script. Напротив, для гибридных приложений подходит, в принципе, любой webфреймворк, поскольку в конечном счёте работа происходит с HTML, DOM и CSS. Конечно, желательно при этом учитывать особенности мобильных экранов, о которых уже было сказано выше. Однако разработчики популярных webфреймворков пошли дальше и разработали версии именно под мобильные браузеры и, более того, — под гибридную разработку на основе Cordova. Среди таких фреймворков можно назвать, например, Ionic (https://ionicframework.com/), jQuery Mobile (https://jquerymobile.com), Sencha Ext JS Modern и Sencha Touch. Также упомянем Kendo UI от всё той же компании Progress — это клиентский фрейморк, в основе которого лежат jQuery и набор виджетов (гриды, графики/диаграммы, выпадающий список и т. д.). В данном фреймворке максимально используются такие технологии, как HTML5 и CSS3 (при этом обеспечивается поддержка достаточно старых браузеров). Также поддерживаются привязка данных, шаблоны, анимация, drag-and-drop и многое другое. Разработчики обещают высокую скорость работы, поддержку мобильных устройств с touch-интерфейсами, а также агрессивную политику развития продукта. Ну и, конечно, поддержка такой мощной компании, как Progress (Telerik), говорит о многом. В заключение ещё раз отметим, что развитие фреймвоков в настоящее время — это чрезвычайно динамичный и до конца не устоявшийся процесс: появляются новые фреймворки, некоторые теряют популярность, некоторые интегрируются с другими. 3.2. Sencha Ext JS 3.2.1. Обзор Sencha Ext JS В настоящее время Sencha Ext JS [12, 13, 14, 17] включает полный набор инструментов, который охватывает разработку, тестирование и дизайн. Центральным звеном является комплект для разработки программного обеспече43
ния — Sencha Ext JS SDK (Software Development Kit), который придётся купить за внушительную сумму с целью коммерческого использования. Однако в учебных целях или для создания продукта с открытым исходным кодом можно скачать GPL SDK бесплатно (https://www.sencha.com/legal/gpl/). Использование этого продукта регламентируется известной лицензией GPL — GNU General Public License (Стандартная общественная лицензия GNU), ознакомится с которой можно здесь — https://www.gnu.org/licenses/gpl.html. Центральной частью SDK является библиотека JavaScript, ядро которой привносит оригинальный синтаксис описания классов и фактически делает эту библиотеку действительно объектно-ориентированной. Остальная часть — это описание классов библиотеки, стили и графика встроенных дизайнерских тем. Объём SDK внушительный, так, версия 6.2.0 занимает приблизительно половину гигабайта в 43 203 файлах. В дополнение к GPL SDK бесплатно распространяется полнофункциональный инструмент командной строки Sencha Cmd (https://www.sencha.com/ products/extjs/cmd-download/). Этот инструмент класса Application Lifecycle Tool позволяет согласно этому определению выполнить полный спектр задач. • Генерация кода. Sencha Cmd позволяет генерировать код каркаса приложения, модели данных, представления или контроллера. Здесь речь идёт о современном подходе к программированию интерфейса — MVC (Моdel View Controller). Этот архитектурный шаблон позволяет разделить код приложения на составляющие: модель (Model), представление (View), контроллер (Controller). Model — описывает формат используемых данных и может выполнять ряд вспомогательных функций, например проверку данных. View — отображает данные. Разные представления могут отображать одни и те же данные разными путями (например, таблицы или графики). Controller — следит за событиями и управляет моделью и представлением. • Web-сервер. Sencha Cmd включает в себя легковесный web-сервер, который можно использовать в качестве локального web-сервера при разработке. • Сборка и распространение (Build and Deployment). Sencha Cmd выполняет одну из основных своих задач — различные варианты сборки приложений. Типа Development — в процессе разработки, и типа Production — окончательный вариант для распространения. Сборка подхватывает только требуемую для приложения часть SDK, сжимает (минифицирует) код, транслирует файлы SCSS (Sassy CSS) в стилевые таблицы CSS. Современные версии Sencha Cmd обходятся без привлечения Rubby для трансляции файлов SCSS в CSS. • Управление рабочим пространством (workspace). Sencha Cmd помогает создавать и поддерживать рабочую среду, позволяющую обмениваться инфраструктурой, пакетами или пользовательским кодом между несколькими приложениями. Для вёрстки кода в известных IDE, например Aptana, Eclipse, VisualStudio, разработаны плагины для поддержки проектов Sencha Ext JS (https://www.sencha.com/products/plugins/). Впрочем, вполне подойдёт любой текстовый редактор, например sublime (http://www.sublimetext.com) или notepad++ (https://notepad-plus-plus.org). 44
Представленный перечень бесплатных инструментов достаточен для полноценной разработки приложений, но, конечно, является неполным. Однако всё остальное распространяется на коммерческой основе. Тем не менее для полноты представления дадим краткую характеристику основным компонентам платного инструментария. Средство визуальной разработки — Sencha Architect. Архитектор автоматически генерирует оптимизированный код приложения и устраняет человеческий фактор — ошибки, которые часто возникают при ручном кодировании. Sencha Themer позволяет быстро и легко создавать собственные дизайнерские темы приложений исключительно с помощью графических инструментов — без написания кода. Как уже отмечалось выше, Sencha использует для описания стилей своих предустановленных дизайнерских тем язык Sassy CSS, обладающий перед традиционным CSS рядом преимуществ. Например, SCSS допускает сквозную настройку параметров встроенных тем с помощью переменных, которые подробно описаны в документации. Sencha Inspector — это инструмент отладки, который обеспечивает прямой доступ к компонентам, классам и объектам Sencha. Он позволяет анализировать и контролировать код на наличие проблем, в том числе излишнюю вложенность компонент, эффективность схем расположения компонент (layouts) и многое другое, что может существенно повысить производительность приложения. Stencils — это полный набор элементов пользовательского интерфейса, который дизайнеры могут использовать в Adobe Illustrator, Sketch, Balsamiq, который также доступен в формате SVG/PNG для использования в других программах. Sencha Stencils предназначен для помощи дизайнерам в общении с разработчиками. Sencha Test является исчерпывающим решением для тестирования приложений Ext JS, позволяющим повысить качество приложений и сократить время и затраты на тестирование. Для тестирования фрагментов кода также можно использовать своеобразную «песочницу», которая вынесена в сеть (https://fiddle.sencha.com/) и предоставляется бесплатно. Особо следует упомянуть бесплатную версию фреймворка — Ext JS Community Edition (Sencha Ext JS CE), предназначенную для стартапов, студентов или любителей. Это решение делает доступной законную продажу программного обеспечения, разработанного на основе Sencha Ext JS CE, для частных лиц или организаций, которые имеют доход менее десяти тысяч долларов или менее пяти разработчиков. Работа с этой версией Ext JS ведётся с помощью модулей node.js, которые становятся доступными в закрытом хранилище (https://sencha.myget.org/) после регистрации на сайте компании (https://www.sencha.com/products/ extjs/communityedition/) посредством менеджера npm. По всей видимости, Sencha заканчивает распространение GPL SDK на версии 6.2.0, более старшие версии будут распространяться исключительно в формате Community Edition. Подготовка рабочего места на основе GPL версии Ext JS SDK и версии Community Edition различается, далее рассмотрим оба варианта. 45
3.2.2. Технология работы с GPL-версией Ext JS SDK и Sencha Cmd Прежде всего следует скачать GPL-версию SDK с официального сайта — https://www.sencha.com/legal/gpl/ (потребуется указать электронную почту, на которую будет выслана ссылка для скачивания). Отметим, что GPL-версии можно найти в неофициальных источниках, например на github.com, или попытаться скачать по ссылке http://cdn.sencha.com/ext/gpl/ext-6.2.0-gpl.zip. Далее можно организовать место для хранения различных версий SDK, например каталог sencha-sdks, где, в свою очередь, организовать подкаталоги ext-6.2.0, ext-6.2.1 и т. д. Более того, можно указать это хранилище в конфигурации Sencha Cmd, выполнив команду: sencha config --prop sencha.sdk.path=sencha-sdks --save Если ориентироваться исключительно на GPL-версию 6.2.0, которая, вероятно, — последняя, то можно и не придерживаться подобной организации хранилищ SDK и настройки конфигурации Sencha Cmd, а развернуть архив в любом каталоге, например ext-6.2.0 в корне диска. Наконец, следует скачать Sencha Cmd из официального источника — https://www.sencha.com/products/extjs/cmd-download/. При этом будет предложено два варианта загрузки: • DOWNLOAD with jre included (то есть с java runtime environment), • DOWNLOAD without jre included (то есть без java runtime environment). Если на компьютере уже установлена рабочая среда для работы javaприложений (java runtime environment (jre)), то можно выбрать второй вариант, без java runtime environment, который меньше по размеру. Первый вариант будет функционировать в любом случае. Однако заметим, что если планируется использовать версию Ext JS CE, то jre всё равно придётся установить (https://www.java.com/ru/). Версии Mac OS X 10.6 и старше имеют собственную версию Java, последняя — это Java 6, которая в настоящее время устарела, и которую по этой причине придётся удалить. Более новые версии Mac OS X не имеют собственных версий Java. Таким образом, для использования Ext JS CE в любом случае потребуется установка Java. Наконец, следует обратить внимание на следующую особенность: установки только jre — недостаточно. Оказывается, Sencha Cmd использует команду java, которая обычно доступна только при установке jdk (java development kit). 3.2.2.1. Создание рабочего пространства С помощью Sencha Cmd возможны различные варианты развёртывания и использования загруженного SDK. Остановимся на варианте организации рабочего пространства (workspace), в рамках которого можно вести разработку нескольких приложений. Преимущество такого подхода — единое место для всех разработок, которые при этом могут использовать общие ресурсы (например, SDK). 46
Сначала следует создать рабочее пространство, а затем уже приложение в нём. Приложений в рабочем пространстве может быть несколько. Общий синтаксис команды выглядит следующим образом: sencha -sdk /path/to/sdk generate workspace /path/to/workspace Например, создать рабочее пространство в текущей директории можно так: sencha -sdk /ext-6.2.0 generate workspace ./WRKSPC Во время этой операции Sencha Cmd скопирует все необходимые части SDK в директорию ext рабочего пространства. Результат — следующее содержимое текущей директории: Рис. 12 Содержимое рабочего пространства сразу после его создания Особого внимания заслуживает файл workspace.json, в котором в формате json представлена организация рабочего пространства и оперативно фиксируются все изменения в этом пространстве. Так, например, будут указаны версия и месторасположение скопированного фреймворка: "frameworks": { "ext": { "path":"ext", "version":"6.2.0.981" } } В данном случае фреймворк и SDK — это одно и то же. 3.2.2.2. Справочная система Sencha Cmd Sencha Cmd очень удобно устроена в плане получения справочной информации. Для получения своеобразного оглавления справки набираем: sencha help Результат — общий обзор команды с перечислением всех её опций, категорий и подкоманд в категориях: Sencha Cmd provides several categories of commands and some global switches. In most cases, the first step is to generate an application based on a Sencha SDK such as Ext JS or Sencha Touch: sencha -sdk /path/to/sdk generate app MyApp /path/to/myapp Sencha Cmd supports Ext JS 4.1.1a and higher and Sencha Touch 2.1 and higher. 47
To get help on commands use the help command: sencha help generate app For more information on using Sencha Cmd, consult the guides found here: http://docs.sencha.com/cmd/ Options * --beta, -be - Enable beta package repositories * --crash-log-dir, -cr - Sets the directory to store crash logs. If unspecified, logs are written to the process cwd. * --cwd, -cw - Sets the directory from which commands should execute * --debug, -d - Sets log level to higher verbosity * --info, -i - Sets log level to default * --nologo, -n - Suppress the initial Sencha Cmd version display * --plain, -pl - enables plain logging output (no highlighting) * --quiet, -q - Sets log level to warnings and errors only * --sdk-path, -sd - The location of the SDK to use for non-app commands * --strict, -st - Treats warnings as errors, exiting with error if any warnings are present * --time, -ti - Display the execution time after executing all commands Categories * app - Perform various application build processes * compile - Compile sources to produce concatenated output and metadata * cordova - Quick init Support for Cordova * diag - Perform diagnostic operations on Sencha Cmd * framework - Commands to manage frameworks in the current workspace * fs - Utility commands to work with files * generate - Generates models, controllers, etc. or an entire application * manager - Commands for interacting with Sencha Web Application Manager. * manifest - Extract class metadata * package - Manages local and remote packages * phonegap - Quick init support for PhoneGap * repository - Manage local repository and remote repository connections * template - Commands for working with templates * theme - Commands for low-level operations on themes * web - Manages a simple HTTP file server * workspace - Commands to perform actions on the current workspace Commands * ant - Invoke Ant with helpful properties back to Sencha Cmd * audit - Search from the current folder for Sencha frameworks and report their license * build - Works as an alias of sencha app build or sencha package build * config - Load a properties file or sets a configuration property * help - Displays help for commands * js - Executes arbitrary JavaScript file(s) * switch - Manage the active Sencha Cmd version * upgrade - Upgrades Sencha Cmd * which - Displays the path to the current version of Sencha Cmd 48 Powered by TCPDF (www.tcpdf.org)
Если потребуется более подробная информация о той или иной категории или подкоманде, то после help достаточно их указать. Например, для получения всех возможностей категории generate следует набрать: sencha help generate В ответ получим все возможности категории generate: This category contains code generators used to generate applications as well as add new classes to the application. Commands * app - Generates a starter application * controller - Generates a Controller for the current application * form - Generates a Form for the current application (Sencha Touch Specific) * model - Generates a Model for the current application * package - Generates a starter package * profile - Generates a Profile for the current application (Sencha Touch Specific) * theme - Generates a theme page for slice operations (Ext JS Specific) * view - Generates a View for the current application (Ext JS Specific) * workspace - Initializes a multi-app workspace Если теперь нас интересует более подробная информация о генерации рабочего пространства (подкоманда workspace в категории generate), то набираем: sencha help generate workspace В ответ получим полное описание подкоманды генерации рабочего пространства: This command generates a workspace for managing shared code across pages or applications. Options * --force, -fo - Forces re-extraction of framework into workspace. * --full, -fu - Enables full mode, which includes the .sencha folder. * --minimal, -mi - Enables minimal mode (no .sencha folder). * --path, -pa - Sets the target path for the workspace Syntax sencha generate workspace [options] [path] Таким образом, даже при минимальных знаниях можно добраться до требуемой опции команды Sencha Cmd, которая, очевидно, обладает очень большим потенциалом. Правда, при этом желательно знание английского языка, чтобы не тратить время на перевод. 49
3.2.2.3. Создание приложения Для генерации стартового каркаса приложения следует перейти в каталог рабочего пространства и дать команду: sencha -sdk ext generate app -modern MyAppName ./MyAppPath Здесь прежде всего указываем SDK, которое будет использовано для создания стартового шаблона приложения. При этом ext — означает точку входа с описанием расположения SDK в файле workspace.json. Пример такого описания представлен выше. В рамках одного рабочего пространства допускается иметь несколько версий SDK, которые, конечно, будут представлены в workspace.json с уникальными названиями описаний их месторасположения. Далее в команде указывается, что следует сгенерировать приложение modern (точнее — приложение, использующее modern toolkit). Sencha Ext JS поддерживает два вида приложений: • modern toolkit — набор интерфейсных компонент для экранов мобильных устройств, управляемых жестами; • classic toolkit — для экранов настольных компьютеров. Возможен также вариант универсального приложения, подходящего для любых устройств. Очевидно, для приложений под мобильные платформы следует выбирать modern toolkit. Наконец, два последних параметра указывают имя приложения (MyAppName) и директорию (MyAppPath), где это приложение будет сгенерировано, о чём в файле workspace.json после выполнения команды будет сделана соответствующая запись: /** * An array of the paths to all the applications present in this workspace */ "apps": [ "MyAppPath" ] Созданное таким образом приложение послужит хорошей основой — каркасом будущей разработки. Рабочее пространство в результате выполнения команды будет иметь следующую структуру (см. рис. 13). В рамках одного рабочего пространства, как уже было сказано, можно сгенерировать и вести разработку нескольких приложений, которые будут совместно использовать общие ресурсы: sencha -sdk ext generate app -classic MyApp1 ./app1 sencha -sdk ext generate app -modern MyApp2 ./app2 Если требуется удалить приложение, то сделать это с помощью одной команды не удастся. Предположим, требуется удалить приложение MyAppName. С этой целью сначала следует удалить все каталоги, где это приложение встречается, в нашем случае — это MyAppPath, затем в каталоге build подката50
логи development/MyAppName и temp/development/MyAppName. После этого для адекватной зачистки конфигурационного файла рабочего пространства (workspace.json) из каталога рабочего пространства следует выполнить команду: sencha workspace cleanup Рис. 13 Содержимое рабочего пространства сразу после создания приложения с именем MyAppName в каталоге MyAppPath 3.2.2.4. Сборка и запуск приложения в браузере Конечно, нашими основными целями являются интеграция созданного приложения с платформой Cordova, сборка, запуск и отладка этого приложения с помощью нативной среды программирования (Xcode или Android Studio) на эмуляторе или устройстве. Тем не менее обсудим запуск приложения в браузере. Сразу оговоримся, что сделать это возможно, только если приложение не обращается к услугам плагинов Cordova, которые браузер в большинстве случаев не поддерживает. Предварительно следует выполнить сборку приложения с помощью команды Sencha, которая должна быть выполнена из каталога приложения (MyAppPath): sencha app build В результате в каталоге build/production/MyAppName будет собрана production-версия приложения, в которой будут реализованы все оптимизирующие процедуры. Версия для отладки будет доступна в исходном каталоге MyAppPath. Работу той или иной версии приложения можно наблюдать, указав в адресной строке браузера соответствующий каталог (предполагается, что web-серверу доступно рабочее пространство). Можно также воспользоваться Sencha Cmd, которая позволяет запустить собственный web-сервер: sencha app watch 51
Команду выполняем из каталога приложения MyAppPath. Сервер будет слушать порт 1841, и, соответственно, приложение будет доступно по адресу http://localhost:1841/MyAppPath, как показано на рисунке ниже. Productionверсия приложения будет доступна по адресу http://localhost:1841/build/ production/MyAppName. Рис. 14 Так будет выглядеть стартовое приложение в браузере. Для просмотра production-версии в адресной строке следует набрать http://localhost:1841/build/production/MyAppName 3.2.3. Технология работы с Ext JS CE 3.2.3.1. Подготовка среды Как уже было сказано выше, для работы с версией Community Edition используются модули (пакеты) платформы node.js, установку (удаление) которых выполняет менеджер npm. Таким образом, для работы потребуется платформа node.js. Кроме этого, однозначно потребуется установка java development kit, поскольку Ext JS CE, в отличие от Sencha Cmd, не поставляется со встроенной поддержкой Java. Для версии 6.7 фреймворка Ext JS CE необходима 8-я версия jdk. Ссылки для скачивания указанных программ представлены выше. После установки проверяем наличие jdk — вводим в терминале команду: java -version В ответ получим: java version "1.8.0_211" Java(TM) SE Runtime Environment (build 1.8.0_211-b12) Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode) Версия node.js должна быть не старше 10-й версии. Проверяем аналогично — выполняем команду: node --version 52
Получим ответ: v10.15.3 Для получения доступа к приватному хранилищу npm-пакетов Sencha следует заполнить форму, предложенную для регистрации по адресу https://www.sencha.com/products/extjs/communityedition/. На самом деле, это приватное хранилище расположено на портале sencha.myget.org, где в случае успешной регистрации будет создана учётная запись. Учётная запись будет совпадать с электронной почтой, в которой символ @ заменён на две точки. В ответ на указанную почту придёт информационное сообщение от Sencha, а также отдельное электронное письмо с портала MyGet для сброса (установки) пароля. Получив пароль, можно подключиться к репозиторию: npm login --registry=https://sencha.myget.org/F/community/npm/ --scope=@sencha Эта команда потребует ввести учётную запись и пароль: Username: xxxxx..mail.ru Password: Email: (this IS public) xxxxx@mail.ru Logged in as xxxxx..mail.ru to scope @sencha on https://sencha.myget.org/F/community/npm/. В результате будет создан скрытый файл .npmrc, в котором в зашифрованном виде будут зафиксированы полномочия пользователя. При обращении к репозиторию теперь не требуется повторный ввод учётной записи и пароля. Если по каким-то причинам необходимо подключиться снова, то этот файл следует удалить. Для генерации приложений в репозитории Sencha предусмотрена команда ext-gen — это модуль node.js, и для его глобальной установки обращаемся к менеджеру пакетов npm: npm install -g @sencha/ext-gen Модуль ext-gen позволяет использовать три различных шаблона генерируемого приложения: moderndesktop, universalmodern и moderndesktopminimal. Все шаблоны используют modern toolkit (выше уже пояснялось различие между modern и classic toolkit). Второй шаблон предназначен для универсальных приложений, которые могут быть собраны как под небольшие экраны гаджетов, так и большие экраны настольных компьютеров (phone или desktop-сборка). Первый и последний шаблоны — для desktop-приложений, последний (moderndesktopminimal) — минимальный шаблон. Полный набор возможностей команды ext-gen можно получить, выполнив эту команду с опцией help: ext-gen --help 53
В ответ получим описание команды: Sencha ExtGen v6.7.0 Community Edition - The Ext JS code generator Quick Start: ext-gen app -a ext-gen app (-h) (-d) (-i) (-t 'template') (-m 'moderntheme') (-n 'name') -h --help -d --defaults -i --interactive play) -t --template -m --moderntheme -n --name -v --verbose show help (no parameters also shows help) show defaults for package.json run in interactive mode (question prompts will disname for Ext JS template used for generate theme name for Ext JS modern toolkit name for Ext JS generated app verbose npm messages (for problems only) Examples: ext-gen app --template universalmodern --moderntheme theme-material -name CoolUniversalApp ext-gen app --interactive ext-gen app -a -t moderndesktop -n ModernApp You can select from the following Ext JS templates provided by Sencha ExtGen Modern Templates: moderndesktop This template contains 1 profile, configured to use the modern toolkit of Ext JS for a desktop application universalmodern This template contains 2 profiles, 1 for desktop and 1 for mobile. Both profiles use the modern toolkit. modern themes: theme-material, theme-ios, theme-neptune, theme-triton 3.2.3.2. Создание приложения Следуя описанию команды ext-gen, представленному выше, воспользуемся самым простым — автоматическим вариантом генерации приложения: ext-gen app -a Все настройки будут выбраны автоматически. В результате в текущем каталоге (в котором была выполнена генерация) будет создан подкаталог my-app, где и будет развёрнуто moderndesktop-приложение. В процессе развёртывания из сети загружаются и устанавливаются все необходимые пакеты платформы node.js. Пакеты Sencha будут загружаться из приватного хранилища sencha.myget.com, для доступа к которому была выполнена предварительная авторизация. Данные для доступа хранятся в скрытом файле .npmrc и используются незаметно для пользователя. В macOS заставить Finder показать скрытые файлы 54
можно с помощью комбинации клавиш shift + cmd +. . Развёртывание приложения сопровождается следующими информационными сообщениями: Sencha ExtGen v6.7.0 Community Edition - The Ext JS code generator Defaults for Ext JS app: appName:MyExtGenApp template:moderndesktop modernTheme:theme-material Defaults for package.json: packageName:my-app version:0.0.1 description:my-ext-gen-app description for Ext JS app MyExtGenApp repositoryURL:https://github.com/ keywords:"Ext JS","Sencha","HTML5" authorName:Sencha, Inc. license:ISC bugsURL:https://github.com/ homepageURL:http://www.sencha.com ℹ 「ext」: ext-gen: ext-gen: ℹ 「ext」: ext-gen: ℹ 「ext」: ext-gen: i [ext]: ext-gen: ℹ 「ext」: /Users/npv/my-app created package.json created for my-app webpack.config.js created for my-app npm install -s started for my-app @sencha/ext-core v6.7.0 is installed Community Edition License ----------------------------------------------------------------------------------------This version of Sencha Ext JS is licensed as Community Edition. See http://www.sencha.com/legal/sencha-sdk-software-license-agreement (Community License) for license terms. ----------------------------------------------------------------------------------------i [ext]: ext-gen: @sencha/ext v6.7.0 is installed i [ext]: ext-gen: @sencha/ext-modern-theme-base v6.7.0 is installed [fsevents] Success: "/Users/npv/myapp/node_modules/fsevents/lib/binding/Release/node-v64-darwinx64/fse.node" is installed via remote ℹ 「ext」: platform-install: @sencha/cmd v6.7.0 installed + @sencha/cmd-macos@6.7.0 added 5 packages from 3 contributors and audited 6 packages in 4.901s found 0 vulnerabilities ℹ 「ext」: platform-install: npm install -s @sencha/cmd-macos@6.7.0 installed i [ext]: ext-gen: @sencha/ext-modern v6.7.0 is installed Thanks for using Webpack! Please consider donating to our Open Collective to help us maintain this package. 55
Donate: https://opencollective.com/webpack/donate added 753 packages from 427 contributors and audited 8920 packages in 26.241s found 1 moderate severity vulnerability run `npm audit fix` to fix them, or `npm audit` for details ℹ 「ext」: ext-gen: npm install -s completed for my-app ℹ 「ext」: ext-gen: Your Ext JS project is ready type "cd my-app" then "npm start" or "npm run desktop" to run the development build and open your new application in a web browser Как видно из этих сообщений, разворачивается moderndesktopприложение с именем MyExtGenApp и дизайнейрской темой theme-material. При этом устанавливается довольно внушительное количество пакетов, в том числе популярный инструментарий webpack, используемый для frontend-сборки приложений. Структура каталога развёрнутого приложения представлена на рисунке ниже. Рис. 15 Файловая структура приложения Sencha Ext JS CE. Разработка приложения выполняется в каталоге app. Конфигурационный файл приложения — app.json. Конфигурация платформы node.js хранится в файле package.json. В этом же файле можно найти скрипты npm для быстрого старта наиболее востребованных команд Параметры приложения хранятся в конфигурационном файле app.json. Например, параметры сборки — builds: { … "builds": { 56
"desktop": { "toolkit": "modern", "theme": "theme-material", "sass": { "generated": { "var": "${build.id}/sass/save.scss", "src": "${build.id}/sass/save" } } } }, … } Если бы развернули универсальное приложение — universalmodern, то его параметры сборки были бы следующие: { … "builds": { "desktop": { "toolkit": "modern", "theme": "theme-material", "sass": { "generated": { "var": "${build.id}/sass/save.scss", "src": "${build.id}/sass/save" } } }, "phone": { "toolkit": "modern", "theme": "theme-material", "sass": { "generated": { "var": "${build.id}/sass/save.scss", "src": "${build.id}/sass/save" } } } } … } Для универсального приложения предусмотрены два варианта сборки: desktop и phone. Конечно, параметры можно менять согласно предпочтениям пользователя. Это не все параметры — краткое описание всех параметров можно найти непосредственно в файле app.json в комментариях или подробное описание в документации (https://docs.sencha.com/cmd/6.7.0/guides/microloader.html). 3.2.3.3. Сборка и запуск приложения в браузере Самый простой способ сборки и запуска приложения в браузере, согласно рекомендациям, полученным в процессе генерации (смотри выше), следующий: переходим в каталог приложения, в нашем случае это каталог my-app (cd myapp), и выполняем команду: 57
npm start Будет выполнена сборка приложения, и результат будет автоматически загружен в браузере, о чём пользователю будут выданы соответствующие информационные сообщения: > my-app@0.0.1 start /Users/npv/my-app > cross-env-shell npm run dev > my-app@0.0.1 dev /Users/npv/my-app > webpack-dev-server --env.environment=development ℹ 「ext」: ext-webpack-plugin v6.7.0, Ext JS v6.7.0 Community Edition, Sencha Cmd v6.7.0.64, webpack v4.32.2 ℹ 「ext」: Building for development ℹ 「ext」: Treeshake is false ℹ 「wds」: Project is running at http://0.0.0.0:1962/ ℹ 「wds」: webpack output is served from / ℹ 「wds」: Content not from webpack is served from ./ ℹ 「wds」: 404s will fallback to /index.html ℹ 「ext」: Building Ext bundle at: ./ ℹ 「ext」: Processing Build Descriptor : desktop (development environment) ℹ 「ext」: Loading compiler context ℹ 「ext」: Loading app json manifest... ℹ 「ext」: Appending content to /generatedFiles/bootstrap.js ℹ 「ext」: Writing content to /generatedFiles/desktop.json ℹ 「ext」: Writing content to /build/temp/development/MyExtGenApp/slicertemp/bootstrap.json ℹ 「ext」: Writing content to /build/temp/development/MyExtGenApp/slicertemp/bootstrap.js ℹ 「ext」: Writing content to /build/development/MyExtGenApp/generatedFiles/desktop.json ℹ 「ext」: Fashion build complete for /build/temp/development/MyExtGenApp/sass/MyExtGenApp-all.scss ℹ 「ext」: Fashion build completed in 2.823 sec. ℹ 「ext」: Appending content to /generatedFiles/bootstrap.js ℹ 「ext」: Writing content to /generatedFiles/desktop.json ℹ 「ext」: Loading Build Environment ℹ 「ext」: Waiting for changes... ℹ 「ext」: Opening browser at http://localhost:1962 ℹ 「ext」: Completed ext-webpack-plugin processing ℹ 「wdm」: Built at: 2019-06-03 11:20:07 ℹ 「wdm」: Compiled successfully. Сборку подхватит и будет отслеживать webpack-dev-server. Из сообщений видно: порт web-сервера — 1962, тип сборки — development, указаны расположение скомпилированных файлов и другие характеристики. Отметим, что эти настройки webpack-dev-server берёт из файла webpack.config.js, а их подробное описание (если необходимо) можно найти здесь — https://webpack.js.org/. Приложение будет запущено в браузере, используемом системой по умолчанию 58
или указанном в конфигурационном файле webpack.config.js. Результат показан на рисунке ниже: Рис. 16 Так выглядит стартовое moderndesktop-приложение, сгенерированное командой ext-gen app -a Заметим, что при запуске представленной команды (npm start) используется так называемый механизм npm-скриптов. В файле package.json (основной файл, описывающий наше приложение, как проект платформы node.js) можно найти поле scripts, в котором раскрываются эта и другие наиболее полезные команды: { … "scripts": { "clean": "rimraf build", "start": "cross-env-shell npm run dev", "dev": "webpack-dev-server --env.environment=development", "prod": "webpack-dev-server --env.environment=production -env.treeshake=true", "build": "cross-env-shell npm run clean && cross-env webpack -env.environment=production --env.treeshake=true", "desktop": "webpack-dev-server --env.profile=desktop -env.environment=development", "desktop-v": "webpack-dev-server --env.profile=desktop -env.environment=development", "build-desktop-testing": "npx sencha app build desktop testing", "build-desktop-production": "npx sencha app build desktop production", 59
"createview": "cross-env-shell npx sencha generate viewpackage $npm_package_extbuild_defaultprofile $VIEW" } … } Можно выполнить любую команду, набрав npm run и далее условное название команды, например prod: npm run prod Будет выполнена команда, «зашитая» в поле prod (выделено крупным шрифтом), которая выполнит production-сборку приложения. Как это происходит? При запуске npm run в переменную окружения path добавляется путь к node_modules/.bin. По этому пути расположены исполняемые файлы, которые могут быть использованы при раскрытии команд. Например, здесь можно найти исполнимый файл webpack-dev-server, который задействован в нашем случае: ls -la webpack-dev-server lrwxr-xr-x 1 npv staff 47 2 июн 14:51 webpack-dev-server -> ../webpack-dev-server/bin/webpack-dev-server.js Как видно из представленного листинга команды ls (команды просмотра содержимого каталога в UNIX-подобных системах) — это символическая ссылка на файл webpack-dev-server.js в одноимённом модуле. 3.2.3.4. Устранение неполадок Если по каким-то причинам что-то пошло не так, то для устранения неполадок можно предпринять ряд действий, описанных в документации (https://docs.sencha.com/extjs/6.7.0-CE/). Сброс системы менеджера пакетов npm. Во-первых, следует сбросить учетные данные, сохраненные менеджером пакетов npm при подключении к приватному хранилищу Sencha, удалив файл .npmrc: rm ~/.npmrc Заметим, что команда в таком виде выполняется в unix-подобных системах, где удаляемый файл — скрытый и расположен в домашнем каталоге пользователя. Расположение этого файла иное в Windows. Далее необходимо очистить кэш npm: npm cache clean -—force Очистить кэш можно, выполнив удаление всего каталога .npm: rm -rf ~/.npm Конечно, после этих действий потребуется повторная авторизация в приватном репозитории Sencha: 60
npm login --registry=https: //sencha.myget.org/F/community/npm/ --scope=@sencha А также переустановка пакета ext-gen: npm uninstall -g @sencha/ext-gen npm install -g @sencha/ext-gen Сброс проекта. По-новой переустановить все пакеты проекта (если такой уже есть), удалив подкаталог node_modules: rm -rf node_modules После чего снова загрузить все зависимости, выполнив команду: npm install Заметим, что установка (удаление) модуля ext-gen, а также кэша npm требует использования полномочий root-пользователя (а в Windows — системного администратора). Вместе с тем работа с ext-gen ведётся с правами обычного пользователя, и при этом может не хватить прав для пересоздания кэша npm. Если возникнут характерные сообщения нарушения прав доступа, то возможное решение — сделать доступным кэш npm всем пользователям: chmod -R a+rwX ~/.npm Возможны и иные причины неполадок в работе Sencha Ext JS CE, которые не описаны в документации, устранение которых потребует знаний системы или обращения к форуму пользователей (https://www.sencha.com/forum/). С подобной ситуацией пришлось столкнуться во время экспериментов с ext-gen, когда после успешной генерации проекта ext-gen отказывался его собрать и запустить командой npm start. При этом генератор просто «падал» с сообщением «Abort trap: 6»: npm start > my-app@0.0.1 start /Users/npv/my-app > cross-env-shell npm run dev > my-app@0.0.1 dev /Users/npv/my-app > webpack-dev-server --env.environment=development ℹ 「ext」: ext-webpack-plugin v6.7.0, Ext JS v6.7.0 Community Edition, Sencha Cmd v6.7.0.64, webpack v4.30.0 ℹ 「ext」: Building for development ℹ 「ext」: Treeshake is false ℹ 「wds」: Project is running at http://0.0.0.0:1962/ ℹ 「wds」: webpack output is served from / ℹ 「wds」: Content not from webpack is served from ./ 61
ℹ 「wds」: 404s will fallback to /index.html ℹ 「ext」: Building Ext bundle at: ./ ℹ 「ext」: Processing Build Descriptor : desktop (development environment) ℹ 「ext」: [ERR] /Users/npv/my-app/node_modules/@sencha/cmd/dist/sencha: line 67: 3409 Abort trap: 6 $java_bin $jvmArgs -jar "$BASEDIR"/sencha.jar "$@" ℹ 「ext」: Opening browser at http://localhost:1962 ℹ 「ext」: Completed ext-webpack-plugin processing ✖ 「wdm」: Built at: 2019-04-23 21:46:52 ERROR in 134 ℹ 「wdm」: Failed to compile. После хакерских манипуляций в отладчике с архивом sencha.jar выяснилось, что это малоинформативное сообщение возникает во время проверки лицензии. Устранить ошибку удалось, удалив файл: ~/Library/Application Support/Sencha/Cmd/Update/app.properties Вероятно, этот файл остался от предыдущих экспериментов с Sencha Cmd, и это не «нравилось» команде ext-gen во время проверки лицензии. К сожалению, в подобных ситуациях обратиться в службу поддержки не удаётся, если пользователь не приобрёл платную версию фреймворка. 3.2.4. Интеграция приложений Sencha Ext JS с Cordova Фреймворк Sencha Ext JS предусматривает возможность интеграции с платформой Cordova с целью разработки гибридных приложений под различные мобильные платформы. Эта возможность реализуется только с помощью Sencha Cmd. Инструментарий Community Edition использовать для этих целей пока невозможно (в документации для версии 6.7 ничего по этому поводу не сказано). 3.2.4.1. Интеграция с Cordova на основе GPL-версии Ext JS SDK и Sencha Cmd Прежде всего в рабочем пространстве следует сгенерировать стартовый шаблон приложения на основе modern toolkit: sencha -sdk ext generate app -modern MyAppName ./MyAppPath Теперь можно выполнить интеграцию с платформой Cordova: sencha cordova init MyAppId MyAppName Здесь MyAppId — это идентификатор приложения, который уже упоминался выше при обсуждении разработки приложений исключительно в рамках платформы Cordova. Вспомним, что этот идентификатор строится по таким же принципам, как и доменные имена, только в обратном порядке и с учётом регистра (в iOS это 62
так называемый AppID). В результате в каталоге проекта команда создаст подкаталог cordova, структура которого показана на рисунке ниже. Рис. 17 Результат интеграции Sencha Ext JS с платформой Cordova. Можно заметить, что структура этого каталога такая же, как и при непосредственной работе с Cordova В отличие от непосредственной работы с платформой Cordova (пример разработки такого приложения представлен выше), в данном случае этот каталог полностью обслуживается Sencha. Кроме этого, в файле app.json будет добавлен раздел, описывающий нативную (native) сборку приложения под ту или иную мобильную платформу: { "builds": { "web": {"default": true}, "native": { "packager": "cordova", "cordova" : { "config": { // Uncomment the line below and add the platforms you wish // to build for //"platforms": "ios android", "id": "MyAppId", "name": " MyAppName" } } } }, … Допустим, разработка ведётся только для платформы iOS. Тогда, следуя рекомендациям (Uncomment the line below ...), раскомментируем соответствующую строку и оставим только iOS: … "platforms": "ios", … После этих манипуляций сборка приложения под заданную платформу (iOS) выполняется командой: 63
sencha app build native В результате в подкаталоге cordova/platforms/ios будет подготовлен проект гибридного приложения для его окончательной сборки в среде Xcode. Для запуска проекта в Xcode достаточно открыть файл проекта с расширением MyAppName.xcodeproj (его можно найти в этом же подкаталоге). Заметим, что собственно разработка приложения ведётся в каталоге app проекта приложения, а всё остальное создаётся в подкаталоге iOS в процессе выполнения команды сборки автоматически. 64 Powered by TCPDF (www.tcpdf.org)
4. ПРИМЕР РАЗРАБОТКИ ГИБРИДНОГО ПРИЛОЖЕНИЯ НА SENCHA EXT JS В рамках настоящего пособия не преследуется цель систематического изложения основ программирования с помощью фреймворка Sencha Ext JS, поскольку это слишком объёмная тема. Однако рассмотреть конкретный пример вполне уместно. Предлагается разработать приложение, которое может оказаться полезным в ситуациях, требующих оперативной обработки графики. Работа с графикой, как правило, оказывается достаточно затратной в плане основных вычислительных ресурсов, таких как процессорное время и оперативная память. Тем более если требуются решения в процессе интерактивного взаимодействия пользователя с приложением. В то же время мобильные устройства, которые, конечно, в настоящее время становятся всё более мощными, не обладают всё-таки достаточной мощностью. Особенно это касается гибридных приложений, которые воспроизводят свой интерфейс опосредованно через встроенный webview, который фактически является интерпретатором языков JavaScipt, HTML и CSS и не может являться платформой для реализации затратных графических алгоритмов. Решением может быть реализация графических алгоритмов: • В виде плагинов, которые пишутся на нативных языках и непосредственно взаимодействуют с операционной системой мобильного устройства. Таких решений достаточно много, например для распознавания штрих-кодов и тому подобное. В то же время оригинальные алгоритмы, конечно, требуют реализации. • В виде кода NativeScript, который работает непосредственно с API системы, но тогда приложение уже не будет гибридным. • Наконец, в виде программ на сервере. Это могут быть скрипты на PHP, которые подключают внешние графические библиотеки типа GD, или программы на C, C++, которые могут взаимодействовать с мобильным приложением, например, через CGI. Именно последнее решение используем в нашем приложении, реализующем обработку графики на стороне web-сервера. Схема работы такого приложения представлена на рисунке 16. Клиентская часть приложения функционирует на стороне мобильного устройства — это и есть гибридное приложение, для разработки которого будем использовать фреймворк Sencha Ext JS совместно с платформой Cordova. Задачи клиентской части: • Получить изображение с помощью камеры мобильного устройства или из альбома этого устройства. • Передать изображение на сервер. • Загрузить обработанное изображение с сервера. 65
Рис. 18 Взаимодействие приложения с web-сервером Для передачи изображения на сервер и для загрузки обработанного изображения используются протокол http и язык php. На php сделать это довольно просто. Для приёма изображения на сервере предусмотрен скрипт upload.php, код которого может выглядеть, например, так: <?php header('Access-Control-Allow-Origin: *'); move_uploaded_file($_FILES["file"]["tmp_name"], "1.jpg"); ?> Таким образом, на сервере переданное изображение, подлежащее обработке, всегда хранится в файле 1.jpg в текущем каталоге (то есть там же, где и файл upload.php). Обработку изображения и передачу обработанного изображения клиентской части реализуем в рамках скрипта process.php: <?php $im = imagecreatefromjpeg('1.jpg'); $white = imagecolorallocate ($im, 255, 255, 255); $red = imagecolorallocate ($im, 255, 0, 0); imagettftext($im, 150, -45, 300, 300, $red, "arialbd.ttf", "О Б Р А Б О Т А Н О"); header ("Content-type: image/jpg"); imagejpeg($im); imagedestroy($im); ?> Обработка изображения является условной — на загруженном изображении красными буквами наносим надпись «О Б Р А Б О Т А Н О» и результат 66
передаём обратно клиентской части (используем графическую библиотеку GD и жирный шрифт arialbd.ttf, который, например, можно заимствовать из среды Windows). Для реализации клиентской части потребуются плагины Cordova. Какие именно плагины и где они будут использованы, схематично представлено на рисунке: Рис. 19 Процессы взаимодействия приложения с плагинами от создания фото до его копирования в песочницу приложения 4.1. Работа с камерой (фотоальбомом) устройства Для того чтобы получить фото с помощью камеры или готовое изображение из альбома устройства, достаточно обратиться к плагину камеры (cordovaplugin-camera) с помощью метода navigator.camera.getPicture() (позиция 1 на рисунке выше) и обеспечить реакцию на событие, соответствующее готовности результата (позиция 2 на рисунке выше). Плагин camera предоставляет полное имя графического файла, в котором содержится только что сделанный снимок или уже готовый снимок из альбома устройства (в зависимости от настроек). Этот файл находится в кэше устройства, то есть в памяти, где его сохранность не гарантируется, поэтому в первую очередь желательно скопировать его в безопасное место — память, выделенную для приложения. Во вторую очередь, конечно, следует каким-то образом представить изображение в приложении. В Sencha Ext JS наиболее подходящим для этих целей является компонент Ext.Img, который располагает свойством src и соответствующим методом setSrc(), позволяющим установить путь к файлу с изображением. 67
Таким образом, логика работы с камерой выглядит следующим образом. Обращаемся к плагину (navigator.camera): navigator.camera.getPicture(success, fail, options); Если работа с камерой прошла успешно (отсняли изображение или выбрали его в альбоме), то управление передаётся функции success, в которой копируем это изображение из кэша в память приложения, например в файл с именем 1.jpg: function success(image_uri) { copyPhotoToFile('1.jpg', image_uri); } Функция copyPhotoToFile (рассматривается далее) копирует файл image_uri, предоставленный плагином камеры, в файл 1.jpg, который расположен в файловом хранилище (песочнице) приложения. Если работа с камерой прошла неуспешно: например, отказались от выбора фото (отснятого или из альбома), или произошла ошибка, то управление передаётся функции fail: function fail(message) { Ext.Msg.alert('Error', "Failed: " + message); } Здесь показываем пользователю формулировку ошибки, предоставленную самим плагином в переменной message. Соответствующее окно с ошибкой реализуется с помощью метода alert библиотеки Ext JS. В переменной options (она фигурирует выше в функции getPicture) задаются настроечные параметры, вот некоторые из них: var options = { quality: 70 // качество фото ,destinationType: // где разместить результат navigator.camera.DestinationType.FILE_URI // в файле ,sourceType: // камера или альбом navigator.camera.PictureSourceType.CAMERA//( или PHOTOLIBRARY) // формат изображения ,encodingType: navigator.camera.EncodingType.JPEG correctOrientation: true }; 4.2. Работа с файловой системой устройства Базовый плагин cordova-plugin-file использует универсальную для всех поддерживаемых мобильных платформ концепцию манипулирования файловой системой. Приложения устройства владеют приватными директориями файлового хранилища, называемыми песочницами, в которых можно выделить поддиректории, различные по назначению, по доступности (чтение, запись), по месту расположения (внутренняя или внешняя память) или по режиму сохранности файлов. Для каждой из таких частей плагин, который в приложении представ68
ляется объектом cordova.file, предусматривает соответствующую объектссылку, которая легко запоминается и является общей для различных мобильных платформ. Корневая директория песочницы приложения представлена объектом cordova.file.applicationStorageDirectory. Директория, в которой приложение установлено, представлена ссылкой cordova.file.applicationDirectory. В iOS и Windows эта директория только для чтения (хотя её поддиректории /Documents в iOS и /localState в Windows доступны и по записи). Директория cordova.file.dataDirectory предназначена для произвольных данных приложения, доступна по чтению и по записи. В iOS эта директория не синхронизируется с iCloud. Если требуется синхронизация с облаками (iCloud), то следует использовать cordova.file.syncedDataDirectory (iOS, Android, BlackBerry 10, Windows). В Android можно обращаться к внешней памяти — cordova.file.externalDataDirectory (если есть). Наконец, своеобразный кэш устройства — cordova.file.externalCache Directory. Здесь целесообразно хранить файлы, которые приложение может легко воссоздать, поскольку операционная система может при необходимости (например, не хватает памяти) удалять файлы в этой директории. В то же время нельзя полагаться на OC и для экономии памяти лучше удалять их в приложении. Это далеко не полный список подобных точек входа, предусмотренных cordova. Еще более универсальный подход предполагает использование cdvfileпротокола. Поясним его на примере. Пусть в директории данных приложения, то есть в cordova.file.dataDirectory, расположен файл с именем 1.jpg. Полное имя этого файл может быть таким: /var/mobile/Containers/Data/Application/46263B0B-4185-44C8-83E0DC3B2C1F0432/Library/NoCloud/1.jpg Здесь замысловатый номер — это некоторый идентификатор песочницы приложения. Понятно, что расположен файл в директории, которая не синхронизируется с облачным хранилищем (NoCloud). Пример взят с реального устройства iPhone. На других устройствах этот путь может быть устроен совершенно по-другому. В то же время для большинства платформ путь к нашему файлу с помощью cdvfile может быть задан универсально и более компактно: cdvfile://localhost/library-nosync/1.jpg Более того, этот способ может быть использован в плагинах cordova, а также DOM-элементах документа, например для src в img. Теперь можно обсудить копирование отснятого изображения из кэша камеры в файл 1.jpg. Напомним, что для этого у нас была предусмотрена функция copyPhotoToFile(), которой передавались два параметра: имя файла — filenm (например, 1.jpg) — и имя файла с фото в кэше камеры — uri. Во-первых, просим систему предоставить доступ к директории cordova.file.dataDirectory с помощью функции window.resolveLocalFileSystemURL(). Функция принимает три 69
параметра — это запрашиваемая директория, функция-обработчик успешного исхода и функция, которая вызывается в случае ошибки: function copyPhotoToFile(filenm, uri) { window.resolveLocalFileSystemURL(cordova.file.dataDirectory, function(directoryEntry) {// успех // здесь с помощью directoryEntry // получим доступ к файлу с именем filenm }, function(e) { // ошибка — не получили доступ к директории console.log('resolveLocalFileSystemURL Error'); } ); } В случае успеха получаем доступ к директории посредством объекта directoryEntry и аналогично получаем доступ к файлу с заданным именем filenm (если его нет — то он будет создан): directoryEntry.getFile(filenm, { create: true }, function(fileEntry) { // успех // здесь копируем фото из кэша камеры uri в файл filenm - 1.jpg }, function(e) { // ошибка console.log('getFile Error'); } ); Копирование файла uri с фото из кэша камеры в файл, представленный объектом fileEntry, реализуем с помощью плагина cordova-file-transfer, который работает не только в сети, но и в рамках локальной памяти устройства: var fileTransfer = new FileTransfer(); var fileURL = fileEntry.toURL(); fileTransfer.download(uri, fileURL, function(entry) { // успех // здесь требуется установить src свойство компонента img // (класса Ext.Img) на файл с изображением // img.setSrc( fileURL + '?' + Date.now() ); // также можно очистить кэш камеры: // navigator.camera.cleanup(onSuccess, onFail); }, function(error) { // ошибка //console.log('fileTransfer Error'); },null,{} ); Поясним представленный программный код: • Предполагается, что в переменной img содержится ссылка на компонент Ext.Img библиотеки Sencha Ext JS (напомним, что именно этот компонент является наиболее подходящим для представления фото). • В переменной fileURL содержится полный путь к скопированному изображению, который получен с помощью метода toURL() объекта fileEntry, 70
посредством которого осуществляется доступ к файлу. Например, эта переменная может содержать следующий URL: file:///var/mobile/Containers/Data/Application/46263B0B-4185-44C8-83E0DC3B2C1F0432/Library/NoCloud/1.jpg • Вместо toURL() можно было бы воспользоваться методом toInternalURL(), который использует протокол cdvfile, в результате получили бы: cdvfile://localhost/library-nosync/1.jpg • Для того, чтобы webview действительно перегружало (а не кэшировало) изображение в компоненте img, используется искусственный приём — добавляется в строку запроса текущее время в миллисекундах — Date.now(). • После копирования изображения в песочницу следует зачистить кэш камеры: navigator.camera.cleanup(onSuccess, onFail); function onSuccess() { //console.log("Camera cleanup success"); } function onFail(message) { //console.log('Failed because: ' + message); } 4.3. Генерация шаблона приложения и настройка рабочей среды Выше достаточно подробно описан процесс подготовки рабочей среды и генерации стартового шаблона приложения Sencha Ext JS. Этого описания вполне достаточно, однако оно разнесено по разным разделам, здесь же опишем необходимые действия в сжатом виде и в одном месте. Для более глубокого усвоения материала предлагается соотнести излагаемое в данном разделе с соответствующими процедурами, предложенными выше. Настройка рабочей среды очень простая. Далее предполагается, что путь к команде sencha внесён в переменную окружения path, обычно это делается автоматически при установке Sencha Cmd. Если по каким-то причинам это не так, то на mac и unix выполняем команду: export PATH=~/bin/Sencha/Cmd/6.0.0.92:$PATH в Windows: set PATH=%PATH%;C:\Sencha\Cmd\6.0.0.92 (где, конечно, 6.0.0.92 меняем на актуальную версию). Последовательность действий: • Инициируем рабочее пространство — каталог с разрабатываемыми приложениями (/path/to/workspace): 71
sencha generate workspace /path/to/workspace • Переходим в этот каталог: cd /path/to/workspace • Создаём приложение — стартовый шаблон: sencha -sdk /path/to/framework generate app modern AppName ./AppPath Путь к разархивированному фреймворку (/path/to/framework) указываем только один раз — фреймворк будет скопирован в рабочее пространство. Для последующих применений команда выглядит проще: sencha generate app -ext -modern AppName ./AppPath Здесь AppName — имя приложения, будет развёрнуто. Например, так: AppPath — каталог, где приложение sencha generate app -ext -modern TstApp ./TstApp • Переходим в каталог приложения и выполняем сборку: cd TstApp sencha app build • В результате будет создано полностью рабочее приложение, которое можно посмотреть в браузере (http://localhost:1841/TstApp/), если выполнить команду: sencha app watch Эта команда запустит web-сервер, слушающий порт 1841. Вид приложения после загрузки в браузере уже был представлен на одном из рисунков выше. Для интеграции проекта с cordova теперь достаточно выполнить ряд простых команд (всё делается из каталога приложения TstApp): • Инициируем cordova: sencha cordova init learn.senchaCordova.TstAppId TstApp Здесь learn.senchaCordova.TstAppId — это ID приложения (AppID в терминах iOS). Этот идентификатор уже неоднократно обсуждался выше, и ему будет уделено достаточно много внимания далее. В результате будет создан подкаталог cordova, в котором будет находиться всё необходимое для успешной работы этой платформы в рамках проекта приложения Sencha Ext JS. • В файле app.json, который находится в каталоге проекта приложения (TstApp) и хранит в популярном формате json его конфигурацию, следует ука- 72
зать мобильную платформу или платформы (достаточно раскомментировать соответствующую строку): "builds": { "web": {"default": true}, "native": { "packager": "cordova", "cordova" : { "config": { // Uncomment the line below and add the platforms you // wish to build for //"platforms": "ios android", "platforms": "ios", "id": "learn.senchaCordova.TstAppId ", "name": "TstApp" } } } } • Наконец, выполнить сборку мобильного гибридного приложения cordova можно так: sencha app build native В результате выполнения этой команды в каталоге cordova/platforms/ios будет собран проект, готовый к загрузке в среду нативной разработки Xcode (для загрузки достаточно дважды кликнуть по файлу проекта с расширением xcodeproj). Заметим, что при указании платформы Android (или любой другой, поддерживаемой cordova), был бы создан каталог с проектом под Android Studio. • Если приложение использует базовые плагины cordova, то их можно установить из каталога cordova стандартным способом, например плагины, необходимые для нашего приложения и перечисленные выше, устанавливаются следующим командами: cordova plugin add cordova-plugin-camera cordova plugin add cordova-plugin-file cordova plugin add cordova-plugin-file-transfer cordova plugin add cordova-plugin-network-information После установки плагинов приложение следует пересобрать. Вся работа с cordova полностью сосредоточена в одноимённом подкаталоге проекта. В этом подкаталоге можно выполнять любые другие действия, предусмотренные платформой cordova. • Разработка приложения ведётся исключительно в рамках подкаталога app проекта на языках Java Script, HTML и CSS согласно современной концепции — MVC (Model View Controller). Модель MVC позволяет разделить весь 73
программный код приложения на три части — код, определяющий данные приложения, пользовательский интерфейс и управляющую логику. 4.4. Классы в Ext JS Ext JS — это библиотека на JavaScript. Объектно-ориентированное программирование (ООП) возможно непосредственно на JavaScript на основе так называемых прототипов. Три «кита», на которых держится ООП, — инкапсуляция, наследование и полиморфизм — могут быть реализованы с помощью этого механизма. В Ext JS используется свой оригинальный подход к определению классовой иерархии и ООП, который, конечно, основан на JavaScript и его прототипах. В данном разделе приведём лишь начальные сведения об определении и использовании классов в Ext JS. 4.4.1. Соглашения об именах Как известно, соглашения (правила) об именовании классов играют не последнюю роль в обеспечении читаемости и организации кода. Вот эти правила: • В именах не рекомендуется использовать подчеркивания, дефисы, цифры и другие не алфавитно-цифровые символы. • Имена классов следует группировать с помощью механизма пространств имён (отделяя имена пространств точками). В имени класса должно фигурировать как минимум одно уникальное пространство имён верхнего уровня. Например: AppName.mydata.Customer Company.Application • Пространства имен верхнего уровня и фактические имена классов должны подчиняться правилу CamelCased (составные слова в именах следует начинать с большой буквы). Все остальное должно быть в нижнем регистре. Например: MyApp.form.UserReg AppName.view.OrderGrid • Sencha используют Ext в качестве пространства имен верхнего уровня, поэтому это имя зарезервировано и не допускается в именах пользовательских классов. • Имена методов и свойств должны подчиняться правилу camelCased, то есть начинаться с маленькой буквы. 4.4.2. Исходные файлы Имя класса указывает путь к файлу, в котором хранится его определение. В файле, таким образом, должен определяется только один класс. Например, определение класса: Ext.util.Observable 74
хранится в файле: path/to/src/Ext/util/Observable.js Определение класса: Ext.form.action.Submit хранится в файле: path/to/src/Ext/form/action/Submit.js Здесь path/to/src — это каталог классов приложения. Заметим, что классы, имена которых начинаются с Ext, — это библиотечные классы. 4.4.3. Определение класса Для определения класса используется метод Ext.define(). Его базовый синтаксис: Ext.define(className, members); Где className — это имя класса, members — объект, представляющий коллекцию членов — методов и свойств класса. Пример, заимствованный из документации: Ext.define('My.sample.Person', { name: 'Unknown', constructor: function(name) { if (name) { this.name = name; } }, eat: function(foodType) { alert(this.name + " is eating: " + foodType); } }); var bob = Ext.create('My.sample.Person', 'Bob'); bob.eat("Salad"); // alert("Bob is eating: Salad"); 4.5. Главное окно приложения Структура каталога приложения TstApp, сгенерированного в предыдущем разделе, представлена на рисунке ниже. Опишем назначение ключевых элементов файловой структуры приложения: • index.html Приложение Ext JS — это так называемое одностраничное приложение — Single Page Application (SPA). Это означает, что код приложения будет переза75
писывать эту единственную страницу (index.html), выполняя воспроизведение (rendering) нового содержимого. Рис. 20 Структура каталога приложения TstApp • app Именно в этом каталоге ведётся разработка приложения. Этот каталог автоматически «подхватывается» командой сборки приложения, всё остальное создаётся автоматически. Это корень пространства имён классов нашего приложения с именем TstApp. Это означает, например, что файл Main.js в каталоге view/main содержит описание класса с именем TstApp.view.main.Main. Это описание в Ext JS выглядит следующим образом: Ext.define('TstApp.view.main.Main', { extend: 'Ext.tab.Panel', // это родительский класс … // далее методы и свойства класса }); 76
• bootstrap.js Именно этот файл является результатом сборки приложения. Единственный тег script в index.html указывает на bootstrap.js. • app.js Код в файле app.js: /* * This file launches the application by asking Ext JS to create * and launch() the Application class. */ Ext.application({ extend: 'TstApp.Application', name: 'TstApp', requires: [ // This will automatically load all classes in the TstApp namespace // so that application classes do not need to require each other. 'TstApp.*' ], // The name of the initial view to create. mainView: 'TstApp.view.main.Main' }); Здесь, очевидно, вызывается метод Ext.application(), которому передаётся конфигурация приложения: имя приложения (name), требуемые классы (requires), класс главного окна (mainView), которое появится сразу после загрузки приложения, а также класс самого приложения (extend), который, следуя общей схеме именования классов, расположен в файле app/Application.js. • app.json Здесь в формате json хранятся глобальные настройки приложения, например тема, toolkit (modern или classic) и т. д. Этот файл более детально уже обсуждался выше. Структура каталога app соответствует концепции разработки приложений MVC (Model View Controller), которая уже неоднократно упоминалась выше. Одним из ключевых элементов этой концепции является представление данных, используемых приложением. Поскольку наше приложение не использует данные, то, не вникая в подробности, лишь упомянем основные сущности, используемые для описания данных: • Store — коллекция из нескольких записей (records), предназначенная для заполнения компонентов пользовательского интерфейса, например табличного представления (Grid). • Model — спецификация записи (record) в коллекции записей Store. В представленной выше файловой структуре можно найти соответствующие каталоги и классы, представляющие эти сущности. Поскольку данные не используются в нашем приложении, то следует очистить каталоги store и model. Каталог view предназначен для хранения классов визуальных компонент (виджетов или представлений), определяющих внешний вид и поведение интерфейса 77
приложения. Каждое представление определяется своим внешним видом и поведением — реакциями на события, в том числе вызванными взаимодействием с пользователем. Так, например, главное окно сгенерированного приложения представлено классами: Main.js — класс главного окна приложения, MainController.js — контроллер главного окна. Наконец, MainModel.js — класс, предоставляющий ещё один удобный сервис для хранения данных, используемых в представлении. Откажемся от главного окна сгенерированного приложения в виде закладок (в терминах Sencha — это Ext.tab.Panel) и создадим для этих целей «свой» view. Команда Sencha Cmd позволяет сгенерировать «болванку» нового представления view: sencha generate view -b Ext.Container -n mainCtr.Main В этой команде просим Sencha сгенерировать визуальный компонент (view) c именем Main, который наследует класс Ext.Container и будет расположен в каталоге app/view/mainCtr. В этом же каталоге будут сгенерированы «болванки» контроллера и модели. Результат показан на рисунке ниже. Рис. 21 В результате генерации нового view с помощью команды sencha в каталоге app/view появится подкаталог mainCtr Каталог app/view/main следует удалить и не забыть указать в файле app.js для свойства mainView новый виджет — TstApp.view.mainCtr.Main, используемый в качестве главного. Корректируем содержимое файла Main.js следующим образом: Ext.define('TstApp.view.mainCtr.Main',{ extend: 'Ext.Container', xtype: 'mainCtr', requires: [ 'TstApp.view.mainCtr.MainController', 'TstApp.view.mainCtr.MainModel' ], controller: 'mainctr-main', viewModel: { type: 'mainctr-main' }, layout: 'card', items: [{ xtype: 'getPhotoPnl' },{ xtype: 'showResultPnl' }] }); 78
Попытаемся разобраться в представленном фрагменте кода. Во-первых, следует, наконец, более детально познакомится со способом определения классов в библиотеке Sencha. Класс определяется с помощью библиотечного метода Ext.define(). В качестве первого параметра передается имя класса, в качестве второго — конфигурационный объект. Конфигурационный объект определяет методы и свойства класса. Ряд свойств конфигурационного объекта имеют специальное назначение. Например, свойство extend определяет базовый класс. В нашем случае — это библиотечный класс Ext.Container. Заметим, что все классы в пространстве имён Ext являются библиотечными, то есть они определены в библиотеке Sencha Ext JS. Свойство xtype определяет своеобразный псевдоним класса, который можно использовать там, где требуется создать объект этого класса. Свойство requires определяет требуемые классы — эти классы используются в определении класса, и файлы с их определениями будут загружены заранее. Теперь обсудим ряд свойств, используемых контейнером Ext.Container. Как следует из самого названия, этот класс служит контейнером для других виджетов, которые представлены в свойстве items в виде массива. В нашем случае это два виджета, заданные своими псевдонимами: getPhotoPnl и showResultPnl. Классы этих виджетов определяются в других файлах и будут обсуждаться далее. Первый предназначен для получения фото с камеры или из альбома камеры. Второй — для просмотра результата обработки на удалённом сервере. Обратим внимание также на свойство layout, которое определяет размещение дочерних компонентов в контейнере. Разметка card означает, что дочерние компоненты будут размещены подобно игральным картам в колоде, перекрывая друг друга («карточная компоновка»). На рисунке ниже представлен активный верхний компонент в этой колоде — getPhotoPnl. В терминологии Sencha это панель — класс этого виджета является подклассом Ext.Panel (порождён от библиотечного класса Ext.Panel). Рабочее поле представленного компонента заполнено рисунком, полученным с помощью плагина камеры. В верхней части панели расположены функциональные кнопки. Иконки на кнопках определяют их назначение: настройки приложения, камера, загрузка на сервер и последняя кнопка — переход к следующему компоненту. По нажатию последней кнопки следующий компонент showResultPnl будет выведен на передний план в разметке card. Этот компонент также является панелью, и его рабочее пространство будет заполнено результатом обработки исходного рисунка. Кнопка back позволяет вернуться к исходной панели. Контроллер (MainController.js) и модель (MainModel.js) не требуют корректировок. 79
Рис. 22 Так выглядит первый компонент в карточной разметке (card) с псевдонимом getPhotoPnl Рис. 23 Так выглядит второй компонент в карточной разметке (card) с псевдонимом showResultPnl 4.6. Компонент getPhotoPnl Вид компонента getPhotoPnl представлен на рисунке выше. Он указан в качестве первого в карточной разметке главного окна, и поэтому именно с ним пользователь начинает работу после запуска приложения. Компонент порождён от Ext.Panel и наследует все свойства и поведение панели. Инструментарий этой панели (toolbar) — в виде кнопок в его верхней части — позволяет обращаться к настройкам приложения, к камере устройства, загружать фото на сервер, наконец, переходить к обработке фото (кнопки можно увидеть на рисунке выше). Для генерации кода компонента (то есть полного набора файлов с определениями классов модели данных, представления и контроллера — Model, View, Controller) выполним уже знакомую команду: sencha generate view -b Ext.Panel -n camera.getPhoto 80 Powered by TCPDF (www.tcpdf.org)
В результате в каталоге app/view получим файловую структуру, представленную на рисунке далее. Рис. 24 Результат генерации компонента getPhotoPnl Напомним, что не нужно запоминать опции команды Sencha Cmd, достаточно научится пользоваться help-ом команды (смотри соответствующий раздел выше). 4.6.1. Визуальное представление компонента Начнём «кодить» в файле определения класса визуального представления компонента (View), то есть в getPhoto.js. Содержимое этого файла корректируем следующим образом: Ext.define('TstApp.view.camera.GetPhoto',{ extend: 'Ext.Panel', xtype: 'getPhotoPnl', requires: [ 'Ext.Button', 'Ext.Img', 'Ext.Toolbar', 'Ext.Dialog', 'Ext.form.Panel', 'Ext.SegmentedButton', 'Ext.field.Text', 'Ext.field.Url', 'Ext.field.Number' ], controller: 'camera-getphoto', viewModel: { type: 'camera-getphoto' }, layout: { type:"vbox", align:"stretch" }, items: [{ reference: 'placeholder', xtype: "image", flex: 1 }], tbar: [{ iconCls: 'x-fa fa-gear', 81
handler: 'settings', },{ xtype: 'spacer' },{ iconCls: 'x-fa fa-camera', handler: 'getPicture', },{ xtype: 'spacer' },{ xtype: 'spacer' },{ iconCls: 'x-fa fa-upload', handler: 'upload' bind: { disabled: '{uploadBtnDisabled}' } },{ xtype: 'spacer' },{ iconCls: 'x-fa fa-angle-double-right', handler: 'result' }], listeners: { photoIsCopied: 'photoIsCopied' }, }); Прокомментируем представленный код. Определяется класс с именем TstApp.view.camera.GetPhoto с помощью библиотечной функции Ext.define(). Второй параметр — это конфигурационный объект, который задаёт специфику определения: • Класс определяет панель, поскольку порождён от Ext.Panel (свойство extend). • Псевдоним класса — getPhotoPnl (свойство xtype). Как уже отмечалось, псевдоним — очень удобный способ, придуманный Sencha, для ссылки на класс там, где необходимо создать объект этого класса. Например, далее в свойстве items задаётся дочерний компонент, у которого в xtype указано значение image. В результате автоматически (неявно) будет создан библиотечный объект Ext.Img. Альтернативой является явное создание объекта: Ext.create('Ext.Img', {reference: 'placeholder', flex: 1}) • Класс использует определения классов, перечисленных в массиве requires. • В свойствах controller и viewModel задаются псевдонимы классов контроллера и модели, которые определяются в файлах getPhotoController.js и getPhotoModel.js соответсвенно. • В свойстве layout задаётся способ размещения дочерних компонент, представленных в свойстве items. Разметка vbox означает, что дочерние компоненты размещаются друг под другом. Дополнительное указание — выравнивание — свойство align. Значение этого свойства stretch означает, что дочерние 82
компоненты будут по возможности максимально растянуты по горизонтали. В нашем случае единственный дочерний компонент — это image, предназначенный для фотографии. Значение 1 свойства flex заставит этот компонент занять всё доступное пространство также и по вертикали. Заметим, что менеджеры разметки, которые указываются в свойстве layout, — это непростая тема, с которой более подробно можно ознакомиться здесь [12, 13, 14, 17]. • В свойстве items указываются дочерние компоненты, которые будут размещаться в пределах рабочего поля родительского компонента согласно заданной разметке (layout). В нашем случае — это единственный компонент для размещения фотографии. Заметим, что для удобного доступа к компоненту предусмотрено его ссылочное имя в свойстве reference. Как это делается — показано далее, в описании контроллера. • В свойстве tbar задаются кнопки инструментальной панели — toolbar. В качестве иконок (iconCls) используются символы популярного иконочного шрифта font-awesome (https://fontawesome.ru/). Специально загружать этот фонт не требуется, поскольку Sencha Ext JS уже содержит его по умолчанию. Классы требуемых иконок можно «подсмотреть» на сайте. Наконец, свойства handler определяют обработчики нажатий на кнопки. Сами обработчики, как и требует концепция MVC, раскрываются в контроллере — getPhotoController.js. • В свойстве listeners задаются названия событий, на которые должен реагировать компонент GetPhoto. Событие photoIsCopied — это пользовательское событие (название придумываем самостоятельно). В данном случае это всего лишь уловка, которая предоставляет удобный способ передать управление функции photoIsCopied с помощью механизма событий. Эта функция также раскрывается в контроллере компонента. Замечательной особенностью библиотеки Sencha Ext JS является возможность автоматического и двухстороннего связывания представления View с данными модели — viewModel. Обратим внимание на свойство bind (связать) в конфигурации кнопки upload. Это объект, который в нашем случае имеет всего лишь одно свойство disabled, значение которого, строка — конструкция с фигурными скобками. Вот таким образом осуществляется связывание свойства disabled кнопки со свойством uploadBtnDisabled в определении модели данных нашего визуального компонента (файл getPhotoModel.js): Ext.define('TstApp.view.camera.GetPhotoModel', { extend: 'Ext.app.ViewModel', alias: 'viewmodel.camera-getphoto', data: { name: 'TstApp', photoBtnDisabled: false, uploadBtnDisabled: true, settingsBtnDisabled: false } }); 83
Изменение свойства uploadBtnDisabled в модели автоматически приведёт к изменению доступности кнопки upload (то есть значения её свойства disabled). Обратно, если меняем состояние disabled кнопки, то автоматически изменится значение связанного свойства модели. Очевидно, аналогично можно управлять состоянием доступности и других кнопок. Оценить по достоинству такой централизованный способ управления визуальными компонетами можно только, получив достаточный отрицательный опыт использования традиционных подходов к решению подобных задач. 4.6.2. Контроллер компонента. Работа с камерой В классе контроллера, как уже неоднократно отмечалось, реализуется функциональность компонента, прежде всего реакции на нажатия кнопок в «тулбаре», которые в классе представления были представлены обычными строками 'getPicture', 'settings' и т. д. — названиями обработчиков. Связку этих строк с одноимёнными методами в контроллере обеспечивает библиотека Ext JS. Объёмный код контроллера будем наращивать постепенно. Прежде всего предусмотрим методы с именами, совпадающими с названиями обработчиков в классе представления: Ext.define('TstApp.view.camera.GetPhotoController', { extend: 'Ext.app.ViewController', alias: 'controller.camera-getphoto', dialog: null, // используется в settings getPicture: function() { … }, photoIsCopied: function() { … }, upload: function() { … }, result: function() { … }, settings: function() { … } }); Начнём с метода getPicture, назначение которого — получить фото с помощью плагина камеры: getPicture: function() { var me = this; navigator.camera.getPicture(this.success.bind(me), this.fail.bind(me), this.getPictureOptions()); } Работа с камерой подробно описана выше. Представленный код отличается тем, что в качестве реакции на успешное завершение работы с камерой указывается метод success, определённый непосредственно в классе самого контроллера. Делается это с помощью известного метода bind — this.success.bind(me). Аналогично определяется реакция на неудачу — fail. 84
Параметры, используемые при обращении к камере, формируются с помощью метода getPictureOptions. Добавим эти методы в определение класса: Ext.define('TstApp.view.camera.GetPhotoController', { extend: 'Ext.app.ViewController', alias: 'controller.camera-getphoto', getPictureOptions: function() { var app = TstApp.getApplication(); var sourceType = app.camera ? navigator.camera.PictureSourceType.CAMERA : navigator.camera.PictureSourceType.PHOTOLIBRARY; var opts = { quality: app.quality ,destinationType: navigator.camera.DestinationType.FILE_URI ,sourceType: sourceType ,encodingType: navigator.camera.EncodingType.JPEG ,mediaType: navigator.camera.MediaType.PICTURE //,allowEdit: true ,correctOrientation: true }; return opts; }, getPicture: function() { var me = this; navigator.camera.getPicture(this.success.bind(me), this.fail.bind(me), this.getPictureOptions()); }, success: function(image_uri) { this.copyPhotoToFile('1.jpg', image_uri); }, fail: function(message) { Ext.Msg.alert('Error', "Failed: " + message); }, … }); Заметим, что такие настроечные параметры камеры, как источник изображения (sourceType) и качество фото (quality), хранятся в одноимённых свойствах объекта app (app.srvr), представляющего приложение. Этот объект удобен для хранения всевозможных переменных (например, конфигурационных параметров), значения которых могут потребоваться в различных частях приложения, поскольку доступ к этому объекту всегда и везде можно получить вот таким образом: var app = TstApp.getApplication(); В случае успешного завершения работы с камерой копируем файл с изображением (его имя передаётся в качестве входного параметра image_uri) в файл с именем '1.jpg.' В случае неудачи — выдаём соответствующее сообще85
ние об ошибке с помощью библиотечной функции Ext.Msg.alert. Копирование файла подробно описано выше в разделе, посвящённом работе с файловой системой. Здесь — это метод контроллера, а по завершении копирования генерируется событие photoIsCopied: copyPhotoToFile: function(filenm, uri) { var me = this; window.resolveLocalFileSystemURL(cordova.file.dataDirectory, function(directoryEntry) { directoryEntry.getFile(filenm, { create: true }, function(fileEntry) { var fileTransfer = new FileTransfer(); var fileURL = fileEntry.toURL(); me.img_uri = fileURL; TstApp.getApplication().photo = fileEntry.toInternalURL(); fileTransfer.download(uri, fileURL, function(entry) { me.getView().fireEvent('photoIsCopied'); }, function (error) { //console.log('fileTransfer Error'); }, null, // or, pass false {} ); }, function(e) { console.log('getFile Error'); } ); }, function(e) { console.log('resolveLocalFileSystemURL Error'); } ); } Генерация события photoIsCopied фактически перенаправляет нас к одноимённому методу контроллера. Здесь после того, как фото скопировано в файловую систему приложения, самое время настроить на отображение этого фото специально предусмотренный компонент image (Ext.Img) со ссылочным именем placeholder: photoIsCopied: function() { var img = this.lookup('placeholder'); var img_uri = this.img_uri + '?' + Date.now(); img.setSrc(img_uri); this.cleanUp(); TstApp.getApplication().saveCfg(); } В представленном фрагменте кода метод lookup позволяет получить требуемый компонент, а метод этого компонента setSrc — установить изображение. Вопрос может возникнуть по поводу текущего времени (Date.now() — количество миллисекунд, прошедших с 1 января 1970 года), добавленного через знак вопроса к адресу изображения (то есть к URI). Это стандартный приём заставить компонент WebView отказаться от кэшированной копии и действительно выполнить переустановку изображения. Суть его состоит в искусственном изменении Query String в URI изображения. 86
Назначение метода cleanUp — зачистить плагин камеры от последствий работы с ним — фактически сводится к вызову одноимённого метода плагина: cleanUp: function() { navigator.camera.cleanup(onSuccess, onFail); function onSuccess() { //console.log("Camera cleanup success"); } function onFail(message) { //console.log('Failed because: ' + message); } } Наконец, метод TstApp.getApplication().saveCfg(), предназначен для сохранения текущей конфигурации приложения. В данном случае это просто название файла фото, которое хотелось бы установить при последующей загрузке приложения. Как это делается, обсудим далее. 4.6.3. Загрузка фото на сервер После всех манипуляций, связанных с фотографированием (или выбором готового фото из альбома), требуется передать это фото на сервер для его обработки. Выполняется это действие при активации соответствующей кнопки в тулбаре (смотри рисунок выше) с реакцией upload: upload: function() { if( navigator.connection.type == Connection.NONE || navigator.connection.type == Connection.UNKNOWN ) { Ext.Msg.alert('Нет сети!', 'Для загрузки фото требуется 3G, 4G, WiFi или иной выход в сеть!' ); return; } var app = TstApp.getApplication(); var me = this, fileURL = "cdvfile://localhost/library-nosync/1.jpg", var options = new FileUploadOptions(); options.fileKey = "file"; options.fileName = "1.jpg"; options.mimeType = "image/jpeg"; var ft = new FileTransfer(); ft.upload(fileURL, encodeURI(app.srvr + "upload.php"), me.uploadSuccess.bind(me), me.uploadFail.bind(me), options); }, uploadSuccess: function (r) { Ext.Msg.alert("Загружено!", "Фотка ушла на сервер!"); }, uploadFail: function (error) { Ext.Msg.alert("Ошибка", 87
"Code = " + error.code + "<br/>upload error source " + error.source + "<br/>upload error target " + error.target); } В представленном фрагменте кода прежде всего проверяется наличие сети. Вспомним, что для этих целей был установлен плагин cordova-pluginnetwork-information. Проверка выполняется довольно просто — определяется тип соединения, который устанавливается плагином в переменной navigator.connection.type. При наличии сети выполняется загрузка фото на сервер с помощью плагина cordova-plugin-file-transfer, который уже использовался выше при перемещении фото в файловую систему приложения. Загрузка осуществляется с помощью метода upload, которому в качестве параметров передаются: • исходный файл (fileURL); • принимающий скрипт на сервере (app.srvr + "upload.php"); • функция, которая вызывается в случае успешной загрузки (me.uploadSuccess.bind(me)); • функция, которая вызывается в случае неудачи (me.uploadFail.bind(me)); • опции, определяющие специфику загрузки (options). Заметим, что адрес сервера (ip или доменное имя) хранится в свойстве srvr объекта app (app.srvr), представляющего приложение. Это ещё один настроечный параметр приложения в дополнение к качеству и источнику фото (камера или альбом). В случае успешной загрузки или неудачи информируем об этом пользователя с помощью уже знакомого метода Ext.Msg.alert. 4.6.4. Обработка фото на сервере После загрузки фото на сервер можно выполнить его серверную обработку и показать результат в приложении. Напомним, что для представления результата предусмотрена отдельная панель — showResultPnl, которая в «карточной разметке» главного контейнера расположена под панелью getPhotoPnl. Таким образом, достаточно активировать эту панель — переместить её на вершину «карточной разметки», а затем выполнить загрузку результата: result: function() { var app = TstApp.getApplication(), mainCtr = this.getView().up('mainCtr'), resultholder = mainCtr.down('showResultPnl').lookup('resultholder'); mainCtr.setActiveItem(1); var res_uri = app.srvr + 'download.php' + '?' + Date.now(); resultholder.setSrc(res_uri); } Здесь следует обратить внимание на технику перемещения по древовидной структуре компонент приложения для получения ссылки на панель showResultPnl. С помощью метода контроллера getView получаем ссылку на теку88
щее представление — панель getPhotoPnl. С помощью её метода up добираемся до родительского контейнера mainCtr, от которого с помощью метода down смещаемся вниз по дереву до искомой панели showResultPnl, в которой отыскиваем (метод lookup) компонент со ссылкой resultholder. Далее в «карточной разметке» родительского контейнера mainCtr активируем первый компонент с помощью метода setActiveItem — панель showResultPnl (нулевой компонент — это панель getPhotoPnl). Наконец, установка в качестве источника содержимого компонента resultholder (метод setSrc) серверного скрипта downlod.php позволяет выполнить его, загрузить и увидеть результат обработки. 4.6.5. Настройка параметров приложения Настроечными параметрами приложения являются: • ip-адрес или доменное имя сервера для обработки фото (srvr); • качество фото (quality); • источник фото (camera — содержит логическое значение, если true, то источник — камера, иначе — альбом). Как уже было сказано, эти параметры хранятся в виде одноимённых свойств объекта приложения. Для их установки и изменения следует предусмотреть диалог форму, содержащий необходимые поля, которая должна появляться при активации соответствующей кнопки в «тулбаре» компонента getPhotoPnl (первая иконка). Библиотека ExtJS обладает мощными средствами для построения подобных форм, некоторые из которых продемонстрируем в данном разделе. Конечно, согласно общей логике построения приложений типа ModelView-Controller следовало бы предусмотреть для требуемого диалога-формы отдельный компонент, для которого необходимо было бы определить как минимум классы представления и контроллера. Однако здесь реализуем иной подход, который не требует создания ещё одного пользовательского компонента с явным определением классов представления (View) и контроллера (Controller). Разместим конфигурацию диалога-формы в определении класса представления панели getPhotoPnl как дополнительное свойство dialog, то есть в файле getPhoto.js добавим (например, после свойства listeners) следующий код: dialog: { xtype: 'dialog', title: 'Настройки', maskTapHandler: 'onCancel', bodyPadding: 0, layout: 'fit', maxWidth: 550, items: [{ xtype: 'formpanel', reference: 'form', 89
autoSize: true, items: [{ xtype: 'segmentedbutton', reference: 'cameraAlbumButton', name: 'cameraAlbumButton', items: [{ text: 'camera', //pressed: true, value: 'camera' },{ text: 'album', value: 'album' }] },{ xtype: 'urlfield', name: 'srvr', label: 'Сервер', labelAlign: 'top', allowBlank: false, required: true },{ xtype: 'numberfield', name: 'quality', label: 'Качество фото', labelAlign: 'top', allowBlank: false, required: true }] }], buttons: { ok: 'onOK', cancel: 'onCancel' }, listeners: { show: 'onShow' } } Представленный конфигурационный объект позволяет создать компонент типа dialog, единственным дочерним компонентом которого является форма (formpanel), содержащая следующие поля: • кнопку segmentedbutton с двумя значениями для выбора — CAMERA и ALBUM, • поле urlfield для ввода URL (Uniform Resource Locator) сервера, • поле numberfield для ввода числа, определяющего качество фото. Типы полей фактически устанавливают контроль ввода, так, например, поле типа urlfield не позволит вводить неправильные URL серверов, а поле типа numberfield — нечисловые значения. Значение false свойства allowBlank полей формы запрещает оставлять эти поля пустыми, а значение true свойства required означает, что поле обязательное. В диалоге предусмотрены кнопки ok и cancel. Обработчики реакции на их нажатия в виде методов onOK и onCancel должны быть реализованы в контрол90
лере панели getPhotoPnl (файл GetPhotoController.js). Наконец, при появлении формы предусмотрена реакция onShow, где должны быть установлены исходные значения полей. Вот так будет выглядеть этот диалог-форма: Рис. 25 Форма для установки (изменения) параметров приложения Показать эту форму требуется в методе settings контроллера (напомним, что это реакция на активизацию соответствующей кнопки): settings: function() { var dialog = this.dialog, view; if(!dialog) { view = this.getView(); dialog = Ext.apply(view.dialog, { ownerCmp: view }); this.dialog = dialog = Ext.create(dialog); } dialog.show(); } Если диалог ещё не был создан (свойство dialog контроллера — null), то он создаётся. При этом конфигурация диалога дополняется свойством ownerCmp, указывающим в качестве владельца панель getPhotoPnl. Реализуется это действие с помощью метода Ext.apply, принимающего исходную конфигурацию в качестве первого параметра, а свойства второго параметра будут добавлены в первый. Установка владельца требуется для соблюдения правильной соподчинённости компонент. Следует также обратить внимание, что dialog в качестве свойства контроллера содержит ссылку на созданный объект диало91
га-формы, а в качестве свойства представления — конфигурационный объект для создания этого диалога-формы (Ext.create(dialog)). При нажатии на кнопку OK требуется определиться с правильностью заполнения полей формы, установить введённые значения параметров приложения и сохранить их для использования при последующих стартах приложения (saveCfg()): onOK: function () { var form = this.lookup('form'); if(form.validate()) { this.hideDialog(); var cameraAlbum = form.getItems().items[0].getValue(), vls = form.getValues(), app = TstApp.getApplication(); app.camera = (cameraAlbum === 'camera'); app.srvr = vls.srvr; app.quality = vls.quality; app.saveCfg(); } }, hideDialog: function () { var dialog = this.dialog; if(dialog) { dialog.hide(); } } Проверка правильности введённых значений выполняется с помощью метода form.validate(). Если валидация прошла успешно, то забрать значения полей позволяет метод формы getValues(), его результат — объект, названия свойств которого совпадают с названиями полей формы, а значения свойств содержат значения полей. Особого обхождения требует кнопка segmentedbutton, поскольку её возможные значения (CAMERA и ALBUM) требуется ретранслировать в логическое значение. Сохранение параметров выполняется с помощью метода saveCfg(), который описывается далее. При нажатии на кнопку CANCEL — скрываем диалог: onCancel: function () { this.hideDialog(); } Наконец, после визуализации диалога (событие show) устанавливаем исходные значения полей: onShow: function(dialog, eOpts) { var app = TstApp.getApplication(), form = this.lookup('form'); form.setValues({ srvr: app.srvr, quality: app.quality }); 92
var btn = form.getItems().items[0]; btn.setValue(app.camera ? 'camera' : 'album'); } Для установки значений полей формы удобен метод setValues(). По своему назначению он противоположен методу getValues(), поэтому объект со значениями полей передаётся этому методу в качестве параметра. Наконец, позаботимся об удалении созданного диалога и связанных с ним ресурсов. Несмотря на то, что владельцем диалога мы назначили панель getPhotoPnl, и его разрушение должно произойти по цепочке автоматически при разрушении панели, перестрахуемся и перегрузим библиотечный метод destroy в контроллере панели, где и выполним разрушение диалога-формы: destroy: function() { Ext.destroy(this.dialog); this.callParent(); } Следует обратить внимание, что мы не забываем также вызвать исходный библиотечный метод destroy, обращение к которому выполняется так — this.callParent(). Библиотека ExtJS, таким образом, обладает полным спектром всех возможностей объектно-ориентированного программирования, в том числе и возможностью перегрузки методов родительского класса. 4.6.6. Сохранение конфигурационных параметров приложения В предыдущих разделах неоднократно упоминался метод saveCfg(), предназначенный для сохранения в файле конфигурационных параметров. При очередном старте приложения эти параметры должны быть прочитаны из этого файла и использованы. Действительно, было бы нехорошо заставлять пользователя каждый раз по-новой настраивать URL сервера, предназначенного для обработки фото, или выбирать качество фото. Наконец, последнее использованное фото было бы хорошо вновь показать пользователю. Хотя метод saveCfg() и является не методом контроллера, а методом приложения, но поскольку он активно используется именно в контроллере, то имеет смысл обсудить его здесь, в одном из разделов, посвящённых его описанию. Заметим, что обычно редко возникает необходимость в корректировке сгенерированного по умолчанию класса приложения. Решение дополнить его специфичной функциональностью связано с возможностью удобного доступа к объекту приложения из любой точки кода — TstApp.getApplication(). Итак, метод saveCfg() — метод приложения, то есть он должен быть представлен в файле Application.js, где описывается класс приложения: Ext.define('TstApp.Application', { extend: 'Ext.app.Application', name: 'TstApp', srvr: 'http://my.server.name/', 93
camera: true, photo: './resources/placeholder_200x200.png', quality: 100, saveCfg: function() { var cfg = { srvr: this.srvr, camera: this.camera, photo: this.photo, quality: this.quality }; this.writeToFile( 'cfg.json', JSON.stringify(cfg) ); }, launch: function () { var me = this; var getPhotoPnl = me.getMainView().down('getPhotoPnl'), vm = getPhotoPnl.getViewModel(); var placeholder = getPhotoPnl.lookupReference('placeholder'); me.readFromFile('cfg.json', function(data) { var cfg = JSON.parse(data); me.srvr = cfg.srvr; me.camera = cfg.camera; me.photo = cfg.photo; me.quality = cfg.quality; placeholder.setSrc(me.photo); vm.set('uploadBtnDisabled', false); }); }, readFromFile: function(filenm, cb) { // … }, writeToFile: function(filenm, data) { // … }, // … }); Как это работает? Здесь определяется класс приложения TstApp.Application (наследует библиотечный класс Ext.app.Application). Мы дополнили этот класс свойствами для хранения конфигурационных параметров: • srvr — сервер; • camera — логическое значение, если true, то используется камера, иначе готовое фото извлекается из альбома устройства; • photo — спецификация файла, содержащего последнее использованное фото; • quality — качество фотографии. 94
Собственно, сам метод saveCfg() формирует объект конфигурации cfg, содержащий перечисленные параметры, и записывает его json-представление в файл cfg.json с помощью вспомогательного метода writeToFile(). Метод launch() — удобное место для выполнения всевозможных дополнительных действий при запуске приложения, например чтение конфигурационных параметров из файла конфигурации, которое реализуется с помощью вспомогательного метода readFromFile(). Рассмотрим, как реализуется запись в файл. Во-первых, точно так же, как это было сделано в п. 4.2, посвящённом работе с файловой системой, и далее в п. 4.6.2, посвященном работе с камерой, создаём файл с заданным именем (filenm) в каталоге cordova.file.dataDirectory: writeToFile: function(filenm, data) { var me = this; window.resolveLocalFileSystemURL(cordova.file.dataDirectory, function (directoryEntry) { directoryEntry.getFile(filenm, { create: true }, function (fileEntry) { // здесь с помощью fileEntry // осуществляем запись data в filenm }, me.onError.bind(me, filenm) ); }, me.onError.bind(me, filenm) ); } В случае успешного создания файла получим представляющий этот файл объект fileEntry, с помощью которого выполняем запись данных data: fileEntry.createWriter( function(fileWriter) { fileWriter.onwriteend = function (e) { if( me.getMainView().getMasked() ) me.getMainView().unmask(); }; fileWriter.onerror = function (e) { if( me.getMainView().getMasked() ) me.getMainView().unmask(); Ext.Msg.alert('Write failed!', e.toString()); }; fileWriter.write(data); С помощью fileEntry.createWriter() создаётся объект fileWriter, метод write() которого позволяет выполнить запись данных. Этот объект также позволяет «поймать» момент окончания записи (fileWriter.onwriteend) и среагировать на возникновение ошибки (fileWriter.onerror). В обработчиках этих событий снимаем маску с главного окна приложения, если она была установлена. Если по каким-то причинам не удаётся получить доступ к каталогу cordova.file.dataDirectory, или не удаётся создать файл в этом каталоге, то вызывается метод onError(), выполняющий обработку ошибок. Как это делается, опишем далее. Рассмотрим, как реализуется чтение данных из файла. Прежде всего отметим, что методу readFromFile, реализующему чтение данных из файла, по95
требуется передать call-back-функцию (cb), которая должна быть вызвана после того, как данные будут прочитаны: readFromFile: function(filenm, cb) { var me = this; var path = cordova.file.dataDirectory + filenm; window.resolveLocalFileSystemURL(path, function (fileEntry) { // здесь с помощью fileEntry // осуществляем чтение данных из filenm }, me.onError.bind(me, filenm) ); } Как видно из представленного фрагмента кода, добраться до объекта fileEntry при чтении оказалось несколько проще, чем при записи данных в файл, который создаётся, если его нет. Последнее обстоятельство при записи в файл диктует необходимость сначала добраться до объекта directoryEntry, а затем с помощь его метода getFile() получить fileEntry. Чтение выполнятся следующим образом: fileEntry.file( function(file) { var reader = new FileReader(); reader.onloadend = function (e) { cb.call(me, this.result); }; reader.readAsText(file); }, me.onError.bind(me, filenm) ); Здесь реагируем на событие onloadend вызовом call-back-функции, которой передаём прочитанные данные (this.result). Сall-back-функция полученные данные в формате json преобразует в объект cfg, свойства которого «раскладывает по полочкам» в объекте приложения (смотри выше). Обработка ошибок (метод onError()) реализуется единообразно следующим образом: onError: function(filenm, e) { var mainView = this.getMainView(); if( mainView.getMasked() ) mainView.unmask(); var msg = ''; switch (e.code) { case FileError.QUOTA_EXCEEDED_ERR: msg = 'Storage quota exceeded'; break; case FileError.NOT_FOUND_ERR: msg = 'File not found'; break; case FileError.SECURITY_ERR: msg = 'Security error'; break; case FileError.INVALID_MODIFICATION_ERR: msg = 'Invalid modification'; break; case FileError.INVALID_STATE_ERR: msg = 'Invalid state'; break; default: msg = 'Unknown error'; break; }; var errMsg = 'Error (' + filenm + '): ' + msg; if(filenm == 'cfg.json') { 96 Powered by TCPDF (www.tcpdf.org)
var placeholder = this.getMainView().down('getPhotoPnl').lookupReference('placeholder'); placeholder.setSrc(this.photo); } Ext.Msg.alert('Error!', errMsg); } Прокомментируем следующие моменты, которые вероятно вызовут недопонимание: • во-первых, снимается маска с главного окна, поскольку перед выполнением операций с файловой системой работа с интерфейсом приложения блокируется установкой маски, которую, конечно, надо убрать и в случае неудачной работы; • во-вторых, если проблемы возникают с конфигурационным файлом (cfg.json), то в панели getPhotoPnl устанавливается заставка ./resources/ placeholder_200x200.png. Например, при первом запуске приложения попытка прочитать конфигурацию из этого файла будет неудачной по причине отсутсвия файла cfg.json. 4.7. Компонент showResultPnl Компонент showResultPnl уже был представлен выше, в п. 4.5. Он указан в качестве второго в карточной разметке главного окна. К этому компоненту пользователь переходит при активации последней кнопки в тулбаре панели getPhotoPnl — переход к следующему компоненту, позволяющему получить результат обработки загруженного на сервер фото. Инструментарий (toolbar) панели showResultPnl состоит всего лишь из одной кнопки, позволяющей вернуться к исходной панели getPhotoPnl. Для генерации кода компонента (то есть полного набора файлов с определениями классов модели данных, представления и контроллера — Model, View, Controller) выполним команду: sencha generate view -b Ext.Panel -n result.ShowResultPnl В результате, в каталоге app/view получим файловую структуру, представленную на рисунке: Рис. 26 Результат генерации компонента showResultPnl 97
4.7.1. Визуальное представление компонента Согласно соглашениям об именовании и определении классов определение класса визуального представления компонента showResultPnl находится в одноимённом файле showResultPnl.js: Ext.define('TstApp.view.result.ShowResultPnl',{ extend: 'Ext.Panel', xtype: 'showResultPnl', requires: [ 'Ext.Button', 'Ext.Img', 'Ext.Toolbar' ], controller: 'result-showresultpnl', viewModel: { type: 'result-showresultpnl' }, layout: { type:"vbox", pack:"center", align:"stretch" }, items: { reference: 'resultholder', xtype: "image", flex: 1 }], tbar: [{ reference: 'backToGetPhoto', text: "back", handler: 'backToGetPhoto', iconCls: 'x-fa fa-angle-double-left' }] }); Представленный код во многом аналогичен коду определения класса представления компонента getPhoto, рассмотренного выше. Класс определяется с помощью метода define() библиотеки ExtJS, которому передаются название класса TstApp.view.result.ShowResultPnl и конфигурационный объект, содержащий уже знакомые свойства: • cвойство extend указывает родительский класс Ext.Panel; • в свойстве xtype задаётся псевдоним класса; • свойство requires определяет классы, используемые в данном определении, которые предварительно должны быть загружены; • наконец, свойства controller и viewModel задают псевдонимы классов контроллера и модели в нашей связке MVC; • Расположение дочерних компонент в пределах рабочего пространства родительской панели определяется свойством layout, согласно которому по98
следние располагаются вертикально (type: "vbox"), отцентрованы по вертикали (pack: "center") и растянуты по горизонтали (align: "stretch"); • дочерние компоненты задаются в свойстве items. В нашем случае — это единственный компонент, псевдоним класса которого image. Такой псевдоним имеет уже знакомый библиотечный класс Ext.Img, инстанции которого предназначены для изображений. Свойство flex, равное 1, позволит изображению занять всё доступное место в рамках родительской панели. Также следует обратить внимание на ссылочное имя resultholder (свойстве reference). Вспомним, что это имя использовалось по назначению в контроллере компонента getPhotoPnl в методе result; • наконец, тулбар компонента (свойство tbar) содержит единственную кнопку backToGetPhoto для возврата к исходной панели getPhotoPnl, одноимённая реакция на активацию этой кнопки раскрывается в контроллере компонента далее. 4.7.2. Контроллер компонента Контроллер компонента showResultPnl довольно простой, здесь реализуется единственная реакция на нажатие кнопки возврата к панели getPhotoPnl: Ext.define('TstApp.view.result.ShowResultPnlController', { extend: 'Ext.app.ViewController', alias: 'controller.result-showresultpnl', backToGetPhoto: function() { var mainCtr = this.getView().up('mainCtr'); mainCtr.setActiveItem(0); } }); В методе backToGetPhoto поднимаемся вверх в иерархии компонент с помощью метода up до главного контейнера с псевдонимом mainCtr. Вспомним, что это главное окно приложения, содержащие обе панели getPhotoPnl и showResultPnl. Теперь достаточно сделать активной требуемую панель с помощью метода setActiveItem главного контейнера. 4.8. Заключение В данном разделе подробно описан процесс разработки гибридного приложения Cordova на основе одного из самых мощных в настоящее время фреймворков Sencha. Последний располагает бесплатным инструментом Sencha Cmd и бесплатной версией библиотеки визуальных компонент Sencha ExtJS, позволяющими активно использовать парадигму Model View Controller в процессе разработки интерфейса web-приложений. Возможности Sencha Cmd включают весь спектр функций от генерации стартовых шаблонов кода и до окончательной сборки приложения и интеграции с Cordova. Представленная технология в подробностях описывается на примере приложения, демонстрирующего использование плагинов для доступа к нативным функциям мобильного устройства, камере, сети, файловой системе. Демонстрируется интеграция мобильного приложения с webсерверной обработкой. Полный код представлен в приложении. 99
5. РАЗВЁРТЫВАНИЕ (DEPLOYMENT) И РАСПРОСТРАНЕНИЕ (DISTRIBUTING) IOS-ПРИЛОЖЕНИЙ Процессы развёртывания (установки) на физических устройствах и распространения iOS-приложений вне Xcode — процессы весьма непростые и не бесплатные. Второй термин — distributing — относится к массовой установке приложений вне программы Xcode, то есть через App Store, или пользуясь adhoc и in-house распространением (здесь всё-таки чаще употребляются термины ad-hoc или in-house deployment, а не distributing). Имея Apple ID и Xcode 7-й версии или старше, можно бесплатно установить приложение на устройство (Free Provisioning), но через неделю приложение перестанет работать, и процедуру установки придётся повторить. В любом случае потребуется macкомпьютер от Apple. Конечно, можно установить Mac OS и Xcode на обычный компьютер, используя виртуальную машину или специальные загрузчики и софт от «хакинтош», но эти затеи неблагодарны, поскольку зачастую заканчиваются блокировкой Apple ID, или результат работает слишком медленно, или обновления устанавливаются «криво». В данном разделе обсуждаются принципиальные основы Appleтехнологий развёртывания (распространения) iOS-приложений и связанные с этими технологиями термины. Детально обсуждаются установка приложений на устройство с помощью Xcode, а также сборка приложений ad-hoc и дальнейшая установка (deployment) сборки на устройства через Интернет. Материал этого раздела послужит хорошей основой для освоения технологий внутреннего и внешнего тестирования приложения в TestFlight с дальнейшей загрузкой в AppStore. Оба этих процесса используют сервис Apple, называемый iTunes Connect. Отметим, что работа с этим сервисом также выполняется из среды Xcode. Наглядное представление об этих процессах даёт нижеследующий рисунок, заимствованный из документации Apple. Рис. 27 Наглядное представление процесса разработки приложения от регистрации — зачисления разработчика в одну из программ (enroll) — до публикации в AppStore 100
5.1. Enroll in Program Если не ограничиваться возможностями Free Provisioning, то потребуется присоединиться к одной из платных программ: Apple Developer Program или Apple Developer Enterprise Program. Различие этих двух программ в основном состоит в том, что участие в первой дает право на централизованное распространение приложений через App Store. Участие во второй ориентировано на распространение приложений только внутри фирмы (организации). Это так называемое In-House распространение (за пределами App Store). Участие в первой программе обойдется в 99$, участие во второй — 299$ в год. В рамках любой программы допускается так называемое Ad Hoc распространение, позволяющее устанавливать приложения на 100 устройствах в год. Обсудим ряд особенностей процедуры Enroll in Program (дословно — зачисление в программу), которая, на первый взгляд, должна быть аналогична покупке в интернет-магазине с регистрацией в личном кабинете. Тем не менее эта процедура несколько сложнее и подробно описывается в этом разделе (хотя эта процедура в деталях, конечно, может быть изменена со временем). Прежде всего необходимо учесть следующее: • не следует путать порталы developer.apple.com и appleid.apple.com. Если на первом хранится информация об активах разработчика (сертификаты, профили и т. д.), то второй — хранит учётную информацию. Оба портала используют один и тот же Apple ID; • если на портале developer.apple.com начать процедуру присоединения к одной из платных программ (эта процедура дословно называется зачислением — enrolling), то с определённого момента её нельзя будет откатить назад — её можно только продолжить (во всяком случае на некоторое время); • Apple очень щепетильно относится к вопросам конфиденциальности и секретности, поэтому покупку членства в одной из платных программ желательно оплачивать картой на имя владельца учётной записи и указывать учётные данные (легальное имя и фамилию на латинице, адрес) в полном соответствии с персональными данными, хранящимися на портале appleid.apple.com. На этом портале заранее следует определиться со способом оплаты. • следует установить двухфакторную аутентификацию для своего Apple ID. Итак, имея Apple ID (если отсутствует, то его придётся создать), авторизуемся на портале http://developer.apple.com/account, после чего Apple предложит принять соглашение — Apple Developer Agreement (показано на скриншоте ниже). Заметим, что интерфейс страниц со временем, скорее всего, изменится внешне, но по сути процедура зачисления, описанная здесь, останется неизменной или изменится незначительно. 101
Рис. 28 После первой авторизации на портале Apple разработчику предложат прочитать и принять соглашение — Apple Developer Agreement Принимаем соглашение и попадаем на страницу (скриншот представлен ниже) с немногочисленными активами разработчика, который еще не присоединился ни к одной из программ — это загрузка Xcode, документация, и наконец, возможность присоединиться к Apple Developer Program. Рис. 29 Активы разработчика на портале developer.apple.com, не участвующего ни в одной из программ 102
Выбираем Join the Apple Developer Program — попадаем на страницу с кратким описанием преимуществ, которые предоставляет зачисление к одной из программ. Рис. 30 Зачисление в Apple Developer Program даёт разработчику возможность размещать приложения в App Store, тестировать приложения и использовать сервисы Apple Выбираем Enroll — переходим на следующую страницу (скриншот представлен ниже), где описываются требования для зачисления в качестве индивидуума (физического лица) или организации. Рис. 31 Требования для двух типов зачисления — в качестве индивидуума или организации 103
Если регистрируется физическое лицо, то в качестве продавца в App Store будет фигурировать его имя, если организация, то название этой организации. Для зачисления организации требуется как минимум наличие номера D-U-N-S, сайта с доменным именем, ассоциированным с названием организации. Ограничимся обсуждением варианта зачисления в качестве индивидуума. В конце представленной выше страницы можно найти кнопку Start You Enrollment — стартуем Enrollment — результат представлен на рисунке ниже. Здесь следует быть осторожным, поскольку процесс приобщения к сообществу разработчиков с данного момента в определённом смысле «зависает». Это означает, что даже повторный вход в учётную запись не прерывает этот процесс. Отказаться и вернуться к первоначальному состоянию активов учётной записи можно только спустя некоторое неопределённое время или после запроса об этом в службу поддержки. Рис. 32 Присоединение к одной из программ разработчиков требует двухфакторной аутентификации Как видно из представленного выше скриншота, зачисление в одну из программ невозможно, пока разработчик не установит двухфакторную аутентификацию (Two-factor authentication) для своего Apple ID. Благодаря двухфакторной аутентификации вход в учетную запись может быть выполнен только на доверенных apple-устройствах — iPhone, iPad или Mac. Устройство становится доверенным после первого входа в учетную запись с этого устройства, при этом кроме пароля потребуется предоставить шестизначный код подтверждения, который автоматически отображается на других доверенных устройствах. Например, если у вас есть iPhone и вы впервые входите в свою учетную запись на новом Mac, вам будет предложено ввести ваш пароль и код подтверждения, который будет автоматически отображаться на вашем iPhone. Также желательно определиться со способом оплаты на портале applied.apple.com и другими персональными данными, включая фамилию и имя на латинице, адрес и почтовый индекс. В разделе «Оплата и доставка» (Payment & Shipping) следует указать кредитную карту, оформленную на имя владельца 104
учётной записи. В дальнейшем вернувшись к процедуре зачисления на портале developer.apple.com, следует в точности повторить ввод этой информации. В противном случае после списания средств с карты есть риск получить письмо от Apple с требованием предоставить government-issued photo ID (скриншот такого письма представлен ниже). Рис. 33 Подобное письмо можно получить вместо кода активации учётной записи на портале developer.apple.com, если указать несовпадающую информацию при регистрации Apple ID и при покупке членства в программе разработчиков В результате придётся высылать фото — заграничный паспорт рядом с лицом владельца учётной записи и паспорта (всё должно быть читабельно). В случае успеха придёт почтовое уведомление, содержащее ссылку активации учетной записи. Тем не менее Apple допускает использование различных способов оплаты в iTunes Store, App Store, Apple Online Store и т. д. даже с использованием чужих карт. После высылки government-issued photo ID проблема обычно решается. Установив двухфакторную идентификацию (как это делается, подробно описывается в разделе Security после входа в учётную запись Apple ID на портале appleid.apple.com), продолжаем регистрацию (рисунок ниже) — проверяем Email, Name и Country и в поле Entity Type выбираем Individual — регистрируемся в качестве физического лица. Другой вариант Entity Type — это команда разработчиков, у которых разные роли и полномочия. Для физического лица команда состоит из одного члена в роли агента, имеющего все полномочия. В команду можно приглашать иных участников. Рис. 34 Регистрируемся как физическое лицо, в поле Entity Type выбираем Individual 105
В следующей форме, представленной ниже, следует указать в точности ту же контактную информацию, которая была введена для учётной записи Apple ID в разделе Payment & Shipping (оплата и доставка). Рис. 35 В этой форме желательно продублировать персональные данные, указанные для учётной записи Apple ID в разделе Payment & Shipping (оплата и доставка) Далее принимаем лицензионное соглашение — ставим галочку. Рис. 36 Принимаем лицензионное соглашение На следующей странице Apple ещё раз показывает введённые данные, проверяем данные, и если всё правильно, то продолжаем. Рис. 37 Проверяем введённые данные и продолжаем — Continue 106
На странице, представленной далее, запрашивается разрешение на автоматическое ежегодное списание средств на оплату регистрации — соглашаемся (или не соглашаемся) и переходим к покупке. Рис. 38 Ставим галочку, тем самым даём разрешение на автоматическое ежегодное списание средств за регистрацию На следующей странице (скриншот показан ниже) повторно требуется ввести Apple ID и пароль. Это на первый взгляд излишнее действие объясняется тем, что покупка реализуется с помощью самостоятельного сервиса Apple Online Store, который требует отдельной аутентификации. Рис. 39 Повторная аутентификация объясняется тем, что покупка осуществляется с помощью самостоятельного сервиса Apple Online Store Далее рекомендуется указать те же самые контактные данные, которые уже указывались ранее, и которые дублируют данные учётной записи Apple ID (на портале appleid.developer.com). Это касается и способа оплаты — желательно использовать тот же способ, который указан для учётной записи Apple ID, кредитная карта должна быть оформлена на владельца учётной записи Apple ID. 107
Рис. 40 Желательно, чтобы не было расхождений с данными, указанными ранее и для учётной записи Apple ID В течение суток Apple пришлёт письмо с кодом активации, пример письма представлен ниже. Достаточно выполнить переход по ссылке для активации учётной записи разработчика. Рис. 41 Для активации учетной записи разработчика достаточно перейти по ссылке Всё — регистрация физического лица как разработчика приложений Apple завершена. Теперь после входа в свою учётную запись на сайте developer.apple.com разработчик найдёт весь необходимый спектр инструментов — создание сертификатов и профилей, регистрация устройств и т. д. Назначение и работа с этими активами частично описываются далее. 5.2. О подписи кода Прежде чем обсуждать формальные действия, связанные с установкой и распространением подписанных iOS-приложений, хотелось бы неформально разобраться в сути технологий и терминов, которые за этими действиями кро108
ются. В первую очередь это касается безопасности распространяемого кода, которая обеспечивается подписью кода. Подпись кода — это технология безопасности, которая гарантирует, что приложение было создано данным разработчиком и не было изменено с момента его установки на устройство. Как только приложение будет подписано, система может обнаружить любые изменения в коде приложения вне зависимости от того, внесено ли изменение случайно или вредоносным кодом. Как это работает? Зарегистрированный разработчик получает возможность создавать так называемые signing identity. По сути, это два ключа: публичный и приватный, которые предназначены для ассиметричного шифрования и дешифрования. Чтобы быть точнее, отметим, что signing identity содержит не публичный ключ непосредственно, а сертификат, в который встроен публичный ключ. Сертификат может быть получен в результате специального запроса, автоматически выполняемого Xcode или утилитой Keychain Access — Связка ключей (об этом далее рассказывается подробнее). Не вникая в тонкости этих процессов, важно понять, что приватный ключ используется для шифрования, а публичный, для дешифрования любых двоичных данных. Приватный — секретный — ключ хранится только на компьютере разработчика и не должен покидать его пределов. Apple к этому ключу доступа не имеет — не получает и не хранит (имеются в виду портал developer.apple.com и активы учётной записи разработчика). Apple хранит публичные ключи, которые фактически являются сертификатами (публичный ключ встраивается в сертификат). Важно — если разработчик по каким-то причинам утратил приватные ключи, то ему придётся пересоздавать все сертификаты и так называемые профили (provisioning profile), которые использовали эти сертификаты. О профилях речь пойдёт далее. Ключи создаются на машине разработчика с помощью специальной программы «Связка ключей» (Keychain Access — на англоязычных компьютерах). Эта утилита входит в состав любой версии mac OS. Для синхронизации с порталом разработчиков чтобы эта связка ключей получила юридическую силу, с помощью этой утилиты создаётся специальный запрос (Certificate Signing Request — CSR). Этот запрос передаётся Apple, в ответ Apple создаёт соответствующий сертификат, который разработчик может загрузить на свой компьютер и использовать по назначению. Образно можно сказать, что Apple подписывает этот сертификат, дополняя его своей учётной информацией — так что если ктонибудь (имеется в виду программное обеспечение) поинтересуется по поводу этого сертификата, то Apple ответит положительно. Таким образом, Apple ручается за личность подписывающего лица. Описанная процедура выполняется намного проще — автоматически с помощью программы Xcode, которая, на самом деле, берёт на себя взаимодействие с утилитой Keychain и порталом разработчиков, и делает она это незаметно для разработчика. После создания сертификатов для безопасности следует сохранить их в надёжном месте с помощью всё тех же программ: Keychain или Xcode. 109
Следующий рисунок (заимствован из документации Apple) поясняет месторасположение и соотношение основных составляющих, используемых для подписи кода — публичных и приватных ключей, сертификатов и идентичностей (signing identity). Рис. 42 Сертификаты и соответствующие им приватные ключи — Signing Identities — хранятся на компьютере разработчика, а сертификаты — на портале подписанных разработчиков, в активах их учётных записей Поясним представленный рисунок. Под Signing Identities понимаются две составляющие — это приватный ключ (Private Key) и публичный ключ (Public Key), точнее сертификат, в который этот ключ встроен. Идентичности (Signing Identities) хранятся на компьютере разработчика под управлением специальной утилиты — Keychain (Связка ключей). Сертификаты дополнительно дублируются в активах учётной записи разработчика. Сертификаты создаются центром сертификации Apple после передачи разработчиком публичных ключей в виде специального запроса CSR. Фактически Apple укомплектовывает публичные ключи дополнительной информацией — подписывает сертификат (если, конечно, полномочий учётной записи разработчика достаточно). Механизм формирования подписи любых двоичных данных представлен на следующем рисунке, заимствованном из документации Apple. Поясним рисунок. Прежде всего формируется хеш (message digest) подписываемых двоичных данных — некоторая более короткая свёртка этих данных, формируемая с помощью специальных алгоритмов хеширования. Важно, что любое изменение исходных данных с большой вероятностью приводит к изменению хеша. Далее этот хеш шифруется с помощью приватного ключа — формируется digital signature. Три составляющие: исходные данные, шифрованный хеш и сертификат (который содержит публичный ключ) — упаковываются в один файл, образуя подписанные данные (code-signed data). 110
Рис. 43 Механизм формирования подписанных данных Представим теперь, что исходные данные каким-то образом изменены. Выявить наличие этих изменений при наличии подписи не составляет труда. Действительно, с помощью публичного ключа дешифруется подпись — получается хеш на момент подписи данных. Далее этот же хеш формируется с помощью применения тех же самых алгоритмов хеширования непосредственно к данным. Оба результата должны совпадать, иначе делается вывод, что данные фальсифицированы. Точно так же может быть подписан любой программный код, который по сути является двоичными данными, о подписи которых речь шла выше. Как и в общем случае, подпись кода предполагает наличие следующих составляющих: Пломба — seal. Это набор контрольных сумм или хешей различных частей кода, созданных с помощью специальных алгоритмов хеширования. Пломба используется во время проверки для обнаружения изменений. Цифровая подпись — digital signature. Это шифрованная с помощью приватного ключа пломба. Правила, регулирующие проверку подписи кода. Некоторые из них установлены верификатором (в зависимости от его целей), другие определяются подписывающим лицом и пломбируются (хешируются) вместе с остальной частью кода. Чтобы проверить целостность кода, необходимо вычислить набор хешей для различных блоков кода и данных, который хранится в зашифрованном виде в цифровой подписи. Затем с помощью открытого ключа, встроенного в сертификат, цифровая подпись дешифрируется. Таким образом получаются исходные хеши, вычисленные подписывающим лицом. Если оба набора хэшей совпада111
ют, то код не был изменён. Наконец, заметим, что подписанный код может содержать несколько различных цифровых подписей. Сам сертификат обычно подписывается доверенным центром сертификации Apple в процессе запроса сертификата с последующей загрузкой сертификата на компьютер разработчика. Этот процесс кратко описан выше. Таким образом, Apple может ручаться за личность подписывающего лица. 5.3. Формальная процедура получения сертификатов с помощью Xcode Прежде всего разработчик должен сообщить Xcode свою учетную запись Apple ID. Для этого следует: • открыть Xcode; • выбрать в меню Xcode — Preferences.; • перейти на вкладку Accounts; • нажать кнопку + в левом нижнем углу и выбрать Apple ID из всплывающего меню; Рис. 44 Добавляем Apple ID зарегистрированного разработчика в Xcode • ввести Apple ID, и если такой существует, то Xcode предложит ввести пароль. Рис. 45 Вводим Apple ID и пароль В результате — Apple ID будет добавлен. В ходе описанного процесса Xcode взаимодействует с порталами applied.apple.com и developer.apple.com, а также с программой Keychain Access, которая централизованно заведует всеми 112 Powered by TCPDF (www.tcpdf.org)
ключами, паролями и сертификатам в mac OS. Картинка, представленная ниже, соответствует разработчику, зачисленному в Apple Developer Program в качестве индивидуума. При этом общая командная логика не нарушается — будет создана команда (team) из одного участника, который в этой команде будет играть роль агента (agent). Агент обладает максимальными полномочиями. Подписанному разработчику разрешается создавать любые сертификаты: iOS Development и iOS Distribution. Сертификаты первого типа, как следует из их названия, предназначены для подписи приложений на стадии разработки, а второго типа — для подписи приложений, предназначенных для распространения вне Xcode. Для создания сертификата достаточно выбрать Manage Certificates, как показано на рисунке ниже (рис. 46). Рис. 46 Для создания сертификата следует использовать кнопку Manage Certificates В результате в отдельном окне будут показаны созданные сертификаты, а также будет предоставлена возможность создавать новые — достаточно воспользоваться кнопкой + в левом нижнем углу, как показано на рисунке ниже (рис. 47). Рис. 47 Разработчик, зачисленный в Apple Developer Program, имеет право создавать любые сертификаты 113
Сертификаты также можно создавать вручную, пользуясь активами учётной записи зарегистрированного разработчика и возможностями утилиты Keychain, но эта процедура здесь не описывается. 5.4. Provisioning Profile При установке приложения на iOS-устройство также устанавливается так называемый профиль — Provisioning Profile. Этот профиль содержит метаданные, предназначенные для выполнения всевозможных проверок, например — возможности установки приложения на данном устройстве или целостности приложения при запуске. Профиль содержит следующую информацию: • сертификат, который однозначно определяет зарегистрированного в Apple Developer Program разработчика (или команды разработчиков), а также публичный ключ, который используется для проверки целостности подписанного приложения (этот процесс проверки подробно описан выше); • идентификатор App ID, которому должен соответствовать идентификатор приложения — Bundle ID; • идентификаторы устройств (UDID), на которых допускаются установка и запуск приложения. Unique Device Identifier (UDID) — это уникальный идентификатор устройства, состоящий из 40 символов. Он есть у каждого мобильного устройства от компании Apple: iPad, iPhone или iPod Touch. Эти данные профиля гарантируют, что все, что создано во время разработки и загружено на устройство, останется в неизменном виде и может быть однозначно связано с учетной записью разработчика в Apple Developer Program. Иными словами, приложение и «чихнуть» не может без соответствующего разрешения Apple. Профиль устанавливается на устройстве до установки приложения. Если информация в профиле не соответствует определенным критериям, приложение не будет установлено или запущено. Содержимое профиля наглядно представлено на следующем рисунке. Рис. 48 Содержимое профиля Provisioning Profile (рисунок заимствован из документации Apple) 114
5.4.1. App ID Идентификатор App ID должен соответствовать уникальному идентификатору приложения — Bundle ID. Идентификаторы приложений Bundle ID составляются таким же образом, как и доменные имена серверов — DNS, только в реверсивном порядке и с учётом разницы прописных и строчных букв. Например, Xcode при создании Bundle ID нового проекта объединяет заданные разработчиком идентификатор организации и название приложения. В качестве идентификатора организации обычно используется доменное имя сайта, записанное в обратном порядке, например com.example.John. В результате получается com.example.John.FirstApp, если в качестве названия приложения было введено FirstApp. Явный App ID (explicit) соответствует только одному приложению и полностью совпадает с его Bundle ID. Неявные App ID (Wildcard App ID) используют схему со звёздочками, замещающими любые символы, и описывают таким образом множество приложений со схожими Bundle ID. Например, если App ID — это com.example.John.*, то ему соответствуют com.example.John. FirstApp и com.example.john.SecondApp, назначаемые в качестве Bundle ID. Если приложение использует специальные сервисы Apple, например, Game Center, In-App Purchase, Data Protection или iCloud, то профиль такого приложения обязательно должен содержать явный App ID. Для создания App ID через портал разработчиков (developer.apple.com) достаточно выполнить следующие несложные действия: • перейти к разделу Certificate, Identifiers and Profiles; • выбрать в подразделе Identifiers работу с App IDs; • кликнуть кнопку +, как показано на рисунке: Рис. 49 Добавление App ID вручную через портал разработчиков • далее потребуется определить тип App ID — явный (explicit) или неявный (wildcard), ввести сам App ID, отметить галочками требуемые службы и нажать Continue. Эти действия реализуются в рамках одной формы и не требуют дополнительных пояснений и рисунков. 115
5.4.2. Добавление идентификаторов устройств (UDID) на портале разработчиков Политика Apple допускает установку приложений по методу Ad Hoc не более чем на ста устройствах в год. При генерации соответствующих профилей эти устройства должны быть указаны. Сделать это можно вручную после входа в свою учётную запись. Как и прежде, следует обратиться к разделу Certificate, Identifiers and Profiles, выбрать подраздел Devices и нажать кнопку +, как показано на рисунке ниже. Рис. 50 Установка приложений Ad Hoc допускается не более чем на ста устройствах в год, которые должны быть добавлены в активы учётной записи разработчика, прежде чем будут сгенерированы соответствующие профили Далее потребуется ввести узнаваемое имя (например, iPhone соседа) и указать соответствующий Unique Device Identifier (UDID). Поскольку в настройках устройства UDID найти нельзя, то для его определения устройство придётся подключить через USB к компьютеру, на котором установлен iTunes или Xcode. После того, как iTunes распознает устройство, и оно появится в виде соответствующей иконки, можно получить информацию об этом устройстве, в том числе и UDID. Следует кликнуть серийный номер — появится UDID — и выбрать из контекстного меню «скопировать» в буфер. Рис. 51 После того, как iTunes распознает устройство, следует кликнуть появившуюся иконку устройства, а в появившихся данных об этом устройстве кликнуть серийный номер, который превратится в UDID, и из контекстного меню выбирать «Скопировать» в буфер Аналогично в Xcode следует выбрать в меню Window — Devices and Simulators, в результате получим всю необходимую информацию об устройстве, включая UDID. Отметим также, что если установка некоторого приложения на устройство выполнялась непосредственно через Xcode, то UDID этого устройство автоматически попадёт в активы учётной записи разработчика. 116
5.4.3. Создание профилей вручную (через портал разработчиков) Созданные App ID и добавленные устройства (точнее, их UDID) можно задействовать в профилях, которые создаются вручную на портале разработчиков или автоматически через Xcode. Для ручной генерации профиля в том же разделе — Certificate, Identifiers and Profiles — следует перейти к подразделу с профилями и нажать +, как показано на рисунке. Рис. 52 После создания App ID можно перейти к созданию профилей, в которых эти App ID будут задействованы Далее будет предложено пройти несколько этапов от выбора типа профиля до его загрузки на компьютер: Select Type — Configure — Generate — Download, как представлено на рисунке далее. Рис. 53 При генерации профиля прежде всего потребуется указать тип профиля Выбор типа определяется назначением профиля — для разработки (Development) или распространения (Distribution), операционной системой (iOS или tvOS), а также способом распространения — на рисунке, представленном выше, выбран способ Ad Hoc. 117
На стадии конфигурации (Configure) потребуется выбрать App ID, выбрать сертификат и указать UDID устройств. Все эти активы добавляются на портал заранее, как описано выше. На стадии Generate профилю даётся имя, желательно такое, чтобы в дальнейшем можно было догадаться, для каких целей профиль был создан. Наконец, на стадии Download созданный профиль можно загрузить на компьютер — он загрузится в виде файла с расширением mobileprovision. Для того чтобы этот файл «подхватил» Xcode, достаточно его дважды кликнуть мышкой. Загрузить профили можно также из среды Xcode, если перейти к окну управления учётными записями (Xcode — Preferences... — Accounts) и нажать кнопку Download Manual Profiles (скриншот окна представлен на одном из рисунков выше). Также отметим, что при сборке и установке приложения на устройство непосредственно в Xcode последний может автоматически создавать профили типа Development на портале разработчиков. 5.4.4. Создание профилей автоматически (через Xcode) Как уже неоднократно упоминалось выше, Xcode может взять на себя взаимодействие с порталом разработчиков для создания требуемых активов, в том числе и профилей. Предположим, что учётная запись разработчика, зарегистрированного в Apple Developer Program, добавлена в Xcode, и есть проект приложения, например гибридного приложения, сгенерированного с помощью Cordova. Открываем проект двойным щелчком мыши по файлу с расширением xcodeproj. В результате откроется навигатор проекта — скриншот представлен ниже. В навигатор проекта можно попасть, выбрав в меню View — Navigators — Show Project Navigator. Рис. 54 Здесь следует определится с командой разработчиков: в поле Team можно будет выбрать одну из групп — команду, в которой участвуют зарегистрированные ранее учётные записи В разделе Signing по умолчанию включена опция автоматического управления, то есть Xcode берёт на себя все заботы по созданию и обновлению профилей, идентификаторов App Ids и сертификатов. Все эти действия Xcode 118
выполнит после выбора в поле Team команды (группы) разработчиков. В этом поле Xcode автоматически подтянет все команды разработчиков, в которых принимают участие владельцы введённых учётных записей. Если разработчик зачислен как индивидуум, то команда будет состоять из одного члена в роли агента. Выбираем команду — результат представлен на рисунке ниже. Рис. 55 Если разработчик зачислялся в Apple Developer Program как индивидуум, то команда будет состоять из одного члена в роли агента Отметим, что такой же результат будет и для разработчика, не зарегистрированного в Apple Developer Program. В этом случае будет иметь место так называемое Free Provisioning. В рамках этой возможности разработчик сможет устанавливать приложения на мобильные устройства через USB, правда, с рядом ограничений, и работать такие приложения будут только неделю. В результате, как следует из рисунка, управление профилями, идентификаторами App ID, UDID и сертификатами будет полностью возложено на Xcode, который также в случае необходимости будет незаметно взаимодействовать с порталом разработчиков (с активами учётной записи разработчика). Такое положение вполне приемлемо на стадии разработки приложения, когда достаточно профиля типа development. Однако на стадии сборки приложения для его распространения вне Xcode (например, типа Ad Hoc) потребуется отключить автоматическое управление профилями. 5.5. Бесплатный ssl-сертификат Прежде чем описать процессы сборки и создания архива приложения для дальнейшего его распространения через Интернет, кратко опишем процесс создания ssl-сертификата для сайта, где этот архив будет храниться. Дело в том, что Apple допускает загрузку и установку приложений на устройство только по защищённому каналу ssl (Secure Socket Level). Поясним кратко принцип работы защищённого канала. Главная идея состоит в том, что данные, которыми обмениваются клиент и сервер, шифруются и дешифруются с помощью так называемого сеансового ключа, о котором знают только клиент и только сервер. Ключ симметричный — то есть он используется как для шифрования, так и дешифрования данных, поэтому если ключ будет перехвачен, то, очевидно, соединение будет взломано. Оказывается, что этот 119
ключ никогда не передаётся между клиентом и сервером в незашифрованном виде, и вот как это реализуется. Предположим, что клиент и сервер уже договорились об алгоритмах шифрования (это происходит на начальной стадии формирования соединения). Затем сервер посылает клиенту ssl-сертификат, который содержит публичный ключ, данные о типе сертификата, его владельце и третьей стороне, которая подтверждает его подлинность. Клиент после подтверждения подлинности сертификата третьей стороной создаёт случайную последовательность — сеансовый ключ — и шифрует его с помощью полученного публичного ключа. Напомним, что при асимметричном шифровании расшифровать эту последовательность можно только с помощью приватного ключа. Приватный ключ хранится только на сервере, поэтому вполне безопасно можно передать зашифрованный сеансовый ключ серверу (даже если его перехватят, расшифровать его не представляется возможным, поскольку нет приватного ключа, который в обмене не участвует). Сервер дешифрует сеансовый ключ с помощью приватного ключа. Таким образом и клиент, и сервер обладают единым ключом для симметричного шифрования. Далее обмен осуществляется сообщениями, зашифрованными с помощью этого уникального сеансового ключа. Ещё раз подчеркнём, что при передаче сеансового ключа от клиента к серверу используется ассиметричное шифрование, а затем этот сеансовый ключ уже используется для симметричного шифрования данных как клиентом, так и сервером. Наконец, отметим, что шифруются и дешифруются данные запросов и ответов, которые полностью соответствуют http-протоколу, который в таком варианте принято называть https (http secure). Исходя из представленного механизма, очевидно, требуется создать два ключа — приватный и публичный. Затем публичный ключ упаковать в сертификат ssl, который вместе с приватным ключом должен быть установлен на сервере. В процессе генерации ключей нет ничего сложного, однако сложность составляет именно упаковка публичного ключа в ssl-сертификат, которая выполняется третьей стороной — центром сертификации (Certificate Authority — CA) — после выполнения ряда проверок. В зависимости от типа проверок и солидности центра сертификации определяется и стоимость получения сертификата. В самом простом варианте проверяется только принадлежность домена заявителю. В принципе, не исключается возможность формирования так называемых самоподписных ssl-сертификатов без привлечения третьих лиц, но в большинстве случаев такие сертификаты не признаются браузерами — как минимум будут выданы пугающие предупреждения. В настоящее время появилась возможность получения бесплатного сертификата от центра сертификации Let’s Encrypt (https://letsencrypt.org). Этот центр функционирует несколько необычно — через специальный сервис, о котором можно подробнее узнать на официальном сайте. Однако делать это не обязательно, поскольку некоторые хостеры уже обеспечивают своим клиентам автоматическое взаимодействие с этим сервисом. Кроме этого, хорошим посредником является сайт https://www.sslforfree.com/, который обеспечивает стандартный способ получения сертификата, принятый у большинства центров 120
CA. Недостатком такого сертификата является слишком малый срок его действия — всего три месяца, после чего его потребуется переиздать. Итак, в любом случае прежде всего потребуется сгенерировать два ключа — приватный и публичный. При этом специальные утилиты, предназначенные для этих целей, вместо публичного ключа создают так называемый запрос в центр сертификации на создание сертификата — Certificate Secure Request (CSR), который содержит публичный ключ. Наиболее известной утилитой является утилита openssl, которая входит в состав unix подобных операционных систем, в том числе mac OS. Чтобы сгенерировать с её помощью приватный ключ, достаточно в терминальном окне выполнить команду: openssl genrsa -out private.key В результате в текущем каталоге появится файл private.key, содержащий приватный ключ (конечно, имя файла можно придумать и другое). Далее для этого приватного ключа создаём публичный ключ — точнее CSR: openssl req -new -key private.key -out request.csr В ответ утилита запросит дополнительную информацию, которая будет встроена в сертификат. Далее приводятся примеры ответов (выделены серым шрифтом), среди которых важно правильно указать доменное имя сервера DNS, для которого создаётся сертификат: You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----Country Name (2 letter code) [AU]:RU State or Province Name (full name) [Some-State]:Saint-Petersburg Locality Name (eg, city) []:Saint-Petersburg Organization Name (eg, company) [Internet Widgits Pty Ltd]:Peterhost.ru Organizational Unit Name (eg, section) []:Virtual Hosting Common Name (e.g. server FQDN or YOUR name) []:my.sitedns Email Address []:support@peterhost.ru Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []: An optional company name []: В результате в текущем каталоге появится файл request.csr (имя файла можно выбрать и другое, чаще всего используется доменное имя сервера, для которого создаётся сертификат). 121
После создания запроса CSR пора его использовать — переходим на сайт sslforfree.com. Здесь прежде всего необходимо указать доменное имя сервера, для которого создаётся сертификат: Рис. 56 Вводим доменное имя сервера, для которого создаётся сертификат Далее потребуется подтвердить владение данным доменным именем одним из предложенных способов, например Manual Verification: Рис. 57 Выбираем ручной (Manual Verification) способ проверки принадлежности сайта Для ручной верификации потребуется разместить на сервере в заданном каталоге специальный файл (или файлы), содержащий случайную последовательность символов как в названии, так и в имени, и перейти по соответствующей ссылке (или ссылкам), как показано на рисунке ниже. Рис. 58 Для двух указанных выше доменов потребуется загрузить два файла (Download File #1 и #2) и переместить их в указанное место на сервере, так чтобы отработали без ошибок указанные в п. 5 ссылки 122
Если всё сделано правильно, то останется только сгенерировать сертификат. При этом генерацию приватного ключа и CSR сервис может взять на себя. В нашем случае, когда и ключ, и CSR уже созданы, следует не забыть установить соответствующую галочку (I have my own CSR) с последующим вводом CSR, как представлено на рисунке ниже. Рис. 59 Достаточно скопировать CSR в предназначенное для этого поле и нажать Download SSL Сertificate После перехода по ссылке Download SSL Certificate сервис предоставит для скачивания сгенерированные сертификаты, готовые к последующей установке на целевом сервере с заданным доменным именем. Скачиваем сертификат — результат скачивания в файле certificate.crt. Заметим, что кроме обычного сертификата сервис также подготовит так называемый bundle-сертификат (ca_bundle.crt), предназначенный для поддоменов, и приватный ключ в файле — private.key. В нашем случае в этом файле будет содержаться сообщение: You provided your own CSR which means the private key was probably generated when you got the CSR. We do not have access to the private key in this case to show it. Действительно, приватный ключ мы сгенерировали самостоятельно. Осталось только правильно установить приватный ключ и сертификат на сервере. С этой целью требуется скорректировать конфигурационный файл сервера. Обычно панель управления сайтом, которая в том или ином виде предоставляется хостерами своим клиентам, позволяет выполнить эту задачу проще. Например, peterhost (peterhost.ru) преподносит эту возможность в следующем виде — интерфейс представлен на рисунке ниже (см. рис. 57). Таким образом, достаточно скопировать сертификат и приватный ключ в соответствующие поля (или загрузить файлы с соответствующим содержимым) и нажать кнопку «Установить» — изменения в конфигурационный файл сервера будут внесены автоматически. 123
Рис. 60 Для установки SSL-сертификата достаточно заполнить соответствующие поля 5.6. Подготовка архива приложения для Ad Hoc распространения 5.6.1. Создание и экспорт архива приложения с помощью Xcode После установки ssl-сертификата и подготовки профиля для Ad Hoc загрузки (как это сделать — описано выше) можно выполнить процедуру сборки архива для его последующей загрузки на web-сервер и распространения на зарегистрированные в профиле устройства через сеть. С этой целью потребуется выполнить ряд формальных действий. Прежде всего в Xcode, в навигаторе проекта (View — Navigators — Show Project Navigator), следует установить требуемую схему сборки (Set the active scheme) — Generic iOS Device, как это показано на рисунке ниже: Рис. 61 Для сборки архива следует установить схему сборки для «общего» (Generic) устройства 124
Далее здесь же, в навигаторе проекта, выбрать заранее подготовленный Ad Hoc профиль для «боевой» сборки (Release): Рис. 62 Выбираем соответствующий профиль для release-сборки (показано стрелкой) Наконец, можно выполнить сборку Product — Build, предварительно очистив место для сборки Product — Clean Build Folder. Если сборка прошла успешно, то создаём архив Product — Archive. Отметим, что для подписи сборки и архива потребуется доступ к сертификатам, хранящимся в связке ключей (как уже упоминалось, управляет связкой ключей приложение Keychain Access), то есть придётся ввести пароль администратора соответствующее количество раз. В результате сборки архива автоматически попадаем в окно органайзера (Window — Organizer) (см. рис. 63). Выбираем Distribute App, в результате Xcode предложит выбрать метод распространения приложения (method of distribution) — скриншот представлен ниже. Распространение через App Store в настоящем пособии не обсуждается. Распространение типа Enterprise (или In-House) аналогично Ad Hoc, но, как уже отмечалось, стоимость соответствующего сертификата значительно выше — 299 долларов в год. При таком методе распространения количество мобильных устройств, на которые можно установить приложение, неограниченно, и, разумеется, их не надо регистрировать при создании профилей. Купить такой сертификат может только компания, которая при этом должна распространять приложение исключительно среди сотрудников и бесплатно. 125
Рис. 63 Окно Window — Organizer, в которое автоматически попадаем после сборки архива Рис. 64 Среди предложенных методов распространения приложения выбираем Ad Hoc Итак, выбрали Ad Hoc, нажали Next — в результате получим форму, представленную на рисунке далее. В поле App Thining следует оставить значение None. В этом поле можно выбрать модель iOS-устройства из довольно объёмного списка, например iPhone 5 или iPadMini, если по каким-то причинам требуется сузить применимость приложения. 126
Обязательно выбираем Include manifest for over-the-air installation. Выбор этой опции позволит загружать приложение через сеть с помощью webобозревателя Safari и web-сервера: Рис. 65 Выбор Include manifest for over-the-air installation позволит загружать приложение на зарегистрированные в профиле устройства через сеть c помощью web-сервера и Safari В следующей форме (рисунок ниже) потребуется указать точное месторасположение архива приложения, а также двух изображений, которые будут представлять приложение в iOS. Эти данные необходимы для формирования файла манифеста. Размеры требуемых изображений 57 и 512 пикселей по ширине и высоте: Рис. 66 В представленной форме необходимо указать месторасположение архива приложения и изображений логотипа приложения в двух форматах: 57 и 512 пикселей по ширине и высоте 127
В форме, представленной на рисунке поля, заполнены для приложения с именем AudioPlayer. Предполагается, что на web-сервере с доменным именем my_server в его корневом каталоге предусмотрен подкаталог iosapps для хранения активов iOS-приложений, распространяемых по технологии Ad Hoc. Для приложения AudioPlayer создан одноимённый подкаталог. Заметим, что логотипы указанных размеров можно создать на основе рисунков, представляющих стандартный логотип Cordova (пока не разработаны оригинальные рисунки). Заметим, что полный набор таких рисунков, отличающихся размерами, для iOS-устройств с различным разрешением экрана довольно внушительный. Логотипы Cordova разных форматов можно найти в каталоге cordova/platforms/ios/AudioPlayer/Images.xcassets, например в подкаталоге AppIcon.appiconset: Рис. 67 В каталоге cordova/platforms/ios/AudioPlayer/Images.xcassets можно найти логотипы Cordova различных размеров. На рисунке показано содержимое подкаталога AppIcon.appiconset. Подкаталог LaunchImage.launchimage, в котором можно найти логотипы других размеров, на рисунке не представлен В следующей форме потребуется указать сертификат разработчика, допускающий распространение приложений (iOS Distribution certificate), а также заранее подготовленный Ad Hoc профиль с идентификаторами (UDID) всех устройств, на которых возможно развёртывание архива (рис. 68). Далее Xcode покажет сводную информацию о приложении (рис. 69). 128 Powered by TCPDF (www.tcpdf.org)
Рис. 68 Для беспроводного Ad Hoc распространения требуется указать distribution certificate и соответствующий профиль Рис. 69 Сводная информация о приложении перед экспортом При активации кнопки Export (смотри рисунок выше) Xcode предложит выбрать место для размещения экспортируемых файлов или воспользоваться 129
готовым названием каталога, включающим дату и время экспорта. Выбираем последнее, в результате получим: Рис. 70 Результат экспорта архива приложения AudioPlayer для его дальнейшего беспроводного распространения 5.6.2. Иконки и заставка приложения По умолчанию гибридное приложение будет представлено логотипом Cordova: Рис. 71 Логотип Cordova будет представлять приложение на рабочем столе мобильного устройства и при старте, если не позаботиться об оригинальных рисунках Это изображение в виде иконки появится на рабочем столе мобильного устройства после успешной загрузки и установки приложения, а при старте — в виде заставки, более известной как Splash Screen. Изменить логотип можно из нативной среды разработки, например из Xcode для iOS, или воспользоваться технологией, предусмотренной Cordova. В любом случае логотип предварительно должен быть создан. При этом если приложение предполагается устанавливать на различных устройствах с различным разрешением экрана, то изображение логотипа должно быть продублировано много раз с разными вариантами размеров по ширине и высоте. Особенно если речь идёт об iOS, то здесь, например, придётся предусмотреть иконку также для представления приложения в iTunes и не только там. Создание таких изображений с разным разрешением и в таком довольно внушительном количестве может представлять определённые проблемы. Возможный вариант автоматизации процесса создания иконок логотипа приложения с разным разрешением предлагается реализовать с помощью графической библиотеки GD. Эта библиотека популярна особенно среди программистов на php. Сущность предлагаемого подхода состоит в том, чтобы сначала смоделировать изображение логотипа в векторном виде, а затем с помощью GD размножить его с различными размерами по ширине и высоте. При этом попут130
но можно генерировать текстовый файл, содержащий описания создаваемых изображений в xml. Эти описания требуются Cordova в файле конфигурации config.xml (файл расположен в каталоге проекта приложения, в подкаталоге cordova (см. рис. 17)). Продемонстрируем этот подход на примере. Предположим, что для некоторого приложения требуется сгенерировать полный набор изображенийиконок для устройств iOS. Пусть это аудиоплеер, для которого предлагается следующий логотип: Рис. 72 Пример логотипа аудиоплеера Представленное изображение состоит из ряда элементов-векторов, которые легко определить в числовом виде, независимо от разрешения, то есть от размеров пиксельного полотна для рисования: • динамик — замкнутая ломаная, которая залита голубым цветом; • глаза-ноты — эллипсы, изображающие глаза и зрачки; • штили нот; • рот — отрезок прямой; • наконец, звуковые волны в виде эллиптических дуг. Вершины ломаной, представляющей контур динамика, в координатной плоскости (x, y) задаются следующими значениями абсцисс и ординат: $dynamic = array( 0.1, 0.0, 0.0, 0.1, 0.0, 1.9, 0.1, 2.0, 1.5, 2, 2.5, 3.4, 2.6, 3.4, 2.6, -0.9, 2.5, -0.9, 1.5, 0.0 ); Значения заданы в виде массива php, чётные номера — абсциссы, нечётные — ординаты. Аналогично можно определить и другие элементы, например центры и размеры эллипсов, изображающих глаза. Левый глаз-нота: $cx1 = 0.5; $cy1 = 1.3; $d1 = 0.7; 131
Правый: $cx2 = $cx1 + 1; $cy2 = $cy1 + 0.2; При проектировании подобных простейших логотипов можно предварительно нарисовать составные элементы на миллиметровке и затем определять их положение и размеры. Согласно канонам компьютерной графики, следует определиться с кадром, который охватывает все элементы, задав его левый нижний и правый верхний углы: $LX = -0.5; $LY = -1.5; $RX = 5; $RY = 4; Этот кадр теперь необходимо однозначно отобразить на пиксельный растр заданных размеров по ширине — W — и по высоте — H (в графике это отображение называется кадрированием). То есть каждой точке кадра с координатами x и y сопоставить точку растра с целочисленными пиксельными координатами X и Y (отсчёт Y ведётся сверху вниз). В php это можно реализовать с помощью следующих функций, принимающих в качестве параметра координаты x и y и возвращающих их пиксельные значения: function X($x) { global $LX, $RX, $W; return (int) (($x - $LX) * $W / ($RX - $LX)); } function Y($y) { global $LY, $RY, $H; return (int) (($RY - $y) * $H / ($RY - $LY)); } Параметры кадра и размеры полотна в определениях функций передаются как глобальные переменные. Кроме этого, потребуются переводы характерных размеров элементов рисунка (например, размеров полуосей эллипса) в пиксельные эквиваленты по осям x и y: function pX($d) { return X($d) - X(0.); } function pY($d) { return Y($d) - Y(0.); } Наконец, реализуем ещё одну сервисную функцию, которая переводит массив координат точек (например, вершин ломаной линии) в их пиксельный эквивалент: function toPixels($xyArr) { $x = true; $xyArrInPixels = array(); foreach ($xyArr as $val) { $xyArrInPixels[] = $x ? X($val) : Y($val); $x = !$x; } 132
return $xyArrInPixels; } Логическая переменная $x внутри тела функции работает как своеобразный переключатель, если её значение — true, то элемент массива чётный, и это абсцисса, иначе — ордината. Теперь всё готово для того, чтобы прорисовать с помощью функций библиотеки GD все элементы рисунка. Реализуем прорисовку в виде функции, которой на вход передаётся название файла для записи готового рисунка. Размеры пиксельного полотна для рисования (W, H) и толщина линии (thickness) доступны в одноимённых глобальных переменных: function buildIcon($nm) { global $W, $H, $thickness; $im = @imagecreatetruecolor($W, $H) or die('Невозможно инициализировать GD поток'); // формируем цвета для рисования $realwhite = imagecolorallocate($im, 0x87, 0xce, 0xfa); $white = imagecolorallocate($im, 0x87, 0xce, 0xfa); $black = imagecolorallocate($im, 0x19, 0x19, 0x70); $gray = imagecolorallocate($im, 0x80, 0x80, 0x80); $blue = imagecolorallocate($im, 0x41, 0x69, 0xe1); // закрашиваем полотно и рисуем рамку imagefill($im, 0, 0, $realwhite); imagerectangle($im, 0, 0, $W-1, $H-1, $black); // координаты вершин ломаной, изображающей динамик $dynamic = array( 0.1, 0.0, 0.0, 0.1, 0.0, 1.9, 0.1, 2.0, 1.5, 2, 2.5, 3.4, 2.6, 3.4, 2.6, -0.9, 2.5, -0.9, 1.5, 0.0 ); // переход к пиксельным координатам динамика $dynamicPxls = toPixels($dynamic); // заливаем динамик голубым цветом imagefilledpolygon($im, $dynamicPxls, count($dynamicPxls) / 2, $blue); // ноты-глаза if($W > 30) { // прорисовываем только при достаточном разрешении // левый глаз-нота $cx1 = 0.5; $cy1 = 1.3; $d1 = 0.7; // центр и диаметр imagefilledellipse($im, X($cx1), Y($cy1), pX($d1), pY($d1), $white); // зрачок imagefilledellipse($im, X($cx1 + 0.099), Y($cy1), pX($d1 - 2*0.099), pY($d1 - 2*0.099), $black); 133
// штиль imagesetthickness($im, $thickness); imageline($im, X($cx1 + 0.099 + ($d1 - 0.2)/2), Y($cy1), X($cx1 + 0.099 + ($d1 - 0.2)/2), Y($cy1 + 1), $black); imagesetthickness($im, $thickness + 2); imageline($im, X($cx1 + 0.099 + ($d1 - 0.2)/2), Y($cy1 + 1), X($cx1 + 0.099 + ($d1 - 0.2)/2 + 0.3), Y($cy1 + 1 + 0.1), $black); // правый глаз-нота $cx2 = $cx1 + 1; $cy2 = $cy1 + 0.2; imagefilledellipse($im, X($cx2), Y($cy2), pX($d1 - 0.1), pY($d1 - 0.1), $white); // зрачок imagefilledellipse($im, X($cx2 + 0.099), Y($cy2), pX($d1 - 0.1 - 2*0.099), pY($d1 - 0.1 - 2*0.099), $black); imagesetthickness($im, $thickness); imageline($im, X($cx2 + 0.099 + ($d1 - 0.1 - 2*0.099)/2), X($cx2 + 0.099 + ($d1 - 0.1 - 2*0.099)/2), $black); imagesetthickness($im, $thickness + 2); imageline($im, X($cx2 + 0.099 + ($d1 - 0.1 - 2*0.099)/2), X($cx2 + 0.099 + ($d1 - 0.1 - 2*0.099)/2 + Y($cy2 + 1.2 + 0.1), $black); Y($cy2), Y($cy2 + 1.2), Y($cy2 + 1.2), 0.3), } // рот imageline($im, X(0.6), Y(0.4), X(1.4), Y(0.4), $black); // звуковые волны динамика imagesetthickness($im, 1); imagefilledarc($im, X(1.6), Y(1), X(3)-X(0.), Y(2.)-Y(0.), -40, 40, $gray, IMG_ARC_NOFILL); imagefilledarc($im, X(1.8), Y(1), X(3.5)-X(0.), Y(3.5)-Y(0.), -40, 40, $gray, IMG_ARC_NOFILL); imagefilledarc($im, X(2), Y(1), X(4)-X(0.), Y(5)-Y(0.), -40, 40, $gray, IMG_ARC_NOFILL); // сохраняем изображение imagepng($im, "$nm.png"); imagedestroy($im); // готовим описание изображения в файл config.xml 134
echo <<<EOS <icon src="../$nm.png" platform="ios" width="$W" height="$H" density="mdpi" />\n EOS; } Заметим, что xml-описания сгенерированных изображений выводятся в стандартный выходной поток. Теперь остаётся определиться с разрешением и названиями генерируемых рисунков-логотипов: $set = array( // iTunes array("iTunesArtwork", 512, 512), array("iTunesArtwork@2x", 1024, 1024), array("Icon-Small-40.png", 40, 40), array("Icon-Small-40@2x", 80, 80), array("Icon-Small-40@3x", 120, 120), array("Icon-Small-50.png", 50, 50), array("Icon-Small-50@2x", 100, 100), array("icon-40", 40, 40), array("icon-40@2x", 80, 80), array("icon-50", 50, 50), array("icon-50@2x", 100, 100), array("icon-60@2x", 120, 120), array("icon-60@3x", 180, 180), array("icon-72", 72, 72), array("icon-72@2x", 144, 144), array("icon-76", 76, 76), array("icon-76@2x", 152, 152), array("icon-83.5@2x", 167, 167), array("icon-small", 29, 29), array("icon-small@2x", 58, 58), array("icon-small@3x", 87, 87), array("icon", 57, 57), array("icon@2x", 114, 114), array("icon-20", 20, 20), array("icon-20@3x", 60, 60), array("icon-98@2x", 196, 196), array("icon-44@2x", 88, 88), array("icon-216", 216, 216), array("icon-24@2x", 48, 48), array("icon-86@2x", 172, 172), array("icon-27.5@2x", 55, 55), array("icon-1024", 1024, 1024), // app watch array("AppIcon40x40@2x", 80, 80), array("AppIcon44x44@2x", 88, 88), array("AppIcon86x86@2x", 172, 172), array("AppIcon98x98@2x", 196, 196), array("AppIcon24x24@2x", 48, 48), array("AppIcon27.5x27.5@2x", 55, 55), array("AppIcon29x29@2x", 58, 58), array("AppIcon29x29@3x", 87, 87), 135
// ad hoc array("AudioPlayer57x57", 57, 57) ); Каждое описание рисунка в представленном массиве описаний включает общепринятое название файла и разрешение по ширине и высоте. Теперь достаточно перебрать в цикле все описания и для каждого из них обратиться к функции рисования buildIcon(): foreach ($set as $val) { $nm = $val[0]; $W = $val[1]; $H = $val[2]; $thickness = 2; buildIcon($nm); } Представленный генератор файлов изображений-логотипов (назовём его icon.php) можно запустить в терминале, в каталоге проекта приложения: php icon.php > RES В результате в файле RES получим xml-описания сгенерированных логотипов, которые легко перенести в файл config.xml. Эти описания выглядят следующим образом, приведём некоторые для примера: <icon density="mdpi" height="40" platform="ios" src="../icon-40.png" width="40" /> <icon density="mdpi" height="80" platform="ios" src="../icon-40@2x.png" width="80" /> В атрибутах тега icon определяются размер, платформа и расположение файла с изображением относительно месторасположения файла config.xml. Аналогичный код позволяет сгенерировать заставки (Splash Screen) для различных iOS-устройств. Набор описаний изображений-заставок следующий: $set = array( array("Default~iphone", 320, 480), array("Default@2x~iphone.png", 640, 960), array("Default-Portrait~ipad.png", 768, 1024), array("Default-Portrait@2x~ipad.png", 1536, 2048), array("Default-Landscape~ipad.png", 1024, 768), array("Default-Landscape@2x~ipad", 2048, 1536), array("Default-568h@2x~iphone", 640, 1136), array("Default-667h", 750, 1334), array("Default-736h.png", 1242, 2208), array("Default-Landscape-736h.png", 2208, 1242), array("Default-2436h", 1125, 2436), array("Default-Landscape-2436h", 2436, 1125), array("AudioPlayer512x512", 512, 512) ); Поскольку ширина и высота существенно отличаются, то при кадрировании, чтобы избежать искажения рисунка (растяжения или сжатия), будем брать за основу минимальный размер по одной из осей, а по другой центровать изоб136
ражение. С этой целью в процессе перебора описаний изображений будем вычислять соответствующие смещения $DX и $DY: foreach ($set as $val) { $nm = $val[0]; $W = $val[1]; $H = $val[2]; $thickness = 2; $DX = 0; $DY = 0; if($W > $H) { $SZE = $H; $DX = ($W - $SZE) / 2; } else { $SZE = $W; $DY = ($H - $SZE) / 2; } buildIcon($nm); } То есть излишек в размере по одной из осей делим пополам, и это и есть величина сдвига, которую надо учесть при кадрировании: function X($x) { global $LX, $RX, $SZE, $DX; $W = $SZE; return (int) (($x - $LX) * $W / ($RX - $LX) + $DX); } function Y($y) { global $LY, $RY, $SZE, $DY; $H = $SZE; return (int) (($RY - $y) * $H / ($RY - $LY) + $DY); } Наконец, описания заставок (для config.xml) также отличаются от аналогичных описаний иконок-логотипов — в функции buildIcon корректируем соответствующие строки: function buildIcon($nm) { … imagepng($im, "$nm.png"); imagedestroy($im); echo <<<EOS <splash src="../$nm.png" width="$W" height="$H"/>\n EOS; } В остальном код генерации заставок такой же, запускаем его из каталога приложения: php splash.php > RES Xml-описания из файла RES копируем в config.xml. Описанных действий вполне достаточно для смены стандартного логотипа Cordova на оригинальный как на рабочем столе, так и при старте приложения. В Xcode при создании архива приложения эти изменения будут «подхвачены» автоматически. 137
5.7. Подготовка активов приложения для загрузки на web-сервер Беспроводная установка iOS и Android-приложений возможна с помощью web-сервера и браузера мобильного устройства. Устройства с Android позволяют непосредственно загружать и устанавливать файлы приложений с расширением apk (Android Package Kit). Можно использовать браузер по умолчанию или любой другой, например Chrome. Беспроводная установка iOS-приложений предполагает использование официального протокола itms-services фирмы Apple и файла-манифеста, содержащего информацию об устанавливаемом приложении, специально подготовленном для Ad Hoc или In-house распространения. 5.7.1. Использование сервиса Diawi Если у разработчика нет сервера, где можно было бы хранить активы приложения для его беспроводной Ad Hoc загрузки, или по каким-то причинам отсутствует ssl-сертификат, то можно воспользоваться услугами сторонних сервисов, например https://www.diawi.com. Diawi — это бесплатный сервис, позволяющий загрузить мобильное приложение в «облака» для его дальнейшего распространения на мобильные устройства через сеть. Допускаются iOS и Android-приложения, подготовленные для Ad Hoc или In-House-распространения: Рис. 73 Достаточно «перетащить» ipa-архив приложения в зону Drag & Drop. В результате сервис подготовит приложение для беспроводной загрузки и покажет ссылку, которую следует использовать в браузере мобильного устройства для установки приложения 138
Для загрузки архива приложения (файла с расширением ipa — для iOS — или apk — для Android) достаточно загрузить последний на сервер diawi — перетащить архив в зону Drag & drop или активировать кнопку Add files. Сервис самостоятельно «вытащит» всю необходимую информацию из архива и подготовит приложение для его беспроводного распространения. В случае успешной обработки будет показана ссылка, которая должна быть активирована в браузере мобильного устройства. Ссылка, например, может выглядеть так: https://i.diawi.com/Eeurrs. Результат активации этой ссылки в Safari на iPhone следующий: Рис. 74 Для установки приложения достаточно активировать ссылку Install application 5.7.2. Есть хостинг и ssl-сертификат Если есть место на web-сервере, на котором установлен ssl-сертификат, то остаётся воспользоваться этой возможностью и подготовить все необходимые для загрузки файлы, требуемые для беспроводного распространения мобильного приложения. Впрочем, основную работу уже выполнил Xcode в процессе экспорта его архива. Этот процесс в подробностях описан выше для некоторого приложения с именем AudioPlayer в качестве примера. 139
Воспользуемся результатом этого экспорта, который представлен на рисунке 70. Из представленного там множества файлов потребуется только архив и файл манифеста, то есть AudioPlayer.ipa и manifest.plist. Заглянув в последний (это обычный xml-файл, который можно открыть в любом текстовом редакторе), вспомним, что требуются ещё и рисунки-логотипы с размерами 57 и 512 пикселей по ширине и высоте. Имена файлов с рисунками при экспорте архива были указаны следующие: AudioPlayer57x57.png и AudioPlayer512x512.png. Вспомним также, что планировалось размещать все эти активы в каталоге /iosapps/AudioPlayer. Эту файловую структуру необходимо создать в корневом каталоге сервера и загрузить туда перечисленные выше файлы (например, с помощью ftp). Теперь осталось создать вспомогательный html-файл, который и будет первоначально загружен в браузере мобильного устройства. Далее предлагается простейший вариант такого файла: <html> <head> <title>My Ad-Hoc Distribution Site</title> <style> li{font-size:60pt margin:20px 0} </style> <meta name="viewport" content="width=device-width" /> <meta name="apple-mobile-web-app-capable" content="yes" /> </head> <body> <ul> <li> <a href="itms-services://?action=downloadmanifest&url=https://my_server/iosapps/AudioPlayer/manifest.plist"> AudioPlayer </a> </li> </ul> </body> </html> Следует обратить внимание на ссылку itms-services, именно её активация в браузере мобильного устройства приведёт к установке приложения. Этот файл, конечно, также следует разместить на сервере, например, под именем AudioPlayer.html. В браузере устройства (допускается только Safari) в адресной строке следует в точности набрать адрес: https://my_server/AudioPlayer.html После загрузки достаточно активировать единственную ссылку и дать согласие на установку приложения. 140
ПРИЛОЖЕНИЕ В приложении приведены листинги всех файлов примера, рассмотренного в главе 4. В названиях файлов пути указаны относительно каталога проекта (TstApp). Напомним, что вся разработка программного кода ведётся в подкаталоге app, исключение составляет файл app.js, который расположен непосредственно в каталоге проекта. Пример мобильного приложения формально состоит из трёх компонент: главного контейнера (mainCtr), панели для работы с камерой (camera), панели для демонстрации результата (result). Контейнер содержит перечисленные панели. Все компоненты разрабатывались согласно технологии Model-View-Conroller, поэтому каждый компонент представлен тремя файлами. Сервисные функции реализованы в рамках класса приложения (Application.js). Кроме этого, в качестве главного окна выступает упомянутый выше контейнер. Эти особенности требуют корректировок в файлах app.js и app/Application.js. Класс Application Файл app.js /* * This file launches the application by asking Ext JS to create * and launch() the Application class. */ Ext.application({ extend: 'TstApp.Application', name: 'TstApp ', requires: [ // This will automatically load all classes // in the TstApp namespace so that application // classes do not need to require each other. 'TstApp.*' ], // The name of the initial view to create. mainView: ' TstApp.view.mainCtr.Main' }); Файл app/Application.js Ext.define('TstApp.Application', { extend: 'Ext.app.Application', name: 'TstApp', quickTips: false, platformConfig: { desktop: { quickTips: true } }, 141
srvr: 'http://MyServer/TstApp/', camera: true, photo: './resources/placeholder_200x200.png', quality: 100, yesPhoto: false, getCfgJson: function() { var cfg = { srvr: this.srvr, camera: this.camera, photo: this.photo }; return JSON.stringify(cfg); }, saveCfg: function() { var cfg = { srvr: this.srvr, camera: this.camera, photo: this.photo, quality: this.quality }; this.writeToFile( 'cfg.json', JSON.stringify(cfg) ); }, launch: function () { var me = this; var getPhotoPnl = me.getMainView().down('getPhotoPnl'), vm = getPhotoPnl.getViewModel(); var placeholder = getPhotoPnl.lookupReference('placeholder'); me.readFromFile('cfg.json', function(data) { var cfg = JSON.parse(data); me.srvr = cfg.srvr; me.camera = cfg.camera; me.photo = cfg.photo; me.quality = cfg.quality; placeholder.setSrc(me.photo); me.yesPhoto = true; vm.set('uploadBtnDisabled', false); }); }, readFromFile: function(filenm, cb) { var me = this; var path = cordova.file.dataDirectory + filenm; window.resolveLocalFileSystemURL(path, function (fileEntry) { fileEntry.file(function (file) { var reader = new FileReader(); reader.onloadend = function (e) { cb.call(me, this.result); }; reader.readAsText(file); }, me.onError.bind(me, filenm) ); }, me.onError.bind(me, filenm) ); }, writeToFile: function(filenm, data) { var me = this; window.resolveLocalFileSystemURL( cordova.file.dataDirectory, function (directoryEntry) { 142
directoryEntry.getFile( filenm, { create: true }, function (fileEntry) { fileEntry.createWriter( function (fileWriter) { fileWriter.onwriteend = function (e) { if( me.getMainView().getMasked() ) me.getMainView().unmask(); }; fileWriter.onerror = function (e) { if( me.getMainView().getMasked() ) me.getMainView().unmask(); Ext.Msg.alert('Write failed!', e.toString()); }; fileWriter.write(data); }, me.onError.bind(me, filenm) ); // createWriter }, me.onError.bind(me, filenm) ); // getFile }, me.onError.bind(me, filenm) ); }, onError: function(filenm, e) { var mainView = this.getMainView(); if( mainView.getMasked() ) mainView.unmask(); var msg = ''; switch (e.code) { case FileError.QUOTA_EXCEEDED_ERR: msg = 'Storage quota exceeded'; break; case FileError.NOT_FOUND_ERR: msg = 'File not found'; break; case FileError.SECURITY_ERR: msg = 'Security error'; break; case FileError.INVALID_MODIFICATION_ERR: msg = 'Invalid modification'; break; case FileError.INVALID_STATE_ERR: msg = 'Invalid state'; break; default: msg = 'Unknown error'; break; }; var errMsg = 'Error (' + filenm + '): ' + msg; if(filenm == 'cfg.json') { var pnl = mainView.down('getPhotoPnl'), placeholder = pnl.lookupReference('placeholder'); placeholder.setSrc(this.photo); } } }); 143
Компонент mainCtr Файл app/view/mainCtr/Main.js Ext.define('TstApp.view.mainCtr.Main',{ extend: 'Ext.Container', xtype: 'mainCtr', requires: [ 'TstApp.view.mainCtr.MainController', 'TstApp.view.mainCtr.MainModel' ], controller: 'mainctr-main', viewModel: { type: 'mainctr-main' }, layout: 'card', items: [{ xtype: 'getPhotoPnl' },{ xtype: 'showResultPnl' }] }); Файл app/view/mainCtr/MainController.js Ext.define('TstApp.view.mainCtr.MainController', { extend: 'Ext.app.ViewController', alias: 'controller.mainctr-main' }); Файл app/view/mainCtr/MainModel.js Ext.define('TstApp.view.mainCtr.MainModel', { extend: 'Ext.app.ViewModel', alias: 'viewmodel.mainctr-main', data: { name: 'TstApp' } }); Компонент getPhotoPnl Файл app/view/camera/GetPhoto.js Ext.define('TstApp.view.camera.GetPhoto',{ extend: 'Ext.Panel', xtype: 'getPhotoPnl', requires: [ 'Ext.Button', 144 Powered by TCPDF (www.tcpdf.org)
'Ext.Img', 'Ext.Toolbar', 'Ext.Progress', 'Ext.Dialog', 'Ext.form.Panel', 'Ext.SegmentedButton', 'Ext.field.Text', 'Ext.field.Url', 'Ext.field.Number' ], controller: 'camera-getphoto', viewModel: { type: 'camera-getphoto' }, layout: { type:"vbox", pack:"center", align:"stretch" }, items: [{ xtype: 'container', height: 4, items: [{ xtype: 'progress', width: '100%', bind: { hidden: '{progresshidden}', value: '{progress}' } }] },{ reference: 'placeholder', xtype: "image", flex: 1 }], tbar: [{ iconCls: 'x-fa fa-gear', handler: 'settings', bind: { disabled: '{settingsBtnDisabled}' }, ui: 'action' },{ xtype: 'spacer' },{ iconCls: 'x-fa fa-camera', handler: 'getPicture', bind: { disabled: '{photoBtnDisabled}' }, ui: 'action' },{ xtype: 'spacer' },{ 145
xtype: 'spacer' },{ iconCls: 'x-fa fa-upload', handler: 'upload', bind: { disabled: '{uploadBtnDisabled}' }, ui: 'action' },{ xtype: 'spacer' },{ iconCls: 'x-fa fa-angle-double-right', handler: 'result' }], listeners: { photoIsCopied: 'photoIsCopied' }, dialog: { xtype: 'dialog', title: 'Настройки', maskTapHandler: 'onCancel', bodyPadding: 0, layout: 'fit', maxWidth: 550, items: [{ xtype: 'formpanel', reference: 'form', autoSize: true, items: [{ xtype: 'segmentedbutton', reference: 'cameraAlbumButton', name: 'cameraAlbumButton', items: [{ text: 'camera', value: 'camera' },{ text: 'album', value: 'album' }] },{ xtype: 'urlfield', name: 'srvr', label: 'Сервер', labelAlign: 'top', allowBlank: false, required: true },{ xtype: 'numberfield', name: 'quality', label: 'Качество фото', labelAlign: 'top', allowBlank: false, required: true }] }], buttons: { 146
ok: 'onOK', cancel: 'onCancel' }, listeners: { show: 'onShow' } } }); Файл app/view/camera/GetPhotoController.js Ext.define('TstApp.view.camera.GetPhotoController', { extend: 'Ext.app.ViewController', alias: 'controller.camera-getphoto', img_uri: null, dialog: null, getPictureOptions: function() { var app = TstApp.getApplication(); var sourceType = app.camera ? navigator.camera.PictureSourceType.CAMERA : navigator.camera.PictureSourceType.PHOTOLIBRARY; var opts = { quality: app.quality ,destinationType: navigator.camera.DestinationType.FILE_URI ,sourceType: sourceType ,encodingType: navigator.camera.EncodingType.JPEG ,mediaType: navigator.camera.MediaType.PICTURE ,correctOrientation: true }; return opts; }, getPicture: function() { var me = this, vm = this.getView().getViewModel(); vm.set('settingsBtnDisabled', true); vm.set('photoBtnDisabled', true); vm.set('uploadBtnDisabled', true); navigator.camera.getPicture( this.success.bind(me), this.fail.bind(me), this.getPictureOptions() ); }, success: function (image_uri) { var img = this.lookup('placeholder'); this.copyPhotoToFile('1.jpg', image_uri); }, fail: function (message) { var vm = this.getView().getViewModel(); Ext.Msg.alert('Error', "Failed: " + message); if(TstApp.getApplication().yesPhoto) vm.set('uploadBtnDisabled', false); vm.set('settingsBtnDisabled', false); 147
vm.set('photoBtnDisabled', false); }, photoIsCopied: function() { var img = this.lookup('placeholder'); var img_uri = this.img_uri + '?' + Date.now(); img.setSrc(img_uri); this.cleanUp(); var vm = this.getView().getViewModel(); vm.set('settingsBtnDisabled', false); vm.set('photoBtnDisabled', false); vm.set('uploadBtnDisabled', false); TstApp.getApplication().saveCfg(); }, copyPhotoToFile: function(filenm, uri) { var me = this; window.resolveLocalFileSystemURL(cordova.file.dataDirectory, function (directoryEntry) { directoryEntry.getFile(filenm, { create: true }, function (fileEntry) { var fileTransfer = new FileTransfer(); var fileURL = fileEntry.toURL(); me.img_uri = fileURL; TstApp.getApplication().photo = fileEntry.toInternalURL(); fileTransfer.download( uri, fileURL, function (entry) { me.getView().fireEvent('photoIsCopied'); }, function (error) { console.log('fileTransfer Error'); }, null, {} ); }, function(e) { console.log('getFile Error'); } ); }, function(e) { console.log('resolveLocalFileSystemURL Error'); } ); // resolveLocalFileSystemURL }, cleanUp: function() { navigator.camera.cleanup(onSuccess, onFail); function onSuccess() { console.log("Camera cleanup success"); } function onFail(message) { console.log('Failed because: ' + message); } 148
}, upload: function() { if( navigator.connection.type == Connection.NONE || navigator.connection.type == Connection.UNKNOWN ) { Ext.Msg.alert('Нет сети!', 'Для загрузки фото требуется 3G, 4G, WiFi или иной выход в сеть!' ); return; } var app = TstApp.getApplication(); var me = this, fileURL = "cdvfile://localhost/library-nosync/1.jpg", vm = this.getView().getViewModel(); vm.set('settingsBtnDisabled', true); vm.set('photoBtnDisabled', true); vm.set('uploadBtnDisabled', true); var options = new FileUploadOptions(); options.fileKey = "file"; options.fileName = fileURL.substr(fileURL.lastIndexOf('/') + 1); options.mimeType = "image/jpeg"; var ft = new FileTransfer(); ft.onprogress = this.uploadProgress.bind(me); vm.set('progresshidden', false); vm.set('progress', 0); ft.upload( fileURL, encodeURI(app.srvr + "upload.php"), this.uploadSuccess.bind(me), this.uploadFail.bind(me), options ); }, uploadProgress: function(progressEvent) { var vm = this.getView().getViewModel(), progress = 0.0; if(progressEvent.lengthComputable) { progress = progressEvent.loaded / progressEvent.total; } else { progress = vm.get('progress'); progress += 0.01; if (progress > 1) progress = 0; } vm.set('progress', progress); }, uploadSuccess: function (r) { var vm = this.getView().getViewModel(); vm.set('progresshidden', true); vm.set('settingsBtnDisabled', false); vm.set('photoBtnDisabled', false); vm.set('uploadBtnDisabled', false); Ext.Msg.alert("Загружено!", "Фотка ушла на сервер!<br/> Смотрим здесь: http://MyServer/TstApp/1.jpg"); }, 149
uploadFail: function (error) { Ext.Msg.alert("An error has occurred", " Code = " + error.code + "<br/>upload error source " + error.source + "<br/>upload error target " + error.target); var vm = this.getView().getViewModel(); vm.set('progresshidden', true); vm.set('settingsBtnDisabled', false); vm.set('photoBtnDisabled', false); vm.set('uploadBtnDisabled', false); }, result: function() { var app = TstApp.getApplication(), mainCtr = this.getView().up('mainCtr'), resultholder = mainCtr.down('showResultPnl').lookup('resultholder'); mainCtr.setActiveItem(1); var res_uri = app.srvr + 'download.php' + '?' + Date.now(); resultholder.setSrc(res_uri); }, destroy: function() { Ext.destroy(this.dialog); this.callParent(); }, hideDialog: function () { var dialog = this.dialog; if(dialog) { dialog.hide(); } }, onCancel: function () { this.hideDialog(); }, onOK: function () { var form = this.lookup('form'); if(form.validate()) { this.hideDialog(); var cameraAlbum = form.getItems().items[0].getValue(), vls = form.getValues(), app = TstApp.getApplication(); app.camera = (cameraAlbum === 'camera'); app.srvr = vls.srvr; app.quality = vls.quality; app.saveCfg(); } }, settings: function() { var dialog = this.dialog, view; if(!dialog) { view = this.getView(); dialog = Ext.apply({ 150
hideMode: 'offsets', ownerCmp: view }, view.dialog); this.dialog = dialog = Ext.create(dialog); } dialog.show(); }, onShow: function(dialog, eOpts) { var app = TstApp.getApplication(), form = this.lookup('form'); form.setValues({ srvr: app.srvr, quality: app.quality }); var btn = form.getItems().items[0]; btn.setValue(app.camera ? 'camera' : 'album'); } }); Файл app/view/camera/GetPhotoModel.js Ext.define('TstApp.view.camera.GetPhotoModel', { extend: 'Ext.app.ViewModel', alias: 'viewmodel.camera-getphoto', data: { name: 'TstApp', photoBtnDisabled: false, uploadBtnDisabled: true, progresshidden: true, progress: 0, settingsBtnDisabled: false } }); Компонент showResultPnl Файл app/view/result/ShowResultPnl.js Ext.define('TstApp.view.result.ShowResultPnl',{ extend: 'Ext.Panel', xtype: 'showResultPnl', requires: [ 'Ext.Button', 'Ext.Img', 'Ext.Toolbar' ], controller: 'result-showresultpnl', viewModel: { type: 'result-showresultpnl' }, layout: { type:"vbox", pack:"center", align:"stretch" 151
}, items: [{ reference: 'resultholder', xtype: "image", flex: 1 }], tbar: [{ reference: 'backToGetPhoto', text: "back", handler: 'backToGetPhoto', iconCls: 'x-fa fa-angle-double-left', ui: 'action' }] }); Файл app/view/result/ShowResultPnlController.js Ext.define('TstApp.view.result.ShowResultPnlController', { extend: 'Ext.app.ViewController', alias: 'controller.result-showresultpnl', backToGetPhoto: function() { var mainCtr = this.getView().up('mainCtr'); mainCtr.setActiveItem(0); } }); Файл app/view/result/ShowResultPnlModel.js Ext.define('TstApp.view.result.ShowResultPnlModel', { extend: 'Ext.app.ViewModel', alias: 'viewmodel.result-showresultpnl', data: { name: 'TstApp' } }); 152
ЛИТЕРАТУРА 1. Заяц, А. М. Проектирование и разработка web-приложений. Введение в frontend и backend разработку на JavaScript и node.js : учеб. пособие / А. М. Заяц, Н. П. Васильев. — СПб. : Лань, 2019. — 120 с. 2. Заяц, А. М. Организация беспроводных Ad Hoc и HotSpot сетей в среде ОС Windows : учеб. пособие / А. М. Заяц, С. П. Хабаров. — СПб. : Лань, 2019. — 220 с. 3. Хабаров, С. П. Основы моделирования технических систем. Среда SimlTech : учеб. пособие / С. П. Хабаров, М. Л. Шилкина. — СПб. : Лань, 2019. — 120 с. 4. Хабаров, С. П. Вычислительные машины, системы и сети : учеб. пособие / С. П. Хабаров, М. Л. Шилкина. — СПб. : СПбГЛТУ, 2017. — 240 с. 5. Хабаров, С. П. Сетевые технологии взаимодействия Ubuntu и Windows платформ / С. П. Хабаров, Ю. А. Жук. — СПб. : Наука и техника, 2013. — 369 с. 6. Заяц, А. М. Исследование алгоритма работы распределенной системы мониторинга лесных территорий / А. М. Заяц, С. П. Хабаров // Известия СПбЛТА. — № 229. — СПб. : СПбГЛТУ, 2019. — С. 243–254. 7. Васильев, Н. П. Мобильные Cordova-приложения сбора данных о состоянии лесных территорий с привязкой к геопозиции // Известия СПбЛТА. — № 230. — СПб. : СПбГЛТУ, 2020. — С. 262–271. 8. Васильев, Н. П. Интеграция гибридных приложений Cordova c webсерверной обработкой графики / Н. П. Васильев, Н. В. Лушкин // Информационные системы и технологии: теория и практика : сб. научн. тр. — Вып. 11. — СПб. : СПбГЛТУ, 2019. — С. 10–24. 9. Васильев, Н. П. Универсальные технологии разработки мобильных приложений // Информационные системы и технологии: теория и практика : сб. научн. тр. — Вып. 10. — Ч. 1. — СПб. : СПбГЛТУ, 2018. — С. 23–30. 10. Васильев, Н. П. Гибридные технологии разработки приложений для мобильных платформ // Информационные системы и технологии: теория и практика : сб. научн. тр. — Вып. 9. — СПб. : СПбГЛТУ, 2017. — С. 12–21. 11. Хабаров, С. П Организация гетерогенных ЛВС с терминальным доступом между ее узлами // Известия СПбЛТА. — № 216. — СПб. : СПбГЛТУ, 2016. — С. 267–280. 12. Dayalan, A. Ext JS 6 By Example. — Birmingham : Mumbai : Packt Publishing Ltd., 2015. — 226 p. 13. Ashworth, S. Ext JS 4 Web Application Development Cookbook / S. Ashworth, A. Duncan. — Birmingham : Mumbai : Packt Publishing Ltd., 2012. — 488 p. 14. Frederick, S. Learning Ext JS / S. Frederick, C. Ramsay, S. 'Cutter' Blades. — Birmingham ; Mumbai : Packt Publishing Ltd., 2008. — 324 p. 15. Apache Cordova [Электронный ресурс]. — Режим доступа: https://cordova.apache.org/. 16. Nodejs [Электронный ресурс]. — Режим доступа: https://nodejs.org/en/. 17. Sencha [Электронный ресурс]. — Режим доступа: https://www.sencha. com/products/extjs/. 153
ОГЛАВЛЕНИЕ Введение ...................................................................................................................... 3 1. Разработка мобильных приложений для web-разработчиков................... 5 1.1. Гибридные приложения.................................................................................... 5 1.1.1. Происхождение термина ............................................................................ 5 1.1.2. Достоинства и недостатки гибридных приложений ............................... 6 1.2. NativeScript ......................................................................................................... 7 1.2.1. Происхождение термина ............................................................................ 7 1.2.2. Достоинства и недостатки NativeScript .................................................... 9 1.2.3. Сравнение гибридной и NativeScript-разработки .................................... 9 1.3. Заключение ...................................................................................................... 10 2. Разработка гибридных приложений на основе Cordova .......................... 12 2.1. Установка Cordova .......................................................................................... 12 2.2. Обновление Cordova ....................................................................................... 12 2.3. Что потребуется дополнительно для разработки под iOS ......................... 13 2.4. Что потребуется дополнительно для разработки под Android .................. 14 2.5. Создание приложения Cordova ...................................................................... 17 2.5.1. Создание стартового (шаблонного) приложения .................................. 17 2.5.2. Назначение мета-тегов ............................................................................. 20 2.5.3. Индикация состояния Cordova ................................................................ 21 2.5.4. Добавление платформы ............................................................................ 22 2.5.5. Плагин cordova-plugin-whitelist ............................................................... 25 2.6. Базовые плагины Cordova .............................................................................. 27 2.7. Пример приложения, демонстрирующего работу с геолокацией ............. 29 2.7.1. Touch-экраны ............................................................................................. 29 2.7.2. Добавление плагина геолокации ............................................................. 31 2.7.3. Отображение данных геолокации ........................................................... 32 2.7.4. Функционирование приложения (index.js) ............................................. 37 2.7.5. Настройка приложения под iOS .............................................................. 40 2.7.6. Окончательная сборка и установка приложения ................................... 40 154
3. Использование фреймворков JavaScript ...................................................... 42 3.1. Обзор популярных web-фреймворков для мобильных платформ ............ 42 3.2. Sencha Ext JS .................................................................................................... 43 3.2.1. Обзор Sencha Ext JS .................................................................................. 43 3.2.2. Технология работы с GPL-версией Ext JS SDK и Sencha Cmd ............ 46 3.2.2.1. Создание рабочего пространства ......................................................... 46 3.2.2.2. Справочная система Sencha Cmd ......................................................... 47 3.2.2.3. Создание приложения............................................................................ 50 3.2.2.4. Сборка и запуск приложения в браузере ............................................. 51 3.2.3. Технология работы с Ext JS CE ............................................................... 52 3.2.3.1. Подготовка среды .................................................................................. 52 3.2.3.2. Создание приложения............................................................................ 54 3.2.3.3. Сборка и запуск приложения в браузере ............................................. 57 3.2.3.4. Устранение неполадок ........................................................................... 60 3.2.4. Интеграция приложений Sencha Ext JS с Cordova................................. 62 3.2.4.1. Интеграция с Cordova на основе GPL-версии Ext JS SDK и Sencha Cmd ....................................................................................................................... 62 4. Пример разработки гибридного приложения на Sencha Ext JS .............. 65 4.1. Работа с камерой (фотоальбомом) устройства .......................................... 67 4.2. Работа с файловой системой устройства.................................................... 68 4.3. Генерация шаблона приложения и настройка рабочей среды ................ 71 4.4. Классы в Ext JS.............................................................................................. 74 4.4.1. Соглашения об именах ............................................................................. 74 4.4.2. Исходные файлы ....................................................................................... 74 4.4.3. Определение класса .................................................................................. 75 4.5. Главное окно приложения ........................................................................... 75 4.6. Компонент getPhotoPnl ................................................................................... 80 4.6.1. Визуальное представление компонента ................................................. 81 4.6.2. Контроллер компонента. Работа с камерой ........................................... 84 4.6.3. Загрузка фото на сервер ........................................................................... 87 4.6.4. Обработка фото на сервере ...................................................................... 88 4.6.5. Настройка параметров приложения ........................................................ 89 4.6.6. Сохранение конфигурационных параметров приложения ................... 93 155
4.7. Компонент showResultPnl............................................................................... 97 4.7.1. Визуальное представление компонента ................................................. 98 4.7.2. Контроллер компонента ........................................................................... 99 4.8. Заключение ...................................................................................................... 99 5. Развёртывание (deployment) и распространение (distributing) iOS-приложений ......................................... 100 5.1. Enroll in Program ............................................................................................ 101 5.2. О подписи кода .............................................................................................. 108 5.3. Формальная процедура получения сертификатов с помощью Xcode .... 112 5.4. Provisioning Profile......................................................................................... 114 5.4.1. App ID ....................................................................................................... 115 5.4.2. Добавление идентификаторов устройств (UDID) на портале разработчиков .................................................................................................... 116 5.4.3. Создание профилей вручную (через портал разработчиков) ............. 117 5.4.4. Создание профилей автоматически (через Xcode) .............................. 118 5.5. Бесплатный ssl-сертификат .......................................................................... 119 5.6. Подготовка архива приложения для Ad Hoc распространения............... 124 5.6.1. Создание и экспорт архива приложения с помощью Xcode .............. 124 5.6.2. Иконки и заставка приложения ............................................................. 130 5.7. Подготовка активов приложения для загрузки на web-сервер................ 138 5.7.1. Использование сервиса Diawi ................................................................ 138 5.7.2. Есть хостинг и ssl-сертификат ............................................................... 139 Приложение ............................................................................................................ 141 Класс Application ............................................................................................... 141 Файл app.js ......................................................................................................... 141 Файл app/Application.js ..................................................................................... 141 Компонент mainCtr............................................................................................... 144 Файл app/view/mainCtr/Main.js ........................................................................ 144 Файл app/view/mainCtr/MainController.js ........................................................ 144 Файл app/view/mainCtr/MainModel.js .............................................................. 144 Компонент getPhotoPnl ........................................................................................ 144 Файл app/view/camera/GetPhoto.js ................................................................... 144 Файл app/view/camera/GetPhotoController.js ................................................... 147 156
Файл app/view/camera/GetPhotoModel.js ......................................................... 151 Компонент showResultPnl.................................................................................... 151 Файл app/view/result/ShowResultPnl.js ............................................................ 151 Файл app/view/result/ShowResultPnlController.js ............................................ 152 Файл app/view/result/ShowResultPnlModel.js .................................................. 152 Литература .............................................................................................................. 153 157
Николай Павлович ВАСИЛЬЕВ, Анатолий Моисеевич ЗАЯЦ ВВЕДЕНИЕ В ГИБРИДНЫЕ ТЕХНОЛОГИИ РАЗРАБОТКИ МОБИЛЬНЫХ ПРИЛОЖЕНИЙ Учебное пособие Зав. редакцией литературы по информационным технологиям и системам связи О. Е. Гайнутдинова Ответственный редактор Т. С. Спирина Подготовка макета Т. Д. Крюкова Корректор Е. А. Романова Выпускающий В. А. Иутин ЛР № 065466 от 21.10.97 Гигиенический сертификат 78.01.10.953.П.1028 от 14.04.2016 г., выдан ЦГСЭН в СПб Издательство «ЛАНЬ» lan@lanbook.ru; www.lanbook.com 196105, СанктПетербург, пр. Юрия Гагарина, д. 1, лит. А Тел./факс: (812) 3362509, 4129272 Бесплатный звонок по России: 88007004071 Подписано в печать 06.04.20. Бумага офсетная. Гарнитура Школьная. Формат 70×100 1/16. Печать офсетная. Усл. п. л. 13,00. Тираж 50 экз. Заказ № 34020. Отпечатано в полном соответствии с качеством предоставленного оригиналмакета в АО «Т8 Издательские Технологии». 109316, г. Москва, Волгоградский пр., д. 42, к. 5. Powered by TCPDF (www.tcpdf.org)