Node.js除了能夠編寫「傳統「的Web應用外,還有其餘更普遍的用途。微服務、REST API
、工具、物聯網,甚至桌面應用,它能知足你的任何開發需求。javascript
本文要作的事情就是利用Node.js
來構建命令行工具CLI
。咱們先來看一些用於建立命令行的第三方npm包
,而後,從零開始構建命令行工具。前端
咱們將要實現一個命令行工具,它的做用是初始化Git倉庫。固然,它不只僅是在後臺運行git init
,他還會作一些別的事情。咱們能夠經過它來初始化Git倉庫,而且容許用戶經過交互的方式建立.gitignore
文件,最終執行提交併推送代碼到遠端倉庫。java
與以往同樣,你們能夠在GitHub(https://github.com/sssssssh/ginit)上找到本教程隨附的代碼。node
在深刻研究以前,咱們有必要了解一下爲何咱們選擇Node.js
來構建命令行工具。git
最明顯的好處是,若是你在閱讀本文那麼大機率是由於你對JavaScript
已經很瞭解。github
另外一個關鍵優點是,使用Node.js
的生態意味着你能夠利用成千上萬種實現各類目的的npm包
。其中有不少是爲了構建強大的命令行工具而生的。shell
最後,咱們能夠經過npm
管理依賴,不須要擔憂特定系統的包管理工具帶來的兼容問題,例如apt
、yum
、homebrew
。npm
經過這個教程,咱們將構建一個叫ginit
的命令行工具。它實現了git init
,但又不只僅只有這個功能。json
你可能想知道它究竟是幹啥用的。api
衆所周知,git init
會在當前文件夾初始化git倉庫
。可是,一般這是將新項目或者已有項目關聯到Git
上的衆多重複步驟中的一步。例如,做爲一個經典的工做流程中的一部分,你可能會:
git init
初始化本地倉庫.gitignore
文件一般會涉及到更多操做,可是,出於教學目的,在本教程中咱們僅僅實現上面的步驟。這些步驟都是重複的,咱們經過命令行工具來實現豈不是比粘貼複製git
倉庫的連接更好呢?
所以,ginit
要作的就是在當前文件夾中建立Git
倉庫,建立一個遠程倉庫(這裏咱們用git),而後將它添加爲遠程倉庫,而後,它將提供一個簡單的交互式嚮導來建立.gitignore
,添加文件並將其推送到遠端。他可能不會減小你的時間,可是,會減小一些你的重複勞動。
基於這一點讓咱們開始吧!
能夠確定的一件事:就外觀而言,控制檯永遠不會具備圖形用戶界面的複雜度。不過,這並不意味着他必須是醜陋的單色文本。你可能會驚訝於在保持功能正常的狀況下,命令行工具也能夠作的很好看。咱們找到了幾個加強界面展現的庫:chalk
用於在終端中輸出彩色的文字;clui
用於添加一些UI組件。還有好玩的,咱們會利用figlet
建立一個基於ASCII
的炫酷橫幅,而且利用clear
來清空控制檯。
在輸入和輸出方面,Node.js
底層的Readline
模塊用於提示用戶輸入綽綽有餘。可是,咱們將利用一個第三方庫Inquirer
,它提供了更多複雜的功能。除了實現詢問用戶的功能外,它在控制檯中還提供了單選框和複選框的功能。
咱們還會使用minimist
來解析命令行中輸入的參數。
這是咱們在開發命令行工具中使用到的完整的npm
包列表:
chalk
: 讓咱們的輸出變得有色彩;clear
: 清空終端屏幕;clui
: 繪製命令行中的表格、儀表盤、加載指示器等;figlet
: 生成基於ASCII
的藝術字;inquirer
: 建立交互式的命令行界面;minimist
: 解析命令行參數;configstore
: 輕鬆的加載和保存配置信息;另外,咱們還會使用下面的包:
@octokit/rest
: 基於Node.js
的Github REST API工具;@octokit/auth-basic
: Github
身份驗證策略的一種實現;lodash
: JavaScript 工具庫;simple-git
: 在Node.js
中執行Git
命令的工具;touch
: 實現Unix touch
命令的工具;儘管咱們是從頭開始建立這個命令行工具,可是不要忘記你也能夠從本文附帶的GitHub倉庫(https://github.com/sssssssh/ginit)中拷貝一份代碼。
爲這個項目建立一個新的目錄,固然,你能夠給他起別的名字,沒必要必定叫他ginit
:
mkdir ginit cd ginit
建立一個新的package.json
文件:
npm init -y
最終將會生成一個這樣的package.json
文件
{ "name": "ginit", "version": "1.0.0", "description": "'git init' on steroids", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ "Git", "CLI" ], "license": "ISC" }
如今開始安裝項目依賴:
npm install chalk clear clui figlet inquirer minimist configstore @octokit/rest @octokit/auth-basic lodash simple-git touch
在項目中建立一個index.js
文件,加上以下代碼:
// index.js const chalk = require('chalk'); const clear = require('clear'); const figlet = require('figlet');
在目錄中建立一個lib
目錄,並將咱們的代碼分爲如下模塊:
file.js
:基礎的文件管理inquirer.js
:處理命令行中的用戶交互;github.js
:管用戶的git token;repo.js
:Git倉庫管理;讓咱們開始寫lib/file.js
中的代碼。咱們須要作如下事情:
.git
目錄是否存在,來判斷當前目錄是否存在git倉庫);這聽起來很簡單,可是,有幾個問題須要考慮。
首先,你可能會想到用fs
模塊的realpathSync
方法來獲取當前目錄:
path.basename(path.dirname(fs.realpathSync(__filename)));
當咱們從命令行所在文件的根目錄下調用時,這個方法沒啥問題。可是,咱們的命令行工具能夠在任何目錄下調用。這意味着咱們須要得到的是當前工做目錄的名稱,而不是命令行代碼所在目錄的名稱。因此,你最好使用process.cwd()
:
path.basename(process.cwd());
第二,檢查文件是否存在的最佳方法一直在變化。目前最好的方法是使用existsSync
,若是文件存在他會返回true
,不然返回false
。
結合上面所說的,讓咱們在lib/files.js
中添加以下代碼:
// files.js const fs = require('fs'); const path = require('path'); module.exports = { // 獲取目錄名稱 getCurrentDirectoryBase: () => { return path.basename(process.cwd()); }, // 判斷目錄是否存在 directoryExists: (filePath) => { return fs.existsSync(filePath); }, };
在index.js
中,添加下面的代碼:
// index.js const files = require('./lib/files');
有了這個,咱們就能夠動手開發咱們的命令行工具了。
如今讓咱們來實現命令行工具的啓動階段。
爲了展現咱們安裝的加強控制臺輸出的模塊,咱們先清空屏幕,再展現一個banner,在index.js
中添加以下代碼:
// index.js // 清除命令行 clear(); // 輸出Logo console.log(chalk.yellow(figlet.textSync('Ginit', { horizontalLayout: 'full' })));
你能夠經過運行node index.js
來執行它,輸出效果以下:
接下來,讓咱們進行一個簡單的檢查,以確保當前目錄不存在git倉庫。這很簡單,只須要利用咱們建立的方法來檢查.git
方法是否存在便可,在index.js
中添加以下代碼:
// 判斷是否存在.git文件 if (files.directoryExists('.git')) { console.log(chalk.red('已經存在一個本地倉庫!')); process.exit(); }
接下來,咱們須要建立一個函數來引導用戶輸入他們的GitHub
帳號和密碼。
咱們可使用Inquirer
來實現,它提供了不少種類型的提示方法。這些方法有些相似於HTML
中的控件。爲了收集用戶的GitHub
帳號和密碼,咱們須要使用到input和password類型的控件。
首先,在lib/inquirer.js
中添加以下代碼:
// inquirer.js const inquirer = require('inquirer'); const files = require('./files'); module.exports = { // 詢問git帳號信息 askGithubCredentials: () => { const questions = [ { name: 'username', type: 'input', message: '請輸入你的git帳號或郵箱地址:', validate: function (value) { if (value.length) { return true; } else { return '請輸入你的git帳號或郵箱地址.'; } }, }, { name: 'password', type: 'password', message: '請輸入你的密碼:', validate: function (value) { if (value.length) { return true; } else { return '請輸入你的密碼.'; } }, }, ]; return inquirer.prompt(questions); } };
如你所見,經過inquirer.prompt()
向用戶詢問一系列問題,咱們將這些問題以數組的形式傳遞給函數prompt
。每一問題都由一個對象構成,其中,name
表示該字段的名稱,type
表示咱們要使用控件類型,message
是咱們要展現給用戶的話,validate
是校驗用戶輸入字段的函數。
inquirer.prompt()
將會返回一個Promise
對象,若是校驗經過,咱們將會獲得一個擁有name
和password
2個屬性的對象。
將以下代碼添加在index.js
:
// index.js const inquirer = require('./lib/inquirer'); const run = async () => { const credentials = await inquirer.askGithubCredentials(); console.log(credentials); }; run();
運行node index.js
結果以下:
提示:當你完成測試後,不要忘了把const inquirer = require('./lib/inquirer');
從index.js
中刪除,由於咱們不須要它。
下一步是建立一個函數,用於獲取GitHub API
的OAuth TOKEN
。實際上,咱們就是經過帳號和密碼來獲取token
。
固然,咱們不但願用戶每次使用咱們的工具時,都須要輸入帳號和密碼。相反,咱們將保存OAuth
令牌用於後續的請求。這就要用到configstore
這個包啦。
保存配置信息表面上看很簡單,你能夠簡單的讀寫一個JSON文件就行了。可是,configstore
這個包有如下優點:
configstore
對象便可,後面的事他幫你搞定;用法很簡單,建立一個實例,傳入一個標識符便可,例如:
const Configstore = require('configstore'); const conf = new Configstore('ginit');
若是configstore
文件不存在,他會返回一個空對象而且在後臺建立一個文件。若是文件存在,你能夠直接利用裏面的內容。你如今能夠根據須要直接修改conf
對象的屬性。同時,你也不須要擔憂怎麼去保存它,它本身會處理好的。
提示:在macOS
系統上,文件將會保存在/Users/[YOUR-USERNME]/.config/configstore/ginit.json
下。在Linux
系統上文件保存在/home/[YOUR-USERNME]/.config/configstore/ginit.json
。
讓咱們來建立一個文件來處理GitHub Token
。建立lib/github.js
並將下列代碼拷入:
// github.js const CLI = require('clui'); const Configstore = require('configstore'); const Spinner = CLI.Spinner; const { Octokit } = require("@octokit/rest") const { createBasicAuth } = require('@octokit/auth-basic'); const inquirer = require('./inquirer'); const pkg = require('../package.json'); // 初始化本地的存儲配置 const conf = new Configstore(pkg.name);
如今讓咱們來建立一個函數來檢查咱們是否擁有token
。咱們還建立了一個函數,方便其餘模塊獲取到octokit
實例。在lib/github.js
中增長下列代碼:
// github.js // ...初始化 // 模塊內部的單例 let octokit; module.exports = { // 獲取octokit實例 getInstance: () => { return octokit; }, // 獲取本地token getStoredGithubToken: () => { return conf.get('github.token'); } }
若是conf
對象存在且github.token
屬性也存在,就表示token
存在。這裏咱們就能夠把token
返回給調用函數。咱們稍後會講它。
若是沒檢查到token
,則須要去獲取一個。固然,獲取OAuth token
涉及到網絡請求,這意味着用戶須要短暫的等待。藉此機會,咱們能夠看到clui
提供控制帶UI加強功能,loading效果就是其中一個。
建立一個loading效果很簡單:
const status = new Spinner('Authenticating you, please wait...'); status.start();
完成後,只須要中止他,他就會在屏幕上消失:
status.stop();
提示:你也能夠用update
來動態的設置文字。若是你須要一個進度指示器,例如展現當前的進度的百分比,這可能很是有用。
將下面代碼拷貝到lib/github.js
中,這是完成GitHub
認證的代碼:
// github.js module.exports = { // 獲取實例 getInstance: () => { ... }, // 獲取本地token getStoredGithubToken: () => { ... }, // 經過我的帳號信息獲取token getPersonalAccessToken: async () => { const credentials = await inquirer.askGithubCredentials(); const status = new Spinner('驗證身份中,請等待...'); status.start(); const auth = createBasicAuth({ username: credentials.username, password: credentials.password, async on2Fa() { // 等待實現 }, token: { scopes: ['user', 'public_repo', 'repo', 'repo:status'], note: 'ginit, the command-line tool for initalizing Git repos', }, }); try { const res = await auth(); if (res.token) { conf.set('github.token', res.token); return res.token; } else { throw new Error('獲取GitHub token失敗'); } } finally { status.stop(); } } };
讓咱們來逐步完成:
askGithubCredentials
來詢問用戶的帳號和密碼;咱們使用createBasicAuth
來建立一個auth
函數,方便後面調用。須要給這個函數傳遞用戶的用戶名和密碼,同時還須要傳遞一個token
對象,它擁有下面2個屬性:
note
:記錄獲取token的用途;scopes
:一個受權信息使用範圍的列表,你能夠在GitHub上了解更多信息;try catch
中利用await
語法等待函數的返回結果;token
,能夠把它放到configstore
中,方便下次直接使用;您建立的任何token
(不管是手動建立的仍是經過API生成的)均可以在此處看到。 在開發過程當中,你可能須要刪除ginit
的token
(能夠經過上面提供的note
參數識別),以便從新生成它。
更新index.js
中的代碼:
// index.js const github = require('./lib/github'); ... const run = async () => { // 從本地獲取token記錄 let token = github.getStoredGithubToken(); if(!token) { // 經過帳號、密碼獲取token token = await github.getPersonalAccessToken(); } console.log(token); };
第一次運行時,你須要輸入你的用戶名和密碼。咱們將會在github
上建立一個token
並把它保存起來。下次運行時,咱們將直接使用保存起來的token
作身份認證。
但願你注意到上面代碼中的on2Fa
函數。當用戶的帳號使用雙重認證時,將會調用到這個函數。讓咱們在lib/inquirer.js
中插入以下代碼:
// inquirer.js const inquirer = require('inquirer'); module.exports = { // 詢問git帳號信息 askGithubCredentials: () => { ... }, // 詢問雙重認證碼 getTwoFactorAuthenticationCode: () => { return inquirer.prompt({ name: 'twoFactorAuthenticationCode', type: 'input', message: '請輸入你的雙重認證驗證碼:', validate: function (value) { if (value.length) { return true; } else { return '請輸入你的雙重認證驗證碼:.'; } }, }); } }
修改lib/github.js
中的on2Fa
函數:
// github.js async on2Fa() { status.stop(); const res = await inquirer.getTwoFactorAuthenticationCode(); status.start(); return res.twoFactorAuthenticationCode; }
如今咱們的程序能夠處理GitHub
雙重認證。
獲取Oauth
令牌以後,咱們就能夠利用它來建立遠程倉庫。
一樣,咱們能夠利用Inquirer
來問一系列問題。咱們須要獲取一個倉庫名字,咱們能夠要求用戶選填一個描述,還須要詢問倉庫是共有仍是私有。
咱們能夠利用minimist
來從命令行參數中獲取倉庫名稱和描述。
ginit my-repo "just a test repository"
下面的代碼將會解析出一個數組:
const argv = require('minimist')(process.argv.slice(2)); // { _: [ 'my-repo', 'just a test repository' ] }
咱們將經過代碼來實現上面所說的提問。首先將下列代碼拷貝到lib/inquirer.js
中:
// inquirer.js const inquirer = require('inquirer'); const files = require('./files'); module.exports = { // 詢問git帳號信息 askGithubCredentials: () => { ... }, // 詢問雙重認證碼 getTwoFactorAuthenticationCode: () => { ... }, // 詢問倉庫詳細信息 askRepoDetails: () => { const argv = require('minimist')(process.argv.slice(2)); const questions = [ { type: 'input', name: 'name', message: '請輸入git倉庫名稱:', default: argv._[0] || files.getCurrentDirectoryBase(), validate: function (value) { if (value.length) { return true; } else { return '請輸入git倉庫名稱.'; } }, }, { type: 'input', name: 'description', default: argv._[1] || null, message: '請輸入倉庫描述(選填):', }, { type: 'list', name: 'visibility', message: '共有倉庫 或 私有倉庫:', choices: ['public', 'private'], default: 'public', }, ]; return inquirer.prompt(questions); } };
建立lib/repo.js
文件,並添加以下代碼:
// repo.js const CLI = require('clui'); const fs = require('fs'); const git = require('simple-git/promise')(); const Spinner = CLI.Spinner; const touch = require('touch'); const _ = require('lodash'); const inquirer = require('./inquirer'); const gh = require('./github'); module.exports = { // 建立遠程倉庫 createRemoteRepo: async () => { const github = gh.getInstance(); const answers = await inquirer.askRepoDetails(); const data = { name: answers.name, description: answers.description, private: answers.visibility === 'private', }; const status = new Spinner('建立遠程倉庫中...'); status.start(); try { const response = await github.repos.createForAuthenticatedUser(data); return response.data.ssh_url; } finally { status.stop(); } } }
獲取以上信息後,我就能夠建立Git倉庫了。咱們這本地將生成好的倉庫設置成咱們的遠程倉庫。可是,在這以前讓咱們以交互的方式來建立一個.gitignore
文件吧。
下一步,咱們將要建立一個簡單的命令行「嚮導」來生成.gitignore
文件。若是用戶在現有項目目錄中執行咱們的命令行工具,請向他們展現當前目錄已經存在的文件和目錄,並容許他們選擇須要忽略的文件和文件夾。
inquirer
提供了一個複選框給咱們使用。
咱們須要掃描當前目錄中.git
和.gitignore
之外的文件。
const filelist = _.without(fs.readdirSync('.'), '.git', '.gitignore');
若是沒有文件須要添加到'.gitignore'中,那麼直接建立一個.gitignore
文件便可
if (filelist.length) { ... } else { touch('.gitignore'); }
讓咱們在lib/inquirer.js
中添加以下代碼:
// inquirer.js // 選擇須要忽略的文件 askIgnoreFiles: (fileList) => { const questions = [ { type: 'checkbox', name: 'ignore', message: '請選擇你想要忽略的文件:', choices: fileList, default: ['node_modules', 'bower_components'], }, ]; return inquirer.prompt(questions); },
注意:咱們能夠提供一些默認選項,若是node_modules
和bower_components
存在的話,咱們將提早選中。
在lib/repo.js
中,咱們添加以下代碼:
// repo.js // 建立git ignore createGitignore: async () => { const fileList = _.without(fs.readdirSync('.'), '.git', '.gitignore'); if (fileList.length) { const answers = await inquirer.askIgnoreFiles(fileList); if (answers.ignore.length) { // 寫入信息 fs.writeFileSync('.gitignore', answers.ignore.join('\n')); } else { // 建立文件 touch('.gitignore'); } } else { // 建立文件 touch('.gitignore'); } },
一旦提交,咱們將會根據選中的文件生成一個.gitignore
文件。既然已經能夠生成.gitignore
文件了,讓咱們初始化git倉庫吧。
有不少實現和git
交互的方法,可是,最簡單的方法多是simple-git。這個庫提供了一批能夠鏈式調用的異步函數,在後臺執行git命令。
這是咱們須要作的重複任務:
git init
.gitignore
在lib/repo.js
中添加以下代碼:
// repo.js // 設置 setupRepo: async (url) => { const status = new Spinner('初始化本地倉庫並推送到遠端倉庫中...'); status.start(); try { await git.init(); await git.add('.gitignore'); await git.add('./*'); await git.commit('Initial commit') await git.addRemote('origin', url); await git.push('origin', 'master'); } finally { status.stop(); } },
首先,咱們須要在lib/github.js
文件中增長一個函數,該函數的做用是創建一個oauth
認證:
// github.js // 經過token登錄 githubAuth: (token) => { octokit = new Octokit({ auth: token, }); },
而後,咱們須要建立一個函數來控制獲取token
的邏輯。在run
函數前,增長以下代碼:
// index.js // 獲取github token const getGithubToken = async () => { // 從本地獲取token記錄 let token = github.getStoredGithubToken(); if (token) { return token; } // 經過帳號、密碼獲取token token = await github.getPersonalAccessToken(); return token; };
最後,咱們用下面的代碼來更新咱們的run
函數:
// index.js const run = async () => { try { // 獲取token const token = await getGithubToken(); github.githubAuth(token); // 建立遠程倉庫 const url = await repo.createRemoteRepo(); // 建立 .gitignore await repo.createGitignore(); // 初始化本地倉庫並推送到遠端 await repo.setupRepo(url); console.log(chalk.green('All done!')); } catch (err) { if (err) { switch (err.status) { case 401: console.log(chalk.red("登錄失敗,請提供正確的登錄信息")); break; case 422: console.log(chalk.red('遠端已存在同名倉庫')); break; default: console.log(chalk.red(err)); } } } };
如你所見,在調用咱們其餘的函數以前(createRemoteRepo()
, createGitignore()
, setupRepo()
),咱們確保用戶已經經過了身份驗證。並且,還處理任何錯誤而且給用戶適當的反饋。
你能夠在git倉庫中找到完整的代碼。
如今,你就擁有了一個能夠運行的命令行工具了。運行一下,看看他是否是按照你的預期工做。
還有一件須要作的事就是讓咱們的命令行全局可用。爲了實現這個事,咱們須要在index.js
文件頭部加上shebang。
#!/usr/bin/env node
而後,咱們須要在package.json
中增長一個bin
屬性。用於綁定命令名稱ginit
和被執行的文件。
"bin": { "ginit": "./index.js" }
而後全局安裝模塊,命令行工具就能夠用了。
npm install -g
若是你想確認安裝是否生效,你能夠把本機上全局安裝的node模塊列出來看看:
npm ls -g --depth=0
咱們已經建立了一個漂亮且簡潔的初始化Git
倉庫的命令行工具。並且你還能夠作不少事情去提高它。
若是你是一個Bitbucket
用戶,你能夠利用Bitbucket API
給這個命令行增長一個建立Bitbucket
倉庫的功能。這個node包 bitbucket-api對你會有幫助。你能夠增長另一個命令行選項或者詢問用戶是否要使用Bitbucket
,或者直接把如今處理GitHub
的代碼替換成Bitbucket
。
你能夠提供一個.gitignore
默認的文件集合,而不是硬編碼。preferences
這個包很適合這個場景,或者你能夠提供一個模版,提示用戶輸入對應的模版類型便可。也能夠把它集成到.gitignore.io中。
除此以外,你還能夠增長其餘驗證,提供跳過某些步驟的功能等等。
這是一篇老文章了,不過,今年2月份做者又更新了一部份內容,剔除了其中失效的依賴。同時,在閱讀的過程當中,我也優化了一下示例代碼。
我是一個莫得感情的代碼搬運工,每週會更新1至2篇前端相關的文章,有興趣的老鐵能夠掃描下面的二維碼關注或者直接微信搜索前端補習班
關注。
精通前端很難,讓咱們來一塊兒補補課吧!
好啦,翻譯完畢啦,原文連接在此 Build a JavaScript Command Line Interface (CLI) with Node.js。