Electron 是一个使用 HTML、CSS、JavaScript、Node.js 来开发桌面应用程序的开源框架,它具有开发速度快和跨平台方便的优点。虽然打包的软件体积比较大,体验也不如 C++ 的 QT 和 C# 的 WPF,但是 Electron 只需要前端程序员就能开发,现在的 PC 软件大多数也是使用 Electron 开发。

Vue 是一个前端 Web 框架。使用 Vue 可以很方便的开发单页面应用,Vue 官方也提供了开箱即用的路由管理和状态管理插件,对于新手来说,Vue 是比较容易上手的框架。

之前我开发 Electron 使用了 Vue CLI Plugin Electron Builder ,这是一个能快速把 Electron 添加到 Vue 项目的 Vue 插件,但这个插件使用的打包工具可配置的选项比较少,官方的文档也不是太详细。

electron-vue 是一套配置好的使用 Vue + Electron 的项目样板,可以很方便的搭建 Vue + Electron 的项目,但是 electron-vue 使用的资源太老了,估计是很早以前就停止维护了,Electron 使用的还是 2.0.4 的版本,现在的 Electron 已经到 19.0.8 了,官方的帮助文档基本上不能用。

我这里会使用 Vue-cli 来初始化一个 Vue 项目,然后手动安装和配置 electron 和 electron-builder。

初始化 Vue

安装 Vue-cli:

npm install -g @vue/cli

初始化一个 Vue2 项目:

vue create projectName

其中 projectName 是项目名称,

我这里直接使用 Vue2 的默认配置:

 Default ([Vue 3] babel, eslint)
> Default ([Vue 2] babel, eslint)
  Manually select features

项目安装完成后使用 cd 项目名称 可以进入项目目录,使用 npm run serve 可以运行项目,使用 npm run build 可以编译打包项目。

安装 Electron

进入 Vue 项目目录后输入:

npm install --save-dev electron

安装 Electron。

安装完成后在 package.json 的 devDependencies 可以看到 Electron 的版本。

配置

可以在项目的 src 目录下创建一个 electron-main.js 作为 Electron 的入口文件,在 electron-main.js 中加入一些 Electron 的代码来创建一个窗口:

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

let mainWindow;  // 用来保存主窗口对象的引用

// 当 Electron 完成初始化并准备创建浏览器窗口时被调用
app.on('ready', () => {
  // 创建主窗口
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600
  });

  // 加载页面文件
  if (app.isPackaged) {
    // 如果是打包好的就加载打包的 HTML 文件
    mainWindow.loadFile('dist/index.html');
  }else {
    // 如果没有打包就直接从本地服务器加载
    mainWindow.loadURL('http://localhost:9999/');
  }

  // 关闭事件
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});

上面在 src 目录下创建了一个 electron-main.js 作为 Electron 的入口文件,下面在 package.json 的 main 中设置 Electron 的入口文件:

{
  "name": "ocr-electron",
  "version": "0.1.0",
  "main": "src/electron-main.js",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.8.3",
    "vue": "^2.6.14"
  }
}

上面的 package.json 只包含一部分内容,完整的 package.json 比较长,我会放到后面。

在 Electron 入口文件中,我会根据是否打包来选择页面文件,如果没有打包就从本地服务器来加载页面,如果已打包就加载 dist 目录的 index.html,Vue 编译后的页面文件也在 dist 目录。

Vue 官方配置的项目打包完成后的 HTML script 如下:

<script src="/js/xxxx.js"></script>

在文件路劲最前面会有一个 /,这种路劲在服务器环境是可以正常加载 JS 文件的,但如果在浏览器直接打开 HTML 文件就会出现找不到 JS 文件的情况,Electron 也找不到 JS 文件。

下面通过更改 vue.config.js 来使用正常的相对路劲加载 JS 文件,如果你的 Vue 项目目录下没有 vue.config.js 可以创建一个,在模块的对象根部加入 publicPath: './',下面是我的 vue.config.js :

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  publicPath: './',
  devServer: {
    port: 9999
  }
})

不同版本的 Vue-cli 创建的项目 vue.config.js 的内容可能会有些不一样,publicPath: './' 加到对象根部就可以。

除了增加 publicPath 外,我这里还更改了本地服务器的端口,我在 Electron 入口文件设置的加载页面文件的地址是 http://localhost:9999/ ,本地服务器端口我设置的也是 9999。

下面还需要在 package.json 中配置 Electron 的运行命令,在 script 中加入一个 start 命令,start 命令的值是 electron . ,如下:

{
  "name": "ocr-electron",
  "version": "0.1.0",
  "main": "src/electron-main.js",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "start": "vue-cli-service build && electron ."
  }
}

上面的 package.json 为了方便查看,还是只包含一部分内容。

下面尝试运行一下,输入:

npm run serve

Vue 的本地服务器启动后再开一个命令行,输入:

npm run start

Electron 检测到没有打包就会从 http://localhost:9999/ 加载页面,如下:

Electron窗口显示着Vue的默认页面

Electron 成功运行。

如果渲染进程的页面内容改变,Electron 也会自动刷新。

测试

下面测试一下 Electron 的 API:

新版的 Electron 是不能在渲染进程使用 Electron API 的,也不推荐在渲染进程使用 Node 模块,而且在默认的 Webpack 中如果在 Vue 组件中引入 Node 模块也肯定会报错。

Electron 官方推荐的是把 Electron API 和 Node 模块放到主进程,渲染进程如果要使用 Electron API 和 Node 模块就使用 ipc 给主进程发送请求,由主进程来运行 Electron API 和 Node 模块。

渲染进程要和主进程通信就需要用到 Electron 的 ipcRenderer 模块,在渲染进程是不能直接引入 ipcRenderer 的,Electron 提供了一个预加载脚本,预加载脚本会在网页内容加载之前运行,预加载可以运行 Electron API ,也可以访问页面的 window 。

Electron 提供了一个 contextBridge 模块,在预加载脚本可以使用 contextBridge 把一部分 Electron API 暴露到 window 给渲染进程使用,渲染进程可以通过 window 来使用 Electron API。

下面在 Vue 项目的 src 目录创建一个 preload.js 作为预加载脚本,在 preload.js 中把 ipcRenderer 模块暴露给渲染进程使用:

const {contextBridge, ipcRenderer} = require('electron');

// 把 ipcRenderer 暴露给渲染进程
contextBridge.exposeInMainWorld('electronApi', {
  ipcRenderer
});

contextBridge.exposeInMainWorld 的第一个参数是 API 名称,可以自定义,我定义的是 electronApi。

在 Electron 入口文件创建 BrowserWindow 的时候,可以在 webPreferences 的 preload 设置预加载脚本的位置,如下:

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

let mainWindow;  // 用来保存主窗口对象的引用

// 当 Electron 完成初始化并准备创建浏览器窗口时被调用
app.on('ready', () => {
  // 创建主窗口
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true
    }
  });

  // 加载页面文件
  if (app.isPackaged) {
    // 如果是打包好的就加载打包的 HTML 文件
    mainWindow.loadFile('dist/index.html');
  }else {
    // 如果没有打包就直接从本地服务器加载
    mainWindow.loadURL('http://localhost:9999/');
  }

  // 关闭事件
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});

要在预加载脚本中使用 contextBridge 暴露 Electron API,webPreferences 的 contextIsolation 也需要设置为 true。

下面我需要实现在 Vue 组件中点击按钮后弹出 Electron 的消息对话框,App 组件:

<template>
  <div id="app">
    <button type="button" @click="showDialog">显示对话框</button>
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

上面只有 显示对话框 的 button 是我新增的,其他元素都是 Vue 项目自动生成的,JavaScript 如下:

import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  methods: {
    showDialog() {
      window.electronApi.ipcRenderer.send('show-dialog');
    }
  }
}

HelloWorld 组件也是 Vue 创建项目时自动生成的,主要看 showDialog 方法。

我在 preload.js 把 ipcRenderer 暴露到了 window ,这里就可以直接调用 ipcRenderer 和主进程通信。

主进程:

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

let mainWindow;  // 用来保存主窗口对象的引用

// 当 Electron 完成初始化并准备创建浏览器窗口时被调用
app.on('ready', () => {
  // 创建主窗口
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true
    }
  });

  // 加载页面文件
  if (app.isPackaged) {
    // 如果是打包好的就加载打包的 HTML 文件
    mainWindow.loadFile('dist/index.html');
  }else {
    // 如果没有打包就直接从本地服务器加载
    mainWindow.loadURL('http://localhost:9999/');
  }

  // 关闭事件
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});

// 监听显示对话框请求
ipcMain.on('show-dialog', () => {
  // 显示对话框
  dialog.showMessageBox({
    type: 'info',
    title: 'Mr. Ma\'s Blog 博主',
    message: '我的 Github 是 https://github.com/changbin1997',
    buttons: ['确定', '取消']
  });
});

测试效果如下:

在Vue组件中调用Electron的消息对话框

成功弹出对话框。

打包

上面安装的 electron 可以运行,但是还不能打包为 exe 之类的可执行程序,打包需要安装 electron-builder :

npm install electron-builder --save-dev

安装完成后在 package.json 的 scripts 中添加打包命令:

{
  "scripts": {
    "pack": "vue-cli-service build && electron-builder --dir",
    "dist": "vue-cli-service build && electron-builder"
  }
}

