Electron-vue开发实战2——引入基于Lodash的JSON数据库lowdb

前言

前段时间,我用electron-vue开发了一款跨平台(目前支持Mac和Windows)的免费开源的图床上传应用——PicGo,在开发过程当中踩了很多的坑,不只来自应用的业务逻辑自己,也来自electron自己。在开发这个应用过程当中,我学了很多的东西。由于我也是从0开始学习electron,因此不少经历应该也能给初学、想学electron开发的同窗们一些启发和指示。故而写一份Electron的开发实战经历,用最贴近实际工程项目开发的角度来阐述。但愿能帮助到你们。vue

预计将会从几篇系列文章或方面来展开:node

  1. electron-vue入门
  2. Main进程和Renderer进程的简单开发
  3. 引入基于Lodash的JSON database——lowdb
  4. 跨平台的一些兼容措施
  5. 经过CI发布以及更新的方式
  6. ...(想到再写)

说明

PicGo是采用electron-vue开发的,因此若是你会vue,那么跟着一块儿来学习将会比较快。若是你的技术栈是其余的诸如reactangular,那么纯按照本教程虽然在render端(能够理解为页面)的构建可能学习到的东西很少,不过在main端(electron的主进程)应该仍是能学习到相应的知识的。react

若是以前的文章没阅读的朋友能够先从以前的文章跟着看。ios

数据持久化存储的必要性

不像平时不少人写的一些demo,就是请求一下api而后把web页面展现出来就了事了。electron应用毕竟是个桌面级应用,若是思惟还留在纯web开发的思路上,那么也就失去了用electron的意义了吧。git

数据持久化存储实际上对于后端很熟悉。一般是指的是把内存里的数据以不一样的存储模型存储到磁盘上,在须要的时候再从存储模型里读取读入内存中的整个流程。这里面的存储模型一般就是咱们熟悉的数据库。说到数据库,不少人会想到MySQL,Mongodb,SQLite等等。常见的这些数据库都是Server-Client模式的,须要启动服务端——一般咱们装的就是这个。可是你通常不多见到叫别人装个桌面软件的同时,叫别人配数据库的吧。github

由于有些数据咱们必须在本地存下来,方便下次使用的时候读取。而对于electron来讲,既然让用户装MySQL、Mongodb是不太优雅的解决办法的话,那么若是能用其余方式,将数据存到本地而不用用户操心如何存储的,对咱们和用户来讲都是一件好事。web

纯JavaScript数据库的选择

既然是JS技术栈的,因而我就找了一些纯JavaScript实现的数据库。通过初步筛选,我找到以下两个:数据库

  1. nedb 7800star(2018-02-12)
  2. lowdb 7269star(2018-02-12)

比较

其中就目前来看,nedb用的更为普遍,star数更多(截止2018-02-12),并且有不少讲到nedb和electron配合使用的文章。不过,nedb已经有快两年没有维护了,并且原生不支持Promise,采用的是异步回调(虽然能够经过第三方插件实现Promise)。json

lowdb是用JSON为基本存储结构基于lodash开发的,有lodash的加持,用起来很顺手。优点在于它在持续的维护,有很多好用的插件。而且很关键的是同步操做,采用链式调用的写法,写起来有种jQuery的感受。再者,用JSON存储的数据,不论是调用仍是备份都很方便,这也是让我很喜欢的一点。axios

综上,PicGo采用的是lowdb。

lowdb的初始化

因为electron给main进程和renderer进程都置入了Node的fs模块,因此咱们能够很方便的在两端都使用跟fs相关的操做。而lowdb本质上就是经过fs来读写JSON文件实现的,正好符合咱们的要求。因此根据官方给出的文档,咱们首先先初始化一下。

为了操做fs更方便,不妨安装一个fs-extra

建立一个datastore.js文件:

import Datastore from 'lowdb'
import FileSync from 'lowdb/adapters/FileSync'
import path from 'path'
import fs from 'fs-extra'
import { app } from 'electron'

const STORE_PATH = app.getPath('userData') // 获取electron应用的用户目录

const adapter = new FileSync(path.join(STORE_PATH, '/data.json')) // 初始化lowdb读写的json文件名以及存储路径

const db = Datastore(adapter) // lowdb接管该文件

export default db // 暴露出去

复制代码

