在现代前端开发中,脚手架工具(Scaffolding Tools)扮演着至关重要的角色。它们能够自动化地生成项目初始结构、配置文件和样板代码,从而极大地提升了开发效率。熟知的 create-vite、create-react-app 和 @vue/cli 等都是优秀的开源脚手架。
然而,通用脚手架创建的往往是标准化的项目模板,无法完全满足企业或团队特定的业务需求和技术栈封装。为了解决这一问题,开发一个符合自身需求的、定制化的脚手架变得尤为重要
本篇文章将深入探讨如何从零到一构建一个 Node.js CLI 脚手架,详细解析其实现原理,并介绍开发过程中涉及的核心 NPM 依赖包及其应用场景
既然社区已经有如此多成熟的脚手架,为何还需要投入精力自建呢?
React + TypeScript + Ant Design + MobX),自建脚手架可以深度整合这些技术,并预设好所有配置。一个完整的 CLI 脚手架其核心工作流程可以概括为以下几个步骤:
npm install 并进行一些清理工作。选择将项目模板作为独立的 NPM 包发布。这种方式带来了诸多好处:
https://registry.npmjs.org/[package-name])动态获取包的元信息,包括最新版本号(dist-tags.latest)、历史版本等。在实现脚手架的过程中,会依赖一系列优秀的 NPM 包来简化开发。
fs-extra:增强版 fs 模块fs-extra 是对 Node.js 原生 fs 模块的扩展,提供了更多便捷的文件系统操作方法,并弥补了原生 fs 的一些不足。
copySync, removeSync, ensureDirSync, readJsonSync, writeJsonSync 等。fs 中递归复制目录需要编写复杂的逻辑,而 fs-extra 只需一行代码。const fse = require("fs-extra")
// 简单一行代码即可递归复制目录
fse.copySync("./template-source", "./my-new-project/")glob:文件路径模式匹配glob 允许你使用类似 Shell 的通配符模式来匹配文件路径,非常适合用于查找特定类型的文件或排除某些目录。
**/*.js)查找文件。.js 文件,同时忽略 node_modules 目录。const { glob } = require("glob")
async function findJsFiles() {
const files = await glob("**/*.js", {
cwd: process.cwd(), // 当前工作目录
ignore: "node_modules/**", // 忽略 node_modules
nodir: true // 只匹配文件,不匹配目录
})
console.log(files)
}
findJsFiles()@inquirer/prompts:交互式命令行界面@inquirer/prompts 是一个功能强大且易于使用的库,用于创建丰富的交互式命令行界面,让你的 CLI 工具更加人性化。
input, select, password, confirm 等多种交互模式。import { input, select } from "@inquirer/prompts"
async function getUserInput() {
const projectName = await input({ message: "请输入项目名称:" })
const template = await select({
message: "请选择一个项目模板:",
choices: [
{ name: "React Template", value: "template-react" },
{ name: "Vue Template", value: "template-vue" }
]
})
console.log({ projectName, template })
}
getUserInput()semver:语义化版本控制工具semver 提供了完整的语义化版本(Semantic Versioning)解析和比较功能,是处理 NPM 包版本号的必备工具。
valid(), gt(), lt(), satisfies() 等。const semver = require("semver")
// 检查版本号是否有效
console.log(semver.valid("1.2.3")) // '1.2.3'
console.log(semver.valid("a.b.c")) // null
// 比较版本号
console.log(semver.gt("2.0.0", "1.5.0")) // true
// 检查是否满足版本范围
if (semver.satisfies(process.version, ">=14.0.0")) {
console.log("Node.js 版本符合要求。")
}npminstall:程序化 NPM 包安装npminstall 是一个可以让你通过 Node.js 代码来安装 NPM 包的库,它通过本地缓存和链接(软链/硬链)机制来加速安装,高效且支持软链接。
const npminstall = require("npminstall")
const path = require("path")
async function downloadTemplate(templateName, version) {
const targetDir = path.join(process.cwd(), ".tmp")
await npminstall({
root: targetDir,
pkgs: [{ name: templateName, version: version || "latest" }],
registry: "https://registry.npmjs.org"
})
console.log(`${templateName} 下载完成!`)
}
downloadTemplate("template-react", "1.0.0")ora 和 cli-spinner:命令行加载动画在执行网络请求、文件读写等耗时操作时,提供一个加载动画(Spinner)可以极大地改善用户体验。ora 是目前社区中最流行和功能最丰富的选择。
注意:
ora从 v6 起是纯 ESM 包,ESM 项目用import ora from "ora"引入;CommonJS 项目要么降级到 v5,要么在 Node.js 22.12+ 上直接require("ora")。
const ora = require("ora")
async function longRunningTask() {
const spinner = ora("正在下载模板...").start()
setTimeout(() => {
spinner.color = "yellow"
spinner.text = "下载速度有点慢,请稍候..."
}, 3000)
setTimeout(() => {
spinner.succeed("模板下载成功!")
}, 6000)
}
longRunningTask()下面是一个简化的脚手架创建流程,帮助你快速开始。
mkdir my-cli-scaffold
cd my-cli-scaffold
npm init -ynpm install fs-extra glob @inquirer/prompts semver npminstall ora创建一个 index.js 文件,并添加以下代码作为起点:
#!/usr/bin/env node
const { input, select } = require("@inquirer/prompts")
const ora = require("ora")
const fse = require("fs-extra")
const path = require("path")
// ... 其他依赖
async function main() {
console.log("欢迎使用 My CLI Scaffold!")
const projectName = await input({ message: "请输入项目名称:" })
const template = await select({
message: "请选择模板:",
choices: [
{ name: "React (JavaScript)", value: "template-react" },
{ name: "Vue (TypeScript)", value: "template-vue-ts" }
]
})
const spinner = ora("正在创建项目...").start()
// 模拟下载和复制
setTimeout(() => {
const destDir = path.join(process.cwd(), projectName)
fse.ensureDirSync(destDir)
// 此处应为下载模板并复制的逻辑
spinner.succeed(`项目 ${projectName} 创建成功!`)
}, 2000)
}
main().catch(console.error)package.json在 package.json 中添加 bin 字段,使其成为一个可执行命令:
{
"name": "my-cli-scaffold",
"version": "1.0.0",
"bin": {
"my-cli": "index.js"
},
"dependencies": {
...
}
}使用 npm link 将你的脚手架链接到全局,方便本地测试:
npm link
my-cliQ1: 如何处理模板中的动态内容,比如 package.json 里的项目名称?
A: 你可以使用模板引擎(如 ejs 或 handlebars)。在复制文件后,读取需要动态替换内容的文件,使用模板引擎渲染,并将用户输入的数据(如项目名称)注入进去,最后再写回文件。
Q2: 脚手架如何进行版本更新提示?
A: 在脚手架启动时,可以异步请求 NPM Registry API 获取脚手架自身的最新版本号,然后使用 semver.gt() 与当前版本号比较。如果发现新版本,可以打印提示信息告知用户更新。
Q3: 为什么推荐使用 npminstall 而不是直接执行 npm install 命令?
A: 使用 npminstall 提供了更强的程序化控制能力,例如可以精确指定安装目录、控制缓存、获取安装过程的详细日志等,这对于在脚手架内部管理模板下载非常有用。
try...catch 错误处理逻辑。ora 显示加载状态,在操作成功或失败时打印明确的消息。自研脚手架是提升团队开发效率、保障项目规范性的重要基建投入。通过本文的介绍,了解了脚手架的核心实现原理,并掌握了 fs-extra、@inquirer/prompts、glob、semver、npminstall 和 ora 等关键依赖包的使用方法。
虽然从零构建一个功能完备的脚手架需要投入不少精力,但其带来的长期收益是显著的。希望本文能为你开启自己的脚手架开发之旅提供有力的支持。