前端Node + Puppeteer生成商品详情

189 阅读2分钟

前言

由于商品详情在不同设备的渲染效果都不一致,所以使用图片的方式保存渲染结果,为了渲染成一致的效果然后截图,所以只能在服务端上进行渲染,经调研使用express + Puppeteer来搭一个node服务

Puppeteer介绍

Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。Puppeteer 默认以 headless 模式运行即“无头”模式,但是可以通过修改配置 headless: false 或者devtools: true运行“有头”模式。 在浏览器中手动执行的绝大多数操作都可以使用 Puppeteer 来完成!

框架搭建

首先使用express搭个基础框架

import express from 'express'
import renderFn from './src'
import type { IReqBody } from './src/types'const app = express()
​
//中间件-处理请求体
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
​
/** 判断服务启动成功 */
app.get('/', async (req, res) => {
  res.status(200).send('访问成功')
})
​
/** 接口路由监听 */
app.post('/render', async (req, res) => {
  try {
    const arr = await renderFn(req.body as IReqBody)
    res.status(200).send(arr)
  } catch (error) {
    console.log(error)
    res.status(500).send(error)
  }
})
​
/** 监听3002端口 */
const server = app.listen(3002, () => {
  const address = server.address()
  if (address && typeof address !== 'string') {
    console.log(`服务启动成功,监听${address.port}端口中`)
  }
})

截图 和 上传

流程: 打开一个浏览器 => 打开一个标签页并截图 => 上传OSS => 删除本地图片 => 关闭浏览器

创建puppeteer实例

部署在linux上一定要加'--no-sandbox', '--disable-setuid-sandbox'这个参数

const browser = await puppeteer.launch({
    devtools: false,
    defaultViewport: {
      width: 1500, /** 750 * 2 */
      height: 1000
    },
    /** 浏览器启动参数(参考: https://www.cnblogs.com/gurenyumao/p/14721035.html) */
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  })

打开一个标签页并截图

  /** 打开一个新页面 */
  const page = await browser.newPage()
  
  /** 设置css */
  page.addStyleTag({ path: __dirname + '/css/index.css' })
​
  /** 把请求体的html渲染出来 */
  await page.setContent(data)
​
  const wrapperArr = await page.$$('.preview-wrapper')
​
  /** 判断有没有image这个目录,没有就新建一个 */
  const imagePath = path.join(__dirname, `../image`)
  if (!fs.existsSync(imagePath)) fs.mkdirSync(imagePath)
​
  const filePath = `${imagePath}/${goodsId}`
​
  /** 创建队列截图 */
  await Promise.all(
    wrapperArr.map((item, index) => {
      return new Promise(async (resolve, reject) => {
        /** 获取元素定位信息 */
        const clipMsg = await item.boundingBox() as ScreenshotClip
        Object.keys(clipMsg).forEach((key) => {
          /** css用zoom放大,元素定位信息乘2倍 */
          clipMsg[key as keyof ScreenshotClip] = clipMsg[key as keyof ScreenshotClip] * 2
        })
        /** 每一个商品一个文件夹 */
        if (!fs.existsSync(filePath)) fs.mkdirSync(filePath)
​
        /** 截图 */
        await page.screenshot({
          path: `${filePath}/floor${index}.png`,
          clip: clipMsg as ScreenshotClip,
          type: 'webp',
          quality: 100
        })
        resolve('shot success')
      })
    })
  )

上传OSS

使用环境变量获取不同的oss配置,生成实例后队列上传

/** 上传队列
 * @param client OSS实例
 * @param files 图片名数组
 * @param goodsId 商品Id
 * @param imagePath 图片路径
 */
const queue = (client: OSS, files: string[], goodsId: string, imagePath: string) =>
  files.map((imageName) => {
    return new Promise<string>(async (resolve, reject) => {
      try {
        const res: any = await client.put(
          `goodsDetails/${goodsId}/${imageName}`,
          `${imagePath}/${imageName}`
        )
        resolve(res.res.requestUrls[0])
      } catch (error) {
        reject({
          error,
          errMsg: 'oss上传错误',
        })
      }
    })
  })

删除文件夹

上传成功后,把文件夹删除,但fs模块只能删除空文件夹,所以需要遍历文件夹,删除全部文件后再删除文件夹

/** 删除文件夹
 * @param { string } dir 文件路径
 */
export function removeFile(dir: string) {
  return new Promise(function (resolve, reject) {
    fs.stat(dir, function (err, stat) {
      if (stat.isDirectory()) {
        fs.readdir(dir, function (err, files) {
          files = files.map((file) => path.join(dir, file))
          const  filesArr = files.map((file) => removeFile(file))
          Promise.all(filesArr).then(function () {
            fs.rmdir(dir, resolve)
          })
        })
      } else {
        fs.unlink(dir, resolve)
      }
    })
  })
}

问题记录和处理

服务器部署出错

问题在于puppeteer在linux上运行需要配置环境,参考puppeteer部署文档

所以直接docker进行部署,参考以下dockerfiles

# 这里是别人已经搭建好的的一个pupptr运行环境                                                           
FROM buildkite/puppeteer                                                                         
                                                                                    
                                                                                                 
# 把当当前目录的模样   所有内容都拷贝到app工作目录                                                                   
COPY ./ ./app                                                                                    
                                                                                                 
                                                                                                 
RUN npm install -g pnpm                                                                          
RUN pnpm install  
RUN pnpm start-dev
​
# 向外暴露3002端口
EXPOSE 3002

首选创建镜像

docker build -t node/pptr:V1 ./

创建容器

docker run -it --privileged -p 8888:3002 容器名或容器ID

!!别在M1中使用docker运行该项目,要用docker x86的兼容模式运行,并且报错(官方issue

字体缺失问题

由于linux系统上没有默认的中文字体,所以导致渲染乱码,导入字体包到服务器/usr/local/share/fonts路径下就可以了

自定义字体同理导入即可

优化

因为每次接口被调用都启动了一个浏览器,截图之后关闭了这个浏览器,造成了资源的浪费,并且启动浏览器也需要耗费时间。并且同时启动的浏览器过多,程序还会抛出异常。

参考