最近打算做一个桌面应用程序, 考虑了几种不同的方案:

  1. .NET : 这是以前最熟悉的, 不过.NET 的框架也有好几个版本, 因为使用的 Winddows 版本不同, 最终用户还是可能需要安装不用的 .NET Framework, 当然, .NET 现在已经忘得差不多了。

  2. Flutter : Flutter 现在已经可以支持各种不同的操作系统桌面开发, 但是它的路由不怎么好用, 没有类似 Vue 的那个子路由, 想要更新界面中的局部内容, 操作比较麻烦, 输入数据的处理也不够方便。

  3. Electron: 可以配合Vue 等前端框架开发应用, 方便灵活, 前两天发现自己最爱用的UI框架 Vuetify 已经发布了 3.0.1 的正式版, 正式支持 Vue3, 所以打算用这个来试一下。另外的一好处是, 要实现 Web 版本, 代码几乎不需要做什么修改。

使用 Vuetify 模板创建 Vue3项目

Vuetify 带有一个创建项目的模板, 使用该模板创建好的项目已经完成了 Vuetify 及相关依赖的配置, 比手动方式更加方便。创建项目命令如下:

1
yarn create vuetify

代码执行过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
PS D:\sources\electron_repos> yarn create vuetify
yarn create v1.22.5
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Installed "create-vuetify@1.0.4" with binaries:
- create-vuetify
[############] 12/12
Vuetify.js - Material Component Framework for Vue

√ Project name: ... vite-vuetify-electron-app
√ Use TypeScript? ... No / Yes
√ Would you like to install dependencies with yarn, npm, or pnpm? » yarn

◌ Generating scaffold...
◌ Installing dependencies with yarn...

yarn install v1.22.5
info No lockfile found.
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
Done in 3.35s.

vite-vuetify-electron-app has been generated at D:\sources\electron_repos\vite-vuetify-electron-app

Discord community: https://community.vuetifyjs.com
Github: https://github.com/vuetifyjs/vuetify
Support Vuetify: https://github.com/sponsors/johnleider
Done in 34.09s.

进入项目文件夹, 运行项目, 效果如以下所示:

1
2
3
4
5
6
7
8
9
10
PS D:\sources\electron_repos> cd .\vite-vuetify-electron-app\
PS D:\sources\electron_repos\vite-vuetify-electron-app> yarn dev
yarn run v1.22.5
warning package.json: No license field
$ vite

VITE v3.2.4 ready in 382 ms

➜ Local: http://127.0.0.1:3000/
➜ Network: use --host to expose

使用 yarn create vuetify 创建的项目不包含路由状态管理模块, 需要自行添加。

配置 Electron

安装 Electron

首先安装electronVuetify项目, 命令如下:

1
yarn add --dev electron

配置文件

  1. vite.config.js 中添加 path 配置
1
2
3
4
5
import path from 'path'  // 新增引入模块

export default defineConfig({
base: path.resolve(__dirname, './dist/'), // 新增
})

完整代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Plugins
import vue from '@vitejs/plugin-vue'
import vuetify from 'vite-plugin-vuetify'

// Utilities
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'

import path from 'path' // 新增引入模块

// https://vitejs.dev/config/
export default defineConfig({
base: path.resolve(__dirname, './dist/'), // 新增
plugins: [
vue(),
// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin
vuetify({
autoImport: true,
}),
],
define: { 'process.env': {} },
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
extensions: [
'.js',
'.json',
'.jsx',
'.mjs',
'.ts',
'.tsx',
'.vue',
],
},
server: {
port: 3000,
},
})

  1. 在项目根目录创建名为 index.js 的新文件, 编辑内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// index.js

// 控制应用生命周期和创建原生浏览器窗口的模组
const { app, BrowserWindow } = require('electron')
const path = require('path')

function createWindow() {
// 创建浏览器窗口
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

// 加载 index.html
mainWindow.loadFile('dist/index.html') // 此处跟electron官网路径不同,需要注意

// 打开开发工具
// mainWindow.webContents.openDevTools()
}

