首页 > 其他分享 > puppeteer的两个可能常用的场景实践

puppeteer的两个可能常用的场景实践

时间:2023-02-07 23:33:31浏览次数:51  
标签:场景 const await 实践 puppeteer html return pool

之前写文章介绍过 puppeteer 在“非侵入式骨架屏”中的实践,今天再来介绍两个场景。

puppeteer + SSR

使用 Puppeteer 实现服务端预渲染的好处是不需要对项目代码进行任何调整,却能获取到 SSR 应用的收益。当然,它的灵活性和扩展性都有所局限。甚至在 Node.js 端渲染的性能成本也较高,不过该技术也逐渐落地,并在很多场景发挥了重要价值。

比如:

<html>
<body>
<div id="container">
<!-- Populated by the JS below. -->
</div>
</body>
<script>
// 使用 JavaScript 脚本,进行 CSR 渲染
function renderPosts(posts, container) {
const html = posts.reduce((html, post) => {
return `${html}
<li class="post">
<h2>${post.title}</h2>
<div class="summary">${post.summary}</div>
<p>${post.content}</p>
</li>`;
}, '');
container.innerHTML = `<ul id="posts">${html}</ul>`;
}
(async() => {
const container = document.querySelector('#container');
// 发送数据请求
const posts = await fetch('/posts').then(resp => resp.json());
renderPosts(posts, container);
})();
</script>
</html>

该页面是一个典型的 CSR 页面,依靠 Ajax,实现了页面动态化渲染。

当在 Node.js 端使用 Puppeteer 渲染时,我们可以实现ssr.mjs,完成渲染任务,如下代码:

import puppeteer from 'puppeteer';
// 将已经渲染过的页面,缓存在内存中
const RENDER_CACHE = new Map();
async function ssr(url) {
// 命中缓存
if (RENDER_CACHE.has(url)) {
return {html: RENDER_CACHE.get(url), ttRenderMs: 0};
}
const start = Date.now();
// 使用 Puppeteer launch 一个无头浏览器
const browser = await puppeteer.launch();
const page = await browser.newPage();
try {
// 访问页面地址直到页面网络状态为 idle
await page.goto(url, {waitUntil: 'networkidle0'});
// 确保 #posts 节点已经存在
await page.waitForSelector('#posts');
} catch (err) {
console.error(err);
throw new Error('page.goto/waitForSelector timed out.');
}
// 获取 html 内容
const html = await page.content();
// 关闭无头浏览器
await browser.close();
const ttRenderMs = Date.now() - start;
console.info(`Headless rendered page in: ${ttRenderMs}ms`);
// 进行缓存存储
RENDER_CACHE.set(url, html);
return {html, ttRenderMs};
}
export {ssr as default};

对应server.mjs代码:

import express from 'express';
import ssr from './ssr.mjs';
const app = express();
app.get('/', async (req, res, next) => {
// 调用 SSR 方法渲染页面
const {html, ttRenderMs} = await ssr(`xxx/index.html`);
res.set('Server-Timing', `Prerender;dur=${ttRenderMs};desc="Headless render time (ms)"`);
return res.status(200).send(html);
});
app.listen(8080, () => console.log('Server started. Press Ctrl+C to quit'));

当然上述实现比较简陋,只是进行原理说明。如果更进一步,我们可以从以下几个角度进行优化:

  • 改造浏览器端代码,防止重复请求接口;
  • 在 Node.js 端,abort 掉不必要的请求,以得到更快的服务端渲染响应速度;
  • 将关键资源内连进 HTML;
  • 自动压缩静态资源;
  • 在 Node.js 端,渲染页面时,重复利用 Chrome 实例。

比如:

import express from 'express';
import puppeteer from 'puppeteer';
import ssr from './ssr.mjs';
// 重复使用 Chrome 实例
let browserWSEndpoint = null;
const app = express();