接着咱们在main进程和renderer进程里就能够这样引入:

import db from '../datastore' // 取决于你的datastore.js的位置
复制代码

踩坑

若是仅仅是上面的基本操做,那么这篇文章未免也太简单了。关于electron引入lowdb的踩坑之路如今才开始。

1. renderer进程要使用remote模块

首先由上面的初始化能明显看到一个问题。app模块是main进程里特有的,renderer进程应该使用remote.app模块。因此上面的代码在renderer进程里会报错。

所以第一次修改,使其既能跑在main进程也能跑在renderer进程:

import Datastore from 'lowdb'
import FileSync from 'lowdb/adapters/FileSync'
import path from 'path'
import fs from 'fs-extra'
import { app, remote } from 'electron' // 引入remote模块

const APP = process.type === 'renderer' ? remote.app : app // 根据process.type来分辨在哪一种模式使用哪一种模块

const STORE_PATH = APP.getPath('userData') // 获取electron应用的用户目录

const adapter = new FileSync(path.join(STORE_PATH, '/data.json')) // 初始化lowdb读写的json文件名以及存储路径

const db = Datastore(adapter) // lowdb接管该文件

export default db // 暴露出去

复制代码

2. 开发模式和生产模式初始化路径问题

在开发模式的时候,经过APP.getPath('userData')获取到的路径形如:/Users/molunerfinn/Library/Application Support/Electron(macOS下)。这个是一个已经自动建立好的路径。因此在开发模式的时候,初始化路径是已经存在的。

然而在生产模式下不是这样。生产模式下,第一次打开应用的过程当中,APP.getPath('userData')获取的路径并未建立,而datastore.js却已经被加载。因此这个时候初始化路径并不存在。用户在第一次打开应用的时候就会遇到以下报错:

因此咱们必须在datastore.js里作一次路径是否存在的判断:

此处的fs是来自fs-extra模块

if (process.type !== 'renderer') {
  if (!fs.pathExistsSync(STORE_PATH)) { // 若是不存在路径
    fs.mkdirpSync(STORE_PATH) // 就建立
  }
}
复制代码

3. 初始化数据

由于有的时候咱们须要预先指定数据库的基本结构,好比是个数组,这样咱们就初始化为[]。若是是个Object,有具体值,就指定为具体值。而初始化数据结构不该该在每次对数据读写的时候来判断,应该在数据库一开始建立的时候就初始化,因此写在datastore.js里是合适的。

好比我要初始化上传列表应该是一个数组,具体以下:

if (!db.has('uploaded').value()) { // 先判断该值存不存在
  db.set('uploaded', []).write() // 不存在就建立
}
复制代码

4. 惟一标识的id字段

用过MySQL的人大多都会在表里初始化一个自增的id字段做为数据的惟一标识。而lowdb虽然没法很方便地建立一个自增的id字段,可是经过lodash-id这个插件能够很方便地为每一个新增的数据自动加上一个惟一标识的id字段。

形如:

{
  "height": 514,
  "type": "weibo",
  "width": 514,
  "id": "7f247aa7-ffeb-4bb1-87f1-a0d69824ec78"
}
复制代码

初始化也很方便:

// ...
import LodashId from 'lodash-id'
// ...

const db = Datastore(adapter)
db._.mixin(LodashId) // 经过._mixin()引入
复制代码

初始化完整代码

经过上述的踩坑,PicGo的初始化代码以下,仅供参考:

import Datastore from 'lowdb'
import LodashId from 'lodash-id'
import FileSync from 'lowdb/adapters/FileSync'
import path from 'path'
import fs from 'fs-extra'
import { remote, app } from 'electron'

const APP = process.type === 'renderer' ? remote.app : app
const STORE_PATH = APP.getPath('userData')

if (process.type !== 'renderer') {
  if (!fs.pathExistsSync(STORE_PATH)) {
    fs.mkdirpSync(STORE_PATH)
  }
}

const adapter = new FileSync(path.join(STORE_PATH, '/data.json'))

const db = Datastore(adapter)
db._.mixin(LodashId)

if (!db.has('uploaded').value()) {
  db.set('uploaded', []).write()
}

if (!db.has('picBed').value()) {
  db.set('picBed', {
    current: 'weibo'
  }).write()
}

