目录

  1. 心路历程
    1. 意外 —— 校招季拿到了网易 Offer
    2. 期待 —— 嗨,猪行动
    3. 满足 —— Mini 项目 『来人吖』
    4. 向往 —— 就要步入真正的业务线去了
    5. 愁 —— 为什么和我想象的不一样,甚至说是非常非常非常不一样
    6. 迷茫 —— 完全看不到方向
    7. 离开 —— 却没有任何留念
  2. 感谢
  3. 回首

思来想去,我决定还是要写一篇文章分享一下我在网易的经历和生活,我对网易的观点或者想法以及评论也许并不是客观公正的,我只是想从一个校招生的角度来讲述。

  • 2018 年 7 月 01 日,我从杭电毕业,进入了网易,从事大数据前端管理页面开发

  • 2019 年 3 月 15 日,我主动离开了网易,一共 257 天、6168 小时、370080 分、22204800 秒。

心路历程

进入网易后,我的心态也一点一点的发生着变化,这其中有一些是因为自己的原因,也有一部分是网易的原因,如果你愿意,我愿和你慢慢阐述

意外 —— 校招季拿到了网易 Offer

我原本没想给网易投递 Offer,那个时候我心里只有阿里,至于原因么,有句话说的好,『所有的 Node 大佬不是在阿里,就去在去阿里的路上』,虽然我不是大佬,但是我也有一颗想成为技术大佬的心。那个时候我只投递了阿里的 Offer。

可是事与愿违,18 届的校招可谓说是及其严格,基本都是社招要求,而且听说就招了 300 人(只是听说)。投了两次阿里云,都以失败告终(毕竟我的朴神来阿里云)。这个时候正好有同学在网易实习,我就看了下,有前端岗位,那就去下试试吧。

说实话,那个时候对网易的前端、Node 没有抱有任何希望。网易在前端的开源社区几乎没有任何动静,也几乎从来没参加过任何技术论坛,也从来没听说他们有用任何前端框架,可以说对网易前端没有任何的概念。

面试的时候我也就啥都没准备,纯裸考,只是觉得这公司进不进无所谓吧,甚至 HR 面的时候,稍微顶撞了一下 HR,HR 问了一个问题,我回答了,结果他说我回答的不是他问的问题,我思考了一下,坚定的回了他一句『我回答的就是你的问题,那您不是想问这个?』。结果意外的,我竟然进了,我当时自己都蒙了。直到入职之后才回过神来,原来是他们太缺人了。。。

但是毕竟校招,怎么样还是要试一下么,就安慰自己说可能他们内部用的技术挺好的,只不过按照丁磊的作风,可能不喜欢招摇吧。但后来结果实力打脸,事与愿违~

说实话,当时也找过其他的公司,可是杭州这边,真正既有技术实力、有大佬助阵、面向开源的公司几乎少的可怜,除了阿里几乎找不到几个,于是当时在好朋友的推荐下,投了有赞的前端、七牛的后端。

七牛的后端笔试直接跪了,有赞的拿到了 offer,但是当时脑抽觉得有赞技术不好(另外是我和室友都拿到了有赞的 offer,但是前端的老大跟我的室友说带着他一起搞波大事情,和我却啥都没说,就加了个微信,我觉得受到了某种歧视),而且觉得有赞的平台有点小,于是给推了。

后来真正进入到网易之后,觉得网易还不如有赞呢。或许大家认识有赞是因为 996,都认为有赞 996 不好,可是有赞和普通的互联网企业一样,并没有 996,相反比其他公司还有活力,有各种兴趣组织,甚至还有官方的游戏比赛,Dota/王者,比赛还有专人解说呢。而且钱多福利好。而且杭州除了阿里能数上的有赞也是接入企业滴滴。

反正说了这么多,最终我算是说服自己,现在网易学习 1 年,然后去我喜欢的公司。

期待 —— 嗨,猪行动

期待着进入网易的生活会是什么样子;期待着会不会遇见很多大佬;期待着网易的食堂有多么好吃;期待着能不能和同事很好的相处;期待能不能用到自己喜欢的技术;期待着别人能不能承认自己的能力。

一切的期待,都在拿到校招 Offer 的那一刻开始。

这期间参加了 『嗨,猪行动』,第一次以校招的身份进入网易,我们在 HR 小哥哥小姐姐们的安排下,一起参加了一些有趣有意思的活动。大家分成各个小队,以小队的身份参加比赛,每完成一个比赛,都会有一定的积分奖励,在活动结束之后,可以去换取很多严选的礼物,比如有行李箱、抱枕、牙刷、拖鞋、杯子、笔等等。

我觉得这场活动最成功的便是在最后,来了一场上百人吃鸡游戏,在园区散落着很多盒子,每个盒子里面武器或者,然后找到其他小队成员消灭之。虽然我们的枪只是简单的水枪,子弹也是可消除墨水,命只是一个简单的丝带绑在头上。而且我不得不承认这个游戏 Bug 太多,不过不得不说真的是很好玩的。

很快,虽然我很轻松的活到了前 10 名的样子,但是我是用某些不正常的方法(在某个门后躺着,假装自己已经死掉了,悠闲的等着;而且我们在这个游戏开始前就已经偷偷攒了好多武器和命,就是在玩吃鸡之前的游戏的时候),感觉不是太很公平,于是我就主动退出了。

静静的看着他们战斗,很快场上就只剩下 2 名妹子,一枚来自其他的组,还有一枚便是我们组的。于是最精彩的决赛来了,我们给他们腾出了场地,每人补足武器。

piupiupiu,piupiupiu,真的是太精彩了,我无法描述,但是结果有点遗憾,我们组的妹子惜败第二名。

一天如此开心,感谢 HR 小哥哥小姐姐们的辛勤付出,也感谢队友的努力奋斗,或许在此刻,我对网易的印象有了那么一些改变,感觉让人期待网易的生活会是怎么样子。

满足 —— Mini 项目 『来人吖』

2018 年 7 月 1 日,这一天终于到来了。我真正的不如了网易的大门,发现一切都是如想象的那么美好。

免费的四餐(包含夜宵喽),除了人太多以外味道还是很不错的;各种严选考拉的内购。有各种各样的折扣,而且有些还会直接在食堂门口摆摊。免费的班车简直不要太舒服,就是要每天 7 点半起床赶班车,真的是难受。园区免费停车、新能源免费充电,虽然这个时候我还没有买车,不过我已经在考虑中了。

入职之后,参加了 2 天的素质拓展,终于圆梦了我一直想去尝试的高空断桥,刺激、爽,还想再来一次。哈哈哈。

