Electron通信流程

news2025/6/9 23:13:43

前言       

 今天讲Electron框架的通信流程,首先我们需要知道为什么需要通信。这得益于Electron的多进程模型,它主要模仿chrome的多进程模型如下图:

chrome多进程模型

作为应用开发者,我们将控制两种类型的进程:主进程和渲染器进程 。

主进程、渲染进程和Preload脚本

主进程

每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。

除此之外,主进程还可通过BrowserWindow类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。

const { BrowserWindow } = require('electron')

const win = new BrowserWindow({ width: 800, height: 1500 })
win.loadURL('https://github.com')

const contents = win.webContents
console.log(contents)

渲染进程

每个 Electron 应用都会为每个打开的 BrowserWindow ( 与每个网页嵌入 ) 生成一个单独的渲染器进程。 洽如其名,渲染器负责 渲染 网页内容。 所以实际上,运行于渲染器进程中的代码是须遵照网页标准的 (至少就目前使用的 Chromium 而言是如此) 。

因此,一个浏览器窗口中的所有的用户界面和应用功能,都应与您在网页开发上使用相同的工具和规范来进行攥写。

此外,这也意味着渲染器无权直接访问 require 或其他 Node.js API。 为了在渲染器中直接包含 NPM 模块,您必须使用与在 web 开发时相同的打包工具 (例如 webpack 或 parcel)

既然如此,那渲染器进程用户界面怎样才能与 Node.js 和 Electron 的原生桌面功能进行交互呢?这用到了Preload脚本

Preload脚本

预加载(preload)脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 。 这些脚本虽运行于渲染器的环境中,却因能访问 Node.js API 而拥有了更多的权限。

预加载脚本可以在 BrowserWindow 构造方法中的 webPreferences 选项里被附加到主进程。

const { BrowserWindow } = require('electron')
// ...
const win = new BrowserWindow({
  webPreferences: {
    preload: 'path/to/preload.js'
  }
})
// ...

 因为预加载脚本与浏览器共享同一个全局 Window 接口,并且可以访问 Node.js API,所以它通过在全局 window 中暴露任意 API 来增强渲染器,以便你的网页内容使用。

// preload.js
window.myAPI = {
  desktop: true
}





// renderer.js
console.log(window.myAPI)
// => undefined

语境隔离(Context Isolation)意味着预加载脚本与渲染器的主要运行环境是隔离开来的,以避免泄漏任何具特权的 API 到您的网页内容代码中。我们可以使用contextBridge方法安全的暴露api

// preload.js 
const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('myAPI', {
  desktop: true
})



// renderer.js
console.log(window.myAPI)
// => { desktop: true }

IPC通道

通过ipcMain和ipcRenderer模块可以创建任意 (您可以随意命名它们)和 双向 (您可以在两个模块中使用相同的通道名称)的通道。

模式 1:渲染器进程到主进程(单向)​

通常使用此模式从 Web 内容调用主进程 API。 我们将通过创建一个简单的应用来演示此模式,可以通过编程方式更改它的窗口标题。

对于此演示,您需要将代码添加到主进程、渲染器进程和预加载脚本。 完整代码如下:

// main.js
const { app, BrowserWindow, ipcMain } = require('electron/main')
const path = require('node:path')

function createWindow () {
  const mainWindow = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })
  // 使用ipcMain.on监听事件
  ipcMain.on('set-title', (event, title) => {
    const webContents = event.sender
    const win = BrowserWindow.fromWebContents(webContents)
    win.setTitle(title)
  })

  mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
  createWindow()

  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
})

// preload.js
const { contextBridge, ipcRenderer } = require('electron/renderer')
 //通过预加载脚本暴露ipcRenderer.send
contextBridge.exposeInMainWorld('electronAPI', {
  setTitle: (title) => ipcRenderer.send('set-title', title)
})


// renderer.js
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
  const title = titleInput.value
  window.electronAPI.setTitle(title)
})
// index.html
// 构建渲染器进程UI
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <title>Hello World!</title>
  </head>
  <body>
    Title: <input id="title"/>
    <button id="btn" type="button">Set</button>
    <script src="./renderer.js"></script>
  </body>
