目录

  1. 写之前请思考
  2. 开发时请遵循以下几点
  3. 交互设计上请遵循以下几点

在平时的开发中,我发现大家特别喜欢将很多自己常用或者公司常用的脚本封装成一个 cli,这虽然无可厚非,但是作为一个有强迫症的患者来说,我认为这其实做不好会导致 cli 非常不好用的。所以这里总结了下平时写 cli 的经验希望和大家分享下。

写之前请思考

  1. 我是不是为了 cli 而去写 cli,换句话我觉得这样比较 cool。如果是,那么请放弃这种想法。

  2. 这个 cli 一定会减轻工作量么?

    • 有很多公司喜欢将各种 webpack/eslint/babel 工具封装成一个 cli,那么这真的会降低使用者的工作量么?如果你封装的不好,相反会增加很大的工作量。最神奇的是,封装了这么多工具,配置文件却一个都没少,babelrc/eslintrc/prettierrc,那封装了有何用。

    • cli 的不透明性就会导致使用者永远都是有使用成本的,不论这个 cli 有多简单。他不会知道你是干了什么。所以能避免写一个 cli 就避免写一个 cli。那如果真的要写,有其他方案么?请看第三条

  3. 除了写 node cli 真的没有其他方案了么?

    • 大部分情况下,写 shell 的效率远远高于写 cli,将一个命令拆分成多个 shell,然后配合简单的帮助文档即可。如果用户不知道是做什么的,那么直接就可以去看源码。

    • 而且使用 git 管理 shell 也同样高效,比如将个人的一些脚本放到 private github 上,然后直接 clone 下来就可以用了。这样不需要每次都 npm publish,进而污染 npm。

    • 大部分情况下,去写 Makefile/Rakefile 同样可行。当然,Node 生态也有 Jake,不过不推荐用,因为要装 jake 包。

    • 如果你这个 cli 是作为一个项目的脚手架工具,那么是不是用 yeoman 或者 degit 这类工具更好?除非你的项目非常热门,功能自定义程度高,否则完全不需要自己去写一个脚手架。如果你只是觉得好玩,想写一个脚手架的话,那么请去看第一条,问问自己。

    • 如果是一个团队使用,那么除非有很大的普世性,那么用 git 管理同样比 npm 管理要强的多。

  4. 最后如果决定一定要写 cli 的话,有必要发布到 npm 么?

    • 是不是发布到自己的 scope 下也是一个不错的选择?

    • 是不是直接让别人通过 npm install [github.com/user/package](http://github.com/user/package) 也是一个不错的选择?

    • 是不是上传到公司 or 个人的私有 npm 更好?

开发时请遵循以下几点

  1. 请使用 npm

  2. 不要写死版本号,优先使用 ^

    • 这是因为有可能你的 cli 会被直接当做依赖安装到项目中,如果你写死了版本号,那么可能会装多分依赖。比如项目依赖了 yargs@^13.2.0 ,但是你锁死了 `yargs@13.1.0`,就会导致安装两个 yargs。
  3. 避免引入一些功能很大,而自己只用其中一部分的包

    • 因为没有 webpack 工具,无法进行 tree shaking,所以导致安装了很多比较大的包。比如你只是用了 lodashcloneDepp 却安装了整个包。优先使用 [lodash.xxx](http://lodash.xxx) 子包。
  4. 如果你使用了某些构建工具(babel,webpack,typescript,flow),那么请将构建之后的文件也加入代码仓库。

    • 比如有一个 src 目录存放了 ts 源文件,而 dist 存放了构建之后的 js 文件。那么很多人的选择往往是将这 dist 文件夹加入 gitignore。这样其实是不太好的,原因如下:

    • 第一,方便追踪变化,比如你自己添加了一些 debug 代码,这个时候构建之后发现没有问题。又把 debug 代码删除,当你提交的时候就可以很清楚的看到自己修改了之后没有构建代码

    • 第二,方便通过 unpkg.com 等工具访问

    • 第三,在版本开发依赖升级之后,可以很方便的看到改变的内容。一般使用者会放心升级 cli 之类的开发工具,所以这部分的质量需要我们自己来保证。

交互设计上请遵循以下几点

  1. Throw as possible,将可能的所有报错向上抛出,是一个好系统所必备的能力。但是在错误展示的时候,可以向用户隐藏部分调用栈信息等,只保留关键信息即可。

  2. 尽可能遵循 linux 下的 cli 规范。

  3. 不要让用户产生无畏的等待,通过添加进度条或者输出完成列表等告诉用户你依旧是在工作中的。

  4. 给予用户想查看所有日志的能力。可以通过 vvv vvv 或者 -log-level debug 来控制显示级别

  5. 对所有命令的描述都不要超过一行。不论屏幕宽度如何(一般默认 80),最好不要超过 70。如果需要大量描述,请尝试通过 man 或者单独的页面。

  6. 帮助是最好的文档,写好 cli 的 help 远比去写好一个文档网站要关键

注释和共享

目录

  1. 框架简介
  2. Nest.js 企业化当中的问题
  3. Nest.js 企业化的尝试
    1. 目录结构
    2. 配置管理
    3. 进程管理
  4. Iron.js

本人是一名 Node.js 实习生,在进入大搜车之后,有幸见识到 Akyuu.js 这个框架。但是这个框架是使用 Express + Callback 的方式,我不是很喜欢。在我的推荐以及社区的发展下,组长决定用 TS + Async/Await 来试一试。于是我也去了解了一下 TS 的后端框架有哪些,结果经过别人推荐,找到了 Nest.js 这个想法几乎和我一模一样的框架。

框架简介

因为我这个不是教程向,所以就不细讲,可以查看 Nest.js 官网。从我的感性角度来讲,简单说一下以下几个特点:

  • 去中心化路由。所有的路由通过装饰器与 Controller 绑定。简单、明了,学习成本低。

  • TypeScript/Rx.js 加持。智能补全,代码分析,静态类型等等优点。如果你只是个人用用的话,可能会觉得很全。但是放在企业当中使用,是非常大的优点。

  • 依赖注入。从 Angular 那里学习而来,但是进行了一些简化,但是完全够用。比如说简化掉了 deps。

  • 模块思想。Node 社区的后端框架,其实都被 Express 导向到了中间件的模式。而 Nest.js 却从 Angular 当中吸取到了模块的思想。不同的 Service、Controller、Component 组成不同的模块。模块之间可以相互依赖,也可以独立存在,这大大减少了测试和逻辑的复杂度。

  • 易于扩展。以往的框架,你能做的就是编写业务逻辑,而其他的你都很难去做到。于是传统的后端框架不得不引入了一套插件机制来增强框架的扩展性。但是 Nest.js 将插件的功能直接内置到了框架当中。传统的插件在这里可以认为就是一个模块,通过加载不同的模块来添加不同的功能。

  • Express 基石。有人会说,不是现在 Koa 才是更好的模型么?洋葱模型可以解决更多复杂的问题。没错,我不反对这个言论。但是我想说的是,Express 还是最简单最通用的方式,因为他不赖 Generator/Promise,只需要你又一个 Node.js 运行环境,支持 Callback 就可以了。(话说应该没有不支持 Callback 的 Node.js 环境吧,哈哈哈)不管怎么样,Express 的覆盖面还是比 Koa 要广不少。

  • 条条大路通罗马。那么有人就问了,那我要实现洋葱模型怎么办呢?我想说,办法总是会有的。而在 Nest.js 当中,通过 Interceptor ,可以很好的实现洋葱模型。也就是说你可以通过 Interceptor 来记录请求的耗时。

  • 同步代码。这里所说的同步代码并不是单单指的是 async/await。在很多支持 async/await 的框架中,如果你想返回值,如果是 Express ,你还是需要调用 resp.send(await getValue()),而 koa 也是需要调用 ctx.body = await getValue()。但是在 Nest.js 中,只需要 return await getValue() 即可。实现真正的同步编写业务逻辑代码。

  • 逻辑分层。其实很多功能,都是可以通过中间件来实现的。但是不同类型的功能有不同的需求,如果只是通过中间件来实现,势必会导致有一些重复的代码。于是 Nest.js 里面引入了 Pipe/Interceptor/Guard/ExceptionFilter 等逻辑层。不同的层里面处理相似的事情,比如说 Pipe 处理的是输入数据的转换。而 Interceptor 来实现洋葱模型。Guard 用于权限校验等拦截任务。ExceptionFilter 用来处理错误数据。这种分层带来的好处就是可以让代码更加清晰,主需要思考这个层需要做的事情,而不需要站在中间件的层面去考虑这个事情。

  • Validation。自带校验,而且和 TS 结合的非常完美,使用起来很舒服,请看教程

  • 输入参数的转换。这个其实是一个很方便的方面。有的时候你需要将输入的参数转换成一个类,这个时候你就可以通过 Validation 进行转换。你要是不想用自动转换,可以通过传统的手动转换的方式。

  • 测试功能完美。由于采用了依赖注入,所以测试简直简单的不得了。而且官方也提供了一系列测试工具,也能很好的解决单元测试的问题。

Nest.js 企业化当中的问题

  • 目录无约束。在企业当中,不对目录进行约束会导致代码越来越乱。从而降低了代码可维护性。

  • 没有配置管理功能。在框架开发中,配置往往是一个很重要的功能。比如说配置数据库的连接,配置监听的端口。

  • 没有进程管理。虽然有提供 @nestjs/cli,但是这个提供的仅仅是一个项目的创建的能力。

  • 部分文档讲解不详细,会提高入门的门槛。

不过总的来说,前面几点也正是 Nest.js 灵活性的保证。但是我们真正在开发当中,还是需要一种合理的约束来保证开发的统一。

Nest.js 企业化的尝试

那么我们这里针对上面的几个问题,尝试采用一些方式来进行约束。

目录结构

我们对项目指定如下的规则:

  • 全部通过 TypeScript 书写,并且全部位于 src 目录下

  • 入口文件是 main.ts 如果没有特殊情况,不动这个文件

  • 配置放在 src/config 文件夹下

  • 所有的 Service/Controller/Logic/Component 等都挂载到 MainModule 下。

  • 其中 module 文件夹存放自定义的 Module,或者说希望独立成模块但是还没有完全独立出来的。其中目录结构和这个项目目录结构类似

  • boot 文件夹是项目启动代码的时候执行的,这部分在 Nest.js 当中没有给出。我这里打算添加这个功能,但是还没有想好具体的实现形式,所以待定。

  • interface/enum 等数据随着对应的 service 导出。不另做说明。比如说 car.service.ts 除了可以导出 CarService 类以外,还可以导出 CarType enum。

  • dest 文件夹是编译之后的文件,可以直接输入 node dest/main.js 运行。

  • 命名规则

    • 所有的文件除了 main.ts 和类文件以外,都要添加类型后缀,比如说 user.model.ts car.controller.ts google.logic.ts。但是比如说只是一个 Car 类,那么可以直接命名成 car.ts

    • 不允许通过 export default 导出数据。一方面是为了方便导入的时候保证命名的统一,另一方面可以随时导出 interface/enum 等内容。

    • 所有的测试文件后缀名都以 .spec.ts.test.ts 结尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|-- dest
|--- ...
|-- src
|-- config
|-- controller
|-- model
|-- service
|-- logic
|-- component
|-- boot
|-- module
|-- module-name
|-- config
|-- index.ts
|-- module-name.module.ts
|-- main.ts
|-- main.module.ts

配置管理

我目前初步的想法是通过提供一个 ConfigModule 暴露出一个 ConfigService 来提供配置的获取和查看。

在某些情况下,可能需要多级配置,模块级别的配置,应用级别的配置。那么 ConfigService 可以在获取配置的时候自动合并这些规则。

进程管理

现在已经是 18 年了,不用 Docker 你真的对得起自己么?很明显是对不起的。所以进程管理这一块,我们就交给 Docker 来处理。包括启动、停止、重启、日志等,都交给 Docker。

于是启动命令就可以简化成 node dest/main.js 即可。

那么你可能会想到,如果一个 Docker 环境给你分配了两个 u,那岂不是会浪费一个 u。理论上是的,那么你就可以通过 pm2 啊啥的自己去管理吧,哈哈哈,不管。

Iron.js

说了这么多,把上面的内容都沉淀下来,我得要给他取个名字,于是我就取成了 Iron。为啥叫 Iron 呢?因为 Iron Man。那为啥因为 Iron Man 呢?因为他制作的盔甲可以自由拆分,自动拼合。非常适合我们这个项目的形态。

不过这个项目什么时候能沉淀下来,看我心情了。不过定个时间线吧,就在 4 月底,争取搞定。

因为这里面最大的问题就是配置的问题,需要深入依赖注入,所以会麻烦一些。但是其他的方面更多的只是一种约束吧。

这就是我用 Nest.js 一周以来的心得。暂时就想到这么多,更多的内容等我后面再分析吧。

写完睡觉,答应女票了,啦啦啦~

注释和共享

目录

  1. 简介
  2. 光速开始
    1. .argv 一切的开始,简单的不要不要的
      1. 普通参数
      2. 简写参数
      3. 全写参数
      4. 结果合并
    2. 我就要你在我的身边,.demandOption(key, msg)**
    3. 啥?你嫌我太长?还是太短:).alias
    4. 你要我怎样我就怎样,.boolean .array .number .count .choices
      1. .array(key)
      2. .boolean(key)
      3. .number(key)
      4. .count(key)
      5. .choices(key, list)
    5. 听说你和别人有千丝万缕的关系:( .conflicts .implies
    6. 大家在一起吧 :) .option .options
      1. 有用但是很简单其余参数
      2. 小弟来了 (-_-) **.command**
      3. 这个位置是你的,别人抢不走 [arg1] <arg2>
      4. 默认命令
      5. 方便一点 .commandDir
    7. 从别的地方来 .config .env .coerce
  3. 总结

简介

话说 yargs 是什么呢?简单来说,就是用来解析 cli 参数的。话不多说,我们来简单了解一下。

光速开始

.argv 一切的开始,简单的不要不要的

使用这个最简单的方式就是直接调用 .argv 这个 getter,他会自动对 process.argv 进行解析。并返回解析后的对象。

1
2
3
// argv.js
const argv = require('yargs').argv;
console.dir(argv)
1
2
# node argv.js -v --version --name whos
{ _: [], v: true, version: true, name: 'whos', '$0': 'argv.js' }

什么缩写,什么参数,统统搞定。是不是 so easy。

在默认情况下,所有的参数只有三种值,BooleanStringArray<Boolean|String

而且 $0 代表当前脚本的名称,这个就不多讲了

参数后面可以使用空格或者 = 。例如 -d=ok —name=bill

普通参数

如果参数没有 - 开头,那么将它放入 _ ,简称为普通参数

1
2
# node argv.js a b c
{ _: [ 'a', 'b', 'c' ], '$0': 'argv.js' }

简写参数

如果参数只有一个 - 开头,那么后面的参数为缩写参数,缩写参数的值默认设置成 true

1
2
# node argv.js -a -b -c
{ _: [], a: true, b: true, c: true, '$0': 'argv.js' }

同时,yargs 支持将缩写参数合并在一起书写。

1
2
# node argv.js -abc
{ _: [], a: true, b: true, c: true, '$0': 'argv.js' }

效果和上面是一样的。

如果缩写参数后面跟着普通参数,那么缩写参数的值就会自动设置成普通参数的值而不再是 true

1
2
# node argv.js -a haha -b lala -c hehe
{ _: [], a: 'haha', b: 'lala', c: 'hehe', '$0': 'argv.js' }

那么有人问了,如果我这样写会怎么样? -abc hahaha,let’s try.

1
2
# node argv.js -abc hahaha
{ _: [], a: true, b: true, c: 'hahaha', '$0': 'argv.js' }

结果显示,其实就和

1
# node argv.js -a -b -c hahaha

是一样的,可以见得,代码中其实就是将 -abc 拆成了 -a -b -c 进行解析的。

全写参数

除去上面两种参数,就剩下全写参数(不要吐槽为啥叫全写参数,因为实在是不知道该叫什么名字)

全写参数和缩写参数差不多,只不过他不能合并成一个书写,其他都是一样的

1
2
# node argv.js --version --laugh haha
{ _: [], version: true, laugh: 'haha', '$0': 'argv.js' }

结果合并

作为一个好 Module ,怎么会没有考虑到下面这种奇葩情况呢?

1
# node argv.js -a -a -a -a -a -a -a

大家猜猜会是什么结果 :) 此处略过 10000 秒。

