cli原理解析

SheldonLaw發表於2019-04-15

vue create my-vue-project

用過Vue的同學基本都接觸過vue腳手架:vue-cli,通過上面那行命令,回答幾個問題,我們就能輕鬆建立出一個Vue專案。因為團隊工程化需要,我最近開始學習cli,打算用作團隊腳手架或收攏個人常用命令(如按照格式提交程式碼)。

vue-cli的原理網上已經有很多不錯的分析,而本文將從基礎知識學習開始,一步步進階到實際應用。希望能幫助大家在學會使用工具的同時,瞭解背後的原理。養成“打破砂鍋問到底”的學習習慣。

目錄

  1. 命令執行過程
  2. npm包打造cli的原理
  3. commander原理簡析
  4. vue-cli:create命令執行過程
  5. 總結

一,命令執行過程

當你在控制檯敲下npm -v並回車的時候,到底發生了什麼?

我們先來看看npm這個命令在哪。通過執行which npm(不瞭解which命令的同學可以參考這裡:linux命令之which),可以看到,npm命令的可執行檔案在/usr/local/bin/npm,開啟資料夾一看,是個替身,右鍵“顯示原身”(Mac使用者的方法,其他使用者請搜尋“軟連結”),就能定位到該命令在哪:/usr/local/lib/node_modules/npm/bin/npm-cli.js,看到是js程式碼相信大家已經鬆了一口氣,我們先不急著看npm-cli.js裡面的源,我們先思考一個問題:

Q1:系統是怎麼找到npm這個這個命令的可執行檔案的?

A1:瞭解which命令的同學都知道,which命令會按照PATH變數中的路徑順序來查詢可執行檔案。執行echo $PATH可以列印出該變數內容:/usr/local/bin:/usr/bin:/bin(例如這是我的部分內容,目錄間用‘:’分隔),所以系統會先在/usr/local/bin下面找npm執行檔案,/usr/local/bin/npm連結到/usr/local/lib/node_modules/npm/bin/npm-cli.js。所以呼叫npm命令相當於執行npm-cli.js

總的來說:在控制檯執行命令時,系統會先去環境路徑(PATH)中找到可執行檔案,然後執行該檔案

cli原理解析

那麼,有沒有同學好奇:

Q2:系統又是怎麼執行這些檔案(例如上面的npm-cli)的呢?

A2:開啟npm-cli.js檔案,我們能看到的第一行程式碼就是:#!/usr/bin/env node,這行程式碼到底有什麼用呢?具體可參考:stackoverflow - #!/usr/bin/env到底有什麼用?,大致意思是告知系統用什麼解釋程式來執行該檔案,例如#!/usr/bin/env node就是告知系統,npm-cli.js要用node來執行。因此npm -v相當於node /usr/local/lib/node_modules/npm/bin/npm-cli.js -v

$ node /usr/local/lib/node_modules/npm/bin/npm-cli.js -v
6.4.1
複製程式碼

二,npm包打造cli的原理

瞭解完命令執行過程之後,我們就可以打造自己的cli命令了。

①先編寫my-cli.js檔案:

#!/usr/bin/env node
console.log('Hello cli!');
複製程式碼

②在/usr/local/bin(或者PATH裡的任意路徑下)建立軟連結:

ln -s my-cli.js my-cli
複製程式碼

③給my-cli命令新增可執行許可權:(若不新增許可權,會報錯bash: /usr/local/bin/my-cli: Permission denied)

chmod 777 my-cli
複製程式碼

④驗證效果:

$ my-cli
Hello cli!
複製程式碼

在上面的基礎上,我們雖然能打造自己的命令,但是這個命令要想給團隊使用,就需要每個人都拷貝my-cli.js檔案,建立軟連結,新增可執行許可權,非常繁瑣。怎麼將自己的命令分發出去給別人使用呢?

我們再往前探索一步,一起打造一個基於npm分發的命令。

我們在下載使用一個npm模組命令的時候,我們會這樣:

npm install -g @vue/cli
vue create my-project
複製程式碼

全域性安裝vue-cli這個npm模組之後,我們全域性新增了vue命令,這背後到底發生了什麼?其實是npm install幫我們把上面提到的②③步自動執行了(個人假設,還沒有時間去看npm的原始碼,如有錯誤,歡迎指出)。既然npm已經幫我們完成這些簡單但是繁瑣的指令碼操作,那我們只需要按照npm的規範來配置一下程式碼即可。流程比較簡單,請參考:通過npm包來製作命令列工具的原理

總結一下開發過程:

  1. npm init新建npm模組目錄;
  2. 開發命令(例如上面的my-cli.js);
  3. package.json中新增bin欄位(bin: { "my-cli": "./my-cli.js" });
  4. 釋出npm;
  5. 全域性安裝即可使用my-cli;

三,commander原理簡析

看了前面一二節,我們已經掌握開發一個團隊cli的方法,剩下的內容將參考[vue-cli原始碼](https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli/bin/vue.js)瞭解怎麼優雅地實現一個cli。

