搭建属于自己的脚手架

2,608 阅读6分钟

搭建属于自己的脚手架

cli是一种通过命令行来交互的工具应用,比较常见的有create-react-app、vue-cli等,它们都是可以将一段js脚本,通过封装为可执行代码的形式,进行一些操作。

使用cli之后,我们能够快速创建出一些我们业务中的模板文件,比如快速创建一个项目内容,配置一些公共的eslint、webpack等工具,最方便的地方在于cli能够提供一些交互式的命令,根据用户回答的结果可以动态地更改从而渲染对应的模板文件。

准备阶段:

脚手架的基本工作流程如下:

  • 通过命令行交互方式询问用户相应问题
  • 根据用户的回答结果生成对应的文件

在搭建过程中我们主要依赖以下工具库:

commander | inquirer | chalk | download-git-repo

我们首先创建一个文件夹cli,并进入,接着npm init创建package.json文件,在cli目录下创建cli.js文件,此时的目录结构如下:

cli
   ├─ cli.js
   └─ package.json

进入cli.js文件,在文件开头输入:

cli.js文件中
#! /usr/bin/env node
这句代码必须加上,主要是为了让系统看到这句话的时候会沿着该路径去查找node并执行
console.log('cli is working')

完成之后我们进入package.json文件中,将name改为"clay-cli",新增"bin":"cli.js",接着在命令行输入npm link,将命令挂载到全局,这样每次我们输入cli,就可以直接运行了。

package.json文件中
{
  "name": "clay-cli",
  "version": "1.0.0",
  "description": "",
  "main": "cli.js",
  "bin":"cli.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
命令行结果
PS F:\clayCli\cli> npm link
npm WARN clay-cli@1.0.0 No description
npm WARN clay-cli@1.0.0 No repository field.

up to date in 0.35s
C:\Program Files\nodejs\clay-cli -> C:\Program Files\nodejs\node_modules\clay-cli\cli.js
C:\Program Files\nodejs\node_modules\clay-cli -> F:\clayCli\cli
PS F:\clayCli\cli> clay-cli
cli is working
PS F:\clayCli\cli>

做完这些我们就可以开始使用以下的工具库了

1.inquirer(该工具主要作用是通过命令行询问用户问题,记录回答结果)

inquirer具体详情可点击

首先npm install inquirer --save,接着进入cli.js文件中将其引入

#! /usr/bin/env node
const inquirer = require('inquirer')
const promptList = [{
    type:'list',
    message:'请选择一种框架',
    name:'framework',
    choices:[
        'vue2.0',
        'vue3.0',
        'react'
    ]
}]
inquirer.prompt(promptList).then(answers=>{
    console.log(answers);
})

由于交互的问题种类不同,inquirer为每个问题提供了很多参数,包括:

  • type:提问的类型,包括:input,confirm,list,rawlist,expand,checkbox,password,editor
  • name: 存储当前问题回答的变量(对应上述例子中answers中key值
  • message:描述问题
  • default:当没选择或者没填写时的默认值
  • choices:顾名思义,选项列表
  • validate:校验用户的回答
  • filter:对用户的回答进行过滤
  • when:根据前面的问题,进行判断 下面举一些常见的例子:
1) type:list 见上
2) type:input
const promptList = [
{
    type:'input',
    message:'请输入手机号',
    name:'phonenum',
    validate:function(val){
        if(val.match(/\d{11}/g)){
            return val
        }else{
            return '请输入11位数字'
        }
    }
}
]
inquirer.prompt(promptList).then(answers=>{
    console.log(answers);
})
3) type:confirm
const promptList = [
{
    type:'confirm',
    message:'是否使用vue',
    name:'usevue',
}
]
inquirer.prompt(promptList).then(answers=>{
    console.log(answers);
})
4) type:checkbox
const promptList = [
{
    type:'checkbox',
    message:'选择模块',
    name:'modules',
    choices:[
        "ts",
        "eslint",
        "webpack"
    ]
}
]
inquirer.prompt(promptList).then(answers=>{
    console.log(answers);
})
2.commander(该工具主要作用是可以自定义命令行指令)

commander具体详情可点击

commander的常用API:

  • command:自定义执行的命令
  • option:定义可选参数
  • description:描述命令
  • action:命令执行完成后所执行的方法
  • parse:解析命令行参数(注意:这个方法一定要在最后的时候调用)

