citrus327/issue-blog-record

记一次Electron开发全过程

Closed this issue · 1 comments

记一次Electron开发全过程

[[toc]]

引子

有一个需要开发PC桌面端的需求。日常中使用的网易云音乐,Twitch client等都属于electron的产品。
Electron对于前端开发来说上手非常简单,虽早有耳闻electron的强大,也看过一些文档和基本实现,但是没有完整的搭建、开发和维护过一个electron项目。借这个机会,熟悉一下electron技术栈,也写下本文,做一次技术积累。

Electron

Electron分为Main进程(主进程)和Renderer进程(渲染进程)

主进程为Node.js服务端环境,负责数据库处理,文件处理等

渲染进程为浏览器环境

主进程会将渲染进程的html,创建window,挂载html运行

主进程与渲染进程通过electron包提供的ipcMain和ipcRenderer进行进程通讯。其实就是一个EventEmitter,发布订阅模式。

项目搭建

起步

项目开始基本跟着首页的文档一步步走。

脚手架

直到Boilerplates and CLIs这一章。这一章开头也提到了说Electron只是提供了一种基于chromium渲染的桌面端包装而已。对于如何开发、构建、打包等是"unopinionated"。这一章中提到了很多的boilerplate和CLI的方案。

  1. electron-forge
  2. electron-builder
  3. electron-react-boilerplate

前两者是比较基本的脚手架。不限制renderer部分选型。electron-react-boilerplate这个从名字看就基本限定死React了。由于时间紧迫,React的水平也确实有限。只能在前两者里挑选。

对于前两个脚手架,我选的是star数较多的electron-builder,时间有限,仅能基于star数选型了。

electron-builder

使用yarn add electron-builder --dev安装。同时electron-builder本身也提供了一些模板项目供快速开发参考。

这里的我使用vue-cli-plugin-electron-builder进行项目搭建。这个是基于Vue CLI3的插件。关于Vue CLI3的安装和插件的安装在此不做赘述。

vue-cli-plugin-electron-builder插件配置

由于这个插件是Vue CLI的插件,那配置文件理所应当在vue.config.js内。
贴上我目前的项目配置:

module.exports = {
  pluginOptions: {
    electronBuilder: {
      builderOptions: {
        "appId": "com.example.app",
        "productName": "test-app",
        directories: {
          buildResources: './build',
          output: "./dist"
        },
        "win":{//win相关配置
          "icon":"./build/icon.png",//图标,当前图标在根目录下,注意这里有两个坑
          "target": [
            {
              "target": "nsis",//利用nsis制作安装程序,
              "arch": [
                "x64",//64位
                "ia32"//32位
              ]
            }
          ],
        },
        "nsis": {
          "allowToChangeInstallationDirectory": true,
          oneClick: false,
        }
      }
    }
  }
}

项目结构

  1. build文件夹
    存放�打包需要的文件,例如icon等
  2. public文件夹
    • 静态文件夹
    • 包含渲染进程favcon.ico,index.html等静态文件
  3. src文件夹
    • 项目代码
    • -assets: 渲染进程静态文件夹
    • -biz: 主进程业务代码
    • -components: Vue.js组件
    • -plugins: Vue.js插件
    • -router: Vue-router
    • -store: Vuex
    • -views: Vue.js主视图
    • background.js: 主进程入口
    • main.js 渲染进程入口
  4. vue.config.js
    • vue-cli3的配置文件

主进程与渲染进程通讯

由于electron是一个unopinioned工具,他仅仅提供了进程通讯的方式,而对于如何组织通讯代码,如何封装API是没有一个最优解的。在这我会放出我对于进程通讯的二次封装。

概念描述

首先定义几个概念

  1. API
    • 应用程序接口。
    • 这里的API是给调用方使用的,也就是渲染进程。
  2. Payload Type
    • 荷载类型。
    • 定义业务类型(事件类型),其实也就是潜在的定义了业务入参和负责业务的具体描述。
  3. Routes
    • 路由。
    • 分发绑定业务句柄和业务类型。
  4. Services
    • 服务。
    • 纯净的业务处理,会涉及业务数据处理,db操作等。

路由与业务绑定

先放出Payload Type的定义

