@beppla/tapas-ui 1.2.9 → 1.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commonjs/WebViewBridge/README.md +564 -0
- package/commonjs/WebViewBridge/WebViewBridge.js +827 -0
- package/commonjs/WebViewBridge/WebViewBridge.js.map +1 -0
- package/commonjs/WebViewBridge/index.js +32 -0
- package/commonjs/WebViewBridge/index.js.map +1 -0
- package/commonjs/WebViewBridge/useWebViewBridge.js +89 -0
- package/commonjs/WebViewBridge/useWebViewBridge.js.map +1 -0
- package/commonjs/index.js +31 -0
- package/commonjs/index.js.map +1 -1
- package/module/WebViewBridge/README.md +564 -0
- package/module/WebViewBridge/WebViewBridge.js +819 -0
- package/module/WebViewBridge/WebViewBridge.js.map +1 -0
- package/module/WebViewBridge/index.js +5 -0
- package/module/WebViewBridge/index.js.map +1 -0
- package/module/WebViewBridge/useWebViewBridge.js +83 -0
- package/module/WebViewBridge/useWebViewBridge.js.map +1 -0
- package/module/index.js +2 -0
- package/module/index.js.map +1 -1
- package/package.json +1 -1
- package/typescript/WebViewBridge/WebViewBridge.d.ts +141 -0
- package/typescript/WebViewBridge/WebViewBridge.d.ts.map +1 -0
- package/typescript/WebViewBridge/index.d.ts +3 -0
- package/typescript/WebViewBridge/index.d.ts.map +1 -0
- package/typescript/WebViewBridge/useWebViewBridge.d.ts +55 -0
- package/typescript/WebViewBridge/useWebViewBridge.d.ts.map +1 -0
- package/typescript/index.d.ts +2 -0
- package/typescript/index.d.ts.map +1 -1
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.handleNativeWebViewMessage = exports.default = exports.createWebViewInjectedScript = exports.WebViewBridge = void 0;
|
|
7
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
8
|
+
var _reactNative = require("react-native");
|
|
9
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
10
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
11
|
+
/**
|
|
12
|
+
* WebViewBridge - WebView/iframe 通信桥接组件
|
|
13
|
+
*
|
|
14
|
+
* 用于 RN Web 模块与 tapas-sys 底座之间的通信
|
|
15
|
+
* - Android/iOS: 使用 WebView postMessage/onMessage
|
|
16
|
+
* - Web: 使用 iframe window.postMessage/onMessage
|
|
17
|
+
*
|
|
18
|
+
* 自动检测运行环境并适配相应的通信方式
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* tapas-sys 消息格式
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 检测当前运行环境
|
|
27
|
+
*/
|
|
28
|
+
const detectEnvironment = () => {
|
|
29
|
+
if (_reactNative.Platform.OS === 'web') {
|
|
30
|
+
// Web 环境:检查是否在 iframe 中
|
|
31
|
+
try {
|
|
32
|
+
if (typeof window !== 'undefined' && window.self !== window.top) {
|
|
33
|
+
return 'iframe';
|
|
34
|
+
}
|
|
35
|
+
// 也可能是在 WebView 中(通过 userAgent 判断)
|
|
36
|
+
const userAgent = window.navigator?.userAgent || '';
|
|
37
|
+
if (userAgent.includes('wv') || userAgent.includes('WebView')) {
|
|
38
|
+
return 'webview';
|
|
39
|
+
}
|
|
40
|
+
return 'iframe'; // 默认 Web 环境视为 iframe
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return 'iframe';
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
// Android/iOS 环境:使用 WebView
|
|
46
|
+
return 'webview';
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* WebViewBridge 组件
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* Base64 编码/解码工具函数
|
|
55
|
+
* 符合 tapas-sys 的解析格式
|
|
56
|
+
*/
|
|
57
|
+
const encodeBase64 = str => {
|
|
58
|
+
try {
|
|
59
|
+
// 浏览器环境:使用 btoa
|
|
60
|
+
if (typeof window !== 'undefined') {
|
|
61
|
+
// 处理 Unicode 字符
|
|
62
|
+
return window.btoa(unescape(encodeURIComponent(str)));
|
|
63
|
+
}
|
|
64
|
+
// Node.js 环境:使用 Buffer
|
|
65
|
+
if (typeof Buffer !== 'undefined') {
|
|
66
|
+
return Buffer.from(str, 'utf-8').toString('base64');
|
|
67
|
+
}
|
|
68
|
+
// 降级方案:尝试全局 btoa
|
|
69
|
+
if (typeof btoa !== 'undefined') {
|
|
70
|
+
return btoa(unescape(encodeURIComponent(str)));
|
|
71
|
+
}
|
|
72
|
+
throw new Error('Base64 编码不可用');
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.warn('WebViewBridge: Base64 编码失败,使用原始字符串', e);
|
|
75
|
+
return str;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const decodeBase64 = str => {
|
|
79
|
+
try {
|
|
80
|
+
// 浏览器环境:使用 atob
|
|
81
|
+
if (typeof window !== 'undefined') {
|
|
82
|
+
// 处理 Unicode 字符
|
|
83
|
+
return decodeURIComponent(escape(window.atob(str)));
|
|
84
|
+
}
|
|
85
|
+
// Node.js 环境:使用 Buffer
|
|
86
|
+
if (typeof Buffer !== 'undefined') {
|
|
87
|
+
return Buffer.from(str, 'base64').toString('utf-8');
|
|
88
|
+
}
|
|
89
|
+
// 降级方案:尝试全局 atob
|
|
90
|
+
if (typeof atob !== 'undefined') {
|
|
91
|
+
return decodeURIComponent(escape(atob(str)));
|
|
92
|
+
}
|
|
93
|
+
throw new Error('Base64 解码不可用');
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.warn('WebViewBridge: Base64 解码失败,返回原始字符串', e);
|
|
96
|
+
return str;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 日志工具函数
|
|
102
|
+
*/
|
|
103
|
+
const createLogger = (debug, logLevel) => {
|
|
104
|
+
const levels = ['none', 'error', 'warn', 'info', 'debug'];
|
|
105
|
+
const currentLevelIndex = levels.indexOf(logLevel);
|
|
106
|
+
const shouldLog = level => {
|
|
107
|
+
const levelIndex = levels.indexOf(level);
|
|
108
|
+
return levelIndex >= 0 && levelIndex <= currentLevelIndex;
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
error: (...args) => {
|
|
112
|
+
if (shouldLog('error')) {
|
|
113
|
+
console.error('[WebViewBridge]', ...args);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
warn: (...args) => {
|
|
117
|
+
if (shouldLog('warn')) {
|
|
118
|
+
console.warn('[WebViewBridge]', ...args);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
info: (...args) => {
|
|
122
|
+
if (shouldLog('info')) {
|
|
123
|
+
console.log('[WebViewBridge]', ...args);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
debug: (...args) => {
|
|
127
|
+
if (debug && shouldLog('debug')) {
|
|
128
|
+
console.log('[WebViewBridge DEBUG]', ...args);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 验证消息来源(origin)
|
|
136
|
+
*/
|
|
137
|
+
const validateOrigin = (origin, allowedOrigins, strictOriginCheck, logger) => {
|
|
138
|
+
// 如果没有设置 allowedOrigins,不进行验证(开发环境)
|
|
139
|
+
if (!allowedOrigins || allowedOrigins.length === 0) {
|
|
140
|
+
logger.debug('Origin 验证已禁用(allowedOrigins 为空)');
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 如果没有 origin(某些环境可能没有),根据 strictOriginCheck 决定
|
|
145
|
+
if (!origin) {
|
|
146
|
+
if (strictOriginCheck) {
|
|
147
|
+
logger.warn('消息没有 origin 信息,且 strictOriginCheck=true,拒绝消息');
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
logger.warn('消息没有 origin 信息,但 strictOriginCheck=false,允许消息');
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 检查 origin 是否在白名单中
|
|
155
|
+
const isAllowed = allowedOrigins.some(allowed => {
|
|
156
|
+
// 支持通配符匹配(如 'https://*.example.com')
|
|
157
|
+
if (allowed.includes('*')) {
|
|
158
|
+
const pattern = allowed.replace(/\*/g, '.*');
|
|
159
|
+
const regex = new RegExp(`^${pattern}$`);
|
|
160
|
+
return regex.test(origin);
|
|
161
|
+
}
|
|
162
|
+
return origin === allowed;
|
|
163
|
+
});
|
|
164
|
+
if (!isAllowed) {
|
|
165
|
+
if (strictOriginCheck) {
|
|
166
|
+
logger.error(`消息来源 ${origin} 不在白名单中,拒绝消息。白名单:`, allowedOrigins);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
logger.warn(`消息来源 ${origin} 不在白名单中,但 strictOriginCheck=false,允许消息。白名单:`, allowedOrigins);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
logger.debug(`消息来源 ${origin} 验证通过`);
|
|
173
|
+
return true;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 验证消息的 from 字段
|
|
178
|
+
*/
|
|
179
|
+
const validateFrom = (messageFrom, allowedFrom, logger) => {
|
|
180
|
+
if (!allowedFrom || allowedFrom.length === 0) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
if (!messageFrom) {
|
|
184
|
+
logger.warn('消息没有 from 字段,但设置了 allowedFrom,拒绝消息');
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
const isAllowed = allowedFrom.includes(messageFrom);
|
|
188
|
+
if (!isAllowed) {
|
|
189
|
+
logger.warn(`消息 from 字段 ${messageFrom} 不在白名单中,拒绝消息。白名单:`, allowedFrom);
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
logger.debug(`消息 from 字段 ${messageFrom} 验证通过`);
|
|
193
|
+
return true;
|
|
194
|
+
};
|
|
195
|
+
const WebViewBridge = exports.WebViewBridge = /*#__PURE__*/(0, _react.forwardRef)(({
|
|
196
|
+
from,
|
|
197
|
+
onMessage,
|
|
198
|
+
onError,
|
|
199
|
+
initPayload,
|
|
200
|
+
autoInit = true,
|
|
201
|
+
initMessageType = 'init',
|
|
202
|
+
enableBase64 = true,
|
|
203
|
+
showPlaceholder = true,
|
|
204
|
+
style,
|
|
205
|
+
testID,
|
|
206
|
+
allowedOrigins,
|
|
207
|
+
strictOriginCheck = false,
|
|
208
|
+
allowedFrom,
|
|
209
|
+
messageFilter,
|
|
210
|
+
debug = false,
|
|
211
|
+
logLevel = 'warn'
|
|
212
|
+
}, ref) => {
|
|
213
|
+
const environmentRef = (0, _react.useRef)(detectEnvironment());
|
|
214
|
+
const messageIdCounterRef = (0, _react.useRef)(0);
|
|
215
|
+
const initSentRef = (0, _react.useRef)(false);
|
|
216
|
+
const messageHandlersRef = (0, _react.useRef)(new Map());
|
|
217
|
+
const loggerRef = (0, _react.useRef)(createLogger(debug, logLevel));
|
|
218
|
+
const cleanupFunctionsRef = (0, _react.useRef)([]);
|
|
219
|
+
|
|
220
|
+
// 更新 logger 当 debug 或 logLevel 改变时
|
|
221
|
+
(0, _react.useEffect)(() => {
|
|
222
|
+
loggerRef.current = createLogger(debug, logLevel);
|
|
223
|
+
}, [debug, logLevel]);
|
|
224
|
+
|
|
225
|
+
// 生成唯一消息 ID
|
|
226
|
+
const generateMessageId = (0, _react.useCallback)(() => {
|
|
227
|
+
messageIdCounterRef.current += 1;
|
|
228
|
+
return `msg_${Date.now()}_${messageIdCounterRef.current}`;
|
|
229
|
+
}, []);
|
|
230
|
+
|
|
231
|
+
// 将 WebViewMessage 转换为 tapas-sys 格式
|
|
232
|
+
const convertToTapasSysFormat = (0, _react.useCallback)(message => {
|
|
233
|
+
const tapasMessage = {
|
|
234
|
+
operate: message.type,
|
|
235
|
+
from: message.from
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// 处理 payload
|
|
239
|
+
if (message.payload !== undefined && message.payload !== null) {
|
|
240
|
+
// 如果 payload 是对象且包含 action 字段,提取 action
|
|
241
|
+
if (typeof message.payload === 'object' && !Array.isArray(message.payload) && 'action' in message.payload) {
|
|
242
|
+
tapasMessage.action = message.payload.action;
|
|
243
|
+
// data 包含 payload 的其他字段(排除 action)
|
|
244
|
+
const {
|
|
245
|
+
action,
|
|
246
|
+
...rest
|
|
247
|
+
} = message.payload;
|
|
248
|
+
tapasMessage.data = Object.keys(rest).length > 0 ? rest : undefined;
|
|
249
|
+
} else {
|
|
250
|
+
// 否则,整个 payload 作为 data,action 使用 operate
|
|
251
|
+
tapasMessage.action = message.type;
|
|
252
|
+
tapasMessage.data = message.payload;
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
// 如果没有 payload,action 使用 operate
|
|
256
|
+
tapasMessage.action = message.type;
|
|
257
|
+
}
|
|
258
|
+
return tapasMessage;
|
|
259
|
+
}, []);
|
|
260
|
+
|
|
261
|
+
// 将 tapas-sys 格式转换为 WebViewMessage
|
|
262
|
+
const convertFromTapasSysFormat = (0, _react.useCallback)(tapasMessage => {
|
|
263
|
+
const message = {
|
|
264
|
+
type: tapasMessage.operate,
|
|
265
|
+
from: tapasMessage.from
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// 构建 payload
|
|
269
|
+
if (tapasMessage.data !== undefined) {
|
|
270
|
+
// 如果有 action 且 action 不等于 operate,将 action 合并到 payload
|
|
271
|
+
if (tapasMessage.action && tapasMessage.action !== tapasMessage.operate) {
|
|
272
|
+
message.payload = {
|
|
273
|
+
action: tapasMessage.action,
|
|
274
|
+
...(typeof tapasMessage.data === 'object' && tapasMessage.data !== null && !Array.isArray(tapasMessage.data) ? tapasMessage.data : {
|
|
275
|
+
data: tapasMessage.data
|
|
276
|
+
})
|
|
277
|
+
};
|
|
278
|
+
} else {
|
|
279
|
+
// 否则直接使用 data 作为 payload
|
|
280
|
+
message.payload = tapasMessage.data;
|
|
281
|
+
}
|
|
282
|
+
} else if (tapasMessage.action && tapasMessage.action !== tapasMessage.operate) {
|
|
283
|
+
// 只有 action,没有 data
|
|
284
|
+
message.payload = {
|
|
285
|
+
action: tapasMessage.action
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
return message;
|
|
289
|
+
}, []);
|
|
290
|
+
|
|
291
|
+
// 解析消息(支持 base64 解码)
|
|
292
|
+
const parseMessage = (0, _react.useCallback)(data => {
|
|
293
|
+
try {
|
|
294
|
+
// 尝试 base64 解码
|
|
295
|
+
let decodedData = data;
|
|
296
|
+
if (enableBase64) {
|
|
297
|
+
try {
|
|
298
|
+
decodedData = decodeBase64(data);
|
|
299
|
+
loggerRef.current.debug('Base64 解码成功');
|
|
300
|
+
} catch (e) {
|
|
301
|
+
// 如果 base64 解码失败,尝试直接解析 JSON
|
|
302
|
+
// 可能是未加密的消息
|
|
303
|
+
loggerRef.current.debug('Base64 解码失败,尝试直接解析 JSON', e);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 解析 JSON
|
|
308
|
+
const parsed = typeof decodedData === 'string' ? JSON.parse(decodedData) : decodedData;
|
|
309
|
+
|
|
310
|
+
// 判断是 tapas-sys 格式还是 WebViewMessage 格式
|
|
311
|
+
let message;
|
|
312
|
+
if (parsed.operate !== undefined) {
|
|
313
|
+
// tapas-sys 格式:{ operate, action, data, from }
|
|
314
|
+
message = convertFromTapasSysFormat(parsed);
|
|
315
|
+
} else {
|
|
316
|
+
// WebViewMessage 格式:{ type, payload, from }
|
|
317
|
+
message = {
|
|
318
|
+
type: parsed.type || 'unknown',
|
|
319
|
+
payload: parsed.payload !== undefined ? parsed.payload : parsed,
|
|
320
|
+
timestamp: parsed.timestamp || Date.now(),
|
|
321
|
+
id: parsed.id,
|
|
322
|
+
from: parsed.from
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
loggerRef.current.debug('消息解析成功:', message);
|
|
326
|
+
return message;
|
|
327
|
+
} catch (e) {
|
|
328
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
329
|
+
loggerRef.current.error('消息解析失败', error, '原始数据:', data);
|
|
330
|
+
if (onError) {
|
|
331
|
+
onError(error, {
|
|
332
|
+
type: 'parse'
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}, [enableBase64, onError, convertFromTapasSysFormat]);
|
|
338
|
+
|
|
339
|
+
// 统一消息格式
|
|
340
|
+
const normalizeMessage = (0, _react.useCallback)(rawMessage => {
|
|
341
|
+
let data = null;
|
|
342
|
+
|
|
343
|
+
// WebView 环境:消息通常在 data 字段中
|
|
344
|
+
if (environmentRef.current === 'webview') {
|
|
345
|
+
if (typeof rawMessage === 'string') {
|
|
346
|
+
data = rawMessage;
|
|
347
|
+
} else if (rawMessage?.nativeEvent?.data) {
|
|
348
|
+
data = rawMessage.nativeEvent.data;
|
|
349
|
+
} else if (rawMessage?.data) {
|
|
350
|
+
data = rawMessage.data;
|
|
351
|
+
} else {
|
|
352
|
+
// 直接是对象:检查是 tapas-sys 格式还是 WebViewMessage 格式
|
|
353
|
+
if (rawMessage?.operate !== undefined) {
|
|
354
|
+
return convertFromTapasSysFormat(rawMessage);
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
type: rawMessage?.type || 'unknown',
|
|
358
|
+
payload: rawMessage?.payload || rawMessage,
|
|
359
|
+
timestamp: rawMessage?.timestamp || Date.now(),
|
|
360
|
+
id: rawMessage?.id,
|
|
361
|
+
from: rawMessage?.from
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
} else if (environmentRef.current === 'iframe') {
|
|
365
|
+
// iframe 环境:消息在 event.data 中
|
|
366
|
+
if (rawMessage?.data) {
|
|
367
|
+
data = rawMessage.data;
|
|
368
|
+
} else if (typeof rawMessage === 'string') {
|
|
369
|
+
data = rawMessage;
|
|
370
|
+
} else {
|
|
371
|
+
// 直接是对象:检查是 tapas-sys 格式还是 WebViewMessage 格式
|
|
372
|
+
if (rawMessage?.operate !== undefined) {
|
|
373
|
+
return convertFromTapasSysFormat(rawMessage);
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
type: rawMessage?.type || 'unknown',
|
|
377
|
+
payload: rawMessage?.payload || rawMessage,
|
|
378
|
+
timestamp: rawMessage?.timestamp || Date.now(),
|
|
379
|
+
id: rawMessage?.id,
|
|
380
|
+
from: rawMessage?.from
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
// 未知环境:尝试通用解析
|
|
385
|
+
if (rawMessage?.operate !== undefined) {
|
|
386
|
+
return convertFromTapasSysFormat(rawMessage);
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
type: rawMessage?.type || 'unknown',
|
|
390
|
+
payload: rawMessage?.payload || rawMessage,
|
|
391
|
+
timestamp: Date.now(),
|
|
392
|
+
from: rawMessage?.from
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 解析字符串数据
|
|
397
|
+
if (data) {
|
|
398
|
+
const parsed = parseMessage(data);
|
|
399
|
+
if (parsed) {
|
|
400
|
+
return parsed;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// 降级:返回原始数据
|
|
405
|
+
return {
|
|
406
|
+
type: 'raw',
|
|
407
|
+
payload: data || rawMessage,
|
|
408
|
+
timestamp: Date.now()
|
|
409
|
+
};
|
|
410
|
+
}, [parseMessage, convertFromTapasSysFormat]);
|
|
411
|
+
|
|
412
|
+
// 发送消息
|
|
413
|
+
const sendMessage = (0, _react.useCallback)((type, payload) => {
|
|
414
|
+
try {
|
|
415
|
+
const message = {
|
|
416
|
+
type,
|
|
417
|
+
payload,
|
|
418
|
+
timestamp: Date.now(),
|
|
419
|
+
id: generateMessageId(),
|
|
420
|
+
from: from // 添加 from 字段
|
|
421
|
+
};
|
|
422
|
+
loggerRef.current.debug('准备发送消息(内部格式):', message);
|
|
423
|
+
|
|
424
|
+
// 转换为 tapas-sys 格式
|
|
425
|
+
const tapasMessage = convertToTapasSysFormat(message);
|
|
426
|
+
loggerRef.current.debug('转换为 tapas-sys 格式:', tapasMessage);
|
|
427
|
+
|
|
428
|
+
// 将消息对象转为 JSON 字符串
|
|
429
|
+
const messageJson = JSON.stringify(tapasMessage);
|
|
430
|
+
|
|
431
|
+
// 根据配置决定是否进行 base64 编码
|
|
432
|
+
const messageToSend = enableBase64 ? encodeBase64(messageJson) : messageJson;
|
|
433
|
+
if (typeof window === 'undefined') {
|
|
434
|
+
const error = new Error('window 对象不存在,无法发送消息');
|
|
435
|
+
loggerRef.current.error(error.message);
|
|
436
|
+
if (onError) {
|
|
437
|
+
onError(error, {
|
|
438
|
+
type: 'send'
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
let sent = false;
|
|
444
|
+
|
|
445
|
+
// 优先尝试 React Native WebView 的 postMessage(适用于 Android/iOS WebView)
|
|
446
|
+
if (window.ReactNativeWebView) {
|
|
447
|
+
try {
|
|
448
|
+
window.ReactNativeWebView.postMessage(messageToSend);
|
|
449
|
+
loggerRef.current.debug('通过 ReactNativeWebView.postMessage 发送成功');
|
|
450
|
+
sent = true;
|
|
451
|
+
return true;
|
|
452
|
+
} catch (e) {
|
|
453
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
454
|
+
loggerRef.current.warn('ReactNativeWebView.postMessage 失败', error);
|
|
455
|
+
if (onError) {
|
|
456
|
+
onError(error, {
|
|
457
|
+
type: 'send'
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 降级:使用 window.parent.postMessage(适用于 iframe 和 Web WebView)
|
|
464
|
+
if (!sent && window.parent && window.parent !== window) {
|
|
465
|
+
try {
|
|
466
|
+
window.parent.postMessage(messageToSend, '*');
|
|
467
|
+
loggerRef.current.debug('通过 window.parent.postMessage 发送成功');
|
|
468
|
+
sent = true;
|
|
469
|
+
return true;
|
|
470
|
+
} catch (e) {
|
|
471
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
472
|
+
loggerRef.current.warn('window.parent.postMessage 失败', error);
|
|
473
|
+
if (onError) {
|
|
474
|
+
onError(error, {
|
|
475
|
+
type: 'send'
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// 最后尝试:直接使用 window.postMessage(某些特殊场景)
|
|
482
|
+
if (!sent && typeof window.postMessage === 'function') {
|
|
483
|
+
try {
|
|
484
|
+
// 注意:某些浏览器可能不支持直接调用 window.postMessage
|
|
485
|
+
// 这里仅作为最后的降级方案
|
|
486
|
+
window.postMessage(messageToSend, '*');
|
|
487
|
+
loggerRef.current.debug('通过 window.postMessage 发送成功');
|
|
488
|
+
sent = true;
|
|
489
|
+
return true;
|
|
490
|
+
} catch (e) {
|
|
491
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
492
|
+
loggerRef.current.warn('window.postMessage 失败', error);
|
|
493
|
+
if (onError) {
|
|
494
|
+
onError(error, {
|
|
495
|
+
type: 'send'
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (!sent) {
|
|
501
|
+
const error = new Error(`所有消息发送方式都失败,当前环境: ${environmentRef.current}`);
|
|
502
|
+
loggerRef.current.error(error.message);
|
|
503
|
+
if (onError) {
|
|
504
|
+
onError(error, {
|
|
505
|
+
type: 'send'
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
return true;
|
|
511
|
+
} catch (e) {
|
|
512
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
513
|
+
loggerRef.current.error('发送消息时发生未知错误', error);
|
|
514
|
+
if (onError) {
|
|
515
|
+
onError(error, {
|
|
516
|
+
type: 'send'
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
}, [generateMessageId, from, enableBase64, onError, convertToTapasSysFormat]);
|
|
522
|
+
|
|
523
|
+
// 消息处理函数
|
|
524
|
+
const handleMessage = (0, _react.useCallback)((rawMessage, eventOrigin) => {
|
|
525
|
+
try {
|
|
526
|
+
// Origin 验证(仅 iframe 环境)
|
|
527
|
+
if (environmentRef.current === 'iframe' && eventOrigin !== undefined) {
|
|
528
|
+
const isValidOrigin = validateOrigin(eventOrigin, allowedOrigins, strictOriginCheck, loggerRef.current);
|
|
529
|
+
if (!isValidOrigin) {
|
|
530
|
+
const error = new Error(`消息来源 ${eventOrigin} 验证失败`);
|
|
531
|
+
loggerRef.current.error(error.message);
|
|
532
|
+
if (onError) {
|
|
533
|
+
onError(error, {
|
|
534
|
+
type: 'origin'
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const normalizedMessage = normalizeMessage(rawMessage);
|
|
541
|
+
if (!normalizedMessage) {
|
|
542
|
+
loggerRef.current.warn('消息标准化失败,忽略消息');
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// 验证 from 字段
|
|
547
|
+
if (!validateFrom(normalizedMessage.from, allowedFrom, loggerRef.current)) {
|
|
548
|
+
loggerRef.current.warn('消息 from 字段验证失败,忽略消息');
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// 消息过滤器
|
|
553
|
+
if (messageFilter && !messageFilter(normalizedMessage)) {
|
|
554
|
+
loggerRef.current.debug('消息被过滤器拒绝,忽略消息');
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
loggerRef.current.debug('处理消息:', normalizedMessage);
|
|
558
|
+
|
|
559
|
+
// 调用外部监听器
|
|
560
|
+
if (onMessage) {
|
|
561
|
+
try {
|
|
562
|
+
onMessage(normalizedMessage);
|
|
563
|
+
} catch (e) {
|
|
564
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
565
|
+
loggerRef.current.error('onMessage 回调执行错误', error);
|
|
566
|
+
if (onError) {
|
|
567
|
+
onError(error, {
|
|
568
|
+
type: 'receive'
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// 调用注册的处理器
|
|
575
|
+
messageHandlersRef.current.forEach(handler => {
|
|
576
|
+
try {
|
|
577
|
+
handler(normalizedMessage);
|
|
578
|
+
} catch (e) {
|
|
579
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
580
|
+
loggerRef.current.error('消息处理器执行错误', error);
|
|
581
|
+
if (onError) {
|
|
582
|
+
onError(error, {
|
|
583
|
+
type: 'receive'
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
} catch (e) {
|
|
589
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
590
|
+
loggerRef.current.error('处理消息时发生未知错误', error);
|
|
591
|
+
if (onError) {
|
|
592
|
+
onError(error, {
|
|
593
|
+
type: 'receive'
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}, [onMessage, normalizeMessage, allowedOrigins, strictOriginCheck, allowedFrom, messageFilter, onError]);
|
|
598
|
+
|
|
599
|
+
// 清理函数
|
|
600
|
+
const cleanup = (0, _react.useCallback)(() => {
|
|
601
|
+
loggerRef.current.debug('清理所有监听器和资源');
|
|
602
|
+
cleanupFunctionsRef.current.forEach(cleanupFn => {
|
|
603
|
+
try {
|
|
604
|
+
cleanupFn();
|
|
605
|
+
} catch (e) {
|
|
606
|
+
loggerRef.current.error('清理函数执行错误', e);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
cleanupFunctionsRef.current = [];
|
|
610
|
+
messageHandlersRef.current.clear();
|
|
611
|
+
}, []);
|
|
612
|
+
|
|
613
|
+
// 注册消息监听
|
|
614
|
+
(0, _react.useEffect)(() => {
|
|
615
|
+
let cleanupFn;
|
|
616
|
+
if (environmentRef.current === 'iframe') {
|
|
617
|
+
// iframe 环境:监听 window message 事件
|
|
618
|
+
const messageHandler = event => {
|
|
619
|
+
// 传递 origin 用于验证
|
|
620
|
+
handleMessage(event, event.origin);
|
|
621
|
+
};
|
|
622
|
+
if (typeof window !== 'undefined') {
|
|
623
|
+
window.addEventListener('message', messageHandler);
|
|
624
|
+
cleanupFn = () => {
|
|
625
|
+
window.removeEventListener('message', messageHandler);
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
} else if (environmentRef.current === 'webview') {
|
|
629
|
+
// WebView 环境:监听消息
|
|
630
|
+
if (_reactNative.Platform.OS === 'web') {
|
|
631
|
+
// Web 环境下的 WebView:监听 window message
|
|
632
|
+
const messageHandler = event => {
|
|
633
|
+
handleMessage(event, event.origin);
|
|
634
|
+
};
|
|
635
|
+
if (typeof window !== 'undefined') {
|
|
636
|
+
window.addEventListener('message', messageHandler);
|
|
637
|
+
cleanupFn = () => {
|
|
638
|
+
window.removeEventListener('message', messageHandler);
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
} else {
|
|
642
|
+
// 原生环境:监听 React Native WebView 的消息
|
|
643
|
+
// 注意:在原生环境中,消息需要通过 WebView 组件的 onMessage prop 传递
|
|
644
|
+
// 这里我们提供一个通用的监听机制,通过全局事件总线
|
|
645
|
+
const messageHandler = event => {
|
|
646
|
+
handleMessage(event);
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
// 使用自定义事件系统(如果存在)
|
|
650
|
+
if (typeof global !== 'undefined' && global.__WebViewBridgeEventBus) {
|
|
651
|
+
const eventBus = global.__WebViewBridgeEventBus;
|
|
652
|
+
eventBus.on('message', messageHandler);
|
|
653
|
+
cleanupFn = () => {
|
|
654
|
+
eventBus.off('message', messageHandler);
|
|
655
|
+
};
|
|
656
|
+
} else if (typeof window !== 'undefined') {
|
|
657
|
+
// 降级:监听 window 事件(某些 WebView 实现可能支持)
|
|
658
|
+
const messageHandler = event => {
|
|
659
|
+
handleMessage(event, event.origin);
|
|
660
|
+
};
|
|
661
|
+
window.addEventListener('message', messageHandler);
|
|
662
|
+
cleanupFn = () => {
|
|
663
|
+
window.removeEventListener('message', messageHandler);
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (cleanupFn) {
|
|
669
|
+
cleanupFunctionsRef.current.push(cleanupFn);
|
|
670
|
+
}
|
|
671
|
+
return cleanupFn;
|
|
672
|
+
}, [handleMessage]);
|
|
673
|
+
|
|
674
|
+
// 发送初始化消息(在 window.onload 完成后自动发送)
|
|
675
|
+
(0, _react.useEffect)(() => {
|
|
676
|
+
if (autoInit && !initSentRef.current && from) {
|
|
677
|
+
const sendInitMessage = () => {
|
|
678
|
+
if (!initSentRef.current) {
|
|
679
|
+
sendMessage(initMessageType, initPayload);
|
|
680
|
+
initSentRef.current = true;
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
// 检查 window 是否已经加载完成
|
|
685
|
+
if (typeof window !== 'undefined') {
|
|
686
|
+
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
|
687
|
+
// 文档已经加载完成,使用 setTimeout 确保在下一个事件循环中执行
|
|
688
|
+
// 这样可以确保所有组件都已挂载完成
|
|
689
|
+
setTimeout(sendInitMessage, 0);
|
|
690
|
+
} else {
|
|
691
|
+
// 等待 window.onload 事件
|
|
692
|
+
const handleLoad = () => {
|
|
693
|
+
sendInitMessage();
|
|
694
|
+
window.removeEventListener('load', handleLoad);
|
|
695
|
+
};
|
|
696
|
+
window.addEventListener('load', handleLoad);
|
|
697
|
+
|
|
698
|
+
// 也监听 DOMContentLoaded 作为备选(某些情况下可能更快)
|
|
699
|
+
const handleDOMContentLoaded = () => {
|
|
700
|
+
// 延迟一点确保所有初始化完成
|
|
701
|
+
setTimeout(sendInitMessage, 10);
|
|
702
|
+
document.removeEventListener('DOMContentLoaded', handleDOMContentLoaded);
|
|
703
|
+
};
|
|
704
|
+
if (document.readyState === 'loading') {
|
|
705
|
+
document.addEventListener('DOMContentLoaded', handleDOMContentLoaded);
|
|
706
|
+
}
|
|
707
|
+
return () => {
|
|
708
|
+
window.removeEventListener('load', handleLoad);
|
|
709
|
+
document.removeEventListener('DOMContentLoaded', handleDOMContentLoaded);
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
} else {
|
|
713
|
+
// 非 Web 环境(React Native),直接发送
|
|
714
|
+
setTimeout(sendInitMessage, 100);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return undefined;
|
|
718
|
+
}, [autoInit, initMessageType, initPayload, sendMessage, from]);
|
|
719
|
+
|
|
720
|
+
// 组件卸载时清理
|
|
721
|
+
(0, _react.useEffect)(() => {
|
|
722
|
+
return () => {
|
|
723
|
+
cleanup();
|
|
724
|
+
};
|
|
725
|
+
}, [cleanup]);
|
|
726
|
+
|
|
727
|
+
// 暴露方法给父组件
|
|
728
|
+
(0, _react.useImperativeHandle)(ref, () => ({
|
|
729
|
+
sendMessage,
|
|
730
|
+
getEnvironment: () => environmentRef.current,
|
|
731
|
+
cleanup
|
|
732
|
+
}), [sendMessage, cleanup]);
|
|
733
|
+
|
|
734
|
+
// 渲染占位 DOM(确保组件有实体)
|
|
735
|
+
if (!showPlaceholder) {
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
739
|
+
style: [styles.container, style],
|
|
740
|
+
testID: testID,
|
|
741
|
+
pointerEvents: "none"
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
WebViewBridge.displayName = 'WebViewBridge';
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* 原生 WebView 消息注入辅助函数
|
|
748
|
+
* 在 WebView 的 injectedJavaScript 中使用,用于接收原生消息
|
|
749
|
+
*/
|
|
750
|
+
const createWebViewInjectedScript = () => {
|
|
751
|
+
return `
|
|
752
|
+
(function() {
|
|
753
|
+
// 创建全局事件总线(如果不存在)
|
|
754
|
+
if (typeof window.__WebViewBridgeEventBus === 'undefined') {
|
|
755
|
+
window.__WebViewBridgeEventBus = {
|
|
756
|
+
listeners: {},
|
|
757
|
+
on: function(event, handler) {
|
|
758
|
+
if (!this.listeners[event]) {
|
|
759
|
+
this.listeners[event] = [];
|
|
760
|
+
}
|
|
761
|
+
this.listeners[event].push(handler);
|
|
762
|
+
},
|
|
763
|
+
off: function(event, handler) {
|
|
764
|
+
if (this.listeners[event]) {
|
|
765
|
+
this.listeners[event] = this.listeners[event].filter(h => h !== handler);
|
|
766
|
+
}
|
|
767
|
+
},
|
|
768
|
+
emit: function(event, data) {
|
|
769
|
+
if (this.listeners[event]) {
|
|
770
|
+
this.listeners[event].forEach(handler => {
|
|
771
|
+
try {
|
|
772
|
+
handler(data);
|
|
773
|
+
} catch (e) {
|
|
774
|
+
console.error('WebViewBridge: 事件处理器错误', e);
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// 监听 React Native WebView 的消息
|
|
783
|
+
if (window.ReactNativeWebView) {
|
|
784
|
+
window.document.addEventListener('message', function(event) {
|
|
785
|
+
if (window.__WebViewBridgeEventBus) {
|
|
786
|
+
window.__WebViewBridgeEventBus.emit('message', event);
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// 监听 postMessage(备用方案)
|
|
792
|
+
window.addEventListener('message', function(event) {
|
|
793
|
+
if (window.__WebViewBridgeEventBus) {
|
|
794
|
+
window.__WebViewBridgeEventBus.emit('message', event);
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
})();
|
|
798
|
+
true; // 确保脚本返回 true
|
|
799
|
+
`;
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* 处理原生 WebView 消息
|
|
804
|
+
* 在 WebView 的 onMessage 回调中调用此函数
|
|
805
|
+
*/
|
|
806
|
+
exports.createWebViewInjectedScript = createWebViewInjectedScript;
|
|
807
|
+
const handleNativeWebViewMessage = (event, onMessage) => {
|
|
808
|
+
try {
|
|
809
|
+
const data = event.nativeEvent?.data || event.data || event;
|
|
810
|
+
const message = typeof data === 'string' ? JSON.parse(data) : data;
|
|
811
|
+
onMessage(message);
|
|
812
|
+
} catch (e) {
|
|
813
|
+
console.error('WebViewBridge: 解析原生消息失败', e);
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
exports.handleNativeWebViewMessage = handleNativeWebViewMessage;
|
|
817
|
+
const styles = _reactNative.StyleSheet.create({
|
|
818
|
+
container: {
|
|
819
|
+
width: 0,
|
|
820
|
+
height: 0,
|
|
821
|
+
position: 'absolute',
|
|
822
|
+
opacity: 0,
|
|
823
|
+
pointerEvents: 'none'
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
var _default = exports.default = WebViewBridge;
|
|
827
|
+
//# sourceMappingURL=WebViewBridge.js.map
|