🧰 一个轻量级前端脚手架工具的开发教程
在现代前端开发中,脚手架工具可以帮助我们快速搭建项目结构,节省大量重复性工作。本文将介绍一个基于 Node.js 开发的轻量级前端脚手架工具 temp-cli,它可以从远程 Git 仓库拉取模板项目并自动下载到本地目录。
📚 项目简介
- 支持通过交互式提示选择项目模板
- 自动从远程 Git 仓库克隆指定模板
- 检查目标路径是否已存在,避免覆盖
- 展示进度条和友好的用户提示
- 支持版本检测与升级
效果图
📦项目准备
本项目使用了 yargs、inquirer、simple-git、chalk、node-fetch、progress-estimator 常用库,并结合 Git 命令实现模板管理。
-
安装常用库
pnpm i chalk inquirer progress-estimator simple-git node-fetch yargs -
初始化package.json文件
pnpm init -
修改package.json内容,添加
bin字段{ "name": "temp-cli", "version": "1.9.1", "description": "简易前端脚手架,克隆远程仓库模板项目", "main": "index.js", "scripts": { "test": "echo "Error: no test specified" && exit 1" }, "keywords": ["vue3","vue2","uni-vue3","uni-vue2","Nuxt3"], "author": "GengJJJJJ", "license": "ISC", "packageManager": "pnpm@10.8.0", "bin": { "temp-cli": "./bin/index.js" //《---这里 }, "dependencies": { "chalk": "4", "inquirer": "8.2.5", "progress-estimator": "^0.3.1", "simple-git": "^3.27.0", "node-fetch": "^2.7.0", "yargs": "^17.7.2" } } -
新建相关文件(最终项目结构)
temp-cli/
├── bin/
│ ├── index.js // 入口文件,命令
│ ├── inquirer.js // 交互,克隆功能
│ ├── projectTem.js // 远程模板项目地址与介绍
│ └── update.js // 更新脚手架功能
└── package.json
🚀 脚手架功能介绍
1. 创建项目(create)
temp-cli create -n my-project
或简写:
temp-cli c -n my-project
执行该命令后,脚手架会:
- 提示用户输入项目名称(默认值可选)
- 验证项目命名规则(英文 + 小写字母开头)
- 提供多个框架/模板选项(如 Vue3、Vue2、Nuxt3、uniapp项目等)
- 检查目标文件夹是否存在
- 若不存在,则从远程 Git 仓库克隆模板到本地
- 删除
.git文件夹以防止冲突 - 输出启动项目的指令
2. 更新脚手架(update)
temp-cli update
或简写:
temp-cli u
执行该命令可更新 temp-cli 到最新版本。
3. 查看版本与帮助信息
temp-cli --version
temp-cli --help
🔧 实现教程
整个脚手架基于 Node.js 编写,主要依赖以下几个模块:
| 模块 | 功能描述 |
|---|---|
yargs | 解析命令行参数,构建 CLI 命令 |
inquirer | 提供交互式命令行界面(如输入、选择) |
simple-git | 执行 Git 操作(如 clone) |
fs | 文件系统操作(如检查文件夹是否存在) |
chalk | 控制台输出颜色美化 |
progress-estimator | 显示进度条,提升用户体验 |
核心流程图解
CLI 输入 (create/update)
↓
解析命令 → yargs
↓
检查版本更新(可选)
↓
inquirer 提示用户输入项目名称及模板
↓
检查目标路径是否存在
↓
如果不存在 → 使用 git clone 下载模板
↓
删除 .git 文件夹
↓
展示成功提示 & 启动命令
💡 核心代码实现
主程序入口:index.js
yargs的每个配置方法(如.version()、.alias()、.help()等)都返回的是yargs实例本身,所以可以连续调用这些方法。yargs.command()的用法介绍(添加一个命令,支持参数和处理函数)
yargs.command(
commandName, // 命令名或数组形式的别名,如 'create',也可以提供别名数组如 ['create', 'c']
description, // 命令描述,显示在帮助信息中
builder, // 用于配置该命令的参数选项
handler // 当用户输入该命令时要执行的函数
)
yargs.version()的用法介绍,(启用版本号支持,可通过-v或--version查看)
yargs.version(
versionString, // 版本号字符串(可选,如果省略则从 package.json 中读取)
[optionName], // 自定义触发版本号的标志,默认是 'version'
[description] // 描述信息(默认是 'Show version number')
)
yargs.version().alias('version', 'v') //给 --version 添加别名 -v,用户输入 --version 或 -v 都能触发版本号输出
yargs.version().help() //启用帮助信息功能,默认是 --help,用户输入会显示所有命令和参数说明
yargs.version().demandCommand(1, '请指定一个命令: create 或 update') //表示至少需要输入一个命令,如果用户不输入任何命令(比如直接运行 temp),就会提示 '请指定一个命令...'
yargs.version().argv //.argv 是一个 getter 属性,用于触发解析命令行参数并执行匹配的命令,会根据你定义的命令结构来决定执行哪个 handler
- 常用配置
| 方法 | 作用 |
|---|---|
.command() | 添加子命令 |
.option() | 添加参数选项 |
.version() | 设置版本号 |
.alias() | 添加参数别名 |
.help() | 启用帮助信息 |
.usage() | 设置帮助首行提示 |
.example() | 添加使用示例 |
.demandCommand() | 强制输入命令 |
.demandOption() | 强制输入参数 |
.boolean() / .string() | 设置参数类型 |
.describe() | 设置参数描述 |
.choices() | 设置参数可选值 |
.hide() | 隐藏参数 |
.group() | 分组显示参数 |
.strict() | 严格模式 |
.middleware() | 执行中间处理逻辑 |
.conflicts() | 设置参数冲突 |
.implies() | 设置参数依赖 |
#!/usr/bin/env node
const yargs = require('yargs');
const path = require('path');
const { inquirerPrompt, checkMkdirExists, clone } = require("./inquirer");
const { checkVersion, update } = require("./update")
// 创建项目命令
yargs.command(
['create', 'c'],
'新建一个项目',
function (yargs) {
return yargs.option('name', {
alias: 'n',
demand: true,
describe: '项目名称',
type: 'string'
})
},
async function (argv) {
const isNeedToUpdate = await checkVersion(); // 检测版本更新
if (!isNeedToUpdate) return;
inquirerPrompt(argv).then(async answers => {
const { name, frame } = answers
const isMkdirExists = checkMkdirExists( // 检测文件是否存在
path.resolve(process.cwd(), name)
);
if (isMkdirExists) {
console.log(`${name}/index.js 文件已经存在`)
} else {
await clone(frame, name);
}
})
}
).argv;
// 更新命令
yargs.command(
['update', 'u'],
'更新 temp-cli',
() => { },
async () => {
await update();
}
).argv;
// 版本号 & 帮助信息
yargs.version()
.alias('version', 'v')
.help()
.alias('help', 'h')
.demandCommand(1, '请指定一个命令: create 或 update')
.argv;
交互与克隆项目功能:inquirer.js
inquirer的用法(创建命令行界面(CLI)交互的 Node.js 库)
// 启动提示
inquirer.prompt([
/* 提示问题数组 */
]).then(answers => {
// answers用户的回答
});
/** 每个问题都是一个对象,包含属性有:
type: 输入类型(如 input, confirm, list, rawlist, expand, checkbox, password, editor)
name: 回答存储在返回答案对象中的键名
message: 显示给用户的问题或指令
default: 默认值
validate: 验证函数,返回 true 表示验证通过,否则返回错误消息
*/
simple-git的用法 ( Git 交互的轻量级封装库)
const simpleGit = require('simple-git');
const gitOptions = {
baseDir: process.cwd(), // 设置工作目录:当前工作目录
binary: "git", // 使用系统默认 Git
maxConcurrentProcesses: 6, // 同时最多执行 6 个 Git 命令
};
const git = simpleGit(gitOptions);
progress-estimator(显示进度条和预计剩余时间)
const fs = require('fs');
const chalk = require("chalk");
const inquirer = require('inquirer');
const simpleGit = require('simple-git');
const createLogger = require("progress-estimator");
const { projectTemplate } = require("./projectTem");
const logger = createLogger({
spinner: {
interval: 300,
frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"].map((item) =>
chalk.blue(item)
),
},
});
const gitOptions = {
baseDir: process.cwd(),
binary: "git",
maxConcurrentProcesses: 6,
};
function checkMkdirExists(path) {
return fs.existsSync(path)
};
function inquirerPrompt(argv) {
const { name } = argv;
return new Promise((resolve, reject) => {
inquirer.prompt([
{
type: 'input',
name: 'name',
message: '项目名称',
default: name,
validate: function (val) {
if (!/^[a-zA-Z]+$/.test(val)) {
return "项目名称只能含有英文";
}
if (!/^[a-z]/.test(val)) {
return "项目名称首字母必须小写"
}
return true;
},
},
{
type: 'list',
name: 'frame',
message: '使用什么框架开发',
choices: ['Vue3', 'Vue2', 'uni-vue2', 'uni-vue3', 'Nuxt3', '权限管理系统'],
},
]).then(answers => {
resolve({
...answers,
})
}).catch(error => {
reject(error)
})
})
}
async function clone(frame, projectName) {
const { downloadUrl, branch } = projectTemplate.get(frame);
const git = simpleGit(gitOptions);
try {
await logger(git.clone(downloadUrl, projectName, ["-b", `${branch}`]), "🚀 ~ 正在克隆远程项目: ", {
estimate: 8000,
})
if (fs.existsSync(`${projectName}/.git`)) {
fs.rm(`${projectName}/.git`, { recursive: true }, (err) => {
if (err) {
console.log(`${chalk.red("克隆远程仓库.git文件移除失败,请手动移除")}`);
}
});
}
console.log();
console.log(`🚀 ~ 项目创建成功 ${chalk.blueBright(projectName)}`);
console.log(`执行以下命令启动项目:`);
console.log(`cd ${chalk.blueBright(projectName)}`);
console.log(`${chalk.yellow("pnpm")} install`);
console.log(`${chalk.yellow("pnpm")} dev`);
} catch (err) {
console.log("下载失败");
console.log(String(err));
}
};
exports.clone = clone;
exports.checkMkdirExists = checkMkdirExists;
exports.inquirerPrompt = inquirerPrompt;
更新功能:update.js
const process = require('child_process');
const chalk = require('chalk');
const os = require('os');
const fetch = require('node-fetch');
const getNpmLatestVersion = async (npmName = 'temp-cli') => {
try {
const npmUrl = `https://registry.npmjs.org/${npmName}/latest`;
const res = await fetch(npmUrl);
const data = await res.json();
return data.version;
} catch (error) {
console.log('error', error);
}
};
const checkVersion = async () => {
const curVersion = require('../package.json').version;
const latestVersion = await getNpmLatestVersion();
const isSame = curVersion === latestVersion;
if (!isSame) {
console.log(
`~~~~~~ 检测到 temp-cli 最新版:${chalk.blueBright(latestVersion)} 当前版本:${chalk.blueBright(curVersion)} ~~~~~~`
);
console.log(
`请使用 ${chalk.yellow('npm install -g temp-cli@latest')} 更新,以使用最新模板项目 ~`
);
console.log(
`或者执行更新命令 ${chalk.yellow('temp update')} ~`
);
}
return isSame;
};
function checkIfInstalled() {
return new Promise((resolve) => {
process.exec('npm list -g temp-cli', (error, stdout, stderr) => {
if (!error && !stderr.includes('empty')) {
resolve(true); // 已安装
} else {
resolve(false); // 未安装
}
});
});
}
function command(type) {
const cmd = type === 'update' ? 'npm install temp-cli@latest -g' : 'npm uninstall temp-cli -g';
const desc = type === 'update' ? '更新成功' : '已卸载旧版本';
return new Promise((resolve, reject) => {
process.exec(cmd, (error, stdout, stderr) => {
const platform = os.platform();
if (error && stderr.includes('EACCES')) {
console.log(chalk.red('没有权限进行操作。请尝试使用以下命令重试:'));
if (platform === 'linux' || platform === 'darwin') {
console.log(chalk.yellow(`sudo ${cmd} // 对于 Linux/macOS 用户`));
} else if (platform === 'win32') {
console.log(chalk.yellow('管理员权限执行命令 // 对于 Windows 用户'));
}
return;
}
if (!error) {
console.log(chalk.green(`🚀 ~ ${desc}`));
resolve()
} else {
console.log(chalk.red('出错了', error));
reject()
}
});
});
}
async function update() {
console.log(chalk.blue('🚀 ~ temp-cli 正在更新...'));
const isInstalled = await checkIfInstalled();
if (isInstalled) {
await command('uninstall');
}
await command('update');
}
exports.update = update;
exports.checkVersion = checkVersion;
exports.getNpmLatestVersion = getNpmLatestVersion;
模板地址:projectTem.js
- 可以换成自己的模板项目
const projectTemplate = new Map([ [ "Vue3", { name: "vue3", downloadUrl: "https://gitee.com/gengJJJJJ/vue3_template.git", description: "Vue3+TS前端开发模板", branch: "master", }, ],
[ "Vue2", { name: "vue2", downloadUrl: "https://gitee.com/gengJJJJJ/vue2_template.git", description: "Vue2前端开发模板", branch: "master", }, ],
[ "uni-vue2", { name: "uni-vue2", downloadUrl: "https://gitee.com/gengJJJJJ/uni-vue2.git", description: "uni-vue2前端开发模板", branch: "master", }, ],
[ "uni-vue3", { name: "uni-vue3", downloadUrl: "https://gitee.com/gengJJJJJ/uni_vue3.git", description: "uni-vue3前端开发模板", branch: "master", }, ]
])
exports.projectTemplate = projectTemplate
发布npm
- 登录npm
npm login
- npm官网检查是否已有同名npm包
- 发布
npm publish
📌 总结
通过本文的介绍,你应该已经了解了如何开发一个简单但实用的前端脚手架工具。这个工具虽然体积小巧,但具备良好的扩展性和实用性,可以大大提升项目的初始化效率。
如果你觉得本文有帮到你,请点个💗
如果你喜欢这个模板项目,欢迎 Fork、Star 并贡献自己的模板!