NodeJS 使用puppeteer创建zip截图下载

vbopmzt1  于 2023-04-05  发布在  Node.js
关注(0)|答案(1)|浏览(121)

**问题:**是否有一种好方法让puppeteer.js截图,然后,而不是为他们指定一个本地路径,将它们捆绑到一个用户可以下载的.zip文件中?这将作为一个部署的Web应用程序而不是命令行工具存在。代替直接的建议,我想也许有一些关于在哪里以及如何最好地搜索这个的提示,这样我就可以更好地自己研究它。
**额外的上下文:**我的目标是制作一个应用程序,我的设计师可以用它来捕获以前工作的屏幕截图。最佳状态是他们能够使用它,而不需要使用命令行做任何事情,所以我能想到的最佳方法是实时部署它(尽管我愿意接受其他建议,以防有我没有考虑过的选项)。 puppet 师屏幕截图的标准方法似乎是为图像指定一个本地路径,但我意识到这会产生安全和隐私问题(即使它只会在内部使用),所以我想知道是否有一个好的方法将屏幕截图捆绑到一个可下载的zip中,而不是让它们直接进入某人的本地文件系统。

kt06eoxx

kt06eoxx1#

我能够让这个工作,唯一剩下的问题是部署应用程序时的超时503错误,但这是一个不同的问题,我认为应该忽略这个特定问题的意图(与puppeteer运行所有需要运行的动作所需的时间长度有关,导致heroku超时,但这仍然是一个本地设置中的工作应用程序)。
在高层次上,这里是使其工作的重要部分(将包括底部的代码):

服务器:

1.导入adm-zip。
1.使用adm-zip声明一个zip变量。
1.删除/确保屏幕截图代码块中没有path属性。
1.由于每个屏幕截图都是用puppeteer记录的,因此使用.addFile()方法将其添加到zip变量中。
1.一旦所有屏幕截图都在zip文件中,将其转换为缓冲区对象并将该文件发送到客户端。

客户:

1.将zip buffer对象转换为新的blob,确保使用Uint8Array()将其转换为8位数组。
1.创建一个新的url窗口对象,使用您刚刚创建的带有返回的zip数据的blob。

  • 创建a标记并应用以下内容:
  • 应用上面创建的URL对象作为href属性
  • 创建一个download属性,并为它指定您希望下载的zip文件的任何文件名
  • 将a标记附加到文档正文
  • 触发a标记上的click方法,以便自动进行下载
    代码(完整):
  • server.js*
const express = require('express');
const path = require('path');
const PORT = process.env.PORT || 3001;
const puppeteer = require('puppeteer-core');
const { executablePath } = require('puppeteer');
const os = require('os');
const AdmZip = require("adm-zip");

// TODO/NICE TO HAVE: Figure out chrome paths for linux
const CHROME_PATHS = {
  darwin: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
  linux: '/usr/bin/google-chrome',
  win32: 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
};
const CHROME_PATH = CHROME_PATHS[os.platform()];

const PREVIEW_SELECTOR = '.dynamic-ad-card-back iframe';
const NEXT_SELECTOR = '.md-icon-button[aria-label="Next"]';
const PIXEL_DENSITY = 2;
let DELAY_FOR_ANIMATION = 15000;

const app = express();

app.use(express.static(path.resolve(__dirname, '../dcsgrab/build')));

