node命令列工具之實現專案工程自動初始化的標準流程
阿新 • • 發佈:2019-08-12
## 一、目的
傳統的前端專案初始流程一般是這樣:
![傳統流程](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,你的肯定是我前進的