☰
t3code代码生成实践:模板+数据+渲染三件套详解
2026/10/26 17:42:33 网站建设 项目流程

1. 从“t3code”这个名字说起:它到底是什么

第一次看到“t3code”这个词,很多人会愣一下——它不像某个具体框架的名字,也不像某个大厂的开源项目代号。我最初接触到它,是在一个做低代码平台的朋友那里,他随口提了一句“我们内部把 t3code 这套东西跑通了”,当时我没太在意,后来越来越多的人在讨论这个词,我才意识到它背后代表的是一类相当实用的技术思路。

t3code 本质上是一套围绕“代码生成”与“模板驱动”的轻量级工程实践方案。名字里的“t3”可以理解为 three-tier(三层)或者 template-driven triple(模板驱动三件套)的缩写,而“code”则直指它的核心动作——生成代码。它不是一个具体的库,也不是某个公司独占的产品,而是一种把“模板 + 数据 + 渲染引擎”组合起来,用来自动化产出重复性代码或配置文件的模式。你可以把它想象成一个“代码印刷机”:你给它一个模板和一份数据清单,它就能批量吐出结构一致、风格统一的代码文件。

这套东西解决的核心问题很朴素:在任何一个稍具规模的项目里,总有大量重复度极高的代码需要写。比如后端的 CRUD 接口、前端的列表页和表单页、数据库的建表语句、API 的文档注释、甚至部署脚本里的环境变量配置。手写这些内容不仅枯燥,而且容易出错——改了一个字段名,要翻遍十几个文件去同步。t3code 的思路就是把这些重复劳动抽象成模板,把变化的部分抽成数据,让机器去干那些“复制粘贴改名字”的活。

适合谁来参考呢?如果你是一个团队里的技术负责人,手底下有几个项目在并行开发,代码风格和目录结构需要统一,那 t3code 这套思路能帮你省下大量 review 和重构的时间。如果你是一个独立开发者,经常需要快速搭起一个项目的骨架,那它也能让你在几分钟内生成一套可运行的初始代码。甚至如果你不是程序员,但需要批量生成一些结构化的文本文件(比如配置文件、测试用例、文档骨架),t3code 的模板渲染思路同样适用。它不挑语言,不挑框架,核心就是“模板 + 数据 = 输出”这个公式。

我见过太多团队在“造轮子”和“用现成”之间反复横跳,最后要么写了一个庞大到没人敢维护的代码生成器,要么干脆放弃、继续手写。t3code 的价值在于它足够轻——轻到你可以用一个下午就搭起一套可用的生成流程,轻到团队里任何一个成员都能看懂模板在干什么。接下来我会从设计思路、核心细节、实操过程、常见问题几个角度,把这套东西拆开揉碎讲清楚。

2. 整体设计思路:为什么是“模板 + 数据 + 渲染”这三件套

2.1 核心公式的由来与优势

t3code 的设计哲学可以用一个极简的公式概括:输出 = 模板(数据)。这个公式看起来简单到近乎废话,但真正把它用好的人并不多。我见过不少团队一开始雄心勃勃,想做一个“智能代码生成平台”,结果搞出一套复杂的 DSL(领域特定语言),最后连写模板的人自己都记不住语法。t3code 反其道而行,它不发明新语言,而是直接复用你已经在用的模板引擎——比如 Jinja2、Handlebars、Mustache、EJS,甚至 Python 自带的 string.Template。这样做的好处是学习成本几乎为零,你不需要额外学一套语法,只要会写普通的模板文件就行。

为什么是“三件套”而不是“两件套”?因为在实际操作中,模板和数据往往需要一层“渲染引擎”来粘合。模板是静态的骨架,数据是动态的血肉,渲染引擎则是那个把血肉填进骨架的“手术刀”。这三者分离之后,每个部分都可以独立演进:模板可以随时调整格式,数据可以从数据库、Excel、YAML 文件甚至 API 接口动态获取,渲染引擎则负责处理转义、循环、条件判断这些逻辑。这种分离带来的最大好处是可测试性——你可以单独测试模板渲染的结果,而不需要跑整个生成流程。

