新闻详情

新闻详情

首页 / 资讯中心 / 详情

Tauri与Electron跨平台桌面应用核心差异解析

发布时间:2026/9/26 13:57:01来源:尧图网络
Tauri与Electron跨平台桌面应用核心差异解析
1. 为什么“体积”和“启动”成了跨平台桌面应用的生死线去年底我接手一个内部工具重构项目一款面向硬件工程师的串口调试助手原版用 Electron 13 打包Windows 下安装包 128MB首次启动耗时 4.7 秒i7-11800H 512GB NVMe用户反馈“点开要等一杯咖啡凉”。更棘手的是客户安全审计报告里明确指出“主进程未启用上下文隔离IPC 通道暴露原始 Node.js API存在原型链污染风险”。这不是性能问题是交付红线。你可能觉得“128MB 也不算大”但真实场景远比想象残酷客户现场网络带宽普遍低于 10Mbps下载 128MB 安装包平均耗时 1分42秒企业级终端常部署深信服/奇安信等终端管控软件对 Electron 默认加载的nodeIntegration: true行为触发高频告警某金融客户要求所有桌面应用必须通过 CICD 流水线自动扫描二进制文件签名而 Electron 的asar包裹机制导致签名校验失败率高达 63%。这些不是理论推演是我连续三周在客户机房蹲点抓包、分析进程树、比对安全扫描日志后确认的事实。Tauri 的宣传页写着“Rust 编写的轻量级框架”但真正决定你能否落地的从来不是官网的 Benchmark 图表而是它如何处理 Windows 上link.exe not found这类报错、如何绕过鸿蒙系统对 WebView2 的兼容限制、如何让 Vue 渲染进程安全调用本地串口驱动——这些细节恰恰藏在 Electron 主进程 IPC 通信与 Vue 生命周期的耦合逻辑里也藏在 Tauri 的tauri.conf.json配置项与 Rust 构建脚本的交互边界中。所以这篇内容不谈“哪个更好”只拆解三个硬核事实第一体积差异的本质不是打包器不同而是运行时依赖模型的根本分裂——Electron 把 Chromium 和 Node.js 当作“操作系统”来提供Tauri 则把系统 WebView 当作“显卡驱动”来调用第二启动速度的瓶颈不在 JS 解析而在进程初始化阶段的符号解析与内存映射Windows 下link.exe报错背后是 MSVC 工具链与 Rust 编译器 ABI 的隐式绑定第三安全重构不是加个contextIsolation: true就能解决的它要求你重新设计主进程与渲染进程的通信契约而这个契约在 Vue 3 的 Composition API 里必须用shallowRef包裹响应式数据否则会触发 Tauri 的invoke调用栈溢出。接下来我会用真实项目中的构建日志、内存快照、安全扫描报告作为证据链带你一层层剥开这两个框架在生产环境中的真实肌理。2. 体积对比从 128MB 到 12MB 的压缩逻辑与代价先看一组实测数据。同一套 Vue 3 TypeScript Element Plus 的串口调试界面在相同构建配置下框架构建命令Windows 安装包大小macOS DMG 大小Linux AppImage 大小Electron 23electron-builder --win --x64128.4 MB142.7 MB139.2 MBTauri 1.10pnpm tauri build --target x64-pc-windows-msvc12.3 MB18.6 MB15.9 MB表面看 Tauri 小了 10 倍但这个数字极具欺骗性。我用7z l命令逐层解压 Electron 安装包发现其体积构成如下resources/app.asar你的代码仅 4.2 MBresources/electron.asarElectron 运行时38.6 MBchrome_100_percent.pak,chrome_200_percent.pakChromium 资源22.1 MBd3dcompiler_47.dll,libEGL.dll,libGLESv2.dllGPU 渲染库19.3 MBffmpeg.dll,icudtl.dat,snapshot_blob.bin音视频与 V8 快照44.2 MB提示Electron 的“体积过大”本质是 Chromium 的体积过大。Chromium 本身包含完整的 Blink 渲染引擎、V8 JavaScript 引擎、Skia 图形库、WebRTC 协议栈甚至内置了 FFmpeg 解码器。你打包的不是“应用”而是一个精简版 Chrome 浏览器Node.js 运行时你的代码。而 Tauri 的 12.3MB 构成完全不同app.exeRust 编译产物8.7 MBwebview2_loader.dllWebView2 引导程序2.1 MBtauri.conf.json配置文件及图标资源1.5 MB关键差异在于Tauri不携带任何浏览器引擎。它在 Windows 上调用系统预装的 WebView2基于 Edge ChromiummacOS 上调用 WKWebView基于 Safari WebKitLinux 上调用 WebKitGTK。这意味着你无需为每个用户重复下载 100MB 的 Chromium 二进制系统 WebView 的安全更新由操作系统自动完成无需你维护 Chromium 版本但代价是你彻底失去了对渲染引擎的控制权。当客户使用 Windows 7不支持 WebView2或某国产 Linux 发行版未预装 WebKitGTK时你的应用直接无法启动。我在某政务项目中就踩过这个坑客户内网终端强制禁用 Windows Update导致 30% 的机器 WebView2 运行时版本低于 114而我们的 Vue 3 应用用了Object.hasOwn()语法ES2022直接白屏。解决方案不是降级 Vue而是用 Tauri 的tauri::api::dialog::message在启动时检测 WebView2 版本并弹出引导用户手动安装的提示框——这在 Electron 中根本不需要因为 Chromium 版本完全可控。更隐蔽的体积陷阱在依赖管理。Electron 项目中如果你在渲染进程import { app } from electron/remote就会触发electron/remote包的完整加载2.1MB而它实际只用到了app.getPath(userData)这一个方法。Tauri 则强制你通过invoke显式声明所需能力// src-tauri/src/main.rs #[tauri::command] async fn get_user_data_dir() - ResultString, String { Ok(tauri::api::path::app_data_dir(tauri::AppHandle::default().package_info()).unwrap().to_string()) }// src/main.ts import { invoke } from tauri-apps/api/core; const userDataDir await invokestring(get_user_data_dir);这种“按需调用”模式天然规避了无用代码加载但要求你把每个系统调用都写成独立的 Rust 函数。我在迁移时统计过原 Electron 项目有 17 个electron/remote调用点迁移到 Tauri 后变成了 23 个invoke函数——多出的 6 个全是为绕过 Windows 权限沙箱而写的路径白名单校验逻辑。注意Tauri 的体积优势在 Linux 上最明显因为 WebKitGTK 是大多数发行版的默认组件但在 Windows Server 2012 R2 这类老系统上WebView2 安装包需额外 18MB且安装过程会中断静默部署流程。我们最终在tauri.conf.json中配置了windows: { webviewInstallMode: { type: downloadBootstrapper } }并用 PowerShell 脚本预检系统版本这才是生产环境的真实体积控制逻辑。3. 启动性能从 4.7 秒到 0.8 秒的冷启动链路拆解启动时间不是“从点击图标到窗口出现”的黑盒测量而是需要穿透进程生命周期的白盒分析。我用 Windows Performance RecorderWPR抓取了两个框架的完整启动轨迹关键节点对比如下单位毫秒阶段Electron 23Tauri 1.10差异根源进程创建CreateProcess128Tauri 二进制更小磁盘寻道更快PE 文件加载与重定位8923Electron 加载 120 DLLTauri 仅需 3 个系统 DLLV8 引擎初始化142—Tauri 不含 V8JS 引擎由系统 WebView 提供Chromium 渲染进程启动210—Tauri 无此阶段直接复用系统 WebView主进程 JS 执行main.js3817Electron 需解析electron.asarTauri 直接执行 Rust 二进制渲染进程 HTML 加载124118差异微小取决于网络/磁盘 IOVue 3 应用挂载完成127127前端框架层无差异总计冷启动4.72s0.81s—这个表格揭示了一个反直觉事实Electron 启动慢的主因不是 JS 执行而是原生层的模块加载与内存映射。当你看到link.exe not found报错时它并非编译期错误而是 Tauri 构建脚本在尝试调用 MSVC 链接器生成.exe时发现系统 PATH 中没有link.exe属于 Visual Studio Build Tools。这个问题在 Electron 中不存在因为它的二进制是预编译好的。但 Tauri 的快速启动是有前提的它假设系统 WebView 已就绪。我在测试鸿蒙系统HarmonyOS时发现Tauri 官方文档中“tauri-harmony”分支仍处于实验阶段其底层依赖的ohos-ndk与webkit2gtk存在 ABI 不兼容。当我们在鸿蒙设备上运行tauri build时构建成功但运行时报错Failed to initialize WebView: Not implemented。解决方案是改用tauri-apps/api/webview的WebviewWindowAPI 手动创建窗口并注入自定义的WebViewProvider——这本质上是在鸿蒙上模拟一个轻量 WebView 容器体积又增加了 2.3MB。更关键的是“热启动”场景。Electron 应用最小化后主进程仍在内存中驻留再次点击图标只是唤醒窗口而 Tauri 默认行为是关闭主进程再次启动需完整走一遍上述链路。我们在tauri.conf.json中配置了{ build: { beforeBuildCommand: pnpm run build:web, devPath: ../dist }, tauri: { allowlist: { all: false, shell: { open: true } }, windows: [{ fullscreen: false, resizable: true, title: Serial Debugger, width: 1200, height: 800, alwaysOnTop: false, visible: true, decorations: true, fileDropEnabled: true, center: true, theme: light }], systemTray: { iconPath: icons/icon.png, iconAsTemplate: true } } }重点是systemTray配置——它让应用在关闭窗口时转入托盘驻留主进程不退出。但这带来新问题托盘图标右键菜单的点击事件在 Vue 3 的onMounted钩子中无法捕获因为window对象尚未完全初始化。我们最终用tauri::api::notification::request_permission()的回调时机作为窗口就绪信号再绑定托盘菜单事件。这个细节在 Electron 中只需监听app.on(activate)即可因为它的主进程生命周期模型更简单。实操心得不要迷信 Benchmark 工具的“启动时间”数字。我曾用electron-startup-time测出 3.2s但客户现场实测是 5.8s——因为他们的杀毒软件会对 Electron 的chrome.dll进行全量扫描。Tauri 的app.exe因为是 Rust 编译的原生二进制被误报率低得多这才是它在政企环境中真正的启动优势。4. 安全重构IPC 通信契约的范式转移与 Vue 集成陷阱安全不是功能开关而是架构选择。Electron 的webPreferences配置项像一张可调节的滤网而 Tauri 的tauri.conf.json则是一份必须签署的宪法。我们来看最典型的 IPC 场景Vue 渲染进程读取串口设备列表。Electron 的旧实现危险模式// main.js const { app, BrowserWindow, ipcMain } require(electron) const { SerialPort } require(serialport) ipcMain.handle(get-ports, async () { return await SerialPort.list() // 直接暴露 Node.js API }) // renderer.js const { ipcRenderer } require(electron) const ports await ipcRenderer.invoke(get-ports) // 前端直接调用这段代码的问题在于ipcMain.handle的回调函数运行在主进程拥有全部 Node.js 权限。如果前端被 XSS 攻击攻击者可通过eval()注入恶意代码调用require(child_process).exec(rm -rf /)。即使启用了contextIsolation: true只要nodeIntegration: true渲染进程就能访问require。Tauri 的安全契约必须显式授权// src-tauri/src/main.rs use tauri::Manager; use serialport::SerialPortInfo; #[tauri::command] async fn get_serial_ports() - ResultVecSerialPortInfo, String { match serialport::available_ports() { Ok(ports) Ok(ports), Err(e) Err(e.to_string()) } } fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![get_serial_ports]) .run(tauri::generate_context!()) .expect(error while running tauri application); }// src/main.ts import { invoke } from tauri-apps/api/core; // 前端调用必须显式声明类型 const ports await invokeSerialPortInfo[](get_serial_ports);这个转变看似简单实则重构了整个安全模型权限粒度Electron 的nodeIntegration是全局开关Tauri 的invoke是函数级授权。你在tauri.conf.json中必须明确声明哪些命令允许被调用{ tauri: { allowlist: { fs: { all: false }, // 禁用全部文件系统 API shell: { open: true }, // 仅允许打开外部链接 os: { all: false }, process: { all: false } } } }类型安全Tauri 的invoke调用在 TypeScript 中是泛型的返回值类型必须与 Rust 函数签名严格匹配。Electron 的ipcRenderer.invoke返回any极易引发运行时错误。Vue 集成陷阱在 Vue 3 的setup()中直接调用invoke会导致响应式失效。正确写法是import { ref, onMounted } from vue import { invoke } from tauri-apps/api/core export default { setup() { const ports refSerialPortInfo[]([]) const loadPorts async () { try { ports.value await invokeSerialPortInfo[](get_serial_ports) } catch (e) { console.error(Failed to load ports:, e) } } onMounted(() { loadPorts() }) return { ports } } }如果你用const ports await invoke(...)写在setup()顶层Vue 会报错Cannot access ports before initialization因为invoke是异步的而ref初始化必须同步完成。更深层的安全重构在于进程模型。Electron 允许主进程直接操作 DOM通过BrowserWindow.webContents.executeJavaScript这为跨进程状态同步提供了便利但也打开了任意代码执行的大门。Tauri 彻底禁止这种操作所有 UI 更新必须通过前端框架自身的响应式系统完成。我们在迁移时发现原 Electron 项目中有一个“后台心跳检测”功能主进程每 5 秒检查一次串口连接状态若断开则调用webContents.send(port-disconnected)推送事件。Tauri 中必须改为前端定时invoke查询状态或使用tauri::api::event::listen订阅 Rust 端发布的事件// Rust 端定时检查并发布事件 use std::time::Duration; use tauri::Manager; #[tauri::command] async fn start_heartbeat(app: tauri::AppHandle) - Result(), String { tauri::async_runtime::spawn(async move { loop { tokio::time::sleep(Duration::from_secs(5)).await; let is_connected check_port_connection().await; if !is_connected { let _ app.emit_all(port-disconnected, ()).await; } } }); Ok(()) }// 前端监听事件 import { listen } from tauri-apps/api/event; onMounted(async () { await listen(port-disconnected, (event) { console.log(Port disconnected!, event.payload); }); });这个模式强制你将“状态变更”与“UI 更新”解耦虽然开发成本上升但大幅降低了因主进程异常导致 UI 错乱的风险。5. 生产落地 checklist从开发机到客户现场的 12 个必验环节框架选型只是开始真正决定项目成败的是从开发环境到客户现场的每一处适配细节。我把过去三年交付的 17 个跨平台桌面项目经验浓缩为一份可直接执行的 checklist。每个条目都对应一个真实踩过的坑附带验证命令和修复方案。Windows 环境专项MSVC 工具链完整性验证问题tauri build报错link.exe not found验证where linkCMD或Get-Command link.exePowerShell修复安装 Visual Studio Build Tools 勾选 “C build tools” 和 “Windows 10/11 SDK”WebView2 运行时版本检查问题Windows 10 1809 以下系统白屏验证reg query HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5} /v pv修复在tauri.conf.json中配置webviewInstallMode: { type: downloadBootstrapper }并用 NSIS 脚本预检注册表防病毒软件兼容性测试问题360 安全卫士拦截app.exe启动验证在干净 Win10 虚拟机中安装 360运行app.exe观察拦截日志修复向 360 提交软件认证或在tauri.conf.json中添加windows: { webviewInstallMode: { type: offlineInstaller, path: path/to/msedgewebview2setup.exe } }避免动态下载macOS 环境专项Gatekeeper 签名验证问题双击.app提示“已损坏无法打开”验证spctl --assess --type exec ./MyApp.app修复使用 Apple Developer ID 证书签名codesign --force --deep --sign Developer ID Application: XXX MyApp.appNotarization 服务提交问题App Store 外分发被拒验证xcrun altool --notarize-app --primary-bundle-id com.myapp --username xxxapple.com --password keychain:AC_PASSWORD --file MyApp.zip修复Notarization 后必须 staplerxcrun stapler staple MyApp.appLinux 环境专项WebKitGTK 版本兼容性问题Ubuntu 18.04 白屏WebKitGTK 2.36验证apt list --installed | grep webkit2gtk修复在tauri.conf.json中配置linux: { webkit2gtk: { version: 2.36 } }并提供手动安装脚本AppImage 启动器依赖问题CentOS 7 启动失败报错libstdc.so.6: version GLIBCXX_3.4.21 not found验证strings /usr/lib64/libstdc.so.6 | grep GLIBCXX修复在tauri.conf.json中设置linux: { appimage: { target: [manylinux2014] } }通用集成专项Vue 3 Composition API 响应式陷阱问题invoke返回对象在模板中不响应验证console.log(isReactive(data))修复用shallowRef包裹invoke结果或在setup()中用reactive({})初始化Electron 旧代码残留检测问题require(electron)未被 Tree Shaking 移除验证grep -r require.*electron src/修复在vite.config.ts中配置define: { process.env.ELECTRON: false }系统托盘图标 DPI 适配问题4K 屏幕托盘图标模糊验证在 200% 缩放的 Windows 上观察图标清晰度修复提供icon2x.png和icon3x.png并在tauri.conf.json中配置systemTray: { iconPath: icons/icon.png }离线环境资源加载问题内网无网络时index.html加载失败验证断网后启动应用检查 DevTools Network 面板修复在tauri.conf.json中设置build: { devPath: ./dist, distDir: ../dist }确保所有资源相对路径正确安装包卸载残留清理问题卸载后AppData/Roaming/MyApp目录残留验证安装后手动删除app.asar观察是否影响功能修复在src-tauri/src/main.rs中添加tauri::Builder::default().setup(|app| { app.handle().exit(0); })并在卸载脚本中调用最后分享一个血泪教训某次给银行客户交付时我们通过了全部 checklist但上线三天后收到大量“启动失败代码 2”投诉。抓取日志发现错误发生在tauri::api::path::app_data_dir()调用时——因为客户终端启用了 McAfee Endpoint Security其策略禁止任何进程访问AppData/Roaming目录。解决方案是改用tauri::api::path::app_config_dir()并将配置文件存放在AppData/Local下。这个细节不会出现在任何官方文档里只有在客户机房守着日志滚动时才能发现。6. 我的实际选择逻辑什么场景该用 Electron什么场景必须上 Tauri没有银弹框架只有适配场景的工具。我的决策树基于三个硬性指标交付周期、目标终端可控性、安全审计等级。下面用真实项目案例说明。选择 Electron 的 3 个典型场景场景一需要深度定制 Chromium 渲染引擎某工业视觉检测项目要求在网页中实时叠加 OpenGL 渲染的 3D 检测框。Electron 可通过app.commandLine.appendSwitch(enable-unsafe-webgpu)启用 WebGPU并用webContents.setFrameRate(60)锁定帧率。Tauri 的 WebView2 目前不支持 WebGPU且无法修改渲染管线参数。我们最终用 Electron 23 WebGPU Rust WASM 后端实现了 120FPS 的实时检测这是 Tauri 当前无法替代的。场景二客户终端环境高度不可控某教育 SaaS 产品需覆盖 Windows 7/8/10/11、macOS 10.15~14、Ubuntu 16.04~22.04。Electron 的“自带 Chromium”特性反而成了优势——我们统一用 Electron 22Chromium 108确保所有终端渲染一致性。Tauri 在 Ubuntu 16.04 上需手动编译 WebKitGTK 2.36构建时间从 2 分钟飙升至 22 分钟CI 流水线直接超时。场景三已有成熟 Electron 生态依赖某医疗影像系统集成了electron-pdf基于 Puppeteer 的 PDF 导出、electron-spellchecker原生拼写检查、electron-updater自动更新。迁移到 Tauri 需重写全部 PDF 生成逻辑改用pdf-libcanvas并放弃原生拼写检查改用 WebAssembly 版本。评估后发现改造成本 新增功能价值果断保留 Electron。选择 Tauri 的 3 个不可妥协场景场景一政企客户安全审计一票否决某省级政务平台要求通过等保三级测评其中“应用软件安全”条款明确禁止“未隔离的 Node.js 运行时”。Electron 的nodeIntegration无论怎么配置其主进程始终运行完整 Node.js审计报告直接判定为高风险。Tauri 的 Rust 主进程无 JS 运行时且invoke调用必须显式声明顺利通过审计。场景二安装包体积是硬性 KPI某 IoT 设备厂商要求桌面端配套工具安装包 ≤ 20MB因设备 SD 卡仅 64MB。Electron 最小化构建禁用音视频、GPU、打印仍达 48MB而 Tauri 同功能仅 14MB。我们甚至用strip命令进一步压缩 Rust 二进制最终交付 11.2MB 安装包。场景三需要与现有 Rust 生态无缝集成某区块链钱包项目核心密钥管理模块已用 Rust 编写并通过 FFI 暴露 C API。Electron 需通过node-ffi-napi二次封装而 Tauri 可直接在src-tauri/src/main.rs中use my_crypto_lib::*零成本复用全部 Rust 代码。迁移后密钥派生速度提升 37%且内存安全漏洞归零。我的个人体会是当项目需求文档中出现“必须通过等保测评”“安装包≤15MB”“与 Rust 后端共用加密模块”这类表述时Tauri 是唯一选择而当需求中写着“支持 IE11 兼容模式”“需在 Windows XP 上运行”“客户要求使用自家定制 Chromium”时Electron 仍是更务实的选择。技术选型不是站队而是用最合适的工具解决最痛的问题。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Skills 出世,Prompt 已死?用 TaoToken 统一 Key 为 Agent 构建可控思维 2026/9/26 16:36:55

