ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

t3code:基于前后端同构的全栈项目模板与代码生成器实战解析

t3code:基于前后端同构的全栈项目模板与代码生成器实战解析

1. 项目缘起:为什么我会想做一个叫 t3code 的东西

t3code 这个名字,第一次看到的人可能会觉得有点奇怪。t3 是什么?code 又代表着什么?其实这里面的逻辑很简单,我在实际开发中反复遇到一个痛点:每一次启动新项目,都要花大量时间在环境配置、目录结构搭建、基础组件封装、请求层设计这些事情上。不同项目的初始化方式还不一样,有的用这个脚手架,有的用那个模板,时间长了之后,维护成本越来越高,团队里每个人的项目结构也五花八门。

所以我决定做一件事,把过去几年里踩过的坑、沉淀下来的最佳实践,全部固化到一个统一的工程模板里。这个模板我给它取名叫 t3code,寓意是“三端贯通、一套代码打天下”。所谓三端,指的是 Web 端、移动端和桌面端;所谓 code,就是把这套能力抽象成一整套可直接复用的代码基础设施。t3code 本质上是一套基于前后端同构思想的全栈项目模板,同时自带了代码生成器和在线编辑器能力,目标是让开发者能从零开始,在十几分钟内拉起一个具备完整工程能力的前端应用。

这个项目适合谁来用?简单来说,适合那些不满足于“只会用脚手架”的开发者,尤其是需要对项目结构有完全掌控力的团队。也适合独立开发者,一个人要维护多个项目,如果不把基础设施统一起来,迟早会被重复劳动拖垮。如果你正在做中后台系统、小程序 H5、桌面端工具类应用,t3code 这套方案可以直接帮你省掉前期最痛苦的搭架子阶段。

2. 整体设计思路与技术选型

2.1 前后端同构:一个模型,三端复用

t3code 的核心思路是前后端同构。大部分传统项目的做法是前端一套代码,后端一套代码,两边的数据模型、校验逻辑、接口定义各写各的,一旦业务规则发生变化,经常出现两边不同步的情况。比如后端改了某个字段的校验规则,前端忘了改,线上就会报出一堆奇怪的错误。t3code 的做法是把这些共享的部分单独抽取出来,形成一个“共享核心层”,前端和后端都从这一层派生自己的代码。

实际的目录结构是这样的:

t3code/ ├── apps/ │ ├── web/ # Web 端应用 │ ├── mobile/ # 移动端应用 │ └── desktop/ # 桌面端应用 ├── packages/ │ ├── core/ # 核心数据模型、工具函数、常量定义 │ ├── ui/ # 跨端 UI 组件库 │ ├── api/ # 接口请求层,统一的 HTTP 客户端封装 │ └── cli/ # t3code 命令行工具 ├── templates/ # 项目模板目录 ├── generator/ # 代码生成器 └── docs/ # 文档

packages/core 是整棵树的根。所有数据模型、枚举、校验函数都放在这里,Web、移动端、桌面端共享这一份代码。这样做的好处是什么?假设业务里有一个订单状态枚举,传统的写法是前端一份、后端一份、数据库注释里再写一份。在 t3code 里只需要定义一次,三个端全部从这里引用。后续要加一个状态,改一处,三端生效,不会出现“前端已经加了,后端还在报错”这种尴尬局面。

2.2 技术栈选型的底层逻辑

t3code 的技术选型没有追逐最新、最热的东西,而是选了一套符合“长期维护”需求的组合。Web 端采用 Vue 3 + TypeScript + Vite,移动端采用 Taro 来编译到微信小程序和 H5,桌面端采用 Electron 封装逻辑。后端虽然是模拟数据为主,但预留了 Node.js 中间层的接口结构,方便后续接入真实后端。

为什么选 Vue 3 而不是 React?没有绝对的对错。我的理由是这样的:Vue 3 的组合式 API 在代码组织上更接近于“按功能划分”而不是“按生命周期划分”,对中小团队来说更容易上手。Taro 在跨端编译这件事上做得比较成熟,一套代码可以同时出微信小程序和 H5,显著降低移动端的维护成本。Electron 则是当前桌面端唯一比较靠谱的选择,虽然体积是个老大难问题,但它的生态最完善,遇到问题查资料也容易。

还有一个关键决定:全链路使用 TypeScript。这里我需要多说一句,TypeScript 不是在给你增加工作量,而是在给你的项目上保险。在 t3code 的设计里,所有 API 请求的入参和出参都有完整类型定义,前端调用接口的时候,参数错了编译期直接报错。这比运行时炸出一个红色错误提示要舒服得多。

