fs
、path
、os
、child_process
……commander
、yargs
、fs-extra
、rxjs
……npm
、yarn
、pnpm
本文围绕"NodeJs
实现脚手架开发联调和发布"为主线,带你分析和讲解教授脚开发过程中的各个功能的的实现,包含以下内容:
关于前端包管理工具,你需要知道
npm init
npm scripts
npm publish
npm install
package.json
相关字段具体请参考我的另一篇文章《前端包管理工具详解》
下面介绍 shell
、bash
、cli
的概念
shell
是什么?shell
是计算机提供用户与其他程序进行交互的接口shell
是一个命令行解释器,当你输入命令后,由shell
进行解析后即交给操作系统内核进行处理shell
,属于GUI Shell
Bash
是什么?Bash
是shell
的一种 可以通过cat /etc/shells
查看mac
支持多少种shell
Bash
通过纯文本的方式与操作系统内核进行交互,Bash
最大的优势就是简单易用,虽然他的显示效果不如GUI
, 一旦熟悉后其操作效率远远大于GUI
CLI
?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
vue
create
vue-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.js
vue.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-cli
commander
的案例有vue-cli
、webpack-cli
、create-react-app
关于commander的使用还是推荐去看官方文章,很好懂。以下是我个人的笔记,你可以选择跳过这部分。
入门示例
commander.js
js#!/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 workspaces
npm init -y
npm init --help
这里是现实workspaces的用法
4. 创建两个子包npm init --workspace my-chalk --workspace my-ora
一路回车(我只自定义了包名),会生成my-chalk
和my-ora
两个文件夹以及修改package.json
如果此时分别为两个子包添加依赖
npm install chalk -w my-chalk
npm install ora -w my-ora
这时候依赖会装到外层 node_modules
下面这是利用node_modules递归往上查找的特性,好处是,如果两个包共同的依赖,只会安装一份,节省空间。
我再往两个子包里增加一下内容
my-chalk/index.mjs
jsimport chalk from 'chalk';
export default function() {
console.log(chalk.red('hello a'));
}
my-ora/index.mjs
jsimport ora from 'ora';
export default function() {
const spinner = ora().start('loading');
setTimeout(() => {
spinner.stop();
}, 3000);
}
注意要修改pkg.main
字段
5. 再创建一个子包分别调用它
npm init -w my-cli
npm install @guojw/my-ora @guojw/my-chalk -ws my-cli
index.mjs
内容如下js#!/usr/bin/env node
import chalk from '@guojw/my-chalk';
import ora from '@guojw/my-ora';
chalk();
ora();
package.json
json"bin": {
"my-cli": "index.mjs"
}
cd my-cli && node index.mjs
npm 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-module
npm install -ws
npx 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 许可协议。转载请注明出处!