1
2
3
{ _: [],
a: [ true, true, true, true, true, true, true ],
 '$0': 'argv.js' }

没错,yargs 将每一个参数单独处理,然后最后合并成了一个数组,是不是很有意思,也就是说你可以写出下面的东东。

1
2
3
4
# node argv.js --fuck whose --fuck your --fuck daddy --fuck
{ _: [],
fuck: [ 'whose', 'your', 'daddy', true ],
 '$0': 'argv.js' }

最简单的模式,也是最有趣的模式,值得去玩。

我就要你在我的身边,.demandOption(key, msg)**

如果你需要某个参数一定存在,这怎么办呢?难道要自己手动 if 一下,那真的好蠢啊。

.demandOption 就是这么来了

1
2
3
// demand.js
const argv = require('yargs').demandOption('baby').argv
console.dir(argv)

baby 在,世界一切太平,不管他是怎么在我的身边的。

1
2
3
4
5
6
# node demand.js --baby
{ _: [], baby: true, '$0': 'demand.js' }
# node demand.js --baby I
{ _: [], baby: 'I', '$0': 'demand.js' }
# node demand.js --baby --baby --baby --baby I
{ _: [], baby: [ true, true, true, 'I' ], '$0': 'demand.js' }

baby 不在,世界爆炸(exit code != 0)

