Day 16:02. 基于 Tauri 2.0 开发后台管理系统-项目初始化配置

一、前言

在开始写代码之前,我已经将官网上的核心概念大致扫了一遍,tauri.app/zh-cn/conce... web 端相差甚远,不过我好像摸到了一些门路,以下都是我个人见解,可能有所偏差,欢迎指正。Tauri 开发的应用程序分为两个部分,前端和后端,其实本质上也是 C/S 架构。前端就和之前 web 开发一样,你可以选择自己擅长的 UI 框架,而后端则是基于 Rust 来做的,可以完成业务逻辑操作,系统调用等等。我的脑海里目前结构是这样的,对应代码的目录,前端就是 src 下的代码,后端就是 src-tauri

看到这其实就比较熟悉了,把 Rust 换成我们熟悉的 Java,又回到了 web 开发的模式了。当然如果你愿意深入学习 Rust,完全用 Rust 来构建服务端也是可以的。

所以最后实际上我的项目就变成了这样。

这就很熟悉了,Tauri 似乎只是简单的套壳了。后来仔细想想好像也不是,相比于 Web 端多了一些访问系统原生 Api 的能力。

Vue3:前端开发体验好

Java 后端:成熟稳定,开发效率高

Tauri:提供桌面应用外壳,未来可扩展系统功能

二、技术栈选择

前端(Tauri):

  • Vue3 + TypeScript
  • UI 组件库:Ant Design Vue
  • HTTP 客户端:axios
  • 状态管理:Pinia

后端(Java):

  • Spring Boot + Sa-Token

  • 数据库:PostgreSQL + MyBatis-Plus

  • API 文档:Swagger

  • 部署:Docker + 云服务器

截止目前 2025.11.26 我会使用当前所有最新稳定版本进行开发。我建议是我当前的文章作为辅助,以官方文档为主,我会在关键位置给出文档链接。

三、完成前后端数据交互

3.1. 目录机构规划

首先和常规 Vue 项目一样,规划好我们的目录结构,大致如下

bash 复制代码
src               
├─ api            # API 接口   
├─ assets          # 静态资源
├─ components       # 通用组件   
├─ layouts         # 布局组件     
├─ router          # 路由配置  
├─ stores      		 # 状态管理(Pinia)  
├─ utils      		 # 工具函数 
├─ views     		 # 页面组件     
├─ App.vue        
├─ main.ts        
└─ vite-env.d.ts  

3.2. 安装整合常用依赖

安装所有依赖

bash 复制代码
# Vue 生态
pnpm add vue-router@4 pinia pinia-plugin-persistedstate
# UI 组件库 (Ant Design Vue)
pnpm add ant-design-vue@4.x @ant-design/icons-vue
# 样式预处理
pnpm add install less 
# HTTP 客户端
pnpm add install axios

一键安装所有

bash 复制代码
pnpm add vue-router@4 pinia pinia-plugin-persistedstate ant-design-vue@4.x @ant-design/icons-vue less  axios

3.2.1. 整合 Ant Design Vue

这里我希望能够实现自动按需导入,官方文档这里写的不够清楚,www.antdv.com/docs/vue/ge... unplugin-vue-componentsunplugin-auto-import这两款插件来完成。

bash 复制代码
pnpm install -D unplugin-vue-components unplugin-auto-import

在插件的 GitHub 仓库中可以看到是支持的,所以无需担心。

github.com/unplugin/un...

github.com/unplugin/un...

vite.config.js 中配置按需引入:

typescript 复制代码
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  // ...
  plugins: [
    // ...
    AutoImport({
      resolvers: [AntDesignVueResolver()],
    }),
    Components({
      resolvers: [
        AntDesignVueResolver({
           importStyle: 'less', // 使用 less
        }),
      ],
    }),
  ],
})

这里要注意, 必须用 less

随便找一个组件,测试一下

3.2.2. 整合 Vue Router

文档:router.vuejs.org/zh/installa...

创建路由实例 router/index.js

typescript 复制代码
import { createRouter, createWebHashHistory } from 'vue-router'

import HomeView from './HomeView.vue'
import AboutView from './AboutView.vue'