if (!db.has('shortKey').value()) {
  db.set('shortKey', {
    upload: 'CommandOrControl+Shift+P'
  }).write()
}

export default db
复制代码

lowdb的基本操做

数据库的基本操做无非就是CURD。

它表明建立(Create)、更新(Update)、读取(Retrieve)和删除(Delete)操做。

下面介绍lowdb的基本使用方法。

建立

主要经过set()或者defaults()方法。其中defaults()专门针对空JSON文件进行初始化。(不过用set也是能够实现相似的,如上一小节说到的初始化)

db.defaults({ posts: [], user: {}, count: 0 })
  .write() // 必定要显式调用write方法将数据存入JSON
复制代码

注意任何写的操做,都必须显式的使用write()方法来保存。

读取

db.get('posts').value() // []
复制代码

固然还能够用lodash的一些方法来查询你的JSON。

好比find()

db.get('posts')
  .find({ id: 1 })
  .value()
复制代码

注意任何读的操做,都必须显式使用value()方法来获取值。

更新

经过不一样的方法对不一样的结构来更新。

好比针对对象就用赋值,针对数组就用push()或者insert()(lowdb-id提供的方法)

db.get('posts').insert({ // 对数组进行insert操做
  title: 'xxx',
  content: 'xxxx'
}).write()
复制代码

针对对象能够直接用set()来更新:

db.set('user.name', 'typicode') // 经过set方法来对对象操做
  .write()
复制代码

还能够这么写:

db.set('user', {
  name: 'typicode'
}).write()
复制代码

很灵活对吧。

针对原有的数据进行更新的能够用update。

db.update('count', n => n + 1) // update方法使用已存在的值来操做
  .write()
复制代码

删除

能够经过remove()方法删除一个符合条件的项:

db.get('posts')
  .remove({ title: 'low!' })
  .write()
复制代码

能够经过unset来删除一个属性:

db.unset('user.name')
  .write()
复制代码

还能够经过lodash-id提供的removeById()来删除指定id的项:

db.get('posts')
  .removeById(id)
  .write()
复制代码

lowdb实际使用的坑

lowdb在使用的过程当中会遇到一个大坑在于,若是就按照基本操做,那么有可能出现我在main进程里存入的值,在renderer进程里读不到。

为啥?由于直接引用的db实际上只是那个时刻在内存里的数据。lowdb在使用过程当中会把JSON数据读入内存中。只有在须要写操做的时候才会将新的数据写入磁盘。

main进程和renderer进程拿到的db都是应用打开时所读取的。在没有额外处理的状况下,在main进程拿到的内存里的db,和renderer拿到的内存里的db不是同一个db,也就是所谓的不是一个db的两份引用,而是一个db的两份拷贝。main进程对其进行的操做,renderer进程是不知道的。换句话说,main进程对db进行了任何读写操做,renderer拿到的db依然是当初应用打开时所读取的db。因此就会遇到main进程更新了数据,而renderer进程依然没法拿到新的数据。

那有没有办法解决呢?有的。就是有点麻烦。那就是在全部的db操做的最开始,都从新读取一遍db的最新状态:

好比:

db.read().get('xxx').value()

db.read().set('xxx', 'xxx')
复制代码

强制在每一个db操做前,都经过read()刷新一遍内存区,这样就能保证拿到的数据都是最新的啦。

Vue里使用lowdb的便捷方法

相似于不少人会在Vue里把axios挂在vue的原型链上同样,咱们也能够用相似的方法来方便咱们在Vue里使用lowdb。

打开Vue项目的入口文件,一般是main.js

// ...
import db from '../datastore'
import Vue from 'vue'
// ...

Vue.prototype.$db = db
复制代码

这样咱们就能够在项目里,用this.$db的方法来使用lowdb啦。

总结

本文详细地介绍了lowdb以及lowdb在electron里的使用。不少都是我在开发PicGo的时候碰到的问题、踩的坑。也许文中简单的几句话背后就是我无数次的查阅和调试。但愿这篇文章可以给你的electron-vue开发带来一些启发。文中相关的代码,你均可以在PicGo的项目仓库里找到。若是本文可以给你带来帮助,那么将是我最开心的地方。若是喜欢,欢迎关注个人博客以及本系列文章的后续进展。

注:文中的图片除未特意说明以外均属于我我的做品,须要转载请私信

相关文章
相关标签/搜索