玩轉Node.js-CLI開發


Node.js能做什么?

開發方向

GUI - Graphical User Interface : 圖形用戶界面 office、vscode、瀏覽器、播放器……

CLI - Command-Line Interface:命令行界面,也稱為 CUI,字符用戶界面;雖然沒有GUI操作直觀,但是CLI更加節省計算機資源(所以一般用於服務器環境)babel、tsc / webpack / vue-cli

Server - 服務提供(Web Server、IM……)

這篇博客主要分享一點點使用node.js開發cli工具的知識。

前端文件自動合並

前端工程中的文件熱更新,CSS、JS代碼自動壓縮合並的原理其實就是通過監聽文件變動,然后進行一系列的操作。

const fs = require('fs');
const filedir = './source';
fs.watch(filedir, function (ev, file) {
  // console.log(ev + '/' + file); // 這里不需要判斷file是否有內容

  // 只要有一個文件發生了變化,我們就需要對這個文件夾下的所有文件進行讀取然后合並
  fs.readdir(filedir, (err, dataList) => {
    let arr = [];
    console.log("dataList: ", dataList);
    dataList.forEach((f) => {
      if (!f) return;
      let info = fs.statSync(filedir + '/' + f)
      if (info.mode === 33206) {
        arr.push(filedir + '/' + f)
      }
    })

    // 讀取數組中的文件內容並且合並
    let content = "";
    arr.forEach((item) => {
      let c = fs.readFileSync(item);
      content += c.toString() + "\n";
    })
    fs.writeFile('./demo/js/index.js', content, function (e) {
      console.log(e);
    });
  })
})

其實node.js可以寫很多類似的工具,下面我們就來學習下如何編寫一個cli。

CLI介紹

CLI就是我們常用的命令行界面中的一些工具,比如vue-cli就是一個典型的例子,我們可以通過vue create app命令創建一個名稱為app的項目。

command [subCommand] [options] [arguments]
command:命令,比如 vue
[subCommand]:子命令,比如 vue create
[options]:選項,配置,同一個命令不同選項會有不一樣的操作結果,比如 vue -h,vue -v
[arguments]:參數,某些命令需要使用的值,比如 vue create myApp
選項與參數的區別:選項是命令內置實現,用戶進行選擇,參數一般是用戶決定傳入的值

選項一般會有全拼與簡寫形式(具體看使用的命令幫助),比如 --version = -v
全拼:以 -- 開頭 / 簡寫:以 - 開頭
選項也可以接受值,值寫在選項之后,通過空格分隔
多個簡寫的選項可以連寫,開頭使用一個 - 即可,需要注意的是,如果有接受值的選項需要放在最后,比如:
vue create -d -r <-r的值> myApp
vue create -dr <-r的值> myApp

開發CLI的第三方框架

1.commander

命令行開發工具,TJ大神出品,官網

安裝npm install commander

demo1

const commander = require('commander');

// 設置當前命令的版本
commander.version("v1.0.0", '-v, --version');
/**
 * 設置其他option,--name 后面的 [val] 是當前這個選項的參數值
 * []表示可選,<>表示必填
 * 如果第三個參數是一個函數的話,那么該函數會接收來自用戶輸入的值並返回最后一個值最為這個參數實際的值
 */
// commander.option('-s, --setname [val]', '設置名稱', (val)=>{
//   console.log(val);
// })

// commander.option('-s, --setname <val>', '設置名稱', (val)=>{
//   console.log(val);
// })

commander.option('-s --setname [val]', '設置名稱', '我是默認值')

commander.command('create');

// 設置命令的動作
commander.action(() => {
  // 這里的setname其實是option中設置的完整命令變量名,用戶輸入命令之后,commander會自動掛載
  console.log("Hello " + commander.setname);
})

// 解析來自process.argv上的數據,commander會自動幫助我們添加一個 -h 的解析
commander.parse(process.argv);

demo2

const commander = require('commander');
const fs = require('fs');

commander.version('v1.0.0', '-v, --version');