const router = createRouter({
    history: createWebHashHistory(import.meta.env.BASE_URL),
    routes: [
        { path: '/', component: HomeView },
        { path: '/about', component: AboutView },
    ]
})
export default router

为了方便测试,我定义了两个页面组件。

main.ts 中注册路由

typescript 复制代码
import { createApp } from "vue";
import App from "./App.vue";
import router from './router'


createApp(App).use(router).mount("#app");

app.vue 中测试

vue 复制代码
<template>
  <h1>Hello App!</h1>
  <p><strong>Current route path:</strong> {{ $route.fullPath }}</p>
  <nav>
    <RouterLink to="/">Go to Home</RouterLink>
    <RouterLink to="/about">Go to About</RouterLink>
  </nav>
  <main>
    <RouterView />
  </main>
</template>

3.2.3. 整合 pinia & pinia-plugin-persistedstate

官方文档:pinia.vuejs.org/zh/introduc...

按模块化定义 store,以下为目录结构

复制代码
stores         
├─ modules     
│  └─ user.ts  
└─ index.ts    
  • 创建 src/stores/index.ts
typescript 复制代码
import { createPinia } from 'pinia'

const store = createPinia()

export default store
  • main.ts 里引入
typescript 复制代码
//main.ts
...
// 引入 pinia
import store from './stores'

const app = createApp(App)
app.use(store)
...
  • modules: 按模块定义对应的 store, user.ts 表示用户相关数据存储的仓库
typescript 复制代码
// user.ts
import { defineStore } from 'pinia'

// 你可以对 `defineStore()` 的返回值进行任意命名,但最好使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾。(比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useUserInfoStore = defineStore('userInfo', {
    // 其他配置...
})

在引入 pinia 的时候做一些调整,不直接在 main.ts 里引入,这样做的好处在于,之后对于 pinia 集成一些插件,或者做一些其他配置,可以独立出来,不用全部堆积在 main.ts 中

  • 使用 pinia-plugin-persistedstate 持久化

官方文档:praz.codeberg.page/pinia-plugi...

web 端的时候可以用 LocalStorageSessionStorage 等,但是桌面端我就不确定了,我去搜了一下,据说是支持 LocalStorage 的,但是有局限性,推荐了一个 Tauri Store,这个等到具体使用的时候再去研究,今天任务就是先整合上。

将插件添加到 pinia 实例上

typescript 复制代码
// ./stores/index.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const store = createPinia()

store.use(piniaPluginPersistedstate)

export default store

只需要在定义的 store 里开启配置即可完成持久化操作

typescript 复制代码
export const useUserInfoStore = defineStore('userInfo', {
    // 其他配置...
    persist: true   
})

默认使用的就是 LocalStorage,这里我暂时不测试了,等到用的时候替换为 Tauri Store 再研究。

3.2.4. 整合 Axios

定义请求实例,这里我直接从之前项目中拷贝过来的,基本上都差不多。

创建 utils/request.ts

typescript 复制代码
import { message } from 'ant-design-vue'
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'

type RequestConfig<T = unknown> = AxiosRequestConfig & {
    // 可以在这里扩展自定义配置
    mock?: boolean // 是否使用mock数据
    mockData?: T // mock数据
    noErrorToast?: boolean // 是否不显示错误提示
}

class Request {
    private instance: AxiosInstance // axios实例

    constructor() {
        this.instance = axios.create({
            baseURL: import.meta.env.VITE_APP_BASE_API,
            timeout: 180000,
            headers: {
                'Content-Type': 'application/json;charset=UTF-8',
            },
        })

        this.setupInterceptors()
    }

