markdown数据转换,处理html2canvas+jsPDF下载后文字截断问题(记录)

文章介绍了在前端将ES中的Markdown数据解析并下载为PDF格式的挑战,包括Markdown转换插件的选择(如marked和markdown-it)、处理文字截断问题,以及动态计算分页的策略。作者详细讨论了marked的不足和markdown-it的优势,并分享了如何通过计算元素高度避免文字被截断的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

声明:此篇文章并不是最优解决办法,下载pdf这一步主要参考睡衣大佬提供的思路和代码,个人在此基础上进行细微修改处理段落文字截断,勉强实现不截断文字效果,但也有诸多限制和不足。
原文引路:https://blog.youkuaiyun.com/web2022050903/article/details/127069911)

项目场景:

项目要求前端将es中的markdown数据解析显示在网页上,并要求提供下载功能,将单个文章下载为pdf格式。


一、markdown数据处理

第一次处理markdown转换,就开启了我的踩坑历程(以下碎碎的怨念可以忽略(ˉ▽ˉ;)…):
1、数据格式踩坑
  据说文章格式固定,就让他们提供了一份样例文件,结果,在文章内容那里,他们直接把es字符串拼到了json中,一导入就报错不是json,因为拼进去的文字换行,是敲回车的空白换行,而不是\n,就像这样:

{
	title:"标题1",
	"content":"**这是一段介绍** 
			
			介绍内容介绍内容介绍内容介绍内容介绍内容介绍内容"	 
}

嗯,是换行了,在编译器里也能看到,但是content的格式前端解析不了啊!!!最后沟通多次,终于把返回数据的换行变成\n,然后开始继续踩坑

2、markdown插件选择
markdown转换插件有很多种,都大同小异。

刚开始采用最简单的marked,该插件采用的原理是替换(类似于replace(a,b)),使用后可以帮你把\n换成<br>,相关markdown格式语法都能精准转换,还有配套的美化css,效率高;唯一不足的是,只帮你把对应的语法标签替换,并没有将段落文字处理成block元素包裹。

最后下载pdf时发现文字被无情截断,网上查找诸多办法,可行的思路是计算块级元素的高度,给需要分页的位置加class样式标识。然而,marked转化后,除了原始包裹的div,整体就是一个涵盖全部内容的元素。没有块级元素就没法计算每一段落的高度,又怎么添加分页节点呢!!
然后尝试markdown-it,发现该插件可以将段落文字用<p></p>包裹,只需要简单配置即可(参考markdown-it中文文档),以下是我项目中的配置:

const md = new MarkdownIt({
  html:true,//在源码中启用 HTML 标签(true为启用)
  xhtmlOut:true,// 使用 '/' 来闭合单标签 (比如 <br/>)
  breaks:true,//转换段落里的'\n'到 <br>(true为启用,否则会直接把\n字符串返回)
  linkify:false// 将类似 URL 的文本自动转换为链接。
 });
 //渲染内容到页面
 this.page = md.render(str)//str是接口获取到的文章字符串

html

<div v-html="page" class="markdown-body1" ref="downArea1"></div>

如果字符串被一对\n\n包裹,则会被一个<p></p>包裹


二、生成dom转pdf下载

1、问题
  块级元素基本划分出来了,但是到哪里分页咱不知道,如果是固定的dom结构,咱还能自己加个class循环遍历,或者后端传回有标记的html也行。现在,dom是根据文章json内容自己生成的,多种元素出现位置都不固定,只能计算元素自力更生了。

2、未成功的尝试
对于不固定的结构,尝试过如下方法:
①网上热度比较高的“元素超过一页内容就分页”法,和图片高度分法差不多,最终的效果是:某段落高度 > 该页剩余高度,这个段落被放到下一页,上一页底部留下一片空白;嗯,这样确实不会出现文字截断的问题了,但可能是某元素过大,计算的间隔过大,有时会在中间留下空白页。
②import “@/utils/markdown/bookjs-eazy.min.js”; ,评论下推荐的这个,可能是我使用方式不对,引用时运行卡死。
③利用columns相关设置解决图片和文字被无情截断问题,源码jq编写,原谅我笨/(ㄒoㄒ)/~~,不知道怎么跑起来,作者也没有给出回复。