2.3 单仓库管理模式

t3code 采用了 monorepo 的单仓库管理方式。刚开始做这个项目的时候,我自己也纠结过要不要把 Web、移动端、桌面端拆成三个独立的仓库。后来实践下来发现,拆开之后问题更多:跨端共享的代码没办法直接引用,只能发 npm 包,而发包的版本管理又是一堆破事。最终决定用 pnpm 的 workspace 功能,把三端放在一个仓库底下管理。

pnpm workspace 的优势主要有三点:

  • 依赖安装速度快,硬链接机制让同一份包不会被重复下载
  • 严格的项目隔离,每个子包都有自己的依赖声明,不会出现“这个应用能跑是因为另一个应用装了某个依赖”这种隐患
  • 本地开发的包之间可以直接引用,core 包改了代码,三端立即生效,不需要发版

这套方案在公司内部实践了大半年,稳定性一直很好。

3. 核心功能拆解:t3code 到底能做什么

3.1 代码生成器的完整流程

t3code 自带一个代码生成器模块,解决重复造轮子的问题。它的核心能力是:你只需要定义一个数据模型,它就能帮你生成一整套增删改查的页面代码。注意,这里不是那种简单的“生成一个表单”,而是从数据模型定义开始,生成完整的表格页、表单页、接口调用层、类型定义、前端状态管理等全套代码。

生成器的工作流程分四步:

  1. 解析用户编写的 schema 文件(JSON 格式)
  2. 根据 schema 生成 TypeScript 类型定义和数据校验函数
  3. 生成基于 prisma schema 的后端 mock 数据服务
  4. 生成前端列表页、表单页的 Vue 组件代码,并自动注册路由

举一个例子,假设要做一个用户管理页面,只需要在 schema 文件里这样定义:

{ "modelName": "User", "fields": [ { "name": "id", "type": "string", "primary": true }, { "name": "name", "type": "string", "required": true }, { "name": "email", "type": "string", "required": true }, { "name": "status", "type": "enum", "enum": ["active", "disabled"] } ] }

执行t3code gen user之后,生成器会产出以下文件:

src/ ├── api/ │ └── user.ts ├── components/ │ └── user/ │ ├── UserList.vue │ └── UserForm.vue ├── types/ │ └── user.ts └── router/ └── user.ts

生成的列表页默认支持搜索、筛选、分页;表单页默认带校验逻辑。在这个基础上,你只需要根据自己的业务场景做一些微调,不需要从零开始写。我实测过,一个简单的单表管理页面,从定义 schema 到页面能跑起来,大概三分钟就够了。

3.2 请求层封装的细节设计

请求层是所有前端项目最容易出问题的地方。t3code 里对这个模块做了非常细致的封装,解决的核心问题是统一鉴权、统一错误处理、统一响应格式。

请求层的设计思路是:封装一个http方法,内部处理 token 的注入、错误码的拦截、超时重试等逻辑。上层接口模块只需要明确自己的参数和返回类型,不需要关心这些细节。

// packages/api/src/http.ts import axios from 'axios' const http = axios.create({ baseURL: import.meta.env.VITE_API_BASE ?? '/api', timeout: 15000 }) http.interceptors.request.use((config) => { const token = getToken() if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) http.interceptors.response.use( (response) => { const res = response.data if (res.code !== 0) { // 统一业务错误处理 notifyError(res.message) return Promise.reject(new Error(res.message)) } return res.data }, (error) => { if (error.response?.status === 401) { // 登出并跳转登录页 clearToken() redirectToLogin() } notifyError('网络请求异常,请稍后重试') return Promise.reject(error) } )

有一个细节值得单独拿出来说:接口出参的类型安全。t3code 强制要求每个接口函数都要有明确的入参出参类型,不允许出现 any。这个约束在刚开始的时候会增加一些工作量,但维护了半年之后你会发现,你改一个接口的返回结构,所有用到这个接口的地方,编译期就会把所有需要调整的地方全部暴露出来。这种安全感是运行时测试给不了的。

3.3 路由与权限控制的联动机制

权限控制是后台管理系统里绕不开的坑。t3code 采用了一种比较实用的方案:前端权限基于路由守卫判断,后端权限基于 mock 数据模拟,两者之间的角色信息通过统一状态管理同步。

具体实现上,路由配置里每个路由可以声明meta.roles字段,表示哪些角色可以访问。路由守卫在跳转前检查用户角色是否在允许列表中:

