说在前面

我们在用脚手架初始化项目的时候,往往会进行一些命令交互,用过vue或者react的用脚手架新建项目的应该都进行过命令交互,vue创建的时候会让你选择vue2还是vue3,也有多选要什么配置,也有输入y或者n选择是否用history路由等,这些简单的交互其实用inquire这个包都能实现,但是最近自己在做一个小工具的时候,想要进行文件和文件夹的选择,这时我发现inquire里并没有这个交互功能,所以便自己尝试去在inquire这个库的基础上实现文件选择和文件夹选择这两种类型的交互。

插件效果

通过该插件,我们可以在控制台通过方向键来选择文件和文件夹,具体效果如下:
2022-07-29-21-02-56 00_00_00-00_00_30.gif

插件实现

Inquirer.js

Inquirer.js试图为NodeJs做一个可嵌入式的美观的命令行界面。如下图:

image.png

inquirer原有参数

  • type

    表示提问的类型,包括:input、confirm、 list、rawlist、expand、checkbox、password、editor。
  • name

    存储当前输入的值。
  • message

    问题的描述。
  • default

    默认值。
  • choices

    列表选项,在某些type下可用,并且包含一个分隔符(separator);
  • validate

    对用户的回答进行校验。
  • filter

    对用户的回答进行过滤处理,返回处理后的值。
  • when

    根据前面问题的回答,判断当前问题是否需要被回答。
  • pageSize

    修改某些type类型下的渲染行数。
  • prefix

    修改message默认前缀。
  • suffix

    修改message默认后缀。

    二次封装

    基于inquirer原有功能及参数,增加一些扩展功能及参数

    新增参数

  • notNull

    是否不能为空,默认为false,设置为true后该参数不能输入空,并且会有不能为空的提示,必须输入字符后才可以回车确认并进行下一步,如下图:
    {
      type:"input",
      message:"请输入你的姓名:",
      name:"name",
      notNull:true
    }

    image.png

  • type

    在原有类型中新增两种类型:file、folder,分别为文件选择器和目录选择器,效果如下图:
    {
      type:"file",
      message:"请选择文件:",
      name:"fileName",
      default:"",
    },
    {
      type:"folder",
      message:"请选择文件夹:",
      name:"folderName",
      default:"",
      pathType:'absolute'
    },

image.png

  • pathType

    此项为新增配置,设置目录和文件选择器选中路径输出的格式,默认为相对路径,可以设置为absolute,此时会输出绝对路径,效果如下图:
    {
      type:"file",
      message:"请选择文件:",
      name:"fileName",
      default:"",
    },
    {
      type:"folder",
      message:"请选择文件夹:",
      name:"folderName",
      default:"",
      pathType:'absolute'
    },

image.png

代码实现

获取指定路径下的文件列表

使用fs中的readdirSync方法可以获取指定目录下的文件列表,具体代码如下:

getFileList = (dirPath)=>{
    const list = fs.readdirSync(dirPath);
    return ['../(返回上一级)',...list];
}
获取指定路径下的目录列表

使用fs中的readdirSync方法可以获取指定目录下的文件列表,通过isDirectory方法可以判断文件是否为目录文件,具体代码如下:

getFolderList = (dirPath)=>{
    const list = fs.readdirSync(dirPath);
    let resList = [];
    list.map(item=>{
        const fullPath = path.join(dirPath,item);
        if(fs.statSync(fullPath).isDirectory()){
            resList.push(item + '(进入文件夹)');
            resList.push(item + '(选择文件夹)');
        }
    });
    return ['../(返回上一级)',...resList];
}
交互类型响应控制

新增的filefolder类型使用自己重新封装的方法,其他依旧使用Inquirer中的响应方法,具体代码如下:

