Electron-vue開發實戰2——引入基於Lodash的JSON資料庫lowdb

Molunerfinn發表於2018-02-12

前言

前段時間,我用electron-vue開發了一款跨平臺(目前支援Mac和Windows)的免費開源的圖床上傳應用——PicGo,在開發過程中踩了不少的坑,不僅來自應用的業務邏輯本身,也來自electron本身。在開發這個應用過程中,我學了不少的東西。因為我也是從0開始學習electron,所以很多經歷應該也能給初學、想學electron開發的同學們一些啟發和指示。故而寫一份Electron的開發實戰經歷,用最貼近實際工程專案開發的角度來闡述。希望能幫助到大家。

預計將會從幾篇系列文章或方面來展開:

  1. electron-vue入門
  2. Main程式和Renderer程式的簡單開發
  3. 引入基於Lodash的JSON database——lowdb
  4. 跨平臺的一些相容措施
  5. 通過CI釋出以及更新的方式
  6. ...(想到再寫)

說明

PicGo是採用electron-vue開發的,所以如果你會vue,那麼跟著一起來學習將會比較快。如果你的技術棧是其他的諸如reactangular,那麼純按照本教程雖然在render端(可以理解為頁面)的構建可能學習到的東西不多,不過在main端(electron的主程式)應該還是能學習到相應的知識的。

如果之前的文章沒閱讀的朋友可以先從之前的文章跟著看。

資料持久化儲存的必要性

不像平時很多人寫的一些demo,就是請求一下api然後把web頁面展示出來就了事了。electron應用畢竟是個桌面級應用,如果思維還留在純web開發的思路上,那麼也就失去了用electron的意義了吧。

資料持久化儲存實際上對於後端很熟悉。通常是指的是把記憶體裡的資料以不同的儲存模型儲存到磁碟上,在需要的時候再從儲存模型裡讀取讀入記憶體中的整個流程。這裡面的儲存模型通常就是我們熟悉的資料庫。說到資料庫,很多人會想到MySQL,Mongodb,SQLite等等。常見的這些資料庫都是Server-Client模式的,需要啟動服務端——通常我們裝的就是這個。但是你一般很少見到叫別人裝個桌面軟體的同時,叫別人配資料庫的吧。

因為有些資料我們必須在本地存下來,方便下次使用的時候讀取。而對於electron來說,既然讓使用者裝MySQL、Mongodb是不太優雅的解決辦法的話,那麼如果能用其他方式,將資料存到本地而不用使用者操心如何儲存的,對我們和使用者來說都是一件好事。

純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)。

lowdb是用JSON為基本儲存結構基於lodash開發的,有lodash的加持,用起來很順手。優勢在於它在持續的維護,有不少好用的外掛。並且很關鍵的是同步操作,採用鏈式呼叫的寫法,寫起來有種jQuery的感覺。再者,用JSON儲存的資料,不管是呼叫還是備份都很方便,這也是讓我很喜歡的一點。

綜上,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卻已經被載入。所以這個時候初始化路徑並不存在。使用者在第一次開啟應用的時候就會遇到如下報錯:

Electron-vue開發實戰2——引入基於Lodash的JSON資料庫lowdb

所以我們必須在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的專案倉庫裡找到。如果本文能夠給你帶來幫助,那麼將是我最開心的地方。如果喜歡,歡迎關注我的部落格以及本系列文章的後續進展。

注:文中的圖片除未特地說明之外均屬於我個人作品,需要轉載請私信

相關文章