這篇文章是關于什麼的?
您想讓使用者能夠通過您的系統浏覽網頁并感覺它是一個真正的浏覽器。
我為什麼創作這篇文章?
很長一段時間以來,我都試圖建立一種方法來讓會員通過一些網頁并填寫他們的詳細資訊。我搜尋了許多可以做到這一點的開源庫,但一無所獲。是以我決定自己實作它。
我們要怎麼做?
對于本文,我将使用 Puppeteer 和 ReactJS。
Puppeteer 是一個 Node.js 庫,可自動執行多種浏覽器操作,例如表單送出、抓取單頁應用程式、UI 測試,尤其是生成網頁的螢幕截圖和 PDF 版本。
我們将使用 Puppeteer 打開一個網頁,向用戶端 (React) 發送每個幀的螢幕截圖,并通過單擊圖像将操作反映給 Puppeteer。首先,讓我們設定項目環境。
Novu - 第一個開源通知基礎設施
隻是關于我們的快速背景。Novu 是第一個開源通知基礎設施。我們基本上幫助管理所有産品通知。它可以是應用内(開發社群中的鈴铛圖示 - Websockets)、電子郵件、短信等。
如果你能給我們一顆星,我會非常高興!它将幫助我每周發表更多文章
https://github.com/novuhq/novu
新的
如何使用 Socket.io 和 React.js 建立實時連接配接
在這裡,我們将為螢幕共享應用程式設定項目環境。您還将學習如何将 Socket.io 添加到 React 和 Node.js 應用程式并連接配接兩個開發伺服器以通過 Socket.io 進行實時通信。
建立項目檔案夾,其中包含兩個名為 client 和 server 的子檔案夾。
mkdir screen-sharing-app
cd screen-sharing-app
mkdir client server
通過終端導航到用戶端檔案夾并建立一個新的 React.js 項目。
cd client
npx create-react-app ./
安裝 Socket.io 用戶端 API 和 React Router。 React Router 是一個 JavaScript 庫,它使我們能夠在 React 應用程式的頁面之間導航。
npm install socket.io-client react-router-dom
從 React 應用程式中删除備援檔案,例如 logo 和測試檔案,并更新App.js檔案以顯示 Hello World,如下所示。
function App() {
return (
<div>
<p>Hello World!</p>
</div>
);
}
export default App;
導航到伺服器檔案夾并建立一個package.json檔案。
cd server & npm init -y
安裝 Express.js、CORS、Nodemon 和 Socket.io 伺服器 API。
Express.js 是一個快速、簡約的架構,它為在 Node.js 中建構 Web 應用程式提供了多種功能。 CORS 是一個允許不同域之間通信的 Node.js 包。
Nodemon 是一個 Node.js 工具,它在檢測到檔案更改後會自動重新開機伺服器,而 Socket.io 允許我們在伺服器上配置實時連接配接。
npm install express cors nodemon socket.io
建立一個index.js檔案 - Web 伺服器的入口點。
touch index.js
使用 Express.js 設定一個簡單的 Node.js 伺服器。當您http://localhost:4000/api在浏覽器中通路時,下面的代碼片段會傳回一個 JSON 對象。
//index.js
const express = require("express");
const app = express();
const PORT = 4000;
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.get("/api", (req, res) => {
res.json({
message: "Hello world",
});
});
app.listen(PORT, () => {
console.log(`Server listening on ${PORT}`);
});
導入 HTTP 和 CORS 庫以允許在用戶端和伺服器域之間傳輸資料。
const express = require("express");
const app = express();
const PORT = 4000;
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
//New imports
const http = require("http").Server(app);
const cors = require("cors");
app.use(cors());
app.get("/api", (req, res) => {
res.json({
message: "Hello world",
});
});
http.listen(PORT, () => {
console.log(`Server listening on ${PORT}`);
});
接下來,将 Socket.io 添加到項目中以建立實時連接配接。在app.get()塊之前,複制下面的代碼。接下來,将 Socket.io 添加到項目中以建立實時連接配接。在app.get()塊之前,複制下面的代碼。
//New imports
.....
const socketIO = require('socket.io')(http, {
cors: {
origin: "http://localhost:3000"
}
});
//Add this before the app.get() block
socketIO.on('connection', (socket) => {
console.log(`⚡: ${socket.id} user just connected!`);
socket.on('disconnect', () => {
console.log(': A user disconnected');
});
});
從上面的代碼片段中,該socket.io("connection")函數與 React 應用程式建立連接配接,然後為每個套接字建立一個唯一的 ID,并在使用者通路網頁時将 ID 記錄到控制台。
當您重新整理或關閉網頁時,套接字會觸發斷開連接配接事件,表明使用者已從套接字斷開連接配接。
通過将 start 指令添加到package.json檔案中的腳本清單來配置 Nodemon。下面的代碼片段使用 Nodemon 啟動伺服器。
//In server/package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon index.js"
},
您現在可以使用以下指令使用 Nodemon 運作伺服器。
npm start
建構使用者界面
在這裡,我們将建立一個簡單的使用者界面來示範互動式螢幕共享功能。
導航到client/src并建立一個元件檔案夾,其中包含Home.js一個名為Modal.js.
cd client/src
mkdir components
touch Home.js Modal.js
更新App.js檔案以呈現新建立的 Home 元件。
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Home from "./components/Home";
const App = () => {
return (
<BrowserRouter>
<Routes>
<Route path='/' element={<Home />} />
</Routes>
</BrowserRouter>
);
};
export default App;
導航到src/index.css檔案并複制下面的代碼。它包含樣式化此項目所需的所有 CSS。
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap");
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
"Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
font-family: "Space Grotesk", sans-serif;
box-sizing: border-box;
}
.home__container {
display: flex;
min-height: 55vh;
width: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
}
.home__container h2 {
margin-bottom: 30px;
}
.createChannelBtn {
padding: 15px;
width: 200px;
cursor: pointer;
font-size: 16px;
background-color: #277bc0;
color: #fff;
border: none;
outline: none;
margin-right: 15px;
margin-top: 30px;
}
.createChannelBtn:hover {
background-color: #fff;
border: 1px solid #277bc0;
color: #277bc0;
}
.form {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin-bottom: 30px;
}
.form__input {
width: 70%;
padding: 10px 15px;
margin: 10px 0;
}
.popup {
width: 80%;
height: 500px;
background: black;
border-radius: 20px;
padding: 20px;
overflow: auto;
}
.popup-ref {
background: white;
width: 100%;
height: 100%;
position: relative;
}
.popup-ref img {
top: 0;
position: sticky;
width: 100%;
}
@media screen and (max-width: 768px) {
.login__form {
width: 100%;
}
}
将下面的代碼複制到Home.js. 它為 URL、送出按鈕和 Modal 元件呈現表單輸入。
import React, { useCallback, useState } from "react";
import Modal from "./Modal";
const Home = () => {
const [url, setURL] = useState("");
const [show, setShow] = useState(false);
const handleCreateChannel = useCallback(() => {
setShow(true);
}, []);
return (
<div>
<div className='home__container'>
<h2>URL</h2>
<form className='form'>
<label>Provide a URL</label>
<input
type='url'
name='url'
id='url'
className='form__input'
required
value={url}
onChange={(e) => setURL(e.target.value)}
/>
</form>
{show && <Modal url={url} />}
<button className='createChannelBtn' onClick={handleCreateChannel}>
BROWSE
</button>
</div>
</div>
);
};
export default Home;
将表示截屏的圖像添加到Modal.js檔案并導入 Socket.io 庫。
import { useState } from "react";
import socketIO from "socket.io-client";
const socket = socketIO.connect("http://localhost:4000");
const Modal = ({ url }) => {
const [image, setImage] = useState("");
return (
<div className='popup'>
<div className='popup-ref'>{image && <img src={image} alt='' />}</div>
</div>
);
};
export default Modal;
啟動 React.js 伺服器。
npm start
檢查伺服器運作的終端;React.js 用戶端的 ID 應該出現在終端上。
恭喜,我們現在可以從應用程式 UI 開始與 Socket.io 伺服器通信了。
使用 Puppeteer 和 Chrome DevTools 協定截屏
在本節中,您将學習如何使用 Puppeteer 和Chrome DevTools 協定對網頁進行自動截圖 。與 Puppeteer 提供的正常螢幕截圖功能不同,Chrome 的 API 建立非常快的螢幕截圖,不會減慢 Puppeteer 和您的運作時,因為它是異步的。
導航到伺服器檔案夾并安裝 Puppeteer。
cd server
npm install puppeteer
更新Modal.js檔案以将使用者提供的網頁的 URL 發送到 Node.js 伺服器。
import { useState, useEffect } from "react";
import socketIO from "socket.io-client";
const socket = socketIO.connect("http://localhost:4000");
const Modal = ({ url }) => {
const [image, setImage] = useState("");
useEffect(() => {
socket.emit("browse", {
url,
});
}, [url]);
return (
<div className='popup'>
<div className='popup-ref'>{image && <img src={image} alt='' />}</div>
</div>
);
};
export default Modal;
browse在後端伺服器上為事件建立一個偵聽器。
socketIO.on("connection", (socket) => {
console.log(`⚡: ${socket.id} user just connected!`);
socket.on("browse", async ({ url }) => {
console.log("Here is the URL >>>> ", url);
});
socket.on("disconnect", () => {
socket.disconnect();
console.log(": A user disconnected");
});
});
由于我們已經能夠從 React 應用程式收集 URL,讓我們使用 Puppeteer 和 Chrome DevTools 協定建立螢幕截圖。
建立一個screen.shooter.js檔案并複制以下代碼:
const { join } = require("path");
const fs = require("fs").promises;
const emptyFunction = async () => {};
const defaultAfterWritingNewFile = async (filename) =>
console.log(`${filename} was written`);
class PuppeteerMassScreenshots {
/*
page - represents the web page
socket - Socket.io
options - Chrome DevTools configurations
*/
async init(page, socket, options = {}) {
const runOptions = {
// Their values must be asynchronous codes
beforeWritingImageFile: emptyFunction,
afterWritingImageFile: defaultAfterWritingNewFile,
beforeAck: emptyFunction,
afterAck: emptyFunction,
...options,
};
this.socket = socket;
this.page = page;
// CDPSession instance is used to talk raw Chrome Devtools Protocol
this.client = await this.page.target().createCDPSession();
this.canScreenshot = true;
// The frameObject parameter contains the compressed image data
// requested by the Page.startScreencast.
this.client.on("Page.screencastFrame", async (frameObject) => {
if (this.canScreenshot) {
await runOptions.beforeWritingImageFile();
const filename = await this.writeImageFilename(frameObject.data);
await runOptions.afterWritingImageFile(filename);
try {
await runOptions.beforeAck();
/* acknowledges that a screencast frame (image) has been received by the frontend.
The sessionId - represents the frame number
*/
await this.client.send("Page.screencastFrameAck", {
sessionId: frameObject.sessionId,
});
await runOptions.afterAck();
} catch (e) {
this.canScreenshot = false;
}
}
});
}
async writeImageFilename(data) {
const fullHeight = await this.page.evaluate(() => {
return Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight,
document.body.offsetHeight,
document.documentElement.offsetHeight,
document.body.clientHeight,
document.documentElement.clientHeight
);
});
//Sends an event containing the image and its full height
return this.socket.emit("image", { img: data, fullHeight });
}
/*
The startOptions specify the properties of the screencast
format - the file type (Allowed fomats: 'jpeg' or 'png')
quality - sets the image quality (default is 100)
everyNthFrame - specifies the number of frames to ignore before taking the next screenshots. (The more frames we ignore, the less screenshots we will have)
*/
async start(options = {}) {
const startOptions = {
format: "jpeg",
quality: 10,
everyNthFrame: 1,
...options,
};
try {
await this.client?.send("Page.startScreencast", startOptions);
} catch (err) {}
}
/*
Learn more here :
https://github.com/shaynet10/puppeteer-mass-screenshots/blob/main/index.js
*/
async stop() {
try {
await this.client?.send("Page.stopScreencast");
} catch (err) {}
}
}
module.exports = PuppeteerMassScreenshots;
從上面的代碼片段:
該runOptions對象包含四個值。beforeWritingImageFile并且afterWritingImageFile必須包含在将圖像發送到用戶端之前和之後運作的異步函數。
beforeAck并将afterAck發送到浏覽器的确認表示為顯示已收到圖像的異步代碼。
該writeImageFilename函數計算截屏的完整高度,并将其與截屏圖像一起發送到 React 應用程式。
建立一個執行個體PuppeteerMassScreenshots并更新server/index.js檔案以擷取螢幕截圖。
// Add the following imports
const puppeteer = require("puppeteer");
const PuppeteerMassScreenshots = require("./screen.shooter");
socketIO.on("connection", (socket) => {
console.log(`⚡: ${socket.id} user just connected!`);
socket.on("browse", async ({ url }) => {
const browser = await puppeteer.launch({
headless: true,
});
// creates an incognito browser context
const context = await browser.createIncognitoBrowserContext();
// creates a new page in a pristine context.
const page = await context.newPage();
await page.setViewport({
width: 1255,
height: 800,
});
// Fetches the web page
await page.goto(url);
// Instance of PuppeteerMassScreenshots takes the screenshots
const screenshots = new PuppeteerMassScreenshots();
await screenshots.init(page, socket);
await screenshots.start();
});
socket.on("disconnect", () => {
socket.disconnect();
console.log(": A user disconnected");
});
});
更新Modal.js檔案以偵聽來自伺服器的截屏圖像。
import { useState, useEffect } from "react";
import socketIO from "socket.io-client";
const socket = socketIO.connect("http://localhost:4000");
const Modal = ({ url }) => {
const [image, setImage] = useState("");
const [fullHeight, setFullHeight] = useState("");
useEffect(() => {
socket.emit("browse", {
url,
});
/*
Listens for the images and full height
from the PuppeteerMassScreenshots.
The image is also converted to a readable file.
*/
socket.on("image", ({ img, fullHeight }) => {
setImage("data:image/jpeg;base64," + img);
setFullHeight(fullHeight);
});
}, [url]);
return (
<div className='popup'>
<div className='popup-ref' style={{ height: fullHeight }}>
{image && <img src={image} alt='' />}
</div>
</div>
);
};
export default Modal;
恭喜! 我們已經能夠在 React 應用程式中顯示螢幕截圖。在下一節中,我将指導您使截屏圖像具有互動性。
使螢幕截圖具有互動性
在這裡,您将學習如何使截屏視訊完全互動,使其表現得像浏覽器視窗并響應滑鼠滾動和移動事件。
對光标的單擊和移動事件作出反應。
将下面的代碼複制到 Modal 元件中。
const mouseMove = useCallback((event) => {
const position = event.currentTarget.getBoundingClientRect();
const widthChange = 1255 / position.width;
const heightChange = 800 / position.height;
socket.emit("mouseMove", {
x: widthChange * (event.pageX - position.left),
y:
heightChange *
(event.pageY - position.top - document.documentElement.scrollTop),
});
}, []);
const mouseClick = useCallback((event) => {
const position = event.currentTarget.getBoundingClientRect();
const widthChange = 1255 / position.width;
const heightChange = 800 / position.height;
socket.emit("mouseClick", {
x: widthChange * (event.pageX - position.left),
y:
heightChange *
(event.pageY - position.top - document.documentElement.scrollTop),
});
}, []);
從上面的代碼片段:
[event.currentTarget.getBoundingClient()](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect)傳回一個對象,其中包含有關截屏視訊相對于視口的大小和位置的資訊。
event.pageX - 傳回滑鼠指針的位置;相對于文檔的左邊緣。
mouseClick然後,計算光标的位置并通過andmouseMove事件發送到後端。
在後端為這兩個事件建立一個偵聽器。
socket.on("browse", async ({ url }) => {
const browser = await puppeteer.launch({
headless: true,
});
const context = await browser.createIncognitoBrowserContext();
const page = await context.newPage();
await page.setViewport({
width: 1255,
height: 800,
});
await page.goto(url);
const screenshots = new PuppeteerMassScreenshots();
await screenshots.init(page, socket);
await screenshots.start();
socket.on("mouseMove", async ({ x, y }) => {
try {
//sets the cursor the position with Puppeteer
await page.mouse.move(x, y);
/*
This function runs within the page's context,
calculates the element position from the view point
and returns the CSS style for the element.
*/
const cur = await page.evaluate(
(p) => {
const elementFromPoint = document.elementFromPoint(p.x, p.y);
return window
.getComputedStyle(elementFromPoint, null)
.getPropertyValue("cursor");
},
{ x, y }
);
// sends the CSS styling to the frontend
socket.emit("cursor", cur);
} catch (err) {}
});
// Listens for the exact position the user clicked
// and set the move to that position.
socket.on("mouseClick", async ({ x, y }) => {
try {
await page.mouse.click(x, y);
} catch (err) {}
});
});
監聽cursor事件并将 CSS 樣式添加到螢幕截圖容器中。
import { useCallback, useEffect, useRef, useState } from "react";
import socketIO from "socket.io-client";
const socket = socketIO.connect("http://localhost:4000");
const Modal = ({ url }) => {
const ref = useRef(null);
const [image, setImage] = useState("");
const [cursor, setCursor] = useState("");
const [fullHeight, setFullHeight] = useState("");
useEffect(() => {
//...other functions
// Listens to the cursor event
socket.on("cursor", (cur) => {
setCursor(cur);
});
}, [url]);
//...other event emitters
return (
<div className='popup'>
<div
ref={ref}
className='popup-ref'
style={{ cursor, height: fullHeight }} // cursor is added
>
{image && (
<img
src={image}
onMouseMove={mouseMove}
onClick={mouseClick}
alt=''
/>
)}
</div>
</div>
);
};
export default Modal;
動圖
例子
響應滾動事件
在這裡,我将指導您使截屏視訊可滾動以檢視所有網頁的内容。
建立一個onScroll函數來測量從視口頂部到截屏容器的距離并将其發送到後端。
const Modal = ({ url }) => {
//...other functions
const mouseScroll = useCallback((event) => {
const position = event.currentTarget.scrollTop;
socket.emit("scroll", {
position,
});
}, []);
return (
<div className='popup' onScroll={mouseScroll}>
<div
ref={ref}
className='popup-ref'
style={{ cursor, height: fullHeight }}
>
{image && (
<img
src={image}
onMouseMove={mouseMove}
onClick={mouseClick}
alt=''
/>
)}
</div>
</div>
);
};
為事件建立一個偵聽器以根據文檔的坐标滾動頁面。
socket.on("browse", async ({ url }) => {
//....other functions
socket.on("scroll", ({ position }) => {
//scrolls the page
page.evaluate((top) => {
window.scrollTo({ top });
}, position);
});
});
恭喜! 我們現在可以滾動浏覽截屏視訊并與網頁内容進行互動。
滾動
結論
到目前為止,您已經學習了如何使用 React.js 和Socket.io建立實時連接配接, 使用 Puppeteer 和 Chrome DevTools 協定截取網頁,并使它們具有互動性。
本文示範了您可以使用 Puppeteer 建構的内容。您還可以生成 PDF 頁面、自動送出表單、UI 測試、測試 chrome 擴充等等。随意探索 文檔。
本教程的源代碼可在此處獲得: https ://github.com/novuhq/blog/tree/main/screen-sharing-with-puppeteer 。
PS如果你能給我們一顆星,我會非常高興!它将幫助我每周發表更多文章
https://github.com/novuhq/novu
謝謝
感謝您的閱讀!