router.beforeEach((to, _from, next) => { const userStore = useUserStore() if (!userStore.isLoggedIn) { if (to.path === '/login') { next() } else { next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) } return } const roles = to.meta.roles as string[] | undefined if (roles && !roles.some((r) => userStore.roles.includes(r))) { next('/403') return } next() })

这套方案的优点是直观、好理解,适合绝大多数中后台项目。如果你的项目权限粒度非常细,需要控制到按钮级,建议在此基础上再加一个自定义指令做细粒度校验,但基础的页面级权限控制用这套方案已经足够稳定了。

4. 从零到一:使用 t3code 搭建一个完整项目

4.1 环境准备与初始化

要上手 t3code,你本机需要有 Node.js 18 以上版本和 pnpm 包管理器。安装命令用的是 pnpm,你直接执行:

pnpm add -g t3code

全局安装完成后,可以用t3code --help查看所有可用指令。初始化的核心命令是 create:

t3code create my-project

这个命令会询问你几个基础问题,包括项目名称、需要启用哪些端(web/mobile/desktop)、UI 风格偏好等。回答完之后,CLI 会基于 templates 目录下的预设模板,复制一份完整的工程代码到你的目标目录里,然后自动执行依赖安装,把基础环境跑起来。

这一步最贴心的是:初始化完成之后不会让你对着一个空壳子发愁,而是直接生成一个带示例页面的可用项目。你打开浏览器,能看到的不是一个 hello world,而是一套完整的布局框架,包含侧边栏、顶栏、用户信息下拉菜单和三个示例页面(首页、列表页、表单页)。这样你可以先去了解整个项目的代码组织方式,再动手改。

4.2 核心构建流程演示

项目初始化之后,下一步通常是把示例代码改成自己的真实业务。我拿列表页改造这个场景来演示完整流程。

假设你要做一个“项目管理”页面。第一步,定义数据模型。打开schema/project.json,写清楚字段定义:

{ "modelName": "Project", "fields": [ { "name": "id", "type": "string", "primary": true }, { "name": "name", "type": "string", "required": true }, { "name": "owner", "type": "string", "required": true }, { "name": "status", "type": "enum", "enum": ["planning", "running", "done"] }, { "name": "deadline", "type": "date" } ] }

然后在项目根目录执行:

t3code gen project

生成器会产出新的接口层、组件和路由,业务代码自动挂载到应用里。打开侧边栏,菜单里已经多了“项目管理”这个入口。点进去,左侧是筛选条件区,右侧是数据表格,支持搜索和分页。你可能需要根据自己的业务流程改一些逻辑,但骨架已经全部齐了。

接下来是登录对接。t3code 自带的登录页面默认连的是 mock 服务,登录接口的逻辑在apps/web/src/api/auth.ts里。切换到真实后端的时候,只需要改这个文件里的请求路径,并确认响应格式和封装层保持一致就可以。

4.3 构建、测试与部署建议

项目开发完成后,t3code 提供了统一的构建命令。因为是 monorepo 结构,构建的时候需要注意依赖的构建顺序。如果直接用 pnpm 递归构建,core 包还没编译完就去构建 web,会找不到产物。t3code 的 CLI 内置了拓扑排序逻辑,会自动先构建依赖,再做应用构建:

t3code build web

构建产物会输出到apps/web/dist目录,你把它丢到任意静态服务器,或者 Nginx、对象存储的静态网站托管服务上都能跑。部署的关键点在于接口请求地址。t3code 通过环境变量来区分不同环境:

# .env.production VITE_API_BASE=/api

如果你是把前端部署在某个域名的子路径下,还要注意配置base路径,避免静态资源 404。这些都是小问题,但真到了上线那天,每一个都能让你折腾半小时起步。所以建议在项目开发第一天就把这些环境配置梳理清楚,后续能省不少事。

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

5.1 依赖安装失败的排查思路

用 t3code 初始化项目最常遇到的问题就是依赖安装失败。这类问题绝大多数出在网络环境和 pnpm 的版本上。我自己的排查顺序是:

  1. 先看 pnpm 版本是否符合要求。t3code 要求 pnpm 8 以上,版本太老会导致 workspace 协议解析失败。
  2. 查看 registry 配置。如果你之前设置过镜像源,偶尔会出现镜像站同步不及时的情况。可以临时切换回来试一下。
  3. 删除 node_modules 和 pnpm-lock.yaml,重新安装。这能解决相当一部分玄学问题。
pnpm clear rm -rf node_modules pnpm-lock.yaml pnpm install

