
图1:Microsoft Word软件图标
在Web项目中,实现前端页面直接编辑Word文档并保存是一个常见的需求。经过调研,开源组件OnlyOffice是一个不错的选择。它功能强大,支持文档转化、多人协同编辑、文档打印等,我们此次主要应用其在线文档编辑功能。
1、OnlyOffice的部署
部署OnlyOffice主要有两种方式:Docker部署和本地直接安装。相比之下,Docker方式更为简便,只需拉取镜像并配置启动参数即可。由于笔者最初搜索到的是Ubuntu本地部署教程,因此采用了后者。以下提供两种部署方式的参考链接,供大家选择:
2、代码逻辑开发
项目技术栈为:前端使用Element UI + Vue,后端采用 Spring Boot。
2.1、前端代码
前端开发主要参考OnlyOffice官方API文档:
首先,需要在页面中引入OnlyOffice的JS API文件,注意将 documentserver 替换为你部署OnlyOffice服务的实际地址。
<div id="placeholder"></div>
<script type="text/javascript" src="https://documentserver/web-apps/apps/api/documents/api.js"></script>
核心初始化配置与编辑器创建代码如下:
const config = {
document: {
mode: 'edit',
fileType: 'docx',
key: String( Math.floor(Math.random() * 10000)),
title: route.query.name + '.docx',
url: import.meta.env.VITE_APP_API_URL+`/getFile/${route.query.id}`,
permissions: {
comment: true,
download: true,
modifyContentControl: true,
modifyFilter: true,
edit: true,
fillForms: true,
review: true,
},
},
documentType: 'word',
editorConfig: {
user: {
id: 'liu',
name: 'liu',
},
// 隐藏插件菜单
customization: {
plugins: false,
forcesave: true,
},
lang: 'zh',
callbackUrl: import.meta.env.VITE_APP_API_URL+`/callback`,
},
height: '100%',
width: '100%',
}
new window.DocsAPI.DocEditor('onlyoffice', config)
关键参数说明:
url: 对应后端提供文档下载的地址,如 http://192.168.123.123:8089/getFile/12,其中 12 是业务ID(如会议ID)。
callbackUrl: 文档操作(如编辑保存)的回调地址,OnlyOffice服务器会向此地址推送状态。
2.2、后端代码
后端使用Spring Boot,主要提供两个接口:/getFile/{id} 用于提供文档流,/callback 用于接收文档操作状态并处理保存。
POM依赖:
<!-- httpclient start -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
</dependency>
控制器核心代码 (OnlyOfficeController):
@Api(value = "OnlyOfficeController")
@RestController
public class OnlyOfficeController {
@Autowired
private IMeetingTableService meetingTableService;
//这里仅写死路径测试
// private String meetingMinutesFilePath = "C:\\Users\\qrs-ljy\\Desktop\\王勋\\c1f15837-d8b4-4380-8161-b85e970ad174\\123435_会议纪要(公开).docx"; //这里仅写死路径测试
private String meetingMinutesFilePath;
/**
* 传入参数 会议id,得到会议纪要文件流,并进行打开
*
* @param response
* @param meeting_id
* @return
* @throws IOException
*/
@ApiOperation(value = "OnlyOffice")
@GetMapping("/getFile/{meeting_id}")
public ResponseEntity<byte[]> getFile(HttpServletResponse response, @PathVariable Long meeting_id) throws IOException {
MeetingTable meetingTable = meetingTableService.selectMeetingTableById(meeting_id);
meetingMinutesFilePath = meetingTable.getMeetingMinutesFilePath();
if (meetingMinutesFilePath == null || "".equals(meetingMinutesFilePath)) {
return null; //当会议纪要文件为空的时候,就返回null
}
File file = new File(meetingMinutesFilePath);
FileInputStream fileInputStream = null;
InputStream fis = null;
try {
fileInputStream = new FileInputStream(file);
fis = new BufferedInputStream(fileInputStream);
byte[] buffer = new byte[fis.available()];
fis.read(buffer);
fis.close();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
// 替换为实际的文档名称
headers.setContentDispositionFormData("attachment", URLEncoder.encode(file.getName(), "UTF-8"));
return new ResponseEntity<>(buffer, headers, HttpStatus.OK);
} catch (Exception e) {
throw new RuntimeException("e -> ", e);
} finally {
try {
if (fis != null) fis.close();
} catch (Exception e) {
}
try {
if (fileInputStream != null) fileInputStream.close();
} catch (Exception e) {
}
}
}
@CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.OPTIONS})
@PostMapping("/callback")
public ResponseEntity<Object> handleCallback(@RequestBody CallbackData callbackData) {
//状态监听
//参见https://api.onlyoffice.com/editors/callback
Integer status = callbackData.getStatus();
switch (status) {
case 1: {
//document is being edited 文档已经被编辑
break;
}
case 2: {
//document is ready for saving,文档已准备好保存
System.out.println("document is ready for saving");
String url = callbackData.getUrl();
try {
saveFile(url); //保存文件
} catch (Exception e) {
System.out.println("保存文件异常");
}
System.out.println("save success.");
break;
}
case 3: {
//document saving error has occurred,保存出错
System.out.println("document saving error has occurred,保存出错");
break;
}
case 4: {
//document is closed with no changes,未保存退出
System.out.println("document is closed with no changes,未保存退出");
break;
}
case 6: {
//document is being edited, but the current document state is saved,编辑保存
String url = callbackData.getUrl();
try {
saveFile(url); //保存文件
} catch (Exception e) {
System.out.println("保存文件异常");
}
System.out.println("save success.");
}
case 7: {
//error has occurred while force saving the document. 强制保存文档出错
System.out.println("error has occurred while force saving the document. 强制保存文档出错");
}
default: {
}
}
// 返回响应
return ResponseEntity.<Object>ok(Collections.singletonMap("error", 0));
}
public void saveFile(String downloadUrl) throws URISyntaxException, IOException {
HttpsKitWithProxyAuth.downloadFile(downloadUrl, meetingMinutesFilePath);
}
@Setter
@Getter
public static class CallbackData {
/**
* 用户与文档的交互状态。0:用户断开与文档共同编辑的连接;1:新用户连接到文档共同编辑;2:用户单击强制保存按钮
*/
// @IsArray()
// actions?:IActions[] =null;
/**
* 字段已在 4.2 后版本废弃,请使用 history 代替
*/
Object changeshistory;
/**
* 文档变更的历史记录,仅当 status 等于 2 或者 3 时该字段才有值。其中的 serverVersion 字段也是 refreshHistory 方法的入参
*/
Object history;
/**
* 文档编辑的元数据信息,用来跟踪显示文档更改记录,仅当 status 等于 2 或者 2 时该字段才有值。该字段也是 setHistoryData(显示与特定文档版本对应的更改,类似 Git 历史记录)方法的入参
*/
String changesurl;
/**
* url 字段下载的文档扩展名,文件类型默认为 OOXML 格式,如果启用了 assemblyFormatAsOrigin(https://api.onlyoffice.com/editors/save<a href="javascript:;">#assemblyFormatAsOrigin</a>) 服务器设置则文件以原始格式保存
*/
String filetype;
/**
* 文档强制保存类型。0:对命令服务(https://api.onlyoffice.com/editors/command/forcesave)执行强制保存;1:每次保存完成时都会执行强制保存请求,仅设置 forcesave 等于 true 时生效;2:强制保存请求由计时器使用服务器中的设置执行。该字段仅 status 等于 7 或者 7 时才有值
*/
Integer forcesavetype;
/**
* 文档标识符,类似 id,在 Onlyoffice 服务内部唯一
*/
String key;
/**
* 文档状态。1:文档编辑中;2:文档已准备好保存;3:文档保存出错;4:文档没有变化无需保存;6:正在编辑文档,但保存了当前文档状态;7:强制保存文档出错
*/
Integer status;
/**
* 已编辑文档的链接,可以通过它下载到最新的文档,仅当 status 等于 2、3、6 或 7 时该字段才有值
*/
String url;
/**
* 自定义参数,对应指令服务的 userdata 字段
*/
Object userdata;
/**
* 打开文档进行编辑的用户标识列表,当文档被修改时,该字段将返回最后编辑文档的用户标识符,当 status 字段等于 2 或者 6 时有值
*/
String[] users;
/**
* 最近保存时间
*/
String lastsave;
/**
* 加密令牌
*/
String token;
}
}
代码中用到的 HttpsKitWithProxyAuth(用于下载文件)和 JsonUtil(JSON工具类)是参考其他博客实现的辅助类,因其代码较长,此处不全文贴出,可参考相关资源实现。
3、问题总结
以下是集成过程中遇到的关键问题及解决方案。
3.1、访问示例失败
部署完成后,访问 example 可能失败。首先检查相关服务是否正常启动:
systemctl status ds*
3.2、加载Word文档失败
如果遇到文档加载或保存权限问题,通常需要修改OnlyOffice的配置文件,关闭令牌验证并调整网络过滤设置。
配置文件位于 /etc/onlyoffice/documentserver,主要修改 local.json 和 default.json。
3.3、系统后端有Token验证问题
如果你的Spring Boot应用本身使用了JWT等Token验证机制,需要确保OnlyOffice回调(/callback)和文档下载接口(/getFile/*)不被安全拦截。在Spring Security配置中将其设为免认证路径:
// 在Security配置类中
requests.antMatchers("/callback", "/getFile/*", "/login", "/register", "/captchaImage").permitAll()
3.4、使用文档地址直接访问
除了通过后端接口动态提供文档流,也可以直接提供文档的静态URL(例如通过Nginx代理静态文件)。你可以使用一个在线的DOCX链接测试OnlyOffice服务是否部署成功:
https://d2nlctn12v279m.cloudfront.net/assets/docs/samples/zh/demo.docx
将此链接替换到前端配置的 url 字段中即可测试。
4、后记
集成OnlyOffice实现在线编辑功能的过程可能会遇到一些挑战,但一旦打通,将为Web应用带来强大的文档处理能力。希望本文的实践总结能为你提供清晰的路径参考。如果你在 前端框架/工程化 或其他技术集成方面有更多心得,欢迎在 云栈社区 交流分享。