文章
问答
冒泡
Win11 下使用 Rust + Tauri 打包桌面 EXE 操作手册

Win11 下使用 Rust + Tauri 打包桌面 EXE 操作手册

1. 适用场景

本文档适用于将一个本地 HTML 页面改造成 Windows 11 桌面应用,并最终打包成 .exe 文件。

适合以下场景:

1. 企业内部工具封装为桌面程序
2. 本地 Web 页面打包成 EXE
3. 需要调用本机或局域网后端接口
4. 需要绕过浏览器 CORS 限制
5. 不想使用 Electron,希望安装包更轻量

推荐技术栈:

Tauri v2
Rust
HTML / CSS / JavaScript
pnpm
Windows 11

2. 最终项目结构

推荐将前端静态资源和 Tauri 工程隔离:

algorithm-engine-console/
├─ package.json
├─ web/
│  ├─ index.html
│  └─ src/
│     ├─ main.js
│     └─ styles.css
└─ src-tauri/
   ├─ Cargo.toml
   ├─ Cargo.lock
   ├─ tauri.conf.json
   ├─ build.rs
   ├─ icons/
   │  └─ icon.ico
   └─ src/
      └─ main.rs

注意:

web/ 目录必须保持干净,只放前端静态资源。

不要让 frontendDist 指向项目根目录,否则会把 node_modules、src-tauri、target 等目录一起打包,导致构建失败。

3. 环境准备

3.1 安装 Node.js

建议安装:

Node.js 22 LTS
pnpm 最新版

检查版本:

node -v
npm -v
pnpm -v

如果没有安装 pnpm:

npm install -g pnpm

3.2 安装 Rust

推荐使用 winget 安装:

winget install --id Rustlang.Rustup

安装完成后重新打开 PowerShell。

检查:

rustc --version
cargo --version
rustup --version

3.3 安装 Visual Studio C++ Build Tools

Rust 在 Windows 下需要 MSVC 工具链。

安装时勾选:

Desktop development with C++

必须包含:

MSVC
Windows SDK
CMake

3.4 安装 WebView2

Windows 11 通常已内置 WebView2。

如果没有,需要安装:

Microsoft Edge WebView2 Evergreen Runtime

4. Cargo 国内镜像配置

如果 Cargo 下载依赖很慢,或者出现类似错误:

repository 'https://rsproxy.cn/crates.io-index/' not found

可以配置 sparse 镜像。

打开配置文件:

notepad $env:USERPROFILE\.cargo\config.toml

推荐内容:

[source.crates-io]
replace-with = "rsproxy-sparse"

[source.rsproxy-sparse]
registry = "sparse+https://rsproxy.cn/index/"

[net]
git-fetch-with-cli = true

清理旧缓存:

Remove-Item -Recurse -Force $env:USERPROFILE\.cargo\registry\index -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force $env:USERPROFILE\.cargo\registry\cache -ErrorAction SilentlyContinue

5. 创建项目

cd D:\my-work\frontend-work

mkdir algorithm-engine-console
cd algorithm-engine-console

pnpm init

安装依赖:

pnpm add @tauri-apps/api@2.11.1
pnpm add -D @tauri-apps/cli@2.11.1

创建目录:

mkdir web
mkdir web\src
mkdir src-tauri
mkdir src-tauri\src
mkdir src-tauri\icons

6. package.json 配置

{
  "name": "algorithm-engine-console",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "tauri": "tauri",
    "dev": "tauri dev",
    "build": "tauri build"
  },
  "devDependencies": {
    "@tauri-apps/cli": "2.11.1"
  },
  "dependencies": {
    "@tauri-apps/api": "2.11.1"
  }
}

7. Rust 配置

文件路径:

src-tauri/Cargo.toml

推荐配置:

[package]
name = "AlgorithmEngine"
version = "1.0.0"
description = "Algorithm Engine Console"
authors = ["your-name"]
edition = "2024"

[build-dependencies]
tauri-build = { version = "2.5.1", features = [] }

[dependencies]
tauri = { version = "2.11.1", features = [] }
serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["blocking", "json"] }

说明:

name 会影响最终生成的 exe 名称。

例如:
name = "AlgorithmEngine"

最终生成:
AlgorithmEngine.exe

注意:

Cargo package name 不建议使用中文或空格。

8. Tauri 配置

文件路径:

src-tauri/tauri.conf.json

推荐配置:

