如何用 Node.js 實現一個微型 CLI

什麼是 CLIjavascript

命令行界面(英語:command-line interface,縮寫:CLI)是在圖形用戶界面獲得普及以前使用最爲普遍的用戶界面,它一般不支持鼠標,用戶經過鍵盤輸入指令,計算機接收到指令後,予以執行。css

實現一個微型 CLI Demo

Node.js 官方示例:微型 CLIhtml


readline.createInterface

首先建立一個接口的實例,用於處理流信息,例:輸入、輸出、提示字符串、自動補全、歷史記錄等。vue

const rli = readline.createInterface({
  // 要監聽的可讀流。此選項是必需的。
  input: process.stdin,
  // 將逐行讀取數據寫入的可寫流。
  output: process.stdout
  // prompt // 要使用的提示字符串。默認值: '> '。
  // historySize //保留的最大歷史記錄行數。 要禁用歷史記錄,請將此值設置爲 0。
  // completer // 用於 Tab 自動補全的可選函數。
});複製代碼

建立完成後一個基本的 CLI 就已經有了。可是,僅僅是擁有了可以處理輸入輸出等流信息的能力而已。可是此時只可以輸入,不可以輸出,若是須要輸出能力則須要進一步進行完善。java


on line

若是須要根據輸入流的信息來反饋一些信息顯示(輸出流),則須要使用返回的實例來監聽輸入流的內容,而後進行相應的處理,再返回流信息用於輸出顯示。node

// on 函數是爲須要監聽的指令
// line 是能接受到當前命令行中的輸入流信息,經過函數回調的方式返回處理過的字符串。
rli.on('line', line => {
  const line2str = line.trim()

  if (line2str === '嗨') { // 在命令行中輸入 「嗨」 並回車,CLI 則會輸出一個 「Hi!」
    console.log('Hi!')
  }
  if (line2str === '你好嗎') { // 沒錯是個英語老方了
    console.log('I\'m fine, thank you, and you?')
  }
})複製代碼

經過監聽輸入的行信息加以處理的邏輯,最後返回一個輸出信息就實現了簡單的輸入輸出互動效果。git

至此,一個大概的互動式的 CLI 核心部分就已經完成了。github


啓動 CLI

如需使用 npm 命令的話則須要在 package.json 中 scripts 里加入你的命令名稱和腳本位置。npm

"scripts": {
  // 其餘命令……
  "cli": "node build/index.js" // 新增的 npm 命令,經過 npm 命令能夠啓動 cli 腳本。
},
// 這時候就能夠經過 npm run cli 命令運行 CLI 了。複製代碼

腳本位置的話不能直接使用 ./filePath 或 /filePath 這樣的路徑會沒法識別。須要使用 node filePath/xxx.js,這樣 node 就會將腳本位置定位至當前項目開始尋找。json


退出 CLI

當全部輸入完成後或者達到特定條件就能夠退出 CLI 模式了。

if (line2str === '再見') {
  console.log('Bye!')
  process.exit(0); // 退出 CLI 模式
}複製代碼

經過 process.exit 就能夠實現退出當前的 CLI 模式返回到命令行中。

process 在接下來的內容中還會使用到,可是能夠先看如下 NodeJs 對他的定義:

process 對象是一個全局變量,它提供有關當前 Node.js 進程的信息並對其進行控制。做爲一個全局變量,它始終可供 Node.js 應用程序使用,無需使用 require()。 它也可使用 require() 顯式地訪問


實現一個簡單的問答式 CLI

什麼狀況會須要用到 CLI 功能呢?咱們能夠假設一個這樣的場景:你在寫 Vue 的時候是否是會重複的新建 xxx.vue 文件呢?這時候就可使用 CLI 生成了。固然你會說:「我能夠 copy & paste 啊!」。你固然能夠,可是每次 copy & paste 完了你又要把裏面的代碼手動刪除掉,不以爲很麻煩嗎?這時候一條命令加上簡單的輸入就能夠生成乾淨的 xxx.vue 模板,甚至附帶的 xxx.js、xxx.css 也能夠一併生成豈不是更有效率?

「我就喜歡 copy & paste!」。好了兄弟,你坐下,當我沒說。

下面咱們繼續來分析一下實現這樣的一個 CLI 須要考慮哪些因素

問題

「一個問答式的 CLI 固然須要問題啦,這不是廢話嘛。」

話是沒錯,可是問題如何問固然也有一點點的講究。那就是問題必定會是封閉式的問題,封閉式問題由於提得比較具體且圈定的範圍固定,也就要求了回答者必須在這個範圍內給予明確的回答。

