🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

基於WASM的純前端Office解決方案:線上編輯/匯入匯出/權限切換/多實例(已開源)

效果展示

所有操作均在瀏覽器進行,先來看看最終效果:

🌐 線上演示: mvp-onlyoffice.vercel.app/

🔗 GitHub倉庫: mvp-onlyoffice

基本示例

image.png

多實例示例

image.png

多tab示例

image.png

。。。。。。。。。。

核心功能演示

  • 文檔上傳:支持本地檔案直接上傳
  • 即時編輯:流暢的文檔編輯體驗
  • 格式轉換:基於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 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(`事件 ${key} 超時,超過 ${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


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝15   💬10   ❤️5
406
🥈
我愛JS
📝2   💬8   ❤️4
94
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付