☰
t3code:三层架构迷你全栈项目的设计与落地
2026/10/27 6:59:35 网站建设 项目流程

做一个叫 t3code 的迷你全栈项目,是我最近几个月比较得意的一件事。一开始只是一时冲动,想给手头几个零散的工具做个统一入口,结果做着做着就发现,这套思路特别适合那些“不想把摊子铺得太大,但又需要落地产物”的小体量开发者。这篇文章就把 t3code 从设计到落地的全过程拆开讲清楚,包括我踩过的坑、抠过的细节、以及几个你大概率也会撞上的问题。

1. t3code 这个名字背后的设计思路

1.1 项目到底想解决什么

t3code 听上去像是一个代号,其实拆开看就是“三层代码”。我当时手上零零散散积了一堆脚本和小工具,有处理数据的,有做文件整理的,还有几个给团队内部用的查询接口。它们散落各处,有的跑在别人机器上,有的只是个单文件 Python 脚本,想找个数据还得问同事。t3code 就是一键把这些东西收拢在一起的项目——前端负责给入口,后端负责统一调度,中间用一层清晰的接口把彼此封起来。

所以 t3code 的定位不是“大而全的平台”,而是“小而美的工具箱”。如果你也是一个经常被临时需求打断的开发者,手里攒了很多一次性代码,或者想给团队搭一个轻量级的内部工具集合,这套思路可以直接借鉴。它不强求微服务、不强求 K8s,甚至不强求你写多少单元测试,核心就是把杂乱的代码变成有序的、可维护的、带界面的产品。

1.2 为 t3code 选型时的三个原则

做 t3code 时我给自己定了三条硬指标,也算是对项目“不过度设计”的约束。

第一,依赖最少。能用标准库解决的绝不引第三方包。中间检查了几轮,发现有些地方为了省一点点代码量挂了很大的依赖,实际上完全没必要,把依赖减下去之后打包体积和部署成本都明显下降。

第二,要能一眼看懂。团队里我不是唯一的接手人,任何新成员打开项目在五分钟内能定位到功能对应的代码文件,而不是在一个巨大的目录树里迷路。结构上保持前后端分离的经典布局,但内部按“功能”而非“类型”划分模块。

第三,必须能快速部署。t3code 的使用场景包括临时演示、内部自用,偶尔还要搬到客户的服务器上,所以部署流程被压到两条命令以内。任何需要繁琐配置的东西一律砍掉,哪怕牺牲一点花哨能力。

这三条原则直接决定了 t3code 的全套技术栈。往后的所有决策,我都反复拿它们来检验:这东西是不是必须的?换了别人能不能马上看懂?换一台 Linux 机器能不能跑起来?这其实是一个很好的自查方法,建议你上手任何项目都先写三条自己的原则。

2. 核心细节拆解:三层架构每层该管什么

2.1 为什么一定要分三层

有人说“一个文件把逻辑写完不香吗”,对于一次性脚本确实没问题,但 t3code 的定位是工具集合,很快功能就会从三五个涨到十几个,如果不分层,最后改一个功能就要全局搜索半天。

我拆的三层是这么分工的。

  • 前端层(展示与交互):负责渲染页面、收集输入、把请求发给后端。所有 HTML 模板、静态资源都在这一层。
  • 接口层(逻辑调度):这一层是最重要的一层,负责接收前端的请求,做参数校验、身份判断,然后调用具体功能模块。它不关心业务是怎么实现的,只关心请求是否合法、结果怎么返回。
  • 服务层(具体能力):真正干活的代码,比如数据处理、文件转换、第三方 API 对接,全部装在这一层。每个服务模块保持独立,互不调用。

这种分层最直接的好处是,你想给工具集合增加一个功能,基本就是加一个服务模块,再在接口层加一个路由,前端配一个入口页面。改动能控制在核心位置,不会动不动就牵连原有功能。

2.2 数据存储方案的取舍

