背景


在公告模块中,开发者希望玩家在点击按钮时播放匹配的音效。为了开发者能够统一处理(比如音效整体开关,播放不同音效等),所以预期通过将交互事件以回调的方式告知开发者。

而 UI 中的按钮主要分为两种,一是 uGUI 的按钮(比如关闭);二是 WebView 中前端页面的按钮。

前者可由 SDK 注册 uGUI Button 点击事件,直接回调即可,比较简单。

后者则需要 SDK 与前端建立通信通道,当前端页面产生交互后,由前端先通知到 SDK,再由 SDK 回调用户。

这里主要介绍 Native 与 WebView 的通信方案。

开发需求


希望在 Native 和 Web 上,都能提供以下能力:

  • 调用对方,并可以获得返回值
  • 处理对方调用,并可以返回值

实现方案


主要基于 WebView Native 执行 JS 代码JS 向 Native 发送消息 的能力

(以下示例代码是基于 Unity PC 的 Vuplex WebView 插件实现的,不同平台的方式略有差别)

Native&JS.png

Native 与 JS 分别提供对外接口

  • 调用并获得返回值 public async Task<object> Call(string method, object data = null
  • 处理调用 public void Define(string method, Action action)(强类型语言可能需要重载,方便用户使用)

注入 Native WebView 实现

考虑到其他平台 WebView 可能不具备添加多个委托分发消息的能力,所以以统一桥接接口的方式,不同模块通过前缀或点表示法区分

固定两个接口通信(参数都下述 约定消息体 的 JSON 字符串)

  • 接受消息 window.tapBridge.onMessage = function (json) {}
  • 发送消息 window.tapBridge.postMessage = function (json) {}

由 Native 注入 WebView 实现

webView.ExecuteJavaScript($@"
	  if (!window.tapBridge) {{
	      window.tapBridge = {{}};
	  }}
	  if (!window.tapBridge.{POST_MESSAGE_FUNC}) {{
	      window.vuplex.addEventListener('message', function (event) {{
	          window.tapBridge.{ON_MESSAGE_FUNC}(event.data)
	      }});
	      window.tapBridge.{POST_MESSAGE_FUNC} = function(message) {{
	          window.vuplex.postMessage(message);
	      }}
	  }}");

约定消息体

public class NativeJSMessage {
    /// <summary>
    /// 调用方法名
    /// </summary>
    [JsonProperty("method")]
    public string Method { get; set; }

    /// <summary>
    /// 传递参数
    /// </summary>
    [JsonProperty("data")]
    public object Data { get; set; }

    /// <summary>
    /// 请求 id
    /// </summary>
    [JsonProperty("requestId")]
    public int? RequestId { get; set; }

    /// <summary>
    /// 应答 id
    /// </summary>
    [JsonProperty("responseId")]
    public int? ResponseId { get; set; }

    /// <summary>
    /// 是否是请求
    /// </summary>
    [JsonIgnore]
    public bool IsRequest => RequestId != null;
    /// <summary>
    /// 是否是应答
    /// </summary>
    [JsonIgnore]
    public bool IsResponse => ResponseId != null;
}

调用发送消息逻辑

public Task<object> Call(string method, object data = null) {
    NativeJSMessage call = NativeJSMessage.NewCall(++requestId, method, data);

    TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
    requestTasks.Add((int)call.RequestId, tcs);
    Invoke(call);

    return tcs.Task;
}

private void Invoke(NativeJSMessage message) {
    JsonSerializerSettings settings = new JsonSerializerSettings {
        NullValueHandling = NullValueHandling.Ignore
    };
    string json = JsonConvert.SerializeObject(message, settings);
    Debug.Log($"=> {json}");
    webView.PostMessage(json);
}

接收消息的路由逻辑

private async void MessageEmitted(object sender, EventArgs<string> args) {
    Debug.Log($"<= {args.Value}");
    NativeJSMessage message = JsonConvert.DeserializeObject<NativeJSMessage>(args.Value);
    if (message.IsResponse) {
        // 应答
        Debug.Log("Response");
        if (requestTasks.TryGetValue((int)message.ResponseId, out TaskCompletionSource<object> tcs)) {
            tcs.TrySetResult(message.Data);
        } else {
            Debug.LogError($"Miss response: {message.ResponseId}");
        }
    } else if (message.IsRequest) {
        // 请求
        Debug.Log("Request");
        if (defines.TryGetValue(message.Method, out Func<object, Task<object>> func)) {
            object result = await func(message.Data);
            await Invoke(NativeJSMessage.NewResponse((int)message.RequestId, result));
        }
    } else {
        Debug.Log($"Invalid message: {args.Value}");
    }
}

使用示例


初始化

// 初始化 WebView
await canvasWebViewPrefab.WaitUntilInitialized();

// 实例化 Bridge
bridge = new WebViewBridge(canvasWebViewPrefab.WebView);

// 实例化模块 Handler
commonBridgeHandler = new CommonBridgeHandler(bridge);
billboardBridgeHandler = new BillboardBridgeHandler(bridge);

请求

// 不需要返回值
_ = bridge.Call("submit");

// 需要返回值
int result = await billboardBridgeHandler.GetRedDotsCount();
Debug.Log($"The count of red dots: {result}");

处理请求

// 不需要返回值
bridge.Define("clickButton", data => {
    Debug.Log(data);
});

// 需要返回值
bridge.Define("getPlatform", () => SystemInfo.operatingSystem);