fs、path、os、child_process……commander、yargs、fs-extra、rxjs……npm、yarn、pnpm本文围绕"NodeJs实现脚手架开发联调和发布"为主线,带你分析和讲解教授脚开发过程中的各个功能的的实现,包含以下内容:
关于前端包管理工具,你需要知道
npm initnpm scriptsnpm publishnpm installpackage.json相关字段具体请参考我的另一篇文章《前端包管理工具详解》
下面介绍 shell、bash、cli的概念
shell是什么?shell是计算机提供用户与其他程序进行交互的接口shell是一个命令行解释器,当你输入命令后,由shell进行解析后即交给操作系统内核进行处理shell,属于GUI ShellBash是什么?Bash是shell的一种 可以通过cat /etc/shells 查看mac支持多少种shellBash通过纯文本的方式与操作系统内核进行交互,Bash最大的优势就是简单易用,虽然他的显示效果不如GUI, 一旦熟悉后其操作效率远远大于GUICLI?CLI)是一种基于文本界面(类似:MacOS终端、Windows cmd.exe),运行命令行程序CLI接受键盘输入,在命令符号提示处理输入命令,然后由计算机执行并返回结果CLI 是 Shell的一个实现,用于执行用户输入的命令用一张图来总结总结
以@vue/cli为例从使用者的角度理解什么是cli
脚手架的本质是操作系统应用的客户端,比如 安装脚手架`
bashnpm i -g @vue/cli`
使用脚手架
bashvue create vue-test-app -r https://registry.npm.taobao.org
vuecreatevue-test-app,镜像源-r https://registry.npm.taobao.org我们先分析一下 @vue/cli
npm i -g @vue/cli后会添加一个全局命令vue ?vue 命令时发生了什么?为什么 vue 指向一个 js 文件,我们却可以直接通过 vue 命令去执行它?www.npmjs.com拉取@vue/cli安装到本地,比如我的电脑是~/.nvm/versions/node/v16.20.2/lib/node_modules./@vue/cli/package.json中的bin字段在~/.nvm/versions/node/v16.20.2/bin/目录下创建软连接(快捷方式),文件名为pkg.bin的key字段,这里是vue,指向value 即bin/vue.js,实际路径为~/.nvm/versions/node/v16.20.2/lib/node_modules/@vue/cli/bin/vue.jsvue.js头部#!/usr/bin/env node的声明, 从环境变量里面寻找node,使用node执行该文件,即node vue.js这就是NodeJS脚手架的实现原理
在开发脚手架的过程中我们可以通过npm link来调试
理解 npm link:
npm link your-lib:将当前项目中 node_modules 下指定的库文件链接到 node 全局 node_modules 下的库文件npm link:将当前项目链接到 node 全局 node_modules 中作为一个库文件,并解析 bin 配置创建可执行文件理解 npm unlink:
npm unlink:将当前项目从 node 全局 node_modules 中移除npm unlink your-lib:将当前项目中的库文件依赖移除关于命令行参数,我们可以根据process.argv获取,但有以下两点
yargs为我们做了这些事情。基本使用

别名和参数校验

命令行宽度

如果想正好占满屏幕宽度 .wrap(cli.terminalWidth())。
其他功能点
.epilogue('页角插入你想说的话')
如果想让内容顶格写,可以引入dedent这个库

options

.group分组

.command()用法
ts#!/usr/bin/env node
const yargs = require('yargs/yargs');
const {hideBin} = require('yargs/helpers');
const arg = hideBin(process.argv);
const cli = yargs(arg);
cli.usage('Usage: cli-yargs [command] <options>')
.demandCommand(1, `A command ias required.
Pass --help to see all avaliable commands and options`)
.strict()
.alias('h', 'help')
.alias('v', 'version')
.command(
'init [name]',
'Do init a project',
(yargs) => {
yargs.option('name', {
type: 'string',
describe: 'Name of a project',
alias: 'n'
})
},
(argv) => {
console.log(argv);
// 示例命令 cli-yargs init -n sam-test
// 示例结果
/* {
_: [ 'init' ],
n: 'sam-test',
name: 'sam-test',
'$0': 'cli-yargs'
} */
}
)
// command还支持对象的形式
.command({
command: 'init [name]',
describe: 'Do init a project',
builder: (yargs) => {
yargs.option('name', {
type: 'string',
describe: 'Name of a project',
alias: 'n'
})
},
handler: (argv) => {
console.log(argv);
}
})
.argv
recommendCommands() 如果你敲错单词,它会给你推荐最接近的命令commander 与 yargs功能一样,但更流行一些。
它的周下载量12708W+,yargs只有8702W+。
yargs的案例有gulp-clicommander的案例有vue-cli、webpack-cli、create-react-app关于commander的使用还是推荐去看官方文章,很好懂。以下是我个人的笔记,你可以选择跳过这部分。
入门示例
commander.jsjs#!/usr/bin/env node
const commander = require('commander');
const pkg = require('../package.json')
const {program} = commander; // 单例模式
program
.version(pkg.version)
.parse();