t3code 最早是直接用 JSON 文件存储数据,毕竟刚开始数据量小,读写都方便。但功能多了以后,并发读写就出现了问题,两个请求同时写文件会互相覆盖。后来我把存储切换成了 SQLite,理由很直接:它不需要单独安装数据库服务,就是一个文件,完美贴合“快速部署”的约束。

用 SQLite 还有一个隐藏优势——备份特别方便,直接复制 .db 文件就行。我在接口层做了统一的数据库访问封装,这样以后真到了数据量爆炸、必须换 MySQL 或 PostgreSQL 的那一天,只需要改封装层内部实现,所有上层代码都不用动。

2.3 接口设计不想踩坑就靠这几点

接口层是最容易出幺蛾子的地方,我总结了四个约束,t3code 全程遵守下来确实省了很多事。

  • 永远有统一返回格式。不管是成功还是失败,返回 JSON 永远是{ "code": 0, "message": "ok", "data": ... }这样。前端只需要解析一次,会话结束还是代码错误一清二楚。
  • 入参校验不能信任前端。所有参数在后端再做一遍完整性校验,尤其是从 URL 拿到的参数。
  • 接口超时全部设置上限。t3code 里凡是可能调用外部服务的操作,我都设置了超时时间,宁可返回一个“超时请重试”的提示,也不让前端一直傻等。
  • 错误不能只返一个“出错了”。日志里要带上下文,比如出错的是哪个接口、哪个参数、哪一行触发。另外接口层单独开日志文件,不跟业务日志混在一起,排查问题时定位效率高很多。

注意:接口层不仅是“转发请求”,它是你整个程序的安全边界。前面的校验做得多认真,后面的服务层就能多省心。这是我在 t3code 项目里感受最深的一条。

3. 实操过程全记录:从环境准备到功能完整落地

3.1 环境准备这一步我帮你踩过坑了

t3code 的运行时环境很简单:一个比较新的 Node.js 版本(我用的是 20.x),一个包管理器 npm,再加上一个编辑器,Windows、macOS、Linux 都可以。我自己主要在 Ubuntu 服务器和 macOS 上跑,Windows 上用 WSL 跑过一次也没问题。

你可能会问怎么没提数据库,这正是故意为之——SQLite 是嵌入式数据库,不需要提前安装服务,Node.js 的包里面已经带了驱动。这个选择在后续部署时省了非常多的事,你不必在一台新机器上先装 MySQL 再初始化密码才能启动项目。

3.2 初始化项目目录结构

t3code 的目录结构是这样设计的,核心是“看起来像一个大项目,但每个子目录里没有任何杂物”。

t3code/ ├── frontend/ # 前端工程(Vite + React) │ ├── index.html │ ├── src/ │ │ ├── App.jsx │ │ ├── api/ # 封装所有请求 │ │ └── pages/ ├── server/ # 后端工程(Express) │ ├── index.js # 启动入口 │ ├── router/ # 接口层路由 │ ├── services/ # 服务层业务 │ ├── db/ # SQLite 封装 │ └── utils/ └── start.sh # 一键启动脚本

实际我是用命令创建这个结构的:mkdir t3code && mkdir t3code/{frontend,server,server/router,server/services,server/db}。用{a,b,c}这是一种批量创建目录的小技巧,一次建一整组,非常方便。

3.3 后端服务从零写:三步跑通核心接口

t3code 后端我用的是 Express,因为它生态成熟,且没有太多魔法。写一个最简启动入口,代码如下。

// server/index.js const express = require('express'); const app = express(); app.use(express.json()); app.get('/api/health', (req, res) => { res.json({ code: 0, message: 'ok', data: { time: Date.now() } }); }); app.listen(3210, () => { console.log('t3code server running at http://localhost:3210'); });