我个人的经验是,很多团队在代码生成上踩坑,往往是因为把这三者混在一起了。比如在模板里直接写数据库查询逻辑,或者在数据文件里嵌入大段代码片段。t3code 的实践要求你严格区分:模板只负责“长什么样”,数据只负责“是什么”,渲染引擎只负责“怎么合”。这条边界一旦守住,后续的维护成本会直线下降。

2.2 与重型代码生成器的对比取舍

市面上有不少重型代码生成器,比如基于 UML 模型驱动的、基于数据库 Schema 反向工程的、甚至基于 AI 大模型直接生成整个项目的。这些方案各有各的适用场景,但 t3code 选择了一条更“笨”但更可控的路。重型生成器的问题在于,它们往往要求你先建立一套完整的元模型,学习成本高,而且一旦生成结果不符合预期,调试起来非常痛苦——你很难定位到底是模型错了、模板错了还是引擎的 bug。

t3code 的取舍很明确:放弃“一键生成整个项目”的幻想,专注于“生成那些你明确知道该怎么写的部分”。比如你很清楚一个 RESTful 接口的 Controller 应该长什么样,只是字段名和路径不同,那 t3code 就帮你把字段名和路径填进去。它不试图理解你的业务逻辑,也不试图替你做出架构决策。这种“有所为有所不为”的定位,让它在实际项目中反而更容易落地。

另一个关键取舍是生成时机。有些团队喜欢在编译期生成代码,把生成器集成到构建流程里;有些团队则喜欢在开发期手动触发生成,生成后的代码直接提交到仓库。t3code 更倾向于后者,因为手动触发给了开发者一个“检查点”——你可以在生成后立刻 review 代码,确认无误再提交。编译期生成虽然自动化程度更高,但一旦模板有误,整个构建就会失败,排查起来反而更麻烦。我自己的做法是:把生成脚本放在tools/目录下,需要时手动跑一下,生成结果先 diff 一遍再决定是否采纳。

2.3 适用场景与不适用场景的边界

t3code 最适合的场景是结构高度重复、变化维度清晰的代码。比如:

  • 后端:每个实体对应的 Model、Repository、Service、Controller 四层代码
  • 前端:每个列表页对应的搜索表单、表格列定义、分页逻辑、新增/编辑弹窗
  • 数据库:每个表对应的建表语句、索引定义、初始化数据
  • 文档:每个 API 对应的接口说明、请求示例、响应示例
  • 配置:每个环境对应的配置文件、部署脚本、环境变量清单

这些场景的共同特点是:骨架固定,只有少量变量在变。你只要把变量抽出来做成数据,剩下的交给模板就行。

但 t3code 不适合什么场景呢?业务逻辑复杂、每个模块差异巨大的代码。比如一个电商系统的订单状态机,每个状态的流转逻辑都不一样,硬要用模板生成只会让模板变得比手写代码还复杂。还有一次性代码——如果你只需要生成一个文件,手写可能比写模板更快。以及需要深度调试的代码——生成出来的代码如果出了问题,你需要在生成器和生成结果之间来回切换,调试成本反而更高。

我的一般原则是:如果一个文件你需要写三遍以上,而且每次只是改改名字和字段,那就值得为它写一个模板。如果只写一遍,或者每次改动都涉及逻辑调整,那就老老实实手写。

3. 核心细节解析:模板、数据与渲染引擎的实操要点

3.1 模板设计:如何写出“好维护”的模板

写模板这件事,看起来简单,实际上有很多门道。我见过太多模板,一开始写得飞快,后来改一个字段要动十几个地方,最后没人敢碰。一个好的 t3code 模板应该具备三个特征:变量集中、逻辑最少、注释清晰。

先说变量集中。模板里所有的可变部分都应该通过变量引用,而不是硬编码。比如下面这个 Jinja2 模板片段:

class {{ entity_name }}Service: def get_{{ entity_name|lower }}(self, id: int): return self.repo.find_by_id(id)

这里的entity_name就是变量,它会在渲染时被替换成具体的实体名。但要注意,变量的命名要统一,不要一会儿用entity_name,一会儿用model_name,一会儿又用className。我建议在项目初期就定好一套命名规范,比如统一用 snake_case 表示变量名,用{{ variable }}的格式引用。

