基于WASM的纯前端Office解决方案:在线编辑/导入导出/权限切换/多实例(已开源)

首页 编程分享 JQUERY丨JS丨VUE 正文

Electrolux 推荐 转载 编程分享 2025-11-25 20:04:50

简介 基于wasm 纯前端实现office 的 在线编辑/导入导出/只读可编辑 等功能 效果展示 所有操作均在浏览器进行,先来看看最终效果: 🌐 在线演示: https://mvp-onlyoffice.


效果展示

所有操作均在浏览器进行,先来看看最终效果:

🌐 在线演示: mvp-onlyoffice.vercel.app/

🔗 GitHub仓库: mvp-onlyoffice

基本示例

多实例示例

多tab示例

。。。。。。。。。。

核心功能演示

  • 文档上传:支持本地文件直接上传
  • 实时编辑:流畅的文档编辑体验
  • 格式转换:基于WASM的文档格式转换
  • 导出保存:一键导出编辑后的文档
  • 模式切换:只读/可编辑模式自由切换
  • 多语言支持:中英文界面无缝切换
  • 多实例支持:同时运行多个独立编辑器实例(Word/Excel/PPT)
  • 资源隔离:每个实例独立的图片上传和媒体资源管理

技术架构

核心技术栈

  • React 19 + Next.js 15:现代化前端框架
  • OnlyOffice SDK:官方JavaScript SDK,提供文档编辑核心能力
  • WebAssembly (x2t-wasm):文档格式转换引擎
  • TypeScript:类型安全的开发体验
  • EventBus:事件驱动的架构设计
  • IndexedDB:WASM文件缓存优化
  • EditorManagerFactory:多实例管理器工厂模式

tip: 事实上不依赖于 react,你可以拿到 项目中的 src/onlyoffice-comp ,然后接入到任何系统中去,接入层可以参考 src/app/excel/page.tsx等应用层文件

架构流程图

用户上传文档
    ↓
React组件层
    ↓
EditorManagerFactory (多实例管理器工厂)
    ↓
EditorManager (编辑器管理器)
    ↓
X2T Converter (WASM转换器)
    ↓
OnlyOffice SDK (文档编辑器)
    ↓
EventBus (事件总线)
    ↓
导出/保存文档

WASM文档转换核心流程

转换流程图解

用户选择文件
    ↓
浏览器读取文件
    ↓
WASM虚拟文件系统
    ↓
X2T引擎执行转换
    ↓
生成二进制数据 + 媒体资源
    ↓
OnlyOffice编辑器加载

核心代码实现

// src/onlyoffice-comp/lib/x2t.ts

/**
 * X2T 工具类 - 负责文档转换功能
 */
class X2TConverter {
  private x2tModule: EmscriptenModule | null = null;
  
  // 支持的文件类型映射
  private readonly DOCUMENT_TYPE_MAP: Record<string, DocumentType> = {
    docx: 'word',
    doc: 'word',
    odt: 'word',
    rtf: 'word',
    txt: 'word',
    xlsx: 'cell',
    xls: 'cell',
    ods: 'cell',
    csv: 'cell',
    pptx: 'slide',
    ppt: 'slide',
    odp: 'slide',
  };

  /**
   * 转换文档格式
   */
  async convertDocument(file: File): Promise<ConversionResult> {
    // 初始化WASM模块
    await this.ensureReady();
    
    // 写入虚拟文件系统
    const data = await file.arrayBuffer();
    this.x2tModule!.FS.writeFile('/working/origin', new Uint8Array(data));
    
    // 执行C++编译的转换模块
    this.executeConversion('/working/params.xml');
    
    // 提取转换结果和媒体文件
    return {
      bin: this.x2tModule!.FS.readFile('/working/output.bin'),
      media: this.collectMediaFiles() // 提取图片等资源
    };
  }
}

编辑器管理器:多实例架构设计

项目采用工厂模式管理多个编辑器实例,每个实例都有独立的容器ID和资源管理:

// src/onlyoffice-comp/lib/editor-manager.ts

// EditorManagerFactory - 多实例管理器工厂
class EditorManagerFactory {
  private static instance: EditorManagerFactory;
  private managers: Map<string, EditorManager> = new Map();
  
  // 创建或获取编辑器管理器实例
  create(containerId?: string): EditorManager {
    if (containerId) {
      // 如果已存在,返回现有实例
      if (this.managers.has(containerId)) {
        return this.managers.get(containerId)!;
      }
      // 创建新实例
      const manager = new EditorManager(containerId);
      this.managers.set(containerId, manager);
      return manager;
    }
    // 创建默认实例
    return this.createDefault();
  }
  
  // 获取指定容器ID的实例
  get(containerId: string): EditorManager | undefined {
    return this.managers.get(containerId);
  }
  