1
2
3
4
5
# node demand.js
Options:
 --baby                                     [required]

Missing required argument: baby

.demandOption(key, msg)key 支持数组和字符串,分别表示单个和多个 required 的参数。而第二个参数值在没有满足条件的时候显示的文字。

啥?你嫌我太长?还是太短:).alias

俗话说的好,参数太长怎么办,变短一点喽

其实是我自己说的,可以给一个命令取一个别名,不管是变长还是变短,都很简单。

1
2
3
// alias.js
const argv = require('yargs').alias('long', ['l', 'lo']).alias('short', 's').argv
console.dir(argv)
1
2
3
4
5
6
7
8
# node alias.js -l --long --lo -s --short
{ _: [],
l: [ true, true, true ],
long: [ true, true, true ],
lo: [ true, true, true ],
s: [ true, true ],
short: [ true, true ],
 '$0': 'alias.js' }

可以看到 l lo long 是一样的,s short 是一样的,可长可短,自由随意。

你要我怎样我就怎样,.boolean .array .number .count .choices

有的时候,需要某些参数是固定的格式,而不是其他的方式,那么就需要这些方法来描述一个参数的类型。这些参数对于 alias 之后的参数同样也是可以的。

.array(key)

顾名思义,直接将参数的类型设置为数组,他会将后面所有的非普通参数作为当前参数的值。

