Files
scan-code-jssdk/src/_export.js
iqudoo b438364656 fix
2026-05-26 14:00:41 +08:00

504 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { isReadyCalled } from "./_core";
import { getConfig } from "./services/config";
import {
readWxLikeEnvFromWindow,
setParentWxEnvReport,
getParentWxEnvReport,
} from "./services/embedEnvProbe";
import {
hasDistinctParentWindow,
resolveUseParentProxy,
} from "./services/embedProxy";
import { createUUID } from "./utils/uuid";
import {
dispatchEmbedScanResult,
acknowledgeEmbedScanConsumed,
dispatchEmbedScanError,
setEmbedScanHostEnabled,
} from "./services/provider/scan";
import {
setEmbedScanResultForwarder,
setEmbedScanErrorForwarder,
} from "./services/embedScanBridge";
import { unlockScanBeep, installScanBeepGestureUnlock } from "./services/web";
const EMBED_SOURCE = "IScanEmbed";
const EMBED_V = 1;
/** 已向父页发起过 invoke 的嵌入子 frame用于父页识别结果回传 */
const embedChildSources = new Set();
const EMBED_LISTENER_METHODS = new Set([
"onScanListener",
"offScanListener",
"onScanErrorListener",
"offScanErrorListener",
"clear",
]);
/** 嵌入 iframe 转发到父页时,须在子页用用户手势解锁提示音 */
const EMBED_SCAN_GESTURE_METHODS = new Set([
"startScan",
"scanImage",
"scanImageFromFile",
]);
function isEmbedMessage(data) {
return data && data.source === EMBED_SOURCE && data.v === EMBED_V;
}
let embedHostInstalled = false;
let embedChildInstalled = false;
const pendingInvokes = Object.create(null);
const childCallbackFns = Object.create(null);
function cloneArgsReplacingFunctions(val, registry, seen) {
if (typeof val === "function") {
const id = createUUID();
registry[id] = val;
return { __IScanEmbedCb__: id };
}
if (val === null || typeof val !== "object") {
return val;
}
if (!seen) {
seen = new WeakMap();
}
if (seen.has(val)) {
throw new Error("[IScan embed]: circular reference in arguments");
}
seen.set(val, true);
if (Array.isArray(val)) {
return val.map((item) => cloneArgsReplacingFunctions(item, registry, seen));
}
const out = {};
Object.keys(val).forEach((k) => {
out[k] = cloneArgsReplacingFunctions(val[k], registry, seen);
});
return out;
}
function serializeEmbedParams(params) {
const serialized = [];
const registry = {};
for (let i = 0; i < params.length; i++) {
serialized.push(cloneArgsReplacingFunctions(params[i], registry));
}
return { serialized, registry };
}
function hydrateEmbedParams(params, messageSource, targetOrigin) {
function walk(val) {
if (val && typeof val === "object" && val.__IScanEmbedCb__) {
const cbId = val.__IScanEmbedCb__;
return function embedCbProxy() {
const args = Array.prototype.slice.call(arguments);
messageSource.postMessage(
{
source: EMBED_SOURCE,
v: EMBED_V,
kind: "callback",
cbId,
args,
},
targetOrigin
);
};
}
if (val === null || typeof val !== "object") {
return val;
}
if (Array.isArray(val)) {
return val.map(walk);
}
const out = {};
Object.keys(val).forEach((k) => {
out[k] = walk(val[k]);
});
return out;
}
return params.map(walk);
}
function broadcastScanErrorToEmbedChildren(error) {
if (embedChildSources.size === 0 || error == null || error === "") {
return;
}
embedChildSources.forEach((source) => {
try {
source.postMessage(
{
source: EMBED_SOURCE,
v: EMBED_V,
kind: "forwardScanError",
error,
},
"*"
);
} catch (e) {
}
});
}
function broadcastScanResultToEmbedChildren(result, meta) {
if (embedChildSources.size === 0 || result == null || result === "") {
return;
}
embedChildSources.forEach((source) => {
try {
source.postMessage(
{
source: EMBED_SOURCE,
v: EMBED_V,
kind: "forwardScanResult",
result,
scanSource: meta && meta.source,
},
"*"
);
} catch (e) {
}
});
}
let embedWxProbeScheduled = false;
let embedWxProbeAttempts = 0;
const EMBED_WX_PROBE_MAX = 4;
function shouldScheduleParentWxProbe() {
if (typeof window === "undefined" || !hasDistinctParentWindow()) {
return false;
}
const mode = getConfig("embedProxyMode");
if (mode === false || mode === "local" || mode === "off") {
return false;
}
return true;
}
/** 向父页询问是否微信环境(子 iframe 跨域时本地 UA 可能不可靠) */
function scheduleEmbedWxEnvProbeIfNeeded() {
if (!shouldScheduleParentWxProbe()) {
return;
}
if (embedWxProbeScheduled) {
return;
}
if (embedWxProbeAttempts >= EMBED_WX_PROBE_MAX) {
return;
}
embedWxProbeScheduled = true;
embedWxProbeAttempts++;
ensureEmbedChildListener();
window.parent.postMessage(
{
source: EMBED_SOURCE,
v: EMBED_V,
kind: "probeWxEnv",
id: createUUID(),
},
"*"
);
window.setTimeout(() => {
embedWxProbeScheduled = false;
if (getParentWxEnvReport() !== null || !shouldScheduleParentWxProbe()) {
return;
}
scheduleEmbedWxEnvProbeIfNeeded();
}, 600);
}
function embedChildOnMessage(ev) {
const data = ev.data;
if (!isEmbedMessage(data)) {
return;
}
if (data.kind === "probeWxEnvResult") {
setParentWxEnvReport(!!data.wx);
return;
}
if (data.kind === "forwardScanResult") {
if (typeof data.result === "string") {
const consumed = dispatchEmbedScanResult(data.result, {
source: data.scanSource,
});
if (consumed && resolveUseParentProxy()) {
window.parent.postMessage(
{
source: EMBED_SOURCE,
v: EMBED_V,
kind: "scanResultConsumed",
result: data.result,
},
"*"
);
}
}
return;
}
if (data.kind === "forwardScanError") {
if (typeof data.error === "string") {
dispatchEmbedScanError(data.error);
}
return;
}
if (data.kind === "invokeResult") {
const pending = pendingInvokes[data.id];
if (!pending) {
return;
}
delete pendingInvokes[data.id];
if (data.ok) {
pending.resolve(data.result);
} else {
pending.reject(new Error(data.error || "[IScan embed]: invoke failed"));
}
return;
}
if (data.kind === "callback") {
const fn = childCallbackFns[data.cbId];
if (typeof fn === "function") {
fn.apply(null, data.args || []);
}
}
}
function ensureEmbedChildListener() {
if (embedChildInstalled || typeof window === "undefined") {
return;
}
embedChildInstalled = true;
installScanBeepGestureUnlock();
window.addEventListener("message", embedChildOnMessage);
}
function embedInvoke(methodKey, params) {
ensureEmbedChildListener();
scheduleEmbedWxEnvProbeIfNeeded();
const id = createUUID();
const { serialized, registry } = serializeEmbedParams(params);
Object.keys(registry).forEach((cbId) => {
childCallbackFns[cbId] = registry[cbId];
});
return new Promise((resolve, reject) => {
pendingInvokes[id] = { resolve, reject };
window.parent.postMessage(
{
source: EMBED_SOURCE,
v: EMBED_V,
kind: "invoke",
id,
methodKey,
params: serialized,
},
"*"
);
});
}
function handleEmbedHostInvoke(lib, ev) {
const data = ev.data;
const { id, methodKey, params } = data;
const hydrated = hydrateEmbedParams(params || [], ev.source, ev.origin);
Promise.resolve()
.then(() => _exec(lib, methodKey, ...hydrated))
.then((result) => {
let out = result;
if (out && typeof out.then === "function") {
return out.then((r) => {
out = r;
return out;
});
}
return out;
})
.then((result) => {
ev.source.postMessage(
{
source: EMBED_SOURCE,
v: EMBED_V,
kind: "invokeResult",
id,
methodKey,
ok: true,
result,
},
ev.origin
);
})
.catch((err) => {
ev.source.postMessage(
{
source: EMBED_SOURCE,
v: EMBED_V,
kind: "invokeResult",
id,
methodKey,
ok: false,
error: typeof err === "string" ? err : String((err && err.message) || err),
},
ev.origin
);
});
}
/**
* 在顶层页面注册 iframe 嵌入代理:子页面(嵌入模式)通过 postMessage 调用同一套 SDK。
* 需在加载 SDK 后调用一次,并传入与 exportSDK 相同的 lib通常为 ./_core 默认导出)。
*/
export function installEmbedHost(lib) {
if (embedHostInstalled || typeof window === "undefined") {
return;
}
embedHostInstalled = true;
setEmbedScanResultForwarder(broadcastScanResultToEmbedChildren);
setEmbedScanErrorForwarder(broadcastScanErrorToEmbedChildren);
setEmbedScanHostEnabled(true);
window.addEventListener("message", (ev) => {
const data = ev.data;
if (!isEmbedMessage(data)) {
return;
}
if (data.kind === "probeWxEnv") {
if (!ev.source || ev.source === window) {
return;
}
const wx = readWxLikeEnvFromWindow(window);
ev.source.postMessage(
{
source: EMBED_SOURCE,
v: EMBED_V,
kind: "probeWxEnvResult",
id: data.id,
wx,
},
ev.origin
);
return;
}
if (data.kind === "scanResultConsumed") {
if (!ev.source || ev.source === window) {
return;
}
if (typeof data.result === "string") {
acknowledgeEmbedScanConsumed(data.result);
}
return;
}
if (data.kind !== "invoke") {
return;
}
if (!data.id || !data.methodKey) {
return;
}
if (!ev.source || ev.source === window) {
return;
}
embedChildSources.add(ev.source);
handleEmbedHostInvoke(lib, ev);
});
}
function _exec(target, func, ...params) {
let instant = target;
let funcs = func.split('.');
while (funcs.length > 1) {
instant = instant[funcs.shift()];
}
if (instant && funcs.length == 1) {
if (instant.hasOwnProperty(funcs[0])) {
return instant[funcs[0]](...params);
}
}
throw `IScan.${func} not defined`;
}
function hook(target, method, action) {
let fns = method.split('.');
fns.forEach((fn, index) => {
if (index == fns.length - 1) {
target[fn] = action;
} else {
if (!target[fn]) {
target[fn] = {};
}
}
target = target[fn];
});
}
function forMethods(obj, parent = "") {
let keys = {}
Object.keys(obj).forEach(key => {
let val = obj[key];
if (typeof val === "object") {
Object.assign(keys, forMethods(val, `${parent}${key}.`));
} else if (typeof val === "function") {
let realKey = `${parent}${key}`;
Object.assign(keys, { [realKey]: realKey });
}
})
return keys;
}
function freezeObj(obj) {
Object.keys(obj).forEach(key => {
let val = obj[key];
if (typeof val === "object") {
freezeObj(val)
}
})
Object.freeze(obj);
}
/**
* 统一调用代理:根据 embedProxyMode + 环境决定走父页面转发或本地 _exec。
*/
function createInvokeTransport(lib, method, methodName, initNames) {
return function IScanInvokeProxy(...params) {
if (resolveUseParentProxy()) {
if (EMBED_LISTENER_METHODS.has(methodName)) {
if (!isReadyCalled() && initNames && initNames.indexOf(method) < 0) {
throw `[IScan]:Can't call the "IScan.${method}" method, because "IScan" not ready, please confirm that "IScan.ready()" has been called. params: ${JSON.stringify(params)}`
}
return _exec(lib, methodName, ...params);
}
if (EMBED_SCAN_GESTURE_METHODS.has(method)) {
unlockScanBeep();
}
return embedInvoke(methodName, params);
}
if (!isReadyCalled() && initNames && initNames.indexOf(method) < 0) {
throw `[IScan]:Can't call the "IScan.${method}" method, because "IScan" not ready, please confirm that "IScan.ready()" has been called. params: ${JSON.stringify(params)}`
}
return _exec(lib, methodName, ...params);
};
}
/**
* 调试:当前解析到的嵌入转发开关(每次读取最新 config
*/
export function getEmbedProxyResolved() {
return resolveUseParentProxy();
}
export { resolveUseParentProxy } from "./services/embedProxy";
export function exportSDK(lib, funcs, ...initNames) {
let methods = {};
if (funcs && typeof funcs === 'object') {
methods = funcs;
} else {
methods = forMethods(lib);
}
const library = {};
Object.keys(methods).forEach(method => {
let methodItem = methods[method];
let methodName = methodItem && methodItem.method || method;
hook(library, method, createInvokeTransport(lib, method, methodName, initNames));
});
scheduleEmbedWxEnvProbeIfNeeded();
freezeObj(library);
return library;
}