手把手带你写一个海报分享

需求梳理

  1. 提取文章部分内容+获取文章url生成二维码,生成海报图
  2. 海报图以弹出框形式显示在页面中心,需要有遮罩层
  3. 提供下载功能,兼容不同浏览器,以及手机长按下载

开发思路

  1. 搭建好海报的html模版,便于后期填充文字以及图片进行绘制
  2. 获取文章的部分文字,渲染在html模版中
  3. 获取文章url,使用qrcodejs2插件生成二维码,渲染在html模版中
  4. 获取海报内的头部图片,对其进行处理并渲染在html模版中
  5. 在确保海报内的头部图片渲染完后,使用html2canvas插件,将html绘制成canvas
  6. 将canvas转成base64格式,进行展示(听说toBlob速度会更快,下次试试)
  7. 提供下载功能,依据不同浏览器、设备的规则做好兼容处理

流程

1.搭建模版

我们生成海报图,很多时候并不是说简单的截图去绘制出来然后保存,场景多半都是有一部分元素需要动态的自定义,有一部分是固定的元素直接获取就好。所以我们首先是根据需求绘制出所对应的html模版。我们这里生成图片使用的是html2canvas插件,它是根据你的dom去绘制出canvas,再根据canvas转为base64来渲染出图片进行保存的。所以我们需要自定义出海报模版。在这里简单贴一下代码(modal为自己写的组件,用于展示海报)

<template>
  <div id="app">
    <div class="my-text">
      <p>这是一篇文章</p>
      <span ref="text">
        记忆里的昨天,充斥着离别的味道,总是显得寂静黯然,凉凉的微风拂过,带着我的莫名惆怅,
        似落花有情流水无意的飘散在回忆的长廊,于是也就习惯了深沉的模样,习惯了寂静的枯灯。
        每当夜幕降临,浓浓的寂寞袭上心头,才知道,指缝中溜走了太多的岁月,尘埃中湮没了太多的往事,
        无奈中留下了太多的叹息。也正是这样,我常常在意犹未尽的时候,怀揣着一种感性的思维,写下浮生流年里,
        那韶光的短暂,低吟浅唱回忆的殇。记得我小的时候,从不在意光阴的流逝,直到当蹉跎的岁月,恍如一梦,什么也没留下,
        却带走了匆匆时光时。我才明白什么是珍惜,却为时已晚,只能暗叹,造化弄人。如今,我们都长大了,那会的时光早已经暗淡成苦涩的味道。
        奔跑在成长的天空下,留住的甜蜜又有几分?徘徊在这样的夜色里,我渐渐觉得,我对年少的光阴早已变成了缅怀,和蜻蜓的翅膀一样透明,
        在那不高不低的晚风中时现时隐。
      </span>
    </div>
    <div class="my-H5" ref="posterHtml">
      <div class="inner-box">
        <div class="img-box">
          <img :src="headImgUrl" alt="" @load="createPosterImg()" />
        </div>
        <div class="text-box">
          {{ posterText }}
        </div>
        <div class="footer">
          <div class="footer-left">
            <div class="left-text">
              <div class="left-hos">xxx医院汇名称</div>
              <div>发布于:2022.08.2</div>
              <p>医疗行业品牌运营专家</p>
            </div>
          </div>
          <div class="footer-right">
            <div ref="qrcodeImg"></div>
          </div>
        </div>
      </div>
    </div>
    <modal
      v-model="modalVisible"
      @downloadImg="downloadImg"
      @cancel="cancel"
    >
      <img class="poster-img" :src="posterImg" alt="" @load="showImg" />
    </modal>

    <button @click="createPoster" class="btn">创建海报</button>
  </div>
</template>
//data内的数据
 data() {
    return {
      posterImg: "", //最终生成的海报图片
      headImgUrl: "", //获取到的海报背景图
      posterText: "", //海报文字
      qrcodeUrl: "https://www.jb51.net/article/178290.htm",//供给生成二维码的图片
      modalVisible: false,
   
    };
  },

