Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Гибридное приложение Яндекс.Авиабилетов

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →

Гибридное приложение Яндекс.Авиабилетов

В ноябре 2016 вышло обновление приложения Яндекс.Авиабилеты для iOS, в котором часть экранов была заменена на веб-страницы. С тех пор мы успели выпустить гибридную версию для Android, наладили выкатку новых вебовых фич и, конечно, знатно прошлись по граблям.

Мы с моим коллегой Андреем Легчилиным расскажем, какие задачи стояли перед нами при переходе на гибридное приложение и насколько нам удалось их достичь. Поговорим про особенности реализации гибридных приложений под iOS и Android. Обязательно остановимся на изменениях в процессах: поправки пришлось внести в релизный цикл и тестирование.

Avatar for Roman Paradeev

Roman Paradeev

April 14, 2017

More Decks by Roman Paradeev

Other Decks in Programming

Transcript

  1. › Дублирование кода › … а значит, и багов ›

    Низкий bus-factor › Различия в реализации › Неравномерное развитие Проблемы 8
  2. React Native › Нативный интерфейс, логика на React Cordova ›

    Нативная оболочка, приложение на HTML + CSS + JavaScript Гибрид › Замена части экранов на web-view Пути решения 10
  3. › вёрстка для гибридного приложения › общение приложения с веб-сайтом

    › обратная совместимость Адаптация веб-сайта под приложение 14
  4. Создание запроса 15 var url = URLComponents(string: "https:!//avia.yandex.ru/")! url.queryItems =

    [ URLQueryItem(name: "fromName", "Екатеринбург"), URLQueryItem(name: "toName", value: "Симферополь"), URLQueryItem(name: "when", value: "2017-05-01"), ] let request = URLRequest(url: url.url) !// malkovich malkovich let webView = WKWebView() webView.load(request)
  5. Идентификатор платформы передаётся
 в кастомном HTTP-заголовке Handshake 17 var request

    = URLRequest(url: requestURL) request.addValue("ios", forHTTPHeaderField: "X-Platform") webView.load(request)
  6. 18 const platforms = { ios: 'ios', android: 'android', wp:

    'wp', } module.exports = (req, res, next) !-> { const name = req.headers['x-platform'] const platform = platforms[name] !|| 'web' res.cookie('platform', platform) next() }
  7. Условия в шаблонах 19 bt.match('search-form', ctx !-> { ctx.setContent([ !platform.isWeb()

    !&& { elem: 'action-back' }, { elem: 'info' }, platform.isWeb() ? { elem: 'action-setting' } : { elem: 'action-share' } ]) })
  8. › Приложение принимает решение на основе URL › Не требует

    изменений в коде веб-сайта › Добавляет неявную связь между сайтом и приложением Перехват события навигации 21
  9. Приложение принимает решение на основе URL Перехват события навигации 22

    <a class="y-button" href="/search/?fromName=Москва&when=2017-04-18"> Купить !</a>
  10. Приложение принимает решение на основе URL Перехват события навигации 23

    <a class="y-button" href="/search/?fromName=Москва&when=2017-04-18"> Купить !</a>
  11. В приложении описываются методы, которые может использовать JavaScript веб-сайта Декларация

    интерфейса 24 this._liveBind(‘click', (event, data) !-> { coordinator.doAction('showPartnerRedirect', { offer: data.offer, redirectUrl: data.redirectUrl, }) })
  12. В приложении описываются методы, которые может использовать JavaScript веб-сайта Декларация

    интерфейса 25 this._liveBind(‘click', (event, data) !-> { coordinator.doAction('showPartnerRedirect', { offer: data.offer, redirectUrl: data.redirectUrl, }) })
  13. 26 class Coordinator { doAction(name, payload) { switch (platform) {

    case 'ios': return iosInterface.handle(name, payload) case 'android': return androidInterface[name](payload) default: return this.propagateEvent(name, payload) } } })
  14. 27 class Coordinator { doAction(name, payload) { switch (platform) {

    case 'ios': return iosInterface.handle(name, payload) case 'android': return androidInterface[name](payload) default: return this.propagateEvent(name, payload) } } })
  15. Сервер принимает решение:
 отдать вёрстку либо ответить ошибкой. Версионирование 29

    var request = URLRequest(url: requestURL) request.addValue("ios", forHTTPHeaderField: «X-Platform") request.addValue("1.14", forHTTPHeaderField: "X-Version")
  16. iOS

  17. › выбор компонента WebView (WKWebView, UIWebView) › обработка событий (WKScriptMessageHandler,

    WKNavigationDelegate, WKUIDelegate) › Apple Transport Security Гибридизация iOS-приложения 31
  18. › Apple рекомендует › Новый JS-движок Nitro › Не работает

    с Interface Builder › Работает в отдельном процессе › Выпущен недоделанным WKWebView 34
  19. Перехват события навигации 36 extension SearchViewController: WKNavigationDelegate { func webView(_

    webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) !-> Void) { if isAviaDomain(navigationAction.request.url!) { decisionHandler(.allow) } else { decisionHandler(.cancel) openURLInBrowser(navigationAction.request.url!) }}}}
  20. Перехват события навигации 37 extension SearchViewController: WKNavigationDelegate { func webView(_

    webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) !-> Void) { if isAviaDomain(navigationAction.request.url!) { decisionHandler(.allow) } else { decisionHandler(.cancel) openURLInBrowser(navigationAction.request.url!) }}}}
  21. target="_blank" 38 extension SearchViewController: WKUIDelegate { func webView(_ webView: WKWebView,

    createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) !-> WKWebView? { if navigationAction.targetFrame !== nil { webView.load(navigationAction.request) } return nil } }
  22. target="_blank" 39 extension SearchViewController: WKUIDelegate { func webView(_ webView: WKWebView,

    createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) !-> WKWebView? { if navigationAction.targetFrame !== nil { webView.load(navigationAction.request) } return nil } }
  23. Декларация интерфейса 40 final class ScriptMessageHandler: WKScriptMessageHandler { func userContentController(_

    userContentController: WKUserContentController, didReceive message: WKScriptMessage) { if let payload = message.body as? ["String": Any] { delegate!?.recieveMessage(withPayload: payload) } } }
  24. Декларация интерфейса 41 final class ScriptMessageHandler: WKScriptMessageHandler { func userContentController(_

    userContentController: WKUserContentController, didReceive message: WKScriptMessage) { if let payload = message.body as? ["String": Any] { delegate!?.recieveMessage(withPayload: payload) } } }
  25. 42 !// iOS let scriptMessageHandler = ScriptMessageHandler() let userContentController =

    WKUserContentController() userContentController.add(scriptMessageHandler, name: "notification") let configuration = WKWebViewConfiguration() configuration.userContentController = userContentController let webView = WKWebView(configuration: configuration) !// Browser (JavaScript) class iosDelegate { handle(name, payload) { const sender = window.webkit.messageHandlers["notification"] sender.postMessage({ name, payload }) } }
  26. 43 !// iOS let scriptMessageHandler = ScriptMessageHandler() let userContentController =

    WKUserContentController() userContentController.add(scriptMessageHandler, name: "notification") let configuration = WKWebViewConfiguration() configuration.userContentController = userContentController let webView = WKWebView(configuration: configuration) !// Browser (JavaScript) class iosDelegate { handle(name, payload) { const sender = window.webkit.messageHandlers["notification"] sender.postMessage({ name, payload }) } }
  27. 47 func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error:

    Error) { let nsError = error as NSError if #available(iOS 9.0, *) { let sslErrorCodes = [ NSURLErrorAppTransportSecurityRequiresSecureConnection, NSURLErrorSecureConnectionFailed, ] if sslErrorCodes.contains(nsError.code) { let url = nsError.userInfo["NSErrorFailingURLKey"] as! URL self.openURLInBrowser(url) } }
  28. 48 func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error:

    Error) { let nsError = error as NSError if #available(iOS 9.0, *) { let sslErrorCodes = [ NSURLErrorAppTransportSecurityRequiresSecureConnection, NSURLErrorSecureConnectionFailed, ] if sslErrorCodes.contains(nsError.code) { let url = nsError.userInfo["NSErrorFailingURLKey"] as! URL self.openURLInBrowser(url) } }
  29. › обработка событий (JSInterface) › разделение на несколько сборок ›

    поэтапная выкатка Гибридизация Android-приложения 50
  30. Передача данных из веба в приложение 51 !// Android
 webview.addJavascriptInterface(


    JSInterface(),
 "MyInterface"
 )
 
 !// Website
 MyInterface.addToFavorites(JSON.stringify({
 flight: 'S7 01'
 }))
  31. 52 inner class JSInterface {
 @JavascriptInterface
 fun addToFavorites(json: String) {


    onAddToFavorites(json)
 }
 
 @JavascriptInterface
 fun removeFromFavorites(json: String) {
 onRemoveFromFavorites(json)
 }
 }
  32. Поддержка JS API По данным caiuse.com 54 Android 4.3 Android

    4.4 Android 4.4.4 Chrome 53 0 % 25 % 50 % 75 % 100 % Да Частично Нет
  33. Поддержка JS API По данным caiuse.com 56 Android 4.3/w Crosswalk

    Android 4.4/w Crosswalk Android 4.4.4/w Crosswalk Chrome 53 0 % 25 % 50 % 75 % 100 % Да Частично Нет
  34. Crosswalk lite 61 › Весит примерно ~10MB › Собран с

    оптимизацией для снижения размера › Отключены: WebRTC, WebP, Web Video, Web Audio, Web Bluetooth, Web Notifications, …
  35. Crosswalk lite 62 › Сжат с применением LZMA › Собран

    с оптимизацией для снижения размера › Отключены: WebRTC, WebP, Web Video, Web Audio, Web Bluetooth, Web Notifications, … › Весит примерно ~10MB
  36. Несколько apk – это просто 63 !// build.gradle productFlavors {

    api16_armv7 { !!... } api16_x86 { !!... } api21 { !!... } }
  37. 64 api16_armv7 { !// Chromium должен собираться на 23-й версии


    targetSdkVersion 23
 compileSdkVersion 23
 buildToolsVersion "23.0.3"
 
 minSdkVersion 16
 
 versionCode defaultConfig.versionCode + 2
 
 ndk {
 abiFilters "armeabi-v7a", ""
 }
 }
  38. Результат 65 API21 (ARM&x86) 6,92 МБ (83.2%) API16+ (x86) 34,03

    МБ (0.7%) API16+ (ARM) 30,24 МБ (16.1%) Crosswalk x86 binary ARM binary
  39. Параллельная выкладка 0 % 25 % 50 % 75 %

    100 % дни 1 2 3 4 API21 API16x86 API16ARM 67
  40. 0 % 100 % 200 % 300 % дни 1

    2 3 4 5 6 7 8 API21 API16x86 API16ARM 68 Последовательная выкладка
  41. Роадмап 70 Апрель М ай И ю нь И ю

    ль Август Сентябрь О ктябрь Н оябрь Д екабрь
  42. iOS party – июнь bit.ly/iOSekb @iospartyekb Droid party – 20

    апреля, 18:30 Минутка рекламы! 79