Electron使用记录

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项目

shell 复制代码
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 fastvo
  npm install
  npm run dev
PS S:\VS Code>npm install
PS S:\VS Code>code .

集成electron

shell 复制代码
# 安装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 更新源
shell 复制代码
# 查询源
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
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
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
tsx 复制代码
// 生产环境配置
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
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
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

ts 复制代码
// 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进程无法渲染;
ts 复制代码
// 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 // 打包

  1. 如果出现"Cannot create symbolic link"的错误,可以以管理员身份运行power shell或vscode重新进行打包
  2. 如此出现下载失败就多试几次;看下git 是否可以进入;
1.打包后无法显示页面

npm run make 后,可以看到index.html的存在,但无法显示vue路由出口文件。

解决:

vue 复制代码
// 路由器实例 由 createWebHistory -修改为-> createWebHashHistory
const router = createRouter({
    history: createWebHashHistory(),
    routes
})
// 原因: 推测为 createWebHistory 不支持 HTML5 History API导致的。

问题太多,使用下面官方推荐的方式进行测试。

Electron Forge

官方脚手架

打包时没法选择 高级选项(electron-builder)

shell 复制代码
# 初始化一个新的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

shell 复制代码
# 添加依赖到运行环境
npm install vue
# 添加依赖到开发环境
npm install --save-dev @vitejs/plugin-vue
html 复制代码
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>
vue 复制代码
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>
ts 复制代码
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');
ts 复制代码
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

ts 复制代码
// 安装上面依赖后新增配置
// 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');
ts 复制代码
// 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 
ts 复制代码
// 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
    }
})
ts 复制代码
// 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

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)

ts 复制代码
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)

ts 复制代码
// 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);
tsx 复制代码
// 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'
})
vue 复制代码
<-- sign.vue -->
<script setup lang="ts">
// 直接使用
userRef.value.username = window.info.pwd()
userRef.value.pwd = window.info.username()
window.electronAPI.ping()
</script>
渲染进程->主进程(单向通信)
tsx 复制代码
// 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.该返回值需要执行异步函数,否则返回值无法回到渲染页面;

tsx 复制代码
// --------------- 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),

ts 复制代码
// --------------- 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一起使用时

需要新建配置文件来全局增强接口,否则无法使用接口

tsx 复制代码
// 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 + 静态资源更新;

shell 复制代码
# 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)

静态资源目录:

  1. nginx映射静态目录
  2. electron-forge 新版本打包后的3个文件(三个文件必须)

问题:正式发布后可以检测到更新

npm run start 测试环境无法检测到更新,应该是该 electron-squirrel-startup 插件的问题,但无法同时在开发环境和正式环境同时安装;

问题:弹出更新提示后无论点击什么都会自动安装新版本;

Electron npm install

shell 复制代码
# *** 打开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入门实践(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 (); // 初始化存储器
使用:

tsx 复制代码
// 存储一个字符串  
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 监听软件头部事件无法触发页面事件(必须来自手势);

触发页面事件报错:必须来自手势。规避主进程无法触发页面特殊事件(如: 打开文件事件)。

ts 复制代码
<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
}))
相关推荐
爬楼的猪1 小时前
OpenAI的对话和图像API简单体验
javascript·python·aigc
在线打码4 小时前
网络攻击行为可视化分析系统【数据分析 + 可视化】
javascript·数据挖掘·数据分析·flask·数据可视化
肖老师xy5 小时前
uniapp使用chooseLocation安卓篇
android·javascript·uni-app
ChoSeitaku6 小时前
No.1|Godot|俄罗斯方块复刻|棋盘和初始方块的设置
java·前端·godot
生信天地6 小时前
jQuery:前端开发的高效利器
前端·jquery
牛奶皮子6 小时前
vue3Class 与 Style 绑定
前端·javascript·vue.js
傻小胖6 小时前
React setState详细使用总结
前端·react.js·前端框架
蒜蓉大猩猩7 小时前
Node.js - HTTP
javascript·网络·后端·网络协议·http·node.js·php
网络安全Jack7 小时前
[CTF/网络安全] 攻防世界 Web_php_unserialize 解题详析
前端·web安全·php