前言
关注微信公众号:乘风破浪的前端 回复:cli获取代码
只要提到脚手架你就会想到vue-cli、create-react-app、dva-cli……他们的特点就是专一!但是在公司中你会发现有以下问题!
-
业务类型多
-
多次造轮子,项目升级等问题
-
公司代码规范无法统一
很多时候我们开发时需要新建项目,把已有的项目代码复制一遍,保留基础能力。(但是这个过程非常的琐碎而耗时)。那我们可以自己定制化模版,自己实现一个属于自己的脚手架,来解决这些问题。 在自己开发cli前,那肯定要先看一下那些优秀的cli是如何实现的!虽然不是第一个吃螃蟹的,那也要想想怎么吃更好^_^#
# 必备模块
commander: 可执行命令行工具
chalk: 一个第三方模块,可以画出更好看的ui
inquirer: 用户与命令行交互的工具
fs-extra: 文件管理的三方库,一般不直接使用原生的fs
path: 路径管理模块
ora: 实现命令行的loading效果
download-git-repo: 通过模版名称和版本号可以把项目下载下来,不支持promise
# 从零实现一个cli脚手架
配置初始环境
新建一个空文件夹conglin, npm init -y初始化package.josn文件\
既然是脚手架,假设脚手架的名称叫conglin,我希望别人在用的时候可以在命令行直接输入,conglin create xxxx 来安装脚手架。
创建可执行脚本文件
一般node会把可执行的文件放在bin文件夹里,在bin目录下新建文件conglin,输入#! /usr/bin/env node,表示我要用node来执行
我先写一个console,待会看看能不能在终端打印出来
配置package.json中的bin属性
npm link 链接到本地环境
但是这还不够,比如create-react-app为什么能执行,是因为它在全局的npm下载中,我们也需要这样。本地开发临时放到全局下,以后发布到线上就可以直接用了
测试一下,在终端直接输入conglin
歪瑞顾的,我们写在bin文件夹里的console被执行了
commander
基本上所有的命令行工具都是用commander来写的,安装commander
npm install commander
看看commander怎么去用
路径:bin/conglin
#! /usr/bin/env node
// 引入可执行命令 commander
const program = require('commander')
// 解析用户执行命令传入的参数
program.parse(process.argv)
// 举例:vue create xxx 中的create xxx就是要解析的参数
写下了这两行命令之后,我们就能执行conglin --help
因为没有任何配置,所以usage里面不太对,我们继续完善
#! /usr/bin/env node
// 配置可执行命令 commander
const program = require('commander')
program
.version(`conglin-cli@${require('../package.json').version}`)
.usage(`<command> [option]`)
// 解析用户执行命令传入的参数
program.parse(process.argv)
现在version和help就有内容了
接下来我们要通过commander完成三个核心内容\
创建项目
#! /usr/bin/env node
// 配置可执行命令 commander
const program = require('commander')
program
.command(`create <app-name>`)
.description(`create a new project`)
.option('-f --force', 'overwrite target directory if it exists') // 万一重名了,强制创建
.action((name, cmd) => {
console.log(name, cmd); //name 项目名称 cmd 输入的命令
})
program
.version(`conglin-cli@${require('../package.json').version}`)
.usage(`<command> [option]`)// 解析用户执行命令传入的参数program.parse(process.argv)
执行conglin create app --force\
配置命令
现在创建命令就写好了,但是我还希望再有一个配置命令,有添加、修改、删除
program
.command('config [value]')
.description('inspect and modify the config')
.option('-g, --get <path>', 'get value from option')
.option('-s, --set <path> <value>')
.option('-d, --delete <path>', 'delete option from config')
.action((value, cmd) => { console.log(value, cmd); })
ui界面
program
.command('ui')
.description('start and open conglin-cli ui')
.option('-p, --port <port>', 'port used for the UI Server')
.action((cmd) => { console.log(cmd); })
但是还缺了一个信息提示,比如我们在执行vue --help的时候,会有以下提示信息,告诉我们可以针对某一个命令去查询,比如vue create --help\
我希望我的命令也有,那么可以在用户打印--help的时候提示一下
program.on('--help', function () {
console.log('Run conglin-cli <command> -help show details');})
但是我们的提示信息还比较难看,可以美化一下
- 前后加上空的console.log
- 使用第三方工具chalk(粉笔)美化字体颜色
program.on('--help', function () {
console.log();
console.log(`Run ${chalk.blueBright('conglin-cli <command> -help')} show details`);
console.log();
})
看下效果
这样是不是顺眼多了
注意:chalk 插件版本 5以上不支持 es Module,可以安装版本4,
npm install chalk@4.1.0
小结:commander就是配置命令行工具,配置参数,然后解析用户的,参数没有其他的功能,都是相同的流程。
# 拉取模版
但是,所有的功能不可能都放在一个文件里去写,每调一个功能就调用一个模块去实现,比如调用create功能,就调用create模块\
新建一个文件夹lib-->新建文件create
路径:bin/conglin
program
.command(`create <app-name>`)
.description(`create a new project`)
.option('-f --force', 'overwrite target directory if it exists') // 万一重名了,强制创建模式
.action((name, cmd) => {
require('../lib/create.js')(name, cmd)
})
路径:lib/create.js
// 里面可能有很多异步的操作,所以包装成async函数
module.exports = async function (projectName, options) {
console.log(projectName);}
测试一下
相当于从我们的入口里面,分发到不同的源码目录里去
创建项目:create模块开发
1、如果创建项目的时候,发生了重名的情况
路径:lib/create.js
const path = require('path')
const fs = require('fs-extra')
const Inquirer = require('inquirer');
const Creator = require('./Creator.JS');
// 里面可能有很多异步的操作,所以包装成async函数
module.exports = async function (projectName, options) {
const cwd = process.cwd(); // 获取当前命令执行时的工作目录
const targetDir = path.join(cwd, projectName) // 目标目录
// 创建项目的时候,判断是否发生了重名的情况
if (fs.existsSync(targetDir)) {
if (options.force) { // 如果强制创建,删除已有的
await fs.remove(targetDir) }
else { // 提示用户是否确定覆盖
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 }
] }])
console.log(action);
if (!action) { return }
else if (action === 'overwrite') {
await fs.remove(targetDir) }
}
}
// 创建项目,单独封装了一个类
const creator = new Creator(projectName, targetDir)
creator.create() // 开始创建项目
}
2、创建项目
路径:lib/request.js
// 通过axios来获取结果
const axios = require('axios')
axios.interceptors.response.use(res => { return res.data})
async function fetchRepoList() { // 可以通过配置问价,拉取不同的仓库对应的用户下的文件
return axios.get('https://api.github.com/orgs/conglin-cli/repos')
}
module.exports = {
fetchRepoList
}
路径:lib/creator.js
主要关注create方法
const {fetchRepoList} = require('./request.js')
const Inquirer = require('inquirer')
const ora = require('ora')
async function sleep(n) {
return new Promise((resolve, reject) => {
setTimeout(resolve, n)
})}
// 只做了一个等待的loadingasync
function wrapLoading(fn, message) {
const spinner = ora(message)
spinner.start() // 开启加载
try {
let repos = await fn()
spinner.succeed() // 成功
return repos }
catch (error) {
// 失败了重新抓取
spinner.fail('request failed, refetch ...')
await sleep(1000)
return wrapLoading(fn, message)
}}
class Creator {
constructor(projectName, targetDir) {
// new 的时候回调用构造函数
this.name = projectName
this.target = targetDir
}
async fetchRepo() {
let repos = await wrapLoading(fetchRepoList, 'waiting fetch template')
if (!repos) return
repos = repos.map((item) => item.name)
const {repo} = await Inquirer.prompt({
name: 'repo',
type: 'list',
choices: repos,
message: 'please choose a template to create project' })
console.log(repo);
}
// 真实开始创建了
// 采用远程拉取的方式 github
async create() {
// 1) 先去啦去当前组织下的模版
let repo = await this.fetchRepo()
}}
module.exports = Creator
终端输入conglin create xxx
可以看到,已经成功把github上的模版拉取了下来\
# 下载资源
1、获取版本号
路径:lib/request.js
使用fetchTagList方法获取版本号
// 通过axios来获取结果
const axios = require('axios')
axios.interceptors.response.use(res => { return res.data})
async function fetchRepoList() {
// 可以通过配置问价,拉取不同的仓库对应的用户下的文件
return axios.get('https://api.github.com/orgs/conglin-cli/repos')}
async function fetchTagList(repo) {
return axios.get(`https://api.github.com/repos/conglin-cli/${repo}/tags`)}
module.exports = {
fetchRepoList,
fetchTagList
}
路径:lib/creator.js
在create方法里添加fetchTag方法,用来获取项目的所有版本
const { fetchRepoList, fetchTagList} = require('./request.js')
const Inquirer = require('inquirer')
const ora = require('ora')
async function sleep(n) {
return new Promise((resolve, reject) => { setTimeout(resolve, n)
})}
// 只做了一个等待的loadingasync
function wrapLoading(fn, message, ...args) {
const spinner = ora(message)
spinner.start() // 开启加载
try {
let repos = await fn(...args)
spinner.succeed() // 成功
return repos }
catch (error) {
// 失败了重新抓取
spinner.fail('request failed, refetch ...')
await sleep(1000)
return wrapLoading(fn, message, ...args) }}
class Creator {
constructor(projectName, targetDir) {
// new 的时候回调用构造函数
this.name = projectName
this.target = targetDir }
async fetchRepo() {
let repos = await wrapLoading(fetchRepoList, 'waiting fetch template')
if (!repos) return
repos = repos.map((item) => item.name)
const {repo } = await Inquirer.prompt({
name: 'repo',
type: 'list',
choices: repos,
message: 'please choose a template to create project' })
console.log(repo);
return repo
}
async fetchTag(repo) {
let tags = await wrapLoading(fetchTagList, 'waitting fetch tag', repo) console.log('tags', tags);
if (!tags) return
tags = tags.map(item => item.name)
const {tag} = await Inquirer.prompt({
name: 'tag',
type: 'list',
choices: tags,
message: 'please choose a tag to create project' })
console.log(tag);
}
async download() {
}
// 真实开始创建了
// 采用远程拉取的方式 github
async create() {
// 1) 先去啦去当前组织下的模版
let repo = await this.fetchRepo()
// 2)再通过模版找到版本号
let tag = await this.fetchTag(repo)
//3) 下载
// let downloadUrl = await this.download(repo, tag)
// 4)编译模版
}}
module.exports = Creator
执行 conglin create xxx
选择vue-template
可以看到,vue-template下的版本号已经被我们获取了。
2、下载模版
npm install download-git-repo
// 通过模版名称和版本号可以把项目下载下来
现在有了版本号和模版名称,我们就可以去下载项目啦
路径:lib/creator.js
虽然代码很长,但是只需要关注download方法
const { fetchRepoList, fetchTagList} = require('./request.js')
const Inquirer = require('inquirer')
const ora = require('ora')
const downloadGitRepo = require('download-git-repo') // 不支持promise
const util = require('util') // node自带的方法
const path = require('path')
async function sleep(n) {
return new Promise((resolve, reject) => {
setTimeout(resolve, n)
})}
// 只做了一个等待的loading
async function wrapLoading(fn, message, ...args) {
const spinner = ora(message)
spinner.start() // 开启加载
try { let repos = await fn(...args)
spinner.succeed() // 成功
return repos }
catch (error) {
// 失败了重新抓取
spinner.fail('request failed, refetch ...')
await sleep(1000)
return wrapLoading(fn, message, ...args)
}}
class Creator {
constructor(projectName, targetDir) {
// new 的时候回调用构造函数
this.name = projectName
this.target = targetDir
this.downloadGitRepo = util.promisify(downloadGitRepo) // 把downloadGitRepo转成promise方法 }
async fetchRepo() {
let repos = await wrapLoading(fetchRepoList, 'waiting fetch template')
if (!repos) return
repos = repos.map((item) => item.name)
const { repo} = await Inquirer.prompt({
name: 'repo',
type: 'list',
choices: repos,
message: 'please choose a template to create project' })
console.log(repo);
return repo }
async fetchTag(repo) {
let tags = await wrapLoading(fetchTagList, 'waitting fetch tag', repo)
if (!tags) return
tags = tags.map(item => item.name)
const { tag } = await Inquirer.prompt({
name: 'tag',
type: 'list',
choices: tags,
message: 'please choose a tag to create project' })
return tag
}
async download(repo, tag) {
// 1、需要拼接出下载路径
let requestUrl = `conglin-cli/${repo}${tag?'#'+tag:''}`
// 2、把资源下载到某个路径上(后续可以增加缓存功能, 应该下载到系统目录中,稍后可以再使用ejs handlebar 去渲染模版,最后生成结果再写入)
await this.downloadGitRepo(requestUrl, path.resolve(process.cwd(), `${repo}@${tag}`))
return this.target }
// 真实开始创建了
// 采用远程拉取的方式 github
async create() {
// 1) 先去啦去当前组织下的模版
let repo = await this.fetchRepo()
// 2)再通过模版找到版本号
let tag = await this.fetchTag(repo)
//3) 下载
await this.download(repo, tag)
// 4)编译模版 }}
module.exports = Creator
这样项目就被我们下载下来了,当然,这只是比较基础的功能实现,后期可以根据需要做很多的优化
总结
这是一个从空文件夹开始的cli练习项目,感兴趣的小伙伴可以从头开始跟着写,其实cli工具并不难,就是通过各种插件搭积木就可以了,所以也不用感觉到造轮子的有多高大上。
我们这个cli项目的流程为:
- 使用commander开发可执行命令行
- 下载当前组织下的模版
- 获取当前模版下的版本号
- 下载资源
\
完整目录\
关注微信公众号:乘风破浪的前端 回复:cli就可以获取代码