一、背景

有一款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`)
})
  1. 方法二(推荐!!!
    因为不能拖拽,会影响文件上传等功能,致使用户体验非常不好,之后又找到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) + '.' + 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}, '')
	      }
	    })
	}
})

这样 我们就已经将图片和视频存储至本地了。

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为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)
    })
}

这里我们就完成了在系统中的文件数据的存储

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的图片视频本地化缓存以及展示功能。

electronjs通过内存传递图片数据给python_vue.js

三、后记

有了缓存之后,可能还需要清除缓存的功能。

// 在主进程中
// 监听获取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)
})

以上功能就补全了, 形成了闭环,完结撒花。如有不足之处,请指出,谢谢。