当前位置 博文首页 > 前端开发博客:前端海报生成的不同方案和优劣

    前端开发博客:前端海报生成的不同方案和优劣

    作者:[db:作者] 时间:2021-07-03 22:06

    点击上方?前端开发博客,回复“加群”

    加入我们一起学习,天天进步

    一、背景

    工作中做了很多生成海报的功能,不同需求,不同场景,使用了几种方案,各有优劣。一直想要整理一下,但这个过程中的思考和遇到的问题没有记录下来,比如图片的跨域问题,文字的问题,做完没有记录,无迹可寻,以至于很难开始。最近重新回顾了一下,整理了一篇文档,或有疏漏和不准确之处,欢迎交流。

    二、业务应用

    pc:

    1.分享海报

    H5应用:

    1.分享优惠券
    2.年度账单
    3.分享海报

    三、实现方式及兼容性问题

    第三方库

    1. html2canvas

    版本 1.0.0rc7

    https://github.com/niklasvh/h...

    star:22k

    时间 2021-01-06

    首页测试demo:https://html2canvas.hertzen.com/

    兼容性测试
    安卓
    • ? 6.0 koobee

    • ? 5.0.2 vovoY51A

    安卓微信版本-内置
    • ? 7.0.3

    • ? 7.0.22

    ios
    • ? 14.2

    • ? 11.2.1

    ios微信内置
    • ? 7.0.20

    • ? 7.0.1

    优点:

    兼容性更好,官方描述如下:

    Browser?compatibility
    The?library?should?work?fine?on?the?following?browsers?(with?Promise?polyfill):
    
    Firefox?3.5+
    Google?Chrome
    Opera?12+
    IE9+
    Safari?6+
    As?each?CSS?property?needs?to?be?manually?built?to?be?supported,?there?are?a?number?of?properties?that?are?not?yet?supported.
    
    缺点:

    一些属性不支持转化:如不支持伪类,border不支持dash,不支持text-shadow,(即还原程度需要和设计沟通,或者考虑采用图片等形式去替换显示元素)

    features:https://html2canvas.hertzen.c...

    备注

    rc5出现过ios生成不了图片的问题,还原到rc4就可以了
    https://github.com/niklasvh/h...

    看到issue里有出现ios某些版本下微信失败的情况,暂没有复现

    2 dom-to-img

    版本2.6.0

    https://github.com/tsayen/dom...

    star:6.9k

    优点

    元素齐全,还原度高

    缺点

    不兼容safari,所以建议只在Chrome下使用

    没有维护更新了

    兼容性问题

    1.作者明确表示不支持safari,因为foreignObject的安全性问题(明说了不支持,我仔细看文档 => 白作工 )

    链接:

    • https://github.com/tsayen/dom...

    • https://html.spec.whatwg.org/...

    • SVG在Web攻击中的应用: https://www.anquanke.com/post...

    2.iOS14.2下,图片丢失问题
    其实在测的时候,发现ios14.2下生成图片第一次始终会出现图片丢失,第二次或第三次正常
    解决方案,2~3次调用,取最后一次(看issue里有些机型还是不支持的)

    3.低端安卓机上会出现失败情况,主要是文字的问题(这里是我写过的旧的记录,不是很确定)

    应用于Pc或android
    1. 清晰度问题:\(看到很多人说要去改源码,其实不用的,作者是提供了参数了\)
      
    通过scale放大设备的DPR倍数
    let?elm?=?document.getElementById('my-node')
    const?scale?=?window.devicePixelRatio
    this.loader.open()
    await?this.$nextTick()
    domtoimage
    ????.toBlob(elm,?{
    ?????????quantity:?1,
    ?????????width:?elm.offsetWidth?*?scale,?//?画布放大
    ?????????height:?elm.offsetHeight?*?scale,
    ????????style:?{
    ????????????transform:?`scale(${scale})`,?//?元素放大
    ????????????transformOrigin:?'0?0',
    ????????},
    ????})
    ????.then(this.upload)?//?处理结果
    ????.catch(function?(error)?{
    ????????this.loader.close()
    ????????console.error('oops,?something?went?wrong!',?error)
    ????})
    

    前端canvas绘制

    0.canvas
    let?canvasBox?=?document.createElement('canvas')
    let?ctx?=?canvasBox.getContext(
    ????????????????????'2d'
    ????????????????)?as?CanvasRenderingContext2D
    
    1.img
    let?bgImag?=?''
    ctx.drawImage(bgImg,?0,?0)
    
    1.2图片跨域问题
    解决办法?:
    1)使用 crossOrigin 属性。
    解决canvas图片getImageData,toDataURL跨域问题
    2)图片本身也需要允许跨域
    3)设置 useCORS:true,原理相同,但使用以上跨域方法,若同时设置为 allowTaint:true?,仍然会认为画布已被污染而不可用;
    
    2.文字
    2.2文字换行
    核心:计算所有文字,根据每行可显示的最大宽度,来拆分成每行渲染
    参考:https://www.zhangxinxu.com/wordpress/2018/02/canvas-text-break-line-letter-spacing-vertical/
    
    2.2字体类型

    2.2.1.只采用默认字体或少量定制字体(Fontmin获取特定字体的字体,写死的数据,如果换行需要计算换行问题-空格回车等奇葩问题)- ==@font-face,这样使用是涉及版权问题的,确保你们有该字体的版权==

    @font-face?{
    ????font-family:?"DIN?Condensed";
    ????//?prettier-ignore
    ????src:?url("~@/assets/fonts/DIN-Condensed-Bold.woff")?format("woff"),?url("~@/assets/fonts/DIN-Condensed-Bold.ttf")?format("ttf");
    }
    
    @font-face?{
    ????font-family:?"PingFang?SC?Bold";
    ????//?prettier-ignore
    ????src:?url("~@/assets/fonts/PingFang-SC-Bold.ttf")?format("ttf"),?url("~@/assets/fonts/PingFang-SC-Bold.woff")?format("woff");
    }
    ctx.fillText('20px?PingFang?SC?Bold',?x,?y)
    ctx.fillText('20px',?x,?y)
    

    2.2.2(一定要完整某种字体的情况下:动态变化的数据,需要接口支持)

    svg?to?img
    直接domtoSvg也有安卓失败的问题
    let?svg?=?接口获取svg(参考年度账单)
    let?svgBase64:?string?=
    ????'data:image/svg+xml;base64,'?+
    ????window.btoa(
    ????????unescape(encodeURIComponent(svg))
    ????)
    
    await?this.loadImg(svgBase64).then(
    ????img?=>?{
    ????????ctxC.drawImage(
    ????????????img,
    ????????????~~(20?*?this.scale),
    ????????????~~(186?*?this.scale)
    ????????)
    ????}
    )
    
    3.手机端比例计算
    //?尽量使用整数
    
    //?devicePixelRatio
    protected?imgQuality?=?window.devicePixelRatio
    
    //?和设计稿的比例?设计稿宽度是640
    get?scale()?{
    ????return?(document.body.clientWidth?*?this.imgQuality)?/?640
    }
    
    //?画布宽高,避免生成的图片模糊情况出现
    canvasBox.width?=?Math.ceil(
    ????document.body.clientWidth?*?this.imgQuality
    )
    canvasBox.height?=?Math.ceil(980?*?this.scale)
    
    //?二维码的宽度
    get?qrCodeSize()?{
    ????return?~~(155?*?this.scale)?//?rpx?*?scale
    }
    
    4.常见元素/线/圆/圆角矩形
    //?画布和设计稿750的比例
    scale?=??(document.body.clientWidth?*?this.imgQuality)?/?750
    线:
    **
    ?*?画线
    ?*/
    export?function?drawLine(
    ????ctx:?CanvasRenderingContext2D,
    ????data:?{
    ????????x:?number
    ????????y:?number
    ????????width:?number
    ????????height:?number
    ????????strokeStyle:?string
    ????},
    ????scale:?number
    )?{
    ????let?x?=?data.x?*?scale
    ????let?y?=?data.y?*?scale
    ????let?dx?=?(data.x?+?data.width)?*?scale
    ????let?dy?=?data.y?*?scale
    
    ????//?开始一个新的绘制路径
    ????ctx.beginPath()
    ????//?定义直线的起点坐标为(10,10)
    ????ctx.moveTo(x,?y)
    ????//?定义直线的终点坐标为(50,10)
    ????ctx.lineTo(dx,?dy)
    ????//?颜色
    ????ctx.strokeStyle?=?data.strokeStyle
    ????//?沿着坐标点顺序的路径绘制直线
    ????ctx.stroke()
    ????//?关闭当前的绘制路径
    ????ctx.closePath()
    }
    
    圆:
    /**
    ?*?画圆
    ?*/
    export?function?drawCircle(
    ????ctx:?CanvasRenderingContext2D,
    ????data:?{
    ????????x:?number?//?圆心
    ????????y:?number
    ????????size:?number?//?半径
    ????????fillStyle:?string
    ????},
    ????scale:?number
    )?{
    ????ctx.save()
    ????ctx.beginPath()
    ????ctx.arc(
    ????????~~(data.x?*?scale),
    ????????~~(data.y?*?scale),
    ????????~~(data.size?*?scale),
    ????????0,
    ????????~~(data.size?*?scale?*?Math.PI)
    ????)
    ????ctx.fillStyle?=?data.fillStyle
    ????ctx.fill()
    ????ctx.restore()
    }
    
    矩形:
    
    /**
    ?*?画圆角矩形
    ?*/
    export?function?drawRoundRect(
    ????ctx:?CanvasRenderingContext2D,
    ????data:?{
    ????????x:?number
    ????????y:?number
    ????????width:?number
    ????????height:?number
    ????????radius:?number
    ????????fillStyle:?string
    ????},
    ????scale:?number
    )?{
    ????let?width?=?~~(data.width?*?scale)
    ????let?height?=?~~(data.height?*?scale)
    ????let?radius?=?~~(data.radius?*?scale)
    ????let?fillStyle?=?data.fillStyle
    ????ctx.save()
    ????ctx.translate(~~(data.x?*?scale),?~~(data.y?*?scale))
    ????ctx.beginPath()
    ????//?从右下角顺时针绘制,弧度从0到1/2PI
    ????ctx.arc(width?-?radius,?height?-?radius,?radius,?0,?Math.PI?/?2)
    
    ????//?矩形下边线
    ????ctx.lineTo(radius,?height)
    
    ????//?左下角圆弧,弧度从1/2PI到PI
    ????ctx.arc(radius,?height?-?radius,?radius,?Math.PI?/?2,?Math.PI)
    
    ????//?矩形左边线
    ????ctx.lineTo(0,?radius)
    
    ????//?左上角圆弧,弧度从PI到3/2PI
    ????ctx.arc(radius,?radius,?radius,?Math.PI,?(Math.PI?*?3)?/?2)
    
    ????//?上边线
    ????ctx.lineTo(width?-?radius,?0)
    
    ????//?右上角圆弧
    ????ctx.arc(width?-?radius,?radius,?radius,?(Math.PI?*?3)?/?2,?Math.PI?*?2)
    
    ????//?右边线
    ????ctx.lineTo(width,?height?-?radius)
    ????ctx.closePath()
    ????ctx.fillStyle?=?fillStyle
    ????ctx.fill()
    ????ctx.restore()
    }
    
    5.canvasBox.toBlob
    canvasBox.toBlob(async?data?=>?{
    ????if?(data)?{
    ????????//上传到7niu
    ????????this.url?=?await?uploadFile(data)
    ????}
    })
    
    6.图片保存
    PC
    import?{?saveAs?}?from?'file-saver'
    saveAs(blob,?filename)
    
    H5
    长按保存
    包壳的H5是可以实现直接下载的,但为了交互了一致性,还是用了长按
    

    接口生成

    接口生成,传入元素和位置等信息,接口直接生成返回图片。需要后端实现。(具体的性能和使用案例,欢迎讨论。)

    优点

    不需要考虑兼容性等问题

    缺点

    不支持字数或字体类型过多,服务器压力较大(看具体实现方案),元素越多,接口越慢

    这个方案其实也是用后端的逻辑实现了绘制元素,输出图片(过程中遇到的问题:如换行情况下需要计算字体高度(同一字体的中英文宽度不同)和后续元素的相对位置发生变化)

    四、图片跨域

    如果存在跨域图片无法下载,仔细阅读以下文字:
    来源:https://segmentfault.com/q/10...

    五、总结

    无特殊情况时,pc下使用dom-to-image即可。
    至于h5,回顾了一下自己绘制海报的实现历程:
    1.第三方库(兼容性问题,太久远以至于不记得发生了什么,只记得这个方案被驳回了)
    2.接口绘制(服务器过载性能问题,速度过慢)
    3.前端canvas绘制(+部分元素接口绘制好返回图片或svg,基本没什么兼容问题)(速度过慢)
    4.第三方库(html2canvas,测试了一些版本环境的兼容性,但项目暂未上线,需要观察)
    目前最优解看起来是html2canvas,但是有些效果无法实现,需要在还原度和性能上做取舍。

    六、更好的方案?

    一些常用的app的海报生成是更快的,体验也更好,不知道是不是存在更好的解决方案?还是h5的限制?

    七、参考

    1. 【JS】节点截图的最终解决方案dom-to-image与html2canvas:https://blog.csdn.net/Mcky_Lo...

    2. 移动端H5页面截图:https://cloud.tencent.com/dev...

    3. 把DOM节点生成base64图片:https://www.jianshu.com/p/c5c...

    4. 更优雅地基于 canvas 在前端画海报:https://juejin.cn/post/684490...

    5. canvas文本绘制自动换行、字间距、竖排等实现 https://www.zhangxinxu.com/wo...

    6. 图片跨域问题:https://segmentfault.com/q/1010000008648867

    作者:JiaXinYihttps://segmentfault.com/a/1190000038910770
    

    - EOF -

    推荐阅读??点击标题可跳转

    JavaScript实现网页截屏方法总结

    H5基于canvas实现电子签名并生成PDF文档

    小程序如何生成海报分享朋友圈

    觉得本文对你有帮助?请分享给更多人

    推荐关注「前端开发博客」,提升前端技能

    回复「1024」领取前端进阶资料

    如果觉得这篇文章还不错,来个【分享、点赞、在看】三连吧,让更多的人也看到~

    cs
    下一篇:没有了