Vue+Electron开发跨平台桌面应用实践

背景

公司去年对 CDN 资源服务器进行了迁移,由原来的通过 FTP 方式的文件存储改为了使用 S3 协议上传的对象存储,部门内 @柴俊堃 同学开发了一个命令行脚本工具 RapidTrans(睿传),使用睿传可以很方便将本地目录下的资源上传到 S3 中。

睿传运行时接收两个主要参数,一个为待上传的本地路径,一个为上传到 CDN 后的路径,我们可以在项目的 package.json 中去配置 scripts执行上传。

npm run rapid-trans -- -s "/home/demo/work/mall2016/release/列表页" -p "2016/m/list"

用了一段时间后觉得如果选择本地路径的时候可以通过可视化的文件选择器的方式选择就太好了,团队一直在做客户端方向技术的储备,所以为了更方便团队的使用产生了将睿传封装成 GUI 的跨平台客户端的想法。

客户端界面

图片

功能分析

  • 桌面客户端,支持 WindowsMac 系统

  • 本地路径可以通过文件对话框或拖拽的方式进行选择

  • CDN 路径可以通过输入框的方式输入

  • 上传成功后将当前选择的本地路径和 CDN 的映射关系存储,下次再选择到当前目录的话直接使用之前 CDN 的路径地址,无需再次输入

  • S3 参数配置化

  • 自动升级

  • 覆盖上传

技术选型

  • Electron

  • Vue

  • LowDB 

Electron 简介

Electron 是由 Github 开发,基于 Chromium 和 Node.js, 让你可以使用 HTML, CSS 和 JavaScript 构建跨平台桌面应用的开源框架。

图片

Electron 可以让你使用纯 JavaScript 调用丰富的原生(操作系统) APIs 来创造桌面应用。 你可以把它看作一个专注于桌面应用的 Node. js 的变体,而不是 Web 服务器。

简单点说,用 Electron 可以让我们在网页中使用 Node.js 的 API 和调用系统 API。

Vue + Electron 环境搭建

使用 vue-cli 脚手架和  electron-vue模板进行搭建,此处需要注意,由于 electron-vue 模板不支持 vue-cli@3.0,所以要使用 2.0 版本。

# 安装 vue-cli@2.0,若已安装则无需重复安装
npm install -g vue-cli
vue init simulatedgreg/electron-vue s3_upload_tool

# 安装依赖并运行
cd s3_upload_tool
npm install
npm run dev

目录结构

├─ .electron-vue
│  ├─ webpack.main.config.js
│  ├─ webpack.renderer.config.js
│  └─ webpack.web.config.js
├─ build
│  └─ icons/
├─ dist
│  ├─ electron/
│  └─ web/
├─ node_modules/
├─ src
│  ├─ main
│  │  ├─ index.dev.js
│  │  └─ index.js
│  ├─ renderer
│  │  ├─ components/
│  │  ├─ router/
│  │  ├─ store/
│  │  ├─ App.vue
│  │  └─ main.js
│  └─ index.ejs
├─ static/
├─ .babelrc
├─ .eslintignore
├─ .eslintrc.js
├─ .gitignore
├─ package.json
└─ README.md

应用的目录结构和平常我们用 Vue 做 WEB 端时生成的结构基本差异不大,所以本文我只介绍下与 Web 不同的几个目录。

.electron-vue

该目录下包含 3 个独立的 Webpack 配置文件

  • .electron-vue/webpack.main.config.js 针对于 Electron 的 main 主进程打包的配置,配置比较简单,主要就是将 src/main/index.js 通过 babel-loader 打包,并且生成 commonjs2模块规范。

  • .electron-vue/webpack.renderer.config.js 针对于 Electron 的 renderer 渲染进程打包的配置,此配置主要用来打包 Vue 的应用程序,这个配置就和平常我们做 Web 端时 Webpack 的配置基本一样,处理 Vue、Sass、Image、Html等。

  • .electron-vue/webpack.web.config.js 为浏览器构建 render渲染进程的配置,主要针对于发布到 Web 的情况。

src/main

主进程代码存放位置,涉及到调取 Node API 、调用原生系统功能的代码。

src/renderer

渲染进程代码存放位置,和平常的 Vue 项目基本一样。

主进程与渲染进程

在 Electron 中有两个进程,分别为主进程渲染进程,主进程负责 GUI 部分,渲染进程负责页面的展示。

主进程

  • 主进程通常是在 package.json  main字段的脚本进程。

  • 一个 Electron 应用只有一个主进程。

  • 主进程一般用来处理 App 生命周期、系统事件的处理、系统原生GUI。

main.js

const { app, BrowserWindow } = require('electron')

