背景: 为什么需要自己开发一个脚手架,vue-cli、create-react-app,dva-cli,他们的特点不用多说拿就是专一!但是在实际公司的业务中你会发现以下一系列问题!
- 业务类型多
- 多次造轮子,项目升级等问题
- 公司代码规范,无法统一
很多时候我们开发时需要新建项目,把已有的项目代码复制一遍,保留基础能力。(但是这个过程非常琐碎而又耗时)。拿我们可以自己定制化模板,自己实现一个属于自己的脚手架。来解决这些问题。
一、项目初始化
1. 新建一个文件夹my-cli
npm init -y // 初始化项目
2. 新建bin/my文件
// 注意my不用写后缀,通过#!来指定由什么程序来解析
// mac系统
#! node
console.log('my-cli')
// windows系统
#! /user/bin/env node
console.log('my-cli')
3. 修改package.json
{
"name": "my",
"version": "1.0.0",
"description": "",
"main": "index.js",
"bin": "./bin/my",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
效果:执行my
4.起别名
// package.json中
"bin": {
"my": "./bin/my",
"my-cli": "./bin/my"
},
// 执行命令
npm link --force
二、使用command,自定义命令
2.1 command命令初体验
// 安装
npm i commander
// 配置基本参数
// 1. 配置可执行命令 commander
const program = require('commander')
program
.version(`my-cli@${require('../package.json').version}`)
.usage(`<command> [option]`)
// 解析用户执行命令传入的参数
program.parse(process.argv)
2.2 command命令设置项目名称,是否-force强行覆盖
// 2. 核心功能: 创建项目 ; 更改配置文件; UI界面
// 项目名称
program
.command('create <app-name>')
.description('create a new project')
.option('-f,--force', 'overrite target directory if it exists')
.action((name, cmd) => {
console.log(name, cmd)
})
2.3 command命令设置config
// 项目config -s set -g get -d delete
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)
})
2.5 on help时候的提示
chalk自定义提示框颜色
// 安装chlk
npm i chalk@3.0.0
// help提示
program.on('--help', function () {
console.log()
console.log(`Run ${chalk.cyan('my-cli <command> --help')} show detail`)
console.log()
})
三、开始写具体的实现方法
3.1 如何新建项目文件夹
新建lib/create.js 这个主要是创建项目目录的,这里需要两个第三方的包,fs-extra支持异步,inquirer写命令提示框的包
// 安装
npm i fs-extra
npm i inquirer@7.0.0
// create.js
// 创建目录前的提示,是否覆盖
const path = require('path')
const fs = require('fs-extra')
const Inquirer = require('inquirer')
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 === 'overwrite') {
return
} else {
await fs.remove(targetDir)
}
3.2 如何拉取创建项目的git模板
以下已vue模板为例,通过axios调用接口,获取模板,通过Inquirer给用户提供选项: 在lib/request.js下
const axios = require('axios')
axios.interceptors.response.use(res => res.data)
async function fetchRepositoryList() {
return axios.get('https://api.github.com/orgs/zhu-cli/repos')
}
module.exports = {
fetchRepositoryList,
}
在lib/Creator.js中
const { fetchRepositoryList } = require('./request')
const Inquirer = require('inquirer')
class Creator {
constructor(projectName, targetDir) {
this.name = projectName
this.target = targetDir
}
async fetchRepository() {
const repositoryList = await fetchRepositoryList()
if (!repositoryList) return
const repositoryNameList = repositoryList.map(item => item.name)
let { repository } = await Inquirer.prompt({
name: 'repository',
type: 'list',
choices: repositoryNameList,
message: 'please choose a template to create project',
})
console.log(`choice`, repository)
}
}
module.exports = Creator
下面就由两个vue的模板供用户选择了:
3.3 如何优化下载模板等待时间和重试机制
使用ora(5.x.x版本)开启命令行loading的效果,使用sleep和promise方式递归调用实现重试 lib/utils.js
const ora = require('ora')
async function sleep(time) {
return new Promise((resolve, reject) => setTimeout(resolve, time))
}
async function loading(fn, message) {
const spinner = ora(message)
spinner.start()
try {
const repositoryList = await fn()
spinner.succeed()
return repositoryList
} catch (error) {
spinner.fail('request failed, refetch...')
await sleep(1000)
return loading(fn, message)
}
}
module.exports = {
loading,
}
// Creator.js
const repositoryList = await loading(fetchRepositoryList, 1000)
成功的时候,有个loading和success的✅
写错请求模板的地址就会触发重试机制了。可以手动终止,也可以自己写个计数器,多少次后不重试了,这块就自己发挥了。
3.4 如何拉载对应模板的tag号
// request.js
async function fetchTagList(repo) {
console.log(`https://api.github.com/repos/zhu-cli/${repo}/tags`, 'tags....')
return axios.get(`https://api.github.com/repos/zhu-cli/${repo}/tags`)
}
async fetchTags(repo) {
const tagList = await loading(fetchTagList, 1000, repo)
if (!tagList) return
const tagNameList = tagList.map(item => item.name)
let { tag } = await Inquirer.prompt({
name: 'tag',
type: 'list',
choices: tagNameList,
message: 'please choose a tag to create project',
})
return tag
}
// Creator.js
async download(repo, tag) {
console.log(repo, tag)
let requestUrl = `zhu-cli/${repo}${tag ? '#' + tag : ''}`
await this.downloadGitRepo(requestUrl, process.cwd())
return this.target
}
async create() {
let repo = await this.fetchRepository()
let tag = await this.fetchTags(repo)
this.download(repo, tag)
}
// 找到模板, 找到tag号,使用download-git-repo的包去下载就好了
const downloadGitRepo = require('download-git-repo')