    private setupInterceptors() {
        // 请求拦截器
        this.instance.interceptors.request.use(
            (config) => config,
            (error) => Promise.reject(error),
        )

        // 响应拦截器
        this.instance.interceptors.response.use(
            (response: AxiosResponse) => {
                // 业务状态码处理
                if (response.data.code !== 200) {
                    const config = response.config as RequestConfig
                    if (!config.noErrorToast) {
                        message.error(response.data.message || '请求失败')
                    }
                    return Promise.reject(response.data)
                }
                return response.data.data
            },
            (error) => {
                // 处理mock数据
                if (error.isMock) {
                    return Promise.resolve(error.data)
                }
                const config = error.config as RequestConfig | undefined

                // 只有配置了不跳过错误处理时才显示错误提示
                if (!config?.noErrorToast) {
                    let msg = '请求错误'
                    if (error.response) {
                        switch (error.response.status) {
                            case 400:
                                msg = '请求参数错误'
                                break
                            case 401:
                                msg = '未授权,请登录'
                                break
                            case 403:
                                msg = '拒绝访问'
                                break
                            case 404:
                                msg = '请求资源不存在'
                                break
                            case 500:
                                msg = '服务器异常,请稍后再试'
                                break
                            default:
                                msg = error.response.data?.message || `服务器错误: ${error.response.status}`
                        }
                    } else if (error.request) {
                        msg = '请求超时或网络异常'
                    } else {
                        msg = error.message || '未知错误'
                    }
                    message.error(msg)
                }
                return Promise.reject(error)
            },
        )
    }

    // 封装核心请求方法
    public request<T = unknown>(config: RequestConfig<T>): Promise<T> {
        // 模拟数据直接返回
        if (config.mock) {
            return Promise.resolve(config.mockData as T)
        }

        return this.instance(config)
    }

    public get<T = unknown>(url: string, config?: RequestConfig<T>): Promise<T> {
        return this.request({ ...config, method: 'GET', url })
    }

    public post<T = unknown>(url: string, data?: unknown, config?: RequestConfig<T>): Promise<T> {
        return this.request({ ...config, method: 'POST', url, data })
    }

    public put<T = unknown>(url: string, data?: unknown, config?: RequestConfig<T>): Promise<T> {
        return this.request({ ...config, method: 'PUT', url, data })
    }

    public delete<T = unknown>(url: string, data?: unknown, config?: RequestConfig<T>): Promise<T> {
        return this.request({ ...config, method: 'DELETE', url, data })
    }
}

const http = new Request()

export default http

定义个接口测试一下

创建 api/user.ts

typescript 复制代码
import http from '../utils/request'

export const userApi = {
    // 获取用户列表
    getUserInfo: () =>
        http.post('/user/info'),

}

app.vue 中测试

vue 复制代码
<a-button type="primary" @click="getUserInfo">获取用户信息</a-button>
typescript 复制代码
<script setup lang="ts">
import { userApi } from './api/user'

const getUserInfo = () => {
  userApi.getUserInfo()
}

</script>

在应用窗口点击按钮,按 F12,看到请求出去了,即表示成功了。

四、总结

到头来发现又回到了熟悉的领域,如果你不想做成桌面端,直接使用 vue 脚手架搭建项目,所有整合步骤几乎一模一样。今天就先这样,现在有个问题,先做前端用 mock 数据做接口,完成基础功能后,再开发服务端,还是直接同时进行。如果有人看到这里,你希望采用哪种方式呢?欢迎留言。

千里之行,始于足下。你的"个人公司"从这第一个2小时开始。欢迎在评论区分享你的进展或遇到的卡点,我会逐一查看,尽可能的帮助解决。我们下一篇文章见!

相关推荐
鹿鹿鹿鹿isNotDefined14 分钟前
逐步手写,实现符合 Promise A+ 规范的 Promise
前端·javascript·算法
一千柯橘15 分钟前
Electron - IPC 解决主进程和渲染进程之间的通信
前端
bcbnb16 分钟前
游戏上架 App Store 的完整发行流程,从构建、合规到审核的多角色协同指南
后端
JavaGuide17 分钟前
美团2026届后端一二面(附详细参考答案)
java·后端
aiopencode17 分钟前
无需源码的 iOS 加固方案 面向外包项目与存量应用的多层安全体系
后端
老前端的功夫17 分钟前
HTTP 协议演进深度解析:从 1.0 到 2.0 的性能革命
前端·网络·网络协议·http·前端框架
拉不动的猪21 分钟前
前端三大权限场景全解析:设计、实现、存储与企业级实践
前端·javascript·面试
语落心生22 分钟前
Apache Geaflow推理框架Geaflow-infer 解析系列(六)共享内存架构
后端
语落心生25 分钟前
Apache Geaflow推理框架Geaflow-infer 解析系列(七)数据读写流程
后端