</html>

模式 2:渲染器进程到主进程(双向)​

双向 IPC 的一个常见应用是从渲染器进程代码调用主进程模块并等待结果。 可以通过 ipcRenderer.invoke和  ipcMain.handle.实现。

在下面的示例中,我们将从渲染器进程打开一个原生的文件对话框,并返回所选文件的路径。完整代码如下:

// main.js 
const { app, BrowserWindow, ipcMain, dialog } = require('electron/main')
const path = require('node:path')

async function handleFileOpen () {
  const { canceled, filePaths } = await dialog.showOpenDialog()
  if (!canceled) {
    return filePaths[0]
  }
}

function createWindow () {
  const mainWindow = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })
  mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
  // 使用ipcMain.handle监听事件
  ipcMain.handle('dialog:openFile', handleFileOpen)
  createWindow()
  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
})

// preload.js
const { contextBridge, ipcRenderer } = require('electron/renderer')
// 预加载脚本暴露ipcRenderer.invoke
contextBridge.exposeInMainWorld('electronAPI', {
  openFile: () => ipcRenderer.invoke('dialog:openFile')
})

// renderer.js 
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')

btn.addEventListener('click', async () => {
  const filePath = await window.electronAPI.openFile()
  filePathElement.innerText = filePath
})
// index.html
// 构建渲染器进程UI
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <title>Dialog</title>
  </head>
  <body>
    <button type="button" id="btn">Open a File</button>
    File path: <strong id="filePath"></strong>
    <script src='./renderer.js'></script>
  </body>
</html>

模式 3:主进程到渲染器进程​

将消息从主进程发送到渲染器进程时,需要指定是哪一个渲染器接收消息。主要通过WebContents实例和send方法实现

为了演示此模式,我们将构建一个由原生操作系统菜单控制的数字计数器。 

对于此演示,您需要将代码添加到主进程、渲染器进程和预加载脚本。 完整代码如下:

// main.js
const { app, BrowserWindow, Menu, ipcMain } = require('electron/main')
const path = require('node:path')

function createWindow () {
  const mainWindow = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

  const menu = Menu.buildFromTemplate([
    {
      label: app.name,
      submenu: [
        {
          // 使用webContents模块发送信息
          click: () => mainWindow.webContents.send('update-counter', 1),
          label: 'Increment'
        },
        {
          // 使用webContents模块发送信息
          click: () => mainWindow.webContents.send('update-counter', -1),
          label: 'Decrement'
        }
      ]
    }

  ])

  Menu.setApplicationMenu(menu)
  mainWindow.loadFile('index.html')

  // Open the DevTools.
  mainWindow.webContents.openDevTools()
}

app.whenReady().then(() => {
  ipcMain.on('counter-value', (_event, value) => {
    console.log(value) // will print value to Node console
  })
  createWindow()

  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
})

// preload.js
const { contextBridge, ipcRenderer } = require('electron/renderer')
// 通过预加载脚本暴露ipcRenderer.on
contextBridge.exposeInMainWorld('electronAPI', {
  onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)),
  counterValue: (value) => ipcRenderer.send('counter-value', value)
})

// renderer.js
const counter = document.getElementById('counter')

window.electronAPI.onUpdateCounter((value) => {
  const oldValue = Number(counter.innerText)
  const newValue = oldValue + value
  counter.innerText = newValue.toString()
  window.electronAPI.counterValue(newValue)
})
// index.html
// 构建渲染器UI
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <title>Menu Counter</title>
  </head>
  <body>
    Current value: <strong id="counter">0</strong>
    <script src="./renderer.js"></script>
  </body>
</html>

模式 4:渲染器进程到渲染器进程​

没有直接的方法可以使用 ipcMain 和 ipcRenderer 模块在 Electron 中的渲染器进程之间发送消息。 为此,您有两种选择:

  • 将主进程作为渲染器之间的消息代理。 这需要将消息从一个渲染器发送到主进程,然后主进程将消息转发到另一个渲染器。
  • 通过MessagePort构建通信通道, 这将允许在初始设置后渲染器之间直接进行通信。

消息端口

