如何动态生成一张完美的海报

3,997 阅读6分钟

背景

在移动端业务中,经常有裂变的需求,这时就需要动态的生成海报。一般海报需要包含分享人的授权信息还有落地页的二维码。
H5端生成海报网上一般传的比较多的是canvas动态画,还有html2canvas库生成,不过这两种基本都是基于canvas,canvas生成的图片对于色彩比较深的图片基本都模糊的不行。所以一般使用这个种方法来生成图片的时候,一般我都会和设计提前沟通,做一些比较浅的图片。
做过几次这样的需求之后,心里一直念念不忘想要找一个更好的办法。忽然有天在用手机截图的时候想到,能不能通过截图来生成海报呢,于是思路一发不可收拾,想到之前使用chrome截取页面长图的命令,就去搜索谷歌有没有提供相关的sdk。
这一搜索就遇到了今天的主角puppeteer,chrome开源的无头浏览器,加上之前写爬虫用过类似的无头浏览器,瞬间思路打开,开始搞起。

第一次尝试

很快看完puppeteer中文文档,思考了一下实现逻辑:

    1. 写一个海报页面,可供浏览器截图
    2. 写一个node服务,提供一个截图的接口
    其中接口逻辑:
    1. 创建浏览器实例
    2. 创建一个page实例
    3. 打开指定的海报页面
    4. 等待加载完成的标记
    5. 截图
    6. 关闭浏览器实例
    

主要逻辑代码很简单,官方给的第一个例子就是,改写一下:


const puppeteer = require('puppeteer')
const router = express.Router();
const devices = require('puppeteer/DeviceDescriptors')
const iPhone = devices['iPhone 8']
const GetPoster = async (url, pageOptions, posterOptions) => {
 // 创建浏览器实例
 // args是配置一些浏览器性能的参数 官方文档都有解释
  const browser = await puppeteer.launch({
    args: ['--no-sandbox', '--disable-setuid-sandbox', '-–disable-gpu',
      '-–disable-dev-shm-usage',
      '-–no-first-run',
      '-–no-zygote',
      '-–single-process'
    ]
  });
  let screenshot = ''
  try {
   // 创建page实例
    const page = await browser.newPage();
    let path = 'example.jpg'
   // 设置机型
    await page.emulate(iPhone)
   // 宽高
    await page.setViewport({
      isMobile: true,
      width: 414,
      height: 736,
      ...pageOptions
    })
   // 打开海报页面
    await page.goto(url, {
      waitUntil: ['load', 'domcontentloaded']
    });
   // 等待海报渲染完成的标记
    await page.waitForSelector('.load-complete')
   // 截图
    screenshot = await page.screenshot({
      path: path,
      ...posterOptions
    });
   // 关闭浏览器实例
    await browser.close();
  } catch (err) {
    console.log(err)
    await browser.close()
  }
  return screenshot
}

写完代码赶紧try一下,本地发现完美截图很OK,高清无瑕疵,于是赶紧兴冲冲的和运维大佬请求部署。
理想照进现实,影子往往惨不忍睹。

运维大佬拿到代码后,开了一台cenos6.x的服务器,结果安装依赖就出了问题。于是继续搜索解决方法,还真的找到了

然后继续尝试,这次图是截出来了,结果图里的文字一片乱码,搜索一下,是由于服务器没有安装中文字体,安装之

终于可以截图了,但是截的图和本地的不一样,有一层白色的蒙层。不过当时由于业务比较赶,就没有继续深究,就上线了。

然后悲剧就来了,倒不是白色蒙层的问题,而是上线没两天服务挂了,好在当时运维大佬帮忙做了守护脚本,服务还能重启。不过运维说内存老是暴涨,于是又review了一下代码,想象了一下浏览器机制。

发现是由于请求来的太多,每次都建一个浏览器实例,每个浏览器实例大概250M,并发一上来服务器就爆掉了,当时就想能不能就开一个或者几个浏览器实例,每次打开海报页面只是打开浏览器tab页来截图。

念念不忘,必有回响,当时当天就刷到了一篇”方凳雅集“的文章使用 generic-pool 优化 puppeteer 并发问题。 于是赶紧优化:


const pool = require('../bin/pool')

const browserPool = pool.InitPool({ // 全局只应该被初始化一次
  puppeteerArgs: {
    ignoreHTTPSErrors: true,
    headless: true, // 是否启用无头模式页面
    timeout: 0,
    pipe: true, // 不使用 websocket
    args: ['--no-sandbox',
      '--disable-setuid-sandbox',
      '-–disable-gpu',
      '-–disable-dev-shm-usage',
      '-–no-first-run',
      '-–no-zygote',
      '-–single-process'
    ]
  }
})

const GetPoster = async (url) => {
  let screenshot = ''
  try {
    const page = await browserPool.use(async (instance) => {
      const page = await instance.newPage()
      await page.emulate(iPhone)
      await page.goto(url, {
        waitUntil: ['load', 'domcontentloaded']
      });
      await page.waitForSelector('.load-complete')
      return page
    })
    screenshot = await page.screenshot({
      fullPage: true,
      quality: 100,
      type: 'jpeg'
    });
    page.close()
  } catch (err) {
    console.log(err)
  }
  return screenshot
}

问题解决

第二次尝试

隐患如果不解决总有爆发的一天,海报模糊的问题经过一年的发酵终于还是回到了我的手里,这次要做裂变2.0了,正好趁着这次机会彻底解决问题。
开始我怀疑是服务器问题,于是就用向运维又申请了一台测试服务器,部署代码,搭好环境,请求,结果发现截的图很清晰啊,没有灰白色的蒙层。

A92911BE-9757-482b-AE56-F432C31F4AEB.png

于是和运维大哥说要不再换一台服务器部署看看,运维大哥同意了,很快部署完成,请求,还是有白色的蒙层。

EE25CA14-EEA6-4dd8-A9A5-7F01DFCDEA51.png

脑袋超大~这时忽然发现测试服务器截图请求一次1s,而服务器上截图只要200ms。于是好像找到了原因,是不是因为浏览器打开太快,还没来及展示完全就截图了,所有有白色蒙层。于是就写了一个sleep:


const sleep = (microSec) => new Promise((res, rej) => setTimeout(res, microSec));
await sleep(100)

在截图前停顿了100ms。然后部署,发现问题解决。

剩余问题

剩下的问题就是图片存储问题了,因为海报可以提前预生成,所以海报生成实际上对请求性能要求并不高,但也尽量控制在1s以内,保不准会遇到各种各样的场景。

我们的图片是传到七牛云存储的,这个图片上传实际上也会花费很大一部分时间,所以可以进行以下优化:

  1. 如果图片资源存储自己的服务器,那么请做一下cdn转发
  2. 如果你使用云服务器,请对服务的带宽做一下提升
  3. 代码层面,可以做缓存,因为海报的请求地址往往对应一个用户就一张,在不考虑用户信息频繁变更的情况下,可以对生成的海报路径做redis缓存,用户第一次生成海报之后,下次请求就直接返回上次的截图。当然这个也可以交给请求方来做,不用放在海报服务器
  4. 这个海报可以跨平台 各个端都可以用 app h5 小程序都可以 非常方便

相关部分代码