This commit is contained in:
iqudoo
2026-05-26 02:37:01 +08:00
parent c08cdaee68
commit cdec1838ac
14 changed files with 1788 additions and 267 deletions

View File

@@ -1,12 +1,19 @@
import './polyfill';
import {
supportList,
onScanListener, offScanListener, setStatusListener, getStatus,
startScan, stopScan, scanVideo, scanImage, clear
onScanListener, offScanListener,
onScanErrorListener, offScanErrorListener,
setStatusListener, getStatus,
startScan, stopScan, scanImage, scanImageFromFile, clear
} from './services/provider/scan';
import { setConfig, getVersion } from './services/config';
import { initWxJssdk } from './services/wx';
import { printDebug } from './utils/logger';
import {
isSupportWebScan,
prepareWebScanBarcodeDetector,
isSupportImageScan
} from './services/web';
import { printDebug, printWarn } from './utils/logger';
let _readyPromise = null;
let _calledReady = false;
@@ -23,18 +30,25 @@ function config(config) {
return _readyPromise;
}
_readyPromise = Promise.resolve().then(() => {
return initWxJssdk().catch(err => {
printDebug('init wx jssdk failed:', err && err.message ? err.message : err);
});
}).then(() => {
printDebug('-------------------------------------');
printDebug('sdk_version:', getVersion());
printDebug('support_list:', supportList.map(item => item.name + ':' + item.support).join(', '));
printDebug('-------------------------------------');
initWxJssdk();
return Promise.resolve().then(() => {
_calledReady = true;
}).catch(err => {
_readyPromise = null;
throw err;
});
})
if (isSupportWebScan() || isSupportImageScan()) {
return prepareWebScanBarcodeDetector().catch(err => {
printWarn('prepare barcode detector failed:', err);
});
}
}).then(() => {
_calledReady = true;
}).catch(err => {
_readyPromise = null;
throw err;
});
return _readyPromise;
}
@@ -42,11 +56,13 @@ export default Object.assign({}, {
config,
onScanListener,
offScanListener,
onScanErrorListener,
offScanErrorListener,
setStatusListener,
getStatus,
startScan,
stopScan,
scanVideo,
scanImage,
scanImageFromFile,
clear,
});

View File

