本文为Electron+Vue3开发桌面端软件系列文章第四篇
通过本文,你将获得
- Electron中进程间通信
- chokidar处理文件实时更新
前言
对于今日清单这款软件,体量小需求简单。我在设计之初,想要的就是本地化使用的场景,因此在技术实现上不会涉及到服务端、数据库,那又想要存储每日清单数据,只能采用本地文件存储的方案。
简单罗列一下需求:
- 当日创建首个待办项时创建文件,文件以当天日期为名
- 需要一个文件目录栏,读取日志文件夹
- 切换每日待办,读取相应文件内容进行展示
- 本地资源管理器中新增或删除文件,目录栏中可以实时更新
在Electron中文件处理都是依赖进程间通信 (IPC) ,可以先在官网中阅读这部分内容,了解一下相关的背景知识。
IPC:www.electronjs.org/zh/docs/lat...
创建今日待办
首次创建时,分成两部分新增逻辑。一个是当前界面中待办列表的数据新增,也就是操作list
,这里的list
就是展示的待办列表项数组;另一个是在指定文件夹下创建当日待办的文件,再将list
数据写入。
每项待办项的数据格式可自行定义,这取决于想在页面中显示什么内容。
新增 list
lua
list.value.push({
id: Math.random() * 100, // Unique key
name: '', // todo item
time: dayjs().format('MM-DD HH:mm:ss'), // create time
isEdit: true
})
那紧接着就是将新增的list
数据写入文件,
javascript
function handleExport() {
const content = list.value
.filter((item) => !item.isEdit)
.map((item, index) => {
const timeArr = [item.time.split(' ')[1], item.notify].filter((item) => item)
return `${item.isFinish ? '√' : '×'} ${index + 1}.${item.name} ->${timeArr}\n`
})
const time = fileTime.value || dayjs().format('YYYY-MM-DD')
window.electronFile.exportFile({
path: fileUrl.value,
time,
content: `${time}待办事项\n${content.join('')}`
})
emits('exportFile')
}
handleExport
方法中content
是对list
原始数据处理,处理成需要在本地文件中的展现形式,这就相当于两种数据格式的转换。
转换规则:
这里更关键的是electronFile.exportFile
,挂载在window
上的方法,就是通过Eelectron
进程间通信实现的。
需要在preload/index.js
预加载脚本中设置,使用 ipcRenderer.send
API 发送消息,
javascript
contextBridge.exposeInMainWorld('electronFile', {
exportFile: (data) => ipcRenderer.send('exportFile', data)
})
在主进程中使用 ipcMain.on API 接收,
javascript
ipcMain.on('exportFile', (_, data) => {
writeFile(data)
})
接收到Vue
应用也就是页面上传递过来的数据,刚刚在handleExport
方法中处理过的数据,
javascript
{
path: fileUrl.value,
time,
content: `${time}待办事项\n${content.join('')}`
}
writeFile
写入文件,使用node
实现,
javascript
import fs from 'node:fs'
import { join } from 'node:path'
export function writeFile({ path, time, content }) {
const filePath = join(path, `${time}.txt`)
fs.writeFileSync(filePath, content, function (err) {
if (err) {
console.log('err', err)
} else {
console.log('file success')
}
})
}
文件目录栏
需要展示文件目录栏,需要在指定目录下读取所有的文件进行展示。
原本的实现方案就是简单的使用node
的fs
模块读取文件,但是考虑到后续的一些需求,需要实时监听目录下文件变化,原生的fs
模块或者是fs.watch
,fs.watchFile
稍显不足。
这里推荐使用一个库chokidar
,查看 www.npmjs.com/package/cho... 进行了解。
依旧是使用进程间通信 (IPC)来实现,在Vue
应用APP.vue
中,
javascript
window.electronFile.readFileNames(url)
这里的url
是提前设定好的待办日志存放目录地址的路径。
在preload/index.js
预加载脚本中设置readFileNames
方法,
javascript
contextBridge.exposeInMainWorld('electronFile', {
readFileNames: (data) => ipcRenderer.send('readFileNames', data)
})
主进程main/index.js
中监听readFileNames
,
javascript
import { normalize } from 'node:path'
import chokidar from 'chokidar'
ipcMain.on('readFileNames', (fileEvent, path) => {
chokidar
.watch(path, {
persistent: true // 持续监听
})
.on('all', (event, path) => {
fileEvent.sender.send('directoryChanges', { event, path: normalize(path) })
})
})
上面代码中,在监听到指定目录下有变化时,fileEvent.sender.send
会发出一个消息事件directoryChanges
,将获取的文件数据暴露出来。
那在App.vue
这端的Vue
应用中可以通过ipcRenderer
来接收主进程回复的消息,并进行数据格式上的处理,就可以把文件中数据回显到页面文件目录栏里。
csharp
window.electron.ipcRenderer.on('directoryChanges', (_, data) => {
const { event, path } = data
const name = path.split('\')[path.split('\').length - 1]
const unit = name.split('.')[1]
const code = dayjs(name.split('.')[0]).valueOf()
const index = fileNames.value.findIndex((item) => item.code === code)
if (event == 'add' && index === -1 && unit === 'txt') {
fileNames.value.unshift({
name,
code
})
}
if (event == 'unlink' && index !== -1) {
fileNames.value.splice(index, 1)
}
fileNames.value.sort((a, b) => b.code - a.code)
})
那上述的实现方法都封装在一个函数getFileNames
里,那何时去触发这个函数呢?换言之,什么时候监听到文件夹下有文件变化?来更新目录栏数据呢。
其实在readFileNames
方法里是需要指定文件夹路径url
参数的,可以监听这个url
,在watch
中执行,同时会读取文件内容getFileContent
。
scss
watch(
fileUrl,
(url) => {
if (url) {
fileNames.value = []
getFileNames(url)
getFileContent()
}
},
{ immediate: true }
)
总结
Electron应用相当一个套壳的Vue应用(也可以是React,原生,或者其他...),它解决了Vue应用实现客户端的问题,例如可以依托node
对于本地文件处理。在 Electron 中,进程使用 ipcMain
和 ipcRenderer
模块,通过定义的通道传递消息来进行通信。那这过程中也不能直接通信,需要使用预加载脚本在上下文隔离渲染器进程中导入 Node.js 和 Electron 模块。
单向传递消息 ,渲染器进程到主进程:使用 ipcRenderer.send
发送消息,然后使用 ipcMain.on
接收。
例如:获取所有文件名称
双向传递消息 ,从渲染器进程代码调用主进程模块并等待结果,这可以通过将 ipcRenderer.invoke
与 ipcMain.handle
搭配使用来完成。
例如:打开指定文件夹