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.jsondevDependencies 可以看到 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.jsonmain 中设置 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 的时候,可以在 webPreferencespreload 设置预加载脚本的位置,如下:

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,webPreferencescontextIsolation 也需要设置为 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.jsipcRenderer 暴露到了 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.jsonscripts 中添加打包命令:

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

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

{
  "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"
  }
}

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

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

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

打包之前还需要在 package.jsonbuild 中配置软件信息和文件过滤,否则 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.jsondependencies 中的项目依赖都放到 devDependencies 中,Webpack 只会打包项目中引入的模块,electron-builder 不会打包 devDependencies 中的模块。

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

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

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

类似文章: