脚手架搭建

npm i chalk commander download-git-repo inquirer ora request -S

commander: https://github.com/tj/commander.js
Inquirer: https://github.com/SBoudrias/Inquirer.js
chalk: https://github.com/chalk/chalk
Simple Git: https://github.com/steveukx/git-js#readme

2020.9.23 星期三 12:51

cli 搭建

简要

我们实现了一个脚手架的基本功能,大致分为三个流程(拉取模板->创建项目->收尾清理

1) commander 创建可执行的node命令
2) command:init: 复制模版

  1. 可提交到git,从服务端下载
  2. 本地缓存(和cli一起),不用从服务端下载最新
  3. 模版可以写死然后重写;
    或者使用模版文件ejs等
    ;可添加其他命令,init,built,publish,test 等。
    3) 创建bin命令;包括package.json中配置。
    4) 测试;npm link
    5) 发布到npm

示例配置
配置
规则

git commit提交规范:
版本规范:
standard

参考

npm i chalk commander download-git-repo inquirer ora request -S

1
2
3
4
5
6
7
8
9
10
11
12
const commander = require('commander');
const chalk = require('chalk');
const fs = require('fs-extra');
const path = require('path');
const request = require('request');
const progress = require('request-progress');
const exec = require('child_process').exec;
// 实际
import fs from 'fs-extra';
import str from 'underscore.string';
import ejs from 'ejs';
import readdir from 'fs-readdir-recursive';

目录

|– bin
|– co
|– co2
|– command
|– download.js
|– generator.js
|– utils
|– api.js
|– template
|– src
|– views/
|– utils/
|– /
|– mainl.js
|– package.json
|– index.js
|– package.json

创建bin命令

1
2
3
4
#!/usr/bin/env node
process.env.NODE_PATH = __dirname + '/../node_modules'

require('../index')

在脚手架的package.json中配置bin

“bin”: {
“easy-cli-react”: “index.js”
}

模板下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const downloadZipName = 'template.zip';
progress(request(templateUrl))
.on('progress', function (state) {
console.log(chalk.cyan(`[easy-cli-react]downloading project template (${Math.floor((state.percent * 100) || 0)​}%)`));
})
.on('error', function (err) {
console.log(chalk.red(`[error]${err}`));
})
.on('end', function () {

console.log(chalk.cyan('[easy-cli-react]downloading project template (100%)'));

// 省略...

// 解压并且重命名文件夹
const cmdStr = [
`unzip -o ${downloadZipName} -d ./`,
`rm ${downloadZipName}`,
`mv easy-template-react-master ${projectName}`
].join(' && ');
exec(cmdStr);

// 省略...
})
.pipe(fs.createWriteStream(downloadZipName));
// 修改package.json内的项目名
const packageJson = fs.readJsonSync(`${projectName}/package.json`);
packageJson.name = projectName;
fs.writeFileSync(
`${projectName}/package.json`,
JSON.stringify(packageJson, null, 4)
);
console.log(chalk.cyan('[easy-cli-react]done!!'));

模板样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|____.babelrc                           # babel配置
|____LICENSE # 项目协议
|____dist # 打包产出目录
|____README.md
|____.gitignore
|____package-lock.json
|____package.json
|____build
| |____plugins # 自定义的webpack插件
| | |____MyPlugin.js
| |____server # 脚本
| | |____buildServer.js # 打包脚本
| | |____devServer.js # 本地开发server
| |____webpackConfig # webpack配置
| | |____webpack.dev.config.js # 开发时配置
| | |____webpack.base.config.js # 基本配置
| | |____webpack.build.config.js # 打包时配置
| | |____devProxyConfig.js # 本地开发代理配置
|____postcss.config.js # postcss 配置
|____src # 项目代码
| |____template # html模板
| | |____template.html
| |____lib # 公用库代码
| | |____tools.js
| |____index.js # 入口文件js
| |____index.scss # 入口文件css

commander

文档:https://github.com/tj/commander.js/blob/master/Readme_zh-CN.md
github: https://github.com/tj/commander.js

实际

  1. program.parse(process.argv)后才可以访问program.args
    parse一般放在最后
  2. program监听不到事件,只好在command的action中 注入函数
    如果每一个命令前都需要,那就。。
    1. 可以监听不存在的command on(command:*)
    2. 可以监听到选项option on(option:small)
    3. 还可以on('--help')
      $_PS: Commander 继承自EventEmitter

      问题

      无法监听到command /#1090 /#1197

