支持IFRAME嵌入模式,嵌入模式下都需要引入SDK

This commit is contained in:
iqudoo
2026-05-02 13:14:17 +08:00
parent 45686d28fc
commit 1966dbbd51
3 changed files with 259 additions and 3 deletions

2
dist/index.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,252 @@
import _core, { isReadyCalled } from "./_core";
import { isReadyCalled } from "./_core";
import { createUUID } from "./utils/uuid";
const EMBED_SOURCE = "IScanEmbed";
const EMBED_V = 1;
function isEmbedded() {
if (typeof window === "undefined") {
return false;
}
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}
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 serializeEmbedInvokeResult(methodKey, result) {
if (
methodKey === "onScanListener" &&
result &&
typeof result === "object" &&
typeof result.key === "string"
) {
return { __IScanEmbedListenerRef__: true, key: result.key };
}
return result;
}
function deserializeEmbedInvokeResult(methodKey, raw) {
if (
methodKey === "onScanListener" &&
raw &&
raw.__IScanEmbedListenerRef__ &&
typeof raw.key === "string"
) {
const key = raw.key;
return {
key,
cancel: () => embedInvoke("offScanListener", [key]),
};
}
return raw;
}
function embedChildOnMessage(ev) {
const data = ev.data;
if (!isEmbedMessage(data)) {
return;
}
if (data.kind === "invokeResult") {
const pending = pendingInvokes[data.id];
if (!pending) {
return;
}
delete pendingInvokes[data.id];
if (data.ok) {
pending.resolve(deserializeEmbedInvokeResult(data.methodKey, 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" || !isEmbedded()) {
return;
}
embedChildInstalled = true;
window.addEventListener("message", embedChildOnMessage);
}
function embedInvoke(methodKey, params) {
ensureEmbedChildListener();
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) => {
const serializedResult = serializeEmbedInvokeResult(methodKey, result);
ev.source.postMessage(
{
source: EMBED_SOURCE,
v: EMBED_V,
kind: "invokeResult",
id,
methodKey,
ok: true,
result: serializedResult,
},
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;
window.addEventListener("message", (ev) => {
const data = ev.data;
if (!isEmbedMessage(data) || data.kind !== "invoke") {
return;
}
if (!data.id || !data.methodKey) {
return;
}
if (!ev.source || ev.source === window) {
return;
}
handleEmbedHostInvoke(lib, ev);
});
}
function _exec(target, func, ...params) {
let instant = target;
@@ -64,12 +312,18 @@ export function exportSDK(lib, funcs, ...initNames) {
let methodItem = methods[method];
let methodName = methodItem && methodItem.method || method;
hook(library, method, (...params) => {
if (isEmbedded()) {
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);
});
});
if (typeof window !== "undefined" && isEmbedded()) {
ensureEmbedChildListener();
}
freezeObj(library);
return library;
}

View File

@@ -1,10 +1,12 @@
import core from "./_core";
import _global from './polyfill/_global';
import { exportSDK } from './_export';
import { exportSDK, installEmbedHost } from './_export';
const IScan = exportSDK(core, null, "config", "setStatusListener", "onScanListener",
"offScanListener", "stopScan", "startScan", "scanImage", "clear");
installEmbedHost(core);
function dispatchIScanReady() {
_global.__IScanReady__ && _global.__IScanReady__();
if (!_global.dispatchEvent) {