commander.option('-s --setname [val]', '設置名稱', '');

commander.command('create <app-name>')
.description('創建項目')
.alias('c')
.usage('使用說明')
.action(appName=>{
  console.log("項目名稱: ", appName);
  // 使用fs.existsSync API判斷文件夾是否存在
  if(fs.existsSync(appName)) {
    return console.log("項目已經存在!");
  }
  fs.mkdirSync(appName)
  console.log("[+] project init success!");
})

commander.parse( process.argv );

.parse(argv: string[])
解析執行傳入的 argv 命令字符串,通常該命令字符串來自用戶在命令行的輸入,process.argv;
commander 同時會默認創建一個 -h, --help 的選項。

.version(str, flags?)

設置版本信息,該方法會自動為命令注冊一個 -V, --version 的 option;
str:版本號
flags:指定的 option,默認為:" -V, --version "

.option(flags, description?, fn?, defaultValue?)
設置命令選項
flags:選項標記名稱,”-v, --version”
description:選項使用說明
fn:默認值,函數返回值為defaultValue,優先級高於defaultValue
defaultValue:選項默認值,如果需要的話

選項屬性
flags 中的格式可以接收參數
-n, --setname[val]
-n, --setname
[] 可選
<> 必填
設置成功以后,會在命令對象下增加一個與全局的同名的屬性

.action(fn)
指定命令要執行的動作行為
該函數執行過程會接收到至少一個參數
如果命令中帶有參數,則是對應的參數列表
參數的最后一個永遠都是 commander 實例

.command(name, desc?, opts?)
子命令
name:命令的名稱,也可以接受值 'create [appName]'
desc:簡介
opts:配置

.description(str)
命令描述
.alias(str)
設置命令別名
.usage(str)
設置或獲取當前命令的使用說明

ls案例

/**
 * ls
 * 輸出當前運行命令所在的目錄下的文件和文件夾
 * ls d:\
 * 我們還可以指定要顯示的目錄
 */

// 加載commander和fs模塊
const commander = require('commander');
const fs = require('fs');

// 設置當前命令工具的版本
commander.version('v1.0.0', '-v, --version');

// 設置命令選項 默認值是當前目錄
commander.option('-p, --path [path]', '設置要顯示的目錄', __dirname);

// 以列表的形式顯示,如果選項不接受用戶輸入的值,那么這個選項將以boolean的形式提供給后面命令使用
commander.option('-l, --list', '以列表的形式顯示');

// 實現命令的具體邏輯
commander.action(()=>{
  // option中的變量會掛載到當前commander
  // console.log(commander.path);
  // console.log(commander.list);
  try {
    // 讀取用戶輸入的目錄
    const files = fs.readdirSync(commander.path);
    if(commander.list) {
      // 用戶輸入了-l,以列表的方式展示
      let output = files.map(item=>{
        // 文件的拓展信息,除了文件內容以外的信息
        let stat = fs.statSync(commander.path + '/' + item);
        // 根據isDirectory()顯示不同的文件類型
        let type = stat.isDirectory() ? '目錄' : '文件';
        return `[${type}]   ${item}\r\n`;
      }).join('');
      console.log(output);
    }else {
      console.log(files);
    }
  } catch (error) {
    console.log(error);
  }
})

commander.parse(process.argv);

使用方式1:什么都不傳默認列出當前目錄文件夾和文件,-p代表路徑,-l代表是否以列表方式顯示

使用方式2:傳入指定路徑

2.chalk

node中命令行樣式風格控制器,官網

安裝:npm install chalk

使用方式:

const chalk = require('chalk')得到一個 chalk 對象,通過這個對象,我們就可以給控制台中的文字加上各種樣式了,就像css一樣。

語法:

chalk.<style>[.<style>...](string, [string...])

Styles
Modifiers 文字修飾:
bold Colors 文字顏色:red、green、yellow、blue、cyan
Background colors 背景顏色:bgRed、bgGreen、bgYellow、bgBlue、bgCyan