{
  "$schema": "https://schema.tauri.app/config/2",
  "productName": "Algorithm Engine Console",
  "version": "1.0.0",
  "identifier": "com.local.algorithm.engine.console",
  "build": {
    "frontendDist": "../web",
    "beforeDevCommand": "",
    "beforeBuildCommand": ""
  },
  "app": {
    "withGlobalTauri": true,
    "windows": [
      {
        "title": "Algorithm Engine Console",
        "width": 1016,
        "height": 818,
        "minWidth": 920,
        "minHeight": 760,
        "resizable": true,
        "fullscreen": false,
        "center": true,
        "devtools": false
      }
    ],
    "security": {
      "csp": null
    }
  },
  "bundle": {
    "active": false,
    "icon": [
      "icons/icon.ico"
    ]
  }
}

关键说明:

frontendDist = "../web"
表示只打包 web 目录。

withGlobalTauri = true
允许普通 HTML/JS 使用 window.__TAURI__ 调用 Rust 命令。

bundle.active = false
表示只生成 exe,不生成安装包,避免 NSIS 下载超时。

9. build.rs

文件路径:

src-tauri/build.rs

内容:

fn main() {
    tauri_build::build()
}

10. Rust 主程序

文件路径:

src-tauri/src/main.rs

示例代码:

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use serde::Serialize;
use std::time::Duration;

#[derive(Serialize)]
struct ApiResponse {
    status: u16,
    body: String,
}

#[tauri::command]
fn request_backend_api(url: String, token: String) -> Result<ApiResponse, String> {
    let client = reqwest::blocking::Client::builder()
        .timeout(Duration::from_secs(60))
        .build()
        .map_err(|e| format!("创建 HTTP Client 失败: {}", e))?;

    let response = client
        .post(url)
        .header("Authorization", token)
        .header("Accept", "*/*")
        .header("Content-Type", "application/json")
        .body("{}")
        .send()
        .map_err(|e| format!("请求后端接口失败: {}", e))?;

    let status = response.status().as_u16();

    let body = response
        .text()
        .map_err(|e| format!("读取响应内容失败: {}", e))?;

    Ok(ApiResponse { status, body })
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![request_backend_api])
        .run(tauri::generate_context!())
        .expect("启动桌面应用失败");
}

重要说明:

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

作用:

打包 release exe 后,双击不会弹出黑色控制台窗口。

如果不加这行,双击 exe 可能会同时弹出 CMD 黑窗口。

11. 前端页面

文件路径:

web/index.html

示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>算法引擎控制台</title>
    <link rel="stylesheet" href="src/styles.css" />
</head>
<body>
<div class="glow glow-blue"></div>
<div class="glow glow-purple"></div>

<main class="panel">
    <div class="top-line"></div>

    <header class="header">
        <div>
            <div class="badge">Direct Mode</div>
            <h1>算法引擎控制台</h1>
        </div>
        <div class="status-box">
            <span id="statusDot" class="dot success"></span>
            <span id="statusText">READY</span>
        </div>
    </header>

    <section class="config-card">
        <div class="config-head">
            <div>
                <label class="config-label" for="serverHost">后端服务地址</label>
            </div>

            <span class="config-badge">HOST</span>
        </div>

        <div class="config-row">
            <div class="input-wrap">
                <span class="input-prefix">URL</span>
                <input
                    id="serverHost"
                    class="server-input"
                    type="text"
                    value="http://localhost:8080"
                    placeholder="http://localhost:8080"
                />
            </div>

            <button id="saveHostBtn" class="secondary-btn">
                保存
            </button>
        </div>

        <p class="config-tip">
            示例:http://localhost:8080、http://127.0.0.1:8080、http://192.168.1.10:8080
        </p>
    </section>

    <section class="card">
        <div class="request-info">
            <p class="card-title">接口请求</p>
            <p id="requestUrlText" class="card-url">
                POST http://localhost:8080/your-api-path
            </p>
        </div>

        <button id="refreshBtn" class="refresh-btn">
            <span id="btnIcon">⟳</span>
            <span>执行请求</span>
        </button>
    </section>

    <section>
        <div class="response-head">
            <span>Response</span>
            <span id="responseTime">-- ms</span>
        </div>

        <pre id="jsonOutput">点击按钮直接请求后端接口...</pre>
    </section>
</main>

<script src="src/main.js"></script>
</body>
</html>

注意:

以下 ID 会被 JS 使用,不要随意修改:

serverHost
saveHostBtn
requestUrlText
refreshBtn
btnIcon
statusDot
statusText
responseTime
jsonOutput

