>  기사  >  웹 프론트엔드  >  Puppeteer 라이브러리를 사용하여 Node에서 포스터를 생성하는 방법에 대한 간략한 분석(구현 계획 공유)

Puppeteer 라이브러리를 사용하여 Node에서 포스터를 생성하는 방법에 대한 간략한 분석(구현 계획 공유)

青灯夜游
青灯夜游앞으로
2022-01-18 19:26:444024검색

Node를 사용하여 포스터를 생성하는 방법은 무엇입니까? 다음 글에서는 Node+Puppeteer를 사용하여 포스터를 생성하는 방법을 소개하겠습니다. 도움이 되길 바랍니다!

Puppeteer 라이브러리를 사용하여 Node에서 포스터를 생성하는 방법에 대한 간략한 분석(구현 계획 공유)

이전 글에서 며칠 전 html2canvas를 사용하다가 많은 호환성 문제에 부딪혀 버킷을 들고 도망갈 뻔했다고 적었습니다. 그러다가 댓글 영역의 전문가들의 지도를 통해 작동이 간단하고 재사용 가능성이 높은 포스터 생성 솔루션인 Node+Puppeteer가 포스터를 생성합니다를 발견했습니다.

주요 디자인 아이디어는: 포스터 생성을 위한 인터페이스에 액세스하는 것입니다. 인터페이스는 Puppeteer를 통해 수신 주소에 액세스하고 해당 요소의 스크린샷을 반환합니다.

캔버스 생성에 비해 Puppeteer를 사용한 포스터 생성의 장점은 무엇입니까?

  • 브라우저 호환성, 플랫폼 호환성 및 기타 문제가 없습니다.
  • 코드는 재사용성이 뛰어나며 h5, 미니 프로그램 및 앱용 포스터를 생성하는 데 사용할 수 있습니다.
  • 최적화 작업을 위한 더 많은 공간. 포스터를 생성하는 인터페이스로 변경되었기 때문에 서버 추가, 캐시 추가

Puppeteer 소개

Puppeteer는 Nodejs입니다. DevTools 프로토콜을 통해 Chromium 또는 Chrome을 제어하기 위한 고급 API를 제공하는 라이브러리입니다. Puppeteer는 기본적으로 헤드리스 모드, 즉 "헤드리스" 모드에서 실행되지만 headless:false 구성을 수정하여 "헤드리스" 모드를 실행할 수 있습니다. 브라우저에서 수동으로 수행하는 대부분의 작업은 Puppeteer를 사용하여 수행할 수 있습니다! 다음은 몇 가지 예입니다.

  • 페이지 PDF 또는 스크린샷을 생성합니다.
  • SPA(단일 페이지 애플리케이션)를 포착하고 사전 렌더링된 콘텐츠(예: "SSR"(서버 측 렌더링))를 생성하세요.
  • 자동으로 양식 제출, UI 테스트, 키보드 입력 등을 수행합니다.
  • 지속적으로 업데이트되는 자동화된 테스트 환경을 만듭니다. 최신 JavaScript 및 브라우저 기능을 사용하여 최신 버전의 Chrome에서 직접 테스트를 실행하세요.
  • 웹사이트의 타임라인 추적을 캡처하여 성능 문제를 분석하는 데 도움을 줍니다.
  • 브라우저 확장 기능을 테스트하세요.

솔루션 구현

1. 간단한 인터페이스 작성

Express는 간단하고 유연한 node.js 웹 애플리케이션 프레임워크입니다. Express를 사용하여 간단한 노드 서비스를 작성하고, 인터페이스를 정의하고, 스크린샷에 필요한 구성 항목을 받아 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([옵션]): 브라우저 실행
  • browser.newPage(): 탭 페이지 생성
  • page.goto(url[, options]): 페이지 탐색
  • page.setViewport(viewport): 페이지를 열 창을 지정
  • page .$ (선택기): 요소 선택
  • elementHandle.screenshot([옵션]): 스크린샷. 인코딩 속성은 반환 값이 base64 또는 Buffer
  • browser.close()임을 지정할 수 있습니다. 브라우저 및 탭 페이지를 닫습니다

3. 최적화

1. goto( url[, options]) 메소드의 waitUntil 구성 항목은 실행이 완료된 상태를 나타냅니다. 기본값은 로드 이벤트가 트리거되는 시점입니다. 이벤트에는 다음이 포함됩니다.

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

networkidle0 솔루션을 사용하여 페이지가 완료될 때까지 기다리는 경우 networkidle0이 500ms를 기다려야 하기 때문에 인터페이스의 응답 시간이 더 길어지는 것을 알 수 있습니다. 대부분의 경우 기다릴 필요가 없으므로 지연 시간을 사용자 정의할 수 있습니다. 예를 들어, 포스터 페이지는 배경 이미지와 QR 코드 이미지만 렌더링합니다. 페이지가 로드되면 대기 시간을 건너뛰기 위해 0을 전달할 수 있습니다.

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

이 방법이 만족스럽지 않고 페이지가 특정 시간에 인형 조종자에게 끝을 알려야 하는 경우 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]]):此方法相当于上面几个方法的选择器,根据第一个参数的不同结果不同,比如:传入一个string类型,会判断是不是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. 优化接口防止图片重复生成

        用同一组参数重复调用时每次都会开启一个浏览器进程去截图,可以使用缓存机制优化重复的请求。可以通过传入唯一的key作为标识位(比如用户id+活动id),将图片base64存入redis或者写入内存中。当接口被请求时先查看缓存里是否已经生成过,如果生成过就直接从缓存取。否则就走生成海报的流程。

结尾

        这个方案目前已经开始在项目里试运行了,这对于我一个前端开发来说简直太友好了,再也不用在小程序里一步一步去绘制canvas,不用考虑资源跨域,也不用考虑微信浏览器、各种自带浏览器的兼容问题。省下了时间可以让我写这篇文章。其次,我比较担心的还是性能问题,因为只有在分享的动作才会触发,并发较小,目前使用还未暴露出性能的问题,有了解的大佬们可以指导我一下可以进一步优化或者预防的点。

代码

完整代码查看:github

https://github.com/yuwuwu/markdown-code/tree/master/puppeteer%E6%88%AA%E5%9B%BE

更多node相关知识,请访问:nodejs 教程!!

위 내용은 Puppeteer 라이브러리를 사용하여 Node에서 포스터를 생성하는 방법에 대한 간략한 분석(구현 계획 공유)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.cn에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제