逻辑最少的意思是,模板里尽量不要写复杂的条件判断和循环嵌套。如果发现模板里出现了三层以上的 if-else,那说明这个模板的抽象层次可能不对,应该考虑拆成多个模板,或者把逻辑移到数据准备阶段。比如下面这个模板就有点“过度逻辑”了:

{% if type == "string" %} {{ name }}: str {% elif type == "integer" %} {{ name }}: int {% elif type == "boolean" %} {{ name }}: bool {% else %} {{ name }}: Any {% endif %}

更好的做法是在数据准备阶段就把类型映射好,模板里只写{{ name }}: {{ python_type }}。这样模板更干净,类型映射的逻辑也更容易测试和修改。

注释清晰这一点经常被忽略。模板文件里应该用注释说明这个模板的用途、依赖哪些变量、生成后的文件放在哪里。比如:

{# 用途:生成 FastAPI 的 Pydantic Schema 变量:entity_name(实体名,如 User) fields(字段列表,每个字段包含 name, type, required) 输出:app/schemas/{{ entity_name|lower }}.py #}

这样即使过了半年再来看这个模板,也能快速回忆起它的作用。

3.2 数据准备:从哪来、怎么组织、如何校验

数据是 t3code 的“燃料”,燃料的质量直接决定生成结果的质量。数据来源通常有三种:手工维护的 YAML/JSON 文件、从数据库 Schema 自动提取、从 Excel 或 CSV 导入。三种方式各有优劣,我一般会根据项目阶段来选择。

项目初期,实体和字段还不稳定,我倾向于用手工维护的 YAML 文件。YAML 的可读性好,改起来方便,而且可以加注释。比如:

entities: - name: User table: users fields: - name: id type: integer primary_key: true - name: username type: string required: true - name: email type: string required: true

项目中期,数据库 Schema 基本稳定了,就可以写一个脚本从information_schema里自动提取字段信息,生成上述 YAML 文件。这样每次数据库有变更,重新跑一下脚本就能同步数据。但要注意,自动提取的数据往往缺少业务语义——比如它知道status是varchar(20),但不知道这个字段的可选值是active、inactive、pending。这些语义信息还是需要手工补充。

数据组织的一个关键原则是扁平化。尽量不要在数据里嵌套太深的结构,因为模板引擎处理深层嵌套时容易出错,而且调试起来很麻烦。如果确实需要嵌套,比如一个实体有多个关联的子实体,可以考虑拆成多个数据文件,分别渲染。

数据校验这一步很多人会跳过,但我强烈建议加上。最简单的做法是在渲染前用 JSON Schema 或 Pydantic 对数据做一次校验,确保必填字段都存在、类型正确。我踩过的坑是:有一次数据文件里少了一个table_name字段,模板渲染时直接报错,但错误信息指向的是模板的某一行,排查了半天才发现是数据的问题。如果提前校验,就能立刻定位到是数据缺失。

3.3 渲染引擎选型:Jinja2、Handlebars 还是自己写

渲染引擎的选择取决于你的技术栈和团队习惯。如果你主要用 Python,那 Jinja2 几乎是默认选择——它功能强大、文档齐全、社区活跃。如果你用 JavaScript/TypeScript,那 Handlebars 或 EJS 更顺手。如果你用 Go,那text/template是标准库自带的,不需要额外依赖。

我个人的偏好是Jinja2,原因有三:第一,它的语法足够表达大多数需求,但又不会过于复杂;第二,它支持宏(macro)和继承(extends),可以把公共部分抽出来复用;第三,它的错误信息相对清晰,调试起来不算痛苦。

但如果你不想引入任何外部依赖,用 Python 自带的string.Template也能凑合。它的语法更简单,只支持$variable和${variable}两种形式,不支持循环和条件判断。对于非常简单的生成任务,这反而是一种优势——因为简单,所以不容易出错。

下面是一个用 Jinja2 渲染的完整示例:

from jinja2 import Environment, FileSystemLoader import yaml env = Environment(loader=FileSystemLoader('templates')) template = env.get_template('service.py.j2') with open('data/entities.yaml') as f: data = yaml.safe_load(f) for entity in data['entities']: output = template.render(entity=entity) filename = f"app/services/{entity['name'].lower()}_service.py" with open(filename, 'w') as f: f.write(output)

这段代码做的事情很清晰:加载模板、加载数据、循环渲染、写入文件。你可以把它封装成一个命令行工具,加上参数解析,就能变成一个可复用的生成脚本。

注意:渲染时要注意转义问题。如果你的模板输出的是代码,而数据里包含特殊字符(比如引号、反斜杠),一定要确保渲染引擎做了正确的转义。Jinja2 默认会转义 HTML,但生成代码时需要关闭自动转义,或者手动处理。

4. 实操过程:从零搭建一套 t3code 生成流程

4.1 环境准备与目录结构规划

在开始写代码之前,先规划好目录结构。我习惯把生成相关的文件都放在一个独立的codegen/目录下,和业务代码分开。这样做的目的是让生成器和生成结果之间有清晰的边界,避免混淆。一个典型的目录结构如下:

project/ ├── codegen/ │ ├── templates/ # 模板文件 │ │ ├── model.py.j2 │ │ ├── service.py.j2 │ │ └── controller.py.j2 │ ├── data/ # 数据文件 │ │ └── entities.yaml │ ├── output/ # 生成结果(可选,也可以直接生成到业务目录) │ ├── generate.py # 生成脚本 │ └── requirements.txt # 依赖清单 ├── app/ # 业务代码 │ ├── models/ │ ├── services/ │ └── controllers/ └── ...

环境准备很简单,只需要 Python 3.8+ 和几个依赖包:

pip install jinja2 pyyaml

如果你需要从数据库提取 Schema,还需要安装对应的数据库驱动,比如psycopg2或pymysql。

4.2 编写第一个模板:从 Model 层开始

我建议从 Model 层开始写第一个模板,因为 Model 的结构最稳定,变量也最清晰。假设我们用 SQLAlchemy 作为 ORM,一个典型的 Model 模板如下:

{# templates/model.py.j2 #} from sqlalchemy import Column, Integer, String, Boolean, DateTime from app.database import Base class {{ entity.name }}(Base): __tablename__ = '{{ entity.table }}' {% for field in entity.fields %} {{ field.name }} = Column( {{ field.sqlalchemy_type }}, {% if field.primary_key %}primary_key=True,{% endif %} {% if field.required %}nullable=False,{% endif %} {% if field.default %}default={{ field.default }},{% endif %} ) {% endfor %} def __repr__(self): return f'<{{ entity.name }} {self.id}>'

这个模板里用到了循环({% for %})和条件判断({% if %}),但逻辑并不复杂。每个字段的sqlalchemy_type需要在数据准备阶段映射好,比如integer映射为Integer,string映射为String(255)。

对应的数据文件:

entities: - name: User table: users fields: - name: id sqlalchemy_type: Integer primary_key: true - name: username sqlalchemy_type: String(50) required: true - name: email sqlalchemy_type: String(100) required: true - name: is_active sqlalchemy_type: Boolean default: true

渲染后的结果:

from sqlalchemy import Column, Integer, String, Boolean, DateTime from app.database import Base class User(Base): __tablename__ = 'users' id = Column(Integer, primary_key=True,) username = Column(String(50), nullable=False,) email = Column(String(100), nullable=False,) is_active = Column(Boolean, default=True,)

可以看到,生成的结果基本符合预期,只是多了一些尾随逗号。如果你在意代码风格,可以在模板里调整,或者用black之类的格式化工具后处理一遍。

4.3 扩展到 Service 和 Controller 层

Model 层跑通之后,就可以把同样的思路扩展到 Service 和 Controller 层。Service 层的模板通常包含基本的 CRUD 方法:

{# templates/service.py.j2 #} from app.models.{{ entity.name|lower }} import {{ entity.name }} from app.repositories.{{ entity.name|lower }}_repository import {{ entity.name }}Repository class {{ entity.name }}Service: def __init__(self, repo: {{ entity.name }}Repository): self.repo = repo def get_by_id(self, id: int) -> {{ entity.name }}: return self.repo.find_by_id(id) def get_all(self) -> list[{{ entity.name }}]: return self.repo.find_all() def create(self, data: dict) -> {{ entity.name }}: return self.repo.create(data) def update(self, id: int, data: dict) -> {{ entity.name }}: return self.repo.update(id, data) def delete(self, id: int) -> None: self.repo.delete(id)

Controller 层的模板则根据你用的 Web 框架来定。如果是 FastAPI:

{# templates/controller.py.j2 #} from fastapi import APIRouter, Depends from app.services.{{ entity.name|lower }}_service import {{ entity.name }}Service from app.schemas.{{ entity.name|lower }} import {{ entity.name }}Create, {{ entity.name }}Response router = APIRouter(prefix='/{{ entity.table }}', tags=['{{ entity.name }}']) @router.get('/', response_model=list[{{ entity.name }}Response]) def list_{{ entity.table }}(service: {{ entity.name }}Service = Depends()): return service.get_all() @router.get('/{id}', response_model={{ entity.name }}Response) def get_{{ entity.name|lower }}(id: int, service: {{ entity.name }}Service = Depends()): return service.get_by_id(id) @router.post('/', response_model={{ entity.name }}Response) def create_{{ entity.name|lower }}(data: {{ entity.name }}Create, service: {{ entity.name }}Service = Depends()): return service.create(data.dict()) @router.delete('/{id}') def delete_{{ entity.name|lower }}(id: int, service: {{ entity.name }}Service = Depends()): service.delete(id) return {'ok': True}

这些模板写完之后,你只需要维护一份 YAML 数据文件,就能批量生成整套后端代码。新增一个实体时,只需要在 YAML 里加几行配置,跑一下生成脚本,Model、Service、Controller、Schema 就全都有了。

4.4 生成脚本的封装与参数化

为了让生成脚本更好用,我通常会把它封装成一个命令行工具,支持几个常用参数:

import argparse import yaml from jinja2 import Environment, FileSystemLoader from pathlib import Path def generate(template_dir, data_file, output_dir): env = Environment(loader=FileSystemLoader(template_dir)) with open(data_file) as f: data = yaml.safe_load(f) for entity in data['entities']: for template_name in ['model.py.j2', 'service.py.j2', 'controller.py.j2']: template = env.get_template(template_name) output = template.render(entity=entity) # 根据模板名决定输出路径 if 'model' in template_name: path = Path(output_dir) / 'models' / f"{entity['name'].lower()}.py" elif 'service' in template_name: path = Path(output_dir) / 'services' / f"{entity['name'].lower()}_service.py" elif 'controller' in template_name: path = Path(output_dir) / 'controllers' / f"{entity['name'].lower()}_controller.py" path.parent.mkdir(parents=True, exist_ok=True) path.write_text(output) print(f"Generated: {path}") if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--templates', default='codegen/templates') parser.add_argument('--data', default='codegen/data/entities.yaml') parser.add_argument('--output', default='app') args = parser.parse_args() generate(args.templates, args.data, args.output)

这样你就可以通过python codegen/generate.py --data codegen/data/entities.yaml来触发生成。如果只想生成某个特定实体,还可以加一个--entity User参数来过滤。

实操心得:生成脚本最好支持“dry-run”模式,也就是只打印将要生成的文件路径和内容摘要,不实际写入。这样在批量生成之前可以先确认一遍,避免覆盖掉手工修改过的文件。

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

5.1 模板渲染报错:变量未定义或类型不匹配

这是最常见的问题,通常有几种表现:UndefinedError: 'entity' is undefined、TypeError: unsupported operand type(s)、或者渲染出来的结果里出现了{{ variable }}原样输出。排查思路如下:

首先确认数据文件是否正确加载。可以在渲染前打印一下data变量,看看结构是否符合预期。如果数据是从 YAML 加载的,注意 YAML 对缩进非常敏感,一个空格错位就可能导致整个结构解析错误。

其次检查模板里的变量名是否和数据里的键名完全一致。Jinja2 是大小写敏感的,entity.Name和entity.name是两个不同的变量。我建议在数据准备阶段就用统一的命名规范,比如全部用小写加下划线。

如果渲染结果里出现了未替换的变量,通常是因为模板里用了{{ variable }}但数据里没有这个键,而 Jinja2 的默认行为是渲染为空字符串。可以在创建 Environment 时设置undefined=StrictUndefined,这样遇到未定义变量就会直接报错,而不是静默忽略。

from jinja2 import Environment, FileSystemLoader, StrictUndefined env = Environment(loader=FileSystemLoader('templates'), undefined=StrictUndefined)

5.2 生成结果不符合预期:缩进、换行与转义问题

模板渲染出来的代码,最常见的问题是缩进混乱。比如循环生成的代码块,每一行的缩进可能不一致。解决方法是使用 Jinja2 的trim_blocks和lstrip_blocks选项:

env = Environment( loader=FileSystemLoader('templates'), trim_blocks=True, lstrip_blocks=True, )

trim_blocks会移除块标签后的第一个换行符,lstrip_blocks会移除块标签前的空白。这两个选项配合使用,可以大幅减少模板里的空白控制问题。

另一个问题是转义。如果你的数据里包含引号、反斜杠、或者模板语法本身(比如{{),渲染时可能会出错。对于代码生成,通常需要关闭自动转义:

env = Environment(autoescape=False)

但如果数据来自不可信来源,关闭转义可能会带来安全风险。在代码生成场景下,数据通常是自己维护的,风险可控。

5.3 生成文件覆盖手工修改:如何保护已有代码

这是 t3code 实践中最容易引发团队矛盾的问题:生成器把手工修改过的文件覆盖了,导致辛苦写的业务逻辑丢失。解决这个问题有几种策略:

第一种是生成到独立目录,然后手动合并。比如生成到codegen/output/,再用 diff 工具对比合并。这种方式最安全,但操作繁琐。

第二种是在生成文件头部加标记,比如# This file is generated by t3code. Do not edit manually.,然后在生成前检查文件是否包含这个标记。如果不包含,说明是手工创建的文件,跳过覆盖。

第三种是只生成一次,后续手工维护。也就是把生成器当作“脚手架”工具,而不是持续集成的环节。生成完初始代码后,后续的修改都在业务代码里进行,不再重新生成。这种方式适合项目初期快速搭建骨架,但不适合需要频繁同步的场景。

我个人的做法是混合使用:对于 Model 层这种结构稳定的代码,用第二种方式持续生成;对于 Service 和 Controller 层这种业务逻辑多变的代码,用第三种方式只生成一次。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
渲染报 UndefinedError数据缺少字段或变量名拼写错误打印数据字典,检查键名补全数据或修正变量名
输出中出现{{ }}变量未定义且未启用严格模式检查模板变量与数据键的对应关系启用 StrictUndefined
缩进混乱模板中空白控制不当检查块标签前后的空白启用 trim_blocks 和 lstrip_blocks
生成文件覆盖手工修改生成前未检查文件状态查看文件头部是否有生成标记加标记检查或生成到独立目录
特殊字符导致语法错误数据包含引号或反斜杠检查数据中的特殊字符关闭 autoescape 或手动转义
循环生成时变量作用域错误Jinja2 的循环变量作用域限制检查循环内外的变量引用使用 namespace 或 set 语句

避坑技巧:在模板文件里用注释记录这个模板依赖哪些变量、生成后的文件路径是什么。这样即使模板数量多了,也能快速定位问题。另外,建议把生成脚本纳入版本控制,每次修改模板或数据后,先跑一遍生成,用git diff检查变更,确认无误再提交。

6. 进阶玩法:让 t3code 融入日常开发流程

6.1 与 CI/CD 结合:自动检查生成结果是否同步

如果你的团队决定把生成器作为持续集成的一部分,可以在 CI 流程里加一步“生成检查”:跑一遍生成脚本,然后用git diff --exit-code检查是否有文件变更。如果有变更,说明有人改了数据或模板但没有重新生成,CI 就会失败。这样可以强制保证生成结果和源数据的一致性。

# .github/workflows/codegen-check.yml - name: Check codegen sync run: | python codegen/generate.py git diff --exit-code || (echo "Generated code is out of sync" && exit 1)

这个做法的前提是生成结果也提交到仓库。如果生成结果不提交,那 CI 检查就没有意义了。

6.2 从数据库自动提取 Schema 生成数据文件

当项目进入稳定期,数据库 Schema 成为权威来源时,可以写一个脚本从information_schema自动提取字段信息,生成 t3code 需要的 YAML 数据文件。以 PostgreSQL 为例:

import psycopg2 import yaml conn = psycopg2.connect("dbname=test user=postgres") cur = conn.cursor() cur.execute(""" SELECT table_name, column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema = 'public' ORDER BY table_name, ordinal_position """) entities = {} for table_name, column_name, data_type, is_nullable in cur.fetchall(): if table_name not in entities: entities[table_name] = {'name': table_name.title(), 'table': table_name, 'fields': []} entities[table_name]['fields'].append({ 'name': column_name, 'type': data_type, 'required': is_nullable == 'NO', }) with open('codegen/data/entities.yaml', 'w') as f: yaml.dump({'entities': list(entities.values())}, f, default_flow_style=False)

这个脚本跑完之后,再手工补充一些业务语义信息(比如枚举值的可选范围、字段的显示名称),就能得到一份完整的数据文件。

6.3 模板继承与宏:减少重复模板代码

当模板数量多了之后,你会发现很多模板有共同的头部和尾部。比如每个 Python 文件都有 import 语句,每个 Controller 都有 router 定义。这时候可以用 Jinja2 的模板继承来减少重复:

{# templates/base.py.j2 #} {% block imports %}{% endblock %} {% block content %}{% endblock %}

然后子模板:

{# templates/model.py.j2 #} {% extends "base.py.j2" %} {% block imports %} from sqlalchemy import Column, Integer, String from app.database import Base {% endblock %} {% block content %} class {{ entity.name }}(Base): ... {% endblock %}

宏(macro)则适合抽取那些在多个模板中重复出现的片段,比如字段定义、路由装饰器等等。用好继承和宏,可以让模板库的维护成本大幅降低。

6.4 生成结果的格式化与后处理

模板渲染出来的代码,格式上可能不完全符合团队的代码规范。比如多了一些空行、缩进不一致、引号风格不统一。这时候可以在生成后加一步格式化:

import subprocess def format_generated_files(paths): for path in paths: if path.suffix == '.py': subprocess.run(['black', str(path)]) elif path.suffix in ('.js', '.ts'): subprocess.run(['prettier', '--write', str(path)])

这样生成出来的代码就直接符合规范,省去了手工调整的麻烦。我一般会把这一步集成到生成脚本的最后,生成完自动格式化。

7. 我踩过的坑与最后分享的几个小技巧

先说几个我实际踩过的坑。第一个坑是模板里用了太多逻辑。刚开始用 t3code 的时候,我恨不得把所有条件判断都塞进模板里,结果模板变得比生成的代码还复杂。后来我学乖了,把逻辑尽量前移到数据准备阶段,模板只做最简单的变量替换和循环。第二个坑是没有版本控制生成结果。有一段时间我把生成结果放在.gitignore里,结果每次换电脑都要重新生成,而且不同人生成的代码可能有细微差异。后来我把生成结果也提交到仓库,配合 CI 检查,问题就解决了。第三个坑是数据文件没有校验。有一次数据里少了一个必填字段,模板渲染时没报错,但生成的代码运行时报了KeyError,排查了半天才发现是数据的问题。后来我在渲染前加了一层 Pydantic 校验,类似的问题就再也没出现过。

最后分享几个小技巧。第一个是给模板加版本号。在模板文件头部写一个注释,比如{# version: 1.2 #},这样当模板更新时,可以快速判断哪些生成结果需要重新生成。第二个是保留生成日志。每次生成时记录一下时间、数据文件的哈希值、生成的文件列表,方便回溯。第三个是不要追求一步到位。t3code 的实践是一个渐进的过程,先从最简单的 Model 层开始,跑通了再扩展到 Service 和 Controller。不要一开始就想着生成整个项目,那样很容易半途而废。

这个内容后续还可以这样扩展:把生成器做成一个 Web 界面,让非技术同学也能通过表单填写数据、点击按钮生成代码;或者把模板库做成一个共享仓库,团队之间互相复用;再或者结合 AI 能力,让模型根据自然语言描述自动生成数据文件。这些都是很有意思的方向,但核心的“模板 + 数据 + 渲染”三件套不会变。

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

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

立即咨询