缘起于进行了一次在线 Office 解决方案的调研,对比了 Office365、可道云、WPS Office、PageOffice 等厂商,最终敲定了使用 Onlyoffice,故整理了一份 Onlyoffice 从零开始系列教程,这是第一篇。
Onlyoffice 是一个多端协同的 Office 办公套件,相当于微软的 Office365 全家桶。
功能强大到什么程度呢?我列了一下 Onlyoffice 对我们需求的支持程度:
| 需求 | 支持程度 |
|---|---|
| 终端支持 | 全端支持,包含桌面端、PC 网页端、移动端等 |
| 客户端操作系统 | Windows、Mac、Linux |
| 服务端操作系统 | Linux、Ubuntu、CentOS、Debian、Alibaba Cloud Image 等 Docker 镜像包 |
| 基础功能 | 具备 Word 基础的字体设置、字体大小、加粗、对齐、颜色、背景颜色等功能,同时还有等同于 Office 的各个高级功能 |
| 插件支持 | 支持自定义插件,官方提供了完整的插件开发文档 |
| 二次开发 | 官方开放了 1000+ API,支持根据业务二次开发、功能定制,甚至扩展或增强基础功能 |
| 深度定制 | 支持 |
| 使用体验 | 安装成本低,编辑体验与本地 Word 高度一致 |
| 开发者社区 | 官方维护了一个开发者社区,内容丰富,也比较活跃 |
| 安全与稳定性 | 文档加密保存、传输支持标准的 JWT 加密,多种场景测试未出现崩溃、卡死等情况 |
| 协同 | 支持多人协同编辑、历史记录查看,文档回滚等功能 |
| 分布式部署 | 支持 |
社区版免费,企业版收费,10w 起步。
预算、私有云、需要二次开发、需要文档协同等。
正文
本文使用docker进行安装,故:
a、windows安装:Windows10 Docker 安装教程-CSDN博客
b、mac安装:【云原生丶Docker】MacOS系统安装Docker【保姆级教程】_mac安装docker-CSDN博客
1、使用JWT验证
sudo docker run -i -t -d -p 8701:80 onlyoffice/documentserver
2、不使用JWT验证
sudo docker run -i -t -d -p 8701:80 --restart=always -e JWT_ENABLED=false onlyoffice/documentserver
可以以后面加上版本号:
sudo docker run -i -t -d -p 8701:80 --restart=always -e JWT_ENABLED=false onlyoffice/documentserver:7.1.1
# -e JWT_ENABLED=false:不使用JWT
sudo docker run -i -t -d -p 8701:80 --restart=always --name=onlyoffice_7.3.3 -e JWT_ENABLED=false onlyoffice/documentserver:7.3.3
从7.2版本起,默认使用了JWT功能,安装Onlyoffice时,可以通过不同的命令参数启动服务,默认不使用JWT验证!如果是第 1 次执行这个命令,会先去下载 Onlyoffice,比较慢,约等待 3~10 分钟,网络畅通一点的会快一些。如果是已经安装过则直接进行启动。待其安装完成:

安装完成后:

1、等上述安装完成后执行命令 ,查看 Onlyoffice 容器 ID:
docker ps

2、执行以下命令进入容器,这里将获取到的 ID 替换为上个步骤你得到的自己的ID!
sudo docker exec -it ID /bin/bash

3、接着执行下面的这两个命令
# 启动所有的内置服务
supervisorctl restart all
# 退出容器
exit
命令输出如下:

4、最后访问 http://IP:8701/welcome 页面(网上其他人说IP 不能是 localhost 或 127.0.0.1,一定要用你自己本地真实 IP 来访问。但是我经过测试完全可以),看到下面的这个效果说明 Onlyoffice 启动成功。
此页面提供了在线文档新增、编辑等功能,你可以点击生成一个文档,后续开发测试功能时会用到。



1、子组件准备,在你的项目的合适目录下新建如下两个文件,将下方的代码复制粘贴进去到你对应的文件中。

