在项目开发中,需要实现前端页面在线编辑Word文档并保存的功能。经过调研,选择了开源组件OnlyOffice,它支持文档转换、多人协同编辑、文档打印等丰富功能,本文重点介绍文档编辑相关的实现方案。
1、OnlyOffice部署
OnlyOffice支持Docker部署和本地安装两种方式。Docker部署相对简单,只需拉取镜像并配置参数即可启动;本地安装步骤稍多但稳定性较好。以下是两种部署方式的参考文档:
本地部署经过验证可完整实现功能,建议初次使用者参考Ubuntu部署方案。
2、代码开发实现
采用Vue + Element UI作为前端框架,SpringBoot作为后端技术栈进行开发。
2.1 前端配置
参考OnlyOffice官方API文档:
在页面中引入OnlyOffice JS文件:
<div id="placeholder"></div>
注意将documentserver替换为实际的OnlyOffice服务地址。
核心配置代码:
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)
其中import.meta.env.VITE_APP_API_URL需配置为实际的后端服务地址,callbackUrl用于处理文档操作的回调通知。
2.2 后端实现
添加HTTP客户端依赖:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
</dependency>
核心控制器代码:
@Api(value = "OnlyOfficeController")
@RestController
public class OnlyOfficeController {
@Autowired
private IMeetingTableService meetingTableService;
private String meetingMinutesFilePath;
@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;
}
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) {
Integer status = callbackData.getStatus();
switch (status) {
case 1:
// 文档正在编辑
break;
case 2:
// 文档准备保存
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:
// 文档保存错误
System.out.println("document saving error has occurred,保存出错");
break;
case 4:
// 文档关闭无修改
System.out.println("document is closed with no changes,未保存退出");
break;
case 6:
// 编辑中自动保存
String autoSaveUrl = callbackData.getUrl();
try {
saveFile(autoSaveUrl);
} catch (Exception e) {
System.out.println("保存文件异常");
}
System.out.println("save success.");
break;
case 7:
// 强制保存错误
System.out.println("error has occurred while force saving the document. 强制保存文档出错");
break;
default:
break;
}
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 {
private Object changeshistory;
private Object history;
private String changesurl;
private String filetype;
private Integer forcesavetype;
private String key;
private Integer status;
private String url;
private Object userdata;
private String[] users;
private String lastsave;
private String token;
}
}
工具类HttpsKitWithProxyAuth和JsonUtil提供文件下载和JSON处理功能,完整代码参见文末参考链接。
3、常见问题处理
3.1 服务启动异常
部署完成后若example访问失败,使用以下命令检查服务状态:
systemctl status ds*
3.2 文档加载失败
修改OnlyOffice配置文件解决权限问题,配置文件位置:/etc/onlyoffice/documentserver
-
local.json中禁用token验证:
"token": {
"enable": {
"request": {
"inbox": false,
"outbox": false
},
"browser": false
}
}
-
default.json中调整安全设置:
"request-filtering-agent": {
"allowPrivateIPAddress": true,
"allowMetaIPAddress": true
},
"token": {
"enable": {
"browser": false,
"request": {
"inbox": false,
"outbox": false
}
}
},
"rejectUnauthorized": false
修改后重启服务生效。
3.3 系统Token验证冲突
若后端系统需要Token验证,在安全配置中放行相关接口:
requests.antMatchers("/callback", "/getFile/*", "/login", "/register", "/captchaImage").permitAll()
3.4 文档地址访问优化
除API方式外,可通过Nginx代理直接访问文档地址。测试时可使用在线文档链接:
https://d2nlctn12v279m.cloudfront.net/assets/docs/samples/zh/demo.docx
通过以上配置和问题处理,可稳定实现基于Docker的OnlyOffice在线文档编辑系统。