  // 销毁指定实例
  destroy(containerId: string): void {
    const manager = this.managers.get(containerId);
    if (manager) {
      manager.destroy();
      this.managers.delete(containerId);
    }
  }
  
  // 销毁所有实例
  destroyAll(): void {
    this.managers.forEach(manager => manager.destroy());
    this.managers.clear();
  }
}

// EditorManager - 单个编辑器管理器
class EditorManager {
  private instanceId: string;
  private containerId: string;
  private editor: DocEditor | null = null;
  
  constructor(containerId?: string) {
    this.instanceId = nanoid(); // 生成唯一实例ID
    this.containerId = containerId || `onlyoffice-editor-${this.instanceId}`;
  }
  
  // 获取实例ID
  getInstanceId(): string {
    return this.instanceId;
  }
  
  // 获取容器ID
  getContainerId(): string {
    return this.containerId;
  }
  
  // 导出文档(事件驱动)
  async export(): Promise<SaveDocumentData> {
    const editor = this.get();
    if (!editor) {
      throw new Error('Editor not available');
    }
    
    // 触发保存
    (editor as any).downloadAs();
    
    // 等待保存事件
    const result = await onlyofficeEventbus.waitFor(
      ONLYOFFICE_EVENT_KEYS.SAVE_DOCUMENT, 
      10000
    );
    
    return result;
  }
  
  // 设置只读模式
  async setReadOnly(readOnly: boolean): Promise<void> {
    // 实现逻辑...
  }
}

事件驱动架构:EventBus解耦设计

项目采用事件总线机制,实现组件间的松耦合通信:

// src/onlyoffice-comp/lib/eventbus.ts

class EventBus {
  private listeners: Map<EventKey, Array<(data: any) => void>> = new Map();
  
  // 监听事件
  on<K extends EventKey>(key: K, callback: (data: EventDataMap[K]) => void): void {
    if (!this.listeners.has(key)) {
      this.listeners.set(key, []);
    }
    this.listeners.get(key)!.push(callback);
  }
  
  // 等待事件触发(返回 Promise)
  waitFor<K extends EventKey>(key: K, timeout?: number): Promise<EventDataMap[K]> {
    return new Promise((resolve, reject) => {
      const timeoutId = timeout
        ? setTimeout(() => {
            this.off(key, handleEvent);
            reject(new Error(`Event ${key} timeout after ${timeout}ms`));
          }, timeout)
        : null;

      const handleEvent = (data: EventDataMap[K]) => {
        if (timeoutId) clearTimeout(timeoutId);
        this.off(key, handleEvent);
        resolve(data);
      };

      this.on(key, handleEvent);
    });
  }
}

支持的事件类型

  • saveDocument - 文档保存完成事件
  • documentReady - 文档加载就绪事件
  • loadingChange - 加载状态变化事件

核心功能特性

1. 多实例支持

支持同时创建和管理多个独立的编辑器实例,每个实例都有独立的容器ID和资源管理:

import { createEditorView } from '@/onlyoffice-comp/lib/x2t';
import { editorManagerFactory } from '@/onlyoffice-comp/lib/editor-manager';

// 创建第一个编辑器实例
const manager1 = await createEditorView({
  isNew: true,
  fileName: 'Document1.docx',
  containerId: 'editor-1', // 指定容器ID
});

// 创建第二个编辑器实例
const manager2 = await createEditorView({
  isNew: true,
  fileName: 'Document2.xlsx',
  containerId: 'editor-2', // 不同的容器ID
});

// 分别操作不同实例
const result1 = await manager1.export();
const result2 = await manager2.export();

// 销毁指定实例
editorManagerFactory.destroy('editor-1');

// 销毁所有实例
editorManagerFactory.destroyAll();

关键特性

  • 容器隔离:每个实例使用唯一的容器ID,通过 data-onlyoffice-container-id 属性精确定位
  • 资源隔离:每个实例管理独立的媒体资源映射,图片上传不会相互干扰
  • 独立事件处理:每个实例通过 createWriteFileHandler(manager) 创建独立的图片上传处理函数

2. 国际化支持

项目内置多语言支持,可自由切换中英文界面。在多实例场景下,切换语言会重新创建所有编辑器实例:

// 切换语言(多实例场景)
const handleLanguageSwitch = async () => {
  const newLang = currentLang === 'zh' ? 'en' : 'zh';
  setCurrentLang(newLang);
  
  // 保存每个编辑器实例的文档信息
  const editorDocuments = {
    manager1: { fileName: 'Doc1.docx', file: file1 },
    manager2: { fileName: 'Doc2.xlsx', file: file2 },
  };
  
  // 重新创建所有编辑器以应用新语言
  if (editorDocuments.manager1) {
    await createEditorView({
      fileName: editorDocuments.manager1.fileName,
      file: editorDocuments.manager1.file,
      containerId: 'editor-1',
      lang: newLang,
    });
  }
  
  if (editorDocuments.manager2) {
    await createEditorView({
      fileName: editorDocuments.manager2.fileName,
      file: editorDocuments.manager2.file,
      containerId: 'editor-2',
      lang: newLang,
    });
  }
};

