基于 Puppeteer 生成分享海报功能的安装和使用

思考怎么去生成图片

用 php 处理图片的水印去实现,比较复杂,需要定位。

用 canvas 去处理 html dom 节点保存输出图片,需要处理定位,小程序同理。

使用 js 插件把 html 转成 canves 在转成 图片输出

使用 puppeteer 无头Chrome节点API 工具去实现。

其实就是用谷歌浏览器去打开网页然后截图保存到本地

经过前后端同学的紧张讨论下,最后勇敢的使用了第4个方案:

在程序员界没学习一项新的知识都需要打印一段 Hello World,那么我们就开始实现第一步

本地部署

首先熟练的打开了 iterm 工具执行命令

安装 puppeteer

mkdir ~/code/puppeteer && cd ~/code/puppeteer

yarn add puppeteer

image-20180913185026947

这通常是由于 Chromium 浏览器未正常下载到而引起的。这个时候,我们可以先设定环境变量,跳过安装 Chromium 的步骤,然后手动翻墙去下载:

set PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1

然后还需要手动的指定执行的目录,这里我们不多的去介绍了。

直接动用淘宝镜像,果然可以,感谢马爸爸。

cnpm i puppeteer

image-20180913185437942

测试 puppeteer

然后就是用官方提供的方式来测试。

创建 example.js 文件。

touch example.js && vim example.js
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://www.baidu.com');
  await page.screenshot({path: 'example.png'});

  await browser.close();
})();
node example.js

image-20180913185819314

图片成功的显示出来了 😄

image-20180913185856473

php 中使用 puppeteer

然后就是放到PHP代码里面。这里我就直接截取了部分代码讲一下思路。

// 图片保存地址
$imgFilePath = storage_path("app/public/activity-record/{$activityRecord}_{$template}.png");

// js保存地址
$jsFilePath = "/public/activity-record/{$activityRecord}_{$template}.js";

// 访问地址
$url = 'http://' . config('api.domains.mp') . "/activity/record/{$activityRecord}/share?template={$template}";

// 创建文件
$jsFileContents = <<<EOF
const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});
const page = await browser.newPage();
await page.setViewport({ width: 375, height: 667, deviceScaleFactor: 2, isMobile: true });
await page.goto('$url');
await page.screenshot({ path: '$imgFilePath' });

await browser.close();
})();
EOF;

Storage::put($jsFilePath, $jsFileContents);

// 主要也就是这一步比较的坑,填了很久很久
// 正式的环境需要指定node目录
// exec('/usr/local/nodejs/bin/node ' . storage_path("app/$jsFilePath"));
// 本地已经实现了
exec('node ' . storage_path("app/$jsFilePath"));

// 这里是上传到腾讯cos
$disk = \Storage::disk('cosv5');

$cosPath = "jielong/activity/record/{$activityRecord}/t/{$template}.png";

$disk->put($cosPath, fopen("$imgFilePath", 'rb'));

好了本地调试通过了,那就开始走测试环境,这里开始就不停的出现问题,依赖问题等等都出来了。

测试部署

首先是测试换进 Centos 6.7 版本

依赖错误

libXcomposite.so.1

error while loading shared libraries: libXcomposite.so.1: cannot open shared object file: No such file or directory

/home/puppeteer/node_modules/puppeteer/.local-chromium/linux-588429/chrome-linux/chrome: error while loading shared libraries: libXcomposite.so.1: cannot open shared object file: No such file or directory

image-20180914124705842

issuess

// 安装依赖
yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 -y

// 安装字体
yum install ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc -y

libatk-bridge-2.0.so.0

error while loading shared libraries: libatk-bridge-2.0.so.0: cannot open shared object file: No such file or directory

/home/puppeteer/node_modules/puppeteer/.local-chromium/linux-588429/chrome-linux/chrome: error while loading shared libraries: libatk-bridge-2.0.so.0: cannot open shared object file: No such file or directory

image-20180914125130578

ldd node_modules/puppeteer/.local-chromium/linux-588429/chrome-linux/chrome

image-20180914130912067

// 网上发现了一个方式,安装一个 firefox 然后把依赖复制过去
yum install firefox-60.1.0-6.el6.centos.x86_64

