การสื่อสารดั้งเดิมแบบซิงโครนัส JavaScript กับ WKWebView

การสื่อสารแบบซิงโครนัสระหว่าง JavaScript และโค้ดเนทิฟ Swift / Obj-C เป็นไปได้โดยใช้ WKWebView หรือไม่

นี่คือแนวทางที่ฉันได้ลองและล้มเหลว

วิธีที่ 1: การใช้ตัวจัดการสคริปต์

วิธีใหม่ในการรับข้อความ JS ของ WKWebView คือการใช้วิธีการมอบหมาย userContentController:didReceiveScriptMessage: ซึ่งถูกเรียกใช้จาก JS โดย window.webkit.messageHandlers.myMsgHandler.postMessage('What's the meaning of life, native code?') ปัญหาของวิธีนี้คือในระหว่างดำเนินการตามวิธีมอบหมายดั้งเดิม การดำเนินการ JS จะไม่ถูกบล็อก ดังนั้นเราจึงไม่สามารถส่งคืนค่าได้ โดยเรียกใช้ webView.evaluateJavaScript("something = 42", completionHandler: nil) ทันที

ตัวอย่าง (จาวาสคริปต์)

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

ตัวอย่าง (สวิฟท์)

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 ทันทีจึงไม่ทำงานที่นี่เช่นกัน

ตัวอย่าง (จาวาสคริปต์)

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

ตัวอย่าง (สวิฟท์)

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 ที่ว่างเปล่า

ตัวอย่าง (จาวาสคริปต์)

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 แบบเก่า

ตัวอย่าง (จาวาสคริปต์)

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

ตัวอย่าง (สวิฟท์)

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 ของคุณจะไม่รอเลย ซึ่งทำให้ UI ตอบสนองได้ดีขึ้น ด้วยวิธีนี้จะคล้ายกับ Node   -  person Mason G. Zhwiti    schedule 23.02.2015
comment
ปัญหาที่ฉันพบที่นี่คือดูเหมือนว่าจะเป็นไปไม่ได้ที่จะเรียกเข้าสู่ JavaScript และใช้ผลลัพธ์ของการเรียกนั้น * เพื่อตอบสนองต่อคำขอ UI / กรอบงาน เช่น การพิมพ์หรือการถ่ายเอกสาร ความพยายามของฉันในการซิงโครไนซ์ผ่านตัวจัดการการดำเนินการเสร็จสิ้นส่งผลให้เกิดการหยุดชะงัก   -  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 (และหากไม่มีโกลบอลอยู่ มันจะสร้างตัวแปรขึ้นมาก่อน จากนั้นจึงกำหนดค่าของมัน) ดังนั้นไม่ว่าเราจะสร้าง Decan something ไว้ล่วงหน้าหรือไม่ก็ตาม มันถูกสร้างขึ้นด้วยโค้ดเนทีฟใช่ไหม?   -  person paulvs    schedule 31.03.2015
comment
@paulvs คุณอาจจะพูดถูก ปกติฉันจะทำงานใน Browserify ดังนั้นฉันจึงคุ้นเคยกับการคิดว่าโค้ดระดับโมดูลทั้งหมดรวมอยู่ในฟังก์ชัน   -  person Steve Landey    schedule 31.03.2015
comment
แทนที่จะใช้ window.location หรือ iframe ฉันสามารถใช้ ajax ได้ไหม   -  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 สำหรับอ็อบเจ็กต์ที่ไม่สามารถทำให้เป็นอนุกรมซึ่งไม่สามารถส่งผ่านค่าได้ วัตถุเหล่านั้นจะถูกส่งผ่านโดยการอ้างอิง การอ้างอิงเป็นวัตถุขนาดเล็กพิเศษ (ทำให้เป็นอนุกรมได้) ซึ่งแสดงถึงวัตถุดั้งเดิมที่ไม่สามารถทำให้เป็นอนุกรมได้ การดำเนินการที่ใช้กับออบเจ็กต์อ้างอิงจะถูกแปลเป็นนิพจน์จาวาสคริปต์และประเมินโดย XWebView - person soflare; 22.08.2016
comment
ถ้าอย่างนั้น ฉันเดาว่าคำถามของฉันคือคุณทำงานกับวัตถุนิรนามนั้นอย่างไร คุณจะเรียกใช้ฟังก์ชัน JavaScript ที่ไม่ระบุชื่อจาก Swift ได้อย่างไร - person Vidar S. Ramdal; 22.08.2016
comment
คำถามที่ดี. ออบเจ็กต์ใดๆ ที่ส่งผ่านโดยการอ้างอิง (ไม่ว่าจะไม่ระบุชื่อหรือไม่ก็ตาม) จะถูก ยังคงอยู่ ใต้เนมสเปซของวัตถุปลั๊กอินที่เริ่มต้น จะมีการเผยแพร่ ในขณะที่การอ้างอิงคือ เผยแพร่ในฝั่งเนทีฟ - person soflare; 23.08.2016

ฉันพบแฮ็คสำหรับการสื่อสารแบบซิงโครนัส แต่ยังไม่ได้ลอง: https://stackoverflow.com/a/49474323/2870783< /ก>

แก้ไข: โดยพื้นฐานแล้วคุณสามารถใช้พรอมต์ JS() เพื่อบรรทุกเพย์โหลดของคุณจากฝั่ง js ไปยังฝั่งเนทีฟ ใน WKWebView ดั้งเดิมจะต้องสกัดกั้นการโทรพร้อมท์และตัดสินใจว่าเป็นการโทรปกติหรือว่าเป็นการโทร jsbridge จากนั้นคุณสามารถส่งคืนผลลัพธ์ของคุณเป็นการโทรกลับไปยังการโทรพร้อมท์ได้ เนื่องจากการเรียกพร้อมท์ถูกนำมาใช้ในลักษณะที่รอให้ผู้ใช้ป้อนข้อมูล การสื่อสารแบบเนทิฟของจาวาสคริปต์ของคุณจึงเป็นแบบซิงโครนัส ข้อเสียคือคุณสามารถสื่อสารได้เฉพาะสตริงรางน้ำเท่านั้น

person h3dkandi    schedule 18.04.2018
comment
ตอนนี้มันดูโอเคแล้ว - person Paul Floyd; 18.04.2018

ฉันกำลังเผชิญกับปัญหาที่คล้ายกัน ฉันแก้ไขได้โดยการจัดเก็บการโทรกลับตามสัญญา

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

เรียกประเมิน JavaScript ด้วย webClient.onMessageReceive(handle, error, response_data)

person Ashwani Chandil    schedule 02.04.2019

เป็นไปได้ที่จะรอผลลัพธ์ของประเมิน JavaScript พร้อมกันโดยการสำรวจเมธอด AcceptInput ของ RunLoop ปัจจุบัน สิ่งนี้จะช่วยให้เธรด UI ของคุณตอบสนองต่ออินพุตในขณะที่คุณรอให้ 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 จนกระทั่งการรอเป็นเท็จ สิ่งนี้ทำให้ UI ของแอปพลิเคชันของคุณสามารถตอบสนองได้

คำเตือนบางประการ:

  • ปุ่มต่างๆ บน UI ของคุณจะยังคงตอบสนอง หากคุณไม่ต้องการให้ใครกดปุ่มในขณะที่ Javascript ของคุณกำลังทำงานอยู่ คุณก็ควรปิดการใช้งานการโต้ตอบกับ UI ของคุณ (โดยเฉพาะอย่างยิ่งกรณีนี้หากคุณกำลังเรียกไปยังเซิร์ฟเวอร์อื่นใน Javascript)
  • ลักษณะการเรียกประเมิน JavaScript แบบหลายกระบวนการอาจช้ากว่าที่คุณคาดไว้ หากคุณกำลังเรียกโค้ดแบบทันที สิ่งต่างๆ อาจยังคงช้าลงหากคุณทำการเรียกซ้ำใน Javascript ในลักษณะวนซ้ำแน่น
  • ฉันได้ทดสอบสิ่งนี้บนเธรด UI หลักเท่านั้น ฉันไม่รู้ว่าโค้ดนี้ทำงานอย่างไรบนเธรดพื้นหลัง หากมีปัญหา ให้ตรวจสอบโดยใช้ NSCondition
  • ฉันทดสอบสิ่งนี้บน macOS เท่านั้น ฉันไม่รู้ว่าสิ่งนี้ใช้ได้กับ iOS หรือไม่
person Andrew Rondeau    schedule 19.07.2021