这个功能是在大佬 Innei 的博客上看到的,效果就是在网站logo处实时的显示博主在PC上使用的应用程序信息,虽然是个不太显眼的功能,但是效果还是挺好的,让我感觉整个网站因为这个变的不再死板,不断变动的程序图标让这个网站像博主的一个身外化身。
真的很想要呀!
遂翻阅了一下大佬的博客源码和一些相关的配套文件,自己大概有了一些思路,后来也将这些想法整理了一下后请教了 一个球 大佬,大佬说这个思路是没什么问题。
所以,喜欢就干!
遂根据自己的思路搭配Kimi和ChatGPT终于在昨天将这个效果实现了一个大概,今天大概修复了一下BUG,撰文总结一下这次折腾和分享实现过程。
对于这个功能的整个流程总结如下:
根据这个结构我们需要一个可以上报数据的监控程序和一个架设API的VPS或可以架设API功能的云函数等,技术架构如下。
能监控电脑应用并自动上报的程序我第一时间想到的是Tai,这个程序我用了好几年了,还写过一篇推荐文 在 Windows 上像智能手机那样统计软件使用情况 ,它可以记录电脑上每个应用程序的使用时间,肯定是可以做这张方面监控的,所以我还跑去作者的项目提了个 issue 咨询,作者 noberumotto 大佬说应该可以,并且给我说了要改那部分代码,同时给我推荐了他们另外一个开源项目 sentry 。
但是我研究了一下,发现如果要Debug这个程序需要安装几十个G的Visual Studio,遂暂时放弃了这个想法。
后来发现 Innei 大佬那个项目 Shiro 其实原本提供了一个Mac版的监控程序,并且有爱好者开发了支持Windows的 Go、Python、C# 版本。
我最后选用了 C# 版本做测试,这个版本不是定时按频率发送,而是在切换窗口时发送,能减少无用发送,不过这个版本的媒体信息发送还不完善(所以当前在听什么歌的功能我还没实现)。
我看了一下这个程序不一定非要搭配 Shiro ,这个程序只是向一个api发送了当前程序的数据,只要接受这个数据并有相关的处理流程就好了。
遂找 Kimi 要了一段启动API服务和WebSocket服务的demo代码,自己在本地测试一次性启动成功:
const express = require('express');
const { WebSocketServer,WebSocket } = require('ws');
const app = express();
// 允许解析JSON格式的请求体
app.use(express.json());
// 存放所有WebSocket客户端的数组
let clients = [];
// 启动WebSocket服务器
const wss = new WebSocketServer({ port: 8081 });
wss.on('connection', function connection(ws) {
// 将新客户端添加到数组中
clients.push(ws);
// 当有消息到达时,将其广播给所有客户端
ws.on('message', function incoming(message) {
console.log('received: %s', message);
});
// 当客户端断开连接时,从数组中移除
ws.on('close', (e) => {
clients = clients.filter(client => client !== ws);
console.log(e+'断开连接了,从列表中移除')
});
});
// 定义一个POST路由,它将接收数据并打印到控制台,然后通过WebSocket广播
app.post('/update', (req, res) => {
console.log('Received Data:', req.body);
// 将数据作为消息发送给所有WebSocket客户端
clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(req.body));
}
});
res.send('Data received and broadcasted via WebSocket');
});
// 启动HTTP服务器,监听3000端口
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`HTTP Server is running on port ${PORT}`);
});
// 同时启动WebSocket服务器,监听8080端口
console.log(`WebSocket Server is running on port 8081`);
打开安装好的 Shiro C# 版本程序,并设置好API接口,发现果然程序开始正常运作,而且在运行日志里能看到程序向搭建好的本地api发了一个结构如下的json数据:
{
"timestamp": 1715344006,
"process": "Chrome",
"key": "apikey"
}
服务端能接受到数据就基本上没什么问题了,接下来处理网页端的显示。
网页端同步显示无非两种办法:轮询和WebSocket,前者设置定时器定时去请求服务就好了,后者则可以主动接收服务下发的信息,之前在咨询 一个球 大佬的时候证实了后者是可行的,所以肯定选WebSocket的方案。
我以前也从来没用过WebSocket,大概查询了一下概念:
WebSocket 是一种在单个TCP连接上进行全双工通信的协议,它允许客户端和服务器之间进行实时、连续的数据交换。以下是其主要特点的详细解释:
1. 全双工通信:与传统的HTTP请求/响应模式不同,WebSocket 允许服务器主动向客户端发送消息,而不需要客户端的请求。这意味着数据可以在任何一方发起,实现真正的双向通信。
2. 持久连接:一旦WebSocket连接建立,它将保持开放状态,直到客户端或服务器决定关闭它。这减少了因频繁建立和关闭连接而产生的开销。
3. 低延迟:由于不需要为每个数据传输重新建立连接,WebSocket 可以减少通信的延迟,这对于需要快速响应的应用(如在线游戏或实时数据更新)非常重要。
4. 较少的控制开销:与HTTP相比,WebSocket 在数据传输时的控制信息更少,这使得它在传输大量数据时更加高效。
5. 应用层协议:WebSocket 是一个独立的、基于TCP的应用层协议,它通过一个HTTP请求升级到WebSocket连接。一旦升级完成,HTTP协议就不再参与通信过程。
6. 端口使用:WebSocket 默认使用端口80和443,与HTTP和HTTPS相同,这使得它能够通过大多数防火墙和代理服务器
为了减少学习成本,我直接向Kimi要了一段如下要求的代码:
使用javascript创建websocket,如果连接失败则继续尝试。
直接在控制台执行,一次成功。
网页端能接受到数据后基本上整个数据的流转已经完全没有问题了,现在要做的就是以何种方式对数据进行渲染、展示,我目前实现了近似 Innei 大佬博客的淡出淡入效果,这里大概说一下我对动画效果的处理。
一、 APP白名单
我这里用了一个独立的 app.json
文件维护一个白名单,方便之后在不更新代码的情况下更新名单数据。
{
"wechat": {
"title": "微信",
"url": "wechat.png",
"action": "摸鱼"
},
"chrome": {
"title": "Chrome",
"url": "chrome.png",
"action": "冲浪"
},
}
二、 VPS的服务搭建
我这里用的是express的docker镜像,将程序代码上传到绑定的目录中即可。
另外直接启动项目可能会提示npm包没安装,所以项目的 package.json
需要加上 "start": "npm install && node index.js"
然后使用nginx进行了反代。
修改 wsUrl
、appListUrl
、cdn
为你自己的服务地址。
// 定义测试用的URL
const wsUrl = "ws://localhost:8081/update";
const appListUrl = "http://localhost:8080/assets/app.json";
// 定义正式环境的url
const cdn = "https://cdn.1900.live/apps/";
// app白名单
let appList = {};
// 保存WebSocket实例的变量
let ws2;
// 初始化WebSocket连接
export default function initWebSocket() {
// 获取远端的app清单
fetch(appListUrl).then((rep) => {
rep.json().then((data) => {
ws2 = new WebSocket(wsUrl);
// 初始化app列表
appList = data;
// 绑定事件处理函数
ws2.onopen = onOpen;
ws2.onmessage = onMessage;
ws2.onclose = onClose;
ws2.onerror = onError;
});
});
}
// 连接成功的处理函数
function onOpen(event) {
// console.log("WebSocket connection opened:", event);
// 可以在这里发送消息等操作
}
// 接收到消息的处理函数
function onMessage(event) {
// 接受服务端下发的程序数据
var data = JSON.parse(event.data);
// 获取页面上actives dom元素
var activs = document.querySelector(".actives");
// 之后用来判断的进程名称统一小写,方便比对
const processName = data.process.toLowerCase();
// 处理接收到的消息
// 条件为:当前页面显示的app和服务器下发的app要不一样(说明是新程序),并且程序在app清单中。
if (activs.dataset.app != data.process && processName in appList) {
// 提前缓存图片(我发现大佬博客图片加载有颜值,不过不知道这个有用没有)
fetch(cdn + appList[processName].url + "!20w").then(function () {
// 先将旧的actives执行退场动画
activs.style.display = "block";
activs.classList.add("exit");
// 0.5s后执行更新操作
setTimeout(function() {
// 重新设置icon
document.querySelector(".actives img").src =
cdn + appList[processName].url + "!20w";
// 执行进场动画
activs.classList.remove("exit");
// 更新dom上app的信息
activs.dataset.app = processName;
// 这里我用Tippy.js做鼠标悬浮提示,更新悬浮提示内容
activeTippy.forEach(function (e) {
e.setContent(
"@1900 在使用 " +
appList[processName].title +
" " +
appList[processName].action
);
});
}, 500);
});
// 如果是不在白名单里的应用就不显示actives元素了
} else if (!(processName in appList)) {
activs.classList.add("exit");
}
}
// 连接关闭的处理函数
function onClose(event) {
// 尝试重新连接
document.querySelector(".actives").classList.add("exit");
setTimeout(initWebSocket, 5000); // 5秒后尝试重新连接
}
// 连接错误的处理函数
function onError(event) {
// 尝试重新连接
document.querySelector(".actives").classList.add("exit");
}
加入评论