cp /usr/lib64/firefox/bundled/lib64/libatk-bridge-2.0.so.0 /usr/lib64
cp /usr/lib64/firefox/bundled/lib64/libatspi.so.0 /usr/lib64
cp /usr/lib64/firefox/bundled/lib64/libgdk-3.so.0 /usr/lib64
cp /usr/lib64/firefox/bundled/lib64/libgtk-3.so.0 /usr/lib64

image-20180914133816963

libGL.so.1

/home/puppeteer/node_modules/puppeteer/.local-chromium/linux-588429/chrome-linux/chrome: error while loading shared libraries: libGL.so.1: cannot open shared object file: No such file or directory
yum install mesa-libGL.x86_64

image-20180914133937739

运行下面的命令行可以看到需要 GLIBC_2.14 GLIBC_2.16

objdump -p node_modules/puppeteer/.local-chromium/linux-588429/chrome-linux/chrome

image-20180914132319923

发现只支持到 GLIBC_2.12

strings /lib64/libc.so.6 |grep GLIBC_ 

image-20180914132530077

所以需要安装 GLIBC_2.14

libc.so.6: version 'GLIBC_2.14' not found报错提示的解决方案

但是GLIBC_2.16在机器上怎么装都装不上,所以后来放弃了。

解决方案是安装低版本的puppeteer 大概是 1.4 以下的版本

cnpm i puppeteer@1.4

好像没有找到不需要 GLIBC_2.14 的包

最后是安装了 GLIBC_2.14 然后执行命令之后发现了一个问题。

版本错误

libcairo-gobject.so.2: undefined symbol: cairo_region_destroy

libcairo-gobject.so.2: undefined symbol: cairo_region_destroy

后面反正是没有找到解决方案。

Docker 部署

centos 安装 docker

yum install docker-io
service docker start
vim Dockerfile

docker build 容器

FROM node:8-slim

# See https://crbug.com/795759
RUN apt-get update && apt-get install -yq libgconf-2-4

# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer
# installs, work.
RUN apt-get update && apt-get install -y wget --no-install-recommends \
    && wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update \
    && apt-get install -y google-chrome-unstable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \
      --no-install-recommends \
    && rm -rf /var/lib/apt/lists/* \
    && apt-get purge --auto-remove -y curl \
    && rm -rf /src/*.deb

# It's a good idea to use dumb-init to help prevent zombie chrome processes.
ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init
RUN chmod +x /usr/local/bin/dumb-init

# Uncomment to skip the chromium download when installing puppeteer. If you do,
# you'll need to launch puppeteer with:
#     browser.launch({executablePath: 'google-chrome-unstable'})
# ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true

# Install puppeteer so it's available in the container.
RUN npm i puppeteer

# Add user so we don't need --no-sandbox.
RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
    && mkdir -p /home/pptruser/Downloads \
    && chown -R pptruser:pptruser /home/pptruser \
    && chown -R pptruser:pptruser /node_modules

# Run everything after as non-privileged user.
USER pptruser

ENTRYPOINT ["dumb-init", "--"]
CMD ["google-chrome-unstable"]
docker build -t puppeteer-chrome-linux .

创建 puppeteer.sh

然后创建了一个文件 puppeteer.sh

#!/bin/bash
readonly USAGE="Usage: puppeteer.sh yourfile.js"

basepath=$(pwd)

local_path="$basepath/storage/app/public/activity-record/"

if [ -z "$1" ]
then
  echo "File not specified."
  echo $USAGE
  exit 0
fi

echo "Running $1 in Puppeteer..."

file=`cat $1`

# set -x # debug on
docker run -it --net=host -v $local_path:/home/pptruser/Downloads --privileged=true --rm --cap-add=SYS_ADMIN \
  --name puppeteer-chrome puppeteer-chrome-linux \
  node -e "$file"

puppeteer.sh 生成图片

使用 puppeteer.sh 来调用 example.js

./puppeteer.sh example.js

php 去调用 puppeteer.sh

exec("./puppeteer.sh example.js")

思考: php 如何使用 root 权限

会发现一个问题,在命令行的时候是可以执行的,但是在网页上面是不行,最后发现是权限问题。命令行是 root 权限但是在网站的时候是 www 权限,因为权限是通过 nginx 的用户来定的。然后又想到一个方法是用队列执行,队列是可以使用 root 权限去执行的,没想到是可以的。但是队列获取不到队列执行的结果,所以每次执行一次就不能用了,需要手动的重启队列,最后还是放弃了。

正式部署

Centos 7.4 一切没有问题。

主要是一个问题,生成图片的时候文字有方格乱码,解决的方案是使用本地的字体库。

目前使用队列来执行代码,但是无法实时的更新,所以尝试 sleep 方法,但是还是不能很好的解决这个问题。

思考后 node express 开发一个 api 来生成图片。

安装 express puppeteer 等工具

mkdir /home/www/puppeteer
cd /home/www/puppeteer
// chrome 无头
cnpm i puppeteer
// 腾讯cos
cnpm i cos-nodejs-sdk-v5 --save
// express
cnpm i express
// 环境变量
cnpm i dotenv
// pm2进程管理
cnpm install pm2 -g

express 搭建 api 服务器

index.js

const express = require('express')
const app = express()// 引入json解析中间件
const puppeteer = require('puppeteer');
const bodyParser = require('body-parser');
const COS = require('cos-nodejs-sdk-v5');
const basePath = process.cwd(); 
const dotenv = require('dotenv').config({ path: `${basePath}/../jielong_mp/.env` });

var cos = new COS({
    AppId: process.env.COSV5_APP_ID,
    SecretId: process.env.COSV5_SECRET_ID,
    SecretKey: process.env.COSV5_SECRET_KEY,
});

// COSV5_APP_ID = 1251581441
// COSV5_SECRET_ID = AKIDbFnBPV9N6g8PYY91TkJp6D92pT0NIpoc
// COSV5_SECRET_KEY = mOp9MRMF2eN0Tq26zhEaZO3XsJk3CA1i
// COSV5_TIMEOUT = 60
// COSV5_CONNECT_TIMEOUT = 60
// COSV5_BUCKET = kemanyun
// COSV5_REGION = ap - shanghai
// COSV5_CDN = #https://{your-bucket-name}-{your-app-id}.file.myqcloud.com
// COSV5_SCHEME = https

// 添加json解析
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));

app.post('/api/puppeteer', (req, res) => {
    const activity_record_id = req.body.activity_record_id;
    const template = req.body.template;
    const path = `${basePath}/../jielong_mp/storage/app/public/activity-record/${activity_record_id}_${template}.png`;
    const mp_domain = process.env.MP_DOMAIN;
    const url = `http:/${mp_domain}/activity/record/${activity_record_id}/share?template=${template}`;

    (async () => {
        const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
        if (process.env.APP_ENV === 'local') {
            const browser = await puppeteer.launch();
        }
        const page = await browser.newPage();
        await page.setViewport({ width: 375, height: 667, deviceScaleFactor: 2, isMobile: true });
        await page.goto(url);
        await page.screenshot({ path: path });

        await browser.close();
        // 分片上传
        await cos.sliceUploadFile({
            Bucket: process.env.COSV5_BUCKET,
            Region: process.env.COSV5_REGION,
            Key: process.env.COSV5_BUCKET_PREFIX + `/activity/record/${activity_record_id}/t/${template}.png`,
            FilePath: path
        }, function (err, data) {
        });

        await res.status(201).json({
            message: '创建图片成功',
            status_code: '201',
        });
    })();

})

app.listen(3000, () => console.log('Example app listening on port 3000!'))

pm2 来管理 express

pm2 start index.js

image-20180914140355472

php 调用接口来实现


use GuzzleHttp\Client;

...

$guzzleCLient = new Client();

$res = $guzzleCLient->post('http://127.0.0.1:3000/api/puppeteer', [
    'form_params' => [
        'activity_record_id' => $activityRecord,
        'template' => $template
    ]
]);

扩展分享

最后我才发现了已经有大神有解决方案了,干得漂亮。

nesk/rialto 工作原理是创建一个Node进程并通过 socket 与它通信。

Puphpeteer:Chrome 无头浏览器 Puphpeteer 项目的 PHP 桥梁

目前线上使用接口,没有使用这个包