嵌入模式识别
This commit is contained in:
12
dist/index.d.ts
vendored
12
dist/index.d.ts
vendored
@@ -6,6 +6,18 @@ interface ScanConfigOptions {
|
||||
* 扫码重启延迟,单位:毫秒,默认500ms
|
||||
*/
|
||||
scanRestartDelay?: number,
|
||||
/**
|
||||
* iframe 场景下是否将 API 调用转发到父页面同名 SDK(postMessage)。
|
||||
* - `'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
2
dist/index.js
vendored
File diff suppressed because one or more lines are too long
147
src/_export.js
147
src/_export.js
@@ -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;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,13 @@ let _defineConfig = {
|
||||
}
|
||||
|
||||
let _defConfig = {
|
||||
/**
|
||||
* iframe / 嵌入场景下是否把调用转发到父页面的同名 SDK(postMessage)。
|
||||
* - 'auto'(默认):只要处于子 frame(parent !== window)即转发,含 startScan 等均走父页逻辑
|
||||
* - true | 'on' | 'parent':在存在父 window 时强制转发
|
||||
* - false | 'off' | 'local':始终在本页执行(子页自己要跑扫码时用)
|
||||
*/
|
||||
embedProxyMode: 'auto',
|
||||
}
|
||||
|
||||
let _customConfig = {
|
||||
|
||||
40
src/services/embedEnvProbe.js
Normal file
40
src/services/embedEnvProbe.js
Normal 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;
|
||||
}
|
||||
37
src/services/embedProxy.js
Normal file
37
src/services/embedProxy.js
Normal 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;
|
||||
}
|
||||
@@ -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
12
types/index.d.ts
vendored
@@ -6,6 +6,18 @@ interface ScanConfigOptions {
|
||||
* 扫码重启延迟,单位:毫秒,默认500ms
|
||||
*/
|
||||
scanRestartDelay?: number,
|
||||
/**
|
||||
* iframe 场景下是否将 API 调用转发到父页面同名 SDK(postMessage)。
|
||||
* - `'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,
|
||||
/**
|
||||
* 桥接是否启用,默认启用
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user