🌙 Flutter webview JSBridge
前面实现了 原生端 android kotlin 和 iOS swift 的 JSBridge。 当原生端用 Flutter 重构后,需基于 Flutter 的
webview_flutter插件实现与 H5 的通信,保持与原有 JSBridge/Schema 协议的兼容性(H5 端无需修改)。
🌙 一、核心依赖
在 pubspec.yaml 中添加依赖:
dependencies:
flutter:
sdk: flutter
webview_flutter: ^4.4.0 # WebView 核心
crypto: ^3.0.3 # 用于签名校验(MD5/SHA 等)
uuid: ^3.0.7 # 生成唯一 ID(用于请求响应关联)
1
2
3
4
5
6
2
3
4
5
6
🌙 二、Flutter 实现 JSBridge
🌙 1. 消息模型定义(与协议对齐)
// 消息类型
enum MessageType { request, response, event }
// 基础消息模型
class BaseMessage {
final String? id;
final MessageType type;
final String method;
final Map<String, dynamic>? params;
final Map<String, dynamic>? data;
final int? code;
final String? error;
BaseMessage({
required this.type,
required this.method,
this.id,
this.params,
this.data,
this.code,
this.error,
});
// 转 JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'type': _typeToString(type),
'method': method,
'params': params,
'data': data,
'code': code,
'error': error,
};
}
// 从 JSON 解析
static BaseMessage fromJson(Map<String, dynamic> json) {
return BaseMessage(
id: json['id'],
type: _stringToType(json['type']),
method: json['method'],
params: json['params'] as Map<String, dynamic>?,
data: json['data'] as Map<String, dynamic>?,
code: json['code'] as int?,
error: json['error'],
);
}
static String _typeToString(MessageType type) {
switch (type) {
case MessageType.request:
return 'request';
case MessageType.response:
return 'response';
case MessageType.event:
return 'event';
}
}
static MessageType _stringToType(String type) {
switch (type) {
case 'request':
return MessageType.request;
case 'response':
return MessageType.response;
case 'event':
return MessageType.event;
default:
throw ArgumentError('未知消息类型:$type');
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
🌙 2. JSBridge 核心管理器
负责处理 H5 消息、注册原生方法、调用 H5 方法、管理回调等。
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:uuid/uuid.dart';
import 'package:webview_flutter/webview_flutter.dart';
class FlutterJsBridge {
final WebViewController _webViewController;
// 白名单:允许 H5 调用的方法
final Set<String> _methodWhitelist = {'getDeviceInfo', 'openCamera'};
// 回调映射(requestId -> 回调函数)
final Map<String, Function(Map<String, dynamic>?, String?)> _callbacks = {};
// H5 注册的方法(供原生调用)
final Map<String, Future<Map<String, dynamic>> Function(Map<String, dynamic>?)> _jsMethods = {};
FlutterJsBridge(this._webViewController) {
_init();
}
// 初始化:注册 JS 通道,让 H5 能发送消息到 Flutter
void _init() {
// H5 通过 window.flutterJsBridge.postMessage 发送消息
_webViewController.addJavaScriptChannel(
'flutterJsBridge',
onMessageReceived: (JavaScriptMessage message) {
_handleH5Message(message.message);
},
);
// 注册 H5 可调用的原生方法(示例)
_registerNativeMethods();
}
// 注册原生实现的方法
void _registerNativeMethods() {
// 获取设备信息
_jsMethods['getDeviceInfo'] = (params) async {
return {
'model': 'Flutter Device',
'os': 'Flutter OS',
'appVersion': '1.0.0',
};
};
// 打开相机
_jsMethods['openCamera'] = (params) async {
final type = params?['type'] ?? 'photo';
return {'result': '相机已打开(类型:$type)'};
};
}
// 处理 H5 发送的消息
void _handleH5Message(String messageStr) {
try {
final json = jsonDecode(messageStr) as Map<String, dynamic>;
final message = BaseMessage.fromJson(json);
switch (message.type) {
case MessageType.request:
_handleH5Request(message);
break;
case MessageType.event:
_handleH5Event(message); // 可选:处理 H5 发送的事件
break;
default:
_sendErrorResponse(message.id!, 400, '不支持的消息类型');
}
} catch (e) {
print('解析 H5 消息失败:$e');
}
}
// 处理 H5 的请求(request)
Future<void> _handleH5Request(BaseMessage message) async {
final id = message.id;
final method = message.method;
if (id == null || method.isEmpty) {
print('H5 请求缺少 id 或 method');
return;
}
// 校验白名单
if (!_methodWhitelist.contains(method)) {
_sendErrorResponse(id, 403, '方法 $method 无权限');
return;
}
// 执行方法并响应
try {
if (!_jsMethods.containsKey(method)) {
_sendErrorResponse(id, 404, '方法 $method 不存在');
return;
}
final result = await _jsMethods[method]!(message.params);
_sendSuccessResponse(id, result);
} catch (e) {
_sendErrorResponse(id, 500, e.toString());
}
}
// 处理 H5 发送的事件(event)
void _handleH5Event(BaseMessage message) {
print('收到 H5 事件:${message.method},参数:${message.params}');
// 可根据需要处理(如日志记录、原生事件响应)
}
// 发送成功响应给 H5
void _sendSuccessResponse(String id, Map<String, dynamic> data) {
final response = BaseMessage(
id: id,
type: MessageType.response,
method: '',
code: 200,
data: data,
);
_sendToH5(response);
}
// 发送错误响应给 H5
void _sendErrorResponse(String id, int code, String error) {
final response = BaseMessage(
id: id,
type: MessageType.response,
method: '',
code: code,
error: error,
);
_sendToH5(response);
}
// 向 H5 发送消息(调用 H5 的 window.JSBridge.receiveMessage)
void _sendToH5(BaseMessage message) {
final jsonStr = jsonEncode(message.toJson());
// 转义单引号,避免 JS 语法错误
final escapedStr = jsonStr.replaceAll("'", r"\'");
final jsCode = "window.JSBridge.receiveMessage('$escapedStr');";
_webViewController.runJavascript(jsCode);
}
// 原生主动调用 H5 方法
Future<Map<String, dynamic>?> callH5Method(String method, {Map<String, dynamic>? params}) async {
final completer = Completer<Map<String, dynamic>?>();
final id = const Uuid().v4(); // 生成唯一 ID
// 注册回调
_callbacks[id] = (data, error) {
if (error != null) {
completer.completeError(error);
} else {
completer.complete(data);
}
_callbacks.remove(id);
};
// 发送请求
final request = BaseMessage(
id: id,
type: MessageType.request,
method: method,
params: params,
);
_sendToH5(request);
// 超时处理(5秒)
Future.delayed(const Duration(seconds: 5), () {
if (!completer.isCompleted) {
completer.completeError('调用 H5 方法 $method 超时');
_callbacks.remove(id);
}
});
return completer.future;
}
// 原生发送事件给 H5
void sendEventToH5(String eventName, Map<String, dynamic> params) {
final event = BaseMessage(
type: MessageType.event,
method: eventName,
params: params,
);
_sendToH5(event);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
🌙 三、Flutter 实现 Schema 拦截
🌙 1. Schema 协议工具类(解析、签名验证)
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:uri/uri.dart';
class SchemaUtils {
static const String _protocol = 'myapp';
static const String _secret = 'myapp_schema_secret_2025'; // 与 H5 约定的密钥
// 解析 Schema URL 为动作路径和参数
static ({String? actionPath, Map<String, String> params}) parseSchema(String url) {
try {
final uri = Uri.parse(url);
if (uri.scheme != _protocol) return (actionPath: null, params: {});
// 动作路径:host + path(如 myapp://page/pay → actionPath 为 page/pay)
final actionPath = '${uri.host}${uri.path}';
// 参数解析
final params = uri.queryParameters;
return (actionPath: actionPath, params: params);
} catch (e) {
print('解析 Schema 失败:$e');
return (actionPath: null, params: {});
}
}
// 验证 Schema 签名
static bool verifySign(Map<String, String> params) {
final sign = params['sign'];
if (sign == null) return false;
// 移除签名参数后重新计算
final paramsWithoutSign = Map.from(params)..remove('sign');
// 按 key 排序参数
final sortedKeys = paramsWithoutSign.keys.toList()..sort();
final sortedParams = sortedKeys.map((k) => '$k=${paramsWithoutSign[k]}').join('');
// 计算预期签名
final bytes = utf8.encode('$sortedParams$_secret');
final digest = md5.convert(bytes);
final expectedSign = base64.encode(digest.bytes).substring(0, 16); // 与 H5 保持一致
return sign == expectedSign;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
🌙 2. Schema 拦截器(基于 WebView 导航代理)
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class SchemaInterceptor extends NavigationDelegate {
final FlutterJsBridge _jsBridge;
// Schema 白名单
final List<String> _schemaWhitelist = ['page/profile', 'page/pay', 'action/close'];
SchemaInterceptor(this._jsBridge);
Future<void> didStartProvisionalNavigation(NavigationRequest request) async {
final url = request.url;
// 拦截自定义 Schema
final parsed = SchemaUtils.parseSchema(url);
if (parsed.actionPath != null) {
await _handleSchemaAction(parsed.actionPath!, parsed.params);
// 取消默认导航
return;
}
// 非 Schema 链接允许跳转
return super.didStartProvisionalNavigation(request);
}
// 处理 Schema 动作
Future<void> _handleSchemaAction(String actionPath, Map<String, String> params) async {
// 1. 校验白名单
if (!_schemaWhitelist.contains(actionPath)) {
_sendSchemaResult(params, 403, '无权限执行:$actionPath');
return;
}
// 2. 校验签名
if (!SchemaUtils.verifySign(params)) {
_sendSchemaResult(params, 401, '签名校验失败');
return;
}
// 3. 执行对应动作
switch (actionPath) {
case 'page/profile':
final userId = params['userId'];
if (userId == null) {
_sendSchemaResult(params, 400, '缺少 userId');
return;
}
_openProfilePage(userId);
_sendSchemaResult(params, 200, data: {'message': '个人页已打开'});
break;
case 'page/pay':
final orderId = params['orderId'];
final amount = params['amount'];
if (orderId == null || amount == null) {
_sendSchemaResult(params, 400, '缺少 orderId 或 amount');
return;
}
await _startPayment(orderId, amount, params);
break;
case 'action/close':
// 关闭当前页面(假设在 Navigator 栈中)
Navigator.of(_jsBridge._webViewController.context as BuildContext).pop();
_sendSchemaResult(params, 200, data: {'message': '页面已关闭'});
break;
default:
_sendSchemaResult(params, 404, '动作路径不存在:$actionPath');
}
}
// 发送 Schema 处理结果给 H5(通过 JSBridge 事件)
void _sendSchemaResult(
Map<String, String> params,
int code, {
String error = '',
Map<String, dynamic> data = const {},
}) {
final callbackId = params['callbackId'];
final actionPath = params['actionPath'];
if (callbackId == null || actionPath == null) return;
final result = {
'callbackId': callbackId,
'code': code,
'error': error,
'data': data,
};
_jsBridge.sendEventToH5('schema:$actionPath', result);
}
// 打开原生个人页
void _openProfilePage(String userId) {
// 跳转到 Flutter 个人页
Navigator.of(_jsBridge._webViewController.context as BuildContext).push(
MaterialPageRoute(
builder: (context) => ProfilePage(userId: userId),
),
);
}
// 发起支付(模拟异步过程)
Future<void> _startPayment(String orderId, String amount, Map<String, String> params) async {
// 模拟支付耗时
await Future.delayed(const Duration(seconds: 3));
// 支付结果
final payResult = {
'orderId': orderId,
'success': true,
'tradeNo': 'PAY${DateTime.now().millisecondsSinceEpoch}',
};
_sendSchemaResult(params, 200, data: payResult);
}
}
// 示例:原生个人页
class ProfilePage extends StatelessWidget {
final String userId;
const ProfilePage({super.key, required this.userId});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('个人中心')),
body: Center(child: Text('用户 ID:$userId')),
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
🌙 四、Flutter 页面集成(WebView + JSBridge + Schema)
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class HybridPage extends StatefulWidget {
const HybridPage({super.key});
State<HybridPage> createState() => _HybridPageState();
}
class _HybridPageState extends State<HybridPage> {
late WebViewController _webViewController;
late FlutterJsBridge _jsBridge;
late SchemaInterceptor _schemaInterceptor;
void initState() {
super.initState();
// 初始化 WebView
_webViewController = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..loadRequest(Uri.parse('https://your-h5-domain.com')); // 加载 H5 页面
// 初始化 JSBridge 和 Schema 拦截器
_jsBridge = FlutterJsBridge(_webViewController);
_schemaInterceptor = SchemaInterceptor(_jsBridge);
_webViewController.setNavigationDelegate(_schemaInterceptor);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('混合开发页面')),
body: WebViewWidget(controller: _webViewController),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// 原生调用 H5 方法示例
FloatingActionButton(
onPressed: () async {
final result = await _jsBridge.callH5Method('showToast', params: {'text': '来自 Flutter 的通知'});
print('H5 方法返回:$result');
},
child: const Icon(Icons.send),
),
const SizedBox(height: 10),
// 原生发送事件给 H5 示例
FloatingActionButton(
onPressed: () {
_jsBridge.sendEventToH5('onNetworkChange', {
'isConnected': true,
'type': 'wifi',
});
},
child: const Icon(Icons.wifi),
),
],
),
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
🌙 五、兼容性说明
与 H5 通信兼容:
Flutter 实现的 JSBridge 完全遵循之前定义的协议(消息结构、错误码、回调机制),H5 端无需任何修改即可正常通信。Schema 协议兼容:
拦截逻辑、签名算法与 Android/iOS 原生实现一致,H5 调用schemaHandler.call(...)可无缝对接。功能覆盖:
支持 H5 调用 Flutter 方法、Flutter 调用 H5 方法、双向事件通知、Schema 页面跳转/支付等所有场景。
通过以上实现,Flutter 重构后的原生端可与原有 H5 端保持通信一致性,同时利用 Flutter 的跨平台特性简化维护成本。