12. 前端逻辑

文件路径:

web/src/main.js

示例代码:

const API_PATH = "/your-api-path";
const STORAGE_KEY_SERVER_HOST = "desktop_app_server_host";
const DEFAULT_SERVER_HOST = "http://localhost:8080";

// 不要在代码中写死真实 token,生产环境建议改为输入框、配置文件或登录接口获取
const TOKEN = "YOUR_TOKEN_HERE";

function getInvoke() {
    if (!window.__TAURI__ || !window.__TAURI__.core || !window.__TAURI__.core.invoke) {
        throw new Error("Tauri API 未注入,请检查 tauri.conf.json 是否配置 withGlobalTauri: true");
    }

    return window.__TAURI__.core.invoke;
}

function normalizeHost(host) {
    let value = (host || "").trim();

    if (!value) {
        return DEFAULT_SERVER_HOST;
    }

    value = value.replace(/\/+$/, "");

    if (!/^https?:\/\//i.test(value)) {
        value = `http://${value}`;
    }

    return value;
}

function getServerHost() {
    const input = document.getElementById("serverHost");
    const inputValue = input ? input.value : "";

    return normalizeHost(inputValue);
}

function buildTargetUrl() {
    return `${getServerHost()}${API_PATH}`;
}

function refreshRequestUrlText() {
    const requestUrlText = document.getElementById("requestUrlText");

    if (requestUrlText) {
        requestUrlText.innerText = `POST ${buildTargetUrl()}`;
    }
}

function saveServerHost() {
    const input = document.getElementById("serverHost");
    const host = normalizeHost(input.value);

    input.value = host;
    localStorage.setItem(STORAGE_KEY_SERVER_HOST, host);

    refreshRequestUrlText();
    setOutput("", `后端服务地址已保存:\n${host}\n\n点击按钮开始请求。`);
    setStatus("success", "READY");
}

function loadServerHost() {
    const input = document.getElementById("serverHost");
    const savedHost = localStorage.getItem(STORAGE_KEY_SERVER_HOST);

    input.value = normalizeHost(savedHost || DEFAULT_SERVER_HOST);
    refreshRequestUrlText();
}

function setStatus(type, text) {
    const dot = document.getElementById("statusDot");
    const statusText = document.getElementById("statusText");

    dot.className = `dot ${type}`;
    statusText.innerText = text;
}

function setOutput(type, text) {
    const output = document.getElementById("jsonOutput");
    output.className = type ? `text-${type}` : "";
    output.innerText = text;
}

function formatResponse(text) {
    try {
        return JSON.stringify(JSON.parse(text), null, 4);
    } catch {
        return text;
    }
}

async function triggerRefresh() {
    const btn = document.getElementById("refreshBtn");
    const btnIcon = document.getElementById("btnIcon");
    const responseTime = document.getElementById("responseTime");

    const targetUrl = buildTargetUrl();
    refreshRequestUrlText();

    btn.disabled = true;
    btnIcon.classList.add("spin");
    setStatus("warning", "REQUESTING...");

    const startTime = performance.now();

    try {
        const invoke = getInvoke();

        const result = await invoke("request_backend_api", {
            url: targetUrl,
            token: TOKEN
        });

        const endTime = performance.now();
        responseTime.innerText = `${Math.round(endTime - startTime)} ms`;

        const pretty = formatResponse(result.body);

        if (result.status >= 200 && result.status < 300) {
            setOutput("success", pretty);
            setStatus("success", "SUCCESS");
        } else {
            setOutput("warning", `HTTP ${result.status}\n${pretty}`);
            setStatus(
                "warning",
                result.status === 401 || result.status === 403 ? "AUTH FAILED" : "ERROR"
            );
        }
    } catch (error) {
        const endTime = performance.now();
        responseTime.innerText = `${Math.round(endTime - startTime)} ms`;

        setOutput(
            "error",
            `[请求失败] ${error}\n\n当前请求地址:\n${targetUrl}\n\n请求方式:POST\n\n排查顺序:\n1. 后端服务地址是否正确。\n2. 后端服务是否已启动。\n3. 接口路径是否正确。\n4. Rust 注册命令名是否和 JS invoke 命令名一致。\n5. Token 是否有效。\n6. 防火墙是否拦截本机请求。`
        );

        setStatus("error", "FAILED");
    } finally {
        btn.disabled = false;
        btnIcon.classList.remove("spin");
    }
}

