likes
comments
collection
share

桌面端Electron基础配置

作者站长头像
站长
· 阅读数 12

机缘

机缘巧合之下获取到一个桌面端开发的任务。

为了最快的上手速度,最低的开发成本,选择了electron。

桌面端Electron基础配置

介绍

Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需要本地开发 经验。

主要结构

相关文章1 相关文章2

桌面端Electron基础配置

electron主要有一个主进程和一个或者多个渲染进程组成,方便的脚手架项目有 electron-vite

安装方式

npm i electron-vite -D

electron-vite分为3层结构

main // electron主进程
preload // electron预加载进程 node 
renderer // electron渲染进程 vue

创建项目

npm create @quick-start/electron

项目创建完成启动之后 会在目录中生成一个out目录

桌面端Electron基础配置

out目录中会生成项目文件代码,在electron-vite中使用ESmodel来加载文件,启动的时候会被全部打包到out目录中合并在一起。所以一些使用CommonJs的node代码复制进来需要做些修改。npm安装的依赖依然可以使用CommonJs的方式引入。

node的引入

桌面端Electron基础配置 在前面的推荐的几篇文章中都有详细的讲解,无需多言。electron是以chrom+node,所以node的加入也非常的简单。 nodeIntegration: true,

main主进程中的简单配置

桌面端Electron基础配置

preload目录下引入node代码,留一个口子在min主进程中调用。

配置数据库

sequelize为例

npm install --save sequelize
npm install --save sqlite3

做本地应用使用推荐sqlite3,使用本地数据库,当然了用其他的数据也没问题,用法和node中一样。需要注意的是C++代码编译的问题,可能会存在兼容性问题,如果一直尝试还是报错就换版本吧。electron-vite新版本问题不大,遇到过老版本一直编译失败的问题

测试能让用版本

  • "electron": "^25.6.0",
  • "electron-vite": "^1.0.27",
  • "sequelize": "^6.33.0",

桌面端Electron基础配置

node-gyp vscode 这些安装环境网上找找也很多就不多说了。

import { Sequelize } from 'sequelize'
import log from '../config/log/log'

const path = require('path')

let documentsPath

if (process.env['ELECTRON_RENDERER_URL']) {
  documentsPath = './out/config/sqlite/sqlite.db'
} else {
  documentsPath = path.join(process.env.USERPROFILE, 'Documents') + '\\sqlite\\sqlite.db'
}

console.log('documentsPath-------------****-----------', documentsPath)

export const seq = new Sequelize({
  dialect: 'sqlite',
  storage: documentsPath
})

seq
  .authenticate()
  .then(() => {
    log.info('数据库连接成功')
  })
  .catch((err) => {
    log.error('数据库连接失败' + err)
  })

终端乱码问题

"dev:win": "chcp 65001 && electron-vite dev", chcp 65001只在win环境下添加

electron多页签

electron日志

import logger from 'electron-log'

logger.transports.file.level = 'debug'
logger.transports.file.maxSize = 30 * 1024 * 1024 // 最大不超过10M
logger.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}]{scope} {text}' // 设置文件内容格式

var dayjs = require('dayjs')
const date = dayjs().format('YYYY-MM-DD') // 格式化日期为 yyyy-mm-dd

logger.transports.file.fileName = date + '.log' // 创建文件名格式为 '时间.log' (2023-02-01.log)

// 可以将文件放置到指定文件夹中,例如放到安装包文件夹中
const path = require('path')
let logsPath

if (process.env['ELECTRON_RENDERER_URL']) {
  logsPath = './out/config/logs/' + date + '.log'
} else {
  logsPath = path.join(process.env.USERPROFILE, 'Documents') + '\\logs\\' + date + '.log'
}

console.log('logsPath-------------****-----------', logsPath) // 获取到安装目录的文件夹名称

// 指定日志文件夹位置
logger.transports.file.resolvePath = () => logsPath

// 有六个日志级别error, warn, info, verbose, debug, silly。默认是silly
export default {
  info(param) {
    logger.info(param)
  },
  warn(param) {
    logger.warn(param)
  },
  error(param) {
    logger.error(param)
  },
  debug(param) {
    logger.debug(param)
  },
  verbose(param) {
    logger.verbose(param)
  },
  silly(param) {
    logger.silly(param)
  }
}

对应用做好日志维护是一个很重要的事情

主进程中也可以在main文件下监听

    app.on('activate', function () {
      // On macOS it's common to re-create a window in the app when the
      // dock icon is clicked and there are no other windows open.
      if (BrowserWindow.getAllWindows().length === 0) createWindow()
    })

    // 渲染进程崩溃
    app.on('renderer-process-crashed', (event, webContents, killed) => {
      log.error(
        `APP-ERROR:renderer-process-crashed; event: ${JSON.stringify(event)}; webContents:${JSON.stringify(
          webContents
        )}; killed:${JSON.stringify(killed)}`
      )
    })

    // GPU进程崩溃
    app.on('gpu-process-crashed', (event, killed) => {
      log.error(`APP-ERROR:gpu-process-crashed; event: ${JSON.stringify(event)}; killed: ${JSON.stringify(killed)}`)
    })

    // 渲染进程结束
    app.on('render-process-gone', async (event, webContents, details) => {
      log.error(
        `APP-ERROR:render-process-gone; event: ${JSON.stringify(event)}; webContents:${JSON.stringify(
          webContents
        )}; details:${JSON.stringify(details)}`
      )
    })

    // 子进程结束
    app.on('child-process-gone', async (event, details) => {
      log.error(`APP-ERROR:child-process-gone; event: ${JSON.stringify(event)}; details:${JSON.stringify(details)}`)
    })

应用更新

在Electron中实现自动更新,需要使用electron-updater

npm install electron-updater --save

需要知道服务器地址,单版本号有可更新内容的时候可以通过事件监听控制更新功能

provider: generic
url: 'http://localhost:7070/urfiles'
updaterCacheDirName: 111-updater

import { autoUpdater } from 'electron-updater'
import log from '../config/log/log'
export const autoUpdateInit = (mainWindow) => {
  let result = {
    message: '',
    result: {}
  }
  autoUpdater.setFeedURL('http://localhost:50080/latest.yml')

  //设置自动下载
  autoUpdater.autoDownload = false
  autoUpdater.autoInstallOnAppQuit = false

  // 监听error
  autoUpdater.on('error', function (error) {
    log.info('检测更新失败' + error)
    result.message = '检测更新失败'
    result.result = error
    mainWindow.webContents.send('update', JSON.stringify(result))
  })

  // 检测开始
  autoUpdater.on('checking-for-update', function () {
    result.message = '检测更新触发'
    result.result = ''
    // mainWindow.webContents.send('update', JSON.stringify(result))
    log.info(`检测更新触发`)
  })

  // 更新可用
  autoUpdater.on('update-available', (info) => {
    result.message = '有新版本可更新'
    result.result = info
    mainWindow.webContents.send('update', JSON.stringify(result))
    log.info(`有新版本可更新${JSON.stringify(info)}${info}`)
  })

  // 更新不可用
  autoUpdater.on('update-not-available', function (info) {
    result.message = '检测更新不可用'
    result.result = info
    mainWindow.webContents.send('update', JSON.stringify(result))
    log.info(`检测更新不可用${info}`)
  })

  // 更新下载进度事件
  autoUpdater.on('download-progress', function (progress) {
    result.message = '检测更新当前下载进度'
    result.result = progress
    mainWindow.webContents.send('update', JSON.stringify(result))
    log.info(`检测更新当前下载进度${JSON.stringify(progress)}${progress}`)
  })

  // 更新下载完毕
  autoUpdater.on('update-downloaded', function () {
    //下载完毕,通知应用层 UI
    result.message = '检测更新当前下载完毕'
    result.result = {}
    mainWindow.webContents.send('update', result)
    autoUpdater.quitAndInstall()
    log.info('检测更新当前下载完毕,开始安装')
  })
}

export const updateApp = (ctx) => {
  let message
  if (ctx.params == 'inspect') {
    console.log('检测是否有新版本')
    message = '检测是否有新版本'

    autoUpdater.checkForUpdates() // 开始检查是否有更新
  }
  if (ctx.params == 'update') {
    message = '开始更新'
    autoUpdater.downloadUpdate() // 开始下载更新
  }
  return (ctx.body = {
    code: 200,
    message,
    result: {
      currentVersion: 0
    }
  })
}

dev下想测试更新功能,可以在主进程main文件中添加

Object.defineProperty(app, 'isPackaged', {
   get() {
     return true
   }
})

接口封装

eletron中可以像node一样走http的形式编写接口,但是更推荐用IPC走内存直接进行主进程和渲染进程之间的通信

前端

import { ElMessage } from 'element-plus'
import router from '../router/index'

