保姆级教程——逐步剖析前端脚手架工具cli的基本原理

前言

本文默认读者已安装了node,npm等工具,并了解其基本用法,涉及到此类内容将不会深入讲解,此类基础环境的安装过程将会略去

开篇

相信写过react和vue的小伙伴们都会用到create-react-app和vue-cli这类脚手架工具,那么有没有想过,这类工具是怎么做出来的呢?cli是command-line interface(命令行界面)的缩写,本篇文章的目标是通过动手写一个cli工具,来剖析vue-cli这类脚手架工具的基本原理

动手

首先我们新建一个test-cli文件夹,里面只有一个index.js文件,index.js的内容如下

#!/usr/bin/env node
console.log('hello cli');

然后我们在test-cli的目录下执行npm init命令,生成一个package.json文件,要求不高,输入后一路回车就可以了,此时我们可以生成如下的package.json

{
  "name": "test-cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

然后我们手动给package.json添加一些代码


  "bin":{
    "test-cli" : "./index.js"
  },

这里暂停一下,解释两个知识点,一个是#!/usr/bin/env node,一个是package.json里面的bin

#!/usr/bin/env node

对于文件来说,#!这个符号代表此文件可以当做脚本运行,那么怎么运行呢,/usr/bin/env node这行的意思就是用node来执行此文件,node怎么来呢,就去用户(usr)的安装根目录(bin)下的env环境变量中去找,简单的说就是如果在windows上面,就去安装node的bin目录去找node执行器,一般我们都放在环境变量中,简单来说,就是帮助文件找到正确的执行器

bin

对于一个模块来说,如果是全局安装,则npm会为bin中配置的文件在bin目录下创建一个软连接(对于windows系统,默认会在C:\Users\username\AppData\Roaming\npm目录下),如果是局部安装,则会在项目内的./node_modules/.bin/目录下创建一个软链接。因此,按上面的例子,安装test-cli的时候,npm就会为index.js在/usr/local/bin/test-cli路径创建一个软链接。这样,当我们安装完test-cli后,我们在控制台中执行test-cli就相当于执行node index.js

安装

那么,接下来,顺理成章,我们安装我们自定义的这个模块,我们在test-cli的目录下,执行npm install -g全局安装test-cli,如果没有意外,安装成功后,在cmd中执行test-cli 命令,你将会发现控制台中输出我们的文本

接收参数

命令行工具怎么少得了自定义参数,我们在index.js中加入以下代码

console.log(process.argv);

保存后执行test-cli app 命令,我们可以得到以下输出

我们可以发现输出了一个数组,第一个参数是nodejs的安装目录,第二个参数是模块的安装目录,第三个参数是我们的自定义参数,于是可以充分利用这些参数了,记住,这就是个node执行的脚本,我们可以做的事情很多,例如我们可以在index.js中添加以下代码

var fs = require('fs');

// 新建目录
function mkdir (path, fn) {
    fs.mkdir(path, function (err) {
        fn && fn()
    })
}
mkdir('./'+process.argv[2]);

然后我们换个目录执行命令,test-cli testdir  ,就会发现目录下面多了一个名字为testdir的文件夹,总之你可以随意发挥

引进一些轮子

俗话说得好,有人的地方,就有轮子,完全手动写这些东西不是不可以,但是太累了,所以我们引进一些成熟的轮子来加快我们的开发进度

命令封装

首先引进commander.js,commander.js已经帮我们封装了很多通用命令,依旧是test-cli目录下执行命令,npm install commander --save

然后我们把index.js中的代码替换成以下的内容


var program = require('commander');
program
    .version('1.0.0', '-v, --version')
    .command('create [createname]')
    .alias('c')
    .description('这是一个帮助提示')
    .option('-a, --modulename [moduleName]', '模块名称')
    .action((createName, option) => {
        console.log('指令 create 后面跟的参数值 createname: ' + createName);
        console.log(option.modulename);
    })
    //自定义帮助信息
    .on('--help', function () {
        console.log('把相亲对象组成球队')
        console.log('去夺下那世界杯')
    })
program.parse(process.argv)

然后执行以下命令 test-cli create app 获得以下输出

执行命令test-cli create app --modulename 小敏哥贼帅 获得以下输出

执行命令 test-cli create --help 将获得以下输出

是不是有点懵逼?莫慌,各种参数的解析如下

  • version - 定义命令程序的版本号,.version('0.0.1', '-v, --version'),第一个参数版本号必须,第二个参数可省略,默认为 -V 和 --version
  • command – 定义命令行指令,后面可跟上一个name,用空格隔开,如 .command('app [name]')
  • alias – 定义一个更短的命令行指令 ,如执行命令$ exercise-cli c 与之是等价的
  • description – 描述,它会在help里面展示
  • option – 定义参数。它接受四个参数,在第一个参数中,它可输入短名字 -a和长名字–name ,使用 | 或者,分隔,在命令行里使用时,这两个是等价的,区别是后者可以在程序里通过回调获取到;第二个为描述, 会在 help 信息里展示出来;第三个参数为回调函数,他接收的参数为一个string,有时候我们需要一个命令行创建多个模块,就需要一个回调来处理;第四个参数为默认值
  • action – 注册一个callback函数,这里需注意目前回调不支持let声明变量,这里可以理解为命令输入后按下回车之后的回调
  • parse – 用于解析process.argv,设置options以及触发commands,用法示例:.parse(process.argv)

有一点需要注意,上述的操作是可重复并且不会向上覆盖的,例如想定义多个命令,只需要执行program.command多次即可生成多个命令

添加用户交互

上述的轮子让我们获得了定义命令和参数的能力,接下来我们更进一步,不知道大家用vue-cli的时候,有没有发现,vue-cli是有用户交互的,你可以一步一步的做一些选择,例如是否使用vuex,是否使用eslint等等,那么这种功能怎么实现呢?我们还是在test-cli目录下执行下列命令安装以来 npm install inquirer  --save  npm install chalk--save

在index.js中引入这两个扩展

var inquirer = require('inquirer');
var chalk = require('chalk');

然后在commander的action回调中添加下列代码

        //用户交互配置,支持多种方式的输入
         var prompList = [
             {
                 type:'input',
                 message:'姓名',
                 name:'name'
             },{
                 type:'input',
                 message:'手机号',
                 name:'phone',
                 validate:val=>{
                     if(val.match(/\d{11}/g)){
                         return true
                     }
                     return '请输入11位数字'
                 }
             },{
                 type:'confirm',
                 message:'是否参加测试?',
                 name:'assess',
                 prefix:'前缀'
             },{
                 type:'confirm',
                 message:'是否同意协议?',
                 name:'notice',
                 suffix:'后缀',
                 when:answers=>{
                     return answers.assess
                 }
             },{
                 type:'list',
                 message:'请选择学历:',
                 name:'eductionBg',
                 choices:[
                     "大专",
                     "本科",
                     "本科以上"
                 ],
                 filter:val=>{//将选择的内容后面加学历
                     return val+'学历'
                 }
             },{
                 type:'rawlist',
                 message:'请选择你爱玩的游戏:',
                 name:'game',
                 choices:[
                     "农药",
                     "吃鸡",
                 ]
             },{
                 type:'expand',
                 message:'请选择你喜欢的水果:',
                 name:'fruit',
                 choices: [
                     {
                         key: "a",
                         name: "Apple",
                         value: "apple"
                     },
                     {
                         key: "O",
                         name: "Orange",
                         value: "orange"
                     },
                     {
                         key: "p",
                         name: "Pear",
                         value: "pear"
                     }
                 ]
             },{
                 type:'checkbox',
                 message:'请选择你喜欢的颜色:',
                 name:'color',
                 choices:[
                     {
                         name: "red"
                     },
                     new inquirer.Separator(), // 添加分隔符
                     {
                         name: "blur",
                         checked: true // 默认选中
                     },
                     {
                         name: "green"
                     },
                     new inquirer.Separator("--- 分隔符 ---"), // 自定义分隔符
                     {
                         name: "yellow"
                     }
                 ]
             },{
                 type:'password',
                 message:'请输入你的游戏密码:',
                 name:'pwd'
             }

         ]
        //获取用户的输入结果
         inquirer.prompt(prompList).then(answers=>{
             console.log(answers);//用户输入结果
             console.log(chalk.green('告诉我你看到啥了'))//字体绿色
             console.log(chalk.blue('五彩缤纷是吧'))//字体蓝色
             console.log(chalk.blue.bgRed('enjoy it')) //支持设置背景
             console.log(chalk.blue(answers))
         })

然后运行命令test-cli create app 开始见证奇迹

这两个包的使用方式比较简单,inquirer是用来创建用户交互的,你可以参照示例代码创建跟用户的交互,最后在prompt的回调中获取用户输入的答案,chalk是用来显示一些花里胡哨的颜色,使用也比较简单,可以直接参考示例代码

拉取远程模板代码

创建脚手架怎么少得了获取模板代码的能力,我们继续引进新的轮子,继续安装依赖,npm install download-git-repo --save  npm install ora --save,其中download-git-repo是用来拉取git上面的文件的,ora则是用来显示loading效果

接着,继续引入依赖

var download = require('download-git-repo');
const ora = require('ora');

然后在index.js中commander的action回调中添加下列代码

        mkdir('./'+createName);//创建文件夹
        const spinner = ora('拉取模板中,请稍候...').start();//显示loading效果
        //拉取git代码
        download('direct:https://github.com/githubxiaominge/testDownload.git', './'+createName, { clone: true }, function (err) {
            spinner.stop();//隐藏loading
            console.log(err ? 'Error' : 'Success')
        })

代码依旧很简单,上述的示例代码基本上都添加了注释,用法就不再赘述了

最后,我们换一个目录,不在test-cli目录下了,重新开一个目录,例如testdir,打开控制台,执行命令test-cli create app 一路操作下去之后,你就会发现testdir目录下面多了个app的文件夹,里面还有git上面拉取下来的模板代码

结语

至此,我们完成了一个很简单的cli,支持简单的用户交互,代码拉取,文件创建等简单的功能,当然,如果需要,你也可以对这个cli再次进行扩展而形成自己的cli。通过这个过程,相信你也基本理解了前端cli工具的运行原理,喜欢就点个赞哦

另外,这篇文章所涉及到的所有代码,可以到这里下载,传送门

猜你喜欢

转载自blog.csdn.net/xiaomingelv/article/details/108310821