从接触到 node 环境来说,其中一个不可或缺的一部分便是 npm 包管理,但是由于官方的 npm 有各种各样的问题,于是催生了很多不同的版本,这其中的曲折也许只有过来人才知道。

放弃 npm?

上古时代

在上古版本(应该是 npm3 以前的版本,具体我也记不清了),npm 的安装策略并不是扁平化的,也就是说比如你安装一个 express,那么你会在 node_modules 下面只找到一个 express 的文件夹。而 express 依赖的项目都放在其文件夹下。

1
2
3
4
5
6
7
8
- app/
- package.json
- node_modules/
- express/
- index.js
- package.json
- node_modules/
- ...

这个带来的问题或许 windows 用户深谙其痛,因为在这种安装环境下,会导致目录的层级特别高,而对于 windows 来说,最大的路径长度限制在 248 个字符(更多请见此),再加上 node_modules 这个单词又特别长,所以你懂得,哈哈哈。解决方案啥的自己去搜索吧,反正估计现在也没人会用上古版本了。

除了 windows 用户出现的问题以外,还有一个更严重的问题,就是模块都是独立的,比如说位于 express 下面的 path-to-regexpconnect 下面的 path-to-regexp 的模块是两个不同的模块。
那么这个会带来什么影响呢?其实在使用上,并没有什么太大的影响,但是内存占用过大。因为很多相同模块位于不同模块下面就会导致有多个实例的出现(为什么会加载多个实例,请查看 Node 模块加载)。你想想,都是同样的功能,为什么要实例这么多次呢?不能就加载一次,复用实例么?

上古时代的 npm 的缺点可以说还是很多的:

  • 目录嵌套层级过深
  • 模块实例无法共享
  • 安装速度很慢,这其中有目录嵌套的原因,也有安装逻辑的问题。因为 npm 是请求完一个模块之后再去请求另一个模块,这就会导致同一个时刻,只有一个模块在下载、解析、安装。

软链时代

后面,有人为了解决目录嵌套层次过高的问题,引入的软链接的方案。

简单来说,就是将所有的包都扁平化安装到一个位置,然后通过软链接(windows 快捷方式)的方式组合到 node_modules 中。

1
2
3
4
5
6
7
8
9
10
11
12
- app/
- node_modules
- .modules/
- express@x.x.x/
- node_modules
- connect -> ../../connect@x.x.x
- path-to-regexp -> ../../path-to-regexp@x.x.x
- ... -> ../../package-name@x.x.x
- connect@x.x.x/
- path-to-regexp@x.x.x/
- ...others
- express -> ./.modules/express@x.x.x

这样做的好处就是可以将整体的逻辑层级简化到很少的几层。而且对于 node 的模块解析来说,可以很好的解决相同模块不同位置导致的加载多个实例,进而导致内存占用的情况。

基于这种方案,有 npminstall 以及 pnpm 这个包实现了这种方案,其中 cnpm 使用的就是 npminstall,不过他们实现的方式和我上面讲的是有差异的,具体请看。简单来讲,他们没有 .modules 这一层。更多的内容,请看 npminstall 的 README。

总的来讲这种解决方案有还有以下几个好处:

  • 兼容性很好
  • 在保证目录足够简洁的情况下,解决了上面的两个问题(目录嵌套和多实例加载)。
  • 安装速度很快,因为采用了软连接的方式加上多线程请求,多个模块同时下载、解析、安装。

那么缺点也是挺致命的:

  • 一般情况下都是第三方库实现这个功能,所以无法保证和 npm 完全一致的行为,所以遇到问题只能去找作者提交一下,然后等待修复。
  • 无法和 npm 很方便的一起使用。最好是要么只用 npm,要么只用 cnpm/pnpm,两者混用可能会产生很奇葩的效果。

npm3 时代

最大的改变就是将目录层级从嵌套变到扁平化,可以说很好的解决了上面嵌套层级过深以及实例不共享的问题。但是,npm3 在扁平化方案下,选择的并不是软连接的方式,而是说直接将所有模块都安装到 node_modules 下面。

1
2
3
4
5
6
- app/
- node_modules/
- express/
- connect/
- path-to-regexp/
- ...

如果出现了不同版本的依赖,比如说 package-a 依赖 `package-c@0.x.x的版本,而package-b依赖package-c@1.x.x` 版本,那么解决方案还是像之前的那种嵌套模式一样。

1
2
3
4
5
6
7
8
9
- app/
- node_modules/
- package-a/
- package-c/
- // 0.x.x
- package-b/
- node_modules/
- package-c/
- // 1.x.x

至于那个版本在外面,那个版本在里面,似乎是根据安装的先后顺序有关的,具体的我就不验证了。如果有人知道的话,欢迎告诉我。

在这个版本之后,解决了大部分问题,可以说 npm 跨入了一个新的世界。但是还要一个问题就是,他的安装速度依旧很慢,相比 cnpm 来说。所以他还有很多进步的空间。

yarn 的诞生

随着 Node 社区的越来越大,也有越来越多的人将 Node 应用到企业级项目。这也让 npm 暴露出很多问题:

  • 无法保证两次安装的版本是完全相同的。大家都知道 npm 通过语义化的版本号安装应用,你可以限制你安装模块的版本号,但是你无法限制你安装模块依赖的模块的版本号。即使有 shrinkwrap 的存在,但是很少有人会用。
  • 安装速度慢。上文已经讲过,在一些大的项目当中,可能依赖了上千个包,甚至还包括了 C++ Addon,严重的话,安装可能要耗时 10 分钟甚至到达半个小时。这很明显是无法忍受的,尤其是配合上 CI/CD。
  • 默认情况下,npm 是不支持离线模式的,但是在有些情况下,公司的网络可能不支持连接外网,这个时候利用缓存构建应用就是很方便的一件事情。而且可以大大减少网络请求。

所以,此时 yarn 诞生了,为的就是解决上面几个问题。

  • 引入 yarn.lock 文件来管理依赖版本问题,保证每次安装都是一致的。
  • 缓存加并行下载保证了安装速度

那个时候我还在使用 cnpm,我特地比较了一下,发现还是 cnpm 比较快,于是我还是继续使用着 cnpm,因为对于我来说足够了。但是后面发现 yarn 真的越来越火,再加上 cnpm 长久不更新。我也尝试着去了用 yarn,在尝试之后,我彻底放弃了 cnpm。而且直到现在,似乎还没有加入 lock 的功能。

