嵌入模式识别

This commit is contained in:
iqudoo
2026-05-02 13:40:28 +08:00
parent 1966dbbd51
commit ede67b61b4
8 changed files with 279 additions and 34 deletions

12
dist/index.d.ts vendored
View File

@@ -6,6 +6,18 @@ interface ScanConfigOptions {
* 扫码重启延迟单位毫秒默认500ms
*/
scanRestartDelay?: number,
/**
* iframe 场景下是否将 API 调用转发到父页面同名 SDKpostMessage
* - `'auto'`(默认):处于子 frame`parent !== window`)即转发,对外 API含 `startScan`)均由父页 SDK 执行
* - `true` / `'on'` / `'parent'`:存在父 window 时强制转发
* - `false` / `'off'` / `'local'`:始终在本页执行(子页自己要跑扫码时用)
*/
embedProxyMode?: 'auto' | boolean | 'on' | 'off' | 'local' | 'parent',
/**
* 请求微信 JS-SDK 签名时使用的页面 URL不含 hash
* 跨域 iframe 无法读取父页地址时需手动设为当前微信内打开的页面链接。
*/
wxJssdkSignatureUrl?: string,
/**
* 桥接是否启用,默认启用
*/

2
dist/index.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,20 +1,19 @@
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";
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;
}
@@ -120,11 +119,62 @@ function deserializeEmbedInvokeResult(methodKey, raw) {
return raw;
}
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 === "invokeResult") {
const pending = pendingInvokes[data.id];
if (!pending) {
@@ -147,7 +197,7 @@ function embedChildOnMessage(ev) {
}
function ensureEmbedChildListener() {
if (embedChildInstalled || typeof window === "undefined" || !isEmbedded()) {
if (embedChildInstalled || typeof window === "undefined") {
return;
}
embedChildInstalled = true;
@@ -156,6 +206,7 @@ function ensureEmbedChildListener() {
function embedInvoke(methodKey, params) {
ensureEmbedChildListener();
scheduleEmbedWxEnvProbeIfNeeded();
const id = createUUID();
const { serialized, registry } = serializeEmbedParams(params);
Object.keys(registry).forEach((cbId) => {
@@ -235,7 +286,27 @@ export function installEmbedHost(lib) {
embedHostInstalled = true;
window.addEventListener("message", (ev) => {
const data = ev.data;
if (!isEmbedMessage(data) || data.kind !== "invoke") {
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 !== "invoke") {
return;
}
if (!data.id || !data.methodKey) {
@@ -300,6 +371,42 @@ function freezeObj(obj) {
Object.freeze(obj);
}
/**
* 统一调用代理:根据 embedProxyMode + 环境决定走父页面转发或本地 _exec。
*/
function createInvokeTransport(lib, method, methodName, initNames) {
return function IScanInvokeProxy(...params) {
if (resolveUseParentProxy()) {
if (methodName === "onScanListener") {
const listener = params[0];
const key = params[1];
if (!key || typeof key !== "string" || typeof listener !== "function") {
return;
}
embedInvoke(methodName, params).catch(() => {});
return {
key,
cancel: () => embedInvoke("offScanListener", [key]),
};
}
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') {
@@ -311,19 +418,9 @@ export function exportSDK(lib, funcs, ...initNames) {
Object.keys(methods).forEach(method => {
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);
hook(library, method, createInvokeTransport(lib, method, methodName, initNames));
});
});
if (typeof window !== "undefined" && isEmbedded()) {
ensureEmbedChildListener();
}
scheduleEmbedWxEnvProbeIfNeeded();
freezeObj(library);
return library;
}

View File

@@ -3,6 +3,13 @@ let _defineConfig = {
}
let _defConfig = {
/**
* iframe / 嵌入场景下是否把调用转发到父页面的同名 SDKpostMessage
* - 'auto'(默认):只要处于子 frameparent !== window即转发含 startScan 等均走父页逻辑
* - true | 'on' | 'parent':在存在父 window 时强制转发
* - false | 'off' | 'local':始终在本页执行(子页自己要跑扫码时用)
*/
embedProxyMode: 'auto',
}
let _customConfig = {

View File

@@ -0,0 +1,40 @@
/**
* 嵌入场景下由父页探测得到的微信环境结论(子页跨域时无法读 top UA
*/
let parentWxEnvReport = null;
export function setParentWxEnvReport(wx) {
parentWxEnvReport = wx ? true : false;
}
export function getParentWxEnvReport() {
return parentWxEnvReport;
}
/**
* 在给定 window 上探测微信系 WebView公众号 / 部分企业微信)
* 与外层页面是否为同一套 SDK无关依赖 UA / WeixinJSBridge
*/
export function readWxLikeEnvFromWindow(win) {
if (!win) {
return false;
}
try {
const nav = win.navigator;
if (nav) {
const ua = nav.userAgent || "";
if (/micromessenger/i.test(ua)) {
return true;
}
if (/wxwork/i.test(ua)) {
return true;
}
}
if (win.WeixinJSBridge) {
return true;
}
} catch (e) {
// ignore
}
return false;
}

View File

@@ -0,0 +1,37 @@
import { getConfig } from "./config";
/** 是否存在可与 postMessage 交互的父 browsing context自身不是顶层 opener/parent */
export function hasDistinctParentWindow() {
if (typeof window === "undefined") {
return false;
}
try {
return window.parent != null && window.parent !== window;
} catch (e) {
return true;
}
}
/**
* 是否通过父页面 SDK 代理执行(每次调用重新读取 config
*
* embedProxyMode 为 auto默认只要 window.parent !== window处于子 frame
* 所有对外 API含 startScan均走 postMessage 由父页同一套 SDK 执行,避免子页重复跑扫码逻辑。
*/
export function resolveUseParentProxy() {
const mode = getConfig("embedProxyMode");
if (mode === false || mode === "local" || mode === "off") {
return false;
}
const parentOk = hasDistinctParentWindow();
if (!parentOk) {
return false;
}
if (mode === true || mode === "on" || mode === "parent") {
return true;
}
if (mode === "auto" || mode === undefined || mode === null) {
return parentOk;
}
return !!mode;
}

View File

@@ -1,4 +1,6 @@
import { getConfig } from "../config";
import { readWxLikeEnvFromWindow, getParentWxEnvReport } from "../embedEnvProbe";
import { resolveUseParentProxy } from "../embedProxy";
import { request } from "../../utils/request";
import { toAny } from "../../utils/toany";
@@ -46,6 +48,27 @@ function loadWxScript() {
});
}
/** 微信 JS-SDK 签名 URL嵌入且走父页代理时用父页地址与微信内打开的页面一致可配置 wxJssdkSignatureUrl 覆盖 */
function getPageUrlForWxJssdkSignature() {
const override = getConfig("wxJssdkSignatureUrl");
if (override) {
return String(override).split("#")[0];
}
if (resolveUseParentProxy()) {
try {
if (typeof window !== "undefined" && window.parent && window.parent !== window) {
return window.parent.location.href.split("#")[0];
}
} catch (e) {
// 跨域父页不可读 location由调用方配置 wxJssdkSignatureUrl
}
}
if (typeof window === "undefined") {
return "";
}
return window.location.href.split("#")[0];
}
function fetchWxConfig() {
let initWechatJssdk = toAny(getConfig("initWechatJssdk"), {});
if (!!initWechatJssdk.sdkConfig) {
@@ -59,7 +82,7 @@ function fetchWxConfig() {
url: apiUrl,
method: "GET",
data: {
url: window.location.href.split("#")[0]
url: getPageUrlForWxJssdkSignature()
}
}).then(res => {
let data = toAny(res.data, {});
@@ -77,8 +100,22 @@ function fetchWxConfig() {
}
export function isWxEnv() {
return typeof navigator !== "undefined"
&& /micromessenger/i.test(navigator.userAgent || "");
if (readWxLikeEnvFromWindow(typeof window !== "undefined" ? window : null)) {
return true;
}
if (typeof window !== "undefined") {
try {
if (window.top && window.top !== window && readWxLikeEnvFromWindow(window.top)) {
return true;
}
} catch (e) {
// 跨域不可读 top依赖父页 probeWxEnv / embedEnvProbe 缓存
}
}
if (resolveUseParentProxy() && getParentWxEnvReport() === true) {
return true;
}
return false;
}
export function isWxMiniProgramEnv() {
@@ -88,11 +125,14 @@ export function isWxMiniProgramEnv() {
export function isSupportWxScan() {
const wx = getWx();
return isWxMiniProgramEnv()
|| isWxEnv()
&& _wxReady
&& wx
&& wx.scanQRCode;
if (isWxMiniProgramEnv()) {
return true;
}
// 嵌入且 API 走父页时,子页未必初始化 wx只要识别为微信环境即视为支持实际能力由父页 SDK 决定)
if (resolveUseParentProxy() && isWxEnv()) {
return true;
}
return !!(isWxEnv() && _wxReady && wx && wx.scanQRCode);
}
export function initWxJssdk() {

12
types/index.d.ts vendored
View File

@@ -6,6 +6,18 @@ interface ScanConfigOptions {
* 扫码重启延迟单位毫秒默认500ms
*/
scanRestartDelay?: number,
/**
* iframe 场景下是否将 API 调用转发到父页面同名 SDKpostMessage
* - `'auto'`(默认):处于子 frame`parent !== window`)即转发,对外 API含 `startScan`)均由父页 SDK 执行
* - `true` / `'on'` / `'parent'`:存在父 window 时强制转发
* - `false` / `'off'` / `'local'`:始终在本页执行(子页自己要跑扫码时用)
*/
embedProxyMode?: 'auto' | boolean | 'on' | 'off' | 'local' | 'parent',
/**
* 请求微信 JS-SDK 签名时使用的页面 URL不含 hash
* 跨域 iframe 无法读取父页地址时需手动设为当前微信内打开的页面链接。
*/
wxJssdkSignatureUrl?: string,
/**
* 桥接是否启用,默认启用
*/