使用 Puppeteer 和 React 构建交互式屏幕共享应用程序
使用 Puppeteer 和 React 构建交互式屏幕共享应用程序
这篇文章是关于什么的?
您想让用户能够通过您的系统浏览网页并感觉它是一个真正的浏览器。
我为什么创作这篇文章?
很长一段时间以来,我都试图创建一种方法来让会员通过一些网页并填写他们的详细信息。我搜索了许多可以做到这一点的开源库,但一无所获。所以我决定自己实现它。
我们要怎么做?
对于本文,我将使用 Puppeteer 和 ReactJS。
傀儡师 是一个 Node.js 库,可自动执行多种浏览器操作,例如表单提交、抓取单页应用程序、UI 测试,尤其是生成网页的屏幕截图和 PDF 版本。
我们将使用 Puppeteer 打开一个网页,向客户端 (React) 发送每个帧的屏幕截图,并通过单击图像将操作反映给 Puppeteer。首先,让我们设置项目环境。
Novu——第一个开源通知基础设施
只是关于我们的快速背景。 Novu 是第一个开源的 通知基础设施 .我们基本上帮助管理所有产品通知。有可能 应用内 (你在开发社区中的铃铛图标—— 网络套接字 )、电子邮件、短信等。
如果你能给我们一颗星,我会非常高兴!它将帮助我每周发表更多文章
https://github.com/novuhq/novu
如何使用 Socket.io 和 React.js 创建实时连接
在这里,我们将为屏幕共享应用程序设置项目环境。您还将学习如何将 Socket.io 添加到 React 和 Node.js 应用程序并连接两个开发服务器以通过 Socket.io 进行实时通信。
创建项目文件夹,其中包含两个名为 client 和 server 的子文件夹。
mkdir 屏幕共享应用程序
cd 屏幕共享应用程序
mkdir 客户端服务器
通过终端导航到客户端文件夹并创建一个新的 React.js 项目。
光盘客户端
npx 创建反应应用程序 ./
安装 Socket.io 客户端 API 和 React Router。 反应路由器 是一个 JavaScript 库,它使我们能够在 React 应用程序的页面之间导航。
npm install socket.io-client react-router-dom
删除 React 应用中的 logo 和测试文件等冗余文件,并更新 应用程序.js
文件以显示 Hello World,如下所示。
函数应用程序(){
返回 (
<div>
<p>你好世界!</p>
</div>
);
}
导出默认应用程序;
导航到服务器文件夹并创建一个 包.json
文件。
cd 服务器 & npm init -y
安装 Express.js、CORS、Nodemon 和 Socket.io 服务器 API。
Express.js 是一个快速、极简的框架,它为在 Node.js 中构建 Web 应用程序提供了多种功能。 CORS 是一个允许不同域之间通信的 Node.js 包。
节点兽 是一个 Node.js 工具,它会在检测到文件更改后自动重启服务器,并且 套接字.io 允许我们在服务器上配置实时连接。
npm install express cors nodemon socket.io
创建一个 index.js
文件 - Web 服务器的入口点。
触摸 index.js
使用 Express.js 设置一个简单的 Node.js 服务器。当您访问它们时,下面的代码片段会返回一个 JSON 对象 http://localhost:4000/api
在您的浏览器中。
//index.js
常量表达 = 要求(“表达”);
常量应用程序 = 快递();
常量端口 = 4000; app.use(express.urlencoded({extended: true }));
app.use(express.json()); app.get("/api", (req, res) => {
res.json({
消息:“你好世界”,
});
}); app.listen(PORT, () => {
console.log(`服务器监听 ${PORT}`);
});
导入 HTTP 和 CORS 库以允许在客户端和服务器域之间传输数据。
常量表达 = 要求(“表达”);
常量应用程序 = 快递();
常量端口 = 4000; app.use(express.urlencoded({extended: true }));
app.use(express.json()); //新的导入
const http = require("http").Server(app);
常量 cors = 要求(“cors”); app.use(cors()); app.get("/api", (req, res) => {
res.json({
消息:“你好世界”,
});
}); http.listen(PORT, () => {
console.log(`服务器监听 ${PORT}`);
});
接下来,将 Socket.io 添加到项目中以创建实时连接。之前 应用程序.get()
块,复制下面的代码。接下来,将 Socket.io 添加到项目中以创建实时连接。之前 应用程序.get()
块,复制下面的代码。
//新的导入
......
const socketIO = require('socket.io')(http, {
科尔斯:{
来源:“http://localhost:3000”
}
}); //在 app.get() 块之前添加这个
socketIO.on('连接', (socket) => {
console.log(`⚡: ${socket.id} 用户刚刚连接!`);
socket.on('断开连接', () => {
console.log(': 一个用户断开连接');
});
});
从上面的代码片段中, socket.io("连接")
函数建立与 React 应用程序的连接,然后为每个套接字创建一个唯一 ID,并在用户访问网页时将 ID 记录到控制台。
当您刷新或关闭网页时,套接字会触发断开连接事件,表明用户已从套接字断开连接。
通过将启动命令添加到中的脚本列表来配置 Nodemon 包.json
文件。下面的代码片段使用 Nodemon 启动服务器。
//在服务器/package.json “脚本”:{
"test": "echo \"错误:没有指定测试\" && exit 1",
“开始”:“nodemon index.js”
},
您现在可以使用以下命令使用 Nodemon 运行服务器。
npm 开始
构建用户界面
在这里,我们将创建一个简单的用户界面来演示交互式屏幕共享功能。
导航到 客户端/源
并创建一个包含 主页.js
和一个名为的子组件 模态.js
.
cd 客户端/src
mkdir 组件
触摸 Home.js Modal.js
更新 应用程序.js
文件以呈现新创建的 Home 组件。
从“反应”导入反应;
从“react-router-dom”导入 { BrowserRouter, Route, Routes };
从“./components/Home”导入主页; 常量应用 = () => {
返回 (
<BrowserRouter>
<Routes>
<Route path='/' element={<Home /> } />
</Routes>
</BrowserRouter>
);
}; 导出默认应用程序;
导航到 src/index.css
文件并复制下面的代码。它包含样式化此项目所需的所有 CSS。
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:[[email protected]](/cdn-cgi/l/email-protection);400;500;600;700&display=swap"); 身体 {
边距:0;
填充:0;
字体系列:-apple-system、BlinkMacSystemFont、“Segoe UI”、“Roboto”、
“氧气”、“Ubuntu”、“Cantarell”、“Fira Sans”、“Droid Sans”、
“Helvetica Neue”,无衬线;
-webkit-font-smoothing:抗锯齿;
-moz-osx-font-smoothing:灰度;
}
* {
font-family: "Space Grotesk", sans-serif;
box-sizing:边框框;
}
.home__container {
显示:弯曲;
最小高度:55vh;
宽度:100%;
弹性方向:列;
对齐项目:居中;
证明内容:中心;
}
.home__container h2 {
边距底部:30px;
}
.createChannelBtn {
填充:15px;
宽度:200px;
光标:指针;
字体大小:16px;
背景颜色:#277bc0;
颜色:#fff;
边框:无;
大纲:无;
右边距:15px;
边距顶部:30px;
}
.createChannelBtn:hover {
背景颜色:#fff;
边框:1px 实心#277bc0;
颜色:#277bc0;
}
。形式 {
宽度:100%;
显示:弯曲;
对齐项目:居中;
证明内容:中心;
弹性方向:列;
边距底部:30px;
}
.form__input {
宽度:70%;
填充:10px 15px;
边距:10px 0;
}
。弹出 {
宽度:80%;
高度:500px;
背景:黑色;
边框半径:20px;
填充:20px;
溢出:自动;
}
.popup-ref {
背景:白色;
宽度:100%;
高度:100%;
位置:相对;
}
.popup-ref img {
顶部:0;
位置:粘性;
宽度:100%;
}
@media screen and (max-width: 768px) {
。登录表单 {
宽度:100%;
}
}
将下面的代码复制到 主页.js
.它为 URL、提交按钮和 Modal 组件呈现表单输入。
从“反应”导入反应,{ useCallback,useState};
从“./Modal”导入模态; 常量首页 = () => {
const [url, setURL] = useState("");
const [显示,setShow] = useState(false);
const handleCreateChannel = useCallback(() => {
设置显示(真);
}, []); 返回 (
<div>
<div className='home__container'>
<h2>网址</h2>
<form className='form'>
<label>提供网址</label>
<input
类型='网址'
名称='网址'
id='网址'
className='form__input'
必需的
价值={网址}
onChange={(e) => setURL(e.target.value)}
/>
</form>
{节目 &&<Modal url={url} /> }
<button className='createChannelBtn' onClick={handleCreateChannel}>
浏览
</button>
</div>
</div>
);
}; 导出默认主页;
将代表截屏视频的图像添加到 模态.js
文件并导入 Socket.io 库。
从“反应”导入 { useState };
从“socket.io-client”导入socketIO;
const socket = socketIO.connect("http://localhost:4000"); const 模态 = ({ url }) => {
const [image, setImage] = useState("");
返回 (
<div className='popup'>
<div className='popup-ref'>{图片 &&<img src={image} alt='' /> }</div>
</div>
);
}; 导出默认模态;
启动 React.js 服务器。
npm 开始
检查服务器运行的终端; React.js 客户端的 ID 应该出现在终端上。
恭喜,我们现在可以从应用程序 UI 开始与 Socket.io 服务器通信了。
使用 Puppeteer 和 Chrome DevTools 协议截屏
在本节中,您将学习如何使用 Puppeteer 和 Chrome 开发者工具协议 .与 Puppeteer 提供的常规屏幕截图功能不同,Chrome 的 API 创建非常快的屏幕截图,不会减慢 Puppeteer 和您的运行时,因为它是异步的。
导航到服务器文件夹并安装 Puppeteer。
光盘服务器
npm 安装 puppeteer
更新 模态.js
文件将用户提供的网页的 URL 发送到 Node.js 服务器。
导入 { useState, useEffect } from "react";
从“socket.io-client”导入socketIO;
const socket = socketIO.connect("http://localhost:4000"); const 模态 = ({ url }) => {
const [image, setImage] = useState(""); 使用效果(()=> {
socket.emit("浏览", {
网址,
});
}, [网址]); 返回 (
<div className='popup'>
<div className='popup-ref'>{图片 &&<img src={image} alt='' /> }</div>
</div>
);
}; 导出默认模态;
为 浏览
后端服务器上的事件。
socketIO.on("连接", (socket) => {
console.log(`⚡: ${socket.id} 用户刚刚连接!`); socket.on("浏览", async ({ url }) => {
console.log("这里是网址 >>>> ", url);
}); socket.on("断开连接", () => {
socket.disconnect();
console.log(": 一个用户断开连接");
});
});
由于我们已经能够从 React 应用程序收集 URL,让我们使用 Puppeteer 和 Chrome DevTools 协议创建屏幕截图。
创建一个 screen.shooter.js
文件并复制以下代码:
常量 { 加入 } = 要求(“路径”); const fs = require("fs").promises;
const emptyFunction = async () => {};
const defaultAfterWritingNewFile = async (文件名) =>
console.log(`${filename} 已写入`); 类 PuppeteerMassScreenshots {
/*
page - 代表网页
套接字 - Socket.io
选项 - Chrome DevTools 配置
*/
异步初始化(页面,套接字,选项 = {}){
常量运行选项 = {
//它们的值必须是异步代码
beforeWritingImageFile: emptyFunction,
afterWritingImageFile:默认AfterWritingNewFile,
beforeAck:空函数,
afterAck:空函数,
...选项,
};
this.socket = 套接字;
this.page = 页; // CDPSession 实例用于讨论原始 Chrome Devtools 协议
this.client = 等待 this.page.target().createCDPSession();
this.canScreenshot = true; // frameObject参数包含压缩后的图片数据
// Page.startScreencast 请求。
this.client.on("Page.screencastFrame", async (frameObject) => {
如果(this.canScreenshot){
等待 runOptions.beforeWritingImageFile();
常量文件名 = 等待 this.writeImageFilename(frameObject.data);
等待 runOptions.afterWritingImageFile(文件名); 尝试 {
等待 runOptions.beforeAck();
/* 确认前端已收到截屏帧(图像)。
sessionId - 代表帧号
*/
等待 this.client.send("Page.screencastFrameAck", {
sessionId:frameObject.sessionId,
});
等待 runOptions.afterAck();
} 抓住 (e) {
this.canScreenshot = false;
}
}
});
} 异步写入图像文件名(数据){
const fullHeight = await this.page.evaluate(() => {
返回数学.max(
document.body.scrollHeight,
document.documentElement.scrollHeight,
document.body.offsetHeight,
document.documentElement.offsetHeight,
document.body.clientHeight,
document.documentElement.clientHeight
);
});
//发送一个包含图像及其全高的事件
return this.socket.emit("image", { img: data, fullHeight });
}
/*
startOptions 指定截屏视频的属性
格式 - 文件类型(允许的格式:'jpeg' 或 'png')
质量 - 设置图像质量(默认为 100)
everyNthFrame - 指定在拍摄下一个屏幕截图之前要忽略的帧数。 (我们忽略的帧越多,截图就越少)
*/
异步启动(选项 = {}){
常量 startOptions = {
格式:“jpeg”,
质量:10,
每NthFrame:1,
...选项,
};
尝试 {
await this.client?.send("Page.startScreencast", startOptions);
} 捕捉(错误){}
} /*
在这里了解更多:
https://github.com/shaynet10/puppeteer-mass-screenshots/blob/main/index.js
*/
异步停止(){
尝试 {
等待 this.client?.send("Page.stopScreencast");
} 捕捉(错误){}
}
} module.exports = PuppeteerMassScreenshots;
- 从上面的代码片段:
- 这
运行选项
对象包含四个值。beforeWritingImageFile
和afterWritingImageFile
必须包含在将图像发送到客户端之前和之后运行的异步函数。 确认前
和确认后
将发送到浏览器的确认表示为显示已接收到图像的异步代码。- 这
写图像文件名
函数计算截屏的完整高度,并将其与截屏图像一起发送到 React 应用程序。
创建一个实例 PuppeteerMassScreenshots
并更新 服务器/index.js
文件以获取屏幕截图。
// 添加以下导入
const puppeteer = require("puppeteer");
const PuppeteerMassScreenshots = require("./screen.shooter"); socketIO.on("连接", (socket) => {
console.log(`⚡: ${socket.id} 用户刚刚连接!`); socket.on("浏览", async ({ url }) => {
常量浏览器 = 等待 puppeteer.launch({
无头:真的,
});
// 创建一个隐身浏览器上下文
const context = await browser.createIncognitoBrowserContext();
// 在原始上下文中创建一个新页面。
const page = 等待 context.newPage();
等待 page.setViewport({
宽度:1255,
身高:800,
});
// 获取网页
等待 page.goto(url);
// PuppeteerMassScreenshots 实例截取截图
const screenshots = new PuppeteerMassScreenshots();
等待 screenshots.init(page, socket);
等待截图.start();
}); socket.on("断开连接", () => {
socket.disconnect();
console.log(": 一个用户断开连接");
});
});
更新 模态.js
文件以侦听来自服务器的截屏图像。
导入 { useState, useEffect } from "react";
从“socket.io-client”导入socketIO;
const socket = socketIO.connect("http://localhost:4000"); const 模态 = ({ url }) => {
const [image, setImage] = useState("");
const [fullHeight, setFullHeight] = useState(""); 使用效果(()=> {
socket.emit("浏览", {
网址,
}); /*
聆听图像和全高
来自 PuppeteerMassScreenshots。
图像也被转换为可读文件。
*/
socket.on("image", ({ img, fullHeight }) => {
setImage("数据:图像/jpeg;base64," + img);
setFullHeight(fullHeight);
});
}, [网址]); 返回 (
<div className='popup'>
<div className='popup-ref' style={{ height: fullHeight }}>
{图片 &&<img src={image} alt='' /> }
</div>
</div>
);
}; 导出默认模态;
恭喜! 我们已经能够在 React 应用程序中显示屏幕截图。在下一节中,我将指导您使截屏图像具有交互性。
使屏幕截图具有交互性
在这里,您将学习如何使截屏视频完全交互,使其表现得像浏览器窗口并响应鼠标滚动和移动事件。
对光标的单击和移动事件作出反应。
将下面的代码复制到 Modal 组件中。
const mouseMove = useCallback((事件) => {
常量位置 = event.currentTarget.getBoundingClientRect();
常量 widthChange = 1255 / position.width;
常量 heightChange = 800 / position.height; socket.emit("mouseMove", {
x: widthChange * (event.pageX - position.left),
是:
高度变化 *
(event.pageY - position.top - document.documentElement.scrollTop),
});
}, []); const mouseClick = useCallback((event) => {
常量位置 = event.currentTarget.getBoundingClientRect();
常量 widthChange = 1255 / position.width;
常量 heightChange = 800 / position.height;
socket.emit("mouseClick", {
x: widthChange * (event.pageX - position.left),
是:
高度变化 *
(event.pageY - position.top - document.documentElement.scrollTop),
});
}, []);
- 从上面的代码片段:
[event.currentTarget.getBoundingClient()](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect)
返回一个对象,其中包含有关截屏视频相对于视口的大小和位置的信息。- event.pageX — 返回鼠标指针的位置;相对于文档的左边缘。
- 然后,计算光标的位置并通过
鼠标点击
和鼠标移动
事件。
在后端为这两个事件创建一个侦听器。
socket.on("浏览", async ({ url }) => {
常量浏览器 = 等待 puppeteer.launch({
无头:真的,
});
const context = await browser.createIncognitoBrowserContext();
const page = 等待 context.newPage();
等待 page.setViewport({
宽度:1255,
身高:800,
});
等待 page.goto(url);
const screenshots = new PuppeteerMassScreenshots();
等待 screenshots.init(page, socket);
等待截图.start(); socket.on("mouseMove", async ({ x, y }) => {
尝试 {
//用Puppeteer设置光标的位置
等待 page.mouse.move(x, y);
/*
这个函数在页面的上下文中运行,
从视点计算元素位置
并返回元素的 CSS 样式。
*/
const cur = 等待 page.evaluate(
(p) => {
常量 elementFromPoint = document.elementFromPoint(px, py);
返回窗口
.getComputedStyle(elementFromPoint, null)
.getPropertyValue("光标");
},
{ x, y }
); // 将 CSS 样式发送到前端
socket.emit("光标", cur);
} 捕捉(错误){}
}); // 监听用户点击的确切位置
// 并将移动设置到该位置。
socket.on("mouseClick", async ({ x, y }) => {
尝试 {
等待 page.mouse.click(x, y);
} 捕捉(错误){}
});
});
听 光标
事件并将 CSS 样式添加到屏幕截图容器中。
导入 { useCallback, useEffect, useRef, useState } from "react";
从“socket.io-client”导入socketIO;
const socket = socketIO.connect("http://localhost:4000"); const 模态 = ({ url }) => {
常量 ref = useRef(null);
const [image, setImage] = useState("");
const [光标,setCursor] = useState("");
const [fullHeight, setFullHeight] = useState(""); 使用效果(()=> {
//...其他函数 // 监听光标事件
socket.on("光标", (cur) => {
setCursor(cur);
});
}, [网址]); //...其他事件发射器 返回 (
<div className='popup'>
<div
参考={参考}
className='popup-ref'
style={{ cursor, height: fullHeight }} // 添加光标
>
{图片 && (
<img
src={图像}
onMouseMove={鼠标移动}
onClick={鼠标点击}
alt=''
/>
)}
</div>
</div>
);
}; 导出默认模态;
响应滚动事件
在这里,我将指导您使截屏视频可滚动以查看所有网页的内容。
创建一个 onScroll
测量从视口顶部到截屏容器的距离并将其发送到后端的函数。
const 模态 = ({ url }) => {
//...其他函数 const mouseScroll = useCallback((事件) => {
常量位置 = event.currentTarget.scrollTop;
socket.emit("滚动", {
位置,
});
}, []); 返回 (
<div className='popup' onScroll={mouseScroll}>
<div
参考={参考}
className='popup-ref'
样式={{ 光标,高度:fullHeight }}
>
{图片 && (
<img
src={图像}
onMouseMove={鼠标移动}
onClick={鼠标点击}
alt=''
/>
)}
</div>
</div>
);
};
为事件创建一个侦听器以根据文档的坐标滚动页面。
socket.on("浏览", async ({ url }) => {
//....其他函数 socket.on("滚动", ({ 位置 }) => {
//滚动页面
page.evaluate((top) => {
window.scrollTo({ top });
}, 位置);
});
});
恭喜! 我们现在可以滚动浏览截屏视频并与网页内容进行交互。
结论
到目前为止,您已经学习了如何使用 React.js 建立实时连接,并且 套接字.io , 使用 Puppeteer 截取网页截图和 Chrome 开发者工具协议 ,并使它们具有交互性。
本文演示了您可以使用 Puppeteer 构建的内容。您还可以生成 PDF 页面、自动提交表单、UI 测试、测试 chrome 扩展等等。随意探索 文件 .
本教程的源代码可在此处获得: https://github.com/novuhq/blog/tree/main/screen-sharing-with-puppeteer .
附言 如果你能给我们一颗星,我会非常高兴!它将帮助我每周发表更多文章
https://github.com/novuhq/novu
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明