当然 yarn 还不只只有这么几个好处,在用户使用方面:

  • 提供了非常简洁的命令,将相关的命令进行分组,比如说 yarn global 下面都是与全局模块相关的命令。而且提示非常完全,一眼就能看明白是什么意思。不会像 npm 一样,npm --help 就是一坨字符串,还不讲解一下是什么用处,看着头疼。
  • 默认情况安装会保存到 dependencies,不需要像 npm 一样手动添加 -S 参数
  • 非常方便的 yarn run 命令,不仅仅会自动查看 package.json 中 scripts 下面的内容,还是查找 node_modules/.bin 下的可执行文件。这个是我用 yarn 最高的频率。比如你安装了 yarn add mocha,然后就可以通过 yarn run mocha 直接运行 mocha。而不需要 ./node_modules/.bin/mocha 运行。是我最喜欢的一个功能
  • 交互式的版本依赖更新。npm 你只能先通过 npm outdated 看看那些包需要更新,然后通过 npm update [packages] 更新指定的包。而在 yarn 当中,可以通过交互式的方式,来选择那些需要更新,那些不需要。
  • 全局模块的管理。npm 管理全局模块的方式是通过直接在 /usr/lib/node_modules 下面安装,然后通过软连接连接到 /usr/local/bin 目录下。而 yarn 的做法是选择一个目录,这个目录就是全局模块安装的地方,然后将所有的全局模块当做一个项目,从而进行管理。这个好处就是,你可以直接备份这个目录当中的 package.json 和 yarn.lock 文件,从而可以很方便的在另一个地方还原你安装了那些全局模块。至于这个目录的问题,通过 yarn global dir 命令就可以找到,mac 下是在 ~/.config/yarn/global/,linux 我没有测试过。

可以说 yarn 用起来非常舒服,但是唯一的缺点就是不是 npm 官方出的,更新力度、兼容性都会差一些。但这也阻挡不住 yarn 在 Node 社区的火热程度。很快,大家纷纷从 npm 切换到 yarn 上面。

重拾 npm 5

在受到 yarn 的冲击之后,npm 官方也决定改进这几个缺点,于是发布了和 Yarn 对抗(这个词是我意淫的)的 npm5 版本。

  1. 引入了 package-lock.json,并且默认就会添加,和 yarn.lock 是一样的作用,并且取代之前的 npm shrinkwrap。
  2. 默认情况下,安装会自动添加 dependencies,不需要手动书写 -S 参数
  3. 提升了安装速度,和之前有了很大的进步,但是和 yarn 相比,还是略微慢一些

至此,yarn 和 npm 的差距已经非常非常小了,更多的差距体现在用户体验层面,我使用 yarn 的功能也只剩下全局模块管理、模块交互式更新和 yarn run 这个命令了。

但是后面推出的 npx 让我放弃了使用 yarn run 这个命令。不是说 npx 比 yarn 有多好,而是说 npm 集成了这个功能,也就没必要再去使用第三方的工具了。而且 npx 还支持临时安装模块,也就是那种只用一次的命令,用完就删掉了。

后面我又发现了 npm-check 这个工具,我用它来替代了 yarn 的交互式更新。

然而 npm6 的出现加入了缓存,并且又进一步提升了速度,可以说直逼 yarn。

于是 yarn 对我来说只剩下一个全局模块管理的功能了。我的整个开发流程以及从 yarn 切换回 npm 上面了。或许后面的日子我也会让 npm 来接管全局模块管理,从而放弃使用 yarn。但是我还是会装 yarn,毕竟有一些老项目还是用 yarn 的。

总结

我经历了从 npm -> cnpm -> yarn -> (npm + npm-check + npx) 的一个循环,也见证了 npm 社区的一步步发展。而且 yarn 的更新频率也非常慢,可能一个月才更新一次,这也让我逐渐放弃使用 yarn。

有的时候感觉,第三方的终究是第三方,还是没有原生的好用和方便,而且用起来安心。

注释和共享

本人是一名 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 一周以来的心得。暂时就想到这么多,更多的内容等我后面再分析吧。

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

注释和共享

2018 年终总结

发布在 年终总结

2017 年,有开心,也有失落。也不知道该从何说起,就随便写写了。高中语文就没学好过,所以可能写成流水账了。请各位看官多多包涵。

实习

如今步入大四,最大的心事就是找实习找 Offer 了。先后经历了两次阿里面试的失利,至于失利的过程,我之前有吐槽过,就不多说了,而且听说今年的面试难度提升到了社招,心痛。饿了么/七牛都有投递过,但是都没有进。如果各位看官想招 Node.js 或者 前端 的校招生的话,请联系我哦~ 请在各大社交媒体请搜索 XGHeaven 即可

终于在 CNode 上面找到了一家美国 AR 公司,Integem。

在里面主要是用 Electron 做客户端,技术栈就是 Vue 全家桶,不多说了。反正整体干下来的感觉其实和自己接了一个外包的感觉差不多,而且设计师设计的页面参差不齐。最可气的是,假设页面 A 和页面 B 相同的部分,没准一个就有边框,另一个就没有。没准一个字体是 12px,另一个可能就是 16px。真是受不了,一开始,我还是让他确认一下到底哪个设计图是对的,到最后,只能用我的佛系心态对待这个设计图,懒得问了。切个图,也是切的乱七八糟,我还是自己来好了。

在里面差不多干了 5 个月,再加上学校要求每个人都要去跟着导师完成一个实践项目,于是我就离开了。

再之后,我在逛实习的时候,突然发现大搜车在招 Node.js 实习生,而且标注的是有大牛带。大搜车……为啥听着这么耳熟啊,不管了,报了再说。于是我就去面试了。面试我的是一个胖胖的留着胡子的人(死月罪过,当时并不知道那是你),于是和他相聊甚欢,最后还记得,当时看到他的 15寸 macbook pro 后面贴着一个 bad apple 的一个贴纸。当时顺口就问了一句,这个是 bad apple 么?当时之所以会问这个,因为我还记得很早以前,看过一个人的博客,里面讲到了他在花瓣网工作,而且很详细的讲解了图片主题色的提取和 Node.js NAN API,感觉受益匪浅,于是我当时在想,这是哪个学校的大牛,竟然这么厉害。后来看到他已经在花瓣工作了。。。不过我记得他很喜欢二次元。。。于是面试的时候,我在想会不会是他,于是就问了句贴纸,以为他会聊起二次元,结果他随便应了一句就过去了。我见状就没再继续问下去了。后面安慰道自己说,没事,那个人应该不会在大搜车。面试我这个人感觉还是很厉害的~~至少有一点,我说我提过 issue 给 node,他能马上就打开 github 看。这一点让我很敬佩,因为大部分面试都是会听你描述,看简历上面写的,而不是当场去查看。举个例子,你跟面试官说你博客写了很多高品质的文章,大部分面试官会直接问你有什么,他不会自己去看。而好的面试官会一边问你,一边自己打开看。。。我是这么觉得的。反正不管当时是不是月老,我已经决定来这家公司了。

哈哈哈,后面等进大搜车之后,剧情反转。那个人其实就是死月。当时看到他在 QQ 群里面的时候,你知道我的心里有多么开心么!!但是,有一个噩耗,就是他在我去的前几周已经跳槽去蚂蚁金服了,哭 (((T____T))) 我的大牛啊,你怎么走了啊~啊~啊~啊~