此时模版大致长这样,暂无图片、文字以及二维码

手把手带你写一个海报分享

2.获取文章内容

由于canvas 生成图片不支持webkit多行省略,所以省略号由暴力截取字符串解决。因为在html2canvas中,会将css中的display中的值映射成一个数字,而在下图的映射表中,只要找到了flex的映射值,并没有–webkit-flex的映射值,所以-webkit-flex被映射成了DISPLAY.NONE,从而导致了isVisible的计算值返回了false,最终导致了无法生成想要的canvas

    getPosterText() {
      let content = this.$refs.text.innerHTML
      this.posterText = content.length > 29 ? (content.substring(0,28) + '...') : content.substring(0,29)
    },

后面新的问题来了,如果题目中数字众多的话,截取后的文字仍会过短,无法占满两行内容,所以我们需要动态的计算每一行的宽度,判断是否超出,再进行换行。但如果第二行的末尾2个是空格的话,切除后的两个空格所占的位置可能会不足以放置省略号,

    canvasWrapTitleText(canvas, text, maxWidth, maxRowNum, font) {
      // text:传入的文本  maxWidth:文字绘制的最大宽度 maxRowNum:最大行数 font: 文字格式
      if (typeof text !== 'string') {
        return
      }
      canvas.font = font
      const arrText = text.split('') // 我们将所有的文字分割成数组
      let line = ''
      let rowNum = 1
      let backText = ''
      const Lines = []
      for (let n = 0; n < arrText.length; n++) {
        const testLine = line + arrText[n]  // 一个字一个字的向当前行添加文字
        const metrics = canvas.measureText(testLine) // 计算canvas绘制当前行的宽度
        const testWidth = metrics.width
        if (testWidth > maxWidth && n > 0) {  // 如果当前行的宽度 > 最大宽度
          if (rowNum >= maxRowNum) { //如果行数 >= 最大行数时,对最后一行数据进行切割
            const arrLine = testLine.split('')
            arrLine.splice(-3)
            let lastLine = arrLine.join('')
            lastLine += '...'
            Lines.push(lastLine)
            Lines.forEach((text) => {
              backText = backText + text
            })
            // 返回最重要渲染的文字,由html2canvas进行绘制
            return backText
          }
          Lines.push(line)
          line = arrText[n]
          rowNum += 1
        } else {
          line = testLine
        }
      }
      return text
    },
 

3.生成二维码

生成二维码时,使用的插件是qrcodejs2.js,只需传入你想要放置二维码的位置的dom以及url即可

   //生成二维码
    createQRcode() {
      this.$nextTick(() => {
        let qrcode = new QRCode(this.$refs.qrcodeImg, {
          width: 70,
          height: 70,
          colorDark: "#000000",
          colorLight: "#ffffff",
          correctLevel: QRCode.CorrectLevel.H,
        });
        qrcode.makeCode(this.qrcodeUrl); //此处传入Url
      });
    },

4.获取海报内的头部图片

我们项目中获取图片的时候,可能会发生跨域问题,所以我们使用base64来进行图片展示,base64是随着页面一起加载的不会造成跨域问题,所以要先获取图片的url然后再将其转成base64格式

我们此处使用构造函数来创建img实例,在赋予src值后就会立刻下载图片,相比于使用creatElement()方法来创建手把手带你写一个海报分享更为简便,省去了append的步骤,且一定要在onload后去压缩转码,否则可能因图片未加载传入,报错或者undefind之类的

