资讯动态

t3code:面向中后台项目的前端工程骨架与分层实践

发布时间:2026/10/27 6:56:16 来源:尧图企业网站定制
t3code 是我最近整理的一套前端工程骨架项目的代号。说白了它不是某个开源框架而是我把这几年在多个中后台项目里反复踩坑、反复重写的那套代码组织方式沉淀成了一个可以直接复制粘贴的起点。如果你打开一个项目第一反应是“目录怎么分”“请求放哪”“状态管理用啥”“测试怎么落地”这些问题都让人头疼那 t3code 就是冲着解决这些事来的。这篇文章我会把它的核心设计、每个模块的取舍理由、具体落地步骤以及我实际跑项目时踩过的坑一次性写清楚。1. 项目整体设计与思路拆解1.1 t3code 到底解决什么问题先说背景。我接手过的中后台项目十有八九一开始都是“能跑就行”但跑到第 6 个月就开始难受一个接口的返回结构改了全局搜出来十几个地方要跟着改某个页面的状态逻辑复杂了组件里堆了二三十个 useState代码根本没法review新同学入职光搞懂目录结构就要三天。t3code 的核心思路是提前用一套相对固定的规范把“代码往哪放”和“数据怎么流”这两件事定死。所谓 t3我自己的解释是 Three-Tier Code也就是把代码强制分成三层UI 展示层、业务逻辑层、基础设施层。这三层之间只能单向依赖——UI 可以调用业务逻辑业务逻辑可以调用基础设施反过来不行。这个约束通过目录结构和 import 规范双重保证比单纯靠“自觉”靠谱得多。UI 展示层只放组件、页面、样式不直接碰请求和 localStorage。业务逻辑层放领域状态、接口调用封装、数据处理、权限判断。基础设施层放 HTTP 客户端、存储封装、日志、工具函数。这样拆完以后绝大多数“一个改动引发连锁反应”的问题都被挡在这一层隔离带上。比如后端接口字段改名理论上只需要改业务逻辑层的类型定义和数据转换函数UI 层完全不动。1.2 为什么不是直接用现成脚手架有人会问现在 create-react-app、Vite 官方模板、Next.js 模板都挺成熟直接再套一层不是多此一举吗我的答案分两层。第一脚手架解决的是“项目能不能跑”不解决“项目好不好维护”。Vite 模板默认给的是最精简结构src 下面一个 components 一个 App.tsx 就完事了。业务一复杂照样会变成大杂烩。第二市面上不少企业级脚手架比如一些内部 CLI为了兼容各种场景集成了很多你可能根本用不到的配置反而给新人造成理解负担。t3code 的定位是“极简骨架 强约束规范”只保留一个中后台项目 80% 场景下都会用到的东西剩下 20% 按需手动加。选择取舍上我这里明确了几条原则不用 CSS 方案上的激进派选 CSS Modules 作为默认零额外依赖、无运行时成本。TypeScript 的 strict 模式从第一天就打开不允许编译期留 any。状态管理默认用轻量的纯 TypeScript 实现不引 Redux避免模板代码起飞。所有异步逻辑统一走 react-query 风格的请求封装缓存、重试、取消都交给同一个工具处理。这套组合不是“全网最新”而是“最不容易出幺蛾子”。对我这种需要同时维护四五个项目的状态来说稳定性优先于新潮。1.3 设计目标与最终交付形态t3code 的最终形态是一个仓库模板解压以后包含t3code/ ├── src/ │ ├── app/ # 应用级配置路由、入口 │ ├── ui/ # 纯展示组件 │ ├── features/ # 业务模块按领域划分 │ ├── lib/ # 基础设施层 │ └── shared/ # 全局共享类型与常量 ├── config/ # vite、eslint、tsconfig 等配置 ├── scripts/ # 工程脚本校验、构建、分析 └── package.json复制这个结构以后你拿到的不只是目录而是一套约定什么文件放什么目录目录之间怎么引用公共代码怎么抽类型声明放哪测试文件跟源码怎么摆。这套约定写进 README 以后团队协作时大家省掉的沟通成本非常可观。2. 核心细节解析与实操要点2.1 分层目录的边界定义很多项目不是不想分层而是分不清楚“业务逻辑”和“基础设施”到底有什么区别。我给 t3code 定了一个很朴素但好用的判别标准如果这行代码换一个框架就要重写它就是基础设施如果换一个业务场景就要重写它就是业务逻辑。举个例子。一个request函数封装了 fetch、超时处理、错误拦截、token 注入它跟具体业务没有任何关系这就是基础设施放src/lib/http.ts。而getUserOrderList这个函数定义了请求参数、返回类型、错误码映射它知道“订单列表”这个业务概念放src/features/orders/api.ts。再比如日期格式化。formatDate是纯工具放src/lib/format.ts。但是formatOrderStatus就不一样了它要把后端返回的1 | 2 | 3翻译成“待付款”“已发货”“已完成”还要配合按钮权限这属于业务规则放src/features/orders/constant.ts。UI 组件也是同样的逻辑。一个Button、一个Table不依赖业务放src/ui。而OrderTable这种“拿到订单数据并展示”的聚合组件放src/features/orders/components里更合适因为它和订单业务强相关。2.2 关键文件结构与依赖规则t3code 里最重要的配置文件之一是tsconfig.json我把strict、noUncheckedIndexedAccess、exactOptionalPropertyTypes全部打开。这几个配置初看很烦人但长期看是帮你兜底的。strict不说noUncheckedIndexedAccess会让数组下标访问变成T | undefined逼你在写代码时处理越界情况。这个约束带来的麻烦很小但避免的线上 bug 是真不少。{ compilerOptions: { strict: true, noUncheckedIndexedAccess: true, exactOptionalPropertyTypes: true, noEmit: true, moduleResolution: bundler, target: ES2022 }, include: [src] }目录依赖规则我是在 ESLint 里用import/no-restricted-paths插件强制实现的。比如限制src/ui不能直接 importsrc/features里面的东西src/lib不能反向 importsrc/app。配置长这样{ rules: { import/no-restricted-paths: [error, { zones: [ { target: src/ui, from: src/features }, { target: src/features, from: src/lib } ] }] } }宁可让开发者在提 MR 的时候被 CI 拦住一次也别让这种坏依赖在代码库生根。CI 里拦一次的成本是 5 秒但排查一次循环依赖的 bug可能是一下午。2.3 UI 组件设计的分寸感t3code 中 UI 层组件我只要求两件事受控、无业务副作用。所谓受控就是组件的数据和交互状态一律由 props 传入不在组件内部偷偷拉接口。无业务副作用指的是组件内不能出现路由跳转、弹窗提交、权限判断这类逻辑。比如一个UserSelect组件它只负责“渲染一个用户下拉框选中后把 id 抛出来”。至于这个用户列表从哪来、选中以后要干嘛t3code 要求你在父级业务组件里用useQuery去拿数据再传给UserSelect。这样设计的一个直接好处是这个组件可以在任何业务里复用。另一个好处是测试变得异常简单给 props 传不同数据渲染结果一定可预期。2.4 状态管理的轻量实现t3code 里没有引入 Redux 全家桶而是给了两个轻量方案。服务器状态服务端数据全部交给 TanStack Query 管理它的缓存、轮询、失败重试机制是这个领域最成熟的。业务侧要做的只是写一个 hook 封装// src/features/orders/api.ts export function useOrderList(params: OrderListParams) { return useQuery({ queryKey: [orders, params], queryFn: () fetchOrderList(params), }) }客户端状态UI 开关、表单临时值我建议直接用useStateuseReducer。只有像“全局用户信息”“多步骤表单的中间状态”这种真正需要跨组件共享的才用一个非常轻的createStore工具——本质上就是一个带订阅的全局对象十几行代码搞定没有任何 Redux 的样板代码负担。这个取舍基于一个统计中后台项目里 80% 的全局状态用 Redux 是大炮打蚊子引入它带来的概念负担比它解决的问题还大。3. 实操过程与核心环节实现3.1 从零初始化 t3code 骨架下面记录我实际初始化 t3code 的完整过程包含每一步执行了什么、为什么这么执行。第一步用 Vite 创建一个基础 React TypeScript 项目npm create vitelatest t3code -- --template react-ts这里用 Vite 不是因为它最新而是因为它冷启动快、配置直观。接下来清理掉模板自带的示例代码然后创建目录结构mkdir -p src/app src/features src/lib src/ui src/shared第二步安装核心依赖。我给 t3code 选的依赖非常克制运行时依赖只有 react、react-dom、tanstack/react-query、react-router-dom。开发依赖包括 typescript、eslint、vite、vitest、testing-library/react。你可能会好奇为什么不用 axios因为浏览器原生 fetch 已经够用我只需要一个非常薄的封装层即可。少一个第三方依赖就少一个版本升级的痛苦来源。第三步建立请求封装。这是 t3code 里基础设施层最重要的一个文件我把超时控制、统一错误处理、token 注入全部集中在一个地方写// src/lib/http.ts const BASE_URL import.meta.env.VITE_API_BASE_URL ?? /api export class HttpError extends Error { constructor( message: string, public status: number, public code?: string, ) { super(message) } } export async function requestT(path: string, options: RequestInit {}): PromiseT { const controller new AbortController() const timer setTimeout(() controller.abort(), 10000) try { const res await fetch(${BASE_URL}${path}, { ...options, headers: { Content-Type: application/json, Authorization: Bearer ${localStorage.getItem(t3_token) ?? }, ...options.headers, }, signal: controller.signal, }) if (!res.ok) { const body await res.json().catch(() null) throw new HttpError(body?.message ?? 请求失败${res.status}, res.status, body?.code) } return (await res.json()) as T } finally { clearTimeout(timer) } }这段代码最核心的地方是AbortController的使用。没有超时控制的 fetch 请求在网络卡顿的情况下会一直挂着用户等不到反馈体验极差。10 秒超时是经过几个项目实测后折中的值太短容易打断正常大列表查询太长用户耐性已经耗光了。3.2 一个完整业务模块的落地过程为了说清楚这套骨架怎么用我以“订单列表”这个最常见的业务场景为例走一遍完整流程。第一步在src/features/orders/目录下创建类型定义。订单在后端返回的结构和前端展示需要的信息往往不完全一致所以我会定义一套领域类型不直接把后端类型透传到组件里// src/features/orders/types.ts export interface OrderItem { id: string orderNo: string customerName: string amount: number status: pending | paid | shipped | completed | cancelled createdAt: string } export interface OrderListParams { page: number pageSize: number status?: OrderItem[status] }这里有个设计细节status字段我用了字符串字面量的联合类型而不是直接string。这样写的好处是后续写switch分支时 TypeScript 能穷尽所有 case不怕漏掉某个状态不处理。第二步写数据请求函数。t3code 里所有请求函数都放在api.ts中并且统一通过http封装发请求// src/features/orders/api.ts import { request } from /lib/http import type { OrderItem, OrderListParams } from ./types interface OrderListResponse { list: OrderItem[] total: number } export function fetchOrderList(params: OrderListParams) { return requestOrderListResponse(/orders, { method: GET, // query string 拼接由 request 内部处理 params, }) }第三步写业务 hook。这一层把请求、状态管理、缓存、错误处理封装成一个对组件极友好的接口。组件不需要关心请求怎么发只需要消费这个 hook 的返回值// src/features/orders/useOrderList.ts import { keepPreviousData, useQuery } from tanstack/react-query import { fetchOrderList } from ./api export function useOrderList(params: OrderListParams) { return useQuery({ queryKey: [orders, params], queryFn: () fetchOrderList(params), placeholderData: keepPreviousData, }) }keepPreviousData是我强烈建议打开的配置。它会在页面切换时保留上一页的数据作为占位新数据到了以后自动替换。没有它的话表格每次翻页都会先闪一下 loading体验会打折扣。第四步写页面组件。页面组件只做三件事调用 hook、渲染加载态/错误态、把数据传给子组件// src/features/orders/pages/OrderListPage.tsx export function OrderListPage() { const [page, setPage] useState(1) const { data, isLoading, isError, error, refetch } useOrderList({ page, pageSize: 10 }) if (isLoading) return Spin / if (isError) return ErrorState message{error.message} onRetry{() refetch()} / return ( div classNamepage-container OrderTable data{data.list} / Pagination current{page} total{data.total} onChange{setPage} / /div ) }3.3 配置路径别名与工程脚本路径别名是我做所有项目的第一步配置。t3code 默认支持/指向src/这个配置在 Vite 和 TypeScript 里各有一份少了哪边都会出问题。Vite 那边如果不配运行时直接报模块找不到TypeScript 这边如果不配编辑器里 import 会飘红虽然也能编译但类型检查过不了。// vite.config.ts import { fileURLToPath, URL } from node:url export default defineConfig({ resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)), }, }, })// tsconfig.json { compilerOptions: { baseUrl: ., paths: { /*: [src/*] } } }工程脚本方面我在 package.json 里加了几个实用命令scripts: { dev: vite, build: tsc --noEmit vite build, preview: vite preview, lint: eslint . --max-warnings 0, typecheck: tsc --noEmit, test: vitest run, check: npm run lint npm run typecheck npm run test }注意build脚本里先跑tsc --noEmit再做打包这一步可以把绝大多数类型错误挡在产物生成之前。很多项目构建速度快是因为跳过了类型检查但代价是类型错误可能被带到线上运行时才发现得不偿失。4. 常见问题与排查技巧实录4.1 目录分层变成空谈怎么办t3code 这套结构最大的敌人不是技术而是团队的惰性。我见过很多项目刚开始分层很干净三个月以后有人图方便直接在组件里写了 fetch然后这个先例一开大家纷纷效仿分层名存实亡。我的方案是用两条腿走路。第一条腿是 ESLint 规则拦截前面提到的import/no-restricted-paths能挡住跨层 import 的坏味道。第二条腿是 Code Review 时的高压线声明看到src/ui目录里有useQuery或fetch的直接打回。这不是不近人情而是这类违规代码一旦进去后面想拆就很麻烦。实际执行中你会发现开发者的第一反应往往是“我就临时在这里拿一下很快改”。这句话我听了太多次十个里有八个没改。所以现在我对这种请求零容忍宁可让他多花十分钟改对也不留债给以后。4.2 接口类型定义冲突怎么处理另一个高频问题是后端接口字段变了前端类型怎么同步。t3code 的答案是业务层类型必须和 UI 层解耦。我们定义一套前端领域的类型比如OrderItem后端返回的原始结构在 api.ts 层做一次映射转换。// api.ts interface BackendOrderDTO { id: number order_no: string customer_name: string total_amount: string status_code: number } export async function fetchOrderList(params: OrderListParams): PromiseOrderItem[] { const data await requestBackendOrderDTO[](/orders, { params }) return data.map((item) ({ id: String(item.id), orderNo: item.order_no, customerName: item.customer_name, amount: Number(item.total_amount), status: mapStatusCode(item.status_code), })) }好处很明显后端怎么折腾都不会直接污染组件层。字段改名了只改 api.ts 这一处金额单位从分变成元也只改这一处的转换逻辑。这个映射层看起来像是多写了代码但它把“变化”集中到最小范围内是被动维护成本最低的方案。4.3 构建性能变差怎么排查t3code 初始状态的构建速度很快但随着业务增加某些配置可能会导致变慢。我遇到过最多的情况是路径别名加了十来条结果 Vite 的 resolve 逻辑变慢或者 tsconfig 的 include 范围没限制住把 node_modules 里的类型也拉进来解析了。排查方法就一句话构建的时候开debug模式看哪一步耗时最长。DEBUGvite:resolve npm run build还有一个我常用的技巧在 Vite 配置里加build.reportCompressedSize: false这个选项关掉以后构建时会少算一次 gzip 压缩大小大项目能省下好几秒时间。如果你真的需要看产物体积单独用rollup-plugin-visualizer去分析不要在每次构建时都跑一遍。4.4 常见问题速查表为方便查阅我把 t3code 使用中常见的问题汇总成表格问题现象可能原因解决办法组件里 import/lib/http后 lint 报错跨层依赖被限制把请求逻辑挪到对应 feature 的 api.ts 中数组访问报可能为 undefinedtsconfig 开了noUncheckedIndexedAccess显式做空值判断而不是直接调arr[0].xxx页面翻页时表格闪烁没有开启placeholderData在 useQuery 中加placeholderData: keepPreviousData接口超时没有提示fetch 未设置 AbortController检查 http.ts 超时配置统一纳入封装后端字段改了编译不报错类型是手动复制且散落多处收敛到 api.ts 做 DTO 映射类型只在业务层定义一份ESLint 规则改了不生效改的是.eslintrc但没重启 IDE重启 ESLint server或者命令行跑一次验证5. 一些实际操作中沉淀下来的体会5.1 骨架是起点不是终点t3code 只是把一些通用问题提前解决了不代表业务上的所有问题都不用思考。比如权限模型、多租户数据隔离、复杂的表单联动这些还是得靠业务上的设计和团队内部的讨论。骨架的意义是让你在思考这些问题时不用再背着“这项目结构和屎山一样”的包袱。我自己的实践体会是t3code 最适合的切入时机是新项目启动的第一周以及旧项目即将进入大规模重构的前期。这时候定好规矩收益最大。一个已经跑了两年、几千个文件的存量项目想硬套这套结构会非常痛苦不建议在没有充分准备的情况下去硬迁移。5.2 规范要写进文档更要写进工具这个观点我在多个场合反复提过文档只能约束愿意看文档的人工具才能约束所有人。t3code 把关键规范固化在 ESLint、tsconfig、CI 脚本里就是为了让“正确做法”成为默认行为让“错误做法”在第一时间被拦截。哪怕团队里有完全的新人只要他能让代码通过npm run check产出就已经在规范内了。这个思路也适合任何想在自己团队里推规范的人。你不需要从零发明一套 t3code只需要把自己项目里“约定俗成但总有人违反”的规则哪怕只是三五条沉淀成工具检查项就已经能显著降低协作成本。5.3 后续可以扩展的方向目前 t3code 默认不包含 mock 方案因为不少团队有自己的接口平台或者后端已经就绪。如果你需要前端独立开发可以加一层 MSWMock Service Worker在请求层做拦截效果非常接近真实网络环境。另外一个值得扩展的是权限路由。我建议做一套轻量的路由守卫基于当前用户角色动态过滤可访问的路由表这个和 UI 层解耦以后新加页面只需要在路由配置里标注权限码就行没有额外的样板代码。最后再分享一个小经验这套骨架里的任何一条约束如果你觉得它在某个场景下确实不合适那就改掉它但改之前要在团队里说清楚“为什么”。规范最大的敌人不是不完美而是不明不白。经过讨论决定的例外好过默默遵守的不理解。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