首页/新闻资讯/正文详情

uni-app 打包 exe:Electron/Tauri 桌面套壳实践

发布时间:2026/11/5 4:08:49 来源:云帆数科 栏目:资讯中心
uni-app 打包 exe:Electron/Tauri 桌面套壳实践
每次接到这个移动端项目能不能也给我一个 exe双击就能用的需求我脑子里都会先咯噔一下。因为如果你做过 uni-app你一定知道它的发行菜单里躺着 H5、小程序、App、鸿蒙这些选项唯独没有Windows 桌面应用。我头一回被问这个需求时第一反应是在官方 IDE 里把发行菜单翻了个遍又去 manifest.json 里逐条找配置项折腾了小半个小时才确认官方根本没提供 exe 产物。这件事的本质是uni-app 的运行时依赖的是各平台的渲染层桌面系统上它没有对应的原生渲染实现所以打包 exe从来不是一次编译的事而是先把项目编译成一份能独立跑的网页再给它套一个桌面外壳的两段式工程。这篇内容就是把这套两段式工程从头到尾讲清楚用什么壳、壳怎么配、uni-app 那边要改哪几个字段、装到用户机器上之后会冒出哪些只有桌面端才会有的问题。适合三类人看——手里有现成的 uni-app 项目要交付桌面版的开发者、想评估 Electron 和 Tauri 该选哪个的技术负责人、以及纯粹想知道网页套壳这件事水有多深的好奇者。全文会给出可直接抄的配置和代码也会把我踩过的那些坑原样摆出来。1. uni-app 没有打包 exe这个按钮我先算了一笔账1.1 产物矩阵里缺失的那一格把 uni-app 的编译目标摊开看其实很清晰一套 Vue 语法编译时按平台分发到不同的运行时。走到小程序它产出的是符合平台规范的 wxml/wxss 那一套走到 App它产出的是原生渲染桥接层加 JS 逻辑走到 H5它产出的就是最朴素的 HTML、CSS、JS 三件套加上一个 index.html 入口。这三条路径里只有 H5 那一条的产物是自带完整运行环境、丢到任何浏览器里都能跑的所以桌面端方案只能从 H5 这条线上长出来。想明白这层之后打包 exe这个动作就被拆成了两件事第一件是让 uni-app 编译出一份路径正确、路由正确、不依赖 HTTP 服务器也能打开的 H5 产物第二件是找一个桌面外壳把这堆静态文件装进去再补上窗口、菜单、文件读写、自动更新这些浏览器给不了的能力。这两件事的难度完全不对等真正让人加班的是第一件——因为默认的 H5 构建产物是给部署在 Web 服务器上这个场景设计的直接双击打开大概率白屏。1.2 三条套壳路线的横向对比能用来装网页的桌面外壳不止一种我在不同项目里分别用过下面这几种差异比想象中大方案产物体积64 位含运行时内存占用开发语言生态成熟度适合场景Electron80MB - 180MB较高单窗口常见 200MB 起纯 JS/TS非常成熟插件齐全内部工具、管理后台、需要文件与串口能力Tauri3MB - 15MB较低复用系统 WebViewRust 前端中等部分能力要自己写 Rust体积敏感、功能相对简单的工具NW.js70MB - 150MB较高纯 JS/TS一般更新节奏慢老项目迁移或需要 node 直连 DOM表格里最直观的结论是体积差了将近一个数量级。但这个数字不能单独看——Electron 之所以大是因为它把整个 Chromium 和 Node 运行时都打进了安装包好处是用户机器上跑的是什么渲染引擎由你决定不会因为系统 WebView 版本老旧而出现莫名其妙的兼容问题。Tauri 走的是相反路线它调用系统自带的 WebView 组件体积小、启动快代价是渲染行为跟着系统走老系统上的一些 CSS 特性可能不生效。我一般会这样判断如果这个 exe 要发给几十上百个不特定的用户而且他们的系统版本你控制不了选 Electron如果只是内部十几个人用、机器环境统一、又特别在意安装包大小Tauri 更合适。NW.js 我只在接手别人老项目时用过新项目基本不考虑。1.3 什么样的项目适合搬到桌面不是所有 uni-app 项目都值得往桌面端搬。搬得最顺的是这几类数据录入和查询类工具表单多、页面跳转多、没什么花哨动画、内部管理后台本身就有 H5 版本只是需要一个不依赖浏览器的入口、离线可用的资料查阅工具本地打包好数据联网时可选择性同步。这三类的共同点是交互逻辑简单、对原生能力要求不高。反过来这几类我会劝退重度依赖相机扫码、蓝牙、定位这些移动端能力的项目桌面端要么没有对应硬件要么需要重写大量原生插件对首屏加载速度要求极苛刻的项目套壳后多了一层文件协议加载冷启动肯定比浏览器慢以及需要上架应用商店的项目——套壳的 exe 走不了正规商店渠道只能自己分发安装包。我见过有团队硬把一个扫码登记 App 搬到 Windows 上最后为了接一个 USB 扫码枪写了两周的设备适配层得不偿失。2. 把 uni-app 编译成桌面壳能吃得下的 H5 产物2.1 路由模式必须先定死hash 还是 history这一条是白屏问题的头号元凶必须先解决。uni-app 的 H5 端支持两种路由模式默认是 hash地址栏带 # 号也可以配置成 history去掉 # 号看起来更像正规网站。在 Web 服务器上history 模式需要服务端配合做一次重写规则把所有路径都指向 index.html而桌面壳加载的是本地文件没有服务端一旦用户在某个子页面按了刷新或者程序内部触发了整页跳转就会去请求一个根本不存在的本地路径结果就是白屏。所以桌面端几乎必须用 hash 模式。配置位置在 manifest.json 里我通常会这么写{ h5: { title: 我的桌面应用, router: { mode: hash, base: ./ } } }mode改成hash之后所有页面路径都会挂在 # 后面浏览器以及桌面壳永远不会真的去请求子路径刷新也不会 404。base设成./是为了让资源引用走相对路径下一节细说。需要提醒一句如果你的项目里有用uni.navigateTo之外的方式做过整页跳转比如直接改window.location.href改成 hash 之后这些地方的拼接逻辑要重新核对一遍否则会出现#/pages/a#/pages/b这种畸形地址。我遇到过一次排查了半天才发现是老代码里手动拼的路由。2.2 资源路径与 basefile:// 协议下的第一道坎默认构建出来的 index.html里面的 script 和 link 标签大概率是/static/js/xxx.js这种以斜杠开头的绝对路径。这个写法在部署到域名根目录时没问题但桌面壳用 file:// 协议加载时这个斜杠会被解析成磁盘根目录实际去找的是C:/static/js/xxx.js或者D:/static/js/xxx.js当然找不到页面直接白屏。解决办法是把构建的 base 改成相对路径./。uni-app 在 vue3 vite 的版本里manifest 的h5.router.base会传递到构建配置但不同版本行为略有差异最稳的做法是同时在 vite.config.js 里显式指定一次import { defineConfig } from vite import uni from dcloudio/vite-plugin-uni export default defineConfig({ base: ./, plugins: [uni()] })配置完之后不要急着打包先做一次肉眼验证打开构建产物里的 index.html看 script 标签的 src 是不是./static/js/index-xxxx.js这种相对形式。这里有个细节要注意如果 src 变成了././static/...或者./static/...前面带上了奇怪的层级说明 base 被设置了两次把其中一处改回默认即可。静态资源目录static 目录下的图片、字体在 uni-app 里是原样拷贝的引用时写/static/xxx.png在 H5 端会被自动处理但如果你在 CSS 里用url(/static/bg.png)这种绝对写法打包后照样会失效得改成相对路径或者用/static/别名。2.3 构建产物自检的三个动作在把产物丢给桌面壳之前我固定会做三个检查这三个动作能拦掉八成以上的低级问题。第一个动作是直接双击 index.html。对就是用系统浏览器以 file:// 协议打开构建产物。如果这一步就是白屏那后面套什么壳都没用。这个测试能立刻暴露路径问题和路由问题比装完 Electron 再排查省事得多。第二个动作是看 Network 面板有没有红色请求。即便页面能显示也要打开开发者工具看看有没有 404 的资源、有没有被拦下的请求。我遇到过一次字体文件因为跨域策略被拦页面上所有图标变成方块肉眼在开发环境看不出来打包后才暴露。第三个动作是在 file:// 下走一遍主流程。登录、列表、详情、返回、刷新每一步都点一遍。这一步能提前发现 localStorage 读写异常、接口请求被拦这类问题。做完这三个动作产物基本就具备被桌面壳加载的条件了。3. Electron 壳工程从空目录到双击可运行的 exe3.1 主进程只做四件事很多人第一次写 Electron 主进程会把它写成一个大杂烩什么都往里塞。我的习惯是让主进程只负责四件事创建窗口、加载页面、处理单实例、接住来自渲染进程的 IPC 调用。剩下的业务逻辑一律留在 uni-app 那一层两边通过 preload 暴露的窄接口通信。一个够用的主进程长这样const { app, BrowserWindow, shell } require(electron) const path require(node:path) let mainWindow null function createWindow() { mainWindow new BrowserWindow({ width: 1280, height: 800, minWidth: 1024, minHeight: 640, show: false, autoHideMenuBar: true, webPreferences: { preload: path.join(__dirname, preload.js), contextIsolation: true, nodeIntegration: false } }) mainWindow.loadFile(path.join(__dirname, dist, index.html)) mainWindow.once(ready-to-show, () { mainWindow.show() }) mainWindow.webContents.setWindowOpenHandler(({ url }) { shell.openExternal(url) return { action: deny } }) } const gotLock app.requestSingleInstanceLock() if (!gotLock) { app.quit() } else { app.on(second-instance, () { if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore() mainWindow.focus() } }) app.whenReady().then(createWindow) } app.on(window-all-closed, () { if (process.platform ! darwin) app.quit() })show: false配合ready-to-show是我一定要加的不加的话用户双击图标后会先看到一个白框几百毫秒后内容才刷出来观感很差。单实例锁也是必须的否则用户手快点两次图标就会开出两个窗口两个窗口同时写同一个本地数据库数据分分钟错乱。setWindowOpenHandler那句是防止页面里的外链在应用内部打开把用户困在一个没有地址栏的窗口里出不来。3.2 安全选项不能随手一关了之网上很多教程为了省事直接教人把nodeIntegration设成 true、contextIsolation设成 false这样渲染进程里能直接用 require 和 fs写起来确实爽。但这个口子一开页面里任何一段第三方脚本都能直接读写用户磁盘风险不小。我的做法是保持contextIsolation: true、nodeIntegration: false需要的原生能力通过 preload 显式暴露成几个函数。preload.js 大致这样const { contextBridge, ipcRenderer } require(electron) contextBridge.exposeInMainWorld(desktop, { saveFile: (payload) ipcRenderer.invoke(file:save, payload), readFile: (payload) ipcRenderer.invoke(file:read, payload), getVersion: () ipcRenderer.invoke(app:version) })主进程那边用ipcMain.handle(file:save, ...)接住真正干活的是主进程。这样一来渲染层能调用的能力被收敛成三个函数页面上跑什么脚本都翻不出花来。uni-app 那边调用就是window.desktop.saveFile(...)记得加个存在性判断方便同一份代码在 H5 环境里也能跑。3.3 electron-builder 的配置重点与产物形态打安装包我用的是 electron-builder配置文件单独放一个 electron-builder.json{ appId: com.example.desktopapp, productName: MyDesktopApp, directories: { output: release, buildResources: build }, files: [ main.js, preload.js, dist/**/* ], asar: true, win: { target: [ { target: nsis, arch: [x64] }, { target: portable, arch: [x64] } ], icon: build/icon.ico }, nsis: { oneClick: false, allowToChangeInstallationDirectory: true, createDesktopShortcut: true, shortcutName: MyDesktopApp } }几个配置项值得单独说。files里我只写必要的三条把dist整个包进去其余源码、测试、文档一律不进包这是控制体积的第一道闸。asar: true会把散落的文件压成一个归档既减小体积又能在一定程度上防止用户直接翻看源码——注意只是一定程度上asar 是可以解开的真有敏感逻辑还是得放服务端。target我一般同时出两个nsis是安装版用户向导式安装能创建桌面快捷方式和开始菜单项适合正式分发portable是免安装版单个 exe 文件拷到 U 盘就能跑适合内网环境或者给临时用户试用。两个产物都跑一遍会多花几分钟但对交付体验的提升很值。3.4 图标、中文名与杀毒软件误报图标这件事有两个坑。第一个是 ico 文件必须是多尺寸的常见做法是包含 16、32、48、256 四个尺寸只放一张 256 的图任务栏上会显示成模糊的一坨。第二个是缓存Windows 会缓存图标改了 ico 后重新打包装出来的还是旧图标得多改一次文件名或者重启资源管理器才生效。中文名主要涉及productName。设成中文之后安装目录、开始菜单项、任务管理器里显示的都是中文用户看着舒服。但要注意appId千万别用中文那是个反向域名格式的标识符会影响升级识别。杀毒软件误报是 Electron 打包绕不开的话题。没做代码签名的小众安装包很容易被判定为可疑程序。我的处理办法是先在可靠的代码签名服务上买一个小品牌的签名证书成本不高签过名的包误报率会显著下降同时把安装包上传到几个主流的在线查杀平台跑一遍确认没有真实问题后把查杀报告一起交给用户。如果实在不能签名至少要在交付说明里提前告知用户可能出现安全提示请选择信任避免用户第一次安装就被吓退。4. 装进桌面之后才冒出来的四类问题4.1 刷新白屏与路由 404 的根因前面说了 hash 模式能解决大部分路由问题但还有一种情况会漏网。uni-app 里如果用了自定义的 tabBar 或者某些第三方 UI 组件它内部可能直接操作 history API在 file:// 环境下这些调用会导致地址变成file:///C:/xxx/dist/index.html/undefined之类的畸形地址然后整页崩溃。判断方法很简单出问题时看一眼窗口标题栏或开发者工具里的 location如果路径明显不对基本就是了。另一种解法是彻底摆脱 file:// 协议在主进程里注册一个自定义协议把本地目录映射成一个虚拟的域名。Electron 较新版本可以这样做const { app, protocol, net } require(electron) const path require(node:path) const { pathToFileURL } require(node:url) protocol.registerSchemesAsPrivileged([ { scheme: app, privileges: { standard: true, secure: true, supportFetchAPI: true } } ]) app.whenReady().then(() { const root path.join(__dirname, dist) protocol.handle(app, (request) { const { pathname } new URL(request.url) const filePath path.join(root, decodeURIComponent(pathname / ? /index.html : pathname)) return net.fetch(pathToFileURL(filePath).toString()) }) })然后在窗口里loadURL(app://local/index.html)。这么一改页面里的一切都跑在一个正规的 origin 下history 模式能用、fetch 能用、localStorage 按域名隔离前面提到的很多问题会一起消失。代价是路径映射逻辑要自己写对并且要在打包后验证所有静态资源都能正确返回。我个人现在更倾向直接用这套方案前期多花半小时后期省无数麻烦。4.2 localStorage 数据丢失的真相用 file:// 加载页面时localStorage 是按文件路径做隔离的。这意味着如果你的安装目录发生变化或者你从便携版换成安装版之前存的数据就消失了——其实数据还在磁盘上只是存在另一个路径对应的存储区里页面读不到。用户不知道这个原理只会觉得我的配置怎么没了。如果走的是自定义协议那套方案localStorage 就按域名隔离跟安装路径无关这个问题自然解决。如果坚持用 file://那就要接受数据存储位置跟随安装路径这个事实并且尽量把重要数据放到主进程管理的独立存储里通过 IPC 读写。主进程可以用app.getPath(userData)拿到一个跟用户账号绑定的固定目录把 JSON 文件写在那儿升级、换安装目录都不影响。另外一个细节是清理逻辑。用户卸载软件时安装目录会被删掉但 userData 目录默认保留下次重装还能接着用。这多数时候是好事但如果业务上要求卸载即清空就得在 NSIS 安装脚本里加一段删除逻辑或者提供一个清空本地数据的入口让用户自己点。4.3 file:// 下的跨域origin 变成 null 之后这是最容易让人抓狂的一类问题。浏览器环境里页面会有个明确的 origin接口那边配上跨域头就能通。到了 file:// 下origin 变成了 null很多服务端的跨域配置会直接拒绝这类请求。表现就是接口全部失败而且报错信息还特别含糊。我的解决方案是让接口请求绕开渲染层。具体做法是在主进程里用 Node 的请求能力发请求再通过 IPC 把结果返回给页面这样请求根本不经过浏览器的同源策略。preload 里暴露一个request方法主进程里用ipcMain.handle接住转发出去再返回。这个改动量比想象中小如果项目里接口请求集中封装在一个 request 工具文件里只需要在那个文件里做一次环境判断——检测到window.desktop存在就走 IPC否则走原来的 uni.request。顺带一提这么改还有个额外好处请求的鉴权头、超时、重试都能在主进程统一处理页面那边不用关心。坏处是进度回调、流式响应这类场景要多写一些中转逻辑如果项目里有大文件上传需要额外设计分片和进度上报。4.4 导出、打印、下载这些浏览器能力全部失效uni-app 的 H5 端导出 Excel、下载文件、调用打印底层依赖的都是浏览器的下载和window.print()。桌面壳里下载行为可能被静默拦掉打印可能弹不出对话框用户点了按钮毫无反应连报错都没有。正确的做法是把这些能力全部挪到主进程。导出文件时页面把数据传给主进程主进程弹出一个保存对话框拿到用户选的路径后用 fs 写盘const { ipcMain, dialog } require(electron) const fs require(node:fs) ipcMain.handle(file:save, async (event, { defaultName, content }) { const { canceled, filePath } await dialog.showSaveDialog({ defaultPath: defaultName, filters: [{ name: Excel 文件, extensions: [xlsx] }] }) if (canceled || !filePath) return { ok: false } fs.writeFileSync(filePath, Buffer.from(content)) return { ok: true, filePath } })打印的话Electron 提供了webContents.print()和webContents.printToPDF()前者直接走系统打印后者生成 PDF 再让用户另存。我更推荐后者因为用户对导出 PDF 再打印这个流程更熟悉排版也可控。这里有个经验改造这部分时先在页面里把原来的下载逻辑用环境判断包起来桌面端走 IPCH5 端保持原样。这样一份代码两端都能跑测试成本最低。别图省事直接全改成 IPC否则以后 H5 版就没法用了。5. Tauri 这条路的实测体积党可以上但要接受三个前提5.1 它靠什么把包压到十几兆Tauri 的思路跟 Electron 完全相反。它不打包浏览器内核而是调用操作系统自带的 WebView 组件后端逻辑用 Rust 写编译成原生二进制。所以它的安装包里几乎没有运行时只有你的前端产物加上一层薄薄的壳体积自然就小了一个数量级。我用同样的 uni-app 产物分别套壳做过对比Electron 装完占 200MB 左右Tauri 装完不到 20MB差距非常直观。内存占用也更友好。Electron 每个窗口都要养一个 Chromium 进程开几个窗口内存就上去了Tauri 复用系统组件多窗口场景下优势明显。启动速度也快一些冷启动大概能省一半时间。5.2 接入 uni-app 产物的配置要点接入流程本身不复杂前提是把 Rust 工具链装好这一步在 Windows 上要装构建工具链第一次配置花的时间比写代码多。项目初始化之后核心配置文件长这样{ productName: my-desktop-app, version: 1.0.0, identifier: com.example.desktopapp, build: { frontendDist: ../dist }, app: { windows: [ { title: 我的桌面应用, width: 1280, height: 800, resizable: true } ], security: { csp: null } }, bundle: { active: true, targets: [nsis], icon: [icons/icon.ico] } }关键就一行build.frontendDist指向 uni-app 的 H5 产物目录。之后的流程跟 Electron 类似——先把 uni-app 编译成 H5再执行打包命令。注意frontendDist是相对于 Tauri 工程目录的路径写错了会报找不到入口文件这个报错信息还算清晰容易定位。图标有格式要求需要准备一张正方形的 PNG 源图然后用命令行工具生成各尺寸的 ico。这一步别偷懒用现成的 ico 直接改后缀生成的图标在部分系统上会显示异常。5.3 三个必须接受的前提第一个前提是系统组件的版本不可控。Tauri 用的是系统自带的 WebView 组件不同系统版本上的渲染行为可能有细微差别。我遇到过一次某个 CSS 的毛玻璃效果在开发机上正常在用户的老系统上完全失效变成一块纯色。这类问题排查起来很费劲因为你的开发环境和用户环境不一致。规避办法是在项目里避免使用太新的 CSS 特性或者在启动时检测版本给出提示。第二个前提是原生能力要写 Rust。Electron 那边缺什么能力装个 npm 包基本就解决了Tauri 这边如果官方插件没覆盖到就得自己写 Rust 命令然后在前端调用。对于不熟悉 Rust 的团队这是一道实打实的门槛。我建议在选型阶段就把需要的原生能力列个清单逐个确认官方是否有现成插件别做到一半才发现要现学一门语言。第三个前提是生态和排错资料相对少。踩到坑的时候能搜到的解决方案数量明显少于 Electron。这就要求团队里有能啃文档、能读源码的人否则一个小问题可能卡好几天。综合下来我的建议是功能简单、体积敏感、团队有 Rust 基础的选 Tauri其余情况别折腾Electron 的成熟度值那 100 多兆的体积。6. 体积、首屏与后续升级最后三处值得花时间的调优6.1 拆开看体积构成砍掉不该进包的东西安装包体积上来之后别急着怪框架先拆开看看里面装了什么。我的习惯是把产物解压逐层看目录占用。最常见的浪费有这几处node_modules里混进了只在开发时用的包、sourcemap 文件被打进了包、图片资源没有压缩、dist里残留了上一次构建的旧文件。对应的处理动作很直接。构建前先清空输出目录避免新旧文件混在一起打包配置里用files白名单而不是黑名单只放必要的路径把 sourcemap 关掉或者单独产出到别的目录图片统一走一遍压缩流程尤其是那些从设计稿直接导出的 PNG转成 WebP 能省一大半。我做过一个项目光是把几张背景图换成压缩后的格式安装包就小了 30 多兆。还有一个容易被忽略的点uni-app 的 H5 产物默认会把所有页面打进一个主包页面多了之后单文件会非常大首屏要等整个包下载完才能渲染。开启分包之后首屏只加载主包的代码其余页面按需加载冷启动时间能明显下降。这个配置在 pages.json 里做改完之后记得在桌面端实测一遍页面跳转分包加载在本地文件协议下偶尔会有路径解析问题。6.2 首屏白屏的三层遮盖桌面应用的用户对启动速度的容忍度比网页低得多。网页转圈两秒大家习惯了桌面应用双击后黑屏两秒用户会怀疑程序没启动然后再点一次。所以首屏体验必须专门做。我的做法是三层遮盖。第一层是窗口本身的show: false等ready-to-show再显示避免出现白框。第二层是加一个极简的启动页纯 HTML 加一个 logo 和进度条主窗口先加载这个页面等真正的内容准备好之后再切换过去。第三层是在 uni-app 内部做骨架屏页面数据没回来之前先渲染占位结构让用户看到东西正在出来。这三层不需要都做但至少要做第一层和第三层。第二层的启动页我通常只在冷启动确实超过三秒的项目里加否则属于过度设计。实测下来做好第 1 层和第 3 层用户对启动速度的抱怨能减少一大半。6.3 版本升级怎么做才不折腾用户桌面应用最麻烦的环节之一就是升级。用户装完之后你发新版本怎么让他拿到内网环境下这个问题尤其突出。如果条件允许接自动更新是最省心的。Electron 生态里有成熟的更新方案配置好更新服务器地址和版本号应用启动时静默检查有新版本就在后台下载下载完提示用户重启生效。要注意的是自动更新必须做代码签名否则部分系统会直接拒绝执行更新包另外更新服务器的地址不能写死在代码里得留个配置项方便测试环境和生产环境切换。内网环境往往没有更新服务器那就退一步做检查更新 手动安装。应用启动时请求一个固定的版本文件地址内网放个静态文件就行对比版本号发现新版本就弹窗提示附带下载地址或者直接打开共享目录。这种方式不优雅但足够可靠用户点两下就能装上。还有一个兜底方案是替换 asar。把新版本的 asar 文件直接覆盖到安装目录重启即生效。这招适合紧急修复不适合常规升级因为用户不会操作而且容易覆盖出问题。我一般只在给少数几个关键用户做紧急修复时用。另外补充一个细节无论用哪种升级方式都要处理好本地数据的兼容。新版本如果改了存储结构升级后要能读取旧数据并自动迁移否则用户升级完发现数据全乱了比不升级还糟。我现在的习惯是每次涉及存储结构变更都在数据里加一个版本号字段启动时先读版本号再决定走哪套迁移逻辑。个人在实际操作中的体会是uni-app 打包桌面端这件事真正的技术难点从来不在怎么打出一个 exe而在打出来之后能不能用得顺。第一次做的时候我把八成时间花在了壳的搭建上结果装到用户机器上之后路由、存储、跨域、文件读写连着炸了一周。后来再做类似项目我会把顺序倒过来先花时间把 H5 产物在 file:// 下调试到完全跑通再考虑套壳最后把文件、打印、更新这些桌面独有的能力一次性补齐。这个顺序能省掉大量反复打包验证的时间。还有一个习惯是给桌面端单独留一个环境判断的文件所有跟原生能力相关的调用都从这里出将来换壳或者同时维护 Electron 和 Tauri 两个版本时改动面会小很多。

