恒创科技

广告

hostinger

广告

TWT Chat在线客服系统接入教程:Flutter WebView与AppBridge通信方案

美国云服务器推荐

AppBridge方案的作用是把H5和APP之间的通信打通。本文结合TWT Chat在线客服系统 的实际接入方案,介绍Flutter WebView与AppBridge的通信实现方式,包括桥接注入、消息模型设计、JS与Flutter双向调用、权限处理及业务路由等内容。

TWT Chat优惠码:IDCSPY(专业版8折优惠,长期有效)

访问TWT Chat官网:点击直达

一、必须接入方案:AppBridge兼容层

1、技术架构

  • 框架:Flutter
  • 核心组件: webview_flutter 4.13.0+
  • 通信机制: 通过 JavaScript Channel 实现 H5 与 Flutter 双向通信
  • H5 调用方式: window.AppBridge.call(method, params, callback)
  • APP 处理: 通过 BridgeAction 类统一处理消息

2、核心实现代码

WebView控制器初始化

import ‘package:business_module/business_module.dart’;

/// 初始化webview控制器
void initWebViewController() async {
late final PlatformWebViewControllerCreationParams params;

// iOS WebKit 适配
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
params = WebKitWebViewControllerCreationParams(
allowsInlineMediaPlayback: true,
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
);
} else {
params = const PlatformWebViewControllerCreationParams();
}

// 创建 WebViewController
webViewController = WebViewController.fromPlatformCreationParams(
params,
onPermissionRequest: (request) => onPermissionRequest(request),
)..setJavaScriptMode(JavaScriptMode.unrestricted);

// Android 平台适配
if (webViewController.platform is AndroidWebViewController) {
AndroidWebViewController.enableDebugging(true);
(webViewController.platform as AndroidWebViewController)
.setOnShowFileSelector(_androidFilePicker);
(webViewController.platform as AndroidWebViewController)
.setMediaPlaybackRequiresUserGesture(true);
}

// 设置 UserAgent 和配置
final userAgent = await webViewController.getUserAgent();
webViewController
..setUserAgent(‘$userAgent $kUserAgentRemark’)
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onProgress: (progress) {
this.progress.value = progress;
},
onPageStarted: (url) async {
LOG(‘webview onPageStarted: $url’);
},
onPageFinished: (url) async {
setAppBarTitle();
// 关键:注入 AppBridge 兼容层
await _injectAppBridgeCompatibilityLayer();
},
onUrlChange: (change) {
LOG(‘webview onUrlChange: ${change.url}’);
},
onHttpError: (error) {},
onWebResourceError: (error) {},
onNavigationRequest: (request) {
return NavigationDecision.navigate;
},
),
)
// 注册 JavaScript Channel – 关键配置
..addJavaScriptChannel(
‘AppBridgeChannel’, // 通道名称,与注入脚本中的 window.AppBridgeChannel_native 对应
onMessageReceived: (message) {
// 使用 BridgeAction 处理消息
BridgeAction().handleJSMessage(
BridgeMessageBean.fromJsonString(message.message),
controller: webViewController,
);
},
);

// 加载 URL
final url = args?.url ?? ”;
if (url.startsWith(‘http’)) {
webViewController.loadRequest(Uri.parse(url));
} else {
webViewController.loadFlutterAsset(url);
}
}

3、AppBridge 兼容层注入(必须实现)