若是是開放式問題的話,那麼就會致使回答者(使用者)就有很大的自我發揮空間。由於問題過於開放籠統的話,那麼答案就沒有固定範圍了,這時候你的問題也就是無效提問了。


答案

這個沒必要過多解釋,既然是封閉式問題那就只有一些固定的選項,以及再照顧一下默認選項便可。

例如:代碼文件類型?【JS/ts/vue/css】

其中大寫/加粗通常爲默認類型,即回車即選擇。


生成路徑

全部問題都回答、選擇完成後文件的生成路徑,通常來講必須默認一個生成路徑以及提供自定義填寫符合文件夾規則的路徑。固然也能夠將其作成半開閉形式,即有固定幾種選擇也能夠自定義填寫符合文件夾規則的路徑。

例如:指定文件路徑?【SRC/components/assets/yourpath】


生成的模板

經過答案得知須要生成的是哪一種類型的文件或者是某一類文件或某一種文件組合生成多個文件。


反饋結果

當全部回答都完成時,須要及時反饋、顯示一些重要的步驟或信息,讓使用者直觀的知道進程如何,以及最終結果。

上面將一些所考慮的因素都說完了,這裏就開始進入代碼的實際編碼和設計部分了。這部分開始會以我本身的項目爲例來講明。


問題 & 答案的設計

首先須要一個問題列表:

// 構建問題列表
const buildQuestion = () => {
  // 問題文字內容、提示、類型
  const questionText = [
    {question: '組件名稱?', tips: '', type: '[template]'},
    {question: '指定文件夾路徑?', tips: '(最大深度:4)', type: '[./views/]'},
    {question: '代碼文件類型?', tips: '', type: '[JS/ts]'},
    {question: '樣式表類型?', tips: '', type: '[CSS/less/sass/scss]'},
    // {question: '是否建立單獨的Api文件?', tips: '', type: '[y/N]'} // 暫時未想好該如何處理 API 文件的構建和寫入
  ];

  return questionText.map(item => {
    return { text: item.question, question: `\x1B[32m?\x1B[97m ${item.question}${item.tips}\x1B[32m${item.type}` }
  });
}複製代碼

你能夠設計你本身的問題列表來決定須要生成的是那些內容/代碼。

以及一個無效回答的默認值和一個記錄回答對象:

// 無有效輸入時使用的默認內容
const defAnswer = {
  fileName: 'template',
  filePath: './views/',
  codeType: 'js',
  cssType: 'css',
  fileApi: false,
};

// 記錄問題的回答內容
const answer = {
  fileName: '',
  filePath: '',
  codeType: '',
  cssType: '',
  fileApi: false,
};複製代碼

當用戶輸入了答案後咱們就須要去檢查這個答案是否符合規則或者有效:

// 檢查是否符合規則,並處理答案默認選項
const checkAnswer = (step, content) => {
  // if (step > 1) { content = content.toLowerCase() }
  switch (step) {
    case 0:
      return answer.fileName = /^[a-zA-Z]{1,20}$/g.test(content)
        ? content : defAnswer.fileName;
    case 1:
      return answer.filePath = path.join(
        findChatIndex(
          path.join(defAnswer.filePath, content), '\\', 3),
        answer.fileName);
    case 2:
      content = content.toLowerCase()
      return answer.codeType = /^js|ts$/ig.test(content)
        ? content : 'js';
    case 3:
      content = content.toLowerCase()
      return answer.cssType = /^css|less|sass|scss$/ig.test(content)
        ? content : 'css';
    case 4:
      if (/^y|Y|n|N$/ig.test(content)) {
        const tempYN = content.toLowerCase()
        answer.fileApi = tempYN === 'y' ? true : false
        return content
      } else {
        answer.fileApi = false
        return 'N'
      }
  };
};複製代碼


處理路徑

針對用戶自定輸入路徑時的處理,以及還要考慮不一樣操做系統路徑分割符不一致的狀況。

// 拼接路徑
let findChatIndex = (str, chat, num) => {
  if (str.match(/\\/g).length <= num) return str;

  let chatIndex = str.indexOf(chat);
  for (let index = 0; index < num; index++) {
    let tempIndex = str.indexOf(chat, chatIndex + 1);
    if (tempIndex !== -1) {
      chatIndex = tempIndex
    }
  }
  return str.substr(0, chatIndex);
};複製代碼


處理模板