现在我们修改cli.js文件,npm install commander --save,接着引入commander

cli.js文件
const program = require('commander')
program
    .command('create <appname>') //其中<>代表必填 []代表可选
    .option('-f,--force','描述性文字')
    .description('创建一个新的项目')
    .action((name,options)=>{
        console.log('项目名是:',name,'options:',options);
    })
program.parse()

上图可以看到:
1.当你输入clay-cli时,会提示所有可输入命令
2.可以通过描述文字判断该命令的作用
3.通过输入-f,可以获得options对象,对象的key就是force,value为true,可以通过判断force值存在与否进行下一步的操作(注意:option根据需要你想设置为啥就是啥,不用非得是-f,--force

commander结合inquiry一起使用

#! /usr/bin/env node
const inquirer = require('inquirer')
const program = require('commander')
program
    .command('create <appname>')
    .description('创建一个新的项目')
    .option('-f,--force','描述性文字')
    .action(name=>{
        return inquirer.prompt([
            {
                type:'list',
                name:'framework',
                message:'请选择一个框架',
                choices:[
                    'vue',
                    'react'
                ]
            }
        ])
        .then(answers=>{
            console.log(name,'选择的框架是:', answers);
        })
    })
program.parse()
3.chalk(该工具主要作用是美化控制台输出的内容)

chalk具体详情可点击

chalk是一个颜色的插件,可以更改命令行的颜色,npm install chalk --save

const program = require('commander')
const chalk = require('chalk')
program
    .command('create <appname>') //其中<>代表必填 []代表可选
    .option('-f,--force','描述性文字')
    .description('创建一个新的项目')
    .action((name,options)=>{
        console.log('项目名是:',chalk.red(name));
        console.log('项目名是:',chalk.blue(name));
        console.log('项目名是:',chalk.yellow(name));
    })
program.parse()
4.download-git-repo(该工具主要作用是下载远程模板)

download-git-repo具体详情可点击

完成用户的选项之后就可以使用download-git-repo可以从github上面下载模板文件,但是download-git-repo是不支持promise的,在使用它的时候需要使用util模板中的promisify方法对其进行promise化

download(repository, destination, options, callback),其中repository是下载地址,destination为下载目录,这两个参数是最主要的

整合搭建

接下来我们利用上述四个工具库来搭建完整的脚手架

1.利用commander创建命令

打开cli.js文件,创建create 命令,过程如上述commander例子

cli.js文件
const program = require('commander')
program
    .command('create <appname>') //其中<>代表必填 []代表可选
    .option('-f,--force','描述性文字')
    .description('创建一个新的项目')
    .action((name,options)=>{
        console.log('项目名是:',name,'options:',options);
    })
program.parse(process.argv)// 解析用户执行命令传入参数

2.接着创建lib文件夹,并在lib下创建create.js文件,写入代码并在cli.js文件中引入

create.js文件
module.exports = async function(name,options){} 
cli.js文件
const program = require('commander')
program
    .command('create <appname>') //其中<>代表必填 []代表可选
    .option('-f,--force','描述性文字')
    .description('创建一个新的项目')
    .action((name,options)=>{
        require('./lib/create.js')(name,options)
    })
program.parse(process.argv)

运行结果:

3.在github中配置模板文件

首先进入github点击右上角Organizations,进行一番配置后创建两个repositories,一个为vue-template-1.0,创建好之后再创建两个tag,分别为v1.0,v2.0;另一个为vue-template-3.0,创建好之后再创建4个tag,分别为v1.0.0、v2.0.0、v3.0.0、v3.1.2。再将对应代码传上去。模板我已上传至github(地址)(注:本文所用模板引自文章 从 0 构建自己的脚手架/CLI知识体系(万字))

4.获取模板信息

在lib目录下新建http.js文件,使用github提供的接口获取数据

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/clay-cli/repos')
}
async function  getTagList(repo) {
    return axios.get(`https://api.github.com/repos/clay-cli/${repo}/tags`)
}
module.exports = {
    getRepoList,
    getTagList
}

5.用户选择模板

打开create.js文件,调用http.js文件中的接口函数获取模板列表,接着使用inquirer引入模板列表

