嵌入模式识别
This commit is contained in:
12
dist/index.d.ts
vendored
12
dist/index.d.ts
vendored
@@ -6,6 +6,18 @@ interface ScanConfigOptions {
|
|||||||
* 扫码重启延迟,单位:毫秒,默认500ms
|
* 扫码重启延迟,单位:毫秒,默认500ms
|
||||||
*/
|
*/
|
||||||
scanRestartDelay?: number,
|
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 { 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 { createUUID } from "./utils/uuid";
|
||||||
|
|
||||||
const EMBED_SOURCE = "IScanEmbed";
|
const EMBED_SOURCE = "IScanEmbed";
|
||||||
const EMBED_V = 1;
|
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) {
|
function isEmbedMessage(data) {
|
||||||
return data && data.source === EMBED_SOURCE && data.v === EMBED_V;
|
return data && data.source === EMBED_SOURCE && data.v === EMBED_V;
|
||||||
}
|
}
|
||||||
@@ -120,11 +119,62 @@ function deserializeEmbedInvokeResult(methodKey, raw) {
|
|||||||
return 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) {
|
function embedChildOnMessage(ev) {
|
||||||
const data = ev.data;
|
const data = ev.data;
|
||||||
if (!isEmbedMessage(data)) {
|
if (!isEmbedMessage(data)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (data.kind === "probeWxEnvResult") {
|
||||||
|
setParentWxEnvReport(!!data.wx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (data.kind === "invokeResult") {
|
if (data.kind === "invokeResult") {
|
||||||
const pending = pendingInvokes[data.id];
|
const pending = pendingInvokes[data.id];
|
||||||
if (!pending) {
|
if (!pending) {
|
||||||
@@ -147,7 +197,7 @@ function embedChildOnMessage(ev) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureEmbedChildListener() {
|
function ensureEmbedChildListener() {
|
||||||
if (embedChildInstalled || typeof window === "undefined" || !isEmbedded()) {
|
if (embedChildInstalled || typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
embedChildInstalled = true;
|
embedChildInstalled = true;
|
||||||
@@ -156,6 +206,7 @@ function ensureEmbedChildListener() {
|
|||||||
|
|
||||||
function embedInvoke(methodKey, params) {
|
function embedInvoke(methodKey, params) {
|
||||||
ensureEmbedChildListener();
|
ensureEmbedChildListener();
|
||||||
|
scheduleEmbedWxEnvProbeIfNeeded();
|
||||||
const id = createUUID();
|
const id = createUUID();
|
||||||
const { serialized, registry } = serializeEmbedParams(params);
|
const { serialized, registry } = serializeEmbedParams(params);
|
||||||
Object.keys(registry).forEach((cbId) => {
|
Object.keys(registry).forEach((cbId) => {
|
||||||
@@ -235,7 +286,27 @@ export function installEmbedHost(lib) {
|
|||||||
embedHostInstalled = true;
|
embedHostInstalled = true;
|
||||||
window.addEventListener("message", (ev) => {
|
window.addEventListener("message", (ev) => {
|
||||||
const data = ev.data;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (!data.id || !data.methodKey) {
|
if (!data.id || !data.methodKey) {
|
||||||
@@ -300,6 +371,42 @@ function freezeObj(obj) {
|
|||||||
Object.freeze(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) {
|
export function exportSDK(lib, funcs, ...initNames) {
|
||||||
let methods = {};
|
let methods = {};
|
||||||
if (funcs && typeof funcs === 'object') {
|
if (funcs && typeof funcs === 'object') {
|
||||||
@@ -311,19 +418,9 @@ export function exportSDK(lib, funcs, ...initNames) {
|
|||||||
Object.keys(methods).forEach(method => {
|
Object.keys(methods).forEach(method => {
|
||||||
let methodItem = methods[method];
|
let methodItem = methods[method];
|
||||||
let methodName = methodItem && methodItem.method || method;
|
let methodName = methodItem && methodItem.method || method;
|
||||||
hook(library, method, (...params) => {
|
hook(library, method, createInvokeTransport(lib, method, methodName, initNames));
|
||||||
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()) {
|
scheduleEmbedWxEnvProbeIfNeeded();
|
||||||
ensureEmbedChildListener();
|
|
||||||
}
|
|
||||||
freezeObj(library);
|
freezeObj(library);
|
||||||
return library;
|
return library;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ let _defineConfig = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _defConfig = {
|
let _defConfig = {
|
||||||
|
/**
|
||||||
|
* iframe / 嵌入场景下是否把调用转发到父页面的同名 SDK(postMessage)。
|
||||||
|
* - 'auto'(默认):只要处于子 frame(parent !== window)即转发,含 startScan 等均走父页逻辑
|
||||||
|
* - true | 'on' | 'parent':在存在父 window 时强制转发
|
||||||
|
* - false | 'off' | 'local':始终在本页执行(子页自己要跑扫码时用)
|
||||||
|
*/
|
||||||
|
embedProxyMode: 'auto',
|
||||||
}
|
}
|
||||||
|
|
||||||
let _customConfig = {
|
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 { getConfig } from "../config";
|
||||||
|
import { readWxLikeEnvFromWindow, getParentWxEnvReport } from "../embedEnvProbe";
|
||||||
|
import { resolveUseParentProxy } from "../embedProxy";
|
||||||
import { request } from "../../utils/request";
|
import { request } from "../../utils/request";
|
||||||
import { toAny } from "../../utils/toany";
|
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() {
|
function fetchWxConfig() {
|
||||||
let initWechatJssdk = toAny(getConfig("initWechatJssdk"), {});
|
let initWechatJssdk = toAny(getConfig("initWechatJssdk"), {});
|
||||||
if (!!initWechatJssdk.sdkConfig) {
|
if (!!initWechatJssdk.sdkConfig) {
|
||||||
@@ -59,7 +82,7 @@ function fetchWxConfig() {
|
|||||||
url: apiUrl,
|
url: apiUrl,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
data: {
|
data: {
|
||||||
url: window.location.href.split("#")[0]
|
url: getPageUrlForWxJssdkSignature()
|
||||||
}
|
}
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
let data = toAny(res.data, {});
|
let data = toAny(res.data, {});
|
||||||
@@ -77,8 +100,22 @@ function fetchWxConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isWxEnv() {
|
export function isWxEnv() {
|
||||||
return typeof navigator !== "undefined"
|
if (readWxLikeEnvFromWindow(typeof window !== "undefined" ? window : null)) {
|
||||||
&& /micromessenger/i.test(navigator.userAgent || "");
|
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() {
|
export function isWxMiniProgramEnv() {
|
||||||
@@ -88,11 +125,14 @@ export function isWxMiniProgramEnv() {
|
|||||||
|
|
||||||
export function isSupportWxScan() {
|
export function isSupportWxScan() {
|
||||||
const wx = getWx();
|
const wx = getWx();
|
||||||
return isWxMiniProgramEnv()
|
if (isWxMiniProgramEnv()) {
|
||||||
|| isWxEnv()
|
return true;
|
||||||
&& _wxReady
|
}
|
||||||
&& wx
|
// 嵌入且 API 走父页时,子页未必初始化 wx;只要识别为微信环境即视为支持(实际能力由父页 SDK 决定)
|
||||||
&& wx.scanQRCode;
|
if (resolveUseParentProxy() && isWxEnv()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !!(isWxEnv() && _wxReady && wx && wx.scanQRCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initWxJssdk() {
|
export function initWxJssdk() {
|
||||||
|
|||||||
12
types/index.d.ts
vendored
12
types/index.d.ts
vendored
@@ -6,6 +6,18 @@ interface ScanConfigOptions {
|
|||||||
* 扫码重启延迟,单位:毫秒,默认500ms
|
* 扫码重启延迟,单位:毫秒,默认500ms
|
||||||
*/
|
*/
|
||||||
scanRestartDelay?: number,
|
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