1. 程式人生 > >node命令列工具之實現專案工程自動初始化的標準流程

node命令列工具之實現專案工程自動初始化的標準流程

## 一、目的 傳統的前端專案初始流程一般是這樣: ![傳統流程](https://wangxiaokai.vip/images/2019-08-11-make-command-line-interface/1.png) 可以看出,傳統的初始化步驟,花費的時間並不少。而且,人工操作的情況下,**總有改漏的情況出現**。這個缺點有時很致命。 甚至有馬大哈,沒有更新專案倉庫地址,導致提交程式碼到舊倉庫,這就很尷尬了。。。 基於這些情況,編寫命令列工具(CLI)的目的就很明確: - 用於新專案工程的初始化 - 利用工具進行初始化,可以節省專案初期的準備時間 - 避免出現改漏的情況 - 杜絕未更新專案版本倉庫地址的問題 以下是新的流程示意圖: ![新的流程](https://wangxiaokai.vip/images/2019-08-11-make-command-line-interface/2.png) ## 二、自動化流程分析 以下是自動化流程圖: ![自動化流程分析](https://wangxiaokai.vip/images/2019-08-11-make-command-line-interface/3.png) 從流程圖可以得出兩個重要的資訊: - 配置資訊 - 模板檔案 命令列工具的角色,是負責將兩個資訊進行融合,提供一個互動平臺給使用者。 ## 三、工具準備 ### 3.1 配置資訊工具 配置資訊的獲得,需要靠和使用者進行互動。由於程式設計師一般是用終端輸入命令進行專案操作。所以,這裡選擇了兩個工具進行支撐。 - **commander** > 借鑑Ruby commander理念實現的命令列執行補全解決方案 `commander`可以接收命令列傳入的引數 例子: ```bash 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`用詢問式的語句,與使用者進行互動,接收引數 例子: ```bash 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`。 專案結構如下: ```text npm-package-cli |-- package.json ``` ### 4.2 生成全域性指令 這裡要生成的全域性指令是`npg-cli`。 #### 4.2.1 新建執行檔案 新建資料夾`bin`,並在資料夾下新建名稱為`cli`的shell指令碼檔案(注意:不能有後綴名)。 `cli`shell指令碼檔案內容如下: ```shell #!/usr/bin/env node console.log('hello world'); ``` 其中,`#!/usr/bin/env node`是告訴編譯器,以`node`的方式,執行程式碼。 並在`package.json`加入以下內容: ```text "bin": { "npg-cli": "bin/cli" } ``` 此時,專案結構如下: ```text npm-package-cli |-- bin |-- cli |-- package.json ``` #### 4.2.2 連結指令到全域性 連結指令有兩種方式: - `npm link` - `npm install -g` 兩種方式,都需要在`npm-package-cli`資料夾下執行,才能生效。 作用是把`npg-cli`指令,指向全域性的`bin`檔案下,實現軟鏈。 #### 4.2.3 執行 在任意資料夾下執行命令: ```shell npg-cli # 輸出 hello world ``` 到這裡,一個基本的指令就算完成了,接下來是指令的工作內容細化。 ### 4.3 初始化操作類Creation `Creation`的作用是整合所有操作,並提供介面給指令檔案`cli`。 `Creation`的結構如下: ```javascript class Creation{ constructor(){ // code } do(){ // code } // other function } ``` 其中`do`方法暴露給指令碼檔案`cli`呼叫。 `Creation`類放在`src/index.js`中。 此時,專案結構如下: ```text npm-package-cli |-- bin |-- cli |-- src |-- index.js |-- package.json ``` ### 4.4 修改`cli`檔案 ```shell #!/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`檔案,檔案內容如下: ```javascript 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; ``` 此時,專案結構如下: ```text npm-package-cli |-- bin |-- cli |-- src |-- command.js |-- index.js |-- package.json ``` 然後在`Creation.do`方法內執行`initCommand()`即可生效。 ```javascript // src/index.js Creation const initCommand = require('./command'); class Creation{ // other code do(){ initCommand(); } } ``` 此時,執行`npg-cli --help`指令,就可以看到: ```shell 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`檔案,檔案內容如下: ```javascript 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; ``` 此時,專案結構如下: ```text npm-package-cli |-- bin |-- cli |-- src |-- command.js |-- index.js |-- setting.js |-- package.json ``` 然後在`Creation.do`方法內執行`initSetting()`即可生效。 ```javascript // 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-fs`和`mem-fs-editor`。 檔案操作,要用到工具`shelljs`。 新建`src/output.js`檔案,檔案內容如下(刪除了部分程式碼,以下只是示例,完整專案看最後分享連結): ```javascript 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-editor`的`copyTpl`方法) - 拷貝其他檔案 - 輸出最終檔案到硬碟上 這裡最重要的一步,是呼叫`mem-fs-editor`的方法後,要執行`mem-fs-editor`的`commit`方法,輸出記憶體中的檔案到硬碟上。 在`Creation.do`方法中,呼叫`output`方法即可輸出新專案檔案。 開啟`src/index.js`檔案,檔案內容增加如下方法: ```javascript // 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包的注意點 命令列工具中,使用的第三方工具包,都需要用`--save`的方式安裝。 體現在`package.json`的表現是`dependencies`欄位: ```javascript "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工具時,才會自動安裝這些依賴。 ## 六、專案開源 我創作的`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工具安裝方式: ```shell npm install -g npm-package-cli ``` 開源倉庫地址:[https://github.com/wall-wxk/npm-package-cli](https://github.com/wall-wxk/npm-package-cli) 如果對你有所幫助,麻煩給個Star,你的肯定是我前進的