【前端探索】移动端H5生成截图海报的探索

2023-04-22 16:06:48 浏览数 (2)

需求场景

移动端H5生成图片海报分享,是比较常见的交互方式。生成的海报中,往往会有用户的个性化信息,比如头像、昵称等等。

为了实现这种交互,我们探索一下可行的实现方案。

方案选择

方案的选择,按生成图片的场所,我们可以分为前端生成服务端生成

前端生成

用html2canvas生成canvas,再由canvas生成base64图片。

服务端生成

在服务端用puppeteer运行无头浏览器,截图后传给前端。

方案对比

html2canvas

puppeteer

速度

快,页面加载完就可以生成

慢,需要在后台运行无头浏览器和传输base64图片

兼容性

支持的样式为css的子集,图片有跨域的问题

无兼容性问题

易用度

简单,主要工作在调整样式和解决html2canvas存在的问题

较简单,需要额外开发一个用于生成图片的页面

下面来对这两种方案的实现进行一个封装,上面表格中,对于易用度的比较,也是基于调用封装好的方法来作比较的。

html2canvas方案

参考文档

html2canvas

html转image

这个方案,作者也是踩了一些坑,最后才总结出一个相对稳定的组件。

我们传入需要生成图片的目标组件,由html2canvas生成canvas,再把canvas转base64图片,设置给img标签的src。

代码语言:javascript复制
/**
  * Dom转化为图片
  * @exports conver
  * @param {string} trigger  Dom的id
  * @param {string} imageElId  需要展示的图片的id
  *
  * @example
  * Dom2Image.conver("app", "appImage");
  */
const conver = function (trigger, imageElId, callback) {
  const opts = {
    useCORS: true, // (图片跨域相关)
    // y: window.pageYOffset // 解决scroll导致截图失败,
    backgroundColor: 'transparent',
  };
  const imgDom = document.getElementById(trigger);
  if (imgDom) { // 放大canvas,避免截图模糊
    const canvas = document.createElement('canvas');
    const scale = 3;
    const width = imgDom.offsetWidth;
    const height = imgDom.offsetHeight;
    canvas.width = width * scale;
    canvas.height = height * scale;

    opts.scale = scale;
    opts.canvas = canvas;
    opts.width = width;
    opts.height = height;
  }

  loader(html2canvasJSLink, () => {
    html2canvas(document.getElementById(trigger), opts).then((canvas) => {
      const base64ImgSrc = canvas.toDataURL('image/png');
      const img = document.getElementById(imageElId);
      img.src = base64ImgSrc;
      if (callback && typeof callback === 'function') {
        callback();
      }
    })
      .catch((error) => {
        console.log(error);
      });
  });
};

解决图片链接跨域问题

如果生成图片的dom中,存在图片链接,在移动端可能会报一个图片跨域的错误,这是因为html2canvas是用html的download属性,来请求图片链接的,在移动端这个属性几乎全不支持。

为了解决这个问题,有两个方案:

  1. 把图片链接改为本地图片。
  2. 用将图片通过其他方法下载下来,转成base64再赋值给img标签的src。

方案1会增加包的体积,一般情况下优先选择方案2,这里也是对方案2封装了一个方法。

代码语言:javascript复制
/**
  * 解决图片跨域问题,将网络图片URL转为base64 URL。
  * @exports url2Base64
  * @param {string} src 网络图片URL
  * @returns {Promise} Promise对象返回base64 URL
  *
  * @example
  * Dom2Image.url2Base64("http://test.com/image.png").then(url=>{});
  */
const url2Base64 = src => new Promise((resolve) => {
  const img = new Image();
  img.setAttribute('crossOrigin', 'anonymous');
  img.src = src;
  img.onload = () => {
    const canvas = convertImageToCanvas(img);
    const url = canvas.toDataURL('image/png');
    resolve(url);
  };
  img.onerror = () => {
    resolve('');
  };
});

通过上面的封装,html2canvas生成海报的方案,我们使用的时候,主要的工作就是去调整样式了,html2canvas不支持的样式,都不能使用。

支持的样式列表在这里:https://html2canvas.hertzen.com/features

如果生成图片出现了样式错乱,优先考虑是否用了不支持的css样式。

其他可能出现的问题,大概率可能是html2canvas的问题,在他仓库的issues能找到大多数的答案。

puppeteer方案

参考文档

express.js

puppeteer

服务端运行无头浏览器,并截图。

这里用express.js来实现服务端代码,实现起来也是比较简单的。

代码语言:javascript复制
const express = require('express');
const router = express.Router();
const puppeteer = require('puppeteer');

router.post('/', async (req, res) => {
  try {
    const { url, width = 375, height = 667 } = req.body;
    console.log('url', url);
    const browser = await puppeteer.launch({
      headless: true,
    });
    const page = await browser.newPage();
    await page.setViewport({ width, height });
    await page.goto(url);
    await page.waitForTimeout(1000);
    const imgBuffer = await page.screenshot({
      type: 'png',
    });
    const base64Str = imgBuffer.toString('base64');
    res.json({
      code: 200,
      data: `data:image/png;base64,${base64Str}`,
      msg: '海报生成成功',
    });
    browser.close();
  } catch (error) {
    res.status(500).json({
      message: error.message,
      errors: error.stack,
    });
  }
});

使用方式

用户端只需要传生成图片的H5链接作为参数。

代码语言:javascript复制
axios({
    method: 'POST',
    url: 'http://localhost:3000/screenshot', // 托管海报的页面
    data: {
        url: `https://xxx.com/poster`,
    }
})

总结

前端生成的方案,作者这边已经在多次活动中使用过了,优点是不需要服务端,一开始的时候确实遍地是坑,但是慢慢摊平后,其实还是一个比较方便的方案。

兼容性其实也还可以,至少遇到的问题,最后都能通过各种调试解决,当然这也是很费时间的,而且不知道在没遇到的机型系统上,是否还存在不为人知的兼容性问题。

服务端生成的方案,作者最近才接触到,尚未用到正式的业务上,其优点是不需要考虑兼容性问题,如果正式使用,还需要考虑服务端性能等等问题。

0 人点赞