如何使用Electron构建桌面端应用

本文介绍如何构建一个electron应用,适用于有HTML/CSS/JavaScript基础的人阅读,在开始前需要先在电脑上安装Node.js。

Electron介绍

Electron 是由 Github开发的开源框架,它允许开发者使用Web技术来开发跨平台的桌面应用。著名项目包括GitHub的Atom和微软的Visual Studio Code。

Electron架构的核心由三部分组成:

  • Chromium: 为electron提供了强大的UI能力,可以不考虑兼容性的情况下,利用强大的Web生态来开发界面
  • Node.js:让electron有了底层的操作能力,比如文件的读写,甚至是集成C++等等操作,并可以使用大量开源的 npm 包来完成开发需求
  • Native API:Native API让electron有了跨平台和桌面端的原生能力,比如说它有统一的原生界面,窗口、托盘这些

主进程和渲染进程

electron是多进程架构,在开始项目搭建之前,先来了解下electron的两个核心概念:主进程渲染进程

主进程

主进程负责创建和管理BrowserWindow实例以及各种应用程序事件。它还可以执行诸如注册全局快捷方式,创建系统菜单和对话框,响应自动更新事件等操作。应用程序的入口点将指向将在主进程中执行的JavaScript文件。
一个项目有且只有一个主进程

渲染进程

渲染过程负责运行应用程序的用户界面。
每创建一个窗口都会创建一个渲染进程;并且每个渲染进程都是独立的。

项目工程搭建

接下来开始搭建electron项目工程

使用quick-start创建项目

为了简化步骤,可以使用 quick-start 来搭建electron项目

// 第一步:clone electron-quick-start
git clone https://github.com/electron/electron-quick-start
// 第二步:安装依赖
cd electron-project && npm install
// 运行项目
npm run start

搭建过程中可能会遇到安装依赖失败的问题,具体解决方法可以文末

项目结构介绍

├── index.html
├── main.html
├── package.json
├── preload.js

上图是刚刚创建的项目的目录结构,接下来介绍下electron应用中最主要的三种文件

package.json(元数据)

配置文件。配置应用的相关信息及工程依赖。其中main字段定义了应用的启动入口,在此项目中,入口文件为src/main.js

mian.js (启动文件)

运行在项目主进程中。在此文件中启动应用,创建浏览器,加载页面。

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

function createWindow () {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })
  mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
  createWindow()

  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})
app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
})

app代表着整个应用,用app.on监听应用的状态,当达到ready状态后,使用BrowserWindow(electron提供的模块)创建了一个宽800,高600的窗口,再使用loadFile,在窗口中加载 index.html 文件。
webPreferencesBrowserWindow的属性,用来设置网页功能。preloadwebPreferences属性的参数,在页面运行其他脚本之前预先加载指定的脚本,无论页面是否集成Node, 此脚本都可以访问所有Node API 脚本路径为文件的绝对路径。
可以在createWindow方法最后添加mainWindow.webContents.openDevTools()代码,表示打开控制台

index.html

运行在项目渲染进程中。该文件为项目展示的界面,类似于移动端开发的h5界面。

项目开发

主进程和渲染进程的通信

electron 可以使用node.js的api和Native API,但是electron不建议直接在渲染进程(即界面)中直接使用,需要通过两个进程的通信,在主进程中完成操作。

考虑到在网页中直接调用原生的 GUI 容易造成资源溢出,这很危险,开发者不能这么使用。如果开发者想要在网页上执行 GUI 操作,必须要通过渲染器进程和主进程的通信实现。

主进程和渲染进程的通信可以使用ipc模块来实现,以实现以下需求为例,简单介绍如何实现渲染器进程和主进程的通信实现。

页面上有一个按钮和一个输入框,当点击按钮之后,向主进程发送了一个 write-file 的消息,当主进程接收到消息之后,在安装目录下创建一个叫 hello.txt的文件,并写入输入框内的内容。文件生成后发送成功的消息给渲染进程,弹出提示告诉用户已完成。

更改preload.js

在preload.js文件中,通过contextBridge模块,将ipcRenderer模块的api暴露给渲染器