1
2
3
// array.js
const argv = require('yargs').array('girls').argv
console.dir(argv)
1
2
3
4
5
# node array.js --girls Abby Aimee --stop --girls Alisa Angelia Amanda
{ _: [],
girls: [ 'Abby', 'Aimee', 'Alisa', 'Angelia', 'Amanda' ],
 stop: true,
 '$0': 'array.js' }

.boolean(key)

将参数类型设置为 Boolean 类型。如果后面的类型不是 Boolean 类型(truefalse),那么将不会设置为当前参数的值,并且当有多个的时候,不会合并成数组。

1
2
3
// boolean.js
const argv = require('yargs').boolean('love').argv
console.dir(argv)
1
2
3
4
# node boolean.js I --love you and --love again
{ _: [ 'I', 'you', 'and', 'again' ],
love: true,
 '$0': 'boolean.js' }

.number(key)

将参数类型设置为 Number 类型。基本规则如下:

  1. 如果没有填写值,那么默认是 undefined

  2. 如果设置的值不合法,那么是 NaN

  3. 否则是格式化为数字,使用 Number 构造方法

1
2
3
// number.js
const argv = require('yargs').number(['bust', 'waist', 'hips', 'height']).argv
console.dir(argv)
1
2
# node number.js --bust --waist 24 --hips zz
{ _: [], bust: undefined, waist: 24, hips: NaN, '$0': 'number.js' }

.count(key)

统计一下这个参数被使用了多少次,使用 .count 之后,参数默认就变成了 Boolean 类型,但是只统计他出现的次数。经常用来作为设置 debug 的输出级别。

1
2
3
// count.js
const argv = require('yargs').count('v').count('people').argv
console.log(argv)
1
2
# node count.js -v -vv --people --people false
{ _: [], v: 3, people: 2, '$0': 'count.js' }

.choices(key, list)

设置某个参数只能为某些值,可以和number boolean count 组合。

其本质是 indexOf 操作,也就是 === 做比较操作,所以这也就是为啥 array 不能和他匹配的原因。

1
2
3
4
5
6
7
// choices
const argv = require('yargs')
.choices('look', ['beatuify', 'oh, god'])
.choices('time', [1,2,3,4]).number('time')
.choices('many', [1,2]).count('many')
.argv
console.dir(argv)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# node choices.js --look "oh, god"
{ _: [], look: 'oh, god', '$0': 'choices.js' }

# node choices.js --look no
Invalid values:
Argument: look, Given: "no", Choices: "beatuify", "oh, god"

# node choices.js --time 1
{ _: [], time: 1, '$0': 'choices.js' }

# node choices.js --time 5
Invalid values:
Argument: time, Given: 5, Choices: 1, 2, 3, 4

# node choices.js --many --many
{ _: [], many: 2, '$0': 'choices.js' }

# node choices.js --many --many --many
Invalid values:
Argument: many, Given: 3, Choices: 1, 2

听说你和别人有千丝万缕的关系:( .conflicts .implies

简单一说:

  • .implies(我, 她) 有我先有她,有她不一定有我

  • .confilcts(我, 他) 有我没他,有他没我

如果两个都存在在一个参数上面的时候,implies 优先级会更高。

1
2
3
4
5
6
// imcon.js
const argv = require('yargs')
.conflicts('me', 'him')
.implies('me', 'her')
.argv
console.dir(argv)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# node imcon.js --me --him --her
Arguments me and him are mutually exclusive

# implies 有更高的优先级
# node imcon.js --me --him
Implications failed:
me -> her

# node imcon.js --me
Implications failed:
me -> her

# node imcon.js --me --her
{ _: [], me: true, her: true, '$0': 'imcon.js' }

# node imcon.js --him --her
{ _: [], him: true, her: true, '$0': 'imcon.js' }

大家在一起吧 :) .option .options

其实就是将上面的的所有的命令合并成一个 object,里面的 key 就是对应的函数名,而值就是参数。只不过 .options 是很多 .option 的集合。

这个就请看官网的例子源码

