注册

桌面端Electron基础配置

机缘


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


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


image.png


介绍


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


主要结构


相关文章1
相关文章2


image.png


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目录


image.png


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


node的引入


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


main主进程中的简单配置


image.png


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",

image.png


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

打完收工


image.png


作者:Alkaid_z
来源:juejin.cn/post/7338265878289301567

0 个评论

要回复文章请先登录注册