序幕:被GFW狙击的第一次构建
当我在工位上输入npm install electron
时,控制台跳出的红色警报如同数字柏林墙上的一道弹痕:
Error: connect ETIMEDOUT 104.20.22.46:443
网络问题不用愁,请移步我的另外文章进行配置:
electron 客户端 windows linux(麒麟V10)多系统离线打包 最新版 <一>_electron linux 离线打包-CSDN博客
第一章:构建electron-builder
builder排除文件夹,简单配置如下(package.json中):
"build": {
"appId": "com.example.win7app",
"win": {
"target": "nsis",
"defaultArch": "ia32"
},
"extraFiles": [
{
"from": "resources",
"to": "Resources",
"filter": [
"**/*"
]
}
],
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
}
},
我们需要在这个文件夹中读取dll文件,同时希望它打包后在安装目录下。
第二章:跨维度通信协议——主进程与渲染进程的量子纠缠
根目录添加preload.js,添加如下代码:
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
// 向渲染进程暴露安全的 API 方法
contextBridge.exposeInMainWorld(
'electronAPI',
{
// 示例:调用 Node.js 文件系统 API
readFile: async (path) => {
const fs = await import('fs/promises');
return fs.readFile(path, 'utf-8');
},
// 示例:进程间通信(IPC)
openDialog: () => ipcRenderer.invoke('dialog:open'),
// 检查是否存在加密狗并且是否匹配成功
checkIfLock: () => ipcRenderer.invoke('checkIfLock'),
captureUKey: () => ipcRenderer.invoke('captureUKey'),
// 关闭窗口
closeWindow: () => ipcRenderer.invoke('closeWindow'),
// 监听打开设置
onAction: (callback) => {
ipcRenderer.on('renderer-action', (event, arg) => callback(arg))
},
}
)
然后再mainjs(electron主进程)中配置文件:
let mainWindow, tray = null;
function createWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'), // 指定预加载脚本
contextIsolation: true, // 开启上下文隔离(安全必备)
nodeIntegration: false // 禁用直接 Node.js 访问
}
})
// 隐藏菜单
mainWindow.setMenu(null);
// 加载本地页面(开发时可替换为本地服务地址,如 http://localhost:3000)
mainWindow.loadFile(path.join(__dirname, 'src', 'index.html'));
// 窗口关闭事件处理
mainWindow.on('close', (event) => {
if (!app.isQuiting) {
event.preventDefault()
const choice = dialog.showMessageBoxSync(mainWindow, {
type: 'question',
buttons: ['直接退出', '最小化到托盘'],
title: '确认',
message: '您要如何操作?',
defaultId: 1
})
if (choice === 0) {
app.isQuiting = true
app.quit()
} else {
mainWindow.hide()
}
}
})
}
最后在html中使用上述方法(html中使用):
<script>
//【IPC通信】检测开关(true false)
const checkIfLock = window.electronAPI.checkIfLock;
const checkArm = window.electronAPI.captureUKey;
const closeWindow = window.electronAPI.closeWindow;
// 打开设置
window.electronAPI.onAction(({ type, data }) => {
switch(type) {
case 'openSettings':
showSetting()
break;
}
})
// 显示设置
function showSetting () {
try {
var remortroot = localStorage.dpm_root;
var remortport = localStorage.dpm_port;
if (remortroot != null) {
$("#remortroot").val(remortroot);
}
if (remortport != null) {
$("#remortport").val(remortport);
}
$('#myModal').modal('show');
} catch (err) {
$('#myModal').modal('show');
}
}
// ===========================================
var timeout;
var lockState = false;
$(function () {
checkIfLock().then(res => {
lockState = res;
})
//初始化设置
var remortroot = localStorage.dpm_root;//服务器IP地址
var remortport = localStorage.dpm_port;//端口号
if (remortroot == null || remortroot == "" || remortroot == "undefined"
|| remortroot == null || remortroot == "" || remortroot == "undefined") {
var err = "首次登录,请填写网络配置";
showConfirmMsg(err, function (r) {
if (r) {
$('#myModal').modal('show');
}
});
} else {
loadLoginPage(remortroot, remortport);
}
});
function isPort(str) {
var parten = /^(\d)+$/g;
if (parten.test(str) && parseInt(str) <= 65535 && parseInt(str) >= 0) {
return true;
} else {
return false;
}
}
function isIP(strIP) {
var re = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/g
if (re.test(strIP)) {
if (RegExp.$1 < 256 && RegExp.$2 < 256 && RegExp.$3 < 256 && RegExp.$4 < 256) return true;
}
return false;
}
function sendUrlPort(remortroot, remortport) {
var err = "";
if (isIP(remortroot)) {
} else {
err += "服务器地址无效,";
}
if (isPort(remortport)) {
} else {
err += "端口号无效,";
}
if (err != "" && err.length > 0) {
err = err.substring(0, err.length - 1);
showConfirmMsg(err, function (r) {
if (r) {
$('#myModal').modal('show');
}
});
} else {
layer.msg('正在加载登录页面,请稍候。。。', {
icon: 16,
shade: 0.01,
time: 5000000,
shadeClose: false
});
var dpmHid = $("#eam_hid").text();
var ifUd = "";
if (false == lockState) {//false代表 u盾登录 需要验证uid
ifUd = "xxx"
}
var url = "https://" + remortroot + ":" + remortport
$.ajax({
url: url,
type: 'GET',
timeout: 100000,
datatype: "json",
complete: function (response, textStatus) {
//启动u盾检测
timeout = setInterval("testUd()", 3000);
layer.closeAll();
if (response.status == 200) {
localStorage.dpm_root = remortroot;
localStorage.dpm_port = remortport;
$('#github-iframe').attr('src', url);
} else if (textStatus == 'timeout') {
showConfirmMsg('未能成功连接系统(超时),请检查网络配置或联系管理员!', function (r) {
if (r) {
$('#myModal').modal('show');
}
});
} else {
showConfirmMsg('未能成功连接系统,请检查网络配置或联系管理员!', function (r) {
if (r) {
$('#myModal').modal('show');
}
});
}
}
});
}
}
//启动时:加载登录页,并判断u端是否存在
function loadLoginPage(remortroot, remortport) {
checkArm().then(res => {
if (true == lockState) {
if (false == res.flag) {
$("#eam_hid").text("");
}
sendUrlPort(remortroot, remortport);
} else {
if (true == res.flag) {
$("#eam_hid").text(res.randomNum);
sendUrlPort(remortroot, remortport);
} else {
showLongErrorMsg("未插入U盾");
}
}
})
}
//启动后 监测u盾是否插入,未插入则退出系统
function testUd() {
checkArm().then(res => {
if (true == lockState) {
if (false == res) {
$("#eam_hid").text("");
}
} else {
if (false == res) {
clearInterval(timeout);
showLongErrorMsg("未插入U盾");
}
}
})
}
function showConfirmMsg(msg, callBack) {
art.dialog({
id: 'confirmId',
title: '系统提示',
content: msg,
icon: 'warning',
background: '#000000',
opacity: 0.1,
lock: true,
button: [{
name: '确定',
callback: function () {
callBack(true);
},
focus: true
}]
});
}
//错误提示
function showErrorMsg(msg) {
top.art.dialog({
id: 'errorId',
title: '系统提示',
content: msg,
icon: 'error',
time: 5,
background: '#000',
opacity: 0.1,
lock: true,
okVal: '关闭',
ok: true
});
}
function showLongErrorMsg(msg) {
top.art.dialog({
id: 'errorId',
title: '5秒后自动关闭客户端...',
content: msg,
icon: 'error',
time: 5,
background: '#000',
opacity: 0.1,
lock: true,
cancelVal: '关闭',
cancel: function () {
closeWindow();
},
close: function () {
closeWindow();
}
});
}
//弹出框事件
$("#initsetbtn").click(function () {
var remortroot = $("#remortroot").val();
var remortport = $("#remortport").val();
$('#myModal').modal('hide');
loadLoginPage(remortroot, remortport);
});
</script>
第三章:调用dll
调用dll推荐使用koffi。另一篇文章也有说明:
Electron驯龙记:在Win7的废墟上唤醒32位DLL古老巨龙-CSDN博客
示例代码:
// 在mainjs中
// 是否捕获U盾
ipcMain.handle('captureUKey', () => {
return new Promise((resolve, reject) => {
let promiseAry = [];
var count = 0;
while (count++ < 20) { //连续20次都失败 才认为失败
promiseAry.push(checkOnceUKey());
}
Promise.all(promiseAry).then((results) => {
console.log('所有检查结果:', results);
if (Array.isArray(results) && results.length > 0) {
resolve({
flag: results.filter(item => item.flag == false).length == 0 ? true: false,
randomNum: results[results.length - 1].randomNum || ''
})
} else {
resolve({
flag: false,
randomNum: ''
})
}
})
}).catch(error => {
console.error('是否捕获U盾出错:', error);
return false; // 读取失败
})
})
// 单次检查U盾
function checkOnceUKey() {
return new Promise((resolve, reject) => {
let flag = false;
let randomNum = ''; // 随机数
// 常量定义
const DONGLE_SUCCESS = 0;
const koffi = require('koffi');
// 加载 DLL
const dllPath = path.join(getDataPath(), 'Dongle_d.dll');
const dongleLib = koffi.load(dllPath);
// 定义结构体字段偏移量(单位:字节)
const InfoStructOffsets = {
m_Ver: 0,
m_Type: 2,
m_BirthDay: 4,
m_Agent: 12,
m_PID: 16,
m_UserID: 20,
m_HID: 24,
m_IsMother: 32,
m_DevType: 36
};
const InfoStructSize = 40;
const Dongle_Enum = dongleLib.func('int Dongle_Enum(void*, int*)');
const Dongle_Open = dongleLib.func('int Dongle_Open(int*, int)');
const Dongle_ResetState = dongleLib.func('int Dongle_ResetState(int)');
const Dongle_GenRandom = dongleLib.func('int Dongle_GenRandom(int, int, void*)');
const Dongle_Close = dongleLib.func('int Dongle_Close(int)');
// 初始化缓冲区
const dongleInfo = Buffer.alloc(1024); // 假设最多 25 个设备(1024 / 40 ≈ 25)
const countBuffer = Buffer.alloc(4);
countBuffer.writeInt32LE(0, 0);
// 1️⃣ 枚举设备
let result = Dongle_Enum(dongleInfo, countBuffer);
console.log(`** Dongle_Enum **: 0x${result.toString(16).padStart(8, '0')}`);
if (result !== DONGLE_SUCCESS) {
console.error(`** Enum errcode **: 0x${result.toString(16).padStart(8, '0')}`);
flag = false;
}
const deviceCount = countBuffer.readInt32LE(0);
console.log(`** Find Device **: ${deviceCount}`);
if (deviceCount === 0) {
console.log('** No Device **');
flag = false;
}
// 3️⃣ 打开设备
const handleBuffer = Buffer.alloc(4);
result = Dongle_Open(handleBuffer, 0);
const handle = handleBuffer.readInt32LE(0);
console.log(`** Dongle_Open **: 0x${result.toString(16).padStart(8, '0')}`);
if (result !== DONGLE_SUCCESS) {
console.error(`** Open Failed **`);
flag = false;
} else {
console.log(`** Open Success **: [handle=0x${handle.toString(16).padStart(8, '0')}]`);
randomNum = `0x${handle.toString(16).padStart(8, '0')}`;
Dongle_Close(handle);
flag = true;
}
// 4️⃣ 重置 COS 状态
/*
result = Dongle_ResetState(handle);
console.log(`Dongle_ResetState 返回值: 0x${result.toString(16).padStart(8, '0')}`);
if (result !== DONGLE_SUCCESS) {
console.error(`重置 COS 状态失败`);
Dongle_Close(handle);
return;
}
console.log('重置 COS 状态成功');
*/
// 5️⃣ 生成随机数
// const randomLen = 16;
// const randomBuffer = Buffer.alloc(randomLen);
// result = Dongle_GenRandom(handle, randomLen, randomBuffer);
// console.log(`Dongle_GenRandom : 0x${result.toString(16).padStart(8, '0')}`);
// if (result !== DONGLE_SUCCESS) {
// console.error(`生成随机数失败`);
// Dongle_Close(handle);
// } else {
// randomNum = randomBuffer.toJSON().data.map(b => PrefixZero(b, 2)).join(' ').toUpperCase();
// Dongle_Close(handle);
// }
//console.log(`随机数据: ${randomBuffer.toJSON().data.map(b => PrefixZero(b, 2)).join(' ').toUpperCase()}`);
/*
// 6️⃣ 关闭设备
result = Dongle_Close(handle);
console.log(`Dongle_Close 返回值: 0x${result.toString(16).padStart(8, '0')}`);
if (result !== DONGLE_SUCCESS) {
console.error(`关闭设备失败`);
return;
}
console.log('成功关闭设备');
*/
resolve({
flag,
randomNum
})
}).catch(err => {
console.error('单次读取U盾失败:', err);
return false; // 读取失败
})
}
后记:与时间赛跑的混乱代码之旅
回首这次Electron的改造征程,更像是一场与编译警告共舞的午夜狂奔。由于项目周期紧张,某些技术方案难免带着「先跑起来再优化」的仓促痕迹——就像在暴雨中搭建帐篷,难免会有几处漏水的接缝。
过程中那些临时添加的Webpack补丁、为绕过环境问题硬编码的路径、甚至为了紧急交付保留的TODO
注释,都如同代码迷宫中未清理的记号。虽然最终功能得以实现,但我深知这座代码大厦的某些承重墙上,或许还留着需要加固的裂缝。
在此特别恳请各位同行:若您在阅读中发现任何逻辑漏洞、安全隐患或架构缺陷,请务必通过Issue或邮件指正。您的一条建议,或许就能避免某个深夜的生产环境告警。技术之路本就如履薄冰,唯有开放交流才能让我们的每一步走得更稳。
~ end