window.addEventListener("DOMContentLoaded", () => {
    const refreshBtn = document.getElementById("refreshBtn");
    const saveHostBtn = document.getElementById("saveHostBtn");
    const serverHostInput = document.getElementById("serverHost");

    loadServerHost();

    refreshBtn.addEventListener("click", triggerRefresh);
    saveHostBtn.addEventListener("click", saveServerHost);

    serverHostInput.addEventListener("input", refreshRequestUrlText);

    serverHostInput.addEventListener("keydown", (event) => {
        if (event.key === "Enter") {
            saveServerHost();
        }
    });
});

安全建议:

不要把真实 token、账号、密码、密钥写死在 main.js 中。

建议方案:
1. 页面增加 token 输入框
2. 使用登录接口动态获取 token
3. 使用本地配置文件读取
4. 使用 Tauri secure storage 或后端代理鉴权

13. 样式文件

文件路径:

web/src/styles.css

基础样式示例:

* {
    box-sizing: border-box;
}

body {
    margin: 0;
    width: 100vw;
    height: 100vh;
    min-height: 100vh;
    background: #0b0f19;
    color: #f8fafc;
    font-family: "Microsoft YaHei", system-ui, sans-serif;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}

@keyframes pulse-glow {
    0%, 100% {
        opacity: 0.6;
        filter: blur(120px);
    }
    50% {
        opacity: 1;
        filter: blur(140px);
    }
}

.glow {
    position: absolute;
    width: 380px;
    height: 380px;
    border-radius: 999px;
    animation: pulse-glow 4s infinite;
}

.glow-blue {
    top: 20%;
    left: 20%;
    background: rgba(37, 99, 235, 0.16);
}

.glow-purple {
    right: 20%;
    bottom: 20%;
    background: rgba(147, 51, 234, 0.16);
    animation-delay: 2s;
}

.panel {
    position: relative;
    z-index: 2;
    width: 760px;
    max-width: 760px;
    max-height: calc(100vh - 56px);
    padding: 30px 32px 32px;
    border-radius: 28px;
    background: rgba(15, 23, 42, 0.76);
    border: 1px solid rgba(51, 65, 85, 0.9);
    box-shadow: 0 28px 80px rgba(0, 0, 0, 0.45);
    backdrop-filter: blur(20px);
    display: flex;
    flex-direction: column;
}

.top-line {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 2px;
    background: linear-gradient(90deg, transparent, #3b82f6, #a855f7, transparent);
}

.header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 24px;
}

.badge {
    display: inline-block;
    padding: 5px 12px;
    border-radius: 999px;
    color: #c084fc;
    background: rgba(168, 85, 247, 0.1);
    border: 1px solid rgba(168, 85, 247, 0.2);
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 1px;
    text-transform: uppercase;
}

h1 {
    margin: 10px 0 0;
    font-size: 28px;
    line-height: 1.2;
    background: linear-gradient(90deg, #fff, #cbd5e1, #94a3b8);
    -webkit-background-clip: text;
    color: transparent;
}

.status-box {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 14px;
    border-radius: 14px;
    background: rgba(2, 6, 23, 0.8);
    border: 1px solid rgba(51, 65, 85, 0.9);
    font-size: 12px;
    color: #94a3b8;
    font-family: Consolas, monospace;
}

.dot {
    width: 8px;
    height: 8px;
    border-radius: 999px;
}

.success {
    background: #10b981;
    box-shadow: 0 0 10px #10b981;
}

.warning {
    background: #f59e0b;
    box-shadow: 0 0 10px #f59e0b;
}

.error {
    background: #f43f5e;
    box-shadow: 0 0 10px #f43f5e;
}

.config-card,
.card {
    border-radius: 22px;
    background: linear-gradient(180deg, rgba(15, 23, 42, 0.48), rgba(2, 6, 23, 0.34));
    border: 1px solid rgba(51, 65, 85, 0.72);
    box-shadow:
        inset 0 1px 0 rgba(148, 163, 184, 0.06),
        0 18px 40px rgba(0, 0, 0, 0.18);
}

.config-card {
    margin-bottom: 18px;
    padding: 20px 24px 18px;
}

.config-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 16px;
    margin-bottom: 16px;
}

.config-label {
    color: #e2e8f0;
    font-size: 15px;
    font-weight: 700;
}

.config-badge {
    padding: 5px 10px;
    border-radius: 999px;
    color: #93c5fd;
    background: rgba(59, 130, 246, 0.08);
    border: 1px solid rgba(59, 130, 246, 0.16);
    font-family: Consolas, monospace;
    font-size: 11px;
    font-weight: 700;
}