有用但是很简单其余参数

  • .default .defaults 设置默认参数值

  • .describe 对参数的描述

  • .usage 设置命令的提示的使用方法

  • .help 设置帮助的指令,添加 -help ,但是没有 h ,需要手动添加,可以选择是否添加 help 子命令

  • .group 分组,比如可以设置启动参数为一组,停止参数为一组,只是看起来比较舒服一些,并不影响什么内容。

  • .normalize 对参数的值用 path.normalize

  • .version 添加版本显示参数 -version,不过不添加缩写参数

  • .wrap 设置信息输出的最大的列宽度,比如说 -help 显示帮助参数。.wrap(null) 取消列宽限制,.wrap(require('yargs').terminalWidth()) 设置为当前宽度。默认是 Math.min(80, windowWidth

小弟来了 (-_-) **.command**

最简单的就是想实现类似 git 的那样的带有子命令的命令行操作,那么就需要这个东西。

他有如下的参数:

  • .command(cmd, desc, [builder], [handler])

  • .command(cmd, desc, [module])

  • .command(module)

  • builder 是构造器,可以是 Object|yargs => {},如果是对象,那么和 .options 是一样的。如果是函数,参数是 yargs 可以通过上面的函数添加参数。

  • handler 是处理器,当解析完成后,传入解析的结果,此时可以对结果进行处理。

  • module 最简单了,就是有

    • command 命令名

    • aliases 别名

    • describe 描述

    • builder 构造器

    • handler 处理器

当匹配到一个命令的时候, yargs 会做如下处理:

  1. 把当前命令输入到当前作用域中

  2. 清空所有的非全局的配置

  3. 如果传入了 builder,就通过其设置当前命令

  4. 解析和验证参数

  5. 如何一切正常,那么运行 handle,如果传入了的话

  6. 从当前作用域中弹出

这个位置是你的,别人抢不走 [arg1] <arg2>

有的时候希望命令必须要接受一个参数,或者接受一个可选参数,那么可以对命令使用 <>[] 设置他的位置。<> 表示这个命令必须要有,[] 表示这个参数可选。

有如下规则:

  • 通过 | 设置别名,例如 [name|username] ,在最后的解析中,nameusername 是一样的。

  • 最后一个可选参数支持添加 变成可变参数,例如 downloadto <from> [to…] 那么 to 是一个数组,并且必须要是命令中的最后一个可选参数才能变成可变参数。

1
2
3
4
5
6
// like.js
const argv = require('yargs')
.command('like <who>', 'you like who', {}, arg => console.dir(arg))
.command('dislike [who]', 'you dislike who', {}, arg => console.dir(arg))
.argv
console.dir(argv)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# node like.js like you
{ _: [ 'like' ], '$0': 'like.js', who: 'you' }
{ _: [ 'like' ], '$0': 'like.js', who: 'you' }

# node like.js like
like.js like <who>

Not enough non-option arguments: got 0, need at least 1

# node like.js dislike
{ _: [ 'dislike' ], '$0': 'like.js' }
{ _: [ 'dislike' ], '$0': 'like.js' }

# node like.js dislike you
{ _: [ 'dislike' ], '$0': 'like.js', who: 'you' }
{ _: [ 'dislike' ], '$0': 'like.js', who: 'you' }

默认命令

有的时候当没有任何命令匹配到的时候,希望有一个默认匹配的,那么可以用 * 代替普通命令的位置。

1
2
3
4
5
// defaultCommand.js
const argv = require('yargs')
.command('*', 'default command', {}, () => console.log('called command'))
.argv
console.dir(argv)
1
2
3
# node defaultCommand.js --name
called command
{ _: [], name: true, '$0': 'defaultCommand.js' }

方便一点 .commandDir

表示直接从文件夹中动态加载命令。详情请参考文档

从别的地方来 .config .env .coerce

写到这里,作者累了,所以:

  • .config 动态的从命令行中接受一个 json 文件路径,并自动加载。 doc

  • .env 设置环境变量的前缀,自动将这些前缀的环境变量去掉前缀,使用小驼峰和下划线方式加载。doc

  • .coerce 获取到变量值之后转化成别的值。doc

还有很多细节的,不过我觉得文档挺详细的,我就不多说了。

总结

感觉还是不错的,接口很简单,也通俗易懂。相比 commander 是两种不同的风格。commander 上手简单,但是前置知识有一些,而 yargs 相比前置知识的要求比较少,而且更加灵活。

注释和共享

Use ES6 With Babel6 in Nodejs

发布在 Node.js

目录

  1. 什么是Babel
    1. ES6
    2. Babel
  2. babel5 和 babel6 的区别
  3. Quick Start
    1. 建立空文件夹 babel6
    2. 安装Babel6
    3. require hook
    4. 安装插件
    5. 书写优雅的ES6代码
    6. Run it
  4. 内容解释
    1. .babelrc
      1. “presets”
    2. require hook
    3. CLI
      1. babel
      2. babel-node
      3. babel-doctor
      4. babel-external-helpers
  5. 总结
  6. 更新

什么是Babel

相信很多新手没有听说过BabelES6,如果你是老手的话,那么请自动忽略~

ES6

ES6也就是ECMAScript 6,也就是最新的一代js规范,添加了很多语言的特性,包括模块管理,类,块级作用域等等内容。我最喜欢的就是箭头函数,优雅~

Babel

然而虽然ES6很棒,但是现在几乎没有浏览器或者Node(我记得5.0已经全部支持了es6,可是为啥我试着却不行。。。似乎要开启全部的harmony)能够完全支持es6的代码,那么问题来了,如果我想体验一下es6的代码,怎么办??

一个很简单的思路便是:

我写个程序,将es6代码转换成es5代码进行运行不就好了,很棒

Babel就是干的这个事情。

babel5 和 babel6 的区别

对于Babel来说,现在有了两个版本,一个是5,一个是6,那么两者有什么区别呢?

  • 5对新手更加友好,因为只需要安装一个babel就可以了,而6需要安装比较多的东西和插件才可以。

  • 相比5来说,6将命令行工具和API分开来了,最直观的感觉就是,当你想在代码中运行es6代码的话,需要安装babel-core,而如果你想在终端编译es6或者是运行es6版本的REPL的话,需要安装babel-cli

  • 也许有人问,原先的babel去哪了?是这样的,这个babel的package到了6版本之后虽然还是能安装,但是已经不具有任何的有效的代码了。取而代之的是一段提示文字,提示你需要安装babel-core或者babel-cli。所以你在babel6的情况下,完全不需要安装babel

  • 6将babel插件化,当你第一次安装babel-core并且按照以前的方式来加载require
    hook的话,你会发现代码无法运行:require('babel-core/register');就是因为babel6整体插件化了,如果你想使用es6语法,需要手动加载相关插件。

这里有一篇文章,建议看一下《TheSix Things You Need To Know About Babel 6》

Quick Start

建立空文件夹 babel6

建立空文件夹babel6作为本次的目录,并npm init

安装Babel6

1
npm install babel-core --save

如果觉得慢,可以使用淘宝镜像cnpm

此时,基础的babel6就安装完成了,如果你想安装babel5,那么执行如下的代码

1
npm install babel@5 --save

即可使用babel5,那么在后文的中,统一使用babel6

require hook

安装好之后,问题来了,如何使用呢?

相信使用过coffee的人一定知道register,那么在babel中同样不例外,也可以使用同样的方法。

1
2
3
require('babel-core/register');

require('./app');

大家可能以为这样我就可以在app.js中优雅的使用es6了,在babel5中确实是这样的,但是在babel6中,却不一样了。

如果你这样写完,并没有任何作用,因为你缺少一个插件。

安装插件

如果想使用es6语法,必须安装一个插件

1
npm install babel-preset-es2015

然后在文件夹下面创建一个叫.babelrc的文件,并写入如下代码:

1
2
3
{
"presets": ["es2015"]
}

下面你就可以很优雅的书写你的es6代码了。

书写优雅的ES6代码

下面我们写一段优雅的代码

1
2
3
4
5
let first = (size, ...args) => [...args].slice(0, size);

export default first;

console.log(first(2,1,2,3));

Run it

直接运行,不说话~~~

内容解释

.babelrc

什么是.babelrc文件呢?熟悉linux的同学一定知道,rc结尾的文件通常代表运行时自动加载的文件,配置等等的,类似bashrc,zshrc。同样babelrc在这里也是有同样的作用的,而且在babel6中,这个文件必不可少。

  • 里面可以对babel命令进行配置,以后在使用babel的cli的时候,可以少写一些配置

  • 还有一个env字段,可以对BABEL_ENV或者NODE_ENV指定的不同的环境变量,进行不同的编译操作

“presets”

这个是babel6新加的,就是代表需要启动什么样的预设转码,在babel6中,预设了6种,分别是

  • es2015

  • stage-0

  • stage-1

  • stage-2

  • stage-3

  • react

至于如何安装,请查看balel官网

而且,对.babelrc的设置,你可以存放在package.json中的。如下:

1
2
3
4
5
6
7
{
...
"babel": {
"presets": ["es2015"]
},
...
}

require hook

require hook
的作用就是替换原先的require,以便在加载自动对代码进行编译,运行。

其实这个做的便是重写require.extensions中对应的扩展名的加载程序,并且默认会判断这个文件是否是node_modules中的模块,如果是的话,那么将不会进行转换。否则的话,会进行转换。

CLI

其实babel也可以当做全局变量来使用的

1
npm install babel-cli -g

安装上后,会安装如下四个程序到全局环境中:

  • babel

  • babel-node

  • babel-doctor

  • babel-external-helpers

babel

这个就是编译js文件的全局变量,具体如何使用,大家请参照官网。使用方法和coffee,style,less了类似,就不多讲了

babel-node

这里主要说一下这个东西,就是这个的作用就是提供一个node命令相同的REPL环境,不过这个环境会在执行之前讲代码进行编译。

坑1:上文讲到,babel6默认是无法编译es6文件的,需要你手动安装es2015的preset,同样,全局模式下,也需要这个preset。

那么问题来了,我们怎么安装这个preset呢?global?所以这是一个坑,我在babel的issue中找到这样的一条。作者给出这样的回答:我们处理preset和plugin是依据于输入的文件,而你直接运行CLI是没有输入文件的,也就无法定位preset和plugin的位置。言下之意就是不要全局安装,虽然我们给你了你全局安装的方式。然后作者关闭了issue,表示很无奈。。。。

解决方案1:经过寻找,找到一种解决方案,就是创建一个空项目,安装babel-preset-es2015,并写入对应的配置进入.babelrc,然后在这个目录下运行babel-node即可正常运行

不过这种方式太麻烦了,所以,如果大家想体验一下es6的REPL的话,建议安装babel5

1
npm install babel@5 -g

babel-doctor

就是检查babel状况的,主要检查以下几个内容

  • 是否发现了.babelrc配置文件

  • 是否有重复的babel安装包,比如说安装了5和6

  • 所有的babel安装包是否已经升级到了最新版

  • 并且 npm >= 3.3.0

babel-external-helpers

就是讲一些公共的帮助函数提取成一个文件,其实就做了这一个作用。。。

总结

这是我的第一篇关于es6的教程,如果大家有什么不好的地方,请及时想我反馈

更新

2015-11-15 添加了CLI

注释和共享

Nodejs EventEmitter 解读

发布在 Node.js

目录

  1. events module
  2. Class EventEmitter
    1. EventEmitter Static Method And Property
      1. defaultMaxListeners
      2. usingDomains
      3. init()
      4. listenerCount(emitter, type)
      5. 疑问:为什么要判断原型上是否含有listenerCount方法呢?
    2. EventEmitter Property
      1. _events
      2. _eventsCount
      3. _maxListeners
    3. EventEmitter Method
      1. addListener() = on()
      2. newListener事件是在事件真正添加之前触发的
      3. emit()
      4. handler的执行
      5. 根据参数个数进行加速
      6. once()
      7. removeListener(type, listener)
      8. removeAllListeners(type)
      9. listeners(type)
      10. listenerCount(type)
  3. TODO
    1. domain 部分没有进行解释

events module

先说一下网上似乎很多人提供的使用例子来看,都是如下的

1
var EventEmitter = require('events').EventEmitter;

然而在源码当中有这样的一句话

模块定义events.js
1
2
module.exports = EventEmitter;
EventEmitter.EventEmitter = EventEmitter;

所以require('events').EventEmitterrequire('events')是一样的。所以以后大家可以直接写

1
var EventEmitter = require('events');

Class EventEmitter

在源码中,构造函数极其的简单。

constructorevents.js
1
2
3
function EventEmitter() {
EventEmitter.init.call(this);
}

思路很简单,就是直接调用EventEmitter上的静态方法init进行构造。之后我们会介绍EventEmitter.init方法

EventEmitter Static Method And Property

挂在到EventEmitter上的静态属性和方法还是很多的,先说明一下静态属性:

  • defaultMaxListeners

  • usingDomains

defaultMaxListeners

顾名思义,默认的最大callback数量,就是如果当前对象没有指定_maxListeners,默认使用的就是这个值,默认是10

更多请看_maxListeners

usingDomains

待定

init()

EventEmitter.initevents.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
EventEmitter.init = function() {
this.domain = null;
if (EventEmitter.usingDomains) {
// if there is an active domain, then attach to it.
domain = domain || require('domain');
if (domain.active && !(this instanceof domain.Domain)) {
this.domain = domain.active;
}
}

if (!this._events || this._events === Object.getPrototypeOf(this)._events) {
this._events = {};
this._eventsCount = 0;
}

this._maxListeners = this._maxListeners || undefined;
};

先不管domain恨死干什么用的,总之init函数给this挂上了以下四个属性:

listenerCount(emitter, type)

获取emitter中指定类型的callback数量

这里有一个比较特殊的地方,就是他对对象是否含有listenerCount方法进行了判断

listenerCountevents.js
1
2
3
4
5
6
7
EventEmitter.listenerCount = function(emitter, type) {
if (typeof emitter.listenerCount === 'function') {
return emitter.listenerCount(type);
} else {
return listenerCount.call(emitter, type);
}
};

疑问:为什么要判断原型上是否含有listenerCount方法呢?

EventEmitter Property

_events

这个属性是一个事件的缓存列表,他的key就是事件名称,value就是事件的回调函数,有如下几种取值:

  • Function

  • Array

当只有一个回调函数的时候,就直接存储为回调函数本身,否则包装成数组进行存储

_eventsCount

顾名思义,就是事件的个数,这里有几个需要注意的就是,这个之有且仅在添加一个没有过的事件的时候才会加一,如果你给一个事件添加了多了callback的话,这个值并不会加一的。

_maxListeners

这个就是可以表示一个event的最大callback的个数。如果小于等于这个个数,不会产生任何问题。但是如果大于这个限制,将会弹出警告,说可能会导致内存泄露。
我猜测可能是因为过多的callback会保存过多的context,从而导致内存泄露。
敬请更正

EventEmitter Method

先从最常用的讲起,就是添加事件侦听喽

addListener() = on()

首先大家一定很熟悉on方法,大家一定想不到的是,竟然还有一个叫addListener方法。那么这两个方法有什么区别呢?

答案就是什么区别都木有~

而且在定义的时候,定义的是addListener而不是on,从源码中便可以看出来

addListenerevents.js
1
2
3
4
5
6
7
8
EventEmitter.prototype.addListener = function addListener(type, listener) {
var m;
var events;
var existing;
// .... 此处省略10000行
}
// 就是这里,大家看到了么~~
EventEmitter.prototype.on = EventEmitter.prototype.addListener;

所以on更多的是一种简化,而正统的却是addListener,但是虽然这个正统,相信也没有几个人来使用吧,毕竟实在是太那啥了,是吧-_-||

好的下面言归正传,整个添加函数的流程可以看做是这样的(大家可以对照的源码看,我就不在这里把源码全部都粘贴过来了,并且我这里可能为了语句的通顺改变部分代码的执行顺序,大家请自行看出来。。。):

  1. 接受两个参数,分别是type表示事件,就是event,第二个是listener,侦听器,也就是callback

  2. 如果listener不是函数,很明显啊,抛出错误不解释

  3. 检测当前对象上是否存在_events属性,如果不存在创建之,并顺手初始化_eventsCount为0.

  4. eventsthis._events

  5. 如果events中没有typelistener,那么不解释,添加之~即events[type] = listener

  6. 如果events里面有的话,不解释,一个就数组包装一下,两个直接push

  7. 如果type类型的listener超过两个了,那么就检测一下有没有超过长度限制,具体的检测逻辑我就不在这里详细的说明了,总之就是给将检测结果挂到events[type]warned属性上了。

  8. 如果有对应的newListener事件侦听的话,就直接用typelistener.listener?listener.listener:listener触发之

  9. 返回this

至此函数执行完毕

那么下一个就讲一下如果触发事件吧

newListener事件是在事件真正添加之前触发的

emit()

很早以前我认为emit只能emit一个参数,到现在我猜明白,想几个就几个,没人限制你。

emit有一个很好玩的特性就是,如果你emit一个"error",如果没有事件侦听这个error的话,就会直接throw出一个error来,而其他的事件不会又这种效果,这就意味着我们最好要侦听error事件,万一出了一点错误,那将是崩溃的节奏啊~

源码在events.js:117

  1. 检测type是否为"error"

  2. 如果是,并且没有监听到error,throw出pass进来的错误或者构建一个未捕捉的错误弹出。

  3. 如果没有对应的回调函数的话,返回false

  4. 获取构造函数并赋值给handler,这个值有可能是函数,或者是数组。

  5. 根据参数个数调用对应的函数,顺序依次运行handler

  6. 返回true

handler的执行

handler执行的时候,会先将当前队列复制一份,然后再进行执行。并且根据参数个数用call或者apply执行函数。放置在某一次执行期间突然掺入了其他的callback或者删除了callback,从而引发错误。

我原本以为会使用process.nextTick进行异步的执行,后来一想不对啊,肯定要按照添加的顺序进行执行,所以依次调用。

根据参数个数进行加速

fastToRunevents.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
// 没有参数的调用
function emitNone(handler, isFn, self) {
if (isFn)
handler.call(self);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].call(self);
}
}
// 一个参数
function emitOne(handler, isFn, self) {
// ...... 此处继续省略10000行
}
// 两个参数
function emitTwo(handler, isFn, self) {
// ...... 此处继续省略10000行
}
// 三个参数
function emitThree(handler, isFn, self) {
// ...... 此处继续省略10000行
}
// 不定参数
function emitMore(handler, isFn, self) {
// ...... 此处继续省略10000行
}

