当我们想要开始一个新项目的时候,首先要搭建一个脚手架,再添加一些基础功能,最后在试着运行或构建项目。由于前端工程化日益完善,所以搭建新项目的一系列操作往往需要不少时间和精力。幸好社区中有不少优秀的 cli 工具和模板工程,比如 create-react-app、create-vite、Awesome Vite,我们可以使用它们来快速搭建新项目。
平时前端开发中,常见的项目类型有 web 站点、SDK、组件库、h5 项目等等。各个类型的项目模板肯定也是有所差异的,为了方便,我们可以写个 cli 工具来利用社区模板或 cli 来快速创建项目模板。
CLI 工具 npm 包简介
在编写 CLI 工具前,我们先介绍 CLI 工具中用到的工具库。
- commander: 定义命令行指令,在输入自定义的命令行的时候,会去执行相应的操作
- inquirer: 可以在命令行询问用户问题,并且可以记录用户回答选择的结果
- fs-extra: 是 fs 的一个扩展,提供了非常多的便利 API,同时添加了 promise 的支持
- chalk: 可以美化控制台的输出
- ora: 控制台的 loading 动画
- degit: 下载远程模板
- cross-spawn: 跨平台的命令调用模块
实现 CLI
注册和运行 CLI
首先创建文件夹和初始化项目
$ mkdir quick-template-cli
$ cd quick-template-cli
$ npm init -y
接下来创建个 bin 目录,在 bin 目录下创建 index.js
#! /usr/bin/env node
console.log('quick-template-cli')
#! /usr/bin/env node
代表这个文件用 node 执行。
然后打开 package.json,模块化方案使用 ESModule,加上配置 "type": "module"
,在增加 bin 字段注册命令。
// package.json
{
...,
"type": "module",
"bin": {
"quick-template": "./bin/index.js",
"qt": "./bin/index.js"
},
...
}
可以看到我们注册了两个命令,一个全称 quick-template
,一个缩写 qt
。
最后使用 npm link
把包链接到全局进行调试,在项目根目录下执行 npm link
,执行完成后命令行输入 qt
即可看到控制台输出 quick-template-cli
基础命令
一般 CLI 工具都会有帮助和查看版本的命令,我们使用 commander 来添加,同时使用 chalk 美化输出。先安装依赖 npm i commander chalk --save
接着我们引入 commander,实例化一个对象,调用对象的相关方法初始化 CLI 常见命令
import { Command } from 'commander'
import chalk from 'chalk'
const program = new Command()
program
.name('quick-template')
.description('Use community cli to create')
.version('0.0.1')
program.parse()
完成名称、描述和版本定义后,我们可以在命令行输入 qt --help
或 qt --version
,控制台会输出相应信息。
我们可以在使用 help 命令的时候补充一些信息,使用 chalk 美化关键输出。
program
.on('--help', () => {
console.log(`\r\nRun ${chalk.cyan(`qt <command> --help`)} for detailed usage of given command\r\n`)
})
create 命令
现在我们开始创建 create 命令,先简单定义命令,输出命令带的参数。
program.command('create <name>')
.description('Create a new project')
.action(async (name) => {
console.log(name)
})
此时可以执行 qt create demo-project
,控制台会输出 demo-project。
简单完成 create 命令定义后,我们梳理下 create 命令的逻辑:
- 询问用户要创建什么类型项目
- 根据用户选择做不同处理 2.1. 如果是普通网站项目,我们直接使用 create-vite 命令 2.2. 如果是 SDK,我们去下载 rollup 官方提供的一个模板工程 2.3. 如果是组件库,我们使用 dumi 来初始化项目
首先处理普通网站模板的创建,安装对应包 npm i inquirer cross-spawn --save
...
import inquirer from 'inquirer'
import spawn from 'cross-spawn'
...
program.command('create <name>')
.description('Create a new project')
.action(async (name) => {
const { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: 'What type of project it is',
choices: [
{ name: 'Project', value: 'Project' },
{ name: 'SDK', value: 'SDK' },
{ name: 'Components', value: 'Components' }
]
}
])
if (action === 'Project') {
spawn('npm', ['create', 'vite@latest', name], { stdio: 'inherit' })
return
}
})
代码逻辑比较简单,使用 inquirer.prompt
方法询问用户项目类型,如果是 Project,通过 spawn 调用 npm create vite@latest project-name
命令,后续流程就是 create-vite 的流程。
接着是 SDK 模板的下载。在创建网站项目模板的时候, create-vite 内部会处理文件夹已存在问题,所以无需我们处理。不过下载 SDK 模板或者组件库模板初始化前,我们就需要自己判断当前文件夹是否存在。
处理逻辑前先引入对应包 npm i degit fs-extra ora --save
。
...
import path from 'path'
import degit from 'degit'
import fs from 'fs-extra'
import ora from 'ora'
...
program.command('create <name>')
.description('Create a new project')
.action(async (name) => {
...
const targetDir = path.join(process.cwd(), name)
if (fs.existsSync(targetDir)) {
// 询问用户是否确定要覆盖
let { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: 'Target directory already exists Pick an action:',
choices: [
{ name: 'Overwrite', value: 'overwrite' },
{ name: 'Cancel', value: false }
]
}
])
if (!action) {
return
} else if (action === 'overwrite') {
await fs.remove(targetDir)
}
}
})
通过 process.cwd()
和 name
拼装出目标文件夹,如果文件夹已存在,询问用户是否要覆盖文件夹,确定要覆盖的话就清空文件夹,否则直接退出结束。
判断完文件夹是否存在后,我们使用 degit 下载项目
program.command('create <name>')
.description('Create a new project')
.action(async (name) => {
...
if (action === 'SDK') {
const spinner = ora('waiting download template')
spinner.start()
const emitter = degit('github:rollup/rollup-starter-lib')
await emitter.clone(targetDir)
.then(() => {
spinner.succeed('download template succeed.')
})
.catch(() => {
spinner.fail('Request failed...')
})
return
}
})
使用 ora 初始化一个 loading 对象,展示 loading ,然后指定下载的模板工程并发起下载到指定目录。下载成功或者下载失败都给与用户提示。
最后处理组件库的初始化,这边使用了 dumi 来初始化组件库。dumi 官方的初始化步骤:
# 先找个地方建个空目录。
$ mkdir myapp && cd myapp
# 通过官方工具创建项目,选择你需要的模板
$ npx create-dumi
我们需要把上面步骤在 CLI 中实现
program.command('create <name>')
.description('Create a new project')
.action(async (name) => {
...
if (action === 'Components') {
spawn.sync('mkdir', [targetDir], { stdio: 'inherit' })
spawn('npx', ['create-dumi'], { cwd: targetDir, stdio: 'inherit' })
return
}
})
来看下代码实现,首先使用 spawn 同步创建文件夹,然后执行 npx create-dumi
,注意需要在指定 targetDir 中执行。
至此我们就实现了所有功能。
总结
通过上述的案例,我们可以认识到 CLI 工具中使用到的一些常见的库,也可以看到一些优秀的社区 CLI 工具或模板工程。像 Awesome Vite 中各个模板,你可以使用 degit 直接下载,当然也可以像上述案例中的 CLI 工具一样,自己做个集成。
最后如果你有一些不错的模板合集,欢迎在评论区分享。
本文正在参加「金石计划」