简介

MessagePort是一个允许在不同上下文之间传递消息的Web功能。 就像 window.postMessage, 但是在不同的通道上。

下面是 MessagePort 是什么和如何工作的一个非常简短的例子:

// renderer.js
// 消息端口是成对创建的。 连接的一对消息端口
// 被称为通道。
const channel = new MessageChannel()

// port1 和 port2 之间唯一的不同是你如何使用它们。 消息
// 发送到port1 将被port2 接收,反之亦然。
const port1 = channel.port1
const port2 = channel.port2

// 允许在另一端还没有注册监听器的情况下就通过通道向其发送消息
// 消息将排队等待,直到一个监听器注册为止。
port2.postMessage({ answer: 42 })

// 这次我们通过 ipc 向主进程发送 port1 对象。 类似的,
// 我们也可以发送 MessagePorts 到其他 frames, 或发送到 Web Workers, 等.
ipcRenderer.postMessage('port', null, [port1])
// main.js
// 在主进程中,我们接收端口对象。
ipcMain.on('port', (event) => {
  // 当我们在主进程中接收到 MessagePort 对象, 它就成为了
  // MessagePortMain.
  const port = event.ports[0]

  // MessagePortMain 使用了 Node.js 风格的事件 API, 而不是
  // web 风格的事件 API. 因此使用 .on('message', ...) 而不是 .onmessage = ...
  port.on('message', (event) => {
    // 收到的数据是: { answer: 42 }
    const data = event.data
  })

  // MessagePortMain 阻塞消息直到 .start() 方法被调用
  port.start()
})

实例使用

在两个渲染进程之间建立 MessageChannel​

在这个示例中,主进程设置了一个MessageChannel,然后将每个端口发送给不同的渲染进程。 这样可以让渲染进程彼此之间发送消息,而无需使用主进程作为中转。

// main.js 
const { BrowserWindow, app, MessageChannelMain } = require('electron')

app.whenReady().then(async () => {
  // 创建窗口
  const mainWindow = new BrowserWindow({
    show: false,
    webPreferences: {
      contextIsolation: false,
      preload: 'preloadMain.js'
    }
  })

  const secondaryWindow = new BrowserWindow({
    show: false,
    webPreferences: {
      contextIsolation: false,
      preload: 'preloadSecondary.js'
    }
  })

  // 建立通道
  const { port1, port2 } = new MessageChannelMain()

  // webContents准备就绪后,使用postMessage向每个webContents发送一个端口。
  mainWindow.once('ready-to-show', () => {
    mainWindow.webContents.postMessage('port', null, [port1])
  })

  secondaryWindow.once('ready-to-show', () => {
    secondaryWindow.webContents.postMessage('port', null, [port2])
  })
})

接下来,在你的预加载脚本中通过IPC接收端口,并设置相应的监听器

// preloadMain.js 和 preloadSecondary.js
const { ipcRenderer } = require('electron')

ipcRenderer.on('port', e => {
  // 接收到端口,使其全局可用。
  window.electronMessagePort = e.ports[0]

  window.electronMessagePort.onmessage = messageEvent => {
    // 处理消息
  }
})

这意味着 window.electronMessagePort 在全局范围内可用,你可以在应用程序的任何地方调用postMessage 方法,以便向另一个渲染进程发送消息。

// renderer.js
// elsewhere in your code to send a message to the other renderers message handler
window.electronMessagePort.postMessage('ping')
Worker进程​

在这个示例中,你的应用程序有一个作为隐藏窗口存在的 Worker 进程。 你希望应用程序页面能够直接与 Worker 进程通信,而不需要通过主进程进行中继,以避免性能开销。

 

// main.js
const { BrowserWindow, app, ipcMain, MessageChannelMain } = require('electron')