很快,最令我欣喜激动的事情来了,『Mini 项目』,简单点来讲,就是网易花了 100w 做了 8 个没啥用的产品。当然了,其实不是这样,重新讲一下,就是大家在一个月的时间里面,从开始,做出一个可以上线的产品,从中体验整个产品的开发流程,学习工作中用到的技术栈,培养同学之间的合作意识。这其实是一个非常有意义的事情,从中你可以体会到创业的过程,需求分析、竞品分析、优势分析、未来产品规划、UI 设计、技术架构、团队合作、测试保证、产品推广,每一步都可谓非常有价值。

当然了,这其中最让我满足的便是我在 Mini 项目中担任了技术负责人的身份,终于可以满足我想玩各种技术的心,把各种想用的技术都集成在一起,话不多说,先申请 10 台机器走起,Docker Swarm;Kafka;Node.js;NestJs;小程序;mpvue;Redis;TiDB;ES;CI/CD;Spring Cloud,简直不要太爽。

让我最欣慰满足的是虽然我这个技术负责人当的不是很称职,但是大家不嫌弃,而且一起推动团队的进步,而且对我定下的技术方向没有太多的异议,尤其是后端龙哥定的微服务化的 Spring Cloud 框架简直不要太符合我的胃口。感觉大家都是有技术追求,热爱技术。

另外最爽的就是入职一个月的便天天加班,甚至有几次我都是凌晨 12 点下班,到家都 1 点了,第二天还要 7 点半爬起来赶班车,谢天谢地那个时候还有顺风车。即便如此,我并没有觉得加班有多累,因为我们都有一个共同的目标——产品上线,拿到第一。

哦顺便说一句,那个时候我们就是想做顺风车匹配,只不过我们是做拼滴滴车,简单来讲就是大家都是乘客,匹配到一起了,由其中一个人打滴滴,然后大家一起走。这样做最大的好处便是省钱,比如说我回家快车 80,拼车 60,如果找齐 4 人,便只要 20 快,甚至比顺风车还便宜,而且有快车的服务。但是即便如此,开始产品评审的时候,评委一直无法绕开滴滴顺风车这个大对手,也对我们的产品产生了质疑,而且我们在这期间也质疑过很多次,直到我们做完了之后没多久,2018 年 8 月,顺风车下架了,顺风车下架了,顺风车下架了,重要事情说三遍,我们这才意识到我们的产品是有多么正确,多么正确,多么正确。

向往 —— 就要步入真正的业务线去了

一个月的 Mini 项目结束之后,虽然我们并没有取得第一名,但是我们在这期间收获了更多的东西。

小组里面的每个人就要到对应的业务线去了,有去云信的,有去严选的,有去云音乐的,也有去 AI、安全部门的。虽然这些部门无一例外经历了裁员。

有了 Mini 项目的经历之后,我对接下来步入真正的工作充满了无比的向往,会不会他们也和我们的组员一样有着技术追求,我能不能发挥我自己最大的力量给项目贡献代码,能不能维护以为开源项目,能不能听到很多大佬们的讲座。

以前这些问题我是有所担心的,但是有了 Mini 项目的经历之后,我更相信我所说的。可是事实却是……

愁 —— 为什么和我想象的不一样,甚至说是非常非常非常不一样

可是事实却是事与愿违,进入部门之后,我才发现是我想多了。

有的时候我一直在吐槽说我们项目技术差,比如什么只能用 ES5;没有 webpack;没有测试;没有 CI;虽然是前后端分离,但是代码却和后端放在一个仓库里面;Git 提交没有规范,各种 pull merge;Git Message 也没有规范;……这是一个已经三年的项目了,而且最有意思的是原本开始的时候是孟导定的用 Regular,想兼容 IE。但是我问现在不是只兼容 Chrome 么,这是为啥呢?他们和我说原因就是因为 IE 实在是兼容不了。这个时候我真想说一句,这个理由真的很棒。

这个问题不仅仅体现在前端,后端也是有如此的问题,觉得只要能解决问题就好了,要什么格式,要什么技术追求。这碗面条(代码)我只要保证计算机能吃就行,乱成啥样关我啥事。我只说一句,看了后端 Java 代码,我是第一次见识到 Space 和 Tab 缩进同时使用的,牛啤。

我甚至还开过一个玩笑说,前端越来越追求规范,开始越来越多的使用 TS 强类型的语言,而后端却想着如果绕开 Java 类型限定,内部几乎都是 JSONObject。当然这是一句玩笑,毕竟有些框架的东西你是没法直接用 JSONObject 的,但也能反映一些问题。

愁啊愁啊,当初的期待全部破灭,想推重构,可是我一个刚入职的又如何推的动?

愁啊愁啊,不仅仅是因为技术,而且内部其他的问题也很多,比如说 QA 测试问题、产品乱接需求、文档书写不全。。。夸张点说,我的大学技术社团除了干活没钱、人力不足以外,算是吊打我们部门,至少从技术上是这么讲的。至少社团的服务器环境是全 Docker 化的。都 9102 年了,还不会用 Docker 是来搞笑的么……

更愁的是,网易似乎不接受内部的合作,我曾经给网易云写了一个更好用对象存储 Node.js SDK,nos-node-sdk,想合并到他们的代码库里面,结果他们一直不给我回复。问他们为啥不改进代码,他说人手不够……我 go……我帮你写了,我帮你维护还不乐意啊,再说我就不吐槽你们的文档写的有多差了,错误一片一片的,也就我愿意认认真真的看下来了。

迷茫 —— 完全看不到方向

时间一久,我就陷入了迷茫期,找不到未来发展的方向,就像是我周围的同学都在踏步前进,有研究 Node 内核的,有使用 Egg 开发后台的,有用 React+Antd 的,还有 Typescript 写 IOS 组件的。而我,现在只能看着 Regular,对未来几乎没有任何帮助。换句话说,他们所在的环境至少给他们拓宽了不少眼界,而网易却没给我拓宽任何的眼界。

我也在安慰自己,毕竟学习是自己的事情,公司不给力,只能靠我们自己啦。确实我也在不停的努力,尝试学习其他的东西,可是这种学习毕竟是没有方向性的,我现在缺的是项目经历,而不是基础。基础就像是地基,只要地基够用,如何快速搭建起高楼是最重要的,而不是说连高楼都没造起来的时候想着拓宽地基。

另外,你见过那个专家是可以脱离他们的环境的?工作不用 React,却想成为 React 专家;工作不用 Java,却想成为 Java 专家;工作不接触 Node 内核,怎么成为 Node 专家;工作不接触浏览器内核;怎么成为浏览器专家。一个公司的天花板牵扯着你能早就多高的喽,也能让你知道你现在的基础够不够用,如何更好的提高自己。自我驱动的学习最大的用处是帮你补足基础,而只有真正的项目经历,才能真正让你有质的成长。