相关推荐

根共识方法:分布式系统中的涌现性与分层验证机制
根共识方法:分布式系统中的涌现性与分层验证机制

1. 项目背景与核心概念解析"整体大于孤立的部分之和"这一哲学命题最早可以追溯到亚里士多德的形而上学思想。在当代系统科学中,这一理念被形式化为"涌现性"(Emergence)概念——当系统各部分以特定方式组织起来时,会产生各部分孤立存… · 2026/11/5 12:00:48

AMD Ryzen AI MAX 395与Windows版ROCm实战指南
AMD Ryzen AI MAX 395与Windows版ROCm实战指南

1. AMD AI MAX 395 不是显卡型号,而是Ryzen AI引擎的代号级命名体系 很多人第一次看到“AMD AI MAX 395”这个名称时,第一反应是——这是一张新出的独立显卡?类似NVIDIA RTX 4090那种命名逻辑?我最初也这么以为,直到在… · 2026/11/5 8:50:13

AI语音‘活人感’缺失的根源:生物节律与对话节奏
AI语音‘活人感’缺失的根源:生物节律与对话节奏

1. “活人感”不是音色问题,而是对话节奏的生物学错觉你有没有过这种体验:刚听完一段AI语音对话,明明每个字都清晰标准,但心里却莫名觉得“这人不太对劲”?不是语调太平,也不是发音不准——恰恰相反&#x… · 2026/11/5 1:31:19

