node命令行工具之實現項目工程自動初始化的標準流程

  • 2019 年 10 月 3 日
  • 筆記

一、目的

傳統的前端項目初始流程一般是這樣:

可以看出,傳統的初始化步驟,花費的時間並不少。而且,人工操作的情況下,總有改漏的情況出現。這個缺點有時很致命。
甚至有馬大哈,沒有更新項目倉庫地址,導致提交代碼到舊倉庫,這就很尷尬了。。。
基於這些情況,編寫命令行工具(CLI)的目的就很明確:

  • 用於新項目工程的初始化
  • 利用工具進行初始化,可以節省項目初期的準備時間
  • 避免出現改漏的情況
  • 杜絕未更新項目版本倉庫地址的問題

以下是新的流程示意圖:

二、自動化流程分析

以下是自動化流程圖:

從流程圖可以得出兩個重要的信息:

  • 配置信息
  • 模板文件

命令行工具的角色,是負責將兩個信息進行融合,提供一個交互平台給用戶。

三、工具準備

3.1 配置信息工具

配置信息的獲得,需要靠和用戶進行交互。由於程序員一般是用終端輸入命令進行項目操作。所以,這裡選擇了兩個工具進行支撐。

  • commander

借鑒Ruby commander理念實現的命令行執行補全解決方案

commander可以接收命令行傳入的參數

例子:

npg-cli --help    ♫ ♫♬♪♫  npm-package-cli ♫ ♫♬♪♫  Usage: npg-cli [options]    Options:    -V, --version  output the version number    -h, --help     output usage information    run testcli and edit the setting.
  • inquirer

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

inquirer用詢問式的語句,與用戶進行交互,接收參數

例子:

npg-cli    ♫ ♫♬♪♫  npm-package-cli ♫ ♫♬♪♫  Follow the prompts to complete the project configuration.    ? project name test  ? version 1.0.0  ? description

3.2 模板信息工具

前端的JavaScript 模板引擎,比如ejs,jade等。可以根據傳入的參數,對模板標籤進行替換,最終生成html。

如果把所有項目文件,不管文件後綴名,都看成是ejs模板,則可以在文件內容中使用ejs語法。
再根據配置信息進行替換,最終生成新文件。

其實,業界依據這個想法,已經有成熟的工具產生。

  • mem-fs

mem-fs是對文件進行讀取,存入內存中。

  • mem-fs-editor

mem-fs-editor是對內存中的文件信息,使用ejs語法進行編譯。最後調用commit方法輸出最終文件。

3.3 提示信息工具

提示信息,除了console.log,還可以使用色彩更豐富的chalk
這樣,可以輸出更直觀、友好的提示。

3.4 文件操作

文件操作,有業界成熟的shelljs
利用shelljs,可以在項目中簡化以下步驟:

  • 一些項目文件,不需要修改,只用直接copy。可以使用shelljs.copySync同步方式生成。
  • 一些文件夾,需要提前構建,可以使用shelljs.mkdir進行創建

四、實現

以下按我做的開源項目——npm-package-cli的創作過程進行分拆、講解。

4.1 初始化

新建項目文件夾npm-package-cli,並在該文件夾下運行npm init,生成package.json
項目結構如下:

 npm-package-cli          |-- package.json

4.2 生成全局指令

這裡要生成的全局指令是npg-cli

4.2.1 新建執行文件

新建文件夾bin,並在文件夾下新建名稱為cli的shell腳本文件(注意:不能有後綴名)。
clishell腳本文件內容如下:

#!/usr/bin/env node    console.log('hello world');

其中,#!/usr/bin/env node是告訴編譯器,以node的方式,運行代碼。

並在package.json加入以下內容:

"bin": {      "npg-cli": "bin/cli"  }

此時,項目結構如下:

 npm-package-cli          |-- bin              |-- cli          |-- package.json

4.2.2 鏈接指令到全局

鏈接指令有兩種方式:

  • npm link
  • npm install -g

兩種方式,都需要在npm-package-cli文件夾下運行,才能生效。
作用是把npg-cli指令,指向全局的bin文件下,實現軟鏈。

4.2.3 運行

在任意文件夾下運行命令:

npg-cli    # 輸出  hello world

到這裡,一個基本的指令就算完成了,接下來是指令的工作內容細化。

4.3 初始化操作類Creation

Creation的作用是整合所有操作,並提供接口給指令文件cli
Creation的結構如下:

class Creation{    constructor(){      // code    }    do(){        // code    }    // other function  }

其中do方法暴露給腳本文件cli調用。

Creation類放在src/index.js中。

此時,項目結構如下:

 npm-package-cli          |-- bin              |-- cli          |-- src              |-- index.js          |-- package.json

4.4 修改cli文件

#!/usr/bin/env node    const Creator = require('../src/index.js');    const project = new Creator();    project.do();  

這樣,只要實現好do方法,就可以完成npg-cli指令的運行了。

4.5 實現命令行參數讀取

實現npg-cli --help,需要藉助上文提到的工具commander
新建src/command.js文件,文件內容如下:

const commander = require('commander');  const chalk = require('chalk');    const packageJson = require('../package.json');  const log = console.log;    function initCommand(){      commander.version(packageJson.version)          .on('--help', ()=>{              log(chalk.green('  run testcli and edit the setting.'));          })          .parse(process.argv);  }    module.exports = initCommand;

此時,項目結構如下:

 npm-package-cli          |-- bin              |-- cli          |-- src              |-- command.js              |-- index.js          |-- package.json

然後在Creation.do方法內執行initCommand()即可生效。

// src/index.js Creation  const initCommand = require('./command');    class Creation{      // other code      do(){          initCommand();      }  }

此時,運行npg-cli --help指令,就可以看到:

Usage: npg-cli [options]    Options:    -V, --version  output the version number    -h, --help     output usage information    run testcli and edit the setting.

4.6 獲取用戶輸入配置信息

要獲取用戶輸入的信息,需要藉助工具inquirer
新建src/setting.js文件,文件內容如下:

const inquirer = require('inquirer');  const fse = require('fs-extra');    function initSetting(){      let prompt = [          {              type: 'input',              name: 'projectName',              message: 'project name',              validate(input){                  if(!input){                      return 'project name is required.'                  }                  if(fse.existsSync(input)){                      return 'project name of folder is exist.'                  }                  return true;              }          },          // other prompt      ];        return inquirer.prompt(prompt);  }    module.exports = initSetting;

此時,項目結構如下:

 npm-package-cli          |-- bin              |-- cli          |-- src              |-- command.js              |-- index.js              |-- setting.js          |-- package.json

然後在Creation.do方法內執行initSetting()即可生效。

// src/index.js Creation  const initCommand = require('./command');  const initSetting = require('./setting');    class Creation{      // other code      do(){          initCommand();          initSetting().then(setting => {              // 用戶輸入完成後,會得到全部輸入信息的json數據 setting          });      }  }

這裡,inquirer.prompt方法裝載好要收集的問題後,返回的是Promise對象。收集完成之後,要在then方法內拿到配置信息,以便進行下一步模板替換的操作。

4.7 模板文件替換輸出

模板文件替換,要用到工具mem-fsmem-fs-editor
文件操作,要用到工具shelljs

新建src/output.js文件,文件內容如下(刪除了部分代碼,以下只是示例,完整項目看最後分享鏈接):

const chalk = require('chalk');  const fse = require('fs-extra');  const path = require('path');  const log = console.log;    function output(creation){      return new Promise((resolve, reject)=>{          // 拿到配置信息          const setting = creation._setting;          const {              projectName          } = setting;          // 獲取當前命令行執行環境所在文件夾          const cwd = process.cwd();            // 初始化文件夾path          const projectPath = path.join(cwd, projectName);          const projectResolve = getProjectResolve(projectPath);            // 新建項目文件夾          fse.mkdirSync(projectPath);            // copy文件夾          creation.copy('src', projectResolve('src'));          // 根據配置信息,替換文件內容          creation.copyTpl('package.json', projectResolve('package.json'), setting);            // 將內存中的文件,輸出到硬盤上          creation._mfs.commit(() => {              resolve();          });      });  }    module.exports = output;

output方法的作用:

  • 新建項目文件夾
  • 把模板文件讀取出來,根據配置信息,進行替換(調用的是mem-fs-editorcopyTpl方法)
  • 拷貝其他文件
  • 輸出最終文件到硬盤上

這裡最重要的一步,是調用mem-fs-editor的方法後,要執行mem-fs-editorcommit方法,輸出內存中的文件到硬盤上。

Creation.do方法中,調用output方法即可輸出新項目文件。
打開src/index.js文件,文件內容增加如下方法:

// src/index.js Creation  const initCommand = require('./command');  const initSetting = require('./setting');  const output = require('./output');    class Creation{      // other code      do(){          initCommand();          initSetting().then(setting => {              // 用戶輸入完成後,會得到全部輸入信息的json數據 setting              this._setting = Object.assign({}, this._setting, setting);              // 輸出文件              output(this).then(res => {                  // 項目輸出完成              });          });      }  }

4.8 階段小結

自動初始化一個項目的流程不外乎以下三點:

  • 讀取用戶配置
  • 讀取模板文件
  • 根據配置,編譯模板文件,輸出最終文件

命令行工具,是對這三點的有效整合,串連成一個規範的流程。

五、發佈npm包的注意點

5.1 安裝依賴包的方式

命令行工具中,使用的第三方工具包,都需要用--save的方式安裝。
體現在package.json的表現是dependencies字段:

"dependencies": {      "chalk": "^2.4.2",      "commander": "^3.0.0",      "fs-extra": "^8.1.0",      "inquirer": "^6.5.0",      "mem-fs": "^1.1.3",      "mem-fs-editor": "^6.0.0",      "shelljs": "^0.8.3"  },

這樣,其他用戶在安裝你發佈的CLI工具時,才會自動安裝這些依賴。

5.2 .gitignore文件

npm官方是默認去除.gitignore文件的,不管你用任何方式聲明.gitignore文件需要publish
解決方式是:將.gitignore改名稱,比如改為gitignore。當使用CLI工具時,再將文件名改回來。
例子:

creation.copy('gitignore', projectResolve('.gitignore'));

六、項目開源

我創作的npm-package-cli,是專門用於生成個人npm package項目的CLI工具。
生成的項目,囊括以下功能點:

  • 支持TypeScrpt
  • mocha+chai自動化測試,支持使用TypeScript編寫測試用例
  • 支持測試覆蓋率coverage
  • 支持eslint,包括對TypeScript的lint檢查
  • Git commit規範提交
  • Git版本自動打標籤(standard-version),更新CHANGELOG.md
  • 輸出的npm包支持各種模塊規範(AMD、CMD、CommonJS、ESModule)

CLI工具安裝方式:

npm install -g npm-package-cli

開源倉庫地址:https://github.com/wall-wxk/npm-package-cli
如果對你有所幫助,麻煩給個Star,你的肯定是我前進的動力~

Exit mobile version