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

其他

相关推荐
长风清留扬9 分钟前
小程序毕业设计-音乐播放器+源码(可播放)下载即用
javascript·小程序·毕业设计·课程设计·毕设·音乐播放器
m0_7482478023 分钟前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
ZJ_.1 小时前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
GIS开发特训营1 小时前
Vue零基础教程|从前端框架到GIS开发系列课程(七)响应式系统介绍
前端·vue.js·前端框架·gis开发·webgis·三维gis
Cachel wood1 小时前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
joan_851 小时前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui
还是大剑师兰特2 小时前
什么是尾调用,使用尾调用有什么好处?
javascript·大剑师·尾调用
Watermelo6172 小时前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
一个处女座的程序猿O(∩_∩)O4 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
燃先生._.10 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js