JavaScript menyinkronkan komunikasi asli ke WKWebView

Apakah komunikasi sinkron antara JavaScript dan kode asli Swift/Obj-C dimungkinkan menggunakan WKWebView?

Ini adalah pendekatan yang telah saya coba dan gagal.

Pendekatan 1: Menggunakan pengendali skrip

WKWebView Cara baru menerima pesan JS adalah dengan menggunakan metode delegasi userContentController:didReceiveScriptMessage: yang dipanggil dari JS oleh window.webkit.messageHandlers.myMsgHandler.postMessage('What's the meaning of life, native code?') Masalah dengan pendekatan ini adalah selama eksekusi metode delegasi asli, eksekusi JS tidak diblokir, jadi kami tidak dapat mengembalikan nilai dengan segera memanggil webView.evaluateJavaScript("something = 42", completionHandler: nil).

Contoh (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

Contoh (Cepat)

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

Pendekatan 2: Menggunakan skema URL khusus

Di JS, pengalihan menggunakan window.location = "js://webView?hello=world" memanggil metode WKNavigationDelegate asli, tempat parameter kueri URL dapat diekstraksi. Namun, tidak seperti UIWebView, metode delegasi tidak memblokir eksekusi JS, jadi segera memanggil evaluateJavaScript untuk meneruskan nilai kembali ke JS juga tidak berfungsi di sini.

Contoh (JavaScript)

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

Contoh (Cepat)

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

Pendekatan 3: Menggunakan skema URL khusus dan IFRAME

Pendekatan ini hanya berbeda dalam cara window.location ditugaskan. Alih-alih menetapkannya secara langsung, atribut src dari iframe kosong digunakan.

Contoh (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();

Meskipun demikian, ini juga bukan solusi, ini menggunakan metode asli yang sama seperti Pendekatan 2, yang tidak sinkron.

Lampiran: Cara mencapainya dengan UIWebView lama

Contoh (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

Contoh (Cepat)

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

person paulvs    schedule 10.11.2014    source sumber
comment
Bukankah pendekatan yang lebih baik (dalam hal UX) adalah memanfaatkan sifat asinkron? Misalnya. Dalam pesan yang Anda posting ke webkit, berikan informasi tentang di mana 'hasil' tindakan tersebut harus diposting dari webkit. Misalnya. metode javascript gotSomething() yang dapat dipanggil oleh WebKit menggunakan stringByEvaluatingJavaScriptFromString?   -  person Mason G. Zhwiti    schedule 21.02.2015
comment
Bayangkan Anda ingin menanyakan kode asli apakah suatu aplikasi diinstal. Jika Anda menggunakan panggilan sinkron, Anda dapat melakukan sesuatu yang sederhana seperti var isTwitterInstalled = isInstalled('twitter');. Jika Anda hanya menerima panggilan asinkron, Anda harus membaginya menjadi dua fungsi; satu yang Anda panggil dari JavaScript dan satu lagi yang Anda panggil dari kode asli, meneruskan hasilnya.   -  person paulvs    schedule 23.02.2015
comment
Benar, tetapi keuntungannya adalah jika diperlukan waktu sama sekali untuk mengeksekusi kode yang Anda coba jalankan di dunia iOS, JavaScript Anda tidak akan menunggu sama sekali, sehingga membuat UI lebih responsif. Dengan cara ini mirip dengan Node.js.   -  person Mason G. Zhwiti    schedule 23.02.2015
comment
Masalah yang saya alami di sini adalah tampaknya tidak mungkin memanggil JavaScript dan menggunakan hasil panggilan itu *sebagai respons terhadap permintaan UI/kerangka kerja. Misalnya, mencetak atau menyalin. Upaya saya untuk melakukan sinkronisasi melalui pengendali penyelesaian semuanya mengakibatkan kebuntuan.   -  person M. Anthony Aiello    schedule 21.03.2015
comment
Saya perhatikan kode contoh Anda menggunakan variabel lokal, tetapi JS yang Anda evaluasi mungkin tidak berjalan dalam cakupan yang sama. Sudahkah Anda mencobanya dengan variabel di window?   -  person Steve Landey    schedule 28.03.2015
comment
@SteveJohnson, menurut saya var something; sebenarnya berlebihan karena injeksi dari kode asli membuat something sebagai global ketika menetapkan nilainya.   -  person paulvs    schedule 30.03.2015
comment
@paulvs var something membayangi global. Itu tidak berlebihan, itu bug.   -  person Steve Landey    schedule 30.03.2015
comment
@SteveJohnson tolong perbaiki saya jika saya salah, tetapi jika baris dengan var something; tidak ada di dalam fungsi apa pun, yaitu dalam lingkup global, ini akan membuat variabel global bernama something (setara dengan window.something = undefined;). Kemudian, kode asli menetapkan 42 ke variabel global window.something (dan jika global tidak ada, kode tersebut akan membuatnya terlebih dahulu, lalu menetapkan nilainya). Jadi, apakah kita membuat deklarasi something terlebih dahulu atau tidak, itu dibuat oleh kode asli, bukan?   -  person paulvs    schedule 31.03.2015
comment
@ Paulvs Anda mungkin benar. Saya biasanya bekerja di Browserify, jadi saya terbiasa berasumsi semua kode tingkat modul dibungkus dalam suatu fungsi.   -  person Steve Landey    schedule 31.03.2015
comment
daripada menggunakan window.location atau iframe, bisakah saya menggunakan ajax saja?   -  person Maria    schedule 16.10.2018


Jawaban (5)


Tidak, saya tidak percaya itu mungkin karena arsitektur multi-proses WKWebView. WKWebView berjalan dalam proses yang sama dengan aplikasi Anda tetapi berkomunikasi dengan WebKit yang berjalan dalam prosesnya sendiri (Memperkenalkan API WebKit Modern). Kode JavaScript akan berjalan dalam proses WebKit. Jadi pada dasarnya Anda meminta komunikasi sinkron antara dua proses berbeda yang bertentangan dengan desainnya.

person Martin Sherburn    schedule 03.12.2014
comment
Sangat disayangkan API baru yang mengkilap seperti WKWebView tidak memiliki fitur yang dimiliki Android selama bertahun-tahun. - person paulvs; 03.12.2014
comment
itu tidak masuk akal sama sekali. mengapa metode postMessage tidak dapat mengembalikan janji? - person Rahul Singh Bhadhoriya; 10.11.2020

Saya juga menyelidiki masalah ini, dan gagal seperti Anda. Untuk mengatasinya, Anda harus meneruskan fungsi JavaScript sebagai panggilan balik. Fungsi asli perlu mengevaluasi fungsi panggilan balik untuk mengembalikan hasil. Sebenarnya ini adalah cara JavaScript, karena JavaScript tidak pernah menunggu. Memblokir thread JavaScript dapat menyebabkan ANR, ini sangat buruk.

Saya telah membuat proyek bernama XWebView yang dapat menjembatani antara asli dan JavaScript. Ia menawarkan API bergaya pengikatan untuk memanggil asli dari JavaScript dan sebaliknya. Ada aplikasi contoh.

person soflare    schedule 28.04.2015
comment
Bagaimana Anda meneruskan fungsi JavaScript sebagai panggilan balik ke WKWebView? Dari apa yang saya dapat melihat, hanya nilai serial (string, angka, boolean) yang diperbolehkan. - person Vidar S. Ramdal; 18.08.2016
comment
@VidarS.Ramdal, Untuk objek non-serializable yang tidak dapat diteruskan berdasarkan nilai, objek tersebut akan diteruskan dengan referensi. Referensi adalah objek kecil khusus (yang dapat diserialkan) yang mewakili objek asli yang tidak dapat diserialkan. Operasi yang diterapkan pada objek referensi akan diterjemahkan ke ekspresi javascript dan dievaluasi oleh XWebView. - person soflare; 22.08.2016
comment
Baiklah, saya rasa pertanyaan saya adalah bagaimana Anda mengoperasikan objek anonim itu. Bagaimana Anda bisa memanggil fungsi JavaScript anonim dari Swift? - person Vidar S. Ramdal; 22.08.2016
comment
Pertanyaan bagus. Objek apa pun yang diteruskan melalui referensi (baik anonim atau tidak) akan dipertahankan di bawah namespace objek plugin yang diinisiasi. Ini akan dirilis sementara referensinya adalah dirilis di sisi asli. - person soflare; 23.08.2016

Saya menemukan peretasan untuk melakukan komunikasi sinkron tetapi belum mencobanya: https://stackoverflow.com/a/49474323/2870783

Sunting: Pada dasarnya Anda dapat menggunakan JS prompt() untuk membawa muatan Anda dari sisi js ke sisi asli. Di WKWebView asli harus mencegat panggilan prompt dan memutuskan apakah itu panggilan normal atau panggilan jsbridge. Kemudian Anda dapat mengembalikan hasil Anda sebagai panggilan balik ke panggilan cepat. Karena panggilan prompt diimplementasikan sedemikian rupa sehingga menunggu input pengguna, komunikasi asli javascript Anda akan sinkron. Kelemahannya adalah Anda hanya dapat berkomunikasi melalui string.

person h3dkandi    schedule 18.04.2018
comment
Tampaknya baik-baik saja sekarang. - person Paul Floyd; 18.04.2018

Saya menghadapi masalah serupa, saya menyelesaikannya dengan menyimpan panggilan balik janji.

Js yang Anda muat di tampilan web Anda melalui 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});
   });
}

