需求描述
每次开新项目时都需要从头搭建架构,或者就是把之前的项目直接复制粘贴过来修修改改。
当然有时稍微勤奋一点的就会弄个基础模板放在本地或者放在 github,需要的时候直接 clone 过来。
但是其便捷性和通用性极差,就开始想为什么不做一个类似 vue-cli 的命令行工具呢(刚好现在有时间~)
脚手架基本功能
1、通过命令行交互式的询问用户问题
2、根据用户的答复选择不同的模版或者生成不同的文件
脚手架构建用到的基本工具
commander 可以自定义一些命令行指令,在输入自定义的命令行的时候,会去执行相应的操作
npm install commanderinquirer 可以在命令行询问用户问题,并且可以记录用户回答选择的结果
npm install inquirerfs-extra 是fs的一个扩展,提供了非常多的便利API,并且继承了fs所有方法和为fs方法添加了promise的支持。
npm install fs-extrachalk 可以美化终端的输出
npm install chalk@4.1.0figlet 可以在终端输出logo
npm install figletora 控制台的loading样式
npm install oradownload-git-repo 下载远程模板
npm install download-git-repo
构建过程
1、首先创建一个文件夹,初始化 package.json 文件
mkdir zyq_fronted_clicd zyq_fronted_clinpm init
2、创建文件夹 bin ,用于放置程序的入口文件
zyq_fronted_cli
├─ bin
└─ package.json
3、创建文件夹 lib ,用来放一些工具函数
zyq_fronted_cli
├─ bin
├─ lib
└─ package.json
4、在 bin 文件夹中创建 cli.js 文件
zyq_fronted_cli
├─ bin
│ ├─ cli.js
├─ lib
└─ package.json
5、在 package.json 文件中指定程序的入口文件为 bin 文件夹下的 cli.js 文件
package.json 文件{"name": "zyq_fronted_cli","version": "1.0.0","description": "","main": "index.js","bin": {"zyq_fronted": "./bin/cli.js"},"scripts": {"test": "echo \"Error: no test specified\" && exit 1"},"author": "","license": "ISC","dependencies": {"chalk": "^4.1.0","commander": "^10.0.1","figlet": "^1.6.0","fs-extra": "^11.1.1"}
}
6、下载安装 commander 自定义命令行指令包
npm install commander
7、在 bin 文件夹下的 cli.js 中引入 commander
注意:文件开头如果带有 #!的话,那么这个文件就会被当做一个执行文件来执行,执行文件也即是可以由操作系统进行加载执行的文件。如果带有这个符号,说明这个文件可以当做脚本来运行。
/usr/bin/env node 的意思是这个文件用 node 来执行,(会去用户的安装根目录下的 env 环境变量里面去寻找 node(/usr/bin/env node)然后用node 来执行整个的脚本文件)
#! /usr/bin/env nodeconst commander = require('commander')commander
.version('0.1.0')
.command('create <project name>')
.discription('create a new project')
.action(res => {console.log(res)
})commander.parse()
8、将当前项目(zyq_fronted_cli) 链接到全局
npm link
9、安装 chalk 和 figlet,在 bin 文件夹的 cli.js 中引入 ,用于自定义字体和颜色
npm install chalk@4.1.0
npm install figlet
#! /usr/bin/env nodeconst commander = require('commander') // 自定义指令// 自定义指令
commander.version('0.1.0').command('create <project_name>').description('create a new project').action(res => {console.log(res)})const chalk = require('chalk') // chalk 改变颜色
const figlet = require('figlet') // figlet 改变字体commander.on('--help', () => { // 监听 --help 执行console.log('\r\n' + figlet.textSync('ZYQ_FRONTED_CLI', {font: 'Ghost',horizontalLayout: 'default',verticalLayout: 'default',width: 500,whitespaceBreak: true}))// 新增说明信息console.log(`\r\nRun ${chalk.cyan(`zyq_fronted <command> --help`)} for detailed usage of given command\r\n`)})commander.parse()
10、在 lib 文件夹中创建 create.js 文件,用于编写创建文件所需的逻辑
zyq_fronted_cli
├─ bin
│ ├─ cli.js
├─ lib
│ ├─ create.js
└─ package.json
create.js 文件
module.exports = async function (name, option){console.log('项目名称以及配置项:', name, option)
}
11、 创建项目时,询问用户是否需要强制覆盖已有的文件(在 cli.js 文件中加入 --force 选项来实现该需求,修改 create.js 文件的逻辑)
创建项目时(zyq_fronted create myproject --force 或者 zyq_fronted create myproject -f),询问用户是否需要强制覆盖已有的文件
cli.js 文件#! /usr/bin/env nodeconst commander = require('commander') // 自定义指令
const create = require('../lib/create.js')commander.version('0.1.0').command('create <project_name>').description('create a new project').option('-f --force', 'overwrite target directory if it exist').action((name, option) => {console.log(name, option)create(name, option)})const chalk = require('chalk') // chalk 改变颜色const figlet = require('figlet') // figlet 改变字体commander.on('--help', () => { // 监听 --help 执行console.log('\r\n' + figlet.textSync('ZYQ_FRONTED_CLI', {font: 'Ghost',horizontalLayout: 'default',verticalLayout: 'default',width: 500,whitespaceBreak: true}))// 新增说明信息console.log(`\r\nRun ${chalk.cyan(`zyq_fronted <command> --help`)} for detailed usage of given command\r\n`)})commander.parse()
create.js 文件module.exports = async function (name, option){const path = require('path')const fs = require('fs-extra') // npm install fs-extraconst cwd = process.cwd() // 当前命令行选择的目录const targeCwd = path.join(cwd, name) // 需要创建的目录地址// 判断是否存在该目录if (fs.existsSync(targetCwd)) { // 目录存在if (options.force) { // 是否强制创建console.log('进行强制创建')} else {console.log('询问用户是否强制创建')}} else { // 目录不存在console.log('目录不存在,进行强制创建')}console.log('项目名称以及配置项:', name, options)
}
12、安装 inquirer ,使用 inquirer 获取终端与用户的交互信息
inquirer 可以在命令行询问用户问题,也可以记住用户在命令行的选择
npm install --save inquirer@^8.0.0
13、修改 create.js 文件的逻辑(当非强制性创建项目的时候,项目存在的话就询问用户要不要覆盖项目;当非强制性创建项目的时候,项目不存在的话直接创建项目;当强制性创建项目的时候,项目存在或不存在都强制创建项目;)
create.js 文件module.exports = async function (name, options){const path = require('path')const fs = require('fs-extra')const inquirer = require('inquirer')const cwd = process.cwd() // 当前命令行选择的目录const targetCwd = path.join(cwd, name) // 需要创建的目录地址console.log(cwd,targetCwd)// 判断是否存在该目录if (fs.existsSync(targetCwd)) { // 目录存在if (options.force) { // 是否强制创建console.log('进行强制创建')// 移除原来存在的项目await fs.remove(targetCwd)} else {console.log('询问用户是否强制创建')// 询问用户是否强制创建项目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 {console.log('移除存在的文件')await fs.remove(targetCwd)}}} else { // 目录不存在console.log('目录不存在,进行强制创建')}console.log('项目名称以及配置项:', name, options)
}
14、在 lib 文件夹下创建 factory.js 文件,用于负责 创建目录、拉取模版等逻辑
zyq_fronted_cli
├─ bin
│ ├─ cli.js
├─ lib
│ ├─ create.js
│ ├─ factory.js
└─ package.json
15、编辑 factory.js 文件内容,并在 create.js 文件中引入
factory.js 文件module.exports = class Factory{constructor(name, targetCwd){this.name = name // 目录名称this.targetCwd = targetCwd // 目录所在地址console.log(this.name, this.targetCwd)}// 创建create() {}
}
create.js 文件module.exports = async function (name, options){const path = require('path')const fs = require('fs-extra')const inquirer = require('inquirer')const cwd = process.cwd() // 当前命令行选择的目录const targetCwd = path.join(cwd, name) // 需要创建的目录地址console.log(cwd,targetCwd)// 判断是否存在该目录if (fs.existsSync(targetCwd)) { // 目录存在if (options.force) { // 是否强制创建console.log('进行强制创建')// 移除原来存在的项目await fs.remove(targetCwd)} else {console.log('询问用户是否强制创建')// 询问用户是否强制创建项目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 {console.log('移除存在的文件')await fs.remove(targetCwd)}}} else { // 目录不存在console.log('目录不存在,进行强制创建')}// 创建项目 const Factory = require('./factory')const factory = new Factory(name, targetCwd)factory.create()console.log('项目名称以及配置项:', name, options)
}
16、接着来写询问用户选择模版的逻辑
github 提供了接口可以获取模板,你可以事先准备好了两个模板发布到 github 上
在 lib 文件夹中创建 http.js 文件,专门用来管理接口,创建好后整个目录结构如下
zyq_fronted_cli
├─ bin
│ ├─ cli.js
├─ lib
│ ├─ create.js
│ ├─ factory.js
│ ├─ http.js
└─ package.json
17、安装 axios ,编写 http.js 文件内容
npm install axios
http.js 文件const axios = require('axios')axios.interceptors.response.use(res => {return res.data
})// 获取模版列表
async function getRepoList(myGithub = 'vue3-0-cli-yd'){return axios.get('https://api.github.com/orgs/vue3-0-cli-yd/repos') // 更换自己的 github 项目 `https://api.github.com/orgs/wangml-gitbub/repos`
}// 获取版本信息
async function getTagList(repo) {return axios.get(`https://api.github.com/repos/vue3-0-cli-yd/${repo}/tags`) // https://api.github.com/orgs/wangml-gitbub/repos
}module.exports = {getRepoList,getTagList
}
18、安装 ora,用于显示加载中的效果;
安装 util , util 可以让没有 node 环境的宿主(如:浏览器)拥有 node 的 util模块;
安装 download-git-repo,用于下载 git 存储库
npm install ora@5.4.1
npm install util
npm install download-git-repo
19、编写 factory.js 文件内容, 添加加载动画、获取用户选择的模版、获取模版的 tag 列表、下载远程模版、创建项目 的逻辑
factory.js 文件const { getRepoList, getTagList } = require('./http')
const ora = require('ora') // 显示加载中的效果
const util = require('util') // 让没有 node 环境的宿主拥有 node 的 util 模块
const downloadGitRepo = require('download-git-repo') // 下载 git 存储库
const inquirer = require('inquirer')
const path = require('path')
const chalk = require('chalk')module.exports = class Factory{constructor(name, targetCwd){this.name = name // 目录名称this.targetCwd = targetCwd // 目录所在地址this.downloadGitRepo = util.promisify(downloadGitRepo) // 对 download-git-repo 进行 promise 化改造console.log(this.name, this.targetCwd)}// 加载动画async loading(fn, message, ...args) {const spinning = ora(message) // 初始化 ora,传入提示信息 message spinning.start() // 开始加载动画try {const result = await fn(...args) // 执行 fn 方法spinning.succeed() // 将状态改为成功return result} catch (err){spinning.fail('Request failed, refetch ...')}}// 获取用户选择的模版async getRepo(){// 从远程拉取模板数据const repoList = await this.loading(getRepoList, 'waiting fetch template')if(!repoList) return// 过滤需要的模板名称const repos = repoList.map(item => item.name) console.log(repos)// 让用户选择模版const { repo } = await inquirer.prompt({name: 'repo',type: 'list',choices: repos,message: 'Please choose a template to create project'})// 返回用户选择的名称return repo }// 获取模版的 tag 列表async getTag(repo){// 从远程拉取模板 tag 列表const tags = await this.loading(getTagList, 'waiting fetch tag', repo)if(!tags) return// 过滤需要的 tag 名称const tagList = tags.map(item => item.name)console.log(tagList)// 让用户选择 tagconst { tag } = await inquirer.prompt({name: 'tag',type: 'list',choices: tagList,message: 'Place choose a tag to create project'})// 返回用户选择的 tagreturn tag}// 下载远程模版async download(repo, tag){const requestUrl = `vue3-0-cli-yd/${repo}${tag ? '#' + tag : ''}` // 拉取模版的地址const createUrl = path.resolve(process.cwd(), this.targetCwd) // 创建项目的地址// 下载方法调用await this.loading(this.downloadGitRepo, 'waiting download template', requestUrl, createUrl)}// 创建项目async create() {console.log('创建项目---', this.name, this.targetCwd)try {// 获取用户选择的模版名称const repo = await this.getRepo()// 获取用户选择的 tagconst tag = await this.getTag(repo)await this.download(repo, tag)// 4)模板使用提示console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`)console.log(`\r\n cd ${chalk.cyan(this.name)}`)console.log(`\r\n npm install`)console.log("\r\n npm run dev\r\n")} catch (error) {console.log(error);}}
}
20、到此就结束啦,可以使用这个自定义的脚手架进行拉取相应的模版
zyq_fronted create my_project
选择模版及 tag
cd my_project
npm install
npm run dev
21、代码地址
cli.js 文件代码
#! /usr/bin/env nodeconst commander = require('commander') // 自定义指令
const create = require('../lib/create.js')commander.version('0.1.0').command('create <project_name>').description('create a new project').option('-f --force', 'overwrite target directory if it exist').action((name, option) => {console.log(name, option)create(name, option)})const chalk = require('chalk') // chalk 改变颜色const figlet = require('figlet') // figlet 改变字体commander.on('--help', () => { // 监听 --help 执行console.log('\r\n' + figlet.textSync('ZYQ_FRONTED_CLI', {font: 'Ghost',horizontalLayout: 'default',verticalLayout: 'default',width: 500,whitespaceBreak: true}))// 新增说明信息console.log(`\r\nRun ${chalk.cyan(`zyq_fronted <command> --help`)} for detailed usage of given command\r\n`)})commander.parse()
create.js 文件代码
module.exports = async function (name, options){const path = require('path')const fs = require('fs-extra')const inquirer = require('inquirer')const cwd = process.cwd() // 当前命令行选择的目录const targetCwd = path.join(cwd, name) // 需要创建的目录地址console.log(cwd,targetCwd)// 判断是否存在该目录if (fs.existsSync(targetCwd)) { // 目录存在if (options.force) { // 是否强制创建console.log('进行强制创建')// 移除原来存在的项目await fs.remove(targetCwd)} else {console.log('询问用户是否强制创建')// 询问用户是否强制创建项目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 {console.log('移除存在的文件')await fs.remove(targetCwd)}}} else { // 目录不存在console.log('目录不存在,进行强制创建')}// 创建项目 const Factory = require('./factory')const factory = new Factory(name, targetCwd)factory.create()console.log('项目名称以及配置项:', name, options)
}
http.js 文件代码
const axios = require('axios')axios.interceptors.response.use(res => {return res.data
})// 获取模版列表
async function getRepoList(){return axios.get('https://api.github.com/orgs/vue3-0-cli-yd/repos') // https://api.github.com/orgs/wangml-gitbub/repos
}// 获取版本信息
async function getTagList(repo) {return axios.get(`https://api.github.com/repos/vue3-0-cli-yd/${repo}/tags`) // https://api.github.com/orgs/wangml-gitbub/repos
}module.exports = {getRepoList,getTagList
}
factory.js 文件代码
const { getRepoList, getTagList } = require('./http')
const ora = require('ora') // 显示加载中的效果
const util = require('util') // 让没有 node 环境的宿主拥有 node 的 util 模块
const downloadGitRepo = require('download-git-repo') // 下载 git 存储库
const inquirer = require('inquirer')
const path = require('path')
const chalk = require('chalk')module.exports = class Factory{constructor(name, targetCwd){this.name = name // 目录名称this.targetCwd = targetCwd // 目录所在地址this.downloadGitRepo = util.promisify(downloadGitRepo) // 对 download-git-repo 进行 promise 化改造console.log(this.name, this.targetCwd)}// 加载动画async loading(fn, message, ...args) {const spinning = ora(message) // 初始化 ora,传入提示信息 message spinning.start() // 开始加载动画try {const result = await fn(...args) // 执行 fn 方法spinning.succeed() // 将状态改为成功return result} catch (err){spinning.fail('Request failed, refetch ...')}}// 获取用户选择的模版async getRepo(){// 从远程拉取模板数据const repoList = await this.loading(getRepoList, 'waiting fetch template')if(!repoList) return// 过滤需要的模板名称const repos = repoList.map(item => item.name) console.log(repos)// 让用户选择模版const { repo } = await inquirer.prompt({name: 'repo',type: 'list',choices: repos,message: 'Please choose a template to create project'})// 返回用户选择的名称return repo }// 获取模版的 tag 列表async getTag(repo){// 从远程拉取模板 tag 列表const tags = await this.loading(getTagList, 'waiting fetch tag', repo)if(!tags) return// 过滤需要的 tag 名称const tagList = tags.map(item => item.name)console.log(tagList)// 让用户选择 tagconst { tag } = await inquirer.prompt({name: 'tag',type: 'list',choices: tagList,message: 'Place choose a tag to create project'})// 返回用户选择的 tagreturn tag}// 下载远程模版async download(repo, tag){const requestUrl = `vue3-0-cli-yd/${repo}${tag ? '#' + tag : ''}` // 拉取模版的地址const createUrl = path.resolve(process.cwd(), this.targetCwd) // 创建项目的地址// 下载方法调用await this.loading(this.downloadGitRepo, 'waiting download template', requestUrl, createUrl)}// 创建项目async create() {console.log('创建项目---', this.name, this.targetCwd)try {// 获取用户选择的模版名称const repo = await this.getRepo()// 获取用户选择的 tagconst tag = await this.getTag(repo)await this.download(repo, tag)// 4)模板使用提示console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`)console.log(`\r\n cd ${chalk.cyan(this.name)}`)console.log(`\r\n npm install`)console.log("\r\n npm run dev\r\n")} catch (error) {console.log(error);}}
}
package.json 内容
{"name": "zyq_fronted_cli","version": "1.0.0","description": "","main": "index.js","bin": {"zyq_fronted": "./bin/cli.js"},"scripts": {"test": "echo \"Error: no test specified\" && exit 1"},"author": "","license": "ISC","dependencies": {"axios": "^1.3.5","chalk": "^4.1.0","commander": "^10.0.1","download-git-repo": "^3.0.2","figlet": "^1.6.0","fs-extra": "^11.1.1","inquirer": "^8.2.5","ora": "^5.4.1","util": "^0.12.5"}
}