有一个细节值得注意:如果你修改了某个子包的依赖,记得在这个子包目录下单独执行pnpm install,而不是直接在仓库根目录执行。直接在根目录装会导致子包的依赖声明被 workspace 的根依赖覆盖,出现非常难排查的版本冲突。

5.2 跨端运行表现不一致的问题

t3code 的核心卖点是一套代码跑三端,但跨端的坑是客观存在的,尤其是移动端编译到小程序的时候。最常见的坑是 Web 端的 DOM API 在小程序里不存在。比如window.localStorage,在 Web 端可以正常用,在小程序里会直接报错。

t3code 在 core 包里封装了一个跨端存储模块,统一处理三端的存储差异:

// packages/core/src/utils/storage.ts export const appStorage = { get(key: string): string | null { if (process.env.TARO_ENV === 'weapp') { return Taro.getStorageSync(key) } return localStorage.getItem(key) }, set(key: string, value: string): void { if (process.env.TARO_ENV === 'weapp') { Taro.setStorageSync(key, value) } else { localStorage.setItem(key, value) } } }

这类问题只要做到了统一封装层,基本都能解决。怕就怕在代码里到处直接调用平台 API,一旦散乱了,排查起来就非常痛苦。所以我的经验是:所有跨端 API 的使用必须收敛到一个公共模块里,禁止在业务代码里直接调平台能力。

5.3 模块热更新失效的快速恢复方案

开发模式下偶尔会遇到 Vite 的热更新失灵。现象是改了代码之后,页面没有自动刷新,比如新增路由后,左侧菜单没有同步变化。大多数情况下是因为新增文件时模块依赖图发生了变化,Vite 需要重启才能重新识别。

遇到这种情况,你不需要手动重启开发服务。t3code 脚手架里内置了一个脚本,会自动清理缓存并重启 Vite 进程:

t3code dev --fix-hmr

建议把热更新失灵当成正常现象来对待,遇到之后不用焦虑。前两次可能还会觉得项目出问题了,后面就很自然地跑一下修复命令继续写代码。

5.4 常用命令速查表

命令作用使用场景
t3code create <name>创建新项目初始化项目
t3code gen <model>根据 schema 生成代码新增一个业务模块
t3code dev启动开发服务日常开发
t3code build构建生产环境产物提测或发布
t3code lint执行代码检查提交代码前的自检
t3code dev --fix-hmr修复热更新失灵问题开发中遇到 HMR 不生效

6. 在线编辑器扩展:让 t3code 脱离本地环境

t3code 还有一个额外的功能模块:在线代码编辑器。这个模块不是核心卖点,但作为补充能力,它解决了一个实际使用场景里的问题——当你在演示项目、临时改代码,或者手边没有本地开发环境的时候,可以快速打开一个云端版本的 t3code 工程。

在线编辑器的基础技术栈组合是 Monaco Editor 加 WebContainer 模拟 Node.js 环境。核心交互逻辑是:左侧文件树,中间代码编辑区,右侧实时预览窗口。这里的实现关键是文件系统协议的统一封装,不是在文本编辑器里简单拼接字符串,而是通过虚拟文件系统操作真实的项目目录结构。

实测下来的体感是:代码提示比本地 IDE 会慢一些,尤其在项目大了之后,内存占用会显著上升。所以在线编辑器适合做轻量级使用场景:演示、修改配置文件、临时调样式。重度开发还是建议老老实实用本地的 t3code 脚手架。

7. 一些实操过程中的真实心得体会

t3code 做到现在,我最满意的地方不在于它的代码质量,而在于它改变了我的工作方式。我现在的日常开发流程变成了:接到需求,先花十分钟想清楚数据模型,然后跑一次t3code gen,剩下的时间全部花在业务逻辑的打磨上。项目结构的统一也让我维护多个项目的时候轻松了很多,因为不管哪个项目,打开文件树的瞬间我都知道自己该去哪找什么。

最后再分享一个写给新手的建议。如果你是第一次接触 monorepo 架构,可能会被多层级的目录结构劝退。这个非常正常,我刚开始也很不适应,光搞明白 packages 底下那堆依赖声明之间的关系就花了不少时间。但我的经验是:先不要死磕架构的每个细节,直接跟着 t3code 初始化一个项目,把示例代码跑起来,然后找一个简单模块,用生成器生成一遍,改一遍。跑完这两个流程之后,你再看这套架构的代码组织,会发现自己已经能理解了。工具始终是拿来用的,用起来之后,一切都比想象中简单。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进