跳至主要内容

APP 內嵌訪客端 WebView

當你將訪客端聊天頁面嵌入 App 的 WebView 時,可透過 AppBridge 實現 H5 與原生層的雙向通訊。

訪客端連結格式:${host}/direct/${appid}?source=webview

source=webview 參數可隱藏訪客端左上角的返回按鈕。

技術架構

  • 框架: Flutter + webview_flutter 4.13.0+
  • 通訊機制: JavaScript Channel(通道名稱 AppBridgeChannel
  • H5 呼叫方式: window.AppBridge.call(method, params, callback)
  • APP 處理: 透過 BridgeAction 類統一路由訊息

必須實作:注入 AppBridge 相容層

每次頁面載入完成後(onPageFinished),都必須注入相容層腳本,否則 window.AppBridge 無法使用。

WebView 初始化(關鍵設定)

import 'package:business_module/business_module.dart';

void initWebViewController() async {
// ... 平台相容設定略,請參考完整文件

webViewController
..addJavaScriptChannel(
'AppBridgeChannel',
onMessageReceived: (message) {
BridgeAction().handleJSMessage(
BridgeMessageBean.fromJsonString(message.message),
controller: webViewController,
);
},
)
..setNavigationDelegate(NavigationDelegate(
onPageFinished: (url) async {
await _injectAppBridgeCompatibilityLayer();
},
));
}

相容層注入腳本

Future<void> _injectAppBridgeCompatibilityLayer() async {
const script = r'''
(function() {
window.AppBridgeChannel_native = {
postMessage: function(msg) {
if (window.AppBridgeChannel?.postMessage) {
window.AppBridgeChannel.postMessage(msg);
}
}
};

window.AppBridge = window.AppBridge || {};
window.AppBridge._callbacks = window.AppBridge._callbacks || {};

window.AppBridge.call = function(method, data, callback) {
return new Promise(function(resolve, reject) {
var callbackId = null;
if (typeof callback === 'function') {
callbackId = 'cb_' + Date.now() + '_' + Math.floor(Math.random() * 10000);
window.AppBridge._callbacks[callbackId] = callback;
}
var msg = { method: method, params: data || {}, callback: callbackId };
window.AppBridgeChannel_native.postMessage(JSON.stringify(msg));
resolve({ status: 'sent', callbackId: callbackId });
});
};

window.AppBridge.invokeCallback = function(callbackId, result) {
var cb = window.AppBridge._callbacks[callbackId];
if (cb) { cb(result); delete window.AppBridge._callbacks[callbackId]; }
};
})();
''';
await webViewController.runJavaScript(script);
}

資料模型

class BridgeMessageBean {
final String method;
final Map<String, dynamic>? params;
final String? callback;

factory BridgeMessageBean.fromJsonString(String jsonString) {
final json = jsonDecode(jsonString);
return BridgeMessageBean(
method: json['method'] ?? '',
params: json['params'] != null ? Map<String, dynamic>.from(json['params']) : null,
callback: json['callback'],
);
}
}

class BridgeCallbackBean {
final int code;
final String msg;
final Map<String, dynamic>? data;

String toString() => jsonEncode({'code': code, 'msg': msg, 'data': data});
}

支援的橋接方法

download — 下載檔案

window.AppBridge.call('download', { url: 'https://...', type: 'image' }, (res) => {
console.log(res.code === 1 ? '下載成功' : res.msg);
});
參數類型說明
urlstring檔案地址
typestringimage / video / 其他

new_message — 播放新訊息提示音

window.AppBridge.call('new_message', {});

android_recording_permission — 取得錄音權限

window.AppBridge.call('android_recording_permission', {}, (res) => {
if (res?.data?.permission) {
console.log('已取得錄音權限');
}
});

回呼資料:{ code: 1, msg: 'success', data: { permission: true/false } }

on_ring / off_ring — 鈴聲控制

window.AppBridge.call('on_ring', {}); // 開啟鈴聲
window.AppBridge.call('off_ring', {}); // 關閉鈴聲

APP 端訊息處理

class BridgeAction {
late BridgeMessageBean message;
late WebViewController webViewController;

Future<void> handleJSMessage(BridgeMessageBean message, {required WebViewController controller}) async {
this.message = message;
webViewController = controller;

switch (message.method) {
case 'download': await _handleDownload(message.params); break;
case 'new_message': await AudioPlayerUtil().togglePlay(R.files.alert); break;
case 'android_recording_permission': await _handleRecordingPermission(); break;
case 'on_ring': await _handleRingControl(true); break;
case 'off_ring': await _handleRingControl(false); break;
}
}

Future<void> callbackJS(BridgeCallbackBean data) async {
final callbackId = message.callback ?? '';
if (callbackId.isEmpty) return;
await webViewController.runJavaScript(
"window.AppBridge?.invokeCallback('$callbackId', ${data.toString()})"
);
}
}

權限設定

在使用錄音、相機等功能前,需先於原生層宣告對應權限:

  • Android:AndroidManifest.xml 中加入 RECORD_AUDIOCAMERA 權限
  • iOS:Info.plist 中加入麥克風與相機的使用說明

除錯

開發階段可啟用 WebView 除錯:

AndroidWebViewController.enableDebugging(true);

若保留 console.log,注入成功後控制台應輸出 [AppBridge] 注入完成