模板系统实现(Template System)

实现可扩展的模板系统,支持多种框架模板(Vue、React、TypeScript 等),包括模板引擎选择、变量注入、动态文件生成等功能。


📋 目录


1. 模板引擎选择

1.1 常用模板引擎对比

引擎语法特点适用场景
EJS<%= %>功能强大,支持 JavaScript复杂逻辑处理
Mustache{{ }}无逻辑,简洁简单变量替换
Handlebars{{ }}Mustache 增强版需要辅助函数
Nunjucks{% %}功能丰富,类似 Jinja2复杂模板

1.2 选择 EJS 的原因

  • 功能强大:支持完整的 JavaScript 语法
  • 灵活性强:可以处理复杂的逻辑
  • 易于使用:语法直观,学习成本低
  • 生态完善:文档和社区支持好

1.3 EJS 基础语法

<%# 注释 %>
 
<%# 变量输出 %>
<%= projectName %>
 
<%# 转义输出(防止 XSS) %>
<%- htmlContent %>
 
<%# 条件判断 %>
<% if (useTypeScript) { %>
import React from 'react';
<% } %>
 
<%# 循环 %>
<% dependencies.forEach(dep => { %>
  "<%= dep.name %>": "<%= dep.version %>",
<% }); %>
 
<%# 包含其他模板 %>
<%- include('header.ejs') %>

2. 内置模板体系

2.1 模板目录结构

templates/
├── basic-js/          # 基础 JavaScript 模板
│   ├── package.json.ejs
│   ├── .gitignore
│   └── index.js
├── basic-ts/          # 基础 TypeScript 模板
│   ├── package.json.ejs
│   ├── tsconfig.json
│   ├── .gitignore
│   └── src/
│       └── index.ts
├── react/             # React 模板
│   ├── package.json.ejs
│   ├── .gitignore
│   ├── public/
│   │   └── index.html
│   └── src/
│       ├── App.jsx
│       └── index.jsx
├── react-ts/          # React + TypeScript 模板
│   ├── package.json.ejs
│   ├── tsconfig.json
│   ├── .gitignore
│   ├── public/
│   │   └── index.html
│   └── src/
│       ├── App.tsx
│       └── index.tsx
├── vue/               # Vue 模板
│   ├── package.json.ejs
│   ├── .gitignore
│   ├── public/
│   │   └── index.html
│   └── src/
│       ├── App.vue
│       └── main.js
└── vue-ts/            # Vue + TypeScript 模板
    ├── package.json.ejs
    ├── tsconfig.json
    ├── .gitignore
    ├── public/
    │   └── index.html
    └── src/
        ├── App.vue
        └── main.ts

2.2 模板管理器

// src/utils/template.ts
import path from 'path';
import fs from 'fs-extra';
import ejs from 'ejs';
 
export interface TemplateConfig {
  name: string;
  description: string;
  dependencies?: Record<string, string>;
  devDependencies?: Record<string, string>;
  scripts?: Record<string, string>;
}
 
export class TemplateManager {
  private templatesDir: string;
 
  constructor() {
    this.templatesDir = path.resolve(__dirname, '../templates');
  }
 
  // 获取所有可用模板
  async getAvailableTemplates(): Promise<string[]> {
    const entries = await fs.readdir(this.templatesDir);
    const templates: string[] = [];
 
    for (const entry of entries) {
      const entryPath = path.join(this.templatesDir, entry);
      const stat = await fs.stat(entryPath);
      if (stat.isDirectory()) {
        templates.push(entry);
      }
    }
 
    return templates;
  }
 
  // 获取模板配置
  async getTemplateConfig(templateName: string): Promise<TemplateConfig | null> {
    const configPath = path.join(
      this.templatesDir,
      templateName,
      'template.config.json'
    );
 
    if (await fs.pathExists(configPath)) {
      return await fs.readJson(configPath);
    }
 
    return null;
  }
 
  // 检查模板是否存在
  async templateExists(templateName: string): Promise<boolean> {
    const templatePath = path.join(this.templatesDir, templateName);
    return await fs.pathExists(templatePath);
  }
 
  // 获取模板路径
  getTemplatePath(templateName: string): string {
    return path.join(this.templatesDir, templateName);
  }
}

3. 模板变量注入

3.1 变量定义

// src/utils/variables.ts
export interface TemplateVariables {
  projectName: string;
  projectDescription?: string;
  author?: string;
  license?: string;
  version?: string;
  useTypeScript?: boolean;
  useESLint?: boolean;
  usePrettier?: boolean;
  useHusky?: boolean;
  packageManager?: 'npm' | 'yarn' | 'pnpm';
  [key: string]: any;
}
 
export function createTemplateVariables(
  projectName: string,
  options: any
): TemplateVariables {
  return {
    projectName,
    projectDescription: options.description || '',
    author: options.author || '',
    license: options.license || 'MIT',
    version: '1.0.0',
    useTypeScript: options.template?.includes('ts') || false,
    useESLint: options.features?.includes('eslint') || false,
    usePrettier: options.features?.includes('prettier') || false,
    useHusky: options.features?.includes('husky') || false,
    packageManager: options.packageManager || 'npm',
    ...options
  };
}

3.2 模板渲染