app.whenReady().then(async () => {
  // Worker 进程是一个隐藏的 BrowserWindow
  // 它具有访问完整的Blink上下文(包括例如 canvas、音频、fetch()等)的权限
  const worker = new BrowserWindow({
    show: false,
    webPreferences: { nodeIntegration: true }
  })
  await worker.loadFile('worker.html')

  // main window 将发送内容给 worker process 同时通过 MessagePort 接收返回值
  const mainWindow = new BrowserWindow({
    webPreferences: { nodeIntegration: true }
  })
  mainWindow.loadFile('app.html')

  // 在这里我们不能使用 ipcMain.handle() , 因为回复需要传输
  // MessagePort.
  // 监听从顶级 frame 发来的消息
  mainWindow.webContents.mainFrame.ipc.on('request-worker-channel', (event) => {
    // 建立新通道  ...
    const { port1, port2 } = new MessageChannelMain()
    // ... 将其中一个端口发送给 Worker ...
    worker.webContents.postMessage('new-client', null, [port1])
    // ... 将另一个端口发送给主窗口
    event.senderFrame.postMessage('provide-worker-channel', null, [port2])
    // 现在主窗口和工作进程可以直接相互通信,无需经过主进程!
  })
})
// worker.html
<script>
const { ipcRenderer } = require('electron')

const doWork = (input) => {
  // 一些对CPU要求较高的任务
  return input * 2
}

// 我们可能会得到多个 clients, 比如有多个 windows,
// 或者假如 main window 重新加载了.
ipcRenderer.on('new-client', (event) => {
  const [ port ] = event.ports
  port.onmessage = (event) => {
    // 事件数据可以是任何可序列化的对象 (事件甚至可以
    // 携带其他 MessagePorts 对象!)
    const result = doWork(event.data)
    port.postMessage(result)
  }
})
</script>
// app.html
<script>
const { ipcRenderer } = require('electron')

// 我们请求主进程向我们发送一个通道
// 以便我们可以用它与 Worker 进程建立通信
ipcRenderer.send('request-worker-channel')

ipcRenderer.once('provide-worker-channel', (event) => {
  // 一旦收到回复, 我们可以这样做...
  const [ port ] = event.ports
  // ... 注册一个接收结果处理器 ...
  port.onmessage = (event) => {
    console.log('received result:', event.data)
  }
  // ... 并开始发送消息给 work!
  port.postMessage(21)
})
</script>
 回复流

Electron的内置IPC方法只支持两种模式:即发即弃(例如, send),或请求-响应(例如, invoke)。 使用MessageChannels,你可以实现一个“响应流”,其中单个请求可以返回一串数据。

// renderer.js
const makeStreamingRequest = (element, callback) => {
  // MessageChannels 是轻量的
  // 为每个请求创建一个新的 MessageChannel 带来的开销并不大
  const { port1, port2 } = new MessageChannel()

  // 我们将端口的一端发送给主进程 ...
  ipcRenderer.postMessage(
    'give-me-a-stream',
    { element, count: 10 },
    [port2]
  )

  // ... 保留另一端。 主进程将向其端口发送消息
  // 并在完成后关闭它
  port1.onmessage = (event) => {
    callback(event.data)
  }
  port1.onclose = () => {
    console.log('stream ended')
  }
}

makeStreamingRequest(42, (data) => {
  console.log('got response data:', data)
})
// 我们会看到 "got response data: 42" 出现了10次
// main.js
ipcMain.on('give-me-a-stream', (event, msg) => {
  // 渲染进程向我们发送了一个 MessagePort
  // 并期望得到响应
  const [replyPort] = event.ports

  // 在这里,我们同步发送消息
  // 我们也可以将端口存储在某个地方,异步发送消息
  for (let i = 0; i < msg.count; i++) {
    replyPort.postMessage(msg.element)
  }

  // 当我们处理完成后,关闭端口以通知另一端
  // 我们不会再发送任何消息 这并不是严格要求的
  // 如果我们没有显式地关闭端口,它最终会被垃圾回收
  // 这也会触发渲染进程中的'close'事件
  replyPort.close()
})

总结:

  • 主进程和渲染器进程之间存在语境隔离
  • 主进程和渲染器进程可通过electron内置IPC通道进行通信
  • 渲染器之间可通过IPC通道,主进程为中间人互相通信;也可通过信息端口构建信道直接进行通信

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2405955.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

华为云Flexus+DeepSeek征文|华为云Flexus服务器dify平台通过自然语言转sql并执行实现电商数据分析

