技术月刊:2023年02月

发布于 更新于
解决一些工程问题

@types/react的错误解析

最近一年,在使用例如react-router/BrowserRouterantd/DatePicker等组件的时候,经常遇到一个TS报错,内容是:

TS2786: 'View' cannot be used as a JSX component.

它并不是一个真正的BUG,它仅仅只是类型标注错误,只在开发时提示,并不影响实际运行编译。也许TS在类型传导过程中的某个环节计算出了错误的类型。问题不大,但是非常烦人,因为项目中经常会用到这些组件,不可能每个组件都写个ts-ignore啊。

这个问题困扰了我很久,最后我终于找到了解决方案,参考阅读:eslint#15802

如果仔细看一下报错提示,会看见下面展开的更多提示中有这样一行:

Type 'React.ReactNode' is not assignable to type 'import("xxx/node_modules/@types/react-dom/node_modules/@types/react/index").ReactNode'.
  Type '{}' is not assignable to type 'ReactNode'.
    Type '{}' is missing the following properties from type 'ReactPortal': key, children, type, props

第一句这句话的意思是:有两个ReactNode类型,它们名字相同,但是来自不同的地方。

  • 一个是由外部的React.FC限制的,来自于@types/react中的类型
  • 一个是由内部组件传出来的,来自于@types/react-dom中的类型。

但问题就在于,后面@types/react-dom它里面自带了一个node_modules,里面安装了另一个版本的@types/react!!也就是说,在这个项目中同时存在两个版本的@types/react(一个17.x,一个18.x),这两个版本的ReactNode是不匹配的,因此TS给出类型错误的提示。

关于ReactNode出现不兼容的变化(breaking-change),问题在ReactPortal这个类型的定义,从v18开始它多了一个奇怪的{}类型。

所以锅是出在@types/react-dom(以及react-routerreact-router-dom)这个库里!!如果仔细查看一下它的源文件,会在@types/react-dom/package.json中看到非常让人吐血的一行定义:

{
  "name": "@types/react-dom",
  "version": "17.0.14",
  "dependencies": {
    "@types/react": "*"
  }
}

看见没,它所依赖的@types/react的版本居然是*!!然后在2022年,也就是react18上线之后,一切都不好了——也就是说,项目依赖中一部分引用的是v17的类型,另一部分引用的是v18的类型,混在一起就会被提示类型错误了。

我们不能直接改已经发布的@types/react-dom的源文件(即package.json),因此我们只能手动修改本地项目中的yarn.lock,来强制将它解析到相同的版本,也就是v17版本上去。

解决方案是使用Selective dependency resolutions,也就是在根目录的package.json中指定一个强制解析版本号,解决效果非常优秀。

CSS将子元素自适应填满父元素

一开始我一直在尝试用熟悉的flex或者grid实现,然而在子元素尺寸、比例不确定的情况下,似乎用纯css是做不到的?

搜索了一下我才想起这个来:关键语法是object-fit: contain;兼容性很好。参考文章

Timer的类型

setTimeoutsetInterval他俩都有返回值。这个返回值的术语叫做『Timer』,可以用来clearTimeout或者clearInterval,这个大家应该都很熟悉了。

小知识:Timeout和Interval两个的Timer在常规浏览器中是可以通用的,他们共享同一个编号池,参考。不过从语义上来说,最好还是严格区分Timeout和Interval。

但是Timer的类型却有坑,在浏览器(即window)环境下,它的类型是number;而在node.js环境下,它的类型是NodeJS.Timer

在写代码时,IDE往往会提供『node.js类型辅助』,也就是@types/node这个类型库。而它会带来一个小小的副作用:它无法分别你的代码是window还是nodejs,所以一律给你把类型标注成了NodeJS.Timer

解决方案参考,这个方案不是强硬地指定运行环境,而是从语义上来规范,达到一种非常理想的效果。核心代码如下:

let timer: ReturnType<typeof setTimeout>;

clearTimeout(timer);

contenthash在webworker编译条件下生成undefiend

参考:webpack 中,hash、chunkhash、contenthash 的区别是什么? 虽然没有直接解决我的问题,但是比较清楚地介绍了几种模式的区别。

在使用webpack打包的前端工程中,对于WebWorker的入口代码文件是需要特殊处理的(在我之前的文章《Web Worker 试玩》中有过介绍)。

如果只是写在同一个包里,问题不大,但是如果将Worker的入口部分代码拆分到另一个包(npm package)里,此时webpack的处理就出现bug了。具体现象就是入口文件的文件名的contenthash部分被解析为undefiend然后被devServer响应404了。文件名大概长这样:

http://127.0.0.1/xxx_src_xxx-worker_entry_ts.undefined.js

经过分析,原因是webworker所在的代码被splitChunks.cacheGroups包含进去了,因此devServer环境下获取不到主entry相关的信息,导致产生了包含undefiend的url引用:

解决方式一:将filename中的[contenthash]改为[hash],但是会导致新代码上线时http缓存无法再利用;

解决方式二:放弃splitChunks.cacheGroups,问题也是影响http缓存利用效率,此时影响的是split的部分无法有效再利用。

综合考虑我觉得方式二会更合适一些。

参考开源项目源码,flv.js这个库借助了webworkify-webpack来进行封装,理由也可以理解,毕竟开源项目有兼容性上的考虑,不能像我们内部项目一样“面向webpack”开发。

2023-08-17 更新:今天更新了 webpack 相关依赖,现在已经不会出现这个问题了,devServer 可以正确处理 WebWorker 的文件名,不需要额外处理。(webpack: 5.49.0->5.88.2,webpack-dev-server: 4.2.0->4.15.1,webpack-cli: 4.10.0->5.1.4)

闲谈:作为负责人

有两个问题我思考了很久:

  • 之前出去面试的时候,会被问到一个很经典的问题:“你在code-review的时候,重点会关注哪些方面?”
  • 在一些传统软件行业人士口中,会听到一个岗位角色叫做“主程”(主程序员),虽然在互联网行业则很少提到这个词。

最近一年的时间,我在实践过程中一直在反思和总结这个问题,到目前我觉得我已经有了一套比较完整的答案了。

我觉得其实“主程”是一个合理且必要的存在,任何一个软件开发项目都需要这样一位可以掌控全局,对项目的健壮性、可迭代性、可维护性负责,对新技术的调研和选用也心中有数的核心开发人员。只不过,可能之前互联网行业发展太快,一方面业务变化快、另一方面人员流动频繁,在这样的情况下是很难在一个项目中积累下这样一个角色的。

“主程”这个角色,对应到互联网的岗位来说我觉得最接近的是“架构师”,或者是具体业务的技术负责人。然而架构师这个词可能在很多场景下会被人误解为、或者自己不知不觉间做成了“运维师”、“基础设施研发”等更偏向于执行的角色。可我心目中的“架构师”,应该不仅局限在“运维架构”这个领域,而是在任何一个具体的软件项目中,负责搭建和维护代码主体结构的那个角色。有了这样一个人负责把关,他可以带若干个能力相对平凡的人一起干活,最终也能产出令人满意的成果,达成比较高的投产比。

这个角色负责搭建一个清晰健壮的项目结构,然后以代码约束(或者约定规范)的形式,将其他研发人员的代码进行对齐。因此他在review的时候需要做的事情也就很明显了:要求各个模块『高内聚、低耦合』。

其实对于『高内聚、低耦合』这个术语,之前我都是嗤之以鼻的态度,因为在网上搜出来的相关文章往往都会对它做过度解读,提出许多条条框框,让我觉得并不可行。但是等我自己想要总结出一套的时候,我承认我懒癌犯了,我宁愿拿出现成的概念来近似地概括我的理念(我们终将变成我们曾经讨厌的那种人

但是这个词语说起来简单做起来难,往往需要结合实际情况来进行定制。因此这个把关的角色也是需要亲自下一线干活的,毕竟“让能听见炮火的人呼叫炮火”才是明智的选择。

另一方面的要求,就是保证代码风格的统一。

而在『风格』这个概念之中,“命名”一定是最重要的规范之一。在我的经验中,“命名”这件事其实一直是被很多团队所低估的。可要知道,“语言”是人类的一项非常强大的能力,我们应该善用它。

举一个正面例子,我突然说“面向对象编程”这个词语,屏幕前的你是否会在脑海里浮现一大堆术语、工作方法、甚至最佳实践等抽象概念?因此只需要这样一个词语,来自不同团队、不同背景的程序员便能以同一种风格进行编码,这其实是一种非常平凡却又非常伟大的事情。

举一个反面例子,我们团队内之前经常会混淆ModalDialogDrawerTooltipPopover甚至Menu等前端组件,产品运营同学经常用“xx弹层”、“xx弹窗”来指代任何“弹”出来的东西,经常会搞得研发同学搞不清楚对方在讲什么东西,导致需要额外的时间来确认。

名字起得好,并且在团队内部统一口径,是可以极大的降低沟通成本的。

除了code-review之外,项目技术负责人当然也还有许多其他的工作,但是我在这里不展开讲了。

闲谈:技术与管理

(注:这里说的管理,并不是指人事、行政那种通用管理,而是一线业务团队管理。)

最近发现一个很有干货的职场区UP主:『产品老曾』。最近看到他的《当管理比自己干活还累?升职加薪后他真香了!》,正好我们团队目前正在扩张阶段,我有切身体会,所以很想聊聊我的观点。

首先明确一个事实,如果自己确实有过硬的技术实力,能在项目核心岗位专心写代码,这件事情确实是非常爽的。这里面最爽的点在于心理层面,没有任何负担、可以抛弃所有的世俗杂念、在自己擅长的领域(舒适区)就能开开心心地把工资赚到手、同时还不断积累硬实力以后万一遭遇变故也完全不愁。这样的工作状态,在我的价值观中,是比任何世间传说的铁饭碗都要“香”得多得多的,也正是我转行来做程序员的“初心”,是我最想要达到的状态。

但是这种状态是很难长期维持的。比如(潜意识中)对金钱和权力的渴望、来自公司和社会的压力、甚至视频中所提到的“同期生之间的攀比心态”,很多因素都可能导致这种安逸的平衡被打破,让人身不由己地走上管理的道路。

至于我自己,我还有一个看起来有点扯淡的解读:“被使命感驱动”。我是一个比较有追求的人,往小了说,日常工作生活中做的事情,都会在一定范围内尽可能做到最好;往大了说,如果机会合适的话,我还是希望能在有生之年干一番事业的。

虽然我已经是一个能力非常全面的技能者了,但是我依然承认:面对更大更复杂的事业的时候,一个人的力量真的远远不够。只有把自己的力量、成功的经验复制或者影响到其他人,由团队一起努力才能够面对更大的挑战。

虽然与他人协作时的总工作效率会明显更低,但这也是一种必要的妥协。有句话叫做:“您无需跑得比熊更快,您只需比旁边的人跑得更快即可”,意思是我也并不需要把事情做到完美无缺,只需要在每个环节都比其他人(其他团队)更好一点点,就足够积累出不可忽视的优势。既然每个团队在长大的过程中都要付出这种妥协,只要我的代价能比别人更少一些,我依然能保持自己的优势。而且我也相信自己能够做好,如果换个人来会有极大概率还不如我做的好,那我就只能自己上了。

我稍微有些庆幸的是,我开始管理工作的时机还算合适。一方面,经过数年的内卷埋头苦干,我积累了扎实的技术实力,足以撑起相当高度的团队技术天花板;另一方面,早年在国企磨练、然后在社会上遭遇的种种变故,也造就了我虽然并不高但是在程序员群体中已经够用的人情世故。没有因过早而显得能力不足,没有因过晚而精力不足,再有缘遇到一个合适的项目机会,似乎一切都是刚刚好,让我没有拒绝的理由(这也算是一种身不由己吧)。

就算如果最后现实证明我不适合管理者的工作,那到时候再回头安心做个大头兵也完全不迟。当然前提是我并没有完全放弃自身的技能水平。(不会真有人会觉得强大的技能者真的会失业吧?)

闲谈:作为面试官

经历了教育行业崩盘、三年疫情、互联网红利尽头等因素叠加影响,按理说,2023年年初这个时间点,市场上应该有很多求职者才对。

但从我们最近的招聘情况来看,似乎并不是这样的。好吧,也并不是完全不对,毕竟人数确实是很多,但是质量还是一言难尽,总结下来就是一句『目前市场上低端人才已经饱和,但是中高端人才仍然紧缺』。

当然,上述结论是只从我们一个公司的样本做出的,是有很大局限性的。但就我所遇到的候选人的具体情况来说,还是真的挺让我难受的。

我们初级前端工程师的岗位的笔试题,是要求写一个非常非常基础的工具函数,类似的代码每天我可能都要写几十遍(因为过于简单都不需要特地抽出来作为公共函数),甚至可以说完全不需要思考、仅凭肌肉记忆便可以获得80分以上的分数进入下一个环节。可就这样的题目,依然能筛掉一半以上的候选人;而通过的候选人往往也是低分飘过,我经常要绞尽脑汁去想象如何思考才能写出这样的代码。这曾一度让我怀疑:难道不正常的人是我?直到有一天我终于看见一份完全令我满意的答案,而这份答卷却来自一个“问题少年”。我不能理解,但是大为震撼。

我这样说可能会伤害到一些人,特别是那些未能通过我们招聘环节的同学们。我也清楚的知道,对于那些我去应聘却未能通过笔试的公司来说,我可能也属于“你怎么连这么简单的题都做不出来啊”的那种低端人才。 但我还是想要扯掉这块遮羞布,把血淋淋的现实拿出来讨论。我们一起,有则改之,无则加勉。

我们也会考八股,但是如果跟其他公司的题目相比较的话(参考我去年的面经《八股复习记录&面经》),我们的题目算是比较接地气的,考的基本上都是一些主流的知识体系,而且经常会结合代码或者场景来分析,比较少有那些边边角角的、没有太多实战意义的纯八股。

我这里说的“八股”,指的是中性色彩的、广义的“原理性的知识点”,而不是贬义的、狭义的“无意义的细枝末节”。

这时候会遇到两类比较极端的候选人:一种是背八股背得很好,但是一写代码就蒙蔽的;一种是他应该知道用,但是那些概念和理论他就是说不出来,大概率是没认真准备八股的。前者不论,后者还是有些可惜的。

我觉得“八股”这种东西,与其说在考察能力,不如说是在考察态度、学习方式。八股是对日常工作的总结和补充,同时也是一项“专项技能”,是需要专门准备的。在平常业务中表现很牛逼的大佬,如果他没认真准备过就出去找工作,他恐怕也是大概率通不过八股这一关的,而且还极有可能落为别人的笑料。

但是这个东西又不可或缺,因为我们需要一些约定俗成的东西作为话题、引出更多的内容,以便于考察候选人的真实水平。同时还有一个可能更重要的原因:公平。每个人都有不同的经验背景和擅长领域,但是在组织中为了保证一定程度的公平,我们必须需要一套能够达成共识的评价标准,而这套标准在经过提炼精简之后自然就行成了所谓的“八股”。一套健康的人才评价体系是不可能缺少这个环节的,我们能做的,只是让它尽可能地少与实际工作脱节罢了。

八股对(认真负责的)面试官来说同样是一种挑战,因为候选人可能只需要答到六七十分就能通过了,但是面试官却要准备到100%、甚至120%才能从容应对候选人各种奇奇怪怪的回答。而那些边边角角的场景是工作中几乎不会遇到的,或者说遇到了随便查一下就能立即掌握的东西,本来让它存放在互联网中、随用随取即可,可面试官们却不得不付出额外的精力把这些对日常工作用处不大的东西装进自己的脑袋里。

算法也是同理。

除了额外的知识储备之外,面试官还需要准备一套合适的面试流程(以及题库),要适当打磨自己的话术和沟通技巧,还要经常与其他面试官沟通、对齐评价标准,等等。能当好面试官并不一定意味着技术实力更强,就像我们不可能简单地对比一个前端工程师和后端工程师的技能水平一样。这是一项“专业工作”,需要投入精力去维护的。