实现 node 和 shell 的交互
首先创建一个项目目录, 叫 learn-cli
mkdir learn-cli
然后, 创建一个 node 项目
cd learn-cli
npm init -y
用你喜欢的编辑器, 打开 learn-cli 目录, 在 package.json 中添加代码
"bin": {
"learn": "./bin/learn.js"
},
创建 bin 目录和 learn.js 文件, 在 learn.js 中添加以下代码. 文件头部的 #!/usr/bin/env node 这一句是告诉 shell 要以 node 来解析接下来的 learn 文件.
#!/usr/bin/env node
console.log('hello world')
通过终端进入到项目的根目录执行 npm link 如果出现一下提示说明 link 成功
这个命令的作用其实就是添加了两个软链(win 用户可以理解为快捷方式)到系统的环境变量下. 此时, 在命令行中输入 learn 即可输出 ‘hello world’
接收命令行传来的参数
使用原生的方式获取命令好传入的参数
调整 learn.js 文件为如下:
#!/usr/bin/env node
console.log(process.argv[2] + 'hello world')
再次回到终端执行 ‘learn 圈圈’, 即可得到输出 ‘圈圈hello world’
关于为什么要用 process.argv[2] 请参阅node文档
使用 commander 接收命令行传入的参数
使用原生的方式更适合简单的演示项目, 在这里推荐 commander 来处理参数问题
安装
npm i commander -S
官方示例
#!/usr/bin/env node
const program = require('commander')
program
.version('0.0.1')
.option('-C, --chdir <path>', 'change the working directory')
.option('-c, --config <path>', 'set config path. defaults to ./deploy.conf')
.option('-T, --no-tests', 'ignore test hook')
program
.command('setup')
.description('run remote setup commands')
.action(function() {
console.log('setup');
});
program
.command('exec <cmd>')
.description('run the given remote command')
.action(function(cmd) {
console.log('exec "%s"', cmd);
});
program
.command('teardown <dir> [otherDirs...]')
.description('run teardown commands')
.action(function(dir, otherDirs) {
console.log('dir "%s"', dir);
if (otherDirs) {
otherDirs.forEach(function (oDir) {
console.log('dir "%s"', oDir);
});
}
});
program
.command('*')
.description('deploy the given env')
.action(function(env) {
console.log('deploying "%s"', env);
});
program.parse(process.argv);
将官方示例粘贴到 learn.js 中, 执行命令 learn 圈圈 即可
这里输出 deploying “圈圈”
配置 eslint 开启 vscode 自动修复
- 安装 eslint
npm i eslint -D
- 初始化 eslint
在项目的根目录下执行 ./node_modules/.bin/eslint –init
根据需求在显示的配置选项中选择
- 编辑器安装 eslint 插件
vscode插件中搜索 eslint ,安装
完成后重启编辑器
开启 vscode 自动修复
- 打开 vscode 配置
- 在功能搜索框中输入 autofix
- 选上 auto fix on save
兼容 es6
原文作者的ps:以下配置用于普通的 node 项目没有问题, 但是在脚手架项目中会出现时而好用时而不好用的问题。目前的解决方案是, 把 es6 的模块导入规则手动改成了 commonjs 规范(实在不喜欢 babel 转码然后还给更改目录)。
引入 babel
npm i @babel/core babel-core babel-plugin-transform-es2015-modules-commonjs babel-polyfill babel-preset-env babel-preset-latest-node babel-register -S
在项目的根目录中添加 .babelrc
{
"presets": ["env"],
"plugins": ["transform-es2015-modules-commonjs"]
}
创建入口文件 index.js
require('babel-register');
const babel = require('@babel/core');
const babelPresetLatestNode = require('babel-preset-latest-node');
babel.transform('code();', {
presets: [[babelPresetLatestNode, {
target: 'current',
}]],
});
require('babel-polyfill');
require('./src');
创建 src 目录, 并添加 index.js a.js 文件
// index.js
import a from './a';
a.a();
// a.js
export default {
a() {
console.log('12345');
},
};
此时执行 node index.js 顺利打印出 12345
最后改造 bin/learn.js 内容如下:
#!/usr/bin/env node
require('../'); // 执行入口文件
此时在命令行中执行 learn, 打印出 12345
兼容 es6 成功
创建一个专门用于维护项目模板的项目组
为了不和自己平时写的各种辣鸡代码混杂在一起, 这里专门创建了一个 organization, 具体创建方法可百度
目录切换到刚刚创建的 organization 上