不过还好,也认识了挺多大牛的,不过还是没见到过我心仪的 朴灵/不四/狼叔 -_-。

现在在大搜车呆了有两个月了吧,那就简单总结一下干了啥吧:

  • 完成了一个图片上传服务,里面包含了公共的图片上传,以及大风车的头像上传,真正的编码时间也就两个星期,但是真的发布上去,却花了一个月。
  • 现在准备一个请求限制框架,讲道理这是很简单的一个工作,但是我看时间很充裕,于是我就想写大,看看能不能独立成一个库,开源骗 star。
  • 期间还要各种小东西,修修补补。

刚进大搜车,按理来说,带我的人应该是小山,但是那个时候他请了几天假,于是就鹏飞暂时带着我。让我看了 Akyuu.js 和帕秋莉网关。之后其实所有的时间都是跟着鹏飞,我师父小山感觉不喜欢多说话,平时也没有太多的共同语言。想平时打打游戏联络一下感情,但是看他很忙的样子,就放弃了。最近才发现,原来小山也看二次元,哈哈哈。反正就这样,和小山半亲近半陌生。和鹏飞一开始也聊的挺多,后面等公司的事情知道的差不多了,也交流的不多了。

而且由于我比较慢热,再加上我进入公司比较晚,没有参加过团建,和大家都不是很熟。就和组内的坐在旁边的外加组内的实习生比较熟。

说一下,我在大搜车实习的感受吧。

  • 代码层面
    • 更加理性的对待 callback 和 promise,因为在之前,我是极力反对使用 callback 的,所以当第一眼看到公司的代码的时候,我懵逼了,怎么全是 callback。于是经过和鹏飞的交流以及自己的领悟,终于放下执念。其实 callback+async 和 promise 没啥区别么,哈哈哈。
    • 尝试先写文档,后写代码。我平时兴起的时候,直接就开始撸,从来不打草稿。小项目可以,但是当项目大了之后,就呵呵哒了。
    • 了解了 Node 的 PR 流程。
    • 其他的好像还真的没有了,什么代码规范,git flow,框架的使用等等,我基本都了解。但是又重新复习了并精进了一下。毕竟之前看时候只是看了几眼,大体明白了内容。正好趁着这次实习,运用一下,看看自己理解的哪里有问题。
  • 交际层面
    • 首先我是一个慢热的,也就是说我不是很擅长去找别人交流,但是别人来找我交流,我是很乐意的。所以说,我当初进入公司之前的幻想,就是大家都在交流着各种新技术,新框架,新事物,当一个人抛出问题的时候,大家会一起去解决研究。结果进来之后,我发现,好像群里半天都不会有任何消息。于是我就努力去带动气氛,有什么好玩的东东都尝试发到群里,结果还是很难带起氛围。不知道是大家太忙了,还是我发的信息太简单。
    • 还记得在学校的时候,社团的技术群,可能一个人发现了新东西,于是群里的大佬一起去尝试,评论,总结。和群里的人撕逼那个语言好,撕逼什么框架好用。我们只追求方便好用,并不怎么在乎稳定性这种东西。哎,这种感觉好难在找回来了。不知道头哥能不能看到我写的,我不知道头哥你想的团队是不是我想的那样,但是感觉大家仅仅为了业务而工作,死气沉沉,不觉得失去了乐趣么?
    • 再讲讲开源的东东,我不知道用我们大学生的思维来思考对不对。至少我会很讨厌所有的公司沉淀出来的产品,比如说阿里的 egg。怎么解释呢?你可以理解假设公司内部的开发版本按照 master 的一条线进行,如果开源了,我就从 master checkout 一个新的分支,然后做一些开源的修改。我为什么不喜欢这种呢?因为这种所有的功能的设计实现就是严重依赖业务的,他只能做到的是在这个业务的情况下尽可能去兼容其他的业务格式而抽离出来的核心。而我真正想要的是什么呢?是社区驱动,一个产品可以由一个公司来开发,但是设计一定还给社区。而且我也不喜欢所谓的二次封装的框架,二次封装的框架我建议内部使用,而不是开源。除非你的二次封装能够提供很多功能。
  • 反正总的来讲,我感觉从知识层面,我获取的很少,没获取到太多新的概念/知识。但是实践层面我获取的还是挺多的。不知道这是不是以后工作的常态。

大学

这一年其实大学生活没有太多的东西,主要是在实习当中度过的。

正是因为如此,我也越发怀念当初的学习生活。回头看看学弟当中的大佬,不由自主的感叹自己好像虚度了大学生活。

不顾了,下学期就准备毕业设计了,希望自己能珍惜最后的时光吧。

2018 展望

看过了死月的总结,我发现其实有一点挺好的,一年给自己定一些目标,来年看看目标有没有实现。

那我也来展望一下好了:

  • 学习
    • 争取研读 Node 源码,至少要把死月那本书给啃完
    • 争取 Github 每周都有贡献,希望能长久的维护一个项目
    • 争取写一些有意思,有难度的代码,比如说《如何写出一个 Babel》
    • 坚持写文章吧(这个有点难),至少保证每两周一篇高质量的
    • 了解一些其他方面的内容,下一年总结一下了解了啥。
  • 坚持锻炼身体,把体重控制在 65 以下,争取练出胸肌(其实我有,只不过有点萎缩了)腹肌二头肌(当然,这些都练出来之后我就不限制体重了)。
  • 争取学会做几个菜(当然指的是在实习期间了),暂时只考虑用电饭煲来做。
  • Minecraft 开新坑,等 1.13 发布~如果有小伙伴想入坑的请联系我,最好有正版,因为我是想单人开坑的。
  • 不知道为啥,自从实习之后,感觉有点思春。。。特别想找人聊聊非技术方面的事情。。没妹子陪,我都不想去电影院看电影了。。。所以,不求找女票,感觉自己现在还不是很适合去当男票,其实是找不到合适的,哈哈哈。只求可以找到有空可以出去看个电影,聊个天,而且臭味相投的妹子就好了。不过目测是完不成这个目标了。
  • 既然没有女票,那就多花一些时间在学习上。但是不要死学习,做程序员路上的书呆子。
  • 恶习
    • 争取改掉拖沓的毛病,有任务赶紧去做,有事情提前安排
    • 争取每天刷牙洗脸(也就是让自己早起,哈哈哈)

先这么多吧,看看 2019 年,我完成了哪些。

注释和共享

Reflect-Metadata 详解

发布在 Jacascript

引言

在 ES6 的规范当中,就已经存在 Reflect API 了。简单来说这个 API 的作用就是可以实现对变量操作的函数化,也就是反射。具体的关于这个 API 的内容,可以查看这个教程

然而我们在这里讲到的,却是 Reflect 里面还没有的一个规范,那么就是 Reflect Metadata

Metadata

想必对于其他语言的 Coder 来说,比如说 Java 或者 C#,Metadata 是很熟悉的。最简单的莫过于通过反射来获取类属性上面的批注(在 JS 当中,也就是所谓的装饰器)。从而可以更加优雅的对代码进行控制。