.config-row {
    display: flex;
    align-items: center;
    gap: 14px;
}

.input-wrap {
    flex: 1;
    height: 50px;
    display: flex;
    align-items: center;
    border-radius: 15px;
    background: rgba(2, 6, 23, 0.62);
    border: 1px solid rgba(51, 65, 85, 0.95);
}

.input-prefix {
    margin-left: 15px;
    padding: 4px 8px;
    border-radius: 8px;
    color: #818cf8;
    background: rgba(99, 102, 241, 0.1);
    font-family: Consolas, monospace;
    font-size: 11px;
    font-weight: 700;
}

.server-input {
    flex: 1;
    height: 100%;
    padding: 0 15px 0 12px;
    border: 0;
    outline: none;
    color: #e2e8f0;
    background: transparent;
    font-family: Consolas, "Microsoft YaHei", monospace;
    font-size: 14px;
}

.config-tip {
    margin: 12px 0 0;
    color: #94a3b8;
    font-size: 12px;
}

.card {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 18px;
    margin-bottom: 22px;
    padding: 22px 24px;
}

.card-title {
    margin: 0;
    color: #cbd5e1;
    font-size: 15px;
    font-weight: 600;
}

.card-url {
    margin: 8px 0 0;
    color: #64748b;
    font-size: 12px;
    font-family: Consolas, monospace;
    line-height: 1.45;
    word-break: break-all;
}

.request-info {
    flex: 1;
    min-width: 0;
}

