LOADING

页面加载中...

给 Web 项目套上 Tauri 外壳

给现有 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。

认识现有前端项目

接入前先确认三个问题:

  1. 前端开发服务器如何启动?
  2. 生产构建输出在哪里?
  3. 页面是否依赖特殊的 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 会依次完成以下工作:

  1. 执行 beforeDevCommand;
  2. 等待 devUrl 对应的前端服务;
  3. 编译并启动 Rust 外壳;
  4. 在桌面窗口中加载前端页面。

如果需要分别调试前端和 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"
  }
}

点击标题栏按钮没有反应

依次检查:

  1. 是否启用了 withGlobalTauri,或是否通过其他方式暴露了 Tauri API;
  2. capability 是否包含对应的 core:window:allow-* 权限;
  3. 窗口是否在 create: false 后由 Rust 正确创建;
  4. 浏览器控制台是否报告了初始化脚本错误。

前端页面空白

检查 devUrl 是否与前端实际监听地址一致。还要确认 beforeDevCommand 启动的服务没有立即退出,并且前端资源路径不是只适用于 file://。

总结

把一个已有前端项目套上 Tauri,核心只有三件事:

  1. 用 frontendDist 指向前端产物;
  2. 用 devUrl、beforeDevCommand 和 beforeBuildCommand 接通开发与构建流程;
  3. 把桌面端专属逻辑尽量放进 src-tauri,避免污染原前端项目。

自定义标题栏、窗口按钮和拖动区域属于进阶可选项。先让普通窗口稳定运行,再通过独立初始化脚本注入控件,通常是兼容性和可维护性更好的路径。