// 这段程序将会在 Electron 结束初始化
// 和创建浏览器窗口的时候调用
// 部分 API 在 ready 事件触发后才能使用。
app.whenReady().then(() => {
createWindow()

app.on('activate', function () {
// 通常在 macOS 上,当点击 dock 中的应用程序图标时,如果没有其他
// 打开的窗口,那么程序会重新创建一个窗口。
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

// 除了 macOS 外,当所有窗口都被关闭的时候退出程序。 因此,通常对程序和它们在
// 任务栏上的图标来说,应当保持活跃状态,直到用户使用 Cmd + Q 退出。
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})

// 在这个文件中,你可以包含应用程序剩余的所有部分的代码,
// 也可以拆分成几个文件,然后用 require 导入。
  1. 在项目根目录创建名为 preload.js 的新文件, 编辑内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// preload.js

// 所有Node.js API都可以在预加载过程中使用。
// 它拥有与Chrome扩展一样的沙盒。
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}

for (const dependency of ['chrome', 'node', 'electron']) {
replaceText(`${dependency}-version`, process.versions[dependency])
}
})
  1. 编辑 package.json

为了确保能够运行相关electron的命令,需要修改package.json文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// package.json
{
"name": "vite-vuetify-electron-app",
"version": "0.0.0",
"main": "index.js", // 新增
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"electron:serve": "electron ." // 新增
},
"dependencies": {
"@mdi/font": "7.0.96",
"roboto-fontface": "*",
"vue": "^3.2.38",
"vuetify": "^3.0.0",
"webfontloader": "^1.0.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^3.0.3",
"electron": "^21.3.0",
"vite": "^3.1.9",
"vite-plugin-vuetify": "^1.0.0-alpha.12"
}
}

package.json文件不能包含 注释, 上面代码的注释只是为了方便标识修改的内容, 在实际开发的时候要把注释删除, 否则项目运行会出现异常

运行项目

运行 Electron 项目时, 要先执行 yarn build 构建项目, 生成 dist 文件夹, 才能执行 yarn electron:serve 运行项目

先构建项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PS D:\sources\electron_repos\vite-vuetify-electron-app> yarn build
yarn run v1.22.5
warning package.json: No license field
$ vite build
(!) "base" option should start with a slash.
(!) "base" option should end with a slash.
vite v3.2.4 building for production...
✓ 154 modules transformed.
dist/assets/materialdesignicons-webfont.861aea05.eot 1214.57 KiB
dist/assets/materialdesignicons-webfont.e52d60f6.woff2 376.33 KiB
dist/assets/materialdesignicons-webfont.48d3eec6.woff 548.61 KiB
dist/assets/materialdesignicons-webfont.bd725a7a.ttf 1214.36 KiB
dist/index.html 0.58 KiB
dist/assets/webfontloader.b777d690.js 12.42 KiB / gzip: 4.98 KiB
dist/assets/index.5048f0d0.js 113.46 KiB / gzip: 42.44 KiB
dist/assets/index.3b4c9523.css 567.04 KiB / gzip: 81.33 KiB
Done in 2.30s.

运行项目

1
2
3
4
PS D:\sources\electron_repos\vite-vuetify-electron-app> yarn electron:serve
yarn run v1.22.5
warning package.json: No license field
$ electron .

项目运行效果如下图所示:

动态模块热重载

按上面步骤建好的项目存在一个问题, 运行中的项目不能实时响应编辑修改后的代码, 所以为了方便开发调试, 需要添加动态模块热重载功能。

编辑index.js

mainWindow.loadFile('dist/index.html')更新为mainWindow.loadURL("http://localhost:3000"), 更新后的文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 控制应用生命周期和创建原生浏览器窗口的模组
const { app, BrowserWindow } = require('electron')
const path = require('path')

const NODE_ENV = process.env.NODE_ENV

function createWindow() {
// 创建浏览器窗口
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

// 加载 index.html
// mainWindow.loadFile('dist/index.html') // 此处跟electron官网路径不同,需要注意

mainWindow.loadURL("http://localhost:3000")


if (NODE_ENV === "development") {
mainWindow.webContents.openDevTools()
}

// 打开开发工具
// mainWindow.webContents.openDevTools()
}

// 这段程序将会在 Electron 结束初始化
// 和创建浏览器窗口的时候调用
// 部分 API 在 ready 事件触发后才能使用。
app.whenReady().then(() => {
createWindow()

app.on('activate', function () {
// 通常在 macOS 上,当点击 dock 中的应用程序图标时,如果没有其他
// 打开的窗口,那么程序会重新创建一个窗口。
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

// 除了 macOS 外,当所有窗口都被关闭的时候退出程序。 因此,通常对程序和它们在
// 任务栏上的图标来说,应当保持活跃状态,直到用户使用 Cmd + Q 退出。
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})

// 在这个文件中,你可以包含应用程序剩余的所有部分的代码,
// 也可以拆分成几个文件,然后用 require 导入。

编辑vite.config.js

修改文件vite.config.jsbase,修改后的文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Plugins
import vue from '@vitejs/plugin-vue'
import vuetify from 'vite-plugin-vuetify'

// Utilities
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'

import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
// base: path.resolve(__dirname, './dist/'), // 新增
base: "./", // 新增
plugins: [
vue(),
// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin
vuetify({
autoImport: true,
}),
],
define: { 'process.env': {} },
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
extensions: [
'.js',
'.json',
'.jsx',
'.mjs',
'.ts',
'.tsx',
'.vue',
],
},
server: {
port: 3000,
},
})