Lakukan Permintaan Js seperti

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

Terima permintaan di userContentController :didReceiveScriptMessage

Panggil evaluasiJavaScript dengan webClient.onMessageReceive(handle, error, respon_data).

person Ashwani Chandil    schedule 02.04.2019

Dimungkinkan untuk menunggu hasil evaluasiJavaScript secara serempak dengan melakukan polling pada metode AcceptInput RunLoop saat ini. Apa yang dilakukan adalah memungkinkan thread UI Anda merespons input sementara Anda menunggu hingga Javascript selesai.

Harap baca peringatan sebelum Anda menempelkannya secara membabi buta ke kode Anda

//
//  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
    }
}

Apa yang terjadi adalah, ketika Javascript dijalankan, thread memanggil RunLoop.current.acceptInput hingga menunggu salah. Hal ini memungkinkan UI aplikasi Anda menjadi responsif.

Beberapa peringatan:

  • Tombol, dan sebagainya, di UI Anda akan tetap merespons. Jika Anda tidak ingin seseorang menekan tombol saat Javascript Anda berjalan, Anda mungkin harus menonaktifkan interaksi dengan UI Anda. (Hal ini terutama terjadi jika Anda memanggil server lain dalam Javascript.)
  • Sifat multi-proses dari pemanggilan evaluasiJavaScript mungkin lebih lambat dari yang Anda harapkan. Jika Anda memanggil kode yang instan, segalanya mungkin masih melambat jika Anda melakukan panggilan berulang ke Javascript dalam satu putaran yang ketat.
  • Saya hanya mengujinya di thread UI Utama. Saya tidak tahu bagaimana kode ini akan bekerja di thread latar belakang. Jika ada masalah, selidiki menggunakan NSCondition.
  • Saya hanya mengujinya di macOS. Saya tidak tahu apakah ini berfungsi di iOS.
person Andrew Rondeau    schedule 19.07.2021