getHeadImgUrl() {
      let url ="https://img0.baidu.com/it/u=857510153,4267238650&fm=253&fmt=auto&app=120&f=JPEG?w=1200&h=675"; 
      //此处应为获取后端提供的图片地址,自己可以用axios请求代替
      var Img = new Image();
      Img.src = url;
      // image.crossOrigin = "*";可以支持跨域图片,但是会导致ios微信报安全错误,所以使用以下写法
      Img.setAttribute("crossOrigin", "anonymous"); 
      // 解决iOS微信端报错 securityError...insource啥的、
      Img.onload = () => {
        //确保图片获取到,这是一个异步事件
        //一定要在onload后去压缩转码,否则可能因图片未加载传入,报错或者undefind之类的
        this.headImgUrl = this.getBase64Image(Img);
      };
    },

//利用canvas将图片转为base64格式的函数    
getBase64Image(img) {
      var canvas = document.createElement("canvas");
      canvas.width = img.width;
      canvas.height = img.height;
      var ctx = canvas.getContext("2d");
      ctx.drawImage(img, 0, 0, img.width, img.height);
      var dataURL = canvas.toDataURL("image/png"); // 可选其他值 image/jpeg
      return dataURL;
    },

5.绘制海报

首先我们要确保海报内的头部图片加载完了才能去进行绘制海报,因为如果那图片还未渲染出来,就进行绘制的话,最后绘制出的海报图可能会缺少头部图片。所以我们在img标签处监听图片的加载状态,当加载成功后再执行绘制海报的函数

<div class="img-box">
    <img :src="headImgUrl" alt="" @load="createPosterImg()" />
</div>

   //将html元素转换为海报图片
createPosterImg() {
      // 生成海报
      const vm = this;
      html2canvas(this.$refs.posterHtml, {
        useCORS: true,//跨域处理
        allowTaint: false, 
        logging: false, //在log中输出信息
        letterRendering: true, //在设置了字符间距时使用
      }).then(function (canvas) {
        vm.posterImg = canvas.toDataURL("image/png"); //得到的是base64格式的图片
      });
    },

成果图:

手把手带你写一个海报分享

6. 进行下载

在处理base64图片下载的时候,我们要注意Ie的兼容

先说ie的兼容,如果浏览器支持msSaveOrOpenBlob方法(也就是使用IE浏览器的时候),那么调用该方法去下载图片

downloadImg() {
       //如果是ie浏览器,对其进行处理
      if (window.navigator.msSaveOrOpenBlob) {
        let bstr = atob(this.posterImg.split(",")[1]);
        let n = bstr.length;
        let u8arr = new Uint8Array(n);
        while (n--) {
          u8arr[n] = bstr.charCodeAt(n);
        }
        let blob = new Blob([u8arr]);
        window.navigator.msSaveOrOpenBlob(blob, "chart-download" + "." + "png");
      } else {
       //否则使用此方法进行处理
        let a = document.createElement("a");
        a.href = this.posterImg; //href是要下载图片的url,此处是用h5中提供给a标签的下载方法
        a.setAttribute("download", "chart-download");
        a.click();
      }
    }
  

也可直接判断浏览器是不是为火狐浏览器,如果是的话可以直接简写下载函数

mounted() {
    if (navigator.userAgent.indexOf("Firefox") > 0) {
      this.isFirefox = true;
    }
  },

//直接使用a标签
   <a
      v-if="isFirefox"
      :href="url"
      download="qrcode.png"//文件后缀名
    >
      下载图片
</a>

**PS:**移动端下载此处并未涉及,此处提供方法,需要的可参考自取

//假设为存放海报的div
<div
   @touchstart="touchStart()"
   @touchend="touchEnd()"
>
   <img :src="URL" alt="">
</div>