而 JS 现在有装饰器,虽然现在还在 Stage2 阶段。但是 JS 的装饰器更多的是存在于对函数或者属性进行一些操作,比如修改他们的值,代理变量,自动绑定 this 等等功能。

所以,后文当中我就使用 TypeScript 来进行讲解,因为 TypeScript 已经完整的实现了装饰器。
虽然 Babel 也可以,但是需要各种配置,人懒,不想配置那么多。

但是却无法实现通过反射来获取究竟有哪些装饰器添加到这个类/方法上。

于是 Reflect Metadata 应运而生。

Reflect Metadata

Relfect Metadata,简单来说,你可以通过装饰器来给类添加一些自定义的信息。然后通过反射将这些信息提取出来。当然你也可以通过反射来添加这些信息。 就像是下面这个例子所示。

1
2
3
4
5
6
7
8
9
10
11
@Reflect.metadata('name', 'A')
class A {
@Reflect.metadata('hello', 'world')
public hello(): string {
return 'hello world'
}
}

Reflect.getMetadata('name', A) // 'A'
Reflect.getMetadata('hello', new A()) // 'world'
// 这里为什么要用 new A(),用 A 不行么?后文会讲到

是不是很简单,那么我简单来介绍一下~

概念

首先,在这里有四个概念要区分一下:

  1. Metadata Key {Any} 后文简写 k。元数据的 Key,对于一个对象来说,他可以有很多元数据,每一个元数据都对应有一个 Key。一个很简单的例子就是说,你可以在一个对象上面设置一个叫做 'name' 的 Key 用来设置他的名字,用一个 'created time' 的 Key 来表示他创建的时间。这个 Key 可以是任意类型。在后面会讲到内部本质就是一个 Map 对象。
  2. Metadata Value {Any} 后文简写 v。元数据的类型,任意类型都行。
  3. Target {Object} 后文简写 o。表示要在这个对象上面添加元数据
  4. Property {String|Symbol} 后文简写 p。用于设置在那个属性上面添加元数据。大家可能会想,这个是干什么用的,不是可以在对象上面添加元数据了么?其实不仅仅可以在对象上面添加元数据,甚至还可以在对象的属性上面添加元数据。其实大家可以这样理解,当你给一个对象定义元数据的时候,相当于你是默认指定了 undefined 作为 Property。 下面有一个例子大家可以看一下。

大家明白了上面的概念之后,我之前给的那个例子就很简单了~不用我多说了。

安装/使用

下面不如正题,我们怎么开始使用 Reflect Metadata 呢?
首先,你需要安装 reflect-metadata polyfill,然后引入之后就可以看到在 Reflect 对象下面有很多关于 Metadata 的函数了。因为这个还没有进入正式的协议,所以需要安装垫片使用。

啥,Reflect 是啥,一个全局变量而已。

你不需要担心这个垫片的质量,因为连 Angular 都在使用呢,你怕啥。

之后你就可以安装我上面写的示例,在 TypeScript 当中去跑了。

类/属性/方法 装饰器

看这个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Reflect.metadata('name', 'A')
class A {
@Reflect.metadata('name', 'hello')
hello() {}
}

const objs = [A, new A, A.prototype]
const res = objs.map(obj => [
Reflect.getMetadata('name', obj),
Reflect.getMetadata('name', obj, 'hello'),
Reflect.getOwnMetadata('name', obj),
Reflect.getOwnMetadata('name', obj ,'hello')
])
// 大家猜测一下 res 的值会是多少?

想好了么?再给你 10 秒钟

10
9
8
7
6
5
4
3
2
1

res

1
2
3
4
5
[
['A', undefined, 'A', undefined],
[undefined, 'hello', undefined, undefined],
[undefined, 'hello', undefined, 'hello'],
]

那么我来解释一下为什么回是这样的结果。

首先所有的对类的修饰,都是定义在类这个对象上面的,而所有的对类的属性或者方法的修饰,都是定义在类的原型上面的,并且以属性或者方法的 key 作为 property,这也就是为什么 getMetadata 会产生这样的效果了。

那么带 Own 的又是什么情况呢?

这就要从元数据的查找规则开始讲起了

原型链查找

类似于类的继承,查找元数据的方式也是通过原型链进行的。

就像是上面那个例子,我实例化了一个 new A(),但是我依旧可以找到他原型链上的元数据。

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {
@Reflect.metadata('name', 'hello')
hello() {}
}

const t1 = new A()
const t2 = new A()
Reflect.defineMetadata('otherName', 'world', t2, 'hello')
Reflect.getMetadata('name', t1, 'hello') // 'hello'
Reflect.getMetadata('name', t2, 'hello') // 'hello'
Reflect.getMetadata('otherName', t2, 'hello') // 'world'

Reflect.getOwnMetadata('name', t2, 'hello') // undefined
Reflect.getOwnMetadata('otherName', t2, 'hello') // 'world'

用途

其实所有的用途都是一个目的,给对象添加额外的信息,但是不影响对象的结构。这一点很重要,当你给对象添加了一个原信息的时候,对象是不会有任何的变化的,不会多 property,也不会有的 property 被修改了。
但是可以衍生出很多其他的用途。

  • Anuglar 中对特殊字段进行修饰 (Input),从而提升代码的可读性。
  • 可以让装饰器拥有真正装饰对象而不改变对象的能力。让对象拥有更多语义上的功能。

API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace Reflect {
// 用于装饰器
metadata(k, v): (target, property?) => void

// 在对象上面定义元数据
defineMetadata(k, v, o, p?): void

// 是否存在元数据
hasMetadata(k, o, p?): boolean
hasOwnMetadata(k, o, p?): boolean

// 获取元数据
getMetadata(k, o, p?): any
getOwnMetadata(k, o, p?): any

// 获取所有元数据的 Key
getMetadataKeys(o, p?): any[]
getOwnMetadataKeys(o, p?): any[]

// 删除元数据
deleteMetadata(k, o, p?): boolean
}

大家可能注意到,针对某些操作,会有 Own 的函数。这是因为有的操作是可以通过原型链进行操作的。这个后文讲解。

深入 Reflect Metadata

实现原理

如果你去翻看官网的文档,他会和你说,所有的元数据都是存在于对象下面的 [[Metadata]] 属性下面。一开始我也是这样认为的,新建一个 Symbol('Metadata'),然后将元数据放到这个 Symbol 对应的 Property 当中。直到我看了源码才发现并不是这样。请看例子

1
2
3
4
@Reflect.metadata('name', 'A')
class A {}

Object.getOwnPropertySymbols(A) // []

哈哈,并没有所谓的 Symbol,那么这些元数据都存在在哪里呢?

其实是内部的一个 WeakMap 中。他正是利用了 WeakMap 不增加引用计数的特点,将对象作为 Key,元数据集合作为 Value,存到 WeakMap 中去。

