模板系统实现(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>📝 总结
核心功能
- ✅ 模板引擎集成(EJS)
- ✅ 多模板支持(React、Vue、TypeScript)
- ✅ 变量注入和条件渲染
- ✅ 动态文件生成
- ✅ 模板配置管理
下一步
- 插件系统实现 - 实现插件机制,扩展脚手架功能