目录 前言 1 华为云Flexus服务器部署Dify平台 1.1 华为云Flexus服务器一键部署Dify平台 1.2 设置账号登录Dify&#xff0c;进入平台 2 构建自然语言转SQL并执行的应用 2.1 创建应用并启动工作流设计 2.2 应用框架设计 2.3 自然语言转SQL模块详解 2.4 代码执行模块实现…

IDEA中微服务指定端口启动

在使用IDEA开发SpringBoot微服务时&#xff0c;经常需要开启多个服务实例以测试负载均衡&#xff0c;以下几种方法开启不同端口。 直接在配置文件中指定 # application.propertiesserver.port8001指定VM参数 点击Modify options&#xff0c;选择Add VM options&#xff0c;值…

Spring Cloud Alibaba Seata安装+微服务实战

目录 介绍核心功能三层核心架构安装微服务实战创建三个业务数据库编写库存和账户两个Feign接口订单微服务 seata-order-service9701库存微服务 seata-store-service9702账户微服务 seata-account-service9703测试结果 总结 介绍 Spring Cloud Alibaba Seata 是一款开源的分布式…

FMC STM32H7 SDRAM

如何无痛使用片外SDRAM? stm32 已经成功初始化了 STM32H7 上的外部 SDRAM&#xff08;32MB&#xff09; 如何在开发中无痛使用SDRAM 使它像普通 RAM 一样“自然地”使用? [todo] 重要 MMT(Memory Management Tool) of STM32CubeMx The Memory Management Tool (MMT) disp…

部署DNS从服务器

部署DNS从服务器的目的 DNS域名解析服务中&#xff0c;从服务器可以从主服务器上获得指定的区域数据文件&#xff0c;从而起到备份解析记录与负载均衡的作用&#xff0c;因此通过部署从服务器可以减轻主服务器的负载压力&#xff0c;还可以提升用户的查询效率。 注意&#xf…

Android Camera Hal中通过Neon指令优化数据拷贝

背景描述&#xff1a; Camera apk普通相机模式录像操作时&#xff0c;一般是同时请求两个流&#xff0c;即预览流和录像流。对于两个流输出图像格式和分辨率相同的情况下&#xff0c;是不是可以通过一个流拷贝得到另一个流的数据&#xff0c;进而节省掉一个Sensor输出处理两次…

C# winform教程(二)----button

一、button的使用方法 主要使用方法几乎都在属性内&#xff0c;我们操作也在这个界面 二、作用 用户点击时触发事件&#xff0c;事件有很多种&#xff0c;可以根据需要选择。 三、常用属性 虽然属性很多&#xff0c;但是常用的并不多 3.常用属性 名称内容含义AutoSize自动调…

Python编码格式化之PEP8编码规范

文章目录 概要PEP8编码风格py文本组织规范命名规范编码风格 PEP8编码检查工具pylintflake8PyCharm中配置检查工具 PEP8编码格式化工具blackautopep8PyCharm配置格式化工具本地git配置hook 总结 概要 在Python项目开发过程中&#xff0c;代码的可读性和一致性对于项目的长期维护…

【Zephyr 系列 14】使用 MCUboot 实现 BLE OTA 升级机制:构建安全可靠的固件分发系统

