1.0.2 • Published 11 months ago

zhihao-cli v1.0.2

Weekly downloads
-
License
ISC
Repository
-
Last release
11 months ago

准备工作

必备的知识库

在完善构建脚手架前,需要引入一些脚手架构建中必须用到的工具库。

  • commander 可以自定义一些命令行指令,在输入自定义的命令行的时候,会去执行相应的操作
  • inquirer 可以在命令行询问用户问题,并且可以记录用户回答选择的结果
  • fs-extra 是fs的一个扩展,提供了非常多的便利API,并且继承了fs所有方法和为fs方法添加了promise的支持。 就是操作本地目录
  • chalk 可以美化终端的输出
  • figlet 可以在终端输出logo
  • ora 控制台的loading动画
  • git-clone 下载远程模板

简单三步做好准备工作~

第一步:注册一个npm的账号,用于后面发包

第二步:mkdir zhihao-cli && cd zhihao-cli && npm init -y  // 创建文件夹 切到文件夹目录 初始化项目

第三步:之后在 `package.json` 文件中填入一些基本的信息即可
{
  "name": "zhihao-cli",
  "version": "1.0.0",
  "description": "A tool for cli creation",
  "main": "index.js",
  "scripts": {
    "pub:beta": "npm publish --tag=beta",
    "pub": "npm publish"
  },
  "publishConfig": {
    "registry": "https://registry.npmjs.org/"
  },
  "keywords": [
    "create",
    "cli"
  ],
  "author": "your name",
  "license": "ISC"

开始造轮子

第一步:定义命令

相信大家都用过脚手架,脚手架工具一般都会提供相应的脚本命令以完成一些操作。比如查看版本号的 --version, 查看帮助信息的 --help 等等。在这一步骤中,我们也先来定义下属于我们自己脚手架的命令

创建脚本文件

首先,我们在项目的根目录下创建 bin 目录,然后在该文件夹下创建定义脚本的文件 index.js。创建完成后,在 package.json 文件中定义命令对应的文件:

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

其中,bin 指定了我们日后在使用脚手架时所需要输入的命令,bin/index.js 指定输入命令后所执行的脚本文件路径。

注意:**npm link**会链接这个包,指定到node_module进行软链接,如果**package.json**有**bin**,那链接完,输入**package.json**文件的**name**,即**my-cli**后会执行**bin**下的文件 **./bin/index.js**

编写脚本

命令行界面

需要用到的相关插件:

Commander.js node.js命令行界面的完整解决方案,

npm install commander

我们在脚本中定义:

#!/usr/bin/env node
// 这里的注释 #!/usr/bin/env node 是一个 shebang,它告诉操作系统:运行这个脚本时,
//应该使用哪个解释器来执行这个脚本。在这个例子中,解释器是 "node"。 这个 shebang 是必需的,
//因为如果没有指定解释器,操作系统就不知道如何执行这个脚本。
// CommonJS (.cjs)
const { program } = require('commander');
// ----------commander的使用----------------

// 通过这个选项可以修改帮助信息的首行提示 例如:Usage: my-command [options] [command]
program
  .name("zhihao-cli")
  .usage("<command> [options]")

//选项:
//Commander 使用.option()方法来定义选项,同时可以附加选项的简介。
//每个选项可以定义一个短选项名称(-后面接单个字符)和一个长选项名称(--后面接一个或多个单词),使用逗号、空格或|分隔。
// 另一类选项则可以设置参数(使用尖括号声明在该选项后,如--expect <value>)。如果在命令行中不指定具体的选项及参数,
//则会被定义为undefined。 <>表示必填参数,[]表示选填参数
program
  .option('-d, --debug', 'output extra debugging')
  .option('-s, --small', 'small pizza size')
  .option('-p, --pizza-type <type>', 'flavour of pizza');

// 命令行:
//Commander 使用.command()方法来定义命令行,同时可以附加命令行的简介。
// .command()的第一个参数为命令名称,参数可为必选的(尖括号表示)、
//可选的(方括号表示)或变长参数(点号表示,如果使用,只能是最后一个参数)。第二个参数为命令简介
program
  .command('clone <source> [destination]')
  .description('clone a repository into a newly created directory')
  .action((source, destination) => {
    console.log('clone command called');
  });
  // 解析命令行参数。解析这些参数并将它们关联到程序中的对应操作 process.argv 是 Node.js 运行时环境中的全局变量
  // 打印process.argv有2个参数,第一个是node的执行路径,第二个是当前执行脚本的路径,
  program.parse(process.argv);

  //它用于获取命令行中传递的所有参数和选项信息,并以一个对象的形式返回。
  // 这个对象中的键名就是用户传递的选项名或参数名,而键值则是用户传递的选项值或参数值
//   const options = program.opts();
//   console.log(options);

注意:这里的注释 #!/usr/bin/env node 是一个 shebang,它告诉操作系统:运行这个脚本时,应该使用哪个解释器来执行这个脚本。在这个例子中,解释器是 "node"。 这个 shebang 是必需的,因为如果没有指定解释器,操作系统就不知道如何执行这个脚本。

编写完成后,如果想要在命令行中进行验证,此时我们可以在命令行中输入以下命令:

npm link

然后在命令行中输入

zhihaoa-cli

npm.io

有我们配置好的选项以及命令行

注意:zhihao-cli —help是默认配置有的

美化终端输出

chalk 终端美化工具

npm install chalk@4.0.0

注意: Chalk 5 is ESM.模块,如果想要用cjs模块,安装4版本的

脚本文件:

const chalk = require('chalk');

//-----------------chalk的使用-----------------
console.log(chalk.blue('Hello world!'));
console.log(chalk.blue.bgRed.bold('Hello world!'));
console.log(chalk.blue('Hello', 'World!', 'Foo', 'bar', 'biz', 'baz'));
console.log(chalk.red('Hello', chalk.underline.bgBlue('world') + '!'));

命令行交互模块

inquirer 命令行交互

常见交互式命令行用户界面的集合。

npm.io

npm install --save inquirer@8.0.0

Inquirer v9 及更高版本是本机 esm 模块,这意味着您不能再使用 commonjs 语法require('inquirer')。所以需要到8.0.0

const inquirer = require('inquirer'); //命令行交互

//-----------------inquirer的使用-----------------

// inquirer.prompt(questions, answers) -> promise 启动提示界面(查询会话)
// questions: array 包含Question 对象 
    // Question 对象包含以下属性:
    // type: 提示的类型,包括:input, number, confirm, list, rawlist, expand, checkbox, password, editor
    // name: 键名,用于获取用户输入的值
    // message: 提示信息
    // default: 默认值
    // choices: 选项,包含:type为list, rawlist, expand, checkbox时的选项
// answers: 默认值(对象){}
inquirer.prompt([
    {
        type: 'input',
        name: 'name',
        message: '请输入你的姓名'
    },
    {
        type: 'input',
        name: 'age',
        message: '请输入你的年龄'
    }]).then(answers => {
        console.log(answers);
    }).catch(err => {
        console.log(err);
    })

控制台加载效果

ora 控制台loading效果

npm install ora@5.0.0

const ora = require('ora'); //loading效果

//-----------------ora的使用----------------

//启动加载效果
const spinner = ora('Loading unicorns').start();

//1秒后加载效果变为Loading rainbows
setTimeout(() => {
  spinner.color = 'yellow';
  spinner.text = 'Loading rainbows';
}, 1000);
//3秒后显示为加载成功或者失败
setTimeout(() => {
  // spinner.succeed('Loading rainbows succeed');
  spinner.fail('Loading rainbows failed');
}, 3000);

终端艺术字

figlet:生成终端艺术字

npm install figlet

const figlet = require("figlet"); //打印大字体

//-----------------figlet的使用----------------
figlet("Hello World!!", function (err, data) {
    if (err) {
      console.log("Something went wrong...");
      console.dir(err);
      return;
    }
    console.log(data);
  });

模仿vue

#!/usr/bin/env node
// 这里的注释 #!/usr/bin/env node 是一个 shebang,它告诉操作系统:运行这个脚本时,
//应该使用哪个解释器来执行这个脚本。在这个例子中,解释器是 "node"。 这个 shebang 是必需的,
//因为如果没有指定解释器,操作系统就不知道如何执行这个脚本。
// CommonJS (.cjs)
const { program } = require('commander');
const chalk = require('chalk');
const inquirer = require('inquirer'); //命令行交互
const ora = require('ora'); //loading效果
const figlet = require("figlet"); //打印大字体
const fs = require('fs-extra'); //文件操作
const path = require('path'); //路径操作
const gitClone = require('git-clone'); //克隆项目

//首行显示提示
program.name('zhihao-cli').usage('<command> [options]');

//显示版本号
program.version(`v${require('../package.json').version}`, '-v, --version');

//增加创建命令
program
    .command('create <app-name>')
    .description('创建一个项目')
    .action(async(name) => {
        // 创建一个名字为name的项目,把我们模板项目代码放到这个文件下的目录下
            // 1.判断当前目录是否为空 process.cwd() 获取当前目录(node命令)  fs.existsSync() 判断文件是否存在
            const targetDir = path.join(process.cwd(), name);
            if(fs.existsSync(targetDir)){
                //存在文件夹 先询问是否清空
                const answers = await inquirer.prompt([
                    {
                        type: 'confirm',
                        name: 'isRemove',
                        message: '当前目录不为空,是否清空当前目录?',
                        default: true
                    }
                ]);
                if(answers.isRemove){
                    //清空当前目录
                    // 使用 fs.rmdir() 时需要添加 { recursive: true } 参数来指示要删除整个目录。
                    fs.rmdir(targetDir, { recursive: true })
                    console.log('targetDir: ', targetDir);
                    console.log('清空成功');
                }else{
                    return;
                }
            }
            // 2.创建目录
             //定义一个对象,存放需要创建的克隆项目模板地址
            const projectList = {
                'vue': "https://github.com/kfc-vme50/vue-template.git",
                'react': 'https://github.com/kfc-vme50/react-template.git',
                'react&ts': 'https://github.com/kfc-vme50/react-template-ts.git',
                'vue&ts': 'https://github.com/kfc-vme50/vue-template-ts.git',
            }
            const res = await inquirer.prompt([
                {
                    type: 'list',
                    name: 'type',
                    message: '请选择项目框架',
                    choices: [
                        {
                            name: 'vue',
                            value: 'vue'
                        },
                        {
                            name: 'react',
                            value: 'react'
                        }
                    ]
                },
                {
                    type: 'list',
                    name: 'ts',
                    message: '是否需要ts',
                    choices: [
                        {
                            name: '是',
                            value: true
                        },
                        {
                            name: '否',
                            value: false
                        }
                    ]
                }
            ])
            const key = res.type + (res.ts ? '&ts' : '');
            const spinner = ora('正在下载项目模板...').start();
            // 3.克隆项目 gitClone 参数1:项目地址 参数2:目标地址 参数3:配置项(有选择克隆的分支) 参数4:回调函数
            gitClone(projectList[key], targetDir, { checkout: 'main'},(err) => {
                if(err){
                    spinner.fail(chalk.red('下载失败'));
                }else {
                    spinner.succeed(chalk.green('下载成功'));
                    // 删除.git文件
                    // fs.removeSync(path.join(targetDir, '.git'));
                    // 成功后提示操作
                    console.log(chalk.green('Download success'));
                    console.log(chalk.green('cd ' + name));
                    console.log(chalk.green('npm install'));
                    console.log(chalk.green('npm run dev'));
                }
            })
    });
// 给help信息增加提示
program.on('--help', function(){
    console.log(figlet.textSync(
        "zhihao-cli",
        {
          font: "Ghost",
          horizontalLayout: "default",
          verticalLayout: "default",
          width: 80,
          whitespaceBreak: true,
        }))
});
//解析命令
program.parse(process.argv);

发包

npm login登陆自己的login ,记得是npm源

npm publish在目录下执行,则可以发包

步骤文件

#!/usr/bin/env node
// 这里的注释 #!/usr/bin/env node 是一个 shebang,它告诉操作系统:运行这个脚本时,
//应该使用哪个解释器来执行这个脚本。在这个例子中,解释器是 "node"。 这个 shebang 是必需的,
//因为如果没有指定解释器,操作系统就不知道如何执行这个脚本。
// CommonJS (.cjs)
const { program } = require('commander');
const chalk = require('chalk');
const inquirer = require('inquirer'); //命令行交互
const ora = require('ora'); //loading效果
const figlet = require("figlet"); //打印大字体

//-----------------figlet的使用----------------

// figlet("Hello World!!", function (err, data) {
//     if (err) {
//       console.log("Something went wrong...");
//       console.dir(err);
//     }
//     console.log(data);
//   });

//-----------------ora的使用----------------

// //启动加载效果
// const spinner = ora('Loading unicorns').start();

// //1秒后加载效果变为Loading rainbows
// setTimeout(() => {
//   spinner.color = 'yellow';
//   spinner.text = 'Loading rainbows';
// }, 1000);
// //3秒后显示为加载成功或者失败
// setTimeout(() => {
//   // spinner.succeed('Loading rainbows succeed');
//   spinner.fail('Loading rainbows failed');
// }, 3000);

//-----------------inquirer的使用-----------------

// inquirer.prompt(questions, answers) -> promise 启动提示界面(查询会话)
// questions: array 包含Question 对象 
    // Question 对象包含以下属性:
    // type: 提示的类型,包括:input, number, confirm, list, rawlist, expand, checkbox, password, editor
    // name: 键名,用于获取用户输入的值
    // message: 提示信息
    // default: 默认值
    // choices: 选项,包含:type为list, rawlist, expand, checkbox时的选项
// answers: 默认值(对象){}
// inquirer.prompt([
//     {
//         type: 'input',
//         name: 'name',
//         message: '请输入你的姓名'
//     },
//     {
//         type: 'input',
//         name: 'age',
//         message: '请输入你的年龄'
//     }]).then(answers => {
//         console.log(answers);
//     }).catch(err => {
//         console.log(err);
//     })

// //-----------------chalk的使用-----------------
// console.log(chalk.blue('Hello world!'));
// console.log(chalk.blue.bgRed.bold('Hello world!'));
// console.log(chalk.blue('Hello', 'World!', 'Foo', 'bar', 'biz', 'baz'));
// console.log(chalk.red('Hello', chalk.underline.bgBlue('world') + '!'));

// // ----------commander的使用----------------

// // 通过这个选项可以修改帮助信息的首行提示 例如:Usage: my-command [options] [command]
// program
//   .name("zhihao-cli")
//   .usage("<command> [options]")

// //选项:
// //Commander 使用.option()方法来定义选项,同时可以附加选项的简介。
// //每个选项可以定义一个短选项名称(-后面接单个字符)和一个长选项名称(--后面接一个或多个单词),使用逗号、空格或|分隔。
// // 另一类选项则可以设置参数(使用尖括号声明在该选项后,如--expect <value>)。如果在命令行中不指定具体的选项及参数,
// //则会被定义为undefined。 <>表示必填参数,[]表示选填参数
// program
//   .option('-d, --debug', 'output extra debugging')
//   .option('-s, --small', 'small pizza size')
//   .option('-p, --pizza-type <type>', 'flavour of pizza');

// // 命令行:
// //Commander 使用.command()方法来定义命令行,同时可以附加命令行的简介。
// // .command()的第一个参数为命令名称,参数可为必选的(尖括号表示)、
// //可选的(方括号表示)或变长参数(点号表示,如果使用,只能是最后一个参数)。第二个参数为命令简介
// program
//   .command('clone <source> [destination]')
//   .description('clone a repository into a newly created directory')
//   .action((source, destination) => {
//     console.log('clone command called');
//   });
//   // 解析命令行参数。解析这些参数并将它们关联到程序中的对应操作 process.argv 是 Node.js 运行时环境中的全局变量
//   // 打印process.argv有2个参数,第一个是node的执行路径,第二个是当前执行脚本的路径,
//   program.parse(process.argv);

//   //它用于获取命令行中传递的所有参数和选项信息,并以一个对象的形式返回。
//   // 这个对象中的键名就是用户传递的选项名或参数名,而键值则是用户传递的选项值或参数值
// //   const options = program.opts();
// //   console.log(options);