本文介绍了一个使用Node.js开发Electron跨平台桌面应用的简单配置方案。主要功能包括:
- 核心配置:
- 基于Electron 25.9.8构建
- 使用electron-builder进行打包
- 支持Windows 32位/64位平台
- 主要功能实现:
- HTTP/HTTPS请求处理(使用axios和request模块)
- AWS S3文件下载功能(@aws-sdk/client-s3)
- PostgreSQL数据库连接(pg模块)
- 图片下载和保存功能
- 自定义右键菜单
- 技术亮点:
- 预加载脚本(index_preload.js)实现安全通信
- 支持自签名证书的HTTPS请求
- 自动重定向处理(最多50次)
- 多环境配置管理
该配置提供了完整的桌面应用开发框架,包含网络请求、文件操作、数据库连接等常用功能模块。
package.json:
javascript
{
"name": "ServiceMonitoring",
"version": "0.1.0",
"description": "工具",
"main": "main.js",
"scripts": {
"start": "chcp 65001 && electron .",
"dist": "chcp 65001 && electron-builder --win --ia32"
},
"keywords": [
"bolt",
"json",
"db"
],
"author": "LZH",
"license": "copy right",
"dependencies": {
"@aws-sdk/client-s3": "^3.621.0",
"@aws-sdk/node-http-handler": "^3.374.0",
"ajv": "^6.12.6",
"axios": "^1.13.2",
"http": "^0.0.1-security",
"https": "^1.0.0",
"pg": "^8.12.0",
"request": "^2.88.2"
},
"devDependencies": {
"babel-plugin-transform-async-to-generator": "^6.24.1",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"electron": "^25.9.8",
"electron-builder": "^22.13.1",
"electron-compile": "^6.4.4",
"electron-packager": "^15.4.0",
"electron-prebuilt-compile": "8.2.0",
"electron-squirrel-startup": "^1.0.0"
},
"build": {
"appId": "com.lt.app",
"productName": "工具",
"directories": {
"output": "target",
"buildResources": "resources/icon.png"
},
"files": [
"**/*",
"**/resources/*",
"!**/schemas/*",
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
"!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
"!**/node_modules/*.d.ts",
"!**/node_modules/.bin",
"!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}",
"!.editorconfig",
"!**/._*",
"!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}",
"!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}",
"!**/{appveyor.yml,.travis.yml,circle.yml}",
"!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}"
],
"extraResources": {
"from": "./ext/",
"to": "../ext/"
},
"asar": true,
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"ia32",
"x64"
]
},
{
"target": "zip",
"arch": [
"ia32",
"x64"
]
}
],
"icon": "resources/icon.png"
}
}
}
main.js:
javascript
const { app, BrowserWindow,dialog,globalShortcut,ipcMain, Menu } = require('electron')
const path = require('path')
const exec = require('child_process');
const fs = require('fs')
const request = require('request');
let mainWindow;
/**
* 启动 BoltDB Api
*/
// exec.spawn('ext/opt')
/**
* 加載程序
*/
app.whenReady().then(() => {
createWindow()
})
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
/**
* 關閉頁面,退出程序
*/
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
//程序主菜单
const template = [
]
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
//右键菜单
ipcMain.handle('saveimg',async (event,args)=>{
// console.log(args)
if (!args.src) {
return;
}
let fName = 'aaa.jpg';
let id = args.id;
if (id) {
const index = id.lastIndexOf('_');
if (index > -1 && id.length>index+1) {
fName = id.substring(0, index) + '.' + id.substring(index + 1);
}
}
let template = [
{
label:'保存图片',
enabled:true,
click:()=>{
saveImage(mainWindow,args.src,fName)
}
}
]
let menu = Menu.buildFromTemplate(template);
menu.popup()
});
/**
* 創建窗體
*/
function createWindow () {
mainWindow = new BrowserWindow({
width: 1400,
height: 975,
title:"Face Recognition System Tool",
webPreferences: {
nodeIntegration:false,
contextIsolation: true,
enableRemoteModule: true,
sandbox: false,
preload: path.join(__dirname, './resources/index_preload.js'),
},
// transparent: true,
resizable:true,
})
//加載頁面
// mainWindow.loadFile(path.join(__dirname, '/resources/faceSearchLogImage.html?country=cn'))
mainWindow.loadURL(path.join(__dirname, '/resources/dashboard.html'))
//註冊F11開發者工具快捷鍵
globalShortcut.register('F11', function () {
mainWindow.webContents.openDevTools();
})
globalShortcut.register('F1', function () {
dialog.showMessageBox({
type: 'info',
message: '這是幫助,但是沒有內容!',
buttons: ['知道了']
})
})
// 引入的自定义菜单
// require("menu.js")
}
/**
* 切换主菜单
* @param {菜单体} data
* @param {菜单标题} label
*/
function changeUrl(data, label) {
mainWindow.loadURL(data.route)
}
/**
* 右键保存图片
* @param {主窗体} mainWindow
* @param {图片src} imageSrc
*/
async function saveImage(mainWindow, imageSrc, fName) {
try {
// console.log(imageSrc)
let stream;
if (imageSrc.indexOf('http://')>0||imageSrc.indexOf('https://')>0) {
const fileName = imageSrc.substr(imageSrc.lastIndexOf('/')+1);
const {filePath} = await dialog.showSaveDialog(mainWindow,{
title:'保存',
defaultPath:path.join(app.getPath('downloads'),fileName),
})
if (!filePath) {
throw new Error('用户取消了保存操作');
}
let writeStream = fs.createWriteStream(filePath);
stream = request(imageSrc).pipe(writeStream);
stream.on('finish', function () {
console.log('保存成功:' + filePath);
stream.destroy(); // 关闭数据流
writeStream.close();
}).on('error', function (err) {
console.log('保存图片时出错:' + err);
stream.destroy(); // 关闭数据流
writeStream.close();
});
}else{
// const mimeType = imageSrc.match(/data:(.*?);/);
// const extension = mimeType.split('/');
// console.log(mimeType)
const {filePath} = await dialog.showSaveDialog(mainWindow,{
title:'保存',
defaultPath:path.join(app.getPath('downloads'),fName),
})
if (!filePath) {
throw new Error('用户取消了保存操作');
}
let imageStr = imageSrc.split(';base64,').pop();
let imageBuffer = Buffer.from(imageStr, 'base64');
// 写入文件
await fs.writeFile(filePath, imageBuffer,(res)=>{});
console.log('文件保存成功:', filePath);
}
} catch (error) {
console.error('保存图片时发生错误:',error.message);
}
}
预加载脚本 index_preload.js:
javascript
const { contextBridge, ipcRenderer, dialog,Menu } = require('electron')
const http = require('http');
const https = require('https')
const fs = require('fs')
const Ajv = require("ajv")
const request = require('request');
const axios = require('axios');
const { Pool, Client } = require('pg');
const { S3, S3Client, GetObjectCommand } = require("@aws-sdk/client-s3");
const schemasDir = "./schemas/"
const extDir = "./ext/"
const configs = new Map();
const s3Map = new Map();
const s3NameMap = new Map();
const apiMap = new Map();
const extApiMap = new Map();
const connMap = new Map();
const connNameList = [];
const fileDir = 'E:\\imageDown\\';
let records;
const { NodeHttpHandler } = require('@aws-sdk/node-http-handler');
const { Agent } = require('https'); // 用于配置 HTTPS 请求
// 配置 HTTPS 客户端
const httpsAgent = new Agent({
rejectUnauthorized: false, // 如果使用自签名证书,设置为 false 以禁用 SSL 验证
});
window.addEventListener('DOMContentLoaded', () => {
let addr = window.location.pathname;
// console.log(addr);
})
window.addEventListener('contextmenu',function(e) {
// console.log(e)
e.preventDefault;
ipcRenderer.invoke('saveimg',{
src:e.srcElement && e.srcElement.currentSrc?e.srcElement.currentSrc:'',
id:e.srcElement && e.srcElement.id?e.srcElement.id:''
}).then(r=>{
// console.log(r);
})
})
contextBridge.exposeInMainWorld('myApi', {
author: '纳兰瑞雪',
saveFile: function (filePath, data) {
let option = {
flags: 'a', // 追加模式
encoding: 'utf8', // 编码格式
fd: fs.openSync(filePath, 'a'), // 打开文件并获取文件描述符
autoClose: true, // 写入完毕后自动关闭文件
}
fs.writeFile(filePath, data, option, (err) => {
// if (err) throw err;
consoleLog('文件已保存'); // 在回调函数中处理保存文件后的操作
});
// // 创建一个可写流
// const writeStream = fs.createWriteStream(filePath, {
// flags: 'w', // 追加模式
// fd: fs.openSync(filePath, 'w'), // 打开文件并获取文件描述符
// autoClose: true, // 写入完毕后自动关闭文件
// });
// // 使用write方法写入数据
// writeStream.write(data);
// writeStream.end(); // 通知写入结束
},
//根据url下载图片
downloadImage: function (filePath, imageUrl) {
downloadImage(filePath, imageUrl);
},
init: function (configFileName) {
let recordFile = extDir + configFileName;
if (fs.existsSync(recordFile)) {
records = fs.readFileSync(recordFile, 'utf8')
records = JSON.parse(records)
if (records) {
for (let u of records) {
let configName = u['configName'],
apiUrl = u['apiUrl'],
extApiUrl = u['extApiUrl'],
endPoint = u['endPoint'],
key = u['key'],
secret = u['secret'],
bucket = u['bucket'],
user= u['db_user'],
password= u['db_password'],
host= u['db_host'],
port= u['db_port'],
database= u['db_database'];
//初始化数据库链接
if (user&&password&&host&&port&&database) {
connNameList.push(configName);
let dbPool = new Pool({
user: user,
password: password,
host: host,
port: port,
database: database
});
dbPool.connect((err, client, done) => {
if (err) {
console.log('connect query:' + err.message);
return;
}
let res = client.query('select now();')
res.then(r => {
console.log(configName + ' 数据库当前时间:' + r.rows[0]['now'].toISOString())
});
connMap.set(configName, client);
});
}
//初始化S3链接
if (endPoint&&key&&secret&&bucket) {
let options = {
endpoint: endPoint,
region: 'us-east-1',
credentials: {
accessKeyId: key,
secretAccessKey: secret,
},
requestHandler: new NodeHttpHandler({
httpsAgent, // 使用自定义的 HTTPS Agent
}),
};
let s3 = new S3Client(options);
s3Map.set(configName, s3)
s3NameMap.set(configName, bucket)
}
//初始化apiUrl
if (apiUrl) {
apiMap.set(configName, apiUrl);
}
//初始化extApiUrl
if (extApiUrl) {
extApiMap.set(configName, extApiUrl);
}
configs.set(configName, u);
}
}
}
},
//从S3下载图片
downloadImageFromS3: function (s3Name, bucket, objName) {
let s3Client = s3Map.get(s3Name);
if (s3Client) {
let params = {
Bucket: bucket,
Key: objName
};
s3Client.send(new GetObjectCommand(params), (err, data) => {
// console.log(data.Body)
if (err) {
consoleLog('文件下载失败:' + objName, err);
} else {
let filePath = fileDir + objName.replace('\\', '_').replace('/', '_');
let writeStream = fs.createWriteStream(filePath);
writeStream.on('error', (err) => {
console.error('写入文件时发生错误:', err);
});
data.Body.pipe(fs.createWriteStream(filePath))
.on("error", (err) => {
console.log(err)
})
.on("close", () => {
writeStream.close();
console.log('下载成功')
return true
})
}
});
} else {
console.log('选择的S3配置不正确:' + s3Name)
}
return false
},
//从本地路径获取imageStr
getImageStrFromPath: function (path){
let imageBuffer = fs.readFileSync(path);
// 将Buffer转为Base64
return imageBuffer.toString('base64');
},
//从网络url获取imageStr
getImageStrFromUrl: function (url){
return new Promise((resolve, reject) => {
let req = http.get(url, res=> {
let chunks = [];
let size = 0;
res.on("data", function (chunk) {
chunks.push(chunk);
size += chunk.length; //累加缓冲数据的长度
});
res.on("end", err=> {
//Buffer.concat()方法将chunks中的所有缓冲区对象合并为一个缓冲区对象
let data = Buffer.concat(chunks, size);
base64Img = data.toString("base64");
resolve(base64Img);
});
});
req.on('error', (e) => {
resolve(e.message);
});
req.end();
});
},
//使用request模块发送请求
sendRequest(url,method,body,contentType){
const options = {
method: method,
url: url,
async: false,
headers: {
'Content-Type': contentType
},
body: body,
agentOptions: {
rejectUnauthorized: false,
}
};
return new Promise((resolve,reject)=>{
request(options,(error, response, body) => {
if (error) reject(error);
resolve(body)
})
})
},
//使用axios模块发送请求,electron架构中使用时,axios会默认使用浏览器环境,即xhr发送请求,造成https.Agent设置无效
sendRequestAxios(url,method,data,contentType){
const insecureAgent = new https.Agent({
rejectUnauthorized: false, // 不拒绝未经授权的证书(关键)
// 可选:明确忽略证书名称不匹配的错误
// 有些环境可能需要这个
checkServerIdentity: (host, cert) => {
// 你可以在这里自定义主机名验证逻辑,或者直接不抛出错误
// 如果想完全忽略,直接返回 undefined
return undefined;
}
});
return axios({
method: method,
url: url,
data: data, // form data 需要不同处理:new URLSearchParams({ key: 'value' }
timeout: 5000,
headers: {
'Content-Type': contentType
},
httpsAgent: insecureAgent
// httpsAgent: new https.Agent({
// rejectUnauthorized: false,
// maxVersion: 'TLSv1.2' // 如果服务器只支持 TLS 1.2
// })
});
},
//使用axios模块发送请求,使用自定义adapter,强制axios使用node.js环境发送请求,解决electron环境无法使用https.Agent配置发送https请求的问题; 并且配置自动跟随重定向50次
sendRequestAxios2(url, method, data, contentType) {
const https = require('https');
const http = require('http');
const { URL } = require('url');
const { default: axios } = require('axios');
// 创建一个支持重定向的自定义适配器
function createRedirectingAdapter(maxRedirects = 50) {
return function(config) {
return new Promise((resolve, reject) => {
let redirectCount = 0;
let currentUrl = config.url;
let currentMethod = config.method.toUpperCase();
let currentData = config.data;
let currentHeaders = { ...config.headers };
// 移除 Content-Length,因为重定向时可能会改变
delete currentHeaders['content-length'];
function makeRequest() {
const urlObj = new URL(currentUrl);
const isHttps = urlObj.protocol === 'https:';
const transport = isHttps ? https : http;
const options = {
hostname: urlObj.hostname,
port: urlObj.port || (isHttps ? 443 : 80),
path: urlObj.pathname + urlObj.search,
method: currentMethod,
headers: currentHeaders
};
if (isHttps) {
options.agent = new https.Agent({
rejectUnauthorized: false
});
}
// 设置超时
const reqTimeout = setTimeout(() => {
req.destroy();
reject(new Error('Request timeout'));
}, config.timeout || 5000);
const req = transport.request(options, (res) => {
clearTimeout(reqTimeout);
let responseData = '';
res.on('data', chunk => responseData += chunk);
res.on('end', () => {
// 检查是否为重定向状态码
const status = res.statusCode;
const location = res.headers.location;
// 如果是重定向且有 Location 头,且未超过最大重定向次数
if ((status === 301 || status === 302 || status === 307) &&
location &&
redirectCount < maxRedirects) {
redirectCount++;
// 更新当前 URL(处理相对路径)
try {
currentUrl = new URL(location, currentUrl).toString();
} catch (e) {
currentUrl = location;
}
// 对于 301/302,POST 请求通常改为 GET 并移除请求体
if ((status === 301 || status === 302) && currentMethod === 'POST') {
currentMethod = 'GET';
currentData = undefined;
// 移除 Content-Type 头,因为 GET 没有 body
delete currentHeaders['content-type'];
}
// 递归调用,继续请求
console.log(`[重定向 ${redirectCount}/${maxRedirects}] ${status} -> ${currentUrl}`);
makeRequest();
} else if (redirectCount >= maxRedirects) {
reject(new Error(`超过最大重定向次数: ${maxRedirects}`));
} else {
// 不是重定向或不需要跟随,返回最终响应
const response = {
data: responseData,
status: status,
statusText: res.statusMessage,
headers: res.headers,
config: {
...config,
url: currentUrl,
method: currentMethod
},
request: req
};
resolve(response);
}
});
});
req.on('error', (err) => {
clearTimeout(reqTimeout);
reject(err);
});
if (currentData) {
req.write(currentData);
}
req.end();
}
// 开始第一次请求
makeRequest();
});
};
}
// 创建使用自定义适配器的 axios 实例
const instance = axios.create({
adapter: createRedirectingAdapter(50),
timeout: 5000
});
return instance({
method: method,
url: url,
data: data,
headers: {
'Content-Type': contentType || 'application/json'
}
});
}
})
function downloadImage(filePath, imageUrl) {
let writeStream = fs.createWriteStream(filePath);
let stream = request(imageUrl).pipe(writeStream);
stream.on('finish', function () {
console.log('寫入結束:' + filePath);
stream.destroy(); // 关闭数据流
writeStream.close();
}).on('error', function (err) {
console.log('保存图片时出错:' + err);
stream.destroy(); // 关闭数据流
});
}