在组内创建一个项目,并添加一个文件, 上传到 github 并打好 tag
打tag
本地新建轻量级标签
git tag xx
提交到服务器
git push origin –tags
ps.下面没用 organization,直接创建了一个项目
通过 github 开放 api 获取项目信息
api.github.com 提供了一些 github 的开发 api
比如
curl https://api.github.com/users/用户名/repos 会得到个人所有repo的JSON格式列表
curl https://api.github.com/repos/用户名/仓库地址/tags 可以得到该仓库每个tag的消息列表
由于 github 开放 api 有请求次数限制(未授权每小时 60 次), 所以可能报告api被限制而无法使用, 解决方法是在请求中加入认证信息(authToken), 其实就是申请一个 github 授权 token 写到代码里. 申请 token 的步骤在这里;也可以使用你的账户+密码
使用账户+密码
curl -u “你的github账号:你的github密码” https://api.github.com/users/用户名/repos
使用token
申请token步骤

token可在请求时作为heade传输
headers: {
Authorization: 'token 0364152cf3b0d2a580508634ab0dfab9949bd3a1'
}
要注意的时,token内容需要先做处理,不能直接使用。否则上传脚手架代码到github时,检测到token明文就会将其删除
比如token为123456,可以写
const token = '6 5 4 3 2 1'
//...
headers: {
Authorization: `token ${token.split(' ').reverse().join('')}`,
},
//...
最终项目
本节用到的工具:
- 获取命令行指令 commander
- 命令行交互工具 inquirer.js
- 命令行显示加载中 ora
- ajax 封装库 axios
- 下载git项目 download-git-repo