基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// const program = require('commander')
// const { program } = require('commander')
const { Command } = require('commander');
const program = new Command();
const pkg = require('./package.json');

/* # options */
program
.option('-d, --debug', 'output extra debugging')
.option('-s, --small', 'small pizza size')
.option('-p, --pizza-type <type>', 'flavour of pizza');

// console.log('program', program)
// 未执行parse前没有args属性
// console.log('program.args', program.args) // undefined
// program.parse(process.argv);
// console.log('program: after parase ', program)
// console.log('program.args', program.args) // [init]

// console.log(program.opts()); // { debug: true, small: undefined, pizzaType: undefined }
if (program.debug) console.log(program.opts());
console.log('pizza details:');
if (program.small) console.log('- small pizza size');
if (program.pizzaType) console.log(`- ${program.pizzaType}`);


/* # version */
program.version(pkg.version)
.usage('<command> [options]')

program.action(function() {
console.log('haha , here is action')
})

/* # command */
program
.command('init')
.description('生成一个新项目')
.alias('i')
.action(function() {
this.emit('command:init')
console.log('init action')
// require('./command/init')()

})

/* event listener */
// ## --help
program.on('--help', function(e){
console.log('on help*.')
})
// ## command
// ### error on unknown commands
program.on('command:*', function (operands) {
console.error(`hahhn error: unknown command '${operands[0]}'`);
console.log('operands',operands, this.name())
// const availableCommands = program.commands.map(cmd => cmd.name());
// mySuggestBestMatch(operands[0], availableCommands);
// process.exitCode = 1;
});

/* parse */
// 执行parse后才可以通过属性访问
program
.parse(process.argv)
if(!program.args.length) {
program.help()
}

inrequirer

github: https://github.com/SBoudrias/Inquirer.js

实际

返回的Promise,可以通过async/await 同步方式使用。不在回调中

1
2
3
4
5
6
7
8
9
10
11
export async function getText(message = '') {
return (
await inquirer.prompt([{
name: 'input',
message: message,
validate: (name) => {
return Boolean(name.trim());
}
}])
).input;
}

基础

使用脚手架的时候最明显的就是与命令行的交互,如果想自己做一个脚手架或者在某些时候要与用户进行交互,这个时候就不得不提到inquirer.js了。

type:表示提问的类型,包括:input, confirm, list, rawlist, expand, checkbox, password, editor;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const inquirer = require('inquirer');
const promptList = [
// 具体交互内容
];
inquirer.prompt(promptList).then(answers => {
console.log(answers); // 返回的结果
})
// ## input
const promptList = [{
type: 'input',
message: '设置一个用户名:',
name: 'name',
default: "test_user" // 默认值
},{
type: 'input',
message: '请输入手机号:',
name: 'phone',
validate: function(val) {
if(val.match(/\d{11}/g)) { // 校验位数
return val;
}
return "请输入11位数字";
}
}];
// ## confirm
const promptList = [{
type: "confirm",
message: "是否使用监听?",
name: "watch",
prefix: "前缀"
},{
type: "confirm",
message: "是否进行文件过滤?",
name: "filter",
suffix: "后缀",
when: function(answers) { // 当watch为true的时候才会提问当前问题
return answers.watch
}
}];

node方式

  1. fs.readSync + process.stdin 同步读取用户输入
  2. readline.question 获取用户输入
  3. 基于 nodejs 模块 readline-sync

readline-sync: https://www.npmjs.com/package/readline-sync

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// ## 1 fs.readSync + process.stdin
const fs = require('fs');
function readSyncByfs(tips) {
let response;
tips = tips || '> ';
process.stdout.write(tips);
process.stdin.pause();
response = fs.readSync(process.stdin.fd, 1000, 0, 'utf8');
process.stdin.end();
return response[0].trim();
}

console.log(readSyncByfs('请输入任意字符:'));
// ## 2 readline.question
const readline = require('readline');
function readSyncByRl(tips) {
tips = tips || '> ';
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

rl.question(tips, (answer) => {
rl.close();
resolve(answer.trim());
});
});
}
readSyncByRl('请输入任意字符:').then((res) => {
console.log(res);
});
// ##3 readline-sync
var readlineSync = require('readline-sync');