如果你认真探寻的话,你会发现其内部的数据结构其实是这样的

1
WeakMap<any, Map<any, Map<any, any>>>

是不是超级绕,但是我们从调用的角度来思考,这就一点都不绕了。

1
weakMap.get(o).get(p).get(k)

先根据对象获取,然后在根据属性,最后根据元数据的 Key 获取最终要的数据。

End

因为 Reflect Metadata 实在是比较简单,这里就不多讲解了。更多内容请查看 Spec

题外话

其实看了源码之后还是挺惊讶的,按照一般的套路,很多 polyfill 会让你提供一些前置的 polyfill 之后,当前的 polyfill 才能使用。但是 reflect-metadata 竟然内部自己实现了很多的 polyfill 和算法。比如 Map, Set, WeakMap, UUID。最惊讶的莫过于 WeakMap 了。不是很仔细的阅读了一下,好像还是会增加引用计数。

注释和共享

vue-lever 是一个使用装饰器模式的插件来帮你管理 true/false 状态

最近一直在写 vue 相关的东西,因为毕竟是前端,所以经常会遇到一种情况就是说,我需要维护一个 true/false 状态,比如说:当按钮点击的时候,显示加载字样,然后等加载完毕之后完成显示。

首先我们考虑出现这种情况的时候,如果我们一行一行的书写,那将是非常麻烦的。

1
2
3
4
5
6
7
8
9
10
11
12
13
new Vue({
data() {
return { loading: false }
}
methods: {
asyncAction() {
this.loading = true
this.$http.doing() // return a promise
.then(() => this.loading = false)
.catch(() => this.loading = false))
}
}
})

我们需要在每一个退出的情况下将 loading 状态设置为 false,这至少意味着你要写 3 遍,非常冗余。而且很容易忘记。

我们可以抽象出来,可以发现每当这种函数运行的时候,首先将相关的变量设置成 true,然后等待异步操作完成或者失败之后,再将变量设置回之前的值。

完成这个操作最简单的方式就是代理,在用户的函数调用之前设置相应的变量,在用户的函数调用完成之后,或者如果函数是异步操作,那么通过返回一个 promise 来表示异步操作。

既然讲到了代理模式,那么在 JS 中有很多,不过我们在这里通过一个 ES6 的新语法,装饰器。

话不多说,上代码

1
2
3
4
5
6
7
8
9
10
11
import Lever from 'vue-lever'
// import others

new Vue({
methods: {
@Lever.Lever('loading')
asyncAction() {
return this.$http.doing() // return a promise
}
}
})

这个功能和上面的那段代码是一模一样的,是不是感觉用了装饰器之后就变得特别简单了呢?

不过这里有一点需要注意的是,为了方便和隔离,我将所有的变量全都放在 levers 这个变量下面,也就是说你需要通过 levers.loading 来使用,而不是 loading

不过现在暂时没有支持回调函数的方式,也就是你必须要返回一个 promise,否则是不行的。

其次我们这里还有手动模式,也就是如果你不想用装饰器的话,你可以通过 this.$lever(name, value),来更改状态,其中 name 为变量名,value 为 true/false 值。当然这里也提供两个 alias,this.$lever.t(name)this.$lever.f(name)。方便设置变量为 true/false。

还有一些其他的参数,大家可以去 Github 网站查看。这篇文章就写到这里。

注释和共享

17年4月,终于迎来了阿里巴巴的实习招聘,我也很开心的通过朴神内推到阿里云的前端开发。故事就是这么展开了。

还记得当初3月份(我也记不清了)朴神在微博上面发了谁愿意和他一起写 Node ,于是我激动的花了一晚上写了一张简历页面,开开心心的给了朴神,他给我回了一句:你联系方式都没有写,让我怎么联系你呀。QAQ,竟然忘了这么重要的事情,好丢脸。于是赶紧补了一下又给了朴神。

过了许久,等来了内推邮件,打开一看,哎不对啊,为啥是前端开发工程师?不是说好的写 Node 么-_-。于是又仔细看了阿里要的职位,发现原来 Node 是包含在前端开发工程师中的呀。

也不知道是谁定的,Node竟然属于前端开发

不管了,于是又开开心心的填上去了。

Long Long Time Passed,果不其然,等来了第一次的电话面试。

电话面试简单来说他一开始先问了我一些关于 Node 方面的内容,比如对中间件的理解,平时遇到的一些困难,对模块化的运用等等,感觉自己都能答出来。之后他也问了一些前端的内容,我也说了一下之前也玩过手机浏览器内核,对兼容性稍微了解一些,用的时候查一下不就好了,何必要记住呢LOL

之后他说道自己是阿里云这边的,因为阿里云要面向海外市场,需要考虑手机兼容性的事情,问我愿意不愿意去解决兼容性的事情。于是我很老实的回答道:可以去做,但是能少碰到就碰到吧~~现在回想起来,我应该直接回答可以,不用加后面那句话。不过我确实想说一下自己的心声吧。

基本没有太多的卡壳,开开心心的结束了电话面试。原本以为自己说的比较好了,但是很久很久之后,都没有消息。某一天,朋友突然说道:“哎,你不用参加阿里的笔试么?”,我说道:“不用啊,我是内推的。”。说完之后我心里犯嘀咕,话说为啥这么久都没有消息,战战兢兢的打开了 campus.alibaba.com,点击个人中心……

WHAT THE FUCK!!! 被回绝了,为啥连个短信都没有的。心中一万个 🐴 飞过。

不过好事就是,我自动从内推变成了自主投递了,至少还有一次新的机会,这也就是说明今晚我要和他们一起笔试了~~都不明白自己是怎么被回绝的,连个原因都不写的么 😫 不管了,平复一下自己的心情,开始写笔试吧,就算不是内推,自主投递也可以的么。

几天后,笔试开始,前面的选择题就不多说了,没什么太难的,都是一些概念性的问题,包括 js 和 前端的。

// 其实是我忘记有哪些选择题了

主要说一些有一道算法题,就是让你将二进制转成十进制,大家第一个想到的便是 Number.parseInt,当然我也是的。不过想想也知道,肯定不会这么简单,坑的地方在于这个二进制很大,js 在处理超过 52 位二进制数的时候就会有精度问题,但是我的时间不多了,也懒得去写高精度了,要同时去写乘法和加法。不给当时时间太紧,其实是有更好的方法的。

简单来说,其实就是高精度的变种,只不过我们不需要实现乘法和加法。具体我就不讲了。

后面还要一道 canvas 的题目,让你画一个东西,反正我以前玩过,看着以前的代码,很快就撸出来了。

最后一道是简答题,让你说一下在实现自动保存功能的时候会遇到什么问题,多人合作会出现什么问题?WTF,这是什么问题啊。还好平时用过 quip 这种基于行锁的多人文本编辑工具,反正就大体扯了扯,顺便扯了一下如何优化前端的速度,和减轻服务器的负载。

