如何让 Puppeteer 在客户端使用 ReactJS 应用程序

Posted

技术标签:

【中文标题】如何让 Puppeteer 在客户端使用 ReactJS 应用程序【英文标题】:How to make Puppeteer work with a ReactJS application on the client-side 【发布时间】:2019-07-28 15:11:45 【问题描述】:

我是 React 的新手,我正在开发一个应用程序,该应用程序将获取网页的实际屏幕截图,并且该应用程序可以在截取的屏幕截图上绘制和添加涂鸦。我最初使用 html2canvas 和 domToImage 来获取客户端屏幕截图,但它并没有完全按照网页中显示的方式呈现图像。

Reddit 用户 /pamblam0 建议我查看 Google 的 Puppeteer。它的工作原理是它会启动一个无头 chromium 浏览器,该浏览器会转到我在本地主机上的 react 应用程序,然后轻松获取整个页面的屏幕截图。然而,我的问题是 puppeteer 在 React 应用程序中表现不佳。它给了我一个 ws 错误,正如在谷歌搜索中所解释的那样,可以通过简单地安装 ws 来修复(顺便说一句,这不起作用)。

现在我的 puppeteer 脚本运行了我的 react 应用程序。据我了解,它不适用于客户端应用程序(我可能错了)。我想要发生的是,每当我从我的 react 应用程序中单击按钮时,puppeteer 应该执行并返回一个 base64 字符串,然后将其传递给我的 react 应用程序中的组件。

这是我到目前为止所做的。

puppeteerApp.js

const puppeteer = require('puppeteer');

const takeScreenshot = async () => 
    puppeteer.launch().then(async browser => 
        const page = await browser.newPage();
        const options = 
            path: 'saved_images/webshot.png',
            encoding: 'base64'
        
        await page.goto('http://localhost:3000/',  waitUntil: 'networkidle2' );
        const elem = await page.$('iframe').then(async (iframe) => 
            return await iframe.screenshot(options)
        );

        await browser.close()
    );


takeScreenshot();

来自 react 应用的代码。 App.js

import React,  Component  from 'react';
import ScreenshotsContainer from './containers/ScreenshotsContainer/ScreenshotsContainer'
import ImageContainer from './containers/ImageContainer/ImageContainer';
import html2canvas from 'html2canvas';
import domtoimage from 'dom-to-image';
import Button from './components/UI/Button/Button'
import classes from './App.module.css';
import  CSSTransition  from 'react-transition-group'
import  ToastContainer, toast  from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';


class App extends Component 

  constructor(props) 
    super(props);
    this.state = 
      imgURIArray: [],
      img: null,
      showImageContainer: false,
      showScreenshotContainer: false,
      selectedImageURI: null,
      showSaveAnimation: false,
      showNotify: false
    
  


  storeImageToArrayHandler = (imgURI) => 
    if (imgURI !== "") 
      this.setState( imgURIArray: [...this.state.imgURIArray, imgURI] , () => 
        this.setState( showImageContainer: !this.state.showImageContainer )
      )
    
  

  getScreenshotHandler = () => 
   //use puppeteer here!!!
  



  getSelectedImageFromContainerHandler(selectedImageURI) 
    this.setState(
      selectedImageURI: selectedImageURI,
      showImageContainer: !this.state.showImageContainer
    )

  

  showImageContainerHandler(showImageContainer) 
    this.setState( showImageContainer: showImageContainer )
  

  showScreenshotContainerHandler = () => 
    this.setState( showScreenshotContainer: !this.state.showScreenshotContainer )
  
  notify = (submitSuccessful, msg) => 
    let message = msg ? msg : ""
    submitSuccessful ?
      toast.success(message, 
        autoClose: 3000,
        position: toast.POSITION.TOP_CENTER
      )
      :
      toast.error(message, 
        position: toast.POSITION.TOP_CENTER
      );

  
  render() 
    let buttonOps = (
      <CSSTransition
        in=!this.state.showScreenshotContainer
        appear=true
        timeout=300
        classNames="fade"
      >
        <div className=classes.optionButtons>
          <Button icon="fas fa-camera" type="button-success" gridClass="" buttonName="" style= width: "100%", height: "70px"  onClick=() => this.getScreenshotHandler />
          <Button icon="fas fa-images" type="button-primary " gridClass="" buttonName="" style= width: "100%", height: "70px"  onClick=() => this.showScreenshotContainerHandler />
        </div>
      </CSSTransition>
    )

    return (
      <div>
        
          this.state.showImageContainer ?
            <div>
              <ImageContainer
                img=this.state.img
                showImageContainer=showImageContainer => this.showImageContainerHandler(showImageContainer)
                storeImageToArrayHandler=imgURI => this.storeImageToArrayHandler(imgURI)
                notify=(submitSuccessful, msg) => this.notify(submitSuccessful, msg)
              />
            </div>
            : null
        
        <CSSTransition
          in=this.state.showScreenshotContainer
          appear=true
          timeout=300
          classNames="slide"
          unmountOnExit
          onExited=() => 
            this.setState( showScreenshotContainer: false )
          
        >
          <ScreenshotsContainer
            imgURIArray=this.state.imgURIArray
            getSelectedImageFromContainerHandler=imgURI => this.getSelectedImageFromContainerHandler(imgURI)
            showScreenshotContainerHandler=() => this.showScreenshotContainerHandler
            notify=(submitSuccessful, msg) => this.notify(submitSuccessful, msg)
          />

        </CSSTransition>
        this.state.showImageContainer ? null : buttonOps
        /* <button onClick=this.notify>Notify !</button> */
        <ToastContainer />

      </div >
    );
  


export default App;

任何帮助将不胜感激。谢谢!

【问题讨论】:

您需要一台服务器。请参考***.com/a/51750943/6161265、***.com/a/51732895/6161265***.com/a/54654516/6161265 How to run Puppeteer code in any web browser?的可能重复 【参考方案1】:

您的 React.js 应用程序在客户端(在浏览器中)运行。 Puppeteer 无法在该环境中运行,因为您无法在浏览器中启动完整的浏览器。

您需要的是一台可以为您完成这些工作的服务器。您可以提供 HTTP 端点(选项 1)或公开您的 puppeteer Websocket(选项 2):

选项 1:提供 HTTP 端点

对于此选项,您设置一个服务器来处理传入请求并为您运行任务(制作屏幕截图):

server.js

const puppeteer = require('puppeteer');
const express = require('express');

const app = express();

app.get('/screenshot', async (req, res) => 
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto(req.query.url); // URL is given by the "user" (your client-side application)
    const screenshotBuffer = await page.screenshot();

    // Respond with the image
    res.writeHead(200, 
        'Content-Type': 'image/png',
        'Content-Length': screenshotBuffer.length
    );
    res.end(screenshotBuffer);

    await browser.close();
)

app.listen(4000);

使用 node server.js 启动应用程序,您现在可以将 URL 传递到您的服务器并从您的服务器获取屏幕截图:http://localhost:4000/screenshot?url=https://example.com/

然后,来自服务器的响应可以用作应用程序中图像元素的来源。

选项 2:将 puppeteer Websocket 暴露给客户端

您还可以通过公开 Websocket 从客户端控制浏览器(在服务器上运行)。

为此,您需要像这样公开服务器的 Websocket:

server.js

const puppeteer = require('puppeteer');

(async () => 
    const browser = await puppeteer.launch();
    const browserWSEndpoint = browser.wsEndpoint();
    browser.disconnect(); // Disconnect from the browser, but don't close it
    console.log(browserWSEndpoint); // Communicate the Websocket URL to the client-side
    // example output: ws://127.0.0.1:55620/devtools/browser/e62ec4c8-1f05-42a1-86ce-7b8dd3403f91
)();

现在您可以通过客户端的puppeteer bundle 从客户端控制浏览器(在服务器上运行)。在这种情况下,您现在可以通过puppeteer.connect 连接到浏览器并以这种方式截屏。

我强烈建议使用选项 1,因为在选项 2 中,您将正在运行的浏览器完全暴露给客户端。即使使用选项 1,您仍然需要处理用户输入验证、超时、导航错误等。

【讨论】:

我继续执行选项 1。我得到了它的工作!谢谢!我不认为像为浏览器截屏这样简单的事情需要很多步骤。我还安装了 cors 库,只是为了解决我遇到的 cors 错误。 @Thomas Dondorf 我尝试了选项 2,但我收到一条错误消息,提示 Puppeteer 包无法解析“ws”包。这是一个类似问题的链接:github.com/puppeteer/puppeteer/issues/5219。帮忙? 这就是我一直在寻找的,非常简单且很好的解释。谢谢@thomas @Thomas Dondorf server.js 可以处理多少流量? 如果我的 react 应用程序在受保护的环境中运行(说只有在登录用户可以查看应用程序之后)并且我想截取 react 应用程序内特定区域的屏幕截图(比如样式富文本编辑器中的文本)。在这种情况下,我不能简单地将 url 提供给 puppetetteer,可能的解决方案是什么?试过 html2canvas 但它不会给出准确的输出,

以上是关于如何让 Puppeteer 在客户端使用 ReactJS 应用程序的主要内容,如果未能解决你的问题,请参考以下文章

Chrome Headless puppeteer CPU 过多

Node.js:如何重启 Tor 客户端

Puppeteer之大屏批量截图

使用Puppeteer将Node悬停在Node.js中的element和getComputedStyle上?

使用 puppeteer 生成 PDF 而不保存

如何使用 webpack 捆绑 puppeteer 进行生产部署?