但是,这些东西在网易,很难寻得。

也许有人会出来说,网易里面技术栈也是很多的,React/Vue 都是有的。确实,有的确有,可能这又能如何?况且使用这部分技术的人也是少之又少,最关键的还是人!

离开 —— 却没有任何留念

最终我选择了离开网易,去寻找新的家园。

当我真正离开的时候,我才发现原来网易并不是那么的不堪。有的地方还是有许多值得学习借鉴的地方,可惜对于我们这些下层人民来讲,上层优秀的想法无法渗透到我们这里,在我们眼里,网易只是一个完成需求就可以下班的地方,不需要你有太多的想法,只需要按照上面的指示一步一步来做就可以了。

难得说网易的福利好,可惜我并没有沾上多少,食堂 Mini 项目之后我就搬离本部园区,在外面写字楼包了一块办公区,所以食堂基本吃不上了;免费停车就更不用说了,只能停本部,然后走一公里到我的工作地点;内部的技术分享全在园区,根本没时间赶过去,只能在线上看看了;各种摆摊更别说看见了;网易二期别人都搬过去了,为啥我们还不搬。简单点来讲,我培养不了我对网易的感情,对我来我,我就像是给大公司做外包,那我为啥不去一个钱多技术好的公司呢。

离开的时候,最大的愧疚便是对 HR 们,愧对了你们对我们的培养,真的真的真的非常抱歉,路过 HR 办公区的时候,害怕你看到我,虽然你还是看到了我。不过我们是主动离职的,比被裁的要好一点点,可惜就是没钱拿。

但是,这并不能阻止我离开网易。

感谢

  • 感谢陪伴和我一起走过 mini 项目的同学们

  • 感谢导师的帮助

  • 感谢身边的同事们

  • 最最感谢校招 HR 小姐姐小哥哥们的努力,为你们实力打 Call。感谢你们对我们的照顾和指导,让我们更快的融入到了网易的大世界中。谢谢,谢谢,尤其感谢森森和 6 姐。

回首

现在我入职了字节跳动,现在回过头来看了看,对我来说,这真的算是一个非常正确的决定,我也就不吐槽网易跟字节跳动相比的基础设施有多差了,我也不吐槽网易有多扣了。

如果你现在问我推不推荐网易,我觉得还是值得推荐的,但是一定一定一定要看部门,一定一定一定要看 Leader,一定一定一定要看环境。看部门有没有发展前景,看 Leader 愿不愿意培养自己,看环境会不会限制到你的天花板。

另外这里实力打 Call 考拉前端,足够开放,也有大佬,如果真的去可以去这个部门。当然严选的就是有点封闭了,搞了一套很不错的 npm 私有仓库,权限系统也做了,却不尝试在公司内部推广,搞不懂。云音乐和严选差不多,这里又要吐槽一下内部的 nei 是好难用,不仅仅是 ui 丑,而且这么久了,才上线在线 mock 功能,还不支持 swagger 导入,还不如之前我实习的公司大搜车推出的 easy-mock 呢。

过去就是过去,迎接未来,至少字节跳动能给我保证多劳多得,这就足够了!!!

另外说一句,有想要内推字节跳动的学弟学妹么,社招也可以哦,请用简历砸我吧,xutianyang.bradley@bytedance.com,如果想要和我交流的话,可以 Telegram 找我,@xgheaven。

注释和共享

目录

  1. 什么是可控和不可控?
  2. 为啥要区分呢?
  3. value? defaultValue? onChange?
  4. propName in this.props?
  5. Independence
  6. 如何使用?
  7. 总结

前言:本人入职之后算是第一次真正去写 React,发现了 React 的组件系统和其他框架的组件系统有些许的不同,这也触发了我对其中组件的可控性的一些思考和总结。

自从前端有了组件系统之后,有一个很常见但是却又被大家忽视的概念,就是可控组件(Controlled Component)和不可控组件(Uncontrolled Component)。

什么是可控和不可控?

官方详细讲解了什么事可控和不可控组件,虽然只是针对 input 组件的 value 属性来讲的。但是对于很多第三方组件库来讲,一个组件不止有一个数据属于可控。比如 Ant Design 的 Select 组件,valueopen 都属于可控的数据,如果你让 value 可控 open 不可控,那这到底是可控组件还是不可控组件呢?

所以从广义来讲使用可控/不可控组件其实不是很恰当,这里使用可控数据不可控数据更加合理一点。一个组件可能可能同时有可控的数据和不可控的数据。

可控数据是指组件的数据被使用者所控制。不可控数据是指组件的数据不由使用者来控制而是由组件内部控制。

之所以会有可控和不可控,主要是跟人奇怪的心理有关。如果把框架比作一个公司,组件比作人,组件之间的关系比作上下级。那么上级对下级的期望就是你既能自己做好分内的事情,也可以随时听从我的命令。这本身就是一件矛盾的事情,一边撒手不管,一边又想全权掌控。遇到这样的上级,下级肯定会疯了吧。

为啥要区分呢?

在 Vue 中,其实都忽视了这两者的区别,我们来看下面这个例子。

1
<input/>

上面是一个最简单 Input 组件,我们来思考一下如下几种使用场景:

  • 如果我只关心最后的结果,也就是输入的值,中间的过程不关心,最简单的方式是用 v-model 或者自己在 change 事件里面获取值并保存下来。
    这种场景是非常普遍,Vue 可以很好的完成,结果也符合人们的预期。

    1
    2
    3
    <input v-model="value"/>
    <!-- OR -->
    <input @change="change"/>
  • 如果我也只是关心结果,但是想要一个初始值。 也很简单,通过 value 传入一个静态字符串不就好了,或者传入一个变量,因为 Vue 的 props 是单向的。
    其中第三个方案并不是非常正确的方式,如果 initValue 在用户输入期间发生了更新,那么他将覆盖用户的数据,且不会触发 change 事件。

    1
    2
    3
    <input v-model="value"/> <!-- value 有初始值 -->
    <input value="init string" @change="change"/>
    <input :value="initValue" @change="change"/>
  • 我不仅仅关心结果,还关心过程,我需要对过程进行控制。比如说把输入的字符串全部大小写,或者锁定某些字符串。 熟练的工程师肯定可以写出下面的代码。
    但是这会有问题:

    1
    2
    <input v-model="value"/> <!-- watch "value",做修改 -->
    <input :value="value" @change="change"/> <!-- 在 change 中修改数据 -->
    1. 数据的修改都是在渲染 dom 之后,也就是说你不管怎么处理,都会出现输入的抖动。

    2. 如果通过第二种方法,恰巧你做的工作是限制字符串长度,那么你这样写 change(e) {this.value = e.target.slice(0, 10)} 函数会发现没有效果。这是因为当超过 10 字符之后,value 的值长度一直是 10,vue 没有检测到 value 的变化,从而不会更新 input.value。

