浏览器收藏栏里的开发者工具网站越来越多,JSON 格式化、时间戳换算、正则调试、JWT 解码、Base64 编解码、颜色转换……每个都挺好用,可每个都要开一个新标签页,还要忍受完全不同的交互方式和时不时弹出来的广告。我大概是在去年下半年彻底受不了这个状态,决定用自己最顺手的 Vue3 和 FastAPI 从头撸一个高颜值的开发者工具台,项目代号叫 Sidereal Hub。
这个项目做下来将近四个月,目前已经稳定跑在我自己的服务器上,平时写代码、排查接口、调试数据,基本不再开别的工具站。如果你也是做前端、后端或者全栈开发的,平时被各种散装工具折磨得够呛,又恰好对 Vue3 和 FastAPI 这套组合感兴趣,那这篇复盘应该能帮到你。我不打算写什么系统教程,就是想把这个项目从立项、选型、目录设计、前后端联调,到那些真正让人抓狂的 Bug 排查过程,完整地讲一遍。
1. 从"收藏栏一团乱麻"到做出 Sidereal Hub:立项逻辑
1.1 我的真实痛点:不是工具不够,而是工具太散
先说说我为什么非要自己做一个。我的浏览器收藏栏里,光工具类网站就攒了三四十个:JSON 格式化用 A 站,时间戳转换用 B 站,正则测试用 C 站,颜色选择用 D 站,JWT 解码用 E 站……每个工具单拿出来都还行,但合在一起就是灾难。
灾难体现在几个地方。第一,交互不统一。有的工具回车触发,有的点按钮触发,有的输入即触发,每次换着用都得重新适应。第二,数据不敢乱贴。有些在线工具会把输入内容发到服务端解析,我调试接口时经常要粘贴内部返回的 JSON,心里总有点不踏实。第三,想要的功能别人不一定有。比如我想把"时间戳 + 请求体 + 响应体"拼成一条调试记录保存下来,方便后来回溯,这种偏个性化的工作流,几乎没有现成工具愿意做。
后来我也试着用开源的导航站或者工具聚合项目直接部署一套,但发现大部分项目要么很久没维护,要么界面停留在几年前的风格,要么扩展新工具非常麻烦。既然找不到合适的,那就自己写一个。Sidereal Hub 的定位,从第一天起就很明确:一个能自己控制一切、可以随手加新工具、且界面拿得出手的私有开发者工具台。
1.2 给工具台划定的三条边界条件
立项光有冲动不够,我给自己定了三个必须满足的条件,后来的所有设计决策都是围绕这三条展开的。
第一,私有优先。工具台部署在自己的服务器上,所有输入数据只在本机或内网处理,绝不把数据转发给任何第三方接口。这是它区别于大部分在线工具站的核心价值。
第二,扩展要轻。我不想每加一个工具都要大动干戈改框架。理想状态是:写一个独立的前端组件,再往后端加一个注册接口,五分钟内新工具就能出现在面板上。
第三,颜值在线。开发者工具大多是"能用就行"的样子,但天天要用的东西,难看真的会影响心情。我希望它有统一的色彩体系、舒服的间距、顺畅的暗色模式,而不是各种组件库默认样式的大杂烩。
这三条边界,直接决定了后面 Vue3 和 FastAPI 的选型,也决定了前端要采用"组件自治 + 统一注册"的架构。
1.3 为什么叫 Sidereal Hub
名字其实是我在听一首后摇时想到的。Sidereal 是"恒星的"的意思,跟天文相关。我想把工具台做成一个导航枢纽——像星空一样,每一颗星星都是一个独立的工具,但它们在同一个坐标系里有规律地排列。这也间接影响了前端面板的卡片式布局和暗色主题的视觉方向。
2. 技术选型复盘:Vue3 + FastAPI 这套组合到底香在哪
2.1 前端为什么选 Vue3,而不是 Next、Nuxt 或者 React
工具台这个场景,本质上是一个需要登录态的、带 CRUD 的"后台管理系统",只是内容变成了各种开发者工具。这类项目最关键的需求是:开发效率、组件复用、状态管理清晰,而不是 SEO、首屏服务端渲染这些东西。所以 SSR 框架对我没有吸引力。
Vue3 打动我的是组合式 API 和响应式系统的配合。举个例子,工具台左侧有一个工具分类栏,右侧是根据分类过滤的卡片列表,还要同步保持 URL 参数一致。如果用 Options API,这些逻辑散落在 data、watch、mounted 里,要来回跳着看;用组合式 API 之后,我把过滤逻辑、URL 同步逻辑、加载逻辑分别抽成几个 composable,每个文件只管一件事,后续维护真的很省心。
热词里很多人搜"vue3 后台管理系统"或者"vue3 商城",其实都是在找类似的整体方案。我的建议是别一上来就套现成的后台脚手架,先想想自己的核心逻辑是什么,Vue3 最值钱的是你组织代码的方式,而不是某个框架给你预置了多少页面。
2.2 后端为什么是 FastAPI,而不是 Flask 或 Node
工具台需要一个后端,是因为有些能力只靠浏览器是不好实现的:调用需要签名算法的时间戳转换、生成 RSA 密钥对、数据持久化保存用户配置和操作记录、定时任务执行健康检查等。这些沾点"计算密集 + 需要保密"的活儿,放在后端更靠谱。
FastAPI 我是对比过 Flask 之后才定的。Flask 老牌、稳定、生态熟,但它的异步支持是后面补的,实际用起来要么靠 gevent 魔改,要么老老实实写同步路由。FastAPI 天生就是 async,配合异步 SQLAlchemy 和 asyncpg,在 IO 密集的操作上表现明显更好——比如工具台首页要同时加载用户信息、工具列表、最近操作记录三个数据源,用asyncio.gather并发请求,响应时间比串行少了一半以上。
更让我舒服的是 Pydantic。路由函数的参数直接声明成模型类,FastAPI 会自动完成请求体解析和校验,校验失败的错误信息还能直接映射成前端需要的格式。以前写 Flask 校验参数,要自己先读request.json,再手动判断字段是否存在、类型对不对,写多了真的烦躁。FastAPI 把这部分干掉了,我只需要定义好数据模型,它自动生成 OpenAPI 文档,前端同事甚至可以拿文档直接当接口契约用。
2.3 SQLAlchemy + 数据模型的两次救命时刻
热词里有人搜"fastapi 和 sqlalchemy 构建高性能 web 服务",我实际用下来,SQLAlchemy 2.0 的声明式模型和 FastAPI 配合得很流畅。我最喜欢的是select()语句的写法,比 1.x 时代的session.query直观很多:
# models.py from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from datetime import datetime class Base(DeclarativeBase): pass class OperationLog(Base): __tablename__ = "operation_logs" id: Mapped[int] = mapped_column(primary_key=True) user_id: Mapped[int] = mapped_column(index=True) action: Mapped[str] = mapped_column(index=True) payload: Mapped[str] = mapped_column(default="{}") created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)印象最深的第一次"救命",是给工具台加"最近使用"功能的时候。我想记录每个用户最近打开过哪些工具、操作过哪些数据,于是加了一张操作日志表。当时很担心这会让工具台变慢,实际上因为用了异步 SQLAlchemy 加上查询条件走索引,插入日志和读取最近记录几乎不感知延迟。
第二次是数据统计页面。工具台内部有一个"每个工具被使用次数"的折线图,一开始我在 Python 里循环查数据库,慢得要命。后来换成 SQLAlchemy 的分组聚合查询,一次select(UserToolUsage.tool_id, func.count())把原始数据拿出来,再在前端做图表聚合,性能立刻正常。这个经验其实很通用:别把所有逻辑都往后端塞,前端能用缓存和本地计算解决的就地解决,后端只提供纪元数据和聚合结果。
3. 项目目录结构:独立开发也要把仓库拆得明明白白
3.1 后端目录:按模块切,而不是按文件类型切
FastAPI 项目目录结构这个问题,热词里搜的人很多。我看到很多新手喜欢把所有路由写在一个main.py里,或者按文件类型建一堆models.py、apis.py、routers.py,文件一多就混乱。我的做法是按业务模块切,一个模块一个包,每个包里自带路由、模型、Schema、服务:
backend/ ├── app/ │ ├── main.py # FastAPI 实例、CORS、路由汇总 │ ├── core/ # 配置、安全、依赖 │ │ ├── config.py │ │ ├── security.py # JWT 生成/校验 │ │ └── deps.py # 通用依赖,比如 get_db │ ├── modules/ │ │ ├── auth/ # 登录注册模块 │ │ │ ├── router.py │ │ │ ├── schemas.py │ │ │ ├── service.py │ │ │ └── models.py │ │ ├── tools/ # 工具模块 │ │ │ ├── router.py │ │ │ ├── schemas.py │ │ │ ├── service.py │ │ │ └── data.py # 工具元数据注册表 │ │ └── logs/ # 操作日志模块 │ ├── models/ # 公共模型,比如用户表 │ └── schemas/ # 公共 Schema ├── alembic/ # 数据库迁移 ├── requirements.txt └── .env这样的结构有几个明显好处。新增一个工具模块时,只需要在app/modules下新建一个包,然后在main.py里include_router一次,其他模块完全不受影响。core目录放全局配置和安全依赖,modules目录放业务逻辑,职责边界清楚。Alembic 单独放迁移脚本,改表结构时不用手动去同步生产库。
3.2 前端目录:除了 views 和 components,还有第三个关键目录
Vue3 项目的目录网上有无数种模板,我用下来最顺手的结构是这样的:
frontend/ ├── src/ │ ├── api/ # 与后端接口一一对应的请求函数 │ │ ├── auth.ts │ │ ├── tools.ts │ │ └── logs.ts │ ├── assets/ │ ├── components/ # 通用 UI 组件,与业务无关 │ ├── composables/ # 可复用的组合式函数 │ ├── layouts/ # 面板布局 │ ├── router/ │ ├── stores/ # Pinia │ ├── styles/ # 设计 Token、全局样式 │ ├── utils/ # 纯函数工具,比如时间戳换算 │ ├── views/ # 页面级组件 │ └── main.ts这里最想强调的第三个目录是composables。很多 Vue3 项目的components目录会越滚越大,各种页面也顺手往里塞代码。我的习惯是:凡是涉及状态逻辑、浏览器 API 操作、数据请求这一段,尽量抽成 composable,组件里只保留模板和事件绑定。比如"时间戳转换"这个工具,它的核心逻辑不是展示界面,而是把时间戳、日期字符串、相对时间之间互相换算的算法,我把它放在composables/useTimestampConverter.ts里,组件只负责调用。
3.3 前后端共享类型:少写一半重复代码
前后端联调时最烦人的是数据结构不一致,前端以为返回的是created_at,后端实际给的是createTime,一改改半天。我在项目早期就被这个问题坑过几次,后来学乖了:先用 FastAPI 自动生成的 OpenAPI 文档固定住 schema,然后手写一份对应的 TypeScript 类型定义放在src/api/types.ts里,前后端都拿这套定义当契约。
虽然手写类型还是会前后端各维护一份,但至少接口字段名和层级是统一的。如果你有精力,可以用openapi-typescript工具直接从openapi.json生成 TS 类型,效果更好。这个环节的价值在项目后期体现得特别明显,工具越来越多,如果类型全靠人脑记,迟早出乱子。
4. CORS、请求封装与动态路由:前后端分离开发里绕不开的几道坎
4.1 FastAPI 的 CORS 配置:为什么本地联调第一次就炸
前后端分离开发时,前端跑在http://localhost:5173,后端跑在http://localhost:8000,这俩端口不一样,浏览器就会触发跨域限制。我记得第一次用 Vite 启动前端、用 Uvicorn 启动后端,在页面上调用登录接口,控制台直接报类似 "CORS policy: No 'Access-Control-Allow-Origin' header" 的错误。
这个问题的本质是浏览器的同源策略:不同源的请求要放行,服务端必须在响应头里明确告诉浏览器"这个源允许访问"。FastAPI 的解决方案是通过CORSMiddleware中间件配置允许的源、方法等。我的开发环境配置大概长这样:
from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=[ "http://localhost:5173", "http://127.0.0.1:5173", ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )注意allow_credentials=True时,allow_origins不能写["*"],必须明确列出允许的源,不然浏览器还是会拦。这个细节坑过不少人——开发环境配置好了,部署到服务器又可能因为域名变化再炸一次。所以我有两个.env文件,dev环境允许本地源,prod环境只允许正式域名,避免把跨域接口裸奔到公网。
4.2 axios 封装与前端"怎么连接后端"
"vue3 怎么连接后端"这个热词几乎每天都有新人搜。其实说到底就是发 HTTP 请求、处理响应和错误。我在项目里用 axios 封装了一个统一请求层,核心是拦截器:
// src/api/request.ts import axios from 'axios' import { useUserStore } from '@/stores/user' import router from '@/router' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000, }) request.interceptors.request.use((config) => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } return config }) request.interceptors.response.use( (response) => response.data, (error) => { if (error.response?.status === 401) { // token 过期,跳回登录页 useUserStore().clear() router.push({ name: 'login' }) } return Promise.reject(error) } )这个封装解决两个问题:一是把 token 自动塞进请求头,不用每个接口手动带;二是统一处理 401 未授权,登录过期时自动跳回登录页。我在封装时特别注意了响应拦截器的返回,因为后端统一包的格式是{ code, message, data },所以我在这里直接剥掉外层,让业务代码拿到的就是data字段,少一层嵌套。
4.3 动态路由与菜单:让工具注册像"填表单"一样简单
工具台的面板左侧是分类菜单,右侧是工具卡片。分类和工具不能写死在前端路由里,不然每加一个工具都要改路由表、改菜单,太麻烦。我用的是"后端注册表 + 前端动态渲染"的方案。
后端在app/modules/tools/data.py里维护一份工具注册表,每个工具包含id、name、category、icon、description、route等字段:
TOOL_REGISTRY = [ { "id": "timestamp", "name": "时间戳转换", "category": "数据转换", "icon": "Clock", "description": "时间戳、日期字符串、相对时间互转", "route": "/tools/timestamp", "enabled": True, }, # 更多工具... ]前端页面启动时,通过/api/tools拉取这份注册表,动态生成侧边菜单和工具卡片的点击路由。这样加一个新工具,后端加一条记录,前端写一个工具组件扔进views/tools/目录,再补一条路由映射,就完事了。按我熟练度,十分钟不到就能上架一个工具。
5. 高颜值是怎么落地的:设计 Token、Element Plus 定制与主题切换
5.1 先定义设计 Token,再写组件
很多后台管理项目颜值拉胯,并不是开发能力不行,而是没有统一的设计约束。我在写第一个页面之前,先建了一个styles/tokens.css,把所有颜色、间距、圆角、阴影、字体大小定义成一个一个的 CSS 变量:
:root { --color-bg-primary: #0f1115; --color-bg-secondary: #161a22; --color-bg-card: #1c212b; --color-border: #2a313c; --color-text-primary: #e5e9f0; --color-text-secondary: #8b95a7; --color-accent: #6c8cff; --radius-md: 8px; --radius-lg: 12px; --shadow-card: 0 4px 20px rgba(0, 0, 0, 0.3); --space-page: 24px; --space-card: 16px; }这一步的价值后面会成倍放大。想要统一调整工具卡片的圆角、让暗色模式整个变一种色调、或者把主色从蓝色换成紫色,只需要改这几个变量,而不是满项目找一个个 class。
热词里有人搜"vue3 修改 tabs 标签页样式",其实根子也在 Token 上。如果组件里的颜色、边框都是从设计 Token 映射过去的,改主题时就不用去挑一个个组件的内部类名,全局换肤会轻松很多。
5.2 Element Plus 的定制:只用基础组件,不让框架定义视觉
我用的是 Element Plus,但刻意没有直接用它的默认主题。默认主题的蓝色偏活泼,跟我想做的深色专业工具台气质不搭。Element Plus 所有组件的 SCSS 变量是暴露出来的,可以覆盖。
我写了一个styles/element.scss,在最前面用@forward或者直接设置变量来覆盖主题色:
// 覆盖 Element Plus 的部分设计变量 $--color-primary: #6c8cff; $--border-radius-base: 8px;再配合全局 CSS 变量,让 Element Plus 的按钮、输入框、弹窗融入整体暗色风格。这里要提醒一句:不要试图把每个组件的内部样式都手动改一遍,那是无底洞。我的原则是,Element Plus 管交互逻辑和基本结构,颜色、圆角、边框这些视觉属性尽量通过主题变量控制,控制不了的小地方再单独覆盖。
5.3 明暗主题切换与动效的克制
开发者工具台的使用环境差异很大,白天办公室光线亮,晚上家里光线暗,明暗主题切换几乎是刚需。我实现的方式比较传统但很稳定:给<html>元素切换>html[data-theme='dark'] { --color-bg-primary: #0f1115; --color-bg-card: #1c212b; --color-text-primary: #e5e9f0; } html[data-theme='light'] { --color-bg-primary: #f5f6f8; --color-bg-card: #ffffff; --color-text-primary: #1a1d24; }
切换逻辑就是给document.documentElement设置>location / { try_files $uri $uri/ /index.html; }
这样不管是用户直接访问/tools/timestamp还是刷新页面,都能回到 Vue 应用,再由前端路由自动定位到对应工具。
7.2 独立开发期间最值得记住的三句话
第一句,工具的"颜值"不是后期加的滤镜,而是贯穿在结构设计里的 Token 体系。我正是因为一开始就建立了统一的设计变量,后面几十个工具组件才能保持视觉一致,没有变成杂牌军。
第二句,比起功能数量,工具的"信任感"更重要。我的工具台坚持私有部署、数据不落第三方,这在一个人人都在谈论数据安全的时代,其实就是最好的差异化。哪怕功能比在线工具少一点,核心用户也会因为这一点留下来。
第三句,遇到 Bug 时,不要急着看别人说的"标准答案",先自己把报错信息、渲染 DOM、请求状态完整梳理一遍。我记录在案的那几个疑难问题,最后排查出的根因往往不是组件库写错了,而是自己代码组织方式、字符细节、渲染时机出了问题。能把这些基础环节控制住,开发效率会明显上一个大台阶。
Sidereal Hub 目前还在慢慢迭代,我给自己定的节奏是每两周新增一个工具,顺便优化一个已有工具的交互细节。工具台这种项目没有做完的一天,但每次往面板里加一个新工具,或者在暗色主题下看到一个交互细节变得更顺滑,都会觉得这四个月的前期投入是值得的。如果你也有类似的想法,不需要等所有条件都完美,先把最常用的两三个工具做成自己能接受的样子,然后逐个补,慢慢就会成为一个真正属于你自己的开发底座。