@@ -13,8 +13,12 @@ import { createUUID } from "./utils/uuid";
import {
dispatchEmbedScanResult,
acknowledgeEmbedScanConsumed,
dispatchEmbedScanError,
} from "./services/provider/scan";
import { setEmbedScanResultForwarder } from "./services/embedScanBridge";
import {
setEmbedScanResultForwarder,
setEmbedScanErrorForwarder,
} from "./services/embedScanBridge";
const EMBED_SOURCE = "IScanEmbed";
const EMBED_V = 1;
@@ -25,6 +29,8 @@ const embedChildSources = new Set();
const EMBED_LISTENER_METHODS = new Set([
"onScanListener",
"offScanListener",
"onScanErrorListener",
"offScanErrorListener",
"clear",
]);
@@ -105,6 +111,26 @@ function hydrateEmbedParams(params, messageSource, targetOrigin) {
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) {
if (embedChildSources.size === 0 || result == null || result === "") {
return;
@@ -198,6 +224,12 @@ function embedChildOnMessage(ev) {
}
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) {
@@ -307,6 +339,7 @@ export function installEmbedHost(lib) {
}
embedHostInstalled = true;
setEmbedScanResultForwarder(broadcastScanResultToEmbedChildren);
setEmbedScanErrorForwarder(broadcastScanErrorToEmbedChildren);
window.addEventListener("message", (ev) => {
const data = ev.data;
if (!isEmbedMessage(data)) {

View File

@@ -1,9 +1,15 @@
import './polyfill';
import core from "./_core";
import _global from './polyfill/_global';
import { exportSDK, installEmbedHost } from './_export';
import { setSdkScriptSrc } from './services/web';
if (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) {
setSdkScriptSrc(document.currentScript.src);
}
const IScan = exportSDK(core, null, "config", "setStatusListener", "onScanListener",
"offScanListener", "stopScan", "startScan", "scanImage", "clear");
"offScanListener", "onScanErrorListener", "offScanErrorListener", "stopScan", "startScan", "scanImage", "scanImageFromFile", "clear");
installEmbedHost(core);

View File

@@ -1,5 +1,12 @@
import { polyfill } from 'es6-promise';
// Object.hasOwn (ES2022) — barcode-detector / ZXing 依赖,旧版 WebView 无此方法
if (typeof Object.hasOwn !== 'function') {
Object.hasOwn = function hasOwn(object, key) {
return Object.prototype.hasOwnProperty.call(object, key);
};
}
// Object.assign
if (typeof Object.assign != 'function') {
// Must be writable: true, enumerable: false, configurable: true

View File

@@ -1,12 +1,24 @@
/** 父页识别到扫码结果时,向嵌入 iframe 转发的回调(由 installEmbedHost 注册) */
let embedScanResultForwarder = null;
/** 父页图片识别失败时,向嵌入 iframe 转发的回调 */
let embedScanErrorForwarder = null;
export function setEmbedScanResultForwarder(fn) {
embedScanResultForwarder = typeof fn === "function" ? fn : null;
}
export function setEmbedScanErrorForwarder(fn) {
embedScanErrorForwarder = typeof fn === "function" ? fn : null;
}
export function forwardEmbedScanResultIfNeeded(result) {
if (embedScanResultForwarder && result != null && result !== "") {
embedScanResultForwarder(result);
}
}
export function forwardEmbedScanErrorIfNeeded(error) {
if (embedScanErrorForwarder && error != null && error !== "") {
embedScanErrorForwarder(error);
}
}

View File

@@ -7,28 +7,83 @@ import {
startScanForImage,
unlockScanBeep,
playScanBeep,
isWebScanImageFallbackEnabled
isWebScanImageFallbackEnabled,
canUseWebCameraScan,
shouldSkipWebCameraProbe,
cleanupWebScanResiduals,
detectImageFileForScan
} from "../web";
import { isSupportWxScan, startScanForWx } from "../wx";
import { isSupportWxScan, startScanForWx, isWxEnv } from "../wx";
import { startScanner, stopScanner } from "../scanner";
import { getConfig } from "../config";
import { toAny } from "../../utils/toany";
import { printDebug } from "../../utils/logger";
import { forwardEmbedScanResultIfNeeded } from "../embedScanBridge";
import { printDebug, printWarn } from "../../utils/logger";
import { forwardEmbedScanResultIfNeeded, forwardEmbedScanErrorIfNeeded } from "../embedScanBridge";
let _scan_status = "ready";
let _scan_status_listener = null;
let _scan_listener_list = [];
let _scan_error_listener_list = [];
let _scan_resolve = null;
let _scan_closing = false;
let _scan_next_start_time = 0;
const SCAN_RESTART_DELAY = 500;
const BRIDGE_SCAN_TIMEOUT = 5000;
const SCAN_SESSION_TIMEOUT = 90000;
function getScanRestartDelay() {
return toAny(getConfig("scanRestartDelay"), SCAN_RESTART_DELAY);
}
function getBridgeScanTimeout() {
const timeout = getConfig("bridgeScanTimeout");
return typeof timeout === "number" && timeout > 0 ? timeout : BRIDGE_SCAN_TIMEOUT;
}
function getScanSessionTimeout() {
const timeout = getConfig("scanSessionTimeout");
return typeof timeout === "number" && timeout > 0 ? timeout : SCAN_SESSION_TIMEOUT;
}
function withScanSessionTimeout(scanPromise) {
return Promise.race([
scanPromise,
new Promise(resolve => {
setTimeout(() => {
printWarn("scan session timeout");
resolve({ cancel: 1, scanTimeout: true });
}, getScanSessionTimeout());
})
]);
}
function __fallbackScanAfterBridgeFailure(err) {
if (!isScanning()) {
throw err;
}
printWarn("bridge scan unavailable, fallback:", err);
return __startNonBridgeScan(err);
}
function __fallbackScanAfterWxFailure(err) {
if (!isScanning()) {
throw err;
}
printWarn("wx scan unavailable, fallback to web/image scan:", err);
return __startNonBridgeScan(err);
}
function __startNonBridgeScan(err) {
if (isSupportWebScan()) {
return __startWebScan();
}
if (isSupportImageScan()) {
return __startImageScan();
}
throw err;
}
function parseBarcodeString(input) {
// 标准化的类型映射表:将各种变体映射到统一标识
// 这样即使传入 EAN_13、EAN-13、EAN13 都能匹配成功
@@ -120,6 +175,68 @@ function __result(result) {
return matched;
}
function normalizeScanError(raw) {
if (typeof raw === "string") {
return raw;
}
if (raw && raw.error != null && raw.error !== "") {
return String(raw.error);
}
if (raw && raw.message) {
return String(raw.message);
}
if (raw == null || raw === "") {
return "";
}
return String(raw);
}
function __scanError(raw, meta) {
const error = normalizeScanError(raw);
if (!error) {
return false;
}
forwardEmbedScanErrorIfNeeded(error);
let matched = false;
for (let i = 0; i < _scan_error_listener_list.length; i++) {
const item = _scan_error_listener_list[i];
if (item.listener && __match(error, item.match)) {
matched = true;
item.listener(Object.assign({
error,
key: item.key
}, meta || {}));
break;
}
}
return matched;
}
function __notifyImageScanFailure(raw, meta) {
if (raw && raw.cancel) {
return false;
}
const error = normalizeScanError(raw);
const notified = __scanError(raw, Object.assign({ source: "image" }, meta || {}));
if (!notified && error) {
printWarn("image scan failed:", error);
}
return notified;
}
function __notifyWebScanFailure(raw, meta) {
if (raw && raw.cancel) {
return false;
}
const payload = raw && raw.error != null ? raw.error : raw;
const error = normalizeScanError(payload);
const notified = __scanError(payload, Object.assign({ source: "web" }, meta || {}));
if (!notified && error) {
printWarn("web scan failed:", error);
}
return notified;
}
function __hasMatchedListener(result) {
for (let i = 0; i < _scan_listener_list.length; i++) {
const item = _scan_listener_list[i];
@@ -178,6 +295,15 @@ export function dispatchEmbedScanResult(raw) {
return __scannerResult(result);
}
/**
* 父页通过 postMessage 将识别错误投递到嵌入 iframe 时调用。
* @returns {boolean} 是否有监听消费了该错误
*/
export function dispatchEmbedScanError(raw) {
const error = normalizeScanError(raw);
return __scanError(error, { source: "image" });
}
/**
* 嵌入 iframe 已消费扫码结果时通知父页结束当前识别(关闭摄像头/UI 等)。
*/
@@ -222,7 +348,7 @@ function __scannerResult(result) {
function __startBridgeScan() {
return bridgeAsync("startScan", {
closeable: true
}).then(resp => {
}, getBridgeScanTimeout()).then(resp => {
if (!isScanning()) {
return resp;
}
@@ -238,10 +364,10 @@ function __startBridgeScan() {
return resp;
}).catch(err => {
if (!isScanning()) {
return err;
throw err;
}
if (!err || !err.result) {
return err;
throw err;
}
if (__result(err.result)) {
return err;
@@ -261,6 +387,9 @@ function __startWxScan() {
if (!isScanning()) {
return resp;
}
if (resp && resp.error && !resp.result) {
throw resp.error;
}
if (!resp || !resp.result) {
return resp;
}
@@ -270,43 +399,50 @@ function __startWxScan() {
return resp;
}).catch(err => {
if (!isScanning()) {
throw err;
}
if (err && err.result) {
if (__result(err.result)) {
return err;
}
return err;
}
if (!err || !err.result) {
return err;
}
if (__result(err.result)) {
return err;
}
return err;
return __fallbackScanAfterWxFailure(err);
});
}
function __startWebScan() {
return startScanForWeb(__result).then(resp => {
function __startWebScan(useImageScan = false) {
return startScanForWeb(__result, __scanError).then(resp => {
if (!isScanning()) {
return resp;
}
if (!resp || !resp.result) {
if (resp && resp.success === false) {
__notifyWebScanFailure(resp);
}
return resp;
}
if (__result(resp.result)) {
return resp;
}
if (isScanning()) {
return __startWebScan();
}
__result(resp.result);
return resp;
}).catch(err => {
if (!isScanning()) {
return err;
}
if (err && err.cancel || err && err.imageFallbackUsed) {
if (err && err.cancel) {
return err;
}
if (isWebScanImageFallbackEnabled()) {
if (err && err.scanTimeout) {
return err;
}
if (err && err.imageFallbackUsed) {
__notifyWebScanFailure(err);
return err;
}
if (isWebScanImageFallbackEnabled() && useImageScan) {
return __startImageScan();
}
__notifyWebScanFailure(err);
printWarn("web scan failed:", err);
return err;
});
}
@@ -316,29 +452,27 @@ function __startImageScan() {
if (!isScanning()) {
return resp;
}
if (resp && resp.cancel) {
return resp;
}
if (!resp || !resp.result) {
__notifyImageScanFailure(resp);
return resp;
}
if (__result(resp.result)) {
return resp;
}
if (isScanning()) {
return __startImageScan();
}
__result(resp.result);
return resp;
}).catch(err => {
if (!isScanning()) {
return err;
}
if (!err || !err.result) {
if (err && err.cancel) {
return err;
}
if (__result(err.result)) {
if (err && err.result) {
__result(err.result);
return err;
}
if (isScanning()) {
return __startImageScan();
}
__notifyImageScanFailure(err);
return err;
});
}
@@ -353,6 +487,11 @@ export function clear() {
item.cancel();
}
_scan_listener_list.length = 0;
for (let i = 0; i < _scan_error_listener_list.length; i++) {
const item = _scan_error_listener_list[i];
item.cancel();
}
_scan_error_listener_list.length = 0;
__checkScanner();
}
@@ -416,6 +555,68 @@ export function offScanListener(listener) {
__checkScanner();
}
function createScanErrorListenerItem(listener, key, match, level) {
const item = {
key,
match: match || "",
level: level || 0,
listener: listener,
cancel: () => {
const index = _scan_error_listener_list.indexOf(item);
if (index !== -1) {
const items = _scan_error_listener_list.splice(index, 1);
for (let i = 0; i < items.length; i++) {
const removed = items[i];
removed.listener && removed.listener({ cancel: 1, key: removed.key });
}
}
}
};
return item;
}
export function onScanErrorListener(listener, key, match, level) {
if (!key || typeof key !== 'string') {
return;
}
if (typeof listener !== 'function') {
return;
}
let item = null;
for (let i = 0; i < _scan_error_listener_list.length; i++) {
const _i = _scan_error_listener_list[i];
if (_i.key === key) {
item = _i;
break;
}
}
if (item) {
item.level = level;
item.match = match;
item.listener = listener;
} else {
item = createScanErrorListenerItem(listener, key, match, level);
_scan_error_listener_list.push(item);
}
_scan_error_listener_list.sort((a, b) => b.level - a.level);
return item;
}
export function offScanErrorListener(listener) {
for (let i = 0; i < _scan_error_listener_list.length; i++) {
const _i = _scan_error_listener_list[i];
if (typeof listener === 'string') {
if (_i.key === listener) {
_i.cancel();
break;
}
} else if (_i.listener === listener) {
_i.cancel();
break;
}
}
}
export function setStatusListener(listener) {
if (typeof listener !== 'function') {
return;
@@ -428,11 +629,15 @@ export function getStatus() {
}
export function stopScan() {
cleanupWebScanResiduals();
if (!isScanning()) {
return;
}
const resolve = __finishScan();
_scan_closing = true;
__stopCurrentScan().then(() => {
__closed();
resolve && resolve({ cancel: 1 });
_scan_closing = false;
});
}
@@ -448,45 +653,23 @@ export function startScan() {
});
let scanPromise = Promise.resolve();
if (inRuntime()) {
scanPromise = __startBridgeScan();
scanPromise = __startBridgeScan().catch(__fallbackScanAfterBridgeFailure);
} else if (isSupportWxScan()) {
scanPromise = __startWxScan();
} else if (isSupportWebScan()) {
scanPromise = __startWebScan();
scanPromise = __startWebScan(true);
} else if (isSupportImageScan()) {
scanPromise = __startImageScan();
} else {
printDebug("Not support scanner");
printWarn("Not support scanner");
}
return Promise.race([scanPromise, scannerPromise]);
return withScanSessionTimeout(Promise.race([scanPromise, scannerPromise]));
}).finally(() => {
_scan_resolve = null;
__closed();
});
}
export function scanVideo() {
if (!isSupportWebScan()) {
printDebug("Not support video scanner");
return;
}
if (isScanning() || _scan_closing || Date.now() < _scan_next_start_time) {
return;
}
unlockScanBeep();
Promise.resolve().then(() => {
__scanning();
return startScanForWeb(__result).then(resp => {
if (resp && resp.result) {
__result(resp.result);
}
throw resp.error;
}).catch(err => { });
}).finally(() => {
__closed();
});
}
export function scanImage() {
if (!isSupportImageScan()) {
printDebug("Not support image scanner");
@@ -496,14 +679,49 @@ export function scanImage() {
return;
}
unlockScanBeep();
Promise.resolve().then(() => {
__scanning();
return startScanForImage().then(resp => {
if (resp && resp.result) {
__result(resp.result);
__scanning();
withScanSessionTimeout(__startImageScan()).catch(err => {
if (err && err.cancel) {
return;
}
__notifyImageScanFailure(err);
}).finally(() => {
__closed();
});
}
/** 由原生/业务传入已选图片 File避免 WebView 无法从 input.files 取文件 */
export function scanImageFromFile(file) {
if (!isSupportImageScan()) {
printDebug("Not support image scanner");
return;
}
if (isScanning() || _scan_closing || Date.now() < _scan_next_start_time) {
return;
}
unlockScanBeep();
cleanupWebScanResiduals();
__scanning();
withScanSessionTimeout(
detectImageFileForScan(file).then(resp => {
if (!isScanning()) {
return resp;
}
throw resp.error;
}).catch(err => { });
if (resp && resp.cancel) {
return resp;
}
if (!resp || !resp.result) {
__notifyImageScanFailure(resp);
return resp;
}
__result(resp.result);
return resp;
})
).catch(err => {
if (err && err.cancel) {
return;
}
__notifyImageScanFailure(err);
}).finally(() => {
__closed();
});
@@ -512,19 +730,30 @@ export function scanImage() {
export const supportList = [
{
name: "bridge",
support: !!inRuntime()
get support() {
return !!inRuntime();
}
},
{
name: "wx",
support: !!isSupportWxScan()
get support() {
return !!isSupportWxScan();
}
},
{
name: "web",
support: !!isSupportWebScan()
get support() {
if (shouldSkipWebCameraProbe()) {
return false;
}
return !!canUseWebCameraScan();
}
},
{
name: "image",
support: !!isSupportImageScan()
get support() {
return !!isSupportImageScan();
}
},
{
name: "scanner",

File diff suppressed because it is too large Load Diff