于是,笔试结束了。感觉稍微有点虚,不过还是稍微有点信心的。也算是平复了一下内推被拒的心情吧。

依旧是漫长的等待,转眼已经5月中旬多了。这个时候还没有消息,我感觉都要跪了,都要打算去找别的公司了,因为再不找的话,就要错过找实习的最佳时机了。各大公司基本都是这个时候开始找实习的。(一些小公司的暑期实习除外)

就在我打算放弃的时候,就收到了阿里的远程一面的通知,这个时候挺开心的,于是就开开心心的去准备面试的内容了,也没心思去找别的公司了。

之后远程的一面、二面、hr面,不知不觉就到了6月份了。

一面的时候,面试官还是我们旁边的学校,我是杭电的,他是理工的。他问了一些问题,主要是关于 Node 的。这次真的是一点前端的内容都没有问。终于问了一下 Node 的内存管理机制,不过话说回来,和 java 的有点像,但不完全一样。记得 java 是采用引用计数的方式,node 是标记清除。不过 java 还有很多别的算法。聊了聊平时项目中遇到的问题,聊了聊 redis 的使用中遇到的坑。不过因为我用的太少,基本没什么坑可以踩。顺便问了我一个问题,就是说如果发现一个服务器的压力过大,该怎么处理(原句忘了,不过大体是这个意思)。想了一会,想到应该大体从代码和架构方面下手,一个优化代码,一个布置集群。但是不知道他想问我那个,于是半天没说。结果他说到,其实很简单的,就是将服务拆分,分布在不同的机器上就可以了。想了一下,嗯他说的也对。不知不觉,面试结束了。最后他说了一句,感觉你基础还可以,就是实践有点少,不过我先帮你送到后面去吧。其实他有一句话挺实在的,就是想要在后端有发展,最好要去学学 Java。但是我就是想在 node 混,除非我觉得 node 已经实现不了公司的需求的时候,我再尝试转 java。

就这样,一面结束了,感觉自己好水,其实很多问题都不能准确清楚的答出来。毕竟自己平时只是做项目的时候用了一下,而且项目不大,不会遇到太多性能问题,也就没有太多深入研究(其实是没那么多时间去研究,而且可以玩的技术还有那么多,想去玩新技术,于是就懒得太深入研究了)

过了许久,也不知道几天,反正感觉挺漫长的,终于等来了终面。

阿里定义的终面有点歧义,不知道是实习生的终面还是技术的终面,不管了,既来之则安之。其实终面对于我来说就是技术二面和hr面LOL

二面,问的问题更加具体了一些,没有问太基础的东西。主要是针对我平时项目中遇到的一些问题展开问了一下。感觉比一面问的少,但是聊的还挺开心的。讲了讲平时 docker 分发部署代码的东东。讲了讲为啥想来阿里,是为了技术还是朴神、deadhorse 他们的,我说都有😂。感觉这一面,他更多的是按照我的简历来提问的,反正也是瞎比回答么~~

然后就结束了,我原本以为这样就结束了,过几天再等 hr 面试。然后我就去搞我自己的 minecraft 管理器了。

结果突然间收到一个短信,说我被 hr 翻牌了,赶紧去准备面试。

OMG,这么快的,我都没准备呢。算了,兵来将挡,水来土掩,硬着头皮就上了,反正是 hr 面,不太需要将技术什么的,聊聊天就好了。我也不喜欢在别人面前装,反正我想说啥就说啥吧,没必要为了 hr 而特意去说他喜欢听的话。不过稍微说的好听点,我还是可以的。😂聊了聊阿里的武侠文化,花名文化,倒立文化等等。聊了聊平时的爱好,喜欢做啥。大学当中的收获什么的。说起这个,感觉大学收获最大的就是大一的时候和一些大神们出去瞎混,参加各种活动,d2,day one,coding线下会,黑客马拉松。coding 线下会拿到了一个洋葱猴顺便还认识了现在的 cnode 站长 alsotang,不过那个时候他还是在阿里工作A_A。而且自己不善于交际,打字聊天还可以,张口说话就不知道该说啥了,所以那个时候他也不认识我。不过听 alsotang 和 wph95 聊得挺开心的,才知道钉钉在阿里其实是蛮流行的,不过钉钉没有 gcm ,差评。

不过不得不佩服 wph95 大神,codevs站长。也就是因为他,我知道了 Angular,Docker,Webpack,Stylus,Gulp 等等好玩的东西。虽然他是做 python 后端的,但是他的知识面真的广,前端后端运维都有涉猎。看他和各位同行大佬撕逼的感觉真爽,尤其知道当他们知道他还是一个杭电学生的时候,哈哈哈,想想就好玩。而且大二上就去 Daocloud 实习了~~真的佩服他是怎么认识那么多人的😂。反正大二之后就不怎么和他联系了,挺感激他的

hr 面也就这么结束了,面试我的 hr 有点胖,顺便调侃了一下他能不能倒立,哈哈哈。

反正这几场面试下来,比较自信但是又没有把握。于是我就在纠结,这个时候已经快6月份了,要不要再去看看别的公司,于是发现那些内推的消息或者实习生的消息,基本都是2、3个月以前的了。饿了么,美团似乎连实习生招聘的链接都没有。腾讯估计已经结束了。自己觉得赌一把吧,到时候再说。

于是这一赌,就等来了下面的这个消息。而且依旧没有短信,我也是前几天耐不住性子了,去刷新了一下。你知道从面试中的状态变成已回绝的状态有多绝望么😫

这个时候,我开始找各大公司的实习,心累中。。。你早回绝一下也行,非要等到这么晚才回绝,而且还没有通知。

总结一下吧,就是面试笔试没什么太大难度,最后就被 K.O. 了。感觉就跟哑巴吃黄连,有苦说不出。。。真心不知道自己哪里 gg 了。也许是最后跟 hr 聊天的时候说我自己太懒吧,经常容易拖。经常是立下 flag 说今晚写完 api,然后晚上去玩 hadoop 了🐭。突然想起来 google 的 instant app 什么时候才能普及呢,好喜欢这个,感觉 react native 可以下岗了。

或许有的人说,你应该早就多准备,多投几个公司。说实话,我不是没有想过,投很多公司,假如正好只有一个公司要我,那还好。就算是一个都不要都可以。最怕的就是同时好几个公司要,如何拒绝其他公司,感觉这好为难啊(PS:这并不表明自己有多么自信,只是讲述一种可能)

最后说一句,感觉自己和大神有一个很大的差距便是大神总会有各种各样的获取信息的途径,而我,只能听大神说🐶而且他们感觉有时候就是一个代码机器,维护着好几个库,还能天天 push。

注释和共享

All in One - Timer

发布在 All in One

All in one - Timer 定时器

目录

  1. All in one - Timer 定时器
  2. Golang
    1. Timer
    2. Ticker

这一篇文章,我们主要讲解一下不同语言之间关于定时器的操作和坑。