// Wait for user's response.
var userName = readlineSync.question('May I have your name? ');
console.log('Hi ' + userName + '!');

chalk

chalk: https://github.com/chalk/chalk

Terminal string styling done right

1
2
3
console.log(chalk.blue('Hello world!'));
log(chalk.blue('Hello') + ' World' + chalk.red('!'));
log(chalk.blue.bgRed.bold('Hello world!'));

ora

ora: https://github.com/sindresorhus/ora

1
2
3
4
5
6
7
8
9
10
11
12
const ora = require('ora');
const spinner = ora('Loading unicorns').start();
setTimeout(() => {
spinner.color = 'yellow';
spinner.text = 'Loading rainbows';
}, 1000);
spinner..stop()
// .succeed(text?) ,.fail(text?) ,...
/* # color of the text */
const ora = require('ora');
const chalk = require('chalk');
const spinner = ora(`Loading ${chalk.red('unicorns')}`).start();

Yeoman

除了上述方法,我们也可以直接通过大名鼎鼎的Yeoman来创建,不过个人觉得没必要,毕竟这玩意也不难。

yeman: https://github.com/yeoman/yeoman
yeoman: https://yeoman.io/

Simple Git

Simple Git: https://github.com/steveukx/git-js#readme
A lightweight interface for running git commands in any node.js application.

提交信息规范

1
2
3
4
5
6
7
8
9
10
11
npm install --save-dev husky
npm install --save-dev @commitlint/config-conventional @commitlint/cli

# 生成配置文件commitlint.config.js,当然也可以是 .commitlintrc.js
echo "module.exports = {extends: ['@commitlint/config-conventional']};" > commitlint.config.js

## 提交
git commit -m <type>[optional scope]: <description>

# 测试
echo 'foo: bar' | commitlint
1
2
3
4
5
"husky": {
"hooks": {
"commit-msg": "commitlint -e $HUSKY_GIT_PARAMS"
}
},

其他

  1. package.json 中的 bin 字段 一个 npm 模块,如果在 package.json 中指定了 bin 字段,那说明该模块提供了可在命令行执行的命令,这些命令就是在 bin 字段中指定的。
    package.json
    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    "bin": {
    "myapp": "./cli.js"
    },
    // 如果你的 npm 包只提供了一个可执行的命令.
    // "name": "my-program",
    // "version": "1.2.5",
    // "bin": "./path/to/program"
    }

程序安装后会可在命令行执行 myapp 命令,实际执行的就是指定的这个 cli.js 文件。
如果是全局安装,会将这个目标 js 文件映射到 prefix/bin 目录下,而如果是在项目中安装,则映射到 ./node_modules/.bin/ 目录下。

在安装第三方带有bin字段的npm,那可执行文件会被链接到当前项目的./node_modules/.bin中,在本项目中,就可以很方便地利用npm执行脚本(package.json文件中scripts可以直接执行:’node node_modules/.bin/myapp’);

  1. npm scripts 原理 npm 脚本的原理非常简单。每当执行npm run,就会自动新建一个 Shell,在这个 Shell 里面执行指定的脚本命令。因此,只要是 Shell(一般是 Bash)可以运行的命令,就可以写在 npm 脚本里面。
    比较特别的是,npm run新建的这个 Shell,会将当前目录的node_modules/.bin子目录加入PATH变量,执行结束后,再将PATH变量恢复原样。
    这意味着,当前目录的node_modules/.bin子目录里面的所有脚本,都可以直接用脚本名调用,而不必加上路径。比如,当前项目的依赖里面有 Mocha,只要直接写mocha test就可以了。
  1. #!/usr/bin/env node 到底是什么? shabang,shebang: sharp/hash/mesh;shell. bang;#!

当你输入一个命令的时候,npm是如何识别并执行对应的文件的呢?
简单的理解,就是输入命令后,会有在一个新建的shell中执行指定的脚本,在执行这个脚本的时候,我们需要来指定这个脚本的解释程序是node。

npm link

本地调试的时候,在根目录下执行npm link
即可把cli命令绑定到全局,以后就可以直接以cli作为命令开头而无需敲入长长的node cli之类的命令了。

knowledge is no pay,reward is kindness
0%