id-scanner-lib
Version:
Browser-based ID card, QR code, and face recognition scanner with liveness detection
430 lines (387 loc) • 10.1 kB
text/typescript
/**
* @file 工具集
* @description 提供通用工具函数
* @module utils
*/
export * from './error-handler';
export * from './retry';
export * from './canvas-pool';
/**
* 创建延迟Promise
* @param ms 延迟毫秒数
*/
export function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 节流函数
* @param fn 要节流的函数
* @param wait 等待时间(ms)
*/
export function throttle<T extends (...args: any[]) => any>(
fn: T,
wait: number
): (...args: Parameters<T>) => ReturnType<T> | undefined {
let lastTime = 0;
let lastResult: ReturnType<T>;
return function(this: any, ...args: Parameters<T>): ReturnType<T> | undefined {
const now = Date.now();
if (now - lastTime >= wait) {
lastTime = now;
lastResult = fn.apply(this, args);
}
return lastResult;
};
}
/**
* 防抖函数
* @param fn 要防抖的函数
* @param wait 等待时间(ms)
* @param immediate 是否立即执行
*/
export function debounce<T extends (...args: any[]) => any>(
fn: T,
wait: number,
immediate: boolean = false
): (...args: Parameters<T>) => void {
let timeout: number | null = null;
return function(this: any, ...args: Parameters<T>): void {
const callNow = immediate && !timeout;
if (timeout !== null) {
clearTimeout(timeout);
}
timeout = window.setTimeout(() => {
timeout = null;
if (!immediate) {
fn.apply(this, args);
}
}, wait);
if (callNow) {
fn.apply(this, args);
}
};
}
/**
* 格式化字节大小
* @param bytes 字节数
* @param decimals 小数位数
*/
export function formatBytes(bytes: number, decimals: number = 2): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
/**
* 生成UUID
*/
export function generateUUID(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
/**
* 检查浏览器支持的功能
*/
export const browserCapabilities = {
/**
* 检查是否支持摄像头
*/
hasCamera(): boolean {
return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
},
/**
* 检查是否支持WebAssembly
*/
hasWasm(): boolean {
return typeof WebAssembly === 'object' &&
typeof WebAssembly.compile === 'function' &&
typeof WebAssembly.instantiate === 'function';
},
/**
* 检查是否支持WebWorker
*/
hasWebWorker(): boolean {
return typeof Worker === 'function';
},
/**
* 检查是否支持WebGL
*/
hasWebGL(): boolean {
try {
const canvas = document.createElement('canvas');
return !!(
window.WebGLRenderingContext &&
(canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))
);
} catch (e) {
return false;
}
},
/**
* 检查是否支持SharedArrayBuffer
*/
hasSharedArrayBuffer(): boolean {
return typeof SharedArrayBuffer === 'function';
},
/**
* 检查是否支持特定的功能
* @param feature 功能名称
*/
supports(feature: string): boolean {
switch (feature.toLowerCase()) {
case 'camera': return this.hasCamera();
case 'wasm': return this.hasWasm();
case 'webworker': case 'worker': return this.hasWebWorker();
case 'webgl': case 'gl': return this.hasWebGL();
case 'sharedarraybuffer': case 'sab': return this.hasSharedArrayBuffer();
default: return false;
}
}
};
/**
* 数组分块
* @param array 要分块的数组
* @param chunkSize 每块大小
*/
export function chunk<T>(array: T[], chunkSize: number): T[][] {
if (chunkSize < 1) throw new Error('Chunk size must be greater than 0');
const result: T[][] = [];
for (let i = 0; i < array.length; i += chunkSize) {
result.push(array.slice(i, i + chunkSize));
}
return result;
}
/**
* 安全解析JSON
* @param text JSON字符串
* @param fallback 解析失败时的默认值
*/
export function safeParseJSON<T = any>(text: string, fallback: T): T {
try {
return JSON.parse(text) as T;
} catch (e) {
return fallback;
}
}
/**
* 限制值在指定范围内
* @param value 要限制的值
* @param min 最小值
* @param max 最大值
*/
export function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
/**
* 等待加载图片
* @param url 图片URL
*/
export function loadImage(url: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
img.src = url;
});
}
/**
* 将Blob转换为Base64
* @param blob Blob对象
*/
export function blobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(blob);
});
}
/**
* 将Base64转换为Blob
* @param base64 Base64字符串
* @param contentType 内容类型
*/
export function base64ToBlob(base64: string, contentType: string = ''): Blob {
const byteString = atob(base64.split(',')[1]);
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], { type: contentType });
}
/**
* 获取媒体约束
* @param width 宽度
* @param height 高度
* @param facingMode 前后置摄像头
* @param frameRate 帧率
*/
export function getMediaConstraints(
width: number = 1280,
height: number = 720,
facingMode: 'user' | 'environment' = 'environment',
frameRate: number = 30
): MediaStreamConstraints {
return {
video: {
width: { ideal: width },
height: { ideal: height },
facingMode: { ideal: facingMode },
frameRate: { ideal: frameRate }
},
audio: false
};
}
/**
* 检查URL是否有效
* @param url 要检查的URL
*/
export function isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch (e) {
return false;
}
}
/**
* DOM帮助函数
*/
export const dom = {
/**
* 创建元素
* @param tag 标签名
* @param attributes 属性
* @param children 子元素
*/
createElement<K extends keyof HTMLElementTagNameMap>(
tag: K,
attributes: Record<string, any> = {},
children: (string | Node)[] = []
): HTMLElementTagNameMap[K] {
const element = document.createElement(tag);
// 设置属性
Object.entries(attributes).forEach(([key, value]) => {
if (key === 'style' && typeof value === 'object') {
Object.assign(element.style, value);
} else if (key.startsWith('on') && typeof value === 'function') {
element.addEventListener(key.substring(2).toLowerCase(), value);
} else if (key === 'className') {
element.className = value;
} else {
element.setAttribute(key, value);
}
});
// 添加子元素
children.forEach(child => {
if (typeof child === 'string') {
element.appendChild(document.createTextNode(child));
} else {
element.appendChild(child);
}
});
return element;
},
/**
* 查找元素
* @param selector 选择器
* @param parent 父元素
*/
find<E extends Element = Element>(
selector: string,
parent: Document | Element = document
): E | null {
return parent.querySelector<E>(selector);
},
/**
* 查找所有元素
* @param selector 选择器
* @param parent 父元素
*/
findAll<E extends Element = Element>(
selector: string,
parent: Document | Element = document
): E[] {
return Array.from(parent.querySelectorAll<E>(selector));
},
/**
* 添加事件监听器
* @param element 元素
* @param event 事件名称
* @param handler 处理函数
* @param options 选项
*/
on<K extends keyof HTMLElementEventMap>(
element: HTMLElement,
event: K,
handler: (event: HTMLElementEventMap[K]) => any,
options?: AddEventListenerOptions
): void {
element.addEventListener(event, handler as EventListener, options);
},
/**
* 移除事件监听器
* @param element 元素
* @param event 事件名称
* @param handler 处理函数
* @param options 选项
*/
off<K extends keyof HTMLElementEventMap>(
element: HTMLElement,
event: K,
handler: (event: HTMLElementEventMap[K]) => any,
options?: EventListenerOptions
): void {
element.removeEventListener(event, handler as EventListener, options);
},
/**
* 设置样式
* @param element 元素
* @param styles 样式对象
*/
setStyles(
element: HTMLElement,
styles: Partial<CSSStyleDeclaration>
): void {
Object.assign(element.style, styles);
},
/**
* 添加类名
* @param element 元素
* @param classNames 类名
*/
addClass(
element: HTMLElement,
...classNames: string[]
): void {
element.classList.add(...classNames);
},
/**
* 移除类名
* @param element 元素
* @param classNames 类名
*/
removeClass(
element: HTMLElement,
...classNames: string[]
): void {
element.classList.remove(...classNames);
},
/**
* 判断是否包含类名
* @param element 元素
* @param className 类名
*/
hasClass(
element: HTMLElement,
className: string
): boolean {
return element.classList.contains(className);
}
};