3、分析
  经历了种种折磨,决定还是从基层入手,这里先对pdf添加图片的参数做一个简单介绍:

//(dom转换成的图片,图片格式,x轴偏移,y轴偏移,放入图片的宽度,放入图片的高度)
PDF.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)

(1)首先,将整个需要转换的dom看成一个大图,有个A4纸大小的木框作为可视区域;以大图左上角为原点,右下为正向区域,y值越小,大图就相当于往上平移拉动,我们即可从木框中将一篇文章从上往下浏览完毕。
(2)imgWidth和imgHeight是生成图片的高度,由计算获取,调整imgHeight并不能控制你截取内容的范围,因为在转换图片时已经将内容固定好了,如果此时减小imgHeight值,只会呈现pdf文字变扁的效果。
(3)综上,还是得在生成图片之前截取适当高度作为一页内容

4、文字截断处理
认真研究了大佬对于分页的理解和方法,对于加页眉页脚,以及首尾间距确实解决的很好(链接在顶部)。以下是个人的理解:
(1)原理分析:生成内容类型
特殊元素:比如图片、富文本、表格等,后两种我没用到。通过假数据模拟测试,图片分页确实很准,不会出现把图片拦腰截断的状况。总结计算原理就是:
①该元素顶部距上一个分页距离直接 > 一页内容,说明这个元素直接在第二页,直接加一页内容高度
②该元素距上一个分页距离 + 该元素高度 > 一页内容应当的高度,说明这张A4放不下,比如图片,为了保持完整图片只能将截取标记放到图片之前

普通元素:比如我文章中会渲染出的段落<p></p>,这个和上面的原理①一样,没检测到分页不会处理,有超出直接计入一页内容的高度,然后就导致横跨两页中间的一段文字容易出现拦腰砍断的现象。

虽然会出现文字截断,但我比较倾向于这种对普通段落文字铺满一页的处理结果,因为如果像2、①中那样,段落高度导致生成pdf后每张纸都大量空白,看上去感觉怪怪的,打印出来还浪费纸张。那么就要重新确定段落分页位置。

(2)解决:修改普通元素转换代码

以我UI给的设计图为例,段落文字的字号为16px,每行高度为28px,这样算下来,一行文字的上下空白距离为6px。为了方便计算元素剩余高度,最好在css中提前将<p>等自带的margin改为内padding间距。

还有,由于markdownit转化后,图片代码有双换行也会将图片用<p>包裹,但段落横跨截取是根据文字整行来计算,容易在外层判断时就把整个图片拦腰截断,所以在判断横跨时加了图片判断。
最后一个判断语句,如果剩余内容不满一行,就直接在这个元素前进行截断,向上稍微挪到上一行字底紧贴文字;如果剩余内容可以放多行,那就截取到整行前,并向上稍微挪到上一行字底紧贴文字。