同时开启viteelectron服务

为了使viteelectron正常运行,需要先运行vite, 使得其开发服务器的url可以正常访问,然后再开启electron去加载url

此处需要安装两个库:

concurrently:阻塞运行多个命令, -k参数用来清除其它已经存在或者挂掉的进程

wait-on:等待资源,此处用来等待url可访问

首先来安装。

1
yarn add -D concurrently wait-on

接着更新文件package.json, scripts新增两条命令:

1
2
3
4
"scripts": {
"electron": "wait-on tcp:3000 && electron .",
"electron:serve": "concurrently -k \"yarn dev\" \"yarn electron\""
},

更新后完整内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"name": "vite-vuetify-electron-app",
"version": "0.0.0",
"main": "index.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"electron": "wait-on tcp:3000 && electron .",
"electron:serve": "concurrently -k \"yarn dev\" \"yarn electron\""
},
"dependencies": {
"@mdi/font": "7.0.96",
"roboto-fontface": "*",
"vue": "^3.2.38",
"vuetify": "^3.0.0",
"webfontloader": "^1.0.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^3.0.3",
"electron": "^21.3.0",
"vite": "^3.1.9",
"vite-plugin-vuetify": "^1.0.0-alpha.12"
}
}

运行

现已添加两条命令:

yarn electron为等待tcp协议3000端口可访问,然后执行electron

yarn electron:serve为阻塞执行开发服务器运行和yarn electron命令

运行项目只要执行命令yarn electron:serve即可,当修改项目文件时,桌面应用也将自动更新。

运行项目执行过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
PS D:\sources\electron_repos\vite-vuetify-electron-app> yarn electron:serve
yarn run v1.22.5
warning package.json: No license field
$ concurrently -k "yarn dev" "yarn electron"
warning package.json: No license field
warning package.json: No license field
$ wait-on tcp:3000 && electron .
$ vite
[0]
[0] VITE v3.2.4 ready in 451 ms
[0]
[0] ➜ Local: http://127.0.0.1:3000/
[0] ➜ Network: use --host to expose

打包

之前为了方便开发过程中的调试设置的热重载, 在打包后还是加载http://localhost:3000是无法运行的,因此, 此处需要先用vite打包好,然后使用electron-builder加载vite打包后的文件进行打包。

为了代码能够根据不同环境在运行时加载http://localhost:3000, 在打包时加载文件, 此处需要使用环境变量来切换生产开发环境。

环境变量

此处使用环境变量NODE_ENV来切换生产开发环境,生产环境为NODE_ENV=production, 开发环境为NODE_ENV=development,若有其它如release等环境可在此基础上拓展。

创建electron文件夹

在项目根目录下创建文件夹electron, 将index.jspreload.js文件移动进来。其结构如下所示:

1
2
3
4
5
6
.
├── README.md
├── electron
│ ├── index.js
│ └── preload.js
...

编辑electron/index.js

该文件主要是需要根据环境变量切换electron加载的内容,修改内容如下:

1
2
3
4
5
mainWindow.loadURL(
NODE_ENV === 'development'
? 'http://localhost:3000'
:`file://${path.join(__dirname, '../dist/index.html')}`
);

编辑后的完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 控制应用生命周期和创建原生浏览器窗口的模组
const { app, BrowserWindow } = require('electron')
const path = require('path')

const NODE_ENV = process.env.NODE_ENV

function createWindow () {
// 创建浏览器窗口
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

// 加载 index.html
// mainWindow.loadFile('dist/index.html') // 此处跟electron官网路径不同,需要注意

// mainWindow.loadURL("http://localhost:3000")
mainWindow.loadURL(
NODE_ENV === 'development'
? 'http://localhost:3000'
:`file://${path.join(__dirname, '../dist/index.html')}`
);

if (NODE_ENV === "development") {
mainWindow.webContents.openDevTools()
}

// 打开开发工具
// mainWindow.webContents.openDevTools()
}