app.get('/', async (req, res, next) => {
if (!browserWSEndpoint) {
// 一下两行代码不必随着渲染重复执行
const browser = await puppeteer.launch();
browserWSEndpoint = await browser.wsEndpoint();
}

const url = `${req.protocol}://${req.get('host')}/index.html`;
const {html} = await ssr(url, browserWSEndpoint);

return res.status(200).send(html);
});

Puppeteer 实现海报 Node.js 服务

海报分享目前最常见的有两种:“截屏分享”和“划句分享”。
拿前者来说,一般生成海报可以使用​​html2canvas​​这样的类库完成,这里面的技术难点主要有跨域处理、分页处理、页面截图时机处理等。整体来说,并不难实现,但是稳定性一般。另一种生成海报的方式就是使用 Puppeteer,构建一个 Node.js 服务来做页面截图。

核心技术无外乎使用 Puppeteer,访问页面并截图,这与刚才的场景是一样的。

但是,为了实现更好的性能,我们可以用一个链接池来存储 Puppeteer 实例,以备所需。就像这样:

链接池优化puppeteer也是面试中可能会出现的考点!

在实现上,笔者使用了 generic-pool 库,这个库提供了 Promise 风格的通用池,可以用来对一些高消耗、高成本资源的调用实现防抖或拒绝服务能力,一个典型场景是对数据库的连接。这里我们把它用于 Puppeteer 实例的创建,如下代码所示:

const puppeteer = require('puppeteer')
const genericPool = require('generic-pool')
const createPuppeteerPool = ({
// pool 的最大容量
max = 10,
// pool 的最小容量
min = 2,
// 连接在池中保持空闲而不被回收的最小时间值
idleTimeoutMillis = 30000,
// 最大使用数
maxUses = 50,
// 在连接池交付实例前是否先经过 factory.validate 测试
testOnBorrow = true,
puppeteerArgs = {},
validator = () => Promise.resolve(true),
...otherConfig
} = {}) => {
const factory = {
// 创建实例
create: () =>
puppeteer.launch(puppeteerArgs).then(instance => {
instance.useCount = 0
return instance
}),
// 销毁实例
destroy: instance => {
instance.close()
},
// 验证实例可用性
validate: instance => {
return validator(instance).then(valid =>
// maxUses 小于 0 或者 instance 使用计数小于 maxUses 时可用
Promise.resolve(valid && (maxUses <= 0 || instance.useCount < maxUses))
)
}
}
const config = {
max,
min,
idleTimeoutMillis,
testOnBorrow,
...otherConfig
}
// 创建连接池
const pool = genericPool.createPool(factory, config)
const genericAcquire = pool.acquire.bind(pool)
// 池中资源连接时进行的操作
pool.acquire = () =>
genericAcquire().then(instance => {
instance.useCount += 1
return instance
})
pool.use = fn => {
let resource
return pool
.acquire()
.then(r => {
resource = r
return r
})
.then(fn)
.then(
result => {
// 释放资源
pool.release(resource)
return result
},
err => {
pool.release(resource)
throw err
}
)
}
return pool
}
module.exports = createPuppeteerPool

使用连接池的方式也很简单,如下代码:

const pool = createPuppeteerPool({
puppeteerArgs: {
args: config.browserArgs
}
})
module.exports = pool

该真正使用了,笔者封装了一个方法,该方法支持接受一个 URL 也支持接受具体的 HTML 字符串去生成相应海报:

