引言
在当今的前端开发中,脚手架工具已经成为项目启动的标配。从 create-react-app 到 Vue CLI,这些工具极大地提升了开发效率。但你是否曾想过,这些工具是如何工作的?更重要的是,当现有工具无法满足你的特定需求时,如何构建一个属于自己的脚手架工具?
本文将带你从零开始,构建一个功能完整的现代化 Node.js 脚手架工具。我们将不仅仅实现基本的文件生成功能,还会涵盖模板渲染、用户交互、插件系统等高级特性。
为什么需要自定义脚手架?
你可能会有疑问:已经有那么多优秀的脚手架工具了,为什么还要自己造轮子?原因包括:
- 项目特定需求:公司内部项目可能有特殊的目录结构或配置要求
- 技术栈定制:需要整合特定的技术栈组合
- 流程自动化:集成公司内部的代码规范、CI/CD 配置等
- 学习价值:深入理解脚手架工具的工作原理
项目规划
我们的脚手架工具将具备以下核心功能:
- 命令行交互(询问用户配置选项)
- 模板文件渲染(支持变量替换)
- 插件系统(可扩展功能)
- Git 仓库初始化
- 依赖安装自动化
技术选型
# 我们将使用以下核心依赖
mkdir my-cli && cd my-cli
npm init -y
npm install commander inquirer ejs chalk ora fs-extra download-git-repo
- commander:命令行参数解析
- inquirer:交互式命令行界面
- ejs:模板引擎
- chalk:终端字符串样式
- ora:优雅的终端加载动画
- fs-extra:增强的 fs 模块
- download-git-repo:从 Git 仓库下载模板
核心架构设计
让我们先来看看项目的整体架构:
my-cli/
├── bin/
│ └── cli.js # 命令行入口
├── lib/
│ ├── core/
│ │ ├── Creator.js # 项目创建器
│ │ ├── Generator.js # 代码生成器
│ │ └── Plugin.js # 插件管理器
│ ├── utils/
│ │ ├── logger.js # 日志工具
│ │ └── file.js # 文件操作工具
│ └── templates/ # 模板目录
├── plugins/ # 插件目录
└── package.json
实现步骤
1. 创建命令行入口
首先,创建 bin/cli.js:
#!/usr/bin/env node
const { program } = require('commander');
const pkg = require('../package.json');
const Creator = require('../lib/core/Creator');
program
.version(pkg.version)
.description('一个现代化的项目脚手架工具')
.option('-d, --debug', '开启调试模式')
.option('-t, --template <template>', '指定模板名称');
program
.command('create <project-name>')
.description('创建一个新项目')
.action(async (projectName, options) => {
const creator = new Creator(projectName, options);
await creator.create();
});
program
.command('add <plugin-name>')
.description('添加插件')
.action(async (pluginName) => {
console.log(`添加插件: ${pluginName}`);
// 插件安装逻辑
});
program.parse(process.argv);
在 package.json 中添加 bin 配置:
{
"name": "my-cli",
"version": "1.0.0",
"bin": {
"my-cli": "./bin/cli.js"
}
}
2. 实现项目创建器
创建 lib/core/Creator.js:
const inquirer = require('inquirer');
const path = require('path');
const fs = require('fs-extra');
const chalk = require('chalk');
const Generator = require('./Generator');
const { installDependencies } = require('../utils/install');
const { initGitRepo } = require('../utils/git');
class Creator {
constructor(projectName, options) {
this.projectName = projectName;
this.options = options;
this.targetDir = path.resolve(process.cwd(), projectName);
this.answers = {};
}
async create() {
// 检查目录是否存在
if (fs.existsSync(this.targetDir)) {
const { overwrite } = await inquirer.prompt([
{
type: 'confirm',
name: 'overwrite',
message: '目录已存在,是否覆盖?',
default: false
}
]);
if (!overwrite) {
console.log(chalk.yellow('操作已取消'));
process.exit(1);
}
await fs.remove(this.targetDir);
}
// 收集用户输入
await this.promptUser();
// 创建项目目录
await fs.ensureDir(this.targetDir);
// 生成项目文件
const generator = new Generator(this.projectName, this.targetDir, this.answers);
await generator.generate();
// 初始化 Git 仓库
if (this.answers.initGit) {
await initGitRepo(this.targetDir);
}
// 安装依赖
if (this.answers.autoInstall) {
await installDependencies(this.targetDir, this.answers.packageManager);
}
// 显示成功信息
this.showSuccessMessage();
}
async promptUser() {
const prompts = [
{
type: 'input',
name: 'projectDescription',
message: '项目描述:',
default: 'A new project'
},
{
type: 'list',
name: 'template',
message: '请选择模板:',
choices: [
{ name: 'React + TypeScript', value: 'react-ts' },
{ name: 'Vue 3 + TypeScript', value: 'vue3-ts' },
{ name: 'Node.js API', value: 'node-api' }
],
default: 'react-ts'
},
{
type: 'list',
name: 'packageManager',
message: '包管理器:',
choices: ['npm', 'yarn', 'pnpm'],
default: 'npm'
},
{
type: 'confirm',
name: 'initGit',
message: '是否初始化 Git 仓库?',
default: true
},
{
type: 'confirm',
name: 'autoInstall',
message: '是否自动安装依赖?',
default: true
}
];
this.answers = await inquirer.prompt(prompts);
}
showSuccessMessage() {
console.log();
console.log(chalk.green('✅ 项目创建成功!'));
console.log();
console.log(chalk.cyan('接下来可以执行以下命令:'));
console.log();
console.log(` cd ${this.projectName}`);
if (!this.answers.autoInstall) {
console.log(` ${this.answers.packageManager} install`);
}
console.log(` ${this.answers.packageManager === 'npm' ? 'npm run' : this.answers.packageManager} dev`);
console.log();
}
}
module.exports = Creator;
3. 实现代码生成器
创建 lib/core/Generator.js:
const fs = require('fs-extra');
const path = require('path');
const ejs = require('ejs');
const chalk = require('chalk');
const ora = require('ora');
class Generator {
constructor(projectName, targetDir, answers) {
this.projectName = projectName;
this.targetDir = targetDir;
this.answers = answers;
this.templateDir = this.getTemplateDir();
}
getTemplateDir() {
// 根据模板类型返回对应的模板目录
const templateMap = {
'react-ts': path.join(__dirname, '../templates/react-ts'),
'vue3-ts': path.join(__dirname, '../templates/vue3-ts'),
'node-api': path.join(__dirname, '../templates/node-api')
};
return templateMap[this.answers.template] || templateMap['react-ts'];
}
async generate() {
const spinner = ora('正在生成项目文件...').start();
try {
// 复制模板文件
await this.copyTemplate();
// 渲染模板文件
await this.renderFiles();
// 生成 package.json
await this.generatePackageJson();
spinner.succeed(chalk.green('项目文件生成完成!'));
} catch (error) {
spinner.fail(chalk.red('文件生成失败'));
console.error(error);
process.exit(1);
}
}
async copyTemplate() {
if (!fs.existsSync(this.templateDir)) {
throw new Error(`模板目录不存在: ${this.templateDir}`);
}
await fs.copy(this.templateDir