一、背景
有一款electron开发的桌面应用,因为包含即时通讯的功能,所以在聊天消息中会有很多的图片视频消息以及会话的头像等,这些图片视频占用了大量的网络资源,领导要求优化一下,将图片和视频缓存至本地。
二、步骤
1.获取所有的图片视频请求
经过调研发现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 () => {
// 在这里调用
})
2.将图片和视频存储至本地
这里我使用的是node的request模块。在监听到请求后,将获取到的图片视频请求地址,通过request模块下载至本地。在这之前先创建本应用的本地文件缓存地址。这里有个坑,在打包之后,如果在应用内生成文件夹,windows系统会报错,因为应用没有访问权限,不能进行文件的操作。这里有两种解决方法:
1.方法一(不推荐) 因为我是通过electron-builder构建的应用,可以在打包配置里面添加 如下代码,将打包的应用等级提升为管理员权限,这样打好包安装之后运行,默认是以管理员身份运行的。
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`)
})
2.方法二(推荐!!!) 因为不能拖拽,会影响文件上传等功能,致使用户体验非常不好,之后又找到electron文档中,有一个方法 app.getPath(name) 可以使用。electron官方文档中对于文件位置的方法
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) + '.' + extconst 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}, '')
}
})
}
})
这样 我们就已经将图片和视频存储至本地了。
3.将文件信息存入本地数据库
我们在上一步已经将图片视频的文件缓存至本地了, 现在我们怎么使用他们呢?首先我们要将这些图片和视频存至本地数据库中,这里我用的是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为keywindow.$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)})
}
这里我们就完成了在系统中的文件数据的存储
4.在渲染进程展示本地图片
首先在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 = 0for (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-objectvar 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 = 0const 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) returnif (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)
})
以上功能就补全了, 形成了闭环,完结撒花。如有不足之处,请指出,谢谢。
最后,整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。
有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享
部分文档展示:



文章篇幅有限,后面的内容就不一一展示了
有需要的小伙伴,可以点下方卡片免费领取