前言
由于商品详情在不同设备的渲染效果都不一致,所以使用图片的方式保存渲染结果,为了渲染成一致的效果然后截图,所以只能在服务端上进行渲染,经调研使用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路径下就可以了
自定义字体同理导入即可
优化
因为每次接口被调用都启动了一个浏览器,截图之后关闭了这个浏览器,造成了资源的浪费,并且启动浏览器也需要耗费时间。并且同时启动的浏览器过多,程序还会抛出异常。