js修改:

	//添加两行代码
  const fontHeight = rate * 28;//在转换后的行高
  const fontBottom = rate * 6;//转换后行高与文字底部距离是(28-16)/ 2 = 6
  ...
  function updateNomalElPos(eheight,top,elem) {
      let outstrip = top - (pages.length > 0 ? pages[pages.length - 1] : 0) > originalPageHeight;//当前内容超过一页内容的高度
      let notOutstrip = top - (pages.length > 0 ? pages[pages.length - 1] : 0) < originalPageHeight;//当前内容位置不超过一页的高度
      let between = notOutstrip && (top - (pages.length > 0 ? pages[pages.length - 1] : 0) + eheight > originalPageHeight);//当前内容位置在分页之前但内容超过高度导致分页
      if (outstrip) {
        pages.push((pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight - pages[0]);
      }
      if(between){
        let extra = originalPageHeight - (top - pages[0] - (pages.length > 0 ? pages[pages.length - 1] : 0));
        if(elem.childNodes && elem.childNodes[0].tagName == 'IMG'){
          pages.push((pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight - extra)
        }
        if(elem.childNodes && elem.childNodes[0].tagName !== 'IMG'){
          let endDis = extra%fontHeight;
          let cut = parseInt(extra/fontHeight)
          if(cut < 1){
            pages.push(top - fontBottom)
          }else{
            pages.push((pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight - endDis - fontBottom)
          }  
        }
      }
   }

减去pages[0]是为了消除一开始偏移带来的误差:文章页内容每个元素距离父元素的offsetTop固定,但第一个截取不为0坐标的话,相当于所有后续截取都向下偏移了一个距离,那么固定某元素top - 上一个位置分页的结果就会偏小,top-pages[0]可以看作将生成的图片原点按pages的起始纵坐标统一。
或者可以写成:
let extra = originalPageHeight - (top - (pages.length > 0 ? pages[pages.length - 1] : 0)) + pages[0];

理解为计算完剩余高度后补充初始偏移的误差,即得到真实剩余高度。(下面内容会继续解释)


三、文字截断踩过的坑与弊端

1、对于横跨两页的元素,剩余距离的计算原理是:

剩余距离 = 一页盛放内容的高度 - (某元素距顶部距离 - 上一个分页截断位置)

但是不知道为什么,计算的结果和实际可放行数根本对不上,比如parseInt(endDis/fontHeight)=3,但实际打印出的pdf,剩余位置放了7.5行 ,最后一行文字被拦腰切断。
直到我把图片数据弄到分页的位置测试,发现本该分页的位置,图片多打印了一块高度,既然计算原理没错,那就是整体循环监测出现了偏移。偶然间,尝试将后续对于普通元素(也就是段落文字)的判断截取点,都减去第一个分页坐标,也就是pages[0],神奇的是,剩余位置行数对了,末行文字也不腰斩了,激动in~。但是!!这样每页底部都空了一段距离
2、所以,那个偏移位置是怎么来的呢?(这段内容很重要!!!)
代码获取元素顶部的偏移的计算是循环往上直到循环到父级有定位的元素,由markdown-it转后无法自定义,所以我初始就给包裹文章的div加了relative,文章容器上下留间距(设计与美观要求),布局如下:
在这里插入图片描述
包裹页面的祖级容器也用到了定位,所以,按照函数中获取顶部距离的方法,这个循环最终捅到了祖级容器(即上图黑框),而祖级容器由于顶部导航栏某功能样式要求,祖级relative定位不可舍弃。

3、单独设立打印页面隐藏
做出这个决定主要是两点原因:
①以上祖级定位影响实际分页点适配a4定位,但又要兼顾ui设计布局。
②整体项目采用postcss响应适配,放在小屏打印时,文字缩放太小,打印出的文字也是迷你费眼睛,而且因为尺寸过小,计算误差也增大,不可避免继续出现文字腰斩;如果保留原尺寸,未下载前在小屏浏览文章,文章16号字体比rem转换后的网站标题还大,违和感太强。(如果网页没有用适配转换的要求,可以直接将显示页面作为pdf下载打印页面,外部包裹一个无定位容器或修改源码自定义循环终止条件)
4、设立专门的打印页面
首先,取消原先打印文章容器顶部的留白距离,布局改为如下:
在这里插入图片描述
然后,将打印的真正页隐藏在给用户浏览页的背后。由于markdown-it对hidden和unvisible元素不会处理,二者设置都会使pdf下载后打开全是空白,所以使用定位来处理打印页(relative)和显示页(absolute)的层级,并用显示页的背景容器宽度100%+background遮挡。

5、文章标题失踪
文章由自建标题markdown语句和json内容拼接后交给markdown-it处理,在去掉顶部距离后,突然发现打印的标题没了,在改标题文字后发现,单行标题无法显示,两行标题只有第二行可以露出,被隐藏遮挡了一个<h1>文字高度,因此在拼接时添加了一行占位,完整字符串和效果如下:

let str = "# <font color=\"#fff\">一级标题占位,防止打印时第一行标题缺失</font>\n# " +title+"\n<font color=\"#999\">来源:"+from+" </font><font color=\"#999\">  &emsp; &emsp;    "+docDate+"</font>\n\n---\n\n"

并去掉占位<h1>的上下空白

.markdown-body>>h1:first-child{margin-top:0;margin-bottom:0;}

(ps:粉色块为调试时内容部分,方便查看文字截断情况,完成后背景要改为白色)
在这里插入图片描述
6、弊端
(1)需要配合修改常见元素的css样式:
  如果需要展示1~6级标题的上下距离,需要将margin都改为padding,<p></p>同理。最重要的是,文章内容与定位包裹的父级,顶部不要有padding或margin间距
(2)此改法对数据内容有局限性:
  比如在上面最大留取文字行数我用的固定28行高值,是因为我和其他同事沟通过,说在正文讲述部分不会出现h1等大文字情况,而我文章中的标题只设置有下padding,h2级纯文本24px,包括h2以下字号都不会超过28这个范围。所以,这里没有对其他情况做限制,有需要可以自己添加限制条件。

### 解决方案概述 当使用 `html2canvas` 和 `jsPDF` 导出 PDF 文件时,可能会遇到文字截断问题。这通常是因为 HTML 页面的内容超出了单页范围,在生成图像的过程中未能正确分割页面内容所致[^1]。 以下是针对此问题的具体分析和解决方案: --- #### 一、原因分析 1. **HTML 结构复杂** 如果页面中的某些元素高度较大或存在嵌套结构,则可能导致分页逻辑无法正常工作,从而引发文字或其他内容被截断的情况[^3]。 2. **CSS 样式冲突** 特定 CSS 属性(如 `position: absolute;` 或者 `transform`)可能会影响渲染效果,进而导致部分区域未被捕获到最终的截图中[^4]。 3. **分辨率设置不当** 默认情况下,`html2canvas` 使用较低分辨率来绘制 DOM 节点,如果目标设备屏幕密度较高 (DPI),则会显得模糊甚至丢失细节信息[^5]。 --- #### 二、具体解决方法 ##### 方法 1:调整 html2canvas 配置参数 通过修改 `scale`, `dpi`, 及其他选项可以改善输出质量并减少裁剪现象的发生概率: ```javascript html2canvas(document.querySelector("#content"), { scale: window.devicePixelRatio, // 自适应当前显示器缩放比例 dpi: 96 * window.devicePixelRatio, // 提高清晰度 useCORS: true, allowTaint: false }).then(canvas => { const imgData = canvas.toDataURL('image/png'); // 将 canvas 数据传递给 jsPDF 进一步处理... }); ``` 此处设置了 `scale` 参数等于浏览器窗口的实际像素比率 (`devicePixelRatio`) 来匹配显示精度;同时也增大了 DPI 值以获得更加精细的结果。 ##### 方法 2:优化 HTML/CSS 设计 确保所有的布局都基于标准流模型构建,避免浮动定位以及固定宽高等属性的应用。对于那些确实需要特殊样式定义的部分,应考虑为其单独创建容器层,并赋予合适的尺寸规格以便于后续计算切割位置[^2]: ```html <div id="content"> <!-- 正常文档结构 --> </div> <style> #content { width: auto; height: fit-content; } </style> ``` 另外还需注意移除任何潜在干扰因素比如 overflow:hidden 等规则限制可见区域大小的行为模式. ##### 方法 3:自定义分页算法 为了精确控制每一页内的具体内容分布情况,可以通过 JavaScript 手动拆解原始 DOM 树形成多个独立片段后再逐一转换成图片形式拼接到一起构成完整的多页文档文件: ```javascript function splitAndExport() { let pages = []; while ($('.markdown-body1').height() > PAGE_HEIGHT_LIMIT) { var tempDiv = $('<div class="split-page"></div>'); $('.markdown-body1').append(tempDiv); // 动态测量新加入占位符后的总长度变化趋势直至满足条件为止 if ($(tempDiv).offset().top >= PAGE_BREAK_POINT) break; $(tempDiv).remove(); } } // 后续调用上述函数完成整个流程即可得到理想状态下的产物 ``` 这里展示了如何向现有主体添加辅助标记用于指示最佳切分时机的过程描述. --- ### 总结 综上所述,要彻底消除由 `html2canvas` 引发的文字截断隐患可以从以下几个方面入手尝试改进措施——合理配置插件初始化参数组合、重构前端界面设计思路遵循良好实践准则还有就是开发专属业务逻辑应对特定场景需求差异等问题均能有效缓解此类状况带来的困扰[^3]. ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值