源码对不同的情况进行了加速,因为人们大部分情况都是使用0参数或者1参数的,所以对这几种情况进行处理是非常高效的。

once()

这个最简单了,就是用一个函数封装一下传入的callback,然后将这个函数,addListener进入到事件中。关于这个封装函数的写法,如下

g()events.js
1
2
3
4
5
6
7
8
9
10
11
var fired = false;
function g() {
this.removeListener(type, g);

if (!fired) {
fired = true;
listener.apply(this, arguments);
}
}
g.listener = listener;
// 挂载原先的callback,用于remove的时候比较使用

具体的就不多说了,总之就是执行后,先删除这个包装过的callback。

我的疑问:这里我有一点不太理解的地方就是为什么还需要fired进行标记一下呢?

removeListener(type, listener)

删除指定类型的指定callback的事件 很简单,分为一下几步

  1. 如果listener不是函数,抛出错误

  2. 如果events不存在或者对应的事件不存在,返回this

  3. listevents[type]

  4. 如果list就是那个callback或者list.listener是那个callback,删除

  5. 否则如果这是一个数组,那么找到对应的listener删除,否则返回this

  6. 如果上面有任意一个删除之后_eventsCount为0了,直接重新赋值this._events = {}(其实并不知道这个意义何在)

  7. 如果有任何一个删除,并且有事件侦听到了removeListener,触发之,传递typelistener

  8. 返回this