添加完成后我的 package.json 的 scripts 如下:

{
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "start": "electron .",
    "pack": "vue-cli-service build && electron-builder --dir",
    "dist": "vue-cli-service build && electron-builder"
  }
}

后面添加的 pack 和 dist 命令前半部分是 Vue 的编译命令,和 build 的内容是一样的,后半部分是 Electron 的打包命令。

运行 npm run pack 或 npm run dist 后,Vue 组件会被编译打包到 dist 目录,然后 Electron 会开始打包。

我在 Electron 入口文件 electron-main.js 中通过 app.isPackaged 来检测是否打包,如果已打包就从 dist 目录加载 index.html 页面文件。

打包之前还需要在 package.json 的 build 中配置软件信息和文件过滤,否则 electron-builder 会把 src 中的源码和 Vue 组件打包进去,可能会导致打包后的软件无法正常运行。

我的 package.json 中的 build 配置如下:

{
  "build": {
    "appId": "ocr-electron",
    "productName": "ocr-electron",
    "icon": "public/favicon.ico",
    "copyright": "Copyright © 2022",
    "compression": "maximum",
    "asar": true,
    "win": {
      "icon": "public/favicon.ico",
      "target": "nsis",
      "legalTrademarks": "changbin1997"
    },
    "nsis": {
      "oneClick": false,
      "perMachine": true,
      "allowToChangeInstallationDirectory": true,
      "createDesktopShortcut": true,
      "createStartMenuShortcut": false
    },
    "directories": {
      "output": "release"
    },
    "files": [
      "**/*",
      "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
      "!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
      "!**/node_modules/*.d.ts",
      "!**/node_modules/.bin",
      "!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}",
      "!.editorconfig",
      "!**/._*",
      "!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}",
      "!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}",
      "!**/{appveyor.yml,.travis.yml,circle.yml}",
      "!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}",
      "!**/src/components/**",
      "!**/src/assets/**",
      "!**/src/main.js",
      "!**/src/App.vue",
      "!**/public/index.html"
    ]
  }
}

下面是一部分配置内容说明:

  • appId: 软件的一个标识符
  • productName: 软件名称
  • icon: 软件图标
  • copyright: 版权信息
  • compression: 软件包的压缩级别
  • win: Windows 相关的配置
  • win > icon: Windows 的软件图标
  • win > target: 封装类型,我设置的 nsis 是 exe安装包
  • win > legalTrademarks: 商标信息
  • nsis: 安装包配置
  • nsis > oneClick: 一键安装
  • nsis > perMachine: 为这台计算机上的所有用户安装,也就是安装到 Program Files 而不是用户目录
  • nsis > allowToChangeInstallationDirectory: 是否允许更改安装位置
  • nsis > createDesktopShortcut: 是否创建桌面快捷方式
  • nsis > createStartMenuShortcut: 是否创建开始菜单快捷方式
  • directories > output: 打包后的软件存放位置
  • files: 设置要打包的文件和文件过滤,其中 **/* 是设置当前目录为程序目录,! 开头的是需要忽略的文件

有些参数的值我这里写的不是太详细,参数值说明也可以参考我之前写的 使用 electron-builder 打包 Electron 应用 ,更详细的配置说明可以看 electron-builder 官方文档 。

运行命令和打包参数都配置完成后就可以开始打包了,输入:

npm run dist

可以生成安装包和包含 exe 的软件目录。

输入:

npm run pack

可以生成包含 exe 的软件目录,不会生成安装包。

第一次打包 electron-builder 会下载平台所需的依赖,如果你在国内使用的话,建议开代理,否则可能会打包失败。

软件体积优化

我在上面的 files 配置中已经做了一些优化,README.md、.idea 、.log 这些文件都会被忽略。

electron-builder 会根据 package.json 的项目依赖来打包文件,如果你的项目依赖被 Webpack 打包过,electron-builder 也会再打包一次,像 Vue、Bootstrap、Element UI 这一类被 Webpack 打包过的就没必要让 electron-builder 打包进去。

解决方法也比较简单,把 package.json 的 dependencies 中的项目依赖都放到 devDependencies 中,Webpack 只会打包项目中引入的模块,electron-builder 不会打包 devDependencies 中的模块。

安装模块的时候可以加 --save-dev 参数,安装完成后就会直接在 devDependencies 中。

一些不需要 Webpack 打包,但是又是项目依赖的模块,例如 SQLite 之类的还是需要放到 dependencies 中让 electron-builder 打包的,否则可能会出现找不到模块的情况。

把需要 Webpack 打包的项目依赖放到 devDependencies 中,再使用 electron-builder 打包,应该可以发现软件体积小了一些,软件的运行不会受到任何影响。

类似文章: