electron基础配置

机缘

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

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

介绍

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

主要结构

相关文章1 相关文章2

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

安装方式

js 复制代码
npm i electron-vite -D

electron-vite分为3层结构

js 复制代码
main // electron主进程
preload // electron预加载进程 node 
renderer // electron渲染进程 vue

创建项目

js 复制代码
npm create @quick-start/electron

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

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

node的引入

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

main主进程中的简单配置

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

配置数据库

sequelize为例

js 复制代码
npm install --save sequelize
npm install --save sqlite3

做本地应用使用推荐sqlite3,使用本地数据库

electron中会编译C++代码,会存在一些兼容性问题,如果一直尝试还是报错就换版本吧electron-vite新版本问题不大,遇到过老版本一直编译失败的问题

测试能让用版本

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

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

js 复制代码
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日志

js 复制代码
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文件下监听

js 复制代码
    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

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

js 复制代码
provider: generic
url: 'http://localhost:7070/urfiles'
updaterCacheDirName: 111-updater
js 复制代码
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文件中添加

js 复制代码
Object.defineProperty(app, 'isPackaged', {
   get() {
     return true
   }
})

接口封装

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

前端

js 复制代码
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)
    }
  })
}

后端

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

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

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

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

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

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

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

本地图片读取

js 复制代码
  // 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

js 复制代码
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

js 复制代码
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 // 返回实际长度 = 消息头长度 + 消息体长度
    }
  }

打完收工

相关推荐
y先森2 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy2 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189112 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿3 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡4 小时前
commitlint校验git提交信息
前端
虾球xz4 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇4 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒5 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员5 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐5 小时前
前端图像处理(一)
前端