3. 导入导出功能

完整的文档导入导出能力:

// 导出文档
const result = await editorManager.export();
// result 包含: { fileName, fileType, binData, media }

// 转换并下载
const buffer = await convertBinToDocument(
  result.binData, 
  result.fileName,
  FILE_TYPE.XLSX, 
  result.media
);

const blob = new Blob([buffer.data], {
  type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
// 执行下载操作

4. 只读/可编辑模式切换

灵活的权限控制,支持动态切换编辑模式:

// 设置为只读模式
await editorManager.setReadOnly(true);

// 切换为可编辑模式
await editorManager.setReadOnly(false);

// 查询当前模式
const isReadOnly = editorManager.getReadOnly();

实现原理

  • 从只读切换到可编辑:重新创建编辑器实例
  • 从可编辑切换到只读:使用processRightsChange命令

5. IndexedDB缓存优化

使用IndexedDB缓存WASM文件,大幅提升二次加载速度:

// 拦截 fetch,缓存 WASM 文件到 IndexedDB
private interceptFetch(): void {
  const originalFetch = window.fetch;
  
  window.fetch = async function(input: RequestInfo | URL): Promise<Response> {
    // 先尝试从缓存读取
    const cached = await this.getCachedWasm(url);
    if (cached) {
      return new Response(cached, {
        headers: { 'Content-Type': 'application/wasm' }
      });
    }
    
    // 缓存未命中,从网络加载并缓存
    const response = await originalFetch(input);
    const arrayBuffer = await response.arrayBuffer();
    await this.cacheWasm(url, arrayBuffer);
    
    return response;
  };
}

使用示例

基本使用(单实例)

import { createEditorView } from '@/onlyoffice-comp/lib/x2t';
import { editorManagerFactory } from '@/onlyoffice-comp/lib/editor-manager';

// 创建编辑器视图(使用默认实例)
await createEditorView({
  file: fileObject,        // File 对象(可选)
  fileName: 'document.xlsx', // 文件名
  isNew: false,            // 是否新建文档
  readOnly: false,        // 是否只读
  lang: 'zh',             // 界面语言
});

// 获取默认实例并导出文档
const defaultManager = editorManagerFactory.getDefault();
const result = await defaultManager.export();
console.log('导出成功:', result);

多实例使用

import { createEditorView } from '@/onlyoffice-comp/lib/x2t';
import { editorManagerFactory } from '@/onlyoffice-comp/lib/editor-manager';

// 创建多个编辑器实例
const manager1 = await createEditorView({
  isNew: true,
  fileName: 'Doc1.docx',
  containerId: 'editor-1',
  lang: 'zh',
});

const manager2 = await createEditorView({
  isNew: true,
  fileName: 'Doc2.xlsx',
  containerId: 'editor-2',
  lang: 'zh',
});

const manager3 = await createEditorView({
  isNew: true,
  fileName: 'Doc3.pptx',
  containerId: 'editor-3',
  lang: 'zh',
});

// 分别导出
const result1 = await manager1.export();
const result2 = await manager2.export();
const result3 = await manager3.export();

React组件集成(多实例)

// src/app/multi/page.tsx
function MultiInstancePageContent() {
  const [managers, setManagers] = useState({
    manager1: null,
    manager2: null,
    manager3: null,
  });
  
  // 保存文档信息,用于语言切换
  const [editorDocuments, setEditorDocuments] = useState({
    manager1: null,
    manager2: null,
    manager3: null,
  });
  
  // 创建编辑器
  const handleView = async (editorKey: string, fileName: string, file?: File) => {
    const containerId = `editor-${editorKey.replace('manager', '')}`;
    
    const manager = await createEditorView({
      file,
      fileName,
      isNew: !file,
      containerId, // 指定容器ID
      lang: getOnlyOfficeLang(),
    });
    
    setManagers(prev => ({
      ...prev,
      [editorKey]: manager,
    }));
    
    // 保存文档信息
    setEditorDocuments(prev => ({
      ...prev,
      [editorKey]: { fileName, file: file || undefined },
    }));
  };
  
  // 语言切换(重新创建所有编辑器)
  const handleLanguageSwitch = async () => {
    const newLang = currentLang === 'zh' ? 'en' : 'zh';
    
    // 重新创建所有编辑器
    if (editorDocuments.manager1) {
      const doc = editorDocuments.manager1;
      await handleView('manager1', doc.fileName, doc.file);
    }
    // ... 其他实例
  };
  
  return (
    <div className="grid grid-cols-3 gap-4">
      {/* 编辑器容器 - 使用 data-onlyoffice-container-id 属性 */}
      <div className="onlyoffice-container" data-onlyoffice-container-id="editor-1">
        <div id="editor-1" className="absolute inset-0" />
      </div>
      <div className="onlyoffice-container" data-onlyoffice-container-id="editor-2">
        <div id="editor-2" className="absolute inset-0" />
      </div>
      <div className="onlyoffice-container" data-onlyoffice-container-id="editor-3">
        <div id="editor-3" className="absolute inset-0" />
      </div>
    </div>
  );
}

项目结构

mvp-onlyoffice/
├── src/
│   ├── app/              # Next.js 应用页面
│   │   ├── excel/        # Excel 编辑器页面
│   │   ├── docs/         # Word 编辑器页面
│   │   ├── ppt/          # PowerPoint 编辑器页面
│   │   └── multi/        # 多实例演示页面
│   ├── onlyoffice-comp/  # OnlyOffice 组件库
│   │   └── lib/
│   │       ├── editor-manager.ts  # 编辑器管理器(支持多实例)
│   │       ├── x2t.ts             # 文档转换模块
│   │       ├── eventbus.ts        # 事件总线
│   │       └── utils.ts            # 工具函数
│   └── components/       # 通用组件
├── public/               # 静态资源
│   ├── web-apps/         # OnlyOffice Web 应用资源
│   ├── sdkjs/            # OnlyOffice SDK 资源
│   └── wasm/             # WebAssembly 转换器
└── onlyoffice-x2t-wasm/  # x2t-wasm 源码

部署方案

Vercel一键部署

项目已配置静态导出,可直接部署到Vercel:

# 安装依赖
npm install

# 构建项目
npm run build

# Vercel 会自动检测并部署

🌐 在线演示: mvp-onlyoffice.vercel.app/

静态文件部署

项目支持静态导出,构建后的文件可部署到任何静态托管服务:

# 构建静态文件
npm run build

# 输出目录: out/
# 可直接部署到 GitHub Pages、Netlify、Nginx 等

技术优势总结

特性 传统方案 本方案
数据安全 ❌ 需要上传服务器 ✅ 完全本地处理
部署成本 ❌ 需要后端服务 ✅ 纯静态部署
格式支持 ⚠️ 有限格式 ✅ 30+种格式
离线使用 ❌ 需要网络 ✅ 完全离线
性能优化 ⚠️ 依赖网络 ✅ IndexedDB缓存
国际化 ⚠️ 需额外配置 ✅ 内置支持
权限控制 ⚠️ 复杂实现 ✅ 简单API
多实例支持 ❌ 不支持 ✅ 原生支持,资源隔离

技术原理

使用x2t-wasm替代OnlyOffice服务

传统OnlyOffice集成需要:

  1. 搭建OnlyOffice Document Server
  2. 配置文档转换服务
  3. 处理文档上传下载
  4. 管理服务器资源

本方案通过WASM技术:

  1. 在浏览器中直接运行x2t转换引擎
  2. 使用虚拟文件系统处理文档
  3. 完全客户端化,无需服务器

多实例架构设计

  • 工厂模式:使用 EditorManagerFactory 统一管理多个编辑器实例
  • 容器隔离:每个实例使用唯一的容器ID,通过 data-onlyoffice-container-id 属性精确定位
  • 资源隔离:每个实例管理独立的媒体资源映射,图片上传通过独立的 writeFile 处理函数
  • 事件隔离:虽然使用全局 EventBus,但每个实例的事件处理函数是独立的

参考项目

开源地址

🔗 GitHub仓库: mvp-onlyoffice

总结

本项目提供了一个完整的纯前端OnlyOffice集成方案,通过WASM技术实现了文档格式转换的本地化,结合React和OnlyOffice SDK,打造了一个功能完善、性能优秀的文档编辑器。

核心亮点

  • 🚀 纯前端架构,无需后端服务
  • 🔒 数据完全本地化,保护隐私安全
  • ⚡ 基于WASM的高性能转换
  • 🌏 内置国际化支持
  • 📦 支持导入导出
  • 🔐 灵活的权限控制
  • 🎯 多实例支持:同时运行多个独立编辑器,资源完全隔离

欢迎Star和Fork,一起推动前端Office编辑技术的发展!


相关阅读

转载链接:https://juejin.cn/post/7575425466904723519


Tags:


本篇评论 —— 揽流光,涤眉霜,清露烈酒一口话苍茫。


    声明:参照站内规则,不文明言论将会删除,谢谢合作。


      最新评论




ABOUT ME

Blogger:袅袅牧童 | Arkin

Ido:PHP攻城狮

WeChat:nnmutong

Email:nnmutong@icloud.com

标签云