怎样从零写一个前端脚手架 CLI?
先理解原理和示例,再练习完整口述与追问。
回答要点
在 package.json 中用 bin 声明命令,入口文件首行加 #!/usr/bin/env node;用 commander 等解析参数,用 prompts 等交互收集选项;从本地模板目录或远程仓库拉取模板,替换变量后写入目标目录;最后可选安装依赖、初始化 git,并输出下一步提示。需要处理目录已存在、Node 版本、包管理器检测和 Windows 兼容。
原理拆解
全局安装或通过 npm create xxx(等于运行 create-xxx 包)时,npm 根据 bin 字段在可执行目录生成命令链接,Windows 上会生成 .cmd 包装。模板存放方式有两种:内置在 CLI 包中,版本一致、离线可用;或从 Git 仓库下载,模板更新不需要发布 CLI。
文件名以点开头的文件(如 .gitignore)可能在发布 npm 包时被忽略,模板中常存为 _gitignore 再重命名。检测包管理器可读取 process.env.npm_config_user_agent。路径统一用 path.join 和 fileURLToPath(import.meta.url),不要手拼斜杠。
示例与推演
#!/usr/bin/env node
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const projectName = process.argv[2] || 'my-app'
const target = path.resolve(process.cwd(), projectName)
const templateDir = fileURLToPath(new URL('../template', import.meta.url))
if (fs.existsSync(target) && fs.readdirSync(target).length > 0) {
console.error(`目录 ${projectName} 已存在且不为空`)
process.exit(1)
}
fs.cpSync(templateDir, target, { recursive: true })
fs.renameSync(path.join(target, '_gitignore'), path.join(target, '.gitignore'))
const pkgPath = path.join(target, 'package.json')
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
pkg.name = path.basename(target)
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
console.log(`\n完成,执行:\n cd ${projectName}\n npm install\n npm run dev`)
易错点
覆盖用户已有目录前必须确认;不要在模板中写死某个包管理器的锁文件。脚手架之外,团队还可以把共享配置做成可升级的 npm 包,避免生成后再难同步。
参考回答可直接练习口述
我会先在 package.json 的 bin 里声明命令,入口加 Node 的 shebang;用 commander 解析参数,用 prompts 交互选择框架、TS 等选项;然后把内置或远程拉取的模板复制到目标目录,替换项目名等变量,处理 _gitignore 这类发布时会丢的点文件;最后安装依赖、初始化 git 并提示下一步。要注意目录已存在的确认、Node 版本检查、包管理器检测和 Windows 路径兼容。长期维护的配置最好抽成独立包,方便升级。