有一款electron开发的桌面应用,因为包含即时通讯的功能,所以在聊天消息中会有很多的图片视频消息以及会话的头像等,这些图片视频占用了大量的网络资源,领导要求优化一下,将图片和视频缓存至本地。
经过调研发现electron的session模块有关于本应用的所有的web请求的监听方法。可参考
electron官方文档链接-WebRequest
于是乎,我们可以用这个方法获取到所有的图片视频的请求了
// 这里filter参数是为了筛选过滤哪些url的请求,
session.defaultSession.webRequest.onCompleted(filter, (details) => {
// 这里可以通过details.resourceType判断请求的是否为图片类型,这里也获取了other,是因为视频也会存在在other中
if ((details.resourceType === 'image' || details.resourceType === 'other')) {
// 获取请求地址
const souceUrl = details.url
}
})
注意这个监听一定要在app.ready之后调用
app.on('ready', async () => {
// 在这里调用
})
这里我使用的是node的request模块。在监听到请求后,将获取到的图片视频请求地址,通过request模块下载至本地。在这之前先创建本应用的本地文件缓存地址。这里有个坑,在打包之后,如果在应用内生成文件夹,windows系统会报错,因为应用没有访问权限,不能进行文件的操作。这里有两种解决方法:
builderOptions: {
...
win: {
...
requestedExecutionLevel: 'highestAvailable'
}
}
但是这种方法有个缺陷,以管理员身份运行的程序,在windows系统中,是不允许文件往里面拖拽的。
设置好之后,创建文件夹
import fs from 'fs'
const path = require('path')
const log = require('electron-log') // 记录日志(如有需要安装)
// 设置存放缓存文件的文件夹
const AVATARPATH = 'temp'
// 设置文件夹位置(在安装应用文件夹内)
const basePath = path.join(__dirname, AVATARPATH)
fs.mkdir(basePath, { recursive: true }, err => {
if (err) log.warn(`mkdir path: ${basePath} err`)
})
app.getPath('userData')
如官方文档所写:这个路径用于存储应用程序配置文件的目录,默认情况下是附加应用程序名称的appData目录。按照惯例,存储用户数据的文件应写入此目录,不建议在此写入大文件,因为某些环境可能会将此目录备份到云存储。
因此我们将文件夹创建在这个目录下,就不需要担心windows系统的文件权限问题了, 也就不需要**requestedExecutionLevel: ‘highestAvailable’**这个配置了。
import fs from 'fs'
const path = require('path')
const log = require('electron-log') // 记录日志(如有需要安装)
// 设置存放缓存文件的文件夹
const AVATARPATH = 'temp'
// 设置文件夹位置(在安装应用文件夹内)
const basePath = path.join(app.getPath('userData'), AVATARPATH)
fs.mkdir(basePath, { recursive: true }, err => {
if (err) log.warn(`mkdir path: ${basePath} err`)
})
这里我们就将第一步创建图片视频缓存文件夹做好了。
之后我们就可以将文件通过request下载至本地了
const request = require('request')
session.defaultSession.webRequest.onCompleted(filter, (details) => {
// 这里可以通过details.resourceType判断请求的是否为图片类型,这里也获取了other,是因为视频也会存在在other中
if ((details.resourceType === 'image' || details.resourceType === 'other')) {
// 获取请求地址
const souceUrl = details.url
let ext = '' // 文件类型
// 设置存储的文件的类型
const filterArr = ['webp', 'jpg', 'jpeg', 'png', 'bmp', 'gif', 'svg', 'mp4', 'wmv']
// ======= 获取文件类型start (根据不同的souceUrl,类型获取方式可能不同) ========
const index = souceUrl.lastIndexOf('.')
ext = souceUrl.substr(index + 1)
// ======= 获取文件类型end (根据不同的souceUrl,类型获取方式可能不同) ========
// 若不是需要存储的文件类型,则不进行以后得步骤
if (!filterArr.includes(ext.toLowerCase())) return
// 这里的uuid是随机生成的字符串(可以自己另寻方法,不做展示)
let filename = uuid(8, 16) + '.' + ext
const req = request({ method: 'GET', uri: souceUrl })
req.pipe(fs.createWriteStream(path.join(basePath, filename)))
// 文件大小
var total = 0
req.on('response', (data) => {
total = parseInt(data.headers['content-length'])
})
req.on('data', (chunk) => {})
req.on('error', (error) => {
log.warn('error====req', error)
})
req.on('end', () => {
// log.warn(uuid(8, 16), 'end', path.join(basePath, filename))
// 获取存储成功后本地路径
let localPath = path.join(basePath, filename)
if (process.platform !== 'darwin') { // 这里判断是否为windows系统,windows系统需要//这种反斜杠才能展示
const arr = localPath.split(path.sep)
localPath = arr.reduce((pre, cue) => {return pre ? `${pre}//${cue}` : cue}, '')
}
})
}
})
这样 我们就已经将图片和视频存储至本地了。
我们在上一步已经将图片视频的文件缓存至本地了, 现在我们怎么使用他们呢?首先我们要将这些图片和视频存至本地数据库中,这里我用的是localforage。首先引入localforage插件。
yarn add localforage
// or
npm install localforage
之后在入口文件创建本地数据库实例。
// main.js
window.$ChatAvatarStore = localforage.createInstance({
name: 'ChatAvatarStore'
})
配置好数据库之后,我们将媒体文件的源路径,本地路径以及文件大小存进去,这里我们用到了electron的进程间的通讯,通过ipc将这些信息,从主进程传输给渲染进程,之后存进本地数据库。
// 【主进程】这里是上面调用request的end的回调之中,在图片保存完之后,再将数据传输给渲染进程
req.on('end', () => {
// ....
win.webContents.send('callbackAvatarPath', {
souceUrl: souceUrl,
localPath: localPath,
size: total
})
})
// main.js 这里我们现将sharedObject这个通用在渲染进程和主进程的存储工具放在入口文件定义,
// 还有ipcRenderer也全局定义在window上
window.ipcRenderer = window.require('electron').ipcRenderer
window.$_SO = window.$remote.getGlobal('sharedObject')
// home.vue【渲染进程】
// 存储远程图片至本地
window.ipcRenderer.on('callbackAvatarPath', (event, localObj) => {
// 存储至数据库,以souceUrl为key
window.$ChatAvatarStore.setItem(localObj.souceUrl, localObj).then(value => {
// 当值被存储后,可执行其他操作
// 存储至全局变量loaclImgs中
window.$_SO.loaclImgs.set(localObj.souceUrl, localObj)
}).catch(function (err) {
// 当出错时,此处代码运行
console.log(err)
})
})
// 每一次进入程序之后先同步本地图片信息
mounted() {
// 存储本地图片同步信息
window.$ChatAvatarStore.iterate((value, key, iterationNumber) => {
// 此回调函数将对所有 key/value 键值对运行
window.$_SO.loaclImgs.set(key, value)
})
}
这里我们就完成了在系统中的文件数据的存储
首先在electron应用中展示本地的图片或视频,我们需要定义一种协议去加载本地图片。这里我们用到了protocol这个模块electron官方文档中protocol模块说明。
// background.js
app.whenReady().then(() => {
// 这个需要在app.ready触发之后使用
protocol.registerFileProtocol('item', (request, callback) => {
const url = request.url.substr(7)
callback(decodeURI(path.normalize(url)))
})
})
这样我们就定义好了本地文件展示协议。
之后我们就在图片渲染的地方,进行拦截加载。以下举例
<img :src="getViewImgUrl(imgURl)" />
const fs = require('fs')
// util.js 放到工具文件中的通用方法
export const getViewImgUrl = (url) => {
// 查看本地数据库是否有缓存这个文件
const result = window.$_SO.loaclImgs.get(url)
if (result) { // 如果有
try {
// 判断图片是否还存在本地文件中
fs.accessSync(result.localPath, fs.constants.F_OK)
console.log('File does exist')
url = 'item:///' + result.localPath
} catch (err) {
// 若不存在,(有可能被人为删除),则清除这条记录
window.$_SO.loaclImgs.delete(url)
window.$ChatAvatarStore.removeItem(url)
console.error('File does not exist')
}
}
return url
}
这样 我们就是现实了electron的图片视频本地化缓存以及展示功能。
有了缓存之后,可能还需要清除缓存的功能。
// 在主进程中
// 监听获取temp文件大小
ipcMain.on('getTempSize', (event, arg) => {
// 遍历文件夹,获取所有文件夹里面的文件信息
const geFileList = (path) => {
var filesList = []
readFile(path, filesList)
let totalSize = 0
for (var i = 0; i < filesList.length; i++) {
var item = filesList[i]
totalSize += item.size
}
return totalSize
}
// 遍历读取文件
const readFile = (paths, filesList) => {
var files = fs.readdirSync(paths)// 需要用到同步读取
files.forEach(walk)
function walk (file) {
try {
var states = fs.statSync(path.join(paths, file))
if (states.isDirectory()) {
readFile(paths + '/' + file, filesList)
} else {
// 创建一个对象保存信息
// eslint-disable-next-line no-new-object
var obj = new Object()
obj.size = states.size// 文件大小,以字节为单位
obj.name = file// 文件名
obj.path = paths + '/' + file // 文件绝对路径
filesList.push(obj)
}
} catch (error) {
log.error('监听获取temp文件大小--被占用文件-----', error)
}
}
}
const AVATARPATH = 'temp'
const basePath = path.join(app.getPath('userData'), AVATARPATH)
console.log(1234, geFileList(basePath))
const size = (geFileList(basePath) / 1024 / 1024).toFixed(2)
// 回调给渲染进程结果
win.webContents.send('callbackTempSize', size)
})
// 清除缓存, 若不传 则全部清空, 若传 则清除次数目以上的大小的文件
ipcMain.on('delAllOrBigtemp', (e, arg) => {
// let cun = 0
const emptyDir = (paths) => {
const files = fs.readdirSync(paths)
files.forEach(file => {
const filePath = path.join(paths, file)
// const filePath = `${paths}/${file}`
try {
const stats = fs.statSync(filePath)
const fileSize = stats.size / 1024 / 1024
// console.log(1111, cun, file, fileSize.toFixed(2))
// cun++
if (fileSize > arg) {
console.log(file)
}
if (arg && fileSize < arg) return
if (stats.isDirectory()) {
emptyDir(filePath)
} else {
fs.unlinkSync(filePath)
console.log(`删除${file}文件成功`)
}
} catch (error) {
log.error('删除文件-被占用文件-----', error, filePath)
}
})
// console.log(222, cun)
}
const AVATARPATH = 'temp'
const basePath = path.join(app.getPath('userData'), AVATARPATH)
emptyDir(basePath)
})
以上功能就补全了, 形成了闭环,完结撒花。如有不足之处,请指出,谢谢。