将electron桌面应用改造为Android平板和ipad平台可运行项目实践


原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上面移植出来,并且可以在浏览器上面跑的通了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容