2023-10-10
前端工程化
00
请注意,本文编写于 271 天前,最后修改于 269 天前,其中某些信息可能已经过时。

目录

必备基础
入门案例
yargs
commander
chalk 颜色库
ANSI转译码
chalk用法
chalk-cli
ora命令行loading
inquirer命令行交互
npm workspace
lerna
  • 脚手架开发是进阶前端架构师的必备技能,工作中可以使用脚手架开发各种提效工具
  • 脚手架的开发不局限于搭建项目,在项目构建、项目发布也需要搭建脚手架
  • NodeJS提供了脚手架开发的各种工具库,可以方便我们快速搭建脚手架
    • 内置库,如:fspathoschild_process……
    • 三方库:如:commanderyargsfs-extrarxjs……
    • 强大的包管理工具 npmyarnpnpm
  • NodeJS开发的脚手架也不局限于用在前端场景,也可以用于后端项目的搭建等。

本文围绕"NodeJs实现脚手架开发联调和发布"为主线,带你分析和讲解教授脚开发过程中的各个功能的的实现,包含以下内容:

  1. CLI是什么与Bash和Shell的区别
  2. NodeJS脚手架的实现原理
  3. 命令行参数解析及帮助文档的实现
  4. 命令行样式实现,颜色控制、进度条样式、表格绘制、列表绘制
  5. 命令行的交互实现,文本输入、键盘监听、checkbox、列表选择等交互

必备基础

关于前端包管理工具,你需要知道

  • 创建,npm init
  • 开发,npm scripts
  • 发布,npm publish
  • 应用,npm install
  • npm package.json相关字段

具体请参考我的另一篇文章《前端包管理工具详解》

下面介绍 shellbashcli的概念

  1. shell是什么?
  • shell是计算机提供用户与其他程序进行交互的接口
  • shell是一个命令行解释器,当你输入命令后,由shell进行解析后即交给操作系统内核进行处理
  • 图形操作系统也是shell,属于GUI Shell
  1. Bash是什么?
  • Bashshell的一种 可以通过cat /etc/shells 查看mac支持多少种shell
  • Bash通过纯文本的方式与操作系统内核进行交互,
  • Bash最大的优势就是简单易用,虽然他的显示效果不如GUI, 一旦熟悉后其操作效率远远大于GUI
  1. 什么是CLI?
  • 命令行界面(CLI)是一种基于文本界面(类似:MacOS终端、Windows cmd.exe),运行命令行程序
  • CLI接受键盘输入,在命令符号提示处理输入命令,然后由计算机执行并返回结果
  • CLIShell的一个实现,用于执行用户输入的命令

用一张图来总结总结

入门案例

@vue/cli为例从使用者的角度理解什么是cli

脚手架的本质是操作系统应用的客户端,比如 安装脚手架`

bash
npm i -g @vue/cli`

使用脚手架

bash
vue create vue-test-app -r https://registry.npm.taobao.org
  • 主命令: vue
  • command: create
  • command 有两个参数: 项目名称vue-test-app,镜像源-r https://registry.npm.taobao.org

我们先分析一下 @vue/cli

  • 为什么全局安装npm i -g @vue/cli后会添加一个全局命令vue ?
  • 执行 vue 命令时发生了什么?为什么 vue 指向一个 js 文件,我们却可以直接通过 vue 命令去执行它?
  1. www.npmjs.com拉取@vue/cli安装到本地,比如我的电脑是~/.nvm/versions/node/v16.20.2/lib/node_modules
  2. 读取./@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
  3. 根据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:将当前项目中的库文件依赖移除

yargs

关于命令行参数,我们可以根据process.argv获取,但有以下两点

  1. 参数的简写形式,参数值的类型 boolean、string、array等
  2. 帮助文档,参数校验提示文档的能 如果我们自己这些功能显然费时费力,yargs为我们做了这些事情。

基本使用 image.png

别名和参数校验 image.png

命令行宽度 image.png