option 的使用
js#!/usr/bin/env node
const commander = require('commander');
const pkg = require('../package.json')
// 手动创建一个program实例
const program = new commander.Command();
// 构造者模式
program
.name(Object.keys(pkg.bin)[0])
.usage('<command> [options]') // <>是必须字段 []是可选字段
.version(pkg.version)
.option('-d --debug', '是否开启调试模式', false)
.option('-e, --env <envName>', '获取环境变量')
.parse();
const options = program.opts();
console.log(options.debug)
console.log(options.env)

与yargs不同,commander调用错误,默认不会打印帮助信息。可通过program.outputHelp()开启
commander注册命令有两种方式
jsprogram
.command('clone <source> [destination]')
.description('clone a repository into a newly created directory')
.option('-f, --force', '是否强制克隆')
.action((source, destination, cmdObj) => {
console.log('clone command called', source, destination, cmdObj);
});

jsconst service = new commander.Command('service')
service
.command('start [port]')
.description('start service at some port')
.action((port) => {
console.log('do service start', port)
})
program.addCommand(service);
总结
chalk用来处理CLI交互的样式的库
首先我们来了解一下 ANSI转译码,它是一个标准,定义了命令行渲染标准,用来控制游标位置、颜色、字体样式等终端交互样式 ANSI escape code
js/**
* \x表示它是16进制数
* 1B是规定内容
* 31是是一种颜色 在 ANSI wiki上查询 FG是前景色,BG是后景色
* %s 会替换前面内容
**/
// console.log('\x1B[31m%s', "your name:")
// 注意 要关闭其他规则再去测试,否则规则会叠加
// console.log('\x1B[41m%s', "your name:") // 41是后景色
// 发现前面 设置的文字前景色没有清楚,即当前终端样式不会恢复
// 可以增加 \x1B[0m 进行复原
// console.log('\x1B[41m%s\x1B[0m', "your name:") // 41是后景色
console.log('\x1B[41m\x1B[4m%s\x1B[0m', "your name:") // 4是下划线
// m是渲染属性的意思 如果想操作光标 用 b
console.log('\x1B[2B%s', "your name2:") // 光标下移2行再打印
console.log('\x1B[2G%s', "your name2:") // 光标水平移动2格再打印
jsimport chalk from 'chalk';
console.log(chalk.red('hello guoguo'));
// 组合使用
console.log(chalk.red('red') + ' default ' + chalk.blue('blue'))
// 链式设置样式
console.log(chalk.red.bgGreen.bold('红色字体绿色背景加粗'))
// 支持多个参数, 使用空格拼接
console.log(chalk.red('hello', 'guoguo'))
// 还支持嵌套调用
console.log(chalk.red('hello', chalk.underline('guoguo')))
// 自定义颜色
console.log(chalk.rgb(255, 255, 0).underline('hello'))
console.log(chalk.hex('#ff00ff').bold('hello'))
console.log(chalk.hex('#ff00ff')('hello', 'guoguo'))
const error = (...text) => console.log(chalk.bold.hex('#ff0000')(text))
const warning = (...text) => console.log(chalk.hex('ffa500')(text))
error('Error!');
warning('Warning!');
/*
还可以通过level 执行支持的颜色范围
- `0` - All colors disabled.
- `1` - Basic 16 colors support.
- `2` - ANSI 256 colors support.
- `3` - Truecolor 16 million colors support.
*/
import {Chalk} from 'chalk'
const customChalk = new Chalk({level: 0})
console.log(customChalk.blue('hello')) // 默认的白色
它是一个NodeJS命令行工具,通过npm i -g chalk-cli安装,安装后可以通过
chalk --help 文档chalk --demo 快速学习
直接看一个案例
jsimport ora from 'ora';
const spinner = ora().start()
let percent = 0;
spinner.color = 'red';
spinner.text = `Loading ${percent} %`;
spinner.prefixText = 'Download xxx';
let task = setInterval(() => {
percent += 10;
spinner.text = `Loading ${percent} %`;
if (percent === 110) {
spinner.stop();
clearInterval(task);
}
}, 300);

ora 也支持promise的写法
jsimport {oraPromise} from 'ora';
(async function(){
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 3000)
})
await oraPromise(promise, {
successText: 'success!',
failText: 'failed!',
prefixText: 'Download XXX',
text: 'Loading',
spinner: {
interval: 120,
frames: ['-', '\\', '|', '/', '-'],
}
})
})()