/// 注入兼容层脚本 – 必须实现
Future<void> _injectAppBridgeCompatibilityLayer() async {
const injectAppBridgeScript = r”’
(function() {
try {
console.log(‘[AppBridge] 开始注入’);

// 创建原生通道引用
if (!window.AppBridgeChannel_native) {
window.AppBridgeChannel_native = {
postMessage: function(msg) {
// 这个会由 Flutter 的 JavaScript Channel 处理
if (window.AppBridgeChannel && window.AppBridgeChannel.postMessage) {
window.AppBridgeChannel.postMessage(msg);
}
}
};
}

// 主桥对象
window.AppBridge = window.AppBridge || {};
window.AppBridge._callbacks = window.AppBridge._callbacks || {};

function genCallbackId() {
return ‘cb_’ + Date.now() + ‘_’ + Math.floor(Math.random() * 10000);
}

// call(method, data, callback) – H5 调用方式
window.AppBridge.call = function(method, data, callback) {
return new Promise(function(resolve, reject) {
if (!method || typeof method !== ‘string’) {
reject(‘[AppBridge] 参数错误:method 必须为字符串’);
return;
}

var callbackId = null;
if (typeof callback === ‘function’) {
callbackId = genCallbackId();
window.AppBridge._callbacks[callbackId] = callback;
}

var msg = {
method: method,
params: data || {},
callback: callbackId
};

try {
if (window.AppBridgeChannel_native && typeof window.AppBridgeChannel_native.postMessage === ‘function’) {
console.log(‘[AppBridge] 发送消息:’, method, data);
window.AppBridgeChannel_native.postMessage(JSON.stringify(msg));
resolve({status: ‘sent’, callbackId: callbackId});
} else {
console.warn(‘[AppBridge] 无可用通道’);
resolve({status: ‘no_bridge’, data: data});
}
} catch (e) {
console.error(‘[AppBridge] 调用异常:’, e);
reject(e);
}
});
};

// Flutter 回调 H5:invokeCallback(callbackId, result)
window.AppBridge.invokeCallback = function(callbackId, result) {
try {
var cb = window.AppBridge._callbacks[callbackId];
if (cb) {
try {
cb(result);
} catch (err) {
console.error(‘[AppBridge] 回调执行错误:’, err);
}
delete window.AppBridge._callbacks[callbackId];
} else {
console.warn(‘[AppBridge] 未找到回调函数:’, callbackId);
}
} catch (e) {
console.error(‘[AppBridge] invokeCallback 异常:’, e);
}
};

console.log(‘[AppBridge] 注入完成’);
} catch (err) {
console.error(‘[AppBridge] 注入失败:’, err);
}
})();
”’;

try {
await webViewController.runJavaScript(injectAppBridgeScript);
debugPrint(‘AppBridge 兼容层注入成功’);
} catch (e) {
debugPrint(‘注入 AppBridge 失败: $e’);
}
}

4、数据模型定义

/// 桥接消息数据模型
class BridgeMessageBean {
final String method;
final Map<String, dynamic>? params;
final String? callback;

BridgeMessageBean({
required this.method,
this.params,
this.callback,
});

factory BridgeMessageBean.fromJsonString(String jsonString) {
try {
final Map<String, dynamic> json = jsonDecode(jsonString);
return BridgeMessageBean(
method: json[‘method’] ?? ”,
params: json[‘params’] != null ? Map<String, dynamic>.from(json[‘params’]) : null,
callback: json[‘callback’],
);
} catch (e) {
throw FormatException(‘解析 BridgeMessageBean 失败: $e, 原始数据: $jsonString’);
}
}

@override
String toString() {
return ‘BridgeMessageBean{method: $method, params: $params, callback: $callback}’;
}
}

/// 桥接回调数据模型
class BridgeCallbackBean {
final int code;
final String msg;
final Map<String, dynamic>? data;

BridgeCallbackBean({
required this.code,
required this.msg,
this.data,
});

String toString() {
return jsonEncode({
‘code’: code,
‘msg’: msg,
‘data’: data,
});
}
}

5、消息处理类(基于实际业务逻辑)

import ‘package:business_module/business_module.dart’;

