Electron 从零开始
01. 介绍
Electron 是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在 Windows 上运行的跨平台应用 macOS 和 Linux——不需要本地开发经验。
02. 技术栈
- electron
- vue3
- element-plus
- vue-router
- vuex
- vuex-persistedstate
03. 起步
3.1 初始化 Vue3 项目
sh
# 全局安装vue-cli
npm install -g @vue/cli
# 创建vue3项目
vue create electron-quick-start
# 启动项目
yarn serve
3.2 安装 Electron
sh
# 安装依赖
vue add electron-builder
# 启动项目
yarn electron:serve
# 解决启动报错,安装
yarn add ts-loader@8.2.0
修改 background.ts
ts
try {
await installExtension(VUEJS3_DEVTOOLS);
} catch (e: any) {
console.error("Vue Devtools failed to install:", e.toString());
}
3.3 配置 vue.config.js
- appId/guid 需保持一致;
- include 为热更新配置,不然你在卸载/更新后安装软件安装不上;主要原因注册机的问题。这个文件中有一句代码也是要和 appId 一致的;
js
const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: false, // 引入插件不使用时不报报错
configureWebpack: {
externals: {
electron: 'require("electron")',
},
},
// 第三方插件配置
pluginOptions: {
// vue-cli-plugin-electron-builder 配置
electronBuilder: {
nodeIntegration: true,
// 设置应用主进程的入口
mainProcessFile: "src/background.ts",
// 设置应用渲染进程的入口
rendererProcessFile: "src/main.ts",
customFileProtocol: "../",
// 打包选项
builderOptions: {
// 解决的问题:在安装到电脑后,系统通知无法工作
appId: "com.ecat.vue3", // 软件id
productName: "软件名称", // 打包后的名称
// windows系统相关配置
win: {
// 应用图标路径(Windows 系统中 icon 需要 256 * 256 的 ico 格式图片)
icon: "./src/assets/logo.png",
target: {
target: "nsis",
// 支持 64 位的 Windows 系统
arch: ["x64"],
},
},
nsis: {
// 如果为false,想要给电脑所有用户安装必须使用管理员权限
allowElevation: true,
// 是否一键安装
oneClick: false,
// 允许修改安装目录
allowToChangeInstallationDirectory: true,
guid: "com.ecat.vue3", // 软件id
include: "./installer.nsh",
},
},
},
},
});
根目录创建 installer.nsh
!macro customInit
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\com.ecat.vue3"
!macroend
3.4 安装 Element-Plus
sh
npm install element-plus --save
npm install @element-plus/icons-vue
修改 main.ts
ts
import { createApp } from "vue";
import ElementPlus from "element-plus";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
import "element-plus/dist/index.css";
import App from "./App.vue";
const app = createApp(App);
app.use(ElementPlus);
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
app.mount("#app");
3.5 开启热更新
sh
yarn add electron-updater
新建 JSPatch.ts
ts
import { ipcMain } from "electron";
const { autoUpdater } = require("electron-updater");
autoUpdater.autoDownload = true; // 是否自动更新
autoUpdater.autoInstallOnAppQuit = true; // APP退出的时候自动安装
/*
* 在开启更新监听事件之前设置
* 一定要保证该地址下面包含lasted.yml文件和需要更新的exe文件
*/
autoUpdater.setFeedURL({
provider: "generic",
url: "https://",
});
export default (win: any) => {
// autoUpdater.setFeedURL({
// provider: 'generic',
// url: process.env.VUE_APP_HOT_UPDATE,
// })
// 发送消息给渲染线程
function sendStatusToWindow(status?: any, params?: any) {
win.webContents.send(status, params);
}
autoUpdater.on("checking-for-update", () => {
sendStatusToWindow("Checking for update...");
});
autoUpdater.on("update-available", (info: any) => {
// 可以更新版本
sendStatusToWindow("autoUpdater-canUpdate", info);
});
autoUpdater.on("error", (err: any) => {
// 更新错误
sendStatusToWindow("autoUpdater-error", err);
});
// 发起更新程序
ipcMain.on("autoUpdater-toDownload", () => {
autoUpdater.downloadUpdate();
});
autoUpdater.on("download-progress", (progressObj: any) => {
// 正在下载的下载进度
sendStatusToWindow("autoUpdater-progress", progressObj);
});
autoUpdater.on("update-downloaded", () => {
// 下载完成
sendStatusToWindow("autoUpdater-downloaded");
});
// 退出程序
ipcMain.on("exit-app", () => {
autoUpdater.quitAndInstall();
});
// 重新检查是否有新版本更新
ipcMain.on("monitor-update-system", () => {
autoUpdater.checkForUpdates();
});
// 检测是否有更新
setTimeout(() => {
autoUpdater.checkForUpdates();
}, 5000);
};
检查更新组件 jspatch.vue
vue
<template>
<div class="jspatch">
<el-dialog
title="JSON工具更新中......"
v-model="showUpdater"
:close-on-click-modal="false"
:close-on-press-escape="true"
:show-close="false"
width="40%"
top="26vh"
center
>
<template v-if="downloadProcess">
<p>
{{
`当前:【${downloadProcess.transferred}】 / 共【${downloadProcess.total}】`
}}
</p>
<el-progress
:text-inside="true"
:stroke-width="18"
status="warning"
:percentage="downloadProcess.percent"
></el-progress>
<p>正在下载({{ downloadProcess.speed }})......</p>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from "vue";
import { ipcRenderer } from "electron";
import { ElMessage, ElMessageBox } from "element-plus";
export default defineComponent({
name: "Jspatch",
setup(props: any, { emit }: { emit: any }) {
const data = reactive({
showUpdater: false,
downloadProcess: {
percent: 10,
speed: 0,
transferred: "1kb",
total: "2M",
},
});
// 发现新版本 once
ipcRenderer.on("autoUpdater-canUpdate", (event, info) => {
/*
* 这儿会监听,如果info.version比现在版本小;就会触发;反之,不会触发
*/
ElMessageBox.confirm(
`发现有新版本【v${info.version}】,是否更新?`,
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
closeOnClickModal: false,
type: "warning",
}
).then(() => {
ipcRenderer.send("autoUpdater-toDownload");
});
});
// 下载进度
ipcRenderer.on("autoUpdater-progress", (event, process) => {
if (process.transferred >= 1024 * 1024) {
process.transferred =
(process.transferred / 1024 / 1024).toFixed(2) + "M";
} else {
process.transferred = (process.transferred / 1024).toFixed(2) + "K";
}
if (process.total >= 1024 * 1024) {
process.total = (process.total / 1024 / 1024).toFixed(2) + "M";
} else {
process.total = (process.total / 1024).toFixed(2) + "K";
}
if (process.bytesPerSecond >= 1024 * 1024) {
process.speed =
(process.bytesPerSecond / 1024 / 1024).toFixed(2) + "M/s";
} else if (process.bytesPerSecond >= 1024) {
process.speed = (process.bytesPerSecond / 1024).toFixed(2) + "K/s";
} else {
process.speed = process.bytesPerSecond + "B/s";
}
process.percent = process.percent.toFixed(2);
data.downloadProcess = process;
data.showUpdater = true;
});
// 下载更新失败
ipcRenderer.once("autoUpdater-error", () => {
ElMessage.error("更新失败!");
data.showUpdater = false;
});
// 下载完成
ipcRenderer.once("autoUpdater-downloaded", () => {
data.showUpdater = false;
ElMessageBox.confirm(`更新完成,是否关闭应用程序安装新版本?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
closeOnClickModal: false,
type: "warning",
}).then(() => {
ipcRenderer.send("exit-app");
});
});
return {
...toRefs(data),
};
},
});
</script>
<style scoped lang="less">
.jspatch {
/deep/ .el-dialog__header {
font-weight: 700;
.el-dialog__title {
color: #00adb5;
}
}
}
</style>
修改 vue.config.js
,builderOptions 对象中添加
js
publish: [
{
provider: "generic",
url: 'https:', // 打包文件地址,与以上链接需相同
},
],
04. 项目渐进式
4.1 Vuex 以及数据持久化
- Vuex 安装
sh
npm install vuex@next --save
- 创建
src\store\index.ts
ts
import { createStore } from "vuex";
const store = createStore({
state: {
count: 0,
},
getters: {
countGetter(state) {
return state.count + "【getters】";
},
},
mutations: {
increment(state, n = 1) {
state.count += n;
},
},
actions: {
increment({ commit }, n) {
commit("increment", n);
},
},
modules: {},
});
export default store;
- 在
main.ts
中引入
ts
import store from "./store";
app.use(store);
- 测试使用
vue
<template>
<div class="home">
{{ count }}/ {{ store.getters.countGetter }}<br />
<el-button type="primary" @click="handleAdd">添加</el-button>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from "vue";
import { useStore } from "vuex";
export default defineComponent({
name: "Vuex",
setup() {
const store = useStore();
const count = computed(() => {
return store.state.count;
});
const handleAdd = () => {
store.commit("increment", 1);
};
return {
count,
store,
handleAdd,
};
},
});
</script>
注意
这个时候刷新页面,数据就没了,需要引入数据持久化功能
- 数据持久化
sh
# 安装
npm i vuex-persistedstate --save
ts
import { createStore } from "vuex";
import createPersistedstate from "vuex-persistedstate";
const store = createStore({
state: {
count: 0,
},
getters: {
countGetter(state) {
return state.count + "【getters】";
},
},
mutations: {
increment(state, n = 1) {
state.count += n;
},
},
actions: {
increment({ commit }, n) {
commit("increment", n);
},
},
modules: {},
plugins: [
createPersistedstate({
key: "system",
}),
],
});
export default store;
4.2 主题切换、暗黑模式
- 书接上题,已经做好全局状态管理以及数据持久化,那就可以开始扩展其他功能
- 移除默认 frame,
background.ts
中,设定frame: false
,electron 的默认顶部菜单就消失了 - 添加 layout 样式,
src\layout\index.vue
src\layout\common\header.vue
src\layout\common\sidebar.vue
,并添加基础路由,这时候页面框架就出来了 - 添加全局设定
settings.vue
组件,表单修改时,将数据保存至 Vuex 中,具体代码参考:Settings.vue - 同时配置 styles 相关样式,整体系统就支持主题切换、暗黑模式了
4.3 ipcRenderer 事件总线
js
import { ipcRenderer, ipcMain } from "electron";
// 发送
ipcRenderer.send("Fn", value);
// 获取
ipcMain.on("Fn", (event, value) => {});
win.webContents.send("Fn", data);
ipcRenderer.on("Fn", (event, value) => {});
4.4 其它渐进式【待补充】
05. Electron 的 API
学习一个框架最主要的就是看官方文档,对应项目的feat-api 分支