出现这个问题最关键的是因为没有很好的区分可控组件和不可控组件,我们来回顾一下上面的某一段代码:

1
<input :value="value" @change="change"/>

你能从这块代码能看出来使用这个组件的用户的意图是什么呢?他是想可控的使用组件还是说只是想设置一个初始值?你无法得知。我们人类都无法得知,那么代码层面就不可能得知的了。所以 vue 对这一块的处理其实是睁一只眼闭一只眼。用户用起来方便,

用一个例子来简单描述一下:上级让你去做一项任务,你询问了上级关于这些任务的信息(props),然后你就开始(初始化组件)工作了,并且你隔一段时间就会向上级汇报你的工作进度(onChange),上级根据你反馈的进度,合理安排其他的事情。看起来一切都很完美。但是有的上级会有比较强的控制欲,当你提交了你的工作进度之后,他还会瞎改你的工作,然后告诉你,按照我的继续做。然后下级就懵逼,当初没说好我要接受你的修改的呀(value props),我这里也有一份工作进度呀(component state),我应该用我自己的还是你的?

对于人来说,如何处理上级的要求(props)和自身工作(state)是一个人情商的表现,这个逻辑很符合普通人的想法,但是对于计算机来说,它没有情商也无法判断究竟应该听谁的。为了克服这个问题,你需要多很多的判断和处理才可以,而且对于一些不变的值,你需要先清空再 nextTick 之后赋值才可以出发组件内部的更新。

最近入职之后,公司用到了 React,我才真正的对这个有所理解。

value? defaultValue? onChange?

如果对 React 可控组件和不可控组件有了解了可以跳过这块内容了。

让我们来看一下 React 如何处理这个的?我们还是拿上面的那三种情况来说:

  • 如果我只关心最后的结果,也就是输入的值,中间的过程不关心

    1
    <input onChange={onChange}/>
  • 如果我也只是关心结果,但是想要一个初始值

    1
    2
    <input defaultValue="init value" onChange={onChange}/>
    <input defaultValue={initValue} onChange={onChange}/>
  • 我不仅仅关心结果,还关心过程,我需要对过程进行控制

    1
    <input value={value} onChange={onChange}/>

当看完了这段你会很清楚的知道什么样的结构是可控,什么结构是不可控:

  • 如果有 value 那么就属于可控数据,永远使用 value 的值

  • 否则属于不可控数据,由组件使用内部 value 的值,并且通过 defaultValue 设置默认值

不论什么情况修改都会触发 onChange 事件。

React 对可控和不可控的区分其实对于计算机来说是非常合理的,而且也会让整个流程变的非常清晰。当然,不仅仅只有这一种设置的方式,你可以按照一定的规则也同样可以区分,但是保证可控和不可控之间清晰的界限是一个好的设计所必须要满足的

propName in this.props?

了解上面的概念之后,我们进入到实战环节,我们怎么从代码的层面来判断当前组件是可控还是不可控呢?

根据上面的判断逻辑来讲:

1
2
3
const isControlled1 = 'value' in this.props // approval 1
const isControlled2 = !!this.props.value // approval 2
const isControlled3 = 'value' in this.props && this.props.value !== null && this.props.value !== undefined // approval 3

我们来观察上面几个判断的方式,分别对应一下下面几个模板(针对第三方组件):

1
2
3
4
<Input value={inputValue} /> // element 1,期望可控
<Input value="" /> // element 2,期望可控
<Input /> // element 3,期望不可控
<Input value={null} /> // element 4,期望???

可以得到如下表格

是否可控 approval 1 approval 2 approval 3
element1 true true true
element2 true false true
element3 false false false
element4 true false false

大家第一眼就应该能看出来方法二其实是不正确的,他无法很好的区分这两种状态,所以直接 pass 掉。

眼尖的同学也会发现为什么 element 4 的期望没有填写呢?这是因为有一条官方的规则没有讲,这条规则是这样的:当设置了 **value** 属性之后,组件就变成了可控组件,会阻止用户修改 input 的内容。但是如果你想在设置了 **value** prop 的同时还想让用户可以编辑的话,只可以通过设置 **value****undefined****null**

在官方的这种规则下面,element 4 期望是不可控组件,也就是说 approval 3 是完全符合官方的定义的。但是这样会导致可控和不可控之间的界限有些模糊。

1
2
<Input value={inputValue} />
// 如果 inputValue 是 string,组件是什么状态?如果是 null 又是什么状态?

所以这里其实我推荐使用 approval 1 的方式,这也是 antd 所采用的。虽然不符合官方的定义,但是我觉得符合人们使用组件的一种直觉。第六感,=逃=

Independence

有了判断的方法,那么我们可以画出一个简单的流程图(Input 组件为例):

图片有点复杂,简单来讲就是每一次需要获取可控数据或者更新可控数据的时候,都需要检测一下当前组件的状态,并根据状态选择是从 props 中获取数据还是从 state 中获取数据已经更新的时候调用的是那个函数等等。图中有一些箭头的方向不是很正确,而且部分细节未画出,大家见谅。

如果只是添加这一个可控的属性 value ,这样写未尝不可,但是如果我们要同时考虑很多属性呢?比如说 Antd Select 组件就同时有 valueopen 两个可控属性,那么整个代码量是以线性方式增长的。这很明显是无法接受的。

于是这里我引入了 Independence 装饰器来做这件事情。架构如下:

我们可以这么理解,一个支持可控和不可控的组件本质上可以拆分成内部一个展示型的无状态受控的组件和外面的包装组件,通过包装(也就是高阶组件的方式)让内部受控组件支持不可控。

这样写其实有如下几个好处:

  1. 组件逻辑复杂度降低,只需要将组件的受控情况

  2. 可以将任意受控组件包装成不受控组件,尤其是对第三方组件的封装上

  3. 组件复杂度降低,代码冗余变少

  4. 非常方便的添加和删除受控属性,只需要修改装饰器即可

如何使用?

目前我简单实现了 Independence 装饰器,代码在网易猛犸开源的组件库 bdms-ui(建设中,组件不全、文档不全、时间不够,敬请期待)中,代码在此

他遵循这样的规范:假如属性名称为 **value**,那么默认值为 **defaultValue**,change 事件为 **onValueChange**。支持通过 onChangeName 修改 change 事件名称,通过 defaultName 修改默认值名称。

另外最简单的使用方式就是通过装饰器了,拿 Select 组件举例。

1
2
3
4
5
6
7
8
9
@Independence({
value: {
onChangeName: 'onChange'
},
open: {} // 使用默认值
})
export default class Select extends Component {
// blahblah,你就可以当受控组件来编写了
}