在事件删除之后才调用removeListener事件

removeAllListeners(type)

根据传递的参数来决定是删除全部的还是只删除指定事件的全部

  1. 确保存在this._events,否则返回this

  2. 如果没有侦听removeListener

    • 如果没有传递type,重新赋值this._eventsthis._eventsCount

    • 如果传递了type,删除type对应的事件,根据情况重新赋值~~~

  3. 如果侦听了removeListener,除了这个本身,依次调用removeAllLiseners()进行删除,最后删除removeListener这个事件

  4. 对于每一个callback,依次调用removeListener方法进行删除

  5. 返回this

listeners(type)

获取指定类型的callback,否则为[]

listenerCount(type)

获取指定类型的callback的数量

TODO

domain 部分没有进行解释

注释和共享

目录

  1. HTTP2 简介
  2. 前言
  3. 准备工作
    1. 通过翻墙打开Google.com测试浏览器是否支持http2
      1. Chrome
      2. 通过 net-intervals 查看是否支持
      3. 如果不支持,请尝试打开http2开关
      4. 否则的话,我也不知道了
      5. firefox
      6. 其他浏览器
      7. 建立Nodejs工作目录,并安装依赖
      8. 使用openssl生成密钥和证书
    2. 创建服务器(start-server 分支)
    3. 访问 https://localhost/
    4. 尝试服务器推(server-push 分支)
      1. 建立一个首页 index.html
      2. 在代码中加入服务器推的代码,并返回上面那个主页
      3. 启动服务器,并访问之
  4. 总结
  5. GitHub 源码

