脚手架开发流程
需求分析 --> 技术选型 --> 项目初始化 --> 命令行设计 --> 模板管理 --> 项目生成 --> 依赖安装 --> 测试验证 --> 发布部署技术架构
命令行解析:commander 和 inquirer 库来实现即可吧
模板引擎:模板的下载和模板的渲染实现,ejs 或者说 handlerbar 来实现
文件操作:目录操作和文件读写的实现
依赖管理:包管理器和依赖安装实现
用户交互:进度处理和错误处理
必须依赖
commander 进行项目中管理命令的第三方库:注册命令以及构建命令行界面
inquirer 实现的式交互式的问答和用户输入输出的收集
download-git-repo 远程拉取 git 仓库资源,以及实现下载项目开发模板实现讷
child_process Nodejs 子进程模块,执行子进程的一些命令,核心用来执行的是 npm install 命令吧
ora 控制台的loading 显示实现,提供加载动画的效果实现
chalk 每个控制台的输入输出,显示不同的颜色文本吧
semver 判断版本是否符合预期效果,进行版本号的管理实现讷
fs-extra 文件系统的操作,增强型的文件操作方案讷
核心模块解析
命令行解析
核心使用的就是我们的 commander 工具来实现吧
import commander from "commander"
// 创建出commander对象出来
const program = commander()
// 基础的命令行工具的信息注册定义吧
program
.name("create-lhx")
.description("juwenzhang 的工程化工具")
.version("1.0.0")
.action(() => {
console.log("hello create-lhx")
})
// 注册命令实现
program
.command("create <projectName>")
.description("创建项目")
.option('-t, --template <template>', "用于指定项目模板,可以是:react、vue")
.option('-f, --force', "是否覆盖已存在的项目")
.action((projectName, options) => {
require('./commands/create')(projectName, options);
})
// 进行命令行的解析实现
program.parse(process.argv)
交互式问答
核心就是使用 inquirer 库来实现吧
import inquirer from "inquirer"
async function getUserInput() {
const answers = await inquirer.prompt([
{
type: "input",
name: "projectName",
message: "请输入项目名称",
validate: (input) => {
if (!input.trim()) {
console.log("项目名称不能为空")
return false
}
return true
}
},
{
type: "list",
name: "template",
message: "请选择项目模板",
choices: [
{ name: "react", value: "react" },
{ name: "vue", value: "vue" }
]
},
{
type: "checkbox",
name: "features",
message: "请选择项目功能",
choices: [
{ name: "react-router", value: "react-router" },
{ name: "redux", value: "redux" },
{ name: "axios", value: "axios" },
{ name: "eslint", value: "eslint" },
{ name: "prettier", value: "prettier" },
{ name: "jest", value: "jest" },
{ name: "husky", value: "husky" }
]
}
])
return answers
}
模板下载
核心就是基于 download-git-repo 来实现吧
import download from "download-git-repo"
import ora from "ora" // 用于打印loading动画
import chalk from "chalk" // 用于打印彩色文本
async function downloadTemplate(template, destination) {
const spinner = ora("正在下载模板中...").start();
try {
await new Promise((resolve, reject) => {
if (template.startsWith("github:")) {
download(template, destination, {
clone: true,
shallow: true,
}, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
} else if (template.startsWith("https://")) {
download(template, destination, {
clone: true,
shallow: true,
}, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
}
})
spinner.succeed("模板下载完成");
} catch (err) {
spinner.fail(chalk.red("模板下载失败"));
throw err;
}
}命令行设计
注册一级命令
就是注册配置一下package.json 中的 bin 字段吧
{
"name": "my-cli",
"version": "1.0.1",
"description": "我的前端脚手架工具",
"main": "./bin/index.js",
"bin": {
"my-cli": "./bin/index.js"
},
"scripts": {
"test": "jest"
},
"keywords": ["cli", "scaffold", "generator"],
"author": "Your Name",
"license": "MIT"
}注册二级命令
#!/usr/bin/env node
const { Command } = require('commander');
const chalk = require('chalk');
const semver = require('semver');
const requiredVersion = require('../package.json').engines.node;
// 检查 Node.js 版本
function checkNodeVersion() {
if (!semver.satisfies(process.version, requiredVersion)) {
console.log(chalk.red(
`您的 Node.js 版本是 ${process.version}, 但是此工具需要 ${requiredVersion}.\n` +
'请升级您的 Node.js 版本.'
));
process.exit(1);
}
}
// 主程序
function main() {
checkNodeVersion();
const program = new Command();
program
.name('my-cli')
.description('我的前端脚手架工具')
.version(require('../package.json').version)
.usage('<command> [options]');
// 创建项目命令
program
.command('create <project-name>')
.description('创建新项目')
.option('-t, --template <template>', '指定模板', 'vue3-ts')
.option('-f, --force', '强制覆盖现有目录')
.option('--skip-install', '跳过依赖安装')
.action(async (projectName, options) => {
const createCommand = require('../commands/create');
await createCommand(projectName, options);
});
// 初始化命令
program
.command('init')
.description('在当前目录初始化项目')
.option('-t, --template <template>', '指定模板', 'vue3-ts')
.action(async (options) => {
const initCommand = require('../commands/init');
await initCommand(options);
});
// 列出模板命令
program
.command('list')
.alias('ls')
.description('列出所有可用模板')
.action(() => {
const listCommand = require('../commands/list');
listCommand();
});
// 添加模板命令
program
.command('add <template>')
.description('添加新模板')
.option('-u, --url <url>', '模板仓库地址')
.option('-b, --branch <branch>', '指定分支', 'main')
.action(async (template, options) => {
const addCommand = require('../commands/add');
await addCommand(template, options);
});
// 删除模板命令
program
.command('remove <template>')
.alias('rm')
.description('删除模板')
.action(async (template) => {
const removeCommand = require('../commands/remove');
await removeCommand(template);
});
// 帮助信息
program.on('--help', () => {
console.log();
console.log('Examples:');
console.log(' $ my-cli create my-project');
console.log(' $ my-cli create my-project --template vue3-ts');
console.log(' $ my-cli init --template react-ts');
console.log(' $ my-cli list');
console.log(' $ my-cli add my-template --url https://github.com/user/repo.git');
});
// 解析命令行参数
program.parse(process.argv);
// 如果没有参数,显示帮助
if (!process.argv.slice(2).length) {
program.outputHelp();
}
}
// 错误处理
process.on('unhandledRejection', (err) => {
console.error(chalk.red('未处理的Promise拒绝:'));
console.error(err);
process.exit(1);
});
process.on('uncaughtException', (err) => {
console.error(chalk.red('未捕获的异常:'));
console.error(err);
process.exit(1);
});
main();模板管理
模板配置
// config/templates.js
module.exports = {
'vue3-ts': {
name: 'Vue 3 + TypeScript',
description: 'Vue 3 + TypeScript + Vite 模板',
url: 'github:my-templates/vue3-typescript-template',
branch: 'main',
offline: false
},
'react-ts': {
name: 'React + TypeScript',
description: 'React + TypeScript + Vite 模板',
url: 'github:my-templates/react-typescript-template',
branch: 'main',
offline: false
},
'node-api': {
name: 'Node.js API',
description: 'Node.js + Express + TypeScript API 模板',
url: 'github:my-templates/node-api-template',
branch: 'main',
offline: false
}
};模板列表处理
const chalk = require('chalk');
const templates = require('../config/templates');
function listTemplates() {
console.log(chalk.bold.blue('\n📋 可用模板列表:\n'));
Object.keys(templates).forEach(key => {
const template = templates[key];
console.log(` ${chalk.bold.cyan(key)}`);
console.log(` ${chalk.gray('名称:')} ${template.name}`);
console.log(` ${chalk.gray('描述:')} ${template.description}`);
console.log(` ${chalk.gray('地址:')} ${template.url}`);
console.log(` ${chalk.gray('分支:')} ${template.branch}`);
console.log('');
});
}
module.exports = listTemplates;项目生成
核心创建命令
// commands/create.js
const path = require('path');
const fs = require('fs-extra');
const chalk = require('chalk');
const ora = require('ora');
const inquirer = require('inquirer');
const download = require('download-git-repo');
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
const templates = require('../config/templates');
async function createProject(projectName, options) {
const targetDir = path.resolve(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: `目录 ${projectName} 已存在,是否覆盖?`,
default: false
}
]);
if (!overwrite) {
console.log(chalk.yellow('取消创建'));
return;
}
await fs.remove(targetDir);
}
}
// 获取模板配置
let template = options.template;
if (!templates[template]) {
const { selectedTemplate } = await inquirer.prompt([
{
type: 'list',
name: 'selectedTemplate',
message: '请选择项目模板:',
choices: Object.keys(templates).map(key => ({
name: `${templates[key].name} - ${templates[key].description}`,
value: key
}))
}
]);
template = selectedTemplate;
}
const templateConfig = templates[template];
// 收集项目信息
const projectInfo = await collectProjectInfo(projectName, templateConfig);
// 下载模板
await downloadTemplate(templateConfig, targetDir);
// 渲染模板
await renderTemplate(targetDir, projectInfo);
// 安装依赖
if (!options.skipInstall) {
await installDependencies(targetDir);
}
// 初始化 Git
await initGit(targetDir);
console.log(chalk.green(`\n🎉 项目 ${projectName} 创建成功!\n`));
console.log(chalk.cyan('下一步:'));
console.log(chalk.cyan(` cd ${projectName}`));
if (options.skipInstall) {
console.log(chalk.cyan(` npm install`));
}
console.log(chalk.cyan(` npm run dev`));
}
async function collectProjectInfo(projectName, templateConfig) {
const questions = [
{
type: 'input',
name: 'author',
message: '作者:',
default: 'Your Name'
},
{
type: 'input',
name: 'description',
message: '项目描述:',
default: `${templateConfig.name} project`
},
{
type: 'input',
name: 'version',
message: '版本号:',
default: '1.0.0'
}
];
// 根据模板动态添加问题
if (templateConfig.name.includes('Vue')) {
questions.push({
type: 'confirm',
name: 'useRouter',
message: '是否使用 Vue Router?',
default: true
});
questions.push({
type: 'confirm',
name: 'usePinia',
message: '是否使用 Pinia 状态管理?',
default: true
});
}
const answers = await inquirer.prompt(questions);
return {
name: projectName,
...answers
};
}
async function downloadTemplate(templateConfig, destination) {
const spinner = ora('正在下载模板...').start();
try {
await new Promise((resolve, reject) => {
const repo = templateConfig.url.replace('github:', '');
const url = `direct:https://github.com/${repo}.git#${templateConfig.branch}`;
download(url, destination, { clone: true }, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
spinner.succeed(chalk.green('模板下载成功'));
} catch (error) {
spinner.fail(chalk.red('模板下载失败'));
throw error;
}
}
async function renderTemplate(targetDir, projectInfo) {
const spinner = ora('正在渲染模板...').start();
try {
// 读取并更新 package.json
const packageJsonPath = path.join(targetDir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
const packageJson = await fs.readJson(packageJsonPath);
packageJson.name = projectInfo.name;
packageJson.version = projectInfo.version;
packageJson.description = projectInfo.description;
packageJson.author = projectInfo.author;
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
}
// 处理模板变量
const filesToProcess = [
'README.md',
'src/main.ts',
'src/App.vue',
'index.html'
];
for (const file of filesToProcess) {
const filePath = path.join(targetDir, file);
if (fs.existsSync(filePath)) {
let content = await fs.readFile(filePath, 'utf-8');
// 替换模板变量
content = content.replace(/\{\{name\}\}/g, projectInfo.name);
content = content.replace(/\{\{description\}\}/g, projectInfo.description);
content = content.replace(/\{\{author\}\}/g, projectInfo.author);
await fs.writeFile(filePath, content);
}
}
// 删除 .git 目录
const gitDir = path.join(targetDir, '.git');
if (fs.existsSync(gitDir)) {
await fs.remove(gitDir);
}
spinner.succeed(chalk.green('模板渲染成功'));
} catch (error) {
spinner.fail(chalk.red('模板渲染失败'));
throw error;
}
}
async function installDependencies(targetDir) {
const spinner = ora('正在安装依赖...').start();
try {
const { stdout } = await execAsync('npm install', {
cwd: targetDir,
stdio: 'pipe'
});
spinner.succeed(chalk.green('依赖安装成功'));
} catch (error) {
spinner.fail(chalk.red('依赖安装失败'));
console.log(chalk.yellow('您可以手动运行 npm install 来安装依赖'));
}
}
async function initGit(targetDir) {
const spinner = ora('正在初始化 Git...').start();
try {
await execAsync('git init', { cwd: targetDir });
await execAsync('git add .', { cwd: targetDir });
await execAsync('git commit -m "feat: initial commit"', { cwd: targetDir });
spinner.succeed(chalk.green('Git 初始化成功'));
} catch (error) {
spinner.fail(chalk.red('Git 初始化失败'));
console.log(chalk.yellow('您可以手动初始化 Git 仓库'));
}
}
module.exports = createProject;插件系统设计
// lib/PluginManager.js
class PluginManager {
constructor() {
this.plugins = [];
}
addPlugin(plugin) {
this.plugins.push(plugin);
}
async runHook(hookName, context) {
for (const plugin of this.plugins) {
if (plugin[hookName]) {
await plugin[hookName](context);
}
}
}
}
// 插件示例
class ESLintPlugin {
async beforeCreate(context) {
console.log('准备配置 ESLint...');
}
async afterCreate(context) {
// 添加 ESLint 配置
const eslintConfig = {
extends: ['@vue/typescript/recommended'],
rules: {
'no-console': 'warn'
}
};
await fs.writeJson(
path.join(context.targetDir, '.eslintrc.json'),
eslintConfig,
{ spaces: 2 }
);
}
}
module.exports = { PluginManager, ESLintPlugin };进度跟踪
// lib/ProgressTracker.js
const chalk = require('chalk');
class ProgressTracker {
constructor(steps) {
this.steps = steps;
this.current = 0;
}
start() {
console.log(chalk.blue(`\n🚀 开始执行 ${this.steps.length} 个步骤...\n`));
}
nextStep(message) {
this.current++;
console.log(chalk.cyan(`[${this.current}/${this.steps.length}] ${message}`));
}
complete() {
console.log(chalk.green(`\n✅ 所有步骤完成!`));
}
error(message) {
console.log(chalk.red(`\n❌ 错误: ${message}`));
}
}
module.exports = ProgressTracker;模板缓存
// lib/TemplateCache.js
const path = require('path');
const fs = require('fs-extra');
const os = require('os');
class TemplateCache {
constructor() {
this.cacheDir = path.join(os.homedir(), '.my-cli', 'cache');
this.ensureCacheDir();
}
ensureCacheDir() {
if (!fs.existsSync(this.cacheDir)) {
fs.mkdirpSync(this.cacheDir);
}
}
getCachePath(template) {
return path.join(this.cacheDir, template);
}
exists(template) {
return fs.existsSync(this.getCachePath(template));
}
async get(template) {
const cachePath = this.getCachePath(template);
if (this.exists(template)) {
return cachePath;
}
return null;
}
async set(template, sourcePath) {
const cachePath = this.getCachePath(template);
await fs.copy(sourcePath, cachePath);
return cachePath;
}
async clear(template) {
const cachePath = this.getCachePath(template);
if (this.exists(template)) {
await fs.remove(cachePath);
}
}
async clearAll() {
await fs.remove(this.cacheDir);
this.ensureCacheDir();
}
}
module.exports = TemplateCache;最佳实践
my-cli/
├── bin/
│ └── index.js # 入口文件
├── commands/
│ ├── create.js # 创建命令
│ ├── init.js # 初始化命令
│ ├── list.js # 列表命令
│ └── add.js # 添加模板命令
├── config/
│ ├── templates.js # 模板配置
│ └── index.js # 全局配置
├── lib/
│ ├── PluginManager.js # 插件管理
│ ├── TemplateCache.js # 模板缓存
│ └── utils.js # 工具函数
├── templates/
│ ├── vue3-ts/ # Vue 3 TypeScript 模板
│ ├── react-ts/ # React TypeScript 模板
│ └── node-api/ # Node.js API 模板
├── tests/
│ └── *.test.js # 测试文件
├── package.json
└── README.md