function createWindow () {   
  // 创建浏览器窗口
  let win = new BrowserWindow({ width: 800, height: 600 })

  // 然后加载 app 的 index.html.
  win.loadFile('index.html')
}

app.on('ready', createWindow)

渲染进程

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
  </body>
</html>

主进程使用 BrowserWindow 实例创建页面。 每个BrowserWindow实例都在自己的渲染进程里运行页面。 当一个 BrowserWindow 实例被销毁后,相应的渲染进程也会被终止。

主进程和渲染进程通讯

进程间通信(IPC,Interprocess communication)是一组编程接口,让开发者能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。

Electron 使用 IPC 的机制,由主进程来创建应用,渲染进程来负责绘制页面,而两个进程之间是无法直接通信的。

图片

渲染进程通过ipcRenderer向主进程发送消息,主进程通过 ipcMain监听事件,当事件响应时对消息进行处理。

主进程监听事件的回调函数中会存在 event 对象及arg 对象。arg 对象为渲染进程传递过来的参数。

如果主进程执行的是同步方法,回复同步信息时,需要设置event.returnValue,如果执行的是异步方法回复时需要使用 event.sender.send向渲染进程发送消息。

下面代码为渲染进程主动向主进程发送消息,在主进程接收后回复渲染进程。

// 主进程
const { ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
  console.log(arg) // prints "ping"
  event.sender.send('asynchronous-reply', 'pong')
})

ipcMain.on('synchronous-message', (event, arg) => {
  console.log(arg) // prints "ping"
  event.returnValue = 'pong'
})

// 渲染器进程
const { ipcRenderer } = require('electron')
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong"

ipcRenderer.on('asynchronous-reply', (event, arg) => {
  console.log(arg) // prints "pong"
})
ipcRenderer.send('asynchronous-message', 'ping')

有时候我们也需要由主进程主动向渲染进程发送消息,面对这种情况我们可以在主进程中通过 BrowserWindow对象的 webContets.send方法向渲染进程发送消息。

// 主进程

const { app, BrowserWindow } = require('electron')

function createWindow () {   
  let win = new BrowserWindow({ width: 800, height: 600 })
  win.loadFile('index.html')
  
  // 向渲染进程发送消息
  win.webContents.send('main-process-message', 'ping')
}

app.on('ready', createWindow)

// 渲染器进程

const { ipcRenderer } = require('electron')
// 监听主进程发送的消息
ipcRenderer.on('main-process-message', (event, arg) => {
  console.log(arg) // prints "ping"
})

持久化存储

在桌面端应用中一些用户设置通常需要进行存持久化存储,方便以后使用的时候获取。 我们做 Web 时候通常是使用像 MySQLMongodb等数据库进行持久化存储, 但是当用户安装桌面软件时候不可能让用户在本地安装这类数据库,所以我们需要一个轻量级的本地化数据库。

lowdb 是一个基于 Lodash API 的轻量级本地 JSON 数据库,支持 Node.jsbrowserElectron

在我们要开发的工具中,用户的 S3 配置,已上传文件的 CDN目录等信息是需要进行持久化存储的,所有我们采用的 lowdb进行数据的存储。

图片

使用也是非常的简单,数据的读写和平常使用 Lodash差不多。

安装

npm install lowdb -save

数据存储路径

Electron 提供了获取系统目录的方法,可以很方便的进行一些系统目录的获取。

const { app, remote } = require('electron')

app.getPath('home'); // 获取用户的 home 文件夹(主目录)路径
app.getPath('userData'); // 获取当前用户的应用数据文件夹路径
app.getPath('appData'); // 获取应用程序设置文件的文件夹路径,默认是 appData 文件夹附加应用的名称
app.getPath('temp'); // 获取临时文件夹路径
app.getPath('documents'); // 获取用户文档目录的路径
app.getPath('downloads'); // 获取用户下载目录的路径
app.getPath('music'); // 获取用户音乐目录的路径
app.getPath('pictures'); // 获取用户图片目录的路径
app.getPath('videos'); // 获取用户视频目录的路径
app.getPath('logs'); // 获取应用程序的日志文件夹路径
app.getPath('desktop'); // 获取系统桌面路径

数据库配置

'use strict'

const DataStore = require('lowdb')
const FileSync = require('lowdb/adapters/FileSync')
const path = require('path')
const fs = require('fs-extra')
const { app, remote } = require('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.defaults({
  project: [], // 存储已上传项目的 CDN 配置信息
  settings: {
    ftp: '', // ftp 用户配置
    s3: '', // s3 用户配置
  }
}).write()

module.exports = db

后台执行命令行程序

由于睿传是一个命令行工具,并没有对外提供 Node.js API,所以用户点击上传按钮时候需要通过 Electron在后台运行命令行程序,并且将命令行运行的日志实时渲染到应用的日志界面中,所以在这里利用 Node.js  child_process子进程的方式来处理。