export const getApi = (url: string, params: object) => {
  return new Promise(async (resolve, rej) => {
    try {
      console.log('-------------------url+params', url, params)

      // 如果有token的话
      let token = sessionStorage.getItem('token')
      // 走ipc
      if (window.electron) {
        const res = await window.electron.ipcRenderer.invoke('getApi', JSON.stringify({ url, params, token }))
        console.log('res', res)
        if (res?.code == 200) {
          return resolve(res.result)
        } else {
          // token校验不通过退出登录
          if (res?.error == 10002 || res?.error == 10002) {
            router.push({ name: 'loginPage' })
          }
          // 添加接口错误的处理
          ElMessage.error(res?.message || res || '未知错误')
          rej(res)
        }
      } else {
        // 不走ipc

      }
    } catch (err) {
      console.error(url + '接口请求错误----------', err)
      rej(err)
    }
  })
}

后端

ipcMain.handle('getApi', async (event, args) => {
    const { url, params, token } = JSON.parse(args)
    // 
})

electron官方文档中提供的IPC通信的API有好几个,每个使用的场景不一样,根据情况来选择

node中使用的是esmodel和一般的node项目写法上还有些区别,得适应一下。

容易找到的都是渲染进程发消息,也就是vue发消息给node,但是node发消息给vue没有写

这时候就需要使用webContents方法来实现

  this.mainWindow.webContents.send('receive-tcp', JSON.stringify({ code: key, data: res.data }))

使用webContents的时候在vue中一样是通过事件监听‘receive-tcp’事件来获取

本地图片读取

  // node中IO操作是异步所以得订阅一下
  const subscribeImage = new Promise((res, rej) => {
    // 读取图片文件进行压缩
    sharp(imagePath)
      .webp({ quality: 80 })
      .toBuffer((err, buffer) => {
        if (err) {
          console.error('读取本地图片失败Error converting image to buffer:', err)
          rej(
            (ctx.body = {
              error: 10003,
              message: '本地图片读取失败'
            })
          )
        } else {
          log.info(`读取本地图片成功:${ctx.params}`)
          res({
            code: 200,
            msg: '读取本地图片成功:',
            result: buffer.toString('base64')
          })
        }
      })
  })

TCP

既然写了桌面端,那数据交互的方式可能就不局限于http,也会有WS,TCP,等等其他的通信协议。

node中提供了Tcp模块,net

const net = require('net')
const server = net.createServer()

server.on('listening', function () {
      //获取地址信息
      let addr = server.address()
      tcpInfo.TcpAddress = `ip:${addr.port}`
      log.info(`TCP服务启动成功---------- ip:${addr.port}`)
})
//设置出错时的回调函数
server.on('error', function (err) {
  if (err.code === 'EADDRINUSE') {
    console.log('地址正被使用,重试中...')
    tcpProt++
    setTimeout(() => {
      server.close()
      server.listen(tcpProt, 'ip')
    }, 1000)
  } else {
    console.error('服务器异常:', err)
  }
})

TCP链接成功获取到数据之后在data事件中,就可以使用webContents方法来主动传递消息给渲染进程 也得对Tcp数据包进行解析,一般都是和外部系统协商沟通的数据格式。一般是十六进制或者是二进制数据,需要对数据进行解析,切割,缓存。 使用 Bufferdata = Buffer.concat([overageBuffer, data]) 对数据进行处理 根据数据的长度对数据进行切割,判断数据的完整性质,对数据进行封包和拆包

粘包处理网上都有 处理完.toString()一下 over

socket.on('data', async (data) => {
    ...
    let buffer = data.slice(0, packageLength) // 取出整个数据包
    data = data.slice(packageLength) // 删除已经取出的数据包
    // 数据处理
    let key = buffer.slice(4, 8).reverse().toString('hex')
    console.log('data', key, buffer)
    let res = await isFunction[key](buffer)
    this.mainWindow.webContents.send('receive-tcpData', JSON.stringify({ code: key, data: res.data }))
})


// 获取包长度的方法
  getPackageLen(buffer) {
    let bufferCopy = Buffer.alloc(12)
    buffer.copy(bufferCopy, 0, 0, 12)
    let bufferSize = bufferCopy.slice(8, this.headSize).reverse().readInt32BE(0)
    console.log('bufferSize', bufferSize, bufferSize + this.headSize, buffer.length)
    if (bufferSize > buffer.length - this.headSize) {
      return -1
    }
    if (buffer.length >= bufferSize + this.headSize) {
      return bufferSize + this.headSize // 返回实际长度 = 消息头长度 + 消息体长度
    }
  }

打完收工

桌面端Electron基础配置

转载自:https://juejin.cn/post/7338265878289301567
评论
请登录