Синхронная нативная связь JavaScript с WKWebView

Возможна ли синхронная связь между JavaScript и собственным кодом Swift / Obj-C с помощью WKWebView?

Это подходы, которые я пробовал и потерпел неудачу.

Подход 1. Использование обработчиков скриптов

Новый способ WKWebView получения JS-сообщений - использование метода делегата userContentController:didReceiveScriptMessage:, который вызывается из JS с помощью window.webkit.messageHandlers.myMsgHandler.postMessage('What's the meaning of life, native code?'). Проблема с этим подходом заключается в том, что во время выполнения собственного метода делегата выполнение JS не блокируется, поэтому мы не можем вернуть значение. немедленно вызвав webView.evaluateJavaScript("something = 42", completionHandler: nil).

Пример (JavaScript)

var something;
function getSomething() {
    window.webkit.messageHandlers.myMsgHandler.postMessage("What's the meaning of life, native code?"); // Execution NOT blocking here :(
    return something;
}
getSomething();    // Returns undefined

Пример (Swift)

func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
    webView.evaluateJavaScript("something = 42", completionHandler: nil)
}

Подход 2. Использование настраиваемой схемы URL

В JS перенаправление с использованием window.location = "js://webView?hello=world" вызывает собственные WKNavigationDelegate методы, из которых можно извлечь параметры запроса URL. Однако, в отличие от UIWebView, метод делегата не блокирует выполнение JS, поэтому немедленный вызов evaluateJavaScript для передачи значения обратно в JS здесь также не работает.

Пример (JavaScript)

var something;
function getSomething() {
    window.location = "js://webView?question=meaning" // Execution NOT blocking here either :(
    return something;
}
getSomething(); // Returns undefined

Пример (Swift)

func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler decisionHandler: (WKNavigationActionPolicy) -> Void) {
    webView.evaluateJavaScript("something = 42", completionHandler: nil)
    decisionHandler(WKNavigationActionPolicy.Allow)
}

Подход 3. Использование настраиваемой схемы URL и IFRAME

Этот подход отличается только способом присвоения window.location. Вместо прямого присвоения используется атрибут src пустого iframe.

Пример (JavaScript)

var something;
function getSomething() {
    var iframe = document.createElement("IFRAME");
    iframe.setAttribute("src", "js://webView?hello=world");
    document.documentElement.appendChild(iframe);  // Execution NOT blocking here either :(
    iframe.parentNode.removeChild(iframe);
    iframe = null;
    return something;
}
getSomething();

Тем не менее, это тоже не решение, он вызывает тот же собственный метод, что и подход 2, который не является синхронным.

Приложение: как этого добиться с помощью старого UIWebView

Пример (JavaScript)

var something;
function getSomething() {
    // window.location = "js://webView?question=meaning" // Execution is NOT blocking if you use this.

    // Execution IS BLOCKING if you use this.
    var iframe = document.createElement("IFRAME");
    iframe.setAttribute("src", "js://webView?question=meaning");
    document.documentElement.appendChild(iframe);
    iframe.parentNode.removeChild(iframe);
    iframe = null;

    return something;
}
getSomething();   // Returns 42

Пример (Swift)

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
    webView.stringByEvaluatingJavaScriptFromString("something = 42")    
}