HTTP2 简介

请大家自行搜索百度 Or Google

前言

本文章内部的术语,我不进行大篇幅的解释,如果你不了解的话,请自行搜索,不要说自己懒~

如果本文章有问题的话,请及时进行评论,并和我联系

准备工作

通过翻墙打开Google.com测试浏览器是否支持http2

Google的官网通过我的观察,几乎已经全部使用了http2,你可以在开发者工具中的 Network 中查看 Protocol 使用的协议。

这里有一个问题,就是如果你的没有 Protocol ,这个怎么办?你可以在上面单击右键,就会弹出来有哪一些列可以让你选择,然后你选择 Protocol 即可。

Chrome

通过 net-intervals 查看是否支持

对于Chrome浏览器,可以在地址栏上输入

1
chrome://net-internals

并在左上角的下拉菜单中寻找 HTTP/2 的字样,如果有的话,说明你的浏览器就支持http2,否则就话可能是另外一种情况

如果不支持,请尝试打开http2开关

在Chrome地址栏中输入

1
chrome://flags

当前页面搜索 http2 ,找到相应的内容,并开启。然后重启浏览器尝试

否则的话,我也不知道了

如果还是前面两种都不可以的话,那么就请自行百度吧。

firefox

这个由于我没有firefox的浏览器,就留给读者自行百度了。

其他浏览器

同样自行百度吧

建立Nodejs工作目录,并安装依赖

浏览器都支持之后,那么就是开始准备我们的工作目录了。创建一个目录,我这里就叫做 try-http2。并安装http2包。

使用openssl生成密钥和证书

这里不再重复,请尝试百度,并将证书和密钥拷贝到目录下面

准备工作结束

创建服务器(start-server 分支)

使用http2服务创建服务器和使用https是基本一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
var http2 = require('http2');
var fs = require('fs');

var server = http2.createServer({
key: fs.readFileSync('privatekey.pem'),
cert: fs.readFileSync('certificate.pem')
}, function(req, res) {
res.end('hello world');
});

server.listen(443, function() {
console.log('listen on 443');
});

此时在terminal中启动服务器即可

记住,这里一定要用sudo来启动,因为在Linux的系统中(Mac也是和Linux相同祖先),普通用户是无法监听到1024端口一下的端口,所以要用sudo来让程序监听443端口,因为https服务就是监听在443端口的

访问 https://localhost/

在浏览器中访问上面那个地址,然后在Network中查看Protocol是否为h2

看成功了,已经成功通过http2协议打开了这个网页。

你会发现在地址栏上https证书出现了错误,这是因为证书是你自己发布的,Chrome自然是不认识了

尝试服务器推(server-push 分支)

说到服务器推,大家一定第一个想到的是WebSocket,但是相比于WebSocket来说,服务器推的东西不是数据,而是网络请求。

其实他的正规说法是,服务器会推测你需要的东西,在解析HTML前将你需要的文件或者数据给你请求回来。这样,你请求了一次数据,返回了好几个资源

建立一个首页 index.html

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>haha</title>
<script src="/client.js"></script>
</head>
<body>
Hello world
</body>
</html>

在代码中加入服务器推的代码,并返回上面那个主页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var http2 = require('http2');
var fs = require('fs');

var server = http2.createServer({
key: fs.readFileSync('privatekey.pem'),
cert: fs.readFileSync('certificate.pem')
}, function(req, res) {
var push = res.push('/client.js')
push.end('alert("this is a server push");');
fs.createReadStream('index.html').pipe(res);
});

server.listen(443, function() {
console.log('listen on 443');
});

启动服务器,并访问之

看成功执行了client.js中的代码。

下面我们来看看Network中的请求

你会发现虽然是请求了两次,但是client.js是服务器推送过来的请求,并不是浏览器去请求的。

总结

虽然服务器推跟我们的心里落差较大,但是这并不影响这个技术的实施。

有了服务器推,可以很好的加快网站的加载速度,完全可以不需要等待整张页面加载完成之后再加载相关的数据。像一些script,CSS之类的可以在渲染页面之前就加载完成。

但是这个服务器推仍然无法替代WebSocket的地位,因为两个东西本质和目的都是不同的。而且服务器推相比来说,我们能控制的能力更少。

但是服务器推也有缺点,就是会无故增加浏览网页的流量,对于移动端来说,这将是致命的!

GitHub 源码

GitHub

注释和共享

  • 第 1 页 共 1 页

XGHeaven

一个弱弱的码农


杭州电子科技大学学生一枚


Weifang Shandong, China