当前位置: 首页 > article >正文

Electron通信流程

前言       

 今天讲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.senderconst 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.valuewindow.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 + valuecounter.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.portsport.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通道,主进程为中间人互相通信;也可通过信息端口构建信道直接进行通信

相关文章:

Electron通信流程

前言 今天讲Electron框架的通信流程&#xff0c;首先我们需要知道为什么需要通信。这得益于Electron的多进程模型&#xff0c;它主要模仿chrome的多进程模型如下图&#xff1a; 作为应用开发者&#xff0c;我们将控制两种类型的进程&#xff1a;主进程和渲染器进程 。 …...

华为云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;值…...

BERT, GPT, Transformer之间的关系

1. Transformer 是什么&#xff1f;简单介绍 1.1 通俗理解 想象你是一个翻译员&#xff0c;要把一句话从中文翻译成英文。你需要同时看句子里的每个词&#xff0c;理解它们之间的关系。Transformer就像一个超级翻译助手&#xff0c;它用“自注意力机制”&#xff08;Attentio…...

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…...

Ubuntu 系统.sh脚本一键部署内网Java服务(组件使用docker镜像,宕机自启动)

#!/bin/bash# 更新系统并安装必要的依赖 sudo apt update sudo apt install -y apt-transport-https ca-certificates curl software-properties-common# 安装 Docker curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - sudo add-apt-repository …...

用 n8n 提取静态网页内容:从 HTTP Request 到 HTML 节点全解析

n8n 的 HTTP Request HTML 节点组合是个实用又高效的工具。这篇文章就带你一步步搞懂如何用它们提取静态网页内容&#xff0c;重点解析 HTML 节点参数和 CSS 选择器&#xff0c;让你轻松上手 。 一、整体流程概览 我们的目标是从静态网页中提取特定内容&#xff0c;流程分两…...

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

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

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

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

AcWing 3417:砝码称重——位集合

【题目来源】 3417. 砝码称重 - AcWing题库 【题目描述】 你有一架天平和 N 个砝码&#xff0c;这 N 个砝码重量依次是 W1,W2,⋅⋅⋅,WN。 请你计算一共可以称出多少种不同的正整数重量&#xff1f; 注意砝码可以放在天平两边。 【输入格式】 输入的第一行包含一个整数 N。 …...

我认为STM32输入只分为模拟输入 与 数字输入

核心概念解析 模拟输入 (Analog Input) 设计目的&#xff1a;直接连接模拟信号&#xff08;如ADC采集电压、温度传感器输出&#xff09; 硬件行为&#xff1a; ✅ 断开内部数字电路&#xff08;施密特触发器禁用&#xff09; ✅ 信号直通模拟外设&#xff08;如ADC、运放&…...

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…...

【Redis】数据库与缓存一致性

目录 1、背景2、核心问题3、常见解决方案【1】缓存更新策略[1]旁路缓存模式&#xff08;Cache-Aside&#xff09;[2]写穿透模式&#xff08;Write-Through&#xff09;[3]写回模式 【2】删除与更新策略[1]先更新数据库再删除缓存[2]先删除缓存再更新数据库 【3】一致性保障机制…...

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…...

跨域请求解决方案全解析

跨域请求可以通过多种技术方案实现&#xff0c;核心是绕过浏览器的同源策略限制。以下是主流解决方案及具体实现方式&#xff1a; 一、CORS&#xff08;跨域资源共享&#xff09; 最常用的标准化方案&#xff0c;通过服务器设置HTTP响应头实现&#xff1a; Access-Control-Al…...

【前端】vue3性能优化方案

以下是Vue 3性能优化的系统性方案&#xff0c;结合核心优化策略与实用技巧&#xff0c;覆盖渲染、响应式、加载、代码等多个维度&#xff1a; ⚙️ 一、渲染优化 精准控制渲染范围 v-if vs v-show&#xff1a; v-if&#xff1a;条件为假时销毁DOM&#xff0c;适合低频切换场景&…...

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)…...

Fetch API 使用详解:Bearer Token 与 localStorage 实践

Fetch API&#xff1a;现代浏览器内置的用于发送 HTTP 请求的 API&#xff0c;Bearer Token&#xff1a;一种基于令牌的身份验证方案&#xff0c;常用于 JWT 认证&#xff0c;localStorage&#xff1a;浏览器提供的持久化存储方案&#xff0c;用于在客户端存储数据。 token是我…...

vue3 vite.config.js 引入bem.scss文件报错

[sass] Can’t find stylesheet to import. ╷ 1 │ use “/bem.scss” as *; │ ^^^^^^^^^^^^^^^^^^^^^^ ╵ src\App.vue 1:1 root stylesheet 分析 我们遇到了一个在Vue3项目中使用Vite时&#xff0c;在vite.config.js中引入bem.scss文件报错的问题。错误信息指出在App.vue…...

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

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

HarmonyOS Next 弹窗系列教程(3)

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

编程笔记---问题小计

编程笔记 qml ProgressBar 为什么valuemodel.progress / 100 在QML中&#xff0c;ProgressBar的value属性用于表示进度条的当前进度值&#xff0c;其范围通常为0到1&#xff08;或0%到100%&#xff09;。当使用model.progress / 100来设置value时&#xff0c;这样做的原因是为…...

【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…...