run(option){
    if(option.type === 'file'){
        return this.chooseFile(option);
    }else if(option.type === 'folder'){
        return this.chooseFolder(option);
    }else{
        if(option.notNull){
            const flag = option.message.slice(-1);
            if([":",":"].includes(flag)){
                option.message = option.message.slice(0,-1) + '(不能为空)' + flag;
            }
        }
        return this.defaultType(option);
    }
}
选择文件
  • 选择的为返回上一级,则将当前目录回退一级:

    this.clear(2);
    return this.chooseFile(option,path.join(dirPath,'/../'));
  • 选择的是目录则进入选择的目录:

    return path.join(dirPath, answer[option.name]);
  • 选择的是文件则返回选择的文件路径并结束操作:

    this.clear(2);
    return this.chooseFile(option,fullPath);
  • 完整代码如下

    chooseFile(option,dirPath = './'){
      option.type = 'list';
      option.suffix = "(当前浏览目录:" + path.join(__dirname,dirPath) + ')';
      option.pageSize = fs.readdirSync('./').length + 1;
      option.choices = [...this.getFileList(dirPath)];
      const answer = await inquirer.prompt([
          option
      ]);
      if(answer[option.name] == '../(返回上一级)'){
          this.clear(2);
          return this.chooseFile(option,path.join(dirPath,'/../'));
      }else{
          const fullPath = path.join(dirPath, answer[option.name]);
          if(!fs.statSync(fullPath).isFile()){
              this.clear(2);
              return this.chooseFile(option,fullPath);
          }else{
              return path.join(dirPath, answer[option.name]);
          }
      }
    }
    选择目录

    如下图,这里使用后缀说明来区分选择文件夹和进入文件夹:
    image.png

  • 选择的为返回上一级,则将当前目录回退一级:

    this.clear(2);
    return this.chooseFile(option,path.join(dirPath,'/../'));
  • 选择的是进入文件夹,则进入该目录,这里需要将加入用于区分的后缀去掉再返回:

    return path.join(dirPath, answer[option.name].slice(0,-7));
  • 选择的是选择文件夹则返回选择的文件夹路径并结束操作:

    this.clear(2);
    return this.chooseFile(option,fullPath);
  • 完整代码如下

    chooseFile(option,dirPath = './'){
      option.type = 'list';
      option.suffix = "(当前浏览目录:" + path.join(__dirname,dirPath) + ')';
      option.pageSize = fs.readdirSync('./').length + 1;
      option.choices = [...this.getFileList(dirPath)];
      const answer = await inquirer.prompt([
          option
      ]);
      if(answer[option.name] == '../(返回上一级)'){
          this.clear(2);
          return this.chooseFile(option,path.join(dirPath,'/../'));
      }else{
          const fullPath = path.join(dirPath, answer[option.name]);
          if(!fs.statSync(fullPath).isFile()){
              this.clear(2);
              return this.chooseFile(option,fullPath);
          }else{
              return path.join(dirPath, answer[option.name]);
          }
      }
    }
    基本类型调用Inquirer处理

    这里增加了notNull(是否不能为空)的参数,代码如下:

    defaultType(option){
      const answer = await inquirer.prompt([
          option
      ]);
      if(option.notNull && answer[option.name] === ''){
          this.clear(2);
          return this.defaultType(option);
      }
      return answer[option.name];
    }

    插件使用

    1、安装依赖

    npm install @jyeontu/j-inquirer

    2、在代码中引用

    const JInquirer = require('@jyeontu/j-inquirer');

    3、示例代码

    const JInquirer = require('@jyeontu/j-inquirer');
    let options = [
      {
          type:"input",
          message:"请输入你的姓名:",
          name:"name",
          notNull:true
      },{
          type:"input",
          message:"请输入你的年龄:",
          name:"age",
          default:18,
          validate:(val)=>{
              if(val < 0 || val > 150){
                  return "请输入0~150之间的数字";
              }
              return true;
          }
      },{
          type:"file",
          message:"请选择文件:",
          name:"fileName",
          default:"",
      },{
          type:"folder",
          message:"请选择文件夹:",
          name:"folderName",
          default:"",
          pathType:'absolute'
      },{
          type:"list",
          message:"请选择你喜欢的水果:",
          name:"fruit",
          default:"Apple",
          choices:[
              "Apple",
              "pear",
              "Banana"
          ],
      },{
          type:"expand",
          message:"请选择一个颜色:",
          name:"color",
          default:"red",
          choices:[
              {
                  key : 'R',
                  value : "red"
              },
              {
                  key : 'B',
                  value : "blue"
              },
              {
                  key : 'G',
                  value : "green"
              }
          ]
      },{
          type:"checkbox",
          message:"选择一至多种颜色:",
          name:"color2",
          choices:[
              "red",
              "blue",
              "green",
              "pink",
              "orange"
          ]
      },{
          type:"password",
          message:"请输入你的密码:",
          name:"pwd"
      },{
          type:"editor",
          message:"写下你想写的东西:",
          name:"editor"
      }
    ];
    let j = new JInquirer(options);
    let res = j.prompt().then(res=>{
      console.log(res);
    });

    源码地址

    https://gitee.com/zheng_yongtao/node-scripting-tool/tree/master/src/JInquirer

    觉得有帮助的同学可以帮忙给我点个star,感激不尽~~~\
    有什么想法或者改良可以给我提个pr,十分欢迎~~~\
    有什么问题都可以在评论告诉我~~~

往期精彩

node封装一个控制台进度条插件

vue实现一个鼠标滑动预览视频封面组件

密码太多不知道怎么记录?不如自己写个密码箱小程序

微信小程序实现一个手势图案锁组件

vue封装一个图案手势锁组件

vue封装一个弹幕组件

为了学(mo)习(yu),我竟开发了这样一个插件

程序员的浪漫之——情侣日常小程序

vue简单实现词云图组件

vue + echarts实现中国地图省份下钻联动

使用学过的算法做个游戏很酷的好吗

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号(前端也能这么有趣)发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。

本文由mdnice多平台发布


JYeontu
15 声望0 粉丝