不需要直接读写文件,只要修改 configstore 对象,后面的事都帮你搞定了。
用法也很简单,创建一个实例,传入应用标识符就行了。例如:
`const Configstore = require('configstore');
const conf = new Configstore('ginit');
`
如果 configstore 文件不存在,它会返回一个空对象并在后台创建该文件。如果 configstore 文件已经存在,内容会被解析成 JSON,应用程序就可以使用它了。 你可以把 conf 当成简单的对象,根据需要获取或设置属性。刚才已经说了,你不用担心保存的问题,它已经帮你做好了。
提示:macOS/Linux 系统该文件位于 /Users/[YOUR-USERNME]/.config/configstore/ginit.json
与 GitHub API 通信
让我们写一个库,处理 GitHub token。新建文件 lib/github.js 并添加以下代码:
`const octokit = require('@octokit/rest')();
const Configstore = require('configstore');
const pkg = require('../package.json');
const _ = require('lodash');
const CLI = require('clui');
const Spinner = CLI.Spinner;
const chalk = require('chalk');
const inquirer = require('./inquirer');
const conf = new Configstore(pkg.name);
`
再添加一个函数,检查访问 token 是否已经存在。我们还添加了一个函数,以便其他库可以访问到 octokit(GitHub) 相关函数:
`...
module.exports = {
getInstance: () => {
return octokit;
},
getStoredGithubToken : () => {
return conf.get('github.token');
},
setGithubCredentials : async () => {
...
},
registerNewToken : async () => {
...
}
}
`
如果 conf 对象存在并且有 github.token 属性,就表示 token 已经存在。在这里我们把 token 值返回给调用的函数。我们稍后会讲到它。
如果 token 没找到,我们需要获取它。当然了,获取 OAuth token 牵涉到网络请求,对用户来说有短暂的等待过程。借这个机会我们可以看看 clui 这个包,它给控制台应用提供了强化功能,转菊花就是其中一个。
创建一个菊花很简单:
`const status = new Spinner('Authenticating you, please wait...');
status.start();
`
任务完成后就可以停掉它,它就从屏幕上消失了:
`status.stop();
`
提示:你也可以用update方法动态更新文字内容。当你需要展示进度时这会非常有用,比如显示完成的百分比。
完成 GitHub 认证的代码在这:
`...
setGithubCredentials : async () => {
const credentials = await inquirer.askGithubCredentials();
octokit.authenticate(
_.extend(
{
type: 'basic',
},
credentials
)
);
},
registerNewToken : async () => {
const status = new Spinner('Authenticating you, please wait...');
status.start();
try {
const response = await octokit.authorization.create({
scopes: ['user', 'public_repo', 'repo', 'repo:status'],
note: 'ginits, the command-line tool for initalizing Git repos'
});
const token = response.data.token;
if(token) {
conf.set('github.token', token);
return token;
} else {
throw new Error("Missing Token","GitHub token was not found in the response");
}
} catch (err) {
throw err;
} finally {
status.stop();
}
},
`
我们一步一步来看:
返回 token
你创建的任何 token,无论是通过人工还是 API,都可以在 这里看到。在开发过程中,你可能需要删除 ginit 的 access token ——可以通过上面的 note 参数辨认—— 以便重新生成。
提示:如果你的 Github 账户启用了双重认证,这个过程会稍微复杂点。你需要请求验证码(比如通过手机短信),然后通过 X-GitHub-OTP 请求头提供该验证码。更多信息请参阅 Github 开发文档
更新下index.js文件里的run()函数,看看效果:
`const run = async () => {
let token = github.getStoredGithubToken();
if(!token) {
await github.setGithubCredentials();
token = await github.registerNewToken();
}
console.log(token);
}
`
请注意,如果某个地方出错的话,你会得到一个 Promise 错误,比如输入的密码不对。稍后我们会讲到处理这些错误的方式。
创建仓库
一旦获得了 OAuth token,我们就可以用它来创建远程 Github 仓库了。
同样,我们可以用 Inquirer 给用户提问。我们需要仓库名称、可选的描述信息以及仓库是公开还是私有。
我们用 minimist 从可选的命令行参数中提取名称和描述的默认值。例如:
`ginit my-repo "just a test repository"
`
这样就设置了默认名称为my-repo ,默认描述为just a test repository
下面这行代码把参数放在一个数组里:
`const argv = require('minimist')(process.argv.slice(2));
// { _: [ 'my-repo', 'just a test repository' ] }
`
提示:这里只展示了 minimist 功能的一点皮毛而已。你还可以用它来解析标志位参数、开关和键值对。更多功能请查看它的文档。
接下来我们加上解析命令行参数的代码,并向用户提出一系列问题。首先更新lib/inquirer.js文件,在askGithubCredentials函数后面加上以下代码:
` ...
askRepoDetails: () => {
const argv = require('minimist')(process.argv.slice(2));
const questions = [
{
type: 'input',
name: 'name',
message: 'Enter a name for the repository:',
default: argv._[0] || files.getCurrentDirectoryBase(),
validate: function( value ) {
if (value.length) {
return true;
} else {
return 'Please enter a name for the repository.';
}
}
},
{
type: 'input',
name: 'description',
default: argv._[1] || null,
message: 'Optionally enter a description of the repository:'
},
{
type: 'list',
name: 'visibility',
message: 'Public or private:',
choices: [ 'public', 'private' ],
default: 'public'
}
];
return inquirer.prompt(questions);
},
`
接着创建lib/repo.js 文件,加上这些代码:
`const _ = require('lodash');
const fs = require('fs');
const git = require('simple-git')();
const CLI = require('clui')
const Spinner = CLI.Spinner;
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('Creating remote repository...');
status.start();
try {
const response = await github.repos.create(data);
return response.data.ssh_url;
} catch(err) {
throw err;
} finally {
status.stop();
}
},
}
`
根据获取的信息,我们就可以利用 Github 包 创建仓库了,它会返回新创建的仓库 URL。然后我们就可以把这个地址设置为本地仓库的 remote。不过还是先新建一个.gitignore 文件吧。
创建 .gitignore 文件
下一步我们将要创建一个简单的“向导”命令行,用来生成 .gitignore 文件。如果用户在已有项目路径里运行我们的应用程序,我们给用户列出当前工作目录的文件和目录, 以让他们选择忽略哪些。
Inquirer 提供的 checkbox 输入类型就是用来做这个的。

