最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Native 与 JS 的双向通信

    正文概述 掘金(米纳尔)   2020-12-25   321

    本文会介绍 Native 应用中 Native 层与 JS 层是如何通信的,以及从通信原理中找到一些需要注意的地方。

    前置知识:进程间通信

    进程间通信(IPC,Inter-Process Communication)指的是两个不同的进程相互传递信息。在一个 Native 程序中,嵌入一个 Webview 控件以后,这个 Webview 控件相当于一个小型的浏览器,它会开启 UI 渲染线程、JS 虚拟机线程、网络线程等。所以 Native 与 JS 通信,其实是 Native 线程与 JS 虚拟机线程的通信。

    不管是进程间通信还是线程间通信,理论上可复用的数据很高,如 Node.js 进程之间甚至可以共享一个 Server 或者 Socket。然而, JS 与 Native 的数据结构不同,所以 Native 的数据结构并不能复用。Native 与 JS 的通信会使用 HTML5 结构化克隆算法来序列化传递的数据,也就是说传递的数据最终会被转换成字符串,所以不能被 JSON.stringify 或其他序列化方法转换的的数据结构就会丢失。

    Native 调用 JS

    首先来说一说 Native 如何调用 JS。其实,所有的 Webview 控件都会自带一个方法用来执行 JS,只是它们的格式有所区别,主要有以下两种格式:

    // 函数名和参数列表分开
    this.webView.InvokeScript("alert", "123");
    
    // 直接执行一段JS代码
    this.webView.EvaluateJavaScriptAsync("alert('123')");
    

    Native 调用 JS 是一件非常简单的事情,但是一般只有做自动化测试的时候才会这么做,因为 JS 能做的事情 Native 也能做,而且做得更好。

    JS 调用 Native

    JS 调用 Native 的方法在不同的平台都不一样,下面我们来分别讲解。

    Internet Explorer

    在 HTML 标准中,微软贡献了一个名为window.external的全局变量。这个变量用来提供添加浏览器的搜索引擎、添加收藏夹、设置主页等外部功能,自然也可以作为 Native 与 JS 通信的桥梁。

    在一个 IE 应用中,Webview 控件有一个ObjectForScripting属性,这个属性可以被 JS 端的window.external访问到。比如有如下 Native 代码:

    public partial class MainWindow: Window {
      public MainWindow() {
        InitializeComponent();
        this.webBrowser.ObjectForScripting = new WebviewClass();
      }
    }
    public class WebviewClass {
      public void Test(String message) {
        MessageBox.Show(message, "client code");
      }
    }
    

    ObjectForScripting属性被指定成WebviewClass这个类的一个实例,而ObjectForScripting又等于window.external,那么这个实例中的Test函数就可以通过window.external.Test访问到。

    Microsoft Edge UWP

    在 UWP 版本 Edge 浏览器中,微软依然是通过window.external这个全局变量来访问 Native 代码,然而它和 IE 不同的是,它不是直接调用 Native 函数,而是通过window.external.notify函数给 Native 层传递一串字符串,Native 层有一个叫ScriptNotify的事件专门用来接收这个字符串。收到字符串以后,再从中提取一些特征信息(调用的函数名、参数等),并且执行响应的逻辑。

    由于频繁手动调用 notify 麻烦且易错,所以一般会在 JS 层指定一个全局变量或全局函数来封装 Native 调用。一个典型的例子如下:

    Native 代码:

    // 注册一个全局变量callNative
    const string JavaScriptFunction = @"
    window.callNative = {
      writeLine(msg) {
        window.external.notify(msg);
      }
    }";
    
    this.Control.InvokeScript("eval", new[] { JavaScriptFunction });
    
    // 绑定ScriptNotify事件
    void OnWebViewScriptNotify(object sender, NotifyEventArgs e)
    {
      Console.WriteLine(e.Value);
    }
    this.Control.ScriptNotify += OnWebViewScriptNotify;
    

    HTML 代码:

    <button type="button" onclick="callNative.writeLine('123');">
      Invoke C# Code
    </button>
    

    Native 端先让 JS 层在 window 对象上挂载一个叫callNative的全局变量,由于 Edge 调用 JS 是采用函数名和函数参数分开的写法,所以这里需要用eval函数来执行 JS 代码。同时,Native 端也需要挂载ScriptNotify事件,这里是直接调用Console.WriteLine输出到控制台。最后,JS 端调用callNative.writeLine函数,这个函数会调用window.external.notify函数,将msg传递给ScriptNotify事件,进而触发Console.WriteLine函数。

    Microsoft Edge Webview2 (Chromium)

    最近微软发布了 Webview2 控件,它是基于 Chromium 的浏览器。Webview2 和传统 Webview 在 Native 与 JS 双向通信上大同小异,主要区别是 Webview2 用window.chrome.webview.postMessage替代了window.external.notify,用WebMessageReceived替代了ScriptNotify,调用 JS 代码也可以直接执行 JS 而不需要用eval函数包裹。

    将上面的案例稍作修改就可以用于 Webview2:

    Native 代码:

    // 注册一个全局变量callNative,这段代码也可以写在JS端
    const string JavaScriptFunction = @"
    window.callNative = {
      writeLine(msg) {
        window.chrome.webview.postMessage(msg);
      }
    }";
    await this.webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(JavaScriptFunction);
    
    
    // 绑定ScriptNotify事件
    void onMessage(object sender, CoreWebView2WebMessageReceivedEventArgs args)
    {
      String msg = args.TryGetWebMessageAsString();
      Console.WriteLine(msg);
    }
    this.webView.CoreWebView2.WebMessageReceived += onMessage;
    

    HTML 代码:

    <button type="button" onclick="callNative.writeLine('123');">
      Invoke C# Code
    </button>
    

    Android

    Android 端的调用方式比较类似于 Internet Explorer,也是将 Native 的函数封装到一个对象里,然后将这个对象写入一个特殊的属性,作为 Native 与 JS 直接的桥梁。比 IE 灵活的一点是,Android 可以通过addJavascriptInterface函数注入多个对象,而不是只能通过window.external访问。

    一个典型的例子:

    Native 代码:

    private final class JSInterface{
      @SuppressLint("JavascriptInterface")
      @JavascriptInterface
      public void Test(String userInfo){
        Toast.makeText(MainActivity.this, userInfo, Toast.LENGTH_LONG).show();
      }
    }
    
    @SuppressLint("JavascriptInterface")
    @Override
    protected void onCreate() {
      wv.addJavascriptInterface(new JSInterface(), "callNative1");
      wv.addJavascriptInterface(new JSInterface(), "callNative2");
    }
    

    HTML 代码:

    <button type="button" onclick="callNative1.Test('123');">Invoke C# Code</button>
    

    上面的例子中,我们先写了一个叫JSInterface的类,里面有一些 Native 函数,然后在onCreate生命周期中调用addJavascriptInterface函数,第一个参数是需要传递给 JS 的对象,第二个参数是全局变量的名字。注入完毕后,就可以在 JS 端调用window.callNative1.Testwindow.callNative2.Test函数了。

    iOS

    iOS 端采用了类似 Internet Explorer 的全局变量注入和类似 Webview2 的postMessage通信注入两种结合的方式。

    iOS 端需要调用AddScriptMessageHandler函数来给 JS 端传递一个对象,第一个参数的要传递的对象,第二个参数是入口名称。和 IE 不同,iOS 端传入的对象中并不直接包含业务代码,而是一个消息接收对象,该对象必须包含一个叫DidReceiveScriptMessage的方法用来接收 JS 传来的消息:

    // 定义消息接收类MessageHandler
    public class MessageHandler : WkWebViewRenderer, IWKScriptMessageHandler
    {
      public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
      {
        System.Console.WriteLine(message.Body.ToString());
      }
    }
    public class HybridWebViewRenderer : WkWebViewRenderer, IWKScriptMessageHandler
    {
      public HybridWebViewRenderer(WKWebViewConfiguration config) : base(config)
      {
        var userController = config.UserContentController;
        // 将MessageHandler的实例注入到JS中
        userController.AddScriptMessageHandler(new MessageHandler(), "入口名称");
      }
    }
    

    注入成功后,就可以在 JS 端通过window.webkit.messageHandlers[入口名称].postMessage给 Native 发送消息了。

    一个典型的例子:

    Native 代码:

    // 定义消息接收类MessageHandler
    public class MessageHandler : WkWebViewRenderer, IWKScriptMessageHandler
    {
      public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
      {
        System.Console.WriteLine(message.Body.ToString());
      }
    }
    
    public class HybridWebViewRenderer : WkWebViewRenderer, IWKScriptMessageHandler
    {
      public HybridWebViewRenderer(WKWebViewConfiguration config) : base(config)
      {
        var userController = config.UserContentController;
        // 将MessageHandler的实例注入到JS中
        userController.AddScriptMessageHandler(new MessageHandler(), "invokeAction");
    
        // 注册一个全局变量callNative,这段代码也可以写在JS端
        const string JavaScriptFunction = @"
    window.callNative = {
      writeLine(msg) {
        window.webkit.messageHandlers.invokeAction.postMessage(msg);
      }
    }";
        var script = new WKUserScript(new NSString(JavaScriptFunction), WKUserScriptInjectionTime.AtDocumentEnd, false);
        userController.AddUserScript(script);
      }
    }
    

    HTML 代码:

    <button type="button" onclick="callNative.writeLine('123');">
      Invoke C# Code
    </button>
    

    上面的例子中,Native 端先通过AddScriptMessageHandler将类MessageHandler的实例作为入口invokeAction注入到 JS 端,然后 JS 端再调用window.webkit.messageHandlers.invokeAction.postMessage与 Native 通信。

    注意事项

    数据丢失

    通过进程间通信的原理和上面的例子,我们发现 Native 和 JS 通信时数据最终会变成字符串的格式。虽然可以通过 JSON5 来传递更多的信息,或者使用二进制流来传递文件,但是像函数、Date 等复杂对象依然不能被正确转换,因此不能传递复杂的数据。

    通信开销

    同上面的场景,通信前后需要对数据进行序列化。并且由于数据信息的缺失,拿到数据后我们可能还要对数据进行处理。如果频繁的进行跨端通信,会对性能产生很大的影响。

    数据截断

    跨端通信对于数据的大小是有限制的,在移动端尤为明显。如果将一个非常大的数据进行跨端传输,可能会造成内存占用大,导致被操作系统杀死。所以如果要传递大数据,可以借鉴 HTTP 通信中的报文机制,进行分段传输。


    起源地下载网 » Native 与 JS 的双向通信

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元