Native 与 JS 的双向通信
本文会介绍 Native 应用中 Native 层与 JS 层是如何通信的,以及从通信原理中找到一些需要注意的地方。
注意:Webview 控件在不同平台、不同时期有不同的叫法,为了方便,本文统称为 Webview。
前置知识:进程间通信
进程间通信 (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.Test
和
window.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);