给现有 Web 项目套上 Tauri 外壳
Tauri 适合把已经可以在浏览器中运行的前端项目,打包成体积更小的桌面应用。它不会要求你把现有项目重写成某种框架:React、Vue、Svelte、原生 HTML/JavaScript,以及只要能通过 HTTP 服务运行的其他前端项目,都可以作为 Tauri 的界面层。
本文以一个已有的 Vite 前端项目为例,介绍如何在不改动原前端子模块的前提下接入 Tauri,并在最后介绍一个可选的进阶功能:隐藏系统标题栏后,通过初始化脚本注入自定义标题控件。
准备环境
以 Windows 为例,需要安装:
- Node.js
- Rust 和
rustup - Visual Studio C++ Build Tools
- WebView2
检查环境:
node --version
rustc --version
cargo --version
Tauri 2 使用系统 WebView 渲染页面。Windows 使用 WebView2,macOS 使用 WKWebView,Linux 通常使用 WebKitGTK。
认识现有前端项目
接入前先确认三个问题:
- 前端开发服务器如何启动?
- 生产构建输出在哪里?
- 页面是否依赖特殊的 HTTP 响应头?
例如,项目的前端位于 src 子目录,src/package.json 中提供了:
{
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "vite build",
"preview": "node server.mjs"
}
}
这里的 preview 不只是一个静态文件服务器,还可以负责提供 COOP、COEP 等响应头。如果项目依赖 SharedArrayBuffer、Web Worker 或 WASM,就不能随便换成一个普通静态服务器。
在项目根目录安装 Tauri CLI
如果前端项目已经是一个独立子目录,建议把 Tauri CLI 安装在仓库根目录,而不是把桌面端依赖混进前端子模块:
pnpm add -D @tauri-apps/cli
根目录的 package.json 可以提供转发脚本:
{
"scripts": {
"dev": "npm --prefix src run dev",
"build": "npm --prefix src run build",
"preview": "npm --prefix src run preview",
"tauri:dev": "tauri dev --config src-tauri/tauri.conf.json",
"tauri:build": "tauri build --config src-tauri/tauri.conf.json"
}
}
这样做有两个好处:
- Tauri 始终从仓库根目录启动;
- 原前端项目仍然使用自己的
package.json和依赖,不需要迁移到根目录。
安装依赖:
pnpm install
pnpm --dir src install
创建 Tauri 配置
Tauri 配置的核心是把前端的开发地址和构建目录告诉 Tauri:
{
"productName": "LightningTeX",
"version": "0.1.0",
"identifier": "top.example.app",
"build": {
"frontendDist": "../src/dist",
"devUrl": "http://localhost:4173",
"beforeDevCommand": "npm run preview",
"beforeBuildCommand": "npm run build"
},
"app": {
"windows": [
{
"title": "LightningTeX",
"width": 800,
"height": 600,
"resizable": true
}
]
}
}
几个字段的含义:
frontendDist:生产环境要嵌入或提供的前端目录的静态文件生成位置;devUrl:开发模式下 WebView 加载的地址;beforeDevCommand:启动 Tauri 前执行的前端服务命令;beforeBuildCommand:打包前执行的前端构建命令。
如果 beforeDevCommand 写的是 npm run preview,这个命令必须在当前工作目录可用。前端脚本位于子目录时,可以像本文前面那样,在根目录增加一个转发脚本。
frontendDist 必须指向一个完整的 Web 根目录,目录下需要直接存在 index.html,并且要包含页面引用的资源。例如,本项目的 Vite 入口是 src/ui/index.html,构建后的默认结构如下:
src/dist/
├─ ui/
│ └─ index.html
├─ assets/
├─ runtime/
├─ luahbtex/
└─ fonts/
不能简单地把 frontendDist 改成 ../src/dist/ui。虽然这样可以找到 HTML 文件,但页面中的 ../assets、../runtime 和 ../luahbtex 等相对资源路径会以 src/dist/ui 为根重新解析,无法稳定映射到完整的生产资源。
一种不改动原前端构建结构的做法,是在构建结束后把入口复制到产物根目录,并将入口中的相对路径调整为根目录路径:
src/dist/
├─ index.html
├─ assets/
├─ runtime/
├─ luahbtex/
└─ fonts/
更目录构建脚本示例:
import {readFile, writeFile} from 'node:fs/promises';
import {resolve} from 'node:path';
const dist = resolve('src', 'dist');
const source = resolve(dist, 'ui', 'index.html');
const target = resolve(dist, 'index.html');
let html = await readFile(source, 'utf8');
html = html.replaceAll('="../', '="./');
await writeFile(target, html);
根目录的构建脚本可以这样串联:
{
"scripts": {
"build": "npm --prefix src run build && node scripts/prepare-tauri-dist.mjs"
}
}
另一种方案是调整 Vite 的构建入口,让它直接输出扁平结构。这个方案不需要复制入口,但可能影响原项目的在线部署和资源路径;如果前端是独立子模块,优先使用构建后的入口准备脚本,通常更容易保持两边兼容。
启动开发模式
在仓库根目录执行:
pnpm run tauri:dev
Tauri 会依次完成以下工作:
- 执行
beforeDevCommand; - 等待
devUrl对应的前端服务; - 编译并启动 Rust 外壳;
- 在桌面窗口中加载前端页面。
如果需要分别调试前端和 Tauri,也可以手动启动:
# 终端一
pnpm --dir src run preview
# 终端二
pnpm exec tauri dev --config src-tauri/tauri.conf.json
生产构建
直接执行:
pnpm tauri build
Tauri 会先执行 beforeBuildCommand,然后生成对应平台的应用包和安装包。输出通常位于:
src-tauri/target/release/
src-tauri/target/release/bundle/
正式发布前,建议检查:
- 应用图标是否齐全;
identifier是否稳定且唯一;- 前端引用的资源是否使用相对路径;
- WASM、Worker 和字体文件是否都被包含;
- 生产环境是否仍然需要 COOP/COEP 响应头。
不要急着修改原前端子模块
将现有前端作为 Tauri 的 frontendDist 后,尽量把桌面端逻辑放在 src-tauri:
project/
├─ package.json
├─ src/ # 原前端项目
└─ src-tauri/
├─ Cargo.toml
├─ tauri.conf.json
├─ capabilities/
└─ src/
这样桌面端适配可以独立演进,减少以下风险:
- 破坏原项目的在线部署;
- 让前端子模块产生与桌面端绑定的依赖;
- 在子模块中混入平台专属代码;
- 让上游同步和后续升级变得困难。
只有当页面本身确实需要桌面端专属 UI 或 API 时,才考虑修改前端代码。否则可以优先使用 Tauri 的配置、Rust 初始化脚本和权限配置完成集成。
进阶可选项:注入自定义标题栏控件
系统标题栏通常最稳定,但如果应用需要统一的视觉风格,可以隐藏原生装饰并注入自定义标题栏。
这不是套壳的必需步骤,建议在基础窗口已经稳定运行后再做。
隐藏系统装饰
在窗口配置中关闭系统装饰:
{
"app": {
"windows": [
{
"create": false,
"decorations": false
}
]
}
}
使用 create: false 是为了让 Rust 代码可以先配置初始化脚本,再创建窗口。
使用 JavaScript 初始化脚本
创建用于注入元素的 JavaScript 脚本,例如:
src-tauri/window-controls.js
脚本可以在页面中找到原项目已有的 header.appbar 后,动态追加按钮:
const header = document.querySelector('header.appbar');
const controls = document.createElement('div');
header.setAttribute('data-tauri-drag-region', '');
controls.setAttribute('data-tauri-drag-region', '');
在 Rust 中注入脚本
由于 Tauri 2 的窗口配置并不接受任意的 initializationScript 字段,应该通过 WebviewWindowBuilder 加载:
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let window_config = &app.config().app.windows[0];
tauri::WebviewWindowBuilder::from_config(app.handle(), window_config)?
.initialization_script(include_str!("../window-controls.js"))
.build()?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while building tauri application");
}
让按钮真正可用
初始化脚本可以调用当前窗口的 API:
const currentWindow = window.__TAURI__?.window?.getCurrentWindow?.();
await currentWindow.minimize();
await currentWindow.toggleMaximize();
await currentWindow.close();
对应的 capability 权限要显式声明:
{
"permissions": [
"core:default",
"core:window:allow-close",
"core:window:allow-minimize",
"core:window:allow-start-dragging",
"core:window:allow-toggle-maximize"
]
}
实现窗口拖动
仅仅添加 data-tauri-drag-region 在某些 WebView2 场景下并不可靠。可以在标题栏空白区域调用:
header.addEventListener('mousedown', event => {
if (event.button !== 0 || event.target.closest('button')) return;
currentWindow.startDragging().catch(console.error);
});
要注意排除窗口按钮,否则用户点击关闭或最大化时可能同时触发拖动。
常见问题
Missing script: preview
说明 Tauri 执行命令的目录中没有 preview 脚本。可以在根目录转发到子项目:
{
"scripts": {
"preview": "npm --prefix src run preview"
}
}
点击标题栏按钮没有反应
依次检查:
- 是否启用了
withGlobalTauri,或是否通过其他方式暴露了 Tauri API; - capability 是否包含对应的
core:window:allow-*权限; - 窗口是否在
create: false后由 Rust 正确创建; - 浏览器控制台是否报告了初始化脚本错误。
前端页面空白
检查 devUrl 是否与前端实际监听地址一致。还要确认 beforeDevCommand 启动的服务没有立即退出,并且前端资源路径不是只适用于 file://。
总结
把一个已有前端项目套上 Tauri,核心只有三件事:
- 用
frontendDist指向前端产物; - 用
devUrl、beforeDevCommand和beforeBuildCommand接通开发与构建流程; - 把桌面端专属逻辑尽量放进
src-tauri,避免污染原前端项目。
自定义标题栏、窗口按钮和拖动区域属于进阶可选项。先让普通窗口稳定运行,再通过独立初始化脚本注入控件,通常是兼容性和可维护性更好的路径。