Electron
参考引用
参考文档:
Electron+Vue3.2+TypeScript+Vite开发桌面端 - 掘金 (juejin.cn)
如何用Electron+vue+vite构建桌面端应用(一) - 掘金 (juejin.cn)
Electron教程(三)如何打包 electron 程序:electron-forge 的使用教程-CSDN博客主要参考:
小满Vue3(第三十九章 electron桌面程序)_哔哩哔哩_bilibili
Electron开发实践(3)——环境&工程搭建(Vite+Electron+React) - 掘金 (juejin.cn)
创建vue项目
PS S:\VS Code> npm create vite@latest
Need to install the following packages:create-vite@4.4.1
Ok to proceed? (y) y
√ Project name: ... fastvo
√ Package name: ... fastvo(需小写即package.json中的name)
√ Select a framework: » Vue
√ Select a variant: » TypeScript
Scaffolding project in S:\VS Code\wxMiniProject...
Done. Now run:cd fastvonpm installnpm run dev
PS S:\VS Code>npm install
PS S:\VS Code>code .
集成electron
# 安装electron依赖 到开发环境
npm install electron electron-builder -D
npm install electron electron-builder --save-dev
# 注意:有时候更换源也会导致下载失败多试几次
# 还是安装失败时可以用:淘宝镜像+cnpm来安装这两个依赖
安装后在vue 项目 src 中新增 background.ts 文件 作为electorn 主进程文件
- background.ts
在vue 项目中 新建 plugins 文件夹 新增配置文件
- vite.electron.dev.ts // 开发环境的配置文件
- vite.electron.build.ts // 生产环境的配置文件
npm 更新源
# 查询源
npm config get registry
# 更换国内源
npm config set registry https://registry.npmmirror.com
# 恢复官方源
npm config set registry https://registry.npmjs.org
# 删除注册表
npm config delete registry
# 淘宝最新源
npm config set registry https://registry.npmmirror.com
# npm 官方原始镜像网址是:https://registry.npmjs.org/
# 淘宝 NPM 镜像:https://registry.npm.taobao.org
# 阿里云 NPM 镜像:https://npm.aliyun.com
# 腾讯云 NPM 镜像:https://mirrors.cloud.tencent.com/npm/
# 华为云 NPM 镜像:https://mirrors.huaweicloud.com/repository/npm/
# 网易 NPM 镜像:https://mirrors.163.com/npm/
# 中科院大学开源镜像站:http://mirrors.ustc.edu.cn/
# 清华大学开源镜像站:https://mirrors.tuna.tsinghua.edu.cn/
background.ts
// 主进程启动文件
// electorn
import {app,BrowserWindow} from 'electron'
// const { app, BrowserWindow } = require('electron')
// 禁用沙盒
app.commandLine.appendSwitch('no-sandbox');// 等待Electron应用就绪后创建BrowserWindow窗口
app.whenReady().then(()=>{const win = new BrowserWindow({height:600,width:800,webPreferences:{nodeIntegration:true,// 启用Node.js集成contextIsolation:false,// 禁用上下文隔离webSecurity:false,//}})if(process.argv[2]){// 打开开发者工具win.webContents.openDevTools()win.loadURL(process.argv[2])}else{win.loadFile('index.html')}
})
vite.electron.dev.ts
// 开发环境配置
import type { Plugin } from 'vite'
import type {AddressInfo} from 'net'
import {spawn} from 'child_process'
import fs from 'node:fs'// 转编函数
const buildBackground = ()=>{// 使用 esbuild 编译ts为jsrequire('esbuild').buildSync({entryPoints: ['src/background.ts'],// 入口文件bundle:true,// 打包所以依赖outfile:'dist/background.js', //输出文件platform:'node',target:'node20',external:['electron'] // 排除依赖})
}// 创建一个配置插件
export const ElectronDevPlugin = ():Plugin => {return {name:'electron-dev',configureServer(server){buildBackground()server?.httpServer?.once('listening',()=>{// 这个地方原本的address是string, 而 address() 函数会返回 AddressInfo,所以可以 as 断言成 AddressInfo类型const addressInfo = server?.httpServer?.address() as AddressInfo// console.log(address) // { address: '::1', family: 'IPv6', port: 5173 }// 1.获取到完整的访问路径用来给 eletron 使用 | 使用 ``来实现拼接const IP = `http://localhost:${addressInfo.port}` // console.log(IP) // http://localhost:5173 // 2. 使用进程传参把 IP地址传入到主进程中// require('electron') 函数的返回是一个路径// electron 无法识别ts文件,所以需要转编成js文件 然后发送到主进程// 进程传参发 把IP发送给 electron // 第0个参数是 require('electron') 第1个参数是'dist/background.js',第2个是IPlet ElectronProcess = spawn(require('electron'),['dist/background.js',IP])fs.watchFile('src/background.ts',()=>{ElectronProcess.kill()buildBackground()ElectronProcess = spawn(require('electron'),['dist/background.js',IP])})ElectronProcess.stdout.on('data',(data)=>{console.log(data.toString())})})}}
}
vite.electron.build.ts
// 生产环境配置
import type { Plugin } from 'vite'
import fs from 'node:fs'
import * as electronBuild from 'electron-builder'
import path from 'path'
// 转编函数
const buildBackground = ()=>{// 使用 esbuild 编译ts为jsrequire('esbuild').buildSync({entryPoints: ['src/background.ts'],// 入口文件bundle:true,// 打包所以依赖outfile:'dist/background.js', //输出文件platform:'node',target:'node20',external:['electron'] // 排除依赖})
}// 打包需要先等vite 打包完后再直接electron builder 打包
export const ElectronBuildPlugin = ():Plugin => {return {name:'electron-build',closeBundle() {buildBackground()// electron-builder 需要指定入口 const json = JSON.parse(fs.readFileSync('package.json','utf-8'))json.main = 'background.js'fs.writeFileSync('dist/package.json',JSON.stringify(json,null,4))fs.mkdirSync('dist/node_modules') // 为了预防electron下载垃圾文件 - ,{recursive:true}electronBuild.build({config:{directories:{output:path.resolve(process.cwd(),'release'),//输出到releaseapp:path.resolve(process.cwd(),'dist'),// 基于dist目录打包},asar:true,// 打包成压缩包appId:'com.suredata.app',productName:'fastvo',nsis:{oneClick:false,//取消一键安装allowToChangeInstallationDirectory:true,// 允许用户自定义安装},}})}}
}
tsconfig.node.json
// tsconfig.node.json
//添加到 tsconfig 中使项目可以检测到该配置文件
{"compilerOptions": {"composite": true,"skipLibCheck": true,"module": "ESNext","moduleResolution": "bundler","allowSyntheticDefaultImports": true},"include": ["vite.config.ts","plugins/**/*.ts"]
}
vite.config.ts
// vite.config.ts
// 注册到项目Plugin中
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// 引入,此时就项目就可以检测到 ElectronDevPlugin
import { ElectronDevPlugin } from './plugins/vite.electron.dev'
import { ElectronBuildPlugin } from './plugins/vite.electron.build'
// https://vitejs.dev/config/
export default defineConfig({plugins: [vue(),// 注册到pluginElectronDevPlugin(),ElectronBuildPlugin()],base:'./',//默认绝对路径,需要修改为相对路径,否则会白屏resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))}}
})
报错处理
1.在ts转编js时 报错:error when starting dev server:
Error: Dynamic require of “file:///S:/electron/fastvo/node_modules/esbuild/lib/main.js” is not supported
// package.json
{"name": "fastvo","private": true,"version": "0.0.0","type": "module", //删除该属性即可恢复"scripts": {"dev": "vite","build": "vue-tsc && vite build","preview": "vite preview"},"dependencies": {"vue": "^3.3.4"},"devDependencies": {"@vitejs/plugin-vue": "^4.2.3","electron": "^28.0.0","electron-builder": "^24.9.1","typescript": "^5.0.2","vite": "^4.4.5","vue-tsc": "^1.8.5"}
}
// 原因:
// 在 node 支持 ES 模块后,要求 ES 模块采用 .mjs 后缀文件名。只要遇到 .mjs 文件,就认为它是 ES 模块。如果不想修改文件后缀,就可以在 package.json文件中,指定 type 字段为 module。
// 这样所有 .js 后缀的文件,node 都会用 ES 模块解释。
//不论package.json中的type字段为何值,.mjs的文件都按照es模块来处理,.cjs的文件都按照commonjs模块来处理
// type字段省略则默认采用commonjs规范
// 不太懂,不过我们只需要把 ts转成js即可
2.启动白屏或无法加载,GPU进程无法渲染;
// background.ts
import {app,BrowserWindow} from 'electron'
// const { app, BrowserWindow } = require('electron')
// 禁用沙盒 (新增解决)
app.commandLine.appendSwitch('no-sandbox');
// 创建一个渲染进程(子进程)
const createWindow =()=>{const win = new BrowserWindow({height:600,width:800,webPreferences:{nodeIntegration:true,// 启用Node.js集成contextIsolation:false,// 禁用上下文隔离webSecurity:false,//}})if(process.argv[2]){// 打开开发者工具win.webContents.openDevTools()win.loadURL(process.argv[2])}else{win.loadFile('index.html')}
}// 等待Electron应用就绪后创建BrowserWindow窗口
app.whenReady().then(createWindow)// 程序激活时,触发流程
app.on('activate', () => {// On OS X it's common to re-create a window in the app when the - 在OS X上,通常会在应用程序中重新创建一个窗口// dock icon is clicked and there are no other windows open. - 单击dock图标,没有其他窗口打开。// 当检测不到窗口时会重新创建if (BrowserWindow.getAllWindows().length === 0) {createWindow();}});// 窗口关闭时
// Quit when all windows are closed, except on macOS. There, it's common -当所有窗口都关闭时退出,除了macOS。在那里,这很常见
// for applications and their menu bar to stay active until the user quits -让应用程序及其菜单栏保持活动状态,直到用户退出
// explicitly with Cmd + Q.- 显式地使用Cmd + Q。
app.on('window-all-closed', () => {if (process.platform !== 'darwin') {app.quit();}});
打包
npm run dev //测试
npm run build // 打包
- 如果出现“Cannot create symbolic link”的错误,可以以管理员身份运行power shell或vscode重新进行打包
- 如此出现下载失败就多试几次;看下git 是否可以进入;
1.打包后无法显示页面
npm run make 后,可以看到index.html的存在,但无法显示vue路由出口文件。
解决:
// 路由器实例 由 createWebHistory -修改为-> createWebHashHistory
const router = createRouter({history: createWebHashHistory(),routes
})
// 原因: 推测为 createWebHistory 不支持 HTML5 History API导致的。
问题太多,使用下面官方推荐的方式进行测试。
Electron Forge
官方脚手架
打包时没法选择 高级选项(electron-builder)
# 初始化一个新的electron 项目 my-app
npm init electron-app@latest fastvo
# 添加模板
npm init electron-app@latest quickTrim -- --template=vite-typescript
# 官方模板
# webpack、webpack-typescript、vite、vite-typescript
# 启动
cd fastvo
npm start
# 编译 成exe按照文件
npm run make
# 发布 app 把项目发布到指定仓库
npm run publish
# 安装 electron 官方的构建工具居然不会导入electron(会导入,但是导入失败并不提示,所以需要再次手动导入)
npm install --save-dev electron
forge.config.js
配置文件可以自定义配置,参考 配置文档,可选项:Options | @electron/packager
问题,白屏报错
app.commandLine.appendSwitch(‘no-sandbox’);
禁用 Chromium 沙箱。 强制渲染器进程和Chromium助手进程以非沙盒化运行。 应该只在测试时使用。
项目目录结构
Mode LastWriteTime Length Name
d----- 2024/1/17 14:23 .vite
d----- 2024/1/17 14:21 node_modules
d----- 2024/1/17 14:04 src
-a---- 2024/1/17 14:04 227 App.vue # 新增 vue页面展示
-a---- 2023/12/22 16:36 166 index.css # index.html样式
-a---- 2024/1/17 14:08 3059 main.ts # 主进程文件
-a---- 2023/12/22 16:36 158 preload.ts # 预载文件
-a---- 2024/1/17 14:09 1125 renderer.ts # 渲染进程文件(即页面渲染)
-a---- 2023/12/22 16:36 348 types.d.ts # ts文件
-a---- 2023/12/22 16:36 352 .eslintrc.json
-a---- 2023/12/22 16:36 1215 .gitignore
-a---- 2023/12/22 16:36 1240 forge.config.ts # forge配置文件
-a---- 2024/1/17 14:04 215 index.html # index.html文件,唯一
-a---- 2024/1/17 14:02 319676 package-lock.json # 版本锁定文件
-a---- 2024/1/17 13:59 1197 package.json # 依赖管理文件
-a---- 2023/12/22 16:36 333 tsconfig.json # ts配置文件
-a---- 2024/1/17 14:25 357 vite.main.config.ts # vite配置文件用于主进程
-a---- 2024/1/17 14:12 119 vite.preload.config.ts # vite配置文件用于预加载
-a---- 2024/1/17 14:26 192 vite.renderer.config.ts # vite配置文件用于渲染进程
main.ts:主进程文件,eletron 程序的入口,运行再一个Node.js环境中,负责控制您应用的生命周期,显示原生界面,执行特殊操作并管理渲染器进程(稍后详细介绍)。主进程的主要目的是使用
BrowserWindow
模块创建和管理应用程序窗口。当一个BrowserWindow
实例被销毁时,与其相应的渲染器进程也会被终止。preload.ts:预加载脚本
包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 。 这些脚本虽运行于渲染器的环境中,却因能访问 Node.js API 而拥有了更多的权限。因为预加载脚本与浏览器共享同一个全局Window
接口,并且可以访问 Node.js API 来增强渲染器,以便你的网页内容使用。
语境隔离(Context Isolation)意味着预加载脚本与渲染器的主要运行环境是隔离开来的,以避免泄漏任何具特权的 API 到您的网页内容代码中。renderer.ts 渲染进程文件(即页面渲染),对应着一个管理应用程序窗口进行
每个 Electron 应用都会为每个打开的BrowserWindow
( 与每个网页嵌入 ) 生成一个单独的渲染器进程。 洽如其名,渲染器负责 渲染 网页内容。 所以实际上,运行于渲染器进程中的代码是须遵照网页标准的 (至少就目前使用的 Chromium 而言是如此) 。
- 以一个 HTML 文件作为渲染器进程的入口点。
- 使用层叠样式表 (Cascading Style Sheets, CSS) 对 UI 添加样式。
- 通过
<script>
元素可添加可执行的 JavaScript 代码。
main.ts 和 renderer.ts 是独立的两个程序,main.ts 控制着 renderer.ts
preload.ts作为两者直接中间层可以对双方交互进行一个增强;
集成VUE3
Vue 3 - 电子锻造 (electronforge.io)
需要使用 electron 模板创建程序
npm init electron-app@latest my-vue-app – --template=vite
npm init electron-app@latest my-vue-app – --template=vite-typescript
# 添加依赖到运行环境
npm install vue
# 添加依赖到开发环境
npm install --save-dev @vitejs/plugin-vue
1.index.html 修改html页面,增加挂载点app
<!DOCTYPE html>
<html><head><meta charset="UTF-8" /><title>Hello World!</title></head><body><div id="app"></div><script type="module" src="/src/renderer.ts"></script></body>
</html>
2. src/App.vue 新增vue模板页面,即单文件入口。
<template><h1>💖 Hello World!</h1><p>Welcome to your Electron application.</p>
</template><script setup>
console.log('👋 This message is being logged by "App.vue", included via Vite');
</script>
3.src/renderer.ts 挂载APP.vue到index.html中
import './index.css';console.log('👋 This message is being logged by "renderer.ts", included via Vite');import { createApp } from 'vue';
import App from './App.vue';// 启用 vue,并挂载,到 index.html中
createApp(App).mount('#app');
4.vite.renderer.config.ts 修改配置文件导入vue插件到环境中
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue'// https://vitejs.dev/config
// 渲染配置
export default defineConfig({plugins:[vue()]
});
添加其他组件
vue-router
pinia
naive-ui
axios
// 安装上面依赖后新增配置
// renderer.ts
import './index.css';
console.log('👋 This message is being logged by "renderer.ts", included via Vite');
import { createApp } from 'vue';
import App from './App.vue';
import {createPinia} from 'pinia'
import router from './router/index';// 启用 vue,并挂载,到 index.html中
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app');
// router/index.ts
// 路由组件配置
import {createRouter,createWebHistory,RouteRecordRaw} from 'vue-router'
// 1.单独使用 loadingBar 进度条
import { createDiscreteApi} from 'naive-ui'
const {loadingBar} = createDiscreteApi(['loadingBar'])// 动态路由 引入文件
// 路由信息
const routes:Array<RouteRecordRaw> = [{path: '/',name: 'Sign',component: () => import('../view/sign.vue'),meta: {namespace: 'sign'}},// {// // 初始化加载index首页组件// path:'/',// component: signVue,// redirect:'/',// meta:{// namespace:'sign',// }// }// {// 测试组件// path:'/home',// namespace:'Home',// component: () => import('@/components/HelloWorld.vue'),// children:[],子组件;// meta:{requiresAuth: false},路由元信息,可以控制组件跳转权限;// },
]
// 路由器实例
const router = createRouter({history: createWebHistory(),routes
})
// export const Sleep = (ms:number)=> {
// return new Promise(resolve=>setTimeout(resolve, ms))
// }
// 设置前置路由守卫
router.beforeEach((to,from,next)=>{// 路由中导入-开始loadingBar.start()next()
})
// 设置后置路由守卫
router.afterEach((to,from,next)=>{// 路由中导入-结束loadingBar.finish()
})
// 对外暴露
export default router
// pinia使用示例
// store/theme.ts
import {darkTheme,lightTheme} from 'naive-ui'
import { defineStore } from 'pinia'
import { ref} from 'vue'
import type {GlobalTheme} from 'naive-ui'// themeStore of pinia
export const useThemeStore = defineStore('themeStore',()=>{// theme ref varconst theme = ref<GlobalTheme>(lightTheme)// actions: update Theme function setTheme(themes:boolean){if(themes){// true lightThemetheme.value = lightTheme}else{// false darkThemetheme.value = darkTheme}}return {theme,setTheme}
})
// vue使用
// App.vue
<template><n-config-provider :theme="useTheme.theme" :locale="zhCN" :date-locale="dateZhCN"><!-- 组件渲染出口 --><router-view></router-view><!-- <h1>💖 Hello World!</h1><p>Welcome to your Electron application.</p> --><!-- <n-button @click="emit('updateTheme')" strong secondary type="success">{{themeFlag?"光明":"黑暗"}}</n-button> -->
</n-config-provider>
</template><script setup lang="ts">
import {zhCN,dateZhCN,NConfigProvider,NButton} from 'naive-ui'
// theme
import {useThemeStore} from './store/theme'
import {ref} from 'vue'console.log('👋 This message is being logged by "App.vue", included via Vite');
const useTheme = useThemeStore()
//与父组件通信修改主题
const emit = defineEmits(["updateTheme"])
// 接受父组件数据信息
defineProps({// 接受父组件传来的参数themeFlag: Boolean,// 写法二,可以设置默认值themeFlags:{type:Boolean,default:''}
})
</script>
index.html
<!-- 清理样式 清理默认样式-->
/* :root 表示文档根元素,优先级比较高,而且再这里边定义的变量也可以作为全局变量 */
:root {font-family: Inter, Avenir, Helvetica, Arial, sans-serif;font-size: 16px;line-height: 24px;font-weight: 400;/* 颜色主题: 阳光 黑暗*/color-scheme: light dark; /* 默认黑色背景色和白色文字 *//* color: rgba(255, 255, 255, 0.87);background-color: #242424; */color: #213547;background-color: #ffffff;font-synthesis: none;text-rendering: optimizeLegibility;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;-webkit-text-size-adjust: 100%;
}
body {margin: 0;padding: 0;display: flex;width: 100%;height: 100%;/* min-width: 320px; *//* min-height: 100vh; */
}
/* body内css样式,整个页面的样式 */
#app {width: 100%;height: 100%;margin: 0;padding: 0;min-height: 100vh;text-align: center;
}
http请求封装
参考:net | Electron (electronjs.org)
import { net } from 'electron';
// --------------------网络请求封装
/*** POST请求数据接口* @param api 接口地址,如:'http://xxxxxxx:7000/user/login'* @param data 请求数据,JOSN 格式,或 object 或 string多需要异步,同步需要需要测试*/
export function sendPOST(api:string,data:JSON|object|string){// const request:RequestInit = {// method:'POST',// body:JSON.stringify(data),// headers:{'Content-Type':'application/json'}// }// net.fetch(api,request)// .then(response => {// console.log('POST 请求成功: ',response);// return response.json();// }).catch(err => {// console.log('POST 请求异常: ',err);// return null;// })sendPOST_ASYNC(api,data).then(response => {console.log('POST 请求成功: ',response);return response;}).catch(err => {console.log('POST 请求异常: ',err);return null;})
}
/*** GET请求数据接口* @param api 接口地址,如:'http://xxxxx:7000/ping'*/
export function sendGET(api:string){// net.fetch(api)sendGET_ASYNC(api).then(response => {console.log('GET 请求成功: ',response);return response;}).catch(err => {console.log('GET 请求异常: ',err);return null;})
}
/*** POST请求数据接口 - 异步接口* @param api 接口地址,如:'http://xxxxxxxx:7000/user/login'* @param data 请求数据,JOSN 格式,或 object 或 string*/
async function sendPOST_ASYNC(api:string,data:JSON|object|string){const request:RequestInit = {method:'POST',body:JSON.stringify(data),headers:{'Content-Type':'application/json'}}const response = await net.fetch(api,request)if (response.ok) {const body = await response.json()return body}
}
/*** GET请求数据接口 - 异步接口* @param api 接口地址,如:'http://xxxxxxxx:7000/ping'*/
async function sendGET_ASYNC(api:string){const response = await net.fetch(api)if (response.ok) {const body = await response.json()return body}
}
IPC通信
参考:
Electron入门实践(3):进程间通讯 - 掘金 (juejin.cn)electron+vue3全家桶+vite项目搭建【13.1】ipc通信的使用,主进程与渲染进程之间的交互_electron vite ipc-CSDN博客
IPC通信主要就是依赖preload预载脚本来实现的,一切的操作均和该脚本相关。
IPC通信[主/渲染]进程对应
方向 | 主进程【ipcMain】 | 渲染进程【ipcRenderer】 |
---|---|---|
渲染=>主 【同步/异步】 | ipcMain.on() | ipcRender.send() / ipcRender.sendSync() 【同步取值】 |
渲染=>主 【异步】 | ipcMain.handle() | ipcRender.invoke() |
主=>渲染 【异步】 | BrowserWindow【实例】.webContents.send() | ipcRender.on() |
涉及到的请求通路都要进行异常处理,否则页面无法识别到返回数据
preload:如何使用预载脚本
参考:contextBridge | Electron (electronjs.org)
// main.ts
import {ipcMain} from 'electron';
// 在初始化Electron时完成。
// 可以作为一个方便的替代检查app. isready()和订阅ready事件,
// 如果应用程序还没有准备好。
app.whenReady().then(()=>{// ipcMain 注册 openFile 通道 触发 回调handleFileOpen函数ipcMain.handle('api:ping', search_server())
})
//这个方法将在Electron完成时被调用
//初始化,并准备创建浏览器窗口。
//某些api只能在此事件发生后使用。
app.on('ready', createWindow);
// preload.ts
// See the Electron documentation for details on how to use preload scripts:
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
// 预加载脚本
import { contextBridge,ipcRenderer } from "electron";// 将函数暴露给 渲染页面使用 通道 electronAPI
contextBridge.exposeInMainWorld('electronAPI',{// 暴露一个单行的函数ping ,该函数会执行 主进程中的函数ping: () => ipcRenderer.invoke('api:ping')
})
// 通道 info
contextBridge.exposeInMainWorld('info',{// 暴露一个单行的函数openFile ,该函数会执行 主进程中的函数username: () => 'xxxxxxxx',pwd: () => 'xxxxxxxx'
})
<-- sign.vue -->
<script setup lang="ts">
// 直接使用
userRef.value.username = window.info.pwd()
userRef.value.pwd = window.info.username()
window.electronAPI.ping()
</script>
渲染进程->主进程(单向通信)
// main.ts
app.whenReady().then(()=>{// ipcMain 注册 openFile 通道 触发 回调handleFileOpen函数ipcMain.handle('api:ping', search_server)// 监听 api:login 通道,触发登录函数ipcMain.on('api:login',(event,username,pwd)=>{console.log('收到消息:',username,pwd) // 收到消息: xxxxxx xxxxxxxsendPOST('http://xxxxxxxx/user/login',{username:username,pwd:pwd});})
})
// preload.ts
contextBridge.exposeInMainWorld('electronAPI',{// 暴露一个单行的函数openFile ,该函数会执行 主进程中的函数username: () => 'xxxxxxx',pwd: () => 'xxxxxx',ping: () => ipcRenderer.invoke('api:ping'),// 对渲染页面暴露登录函数 api:loginlogin: (username:string,pwd:string) => ipcRenderer.send('api:login',username,pwd)
})
// interface.d.ts
export interface IElectronAPI {ping: () => Promise<void>,// 新增声明login: (username:string,pwd:string) => Promise<void>,
}declare global {
interface Window {electronAPI: IElectronAPI
}
}
// sing.vue
<-- sign.vue -->
<script setup lang="ts">
<-- 直接使用 -->
userRef.value.username = window.info.pwd()
userRef.value.pwd = window.info.username()
window.electronAPI.ping()
<-- vue 页面调用 -->
window.electronAPI.login(userRef.value.account,userRef.value.password)
</script>
渲染进程<=>主进程(双向通信)
1.与 单向通信不同,具有返回值,单向与双向的差别主要是 ipcMain.on() & ipcMain.handle() 和 预加载脚本中调用的 ipcRenderer.send() & ipcRenderer.invoke()的差别;
2.该返回值需要执行
异步
函数,否则返回值无法回到渲染页面;
// --------------- main.ts
app.whenReady().then(()=>{// ipcMain 注册 openFile 通道 触发 回调handleFileOpen函数ipcMain.handle('api:ping', search_server)// 监听 api:login 通道,触发登录函数 ipcMain.handle('api:login',async (event,username,pwd)=> {return await sendPOST_ASYNC('http://xxxxxxxxxxxxx/user/login',{username:username,pwd:pwd});})
})
/*** 查找服务器函数-异步* @returns */
const search_server = async ()=> {return await sendGET_ASYNC('http://xxxxxxxxx/ping');
}
// --------------- preload.ts 同单向不同需要异步声明
contextBridge.exposeInMainWorld('electronAPI',{// 暴露一个单行的函数openFile ,该函数会执行 主进程中的函数username: () => 'xxxxxxx',pwd: () => 'xxxxxxx',ping: async () => await ipcRenderer.invoke('api:ping'),// 对渲染页面暴露登录函数 api:loginlogin: async (username:string,pwd:string) => await ipcRenderer.invoke('api:login',username,pwd),
})
// --------------- interface.d.ts 同单向
export interface IElectronAPI {ping: () => Promise<void>,// 新增声明login: (username:string,pwd:string) => Promise<void>,
}declare global {
interface Window {electronAPI: IElectronAPI
}
}
// --------------- sing.vue
<-- sign.vue -->
<script setup lang="ts">
<-- 直接使用 -->
userRef.value.username = window.info.pwd()
userRef.value.pwd = window.info.username()
window.electronAPI.ping()
<-- vue 页面调用 通过通道后需要再次解一次 -->
window.electronAPI.login(userRef.value.account,userRef.value.password)
.then(response => {console.log('请求登录 成功: ',response);return response;
}).catch(err => {console.log('请求登录 异常: ',err);return null;
})
</script>
主进程=>渲染进程(单向)
主线程创建后直接发送,页面会接收不到,应该是监听还没有开启就已经发送过去了。
直接发送object会报异常: Error: Failed to serialize arguments ,发送的数据未能序列化,发送基础数据可以,需要注意;
自定义的object也是可以的,如下面的修改:// 1.创建一个基本object
const server:serverInfo = {
ip: ‘’,
ivm: ‘’,
sn: ‘’,
timestamp:0
}// 2.初始化时给object赋值
function search_info(){
// 1.获取服务器列表
search_server().then(res=>{
console.log(‘search_info’,res)
server.ip=res.ip
server.timestamp = res.timestamp
server.ivm = res.ivm
server.sn = res.sn
}).catch(err=>{
console.log(‘search_info’,err)
})}
// 3.初始化后在启动后发送给渲染页面( 发送失败,应该是时机不对)
不过可以使用按钮发送初始化后取到值后的 server:
click: () => mainWindow.webContents.send(‘api:syncserver’,server),
// --------------- main.ts
// 新增菜单按钮,点击后会触发事件让其发送到渲染页面
const createWindow = () => {// Create the browser window.const mainWindow = new BrowserWindow({width: 800,height: 600,webPreferences: {preload: path.join(__dirname, 'preload.js'),nodeIntegration:true,},});const menu = Menu.buildFromTemplate([{label: '查看',submenu: [{click: () => mainWindow.webContents.send('api:syncserver', 1),label: 'getServer'},]}])Menu.setApplicationMenu(menu)// and load the index.html of the app.if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);// Open the DevTools.mainWindow.webContents.openDevTools();} else {mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));}return mainWindow
};
// 在初始化Electron时完成。
// 可以作为一个方便的替代检查app. isready()和订阅ready事件,
// 如果应用程序还没有准备好。
app.whenReady().then(()=>{// ipcMain 注册 openFile 通道 触发 回调handleFileOpen函数ipcMain.handle('api:ping', search_server) // 等待渲染页面调用ipcMain.handle('api:login',async (event,username,pwd)=> {return await sendPOST_ASYNC('http://xxxxxxx/user/login',{username:username,pwd:pwd});})// 主线程 => 渲染线程 : 取到的服务信息要发送到渲染进程中一份,方便用户查看const mainWindow = createWindow() // 创建窗口// mainWindow.webContents.send('api:syncserver',search_server) // 发送获取到的server信息 ,直接发送,页面会接收不到,应该是监听还没有开启就已经发送过去了。
})
// --------------- preload.ts 新增ipcRenderer.on监听器
contextBridge.exposeInMainWorld('electronAPI',{// 暴露一个单行的函数openFile ,该函数会执行 主进程中的函数username: () => 'xxxxx',pwd: () => 'xxxxxxxxxx',ping: async () => await ipcRenderer.invoke('api:ping'),login: async (username:string,pwd:string) => await ipcRenderer.invoke('api:login',username,pwd),syncserver: async (callback:any) => ipcRenderer.on('api:syncserver',(_event, value) => callback(value))
})
// --------------- interface.d.ts 同单向
export interface IElectronAPI {ping:() => Promise<void>,login: (username:string,pwd:string) => Promise<T>,syncserver: (callback) => Promise<T>,
}
declare global {
interface Window {electronAPI: IElectronAPI
}
}
// --------------- sing.vue
// 初始化加载
// 调用 预载脚本中的监听函数,监听api:syncserver通道,等待主线程发送消息;
window.electronAPI.syncserver((value:any)=>{console.log('触发syncserver:',value)
})
IPC通信与Typescript一起使用时
需要新建配置文件来全局增强接口,否则无法使用接口
// interface.d.ts 需要放到src下才会编译进去
export interface IElectronAPI {ping: () => Promise<void>,
}declare global {
interface Window {electronAPI: IElectronAPI
}
}
Electron Aunet | Electron (electronjs.org)toUpdate
electron-release-server 自动更新功能
electron-forge + 静态资源更新;
# electron-forge 创建的项目,添加下面代码
# main.ts
# 设置服务器地址
autoUpdater.setFeedURL({url:'http://xxxxxxxxxx/version/'})
# 60s检测一次
setInterval(() => {autoUpdater.checkForUpdates()
}, 10000)
# 检测到更新事件,触发弹窗
autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName) => {
# 设置弹窗内容dialog.showMessageBox({type: 'info',buttons: ['Restart', 'Later'],title: 'Application Update',message: process.platform === 'win32' ? releaseNotes : releaseName,detail:'A new version has been downloaded. Starta om applikationen för att verkställa uppdateringarna.'}).then((returnValue) => {if (returnValue.response === 0) autoUpdater.quitAndInstall()})
})
# 异常告警 (否则会弹窗报错)
autoUpdater.on('error', (message) => {console.error('error try catch is :',message.message)
})
# ------------优化---------------
# 检测5次后不在使用
let updateNumber = 5
setInterval(() => {try {if(updateNumber>0){autoUpdater.checkForUpdates()updateNumber--}}catch (error) {console.log(error)}
}, 10000)
静态资源目录:
- nginx映射静态目录
- electron-forge 新版本打包后的3个文件(三个文件必须)
问题:正式发布后可以检测到更新
npm run start 测试环境无法检测到更新,应该是该 electron-squirrel-startup 插件的问题,但无法同时在开发环境和正式环境同时安装;
问题:弹出更新提示后无论点击什么都会自动安装新版本;
Electron npm install
# *** 打开npm配置文件 修改electron_mirror指定镜像
npm config editregistry=https://registry.npmmirror.comelectron_mirror=https://cdn.npmmirror.com/binaries/electron/electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
# 下载
npm install --save-dev electron
npm 执行指令异常
Failed to remove some directories [
npm WARN cleanup [
npm WARN cleanup ‘D:\V3Work\v3project\node_modules\@vue’,
npm WARN cleanup [Error: EPERM: operation not permitted, rmdir ‘D:\V3Work\v3project\node_modules@vue\reactivity\dist’] {
npm WARN cleanup errno: -4048,
npm WARN cleanup code: ‘EPERM’,
npm WARN cleanup syscall: ‘rmdir’,
npm WARN cleanup path: ‘D:\V3Work\v3project\node_modules\@vue\reactivity\dist’
npm WARN cleanup }
npm WARN cleanup ],
operation not permitted 无法执行删除操作,没有权限,可以使用管理员运行dos后再执行命令
Electron-Store
参考:
Electron入门实践(4):数据缓存 - 掘金 (juejin.cn)电子存储 - npm (npmjs.com)
Electron食用指南: 数据持久化组件Electron-Store - 掘金 (juejin.cn)
electron-store
是一个基于Node.js文件系统的数据存储库,它可以将数据以JSON文件的形式保存在本地。
优点:
- 简单易用,无需安装数据库或其他依赖;
- 支持多进程访问,可以在主进程和渲染进程中使用;
- 支持点符号访问嵌套属性,例如store.get(‘foo.bar’);
- 支持默认值,自动合并用户设置和默认设置;
- 支持加密,可以使用密码对数据进行加密和解密;
- 支持类型检查,可以使用TypeScript或JSDoc来定义数据类型;
- 支持观察者模式,可以监听数据变化并执行回调函数;
安装:npm install electron-store
主线程导入:
import Store = require(‘electron-store’);
// ------ 初始化
const store = new Store(); // 初始化存储器
使用:
// 存储一个字符串
store.set('name', 'Allen'); // 获取一个字符串
console.log(store.get('name')); //=> 'Allen' // 存储一个对象
store.set('user', {
id: 1,
username: 'Allen',
email: 'allen@example.com'
}); // 获取一个对象
console.log(store.get('user')); //=> {id: 1, username: 'Allen', email: 'allen@example.com'} // 使用点符号访问嵌套属性
store.set('user.profile.avatar', 'https://example.com/avatar.png');
console.log(store.get('user.profile.avatar')); //=> 'https://example.com/avatar.png' // 删除一个属性
store.delete('name');
console.log(store.get('name')); //=> undefined // 判断一个属性是否存在
console.log(store.has('name')); //=> false // 获取所有的数据
console.log(store.store); //=> {user: {...}} // 清空所有的数据
store.clear();
console.log(store.store); //=> {}
使用方法:通过IPC通信暴露给页面调用,存储或查询;
使用问题
electron 监听软件头部事件无法触发页面事件(必须来自手势);
触发页面事件报错:必须来自手势。规避主进程无法触发页面特殊事件(如: 打开文件事件)。
<n-button id="realClickButton" @click="selectFile()" type="info">打开文件
</n-button>
// 主进程触发->打开文件
const realClickButton = document.getElementById('realClickButton');
realClickButton.dispatchEvent(new MouseEvent('click', {view: window,bubbles: true,cancelable: true
}))