index.vue页面代码:
- <div :class="s.view">
- <div :id="id">div>
- div>
-
- <script>
- import loadScript from './loadScript.js';
-
- export default {
- name: 'DocumentEditor',
- props: {
- id: {
- type: String,
- default: '',
- },
- documentServerUrl: {
- type: String,
- default: '',
- },
- config: {
- type: Object,
- default: () => { },
- },
- document_fileType: {
- type: String,
- default: '',
- },
- document_title: {
- type: String,
- default: '',
- },
- documentType: {
- type: String,
- default: '',
- },
- editorConfig_lang: {
- type: String,
- default: '',
- },
- height: {
- type: String,
- default: '',
- },
- type: {
- type: String,
- default: '',
- },
- width: {
- type: String,
- default: '',
- },
- events_onAppReady: {
- type: Function,
- default: () => { },
- },
- events_onDocumentStateChange: {
- type: Function,
- default: () => { },
- },
- events_onMetaChange: {
- type: Function,
- default: () => { },
- },
- events_onDocumentReady: {
- type: Function,
- default: () => { },
- },
- events_onInfo: {
- type: Function,
- default: () => { },
- },
- events_onWarning: {
- type: Function,
- default: () => { },
- },
- events_onError: {
- type: Function,
- default: () => { },
- },
- events_onRequestSharingSettings: {
- type: Function,
- default: () => { },
- },
- events_onRequestRename: {
- type: Function,
- default: () => { },
- },
- events_onMakeActionLink: {
- type: Function,
- default: () => { },
- },
- events_onRequestInsertImage: {
- type: Function,
- default: () => { },
- },
- events_onRequestSaveAs: {
- type: Function,
- default: () => { },
- },
- events_onRequestMailMergeRecipients: {
- type: Function,
- default: () => { },
- },
- events_onRequestCompareFile: {
- type: Function,
- default: () => { },
- },
- events_onRequestEditRights: {
- type: Function,
- default: () => { },
- },
- events_onRequestHistory: {
- type: Function,
- default: () => { },
- },
- events_onRequestHistoryClose: {
- type: Function,
- default: () => { },
- },
- events_onRequestHistoryData: {
- type: Function,
- default: () => { },
- },
- events_onRequestRestore: {
- type: Function,
- default: () => { },
- },
- },
- data() {
- return {};
- },
- watch: {
- config: {
- handler() {
- this.onChangeProps();
- },
- deep: true,
- },
- document_fileType() {
- this.onChangeProps();
- },
- document_title() {
- this.onChangeProps();
- },
- documentType() {
- this.onChangeProps();
- },
- editorConfig_lang() {
- this.onChangeProps();
- },
- height() {
- this.onChangeProps();
- },
- type() {
- this.onChangeProps();
- },
- width() {
- this.onChangeProps();
- },
- },
- mounted() {
- let url = this.documentServerUrl;
- if (!url.endsWith('/')) {
- url += '/';
- }
- const docApiUrl = `${url}web-apps/apps/api/documents/api.js`;
- loadScript(docApiUrl, 'onlyoffice-api-script')
- .then(() => this.onLoad())
- .catch((err) => console.error(err));
- },
- beforeDestroy() {
- const id = this.id || '';
- if (window?.DocEditor?.instances[id]) {
- window.DocEditor.instances[id].destroyEditor();
- window.DocEditor.instances[id] = undefined;
- }
- },
- methods: {
- onLoad() {
- try {
- const id = this.id || '';
- if (!window.DocsAPI) throw new Error('DocsAPI is not defined');
- if (window?.DocEditor?.instances[id]) {
- console.log('Skip loading. Instance already exists', id);
- return;
- }
- if (!window?.DocEditor?.instances) {
- window.DocEditor = { instances: {} };
- }
- const initConfig = {
- document: {
- fileType: this.document_fileType,
- title: this.document_title,
- },
- documentType: this.documentType,
- editorConfig: {
- lang: this.editorConfig_lang,
- },
- events: {
- onAppReady: this.onAppReady,
- onDocumentStateChange: this.events_onDocumentStateChange,
- onMetaChange: this.events_onMetaChange,
- onDocumentReady: this.events_onDocumentReady,
- onInfo: this.events_onInfo,
- onWarning: this.events_onWarning,
- onError: this.events_onError,
- onRequestSharingSettings: this.events_onRequestSharingSettings,
- onRequestRename: this.events_onRequestRename,
- onMakeActionLink: this.events_onMakeActionLink,
- onRequestInsertImage: this.events_onRequestInsertImage,
- onRequestSaveAs: this.events_onRequestSaveAs,
- onRequestMailMergeRecipients: this.events_onRequestMailMergeRecipients,
- onRequestCompareFile: this.events_onRequestCompareFile,
- onRequestEditRights: this.events_onRequestEditRights,
- onRequestHistory: this.events_onRequestHistory,
- onRequestHistoryClose: this.events_onRequestHistoryClose,
- onRequestHistoryData: this.events_onRequestHistoryData,
- onRequestRestore: this.events_onRequestRestore,
- },
- height: this.height,
- type: this.type,
- width: this.width,
- ...(this.config || {}),
- };
- const editor = window.DocsAPI.DocEditor(id, initConfig);
- window.DocEditor.instances[id] = editor;
- } catch (err) {
- console.error(err);
- this.events_onError(err);
- }
- },
- onAppReady() {
- const id = this.id || '';
- this.events_onAppReady(window.DocEditor.instances[id]);
- },
- onChangeProps() {
- const id = this.id || '';
- if (window?.DocEditor?.instances[id]) {
- window.DocEditor.instances[id].destroyEditor();
- window.DocEditor.instances[id] = undefined;
-
- console.log('Important props have been changed. Load new Editor.');
- this.onLoad();
- }
- },
- },
- };
- script>
-
- <style lang="scss" module="s">
- .view {
- width: 100%;
- height: 100%;
-
- iframe {
- width: 100%;
- height: 100%;
- }
- }
-
- style>
loadScript.js文件:
- const loadScript = async (url, id) =>
- new Promise((resolve, reject) => {
- try {
- if (document.getElementById(id)) {
- if (window.DocsAPI) return resolve(null);
-
- const intervalHandler = setInterval(() => {
- if (!window.DocsAPI) return;
-
- clearInterval(intervalHandler);
-
- return resolve(null);
- }, 500);
- } else {
- const script = document.createElement("script");
- script.setAttribute("type", "text/javascript");
- script.setAttribute("id", id);
-
- script.onload = resolve;
- script.onerror = reject;
-
- script.src = url;
- script.async = true;
-
- document.body.appendChild(script);
- }
- } catch (e) {
- console.error(e);
- }
- });
-
- export default loadScript;
2、在页面中使用。在合适的位置创建如下页面,将代码复制粘贴进去。

