前端工程化 - 借助 puppeteer 批量生成分享图

前言

由于个人的一些情况,前端工程化的专栏停了有段时间,接下来会陆续更新一些新的工程化的文章,希望给各位同学带来一些实在的干货。

最近也在担任本月的 Nodejs 专题 的嘉宾,借此机会写一篇关于 Node 方面的博文,希望大家在工程化以及 Node 方面能有更高的提升。

需求分析

ToC 的场景中,营销是一件很重要的手段,要让更多的人看到我们的产品,需要覆盖到更大的范围,获取更多的流量,触达和影响更多的用户,从而提升品牌知名度和影响力。

在营销环节有一个关键模块叫分享海报,在营销活动中,无论营销模式有多高明、多接地气、流行甚至创新,单纯靠文字来表达远不如图片来的震感,这种情况在小程序端尤为常见,借助微信的识别二维码功能,可以减少用户的使用成本。

那么如何快速的批量生成分享图就一件比较棘手的事情。

技术选型

市面常用的方案基本有下面 3 种:

  1. 前端直接根据素材使用 canvas 绘图并生成分享图
  2. 前端使用 html 使用 html2canvas 生成分享图
  3. 后端根据素材绘制图层,生成分享图后再返回给前端

其他的一些方案基本也是围绕上述 3 种进行组合、拓展。如果有更好的方案可以再评论区一起探讨下。

canvas 绘图

上手难度最大,对开发要求较高,canvas 和纯 html 布局相去甚远,在绘制图层的时候,样式还原度会比较差,但能够兼容小程序与 web 端,同时在需要转换成 node 服务的情况下,也有 node-canvas 插件支持,目前来看是最通用的解决方案。

html2canvas

从使用角度以及开发难度上来看,是最为便捷且样式还原度最高的一种方式,且相对于其他方案而言,成本是最少的,最大的缺点是在小程序端做分享图的时候,web 与小程序之间的交互会显得比较麻烦。除此之外,它是首推选择。

服务端绘制

服务端可以完成较为简单的需求,生成一些简单的图片,再让前端根据规则获取即可,但是对于服务端的同学来说,可选库能够提供的功能也不会很多,样式还原度也一样会存在一些问题,同时需要考虑并发的问题。

要解决后端并发问题的话,也可以使用批量预生产图片,但这样带来的问题就是生成的图片没办法带上临时的信息,例如分享二维码里面需要携带一些额外的参数,用户信息等内容。

对于前两种选择都有一样缺点,所有的资源依赖都是从服务端获取,在同步生成分享图的时候需要等待资源加载完成,再加上自己绘制的时间,会有一定的延迟。优势在于客户端渲染,渲染成本都嫁接在用户身上,而选择服务端渲染的方案,服务器的成本会增加。

当然如果条件允许的情况下,做预渲染,提前把可推测的资源预先加载,生成分享海报也是一种很好的手段,并且可以降低服务器的一些成本。

最后在多机型、微信版本中可能存在未知兼容、缓存等情况,UI 设计的再完美,客户端渲染也可能出现不可预期的情况。

解决方案

在我们的业务场景中,有大量的商详需要做分享图,而且有些分享图需要携带用户信息,这样就导致如果全靠后端来渲染图片是不太合适的。但是我们也存在长图的情况,全部放在前端渲染也会有一定的性能瓶颈。

目前尝试的方案是后端根据商品属性预渲染完整的图后挂载在 cdn 上,前端根据需求,当需要携带用户信息可以根据生成好的图片当做底图使用 canvas 将二维码绘制上去,如果没有额外的信息的话,就可以直接使用后端渲染的图,同时配合前端预加载内容使得分享海报绘制的效率达到最高。

那么在选择后端渲染的方案上,除了 node-canvas、其他的绘图类库之外,为了保证最好的还原度以及开发成本,最终选择了渲染模板 + 无头浏览器截屏的方式来获取分享图。

上述的方案并不一定适合你目前的场景,具体的解决方案还是需要根据自身的业务情况来选择,或者混合在一起使用。

项目实战

搭建基础环境

  1. 初始化 koa 项目

现在的 TS 项目比较流行,所以接下来使用 TS 来搭建一下 koa 环境方便测试。

使用下述命令初始化项目并安装对应依赖

// 安装依赖
npm init // 初始化 package.json
npm i koa koa-router
npm i --save-dev typescript ts-node nodemon cross-env
npm i --save-dev @types/koa @types/koa-router
复制代码

新建 tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es2017",
    "noImplicitAny": true,
    "moduleResolution": "node",
    "sourceMap": true,
    "outDir": "dist", // TS 文件编译产物会放在此处
    "baseUrl": ".",
    "paths": {
      "*": [
        "node_modules/*",
        "src/types/*"
      ]
    }
  },
  "include": [
    "src/**/*"
  ]
}
复制代码

