原electron桌面應用技術棧背景:
- electron:跨端桌面應用支持,調用底層api等
- vue:編寫純web界面,在electron殼子上展示
- better-sqlite3:緩存支持
- electron-vue:腳手架
把electron桌面應用改造為在安卓或者ios上面運行(即純web),網上幾乎搜不到這種改造方案,為了做到最小改動量,只改造出入口邏輯,也就是修改底層調用,遵循業務邏輯一概不動的原則。
一、需要的改造點:
主要分為三種:修改底層通信方式、修改底層存儲方式、其他細節
二、進程通信方法改造:
electron的主進程和渲染進程通信是基于 node事件系統events 進行改造的。因此我們可以通過使用events來模擬通信。
改造方案:
首先先使用events模擬electron的主、渲進程通信。生成ipcRender、ipcMain兩個實例。
// myEvents.js文件
import EventEmitter from 'events'
class ipcMainEmitter extends EventEmitter {}
class ipcRenderEmitter extends EventEmitter {
constructor() {
super()
}
emit (event, payload) {
super.emit(event, event, payload) //這個方法是我為了兼容以前傳參需要的
}
}
const ipcRenderMy = new ipcRenderEmitter()
const ipcMainMy = new ipcMainEmitter()
export {ipcRenderMy, ipcMainMy}
1、electron的異步通信 ---雙向
原先使用雙向通信的圖例,我整理了一張:
2022.10.12_e9d46b6ebb66d87de2f36012118dc0cc.png
原先代碼結構如下:
//在主進程中.
const { ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
event.reply('asynchronous-reply', 'pong')
})
//---------------------------------------------------------------------------//
//在渲染器進程 (網頁) 中。
const { ipcRenderer } = require('electron')
ipcRenderer.on('asynchronous-reply', (event, arg) => {
console.log(arg) // prints "pong"
})
ipcRenderer.send('asynchronous-message', 'ping')
改造后的方案如下:
我整理了一張改造的雙向通信的示例圖:
2022.10.12_&f33e21173898cad0ece90f88310457d5.png
如上圖所示,這樣就能保證到在異步通信上我們不用改到原先的代碼,只需要修改引入的文件即可。
// const { ipcRenderer } = require('electron')
// const { ipcMain } = require('electron')
// 改為如下
import {ipcMain, ipcRender} from './myEvents'
2、electron的同步通信
//在主進程中
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
event.returnValue = 'pong'
})
//---------------------------------------------------------------------------//
//在渲染器進程 (網頁) 中
const { ipcRenderer } = require('electron')
cosnt data = ipcRenderer.sendSync('synchronous-message', 'ping')
console.log(data ) // prints "pong"
可以看到同步的話是用event.returnValue
返回數據(我這里項目是用來等待接口請求返回數據),所以我們可以用es6的async await
來模擬這個功能(可能這個需要在原來的方法前面加上async并且await數據返回,暫時沒有其他更好的做法)
改造為:
// 例子,直接在sendSync時候發請求返回數據
async sendSync(methodName, payload = {}) {
if (methodName) {
return await fetchData(payload)
}
}
三、sqlite3改造為瀏覽器的 idnexedDB:
(這需要根據實際業務情況來,即你業務層調用的方式)
以下是數據庫增刪改查的對應4種辦法,值得注意的是,因為indexedDB是異步操作所以需要使用promise,來等待異步完成,以及將對應的sql語句替換為indexdb的寫法。
let IDBOpenRequest = null //打開數據庫連接對象
let IDBDatabase = null //連接的數據庫,后面對數據庫操作都通過這個對象完成
const indexPrimaryKeys = 'KEY_TYPE'
/**
* 在創建好的數據庫上新建倉庫(即新建表)
*/
const createTable = ({}) => {
if (!IDBDatabase.objectStoreNames.contains('cache_records')) {
let objectStore = IDBDatabase.createObjectStore('cache_records', { autoIncrement: true })
//新建索引
// objectStore.createIndex(indexName, keyPath, objectParameters) 語法
objectStore.createIndex('key', 'key', { unique: false })
objectStore.createIndex('type', 'type', { unique: false })
objectStore.createIndex('last_modify', 'last_modify', { unique: false })
objectStore.createIndex(indexPrimaryKeys, ['key', 'type'], { unique: false })
}
if (!IDBDatabase.objectStoreNames.contains('common_configs')) {
const objectStore = IDBDatabase.createObjectStore('common_configs', { autoIncrement: true })
objectStore.createIndex('setting_value', 'setting_value', { unique: false })
objectStore.createIndex('setting_key', 'setting_key', { unique: true })
}
}
/**
* 打開數據庫連接,并創建一個數據庫對象
* @param databaseName 數據庫名稱
* @param version 版本號
*/
function initIDB({ databaseName = 'myDatabase', version = 1 }) {
console.log('|||||||||||---數據庫初始化程序開始---||||||||||||')
if (!indexedDB) {
alert('indexdb does not support in this browser!')
return
}
IDBOpenRequest = indexedDB.open(databaseName, version)
//數據庫的success觸發事件
IDBOpenRequest.onsuccess = function (event) {
IDBDatabase = IDBOpenRequest.result //拿到數據庫實例
console.log('數據庫打開成功')
}
//錯誤時候事件
IDBOpenRequest.onerror = function (event) {
console.log('數據庫打開出錯:', event)
}
//指定版本號大于實際版本號時候觸發,它會優先執行
IDBOpenRequest.onupgradeneeded = function (event) {
IDBDatabase = event.target.result //若升級,此時通過這里拿到數據庫實例
createTable({})
}
}
initIDB({})
class Sqlite {
/**
* 處理不同字段情況下查詢數據返回IDBRequest
* @param objectStore
* @param search
* @returns {IDBRequest<IDBCursorWithValue | null>}
*/
static returnIDBCursorForGetPrimaryKey({ objectStore, search }) {
try {
const getIndexKeyBySearchKey = Object.keys(search)
const getIndexValuesBySearchValues = Object.values(search) || ['']
const singleParams = getIndexKeyBySearchKey.length === 1
const index = objectStore.index(singleParams ? getIndexKeyBySearchKey[ 0 ] : indexPrimaryKeys)
return index.openCursor(IDBKeyRange.only(singleParams ? getIndexValuesBySearchValues[ 0 ] : [search.key, search.type]))
} catch (e) {
throw `openCursor查找方式出錯!!!${e}`
}
}
/**
* 新增
* @param {String} tableName 表名
* @param {Object} value 插入的值 例如: {字段名: 值}
*/
insert({ tableName, value = {} }) {
const objectStore = IDBDatabase.transaction([tableName], 'readwrite').objectStore(tableName)
let valueEdit = value.key ? value : { ...value, key: null }
// objectStore.add(value, key) 語法
const objectStoreRequest = objectStore.add(valueEdit)
objectStoreRequest.onsuccess = function (event) {
console.log('=====insert=====:數據寫入成功', { tableName, value })
}
objectStoreRequest.onerror = function (event) {
console.warn('數據寫入失敗!!!', event.target.error.message)
}
}
/**
* 查詢表=>查詢索引
* @param {String} tableName 表名
* @param {Object} search 查詢條件 例如: {字段名1: 值, 字段名2: 值}
*/
queryOne({ tableName, search }) {
if (!search) return
try {
const objectStore = IDBDatabase.transaction([tableName]).objectStore(tableName)
return new Promise(((resolve, reject) => {
const request = Sqlite.returnIDBCursorForGetPrimaryKey({ objectStore, search })
request.onsuccess = function (event) {
console.log('=====queryOne=====:成功', { tableName, search }, request.result && request.result.value)
resolve(request.result && request.result.value)
}
request.onerror = function (event) {
console.warn('查詢事務失敗!', event.target)
reject({})
}
}))
} catch (e) {
throw `查詢出錯了:${e}`
}
}
/**
* 查詢多條
* @param {String} tableName 表名
* @param {Array} field 查詢的字段 例如: [字段名1, 字段名2]
* @param {Object} search 查詢條件 例如: {字段名1: 值, 字段名2: 值}
*/
query({ tableName, field, search }) {
const data = []
const list = []
const objectStore = IDBDatabase.transaction([tableName]).objectStore(tableName)
if (field && field.length > 0) { //向上兼容,保持調用方法不變
const index = objectStore.index('type')
return new Promise((resolve) => {
index.openCursor().onsuccess = function (event) {
let cursor = event.target.result;
if (cursor) {
list.push(cursor.value)
cursor.continue();
} else {
list.filter(item => item.type === search.type).forEach(item => {
if (item && item.key) data.push({ key: item.key, last_modify: item.last_modify })
})
console.log('======query=====:=查詢所有=', { tableName, field, search }, data)
resolve(data)
}
}
})
} else {
return new Promise((resolve, reject) => {
objectStore.openCursor().onsuccess = function (event) {
const cursor = event.target.result;
if (cursor) {
data.push(cursor.value)
cursor.continue();
} else {
console.log('======query=====:=查詢所有=', { tableName, field, search }, data)
resolve(data)
}
};
})
}
}
/**
* 刪除
* @param {String} tableName 表名
* @param {Object} search 刪除條件 例如: {字段名: 值}
*/
async delete({ tableName, search }) {
const objectStore = IDBDatabase.transaction([tableName], 'readwrite').objectStore(tableName)
const request = Sqlite.returnIDBCursorForGetPrimaryKey({ objectStore, search })
request.onsuccess = function (event) {
const objectStoreRequest = objectStore.delete(event.target.result && event.target.result.primaryKey)
objectStoreRequest.onsuccess = function (event) {
console.log('=====delete=====:刪除成功??', { tableName, search }, event.target.result)
}
}
}
/**
* 修改
* @param {String} tableName 表名
* @param {Object} field 更新的字段 例如: {字段名: 值}
* @param {Object} search 查詢條件 例如: {字段名: 值}
*/
async update({ tableName, field, search }) {
try {
const objectStore = IDBDatabase.transaction([tableName], 'readwrite').objectStore(tableName)
const request = Sqlite.returnIDBCursorForGetPrimaryKey({ objectStore, search })
request.onsuccess = function (event) {
// objectStore.put(item, key) 語法
const objectStoreRequest = objectStore.put(field, event.target.result && event.target.result.primaryKey)
objectStoreRequest.onsuccess = function (event) {
console.log('=====update=====:成功', { tableName, search })
}
}
} catch (e) {
console.warn('update失敗!')
}
}
}
export default Sqlite
四、其余需要改造功能點:
- 下載功能:原本的文件下載,改為直接瀏覽器原生下載
- 緩存存儲功能:sqlite數據庫緩存改為瀏覽器的indexedDB
- 請求功能改造:原有在主進程里面的請求改為web請求
- 新開窗口:electron新開彈窗改為web的彈窗
以上算是完成了基本改造,已經把界面從electron上面移植出來,并且可以在瀏覽器上面跑的通了。