278 lines
8.8 KiB
TypeScript
278 lines
8.8 KiB
TypeScript
|
|
import type {
|
|||
|
|
IPlatformAdapter,
|
|||
|
|
PlatformWorker,
|
|||
|
|
WorkerCreationOptions,
|
|||
|
|
PlatformConfig,
|
|||
|
|
BrowserDeviceInfo
|
|||
|
|
} from './IPlatformAdapter';
|
|||
|
|
import { createLogger, type ILogger } from '../Utils/Logger';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 浏览器平台适配器
|
|||
|
|
* 支持标准Web API的浏览器环境
|
|||
|
|
*/
|
|||
|
|
export class BrowserAdapter implements IPlatformAdapter {
|
|||
|
|
public readonly name = 'browser';
|
|||
|
|
public readonly version = this.getBrowserVersion();
|
|||
|
|
private readonly logger: ILogger;
|
|||
|
|
|
|||
|
|
constructor() {
|
|||
|
|
this.logger = createLogger('BrowserAdapter');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查是否支持Worker
|
|||
|
|
*/
|
|||
|
|
public isWorkerSupported(): boolean {
|
|||
|
|
return typeof Worker !== 'undefined' && typeof Blob !== 'undefined' && typeof URL !== 'undefined';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查是否支持SharedArrayBuffer
|
|||
|
|
*/
|
|||
|
|
public isSharedArrayBufferSupported(): boolean {
|
|||
|
|
return typeof SharedArrayBuffer !== 'undefined' &&
|
|||
|
|
typeof self !== 'undefined' &&
|
|||
|
|
self.crossOriginIsolated === true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取硬件并发数
|
|||
|
|
*/
|
|||
|
|
public getHardwareConcurrency(): number {
|
|||
|
|
if (typeof navigator !== 'undefined' && navigator.hardwareConcurrency) {
|
|||
|
|
return navigator.hardwareConcurrency;
|
|||
|
|
}
|
|||
|
|
return 4; // 默认值
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 创建Worker
|
|||
|
|
*/
|
|||
|
|
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
|||
|
|
const blob = new Blob([script], { type: 'application/javascript' });
|
|||
|
|
const scriptURL = URL.createObjectURL(blob);
|
|||
|
|
|
|||
|
|
const worker = new Worker(scriptURL, {
|
|||
|
|
type: options.type || 'classic',
|
|||
|
|
credentials: options.credentials || 'same-origin',
|
|||
|
|
name: options.name
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return new BrowserWorker(worker, scriptURL);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 创建SharedArrayBuffer
|
|||
|
|
*/
|
|||
|
|
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
|||
|
|
if (!this.isSharedArrayBufferSupported()) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
return new SharedArrayBuffer(length);
|
|||
|
|
} catch (error) {
|
|||
|
|
this.logger.warn('Failed to create SharedArrayBuffer:', error);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取高精度时间戳
|
|||
|
|
*/
|
|||
|
|
public getHighResTimestamp(): number {
|
|||
|
|
if (typeof performance !== 'undefined' && performance.now) {
|
|||
|
|
return performance.now();
|
|||
|
|
}
|
|||
|
|
return Date.now();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取平台配置
|
|||
|
|
*/
|
|||
|
|
public getPlatformConfig(): PlatformConfig {
|
|||
|
|
return {
|
|||
|
|
maxWorkerCount: this.getHardwareConcurrency(),
|
|||
|
|
supportsModuleWorker: this.supportsModuleWorker(),
|
|||
|
|
supportsTransferableObjects: this.supportsTransferableObjects(),
|
|||
|
|
maxSharedArrayBufferSize: this.getMaxSharedArrayBufferSize(),
|
|||
|
|
workerScriptPrefix: '',
|
|||
|
|
limitations: {
|
|||
|
|
noEval: false,
|
|||
|
|
requiresWorkerInit: false
|
|||
|
|
},
|
|||
|
|
extensions: {
|
|||
|
|
supportsServiceWorker: typeof navigator !== 'undefined' && 'serviceWorker' in navigator,
|
|||
|
|
supportsWebAssembly: typeof WebAssembly !== 'undefined'
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取浏览器设备信息
|
|||
|
|
*/
|
|||
|
|
public getDeviceInfo(): BrowserDeviceInfo {
|
|||
|
|
try {
|
|||
|
|
const deviceInfo: BrowserDeviceInfo = {};
|
|||
|
|
|
|||
|
|
// 浏览器基础信息
|
|||
|
|
if (typeof navigator !== 'undefined') {
|
|||
|
|
deviceInfo.userAgent = navigator.userAgent;
|
|||
|
|
deviceInfo.vendor = navigator.vendor;
|
|||
|
|
deviceInfo.language = navigator.language;
|
|||
|
|
deviceInfo.languages = navigator.languages as string[];
|
|||
|
|
deviceInfo.cookieEnabled = navigator.cookieEnabled;
|
|||
|
|
deviceInfo.onLine = navigator.onLine;
|
|||
|
|
deviceInfo.doNotTrack = navigator.doNotTrack;
|
|||
|
|
deviceInfo.platform = navigator.platform;
|
|||
|
|
deviceInfo.appVersion = navigator.appVersion;
|
|||
|
|
deviceInfo.appName = navigator.appName;
|
|||
|
|
|
|||
|
|
// 硬件信息
|
|||
|
|
deviceInfo.hardwareConcurrency = navigator.hardwareConcurrency;
|
|||
|
|
deviceInfo.maxTouchPoints = navigator.maxTouchPoints;
|
|||
|
|
|
|||
|
|
// 设备内存(实验性API)
|
|||
|
|
if ('deviceMemory' in navigator) {
|
|||
|
|
deviceInfo.deviceMemory = (navigator as any).deviceMemory;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 网络连接信息(实验性API)
|
|||
|
|
if ('connection' in navigator) {
|
|||
|
|
const connection = (navigator as any).connection;
|
|||
|
|
deviceInfo.connection = {
|
|||
|
|
effectiveType: connection.effectiveType,
|
|||
|
|
downlink: connection.downlink,
|
|||
|
|
rtt: connection.rtt,
|
|||
|
|
saveData: connection.saveData
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 屏幕信息
|
|||
|
|
if (typeof screen !== 'undefined') {
|
|||
|
|
deviceInfo.screenWidth = screen.width;
|
|||
|
|
deviceInfo.screenHeight = screen.height;
|
|||
|
|
deviceInfo.availWidth = screen.availWidth;
|
|||
|
|
deviceInfo.availHeight = screen.availHeight;
|
|||
|
|
deviceInfo.colorDepth = screen.colorDepth;
|
|||
|
|
deviceInfo.pixelDepth = screen.pixelDepth;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 窗口信息
|
|||
|
|
if (typeof window !== 'undefined') {
|
|||
|
|
deviceInfo.innerWidth = window.innerWidth;
|
|||
|
|
deviceInfo.innerHeight = window.innerHeight;
|
|||
|
|
deviceInfo.outerWidth = window.outerWidth;
|
|||
|
|
deviceInfo.outerHeight = window.outerHeight;
|
|||
|
|
deviceInfo.devicePixelRatio = window.devicePixelRatio;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return deviceInfo;
|
|||
|
|
} catch (error) {
|
|||
|
|
this.logger.warn('获取浏览器设备信息失败', error);
|
|||
|
|
return {} as BrowserDeviceInfo;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取浏览器版本信息
|
|||
|
|
*/
|
|||
|
|
private getBrowserVersion(): string {
|
|||
|
|
if (typeof navigator !== 'undefined') {
|
|||
|
|
return navigator.userAgent;
|
|||
|
|
}
|
|||
|
|
return 'unknown';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查是否支持模块Worker
|
|||
|
|
*/
|
|||
|
|
private supportsModuleWorker(): boolean {
|
|||
|
|
try {
|
|||
|
|
// 尝试创建一个简单的模块Worker来测试支持
|
|||
|
|
const blob = new Blob(['export {};'], { type: 'application/javascript' });
|
|||
|
|
const url = URL.createObjectURL(blob);
|
|||
|
|
const worker = new Worker(url, { type: 'module' });
|
|||
|
|
worker.terminate();
|
|||
|
|
URL.revokeObjectURL(url);
|
|||
|
|
return true;
|
|||
|
|
} catch {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查是否支持Transferable Objects
|
|||
|
|
*/
|
|||
|
|
private supportsTransferableObjects(): boolean {
|
|||
|
|
try {
|
|||
|
|
const buffer = new ArrayBuffer(8);
|
|||
|
|
const blob = new Blob(['self.postMessage(null);'], { type: 'application/javascript' });
|
|||
|
|
const url = URL.createObjectURL(blob);
|
|||
|
|
const worker = new Worker(url);
|
|||
|
|
worker.postMessage(buffer, [buffer]);
|
|||
|
|
worker.terminate();
|
|||
|
|
URL.revokeObjectURL(url);
|
|||
|
|
return buffer.byteLength === 0; // Buffer应该被transfer走
|
|||
|
|
} catch {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取SharedArrayBuffer最大大小限制
|
|||
|
|
*/
|
|||
|
|
private getMaxSharedArrayBufferSize(): number {
|
|||
|
|
// 浏览器环境返回保守的默认值
|
|||
|
|
// 大多数现代浏览器都支持较大的SharedArrayBuffer
|
|||
|
|
return 256 * 1024 * 1024; // 256MB,适合大多数ECS应用场景
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 浏览器Worker封装
|
|||
|
|
*/
|
|||
|
|
class BrowserWorker implements PlatformWorker {
|
|||
|
|
private _state: 'running' | 'terminated' = 'running';
|
|||
|
|
|
|||
|
|
constructor(
|
|||
|
|
private worker: Worker,
|
|||
|
|
private scriptURL: string
|
|||
|
|
) {}
|
|||
|
|
|
|||
|
|
public get state(): 'running' | 'terminated' {
|
|||
|
|
return this._state;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public postMessage(message: any, transfer?: Transferable[]): void {
|
|||
|
|
if (this._state === 'terminated') {
|
|||
|
|
throw new Error('Worker has been terminated');
|
|||
|
|
}
|
|||
|
|
if (transfer && transfer.length > 0) {
|
|||
|
|
this.worker.postMessage(message, transfer);
|
|||
|
|
} else {
|
|||
|
|
this.worker.postMessage(message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public onMessage(handler: (event: { data: any }) => void): void {
|
|||
|
|
this.worker.onmessage = (event: MessageEvent) => {
|
|||
|
|
handler({ data: event.data });
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public onError(handler: (error: ErrorEvent) => void): void {
|
|||
|
|
this.worker.onerror = handler;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public terminate(): void {
|
|||
|
|
if (this._state === 'running') {
|
|||
|
|
this.worker.terminate();
|
|||
|
|
URL.revokeObjectURL(this.scriptURL);
|
|||
|
|
this._state = 'terminated';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|