从此编写可控和不可控的数据从未如此简单。另外 Independence 还实现了 forward ref 的功能。

不过现在功能还比较薄弱,需要经过时间的检验,等完备之后可以封装成一个库。

总结

本文简单讲解了一下什么是可控和不可控,以及提出了一个 React 的解决方案。

这些只是基于我的经验的总结,欢迎大家积极交流。

注释和共享

目录

  1. 环境搭建
  2. useState 第一个接触的 Hook
  3. useEffect 监听开始和结束事件
  4. 其他 Hook
  5. 我们来用 Emotion 加点样式
  6. 收尾
  7. 总结复盘 —— 性能问题?
  8. 总结

阅读推荐:本人需要您有一定的 React 基础,并且想简单了解一下 Hook 的工作方式和注意点。但是并不详细介绍 React Hook,如果想有进一步的了解,可以查看官方文档。因为项目比较简单,所以我会比较详细的写出大部分代码。建议阅读文章之前请先阅读目录找到您关注的章节。

几天前,我女票和我说他们新人培训需要一个《真心话大冒险》的界面,想让我帮她写一个。我说好呀,正好想到最近的 React Hook 还没有玩过,赶紧来试试,于是花了一个晚上的时间,其实是俩小时,一个小时搭建项目,一个小时写。

Demo: http://souche-truth-or-dare.surge.sh (因为女票是大搜车的)

环境搭建

首先我们创建一个文件夹,做好初始化操作。

1
mkdir truth-or-darecd truth-or-darenpm init -y

安装好依赖,react@next react-dom@next parcel-bundler emotion@9 react-emotion@9 babel-plugin-emotion@9

React Hook 截止发稿前(2018-12-26)还处于测试阶段,需要使用 next 版本。

1
npm i react@next react-dom@next emotion@9 react-emotion@9npm i parcel-bundler babel-plugin-emotion@9 -D

创建 .babelrc 文件或者在 package.json 中写入 Babel 配置:

1
2
3
4
5
{
"plugin": [
["emotion", {"sourceMap": true}]
]
}

创建 src 文件夹,并创建 index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>真心话大冒险</title>
</head>
<body>
<div id="app"></div>
<script src="./index.jsx"></script>
</body>
</html>

index.jsx 文件

1
2
3
4
import * as React from 'react'
import { render } from 'react-dom'

render(<div>First Render</div>, document.getElementById('app'))

最后添加如下 scriptspackage.json

1
2
3
4
{
"start": "parcel serve src/index.html",
"build": "rm -rf ./dist && parcel build src/index.html"
}

最后我们就可以 npm start 就可以成功启动开发服务器了。在浏览器中打开 localhost:1234 即可。

parcel 已经内建了 Hot Reload,所以不需要进行额外的配置,开箱即用。是不是觉得非常简单,有了它,手动搭建项目不再困难。当然了,TS 也是开箱即用的,不过这次我这个项目真的很小,就不用 TS 了。

useState 第一个接触的 Hook

我们创建一个 App.jsx 开始我们真正的编码。先简单来看一下

1
2
3
4
5
6
7
8
9
10
11
export default function App() {
const [selected, setSelected] = useState('*')
const [started, setStarted] = useState(false)

return (
<div>
<div>{selected}</div>
<button>{started ? '结束' : '开始'}</button>
</div>
)
}

我们就完成了对 Hook 最简单的使用,当然了现在还没有任何交互效果,也许你并不明白这段代码有任何用处。

简单讲解一下 useState,这个函数接受一个参数,为初始值,可以是任意类型。它会返回一个 [any, (v: any) => void] 的元组。其中第一个 State 的值,另一个是一个 Setter,用于对 State 设置值。

这个 Setter 我们如何使用呢?只需要在需要的地方调用他就可以了。

1
<button onClick={() => setStarted(!started)}>{started ? '结束' : '开始'}</button>

保存,去页面点击一下这个按钮看看,是不是发现他会在 结束开始 之间切换?Setter 就是这么用,非常简单,如果用传统的 Class Component 来理解的话,就是调用了 this.setState({started: !this.state.started}) 。不过和 setState 不同的是,Hook 里面的所有数据比较都是 ===(严格等于)。

useState 还有很多用法,比如说 Setter 支持接收一个函数,用于传入之前的值以及返回更新之后的值。

useEffect 监听开始和结束事件

接下来,我们想要点击开始之后,屏幕上一直滚动,直到我点击结束。

如果这个需求使用 Class Component 来实现的话,是这样的:

  1. 监听按钮点击事件

  2. 判断是开始还是结束

    • 如果是开始,那么就创建一个定时器,定时从数据当中随机获取一条真心话或大冒险并更新 selected

    • 如果是结束,那么就删除之前设置的定时器

非常直接,简单粗暴。

用了 Hook 之后,当然也可以这样做了,不过你还需要额外引入一个 State 来存储 timer,因为函数组件无法持有变量。但是如果我们换一种思路:

  1. 监听 started 变化

    • 如果是开始,那么创建一个定时器,做更新操作

    • 如果是结束,那么删除定时器

好像突然变简单了,让我们想象这个用 Class Component 怎么实现呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default class App extends React.Component {
componentDidUpdate(_, preState) {
if (this.state.started !== preState.started) {
if (this.state.started) {
this.timer = setInterval(/* blahblah*/)
} else {
clearInterval(this.timer)
}
}
}

render() {
// blahblah
}
}

好麻烦,而且逻辑比较绕,而且如果 componentDidUpdate 与 render 之间有非常多的代码的时候,就更难对代码进行分析和阅读了,如果你后面维护这样的代码,你会哭的。可是用 useEffect Hook 就不一样了。画风如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default function App() {
// 之前的代码

// 当 started 变化的时候,调用传进去的回调
useEffect(() => {
if (started) {
const timer = setInterval(() => {
setSelected(chooseOne())
}, 60)

return () => clearInterval(timer)
}
}, [started])

return (
// 返回的 View
)
}

当用了 React Hook 之后,所有的逻辑都在一起了,代码清晰且便于阅读。

useEffect 从字面意义上来讲,就是可能会产生影响的一部分代码,有些地方也说做成副作用,其实都是没有问题的。但是副作用会个人一种感觉就是这段代码是主动执行的而不是被动执行的,不太好理解。我觉得更好的解释就是受到环境(State)变化影响而执行的代码。

为什么这么理解呢?你可以看到 useEffect 还有第二个参数,是一个数组,React 会检查这个数组这次渲染调用和上次渲染调用(因为一个组件内可能会有多次 useEffect 调用,所以这里加入了渲染限定词)里面的每一项和之前的是否变化,如果有一项发生了变化,那么就调用回调。

当理解了这个流程之后,或许你就能理解为什么我这么说。

