背景
近期组长布置了一个技术任务,要标准化各工程的 readme 文件。定义好模板,具体值由使用方填充,比如该工程的产品经理主要是谁?谁是主要负责人?有什么注意事项等等。我想了下,也许可以用脚手架来帮我实现这个功能,使用方只要在原有项目跑我定义的命令,就能生成一份覆盖原工程 readme 的新定义好的 readme 文件,下面让我们来了解一下这样的脚手架该如何搭建。
一、什么是脚手架(Scaffold)
在工程领域,脚手架是:
👉 用于快速生成项目初始结构和基础配置的工具
它解决的问题是:
- 不用每次手动搭目录
- 不用重复配 webpack / vite / tsconfig
- 不用重复写 README、eslint、gitignore
- 保证项目风格统一、可维护
本质能力
脚手架 = 模板 + 参数化 + 自动生成 + 可扩展逻辑
二、一个标准脚手架应该具备什么能力
| 能力 | 说明 |
|---|---|
| 项目模板 | vue/react/node/低代码引擎/组件库等 |
| 交互式创建 | 通过命令选择类型、语言、包管理器 |
| 自动生成 README | 按项目类型输出不同 README |
| 环境初始化 | 自动装依赖、初始化 git |
| 可扩展 | 插件化 / 多模板 |
| 团队规范内置 | eslint/prettier/commitlint |
三、脚手架技术选型
核心技术栈
| 功能 | 推荐库 |
|---|---|
| CLI 构建 | commander / cac |
| 交互式问答 | inquirer / prompts |
| 文件生成 | ejs / handlebars |
| 拷贝模板 | fs-extra |
| 下载模板 | download-git-repo |
| README 模板渲染 | ejs + markdown |
四、一个“生成 README.md 的脚手架”完整实现示例
1️⃣ 初始化 CLI 项目
mkdir my-cli
cd my-cli
pnpm init
pnpm add commander inquirer fs-extra
目录结构:
my-cli/
├── bin/
│ └── index.js # CLI 入口
├── lib/
│ └── readme.js
│ └── readmeGenerator.js
├── package.json
2️⃣ CLI 入口(bin/index.js)
#!/usr/bin/env node
const program = require('commander');
// readme 相关脚本指令
program
.command('readme')
.description(`生成或更新项目的 README.md 文档`)
.option('-d, --dir <dir>', '指定项目目录路径, 默认当前目录<projectRoot>/.')
.option('-f, --force', '强制覆盖已存在的 README.md 文件')
.option('--debug', '开启debug模式, 控制台会输出日志')
.action((options) => {
require('./lib/readme')(options);
});
// 解析命令行参数
program.parse(process.argv);
3️⃣ 生成 README 文件
// lib/readme
const path = require('path');
const debug = require('debug');
const chalk = require('chalk');
const fs = require('fs-extra');
const { log, error, done, warn } = require('../utils/logger'); // 日志相关
const { stopSpinner, logWithSpinner } = require('../utils/spinner'); // spinner 相关
const { resolvePkg } = require('../utils/pkg');
const ReadmeGenerator = require('./readmeGenerator');
async function readme(options) {
// 当前命令执行所在目录
const cwd = options.cwd || process.cwd();
// 命令行选项默认值设置
options.dir = options.dir || '.';
options.force = options.force || false;
debug('debug:readme:options')(options);
const { dir } = { ...options };
const inCurrent = dir === '.';
const name = inCurrent ? path.basename(cwd) : dir;
const targetDir = path.resolve(cwd, dir || '.');
// 检查目录是否存在
debug('debug:readme:targetDir')(targetDir);
if (!fs.existsSync(targetDir)) {
return error(`项目目录 ${chalk.cyan(targetDir)} 不存在!`);
}
// 读取项目 package.json
const pkg = resolvePkg(targetDir);
if (!pkg || !pkg.name) {
return error(`项目目录 ${chalk.cyan(targetDir)} 中未找到 package.json 或 package.json 无效!`);
}
// 检查 README.md 是否已存在
const readmePath = path.resolve(targetDir, 'README.md');
const readmeExists = fs.existsSync(readmePath);
if (readmeExists && !options.force) {
warn(`README.md 文件已存在。使用 ${chalk.cyan('--force')} 选项可以强制覆盖。`);
return;
}
if (readmeExists && options.force) {
warn(`将覆盖已存在的 README.md 文件。`);
}
logWithSpinner('📝', `正在为项目 ${chalk.cyan(name)} 生成 README.md...`);
try {
const generator = new ReadmeGenerator(name, targetDir, pkg, options);
await generator.generate();
stopSpinner();
done(`项目 ${chalk.cyan(name)} 的 README.md 生成成功!`);
log(`文件路径: ${chalk.gray(readmePath)}`);
} catch (e) {
stopSpinner();
error(`生成 README.md 失败: ${e.message}`);
if (options.debug) {
console.error(e);
}
}
}
module.exports = readme;
4️⃣ README 模板
// readmeGenerator.js
const path = require('path');
const fs = require('fs-extra');
const PackageManager = require('../utils/projectPackageManager');
/**
* README 生成器
*/
module.exports = class ReadmeGenerator {
constructor(name, targetDir, pkg, options = {}) {
this.name = name;
this.targetDir = targetDir;
this.pkg = pkg;
this.options = options;
this.packageManager = new PackageManager({ context: targetDir });
}
async generate() {
const readmeContent = await this.generateReadmeContent();
const readmePath = path.resolve(this.targetDir, 'README.md');
await fs.writeFile(readmePath, readmeContent, 'utf-8');
}
async generateReadmeContent() {
const sections = [];
// 标题和描述
sections.push(this.generateTitle());
sections.push(this.generateDescription());
sections.push(this.generateBadges());
// 目录(可选)
sections.push(this.generateTableOfContents());
// 安装
sections.push(this.generateInstallation());
// 使用/快速开始
sections.push(this.generateUsage());
// 项目结构
sections.push(this.generateProjectStructure());
// 脚本命令
sections.push(this.generateScripts());
// 依赖
sections.push(this.generateDependencies());
// 开发
sections.push(this.generateDevelopment());
// 额外信息(产品负责人等)
sections.push(this.generateExtra());
// 贡献
sections.push(this.generateContributing());
// 许可证
sections.push(this.generateLicense());
// 作者信息
sections.push(this.generateAuthor());
return sections.filter(Boolean).join('\n\n');
}
generateTitle() {
const name = this.pkg.name || this.name;
return `# ${name}`;
}
generateDescription() {
const description = this.pkg.description || '';
if (description) {
return description;
}
return '';
}
generateBadges() {
const badges = [];
const pkg = this.pkg;
// npm 版本
if (pkg.version) {
badges.push(``);
}
// license
if (pkg.license) {
badges.push(``);
}
// node 版本
if (pkg.engines && pkg.engines.node) {
badges.push(``);
}
if (badges.length > 0) {
return badges.join(' ');
}
return '';
}
generateTableOfContents() {
// 简单返回一个目录,用户可以根据需要自行调整
return `## 📑 目录
- [安装](#安装)
- [快速开始](#快速开始)
- [项目结构](#项目结构)
- [可用脚本](#可用脚本)
- [开发指南](#开发指南)`;
}
generateInstallation() {
const pkgManager = this.packageManager.bin || 'npm';
const installCommand = pkgManager === 'yarn' ? 'yarn' : `${pkgManager} install`;
return `## 📦 安装
```bash
${installCommand}
````;
}
generateUsage() {
const pkg = this.pkg;
const pkgManager = this.packageManager.bin || 'npm';
// 如果有 bin 字段,说明是可执行文件
if (pkg.bin) {
let usage = '## 🚀 快速开始\n\n';
let binName;
if (typeof pkg.bin === 'string') {
binName = pkg.name;
} else if (typeof pkg.bin === 'object') {
binName = Object.keys(pkg.bin)[0] || pkg.name;
} else {
binName = pkg.name;
}
usage += `安装后,你可以直接使用:\n\n`;
usage += ````bash\n${binName}\n```\n`;
return usage;
}
// 普通项目
return `## 🚀 快速开始
```bash
# 开发模式
${pkgManager} ${pkgManager !== 'yarn' ? 'run ' : ''}serve
# 构建生产版本
${pkgManager} ${pkgManager !== 'yarn' ? 'run ' : ''}build
````;
}
generateProjectStructure() {
// 尝试读取常见的目录结构
const commonDirs = ['src', 'lib', 'dist', 'test', 'tests', 'docs', 'public', 'assets'];
const existingDirs = commonDirs.filter(dir => {
const dirPath = path.resolve(this.targetDir, dir);
return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory();
});
if (existingDirs.length === 0) {
return '';
}
let structure = '## 📁 项目结构\n\n';
structure += '```\n';
structure += `${this.name}/\n`;
// 列出主要目录
existingDirs.forEach(dir => {
structure += `├── ${dir}/\n`;
});
structure += '└── package.json\n';
structure += '```\n';
structure += '\n主要目录说明:\n';
existingDirs.forEach(dir => {
const descriptions = {
'src': '源代码目录',
'lib': '库文件目录',
'dist': '构建输出目录',
'test': '测试文件目录',
'tests': '测试文件目录',
'docs': '文档目录',
'public': '公共资源目录',
'assets': '静态资源目录'
};
structure += `- `${dir}/`: ${descriptions[dir] || '相关文件目录'}\n`;
});
return structure;
}
generateScripts() {
const scripts = this.pkg.scripts || {};
if (Object.keys(scripts).length === 0) {
return '';
}
const pkgManager = this.packageManager.bin || 'npm';
const scriptDescriptions = {
'serve': '启动开发服务器',
'dev': '启动开发服务器',
'start': '启动项目',
'build': '构建生产版本',
'build:daily': '构建日常环境版本',
'build:beta': '构建测试环境版本',
'build:prod': '构建生产环境版本',
'test': '运行测试',
'test:unit': '运行单元测试',
'test:e2e': '运行端到端测试',
'lint': '代码检查和格式化',
'format': '格式化代码',
'prepublishOnly': '发布前准备'
};
let scriptsSection = '## 📜 可用脚本\n\n';
Object.keys(scripts).forEach(key => {
const description = scriptDescriptions[key] || '执行脚本命令';
scriptsSection += `### ${description}\n\n`;
scriptsSection += ````bash\n`;
scriptsSection += `${pkgManager} ${pkgManager !== 'yarn' ? 'run ' : ''}${key}\n`;
scriptsSection += ````\n\n`;
});
return scriptsSection.trim();
}
generateDependencies() {
const deps = this.pkg.dependencies || {};
const devDeps = this.pkg.devDependencies || {};
if (Object.keys(deps).length === 0 && Object.keys(devDeps).length === 0) {
return '';
}
let depsSection = '## 📦 依赖\n\n';
if (Object.keys(deps).length > 0) {
depsSection += '### 生产依赖\n\n';
depsSection += '| 依赖 | 版本 |\n';
depsSection += '|------|------|\n';
Object.entries(deps).slice(0, 10).forEach(([name, version]) => {
depsSection += `| ${name} | ${version} |\n`;
});
if (Object.keys(deps).length > 10) {
depsSection += `| ... | 共 ${Object.keys(deps).length} 个依赖 |\n`;
}
depsSection += '\n';
}
if (Object.keys(devDeps).length > 0) {
depsSection += '### 开发依赖\n\n';
depsSection += '| 依赖 | 版本 |\n';
depsSection += '|------|------|\n';
Object.entries(devDeps).slice(0, 10).forEach(([name, version]) => {
depsSection += `| ${name} | ${version} |\n`;
});
if (Object.keys(devDeps).length > 10) {
depsSection += `| ... | 共 ${Object.keys(devDeps).length} 个依赖 |\n`;
}
}
return depsSection;
}
generateDevelopment() {
return `## 🔧 开发指南
### 环境要求
${this.pkg.engines ? `- Node.js: ${this.pkg.engines.node || '>= 12.0.0'}` : '- Node.js: >= 12.0.0'}
- ${this.packageManager.bin === 'yarn' ? 'Yarn' : this.packageManager.bin === 'pnpm' ? 'pnpm' : 'npm'}
### 开发流程
1. 克隆项目
2. 安装依赖
3. 启动开发服务器
4. 开始开发`;
}
generateContributing() {
// 检查是否有 CONTRIBUTING.md 文件
const contributingPath = path.resolve(this.targetDir, 'CONTRIBUTING.md');
if (fs.existsSync(contributingPath)) {
return `## 🤝 贡献
请阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 了解如何为项目做贡献。`;
}
return `## 🤝 贡献
欢迎提交 Issue 和 Pull Request!`;
}
generateLicense() {
const license = this.pkg.license || '';
if (license) {
return `## 📄 许可证
本项目基于 [${license}](LICENSE) 许可证开源。`;
}
return '';
}
generateAuthor() {
const pkgAuthor = this.pkg.author || '';
const repository = this.pkg.repository || {};
const homepage = this.pkg.homepage || '';
// 处理 author 字段,可能是字符串或对象
let author = '';
if (typeof pkgAuthor === 'string') {
author = pkgAuthor;
} else if (typeof pkgAuthor === 'object' && pkgAuthor !== null) {
if (pkgAuthor.name) {
author = pkgAuthor.name;
if (pkgAuthor.email) {
author += ` <${pkgAuthor.email}>`;
}
if (pkgAuthor.url) {
author += ` (${pkgAuthor.url})`;
}
}
}
// 处理 repository.url,可能是字符串或对象
let repoUrl = '';
if (typeof repository === 'string') {
repoUrl = repository;
} else if (typeof repository === 'object' && repository !== null && repository.url) {
repoUrl = repository.url;
}
let authorSection = '';
if (author || repoUrl || homepage) {
authorSection = '## 👤 作者\n\n';
if (author) {
authorSection += `- ${author}\n`;
}
if (repoUrl) {
authorSection += `- 仓库: ${repoUrl}\n`;
}
if (homepage) {
authorSection += `- 主页: ${homepage}\n`;
}
}
return authorSection;
}
generateExtra() {
// 定义可配置的字段模板
const extraFields = {
productOwner: {
label: '主要产品负责人',
key: 'productOwner'
},
techLead: {
label: '技术负责人',
key: 'techLead'
},
projectStartDate: {
label: '项目开始时间',
key: 'projectStartDate'
},
projectDescription: {
label: '项目详细描述',
key: 'projectDescription'
},
contactEmail: {
label: '联系邮箱',
key: 'contactEmail'
}
};
// 从 package.json 的 hs.readme 字段或 options 中读取值
const hsReadme = this.pkg.hs && this.pkg.hs.readme ? this.pkg.hs.readme : {};
const optionsReadme = this.options.readme || {};
const extraData = { ...hsReadme, ...optionsReadme };
// 收集有值的字段
const filledFields = [];
Object.keys(extraFields).forEach(fieldKey => {
const field = extraFields[fieldKey];
const value = extraData[field.key];
filledFields.push({
label: field.label,
value: value || ''
});
});
// 生成额外信息部分
let extraSection = '## 📋 项目信息\n\n';
filledFields.forEach(field => {
extraSection += `- **${field.label}**: ${field.value}\n`;
});
// 添加提示信息,告诉用户如何配置这些字段
extraSection += '\n> 💡 提示:你可以在 `package.json` 的 `hs.readme` 字段中配置这些信息,例如:\n';
extraSection += '> ```json\n';
extraSection += '> {\n';
extraSection += '> "hs": {\n';
extraSection += '> "readme": {\n';
extraSection += '> "productOwner": "产品负责人姓名",\n';
extraSection += '> "techLead": "技术负责人姓名",\n';
extraSection += '> "projectStartDate": "2024-01-01",\n';
extraSection += '> "projectDescription": "项目详细描述",\n';
extraSection += '> "contactEmail": "contact@example.com"\n';
extraSection += '> }\n';
extraSection += '> }\n';
extraSection += '> }\n';
extraSection += '> ```\n';
return extraSection;
}
};