這裏我就不貼代碼了,由於我是使用了字符串模板來做爲模板的輸出內容,由於方便且字符串模板能夠保存格式(縮進和換行)

參考這裏:template.js

到這就完了?不,到這只是完成了考慮因素的代碼實現部分,還有一些是須要咱們繼續完善的,例如輸入輸出的處理,顯示、反饋處理等


輸入輸出的設計

通常來講在進入一個獨立的 CLI 模式以前會對控制檯以前的內容進行一個簡單的清理:

readline.cursorTo(process.stdout, 0, 0); // 光標位置 0,0 即第一行第一位
readline.clearScreenDown(process.stdout); // 清理屏幕內容複製代碼

這一步,簡單來講就有很好,清理以後沒有其餘無關信息。固然沒有的話也無傷大雅,屬於錦上添花的部分,看須要來吧。

而後就是開始初始化第一個問題:

// 初始化第一個問題。
console.log(questionList[stepQuestion].question); // 問題一
// 設定輸入內容樣式
console.log('\x1B[36m'); // 控制檯字符樣式
複製代碼

固然你也能夠寫多一點東西,好比輸出一段簡介或者輸出一些其餘本身喜歡的內容。

接下來就是比較重要的問題和答案處理部分了,監聽 line 輸入天然是不用多說。

// on 函數是爲須要監聽的指令
// line 是能接受到當前命令行中的輸入流信息,經過函數回調的方式返回處理過的字符串。
  const line2str = line.trim()

  // 將檢查處理後的答案信息存儲用於後續命令行內容輸出
  let tempAnswer = checkAnswer(stepQuestion, line2str);

  // 將光標移入上一次步驟的位置,能夠形成用戶已經選擇完成的效果。
  readline.cursorTo(process.stdout, 0, stepQuestion);
  // 清理以前的輸入內容。
  readline.clearScreenDown(process.stdout);
  // 選擇完成後輸出選擇後的結果信息。
  console.log(`\x1B[32m?\x1B[97m ${questionList[stepQuestion].text}\x1B[36m%s`, tempAnswer);

  // 重置控制檯樣式。
  console.log('\x1B[0m');

// 若是當問題的步驟小於問題的長度時,則問題步長 + 1。
  if (stepQuestion < lenQuestion) {
    stepQuestion++;
    // 輸出下一個問題內容
    console.log(questionList[stepQuestion].question);
  } else { …… }複製代碼

這裏能夠看出我使用的是記錄步長的方式來處理何時開始下一個問題的提問與答案的記錄。

以前也考慮過使用遞歸,可是最終實現起來處理提問與答案的記錄稍微麻煩,固然你也能夠嘗試。

else 部分呢就是處理全部答案都回答完成的狀況了:

else {
    tpl.bulidTpl(answer)
    .then(() => {
      // 不然能夠認爲已經選擇完成
      // console.log('再見! %o', answer);
      console.log('再見!');
      console.log('\x1B[0m');
      process.exit(0);
    })
    .catch(err => {
      console.log(`\x1B[31m${err.message}`);
      console.log(`\x1B[31m${err.error}`);
      console.log('\x1B[0m');
      process.exit(0);
    });
  }複製代碼

最後最後仍是須要額外考慮一下意外觸發 CLI 任務中斷的狀況:

rli.on('line', line => {})
.on('close', () => {
  console.log('\x1B[0m');
  console.log('【信息】您已中斷模板建立任務,感謝您的使用再見!');
  process.exit(0);
});
複製代碼


能看到到這裏呢也就說明了,這個 微型的問答式的 CLI 也就完成了。


最終效果


最後

固然這個只是一個簡單的 CLI 實現而已,關於這個 CLI 我本身也還有一些想法,由於這裏面仍是有一些能夠改進和優化的地方,例如如今是隻能生成 Vue 這一套單一的文件模板,哪能不能生成其餘框架的文件模板呢?或者是能夠經過配置文件的方式生成的是一整套項目結構呢?又或者是代碼模板能不能使用代碼的方式而不是字符串模板生成代碼模板呢?

這些也都是我本身須要考慮和更深刻學習瞭解的地方。

各位小夥伴可能也會有本身的想法能夠創造不少有趣、好玩的 CLI。固然也祝各位小夥伴可以學到有用的知識,而後把這些知識轉變成代碼而後創造生產力工具爲本身和公司、企業、社區增磚添瓦。


GitHub:Template Build




版權聲明:

本文版權屬於做者 林小帥,未經受權不得轉載及二次修改。

轉載或合做請在下方留言及聯繫方式。

相關文章
相關標籤/搜索