尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

t3code 实践:用“代码优先”理念搭建轻量级低代码生成器

t3code 实践:用“代码优先”理念搭建轻量级低代码生成器 1. 项目起底t3code 到底是什么先说结论t3code 是我在小团队里折腾了大半年的一套低代码方案全称是 Third Generation Code直译就是“第三代代码”。很多朋友第一次听到这个名字都会愣一下这其实不是某个大厂出的商业平台也不是某个开源框架的官方代号而是一套我们自己沉淀的、以“代码优先、可视化为辅”的开发范式。传统低代码平台这两年真的很火拖拽几下就能生成一个管理后台确实爽。但真跑到生产环境里问题就全出来了。业务逻辑复杂一点平台自带的组件根本覆盖不了想自己写点定制逻辑平台又给你锁得死死的代码导不出来逻辑黑盒跑着出了问题只能干瞪眼。我们团队早期踩过这个坑接了一个第三方低代码平台做内部运营后台前两个月确实快得飞起第三个月开始改需求每一个改动都要去平台里找那个藏得很深的配置项有些配置项还不支持表达式只能等平台官方发版本。那种感觉大家体会过就知道有多痛苦。t3code 的思路完全是反着来的。核心资产始终是一份我们自己的代码可视化编辑器只是帮我们生成这份代码的辅助工具。它适合三类人第一类是受够了商业低代码平台锁定、需要完全掌控代码的技术负责人第二类是手里同时管着好几个项目想把手头重复的增删改查页面统一收编的全栈工程师第三类是刚入行不久想搞明白“低代码到底怎么设计”的进阶开发者。注意t3code 不是一个具体的软件包或者开源项目而是一整套可以自建的方法论和工具链这一点在阅读下文之前先明确后面所有内容都围绕这个定位展开。2. 核心设计为什么“代码优先”比“平台优先”更稳妥2.1 传统低代码的四宗罪先说痛点的来源。第一宗罪是锁定效应。你在平台上画的每一个页面、配的每一条逻辑本质上都存成了平台自己的元数据。今天用得好好的明天平台调整收费策略或者停服你的所有资产全部作废。这不只是商业风险更是技术债元数据导出出来是一堆 JSON根本没法直接当代码用。第二宗罪是黑盒调试。业务出问题了你只能看到平台给的错误提示根本看不到内部的调用链路。数据库里慢查询是不是平台生成的 SQL 引起的内存占用为什么一直涨这些问题在传统低代码平台上基本无解因为你拿不到生成的源码。第三宗罪是性能不可控。平台为了兼容所有用户的场景生成的前端包普遍偏大,Gzip 之后动辄一两兆,页面首屏加载被拖得很慢。后端接口也一样通用查询框架什么字段都能查但每个查询都带着一堆用不到的 JOIN数据库压力全被转嫁到底层。第四宗罪是定制成本高。一旦遇到平台组件覆盖不了的需求要么等工作区官方更新要么用平台提供的“后门”写一些极其别扭的脚本维护起来比直接手写代码还费劲。2.2 t3code 的三层架构设计t3code 的设计初衷很简单把“可视化生成”和“手写代码”的通路全部打开。整体架构分三层DSL 定义层用一份结构化的描述文件我们用的是 TypeScript 写的 schema定义数据模型、页面布局、交互逻辑。这一层是可编辑的也是唯一需要长期维护的资产。代码生成层通过自研的生成器把 DSL 编译成标准的前端 React 代码和后端 Node.js 接口代码。生成出来的代码可以直接 commit 进 Git 仓库可以 review、单测、加注释。运行时层不依赖任何私有运行时。前端跑的是标准 React后端跑的是标准 Express/Fastify所有底层技术都是社区主流方案。这个架构最大的好处在于任何一层出了问题你都能降级到源码层面去处理。生成器今天心情不好生成了一坨不太优雅的代码没关系你直接手改生成结果改完就完事不会影响下一次生成。反过来说你手改过的代码如果不想被后续生成覆盖还可以在 DSL 里打一个专属标记生成器会自动跳过对应区域。2.3 关于选型的逻辑技术选型上t3code 选择了 TypeScript 全栈这不是拍脑袋决定的。为什么不用 JavaJava 的后端生态非常成熟但代码生成器在处理动态类型和泛型推导时非常费劲生成出来的样板代码极其啰嗦。在小团队快速迭代的场景下TypeScript 的开发效率有绝对优势。为什么前端选 React 而不是 Vue其实这更多是团队技术栈惯性Vue 也完全可以。关键是生成器只要按约定的组件接口输出 JSX/TSX底层框架是谁根本不重要。为什么自研生成器而不是用现成的代码生成工具现成的工具如 Yeoman、Plop能帮我们搭脚手架但没法理解业务 DSL。我们需要的是一个能理解“这个字段是日期范围需要对应 DatePicker.RangePicker”这种语义的编译器所以必须自己做语义层的解析。从结果来看这个选择最大的收益就是排错成本低。团队成员本来就会写 React、写 Node.jst3code 生成的代码对他们来说和手写的没有本质区别完全不存在“这个代码是机器生成的我不太敢改”的心理负担。3. 从零搭建一个后台管理系统 30 分钟的完整流程空谈架构没有意义我直接带大家走一遍搭建流程。我们拿最常见的“订单管理后台”来举例目标是在 30 分钟内完成从建模到前端页面的全部工作。3.1 环境准备与 DSL 初始化先说明一下t3code 本身是一套我们自己的工具链闭源使用但下述流程中除了生成器其余都是社区标准工具。如果你也想复刻这套流程需要准备Node.js 16、pnpm、一个 Git 仓库、一个 PostgreSQL 实例。初始化项目结构时我用 pnpm workspace 管理了三块apps/web前端、apps/server后端、packages/schemaDSL 定义。mkdir t3code-demo cd t3code-demo pnpm init pnpm create react-app apps/web --template typescript mkdir apps/server mkdir packages/schemaDSL 层最核心的文件是packages/schema/order.ts它描述订单模型。写 DSL 有写代码的感觉但比写 API 和页面简单太多export const OrderModel defineModel({ name: Order, tableName: orders, fields: [ { name: id, type: uuid, primary: true, auto: true }, { name: orderNo, type: string, label: 订单编号, required: true }, { name: customerName, type: string, label: 客户名称, required: true }, { name: amount, type: decimal, label: 订单金额, precision: 10, scale: 2 }, { name: status, type: enum, label: 订单状态, options: [pending, paid, shipped, done] }, { name: createdAt, type: datetime, label: 创建时间, auto: true }, ], listView: { columns: [orderNo, customerName, amount, status, createdAt] }, formView: { fields: [orderNo, customerName, amount, status] }, });看到没有这本质上还是在写代码但写的只是“业务描述”不需要关心数据库连接、分页逻辑、前端表格组件 API、表单校验规则这些通用部分。3.2 代码生成核心步骤与产物生成器是一个命令行工具核心命令就一句cd packages/schema t3code generate它会读取当前目录下所有.ts结尾的 model 定义然后做三件事生成数据库迁移脚本输出apps/server/migrations/xxxx_create_orders.sql里面包含建表语句和索引建议。生成后端 CRUD 路由输出到apps/server/src/routes/order.ts包含GET /api/orders带分页、筛选、排序、GET /api/orders/:id、POST /api/orders、PUT /api/orders/:id、DELETE /api/orders/:id五个标准接口。生成前端页面输出到apps/web/src/pages/OrderList.tsx和OrderForm.tsx前者是带搜索栏和表格的列表页后者是带校验的表单页。生成出来的后端路由长这样简化版你会发现它和你手写的几乎没有区别// apps/server/src/routes/order.ts import { Router } from express; import { prisma } from ../db; export const orderRouter Router(); // 列表接口支持分页、关键字搜索、状态筛选 orderRouter.get(/, async (req, res) { const { page 1, pageSize 20, keyword, status } req.query; const where: any {}; if (keyword) { where.OR [ { orderNo: { contains: keyword } }, { customerName: { contains: keyword } }, ]; } if (status) where.status status; const [list, total] await Promise.all([ prisma.order.findMany({ where, skip: (Number(page) - 1) * Number(pageSize), take: Number(pageSize), orderBy: { createdAt: desc }, }), prisma.order.count({ where }), ]); res.json({ list, total, page: Number(page), pageSize: Number(pageSize) }); });第三方的低代码平台也有类似接口但它是黑盒你没法在中间加一个缓存中间件、没法加一个字段级权限校验。在 t3code 里生成的接口只不过是你自己的代码你想加什么直接改就行。生成前端页面时有一个小细节值得说列表页会默认生成一个“新建”按钮和“编辑”按钮但不生成“删除”按钮。删除操作默认隐藏需要手动打开这是因为后台系统的删除操作实在太容易误触了我们故意在生成器里默认关闭。这也是我们自己踩到一次生产事故之后加进去的默认策略。3.3 业务逻辑扩展代码生成与手写的边界光有 CRUD 肯定不够真实业务总得有专属逻辑。t3code 的约定是生成的代码里凡是标注了// generated的区块下次生成时会被覆盖凡是标注了// handwritten的区块生成器会自动跳过、保留你的改动。举个实际例子。订单创建后要扣减库存这是很典型的业务逻辑。在生成的路由文件里我们直接把逻辑写在POST /接口中// handwritten-start orderRouter.post(/, async (req, res) { const data req.body; // 事务里同时创建订单和扣库存 const result await prisma.$transaction(async (tx) { const order await tx.order.create({ data }); await tx.inventory.update({ where: { skuId: data.skuId }, data: { stock: { decrement: data.quantity } }, }); return order; }); res.status(201).json(result); }); // handwritten-end我第一次跑通这个流程时的感觉是这玩意儿总算不是玩具了。它既给了你效率又给了你掌控力。对团队来说代码评审依然是有效的因为评审对象是真正的代码而不是平台里一个没法 diff 的配置文件。3.4 权限系统约定优于配置后台系统另一个麻烦事是权限。t3code 的做法很朴素生成器会自动扫描所有路由提取操作类型生成一张权限清单。例如// 自动生成无需手写 export const permissions { Order.create: 订单创建, Order.read: 订单查看, Order.update: 订单编辑, Order.delete: 订单删除, };后端加一个简单的中间件每个请求进来时校验当前用户的角色是否包含对应操作码。前端则根据同样的权限清单在渲染按钮时做条件判断。这个方案不复杂但胜在自动同步你多一个接口权限清单会自动多一项不像传统开发模式那样容易漏配。注意这里说的是最基础的 RBAC 模型。如果你的系统里有非常复杂的数据范围权限比如业务员只能看自己的订单生成器只能做接口占位符具体的查询条件还得手写。这是刻意留的边界——数据权限太依赖业务上下文强行做成通用方案反而会带来很大的理解成本。4. 常见问题与排查实录任何工具链都逃不过维护阶段。下面这些问题全是我们在真实项目中踩过的坑整理成速查表方便遇到相同问题时快速定位。问题现象可能原因排查思路解决方案生成代码后页面白屏控制台报Cannot read properties of undefinedDSL 里定义了引用字段但生成器没有正确生成字段间的关联关系检查model定义中的ref字段是否写错了目标模型名重命名目标模型或者给字段加lazy: true让关联按需加载生成的 SQL 迁移脚本执行失败字段类型与数据库方言不匹配比如uuid在 PostgreSQL 里没问题但切到 MySQL 就报错检查生成器的方言配置在defineModel中显式声明dbType: varchar(36)热更新后页面还在显示旧数据前端生成器没有监听 DSL 文件的变化检查 watcher 是否只监听.ts文件而忽略了.yaml等配置文件在 watcher 配置里加上packages/schema/**/*.ts的监听手动改动被下一次生成覆盖手写代码没有包在handwritten注释块里搜索代码中是否出现generated标记将改动挪到handwritten-start和handwritten-end之间接口在本地访问正常部署后 404前端 Nginx 没有配置/api反向代理请求网络面板看返回状态码和后端日志在 Nginx 配置中增加location /api { proxy_pass: http://backend:3000 }列表页搜索非常慢生成了默认模糊搜索没有命中索引打印 Prisma 生成的 SQL查看LIKE语句给搜索字段加index或改用前缀搜索权限接口 403权限缓存未刷新检查权限缓存 TTL 设置修改角色后调用刷新接口或缩短缓存时间排查过程中我最大的体会是80% 的问题都出在 DSL 定义本身而不是生成器。有一回我们页面上的金额字段怎么都显示不出来排查了半天最后发现是 DSL 里把字段类型写成了number但前端表格组件只认decimal导致格式化函数没有触发。这类问题只要写 DSL 时多留意类型定义就能避免。5. 几个影响团队效率的细节调整整套方案用了大半年很多细节直接影响团队日常使用的体感这里分享三个对我们帮助最大的调整。5.1 生成代码也要保持“手写感”最初的生成器代码风格非常机械变量名全是data、res、item看得人头疼。后来我们把生成器里的模板全部改成团队自己的代码风格规范使用有语义的命名orderList不是list、每个函数上方自带 JSDoc 注释、所有 console.log 全部替换成logger.info。这个改动对效率的提升比想象中大得多因为代码评审时大家不会因为“这是机器生成的”就放松标准风格统一能减少很多无意义的争吵。5.2 生成器不支持的功能写成“待扩展钩子”遇到生成器不支持的需求我们的第一反应不是改生成器而是在生成的代码里留一个 hook 文件。比如列表页支持行内编辑这个功能我们一直没有落进 DSL因为业务形态差异太大。我们的做法是在列表页组件里预留一个rowActions插槽谁有需要就在前端代码里自己实现。半年下来这种插槽积累了不少二开能力后面做新项目时再逐步把它沉淀进生成器。5.3 代码生成的时机放在提交前最初我们让开发者在改动 DSL 后立刻执行生成命令但很快发现一个问题生成的代码没有经过人工确认就直接进入 Git容易把一些中间态的半成品提交上去。后来我们把生成动作绑定到pre-commit钩子上每次 commit 时如果检测到 DSL 有变化自动执行生成、自动运行一次 TypeScript 类型检查和 ESLint全部通过之后才允许提交。这样生成的代码永远是经过校验的、和 DSL 同步的。注意这个钩子第一次跑会很慢因为要执行一遍完整的类型检查。但换来的是“提交即可用”的确定性尤其多人协作时能避免一个人改了 DSL、另一个人拉到旧代码导致的前后端不一致问题。6. 聊聊这套方案的边界什么时候不建议上 t3code写到这里我也得泼几盆冷水。t3code 不是银弹它解决的是“大量标准化页面 少量复杂业务逻辑”的问题但在某些场景下老老实实手写代码反而更合适。第一类不建议上的场景是业务模型的字段和关系极其复杂字段之间有很多联动校验、跨模型约束。这种场景下把逻辑塞进 DSL 会异常痛苦生成器也不可避免地要写得越来越复杂最终维护成本远超手写。第二类场景是团队本身没有足够的 Node.js/TypeScript 经验。如果团队是 Java 后端 Vue 前端的组合硬要切到 t3code 这套 TypeScript 全栈方案学习成本和迁移成本会远大于收益。工具链好不好用和团队已有技能栈是否匹配有直接关系。第三类场景是对代码审计要求非常严格的行业比如金融核心系统。生成器毕竟是一个额外的信任依赖如果审计方要求每一行生产的代码都有人工溯源那么机器生成的代码很难满足这种流程要求。我当时给团队定的一条规矩是能用标准化组件解决的就用 t3code需要大量手写自定义交互的页面直接手写不强行套模板。两个区域之间的界限要划得足够清晰不然会变成一个“什么都能生成但什么都难改”的鸡肋。7. 最后分享一个使用技巧作为收尾说一个我在实际使用中觉得最有价值的小技巧给生成的代码打上可追踪标记。我们会在每个生成文件的顶部自动加上一行注释// Generated by t3code at 2025-01-15T10:30:00Z | model: Order | version: 1.2.3别小看这一行注释的价值。当线上出现问题时你可以快速确认当前代码是不是和 DSL 同步的版本。比如接口行为很奇怪但代码看起来没问题看一眼版本号发现生成时间是三天前而 DSL 是今天改的问题就迎刃而解——生成的代码过期了。另外这个标记还有一个隐藏好处生成器会记录模型定义的版本号一旦 DSL 变更生成的代码版本号不对pre-commit钩子会直接拦截不允许把新旧混用的代码提交上去。这个小机制为我们省掉了大量“怎么代码和设计稿对不上”的沟通成本。t3code 这套方案我们团队到现在已经跑了大半年生产环境里管理着二十多个内部系统和两个对外业务后台。踩过坑也填过坑目前来看它在一个 5 到 15 人的技术团队里确实能把“重复的 CRUD 开发”压缩到极低的成本边界,同时不牺牲架构的透明度和可维护性。如果你也正在被商业低代码平台的封闭性和不可控性困扰可以尝试在自己的项目里沉淀一套类似“代码优先”的轻量工具链不要追求一步到位先从自己的业务模型里抽三五个高频页面跑通闭环再逐步放量你会发现这种模式带来的掌控感和传统低代码完全不同。
返回列表