运行node server/index.js,浏览器访问/api/health拿到 JSON,说明后端活了。要注意app.use(express.json())是必须的,没有这一行的话接口接收前端传来的 JSON body 时,拿到的会是 undefined,这是新手很容易踩的坑。

接下来把路由拆到独立文件里,每个模块的路由自己注册。一个功能模块对应一个 router 文件和一个 service 文件。比如我做一个“统计文本字数”的工具模块,就是 demoRouter 和 demoService,逻辑十分清晰。

// server/router/demo.js const express = require('express'); const router = express.Router(); function validateParams(req, res, next) { const { text } = req.body; if (typeof text !== 'string' || text.length > 1000) { res.status(400).json({ code: 1, message: '参数非法', data: null }); return; } req.validatedText = text; next(); } router.post('/word-count', validateParams, (req, res) => { res.json({ code: 0, message: 'ok', data: { count: req.validatedText.length } }); }); module.exports = router;

看到这个写法,你可能会好奇校验逻辑为什么要放在一个独立的validateParams函数里,而不是直接在路由回调里写。原因是这样可以把“校验参数”和“处理业务”两个关注点分开,将来万一校验规则复杂起来,比如要检查是不是敏感词命中、是否超长、是否空字符串,这些规则都集中在独立的中间件里,改动就非常安全。

3.4 前端页面用 Vite 搭起来

前端我是用 Vite 加的 React,选它的原因就是“快”。npm create vite@latest frontend -- --template react一条命令就完成初始化。然后把请求封装到统一的 api 文件里,这样页面里不直接写 fetch 而是统一走封装好的请求方法。

// frontend/src/api/index.js const BASE_URL = '/api'; export async function request(path, options = {}) { const resp = await fetch(`${BASE_URL}${path}`, { headers: { 'Content-Type': 'application/json' }, ...options, body: options.body ? JSON.stringify(options.body) : undefined, }); const result = await resp.json(); if (result.code !== 0) { throw new Error(result.message || '请求失败'); } return result.data; }

这样做还有个好处是,以后如果想给所有请求自动加 token、加时间戳或者做统一埋点,就只需要改这一个文件。前端页面里调用就非常清爽了:

// frontend/src/pages/Home.jsx import { request } from '../api'; async function handleCount() { const data = await request('/demo/word-count', { method: 'POST', body: { text: 'hello t3code' }, }); setResult(data.count); }

这里有个细节:我在开发时用了 Vite 的代理配置,把/api转发到后端的3210端口,这样前端请求就完全不需要写完整的 URL,最终部署时不管是反代还是直接静态托管,改动都非常小。具体配置见下。

// frontend/vite.config.js export default { server: { proxy: { '/api': 'http://localhost:3210', }, }, };

3.5 数据模块落地:文件型数据的读写防坑

在 t3code 里,有几个工具模块需要保存历史记录。最开始我用fs.writeFile每次全覆盖写入 JSON 文件,很快发现两个请求同时写会丢数据。后来我换成了 sqlite3 的better-sqlite3库,同步写法简单,不需要处理回调。

// server/db/index.js const Database = require('better-sqlite3'); const path = require('path'); const db = new Database(path.join(__dirname, 't3code.db')); db.exec(` CREATE TABLE IF NOT EXISTS task_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_name TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); function insertTask(name) { const stmt = db.prepare('INSERT INTO task_log (task_name) VALUES (?)'); return stmt.run(name); } module.exports = { insertTask };

这里解释一下为什么用better-sqlite3而不是其他数据库库:这个库是同步 API 的,代码写起来像写普通函数一样,不用一层层回调或 async/await。对于一个内部工具项目来说,读写量远没到“需要异步来提速”的程度,同步代码的可读性和出错率比回调风格友好太多。真要未来数据量涨到几十万行,SQLite 只要开启 WAL 模式也照样顶得住日常使用。

3.6 一键启动脚本:从裸机到运行只敲一条命令

