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:
[email protected]
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 fastvo
npm install
npm 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为js
require('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个是IP
let 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为js
require('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'),//输出到release
app: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(),
// 注册到plugin
ElectronDevPlugin(),
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
需要使用 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 var
const theme = ref<GlobalTheme>(lightTheme)
// actions: update Theme
function setTheme(themes:boolean){
if(themes){
// true lightTheme
theme.value = lightTheme
}else{
// false darkTheme
theme.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请求封装
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:如何使用预载脚本
// 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 xxxxxxx
sendPOST('http://xxxxxxxx/user/login',{username:username,pwd:pwd});
})
})
// preload.ts
contextBridge.exposeInMainWorld('electronAPI',{
// 暴露一个单行的函数openFile ,该函数会执行 主进程中的函数
username: () => 'xxxxxxx',
pwd: () => 'xxxxxx',
ping: () => ipcRenderer.invoke('api:ping'),
// 对渲染页面暴露登录函数 api:login
login: (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:login
login: 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 edit
registry=https://registry.npmmirror.com
electron_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-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: '[email protected]'
});
// 获取一个对象
console.log(store.get('user')); //=> {id: 1, username: 'Allen', email: '[email protected]'}
// 使用点符号访问嵌套属性
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
}))