docPreview.vue代码
-
- <div class='qualityManual-container'>
- <div class='qualityManual-container-office'>
- <vab-only-office id="office-preview" :documentServerUrl='documentServerUrl' :config="config" />
- div>
- div>
-
- <script>
- import vabOnlyOffice from '@/components/docPreview/index.vue'
-
- export default {
- components: { vabOnlyOffice },
- data() {
- return {
- documentServerUrl: "http://192.168.0.15:8701/",
- config: {
- document: {
- fileType: "docx",
- key: "Khirz6zTPdfd7",
- title: "Example Document Title.docx",
- url: "http://192.168.0.15:8701/example/editor?fileName=new.docx"
- },
- documentType: "word",
- editorConfig: {
- callbackUrl: "https://example.com/url-to-callback.ashx"
- }
- }
- }
- },
- methods: {
- //这里的val是传递的参数
- loadOnlyOffice(val) {
- this.option.key = // key 默认置空则不走缓存
- this.option.title = '' // 该文件名在下载文档时也将用作文件名
- this.option.url = // 定义存储原始查看或编辑的文档的绝对URL
- this.option.fileType = 'docx' // 文件类型
- this.option.callbackUrl = '' // 回调地址
- this.show = true // 打开onlyOffice窗口
- console.log(val, '编辑word默认配置参数')
- },
- }
- }
- script>
-
- <style rel="stylesheet/scss" lang="scss">
- .qualityManual-container {
- padding: 0 !important;
- width: 100%;
- height: calc(100vh - 180px);
- }
-
- .qualityManual-container-office {
- width: 100%;
- height: calc(100% - 55px);
- }
-
- style>
下来则是重点功能分析及使用:
- data() {
- return {
- //本地onlyoffice安装成功后的服务
- documentServerUrl: "http://192.168.0.15:8701/",
- config: {
- document: {
- fileType: "docx",
- key: "Khirz6zTPdfd7",
- title: "Example Document Title.docx",
- //你要打开的文档绝对路径,这里可以使用7.4页面左侧去生成文档并复制其文档地址进行开发测试!
- url: "http://192.168.0.15:8701/example/editor?fileName=new.docx"
- },
- documentType: "word",
- editorConfig: {
- callbackUrl: "https://example.com/url-to-callback.ashx"
- }
- }
- }
- },
运行项目查看!祝你成功。