t3code 部署时我希望“一条命令起全部”,所以在根目录放了一个start.sh脚本。它做的事很简单:先检查依赖是否安装,如果没装就自动安装,然后启动后端,再用静默方式托管前端构建产物。

#!/bin/bash cd "$(dirname "$0")" echo "[t3code] checking dependencies..." if [ ! -d "server/node_modules" ]; then echo "[t3code] installing server dependencies..." cd server && npm install && cd .. fi if [ ! -d "frontend/node_modules" ]; then echo "[t3code] installing frontend dependencies..." cd frontend && npm install && cd .. fi echo "[t3code] building frontend..." cd frontend && npm run build && cd .. echo "[t3code] starting server..." cd server && node index.js

因为后端在构建好的前端目录,用一句app.use(express.static('../frontend/dist'))就能把静态文件一起托管掉,部署步骤真正压缩到“安装 Node、克隆代码、运行 start.sh、访问页面”。

4. 常见问题与排查技巧实录

4.1 问题速查表:我实测遇到的五个典型故障

做 t3code 过程中遇到不少莫名其妙的问题,这里挑几个有代表性的整理成速查表,你遇到相似问题时可以直接对照。

症状原因解决方法
前端请求接口返回 404Vite 代理没有匹配到/api前缀检查vite.config.js的 proxy 配置
提交数据后接口返回 400后端没有加express.json()中间件在 Express 入口处添加app.use(express.json())
数据写入后重启丢失数据库文件路径写到了临时目录检查 db 路径是否用了process.cwd()而不是__dirname
前端页面打不开后端没有托管前端构建产物用express.static指向frontend/dist
端口被占用上一个 t3code 实例还在运行lsof -i:3210查进程并杀掉

我最想重点提醒的是第二条,这个错误我第一次做时毫无头绪,前端明明把 JSON 传过去了,后端怎么都拿不到 req.body 的内容。折腾了十几分钟才发现就是少了express.json()。这种问题就是典型的“看似玄学,实则是少了一行代码”。

4.2 三个让我拍大腿的避坑经验

第一点是SQLite 数据库路径写死了 process.cwd()。当时我从项目根目录启动没事,但从别的目录启动后,数据库文件被创建到了执行目录下,导致重启后读不到数据。后来我统一用path.join(__dirname, 't3code.db')解决。

第二点是热更新和实际部署的行为差异。开发时 Vite 代理把接口转到后端,部署后静态页面和后端在同一端口服务,但前端页面里如果写死了http://localhost:3210,部署到服务器后就会失效。正确做法是像上面那样只写相对路径/api,域名变化都不用改代码。

第三点是路由文件一多就容易文件顶部相互引用。我写的时候开门见山地要求“功能之间不能互相调用”,后面为清晰就定了这个规则。如果你允许模块互相 import,很快方法的归属就混沌了。一旦发现一个 service 文件里 import 了另一个 service 文件,就要停下来重新思考模块划分。

4.3 从 t3code 里抽象出的通用增量迭代法

项目跑通之后,最让我惊讶的不是功能本身,而是增量迭代的顺畅程度。其实 t3code 的维护模式是可以抽出来的:为每一个新需求建一个需求描述条目,然后“加服务接口、加路由、加前端入口”三步走,一个功能从立项到上线通常也就一小时。

t3code 未来如果能持续更新,我打算把服务层部分从“内置模块”变成“插件模式”,写一个简单的 manifest 声明,加载指定目录下的服务文件。这次的经验证明了一件我一直坚信的事:一套清晰的分层架构 + 克制但完整的技术选型,远比堆砌一堆高深框架更能让你走得远。

我个人实际做下来最明显的感受是,t3code 的价值不在代码量,而在“约束”——先定清楚边界,再填内容,项目就会一直往收敛方向走。如果你手里也有几段一直在角落吃灰的脚本,不妨试着用这套思路给他们安个家,你会看到一个还不错的“工具小产品”是怎么从零长出来的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询