app.get('/dcsgrab', (request, response) => {
    const zip = new AdmZip();

    (async () => {
        const browser = await puppeteer.launch({
            headless: true,
            // executablePath: executablePath(), // use if app is deployed
            executablePath: CHROME_PATH, // use if app is local
            args: [
          '--no-sandbox',
        '--disable-setuid-sandbox',
        '--single-process',
        ],
        });

        let screenshotCounter = 1;

        const page = await browser.newPage();

        page.setViewport({width: 1280, height: 6000, deviceScaleFactor: PIXEL_DENSITY});

        await page.goto(request.query.tearsheetUrl, { waitUntil: 'networkidle0' });

        /**
       * Checks if the pagination button is active
       * @return {Promise.<Boolean>} Promise which resolves with a true boolean if the button is active
       */
      async function isNextButtonActive() {
        return await page.evaluate((selector) => {
          return !document.querySelector(selector).disabled;
        }, NEXT_SELECTOR);
      }

      /**
       * Clicks the pagination button
       * @return {Promise} Promise which resolves when the element matching selector is successfully clicked. The Promise will be rejected if there is no element matching selector
       */
      async function clickNextButton() {
        return await page.click(NEXT_SELECTOR, {delay: 100});
      }

      /**
       * Waits for the loading spinner widget to go away, indicating the iframes have been added to the page
       * @return {Promise.undefined}
       */
      async function waitForLoadingWidget() {
        return await page.waitForSelector('.preview-loading-widget', {hidden: true}).then(() => {
          console.log('Loading widget is gone');
        })
          .catch(e => {
            console.log(e.message);
          });
      }

      /**
       * Gets the name of the tear sheet
       * @return {Promise<string>} The name
       */
      async function getSheetName() {
        return await page.evaluate((selector) => {
          return document.querySelector(selector).textContent.replace(/[*."/\\[\]:;|=,]/g, '-');
        }, '.preview-sheet-header-text span');
      }

      /**
       * Screenshot the creative elements on the current page
       * @return {Promise.<Array>} Promise which resolves with an array of clipping paths
       */
        async function getScreenShots() {
            const rects = await page.$$eval(PREVIEW_SELECTOR, iframes => {
              return Array.from(iframes, (el) => {
                const {x, y, width, height} = el.getBoundingClientRect();

                return {
                  left: x,
                  top: y,
                  width,
                  height,
                  id: el.id,
                };
              });
            }, PREVIEW_SELECTOR).catch(e => {
              console.error(e.message);
            });

            return Promise.all(rects.map(async (rect) => {
              return await page.screenshot({
                clip: {
                  x: rect.left,
                  y: rect.top,
                  width: rect.width,
                  height: rect.height,
                },
              }).then((content) => {
                zip.addFile(`screenshot-${screenshotCounter++}.png`, Buffer.from(content, "utf8"), "entry comment goes here");
                console.log(`${rect.id} element captured and stored in zip`);
              })
                .catch((e) => {
                  console.error(e.message);
                });
            }));
        }

        // Wait a bit then take screenshots
      await new Promise(resolve => setTimeout(resolve, DELAY_FOR_ANIMATION));
      await getScreenShots().catch((e) => console.error(e.message));

        // Continue taking screenshots till there are no pages left
      while (await isNextButtonActive()) {
        await clickNextButton();
        await waitForLoadingWidget();
        await new Promise(resolve => setTimeout(resolve, DELAY_FOR_ANIMATION)),
        await getScreenShots().catch((e) => console.error(e.message));
      }

        await browser.close();

        const zipToSend = zip.toBuffer();

        response.json({ 
            message: 'Screenshots are done!\nPlease check the zip file that was just downloaded.',
            zipFile: zipToSend
        });
    })();
});

app.get('*', (request, response) => {
    response.sendFile(path.resolve(__dirname, '../dcsgrab/build', 'index.html'));
});

app.listen(PORT, () => {
    console.log(`Server is listening on port ${PORT}`);
});
  • 应用程序.js*
import React, { useState, useRef, useLayoutEffect } from 'react';
import { gsap } from 'gsap';
import './App.css';
import DataInput from './Components/data-input';
import Footer from './Components/footer';
import Header from './Components/header';
import RedBall from './Components/red-ball';

const timeline = gsap.timeline({paused: true, repeat: -1, yoyo: true});

function App() {
  const [messageData, setMessageData] = useState(null);
  const [statusMessage, showStatusMessage] = useState(false);

  const tl = useRef(timeline);
  const app = useRef(null);

  let zipBlob;
  let zipDownload;
  let url;

  useLayoutEffect(() => {
    const ctx = gsap.context(() => {
      tl.current.fromTo('.red-ball', .5, {autoAlpha: 0, x: 0}, {autoAlpha: 1, x: 20});
    }, app.current);

    return () => ctx.revert();
  }, []);

  const getScreenshotData = (screenShotData) => {
    showStatusMessage(true);
    setMessageData('');

    if (statusMessage) {
      timeline.play();
    }

    fetch(`/dcsgrab?tearsheetUrl=${screenShotData}`)
      .then((response) => response.json())
      .then((data) => {
        zipBlob = new Blob([new Uint8Array(data.zipFile.data)], {type: "octet/stream"});
        url = window.URL.createObjectURL(zipBlob);
        zipDownload = document.createElement("a");

        setMessageData(data.message);

        zipDownload.href = url;
        zipDownload.download = "screenshot-download.zip";
        document.body.appendChild(zipDownload);
        zipDownload.click();

        console.log(zipBlob);
        console.log([new Uint8Array(data.zipFile.data)]);
        console.log(data);
      });
  };

  return (
    <div className="App" ref={app}>
      <Header />
      <DataInput getScreenshotData={getScreenshotData} />
      {
        !statusMessage ? '' : <p>{!messageData ? 'Taking screenshots...' : messageData}</p>
      }
      {
        !statusMessage ? '' : <div className="waiting-anim-container">{!messageData ? <RedBall /> : ''}</div>
      }
      <Footer />
    </div>
  );
}

export default App;

相关问题