// bin/learn.js
#!/usr/bin/env node
require('../');
// index.js
require('babel-register');
const babel = require('@babel/core');
const babelPresetLatestNode = require('babel-preset-latest-node');
babel.transform('code();', {
presets: [[babelPresetLatestNode, {
target: 'current',
}]],
});
require('babel-polyfill');
require('./src');
// src/index.js
// colors console.log 文本添加字体颜色, 美观
// import 'colors';
// 接收命令行参数, 提供基础信息提示功能
import commander from 'commander';
// 内部模块
import { existsSync } from 'fs';
import { resolve } from 'path';
import { version } from '../package.json';
commander.version(version)
.parse(process.argv);
// 获取命令行中传入的第一个参数
const [todo = ''] = commander.args;
// 判断如果 command 目录下是否存在用户输入的命令对应的文件
if (existsSync(resolve(__dirname, `command/${todo}.js`))) {
require(`./command/${todo}.js`);
} else {
console.log(
`
你输入了未知指令, 小哥哥我已经受不了挂了...
`.red,
);
process.exit(-1);
}
// command/download.js
// command 目录下存放的是我们整个项目中所有的命令文件, 不同的命令对应不同的文件, 体现了单一职责的设计. download 命令用到了我们上一节中提到的两个接口(即获取项目列表和获取版本号列表)
// 命令管理
import commander from 'commander';
// 命令行交互工具
import inquirer from 'inquirer';
// 命令行中显示加载中
import ora from 'ora';
import Git from '../tools/git';
class Download {
constructor() {
this.git = new Git();
this.commander = commander;
this.inquirer = inquirer;
this.getProList = ora('获取项目列表...');
this.getTagList = ora('获取项目版本...');
this.downLoad = ora('正在加速为您下载代码...');
}
run() {
this.commander
.command('download')
.description('从远程下载代码到本地...')
.action(() => { this.download(); });
this.commander.parse(process.argv);
}
async download() {
let getProListLoad;
let getTagListLoad;
let downLoadLoad;
let repos;
let version;
// 获取所在项目组的所有可开发项目列表
try {
getProListLoad = this.getProList.start();
repos = await this.git.getProjectList();
getProListLoad.succeed('获取项目列表成功');
} catch (error) {
console.log(error);
getProListLoad.fail('获取项目列表失败...');
process.exit(-1);
}
// 向用户咨询他想要开发的项目
if (repos.length === 0) {
console.log('\n可以开发的项目数为 0, 肯定是配置错啦~~\n'.red);
process.exit(-1);
}
const choices = repos.map(({ name }) => name);
const questions = [
{
type: 'list',
name: 'repo',
message: '请选择你想要开发的项目类型',
choices,
},
];
const { repo } = await this.inquirer.prompt(questions);
// 获取项目的版本, 这里默认选择确定项目的最近一个版本
try {
getTagListLoad = this.getTagList.start();
[{ name: version }] = await this.git.getProjectVersions(repo);
getTagListLoad.succeed('获取项目版本成功');
} catch (error) {
console.log(error);
getTagListLoad.fail('获取项目版本失败...');
process.exit(-1);
}
// 向用户咨询欲创建项目的目录
const repoName = [
{
type: 'input',
name: 'repoPath',
message: '请输入项目名称~',
validate(v) {
const done = this.async();
if (!v.trim()) {
done('项目名称不能为空~');
}
done(null, true);
},
},
];
const { repoPath } = await this.inquirer.prompt(repoName);
// 下载代码到指定的目录下
try {
downLoadLoad = this.downLoad.start();
await this.git.downloadProject({ repo, version, repoPath });
downLoadLoad.succeed('下载代码成功');
} catch (error) {
console.log(error);
downLoadLoad.fail('下载代码失败...');
}
}
}
const D = new Download();
D.run();
// tools/git.js
// 此文件是 git 相关的操作的文件, 由于脚手架的核心功能就是获取项目的 github 地址, 并下载, 所以我的 Git 类规划了以上几个功能, 获取项目列表 获取项目版本号列表 获取项目地址 下载项目
import download from 'download-git-repo';
import request from './request';
import { orgName } from '../../config';
class Git {
constructor() {
this.orgName = orgName;
}
getProjectList() {
return request(`/users/LinXiudong/repos`);
// 好像是用organization时的接口写法
// return request(`/orgs/${this.orgName}/repos`);
}
getProjectVersions(repo) {
return request(`/repos/LinXiudong/${this.orgName}/tags`);
// 好像是用organization时的接口写法
// return request(`/repos/${this.orgName}/${repo}/tags`);
}
downloadProject({ repo, version, repoPath }) {
return new Promise((resolve, reject) => {
// 下载到运行命令的目录的 test/tmp 下
// 这里先直接把路径写死了,前面不论选什么都会下该路径的内容
download(`LinXiudong/learn-cli`,'test/tmp', (err) => {
//download(`${this.orgName}/${repo}#${version}`, repoPath, (err) => {
if (err) reject(err);
resolve(true);
});
});
}
}
export default Git;
// tools/request.js
// axios封装
import axios from 'axios';
import { baseURL } from '../../config';
const instance = axios.create({
baseURL,
timeout: 1e4,
});
// Add a request interceptor
instance.interceptors.request.use(config => config,
error => Promise.reject(error));
// Add a response interceptor
instance.interceptors.response.use(response => response.data,
error => Promise.reject(error));
export default instance;
// config/index.js
// github 接口基础地址
export const baseURL = 'https://api.github.com';
// organization 名称,这里没建组织,就用仓库名称
export const orgName = 'test';
// github token
exports.token = 'b 2 b 4 c 7 8 c 5 9 d 2 1 6 6 e a d 0 2 1 a 5 4 1 a 2 7 b 0 e 4 0 d 8 6 9 4 0 c';
之所以要放在organization下是为了防止污染其他代码,不过也可以在写代码路径时稍微改下来处理
将代码发布到 npm 上
- 登录 npm 官网
- 把 npm 镜像替换为官方镜像 重点要考
npm config set registry http://registry.npmjs.org
发包完成后可执行下面的语句, 把镜像替换为淘宝镜像速度快的飞起.
npm config set registry https://registry.npm.taobao.org
- 运行 npm publish
如果报错,根据报错信息运行 npm adduser
成功后执行 npm i learn-cli -g 全局安装 learn-cli
林秀栋的技术博客