create.js文件
const {getRepoList,getTagList} = require('./http.js')
const inquirer = require('inquirer')
async function getRepos){
    const repoList = await getRepoList()
    const repos = repoList.map(item=>item.name)
    const {repo} = await inquirer.prompt([{
        name:'repo',
        type:'list',
        choices:repos,
        message:'请选择一个模板'
    }])
    return repo
}
module.exports = async function(name,options){
    const repo = await getRepos)
    console.log('用户选择的模板是',repo);
} 

6.用户选择版本

逻辑同选择模板

const {getRepoList,getTagList} = require('./http.js')
const inquirer = require('inquirer')

async function getRepos(){
    const repoList = await getRepoList()
    const repos = repoList.map(item=>item.name)
    const {repo} = await inquirer.prompt([{
        name:'repo',
        type:'list',
        choices:repos,
        message:'请选择一个模板'
    }])
    return repo
}

async function getTags(repo){
    const tagList = await getTagList(repo)
    const tags = tagList.map(item=>item.name)
    const {tag} = await inquirer.prompt([{
        name:'tag',
        type:'list',
        choices:tags,
        message:'请选择一个版本'
    }])
    return tag
}

module.exports = async function(name,options){
    const repo = await getRepos()
    const tag = await getTags(repo)
    console.log('用户选择的模板是',repo,'用户选择的版本是',tag);
} 

7.下载模板

完成以上工作后我们就可以使用download-git-repo远程下载模板了
1)reate.js文件中引入,因为其是不支持promise的,需要先进行promise化。
2) 获取当前命令行选择的目录
3) 新增需要创建的目录地址,即模板所在地址
4) 创建下载地址
5)模板下载完成后使用chalk进行提示

const {getRepoList,getTagList} = require('./http.js');
const inquirer = require('inquirer');
const path = require('path');
const util = require('util');
const downloadGitRepo = require('download-git-repo');
const chalk = require('chalk');
//获取模板
async function getRepos(){
    const repoList = await getRepoList();
    const repos = repoList.map(item=>item.name);
    const {repo} = await inquirer.prompt([{
        name:'repo',
        type:'list',
        choices:repos,
        message:'请选择一个模板'
    }])
    return repo
}
//获取标签
async function getTags(repo){
    const tagList = await getTagList(repo);
    const tags = tagList.map(item=>item.name);
    const {tag} = await inquirer.prompt([{
        name:'tag',
        type:'list',
        choices:tags,
        message:'请选择一个版本'
    }])
    return tag;
}
//下载
async function onDownload(name,repo,tag){
    const requestUrl = `clay-cli/${repo}${tag?'#'+tag:''}`;//创建下载地址
    const cwd = process.cwd(); //获取当前命令行选择的目录
    const targetPath = path.join(cwd,name); //模板下载所在地址
    const downloadFunc = util.promisify(downloadGitRepo);
    downloadFunc(requestUrl,targetPath);
}

module.exports = async function(name,options){
    const repo = await getRepos();
    const tag = await getTags(repo);
    await onDownload(name,repo,tag);
    console.log(`\r\n成功创建项目 ${chalk.cyan(name)}`);
    console.log(`\r\n  cd ${chalk.cyan(name)}`);
    console.log('  npm run dev\r\n');
} 

最终的目录结构

clayCli
├─ cli
│  ├─ clay
│  │  └─ vue3.0-template
│  │     ├─ babel.config.js
│  │     ├─ package.json
│  │     ├─ public
│  │     │  ├─ favicon.ico
│  │     │  └─ index.html
│  │     ├─ README.md
│  │     └─ src
│  │        ├─ App.vue
│  │        ├─ assets
│  │        │  └─ logo.png
│  │        ├─ components
│  │        │  └─ HelloWorld.vue
│  │        └─ main.js
│  ├─ cli.js
│  ├─ lib
│  │  ├─ create.js
│  │  └─ http.js
│  ├─ package-lock.json
│  └─ package.json
└─ README.md

参考文章

inquirer

commander

chalk

download-git-repo

从 0 构建自己的脚手架/CLI知识体系(万字),作者:ITEM (写的非常非常好,想学习cli的一定要拜读)

你还在重复的搬砖!?写个 cli 工具解放你的双手吧 - 动态生成代码模板,作者:花果山技术团队

面试官:请简述一下vue-cli命令行工具,你能自己手写一个吗?