突破限制:vue-plugin-hiprint 富文本支持深度解析与解决方案

前言

本文章旨在介绍该插件对于富文本的支持情况,及二次开发以支持部分富文本元素显示提供思路。对于初次接触的人可以先看文章不限于Vue!vue-plugin-hiprint 打印插件完整使用指南,里面包含了我对这个插件的全部了解,之后再看本文章。

插件对于富文本的支持情况

分析过程

这一步需要查看该插件具体是如何渲染的各个元素,因为此处的分析没有什么太多价值,都是力气活儿,所以只介绍一下思路:从hiprintTemplate.getHtml入手,查找各个元素是什么时候被具体转为了真正的dom。排查后可以总结出这样一条渲染链路

hiprintTemplate.getHtml=>panel.getHtml=>元素的getHtml

需要注意的是,由于每一种类型的元素的真实dom不同,所以他们的getHtml也就不同,只有找寻对应元素的getHtml才能知道它们是如何渲染的。所以我们可以直接打印hiprintTemplate的实例,按照 printPanels.printElements的路径找到对应的元素实例,[[Prototype]]上即可找对应的getHtml。

分析结果

1.内置类型html

js 复制代码
function(t, e, printData) {
    return '<div style="height:50pt;width:50pt;background:red;border-radius: 50%;"></div>';
}

html通过formatter函数可以渲染出dom元素,通过printData获取到富文本数据,在return中返回即可。

-:html内部没有分页的逻辑,虽然可以渲染,但是超过当前页的数据会被截断。如果使用这个元素的话,就需要自己完善html的分页逻辑,目测相当麻烦。

2.longText

在插件的使用过程中发现longText中直接放置富文本,文字的部分是可以渲染出来,并且longText本身支持处理长文本分页的情况,虽然该元素类型的本身的分页方法只能处理普通文本,但是起码分页的部分不用自己从头写。

