TinyMCE word 文档导入
2024年1月9日 • ☕️ 5 min read
前言
遇到这种需求第一时间当然是看看有没有可以直接用的轮子,但是找到的 ImportWord 插件无法正常使用,而且很久没有维护了,所以只能自己动手实现这个功能了
上网冲浪后得知,word 文档解析主要有 mammoth.js 和 docx-preview.js 这两个库,据网友所言,后者效果更为甚之,故用之
开始
在 TinyMCE 菜单添加导入按钮
<template>
...
<input type="file" ref="importWordUploadRef" accept=".docx" style="display: none;" @input="handleWordFileUpload" />
...
</template>
<script setup>
const importWordUploadRef = ref()
const initOptions = {
...
// 添加 importWord
toolbar: '... | importWord | ...',
setup: () => {
// 注册 importWord
editor.ui.registry.addButton("importWord", {
text: "导入word",
// icon:'', // 目前使用文字按钮,如果需要图标展示,根据文档中自定义图标中的内容进行配置
onAction: function () {
// 触发文件上传
importWordUploadRef.value.click()
},
})
}
}
</script>
效果如下:
核心逻辑实现
安装 docx-preview
npm install docx-preview
使用
import * as Docx from 'docx-preview'
// docx-preview 没有提供直接返回 html 的方法,所以用创建 dom 接收 html 的方法“曲线救国”
const renderHtmlAsync = async wordFile => {
const importWordUploadContainerDom = document.createElement("div")
await Docx.renderAsync(wordFile, importWordUploadContainerDom, null, {
// 取消宽高限制,不然会固定一个页面大小,展示效果不好
ignoreHeight: true,
ignoreWidth: true
})
// 获取 style 标签
const wordStyleContent = Array.from(importWordUploadContainerDom.querySelectorAll('style')).map(dom => dom.outerHTML).join('')
// 去除 word 的纸页容器
const wordHtmlContent = Array.from(importWordUploadContainerDom.querySelectorAll('section.docx')).map(dom => dom.innerHTML).join('')
return wordStyleContent + wordHtmlContent
}
const handleWordFileUpload = (e) => {
const wordFile = e.srcElement.files[0]
// 将上传文件的 input value 置空,否则会出现第二次上传相同文件的时候不回调的问题
importWordUploadRef.value.value = null
const wordHtmlContent = await renderHtmlAsync(wordFile)
// 将导入的 word 内容追加到富文本内容中
tinymceEditor.setContent(tinymceEditor.getContent() + wordHtmlContent)
}
遇到的坑
文件类型丢失
使用 TinyMCE 的 setContent
方法时,如果 html 里有图片,会触发 TinyMCE 的 images_upload_handler
方法。我们可以在这里将word 原有的图片上传到服务器上。但是实际操作时会发现一个问题:docx-preview.js 在渲染 html 的时候,将 word 中的文件替换成了临时的 blob 链接,而且这个 blob 的 type 变成了 text/plain
,这就意味着原文件的类型丢失了。如果直接上传这个 blob 文件,下载这个文件时只能得到 .blob
后缀的文件
images_upload_handler: blobInfo => {
console.log(blobInfo.blob())
}
再次冲浪后得知,就算 blob 中的 type 丢失了,还有方法可以获取文件的真实类型 —— 魔数
魔数
文件起始的几个字节内容是固定的,这几个字节的内容记录着文件的类型,这些内容被称为”Magic Number(魔数/幻术)”
file-type 这个 js 库就是通过魔数去获取文件类型的,所以我们可以使用这个库读取文件的真实类型
file-type
安装
安装 16.5.4 版本的 file-type,19.0.0 版本经过测试已经无法使用下面提到的方法解决 Buffer undefined 的问题,其他版本暂时不确定行不行,建议直接安装 16.5.4 版本
npm install file-type@16.5.4
使用
import { fromBuffer } from 'file-type/core'
const blobType = await fromBuffer(await blob.arrayBuffer())
因为 file-type 是 nodejs 的库,如果直接在浏览器中使用,会出现 Buffer is not defined 的报错
我们可以手动设置 Buffer 全局变量来解决这个问题
安装 buffer 依赖
npm install buffer
一定要注意,这里安装的依赖是 buffer 而不是 Buffer
import { Buffer } from 'buffer'
// 添加 Buffer 全局变量
window.Buffer = window.Buffer || Buffer
至此,就能完整的实现 TinyMCE 导入 word 文件的功能了,并且可以将导入的图片上传到服务器上:
images_upload_handler: (blobInfo, succFun, failFun) => {
return new Promise(async (resolve) => {
let file = blobInfo.blob()
if (isUnknownTypeBlob(blobInfo.blobUri())) {
const blob = blobInfo.blob()
const blobType = await fromBuffer(await blob.arrayBuffer())
file = new File([blob], `${Date.now()}.${blobType?.ext || 'blob'}`)
}
exampleUploadApi(file).then(res => {
succFun && succFun(res.fileName)
resolve(res.fileName)
// 主动触发 input 事件,图片地址替换之后不会主动触发 input 事件
setTimeout(() => {
tinyMCEEditor.fire('input')
})
}).catch(err => {
failFun && failFun(err.message || 'error')
})
})
}
样式丢失
导入 word 之后发现有部分样式丢失,而且在 docx-preview 提供的 demo 上面没有问题,最后发现是 TinyMCE 在导入 html 的时候”动了手脚”——TinyMCE 把 style 标签、没有属性的 span 标签都移除了,解决方法如下:
const initOptions = {
...
// 允许添加 style 标签
valid_children : '+body[style]',
// 防止 tinyMCE 移除没有属性的 span 标签,保留所有标签的 style 属性
extended_valid_elements: 'span|*[style]',
...
}
完整示例
l123wx/tinyMCE-docx-preview-demo (github.com)
核心代码:
<template>
<div>
<!-- <textarea :id="TINYMCE_ID"></textarea> -->
<input type="file" ref="importWordUploadRef" accept=".docx" style="display: none;" @input="handleWordFileUpload" />
</div>
</template>
<script>
import { Buffer } from 'buffer'
window.Buffer = window.Buffer || Buffer
</script>
<script setup>
import * as Docx from 'docx-preview'
import { fromBuffer } from 'file-type/core
const TINYMCE_ID = "selector_" + Date.now()
const importWordUploadRef = ref()
const isUnknownTypeBlob = (blobUri) => {
return blobUri.startsWith('blob')
}
const initOptions = {
...
toolbar: 'importWord',
images_upload_handler: (blobInfo, succFun, failFun) => {
return new Promise(async resolve => {
let file = blobInfo.blob()
if (isUnknownTypeBlob(blobInfo.blobUri())) {
const blob = blobInfo.blob()
const blobType = await fromBuffer(await blob.arrayBuffer())
file = new File(
[blob],
`${Date.now()}.${blobType?.ext || 'blob'}`
)
}
exampleUploadImageMethod(file).then(res => {
succFun && succFun(res.fileName)
resolve(res.fileName)
}).catch(err => {
failFun && failFun(err.message || 'error')
})
})
},
setup: editor => {
editor.ui.registry.addButton("importWord", {
text: "导入word",
onAction: function () {
importWordUploadRef.value.click()
},
})
}
...
}
const renderHtmlAsync = async wordFile => {
const importWordUploadContainerDom = document.createElement("div")
await Docx.renderAsync(wordFile, importWordUploadContainerDom, null, {
ignoreHeight: true,
ignoreWidth: true
})
importWordUploadContainerDom.querySelector('.docx').style.padding = 0
const wordHtmlContent = importWordUploadContainerDom.innerHTML
return wordHtmlContent
}
const handleWordFileUpload = async (e) => {
const wordFile = e.srcElement.files[0]
importWordUploadRef.value.value = null
const wordHtmlContent = await renderHtmlAsync(wordFile)
tinymceEditor.setContent(tinymceEditor.getContent() + wordHtmlContent)
}
</script>
相关链接
VolodymyrBaydalka/docxjs: Docx rendering library (github.com)
Browser compatibility questions · Issue #354 · sindresorhus/file-type (github.com)
幻数(编程) - 维基百科,自由的百科全书 --- Magic number (programming) - Wikipedia
sindresorhus/file-type: Detect the file type of a Buffer/Uint8Array/ArrayBuffer (github.com)
mammoth.js 预览 word docx 文档 使用示例 demo example (jstool.gitlab.io)