// 这段程序将会在 Electron 结束初始化
// 和创建浏览器窗口的时候调用
// 部分 API 在 ready 事件触发后才能使用。
app.whenReady().then(() => {
createWindow()

app.on('activate', function () {
// 通常在 macOS 上,当点击 dock 中的应用程序图标时,如果没有其他
// 打开的窗口,那么程序会重新创建一个窗口。
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

// 除了 macOS 外,当所有窗口都被关闭的时候退出程序。 因此,通常对程序和它们在
// 任务栏上的图标来说,应当保持活跃状态,直到用户使用 Cmd + Q 退出。
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})

// 在这个文件中,你可以包含应用程序剩余的所有部分的代码,
// 也可以拆分成几个文件,然后用 require 导入。

编辑package.json

首先修改main 属性,将main: index.js改为main: electron/index.js

1
2
3
4
5
6
{
"name": "kuari",
"version": "0.0.0",
"main": "electron/index.js",
...
}

接着, 添加build属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"build": {
"appId": "com.your-website.your-app",
"productName": "ElectronApp",
"copyright": "Copyright © 2021 <your-name>",
"mac": {
"category": "public.app-category.utilities"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
},
"files": [
"dist/**/*",
"electron/**/*"
],
"directories": {
"buildResources": "assets",
"output": "dist_electron"
}
}

更新scripts属性

先安装两个库:

cross-env: 该库让开发者只需要注重环境变量的设置,而无需担心平台设置

electron-builder: electron打包库

1
yarn add -D cross-env electron-builder

更新后的scripts如下:

1
2
3
4
5
6
7
8
{
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"electron": "wait-on tcp:3000 && cross-env NODE_ENV=development electron .", // 此处需要设置环境变量以保证开发时加载url
"electron:serve": "concurrently -k \"yarn dev\" \"yarn electron\"",
"electron:build": "vite build && electron-builder" // 新增打包命令
}

更新后完整的packages.json代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
{
"name": "vuetify-electron-app",
"version": "0.0.0",
"main": "electron/index.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"electron": "wait-on tcp:3000 && cross-env NODE_ENV=development electron .",
"electron:serve": "concurrently -k \"yarn dev\" \"yarn electron\"",
"electron:build": "vite build && electron-builder"
},
"dependencies": {
"@mdi/font": "7.0.96",
"leancloud-storage": "^4.13.4",
"pinia": "^2.0.24",
"roboto-fontface": "*",
"vue": "^3.2.38",
"vue-router": "^4.1.6",
"vuetify": "^3.0.1",
"webfontloader": "^1.0.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^3.0.3",
"concurrently": "^7.5.0",
"cross-env": "^7.0.3",
"electron": "^21.3.0",
"electron-builder": "^23.6.0",
"vite": "^3.1.9",
"vite-plugin-vuetify": "^1.0.0-alpha.12",
"wait-on": "^6.0.1"
},
"build": {
"appId": "hujiyi.github.io",
"productName": "Teaching Plan",
"copyright": "Copyright © 2022 laohoo",
"mac": {
"category": "public.app-category.utilities"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
},
"files": [
"dist/**/*",
"electron/**/*"
],
"directories": {
"buildResources": "assets",
"output": "dist_electron"
}
}
}

打包

直接执行打包命令即可开始打包。

1
2
yarn electron:build

打包完成之后,会多出两个文件夹distdist_electron,其文件结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── README.md
├── dist
│ ├── assets
│ ├── favicon.ico
│ └── index.html
├── dist_electron
│ ├── MyApp-0.0.0-mac.zip
│ ├── MyApp-0.0.0-mac.zip.blockmap
│ ├── MyApp-0.0.0.dmg
│ ├── MyApp-0.0.0.dmg.blockmap
│ ├── builder-debug.yml
│ ├── builder-effective-config.yaml
│ └── mac
...

错误修改

在第一次运行 Electron 项目的时候, 有可能会遇到类似包含有 Electron failed to install correctly, please delete node_modules/electron and try installing again; 信息的错误提示。

这个问题是因为在install的时候node_modules/electron/ 中文件的丢失造成程序无法执行。缺少了path.txtdist文件夹。

可以通过 electron-fix 来修改解决

  1. 安装electron-fix, 执行命令: npm install electron-fix -g

  2. 进行修改, 执行命令: electron-fix start

  3. 执行 npm run fix

完成以上三个步骤之后, 再运行 Electron 项目就没有问题了。