/// 桥接动作处理类
class BridgeAction {
late BridgeMessageBean message;
late WebViewController webViewController;

/// 处理JS消息
Future<void> handleJSMessage(
BridgeMessageBean message, {
required WebViewController controller,
}) async {
this.message = message;
webViewController = controller;

kLog(‘handleJSMessage – ${message.toString()}’);
final method = message.method;

// 根据 method 路由到不同的处理方法
switch (method) {
case ‘download’:
await _handleDownload(message.params);
break;
case ‘new_message’:
await _handleNewMessage();
break;
case ‘android_recording_permission’:
await _handleRecordingPermission();
break;
case ‘on_ring’:
await _handleRingControl(true);
break;
case ‘off_ring’:
await _handleRingControl(false);
break;
default:
kLog(‘未知的桥接方法: $method’);
await _handleUnknownMethod(method, message.params);
}
}

/// 处理下载请求
Future<void> _handleDownload(Map<String, dynamic>? params) async {
final fileUrl = params?[‘url’];
if (fileUrl == null || fileUrl.isEmpty) {
kLog(‘下载链接为空’);
return;
}

final type = params?[‘type’];
if (type == null || type.isEmpty) {
kLog(‘文件类型为空’);
return;
}

kLog(‘ 收到下载请求: $fileUrl, 类型: $type’);

try {
if (type == ‘image’) {
await ImageUtil.saveImageWithString(fileUrl);
} else if (type == ‘video’) {
await FileUtil.downloadFile(fileUrl, onReceiveProgress: (count, total) {
JMLoading.progress(msg: S.common.downloading, value: count / total);
final progress = ((count / total) * 100).toInt();
if (progress == 100) {
kShowToast(S.chat.frameChatMsgDownloadSuccess);
}
});
} else {
await FileUtil.downloadFile(fileUrl, onReceiveProgress: (count, total) {
JMLoading.progress(msg: S.common.downloading, value: count / total);
if (count == total) {
kShowToast(S.chat.frameChatMsgDownloadSuccess);
}
});
}
} catch (e) {
kLog(‘下载文件失败: $e’);
}
}

/// 处理新消息铃声
Future<void> _handleNewMessage() async {
kLog(‘新消息铃声’);
try {
await AudioPlayerUtil().togglePlay(R.files.alert);
} catch (e) {
kLog(‘播放铃声失败: $e’);
}
}

/// 处理录音权限请求
Future<void> _handleRecordingPermission() async {
kLog(‘获取录音权限’);
try {
final status = await _handleRecordingPermissionStatus();
await callbackJS(BridgeCallbackBean(
code: 1,
msg: ‘success’,
data: {‘permission’: status}
));
} catch (e) {
kLog(‘处理录音权限失败: $e’);
await callbackJS(BridgeCallbackBean(
code: 0,
msg: ‘error’,
data: {‘permission’: false}
));
}
}

/// 处理铃声控制
Future<void> _handleRingControl(bool enable) async {
kLog(‘${enable ? ‘开启’ : ‘关闭’}铃声’);
// 实现铃声控制逻辑
}

/// 处理未知方法
Future<void> _handleUnknownMethod(String method, Map<String, dynamic>? params) async {
kLog(‘未知桥接方法: $method, 参数: $params’);
// 可以返回错误信息给 H5
if (message.callback != null) {
await callbackJS(BridgeCallbackBean(
code: 0,
msg: ‘未知方法: $method’,
data: null,
));
}
}

/// 检查录音权限状态
Future<bool> _handleRecordingPermissionStatus() async {
// 检查当前权限状态
final hasAccess = await PermissionUtil.getPermissionStatus(PermissionType.microphone);
if (hasAccess == false) {
// 请求权限
await PermissionUtil.requestPermission(PermissionType.microphone);
// 再次检查权限状态
return await PermissionUtil.getPermissionStatus(PermissionType.microphone);
}
return true;
}

/// 回调JS的通用方法
Future<void> callbackJS(BridgeCallbackBean data) async {
final callbackId = message.callback ?? ”;
if (callbackId.isEmpty) {
kLog(‘callbackId 为空,无法回调’);
return;
}

final script = “window.AppBridge && window.AppBridge.invokeCallback(‘$callbackId’, ${data.toString()})”;
kLog(‘回调 JS: $script’);

try {
await webViewController.runJavaScript(script);
} catch (e) {
kLog(‘回调 JS 失败: $e’);
}
}
}

二、H5调用示例(基于实际使用)

// 获取录音权限 – 实际调用方式
window.AppBridge.call(‘android_recording_permission’, {}, (res) => {
if (res?.data?.permission) {
hasPermission.value = true;
console.log(‘录音权限已获取’);
} else {
console.log(‘录音权限被拒绝’);
}
});

// 下载文件
window.AppBridge.call(‘download’, {
url: ‘https://example.com/file.jpg’,
type: ‘image’
}, (res) => {
if (res.code === 1) {
console.log(‘下载成功’);
} else {
console.log(‘下载失败:’, res.msg);
}
});

// 新消息通知
window.AppBridge.call(‘new_message’, {}, (res) => {
console.log(‘铃声播放状态:’, res);
});

三、权限处理配置

/// 处理 WebView 权限请求
Future<void> onPermissionRequest(PermissionRequest request) async {
final permissions = request.types;
final results = <PermissionResourceType, PermissionDecision>{};

for (final permission in permissions) {
switch (permission) {
case PermissionResourceType.camera:
final granted = await _requestCameraPermission();
results[permission] = granted ? PermissionDecision.grant : PermissionDecision.deny;
break;
case PermissionResourceType.microphone:
final granted = await _requestMicrophonePermission();
results[permission] = granted ? PermissionDecision.grant : PermissionDecision.deny;
break;
default:
results[permission] = PermissionDecision.deny;
}
}

request.grant(results);
}

Future<bool> _requestCameraPermission() async {
final status = await Permission.camera.request();
return status.isGranted;
}

Future<bool> _requestMicrophonePermission() async {
final status = await Permission.microphone.request();
return status.isGranted;
}

四、调试和验证

1、调试配置

// 在开发阶段启用调试
AndroidWebViewController.enableDebugging(true);

// 查看关键日志
kLog(‘handleJSMessage – ${message.toString()}’);
kLog(‘回调 JS: $script’);

2、验证步骤

  • 检查页面加载完成后是否输出 [AppBridge] 注入完成;
  • 在 H5 控制台调用 window.AppBridge.call(),确认没有 undefined 报错;
  • 测试 android_recording_permission,确认 APP 能收到消息并返回权限状态;
  • 测试 download,确认文件下载逻辑正常;
  • 测试 new_message,确认消息铃声或提醒逻辑可以触发;
  • 检查 callbackId 是否能正确回调到 H5;
  • 分别在 Android 和 iOS 设备上验证权限弹窗和回调表现。

开发阶段可以开启 Android WebView 调试:

AndroidWebViewController.enableDebugging(true);

注意事项

  • 权限配置: 确保在 AndroidManifest.xml 和 Info.plist 中声明所需权限
  • webview 嵌入访客的链接为:”${host}/direct/${appid}?source=webview”,其中参数source可选,设置为 webview 可实现去掉访客应用的顶部左侧返回按钮

相关阅读:

TWT Chat智能客服聊天创建与管理指南

TWT Chat优惠码及使用分享:独家8折优惠 长期有效

(本文由美国主机侦探原创,转载请注明出处“美国主机侦探”和原文地址!)

主机侦探企业微信

微信扫码加好友进群

主机优惠码及时掌握

主机侦探QQ群

QQ群号:164393063

主机优惠发布与交流

温馨提示:

1、本站部分图片来源于互联网,如有侵权请联系删除。邮箱:2942802716#qq.com(#改为@)

2、本文评论没有专人回复,如果您有问题请到美国主机侦探论坛提问!

3、美国主机侦探免费为您提供美国主机购买咨询。

RakSmart美国服务器
下一篇
TWT Chat
已经没有了
返回顶部