504 lines
13 KiB
JavaScript
504 lines
13 KiB
JavaScript
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;
|
||
}
|