//方法
data(){
  Loop:'', //  定时器 
  qrcodeUrl:'', // 后端返回的二维码图片路径
  qrcodeUrl64:'', //是后端返回二维码图片的二进制流
},
methods:{
  touchEnd() {
      //手指离开
      clearTimeout(this.Loop);
    },
    touchStart() {
      //手指触摸
      clearTimeout(this.Loop); //再次清空定时器,防止重复注册定时器
      this.Loop = setTimeout(() => {
        this.downloadIamge(this.qrcodeUrl64, "pic");
      }, 1000);
    },
     downloadIamge(imgsrc, name) {
      //下载图片地址和图片名
      var image = new Image();
      // 解决跨域 Canvas 污染问题
      image.setAttribute("crossOrigin", "anonymous");
      image.onload = function() {
        var canvas = document.createElement("canvas");
        canvas.width = image.width;
        canvas.height = image.height;
        var context = canvas.getContext("2d");
        context.drawImage(image, 0, 0, image.width, image.height);
        var url = canvas.toDataURL("image/png"); //得到图片的base64编码数据
        var a = document.createElement("a"); // 生成一个a元素
        var event = new MouseEvent("click"); // 创建一个单击事件
        a.download = name || "photo"; // 设置图片名称
        a.href = url; // 将生成的URL设置为a.href属性
        a.dispatchEvent(event); // 触发a的单击事件
      };
      image.src = imgsrc;
    },
}

项目痛点

图片跨域限制

当我们使用canvas画图的时候,使用了外域的图片,那么在我们使用toDataURL生成base64的时候就会报错

Base64 是一种用于传输 8Bit 字节码的编码方式,通过对图片进行 Base64 编码,我们可以直接将编码结果写入 HTML 或者写入 CSS,从而减少 HTTP 请求的次数。由于它随着页面一起进行加载,所以不会造成跨域问题。

页面滚动造成生成的海报空白或者残缺

当我们使用html2canvas将html绘制为canvas的时候,海报模版一定要存在水平视窗内。你会发现如果海报模版滚动到了视窗上方,此时生成的图是不完整的。

方案一:

我们使用绝对定位使其脱离文档流,左移动出去,让用户看不见海报模版。

在进行绘制之前的操作,要将海报图拉回水平视窗内。我在这采用监听滚动事件的方式,直接移动滚动条到顶部,再进行渲染。

   handleScroll() {
      // 获取滚动距顶部的距离,显示
      let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
      if (scrollTop > 0) {
        // 监听是否滚动,只要滚动了,每次点击生成的时候就让页面回到顶部,解决因滑动导致的生成图片不全甚至空白、
        var c = setTimeout(() => window.scrollTo(0, 0), 16); //滚动至顶部
      } else {
        clearTimeout(c);
      }
    },

方案二:

使用fixed定位,并将海报层级设置为负数,然后用一个背景图将其覆盖,这样海报就始终存在于视窗内部了。

不过此时存在一个问题,此时绘制出来的图片只有半截

手把手带你写一个海报分享

通过查阅html2canvas官方文档得知,针对fixed的情况,要进行特殊处理。

手把手带你写一个海报分享

此时代码只需加上scrollX、scrollY并将其设置为0即可,相当于对其直接拉回浏览器顶部

createPosterImg() {
      // 生成海报
      const vm = this;
      html2canvas(this.$refs.posterHtml, {
        useCORS: true,
        allowTaint: false, //允许跨域
        logging: false, //在log中输出信息
        letterRendering: true, //在设置了字符间距时使用
        scrollX: 0,
        scrollY: 0,
      }).then(function (canvas) {
        console.log(2);
        vm.posterImg = canvas.toDataURL("image/png", 0.8); //得到的是base64格式的图片
      });
    },

如何确保海报加载完后再进行展示

