前言
我們公司目前在做基于tiptap的在線協(xié)同文檔术吗,最近需要做導(dǎo)出 pdf、word 需求珊拼。
導(dǎo)出 word 文檔使用的是html-docx-js-typescript础拨,是用 typescript 重寫了一下html-docx-js瘩绒,可以看到最近的提交記錄是 2016 年,貌似已經(jīng)不維護(hù)了,很多 Issues 沒人管。
實(shí)在找不到其他的 html 轉(zhuǎn) word 的插件吐句,最后只能使用它來處理,我把我在使用過程中遇到的問題一一列出來店读,就有了這篇避坑指南。
使用說明
-
安裝
安裝
html-docx-js-typescript
攀芯,同時(shí)安裝FileSaver用于瀏覽器端保存文件屯断。npm install html-docx-js-typescript file-saver --save-dev npm install @types/html-docx-js @types/file-saver --dev
-
使用方法
參考官方示例
使用過程遇到的問題及處理方案
字體加粗不生效、字體背景顏色不生效處理
字體加粗<strong>
和標(biāo)記文本元素<mark>
標(biāo)簽需要替換為<b>
和<span>
標(biāo)簽
const innerHtml = cloneEle.innerHTML
// strong在word中不生效問題
.replace(/<strong>/g, '<b>')
.replace(/<\/strong>/g, '</b>')
// 背景色不生效問題
.replace(/<mark/g, '<span')
.replace(/<\/mark>/g, '</span>')
h1 - h6 標(biāo)題高度優(yōu)化及未同步 word 文檔標(biāo)題
我們文檔中的標(biāo)題對應(yīng)的 HTML 內(nèi)容長這樣
需要將內(nèi)容轉(zhuǎn)換為類似<h1>xxx</h1>
這樣侣诺,不然 word 中編輯時(shí)不能對應(yīng)標(biāo)題殖演,修改如下:
// 標(biāo)題高度和字體失效 需要設(shè)置lineHeight和fontWeight
const handleLevelStyle = (cloneEle: HTMLElement) => {
Array.from({ length: 6 }).forEach((_, index) =>
(cloneEle.querySelectorAll(`h${index + 1}`) as unknown as HTMLElement[]).forEach((h) => {
h.innerText = (h.children[0] as HTMLElement).innerText
h.style.fontSize = ''
})
)
}
圖片下多出一個(gè)白框
Prosemiror-images上傳圖片后,會在圖片后面生成.ProseMirror-separator這個(gè)標(biāo)簽年鸳,我們在導(dǎo)出時(shí)只需要刪除它即可趴久。
const removeWhiteBox = (cloneEle: HTMLElement) => {
const separators: NodeListOf<Element> = cloneEle.querySelectorAll(
'.ProseMirror-separator'
)
separators.forEach((separator) =>
separator.parentElement?.removeChild(separator)
)
}
列表 ul、ol
在開始處理之前搔确,先介紹一個(gè)插入 DOM 的 API insertAdjacentElement彼棍。
在 vue、react 這些框架的盛行膳算,基本上我們已經(jīng)不會再用到 DOM 操作座硕,不過可以了解一下,萬一以后用得到呢涕蜂。
// 將給定元素element插入到調(diào)用的元素的某個(gè)位置
element.insertAdjacentElement(position, element)
參數(shù)position
可以是以下位置
- 'beforebegin': 插入元素之前华匾,類似 insertBefore
- 'afterbegin': 插入元素第一個(gè) children 之前,類似 prepend
- 'beforeend': 插入元素最后一個(gè) children 之后机隙,類似 appendChild
- 'afterend': 插入元素之后蜘拉,類似 insertAfter
接著我們看一下列表這部分的修改,由于我們項(xiàng)目功能上的需求有鹿,列表是使用 div 標(biāo)簽來改造的旭旭,所以需要將 div 標(biāo)簽轉(zhuǎn)為 ul/ol,下面是我的實(shí)現(xiàn)
const changeDiv2Ul = (div: HTMLElement | Element, parent?: HTMLElement | Element) => {
const kind = div.getAttribute('data-list-kind')
const ul = kind === 'ordered' ? document.createElement('ol') : document.createElement('ul')
const li = document.createElement('li')
// 去除margin 不然在word中會偏移
!parent && (ul.style.margin = '0')
li.innerHTML = div.innerHTML
ul.appendChild(li)
parent ? parent.insertAdjacentElement('afterend', ul) : div.insertAdjacentElement('afterend', ul)
div.parentElement?.removeChild(div)
li.querySelectorAll('.list-marker').forEach((marker) => marker.parentElement?.removeChild(marker))
// 內(nèi)容區(qū)域
li.querySelectorAll('.list-content').forEach((content) => {
const span = document.createElement('span')
span.innerHTML = (content.firstChild as HTMLElement).innerHTML
content.insertAdjacentElement('beforebegin', span)
if (content.querySelectorAll('.prosemirror-flat-list').length) {
content.querySelectorAll('.prosemirror-flat-list').forEach((div) => changeDiv2Ul(div, content))
}
content.parentElement?.removeChild(content)
})
}
cloneEle.querySelectorAll('.prosemirror-flat-list').forEach((div) => changeDiv2Ul(div))
復(fù)選框 checkbox
復(fù)選框 checkbox 的處理印颤,首先考慮的是轉(zhuǎn)為<input type='checkbox' />
來處理您机,結(jié)果轉(zhuǎn)完后并沒有顯示復(fù)選框;
接著又想著用 span 標(biāo)簽生成一個(gè)方框,<span style='width: 16px;height: 16px...' />
际看,這樣總能顯示了吧咸产!結(jié)果依然不行。
正當(dāng)我想不到辦法的時(shí)候仲闽,突然靈機(jī)一動脑溢,可不可以把 word 轉(zhuǎn)成 html 后看看 checkbox 最終會顯示成啥樣呢?
于是通過在線 word 轉(zhuǎn) html將 word 轉(zhuǎn)為 html 后赖欣,看到復(fù)選框?qū)?yīng)的 html 內(nèi)容為<span style="color:#333333; font-family:'Wingdings 2'; font-size:11pt">?</span>
屑彻,改一下吧。
const span = document.createElement('span')
span.innerHTML = `<span style="color:#333333; font-family:'Wingdings 2'; font-size:11pt">?</span>`
marker.insertAdjacentElement('beforebegin', span)
marker.parentElement?.removeChild(marker)
轉(zhuǎn)成 word 后顶吮,復(fù)選框的選中和取消功能也能正常使用社牲。
附件導(dǎo)出、多維表等 iframe 內(nèi)容
參考了一下釘釘文檔
這樣就很好改了悴了,只需要把附件對應(yīng)的節(jié)點(diǎn)內(nèi)容搏恤,改為鏈接即可。
cloneEle.querySelectorAll('.attachment-node-wrap').forEach((attach) => {
const title = `請至One文檔查看附件《${attach.getAttribute('name')}》`
const anchorId = attach.parentElement?.getAttribute('data-id')
const a = document.createElement('a')
a.target = '_blank'
a.href = `${location.href}&anchor=${anchorId}`
a.innerHTML = `<span>${title}</span>`
attach.insertAdjacentElement('beforebegin', a)
attach.parentElement?.removeChild(attach)
})
未解決的部分
- 表情無法導(dǎo)出湃交,這個(gè)我看了下其他在線協(xié)作文檔熟空,也有同樣的問題。
小結(jié)
其實(shí)搞莺,處理這些問題的方式也是很簡單息罗,因?yàn)閔tml-docs-js是用html字符串來作為導(dǎo)出文檔的輸入。如果導(dǎo)出后發(fā)現(xiàn)樣式不對的情況時(shí)才沧,我們只需要去修改html內(nèi)容即可迈喉。
如果有遇到像復(fù)選框checkbox這類不知道怎么解決的問題,也可以采用反推温圆,先通過word轉(zhuǎn)html弊添,然后看轉(zhuǎn)為html后的內(nèi)容,再去修改需要導(dǎo)出的html內(nèi)容捌木,這也不失為一種解決問題的方式油坝。
以上是我在使用html-docs-js插件時(shí)遇到的一些問題及處理方式,如果有遇到同樣問題的小伙伴刨裆,可以說下你們的處理方式澈圈。或者這里沒有提到的問題帆啃,也歡迎大家補(bǔ)充瞬女。