const PAYLOAD_TYPES = Object.freeze({
  FETCH_FULL_DATA: 'FETCH_FULL_DATA',
  FETCH_DROPDOWN_LIST: 'FETCH_DROPDOWN_LIST',
  UPDATE_CELL: 'UPDATE_CELL',
  SORT_SHEET: 'SORT_SHEET',
  VALIDATE_SHEET: 'VALIDATE_SHEET',
  CALCULATE_SHEET: 'CALCULATE_SHEET',
  FETCH_INITIAL_DATA: 'FETCH_INITIAL_DATA',
  DELETE_ROW: 'DELETE_ROW'
})

Routes会基于Payload Type进行类型和句柄的绑定,句柄内调用Services进行业务处理。

import { PAYLOAD_TYPES } from './constants'
import * as service from '../service'

export default {
  [PAYLOAD_TYPES.FETCH_FULL_DATA]: function (payload, callback) {
    callback(service.fetchFullData(payload))
  },
  [PAYLOAD_TYPES.FETCH_DROPDOWN_LIST]: function (payload, callback) {
    callback(service.fetchDropdownList(payload))
  },
  [PAYLOAD_TYPES.UPDATE_CELL]: function (payload, callback) {
    callback(service.updateCell(payload))
  }

绑定路由至主进程

const establishConnection = () => {
  Object.keys(routes).forEach((routeName) => {
    ipcMain.on(routeName, (e, payload) => {
      routes[routeName](payload, (respond) => {
        e.sender.send(routeName, respond)
      })
    })
  })
}

API声明

import { ipcRenderer } from 'electron'
import { PAYLOAD_TYPES } from '../channel/constants'
export default function fetchFullData(sheetName) {
  return new Promise((resolve, reject) => {
    // 定义请求参数
    const payload = {
      sheetName
    }

    // 发送请求
    ipcRenderer.send(PAYLOAD_TYPES.FETCH_FULL_DATA, payload)

    // 接受响应给callback
    ipcRenderer.once(PAYLOAD_TYPES.FETCH_FULL_DATA, (e, res) => {
      resolve(res)
    })
  })
}

�以上操作之后,API部分只要处理payload参数,发送到对应payload type的频道和绑定相应类型的响应句柄就可以了。

这样API的入参很清晰,回参就是service的回参。一番映射之后,API和Services的逻辑就相当纯净了,无需考虑ipcMain和ipcRenderer等一系列Electron相关的通讯操作。

新增API也只需要定义payload type,定义api,映射payload type和句柄,调用service就行了。

这套设计的优点在于,基本API, Routes,Service的创建只需要copy过去就能行,改改payload和方法名称,设计一下入参即可。之后的开发就可以完全沉浸在Services这一层里。

这套设计借鉴了Redux和Node.js里很多框架都采用的中心化路由映射的**。

数据存储

数据存储的话由于数据比较简单,目前我遇到的业务需求,基本一个mongodb就能够搞定。但是由于是electron,肯定不希望往使用方电脑装太多东西,其实mongodb也就是个json。那只需要找一个简单、成熟、语法友好的基于json的本地持久化工具即可。

在这里使用lowdb

数据持久化

db文件放在user data里做持久化。
获取user data的方法:app.getPath()

const filePath = path.join(app.getPath('userData'), '/data.json')

lowdb

lowdb是基于lodash API的json持久化工具。前端做久了,Lodash肯定是知道的,语法也比较熟。在这放出一些简单的db操作

初始化

const low = require('lowdb')
const FileSync = require('lowdb/adapters/FileSync')

const filePath = path.join(app.getPath('userData'), '/data.json')
const adapter = new FileSync(filePath)
const db = low(adapter)

db操作-增删改查

db.get('posts')
  .push({title: 'example'})
  .write()

db.get('posts')
  .remove({title: 'example'})
  .write()

db.get('posts')
  .find({ title: 'example' })
  .assign({ title: 'hi!'})
  .write()

db.get('posts').value()

渲染进程

因为是基于Vue CLI搭的项目,那渲染进程这边肯定是选用Vue.js作为前端选型。到这里基本就跟普通web前端开发一样了。
业务就调用API就行了。

打包

electron-builder提供了很多的配置项。参考https://www.electron.build/configuration/configuration

配置文件也已经在上文中给出。

总结

整个项目从构思到搭建,再到开发流程基本没采坑,主要还是因为文档比较给力。

感觉唯一学到的东西就是终于有一套构建桌面端的解决方案了。。

如果有后续更新会在此补充,感谢阅读。

666