person paulvs    schedule 10.11.2014    source источник
comment
Разве не лучший подход (с точки зрения UX) - воспользоваться преимуществами асинхронности? Например. В своем опубликованном сообщении для webkit укажите информацию о том, где должен быть опубликован «результат» действия из webkit. Например. javascript-метод gotSomething (), который может быть вызван WebKit с помощью stringByEvaluatingJavaScriptFromString?   -  person Mason G. Zhwiti    schedule 21.02.2015
comment
Представьте, что вы хотите спросить у нативного кода, установлено ли приложение. Если вы используете синхронные вызовы, вы могли бы сделать что-нибудь столь же простое, как var isTwitterInstalled = isInstalled('twitter');. Если у вас есть только асинхронные вызовы, вам придется разделить их на две функции; один, который вы вызываете из JavaScript, а другой, который вы вызываете из собственного кода, передавая результат.   -  person paulvs    schedule 23.02.2015
comment
Верно, но преимущество в том, что если для выполнения кода, который вы пытаетесь запустить в мире iOS, требуется какое-то время, ваш JavaScript вообще не будет его ждать, что делает пользовательский интерфейс более отзывчивым. В этом смысле он похож на Node.   -  person Mason G. Zhwiti    schedule 23.02.2015
comment
Проблема, с которой я столкнулся, заключается в том, что кажется невозможным вызвать JavaScript и использовать результат этого вызова * в ответ на запрос пользовательского интерфейса / фреймворка. Например, печать или копирование. Все мои попытки выполнить синхронизацию с помощью обработчика завершения привели к тупиковой ситуации.   -  person M. Anthony Aiello    schedule 21.03.2015
comment
Я заметил, что ваш примерный код использует локальную переменную, но ваш оцененный JS может не работать в той же области. Вы пробовали это с переменной на window?   -  person Steve Landey    schedule 28.03.2015
comment
@SteveJohnson, я думаю, что var something; на самом деле лишний, потому что инъекция из собственного кода создает something как глобальный, когда он присваивает ему свое значение.   -  person paulvs    schedule 30.03.2015
comment
@paulvs var something затмевает глобальное. Это не лишнее, это баг.   -  person Steve Landey    schedule 30.03.2015
comment
@SteveJohnson, пожалуйста, поправьте меня, если я ошибаюсь, но если строка с var something; не находится внутри какой-либо функции, т.е. она находится в глобальной области видимости, она создает глобальную переменную с именем something (эквивалент window.something = undefined;). Затем собственный код присваивает 42 глобальной переменной window.something (и если глобальная переменная не существует, он сначала создает ее, а затем присваивает ей значение). Итак, создадим ли мы объявление something заранее или нет, оно создается собственным кодом, верно?   -  person paulvs    schedule 31.03.2015
comment
@paulvs Вы, наверное, правы. Обычно я работаю в Browserify, поэтому я привык предполагать, что весь код уровня модуля заключен в функцию.   -  person Steve Landey    schedule 31.03.2015
comment
Могу ли я просто использовать ajax вместо window.location или iframe?   -  person Maria    schedule 16.10.2018


Ответы (5)


Нет, я не верю, что это возможно из-за многопроцессорной архитектуры WKWebView. WKWebView работает в том же процессе, что и ваше приложение, но взаимодействует с WebKit, который работает в собственном процессе (Знакомство с Modern WebKit API). Код JavaScript будет выполняться в процессе WebKit. По сути, вы просите синхронную связь между двумя разными процессами, что противоречит их замыслу.

person Martin Sherburn    schedule 03.12.2014
comment
Это действительно позор для нового блестящего API, такого как WKWebView, из-за отсутствия функции, которую Android имел в течение многих лет. - person paulvs; 03.12.2014
comment
в этом нет никакого смысла. почему этот метод postMessage не может вернуть обещание? - person Rahul Singh Bhadhoriya; 10.11.2020

Я тоже исследовал эту проблему, но потерпел неудачу, как и вы. Чтобы обойти эту проблему, вы должны передать функцию JavaScript в качестве обратного вызова. Собственная функция должна оценить функцию обратного вызова, чтобы вернуть результат. На самом деле это путь JavaScript, потому что JavaScript никогда не ждет. Блокировка потока JavaScript может вызвать ANR, это очень плохо.

Я создал проект под названием XWebView, который может установить мост между нативным и JavaScript. Он предлагает API в стиле привязки для вызова нативного кода из JavaScript и наоборот. Существует образец приложения.

