| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- const express = require('express');
- const puppeteer = require('puppeteer-core');
- const app = express();
- const port = process.env.PORT || 4000;
- const wsEndpoint = process.env.BROWSER_WS_ENDPOINT;
- const poolSize = parseInt(process.env.MAX_CONCURRENCY, 10) || 5;
- const renderTimeout = parseInt(process.env.RENDER_TIMEOUT_MS, 10) || 10000;
- let browser, context;
- let isReady = false;
- const pagePool = [];
- const pageQueue = [];
- // 初始化单个页面拦截和加载空白页
- async function initializePage(page) {
- await page.setRequestInterception(true);
- page.on('request', req => {
- const url = req.url();
- if (/\.(png|jpg|jpeg|gif|svg|woff2?|ttf)$/.test(url) || /analytics|gtag|ads|doubleclick/i.test(url)) {
- req.abort();
- } else {
- req.continue();
- }
- });
- await page.goto('about:blank', { timeout: 5000, waitUntil: 'networkidle2' });
- }
- // 创建新的浏览器上下文和页面池
- async function createContextAndPages() {
- if (context) {
- try {
- await context.close();
- } catch {}
- }
- context = await browser.createIncognitoBrowserContext();
- pagePool.length = 0;
- for (let i = 0; i < poolSize; i++) {
- const page = await context.newPage();
- await initializePage(page);
- pagePool.push(page);
- }
- console.log(`Created new context and initialized ${pagePool.length} pages`);
- }
- // 定时重启浏览器上下文,避免 browserless 自动关闭
- async function periodicContextRestart() {
- try {
- console.log('Periodic context restart started...');
- isReady = false;
- await createContextAndPages();
- isReady = true;
- console.log('Periodic context restart done.');
- } catch (err) {
- console.error('Error during periodic context restart:', err);
- }
- }
- // 连接并初始化浏览器和上下文
- async function initializeBrowser() {
- browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint });
- browser.on('disconnected', () => {
- console.warn('Browser disconnected, marking not ready and reconnecting...');
- isReady = false;
- reconnectBrowser();
- });
- await createContextAndPages();
- isReady = true;
- console.log('Browser connected and ready');
- }
- // 重连逻辑,失败最多重试5次,失败后退出进程
- let reconnectCount = 0;
- async function reconnectBrowser() {
- if (browser) {
- try {
- await browser.close();
- } catch {}
- }
- reconnectCount++;
- if (reconnectCount > 5) {
- console.error('Reconnect failed 5 times, exiting process');
- process.exit(1);
- }
- console.log(`Reconnect attempt #${reconnectCount}...`);
- try {
- await initializeBrowser();
- reconnectCount = 0; // 成功重连后计数归零
- } catch (err) {
- console.error('Reconnect failed:', err.message);
- setTimeout(reconnectBrowser, 3000);
- }
- }
- // 页面池获取
- function acquirePage() {
- return new Promise(resolve => {
- if (pagePool.length > 0) {
- resolve(pagePool.pop());
- } else {
- pageQueue.push(resolve);
- }
- });
- }
- // 页面释放
- function releasePage(page) {
- if (page.isClosed()) return;
- if (pageQueue.length > 0) {
- const next = pageQueue.shift();
- next(page);
- } else {
- pagePool.push(page);
- }
- }
- // 启动服务主流程
- (async () => {
- try {
- await initializeBrowser();
- // 每7分钟重启一次上下文,避开 browserless 10分钟超时关闭
- setInterval(periodicContextRestart, 7 * 60 * 1000);
- } catch (err) {
- console.error('Startup failed:', err.message);
- reconnectBrowser();
- }
- })();
- app.get('/healthz', (req, res) => {
- res.json({ ready: isReady, poolSize, queueLength: pageQueue.length });
- });
- app.get('/', (req, res) => {
- res.send(isReady ? 'Renderer running' : 'Renderer not ready');
- });
- app.get('/render', async (req, res) => {
- if (!isReady) return res.status(503).send('Renderer not ready');
- const url = req.query.url;
- if (!url) return res.status(400).send('Missing url parameter');
- let page;
- try {
- page = await acquirePage();
- await page.goto('about:blank', { waitUntil: 'networkidle2', timeout: 5000 });
- await page.goto(url, { waitUntil: 'networkidle2', timeout: renderTimeout });
- await page.waitForFunction('document.readyState === "complete"', { timeout: 5000 });
- const html = await page.content();
- res.set('Content-Type', 'text/html; charset=utf-8');
- res.send(html);
- } catch (err) {
- console.error('Render error:', err.message);
- res.status(504).send('Render failed: ' + err.message);
- } finally {
- if (page && !page.isClosed()) {
- try {
- releasePage(page);
- } catch (e) {
- console.warn('Release page error:', e.message);
- await page.close().catch(() => {});
- }
- }
- }
- });
- app.listen(port, () => {
- console.log(`Renderer listening on port ${port}`);
- });
|