我们公司目前在做基于tiptap的在线协同文档,最近需要做导出 pdf、word 需求。
导出 word 文档使用的是html-docx-js-typescript,是用 typescript 重写了一下html-docx-js,可以看到最近的提交记录是 2016 年,貌似已经不维护了,很多 Issues 没人管。
实在找不到其他的 html 转 word 的插件,最后只能使用它来处理,我把我在使用过程中遇到的问题一一列出来,就有了这篇避坑指南。
(资料图片)
安装
安装html-docx-js-typescript
,同时安装FileSaver用于浏览器端保存文件。
npm install html-docx-js-typescript file-saver --save-devnpm install @types/html-docx-js @types/file-saver --dev
使用方法
参考官方示例
字体加粗和标记文本元素
标签需要替换为
和
标签
const innerHtml = cloneEle.innerHTML // strong在word中不生效问题 .replace(//g, "") .replace(/<\/strong>/g, "") // 背景色不生效问题 .replace(//g, "")
我们文档中的标题对应的 HTML 内容长这样
需要将内容转换为类似
这样,不然 word 中编辑时不能对应标题,修改如下:xxx
// 标题高度和字体失效 需要设置lineHeight和fontWeightconst 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 = "" }) )}
Prosemiror-images上传图片后,会在图片后面生成.ProseMirror-separator
这个标签,我们在导出时只需要删除它即可。
const removeWhiteBox = (cloneEle: HTMLElement) => { const separators: NodeListOf= cloneEle.querySelectorAll( ".ProseMirror-separator" ) separators.forEach((separator) => separator.parentElement?.removeChild(separator) )}
在开始处理之前,先介绍一个插入 DOM 的 APIinsertAdjacentElement。
在 vue、react 这些框架的盛行,基本上我们已经不会再用到 DOM 操作,不过可以了解一下,万一以后用得到呢。
// 将给定元素element插入到调用的元素的某个位置element.insertAdjacentElement(position, element)
参数position
可以是以下位置
接着我们看一下列表这部分的修改,由于我们项目功能上的需求,列表是使用 div 标签来改造的,所以需要将 div 标签转为 ul/ol,下面是我的实现
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)) // 内容区域 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))
复选框 checkbox 的处理,首先考虑的是转为来处理,结果转完后并没有显示复选框;
接着又想着用 span 标签生成一个方框,,这样总能显示了吧!结果依然不行。
正当我想不到办法的时候,突然灵机一动,可不可以把 word 转成 html 后看看 checkbox 最终会显示成啥样呢?
于是通过在线 word 转 html将 word 转为 html 后,看到复选框对应的 html 内容为
,改一下吧。
const span = document.createElement("span")span.innerHTML = ``marker.insertAdjacentElement("beforebegin", span)marker.parentElement?.removeChild(marker)
转成 word 后,复选框的选中和取消功能也能正常使用。
参考了一下钉钉文档
这样就很好改了,只需要把附件对应的节点内容,改为链接即可。
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 = `${title}` attach.insertAdjacentElement("beforebegin", a) attach.parentElement?.removeChild(attach)})
其实,处理这些问题的方式也是很简单,因为html-docs-js
是用html字符串来作为导出文档的输入。如果导出后发现样式不对的情况时,我们只需要去修改html内容即可。
如果有遇到像复选框checkbox这类不知道怎么解决的问题,也可以采用反推,先通过word转html,然后看转为html后的内容,再去修改需要导出的html内容,这也不失为一种解决问题的方式。
以上是我在使用html-docs-js
插件时遇到的一些问题及处理方式,如果有遇到同样问题的小伙伴,可以说下你们的处理方式。或者这里没有提到的问题,也欢迎大家补充。
标签: