vue vite ts electron ipc addon-napi c arm64

初始化

因网络问题建议使用 cnpm 代替 npm

复制代码
npm init vue # 全选 yes
npm i # 进入项目目录后使用
npm i electron electron-builder -D
npm i commander -D # 额外组件

electron

新建 plugins、src/electron 文件夹

添加 src/electron/background.ts

属于主进程

ipcMain.on、ipcMain.handle 都用于主进程监听 ipc,ipcMain.on 用于监听 ipcRenderer.send,ipcMain.handle 用于监听 ipcRenderer.invoke 并 return xxx

ipc 单向:

从渲染进程发向主进程:ipcRenderer.send

从主进程发向渲染进程:window.webContents.send

ipc 双向:

从渲染进程发向主进程,主进程还会返回发向渲染进程:ipcRenderer.invoke

从主进程发向渲染进程,渲染进程还会返回发向主进程:没有类似于 ipcRenderer.invoke 的,需要间接实现。主进程使用 window.webContents.send,渲染进程使用 ipcRenderer.send

渲染进程之间的 ipc 通信:

让主进程中转,也就是"渲染进程1"使用 ipcRenderer.send 到主进程的 ipcMain.on,然后主进程在这个 ipcMain.on 里用相应的 window2.webContents.send 发送到"渲染进程2"里。每个 "const windowxxx = new BrowserWindow" 就是一个新的渲染进程

复制代码
import { app, BrowserWindow, screen, ipcMain } from 'electron'
import path from 'path'
import { Command } from 'commander'

// 当 electron 准备好时触发
app.whenReady().then(() => {
  // 使用 c 编译出的 hello.node
  try {
    var addon = require('./addon/hello')
    console.log(addon.hello());
  } catch (error) {
    console.log(error)
  }
  
  // 命令行参数解析
  const command = new Command
  command
    .option('-m, --maximize', 'maximize window')
    .option('-l, --location <>', 'location of load index page', 'index.html')
    .option('-d, --dev', 'openDevTools')
    .option('--no-sandbox', 'other')
    .parse()

  const options = command.opts()

  // 创建窗口
  const window = new BrowserWindow({
    // 命令行加 -m 则全屏
    width: options.maximize ? screen.getPrimaryDisplay().workAreaSize.width : 800,
    height: options.maximize ? screen.getPrimaryDisplay().workAreaSize.height : 600,

    autoHideMenuBar: true,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js') // 加载 预加载脚本,用于向渲染进程提供使用 ipc 通信的方法
    }
  })

  // 命令行加 -l http://xxx:xxx 则加载该 url 的 index.html,可实时刷新页面的更改,用于调试
  if (options.location.indexOf(':') >= 0)
    window.loadURL(options.location)
  else
    window.loadFile(options.location)

  // 命令行加 -d 则打开开发者工具
  if (options.dev)
    window.webContents.openDevTools()

  // ipc
  ipcMain.on('rtm', () => {
    console.log('rtm')
    window.webContents.send('mtr')
  })

  ipcMain.on('rtm_p', (e, p) => {
    console.log(p)
    window.webContents.send('mtr_p', `mtr_p ${p}`)
  })

  ipcMain.handle('rtmmtr_p', (e, p) => {
    console.log(p)
    return `rtmmtr_p ${p}`
  })
})

添加 src/electron/preload.ts

预加载脚本,用来给渲染进程提供使用 ipc 的方法

rtm 是渲染进程发向主进程;rtmmtr 是从渲染进程发向主进程,主进程还会返回发向渲染进程;mtr 是主进程发向渲染进程

复制代码
import { contextBridge, ipcRenderer } from 'electron'

contextBridge.exposeInMainWorld('electronAPI', {
  rtm: () => ipcRenderer.send('rtm'),
  rtm_p: (p: any) => ipcRenderer.send('rtm_p', p),
  rtmmtr_p: (p: any) => ipcRenderer.invoke('rtmmtr_p', p),
  mtr: (p: any) => ipcRenderer.on('mtr', p),
  mtr_p: (p: any) => ipcRenderer.on('mtr_p', p),
})

添加 src/electron/renderer.d.ts

给渲染进程用的 preload.ts 里的方法的类型声明

复制代码
export interface IElectronAPI {
    rtm: () => Promise<any>,
    rtm_p: (p: any) => Promise<any>,
    rtmmtr_p: (p: any) => Promise<any>,
    mtr: (p: any) => Promise<any>,
    mtr_p: (p: any) => Promise<any>,
}

declare global {
    interface Window {
        electronAPI: IElectronAPI
    }
}

添加 plugins/vite.electron.dev.ts

自定义 dev 方法,用于启动 vite 后带起 electron

复制代码
import type { Plugin } from 'vite'
import type { AddressInfo } from 'net'
import { spawn } from 'child_process'
import fs from 'fs'