'use strict'
import { ipcMain } from 'electron'
import { exec } from 'child_process'
import path from 'path'
import fixPath from 'fix-path'
import { logError, logInfo, logExit } from './log'
const cmdPath = path.resolve(__static, 'lib/rapid_trans') // 睿传路径
let workerProcess
ipcMain.on('upload', (e, {dirPath, cdnPath, isCover}) => {
  runUpload(dirPath, cdnPath, isCover)
})

function runUpload (dirPath, cdnPath, isCover) {
  let cmdStr = `node src/rapid-trans.js -s "${dirPath}" -p "${cdnPath}" -q`
  if (isCover) {
    cmdStr += ' -f'
  }
  fixPath()
  logInfo('================== 开始上传 ================== \n')
  workerProcess = exec(cmdStr, {
    cwd: cmdPath
  })
  workerProcess.stdout.on('data', function (data) {
    logInfo(data)
  })

  workerProcess.stderr.on('data', function (data) {
    logError(data)
  })

  workerProcess.on('close', function (code) {
    logExit(code)
    logInfo('================== 上传结束 ================== \n')
  })
}

// log.js
'use strict'
const win = global.mainWindow
export function logInfo (msg) {
  win.webContents.send('logInfo', msg)
}

export function logError (msg) {
  win.webContents.send('logError', msg)
}

export function logExit (msg) {
  win.webContents.send('logExit', msg)
}

export default {
  logError,
  logExit,
  logInfo
}

应用打包

应用开发完成后需要进行打包,我们可以使用 electron-builder 将应用打包成 Windows、Mac 平台的应用。

在执行npm run build之前需要在 package.json进行打包配置的编辑。

{
  "build": {
    "productName": "S3上传工具",  // 应用名称,最终生成的可执行文件的名称
    "appId": "com.autohome.s3", // 应用 APP.ID
    "directories": {
      "output": "build" // 打包后的输出目录
    },
    "asar": false, // 关闭 asar 格式
    "publish": [
      {
        "provider": "generic", // 服务器提供商
        "url": "http://xxx.com:8003/oss" // 更新服务器地址
      }
    ],
    "releaseInfo": {
      "releaseNotes": "新版更新" // 更新说明
    },
    "files": [
      "dist/electron/**/*",
      {
        "from": "dist/electron/static/lib/rapid_trans/node_modules",
        "to": "dist/electron/static/lib/rapid_trans/node_modules"
      } // 将睿传的依赖打包进应用
    ],
    // 平台的一些配置
    "dmg": {
      "contents": [
        {
          "x": 410,
          "y": 150,
          "type": "link",
          "path": "/Applications"
        },
        {
          "x": 130,
          "y": 150,
          "type": "file"
        }
      ]
    },
    // 应用图标
    "mac": {
      "icon": "build/icons/icon.icns"
    },
    "win": {
      "icon": "build/icons/icon.ico"
    },
    "linux": {
      "icon": "build/icons"
    }
  }
}

应用更新提示

由于软件不进行 App Store 的上架,只在团队内部使用没有配置证书,不配置证书的话 Mac 中无法进行自动更新安装,所以我们在检测到用户的当前版本不是最新版本的时候是采用的弹层提示的方式让用户自己下载。

使用 electron-updater 打包的应用自动更新非常方便,将打包后 build 目录下的 latest-mac.yml文件上传至package.json 中配置的 publish.url 目录下,并且在主进程文件中监听 update-availabl事件。

// 主进程 main.js
import { autoUpdater } from 'electron-updater'
// 关闭自动下载
autoUpdater.autoDownload = false

// 应用可更新
autoUpdater.on('update-available', (info) => {
  // 通知渲染进程应用需要更新
  mainWindow.webContents.send('updater', info)
})

app.on('ready', () => {
  if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates()
})

// 渲染进程 updater.js
import { ipcRenderer, shell } from 'electron'
import { MessageBox } from 'element-ui'

ipcRenderer.on('updater', (e, info) => {
  MessageBox.alert(info.releaseNotes, `请升级${info.version}版本`, {
    confirmButtonText: '立即升级',
    showClose: false,
    closeOnClickModal: false,
    dangerouslyUseHTMLString: true,
    callback (action) {
      if (action === 'confirm') {
        // 在用户的默认浏览器中打开存放应用安装包的网络地址
        shell.openExternal('http://10.168.0.49/songjinda/s3_tool/download/')
        return false
      }
    }
  })
})

作者|宋金达

posted @ 2023-02-15 08:28  古道轻风  阅读(1621)  评论(0编辑  收藏  举报