在当今快节奏的前端开发领域,如何快速启动一个新项目、统一团队技术栈、保证代码质量一致性,是每个团队都面临的挑战。传统的复制粘贴模板项目的方式已经无法满足现代开发需求。本文将带你从零开始构建一个现代化的前端脚手架,它不仅仅是一个简单的CLI工具,更是提升团队开发效率和代码质量的完整解决方案。
为什么需要自定义脚手架?
你可能已经使用过 create-react-app、Vue CLI 或 Vite 等流行脚手架,它们确实提供了优秀的开箱即用体验。但在企业级开发中,我们往往需要:
- 统一技术栈:确保团队使用相同的依赖版本和工具链
- 项目规范:一致的代码风格、目录结构和最佳实践
- 自动化配置:集成CI/CD、代码检查、测试等工具
- 业务定制:包含公司特定的组件库、工具函数和配置
脚手架架构设计
一个完整的脚手架系统通常包含以下核心模块:
modern-scaffold/
├── packages/
│ ├── cli/ # 命令行工具
│ ├── templates/ # 项目模板
│ ├── utils/ # 共享工具函数
│ └── generator/ # 代码生成器
├── docs/ # 文档
└── examples/ # 示例项目
第一步:构建CLI核心
让我们从最基础的CLI工具开始。我们将使用 commander 处理命令行参数,inquirer 提供交互式问答,chalk 美化输出。
// packages/cli/bin/cli.js
#!/usr/bin/env node
const { program } = require('commander');
const inquirer = require('inquirer');
const chalk = require('chalk');
const fs = require('fs-extra');
const path = require('path');
const { spawn } = require('child_process');
// 定义脚手架版本
program
.version('1.0.0')
.description('Modern Frontend Scaffold CLI');
// 创建项目命令
program
.command('create <project-name>')
.description('创建一个新项目')
.option('-t, --template <template>', '指定项目模板')
.option('-f, --force', '强制覆盖已存在目录')
.action(async (projectName, options) => {
console.log(chalk.cyan(`🚀 开始创建项目: ${projectName}`));
// 检查目录是否存在
const targetDir = path.join(process.cwd(), projectName);
if (fs.existsSync(targetDir)) {
if (options.force) {
await fs.remove(targetDir);
} else {
const { overwrite } = await inquirer.prompt([
{
type: 'confirm',
name: 'overwrite',
message: '目录已存在,是否覆盖?',
default: false
}
]);
if (!overwrite) {
console.log(chalk.yellow('操作已取消'));
return;
}
await fs.remove(targetDir);
}
}
// 选择模板
let template = options.template;
if (!template) {
const { selectedTemplate } = await inquirer.prompt([
{
type: 'list',
name: 'selectedTemplate',
message: '请选择项目模板',
choices: [
{ name: 'React + TypeScript + Vite', value: 'react-ts' },
{ name: 'Vue 3 + TypeScript + Vite', value: 'vue-ts' },
{ name: 'Next.js + TypeScript', value: 'next-ts' }
]
}
]);
template = selectedTemplate;
}
// 收集项目配置
const answers = await inquirer.prompt([
{
type: 'input',
name: 'description',
message: '项目描述',
default: 'A modern frontend project'
},
{
type: 'input',
name: 'author',
message: '作者',
default: ''
},
{
type: 'confirm',
name: 'useEslint',
message: '是否启用ESLint代码检查?',
default: true
},
{
type: 'confirm',
name: 'usePrettier',
message: '是否启用Prettier代码格式化?',
default: true
},
{
type: 'confirm',
name: 'useHusky',
message: '是否启用Git Hooks?',
default: true
}
]);
// 创建项目
await createProject(projectName, template, answers);
});
program.parse(process.argv);
第二步:实现模板系统
模板系统是脚手架的核心。我们将使用 handlebars 作为模板引擎,支持条件渲染和变量替换。
// packages/cli/src/createProject.js
const fs = require('fs-extra');
const path = require('path');
const handlebars = require('handlebars');
const { execSync } = require('child_process');
async function createProject(projectName, template, config) {
const targetDir = path.join(process.cwd(), projectName);
const templateDir = path.join(__dirname, '../../templates', template);
// 检查模板是否存在
if (!fs.existsSync(templateDir)) {
throw new Error(`模板 ${template} 不存在`);
}
// 复制模板文件
await fs.copy(templateDir, targetDir);
// 处理模板文件
await processTemplateFiles(targetDir, {
projectName,
...config,
year: new Date().getFullYear()
});
// 初始化Git仓库
if (config.useHusky) {
execSync('git init', { cwd: targetDir, stdio: 'inherit' });
}
// 安装依赖
console.log(chalk.cyan('📦 正在安装依赖...'));
execSync('npm install', { cwd: targetDir, stdio: 'inherit' });
console.log(chalk.green(`✅ 项目 ${projectName} 创建成功!`));
console.log(chalk.blue(`📁 目录: ${targetDir}`));
console.log(chalk.blue('🚀 启动项目: npm run dev'));
}
async function processTemplateFiles(dir, data) {
const files = await fs.readdir(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = await fs.stat(filePath);
if (stat.isDirectory()) {
await processTemplateFiles(filePath, data);
} else if (file.endsWith('.hbs')) {
// 处理模板文件
const content = await fs.readFile(filePath, 'utf-8');
const template = handlebars.compile(content);
const result = template(data);
// 写入处理后的文件,并移除.hbs扩展名
const newFilePath = filePath.replace(/\.hbs$/, '');
await fs.writeFile(newFilePath, result);
await fs.remove(filePath);
} else if (file === 'package.json') {
// 处理package.json
const content = await fs.readFile(filePath, 'utf-8');
const pkg = JSON.parse(content);
// 更新package.json信息
pkg.name = data.projectName;
pkg.description = data.description;
pkg.author = data.author;
// 根据配置添加或移除脚本和依赖
if (!data.useEslint) {
delete pkg.scripts.lint;
delete pkg.devDependencies.eslint;
}
if (!data.usePrettier) {
delete pkg.devDependencies.prettier;
}
await fs.writeFile(filePath, JSON.stringify(pkg, null, 2));
}
}
}
第三步:创建智能模板
让我们创建一个React + TypeScript + Vite的智能模板:
// templates/react-ts/package.json.hbs
{
"name": "{{projectName}}",
"version": "1.0.0",
"description": "{{description}}",
"author": "{{author}}",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
{{#if useEslint}}
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
{{/if}}
{{#if useHusky}}
"prepare": "husky install",
{{/if}}
"type-check": "tsc --noEmit"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.0"
},
"devDependencies": {