Colors
.hex('#DEADED')
.keyword('orange')
.rgb(15, 100, 204)

Background colors
.hex('#DEADED')
.keyword('orange')
.rgb(15, 100, 204)

添加了顏色的ls案例

const fs = require("fs");
const commander = require("commander");
// 引入美化命令行模塊
const chalk = require("chalk");

// 設置當前cli的版本
commander.version('v1.0.0', '-v, --version');

// 設置命令選項,默認值是當前目錄
commander.option('-p, --path [path]', '設置要顯示的目錄', __dirname);

// 以列表的形式顯示
commander.option('-l, --list', '以列表的形式顯示');

// 編寫命令具體邏輯
commander.action(()=>{
  try {
    // 顯示用戶輸入的路徑下的所有文件和目錄
    const files = fs.readdirSync(commander.path);
    if(commander.list) {
      let output = files.map(item=>{
        let stat = fs.statSync(commander.path + '/' + item);
        return stat.isDirectory() ? chalk.greenBright.bgBlack.bold(`[目錄]   ${item}\r\n`) : `[文件]   ${item}\r\n`;
      }).join('');
      console.log(output);
    }else {
      console.log(files);
    }
  } catch (error) {
    console.log(error)
  }
})

commander.parse(process.argv);

使用:

3.inquirer

交互式命令,提問用戶,收集用戶輸入數據,官網

安裝:npm install inquirer

使用:

require('inquirer')

inquirer.prompt(questions).then(answers=>{
			...
		})

questions
type:提問類型,input, confirm, list, rawlist, expand, checkbox, password, editor
name:問題名稱,供程序后續使用
message:問題文字,給用戶看的
default:默認值
choices:選項
validate:輸入驗證
filter:數據過濾

input
提出問題,用戶輸入答案
可用選項:type, name, message[, default, filter, validate, transformer]

confirm
提出選擇,用戶選擇 Y or N
可用選項:type, name, message, [default]
default如果提供,必須是 boolean 類型

list
單選
可用選項:type, name, message, choices[, default, filter]
choices為一個數組,數組中可以是簡單的字符串,也可以是一個包含了name和value屬性的對象
默認選中項為數組中某條數據的下標,通過default設置

rawlist
單選
可用選項:type, name, message, choices[, default, filter]
choices為一個數組,數組中可以是簡單的字符串,也可以是一個包含了name和value屬性的對象
通過數字進行選擇

checkbox
多選
可用選項:type, name, message, choices[, filter, validate, default]
choices 為一個對象數組,對象中 checked 屬性 為 true 的表示默認選中項

validate方法
對用戶輸入或選擇的內容進行驗證,返回boolean值,確定提問是否繼續
可以返回字符串作為驗證失敗的提示

filter方法
對用戶輸入或選擇的內容進行過濾
接受一個參數:用戶輸入或選擇的內容
返回的值將作為過濾后的值

demo

const inquirer = require('inquirer');

// 提問用戶,與用戶進行命令行的交互
// prompt數組中存放一個指定格式的對象,我們稱之為question對象
inquirer.prompt([{
    type: 'input',
    name: 'username',
    message: 'please input your project name',
    default: 'app',
    // 對用戶輸入的數據或選擇的數據進行驗證
    validate(val) {
      if (val.trim() === "") {
        return 'project name can not be empty'
      }
      return true;
    },
    // 對用戶輸入的數據或選擇的數據進行過濾
    filter(val) {
      return val.toLowerCase();
    }
  },
  {
    type: 'confirm',
    name: 'useEs6',
    message: '是否啟用ES6支持',
    default: true
  },
  /*   {
      type: 'list',
      name: 'framework',
      message: '請選擇后端框架',
      choices: ['Express.js', 'Koa2.js', 'Egg.js'],
      default: 0
    }, */
  {
    type: 'rawlist',
    name: 'framework2',
    message: '請選擇前端框架',
    choices: ['Vue', 'React', 'Angular'],
    default: 1
  },
  {
    type: 'checkbox',
    name: 'tools',
    message: '開發工具',
    choices: [{
        name: '使用ESLint',
        value: 'eslint',
        checked: true
      },
      {
        name: '使用mocha單元測試',
        value: 'mocha'
      }
    ]
  }
]).then(res => {
  console.log(res);
})