🧠关键词:Zephyr、MCUboot、OTA 升级、BLE DFU、双分区、Bootloader、安全固件管理 📌面向读者:希望基于 Zephyr 为 BLE 设备加入安全 OTA 升级功能的开发者 📊预计字数:5200+ 字 🧭 前言:为什么你需要 OTA? 随着设备部署数量增多与产品生命周期延长,远程升级(…

K8S认证|CKS题库+答案| 8. 沙箱运行容器 gVisor

目录 8. 沙箱运行容器 gVisor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作&#xff1a; 1&#xff09;、切换集群 2&#xff09;、官网找模板 3&#xff09;、创建 RuntimeClass 4&#xff09;、 将命名空间为 server 下的 Pod 引用 RuntimeClass 5&#xff09…

Selenium4+Python的web自动化测试框架

一、什么是Selenium&#xff1f; Selenium是一个基于浏览器的自动化测试工具&#xff0c;它提供了一种跨平台、跨浏览器的端到端的web自动化解决方案。Selenium主要包括三部分&#xff1a;Selenium IDE、Selenium WebDriver 和Selenium Grid。 Selenium IDE&#xff1a;Firefo…

【论文解读】MemGPT: 迈向为操作系统的LLM

1st author: Charles Packer paper MemGPT[2310.08560] MemGPT: Towards LLMs as Operating Systems code: letta-ai/letta: Letta (formerly MemGPT) is the stateful agents framework with memory, reasoning, and context management. 这个项目现在已经转化为 Letta &a…

vb监测Excel两个单元格变化,达到阈值响铃

需求 在Excel中实现监控两个单元格之间的变化范围&#xff0c;当达到某个设定的值的范围内时&#xff0c;实现自动响铃提示。 实现&#xff1a; 首先设置Excel&#xff0c;开启宏、打开开发者工具&#xff0c;点击visual Basic按钮&#xff0c;然后在左侧双击需要监测的shee…

node 进程管理工具 pm2 的详细说明 —— 一步一步配置 Ubuntu Server 的 NodeJS 服务器详细实录 7

前言 我以 Ubuntu Server 打造的 NodeJS 服务器为主题的系列文章&#xff0c;经过五篇博客&#xff0c;我们顺利的 安装了 ubuntu server 服务器&#xff0c;并且配置好了 ssh 免密登录服务器&#xff0c;安装好了 服务器常用软件安装, 配置好了 zsh 和 vim 以及 通过 NVM 安装…

Flask与Celery 项目应用(shared_task使用)

目录 1. 项目概述主要功能技术栈 2. 项目结构3. 环境设置创建虚拟环境并安装依赖主要依赖 4. 应用配置Flask应用初始化 (__init__.py)Celery应用初始化 (make_celery.py) 5. 定义Celery任务 (tasks.py)任务说明 6. 创建API端点 (views.py)API端点说明 7. 前端界面 (index.html)…

二叉树-226.翻转链表-力扣(LeetCode)

一、题目解析 翻转可以理解为树的左右子树交换&#xff0c;从根到叶子节点&#xff0c;但是这里交换的是链接的指针&#xff0c;而不是单纯的交换值&#xff0c;当出现nullptr时&#xff0c;也是可以交换链接的&#xff0c;交换值的话就不行了。 二、算法原理 依旧的递归&…

HarmonyOS Next 弹窗系列教程(3)

HarmonyOS Next 弹窗系列教程&#xff08;3&#xff09; 选择器弹窗 (PickerDialog) 介绍 选择器弹窗通常用于在用户进行某些操作&#xff08;如点击按钮&#xff09;时显示特定的信息或选项。让用户可以进行选择提供的固定的内容。 以下内容都属于选择器弹窗&#xff1a; …

【docker】Windows安装docker

环境及工具&#xff08;点击下载&#xff09; Docker Desktop Installer.exe &#xff08;windows 环境下运行docker的一款产品&#xff09; wsl_update_x64 &#xff08;Linux 内核包&#xff09; 前期准备 系统要求2&#xff1a; Windows 11&#xff1a;64 位系统&am…

无人机避障——感知部分(Ubuntu 20.04 复现Vins Fusion跑数据集)胎教级教程

硬件环境&#xff1a;NVIDIA Jeston Orin nx 系统&#xff1a;Ubuntu 20.04 任务&#xff1a;跑通 EuRoC MAV Dataset 数据集 展示结果&#xff1a; 编译Vins Fusion 创建工作空间vins_ws # 创建目录结构 mkdir -p ~/vins_ws/srccd ~/vins_ws/src# 初始化工作空间&#xf…

如何安装并使用RustDesk

参考&#xff1a; 搭建 RustDesk Server&#xff1a;打造属于自己的远程控制系统&#xff0c;替代 TeamViewer 和 ToDesk&#xff01; 向日葵、ToDesk再见&#xff01;自己动手&#xff0c;自建RustDesk远程服务器真香&#xff01; 通俗易懂&#xff1a;RustDesk Server的搭…