不足:1.HTML转义符截断,虽然大部分文本都可以正常显示,但是如果分页刚巧在转义符中间(比如:"),那么这个转义符就会被截断 2.标签截断,当分页刚巧在标签上的时候(比如:<img sr="" />的自中间),图片将无法显示,文本部分会出现例如 /> 剩余的残缺字符。

两者相比,我觉得还是longText的改造难度小一些,所以接下来就是二次开发longText。

二次开发longText

思路

问题1:如何获取元素的原型

由于元素的getHTML方法是放在元素的原型上的,我们想要二次开发getHTML方法就需要找到它的原型。

为了找到原型,我们就需要从初始化模板hiprintTemplate.PrintTemplate()开始,一直找到元素创建实例的部分。

最终发现可以通过 getElementType 获取元素类型实例,通过元素类型实例的createPrintElement即可创建对应的元素实例,而 hiprint.PrintElementTypeManager上就有getElementType

js 复制代码
hiprint.PrintElementTypeManager.getElementType(tid).createPrintElement()

通过传入longText的tid,最终获取到了元素的实例

问题2:如何处理截断问题

首先分析了longText的getHtml,大概的逻辑是他把整个长文数据拆成单个字符的数组 ,然后通过 IsPaginationIndex 渲染临时容器判断是否超过页面的大小。超过就是使用二分法寻找分页点,因为是这种直接断开的方式所以,会造成截断现象。

js 复制代码
IsPaginationIndex = function(e3, t3, i4, n3) {
    // e3: 字符数组
    // t3: 尝试的索引位置
    // i4: 最大允许高度(px)
    // n3: 临时容器
​
    // 有高度限制模式
    // 测试:包含 t3+2 个字符
    n3.find(".hiprint-printElement-longText-content")
      .html(e3.slice(0, t3 + 2).join(""));
    var r2 = n3.height();  // 高度1
    
    // 测试:包含 t3+1 个字符  
    n3.find(".hiprint-printElement-longText-content")
      .html(e3.slice(0, t3 + 1).join(""));
    var A3 = n3.height();  // 高度2
    
    // 判断逻辑:
    return t3 >= e3.length - 1 && A3 < i4 ?  // 情况1:处理完所有字符且未超限
        { IsPagination: true, height: o.a.px.toPt(A3), length: e3.length, target: n3.clone() } : 
        A3 <= i4 && i4 <= r2 ?  // 情况2:当前高度<=限制<=下一字符高度(找到分页点)
        { IsPagination: true, height: A3, length: t3 + 1, target: n3.clone() } : 
        i4 <= A3 ?  // 情况3:限制高度小于等于当前高度(向左搜索)
        { IsPagination: false, move: "l" } : 
        r2 <= i4 ?  // 情况4:限制高度大于等于下一字符高度(向右搜索)
        { IsPagination: false, move: "r" } : 
        { IsPagination: true, result: 1 };  // 其他情况
}

我想的解决方法就是,识别分页点所在的位置是否处于不能被截断的字符中间,返回不能被截断的字符的开头索引。

代码

整体代码不变,只需要替换情况2时,重新找到分页点即可,也就是将

js 复制代码
{ IsPagination: true, height: A3, length: t3 + 1, target: n3.clone() }

替换为

js 复制代码
{
    IsPagination: true,
    height: A3,
    length: (length = adjustSplitForRichText(e3, t3 + 1), length),
    target: (n3.find(".hiprint-printElement-longText-content").html(e3.slice(0, length).join("")), n3.clone()) // 因为已经重新设置分页点了,所以前一页的内容需要按新分页点重新渲染
}
js 复制代码
// html: 字符数组
// position: 原始分页位置
// 返回值: 调整后的安全分页位置
function adjustSplitForRichText(html, position) {
    // 检查并调整HTML实体
    const entityAdjusted = getEntityEndOrPosition(html, position);
    if (entityAdjusted !== position) return entityAdjusted;
    
    // 检查并调整图片标签
    const imgAdjusted = getImgTagStartIfInside(html, position);
    if (imgAdjusted !== -1) return imgAdjusted;
    
    return position;
}

我在这里只写了识别HTML实体和图片标签的方法,因为目前项目中只用到了这些,如果需要更多的富文本支持,就需要自己写更多的识别算法,这个就看需求了。

getEntityEndOrPositiongetImgTagStartIfInside都是用来识别不可拆分字符串的算法,我的算法放在这里仅作参考,如果有性能需求,可以自己重新实现。

js 复制代码
// 常见HTML实体集合
const HTML_ENTITIES = new Set([
    'nbsp', 'lt', 'gt', 'amp', 'quot', 'apos',
    'ldquo', 'rdquo', 'lsquo', 'rsquo',
    'mdash', 'ndash', 'copy', 'reg', 'trade',
    'euro', 'pound', 'cent', 'yen', 'sect',
    'deg', 'plusmn', 'times', 'divide',
    'middot', 'bull', 'hellip', 'prime', 'Prime',
    'oline', 'frasl', 'weierp', 'image', 'real',
    'alefsym', 'larr', 'uarr', 'rarr', 'darr',
    'harr', 'crarr', 'lArr', 'uArr', 'rArr', 'dArr',
    'hArr', 'forall', 'part', 'exist', 'empty',
    'nabla', 'isin', 'notin', 'ni', 'prod', 'sum',
    'minus', 'lowast', 'radic', 'prop', 'infin',
    'ang', 'and', 'or', 'cap', 'cup', 'int',
    'there4', 'sim', 'cong', 'asymp', 'ne',
    'equiv', 'le', 'ge', 'sub', 'sup', 'nsub',
    'sube', 'supe', 'oplus', 'otimes', 'perp',
    'sdot', 'lceil', 'rceil', 'lfloor', 'rfloor',
    'lang', 'rang', 'loz', 'spades', 'clubs',
    'hearts', 'diams', 'ensp', 'emsp', 'thinsp',
    'zwnj', 'zwj', 'lrm', 'rlm', 'shy'
]);
​
function getEntityEndOrPosition(html, position, maxLook = 7) {
    // 边界检查
    if (position < 0 || position >= html.length) {
        return position;
    }
    
    // 向前查找&,最多查找maxLook个字符
    let ampPos = -1;
    for (let i = position; i >= Math.max(0, position - maxLook); i--) {
        if (html[i] === '&') {
            ampPos = i;
            break;
        }
    }
    
    // 没有找到&,返回原位置
    if (ampPos === -1) {
        return position;
    }
    
    // 向后查找;,最多查找maxLook个字符
    let semicolonPos = -1;
    for (let i = ampPos + 1; i < Math.min(html.length, ampPos + maxLook + 1); i++) {
        if (html[i] === ';') {
            semicolonPos = i;
            break;
        }
    }
    
    // 没有找到;,返回原位置
    if (semicolonPos === -1) {
        return position;
    }
    
    // 提取&和;之间的内容
    const entityContent = html.slice(ampPos + 1, semicolonPos).join('');
    
    // 检查是否为数字实体(&#123;)或十六进制实体(&#xA0;)
    if (entityContent.startsWith('#')) {
        // 数字实体或十六进制实体,返回&位置
        return ampPos;
    }
    
    // 检查是否在HTML实体集合中
    if (HTML_ENTITIES.has(entityContent.toLowerCase())) {
        // 是有效的HTML实体,返回&的位置
        return ampPos;
    }
    
    // 不是有效的HTML实体,返回原位置
    return position;
}
​
function getImgTagStartIfInside(htmlArray, position, maxLook = 100) {
    if (position < 0 || position >= htmlArray.length) return -1;
    
    // 向前查找,最多maxLook个字符
    for (let i = position-1; i >= Math.max(0, position - maxLook); i--) {
        // 如果先遇到>,说明在标签外部
        if (htmlArray[i] === '>') {
            return -1;
        }
        
        // 如果遇到<,检查是否是<img
        if (htmlArray[i] === '<') {
            // 检查是否是<img标签
            if (i + 3 < htmlArray.length &&
                htmlArray[i + 1].toLowerCase() === 'i' &&
                htmlArray[i + 2].toLowerCase() === 'm' &&
                htmlArray[i + 3].toLowerCase() === 'g') {
                
                // 确保img后面是空格、>或/(避免匹配<imagine>等)
                if (i + 4 < htmlArray.length) {
                    const nextChar = htmlArray[i + 4];
                    if (nextChar !== ' ' && nextChar !== '>' && nextChar !== '/') {
                        return -1;
                    }
                }
                
                // 是<img标签,返回开始位置
                return i;
            }
            
            // 是其他标签,不在img内部
            return -1;
        }
    }
    
    // 没找到<,也不在img内部
    return -1;
}

注意事项

  1. 更新风险:直接修改原型方法可能在插件更新时失效

结语

以这种方式处理的富文本数据,在一定程度上可以解决对应截断问题,但是如果需要更加完善的解决方法,还是得写一套完整的富文本解析,嵌入到longText的方法中,提交给vue-plugin-hiprint的作者。抛砖引玉,希望有哪位大神完善一下吧~~~

如果文章你有所帮助,还请不吝啬您的点赞👍👍👍

相关推荐
Bigger2 小时前
shadcn-ui 的 Radix Dialog 这两个警告到底在说什么?为什么会报?怎么修?
前端·react.js·weui
用户4099322502122 小时前
Vue3中v-if与v-for为何不能在同一元素上混用?优先级规则与改进方案是什么?
前端·vue.js·后端
与兰同馨2 小时前
【踩坑实录】一次 H5 页面在 PC 端的滚动条与轮播图修复全过程(Vue + Vant)
前端
全栈技术负责人2 小时前
前端架构演进之路——从网页到应用
前端·架构
T___T2 小时前
React Props:从基础使用到高级组件封装
前端·react.js
汉堡大王95272 小时前
React组件通信全解:父子、子父、兄弟及跨组件通信
前端·javascript·前端框架
霍理迪2 小时前
CSS继承,优先级以及字体样式
前端·css
LeeHK2 小时前
在项目中调试vue2源码,watch,nextTick执行顺序梳理
前端
爱敲点代码的小哥3 小时前
json序列化和反序列化 和 数组转成json格式
java·前端·json