// 导出Vite插件函数
export const viteElectronDev = (): Plugin => {
    return {
        name: 'vite-electron-dev',
        // 在configureServer中实现插件的逻辑
        configureServer(server) {
            // 定义初始化Electron的函数
            const initElectron = () => {
                // 使用esbuild编译TypeScript代码为JavaScript
                require('esbuild').buildSync({
                    entryPoints: ['src/electron/background.ts', 'src/electron/preload.ts'],
                    bundle: true,
                    outdir: 'dist',
                    platform: 'node',
                    external: ['electron']
                })

                // 复制 .node
                try {
                    fs.mkdirSync('dist/addon')
                } catch (error) {}
                fs.copyFileSync('addon/hello/build/Release/hello.node', 'dist/addon/hello.node')
            }

            // electron 运行
            let electron_run = (ip: string) => {
                initElectron()
                // 启动Electron进程并添加相应的命令行参数
                let electronProcess = spawn(require('electron'), ['dist/background.js', '-l', ip, '-d'])

                // 监听Electron进程的stdout输出
                electronProcess.stdout?.on('data', (data) => {
                    console.log(`${data}`);
                });

                return electronProcess
            }

            // 监听Vite的HTTP服务器的listening事件
            server?.httpServer?.once('listening', () => {
                // 获取HTTP服务器的监听地址和端口号
                const address = server?.httpServer?.address() as AddressInfo
                const ip = `http://localhost:${address.port}`

                let electronProcess = electron_run(ip)

                // 监听主进程代码的更改以自动编译这些 .ts 并重启 electron
                fs.watch('src/electron', () => {
                    // 杀死当前的Electron进程
                    electronProcess.kill()

                    electronProcess = electron_run(ip)
                })
            })
        }
    }
}

添加 plugins/vite.electron.build.ts

自定义 build 方法,这里打包了 linux 的 x64、arm64 的包

复制代码
import type { Plugin } from 'vite'
import * as electronBuilder from 'electron-builder'
import path from 'path'
import fs from 'fs'

// 导出Vite插件函数
export const viteElectronBuild = (): Plugin => {
    return {
        name: 'vite-electron-build',

        // closeBundle是Vite的一个插件钩子函数,用于在Vite构建完成后执行一些自定义逻辑。
        closeBundle() {

            // 定义初始化Electron的函数
            const initElectron = () => {
                // 使用esbuild编译TypeScript代码为JavaScript
                require('esbuild').buildSync({
                    entryPoints: ['src/electron/background.ts', 'src/electron/preload.ts'],
                    bundle: true,
                    outdir: 'dist',
                    platform: 'node',
                    external: ['electron']
                })

                // 复制 .node
                fs.mkdirSync('dist/addon')
                fs.copyFileSync('addon/hello/build/Release/hello.node', 'dist/addon/hello.node')
            }

            // 调用初始化Electron函数
            initElectron()

            // 修改package.json文件的main字段,不然会打包失败
            const json = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
            json.main = 'background.js'
            fs.writeSync(fs.openSync('dist/package.json', 'w'), JSON.stringify(json, null, 2))

            // 创建一个空的node_modules目录 不然会打包失败
            fs.mkdirSync('dist/node_modules')

            // 使用electron-builder打包Electron应用程序
            electronBuilder.build({
                config: {
                    appId: 'com.example.app',
                    productName: 'vite-electron',
                    directories: {
                        app: path.join(process.cwd(), "dist"), // 被打包的散文件目录
                        output: path.join(process.cwd(), "release"), // 生成的包的目录
                    },
                    linux: {
                        "target": [
                            {
                                "target": "AppImage", // 生成的包的类型 .AppImage
                                "arch": ["x64", "arm64"] // 会对每个架构都会生成对应的包。会下载对应架构的 electron,可能会失败,多试
                            }
                        ]
                    }
                }
            })
        }
    }
}

修改 src/App.vue

添加按钮和 ipc

属于渲染进程

window.electronAPI.xxx() 就是预加载脚本(preload.ts)给渲染进程提供的使用 ipc 的方法

window.electronAPI.mtr 和 ...mtr_p (mtr:main to renderer)用于监听主进程发过来的消息

由于 window.electronAPI.rtmmtr_p 使用 ipcRenderer.invoke,这是异步方法,如果不在其前面加 await 而直接获取会得到一个用于异步执行的对象(Promise),其内容包含了需要异步执行的东西,await 就是等待该对象运行结束从而获取正确值,而 await 需要其调用者是异步的,所以 increment() 也加上了 async(异步标志)

复制代码
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
import { ref } from 'vue'

// 响应式状态
const count = ref(0)

// 用来修改状态、触发更新的函数
async function increment() {
  count.value++
  window.electronAPI.rtm()
  window.electronAPI.rtm_p(`rtm_p ${count.value}`)
  const rtmmtr_p = await window.electronAPI.rtmmtr_p(`rtmmtr_p ${count.value}`)

  console.log(rtmmtr_p)
}

window.electronAPI.mtr(() => {
  console.log('mtr')
})

window.electronAPI.mtr_p((e: any, p: any) => {
  console.log(p)
})
</script>

<template>
  <button @click="increment">hhh: {{ count }}</button>

  <header>
    <img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />

    <div class="wrapper">
      <HelloWorld msg="You did it!" />

      <nav>
        <RouterLink to="/">Home</RouterLink>
        <RouterLink to="/about">About</RouterLink>
      </nav>
    </div>
  </header>

  <RouterView />
</template>

<style scoped>
header {
  line-height: 1.5;
  max-height: 100vh;
}

.logo {
  display: block;
  margin: 0 auto 2rem;
}

nav {
  width: 100%;
  font-size: 12px;
  text-align: center;
  margin-top: 2rem;
}

nav a.router-link-exact-active {
  color: var(--color-text);
}

nav a.router-link-exact-active:hover {
  background-color: transparent;
}

nav a {
  display: inline-block;
  padding: 0 1rem;
  border-left: 1px solid var(--color-border);
}

nav a:first-of-type {
  border: 0;
}

@media (min-width: 1024px) {
  header {
    display: flex;
    place-items: center;
    padding-right: calc(var(--section-gap) / 2);
  }

  .logo {
    margin: 0 2rem 0 0;
  }

  header .wrapper {
    display: flex;
    place-items: flex-start;
    flex-wrap: wrap;
  }

  nav {
    text-align: left;
    margin-left: -1rem;
    font-size: 1rem;

    padding: 1rem 0;
    margin-top: 1rem;
  }
}
</style>

修改 tsconfig.node.json

添加 "plugins/**/*.ts"

复制代码
{
  "extends": "@tsconfig/node18/tsconfig.json",
  "include": [
    "vite.config.*",
    "vitest.config.*",
    "cypress.config.*",
    "nightwatch.conf.*",
    "playwright.config.*",
    "plugins/**/*.ts"
  ],
  "compilerOptions": {
    "composite": true,
    "module": "ESNext",
    "types": ["node"]
  }
}

修改 vite.config.ts

添加 plugins

复制代码
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'

// plugins
import { viteElectronDev } from './plugins/vite.electron.dev'
import { viteElectronBuild } from './plugins/vite.electron.build'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    viteElectronDev(),
    viteElectronBuild()
  ],
  base: './', //默认绝对路径改为相对路径 否则打包白屏
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

addon-napi

为了让 electron 调用 c

再新建 addon/hello 目录

添加 addon/hello/hello.c

复制代码
#include <assert.h>
#include <node_api.h>

static napi_value Method(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value world;
  status = napi_create_string_utf8(env, "hello world", 11, &world);
  assert(status == napi_ok);
  return world;
}

#define DECLARE_NAPI_METHOD(name, func)                                        \
  { name, 0, func, 0, 0, 0, napi_default, 0 }

static napi_value Init(napi_env env, napi_value exports) {
  napi_status status;
  napi_property_descriptor desc = DECLARE_NAPI_METHOD("hello", Method);
  status = napi_define_properties(env, exports, 1, &desc);
  assert(status == napi_ok);
  return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

添加 addon/hello/hello.js

复制代码
var addon = require('./build/Release/hello')

console.log(addon.hello())

添加 addon/hello/binding.gyp

复制代码
{
  "targets": [
    {
      "target_name": "hello",
      "sources": [ "hello.c" ]
    }
  ]
}

添加 addon/hello/package.json

复制代码
{
  "name": "hello_world",
  "version": "0.0.0",
  "description": "Node.js Addons Example #1",
  "main": "hello.js",
  "private": true,
  "scripts": {
    "test": "node hello.js"
  },
  "gypfile": true
}

.node 的编译与测试

cd 到 addon/hello

复制代码
npm i # 安装依赖和编译
node ./ # 测试,会输出 hello world

修改 hello.c 后使用npm i重新编译

使用

启动:npm run dev

打包:npm run build

npm run dev后会在桌面出现应用界面,并自动打开开发者工具,命令行会输出 hello world

修改 src/electron 下的任何文件都会自动编译这些 .ts 并重启 electron

npm run build 后会在 release 下生成 vite-electron-0.0.0.AppImage 和 vite-electron-0.0.0-arm64.AppImage

其他

相关推荐
烛阴35 分钟前
Promise无法中断?教你三招优雅实现异步任务取消
前端·javascript
GUIQU.41 分钟前
【Vue】单元测试(Jest/Vue Test Utils)
前端·vue.js
前端张三1 小时前
vue3中ref在js中为什么需要.value才能获取/修改值?
前端·javascript·vue.js
爱的叹息2 小时前
解决 Dart Sass 的旧 JS API 弃用警告 的详细步骤和解决方案
javascript·rust·sass
夕水2 小时前
这个提升效率宝藏级工具一定要收藏使用
前端·javascript·trae
会飞的鱼先生2 小时前
vue3 内置组件KeepAlive的使用
前端·javascript·vue.js
苹果酱05673 小时前
【Azure Redis 缓存】在Azure Redis中,如何限制只允许Azure App Service访问?
java·vue.js·spring boot·mysql·课程设计
前端大白话3 小时前
前端崩溃瞬间救星!10 个 JavaScript 实战技巧大揭秘
前端·javascript
一千柯橘3 小时前
Nestjs 解决 request entity too large
javascript·后端
举个栗子dhy4 小时前
如何处理动态地址栏参数,以及Object.entries() 、Object.fromEntries()和URLSearchParams.entries()使用
javascript