Upload
yandex
View
241
Download
3
Embed Size (px)
DESCRIPTION
Зачем нужен JavaScript в iOS-приложениях
Citation preview
JavaScript в iOS приложениях
Александр Денисов Евгений Дымов
Часть 1 JavaScript и WebView
Почему WebView?
5
Просто и удобно
Открытие веб-страниц
Safari
Пользователь остается в нашем приложении
WebView
Социальная авторизация
6
Потому что никто не любит регистрироваться
Просмотр документов
7
〉Microsoft Office
〉iWork
〉Медиа-контент
UIWebView
WKWebView!
WKWebView
〉WebKit framework
〉Богатый API
〉Быстрый рендеринг
〉Быстрый JavaScript
〉Отдельный процесс
10
http://bigwol.com/surprised-baby-hd-widescreen-hd-free-wallpaper
Скорость выполнения JavaScript
SunSpider: https://www.webkit.org/perf/sunspider/sunspider.html
11
@ iPhone 5s, iOS 8.1
WKWebView
UIWebView
0 375 750 1125 1500
Время выполнения, мс
WKWebView
Бонус: “из коробки” получаем:
〉Навигация жестами
〉Звонок с другого устройства
〉Открытие в новой вкладке
〉Заголовок страницы
API diff: http://nshipster.com/wkwebkit/
12
Как?
Что нам потребуется
〉Процесс загрузки страницы
〉Выполнение JavaScript
〉Связь JavaScript — нативный код
14
Процесс загрузки страницы
15
WKWebView
@protocol WKNavigationDelegate; @protocol WKUIDelegate;
UIWebView
@protocol UIWebViewDelegate;
- (void)webView:(WKWebView*)webView decidePolicyForNavigationAction:(WKNavigationAction*)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
- (void)webView:(WKWebView*)webView decidePolicyForNavigationResponse: (WKNavigationResponse*)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
- (void)webView:(WKWebView*)webView didStartProvisionalNavigation:(WKNavigation*)navigation;
- (void)webView:(WKWebView*)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation*)navigation;
WKNavigationDelegate
16
17
- (void)webView:(WKWebView*)webView didFailProvisionalNavigation:(WKNavigation*)navigation withError:(NSError*)error;
- (void)webView:(WKWebView*)webView didCommitNavigation:(WKNavigation*)navigation;
- (void)webView:(WKWebView*)webView didFinishNavigation:(WKNavigation*)navigation;
- (void)webView:(WKWebView*)webView didFailNavigation:(WKNavigation*)navigation withError:(NSError*)error;
- (void)webView:(WKWebView*)webView didReceiveAuthenticationChallenge: (NSURLAuthenticationChallenge*)challenge completionHandler:(void (^)( NSURLSessionAuthChallengeDisposition disposition, NSURLCredential*credential))completionHandler;
- (WKWebView*)webView:(WKWebView*)webView createWebViewWithConfiguration: (WKWebViewConfiguration*)configuration forNavigationAction:(WKNavigationAction*)navigationAction windowFeatures:(WKWindowFeatures*)windowFeatures;
- (void)webView:(WKWebView*)webView runJavaScriptAlertPanelWithMessage:(NSString*)message initiatedByFrame:(WKFrameInfo*)frame completionHandler:(void (^)())completionHandler;
WKUIDelegate
18
19
- (void)webView:(WKWebView*)webView runJavaScriptConfirmPanelWithMessage:(NSString*)message initiatedByFrame:(WKFrameInfo*)frame completionHandler:(void (^)(BOOL result))completionHandler;
- (void)webView:(WKWebView*)webView runJavaScriptTextInputPanelWithPrompt:(NSString*)prompt defaultText:(NSString*)defaultText initiatedByFrame:(WKFrameInfo*)frame
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType;
- (void)webViewDidStartLoad:(UIWebView*)webView;
- (void)webViewDidFinishLoad:(UIWebView*)webView;
- (void)webView:(UIWebView*)webView didFailLoadWithError:(NSError*)error;
UIWebViewDelegate
20
Выполнение кода JavaScript
21
WKWebView
- (void)evaluateJavaScript:(NSString*)javaScriptString completionHandler:(void (^)(id, NSError*))completionHandler;
UIWebView
- (NSString*)stringByEvaluatingJavaScriptFromString:(NSString*)script;
Выполнение кода JavaScript: пример
22
// Favicon NSString* faviconJS = @"document.querySelector(\"link[rel='shortcut icon']\")" ".getAttribute('href')"; [wkWebView evaluateJavaScript:faviconJS completionHandler: ^(NSString* favicon, NSError* error) { if (favicon) { [self didObtainFavicon:favicon]; } }];
WKWebView
Выполнение кода JavaScript: пример
23
// Заголовок страницы NSString* titleJS = @"document.title"; return [uiWebView stringByEvaluatingJavaScriptFromString:titleJS];
UIWebView
Связь JavaScript — нативный код
24
WKWebView
@protocol WKScriptMessageHandler <NSObject>
@required
- (void)userContentController: (WKUserContentController*)userContentController didReceiveScriptMessage:(WKScriptMessage*)message;
@end
25
@interface WKUserContentController : NSObject
...
- (void)addScriptMessageHandler: (id<WKScriptMessageHandler>)scriptMessageHandler name:(NSString*)name;
@end !
!
!
// Добавление scriptMessageHandler создает для всех веб-фреймов // функцию: window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
Связь JavaScript — нативный код
26
UIWebView
function YACSendToNativeCode(name, data) { var message = { }; message.name = name; message.data = data; window.location = "jsbridge://" + escape(JSON.stringify(message)); };
27
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType { if ([request.URL.scheme isEqualToString:@"jsbridge"]) { // Обработка сообщения return NO; } return YES; }
JavaScript библиотеки
Пример: jQuery
〉Быстро получаем богатую функциональность
〉Возможен конфликт
〉Снижение производительности
〉Кроссбраузерная совместимость?
Можем реализовать необходимую функциональность самостоятельно!
28
NSURLProtocol (только UIWebView)
Управление сетевым стеком с помощью NSURLProtocolhttps://tech.yandex.ru/events/yac/2013/talks/1076/
Можем реализовать:
〉Сжатие трафика
〉Поддержку новых форматов WebP WebM …
29
Саади
«Два человека бесплодно трудились и без пользы старались: тот, кто копил богатство и не пользовался им, и тот, кто учился наукам, но не применял их»
А теперь делаем «фичи»!
Inspired by
Примеры
〉Prerendering, DNS prefetching
〉Управление медиаконтентом
〉Контекстное меню
〉Сохранение паролей
〉Социальная авторизация
〉Изменение DOM
32
Prerendering, DNS prefetching
DNS-запросы: от 1мс до нескольких секунд
Prerendering: субъективное ускорение загрузки страниц
33
<link rel="prerender" href="http://example.org/index.html">
<link rel="dns-prefetch" href="http://host-to-prefetch.com">
34
function YACPrerenderLinks() { var results = [], nodeList = document.querySelector("link[rel='prerender']"); !
for (var i = 0; i < nodeList.length; i++) { var href = nodeList[i].getAttribute("href"); results.push(href); } return results.join(); } !
function YACDNSPrefetchLinks() { ... }
Управление медиаконтентом
var YACAudioElementsList = [ ], YACHTMLAudioElementPlay = window.HTMLAudioElement.prototype.play; !
function YACAddHTMLAudioElementToList(el) { YACHTMLAudioElementsList.push(el); } !
window.HTMLAudioElement.prototype.play = function() { YACAddHTMLAudioElementToList(this); this.play = YACHTMLAudioElementPlay; this.play(); }
35
36
function YACPauseHTMLAudioElements() { for (i = 0; i < YACHTMLAudioElementsList.length; i++) { YACHTMLAudioElementsList[i].pause(); } } !
function YACResumePausedHTMLAudio() { ... }
Контекстное меню
37
〉Открыть вкладку
〉Копировать ссылку
〉Поделиться ссылкой
〉Сохранить изображение
〉Позвонить с другого устройства
Контекстное меню
// 1. Получаем элемент по координатам и декодируем из JSON // 2. Заполняем и показываем UIActionSheet - (void)openContextualMenuAtPoint:(CGPoint)point { YACInteractiveHTMLElement* element = [self interactiveElementAtPoint:point]; [self showContextualMenuForElement:element atPoint:point]; }
38
Разберем на примере YACImageLinkForTouchAtPoint
39
function YACElementFromPoint(x, y) { var currentDocument = document, element = currentDocument.elementFromPoint(x, y), boundRect; try { while (element && (element.nodeName === "FRAME" || element.nodeName === "IFRAME")) { boundRect = element.getBoundingClientRect(); x -= boundRect.left; y -= boundRect.top; currentDocument = element.contentDocument; element = currentDocument.elementFromPoint(x, y); } } catch (e) { } return element; }
40
function YACImageLinkForTouchAtPoint(x, y) { var element = YACElementFromPoint(x, y); return YACImageLinkForElement(element); } !
!
function YACImageLinkForElement(element) { if (element.tagName === "IMG" && element.src) { return element.src; } return null; }
Сохранение паролей
41
Обязательно спрашиваем разрешения!
42
// 1. Перехватываем событие отправки формы // Обработчик вызывается для каждой отправляемой формы window.addEventListener("submit", YACFormSubmitHandler, false); !
!
// 2. Сохраняем заполненные данные в обработчике события function YACFormSubmitHandler(e) { if (e.target.nodeName !== "FORM") { return; } !
YACCollectLoginFormDataAndSendToNativeCode(e.target); }
43
// 3. Заполняем данные при загрузке страницы с такой же формой function YACFillLoginFormWithData( form, usernameElementName, usernameValue, passwordElementName, passwordValue) { !
var usernameElement = form.querySelector( "input[name='" + usernameElementName + "']"); usernameElement.value = usernameValue; !
var passwordElement = form.querySelector( "input[type='password'][name='" + passwordElementName + "']"); passwordElement.value = passwordValue; }
Социальная авторизация
44
Это:
〉Удобно
〉Привычно для пользователя
〉“Must have”
Социальная авторизация
45
Реализация с помощью WebView:
Доступ к данным на странице авторизации!
Социальная авторизация
45
Реализация с помощью WebView:
Доступ к данным на странице авторизации!
Социальная авторизация
45
Реализация с помощью WebView:
Доступ к данным на странице авторизации!
Совет: не используйте WebView для социальной авторизации
Реализация с помощью OAuth
1. Регистрируем приложение
2. Отправляем запрос https://oauth.yandex.ru/authorize? response_type=token & client_id=<идентификатор приложения>
3. Пользователь авторизуется и выдает разрешения для нашего приложения
4. Получаем данные в ответе http://www.example.com/token# access_token=<новый OAuth-токен> & expires_in=<время жизни токена в секундах>
46
47
// 1. Native SDK !
// 2. Callback URL scheme - обрабатываем в AppDelegate - (BOOL)application:(UIApplication*)application openURL:(NSURL*)url sourceApplication:(NSString*)sourceApplication annotation:(id)annotation; !
// 3. WebView (пример для UIWebView, WKWebView - аналогично) - (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType;
Изменение DOM с помощью JavaScript
48
VIDEO
Изменение DOM с помощью JavaScript
48
VIDEO
Что же получилось?
〉Мы разобрались в возможностях UIWebView и WKWebView
〉Научились писать “фичи” для веб-контента
〉Делаем пользователей счастливее
〉И все это — благодаря JavaScript!
49
Часть 2 JavaScript без WebView
Зачем?
Расширяемость
〉Внешняя логика
〉Интерпретация в runtime
〉JavaScript!
52
Платформонезависимость
〉Нативный UI
〉Общая бизнес-логика
〉Мгновенное обновление бизнес-логики
〉JavaScript!
53
Программируемость
Мы продолжаем наши передачи на этой частоте!
54
Как?
JavaScriptCore
〉Objective-C API доступно с iOS 7, Mac OS X 10.9
〉JavaScript движок в WebKit
〉Также известен как SquirrelFish, Nitro
56
© 2008 Goopymart
JavaScriptCore
〉JSContext
〉JSValue
〉<JSExport>
〉JSManagedValue
〉JSVirtualMachine
57
JSContext
〉Среда выполнения JavaScript кода
〉Глобальный объект (à la window)
!
- (JSValue*)evaluateScript:(NSString*)script;
58
JSValue
Ссылка на JavaScript объект
!
+ (JSValue*)valueWithObject:(id)value inContext:(JSContext*)context;
59
<JSExport>
Экспорт методов и свойств в JavaScript окружение
60
@protocol MyClassExports <JSExport>
- (void)foo; // Доступен из JavaScript
@end
@interface MyClass : NSObject <MyClassExports>
- (void)bar; // Недоступен из JavaScript
@end
Поехали!
Реализация
〉Нативная графика — Sprite Kit
〉Управление персонажем — JavaScript
〉Соединительное звено — JavaScriptCore
63
CharacterAI.js
var CharacterAI = function() { }; !
CharacterAI.prototype.onIdle = function(event) { this.character.ahead(100); this.character.turn(45); this.character.back(100); }; !
CharacterAI.prototype.onScannedEnemy = function(event) { this.character.fire(); }; ...
65
// TODO:
〉Создать экземпляр CharacterAI
〉Передать персонаж в JavaScript окружение
〉Экспортировать необходимые методы
〉Сообщать о событиях (onIdle, onScannedEnemy…)
66
Создать экземпляр CharacterAI
@property JSContext* context; @property JSValue* characterAI; !
... !
self.context = [[JSContext alloc] init]; [self.context evaluateScript:self.script.contents]; self.characterAI = [self.context evaluateScript:@"new CharacterAI()"];
67
Передать персонаж в JavaScript окружение
@property APAIntelligentHeroCharacter* character; !
... !
self.characterAI[@"character"] = self.character;
68
Экспортировать необходимые методы
@protocol APAIntelligentHeroCharacterExports <JSExport> - (void)ahead:(CGFloat)amount; ... @end !
@interface APAIntelligentHeroCharacter : APAHeroCharacter <APAIntelligentHeroCharacterExports> @end !
@implementation APAIntelligentHeroCharacter - (void)ahead:(CGFloat)amount { } ... @end
69
Сообщать о событиях
- (void)updateWithTimeSinceLastUpdate:(CFTimeInterval)interval { ... if (self.character.state == APACharacterStateIdle) { [self.characterAI invokeMethod:@"onIdle" withArguments:@[ event ]]; } ... }
70
Demo
72
72
Two more things…
Управление памятью@property JSValue* characterAI; ... - (void)setCharacterAI:(JSValue*)characterAI { _managedCharacterAI = [JSManagedValue managedValueWithValue:characterAI]; [characterAI.context.virtualMachine addManagedReference:_managedCharacterAI withOwner:self]; } !
- (JSValue*)characterAI { return [_managedCharacterAI value]; }
74
removeManagedReference:withOwner: для удаления ссылки
Многопоточность
〉API потокобезопасно
〉Область блокировки — JSVirtualMachine
〉JSVirtualMachine — контейнер для JSContext
〉Использование отдельной JSVirtualMachine для каждого потока
75
Материалы
〉WWDC 2014 Introducing the Modern WebKit API
〉nshipster.com/wkwebkit
〉tech.yandex.ru/events/yac/2013/talks/1076
〉WWDC 2013 Integrating JavaScript into Native Apps
〉trac.webkit.org/wiki/JavaScriptCore
〉github.com/WebKit/webkit
〉bit.ly/yac2014-ios-jstalks76
Зачем JavaScript в iOSприложениях?
Спасибо за внимание!
79
Александр Денисов
Контакты
stonespb
@_dymv
Евгений Дымов