代码生成与模板
概述
包括模板引擎、AST操作、代码脚手架和自动化样板代码生成在内的全面代码生成技术指南,用于提高生产力和一致性。
何时使用
- 脚手架新项目或组件
- 生成重复的样板代码
- 自动创建CRUD操作
- 从OpenAPI规范生成API客户端
- 从模板构建代码
- 从模式创建数据库模型
- 从JSON Schema生成TypeScript类型
- 构建自定义CLI生成器
指令
1. 模板引擎
Handlebars模板
// templates/component.hbs
import React from 'react';
export interface {{pascalCase name}}Props {
{{#each props}}
{{this.name}}{{#if this.optional}}?{{/if}}: {{this.type}};
{{/each}}
}
export const {{pascalCase name}}: React.FC<{{pascalCase name}}Props> = ({
{{#each props}}{{this.name}},{{/each}}
}) => {
return (
<div className="{{kebabCase name}}">
{/* 组件实现 */}
</div>
);
};
// generator.ts
import Handlebars from 'handlebars';
import fs from 'fs';
// 注册助手
Handlebars.registerHelper('pascalCase', (str: string) =>
str.replace(/(\w)(\w*)/g, (_, first, rest) =>
first.toUpperCase() + rest.toLowerCase()
)
);
Handlebars.registerHelper('kebabCase', (str: string) =>
str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
);
// 加载模板
const templateSource = fs.readFileSync('templates/component.hbs', 'utf8');
const template = Handlebars.compile(templateSource);
// 生成代码
const code = template({
name: 'userProfile',
props: [
{ name: 'userId', type: 'string', optional: false },
{ name: 'onUpdate', type: '() => void', optional: true }
]
});
fs.writeFileSync('src/components/UserProfile.tsx', code);
EJS模板
// templates/api-endpoint.ejs
import { Router } from 'express';
import { <%= modelName %>Service } from '../services/<%= kebabCase(modelName) %>.service';
const router = Router();
const service = new <%= modelName %>Service();
// GET /<%= pluralize(kebabCase(modelName)) %>
router.get('/', async (req, res) => {
try {
const items = await service.findAll();
res.json(items);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /<%= pluralize(kebabCase(modelName)) %>/:id
router.get('/:id', async (req, res) => {
try {
const item = await service.findById(req.params.id);
if (!item) {
return res.status(404).json({ error: 'Not found' });
}
res.json(item);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST /<%= pluralize(kebabCase(modelName)) %>
router.post('/', async (req, res) => {
try {
const item = await service.create(req.body);
res.status(201).json(item);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
export default router;
// 使用EJS
import ejs from 'ejs';
const code = await ejs.renderFile('templates/api-endpoint.ejs', {
modelName: 'User',
kebabCase: (str: string) => str.replace(/([A-Z])/g, '-$1').toLowerCase().slice(1),
pluralize: (str: string) => str + 's'
});
2. 基于AST的代码生成
使用Babel/TypeScript AST
// ast-generator.ts
import * as ts from 'typescript';
export class TypeScriptGenerator {
// 生成接口
generateInterface(name: string, properties: Array<{ name: string; type: string; optional?: boolean }>) {
const members = properties.map(prop =>
ts.factory.createPropertySignature(
undefined,
ts.factory.createIdentifier(prop.name),
prop.optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined,
ts.factory.createTypeReferenceNode(prop.type)
)
);
const interfaceDecl = ts.factory.createInterfaceDeclaration(
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier(name),
undefined,
undefined,
members
);
return this.printNode(interfaceDecl);
}
// 生成类
generateClass(name: string, properties: Array<{ name: string; type: string }>) {
const propertyDecls = properties.map(prop =>
ts.factory.createPropertyDeclaration(
[ts.factory.createToken(ts.SyntaxKind.PrivateKeyword)],
ts.factory.createIdentifier(prop.name),
undefined,
ts.factory.createTypeReferenceNode(prop.type),
undefined
)
);
const constructor = ts.factory.createConstructorDeclaration(
undefined,
properties.map(prop =>
ts.factory.createParameterDeclaration(
undefined,
undefined,
ts.factory.createIdentifier(prop.name),
undefined,
ts.factory.createTypeReferenceNode(prop.type)
)
),
ts.factory.createBlock(
properties.map(prop =>
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createThis(),
prop.name
),
ts.SyntaxKind.EqualsToken,
ts.factory.createIdentifier(prop.name)
)
)
),
true
)
);
const classDecl = ts.factory.createClassDeclaration(
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier(name),
undefined,
undefined,
[...propertyDecls, constructor]
);
return this.printNode(classDecl);
}
private printNode(node: ts.Node): string {
const sourceFile = ts.createSourceFile(
'temp.ts',
'',
ts.ScriptTarget.Latest,
false,
ts.ScriptKind.TS
);
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
return printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
}
}
// 使用
const generator = new TypeScriptGenerator();
const interfaceCode = generator.generateInterface('User', [
{ name: 'id', type: 'string' },
{ name: 'email', type: 'string' },
{ name: 'name', type: 'string', optional: true }
]);
const classCode = generator.generateClass('UserService', [
{ name: 'repository', type: 'UserRepository' },
{ name: 'logger', type: 'Logger' }
]);
3. 项目脚手架
简单的CLI生成器
// cli/generate.ts
#!/usr/bin/env node
import { Command } from 'commander';
import inquirer from 'inquirer';
import fs from 'fs-extra';
import path from 'path';
const program = new Command();
program
.name('generate')
.description('代码生成CLI')
.version('1.0.0');
program
.command('component <name>')
.description('生成React组件')
.option('-d, --dir <directory>', '输出目录', 'src/components')
.action(async (name, options) => {
const answers = await inquirer.prompt([
{
type: 'list',
name: 'type',
message: '组件类型?',
choices: ['functional', 'class']
},
{
type: 'confirm',
name: 'typescript',
message: '使用TypeScript?',
default: true
},
{
type: 'confirm',
name: 'test',
message: '生成测试文件?',
default: true
}
]);
await generateComponent(name, options.dir, answers);
});
program
.command('api <resource>')
.description('生成API端点,包括控制器、服务和模型')
.action(async (resource) => {
await generateApiResource(resource);
});
program.parse();
async function generateComponent(name: string, dir: string, options: any) {
const componentName = pascalCase(name);
const ext = options.typescript ? 'tsx' : 'jsx';
const template = options.type === 'functional'
? getFunctionalComponentTemplate(componentName, options.typescript)
: getClassComponentTemplate(componentName, options.typescript);
const componentPath = path.join(dir, `${componentName}.${ext}`);
await fs.ensureDir(dir);
await fs.writeFile(componentPath, template);
console.log(`✓ 创建 ${componentPath}`);
if (options.test) {
const testTemplate = getTestTemplate(componentName, options.typescript);
const testPath = path.join(dir, `${componentName}.test.${ext}`);
await fs.writeFile(testPath, testTemplate);
console.log(`✓ 创建 ${testPath}`);
}
}
function getFunctionalComponentTemplate(name: string, ts: boolean): string {
if (ts) {
return `import React from 'react';
export interface ${name}Props {
// 在这里添加属性
}
export const ${name}: React.FC<${name}Props> = (props) => {
return (
<div className="${kebabCase(name)}">
<h1>${name}</h1>
</div>
);
};
`;
}
return `import React from 'react';
export const ${name} = (props) => {
return (
<div className="${kebabCase(name)}">
<h1>${name}</h1>
</div>
);
};
`;
}
async function generateApiResource(resource: string) {
const name = pascalCase(resource);
// 生成模型
const modelCode = `export interface ${name} {
id: string;
createdAt: Date;
updatedAt: Date;
// 在这里添加字段
}
`;
await fs.writeFile(`src/models/${kebabCase(resource)}.model.ts`, modelCode);
// 生成服务
const serviceCode = `import { ${name} } from '../models/${kebabCase(resource)}.model';
export class ${name}Service {
async findAll(): Promise<${name}[]> {
// 实现
return [];
}
async findById(id: string): Promise<${name} | null> {
// 实现
return null;
}
async create(data: Partial<${name}>): Promise<${name}> {
// 实现
throw new Error('未实现');
}
async update(id: string, data: Partial<${name}>): Promise<${name}> {
// 实现
throw new Error('未实现');
}
async delete(id: string): Promise<void> {
// 实现
}
}
`;
await fs.writeFile(`src/services/${kebabCase(resource)}.service.ts`, serviceCode);
// 生成控制器
const controllerCode = `import { Router } from 'express';
import { ${name}Service } from '../services/${kebabCase(resource)}.service';
const router = Router();
const service = new ${name}Service();
router.get('/', async (req, res) => {
const items = await service.findAll();
res.json(items);
});
router.get('/:id', async (req, res) => {
const item = await service.findById(req.params.id);
if (!item) return res.status(404).json({ error: '未找到' });
res.json(item);
});
router.post('/', async (req, res) => {
const item = await service.create(req.body);
res.status(201).json(item);
});
router.put('/:id', async (req, res) => {
const item = await service.update(req.params.id, req.body);
res.json(item);
});
router.delete('/:id', async (req, res) => {
await service.delete(req.params.id);
res.status(204).send();
});
export default router;
`;
await fs.writeFile(`src/controllers/${kebabCase(resource)}.controller.ts`, controllerCode);
console.log(`✓ 生成API资源:${name}`);
}
4. OpenAPI客户端生成
// openapi-client-generator.ts
import SwaggerParser from '@apidevtools/swagger-parser';
import { compile } from 'json-schema-to-typescript';
export class OpenAPIClientGenerator {
async generate(specPath: string, outputDir: string) {
const api = await SwaggerParser.parse(specPath);
// 从模式生成TypeScript类型
if (api.components?.schemas) {
for (const [name, schema] of Object.entries(api.components.schemas)) {
const ts = await compile(schema as any, name, {
bannerComment: ''
});
await fs.writeFile(
path.join(outputDir, 'types', `${name}.ts`),
ts
);
}
}
// 生成API客户端方法
for (const [path, pathItem] of Object.entries(api.paths)) {
for (const [method, operation] of Object.entries(pathItem)) {
if (['get', 'post', 'put', 'delete', 'patch'].includes(method)) {
const clientMethod = this.generateClientMethod(
method,
path,
operation as any
);
// 写入文件...
}
}
}
}
private generateClientMethod(
method: string,
path: string,
operation: any
): string {
const functionName = operation.operationId || this.pathToFunctionName(method, path);
const parameters = operation.parameters || [];
return `
async ${functionName}(${this.generateParameters(parameters)}): Promise<${this.getResponseType(operation)}> {
const response = await this.request('${method.toUpperCase()}', '${path}', {
${this.generateRequestOptions(parameters)}
});
return response.json();
}
`;
}
private generateParameters(parameters: any[]): string {
return parameters
.map(p => `${p.name}${p.required ? '' : '?'}: ${this.schemaToType(p.schema)}`)
.join(', ');
}
private getResponseType(operation: any): string {
const successResponse = operation.responses['200'] || operation.responses['201'];
if (!successResponse) return 'any';
const schema = successResponse.content?.['application/json']?.schema;
return schema ? this.schemaToType(schema) : 'any';
}
private schemaToType(schema: any): string {
if (schema.$ref) {
return schema.$ref.split('/').pop();
}
if (schema.type === 'string') return 'string';
if (schema.type === 'number' || schema.type === 'integer') return 'number';
if (schema.type === 'boolean') return 'boolean';
if (schema.type === 'array') return `${this.schemaToType(schema.items)}[]`;
return 'any';
}
private pathToFunctionName(method: string, path: string): string {
const cleanPath = path.replace(/\{.*?\}/g, 'By').replace(/[^a-zA-Z0-9]/g, '');
return `${method}${cleanPath}`;
}
}
5. 数据库模型生成
// prisma-schema-generator.ts
export class PrismaSchemaGenerator {
generateModel(table: DatabaseTable): string {
return `model ${pascalCase(table.name)} {
${table.columns.map(col => this.generateField(col)).join('
')}
${this.generateRelations(table.relations)}
${this.generateIndexes(table.indexes)}
}`;
}
private generateField(column: Column): string {
const optional = !column.required ? '?' : '';
const unique = column.unique ? ' @unique' : '';
const defaultValue = column.default ? ` @default(${column.default})` : '';
return ` ${column.name} ${this.mapType(column.type)}${optional}${unique}${defaultValue}`;
}
private mapType(sqlType: string): string {
const typeMap: Record<string, string> = {
'varchar': 'String',
'text': 'String',
'integer': 'Int',
'bigint': 'BigInt',
'boolean': 'Boolean',
'timestamp': 'DateTime',
'date': 'DateTime',
'json': 'Json'
};
return typeMap[sqlType.toLowerCase()] || 'String';
}
private generateRelations(relations: Relation[]): string {
return relations.map(rel => {
if (rel.type === 'hasMany') {
return ` ${rel.name} ${rel.model}[]`;
} else if (rel.type === 'belongsTo') {
return ` ${rel.name} ${rel.model} @relation(fields: [${rel.foreignKey}], references: [id])`;
}
return '';
}).join('
');
}
}
6. GraphQL代码生成
// graphql-codegen.config.ts
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: 'http://localhost:4000/graphql',
documents: ['src/**/*.tsx', 'src/**/*.ts'],
generates: {
'./src/generated/graphql.ts': {
plugins: [
'typescript',
'typescript-operations',
'typescript-react-apollo'
],
config: {
withHooks: true,
withComponent: false,
withHOC: false
}
},
'./src/generated/introspection.json': {
plugins: ['introspection']
}
}
};
export default config;
7. Plop.js生成器
// plopfile.ts
import { NodePlopAPI } from 'plop';
export default function (plop: NodePlopAPI) {
// 组件生成器
plop.setGenerator('component', {
description: 'React组件',
prompts: [
{
type: 'input',
name: 'name',
message: '组件名称:'
},
{
type: 'list',
name: 'type',
message: '组件类型:',
choices: ['functional', 'class']
}
],
actions: [
{
type: 'add',
path: 'src/components/{{pascalCase name}}/{{pascalCase name}}.tsx',
templateFile: 'templates/component.hbs'
},
{
type: 'add',
path: 'src/components/{{pascalCase name}}/{{pascalCase name}}.test.tsx',
templateFile: 'templates/component.test.hbs'
},
{
type: 'add',
path: 'src/components/{{pascalCase name}}/index.ts',
template: "export { {{pascalCase name}} } from './{{pascalCase name}}';
"
}
]
});
// API生成器
plop.setGenerator('api', {
description: '全栈API端点',
prompts: [
{
type: 'input',
name: 'name',
message: '资源名称(例如,用户,帖子):'
}
],
actions: [
{
type: 'add',
path: 'src/models/{{kebabCase name}}.model.ts',
templateFile: 'templates/model.hbs'
},
{
type: 'add',
path: 'src/services/{{kebabCase name}}.service.ts',
templateFile: 'templates/service.hbs'
},
{
type: 'add',
path: 'src/controllers/{{kebabCase name}}.controller.ts',
templateFile: 'templates/controller.hbs'
},
{
type: 'add',
path: 'src/routes/{{kebabCase name}}.routes.ts',
templateFile: 'templates/routes.hbs'
}
]
});
}
最佳实践
✅ 要做
- 对重复的代码模式使用模板
- 从模式生成TypeScript类型
- 在生成的代码中包含测试
- 在模板中遵循项目约定
- 对生成的代码添加注释
- 对模板进行版本控制
- 使模板可配置
- 与代码一起生成文档
- 在生成前验证输入
- 使用一致的命名约定
- 提供易于使用的CLI进行生成
❌ 不要做
- 过度生成(避免不必要的复杂性)
- 生成难以维护的代码
- 忘记验证生成的代码
- 在模板中硬编码值
- 无文档生成代码
- 为一次性用例创建生成器
- 在模板中混合业务逻辑
- 无格式化生成代码
- 在生成器中跳过错误处理
- 创建过于复杂的模板
常见模式
模式1:CRUD生成器
export function generateCRUD(entityName: string) {
return {
model: generateModel(entityName),
service: generateService(entityName),
controller: generateController(entityName),
routes: generateRoutes(entityName),
tests: generateTests(entityName)
};
}
模式2:迁移生成器
export function generateMigration(name: string, changes: SchemaChange[]) {
return {
up: generateUpMigration(changes),
down: generateDownMigration(changes)
};
}
模式3:工厂生成器
export function generateFactory(model: Model) {
return `export const create${model.name} = (overrides?: Partial<${model.name}>): ${model.name} => ({
${model.fields.map(f => `${f.name}: ${getDefaultValue(f)}`).join(',
')},
...overrides
});`;
}
工具与资源
- Plop: 微型生成框架
- Yeoman: 脚手架工具
- Hygen: 带模板的代码生成器
- GraphQL Code Generator: 从GraphQL生成代码
- Prisma: 带代码生成的数据库ORM
- OpenAPI Generator: 从OpenAPI生成客户端
- json-schema-to-typescript: 生成TS类型
- TypeScript Compiler API: AST操作