// src/utils/render.ts
import ejs from 'ejs';
import fs from 'fs-extra';
import path from 'path';
 
export async function renderTemplate(
  templatePath: string,
  targetPath: string,
  variables: Record<string, any>
): Promise<void> {
  const content = await fs.readFile(templatePath, 'utf-8');
  const rendered = ejs.render(content, variables, {
    filename: templatePath
  });
 
  // 移除 .ejs 扩展名
  const targetFile = targetPath.replace(/\.ejs$/, '');
  await fs.ensureDir(path.dirname(targetFile));
  await fs.writeFile(targetFile, rendered);
}

4. 动态文件生成

4.1 递归处理模板目录

// src/utils/file.ts
import fs from 'fs-extra';
import path from 'path';
import { renderTemplate } from './render';
 
export async function copyTemplate(
  srcDir: string,
  targetDir: string,
  variables: Record<string, any>
): Promise<void> {
  await fs.ensureDir(targetDir);
 
  const entries = await fs.readdir(srcDir);
 
  for (const entry of entries) {
    // 跳过配置文件
    if (entry === 'template.config.json') {
      continue;
    }
 
    const srcPath = path.join(srcDir, entry);
    const targetPath = path.join(targetDir, entry);
    const stat = await fs.stat(srcPath);
 
    if (stat.isDirectory()) {
      await copyTemplate(srcPath, targetPath, variables);
    } else {
      // 处理模板文件
      if (entry.endsWith('.ejs')) {
        await renderTemplate(srcPath, targetPath, variables);
      } else {
        // 普通文件直接复制
        await fs.copy(srcPath, targetPath);
      }
    }
  }
}

4.2 条件文件生成

// 根据选项决定是否生成某些文件
export async function generateFiles(
  targetDir: string,
  variables: TemplateVariables
): Promise<void> {
  // 基础文件
  await copyTemplate(templatePath, targetDir, variables);
 
  // 条件生成 ESLint 配置
  if (variables.useESLint) {
    await generateESLintConfig(targetDir, variables);
  }
 
  // 条件生成 Prettier 配置
  if (variables.usePrettier) {
    await generatePrettierConfig(targetDir, variables);
  }
 
  // 条件生成 Husky 配置
  if (variables.useHusky) {
    await generateHuskyConfig(targetDir, variables);
  }
}

5. 模板配置管理

5.1 模板配置文件

// templates/react-ts/template.config.json
{
  "name": "react-ts",
  "description": "React + TypeScript 项目模板",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "typescript": "^5.0.0",
    "vite": "^4.0.0",
    "@vitejs/plugin-react": "^4.0.0"
  },
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "features": ["typescript", "vite"]
}

5.2 使用模板配置

// src/commands/create.ts
import { TemplateManager } from '../utils/template';
 
const templateManager = new TemplateManager();
 
export async function createCommand(projectName: string, options: any) {
  // 获取模板配置
  const config = await templateManager.getTemplateConfig(options.template);
  
  if (config) {
    // 合并依赖
    const packageJson = {
      name: projectName,
      version: '1.0.0',
      dependencies: config.dependencies || {},
      devDependencies: config.devDependencies || {},
      scripts: config.scripts || {}
    };
 
    // 生成 package.json
    await generatePackageJson(targetDir, packageJson);
  }
 
  // 复制模板文件
  const templatePath = templateManager.getTemplatePath(options.template);
  await copyTemplate(templatePath, targetDir, variables);
}

6. 模板示例

6.1 React + TypeScript 模板

package.json.ejs

{
  "name": "<%= projectName %>",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "@vitejs/plugin-react": "^4.0.0",
    "typescript": "^5.0.0",
    "vite": "^4.0.0"
    <% if (useESLint) { %>,
    "eslint": "^8.0.0",
    "@typescript-eslint/eslint-plugin": "^5.0.0",
    "@typescript-eslint/parser": "^5.0.0"
    <% } %>
    <% if (usePrettier) { %>,
    "prettier": "^2.8.0"
    <% } %>
  }
}

src/App.tsx.ejs

import React from 'react';
 
function App() {
  return (
    <div className="App">
      <h1>Welcome to <%= projectName %></h1>
      <% if (projectDescription) { %>
      <p><%= projectDescription %></p>
      <% } %>
    </div>
  );
}
 
export default App;

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

6.2 Vue + TypeScript 模板

package.json.ejs

{
  "name": "<%= projectName %>",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.3.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.0.0",
    "typescript": "^5.0.0",
    "vite": "^4.0.0",
    "vue-tsc": "^1.8.0"
  }
}

src/App.vue.ejs

<template>
  <div class="app">
    <h1>{{ message }}</h1>
  </div>
</template>
 
<script setup lang="ts">
import { ref } from 'vue';
 
const message = ref('Welcome to <%= projectName %>');
</script>
 
<style scoped>
.app {
  text-align: center;
}
</style>

📝 总结

核心功能

  1. ✅ 模板引擎集成(EJS)
  2. ✅ 多模板支持(React、Vue、TypeScript)
  3. ✅ 变量注入和条件渲染
  4. ✅ 动态文件生成
  5. ✅ 模板配置管理

下一步


脚手架 前端工程化 模板系统 EJS TypeScript