Inquirer’s checkboxes in action
首先我们需要做的就是扫描当前目录,忽略.git文件夹和任何现有的 .gitignore 文件。我们用 lodash 的 without 方法来做:
`const filelist = _.without(fs.readdirSync('.'), '.git', '.gitignore');
`
如果没有符合条件的结果,就没必要继续执行了,直接 touch 当前的.gitignore 文件并退出函数。
`if (filelist.length) {
...
} else {
touch('.gitignore');
}
`
最后,我们用 Inquirer’s 的 checkbox 列出所有文件。在 lib/inquirer.js 加上如下代码:
`...
askIgnoreFiles: (filelist) => {
const questions = [
{
type: 'checkbox',
name: 'ignore',
message: 'Select the files and/or folders you wish to ignore:',
choices: filelist,
default: ['node_modules', 'bower_components']
}
];
return inquirer.prompt(questions);
},
..
`
请注意,我们也可以提供默认忽略列表。在这里我们预先选择了 node_modules 和 bower_components 目录,如果存在的话。
有了 Inquirer 的代码,现在我们可以写 createGitignore() 函数了。在 lib/repo.js 文件里插入这些代码:
`...
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 操作
操作 Git 的方法有很多,最简单的可能就是使用 simple-git 了。它提供一系列的链式方法运行 Git 命令。
我们用它来自动化的重复性任务有这些:
push 工作目录到远端
在 lib/repo.js 中插入以下代码:
`...
setupRepo: async (url) => {
const status = new Spinner('Initializing local repository and pushing to remote...');
status.start();
try {
await git
.init()
.add('.gitignore')
.add('./*')
.commit('Initial commit')
.addRemote('origin', url)
.push('origin', 'master');
return true;
} catch(err) {
throw err;
} finally {
status.stop();
}
},
...
`
全部串起来
首先在 lib/github.js中写几个 helper 函数。一个用来方便地存取 token,一个用来建立 oauth认证:
`...
githubAuth : (token) => {
octokit.authenticate({
type : 'oauth',
token : token
});
},
getStoredGithubToken : () => {
return conf.get('github.token');
},
...
`
接着在 index.js里写个函数用来处理获取 token 的逻辑。在run()函数前加入这些代码:
`const getGithubToken = async () => {
//从 config store 获取 token
let token = github.getStoredGithubToken();
if(token) {
return token;
}
// 没找到 token ,使用凭证访问 GitHub 账号
await github.setGithubCredentials();
// 注册新 token
token = await github.registerNewToken();
return token;
}
`
最后,更新run() 函数,加上应用程序主要逻辑处理代码。
`const run = async () => {
try {
// 获取并设置认证 Token
const token = await getGithubToken();
github.githubAuth(token);
// 创建远程仓库
const url = await repo.createRemoteRepo();
// 创建 .gitignore 文件
await repo.createGitignore();
// 建立本地仓库并推送到远端
const done = await repo.setupRepo(url);
if(done) {
console.log(chalk.green('All done!'));
}
} catch(err) {
if (err) {
switch (err.code) {
case 401:
console.log(chalk.red('Couldn\'t log you in. Please provide correct credentials/token.'));
break;
case 422:
console.log(chalk.red('There already exists a remote repository with the same name'));
break;
default:
console.log(err);
}
}
}
}
`
如你所见,在顺序调用其他函数(createRemoteRepo(), createGitignore(), setupRepo())之前,我们要确保用户是通过认证的。代码还处理了异常,并给予了用户适当的反馈。
让 ginit 命令全局可用
剩下的一件事是让我们的命令行在全局可用。为此,我们需要在index.js文件顶部加上一行叫 shebang 的代码:
`#!/usr/bin/env node
`
接着在package.json 文件中新增一个 bin 属性。它用来绑定命令名称(ginit)和对应被执行的文件(路径相对于 package.json)。
`"bin": {
"ginit": "./index.js"
}
`
然后,在全局安装这个模块,这样一个可用的 shell 命令就生成了。
`npm install -g
强大的 Node.js 除了能写传统的 Web 应用,其实还有更广泛的用途。微服务、REST API、各种工具……甚至还能开发物联网和桌面应用。JavaScript 不愧是宇宙第一语言。
Node.js 在开发命令行工具方面也是相当方便,通过这篇教程我们可以来感受下。我们先看看跟命令行有关的几个第三方包,然后从零开始写一个真实的命令行工具。
这个 CLI 的用途就是初始化一个 Git 仓库。当然,底层就是调用了
git init,但是它的功能不止这么简单。它还能从命令行创建一个远程的 Github 仓库,允许用户交互式地创建.gitignore文件,最后还能完成提交和推代码。为什么要用 Node.js 写命令行工具
在动手之前,我们有必要知道为什么选择 Node.js 开发命令行工具。
最明显优势就是——相信你已经猜到了——它是用 JavaScript 写的。
另外一个原因是 Node.js 生态系统非常完善,各种用途的 package 应有尽有,其中就有不少是专门为了开发命令行工具的。
最后一个原因是,用
npm管理依赖不用担心跨平台问题,不像 Aptitude、Yum 或者 Homebrew 这些针对特定操作系统的包管理工具,令人头疼。注:这么说不一定准确,可能命令行只是需要其他的外部依赖。
动手写一个命令行工具: ginit
在这篇教程里我们来开发一个叫做 ginit 的命令行工具。我们可以把它当做高配版的
git init。什么意思呢?我们都知道,git init命令会在当前目录初始化一个 git 仓库。但是,这仅仅是创建或关联已有项目到 Git 仓库的其中一步而已。典型的工作流程是这样的:git init初始化本地仓库.gitignore文件可能还有更多步骤,为了演示我们只看关键部分。你会发现,这些步骤很多都是机械式、重复性的,为什么不用命令行来完成这些工作呢?比如复制粘贴 git 地址这种事情,能忍受手动操作?
ginit 可以做到:在当前目录创建 git 仓库,同时创建远程仓库(我们这里用 Github 演示),然后提供一个类似操作向导的界面来创建
.gitignore文件,最后提交文件夹内容并推送到远程仓库。可能这也节省不了太多时间,但是它确实给创建新项目带来了些许便利。好了,我们开始吧。
项目依赖
有一点是肯定的:说到外观,控制台无论如何也不会有图形界面那么复杂。尽管如此,也并不是说控制台一定是那种原始的纯文本丑陋界面。你会惊讶地发现,原来命令行也可以那么好看!我们会用到一些美化命令行界面的库: chalk 给输出内容着色, clui 提供一些可视化组件。还有更好玩的, figlet 可以生成炫酷的 ASCII 字符图案, clear 用来清除控制台。
输入输出方面,低端的 Readline Node.js 模块可以询问用户并接受输入,简单场景下够用了。但我们会用到一个更高端的工具—— Inquirer。除了询问用户的功能,它还提供简单的输入控件:单选框和复选框,这可是在命令行控制台啊,有点意外吧。
我们还用到 minimist 来解析命令行参数。
以下是完整列表:
还有这些:
开始
创建一个项目文件夹。
files.js — 基本的文件管理
inquirer.js — 命令行用户界面
github.js — access token 管理
repo.js — Git 仓库管理
先看
lib/files.js,这里需要完成:获取当前路径(文件夹名作为默认仓库名)
检查路径是否存在(通过查找名为
.git的目录,判断当前目录是否已经是 Git 仓库)看上去很简单直接,但这里还是有点坑的。
首先,你可能想用
fs模块的 realpathSync 方法获取当前路径:当我们在同一路径下运行应用时(即
node index.js),这没问题。但是要知道,我们要把这个控制台应用做成全局的,就是说我们想要的是当前工作目录的名称,而不是应用的安装路径。因此,最好使用 process.cwd:其次,检查文件或目录是否存在的推荐方法一直在变。目前的方法是用
fs.stat或fs.statSync。这两个方法在文件不存在的情况下会抛异常,所以我们要用try...catch。最后需要注意的是,当你在写命令行应用的时候,使用这些方法的同步版本就可以了。
整理下
lib/files.js代码,一个工具包就出来了:回到
index.js文件,引入这个文件:有了这个,我们就可以动手开发应用了。
初始化 Node CLI
现在让我们来实现控制台应用的启动部分。
为了展示安装的这些控制台输出强化模块,我们先清空屏幕,然后展示一个banner:
输出效果如下图:
接着运行简单的检查,确保当前目录不是 Git 仓库。很容易,我们只要用刚才创建的工具方法检查是否存在
.git文件夹就行了:提示:我们用了 chalk 模块 来展示红色的消息。
提示用户输入
接下来我们要做的就是写个函数,提示用户输入 Github 登录凭证。这个可以用 Inquirer 来实现。这个模块包含一些支持各种提示类型的方法,语法上跟 HTML 表单控件类似。为了收集用户的 Github 用户名和密码,我们分别用了
input和password类型。首先新建
lib/inquirer.js文件,加入以下代码:如你所见,
inquirer.prompt()向用户询问一系列问题,并以数组的形式作为参数传入。数组的每个元素都是一个对象,分别定义了name、type和message属性。用户提供的输入信息返回一个 promise 给调用函数。如果成功,我们会得到一个对象,包含
username和password属性。可以在
index.js里测试下:运行
node index.js:处理 GitHub 身份验证
下一步是创建一个函数,用来获取 Github API 的OAuth token。我们实际上是用用户名和密码换取 token的。
当然了,我们不能让用户每次使用这个工具的时候都需要输入身份凭证,而是把 OAuth token 存起来给后续请求使用。这个是就要用到 configstore 这个包了。
保存配置信息
保存配置信息表面上看起来非常简单直接:无需第三方库,直接存取 JSON 文件就好了。但是,configstore 这个包还有几个关键的优势:
它会根据你的操作系统和当前用户来决定最佳的文件存储位置。
不需要直接读写文件,只要修改 configstore 对象,后面的事都帮你搞定了。
用法也很简单,创建一个实例,传入应用标识符就行了。例如:
如果
configstore文件不存在,它会返回一个空对象并在后台创建该文件。如果configstore文件已经存在,内容会被解析成 JSON,应用程序就可以使用它了。 你可以把conf当成简单的对象,根据需要获取或设置属性。刚才已经说了,你不用担心保存的问题,它已经帮你做好了。提示:macOS/Linux 系统该文件位于
/Users/[YOUR-USERNME]/.config/configstore/ginit.json与 GitHub API 通信
让我们写一个库,处理 GitHub token。新建文件
lib/github.js并添加以下代码:再添加一个函数,检查访问 token 是否已经存在。我们还添加了一个函数,以便其他库可以访问到
octokit(GitHub) 相关函数:`... module.exports = { getInstance: () => { return octokit; }, getStoredGithubToken : () => { return conf.get('github.token'); }, setGithubCredentials : async () => { ... }, registerNewToken : async () => { ... } } `如果
conf对象存在并且有github.token属性,就表示 token 已经存在。在这里我们把 token 值返回给调用的函数。我们稍后会讲到它。如果 token 没找到,我们需要获取它。当然了,获取 OAuth token 牵涉到网络请求,对用户来说有短暂的等待过程。借这个机会我们可以看看 clui 这个包,它给控制台应用提供了强化功能,转菊花就是其中一个。
创建一个菊花很简单:
任务完成后就可以停掉它,它就从屏幕上消失了:
提示:你也可以用
update方法动态更新文字内容。当你需要展示进度时这会非常有用,比如显示完成的百分比。完成 GitHub 认证的代码在这:
我们一步一步来看:
用之前定义的
setGithubCredentials方法提示用户输入凭证试图获取 OAuth token之前采用 basic authentication
尝试注册新的 token
如果成功获取了 token,保存到
configstore返回 token
你创建的任何 token,无论是通过人工还是 API,都可以在 这里看到。在开发过程中,你可能需要删除 ginit 的 access token ——可以通过上面的
note参数辨认—— 以便重新生成。提示:如果你的 Github 账户启用了双重认证,这个过程会稍微复杂点。你需要请求验证码(比如通过手机短信),然后通过
X-GitHub-OTP请求头提供该验证码。更多信息请参阅 Github 开发文档更新下
index.js文件里的run()函数,看看效果:请注意,如果某个地方出错的话,你会得到一个
Promise错误,比如输入的密码不对。稍后我们会讲到处理这些错误的方式。创建仓库
一旦获得了 OAuth token,我们就可以用它来创建远程 Github 仓库了。
同样,我们可以用
Inquirer给用户提问。我们需要仓库名称、可选的描述信息以及仓库是公开还是私有。我们用 minimist 从可选的命令行参数中提取名称和描述的默认值。例如:
`ginit my-repo "just a test repository" `这样就设置了默认名称为
my-repo,默认描述为just a test repository下面这行代码把参数放在一个数组里:
提示:这里只展示了 minimist 功能的一点皮毛而已。你还可以用它来解析标志位参数、开关和键值对。更多功能请查看它的文档。
接下来我们加上解析命令行参数的代码,并向用户提出一系列问题。首先更新
lib/inquirer.js文件,在askGithubCredentials函数后面加上以下代码:` ... askRepoDetails: () => { const argv = require('minimist')(process.argv.slice(2)); const questions = [ { type: 'input', name: 'name', message: 'Enter a name for the repository:', default: argv._[0] || files.getCurrentDirectoryBase(), validate: function( value ) { if (value.length) { return true; } else { return 'Please enter a name for the repository.'; } } }, { type: 'input', name: 'description', default: argv._[1] || null, message: 'Optionally enter a description of the repository:' }, { type: 'list', name: 'visibility', message: 'Public or private:', choices: [ 'public', 'private' ], default: 'public' } ]; return inquirer.prompt(questions); }, `接着创建
lib/repo.js文件,加上这些代码:根据获取的信息,我们就可以利用 Github 包 创建仓库了,它会返回新创建的仓库 URL。然后我们就可以把这个地址设置为本地仓库的 remote。不过还是先新建一个
.gitignore文件吧。创建 .gitignore 文件
下一步我们将要创建一个简单的“向导”命令行,用来生成
.gitignore文件。如果用户在已有项目路径里运行我们的应用程序,我们给用户列出当前工作目录的文件和目录, 以让他们选择忽略哪些。Inquirer 提供的
checkbox输入类型就是用来做这个的。首先我们需要做的就是扫描当前目录,忽略
.git文件夹和任何现有的.gitignore文件。我们用 lodash 的 without 方法来做:如果没有符合条件的结果,就没必要继续执行了,直接
touch当前的.gitignore文件并退出函数。最后,我们用 Inquirer’s 的 checkbox 列出所有文件。在
lib/inquirer.js加上如下代码:请注意,我们也可以提供默认忽略列表。在这里我们预先选择了
node_modules和bower_components目录,如果存在的话。有了 Inquirer 的代码,现在我们可以写
createGitignore()函数了。在lib/repo.js文件里插入这些代码:一旦用户确认,我们把选中的文件列表用换行符拼接起来,写入
.gitignore文件。 有了.gitignore文件,可以初始化 Git 仓库了。应用程序中的 Git 操作
操作 Git 的方法有很多,最简单的可能就是使用 simple-git 了。它提供一系列的链式方法运行 Git 命令。
我们用它来自动化的重复性任务有这些:
运行
git init添加
.gitignore文件添加工作目录的其余内容
执行初次 commit
添加新创建的远程仓库
push 工作目录到远端
在
lib/repo.js中插入以下代码:全部串起来
首先在
lib/github.js中写几个 helper 函数。一个用来方便地存取 token,一个用来建立oauth认证:接着在
index.js里写个函数用来处理获取 token 的逻辑。在run()函数前加入这些代码:最后,更新
run()函数,加上应用程序主要逻辑处理代码。如你所见,在顺序调用其他函数(
createRemoteRepo(),createGitignore(),setupRepo())之前,我们要确保用户是通过认证的。代码还处理了异常,并给予了用户适当的反馈。让 ginit 命令全局可用
剩下的一件事是让我们的命令行在全局可用。为此,我们需要在
index.js文件顶部加上一行叫 shebang 的代码:`#!/usr/bin/env node `接着在
package.json文件中新增一个bin属性。它用来绑定命令名称(ginit)和对应被执行的文件(路径相对于package.json)。然后,在全局安装这个模块,这样一个可用的 shell 命令就生成了。
提示:Windows下也是有效的, 因为 npm 会帮你的脚本安装一个 cmd 外壳程序
更进一步
我们已经做出了一个漂亮却很简单的命令行应用程序用来初始化 Git 仓库。但是你还可以做很多事来进一步加强它。
如果你是 Bitbucket 用户,你可以适配该程序去使用 Bitbucket API 来创建仓库。有个 [Node.js API (https://www.npmjs.com/package/bitbucket-api) 可以帮你起步。你可能希望增加几个命令行选项,或者让用户选择使用 Github 还是 Bitbucket(用 Inquirer 再适合不过了),或者直接把 Github 相关的代码替换成 Bitbucket 对应的代码。
你还可以指定
.gitgnore文件默认列表,这方面preferences包比较合适,或者可以提供一些模板—— 可能是让用户选择项目类型。还可以把它集成到 .gitignore.io 。除此之外,你还可以添加额外的验证、提供跳过某些步骤的功能等等。发挥你的想象力,如果还有其他想法,欢迎留言评论!