nodeJs electron程式开发demo

本文介绍了一个使用Node.js开发Electron跨平台桌面应用的简单配置方案。主要功能包括:

  1. 核心配置:
  • 基于Electron 25.9.8构建
  • 使用electron-builder进行打包
  • 支持Windows 32位/64位平台
  1. 主要功能实现:
  • HTTP/HTTPS请求处理(使用axios和request模块)
  • AWS S3文件下载功能(@aws-sdk/client-s3)
  • PostgreSQL数据库连接(pg模块)
  • 图片下载和保存功能
  • 自定义右键菜单
  1. 技术亮点:
  • 预加载脚本(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(); // 关闭数据流
  });
}
相关推荐
奔波霸的伶俐虫1 小时前
redisTemplate.opsForList()里面方法怎么用
java·开发语言·数据库·python·sql
EndingCoder1 小时前
函数基础:参数和返回类型
linux·前端·ubuntu·typescript
码客前端1 小时前
理解 Flex 布局中的 flex:1 与 min-width: 0 问题
前端·css·css3
Komorebi゛1 小时前
【CSS】圆锥渐变流光效果边框样式实现
前端·css
yesyesido1 小时前
智能文件格式转换器:文本/Excel与CSV无缝互转的在线工具
开发语言·python·excel
_200_1 小时前
Lua 流程控制
开发语言·junit·lua
环黄金线HHJX.1 小时前
拼音字母量子编程PQLAiQt架构”这一概念。结合上下文《QuantumTuan ⇆ QT:Qt》
开发语言·人工智能·qt·编辑器·量子计算
王夏奇1 小时前
python在汽车电子行业中的应用1-基础知识概念
开发语言·python·汽车
He_Donglin1 小时前
Python图书爬虫
开发语言·爬虫·python
工藤学编程1 小时前
零基础学AI大模型之CoT思维链和ReAct推理行动
前端·人工智能·react.js