在公告模块中,开发者希望玩家在点击按钮时播放匹配的音效。为了开发者能够统一处理(比如音效整体开关,播放不同音效等),所以预期通过将交互事件以回调的方式告知开发者。
而 UI 中的按钮主要分为两种,一是 uGUI 的按钮(比如关闭);二是 WebView 中前端页面的按钮。
前者可由 SDK 注册 uGUI Button 点击事件,直接回调即可,比较简单。
后者则需要 SDK 与前端建立通信通道,当前端页面产生交互后,由前端先通知到 SDK,再由 SDK 回调用户。
这里主要介绍 Native 与 WebView 的通信方案。
希望在 Native 和 Web 上,都能提供以下能力:
- 调用对方,并可以获得返回值
- 处理对方调用,并可以返回值
主要基于 WebView Native 执行 JS 代码
与 JS 向 Native 发送消息
的能力
(以下示例代码是基于 Unity PC 的 Vuplex WebView 插件实现的,不同平台的方式略有差别)
- 调用并获得返回值
public async Task<object> Call(string method, object data = null
- 处理调用
public void Define(string method, Action action)
(强类型语言可能需要重载,方便用户使用)
考虑到其他平台 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);