从零构建一个现代化的前端脚手架:不只是CLI,更是开发体验的革命

5 阅读1分钟

在当今快节奏的前端开发领域,如何快速启动一个新项目、统一团队技术栈、保证代码质量一致性,是每个团队都面临的挑战。传统的复制粘贴模板项目的方式已经无法满足现代开发需求。本文将带你从零开始构建一个现代化的前端脚手架,它不仅仅是一个简单的CLI工具,更是提升团队开发效率和代码质量的完整解决方案。

为什么需要自定义脚手架?

你可能已经使用过 create-react-appVue CLIVite 等流行脚手架,它们确实提供了优秀的开箱即用体验。但在企业级开发中,我们往往需要:

  1. 统一技术栈:确保团队使用相同的依赖版本和工具链
  2. 项目规范:一致的代码风格、目录结构和最佳实践
  3. 自动化配置:集成CI/CD、代码检查、测试等工具
  4. 业务定制:包含公司特定的组件库、工具函数和配置

脚手架架构设计

一个完整的脚手架系统通常包含以下核心模块:

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": {