耕升显卡附赠的 Gainward Expertool 是一款经典的显卡调节工具,核心能力集中在几块:实时显示 GPU 核心温度、风扇转速、核心与显存频率、功耗和显存占用,允许用户自定义风扇曲线、手动拉频率做超频,部分型号还带灯光控制。这类软件给人的第一印象是硬件级的,跟前端技术似乎不沾边,但把它的界面拆开看,本质就是一个数据密集型的桌面客户端:界面层负责展示与交互,数据层持续从驱动和传感器读取状态。而界面这一层,恰好是 Vue 3 加上成熟工程化体系最能发挥的地方。这篇文章就完整走一遍:怎么用 Vite 加 Electron 搭骨架,怎么拆组件,数据怎么从传感器流到界面,风扇曲线编辑器怎么写,最后怎么打包成 Windows 安装包。

先弄清楚 Expertool 的技术本质
Expertool 能读到温度和转速,靠的不是什么黑魔法,而是显卡驱动暴露出来的底层接口。NVIDIA 显卡主要走 NVAPI,AMD 显卡走 ADL SDK,另外还有通用硬件监控库的路线,比如 LibreHardwareMonitor 的做法是直接读 MSR 寄存器和 SMBus 设备拿到原始数值。这些接口吐出来的都是裸数据,界面层拿到之后再做格式化、阈值判断和图表绘制。所以一个显卡工具的完整公式很清晰:原生数据采集,加上一个桌面壳,再加上界面渲染。
把这个公式映射到前端技术栈,对应关系是一一对应的:桌面壳用 Electron 承担,窗口管理、系统托盘、开机自启这些桌面软件的标配能力它都有;界面用 Vue 3 写,组件化正好匹配监控面板这种多模块布局;数据采集放在 Electron 主进程里,用 Node.js 原生模块或者调用系统命令完成。渲染进程不直接碰硬件,全部通过 IPC 通道拿数据,职责划分和 Expertool 本身没有区别,差别只在实现语言。
有一点要提前说清楚:读数据和写数据是两个难度等级。读取温度、频率、利用率属于相对安全的信息查询,Windows 自带的性能计数器就能覆盖一部分;而写入频率、电压(也就是超频)需要驱动层的特权调用,得依赖带签名的原生模块,操作不当还有风险。本文把重点放在监控和风扇策略这条链路上,写入部分只给出思路。
搭建 Vite 加 Electron 的工程骨架
先看目录结构。整个项目分成两块:electron 目录放主进程、preload 脚本和硬件采集封装,src 目录放纯前端的 Vue 代码,两边通过 IPC 通信,互不越界。
gpu-expertool/
electron/
main.js 主进程:窗口管理与数据推送
preload.js 上下文桥:暴露安全 API 给页面
gpu-sensor.js 硬件数据采集封装
src/
App.vue
stores/gpu.js Pinia 数据仓库
components/
GpuStatusCard.vue
FanCurveEditor.vue
ClockPanel.vue
vite.config.js
package.json
package.json 里有几个关键点。main 字段必须指向主进程入口,开发脚本要用 concurrently 同时拉起 Vite 开发服务器和 Electron,再用 wait-on 等端口就绪后才启动桌面进程,否则 Electron 会加载一个还没准备好的空白页。
{
"name": "gpu-expertool",
"version": "0.1.0",
"main": "electron/main.js",
"scripts": {
"dev": "concurrently \"npm:dev:vite\" \"npm:dev:electron\"",
"dev:vite": "vite",
"dev:electron": "wait-on tcp:5173 && cross-env VITE_DEV_SERVER_URL=http://127.0.0.1:5173 electron .",
"build": "vite build && electron-builder"
},
"devDependencies": {
"vue": "^3.4.0",
"pinia": "^2.1.0",
"vite": "^5.0.0",
"electron": "^28.0.0",
"electron-builder": "^24.0.0",
"concurrently": "^8.0.0",
"wait-on": "^7.0.0",
"cross-env": "^7.0.0"
}
}
主进程的写法是标准套路,重点在 webPreferences 的配置:contextIsolation 打开、nodeIntegration 关闭,这是 Electron 官方反复强调的安全基线,页面里拿不到 Node 能力,只能通过 preload 暴露的白名单接口干活。
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const { startGpuPolling } = require('./gpu-sensor');
function createWindow() {
const win = new BrowserWindow({
width: 1180,
height: 760,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
});
if (process.env.VITE_DEV_SERVER_URL) {
win.loadURL(process.env.VITE_DEV_SERVER_URL);
} else {
win.loadFile(path.join(__dirname, '../dist/index.html'));
}
}
// 页面发起订阅后,主进程按固定间隔推送显卡状态
ipcMain.on('gpu:subscribe', (event) => {
startGpuPolling(1000, (status) => {
event.sender.send('gpu:data', status);
});
});
app.whenReady().then(createWindow);
preload 这一层是渲染进程和主进程之间唯一的大门,用 contextBridge 把订阅和设置风扇转速两个能力挂到 window 上,页面代码从此只认 gpuBridge 这个对象,完全感知不到 IPC 的存在。
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('gpuBridge', {
subscribe: (callback) => {
ipcRenderer.on('gpu:data', (_event, status) => callback(status));
ipcRenderer.send('gpu:subscribe');
},
setFanSpeed: (percent) => ipcRenderer.invoke('gpu:setFanSpeed', percent)
});
组件化拆分监控界面
回头观察 Expertool 的界面布局,可以很自然地切成几块:顶部是状态总览卡(型号、温度、功耗、显存占用),中间是实时曲线区域,右侧是风扇控制面板,底部是频率与电压调节。映射到 Vue 组件,就是一个根组件挂着 GpuStatusCard、FanCurveEditor、ClockPanel 三类子组件,各自独立、各自可测。
先写最简单的状态卡。用 <script setup> 语法,props 接收一个 status 对象,模板里直接渲染各项指标。注意温度超过 80 度时的告警高亮,用动态 class 一行就能搞定。
<template>
<div class="status-card">
<h3>{{ gpuName }}</h3>
<p class="metric">
核心温度
<strong :class="{ danger: status.temp >= 80 }">{{ status.temp }}℃</strong>
</p>
<p class="metric">风扇转速 {{ status.fanSpeed }} RPM({{ status.fanPercent }}%)</p>
<p class="metric">核心频率 {{ status.coreClock }} MHz / 显存 {{ status.memClock }} MHz</p>
<p class="metric">显存占用 {{ status.memUsed }} / {{ status.memTotal }} GB</p>
</div>
</template>
<script setup>
defineProps({
gpuName: { type: String, default: '耕升 RTX 显卡' },
status: { type: Object, required: true }
});
</script>
这里有个设计取舍值得说一句:组件只做展示,数据一律从 props 进来,组件内部不去订阅数据源。这样 GpuStatusCard 拿一份 mock 数据就能单独跑起来调试,不用先起 Electron 再等传感器出数,开发体验完全不是一个量级。真实数据的注入统一放在根组件里完成,单向数据流也保持了干净。
数据链路:从传感器流到 Pinia
数据从主进程推过来之后,需要一个落脚点。用 Pinia 建一个 gpu 仓库,除了保存当前状态,还要维护一份历史序列,因为温度曲线图需要过去几分钟的数据才能画出来。
import { defineStore } from 'pinia';
export const useGpuStore = defineStore('gpu', {
state: () => ({
status: {
temp: 0, fanSpeed: 0, fanPercent: 0,
coreClock: 0, memClock: 0,
memUsed: 0, memTotal: 0, power: 0
},
history: []
}),
actions: {
update(next) {
this.status = next;
this.history.push({ time: Date.now(), temp: next.temp });
// 每秒一条,只保留最近十分钟
if (this.history.length > 600) {
this.history.shift();
}
}
}
});
根组件在挂载时发起订阅,之后每一条推送都交给仓库处理,组件树里任何需要数据的地方直接从仓库取,订阅动作全局只发生一次。
import { onMounted } from 'vue';
import { useGpuStore } from './stores/gpu';
const gpuStore = useGpuStore();
onMounted(() => {
window.gpuBridge.subscribe((status) => {
gpuStore.update(status);
});
});
剩下的问题是主进程怎么真正拿到显卡数据,这也是整个项目里最非前端的一环,现实中有三条路可走。第一条是 Windows 性能计数器,系统自带,能拿到 GPU 利用率和部分温度指标,适合起步阶段验证链路;第二条是基于 LibreHardwareMonitor 的思路,读底层寄存器拿全量传感器数据,社区有现成的 .NET 库,可以让主进程 spawn 一个小的采集进程通过标准输出回传 JSON;第三条是针对 N 卡的 NVAPI 原生绑定,能力最全,读写都支持,但需要自己编译原生模块。下面用性能计数器演示第一条路的写法:
const { execFile } = require('child_process');
// 通过 Windows 性能计数器读取 GPU 利用率,仅用于链路验证
function readGpuUsage() {
return new Promise((resolve) => {
execFile('typeperf', ['\\GPU Engine(*)\\Utilization Percentage', '-sc', '1'], (err, stdout) => {
if (err) return resolve(0);
const lines = stdout.split('\r\n');
const values = lines[2] ? lines[2].split(',').slice(1) : [];
const nums = values
.map(v => parseFloat(v.replace(/"/g, '')))
.filter(v => !isNaN(v));
resolve(nums.length ? Math.max(...nums) : 0);
});
});
}
必须提醒一句,execFile 拉起外部命令这种方式延迟高、开销大,拿来做原型可以,正式版本请换成原生模块或者常驻采集进程,否则每秒一次的轮询会把 CPU 白白吃掉几个百分点,对一个硬件监控工具来说相当讽刺。
风扇曲线编辑器的实现
Expertool 最有辨识度的交互就是那条可拖拽的风扇曲线:横轴温度、纵轴转速百分比,玩家拖几个控制点就能让显卡在低温时安静、高负载时全力散热。这个交互用 SVG 实现非常顺手,polyline 画曲线,circle 画控制点,事件绑定都是现成的。
<template>
<svg ref="svgEl" viewBox="0 0 400 240" class="fan-curve"
@mousemove="onMove" @mouseup="endDrag" @mouseleave="endDrag">
<polyline :points="curvePoints" fill="none" stroke="#4cc2ff" stroke-width="2" />
<circle
v-for="(p, i) in controlPoints"
:key="i"
:cx="p.x" :cy="p.y" r="6"
class="ctrl-point"
@mousedown="startDrag(i)"
/>
</svg>
</template>
拖拽的核心是把鼠标的像素坐标换算回 SVG 的 viewBox 坐标,很多初学者在这里栽跟头:直接用 offset 坐标在视图被拉伸时会飘掉,正确做法是拿 getBoundingClientRect 算比例。
import { ref, computed } from 'vue';
const svgEl = ref(null);
const dragIndex = ref(-1);
const controlPoints = ref([
{ x: 40, y: 200 }, // 低温对应低转速
{ x: 160, y: 160 },
{ x: 280, y: 80 },
{ x: 380, y: 20 } // 接近满载时接近全速
]);
const curvePoints = computed(() =>
controlPoints.value.map(p => `${p.x},${p.y}`).join(' ')
);
function onMove(e) {
if (dragIndex.value === -1) return;
const rect = svgEl.value.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 400;
const y = ((e.clientY - rect.top) / rect.height) * 240;
const p = controlPoints.value[dragIndex.value];
p.x = Math.min(400, Math.max(0, x));
p.y = Math.min(240, Math.max(0, y));
}
function startDrag(i) { dragIndex.value = i; }
function endDrag() { dragIndex.value = -1; }
控制点确定之后,把坐标反算成温度到转速的映射表(比如 x 除以 4 得到温度、y 换算成百分比),调用 gpuBridge 的 setFanSpeed 或者整表提交给主进程,由主进程按温度区间应用转速。这里有两个工程细节别忽略:一是转速要设下限,比如 30%,否则曲线拉太低显卡风扇停转,热量堆积反而危险;二是首尾两个控制点建议锁定,只开放中间点拖拽,避免用户把曲线改成非单调的奇怪形状。
打包成 Windows 安装包
功能写完之后就是分发环节。electron-builder 是目前的事实标准,配置放在 package.json 的 build 字段或者单独的 electron-builder.yml 里,Windows 平台选 nsis 作为目标,产出常规的下一步式安装向导。
{
"appId": "com.ipipp.gputool",
"productName": "GpuExpertool",
"directories": { "output": "release" },
"win": {
"target": "nsis",
"icon": "build/icon.ico"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
},
"asar": true
}
执行 npm run build,Vite 先把 Vue 项目产出到 dist,electron-builder 再把 dist 连同主进程代码一起塞进 asar 包并生成安装程序。图标记得准备 ico 格式且包含 256 尺寸,否则安装后任务栏图标会糊。
最后聊几个容易踩的坑。第一,硬件工具建议做代码签名,没有签名的安装包会被 Windows SmartScreen 拦一道,普通用户看到警告基本就直接放弃了;第二,如果后续接了原生模块,记得在 electron-builder 里配置重新编译,原生模块必须和 Electron 自带的 Node 版本匹配,否则启动直接报错;第三,读数据容易写数据难,超频相关的写入操作务必走带校验的原生接口,并且给用户留好一键恢复默认的退路,这也是 Expertool 多年来坚持的设计原则。整套架构跑通之后你会发现,所谓硬件工具,前端承担的那一半工作量和普通后台管理系统差不多,真正深的功夫都在主进程那一侧。
Vue 3 工程化Gainward Expertool显卡监控工具修改时间:2026-10-03 19:27:09