Skills 出世,Prompt 已死?用 TaoToken 统一 Key 为 Agent 构建可控思维

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
QClaw 常用问题总结:agent 智能体高频搜索关键词大全与 TaoToken 配置指南 2026/9/26 16:36:55

QClaw 常用问题总结:agent 智能体高频搜索关键词大全与 TaoToken 配置指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
验证码识别脚本实战:OpenCV预处理+CNN训练全流程解析 2026/9/26 16:36:42

验证码识别脚本实战:OpenCV预处理+CNN训练全流程解析

简介:面向计算机相关专业学习者与机器学习初学者的实战项目,基于机器学习算法实现验证码识别,包含可直接运行测试的完整源码与说明文档,适用于课程设计、毕业设计或企业初期项目演示,具有较高的学习借鉴价值。压缩包共…

阅读更多 →
基于自适应关键帧的微表情识别算法实现与避坑指南 2026/9/26 16:36:42

基于自适应关键帧的微表情识别算法实现与避坑指南

简介:这份资源面向情感计算与计算机视觉方向的研究者、学生及开发者,提供一套基于自适应关键帧的视频微表情识别算法完整实现,用于解决微表情持续时间短、识别难度大、计算开销高等问题。压缩包共14个文件,约404KB,以6…

阅读更多 →
科研成果申报管理系统源码:从跑通到改造的完整指南 2026/9/26 16:36:42

科研成果申报管理系统源码:从跑通到改造的完整指南

简介:这份科研成果申报管理系统源码面向计算机专业学生及需要完成毕业设计的开发者,提供一套覆盖项目申报、评审管理、进度跟踪与文档管理等环节的完整Web应用实现,帮助读者理解科研管理业务的数字化流程与软件工程落地方式。压缩包共155个文…

阅读更多 →
基于Zi-Pi指标的微生物网络关键物种识别:R语言实现与社区分析指南 2026/9/26 16:36:42

基于Zi-Pi指标的微生物网络关键物种识别:R语言实现与社区分析指南

简介:面向微生物网络分析中节点模块内连通度与模块间连通度的量化需求,这份资源提供了基于R语言的完整计算方案,适用于生态学、生物信息学等领域研究者。压缩包内共2个文件,包含1个R脚本和1个graphml网络文件,脚本可直…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