所有操作均在瀏覽器進行,先來看看最終效果:
🌐 線上演示: mvp-onlyoffice.vercel.app/
🔗 GitHub倉庫: mvp-onlyoffice
基本示例

多實例示例

多tab示例

。。。。。。。。。。
tip: 事實上不依賴於react,你可以拿到項目中的src/onlyoffice-comp,然後接入到任何系統中去,接入層可以參考src/app/excel/page.tsx等應用層文件
用戶上傳文檔
↓
React組件層
↓
EditorManagerFactory (多實例管理器工廠)
↓
EditorManager (編輯器管理器)
↓
X2T Converter (WASM轉換器)
↓
OnlyOffice SDK (文檔編輯器)
↓
EventBus (事件總線)
↓
匯出/保存文檔
用戶選擇文件
↓
瀏覽器讀取文件
↓
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> {
// 實現邏輯...
}
}
項目採用事件總線機制,实现組件間的鬆耦合通信:
// 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 - 加載狀態變化事件支持同時創建和管理多個獨立的編輯器實例,每個實例都有獨立的容器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();
關鍵特性:
data-onlyoffice-container-id 屬性精確定位createWriteFileHandler(manager) 創建獨立的圖片上傳處理函數項目內建多語言支持,可自由切換中英文界面。在多實例場景下,切換語言會重新創建所有編輯器實例:
// 切換語言(多實例場景)
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,
});
}
};
完整的文檔匯入匯出能力:
// 匯出文檔
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'
});
// 執行下載操作
靈活的權限控制,支持動態切換編輯模式:
// 設置為只讀模式
await editorManager.setReadOnly(true);
// 切換為可編輯模式
await editorManager.setReadOnly(false);
// 查詢當前模式
const isReadOnly = editorManager.getReadOnly();
實現原理:
processRightsChange命令使用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();
// 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:
# 安裝依賴
npm install
# 建構項目
npm run build
# Vercel 會自動檢測並部署
🌐 線上演示: mvp-onlyoffice.vercel.app/
項目支持靜態導出,建構後的文件可部署到任何靜態託管服務:
# 建構靜態文件
npm run build
# 輸出目錄: out/
# 可直接部署到 GitHub Pages、Netlify、Nginx 等
| 特性 | 傳統方案 | 本方案 |
|---|---|---|
| 數據安全 | ❌ 需要上傳伺服器 | ✅ 完全本地處理 |
| 部署成本 | ❌ 需要後端服務 | ✅ 纯靜態部署 |
| 格式支持 | ⚠️ 有限格式 | ✅ 30+種格式 |
| 離線使用 | ❌ 需要網絡 | ✅ 完全離線 |
| 性能優化 | ⚠️ 依賴網絡 | ✅ IndexedDB快取 |
| 國際化 | ⚠️ 需額外配置 | ✅ 內置支持 |
| 權限控制 | ⚠️ 複雜實現 | ✅ 簡單API |
| 多實例支持 | ❌ 不支持 | ✅ 原生支持,資源隔離 |
傳統OnlyOffice集成需要:
本方案通過WASM技術:
EditorManagerFactory 統一管理多個編輯器實例data-onlyoffice-container-id 屬性精確定位writeFile 處理函數🔗 GitHub倉庫: mvp-onlyoffice
本項目提供了一個完整的純前端OnlyOffice整合方案,通過WASM技術實現了文檔格式轉換的本地化,結合React和OnlyOffice SDK,打造了一個功能完善、性能優秀的文檔編輯器。
核心亮點:
歡迎Star和Fork,一起推動前端Office編輯技術的發展!
相關閱讀: