注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

程序员还是得明白,除了技术,你必须学会与人沟通

前言 Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。 工作越久,越觉得沟通能力重要,所以今天想和大家聊聊一个被挺多程序员忽视的能力,沟通能力。 因为忽略沟通能力,自己也吃过不少亏: 遇到问题不知道该请教谁,怕被别人觉着自己菜,怕麻...
继续阅读 »

前言


Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。


工作越久,越觉得沟通能力重要,所以今天想和大家聊聊一个被挺多程序员忽视的能力,沟通能力


因为忽略沟通能力,自己也吃过不少亏:



  1. 遇到问题不知道该请教谁,怕被别人觉着自己菜,怕麻烦别人,最后只能自己死磕。

  2. 跨部门协调时,自己的催促总是被人忽视,永远得到的是“我还有事情在忙,你先等等”。

  3. 日常只会低头写代码,甚至不知道代码为了谁而写,公司的方向、目标一概不知。

  4. 职业生涯感到困惑,想升职加薪,却又不知道如何向领导开口,到离职可能都没和领导私下说过10句话。


既然我们聊沟通,你是不是以为我要聊沟通的技巧、沟通的方式这些,当然一些方法很重要,但方式方法只能算是“术”的层面。


那些在职场中沟通顺畅,让别人愿意配合,被人信任的那些同事,仅仅靠的是方式方法吗?想要沟通更加轻松、顺畅,我觉着最终要的,还是取决于你是谁,你的表现如何,你能提供什么价值


话不多说,我们开始吧。


横向沟通


先说横向沟通,就是和没有汇报关系的同事或者合作方的沟通,也是我们在工作中,需要沟通场景最多的地方。
因为忽略沟通能力,自己也吃过不少亏:



  1. 遇到问题不知道该请教谁,怕被别人觉着自己菜,怕麻烦别人,最后只能自己死磕。

  2. 跨部门协调时,自己的催促总是被人忽视,永远得到的是“我还有事情在忙,你先等等”。

  3. 日常只会低头写代码,甚至不知道代码为了谁而写,公司的方向、目标一概不知。

  4. 职业生涯感到困惑,想升职加薪,却又不知道如何向领导开口,到离职可能都没和领导私下说过10句话。


既然我们聊沟通,你是不是以为我要聊沟通的技巧、沟通的方式这些,当然一些方法很重要,但方式方法只能算是“术”的层面。


那些在职场中沟通顺畅,让别人愿意配合,被人信任的那些同事,仅仅靠的是方式方法吗?想要沟通更加轻松、顺畅,我觉着最终要的,还是取决于你是谁,你的表现如何,你能提供什么价值


话不多说,我们开始吧。因为忽略沟通能力,自己也吃过不少亏:



  1. 遇到问题不知道该请教谁,怕被别人觉着自己菜,怕麻烦别人,最后只能自己死磕。

  2. 跨部门协调时,自己的催促总是被人忽视,永远得到的是“我还有事情在忙,你先等等”。

  3. 日常只会低头写代码,甚至不知道代码为了谁而写,公司的方向、目标一概不知。

  4. 职业生涯感到困惑,想升职加薪,却又不知道如何向领导开口,到离职可能都没和领导私下说过10句话。


既然我们聊沟通,你是不是以为我要聊沟通的技巧、沟通的方式这些,当然一些方法很重要,但方式方法只能算是“术”的层面。


那些在职场中沟通顺畅,让别人愿意配合,被人信任的那些同事,仅仅靠的是方式方法吗?想要沟通更加轻松、顺畅,我觉着最终要的,还是取决于你是谁,你的表现如何,你能提供什么价值


话不多说,我们开始吧。
不知道横向沟通时,大家会不会遇见几种场景?



  1. 和团队内同事沟通,都把方案说的很明白了,但总感觉他不懂我的设计思路。

  2. 有问题想请教同事,但却频频被婉拒,甚至被刁难

  3. 与合作团队配合,你急的像热锅上的蚂蚁,但他们不急不慢,甚至不支持,导致项目延期。


我在刚毕业作为一个职场小透明的时候,遇到工作中不会的问题是,请教同事特别小心翼翼,生怕同事拒绝我,或者嫌弃我菜。


遇到比较和善的同事还好,愿意帮助你,但是最怕的就是最怕的就是被一些同事回复:“你能不能自己再看看代码。”


虽然绝大部分程序问题都能通过看代码解决,但是我提出了问题,那一定是我真的看不明白了,不一定是看不懂代码,可能是因为不了解一些业务的背景和历史原因而已。


我特么肯定是看完了没思路才问的,被这么一回复,下次鼓起勇气再问,不知道要到什么时候了。


所以,横向沟通,其实最关键的,就是对于合作关系的同事,如何获得他们的认可与帮助


如何获得他们的认可呢?最重要的在于你对他有多大影响力


比如你有着更好的职级,更老的资历,更广的职场人脉,在横向沟通时都会比较顺利。


但是当你是一个职场新人,或者刚换了一个岗位、一个公司,那么该如何利用自己的影响力,去横向沟通呢?


因此,我会从职业生涯不同的阶段,结合自己的实际经历,来介绍给大家。


职场初期


程序员在工作的前几年,需要提升自己的专业能力为主。


你缺乏业务经验,缺乏技术经验,在工作的沟通中,便很容易处于较低的位置。相信你也有这种感觉,你觉着一个同事更专业、更有能力,那么他和你沟通方案、寻求支持,你一定很容易被说服。


那么你要问,那职场初期,专业能力一定还不够好,那么没有更好的办法提升自己的影响力,和同事沟通了吗?


当然不是,可以通过借势,提升自己的权威度。比如技术方案讨论,如果你对方案存疑,却不能提出更好的方案,如果你拿出比如大厂的解决方案出来,通过对比优劣,你的说服力就能够大大提升。


当然,除了业务、技术经验,提升自己的逻辑能力,也是非常重要的。在寻求帮助、获得支持的时候,有准确的数据,充分的论据,都可以提升你的观点的说服力和可信度。


职场发展期


职场发展期,相信你的技术能力、业务能力会有很大的提升,或许你已经可以独当一面,你的工作范围也不限制与团队内部,可能需要与多个职能的同事一起配合完成工作。


从影响力角度分析,这时候你需要提升在同事眼中的“信任度”为主。


什么构成了你同事眼中的信任度呢?我想从几个方面来分析


你的人品如何,对待事情是否积极、公正。
职场中你更愿意相信谁呢?一定是那些积极主动,并且对所有同事一视同仁的人。


在你的职场发展期,积极主动,是一个人能否继续进步的关键。这个阶段除了快速学习技术,对于业务的学习很容易被忽略。因为遇到技术问题,这是你的工作,你不去解决,你的工作就无法完成。


但是业务知识,你不学,没有人去管你,你只需要看着产品的PRD写代码,也不会出什么错。


但如果你只关注自己的一亩三分地,工作中遇见问题不推进,什么都等着别人催,等着别人解决,那么你的影响力就无从谈起,和同事沟通也会遇到很大阻碍。


你过去的表现如何,你这个人是否靠谱?
代码讲究鲁棒性,人其实也一样。工作中面对不同的环境、条件,都需要能保证工作产出的稳定和可靠。



  1. 比如你的代码质量是否足够高,之前上线的功能,是否稳定。

  2. 对于别人提出的问题、线上的bug,你是否能快速响应不拖沓。最怕的就是别人问你一个问题,你说等一会告诉你,然后你转头就忘了这件事情。

  3. 答应别人的事情,是否能够按时完成,而不是到dead line,才告诉别人还有问题。


职场稳定期


在职场稳定期,你既有了足够的技术专业能力,也成为了一个正直、靠谱的人,这时候我认为“互惠原则”是支撑你沟通顺畅的一个小窍门。



互惠原则是社会心理学中的一个概念,指的是人与人之间天生有回馈他人恩惠的倾向。简单来说,就是“你对我好,我忍不住要报答你”。



我想通过几个例子和大家说明我是如何因为互惠原则


有一次,合作团队负责的一个需求改造很紧急,但是因为他们技术方案没有评估到我负责团队的改动,所以没有给我们的产品提需求,因此我这边没有技术排期。


如果临时提需求,那么根据排期就要排到下一个开发周期了,他们需求自然也要延期。


他们组长找到我,我和他一起评估了一下改造的复杂性,并不复杂,于是我答应他在他们上线前我也会改造完并发版。于是自己提了一个技术需求,完成了相关改造,他们的需求也顺利上线。


还有一次,产品经理考虑不周,新上线的功能校验比较严格,因为设计问题,运营同事频繁吐槽产品,影响了他们的使用效率。


正常走需求迭代,需要等到2周后了,于是产品找到我沟通,看是否有什么临时的解决方案。
为了解决运营问题,我看了下,需要临时处理一下数据,既能保证使用,也能节约不少运营的人力成本,于是我写了几个脚本,临时处理线上数据,解决了这个问题。


在这两个场景中,我发现了他们的诉求,并且发现他们对此真的很着急,于是我尽我所能,主动的为他们提供帮助。你可能会说,你这样临时多干了很多事,太亏了。


是的,短期来看,我确实牺牲了自己一部分时间去帮助他们,但是我也收获了对他们的影响力,比如承诺的一定完成,还能帮他们摆平一些问题,慢慢的我的影响力就越来越强。在以后我需要帮助的时候,他们自然也乐意去帮助我。年度的360绩效评估,我自然能够收获他们的认可与好评,长期来看,受益的就是你自己。


向上沟通


向上沟通就是和你的上级进行沟通,即使工作好多年了,我对于和领导沟通依然觉着很头大,相信这也是你非常头疼的问题。


看看一下几个场景,你有没有中招呢?



  1. 觉着领导太忙了,我目前的工作似乎也没这么重要需要汇报,找领导也不知道说什么,等领导不忙了再说。

  2. 我做好领导交给我的事情就行了,拿结果说话。

  3. 有困难需要和领导协调,但是不知道怎么说,领导会不会觉着我能力不行?

  4. 领导交给我的任务到底是什么意思?需要做到什么程度,到底是否着急。


我认为向上沟通,最重要的一个点,就是主动大胆,跨过心里的那道坎,因为绝大部分时候,我们就是内心有一个卡点,觉着无话可说,觉着没有必要。


向上沟通如果你要想影响上级,实话实说太难了,并不适用于我们每一个人。所以说预期说向上沟通,不如说我们如何才能够利用好我们的上级,帮助自己更好的发展。


展示自己


自己作出成绩的时候,觉着无人知晓,那么和上级适当的展示自己。


展示自己,你是不是觉着,这样有点显摆的意思?其实并不是,在沟通过程中,你或许了解到,这件事是否符合当下团队的发展方向,你感觉有了成绩,是否是自嗨,有没有地方需要被纠偏?


如果真正做得好,被领导认可,那么可以极大的增加你的自信心,输出你的影响力,避免“酒香也怕巷子深”。如果发现问题,那也从上级的角度发现了可以提升的地方,对你来讲也是百利而无一害。


当然,展示自己并不是直接去和领导说,我做了xxx东西,非常厉害,用了什么什么技术,而是有一种其他方式,比如协调大家,做一次技术分享,把自己的东西展示出来。


信息同步


上级安排的工作任务,无论遇到什么问题,都自己扛。


我在工作的前几年里,一直是一个低头干活的人,自己很有计划性,即使遇到问题,我也会靠着自己的力量去死磕,我一直以为,我是一个靠谱的人,领导给我安排的工作我都能自己完成,多给领导省心。


但后来有一次和领导沟通,领导说有时候一旦周期拉长,领导对我的信心就会减弱,最关键的就是我向上的反馈不够多,像是两个月的OKR,或者半年的规划时,领导很难知道具体的进度如何,最后是否能达成,因为上级需要识别风险,提前处理。


持续的做好信息同步,领导对你的信任度才能不断加深,你才能过承担更重要的工作。


困惑解答


工作久了,一定会有职业生涯的困惑,,未来怎么发展,干的不开心,甚至想离职,都可以试着和上级聊聊。


因为我们的工作内容比较单一,所以我们对于很多事情看待的角度也会单一,和领导聊聊,可以从更高的角度看一下自己当前的阶段与状态。


向下沟通


还记得开头说的那句话吗:“真正会沟通的人,不需要能说会道、口若悬河,而是懂提问、会倾听,能洞察需求、摸透人心。”



学会听,比学会讲更重要。




学会提问


我们日常生活中的提问,往往分为2种:开放式提问和限制式提问。


比如,你询问下级:最近团队比较忙,加班比较多,当然也做出了不错的成绩,不知道你怎么看待我们最近完成的这个项目呢?


这就是开放式提问,对于最近的忙碌,可能会有很多问题,或许是产品需求不合理,也可能是工程质量不高或者大家配合不够顺畅,这就是让对方做开放问答题。


如果换一个问法:最近加班比较多,你这块的工作,是否都按期完成了呢?


这种提问方式,对方只能回答是或者否。你能获得的信息就比较少。


在沟通的时候,我们要尽量多用开放式提问,要鼓励对方自由回答,多让他们讲。这样有助于你收集资料、挖掘需求,而且,还能鼓励对方对问题做出详细说明。


试着倾听


向下沟通,大家可能都会觉得重点应该在怎么说,但是向下沟通,更重要的是倾听。为什么这么说呢,因为在向下沟通的环境中,你的职级、经验通常是要比沟通对象多出一些的,如果在没有理解对方意思的情况下,很容易陷入单向输出的情况,你哇啦哇啦说了一堆方法论、公司目标与方向,但实际上沟通起不到太好的效果。


3F倾听法是一种有效的沟通技巧,它强调在倾听过程中要关注三个核心方面:Fact(事实)、Feel(感受)和Focus(意图)。这种方法可以帮助我们更全面地理解说话者的意图和需求,从而促进更有效的沟通。


倾听事实:这一步骤要求倾听者专注于对方所陈述的客观事实,避免加入自己的主观评判。倾听者需要区分事实与观点,确保理解的是对方所描述的已发生且可考证的事情。在这个过程中,倾听者应保持开放的心态,不急于作出解释或提供建议,而是先确保对事实有准确的理解。


倾听感受:在倾听事实的同时,倾听者需要注意观察对方的情绪状态,感知对方的感受。通过观察对方的肢体动作、语言、声调、表情变化等,可以更好地理解对方的情绪,与对方共情,尝试站在对方的角度去感受和理解其情绪。


倾听意图:这一步骤要求倾听者深入了解对方话语背后的真实意图和期望,而不仅仅是表面的意思。通过提问和澄清,确保准确理解对方的意图,避免误解和沟通障碍。在理解对方意图的基础上,可以更好地回应对方的需求,促进有效的沟通。


说在最后


好了,文章到这里就要结束啦,很感谢你能看到最后,经验有限,文章中如果有问题,希望你能够指正。


希望你看完之后,能够重视沟通这件事,在和代码“沟通“越来越熟练的同时,也要注重与人如何沟通。


不知道你在和同事的沟通过程中,有没有遇到什么困难或者好的经验呢?欢迎你在评论区和我分享,也希望你点赞、评论、收藏,让我知道对你有所收获,这对我来说很重要。也欢迎你加我的wx:Ldhrlhy10,一起交流~


本篇文章是第36篇原创文章,2024目标进度36/100,欢迎有趣的你关注我~


作者:东东拿铁
来源:juejin.cn/post/7373474414430797863
收起阅读 »

代码和人,有一个能跑就行。为啥程序员总写dirty code

在程序员行业有一句:“代码和人,有一个能跑就行”。这句话对吗?为什么会产生这个问题?哪些代码能跑就行?有一些代码,真的就是不能动,一动就崩。里面逻辑复杂, 代码冗余,具备了一些不良代码的特征,但是它就是能跑,就是能支撑业务。有一个通俗的称谓为“屎山代码”。&n...
继续阅读 »

在程序员行业有一句:“代码和人,有一个能跑就行”。这句话对吗?为什么会产生这个问题?

哪些代码能跑就行?

有一些代码,真的就是不能动,一动就崩。里面逻辑复杂, 代码冗余,具备了一些不良代码的特征,但是它就是能跑,就是能支撑业务。有一个通俗的称谓为“屎山代码”。 image.png

哪些是dirty code

  • 缺乏注释和文档:完全没有注释或者复杂逻辑无文档
  • 命名不规范:变量、函数和类的命名不符合约定或没有约定 例如a,b,c,变量用动词,方法用名词,驼峰下划线混用等
  • 代码重复:同样的代码逻辑在多个地方重复出现,增加了维护的难度。 代码不做抽象,公共方法复制拷贝,一个方法复制多份
  • 复杂的逻辑:代码逻辑过于复杂,缺乏清晰的结构和模块化设计。
  • 硬编码值:使用硬编码的数值或字符串,而不是使用常量或配置文件

dirty code是如何产生的?

时间压力

项目不给足够时间,倒排期工程,项目经理整天催催催,老板天天问进度。预计5天,报了6天,砍到3天,1天的时候问做到哪了,2天问怎么还没做完。 你让我抽象,你让我搞架构,但是不给我时间,写出来的代码优先要进测试,提了bug再改改呗,反正缝缝补补又3年。

过于自信

自认自己的代码足够牛b。

不需要注释就可以看懂,不就是几个变量名吗?别人理一理逻辑就可以了,我的代码自己可以解释自己。 不需要抽象,这里都是一整套逻辑的。什么?你也要用这套代码,自己复制出去,别动我代码。我们要签订《代码互不侵犯条约》。

经验不足

新手小白能完成任务就不错了,什么鲁棒,什么设计模式,完全不需要考虑。一个函数500行?抱歉那是这个功能的瓶颈,不是我的瓶颈。

企业文化,标准/规范缺乏

你还记得你上一次做code review是啥时候吗?在夜深人静的时候,有没有回想每天996为啥老板还没开上大奔?

老板要的是结果,不是过程,代码写的再好,最后业务不核心,不干掉你干掉谁?

防御型编程

这个不多说了,懂的都懂。

明明知道有问题,为什么不重构呢?

重点项目,核心代码,不敢动。

在一些关键项目中,核心代码往往被视为系统的“心脏”。由于这些代码对于系统的稳定性至关重要,任何改动都可能带来巨大的风险。一旦出现问题,不仅影响系统的正常运行,还会直接影响团队的绩效和公司业务。因此,程序员往往选择维持现状,尽量避免对这些代码进行大的改动。

边缘项目,长期不迭代代码,不敢动。

对于一些边缘项目或已经很久没有进行过迭代的代码,由于缺乏持续的维护和更新,这些代码的整体质量和可读性往往较低。如果要对其进行修改,可能需要对整个系统进行全流程回归测试,这不仅费时费力,还可能导致人力资源的浪费。因此,除非遇到重大问题,否则这些代码通常也不会轻易被动。 image.png

代码能跑就行的结果是啥?

经过你一系列深思熟虑,不断优化重构,代码终于写的跟诗一样的。但是你的工期比别人多了1/2,虽然bug少了,但是研发成本大增。

尽管你的代码十分优秀,但不出意外的,在绩效评定的时候,你只拿到了及格。相反,另外一个能跑就行的同事,在每次线上出现问题的时候,都能及时化解,拿到了优秀。

因为你的项目进度慢,一些新项目和重点项目优先分配给了其他人。

慢慢的你对这家公司失去了信心,转投其他公司,但新公司的领导看到你的代码,惊为天人,于是你顺利的走上人生巅峰(给个happy end吧)。 image.png

究竟应该怎么做?

中国人讲究“中庸”,大多数情况,在非开源项目或公司无具体要求时,要求我们要掌握一个开发成本/代码质量的度。

尤其在一些并不太优秀的团队中,我们优秀的代码质量无法为我们换得足够匹配的价值回报。相反,交付效率/交付质量/线上稳定性才是优先考虑的问题。

尤其在现在降本增笑的大环境下,保护自己才是最重要的。

但优质的代码,带来的是身心的愉悦,后续维护的简单,代码的灵活性更高。建议在核心代码,工具类等优先使用高质量代码,而在一些增删改查,非核心/重点项目内容上,还是难得糊涂一下吧。


作者:天元reborn
来源:juejin.cn/post/7368397264027402275
收起阅读 »

如何选择 Android 唯一标识符

前言 大家好,我是未央歌,一个默默无闻的移动开发搬砖者~ 本文针对 Android 各种标识符做了统一收集,方便大家比对,以供选择适合大家的唯一标识符。 标识符 IMEI 从 Android 6.0 开始获取 IMEI 需要权限,并且从 Android 10...
继续阅读 »

前言


大家好,我是未央歌,一个默默无闻的移动开发搬砖者~


本文针对 Android 各种标识符做了统一收集,方便大家比对,以供选择适合大家的唯一标识符。


标识符


IMEI



  • 从 Android 6.0 开始获取 IMEI 需要权限,并且从 Android 10+ 开始官方取消了获取 IMEI 的 API,无法获取到 IMEI 了


fun getIMEI(context: Context): String {
val telephonyManager = context
.getSystemService(TELEPHONY_SERVICE) as TelephonyManager
return telephonyManager.deviceId
}

Android ID(SSAID)



  • 无需任何权限

  • 卸载安装不会改变,除非刷机或重置系统

  • Android 8.0 之后签名不同的 APP 获取的 Android ID 是不一样的

  • 部分设备由于制造商错误实现,导致多台设备会返回相同的 Android ID

  • 可能为空


fun getAndroidID(context: Context): String {
return Settings.System.getString(context.contentResolver,Settings.Secure.ANDROID_ID)
}

MAC 地址



  • 需要申请权限,Android 12 之后 BluetoothAdapter.getDefaultAdapter().getAddress()需要动态申请 android.permission.BLUETOOTH_CONNECT 权限

  • MAC 地址具有全局唯一性,无法由用户重置,在恢复出厂设置后也不会变化

  • 搭载 Android 10+ 的设备会报告不是设备所有者应用的所有应用的随机化 MAC 地址

  • 在 Android 6.0 到 Android 9 中,本地设备 MAC 地址(如 WLAN 和蓝牙)无法通过第三方 API 使用 会返回 02:00:00:00:00:00,且需要 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 权限


Widevine ID



  • DRM 数字版权管理 ID ,访问此 ID 无需任何权限

  • 对于搭载 Android 8.0 的设备,Widevine 客户端 ID 将为每个应用软件包名称和网络源(对于网络浏览器)返回一个不同的值

  • 可能为空


fun getWidevineID(): String {
try {
val WIDEVINE_UUID = UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L)
val mediaDrm = MediaDrm(WIDEVINE_UUID)
val widevineId = mediaDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID);
val sb = StringBuilder();
for (byte in widevineId) {
sb.append(String.format("x", byte))
}
return sb.toString();
} catch (e: Exception) {
} catch (e: Error) {
}
return ""
}

AAID



  • 无需任何权限

  • Google 推出的广告 ID ,可由用户重置的标识符,适用于广告用例

  • 系统需要自带 Google Play Services 才支持,且用户可以在系统设置中重置



重置后,在未获得用户明确许可的情况下,新的广告标识符不得与先前的广告标识符或由先前的广告标识符所衍生的数据相关联。




还要注意,Google Play 开发者内容政策要求广告 ID“不得与个人身份信息或任何永久性设备标识符(例如:SSAID、MAC 地址、IMEI 等)相关联。”




在支持多个用户(包括访客用户在内)的 Android 设备上,您的应用可能会在同一设备上获得不同的广告 ID。这些不同的 ID 对应于登录该设备的不同用户。



OAID



  • 无需任何权限

  • 国内移动安全联盟出台的“拯救”国内移动广告的广告跟踪标识符

  • 基本上是国内知名厂商 Android 10+ 才支持,且用户可以在系统设置中重置


UUID



  • 生成之后本地持久化保存

  • 卸载后重新安装、清除应用缓存 会改变


如何选择


同个开发商需要追踪对比旗下应用各用户的行为



  • 可以采用 Android ID(SSAID),并且不同应用需使用同一签名

  • 如果获得的 Android ID(SSAID)为空,可以用 UUID 代替【 OAID / AAID 代替也可,但需要引入第三方库】

  • 在 Android 8.0+ 中, Android ID(SSAID)提供了一个在由同一开发者签名密钥签名的应用之间通用的标识符


希望限制应用内的免费内容(如文章)



  • 可以采用 UUID ,作用域是应用范围,用户要想规避内容限制就必须重新安装应用


用户群体主要是大陆



  • 可以采用 OAID ,低版本配合采用 Android ID(SSAID)/ UUID

  • 可以采用 Android ID(SSAID),空的时候配合采用 UUID 等


用户群体在海外



  • 可以采用 AAID

  • 可以采用 Android ID(SSAID),空的时候配合采用 UUID 等




作者:未央歌
来源:juejin.cn/post/7262558218169008188
收起阅读 »

连公司WiFi后,无法访问外网,怎么回事,如何解决?

问题描述 从甲方项目组返回公司后,我习惯性连上公司WiFi,准备百度一个bug,突然我发现无打开百度,F5刷新了好几次也没用,浏览器报了下面的错误信息 尝试ping了一下 http://www.badu.com,好家伙,直接丢包 然后运行 ipconfig/...
继续阅读 »

问题描述


从甲方项目组返回公司后,我习惯性连上公司WiFi,准备百度一个bug,突然我发现无打开百度,F5刷新了好几次也没用,浏览器报了下面的错误信息


523d43c309e5d5b786dff74cc54894bf.png


尝试ping了一下 http://www.badu.com,好家伙,直接丢包


然后运行 ipconfig/all 命令看了一下本机的DNSF服务器信息


b75ff8dab79e97b5698daa4e060e24af.png


我的本机DNS地址是192.168.0.1


通常,本机DNS地址若为192.168.0.1,说明所连WiFi的路由器可能被设定为执行DNS转发职责,或者是期望客户端直接使用路由器作为DNS解析的入口点。而192.168.0.1一般是路由器的默认IP地址,并非一个标准的公共DNS服务器地址。在这种情况下,访问不了外网,例如百度,新浪微博等,有可能是路由器的DNS转发功能没有正常工作,或者路由器自身没有被配置正确以访问外部的DNS服务器


最简单直接的解决方法是手动设置主机的DNS地址为公共的DNS服务器地址



  • Google DNS:8.8.8.8 & 8.8.4.4

  • Cloudflare DNS: 1.1.1.1

  • 中国电信:114.114.114.114

  • 中国联通:223.5.5.5


7785e7beed4c08710c07617232dad576.png




OK,可以正常访问百度了



45db3372360d9ecab863ffb1da9e1246.png


这让我产生了非常浓烈的好奇,从浏览器上输入URL到显示页面,中间究竟发生了什么?


image.png


问题探究



这是一道面试题



1716627344750.png


从浏览器中输入URL并按下回车键后,直到网页内容完全显示在屏幕上,这个过程中发生了一系列复杂的步骤,大致可以概括如下:



  1. URL解析:浏览器首先解析输入的URL,提取出协议、域名、路径以及查询字符串等信息。

  2. 检查缓存:在发起网络请求之前,浏览器会检查本地缓存(包括浏览器缓存、系统缓存乃至路由器缓存),看看是否已经存储了该请求的资源。如果有且未过期,则直接使用缓存内容,无需继续下面的步骤。

  3. DNS解析:如果缓存中没有所需资源,浏览器会通过DNS(域名系统)将网址的域名转换为IP地址,因为网络通信是基于IP地址的。这个过程中可能涉及递归查询和迭代查询,直至找到域名对应的IP地址。

  4. TCP连接建立:获得服务器IP后,浏览器使用TCP协议与服务器建立连接。这通常涉及TCP三次握手过程,确保数据传输的可靠性和连接的双方都准备好通信。

  5. 发起HTTP/HTTPS请求:建立连接后,浏览器构造HTTP或HTTPS请求报文,包含请求方法(如GET或POST)、请求头(携带浏览器信息、请求资源的位置等)以及可能的请求体,然后发送给服务器。

  6. 服务器处理请求:服务器接收到请求后,根据请求的内容处理并准备响应,这可能涉及数据库查询、服务器端脚本执行等操作。

  7. 响应浏览器:服务器将处理好的响应数据(包括状态码、响应头、响应体等)封装成HTTP响应报文,发送回浏览器。

  8. 浏览器接收响应:浏览器接收响应数据,如果响应中有新的资源(如CSS、JavaScript、图片等),浏览器会根据需要再次发起请求获取这些资源。

  9. 渲染页面:浏览器开始解析HTML文档,构建DOM(文档对象模型)树,同时解析CSS文件构建CSSOM(CSS对象模型)树,结合这两棵树形成渲染树(Render Tree)。接着进行布局(Layout)和绘制(Painting),即确定每个节点在屏幕上的位置和外观,最终将页面内容呈现给用户。

  10. 执行JavaScript:页面中的JavaScript代码会被解析和执行,它可能修改DOM和CSSOM,导致重新布局和绘制。此外,异步请求如Ajax也可以在这个阶段发起,动态更新页面内容。

  11. 页面交互:页面加载完毕后,用户可以与页面进行交互,触发事件处理程序,进一步的JavaScript执行可能会改变页面状态。

  12. 连接关闭:当所有数据传输完毕,TCP连接会通过四次挥手的过程优雅地关闭。


上述过程中涉及到了多个层次的技术和协议,从应用层的HTTP/HTTPS、运输层的TCP、网络层的IP到链路层的以太网协议等,共同协作完成了从简单的URL输入到复杂页面展示的任务。


cbacfb95186577a2e2d92fe72fa8d0c5.png


基于上述分析,问题发生在第③步(DNS解析)上,要想回答何为DNS解析,就必须弄明白何为DNS。


何为DNS?


DNS,英文全称为Domain Name System,即域名系统。当我们在浏览器输入一个 URL 地址时,浏览器要向这个 URL 的主机名对应的服务器发送请求,就得知道这个服务器对应的 IP地址,而对于浏览器来说,DNS 的作用就是将主机名转换成 IP 地址【正向解析】。以下定义概念摘自《计算机网络:自顶向下方法》:




  1. 一个由分层的 DNS 服务器( DNS server) 实现的分布式数据库

  2. 一个使得主机能够查询分布式数据库的应用层协议



分布式,层次数据库


如何理解分布式?


随着互联网的快速发展,主机日益增多且数量庞大,采用单一DNS服务器上集中响应的设计并不可取,这种设计容易造成单点故障维护困难通信容量受限等问题。


为了应对上述问题和扩展性, DNS 使用了大量的 DNS 服务器并分布在全世界范围内。因为没有一台 DNS 服务器可以存放Internet上所有主机的映射数据, 相反,该映射数据被分布存储在所有的 DNS 服务器上。


如何理解层次?


DNS服务器采用层次组织,大致说来,有3种类型的 DNS 服务器:根 DNS 服务器、 顶级域 (Top- Level Domain , TLD) DNS 服务器和权威 DNS 服务器。它们的层次结构方式如下所示:


1716628019691.png


图片来源:《计算机网络:自顶向下方法》



  • 根DNS服务器


    我们首先要明确根域名是什么,它没有特定的名称,仅由一个点(.)表示。在技术层面上,它是所有域名查询的起点,负责指引域名解析过程中的查询请求到相应的顶级DNS(TLD)服务器,如.com.net.org等。而在实际的网址中,根域名通常隐含而不显示,例如com.baidu.com.,后面的点一般不会显示。


    根DNS服务器是互联网基础设施的关键部分,全球共有13组根DNS服务器,它们存储了顶级DNS服务器的地址信息,从而帮助我们将域名转换为用于网络通信的IP地址。根DNS的管理由国际互联网名称与数字地址分配机构(ICANN)负责。


  • 顶级域服务器


    这些服务器负责顶级域名,如comorgnetedugov,以及所有国家的顶级域名如uk、r、ca和jp。TLD提供了它的下一级,也就是权威 DNS 服务器的 IP 地址。




  • 权威DNS服务器



    在因特网上具有公共可访问主机(如Wb服务器和邮件服务器)的每个组织机构必须提供公共可访问的DNS记录,这些记录将这些主机的名字映射为IP地址。



    以上内容摘自《计算机网络:自顶向下方法》,比较绕口,通俗来讲就是提供最终的主机—IP映射



本地DNS服务器


在上一节的DNS层次结构中,眼尖的小伙伴会发现,并未提及本地DNS服务器,那为什么呢?一个本地DNS服务器,从严格说来,它并不属于上述DNS服务器的层次结构,但它对DNS层次结构0是至关重要的


每个ISP(Internet Service Provider,即网络业务提供商)都有一台本地DNS服务器(也叫默认名字服务器)。当主机与某个ISP连接时,例如一个小区的ISP,一个学校的ISP等,该ISP会提供一台主机的IP地址,该主机具有一台或多台其本地DNS服务器的IP地址,通常主机的本地DNS服务器会临近主机,当主机发出DNS请求时,该请求被发往本地DNS服务器,它起着代理的作用,并将该请求转发到DNS服务器层次结构中。


迭代查询,递归查询


如下图所示,假设主机abc.net想要获取主机xyz.edu的IP地址,大致会进行如下步骤:


1716647441277.png



  1. 主机abc.net首先向它的本地DNS服务器发送一个查询报文,该报文会含有被转换的主机名xyz.edu。

  2. 本地DNS服务器会将该报文转发给根DNS服务器。

  3. 该根DNS服务器注意到其edu前缀并向本地DNS服务器返回负责edu的TLD(顶级域服务器)的IP地址列表。

  4. 该本地DNS服务器则再次向这些TLD 服务器中的其中一台发送查询报文。

  5. 该 TLD 服务器注意到 xyz. edu 前缀,并把权威DNS服务器的IP地址响应给该本地DNS服务器。

  6. 本地 DNS 服务器直接向权威DNS服务器中的其中一台重发查询报文。

  7. 该权威服务器会用xyz.edu的lP地址进行响应。

  8. 本地DNS服务器会将主机xyz.edu及其IP地址的映射数据响应给主机abc.net,主机abc.net拿到它的IP就能给主机xyz.edu发送请求。


在上图例子中,主机abc.net向本地DNS服务器发出的查询是递归查询因为该查询请求是以主机abc.net以自己的名义获得该映射。 而后继的3 个查询是迭代查询,因为所有的回答都是直接返回给本地DNS服务器。 即第①步是递归查询 ,第②,④,⑥步是迭代查询。


那所有的DNS查询都遵循迭代 + 递归的方式吗?


答案并非如此,虽然在理论上,任何DNS查询既可以是迭代的,也能是递归的。


如下图,所有的DNS查询是都是递归的,因为所有的查询请求是以主机abc.net以自己的名义获得该映射。


1716650770678.png


DNS缓存


实际上,为了改善时延性能并减少在Internet上到处传输的 DNS报文数量,DNS 广泛使用了缓存技术。 DNS 缓存的原理非常简单。 在一个请求链中,当某 DNS服务器接收一个 DNS 回答(例如,包含主机名到IP地址的映射)时,它能将该回答中的信息缓存在本地中。 下次查询时便可直接用缓存里的内容。


注意,缓存并不是永久的,每一条映射记录都有一个对应的生存时间,通常设置为两天时间,一旦过了生存时间,这条记录就会从缓存移出。


有了缓存,本地 DNS 服务器可以立即返回所要解析主机的IP地址,而不必查询任何其他DNS服务器。 而本地 DNS服务器也能够缓存TLD服务器的地址,因而经常绕过查询链中的根 DNS服务器。


参考资料


计算机网络:自顶向下方法(原书第8版) (豆瓣) (douban.com)


作者:Jormungand581
来源:juejin.cn/post/7372456890344243215
收起阅读 »

29岁,大厂女程序员,总包六折结束北漂,聊聊换城市。

先简单描述下下我的背景。 95,双非本科,多段大厂前端背景,未婚未育,北漂快七年。 三段Gap经历,最长4个月。 回二线三个月,目前对人生很乐观。 1. 离开京东,结束北漂 昨天前同事发我消息吐槽,东子又跟兄弟们发言,不拼搏的也不再认兄弟了。 考勤新规真是十分...
继续阅读 »

先简单描述下下我的背景。
95,双非本科,多段大厂前端背景,未婚未育,北漂快七年。


三段Gap经历,最长4个月。


回二线三个月,目前对人生很乐观。


1. 离开京东,结束北漂


昨天前同事发我消息吐槽,东子又跟兄弟们发言,不拼搏的也不再认兄弟了。


考勤新规真是十分劝退,北京西南角五环外的京东总部。
研发卡到九点上班,砍掉午休。


在东子,如果是9点上班,一定不存在6点下班,东子的各大Bu潜规则都是晚上九十点左右下班,
工时每天要打满10个小时往上, 边缘部门也不例外。


在北京如果是9点到工位,我肯定是做不到。
也幸亏在2024年年初,我从京东拿到了礼包+年终奖,顺便结束了北漂。


现在在西北省会城市继续搞老本行做前端,不用再忧患9点上班带来的痛苦。


离开北京的原因和大多数人一样,


从工作发展上看,21年后,感觉“混”不再是个简单的事。
无论是晋升或是涨薪都难度比之前要大,并且这往前迈出的一步,也意味着要做更多内卷和向上管理的工作,性价比很低。


从生活体验这一面来讲,六七年的北漂体验已经让我对北京这座大城市带来的“通勤疲惫”以及“人际关系冷漠”感到麻木。挣了工资就攒着,没啥钱用来消费。 同时,我对“女前端”的职业生涯年限乐观的预期也就在三十左右,在北京的焦虑感也比其他城市会更重些,大家都是孤舟,去年因为焦虑也让我身体出了一些异常,还是快逃吧。


还有一点,我也跟大部分广大民群众一样,有个“娇夫孩子热炕头”的朴实生活愿望,“媳妇”还是回家找吧。


2. 换到二线城市之后的工作体验


回到家的这三个月,吃了很多好吃的(西北人就是西北胃!!),见了很多大学同学以及在省会的亲戚朋友。


换了个赛道,不在互联网行业,每天没那么多会议要开。
6点多就能下班,我开始下班看到夕阳,很幸福。


IMG_2629.JPG


回想在上家公司的工作内容,工作时间不是开会就是扯皮,还要保证产出和质量。
同时还有一无是处的“小组长” 要对你考核,组织架构不断的调整,职场环境糟糕,自己内耗严重。
赚那么点工资,医美钱都不够。
真的划不来。


通过互联网,也认识了很多从 北上广 回西安的前端同行,拉了群交流。
有需要的小伙伴可以后台didi我。


3. 年前找工作的感受


年前为了换工作,面试了一个多月,大概30多家公司。


包含大中小厂,面试通过大概在50%+ ,也拿了些offer。
跟周围很多同行都交流过我的面试情况,大部分都觉得我是,实力+运气。


我是业务型前端,之前从B到C,pc移动,跨端都有经验。
个人感受是面试整体的内容和之前内的差不多,前端还是 八股+项目 为主。


btw,相关面经我和其他的大厂前端朋友也沉淀了一份前端知识库,持续更新中,有需要dd.


不过已经是5年+经验 ,明显的能感觉到在问项目的时候,更加的细致和深入。细节挖得很深。
除了技术外,软实力方面也有所要求。面试中经常会被问到,如何去做一些项目管理及团队赋能的相关内容等等。


面到后期 ,跟HR谈薪才是最疲惫的环节。
京东整体研发的工资也不高,30%的涨幅都不好谈,这个跟我的面试表现也有关系。
我确实不是什么技术大佬。


回顾了下年前的面试记录。


投递了很多家公司,内推+BOSS直聘+脉脉 的简历通过率是最高的。


快手各个部门都显示不匹配,字节仍旧一轮游。
有约在晚上九点面试的,持续面到十一点多才结束。
我以为这么辛苦了,至少能给个二面,结果也是不匹配截止到一面。


北京真的把我面麻了,不qiu面了,到后期焦虑感也上来了。
直接留了两三周gap, 杭州长沙玩了一通。还是很开心的~


IMG_4655.JPG


后面西安的公司发了offer,降薪就降薪吧。


北京我实在是不想玩了,这几年跟老鼠一样,体验实在是太差,再升级苦难,我也是扛不住。


整体上看,2024年春节前,于我个人来看没有什么特别好的hc.
跟同行交流,大家也都是类似的情况。
而且一个比一个叫的惨,
我认识几个我认为技术不错的前端大佬,有从2022就开始gap的。
按照现在HR的标准,Gap真的拉黑率太高了,要打工进厂的话,还是尽量不要Gap吧,技术大神除外。


4. 其他


回西安工作后,好处就是,通勤十分钟,也不再吃外卖。
朋友家人都在身边,
总包虽然打折,也够养活自己了。
“外包之都”的名字也不是白叫的。我也能感觉到,跳槽应该是没啥地方能跳的了,基本整个西安市场上,好的前端HC是阶段性间歇出现的,机会不多。


工作上还是会有些焦虑,也逐渐意识到沉淀技术能力的重要性。
顺着最近对工作生活思考,
调整了下工作和学习上的方向,目前看来收益不小。
感兴趣的话,欢迎点赞收藏,我准备后面再写一期。


总之,目前来看体验还是很不错的,我本来物欲也不高,
希望同行们都心态向好,
努力生活。


作者:程序员班班
来源:juejin.cn/post/7372577541112987660
收起阅读 »

关注用户隐私安全 OPPO助力开发者保护个人信息安全

为助力广大APP开发者做好个人信息保护工作,更好地维护用户合法权益,在工业和信息化部信息通信管理局的指导下,5月23日,OPPO在深圳组织开展「个保合规我参与」公益培训宣讲会——OPPO站,这也是本次系列活动的首站。近百名移动应用开发者参加了此次活动,现场多位...
继续阅读 »

为助力广大APP开发者做好个人信息保护工作,更好地维护用户合法权益,在工业和信息化部信息通信管理局的指导下,5月23日,OPPO在深圳组织开展「个保合规我参与」公益培训宣讲会——OPPO站,这也是本次系列活动的首站。近百名移动应用开发者参加了此次活动,现场多位行业专家结合工作实践进行内容分享交流,探讨保护用户个人信息安全,共同推动APP行业健康发展。

1.JPG

深圳市通信管理局副局长陈逸菁在开场致辞中表示,广大企业和应用开发者要重视并做好APP个人信息和用户权益保护工作,保障互联网行业规范健康发展,维护网络安全和公众利益,共同推进移动互联网应用产业高质量发展。

2.jpeg

随后,OPPO全球数据与网络安全总经理韩方登场致辞,他指出日新月异的信息科技为生活带来了极大便利,同时也给个人隐私和信息安全带来了诸多挑战。维护个人隐私权益,确保公众在数字生活中的安全感和舒适度,已然成为当下的一项重大课题。为此,OPPO建立了完善的隐私合规体系,从技术、制度、培训等多方面入手,全面落实隐私保护。

3.jpeg

深圳市前海互联网安全保障中心互联网管理部主任梁建翔基于APP监管合规体系和APP备案管理要求两大方面,结合当前APP个人信息保护问题及相关法律法规要求,对APP备案与合规管理政策进行了介绍。他指出,移动互联网应用程序主办者和分发平台要切实履行合规义务,落实个人信息保护和“先备案后服务”要求,充分保护用户权益,维护网络安全和公共利益,促进互联网行业规范健康发展。

4.JPG

中国信息通信研究院泰尔终端实验室信息安全部副主任王艳红对《工业和信息化部关于进一步提升移动互联网应用服务能力的通知》进行了解读。从通知出台的背景和目的、重点工作考虑、APP开发运营者、分发平台等产业链上下游主体如何落实主体责任做了详细说明。

5.jpeg

中国信息通信研究院政策与经济研究所法律研究部专家端晨希从智能时代个人信息保护法律合规问题入手,介绍了当前APP个人信息保护的问题形势、相关法律法规及政策要求,就内容推荐服务可能存在的合规难点进行了解读。

6.jpeg

中国信息通信研究院技术与标准研究所产业互联网研究部专家李鑫对SDK个人信息和权益保护技术要求及SDK管理服务平台做了介绍,并现场展示了SDK管理服务平台相关功能服务,倡议行业企业共同推动SDK生态合规,以更好地保护用户合法权益,共同促进行业健康发展。

7.jpeg

中国信息通信研究院云计算与大数据研究所审计与治理部专家甘泉围绕《个人信息保护合规审计实施方法和实务》,分享了合规审计的背景、方法等和相关事件案例,为评估个人信息保护制度的可操作性以及相关制度的有效实施提供了指引。

8.jpeg

中国信息通信研究院泰尔终端实验室信息安全部专家邓佑军从个人权利响应和终端厂商义务两大维度展开,分享了移动智能终端用户权益保护规范要点相关内容,并结合实践中发现的违规收集个人信息、过度索取权限、欺骗误导强迫用户等典型问题,介绍了专项整治行动的整改流程,助力开发运营者高效完成合规整改。

9.jpeg

中国信息通信研究院泰尔终端实验室信息安全部专家武林娜结合生成式人工智能的交互方式、产品形态、产业生态现状,指出AIGC时代移动应用对个人信息安全带来的全方位影响及生成式人工智能模型本身带来的挑战,并提出AI大模型赋能个人信息保护的路径探索。

10.jpeg

加强用户信息安全及隐私保护,一直是OPPO关注的重点之一。现场OPPO高级安全研发工程师王学成分享了OPPO保护消费者个人信息安全的实践经验,并介绍了目前OPPO已建立起安全隐私全流程全场景防护体系, 通过OPPO智能护盾、7×24小时人工、三方引擎三重扫描,全天候对应用进行严格监测,及时拦截违规应用,并已通过《白皮书》形式对外展示应用安全治理成果。

11.jpeg

据了解,OPPO智能护盾通过安全大脑实现了贯穿应用程序从上架、下载、安装、启动、运行、卸载阶段全生命周期的安全隐私治理。在应用程序上架前,OPPO通过开发者信誉管理系统, 以敏感权限检测、隐私政策合规检测、隐私自动化检测方式审查开发者资质,确保应用来源的可靠性。通过对全量应用开发者进行实名和信誉排名,确保上线应用的开发者身份真实可信;同时,严控存疑开发者、强制清退封禁恶意开发者,从源头上确保OPPO软件商店在架应用来源的安全性与真实性。

除了应用上架环节外,针对应用下载、安装环节的潜在风险OPPO也进行了保护。当用户从浏览器等非官方渠道获取应用包、进行安装流程时,系统会进入安装扫描环节,由OPPO智能护盾安全大脑提供检测能力,对恶意应用进行警示和拦截。据统计,OPPO手机终端每年拦截恶意应用安装10亿多次, 恶意应用下载13亿次,拦截72亿次风险APP行为,高效保护用户隐私和安全。

12.jpeg

此外,手机应用违规收集个新信息、超范围收集个新信息、强制过度索取权限、欺骗误导强迫行为等困扰用户的问题,OPPO也给出了应对方案。OPPO智能护盾会在应用首次启动时结合应用类型和功能场景,为用户提供合理的授权建议,减少隐私数据泄漏风险;同时,在应用使用过程中,还将通过应用行为记录展示当前过度授权的应用与风险,引导用户一键优化,进一步保障安全性。

一直以来OPPO高度重视用户隐私和数据安全, 坚持"以用户为中心"的理念,积极践行“隐私守门人”使命,为每一个OPPO用户打造更便捷、更安心的用机环境。OPPO通过建立贯穿应用全流程的安全防护体系,提高APP开发者的个人信息保护水平,保障用户的信息安全,共同推动APP行业的健康发展,与用户一起展望更安全可靠的数智化生活。

收起阅读 »

Git提交错了,于是我把锅甩给了新来的baby

又是一遭悲惨的遭遇,git提交了一连串代码之后,发现提交错了。其实是把给老婆发的消息打到了comment里,然后还提交上去了。怎么办,这被看到岂不是要社死了。 一连串的研究之后,找到了几个解决方案。接下来我们一起搞搞这种错误提交的弥补方案。其中最离谱的是第三...
继续阅读 »

又是一遭悲惨的遭遇,git提交了一连串代码之后,发现提交错了。其实是把给老婆发的消息打到了comment里,然后还提交上去了。怎么办,这被看到岂不是要社死了。


image.png


一连串的研究之后,找到了几个解决方案。接下来我们一起搞搞这种错误提交的弥补方案。其中最离谱的是第三个方案。哈哈。


赛前准备


这里模拟一下这个操作,毕竟不能直接看我们的代码记录。我们新建一个项目,新建一个文件,起名001。


image.png


然后依次改为 002 003 004 005,每次都提交一次,在005的时候,执行异常提交。


最终我们得到一个005的文件


image.png


gitee上看是这样的


image.png


对于我们来说,现在是想删除这个异常提交,不仅删除代码,还想删除记录


也就是说,期待的是,文件变为004,而且这个提交记录删除掉。


方案1 交互式 rebase


首先我们尝试一下 git rebase -i HEAD~3,这样会取出最后的三条提交记录供我们编辑。


image.png


我们可以看到顶上有三条记录,这时候,我们删除这个异常的提交5


image.png


保存之后,会返回


git rebase -i HEAD~3
Successfully rebased and updated refs/heads/master.

这时候查看记录


image.png


异常提交已经没有了。


但是若是我们直接git push 会报错


image.png


告诉我们,我们当前的分支的版本是落后于远程分支的,不能提交。


这时候就需要git push --force这个命令,强制推送!!!


需要注意的是,强制推送会覆盖远程仓库中的历史记录,因此请确保你知道这个命令是个啥,并且有必要的话,需要通知团队其他成员协调好操作。


image.png


可以看到,git push --force 是可以成功的,而且再看gitee的记录


image.png


异常提交5已经不见了。并且本地的文件已经变为了004


image.png


其实在git rebase -i HEAD~3这个命令打开的交互框里是可以更改提交的顺序的,但是不能针对同一个文件的同一行,会冲突。


方案2 git reset


git reset 其实之前写文章讲过Git reset到底该如何使用,一文读懂系列 这次我们就直接为达目的,直接使用。
我们在上边的基础上,再提交一个异常提交5,使其恢复最初的情况。


image.png


然后gitee的情况:


image.png


这时候我们执行


git reset --hard HEAD~1

这个命令将删除最近的一个提交,包括提交所做的更改。请注意,这种方法可能会导致丢失未提交的更改,也就是说,本地写的没提交的代码就没了。所以请谨慎使用。


image.png


执行之后,我们可以看到异常提交5不见了


image.png


提交的时候也需要git push --force这个命令,强制推送!!!为啥每次都使用三个!!!呢,我只想告诉你,这个命令很恐怖,一定要慎之又慎。


这时候查看gitee记录


image.png


异常提交5没有了。


使用 git revert


还有小伙伴会说,为啥不用git revert呢,这不是git专门用来回滚代码的吗?


我们恢复异常提交005,再试试


image.png


我们执行 git revert f3d8db 并且 push


image.png


可以看到,文件是从005变为004了。但是从提交记录来看,不仅没有删除记录,还多了一条。其实,除非提交的注释特别社死,不然一般用的就是git revert,因为它不仅可以保存记录,还能确保版本是往前走的。


image.png


方案3 git filter-branch(谨慎使用)


查资料的时候,还看到一个这个命令,可以来一波骚的了。那既然提错了,把这锅甩给新人不就行了,哇咔咔咔咔咔。


git filter-branch --commit-filter '
if git log --format="%B" -n 1 $GIT_COMMIT | grep -q "异常提交"; then
GIT_AUTHOR_NAME="new baby";
GIT_COMMITTER_NAME="new baby";
git commit-tree "$@";
else
git commit-tree "$@";
fi'
-- --all

然后就是这样的


image.png


image.png


可以看到名字变了。当然邮箱也是可以改的。哇咔咔,这异常不就与我没关系了么。。。但是,极其不建议这么瞎折腾哈。


这个命令会根据条件重写整个历史。操作之前备份一下吧,别折腾坏了。而且一定先和其他的小伙伴商量一下,尤其是新人哈。


在此,就研究完毕了。正常来说使用第一种或者第二种方案都是可以的。不怕挨打的话,第三种方案也行。


git rebase 和 git reset 的区别



  • git rebase 命令用于将一个分支的提交移动到另一个分支上,或者重新应用一系列的提交。它的主要作用是改变提交的基础,即重新设置提交的起点。

  • git reset 命令用于修改当前分支的 HEAD 引用,或者用于撤销之前的提交操作。


也就是说git rebase 用于重新整理提交历史,而 git reset 用于调整当前分支的位置或撤销更改。关于这两个详细的使用,git reset已经写过了,有关git rebase的我会新开一篇文章,有关将一个分支的提交移动到另一个分支上这个操作虽不常用,但总有需要用到的时候。


作者:奔跑的毛球
来源:juejin.cn/post/7365414174217355314
收起阅读 »

在滴滴开发H5一年了,我遇到了这些问题

web
IOS圆角不生效 ios中使用border-radius配合overflow:hidden出现了失效的情况: 出现此问题的原因是因为ios手机会在transform的时候导致border-radius失效 解决方法:在使用动画效果带transform的元...
继续阅读 »

IOS圆角不生效


ios中使用border-radius配合overflow:hidden出现了失效的情况:


image.png



出现此问题的原因是因为ios手机会在transform的时候导致border-radius失效



解决方法:在使用动画效果带transform的元素的上一级div元素的css加上下面语句:


-webkit-transform:rotate(0deg);

IOS文本省略溢出问题


在部分ios手机上会出现以下情况:


image.png


原因


在目标元素上设置font-size = line-height,并加上以下单行省略代码:


.text-overflow {
display: -webkit-box;
overflow : hidden;
text-overflow: ellipsis;
word-break: break-all;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}

或者:


.text-overflow {
overflow : hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

由于不同系统包含的字体的行高不一致,即使设置了height = line-height 一样会有以上问题


解决方案


经过测试,在height = line-height = font-szie的情况下,加上padding-top: 1px可以解决这个问题,即在需要使用单行省略的地方加上:


.demo {
height: 28px;
line-height: 28px;
font-size: 28px;
padding-top: 1px;
}

如:<div class="text-overflow demo">我是需要进行单行省略的文案</div>


安卓手机按钮点击后有橙色边框


image.png


解决方案:


button:focus {
outline: none;
}

优惠券打孔效果


需求中经常需要实现一类效果:优惠券打孔,如下图所示:


image.png


通常情况下会找设计采用图片的的形式,但这个方案最大的缺陷是无法适配背景的变化。
因此,我们可以采用如下方案,左右两侧各打一个孔,且穿透背景:


image.png


具体细节可以参考这篇文章:纯 CSS 实现优惠券透明圆形镂空打孔效果


Clipboard兼容性问题


navigator.clipboard兼容性不是很好,低版本浏览器不支持


image.png


解决方案:


const copyText = (text: string) => {
return new Promise(resolve => {
if (navigator.clipboard?.writeText) {
return resolve(navigator.clipboard.writeText(text))
}
// 创建输入框
const textarea = document.createElement('textarea')
document.body.appendChild(textarea)
// 隐藏此输入框
textarea.style.position = 'absolute'
textarea.style.clip = 'rect(0 0 0 0)'
// 赋值
textarea.value = text
// 选中
textarea.select()
// 复制
document.execCommand('copy', true)
textarea.remove()
return resolve(true)
})
}

Unocss打包后样式不生效


这个问题是由webpack缓存导致的,在vue.config.js中添加以下代码:


config.module.rule('vue').uses.delete('cache-loader')

具体原因见:UnoCSS webpack插件原理


低端机型options请求不过问题


在我们的业务需求中,覆盖的人群很广,涉及到的机型也很多。于是我们发现在部分低端机型下(oppo R11、R9等),有很多请求只有options请求,没有真正的业务请求。导致用户拿不到数据,报network error错误,我们的埋点数据也记录到了这一异常。


在我们的这个项目中,我们的后台有两个,一个提供物料,一个提供别的数据。但是奇怪的是,物料后台是可以正常获取数据,但业务后台就不行!


经过仔细对比二者发送的options请求,发现了问题所在:


image.png


发现二者主要存在以下差异:



  1. Access-Control-Allow-Headers: *

  2. Access-Control-Allow-origin: *


于是我便开始排查两个响应头的兼容性,发现在这些低端机型上,Access-Control-Allow-Headers: *确实会有问题,这些旧手机无法识别这个通配符,或者直接进行了忽略,导致options请求没过,自然就没有后续真正的请求了。


image.png


解决方案:由后台枚举前端需要的headers,在Access-Control-Allow-Headers中返回。


此外,将Access-Control-Allow-Origin设置为*也有一些别的限制:



参考



作者:WeilinerL
来源:juejin.cn/post/7372396174249459750
收起阅读 »

如果你要离开,就必须头也不回地离开!

再次重温《夜航西飞》时,里面的这段话更加让我觉得有智慧,甚至会觉得是整本书里最好的句子:如果必须离开你曾经住过、爱过、深埋着所有过往的地方,无论以何种方式,都不要慢慢离开,要决绝地离开,永远不回头。不要相信过去的时光才更好,它们已经消亡了。 只是有的话需要当...
继续阅读 »

再次重温《夜航西飞》时,里面的这段话更加让我觉得有智慧,甚至会觉得是整本书里最好的句子:如果必须离开你曾经住过、爱过、深埋着所有过往的地方,无论以何种方式,都不要慢慢离开,要决绝地离开,永远不回头。不要相信过去的时光才更好,它们已经消亡了。


图片


只是有的话需要当了一定的年龄,经历了一些事情之后才能对其有更加深刻的了解,并且能付诸行动。


我的烟龄已经十年了,戒烟对我来说是一件下了无数次决心的事,但是每次基本上都是以失败而告终,里面有一个搞笑和讽刺的点,就是每次下定决心戒烟的时候,我不会把现存的烟给丢掉,反而会进行自我暗示:抽完这包就不再抽了,呵呵!


这其实就像赌徒已经输得倾家荡产了,于是还在想着再赌一次。但是只要有一次,就会有第二次,无数次。


所以说,如果没有砸碎一切的决心,那么是不可能彻底走出的。


我这次下定决心戒烟是因为喉咙确实不太舒服,其次我是想好好当一个健康的人,所以前段时间,我抽完一支后,觉得不能再抽了,于是毫无留情将剩下的烟全部用水淋湿,丢进了垃圾桶。


在戒烟一段时间后,嗓子明显舒服了许多,昨天去见了一个朋友,他说我最近的气色好多了哎。我自己也感觉好了一点。无论从身体的舒适程度还是精神状态。


所以当我再回过头来阅读《夜航西飞》的时候,里面的这句话不由让我感叹感叹其力量。


我前段时间和一个妹子聊天的时候,她说自己谈了一段时间的男朋友,后面发现其出轨了,于是自然就分手了。但是到现在依然还在想着他。现在,我也想把开头的那句话送给你。


因为一个伤害了你的人,就不要再对其有任何留恋,做事情就别再拖泥带水,而且大概率在之前的感情中,你也只是一厢情愿,别人实际上根本不care,那这样的感情注定就是你输。


此时你还深陷其中干嘛?生活的美好处处都是,何不去感受美好的生活,头也不回地离开才是最理智的做法。


后面我愈发觉得,一个总是活在过去的人,是不值得拥有更好的东西的,总觉得过去的时光是美好的,但是再美好的昨天,它已经消亡了,而此刻,才是最美好的时光碎片。


就像此刻,六点起床后,我洗漱完毕,拉开窗帘,阳光洒在我的身上,我烧了一杯温水来喝,接着打开电脑来写下心里的想法,那么我就觉得此刻比过往的任何时刻都要美好,纵使我会回忆高中六点起床,然后走在绿树成荫的校园路上,再去操场上和同学笑着跑几圈,那虽然也是美好的时光,但是它已经消亡了,而此刻的时光是我能够牢牢抓在手里的!


那么过了今天,明天当我早起后,戴着耳机去附近的公园走上几圈后,我也会觉得昨天是美好的,但是我不会活在昨天,而是牢牢把握住今天!


作者:苏格拉的底牌
来源:juejin.cn/post/7372871883337039935
收起阅读 »

前端命令行部署:再也不用把dist包给后端部署服务了!

web
好物推荐 超简单的命令行部署。给在小公司部署还是给后端dist包的萌新小伙伴们~ 这边项目本身就是使用命令行部署到,不过那个命令行工具是自己写的,是嵌入到公司某一个私有npm包里,和其他依赖耦合在一起。灵活性不是很好。 这两天发现了一个别人写的一个deploy...
继续阅读 »

好物推荐


超简单的命令行部署。给在小公司部署还是给后端dist包的萌新小伙伴们~


这边项目本身就是使用命令行部署到,不过那个命令行工具是自己写的,是嵌入到公司某一个私有npm包里,和其他依赖耦合在一起。灵活性不是很好。
这两天发现了一个别人写的一个deploy cli。感觉蛮好用的。分享一下。


希望可以帮助更多刚入行小伙伴了解更多前端玩法。


前端命令行部署


很多公司的前端部署流程都是先打一个dist包。然后给后端同事帮忙部署。


前端:::
1714281510854.png


后端:::


529ae5c36b03377bf116bafea2e95f1.png


(开玩笑的,工作中的后端同事都没那么调皮)


本文的内容就是如何使用命令行进行前端自动部署。


我们整个网站的读取,其实就是我们上传一个静态的文件包到服务器,然后服务器上的后台服务读取我们的静态包,来进行页面的展示。所以,前端自动化部署的关键,就是,能把dist包传到服务器的指定目录下就OK了。


部署流程


推荐一个deploy cli工具(deploy-cli-service)


安装


  1. 执行 npm install deploy-cli-service -g 进行全局安装 。

  2. 执行 deploy-cli-service - v 查看版本


初始化配置文件

在项目根目录执行 deploy-cli-service init 进行初始化


deploy-cli-service init命令执行后项目目录下会出现一个名为deploy.config的文件


image.png


deploy-cli-service init初始化的内容会被默认输入到 deploy.config


修改配置文件

deploy-cli-service init初始化之后输入的内容都会默认被写入deploy.config文件中。


image.png


然后看看相关的属性有没有什么需要修改的就ok。


配置部署命令


image.png


"deploy:test": "deploy-cli-service deploy --mode test"," 写入到 package.json中的script里。


然后在命令行执行 "npm run deploy:test"


成功部署后会如下显示


image.png


image.png


注意


配置 deploy.config.js时尽量使用ssh证书登录,不要使用服务器密码,把服务器密码写在前端代码里是一件非常不好的操作。


deploy-cli-service npm地址


luck


作者:工边页字
来源:juejin.cn/post/7362924623825256463
收起阅读 »

互联网+《周易》:我在github学算卦

web
前言 《周易》乃周文王姬昌所作,是中国传统思想文化中自然哲学与人文实践的理论根源,是古代汉民族思想、智慧的结晶,被誉为“大道之源”。内容极其丰富,对中国几千年来的政治、经济、文化等各个领域都产生了极其深刻的影响。 像这种千古奇书,每个中国人都应该读一读,一是因...
继续阅读 »

前言


《周易》乃周文王姬昌所作,是中国传统思想文化中自然哲学与人文实践的理论根源,是古代汉民族思想、智慧的结晶,被誉为“大道之源”。内容极其丰富,对中国几千年来的政治、经济、文化等各个领域都产生了极其深刻的影响。


像这种千古奇书,每个中国人都应该读一读,一是因为这是老祖宗的智慧,我们不能丢弃;二是因为《周易》蕴含宇宙人文的运行规律,浅读可修身养性,熟读可明自我,深究可知未来,参透就可知天命了。


东汉著名史学家、文学家班固在《汉书•艺文志》中提出《周易》的成书是:人更三圣,世历三古


那么在哪里才可以读到呢?


其实易经的完本在网上随便就可以找到,但是都不适合在摸鱼的时候读 (!🤡),打开花花绿绿或者神神叨叨的小网站,你的 leader 肯定一眼就看出你在摸鱼。


既然没有这种网站,那干脆自己做一个。


vitePress + github pages 快速搭建


vitePress 快速开始


pnpm add -D vitepress

pnpm vitepress init

填写完 cli 里的几个问题,项目就可以直接运行了。可以看到网站直接解析了几个 示例的 md 文件,非常的神奇。


处理《周易》文本


那么哪里才可以找到《周易》的 markdown 版本呢,找了一圈也没有找到,最后找到了一个 txt 的,我觉得写个脚本转换一下。


首先,我拿 vscode 的正则给每个标题加上井号,使其成为一级标题


QQ2024511-183935.webp


此时,所有的标题都被改成了md格式的一级标题,然后直接将后缀名从 .txt 改为 .md 即可。


看过 vitepress 的文档并经过实操后发现,它的目录是一个一个的小 markdown 文件组成的,而单个 markdown 内的标题等在右侧显示


image.png


那么此时就需要把《周易》完本,按照六十四卦分为六十四个 md 文件。


我写了一个node脚本:


const fs = require('fs');

// 读取zhouyi.md文件
fs.readFile('zhouyi.md', 'utf8', (err, data) => {
 if (err) {
   console.error('读取文件出错:', err);
   return;
}

 // 按一级标题进行分割
 const sections = data.split('\n# ');

 // 循环处理每个一级标题的内容
 sections.forEach((section, index) => {
   // 提取标题和内容
   const lines = section.split('\n');
   const title = lines[0];
   const content = lines.slice(1).join('\n');

   // 写入到单独的文件中
   const fileName = `zhouyi_${index + 1}.md`;
   fs.writeFile(fileName, `# ${title}\n\n${content}`, err => {
     if (err) {
       console.error(`写入文件 ${fileName} 出错:`, err);
    } else {
       console.log(`已创建文件: ${fileName}`);
    }
  });
});
});


取名为md-slicer.js ,在控制台输入


node md-slicer.js

即可生成


image.png


然后写一个在 .vitepress/config.mtssidebar的生成函数:


let itemsLength = 64
function getSidebar() {
 let items: {}[] = [{
   text: '《周易》是什么?',
   link: '/what.md'
}]
 for (let i = 1; i <= itemsLength; i++) {
   items.push({ text: `第${numberToChinese(i)}卦`, link: `/zhouyi_${i}.md` })
}
 return items
}

numberToChinese函数用来将阿拉伯数字转为中文数字,因为周易只有六十四卦,所以不用考虑很多,够用即可


// numberToChinese
function numberToChinese(number) {
 const chineseNumbers = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
 const chineseUnits = ['', '十', '百', '千', '万', '亿'];

 // 将数字转换为字符串,以便于处理每一位
 const numStr = String(number);

 let result = '';
 let zeroFlag = false; // 用于标记是否需要加上“零”

 for (let i = 0; i < numStr.length; i++) {
   const digit = parseInt(numStr[i]); // 当前位的数字
   const unit = chineseUnits[numStr.length - i - 1]; // 当前位的单位

   if (digit !== 0) {
     if (zeroFlag) {
       result += chineseNumbers[0]; // 如果前一位是零,则在当前位加上“零”
       zeroFlag = false;
    }
     result += chineseNumbers[digit] == "一" && unit == "十" ? unit : chineseNumbers[digit] + unit; // 加上当前位的数字和单位,当一十时,省略前面的一
  } else {
     zeroFlag = true; // 如果当前位是零,则标记为需要加上“零”
  }
}
 return result;
}

然后,设置一下vitepress基础配置和打包输出路径


export default defineConfig({
 title: "周易",
 description: "周易",
 base: "/thebookofchanges/",
 head: [
  ['link', { rel: 'icon', href: 'yi.svg' }] // 这里是你的 Logo 图片路径
],
 outDir: 'docs', // 输出到docs ,可以直接在 github pages 使用
 themeConfig: {
   // https://vitepress.dev/reference/default-theme-config
   nav: [
    { text: '首页', link: '/' },
    { text: '阅读', link: '/zhouyi_1.md' }
  ],
   logo: '/yi.svg',
   sidebar: [
    {
       text: '目录',
       items: getSidebar()
    }
  ],

   socialLinks: [
    { icon: 'github', link: 'https://github.com/LarryZhu-dev/thebookofchanges' }
  ]
}
})


然后简单给网站设计一个logo


image.png


字体是华文隶书,转化为路径后,将它拉瘦一点,再导出为 svg。


最后,用 pnpm run docs:build打包即可,打包时注意设置基本路径为 github pages 的仓库名。


发布


push到github后,在 Setting/Pages 页面发布即可。


image.png


效果预览


最后,网站运行在:larryzhu-dev.github.io/thebookofch…


image.png


image.png


仓库地址:github.com/LarryZhu-de… 来点star🤣


结语


现在只有简单的原文,如有 《周易》大佬,欢迎大佬提交注解PR。


作者:德莱厄斯
来源:juejin.cn/post/7367659849101312015
收起阅读 »

gpt-4o这些玩法真的太逆天了

OpenAI在近期发布了GPT-4系列的新模型GPT-4o。这一更新主要聚焦于多模态和端侧应用,为用户提供了全新的交互体验。 GPT-4o作为OpenAI的新模型,具有三大显著特点: 多模态:GPT-4o能够接受文本、音频、图像作为组合输入,并生成任何文本、音...
继续阅读 »

OpenAI在近期发布了GPT-4系列的新模型GPT-4o。这一更新主要聚焦于多模态和端侧应用,为用户提供了全新的交互体验。


GPT-4o作为OpenAI的新模型,具有三大显著特点:


多模态:GPT-4o能够接受文本、音频、图像作为组合输入,并生成任何文本、音频和图像的组合输出。这种多模态的理解能力让GPT-4o在处理复杂任务时更具优势,如识别人类的感情并根据感情做出“有感情的反应”。


几乎无延迟:GPT-4o对音频输入的响应时间最短为232毫秒,平均为320毫秒,这与人类在对话中的响应时间相似。这种极快的响应速度使得GPT-4o能够实时地与用户进行交互,提供流畅的用户体验。


可在电脑桌面运行:OpenAI还将与苹果合作推出了适用于macOS的ChatGPT桌面级应用。这一应用允许用户在没有网络的情况下使用ChatGPT,并且可以在本地设备上处理敏感信息,保护用户隐私。


一些逆天的视频展示


下面来一起了解一下它官网的一些视频展示的逆天操作:


第一个王炸,作业辅导


作业辅导


视频中展示的是巨佬在使用 GPT-4o 对他儿子进行作业辅导。它开始就告诉gpt-4o 说不要直接说出答案,而是帮助它一步一步解决这个几何题目,我们在视频中可以看到,的确是这样,gpt-4o 一步一步的帮助他儿子解决了这个问题,而且还是非常细致的解释,并且是非常有情感的,每当他儿子完成一步之后,gpt-4o 从语气上都会有一种更进一步的感觉,这种情感化的交互方式,让人感觉非常的亲切。


而且,所有的过程都是这个娃在拿着笔在一步一步的解决这个几何题目,gpt-4o 就是看着这个娃做的解题过程,它会判断这个娃是否步骤对了,这个交互简直太赞了!这明显得益于GPT-4o的图像理解能力的增强。


作业辅导


讲真,按照这个趋势,教培行业似乎极有可能被干掉,那些不会做奥数题的家长,有福了,因为 安特曼说,gpt-4o 是会免费的。这意味着,你不需要花费一分钱,就可以请一个专业的教培老师,帮助你的孩子解决问题。


第二个王炸,精神分裂,一个端中两个 gpt-4o 互动起来了


之前我们于gpt 的实时语音对话只能是一对一,好了,颠覆认知的时刻来了,你在一个对话窗口中,可以同时存在两个gpt-4o对话,甚至,它两还可以互相对话,这个视频中,这两 gpt-4o 相互唱起了小曲。。。
两个 gpt-4o 协调


外语学习


外语学习


在这个例子中,研究人员展示的是,它告诉 gpt-4o 它想学习西班牙语言,当然它使用英语说的,然后它使用摄像头对着苹果和香蕉,问gpt-4o这个是什么,gpt-4o 利用它图像识别的能力,认出了香蕉和苹果,然后告诉研究人员。


但是!但是!但是!它回的语言居然是英语和西班牙语的混合,也就是,gpt-4o 回答,this is manzana and plátano。差点没有惊掉我的下巴,一句回答中包含了多种语言。这中组合输出的能力,简直太强了。


参与多人对话中来


图 4


这个视频展示的是 gpt-4o 加入到了一个在线会议中,它可以看到共享的屏幕,因此它知道会议有多少个人,然后开始是每个人说了一下自己的喜欢的人和事,接着主持人发文,他们各自有哪些爱好,gpt-4o 一一都回答出来了,而且是非常的准确,最后还来了一个总结,后面腾讯会议,zoom 估计交互得更上啊,不加入一个智能记录员,这体验就得甩开好几条街了。


同声传译


同声传译


这个视频展示的是 gpt-4o 扮演的事一个翻译者的角色,画面中的两个人一个人是将英语的,一个人是将西班牙语的,gpt-4o 就负责把听到的英语转化为西班牙语,把西班牙语转换为英语,然后两个哥们就愉快的对话了,你说你的西班牙,我说我的英语,我们都听得很懂的,所以,同声传译这个行业,是不是也要凉凉了。


外婆的澎湖湾


催命曲


歪日哦,富有情感的和你对话是王炸的话,和这个对比简直小巫见大巫,它哼起了小区,而且还会偶尔和你聊天的时候爽朗的发出笑声,这种情感化的交互方式,让人感觉非常的亲切。当这个老外说它想睡觉,哼个小曲,gpt-4o 就开始哼起了外婆的澎湖湾,听得我差点给睡着了...这种情感化的交互方式,让人感觉非常的亲切。


语速控制


语速控制


在这个视频中,老外让 gpt-4o 数数,1,2,3,。。。10. gpt-4o 一口气说完了,然后老外说,你能不能慢一点,gpt-4o 就慢慢的说了一遍,然后老外说,你能不能快一点,gpt-4o 就快速的说了一遍,这种语速控制就完全可以用来训练自己的听力了,这个功能比较赞,不过,我的下巴还在。


开玩笑


开玩笑


这个视频中,老外给 gpt-4o 说它要给它老爸讲个笑话,然后他想让 gpt-4o 先听听它这个笑话是不是好笑,结果,gpt-4o 真的爽朗得笑了,笑得一点都不像机器人,听到它这个笑声的时候,我的下巴还差那么一点就掉了。。。


你是我的眼


你是我的眼


这个视频的效果也是相当的炸裂,视频中时候一个盲人,很显然他看不见,因此它所到之处,让 gpt-4o 告诉它周围都有写什么风景,gpt-4o 一一告诉他,从这个视频中,我有点小小的启发!


gpt-4o 可能更好的交互是类似 Google Glass的形式,这样的产品出来,估计全世界的盲人都要为之震撼,他们都将会重见光明,这个产品的价值,简直不可估量。


两个 gpt-4o 互相对话


两个 gpt-4o 互相对话


两个 gpt-4o 互相对话


这个视频中显示了两个gpt-4o 开始了对话,什么,永动机???实际上不是,是视频中 openai 的大佬先告诉一个 gpt-4o 说等会有个可以看见世界的 AI 会和你对话,你可以和他交流,随后它启动了另外一个 gpt-4o,然后两个 gpt-4o 开始对话了,并且大佬还可以随时打断加入他们的对话。我的脑袋已经开始疼了,这个视频太炸裂了。这意味着,我是不是可以搞 3 个手机,搞一桌四川麻将了???


着装建议


着装建议


视频中,这个大佬要准备面试了,问 gpt-4o 怎么穿着得体,然后 gpt-4o 告诉他带个帽子试试,结果带上 gpt-4o 就开始爽朗的笑了。。。,嗯,后面穿什么出门,估计可以让 gpt-4o 建议建议合不合适。。。


桌游助手


桌游助手


这个场景是两个人想玩石头剪刀布的游戏,然后让 gpt-4o 做裁判,然后就开始了,gpt-4o 说 1,2,3,亮出你们的爪子,然后判断谁输谁赢,好了,我似乎又找到了一个乐子。




作者:brzhang
来源:juejin.cn/post/7369481217030438921
收起阅读 »

7个Js async/await高级用法

web
7个Js async/await高级用法 JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护...
继续阅读 »

7个Js async/await高级用法


JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护性。在掌握了基础用法之后,下面将介绍一些高级用法,以便充分利用async/await实现更复杂的异步流程控制。


1. async/await与高阶函数


当需要对数组中的元素执行异步操作时,可结合async/await与数组的高阶函数(如mapfilter等)。


// 异步过滤函数
async function asyncFilter(array, predicate) {
const results = await Promise.all(array.map(predicate));

return array.filter((_value, index) => results[index]);
}

// 示例
async function isOddNumber(n) {
await delay(100); // 模拟异步操作
return n % 2 !== 0;
}

async function filterOddNumbers(numbers) {
return asyncFilter(numbers, isOddNumber);
}

filterOddNumbers([1, 2, 3, 4, 5]).then(console.log); // 输出: [1, 3, 5]

2. 控制并发数


在处理诸如文件上传等场景时,可能需要限制同时进行的异步操作数量以避免系统资源耗尽。


async function asyncPool(poolLimit, array, iteratorFn) {
const result = [];
const executing = [];

for (const item of array) {
const p = Promise.resolve().then(() => iteratorFn(item, array));
result.push(p);

if (poolLimit <= array.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= poolLimit) {
await Promise.race(executing);
}
}
}

return Promise.all(result);
}

// 示例
async function uploadFile(file) {
// 文件上传逻辑
}

async function limitedFileUpload(files) {
return asyncPool(3, files, uploadFile);
}

3. 使用async/await优化递归


递归函数是编程中的一种常用技术,async/await可以很容易地使递归函数进行异步操作。


// 异步递归函数
async function asyncRecursiveSearch(nodes) {
for (const node of nodes) {
await asyncProcess(node);
if (node.children) {
await asyncRecursiveSearch(node.children);
}
}
}

// 示例
async function asyncProcess(node) {
// 对节点进行异步处理逻辑
}

4. 异步初始化类实例


在JavaScript中,类的构造器(constructor)不能是异步的。但可以通过工厂函数模式来实现类实例的异步初始化。


class Example {
constructor(data) {
this.data = data;
}

static async create() {
const data = await fetchData(); // 异步获取数据
return new Example(data);
}
}

// 使用方式
Example.create().then((exampleInstance) => {
// 使用异步初始化的类实例
});

5. 在async函数中使用await链式调用


使用await可以直观地按顺序执行链式调用中的异步操作。


class ApiClient {
constructor() {
this.value = null;
}

async firstMethod() {
this.value = await fetch('/first-url').then(r => r.json());
return this;
}

async secondMethod() {
this.value = await fetch('/second-url').then(r => r.json());
return this;
}
}

// 使用方式
const client = new ApiClient();
const result = await client.firstMethod().then(c => c.secondMethod());

6. 结合async/await和事件循环


使用async/await可以更好地控制事件循环,像处理DOM事件或定时器等场合。


// 异步定时器函数
async function asyncSetTimeout(fn, ms) {
await new Promise(resolve => setTimeout(resolve, ms));
fn();
}

// 示例
asyncSetTimeout(() => console.log('Timeout after 2 seconds'), 2000);

7. 使用async/await简化错误处理


错误处理是异步编程中的重要部分。通过async/await,可以将错误处理的逻辑更自然地集成到同步代码中。


async function asyncOperation() {
try {
const result = await mightFailOperation();
return result;
} catch (error) {
handleAsyncError(error);
}
}

async function mightFailOperation() {
// 有可能失败的异步操作
}

function handleAsyncError(error) {
// 错误处理逻辑
}

通过以上七个async/await的高级用法,开发者可以在JavaScript中以更加声明式和直观的方式处理复杂的异步逻辑,同时保持代码整洁和可维护性。在实践中不断应用和掌握这些用法,能够有效地提升编程效率和项目的质量。


作者:慕仲卿
来源:juejin.cn/post/7311603994928513076
收起阅读 »

Google 如果把 Go 团队给裁了会怎么样?

大家好,我是煎鱼。 节前有一则劲爆消息,Google 把 Python 基础团队和 flutter/dart 团队里相当多的开发人员给解雇了,据说可能是要换个城市重组(真是熟悉的 CY 套路)。 据悉被解雇的人中基本都是负责了 Python 重要维护的相关核心...
继续阅读 »

大家好,我是煎鱼。


节前有一则劲爆消息,Google 把 Python 基础团队和 flutter/dart 团队里相当多的开发人员给解雇了,据说可能是要换个城市重组(真是熟悉的 CY 套路)。


据悉被解雇的人中基本都是负责了 Python 重要维护的相关核心成员。


如下图所示:



此时引发了国内外社区一个较大的担忧,如果 Google 如法炮制,要放弃 Go 核心团队。会发生什么事,会不会有什么问题?



现在有什么


先知道可能会失去什么,那得先盘点一下 Go 这一门编程语言和 Go 核心团队在 Google 获得了什么。


根据我们以往对 @Lance Taylor 所澄清以及各处的描述,可以估算 Go 在 Google 大概获得了什么。


其至少包含以下内容:



  1. 工作岗位:Go 核心团队相关成员的工作岗位,包含薪资、福利等各种薪酬内容。

  2. 软硬件资源:Go 相关的软硬件资源(例如:知识产权、服务器、域名、模块管理镜像)等网上冲浪所需信息。

  3. 线下活动:Go 世界各地部分大会的开展可能会变少,或缩减规模(资金、背书等)。

  4. 大厂内部资源:因为失去 Google 内部的资源,可能逐步失去一些先进项目的熏陶和引入使用 Go 这一门编程语言的机会。

  5. 推广和反馈渠道:Go 一些显著问题和特性的发现、响应,可能会变慢。因为 Go 对于 Google 内部的问题处理和特性需要,历史上来看都是按最高优先级处理。


可能会发生什么事


如果真的一刀切,Google 把 Go 核心团队干没了,基础设施全部都不提供了。


大家普遍认为,会出现如下几种情况:



  1. 如果 Go 团队中的很多人被裁员,他们会另谋高就。各散东西。维护积极性和组织性会大幅下降。

  2. 如果 Google 决定完全停止对 Go 的投资,Go 的维护可能会变得更加复杂,因为它需要运行大量的基础设施。在这种情况下,可能会出现 Go 由 Google 转移到一个外部的基金会,会有明显的阶段性维护波动。

  3. 如果 Google 选择在内部其他团队对 Go 继续投入,较差的情况是 Google 会灵活运用他们对知识产权的所有权 --Go 很可能会更名为其他东西。


基金会方面,另外大家认为最有可能接受 Go 的基金会是:CNCF,因为 Go 项目在 CNCF 中基于数量来讲是最大的。


如下图部分所示:



同时 CNCF 和 Go 的云原生属性最为强烈,契合度非常高。


参考 Rust 发展史


@azuled 根据 Rust 的发展历史,给出了自己的一些见解。如下所表述:


1、Rust 被踢出 Mozilla 核心,成为一个独立的基金会,但它仍然存活了下来。事实上,它后来可能做得更好。


2、我认为很有可能围绕 Go 成立一个非营利组织,而且很有可能有足够多的大公司使用它来支持它,至少在一段时间内是这样。


总结


在目前这个大行情下,Go 作为 Google Cloud 团队的一员,和云原生的故事捆绑在一起。如果 Google 业绩出现波动,或者要继续降本增效。


这类没有直接营收的基础部门或团队还是比较危险的,因为其会在企业中根据利润中心、成本中心进行分摊和计算人效成本等。


如果真的强硬切割,势必会对 Go 这门编程语言产生阶段性的冲击。但未来是好是坏,就不好说了。



作者:煎鱼eddycjy
来源:juejin.cn/post/7366070642047008783
收起阅读 »

Spring Boot 3 集成 Jasypt详解

随着信息安全的日益受到重视,加密敏感数据在应用程序中变得越来越重要。Jasypt(Java Simplified Encryption)作为一个简化Java应用程序中数据加密的工具,为开发者提供了一种便捷而灵活的加密解决方案。本文将深入解析Jasypt的工作原...
继续阅读 »

随着信息安全的日益受到重视,加密敏感数据在应用程序中变得越来越重要。Jasypt(Java Simplified Encryption)作为一个简化Java应用程序中数据加密的工具,为开发者提供了一种便捷而灵活的加密解决方案。本文将深入解析Jasypt的工作原理,以及如何在Spring Boot项目中集成和使用Jasypt来保护敏感信息。


springboot-jasypt.jpg


springboot-jasypt.jpg


Jasypt简介


Jasypt(Java Simplified Encryption)是一个专注于简化Java加密操作的工具。它提供了一种简单而强大的方式来处理数据的加密和解密,使开发者能够轻松地保护应用程序中的敏感信息,如数据库密码、API密钥等。


Jasypt的设计理念是简化加密操作,使其对开发者更加友好。它采用密码学强度的加密算法,支持多种加密算法,从而平衡了性能和安全性。其中,Jasypt的核心思想之一是基于密码的加密(Password Based Encryption,PBE),通过用户提供的密码生成加密密钥,然后使用该密钥对数据进行加密和解密。


该工具还引入了盐(Salt)的概念,通过添加随机生成的盐值,提高了加密的安全性,防止相同的原始数据在不同的加密过程中产生相同的结果,有效抵御彩虹表攻击。


Jasypt与Spring Boot天然契合,可以轻松集成到Spring Boot项目中,为开发者提供了更便捷的数据安全解决方案。通过Jasypt,开发者可以在不深入了解底层加密算法的情况下,轻松实现数据的安全保护,使得应用程序更加可靠和安全。


官网地址: http://www.jasypt.org/


github地址: github.com/ulisesbocch…


Spring Boot 3 集成 Jasypt


添加依赖


在pom文件中添加一下依赖


<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot</artifactId>
<version>3.0.5</version>
</dependency>

添加配置文件


未指定前后缀的话默认格式ENC()括号里面是加密后的密文 然后实现自动解密


spring:
# 数据源配置
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.10.106:3306/xj_doc?characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: ENC(BLC3UQBxshlcA9tnMyJL7w==)

# 加密配置
jasypt:
encryptor:
# 指定加密密钥,生产环境请放到启动参数里面
password: 0f7b0a5d-46bc-40fd-b8ed-3181d21d644f
# 指定解密算法,需要和加密时使用的算法一致
algorithm: PBEWithMD5AndDES

iv-generator-classname: org.jasypt.iv.NoIvGenerator

# property:
# # 算法识别的前后缀,默认ENC(),包含在前后缀的加密信息,会使用指定算法解密
# prefix: ENC@[
# suffix: ]

启动类添加注解


在启动类上添加注解@EnableEncryptableProperties注解来开启自动解密


@SpringBootApplication
@MapperScan("cn.xj.xjdoc.**.mapper")
@EnableEncryptableProperties //开启自动解密功能
public class XjdocApplication {
public static void main(String[] args) {
SpringApplication.run(XjdocApplication.class, args);
}
}

测试类


public class JasyptUtil {

public static void main(String[] args){
StandardPBEStringEncryptor standardPBEStringEncryptor =new StandardPBEStringEncryptor();
/*配置文件中配置如下的算法*/
standardPBEStringEncryptor.setAlgorithm("PBEWithMD5AndDES");
/*配置文件中配置的password*/
standardPBEStringEncryptor.setPassword("0f7b0a5d-46bc-40fd-b8ed-3181d21d644f");
//加密
String jasyptPasswordEN =standardPBEStringEncryptor.encrypt("xj2022");
//解密
String jasyptPasswordDE =standardPBEStringEncryptor.decrypt(jasyptPasswordEN);
System.out.println("加密后密码:"+jasyptPasswordEN);
System.out.println("解密后密码:"+jasyptPasswordDE);
}
}

生产环境安全处理


jasypt的password值放在配置文件中在生产环境中是不安全的,我们可以将password值放到启动命令中,删除配置文件中password 的配置行,启动命令如下所示:


java -Djasypt.encryptor.password=password -jar jasypt-spring-boot-demo-0.0.1-SNAPSHOT.jar

或者


java -jar jasypt-spring-boot-demo-0.0.1-SNAPSHOT.jar --jasypt.encryptor.password=password

总结


Jasypt作为一个简单而强大的加密工具,为Java应用程序提供了便捷的数据保护方案。通过与Spring Boot的集成,开发者可以在应用程序中轻松地加密和解密敏感信息。在实际项目中,选择合适的加密方式、安全存储密码以及与Spring Security等安全框架的集成,都是保障应用程序安全的关键步骤。希望本文能够帮助读者更深入地了解Jasypt,并在实际项目中合理地运用加密技术。


作者:修己xj
来源:juejin.cn/post/7318616887415717924
收起阅读 »

Springboot3 + SpringSecurity + JWT + OpenApi3 实现认证授权

Springboot3 + SpringSecurity + JWT + OpenApi3 实现双token 目前全网最新的 Spring Security + JWT 实现双 Token 的案例!收藏就对了,欢迎各位看友学习参考。此项目由作者个人创作,可以供...
继续阅读 »

Springboot3 + SpringSecurity + JWT + OpenApi3 实现双token


目前全网最新的 Spring Security + JWT 实现双 Token 的案例!收藏就对了,欢迎各位看友学习参考。此项目由作者个人创作,可以供大家学习和项目实战使用,创作不易,转载请注明出处!


该项目使用目前最新的 Sprin Boot3 版本,采用目前市面上最主流的 JWT 认证方式,实现双token刷新。



温馨提示:SpringBoot3 版本必须要使用 JDK11 或 JDK19



SpringBoot3 新特性


Spring Boot3 是一个非常重要的版本,将会面临一个新的发展征程!Sprin Boot 3.0 包含了 12 个月以来,151 个人的 5700+ 次 commit 的贡献。这是自 4 年半前发布的 2.0 版本以来的第一次重大修订,这也是第一个支持 Spring Framework 6.0 和 GraaIVM 的 Spring Boot GA 版本。


Spring Boot 3.0 新版本的主要亮点:



  1. 最低要求为 Java 17 ,兼容 Java 19

  2. 支持用 GraalVM 生成原生镜像,代替了 Spring Native

  3. 通过 Micrometer 和 Micrometer 追踪提高应用可观察性

  4. 支持具有 EE 9 baseline 的 Jakarta EE 10


为什么采用双 Token刷新?


**场景假设:**星期四小金上班的时候摸鱼,准备在某APP 上面追剧,已经深深的陷入了角色中无法自拔,此时如果 Token 过期了 ,小金就不得不重新返回登录界面,重新进行登录,那么这样小金的一次完整的追剧体验就被打断了,这种设计带给小金的体验并不好,于是就需要使用双 Token 来解决。


**如何使用:**在小金首次登陆 APP 时,APP 会返回两个 Token 给小金,一个 accessToken,一个 refreshToken,其中 accessToken 的过期时间比较短,refreshToken 的时间比较长。当 accessToken 失效后,会通过 refreshToken 去重新获取 accessToken,这样一来就可以在不被察觉的情况下仍然使小金保持登录状态,让小金误以为自己一直是登录的状态。并且每次使用refreshToken 后会刷新,每一次刷新后的 refreshToken 都是不相同的。


**优势说明:**小金能够有一次完整的追剧体验,除非摸鱼时被老板发现了。accessToken 的存在,保证了登录的正常验证,因为 accessToken 的过期时间比较短,所以也可以保证账号的安全性。refreshToken 的存在,保证了小金无需在短时间内反复的登录来保持 Token 的有效性,同时也保证了活跃用户的登录状态可以一直延续而不需要重新登录,反复刷新也防止了某些不怀好意的人获取 refreshToken 后对用户账号进行不良操作。


一图胜千言:


image-20230604084837740


项目准备


项目采用 Spring Boot 3 + Spring Security + JWT + MyBatis-Plus + Lombok 进行搭建。


创建数据库


user 表


image-20230603220205094


token 表


在实际中应该把 token 信息保存到 redis


image-20230603220333914


创建 Spring Boot 项目


创建一个 Spring Boot 3 项目,一定要选择 Java 17 或者 Java 19


引入依赖


<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
<version>3.0.4version>
dependency>

<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-apiartifactId>
<version>0.11.5version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-implartifactId>
<version>0.11.5version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-jacksonartifactId>
<version>0.11.5version>
dependency>

编写配置文件


server:
port: 8417
spring:
application:
name: Spring Boot 3 + Spring Security + JWT + OpenAPI3
datasource:
url: jdbc:mysql://localhost:3306/w_admin
username: root
password: jcjl417
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
table-prefix: t_
id-type: auto
type-aliases-package: com.record.security.entity
mapper-locations: classpath:mapper/*.xml
application:
security:
jwt:
secret-key: VUhJT0pJT0hVWUlHRFVGVFdPSVJISVVHWUZHVkRVR0RISVVIREJZI1VJSEZTVUdZR0ZTVVk=
expiration: 86400000 # 1天
refresh-token:
expiration: 604800000 # 7 天
springdoc:
swagger-ui:
path: /docs.html
tags-sorter: alpha
operations-sorter: alpha
api-docs:
path: /v3/api-docs

项目实现


准备项目所需要的一系列代码,如 entity、controller 、service、mapper 等


系统角色 Role


定义一个角色(Role)枚举,详细代码参考文章结尾处的项目源码


public enum Role {

// 用户
USER(Collections.emptySet()),
// 一线人员
CHASER( ... ),
// 部门主管
SUPERVISOR( ... ),
// 系统管理员
ADMIN( ... ),
;

@Getter
private final Set permissions;

public List getAuthorities() {
var authorities = getPermissions()
.stream()
.map(permission -> new SimpleGrantedAuthority(permission.getPermission()))
.collect(Collectors.toList());
authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name()));
return authorities;
}
}

User 实现 UserDetails


温馨提示:


由于 Spring Security 源码设计的时候 ,将用户名和密码属性定义为 username 和 password,所以我们看到的大部分教程都会遵循源码中的方式,习惯性的将用户名定义为 username,密码定义为 password。


其实我们大可不必遵守这个规则,在我的系统中使用邮箱登录,也即是将邮箱(email)作为 Security 中的用户名(username),那么我必须要将用户输入的 email 作为 username 来存放,这会使我感到非常的不适,因为我的系统中正真的 username 将会 用另外一个单词来命名。


如何避免登录时的字段必须设置为 username 和 password 呢?



重写 getter方法, 只有你的系统中登录的用户名和密码属性不是 username 和 password 的情况下 ,你进行重写才会看到下面红色框中的提示。


202306032035283

重写 username 和 password 的 getter方法


@Override
public String getUsername() {
return email;
}

@Override
public String getPassword() {
return password;
}

Security 配置文件



需要注意的是 WebSecurityConfigurerAdapter 在 Spring Security 中已经被弃用和移除


下面将采用新的配置文件



@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity
public class SecurityConfiguration {

private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;
private final RestAuthorizationEntryPoint restAuthorizationEntryPoint;
private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeHttpRequests()
.requestMatchers(
"/api/v1/auth/**",
"/api/v1/test/**",
"/v2/api-docs",
"/v3/api-docs",
"/v3/api-docs/**",
"/swagger-resources",
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui/**",
"/doc.html",
"/webjars/**",
"/swagger-ui.html",
"/favicon.ico"
).permitAll()
.requestMatchers("/api/v1/supervisor/**").hasAnyRole(SUPERVISOR.name(), ADMIN.name())

.requestMatchers(GET, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_READ.name(), ADMIN_READ.name())
.requestMatchers(POST, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_CREATE.name(), ADMIN_CREATE.name())
.requestMatchers(PUT, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_UPDATE.name(), ADMIN_UPDATE.name())
.requestMatchers(DELETE, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_DELETE.name(), ADMIN_DELETE.name())

.requestMatchers("/api/v1/chaser/**").hasRole(CHASER.name())

.requestMatchers(GET, "/api/v1/chaser/**").hasAuthority(CHASER_READ.name())
.requestMatchers(POST, "/api/v1/chaser/**").hasAuthority(CHASER_CREATE.name())
.requestMatchers(PUT, "/api/v1/chaser/**").hasAuthority(CHASER_UPDATE.name())
.requestMatchers(DELETE, "/api/v1/chaser/**").hasAuthority(CHASER_DELETE.name())

.anyRequest()
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(authenticationProvider)
//添加jwt 登录授权过滤器
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.logout()
.logoutUrl("/api/v1/auth/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext())

;
//添加自定义未授权和未登录结果返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthorizationEntryPoint);

return http.build();
}
}

OpenApi 配置文件


OpenApi 依赖


<dependency>
<groupId>org.springdocgroupId>
<artifactId>springdoc-openapi-starter-webmvc-uiartifactId>
<version>2.1.0version>
dependency>

OpenApiConfig 配置


OpenApi3 生成接口文档,主要配置如下



  • Api Gr0up(分组)

  • Bearer Authorization(认证)

  • Customer(自定义请求头等)


@Configuration
public class OpenApiConfig {

@Bean
public OpenAPI customOpenAPI(){
return new OpenAPI()
.info(info())
.externalDocs(externalDocs())
.components(components())
.addSecurityItem(securityRequirement())
;
}

private Info info(){
return new Info()
.title("京茶吉鹿的 Demo")
.version("v0.0.1")
.description("Spring Boot 3 + Spring Security + JWT + OpenAPI3")
.license(new License()
.name("Apache 2.0") // The Apache License, Version 2.0
.url("https://www.apache.org/licenses/LICENSE-2.0.html"))
.contact(new Contact()
.name("京茶吉鹿")
.url("http://localost:8417")
.email("jc.top@qq.com"))
.termsOfService("http://localhost:8417")
;
}

private ExternalDocumentation externalDocs() {
return new ExternalDocumentation()
.description("京茶吉鹿的开放文档")
.url("http://localhost:8417/docs");
}

private Components components(){
return new Components()
.addSecuritySchemes("Bearer Authorization",
new SecurityScheme()
.name("Bearer 认证")
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
)
.addSecuritySchemes("Basic Authorization",
new SecurityScheme()
.name("Basic 认证")
.type(SecurityScheme.Type.HTTP)
.scheme("basic")
)
;

}

private SecurityRequirement securityRequirement() {
return new SecurityRequirement()
.addList("Bearer Authorization");
}

private List security(Components components) {
return components.getSecuritySchemes()
.keySet()
.stream()
.map(k -> new SecurityRequirement().addList(k))
.collect(Collectors.toList());
}


/**
* 通用接口
*
@return
*/

@Bean
public Gr0upedOpenApi publicApi(){
return Gr0upedOpenApi.builder()
.group("身份认证")
.pathsToMatch("/api/v1/auth/**")
// 为指定组设置请求头
// .addOperationCustomizer(operationCustomizer())
.build();
}

/**
* 一线人员
*
@return
*/

@Bean
public Gr0upedOpenApi chaserApi(){
return Gr0upedOpenApi.builder()
.group("一线人员")
.pathsToMatch("/api/v1/chaser/**",
"/api/v1/experience/search/**",
"/api/v1/log/**",
"/api/v1/contact/**",
"/api/v1/admin/user/update")
.pathsToExclude("/api/v1/experience/search/id")
.build();
}

/**
* 部门主管
*
@return
*/

@Bean
public Gr0upedOpenApi supervisorApi(){
return Gr0upedOpenApi.builder()
.group("部门主管")
.pathsToMatch("/api/v1/supervisor/**",
"/api/v1/experience/**",
"/api/v1/schedule/**",
"/api/v1/contact/**",
"/api/v1/admin/user/update")
.build();
}

/**
* 系统管理员
*
@return
*/

@Bean
public Gr0upedOpenApi adminApi(){
return Gr0upedOpenApi.builder()
.group("系统管理员")
.pathsToMatch("/api/v1/admin/**")
// .addOpenApiCustomiser(openApi -> openApi.info(new Info().title("京茶吉鹿接口—Admin")))
.build();
}
}

image-20230603224928028


Security 接口赋权的方式


hasRole及hasAuthority的区别?



hasAuthority能通过的身份必须与字符串一模一样,而hasRole能通过的身前缀必须带有ROLE_,同时可以通过两种字符串,一是带有前缀ROLE_,二是不带前缀ROLE_



通过配置文件


在配置文件中指明访问路径的权限


.requestMatchers("/api/v1/supervisor/**").hasAnyRole(SUPERVISOR.name(), ADMIN.name())
.requestMatchers(GET, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_READ.name(), ADMIN_READ.name())
.requestMatchers(POST, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_CREATE.name(), ADMIN_CREATE.name())
.requestMatchers(PUT, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_UPDATE.name(), ADMIN_UPDATE.name())
.requestMatchers(DELETE, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_DELETE.name(), ADMIN_DELETE.name())


.requestMatchers("/api/v1/chaser/**").hasRole(CHASER.name())
.requestMatchers(GET, "/api/v1/chaser/**").hasAuthority(CHASER_READ.name())
.requestMatchers(POST, "/api/v1/chaser/**").hasAuthority(CHASER_CREATE.name())
.requestMatchers(PUT, "/api/v1/chaser/**").hasAuthority(CHASER_UPDATE.name())
.requestMatchers(DELETE, "/api/v1/chaser/**").hasAuthority(CHASER_DELETE.name())

通过注解


@RestController
@RequestMapping("/api/v1/admin")
@PreAuthorize("hasRole('ADMIN')")
@Tag(name = "系统管理员权限测试")
public class AdminController {

@GetMapping
@PreAuthorize("hasAuthority('admin:read')")
public String get() {
return "GET |==| AdminController";
}


@PostMapping
@PreAuthorize("hasAuthority('admin:create')")
public String post() {
return "POST |==| AdminController";
}
}

测试


我们登录认证成功后,系统会为我们返回 access_token 和 refresh_token。


image-20230604082145598




作者:京茶吉鹿
来源:juejin.cn/post/7241399184594993208
收起阅读 »

20个你不得不知道的Js async/await用法

web
20个你不得不知道的Js async/await用法 JavaScript的async和await关键词是现代JavaScript异步编程的核心。它们让异步代码看起来和同步代码几乎一样,使得异步编程变得更加直观和易于管理。本文介绍20个关于async/awai...
继续阅读 »

20个你不得不知道的Js async/await用法


JavaScript的asyncawait关键词是现代JavaScript异步编程的核心。它们让异步代码看起来和同步代码几乎一样,使得异步编程变得更加直观和易于管理。本文介绍20个关于async/await的实用技巧,将大大提升编程效率和代码的清晰度。


1. 基础用法


async函数返回一个Promise,而await关键词可以暂停async函数的执行,等待Promise解决。


async function fetchData() {
let data = await fetch('url');
data = await data.json();
return data;
}

2. 错误处理


使用try...catch结构处理async/await中的错误。


async function fetchData() {
try {
let response = await fetch('url');
response = await response.json();
return response;
} catch (error) {
console.error('Fetching data error:', error);
}
}

3. 并行执行


Promise.all()可以用来并行执行多个await操作。


async function fetchMultipleUrls(urls) {
const promises = urls.map(url => fetch(url).then(r => r.json()));
return await Promise.all(promises);
}

4. 条件异步


根据条件执行await


async function fetchData(condition) {
if (condition) {
return await fetch('url');
}
return 'No fetch needed';
}

5. 循环中的await


在循环中使用await时,每次迭代都会等待。


async function sequentialStart(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.json());
}
}

6. 异步迭代器


对于异步迭代器(例如Node.js中的Streams),可以使用for-await-of循环。


async function processStream(stream) {
for await (const chunk of stream) {
console.log(chunk);
}
}

7. await之后立即解构


直接在await表达式后使用解构。


async function getUser() {
const { data: user } = await fetch('user-url').then(r => r.json());
return user;
}

8. 使用默认参数避免无效的await


如果await可能是不必要的,可以使用默认参数避免等待。


async function fetchData(url = 'default-url') {
const response = await fetch(url);
return response.json();
}

9. await在类的方法中


在类的方法中使用async/await


class DataFetcher {
async getData() {
const data = await fetch('url').then(r => r.json());
return data;
}
}

10. 立刻执行的async箭头函数


可以立即执行的async箭头函数。


(async () => {
const data = await fetch('url').then(r => r.json());
console.log(data);
})();

11. 使用async/await进行延时


利用async/await实现延时。


function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

async function delayedLog(item) {
await delay(1000);
console.log(item);
}

12. 使用async/await处理事件监听器


在事件处理函数中使用async/await


document.getElementById('button').addEventListener('click', async (event) => {
event.preventDefault();
const data = await fetch('url').then(r => r.json());
console.log(data);
});

13. 以顺序方式处理数组


使用async/await以确定的顺序处理数组。


async function processArray(array) {
for (const item of array) {
await delayedLog(item);
}
console.log('Done!');
}

14. 组合async/awaitdestructuring以及spread运算符


结合使用async/await,解构和展开操作符。


async function getConfig() {
const { data, ...rest } = await fetch('config-url').then(r => r.json());
return { config: data, ...rest };
}

15. 在对象方法中使用async/await


async方法作为对象的属性。


const dataRetriever = {
async fetchData() {
return await fetch('url').then(r => r.json());
}
};

16. 异步生成器函数


使用async生成器函数结合yield


async function* asyncGenerator(array) {
for (const item of array) {
yield await processItem(item);
}
}

17. 使用顶级await


在模块顶层使用await(需要特定的JavaScript环境支持)。


// ECMAScript 2020引入顶级await特性, 部署时注意兼容性
const config = await fetch('config-url').then(r => r.json());

18. async/await与IIFE结合


async函数与立即执行函数表达式(IIFE)结合。


(async function() {
const data = await fetch('url').then(r => r.json());
console.log(data);
})();

19. 使用async/await优化递归调用


优化递归函数。


async function asyncRecursiveFunction(items) {
if (items.length === 0) return 'done';
const currentItem = items.shift();
await delay(1000);
console.log(currentItem);
return asyncRecursiveFunction(items);
}

20. 在switch语句中使用await


switch语句的每个case中使用await


async function fetchDataBasedOnType(type) {
switch (type) {
case 'user':
return await fetch('user-url').then(r => r.json());
case 'post':
return await fetch('post-url').then(r => r.json());
default:
throw new Error('Unknown type');
}
}

作者:慕仲卿
来源:juejin.cn/post/7311603506222956559
收起阅读 »

往事-迷茫-如何抉择?

〇、往事 起初 我11年网络技术专科毕业,直到15年春节后打算开始程序员生涯,那年我25岁。那时候我只会很浅的语言基础,C、java、C#、php 都会一些,由于没有开发经验,在苏州15天,后辗转来到上海,又是5天。当初懵懵懂懂,并没有什么理想和目标,只是知道...
继续阅读 »

〇、往事


起初


我11年网络技术专科毕业,直到15年春节后打算开始程序员生涯,那年我25岁。那时候我只会很浅的语言基础,C、java、C#、php 都会一些,由于没有开发经验,在苏州15天,后辗转来到上海,又是5天。当初懵懵懂懂,并没有什么理想和目标,只是知道程序员会有一份不错的收入,仅此而已,我和众多小白一样对使用什么语言难以抉择,我跑招聘会、也投简历,无论是什么公司、什么语言,只要有人要我,我就去。


我的第一份工作


辗转来到上海的第5天,我获得了第一份开发工作,这份工作的面试,现在回想起来还是觉得不可思议,一位不知道是什么职位的A让我把页面上的一个按钮调整位置、颜色?我记不清了,只记得我好像没有弄成功,已经不记得是怎么获得的工作了。我只记得入职的第二天,公司就让我去了另外一个办公地点工作,开始打杂,都算不上搬砖,因为我能力有限……


直到很久以后,我才知道,我做的工作叫外包,我算是被卖了,起初的半年都是不缴社保的,付现金,我拿6k,公司拿9k,公司净赚3k。好吧,我没有怨天尤人,因为那时候在老家我只能拿2k,而且就凭我快半年才知道自己 “被卖” 这个事实,说明我是多笨,也许我也只配做外包,补充一句,我们做的是 .NET, asp.net。又过了半年,我稍微有些入门了,似乎也聪明一点了,知道不能再混下去,于是辞职找下家……


后来听说,这个我参与的外包项目黄了……
在后来不知道多久,曾经的公司倒闭了……
几年后我还接到银行的电话,问我这个公司的一个负责人我是否有联系,或许是追债的吧,和我又有什么关系呢?


一份不错的工作


真的不推荐新人去外包公司,因为没有人引路,都是自己摸索,如果可以的话,去个有人带的公司,或大或小,或多或少都是会有帮助的。


又是一个春节后,我开始寻找我的第二份工作,老家说七不出八不归,于是我初六就出门了,和前一年一样……
没过几天,我寻找到的第二份工作,并且还是一份甲方工作,从事汽车检测行业,IT部开发的主要是自用的产品,也是一份 .NET 开发工作。这份工作给我提供了不错的工作环境、好的同事或者也算不错的老板。2年时间,我慢慢学习,逐渐进步,可能从这之后我才算是一个合格一点的程序员。年初3月,老板找全体IT部门谈话:公司需要的产品都已经齐全,现在IT部门,软件方面也只需要一些小的功能调整,愿意留在公司的就留下来但是工资不会涨,不愿意留下来的你们就去找工作,找到下家你们随时可以离开。


你们看,我是不是说的没错,算是不错的老板吧。而且这份工作包午餐,加班有加班调休呢!


这两年我是逐渐成熟的两年,除了同事间的活动、聚餐外,我偶尔会去逛逛花鸟市场,我记得很多的日子我都是学习到10点多,对着我的笔记本,我已经不记得当初学的什么,怎么学习的,应该是没有系统的学习路线,也没人指引,这是我现在回头看的,这也导致了我现在的技术面广但是颇为杂乱,我的技术能力没有重心、飘飘的……


新入行的弟弟们,选好方向很重要,现在网上很多资源的,不能盲目学习不关联的技术栈,系统性的学习很重要,最好先有计划,按照计划路线学习。


三战


第三份工作,也就是我现在的工作,18年到现在5年半多了,其实原先没有准备在这份工作上停留这么久的,恰逢19年底-23年初的疫情,23年初我手头上又有一个不太能放下的项目,所以一直拖到现在。


找工作很难擦亮自己的眼睛,招聘的同事说的天花乱坠,可能与实际不符。进入公司后才发现,缴税是按最低的来,没有奖金,没有福利,公司不推荐、不鼓励加班(所以加班没有调休、没有加班费,工作做不完是你自己的问题),公司项目技术栈陈旧,面试说“造大炮”,进公司才发现是“做鞭炮”,而且刚进公司都算是大佬级别了,可见公司人员技术凋零,我很难想象这么些同事是怎么进入公司的。


我为什么这么说,因为现在的公司,我投过一次简历,来面试过一次没有通过,后来不小心又投了一次简历,人事应该没有注意,我又来了一次,到门口发现是这家,于是打电话,接电话的人说:既然来了就再聊聊。换了一个面试的经理,也是这样,我在 “二面” 后成功入职。


那时候我自认为我的能力介于.NET初级开发和中级开发之间,然后发现这样的水平能胜过公司的9成……,是的,我一点都不谦虚,因为项目质量太差。


5年,我有多次要离开的意图,先是疫情和大环境的原因束缚着我,我妈也劝我过了疫情再说。23年初,我负责了一个比较大的项目,也是一个大的客户,我觉得在做项目上这是一个成长,虽然现在我也不知道到底有没有长长那么1cm。我却因此放弃了一份即将拿到手的外企offer,说不后悔是假的。


快35岁了,我现在税前薪资 20k,作为一个快9年的老程序员了,这着实太低了。怪我这份工作入职工资就低,怪公司这些年涨幅太少,怪疫情耽误了我的跳槽和发展,怪自己年龄大了没有太多学习成长的精力。曾经风华正茂,怀着一个在上海发展,凭借一己之力留在上海的梦想,近乎破灭了。


如果一份工作配不上你的成长脚步,不要犹豫,换掉它。(如果薪资足够动人,当我没说)


番外


这些年里:



  1. 我学了网教,成功拿到了上交大的继续教育本科学历学位证书

  2. 我找中介,写各种项目技术文档,评了中级职称
    这算是这些年能拿出来的获得了


一、迷茫


一鼓作气,再而衰,三而截吗?


从成为程序员开始,我学过很多有关无关的技能,工作中一直算是半个全栈开发,最近两年前端稳定为了 vue,后端 .net/.net core。而.NET 在我国发展不太行,恰逢2024年春节前夕,笔者准备换工作,却难求一份前端、后端或 vue+.net 全栈开发、项目经理工作。



  • 前端我不专业,稍微欠缺了火候,我能行吗?

  • 有人说熟悉 C# 可以转游戏开发,可是我深知道语言仅仅是游戏开发以及其他开发的基础,对游戏开发一无所知的我转游戏开发稍显有些晚了。

  • 全栈开发,在上海,前端 VUE,后端 .NET 的工作屈指可数,又怎么有机会呢。

  • .NET 也少到快灭绝


下面是我从某照片网站关键词职位截图:


image.png

image.png

image.png

image.png

image.png

image.png


C#/.NET的职位真的有那么少吗?再排除名企和低薪工作,这不是快绝技(绝迹,爵迹)吗?
我没法和掘金大佬们相比,名企的职位我看都不带看的,看我多潇洒,可惜内心灰暗无比,我只是有自知之明!


我学过 Node.js、Python、java 几乎没有运用到实际项目中,我现在转 java、python 好像有些迟了吧?我相信一定会有人要的,但是薪资比 20k 低了多少我说不准,而且现在我是期望涨薪,低了能过得了自己心里的门槛吗?


如果是再整整 Node.js 开源项目,直接找 VUE+Node.js 会不会更好呢?毕竟都是 js 栈的东西,没什么门槛,深入学一个 Express.js 是不是能给自己机会呢?


我没有能力,没啥人脉,找不到私活做,我也没有 Plan B,有的都是迷茫和无奈。


二、抉择


我要如何抉择?



  1. 继续 VUE+.NET 的全栈模式,等到找到下份工作为止

  2. 深入学习 Node.js,转 VUE+Node.js 全栈模式,寻求新计划

  3. 深入学习 Python 或 Java,改变方向,置之死地而后生

  4. 转项目经理,我负责过不少项目,但是没有系统的学过项目管理,没有 PMP 证书,也不知道项目经理工作是否好找,另外,项目经理很多也是需要有对应的技术栈要求的,算是技术型项目经理吧??

  5. 换行业……没有相关工作经验谁要呢?或者把自己洗白重来?

  6. 创业……能力够吗,而且启动金从何而来?


呵呵,废物如我,活该碌碌无为,已近半生……

年龄到了,这可能也是我最后一次换工作了,或许我应该去学个 “水电工证书” 呢!


作者:程序园丁
来源:juejin.cn/post/7330442541439336448
收起阅读 »

一个巧妙的分库分表设计:异构索引表

前言 最近计划参与一个换书活动,翻到《企业IT架构转型之道阿里巴巴中台战略思想与架构实战》这本书时,回想起令我印象比较深刻的一个知识点:“异构索引表”,所以在此记录并分享,和大家共同学习交流。 异构索引表的作用 如果《一致性哈希在分库分表的应用》说的是分库分表...
继续阅读 »

前言


最近计划参与一个换书活动,翻到《企业IT架构转型之道阿里巴巴中台战略思想与架构实战》这本书时,回想起令我印象比较深刻的一个知识点:“异构索引表”,所以在此记录并分享,和大家共同学习交流。


异构索引表的作用


如果《一致性哈希在分库分表的应用》说的是分库分表的方法和策略,那么本文所探讨的“异构索引表”,则是在实施分库分表过程中一个非常巧妙的设计,可以有效的解决分库分表的查询问题。


分库分表的查询问题


问题说明


在哈希分库分表时,为了避免分布不均匀造成的“数据倾斜”,通常会选择一些数据唯一的字段进行哈希操作,比如ID。


以订单表为例,通常有(id、uid、status、amount)等字段,通过id进行哈希取模运算分库分表之后,效果如下图


哈希分库分表效果


这样分库分表的方法没有问题,但是,在后期的开发和维护过程中,可能会存在潜在的问题。


举个例子:现在要查询uid为1的记录,应该去哪个表或库去查询?


对于用户来讲,这个场景可以说是非常频繁的。


这个时候就会发现,要想查询uid为1的记录,只能去所有的库或分表上进行查询,也就是所谓的“广播查询”。


整个查询过程大概是这样的


分库分表查询


性能问题


显然,整个查询过程需要进行全库扫描,涉及到多次的网络数据传输,一定会导致查询速度的降低和延迟的增加


数据聚合问题


另外,当这个用户有成千上万条数据时,不得已要在一个节点进行排序、分页、聚合等计算操作,需要消耗大量的计算资源和内存空间。对系统造成的负担也会影响查询性能。


这是一个非常典型的“事务边界大”的案例,即“一条SQL到所有的数据库去执行”。



那么如何解决这一痛点?



解决分库分表的查询问题


本文重点:“异构索引表”是可以解决这个问题的。


引入异构索引表


简单来说,“异构索引表”是一个拿空间换时间的设计。具体如下:


添加订单数据时,除了根据订单ID进行哈希取模运算将订单数据维护到对应的表中,还要对uid进行哈希取模运算,将uid和订单id维护在另一张表中,如图所示。


异构索引表


引入“异构索引表”后,因为同一个uid经过哈希取模运算后得到的结果是一致的,所以,该uid所有的订单id也一定会被分布到同一张user_order表中。


当查询uid为1的订单记录时,就可以有效地解决数据聚合存在的计算资源消耗全库扫描的低效问题了。


接下来,通过查询过程,看看这两个问题是怎么解决的。


引入后的查询过程


引入“异构索引表”后,查询uid为1的订单记录时,具体过程分为以下几步:



  1. 应用向中间件发送select * from order where uid = 1,请求查询uid为1的订单记录。

  2. 中间件根据uid路由到“异构索引表”:user_order,获得该uid相关的订单ID列表(排序、分页可以在此sql操作)。

  3. 中间件根据返回的订单ID,再次准确路由到对应的订单表:order

  4. 中间件将分散的订单数据进行聚合返回给应用。


引入异构索引表查询


看上去引入“异构索引表”之后,多了一个查询步骤,但换来的是:



  1. 根据订单ID准确路由到订单表,避免了全库扫描。

  2. user_order表进行了排序、分页等操作,避免大量数据回到中间件去计算。


异构索引表解决不了的场景


“异构索引表”只适合简单的分库分表查询场景,如果存在复杂的查询场景,还是需要借助搜索引擎来实现。


总结


异构索引表作为一种巧妙的设计,避免了分库分表查询存在的两个问题:全库扫描不必要的计算资源消耗


但是,异构索引表并不适用所有场景,对于复杂的查询场景可能需要结合其他技术或策略来解决问题。


作者:王二蛋呀
来源:juejin.cn/post/7372070947820109851
收起阅读 »

表设计的18条军规

前言 对于后端开发同学来说,访问数据库,是代码中必不可少的一个环节。 系统中收集到用户的核心数据,为了安全性,我们一般会存储到数据库,比如:mysql,oracle等。 后端开发的日常工作,需要不断的建库和建表,来满足业务需求。 通常情况下,建库的频率比建表要...
继续阅读 »

前言


对于后端开发同学来说,访问数据库,是代码中必不可少的一个环节。


系统中收集到用户的核心数据,为了安全性,我们一般会存储到数据库,比如:mysql,oracle等。


后端开发的日常工作,需要不断的建库和建表,来满足业务需求。


通常情况下,建库的频率比建表要低很多,所以,我们这篇文章主要讨论建表相关的内容。


如果我们在建表的时候不注意细节,等后面系统上线之后,表的维护成本变得非常高,而且很容易踩坑。


今天就跟大家一起聊聊,数据库建表的18个小技巧。


文章中介绍的很多细节,我在工作中踩过坑,并且实践过的,非常有借鉴意义,希望对你会有所帮助。


图片


1.名字


建表的时候,给字段索引起个好名字,真的太重要了。


1.1 见名知意


名字就像字段索引的一张脸,可以给人留下第一印象。


好的名字,言简意赅,见名知意,让人心情愉悦,能够提高沟通和维护成本。


坏的名字,模拟两可,不知所云。而且显得杂乱无章,看得让人抓狂。


反例:


用户名称字段定义成:yong_hu_ming、用户_name、name、user_name_123456789

你看了可能会一脸懵逼,这是什么骚操作?


正例:


用户名称字段定义成:user_name


温馨提醒一下,名字也不宜过长,尽量控制在30个字符以内。



1.2 大小写


名字尽量都用小写字母,因为从视觉上,小写字母更容易让人读懂。


反例:


字段名:PRODUCT_NAME、PRODUCT_name

全部大写,看起来有点不太直观。而一部分大写,一部分小写,让人看着更不爽。


正例:


字段名:product_name

名字还是使用全小写字母,看着更舒服。


1.3 分隔符


很多时候,名字为了让人好理解,有可能会包含多个单词。


那么,多个单词间的分隔符该用什么呢?


反例:


字段名:productname、productName、product name、product@name

单词间没有分隔,或者单词间用驼峰标识,或者单词间用空格分隔,或者单词间用@分隔,这几种方式都不太建议。


正例:


字段名:product_name

强烈建议大家在单词间用_分隔。


1.4 表名


对于表名,在言简意赅,见名知意的基础之上,建议带上业务前缀


如果是订单相关的业务表,可以在表名前面加个前缀:order_


例如:order_pay、order_pay_detail等。


如果是商品相关的业务表,可以在表名前面加个前缀:product_


例如:product_spu,product_sku等。


这样做的好处是为了方便归类,把相同业务的表,可以非常快速的聚集到一起。


另外,还有有个好处是,如果哪天有非订单的业务,比如:金融业务,也需要建一个名字叫做pay的表,可以取名:finance_pay,就能非常轻松的区分。


这样就不会出现同名表的情况。


1.5 字段名称


字段名称是开发人员发挥空间最大,但也最容易发生混乱的地方。


比如有些表,使用flag表示状态,另外的表用status表示状态。


可以统一一下,使用status表示状态。


如果一个表使用了另一个表的主键,可以在另一张表的名后面,加_id_sys_no,例如:


在product_sku表中有个字段,是product_spu表的主键,这时候可以取名:product_spu_id或product_spu_sys_no。


还有创建时间,可以统一成:create_time,修改时间统一成:update_time。


删除状态固定为:delete_status。


其实还有很多公共字段,在不同的表之间,可以使用全局统一的命名规则,定义成相同的名称,以便于大家好理解。


1.6 索引名


在数据库中,索引有很多种,包括:主键、普通索引、唯一索引、联合索引等。


每张表的主键只有一个,一般使用:id或者sys_no命名。


普通索引和联合索引,其实是一类。在建立该类索引时,可以加ix_前缀,比如:ix_product_status。


唯一索引,可以加ux_前缀,比如:ux_product_code。


2.字段类型


在设计表时,我们在选择字段类型时,可发挥空间很大。


时间格式的数据有:date、datetime和timestamp等等可以选择。


字符类型的数据有:varchar、char、text等可以选择。


数字类型的数据有:int、bigint、smallint、tinyint等可以选择。


说实话,选择很多,有时候是一件好事,也可能是一件坏事。


如何选择一个合适的字段类型,变成了我们不得不面对的问题。


如果字段类型选大了,比如:原本只有1-10之间的10个数字,结果选了bigint,它占8个字节。


其实,1-10之间的10个数字,每个数字1个字节就能保存,选择tinyint更为合适。


这样会白白浪费7个字节的空间。


如果字段类型择小了,比如:一个18位的id字段,选择了int类型,最终数据会保存失败。


所以选择一个合适的字段类型,还是非常重要的一件事情。


以下原则可以参考一下:



  1. 尽可能选择占用存储空间小的字段类型,在满足正常业务需求的情况下,从小到大,往上选。

  2. 如果字符串长度固定,或者差别不大,可以选择char类型。如果字符串长度差别较大,可以选择varchar类型。

  3. 是否字段,可以选择bit类型。

  4. 枚举字段,可以选择tinyint类型。

  5. 主键字段,可以选择bigint类型。

  6. 金额字段,可以选择decimal类型。

  7. 时间字段,可以选择timestamp或datetime类型。


3.字段长度


前面我们已经定义好了字段名称,选择了合适的字段类型,接下来,需要重点关注的是字段长度了。


比如:varchar(20),biginit(20)等。


那么问题来了,varchar代表的是字节长度,还是字符长度呢?


答:在mysql中除了varcharchar是代表字符长度之外,其余的类型都是代表字节长度。


biginit(n) 这个n表示什么意思呢?


假如我们定义的字段类型和长度是:bigint(4),bigint实际长度是8个字节。


现在有个数据a=1,a显示4个字节,所以在不满4个字节时前面填充0(前提是该字段设置了zerofill属性),比如:0001。


当满了4个字节时,比如现在数据是a=123456,它会按照实际的长度显示,比如:123456。


但需要注意的是,有些mysql客户端即使满了4个字节,也可能只显示4个字节的内容,比如会显示成:1234。


所以bigint(4),这里的4表示显示的长度为4个字节,实际长度还是占8个字节。


4.字段个数


我们在建表的时候,一定要对字段个数做一些限制。


我之前见过有人创建的表,有几十个,甚至上百个字段,表中保存的数据非常大,查询效率很低。


如果真有这种情况,可以将一张大表拆成多张小表,这几张表的主键相同。


建议每表的字段个数,不要超过20个。


5. 主键


在创建表时,一定要创建主键


因为主键自带了主键索引,相比于其他索引,主键索引的查询效率最高,因为它不需要回表。


此外,主键还是天然的唯一索引,可以根据它来判重。


单个数据库中,主键可以通过AUTO_INCREMENT,设置成自动增长的。


但在分布式数据库中,特别是做了分库分表的业务库中,主键最好由外部算法(比如:雪花算法)生成,它能够保证生成的id是全局唯一的。


除此之外,主键建议保存跟业务无关的值,减少业务耦合性,方便今后的扩展。


不过我也见过,有些一对一的表关系,比如:用户表和用户扩展表,在保存数据时是一对一的关系。


这样,用户扩展表的主键,可以直接保存用户表的主键。


6.存储引擎


mysql8以前的版本,默认的存储引擎是myisam,而mysql8以后的版本,默认的存储引擎变成了innodb


之前我们还在创建表时,还一直纠结要选哪种存储引擎?


myisam的索引和数据分开存储,而有利于查询,但它不支持事务和外键等功能。


innodb虽说查询性能,稍微弱一点,但它支持事务和外键等,功能更强大一些。


以前的建议是:读多写少的表,用myisam存储引擎。而写多读多的表,用innodb。


但虽说mysql对innodb存储引擎性能的不断优化,现在myisam和innodb查询性能相差已经越来越小。


所以,建议我们在使用mysql8以后的版本时,直接使用默认的innodb存储引擎即可,无需额外修改存储引擎。


7. NOT NULL


在创建字段时,需要选择该字段是否允许为NULL


我们在定义字段时,应该尽可能明确该字段NOT NULL


为什么呢?


我们主要以innodb存储引擎为例,myisam存储引擎没啥好说的。


主要有以下原因:



  1. 在innodb中,需要额外的空间存储null值,需要占用更多的空间。

  2. null值可能会导致索引失效。

  3. null值只能用is null或者is not null判断,用=号判断永远返回false。


因此,建议我们在定义字段时,能定义成NOT NULL,就定义成NOT NULL。


但如果某个字段直接定义成NOT NULL,万一有些地方忘了给该字段写值,就会insert不了数据。


这也算合理的情况。


但有一种情况是,系统有新功能上线,新增了字段。上线时一般会先执行sql脚本,再部署代码。


由于老代码中,不会给新字段赋值,则insert数据时,也会报错。


由此,非常有必要给NOT NULL的字段设置默认值,特别是后面新增的字段。


例如:


alter table product_sku add column  brand_id int(10not null default 0;

8.外键


在mysql中,是存在外键的。


外键存在的主要作用是:保证数据的一致性完整性


例如:


create table class (
  id int(10primary key auto_increment,
  cname varchar(15)
);

有个班级表class。


然后有个student表:


create table student(
  id int(10primary key auto_increment,
  name varchar(15not null,
  gender varchar(10not null,
  cid int,
  foreign key(cid) references class(id)
);

其中student表中的cid字段,保存的class表的id,这时通过foreign key增加了一个外键。


这时,如果你直接通过student表的id删除数据,会报异常:


a foreign key constraint fails

必须要先删除class表对于的cid那条数据,再删除student表的数据才行,这样能够保证数据的一致性和完整性。



顺便说一句:只有存储引擎是innodb时,才能使用外键。



如果只有两张表的关联还好,但如果有十几张表都建了外键关联,每删除一次主表,都需要同步删除十几张子表,很显然性能会非常差。


因此,互联网系统中,一般建议不使用外键。因为这类系统更多的是为了性能考虑,宁可牺牲一点数据一致性和完整性。


除了外键之外,存储过程触发器也不太建议使用,他们都会影响性能。


9. 索引


在建表时,除了指定主键索引之外,还需要创建一些普通索引


例如:


create table product_sku(
  id int(10primary key auto_increment,
  spu_id int(10not null,
  brand_id int(10not null,
  name varchar(15not null
);

在创建商品表时,使用spu_id(商品组表)和brand_id(品牌表)的id。


像这类保存其他表id的情况,可以增加普通索引:


create table product_sku (
  id int(10primary key auto_increment,
  spu_id int(10not null,
  brand_id int(10not null,
  name varchar(15not null,
  KEY `ix_spu_id` (`spu_id`USING BTREE,
  KEY `ix_brand_id` (`brand_id`USING BTREE
);

后面查表的时候,效率更高。


但索引字段也不能建的太多,可能会影响保存数据的效率,因为索引需要额外的存储空间。


建议单表的索引个数不要超过:5个。


如果在建表时,发现索引个数超过5个了,可以删除部分普通索引,改成联合索引


顺便说一句:在创建联合索引的时候,需要使用注意最左匹配原则,不然,建的联合索引效率可能不高。


对于数据重复率非常高的字段,比如:状态,不建议单独创建普通索引。因为即使加了索引,如果mysql发现全表扫描效率更高,可能会导致索引失效。


如果你对索引失效问题比较感兴趣,可以看看我的另一篇文章《聊聊索引失效的10种场景,太坑了》,里面有非常详细的介绍。


10.时间字段


时间字段的类型,我们可以选择的范围还是比较多的,目前mysql支持:date、datetime、timestamp、varchar等。


varchar类型可能是为了跟接口保持一致,接口中的时间类型是String。


但如果哪天我们要通过时间范围查询数据,效率会非常低,因为这种情况没法走索引。


date类型主要是为了保存日期,比如:2020-08-20,不适合保存日期和时间,比如:2020-08-20 12:12:20。


datetimetimestamp类型更适合我们保存日期和时间


但它们有略微区别。



  • timestamp:用4个字节来保存数据,它的取值范围为1970-01-01 00:00:01 UTC ~ 2038-01-19 03:14:07。此外,它还跟时区有关。

  • datetime:用8个字节来保存数据,它的取值范围为1000-01-01 00:00:00 ~ 9999-12-31 23:59:59。它跟时区无关。


优先推荐使用datetime类型保存日期和时间,可以保存的时间范围更大一些。



温馨提醒一下,在给时间字段设置默认值是,建议不要设置成:0000-00-00 00:00:00,不然查询表时可能会因为转换不了,而直接报错。



11.金额字段


mysql中有多个字段可以表示浮点数:float、double、decimal等。


floatdouble可能会丢失精度,因此推荐大家使用decimal类型保存金额。


一般我们是这样定义浮点数的:decimal(m,n)。


其中n是指小数的长度,而m是指整数加小数的总长度。


假如我们定义的金额类型是这样的:decimal(10,2),则表示整数长度是8位,并且保留2位小数。


12. json字段


我们在设计表结构时,经常会遇到某个字段保存的数据值不固定的需求。


举个例子,比如:做异步excel导出功能时,需要在异步任务表中加一个字段,保存用户通过前端页面选择的查询条件,每个用户的查询条件可能都不一样。


这种业务场景,使用传统的数据库字段,不太好实现。


这时候就可以使用MySQL的json字段类型了,可以保存json格式的结构化数据。


保存和查询数据都是非常方便的。


MySQL还支持按字段名称或者字段值,查询json中的数据。


13.唯一索引


唯一索引在我们实际工作中,使用频率相当高。


你可以给单个字段,加唯一索引,比如:组织机构code。


也可以给多个字段,加一个联合的唯一索引,比如:分类编号、单位、规格等。


单个的唯一索引还好,但如果是联合的唯一索引,字段值出现null时,则唯一性约束可能会失效。


关于唯一索引失效的问题,感兴趣的小伙伴可以看看我的另一篇文章《明明加了唯一索引,为什么还是产生重复数据?》。



创建唯一索引时,相关字段一定不能包含null值,否则唯一性会失效。



14.字符集


mysql中支持的字符集有很多,常用的有:latin1、utf-8、utf8mb4、GBK等。


这4种字符集情况如下:图片


latin1容易出现乱码问题,在实际项目中使用比较少。


GBK支持中文,但不支持国际通用字符,在实际项目中使用也不多。


从目前来看,mysql的字符集使用最多的还是:utf-8utf8mb4


其中utf-8占用3个字节,比utf8mb4的4个字节,占用更小的存储空间。


但utf-8有个问题:即无法存储emoji表情,因为emoji表情一般需要4个字节。


由此,使用utf-8字符集,保存emoji表情时,数据库会直接报错。


所以,建议在建表时字符集设置成:utf8mb4,会省去很多不必要的麻烦。


15. 排序规则


不知道,你关注过没,在mysql中创建表时,有个COLLATE参数可以设置。


例如:


CREATE TABLE `order` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `code` varchar(20COLLATE utf8mb4_bin NOT NULL,
  `name` varchar(30COLLATE utf8mb4_bin NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `un_code` (`code`),
  KEY `un_code_name` (`code`,`name`USING BTREE,
  KEY `idx_name` (`name`)
ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin

它是用来设置排序规则的。


字符排序规则跟字符集有关,比如:字符集如果是utf8mb4,则字符排序规则也是以:utf8mb4_开头的,常用的有:utf8mb4_general_ciutf8mb4_bin等。


其中utf8mb4_general_ci排序规则,对字母的大小写不敏感。说得更直白一点,就是不区分大小写。


而utf8mb4_bin排序规则,对字符大小写敏感,也就是区分大小写。


说实话,这一点还是非常重要的。


假如order表中现在有一条记录,name的值是大写的YOYO,但我们用小写的yoyo去查,例如:


select * from order where name='yoyo';

如果字符排序规则是utf8mb4_general_ci,则可以查出大写的YOYO的那条数据。


如果字符排序规则是utf8mb4_bin,则查不出来。


由此,字符排序规则一定要根据实际的业务场景选择,否则容易出现问题。


最近就业形式比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。


你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。


image.png


进群方式


添加苏三的私人微信:su_san_java,备注:掘金+所在城市,即可加入。


16.大字段


我们在创建表时,对一些特殊字段,要额外关注,比如:大字段,即占用较多存储空间的字段。


比如:用户的评论,这就属于一个大字段,但这个字段可长可短。


但一般会对评论的总长度做限制,比如:最多允许输入500个字符。


如果直接定义成text类型,可能会浪费存储空间,所以建议将这类字段定义成varchar类型的存储效率更高。


当然,我还见过更大的字段,即该字段直接保存合同数据。


一个合同可能会占几Mb


在mysql中保存这种数据,从系统设计的角度来说,本身就不太合理。


像合同这种非常大的数据,可以保存到mongodb中,然后在mysql的业务表中,保存mongodb表的id。


17.冗余字段


我们在设计表的时候,为了性能考虑,提升查询速度,有时可以冗余一些字段。


举个例子,比如:订单表中一般会有userId字段,用来记录用户的唯一标识。


但很多订单的查询页面,或者订单的明细页面,除了需要显示订单信息之外,还需要显示用户ID和用户名称。


如果订单表和用户表的数据量不多,我们可以直接用userId,将这两张表join起来,查询出用户名称。


但如果订单表和用户表的数据量都非常多,这样join是比较消耗查询性能的。


这时候我们可以通过冗余字段的方案,来解决性能问题。


我们可以在订单表中,可以再加一个userName字段,在系统创建订单时,将userId和userName同时写值。


当然订单表中历史数据的userName是空的,可以刷一下历史数据。


这样调整之后,后面只需要查询订单表,即可查询出我们所需要的数据。


不过冗余字段的方案,有利也有弊。


对查询性能有利。


但需要额外的存储空间,还可能会有数据不一致的情况,比如用户名称修改了。


我们在实际业务场景中,需要综合评估,冗余字段方案不适用于所有业务场景。


18.注释


我们在做表设计的时候,一定要把表和相关字段的注释加好。


例如下面这样的:


CREATE TABLE `sys_dept` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `name` varchar(30NOT NULL COMMENT '名称',
  `pid` bigint NOT NULL COMMENT '上级部门',
  `valid_status` tinyint(1NOT NULL DEFAULT 1 COMMENT '有效状态 1:有效 0:无效',
  `create_user_id` bigint NOT NULL COMMENT '创建人ID',
  `create_user_name` varchar(30NOT NULL COMMENT '创建人名称',
  `create_time` datetime(3DEFAULT NULL COMMENT '创建日期',
  `update_user_id` bigint DEFAULT NULL COMMENT '修改人ID',
  `update_user_name` varchar(30)  DEFAULT NULL COMMENT '修改人名称',
  `update_time` datetime(3DEFAULT NULL COMMENT '修改时间',
  `is_del` tinyint(1DEFAULT '0' COMMENT '是否删除 1:已删除 0:未删除',
  PRIMARY KEY (`id`USING BTREE,
  KEY `index_pid` (`pid`USING BTREE
ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='部门';

表和字段的注释,都列举的非常详细。


特别是有些状态类型的字段,比如:valid_status字段,该字段表示有效状态, 1:有效 0:无效。


让人可以一目了然,表和字段是干什么用的,字段的值可能有哪些。


最怕的情况是,你在表中创建了很多status字段,每个字段都有1、2、3、4、5、6、7、8、9等多个值。


没有写什么注释。


谁都不知道1代表什么含义,2代表什么含义,3代表什么含义。


可能刚开始你还记得。


但系统上线使用一年半载之后,可能连你自己也忘记了这些status字段,每个值的具体含义了,埋下了一个巨坑。


由此,我们在做表设计时,一定要写好相关的注释,并且经常需要更新这些注释。




作者:苏三说技术
来源:juejin.cn/post/7352789840352690185
收起阅读 »

接口设计的18条军规

大家好,我是苏三,又跟大家见面了。 前言 之前写过一篇文章《表设计的18条军规》,发表之前,在全网广受好评。 今天延续设计的话题,给大家总结了接口设计的18条军规,希望对你会有所帮助。 1. 签名 为了防止API接口中的数据被篡改,很多时候我们需要对API接...
继续阅读 »

大家好,我是苏三,又跟大家见面了。


前言


之前写过一篇文章《表设计的18条军规》,发表之前,在全网广受好评。


今天延续设计的话题,给大家总结了接口设计的18条军规,希望对你会有所帮助。


图片


1. 签名


为了防止API接口中的数据被篡改,很多时候我们需要对API接口做签名


接口请求方将请求参数 + 时间戳 + 密钥拼接成一个字符串,然后通过md5等hash算法,生成一个前面sign。


然后在请求参数或者请求头中,增加sign参数,传递给API接口。


API接口的网关服务,获取到该sign值,然后用相同的请求参数 + 时间戳 + 密钥拼接成一个字符串,用相同的m5算法生成另外一个sign,对比两个sign值是否相等。


如果两个sign相等,则认为是有效请求,API接口的网关服务会将给请求转发给相应的业务系统。


如果两个sign不相等,则API接口的网关服务会直接返回签名错误。


问题来了:签名中为什么要加时间戳?


答:为了安全性考虑,防止同一次请求被反复利用,增加了密钥没破解的可能性,我们必须要对每次请求都设置一个合理的过期时间,比如:15分钟。


这样一次请求,在15分钟之内是有效的,超过15分钟,API接口的网关服务会返回超过有效期的异常提示。


目前生成签名中的密钥有两种形式:


一种是双方约定一个固定值privateKey。


另一种是API接口提供方给出AK/SK两个值,双方约定用SK作为签名中的密钥。AK接口调用方作为header中的accessKey传递给API接口提供方,这样API接口提供方可以根据AK获取到SK,而生成新的sgin。


2. 加密


有些时候,我们的API接口直接传递的非常重要的数据,比如:用户的登录密码、银彳亍卡号、转账金额、用户身-份-证等,如果将这些参数,直接明文,暴露到公网上是非常危险的事情。


由此,我们需要对数据进行加密


比如:用户注册接口,用户输入了用户名和密码之后,需要将密码加密。


我们可以使用AES对称加密算法。


在前端使用公钥对用户密码加密。


然后注册接口中,可以使用密钥解密,做一些业务需求校验。然后再换成其他的加密方式加密,保存到数据库当中。


3. ip白名单


为了进一步加强API接口的安全性,防止接口的签名或者加密被破解了,攻击者可以在自己的服务器上请求该接口。


需求限制请求ip,增加ip白名单


只有在白名单中的ip地址,才能成功请求API接口,否则直接返回无访问权限。


ip白名单也可以加在API网关服务上。


但也要防止公司的内部应用服务器被攻破,这种情况也可以从内部服务器上发起API接口的请求。


这时候就需要增加web防火墙了,比如:ModSecurity等。


4. 限流


如果你的API接口被第三方平台调用了,这就意味着着,调用频率是没法控制的。


第三方平台调用你的API接口时,如果并发量一下子太高,可能会导致你的API服务不可用,接口直接挂掉。


由此,必须要对API接口做限流


限流方法有三种:



  1. 对请求ip做限流:比如同一个ip,在一分钟内,对API接口总的请求次数,不能超过10000次。

  2. 对请求接口做限流:比如同一个ip,在一分钟内,对指定的API接口,请求次数不能超过2000次。

  3. 对请求用户做限流:比如同一个AK/SK用户,在一分钟内,对API接口总的请求次数,不能超过10000次。


我们在实际工作中,可以通过nginxredis或者gateway实现限流的功能。


5. 参数校验


我们需要对API接口做参数校验,比如:校验必填字段是否为空,校验字段类型,校验字段长度,校验枚举值等等。


这样做可以拦截一些无效的请求。


比如在新增数据时,字段长度超过了数据字段的最大长度,数据库会直接报错。


但这种异常的请求,我们完全可以在API接口的前期进行识别,没有必要走到数据库保存数据那一步,浪费系统资源。


有些金额字段,本来是正数,但如果用户传入了负数,万一接口没做校验,可能会导致一些没必要的损失。


还有些状态字段,如果不做校验,用户如果传入了系统中不存在的枚举值,就会导致保存的数据异常。


由此可见,做参数校验是非常有必要的。


在Java中校验数据使用最多的是hiberateValidator框架,它里面包含了@Null、@NotEmpty、@Size、@Max、@Min等注解。


用它们校验数据非常方便。


当然有些日期字段和枚举字段,可能需要通过自定义注解的方式实现参数校验。


6. 统一返回值


我之前调用过别人的API接口,正常返回数据是一种json格式,比如:


{
    "code":0,
    "message":null,
    "data":[{"id":123,"name":"abc"}]
},

签名错误返回的json格式:


{
    "code":1001,
    "message":"签名错误",
    "data":null
}

没有数据权限返回的json格式:


{
    "rt":10,
    "errorMgt":"没有权限",
    "result":null
}

这种是比较坑的做法,返回值中有多种不同格式的返回数据,这样会导致对接方很难理解。


出现这种情况,可能是API网关定义了一直返回值结构,业务系统定义了另外一种返回值结构。如果是网关异常,则返回网关定义的返回值结构,如果是业务系统异常,则返回业务系统的返回值结构。


但这样会导致API接口出现不同的异常时,返回不同的返回值结构,非常不利于接口的维护。


其实这个问题我们可以在设计API网关时解决。


业务系统在出现异常时,抛出业务异常的RuntimeException,其中有个message字段定义异常信息。


所有的API接口都必须经过API网关,API网关捕获该业务异常,然后转换成统一的异常结构返回,这样能统一返回值结构。


7. 统一封装异常


我们的API接口需要对异常进行统一处理。


不知道你有没有遇到过这种场景:有时候在API接口中,需要访问数据库,但表不存在,或者sql语句异常,就会直接把sql信息在API接口中直接返回。


返回值中包含了异常堆栈信息数据库信息错误代码和行数等信息。


如果直接把这些内容暴露给第三方平台,是很危险的事情。


有些不法分子,利用接口返回值中的这些信息,有可能会进行sql注入或者直接脱库,而对我们系统造成一定的损失。


因此非常有必要对API接口中的异常做统一处理,把异常转换成这样:


{
    "code":500,
    "message":"服务器内部错误",
    "data":null
}

返回码code500,返回信息message服务器内部异常


这样第三方平台就知道是API接口出现了内部问题,但不知道具体原因,他们可以找我们排查问题。


我们可以在内部的日志文件中,把堆栈信息、数据库信息、错误代码行数等信息,打印出来。


我们可以在gateway中对异常进行拦截,做统一封装,然后给第三方平台的是处理后没有敏感信息的错误信息。


8. 请求日志


在第三方平台请求你的API接口时,接口的请求日志非常重要,通过它可以快速的分析和定位问题。


我们需要把API接口的请求url、请求参数、请求头、请求方式、响应数据和响应时间等,记录到日志文件中。


最好有traceId,可以通过它串联整个请求的日志,过滤多余的日志。


当然有些时候,请求日志不光是你们公司开发人员需要查看,第三方平台的用户也需要能查看接口的请求日志。


这时就需要把日志落地到数据库,比如:mongodb或者elastic search,然后做一个UI页面,给第三方平台的用户开通查看权限。这样他们就能在外网查看请求日志了,他们自己也能定位一部分问题。


9. 幂等设计


第三方平台极有可能在极短的时间内,请求我们接口多次,比如:在1秒内请求两次。有可能是他们业务系统有bug,或者在做接口调用失败重试,因此我们的API接口需要做幂等设计


也就是说要支持在极短的时间内,第三方平台用相同的参数请求API接口多次,第一次请求数据库会新增数据,但第二次请求以后就不会新增数据,但也会返回成功。


这样做的目的是不会产生错误数据。


我们在日常工作中,可以通过在数据库中增加唯一索引,或者在redis保存requestId和请求参来保证接口幂等性。


对接口幂等性感兴趣的小伙伴,可以看看我的另一篇文章《高并发下如何保证接口的幂等性?》,里面有非常详细的介绍。


10. 限制记录条数


对于对我提供的批量接口,一定要限制请求的记录条数


如果请求的数据太多,很容易造成API接口超时等问题,让API接口变得不稳定。


通常情况下,建议一次请求中的参数,最多支持传入500条记录。


如果用户传入多余500条记录,则接口直接给出提示。


建议这个参数做成可配置的,并且要事先跟第三方平台协商好,避免上线后产生不必要的问题。


对于一次性查询的数据太多的情况,我们需要将接口设计成分页查询返回的。


11. 压测


上线前我们务必要对API接口做一下压力测试,知道各个接口的qps情况。


以便于我们能够更好的预估,需要部署多少服务器节点,对于API接口的稳定性至关重要。


之前虽说对API接口做了限流,但是实际上API接口是否能够达到限制的阀值,这是一个问号,如果不做压力测试,是有很大风险的。


比如:你API接口限流1秒只允许50次请求,但实际API接口只能处理30次请求,这样你的API接口也会处理不过来。


我们在工作中可以用jmeter或者apache benc对API接口做压力测试。


12. 异步处理


一般的API接口的逻辑都是同步处理的,请求完之后立刻返回结果。


但有时候,我们的API接口里面的业务逻辑非常复杂,特别是有些批量接口,如果同步处理业务,耗时会非常长。


这种情况下,为了提升API接口的性能,我们可以改成异步处理


在API接口中可以发送一条mq消息,然后直接返回成功。之后,有个专门的mq消费者去异步消费该消息,做业务逻辑处理。


直接异步处理的接口,第三方平台有两种方式获取到。


第一种方式是:我们回调第三方平台的接口,告知他们API接口的处理结果,很多支付接口就是这么玩的。


第二种方式是:第三方平台通过轮询调用我们另外一个查询状态的API接口,每隔一段时间查询一次状态,传入的参数是之前的那个API接口中的id集合。


13. 数据脱敏


有时候第三方平台调用我们API接口时,获取的数据中有一部分是敏感数据,比如:用户手机号、银彳亍卡号等等。


这样信息如果通过API接口直接保留到外网,是非常不安全的,很容易造成用户隐私数据泄露的问题。


这就需要对部分数据做数据脱敏了。


我们可以在返回的数据中,部分内容用星号代替。


已用户手机号为例:182****887


这样即使数据被泄露了,也只泄露了一部分,不法分子拿到这份数据也没啥用。


14. 完整的接口文档


说实话,一份完整的API接口文档,在双方做接口对接时,可以减少很多沟通成本,让对方少走很多弯路。


接口文档中需要包含如下信息:



  1. 接口地址

  2. 请求方式,比如:post或get

  3. 请求参数和字段介绍

  4. 返回值和字段介绍

  5. 返回码和错误信息

  6. 加密或签名示例

  7. 完整的请求demo

  8. 额外的说明,比如:开通ip白名单。


接口文档中最好能够统一接口和字段名称的命名风格,比如都用驼峰标识命名。


统一字段的类型和长度,比如:id字段用Long类型,长度规定20。status字段用int类型,长度固定2等。


统一时间格式字段,比如:time用String类型,格式为:yyyy-MM-dd HH:mm:ss。


接口文档中写明AK/SK和域名,找某某单独提供等。


最近建了一些高质量的粉丝群,里面可以交流技术,有工作内推,有粉丝福利。


进群方式


添加苏三的私人微信:su_san_java,备注:掘金+粉丝,即可加入。


15. 请求方式


接口支持的请求方式有很多,比如:GET、POST、PUT、DELETE等等。


我们在设计接口的时候,要根据实际情况选择使用哪种请求方式。


实际工作中使用最多的是:GETPOST,这两种请求方式。


如果没有输入参数的接口,可以使用GET请求方式,问题不大。


如果有输入参数的接口,推荐使用POST请求方式,坑更少。


主要原因有下面两点:



  1. POST请求方式更容易扩展参数,特别是在Fegin调用接口的场景下,比如:增加一个参数,调用方可以不用修改代码。而GET请求方式,需要修改代码,否则编译会出错。

  2. GET请求方式的参数,有长度限制,最长是5000个字符,而POST请求方式对参数的长度没有做限制,可以传入更长的参数值。


16. 请求头


对于一些公共的功能,比如:接口的权限验证,或者接口的traceId参数。


我们在设计接口的时候,不用把所有的参数,都放入接口的请求参数中。


有些参数可以放到Header请求头中。


比如:我们需求记录每个请求的traceId,不用在所有接口中都加traceId字段。


而改成让用户在header中传入traceId,在服务端使用统一的拦截器解析header,即可获取该traceId了。


17. 批量


我们在设计接口的时候,无论是查询数据、添加数据、修改数据,还是删除的场景,都应该考虑一下能否设计成批量的。


很多时候,需要通过id查询数据详情,比如:通过订单id,查询订单详情。


如果你的接口只支持,通过一个id,查询一个订单的详情。


那么,后面需要通过多个id,查询多个订单详情的时候,就需要额外增加接口了。


如果你添加数据的接口,只支持一条数据一条数据的添加。


后面,有个job需要一次性添加1000条数据的时候,这时在代码中循环1000次,一个个添加,这种做法效率比较低。


为了让你的接口设计的更加通用,满足更多的业务场景,能设计成批量的,尽量别设计成单个的。


18. 职责单一


我之前见过有些小伙伴设计的接口,在入参中各种条件都支持,在Service层有N多的if...else判断。


而且返回的实体类中,包含了各种场景下的返回值字段,字段很多很全。


接口上线一年之后,自己可能都忘了,在哪些业务场景下,要传入哪些字段,返回值是哪些字段。


这类接口的维护成本非常高,而且又不敢轻易重构,怕改了A业务场景,影响B业务场景的功能,这种接口让人非常痛苦的。


好的接口设计原则是:职责单一


比如用户下单的场景,有web端和移动端。


而每个端都有普通下单和快速下单,两种不同的业务场景。


我们在设计接口的时候,可以将web端和移动端的接口在controller层完全分开。


/web/v1/order
/mobile/v1/order

并且将普通下单和快速下单也分开:


/web/v1/order/create
/web/v1/order/fastCreate
/mobile/v1/order/create
/mobile/v1/order/fastCreate

这样可以设计成4个接口。


业务逻辑更清晰一些,方便后面维护。




作者:苏三说技术
来源:juejin.cn/post/7372094258793414710
收起阅读 »

记录裁员后的半年前端求职经历

普通的人生终起波澜 去年下半年应该算是我毕业以来发生人生变故最多的一段时间。 先是 7 月份的时候发作了一次急性痛风,一个人在厦门,坐在床上路都走不了,那时候真的好想旁边能有个人能扶我去医院,真的是感受到 10 级的孤独。尝试了好几次想蹦到路边都顶不住,最后还...
继续阅读 »

普通的人生终起波澜


去年下半年应该算是我毕业以来发生人生变故最多的一段时间。


先是 7 月份的时候发作了一次急性痛风,一个人在厦门,坐在床上路都走不了,那时候真的好想旁边能有个人能扶我去医院,真的是感受到 10 级的孤独。尝试了好几次想蹦到路边都顶不住,最后还是打电话给岛外的堂哥来接我去医院。


然后开始减肥健身,每天 7 点下班就去健身房练到 10:30 健身房关门,每周末去走山海健康步道临海线或者骑行环岛东路,最终是从去年 7 月 200 斤到今年 4 月减到了 140 多。


减肥这段时间戒掉了很多坏习惯,比如奶茶,喝汤,含糖饮料,熬夜,晚起。一方面上班的时候的时候感觉自己有点营养不良,尤其是上午的时候大脑有时候会空白,但另一方面又被自己能坚持走 4,5 个小时的健康步道的毅力感到惊奇。身材也因为健身力量训练慢慢变好,洗澡的时候开始喜欢照镜子了。但是我总觉得人生不可能一帆风顺,一直向着好的方向发展。


去年 10 月国庆回家参加亲姐姐的婚礼,欢欢喜喜过了个国庆回来不久就被 leader 约到会议室通知我上了裁员名单。补偿是 n+1,last day 是下周五。


其实我当时听说我被裁了,我当时的状态是觉得很突然,但又不觉得意外,觉得有点不知所措,但又觉得有点解脱后的小确幸。不意外是因为公司很比我优秀的大佬都先我一步被裁了,在目前看不到公司起色的情况下,轮到自己是迟早的事,另一方面我一年前就和 leader 聊过我想离开的想法了。觉得不知所措是因为我还没有做好能通过 2 个月内面试快速找到并进入下一段工作(那时候还没体会到行情的恶劣)。去年年后调薪失败后我就开始认真准备面试了,后面被裁后找工作的时候刷算法题的仓库还是那时候建的,只不过后面因为要痛风之后准备花一两年好好调理身体就没怎么刷题了。


后面很多次面试面试官都问过我为什么离职,我都是直说裁员,一方面我觉得现如今行情裁员是很正常的,如果一家公司歧视被裁的,那我也会瞧不起这家公司。虽然我完全可以说是我自己裸辞的,而且公司方面态度也是完全支持或者说希望我们这么做的,但是我这人就是不喜欢作假。我可以承认我的不优秀,但是我也不愿意让自己心里有疙瘩。


坦白说,对于上家公司,我没感觉受啥委屈,leader 和同事们也非常 nice,我觉得对我挺好的,换做 35 岁,快退休的时候,我还挺乐意在那养老的。但是我毕竟 30 岁没到,还是受不了一成不变的生活,想出去折腾折腾。再加上我并不是厦门本地人,我感觉 80% 的同事都是福建人,作为来自江西的外地人的始终没有啥归属感。


但老实说,来了深圳我还是挺怀念以前厦门的生活的。怀念在环岛东路边吹海风边骑行,骑累了,就躺在路旁的草地或者石椅上;怀念五缘湾公园和陌生人一起跑步的日子,跑累了就躺在阶梯超宽的木桥上吹海风;怀念林海线的广播音乐,怀念健身房那个每次见到我都会和我打招呼教了我好多东西的练得超好的做产品的老哥,怀念每天早上都要去消费的美宜佳(对比深圳荔林这边美宜佳店面大得多,东西又全又新),怀念我常去的那家理发店爱吹牛健谈的老板(可能是因为冲的会员还有好几百没消费完^_^)。


通知被裁了以后,我就没咋干活了,一边交接工作,一遍把自己做的一些工具能开源的都给开源了。现在想来,我应该学习另外一个和我同期被裁老哥一样争取混到月底再走的。在 last day 和另外一个同事一起请客散伙饭,把部门移动端和前端同事,以及部分先我被裁的同事都叫来了,破例的喝了快 10 瓶百威,最后散场和一个先我被裁的同事一起去五缘湾散步解酒到深夜,算是正式结束了毕业后第一份工作的旅程。


通知被裁的当天我就给父母打电话告知了自己被裁的消息,本想找家人分享找点安慰缓解下自己的焦虑,但是反倒是自找烦恼了。在他们眼里就是:你被裁了肯定是你做的不够好,你做错事了,你没有打理好和领导的关系,休息个屁,赶紧找工作,明天就开始找工作。


聊不了一点,对着电话那头吼了几句直接挂了。


享受生活


被裁后我没有选择立即找工作,一方面自己完全没准备好,在上家公司工作三年从来没出去面试过(反面教材),另一方面,趁着现在有时间我想尝试一些我没试过的东西:



  • 骑行绕岛内一圈

  • 骑行往返从岛内到岛外,我在厦门的亲戚基本都在岛外同安区,顺便在亲戚那里好吃好喝了一天


离开厦门的前一晚还是去健身房健身了,和熟识的老哥道了个别,第二天就不悲不喜的离开了厦门回老家了。相比前一段伤心的北京实习经历,那肯定要好受的多。


回家之后时间来到 10 月底,那时江西老家的天气还没开始降温,但也很快就开始降温了。早在国庆节之前就买了组合哑铃和卧推凳到家里,所以在老家有坚持练胸和背,腿没怎么练了,跑步就当练腿了。国庆期间还买了很多渔具,买了汉鼎的 5.4 得螺纹钢,平时很喜欢看钓鱼短视频,小时候也经常钓鱼,在我们村我自己家有三个鱼塘,所以国庆长假钓鱼还是蛮有意思,我姐婚礼酒席上的十多条大草鱼就是我和伯父一起钓的。


当然我也没闲着啥事不干,去县城驾校报考了个 C1 驾考,回家老考驾-照 也成为了我回家以后应付村里老乡问我 为啥离过年还有 3 个月就提前回家过年 的借口。10 月 20 多号报名,最后到 12 月中旬考完,下旬拿的驾-照,分别考了 98, 100, 100, 94,科目二,三都是一把过。冬天考驾-照好处是考的人少,不怕没车子练,缺点是得早上 7 点就得起床顶着严寒去驾校练车。我发现组里 7,8 个人,总会有一两个没考过的,没考过的往往还是平时练得好的,像我这种科目二模拟考从来没过的反倒满分一把过。有事没事会去自家鱼塘钓鱼,直到后面草鱼和小鲫鱼都吃腻了就不去钓了,后面天气彻底冷下来了后也没法钓了。之前有一次大冬天去钓鱼,坐那吹了一个小时的冷风,鱼不开口就算了,还把自己整感冒了,打那就再也提不起钓鱼的兴致了。


考驾-照期间没去面试,但是有坚持每天刷 leetcode 和 ts 体操,去掘金上或者牛客上找找近两年的一些大厂面前端试题。平时有看 b 站的习惯(真不爱看擦边视频。。。),天天给我推送一些考驾-照还有前端的一些技术视频。刷八股或者看技术视频看到一些感兴趣前端知识点的就会记到便签里面,第二天在自己的 fe-demo 仓库写代码验证。刷题是为了面试,写 demos 是为了查漏补缺,学习进步。


感谢这段时间老妈每天的投喂,回想这半年我觉得一个很大的遗憾是没有带老妈出去旅游,感觉人生中这么有空的机会不多。


年前面试


12 月中旬拿到驾-照后就开始投简历面试了,一开始就投大厂:



  • wxg 视频号直播

  • 米哈游协同文档

  • 字节杭州巨量星图

  • 飞猪

  • 北京即时设计

  • MoeGo(不算大厂,但是主动约我面试的)


都一面挂了,前两个我录了面试语音,后面听的时候就发现我面试的时候表达问题很大,首先是非常紧张,说话经常结巴和重复,二是回答问题不够系统有条理。挂了我觉得不意外吧。


腾讯这个岗位应该是真招人,面完的感受是对 nodejs 要求应该蛮高。


米哈游协同文档这个岗位,我本来不想面的,因为我对这个工作方向不是很感兴趣,但是 hr 又是知乎私信又是微信加我想要约面试,就试了试,结合自己的感受和一些网上对这个岗位的评价,有较大概率不是真招人就是想骗方案。。。国内大厂貌似都喜欢自己做 IM 和 文档。


字节面挂了不知道是啥原因,我觉得可能得原因



  1. 题目没写出来,它们那个面试平台没用过,当时不知道咋测试,折腾半天就说了下思路,面试官说没问题

  2. 当时面试官问我我前公司相对其它公司竞品的优势是什么,这么基础的问题,我卡了半天,答的不好,坦白说平时很少研究竞品,自己做的产品平时都很少用

  3. 最后他问我对工作的期望,我说希望最好还是能 8 点前下班,然后问他们那工作作息,一般 9 点下班...


挂就挂了吧,但是好像面试官给的面评不好,导致后面想换个部门面 hr 不给过。我后面发现面试官好像后面还 follow 了我 github...


飞猪这个貌似是部门 leader(不确定),是 V2EX 看到我发的求职帖子直接微信视频面了下,面完他说我还没准备好面试,等我准备好了再找他,但是他给我的感觉阿里味太浓了,聊完我直接把他微信删了。


即时设计据说年底融到钱了在扩招,而且貌似很喜欢约我前公司的人面试,据说开的还挺高。好像是他们 hr 主动在 boss 上约我面试,一面结束,我在 boss 上问他面试结果咋样,告知一面通过了,马上会约二面,结果我等了两个星期都没约面试。boss 上问他后面也没回复,第三个星期当时我姐在九江医院要生孩子,我配我老妈去陪护我姐。打个电话说是前段时间忙着公司团建,没空约面试,问我明天周五约不约面试,我说我没带电脑,要不下周一,说是没问题。结果后面就没后面了,真是草了。。。说起来和一面面试官聊的还蛮开心的,最后问的一个算法题都感觉有点像是放水了,说是最后我们来考道简单的动态规划的问题。我一听是动态规划瞬间就紧张了,结果问的是青蛙跳台阶那道题,题目刚说完,我就把思路和面试官说了,面试官就说不用写了开始聊天...


MoeGo 这个公司网上风评很好,我实际感觉也确实挺好。他们 hr 我感觉就很专业很舒服,约面试之前还给我详细介绍了他们公司的业务,发了一些他们背景资料给我看,我提问的时候也都很耐心的回答。一面前做了个笔试题,我觉得题目出的也挺有水准。一面是个姓毛的面试官,最开始他们我平时在干嘛,我说平时喜欢刷类型体操,就来了几道体操题,都是那种入门级别的 Exclude 之类的,有一说一面试官平时应该是没怎么刷过 ts 体操的,用了一些很简单的技巧,感觉面试官的反应是没太看懂,例如:


type Exclude<T, U, E = U> = E extends U ? never : E;

面试官还问这个 E 是干嘛用的。题目太简单,表现欲太强以至于我面试的时候就说你出的这些题都是入门级的。面试官的于是转考手写 react 代码,他们公司貌似是 react/react native 技术栈,其中一个考的是使用 hooks 实现 useDebouncedState。要是考 debounce 怎么写,我能很快写出好几种写法并且考虑 leading 和 trailing,但是我是真的很少写自定义 hooks,要我用 hook 来实现 debounce 我还是真一下子搞不定,以前也没用过。最后在他耐心的提示下写出来了,但是我觉得可能就是写的真的很呆逼,可能是因为这个原因觉得我 react 写的太少,一面给挂了。也有可能是竞争比较激烈,虽然挂了,但是他们公司 hr 和面试官给我的印象都挺好。由于自定义 hooks 没写好,于是还专门创了个仓库练习手写一些常用的 hooks:react-hooks


总结下前面几个面试:



  1. 太紧张,在上家公司呆了三年,没怎么出来面试

  2. 技术问题没啥太大问题,项目没准备好

  3. 不应该上来就投大厂的或者想去的公司,应该先面些去的意愿不是很强的公司磨炼一下面试和表达技巧。进入下份工作,就算不是想换工作,也应该每年出去面面试,好处很多:

    • 了解行情和自己的水平

    • 拿个高于现在的薪资的 offer 聊涨薪的时候有筹码,比较有底气(这是另一个前同事的现身说法)

    • 磨炼下面试技巧,现在这行情,被裁了真不好找工作,便于被裁了快速过渡到下一份工作




面完这几家,在九江看着我外甥女出生后就回家过年了,这段时间基本上也没咋准备面试,大过年就该好好玩享受生活。不过过年的时候看到放烟花举国欢庆,喜气洋洋的时候,入睡前想到之前面试的失利和前途未卜,还是感觉心里有些阴霾。


插个题外话,有次村里一个媒人来我家,想给我牵线,问我现在哪工作,我说是在找工作,媒人就悻悻的离开了。。。所以说没有工作,相亲都没人给你介绍。


年后面试


年后我要求就放低了些,大小厂都投,包括一些小的初创公司。


大厂


我发现中大厂除了我之前面过的几家,基本上都不给面试机会。
这几家我觉得是不招人,没 hc 的:



  • 快手

  • 百度

  • 美团

  • 蚂蚁

  • 滴滴

  • 知乎,b 站,boss 直聘等等


这几家我觉得是招人但对简历背景要求比较高:



  • 携程

  • 小红书

  • shopee

  • AfterShip

  • 得物

  • 希音(内推大佬说是学历没达标)


以上公司全部没有面试机会。我发现,那些发展比较好的中厂对背景特别的看中,反倒是部分大厂只要能力出众走内推大概就会给面试机会。


年后大厂里面就只有阿里和网易约了面试,其实我也试过再换个字节的部门面试,貌似是面评的原因没 hr 那边没过。


阿里


这里点名表扬阿里,我面了五个部门,虽然没去成,但是很感谢给了这么多面试体会。



  • 飞猪

  • 淘宝(twitter 上一个老哥内推的),一面挂,貌似是在用新版 weex 重构项目,感觉是当时那个 lazyMan 的问题写的不是很好,还有后面一些 vue 的问题答的一般

  • 淘宝客服团队的,二面挂了,问的一些项目相关的问题例如你觉得你在项目里面做的好的地方,做的不好的地方,新版上线旧版咋维护的,反馈说是缺乏对项目的思考和动机,表达不是很通畅的。关于这个项目的思考我得洗一下,我当时参与那个项目的基建工作,是半路被 leader 拉过来救场的,你问我这个项目的很多规划上的问题,我是真没去了解。

  • 阿里文娱淘票票,当时晚上 8 点多,躺床上准备刚眯了一会,被阿里面试电话吵醒。脑子有点不清醒,刚好又问了一些平时没咋准备过的问题,感觉挂了也不意外吧。我还问了为啥 8 点多还来面试,他说这个点刚好工作忙完了有时间面试。挺无语的,面试难道不是工作的一部分吗?

  • AliExpress,这个是 Boos 上我投的,结果打电话给我说是外包岗位,觉得我很合适(无语),问我面不面试,我说不考虑外包,然后他好像挺纠结的说好像也有正式岗位在招,就挂电话了。过了一会同一个人又打电话过来说是确定了有正式岗位的 hc,然后就一面完了,第 3 天就二面了,二面完了就收到邮件进了内推,后面就一直没消息了。挺奇怪的,你说你二面如果不打算让我过为啥还要再进内推流程,而且面试过程中我觉得表现是很好的,聊的也挺开心的。两个猜测,1.要么是没 hc 了,或者有更好的候选人把我当备胎了 2. 骗方案的,一开始就没 hc,因为两个面试官都问了同一个问题,说是他们有一些内部遗留项目没有做 I18n,问怎么搞


拼多多


这个好像是脉脉找了个内推链接,自己投的,面到谈薪阶段,因为薪资达不到预期就不继续了。



  1. 一面的时候 hr 明确告知了 11, 11, 6

  2. 安排面试的 hr 和谈薪的 hr 不是同一个人,谈薪的 hr 很强势,给你一种爱来不来的感觉,我问到竞业之类的一些问题,她说我顾虑太多。。。

  3. 强度这么高钱没给够,我是不可能去的,我一个工作三年的前端让我背应届生倒挂我是不可能接受的呀,就因为我上份工作工资低?

  4. 确实卡涨幅的,给钱没几年前网传的大方


杭州某音乐公司


一面面试官是我这么多面试以来我觉得技术和交流最舒服的一位,为什么呢:



  1. 回答两个问题后,他就说,后面你答到关键点我们就过,直接下一道,真直接啊。。。我就喜欢直接的面试官。

  2. 问了个 babel-runtime 的作用,还夸了下我对前端工程化理解确实有点东西,感觉确实问的问题有水平,不是上来就让你背 webpack 的打包流程...

  3. 有些问题没达到关键点或者有些问题,他会直说,引导你把正确答案或者说他想要的答案说出来。比如他问了说 FCP 怎么提高,我说了很多,像是从网络协议和传输体积这些方面回答,但是他说这些不是他想要的答案,我就忽然想到说 SSR,后面就继续聊 SSR 相关的一些东西。很多时候面试问的问题我真不是不会,我感觉就是当时理解错了问题,或者和面试官已有的观念不一致。有些面试官就听到你的回答,不符合他的想法,甚至用有点嘲笑般的语气说下一道,我不知道这些人是平时工作生活不愉快来面试的时候找优越感,还是活太多想快点面完就去干活。

  4. 面完了以后他还问我离职半年都是脱产状态吗,以我的水平不至于找不到工作啊,之类关心我的话,还是头一个面试官会关心我是不是脱产。当时我说感觉可能过不了,我说我面试时间长了对自己是越来越没自信了,然后他说面试给他的感觉还是很不错的,能力还是很可以的。


面完之后第二周约了二面,这个面试就有点不愉快了:



  1. 时间约到了晚上的 7:30,面过的公司很少有约晚上面试的,我也不喜欢晚上面试,应该没和我提前沟通过(不确定)

  2. 面试官要求我开摄像头,他自己不开。面完我就问他了,我说为啥要求我开摄像头,他不开,他说没约到会议室的,灯光很暗开了摄像头我也看不到啥之类的。但我还是觉得,你不开摄像头,就不应该要求我开,我觉得候选人和面试官是平等的,我觉得我没有被尊重,面试反馈直接给了个差评。这让我会想到当初应届的时候面试腾讯 IMWeb 的经历,当时是个女面试官,也是没开摄像头,也是面试没半小时就结束面试了。

  3. 我记得他当时好像问了说是为啥用 importmap 比直接配制 splitChunks 好,我都说了可以更精细化的控制缓存,加快生产环境打包速度,但是貌似还说说服不了他,硬是逮着这个问题一直问,我就差点直接和他说了这个是我们公司当时基建部门硬推的,但是我觉得好处我也说了,说服不了就不能换个别的正常点的问题问吗?面试时间我记得比较短,可能半小时不到。

  4. 把我面挂的面试官挺多,但让我不舒服的他算一个


中小厂


中小厂真的是踩坑比较多,各有各的花活,让我印象好的不多。


x 麦物流


首先我没走进面试流程过,但是还没进到面试就让我对这家公司印象贼差:



  1. 找人内推,说是因为他们 leader 看简历感觉我是熟悉 vue 而不是 react,所以简历没过,他们只想找熟悉 react 的。我是真没想到现实中真有歧视技术栈的

  2. 后面应该是换了个各渠道投简历,hr 打了个电话过来。首先说她不是来约面试的,只是来了解情况的,面试还没开始就问我上家公司工资多少,预期是多少,我报了个预期薪资,她反问道你凭什么要求这么高的涨幅,我算了下也就 20% 的涨幅,而且是从厦门换到深圳。给我一种暴发户在施舍工作的感觉,电话聊完还是没有进面试


后来在推特上也看到有人吐槽这家公司的骚操作,还在找工作的可以留个心眼。


杭州 x 登科技


别的我先不吐槽了,我就先吐槽面试流程:



  1. 笔试

  2. 电话面试,考手写算法

  3. 要求必须现场面试,ok,我从老家江西坐高铁去现场面试

  4. 一面技术面,考前端基础

  5. 二面 leader 面,考算法

  6. 三面杭州负责人面

  7. 四面 hr 面

  8. 完了回老家之后还要再加一轮业务负责人面


8 轮面试,我和我朋友聊到我面试某家公司面了 8 轮,没一个人不是开口就说这家公司 xx 的,搞这么多轮面试。


槽点不只是这个,当时一面面试的时候,面试官端了个电脑过来,我也背了包带了电脑过去,他要考我写一个深克隆,我说我用电脑写吧,他不让,硬是要求我必须用水彩笔在白板上给他写代码,说是他们平时交流都会用到白板。我是觉得真 xx,完全是形式主义。


更过分的是,当时我手上有另外一家公司的 offer,但是已经到了回复的 deadline 了,hr 也是知道我有另一家的 offer,在我拒了另一家回复说愿意接受开出的价格的时候,hr 又回复说觉得我稳定性不够不发 offer 了。这一轮操作直接搞得我两个 offer 都没了。每次和老爸聊天他都拿有 offer 的时候不去这个来训我,每次也因为这个和老爸大吵。我的观点就是我想争取更好的工作一点问题都没有,而他的观点就说我不知足,好高骛远,有份工作就应该去干。


经验教训:没收到 offer 邮件都不稳。你对别人的真诚,别人可能觉得你是个傻逼。但凡要求必须线下面试的,基本上都有坑。


成都 xx 英


槽点:



  1. 笔试,要求写一个项目发源码,确实有套方案的嫌疑。当时写了个浏览器端的文件管理器:react-file-explorer,他们内部好像是要做一个 web 端的小组件编辑器

  2. 面前端面试官不是很懂前端

  3. 公积金确实是按最低标准交的

  4. 和脉脉上某个人遭遇是一样的,面完一轮后过了快一个月又联系我面试

  5. hr 和脉脉上说的一样回复消息超慢,你不主动问基本不会联系你是真的

  6. 由于上一点,导致当时谈 offer 的时候她发了个消息,我当时可能在忙啥,我想着就故意想着晚点回复,结果后面被我忘了,然后大概一周后我才回复她,她回复说看我没回消息就让别人先入职了。。。


还有一些其它骚操作的公司我就不说公司名了:



  • 第一轮面试全就发张表给你,让你把包括上家公司,预期薪资就填上去的。其实我觉得这些比较私密的信息应该到最后阶段才适合交流,上来就查水表,会让我觉得这家公司很不尊重人

  • 有问为啥 typeof null === 'object' 的,我说这就是设计失误没啥可解释的,面试官让我再想想。。。

  • 其它的想到再说


厦门互联网公司


其实我还是蛮喜欢在厦门工作的,待了三年有点感情了,除了工资低点,城中村好多都拆了不好租房子,饮食不辣外能吐槽的点也不多。但环境是真好,厦门政府是真有钱啊!


找同事了解了下厦门的一些不坑的互联网公司,但问了一圈基本上都没 hc,有些我还是直接找在职的前同事问的,确实不咋招人了。当时时间是 3 月份,所以金三银四已经成为一个笑话。


唯一面上的是美 X,美 X 是因为我曾经的 leader 在那,直接找他内推的,但是一开始推的不是他自己的部门,面完两轮后,反馈说是想招偏业务的而不是偏基建的。我不知道这是不是借口哈,因为我之前被问到我和内推我的人是啥关系,为啥不内推到他自己的部门,我有点怀疑是因为内部斗争有顾忌?(我自己的想法)。


好吧前 leader 把我推到他自己在的部门,结果面试的时候貌似是两个人一起面的我,像是一老带一新,这个面试官感觉很不专业,说话比我还紧张,问的问题也贼离谱,也不会引导话题,体验贼差。我还记得他们到说看我写了熟悉 typescript,就问我我平时是怎么使用函数重载和泛型的(咋不问我 if 和 while 的区别,贼尬),问我平时是咋做埋点的,我都说了我平时很少做埋点,还一直追着问。给我整烦了,问的问题全是没准备过的问题,回答的有点乱。


小型初创公司


基本上都是在 V2EX 酷工作板块看到的招聘信息。


这些小型初创公司基本上都是在一线城市,人数在 1 ~ 10 人之间,基本上都是做 AI 出海项目的,简单分为几类:



  1. 薪资开的很低,喜欢画大饼,能理解,毕业创业不易

  2. 不是想招人,只是想短期外包个前端,也能理解,但是不要拐弯抹角,想外包一开始就直说,浪费彼此时间

  3. 比较正常(指的是我现在这家)


我当时对工作的要求简单来说就是:



  1. 要么:钱给的不多,但工作轻松

  2. 或者也可以:很累,但钱给的多


对于上面第一类公司,面过两家,应该都是觉得我薪资要求高于预期,也有可能觉得我的水平高于他们预期,去了也待不久,就说不合适。其实还好,我去的意愿也不是很强。第二类碰到过一家。第三类其实说的就是我现在待的这家公司。


现在这家公司之前在 V2EX 上发招聘贴的时候,我其实看到了,但当时觉得背景挺牛逼的可能不会招我这种双非本科的就没投,但是后来团队里的人发邮件给我说狼叔推荐我给他们,问我要不要面试,然后我就去试了试。其实我感觉缘分就很奇妙,应该是诸多原因给了我面试机会的:



  1. 狼叔推荐的

  2. 组里大佬之前有面试过我前东家

  3. 组里大佬又找我前东家的原技术大佬了解我的情况


不过当时面完最后还是没发 offer,在两个候选人之中最后他们选了另一个。介绍我去了另一个搞 AI 生成广告视频的团队。当时我和他老板聊天,聊下来的感觉是觉得他有点不太尊重我,聊完直接删了微信。


至于我最后是怎么又去了这个公司,那得等到 3 月底了,也就是 1 个半月之后,据说是因为某些原因之前选择的另一个候选人离开了公司,岗位又空缺了,于是问我愿不愿意去,当时正非常焦虑的我就接下了 offer。


说来当时也是真巧了,那天我大概是午饭前接的 offer。本来打算等下午老爸从工地回来和他分享喜讯的,但是午睡完就接到老姐一个电话,说是老爸给他打电话给她骂了一顿,向她吐槽说是你弟弟什么有 offer 的时候拖着不去,后面又去不了,什么不好好找工作,搞得他干活都没力气,烦的都想要喝农药了,说她做姐姐的也不去说说弟弟。我怕老爸真喝农药去了就立马给他打了个电话说我找到工作了,他还不相信,说我是骗他的,安慰他的,再三说明下才相信是真的。诶,没工作的那段时间,前期还好,没咋吵架,越是到后面,不但我自己焦虑,每次和老爸碰面都要吵一架,搞得我更烦。


心态变化


刚被裁的时候其实就没啥想法,也没急着想找工作,想着反正拿了大礼包,先玩两个月。


年前面完失利后,就感觉自己准备不足。


对自己的实力其实还是很自信,但是挂多了后也开始慢慢对自己产生了怀疑。虽然没又明显感受到对长 gap 期的歧视,但是被问到为啥 gap 期这么长还是有点忏愧。


年前到 3 月初还能约到面试,3 月开始整个招聘市场就好像毫无波澜般的死寂,投个简历出去一个星期没约到面试。自从发生了那次被口头毁 offer 事件后就有点摆烂了,想随便面个公司上班去得了,实在是面试面的我心里憔悴,煎熬的很。


B 站和掘金开始经常给我推荐失业找工作的内容了,也让我更加焦虑。在推特上发了一些找工作的帖子,评论的特别多,确实是流量密码,发了两三个帖子后粉丝直接破千了,但这确实不是我的本意,我分享的也都是真情实意,往往也是确实无处发泄情感想找个地方发出来在网络上找点认同感和安慰。


由于多次发生了我感觉是因为我价格报高了没后续的情况,后面问预期薪资我就直接报最低预期了。我感觉很多中小厂的 hr 是真的有这种心态:你不来有的是人来,还更便宜。当时还发生了飞书裁员事件,虽然我觉得对招聘市场影响不大。但是时不时爆出来大厂裁员的新闻说明了一个问题,招聘市场有不少大厂背景出来的,竞争很激烈。


几次大厂面试挂在项目面的经历让我有点沮丧,技术面都没问题。项目这东西我也不知道咋准备,我做过的项目包括:自研的跨端框架(多数面试官听不懂),adobe 插件(技术栈太冷门),前端基建。没有那种大型的 to c 或者 to b 的项目,就显得项目没啥亮点,也想过去硬编那些不是自己做的东西,但是也许是我太真诚或者是蠢,确实没编过,做过啥就说啥。


好在得到现在老板的赏识,啥也没纠结直接就接 offer 入职了。


求职工具分享


渠道


这个我之前发过一个 twitter,这里再简单列举下:



  1. boss

  2. V2EX 酷工作板块

  3. 推特上一些大佬经常会转发一些工作机会的帖子,例如狼叔,大圣等,关注的一些老哥自己公司有 hc 也会发招聘帖子像 chenqing663, 荣顶

  4. 脉脉系统推荐

  5. 阮一峰 weekly

  6. 掘金沸点 - 内推招聘广场

  7. 各大公司官网投递(不推荐,基本上没有用)


准备面试



  1. leetcode,建议按照 tag 刷,推荐顺序二叉树,链表,数组,字符串,回溯,动态规划的,建议开个会员按出现频率顺序刷。不知道啥时候 leetcode 增加了个 javascript 分类,建议全刷,挺基础的质量还行。刷题一定要自己动手啊,光看题解是没用的。

  2. ts 体操不建议刷,基本上不考,参加了那么多面试,问的少不说,问的问题也都很简单,想刷的推荐 typeherotype-challenges。有个面试面试官发了个 typehero 某道题的链接让我做题,结果我打开后显示我已经做过了,面试官还说思路比他自己的还要好。。。

  3. bigfront 这个真是一个宝藏网站,优点很多,可以刷的题目类型很多,包括算法题,react, css 等,题目质量也不错,还有评论区可以看大佬们的题解,不但能测试算法题,react, css 也能测试,还能直接看到测试用例的代码。

  4. 刷面经,可以去牛客网,掘金,或者百度搜

  5. 刷八股建议搜索掘金的文章,这个时效性不是很重要,质量更重要。建议直接看点赞多的,一篇没整明白多看几篇,看完一定要总结。除了掘金,B 站也有很多优秀资源,例如 https 可以看看 技术蛋老师的视频

  6. 准备面试就是一个查漏补缺的过程,这个缺包括面试过程中没答好的点,自己看面经或者技术文章看到不熟悉的或者存有疑惑的知识点,还有一些实时热门知识点,例如跨标签页通信,服务器端推送。一定要自己写 demo 验证,只有这样才会印象深刻,面试的时候万一手写也没啥怕的。

  7. 可以让 chatgpt 给自己出题,可以把自己简历发给 gpt 读,结合自己的实际情况和面试的期望优化上下文,出题的时候让它只出题目不要解答,不然会影响自己思考。如果你能 10 道题 9 道都比较完美解答的化我觉得就不用刷啥八股了,准备项目吧

  8. 简历最好套个简洁的模版导出 pdf,我用的 resume-chinese,直接 Overleaf 在线编辑导出

  9. 可以整个博客网站,hr 和面试官确实是有可能会看你的博客的,所以建议把你希望被看到的博客置顶。不建议 github 没啥东西也搁简历上放个 github 链接

  10. 我写的油猴脚本:重新定义 Boss 直聘

  11. 自我介绍建议背诵道滚瓜烂熟,不要面试一开始就乱了方寸

  12. 面试前建议了解下面试的背景,它们的竞品,访问它们的官网,体验下它们的产品,可以用 lighthouse 检测下它们的网站

  13. 情绪积压多了,确实影响身心健康,挂多了我吃饭都没胃口,条件允许建议去跑步,有啥不痛快的情绪也可以在网络平台去分享,推荐推特,推特混久了你会发现前端圈子真小。。。

  14. 如果你觉得面试的时候就对这家公司有点不认可,建议千万别将就着去。例如面试的时候感觉 leader 面的 leader 喜欢 pua,掌控欲很强,阿里味很浓,建议远离。我还是对喜欢说大白话的面试官比较有好感,那些说话一套一套整那些虚的,喜欢给你画大饼的在我这是很减分的。


感谢



  1. 感觉家人对我的关心和照顾,感谢老妈每天的投喂,感谢老爸大冬天一大早开摩托送我去练车,感谢奶奶经常给我做好吃的,感谢我可爱的外甥女让我在焦虑的面试中感受到一些生活的温馨。

  2. 感谢一些前同事的关心,工作介绍,和经验分享

  3. 感谢网友的内推,耐心回答我对他们公司情况的疑问,重点感谢 柳家忍 和狼叔

  4. 感谢老板的赏识,感谢入职后可爱同事们的照顾


未来


感觉目前公司的业务充满了机遇和挑战,这还是第一次感觉自己做的东西会对公司的业务产生非常大的影响。原本想着工作之外搞搞开源玩点自己想玩的,但是我觉得我们项目目前能做的优化就很多,优化的过程就能学到很多,优化好了就会对同事的开发体验或者用户体验产生很大影响。所以工作外的时间除了享受生活外可能会花在优化公司项目上,开源项目会做的比较少了。不过我现在已经习惯了将自己做的东西开源,以及将开源的东西用到公司的项目,所以其实也是在维护开源项目。


作为一个技术洁癖者,在创业公司要做好技术和开发效率之间的平衡。


进入 ai 行业,我要学习的东西也还很多。


对于深圳熟悉又陌生的城市,也要努力的去适应,努力工作的目标是为了更好的生活,不要舍本逐末。


前段时间压力比较大没加上有点不适应这边居住环境吃的有点放纵(好吧我就是贪吃),体重有点回升,接下来要注意控制了,多出去运动。


职业生涯第二站来到了一家创业公司,相信一切都是最好的安排。煎熬的求职经历让我明白了居安思危的重要性,要更加重视职业规划。我觉得我老板和同事还都挺靠谱的,可以向他们学习怎么做更有价值的事,怎么即时正确的调整产品方向。程序员的出路我感觉也就是走管理和创业(包括独立开发),指不定以后自己 30 多岁没工作了也去创业了,这段经历肯定会有所帮助。


感兴趣可以体验下我司的产品,给我反馈 bug:openart.ai


话说这篇博客的 og 图片也是用我司的平台生成的 😀,prompt 很简单:one boy, no job,然后用了 Expand Image 功能把图片横向扩展了下。


作者:余腾靖
来源:juejin.cn/post/7368288987641774120
收起阅读 »

近几年很火的「浏览器指纹」是怎么回事?

前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 背景 不知道大家在浏览一些网站时,有没有注意到这么一件事情,就是你在某一个页面浏览了一些你喜欢的东西,但是你并没有登录,等你换一个标签页打开这个网站的时候,他照...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~


背景


不知道大家在浏览一些网站时,有没有注意到这么一件事情,就是你在某一个页面浏览了一些你喜欢的东西,但是你并没有登录,等你换一个标签页打开这个网站的时候,他照样能推送一些你比较感兴趣的内容供你阅读


就比如一些新闻网站、资讯网站、购物网站。我们并没有登录,他是怎么知道我们的喜好的呢?或者说他们是怎么记得我们的呢?



什么?浏览器也有指纹?


这里的指纹不是指的手机上的那种指纹解锁,你可以认为:浏览器指纹就是浏览器的标记


有了这个标记之后,每次请求接口的时候,浏览器都会带着这个标记去发送请求,这样后端那边就会缓存起来你这个标记,并且等下次遇到你个标记的时候,就给你推送对应的你感兴趣的内容



其实浏览器指纹这类的技术已经被运用的很广泛了,通常都是用在一些网站用途上,比如:



  • 新闻、资讯网站: 要精确推送一些你感兴趣的内容供你阅读

  • 购物网站: 要精确推送一些你近期浏览量比较多的商品展示给你看

  • 广告投放: 有一些网站是会有根据你的喜好,去投放不同的广告给你看的,大家在一些网站上经常会看到广告投放吧?

  • 网站防刷: 有了浏览器指纹,就可以防止一些恶意用户的恶意刷浏览量,因为后端可以通过浏览器指纹认得这些恶意用户,所以可以防止这些用户的恶意行为


浏览器指纹怎么算出来的呢?


刚刚说了,浏览器指纹就是浏览器的标记


你可以理解就是一段标识字符串,比如这样:



指纹算法


其实每个网站都有自己的一套计算浏览器指纹的算法,每个网站可能都不一样


但是其实市面上已经提供了很多浏览器指纹计算的算法了,大家可以到这个网站:browserleaks.com/,这个网站上展示了一些…



就比如使用 canvas 去计算浏览器指纹,通过介绍可以粗略知道,这是一种使用 canvas 画布去进行计算的指纹算法



我们可以点进去看看,在这里我们可以清楚看到目前我们这个浏览器的指纹长什么样



我们甚至可以看看这个算法,到底是什么原理,看介绍,大概就是分为几步:



  • 用 canvas 画出一个图像

  • 不同的浏览器、操作系统、cpu、显卡等等,画出来的 canvas 是不一样的,甚至可能是唯一的

  • 接着把 canvas图像 转成字符串,这样就得到了一个趋近唯一的浏览器指纹



为了防止可能是浏览器缓存影响到浏览器指纹的计算,我们可以打开一个无痕浏览器,发现浏览器指纹是一致的,那就说明这个计算的算法跟浏览器缓存是无关的~



真的唯一吗?


其实浏览器指纹只能是趋近于唯一,毕竟他是通过你的电脑信息计算出来的一个标识,在你没登录的情况下,这已经是一个比较稳妥的计算方式了~


可以看到,canvas 算法也只能做到99.99%的唯一性,所以只能是趋近唯一,所以你有没有发现,很多网站或者APP都不断在某些时机提醒用户进行登录,那是为了能更精准地投用户之所好,提高用户的黏度~





作者:Sunshine_Lin
来源:juejin.cn/post/7364409181053763610
收起阅读 »

在工作中,大家会重新优化自己写过的代码嘛?

前言 今天在忙完工作的时候,发现还有很多时间,于是......我利用这些时间来优化自己之前写的代码。 项目:Vue2 + Element ui 😺 为什么要优化? 因为看见一个页面代码篇幅太“长”(950行+),这里的950行,并不是说所有900行的单页面...
继续阅读 »

前言



  • 今天在忙完工作的时候,发现还有很多时间,于是......我利用这些时间来优化自己之前写的代码。

  • 项目:Vue2 + Element ui


😺 为什么要优化?


因为看见一个页面代码篇幅太“长”(950行+),这里的950行,并不是说所有900行的单页面就是长代码了,我前面提到的这个 “长”针对我这个页面的功能的。


我认为我这个页面的功能,可以把这部分代码写得更少,封装得更好,可读性也能继续提高,所以选择重构。虽然这个页面功能较多这个无可厚非,但是这个不是 长篇幅 的理由哈哈哈😂。


image.png


虽然里面已经有封装了组件,但是不够完善,data属性过多methods方法多拆分不完善不全面,后期想快速 定位问题 可能比较 困难,所以选择 重构


😺 重构的前提


我们 不要代码行数 来作为 代码好坏的标准。我认为这是较为狭义的,不严谨的一种说法。
Vue单页面因为 templatescriptstyle 都在一个文件里,很容易写出行数较长代码,尤其是样式,如果说需要响应式,行数根本 hold 不住,所以一般我们会把 <style> 放到文件的底部。


而影响 Vue单页面 可读性可维护性 的,主要在脚本那部分。这部分写得烂的话,不需要堆成山就足够让人崩溃了,这也和行数无关。


对于独立且比较重的业务 来讲,写在 一个文件 里也是 没太大问题 的,那些把代码拆解到300~500行一个文件的同学,有一部分是为了拆而拆。不复用的话,我们拆了可能意义不大,所以我们要具体分析是否有拆分的意义所在。


😺 拆分优化


第一:分析页面结构,拆分出可独立维护的子模块


我们来看看掘金的首页,我们可以大致分成这几块,具体内容在具体分析(这里简单给大家演示一下 页面结构 基础拆分,详细的小伙伴可以自己继续深入研究😀)


image.png


第二:明确子模块对应的代码,确定可以拆分的子组件


其中像我今天优化的这个页面中存在比较多的Tabs 标签页每个tab 里面又包含了基础的表单查询+表格+分页,很明显这部分如果写在同一个页面将产生很多基础组件方法(函数)等,这部分可以优先拆分。还有就是弹窗,抽屉 一类的也可进行拆分,即便在同一个 Vue 文件中编写,这类组件也是比较独立的部分,拆分起来相对容易。


第三:针对子组件负责的渲染及业务逻辑,明确其所需的属性及事件


细分每个子组件负责的事情,比如还是用我们掘金的首页做分析,我们要把 header 部分拆分出来,首先需要明细 header 需要渲染的内容,像 logo ,导航菜单啊,创作者中心,用户头像等等;其次确定哪些是 header 内部维护的数据,哪些需要父组件传入;另外确定暴露的事件(分发事件),比如搜索,传递出去的参数,要告诉父组件触发了搜索事件,父组件接收到才会去更新内容部分。


🐱 写到最后



  • 今天的分享就到此为止啦!😉

  • 如果大家还有不懂的地方可以在评论区留言或者大家一起讨论哦!

  • 如果这篇文章有帮到您的的话不妨 关注 点赞 收藏 评论 转发支持一下,大家的支持就是我更新最大的动力啦~😆

  • 如果想跟我一起讨论和学习的话,可以私信留言,或者评论区留言,拉你进我的前端学习群哦!

  • 感谢大家支持🤩


作者:up_up_up
来源:juejin.cn/post/7371312966364184586
收起阅读 »

好烦啊,我真的不想写增删改查了!

大家好,我是程序员鱼皮。 很想吐槽:我真的不想写增删改查这种重复代码了! 大学刚做项目的时候,就在写增删改查,万万没想到 7 年后,还在和增删改查打交道。因为增删改查是任何项目的基础功能,每次带朋友们做新项目时,为了照顾更多同学,我都会带大家把增删改查的代码再...
继续阅读 »

大家好,我是程序员鱼皮。


很想吐槽:我真的不想写增删改查这种重复代码了!


大学刚做项目的时候,就在写增删改查,万万没想到 7 年后,还在和增删改查打交道。因为增删改查是任何项目的基础功能,每次带朋友们做新项目时,为了照顾更多同学,我都会带大家把增删改查的代码再编写并讲解一遍。


不开玩笑地说,我绝对有资格在简历上写 “自己精通增删改查的编写” 了!



相信很多已经在工作中的小伙伴,80% 甚至更多的时间也在天天写增删改查这种重复代码,也会因此感到烦恼。那大家有没有思考过:如何提高写增删改查的效率?让自己有更多时间进步(愉快摸鱼)呢?


其实有很多种方法,鱼皮分享下自己的提效小操作,看看朋友们有没有实践过~


如何提高增删改查的编写效率?


方法 1、复制粘贴


复制粘贴前人的代码是一种最简单直接的方法,估计大多数开发者在实际工作中都是这么干的。



但这种方式存在的问题也很明显,如果对复制的代码本身不够理解,很有可能出现细节错误。而且不同数据表的字段和校验规则是不同的,往往复制后的代码还要经过大量的人工修改。


还有很多 “小迷糊”,经常复制完代码后忘了修改一些变量名称和注释,出现类似下面代码的名场面:


// 帖子接口
class UserController {
}

方法 2、使用模板


一般新项目都是要基于模板开发的,而不是每次都重复编写一大堆的通用代码。比如我之前给编程导航同学编写的 Spring Boot 后端万用模板,内置了用户注册、账号密码登录、公众号登录等通用能力。基于这种模板二次开发,能够大大提高开发效率,也有助于开发同学遵循一致的规范。


模板支持的功能


然而,使用模板也存在一些风险,如果模板本身有功能存在漏洞,那么所有基于这个模板开发的项目可能都会存在风险。而且别人的模板也不是万能的,建议还是根据自己的开发经验,自己沉淀和维护一套模板。对团队来说,沉淀模板是必须要做的事。


方法 3、AI 工具


利用 AI 工具来生成增删改查的代码是一种新兴的方法。只需要甩给 AI 要生成代码的表结构,然后精准地编写要生成的代码要求,就可以让 AI 快速生成了。



这种方式的优点是非常灵活,能帮开发者提供一些灵感;缺点就是对编写 prompt(提示词)的要求会比较高,而且生成后的代码还是得仔细检查一遍的。


方法 4、超级抽象


这是一种更高级别的代码复用方法。通过设计 通用的 数据模型和操作接口,实现用一套代码满足多种不同业务场景下的增删改查操作。


举个例子,如果有帖子、评论、回答等多个资源需要支持用户收藏功能,系统规模不大的情况下,不需要编写 3 张不同的收藏表、并分别编写增删改查代码。而是可以设计 1 张通用的收藏表,通过 type 字段来区分不同类型的资源,从而实现统一的收藏操作。


像点赞、消息通知、日志、数据收集等业务场景,都可以采用这种方式,通过极致的复用完成快速开发。


但也要注意,千万不要把区别较大的功能强行合并到一起,反而会增加开发者的理解成本;而且如果系统数据量较大,分开维护表更有利于系统的性能和稳定性。


方法 5、代码生成器


这也是非常典型的一种提高增删改查效率的方法。后端可以使用 MyBatis X 插件来生成数据模型和数据访问层的 Mapper 代码,前端可以用 OpenAPI 工具生成请求函数和 TS 类型代码等。


不过用别人的生成器难免会出现无法满足需求的情况,生成后的代码一般还是要自己再修改一下的。


所以,我建议可以使用模板引擎技术,自己开发一套更灵活、更适合自己业务的代码生成器。


比如鱼皮给后端万用模板补充了代码生成器功能,使用 FreeMarker 模板引擎技术实现,定制了 Controller、Service、数据包装类的代码模板。用户只需要指定几个参数,就可以在指定位置生成代码了~ 昨天 AI 答题应用平台的开发中,就是用了这个代码生成器,几分钟写好一套功能。



可以在代码小抄阅读生成器的核心实现代码:http://www.codecopy.cn/post/edkpo4 。之前我从 0 到 1 直播带大家开发过一个代码生成器共享平台,感兴趣的同学也可以学习下,保证能把代码生成玩得很熟练~


方法 6、云服务


这种方式也比较新颖了,利用某些云服务提供的现成的数据库和操作数据库的接口,都不需要自己去编写增删改查了!


比如我之前用过的腾讯云开发 Cloudbase,开通服务后,只要在平台上建号数据表,就能自动得到数据管理页面,可以直接通过 HTTP 请求或 SDK 实现增删改查,尤其适合前端同学使用。


但这种方式的缺点也很明显,灵活性相对差了一些,而且会产生一些额外的费用。


所以还是那句话,没有最好的技术,只有最适合自身需求和业务场景的技术。




作者:程序员鱼皮
来源:juejin.cn/post/7369094945154711578
收起阅读 »

一个失败的独立开发的300多天的苦果

历史是成功者书写的,所以我们能看到的成功的独立开发者,正所谓一将功成万骨枯,其实失败的才是大多数。从2023年7月14到现在2024年5月22,10个多月,一个313天总共的收入只有652元(😭😭😭) appStore的收入($72.14=¥522) 微软商...
继续阅读 »

历史是成功者书写的,所以我们能看到的成功的独立开发者,正所谓一将功成万骨枯,其实失败的才是大多数。从2023年7月14到现在2024年5月22,10个多月,一个313天总共的收入只有652元(😭😭😭)


appStore的收入($72.14=¥522)


image.png


微软商店的收入($17.97=¥130)


image.png


总结一下失败原因



  1. 做了一堆垃圾,没有聚焦的做好一款产品

  2. 没有扬长避短,其实前端开发最适合的产品方向应该是web和微信小程序,在electron上架appStore上花费了大量的时间(15天真实的时间)

  3. 归根结底还是在做产品这方面的储备不够,做产品没有定力,心静不下来,如果其他的都不做把全部的精力都拿来做aweb浏览器(包括研发和宣传),结果也不至于这么差。


分享一下失败的经验吧



  1. 全职独立开发初期很难沉下来打磨产品,还是建议边工作边搞,沉不下来就会原来越乱

  2. 如果感觉效率低,还是不要在家里办公了,咖啡馆、图书管、公创空间(武汉这边500一个公位)都是不错的选择

  3. 有单还是接吧,不然真的是太难了

作者:九段刀客
来源:juejin.cn/post/7371638121279848499
收起阅读 »

😰我被恐吓了,对方扬言要压测我的网站

大家好我是聪,昨天真是水逆,在技术群里交流问题,竟然被人身攻击了!骂的话太难听具体就不加讨论了,人身攻击我可以接受,我接受不了他竟然说要刷我接口!!!!这下激发我的灵感来写一篇如何抵御黑子的压测攻击,还真得要谢谢他。 🔥本次的自动加入黑名单拦截代码已经上传到...
继续阅读 »

大家好我是聪,昨天真是水逆,在技术群里交流问题,竟然被人身攻击了!骂的话太难听具体就不加讨论了,人身攻击我可以接受,我接受不了他竟然说要刷我接口!!!!这下激发我的灵感来写一篇如何抵御黑子的压测攻击,还真得要谢谢他。


image-20240523081706355.png


🔥本次的自动加入黑名单拦截代码已经上传到短链狗,想学习如何生成一个短链可以去我的 Github 上面查看哦,项目地址:github.com/lhccong/sho…


思维发散


如果有人要攻击我的网站,我应该从哪些方面开始预防呢,我想到了以下几点,如何还有其他的思路欢迎大家补充:



  1. 从前端开始预防!


    聪 A🧑:确实是一种办法,给前端 ➕ 验证码、短信验证,或者加上谷歌认证(用户说:我谢谢你哈,消防栓)。


    聪 B🧑:再次思考下还是算了,这次不想动我的前端加上如何短信验证还消耗我的💴,本来就是一个练手项目,打住❌。


  2. 人工干预!


    聪 A🧑:哇!人工干预很累的欸,拜托。


    聪 B🧑:那如果是定时人工检查进行干预处理,辅助其他检测手段呢,是不是感觉还行!


  3. 使用网关给他预防!


    聪 A🧑:网关!好像听起来不错。


    聪 B🧑:不行!我项目都没有网关,单单为了黑子增加一个网关,否决❌。


  4. 日志监控!


    聪 A🧑:日志监控好像还不错欸,可以让系统日志的输出到时候统一监控,然后发短信告诉我们。


    聪 B🧑:日志监控确实可以,发短信还是算了,拒绝一切花销哈❌。


  5. 我想到了!后端 AOP 拦截访问限流,通过自动检测将 IP + 用户ID 加入黑名单,让黑子无所遁形。


    聪 A🧑:我觉得可以我们来试试?


    聪 B🧑:还等什么!来试试吧!



功能实现


设置 AOP 注解


1)获取拦截对象的标识,这个标识可以是用户 ID 或者是其他。


2)限制频率。举个例子:如果每秒超过 10 次就直接给他禁止访问 1 分钟或者 5 分钟。


3)加入黑名单。举个例子:当他多次触发禁止访问机制,就证明他还不死心还在刷,直接给他加入黑名单,可以是永久黑名单或者 1 天就又给他放出来。


4)获取后面回调的方法,会用反射来实现接口的调用。


有了以上几点属性,那么注解设置如下:


/**
* 黑名单拦截器
*
*
@author cong
*
@date 2024/05/23
*/

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface BlacklistInterceptor {

   /**
    * 拦截字段的标识符
    *
    *
@return {@link String }
    */

   String key() default "default";;

   /**
    * 频率限制 每秒请求次数
    *
    *
@return double
    */

   double rageLimit() default 10;

   /**
    * 保护限制 命中频率次数后触发保护,默认触发限制就保护进入黑名单
    *
    *
@return int
    */

   int protectLimit() default 1;

   /**
    * 回调方法
    *
    *
@return {@link String }
    */

   String fallbackMethod();
}

设置切面具体实现


@Aspect
@Component
@Slf4j
public class RageLimitInterceptor {
   private final Redisson redisson;

   private RMapCache blacklist;
   // 用来存储用户ID与对应的RateLimiter对象
   private final Cache userRateLimiters = CacheBuilder.newBuilder()
          .expireAfterWrite(1, TimeUnit.MINUTES)
          .build();

   public RageLimitInterceptor(Redisson redisson) {
       this.redisson = redisson;
       if (redisson != null) {
           log.info("Redisson object is not null, using Redisson...");
           // 使用 Redisson 对象执行相关操作
           // 个人限频黑名单24h
           blacklist = redisson.getMapCache("blacklist");
           blacklist.expire(24, TimeUnit.HOURS);// 设置过期时间
      } else {
           log.error("Redisson object is null!");
      }
  }


   @Pointcut("@annotation(com.cong.shortlink.annotation.BlacklistInterceptor)")
   public void aopPoint() {
  }

   @Around("aopPoint() && @annotation(blacklistInterceptor)")
   public Object doRouter(ProceedingJoinPoint jp, BlacklistInterceptor blacklistInterceptor) throws Throwable {
       String key = blacklistInterceptor.key();

       // 获取请求路径
       RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
       HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
       //获取 IP
       String remoteHost = httpServletRequest.getRemoteHost();
       if (StringUtils.isBlank(key)) {
           throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "拦截的 key 不能为空");
      }
       // 获取拦截字段
       String keyAttr;
       if (key.equals("default")) {
           keyAttr = "SystemUid" + StpUtil.getLoginId().toString();
      } else {
           keyAttr = getAttrValue(key, jp.getArgs());
      }

       log.info("aop attr {}", keyAttr);

       // 黑名单拦截
       if (blacklistInterceptor.protectLimit() != 0 && null != blacklist.getOrDefault(keyAttr, null) && (blacklist.getOrDefault(keyAttr, 0L) > blacklistInterceptor.protectLimit()
               ||blacklist.getOrDefault(remoteHost, 0L) > blacklistInterceptor.protectLimit())) {
           log.info("有小黑子被我抓住了!给他 24 小时封禁套餐吧:{}", keyAttr);
           return fallbackMethodResult(jp, blacklistInterceptor.fallbackMethod());
      }

       // 获取限流
       RRateLimiter rateLimiter;
       if (!userRateLimiters.asMap().containsKey(keyAttr)) {
           rateLimiter = redisson.getRateLimiter(keyAttr);
           // 设置RateLimiter的速率,每秒发放10个令牌
           rateLimiter.trySetRate(RateType.OVERALL, blacklistInterceptor.rageLimit(), 1, RateIntervalUnit.SECONDS);
           userRateLimiters.put(keyAttr, rateLimiter);
      } else {
           rateLimiter = userRateLimiters.getIfPresent(keyAttr);
      }

       // 限流拦截
       if (rateLimiter != null && !rateLimiter.tryAcquire()) {
           if (blacklistInterceptor.protectLimit() != 0) {
               //封标识
               blacklist.put(keyAttr, blacklist.getOrDefault(keyAttr, 0L) + 1L);
               //封 IP
               blacklist.put(remoteHost, blacklist.getOrDefault(remoteHost, 0L) + 1L);
          }
           log.info("你刷这么快干嘛黑子:{}", keyAttr);
           return fallbackMethodResult(jp, blacklistInterceptor.fallbackMethod());
      }

       // 返回结果
       return jp.proceed();
  }

   private Object fallbackMethodResult(JoinPoint jp, String fallbackMethod) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
       Signature sig = jp.getSignature();
       MethodSignature methodSignature = (MethodSignature) sig;
       Method method = jp.getTarget().getClass().getMethod(fallbackMethod, methodSignature.getParameterTypes());
       return method.invoke(jp.getThis(), jp.getArgs());
  }

   /**
    * 实际根据自身业务调整,主要是为了获取通过某个值做拦截
    */

   public String getAttrValue(String attr, Object[] args) {
       if (args[0] instanceof String) {
           return args[0].toString();
      }
       String filedValue = null;
       for (Object arg : args) {
           try {
               if (StringUtils.isNotBlank(filedValue)) {
                   break;
              }
               filedValue = String.valueOf(this.getValueByName(arg, attr));
          } catch (Exception e) {
               log.error("获取路由属性值失败 attr:{}", attr, e);
          }
      }
       return filedValue;
  }

   /**
    * 获取对象的特定属性值
    *
    *
@param item 对象
    *
@param name 属性名
    *
@return 属性值
    *
@author tang
    */

   private Object getValueByName(Object item, String name) {
       try {
           Field field = getFieldByName(item, name);
           if (field == null) {
               return null;
          }
           field.setAccessible(true);
           Object o = field.get(item);
           field.setAccessible(false);
           return o;
      } catch (IllegalAccessException e) {
           return null;
      }
  }

   /**
    * 根据名称获取方法,该方法同时兼顾继承类获取父类的属性
    *
    *
@param item 对象
    *
@param name 属性名
    *
@return 该属性对应方法
    *
@author tang
    */

   private Field getFieldByName(Object item, String name) {
       try {
           Field field;
           try {
               field = item.getClass().getDeclaredField(name);
          } catch (NoSuchFieldException e) {
               field = item.getClass().getSuperclass().getDeclaredField(name);
          }
           return field;
      } catch (NoSuchFieldException e) {
           return null;
      }
  }


}

这段代码主要实现了几个方面:



  • 获取限流对象的唯一标识。如用户 Id 或者其他。

  • 将标识来获取是否触发限流 + 黑名单 如果是这两种的一种,直接触发预先设置的回调(入参要跟原本接口一致喔)。

  • 通过反射来获取回调的属性以及方法名称,触发方法调用。

  • 封禁 标识 、IP 。


代码测试


@BlacklistInterceptor(key = "title", fallbackMethod = "loginErr", rageLimit = 1L, protectLimit = 10)
   @PostMapping("/login")
   public String login(@RequestBody UrlRelateAddRequest urlRelateAddRequest) {
       log.info("模拟登录 title:{}", urlRelateAddRequest.getTitle());
       return "模拟登录:登录成功 " + urlRelateAddRequest.getTitle();
  }

   public String loginErr(UrlRelateAddRequest urlRelateAddRequest) {
       return "小黑子!你没有权限访问该接口!";
  }


  • key:需要拦截的标识,用来判断请求对象。

  • fallbackMethod:回调的方法名称(这里需要注意的是入参要跟原本接口保持一致)。

  • rageLimit:每秒限制的访问次数。

  • protectLimit:超过每秒访问次数+1,当请求超过 protectLimit 值时,进入黑名单封禁 24 小时。


以下是具体操作截图:


Snipaste_2024-05-23_11-28-41.png


到这里这个黑名单的拦截基本就实现啦,大家还有什么具体的补充点都可以提出来,一起学习一下,经过这次”恐吓风波“,让我知道互联网上的人戾气还是很重的,只要坚持好做自己,管他别人什么看法!!


作者:cong_
来源:juejin.cn/post/7371761447696121866
收起阅读 »

环信 X 星野| 共创沉浸式 AI 互动体验

大模型技术的发展使虚拟人更加智能和情感丰富,推动人与 AI 智能体互动体验进入新时代。星野App 是一款沉浸式 AI 内容社区,短短几个月日活过百万。虽然市面上的社交产品很多,但社交关系更多的是停留在表面,无法满足深层次情感交流需求。星野通过 AI 扮演驱动社...
继续阅读 »

大模型技术的发展使虚拟人更加智能和情感丰富,推动人与 AI 智能体互动体验进入新时代。星野App 是一款沉浸式 AI 内容社区,短短几个月日活过百万。虽然市面上的社交产品很多,但社交关系更多的是停留在表面,无法满足深层次情感交流需求。星野通过 AI 扮演驱动社交方式实现深层情感交流,短短几个月迅速占领社交市场。这其中环信IM通过提供高效、稳定、安全的即时通讯服务,助力星野提高用户粘性和转化率,推动人与AI智能互动体验共同发展。

在这里插入图片描述

当前社交产品的局限性

当前社交产品和用户需求之间存在显著的不匹配,尤其在深层次社交关系和真实情感交流方面。社交媒体如 Facebook 和微信,虽然用户众多,但这些平台上的关系多停留在表面,缺乏深度和真实性。很多“好友”实际上是完全不认识的人,导致人际关系的虚拟化和表层化,无法有效缓解孤独感。社交产品开发者需要更多关注如何促进真实的人际互动和情感支持,提供更人性化的产品设计,满足用户的深层次情感需求。

Z世代的影响力

Z 世代对科技和数字产品有天然亲和力和高度依赖,利用社交媒体表达观点,影响更多的网络群体。Z 世代重视品牌社会责任和道德标准,倾向支持反映其价值观的品牌,推动企业在营销和公关策略上的转变。他们对个性化需求极高,寻求能表达个人身份的定制化选项,这种需求体现在消费品和社交互动上。随着 Z 世代成为经济和文化主导力量,市场将继续向这些年轻消费者靠拢,开发符合他们需求的产品和服务。企业必须理解和适应 Z 世代的消费心理和行为模式,才能在市场竞争中占据优势。

解决情绪需求的商业潜力

解决情绪价值预示着一个潜力巨大的市场。在数字化和人工智能技术迅速发展的背景下,深层次情感交流和个性化关怀成为技术应用新前沿。AI 技术在情感识别和情感交互方面的突破,已见证初步商业化应用。AI 驱动的虚拟助手和聊天机器人在用户中受欢迎,提供陪伴和支持,在单身人群、老年人和需要情感支持的用户中尤其受欢迎。情感驱动的电商和内容平台改变传统购物和内容消费模式,通过情感分析优化的推荐系统,根据用户心情和偏好推送产品和内容,提高转化率和用户粘性。未来,将看到更多利用 AI 实现深层情感交流的创新应用,在教育、健康护理、客户服务等领域发挥重要作用。

星野App Z世代的一味社交解药

在星野App中,用户与基于 AIGC 技术创造的“智能体”之间可以实现实时沟通、互动并建立情感连接。目前,除了1对1虚拟社交,星野也在着力打造多 NPC 模式的虚拟社交,给与玩家更丰富、有趣的互动体验。

在这里插入图片描述

星野搭载的AIGC技术,可以让用户根据自己的喜好来创建智能体的人设、形象和声音,智能体的性格特质只需要通过一段简短的描述实现,并能在后续对话中不断调整强化,调教成用户所希望的性格和样子。用户可以与心目中幻想的、虚构的、想却无法触及的对象进行对话,演绎或是表达情感。在多种多样的虚拟社交场景,满足不同用户的社交需求。真正的实现千人千面,打造比肩真实世界的互动体验。

图片

人与智能体对话方案,打造千人千面专属对话

在星野App中,终端用户和智能体可以通过文字、语音、视频等多种载体来对话沟通,这种沉浸式的虚拟互动体验,背后是基于环信的 Chat AI 技术方案来实现的。通过环信提供的高效、稳定、安全的即时通讯服务,终端用户可以获得更加自然、便捷和高效的人机交互模式,从而满足用户的需求。同时,即时通讯服务还可以为应用提供数据传输、消息推送等基础服务,实现应用功能的完整性和可扩展性。

在环信方案中,通过建立一一对应的环信ID关系,形成相应的单聊会话或多人沟通场景。其中,为了进一步提升用户体验,⻓链接通道是非常重要的一个措施,可以有效地缓解移动端弱网环境下的网络延迟和不稳定性问题。

人与智能体对话方案

用户与AI智能体互动的场景非常依赖回调功能。环信丰富的消息回调和 RESTful API 相关能力,可以将星野App AI返回给环信的消息发送给终端用户,从而实现用户消息即时送达。在这一过程中,稍有延迟直接会在前端产生不好体验,这就要求环信提供更加稳定和高效的回调服务,来满足数据存储和业务运营的需求。

同时,环信即时通讯云还提供内容合规审核功能,能够对消息进行过滤,避免出现涉⻩、涉政等违规内容。该功能基于先进的算法和AI技术,在保证高效性和准确性的同时,能够精准判断和拦截不良信息,保障用户的信息安全和隐私。

智能体离线唤醒方案,有力提升用户粘性

对星野App来说,提升用户留存率和老用户激活是非常重要的运营指标。在这方面,环信的智能体离线唤醒方案提供了强有力的支持。通过该方案,即使终端用户离线或应用被关闭,也可以确保他们能够及时地收到重要的消息和提醒,从而增加使用频次和粘性,并提高用户满意度。

图片
智能体离线唤醒方案

同时,离线推送还可以帮助应用开发者实现更加智能、个性化的推送策略,根据用户行为和偏好进行定制化推送,提高推送效果和转化率。另外,环信即时通讯云的离线推送功能还具备灵活、可扩展的特点,支持多种推送方式和协议,以适应不同的业务需求和技术要求。

携手共创,打造极致即时沟通体验

在人工智能社交领域,环信IM作为领先的PaaS供应商,为星野APP的开发提供了关键支持。凭借卓越的技术实力和快速响应能力,环信IM与星野团队紧密合作,提供高效的即时通讯解决方案,大幅提升了用户体验,并加速了星野APP的市场推广。这种合作模式不仅提高了星野的运营效率,也增强了其市场竞争力。环信IM的技术专业性和创新能力在与星野APP的合作中得到了充分展现,成功打造了极致的沟通体验,赢得了用户的广泛认可。

丰富消息和会话能力:场景搭建的基石

在星野APP中,人与智能体的沟通是核心应用场景。环信IM提供了文本、音频、图片、视频等多种消息类型,极大丰富了人机沟通体验;消息编辑、删除功能满足了业务对生成内容处理的需求;离线消息和推送消息保障了移动应用场景下的用户体验;会话管理能力为人与多个智能体的沟通提供了场所和管理支持。

用户关系系统:社交关系裂变

作为社交应用,星野APP包含大量的社交关系:人与智能体的关系,人与频繁交流的智能体的关系,以及一组人与一组智能体之间的关系。环信IM的好友管理和群组管理功能,为星野APP提供了坚实的关系管理基础,增强了用户粘性。

全面的监控体系:为业务增长保驾护航

作为备受关注的AI社交应用,星野面临着业务超高速增长的挑战。环信IM的水晶球服务提供了全面的用量和质量监控告警服务,确保在用户规模快速增长的情况下,系统依然能平稳运行,为业务成功保驾护航。

高保障的回调服务:服务端消息事件同步

环信IM为星野APP提供了低延时高保障的回调服务,确保每天海量消息和事件的同步。在业务高峰时段,回调服务提供了强有力的支持;在应用服务器因异常无法接受回调时,环信IM提供高保障的回调内容存储和补发服务,确保数据的可靠性。

高质量的全球网络:丝滑的全球用户体验

面对全球复杂的网络环境,环信IM通过全球多地数据中心、公网直连、AWS-GA、SD-RTN三路智能网络路由切换,部署了近千个边缘接入节点,保障了终端用户在网络基础设施较差区域的低延时登录和消息收发,提供了极佳的用户体验。

消息流式输出:支持更多AI应用场景

AI服务正逐渐成为终端用户接入网络的主要界面,信息传输需求日益增加。环信IM结合大模型内容生成的特点,推出了消息流式传输方案,为星野APP等AI创新应用提供了多样化的解决方案,支持更多AI应用场景。

环信 IM 结合 AI 技术方案优势

海量并发

支持单日数十亿级别的消息传 输和处理,能够满足不同场景和业务需求,确保系统稳定。

精简流程海量并发

提供易用、可靠、高效的平台和会话API,让开发者专注于业务逻辑实现,减少实现难度和成本。

专线回调

提供安全、稳定、快速的回调,回调容灾机制,确保消息不丢和用户体验,带来更好交互效果。

低延时
全球平均时延小于100ms,使得用户交互过程流畅自然,并提升应用竞争力和用户满意度 。

高可用性
提供多备份、灾备恢复等技术手段,SLA 99.99%,确保系统24小时不停机、持续稳定运行。

监控保障

提供实时监测、故障排除等技术支持,确保数据安全和服务稳定性,维护系统的可靠运行。

相关文档:

收起阅读 »

程序员兼职那些事儿

最近周边发生一起程序员兼职引起的纠纷事件,作为一名资深程序员的我也做过兼职,所以不禁思考作为程序员做兼职时的一些套路,以及应该遵循的原则。 1、兼职引起的纠纷 最近笔者发现周边有些程序员常年利用上班时间做兼职工作,还拉拢一些在职同事一起参与,而且做兼职的过程...
继续阅读 »

最近周边发生一起程序员兼职引起的纠纷事件,作为一名资深程序员的我也做过兼职,所以不禁思考作为程序员做兼职时的一些套路,以及应该遵循的原则。



1、兼职引起的纠纷


最近笔者发现周边有些程序员常年利用上班时间做兼职工作,还拉拢一些在职同事一起参与,而且做兼职的过程中无意间泄露了所在公司的软件代码。后来被给所在公司发现,所在公司为了维护利益,进行了报警处理,经过一些争执后,最终双方和解,好聚好散。


2、个人很认可兼职


笔者个人是非常支持程序员朋友做兼职的,而且要尽早开启兼职事业,毕竟大部分程序员都是很普通的家庭出生,唯有通过自己更多的劳作才能创造更多的收入。(之前我经常开玩笑说,谁有钱还去做程序员啊!)


程序员做兼职还可以提升自己的技能,结实更多的朋友,开拓更多的可能性。同时这也是轻创业的一种方式,一个人一台电脑就可以开始自己的事业,说句扎心的话,确实比较适合没钱没背景的程序员朋友。


3、程序员兼职的种类



程序员兼职的种类较多,每个人根据自身情况决定。总之,只要能给别人带来价值,都可以去尝试。常见的程序员兼职种类如下:




  • 接项目:比如几个程序员朋友组队,接一些不大的项目,项目规模一般几千 ~ 几万 ~ 几十万不等。或者承接一些小工具或者小爬虫之类的项目。一般这种情况会承接熟人的项目,有些可能靠自己在闲鱼或者淘宝上的推广。

  • 做技术顾问:有些朋友技术很棒,有深度有广度,对业务也精通,可以长期给别的公司做技术顾问。不过这种类型需要个人在业内或者圈内有不错的口碑和知名度。

  • 知识付费:有朋友开玩笑说,程序员的尽头是卖课,哈哈!!!知识付费确实也是资深程序员在做的事情,比如在某些领域比较深入的朋友,会在一些付费平台上卖课。或者做私域的内容付费,比如知识星球、小报童等等。

  • 自媒体:有程序员通过开通博客或者自媒体,讲讲技术领域相关的内容,赚取广告费,后期也可能引流到私域,做一些增值付费的内容。

  • 打造小型产品:这种类型一般是做一些工具型的产品,或者某个行业的小型软件,或者维护一些开源的产品。产品的呈现形态可能是网站、APP、小程序等,然后在搭配上适当的营销推广,完成商业闭环。


4、程序员兼职的优先级


在选择兼职时,应该优先选择那些能够提升自身能力沉淀资源的事情。


这些事情不仅可以让我们学习到更多的知识(技术知识、商业知识、营销知识等等),还可以沉淀资源和拓展人脉,为未来的发展打下坚实的基础。


5、程序员兼职的自我保护



每个人的情况不同,选择的兼职种类也会有所不同。但无论选择什么样的兼职,都应该注意以下几点




  • 确保兼职时间在下班后,避免影响本职工作。本职工作一定要保质保量的完成,毕竟拿着公司的这份薪水。切莫因为公司给你安排了本职工作,让你感觉耽误了你的兼职工作,从而产生抵抗情绪。如果有紧急兼职事情非要在上班时间处理,那就不要留下痕迹。

  • 不要在兼职过程中泄露公司的任何机密信息,包括软件代码、项目计划等等。兼职期间写的代码,不要从所在公司的项目里拷贝,这样很容易引发泄密事件。不要给所在公司的竞对去做兼职。这样会让你不自觉的陷入到泄密陷阱里。

  • 不要在公司的电脑上进行兼职工作,以免留下证据或引起不必要的误会。

  • 尽量不要与兼职公司签订固定合同,以免因为兼职工作而影响自己的全职工作稳定性。如果非要签合同,建议使用家人的身份信息。


6、各方要遵守的底线


6.1、程序员的底线


程序员做兼职,大部分场景其实是处在与所在公司的对立面的,所以首要职责是完成好自己的本职工作,保证公司的正常运转和项目的顺利进行。如果因为兼职而耽误了本职工作,不仅会影响自己的职业发展,也会给公司带来损失。


一旦觉得兼职或者副业,可以全身心的投入了,笔者建议主动转向副业,副业变主业,对于自己和所在公司都是一种好的选择。有可能你的副业将来还能给所在公司带来更大的价值。笔者确实见过些好的案例:员工通过副业成功创业,然后和所在公司相互成就。


6.2、老板的胸怀


让我们换个角度思考一下。如果我们是公司的老板,我们应该如何看待员工的兼职行为呢?


作为老板,还是要保有一些胸怀和格局,允许员工自由发展。虽然老板希望员工能够全心全意地投入到工作中去,为公司创造更多的价值。但同时,老板也要理解员工需要追求个人成长兴趣满足收入创新等需求。


因此,在允许员工兼职的同时,公司也需要制定一些规范和原则,以确保公司的利益不受损害,以确保兼职行为不会给公司带来负面影响。


6.3、共赢


总之,程序员做兼职需要谨慎处理,既要追求个人成长、兴趣满足、收入创新等,也要遵守公司的规定和原则。


只有在良好的平衡和取舍中,个体的兼职公司的利益才能稳步前行,才能实现个人与公司的共赢。只有这样,我们才能在兼职和副业的道路上走得更远、更稳。


7、总结


本文主要聊了程序员做兼职时的一些套路,以及应该遵循的原则。主要内容如下:



  • 程序员做兼职的种类较多,优先做对个人能力提升较大或者能够沉淀资源的事情。

  • 一旦兼职或者副业发展起来,就可以辞去主业,全力投入副业。

  • 尽量避免站在公司的对立面,做兼职也可以正大光明的做,尽量打造个人和公司双赢的局面。

  • 如果实在无法正大光明的做,那就尽量避开一些坑点。




作者:程序员半支烟
来源:mp.weixin.qq.com/s/_bF0AspoPGdiZ-XahlS_FQ
收起阅读 »

打工人回家过年:只想休息,讨厌拜年、走亲戚、被催婚

大家好,我是杨成功。昨天楼下吃饭,听到一个女孩在打电话,声音很大,听起来很生气。原因是父母让她过年回去的时候给亲戚带礼物,女孩不愿意,和父母吵起来了。女孩说:“今年本来就没攒下钱,回家来回的车票就花了一大笔,给你们带礼物也花了不少,为啥非得给亲戚带礼物?你们别...
继续阅读 »

大家好,我是杨成功。

昨天楼下吃饭,听到一个女孩在打电话,声音很大,听起来很生气。

原因是父母让她过年回去的时候给亲戚带礼物,女孩不愿意,和父母吵起来了。

女孩说:“今年本来就没攒下钱,回家来回的车票就花了一大笔,给你们带礼物也花了不少,为啥非得给亲戚带礼物?你们别光考虑你们的面子,能不能考虑一下我,年后还要交房租...”

听到这里,我心里一痛。

作为一个资深北漂,我被戳中了。

很多人以为呆在北上广的人光鲜亮丽,实际上也只是两点一线的打工人;看起来钱赚的不少,实际上开销大到离谱,一年到头剩不下多少。

今年互联网裁员潮,一片一片地裁,搞的大家人心惶惶。好几个朋友上午还在开心地写代码,下午就被请到会议室喝茶。

有些拿不到赔偿的伙伴年底还在跑仲裁,真的很不容易。

如果连父母都不能理解的话,我实在不敢想象,这个女孩回家过年的压力有多大。

前几天有一条热搜:为什么年轻人不愿意回家过年了?

年轻人不愿意回家过年,很多父母的第一反应是不孝顺,白眼狼,在外面呆野了。

哎,谁不想回家过年啊,不回去肯定是不开心,而且不是一点点不开心,是压力重重。

可能父母认为,孩子回家过年就图个热闹,到七大姑八大姨家串门拜年,见一见亲戚朋友兄弟姐妹,喝酒吃肉聊天,好不开心。

其实不是的,真不是。就拿我来说,我回家只想睡觉嗑瓜子看电视,不洗脸不洗头谁都不见,同学聚会我都不想去。除非是几个关系极好的发小,其他任何社交局都是负担。

除了社交压力,还有经济压力。

像开头说的那个女孩一样,回一趟家要花车票钱、礼物钱、亲戚孩子压岁钱、给老人钱。赚钱了还好,如果一年没赚钱,这些人情开销就是一笔负担。

累了一整年,只想回家休息,好好过个年,结果还要看钱包。

当然还有催婚压力。

像我这个年纪,马上奔三的人,过年回家见个人就是“找对象了没”。我家人比较开明,最多开玩笑问一句,亲戚朋友问就是“明年”。

但我知道很多朋友、尤其女性朋友,过年催婚会把人逼疯。

有些父母的催婚极其致命:“快三十了还不结婚,过了三十谁要你?你不成家我都没脸出门;人家谁谁都二胎了,你到底想咋样?你对得起...”。

现在是 2024 年啊,找对象的难度不比打工挣钱低。如果再和父母吵上一架,这个年过的还有啥意思。

这一层层的压力,早把年轻人回家过年的热情打散了,过个年比上班还累。

现在能理解为啥年轻人不回家过年了吗?

对父母来说,如果孩子愿意回家过年,就别要求那么多了,人回来图个开心就好。

如果孩子在读大学,回家后就是想享受一下。你就让他睡到自然醒,让他每天蓬头垢面打游戏看电视,反正呆不了几天。

如果愿意出去走亲戚,那就带上,不愿意也别勉强。更不要动不动就要求上酒桌,给长辈敬个酒,还得提一个,真的很尴尬。

如果孩子在上班,一年已经很累了,她回家可能只想休息。父母们管好自己的嘴,少催婚,少安排相亲,少要求这要求那。

更不要说谁谁家孩子赚了多少钱,谁谁家都抱孙子了。这样大家都不舒服,开开心心过个年不好吗?

可能会有父母认为:我不催她都不上心。

想想上学的时候,天天盯着学习,不能上网,不能找对象,不能玩这玩那,结果考上985了吗?

结婚这事催不得,终身大事,你不能随便拉一个就领证吧,现在又不是70年代。

如果逼的太急,很可能孩子明年就不回来过年了,骂也没有用。

社会压力大,年轻人不比上一代轻松。多一点体贴关照,少一点要求,开心过年。

车上没网,有感而发,到此为止。


作者:杨成功
来源:juejin.cn/post/7332293353197748258
收起阅读 »

前仇旧怨一笔勾销!周鸿祎与傅盛和解背后的底层逻辑

今天我们来聊聊两位科技和AI圈都颇有名气的两位人物——周鸿祎和傅盛。这两位各自在网络安全和人工智能领域都有着显著的成就,但他们之间的恩怨纠葛也是众所周知的。不过最近,他们竟然和解坐到了一起,引发了不少人的猜测和讨论。 那么,到底是什么原因让这两位昔日的对手能...
继续阅读 »

今天我们来聊聊两位科技和AI圈都颇有名气的两位人物——周鸿祎和傅盛。这两位各自在网络安全和人工智能领域都有着显著的成就,但他们之间的恩怨纠葛也是众所周知的。不过最近,他们竟然和解坐到了一起,引发了不少人的猜测和讨论。



那么,到底是什么原因让这两位昔日的对手能够放下过往,重新坐到一起呢?接下来,我就从他们的恩怨历史和当前的商业逻辑来给大家分析分析。


首先,让我们来回顾一下两人的恩怨历史。


360安全卫士大家可能都用过吧,其首创的免费模式把一众杀毒软件公司都干死了,以前可以说是电脑的必装应用,当然也因为难以卸载,一直背负流氓软件的称号。


5g3ogX0Ap9FbTZYBuTWYMLmnOgFtWnu0z4qHyTIFoem4N1575941237193compressflag.jpg


周鸿祎,360公司的创始人,曾经是中国互联网安全界的领军人物,因免费杀毒和大战QQ而一战成名。而傅盛,曾经是周鸿祎的爱将,早年在360公司担任高管,负责360安全卫士产品,对360的成长有着不可磨灭的贡献。然而好景不长,由于理念和发展方向上的分歧,傅盛最终离开了360,创办了猎豹移动,开启了自己的创业之路。此后,两人在商业上多有摩擦,甚至公开在媒体上互相指责,成为了科技圈内的一个热门话题。



但是,科技圈的水很深,商业的世界里没有永远的敌人,只有永远的利益。现在,我们看到周鸿祎和傅盛重新坐到了一起,背后一定有着他们共同关注的商业逻辑。


现在的互联网世界,AI成了新的风口。周鸿祎的360,虽然在安全领域深耕多年,但随着互联网环境的变化,传统的安全产品和服务正面临着增长的瓶颈。而傅盛的猎豹移动,虽然在移动应用方面有所建树,但在AI领域的竞争也是异常激烈。AI和大数据的结合,正成为推动各行各业发展的新引擎,这无疑是两人和解坐到一起的一个重要原因。


周鸿祎拥有庞大的用户基础和数据积累,而傅盛在AI领域的探索也有着自己的独到见解。两人的合作,可以说是强强联合,360可以利用AI技术为用户提供更智能的安全解决方案,而猎豹移动也可以借助360的用户基础,扩大自己在AI领域的应用场景。这样的合作,对双方来说都是一次难得的发展机遇。



而且还有一个很重要的问题,现在是网红经济时代,谁吸引到了流量,谁就能挣到钱。两个人的和解也必定会收获很多的流量,让更多的人了解到他们,关注他们。在这一点上,两个人都不傻,都知道流量的价值。


当然,和解坐到一起并不意味着以前的恩怨就此烟消云散,科技圈的合作与竞争往往是同步进行的。这次的和解,也许只是双方为了共同的商业利益所做出的战略选择。未来他们是否能够真正放下过往,携手共进,还需要时间来证明。


最后,这次周鸿祎和傅盛的和解,不仅仅是两个人的事,也是整个科技圈发展趋势的一个缩影。在AI成为新的竞争焦点的今天,许多科技企业都在寻求转型和突破,合作成为了一种新的生存策略。那么,这是否意味着我们将会看到更多科技领袖之间的联合与和解呢?又或者,这只是一场精心策划的商业秀,背后隐藏着更深层次的商业计算呢?这些问题,都值得我们继续关注和思考。


好了,今天的内容就到这里。对于周鸿祎和傅盛的和解,你有什么看法呢?欢迎在评论区留言讨论,我们下次再见!


作者:萤火架构
来源:juejin.cn/post/7328366295090888742
收起阅读 »

一条SQL差点引发离职

文章首发于微信公众号:云舒编程 关注公众号获取: 1、大厂项目分享 2、各种技术原理分享 3、部门内推 背景        最近组里的小伙伴在开发一个更新功能时踩了MySQL的一个类型转换的坑,差点造成线上故障。 本来是一个很简单的逻辑,就是根据唯一的id去...
继续阅读 »

文章首发于微信公众号:云舒编程

关注公众号获取:
1、大厂项目分享
2、各种技术原理分享
3、部门内推



背景


       最近组里的小伙伴在开发一个更新功能时踩了MySQL的一个类型转换的坑,差点造成线上故障。

本来是一个很简单的逻辑,就是根据唯一的id去更新对应的MySQL数据,代码简化后如下:


var updates []*model.Goods
for id, newGoods := range update {
 if err := model.GetDB().Model(&model.Goods{}).Where("id = ?", id).Updates(map[string]interface{}{
  "selling_price":  newGoods.SellingPrice,
  "sell_type":      newGoods.SellType,
  "status":         newGoods.Status,
  "category_id":    newGoods.CategoryID,
 }).Error; err != nil {
  return nil, err
 }
}

很明显,updates[]model.Goods\color{red}{updates []*model.Goods}本来应该是想声明为 map[string]model.Goods\color{red}{map[string]*model.Goods}类型的,然后key是唯一id。这样下面的更新逻辑才是对的,否则拿到的id其实是数组的下标。

但是code review由于跟着一堆代码一起评审了,并且这段更新很简单,同时测试的时候也测试过了(能测试通过也是“机缘巧合”),所以没有发现这段异常。

发到线上后,进行了灰度集群的测试,这个时候发现只要调用了这个接口,灰度集群的数据全部都变成了一样,回滚后正常。


分析


       回滚后在本地进行复现,由于本地环境是开启了SQL打印的,于是看到了这么一条SQL:很明显是拿数组的下标去比较了


update db_name set selling_price = xx,sell_type = xx where id = 0;

       由于我们的id是全部是通过uuid生成的,所以下意识的认为这条sql应该啥也不会更新才对,但是本地的确只执行了这条sql,没有别的sql,并且db中的数据全部都被修改了。

这个时候想起福尔摩斯的名言“排除一切不可能的,剩下的即使再不可能,那也是真相”\color{blue}{“排除一切不可能的,剩下的即使再不可能,那也是真相”} ,于是抱着试一试的心态直接拿这条sql去db控制台执行了一遍,发现果然所有的数据又都被修改了。

也就是 whereid=0\color{red}{where id = 0}  这个条件对于所有的记录都是恒为true,就会导致所有记录都被更新。在这个时候,想起曾经看到过MySQL对于不同类型的比较会有 【隐式转换】\color{red}{【隐式转换】},难道是这个原因导致的?


隐式转换规则


在MySQL官网找到了不同类型比较的规则:



最后一段的意思是:对于其他情况,将按照浮点(双精度)数进行比较。例如,字符串和数字的比较就按照浮点数规则进行比较。

也就是id会首先被转换成浮点数,然后再跟0进行比较。


MySQL字符转为浮点数时会按照如下规则进行:


1.如果字符串的第一个字符就是非数字的字符,那么转换结果就是0;

2.如果字符串以数字开头:

(1)如果字符串都是数字,转换结果就是整个字符串对应的数字;

(2)如果字符串中存在非数字,转换结果就是开头的那些数字对应的值;

举例说明:

"test" -> 0

"1test" -> 1

"12test12" -> 12

由于我们生成的uuid没有数字开头的字符串,于是都会转变成0。那么这条SQL就变成了:


update db_name set selling_price = xx,sell_type = xx where 0 = 0;

就恒为true了。

修复就很简单了,把取id的逻辑改成正确的就行。


为什么测试环境没有发现


       前面有提到这段代码在测试环境是测试通过了的,这是因为开发和测试同学的环境里都只有一条记录,每次更新他发现都能正常更新就认为是正常的了。同时由于逻辑太简单了,所以都没有重视这块的回归测试。

幸好在灰度集群就发现了这个问题,及时进行了回滚,如果发到了线上影响了用户数据,可能就一年白干了。


最后


代码无小事,事事需谨慎啊。一般致命问题往往是一行小小的修改导致的。


作者:云舒编程
来源:juejin.cn/post/7275550679790960640
收起阅读 »

Android 沉浸式状态栏,透明状态栏 采用系统api,超简单近乎完美的实现

前言 沉浸式的适配有多麻烦,相信大家既然来搜索这个,就说明都在为此苦恼,那么看看这篇文章吧,也许对你有所帮助(最下面有源码链接) 有写的不对的地方,欢迎指出 从adnroid 6.0开始,官方逐渐完善了这方面的api,直到android 11... ... 让...
继续阅读 »

前言


沉浸式的适配有多麻烦,相信大家既然来搜索这个,就说明都在为此苦恼,那么看看这篇文章吧,也许对你有所帮助(最下面有源码链接)


有写的不对的地方,欢迎指出


从adnroid 6.0开始,官方逐渐完善了这方面的api,直到android 11...


... 让我们直接开始吧


导入核心包


老项目非androidx的请自行研究下,这里使用的是androidx,并且用的kotlin语言
本次实现方式跟windowInsets息息相关,这可真是个好东西
首先是需要导入核心包
androidx.core:core

kotlin可选择导入这个:
androidx.core:core-ktx
我用的版本是
androidx.core:core-ktx:1.12.0

开启 “沉浸式” 支持


沉浸式原本的意思似乎是指全屏吧。。。算了,不管那么多,喊习惯了 沉浸式状态栏,就这么称呼吧。

在activity 的oncreate里调用
//将decorView的fitSystemWindows属性设置为false
WindowCompat.setDecorFitsSystemWindows(window, false)
//设置状态栏颜色为透明
window.statusBarColor = Color.TRANSPARENT
//是否需要改变状态栏上的 图标、字体 的颜色
//获取InsetsController
val insetsController = WindowCompat.getInsetsController(window, window.decorView)
//mask:遮罩 默认是false
//mask = true 状态栏字体颜色为黑色,一般在状态栏下面的背景色为浅色时使用
//mask = false 状态栏字体颜色为白色,一般在状态栏下面的背景色为深色时使用
var mask = true
insetsController.isAppearanceLightStatusBars = mask
//底部导航栏是否需要修改
//android Q+ 去掉虚拟导航键 的灰色半透明遮罩
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
//设置虚拟导航键的 背景色为透明
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//8.0+ 虚拟导航键图标颜色可以修改,所以背景可以用透明
window.navigationBarColor = Color.TRANSPARENT
} else {
//低版本因为导航键图标颜色无法修改,建议用黑色,不要透明
window.navigationBarColor = Color.BLACK
}
//是否需要修改导航键的颜色,mask 同上面状态栏的一样
insetsController.isAppearanceLightNavigationBars = mask

修改 状态栏、虚拟导航键 的图标颜色,可以在任意需要的时候设置,防止图标和字体颜色和背景色一致导致看不清

补充一下:
状态栏和虚拟导航栏的背景色要注意以下问题:
1.在低于6.0的手机上,状态栏上的图标、字体颜色是白色且不支持修改的,MIUI,Flyme这些除外,因为它们有自己的api能实现修改颜色
2.在低于8.0的手机上,虚拟导航栏的图标、字体颜色是白色且不支持修改的,MIUI,Flyme这些除外,因为他们有自己的api能实现修改颜色
解决方案:
低于指定版本的系统上,对应的颜色就不要用透明,除非你的APP页面是深色背景,否则,建议采用半透明的灰色

在带有刘海或者挖孔屏上,横屏时刘海或者挖孔的那条边会有黑边,解决方法是:
给APP的主题v27加上
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
参考图:

image.png


监听OnApplyWindowInsetsListener


//准备一个boolean变量 作为是否在跑动画的标记
var flagProgress = false

//这里可以使用decorView或者是任意view
val view = window.decorView

//监听windowInsets变化
ViewCompat.setOnApplyWindowInsetsListener(view) { view: View, insetsCompat: WindowInsetsCompat ->
//如果要配合下面的setWindowInsetsAnimationCallback一起用的话,一定要记得,onProgress的时候,这里做个拦截,直接返回 insets
if (flagProgress) return@setOnApplyWindowInsetsListener insetsCompat
//在这里开始给需要的控件分发windowInsets

//最后,选择不消费这个insets,也可以选择消费掉,不在往子控件分发
insetsCompat
}
//带平滑过渡的windowInsets变化,ViewCompat中的这个,官方提供了 api 21-api 29的支持,本来这个只支持 api 30+的,相当不错!
//启用setWindowInsetsAnimationCallback的同时,也必须要启用上面的setOnApplyWindowInsetsListener,否则在某些情况下,windowInsets改变了,但是因为不会触发setWindowInsetsAnimationCallback导致padding没有更新到UI上
//DISPATCH_MODE_CONTINUE_ON_SUBTREE这个代表动画事件继续分发下去给子View
ViewCompat.setWindowInsetsAnimationCallback(view, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
override fun onProgress(insetsCompat: WindowInsetsCompat, runningAnimations: List<WindowInsetsAnimationCompat>): WindowInsetsCompat {
//每一帧的windowInsets
//可以在这里分发给需要的View。例如给一个聊天窗口包含editText的布局设置这个padding,可以实现键盘弹起时,在底部的editText跟着键盘一起滑上去,参考微信聊天界面,这个比微信还丝滑(android 11+最完美)。
//最后,直接原样return,不消费
return insetsCompat
}

override fun onEnd(animation: WindowInsetsAnimationCompat) {
super.onEnd(animation)
//动画结束,将标记置否
flagProgress = false
}

override fun onPrepare(animation: WindowInsetsAnimationCompat) {
super.onPrepare(animation)
//动画准备开始,在这里可以记录一些UI状态信息,这里将标记设置为true
flagProgress = true
}
})

读取高度值


通过上面的监听,我们能拿到WindowInsetsCompat对象,现在,我们从这里面取到我们需要的高度值


先定义几个变量,我们需要拿的包含:
1. 刘海,挖空区域所占据的宽度或者是高度
2. 被系统栏遮挡的区域
3. 被输入法遮挡的区域

//cutoutPadding 刘海,挖孔区域的padding
var cutoutPaddingLeft = 0
var cutoutPaddingTop = 0
var cutoutPaddingRight = 0
var cutoutPaddingBottom = 0

//获取刘海,挖孔的高度,因为这个不是所有手机都有,所以,需要判空
insetsCompat.displayCutout?.let { displayCutout ->
cutoutPaddingTop = displayCutout.safeInsetTop
cutoutPaddingLeft = displayCutout.safeInsetLeft
cutoutPaddingRight = displayCutout.safeInsetRight
cutoutPaddingBottom = displayCutout.safeInsetBottom
}


//systemBarPadding 系统栏区域的padding
var systemBarPaddingLeft = 0
var systemBarPaddingTop = 0
var systemBarPaddingRight = 0
var systemBarPaddingBottom = 0

//获取系统栏区域的padding
//系统栏 + 输入法
val systemBars = insetsCompat.getInsets(WindowInsetsCompat.Type.ime() or WindowInsetsCompat.Type.systemBars())
//左右两侧的padding通常直接赋值即可,如果横屏状态下,虚拟导航栏在侧边,那么systemBars.left或者systemBars.right的值就是它的宽度,竖屏情况下,一般都是0
systemWindowInsetLeft = systemBars.left
systemWindowInsetRight = systemBars.right
//这里判断下输入法 和 虚拟导航栏是否存在,如果存在才设置paddingBottom
if (insetsCompat.isVisible(WindowInsetsCompat.Type.ime()) || insetsCompat.isVisible(WindowInsetsCompat.Type.navigationBars())) {
systemWindowInsetBottom = systemBars.bottom
}
//同样判断下状态栏
if (insetsCompat.isVisible(WindowInsetsCompat.Type.statusBars())) {
systemWindowInsetTop = systemBars.top
}

到这里,我们需要的信息已经全部获取到了,接下来就是根据需求,设置padding属性了

补充一下:
我发现在低于android 11的手机上,insets.isVisible(Type)返回始终为true
并且,即使系统栏被隐藏,systemBars.top, systemBars.bottom也始终会有高度
所以这里


保留原本的Padding属性


上述获取的值,直接去设置padding的话,会导致原本的padding属性失效,所以我们需要在首次设置监听,先保存一份原本的padding属性,在最后设置padding的时候,把这份原本的padding值加上即可,就不贴代码了。


第一次写文章,写的粗糙了点

可能我写的不太好,没看懂也没关系,直接去看完整代码吧


我专门写了个小工具,可以去看看:
沉浸式系统栏 小工具


如果有更好的优化方案,欢迎在github上提出,我们一起互相学习!


作者:Matchasxiaobin
来源:juejin.cn/post/7275943802938130472
收起阅读 »

系统干崩了,只认代码不认人

各位朋友听我一句劝,写代码提供方法给别人调用时,不管是内部系统调用,还是外部系统调用,还是被动触发调用(比如MQ消费、回调执行等),一定要加上必要的条件校验。千万别信某些同事说的这个条件肯定会传、肯定有值、肯定不为空等等。这不,临过年了我就被坑了一波,弄了个生...
继续阅读 »

各位朋友听我一句劝,写代码提供方法给别人调用时,不管是内部系统调用,还是外部系统调用,还是被动触发调用(比如MQ消费、回调执行等),一定要加上必要的条件校验。千万别信某些同事说的这个条件肯定会传、肯定有值、肯定不为空等等。这不,临过年了我就被坑了一波,弄了个生产事故,年终奖基本是凉了半截。


为了保障系统的高可用和稳定,我发誓以后只认代码不认人。文末总结了几个小教训,希望对你有帮助。


一、事发经过


我的业务场景是:业务A有改动时,发送MQ,然后应用自身接受到MQ后,再组合一些数据写入到Elasticsearch。以下是事发经过:



  1. 收到一个业务A的异常告警,当时的告警如下:



  2. 咋一看觉得有点奇怪,怎么会是Redis异常呢?然后自己连了下Redis没有问题,又看了下Redis集群,一切正常。所以就放过了,以为是偶然出现的网络问题。

  3. 然后技术问题群里 客服 反馈有部分用户使用异常,我警觉性的感觉到是系统出问题了。赶紧打开了系统,确实有偶发性的问题。

  4. 于是我习惯性的看了几个核心部件:



    1. 网关情况、核心业务Pod的负载情况、用户中心Pod的负载情况。

    2. Mysql的情况:内存、CPU、慢SQL、死锁、连接数等。



  5. 果然发现了慢SQL和元数据锁时间过长的情况。找到了一张大表的全表查询,数据太大,执行太慢,从而导致元数据锁持续时间太长,最终数据库连接数快被耗尽。


SELECT xxx,xxx,xxx,xxx FROM 一张大表


  1. 立马Kill掉几个慢会话之后,发现系统仍然没有完全恢复,为啥呢?现在数据库已经正常了,怎么还没完全恢复呢?又继续看了应用监控,发现用户中心的10个Pod里有2个Pod异常了,CPU和内存都爆了。难怪使用时出现偶发性的异常呢。于是赶紧重启Pod,先把应用恢复。

  2. 问题找到了,接下来就继续排查为什么用户中心的Pod挂掉了。从以下几个怀疑点开始分析:



    1. 同步数据到Elasticsearch的代码是不是有问题,怎么会出现连不上Redis的情况呢?

    2. 会不会是异常过多,导致发送异常告警消息的线程池队列满了,然后就OOM?

    3. 哪里会对那张业务A的大表做不带条件的全表查询呢?



  3. 继续排查怀疑点a,刚开始以为:是拿不到Redis链接,导致异常进到了线程池队列,然后队列撑爆,导致OOM了。按照这个设想,修改了代码,升级,继续观察,依旧出现同样的慢SQL 和 用户中心被干爆的情况。因为没有异常了,所以怀疑点b也可以被排除了。

  4. 此时基本可以肯定是怀疑点c了,是哪里调用了业务A的大表的全表查询,然后导致用户中心的内存过大,JVM来不及回收,然后直接干爆了CPU。同时也是因为全表数据太大,导致查询时的元数据锁时间过长造成了连接不能够及时释放,最终几乎被耗尽。

  5. 于是修改了查询业务A的大表必要校验条件,重新部署上线观察。最终定位出了问题。


二、问题的原因


因为在变更业务B表时,需要发送MQ消息( 同步业务A表的数据到ES),接受到MQ消息后,查询业务A表相关连的数据,然后同步数据到Elasticsearch。


但是变更业务B表时,没有传业务A表需要的必要条件,同时我也没有校验必要条件,从而导致了对业务A的大表的全表扫描。因为:


某些同事说,“这个条件肯定会传、肯定有值、肯定不为空...”,结果我真信了他!!!

由于业务B表当时变更频繁,发出和消费的MQ消息较多,触发了更多的业务A的大表全表扫描,进而导致了更多的Mysql元数据锁时间过长,最终连接数消耗过多。


同时每次都是把业务A的大表查询的结果返回到用户中心的内存中,从而触发了JVM垃圾回收,但是又回收不了,最终内存和CPU都被干爆了。


至于Redis拿不到连接的异常也只是个烟雾弹,因为发送和消费的MQ事件太多,瞬时间有少部分线程确实拿不到Redis连接。


最终我在消费MQ事件处的代码里增加了条件校验,同时也在查询业务A表处也增加了的必要条件校验,重新部署上线,问题解决。


三、总结教训


经过此事,我也总结了一些教训,与君共勉:



  1. 时刻警惕线上问题,一旦出现问题,千万不能放过,赶紧排查。不要再去怀疑网络抖动问题,大部分的问题,都跟网络无关。

  2. 业务大表自身要做好保护意识,查询处一定要增加必须条件校验。

  3. 消费MQ消息时,一定要做必要条件校验,不要相信任何信息来源。

  4. 千万别信某些同事说,“这个条件肯定会传、肯定有值、肯定不为空”等等。为了保障系统的高可用和稳定,咱们只认代码不认人

  5. 一般出现问题时的排查顺序:



    1. 数据库的CPU、死锁、慢SQL。

    2. 应用的网关和核心部件的CPU、内存、日志。



  6. 业务的可观测性和告警必不可少,而且必须要全面,这样才能更快的发现问题和解决问题。




作者:程序员半支烟
来源:mp.weixin.qq.com/s/TvIpTZq0XO8v9ccYSsM37Q
收起阅读 »

那些年走岔的路,一个人总要为自己的认知买单!

前天晚上彻夜难眠,翻来覆去,直到差不多凌晨四点才睡着,早上八点就起床上班了,很久都没有失眠了,失眠真的让人很痛苦。 回想起一些往事,自己做对了一些选择,但是也做错了很多选择,我想这大概就是人生,现在回想起来,不曾后悔,只有总结! 一 大四下学期我们就离开学校了...
继续阅读 »

前天晚上彻夜难眠,翻来覆去,直到差不多凌晨四点才睡着,早上八点就起床上班了,很久都没有失眠了,失眠真的让人很痛苦。


回想起一些往事,自己做对了一些选择,但是也做错了很多选择,我想这大概就是人生,现在回想起来,不曾后悔,只有总结!



大四下学期我们就离开学校了,加上寒假的两个月,实际上我们的实习期有半年多,但是找工作应该是大四上学期就开始了。


那时候彪哥整天都在面试,积累了不少面试经验,也学习了不少知识,而那时候我鬼迷心窍,去做项目去了。


因为一些巧合,我加入了一个SAAS软件开发的小团队,做的是酒店方面的业务,我是远程办公,那段时间一边做毕设,一边做项目,但是做毕设的时间很少,因为论文就花了五天时间去写,更多是在做酒店项目。


现在我有一部分读者都是从我的区块链毕设过来的,我想对你们说一声,感谢你们的付费,但是也想对你们说一声对不起,如果当时我专心去做毕设,或许呈现在你们眼前的作品会更好,但是时间不能重来!


但是后来我仔细思考,我既不应该花时间去做毕设,也不应该为了点钱去做项目!


纵使我的毕设得了优秀毕设,算是我们那一届最优秀的毕设,但是并没有什么卵用,你的简历并不会因为一个优秀毕设而变得多么耀眼。


为了一点钱去做项目也不理智,因为一个人的时间是有限的,当把时间碎片化后,就很难集中去做一件事了,当时虽然说给我6k一个月,但是因为很多东西不熟悉,所以现去学,像uniapp都去学了,所以功能完成度和质量不高,一个月只给我结了3000不到!


干了两个月我们就毕业了,我收拾行李就回家了。



回到家里后,他们说直接给我一个单独项目做,也是一个SAAS的系统,说开发周期2个月,5万块钱,我当时心里想,一个月两万多,我直接不去实习了,安心干,干完我还可以玩几个月,这他妈多好啊。


于是我就接下来了,就开始进入coding状态,白天干,晚上干,后面在家里呆烦了,又跑回学校去。


在学校呆了半个多月,我做了50%,于是迫于经济压力,又回家了,回家最起码不愁饭吃。


图片


那时候,我把自己定义为一个自由职业者,我也挺享受这样的生活,coding累了,就出去走走,回来后又继续coding,说实话,还挺享受!


那时候基本上大多同学都出去实习了,有些去了很不错的互联网公司,听他们说公司又是用什么牛逼的技术了,心里就突然有点羡慕。


但是想到项目做完马上能拿到钱了,就没有去羡慕了。


两个月时间很快到了,老板准时来验收了,不过一验bug足足提了几百个,还有很多变更,老板说尽快改完!


当时我有点懵,不应该先给我点钱吗?


我就说先付40%给我,但是人家说,你这玩意用起来到处是问题,无法用啊,怎么给钱?


我无话可说,拿不到钱,心里更加焦虑了,想不干了,那么就前功尽弃,如果继续干,问题越来越多,变更越来越多,思来想去,最后还是硬着头皮干了!


陆陆续续又干了半个多月,这时候二验又开始了,老板说这次稍微好了一点,但是也无法用啊,于是叫我把代码上传到他们仓库,然后给我付3000块钱,开发完后再一起结,我自然不愿意。


我想,代码给你了,你不理我了怎么办,所以我还是想等开发完以后拿到钱再交代码。


这时候我干了快三个月了,心里虽然看到一点希望,但是更多的是焦虑,因为再有几个月了就要毕业了,而我还没有去实习!


父母也开始念叨,心里的压力就更大了,我想,再干半个月,还拿不了钱,我真的就不干了。


我又继续做,为了快速做完,很多东西我都是没有考虑的,所以问题自然也多,特别还有硬件对接,还有一些复杂的操作。


说实话,这东西暂时肯定是用不了的,但是为了能拿到钱,我也带有一点骗的成分在里面,偷工减料,以为人家看不出来,实际上别人比你精多!


很多项目二验不通过,那基本就烂尾了,但是老板说,来个三验,果然还是用不了,问题很多,所以依然没拿到钱。


心里更加烦躁了,后面我直接说要么给钱,要么不做了,心里彻底崩溃了,心里后悔,为啥要去接这个项目,为啥浪费这么多时间,为啥不去实习。


后面老板说,如果你不想开发了也可以,把代码交出来,给你5000块钱,后面你和别人一起协同开发,不用全职开发。


我心里是抗拒的,干了这么久才几千块钱,心有不甘,不过过了几天,因为经济压力,所以还是选择交出代码了,谈成了6000块钱。


因为我知道他们会一直加需求,一直在变更,是一个无底洞!


三个多月,就得了6000块钱,心里别提多难受,不过好在暂时有点钱用。


于是直接就不干了,在家里呆了几天就开始投简历了,只有三个月不到就毕业了,所以自然去不了外面了,于是只能在省会城市找实习了。


还好那时候面试机会还挺多,一个星期不到就入职了,6000块钱的实习,就去干了,说实话,一个三线城市,也只能开这么多了!


不过现在这种就业环境,如果学历背景没有占优势,三线城市找6000以上的实习,还是比较难的,这两年市场真的比较低迷了!


“自由职业者“的那段时间,大概是我这么多年来最煎熬的时光,因为总是在希望和失望中来回穿梭。


后来我在书中看到一段话,“如果命运给你一次机会,哪怕是一根稻草,你也要牢牢抓住”,显然那个时候我的认知比较低,认为那就是命运的稻草,但是实际上那不是,那是荆棘!


当你的认知和能力都不够的时候,就算钱摆在你面前你都拿不了。



落笔到这里,心里不禁泛起一阵酸楚!


一个人总要为自己的认知买单的,因为在很黄金的时间阶段,我去做了不太正确的选择,虽然不曾后悔,但是我知道那是不理智的选择。


这段回忆虽然会成为我人生的阅历,甚至可以说是一种财富,但是他终归是一个教训,不值得提倡!



在大四上学期,应该快速把毕设做完,然后进入复习,投简历,即使找不到工作,也能锻炼面试能力,对自己的知识体系进行查缺补漏!


优秀毕设,论文,这些在本科阶段实际上没什么卵用,不过是教育的一个考核而已。


在校期间,那些社团活动,学生会并不能为你将来的职业发展发挥多大的作用,切勿过于沉迷!


眼前的小钱是陷阱,在未来很快就能赚回来!


在学校期间,兼职是完全没有必要的,因为赚不了几个钱,但是却花费了大量的时间,学生时期正是学习知识的时候,浪费了就没有了。


因为把只是学扎实,这点钱等毕业后一个月就能全部赚回来,但是如果浪费了,将要用很多时间去弥补,这时候你已经落后于别人很多了!


虽然我去做项目也能锻炼自己的能力,但是时机不对,如果大三去做那么没问题,但是在临近毕业之际去做,这就是不理智的。



学生时代,对于项目我们是没有风险把控能力的,也不清楚项目的流程,所以能赚到钱的几率不大!


我浪费了三四个月的时间去做一个项目这是不理智的,首先单干很有局限性,因为独木不成舟,你很多东西考虑不到位,所以会有很多漏洞。


还有你不能学习优秀的人的逻辑,实际上你是处于一个封闭的状态。


我觉得正确的做法是应该找一个不错的公司进去学习,融入团队,这样才能真的学到东西。


天真的是,我当时还想将其打造成一个产品,然后进行创业!


后来想想,自己如果真的投入时间去做了,那么不仅赚不到钱,可能还会饿肚子。


不用说什么不去试试怎么知道。


当你的认知跟不上的时候,你所想的,所做的,基本上都不会成功,不要想着幸运之神降临在你的身上。



那年,我傻逼地把自己定义为自由职业者。


实际上我连边都沾不上,因为没有赚到钱,还谈什么自由,叫“烂账职业者”还差不多。


今天,我们总是去羡慕那些自由职业者每天不用上班也能赚钱,实际上和你看到的不一样。


自由职业者赚到钱的人只有少数,但是都是经历过很多尝试,认知得到飞跃地提升后才成的。


不过可以肯定的是,未来自由职业者会越来越多,个人IP也将在未来大爆发。


布局是我们该做的事。


种一棵树最好的时间是十年前,其次是现在。



以上也就是对于过去的一些反思,我从来不去抱怨过去,只是去思考自己。


因为每一条路都没有对错,只能说很多时候选择大于努力。


路走岔了的时候要及时止损,不要一头黑走到底,这样对自己不好。


对于未来,还是得比较理性去看待,虽然充满各种不确定性,但是很多确定性的东西我们是能看到的。


行文至此,已经凌晨2点!


作者:苏格拉的底牌
来源:juejin.cn/post/7306143755585486848
收起阅读 »

MQ消息积压,把我整吐血了

大家好,我是苏三,又跟大家见面了。 前言 我之前在一家餐饮公司待过两年,每天中午和晚上用餐高峰期,系统的并发量不容小觑。为了保险起见,公司规定各部门都要在吃饭的时间轮流值班,防止出现线上问题时能够及时处理。 我当时在后厨显示系统团队,该系统属于订单的下游业务...
继续阅读 »

大家好,我是苏三,又跟大家见面了。



前言


我之前在一家餐饮公司待过两年,每天中午和晚上用餐高峰期,系统的并发量不容小觑。为了保险起见,公司规定各部门都要在吃饭的时间轮流值班,防止出现线上问题时能够及时处理。


我当时在后厨显示系统团队,该系统属于订单的下游业务。


用户点完菜下单后,订单系统会通过发kafka消息给我们系统,系统读取消息后,做业务逻辑处理,持久化订单和菜品数据,然后展示到划菜客户端。


这样厨师就知道哪个订单要做哪些菜,有些菜做好了,就可以通过该系统出菜。系统自动通知服务员上菜,如果服务员上完菜,修改菜品上菜状态,用户就知道哪些菜已经上了,哪些还没有上。这个系统可以大大提高后厨到用户的效率。图片


这一切的关键是消息中间件:kafka,如果它出现问题,将会直接影响到后厨显示系统的用户功能使用。


这篇文章跟大家一起聊聊,我们当时出现过的消息积压问题,希望对你会有所帮助。


1 第一次消息积压


刚开始我们的用户量比较少,上线一段时间,mq的消息通信都没啥问题。


随着用户量逐步增多,每个商家每天都会产生大量的订单数据,每个订单都有多个菜品,这样导致我们划菜系统的划菜表的数据越来越多。


在某一天中午,收到商家投诉说用户下单之后,在平板上出现的菜品列表有延迟。


厨房几分钟之后才能看到菜品。


我们马上开始查原因。


出现这种菜品延迟的问题,必定跟kafka有关,因此,我们先查看kafka。


果然出现了消息积压


通常情况下,出现消息积压的原因有:



  1. mq消费者挂了。

  2. mq生产者生产消息的速度,大于mq消费者消费消息的速度。


我查了一下监控,发现我们的mq消费者,服务在正常运行,没有异常。


剩下的原因可能是:mq消费者消费消息的速度变慢了。


接下来,我查了一下划菜表,目前不太多只有几十万的数据。


看来需要优化mq消费者的处理逻辑了。


我在代码中增加了一些日志,把mq消息者中各个关键节点的耗时都打印出来了。


发现有两个地方耗时比较长:



  1. 有个代码是一个for循环中,一个个查询数据库处理数据的。

  2. 有个多条件查询数据的代码。


于是,我做了有针对性的优化。


将在for循环中一个个查询数据库的代码,改成通过参数集合,批量查询数据。


有时候,我们需要从指定的用户集合中,查询出有哪些是在数据库中已经存在的。


实现代码可以这样写:


public List<User> queryUser(List<User> searchList) {
    if (CollectionUtils.isEmpty(searchList)) {
        return Collections.emptyList();
    }

    List<User> result = Lists.newArrayList();
    searchList.forEach(user -> result.add(userMapper.getUserById(user.getId())));
    return result;
}

这里如果有50个用户,则需要循环50次,去查询数据库。我们都知道,每查询一次数据库,就是一次远程调用。


如果查询50次数据库,就有50次远程调用,这是非常耗时的操作。


那么,我们如何优化呢?


具体代码如下:


public List<User> queryUser(List<User> searchList) {
    if (CollectionUtils.isEmpty(searchList)) {
        return Collections.emptyList();
    }
    List<Long> ids = searchList.stream().map(User::getId).collect(Collectors.toList());
    return userMapper.getUserByIds(ids);
}

提供一个根据用户id集合批量查询用户的接口,只远程调用一次,就能查询出所有的数据。


多条件查询数据的地方,增加了一个联合索引,解决了问题。


这样优化之后, mq消费者处理消息的速度提升了很多,消息积压问题被解决了。


2 第二次消息积压


没想到,过了几个月之后,又开始出现消息积压的问题了。


但这次是偶尔会积压,大部分情况不会。


这几天消息的积压时间不长,对用户影响比较小,没有引起商家的投诉。


我查了一下划菜表的数据只有几百万。


但通过一些监控,和DBA每天发的慢查询邮件,自己发现了异常。


我发现有些sql语句,执行的where条件是一模一样的,只有条件后面的参数值不一样,导致该sql语句走的索引不一样。


比如:order_id=123走了索引a,而order_id=124走了索引b。


有张表查询的场景有很多,当时为了满足不同业务场景,加了多个联合索引。


MySQL会根据下面几个因素选择索引:



  1. 通过采样数据来估算需要扫描的行数,如果扫描的行数多那可能io次数会更多,对cpu的消耗也更大。

  2. 是否会使用临时表,如果使用临时表也会影响查询速度;

  3. 是否需要排序,如果需要排序则也会影响查询速度。


综合1、2、3以及其它的一些因素,MySql优化器会选出它自己认为最合适的索引。


MySQL优化器是通过采样来预估要扫描的行数的,所谓采样就是选择一些数据页来进行统计预估,这个会有一定的误差。


由于MVCC会有多个版本的数据页,比如删除一些数据,但是这些数据由于还在其它的事务中可能会被看到,索引不是真正的删除,这种情况也会导致统计不准确,从而影响优化器的判断。


上面这两个原因导致MySQL在执行SQL语句时,会选错索引


明明使用索引a的时候,执行效率更高,但实际情况却使用了索引b。


为了解决MySQL选错索引的问题,我们使用了关键字force index,来强制查询sql走索引a。


这样优化之后,这次小范围的消息积压问题被解决了。


3 第三次消息积压


过了半年之后,在某个晚上6点多钟。


有几个商家投诉过来,说划菜系统有延迟,下单之后,几分钟才能看到菜品。


我查看了一下监控,发现kafka消息又出现了积压的情况。


查了一下MySQL的索引,该走的索引都走了,但数据查询还是有些慢。


此时,我再次查了一下划菜表,惊奇的发现,短短半年表中有3千万的数据了。


通常情况下,单表的数据太多,无论是查询,还是写入的性能,都会下降。


这次出现查询慢的原因是数据太多了。


为了解决这个问题,我们必须:



  1. 做分库分表

  2. 将历史数据备份


由于现阶段做分库分表的代价太大了,我们的商户数量还没有走到这一步。


因此,我们当时果断选择了将历史数据做备份的方案。


当时我跟产品和DBA讨论了一下,划菜表只保留最近30天的数据,超过几天的数据写入到历史表中。


这样优化之后,划菜表30天只会产生几百万的数据,对性能影响不大。


消息积压的问题被解决了。


最近就业形式比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。


你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。


进群方式


添加苏三的私人微信:su_san_java,备注:掘金+所在城市,即可加入。


4 第四次消息积压


通过上面这几次优化之后,很长一段时间,系统都没有出现消息积压的问题。


但在一年之后的某一天下午,又有一些商家投诉过来了。


此时,我查看公司邮箱,发现kafka消息积压的监控报警邮件一大堆。


但由于刚刚一直在开会,没有看到。


这次的时间点就有些特殊。


一般情况下,并发量大的时候,是中午或者晚上的用餐高峰期,而这次出现消息积压问题的时间是下午


这就有点奇怪了。


刚开始查询这个问题一点头绪都没有。


我问了一下订单组的同事,下午有没有发版,或者执行什么功能?


因为我们的划菜系统,是他们的下游系统,跟他们有直接的关系。


某位同事说,他们半小时之前,执行了一个批量修改订单状态的job,一次性修改了几万个订单的状态。


而修改了订单状态,会自动发送mq消息。


这样导致,他们的程序在极短的时间内,产生了大量的mq消息。


而我们的mq消费者根本无法处理这些消息,所以才会产生消息积压的问题。


我们当时一起查了kafka消息的积压情况,发现当时积压了几十万条消息。


要想快速提升mq消费者的处理速度,我们当时想到了两个方案:



  1. 增加partion数量。

  2. 使用线程池处理消息。


但考虑到,当时消息已经积压到几个已有的partion中了,再新增partion意义不大。


于是,我们只能改造代码,使用线程池处理消息了。


为了开始消费积压的消息,我们将线程池的核心线程最大线程数量调大到了50。


这两个参数是可以动态配置的。


这样调整之后,积压了几十万的mq消息,在20分钟左右被消费完了。


这次突然产生的消息积压问题被解决了。


解决完这次的问题之后,我们还是保留的线程池消费消息的逻辑,将核心线程数调到8,最大线程数调到10


当后面出现消息积压问题,可以及时通过调整线程数量,先临时解决问题,而不会对用户造成太大的影响。



注意:使用线程池消费mq消息不是万能的。该方案也有一些弊端,它有消息顺序的问题,也可能会导致服务器的CPU使用率飙升。此外,如果在多线程中调用了第三方接口,可能会导致该第三方接口的压力太大,而直接挂掉。



总之,MQ的消息积压问题,不是一个简单的问题。


虽说产生的根本原因是:MQ生产者生产消息的速度,大于MQ消费者消费消息的速度,但产生的具体原因有多种。


我们在实际工作中,需要针对不同的业务场景,做不同的优化。


我们需要对MQ队列中的消息积压情况,进行监控和预警,至少能够及时发现问题。


没有最好的方案,只有最合适当前业务场景的方案。


最后说一句(求关注,别白嫖我)


如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。


求一键三连:点赞、转发、在看。


关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。


作者:苏三说技术
来源:juejin.cn/post/7368308963128000512
收起阅读 »

工作7年了,才明白技术的本质不过是工具而已,那么未来的方向在哪里?

前言 Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。 五一过去了,不知道大家有没有好好的放松自己呢?愉快的假期总是这么短暂,打工人重新回到自己的岗位。 我目前工作7年了,这几年来埋头苦干,学习了很多技术,做了不少系统,也解决过不少线...
继续阅读 »

前言


Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。


五一过去了,不知道大家有没有好好的放松自己呢?愉快的假期总是这么短暂,打工人重新回到自己的岗位。


我目前工作7年了,这几年来埋头苦干,学习了很多技术,做了不少系统,也解决过不少线上问题。自己虽然在探寻个人IP与副业,自己花了很多时间去思考技术之外的路该怎么走。但转念一想,我宁愿花这么多时间去探索技术之外的路线,但是却从没好好静下来想一下技术本身。


技术到底是什么,你我所处的技术行业为什么会存在,未来的机会在哪里。


因此,我结合自己的工作经历,希望和大家一起聊聊,技术的本质与未来的方向,到底在哪里,才疏学浅,如果内容有误还希望你在评论区指正。


背景


行业现状


互联网行业发展放缓,进入调整阶段,具体表现为市场需求、用户规模、营收利润、创新活力等方面的放缓或下降。


一些曾经风光无限的互联网公司也遭遇了业绩下滑、股价暴跌、裁员潮等困境,你是不是也曾听过互联网的寒冬已至的言论?


其实互联网本身,并没有衰败或消亡,而是因为互联网高速发展的时代过去了。



  1. 中国经济增速放缓、消费升级趋势减弱、人口红利消失等因素的影响,中国互联网市场的需求增长趋于饱和或下降。

  2. 用户规模停滞,智能手机普及率饱和,互联网用户规模增长趋于停滞,由增量市场变为存量市场,互联网获客成本越来越高。

  3. 监管政策收紧,互联网行业规范和监管愈加严格,更加注重合规,因此互联网行业也会收到影响。


供需环境


供需环境变化,应届生要求越来越高,更加注重学历。


社招更是看中学历的同时,开始限制年龄。招聘更看重项目经验,业务经验。五年前,你只要做过一些项目,哪怕不是实际使用的,也很容易拿到offer。而现在企业在看中技术能力的同时,还会关注候选人对与行业的理解,以及以往的工作经验。


技术的本质


先说结论,技术的本质是工具。 我把过去几年的认知变化分成了四个阶段,给大家展示一下我对于技术的认知成长过程。


第一阶段


技术就是应用各类前沿的框架、中间件。


刚毕业时,我就职于一家传统信息企业。谈不上所谓的架构,只需要Spring、Mysql就构建起了我们的所有技术栈。当然,微服务框架更不可能,Redis、MQ在系统中都没使用到。


此时互联网企业已经开始快速发展,抖音诞生区区不过一年。


一线城市的互联网公司,都已经开始使用上了SpringBoot、微服务,还有各类我没有听说过的中间件。


工作环境的闭塞,让我对各类技术有着无限憧憬,因为很多当下难以解决的问题,应用一些新技术、新架构,就能立刻对很多难题降维打击。


举个例子,如果你使用本地缓存,那么集群部署时,你一定要考虑集群的缓存一致性问题,可这个问题如果用上分布式缓存Redis,那么一致性问题迎刃而解。


所以那个时候的我认为,技术就是应用各类中间件,只要用上这些中间件、框架,我就已经走在了技术的前沿。


第二阶段


技术对我而言就是互联网。
半年后,我摆脱传统行业,来到了一个小型互联网公司,用上了不少在我眼中的新技术。


但任何新技术,如果只停留在表面,那么对于使用者来说,就是几个API,几行代码,你很快就会感到厌倦,发现问题也会焦虑,因为不清楚原理,问题就无从排查。


很快,所谓的“新技术”,就不能给我带来成就感了。我开始羡慕那些互联网行业APP,无时无刻都在畅想着,如果我做的产品能够被大家看到并应用,那该是多么有意思的一件事情。


于是我又认为,技术就是做那些被人看见、被人应用的网站、APP。


第三阶段


技术就是高并发、大流量、大数据。
当自己真正负责了某一个APP的后端研发后,很多技术都有机会应用,也能够在AppStore下载自己的APP了,没事刷一刷,看到某一个信息是通过我自己写的代码展示出去,又满足了第二阶段的目标了。


那么我接下来追求的变成了,让更多的人使用我做的产品,起码让我的亲人、朋友也能看到我做的东西。


当然,随之而来的就是日益增长的数据规模和大流量,这些无时无刻都在挑战系统的性能,如何去解决这些问题,成为了我很长一段时间的工作主线。


应对高并发、大流量,我们需要对系统作出各种极致性能的优化。


为了性能优化,还需要了解更多的底层原理,才能在遇到问题时有一个合理的解决方案。


所以,我认为技术就是高并发、大数据,做好这些,才算做好了技术。


第四阶段


经过了传统企业,到互联网公司,再到互联网大厂的一番经历,让我发现技术的本质就是工具,在不同阶段,去解决不同的问题。


在第一阶段,技术解决了各类行业的数据信息化问题,借助各类中间件、架构把具体的需求落地。


在第二阶段、第三阶段,技术解决了业务的规模化问题,因为在互联网,流量迅猛增长,我需要去用技术解决规模化带来的各类的问题,做出性能优化。


当然,技术在其他领域也发挥着作用,比如AI&算法,给予了互联网工具“智能化”的可能,还有比如我们很难接触到的底层框架研发,也就是技术的“技术”,这些底层能力,帮助我们更好的发挥我们的技术能力。


未来机会


大厂仍是最好的选择


即使是在互联网增速放缓、内卷持续严重的今天,即使我选择从大厂离职,但我依然认为大厂是最好的选择。


为什么这么说,几个理由



  • 大厂有着更前沿的技术能力,你可以随意选择最适合的工具去解决问题

  • 大厂有着更大的数据、流量规模,你所做的工作,天然的就具备规模化的能力

  • 大厂有先进的管理方法,你所接触的做事方法、目标管理可能让你疲倦,但工作方法大概率是行业内经过验证的,你不会走弯路,能让你有更快的进步速度


数字化转型


如果你在互联网行业,可能没有听说过这个词,因为在高速发展的互联网行业,本身就是数字驱动的,比如重视数据指标、AB实验等。但在二线、三线城市的计算机行业或者一些传统行业,数字化转型是很大的发展机会。


过去十年,传统行业做的普遍是信息化转型,也就是把线下,需要用纸、笔来完成工作的,转移到系统中。


那什么是数字化转型?



我用我自己的理解说一下,数字化转型就是业务流程精细化管理,数据驱动,实现降本增效。



我目前所在的公司的推进大方向之一,就是数字化转型。因为许多行业的数字化程度非常低,本质而言,就是把数字驱动的能力,带给传统企业,让传统企业也能感受到数字化带来的发展可能。


举个例子,比如一个餐饮系统数字化转型后,一方面可以把用户下单、餐厅接单、开始制作、出餐、上餐线上化,还可以和原材料供应系统打通,当有订单来时,自动检测餐饮的库存信息,库存不足及时提供预警,甚至可以作出订单预测,比如什么时间点,哪类餐品的点单量最高。


当然,数字化转型与互联网有着极大的不同,在互联网行业,你只需要坐在工位,等着产品提出需求就可以了。但是传统行业,你需要深入客户现场,实地查看业务流程,与用户交谈,才能真正的理解客户需求。


或许这样的工作并不炫酷,还需要出差,但在互联网行业饱和的今天,用技术去解决真实世界的问题,也不失为一个很好的选择。


AI&智能化


随着AI快速发展,各类智能化功能已经遍布了我们使用的各类APP,极客时间有了AI自动总结,懂车帝有了智能选车度搜索问题,有时候第一个也会是AI来给我们解答。



任何行业遇上AI都可以再做一遍。



抛开底层算法、模型不谈,但从使用者角度来说,最重要的是如何与行业、场景结合相使用。但是想要做好应用,需要你在行业有着比较深的沉淀,有较深的行业认知。


当然,智能化也不仅限于AI,像上面餐饮系统的例子,如果能够实现订单预测、自动库存管理,其实也是智能化的体现。


终身学习


技术能力


持续精进专业技术能力,相信大家对此都没有疑问。


对于日常使用到的技术,我们需要熟练掌握技术原理,积累使用经验,尤其是线上环境的问题处理经验。


第一个是基础。比如对集合类,并发包,IO/NIO,JVM,内存模型,泛型,异常,反射,等有深入了解,最好是看过源码了解底层的设计。


第二你需要有全面的互联网技术相关知识。从底层说起,你起码得深入了解mysql,redis,nginx,tomcat,rpc,jms等方面的知识。


第三就是编程能力,编程思想,算法能力,架构能力。


在这个过程中,打造自己的技能树,构建自己的技术体系。


对于不断冒出的新技术,我们一方面要了解清楚技术原理,也要了解新技术是为了解决什么问题,诞生于什么背景。


业务能力


前面说到技术是一种工具,解决的是现实世界的问题,如果我们希望更好的发挥技术的作用,那么就需要我们先掌握好业务领域。


互联网领域
如果你想要快速地入门互联网领域的业务,你可以使用AARRR漏斗模型来分析。


AARRR这5个字母分别代表 Acquisition、Activation、Retention、Revenue 和 Refer
五个英文单词,它们分别对应用户生命周期中的 5 个重要环节:获取(Acquisition)、激活(Activation)、留存(Retention)、收益(Revenue)和推荐(Refer)。


AARRR 模型的核心就是以用户为中心,以完整的用户生命周期为指导思想,分析用户在各个环节的行为和数据,以此来发现用户需求以及产品需要改进的地方。


举一个简单的例子,我们以一个互联网手游 LOL来举例:
获取就是用户通过广告、push等形式,了解到了游戏并注册或者登陆。
激活就是用户真正的开始游戏,比如开始了一场匹配。
留存就是用户在7天、30天内,登陆了几次,打了几把比赛,几天登陆一次,每日游戏时常又是多少。
收益,用户购买皮肤了,产生了收益。
推荐,用户邀请朋友,发送到微信群中,邀请了朋友一起开黑。


如果你所在的行业是C端产品,那么这个模型基本可以概括用户的生命周期全流程。


传统行业
传统行业没有比较通用的业务模型,如果想要入手,需要我们从以下三个角度去入手



  1. 这个行业的商业模式是什么,也就是靠什么赚钱的?比如售卖系统收费,收取服务费等

  2. 行业的规模如何?头部玩家有哪些?它们的模式有哪些特色?

  3. 这个行业的客户是谁、用户是谁?有哪些经典的作业场景?业务操作流程是什么样的?


如何获取到这些信息呢?有几种常见的形式



  1. 权威的行业研究报告,这个比较常见

  2. 直接关注头部玩家的官网、公众号、官媒

  3. 深入用户现场


我们以汽车行业来举例
商业模式:整车销售、二手车、汽车租赁等,细分一点,又有传统动力和新能源两种分类。
规模:如下图


头部车企:传统的四大车企一汽、东风、上汽、长安,新势力 特斯拉、蔚小理


经典场景:直接去4S店体验一下汽车销售模式、流程


说在最后


好了,文章到这里就要结束啦,我用我自己工作几年的不同阶段,给你介绍了我对于技术的本质是工具的思考过程,也浅浅的探寻了一下,未来的发展机会在哪里,以及我们应该如何提升自己,很感谢你能看到最后,希望对你有所帮助。




作者:东东拿铁
来源:juejin.cn/post/7365679089812553769
收起阅读 »

面试官:“你知道什么情况下 HTTPS 不安全么”

面试官:“HTTPS的加密过程你知道么?”我:“那肯定知道啊。”面试官:“那你知道什么情况下 HTTPS 不安全么”我:“这....”越面觉得自己越菜,继续努力学习!!!什麽是中间人攻击?中间人攻击(MITM)在密码学和计算机安全领域中是指攻击者与通讯的两端分...
继续阅读 »

面试官:“HTTPS的加密过程你知道么?”

我:“那肯定知道啊。”

面试官:“那你知道什么情况下 HTTPS 不安全么”

我:“这....”

越面觉得自己越菜,继续努力学习!!!


什麽是中间人攻击?

中间人攻击MITM)在密码学计算机安全领域中是指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者完全控制[1]。在中间人攻击中,攻击者可以拦截通讯双方的通话并插入新的内容。在许多情况下这是很简单的(例如,在一个未加密的Wi-Fi 无线接入点的接受范围内的中间人攻击者,可以将自己作为一个中间人插入这个网络)。

一个中间人攻击能成功的前提条件是攻击者能将自己伪装成每一个参与会话的终端,并且不被其他终端识破。中间人攻击是一个(缺乏)相互认证的攻击。大多数的加密协议都专门加入了一些特殊的认证方法以阻止中间人攻击。例如,SSL协议可以验证参与通讯的一方或双方使用的证书是否是由权威的受信任的数字证书认证机构颁发,并且能执行双向身份认证。

以上定义来自维基百科,我们来举一个通俗的例子来理解中间人攻击:

image.png

  1. A发送给B一条消息,却被C截获:

A: “嗨,B,我是A。给我你的公钥”

  1. C将这条截获的消息转送给B;此时B并无法分辨这条消息是否从真的A那里发来的:

C: “嗨,B,我是A。给我你的公钥”

  1. B回应A的消息,并附上了他的公钥:

B -> B 的公钥 -> C

  1. C用自己的密钥替换了消息中B的密钥,并将消息转发给A,声称这是B的公钥:

C -> C 的公钥 -> A

  1. A 用它以为是 B的公钥,加密了以为只有 B 能看到的消息

A -> xxx -> C

  1. C 用 B 的密钥进行修改

C -> zzz -> B

这就是整个中间人攻击的流程。

中间人攻击怎么作用到 HTTPS 中?

首先让我来回顾一下 HTTPS 的整个流程:

回顾 HTTPS 过程

image.png

这是 HTTPS 原本的流程,但是当我们有了 中间人服务器之后,整个流程就变成了下面这个样子。

这个流程建议动手画个图,便于理解

  1. 客户端向服务器发送 HTTPS 建立连接请求,被中间人服务器截获。
  2. 中间人服务器向服务器发送 HTTPS 建立连接请求
  3. 服务器向客户端发送公钥证书,被中间人服务器截获
  4. 中间人服务器验证证书的合法性,从证书拿到公钥
  5. 中间人服务器向客户端发送自己的公钥证书

注意!在这个时候 HTTPS 就可能出现问题了。客户端会询问你:“此网站的证书存在问题,你确定要信任这个证书么。”所以从这个角度来说,其实 HTTPS 的整个流程还是没有什么问题,主要问题还是客户端不够安全。

  1. 客户端验证证书的合法性,从证书拿到公钥
  2. 客户端生成一个随机数,用公钥加密,发送给服务器,中间人服务器截获
  3. 中间人服务器用私钥加密后,得到随机数,然后用随机数根据算法,生成堆成加密密钥,客户端和中间人服务器根据对称加密密钥进行加密。
  4. 中间人服务器用服务端给的证书公钥加密,在发送给服务器时
  5. 服务器得到信息,进行解密,然后用随机数根据算法,生成对称加密算法

如何预防?

刚才我们说到这里的问题主要在于客户端选择信任了,所以主要是使用者要放亮眼睛,保持警惕

参考文章:


作者:阳树阳树
来源:juejin.cn/post/7238619890993643575
收起阅读 »

Shadcn UI 现代 UI 组件库

web
前言 不知道大家是否使用过 Shadcn UI,它在Github 上拥有了 35k star,它与大多数 UI 组件库(如 Ant desgin 和 Chakra UI)不同,一般组件库都是通过 npm 的方式给项目使用,代码都是存在 node_modules...
继续阅读 »

image.png


前言


不知道大家是否使用过 Shadcn UI,它在Github 上拥有了 35k star,它与大多数 UI 组件库(如 Ant desgin 和 Chakra UI)不同,一般组件库都是通过 npm 的方式给项目使用,代码都是存在 node_modules 中,而 Shadcn UI 可以将单个 UI 组件的源代码下载到项目源代码中(src 目录下),开发者可以自由的修改和使用想要的 UI 组件,它已经被一些知名的网站(vercel.combestofjs.org)等使用。那么它到底有什么优势呢? 一起来来探讨下。


Shadcn UI 介绍


Shadcn UI 实际上并不是组件库或 UI 框架。相反,它是可以根据文档“让我们复制并粘贴到应用程序中的可复用组件的集合”。它是由 vercel 的工程师Shadcn创建的,他还创建了一些知名的开源项目,如 TaxonomyNext.js for DrupalReflexjs


Radix UI - 是一个无头 UI 库。也就是说,它有组件 API,但没有样式。Shadcn UI 建立在 Tailwind CSS 和 Radix UI 之上,目前支持 Next.js、Gatsby、Remix、Astro、Laravel 和 Vite,并且拥有与其他项目快速集成的能力——安装指南


Shadcn UI 功能特点


多主题和主题编辑器



在 Shadcn UI 的官网上有一个主题编辑器,我们可以点击 Customize 按钮实时切换风格和主题颜色,设计完成后,我们只需要拷贝 css 主要变量到我们的程序中即可。 下图是需要拷贝的 css 颜色变量。



颜色使用 hls 表示,主题变量分为背景色(background) 和 前景色(foreground),Shadcn UI 约定 css 变量省略 background,比如 --card 就是表示的是 card 组件的背景颜色。


深色模式


可以看到复制的 css 变量支持生成深色模式,如果你使用 react, 可以使用 next-themes,这个包来实现主题切换,当然也可以通过 js 在 html 上切换 dark 这个样式来实现。 除了 react 版,社区还自发实现了 vuesvelte 版本


CLI


除了手动从文档中复制组件代码到项目中,还可以使用 cli 来自动生成代码



  • 初始化配置


npx shadcn-ui@latest init



  • 添加组件


npx shadcn-ui@latest add


按空格选择想要的组件,按回车就会下载选中的 UI 组件代码



下载的源码在 components/ui 目录下,并且自动安装 Radix UI 对应的组件。


丰富的组件库


Shadcn UI 拥有丰富的组件,包括 常见的 Form、 Table、 Tab 等 40+ 组件。





使用 Shadcn UI 创建登录表单


接下来我们一起实战下,使用 Shadcn UI 创建登录表单, 由于 Shadcn UI 是一个纯 UI 组件,对于复杂的表单,我们还需要使用 react-hook-form 和 zod。


首先下载 UI


npx shadcn-ui@latest add form

安装 react-hook-form 以及 zod 验证相关的包


yarn add add react-hook-form zod @hookform/resolvers

zod 用于格式验证


下面代码是最基本的 Form 结构


import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"

<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>This is your public display name.</FormDescription>
<FormMessage />
</FormItem>

)}
/>


  • FormField 用于生成受控的表单字段

  • FormMessage 显示表单错误信息


登录表单代码


"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"

import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"

const formSchema = z.object({
email: z.string().email({message:'邮箱格式不正确'}),
password: z.string({required_error:'不能为空'}).min(6, {
message: "密码必须大于6位",
}),
})

export default function ProfileForm() {
// 1. Define your form.
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
},
})

// 2. Define a submit handler.
function onSubmit(values: z.infer<typeof formSchema>) {
// Do something with the form values.
// ✅ This will be type-safe and validated.
console.log(values)
}

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 w-80 mx-auto mt-10">
<FormField
control={form.control}
name="email"
render={({ field }) =>
(
<FormItem>
<FormLabel>邮箱</FormLabel>
<FormControl>
<Input placeholder="请输入邮箱" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) =>
(
<FormItem>
<FormLabel>密码</FormLabel>
<FormControl>
<Input placeholder="请输入密码" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">登录</Button>
</form>
</Form>

)
}


展示效果



小结


与其他组件库相比,Shadcn UI 提供了几个好处。



  • 易用性:使用复制和粘贴或 CLI 安装方法可以轻松访问其组件.

  • 可访问性:Shadcn UI 的组件是完全可访问的,并符合 Web 内容可访问性指南 (WCAG) 标准,它支持屏幕阅读器、键盘导航和其他辅助设备。

  • 灵活和可扩展性:Shadcn UI 只会下载需要使用的组件在源码中,并且开发者可以灵活定制和修改。


当然需要手动拷贝安装每一个组件可能是一件麻烦的事情,这也会导致源码量的增加,因此是否使用 Shadcn UI 还得开发者自行决定,总的来说 Shadcn UI,我还是非常看好,我将配合 next.js 在一些新项目中使用。


作者:狂奔滴小马
来源:juejin.cn/post/7301573649328668687
收起阅读 »

产品经理:为什么你做的地图比以前丝滑了许多?

web
从业多年第一次接触地图相关的需求,开发过程中产生了一些思考,遂记录下来,欢迎讨论Vue3 + 高德地图 JS API 2.0 + 高德地图 AMapUI组件库近两年前端大家是真的不好混,在职的人呢被极限压榨,待业的人呢投简历都是【未读不回】。照常理来说,地图相...
继续阅读 »

从业多年第一次接触地图相关的需求,开发过程中产生了一些思考,遂记录下来,欢迎讨论

Vue3 + 高德地图 JS API 2.0 + 高德地图 AMapUI组件库

近两年前端大家是真的不好混,在职的人呢被极限压榨,待业的人呢投简历都是【未读不回】。

1.gif

照常理来说,地图相关的需求都是由组内的地图大佬负责的,但眼瞅着公司里前端同学越来越少,这“泼天的富贵”终于有一天也落到了我头上。

需求的内容倒是很简单:要在地图上绘制一些轨迹点和一条轨迹线,以及一个目标点KeyPoint,让使用者来审查轨迹线是否经过KeyPoint,以及系统中记录KeyPoint的信息是否正确。当轨迹未经过 或 KeyPoint信息不正确时,会再提供一些辅助点SubPoint供用户选择,替换掉KeyPoint。(轨迹点也属于一种SubPoint

本着能CV就不手写的原则,我打开了项目代码(Vue2)寻找之前类似的地图需求,看看能不能套用一下然后快速下班,结果我看到了若干个大几千行的文件,以及这样的渲染效果(轨迹点上的箭头表示当前移动的方向):

1.png

2.png

大哥喂,咱就是说,方向盘打不正的话要抓紧去修,上路是要出事故的 3.jpg

得,言归正传,且不说那加起来几万行的代码我能不能捋顺喽,就是这个效果,干脆我还是用Vue3重新实现一下吧。

别忘了,前端的老本行是什么


业务的关注点

开始之前,我们先思考一个问题:业务的关注点是什么?

想明白了这个问题,我们在设计地图样式以及一些交互细节时,才能有更好的针对性。

(让我看看有多少人是默认样式+内置组件一把梭的)

image.png

ok那既然涉及到了地图,归根结底我们的关注点无非是这三方面:

  • 线
  • 区域

如果按照关注点的归属粗略的分为两类:外部添加的地图自身的

当业务更关注外部添加的元素时(如maker、轨迹),随着地图缩放、地形改变、POI显隐,我们添加的元素是否始终有一个比较醒目的显示效果?

当业务更关注地图自身的元素时(如兴趣点),对于POI的 pick 动作,是否贴合业务流程?是否足够智能与便捷?(可参考高德自己的效果)

这里针对第一类推荐两个初始化地图的可选配置项:

  1. features:地图显示要素(查看效果
  2. mapStyle:地图主题(查看效果

相信很多人可能都没关注过这两个配置项,而这两个东西组合起来使用,不仅能使你添加的外部元素始终处于一个高醒目的level,也可以与你项目本身的风格主题更搭,如何抉择,诸君自行思量。

(浅浅吐槽一下,高德提供的功能和配置项非常丰富,但文档真的是一言难尽。。。一样的功能在不同的地方都有文档,有些内容还不一致)

4.png

选择画点的方法

当你大概明白自己要做什么样的地图之后,让我们稍微进入一点正题:怎么选择合理的画点方法?

高德提供了哪些画点的方法呢?

  • JS API
    1. 默认点标记Marker
    2. 圆形标记CircleMarker
    3. 灵活点标记ElasticMarker
    4. 海量标注LabelMarker:需要维护图层、维护避让等级、自定义样式实现起来比较麻烦
    5. 海量点标记MassMarker:无法显示文字label
    6. 点聚合
      • 按距离聚合MarkerCluster:需要维护权重
      • 按索引聚合IndexCluster:需要维护索引规则
  • JS API UI组件库
    1. 简单标注SimpleMarker
    2. 字体图标标注AwesomeMarker
    3. 矢量标注SvgMarker
    4. 海量点PointSimplifier:可使用Canvas

至于像文本标记、折线、多边形等等一些通过某些黑科技实现类似点标记的方法(GPT说的),和Native端的画点方法,不在本文的讨论范围中。


美国五星上将麦克阿瑟曾说过,一切抛开实际背景去讨论问题的行为都是耍流氓。

画点的方法找到了很多,那我们要画什么样的点呢?

1. 从数量上看

动辄上万

为什么我要先看数量呢,因为可自定义样式的画点方法很多,但是要支持大数量级渲染且性能良好,就把上边一多半方法给pass掉了。

还剩下这些可供选择:海量标注LabelMarker海量点标记MassMarker按距离聚合MarkerCluster按索引聚合IndexCluster海量点PointSimplifier

2. 从样式上看

需要自定义。从上边的截图中可以看出,点的形状为圆形,黑边黄底,中心有个箭头,且整体随着当前运动方向有一个rotate deg

上述画点方法至少都支持(图片 或 HTML String 或 CSS Style)中的一种方式,而这三种方式理论上也都能实现我们想要的效果,所以下一个。

3. 特性

test.gif

虽然我们需要关注轨迹点,但并不是所有状态下都需要。比如在地图的缩放等级很小时(看到的是省、国家级别),并不需要把每一个轨迹点都展示出来。所以可以看到,之前的实现效果中,放大缩小都会重新适应尺寸,并且临近的点有自动合并的效果。

海量标注LabelMarker海量点标记MassMarker退出了游戏,他俩是全量绘制并且没有外部接入的话点是始终展示的。

至此,只有按距离聚合MarkerCluster按索引聚合IndexCluster海量点PointSimplifier三者进入了决赛圈。

现在来综合对比一下这三种方法:

方法1w+点渲染性能自定义样式合并逻辑
按距离聚合MarkerCluster渲染迅速,操作不卡顿HTML String或图片距离+权重就近合并
按索引聚合IndexCluster渲染迅速,操作不卡顿HTML String或图片距离+索引分组合并
海量点PointSimplifier渲染迅速,操作不卡顿Canvas或图片TopN

渲染方面在1w+点的竞赛中大家表现得都不错,官网示例中心可以看到,这里不再赘述。

自定义样式则有三种途径,图片、HTML字符串和新出现的Canvas。图片和Canvas比较简单,我们先讲一讲这个HTML字符串,也就是原生的HTML。

假如你用了某个现代化的前端框架在开发你的系统,用到了高德地图,并且想画一些漂漂亮亮的点在你需要标注的地方。在翻阅了文档之后,发现似乎直接传入HTML字符串这种方法是最快的,于是你开开心心的输入了一个

My Marker
试试水,接着,保存、等待hot reload,并把期待的目光投向了屏幕...

353ad3a27c3ac95c86c30e66f1b4f15.png

好家伙,这小Marker不仔细看,还真有点找不到呢...你决定继续添加一些样式

2M2we.gif

之后代码可能逐渐变成了这样...

微信图片_20240514153858.png

你:

WzgGg.png

把手指从ctrl C V三个键拿下来之后,你陷入了沉思:我能不能用XXX UI组件库来自定义Marker?

答案当然是肯定的~下面请允许我用Vue@3.3.4 + ant-design-vue@3.2.20来做个示范~~

首先,他接收的是HTML字符串,所以直接传进去一个vue组件肯定是不行的

import MyComponent from './MyComponent.vue'

// 以普通点标记举例
new Marker({
content: MyComponent,
// ...other configs
})

// not work

所以我们要做的就是把MyComponent给转成原生HTML,最简单的办法当然就是Vue实例的mount()API啦:

MyComponent作为根组件创建一个新的Vue实例

import {createApp} from 'vue'
import MyComponent from './MyComponent.vue'

const app = craeteApp(MyComponent)

将实例挂载到一个div上,得到原生的HTML

const div = document.createElement("div")
app.mount(div)

console.log('div: ', div)

打印一下:

223.png

使用:

// 以普通点标记举例
const marker = new Marker({
content: div,
// ...other configs
});

效果图就不放啦,有几个注意的点要提一下:

  1. 最重要的放在最前边:如果你的点在整个页面的生命周期内仅会绘制一次,那你可以跳过这一条。否则一定要记得app.unmount()。一种比较好的实践是,把点数据画点的方法移除点的方法写进一个hook里。
// example
import { createGlobalState } from "@vueuse/core";
import { ref, createApp } from "vue";
import Map from './Map.js' // 地图实例
import MyComponent from "./MyComponent.vue";

export const useCustomPoints = createGlobalState(() => {
const pointData = ref([]);
const removePointsCb = [];

const setPoint = () => {
const data = pointData.value.map(point => {
const div = document.createElement("div");
const app = createApp(MyComponent);
app.mount(div);

removePointsCb.push(() => app.unmount()) // 清除图标实例的回调

// 以普通点标记举例
const marker = new Map.Constructors.Marker({
map: Map,
content: div,
// ...other config
});

return marker
})

Map.add(data); // 将点添加到地图上

removePointsCb.push(() => Map.remove(data)); // 移除点的回调
}

const removePoints = () => {
while (removePointsCb.length) {
removePointsCb.pop()();
}
};

return {
pointData,
setPoint,
removePoint
}
})
  1. 可以通过createApp的第二个参数传递props进去,这些props是响应式的
  2. 新创建的Vue实例与你项目自身的实例不共享全局的配置,比如路由组件Store等,需要单独配置
  3. Vue2以及其他的一些框架,实现思路类似

好了,言归正传。

看起来似乎三种方法都可以实现需求,但是仔细翻看点聚合方法的文档,发现使用图片自定义点时没有提供旋转的配置,也就是说我们可能需要准备n张图片(取决于你想实现角度渲染的精确度),不,这太不优雅了。而如果使用原生HTML去自定义,要么接受丑炸的效果(纯手工css),要么面临着卡顿的风险(大量的app实例)

没办法,只好被(xin)迫(ran)接受用海量点PointSimplifier的Canvas去做了~毕竟能用Canvas画就约等于能画一切嘛~~

抱着视死如归的心情去翻了一下JS API UI组件库的海量点PointSimplifiercanvas绘制function文档,发现了一句了不得的话:

微信图片_20240514173923.png

划重点:通常只是描绘路径尽量不要fill或者stroke引擎自己一次性

翻译:该函数通常只是描绘路径,但是也能描绘形状。尽量不要fill或者stroke,除非你能搞明白我们的描绘机制。所有点的路径描绘完成后,引擎自己会在尾部调用fill以及stroke,一次性绘出所有路径,所以你要注意尾部的这次操作,避免冲突。

三个字总结海量点PointSimplifier的描绘机制就是:连笔画

不是每个点都创建一个新的Canvas画布,绘制完成后立即渲染;而是所有的点都共用一个Canvas画布,以当次你能绘制的区域坐标作为参数,重复绘制n(点的数量)次,最后一把全渲染出来。

微信图片_20240515171523.png

微信图片_20240516093003.png

明白了这个,我们在书写function的逻辑的时候,只要注意保证每次绘制开始、结束时笔触的落点和绘制上下文状态即可。绘制一个有旋转角度的、中间有箭头的圆形(圆形的背景色是通过海量点PointSimplifier的lineStyle配置的),示例代码如下:

由于叠加了变换,处理状态时偷懒使用了save()、restore()

renderOptions: {
// 这里使用了样式分组引擎:https://lbs.amap.com/demo/amap-ui/demos/amap-ui-pointsimplifier/group-style-render
// 以点的旋转角作为组id入参,方便操作
// 无需分组时,renderOptions.pointStyle.content = renderOptions.groupStyleOptions.pointStyle.content 逻辑一致
groupStyleOptions: function (gid) {
return {
pointStyle: {
content: function (ctx, x, y, width, height) {
// 存了一个坐标,画箭头的时候用
const startX = x + width / 2;
const startY = y + height / 4;

// 移动到画布的最右侧、中间位置
ctx.moveTo(x + width, y + height / 2);

// 画圆
ctx.arc(
x + width / 2,
y + height / 2,
width / 2,
0,
Math.PI * 2,
true
);

// 变换前保存一下状态
ctx.save();

// 以圆心为旋转的中心点
ctx.translate(x + width / 2, y + height / 2);
// 按照轨迹方向旋转
ctx.rotate((Math.PI / 180) * gid);
// 重置中心点
ctx.translate(-(x + width / 2), -(y + height / 2));

// 画箭头
ctx.moveTo(startX, startY);
ctx.lineTo(x + width / 4, y + height / 2);
ctx.moveTo(startX, startY);
ctx.lineTo(startX, y + (height * 3) / 4);
ctx.moveTo(startX, startY);
ctx.lineTo(x + (width * 3) / 4, y + height / 2);

// 由于箭头需要在旋转的状态下绘制,所以在箭头绘制完成后再恢复状态
ctx.restore();
},
},
};
},
}

来一个无旋转时的笔触顺序动图,我尽力了6j4l.png

test.gif

画完之后,看一下对比效果:




OK,点画出来了。

上边特性中有提到:当我们距离很远时,就不需要再关注某个具体的轨迹点。所以可以再进一步优化,当地图的缩放等级zoom小于某个阈值时,清空point:

import {computed, watch, ref} from 'vue'
import {Map, PointSimplifierIns} from 'Map.js' // 地图实例、海量点实例

const zoom = ref(null);

const showPoint = computed(() => zoom.value > 10);

const pointData = ref([ /* ...赋值逻辑省略 */]);

Map.on("zoomchange",
debounce(() => {
zoom.value = Map.getZoom();
}, 200)
);

watch(showPoint, (show) => {
PointSimplifierIns.setData(show ? pointData.value : []);
})

效果如下:

test.gif

控制显示隐藏没有用自带的show()hide()方法,而是选择直接重设数据源,是因为:海量点PointSimplifiershow状态下时对地图进行缩放,会自动重绘适应尺寸;hide状态下则不会。从show变为hide时,会保存当前zoom下点的尺寸,供下次hideshow时用。如果地图缩放的太快,当前的zoom与上次保存尺寸时的zoom跨度太大,可能会导致点位不匹配现象。


选择画轨迹的方法

画线的选择过程就简单了很多,之前需求中是用折线Polyline实现的,画出来的效果总感觉差点意思,所以就去翻了翻高德的文档,共找到常规画线方法3种:

  • JS API
    1. 折线Polyline
    2. 贝塞尔曲线BesizerCurve
  • JS API UI组件库
    1. 轨迹展示PathSimplifier

基本上毫无疑问了嘛~我们本身就是要画轨迹,还有什么好选的~~ 必须用轨迹展示

不过这里还是分享一些对三种方法实际体验之后的感受:

  1. 折线Polyline:无法识别线上的点。如果轨迹数据没有经过噪点清除,画出来之后在细节处会有比较严重的锯齿。不过整体上感觉,倒也不是不能用~
  2. 贝塞尔曲线BesizerCurve:无法识别线上的点。但理论上是唯一可以绘制出完全符合真实运动轨迹的、贴合地图路线的方法了,代价也是相当的大——至少要在原本轨迹点的基础上额外维护n-1个控制点,放弃~~
  3. 轨迹展示PathSimplifier:性能好,相同数据量下的显示效果要比折线画出来的平滑许多。以及来自官网的优点罗列:
    • 使用Simplify.js进行点的简化处理,提高性能
    • 支持识别线上的点做信息展示
    • 内置巡航器
    • 样式配置更加丰富

实现过程比较简单,照着文档撸就行,可以对比下折线和轨迹展示两种方式,在拐角细节处的差异:

折线


轨迹展示


小tips: 适当增加线宽lineWidth可以有效的缓解锯齿现象


Loading的区域与时机

当我第一次打开上文提到的老版本地图页面时,除了渲染效果不够理想外,最大的一个感受就是:Loading太长

不是想像中那样常规的:打开页面,给一个满屏Spin等待加载各种数据、等待绘制点、线的动作,所有准备工作完成后,取消Spin允许用户开始操作。

咱就是说,像这样的交互逻辑,其实也没啥问题。毕竟谁还没个业务繁忙的时候,最简单最原始最暴力的满屏Spin虽然在体验上不尽如人意,但我觉得是符合上线标准的。

但您猜我看到了什么?

微信图片_20240520171349.png

Form、Map、Action Bar三个区域各自一个小Spin,整体有个大Spin,可以透过大Spin的透明遮罩层看到下面的小spin们反复交替进行,以及大Spin自己也时不时的闪现一下...

7D91.gif

Spin为什么会闪现?回到需求当中来:

地图上点、线的绘制依赖了多个数据源

  • 轨迹点、轨迹线数据源
  • KeyPoint数据源
  • SubPoint数据源
    • Type 1
    • Type 2

这些接口一部分是并发请求,但也有个别的接口请求参数依赖于其他接口的返回值

以及,使用高德地图提供的API绘制点、线时,也共用了接口请求时的Spin。

还有诸多类似这样的代码:

setTimeout(() => {
loading = false
}, 2000)

对渲染流程管理混乱、对数据流向不了解、对自己代码不自信,故意延迟loading的结束时机,防止用户过早操作导致报错


const interval = setInterval(() => {
if(conditon) {
clearInterval(interval);
loading = false
}
}, 1000)

依赖第三方的内容加载,或将多个小loading合并为一个大loading


最终的结果就是让人一整个loading住...

pj7dW.png

而我做了哪些改变

首先,将单个loading覆盖的区域尽可能的缩小

举个例子,上边提到的Action Bar,假设里边既有展示KeyPoint信息的列表,又有展示所有SubPoint信息的列表。在之前的处理方案中,Action Bar区域只有一个整体的Spin,所以整个区域loading的流程大概是:

%%{init: { 'theme': 'base', 'themeVariables': {
'cScale0': '#996666', 'cScaleLabel0': '#ffffff',
'cScale1': '#996633','cScaleLabel1': '#ffffff',
'cScale2': '#999999', 'cScaleLabel2': '#ffffff'
}}}%%

timeline
title Loading 状态

section 阶段一
show : request for KeyPoint data
hide : request success

section 阶段二
show : request for SubPoint type1 data
hide : request success

section 阶段三
show : request for SubPoint type2 data
hide : request success

而我则是把每个数据源对应的列表都单独分配了一个loading组件

聪明的看官老爷可能会问,同时存在多个Spin,不也很奇怪吗?

test.gif

所以我选择了骨架屏Skeleton作为loading组件:

test.gif

受gif图的帧率影响,实际效果还是很丝滑的。(但使用骨架屏时也有一个注意的点:骨架屏的占位高度需要配置段落占位图行数来调整,避免loading结束时真实的渲染内容与骨架屏高度相差太大产生视差)

然后,在Map区域用其他形式的提示代替传统的loading

与上边类似,Map区域不仅同时用到了KeyPointSubPoint数据源,而且在绘制点、线时也有loading。并且也是一个整体的Spin,你应该能想象出每次数据初始化时,Map上闪来闪去的Spin。。。

地图本身,是高德提供出来可以开箱即用的组件,我们所添加的点、线只是附加属性,并不应该使用整体的Spin遮罩阻止用户使用地图的其他功能。在某些附加属性成功添加之前,用户只需要知道与之相关的功能是不可用状态即可。

我的方案是:图例化提供一个loading-box,里边展示了每个数据源的加载状态

test.gif

为了不遮挡地图,loading-box不是始终展示的,基础显示逻辑是:

  1. watch监听loadings 数组
  2. 只要有一个数据源loading中,则显示。
  3. 全部数据源都不在loading中,则debounce n秒后隐藏。

显示动作是实时的,只要有一个数据源在loading中,就应该立刻让用户感知到。

而隐藏动作如果也是实时的,loading-box的显隐切换会比较频繁,显得很突兀。

  • 如果使用setTimeout做延时,期望是发出hide指令n秒后执行,但无法保证n秒后没有新的loading正在进行,导致显隐切换逻辑紊乱。
  • 如果使用throttle做延时,导致的问题与setTimeout相同,只是发生概率会小一些。
  • 相比之下debounce最适合做这个场景的解决方案。

再结合上边提到的hook写法,把loading状态也放进去,方便loading-box使用:

// example
import { createGlobalState } from "@vueuse/core";
import { ref, createApp } from "vue";

export const useCustomPoints = createGlobalState(() => {
const pointData = ref([]);
const pointLoading = ref(false);
const removePointsCb = [];

const getPoint = async () => {
pointLoading.value = true;
const data = await requestPointData();
pointLoading.value = false;

pointData.value = data;
}

const setPoint = () => {
// ...
}

const removePoint = () => {
// ...
};

return {
pointData,
pointLoading,
getPoint,
setPoint,
removePoint
}
})
// loading-box.vue

<script setup>
import { watch } from 'vue';
import { useCustomPoints } from 'useCustomPoints.js';

const { pointLoading } = useCustomPoints();

watch([pointLoading, /* and other loadings */], () => {
// do loading-box show/hide logic
})
script>

应用了上述loading相关的优化后,虽然跟核心业务逻辑相关的代码改动几乎为0,但用户的体验却有相当大的提升,究其原因:

在老版本的实现中,因为全屏Spin的存在,任何一项页面准备工作完成前,页面都无可交互区域;拆分loading后,把一大部分无可交互区域的时间变成了局部可交互区域的时间,甚至在Map模块替换了loading的形式,完全避免了Spin遮罩层这种阻隔用户的效果。加上Spin动画本身的耗时、显示/隐藏Spin的耗时,积少成多,产生质变。

image.png

可以看到,在局部loading耗时完全一样的情况下,老版本中:

无可交互区域时间 = 全屏Spin时间 = 局部loading的最大时间

而在新版本中:

无可交互区域时间 = 几个all loading片段的时间之和

而这,也是一些复杂应用做体验优化的思路之一。


结语

okok,先写到这,毕竟马上就要下班了

1b006c53e15945a09253df289b6192cc~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.awebp

没什么高大上理论也没什么八股文,只是一个从业多年一事无成小前端在重构需求时的一些感想~~

还是那句话,欢迎真诚交流,但如果你来抬杠?阿,对对对~ 你说的都对~


彩蛋

文章标题来自需求上线后,产品经理的真实评价

image.png


作者:Elecat
来源:juejin.cn/post/7371633297153687606
收起阅读 »

2024 前端趋势:全栈也许已经是必选项

web
过年期间,回想起来,似乎感觉到这次有一点不一样:也许,全栈的时代已经到来了。 React 与 Vue 生态对比 首先,我们来看看 React 与 Vue 生态的星趋势对比: 上图中,React 整个生态的星星数远超于 Vue,第十名都要比 Vue 第一名的多...
继续阅读 »

过年期间,回想起来,似乎感觉到这次有一点不一样:也许,全栈的时代已经到来了。


React 与 Vue 生态对比


首先,我们来看看 React 与 Vue 生态的星趋势对比:


截屏2024-02-29 10.05.39转存失败,建议直接上传图片文件


上图中,React 整个生态的星星数远超于 Vue,第十名都要比 Vue 第一名的多。我们将其做一个分类:


排名ReactVue
1UI全栈
2白板演示文稿
3全栈后台管理系统
4状态管理hook
5后台管理系统UI
6文档文档
7全栈框架集成UI
8全栈框架UI框架
9后台管理系统UI
10无服务栈状态管理

可以看到 React 这边的生态链基本成熟,几乎每一个分类都有一个上榜的库,不再像 Vue 那样还在卷 UI 框架。


在全栈方面,Vue 的首位就是全栈 Nuxt。


React 的 Next.js 虽然不在首位,但是服务端/全栈相关的内容就占了 4 个,其中包含第 10 名的无服务栈。另外值得注意的是,React 这边还有服务端组件的概念。Shadcn/ui 能占到第一位,因为它基于无头 UI Radix 实现的,在服务端组件也能运用。所以,服务端/全栈在 React 中占的比重相当大的。


这样看来,前端往服务端进发已经成为一个必然趋势。


htmx 框架的倒退


再看看框架这边,htmx 在星趋势里,排行第二位,2023增长的星星数为 15.6K,与第一位的 React 颇为相近。


而 htmx 也是今年讨论度最高的。


在我经历过前后端不分离的阶段中,使用 jsp 生成前端页面,js 更多是页面炫技的工具。然后在 jQuery + Ajax 得到广泛应用之后,才真正有前后端分离的项目。


htmx 的出现,不了解的人,可能觉得是倒退到 Java + jQuery + Ajax 的前后端分离状态。但是,写过例子之后,我发现,它其实是倒退到了前后端不分离的阶段。


用 java 也好,世界上最好的 php 也好,或者用现在的 nodejs 服务,都能接入 htmx。你只要在服务端返回 html 即可。


/** nodejs fastity 写的一个例子 **/
import fastify from 'fastify'
import fastifyHtml from 'fastify-html'
import formbody from '@fastify/formbody';

const app = fastify()
await app.register(fastifyHtml)
await app.register(formbody);
// 省略首页引入 htmx

// 首页的模板,提供一个按钮,点击后请求 html,然后将请求返回的内容渲染到 parent-div 中
app.get('/', async (req, reply) => {
const name = req.query.name || 'World'
return reply.html`

Hello ${name}


`
, reply
})

// 请求返回 html
app.post('/clicked', (req, reply) => {
reply.html`

Clicked!

`
;
})

await app.listen({ port: 3000 })

也许大家会觉得离谱,但是很显然,事情已经开始发生了变化,后端也来抢前端饭碗了。


截屏2024-02-29 10.32.24.png


htmx 在 github 上已经有不少跟随者,能搜出前端代码已有不少,前三就有基于 Python 语言的 Django 服务端框架。


jQuery 见势头不错,今年也更新了 4.0 的 beta 版本,对现代浏览器提供了更好的支持。这一切似乎为旧架构重回大众视野做好了准备。


企业角度


站在企业角度来看,一个人把前后端都干了不是更好吗?


的确如此。前后端一把撸更符合企业的利益。国外的小公司更以全栈作为首选项。


也许有人觉得国情不同,但是在我接触的前端群里,这两年都有人在群里说他们公司前后端分离的情况。


还有的人还喜欢大厂那一套,注意分工合作,但是其实大厂里遗留项目也不少,有的甚至是 php;还有新的实验项目,如果能投入最少人力,快速试错,这种全栈的框架自然也是最优选择。


我并不是说,前后端分离不值得。但是目前已经进入 AI 赛道,企业对后台系统的开发,并不愿意投入更多了。能用就行已经成为当前企业的目标,自然我们也应该跟着变化。


全栈破局


再说说前端已死的论调。我恰恰觉得这是最好做改变的时机。


在浏览器对新技术支持稳定,UI 框架趋同,UI 组件库稳定之后,前端不再需要为浏览器不兼容素手无策了,不再需要苦哈哈地为1个像素争辩不停了,也不再需要为产品莫名其妙的交互焦头烂额了。


这并不意味着前端已死,反而可能我们某个阶段的任务完成了,后面有更重要的任务交给我们。也许,全栈就是一个破局。


在云服务/云原生如此普遍的情况下,语言不再是企业开发考虑的主要因素,这也为 nodejs 全栈铺平了道路。


前端一直拣最苦最脏的话来做,从 UI 中拿到了切图的工作,然后接手了浏览器兼容的活,后来又从后端拿到了渲染页面的工作。


那我们为何不再进一步,主动把 API 开发的工作也拿过来?


作者:陈佬昔没带相机
来源:juejin.cn/post/7340603873604599843
收起阅读 »

OPPO举办OTalk 开发者交流专场,提供Android 15多元化适配服务

在日前举行的2024谷歌 I/O 大会上,备受期待的 Android 15 Beta 版本操作系统正式亮相。作为 Android 生态系统的关键参与者,OPPO 连续六年首批适配 Android 新系统,第一时间推出基于 Android 15 Beta 的 C...
继续阅读 »

在日前举行的2024谷歌 I/O 大会上,备受期待的 Android 15 Beta 版本操作系统正式亮相。作为 Android 生态系统的关键参与者,OPPO 连续六年首批适配 Android 新系统,第一时间推出基于 Android 15 Beta 的 ColorOS 开发者预览版。

5月22日,OPPO 特别联合 51CTO 举办了「OTalk | Android 15 适配开发者交流专场」,以帮助开发者更好地理解和利用新版本的特性进行适配开发,活动以线上直播的形式展开,共吸引了27000+开发者和技术爱好者实时观看,并在40多个开发者社群中引发了热烈讨论。

1.png

OPPO技术大咖在线解答,拓宽开发者适配思路

全新的 Android 15 带来了一系列令人瞩目的新功能和改进,包括全新设计的兼容性调试工具、安全与隐私相关的强化措施、系统优化和新的API支持。

在此次「OTalk | Android 15 适配开发者交流专场」上,OPPO ColorOS 高级系统工程师纪昌杰首先通过带领大家回顾了Android历史版本的关键特性,帮助开发者更好地理解谷歌的更新逻辑,包括更安全地导出上下文注册的接收器和前台服务类型及权限的新要求等。

随后,纪昌杰全面且深入解析了 Android 15 的一系列新特性,特别是Manifest TAG限制、前台服务的启动限制、以及ART库中符号可见性属性的更新,这些改动旨在提高应用的安全性和性能,限制非公开API的访问,并确保服务的透明度与系统的及时响应。此外,纪昌杰还对Android 15 一些较小的更新进行了说明,如紧凑字体变更、提升的最低可安装目标API级别、Vulkan替换OpenGL ES、包名校验,以及16KB page size等功能。

2.png

此次 OTalk 通过详尽阐释这些新特性带来的影响,为开发者在适配过程中划明重点,提供了切实可行的适配策略。在互动答疑环节,面对开发者们的积极提问,纪昌杰还给出了一些针对性的解决方案和适配指导,确保开发者们能够快速、高效地应对新版本的变化。

Google 于2024年2月推出 Android 15 的开发者预览版,随后在4月发布 Android 15 首个 Beta 版本,为开发者提供了更加稳定的测试环境。6月份,平台稳定性里程碑版本将发布,帮助开发者规划最终测试和发布应用。最终的正式版也将在稳定版发布两个月后向公众推出,届时,Android 15 将全面开启全新的智能移动体验。鉴于 Android 15 的全新特性和适配计划,开发者可积极参与早期测试,尽快推进适配,确保应用在新系统上的平稳运行和优化。

3.png

能力共享服务多元,OPPO助力开发者高效有序适配

在此次OTalk上,纪昌杰还介绍了 OPPO 为支持开发者顺利适配 Android 15 所提供的全方位服务和支持。这其中包括详尽的兼容性适配指导文档,指引开发者迅速找到适配方案;免费的云真机/云测服务,可提供实时在线的远程调试功能,支持开发者随时接入,帮助开发者快速验证适配结果;开发者预览版允许早期测试应用在新系统上的表现,而应用上架应用商店新特性检测可以确保应用符合 Android 15 的所有新标准。此外,开发者还可以通过适配答疑交流社群、OPPO 开放平台适配支持专区等多元渠道获得支持,以提高适配效率。

4.png

「OTalk | Android 15 适配开发者交流专场」的成功举行,提供了更多 Android 15 高效适配思路,助力开发者及时解决适配疑难问题。本次 OTalk 活动中分享的适配资源信息以及高频适配问题解答,将在「OPPO开放平台」公众号及OPPO开发者社区官网发布,广大开发者可以随时查阅,并应用于实际开发当中。

OPPO也将持续为开发者提供全流程、多元化的适配支持服务,携手开发者共同推进新版本适配工作高效进行,打造更优质的用户体验。

收起阅读 »

8个小而美的前端库

web
前端有很多小而美的库,接入成本很低又能满足日常开发需求,同时无论是 npm 方式引入还是直接复制到本地使用都可以。 2024年推荐以下小而美的库。 radash 实用的工具库,相比与 lodash,更加面向现代,提供更多新功能(tryit,retry 等函数)...
继续阅读 »

前端有很多小而美的库,接入成本很低又能满足日常开发需求,同时无论是 npm 方式引入还是直接复制到本地使用都可以。


2024年推荐以下小而美的库。


radash


实用的工具库,相比与 lodash,更加面向现代,提供更多新功能(tryit,retry 等函数),源码可读性高,如果不想安装它,大部分函数可以直接复制到本地使用。



use-debounce


React Hook Debouce 库,让你不再为使用防抖烦恼。库的特点:体积小 < 1 Kb、与 underscore / lodash impl 兼容 - 一次学习,随处使用、服务器渲染友好。



timeago.js


格式化日期时间库,比如:“3 hours ago”,支持多语言,仅 2Kb 大小。同时提供了 React 版本 timeago-react。


timeage.format(1544666010224, 'zh_CN') // 输出 “5 年前”
timeage.format(Date.now() - 1000, 'zh_CN') // 输出 “刚刚”
timeage.format(Date.now() - 1000 * 60 * 5, 'zh_CN') // 输出 “5 分钟前”

react-use


实用 Hook 大合集 - 内容丰富,从跟踪电池状态和地理位置,到设置收藏夹、防抖和播放视频,无所不包。



dayjs


Day.js 是一个简约的 JavaScript 库,仅 2 Kb 大小。它可以使用基本兼容 Moment.js,为你提供日期的解析、处理和显示,支持多语言能力。



filesize


filesize.js 提供了一种简单方法,便于从数字(浮点数或整数)或字符串转换成可读性高的文件大小。


import {filesize} from "filesize";
filesize(265318, {standard: "jedec"}); // "259.1 KB"
driver.js:driver.js 是一款用原生 js 实现的页面引导库,上手非常简单,体积在 gzip 压缩下仅仅 5kb。

driver.js


driver.js 是一款用原生 js 实现的页面引导库,上手非常简单,体积在 gzip 压缩下仅仅 5kb。



@formkit/drag-and-drop


FormKit DnD 是一个小型库,它简单、灵活、与框架无关,压缩后只有 4Kb 左右,设计理念为数据优先。



小结


前端小而美的库使用起来一般都比较顺手,欢迎在评论区推荐你们开发中的使用小而美的库。


作者:晓得迷路了
来源:juejin.cn/post/7350140676615798824
收起阅读 »

如此丝滑的API设计,用起来真香

分享是最有效的学习方式。 博客:blog.ktdaddy.com/ 故事 工位上,小猫一边撸着代码,一边吐槽着前人设计的接口。 如下: “我艹,货架模型明明和商品SKU模型是一对多的关系,接口入参的时候偏偏要以最小粒度的SKU将重复入参进行平铺”。 “一个...
继续阅读 »

分享是最有效的学习方式。


博客:blog.ktdaddy.com/





故事


工位上,小猫一边撸着代码,一边吐槽着前人设计的接口。


如下:


“我艹,货架模型明明和商品SKU模型是一对多的关系,接口入参的时候偏偏要以最小粒度的SKU将重复入参进行平铺”。


“一个接口居然做了多件事情,传入参数复杂异常,不是一块业务类型的东西,非得全部揉在一起”。


“如此长的业务流程,接口能快起来么,难怪天天收到接口慢的告警”。


00.png


“这都啥啊,这名字怎么能这么取呢,这也太随意了吧....”


......


小猫一边写着V2版本的新接口,一边骂着现状接口。


聊聊APi设计


在日常开发过程中,相信大家在维护老代码的时候也多多少少会像小猫一样吐槽现有接口设计。很多项目经过历史沉淀以及业务验证,接口设计问题就慢慢放大暴露出来了。具体原因是这样的:


第一种情况可能是业务发展的必然趋势:不同技术人员对业务的看法和理解不同,一个接口可能经过多人的维护开发迭代,很多时候,新增功能也只是在原有的接口上直接拓展,当业务需求比较紧急的时候,大部分的研发一般都会选择快速去实现,而不会太过去考虑现有接口拓展的合规性。


第二种情况可能是本身开发人员自身能力问题,对业务的把控以及评估不合理导致的最终接口设计缺陷问题。


在系统软件开发过程中,一个好的UI设计可以让用户更好地使用一款产品。那么深入一层,一个好的API设计则可以让开发者高效地使用一个系统的能力,尤其是现在很多大型微服务项目中,API设计更加重要,因为此时的API调用方不仅仅是前端,甚至直接是其他服务。


那么接下来,老猫会和大家从下面的几个方面探讨一下,日常开发中我们应该如何去设计API。


0.png


API设计需要明确边界


在实际职场中,部门与部门之间、管理员与管理员之间容易出现扯皮、推诿现象。当然在系统和系统之间API的交互中其实往往也存在这样的情况。打个比方客户端的交互细节让后端代码通过接口来兜,你觉得合理不?


所以这就要求我们遵循下面两个点,咱们分别中两个维度来看,一个是“面向于服务和服务之间的API”,另一个是“面向客户端和服务之间的API”。


1、我们在设计API的过程中应该聚焦软件系统需要提供的服务或者能力。API是系统和外部交互的接口,至于外部如何使用,通过什么途径使用并不是重点。


2、对于面向UI的API设计中,我们更应该避免去过多关注UI的交互细节。交互属于客户端范畴,不同的终端设备,其交互必然也是不一样的。


API设计思路尽量面向结果设计而不是面向过程设计


相信大家应该都知道面向对象编程和面向过程编程吧。


老猫虽说的这里的面向结果设计其实和面向对象的概念有点类似。这种情况下的API应该是根据对象的行为来封装具体的业务逻辑,调用方直接发起请求需要什么就能给出一个最终的结果性质的东西,而不是中间过程中某个状态性质的东西。上层业务无需多次调用底层接口进行组装才能获取最终结果。


如下图:


面向执行过程API设计


面向最终结果API设计


举个例子。


银行提现逻辑中,


如果面向执行过程设计的API应该是这样的,先查询出余额,然后再进行扣减。于是有了下面这样的伪代码。


public interface BankService {
AccountInfo getAccountByUserName(String userName);
void updateAccount(AccountInfoReq accountInfoReq);
}

如果是面向结果设计,那么应该就是这样的伪代码。


public interface BankService {
AccountInfo withdraw(String userName,Long amount);
}

API设计需要尽量保证职责单一


在设计API的时候,应该尽力要求一个API只做一件事情,职责单一的API可以让API的外观更加稳定,没有歧义。并且上层调用层也是一目了然,简单易用。


对于一个API如果符合下面条件的时候,咱们就可以考虑对其进行拆分了。


1、一个API内部完成了多件事情。例如:一个API既可以发布新商品信息,又能更新商品的价格、标题、规格信息、库存等等。如果这些行为在一个接口进行调用,接口复杂度可想而知。
另外的接口的性能也是需要考虑的一部分,再者如果后续涉及权限粒度拆分,其实这种设计就不便于权限管控了。


2、一个API用于处理不同类型对象的业务。例如:一个API编辑不同的商品类型,由于不同类型的商品对应的模型通常是不同的(例如出行类的商品以及卡券类的商品差别就很大),
如果放在一个API中,API的输入和输出参数会非常复杂,使用和维护成本就很高。


其实关于API单一职责相关的话题,老猫在之前的文章中也有提及过,有兴趣的小伙伴可以戳【忍不了,客户让我在一个接口里兼容多种业务功能


API不应该基于实现去设计


在API设计过程中,我们应该避免实现细节。一个API有多种实现,在API层面不应该暴露实现细节,从而误导用户。


例如生成token是最为常见的,生成token的方式也会有很多种。可以通过各种算法生成token,
有的是根据用户信息的hash算法生成,或者也可以用base64生成,甚至雪花算法直接生成。如果对外暴露更多实现细节,其实内部实现的可拓展性就会相当差。
我们来看一下下面的代码。


//反例:暴露实现细节
public interface tokenService {
TokenInfo generateHashTokenByUserName(String userName);
}
//正例:足够抽象、便于拓展
public interface tokenService {
TokenInfo generateToken(Object key);
}

API的命名相当重要


一个好的API名字无疑是相当重要的,使用者一看API的命名就能知道如何使用,可以大大降低调用方的使用成本。所以我们在设计API的时候需要注意下面几个方面。


1、API的名字可以自解释,一个好的API的名称可以清晰准确概括出API本身提供的能力。


2、保持对称性。例如read/write,get/set。


3、基本的API的拼写务必准确。API一旦发布之后,只能增加新的API去订正,旧API完全没有请求量之后才能废弃,错误的API的拼写可能会带给调用方理解上的歧义。


API设计需要避免标志性质的参数


所谓标志性的参数,就是一个接口为了兼容不同的逻辑分支,增加参数让调用方去抉择。这块其实和上述提及的API设计保证职责单一有点重复,但是老猫觉得很重要,所以还是
单独领出来细说一下。举个例子,上述提及的发布商品,在发布商品中既有更新的原有商品信息的功能在,又有新增商品的功能在。于是就有了这样错误的设计,如下:


public class PublishProductReq {
private String title;
private String headPicUrl;
private List<Sku> skuList;

//是否为更新动作,isModify就是所说的标志性质的参数
private Boolean isModify;
.....
}

那么对应的原始的发布接口为:


//反例:内部入参通过isModify抉择区分不同的逻辑
public interface PublishService {
PublishResult publishProduct(PublishProductReq req);
}

比较好的逻辑应将其区分开来,移除原来的isModify标志位:


public interface PublishService {
PublishResult addProduct(PublishProductReq req);
PublishResult editProduct(PublishProductReq req);
}

API设计出入参需要保证风格一致


这里所说的出入参的风格一致主要指的是字段的定义需要保持一个,例如对外的订单编号,一会叫做outerNo,一会叫做outerOrderNo。相关的用户在调用的时候八成是会骂娘的。


老猫最近其实在对接供应商的相关API,调用对方创建发货订单之后返回的订单编号是orderNo,后来用户侧完成订单需要通知供应商,入参是outerNo。老猫此时是懵逼的,都不知道这个
outerNo又是个什么,后来找到对面的研发沟通了一轮才知道原来outerNo就是之前返回的orderNo。


于是“我艹,坑笔啊”收尾.....


API设计的时候考虑性能


最后再聊聊API性能,维护了很多的项目,发现很多小伙伴在设计接口的时候并不会考虑接口性能。或者说当时那么设计确实不会存在接口的性能问题,可是随着业务的增长,数据量的增长,
接口性能问题就暴露出来了。就像上面小猫吐槽的,接口又又又慢了,又在报接口慢警告了。


举个例子,查询API,当数据量少的情况下,一个List作为最终返回搞定没有问题的。但是随着时间的推移,数据量越来越大,List能够cover吗?显然是不行的,此时就要考虑是否需要通过分页去做。
所以原来的List的接口就必须要改造成分页接口。


当然关于API性能的优化提升,老猫整理了如下提升方式。


1、缓存:CRUD的读写性能毕竟是有限的。所以对某些数据进行频繁的读取,这时候,可以考虑将这些数据缓存起来,下次读取时,直接从缓存中读取,减少对数据库的访问,提升API性能。


2、索引优化:很多时候接口慢是由于数据库性能瓶颈,如果不用上述提及的缓存,那么我们就需要看一下接口究竟是慢在哪个环节,可能是某个查询,可能是更新,所以我们就要分析
执行的SQL情况去添加一些索引。当然这里涉及如何进行MYSQL索引优化的知识点了,老猫在此不展开。


3、分页读取:如上述老猫举的例子中,针对的是那种随着数据量增长暴露出来的,那么我们就要对这些数据进行分页读取处理。


4、异步操作:在一个请求中开启多任务模式。


异步操作模式


举个例子:订单支付中,支付是核心链路,支付后邮件通知是非核心链路,因此,可以把这些非核心链路的操作,改成异步实现,
这样就可以提升API的性能。常用的异步方式有:线程池,消息队列,事件总线等。当然自从Java8之后还有比较好用的CompletableFuture。


5、Json序列化:JSON可以将复杂的数据结构或对象转换为简单的字符串,以便在网络传输、存储或与其他程序交互时进行数据交换。
优化JSON序列化过程可以提高API性能。使用高效的序列化库,减少不必要的数据字段,以及采用更紧凑的数据格式,都可以减少响应体的大小,从而加快数据传输速度和解析时间。


6、其他提升性能方案:例如运维侧提升带宽以及网速等等


上述罗列了相关API性能提升的一些措施,如果大家还有其他不错的方法,也欢迎留言。


总结


谈及软件中的设计,无论是架构设计还是程序设计还是说API设计,
原则其实都差不多,要能够松耦合、易扩展、注意性能。遵循上述这些API的设计规则,
相信大家都能设计出比较丝滑的API。当然如果还有其他的API设计中的注意点也欢迎在评论区留言。


作者:程序员老猫
来源:juejin.cn/post/7369783680427409418
收起阅读 »

请大家一定不要像我们公司这样打印log日志

前言 最近接手了公司另一个项目,熟悉业务和代码苦不堪言。 我接手一个新项目,有个习惯,就是看结构,看数据库,搜代码。 其中搜代码是我个人这些年不知不觉形成的癖好,我下面给大家展示下这个小癖好。 正文 我面对一个到手的新项目,会主动去搜索一些关键词...
继续阅读 »

前言



最近接手了公司另一个项目,熟悉业务和代码苦不堪言。




我接手一个新项目,有个习惯,就是看结构,看数据库,搜代码。




其中搜代码是我个人这些年不知不觉形成的癖好,我下面给大家展示下这个小癖好。



正文



我面对一个到手的新项目,会主动去搜索一些关键词,让我对这个项目有个整体健康的认识。



1、直接打印堆栈



比如搜索了printStackTrace(),目的是为了看这个项目中有多少地方直接打印了堆栈。




不搜还好,一搜,沃日,这滚动条,是奏响我悲痛的序章,竟然到处都是这种打印,而且是release分支。



1.png



我抽点了一些,看看具体是怎么写的,比如下面这样。



2.png



再比如下面这样,我反正长见识了,也可能只是我不会。



3.png


2、堆栈+log



比较典型的可能是下面这样,我以前就见过不少次,堆栈和log混合双打。



4.png



还无意间发现了这样的打印方式,log、堆栈、throw,纵享丝滑,一气呵成,让我们一起摇摆,哎,一起摇摆哎~



5.png


3、log+Json



最后这种,我怀疑是正在看文章的很多人就干过的,入参打印JSON,舒爽的做法,极致的坑爹。




我公司这个更酸爽,用的还是FastJson。



6.png


4、小插曲



写到这里,我可以告诉大家我写这篇文章的初衷不是我想教大家学习,因为这就是常识的东西。




我是因为今天的一件事感到意外。




我同组的工作了12年的Java工程师,做过非常多的项目,也确实很有经验且有责任心的同事。




他也写过这样的代码,因为我用IDEA查看了提交人,其中就有他的贡献。




另外,我有把上面log+堆栈+throw的写法给他看看,他的回答非常理所当然。




“这有问题吗,没报错啊”




我当场石化了,然后尴尬的笑笑就聊别的话题了。




讲这个小插曲的原因是什么,一叶知秋,从他身上我能断定,这样的工程师比比皆是。




干了这么多年,连个基本的日志规范都没有概念,哪怕不看什么阿里编码规范,至少对基础性的东西有个了解吧。



5、日志规范


所以,我专程又把以前分享过给大家的阿里巴巴《Java开发手册(黄山版)》掏出来,找出了里面日志规范着重说明的这部分。



正确的打印日志方式如下:



7.png



再看这个,第8条,禁止直接打印堆栈。




第9条,正确的打印异常日志的规范,我本人也一直都是第9条这种方式打印的。




另外,第10条说的很清楚,为什么不要在log里面用JSON转换工具,说简单点就是可能会报错,然后导致业务不走了。




一个日志打印本来是辅助排查问题用的,结果影响了正常业务流程,你说这是不是隐患。



8.png



而且,还告诉你了要如何打印入参,就是用toString()方法就行。




看看,写得多好,但是有多少人真的看了,都像你买的网课一样存在那里摆烂了吧。



总结



希望大家认真看一看,虽然简单,可很多程序员就差这么点意思,还是要养成好习惯哦。




作者:程序员济癫
来源:juejin.cn/post/7275974397005201449
收起阅读 »

文科生在三本院校,读计算机专业

6岁,进入村小,一年级,老师问我的梦想是什么,我说我长大了我要成为科学家。9岁,三年级,知道科学家不现实,开始学习英语。又因为科学家英语不好发音,于是我的梦想变了,长大了我要成为经理。11岁,五年级,开始成为网瘾少年,边玩游戏边挣钱才是我的梦想,所以我长大了我...
继续阅读 »

6岁,进入村小,一年级,老师问我的梦想是什么,我说我长大了我要成为科学家。

9岁,三年级,知道科学家不现实,开始学习英语。又因为科学家英语不好发音,于是我的梦想变了,长大了我要成为经理。

11岁,五年级,开始成为网瘾少年,边玩游戏边挣钱才是我的梦想,所以我长大了我要做网吧管理员或开一间电脑修理店。

12岁,小升初,差1.5分进区内重点中学,调剂到普通中学。

中学情况:至少一半以上的人无法考进高中,校园暴力也很是常见的事。

初中三年,有沉沦过,也有突击努力过,受环境影响大,人是易染的

15岁,初中升高中,正常发挥,进入普通高中。

高中情况:至少一半以上的人无法考到本科分数线,年级内有一半的班是艺术类的,文理科几乎无211、985。文理科若有实力上重点本科(一本),能稳坐年级前3。

17岁,高二分文理艺术,数理太差,没钱且没天赋搞艺术类,选择了文科

高二暑假玩梦幻挣了7000+,真正取出到银行卡,但又成功戒掉了网瘾:每天12小时以上在游戏内做着重复的事,性质发生了变化,最终卖号,累计收益几万块。

18岁,高考正常发挥,考入三本院校,在广东也叫2B院校。

高三这一年要说梦想,是想考上二本,少付点学费。不过平时试卷测试或模拟考,始终在二本线上下徘徊,最终离二本分数线差4分。

我是2B里靠前的

18岁,填选计算机专业,在文科院校学习计算机

最初志愿选的是工商管理,后来我爸不知道在哪听说到互联网+,最后就让我选了计算机科学与技术专业。


《底层程序员》我的故事持续连载中,下一篇:「上课,是耽误我学习了

收起阅读 »

学校上课,是耽误我学习了。。

>>上一篇(文科生在三本院校,读计算机专业) 2015年9月,我入学了。 我期待的大学生活是多姿多彩的,我会参加各种社团,参与各种有意思的活动。 但我是个社恐,有过尝试,但还是难以融入各种社交活动。 学习,我是有想过的。 学校开设的C++课程已经上...
继续阅读 »

>>上一篇(文科生在三本院校,读计算机专业


2015年9月,我入学了。


我期待的大学生活是多姿多彩的,我会参加各种社团,参与各种有意思的活动。


但我是个社恐,有过尝试,但还是难以融入各种社交活动。


学习,我是有想过的。


学校开设的C++课程已经上了一段时间,但我无法理解双层for循环执行过程、亦无法理解代码最终运行效果是黑框字符,更无法理解算法的美。


打印杨辉三角形?这到底有什么用啊!


面向对象?不都是大象放进冰箱吗!


我开始觉得校园生活很无趣,那回归老本行打游戏吧。


为了报考学计算机,我还买了游戏本呢。


高中的时候接触过DOTA、LOL这类游戏,刚上手的时候喜欢的不得了,实时+炫酷技能+公平,让我感叹这才是游戏啊!梦幻那都是什么坑人的东西。


不过我没有玩下去,我竞技水平太菜了,反应力跟不上。


不过现在有960M显卡的加持,我怒下了几款三A大作,却发现自己晕3D。


好了,游戏不用玩了。


后来,我沉迷各种电影&动漫&悬疑小说。


我这人就爱看经典,甭管我看没看懂,反正豆瓣低于8分我不看。


除了作品本身,我看别人影评也是一种享受,就爱看他们是怎么吹的。每每看到,原来这里还能这样解读,我就浑身发爽。


有过好几次,宿舍午休关了灯,外面下着雨,下午没课,我躺着床上看悬疑小说。很快,我刷完一本,找了些影评看,心满意足。


但当我静下来时,负罪感油然而生。


给这么贵的学费,我好像什么都没学到,等毕业找工作的时候,我该怎么办。


负罪感是暂时的,吃顿饭就消散了。


每当焦虑时,我就爱去知乎搜索:


「C++好还是Java好」、「如何入门编程」、「计算机什么方向容易就业」、「编程学到什么程度能找到工作」、「Java的学习路线」


看到满意的回答就点个收藏。


那时候的知乎百花争鸣,不像现在动不动就卖课。


「程序员的三大浪漫」、「数学是计算机的基础」这些内容都是真大道理,毕竟这么多大佬点赞了。


但越看这些,就越发感觉编程和计算机领域遥不可及。


这期间,课我有好好上,作业也有好好做,但编程是没能入门


上课老师对着PPT讲述一番之后,用Microsoft Visual C++ 6.0手敲着各种字符,我都不知道老师是怎么把代码记下来的。


大一就学个C++课,计算机类的课程占比很少,有时我还怀疑是不是读的计算机专业。


很快啊,大一学期快过去了,我在学期末意外地下载了些网课,看了几集,得出的结论:


原来,上课,是耽误我学习了


不是我学不会,不够努力,是老师教不好




《底层程序员》我的故事持续连载中,下一篇:「爪哇,我初学乍道


作者:Java3y
来源:juejin.cn/post/7370955971017146378
收起阅读 »

我为展开收起功能做了动画,被老板称赞!

web
需求简介 这几天接了个新项目,需要实现下图中左侧边栏的菜单切换。这种功能其实就是一个折叠面板,实现方式多种多样。 实现上面的功能,无非就是一个v-show的事儿,但没有过渡,会显得非常生硬。想添加一些过渡效果, 最简单的就是使用element ui、或者a...
继续阅读 »

需求简介


这几天接了个新项目,需要实现下图中左侧边栏的菜单切换。这种功能其实就是一个折叠面板,实现方式多种多样。



实现上面的功能,无非就是一个v-show的事儿,但没有过渡,会显得非常生硬。想添加一些过渡效果,



最简单的就是使用element ui、或者ant的折叠面板组件了。但可惜的是,我们的项目不能使用任何第三方组件库。



为了做好产品,我还是略施拳脚,实现了一个简单且丝滑的过渡效果:



老板看后,觉得我的细节处理的很好,给我一顿画饼,承诺只要我好好坚持,一定可以等到升职加薪!当然,我胃口小,老板的饼消化不了。我还是分享一下自己在不借助第三方组件的情况下,如何快速的实现这样一个效果。


技术实现方案


业务分析


仔细观察需求,我们可以分析出其实动画主要是两个部分:一级标题的箭头旋转二级标题区域的折叠展开



我们先实现一下基本的html结构:


<template>
<div class="nav-bar-content">

<div class="header-wrap" @click="open = !open">
<span class="text">自动化需求计算条件输如</span>
<span class="arrow">
>
</span>
</div>

<div v-show="open" class="content">
<p>算法及跃变计算条件</p>
<p>空间品质判断条件</p>
<p>需求自动计算条件</p>
<p>通风系统</p>
</div>

</div>

</template>

<script setup>
const open = ref(false);
</script>


上述代码非常简单,点击一级标题时,更改open的值,从而实现二级标题的内容区域展示与隐藏。


箭头旋转动画



实现箭头旋转动画其实非常容易,我们只要在红色面板展开时,给箭头添加一个新的类名,在这个类名中做一些动画处理即可。


<template>
<div class="header-wrap" @click="open = !open">
<span class="text">自动化需求计算条件输如</span>
<span class="arrow flex-be-ce" :class="{ open: open }">
>
</span>
</div>

</template>
<style lang="less" scoped>
.arrow {
width: 16px;
height: 16px;
cursor: pointer;
margin-left: 1px;
transition: transform 0.2s ease;
}
.open {
transform: rotate(90deg);
transition: transform 0.2s ease;
}
</style>


上述的代码通过 CSS 的 transform 属性和动态绑定open类名实现了箭头的旋转效果。



注意:arrow也需要定义过渡效果



折叠区域动画效果


要实现折叠区域的动画效果,大致思路和上面一样。


使用vue的transition组件实现


借助vue的transition组件,我们可以实现折叠区域进入(v-show='true')和消失(v-show='fasle')的动画。一种可行的动画方案就是让面板进入前位置在y轴-100%的位置,进入后处于正常位置。



<template>
<div class="nav-bar-content">

<div class="header-wrap" @click="open = !open">
<span class="text">自动化需求计算条件输如</span>
<span class="arrow" :class="{ open: open }">
>
</span>
</div>

<div class="content-wrap">
<Transition>
<div v-show="open" class="content">
<p>算法及跃变计算条件</p>
<p>空间品质判断条件</p>
<p>需求自动计算条件</p>
<p>通风系统</p>
</div>
</Transition>
</div>

</div>

</template>

<script setup>

const open = ref(false);
</script>


<style lang="less" scoped>
.v-enter-active,
.v-leave-active {
transition: transform 0.5s ease;
}
.v-enter-from,
.v-leave-to {
transform: translateY(-100%);
}
</style>


上述效果有一点瑕疵,就是出现位置把一级标题盖住了,我们稍微修改下


<div class="content-wrap">
<Transition>
<div v-show="open" class="content">
<p>算法及跃变计算条件</p>
<p>空间品质判断条件</p>
<p>需求自动计算条件</p>
<p>通风系统</p>
</div>
</Transition>

</div>

.content-wrap {
overflow: hidden;
}


使用动态类名的方式实现


效果好很多!但这种效果和第三方组件库的效果不太一致,我们以element的折叠面板效果为例:



我们可以发现,它的这种动画,是折叠面板的高度从0逐渐增高的一个过程。所以最简单的就是,如果我们知道折叠面板的高度,一个类名就可以搞定!


<template>
<div class="nav-bar-content">

<div class="header-wrap" @click="open = !open">
<span class="text">自动化需求计算条件输如</span>
<span class="arrow flex-be-ce" :class="{ open: open }">
>
</span>
</div>

<div class="content-wrap" :style="{ height: open ? '300px' : 0 }">
<div class="content">
<p>算法及跃变计算条件</p>
<p>空间品质判断条件</p>
<p>需求自动计算条件</p>
<p>通风系统</p>
</div>
</div>

</div>

</template>

<script setup>
const open = ref(false);
</script>


<style lang="less" scoped>
.content-wrap {
height: 0;
transition: height 0.5s ease;
}
</style>



如果这个折叠面板的内容通过父组件传递,高度是动态的,我们只需要使用js计算这里的高度即可:


<template>
<div class="nav-bar-content">

<div class="header-wrap" @click="open = !open">
<span class="text">自动化需求计算条件输如</span>
<span class="arrow flex-be-ce" :class="{ open: open }">
>
</span>
</div>

<div class="content-wrap" :style="{ height: open ? '300px' : 0 }">
<div class="content" ref="contentRef">
<slot></slot>
</div>
</div>

</div>

</template>

<script setup>
const open = ref(false);
const contentRef = ref();
const height = ref(0);
onMounted(() => {
height.value = contentRef.value.offsetHeight + 'px';
});
</script>


<style lang="less" scoped>
.content-wrap {
height: 0;
transition: height 0.5s ease;
}
</style>


这样,我们就通过几行代码就实现了一个非常简单的折叠面板手风琴效果!



总结


要想实现一个折叠面板的效果,最简单的还是直接使用第三方组件库,但是如果项目不能使用其他组件库的话,手写一个也是非常简单的!也希望大家能在评论区给出更好的实现方式,供大家学习!


作者:石小石Orz
来源:juejin.cn/post/7369029201579278351
收起阅读 »

为什么年轻人要珍惜机会窗口

今天来跟大家分享一下什么是机会窗口以及为什么要珍惜机会窗口?首先从我个人的经验出发,我觉得不管是在学习,在职业,在投资,现在社会各个方面都是有很多非常好的机会的。但是这些好的机会又不经常有,那到底如何定义好机会,又如何抓住机会?那这里面先说一下什么叫好的机会。...
继续阅读 »


今天来跟大家分享一下什么是机会窗口以及为什么要珍惜机会窗口?首先从我个人的经验出发,我觉得不管是在学习,在职业,在投资,现在社会各个方面都是有很多非常好的机会的。但是这些好的机会又不经常有,那到底如何定义好机会,又如何抓住机会?那这里面先说一下什么叫好的机会。


什么是好机会


就以职业的成长性来说,互联网整个行业的二十年蓬勃发展就是极好的一个机会,大概从20年起到如今这个时间段都有一个非常好的机会,那指的就是哪怕你的能力稍微弱一点,你都能够在这个机会里面找到自己的红利。比如我有很多稍微找我几届的同事或者主管,他们可能在学历或者能力方面都没有特别高,但是正因为赶上了红利,他们的晋升特别快,拿到了股票也特别多,我好几个同事基本上在上海或者杭州都有两三套房,并且还有大量的现金。甚至有一些大专的同事,都拿到大量的股票,接近财富自由。


所以这种机会窗口是整个行业变革,整个现代社会发展带来的,它打开了一扇可以改变命运的窗口。这种时间窗口相对来说会比较长,特别是相对一个人的职业三十年来说。而且这种行业的机会,可能就有持续五年或者十年这样的时间。而在这样的机会窗口内,你不管是哪个点入局都能吃到一定的发展红利。


比如我记得早个五六年,很多人在找工作的时候,往往会纠结于去百度还是腾讯或者是阿里,但实际上我们发现站在更高,更长远的角度来说,他们选择任何一个公司收获到的都非常的丰厚,相比现在的毕业生,哪怕是双985可能也是无法找到一份工作,想想那时候是不是很幸福?在这种大背景下,在机会窗口来临的时候,你选错了,选的不是很好,都没有关系,你都能够收获到足够的红利,最多就是你赚50万还是100万的区别,而时代没有的话,上限就是赚10万。


除了这个例子之外,还有一个红利机会点就是房地产。我知道在差不多2005年~2018年这个时间段里面,只要你买房基本上都是赚的,所以我很多同学往往都有一个非常巨大的认知论,就认为他买房赚钱是因为他牛逼,他地段选的好,户型选的好,他完全归因于他买的房价大涨是因为眼光好,怎么样怎么样才能赚到钱,而实际上这只是时代给他的红利而已,其实再往回倒个七八年你在哪里买房都是赚的。但实际上以我的经验来看,不管那个时候,哪怕你在小城市买一套房子,涨幅可能都是两三倍的。


所以当时的眼光和认知和选择能力确实会决定了你的资产增值多少,但是只要在那个红利周期内,你做的选择大概率都不会太差,这也是雷军所说,站在风口上的猪也可以飞起来,说的就是这个道理。



这就是整个时代给我们的窗口,这个窗口可能会给的特别大,而且很多时候在这个周期里面,你根本感觉不到这是时代给你的机会,你只是做你正常的操作,到了指定的时间去指定的公司,去选合适热门专业,去买认为合适的房子,你觉得很自然,但实际上从后面再看,你会发现你在十年前做的选择和十年后做的选择成本、难度以及你付出的代价完全不一样。同样是89平米的房子,放在2010年就是3000一平米,放在现在就是8万一平米。同样是去阿里巴巴,以前大专就行,现在本硕985都直接被Pass。


上面说的都是比较大的机会,那我再说一个相对来说比较小的窗口。这些非常大的机会窗口还是依赖于各种不同不一样的大背景,但是有很多机会并没有像这种时代给的机会一样,可以有长达五年,十年你可以认真去选,你可以去大胆的犯错和试错,选错了你重新再来一次就可以了,但是我们在实际工作里面,我们碰到的一些机会点,其实时间窗口非常的短。如果你稍微不慎,可能就错过了这个机会,而等待下一个机会就不知道猴年马月了,所以我们就要在这个地方要抓住那稍纵即逝的机会窗口。



我举一个例子,比如说这两年是低代码的元年,而这个时候如果你之前刚好一直在从事低代码或者低代码相关的工作,那么到了这两年,你的议价空间是非常大的,因为很多公司都在如火如荼的去做这块的业务,在短时间内是没有办法慢慢培养出或者招聘到这类专才,所以往往公司愿意溢价去花费大价钱去购买和招聘相关的同学,所以这个时候如果你抓住了机会,你可以得到一个很高的议价,比如说层级直接变高了一层或者你的总包直接变成了两倍,甚至非常有机会作为骨干负责人拉起一支团队,那么你进入管理岗位也就水到渠成了。


为什么机会有窗口


而这种机会窗口往往只有半年,一年或者最多两年,因为到了一两年之后,有很多的同学也感知到了这个先机,往往就会把自己的精力投到这一块来,那么意味着供需就发生了变化,供应方就会越来越多,那么就使得需求方有溢价的能力,这个时候到了两年之后可能就完全拉平了,这个低代码行业跟其他行业变得完全一样,甚至再往后人才堆积的更加的过分,你可能连这个机会都没有了,只剩下被选择的命运。历史历代,都演绎着完全相同的剧本。


到了直播行业也是一样,在直播刚刚兴起的时候,如果你恰巧做的是相关业务,这个时候你跳过去往往会能够涨薪特别高,工资的幅度也是特别高,所以在这个时候你有充分的议价权,但是窗口我们也知道往往只有几年,而且在互联网这么变化快的情况下的话,时间可能会进一步缩短,比如这两年已经到了直播的红海,基本上该用直播的用户已经到顶了,这个时候虽然还有大把的招聘,但需求实际上已经是强弩之末了。


随着人口红利到底的时候,我们所谓的互联网这些机会的窗口实际上已经是没了,变得普普通通的一份职业而已,而且这个时候入局往往有可能会遭受灭顶之灾,比如说最近就听说到整个直播行业要整顿,一旦业务发生了整顿,对人才的需求的调整就会变得非常的明显,往往再激烈一点可能就会快速裁员,不要说红利了,拿到的全部是负债。


再往小的一些说,可能针对每个人的职业窗口也是不一样的,比如说对于有些大企业,有一些管理的岗位,但往往是因为原管理的同学离职或者新增的岗位,这个时候会有短时间的招聘名额来等待这个位置,而一旦你错过了这个机会以后,这个位置没了以后,可能这个坑位就不需要人了。这个时候不是你能力好不好的问题,是有没有坑位的问题。


所以好机会往往只是一瞬间而已,很多同学担心稳定性,希望在一个地方一直苟着求稳定,这个其实跟体制内没有任何的区别。风险和收益从哲学层面上来说,都是相对的,或者说没有决定的风险,也没有决定的稳定,风险和稳定阶段性只能取其一,长期看稳定和风险是互相转化的。我经常听到有人说大厂稳定,但是实际上我们在分析背后的原因,大厂稳定本身就是个伪命题。又稳定,又高薪,又轻松,这是不可能的。所以我称之为「工作不可能的三角特点」。


但很多人说我能否要里面的两个因素,我要稳定要高薪但是我愿意加班吃苦。


对不起,这个其实也是不可能的。我们可以站在企业的角度来考虑一下,一旦我这个工作特别的高薪又稳定的情况下的话,那虽然你干的很苦,但我始终在人力成本特别充分的情况下的话,公司能找到更好的替代者来。同样的工作量,但是花更少的钱来解决,说白了大部分所谓的高薪岗位没有什么严格的技术壁垒。


所以我们说过的,站在更大的角度来说,互联网也是一个机会窗口,因为过了这个窗口之后,可能你想加班加点熬夜,你可能都拿不到这样的一个薪水和待遇。


如何抓住机会窗口


反而换一个角度来说,我们一定要抓住这样的机会窗口,这样的机会窗口可以给我们的发展带来一个质的变化,当然也有很多时候我们会做一些错误的选择,比如说我们找到了一个我们认为好的机会,但实际上这个机会是有问题的,比如说我去了某一个创业公司,原本以为会有巨大的发展,但是后面倒闭了。当然这个也是一种博弈,这里面非常考核一个同学的综合的认知能力、选择能力和纠错能力。不仅要判断能否找到合适的机会,还要在碰到了困难的时候能够去快速的去纠错。


从我的例子来看,如敢于去挑战这种新机会的同学,哪怕其中有一些不如意的变动,但是大概率他的结果大概率不会太差。比如我有个同学从集团跳槽到蚂蚁国际,呆了一年就觉得部门有问题,后面又去了字节头条,现在也非常稳定。还有一个同学出去创业,也不顺利,但是后面又折腾成了另外一个大型公司的高级主管。


反而是事事求稳,稳住某一个大厂,稳住某一个职位,稳住每一个薪水,到了最后往往收益会越来越小,直到最后完全被动。整体上来看,整个社会会把更多的报酬分向于这些敢于挑战,敢于冒险,敢于拼搏的人的,而不会把大量的资源分享到又稳定,又顽固,又不愿意改变的这群人,这是当前社会的游戏规则。这个在大数据上面完全是合理的,只不过落到每个人的头上的尺度和比例会有点不一样。


所以站在我现在的角度上来看,我觉得所有的想向上奋进的同学都应该主动抓住变革的机会。因为这个好机会可能对在你的人生来说,几十年可能就这么一两次,甚至有些都是完全为你量身定做的机会,如果你一旦错过了以后,可能你抓住下一个机会的成本和代价就变得会非常的大。



尤其是年轻人更应该去折腾,因为你的试错的成本会非常低,当你发现了你的错误决策以后,你能够快速的去更正,去变化,所以在年轻的时候往往就应该多折腾一点,善于去准备好去等待好的机会,如果机会来了,大胆的出击。




作者:ali老蒋
来源:juejin.cn/post/7296865632166805513
收起阅读 »

解决LiveData数据倒灌的新思路

⏰ : 全文字数:5500+ 🥅 : 内容关键字:LiveData数据倒灌 数据倒灌现象 对于LiveData“数据倒灌”的问题,我相信很多人已经都了解了,这里提一下。所谓的“数据倒灌”:其实是类似粘性广播那样,当新的观察者开始注册观察时,会把上次发的最后一...
继续阅读 »

⏰ : 全文字数:5500+

🥅 : 内容关键字:LiveData数据倒灌



数据倒灌现象


对于LiveData“数据倒灌”的问题,我相信很多人已经都了解了,这里提一下。所谓的“数据倒灌”:其实是类似粘性广播那样,当新的观察者开始注册观察时,会把上次发的最后一次的历史数据传递给当前注册的观察者


比如在在下面的例子代码中:


val testViewModel = ViewModelProvider(this)[TestViewModel::class.java]
testViewModel.updateData("第一次发送数据")
testViewModel.testLiveData.observe(this,object :Observer{
override fun onChanged(value: String) {
println("==============$value")
}
})

updateData方法发送了一次数据,当下面调用LiveData的observe方法时,会立即打印==============第一次发送数据,这就是上面说的“数据倒灌”现象。


发生原因


原因其实也很简单,其实就是 LiveData内部有一个mVersion字段,记录版本,其初始的 mVersion 是-1,当我们调用了其 setValue 或者 postValue,其 mVersion+1;对于每一个观察者的封装 ObserverWrapper,其初始 mLastVersion 也为-1,也就是说,每一个新注册的观察者,其 mLastVersion 为-1;当 LiveData 设置这个 ObserverWrapper 的时候,如果 LiveDatamVersion 大于 ObserverWrappermLastVersionLiveData 就会强制把当前 value 推送给 Observer


也就是下面这段代码


    private void considerNotify(ObserverWrapper observer) {
if (!observer.mActive) {
return;
}

if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
// 判断observer的版本是否大于LiveData的版本mVersion
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
observer.mObserver.onChanged((T) mData);
}

所以要解决这个问题,思路上有两种方式:



  • 通过改变每个ObserverWrapper的版本号的值

  • 通过某种方式,保证第一次分发不响应


解决方法


目前网络上可以看到有三种解决方式


每次只响应一次


public class SingleLiveData<T> extends MutableLiveData<T> {
private final AtomicBoolean mPending = new AtomicBoolean(false);

public SingleLiveData() {
}

public void observe(@NonNull LifecycleOwner owner, @NonNull Observersuper T> observer) {
super.observe(owner, (t) -> {
if (this.mPending.compareAndSet(true, false)) {
observer.onChanged(t);
}

});
}

@MainThread
public void setValue(@Nullable T t) {
this.mPending.set(true);
super.setValue(t);
}

@MainThread
public void call() {
this.setValue((Object)null);
}
}

这个方法能解决历史数据往回发的问题,但是对于多Observe监听就不行了,只能单个监听,如果是多个监听,只有一个能正常收到,其他的就无法正常工作


反射


这种方式就是每次注册观察者时,通过反射获取LiveData的版本号,然后又通过反射修改当前Observer的版本号值。这种方式的优点是:



  • 能够多 Observer 监听

  • 解决粘性问题


但是也有缺点:



  • 每次注册 observer 的时候,都需要反射更新版本,耗时有性能问题


UnPeekLiveData


public class UnPeekLiveData extends LiveData {

protected boolean isAllowNullValue;

private final HashMap observers = new HashMap();

public void observeInActivity(@NonNull AppCompatActivity activity, @NonNull Observer super T> observer) {
LifecycleOwner owner = activity;
Integer storeId = System.identityHashCode(observer);
observe(storeId, owner, observer);
}

private void observe(@NonNull Integer storeId,
@NonNull LifecycleOwner owner,
@NonNull Observer super T> observer) {

if (observers.get(storeId) == null) {
observers.put(storeId, true);
}

super.observe(owner, t -> {
if (!observers.get(storeId)) {
observers.put(storeId, true);
if (t != null || isAllowNullValue) {
observer.onChanged(t);
}
}
});
}

@Override
protected void setValue(T value) {
if (value != null || isAllowNullValue) {
for (Map.Entry entry : observers.entrySet()) {
entry.setValue(false);
}
super.setValue(value);
}
}

protected void clear() {
super.setValue(null);
}
}

这个其实就是上面 SingleLiveData 的升级版,SingleLiveData 是用一个变量控制所有的 Observer,而上面采用的每个 Observer 都采用一个控制标识进行控制。
每次 setValue 的时候,就打开所有 Observer 的开关,表示可以接受分发。分发后,关闭当前执行的 Observer 开关,即不能对其第二次执行了,除非你重新 setValue
这种方式基本上是比价完美了,除了内部多一个用HashMap存放每个Observer的标识,如果Observer比较多的话,会有一定的内存消耗。


新的思路


我们先看下LiveData获取版本号方法:


int getVersion() {
return mVersion;
}

这个方法是一个包访问权限的方法,如果我新建一个和LiveData同包名的类,是不是就可以不需要反射就能获取这个值呢?其实这是可行的


// 跟LiveData同包名
package androidx.lifecycle

open class SafeLiveData<T> : MutableLiveData<T>() {

override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
// 直接可以通过this.version获取到版本号
val pictorialObserver = PictorialObserver(observer, this.version > START_VERSION)
super.observe(owner, pictorialObserver)
}

class PictorialObserver<T>(private val realObserver: Observer<in T>, private var preventDispatch: Boolean = false) :
Observer {

override fun onChanged(value: T) {
// 如果版本有差异,第一次不处理
if (preventDispatch) {
preventDispatch = false
return
}
realObserver.onChanged(value)
}

}
}

这种取巧的方式的思路就是:



  • 利用同包名访问权限可以获取版本号,不需要通过反射获取

  • 判断LiveDataObserver是否有版本差异,如果有,第一次不响应,否则就响应


我个人是偏向这种方式,也应用到了实际的开发中。这种方式的优点是:改动小,不需要反射,也不需要用HashMap存储等,缺点是:有一定的侵入性,假如后面这个方法的访问权限修改或者包名变动,就无效了,但是我认为这种可能性是比较小,毕竟androidx库迭代了这么多版本,算是比较稳定了。



作者:卒子行
来源:juejin.cn/post/7268622342728171572
收起阅读 »