Cli 脚手架
定制脚手架
一般公司往往都会根据公司主要技术栈,预先搭建好自己的一套项目模板,存放在公司的代码仓库中,例如 web-template 、 h5-template 、 weapp-template 等,在开发新项目的时候,只需要下载项目模板即可快速进行开发。
但每次都得打开公司代码仓库,找到相应的模板进行下载,这时就可以开发自己的一套命令行指令,像 create-vite-app 一样能够自己选择对应模板后,自动下载项目模板下来,提升项目开发前的一点点体验。
my-cli 功能
暂且叫这款简易的脚手架名为 my-cli ,先确定以下我们希望这款简易的脚手架能提供什么功能
npm i my-cli方式下载安装my-cli create [projectName]快速创建(下载)模板
应该具备哪些能力
- 解析命令行参数能力,拿到命令行输入的项目名、下载路径等
- 提供命令行交互功能,选择指定模板
- 根据选择模板的
git地址下载项目模板到指定路径 - 判断路径是否存在项目,提供是否覆盖询问功能
常见 node 包
以下是开发 my-cli 过程中可能会使用到的 node 包,目前大概看个预览功能,可以在使用到的时候再去查询相应的详细功能
minimist- 命令行参数解析chokidar-fs.watch和fs.watchFile替代品,解决两者存在的文件监听不正确的问题child_process-nodejs包开启子线程pathe- 处理路径,与path类似inquirer- 命令行交互prompts- 也是命令行交互git-clone-git clone操作,用于下载仓库和执行git命令,支持promisecli-spinner- 命令行loading...fs-extra-nodejs中的fs替代,用于操作文件semver- 版本管理clear- 清空命令行输出consola- 命令行输出colorette- 命令行输出着色,配合consola使用
功能
逐一进行功能开发,可能会遇到的功能点
- 选择模板
- 输入项目名称
- 选择是否是小程序,小程序需要提供
appid git模板下载- 文件路径判断
- 文件读取、编辑、写入
- 命令行下载提示、着色
准备
先准备基础环境,安装 node ,初始化项目
npm init安装 typescript 和 ts-node ,ts-node 用于编译并执行 ts 文件
pnpm i typescript ts-node -D新建 script 命令,使用 ts-node 来编译执行 ts 文件
{
"scripts": {
"dev": "ts-node ./src/index.ts"
}
}新建 src/index.ts ,并写入一些代码,运行 dev 命令即可看到 ts-note 正常使用,以下出现 运行 dev 命令 即执行以下命令
npm run devinquirer 命令行交互
首先从命令行交互开始,命令行交互需要借助 inquirer 包,这里使用 inquirer@8 (inquirer@9 版本会报错不支持 require 引入使用)
下载
pnpm i inquirer@8
pnpm i @types/inquirer -D获取选择的模板
// src/index.ts
import inquirer from 'inquirer'
const { prompt } = inquirer
const run = async () => {
const { template } = await prompt({
name: 'template',
message: '请选择模板',
type: 'list',
default: 'web',
choices: ['web', 'h5', 'weapp']
})
console.log(template)
}
run()运行 dev 命令,即可看到命令行中出现 提示
? 请选择模板 (Use arrow keys)
> web
h5
weapp选择 web 回车后打印的结果是 'web' ,至此简单介绍下传入 prompt 的参数是什么意思
name: 返会结果所存放的字段message: 提示文案type: 交互类型,这里list指的是 列表选择,此外还有checkbox、confirm、input、password等类型default: 默认选中choices: 可选项,也可是{ value: string, name: string }[]类型
获取项目名
借助 inquirer.prompt 我们能做到提供命令行交互功能,并且能顺利拿到了选择的结果,以此类推,也可以拿到输入的 项目名称
import consola from consola
// code...
const { projectName } = await prompt({
name: 'projectName',
message: '请输入项目名称',
type: 'input',
default: `${template}-template`,
validate: (val) => {
const reg = /^[\w-]+$/
if (!reg.test(val)) {
consola.error('请输入正确的项目名称')
return false
}
return true
}
})
consola.log(green(projectName))项目名称做了校验,不符合的名称将会输出 error 并且等待重新输入,注意这里改用 consola 输出,需要安装一下包,并引入使用,它与 console 功能类似,但可以美化命令行输出,此外可以搭配 colorette 使用
pnpm i consola colorette模板下载
前面拿到了 模板 和 项目名称 ,接下来进行下载,此时会用到 文件、路径处理 和 下载 等,需要先安装相应的依赖包
pnpm i pathe cli-spinner fs-extra git-clone路径处理
下载前进行文件夹、路径处理的函数
import { pathExistsSync, readdirSync, removeSync, existsSync } from 'fs-extra'
export const isEmpty = (path: string) => {
return !(pathExistsSync(path) && readdirSync(path).length !== 0)
}
export const emptyDir = (path: string) => {
if (!existsSync(path)) return
removeSync(path)
}覆盖询问
默认覆盖,否则退出
import { join } from 'pathe'
// code...
const targetDir = join(process.cwd(), projectName)
if (!isEmpty(targetDir)) {
const { overwrite } = await prompt({
name: 'overwrite',
default: true,
type: 'confirm',
message: () => (targetDir === '.' ? 'Current directory' : `Target directory "${targetDir}"`) + ' is not empty, Remove existing files and continue?',
})
if (!overwrite) {
process.exit(1)
} else {
emptyDir(targetDir)
}
}下载模板
准备项目模板的 git 地址
const remotes = [
{ git: 'exmaple.git', for: 'web' },
{ git: 'exmaple.git', for: 'h5' },
{ git: 'exmaple.git', for: 'weapp' },
]开始下载模板
- 使用
spinner提示下载中 - 使用
git-clone/promise下载远程仓库模板, readJson和writeJSON则用于编辑package.json文件- 需要另外注意的是,如果选择小程序模板有传入
appid的话,需要另外编辑project.config.json文件写入appid
import { Spinner } from 'cli-spinner'
import gitClone from 'git-clone/promise'
import { existsSync, readJson, remove, writeJSON } from 'fs-extra'
import { greenBright } from 'colorette'
// code...
const remote = remotes.find(v => v.for === template).git
const spinner = new Spinner('正在下载中... %s')
spinner.setSpinnerString('|/-\\')
spinner.start()
await gitClone(remote, projectName)
await remove(join(targetDir, '.git'))
const packageJson = await readJson(join(targetDir, 'package.json'))
packageJson.name = projectName
if (template === 'weapp' && appid !== '') {
if (existsSync(join(targetDir, 'project.config.json'))) {
const miniProjectConfig = await readJson(join(targetDir, 'project.config.json'));
miniProjectConfig.appid = appid;
await writeJSON(join(targetDir, 'project.config.json'), miniProjectConfig, { spaces: 2 });
}
}
await writeJSON(join(targetDir, 'package.json'), packageJson, { spaces: 2 })
spinner.stop(true)
consola.log(greenBright('模板下载完成'))至此完成一个简易的模板下载脚手架
优化打包
前面使用的是 ts-node 做编译执行调试,不是很友好,可以使用 unbuild 来优化打包体验,开发环境下采用即时编译 jiti 实时获取最新代码,省去监听文件变动重新编译过程。
安装依赖
pnpm i -D unbuildscripts
{
"build": "unbuild",
"dev": "unbuild --stub",
}Sources
unbuild 更多可以参考 unbuild-demo