inquirer的使用非常简单,一下示例你可以一次体验下
jsimport inquirer from 'inquirer';
inquirer.prompt([
/* {
type: 'input',
name: 'username',
message: 'please input your name',
default: 'nonname',
validate (v) {
return v.length >= 2 && v.length <=10
},
// transformer(v) { // 主要用来展示转换
// return `name[${v}]`
// },
// filter(v) { // 修改最终结果
// return `name[${v}]`
// },
}, */
/* {
type: 'number', // 输入非数字会展示NaN,结果转换为null
name: 'age',
message: 'please input your age',
}, */
/* {
type: 'confirm',
name: 'confirm',
message: 'Are you OK ?',
default: false
}, */
/* {
// type: 'list',
type: 'rawlist', // 与list相比交互形式更优化一些
name: 'select',
default:'b', // 必须有默认值,如果是一个错误值会转换为第一个值
choices: [
{value: 'a', name: 'A'},
{value: 'b', name: 'B'},
{value: 'c', name: 'C'},
]
}, */
/* {
type: 'expand', // 与list类似,快捷选择,输入H变成 rawlist
name: 'select2',
message: 'your choice',
default: 'red',
choices: [
{key: 'R', value: 'red'},
{key: 'G', value: 'green'},
{key: 'B', value: 'blue '},
]
}, */
/* {
type: 'checkbox', // 与list类似,快捷选择,输入H变成 rawlist
name: 'checkbox',
message: 'your choice',
default: [1,2],
choices: [
{value: 1, name: 'Stylelint'},
{value: 2, name: 'Prettier'},
{value: 3, name: 'Eslint'},
],
validate (v) {
return v.length >= 1
},
}, */
/* {
type: 'passward',
name: 'passward',
message: 'please input passward',
}, */
{
type: 'editor', // 创建一个临时文件,vi编辑,复制结果返回,删除临时文件
name: 'editor',
message: '请输入内容',
}
]).then(
answers => {
console.log(JSON.stringify(answers));
}
).catch(error => {
console.log(error)
})
命令行交互原理
readline/events/stream和ansi-escapes、rxjs有了前面 npm基础 + yargs/commander + chalk + ora + inquirer + nodejs基础,已经可以完成一个前端脚手架的开发了。但对于复杂功能的脚手架,需要一个好的架构分层与组织,npm workspace就提供了这种方式
注意:workspaces 是 npm v7.x 也就是 Node@15.0.0 新增的功能,所以请保持你的本地环境版本大于它们。
下面看一个示例
mkdir workspacesnpm init -ynpm init --help 这里是现实workspaces的用法
4. 创建两个子包npm init --workspace my-chalk --workspace my-ora
一路回车(我只自定义了包名),会生成my-chalk和my-ora两个文件夹以及修改package.json

如果此时分别为两个子包添加依赖
npm install chalk -w my-chalknpm install ora -w my-ora
这时候依赖会装到外层 node_modules下面
这是利用node_modules递归往上查找的特性,好处是,如果两个包共同的依赖,只会安装一份,节省空间。
我再往两个子包里增加一下内容
my-chalk/index.mjsjsimport chalk from 'chalk';
export default function() {
console.log(chalk.red('hello a'));
}
my-ora/index.mjsjsimport ora from 'ora';
export default function() {
const spinner = ora().start('loading');
setTimeout(() => {
spinner.stop();
}, 3000);
}
注意要修改pkg.main字段
5. 再创建一个子包分别调用它
npm init -w my-clinpm install @guojw/my-ora @guojw/my-chalk -ws my-cliindex.mjs内容如下js#!/usr/bin/env node
import chalk from '@guojw/my-chalk';
import ora from '@guojw/my-ora';
chalk();
ora();
package.jsonjson"bin": {
"my-cli": "index.mjs"
}
cd my-cli && node index.mjsnpm publish -ws会将所有包发布
注意:npm限制private包不能发布,你需要修改package.jsonjson"publishConfig": {
"access": "public"
}
npm i -g @guojw/my-cli 发布可能有延迟,如果遇到错误等待1分钟再试my-cli
npm workspace是最近几年的新方案,早期的解决方式是lerna。
lerna是一个monorepo项目的工具。 monorepo是一种将多个项目代码存储在一个仓库里的软件开发策略(mono 意为单一,repo 意为 仓库)
它除了实现了npm workspace分包的功能,还默认继承了mocha单元测试、执行任务
、发布时自动更新版本号。
mkdir lerna-study && cd lerna-study && npx lerna init lerna创建一个项目npx lerna -h查看命令行文档npx lerna create a创建一个子项目npx lerna add chalk packages/a 给子项目a增加chalk依赖并安装, 这一步会去安装所有依赖npx lerna create --help 查看lerna创建子项目的文当npx lerna create cli --access public --bin --es-modulenpm install -wsnpx lerna link 将子项目pkg.bin写入根项目node_modules/.bin.目录,这样通过npx可以执行这些clinpm set-script cli learn-cli 根项目注册调用npm run cli 试试看到这里为止lerna的脚手架已经做好了, 但是实际运行npm run cli还会遇到问题,需要调整一下代码 再次执行npx lerna bootstrap (等于 npm install -ws && npx lerna link)就可以运行了。
改动后的项目点这里
npx lerna test会执行所有子项目里的单元测试。npx lerna publish自动发布所有子项目询问你下一个包版本是多少
会自动更新所有子项目的版本号
发布成功

本文作者:郭郭同学
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!