package.json 添加对应的 scripts 脚本命令

  "scripts": {
    "start": "tsc && node dist/server.js",
    "start:dev": "cross-env nodemon --watch 'src/**/*' -e ts,tsx --exec 'ts-node' ./src/server.ts",
    "tsc": "tsc",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
复制代码

安装 cross-env 是为了跨平台,主要是兼容 windows,如果是 mac 用户可以不安装这个模块

新建 src/server.ts

import * as Koa from 'koa';
import * as Router from 'koa-router';

import { renderHtml } from './renderHtml'
import { screenShoot } from './screenShoot'
import { resolve } from 'path'

const app = new Koa();
const router = new Router();

router.get('/', async (ctx) => {
  ctx.body = 'Hello World!';
});

app.use(router.routes());

app.listen(3000);
console.log('Server running on port 3000');
复制代码

接下来启动命令即可:npm run start:dev

image.png

浏览器打开窗口输入 http://localhost:3000/,得到如下反馈则表示 koa 项目创建成功

image.png

截屏功能

在截屏功能的选择上,我们选择了 puppeteer 作为无头浏览器,模板插件选择了更贴近 vue 语法的 nunjucks

安装对应的依赖

npm i koa koa-router
npm i --save-dev nunjucks puppeteer
npm i --save-dev @types/nunjucks 
复制代码

创建 src/screenShoot.ts

const puppeteer = require('puppeteer');

interface ILink {
  contain: string,
  name: string,
}

export const screenShoot = async (links: ILink[]) => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.emulate(puppeteer.devices['iPhone X']);

  for (let link of links) {
    await page.setContent(link.contain);
      await page.screenshot({ path: `./${link.name}.png`, fullPage: true });
  }
  await browser.close();
}
复制代码

创建 renderHtml.ts

const nunjucks = require('nunjucks')

export const renderHtml = (tpl: string, params: object) => {
  const res = nunjucks.render(tpl, params);
  return res
}
复制代码

创建 tpl/test.njk

<html>
  <head>
    <style type="text/css">
      .share {
        text-align: center;
      }
    </style>
  </head>
  <body>
    <div class='shareImage' id="shareImage">
      <p style='font-size:200px;'>{{username}}</p>
      <p style='font-size:50px;color:red;'>{{email}}</p>
    </div>
    <div class="share" id="shareContain">
      <p>我是一段很长的描述</p>
      <img class="shareImge" src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic.616pic.com%2Fys_bnew_img%2F00%2F24%2F13%2FTvzVABfVpn.jpg&refer=http%3A%2F%2Fpic.616pic.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1649742353&t=ab74e701f176154b6d03195f138bc6dd" class="lazyload-img hotzone-pic more fadein fat" data-resize="0">
    </div>
  </body>
</html>
复制代码

再添新的路由请求即可:

router.get('/screenShoot', async (ctx) => {
  const html = renderHtml(resolve(__dirname, './tpl/test.njk'), { username: 'cookie', email: '[email protected]' })
  screenShoot([{
    contain: html,
    name: 'cookie',
  }])
  ctx.body = 'true!';
});
复制代码

模板在直接渲染在浏览器的样式:

image.png

通过上述代码使用 puppeteer 截图出来的样式:

image.png

通过对比不难看出,使用 puppeteer 截图出来的样式基本上能够保证较高的还原度。

但是截图中还是有空白区域,以及我们要截图可能只有详情的区域,所以我们可以稍微改造一下截屏代码,添加选择器来限制截屏区域。

  for (let link of links) {
    await page.setContent(link.contain);
    if (link.el) {
      const element = await page.$(link.el);
      await element.screenshot({ path: `./${link.name}.png`, fullPage: false });
    } else {
      await page.screenshot({ path: `./${link.name}.png`, fullPage: true });
    }
  }
复制代码

传入参数添加选择器:

  screenShoot([{
    contain: html,
    name: 'cookie',
    el: '#shareContain'
  }])
复制代码

最后截屏出来的内容如下所示,非常干净,还原度基本达到 1 比 1 的程度。

image.png

由于使用的是高清截屏,图片的 size 会比较大,大家在使用的时候,可以对其进行一定比例的压缩,根据自己对图片质量的要求将图片压缩至可接受的范围即可。

写在最后

本文到此结束,借助于 Nodejs 完成一个常见的营销分享图的方案,而这只是 Nodejs 的一块很小的应用,另外 Nodejs 也不仅仅是用作于服务端,上述的方案即使不使用 koa 来作为服务,我们依然可以把它组装成工具库提供给其他端或者工具来使用。

如果想对自己的技术精进有更高的追求,不妨借助 Nodejs 来攻克一下目前手上的业务难点、繁点。

招贤纳士

体验技术研发中心主要负责用体验技术为行云集团所有端侧产品的提供技术支撑,为用户提供极致体验,让数字化触手可及。技术架构上包含搭建、工程化、微前端、UI 规范、研发效能、CI/CD 、AI 智能等技术体系。体验技术研发中心由前端团队、客户端团队、行云生态业务团队组成,团队规模 60+,成员来自于阿里、腾讯、有赞、涂鸦等互联网公司,包含众多技术专家,实力强劲。

以上所有的产品都由体验技术团队同学全栈参与完成,无论是前端&客户端,还是运维、数据库,亦或者是 AI 技术。

如果你想对技术有进一步的追求、对工程化有诸多想法却没有机会与场景、想体验从 0-1 然后从 1-10 完成一件有价值的产品,那么在这里有很多好玩、有趣、有意义的技术产品等着你来

おすすめ

転載: juejin.im/post/7074469127393378340