contextBridge Create a safe, bi-directional, synchronous bridge across isolated contexts

// preload.js
const ipcRenderer = require('electron').ipcRenderer
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('ipcRenderer', {
  on: (eventName, callback) => {
    ipcRenderer.on(eventName, callback)
  },
  once: (eventName, callback) => {
    ipcRenderer.once(eventName, callback)
  },
  send: ipcRenderer.send,
})

ps:
在低版本的electron中,直接赋值在window上
window.ipcRenderer = require('electron').ipcRenderer

渲染进程 => 主进程
  1. 在index.html渲染进程中添加一个input输入框和button按钮,在点击按钮时获取输入框的内容,并使用ipcRenderer.send方法发送write-file事件

ipcRenderer 是一个 EventEmitter 的实例。 可以使用它提供的一些方法从渲染进程 (web 页面) 发送同步或异步的消息到主进程。 也可以接收主进程回复的消息。

// index.html
// html部分
<input type="text" id="input">
<button id="button">say hi</button>
// js部分
document.getElementById('button').onclick =  function () {
  const content = document.getElementById('input').value
  window.ipcRenderer.send('write-file', {
    content: content,
  });
};
  1. 在main.js主进程中使用ipcMain.on监听write-file事件,接受到信息后使用node的fs模块生成并写入hello.txt文件

ipcMain 是一个 EventEmitter 的实例。 当在主进程中使用时,它处理从渲染器进程(网页)发送出来的异步和同步信息。 从渲染器进程发送的消息将被发送到该模块。

const { app, BrowserWindow, ipcMain } = require('electron');
const fs = require('fs')
ipcMain.on('write-file', (evt, data) => {
  fs.writeFileSync('./hello.txt', data.content, 'utf-8')
})
主进程 => 渲染进程
  1. 在文件生成成功后,使用当前窗口 BrowserWindow 实例的webContents属性的send方法,发送file-complete 事件

webContents是一个EventEmitter. 负责渲染和控制网页, 是 BrowserWindow 对象的一个属性。

+ let mainWindow
const createWindow = () => {
+  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
    },
  });
  mainWindow.loadFile(path.join(__dirname, 'index.html'));
  mainWindow.webContents.openDevTools();
};
ipcMain.on('write-file', (evt, data) => {
  fs.writeFileSync('./hello.txt', data.content, 'utf-8')
+  mainWindow.webContents.send('write-complete', { status: true })
})
  1. 在index.html渲染进程使用ipcRenderer.on方法监听事件
document.getElementById('button').onclick =  function () {
  const content = document.getElementById('input').value
  window.ipcRenderer.send('write-file', {
    content: content,
  });
+  window.ipcRenderer.once('write-complete', (event, data) => {
+    if (data.status) alert('文件生成成功')
+  });
};

打包及自动更新

应用打包

使用electron-builder来打包

安装依赖

npm install electron-builder --save-dev

配置build属性

在package.json文件中,添加build属性

  "build": {
    "appId": "com.test.electron", // 包名
    "productName": "electron-project", // 项目名,也是生成包的前缀名
    "mac": { // mac平台相关配置
      "icon": "public/icon.png", // mac应用图标,最小为512x512
      "target": [ "dmg", "zip" ]
    },
    "dmg": {
      "window": { // dmg安装器窗口设置
        "x": 200,
        "y": 200,
        "width": 400,
        "height": 400
      }
    },
    "win": { // windows平台相关配置
      "icon": "public/icon.png" // windows应用图标,最小为256x256
    },
    "nsis": { // 安装过程的配置
      "oneClick": false,
      "allowToChangeInstallationDirectory": true, //允许修改安装目录
      "createDesktopShortcut": "always", //创建桌面图标
      "createStartMenuShortcut": false, //创建开始菜单图标
      "installerIcon": "", // 安装图标
      "uninstallerIcon": "" // 卸载图标
    }
  },
配置打包命令

在package.json文件中添加script命令

"script": {
  "start": "electron .",
+  "build": "electron-builder"
}