首先说一些这个坑中的一些前提,首先我这里所有的都是和 JavaScript 与 C++ 相比较进行学习的,所以下面不会出现 JavaScript 相关的教程,除非 JavaScript 在这个上面的坑太多,非讲不可。如果你没有相关的语言基础,请去学习。谢谢。

首先先从 golang 语言讲起来

Golang

在 golang 中单次定时器和循环定时器分别是 Timer 和 Ticker。这两个都是一个结构体,结构如下

1
2
3
4
5
6
7
8
type Timer struct {
C <-chan Time
// contains filtered or unexported fields
}
type Ticker struct {
C <-chan Time // The channel on which the ticks are delivered.
// contains filtered or unexported fields
}

其中 C 是一个 只读的 Time 类型的 channel,定时器根据传入的时间设置,定时向这个 channel 输入时间。你需要做的是等待这个 channel 中的数据。

每一个结构体都有如下方法:

1
2
3
func (t *Timer) Reset(d Duration) bool
func (t *Timer) Stop() bool
func (t *Ticker) Stop()

首先 Timer 和 Ticker 的区别不是很大,下面我们将两者的共性以 Timer 来讲述, 后面会针对两者不同的内容进行讲解。

1
2
3
4
5
6
7
8
9
package main

import "time"

func main() {
timer := time.NewTimer(time.Second * 2)
time := <- timer.C
println("Timer expired")
}

参数接受一个时间参数,表示多久后向 channel 传输数据,也就是你想要定时的间隔,上面这个例子表示两秒之后输出 Timer expired

那么每次我们都要这么麻烦的设置一个时间,然后等待 channel,运行制定的函数么?

其实不是的,golfing 给我们内置了一个函数,类似于 JavaScript 的 setTimeout,那就是 AfterFunc。

函数如下:

1
func AfterFunc(d Duration, f func()) *Timer

就是他会给你返回一个 *Timer ,并在指定时间之后,在 goroutine 中运行你的函数。

上面说完了如何启动,那么怎么停止呢?很简单,就是 Stop

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "time"

func main() {
timer := time.NewTimer(time.Second)
go func() {
<- timer.C
println("Timer expired")
}()
stop := timer.Stop()
println("Timer cancelled:", stop)
}
1
Timer cancelled: true

调用停止之后,会返回一个 Bool 值:

  • true 停止成功
  • false 已经被停止或者已经到期触发

这里有一点需要注意的是,调用停止之后,并不会关闭 channel,如果你想检测那么你可以通过额外添加一个 done channel 来协助。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "time"

func main() {
timer := time.NewTimer(time.Second)
done := make(chan bool)
go func() {
select{
case <- timer.C:
println("Timer expired")
case <- done:
println("Timer stop")
}
}()
stop := timer.Stop()
done <- true
println("Timer cancelled:", stop)
}
1
2
Timer stop
Timer cancelled: true

所以比较麻烦,大家可以自己封装一下。

Timer

单次定时器有一个特殊的方法就是 Reset,他可以将一个定时器的超时时间重新定义,这样你可以重复利用这个定时器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "time"

func main() {
timer := time.NewTimer(time.Second)
go func() {
<- timer.C
println("Timer expired")
<- timer.C
println("Timer expired again")
}()
time.Sleep(time.Second * 2)
timer.Reset(time.Second)
time.Sleep(time.Second * 2)
}
1
2
Timer expired
Timer expired again

同理,你可以用在 AfterFunc 中

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "time"

func main() {
timer := time.AfterFunc(time.Second, func() {
println("Timer expired")
})
time.Sleep(time.Second * 2)
timer.Reset(time.Second)
time.Sleep(time.Second * 2)
}
1
2
Timer expired
Timer expired

简直和 JavaScript 中的 setTimeout 像的不能再像了。

Ticker

这里的 Ticker 其实相当于 JavaScript 中的 setInterval,不过他没有类似 Timer 的 AfterFunc,只有一个最基础的构造器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "time"
import "fmt"

func main() {
ticker := time.NewTicker(time.Millisecond * 500)
go func() {
for t := range ticker.C {
fmt.Println("Tick at", t)
}
}()
time.Sleep(time.Millisecond * 1500)
ticker.Stop()
fmt.Println("Ticker stopped")
}
1
2
3
4
Tick at 2012-09-22 15:58:40.912926 -0400 EDT
Tick at 2012-09-22 15:58:41.413892 -0400 EDT
Tick at 2012-09-22 15:58:41.913888 -0400 EDT
Ticker stopped

Ticker 同样也有同 Timer 一样无法关闭 channel 的问题,解决方法和 Timer 类似,我就不多说了。

不过 Ticker 的 Stop 函数与 Timer 的不太一样,因为他没有是否触发过的问题,所以 Ticker 的 Stop是没有返回值的,这一点需要注意。

暂时先写这一个,以后如果有新的,持续更新。

注释和共享

目录

  1. 命令行工具构造工具之 yargs
    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. 可以
      7. 大家在一起吧 :) .option .options
        1. 有用但是很简单其余参数
      8. 小弟来了 (-_-) .command
        1. 这个位置是你的,别人抢不走 [arg1] <arg2>
        2. 默认命令 *
        3. 方便一点 .commandDir
      9. 从别的地方来 .config .env .coerce
    3. 总结

命令行工具构造工具之 yargs

简介

话说 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 相比前置知识的要求比较少,而且更加灵活。

注释和共享

目录

  1. 前言
  2. 工具
  3. 直播过程详解
    1. 直播源与 OBS
      1. OBS
      2. 下载/安装(自行解决)
      3. 概念
        1. 场景 & 来源
        2. 混音器
      4. 直播源
      5. 音频
        1. Step 1: Install iShowU Audio Capture
        2. Step 2:Setup Multi-Output Device
        3. Step 3: Switch to New Output
        4. Step 4:Set New Audio Input In OBS
        5. BUG
    2. 编码/转化/压缩 配置
    3. RTMP 协议传送数据
    4. 观看
  4. 总结

前言

随着现在互联网的发展,直播行业也是越来越火了,但是有一个现象就是大部分的直播客户端都是面向的 Window 系统,大部分的教程也是针对 Window 的,但我是一枚 Macer,所以我再这里将我使用 Macos 开启直播中重要的一些事情记录一下。

工具

直播过程详解

首先如果我们想要直播,那么首先要搞清楚直播的整个流程:

  1. 在不同的直播平台开启直播,然后获取 RTMP 地址和密钥
  2. 将 RTMP 地址和密钥填写到 OBS 的串流配置中
  3. 配置 OBS 直播界面,比如录制整个屏幕,录制某个应用程序,录制一个区域,添加字幕,添加本地影片等等内容
  4. 在 OBS 中开启直播,整个直播就开始了,其他人就可以在直播平台看到你的直播内容了

也就是说整个直播流的流动是这样的:

直播源 -> OBS 录制 -> 编码/转换/压缩 -> 通过 RTMP 协议将数据发送到直播平台 -> 用户打开直播平台 -> 获取数据 -> 直播播放