// 获取连接池
const pool = require('./pool')
const config = require('./config')
const render = (ctx, handleFetchPicoImageError) =>
// 使用连接池资源
pool.use(async browser => {
const { body, query } = ctx.request
// 打开新的页面
const page = await browser.newPage()
// 服务支持直接传递 HTML 字符串内容
let html = body
// 从请求服务的 query 获取默认参数
const {
width = 300,
height = 480,
ratio: deviceScaleFactor = 2,
type = 'png',
filename = 'poster',
waitUntil = 'domcontentloaded',
quality = 100,
omitBackground,
fullPage,
url,
useCache = 'true',
usePicoAutoJPG = 'true'
} = query
let image
try {
// 设置浏览器视口
await page.setViewport({
width: Number(width),
height: Number(height),
deviceScaleFactor: Number(deviceScaleFactor)
})
if (html.length > 1.25e6) {
throw new Error('image size out of limits, at most 1 MB')
}
// 访问 URL 页面
await page.goto(url || `data:text/html,${html}`, {
waitUntil: waitUntil.split(',')
})
// 进行截图
image = await page.screenshot({
type: type === 'jpg' ? 'jpeg' : type,
quality: type === 'png' ? undefined : Number(quality),
omitBackground: omitBackground === 'true',
fullPage: fullPage === 'true'
})
} catch (error) {
throw error
}
ctx.set('Content-Type', `image/${type}`)
ctx.set('Content-Disposition', `inline; filename=${filename}.${type}`)
await page.close()
return image
})
module.exports = render

至此,基于 Puppeteer 的海报系统就已经开发完成了。它是一个对外的 Node.js 服务。

标签:场景,const,await,实践,puppeteer,html,return,pool
From: https://blog.51cto.com/u_15296224/6042728

相关文章

  • 顶级开发者的7个日常实践
    我通过艰难的方式学会了这些做法。从我的经验中学习。始终如一地完成这7件事,您就会在团队中受到关注。交付价值,为您的开发人员职业生涯打下坚实的基础。顶级开发者有......
  • 【Redis场景4】单机环境下秒杀问题
    单机环境下的秒杀问题全局唯一ID为什么要使用全局唯一ID:当用户抢购时,就会生成订单并保存到订单表中,而订单表如果使用数据库自增ID就存在一些问题:受单表数据量的限制i......
  • vue高级进阶( 三 ) 组件高级用法及最佳实践
     vue高级进阶(三)组件高级用法及最佳实践世界上有太多孤独的人害怕先踏出第一步。---绿皮书书接上回,上篇介绍了vue组件通信比较有代表性的几种方法,本篇主要讲......
  • 理论+实践,教你如何使用Nginx实现限流
    摘要:Nginx作为一款高性能的Web代理和负载均衡服务器,往往会部署在一些互联网应用比较前置的位置。此时,我们就可以在Nginx上进行设置,对访问的IP地址和并发数进行相应的限制。......
  • 实战分享 | 金融数据采集报送平台实践
    大数据时代,数据在企业的日常经营中无处不在,各类数据的汇总、整合、分析、研究对企业的决策和发展有着至关重要的作用。企业要进行数字化转型,本质是强化对数据的使用,包含数据......
  • centos7+nginx+uwsgi+python3.7.4+django部署实践
    yuminstallpython3yuminstallpython3-develyum-yinstallgccgcc-c++pip3installuwsgipython3​​manage.py​​runserver10.5.1.65:82uwsgi--iniuwsgi.i......
  • 用户行为分析模型实践(三)——H5通用分析模型
    作者:vivo互联网大数据团队-ZhaoWei、TianFengbiao、LiXiong本文从提升用户行为分析效率角度出发,详细介绍了H5埋点方案规划,埋点数据采集流程,提供可借鉴的用户行为数据采......
  • Redis的十六种应用场景
    Redis16个常见使用场景这个场景最开始是是一篇介绍微博Redis应用的PPT中看到的,其中提到微博的Redis主要是用在在计数和好友关系两方面上,当时对好友关系方面的用法不太了......
  • 大咖说·云教育科技|教育数字化变革的实践探索之路
    新时代背景下,中小学如何实现学习时空的开放共享?如何利用数据驱动教育教学?云机房在其中发挥了怎样的作用?本期大咖说,杭州市学军小学教育集团总校长张军林,分享中小学场景中......
  • 基于Flutter的移动端跨平台应用实践
    长期以来,移动端的开发都需要为相同的产品逻辑实现两套代码。在大多数情况下,这两套代码所描述的逻辑基本是一致的,只是用不同的编程语言在阐述,为的是部署到不同的平台上。这......