在不同平台的环境下运行npm run build打包,成功后安装包在dist文件夹下
在打包过程中可能会遇到下载文件失败的问题,具体解决方法看文末

自动更新

可以使用electron-builder来实现自动更新

安装electron-builder

npm install electron-builder --save-dev

配置publish

配置publish 字段,在打包后生成latest.yml文件,程序更新依赖这个文件做版本判断
latest.yml文件是打包过程生成的文件,为避免自动更新出错,打包后禁止对latest.yml文件做任何修改。如果文件有误,必须重新打包获取新的latest.yml文件

// package.json
 "build": {
+    "publish": [
+      {
+        "provider": "generic",
+        "url": ""
+      }
+    ],
 }
主进程代码

在main.js中添加以下autoUpdater代码

// main.js
const { autoUpdater } = require('electron-updater')
function createWindow () {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })
  mainWindow.loadFile(path.join(__dirname, 'index.html'))
  mainWindow.webContents.openDevTools()
  mainWindow.webContents.on('did-finish-load', function () {
    setTimeout(() => {
      updateApp()
    }, 5000)
  })
}
function sendUpdateMessage(message, data) {
  mainWindow.webContents.send("message", { message, data });
}
function updateApp () {
  let url = 'http://127.0.0.1:8080/' + process.platform // 安装包所在服务器地址,本地测试可以使用http-server搭建静态服务器
  autoUpdater.setFeedURL(url); // 设置更新服务器的地址
  autoUpdater.on("error", function(message) { // 报错
    sendUpdateMessage("error", message);
  })
  autoUpdater.on("checking-for-update", function(message) { // 检查更新事件
    sendUpdateMessage("checking-for-update", message);
  })
  autoUpdater.on("update-available", function(message) { // 有需要更新的版本
    sendUpdateMessage("update-available", message);
  })
  autoUpdater.on("update-not-available", function(message) { // 没有需要更新的版本
    sendUpdateMessage("update-not-available", message);
  })
  autoUpdater.on("download-progress", function(progressObj) { // 更新下载进度事件
    sendUpdateMessage("downloadProgress", progressObj);
  })
  autoUpdater.on("update-downloaded", function( // 下载成功事件
    event,
    releaseNotes,
    releaseName,
    releaseDate,
    updateUrl,
    quitAndUpdate
  ) {
    ipcMain.on("updateNow", (e, arg) => {
      // 停止当前程序并安装
      autoUpdater.quitAndInstall();
    });
    sendUpdateMessage("isUpdateNow", null);
  })
  autoUpdater.checkForUpdates(); // 执行检查更新
}
渲染进程代码

在渲染进程的js文件中添加以下代码

window.ipcRenderer.on("message", (event, { message, data }) => {
  switch (message) {
    case "isUpdateNow":
      if (confirm("现在更新?")) {
        ipcRenderer.send("updateNow");
      }
      break;
    default:
      break;
  }
});
上传应用

更新packge.json文件中的版本号,打包后将安装包和yml文件(MAC下是latest-mac.yml,zip和dmg文件;Windows下是latest.yml和exe文件)放在服务器对应平台的目录下

  • mac


    80285313-F89E-4FE0-8459-EAA643012081.png
  • windows


    FC5639BA-90CA-48EC-B31A-42A2063E9283.png

打开低版本的应用,electron-updater会通过对应url下的yml文件检查更新

问题

依赖安装失败问题解决

在创建项目的过程中,安装依赖可能会报错,可以尝试使用以下方式解决
1. 设置国内electron镜像地址

mac直接运行以下命令

export ELECTRON_MIRROR="https://npm.taobao.org/mirrors/electron/"

window需要添加环境变量 ELECTRON_MIRROR 值为 https://npm.taobao.org/mirrors/electron/
2. 重新npm install

electron-builder打包失败

9B96BB0C-E3B0-491B-AA8A-9DD08DE127DA.png

electron-builder 在打包时会检测cache中是否有electron 包,如果没有的话会从github上拉去,在国内网络环境中拉取的过程大概率会失败,所以你可以自己去下载一个包放到cache目录里
各个平台的目录地址

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

推荐阅读更多精彩内容