Node+Puppeteer генерирует постеры

Node.js внешний интерфейс Puppeteer
Node+Puppeteer генерирует постеры

предисловие

Последняя статья была написана несколько дней назад, потому что я использовал ееhtml2canvasЯ столкнулся с множеством проблем с совместимостью и чуть не убежал с ведром. Затем, под руководством больших парней в области комментариев, я нашел схему генерации плакатов с простой операцией и высокой возможностью повторного использования——Node+Puppeteer генерирует постеры.

Основная идея дизайна: доступ к интерфейсу генерации постеров, интерфейс обращается к входящему адресу через Puppeteer и возвращает скриншот соответствующего элемента.

PuppeteerСоздание плакатов относительноCanvasВ чем преимущества генерации:

  • Нет совместимости браузера, совместимости платформы и других проблем.
  • Возможность повторного использования кода высока, и можно использовать службу создания плакатов h5, апплета и приложения.
  • Пространство операций оптимизации больше. Поскольку интерфейс изменен на форму генерации постеров, для оптимизации скорости отклика можно использовать различные методы на стороне сервера, такие как: добавление серверов и добавление кешей

Знакомство с кукловодом

Puppeteer — это библиотека Node, которая предоставляет высокоуровневый API для управления Chromium или Chrome через протокол DevTools. Puppeteer по умолчанию работает в безголовом режиме, который является «безголовым» режимом, но его можно настроить, изменивheadless:falseЗапустите режим «заголовок». Подавляющее большинство вещей, которые вы можете сделать вручную в браузере, можно сделать с помощью Puppeteer! Вот некоторые примеры:

  • Создайте страницу в формате PDF или снимок экрана.
  • Возьмите SPA (одностраничное приложение) и создайте предварительно обработанный контент (он же «SSR» (рендеринг на стороне сервера)).
  • Автоматически отправить форму, выполнить тест на пользователь, ввод клавиатуры и т. Д.
  • Создайте постоянно обновляемую автоматизированную тестовую среду. Выполняйте тесты непосредственно в последней версии Chrome, используя новейшие функции JavaScript и браузера.
  • Захватите временную трассировку веб-сайта, чтобы помочь проанализировать проблемы с производительностью.
  • Протестируйте расширение браузера.

Реализация схемы

1. Напишите простой интерфейс

Express — это лаконичная и гибкая платформа веб-приложений node.js. Используйте экспресс, чтобы написать простую службу узла, определить интерфейс и передать элементы конфигурации, необходимые для получения снимков экрана, в puppeteer.

const express = require('express')
const createError = require("http-errors")
const app = express()
// 中间件--json化入参
app.use(express.json())
app.post('/api/getShareImg', (req, res) => {
    // 业务逻辑
})
// 错误拦截
app.use(function(req, res, next) {
    next(createError(404));
});
app.use(function(err, req, res, next) {
    let result = {
        code: 0,
        msg: err.message,
        err: err.stack
    }
    res.status(err.status || 500).json(result)
})
// 启动服务监听7000端口
const server = app.listen(7000, '0.0.0.0', () => {
    const host = server.address().address;
    const port = server.address().port;
    console.log('app start listening at http://%s:%s', host, port);
});

2. Создайте модуль скриншота

Откройте браузер => откройте вкладку => сделайте снимок экрана => закройте браузер

const puppeteer = require("puppeteer");

module.exports = async (opt) => {
    try {
        const browser = await puppeteer.launch();
        const page = await browser.newPage();
        await page.goto(opt.url, {
            waitUntil: ['networkidle0']
        });
        await page.setViewport({
            width: opt.width,
            height: opt.height,
        });
        const ele = await page.$(opt.ele);
        const base64 = await ele.screenshot({
            fullPage: false,
            omitBackground: true,
            encoding: 'base64'
        });
        await browser.close();
        return 'data:image/png;base64,'+ base64
    } catch (error) {
        throw error
    }
};
  • puppeteer.launch([options]): запустить браузер
  • browser.newPage(): создать вкладку
  • page.goto(url[ options]): перейти на страницу
  • page.setViewport(viewport): сформулируйте окно, чтобы открыть страницу
  • page.$(селектор): выбор элемента
  • elementHandle.screenshot([options]): Скриншот. вencodingСвойство может указать, что возвращаемое значение — base64 или Buffer.
  • browser.close(): закрыть браузер и вкладку

3. Оптимизация

1. Оптимизация времени запроса

page.goto(url[, options])Элементы конфигурации методаwaitUntilУказывает, в каком состоянии выполнение завершено, по умолчанию — когда запускается событие загрузки. События включают в себя:

 await page.goto(url, {
     waitUntil: [
         'load', //页面“load” 事件触发
         'domcontentloaded', //页面 “DOMcontentloaded” 事件触发
         'networkidle0', //在 500ms 内没有任何网络连接
         'networkidle2' //在 500ms 内网络连接个数不超过 2 个
     ]
 });

При использованииnetworkidle0Решение ожидания завершения страницы, вы обнаружите, что время отклика интерфейса будет больше, потому чтоnetworkidle0Ему нужно ждать 500 мс.В реальных бизнес-сценариях во многих случаях ждать не нужно, поэтому вы можете инкапсулировать устройство задержки и настроить время ожидания. Например, наша страница постера просто отображает фоновое изображение и изображение QR-кода, а страница запускаетloadКогда загрузка завершена, время ожидания не требуется, вы можете указать 0, чтобы пропустить время ожидания.

 const waitTime = (n) => new Promise((r) => setTimeout(r, n));
 //省略部分代码
 await page.goto(opt.url);
 await waitTime(opt.waitTime || 0);