其实就是考虑将visible = true 这个操作放置于何处的问题。经过与导师讨论后,可监听渲染海报的img标签的onload事件,当海报加载完毕后再对visible进行赋值。(注意:img能成功加载的前提是存在img标签,所以控制海报图的显示和隐藏,需要使用v-show而不是v-if,因为v-if在为false的时候,dom树并没有你想要的img节点

在SSR环境中使用生成二维码插件报错 ‘document is not defined’

这是因为一些只兼容客户端的脚本被打包进去了服务端去执行。简单的说:是由于nuxt.js会在服务端渲染页面,而服务端并没有window或document。此时我们需要对其根据判断条件进行动态引入组件,因为项目在启动的时候对import后的文件进行了预编译,所以编译到document、window的时候报错,因为此时的环境并不是在客户端内,所我我们需要在客户端内执行相应操作的时候,去动态引入这个插件且使用。

  async created() {
    if (process.client) { //判断是否为客户端环境
       QRCode = await import('qrcodejs2')
    }
  },
  png支持透明 jpg不支持

如果是引入的组件无法使用

  1. 在plugins目录下新建一个js文件
  2. 在文件内引入组件,并将其挂载到全局
import Vue from 'vue'
import avatarUpload from 'vue-image-crop-upload/upload-2.vue';
Vue.component('avatar-upload',avatarUpload)
//以头像上传组件为例
  1. 将插件引入SSR项目中的nuxt.config.js文件中,并将其 ssr设置为 false,这样服务端渲染时就不会渲染这个组件了
{
  src: '@/plugins/XXXX',
  ssr: false
}

二维码过于密集,导致无法识别问题?

const URL = location.origin + this.$route.fullPath 
//fullPath与path的区别:前者带参数,后者不带参数,我们生成二维码应选择带参的

首先我们要知道二维码的长度是根据输入值得来的,在保证功能的前提下我们可以降低容错级别或者调整生成二维码的尺寸,但降低容错还是得结合自身场景来进行调整

**思考:**比如只是保存二维码在手机上进行扫描的话,那么可以将容错级别设置为最低,因为直接手机图片自身扫描的话,并不会存在遮挡的问题。但如果二维码的使用场景是打印出来置于室外供人扫描的话,可能处于一些外界因素(比如码身损毁、用户扫码距离过远)导致识别失败,所以还是需要根据实际场景来进行修改。

在容错级别已经是最低的情况下仍然太密,就说明你的内容太多了。

有关多大尺寸的二维码可以存放多少数据的完整对照关系,可以参考 QRCode 官网。

P.S. 这是数学问题,不是技术问题。

概念:QR容错级别是指QR码被遮挡或残破时依然能被识别的几率,容错级别越高抗残破或遮挡的能力就越强,同时注意,提高容错级别会增大点阵密度,识别速度随之降低。

以google的zxing库为例,zxing中QR码的容错率分为四个等级:

L(低):容错率为 7%

M(中):容错率为 15%

Q(较高):容错率为 25%

H(高):容错率为 30%

判断是移动端还是PC端

    _isMobile() {
      const flag = navigator.userAgent.match(
        /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
      )
      return flag
    },
    
    //直接调函数,true就是移动,false就是PC端

部分机型 海报标题无法加粗

原因是文字在绘制时是用font- weight: 600 进行绘制的,但是不同机型对于具体数值的解析存在差异,所以此处要用font- weight: bold

题外话

因为这个功能涉及到移动端的一些适配,所以为了更好的去查看,需要在移动端上进行调试,这里分享我所学到的几种调试方法

前提:同一局域网

IOS

硬件连接:

  1. 插线就不多说了(用原装线、不用拓展坞)
  2. 打开电脑Safari浏览器 => 开发 => 找到连接的机器
  3. 选择要调试的页面,点击分享生成二维码,手机直接扫码,开始调试

xcode连接:

  1. 使用xcode的模拟器
  2. 在模拟器的Safari浏览器中输入要调试的页面
  3. 在电脑的safari浏览时器上 => 开发 => 找到连接的机器

Android:

硬件连接:

  1. 插线!!!!选择传输文件
  2. 在手机的 关于手机位置,找到版本号,连续点击进入开发者模式,点击选择usb调试
  3. 用手机原生浏览器输入要调试的地址
  4. 打开Chrome浏览器,输入chrome://inspect/#devices

手把手带你写一个海报分享

点击inspect

手把手带你写一个海报分享