🧰 彦祖,亦菲!难道你不想自己做一个轻量级前端脚手架工具吗?

234 阅读6分钟

🧰 一个轻量级前端脚手架工具的开发教程

在现代前端开发中,脚手架工具可以帮助我们快速搭建项目结构,节省大量重复性工作。本文将介绍一个基于 Node.js 开发的轻量级前端脚手架工具 temp-cli,它可以从远程 Git 仓库拉取模板项目并自动下载到本地目录。

📚 项目简介

  • 支持通过交互式提示选择项目模板
  • 自动从远程 Git 仓库克隆指定模板
  • 检查目标路径是否已存在,避免覆盖
  • 展示进度条和友好的用户提示
  • 支持版本检测与升级

效果图

image.png

image.png

image.png

image.png

image.png

📦项目准备

本项目使用了 yargsinquirersimple-gitchalknode-fetchprogress-estimator 常用库,并结合 Git 命令实现模板管理。

  1. 安装常用库

    pnpm i chalk inquirer progress-estimator simple-git node-fetch yargs
    
  2. 初始化package.json文件

    pnpm init
    
  3. 修改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"
      }
    }
    
  4. 新建相关文件(最终项目结构)

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 nodeconst 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 并贡献自己的模板!