EasyU优启通PE维护盘实战指南:制作、启动与核心操作全解析
EasyU优启通PE维护盘实战指南:制作、启动与核心操作全解析

1. 为什么一个PE维护盘值得单独拿出来聊装系统、修引导、救数据、清病毒,这些活儿但凡干过几次的人都知道,真正决定效率的往往不是你手速多快,而是手边那套PE环境靠不靠谱。EasyU(圈内习惯叫优启通)就是在这个场景下被… · 2026/11/5 16:49:35

Rust原来很简单二:所有权系统
Rust原来很简单二:所有权系统

Part 2 核心篇 —— 所有权系统 本篇覆盖 第 6 ~ 10 章:所有权、借用与引用、生命周期、字符串与切片、内存模型。 ⚠️ 这是全书最重要的一篇。 把它吃透,Rust 从此“很简单”;跳过它,你会处处碰壁。请慢下来,动手敲每一段代码。 第 6 章 所有权:Rust 的第一性原理 6… · 2026/11/5 16:26:08

基于Hadoop+Spark的民宿智能推荐系统设计与实现
基于Hadoop+Spark的民宿智能推荐系统设计与实现

1. 项目概述:大数据时代的民宿智能推荐这个毕业设计项目融合了当前大数据领域最主流的技术栈,构建了一个完整的民宿推荐系统解决方案。作为一名长期从事大数据系统开发的工程师,我认为这个选题非常贴合实际应用场景——随着旅游业的复苏和共享… · 2026/11/5 14:59:25

ADS122U04IPWR+STM32F427ZI工业级温度测量信号链设计
ADS122U04IPWR+STM32F427ZI工业级温度测量信号链设计

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

UART接口24位ADC搭配RS485,解决远距离温度监控难题
UART接口24位ADC搭配RS485,解决远距离温度监控难题

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

UART接口24位ADC搭配PIC18F46K22的RTD与热电偶测温实践
UART接口24位ADC搭配PIC18F46K22的RTD与热电偶测温实践

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

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

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

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

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

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

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

多智能体协作系统架构设计与实践优化
多智能体协作系统架构设计与实践优化

1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39

大模型系统性入门实战指南:从原理到部署微调的全链路路线
大模型系统性入门实战指南:从原理到部署微调的全链路路线

提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39

鸿蒙文件系统沙箱机制解析与开发实践
鸿蒙文件系统沙箱机制解析与开发实践

1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

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

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

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

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

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

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码