在当今的前端开发中,脚手架工具已经成为项目启动的标配。从 create-react-app 到 Vue CLI,这些工具极大地提升了开发效率。但你是否曾想过,这些工具背后的原理是什么?如何构建一个适合自己团队需求的脚手架工具?
本文将带你从零开始,构建一个功能完整的现代化 Node.js 脚手架工具。我们将不仅实现基本的文件生成功能,还会加入模板系统、用户交互、依赖安装等高级特性。
为什么需要自定义脚手架?
你可能会有疑问:已经有那么多成熟的脚手架工具,为什么还要自己造轮子?
- 团队规范统一:统一的目录结构、代码规范和开发流程
- 技术栈定制:根据团队技术栈定制模板,避免每次手动配置
- 效率提升:一键生成项目基础结构,节省重复劳动时间
- 知识沉淀:将最佳实践固化到工具中,降低新人上手成本
项目规划
我们的脚手架工具将具备以下功能:
- 命令行交互(选择模板、配置项目)
- 模板系统(支持本地和远程模板)
- 文件生成与变量替换
- 自动安装依赖
- Git 初始化
1. 项目初始化
首先创建我们的脚手架项目:
mkdir my-cli && cd my-cli
npm init -y
修改 package.json:
{
"name": "my-cli",
"version": "1.0.0",
"description": "A modern CLI tool for project scaffolding",
"bin": {
"my-cli": "./bin/cli.js"
},
"scripts": {
"start": "node ./bin/cli.js"
},
"keywords": ["cli", "scaffold", "boilerplate"],
"author": "Your Name",
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2",
"commander": "^9.4.1",
"inquirer": "^8.2.5",
"ora": "^5.4.1",
"fs-extra": "^10.1.0",
"download-git-repo": "^3.0.2"
}
}
2. 核心架构设计
我们的脚手架将采用以下架构:
my-cli/
├── bin/
│ └── cli.js # 命令行入口
├── lib/
│ ├── commands/ # 命令模块
│ ├── templates/ # 模板管理
│ ├── utils/ # 工具函数
│ └── generators/ # 生成器
├── templates/ # 本地模板
└── package.json
3. 实现命令行入口
创建 bin/cli.js:
#!/usr/bin/env node
const { program } = require('commander');
const chalk = require('chalk');
const createCommand = require('../lib/commands/create');
// 设置版本信息
program
.version('1.0.0')
.description('A modern CLI tool for project scaffolding');
// 创建项目命令
program
.command('create <project-name>')
.description('Create a new project')
.option('-t, --template <template>', 'Specify template name')
.option('-f, --force', 'Overwrite target directory if it exists')
.action(async (projectName, options) => {
try {
await createCommand(projectName, options);
} catch (error) {
console.error(chalk.red('Error:', error.message));
process.exit(1);
}
});
// 模板列表命令
program
.command('list')
.description('List all available templates')
.action(() => {
console.log(chalk.blue('Available templates:'));
console.log(' - react-template');
console.log(' - vue-template');
console.log(' - node-template');
});
// 解析命令行参数
program.parse(process.argv);
4. 实现创建命令
创建 lib/commands/create.js:
const path = require('path');
const fs = require('fs-extra');
const inquirer = require('inquirer');
const chalk = require('chalk');
const ora = require('ora');
const Generator = require('../generators/project-generator');
module.exports = async function createCommand(projectName, options) {
const cwd = process.cwd();
const targetDir = path.resolve(cwd, projectName);
// 检查目标目录是否存在
if (fs.existsSync(targetDir)) {
if (options.force) {
await fs.remove(targetDir);
} else {
const { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: `Target directory ${chalk.cyan(targetDir)} already exists. Pick an action:`,
choices: [
{ name: 'Overwrite', value: 'overwrite' },
{ name: 'Merge', value: 'merge' },
{ name: 'Cancel', value: false }
]
}
]);
if (!action) {
return;
} else if (action === 'overwrite') {
console.log(`\nRemoving ${chalk.cyan(targetDir)}...`);
await fs.remove(targetDir);
}
}
}
// 收集项目信息
const answers = await inquirer.prompt([
{
name: 'projectName',
type: 'input',
message: 'Project name:',
default: projectName,
validate: (input) => {
if (/^[a-z0-9-]+$/.test(input)) return true;
return 'Project name may only include lowercase letters, numbers, and hyphens.';
}
},
{
name: 'description',
type: 'input',
message: 'Project description:',
default: 'A new project created with my-cli'
},
{
name: 'author',
type: 'input',
message: 'Author:',
default: ''
},
{
name: 'template',
type: 'list',
message: 'Select a template:',
choices: [
{ name: 'React + TypeScript', value: 'react-ts' },
{ name: 'Vue 3 + Vite', value: 'vue3-vite' },
{ name: 'Node.js + Express', value: 'node-express' }
],
when: !options.template
}
]);
// 合并选项
const projectOptions = {
...answers,
template: options.template || answers.template,
targetDir
};
// 创建项目
const generator = new Generator(projectOptions);
await generator.generate();
console.log(chalk.green('\n✅ Project created successfully!'));
console.log(chalk.blue('\nNext steps:'));
console.log(` cd ${projectName}`);
console.log(' npm install');
console.log(' npm start\n');
};
5. 实现项目生成器
创建 lib/generators/project-generator.js:
const path = require('path');
const fs = require('fs-extra');
const { execSync } = require('child_process');
const chalk = require('chalk');
const ora = require('ora');
class ProjectGenerator {
constructor(options) {
this.options = options;
this.templateDir = this.getTemplateDir(options.template);
}
getTemplateDir(templateName) {
// 这里可以扩展为从远程仓库下载模板
const localTemplates = {
'react-ts': 'templates/react-typescript',
'vue3-vite': 'templates/vue3-vite',
'node-express': 'templates/node-express'
};
const templatePath = localTemplates[templateName];
if (!templatePath) {
throw new Error(`Template ${templateName} not found`);
}
return path.resolve(__dirname, '../../', templatePath);
}
async generate() {
const spinner = ora('Creating project...').start();
try {
// 1. 复制模板文件
await this.copyTemplate();
// 2. 处理模板变量
await this.processTemplateVariables();
// 3. 初始化 package.json
await this.initPackageJson();
// 4. 初始化 Git
await this.initGit();
spinner.succeed('Project created successfully');
// 5. 安装依赖(可选)
const { install } = await inquirer.prompt([
{
name: 'install',
type: 'confirm',
message: 'Install dependencies now?',
default: true
}
]);
if (install) {
await this.installDependencies();
}
} catch (error) {
spinner.fail('Failed to create project');
throw error;
}
}
async copyTemplate() {
const { targetDir } = this.options