平常經(jīng)常使用一些npm cli工具期升,你是否好奇這些工具又是怎么開發(fā)出來(lái)的呢?接下來(lái)這篇文章亲茅,就會(huì)介紹如何利用Node.js開發(fā)一個(gè)屬于你自己的命令行工具略步。
創(chuàng)建基礎(chǔ)的文件目錄
首先,我們需要先創(chuàng)建一個(gè)基本的項(xiàng)目結(jié)構(gòu):
mkdir git-repo-cli
cd git-repo-cli
npm init #初始化項(xiàng)目
接著我們創(chuàng)建所需的文件:
touch index.js
mkdir lib
cd lib
touch files.js
touch github_credentials.js
touch inquirer.js
touch create_a_repo.js
接著我們先來(lái)寫一個(gè)簡(jiǎn)單的入口程序,在index.js
文件中代碼如下:
const chalk = require('chalk');
const clear = require('clear');
const figlet = require('figlet');
const commander = require('commander');
commander
.command('init')
.description('Hello world')
.action(() => {
clear();
console.log(chalk.magenta(figlet.textSync('Git Repo Cli', {
hosrizontalLayout: 'full'
})));
});
commander.parse(process.argv);
if (!commander.args.length) {
commander.help();
}
上面的代碼中引用了chalk
,clear
,figlet
和commander
這幾個(gè)npm庫(kù),,其中chalk
負(fù)責(zé)命令行不同顏色文本的顯示,便于用戶使用放妈。clear
庫(kù)負(fù)責(zé)清空命令行界面,figlet
可以在命令行中以ASCII ART形式顯示文本荐操。最后commander
庫(kù)就是用來(lái)實(shí)現(xiàn)命令行接口最主要的庫(kù)芜抒。
寫完代碼后,我們可以在命令行中輸入如下代碼啟動(dòng):
node index.js init
功能模塊
文件模塊
把基本的架子搭起來(lái)后托启,我們就可以開始寫功能模塊的代碼了宅倒。
在lib/files.js
文件中,主要要實(shí)現(xiàn)以下兩個(gè)功能::
- 獲取當(dāng)前文件目錄名
- 檢查該路徑是否已經(jīng)是個(gè)git倉(cāng)庫(kù)
const fs = require('fs');
const path = require('path');
module.exports = {
getCurrentDirectoryBase: () => path.basename(process.cwd()),
directoryExists: (filePath) => {
try {
return fs.statSync(filePath).isDirectory();
} catch (err) {
return false;
}
},
isGitRepository: () => {
if (files.directoryExists('.git')) {
console.log(chalk.red("Sorry! Can't create a new git repo because this directory has been existed"))
process.exit();
}
}
};
詢問(wèn)模塊
在用戶在執(zhí)行命令行工具的時(shí)候屯耸,需要收集一些變量信息拐迁,因此蹭劈,可以我們需要利用inquirer這個(gè)npm庫(kù)來(lái)實(shí)現(xiàn)“詢問(wèn)模塊”。
const inquirer = require('inquirer');
module.exports = {
askGithubCredentials: () => {
const questions = [
{
name: "username",
type: 'input',
message: 'Enter your Github username or e-mail address:',
validate: function(value) {
if (value.length) {
return true;
} else {
return 'Please enter your Github username:'
}
}
},
{
name: "password",
type: 'password',
message: 'Enter your password:',
validate: function(value) {
if (value.length) {
return true;
} else {
return 'Please enter your Github username:'
}
}
}
];
return inquirer.prompt(questions);
}
}
github認(rèn)證
為了實(shí)現(xiàn)和github的接口通信线召,需要獲得token認(rèn)證信息铺韧。因此,在lib/github_credentials.js
文件中缓淹,我們獲得token,并借助configstore
庫(kù)寫入package.json
文件中哈打。
const Configstore = require('configstore');
const pkg = require('../package.json')
const octokit = require('@octokit/rest')();
const _ = require('lodash');
const inquirer = require("./inquirer");
const conf = new Configstore(pkg.name);
module.exports = {
getInstance: () => {
return octokit;
},
githubAuth: (token) => {
octokit.authenticate({
type: 'oauth',
token: token
});
},
getStoredGithubToken: () => {
return conf.get('github_credentials.token');
},
setGithubCrendeitals: async () => {
const credentials = await inquirer.askGithubCredentials();
octokit.authenticate(
_.extend({
type: 'basic'
}, credentials)
)
},
registerNewToken: async () => {
// 該方法可能會(huì)被棄用,可以手動(dòng)在github設(shè)置頁(yè)面設(shè)置新的token
try {
const response = await octokit.oauthAuthorizations.createAuthorization({
scope: ['user', 'public_repo', 'repo', 'repo:status'],
note: 'git-repo-cli: register new token'
});
const token = response.data.token;
if (token) {
conf.set('github_credentials.token', token);
return token;
} else {
throw new Error('Missing Token', 'Can not retrive token')
}
} catch(error) {
throw error;
}
}
}
其中@octokit/rest是node端與github通信主要的庫(kù)讯壶。
接下來(lái)就可以寫我們的接口了:
// index.js
const github = require('./lib/gitub_credentials');
commander.
command('check-token')
.description('Check user Github credentials')
.action(async () => {
let token = github.getStoredGithubToken();
if (!token) {
await github.setGithubCredentials();
token = await github.registryNewToken();
}
console.log(token);
});
最后料仗,在命令行中輸入如下命令:
node index.js check-token
它會(huì)先會(huì)在configstore
的默認(rèn)文件夾下~/.config
尋找token, 如果沒(méi)有發(fā)現(xiàn)的話,就會(huì)提示用戶輸入用戶名和密碼后新建一場(chǎng)新的token伏蚊。
有了token后罢维,就可以執(zhí)行g(shù)ithub的很多的操作,我們以新建倉(cāng)庫(kù)為例:
首先丙挽,先在inquirer.js
中新建askRepositoryDetails
用來(lái)獲取相關(guān)的repo
信息:
askRepositoryDetails: () => {
const args = require('minimist')(process.argv.slice(2));
const questions = [
{
type: 'input',
name: 'name',
message: 'Please enter a name for your repository:',
default: args._[1] || files.getCurrentDirectoryBase(),
validate: function(value) {
if (value.length) {
return true;
} else {
return 'Please enter a unique name for the repository.'
}
}
},
{
type: 'input',
name: 'description',
default: args._[2] || null,
message: 'Now enter description:'
},
{
type: 'input',
name: 'visiblity',
message: 'Please choose repo type',
choices: ['public', 'private'],
default: 'public'
}
];
return inquirer.prompt(questions);
},
askIgnoreFiles: (filelist) => {
const questions = [{
type: 'checkbox',
choices: filelist,
message: 'Please choose ignore files'
}];
return inquirer.prompt(questions);
}
接著,實(shí)現(xiàn)對(duì)應(yīng)的新建倉(cāng)庫(kù)匀借、新建.gitignore
文件等操作:
// create_a_repo.js
const _ = require('lodash');
const fs = require('fs');
const git = require('simple-git')();
const inquirer = require('./inquirer');
const gh = require('./github_credentials');
module.exports = {
createRemoteRepository: async () => {
const github = gh.getInstance(); // 獲取octokit實(shí)例
const answers = await inquirer.askRepositoryDetails();
const data = {
name: answers.name,
descriptions: answers.description,
private: (answers.visibility === 'private')
};
try {
// 利用octokit 來(lái)新建倉(cāng)庫(kù)
const response = await github.repos.createForAuthenticatedUser(data);
return response.data.ssh_url;
} catch (error) {
throw error;
}
},
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('.gitnore');
}
} else {
touch('.gitignore');
}
},
setupRepo: async (url) => {
try {
await git.
init()
.add('.gitignore')
.add('./*')
.commit('Initial commit')
.addRemote('origin', url)
.push('origin', 'master')
return true;
} catch (err) {
throw err;
}
}
}
最后颜阐,在index.js
文件中新建一個(gè)create-repo
的命令,執(zhí)行整個(gè)流程吓肋。
// index.js
commander
.command('create-repo')
.description('create a new repo')
.action(async () => {
try {
const token = await github.getStoredGithubToken();
github.githubAuth(token);
const url = await repo.createRemoteRepository();
await repo.createGitIgnore();
const complete = await repo.setupRepository(url);
if (complete) {
console.log(chalk.green('All done!'));
}
} catch (error) {
if (error) {
switch (error.status) {
case 401:
console.log('xxx');
break;
}
}
}
})
寫完代碼后凳怨,在命令行中執(zhí)行如下命令即可:
node index.js create-repo
總結(jié)
總的來(lái)說(shuō),利用node.js來(lái)實(shí)現(xiàn)命令行工具還是比較簡(jiǎn)單的是鬼。目前有很多比較成熟的工具庫(kù)肤舞,基本上常用的功能都能夠?qū)崿F(xiàn)。如果有需要自己造輪子均蜜,大家可以參考本文的實(shí)現(xiàn)思路李剖。
參考資料
代碼參考https://github.com/Tereflech17/musette
https://github.com/tj/commander.js/blob/HEAD/Readme_zh-CN.md
https://www.lynda.com/Node-js-tutorials/Building-Your-First-CLI-App-Node