效果展示
所有操作均在浏览器进行,先来看看最终效果:
🌐 在线演示: 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集成需要:
- 搭建OnlyOffice Document Server
- 配置文档转换服务
- 处理文档上传下载
- 管理服务器资源
本方案通过WASM技术:
- 在浏览器中直接运行x2t转换引擎
- 使用虚拟文件系统处理文档
- 完全客户端化,无需服务器
多实例架构设计
-
工厂模式:使用
EditorManagerFactory统一管理多个编辑器实例 -
容器隔离:每个实例使用唯一的容器ID,通过
data-onlyoffice-container-id属性精确定位 -
资源隔离:每个实例管理独立的媒体资源映射,图片上传通过独立的
writeFile处理函数 - 事件隔离:虽然使用全局 EventBus,但每个实例的事件处理函数是独立的
参考项目
- Qihoo360/se-office - se-office扩展,提供基于开放标准的全功能办公生产力套件
- cryptpad/onlyoffice-x2t-wasm - CryptPad WebAssembly文件转换工具
- ranuts/document - 参考静态资源实现
开源地址
🔗 GitHub仓库: mvp-onlyoffice
总结
本项目提供了一个完整的纯前端OnlyOffice集成方案,通过WASM技术实现了文档格式转换的本地化,结合React和OnlyOffice SDK,打造了一个功能完善、性能优秀的文档编辑器。
核心亮点:
- 🚀 纯前端架构,无需后端服务
- 🔒 数据完全本地化,保护隐私安全
- ⚡ 基于WASM的高性能转换
- 🌏 内置国际化支持
- 📦 支持导入导出
- 🔐 灵活的权限控制
- 🎯 多实例支持:同时运行多个独立编辑器,资源完全隔离
欢迎Star和Fork,一起推动前端Office编辑技术的发展!
相关阅读:
Electrolux 
![[爱了]](/js/img/d1.gif)
![[尴尬]](/js/img/d16.gif)