当然了,第二个参数是可以省略的,省略之后就相当于默认监听了全部的 State。(现在你可以这么理解,但是当你进一步深入之后,你会发现不仅仅有 State,还有 Context 以及一些其他可能触发状态变化的 Hook,本文不再深入探究)

到现在,我们再来回顾一下关于定时器的流程,先看一下代码:

1
2
3
4
5
6
7
if (started) {
const timer = setInterval(() => {
setSelected(chooseOne())
}, 60)

return () => clearInterval(timer)
}

理想的流程是这样的:

  • 如果开始,那么注册定时器。——Done!

  • 如果是结束,那么取消定时器。——Where?

咦,else 的分支去哪里了?为啥在第一个分支返回了取消定时器的函数?

这就牵扯到 useEffect 的第二个特性了,他不仅仅支持做正向处理,也支持做反向清除工作。你可以返回一个函数作为清理函数,当 effect 被调用的时候,他会先调用上次 effect 返回的清除函数(可以理解成析构),然后再调用这次的 effect 函数。

于是我们轻松利用这个特性,可以在只有一条分支的情况下实现原先需要两条分支的功能。

其他 Hook

在 Hook 中,上面两个是使用非常频繁的,当然还有其他的比如说 useContext/useReducer/useCallback/useMemo/useRef/useImperativeMethods/useLayoutEffect

你可以创建自己的 Hook,在这里 React 遵循了一个约定,就是所有的 Hook 都要以 use 开头。为了 ESLint 可以更好对代码进行 lint。

这些都属于高级使用,感兴趣的可以去研究一下,本片文章只是入门,不再过多讲解。

我们来用 Emotion 加点样式

css-in-js 大法好,来一顿 Duang, Duang, Duang 的特技就好了,代码略过。

收尾

重新修改 src/index.jsx 文件,将 <div/> 修改为 <App/> 即可。

最后的 src/App.jsx 文件如下:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import React, { useState, useEffect } from 'react'
import styled from 'react-emotion'

const lists = [
'说出自己的5个缺点',
'绕场两周',
'拍一张自拍放实习生群里',
'成功3个你说我猜',
'记住10个在场小伙伴的名字',
'大声说出自己的名字“我是xxx”3遍',
'拍两张自拍放实习生群里',
'选择另一位小伙伴继续游戏',
'直接通过',
'介绍左右两个小伙伴',
]

function chooseOne(selected) {
let n = ''
do {
n = lists[Math.floor(Math.random() * lists.length)]
} while( n === selected)
return n
}

const Root = styled.div`
background: #FF4C19;
height: 100vh;
width: 100vw;
text-align: center;
`

const Title = styled.div`
height: 50%;
font-size: 18vh;
text-align: center;
color: white;
padding: 0 10vw;
font-family:"Microsoft YaHei",Arial,Helvetica,sans-serif,"宋体";
`

const Button = styled.button`
outline: none;
border: 2px solid white;
border-radius: 100px;
min-width: 120px;
width: 30%;
text-align: center;
font-size: 12vh;
line-height: 20vh;
margin-top: 15vh;
color: #FF4C19;
cursor: pointer;
`

export default function App() {
const [selected, setSelected] = useState('-')
const [started, setStarted] = useState(false)

function onClick() {
setStarted(!started)
}

useEffect(() => {
if (started) {
const timer = setInterval(() => {
setSelected(chooseOne(selected))
}, 60)

return () => clearInterval(timer)
}
}, [started])

return (
<Root>
<Title>{selected}</Title>
<Button onClick={onClick}>{started ? '结束' : '开始'}</Button>
</Root>
)
}

总结复盘 —— 性能问题?

最近刚刚转正答辩,突然发现复盘这个词还挺好用的,哈哈哈。

虽然这么短时间的使用,还是有一些自己的思考,说出来供大家参考一下。

如果你仔细思考一下会发现,当使用 useEffect 的时候,其实每次都是创建了一个新的函数,但并不是说每次都会调用这个函数。如果你代码里面 useEffect 使用的很多,而且代码还比较长,每次渲染都会带来比较大的性能问题。

所以解决这个问题有两个思路:

  1. 不要在 Hook 中做太多的逻辑,比如说可以让 Hook 编写一些简单的展示组件,比如 Tag/Button/Loading 等,逻辑不复杂,代码量小,通过 Hook 写在一起可以降低整个组件的复杂度。

  2. 将 Effect 拆分出去,并通过参数传入。类似于这个样子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function someEffect(var1, var2) {
    // doSomething
    }

    export function App() {
    // useState...
    useEffect(() => someEffect(var1, var2), [someVar])
    // return ....
    }

    虽然这也是创建了一个函数,但是这个函数创建的速度和创建一个几十行几百行的逻辑的函数相比,确实快了不少。其次不建议使用 .bind 方法,他的执行效率并没有这种函数字面量快。

    这种方式不建议手动来做,可以交给 babel 插件做这部分的优化工作。

其实作为一个开发者来说,不应该太多的关注这部分,但是性能就是程序员的 XX 点,我还是会下意识从性能的角度来思考。这里只是提出了一点小小的优化方向,希望以后 React 官方也可以进一步做这部分的优化工作。

已经有的优化方案,可以查看官方 FAQ

总结

经过这个简短的使用,感觉用了 Hook 你可以将更多的精力放在逻辑的编写上,而不是数据流的流动上。对于一些轻组件来说简直是再合适不过了,希望早点能够正式发布正式使用上吧。

另外 parcel 提供了强大的内置功能,让我们有着堪比 webpack 的灵活度却有着比 webpack 高效的开发速度。

好的,一篇 1 小时写代码,1 天写文章的水文写完了。以后如果有机会再深入尝试。

注释和共享

目录

  1. position: sticky 这究竟是一个什么鬼?
  2. 正确的使用姿势
  3. 举个栗子 - 通讯录列表头部
  4. 拓展思考
    1. 如何检测是否已经被固定?
    2. 那能不能实现表格头/列固定呢?

不知何时,曾经我们认为的东西便会被打破,如果我们不坚持着去学习,那么我们终将会被社会所淘汰。于是我决定写《后知后觉系列》来记录一下我曾经跟不上的知识和关键点,内容不一定复杂,内容含量不一定高,也许别人已经写过一个一样的教程了,但是希望你能从我的笔记中获取你认为重要的东西,在纷繁复杂的工作中留下一个真正极客的世界,希望某一天这些东西都能够运用到工作当中。——XGHeaven

记得先看一下目录,找到你喜欢好奇的内容去针对性阅读,毕竟我不是来写教程的。

position: sticky 这究竟是一个什么鬼?

最近公司在用 Regular 封装一个表格组件,需要实现固定表头的功能。这个是几乎所有的组件库都会实现的一个效果,所以实现方式有很多种:

  1. 因为 thead/tr 的 position 属性是无效的,所以需要单独用 div 创建一个表头。然后设置这个表头的 position: absolute,同时 top: 0。同时这种模式下,需要用户指定每一列的宽度,保证自制的表头和下面原生的表格一一对应起来。如果不指定的话,也可以等待 dom 渲染完成之后,再测量宽度。比如 Ant Design 就是使用的这种方式。

  2. 因为上面那种方案的难点在于无法很好的保证自制表头和原生表格宽度的一致性,所以我们组的大佬提出了使用原生 thead,监听 scroll 事件,设置 transform 属性使得表头进行偏移,从而实现 fixHeader 的问题,这种方式解决了第一个的问题,但是需要手动监听 scroll 事件,在快速滚动的情况下,可能会有一定的性能问题。而且不够优雅。如果后面的表格内容中有 position: relative 的元素,会覆盖到表头。

不管是哪种方式,我总感觉不是很完美,于是我就在思考,除了手动更新的方式,难道就没有一些比较好的方式去做。然后我就去翻看了 github 的固定表头的方式,顿时豁然开朗。于是就延伸出了这篇文章,position: sticky 属性。

Pay Attention:后面所讲的内容就不怎么和表格固定表头相关,如果你对表格固定表头或者固定列有一定问题,可以查看网易考拉的这篇文章 《一起来聊聊table组件的固定列》

当第一眼看到这个熟悉的时候,第一句话就是“我 CA”,这 TMD 是什么鬼属性,position 什么时候有了这个属性。于是去看了 MDN 的介绍,可以理解为,这个属性是实现固定顶部最简单的实现方式

他其实是一种 position:relativeposition: fixed 的结合体,一定要配合 top/right/bottom/left 的属性一起才有作用,设置对应方向的最小值。当大于最小值的时候,他就像 relative 一样,作为文档流的一部分,并且 top/right/bottom/left 属性也会失效。否则当小于设置的值的时候表现的像 fixed,只不过这个 fixed 不再现对于窗口,而是相对于最近的可滚动块级元素。

如果你看过其他关于 sticky 的文章,大部分都会以黏贴的意思来解释他,那么很明显,确实也是这个意思,如果你觉得看了其他教程能够清楚的话,那么可以不用看我这篇了,如果你没看懂的话,可以来我这里看看。

废话少说,我们先来看一下如何正确使用 sticky。

正确的使用姿势

以下的代码预览请使用最新 Chrome 查看,或者支持 position: sticky 的浏览器查看。部分网站不支持 iframe,可以去我的 Blog 查看

  1. position: sticky 只相对于第一个有滚动的父级块元素(scrolling mechanism,通过 overflow 设置为 overflow/scroll/auto/overlay 的元素),而不是父级块元素。

  2. position: sticky 只有当设置对应的方向(top/right/bottom/left),才会有作用,并且可以互相叠加,可以同时设置四个方向。

  3. 即使设置了 position: sticky,也只能显示在父级块元素的内容区域,他无法超出这个区域,除非你设置了负数的值。

  4. position: sticky 并不会触发 BFC,简单来讲就是计算高度的时候不会计算 float 元素。

  5. 当设置了 position: sticky 之后,内部的定位会相对于这个元素

  6. 虽然 position: sticky 表现的像 relative 或者 fixed,所以也是可以通过 z-index 设置他们的层级。当这个元素的后面的兄弟节点会覆盖这个元素的时候,可以通过 z-index 调节层级。

    See the Pen position: sticky 通过 z-index 调节层级 by Bradley Xu (@xgheaven) on CodePen.

当你懂了这几个之后,其实这个属性就用起来就很简单了。

举个栗子 - 通讯录列表头部

no code no bb,直接上代码。

拓展思考

如何检测是否已经被固定?

最常见的需求就是,当还在文档流当中的时候,正常显示,但是当固定住的时候,添加一些阴影或者修改高度等操作。要想实现这个效果,第一反应可能就是手动监听 scroll 事件,判断位置,这当然是没有问题的,但是随之而来的确实性能的损耗。

最好的方式是使用 IntersectionObserver,这是一个可以监听一个元素是否显示在视窗之内的 API,具体内容见阮老师的《IntersectionObserver API 使用教程》。基本原理就是在一段滚动的头部和尾部分别添加两个岗哨,然后通过判断这两个岗哨的出现和消失的时机,来判断元素是否已经被固定。

例子详见此处

那能不能实现表格头/列固定呢?

–理想很丰满,显示很骨感,因为 thead/tbody 对 position 无爱,所以也就不支持 sticky 属性,所以我们还是要单独创建一个头部。–

后来经过网友提醒,自己又去研究了一下,发现还是有办法做到固定表头和列的。

首先针对 Firefox,它本身就支持 thead/tbody 的 position 属性,所以可以直接通过对 thead/tbody 设置 position 来实现。而对于 Chrome 浏览器来讲,可以通过设置 thead 内的 th 来实现。具体见 Demo.

See the Pen position sticky 通过设置 td 来实现固定表头 by Bradley Xu (@xgheaven) on CodePen.

然后好像就没有了,谢谢观看水水的《后知后觉系列》

注释和共享

目录

  1. Trouble 1: U 盘启动找不到启动盘
    1. 问题描述
    2. 解决方案
  2. Trouble 2: 安装的时候明明插着网线但是却提示无网络
    1. 问题描述
    2. 解决方案
  3. Trouble 3: CentOS 安装的时候一直卡死在设置安装源
    1. 问题描述
    2. 解决方案
  4. 安装完毕

自己家里有一台 Dell 服务器,之前一直跑着 FreeNAS,后来发现自己对 NAS 的需求并不是很高,所以我决定装回 Linux,之前用的 Debian,虽然 Debian 很好,但是没法只使用 root 用户,所以我又回归了 CentOS 系统,但是问题并没有我想的那么简单。

另外,这是一篇总结文章,所以没有图,见谅。

Trouble 1: U 盘启动找不到启动盘

这里所说的找不到启动盘并不是说无法进入引导界面,而是说进入了引导界面但是无法正确加载安装镜像。具体表现请往下看

问题描述

相信大家装过系统的都知道,在 Windows 上面有很多 ISO 刻录到 U 盘的工具,这些工具非常好用,所以我就理所当然的用 ISO to USB 这个软件刻录启动 U 盘。

我刻录的是 CentOS-7-DVD 的版本,大约 4.2G,里面包含了必要的安装包。