button {
    border: 0;
    cursor: pointer;
    color: white;
    border-radius: 14px;
    font-size: 14px;
    font-weight: 600;
    background: linear-gradient(90deg, #2563eb, #9333ea);
    box-shadow: 0 8px 28px rgba(99, 102, 241, 0.25);
}

button:hover {
    background: linear-gradient(90deg, #3b82f6, #a855f7);
}

button:disabled {
    opacity: 0.65;
    cursor: not-allowed;
}

.secondary-btn {
    flex: 0 0 78px;
    height: 50px;
    padding: 0 18px;
    white-space: nowrap;
}

.refresh-btn {
    min-width: 176px;
    height: 66px;
    padding: 0 26px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    white-space: nowrap;
}

.spin {
    display: inline-block;
    animation: spin 0.8s linear infinite;
}

@keyframes spin {
    to {
        transform: rotate(360deg);
    }
}

.response-head {
    display: flex;
    justify-content: space-between;
    padding: 0 4px 8px;
    color: #94a3b8;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.6px;
    text-transform: uppercase;
}

#responseTime {
    color: #475569;
    font-family: Consolas, monospace;
}

pre {
    min-height: 260px;
    max-height: 260px;
    overflow: auto;
    margin: 0;
    padding: 20px;
    border-radius: 20px;
    background: rgba(2, 6, 23, 0.82);
    border: 1px solid rgba(51, 65, 85, 0.8);
    color: #60a5fa;
    white-space: pre-wrap;
    word-break: break-word;
    font-family: Consolas, monospace;
    font-size: 14px;
    line-height: 1.6;
}

.text-success {
    color: #34d399;
}

.text-warning {
    color: #fbbf24;
}

.text-error {
    color: #fb7185;
}

14. 图标生成

如果缺少图标,打包可能报:

icons/icon.ico not found

可以先准备一个 PNG 图片,然后执行:

pnpm tauri icon .\app-icon.png

生成目录:

src-tauri/icons/

其中必须包含:

src-tauri/icons/icon.ico

15. 开发运行

cd D:\my-work\frontend-work\algorithm-engine-console

pnpm install

pnpm tauri dev

16. 打包 EXE

pnpm tauri build

如果 bundle.active=false,最终 exe 路径一般为:

src-tauri/target/release/AlgorithmEngine.exe

17. 常见错误与解决方案

17.1 Cargo 找不到 lib

错误:

can't find library `xxx_lib`, rename file to `src/lib.rs` or specify lib.path

原因:

Cargo.toml 声明了 [lib],但项目只有 main.rs。

解决:

删除 Cargo.toml 中的 [lib] 配置。

17.2 frontendDist 指向项目根目录

错误:

The configured frontendDist includes the ["src-tauri\\target", "node_modules", "src-tauri"] folders.

原因:

"frontendDist": "../"

解决:

"frontendDist": "../web"

17.3 Tauri 版本不一致

错误:

tauri (v2.0.0) : @tauri-apps/api (v2.11.1)

解决:

统一 Rust tauri 和 NPM @tauri-apps/api / cli 版本。

推荐:

"@tauri-apps/api": "2.11.1",
"@tauri-apps/cli": "2.11.1"
tauri = { version = "2.11.1", features = [] }
tauri-build = { version = "2.5.1", features = [] }

清理后重装:

Remove-Item -Recurse -Force node_modules -ErrorAction SilentlyContinue
Remove-Item -Force pnpm-lock.yaml -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force src-tauri\target -ErrorAction SilentlyContinue
Remove-Item -Force src-tauri\Cargo.lock -ErrorAction SilentlyContinue

pnpm install
pnpm tauri build

17.4 icons/icon.ico not found

错误:

icons/icon.ico not found

解决:

pnpm tauri icon .\app-icon.png

或者手动放置:

src-tauri/icons/icon.ico

17.5 双击 exe 弹出黑窗口

解决:

main.rs 顶部添加:

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

17.6 Command not found

错误:

Command request_xxx not found

原因:

JS invoke 的命令名和 Rust 注册的命令名不一致。

解决:

JS:

invoke("request_backend_api", ...)

Rust:

tauri::generate_handler![request_backend_api]

两边必须完全一致。

17.7 按钮点击没反应

常见原因:

import { invoke } from "@tauri-apps/api/core";

纯 HTML 页面不经过 Vite/Webpack 打包时,不能直接这样导入。

解决:

const invoke = window.__TAURI__.core.invoke;

并确保 tauri.conf.json 中有:

"withGlobalTauri": true

17.8 请求失败 error sending request

说明:

页面按钮和 Rust 命令已经打通,但 Rust 请求后端失败。

排查:

1. 后端服务是否启动
2. 后端地址是否正确
3. 接口路径是否正确
4. 请求方式是否匹配 GET / POST
5. Token 是否有效
6. 后端是否需要 body、文件或 multipart
7. 本机防火墙是否拦截

17.9 NSIS 下载超时

错误:

failed to bundle project `timeout: global`

原因:

Tauri 打安装包时需要下载 NSIS,网络超时。

解决:

先关闭安装包,只生成 exe:

"bundle": {
  "active": false,
  "icon": [
    "icons/icon.ico"
  ]
}

18. 推荐发布方式

内部工具推荐先交付单文件 exe:

src-tauri/target/release/AlgorithmEngine.exe

优点:

1. 不依赖安装器
2. 不需要 NSIS
3. 构建更稳定
4. 复制到其他 Windows 电脑即可运行

后续如果需要正式安装包,再开启:

"bundle": {
  "active": true,
  "targets": ["nsis"],
  "icon": [
    "icons/icon.ico"
  ]
}

19. 安全注意事项

不要在文档、代码、截图、Git 仓库中保留以下内容:

真实 JWT
账号密码
接口密钥
客户地址
内网敏感 IP
生产环境接口地址
数据库连接串

推荐替换为:

YOUR_TOKEN_HERE
YOUR_API_PATH
http://localhost:8080
http://your-server-host:port

如必须使用 token,建议改成:

1. 页面输入 token
2. 登录接口动态获取 token
3. 使用本地配置文件
4. 使用后端代理鉴权

20. 完整操作命令汇总

cd D:\my-work\frontend-work

mkdir algorithm-engine-console
cd algorithm-engine-console

pnpm init

pnpm add @tauri-apps/api@2.11.1
pnpm add -D @tauri-apps/cli@2.11.1

mkdir web
mkdir web\src
mkdir src-tauri
mkdir src-tauri\src
mkdir src-tauri\icons

pnpm install

pnpm tauri dev

打包:

pnpm tauri build

运行:

.\src-tauri\target\release\AlgorithmEngine.exe

结论

使用 Tauri + Rust 可以非常轻量地将 HTML 页面封装为 Windows 桌面 EXE。

推荐最终方案:

前端:HTML + CSS + JavaScript
桌面框架:Tauri v2
后端请求:Rust reqwest 代理调用
配置保存:localStorage
窗口模式:固定默认大窗口,页面无滚动条
打包方式:优先生成单文件 exe
敏感信息:全部使用占位符,不写入代码仓库

这套方案适合企业内部桌面工具、本地运维工具、算法引擎控制台、局域网接口调用工具等场景。

rust
tauri

关于作者

Miraclewcg
上善若水
获得点赞
文章被阅读