从零开始构建脚手架(Build Scaffold from Scratch)
从创建一个空目录开始,逐步搭建出完整的前端脚手架系统,包括 CLI 命令处理、用户交互、文件生成等核心功能。
📋 目录
1. 项目初始化
1.1 创建项目目录
mkdir my-cli
cd my-cli
npm init -y1.2 安装依赖
# 核心依赖
npm install commander inquirer chalk fs-extra ejs
# 开发依赖
npm install -D typescript @types/node @types/inquirer ts-node nodemon1.3 目录结构
my-cli/
├── package.json
├── tsconfig.json
├── bin/
│ └── cli.js # CLI 入口文件
├── src/
│ ├── index.ts # 主入口
│ ├── commands/ # 命令处理
│ │ └── create.ts
│ ├── utils/ # 工具函数
│ │ ├── logger.ts
│ │ └── file.ts
│ └── templates/ # 模板文件
│ └── basic/
└── dist/ # 编译输出
1.4 package.json 配置
{
"name": "my-cli",
"version": "1.0.0",
"description": "前端脚手架工具",
"main": "dist/index.js",
"bin": {
"my-cli": "./bin/cli.js"
},
"scripts": {
"build": "tsc",
"dev": "ts-node src/index.ts",
"start": "node dist/index.js"
},
"keywords": ["cli", "scaffold", "frontend"],
"author": "",
"license": "MIT"
}1.5 TypeScript 配置
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}2. CLI 命令处理
2.1 创建 CLI 入口文件
#!/usr/bin/env node
// bin/cli.js
require('../dist/index.js');2.2 主入口文件
// src/index.ts
import { Command } from 'commander';
import { createCommand } from './commands/create';
const program = new Command();
program
.name('my-cli')
.description('前端脚手架工具')
.version('1.0.0');
// 注册 create 命令
program
.command('create')
.description('创建新项目')
.argument('<project-name>', '项目名称')
.option('-t, --template <template>', '选择模板', 'basic-js')
.option('--no-install', '不安装依赖')
.option('--package-manager <manager>', '包管理器', 'npm')
.action(async (projectName, options) => {
await createCommand(projectName, options);
});
program.parse();2.3 Create 命令实现
// src/commands/create.ts
import inquirer from 'inquirer';
import { createProject } from '../utils/file';
import { installDependencies } from '../utils/package';
import { logger } from '../utils/logger';
export async function createCommand(
projectName: string,
options: {
template?: string;
install?: boolean;
packageManager?: string;
}
) {
try {
// 1. 收集用户输入
const answers = await collectUserInput(projectName, options);
// 2. 创建项目
logger.info(`正在创建项目: ${projectName}`);
await createProject(projectName, answers);
// 3. 安装依赖
if (answers.installDeps) {
logger.info('正在安装依赖...');
await installDependencies(projectName, answers.packageManager);
}
logger.success(`项目 ${projectName} 创建成功!`);
} catch (error) {
logger.error(`创建项目失败: ${error.message}`);
process.exit(1);
}
}
async function collectUserInput(
projectName: string,
options: any
): Promise<any> {
// 如果已经通过命令行参数指定,直接使用
if (options.template && options.packageManager) {
return {
projectName,
template: options.template,
installDeps: options.install !== false,
packageManager: options.packageManager,
initGit: false
};
}
// 否则进行交互式询问
const answers = await inquirer.prompt([
{
type: 'list',
name: 'template',
message: '选择项目模板:',
choices: [
{ name: 'Basic JavaScript', value: 'basic-js' },
{ name: 'Basic TypeScript', value: 'basic-ts' },
{ name: 'React', value: 'react' },
{ name: 'React + TypeScript', value: 'react-ts' },
{ name: 'Vue', value: 'vue' },
{ name: 'Vue + TypeScript', value: 'vue-ts' }
],
default: options.template || 'basic-js'
},
{
type: 'list',
name: 'packageManager',
message: '选择包管理器:',
choices: ['npm', 'yarn', 'pnpm'],
default: options.packageManager || 'npm'
},
{
type: 'confirm',
name: 'installDeps',
message: '是否立即安装依赖?',
default: options.install !== false
},
{
type: 'confirm',
name: 'initGit',
message: '是否初始化 Git 仓库?',
default: true
}
]);
return {
projectName,
...answers
};
}3. 用户交互实现
3.1 使用 Inquirer
import inquirer from 'inquirer';
// 文本输入
const { name } = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: '请输入项目名称:',
validate: (input) => {
if (!input.trim()) {
return '项目名称不能为空';
}
return true;
}
}
]);
// 选择列表
const { template } = await inquirer.prompt([
{
type: 'list',
name: 'template',
message: '选择模板:',
choices: ['react', 'vue', 'angular']
}
]);
// 确认
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: '是否继续?',
default: true
}
]);
// 多选
const { features } = await inquirer.prompt([
{
type: 'checkbox',
name: 'features',
message: '选择功能:',
choices: [
{ name: 'ESLint', value: 'eslint' },
{ name: 'Prettier', value: 'prettier' },
{ name: 'Husky', value: 'husky' }
]
}
]);3.2 自定义提示样式
import chalk from 'chalk';
// 使用 chalk 美化输出
console.log(chalk.blue('正在创建项目...'));
console.log(chalk.green('✓ 项目创建成功'));
console.log(chalk.red('✗ 创建失败'));
console.log(chalk.yellow('⚠ 警告信息'));4. 文件生成与目录创建
4.1 文件工具函数
// src/utils/file.ts
import fs from 'fs-extra';
import path from 'path';
import ejs from 'ejs';
export async function createProject(
projectName: string,
options: {
template: string;
[key: string]: any;
}
) {
const targetDir = path.resolve(process.cwd(), projectName);
const templateDir = path.resolve(__dirname, '../templates', options.template);
// 检查目录是否存在
if (await fs.pathExists(targetDir)) {
throw new Error(`目录 ${projectName} 已存在`);
}
// 检查模板是否存在
if (!(await fs.pathExists(templateDir))) {
throw new Error(`模板 ${options.template} 不存在`);
}
// 创建目标目录
await fs.ensureDir(targetDir);
// 复制并处理模板文件
await copyTemplate(templateDir, targetDir, {
projectName,
...options
});
}
async function copyTemplate(
srcDir: string,
targetDir: string,
data: any
) {
const files = await fs.readdir(srcDir);
for (const file of files) {
const srcPath = path.join(srcDir, file);
const targetPath = path.join(targetDir, file);
const stat = await fs.stat(srcPath);
if (stat.isDirectory()) {
await fs.ensureDir(targetPath);
await copyTemplate(srcPath, targetPath, data);
} else {
// 处理模板文件
if (file.endsWith('.ejs')) {
const template = await fs.readFile(srcPath, 'utf-8');
const content = ejs.render(template, data);
const targetFile = targetPath.replace(/\.ejs$/, '');
await fs.writeFile(targetFile, content);
} else {
await fs.copy(srcPath, targetPath);
}
}
}
}4.2 模板文件示例
// templates/basic-js/package.json.ejs
{
"name": "<%= projectName %>",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"keywords": [],
"author": "<%= author || '' %>",
"license": "MIT"
}// templates/basic-js/index.js
console.log('Hello, <%= projectName %>!');5. 包管理器兼容
5.1 包管理器工具函数
// src/utils/package.ts
import { execSync } from 'child_process';
import path from 'path';
import { logger } from './logger';
export async function installDependencies(
projectName: string,
packageManager: 'npm' | 'yarn' | 'pnpm'
) {
const projectPath = path.resolve(process.cwd(), projectName);
try {
logger.info(`使用 ${packageManager} 安装依赖...`);
const commands = {
npm: 'npm install',
yarn: 'yarn install',
pnpm: 'pnpm install'
};
execSync(commands[packageManager], {
cwd: projectPath,
stdio: 'inherit'
});
logger.success('依赖安装完成');
} catch (error) {
logger.error(`依赖安装失败: ${error.message}`);
throw error;
}
}
export async function initGit(projectPath: string) {
try {
logger.info('初始化 Git 仓库...');
execSync('git init', { cwd: projectPath, stdio: 'inherit' });
logger.success('Git 仓库初始化完成');
} catch (error) {
logger.warn(`Git 初始化失败: ${error.message}`);
}
}6. 错误处理和日志系统
6.1 日志工具
// src/utils/logger.ts
import chalk from 'chalk';
export const logger = {
info: (message: string) => {
console.log(chalk.blue('ℹ'), message);
},
success: (message: string) => {
console.log(chalk.green('✓'), message);
},
error: (message: string) => {
console.log(chalk.red('✗'), message);
},
warn: (message: string) => {
console.log(chalk.yellow('⚠'), message);
},
step: (message: string) => {
console.log(chalk.cyan('→'), message);
}
};6.2 错误处理
// src/utils/error.ts
import { logger } from './logger';
export class ScaffoldError extends Error {
constructor(message: string, public code?: string) {
super(message);
this.name = 'ScaffoldError';
}
}
export function handleError(error: unknown) {
if (error instanceof ScaffoldError) {
logger.error(error.message);
process.exit(1);
} else if (error instanceof Error) {
logger.error(error.message);
process.exit(1);
} else {
logger.error('未知错误');
process.exit(1);
}
}7. 完整代码示例
7.1 项目结构
my-cli/
├── package.json
├── tsconfig.json
├── bin/
│ └── cli.js
├── src/
│ ├── index.ts
│ ├── commands/
│ │ └── create.ts
│ ├── utils/
│ │ ├── logger.ts
│ │ ├── file.ts
│ │ └── package.ts
│ └── templates/
│ └── basic-js/
│ ├── package.json.ejs
│ └── index.js
└── dist/
7.2 使用示例
# 开发模式运行
npm run dev create my-app
# 构建
npm run build
# 链接到全局(开发测试)
npm link
# 使用
my-cli create my-app
my-cli create my-app --template react-ts
my-cli create my-app --template vue --package-manager pnpm7.3 运行效果
$ my-cli create my-app
? 选择项目模板: (Use arrow keys)
❯ Basic JavaScript
Basic TypeScript
React
React + TypeScript
Vue
Vue + TypeScript
? 选择包管理器: (Use arrow keys)
❯ npm
yarn
pnpm
? 是否立即安装依赖? (Y/n) Y
? 是否初始化 Git 仓库? (Y/n) Y
ℹ 正在创建项目: my-app
✓ 项目创建成功
ℹ 正在安装依赖...
✓ 依赖安装完成
✓ Git 仓库初始化完成
✓ 项目 my-app 创建成功!📝 总结
核心功能
- ✅ CLI 命令处理(Commander)
- ✅ 用户交互(Inquirer)
- ✅ 文件生成(fs-extra + EJS)
- ✅ 包管理器兼容(npm/yarn/pnpm)
- ✅ 错误处理和日志(Chalk)
下一步
- 模板系统实现 - 实现完整的模板系统,支持多种框架模板
脚手架 前端工程化 CLI工具 Node.js TypeScript