這裡直接和大家介紹幾個vue-cli裡面用到的幾個庫:

  1. commander - 命令列引數解析庫;
  2. Inquirer.js - 命令列常用互動形式集合(問答,選擇...);
  3. chalk - 在命令列樣式美化;
  4. ora - 命令列loader;

更多好用的cli開發庫歡迎大家留言補充。

commander幾乎是開發cli必不可少的工具,基本使用方法如下:

#!/usr/bin/env node
var program = require('commander');

program
  .version('0.1.0')
  .option('-p, --peppers', 'Add peppers')
  .option('-P, --pineapple', 'Add pineapple')
  .option('-b, --bbq-sauce', 'Add bbq sauce')
  .option('-c, --cheese [type]', 'Add the specified type of cheese [marble]', 'marble')
  .parse(process.argv);
複製程式碼

這裡簡單分析一下其原理(參考commander原始碼),核心流程如下:

1. 通過option定義收集命令的功能選項;

2. parse解析命令引數(有process獲得);

3. 由命令引數去匹配前面收集到的功能選項,執行前面的方法(將引數傳入);

核心原始碼如下:

/**
 * Parse `argv`, settings options and invoking commands when defined.
 *
 * @param {Array} argv
 * @return {Command} for chaining
 * @api public
 */

Command.prototype.parse = function(argv) {
  // implicit help
  if (this.executables) this.addImplicitHelpCommand();

  // store raw args
  this.rawArgs = argv;

  // guess name
  this._name = this._name || basename(argv[1], '.js');

  // github-style sub-commands with no sub-command
  if (this.executables && argv.length < 3 && !this.defaultExecutable) {
    // this user needs help
    argv.push('--help');
  }

  // process argv
  var parsed = this.parseOptions(this.normalize(argv.slice(2)));
  var args = this.args = parsed.args;

  var result = this.parseArgs(this.args, parsed.unknown);

  // executable sub-commands
  var name = result.args[0];

  var aliasCommand = null;
  // check alias of sub commands
  if (name) {
    aliasCommand = this.commands.filter(function(command) {
      return command.alias() === name;
    })[0];
  }

  if (this._execs[name] && typeof this._execs[name] !== 'function') {
    return this.executeSubCommand(argv, args, parsed.unknown);
  } else if (aliasCommand) {
    // is alias of a subCommand
    args[0] = aliasCommand._name;
    return this.executeSubCommand(argv, args, parsed.unknown);
  } else if (this.defaultExecutable) {
    // use the default subcommand
    args.unshift(this.defaultExecutable);
    return this.executeSubCommand(argv, args, parsed.unknown);
  }

  return result;
};
複製程式碼

四,vue-cli:create命令執行過程

最後我們以vue create my-project這個命令執行過程結尾,有興趣的同學推薦看vue-cli的原始碼,看原始碼是一個很好的學習過程。

vue create my-project命令執行過程:

  1. 【系統】系統定位到bin/vue.js檔案,通過node bin/vue.js create my-project來執行該檔案;
  2. 【vue.js】bin/vue.js利用commander來定義命令選項create,將create命令匹配到create方法(lib/create.js),執行該方法;
  3. 【create.js】lib/create.js使用Inquirer.js來詢問使用者,進行專案配置;
  4. 【Creator.js】根據使用者配置生成package.json檔案(基礎資訊,從專案配置中注入對應的開發依賴devDependencies);
  5. 【Creator.js】執行npm i來安裝依賴;(PS: 這裡封裝了常用的npm操作,可以直接拷貝到自己專案中使用)
  6. 【Creator.js】載入vue-cli外掛(@vue/cli-service是第一個被執行的外掛);
  7. 【Generator.js】執行所有外掛(執行cli-service外掛會生成專案檔案結構);
  8. 【Creator.js】生成README.md檔案;

上面就是create命令的基本執行過程,如果我們想擴充套件create方法,例如按照我們的定義的模板生成目錄結構,可以新建一個外掛(generator,可以參考cli-service),在外掛裡生成自定義的目錄結構即可。

通過閱讀原始碼有兩個收穫:

  1. 利用外掛形式來擴充套件,能在保證核心主流程簡潔可維護的同時最大限度地提高擴充套件靈活性(之前已經有所實踐,在這裡再次確認外掛架構的重要意義)
  2. 外掛API可以通過呼叫外掛時以引數的形式注入。(相比全域性掛載的方式有兩個好處:①沒有全域性變數汙染;②能按照外掛類別來注入不同的API,達到許可權管控的效果);

五,總結

至此,我們已經具備寫出一個實現良好,易於擴充套件的團隊cli的知識,剩下的事情,就在鍵盤上完成吧。

此外,我提倡“打破砂鍋問到底”,而文章內顯然沒有做到,例如npm甚至是node的執行過程還沒有去深入瞭解。時間永遠是有限的,先有一個好的思考方向,再逐步去深入就好了。而這往往也能解決當代人的焦慮,方向和努力缺一不可,共勉。

相關文章