不多說,我們直奔主題。這篇文章教大家如何編寫一個視頻聊天應用,使已連接的兩用戶端能共享視頻和音頻。操作很簡單,非常適合JavaScript語言訓練——更準確地說是WebRTC技術和Node.js。
什么是WebRTC?
Web Real-Time Communications 網頁實時通信,簡稱WebRTC。WebRTC是一個HTML5規范,它允許用戶在瀏覽器之間直接進行實時通信,不需要任何第三方插件。WebRTC可用于多種情境(比如文件共享),但端對端實時音頻和視頻通信是其主要功能。本文將著重為大家介紹這兩項。
WebRTC所做的就是允許接入設備。你可以借WebRTC來實時使用麥克風、攝像頭和分享你的屏幕。
所以,WebRTC可以用最簡單的方式在網頁中實現音頻和視頻通信。
WebRTC JavaScript API
WebRTC說起來很復雜,它涉及到很多技術。但建立連接、通信和傳輸數據的操作是通過一套JS API來實現的,還比較簡單。其中主要的API包括:
RTCPeerConnection:創建和導航端對端連接。
RTCSessionDescription:描述連接(或潛在連接)的一端,以及它的配置方式。
navigator.getUserMedia:捕捉音頻和視頻。
為什么選擇Node.js?
若要在兩個或多個設備之間進行遠程連接,你就需要一個服務器。在這種情況下,你也需要一個處理實時通信的服務器。Node.js是為實時可擴展的應用而構建的。要開發自由數據交換的雙向連接應用程序,你可能會用到WebSockets,它允許在客戶端和服務器之間建立一個會話窗口。來自客戶端的請求會以循環的方式,更準確地說是事件循環進行處理,這時Node.js是我們很好的一個選擇,因為它采取 “非阻塞(non-blocking) “的方式來解決請求。這樣我們在這該過程中就能實現低延遲和高吞吐量。
如果你對開發微服務感興趣的話,一定要看看查看我們內含650多位微服務專家意見的2020年微服務狀態報告!
思路拓展:我們要創建的是什么?
我們會創建一個非常簡單的應用程序,它能讓我們將音頻和視頻流傳輸到連接的設備——一個基礎款視頻聊天應用程序。我們會用到的技術有:
Express庫,提供靜態文件,比如代表用戶界面(UI)的HTML文件;
socket.io庫,在兩個設備之間用WebSockets建立連接;
WebRTC,允許媒體設備(攝像頭和麥克風)在連接的設備之間傳輸音頻和視頻流。
實現視頻會話
我們要做的第一件事是給我們的應用程序提供一個作為UI的HTML文件。讓我們通過運行:npm init.js來初始化新的node.js項目。然后,我們需要通過運行:npm i -D typescript ts-node nodemon @types/express @types/socket.io安裝一些開發依賴項,運行:npm i express socket.io安裝生產依賴項。
之后我們就可以在package.json文件中定義腳本,來運行我們的項目了。
{
"scripts": {
"start": "ts-node src/index.ts",
"dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts"
},
"devDependencies": {
"@types/express": "^4.17.2",
"@types/socket.io": "^2.1.4",
"nodemon": "^1.19.4",
"ts-node": "^8.4.1",
"typescript": "^3.7.2"
},
"dependencies": {
"express": "^4.17.1",
"socket.io": "^2.3.0"
}
}
當我們運行npm run dev命令時,nodemon會監控src文件夾中每個以.ts結尾的文件有無任何變化。現在,我們要創建一個src文件夾。在這個文件夾中,我們會創建兩個typescript文件:index.ts和server.ts。
在server.ts中,我們會創建server類,并使其與express和socket.io一起工作。
import express, { Application } from "express";
import socketIO, { Server as SocketIOServer } from "socket.io";
import { createServer, Server as HTTPServer } from "http";
export class Server {
private httpServer: HTTPServer;
private app: Application;
private io: SocketIOServer;
private readonly DEFAULT_PORT = 5000;
constructor() {
this.initialize();
this.handleRoutes();
this.handleSocketConnection();
}
private initialize(): void {
this.app = express();
this.httpServer = createServer(this.app);
this.io = socketIO(this.httpServer);
}
private handleRoutes(): void {
this.app.get("/", (req, res) => {
res.send(`<h1>Hello World</h1>`);
});
}
private handleSocketConnection(): void {
this.io.on("connection", socket => {
console.log("Socket connected.");
});
}
public listen(callback: (port: number) => void): void {
this.httpServer.listen(this.DEFAULT_PORT, () =>
callback(this.DEFAULT_PORT)
);
}
}
為正常運行服務器,我們需要在index.ts文件中創建一個新的Server類實例并調用listen方法。
import { Server } from "./server";
const server = new Server();
server.listen(port => {
console.log(`Server is listening on http://localhost:${port}`);
});
現在,如果我們運行:npm run dev會看到下面這樣的情景:
當打開瀏覽器,輸入http://localhost:5000,我們應該注意到左上的 “Hello World “信息。
然后我們就可以在public/index.html中創建一個新的HTML文件了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Dogeller</title>
<link
href="https://fonts.googleapis.com/css?family=Montserrat:300,400,500,700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="./styles.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
</head>
<body>
<div class="container">
<header class="header">
<div class="logo-container">
<img src="./img/doge.png" alt="doge logo" class="logo-img" />
<h1 class="logo-text">
Doge<span class="logo-highlight">ller</span>
</h1>
</div>
</header>
<div class="content-container">
<div class="active-users-panel" id="active-user-container">
<h3 class="panel-title">Active Users:</h3>
</div>
<div class="video-chat-container">
<h2 class="talk-info" id="talking-with-info">
Select active user on the left menu.
</h2>
<div class="video-container">
<video autoplay class="remote-video" id="remote-video"></video>
<video autoplay muted class="local-video" id="local-video"></video>
</div>
</div>
</div>
</div>
<script src="./scripts/index.js"></script>
</body>
</html>
在這個新文件中,我們創建了兩個視頻元素:一個用于遠程視頻連接,另一個用于本地視頻。你可能已經注意到我們也在導入本地腳本了。現在我們就來創建一個新的文件夾“腳本”,并在這個目錄下創建index.js文件。至于樣式,你可以從GitHub庫中下載它們。
接下來你需要給瀏覽器提供index.html。首先,你需要告訴express你想提供哪些靜態文件。為了實現這一點,我們決定在Server類中實現一個新方法。
private configureApp(): void {
this.app.use(express.static(path.join(__dirname, "../public")));
}
不要忘記在initialize中調用configureApp。
private initialize(): void {
this.app = express();
this.httpServer = createServer(this.app);
this.io = socketIO(this.httpServer);
this.configureApp();
this.handleSocketConnection();
}
當你輸入http://localhost:5000后,你應該能看到你的index.html文件在運行。
下一步要實現的是允許攝像頭和視頻訪問并將其流式傳輸到local-video元素。要做到這一點,你需要打開public/scripts/index.js文件,并用以下方法實現它。
navigator.getUserMedia(
{ video: true, audio: true },
stream => {
const localVideo = document.getElementById("local-video");
if (localVideo) {
localVideo.srcObject = stream;
}
},
error => {
console.warn(error.message);
}
);
當回到瀏覽器時,界面會出現一個提示請求訪問你的媒體設備,在接受請求后,你電腦的攝像頭就開始工作了。
更多細節詳見A simple guide to concurrency in Node.js and a few traps that come with it。
如何處理socket連接?
接下來我們講講如何處理socket連接。我們需要將客戶端與服務器連接起來。為此,我們將使用socket.io。在public/scripts/index.js中,添加以下代碼:
this.io.on("connection", socket => {
const existingSocket = this.activeSockets.find(
existingSocket => existingSocket === socket.id
);
if (!existingSocket) {
this.activeSockets.push(socket.id);
socket.emit("update-user-list", {
users: this.activeSockets.filter(
existingSocket => existingSocket !== socket.id
)
});
socket.broadcast.emit("update-user-list", {
users: [socket.id]
});
}
}
頁面刷新后,電腦會彈出一條消息,顯示 “Socket已連接”
然后我們回到server.ts中,把已連接的socket存儲在內存中,這只是為了保留唯一連接。所以,我們需要在Server類中添加一個新的私有字段,如下:
private activeSockets: string[] = [];
然后我們需要在socket連接中檢查socket是否已經存在。如果不存在,把新的socket推送到內存中,并向已連接的用戶發送數據。
this.io.on("connection", socket => {
const existingSocket = this.activeSockets.find(
existingSocket => existingSocket === socket.id
);
if (!existingSocket) {
this.activeSockets.push(socket.id);
socket.emit("update-user-list", {
users: this.activeSockets.filter(
existingSocket => existingSocket !== socket.id
)
});
socket.broadcast.emit("update-user-list", {
users: [socket.id]
});
}
}
你還需要在socket斷開連接時及時響應,所以在socket連接中,你需要添加:
socket.on("disconnect", () => {
this.activeSockets = this.activeSockets.filter(
existingSocket => existingSocket !== socket.id
);
socket.broadcast.emit("remove-user", {
socketId: socket.id
});
});
客戶端(即public/scripts/index.js)這邊,你需要妥善處理那些信息:
socket.on("update-user-list", ({ users }) => {
updateUserList(users);
});
socket.on("remove-user", ({ socketId }) => {
const elToRemove = document.getElementById(socketId);
if (elToRemove) {
elToRemove.remove();
}
});
以下是 updateUserList 函數:
function updateUserList(socketIds) {
const activeUserContainer = document.getElementById("active-user-container");
socketIds.forEach(socketId => {
const alreadyExistingUser = document.getElementById(socketId);
if (!alreadyExistingUser) {
const userContainerEl = createUserItemContainer(socketId);
activeUserContainer.appendChild(userContainerEl);
}
});
}
以及createUserItemContainer函數:
function createUserItemContainer(socketId) {
const userContainerEl = document.createElement("div");
const usernameEl = document.createElement("p");
userContainerEl.setAttribute("class", "active-user");
userContainerEl.setAttribute("id", socketId);
usernameEl.setAttribute("class", "username");
usernameEl.innerHTML = `Socket: ${socketId}`;
userContainerEl.appendChild(usernameEl);
userContainerEl.addEventListener("click", () => {
unselectUsersFromList();
userContainerEl.setAttribute("class", "active-user active-user--selected");
const talkingWithInfo = document.getElementById("talking-with-info");
talkingWithInfo.innerHTML = `Talking with: "Socket: ${socketId}"`;
callUser(socketId);
});
return userContainerEl;
}
需要注意的是,我們給用戶容器元素添加了一個可以調用callUser函數的點擊監聽器——但現在,它可以是一個空的函數。接下來,當運行兩個瀏覽器窗口(其中一個作為私人窗口)時,你應該注意到你的Web應用程序中有兩個已經連接的socket。
點擊列表中的活躍用戶,這時我們需要調用callUser函數。但是在實現之前,你還需要在window對象中聲明兩個類。
const { RTCPeerConnection, RTCSessionDescription } = window;
我們會在callUser函數用到這兩個類:
async function callUser(socketId) {
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(new RTCSessionDescription(offer));
socket.emit("call-user", {
offer,
to: socketId
});
}
現在我們要創建一個本地請求并發送給選定的用戶。服務器會監聽一個叫做call-user的事件、攔截請求并將其轉發給選定的用戶。讓我們用server.ts來實現該操作:
socket.on("call-user", data => {
socket.to(data.to).emit("call-made", {
offer: data.offer,
socket: socket.id
});
});
對于客戶端,你需要就call-made事件作出調整:
socket.on("call-made", async data => {
await peerConnection.setRemoteDescription(
new RTCSessionDescription(data.offer)
);
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(new RTCSessionDescription(answer));
socket.emit("make-answer", {
answer,
to: data.socket
});
});
之后,在你從服務器得到的請求上設置一個遠程描述,并為這個請求創建一個答復。對于服務器端,你只需要將適當的數據傳遞給選定的用戶即可。然后我們再在server.ts里面添加一個監聽器。
socket.on("make-answer", data => {
socket.to(data.to).emit("answer-made", {
socket: socket.id,
answer: data.answer
});
});
對于客戶端,我們需要處理 answer-made 事件。
socket.on("answer-made", async data => {
await peerConnection.setRemoteDescription(
new RTCSessionDescription(data.answer)
);
if (!isAlreadyCalling) {
callUser(data.socket);
isAlreadyCalling = true;
}
});
我們可以使用標志isAlreadyCalling,它能幫助確保我們只需調用一次用戶。
最后你需要做的是添加本地軌道,包括音頻和視頻到你的連接端。只有做到這一點,我們才能夠與連接的用戶共享視頻和音頻。要做到這一點,我們需要在navigator.getMediaDevice回調中調用peerConnection對象的addTrack函數。
navigator.getUserMedia(
{ video: true, audio: true },
stream => {
const localVideo = document.getElementById("local-video");
if (localVideo) {
localVideo.srcObject = stream;
}
stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
},
error => {
console.warn(error.message);
}
);
另外,我們還需要為ontrack事件添加一個適當的處理程序。
peerConnection.ontrack = function({ streams: [stream] }) {
const remoteVideo = document.getElementById("remote-video");
if (remoteVideo) {
remoteVideo.srcObject = stream;
}
};
如圖示,我們已經從傳遞的對象中獲取了流,并改變了遠程視頻中的srcObject來使用接收到的流。所以現在當你點擊活躍用戶后,你應該建立一個視頻和音頻連接,像下圖這樣:
欲了解細節,請參閱:Node.js and dependency injection – friends or foes?
現在你知道如何編寫一個視頻聊天應用了吧!
WebRTC是一個很大的話題,內容非常龐雜。如果你想了解它的運作原理,就需要花很大功夫。幸運的是,我們可以訪問易于使用的JavaScript API,它可以幫助我們創建很簡潔的應用程序,例如視頻共享、聊天應用程序等等。
如果你想深入了解WebRTC,點擊此WebRTC官方文檔的鏈接。另外,我也推薦你閱讀MDN的文檔說明,它能幫助你更加了解此技術。
年前,濱海之邊馬上畢業的老少年
經過幾天半死不活的思考之后決定干前端
那個時候為了面試各種css屬性js API背的是滾瓜爛熟
然后投簡歷,企業要項目經驗,
我沒有工作我哪來的項目經驗啊
沒人會管你為什么沒有
so
自己編唄,于是乎為了炫技,為了證明自己開始了我的第一個自己給自己找需求的項目 ,他有個一吊炸天的項目名稱
還有我辛苦的汗水結晶。
為了吸引別人的眼光 首先得弄一個超級醒目得banner,怎么弄呢,別人的都是平面得 我就弄成立體得,于是乎四處找資料,最終弄成了一個如下圖所示得banner:給你來個俯視圖得角度
然后實際的顯示效果是醬紫:
怎么做到的呢,實際就是將一個元素的transform-style設置成preserve-3d 這個屬性允許他的子元素在一個3d空間中以一種以立體的方式呈現而不是跟平面一樣,然后將子元素,對于這個banner來說就是六張圖,絕對定位至同一中心點,然后分別旋轉0,60,120,。。。,360這種角度,然后再給個translateZ讓他們在z軸撐開,這樣就是一個3d得展示效果咯。
其實那個時候遇到的一個難點是讓這個3dbanner適配各種大小的屏幕,移動端先不說,pc端瀏覽器的寬度就是五花八門的,還有另外一點就是得讓這個banner居中
年少的我,那個時候為了適配直接將某些值寫死的 這樣的話其實在大小屏上呈現的效果會差很多,而且最為重要的一點是,在沒有進行適配的屏幕上,效果可能會很差。
關于怎么布局,以及其他細節可以查看張鑫旭的這篇文章,著此進行的一些小修改就是將原有的寫死的數值改成百分比,然后圖片上的translateZ采用js動態計算,然后居中banner的時候也是,為了使其居中,給整個banner的容器給一個left,這個值得大小等于容器的大小減去圖片的寬度再除以2.
關于translateZ的計算:
因為實際是一個等邊六邊形嘛,所以θ的大小等于 (360 / 6)/2,然后translateZ的大小就是 (x/2) / tanθ 還挺簡單。
整體上來說沒有太過復雜的處理,額外注意的一點是,居中那個banner,如果無法使內部的圖片元素寬度等于包圍他父級元素的時候 保持旋轉居中的時候是很困難的,但是為了凸顯一個立體的事物我們自然需要使得父元素的內能顯示當前正面的一張圖片,還有兩側的側面,這個時候就需要一個技巧,就是在父元素外層再包一層元素,然后我們的居中操作在這個元素上設置,就是上面說的設置left值,這樣的話既能保證圖片的寬度與父級是一致的,也同時滿足了立體的顯示效果。
預覽地址https://daxiazilong.github.io/extream/
技術講解完畢,以下是牢騷時間。
現在再去看以前寫過的東西,那些曾經引以為豪的
絞盡腦汁 去創造的
以及那時不能完美地解決的
好像都變得平平淡淡
額外一件值得開心的事情是 現在得我能解決以前得自己不能完美解決得東西了,這種感覺很舒服~
時間過得真快。
考試系統中,考生在考試過程中切換屏幕可能存在作弊風險。為了解決這一問題,可以使用Spring Boot結合Web前端監聽技術和后臺實時檢測,禁用特定熱鍵,監控前后臺切換事件并記錄日志。本文將詳細介紹技術實現和具體解決方案,并提供示例代碼進行深入講解。
在在線考試中,考生可能通過切換屏幕的方式查看其他資料,從而違背考試規則。因此,實時監測考生的屏幕切換行為并做出相應處理,是確保考試公平性的關鍵。
前端監聽技術結合Spring Boot后臺服務,可以有效地檢測并記錄屏幕切換事件。具體技術實現步驟如下:
頁面可見性API(Page Visibility API)
頁面可見性API允許網頁檢測其當前的可見性狀態,例如,網頁是否處于前臺或被最小化到后臺。我們可以通過監聽visibilitychange事件來捕獲這些狀態變化。
document.addEventListener('visibilitychange', function() {
// 檢測頁面是否可見
if (document.hidden) {
console.log('頁面不可見');
// 調用后端API記錄
fetch('/api/log-screen-switch', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ event: 'screen_hidden', timestamp: new Date() })
});
} else {
console.log('頁面可見');
// 調用后端API記錄
fetch('/api/log-screen-switch', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ event: 'screen_visible', timestamp: new Date() })
});
}
});
上述代碼通過監聽visibilitychange事件捕獲頁面的可見性變化,并將記錄發送到服務器。
禁用特定熱鍵
某些熱鍵組合如F11(全屏)和Alt+Tab(切換應用程序)會引發屏幕切換行為。可以通過監聽鍵盤事件禁用這些熱鍵。
document.addEventListener('keydown', function(event) {
if (event.key === 'F11' || (event.altKey && event.key === 'Tab')) {
event.preventDefault();
alert('切換屏幕操作被禁用');
}
});
此段代碼監聽keydown事件,如果檢測到特定熱鍵組合,就阻止其默認行為并提示用戶操作被禁用。
Spring Boot REST API
創建一個Controller來處理前端發送的屏幕切換事件。通過定義REST接口,我們可以輕松接收并處理這些請求。
@RestController
@RequestMapping("/api")
public class ScreenSwitchController {
private static final Logger logger = LoggerFactory.getLogger(ScreenSwitchController.class);
@PostMapping("/log-screen-switch")
public ResponseEntity<Void> logScreenSwitch(@RequestBody ScreenSwitchEvent event) {
// 記錄屏幕切換事件日志
logger.info("Screen switch event: {} at {}", event.getEvent(), event.getTimestamp());
return ResponseEntity.ok().build();
}
// 屏幕切換事件類
public static class ScreenSwitchEvent {
private String event;
private LocalDateTime timestamp;
// getters and setters
public String getEvent() {
return event;
}
public void setEvent(String event) {
this.event = event;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}
}
}
此代碼片段定義了一個RESTful API來接收屏幕切換事件。事件包含兩個字段:event(事件類型,例如screen_hidden)和timestamp(事件發生時間)。
日志記錄
采用日志記錄(Logging)技術來記錄每個切換事件。Spring Boot默認采用SLF4J和Logback組件,通過配置文件可以靈活定制日志記錄的格式和存儲位置。
logging:
level:
org.example: INFO
file:
name: logs/screen_switch.log
這個配置文件指定了日志的記錄級別以及輸出文件路徑。
綜合以上實現,我們可以獲得一個完整的前后端集成示例:
前端HTML及腳本
首先確保前端頁面能夠監聽屏幕切換事件,并在切換時向后端發送請求:
<!DOCTYPE html>
<html>
<head>
<title>考試系統</title>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 監聽頁面可見性變化
document.addEventListener('visibilitychange', function() {
const eventType = document.hidden ? 'screen_hidden' : 'screen_visible';
logScreenSwitchEvent(eventType);
});
// 禁用特定熱鍵
document.addEventListener('keydown', function(event) {
if (event.key === 'F11' || (event.altKey && event.key === 'Tab')) {
event.preventDefault();
alert('切換屏幕操作被禁用');
}
});
});
/**
* 向后端發送屏幕切換事件
* @param {string} eventType - 事件類型
*/
function logScreenSwitchEvent(eventType) {
fetch('/api/log-screen-switch', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ event: eventType, timestamp: new Date().toISOString() })
}).catch(console.error);
}
</script>
</head>
<body>
<h1>考試進行中</h1>
</body>
</html>
Spring Boot Controller
在Spring Boot中,創建一個Controller來處理前端發送的屏幕切換事件。引入更完善的日志管理和錯誤處理機制。
依賴配置
首先,在Spring Boot項目中確保引入必要的依賴:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
</dependencies>
Controller實現
優化后的Spring Boot Controller,包含詳細的注釋及完善的日志管理。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
@RestController
@RequestMapping("/api")
public class ScreenSwitchController {
private static final Logger logger = LoggerFactory.getLogger(ScreenSwitchController.class);
/**
* 處理前端發送的屏幕切換事件
* @param event 屏幕切換事件對象
* @return ResponseEntity<Void>
*/
@PostMapping("/log-screen-switch")
public ResponseEntity<Void> logScreenSwitch(@RequestBody ScreenSwitchEvent event) {
// 記錄屏幕切換事件日志
logger.info("Screen switch event: {} at {}", event.getEvent(), event.getTimestamp());
return ResponseEntity.ok().build();
}
// 屏幕切換事件類
public static class ScreenSwitchEvent {
private String event;
private String timestamp;
public String getEvent() {
return event;
}
public void setEvent(String event) {
this.event = event;
}
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
}
}
配置日志
Spring Boot可以通過application.yml文件進行日志配置,以確保日志輸出到指定位置:
logging:
level:
root: INFO
com.example: DEBUG
file:
name: logs/screen_switch.log
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
通過上述配置,可以更靈活地管理日志的輸出格式和存儲路徑。
兼容性測試
在不同操作系統和瀏覽器上進行測試,確保頁面可見性API和禁用熱鍵功能在各種環境下正常工作。
function isFeatureSupported() {
return typeof document.hidden !== "undefined";
}
if (!isFeatureSupported()) {
alert('您的瀏覽器不支持頁面可見性檢測,請更換瀏覽器。');
}
用戶體驗
盡量減少干擾考生的正常操作,同時確保監控和防護措施有效。例如,在禁用熱鍵時提供明確的提示信息,讓考生明白其行為受限的原因。
document.addEventListener('keydown', function(event) {
if (event.key === 'F11' || (event.altKey && event.key === 'Tab')) {
event.preventDefault();
alert('切換屏幕操作被禁用,防止作弊行為。');
}
});
通過精細化的前后端實現和優化,我們可以確保考試系統能夠有效檢測并防止屏幕切換行為,從而保障考試的公平性和安全性。
為了確保屏幕切換檢測與防護方案在實際應用中穩定有效,以下幾個注意事項需要被詳細考量和處理:
瀏覽器兼容性:頁面可見性API (Page Visibility API) 并不是所有瀏覽器都完全支持。為了確保系統能在各種環境下正常運行,需要進行廣泛的兼容性測試。
// 示例代碼:檢測瀏覽器是否支持頁面可見性API
function isVisibilityApiSupported() {
return typeof document.hidden !== "undefined";
}
if (!isVisibilityApiSupported()) {
alert('您的瀏覽器不支持頁面可見性檢測,請更換瀏覽器。');
}
前端性能:過多的事件監聽或不合理的網絡請求頻率會影響前端性能,尤其是在高并發場景下。
// 示例代碼:使用防抖函數限制請求頻率
let debounceTimer;
function logScreenSwitchEvent(eventType) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
fetch('/api/log-screen-switch', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ event: eventType, timestamp: new Date().toISOString() })
}).catch(console.error);
}, 300); // 設置防抖時間為300毫秒
}
后端性能:高并發環境下,日志記錄操作可能成為系統瓶頸。因此需要確保日志記錄機制高效且可伸縮。
<!-- 示例代碼:Logback配置異步Appender -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE"/>
</appender>
操作提示:在禁用特定熱鍵時,應當給予用戶明確友好的提示,避免用戶因誤操作導致不良體驗。
document.addEventListener('keydown', function(event) {
if (event.key === 'F11' || (event.altKey && event.key === 'Tab')) {
event.preventDefault();
alert('切換屏幕操作被禁用,防止作弊行為。');
}
});
數據傳輸安全性:確保前端與后端的通信安全,防止數據在傳輸過程中被篡改或竊取。
// 示例代碼:啟用Spring Security的CSRF防護
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.authorizeHttpRequests(authz -> authz
.anyRequest().authenticated()
)
.formLogin(withDefaults());
return http.build();
}
}
日志信息安全:防止日志信息泄露或濫用。
日志持久化:確保日志數據持久化到可靠的存儲系統中,防止日志數據丟失。
logging:
level:
root: INFO
com.example: DEBUG
file:
name: logs/screen_switch.log
通過深入考慮和處理以上注意事項,可以確保屏幕切換檢測與防護系統在真實環境中的穩定性、可靠性和用戶體驗。同時,也能有效地提升系統的安全性和性能,從而更好地保障考試的公平性和維護考生的正當權益。
通過前端和后臺的協同工作,可以有效地檢測并記錄考生在考試過程中屏幕切換的行為,從而減少作弊風險。前端通過監聽visibilitychange事件和禁用特定熱鍵,結合Spring Boot后臺日志記錄,實現了一個完整的屏幕切換檢測與防護方案。希望本文提供的技術實現和代碼示例能幫助開發者輕松應對考試系統中的這一挑戰。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。