机缘
机缘巧合之下获取到一个桌面端开发的任务。
为了最快的上手速度,最低的开发成本,选择了electron。
介绍
Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux------不需要本地开发 经验。
主要结构
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 // 返回实际长度 = 消息头长度 + 消息体长度
}
}
打完收工