那么我们就针对整个直播流进行说明:

直播源与 OBS

由于这两部分可以在一起讲解,我们主要是讲解 OBS。

OBS

官网是这样描述这个软件的:

Open Broadcaster Software. Free and open source software for video recording and live streaming.

所以这款软件主要是面向的是视频录制和直播流,但是我不得不说用这个软件还是直播用的多。视频录制简单来说就是类似于 B 站的游戏视频似得,或者相关的视频教程。不过不得不说,如果是游戏录制的话,还是使用 navida 自带的视频录制好一些,那个对性能的影响不是很高。这个 obs 真占用 CPU >__>(后面会有相应的测试)。

下载/安装(自行解决)

在官网一个硕大的 Download,请自行点击,并选择合适的版本下载。

概念

场景 & 来源

如果是学过导播的同学,或许对这个并不陌生。那么我简单来说,用电视台举个例子,一个场景对应的是一个电视台的画面,而一个来源对应的电视台中的一个图层。比如说山东卫视是一个场景,但是山东卫视画面当中的台标是一个来源,电视剧画面是一个来源,底部滚动信息是一个来源,右下角广告是一个来源,这些来源共同组成了这个场景,也就是山东卫视的画面。

混音器

主要是针对输出的音频进行合成的

直播源

在 obs 启动之后,默认只有一个场景,但是场景里面是空的,那么我们需要添加自己需要录制或直播的内容:

在下方的来源中的右下角有一个加号,点击之后会有一个菜单让我们选择添加什么样的来源,有如下:

  • 图像 添加一个图像,类似于电视台添加台标
  • 图像幻灯片放映 添加图像幻灯片,不多解释
  • 场景 可以在一个场景中嵌入另一个场景的内容,类似在山东卫视里面播放了浙江卫视的内容
  • 媒体源 添加一个媒体,一般是视频
  • 文本 添加文字
  • 显示捕获 捕获屏幕
  • 窗口捕获 捕获一个窗口的内容
  • 游戏录制(Syphon) 相当于窗口捕获,不过他是利用 Syphon 直接从 GPU 中获取画面,而且没有窗口边框的,这点对于游戏直播来说还是不错的。
  • BrowserSource 嵌入一个网页

上面就是平时用到的一些内容,具体操作我就不讲解了,不同的来源有不同的配置内容,不过整体相对来说还是简单的,如果有需要的话,大家可以留言,我再详细讲解一下,当然还有音频的来源,这个我们后面再讲。

音频

在解决了画面的问题之后,那么我们剩下的问题就剩下音频了。大家或许想了,添加音频不是很简单的事情么,有啥复杂的。不过我想说的是,在 Window 上面或许确实简单,但是在 Macos 上面,由于 Apple 在 OSX10.11 加入的新的安全机制,导致无法正常的获取系统声音,麦克风的声音还是可以正常获取的。也就是说无法录制游戏内部的声音,但是可以录制你说话的声音。那么如果是 Linux 思维用户的话,那么就会直接把 SIP 关掉,哈哈哈。简单就是智慧,复杂就是愚蠢,😂

不过对于不想关闭的用户来说,还是有办法的。那么我们可以换个思路来思考,我们可以将音频输出到一个虚拟的输出设备,然后这个设备再虚拟成输入设备,在 OBS 从这个虚拟的输入设备中获取输入,就可以成功解决无法录制系统音频的问题了。

不过也有人问了,那这样的话,我不就听不到声音了么???解决办法就是:用另一台设备打开自己的直播网站,播放自己的直播视频,然后放在旁边,只不过可能会有几十秒的延迟而已 ^_^

不闹了,其实是用到了 Mac 自带的 Audio MIDI Setup ,需要这个软件先虚拟一个多输出设备,然后将音频输出到这个多输出设备上,然后由多输出设备将输出一部分送到扬声器,另一部分送到虚拟的输出设备,剩下的照常。

这样我们就解决了因为系统权限问题而无法获取系统声音的问题。

具体数据流见如下图:

Step 1: Install iShowU Audio Capture

首先解释一下 iShowU Audio Capture 是干什么用的,其实主要作用就是生成一个虚拟的输入输出设备。在这里,有很多的替代品,不过我觉得这个是比较纯粹的一个,只有这一个功能。

首先这里工具有官网链接,下载后安装即可。然后打开系统偏好设置 -> 声音,选择输出选项卡,会有一个叫做 iShowU Audio Capure 的选项,并且切换到输入选项卡,也可以看到一个叫做 iShowU Audio Capture 的选项,那么说明你已经安装成功了。

Step 2:Setup Multi-Output Device

打开 Audio MIDI Setup ,点击左下角的加号,选择创建多输出设备,分别在 iShowU Audio CaptureBuilt-in Output 前面勾选使用。

Step 3: Switch to New Output

打开系统偏好设置 -> 声音 -> 输出选项卡,选择我们刚刚添加的多输出设备,将输出定向到这个新的多输出设备上。

Step 4:Set New Audio Input In OBS

打开 OBS,选择添加音频输入来源,选择我们刚刚添加的 iShowU Audio Capture 即可。

下面我们可以进行一下测试,打开一个音乐播放,可以在混音器中看到有音频信号传入,并且扬声器中也有声音发出,那么说明你已经成功了!!!

赶紧打开一瓶饮料庆祝一下吧,不过且慢。🐛来了

BUG

此时如果你带着耳机你会发现麦克风不管用了,麦克风传入的信号也是系统声音,这是什么原因呢?不知。。。不过这里有一个解决方案。

创建一个输入聚合设备,跟创建多输出设备类似,不过这次我们选择创建聚合设备(aggregate device),在 Built-in MicrophoneiShowU Audio Capture 前面选中使用即可。然后再试一下,发现这个 BUG 是不是就解决了。如果还是不行,请在下方留言。

编码/转化/压缩 配置

这里主要说一下推荐的配置,针对 Macbook Pro 来说,建议使用 1280*800 的配置,码率选择 2000Kbps,帧速率 30 即可,这个配置下面可以达到较高的画质,而且 CPU 占用也不是很高。

RTMP 协议传送数据

在这里我就不详细解释 RTMP 了,也不解释 RTMP 和 HLS 的区别和联系了,这个大家可以自行 Google。

OBS 配置 RTMP 的位置是在设置 -> 串流里面,填写入地址和密钥即可。

保存之后,点击开始串流就可以进行直播了。

观看

这里顺便打个广告,我的 B 站直播间,主要是直播 Minecraft 和 Dota,纯属娱乐直播,没有什么意义。有时候可能还是直播 Coding。

总结

这篇文章算不上什么教程,也没有什么通俗易懂的图片之类的,因为大部分的内容都可以 Google 到,我这里只是针对一些可能出现问题的地方记录了一下,方便大家。

注释和共享

XGHeaven

一个弱弱的码农


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


Weifang Shandong, China