Если этот метод не может быть удовлетворен, вам нужна страница, чтобы уведомить puppeteer об окончании в определенное время, вы также можете использоватьpage.waitForSelector(selector[, options])Ожидает появления указанного элемента страницы. Например: когда страница завершает выполнение операции, вставьтеid="end"элемент, puppereer ожидает появления этого элемента.

 await page.waitForSelector("#end")

К подобным методам относятся:

  • page.waitForXPath(xpath[ options]): дождитесь появления на странице элемента, соответствующего xPath.
  • page.waitForSelector(selector[ options]): подождите, пока элемент, соответствующий указанному селектору, не появится на странице. Если при вызове этого метода уже есть соответствующий элемент, этот метод возвращается немедленно.
  • page.waitForResponse(urlOrPredicate[ options]): дождитесь окончания указанного ответа.
  • page.waitForRequest(urlOrPredicate[ options]): дождитесь появления указанного ответа.
  • page.waitForFunction(pageFunction[ options[ ...args]]): дождитесь выполнения метода.
  • page.waitFor(selectorOrFunctionOrTimeout[ options[ ...args]]): этот метод эквивалентен селектору вышеуказанных методов, и результат отличается в зависимости от первого параметра. Например, если передается строковый тип in, он будет судить, является ли это xpath или selector, что эквивалентно waitForXPath или waitForSelector.

2. Оптимизация запуска

При запуске Chromium также включит множество ненужных функций, а некоторые элементы автозагрузки можно будет отключить через параметры.

    const browser = await puppeteer.launch({
        headless: true,
        slowMo: 0,
        args: [
            '--no-zygote',
            '--no-sandbox',
            '--disable-gpu',
            '--no-first-run',
            '--single-process',
            '--disable-extensions',
            "--disable-xss-auditor",
            '--disable-dev-shm-usage',
            '--disable-popup-blocking',
            '--disable-setuid-sandbox',
            '--disable-accelerated-2d-canvas',
            '--enable-features=NetworkService',
        ]
    });

3. Повторно используйте браузеры

Потому что каждый раз при вызове интерфейса запускается браузер, а после снятия скриншота браузер закрывается, что приводит к пустой трате ресурсов, а запуск браузера требует времени. А если одновременно запущено слишком много браузеров, программа выдаст исключение. Поэтому используется пул соединений: запустите несколько браузеров, создайте вкладку под одним из браузеров, чтобы открыть страницу, и закройте вкладку только после того, как снимок экрана будет завершен, выйдя из браузера. Когда приходит следующий запрос, вкладка создается непосредственно для достижения цели повторного использования браузера. Браузер закрывается, когда количество использований браузера достигает определенного числа или когда он не использовался в течение определенного периода времени. Некоторые боссы ужеgeneric-poolЭтот пул соединений был обработан, и я использовал его напрямую.

const initPuppeteerPool = () => {
 if (global.pp) global.pp.drain().then(() => global.pp.clear())
 const opt = {
   max: 4,//最多产生多少个puppeteer实例 。
   min: 1,//保证池中最少有多少个puppeteer实例存活
   testOnBorrow: true,// 在将实例提供给用户之前,池应该验证这些实例。
   autostart: false,//是不是需要在池初始化时初始化实例
   idleTimeoutMillis: 1000 * 60 * 60,//如果一个实例60分钟都没访问就关掉他
   evictionRunIntervalMillis: 1000 * 60 * 3,//每3分钟检查一次实例的访问状态
   maxUses: 2048,//自定义的属性:每一个 实例 最大可重用次数。
   validator: () => Promise.resolve(true)
 }
 const factory = {
   create: () =>
     puppeteer.launch({
       //启动参数参考第二条
     }).then(instance => {
       instance.useCount = 0;
       return instance;
     }),
   destroy: instance => {
     instance.close()
   },
   validate: instance => {
     return opt.validator(instance).then(valid => Promise.resolve(valid && (opt.maxUses <= 0 || instance.useCount < opt.maxUses)));
   }
 };
 const pool = genericPool.createPool(factory, opt)
 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 resource
     })
     .then(fn)
     .then(
       result => {
         // 不管业务方使用实例成功与后都表示一下实例消费完成
         pool.release(resource)
         return result
       },
       err => {
         pool.release(resource)
         throw err
       }
     )
 }
 return pool;
}
global.pp = initPuppeteerPool()

4. Оптимизируйте интерфейс, чтобы предотвратить повторную генерацию изображений.

Когда повторные вызовы с одним и тем же набором параметров будут каждый раз открывать процесс браузера для создания снимков экрана, вы можете использовать механизм кэширования для оптимизации повторных запросов. Вы можете сохранить изображение base64 в Redis или записать его в память, передав уникальный ключ в качестве идентификатора (например, идентификатор пользователя + идентификатор действия). Когда интерфейс запрашивается, сначала проверьте, был ли он сгенерирован в кеше, и если он был сгенерирован, он напрямую извлекается из кеша. В противном случае пройдите процесс создания плакатов.

конец

Данное решение уже начало опытную эксплуатацию в проекте, что очень удобно мне как фронтенд разработчику, мне больше не нужно пошагово отрисовывать канвас в апплете, и не нужно учитывать междоменные ресурсы , и мне не нужно учитывать браузер WeChat. , Проблемы совместимости с различными встроенными браузерами. Сэкономленное время, чтобы позволить мне написать эту статью. Во-вторых, меня больше беспокоят проблемы с производительностью, потому что будет запускаться только совместное действие, параллелизм небольшой, а текущее использование не выявило проблем с производительностью.Знающие люди могут подсказать мне дальнейшую оптимизацию или профилактику.

код

Посмотреть полный код:github

Связанное чтение

Про то, что я использовал html2canvas и чуть не убежал с ведром

использованная литература