前段時間,我用electron-vue開發了一款跨平臺(目前支持Mac和Windows)的免費開源的圖牀上傳應用——PicGo,在開發過程當中踩了很多的坑,不只來自應用的業務邏輯自己,也來自electron自己。在開發這個應用過程當中,我學了很多的東西。由於我也是從0開始學習electron,因此不少經歷應該也能給初學、想學electron開發的同窗們一些啓發和指示。故而寫一份Electron的開發實戰經歷,用最貼近實際工程項目開發的角度來闡述。但願能幫助到你們。vue
預計將會從幾篇系列文章或方面來展開:node
PicGo
是採用electron-vue
開發的,因此若是你會vue
,那麼跟着一塊兒來學習將會比較快。若是你的技術棧是其餘的諸如react
、angular
,那麼純按照本教程雖然在render端(能夠理解爲頁面)的構建可能學習到的東西很少,不過在main端(electron的主進程)應該仍是能學習到相應的知識的。react
若是以前的文章沒閱讀的朋友能夠先從以前的文章跟着看。ios
不像平時不少人寫的一些demo,就是請求一下api而後把web頁面展現出來就了事了。electron應用畢竟是個桌面級應用,若是思惟還留在純web開發的思路上,那麼也就失去了用electron的意義了吧。git
數據持久化存儲實際上對於後端很熟悉。一般是指的是把內存裏的數據以不一樣的存儲模型存儲到磁盤上,在須要的時候再從存儲模型裏讀取讀入內存中的整個流程。這裏面的存儲模型一般就是咱們熟悉的數據庫。說到數據庫,不少人會想到MySQL,Mongodb,SQLite等等。常見的這些數據庫都是Server-Client模式的,須要啓動服務端——一般咱們裝的就是這個。可是你通常不多見到叫別人裝個桌面軟件的同時,叫別人配數據庫的吧。github
由於有些數據咱們必須在本地存下來,方便下次使用的時候讀取。而對於electron來講,既然讓用戶裝MySQL、Mongodb是不太優雅的解決辦法的話,那麼若是能用其餘方式,將數據存到本地而不用用戶操心如何存儲的,對咱們和用戶來講都是一件好事。web
既然是JS技術棧的,因而我就找了一些純JavaScript實現的數據庫。通過初步篩選,我找到以下兩個:數據庫
其中就目前來看,nedb用的更爲普遍,star數更多(截止2018-02-12),並且有不少講到nedb和electron配合使用的文章。不過,nedb已經有快兩年沒有維護了,並且原生不支持Promise,採用的是異步回調(雖然能夠經過第三方插件實現Promise)。json
lowdb是用JSON爲基本存儲結構基於lodash開發的,有lodash的加持,用起來很順手。優點在於它在持續的維護,有很多好用的插件。而且很關鍵的是同步操做,採用鏈式調用的寫法,寫起來有種jQuery的感受。再者,用JSON存儲的數據,不論是調用仍是備份都很方便,這也是讓我很喜歡的一點。axios
綜上,PicGo採用的是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的踩坑之路如今纔開始。
首先由上面的初始化能明顯看到一個問題。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 // 暴露出去
複製代碼
在開發模式的時候,經過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) // 就建立
}
}
複製代碼
由於有的時候咱們須要預先指定數據庫的基本結構,好比是個數組,這樣咱們就初始化爲[]
。若是是個Object,有具體值,就指定爲具體值。而初始化數據結構不該該在每次對數據讀寫的時候來判斷,應該在數據庫一開始建立的時候就初始化,因此寫在datastore.js
裏是合適的。
好比我要初始化上傳列表應該是一個數組,具體以下:
if (!db.has('uploaded').value()) { // 先判斷該值存不存在
db.set('uploaded', []).write() // 不存在就建立
}
複製代碼
用過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
複製代碼
數據庫的基本操做無非就是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在使用的過程當中會遇到一個大坑在於,若是就按照基本操做,那麼有可能出現我在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裏把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的項目倉庫裏找到。若是本文可以給你帶來幫助,那麼將是我最開心的地方。若是喜歡,歡迎關注個人博客以及本系列文章的後續進展。
注:文中的圖片除未特意說明以外均屬於我我的做品,須要轉載請私信