前言
前段時間,我用electron-vue開發了一款跨平臺(目前支援Mac和Windows)的免費開源的圖床上傳應用——PicGo,在開發過程中踩了不少的坑,不僅來自應用的業務邏輯本身,也來自electron本身。在開發這個應用過程中,我學了不少的東西。因為我也是從0開始學習electron,所以很多經歷應該也能給初學、想學electron開發的同學們一些啟發和指示。故而寫一份Electron的開發實戰經歷,用最貼近實際工程專案開發的角度來闡述。希望能幫助到大家。
預計將會從幾篇系列文章或方面來展開:
- electron-vue入門
- Main程式和Renderer程式的簡單開發
- 引入基於Lodash的JSON database——lowdb
- 跨平臺的一些相容措施
- 通過CI釋出以及更新的方式
- ...(想到再寫)
說明
PicGo
是採用electron-vue
開發的,所以如果你會vue
,那麼跟著一起來學習將會比較快。如果你的技術棧是其他的諸如react
、angular
,那麼純按照本教程雖然在render端(可以理解為頁面)的構建可能學習到的東西不多,不過在main端(electron的主程式)應該還是能學習到相應的知識的。
如果之前的文章沒閱讀的朋友可以先從之前的文章跟著看。
資料持久化儲存的必要性
不像平時很多人寫的一些demo,就是請求一下api然後把web頁面展示出來就了事了。electron應用畢竟是個桌面級應用,如果思維還留在純web開發的思路上,那麼也就失去了用electron的意義了吧。
資料持久化儲存實際上對於後端很熟悉。通常是指的是把記憶體裡的資料以不同的儲存模型儲存到磁碟上,在需要的時候再從儲存模型裡讀取讀入記憶體中的整個流程。這裡面的儲存模型通常就是我們熟悉的資料庫。說到資料庫,很多人會想到MySQL,Mongodb,SQLite等等。常見的這些資料庫都是Server-Client模式的,需要啟動服務端——通常我們裝的就是這個。但是你一般很少見到叫別人裝個桌面軟體的同時,叫別人配資料庫的吧。
因為有些資料我們必須在本地存下來,方便下次使用的時候讀取。而對於electron來說,既然讓使用者裝MySQL、Mongodb是不太優雅的解決辦法的話,那麼如果能用其他方式,將資料存到本地而不用使用者操心如何儲存的,對我們和使用者來說都是一件好事。
純JavaScript資料庫的選擇
既然是JS技術棧的,於是我就找了一些純JavaScript實現的資料庫。經過初步篩選,我找到如下兩個:
比較
其中就目前來看,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
卻已經被載入。所以這個時候初始化路徑並不存在。使用者在第一次開啟應用的時候就會遇到如下報錯:
所以我們必須在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的專案倉庫裡找到。如果本文能夠給你帶來幫助,那麼將是我最開心的地方。如果喜歡,歡迎關注我的部落格以及本系列文章的後續進展。
注:文中的圖片除未特地說明之外均屬於我個人作品,需要轉載請私信