U 盘刻录完成,插入电脑,启动,一切正常,进入选择界面,选择 Install CentOS 7,之后问题来了,出现了 failed to map image memory 提示,之后等了一段时间之后就一直出现 warning:dracut-initqueue timeout-starting timeout script 的提示,最终显示启动失败,进入恢复模式,显示 dracut:/# 终端提示符,不管怎么重启都不行。

当然,Google 当然是有结果的,《安装CentOS7.4出现failed to map image memory以及warning:dracut-initqueue timeout的解决办法》 指出了原因和解决方案。

解决方案

出现这个原因是因为找不到启动盘,解决方案其实也很简单,就是手动设置一下就可以了。

首先找到你的 U 盘是哪个硬盘符,方法就是在恢复模式下运行 ls -l /dev | grep sd,可以看到一系列的文件,一般情况下 sda 是硬盘,sdb 是 U 盘,sda1 是硬盘上第一个分区,同理 sda2 是第二个分区。如果你有多个硬盘,那么 U 盘可能是 sdc, sdd 等等,找到你的 U 盘启动盘的分区,我的是 sdb1。

然后在选择界面的时候按 e,然后将 inst.stage2=hd:LABEL=CentOS\x207\x20x86_64.check 修改为 inst.stage2=hd:/dev/sdbx(你u盘所在),之后修改结束之后按 Ctrl+x 退出就可以正常进入安装界面了。

那么有人会说每次启动都要这么做么?答案是的,但实在是太麻烦了,不急,这个也是有办法解决的,办法请看 Trouble 3 的解决方案。

Trouble 2: 安装的时候明明插着网线但是却提示无网络

问题描述

进入安装界面之后,会有一个网络的选项,一直提示未连接,不管你怎么设置。无论是你拔插网线还是禁用启用都不行。

解决方案

这是因为 CentOS 默认的网络是不自动连接的,你可以在设置界面,选择 General ,也就是第一个标签页,把启动时自动连接网络的选项勾选,然后保存。然后就会自动去获取 ip 地址然后可以上网了。

这一点的设计实在是太 SB 了,不知道为啥要设计成这个样子。

Trouble 3: CentOS 安装的时候一直卡死在设置安装源

问题描述

当我们成功进入安装界面的时候,你会发现他的安装源是无效的,需要你自己去设置。 理论上来讲使用的是 DVD 的镜像,是自带了很多包的,不会出现这种情况。 而且出现这种情况的时候,是无法选择本地的安装源的,只能填写网络。 但是不管你填写的是官方的安装源还是阿里、网易的安装源,都会一直卡死在设置安装源这个环节。

解决方案

经过 Google,得知原因是因为刻录 U 盘的方式不对。《CentOS7安装时的新问题》 给出了原因和解决方案。

引用之

绝望中,无意间看到 Centos 百科(https://wiki.centos.org/zh/HowTos/InstallFromUSBkey) 上的一段话:“由于 CentOS 7 安装程序的映像采用了特殊的分区,而截至 2014 年 7 月,大部份Windows 工具都不能正确地转移,因此利用 USB 存储器开机时会导致不能预知的结果。(暂时)已知不适用的工具包括 unetbootin 和 universal usb installler。已确定能正确运作的有 Rufus、Fedora LiveUSB Creator、Win32 Disk Imager、Rawrite32 及 dd for Windows。如果采用 Windows 7 以上的版本,请先卸下该 USB 存储器(其中一个方法是在执行工具程序前把存储器格式化),否则 indows 可能会拒绝写入该存储器,出现 can’t write to drive 错误及取消行动。”

可以得知,CentOS 的 ISO 镜像的刻录方式比较特殊,大部分软件都无法很好的兼容,但是 dd 可以非常好的兼容,所以这里我切换了刻录软件,使用 Rufus 进行刻录,并且选择 DD 模式。

然后重启启动,哈哈哈,不仅仅这个问题解决了,还完美解决了 Trouble 1 无法找到启动盘的问题。

安装完毕

记得之前安装的时候,也没有遇到这么多麻烦。这次既然遇到了就记录下来。

之后我就可以开心的玩耍了,装了 Docker,切换安装源到网易,安装 epel 等等。

注释和共享

目录

  1. 放弃 npm?
    1. 上古时代
    2. 软链时代
  2. npm3 时代
  3. yarn 的诞生
  4. 重拾 npm 5
  5. 总结

从接触到 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。

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

注释和共享

目录

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

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

注释和共享

2018 年终总结

发布在 年终总结

目录

  1. 实习
  2. 大学
  3. 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 详解

发布在 Frontend

目录

  1. 引言
  2. Metadata
  3. Reflect Metadata
    1. 概念
    2. 安装/使用
    3. 类/属性/方法 装饰器
    4. 原型链查找
    5. 用途
    6. API
  4. 深入 Reflect Metadata
    1. 实现原理
  5. End
  6. 题外话

引言

在 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
@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
@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
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
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
@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 了。不是很仔细的阅读了一下,好像还是会增加引用计数。

注释和共享

Boostnote 是一款通过 Electron 构建的桌面笔记应用。它支持离线存储,无需注册、Markdown 编辑、像 Github Gist 的代码片段的管理等等内容。全球 198 个国家和地区的程序员们在使用这款应用。

Boostnote 就如同器名字一般,最主要的一个用途便是 Markdown 笔记编辑器。你所有的 Markdown 笔记拥有自动保存的功能,并且支持多种展示格式。拥有半实时的预览,所以你可以及时的检查最后的格式是否是和你所输入的一致。

Latex 公式编辑器也内置在 Boostnote 当中,你可以很轻松的插入各种公式在你的笔记中。

不管是笔记还是代码片段都可以通过标签的方式进行管理。

对于代码片段来说,他支持高达 100 多种语言的高亮,其中包括 JavaScript, Python, HTML 和 CSS。当然你也可以在一份代码片段当中存储多段代码,比如说你可以同时存储 HTML,CSS,JS 代码在一份当中。而且不管你是用什么样子的缩进(tab/空格)或者缩进的程度(2个字符,4个字符,8个字符)都可以在文档中进行配置。

最后,将笔记导出成普通文本(.txt) 或者 Markdown(.md) 也是支持的功能。

外观(UI)

丰富的快捷键可以让你让更快的浏览、搜索笔记,以及更快的执行一些重要操作。

对于 Boostnote 的外观来说,你有很多种不同的主题可以选择。当然,编辑器的高亮也是有很多种可以选择的,你可以根据你自己的喜好自由搭配。你可以在 Preferences > UI > Theme 查看支持的主题。

下载

Boostnote 目前是开源的,你可以通过官网下载。不过要注意的是,存储使用的是亚马逊的 s3,所以你需要能够翻墙,否则无法下载。

它目前是支持全平台(Windows, MacOS, Linux,IOS,Android)

注释和共享

XGHeaven

一个弱弱的码农


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


Weifang Shandong, China