person soflare    schedule 28.04.2015
comment
Как передать функцию JavaScript в качестве обратного вызова WKWebView? Из что я может видеть, разрешены только сериализуемые значения (строка, число, логическое значение). - person Vidar S. Ramdal; 18.08.2016
comment
@ VidarS.Ramdal, для тех несериализуемых объектов, которые нельзя передать по значению, они будут переданы по ссылке. Ссылка - это специальный крошечный (сериализуемый) объект, который представляет исходный несериализуемый объект. Операции, применяемые к объекту ссылки, будут преобразованы в выражение javascript и оценены XWebView. - person soflare; 22.08.2016
comment
Что ж, тогда я думаю, мой вопрос в том, как вы оперируете этим анонимным объектом. Как вы можете вызвать анонимную функцию JavaScript из Swift? - person Vidar S. Ramdal; 22.08.2016
comment
Хороший вопрос. Любой объект, переданный по ссылке (анонимный или нет), будет сохранен. в пространстве имен инициированного объекта плагина. Он будет выпущен, пока его ссылка выпущено на собственной стороне. - person soflare; 23.08.2016


Я столкнулся с аналогичной проблемой, я решил ее, сохранив обратные вызовы обещаний.

Js, которые вы загружаете в веб-представление через WKUserContentController :: addUserScript

var webClient = {
    id: 1,
    handlers: {},
};

webClient.onMessageReceive = (handle, error, data) => {
    if (error && webClient.handlers[handle].reject) {
        webClient.handlers[handle].reject(data);
    } else if (webClient.handlers[handle].resolve){
        webClient.handlers[handle].resolve(data);
    }

    delete webClient.handlers[handle];
};

webClient.sendMessage = (data) => {
    return new Promise((resolve, reject) => {
       const handle = 'm' + webClient.id++;
       webClient.handlers[handle] = { resolve, reject };
       window.webkit.messageHandlers.<message_handler_name>.postMessage({data: data, id: handle});
   });
}

Выполните Js-запрос как

webClient.sendMessage(<request_data>).then((response) => {
...
}).catch((reason) => {
...
});

Получить запрос в userContentController: didReceiveScriptMessage

Вызовите evalJavaScript с помощью webClient.onMessageReceive (дескриптор, ошибка, response_data).

person Ashwani Chandil    schedule 02.04.2019

Можно синхронно дождаться результата evalJavaScript, опросив текущий метод acceptInput RunLoop. Это позволяет вашему потоку пользовательского интерфейса отвечать на ввод, пока вы ждете завершения Javascript.

Прочтите предупреждения, прежде чем слепо вставлять их в свой код

//
//  WKWebView.swift
//
//  Created by Andrew Rondeau on 7/18/21.
//

import Cocoa
import WebKit

extension WKWebView {
    func evaluateJavaScript(_ javaScriptString: String) throws -> Any? {

        var result: Any? = nil
        var error: Error? = nil
        
        var waiting = true
        
        self.evaluateJavaScript(javaScriptString) { (r, e) in
            result = r
            error = e
            waiting = false
        }
        
        while waiting {
            RunLoop.current.acceptInput(forMode: RunLoop.Mode.default, before: Date.distantFuture)
        }
        
        if let error = error {
            throw error
        }
        
        return result
    }
}

Что происходит, так это то, что во время выполнения Javascript поток вызывает RunLoop.current.acceptInput, пока ожидание не станет ложным. Это позволяет пользовательскому интерфейсу вашего приложения быть отзывчивым.

Некоторые предупреждения:

  • Кнопки и т. д. на вашем пользовательском интерфейсе по-прежнему будут реагировать. Если вы не хотите, чтобы кто-то нажимал кнопку во время работы вашего Javascript, вам, вероятно, следует отключить взаимодействие с вашим пользовательским интерфейсом. (Это особенно актуально, если вы звоните на другой сервер в Javascript.)
  • Многопроцессорный характер вызова evalJavaScript может быть медленнее, чем вы ожидаете. Если вы вызываете код, который является мгновенным, все еще может замедлиться, если вы будете повторять вызовы Javascript в тесном цикле.
  • Я тестировал это только в основном потоке пользовательского интерфейса. Я не знаю, как этот код будет работать в фоновом потоке. Если есть проблемы, исследуйте их с помощью NSCondition.
  • Я тестировал это только на macOS. Я не знаю, работает ли это на iOS.
person Andrew Rondeau    schedule 19.07.2021