如果想正好占满屏幕宽度 .wrap(cli.terminalWidth())

其他功能点

  • .epilogue('页角插入你想说的话')

  • 如果想让内容顶格写,可以引入dedent这个库 image.png

  • options image.png

  • .group分组 image.png

  • .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

commanderyargs功能一样,但更流行一些。 它的周下载量12708W+yargs只有8702W+

  • 使用yargs的案例有gulp-cli
  • 而使用commander的案例有vue-cliwebpack-clicreate-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();

image.png

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)

image.png

与yargs不同,commander调用错误,默认不会打印帮助信息。可通过program.outputHelp()开启

commander注册命令有两种方式

  1. command命令
js
program .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); });

image.png

  1. addCommand 注册子命令
js
const 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);

总结

  • 总体看commander的写法与帮助文档更直观
  • commander注册子命令的方式提供了一种插件扩展的机制,这也许是它更流行的原因。

chalk 颜色库

chalk用来处理CLI交互的样式的库

ANSI转译码

首先我们来了解一下 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格再打印

chalk用法

js
import 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')) // 默认的白色

chalk-cli

它是一个NodeJS命令行工具,通过npm i -g chalk-cli安装,安装后可以通过

  • chalk --help 文档
  • chalk --demo 快速学习

image.png

ora命令行loading

直接看一个案例

js
import 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);

spinner.gif

ora 也支持promise的写法

js
import {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: ['-', '\\', '|', '/', '-'], } }) })()

spinner.gif

inquirer命令行交互

inquirer的使用非常简单,一下示例你可以一次体验下

js
import 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) })

命令行交互原理

  1. 命令行交互没有web GUI交互那样灵活,它整体是一个控件,监听键盘事件,然后清屏,重新绘制,最后关闭输入流没返回Promise
  2. 底层依赖NodeJS基础readline/events/streamansi-escapesrxjs

npm workspace

有了前面 npm基础 + yargs/commander + chalk + ora + inquirer + nodejs基础,已经可以完成一个前端脚手架的开发了。但对于复杂功能的脚手架,需要一个好的架构分层与组织,npm workspace就提供了这种方式

注意:workspaces 是 npm v7.x 也就是 Node@15.0.0 新增的功能,所以请保持你的本地环境版本大于它们。

下面看一个示例

  1. mkdir workspaces
  2. npm init -y
  3. npm init --help 这里是现实workspaces的用法

image.png 4. 创建两个子包npm init --workspace my-chalk --workspace my-ora 一路回车(我只自定义了包名),会生成my-chalkmy-ora两个文件夹以及修改package.json

image.png

如果此时分别为两个子包添加依赖

  • npm install chalk -w my-chalk
  • npm install ora -w my-ora 这时候依赖会装到外层 node_modules下面

image.png

这是利用node_modules递归往上查找的特性,好处是,如果两个包共同的依赖,只会安装一份,节省空间。

我再往两个子包里增加一下内容

  • my-chalk/index.mjs
js
import chalk from 'chalk'; export default function() { console.log(chalk.red('hello a')); }
  • my-ora/index.mjs
js
import 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
  1. 发布 npm publish -ws会将所有包发布 注意:npm限制private包不能发布,你需要修改package.json
json
"publishConfig": { "access": "public" }
  1. 最后验证
  • npm i -g @guojw/my-cli 发布可能有延迟,如果遇到错误等待1分钟再试
  • my-cli

workspace.gif

lerna

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创建子项目的文当
  • 我们再创建一个cli的子项目采用esm规范 npx lerna create cli --access public --bin --es-module
  • 给所有子项目安装依赖npm install -ws
  • npx lerna link 将子项目pkg.bin写入根项目node_modules/.bin.目录,这样通过npx可以执行这些cli
  • npm 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自动发布所有子项目

询问你下一个包版本是多少 image.png  会自动更新所有子项目的版本号

发布成功 image.png

本文作者:郭郭同学

本文链接:

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