Web Worker 示例
在 Web Worker 环境中使用 HTML Layout Parser 的完整示例。
准备工作
方法 1:直接导入(推荐)
对于 Vite 用户,首先在 vite.config.ts 中添加:
typescript
export default defineConfig({
optimizeDeps: {
exclude: ['html-layout-parser']
}
})然后使用直接导入:
typescript
// 直接从 npm 包导入
import { HtmlLayoutParser } from 'html-layout-parser/worker';
// 在 worker 中
const parser = new HtmlLayoutParser();
await parser.init(); // 自动从 node_modules 加载 WASM方法 2:手动复制(备选)
如果遇到打包器问题,复制 worker bundle:
bash
# 复制 worker bundle 到 workers 目录
cp -r node_modules/html-layout-parser/worker public/workers/html-layout-parsertypescript
import { HtmlLayoutParser } from 'html-layout-parser/worker';
const parser = new HtmlLayoutParser();
await parser.init('/workers/html-layout-parser/html_layout_parser.mjs'); // 自定义路径基础 Worker 设置
主线程
typescript
// main.ts
interface WorkerMessage {
type: 'init' | 'loadFont' | 'parse' | 'destroy';
id: number;
payload?: any;
}
class ParserWorkerClient {
private worker: Worker;
private messageId = 0;
private pendingRequests: Map<number, { resolve: Function; reject: Function }> = new Map();
constructor(workerUrl: string) {
this.worker = new Worker(workerUrl, { type: 'module' });
this.worker.onmessage = this.handleMessage.bind(this);
}
private handleMessage(event: MessageEvent) {
const { id, payload, error } = event.data;
const pending = this.pendingRequests.get(id);
if (pending) {
this.pendingRequests.delete(id);
if (error) {
pending.reject(new Error(error));
} else {
pending.resolve(payload);
}
}
}
private sendMessage<T>(type: string, payload?: any): Promise<T> {
return new Promise((resolve, reject) => {
const id = ++this.messageId;
this.pendingRequests.set(id, { resolve, reject });
this.worker.postMessage({ type, id, payload });
});
}
async init(): Promise<void> {
await this.sendMessage('init');
}
async loadFont(fontData: Uint8Array, fontName: string): Promise<number> {
return this.sendMessage('loadFont', { fontData, fontName });
}
async parse(html: string, options: { viewportWidth: number; css?: string }): Promise<any> {
return this.sendMessage('parse', { html, options });
}
async destroy(): Promise<void> {
await this.sendMessage('destroy');
this.worker.terminate();
}
}
// 使用示例
async function main() {
const client = new ParserWorkerClient('./parser-worker.js');
try {
await client.init();
const fontResponse = await fetch('/fonts/arial.ttf');
const fontData = new Uint8Array(await fontResponse.arrayBuffer());
await client.loadFont(fontData, 'Arial');
const layouts = await client.parse(
'<div style="font-size: 24px;">来自 Worker 的问候!</div>',
{ viewportWidth: 800 }
);
console.log(`解析了 ${layouts.length} 个字符`);
} finally {
await client.destroy();
}
}Worker 线程
typescript
// parser-worker.ts
// 从环境特定入口点导入
import { HtmlLayoutParser } from 'html-layout-parser/worker';
let parser: HtmlLayoutParser | null = null;
self.onmessage = async (event: MessageEvent) => {
const { type, id, payload } = event.data;
try {
let result: any;
switch (type) {
case 'init':
parser = new HtmlLayoutParser();
await parser.init('/workers/html-layout-parser/html_layout_parser.js');
result = true;
break;
case 'loadFont':
if (!parser) throw new Error('解析器未初始化');
const fontId = parser.loadFont(payload.fontData, payload.fontName);
if (fontId > 0) parser.setDefaultFont(fontId);
result = fontId;
break;
case 'parse':
if (!parser) throw new Error('解析器未初始化');
result = parser.parse(payload.html, payload.options);
break;
case 'destroy':
if (parser) {
parser.destroy();
parser = null;
}
result = true;
break;
}
self.postMessage({ id, payload: result });
} catch (error) {
self.postMessage({
id,
error: error instanceof Error ? error.message : String(error)
});
}
};OffscreenCanvas 渲染
使用 OffscreenCanvas 直接在 worker 中渲染。
主线程
typescript
// main-offscreen.ts
class OffscreenRendererClient {
private worker: Worker;
private messageId = 0;
private pendingRequests: Map<number, { resolve: Function; reject: Function }> = new Map();
constructor(canvas: HTMLCanvasElement, workerUrl: string) {
this.worker = new Worker(workerUrl, { type: 'module' });
this.worker.onmessage = this.handleMessage.bind(this);
}
private handleMessage(event: MessageEvent) {
const { id, payload, error } = event.data;
const pending = this.pendingRequests.get(id);
if (pending) {
this.pendingRequests.delete(id);
error ? pending.reject(new Error(error)) : pending.resolve(payload);
}
}
private sendMessage<T>(type: string, payload?: any, transfer?: Transferable[]): Promise<T> {
return new Promise((resolve, reject) => {
const id = ++this.messageId;
this.pendingRequests.set(id, { resolve, reject });
this.worker.postMessage({ type, id, payload }, transfer || []);
});
}
async init(canvas: HTMLCanvasElement): Promise<void> {
const offscreen = canvas.transferControlToOffscreen();
await this.sendMessage('init', {
canvas: offscreen,
width: canvas.width,
height: canvas.height
}, [offscreen]);
}
async loadFont(fontData: Uint8Array, fontName: string): Promise<number> {
const buffer = fontData.buffer.slice(0);
return this.sendMessage('loadFont', {
fontData: new Uint8Array(buffer),
fontName
}, [buffer]);
}
async render(html: string, css?: string): Promise<void> {
await this.sendMessage('render', { html, css });
}
async destroy(): Promise<void> {
await this.sendMessage('destroy');
this.worker.terminate();
}
}
// 使用示例
async function main() {
const canvas = document.getElementById('renderCanvas') as HTMLCanvasElement;
canvas.width = 800;
canvas.height = 600;
const renderer = new OffscreenRendererClient(canvas, './offscreen-worker.js');
try {
await renderer.init(canvas);
const fontResponse = await fetch('/fonts/arial.ttf');
const fontData = new Uint8Array(await fontResponse.arrayBuffer());
await renderer.loadFont(fontData, 'Arial');
await renderer.render(`
<div style="padding: 20px;">
<h1 style="font-size: 32px; color: #333333FF;">在 Worker 中渲染</h1>
<p style="font-size: 18px; color: #666666FF;">
使用 OffscreenCanvas 获得最佳性能。
</p>
</div>
`);
} catch (error) {
console.error('错误:', error);
}
window.addEventListener('beforeunload', () => renderer.destroy());
}Worker 线程
typescript
// offscreen-worker.ts
// 从环境特定入口点导入
import { HtmlLayoutParser, CharLayout } from 'html-layout-parser/worker';
let parser: HtmlLayoutParser | null = null;
let canvas: OffscreenCanvas | null = null;
let ctx: OffscreenCanvasRenderingContext2D | null = null;
function parseColor(color: string): string {
if (!color || color === '#00000000') return 'transparent';
if (color.startsWith('#') && color.length === 9) {
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
const a = parseInt(color.slice(7, 9), 16) / 255;
return `rgba(${r}, ${g}, ${b}, ${a.toFixed(3)})`;
}
return color;
}
function renderCharacter(ctx: OffscreenCanvasRenderingContext2D, char: CharLayout) {
if (char.backgroundColor && char.backgroundColor !== '#00000000') {
ctx.fillStyle = parseColor(char.backgroundColor);
ctx.fillRect(char.x, char.y, char.width, char.height);
}
ctx.font = `${char.fontStyle} ${char.fontWeight} ${char.fontSize}px ${char.fontFamily}`;
ctx.globalAlpha = char.opacity ?? 1;
ctx.fillStyle = parseColor(char.color);
ctx.fillText(char.character, char.x, char.baseline);
ctx.globalAlpha = 1;
}
self.onmessage = async (event: MessageEvent) => {
const { type, id, payload } = event.data;
try {
let result: any = true;
switch (type) {
case 'init':
canvas = payload.canvas;
canvas.width = payload.width;
canvas.height = payload.height;
ctx = canvas.getContext('2d');
parser = new HtmlLayoutParser();
await parser.init('/workers/html-layout-parser/html_layout_parser.js');
break;
case 'loadFont':
if (!parser) throw new Error('解析器未初始化');
const fontId = parser.loadFont(payload.fontData, payload.fontName);
if (fontId > 0) parser.setDefaultFont(fontId);
result = fontId;
break;
case 'render':
if (!parser || !canvas || !ctx) throw new Error('未初始化');
ctx.clearRect(0, 0, canvas.width, canvas.height);
const layouts = parser.parse(payload.html, {
viewportWidth: canvas.width,
css: payload.css
});
for (const char of layouts) {
renderCharacter(ctx, char);
}
result = layouts.length;
break;
case 'destroy':
if (parser) {
parser.destroy();
parser = null;
}
canvas = null;
ctx = null;
break;
}
self.postMessage({ id, payload: result });
} catch (error) {
self.postMessage({
id,
error: error instanceof Error ? error.message : String(error)
});
}
};Worker 池模式
使用多个 worker 进行并行处理。
typescript
import { CharLayout } from 'html-layout-parser';
interface PoolTask {
id: string;
html: string;
css?: string;
resolve: (result: CharLayout[]) => void;
reject: (error: Error) => void;
}
class ParserWorkerPool {
private workers: Worker[] = [];
private busyWorkers: Set<Worker> = new Set();
private taskQueue: PoolTask[] = [];
private workerTaskMap: Map<Worker, PoolTask> = new Map();
constructor(
private workerUrl: string,
private poolSize: number = navigator.hardwareConcurrency || 4
) {}
async init(fontData: Uint8Array, fontName: string): Promise<void> {
const initPromises = [];
for (let i = 0; i < this.poolSize; i++) {
const worker = new Worker(this.workerUrl, { type: 'module' });
worker.onmessage = (event) => this.handleWorkerMessage(worker, event);
this.workers.push(worker);
const initPromise = new Promise<void>((resolve, reject) => {
const handler = (event: MessageEvent) => {
if (event.data.type === 'ready') {
worker.removeEventListener('message', handler);
resolve();
}
};
worker.addEventListener('message', handler);
worker.postMessage({ type: 'init', fontData, fontName });
});
initPromises.push(initPromise);
}
await Promise.all(initPromises);
}
private handleWorkerMessage(worker: Worker, event: MessageEvent) {
const task = this.workerTaskMap.get(worker);
if (!task) return;
const { payload, error } = event.data;
error ? task.reject(new Error(error)) : task.resolve(payload);
this.workerTaskMap.delete(worker);
this.busyWorkers.delete(worker);
this.processNextTask();
}
private processNextTask() {
if (this.taskQueue.length === 0) return;
const availableWorker = this.workers.find(w => !this.busyWorkers.has(w));
if (!availableWorker) return;
const task = this.taskQueue.shift()!;
this.busyWorkers.add(availableWorker);
this.workerTaskMap.set(availableWorker, task);
availableWorker.postMessage({
type: 'parse',
html: task.html,
css: task.css
});
}
parse(html: string, css?: string): Promise<CharLayout[]> {
return new Promise((resolve, reject) => {
this.taskQueue.push({
id: `task_${Date.now()}`,
html,
css,
resolve,
reject
});
this.processNextTask();
});
}
async parseAll(documents: Array<{ html: string; css?: string }>): Promise<CharLayout[][]> {
return Promise.all(documents.map(doc => this.parse(doc.html, doc.css)));
}
getStats() {
return {
totalWorkers: this.workers.length,
busyWorkers: this.busyWorkers.size,
queuedTasks: this.taskQueue.length
};
}
destroy() {
for (const worker of this.workers) {
worker.terminate();
}
this.workers = [];
this.busyWorkers.clear();
this.taskQueue = [];
}
}
// 使用示例
async function main() {
const pool = new ParserWorkerPool('./pool-worker.js', 4);
try {
const fontResponse = await fetch('/fonts/arial.ttf');
const fontData = new Uint8Array(await fontResponse.arrayBuffer());
await pool.init(fontData, 'Arial');
const documents = Array.from({ length: 100 }, (_, i) => ({
html: `<div>文档 ${i + 1}</div>`
}));
console.log('处理 100 个文档...');
const startTime = performance.now();
const results = await pool.parseAll(documents);
const endTime = performance.now();
console.log(`处理了 ${results.length} 个文档,耗时 ${(endTime - startTime).toFixed(2)}ms`);
} finally {
pool.destroy();
}
}后台处理
处理大型文档而不阻塞主线程。
typescript
// 主线程
class BackgroundProcessor {
private worker: Worker;
private onProgress?: (progress: number) => void;
private onComplete?: (result: any) => void;
constructor(workerUrl: string) {
this.worker = new Worker(workerUrl, { type: 'module' });
this.worker.onmessage = this.handleMessage.bind(this);
}
private handleMessage(event: MessageEvent) {
const { type, progress, payload } = event.data;
if (type === 'progress') {
this.onProgress?.(progress);
} else if (type === 'complete') {
this.onComplete?.(payload);
}
}
setProgressHandler(handler: (progress: number) => void) {
this.onProgress = handler;
}
setCompleteHandler(handler: (result: any) => void) {
this.onComplete = handler;
}
async init(): Promise<void> {
return new Promise((resolve) => {
const handler = (event: MessageEvent) => {
if (event.data.type === 'ready') {
this.worker.removeEventListener('message', handler);
resolve();
}
};
this.worker.addEventListener('message', handler);
this.worker.postMessage({ type: 'init' });
});
}
process(html: string, css?: string): void {
this.worker.postMessage({ type: 'process', html, css });
}
destroy() {
this.worker.terminate();
}
}
// 使用示例
async function backgroundExample() {
const processor = new BackgroundProcessor('./background-worker.js');
processor.setProgressHandler((progress) => {
console.log(`进度: ${progress}%`);
});
processor.setCompleteHandler((result) => {
console.log(`完成: ${result.length} 个字符`);
});
await processor.init();
processor.process('<div>大型文档内容...</div>');
}