效果演示:

DJp6sK.md.gif

去除node

如果不想每次都輸入node inquirer.js運行腳本,可以創建一個bat文件,然后將下面代碼寫入,下次直接運行這個bat腳本文件即可。下面的代碼我們只需要關心文件名即可。

@IF EXIST "%~dp0\node.exe" (
  "%~dp0\node.exe"  "%~dp0\inquirer.js" %*
) ELSE (
  @SETLOCAL
  @SET PATHEXT=%PATHEXT:;.JS;=;%
  node  "%~dp0\inquirer.js" %*
)

個人問卷調查Demo

/**
 * 個人問卷調查
 * 功能:
 *    1.姓名、用戶輸入、必填
 *    2.性別、用戶選擇、男女、默認男
 *    3.年齡、用戶輸入、必填
 *    4.手機號、用戶輸入、必填、作校驗,必須符合正常的手機號格式
 *    5.選擇你最熟悉的框架、[Vue,React,Angular]
 *    7.將信息寫入腳本同目錄下的log.txt
 */

const commander = require('commander');
const fs = require('fs');
const inquirer = require('inquirer');
const chalk = require('chalk');

inquirer.prompt([{
    type: 'input',
    name: 'username',
    message: '請輸入你的姓名,請確保英文字符為小寫',
    // 對用戶輸入的數據進行驗證
    validate(val) {
      if (val.trim() === "") {
        return '姓名不能為空!'
      }
      return true; // 代表驗證通過
    },
    filter(val) {
      return val.toLowerCase();
    }
  },
  {
    type: 'rawlist',
    name: 'gender',
    message: '請選擇你的性別',
    choices: ['男', '女'],
    default: 0
  },
  {
    type: 'input',
    name: 'age',
    message: '請輸入年齡',
    validate(val) {
      if (val.trim() === "") {
        return '年齡不能為空'
      }
      return true;
    }
  },
  {
    type: 'input',
    name: '手機號',
    message: '請輸入手機號',
    validate(val) {
      console.log("phone number: ", val);
      let re = new RegExp(/^(13[0-9]|14[5|7]|15[0|1|2|3|4|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$/);
      if (!re.test(val)) {
        return '請輸入合法的手機號!'
      }
      return true;
    }
  },
  {
    type: 'list',
    name: 'framework',
    message: '請選擇你最熟悉的框架',
    choices: ['Vue', 'React', 'Angular'],
    default: 1
  },
  {
    type: 'confirm',
    name: 'saveFile',
    message: '是否保存信息到本地',
    default: true
  },
]).then(res => {
  console.log(res);
  if (res.saveFile) {
    try {
      fs.writeFileSync(__dirname + '/logs/question' + Math.ceil(Math.random() * 100000000) + 'log.txt', JSON.stringify(res));
    } catch (error) {
      console.log(error)
    }
  }
})

CLI補充

commander 多個參數獲取

/**
 * commander.js commander多個參數獲取
 */

 const commander = require('commander');

 const subCommand = commander.command('create <a> <b> <c>');


 // 在action的回調函數的列表中參數列表就是command定義的參數
 // option就是一個參數(選項)option('-p', --path <path>)
 subCommand.action((a,b,c)=>{
   console.log(a,b,c);
 })

 commander.parse(process.argv);

Global CLI

配置package.json,關鍵是添加了bin這個關鍵字,bin的鍵就是包名,值就是main字段配置的的入口主文件。

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

index.js

#!/usr/bin/env node
console.log("Hello, my cli");

!/usr/bin/env node表示當前文件需要以node腳本執行

發布包到npm,然后別人全局安裝你的包就可以使用了。

全局安裝包的路徑:C:\Users\replaceroot\AppData\Roaming\npm


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM