service.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. const express = require('express');
  2. const puppeteer = require('puppeteer-core');
  3. const app = express();
  4. const port = process.env.PORT || 4000;
  5. const wsEndpoint = process.env.BROWSER_WS_ENDPOINT;
  6. const poolSize = parseInt(process.env.MAX_CONCURRENCY, 10) || 5;
  7. const renderTimeout = parseInt(process.env.RENDER_TIMEOUT_MS, 10) || 10000;
  8. let browser, context;
  9. let isReady = false;
  10. const pagePool = [];
  11. const pageQueue = [];
  12. // 初始化单个页面拦截和加载空白页
  13. async function initializePage(page) {
  14. await page.setRequestInterception(true);
  15. page.on('request', req => {
  16. const url = req.url();
  17. if (/\.(png|jpg|jpeg|gif|svg|woff2?|ttf)$/.test(url) || /analytics|gtag|ads|doubleclick/i.test(url)) {
  18. req.abort();
  19. } else {
  20. req.continue();
  21. }
  22. });
  23. await page.goto('about:blank', { timeout: 5000, waitUntil: 'networkidle2' });
  24. }
  25. // 创建新的浏览器上下文和页面池
  26. async function createContextAndPages() {
  27. if (context) {
  28. try {
  29. await context.close();
  30. } catch {}
  31. }
  32. context = await browser.createIncognitoBrowserContext();
  33. pagePool.length = 0;
  34. for (let i = 0; i < poolSize; i++) {
  35. const page = await context.newPage();
  36. await initializePage(page);
  37. pagePool.push(page);
  38. }
  39. console.log(`Created new context and initialized ${pagePool.length} pages`);
  40. }
  41. // 定时重启浏览器上下文,避免 browserless 自动关闭
  42. async function periodicContextRestart() {
  43. try {
  44. console.log('Periodic context restart started...');
  45. isReady = false;
  46. await createContextAndPages();
  47. isReady = true;
  48. console.log('Periodic context restart done.');
  49. } catch (err) {
  50. console.error('Error during periodic context restart:', err);
  51. }
  52. }
  53. // 连接并初始化浏览器和上下文
  54. async function initializeBrowser() {
  55. browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint });
  56. browser.on('disconnected', () => {
  57. console.warn('Browser disconnected, marking not ready and reconnecting...');
  58. isReady = false;
  59. reconnectBrowser();
  60. });
  61. await createContextAndPages();
  62. isReady = true;
  63. console.log('Browser connected and ready');
  64. }
  65. // 重连逻辑,失败最多重试5次,失败后退出进程
  66. let reconnectCount = 0;
  67. async function reconnectBrowser() {
  68. if (browser) {
  69. try {
  70. await browser.close();
  71. } catch {}
  72. }
  73. reconnectCount++;
  74. if (reconnectCount > 5) {
  75. console.error('Reconnect failed 5 times, exiting process');
  76. process.exit(1);
  77. }
  78. console.log(`Reconnect attempt #${reconnectCount}...`);
  79. try {
  80. await initializeBrowser();
  81. reconnectCount = 0; // 成功重连后计数归零
  82. } catch (err) {
  83. console.error('Reconnect failed:', err.message);
  84. setTimeout(reconnectBrowser, 3000);
  85. }
  86. }
  87. // 页面池获取
  88. function acquirePage() {
  89. return new Promise(resolve => {
  90. if (pagePool.length > 0) {
  91. resolve(pagePool.pop());
  92. } else {
  93. pageQueue.push(resolve);
  94. }
  95. });
  96. }
  97. // 页面释放
  98. function releasePage(page) {
  99. if (page.isClosed()) return;
  100. if (pageQueue.length > 0) {
  101. const next = pageQueue.shift();
  102. next(page);
  103. } else {
  104. pagePool.push(page);
  105. }
  106. }
  107. // 启动服务主流程
  108. (async () => {
  109. try {
  110. await initializeBrowser();
  111. // 每7分钟重启一次上下文,避开 browserless 10分钟超时关闭
  112. setInterval(periodicContextRestart, 7 * 60 * 1000);
  113. } catch (err) {
  114. console.error('Startup failed:', err.message);
  115. reconnectBrowser();
  116. }
  117. })();
  118. app.get('/healthz', (req, res) => {
  119. res.json({ ready: isReady, poolSize, queueLength: pageQueue.length });
  120. });
  121. app.get('/', (req, res) => {
  122. res.send(isReady ? 'Renderer running' : 'Renderer not ready');
  123. });
  124. app.get('/render', async (req, res) => {
  125. if (!isReady) return res.status(503).send('Renderer not ready');
  126. const url = req.query.url;
  127. if (!url) return res.status(400).send('Missing url parameter');
  128. let page;
  129. try {
  130. page = await acquirePage();
  131. await page.goto('about:blank', { waitUntil: 'networkidle2', timeout: 5000 });
  132. await page.goto(url, { waitUntil: 'networkidle2', timeout: renderTimeout });
  133. await page.waitForFunction('document.readyState === "complete"', { timeout: 5000 });
  134. const html = await page.content();
  135. res.set('Content-Type', 'text/html; charset=utf-8');
  136. res.send(html);
  137. } catch (err) {
  138. console.error('Render error:', err.message);
  139. res.status(504).send('Render failed: ' + err.message);
  140. } finally {
  141. if (page && !page.isClosed()) {
  142. try {
  143. releasePage(page);
  144. } catch (e) {
  145. console.warn('Release page error:', e.message);
  146. await page.close().catch(() => {});
  147. }
  148. }
  149. }
  150. });
  151. app.listen(port, () => {
  152. console.log(`Renderer listening on port ${port}`);
  153. });