注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

大专生两年经验的年度总结

  🍉写在前面,00后程序猿(身高183!),今天是12.11,已经可以算的上是23年的末尾了。从21年8月开始入行,已经一坤年的时间了。 🍋年度目标 目标的话,基本是重复上年的操作,没有几条是达成的。 薪资目标        算是勉强到达年前的目标了吧 手...
继续阅读 »

  🍉写在前面,00后程序猿(身高183!),今天是12.11,已经可以算的上是23年的末尾了。从21年8月开始入行,已经一坤年的时间了。


🍋年度目标


目标的话,基本是重复上年的操作,没有几条是达成的。



  1. 薪资目标        算是勉强到达年前的目标了吧

  2. 手写promise全部规范   总体流程是可以的,个别规范还未实现,算成功85%

  3. react          不好估计,是学完了,但是一直没有完整的demo项目,算完成70%

  4. java          只会java基础,算完成15%左右

  5. 《vue的设计与实现》  仅把书买了,进度0%

  6. 个人博客系统      前端完成七七八八了,由于java的原因后端没写,前端完成度:80%,后端:0%

  7. 自己组装个台式电脑   完成完成,这种目标肯定是第一时间完成的

    看了一下上面的目标和完成度的对比,今年真的蛮失败的,尤其是工作上,这个等文章后面再提。


先来说一下组装台式机的过程


  为什么首先写这个呢?肯定不是因为我想炫耀。电脑是四月份开始决定组装的,依据贴吧老哥和自己的要求在JD上选了一些配置(3060ti g6x+12600kf),机箱选的罗宾3,总体体验还不错,尤其这9个棱镜风扇,风扇当初考虑了半天,因为已经买了水冷,只打算再买三个风扇,毕竟我是第一次装机,风扇太多我不一定装的好,后面一想(风扇肯定是越多越帅啊),就多入手了6个风扇。后面又多加了一个发光的显卡支架,最终成了下面的样子(手机像素不行,电脑实物还是很帅的)。


86DE025CB6BFCC2B2E116C511C5EBAD7.jpg


2023这一年我是如何实现上面的目标的?


  工资这方面,没什么好说的,肯定是正常的面试吹嘘。上面列出的几条技术要求,一般都是工作闲的时候写一写,没有刻意要求自己(可能正是这种心态,导致了那么多目标没有完成)。下面的图片便是我今年的所有收获。其实还有一个小目标完成了,上面没有提到,那就是锻炼身体,毕竟钱再多,技术再好都不如身体重要,晚上都会去小区广场那边进行跳绳,从起初的一天500已经变成一天3000个^_^


image.png


然后就是这两年就职的公司情况


  一坤年的时间,我已经入职了三家公司,离职的原因都是一些不可抗力因素。



第一家:南京,公司是做自研项目的,开发团队有10个人左右,前端最多的时候是4个前端,单休,每周三下午固定时间有下午茶,工作很辛苦,但是公司氛围很不错,每个人都很好沟通,都很照顾我。后面离职是因为老板认为南京这边人力成本有点高,把公司搬回老家了。




第二家:南京,公司算是做自研项目的,为什么说算是呢?这边的主要业务是做自己的项目,然后把这个项目的核心内容卖出去(嵌套到甲方的项目中),当时入职时就我一个前端,4个后端,老板本身也是后端,一个测试,大小周,一般是同时进展两个项目(老板和领导能力比较强,他俩负责一个项目,我和另外两个后端负责另一个项目),每周三下午是固定的羽毛球运动,小零食管饱,公司氛围同第一家一样很好,每个人都很好沟通,第一天入职时,老板会请吃饭,一般是一个项目结束后会团建聚餐一次。后面离职是因为当时公司暂时没项目,老板和我们讨论:他想降低整体的薪资(是讨论,并不是那种直接通知),我和几个同事都能理解老板,但是都表示不能接受(这是很现实的事情),老板最终给予了我们三个n+1(已经很好了)。`




第三家:依旧在南京,也就是现在在职的一家公司,仍然是自研,工作内容十分轻松,一共是三个开发,年终奖、午餐、双休这些都有,并且项目已经很成熟了,基本不会有什么大改动(至少目前是,来这边一年了都是一些小问题的修改)。并且公司基本不会存在倒闭的问题(老板的其它业务需要这个系统,并且其它业务十分赚钱,老板是身价很高的那种人)。从上面的几条内容来看,这个公司应该是大多数人心中不错的公司了,但是可能有人会发现我没有提到公司氛围这个内容:

  接下来我要提的便是氛围了,我只能说差,并且不是一般的差。为什么这么说呢?首先我们是只有三个开发,按常理来说:人越少,氛围会越好,但是我们办公的地方很独特,我们是同公司其它业务的人一同办公的,都是在同一个场地办公,问题的一小部分就出现在这,其它业务的人员都是官职很高的老领导(你可以这么理解:和你一起办公的都是清华大学、北京大学各大高校的退休校长、退休教授),如果是同样业务,那倒很好,说不定咱还能攀攀别人的关系的,可惜不是,并且我是基本不敢进行激情讨论的,我这边的领导怕激情讨论影响他人。并且在这个公司让我学会了语言的艺术,勾心斗角的人是真TiMi的多,到处都是人情世故、阿谀奉承(排除个别不是的),这个时候可能又会有人说,小团队氛围干嘛要受别人的影响呢,做好自己就可以了。

  对于上面的问题,我只能说,哥们,真不能怪我们,我本人还是蛮开朗的,在我之前的两位同事还未离职的时候,我们三个的氛围也算还不错吧,后面他俩因为种种原因离职了。后面就入职了另一个同事,可能是性格问题,我们之间的沟通很少,除了对接口的时候会说几句话。可能又有人会说了,领导主持一次聚餐,大家互相熟悉一下不就好了吗。看到这种,只能微微一笑,我可以这么给你解释我的领导,我之所以能学会人情世故、阿谀奉承都得去谢谢他。对外他是唯唯诺诺,对内是重拳出击。变脸比翻书还快,上级对他有好脸色,他对我们不一定有好脸色,上级对他没好脸色,他对我们必定是重拳出击(这个哥们让我见识到了心态可以决定年纪,因为他真会装孙子)。比活火山还离谱,事情多的时候嫌我们做的不行、做的慢,事情少的时候,嫌我太闲,怎么看怎么不顺眼,我是真提莫的无语了。并且这个人十分喜欢讲冷笑话(至少我是这么认为的),十分冷的那种。



image.png


image.png


  上面两张截图,是我周末在外面玩,然后领导叫我来加班的乌龙事情,我只能说:罕见,这种罕见的极品是怎么混上领导的(我刚入职的时候只有他一个光杆司令)。工作中的甩锅事情我就不想说了,因为根本说不过来(我不知道这个甩锅在其它公司多不多),在我之前的公司出了问题基本都是领导自己揽下来(无论是不是他的问题),并且这种人我们都很听他的,也很佩服这种人。但是这个极品就不一样了只要是对外演示的时候,无论是谁的问题,永远是甩锅给我们,绝对不能因为这个问题影响他装逼,是真极品。日常中还有更极品的事情我就不写了(只能说比三国杀还恶心)。


🍍日后的计划


  换工作,年后必须先把工作换了,哪怕是裸辞,这种极品领导很难相处。其次是java,java已经学了两年了,去年的计划就已经有java了,一直没有学,2024一定要学会。然后就是爬武功山,是真想去那看看,另一个目标的话就是要攒一部分钱出来,要把买车提上日程。


作者:外围前端吴彦祖
来源:juejin.cn/post/7310964581052121100
收起阅读 »

设计呀,你是真会给前端找事呀!!!

web
背景 设计:我想要的你听明白了吗,你做出来的和我想要的差距很大,你怎么没有一点审美(你个臭男人,你怎么不按我画的做)! 我:啊?这样自适应不是很好吗,适配了大部分机型呀,而且不会有啥显示的兼容性,避免不必要的客户咨询和客户投诉。 设计: 你上一家公司就是因为...
继续阅读 »

背景



  • 设计:我想要的你听明白了吗,你做出来的和我想要的差距很大,你怎么没有一点审美(你个臭男人,你怎么不按我画的做)!

  • :啊?这样自适应不是很好吗,适配了大部分机型呀,而且不会有啥显示的兼容性,避免不必要的客户咨询和客户投诉。

  • 设计: 你上一家公司就是因为有你这样的优秀员工才倒闭的吧?!

  • :啊?ntm和产品是一家的是吗?





我该如何应对


先看我实现的


b0nh2-9h1qy.gif


在看看设计想要的


9e2b0572-aff4-4644-9eeb-33a9ea76265c.gif
总结一下:



  • 1.一个的时候宽度固定,不管屏幕多大都占屏幕的一半。

  • 2.俩个的时候,各占屏幕的一半,当屏幕过小的时候两个并排展示换行。

  • 3.三个的时候,上面俩,下面一个,且宽度要一样。

  • 4.大于三个的时候,以此类推。



有句话叫做什么,乍一看很合理,细想一下,这不是扯淡么。



所以我又和设计进行了亲切的对话



  • :两个的时候你能考虑到小屏的问题,那一个和三个的时候你为啥不考虑,难道你脑袋有泡,在想一个和三个的时候泡刚好堵住了?

  • 设计: 你天天屌不拉几的,我就要这样,这样好看,你懂个毛的设计,你知道什么是美感和人体工学设计,视觉效果拉满吗?

  • :啊?我的姑奶奶耶,你是不是和产品一个学校毕业的,咋就一根筋呢?

  • 产品:ui说的对,我听ui的。汪汪汪(🐶)


当时那个画面就像是,就像是:





而我就像是
1b761c13b4439463a77ac8abf563677d.png


那咋办,写呗,我能咋办?



我月黑风夜,
黑衣傍我身,
潜入尔等房,
打你小屁屁?



代码实现


   class={[
'group-even-number' : this.evenNumber,
'group-odd-number' : this.oddNumber,
'themeSelectBtnBg'
]}
value={this.currentValue}
onInput={(value: any) => {
this.click(value)
}}
>
...


   .themeSelectBtnBg {
display: flex;
&:nth-child(2n - 1) {
margin-left: 0;
margin-right: 10px;
}
&:nth-child(2n) {
margin-left: 0;
margin-right: 0;
}

}
// 奇数的情况,宽度动态计算,将元素挤下去
.group-odd-number {
// 需要减去padding的宽度
width: calc(50% - 7.5px);
}

.group-even-number {
justify-content: space-between;
@media screen and (max-width:360px) {
justify-content: unset;
margin-right: unset;
flex: 1;
flex-wrap: wrap;
}
}

行吧,咱就这样吧




作者:顾昂_
来源:juejin.cn/post/7304268647101939731
收起阅读 »

完蛋,我失忆了,记一次团建的翻车之旅😵

0x0. 背景介绍 本文首发于我的同名公众号,「野生的码农」。 转眼间,距离上篇文章的发表已经1个多月了,正当我纠结于再写点什么的时候,一场突如其来的事故,让我短暂地失忆了。不久后,流感来袭,发烧咳嗽了许久,完全打断了我的计划。现已基本康复,就分享下团建而引发...
继续阅读 »

0x0. 背景介绍


本文首发于我的同名公众号,「野生的码农」。


转眼间,距离上篇文章的发表已经1个多月了,正当我纠结于再写点什么的时候,一场突如其来的事故,让我短暂地失忆了。不久后,流感来袭,发烧咳嗽了许久,完全打断了我的计划。现已基本康复,就分享下团建而引发的「血案」,顺便聊聊一些倒霉事,博君一笑😁。


2015年6月,刚到腾讯不久,在张北草原团建,有个斗鸡的热身环目,就像这样:


斗鸡


不同的是,我们是在水泥地上开斗的。组里有个身高1米85的大长腿同事,可能因为重心太高或者忌惮对手是领导,被斗输,摔倒在地。看上去摔的并不重,但他脸色不太好,走路一瘸一拐的。


次日,返回北京,积水潭医院,诊断为股骨骨折。先后做了两次大手术,需要卧床休息很久,印象中休了近1年的假。屋漏偏逢连夜雨,他受伤时老婆快生了,算是陪他老婆休了个产假。


因为是在团建中受伤的,走的工伤保险报销医疗费和申请假期。万万没想到,码农有朝一日也能用上工伤保险。


说到保险,当初我老婆生娃,我问 HR 能否用我的生育保险申请北京的什么津贴,她说刚取消了。好吧,那我的生育保险能干啥?答曰:



可以报销男性的输精管结扎术的费用



啊?这。。。此时无声胜有声。。。再见。。。


自「斗鸡」团建之后,我拒绝参加可能有风险的活动,只参加爬山、散步、扯淡之类的「老年团」了,我可不想再休个产假。


10月底,组里在筹划「鬼屋探险」主题的剧本杀团建,在看了宣传片后,我怂了,立刻放弃。


虽然我本科是学医的,多次接触过大体老师,是坚定的无神论者,但我不敢看恐怖片。初中,不小心和老表们一起看了《山村老尸》,连续多个晚上,刚闭上眼就开始放电影,很久才能睡着。某天,房间突然出现一滩水,和电影里的情节一样,吓的我以为女鬼要来杀我了。。。


在我退出之后,陆续又有几位老哥跑路,组织的同学又贴心地攒了个「泡温泉」的老年团,而且还有按摩😍。鹅妹子嘤!这个不戳,零风险,带条泳裤即可,冲!


美中不足的是,稍微有点远,接近70多公里。出发前一天,统计拼车信息,顺路的几位老哥,都跑去捉鬼了,我只能单飞了。温泉在隔壁的巢湖市,不走高速的话,沿途会经过巢湖,应该会有些不错的风景,遂决定骑摩托车去,也就1个多小时。


巢湖


同事问我是否确定骑车去,必须确定啊,小 case。去年7月,历时两天,先后经历暴晒和暴雨的「冰火两重天」,把我的125踏板摩托车从北京骑回合肥,接近1200公里,我每年骑车都超过7000公里,区区70公里,何足挂齿?


然鹅,自认为老司机的我,终究还是翻车了,温泉团建之后,我就基本没碰过摩托车了。有意思的是,至今我都不知道车祸是怎么发生的,那段记忆完全丢失了😵‍💫。。。


0x1. 案发经过


10月27日,周五,阴,20℃,上午10:50,我拿着手机,一脸懵逼地站在一个陌生城市的某大道的辅路上。旁边躺着我的小踏板,后视镜和转向灯摔烂了,手机支架掉在地上,外套、护膝、鞋子都有损坏,手臂、胳膊、膝盖等多处擦伤,正在流血,身上也有点疼,但我完全想不起来发生什么了。


扶起踏板后,发现旁边工地有个保安一直在朝我这边看,遂走上前咨询刚才发生了什么事。他说我自己摔倒了,没有人撞我,我也没撞到人,提醒我满脸是血,让我赶紧去医院。打开前置摄像头看了下,嘴巴肿了,下巴紫了,脸上有几个出血点,皮肉伤,破相而已,不碍事,大不了去挂个「丑科」。


丑科


比起伤痛来说,此刻,我更想知道的是:



我是谁?我在哪?我要去干啥?



走到路边,找块空地坐了下来,试图想起点什么。发现当天早上的部分记忆有问题,时常出现「该内存不能为 Read」,而之前的记忆都可任意读取。


打开微信看了看,有个团建群在活跃着,依稀想起来今天是去泡温泉,但记忆非常模糊,像是在回忆几十年前的事情,又非常像是在做梦。打电话给同事确认了团建的事,我在原地待命,他开车过来接我。


等待的间隙,打电话给老婆,告诉她我因为车祸失忆了,今天早上的事记不清了,问她我是否来泡温泉的。我特意强调了我没在开玩笑,这略带搞笑的提问,似乎把她给吓到了,问我是不是脑子摔坏了。我随后补充道问题不大,我还记得她跟儿子,也记得我父母,同事在过来的路上了,一会就去医院。在确认了我的安全后,她问我工资卡密码,啊,我失忆了,不记得了,再见👋🏻👋🏻


同事到了后,跟刚才的保安又简单聊了下,得知工地上没有摄像头。如果报警的话,应该能调取附近的监控,我嫌麻烦,毕竟除了失忆,也没啥大事,就不麻烦警察蜀黍了。


前往附近医院,急诊,CT,万幸戴了头盔,没有脑出血。急诊医生说他看片不专业,是放射科医生出具的报告。遗憾的是,忘了拿 CT 的报告单了,也不知道上面写的啥,现在只有病历能证明我曾经「脑子坏了」:


病历


完事后,同事帮忙买了碘伏和湿巾,往伤口喷了喷,嘶~,嘶~~,我靠,这么疼😭。消毒时,才发现腹股沟和肚子也有多处血肉模糊的,虽然有点疼,但也都是皮肉伤,并无大碍。


幸好当天气温不高,穿的比较厚,戴了手套,穿了护膝,身上只有一些表面的擦伤。最重要的还是戴了头盔,否则团建就要改吃席了。我记得最后一次看导航是10:30左右,联系同事是10:50,所以我很可能倒地后昏迷了一段时间。


去年从北京骑回合肥,头盔是唯一的装备,连手套都没有,还在高速上跑了300多公里,差点被大车撞到,现在想想都后怕。刚到家时,问我老婆我是否很牛逼,她回复道:



我觉得你是傻逼



彼时,我觉得她不懂我的爱好,话不投机半句多。现在,我认可她的说法。两天,骑行1200公里,确实很牛逼,但也是一种不负责任,毕竟上有老下有小的,没资格去冒险做这种「装逼」的事了。


继续说回团建,虽然下巴和嘴唇受伤了,应该不影响吃饭吧,而且中午有大餐,正好补补身子。出发,干饭🍚。


0x2. 参观温泉


干饭完毕,前往温泉度假酒店。我这一身伤,肯定是不能下水了,本着来都来了的原则,土鳖的我决定进去涨个见识,至少能看看温泉是啥样的。


好么,原来温泉就是公共澡堂子啊,还不区分男女。有意思的是,这澡堂还是露天的:


露天温泉


可能是工作日的缘故,人很少。实话说,我觉得所谓温泉,其实是电加热的(泉)水,不过我没有证据,如果我错了,就当我胡说吧。


澡堂的种类还挺多的,区别就是添加剂不同,有红酒澡堂、牛奶澡堂、玫瑰澡堂等等,建议老板可以再搞个酱香澡堂,说不定会门庭若市。室内有个澡堂养了些小鱼,可去除腿部和脚上的死皮,就是不知道小鱼是否会觉得恶心🤮


同事们泡完澡,去二楼按摩,问我是否一起。拒绝,我当时浑身都在疼,别给我送走了。后来,听他们描述按摩的过程,还挺有意思的,此处省略18万字,付费后可见🐶。


17:00,约了辆货拉拉,正好有同事要提前走,搭他的车去了事故地点。其他同事留下来,晚上又搓了一顿(饭)。


19:00,我和踏板到家了。


货拉拉


就这样,我参加了团建,但又没有完全参加。


啊!这是一次多么难忘的团建啊!!虽然我失忆了,但这次刻骨铭心的团建将永远在我的脑海中「阴魂不散」!!!这一刻,感觉脖子上的工牌都更加鲜艳了,唯一不好的是,工牌是被我的鲜血染红的😎。


写到这,想起了另一件悲催的事:


7月底,带孩子去海边玩,买了条泳裤。傍晚到达目的地后,去附近熟悉下环境,然后,我就摔倒在海边的礁石上了。胳膊破了一大块,碰到海水就生疼。次日,只敢在浅水区站着,泳裤几乎没碰到水。温泉团建,泳裤一直躺在踏板的坐垫下,都没能出来看一眼温泉。。。


0x3. 第二滴血


因为后视镜摔碎了,上路非常危险,手上的伤口尚未愈合,气温也在逐渐走低,今年基本是告别摩托车了,只能被迫开着「仅摇号一年就中标的京牌油车」上下班了。


团建后的第1个工作日晚上,开车前往合肥的「华强北」--「大钟楼」,把车停在城泊画的停车位上。拿到送修的 iPad 后,在开出车位时,看到左侧有个电动车飞奔而来,立刻踩下了刹车。在我静止了3秒后,他还是直接撞了上来,摔倒在地。实话说,我怕他讹我,没有立刻下车。


对方是个年轻小伙,让我帮忙扶一把,说起不来,应该不是碰瓷的,遂下车把他的电动车扶了起来。随后,他自己站起来了,膝盖破了一大块,还在流着血。小伙看着学生模样,戴着个耳环,抽烟,看着还算实诚。我说我停着在啊,你咋还撞上来了?他回答说「我知道,我知道,接电话没注意到」。


我问他伤势怎么样,是否要去医院看看。他说没事没事,不需要去医院,他去找朋友有急事,让我直接走。我不放心,我不知道这算谁的责任,万一他后面有啥事,我这算是肇事逃逸吗?我跟他说先别走,还是让专业的人来处理吧,遂报警并打了保险公司电话。


等待的间隙,看了下车损情况,前保险杠被撞变形了,松松垮垮的,有个雷达被撞脱落了:


撞车现场


很快,交警就来了,出示正件,拍照,如实反映情况,小伙说他是在打电话没注意到汽车。流程走完后,交警说情况复杂,他定不了责,需要我和小伙一起前往某指定的地点定责。


啥??还有交警定不了责的?难道是我全责?我已经停车了啊,交警说那肯定不是。我又问道,如果我自认全责,保险公司是否会认可?交警笑道,那当然可以,保险公司是以交警的结论来赔付的,但是没必要,我不是全责,会影响保费。


交警说我们自己协商也行,但是估计协商不出来,还是一起去定责吧。确实,我不知道责任如何划分,也不知道修车费用,完全没法协商。


小伙一脸诚恳地说肯定会承担修车费用,我们互留了联系方式,后面再约时间一起去定责。交警特意提醒了我们,任何一方都有义务协助对方完成定责,否则可能会被追究法律责任。


搞定,各回各家。然后,就没有然后了。


消失的他


次日上午,微信问他是否需要去医院,傍晚回复我没啥事;傍晚约定责时间,久未回复,电话之,不接,凌晨回复我待定。


考虑到定责还要请假,我那破车也不值钱,就近找了个便宜的修车店,150元就搞定了。我没指望他全付,随便给多少都行,微信之,让他看着给。整整一天,没有回复,难道他没看懂「看着给」的意思?


修车后的第2天,打了两个电话给他,都被挂断,微信回复我说要等发工资。罢了,我自认倒霉,不想为这点钱再浪费时间了。原打算说他两句的,想想还是算了,不值得。


本来,这里是放了聊天记录截图的,最后整理文章内容时删除了,没必要。


很久之后,我想到一件事,他骑的是美团共享电动车,应该自带保险?很可能根本不需要他掏钱,我懒得去操心了,就酱。


果然,不要考验人性,男人都是大猪蹄子🐷。


0x4. 第一滴血


现在回想起来,那段时间一直在倒霉,交通工具陆续因为意外而罢工,只能靠「11路公交车」了。但是,很不幸,最先遭殃的,恰恰就是我的大长腿。


在团建的前两周,周一早上,送孩子上学,电梯死活不来,马上要迟到了,只能走楼梯了。因为走的很着急,没注意到一楼大堂里的平板车,发生了下面悲剧的一幕:




  • 地上有辆平板车,平板车前面有个30多岁的女人摔倒了,面部朝下

  • 平板车后面有个30多岁的男人摔倒了,面部朝上

  • 平板车附近有一些液体的药渍

  • 平板车右侧是一扇玻璃门,玻璃门外站着一个小孩,一脸茫然地望着里面的男女。



我让 AI 帮我画出上面的场景,很快啊,它画了4张图:


AI 画的图


呃,它应该是理解错了平板车,我补充说明「平板车」是用来临时拉货的小推车,它又重新画了两幅图:


AI 画的图2


得,叫狗不如自走,这画的都是啥啊,驴头不对马嘴的,只能老夫亲自出马了,上述惨案现场的真实还原图如下:


惨案现场


惨案的复现步骤如下:




  1. 我踩到了平板车尾部,摔倒,右腿撞到了车尾

  2. 平板车动起来了,撞到走在前面的老婆,摔倒

  3. 老婆手里端着一杯液体的药,摔倒时洒落一地



我骂骂咧咧地站了起来,忍着右腿的疼痛,一脚踹向了平板车。只见平板车不急不慢地驶向了玻璃门外,不偏不倚地撞到我儿子腿上了,尼玛。。。


送完孩子后,返回家中,看了下伤口,右侧大腿,有10cm的划痕,在出血,估计是碰到了平板车的拐角了。在微信上跟物业说了此事,回复了个捂脸的表情;问我是啥车,回复道挺大的一辆铁车,继续捂脸。他这是觉得我是傻逼?我本来是想调监控看下当时发生了啥,为啥会没看见那么大的车,算了,自讨没趣,喷了点酒精和紫汞就上班去了。


当晚,划痕的「线」就演化成了「面」,大腿内侧接近1/5的皮肤都是瘀伤的红色,挺吓人的,就不放照片了。应该没啥大事,肯定是没伤到骨头,就没去医院了。


几天后,物业主动联系我,说他前几天在休息,今天刚回来,现在找到了当初放平板车的人,让我把伤势说的重一点,可以多索要一些赔偿。我回复道,不用了,我不交物业费就是了,你也别再找我了,然后我就挂断了电话。


后来我才知道,是我老婆跟物业总公司交涉后,物业才找我的。特么的,我缺那点赔偿?物业这工作态度,感觉是我为他服务似的。原来,我都是提前预存一年的物业费,看来我脑子早就坏了。


好在,老婆孩子的伤势较轻,没多久就好了。我到现在也没完全恢复,腿上的大片红色已经褪去,平时完全没感知,但是按上去还有点疼,只能等着时间来磨平伤痛了。


0x5. 总结 & 体会


半个月前,我又去了趟巢湖,喝喜酒。高速入口,取卡,发现左侧胳膊不能完全抬起来,稍微用点力就巨疼无比。其实,自从团建骑车摔倒后,左侧肩膀一直在疼,因为不影响日常活动,我就没在意。


原来那次事故,除了「失忆」外,还摔出了隐藏 bug,只是需要特定的姿势才能复现:



坐着,手臂外展,向上抬起



拍了个核磁共振,问题不大,肩袖损伤,保守治疗,开药,外敷内用。一周后,依然疼痛,复查,医生说等它自己慢慢恢复吧,也不需要再来复查了。「伤筋动骨一百天」,古人总结的经验数据,多少还是有点道理的。


前几天,骑车去修后视镜和转向灯,因为路太烂,手机从支架上掉了下来,可能是情景再现,依稀想起了一点事。当初貌似是为了躲避路上突然出现的大坑或什么东西,双手捏死了刹车,因为车速较快,又没有 abs,然后就翻车了。但是记忆非常模糊,我也不知道是真实发生还是自己的心理暗示。


简单算了下,因为骑摩托车参加团建,我多花了2000余元,也不知道踏板能否卖这么多钱:




  1. 外套:300元

  2. 货拉拉:150元

  3. 摩托车后视镜 + 手机支架 + 转向灯:200元

  4. 汽车保险杠:150元

  5. 头部 CT:260元

  6. 核磁共振 + 医药费:1000元



整个团建,我只参加了吃饭,然后看人泡澡,听人描述按摩。我这算是响应刺激消费的号召,创造了2000元的 GDP 吗?


你很机车耶


在我「失忆」后不久,我的台式机也「失忆」了,这个机器是为组内提供 Crash 堆栈在线解析服务的,服务部署在 WSL 里。某天,WSL 的文件系统突然变成只读的了,不能创建和修改任何文件,也即无法产生新的记忆了。


我怀疑是所谓的安全软件搞的鬼,联系 IT,回复说要等更高权限的人来处理。我等不及了,尝试了微软官网提供的修复方法Read-only fallback error


so easy,只需3个命令,一顿操作猛如虎后,WSL 彻底凉凉了,完全启动不了了,之前好歹还能进入系统。。。


其实,Crash 除了表示「崩溃」,还有「车祸」之意,也就是说:



Crash 后的我,让解析 Crash 堆栈的机器 Crash 了



宕机后的几天,我可能中招了甲流或支原体,39℃,周末连续烧了3天,忽冷忽热的,又体验了一把「冰火两重天」。


周一,还在发着烧,但感觉好了不少,就去公司了。发烧时精神状态不好,代码肯定是写不了了,但来都来了,索性乘着一股热劲,花了一天时间,把之前的坑又踩了一遍,重新安装了 WSL 并搭建了环境,有种失而复得的感觉。这次,我把坑记录下来了,防止哪天又 Crash 了。


看了下安全软件的日志,某天强制重启了电脑,估计是突然断电导致没有正确关闭 WSL,把 WSL 的文件系统搞坏了,变成只读了。我猜测,车祸后的失忆也是类似的,因为摔倒后大脑突然断电,导致Cache在大脑RAM里的记忆没能及时FlushDisk上,那段记忆就这么永远丢失了。


不同的是,电脑挂了可以重装,人要是挂了,再也没有机会重来了,直接少走几十年弯路。


这次「失忆」的经历,既是不幸,也是万幸。万幸在没有酿成大祸前,让我深刻地理解了随处可见的标语:



道路千万条,安全第一条



最后,也请读者朋友们时刻牢记「安全第一」,包括但不限于走路、骑车、开车,祝大家在人生的旅途中一路平安。


道路千万条,安全第一条


作者:野生的码农
来源:juejin.cn/post/7310423310167916594
收起阅读 »

一个排查了一天的BUG,你在摸鱼🐟吧!

web
站会 在一次日常站会上,组员们轮流分享昨天的工作进展。一个组员提到:“昨天我整天都在排查一个BUG,今天还得继续。” 出于好奇,我问:“是什么BUG让你排查了这么久还没解决呢?” 他解释说:“是关于一个数据选择弹窗的问题。这个弹窗用表格展示数据,并且表格具有选...
继续阅读 »

站会


在一次日常站会上,组员们轮流分享昨天的工作进展。一个组员提到:“昨天我整天都在排查一个BUG,今天还得继续。”


出于好奇,我问:“是什么BUG让你排查了这么久还没解决呢?”


他解释说:“是关于一个数据选择弹窗的问题。这个弹窗用表格展示数据,并且表格具有选择功能。问题在于,编辑这个弹窗时,表格中原本应该显示为已选状态的数据并没有正确显示已选状态。”


我猜测道:“是不是因为表格中数据的主键ID是大数值导致的?”


他回答说:“大数值?我不太确定。”


我有些质疑地问:“那你昨天都是怎么排查的?需要花一整天的时间,难道是在摸鱼吗?”


“没有摸鱼,只是这个BUG真得有点难搞,那个什么是大数值?”


“行吧,姑且信你,我待会给你看看。”


排查


表格使用的是 Ant Design 4.0 提供的 Table 组件。我检查了组件的 rowKey 属性配置,如下所示:


<Table rowKey={record => record.obj_id}></Table>

这表明表格行的 key 是通过数据中的 obj_id 字段来指定的。随后,我进一步查看了服务端返回的数据。


image.png

可以看到一条数据中的 obj_id 字段值为 "898186844400803840",这是一个18位的数值。



在ES6(ECMAScript 2015)之前,JavaScript没有专门的整数类型,所有的数字都被表示为双精度64位浮点数(遵循IEEE 754标准)。这意味着在这种情况下,JavaScript能够安全地表示的整数范围是从253+1-2^{53} + 125312^{53} - 1(即-9,007,199,254,740,991到9,007,199,254,740,991)。可以简单地认为超过16位的数值就是大数值。



JavaScript中很多操作处理大数值时会导致大数值失去精度。比如 Number("898186844400803840")


image.png


可以看到 "898186844400803840""898186844400803800" 的区别在第16位后,从 40 变成 00 这就是大数值失去精度的表现。


在看一下表格的数据展示,如下图所示:


image.png


可以确定的是,从服务端返回的数据到在表格中的渲染过程是没有问题的。那么,可能出现问题的地方还有两个:一是在选择数据后,数据被传递到父组件的过程中;二是父组件将已选数据发送回选择数据组件的过程中。


定位


我检查了他将数据传递给父组件的逻辑代码,发现了一个可疑点。


image.png

在上述代码中,JSON.parse 被用来转换数据中的每个值。在这个转换过程中,如果 item[key] 是以字符串形式出现的数值,并且这个字符串能够被 JSON.parse() 解析为 JSON 中的数值类型,那么 JSON.parse() 将会把它转换为 JavaScript 的 Number 类型。


这种转换过程中可能会出现精度丢失的问题。因为一旦字符串表示的数值的位数超过16位后,在转换为 Number 类型时就无法保证其精度完整无损。


解决


我们通过正则表达式排除了这种情况,如下所示:


newItem[key] = typeof item[key] === 'string' && /^\d{16,}$/.test(item[key]) ? 
item[key] :
JSON.parse(item[key]);

经过修改并重新验证,问题得到了解决,数据选择弹窗现在可以正确展示已选择状态。


image.png


反思


这个表面上不起眼的BUG为何花费了如此长的时间来排查?除了对大数值的概念不甚了解外,还有一个关键原因是对JavaScript中可能导致大数值失去精度的操作缺乏深入理解。


大数值通常由两种表示方式,一个是用数值类型表示,一个是字符串类型表示。


如果用数值类型表示一个大数值,而且你不能直接修改源代码或源数据,这种情况比较棘手,因为一旦 JavaScript 解析器处理这个数值,它可能已经失去了精度。


这种情况通常发生在你从某个源(比如一个API或者外部数据文件)接收到一个数值类型的大数值,如果数据源头不能修改,只能使用第三方库lossless-json、json-bigint来解决。


如果用字符串类型表示一个大数值,在JS中只要有把其转成Number类型的值就会失去精度,不管是显式转换还是隐式转换。


显式转换,比如 Number()parseInt()parseFloat()Math.floorMath.ceilMath.round等等。


隐式转换,比如除了加法外的算术运算符、JSON.parseswitch 语句、sort的回调函数等等。


作者:前端大骆
来源:juejin.cn/post/7348712837849284644
收起阅读 »

被裁员半年了,谈谈感想

后端开发,22年9月,跳槽到某新能源生态企业,23年3月中旬的某个周一下午,被HR通知到会议室做个沟通,两周前收到转正答辩PPT模板让我填写,原本以为是做转正答辩的相关沟通,结果是沟通解除劳动合同,赔偿N+1,第二天就是lastday。 进入公司后经历了几次组...
继续阅读 »

后端开发,22年9月,跳槽到某新能源生态企业,23年3月中旬的某个周一下午,被HR通知到会议室做个沟通,两周前收到转正答辩PPT模板让我填写,原本以为是做转正答辩的相关沟通,结果是沟通解除劳动合同,赔偿N+1,第二天就是lastday。

进入公司后经历了几次组织架构调整,也不断变化着业务形态,但本着拥抱变化的心态,想着会越来越好,所以对于这个突发状况毫无准备。


心路历程


首月


刚刚经历裁员,下个月会有工资、奖金和赔偿金入账,赔偿金不扣税,同时对于市场环境没有了解,比较乐观。首月的想法就是写简历,并开始投递,先投不想去的公司找面试经验;找学习资料、刷题;期望薪资是不需要涨薪,大概平薪就行。

首月面了三家公司,发现了自己的诸多漏洞,项目比较垂类,讲解过程混乱;基础知识复习不足,很多新出来的延展概念了解不够。


第二个月


上个月期盼的奖金到账了,有些庆幸,又有些失落。庆幸的是收到一笔不菲的补偿金,失落的是下月开始就没有收入了。

发现面试机会变少了,整月才面了三四家,这个月发现的问题,更多的是从架构角度来的,诸如幂等、一致性hash等场景,个人了解的相对简单了。


第三个月


广深的工作机会实在是少,开始同时投递其他城市的岗位试水。月初一家公司现场面了4轮都很顺利,第二天最后一轮CTO面,被嘲讽之前业务简单,比较受打击。月底面其他城市的岗位,一面过后第二天晚上10点又被拉上线做一面的补充面。

开始焦虑了,一想到还没找到工作,补偿金估计一两个月也会花完,可能要动用积蓄了,心跳就加速,越想越加速。努力想让自己变得不去想,只去想没有掌握的知识点,算是熬过了这个月。


第四个月


这个月,感觉蛮顺利,月初面一家大厂,技术面、主管面、HR面、提交资料都很顺利,感觉稳了,每天都看看公众号的面试状态,希望能快点沟通offer;月中也走完了一家中厂的4轮面试流程;月底又走完了另一家新能源车企的面试流程。

整个月过完,自己感觉飘了,感觉同时手握3个offer机会,晚几天随便一家给offer call就去了。个人心态一下子就变了,月内简历几乎没怎么投了,看知识点好像也没那么认真了。


第五、第六个月


好吧,上个月的3个机会,全都没有等来,继续面试。心态有点躺平,焦虑感少了,颓废感来了,BOSS直聘岗位几乎能投的都投过了,没有面试的日子,会过得略显浑浑噩噩,不知道要做什么。
陆续来了几个offer,也终于决定下来了,降薪差不多40%,但好在稳定性应该有保障。


心态的转变



  • 从渴望周末,到期盼工作日


    工作时渴望周末的休息 ,没找到工作时,每一个周末的到来,都意味着本周没有结果,而过完周末,意味着过完了1/4月。感觉日子过得好快,以前按天过,现在按周过,半年时间感觉也只是弹指一挥间。

    每一个周一的到来,意味着拥抱新的机会。每周的面试频率比较高时,会感到更充实;面试频率低下来时,焦虑感会时不时的涌上心头,具体表现是狂刷招聘软件,尝试多投递几个职位。


  • 肯定 -> 否定 -> 肯定


    找工作初期,信心满满。定制计划,每天刷多少题,每天看什么知识点,应该按照什么节奏投递简历,自己全都规划好了

    中期,备受打击,总有答不上来的问题,有些之前看过的知识点,临场也会突然忘记,感觉太糟糕了。

    后期,受的打击多了,自己不会的越来越少,信心又回来了



可能能解决你的问题


要不要和家里人说


自己这半年下来,没有和家里人说,每周还是固定时间给家里打电话,为了模拟之前在路边遛弯打电话,每次电话都会坐在阳台。

个人情况是家在北方,本人在南方,和爸妈说了只能徒增他们的焦虑,所以我先瞒着了。


被裁员,是你的问题吗?


在找工作的初期,总会这样问自己,是不是自己选错了行业,是不是自己不该跳槽,会陷入一种自责的懊恼情绪。请记住,你没有任何问题,你被裁员是公司的损失,你不需要为此担责,你需要做的是让自己更强,不管是心理、身体还是技术。


用什么招聘软件


我用了BOSS直聘和猎聘两个,建议准备好了的话,可以再多搞几个平台海投。另外需要注意几点:



  1. 招聘者很久没上线,对应岗位应该是不招的

  2. 猎聘官方会不定期打电话推荐岗位,个人感觉像是完成打电话KPI,打完电话或加完微信后就没有后续跟进消息了

  3. 你看岗位信息,招聘者能看到你的查看记录,如果对某个岗位感兴趣,怕忘记JD要求,可以截图保存,避免暴露特别感兴趣的想法被压价


在哪复习


除非你已经有在家里持续专注学习的习惯,否则不管你有没有自己的书房,建议还是去找一个自习室图书馆,在安静的氛围中,你会更加高效、更加专注。

如果只能在家里复习,那么远离你的手机,把手机放到其他房间,并确保有电话你能听到,玩手机会耗费你的专注力和执行力。

(你在深圳的话,可以试试 南山书房 ,在公众号可以预约免费自习室,一次两小时)


如何度过很丧的阶段


多多少少都会有非常沮丧的阶段,可能是心仪的offer最终没有拿到手,可能是某些知识点掌握不牢的自我批判。

沮丧需要一个发泄的出口,可以保持运动习惯,比如日常爬楼梯、跑步等,一场大汗淋漓后,又是一个打满鸡血积极向上的你。

不要总在家待着,要想办法出门,多建立与社会的联系,社会在一直进步,你也不能落下。


一些建议


1. 项目经历


讲清楚几点:



  • 项目背景


    让人明白项目解决了什么问题,大概是怎么流转的,如果做的比较垂类,还需要用通俗易懂的话表达项目中的各个模块。


  • 你在其中参与的角色


    除了开发之外,是否还承担了运维、项目管理等职责,分别做了什么


  • 取得的成果


    你的高光时刻,比如解决了线上内存泄漏问题、消息堆积问题、提升了多少QPS等,通常这些亮点会被拿出来单独问,所以成果相关的延展问题也需要提前想好



还比较重要的是,通过项目介绍,引导面试官的问题走向,面试只通过几十分钟的时间来对你做出评价,其实不够客观,你需要做的是在这几十分钟的时间内尽可能的放大你的优势



除此之外,还需要做项目的延展思考



比如我自己,刚工作时做客户端开发,负责客户端埋点模块的重构,面试时被问到,“如果让你设计一个埋点服务端系统,你会考虑哪些方面”? 对于这类问题,个人感觉需要在场景设计类题目下功夫,需要了解诸如秒杀抢购等场景的架构实现方案,以及方案解决的痛点问题,这类问题往往需要先提炼痛点问题,再针对痛点问题做优化。


2. 知识点建议


推荐两个知识点网站,基本能涵盖80%的面试知识点,通读后基本能实现知识点体系化

常用八股 -- JavaGuide

操作系统、网络、MYSQL、Redis -- 小林coding


知识成体系,做思维导图进行知识记忆

那么多知识点,你是不可能全都记全的,部分知识点即使滚瓜烂熟了,半个月后基本也就忘光了。让自己的知识点成框架、成体系,比如Redis的哨兵模式是怎么做的,就需要了解到因为要确保更高的可用性,引入了主备模式,而主备模式不能自动进行故障切换,所以引入了哨兵模式做故障切换。

不要主观认为某个知识点不会被问到

不要跳过任何一个知识点,不能一味的把认为不重要的知识点往后放,因为放着放着可能就不会去看了。建议对于此类知识点,先做一个略读,做到心中大概有数,细节不必了解很清楚,之后有空再对细节查漏补缺。

之前看到SPI章节,本能认为不太重要,于是直接略过,面试中果然被问到(打破双亲委派模型的方式之一),回过头再去看,感觉其实不难,别畏惧任何一个知识点。

理论结合实践

不能只背理论,需要结合实践,能实践的实践,不能实践的最好也看看别人的实现过程。

比如线程顺序打印,看知识点你能知道可以使用join、wait/notify、condition、单线程池等方式完成,但如果面试突然让你写,对于api不熟可能还是写不出。

又比如一些大型系统的搭建,假如是K8S,你自己的机器或者云服务器没有足够的资源支撑一整套系统的搭建,那么建议找一篇别人操作的博客细品。

不要强关联知识点

被面试官问到一些具体问题,不要强行回答知识点,可能考察的是一个线上维护经验,此时答知识点可能给面试官带来一个理论帝,实操经验弱的感觉。

举两个例子,被问过线上环境出问题了,第一时间要如何处理?,本能的想到去看告警、基于链路排查工具排查是哪个环节出了问题,但实际面试官想得到的答案是版本回滚,第一时间排查出问题前做了什么更新动作,并做相应动作的回滚;又被问过你调用第三方服务失败了,如何本地排查问题?,面试官想考察的是telnet命令,因为之前出现过网络环境切换使用不同hosts配置,自己回答的是查看DNS等问题,这个问题问的并不漂亮,但是也反映出强关联知识点的问题。

建立自己的博客,并长期更新

养成写博客的习惯,记录自己日常遇到的问题,日常的感受,对于某些知识点的深度解析等。面试的几十分钟,你的耐心,你解决问题的能力,没办法完全展示,这时候甩出一个持续更新的博客,一定是很好的加分项。同时当你回顾时,也是你留下的积累和痕迹。



半年很长,但放在一生来看却又很短

不管环境怎样,希望你始终向前,披荆斩棘

如果你也正在经历这个阶段,希望你早日上岸



作者:雪梨酒色托帕石
来源:juejin.cn/post/7274229908314308666
收起阅读 »

泰国游记:声色张扬的奇妙之旅

囧途开始 四月初的时候,阿宇问我:“老三,五一去泰国不?” 其实那一阵状态“水深火热”,没什么太多玩的心思,但是一想到小卡拉米一把年纪了,还没出过国,这次不去,下次又不知道什么时候了。 “去他妈的,整!” 订好机票,简单做个攻略。四月三十号,上海下着雨,浦东机...
继续阅读 »

囧途开始


四月初的时候,阿宇问我:“老三,五一去泰国不?”


其实那一阵状态“水深火热”,没什么太多玩的心思,但是一想到小卡拉米一把年纪了,还没出过国,这次不去,下次又不知道什么时候了。


“去他妈的,整!”


订好机票,简单做个攻略。四月三十号,上海下着雨,浦东机场,还有几分凉意,但是此时的我,已经开始畅想那头的曼谷有多么火热。


万万没想到,飞机延误!延了三次!原本五点开的飞机,到了差不多八点,还没动静,而且最后一次航司没有给出任何官方通知和说明。我心想:“今天不会走不了,要打道回府吧?”


飞机延误


正忐忑间,通知飞机起飞!哦吼,晚了三个多小时,但是终于飞了!望着上海的的灯光渐渐模糊,这下终于要曼谷见了。


上海出发


当天的气候不太友好,航程中,经历了三次颠簸。到了凌晨,终于抵达了曼谷,按照原定计划,找到接机司机,直奔芭提雅。


飞机到达曼谷


在去的路上,我们讨论了一下要不要订酒店,合计了一下,到了那先整点夜市小烧烤,再马杀鸡一下解解乏,酒店这事,不着急。


经历了两个多小时的路程,终于到达了芭提雅,这时候发现,我们去的那片地方,怎么都是灯火灰暗,说好的夜市小烧烤呢?哥俩累了,要不先把酒店顶了,携程再一看,明明曼谷看还有很多房间,这回附近的酒店都满了。要不先找个马杀鸡店?哥俩茫然地转在街上,芭提雅的热浪、下水道的恶臭,冲面而来,转了一圈,发现马杀鸡店也都打烊了,哥俩大眼瞪小眼,一时相对无言。


携程是拉了,要不试试美团吧?抱着试一试的态度,用美团搜了一下,哎,发现几百米远的一个酒店有房间,订!顺着高德,一路导航了过去。进了一个疑似前台的大厅,结果一看,没有人,什么情况?


疑似的酒店大堂


赶紧打电话问问,电话打通,用我多年来一直稳定磕碜的英语,开始磕磕巴巴地咨询:


“We we… book en… your room on meituan er website,we en are in the the the…hotel, but there is no body.”


对面也回复了一堆话,英语也不是很好,当然主要是我听力也差,只听懂了一点点:


“Do you in the hotel?”


"Ah yes."


……


又鸡同鸭讲了一阵,阿宇又接过去掰扯了一会,还是没掰扯明白。


我接过来,只能说,一时无言,“We need help.”,她也想帮我,但她回复的我都听不懂,唉,挂了吧,哥俩这个酒店看样子是住不上了。


我看了眼大堂的沙发,心想,要不哥俩就在这凑合一宿得了。结果,接电话的那个前台大姐找过来了!


Madam,你就是我们的光!


原来酒店的前台就在对面,我还埋怨高德又缺德了,导错了地方,结果领完房卡,发现住房还真差不多真就在高德导航到的位置——我也知道为什么前台大姐会知道要出来找我们,看起来不是第一次了。


此时,已经是凌晨三点了,洗漱洗漱,躺在床上,凌晨三点半。


再规划规划第二天的行程,订订酒店,凌晨四点,奔波了大半天的三某,终于睡了。


住的房间


第二天不太早,老三在一阵饿意中醒来,得出去找点吃的,不知道吃什么,找个711吧,去的途中,发现有一条小路,看着是通向海边,先去海边看看吧,结果路边的小棚子里,跳出了三只狗子,一阵狂吠,奔着我而来,没感受到芭提雅人的热情,先感受到了芭提雅狗的”热情“,我随手抄起一根棍子,今天非得让你们尝点中国的颜色。


——狗主人出来了,我扔掉棍子,算了,今天先放你们一马,以后好好做狗。


小路


随便吃点东西,回去准备换酒店,我突然反应过来,我们看了半天地图,要住海边,要离今天去的真理寺近,选来选去,阿宇重新订的,不就是现在这家吗?我俩真是热昏了头,这还不如直接续呢,瞅瞅这破酒店,哪有海景?


换房间吧,先住着,放下东西,到了前台,正在办手续,我往外面瞟一眼,大海!这个酒店还真的是海景酒店!酒店的楼下,泳池,海滩,在往侧前方一看,真理寺也赫然在目——芭提雅的热辣假期要开始了!


酒店泳池


沙滩和真理寺


芭提雅


海滩午餐


一天没怎么正经吃饭的老三和阿宇,决定中午得找个餐厅好好吃一顿,刷了会大众点评,本来想去附近的一个海鲜市场,但是阿宇不能吃海鲜,找了另外一家评分不错的餐厅Surf & Turf Beach Club & Restaurant,这家餐厅在海边,吹着海风,看着大海和沙滩,用餐心情非常美妙。


海边餐厅


服务员阿姨不太会英文,对着菜单,像两个看图学字的小学生一样,我们点了饮料和餐,整体上还是没让人失望的。


菠萝炒饭色泽金黄,味道清甜,混杂着坚果、果干、肉松,口感非常丰富,我们一致认为,这是目前为止,吃过的最好吃的菠萝饭。


点的餐


冬阴功汤,一口下去——嗯,泰国味儿!酸辣鲜三种口味汇聚,咖喱和辣椒的辣,柠檬的酸,在口腔里交相奏响,互相融合。


点之前,服务员阿姨特意提示了我“辣”,我还忐忑了一下,结果发现这种辣和我想象的辣不一样。怎么形容呢,如果说国内的菜,比如川菜的辣是一种干燥的辣,这里的辣是一种湿润的辣。


路边马杀鸡


来了泰国,怎么能不体验一下“马杀鸡”呢?吃过午饭,天气正热,下午的行程还早,穿过烫脚的街头,找到了路边的一家按摩店。


价格看起来很合算,按!我选了肩背按摩。


路边按摩店


先洗个脚——咱这也算两只脚踏进洗脚界了。换好衣服,趴在塌板上,来自中国的面团,要被泰国的师傅揉搓了。


洗脚


先刷油,精油滴在背上,有一点辛辣的感觉,让我想起了以前尝试用过的泰王油。上好油,开始反复揉搓,师傅的手、肘、膝都是这次“白案面点制作”的工具。


和中式按摩相比,泰式按摩要轻柔很多,这种轻柔是恰到好处的轻柔,既能感受到力量的渗透,又不会感觉到很痛。最后,师傅拉扯一下手指,就像是把面团给拉长,结束了这次下厨。打开店门,走进芭提雅滚烫的天气离,中国的面团”下锅“了。


真理寺


下午去了一片海滩之隔的真理寺,直线距离很近,但是真要去的话,得绕很长一段路。


真理寺外景


进入真理寺之前,工作人员给我们发了顶安全帽,去过很多景点,真理寺是第一个需要戴安全帽参观的。为什么呢?因为真理寺还在施工。


真理寺始建于1981年,历经多年建设,至今仍然没有完工。它是纯木制结构,所有的构造都采用卡榫结构,雕塑都是纯手工打造,虽然它被称之为寺,但其实不是一座寺庙,而是一件艺术品。


真理寺正在施工中的雕刻师傅


真理寺的修建耗时弥久,似乎不知道它到底会什么时候结束,但是和真正的真理相比,又不一样,毕竟真理寺总有一天修完,但是真理是无穷无尽的。


真理寺的小奶猫


环绕真理寺的时候,在一个台阶上看到两只稚嫩的小奶猫正在打闹,真理是有生命力的,这两只小猫比我们离”真理“更近。


真理寺和花与海


真理寺在一座宁静的海滩,从一汪孔洞看过去,一枝白花,开在绿的大海旁,远处的天是蓝的,光洒进来,滴在老的木头上。


真理寺内的雕塑风格各异,能看出来一些佛教和印度教的风格,我们参观的时候,刚好有一个旅游团,蹭到了一点导游的解说,导游说到:”……观世音菩萨……孔子“,这两个熟悉的名字引起了我的注意,仔细一看,还真有这样的雕塑,真理寺真是一个多国家、多宗教的文化汇聚地。


真理寺内景


在真理寺还看到了大象,不过这大象驮着人,看起来很温顺的样子,希望以后有机会能看看野生的大象吧。


真理寺驼人的大象


芭提雅落日


在真理寺结束地比较早,天气又比较热,回到了我们的海景酒店休息,下了水,在蓝色的泳池里休息扑腾了一会。


天色渐晚,太阳没那么晒人,躺在沙滩的躺椅上,喝口冰凉的啤酒,吃点水果,吹着海风,看着太阳慢慢变成橘色。


躺在海边


没忍住下海泡了一会,在温热的海水里,看着太阳慢慢落到海平面之下,这一刻,感到格外松弛。


日落


日落真理寺


蒂芬妮人妖秀


日落不是芭提雅一天生活的结束,夜晚才刚刚开始。


来了芭提雅,怎么能不看人妖秀,芭提雅知名度最高的是蒂芬妮人妖秀。蒂芬妮的场馆,白色的礼堂风格,喷泉闪着不同的颜色。


芬妮人妖秀场馆


蒂芬妮秀禁止摄影,所以只拍了开幕和谢幕。抛开猎奇的成分,单纯从表演的角度来看,我觉得蒂芬妮秀的艺术水平很高。


蒂芬妮秀开场


演员都称得上是端庄典雅,尤其是每场得领舞,我不知道这是不是就是所谓得”人妖皇后“,身材笔挺、面容姣好,超模的风范、专业的歌舞水平。


演出的节目,风格形式也非常多样,歌舞剧、独舞、现代舞、民族舞……全场大概有三十个节目,每个节目都有不一样的特点。很多节目看得出来,是迎合了游客的审美,包含了不同国家的歌舞形式,泰国、美国、中国、印度、越南、朝鲜……


我印象里特别深刻的节目有两个。


一个是“菊花台”,当《菊花台》的音乐响起,演员表演起了中国风的舞蹈,整个剧院充满了中国观众的欢呼,剧院是懂怎么撩拨观众的。


另外一个,也是全场印象最深刻的一个,是“对唱”,“男演员”戴礼帽,穿礼服,声音清澈,灯光熄灭,“女演员”迅速登场,声音妩媚。


我最开始以为这是两个演员,切换的时候,一个演员钻到幕布后,另外一个演员钻出来。瞪大眼睛,我想看看到底怎么换的,不对——这好像是一个演员!


灯光打开,谜底揭晓,果然是一个演员,左边身子穿着男礼服,右边身子穿着女礼服,左面示人,男声开唱,右边示人,女声开唱。


演员快速左右转身,男女声切换自如,全场充满了欢呼。


这位表演者唱什么我没听懂,但听出来了情绪非常激亢,甚至挺出来了愤怒。“人妖”这个群体,对自己的认同到底是男还是女呢?是向左,还认为自己是男,还是向右,认为自己是女?或者正面对人,雌雄莫辨?


来泰国的路上,我看了一些书,我以前以为人妖在泰国有悠久的历史,后来发现,”人妖“是现代的产物。


芭提雅曾经只是个小渔村,为什么后来逐渐能逐渐成为度假胜地呢?因为美国来过,确切说美国海军陆战队来过。二战后,泰国倒向美国,成为美国反共的桥头堡,芭提雅就曾经是美国海军陆战队的驻地,它也因此而兴。


人妖同样也因此而来,驻扎的美国大兵给芭提雅的市场带来了消费,当然也包括成人市场,因为汇率物价等等方面的差异,美国大兵指甲缝里漏出的一点油,都可能会改变一个泰国的贫穷家庭。有些家庭,家里只有儿子,穷得实在受不了了怎么办呢?如果儿子长的还算清秀,那就牙一咬,送去做变性手术,去挣美国大兵的钱,没想到有些美国大兵还挺喜欢,因为不用担心怀孕。


富可能使人变坏,穷可能使人变态。人妖的来源,算是穷的没办法的办法。


蒂芬妮秀落幕


蒂芬妮秀落幕后,演员会站在小广场上,招徕游客合影,一次100泰铢。歌舞秀中的艺术感,终究还是变成生活的真实感,这不是艺术,这是生意。


等待合影的演员


这是一条不能回头的路,演员得在年轻的时候尽可能挣到养老的钱——如果有老可养的话,因为注射激素过多,通常很难长寿。


到了三十岁以后,雄性激素的分泌难以抑制,TA们的男性特征会越来越明显,难以遮掩,TA们也会被老板无情地抛弃。我在蒂芬妮的前台,看到售货员,化妆、穿裙子、踩高跟,但是男性特征已经无法遮掩,检票员也是如此,剧场的一个内场管理员,留了美式的油头,乍一看是一个“小哥”,仔细一看也发现步态和声音不太对劲。


整个蒂芬妮秀才几个工作人员,其他人都去哪了呢?而且,蒂芬妮的演员,绝对都是百里挑一的,那剩下的九十九呢?曲终人落幕,台上永远会有人,谁会知道离开台子的人呢。


风俗步行街


芭提雅的夜生活,还有一个地方,是一定要去看一看的,那就是风月步行街。坐着双条车,穿行在中滩天海滩旁的长街,风月步行街到了。


坐双条车


刚到街口,五色的灯光,喧闹的人声,嘈杂的音乐铺面而来。


风月步行街的街口


街边的夜店灯红酒绿,街边的女郎穿着清凉性感,招徕客人的皮条客穿梭其间。


风月步行街


皮条客和街边女郎


路上的行人来来往往,有的走着走着,脚就不听使唤地就拐进了街边的一家店,成为喝着、唱着、跳着的人群中的一员。


夜店里的人群


——欲望在这条街上流淌。


真是万恶的资本主义啊!唉,怎么这就到头了,阿宇,你拉我回去干什么?


格兰岛


如果想在芭提雅体验一下热带小岛风情,那么格兰岛是个不错的选择。


从芭提雅的码头登船,大概过了半个小时,在海船的摇晃中,一座小岛渐渐映入眼帘,青的山下,红瓦的房子,和蓝的海界限分明。


抵达格兰岛


在燥热的风里,登上Tawaen观景点,浅蓝色的天空下,白色的沙滩渐渐变得湿润,绿色的海,渐渐变成深深的墨绿。


Tawaen观景点看海


下到Tawaen海滩,满是戏水的游客,男男女女,各个国家。


Tawaen海滩的游客


Tawaen的水上项目很多,我选择了4个项目,摩托艇+香蕉船+浮潜+滑翔伞。


摩托艇,之前在秦皇岛试过,这里再试一次,感觉是海水更清,浪更大,不到一会,全身上下都湿透了。


看到了香蕉船,那必须尝试一下,为什么呢?因为想起了NBA的”香蕉船兄弟“,对了,最后带完香蕉船的小哥,一定会推荐玩一下”翻船“,拉着香蕉船的快艇,最后会控制让香蕉船翻掉,所有人都落到水里。


NBA香蕉船兄弟


可惜坐船的时候只有我一个人,如果“支付四狗”组合能齐聚,那大可Cosplay一下香蕉船兄弟,对了,我们也要翻船的。


香蕉船和湿透的我


试了下浮潜,体验一般。作为一个旱鸭子,只能在水里乱扑腾,一不小心,就是原地打陀螺,头埋进水里,满眼都是绿色,海水往耳朵里,顺着缝隙往口鼻里流。当然,如果会游泳,应该体验会很不错,同船会游泳的一些伙计,开心地都流连忘返。在会游泳之前,我应该是不会再玩这个项目了。


浮潜


最期待的的是滑翔伞,远远地看着,人像风筝一样在天上飘着。


远看滑翔伞


上了快艇,两位师傅都很皮,一个师傅话多、活泼,我们一上船,“中国人?”肯定之后,这位师傅放起了音乐《我们不一样》,放着放着他也跟着在船头唱了起来“我们不一样,每个人有不同的境遇…”唱着唱着还跳了起来。


唱跳的皮师傅


另一位师傅看起来稳重一点,安静地掌着舵。但是快艇开起来之后,就不是那么回事了,这位师傅喜欢开“快船”,快艇在海上速度拉满,还玩起了漂移。


很快,开始滑翔,第一位勇士上了天,小船上充满了欢呼声。第二位,第三位,第四…快乐戛然而止,船抛锚了。


两个师傅尝试了半天,都没能打响快艇,抛锚的小船,就像一块木板,在海浪中飘摇晃荡,我终于体会到什么叫“海上孤舟”,我们只是在海滩上,想想如果远洋抛锚,那该多么绝望。最后另外一艘快艇过来救了我们,拖着抛锚的快艇回了码头。


船拖船


又想起了皮师傅之前唱的歌:“我们不一样,我们的船会Done”……下次有机会再玩吧。


伴随着半斜的太阳,从码头乘船离开了格兰岛。看着身后的格兰岛越来越远,又回到了暮色中的芭提雅。


离开格兰岛


暮色中的芭提雅


芭提雅码头远眺


曼谷


大皇宫


来到曼谷的第一站,直奔大皇宫,大皇宫在泰国的地位,就相当于中国的故宫,去了曼谷不去大皇宫,就像是去了北京不去故宫。


大皇宫外景


但是去的时候不赶巧,大皇宫似乎在接待什么团体,暂时不开放。第二天再去大皇宫,终于开放了,而且免费。


大皇宫的殿宇


一座大皇宫,一部泰国近代史。


泰国的历史不算悠久,它有四个王朝,素可泰王朝、阿瑜陀耶王朝、吞武里王朝、曼谷王朝,大皇宫就是曼谷王朝的皇宫,也称作拉玛王朝,在现在的泰国仍然延续,目前在位的是拉玛十世。


现任泰王-拉玛十世


拉玛王朝创立于公元1782年,此时的中国处于清朝乾隆年间,拉玛一世初建大皇宫,后来历代泰王逐渐完善。


大皇宫舍利塔


大皇宫的建筑风格很混搭,既有传统的东南亚风格,白墙红瓦,也有宗教元素,金灿灿的舍利塔,华丽的佛堂。西洋风格同样也非常浓厚,很多建筑都被珐琅点缀,有些建筑完全是欧式风格。


这是因为大皇宫后来的完善,同样伴随着拉玛王朝的动荡与变革,资本主义殖民浪潮兴起,泰国虽然没有沦为殖民地,但是却沦为了半殖民地。和宗主国清国被人用坚船利炮打开国门不一样,从拉玛四世开始,泰国主动打开了国门。


大皇宫一角


拉玛四世主动派遣留学生到欧洲学习军事政治法律,并对国内的政治经济进行了改革。拉玛四世的继任者,也就是著名的朱拉隆功大帝,是个更激进的改革者,少年即位的朱拉隆功,组织了一些王室和贵族青年,自称为“少年暹罗”,在“进步”和“改革”的主题下争辩和前进。


“少年暹罗”的意思,是把他们的对手暗喻为“老年暹罗”, 立志要把他们扫进故纸堆。朱拉隆功的改革是成功的,所以他成为泰国五大帝之一。


这跟大皇宫的建筑风格有什么关系呢?《泰国史》里写了这么一段:



在19世纪70年代,朱拉隆功建造了一座新的宫殿,采用意大利风格的设计,但是在顶部采用了暹罗式的屋顶,以取悦传统主义者。1907年,这种妥协就被抛弃了,国王修建了带有强烈古典主义风格的阿南达沙玛空御座厅(Ananta Samakhom throne hall),采用了卡雷拉大理石、米兰的花岗岩、德国的铜和维也纳的陶瓷。整座建筑成为通往旧城区北部的新的王室宫邸区的入口,那是一片欧式风格的宫殿群,以及为其他王室家族成员建造的宅邸。国王和贵族们进口了许多欧洲的小摆设来装点这些新房子。



泰国宫廷和欧洲联系越来越紧密,体现在建筑风格,就是有很多欧化的地方。对了,从《泰国通史》看到比较有意思的一个点,拉玛王朝的前四位国王都有中文名:郑华、郑佛、郑福、郑明,拉玛五世也就是朱拉隆功开始,没有中文名,朱拉隆功即位于1868年,此时的中国属于清朝的同治年间。


大皇宫一角


大皇宫也有血色,年轻的拉玛八世在大皇宫饮弹身亡,泰王是自杀还是他杀?行刺者是谁?已经成了历史谜案。拉玛八世的弟弟普密蓬·阿杜德即位,命运的改变总是这么突然,年轻的普密蓬应该是个热爱自由的人,在瑞士留学的时候,他因为飙车失去了一只眼睛。


街头供奉的普密蓬


如果没有兄长的遇刺,他也许会成为一个风流贵族,也许泰国会是另外一个样子。命运把他推到他该在的位置,他做的非常好,正如他的名字泰文的意思,“无与伦比的能力”,可以说是名副其实。


他影响了几乎整个泰国的现代史,军政府、民选政府……在每次政治危机的时候,普密蓬都会在恰当的时候出手,来保证政局的基本稳定。他亲近民众,是泰国民众非常热爱的”父亲“。


也正是由于拉玛八世的遇刺,王室认为大皇宫不详,搬到了其它地方,所以我们今天才有机会参观大皇宫。


郑王庙


郑王庙和大皇宫大概一水之隔,它纪念的是一个传奇国王——郑信大帝。


湄南河岸的郑王庙


郑信大帝的一生,是一个传奇,也如同一部跌宕起伏的戏剧。


他的前半生比起爽文不遑多让,郑信的父亲郑镛是潮州人,因为谋生,孤身来到泰国。郑镛从贩水果的小贩做起,逐渐发达,承包赌税,也籍着这个关系,郑信被过继给当时的财政大臣昭披耶却克里。从贫民到大亨,两代完成阶层晋升,郑镛已经足够励志,但是他的儿子更加传奇。


郑王庙塔


青年郑信,成为北方的一个城主。泰缅战争爆发,阿瑜陀耶王朝灭亡,属于郑信的舞台却搭建好了。郑信率兵赴京勤王,参与了出城出击,但是遭遇了失败,在撤退到城下的时候,发现已经被阻拦在外,进退无据之中,郑信毅然带兵突围,没想到阿瑜陀耶城亡,郑信生。


太阳照耀下的郑王庙


王朝乱世之中,郑信逐渐崛起,他先是在泰国南部站稳脚跟,又团结各方力量,挥师北上,收复阿瑜陀耶城。


1768年1月4日,郑信加冕为吞武里王。


郑信和吞武里王朝的落幕也很突然,阿瑜陀耶城发生骚动,郑信派遣大臣披耶讪镇压,披耶讪却被叛军说服,加入了叛军。此时吞武里空虚,叛军长驱直入,王宫卫队不支,郑信不得已退位,出家为僧。


郑王庙塔侧面


此时听闻国内政变的远征军主帅,郑信的女婿通銮,回国平乱,平乱之后的第二天,郑信身亡,死于紫檀木棍的击打之下——这是帝王的死法。


通銮,也就是拉玛一世,他让我想起了另外一个历史人物——司马懿,郑信出家为僧按照惯例可以免死,司马懿同样指洛水为誓,一定会放过曹爽——政治也许有时候不能讲道德吧。


对了,这位拉玛一世的中文名叫郑华,在对宗主国清国的国书中,他自称郑信的儿子。


郑信大帝的一生,在绝境中奋起,在最辉煌时戛然而止,让人不得不感概历史和命运的无常。


拳赛


来到泰国,我最大的愿望就是看一场泰拳赛,泰国有两大知名泰拳场,伦披尼和迦南隆,可以说是泰拳圣地,几乎所有泰拳手都梦想披上这两个拳场的金腰带。


伦披尼经历了一次搬迁,目前在曼谷郊区,迦南隆,在曼谷市区,在某些app上,被翻译成那差达慕,我一开始还吃惊,点评拳赛,竟然没有迦南隆,原来是这个翻译闹了乌龙。


这次我选择是更远的伦披尼,是因为周五,伦披尼和One冠军赛合作,有ONE周五格斗夜比赛。ONE冠军赛是是目前世界排名第二、东南亚排名第一的综合格斗赛事,当然,地处东南亚,ONE冠军赛自然也免不了泰拳比赛,甚至目前ONE的踢拳赛事同样开始迅猛发力。


能在泰拳两大圣地之一的伦披尼,看一场顶级的现代格斗赛事,实在是一件无比快乐的事情。


ONE格斗夜


我已经有六七年没到现场看比赛了,经历了两个小时的拥堵路程,我们终于赶到了伦披尼。


领票,进场,坐到座位上,音乐、灯光,现场的欢呼声,我感觉开始兴奋起来了———It's Time!


拳赛准备开场


开局的第一场比赛,日本选手VS白俄罗斯选手,日本选手有很浓重的空手道风格的意味,站立更强一些,几次打晃白俄罗斯选手,但是白俄罗斯选手的摔柔更胜一筹,在第三回后半段,裸绞终结了日本选手。第一场比赛就出现了终结,太兴奋了!


日本选手被终结


还有一场日本vs泰国的比赛,日本选手身高臂展更有优势,一度压制了泰国选手,肘法给泰国选手的脸开了一个大口子,泰国选手血流满面,苦苦支撑,我以为这场比赛就这么结束了,没想到泰国选手抓住一个漏洞,前手摆拳命中,日本选手重重倒地不起,赛事方赶紧用担架把日本选手抬走。我觉得这场是整个比赛最残酷的一场,赢的人血流满面,输的人担架抬走。


输赢皆惨烈


联合主赛同样精彩,一个年轻的黎巴嫩选手,挑战老牌的泰拳王,这个泰拳王应该是个明星选手,出场的时候全场欢呼,还现场表演了一段拜师舞,没想到场上局势风云突变,年轻选手,一开始就凶猛发力,老拳王显然有些慢热,没有进入节奏,第一回合就被击倒了两次,虽然最后依靠老道的经验,撑到了最后,但还是一致判定负。


现场还有很多精彩的比赛,现场看比赛和隔着屏幕看比赛,完全是不同的感觉,现场的欢呼声,选手的打击声,再好的摄影设备都制造不出来这样的临场感,去了现场,才能真正感受到现场的热烈!看比赛的时候,我完全兴奋起来了,呐喊,嘶吼,赛事结束后嗓子都是哑的。


只能说:过瘾,下次还看!


ONE巅峰赛


ONE格斗夜的赛事结束之后,我突然发现周六的早上还有一场比赛,ONE巅峰赛,这场比赛有一场金腰带的卫冕战,而且最重要的是,这一场有三位中国选手:胡勇、魏锐、张立鹏,两场MMA赛事,一场踢拳赛事。


尤其是魏锐VS秋元皓贵的这场比赛,让我想起了之前我非常喜欢的选手邱建良,带着前世界第一的势头,打入了ONE冠军赛,结果第一场被秋元皓贵阻击,至今还没有再次复出比赛。这场,毫无疑问,是一场恩怨局,这样的机会可遇不可求,我一定要来现场,一定要亲眼看到魏锐击败秋元皓贵。


晚上回到酒店已经差不多两点,我完全兴奋地睡不着,一早五点,我又起床到了伦披尼现场,这次现场摆放了一条金腰带,金腰带在前,谁能拒绝合影呢?


和金腰带的合影


开局两场速杀


首局终结获胜,在拳赛里是很少见的,终结一般发生在选手体能下降和伤害累积之后,但是今天的比赛,开场的两局都是首回合终结速胜。


今天比赛的第一场是泰拳比赛,在第一回合的后段,先是一个摆拳,打晃,接着一个击腹的直拳,选手痛苦倒地,这一拳应该是岔气了。


第二场比赛是无道服的巴西柔术比赛,日本选手上场之后,很快就被巴西选手压制,拿背裸绞一气呵成,几秒,裁判试了一下,日本选手胳膊已经软了,赶紧终止比赛。巴西选手在赛后抑制不住地哭泣,巴西柔术比赛,需要一点欣赏的门槛,所以纯巴西柔术选手很难接到商业比赛,很难赚到钱,平时生活很艰难,所以也能理解为什么巴西选手忍不住哭泣。


为了三位中国选手的比赛而来,加上状态不佳,中间的几场比赛,都没有上一场那么高的兴致。


胡勇被克制


终于来了,第一位出场的中国选手是胡勇,伴随着中文的出场音乐,全场响起了巨大的欢呼声,能在异国他乡,为本国的选手加油助威,这是一件多么令人激动的事!


现场的声音非常整齐,一位大哥自发担当了”领喊“的角色,他喊”胡勇“,现场的中国观众跟着一起喊”加油“,”胡勇……加油“的声音响彻整个场馆。


胡勇的脚下移动比较灵活,能看出,他以前应该是有散打的背景,他的站立比对手出色,开局几次重击对手。


对手随后调整了策略,坚决要和胡勇打地面,对手的摔柔水平在胡勇之上,每一次防守,胡勇都得拼尽全力,几次被拖入地面,被拿到了很深的把位,现场的中国观众心都揪了起来。


胡勇


最终三局战罢,胡勇点数落败。胡勇还很年轻,还有时间能继续完善和提高自己,加油!


魏锐稳健取胜


这是我最期待的比赛,魏锐是我特别佩服的一个选手,已过而立之年,家庭圆满,带的徒弟都成了冠军,他的职业生涯,武林风金腰带、勇士的荣耀金腰带、还有最具含金量的K1金腰带,功成名就,他已经不需要证明什么了。但是在职业生涯暮年,他还是选择了走出国门,挑战自己,继续冲击ONE的金腰带,这就是真正的武者意气!


魏锐出场


这场也是一场复仇局,对手秋元皓贵,曾经击败过邱建良,这次魏锐前来,也有为好兄弟复仇的意味。魏锐是是非常有名字的选手,他出场的时候,全场的欢呼像海啸一样。


魏锐迎击拳命中


伴随着同样整齐的”魏锐……加油“的声音,比赛开始。魏锐赛前说,针对秋元的技术特点,他已经找到了应对的办法,果然所言非虚。魏锐灵活的脚下移动,让秋元皓贵根本打不出他习惯的快速组合,到了第二回合,秋元皓贵甚至顶着伤害,硬往前压,给魏锐造成了一些麻烦,但是魏锐经验丰富、拳商很高,进行了调整,加强移动和迎击,整场比赛,基本上没见到秋元像往常一样的高速组合。


第三回合开始


就这样,三局战罢,在异国他乡的赛场上,魏锐一致判定击败了秋元皓贵,向着金腰带又迈进了一步。


获胜的魏锐身披国旗


裁判举起魏锐的手的时候,我满脑子和嘶吼的只有四个字:”魏锐牛逼!“


张立鹏憾负


张立鹏是老牌的MMA选手,曾经也打进过UFC,这次他遇到了不小的麻烦,他的对手赛前超重,协议之下,张立鹏选择了接下比赛。


果然,临场,对手的维度比张立鹏要大一号,在地面的缠斗中,张立鹏在力量上劣势太大,根本压不住对手,而且体能消耗地过快,到了第三回合,完全是强撑着打完,中间有一些好机会,因为体能不足都没有抓住。


张立鹏拿到把位但压制不住


最终,对手一致判定获胜。


张立鹏因为体能消耗过度,被轮椅推离现场,希望他能尽快恢复,早日拿下下一场胜利。


这场有个彩蛋,目前在UFC排名最高的中国男子选手宋亚东也来到了现场,作为张立鹏的边角,希望中国MMA的未来越来越好!


宋亚东来到现场


射击


来泰国,有第二件必做的事情,就是体验射击,因为没有提前预订,去不了最推荐的海军射击俱乐部,我选择了陆军俱乐部。


带着兴奋过后的眩晕,我来到了陆军射击俱乐部,下午两点开门,经历了一阵等待之后,领到了我的子弹,黄橙橙,亮晶晶。


领到了子弹


等待的时间,看着里面正在射击的游客,耳边传来了鞭炮一样的声音,震得耳朵嗡嗡的。


下午的射击场


终于轮到我了,我选择的套餐是三款手枪,教练帮忙上子弹上膛,我接枪瞄准,想起之前看的一点教程,身子往前压——后来看视频,嗯,怎么是勾着头的,好丑,以后有机会动作练漂亮一点。


设计中


开枪,手枪的后坐力不大,但是射击的时候,枪会往上跳,一开始,空靶了若干次,后来强打精神,努力瞄准,终于上靶了,甚至还蒙中了一发靶心。


调整射击姿势


这次射击体验呢,整体感觉是隔靴搔痒,瞄准、扣动扳机,多少感觉有点麻木,当然也可能和我此时头脑已经昏沉有关。


我买了五十发子弹,本来以为不少,打完却觉得意犹未尽。最后离开的时候,旁边的步枪靶场,一声巨响,我的耳朵瞬间轰鸣,回头一看,原来是有个大哥在打霰弹枪。


步枪靶场


没有哪个男人不爱枪,希望以后有机会能更深度地体验甚至学习射击。


感受


人生意义就是体验,我们回忆的时候,永远只会回忆自己体验过的东西,看一万个短视频,看一百本书,不如一次滚烫的沙滩来的记忆深刻。


泰国这个国家给我的感受,就是混杂,各种东西混杂,有寺庙的肃穆,有海岛的宁静、有拳赛的刺激,有红灯区的放荡…就像冬阴功汤,一下子把各种味道混杂在了一起。


在芭提雅和曼谷的街头,也能感受到泰国的贫富差距,大街时不时能看到几辆超跑,但更多的是坐在路边的乞丐,那天阿宇查了一下泰国王室的财富,他惊讶了一下,我也是——殿堂之下有杂草。


泰国很自由,但我觉得太过自由不是一件好事,大麻店、红灯区…这类声色刺激的东西,其实就像是表面能看到的纱布,底下不知道隐藏着多少溃烂的伤口,感谢我们的国家,社会主义铁拳,压得这些东西抬不了头。


最后,世界这么大,还是要去看看!




参考



  1. 《泰国常识》

  2. 《泰国通史》

  3. 《爱上泰国:你的色彩惊艳了我的时光》

  4. 《泰国史》

  5. 《泰国攻略》


作者:三分恶
来源:juejin.cn/post/7364785775345762356
收起阅读 »

怎么用一句话证明你在游戏公司里的最底层?

引言 今天在知乎看到一个有趣的帖子:如何一句话证明你在公司最底层?我们把范围缩小到游戏公司。 关于这个问题,身边80%的朋友描述了自己在公司底层的难忘回忆,还有几位朋友甚至因为这不堪的回忆破防了。 刚进入游戏公司的新人,迷茫是常态。和大家一样,笔者也曾是公司的...
继续阅读 »

如何一句话证明你在公司最底层?


引言


今天在知乎看到一个有趣的帖子:如何一句话证明你在公司最底层?我们把范围缩小到游戏公司。


关于这个问题,身边80%的朋友描述了自己在公司底层的难忘回忆,还有几位朋友甚至因为这不堪的回忆破防了。


刚进入游戏公司的新人,迷茫是常态。和大家一样,笔者也曾是公司的最底层,总觉得每天一睁眼就是各种困难的事等着我:



担心工作内容不会做,担心与同事沟通不好,担心自己考核不过关......



今天的这篇文章,大家一起来看看一位位于游戏公司底层的游戏开发者的最底层体验。


最底层体验


图片源于网络


1.介绍一下你自己


大家好,我是XXX,来自XXX。虽然我是一个新人,但我对游戏充满了热情,这种热情已经伴随我多年。小时候,我就沉迷于各种游戏,从那时起,我就梦想着有一天能够为创造令人陶醉的游戏世界做出贡献。我加入这个行业的目标是成为一个出色的游戏开发者,并参与创造令人惊叹的游戏体验。我相信,通过与这个行业的优秀人才一起工作,我可以不断成长,并为我们的团队和项目做出贡献。谢谢大家,请多多指教。


此处应有一阵热烈的掌声,那是对一位懵懂的游戏行业新人的勇敢表示敬畏。他或许不知道他的棱角将在这里被磨平。


熟悉又让人崩溃的弹窗


2.熟悉项目,体验游戏。


游戏行业新人刚进到游戏公司,可能第一件事就是登陆公司内部使用的通讯工具。你的直属上司可能早早的在网线那头等候着你的上线。


你好,XXX。你先接收一下这份文档,仔细阅读一下里面的内容。检出一下公司的游戏项目,然后根据文档把游戏跑起来。体验一下游戏,熟悉一下游戏的每个系统。有问题可以请教你旁边的那位大神,他负责带你。


好的,谢谢。由于在来公司之前做足了准备,检出项目、运行项目这种小问题肯定难不倒你。这时候你会惊讶,原来这就是大型的商业化游戏项目,看起来有那么点高大上,但是最多的是还是看不懂。不过这游戏玩着好无聊,不是我喜欢的类型。想到未来的日子里,需要不停地重复地在这个游戏里面遨游,"真的会谢"。



3.分配任务



  • 修改禅道bug序号XXX的问题。

  • 修改活动XXX文本显示异常问题。

  • 修改XXX报错问题,完成禅道单子序号1、2、3、4、5......。


游戏行业新人的入门任务往往就是这些看起来微不足道,但是却非常细节的问题。正所谓不积跬步无以至千里,通过慢慢处理这些小小的bug和显示异常的问题,无疑是熟悉项目的最好方式。虽然这些都是比较基本的内容,修改bug、调整UI、修复报错。但是能够体现一个新人的基本功:阅读问题描述、理解问题描述、定位问题所在系统、定位系统所在代码、读懂代码原有逻辑、修改错误代码、验证问题是否修复、思考会不会对其他内容造成影响。


这对于管理者来说是非常合理的,但对于新人来说,未免太过于简单了。


支线任务


4.支线任务


游戏行业新人入门有可能并不能第一时间接触到游戏项目主分支的代码,往往是参与其他的一些分支版本,例如审核服(专门为了应对平台审核员的审核搭建的游戏服)、版署服(用于申请版号专门搭建的游戏服)、海外服(主要负责多语言版本的语言提取、翻译替换、本地化处理)等等。


安排新人去处理这些支线任务,为的就是让新人从另外一个相对安全的分支去熟悉游戏项目,避免因新人的处理不当造成线上版本出问题,从而造成公司的经济损失。支线任务通常就是枯燥单一的体力劳动,不需要过多的技巧,只需要耐得住寂寞的心。


图片源于网络


5.几点下班


一位有着远大抱负的新人,往往在刚进入公司的日子里,不知道几点下班。领导分配给我的任务,实在太简单了,三两下就完成了,还不到规定的时间。为了能够更加快速地熟悉项目,参与游戏功能的开发,继续研究代码。


HR说19点下班,但是18点的时候大家都跑去吃饭,不解,跟着。等到19点的时候,果然没有人下班。继续奋笔疾书。20点的时候终于有人下班了,可是领导还是没动静,算了,再看看代码吧。21点,领导好像发现了这个新人,让他早点回去休息。(没有人告诉他,这将是常态。) "没事,我再看会代码,马上就回去了。"


手机先吃


6.福利


同事: “公司发月饼了,你没去领吗?”,“不知道啊,没人通知。我刚来几天。”


同事: ”我看大家都去领了,现在。“,”我不知道自己是否算正式员工“


同事: ”你先去看看吧,反正大家都在领。“,兴致冲冲地跑到发月饼的地方。


发月饼的: “叫什么名字?”,”XXX“


发月饼的: ”名单上没这个人,不能领!“


刚加入公司的时候,可能由于没转正或者名字还没有进入公司的名册,往往会导致有些福利不能享受。例如公司发月饼的时候,人人有份,唯独你。又或者公司发奖金,你拿200慰问金。公司发年终奖,你还是拿慰问金。 但是如果你想请假,领导秒批。甚至说你想离职,领导也是轻描淡写,“好的”。没有丝毫的牵挂留恋。这是前所未有的福利。


结语


不管怎样,虽然你是公司的最底层,但你是公司中最坚实的基石,因为你在每一颗砖石上都留下了你的汗水和努力,为了让整座大厦能够稳固地矗立在成功的巅峰。加油,请认真工作,积极向上。




作者:亿元程序员
来源:juejin.cn/post/7281589318329925689
收起阅读 »

从事程序媛工作的我都经历了什么?

第一段工作经历     大学一直就是学的Java,学校在湖南后面安排去浙江嘉兴实习做毕业设计项目的时候发现更加喜欢前端, 就想往这个方向去发展,2020年6月毕业就开始了我的前端求职之路,好在那个时候互联网对这块需求还是挺大的,以应届生的身份成功入职了一家小型...
继续阅读 »

第一段工作经历


    大学一直就是学的Java,学校在湖南后面安排去浙江嘉兴实习做毕业设计项目的时候发现更加喜欢前端,
就想往这个方向去发展,2020年6月毕业就开始了我的前端求职之路,好在那个时候互联网对这块需求还是挺大的,以应届生的身份成功入职了一家小型公司,面试也比较简单,更多的还是了解性格和学习方式以及自己对未来发展想法,也很顺利就入职了。


离职原因


    在那家公司大概做了半年,到了发展的瓶颈期,就两个前端,来来往往离职了三批人,就我还在原地,很多时候感觉学不到什么实际性的东西就果断离职了......


   我发现入职的第一家公司对自己的职业规划还是挺重要的,有人带你给予你学习的方向这点很重要,但可惜我没有这样的运气,基本都是靠自己在工作中摸索和自学试错成本也高,学校学的大部分东西在工作中基本都派不上用场,这也让我很苦恼,不过感觉大部分的人基本都是这样吧......


第二段工作经历


    第二家公司是一个大型的厂做晶导体和手机电脑电子产品,幸运的是,遇上了急需人手的时候,面试基本都能对答如流,他们对我也挺满意的,就破格让大专学历的我入职了,收到offer的时候还是很开心的,毕竟这样的机会少之又少......


    实际开发的时候整个IT开发团队都是分不同的组,我所在的组做的项目更多的是偏向公司内部的考勤、人事、薪资这类型的后台管理系统。项目用的vue2是基于vue-element-template后台模板进行二次开发,之前的老大会进行项目搭建给我们顺流程和定开发规则,让我根据规则来进行模块开发,这在某种程度上做了统一性也省了很多的麻烦更方便后期维护。


    熟悉了之后有时候也自己尝试从0开始搭建项目自己也学到了很多。其他的项目组更多的是跟流水线上的产品打交道,基本每天都在加班改需求事情太多流水线也不稳定,有bug的时候都要在公司守株待兔去解决这种突发情况,但是我几乎都是早上8.30上班到下午5.30就下班,加班的情况很少。


    上班有时候活多的时候就忙的键盘真的冒火,不忙的时候时间都属于自己,通过刷刷前端的视频,看看知乎和掘金之类的补充能量来度过这普通的一天......


嘉兴生活


    工作坐标嘉兴,是个很悠闲慢节奏适合养老的城市,不内卷,生活也没什么压力,消费也不高,过的很轻松,平时周末约上小姐妹出去玩玩逛逛街啥的,后面搬家换了个离公司近的地方感觉晚上阴森森的有点害怕,就养了一只蓝猫,现在已经快4岁了,性格超级好,每天等着我回家跟家人一样很温暖......搬家后通勤时间基本骑个共享电动车10分钟左右就能到,也没有啥特别大的变化,除了不包吃住,其他方面真的挺好的,同事也很好相处,后面还内推了一个姐妹来这边上班,刚好也是大学同学,我们就开始了合租的生活,她也有一只猫猫,就这样过上了两人两猫的生活,这样的生活持续了一年半左右...


离开浙江去深圳


    后面找男朋友了,算是大学同学,实习的时候分到一个班级做毕业设计项目,变成了同桌。那个时候我们还不熟,很少讲话。我比较高冷,我俩对话仅限于问问作业以及项目上的一些问题,直到毕业设计答辩完我们两个加起来的对话也不超过20句,加了个QQ也是为了方便传递老师布置的作业,偷懒直接抄他的作业罢了......


    2020年疫情居家期间,无聊就开始玩王者,突然看到他在线,就随便点了一下邀请,结果他同意了,这在我意料之外,然后就开始带游戏,慢慢熟悉起来了,这样的生活持续了差不多一年左右,算是暧昧期吧。2021的某一天,突然在QQ上跟我表白,我没准备好拒绝了!后面我们状态依旧持续这样,大概过了一两个月之后,我感觉我们之间状态还是没变化,后面就同意了在一起了。就这样开始了异地恋,我们除了五一和国庆放假会去到双方城市见面到处去玩,其他时候都在自己的城市做着忙碌着自己的工作,大部分也只能通过手机微信聊天去了解和关心对方。


    他跟我一样都是做前端,一开始他在中山工作,工作一段时间他感觉工资低就去深圳发展,在那边上班时间越来越久,通勤时间是一个小时左右,我们只有下班才有机会聊聊天也让我们格外珍惜,我每天都等他下班洗漱完就一块休息,久而久之习惯了,基本每天晚上都会开着语音一整晚不挂电话。后面下班越来越晚,异地了一年多,很多矛盾开始出现。就不太想继续异地恋,他就想让我去深圳发展,他觉得我们老家都在湖南,离深圳比较近,我想让他来浙江,他说离家太远了,家里有爷爷奶奶回去一趟不方便,后面拗不过,争执了半年,最后我妥协去了深圳......


深圳工作后续


    2022年7月份来的深圳,提前把猫从浙江托运到深圳,花了我800大洋。行李太多,打包了一堆包裹寄过去让他给我拿。跨越1600公里坐了8个小时的高铁,不远千里只为你而来!我当时想:“如果有个人能为我这样,我辜负全世界也绝对不能辜负他”。


    我男朋友提前一个半小时坐公交来高铁站接我,傻傻的在那等了我一个多小时。到站下车就快要见面的那段时间我心里好紧张,有点不知所措,他在出口等我,看到对方后,一时间双方都有点尴尬,不知道该说些什么......


    每次很久没见之后都会这样,但我还挺喜欢这种感觉,我称之为属于两人的“新鲜感”。他先开的口:"坐车辛苦了,累不累,饿不饿?”,我回了个还好,接过我手里的密码箱,知道我中午没吃饭,提前从车站下面的商圈打包了一份鱼粉。给我找了个能坐着的地方,递给我让我先吃,不知道怎么描述当时的场景又好笑又温馨,异地了一年多,终于奔现的感觉(~ ̄(OO) ̄)ブ。让我开心又有点陌生,之前都是短暂的相聚,所以这次跟以往的感觉都不太一样,打怪终于打到大BOSS了......


   他一直看着我吃,我有些不好意思,坐了一天车顾不上形象有些许狼狈。我叫他背对着我别老看,他就时不时偷瞄,一碗粉都能吃半个小时挺不可思议的~ 我偶尔也关注看着他,只是不敢直视他只是偷瞄,可能是害羞吧(✿◡‿◡)


   他穿着一个白衬衫牛仔裤白白净净的,那天的印象一直留存在我的脑海中,都说情人眼里出西施,不外乎这种吧,人活几个瞬间,喜欢也是,就是在某个时间里面在某种环境的衬托下刚好对上了眼刚好他的行为让你有喜欢和心动的感觉....


   吃了准备回去了,走着走着突然停下:“把手给我”,我挺惊讶的也挺开心的,感觉他的手大能直接把我两个手都包住,天气太热了,牵手热的出汗了也没放开过......后面不太熟悉高铁站,他带着我转了大半个小时,后面终于找到出口了,打了个出租车花了70多,真的颠覆了我的想象力,深圳消费确实是挺高的! 一路上看着深圳的风景对这个地方充满了好奇......


          持续更新中......


作者:小婉婉
来源:juejin.cn/post/7363209007829041167
收起阅读 »

Node拒绝当咸鱼,Node 22大进步

web
这几年,deno和bun风头正盛,大有你方唱罢我登场的态势,deno和bun的每一次更新版本,Node都会被拿来比较,比较结果总是Node落后了。 这种比较是不是非常熟悉,就像卖手机的跟iPhone比,卖汽车的跟特斯拉比,比较的时候有时候还得来个「比一分钱硬币...
继续阅读 »

这几年,deno和bun风头正盛,大有你方唱罢我登场的态势,deno和bun的每一次更新版本,Node都会被拿来比较,比较结果总是Node落后了。


这种比较是不是非常熟悉,就像卖手机的跟iPhone比,卖汽车的跟特斯拉比,比较的时候有时候还得来个「比一分钱硬币还薄」的套路。


1.png


Node虽然没有落后了,但是确实有点压力了,所以20和22版本都大跨步前进,拒绝当咸鱼了。


因为Node官网对22版本特性的介绍太过简单,所以我决定来一篇详细介绍新特性的文章,让学习Node的朋友们知道,Node现在在第几层。


首先我把新特性分为两类,分别是:开发者可能直接用到的特性、开发者相对无感知的底层更新。本文重点介绍前者,简单介绍后者。先来一个概览:


开发者可能直接用到的特性:



  1. 支持通过 require() 引入ESM

  2. 运行 package.json 中的脚本

  3. 监视模式(--watch)稳定化

  4. 内置 WebSocket 客户端

  5. 增加流的默认高水位线

  6. 文件模式匹配功能


开发者相对无感知的底层更新:



  1. V8 引擎升级至 12.4 版本

  2. Maglev 编译器默认启用

  3. 改进 AbortSignal 的创建性能


接下来开始介绍。


支持通过 require() 导入 ESM


以前,我们认为 CommonJS 与 ESM 是分离的。


例如,在 CommonJS里,我们用并使用 module.exports 导出模块,用 require() 导入模块:


// CommonJS

// math.js
function add(a, b) {
return a + b;
}
module.exports.add = add;

// useMath.js
const math = require('./math');
console.log(math.add(2, 3));

在 ECMAScript Modules (ESM) **** 里,我们使用 export 导出模块,用 import 导入模块:


// ESM

// math.mjs
export function add(a, b) {
return a + b;
}

// useMath.js
import { add } from './math.mjs';
console.log(add(2, 3));

Node 22 支持新的方式——用 require() 导入 ESM:


// Node 22

// math.mjs
export function add(a, b) {
return a + b;
}

// useMath.js
const { add } = require('./mathModule.mjs');
console.log(add(2, 3));

这么设计的原因是为了给大型项目和遗留系统提供一个平滑过渡的方案,因为这类项目难以快速全部迁移到 ESM,通过允许 require() 导入 ESM,开发者就可以逐个模块迁移,而不是一次性对整个项目进行修改。


目前这种写法还是实验性功能,所以使用是有“门槛”的:



  • 启动命令需要添加 -experimental-require-module 参数,如:node --experimental-require-module app.js

  • 模块标记:确保 ESM 模块通过 package.json 中的 "type": "module" 或文件扩展名是 .mjs

  • 完全同步:只有完全同步的ESM才能被 require() 导入,任何含有顶级 await 的ESM都不能使用这种方式加载。


运行package.json中的脚本


假设我们的 package.json 里有一个脚本:


"scripts": {
"test": "jest"
}

在此之前,我们必须依赖 npm 或者 yanr 这样的包管理器来执行命令,比如:npm run test


Node 22 添加了一个新命令行标志 --run,允许直接从命令行执行 package.json 中定义的脚本,可以直接使用 node --run test 这样的命令来运行脚本。


刚开始我还疑惑这是不是脱裤子放屁的行为,因为有 node 的地方一般都有 npm,我要这 node —run 有何用?


后来思考了一下,主要原因应该还是统一运行环境和提升性能。不同的包管理器在处理脚本时可能会有微小的差异,Node 提供一个标准化的方式执行脚本,有助于统一这些行为;而且直接使用 node 执行脚本要比通过 npm 执行脚本更快,因为绕过了 npm 这个中间层。


监视模式(--watch)稳定化


在 19 版本里,Node 引入了 —watch 指令,用于监视文件系统的变动,并自动重启。22 版本开始,这个指令成为稳定功能了。


要启用监视模式,只需要在启动 Node 应用时加上 --watch ****参数。例如:


node --watch app.js

正在用 nodemon 做自动重启的朋友们可以正式转战 --watch 了~


内置 WebSocket 客户端


以前,要用 Node 开发一个 socket 服务,必须使用 ws、socket.io 这样的第三方库来实现。第三方库虽然稳如老狗帮助开发者许多年,但是终究是有点不方便。


Node 22 正式内置了 WebSocket,并且属于稳定功能,不再需要 -experimental-websocket 来启用了。


除此之外,WebScoket 的实现还遵循了浏览器中 WebSocket API 的标准,这意味着在 Node 中使用 WebSocket 的方式将与在 JavaScript 中使用 WebSocket 的方式非常相似,有助于减少学习成本并提高代码的一致性。


用法示例:


const socket = new WebSocket("ws://localhost:8080");

socket.addEventListener("open", (event) => {
socket.send("Hello Server!");
});

增加流(streams)的默认高水位线(High Water Mark)


streams 在 Node 中有举足轻重的作用,读写数据都得要 streams 来完成。而 streams 可以设置 highWaterMark 参数,用于表示缓冲区的大小。highWaterMark 越大,缓冲区越大,占用内存越多,I/O 操作就减少,highWaterMark 越小,其他信息也对应相反。


用法如下:


const fs = require('fs');

const readStream = fs.createReadStream('example-large-file.txt', {
highWaterMark: 1024 * 1024 // 设置高水位线为1MB
});

readStream.on('data', (chunk) => {
console.log(`Received chunk of size: ${chunk.length}`);
});

readStream.on('end', () => {
console.log('End of file has been reached.');
});

虽然 highWaterMark 是可配置的,但通常情况下,我们是使用默认值。在以前的版本里,highWaterMark 的默认值是 16k,Node 22 版本开始,默认值被提升到 64k 了。


文件模式匹配——glob 和 globSync


Node 22 版本在 fs 模块中新增了 globglobSync 函数,它们用于根据指定模式匹配文件路径。


文件模式匹配允许开发者定义一个匹配模式,以找出符合特定规则的文件路径集合。模式定义通常包括通配符,如 *(匹配任何字符)和 ?(匹配单个字符),以及其他特定的模式字符。


glob 函数(异步)


glob 函数是一个异步的函数,它不会阻塞 Node.js 的事件循环。这意味着它在搜索文件时不会停止其他代码的执行。glob 函数的基本用法如下:


const { glob } = require('fs');

glob('**/*.js', (err, files) => {
if (err) {
throw err;
}
console.log(files); // 输出所有匹配的.js文件路径
});

在这个示例中,glob 函数用来查找所有子目录中以 .js 结尾的文件。它接受两个参数:



  • 第一个参数是一个字符串,表示文件匹配模式。

  • 第二个参数是一个回调函数,当文件搜索完成后,这个函数会被调用。如果搜索成功,err 将为 null,而 files 将包含一个包含所有匹配文件路径的数组。


globSync 函数(同步)


globSyncglob 的同步版本,它会阻塞事件循环,直到所有匹配的文件都被找到。这使得代码更简单,但在处理大量文件或在需要高响应性的应用中可能会导致性能问题。其基本用法如下:


const { globSync } = require('fs');

const files = globSync('**/*.js');
console.log(files); // 同样输出所有匹配的.js文件路径

这个函数直接返回匹配的文件数组,适用于脚本和简单的应用,其中执行速度不是主要关注点。


使用场景


这两个函数适用于:



  • 自动化构建过程,如自动寻找和处理项目中的 JavaScript 文件。

  • 开发工具和脚本,需要对项目目录中的文件进行批量操作。

  • 任何需要从大量文件中快速筛选出符合特定模式的文件集的应用。


V8 引擎升级至 12.4 版本


从这一节开始,我们了解一下开发者相对无感知的底层更新,第一个就是 V8 引擎升级到 12.4 版本了,有了以下特性升级:



  • WebAssembly 垃圾回收:这一特性将改善 WebAssembly 在内存管理方面的能力。

  • Array.fromAsync:这个新方法允许从异步迭代器创建数组。

  • Set 方法和迭代器帮助程序:提供了更多内建的Set操作和迭代器操作的方法,增强了数据结构的操作性和灵活性。


Maglev 编译器默认启用


Maglev 是 V8 的新编译器,现在在支持的架构上默认启用。它主要针对短生命周期的命令行程序(CLI程序)性能进行优化,通过改进JIT(即时编译)的效率来提升性能。这对开发者编写的工具和脚本将带来明显的速度提升。


改进AbortSignal的创建性能


在这次更新中,Node 提高了 AbortSignal 实例的创建效率。AbortSignal 是用于中断正在进行的操作(如网络请求或任何长时间运行的异步任务)的一种机制。通过提升这一过程的效率,可以加快任何依赖这一功能的应用,如使用 fetch 进行HTTP请求或在测试运行器中处理中断的场景。


AbortSignal 的工作方式是通过 AbortController 实例来管理。AbortController 提供一个 signal 属性和一个 abort() 方法。signal 属性返回一个 AbortSignal 对象,可以传递给任何接受 AbortSignal 的API(如fetch)来监听取消事件。当调用abort()方法时,与该控制器关联的所有操作将被取消。


const controller = new AbortController();
const signal = controller.signal;

fetch(url, { signal })
.then(response => response.json())
.catch(err => {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Fetch error:', err);
}
});

// 取消请求
controller.abort();

总结


最后,我只替 Node 说一句:Node 没有这么容易被 deno 和 bun 打败~


3.jpeg


关于我


全栈工程师,Next.js 开源手艺人,AI降临派。


今年致力于 Next.js 和 Node.js 领域的开源项目开发和知识分享。




作者:程普
来源:juejin.cn/post/7366185272768036883
收起阅读 »

2万块钱买平板:苹果新一代iPad Pro直接上M4芯片,最强也最贵

万众瞩目的苹果 M4 芯片,刚刚在新一代的 iPad Pro 上亮相了。 北京时间 5 月 7 日晚 10 点,苹果举行了春季新品发布特别活动。这次活动发布了新款 iPad(Air 和 Pro)以及新一代 Apple Pencil 和妙控键盘配件等新品。 ...
继续阅读 »

万众瞩目的苹果 M4 芯片,刚刚在新一代的 iPad Pro 上亮相了。



图片


北京时间 5 月 7 日晚 10 点,苹果举行了春季新品发布特别活动。这次活动发布了新款 iPad(Air 和 Pro)以及新一代 Apple Pencil 和妙控键盘配件等新品。


当然,新一代 iPad Pro 是此次发布会上大家关注的重点,尤其是它所搭载的芯片。


苹果没有让大家失望,为 iPad Pro 装上了自家新一代的 M4 芯片,这也是该芯片的首次亮相。


「你可能会认为我们会使用极其强大的 M3 芯片,但我们跨越到下一代 ——M4 芯片。」


也就是说全新 iPad Pro 搭载的芯片直接从 M2 跳到了 M4。


下面这张图,可以说是用一图总结了 M4 芯片的强大性能。


图片


展开来说,全新 M4 芯片由 280 亿晶体管组成,基于第二代 3nm 技术打造,并在 CPU、GPU 和 NPU 方面迎来一系列提升。


全新 10 核心 CPU


在 CPU 方面,M4 拥有 10 核心 CPU,包含 4 个性能核心和 6 个能效核心。下一代核心改进了分支预测功能,为性能核心提供更广泛的解码和执行引擎,为能效核心提供更深层次执行引擎。


此外,性能核心和能效核心还具有增强的下一代机器学习(ML)加速器。


图片


与前代 iPad Pro 搭载的 M2 相比,M4 的 CPU 性能提升了 50%


图片


因此,无论是在 Logic Pro 中处理复杂的管弦乐文件,还是在 LumaFusion 中向 4K 视频添加高要求的效果,M4 都能提高整个专业工作流程的性能。


图片


10 核心 GPU


其次,GPU 部分。M4 的全新 10 核心 GPU 建立在 M3 系列芯片的新一代图形架构之上。它具有动态缓存功能,这是苹果的一项创新,可以在硬件中实时动态分配本地内存,从而显著提高 GPU 的平均利用率。


图片


此次,M4 芯片提高了专业应用程序以及游戏方面的性能。苹果表示,这是硬件加速光线追踪首次登陆 iPad,在游戏等体验中实现更真实的阴影和反射。


图片


《暗黑破坏神:不朽》游戏。


硬件加速的网格着色也内置于 GPU 中,可以提供更强大的几何处理能力和效率。相比之下,M4 芯片专业渲染性能得到了巨大提升,是 M2 芯片速度的四倍


图片


图片专业渲染软件 Octane。


在能耗方面,M4 只需一半的功耗即可提供与 M2 相同的性能。即使与轻薄笔记本电脑中最新的 PC 芯片相比,M4 只需四分之一的功耗即可提供相同的性能。


全新显示引擎


图片


M4 采用了开创性技术加持的全新显示引擎,实现了 Ultra Retina XDR 的精度、色彩准确度和亮度均匀性,这是一种结合两个 OLED 面板的光线创建的最先进的显示屏。


图片


最强大的神经引擎


M4 的神经引擎采用 16 核心设计,使得芯片更快、性能更强。


苹果表示,M4 拥有苹果有史以来最强大的神经引擎,每秒能够执行惊人的 38 万亿次操作,是 A11 Bionic 中的第一代神经引擎速度的 60 倍


图片


图片


神经引擎与 CPU 中的下一代机器学习加速器、高性能 GPU 和更高带宽的统一内存一起,使 M4 成为一款极其强大的 AI 芯片。


借助 iPadOS 中的 AI 功能(例如用于实时音频字幕的 Live Captions 以及识别视频和照片中目标的 Visual Look Up),新款 iPad Pro 允许用户在设备上快速完成令人惊叹的 AI 任务。


在苹果的展示中,配备 M4 的 iPad Pro 只需轻按一下,即可轻松将 Final Cut Pro 中 4K 视频的主题与背景分离。


图片


苹果表示,M4 中的神经引擎比当今任何 AI PC 中的神经处理单元都更强大


自 2017 年以来,苹果所有的芯片都包含了某种版本的神经引擎,尽管到目前为止,这些芯片主要用于增强和分类照片、光学字符识别、离线听写和其他事情。但苹果可能需要更快的东西来支持端侧以大型语言模型为核心的生成式人工智能,苹果预计将在下个月的 WWDC 上在 iOS 和 iPadOS 18 上推出这种人工智能。


从往年来看,M1 和 M2 之间的等待以及 M2 和 M3 之间的等待期都是一年半左右。由于苹果公布的技术细节很少,很难知道 M3 和 M4 之间更快的转变是什么原因。可能是 M3 落后于计划,而 M4 准时或提前;也有可能 M4 只是对 M3 进行了相对温和的架构更新。这需要拿到后续测试结果才能判断。


M4 版 iPad Pro:8999 元起


除了所搭载的芯片,苹果发布会上还介绍了新款 iPad 的其他细节。


新款 iPad Air 分为 11 英寸和 13 英寸两个版本,搭载 M2 芯片,支持 Wi-Fi 6E(可以选择支持 5G 的型号),最大存储空间 1TB,比搭载 M1 芯片的 iPad Air 快 50%,但显示屏仍然是 LED 显示屏。售价方面:11 英寸机型 4799 元起,13 英寸 6499 元起。


图片


新款 iPad Pro 同样分为 11 英寸和 13 英寸两个版本,采用了双层串联 OLED 屏幕,亮度更高,色彩显示更精准。其全屏亮度可以达到 1000 尼特,峰值亮度达到 1600 尼特,苹果称其为「超精视网膜 XDR 显示屏」。


售价方面,11 英寸机型 8999 元起,13 英寸 11499 元起。但如果想要更高的配置,预算会一路飙升。13 英寸 2TB 顶配达到了 19999 元。如果选择纳米纹理玻璃,售价将达到 20799 元。这可能是 iPad 史上最贵的机型。


图片


这款新品 5 月 9 日上午 9 点接受订购,5 月 15 日发售。你准备入手吗? 


参考链接


http://www.apple.com/newsroom/20…


arstechnica.com/apple/2024/…




作者:机器之心
来源:juejin.cn/post/7366149991159250954
收起阅读 »

房车用了两年多,这个油耗我是没有想到的

2021年12月买了一台基于大同V80改装的B型小房车,转眼已过两年半的时间,在这两年半的时间里,总行程达四万多公里,带我们走了许许多多的地方,看了祖国的山川湖海,秀美风光,去跑山、去露营、去看海、去旅行,留下了太多太多美好的回忆 有许多小伙伴对房车感兴趣,...
继续阅读 »

2021年12月买了一台基于大同V80改装的B型小房车,转眼已过两年半的时间,在这两年半的时间里,总行程达四万多公里,带我们走了许许多多的地方,看了祖国的山川湖海,秀美风光,去跑山、去露营、去看海、去旅行,留下了太多太多美好的回忆



有许多小伙伴对房车感兴趣,会咨询关于房车各种各样的问题,所以我准备写几篇文章来详细介绍下房车使用过程中的各种细节问题。今天这篇文章主要来分享下关于这台房车的加油和油耗相关的问题,主要包含柴油、尿素和油耗三部分,看完之后相信你对房车加油及油耗会有更加清晰的认识


柴油


我买的这台房车是烧柴油的,有小伙伴对柴油不了解,有问柴油在城市里好不好加的问题,目前我去过的所有加油站,无论是城市还是乡村,都有柴油供应,柴油相比汽油来说有很多优势,例如:


1.柴油更便宜:柴油往往比最便宜的92号汽油还要便宜一点点


2.燃烧效率高:柴油引擎通常具有更高的压缩比,这意味着在燃烧燃料时能够更高效地释放能量。这使得柴油引擎在燃料效率上表现更优秀,相对于同等排量的汽油引擎,柴油引擎的燃油消耗通常更低,也就是更省油


3.扭矩输出大:由于柴油燃料的化学性质,柴油引擎在高压下能够产生更多的扭矩。这使得柴油引擎在牵引重载或需要大扭矩输出的应用中表现优异,比如卡车、拖拉机等


4.长途行驶经济性好:上边说了柴油引擎的燃油效率高和扭矩输出大,同时因为其价格本身就比汽油低,所以柴油车辆通常在长途行驶中具有更好的经济性


5.动力输出稳定:柴油燃料的燃烧过程相对稳定,这意味着柴油引擎在低转速和高负荷下能够提供更加稳定的动力输出,这对于需要长时间持续工作的应用,如发电机和工程机械,尤其重要


鉴于以上几点,除了家用轿车外的很多商用车辆很多使用柴油,尤其是那些跑长途的大货车、客车和大巴,以及对动力要求比较高的农用和工程机械等,城市内的商用车辆也很多,所以加油根本不是问题



说回房车,加柴油的优势除了以上几点外,更为关键的是加油基本不用等,尤其是在节假日的高速服务区,之前开汽油车节假日出行,在高速服务区加油站排队加油等待半小时一小时的都是常有的事,但自从开了这个柴油车,就再也没有等过了


尿素


上边说了那么多柴油相比汽油等优势,那为什么家用轿车基本上都是烧汽油而非柴油。那这就要说说柴油车相比于汽油车的劣势了


1.低温启动问题:在寒冷的气候条件下,柴油引擎的启动可能会更困难,尤其是在没有预热系统的情况下。相比之下,汽油引擎在低温下启动更为容易,家用车要的是更易用


2.振动和噪音:柴油引擎通常比汽油引擎产生更多的振动和噪音,尤其是在低速行驶时。这可能会降低驾驶舒适度,特别是对于家庭轿车这样的日常驾驶,汽油车的舒适性更好


3.排放标准:柴油引擎在排放方面通常比汽油引擎更具挑战性


家用车更看重的是易用性和舒适性,所以这也是为什么大多数的家用车都采用汽油的主要原因。同时柴油车为了达到目前最新的国六B排放标准,除了常规的技术优化升级外,往往还要通过添加尿素溶液,减少氮氧化物的排放来达标国六B标准



那有小伙伴问添加尿素频繁吗?麻烦吗?加尿素就跟加柴油差不多,加到尿素箱里即可,尿素在每个加油站里都有卖,一桶10KG的价格大概是30元,我的车子一次加一桶多不到两桶,加一次的总花费大概也就60块,我没有具体计算过加满一箱能跑多远,预估大约有五千公里左右的样子,不是很频繁,一年如果行驶两万公里的话,也就是说需要添加3至4次尿素


油耗


最后来聊聊油耗,我的房车是基于大通V80短轴中顶底盘改装的B型房车,原车重应该在2吨左右,再加上上装家具,还有水箱电器之类的,保守估计重3吨,经过两年多的实际行驶,现在整体油耗在8.5L/百公里左右


其中最高油耗10个多一点,10个多油是在非常非常堵的情况下拿到的数据,所以我姑且认为全是最高油耗吧,最低油耗大概也在8个油左右,平时开车有关注,全程高速也有接近8个油,所以算是最低油耗吧,这个车一般都是周末节假日出去玩儿开的多,上班通勤开的少,整体路况都很好,不常遇到堵车的情况,所以综合油耗也是比较低



这个油耗我是非常满意了,毕竟这么大的车,我之前上下班通勤开的CRV,综合油耗都在百公里9升多。买之前以为这么大的车油耗怎么着都要十几个,确实没想到实际会这么低,网上发了帖子,许多同样的车主都表示与我的数据相差不大,可见这是真实表现了


最后


曾经跟多个加油站的一线加油员聊过,比起汽油车,他们更愿意给柴油车加油,主要是汽油车一般都是家用小轿车为主,而柴油车都是运输/工具车为主,家用轿车油箱一般都比较小,50升左右,而柴油车油箱普遍比较大,上百升甚至几百升很正常,一次能加更多的油。更为重要的是,汽油车大多都是自己的,部分汽油车的车主能买个车就觉得自己高人一等,到加油站之类的地方就会对服务人员颐指气使,而柴油车大多都司机或者一线体力劳动者居多,跟加油站员工属于类似工种,更能理解,相对会好说话,不计较,更平和


作者:37丫37
来源:juejin.cn/post/7367275063873470502
收起阅读 »

程序媛28岁前畅游中国是什么体验?

本人计算机硕士毕业,先后在三家厂工作,工作节奏虽说不是 007 吧,但偶尔 996 是有的,勤勤恳恳搬砖是常态,也偶尔累了就划划水摸鱼。在这行业不焦虑是假的,35 岁危机时刻提醒着每一位年轻的程序员,这行主打一个精神内耗。 前几年互联网飞速发展高薪招人时,大家...
继续阅读 »

本人计算机硕士毕业,先后在三家厂工作,工作节奏虽说不是 007 吧,但偶尔 996 是有的,勤勤恳恳搬砖是常态,也偶尔累了就划划水摸鱼。在这行业不焦虑是假的,35 岁危机时刻提醒着每一位年轻的程序员,这行主打一个精神内耗


前几年互联网飞速发展高薪招人时,大家都有肉吃,现在遇到互联网寒冬了,有汤喝就不错了,尤其对晚入行的95 后社畜,现在回过头看,已经是互联网红利退潮的末期了。对于 80 后早一批入行的程序员, 肯定钱也挣够了,房子也早就翻几倍了,早就有抗御风险的能力了,即使裁员了也能拿着分手费找个差不多的厂子继续苟着。但是对于 95 后来说,惨不忍睹,行业内卷及其严重,刚有点工作经验就遭遇大规模裁员,重点买房都是踩在最高点接盘,现在房价跌了,车子打价格战,直接把前几年辛辛苦苦挣的首付跌没了,这几年白干了,说起来,心就抽搐的疼。不像人家00 后,直接看开了,不破三个 dai,房贷,车贷和传宗接代,直接卷老家公务员躺平,享受人生,逃离大城市的拥挤,拒绝被房子的套牢。


金融危机,经济下行,行业越来越卷,精神内耗极其严重,身体健康堪忧。我突然顿悟了,我决定,为自己而活。想看世界的心也越来越强烈,最后我坐不住了,做了个大胆的决定,畅游中国。刚好疫情快结束时,航空公司推出了自己的产品,畅游中国随心飞,我立刻入手了,入手价是三千多点,全国飞不限次数。我一边安排好自己的时间订机票,一边计划旅行路线,一个女生独自环游中国之旅开始了。没有队友,不给生活中任何糟心事打断我的计划,一人吃饱,全家不饿。下面给大家说说我去了哪些地方。


贵州-贵阳


我看好时间后立刻定机票,从上海飞到了贵阳,准备打卡黄果树瀑布。我定的酒店就在黄果树景点附近不远,一大清早7点我就起床了,呼吸着让人神清气爽的空气,吃了一些自己带的进口苹果作为早餐,特别甘甜,饱腹感足足的。8点进山了,那一刻,我别提多开心了。


回想起当社畜时,每次都是8.30起床,9.30左右到公司,每天上班心情比上坟都沉重,永远干不完的KPI,OCR,不是被PUA就是吃老板画的大饼,再丰盛的早餐一想到一堆任务要做,吃着也如同嚼蜡,更别提神清气爽,心境开拓了。


在进入黄果树后,我欢快的脚步往前走,因为我是一个女孩子独行,所以不太愿意跟陌生人说话,一路上虽然很沉默,但看到这些壮观的自然景色,闻着草木花果香,内心激动不已。爬了一个钟左右的山,终于看到了大瀑布。


下面是我实拍的景点图:


WechatIMG110.jpg
WechatIMG114.jpg
WechatIMG111.jpg
WechatIMG115.jpg

有句古诗,疑似银河落九天,一路好山好水,逛完黄果树后我出来就去吃了贵州的特色菜,价格美丽,味道很不错,超级喜欢,
当时就在感慨,上海要是能吃到这么好吃又鲜美的酸汤鱼就好了。


WechatIMG109.jpg

重庆


本来下一步去梵净山再顺路去成都自驾318路线的,但时间紧迫,我弟弟在重庆读书,说要跟我一起去自驾318,我就先去重庆跟他汇合了。


1191711609983_.pic.jpg
1211711610320_.pic.jpg
1241711610640_.pic.jpg

最后那个火锅要适度吃啊,吃两顿辣的我的陈年胃病都犯了,好几天没缓过来,哭晕在厕所,我弟跟个没事人一样,这是我深刻认识到当了多年的社畜的后果就是,经常熬夜加班点外卖,把好好的身体给造坏了。重庆的洪崖洞,解放碑也去了很多次了,这里给个图


1201711609995_.pic.jpg

成都


抵达成都,在春熙路逛了逛,宽窄巷子之前逛过就没去了,


1221711610329_.pic.jpg
1461711611961_.pic.jpg

本来想租自驾神车-坦克300的,价格是普通suv的2倍,结果路上纠结一会的功夫就被抢先租走了(自我反思:以后看准就下手吧,人生有几次这种机会,有啥好犹豫的),租了一辆1.5T的大众SUV,跟我弟一起直奔车行,然后去超市采购路上的食物,大包小包买了一堆,放车后备箱,深夜就起航了


都江堰


教科书上的都江堰,真正去看了,才深深佩服古人治水的智慧,我不是文盲,所以不用一句:卧槽,发表感叹。之前也去过洛阳的黄河小浪底水库,武汉的长江大桥,这些水利工程的智慧。


1261711610658_.pic.jpg
1251711610651_.pic.jpg

青城山


1451711611634_.pic.jpg

这里是青城山下白素贞的故事发源地。爬山是个体力活,当时穿着拖鞋就上山了,下山就傻眼了,不好意思,这里我偷懒了,坐缆车下车,嘿嘿。


泸定桥


1281711611190_.pic.jpg
1271711610783_.pic.jpg

打卡泸定桥,走上面摇摇晃晃确实需要一些勇气,特别怕手机掉下去。


海螺沟


一鼓作气,一路直行,抵达海螺沟。来之前,我觉得新能源车咋自驾318,路上看到同样是特斯拉车主,我感觉自己有点狭隘。人啊,果然要多出去看看,不能活在自己的局限认知中。


1301711611238_.pic.jpg
1481711613272_.pic.jpg
1311711611256_.pic.jpg

不过开车还是要小心,路上遇到有车盘山时发生侧翻的。还有山上偶尔会有落石下来,要当心了。


木格措


一路景色壮观,蓝天白云,川西一定要必去。到了康定情歌的原地。打个卡。


1331711611313_.pic.jpg
1471711612939_.pic.jpg

不过我路上听的歌一直都是朴树的《平凡之路》,一路循环:


我曾经跨过山和大海 也穿过人山人海

我曾经拥有着的一切 转眼都飘散如烟

我曾经失落失望 失掉所有方向

直到看见平凡 才是唯一的答案
....


不正是正值青春的我受伤了,但又奋力前行寻找答案吗。


四姑娘山


一路直行。。。抵达四姑娘山,四姑娘山有四座雪山组成,远看景色很壮观,雪已经化了很多。


1291711611214_.pic.jpg
1441711611506_.pic.jpg
1421711611464_.pic.jpg

当地信仰


遇到了一群一动不动的牦牛,还有一匹热情好客的长脸马。拿出来一个饼给它,它吃的还很香。本来开心的事现在记录起来突然感觉在暗示自己在公司当牛做马,不说了,emo了。据说那白色塔这是当地的信仰,表示尊重。


1361711611362_.pic.jpg
1351711611349_.pic.jpg
1391711611410_.pic.jpg

雅拉山口


盘山路,1.5T的车开着有点吃力,油门上不去。终于爬上山了,下车拍照时,激动过头了,开始缺氧,头疼,吸氧。。。。。。。。


1371711611374_.pic.jpg
1381711611398_.pic.jpg
1561711614555_.pic.jpg

后面走着走着身体扛不住了,我去当地买了高反的药,吃了没啥用,氧气越吸头越疼,我弟要回去上课,我身体不抗造,遗憾的半途而归了。再次强调一下,好风景要趁年轻,体力好,等老了走不动了,确实再好的风景,都没那心情和体力去欣赏了。


1551711614521_.pic.jpg
1571711614582_.pic.jpg

乐山


跟我弟散伙后,我自己开车去了乐山大佛,保佑我顺风顺水吧。还去看了东方佛群,卧佛,药师佛,看了各种佛,记不清楚了。。。


1531711614452_.pic.jpg
1601711617453_.pic.jpg
1611711617471_.pic.jpg

峨眉山


接着我自己又自驾去了峨眉山,两个地方相差不是很远,看到了峨眉山的云海,云雾缭绕,超级刺眼!


1521711614435_.pic.jpg
1511711614416_.pic.jpg

下山后当晚接着又踩着点返回成都还车。休息一晚后,又顺路打卡了锦里。感受人世间的烟火和繁华


1591711614637_.pic.jpg
1581711614623_.pic.jpg

又吃了一顿火锅后,回上海。这时,胃没有不舒服,看来,这一圈下来,肠胃好很多了。


1621711617506_.pic.jpg

又回到了我熟悉的大上海。


安徽


经过一段时间的调养后,我觉得的身体状态老好了,爬山那不是小意思,走,爬山去,什么黄山,三清山,庐山,武功山,离沪这么近,爬起来不费劲!我到了安徽省,黄山市,休息一晚准备去爬山。当晚被出租车司机拉到了老街逛逛。


1631711617723_.pic.jpg

就一个小型的徽派建筑青砖白瓦的特色,跟顾村差不多。逛完后突然下起了大雨,我猝不及防没带伞,
就记得那晚的雨,比情深深雨蒙蒙中依萍找她爸要钱被鞭子抽回去时遇到的那场大雨还大。。。。。。


黄山


不凑巧,上山时遇到了大雾,但来都来了,那就爬山下去吧。到了光明顶也啥都看不见,但幸运的时,下山时,守得云开见月明,气喘吁吁的开心拍照。


1641711617970_.pic.jpg
1651711617990_.pic.jpg
1661711618010_.pic.jpg

江西


黄山结束后,顺路就来到了江西,江西景色比较集中,一定要去上饶啊,那就先去望仙谷看看吧。


上饶-望仙谷


人工打造的经典,现实版的仙侠世界。小雨朦胧,青山傍水,景色秀丽。


1681711618049_.pic.jpg
1671711618030_.pic.jpg
1691711618070_.pic.jpg

上饶-三清山


谁说黄山归来不看山,我觉得三清山值得一去,至少我是不后悔的。每座山都有每座山的特色,爬到这时,腿开始抖了,但我可不是那么轻易就能认输的人啊,继续爬,专挑难爬的道:一线天!!!!!


1721711618536_.pic.jpg
1701711618509_.pic.jpg
1711711618524_.pic.jpg

哈哈,说这个像蟒蛇,像吗?


1731711618548_.pic.jpg

下山时腿疼的不行,扛不住了,嘴不硬了,不去庐山了,武功山了。。。。


南昌


对了,不明白为啥江西彩礼那么高?


1741711619525_.pic.jpg

广东


广州


从南昌飞到广州了,看了小蛮腰,在附近喝喝茶,遛遛弯,吃点茶点


1751711619548_.pic.jpg
1761711619562_.pic.jpg

深圳


到深圳后租了个车溜达到海边吃海鲜,还去华强北也溜一溜,吃了很多粤菜


2121711623487_.pic.jpg
2131711623507_.pic.jpg
2141711623518_.pic.jpg

香港


从深圳坐高铁到香港也就十几分钟,跟快的。香港巴士,香港茶餐厅,路过金店,想买项链的,但又怕弄丢了就没买,现在金价那么高,有点损失。


2171711623568_.pic.jpg
2181711623590_.pic.jpg
2161711623554_.pic.jpg
2151711623538_.pic.jpg

新疆


从上海飞新疆要4个多小时,一路太无聊了,下飞机后,心情就好很多


1781711619752_.pic.jpg

乌鲁木齐


去了大巴扎,吃了羊肉串和切糕,还有新疆大盘鸡


1911711619947_.pic.jpg
1921711619959_.pic.jpg
1931711619971_.pic.jpg
1951711620020_.pic.jpg

无人区


没信号,没水,荒漠一片。。。


1901711619929_.pic.jpg
1891711619916_.pic.jpg

伊犁


到了伊犁市区后,去了小吃街,吃了羊肉


1961711620038_.pic.jpg

赛里木湖


高原湖泊,非常适合自驾游玩,我这里是跟人拼车去的。看着真舒服,可惜我把单反带来,也背不动,这是人家的


1851711619856_.pic.jpg
1841711619835_.pic.jpg
1801711619776_.pic.jpg
1831711619820_.pic.jpg
1811711619793_.pic.jpg
1821711619807_.pic.jpg

边境-国门,果子沟大桥, 薰衣草


1881711619895_.pic.jpg
1871711619882_.pic.jpg
1861711619867_.pic.jpg

新疆白天长,夜里段,到了晚上9点多,天才慢慢开始变黑。


北京


这次我飞到了老北京,看了天安门,看了老城墙


1971711620074_.pic.jpg
1981711620099_.pic.jpg

内蒙古


从北京顺路来了内蒙古呼和浩特,先填饱肚了,去那个什么街买了一堆牛肉干


呼和浩特


1991711623013_.pic.jpg
2011711623052_.pic.jpg
2021711623101_.pic.jpg

青甘环线


说到去青甘,想起有个在学生时期就在玩的狐朋狗友,听说我打算去自驾就想跟我一起去。因为我的车是新能源,自驾充电比较麻烦,他打算提混动车方便些,他说让我等他提车带他一起去自驾,本来约定好了时间,到快出发时,一会又说不打算提车了,又说等他面试换好工作后,最后他自己又各种理由怂了,这种又想出去玩,又想挣钱,又不舍得花钱,这种拧巴的状态,我很无语,当然,这也是现实中大部分人的写实吧,这里我想说,做好权衡利弊和取舍就好,既然决定去追求诗和远方,就不要再去跟钱分文必争了,不可否认,旅行确实需要花钱,我们能做的就是按照自己能承担的最低的成本去看世界。人家说勇敢的人先享受世界,让他纠结犹豫去吧,我就先溜了,毕竟老祖宗给的经验是:欲买桂花同载酒,终不似,少年游。再后来,他说他提车了,问我还去不去,我说我早就已经打卡过了。我问他新工作找好了?他说还没有。。。所以他白拧巴了,车还是要提,想去的地方最终还是要去,挣不了的钱最终还是没到口袋里去。毕竟能随时说走就走的同行者只有自己。


我是从内蒙飞到了青海的西宁。


西宁市


填饱肚子先,然后出发去青海湖,远看蓝色,近看青色,全靠天气


2031711623308_.pic.jpg
2351711690717_.pic.jpg

青海湖


2091711623426_.pic.jpg
2051711623359_.pic.jpg

茶卡盐湖


天空之境,名不虚传。


2061711623379_.pic.jpg
2081711623412_.pic.jpg
2361711690772_.pic.jpg

丹霞地貌,策马奔腾


2111711623453_.pic.jpg
2101711623438_.pic.jpg

策马奔腾很潇洒,归来草原上都是马粪,有点臭。。。
仙气飘飘的牦牛,跟川西的大黑牛不一样
2231711624048_.pic.jpg
2041711623341_.pic.jpg


后面的敦煌,莫高窟去不了了,青海也是有3000多海拔的,玩嗨了,又又又高反了,不得已要回去了,哎,当了这么多年生产驴,身体熬废了。回去后多锻炼身体吧。毕竟身体是革命的成本。


武汉


于是,先飞回了武汉玩几天。回家转转,熟悉的感觉。喜欢武汉的大江大湖和历史文化。黄鹤一去不复返,白云千载空悠悠。
然后又从武汉飞到上海狗着。


2241711624590_.pic.jpg
2261711624823_.pic.jpg
2251711624807_.pic.jpg

上海市


这个城市充满了魅力。只要你有钱,就可以纸醉金迷,去和平饭店享受,去挥霍。没钱,只能继续搬砖。


2291711625200_.pic.jpg
2301711625213_.pic.jpg
2271711624846_.pic.jpg


回去后改善饮食,一边努力干活学习,一边下定决心锻炼,都有马甲线了,五公里so easy ,哈哈哈哈。每次回到上海这个繁华的国际大都市,我都深深感受到,这座城市虽然压力大,但终究是自由的,没人关心和打扰你的私人生活,你可以为自己而活,安排自己的一生,不必循规蹈矩,不必顾及世俗的眼光,这个城市包容能力很强,不妨大胆一些,追求自己的人生。去不同的城市体验不一样的生活和文化。




在买随心飞之前我也去过很多城市,比如:湖北的荆州,湖南的岳阳,张家界,广东的东莞,广西的桂林和北海,海南的三亚,云南的昆明大理丽江,江浙沪包邮一带的杭州,南京,无锡,湖州,台州,宁波,福建的厦门,河南的洛阳,开封,郑州,信阳,山东青岛,陕西西安,安徽合肥等城市。时间有限,码字不易,很抱歉这里我就不全部列出了。尤其在学生时代,那是真的快乐,没有一丝丝杂念,单纯的快乐。后面打算环游世界了,已经去了东南亚的一些国家,这里我想说我本来就是为了WLB努力的,工作生活两不误,我的旅途未完待续~


回顾这么多年,走过的国内大大小小的城市,也没具体统计过,开始逐渐让自己的眼界开阔起来,不让自己的眼光那么狭隘了,看待任何事物更具包容性吧,以前不理解的东西,现在慢慢理解了。也许人生就是这样,思想和观念一直变化。还是那句话,勇敢的人先享受人生吧,不要辜负努力写代码的自己。


作者:为了WLB努力
来源:juejin.cn/post/7351301965034586152
收起阅读 »

携手15年,语雀创始人玉伯从蚂蚁离职,选择一个人远行

转载好文:雷锋网 本文作者:何思思 2023年4月28日,即4月28日凌晨,玉伯发朋圈称将要离开蚂蚁,今天也是玉伯在蚂蚁的最后一天。 他写道:“再见,山峰下的园区。一个人选择远行,并不一定是马云说的钱给少了或者心委屈了,也可以是为了远方有西湖般的美景。”下面...
继续阅读 »

转载好文:雷锋网 本文作者:何思思


image.png


2023年4月28日,即4月28日凌晨,玉伯发朋圈称将要离开蚂蚁,今天也是玉伯在蚂蚁的最后一天。


他写道:“再见,山峰下的园区。一个人选择远行,并不一定是马云说的钱给少了或者心委屈了,也可以是为了远方有西湖般的美景。”下面的配图是园区风景,还有眺望远方的景色。


不愿做技术大佬,要做为产品服务的技术


“前端大牛、技术大佬”是业界给玉伯贴的标签,2008年加入淘宝后,玉伯先后做出了前端领域很火的框架 SeaJS、KISSY,之后带领团队通过开源做了很多技术产品。


但玉伯始终认为,技术只是工具,最终还是要为产品服务。所以当时在淘宝内部,玉伯一直是“折腾”的状态,加入淘宝那年,玉伯就参加了内部的赛马机制,跟团队做了几个月的创新产品,最后以失败告终,又回到了Java 团队做技术。


但这并有改变他要做创新产品的初心,于是2010 年到2011年,他一边做技术研发,一边继续摸索创新产品,但一直没做出能拿的出手的产品。直到2016年,在蚂蚁体验技术部的创新产品孵化机制策马扬鞭项目中,玉伯团队主导的语雀问世,并于2018年正式对公网提供服务。


也有内部人士称:玉伯当时和老板提了条件说,光做前端没意思,你要想留住我,就得给我一款产品做。所以当时玉伯自己要了一个团队,专门做一个闭环产品。


其实,从语雀诞生到现在经历了两次生死局:第一次是2018年,腾讯文档、钉钉文档、飞书文档相继亮相,文档产品迎来爆发期,当时阿里也想抓住这个风口,语雀最终把三分之二的人输送给了钉钉,作为钉钉文档的初始团队。在团队仅剩七八个人的时候,玉伯再次招人将团队扩充到二十人左右。


第二次是2020年,彼时,钉钉文档做了很久但并没达到预期效果,而语雀正值上升期,阿里云为了尽快把文档做起来,想把语雀、钉钉文档、阿里云笔记等内部各种文档团队聚集起来,成立一个独立的阿里文档事业部,由玉伯牵头,但却被无招反对,这也间接帮助了玉伯。


直到2021年,蚂蚁成立了智能协同事业部,其中语雀作为重点产品,以独立BU运作。


创业中的理想派,为了做好一件事而做


从2016年到现在,为了做好语雀,玉伯做了大量的工作。


玉伯曾回忆道,做语雀最大的一个感触是,啥都得做。最开始是半个PD,很快变成了客服,同时兼做运营,还需要去承担BD的工作,因为没有BD,只能逼着自己去做,一切为了产品往前跑。


也有用户在即刻分享道,自己曾经在语雀的付费用户群中提了一个文档的排序问题,当时玉伯就在群里,很快的响应了这个需求并做了优化。


image.png


此外,玉伯也背负了巨大的营收压力,尤其是近两年在阿里集团整体缩紧的状态下。雷峰网通过其他渠道了解到,集团也给语雀定了目标——“盈亏平衡”。


迫于压力,近两年语雀也调整了收费策略,2019年语雀开始尝试简单的商业化模式,即初级的团队语雀空间和语雀个人的收费版本;紧接着又重新设计了个人版价格策略,分为99元会员、199元超级会员、299元至尊会员三个档次,团队和空间版的收费则更高。


这对一个小团队来说并不容易,首先,较钉钉、飞书、腾讯文档而言,语雀强调的是知识管理的逻辑,其次,语雀服务的对象偏小众聚焦在侧重知识管理的用户,且这些目标对象比较分散,很难第一时间发掘到,这就意味着需要花很长时间去培养,没办法快速完成转化;再就是,虽然语雀团队不大,只有五六十人左右,但这部分人大都是互联网人才,成本也是一笔不小的支出。


雷峰网在之前拜访玉伯时听闻,目前语雀主要服务蚂蚁和阿里内部,在阿里内的日活已经达到了11万左右,商业化方面还比较单一,主要是通过发布会的方式宣传。由此可见,语雀的商业化路径还没完全打开。


无论选择出去创业还是集团内部创业,背负营收压力都是不可避免的。但抛开这个不谈,仅玉伯的个人角度出发,他曾谈过自己做语雀的初心,就是想把自己内心想做的事情做完,且这件事还能帮助到别人,就做了。


正是这种简单纯粹的心态,让玉伯在做语雀时只专注事情的本身以及这件事情创造的价值,而并非拼命地追求变现。


雷峰网(公众号:雷峰网)曾发表文章《留给飞书的时间》,他如此评论:



“现实主义者关注的是钱,理想主义者关注的是时间,当代这个社会,钱很重要。但更重要的,对个体来说,是如何提高时间的质量,对人类来说,不仅关注时间的质量,还关注整个人类时间的长短,是否可延续下去。赚钱是为了花钱,花钱是为了提升时间的品质甚至长度。围绕钱的现实主义者,最终会为围绕时间的理想主义者服务。”



从玉伯最新的朋友圈内容,不难看出,他的离开或许和钱权没有太大的关系,而是为了追求心目中的诗和远方。他也曾经说过自己有三个梦:“技术梦、产品梦、自由梦。”离开蚂蚁,或许是为了去实现他的“自由梦。”


作者:狗头大军之江苏分军
来源:juejin.cn/post/7299035378589040667
收起阅读 »

Vue3 新项目,没必要再用 Pinia 了!

web
最近弄了一个新的 Vue3 项目,页面不多,其中有三四个页面需要共享状态,我几乎条件反射般地安装了 Pinia 来做状态管理。后来一想,我只需要一个仓库,存放几个状态而已,有必要单独接一套 Pinia 吗?其实不需要,我差点忘记了 Vue3...
继续阅读 »

最近弄了一个新的 Vue3 项目,页面不多,其中有三四个页面需要共享状态,我几乎条件反射般地安装了 Pinia 来做状态管理。

后来一想,我只需要一个仓库,存放几个状态而已,有必要单独接一套 Pinia 吗?

其实不需要,我差点忘记了 Vue3 的一个重要特性,那就是 组合式函数

组合式 API 大家都知道,组合式函数可能大家没有特别留意。但是它功能强大,足矣实现全局状态管理。

组合式函数

什么是组合式函数?以下是官网介绍:

在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

从这段介绍中可以看出,组合式函数要满足两个关键点:

  1. 组合式 API。
  2. 有状态逻辑的函数。

在 Vue 组件中,状态通常定义在组件内部。比如典型的选项式 API,状态定义在组件的 data() 方法下,因此这个状态只能在组件内使用。

Vue3 出现之后,有了组合式 API。但对于大部分人来说,只是定义状态的方式从 data()变成了 ref(),貌似没有多大的区别。

实际上,区别大了去了。

组合式 API 提供的 ref() 等方法,不是只可以在 Vue 组件内使用,而是在任意 JS 文件中都可以使用。

这就意味着,组合式 API 可以将 组件与状态分离,状态可以定义在组件之外,并在组件中使用。当我们使用组合式 API 定义了一个有状态的函数,这就是组合式函数。

因此,组合式函数,完全可以实现全局状态管理。

举个例子:假设将用户信息状态定义在一个组合式函数中,方法如下:

// user.js
import { ref } from 'vue'

export function useUinfo() {
// 用户信息
const user_info = ref(null)
// 修改信息
const setUserInfo = (data) => {
user_info.value = data
}
return { user_info, setUserInfo }
}

代码中的 useUinfo() 就是一个组合式函数,里面使用 ref() 定义了状态,并将状态和方法抛出。

在 Vue3 组件之中,我们就可以导入并使用这个状态:


仔细看组合式函数的使用方法,像不像 React 中的 Hook?完全可以将它看作一个 Hook。

在多个组件中使用上述方法导入状态,跨组件的状态管理方式也就实现了。

模块化的使用方法

组合式函数在多个组件中调用,可能会出现重复创建状态的问题。其实我们可以用模块化的方法,更简单。

将上方 user.js 文件中的组合式函数去掉,改造如下:

import { ref } from 'vue'

// 用户信息
export const user_info = ref(null)
// 修改信息
export const setUserInfo = (data) => {
user_info.value = data
}

这样在组件中使用时,直接导入即可:


经过测试,这种方式是可以的。

使用模块化的方法,也就是一个文件定义一组状态,可以看作是 Pinia 的仓库。这样状态模块化的问题也解决了。

Pinia 中最常用的功能还有 getters,基于某个状态动态计算的另一个状态。在组合式函数中用计算属性完全可以实现。

import { ref, computed } from 'vue'

export const num1 = ref(3)

export const num2 = computed(()=> {
return num1 * num1
}

所以思考一下,对于使用 Vue3 组合式 API 开发的项目,是不是完全可以用组合式函数来替代状态管理(Pinia,Vuex)呢?

当然,以上方案仅适用于组合式 API 开发的普通项目。对于选项式 API 开发的项目,或者需要 SSR,还是乖乖用 Pinia 吧 ~

最重要的是!如果面试官问你:除了 Pinia 和 Vuex 还有没有别的状态管理方案?

你可别说不知道,记住这几个字:组合式函数!


作者:杨成功
来源:juejin.cn/post/7348680291937435682
收起阅读 »

当遇到需要在Activity间传递大量的数据怎么办?

在Activity间传递的数据一般比较简单,但是有时候实际开发中也会传一些比较复杂的数据,尤其是面试问道当遇到需要在Activity间传递大量的数据怎么办? Intent 传递数据的大小是有限制的,它大概能传的数据是1M-8K,原因是Binder锁映射的内存大...
继续阅读 »

在Activity间传递的数据一般比较简单,但是有时候实际开发中也会传一些比较复杂的数据,尤其是面试问道当遇到需要在Activity间传递大量的数据怎么办?


Intent 传递数据的大小是有限制的,它大概能传的数据是1M-8K,原因是Binder锁映射的内存大小就是1M-8K.一般activity间传递数据会要使用到binder,因此这个就成为了数据传递的大小的限制。那么当activity间要传递大数据采用什么方式呢?其实方式很多,我们就举几个例子给大家说明一下,但是无非就是使用数据持久化,或者内存共享方案。一般大数据的存储不适宜使用SP, MMKV,DataStore。


Activity之间传递大量数据主要有如下几种方式实现:


  • LruCache

  • 持久化(sqlite、file等)

  • 匿名共享内存


使用LruCache

LruCache是一种缓存策略,可以帮助我们管理缓存,想具体了解的同学可以去Glide章节中具体先了解下。在当前的问题下,我们可以利用LruCache存储我们数据作为一个中转,好比我们需要Activity A向Activity B传递大量数据,我们可以Activity A先向LruCache先写入数据,之后Activity B从LruCache读取。


首先我们定义好写入读出规则:


public interface IOHandler {
   //保存数据
   void put(String key, String value);
   void put(String key, int value);
   void put(String key, double value);
   void put(String key, float value);
   void put(String key, boolean value);
   void put(String key, Object value);

   //读取数据
   String getString(String key);
   double getDouble(String key);
   boolean getBoolean(String key);
   float getFloat(String key);
   int getInt(String key);
   Object getObject(String key);
}

我们可以根据规则也就是接口,写出具体的实现类。实现类中我们保存数据使用到LruCache,这里面我们一定要设置一个大小,因为内存中数据的最大值是确定,我们保存数据的大小最好不要超过最大值的1/8.


LruCache mCache = new LruCache<>( 10 * 1024*1024);

写入数据我们使用比较简单:


@Override
public void put(String key, String value) {
   mCache.put(key, value);
}

好比上面写入String类型的数据,只需要接收到的数据全部put到mCache中去。


读取数据也是比较简单方便:


@Override
public String getString(String key) {
   return String.valueOf(mCache.get(key));
}

持久化数据

那就是sqlite、file等方式。将需要传递的数据写在临时文件或者数据库中,再跳转到另外一个组件的时候再去读取这些数据信息,这种处理方式会由于读写文件较为耗时导致程序运行效率较低。这种方式特点如下:


优势:


(1)应用中全部地方均可以访问


(2)即便应用被强杀也不是问题了


缺点:


(1)操做麻烦


(2)效率低下


匿名共享内存

在跨进程传递大数据的时候,我们一般会采用binder传递数据,但是Binder只能传递1M一下的数据,所以我们需要采用其他方式完成数据的传递,这个方式就是匿名共享内存。


Anonymous Shared Memory 匿名共享内存」是 Android 特有的内存共享机制,它可以将指定的物理内存分别映射到各个进程自己的虚拟地址空间中,从而便捷的实现进程间内存共享。


Android 上层提供了一些内存共享工具类,就是基于 Ashmem 来实现的,比如 MemoryFile、 SharedMemory。


作者:派大星不吃蟹
来源:juejin.cn/post/7264503091116965940
收起阅读 »

不容错过的秘籍:JavaScript数组的创建和使用详解

在编程的世界里,数据是构建一切的基础。而在JavaScript中,有一种特殊且强大的数据结构,它就是——数组。今天,我们就来一起探索数组的奥秘,从创建到使用,一步步掌握这个重要的工具。一、什么是数组数组(Array)是一种按顺序存储多个值的数据结构。你可以把它...
继续阅读 »

在编程的世界里,数据是构建一切的基础。而在JavaScript中,有一种特殊且强大的数据结构,它就是——数组。

今天,我们就来一起探索数组的奥秘,从创建到使用,一步步掌握这个重要的工具。

一、什么是数组

数组(Array)是一种按顺序存储多个值的数据结构。你可以把它想象成一个盒子,这个盒子可以存放多个物品,而且每个物品都有一个编号,我们可以通过这个编号来找到或者修改这个物品。

在JavaScript中,数组是一种特殊的对象,用于存储和操作多个值。与其他编程语言不同,JavaScript的数组可以同时存储不同类型的值,并且长度是动态的,可以根据需要随时添加或删除元素。

Description

JavaScript数组使用方括号([])来表示,其中的每个元素用逗号分隔。例如,以下是一个包含不同类型元素的数组的示例:

var myArray = [1, "two", true, [3, 4, 5]];

数组中的元素可以通过索引来访问和修改,索引从0开始。例如,要访问数组中的第一个元素,可以使用以下代码:

var firstElement = myArray[0];

JavaScript也提供了一些内置方法来操作数组,如push()、pop()、shift()、unshift()等,用于添加、删除和修改数组中的元素。

二、数组的作用

数组在编程中扮演着非常重要的角色。它可以帮助我们:

  • 存储多个值:我们可以在一个变量中存储多个值,而不需要为每个值创建单独的变量。

  • 操作数据:我们可以对数组中的元素进行添加、删除、修改和查找等操作。

  • 实现各种算法:通过数组,我们可以实现排序、搜索等常见算法。

  • 循环遍历:数组的元素是有序的,可以使用循环结构遍历数组的每个元素,从而对每个元素进行相同或类似的操作。这在处理大量数据时非常有用。

三、创建数组的方法

在JavaScript中,有多种方法可以创建数组,下面列出常见的三种:

1)字面量方式:

这是最常见的创建数组的方式,只需要在一对方括号[]中放入元素即可,如

var arr = [];

2)使用Array构造函数:

通过new Array()也可以创建数组,如

var arr = new Array();

3)使用Array.of()方法:

这个方法可以创建一个具有相同元素的新数组实例,如

var arr = Array.of(1, 2, 3);

四、使用数组的方法

创建了数组后,我们就可以对它进行各种操作了:

1、访问和修改数组元素

要访问和修改数组元素,需要使用数组的索引。数组的索引从0开始,依次递增。要访问数组元素,可以使用以下语法:

console.log(arr[0]); // 输出第一个元素
arr[1] = 4; // 修改第二个元素的值

2、向数组末尾添加元素

要向数组的末尾添加一个元素,可以使用数组的push()方法。该方法 会在数组的末尾添加指定的元素。以下是使用push()方法向数组末尾添加元素的示例:

arr.push(5);

3、从数组末尾移除元素

要从数组的末尾移除一个元素,可以使用数组的pop()方法。该方法 会移除并返回数组中的最后一个元素。以下是使用pop()方法从数组末尾移除元素的示例:

arr.pop();

4、从数组末尾移除元素
要从数组的末尾移除一个元素,可以使用数组的unshift()方法。该方法 会移除并返回数组中的最后一个元素。以下是使用unshift()方法从数组末尾移除元素的示例:

arr.unshift(0);

5、从数组开头移除元素
要从数组的开头移除一个元素,可以使用数组的shift()方法,并将索引值设置为0。该方法 会移除并返回数组中的第一个元素。以下是使用shift()方法从数组开头移除元素的示例:

arr.shift();

6、获取数组的长度
要获取数组的长度,可以使用内置函数length()。length()函数返回数组中元素的个数。以下是获取数组长度的示例:

console.log(arr.length);

7、遍历数组

要遍历数组的所有元素,可以使用for循环。下面是遍历数组的示例:

for (var i = 0; i < arr.length; i++) {
console.log(arr[i]);
}

8、数组排序

要对数组进行排序,可以使用JavaScript内置的sort()方法。下面是对数组进行排序的示例:

arr.sort();

9、数组反转

要对数组进行反转,可以使用JavaScript内置的reverse()方法。下面是对数组进行反转的示例:

arr.reverse();

10、数组搜索
要在数组中搜索特定的元素,可以使用循环遍历数组,逐个比较每个元素与目标值,找到目标值后返回其索引。下面是一个示例代码:

console.log(arr.indexOf(3)); // 返回3在数组中的索引位置
console.log(arr.includes(4)); // 检查数组中是否包含4

以上就是一些常见的数组操作方法,可以根据需要使用适当的方法来操作数组中的元素。

想要快速入门前端开发吗?推荐一个免费前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!点这里前往学习哦!

五、使用数组方法的注意事项

  • 数组方法是JavaScript中针对数组对象的内置方法,可以方便地对数组进行操作和处理。

  • 使用数组方法之前,需要先创建一个数组对象。可以使用数组字面量创建一个数组,也可以使用Array()构造函数来创建一个数组。

  • 数组方法可以改变原始数组,也可以返回一个新的数组。需要根据实际需求来选择使用具体的方法。

  • 改变原始数组的方法包括:push()、pop()、shift()、unshift()、splice()、sort()、reverse()等。

  • 不改变原始数组的方法包括:slice()、concat()、join()、map()、filter()、reduce()、forEach()等。

  • 使用数组方法时需要注意方法的参数和返回值,不同的方法可能需要不同的参数,并且返回值类型也可能不同。

  • 数组方法的具体用法可以参考JavaScript官方文档或者其他相关教程和资源。熟练掌握数组方法可以提高代码的效率和可读性。

以上就是JavaScript数组的创建和使用方法,希望对你有所帮助。记住,数组是JavaScript中非常重要的一部分,掌握好它可以让我们的编程工作更加高效。

收起阅读 »

Chat Gpt详细教程:手把手带你Open AI 的API对接

AI
今年4月最大的一个瓜,就是Open AI全面免费开放了,所以很多人想白漂API但却不知该如何去获取Open AI的API,甚至好多小伙伴都碰壁在注册Open AI的半路上了,甚至是如何开通海外付费都变成一个难题~所以针对以上的问题,我将出一份教程为大家一一解决...
继续阅读 »

今年4月最大的一个瓜,就是Open AI全面免费开放了,所以很多人想白漂API但却不知该如何去获取Open AI的API,甚至好多小伙伴都碰壁在注册Open AI的半路上了,甚至是如何开通海外付费都变成一个难题~

所以针对以上的问题,我将出一份教程为大家一一解决。当然,本次教程全程是由本人跑过一遍的,本人亲测不封号、不踩雷、不墨迹。

Description

在整个的注册、激活Open AI账户、升级Open AI使用级别、对接API等等都会借助其他工具而产生一些费用,请各位老板慎重考虑并尝试。

在教程学习中产生的其他平台的工具费用与任何问题都与本教程无关,请各位老板悉知。

注意:所有工具的昵称全部统称为“XXX国际旅游卡”担心被优化了~ 不懂的可以问。

话不多说,开始进入正题。

一、第一步,注册谷歌邮箱

大家去下载一个谷歌浏览器,然后安装到电脑上,再去寻一个稳定“梯子”。准备工作就算完了。接下来给大家演示注册流程哈~

咳咳~ 有一些小伙伴可能不理解什么是“梯子”请自行百度搜索哈。或者可以来咨询我给你推荐一个好用的。

1、在谷歌网页输入Goolgle的地址:www.google.com,点击右上角的登录页面。
Description
2、跳转到登录页面后,点击“创建账号”,选择个人用途。下一步。
Description
3、填写基本信息,我一般会选择把名字写成英文字母,因为这样会显得很高级。Hhh~
Description
4、填写生日,随便写也可以,完全不影响后面的使用。
Description
5、创建你自己的Gmail地址,也可以使用默认生成的Gmail地址。看个人喜好了。
Description
6、设置密码,这一步重要的不是设置密码,而是保存好密码。省的到时候记不得密码了。我一般会写在我的便签里,方便后面查找使用。
Description
7、这里填写一个你的QQ邮箱即可,方便后续重要操作。多一个保障么~如果你不打算长期用的话,就直接跳过即可。
Description
8、确认信息,下一步,阅读协议,同意,注册成功!恭喜恭喜~
Description
Description
9、修改谷歌个人资料,不想改的跳过即可。
Description
10、首次登录后会出现验证登录的情况。重新验证登录一下就OK了
Description
11、手机号绑定验证,这个时候写国内的手机号码即可,短信都是秒到。输入验证码,就OK了

验证码是G- 开头的,输入后面的纯数字就OK。
Description
Description
成功登录!记得自己切换成中文模式哦~

二、第二步,注册Open AI

1、访问Open Ai 官网,点击登录。
Description
2、登录验证,正常跟着指示操作即可。
Description
配合验证就OK,
3、关键的时刻到了,使用谷歌账号登录!省去一切繁琐步骤。
Description
4、选择当前已登录的谷歌账号即可。没什么技术含量了~ 跟着步骤走就准没错。
Description
5、点击继续,(要是英文看着难受,就点下面的切换语言即可,不切换的话点击的位置都是一样的。)
Description
6、创建Open AI的基本账户信息,最好是英文,你写中文也行,就是后面会显示的很奇怪。
Description
示范的模板~ 按着下面的格式去写就OK了。
Description
7、点击前往获取API,这点毋庸置疑了。先把API拿到手,再去体验GPT吧!
Description
8、到达了主页面!点击侧边的菜单栏,选择API Keys——创建新的API,看图吧!
Description
Description
9、这一步很重要了!非常的重要。在首次获取API的时候,Open AI会要求你验证手机号的!国内手机暂时不大行,所以这个时候你需要一个海外的旅游卡,这是重点来咯!!!

三、第三步,国际SIM旅游卡

1、现在我们需要借助一个工具!国际SIM旅游卡租赁~ 我们去访问s ms-activate。短期出国、酷爱外服游戏的的朋友们应该清楚这玩意儿的好处。此处不做过多讲解。
Description
2、这里的注册流程与前面一样,选择用谷歌账号登录即可,这里不用过多的废话,里面都是中文介绍的方便很多。但是记得先充值点余额进去,方便后面使用它的短信接受功能。
Description
3、充值的话国际旅游卡支持多重支付方式,也包括了咱们国内的支付宝。充值的时候选择一个最低档就可以,够你使用了。
Description
4、充值成功后,就不用管它了。我们去租用一个国际SIM旅游卡。在首页选择租用——Whatsapp——选择一个国家的旅游卡
Description
5、但你租赁成功后,在右边会显示你的旅游卡的SIM卡 号,这个时候你就可以去激活Open ai 的API 了。(回到第二步的第7点位置,应该不需要再截图给大家看了~ )

提示:它的最低租用时效是4个小时,4个小时够你随便玩耍了。

四、第四步,创建API

1、回到openai 界面后点击创建秘钥——填写项目昵称——选择项目类——全部权限——创建秘钥。
Description
Description
2、这一步非常的关键!一定要点复制,然后保存好,API秘钥仅在首次能看见全部秘钥,等你退出这个界面的时候,就再也看不到全部秘钥了。切记切记!!!
Description
好了,这一步你已经成功的获取秘钥了!!!接下来是升级Open AI的账户级别了。

五、第五步,升级Open AI使用级别

首先点击绑定国际旅游卡信息~
Description
我们的账户默认级别是0,我们需要升级到级别1。
Description
下图为未升级级别是对接API的报错信息,不信的可以去试试~ 白漂还是有点门槛的。
Description
但是我们没有国际卡可怎么办呢?别急,教都教了怎么可能教一半呢?各位老板继续往下看!


想要快速入门前端开发吗?推荐一个免费前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!点这里前往学习哦!


六、第六步,注册+绑定

我们需要通过国际旅游卡去升级Open AI的使用级别,这一步是需要付费的哦。

首先我们要去访问bewildcard。

1、点击登录,直接用国内的号码即可登录。这里无需多说什么咯~
Description
2、我的卡片——选择一年——支付。后面的步骤就不详细展示了,基本按着提示去操作就OK了。大家选择ZFB认证会比较顺畅一些。
Description
在这个界面里按着图片步骤走即可,中英文的显示都一样。
Description
3、当你将卡片注册成功后,在“我的卡片”里会显示你的卡片信息了,如下图:
Description
4、此时你的卡片中余额是0,Wild Card 最低起步是10美元~ 所以各位老爷都懂的~ 含泪付吧…舍不得余额套不着API……
Description
5、返回Open AI官网,绑定Open AI的账户,升级使用级别。
Description
6、选择个人个体账户。
Description
7、填写详细信息。在这里要注意这个CVC,是你卡片的安全码,记得填上,是需要验证的。按着旅游卡的卡片信息去填写下面的即可。
Description
OKK,一切都大功告成!!!当你将账户“使用级别”升级成功后,可以开开心心的去对接API进行使用了。一下是使用级别1的权限了,各位老板要详细阅读哦~
Description
Description
以下是我做测试时用的真实数据,如果大家想长期使用的话,建议每个月定期给国际旅游卡(WildCaard)充上10美元,避免断粮导致的各种报错~

往往我们会最容易忽略这一点~ 然后开始在程序里疯狂找报错原因,哈哈哈~
Description
好了,本次的教程到此结束了。如果在这过程中还有什么不懂的,可以来与我交流哦。

最后祝各位老板,身体健康,工作顺利!拜拜咯~

收起阅读 »

关于“明显没有bug的代码”的一些拙见

以前听过过一个有趣的说法:不要编写没有明显bug的代码,要去编写明显没有bug的代码。这里提到的两个概念:“没有明显bug的代码”和“明显没有bug的代码”。同样的文字,只是调换了下顺序,表达的就是完全不同的概念了。前者“没有明显bug的代码”大概是最常见的代...
继续阅读 »

以前听过过一个有趣的说法:不要编写没有明显bug的代码,要去编写明显没有bug的代码。这里提到的两个概念:“没有明显bug的代码”和“明显没有bug的代码”。同样的文字,只是调换了下顺序,表达的就是完全不同的概念了。前者“没有明显bug的代码”大概是最常见的代码了,特征就是:



  1. 每段程序看起来合理,但结果就是不对

  2. 程序看起来复杂、奇怪,但就是可以正常运行

  3. 天书一般的程序

  4. 待补充


平时工作中到处缝缝补补的代码大概就是这种代码吧。背后的原因一般比较复杂,有时还不可追溯,项目工期紧,人员交接等等都有可能。因此,与其思考“如何避免没有明显bug的代码”,还不如思考“如何写出明显没有bug的代码”。本文就何为“明显没有bug的代码”总结一些个人的思的胡思乱想,阐述这类代码的几个特征。


特征1:代码简短


“明显没有xx”意味着一眼能看出来,而“一眼”这个条件就有很大的限制。如果给我一个函数,包含1000多行代码,我鼠标滚轮要滚好久,才能过完一遍代码,那么这种代码一定不是“明显没有bug的代码”。那么,反过来说,“明显没有bug的代码”一定是短小的代码。比如,Java中的Objects.equals方法:


public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}

这一段代码简短到,代码跟功能定义的文字篇幅差不多,连写文档注释的必要都没有了。还有一个更极端的例子是Java的Objects.isNull方法:


public static boolean isNull(Object obj) {
return obj == null;
}

简直就是一段“废话”。


特征2:功能完整且连贯


“一眼能看出”还意味着功能不能太分散,如果一个功能,分散在十多个函数或文件中,那么看这段功能就得在很多代码片段中跳来跳去,这个就需要开发者来阅读代码时充当一个“人肉解释器”的角色,在大脑中把各个代码片段组合起来才能明白整个流程和细节,这无疑降低阅读代码的效率,bug也容易隐藏在各个代码片段的“缝隙”中。举个常见的例子:在图形界面应用中,用户登录后,弹出登录成功的提示,然后关闭登录页面。一种普通的实现是:


//1. 在登录按钮触发登录操作
loginButton.setOnClickListener(v -> controller.login(username, poassword))

//2. 在登录成功的回调中展示弹窗
public void onLoginSuccess(User user){
LoginSuccessDialog.show("login success")
}

//3. 在失败的回调中展示错误信息
public void onLoginFailed(String errorMsg){
MessageDialog.showMessage(errorMsg);
}

//4. 在LoginSuccessDialog确认后关闭页面
public void onLoginDialogConfirmed(){
loginPage.close();
}

看上去好像解耦很不错,但功能都变得七零八落。要拼凑出完整的功能大概得仔细阅读整段代码,更别提“一眼看出”了。那么一眼能看出的代码大概长啥样呢?我想,大概是这样:


loginButton.setOnClickListener(v -> {
controller.login(username, poassword)
.onSuccess(user ->
LoginSuccessDialog.总而言之,解耦需谨慎,不要因过度解耦而牺牲了内聚性和连贯性。show("login success")
.onConfirmed(() -> loginPage.close()))
.onFailed(errorMsg ->
MessageDialog.showMessage(errorMsg));
});

这是对该视图流程的一个连贯的描述,而且篇幅更短。至于获取到用户数据存储到本地数据库、通知其他页面更新等操作,跟当前视图没有关系,也就不需要放在这段代码里。总而言之,解耦需谨慎,不要因过度解耦而牺牲了内聚性和连贯性。


特征3:良好的表达


代码的篇幅得到控制后,要让人一眼看懂,还需要容易理解才行。设计心理学提出“设计传达所有必要的信息,创造一个良好的系统概念模型,引导用户理解系统状态,带来掌控感”。程序设计也是如此,代码是程序功能的文本表达,需要传达对应信息来让人产生该功能正确的概念模型。


以一个常见的上传图片的弹窗为例,思考一个菜单弹窗,包含取消和两个功能按钮:从相册选择和拍照上传,例如下图这样。


在这里插入图片描述


那么对应的代码可以表达为:


MenuDialog.create()
.withAction("拍照上传", dialog -> {
takePhoto();
})
.withAction("本地上传", dialog -> {
chooseFromGallery();
})
.onCancel(() -> {
//do something
})
.show();

或者


MenuDialog.create()
.withAction("拍照上传", controller::takePhoto)
.withAction("本地上传", controller::chooseFromGallery)
.onCancel(() -> {
//do something
})
.show();

没有多余的代码,该有的信息都表达到位,而且和实际功能有良好的对应关系。


4. 特征4:可验证正确性


代码可以让人一眼看懂之后,那么判断其有没有bug,还有一个重要前提:这个代码是有正确性可言的,可以被验证。


例如,来看下面这段随意的代码:


int type;
boolean isClosed;

void doSomething(String text) {
if (type == 0) {
if (isClosed) {
println(text);
} else {
error("something wrong");
}
} else if (type == 1) {
//do something
}
}

这段程序简短、易读,但是doSomething函数的行为依赖两个外部变量,而这两个外部变量又容易被其他地方随意改动。比如,type的定义域为1、2、3,但如果type新增类型4的时候或者被错误地赋值为-1的时候,这个doSomething函数的行为还是正确的吗?doSomething函数的正确性依赖于type变量的正确性,那么又依赖于读写type变量的程序的正确性,这样的程序是难以验证的。而且,对上下文依赖越多的程序,越难以产生明确的定义,因为这个定义也依赖上下文的定义。定义不明确,更难以验证内容的正确性。


相比之下,Objects.equalsObjects.isNull方法有着明确的定义,而且不受上下文影响,可以一眼就看出对错。而下面这段代码:


MenuDialog.create()
.withAction("拍照上传", controller::takePhoto)
.withAction("本地上传", controller::chooseFromGallery)
.onCancel(() -> {
//do something
})
.show();

表达明确,可以快速判断出程序行为是否正确、符合期望。即便MenuDialog出现异常,或者takePhotochooseFromGallery出了什么问题,也不需要来修改这段程序。


不过,程序验证是一个有点高深的科研方向,要严格验证一个程序的正确性是很困难的一件事,不过我们仍然可以试着去编写一些“看起来”正确的程序。(利用函数式编程思想写出来的代码通常容易验证一些)


作者:乐征skyline
来源:juejin.cn/post/7236010330051887164
收起阅读 »

程序员黑话之故障专辑(中英文对照版)

正好最近业内接连发生了几起影响比较大的故障,那我们就专门做一期「故障专辑」吧。 故障 故障有好几种叫法,比较正式的 故障 - Outage 事故 - Incident 不怎么严重的,时间很短的 抖了一下 - Jitter(多用于网络) Hiccup (中...
继续阅读 »

正好最近业内接连发生了几起影响比较大的故障,那我们就专门做一期「故障专辑」吧。


故障


故障有好几种叫法,比较正式的



  • 故障 - Outage

  • 事故 - Incident


不怎么严重的,时间很短的



  • 抖了一下 - Jitter(多用于网络)

  • Hiccup (中文翻译是打了个嗝,不过中文里貌似没有这个讲法)


通俗点的说法



  • 挂了/崩了- Down
    file


500


当在请求某个网络资源时,服务器内部发生错误时,返回的错误编号。扩展为系统发生内部故障。


file


变更


虽然突然的流量暴涨,或者光缆被挖断,数据中心着火,被雷劈都有可能,但绝大多数时候,故障都是变更导致的。


file


变更分为三大类:



  • 代码变更 - Code Change

  • 配置变更 - Config Change

  • 数据库变更 - Database Change


左移 (shift-left)


降低变更风险的一个方法,就是做变更前检查,问题越早发现越好。因为变更的流水线是从左往右画的,起点在左边。所以左移就是把检查尽量靠近起点。


金丝雀 (Canary)


以前矿工下井,会带一只金丝雀,如果井下空气出现状况,更敏感的金丝雀会先有异常。这个概念也带到了软件研发里。会循序渐进地做变更。另外一种叫法是灰度 (Grayscale)。


file


单元化/区域化 (Regionalization)


在互联网公司逐渐普及的架构,主要由 AWS 发扬光大,把服务进行隔离。


爆炸半径 (Blast Radius)


金丝雀和单元化都是为了降低爆炸半径,减少故障的影响面。


file


值班 (On-call)


也叫 Carry the pager。以前带着的传呼机叫做 Pager。现在传呼机被手机/软件取代了,但 Pager 这个名字沿用了下来。


file


复盘 (Postmortem)


原义是尸检报告。在软件研发领域,指详细的故障分析报告。


惊群 (Thundering Herd)


file


打雷后,动物一下子被惊醒了,到处乱窜,造成混乱。在故障恢复阶段要小心的问题,很容易刚拉起一个服务,立马又被积压的请求打挂。


结语


船停在港口是最安全的,但那不是造船的目的。软件需要持续的变更迭代,变更就有风险。但研发团队可以通过引入工具,来降低风险,针对一开始变更的三种类型,市面上也有成熟的开源方案:
代码变更 - 老牌的有 Jenkins,新兴的有 Drone CI 和 Zadig




作者:Bytebase
来源:juejin.cn/post/7301244964297670693
收起阅读 »

室友打一把王者就学会了Java多线程

大家好,我是二哥呀。 对于 Java 初学者来说,多线程的很多概念听起来就很难理解。比方说: 进程,是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。 线程,是进程的子任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发...
继续阅读 »


大家好,我是二哥呀。


对于 Java 初学者来说,多线程的很多概念听起来就很难理解。比方说:



  • 进程,是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。

  • 线程,是进程的子任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发。


很抽象,对不对?打个比喻,你在打一把王者(其实我不会玩哈 doge):



  • 进程可以比作是你开的这一把游戏

  • 线程可以比作是你所选的英雄或者是游戏中的水晶野怪等之类的。


带着这个比喻来理解进程和线程的一些关系,一个进程可以有多个线程就叫多线程。是不是感觉非常好理解了?


进程和线程


❤1、线程在进程下进行


(单独的英雄角色、野怪、小兵肯定不能运行)


❤2、进程之间不会相互影响,主线程结束将会导致整个进程结束


(两把游戏之间不会有联系和影响。你的水晶被推掉,你这把游戏就结束了)


❤3、不同的进程数据很难共享


(两把游戏之间很难有联系,有联系的情况比如上把的敌人这把又匹配到了)


❤4、同进程下的不同线程之间数据很容易共享


(你开的那一把游戏,你可以看到每个玩家的状态——生死,也可以看到每个玩家的出装等等)


❤5、进程使用内存地址可以限定使用量


(开的房间模式,决定了你可以设置有多少人进,当房间满了后,其他人就进不去了,除非有人退出房间,其他人才能进)


创建线程的三种方式


搞清楚上面这些概念之后,我们来看一下多线程创建的三种方式:


继承 Thread 类


♠①:创建一个类继承 Thread 类,并重写 run 方法。


public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ":打了" + i + "个小兵");
}
}
}

我们来写个测试方法验证下:


//创建MyThread对象
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
//设置线程的名字
t1.setName("鲁班");
t2.setName("刘备");
t3.setName("亚瑟");
//启动线程
t1.start();
t2.start();
t3.start();

来看一下执行后的结果:



实现 Runnable 接口


♠②:创建一个类实现 Runnable 接口,并重写 run 方法。


public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {//sleep会发生异常要显示处理
Thread.sleep(20);//暂停20毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "打了:" + i + "个小兵");
}
}
}

我们来写个测试方法验证下:


//创建MyRunnable类
MyRunnable mr = new MyRunnable();
//创建Thread类的有参构造,并设置线程名
Thread t1 = new Thread(mr, "张飞");
Thread t2 = new Thread(mr, "貂蝉");
Thread t3 = new Thread(mr, "吕布");
//启动线程
t1.start();
t2.start();
t3.start();

来看一下执行后的结果:



实现 Callable 接口


♠③:实现 Callable 接口,重写 call 方法,这种方式可以通过 FutureTask 获取任务执行的返回值。


public class CallerTask implements Callable<String> {
public String call() throws Exception {
return "Hello,i am running!";
}

public static void main(String[] args) {
//创建异步任务
FutureTask<String> task=new FutureTask<String>(new CallerTask());
//启动线程
new Thread(task).start();
try {
//等待执行完成,并获取返回结果
String result=task.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}

关于线程的一些疑问


❤1、为什么要重写 run 方法?


这是因为默认的run()方法不会做任何事情。


为了让线程执行一些实际的任务,我们需要提供自己的run()方法实现,这就需要重写run()方法。


public class MyThread extends Thread {
public void run() {
System.out.println("MyThread running");
}
}

在这个例子中,我们重写了run()方法,使其打印出一条消息。当我们创建并启动这个线程的实例时,它就会打印出这条消息。


❤2、run 方法和 start 方法有什么区别?



  • run():封装线程执行的代码,直接调用相当于调用普通方法。

  • start():启动线程,然后由 JVM 调用此线程的 run() 方法。


❤3、通过继承 Thread 的方法和实现 Runnable 接口的方式创建多线程,哪个好?


实现 Runable 接口好,原因有两个:



  • ♠①、避免了 Java 单继承的局限性,Java 不支持多重继承,因此如果我们的类已经继承了另一个类,就不能再继承 Thread 类了。

  • ♠②、适合多个相同的程序代码去处理同一资源的情况,把线程、代码和数据有效的分离,更符合面向对象的设计思想。Callable 接口与 Runnable 非常相似,但可以返回一个结果。


控制线程的其他方法


针对线程控制,大家还会遇到 3 个常见的方法,我们来一一介绍下。


1)sleep()


使当前正在执行的线程暂停指定的毫秒数,也就是进入休眠的状态。


需要注意的是,sleep 的时候要对异常进行处理。


try {//sleep会发生异常要显示处理
Thread.sleep(20);//暂停20毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}

2)join()


等待这个线程执行完才会轮到后续线程得到 cpu 的执行权,使用这个也要捕获异常。


//创建MyRunnable类
MyRunnable mr = new MyRunnable();
//创建Thread类的有参构造,并设置线程名
Thread t1 = new Thread(mr, "张飞");
Thread t2 = new Thread(mr, "貂蝉");
Thread t3 = new Thread(mr, "吕布");
//启动线程
t1.start();
try {
t1.join(); //等待t1执行完才会轮到t2,t3抢
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
t3.start();

来看一下执行后的结果:



3)setDaemon()


将此线程标记为守护线程,准确来说,就是服务其他的线程,像 Java 中的垃圾回收线程,就是典型的守护线程。


//创建MyRunnable类
MyRunnable mr = new MyRunnable();
//创建Thread类的有参构造,并设置线程名
Thread t1 = new Thread(mr, "张飞");
Thread t2 = new Thread(mr, "貂蝉");
Thread t3 = new Thread(mr, "吕布");

t1.setDaemon(true);
t2.setDaemon(true);

//启动线程
t1.start();
t2.start();
t3.start();

如果其他线程都执行完毕,main 方法(主线程)也执行完毕,JVM 就会退出,也就是停止运行。如果 JVM 都停止运行了,守护线程自然也就停止了。


小结


本文主要介绍了 Java 多线程的创建方式,以及线程的一些常用方法。最后再来看一下线程的生命周期吧,一图胜千言。



好了,如果你想学好 Java,GitHub 上标星 10000+ 的《二哥的 Java 进阶之路》不容错过,据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发(Git、Nginx、Maven、Intellij IDEA、Spring、Spring Boot、Redis、MySql 等等)、Java 面试等核心知识点。学 Java,就认准二哥的 Java 进阶之路😄。


Github 仓库:github.com/itwanger/to…


码云仓库(国内访问更快):gitee.com/itwanger/to…


star 了这个仓库就等于你拥有了成为了一名优秀 Java 工程师的潜力。



把二哥的座右铭送给你:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟


作者:沉默王二
来源:juejin.cn/post/7329413905028186124
收起阅读 »

你还以为前端无法操作文件吗

web
这里面有个值得说明一点的问题是,我一直以为(可能有人跟我一样)前端是无法操作文件的,可实际上自从HTML5标准出现之后,前端虽然无法像后端那样能灵活的进行文件处理,但因为有了File System Api这套接口,前端也能够进行简单的文件处理操作(不只是读,还...
继续阅读 »

这里面有个值得说明一点的问题是,我一直以为(可能有人跟我一样)前端是无法操作文件的,可实际上自从HTML5标准出现之后,前端虽然无法像后端那样能灵活的进行文件处理,但因为有了File System Api这套接口,前端也能够进行简单的文件处理操作(不只是读,还有写)。当然,网络环境鱼龙混杂,为防止不法网站任意获取和修改用户数据,所有本地文件操作都需要用户手动操作,不能自动保存或打开。

  1. 使用场景

    File System Api为浏览器应用增加了无限可能,比如我们经常用到的一些流程图工具,上面的保存到本地的功能,就不用再依赖后端,可以直接将数据保存到本地的文件系统中,下次打开时选中本地的指定文件,可以直接加载到浏览器中,大大提高的前端的能力边界。

  2. 功能描述

    我们就利用File Access Api搞一个简单的在线编辑器,能实现的功能如下:

    第一步,新建一个文件,命名为hello.txt,并填写初始信息 "hello world"

    第二步,打开文件,修改文件内容为“hello world,hello you!”

    第三步,保存文件

editfile.gif

  1. 实现方式概述

    直接看代码:

    <template>
     <div>
       <el-button type="primary" @click="editFile">编辑文件el-button>
       <el-button type="primary" @click="saveFile">保存文件el-button>
       <el-input
           type="textarea"
           :rows="20"
           placeholder="请输入内容"
           v-model="textarea">
    el-input>
     
     div>
    template>

    <script>
    export default {
       data() {
           return {
               textarea: ''
          }
      },
       methods: {
           editFile: async function() {
               // 选择文件
               let [fileHandle] = await window.showOpenFilePicker()
               // 复显文件内容
               fileHandle.getFile().then(blob => {
                   blob.text().then(val => {
                       this.textarea = val
                  })
              })
          },
           saveFile: async function() {
               // 新建一个文件
               const fileHandle = await window.showSaveFilePicker({
                   types: [
                      {
                           description: 'hello',
                           accept: {
                               'text/plain': ['.txt']
      // 对于一些非常用的后缀,均使用这种方式进行定义
                               // 参考:https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
                               // 'application/octet-stream': ['.a','.b']
                          }
                      }
                  ]
              })
               // 在文件内写入内容,写入内容用的是Stream Api,流式写入
               const writable = await fileHandle.createWritable();
               await writable.write(this.textarea);
               await writable.close();
          }
      }
    }
    script>

    可以看到,只需要短短的几行代码就可以完成本地文件的修改,需要注意的是,文件的保存不是实际意义上的修改,而是新建一个文件,进行替换,然后在新的文件里写入最新信息进行的修改。

    另:File System Api目前支持程度还不够普遍,从mdn上来看,大多数api上还有Experimental: This is an experimental technology Check the Browser compatibility table carefully before using this in production.的描述,使用前需要确认好是否满足浏览器要求。


作者:DaEar图图
来源:juejin.cn/post/7365679089811947561
收起阅读 »

眼看他搭中台,眼看他又拆了

曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之际,我们可以发现各大企业又在反其道而行,纷纷不断进行“拆中台”,那么中台对于企业而言,究竟发...
继续阅读 »

曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之际,我们可以发现各大企业又在反其道而行,纷纷不断进行“拆中台”,那么中台对于企业而言,究竟发挥了哪些作用,当前又出现了哪些问题?今天,我们特邀了高级研发管理专家、腾讯云 TVP 程超老师,他将从搭中台到拆中台的风向转变,探讨企业软件架构的底层逻辑。



中台都在忽悠吗?都被忽悠瘸了?我们都在悄悄淘汰中台,你们还在建?最近网上充斥大量文章和观点,都在说中台过时。为什么会这样说?是因为成本与复杂性?技术限制与业务变化?还是因为组织变化?为什么会这样呢?且听我一一分析。


众所周知,中台是指企业内部的中间层平台,负责连接上下游系统,提供数据和功能服务。而在过去几年中台概念曾经风靡一时,甚至被认为是企业数字化转型的关键。然而,近年来,一些企业确实出现了对中台战略的重新评估,不再像之前那样盲目地追求中台建设。其实,中台的概念兴起于企业数字化转型的浪潮中,企业开始意识到传统的前台系统(如客户端应用)与后台系统(如企业资源规划系统)之间的断层,而中台则被认为是弥合这种断层的理想方式。


值得一提的是,关于中台的定义,业内大佬也曾经发表过一些观点:


提炼各个业务条线的共性需求,并将这些打造成组件化的资源/能力包,然后以接口的形式提供给前台各业务部门使用,这样就可以最大限度地避免“重复造轮子”的问题,也让每一个新的前台业务创新能够真正意义上“站在巨人的肩膀上”,而不用每次开辟一个新业务都像新建一家创业公司那么艰难,甚或更为艰难。——某企业资深架构师 钟华


总结而言,中台的核心点主要有以下三个:



  • 中台是为前台而生。

  • 提炼各业务条线的共性需求。

  • 减少“重复造轮子”的时间与资源浪费。


01四大层面解读中台备受追捧原因


2015年,业界首次提出“大中台、小前台”战略,是想打造统一技术架构、产品支撑体系、数据共享平台、安全体系等等,把整个组织“横”过来,支撑多种多样的业务形态。中台似乎已经成为行业标配,稍有规模的公司都建设了自己的中台,掀起了一股强劲的中台风。


中台能够解决哪些问题呢?在我看来,主要有以下四种:



  • 项目重复造轮子严重,无法形成抽象共用


中台提供了一种在企业内部建立统一的技术平台或者服务平台的模式。这个平台可以被不同部门或者项目共享和复用,从而减少了重复开发的情况。随着新业务的不断接入,共享服务也从仅能提供单一的业务功能,不断的自我进化成更健壮更强大的服务,不断适应各种业务线的新需求。同时在数据积累方面,通过数据中台将各业务的数据都沉淀下来,不断地积累数据,发挥数据的最大威力。



  • 业务变化快,缓慢的研发流程难以迅速响应


很多企业开发响应慢,其实大部分都是因为数据问题,没有做到实时、准确和统一。比如一家公司的订单,分为 C 端订单,B 端订单,共享单车订单等等,这些订单分管在不同部门中,想要做订单统计、预测等就比较困难,各类型订单彼此割裂,而如果企业只有一个订单中心的话,数据就能够在不同场景下感知到业务的变化和联动。



  • 提高资源利用率和研发效率



说起如何提高资源利用率和研发效率,我总结为中台建设五步法:插件化、服务化、配置化、异步化和数据化。这五步环环相扣,其中插件化就是提高研发效率的关键点,我们将对核心交易流程进行抽象建模设计,并通过流程引擎的改造,实现增加多个插件和扩展点。这样,不同的业务场景可以根据需求自定义其个性化逻辑,将整个交易环节抽象为一个流程框架,并在其基础上引入一系列业务扩展。这种设计使得各业务间互不干扰,更灵活地满足各自需求。


提高资源利用率,这也是必然的,服务、数据、组件等形成统一复用,各资源也不再分散,只需通过一套服务来做支撑,并且可以通过各业务线的忙闲情况,做资源的调控、比如某个业务线使用交易中台服务,高峰时期是在早上8点到晚上12点,凌晨以后基本没有业务量,则可以考虑把针对这个业务线的资源配置降低,从而实现降本增效。



  • 提高系统稳定性和可靠性


一般来说系统的故障由三个方面引起,系统 bug、变更配置、并发流量变化。而技术中台避免了各个部门为解决自身技术问题而随意修改系统设置和配置的情况,这样做有助于防止整个系统因为随意修改而出现不稳定和安全问题。


02拆分中台并非全盘否定中台


前面我主要介绍了中台能解决哪些问题,但其实很多企业在实际引入中台的过程中,也遇到了很多问题:



  • 中台与前台的边界模糊


很多前台的业务让中台接管开发,到底是接还是不接?中台的角色和范围缺乏明确界定,导致中台与业务之间的责任划分模糊不清,引发了重复建设、资源浪费和沟通成本等问题。



  • 稳定性与灵活性的冲突


稳定与灵活一直是个矛盾体,中台接入的业务线非常多,一旦出问题影响面巨大,代码质量如何把控、上线流程如何稳定、业务如何做好隔离,都需要考虑清楚。



  • 沟通障碍与目标差异


协调中台团队和业务团队之间的沟通和合作,平衡双方的需求和利益,以及处理中台和业务之间的依赖和变更,都是一项复杂的管理任务。



  • 中台规划与业务需求之间的平衡


中台的服务需求和响应之间存在不匹配,这导致中台无法满足业务的多样化和个性化需求。有时中台过度迎合业务的短期需求,却牺牲了其长期规划和可持续发展。



  • 利益分配


距离业务近的地方,比距离业务远的地方更能得到公司增长的成果,中台看似业务,其实只是沉淀,追求的是稳定和灵活。还有业务下沉的时候,会涉及到与中台的业务交接,前台业务必定会减少。如果是部门划到中台,是否会有人员变动?当中台的服务价值和收益缺乏清晰界定,将难以有效衡量自身的贡献和影响。


综上,中台看似很美好,但很多企业在实际落地的时候却因为遇到这些问题,导致陷入困境,中台建设越建越复杂,甚至有些企业对中台也逐渐失去了信心,反而成了阻碍企业发展的瓶颈。


近两年业界开始风行“拆中台”策略——将中台变“薄”,拆分到多个独立的业务单元。这使得很多企业又开始认为中台已成明日黄花,引进中台并不是一个好选择,甚至有些企业将自身发展不顺的原因也归在了中台上面,一时间中台被全盘否定了。


我个人则认为拆分中台并非全盘否定中台,而是基于自身发展阶段和市场环境的变化进行战略调整和优化。“天下大事,合久必分,分久必合”,这就意味着在中台的管理和战略中,必须根据具体情况来做出分合的决策。有时候,将中台进行分散管理或者分解成更小的部分可能更为合适,因为这样有助于更好地满足各个业务单位的需求,提高灵活性和适应性。互联网大厂们将庞大而僵化的共享中台重新组织为灵活的业务域中台,可以更好适应具体业务场景和用户需求,既能保留中台提供通用能力和协同效率的优势,又能增加中台的灵活性和个性化。


03企业应该因地制宜选择是否需要中台


首先,我想强调的是,“中台”本身并不是一个新的架构思想,这个架构思想早在若干年以前就已经有了,很多企业已经是这么做了,就像面向对象编程语言中(Java)高内聚,低耦合,便是这种思想。


当企业处在初创期,随着业务发展产生多条业务线或产品线的时候,就会面临协同方面的挑战,如果每条业务线都要自己成立技术、运维、数据等部门,这样显然是非常浪费人力和资源的。为了适应快速发展的业务,就需要成立中台部门,来抽取、复用共性的东西,形成统一,这样既能满足“小前台,大中台”策略,让业务快跑抢占市场,中台提供稳定的炮火支援,又能提高协同和研发效率。参考示意图如下:



当企业已经渡过初创期,发展已经具有较大规模时,各条业务线人员和业务场景也比初创时更加庞大和复杂,企业了将面临更加多样化的市场,以及强大的响应能力,甚至每条业务线都要独立去创新,这样统一的中台部门就会变成瓶颈,人员、响应时间、需求变化和沟通等都会成为阻碍多样化需求的绊脚石。这时候企业就需要根据市场需要,将庞大而僵化的大共享中台,拆分到各业务单元中,将中台下沉到各业务单元中,这样既能保留中台的通用和协同能力,又能针对具体业务和场景不断增加灵活性和定制性。参考示意图如下:



总而言之,中台不是一直不变的,它需要根据市场需求不断进化,演变成能够满足当前企业市场需要的形态。中台不是万能的,它只是企业数字化转型的一种重要实现路径,我们不能对中台有过高的期望,而是应该理性地回归到企业数字化转型的价值上来。


作者简介


程超,腾讯云 TVP,高级研发管理专家,14年 Java 研发经验,8年技术管理和架构经验,曾任京东架构师,易宝支付和松果出行架构技术负责人,熟悉支付和电商领域,擅长微服务生态建设和运维监控,对 Dubbo、Spring Cloud 和 gRPC 等微服务框架有深入研究,并应用于项目,帮助过多家公司进行过微服务建设和改造,目前正在建设业务中台。 合著作品《深入分布式缓存》和《高可用可伸缩微服务架构》,极客时间每日一课讲师和出品人,CSDN 博主专家。


作者:腾讯云开发者
来源:juejin.cn/post/7366175769602932755
收起阅读 »

如果按代码量算工资,也许应该这样写

前言 假如有一天我们要按代码量来算工资,那怎样才能写出一手漂亮的代码,同时兼顾代码行数和实际意义呢? 要在增加代码量的同时提高代码质量和可维护性,能否做到呢? 答案当然是可以,这可难不倒我们这种摸鱼高手。 耐心看完,你一定有所收获。 正文 1. 实现更多的...
继续阅读 »

前言


假如有一天我们要按代码量来算工资,那怎样才能写出一手漂亮的代码,同时兼顾代码行数和实际意义呢?


要在增加代码量的同时提高代码质量和可维护性,能否做到呢?


答案当然是可以,这可难不倒我们这种摸鱼高手。


耐心看完,你一定有所收获。


giphy.gif


正文


1. 实现更多的接口:


给每一个方法都实现各种“无关痛痒”的接口,比如SerializableCloneable等,真正做到不影响使用的同时增加了相当数量的代码。


为了这些代码量,其中带来的性能损耗当然是可以忽略的。


public class ExampleClass implements Serializable, Comparable<ExampleClass>, Cloneable, AutoCloseable {

@Override
public int compareTo(ExampleClass other) {
// 比较逻辑
return 0;
}

// 实现 Serializable 接口的方法
private void writeObject(ObjectOutputStream out) throws IOException {
// 序列化逻辑
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 反序列化逻辑
}

// 实现 Cloneable 接口的方法
@Override
public ExampleClass clone() throws CloneNotSupportedException {
// 复制对象逻辑
return (ExampleClass) super.clone();
}

// 实现 AutoCloseable 接口的方法
@Override
public void close() throws Exception {
// 关闭资源逻辑
}

}


除了示例中的SerializableComparableCloneableAutoCloseable,还有Iterable


2. 重写 equals 和 hashcode 方法


重写 equalshashCode 方法绝对是上上策,不仅增加了代码量,还为了让对象在相等性判断和散列存储时能更完美的工作,确保代码在处理对象相等性时更准确、更符合业务逻辑。


public class ExampleClass {
private String name;
private int age;

// 重写 equals 方法
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}

if (obj == null || getClass() != obj.getClass()) {
return false;
}

ExampleClass other = (ExampleClass) obj;
return this.age == other.age && Objects.equals(this.name, other.name);
}

// 重写 hashCode 方法
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}


giphy (2).gif


3. 增加配置项和参数:


不要管能不能用上,梭哈就完了,问就是为了健壮性和拓展性。


public class AppConfig {
private int maxConnections;
private String serverUrl;
private boolean enableFeatureX;

// 新增配置项
private String emailTemplate;
private int maxRetries;
private boolean enableFeatureY;

// 写上构造函数和getter/setter
}

4. 增加监听回调:


给业务代码增加监听回调,比如执行前、执行中、执行后等各种Event,这里举个完整的例子。


比如创建个 EventListener ,负责监听特定类型的事件,事件源则是产生事件的对象。通过EventListener 在代码中增加执行前、执行中和执行后的事件。


首先,我们定义一个简单的事件类 Event


public class Event {
private String name;

public Event(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

然后,我们定义一个监听器接口 EventListener


public interface EventListener {
void onEventStart(Event event);

void onEventInProgress(Event event);

void onEventEnd(Event event);
}

接下来,我们定义一个事件源类 EventSource,在执行某个业务方法时,触发事件通知:


public class EventSource {
private List<EventListener> listeners = new ArrayList<>();

public void addEventListener(EventListener listener) {
listeners.add(listener);
}

public void removeEventListener(EventListener listener) {
listeners.remove(listener);
}

public void businessMethod() {
Event event = new Event("BusinessEvent");

// 通知监听器:执行前事件
for (EventListener listener : listeners) {
listener.onEventStart(event);
}

// 模拟执行业务逻辑
System.out.println("Executing business method...");

// 通知监听器:执行中事件
for (EventListener listener : listeners) {
listener.onEventInProgress(event);
}

// 模拟执行业务逻辑
System.out.println("Continuing business method...");

// 通知监听器:执行后事件
for (EventListener listener : listeners) {
listener.onEventEnd(event);
}
}
}

现在,我们可以实现具体的监听器类,比如 BusinessEventListener,并在其中定义事件处理逻辑:


public class BusinessEventListener implements EventListener {
@Override
public void onEventStart(Event event) {
System.out.println("Event Start: " + event.getName());
}

@Override
public void onEventInProgress(Event event) {
System.out.println("Event In Progress: " + event.getName());
}

@Override
public void onEventEnd(Event event) {
System.out.println("Event End: " + event.getName());
}
}

最后,我们写个main函数来演示监听事件:


public class Main {
public static void main(String[] args) {
EventSource eventSource = new EventSource();
eventSource.addEventListener(new BusinessEventListener());

// 执行业务代码,并触发事件通知
eventSource.businessMethod();

// 移除监听器
eventSource.removeEventListener(businessEventListener);
}
}

如此这般那般,代码量猛增,还顺带实现了业务代码的流程监听。当然这只是最简陋的实现,真实环境肯定要比这个复杂的多。


5. 构建通用工具类:


同样的,甭管用不用的上,定义更多的方法,都是为了健壮性。


比如下面这个StringUtils,可以从ApacheCommons、SpringBoot的StringUtil或HuTool的StrUtil中拷贝更多的代码过来,美其名曰内部工具类。


public class StringUtils {
public static boolean isEmpty(String str) {
return str == null || str.trim().isEmpty();
}

public static boolean isBlank(String str) {
return str == null || str.trim().isEmpty();
}

// 新增方法:将字符串反转
public static String reverse(String str) {
if (str == null) {
return null;
}
return new StringBuilder(str).reverse().toString();
}

// 新增方法:判断字符串是否为整数
public static boolean isInteger(String str) {
try {
Integer.parseInt(str);
return true;
} catch (NumberFormatException e) {
return false;
}
}
}

6. 添加新的异常类型:


添加更多异常类型,对不同的业务抛出不同的异常,每种异常都要单独去处理


public class CustomException extends RuntimeException {
// 构造函数
public CustomException(String message) {
super(message);
}

// 新增异常类型
public static class NotFoundException extends CustomException {
public NotFoundException(String message) {
super(message);
}
}

public static class ValidationException extends CustomException {
public ValidationException(String message) {
super(message);
}
}
}

// 示例:添加不同类型的异常处理
public class ExceptionHandling {
public void process(int value) {
try {
if (value < 0) {
throw new IllegalArgumentException("Value cannot be negative");
} else if (value == 0) {
throw new ArithmeticException("Value cannot be zero");
} else {
// 正常处理逻辑
}
} catch (IllegalArgumentException e) {
// 异常处理逻辑
} catch (ArithmeticException e) {
// 异常处理逻辑
}
}
}


7. 实现更多设计模式:


在项目中运用更多设计模式,也不失为一种合理的方式,比如单例模式、工厂模式、策略模式、适配器模式等各种常用的设计模式。


比如下面这个单例,大大节省了内存空间,虽然它存在线程不安全等问题。


public class SingletonPattern {
// 单例模式
private static SingletonPattern instance;

private SingletonPattern() {
// 私有构造函数
}

public static SingletonPattern getInstance() {
if (instance == null) {
instance = new SingletonPattern();
}
return instance;
}

}

还有下面这个策略模式,能避免过多的if-else条件判断,降低代码的耦合性,代码的扩展和维护也变得更加容易。


// 策略接口
interface Strategy {
void doOperation(int num1, int num2);
}

// 具体策略实现类
class AdditionStrategy implements Strategy {
@Override
public void doOperation(int num1, int num2) {
int result = num1 + num2;
System.out.println("Addition result: " + result);
}
}

class SubtractionStrategy implements Strategy {
@Override
public void doOperation(int num1, int num2) {
int result = num1 - num2;
System.out.println("Subtraction result: " + result);
}
}

// 上下文类
class Context {
private Strategy strategy;

public Context(Strategy strategy) {
this.strategy = strategy;
}

public void executeStrategy(int num1, int num2) {
strategy.doOperation(num1, num2);
}
}

// 测试类
public class StrategyPattern {
public static void main(String[] args) {
int num1 = 10;
int num2 = 5;

// 使用加法策略
Context context = new Context(new AdditionStrategy());
context.executeStrategy(num1, num2);

// 使用减法策略
context = new Context(new SubtractionStrategy());
context.executeStrategy(num1, num2);
}
}

对比下面这段条件判断,高下立判。


public class Calculator {
public static void main(String[] args) {
int num1 = 10;
int num2 = 5;
String operation = "addition"; // 可以根据业务需求动态设置运算方式

if (operation.equals("addition")) {
int result = num1 + num2;
System.out.println("Addition result: " + result);
} else if (operation.equals("subtraction")) {
int result = num1 - num2;
System.out.println("Subtraction result: " + result);
} else if (operation.equals("multiplication")) {
int result = num1 * num2;
System.out.println("Multiplication result: " + result);
} else if (operation.equals("division")) {
int result = num1 / num2;
System.out.println("Division result: " + result);
} else {
System.out.println("Invalid operation");
}
}
}


8. 扩展注释和文档:


如果要增加代码量,写更多更全面的注释也不失为一种方式。


/**
* 这是一个示例类,用于展示增加代码数量的技巧和示例。
* 该类包含一个示例变量 value 和示例构造函数 ExampleClass(int value)。
* 通过示例方法 getValue() 和 setValue(int newValue),可以获取和设置 value 的值。
* 这些方法用于展示如何增加代码数量,但实际上并未实现实际的业务逻辑。
*/

public class ExampleClass {

// 示例变量
private int value;

/**
* 构造函数
*/

public ExampleClass(int value) {
this.value = value;
}

/**
* 获取示例变量 value 的值。
* @return 示范变量 value 的值
*/

public int getValue() {
return value;
}

/**
* 设置示例变量 value 的值。
* @param newValue 新的值,用于设置 value 的值。
*/

public void setValue(int newValue) {
this.value = newValue;
}
}

结语


哪怕是以代码量算工资,咱也得写出高质量的代码,合理合法合情的赚票子。


giphy (1).gif


作者:一只叫煤球的猫
来源:juejin.cn/post/7263760831052906552
收起阅读 »

一个30岁前端老社畜的人生经历

前言 在掘金多年,我一直是一个读者,从事前端快8年了,每天都在看一些视频和资料以及别人的日记,零零碎碎我也做过一些笔记,但是都不成体系。这些笔记至今留存在各种应用上,写了就再也没打开过,还是没有养成习惯,我希望能坚持下去,为自己的人生添加一点历史,等以后老了,...
继续阅读 »

前言


在掘金多年,我一直是一个读者,从事前端快8年了,每天都在看一些视频和资料以及别人的日记,零零碎碎我也做过一些笔记,但是都不成体系。这些笔记至今留存在各种应用上,写了就再也没打开过,还是没有养成习惯,我希望能坚持下去,为自己的人生添加一点历史,等以后老了,我还能证明我的青春有过一些记录,偶尔回味也会是一件比较幸福的事情。


近些年,感觉社会戾气挺重的,特别是疫情的时候,抖音里面的那些评论很让人糟心,现在年轻人也逐渐选择躺平,也是对社会的卷系妥协,随着经济的下滑,一般学校的研究生可能都很难找到一个比较ok的工作,更别提本科或者大专,作为学历真的拿不出手的我,更加焦虑。


从业前端快8年了,做过很多类型的项目,小到一般的H5展示网页,大到区块链应用、智能能源项目;其实回过头来看,没有什么大的成就感,我的从业经验只获得过一次奖杯,就是吃苦耐劳奖一个镀金的大手指,那还是我4年前在一家外包公司连续工作48小时做一个小程序上线后,老板看我确实辛苦,于是发了一个这个奖杯给我,后面被我娃摔坏了,就啥也没有了。


2023


2023年其实回头来看,收获并不是很大,归纳下来也没有几条:


  1. 今年非全研究生在读了

  2. 今年提交了入党申请

  3. 第二套房子装修完成

  4. 小孩来到了身边读书(之前在农村读幼儿园)

  5. 工作中学会了Vue3,能用Java做开发,同时更了解了业务方面的知识

  6. 开始了写作的习惯

  7. 跑了5场马拉松


2024 展望


2024年还有几天就到了,我希望每一年都能有点收获吧,立几个flag:


  1. 带妈妈旅游一次

  2. 成为党员积极分子

  3. 提高Java方面的基础知识,以及three 3D方面的能力

  4. 看不低于5本技术书籍,至少写30篇技术文章

  5. 还清自己的个人债务,当然不包含房贷

  6. 跑5场马拉松


行业展望


目前行业有些自媒体在唱衰,说前端已死,但是我觉得没这么悲观,国家多次强调往智能方向发展,各行业的智能得依托计算机才能智能,像什么智慧制造,智慧能源,智慧农村等等都需要计算机技术来运算和展示。前端只是比以前的要求会高一些了,在5,6年前对前端技术要求没有那么高,大家0基础都可以参与,但如今可能不行了,我觉得这是一个好事,要求高一点,薪资也会高一些。淘汰的,就是一开始就不适合这个行业的人。我目前在一家大型央企工作,还是算较为稳定,但同时也需要不断学习,因为或许某一天的淘汰人选就会是我,社会是残酷的,混日子终究不是一个好的方式。


个人真实经历分享


给大家分享一下我的个人真实经历,与君共勉。


我出生在一个普通的农村家庭,初中开始接触老虎机,高中接触网吧,17岁前没有出过县城,是个十足的井底之蛙,父亲几十年一直在外地工作,一年就回家两三次,从小就我妈妈一个人带我,她做装修的,每天早上7点就骑车外出上班了,下午5点回家,我在家做好饭菜等她回家吃了后,她就马上去田里土里种庄稼,喂了很多牲畜,高中毕业前一直都是这样(不过高中我住校,就我妈妈和我妹在家),我妈妈非常节约,从我记事起,每年只有过年她才会舍得给自己买一两件衣服,因为她觉得过年要穿新的衣服,寓意者新的一年有好的开始,从来没给自己买过首饰,也从来没有烫染过头发,也从来没有赌博过,但同时我父亲其实并不是有责任心的人,基本从不过问家里,以及我的学习。


我的学习打小从小就不好,学习生涯当了两个月的劳动委员,这就是我的荣耀,因为我觉得我小时就是sb,在干啥完全不知道,在学校就是为了吃那一顿饭,和同学天天玩,初中要毕业就被学校各种“”“好言相劝” 去中专学技术,学会拿高薪,实际上是为了赚中专学校的回扣。中专后面又把我们送去富士康,天天12个小时流水线,学校也是为了赚富士康的回扣,我的学业就是这样被卖来卖去,突然觉得有些可悲。这也是普遍读书不行的农村孩子的现状。


我的第一份工作是从2013年开始的,到现在已经差不多10年了,那就做一个时间线看看我的悲催往事吧,这也是我第一次对外讲


2013-2014.02


毕业季,和同学们坐着学校包的几辆大巴车,开到了成都郫县的富士康厂区,哪个时候富士康才在这里建厂,每天的工作就是搬东西,从另外一个地方往厂区里面搬,后面正式开工就开始了每天12个小时的白夜班交替,本来身体从小就弱,经常生病,在富士康就是上班,生病,加上富士康十三跳以及厂区经常出事,我和同学晚上提着东西,连夜翻墙走的,对,真是翻墙走的,后面线长给我打电话,我说我已经不在成都了。不过线长是我老乡,还是跟我没算旷工,算正常离职。 这里不得不说一句,在厂里,一个芝麻官都的官威都不得了,我实在看不惯,加上没前途,才走的。那时候天天12小时到手工资3500,自己上班赚钱还挺潇洒的,下班就去麻辣烫,一人吃饱全家不饿,和同学们啤酒小菜吃着,真是潇洒,厂区还有来自五湖四海的同龄妹子,都是中专生,还是挺快乐的,因为大家年龄相同,就是吃喝玩乐。自然半年才没存什么钱,灰溜溜回了老家,被我妈骂了一顿。


总结:富士康收获: 吃喝玩乐,此刻我的人生规划一无所知


2014.2-2015.07


回了老家,每天早上我妈6点就起来做饭、洗衣服、扫地等等,我起来烧柴,跟猪熬糠羹,喂猪,经常都是公鸡还没叫,我们都忙活一阵了,坚持了十天我就受不了了,因为我得承认,我出去工作后我变懒了,但是每天晚上很晚才睡,因为我在成都买了一个山寨的洛基亚手机,我开始在QQ聊天了,枯燥的生活我受不了,我要出去上班,我就去了重庆。就我妈和我妹两个人在家,这里我的说一下,我去重庆了,我妹才读幼儿园,我妈每天已经非常忙了,我妹从小就是邻居照看,她是位留守的老人,她每天给我妹妹吃好喝好的,比我奶奶好了太多,因为我妈性格很强势,和奶奶性格不合,我奶奶从来没照顾过我和我妹,都在伯伯家带他们的孩子,我妈妈经常晚上8.9点才从田里回家,我妹都是在邻居婆婆那儿吃饭睡觉,前几年她去世了,我妈妈哭了好久,因为她是我家的大贵人,现在每次走到她的坟前,我们都会去跪拜她。现在想起来,我妈太伟大了,她一生都是这么勤劳,吃苦。


到重庆了,上了一年多的厂,其实也是浑浑噩噩的,没有学历,只有在厂里做检验员,一个月2400的工资,入不敷出,因为当时听说主管也是中专学习,干了10多年才当主管,主管才5200一月工资,我觉得没前途,加上厂里玩的好的同事也走了,我也就走了


收获:C1驾-照, 成人高考专科录取通知书, iSO9O001,iSO14001 两个体系证书


2015.7-2016.7


这一年我就像做梦一样,2015年3月去学校报到,认识了班花我老婆,然后就开始交往,然后10月的时候,检查到怀孕了,过年就去了她的老家,因为怀孕了,也就准备结婚的事情了,同学们简直惊叹,纷纷问我怎么办到的?我才23岁,当父亲完全没概念,不过这也满足了我家人的愿望,穷人家里早当家,就在这一年,我妈妈存了一辈子的钱就被我花完了。10月检查怀孕,11月孩子她妈跟我父母說了要了买房买车的事情,我妈妈非常反对,后面我外婆对我妈说:你就这一个儿子,你都不帮他,以后他不恨你吗? 我妈妈想了几晚她咬咬牙还是同意了,过年去了女方家,她父母挺喜欢我的,我妈妈第二年年初付了房子首付26万,后面装修8万,买车8万,结婚7万,对没听错,全是我妈出的,她平时在农村做装修,有的时候包工,一个月7,8000有的时候包工一万多一个月工资,省吃俭用,全部存下的,都被我全部榨干了,好在岳父岳母没有要我一分彩礼,还给我2万块钱装修,他们也是农村人,也是吃了很多苦,2万得他们在工地干很久了,他们在老家为我们办了十多桌,请了一村的人来吃饭。


我妈后面才跟我说,这么多钱,我爸只出了一万块钱,我现在都不可思议,他在外面这么多年的钱去哪儿了? 但我也不恨他,毕竟每个人想法不同,他没有义务要给我出这些费用,不过好在之前房子一个平方8000,算是重庆比较贵的房子了,现在26000一个平方,算是赚了一些,有了一个家庭的财产保障,之前还要贵一点,现在房地产不行降了一些。


2016年7月后我出来也是误打误撞的进入了计算机这个行业,我之前压根就不了解这个行业,是看的招聘网站,招聘信息写的5000的工资,我那时候才3000多,在做销售,简直是高薪了,结果去了才知道,原来是计算机培训学校,耐不住那个美女姐姐各种软磨硬泡,我还是去学了计算机,当然,钱还是我妈跟我出的,因为也是孩子她妈跟我妈说这个行业好,比上厂强,我妈才听了她的,要是我说,那根本不管用。


收获:买房,结婚,买车,装修,好像所有的大事这段时间都基本完成了,虽然都是我妈出的钱


2016.7-2018.7


2016年10月孩子出生了,我也从培训学校出来工作了一段时间,培训机构学了4个月,时间都忙家里的事情去了,所以一毕业面试了20多家公司,都被打击了,每次都想放弃,但是回到家,看到家人,我都心里说不出的滋味,为此也哭了好多次,孩子她妈跟着我这些年,没买过一件超过300的衣服,全是淘宝的几十块一件的,我妈妈为了我在农村不管工作有多远,天气有多冷,都要去工作,我觉得我就是累赘,那时我24岁,我压力可能已经超出了我的极限,房贷3000,孩子每个月2000,车子和物业1000,还有生活费,每个月花销都要8000,有的时候孩子一生病就可能要一万以上,我后面找到一份工作4500,是切图仔,每天就jQuery,才稍微帮家里分担了一下,其实压力全部都在我妈妈哪儿,我妈妈为了我,操碎了不少心。


2018年我拿到了大专学历,然后随即开始了报名成人高考本科,孩子她妈就没有报名了,她觉得女孩子大专就够了,加上家里也没钱


收获:
1.当父亲了,压力更大了。我必须得成熟一点了,在前端行业算是正式入行了,通过自己每天工作之外,在各种QQ群里聊天拉业务,我的外快收入也逐渐多了起来,虽然很多时候工作到2点,但是总算是跟家庭减少一点压力,虽然期间换了3家工作,但是我的工资也高了一些,月薪到手9000了,加上外快时多时不多,一个月平均有个1.3的收入了;我也有一点点经济带家人去自驾游了(不过只有两次)
2.成人高考本科录取通知书


2018.7-2020.7


这一年通过我经常在QQ群聊天的好友介绍,我到了一家外包公司(他当时也拿了回扣,但是我也很感激他。因为他教我怎么面试,跟我出面试题),因为通过了客户的面试,我厚着脸皮开到了1.6一个月的工资。到手14k,我当时说我拿这么多,家里人都以为骗他们,不过等发第一个月工资的时候,他们觉得我以前选择计算机是对的,我妈妈也多了很多笑容,这个时候小孩也是大了,妈妈一个人带着孩子读幼儿园,我和孩子他妈在重庆上班,我妈也在上班,家庭算是好了起来,大家笑容也多了起来。当时加上的我的外快业务,一年也能赚个6,7万,因为大家知道我在做这块,后面一些朋友陆续的给我介绍,我也会给他们相对满足的回扣。平均一个月收入已经超过2万了,不过有点不厚道的是,我上班没事也在做外包。


收获:自己随着年龄的增加,人的心态也在发生变化,随着收入多了起来,脸上的笑容也多了,家庭矛盾也少了,日子也越来越有奔头了


2020.7-2023.10


2021年因为公司被客户从人力资源池给移除了,我们没有资格做客户的业务了,我随即也面临着失业,我28岁了,其实我还是很恐慌的,因为家庭开支这么大,加上我长期做外包,技术底子很薄弱,可能失业找不到这么高的工资,所以我很担忧,工作随便都是全日制本科起,我一个半罐子学历,能干点啥,但是后面客户对我的技术能力还有做事能力还算比较认可的,给我推荐了另外还在资源池的外包公司,但是我都不去,我觉得外包没有前途,同时他们也开不起16k的工资(虽然技术不咋的,但是现在是这工资,让我转到其他外包公司才13k,我也心有不甘啊),最后客户他们把我转进了客户内部,于是我一个中专生进入了体制内,不过在进入之前,各部长对我的学历还是有一定质疑,不过我的直系领导以自身名誉担保,我还是通过他们的几轮面试,最终成功进入。进入到体制内,身边的同事都是985的博士,研究生,还有都是留学回来的,也有一些清华北大北航的研究生,其实还是很自卑的,大家学历这么高,有的时候不得不承认,他们的专业素养,学术知识,脑回路都比较灵活,他们的英语都非常厉害,有的同事28岁都上中央台了,太强了,我妈妈非常担心我的学历,怕我一在公司一犯错就被开除,其实有的时候她还是多虑了,我也在尽力 的追赶他们,希望差距尽量小一点点。所以在2023年我拿到了非全的研究生录取通知书,继续读软件工程。在公司也申请入了党,因为他们全是党员,我在公司负责两个部门的前端管理工作,也在带一些校招的研究生,同时我也在2022年5月买了第二套房,首付42万,其他非要加上差不多47万,为此把车卖了一万五,凑点首付钱,我妈妈又出了26万,再一次吧我妈给榨干了,这次我爸也没有出一分钱。不过还在我是组合贷,每个月只出商业贷,每个月出差不多2000的房贷,第一套房贷也还有20多万就还完了,2023年我孩子读小学了,我妈妈也来重庆带孩子了,为此她没有继续在做装修了,每天接送小孩子上学放学,中间有两个小时去一家店里打扫卫生,每个月2500的收入。我在每个月给他2000多生活费,虽然她不太适应城市的生活,觉得城市的人会看不起她是农村人,走哪儿都不会用导航,但是她慢慢的还是习惯了,城市的人并没有觉得自己高人一等,她还算是过得比较快乐,现在我的收入在重庆来说还算OK,外快也有,但是我也不想太累了,我想把时间利用在学习上,因为同事们都很强,我尽量向他们看齐,


收获:本科毕-业-证,非全研究生录取通知书,稳定的工作,第二套房子首付+装修(因为旁边学校更好一点)


最后


今年30了,孩子已经7岁了,我已经开始享受十天的年假了。其实我已经算是走了很多路,深夜哭了很多次,第二天依旧怀揣着斗志,我数次回想我这30年的发展,其实都过得不是很灿烂,或许平凡的就是这样,一无所有的农村人,只能靠父母,如果父母靠不住,那自己也开心点,随着父母的年龄越来越大,我的压力也变大了,他们很多时候会征求我的意见,我也要拿钱支持他们了,也有一些感悟:


1.每年还是得有一个目标,细分到每个月,每一周去完成它,如果没有目标,那就认真的把每一件事情尽量做好,贵人每个人都会遇到,只是看能否抓住,可能会是工作中,生活中的某一个,他愿意提携你一下,真的能少走很多弯路,我的经验告诉我,我有两三次都有贵人帮助我,只是我没有把握住,就像之前一起做区块链,一起做电商的公司老板就很喜欢我,因为我比较踏实,没攻击性,人老实。但是我还是太年轻,很多时候做事不够成熟,就这样和机会擦肩而过,他们现在已经是财富自由了


2.与人交流,说话适可而止,充分尊重他人,聊天中尽量带点幽默,学习一下话题的扩展


3.没事多扩展一下人脉,我才开始培训机构出来,基本没学,全靠在QQ群聊天的人带的我,怎么学,我每次遇到问题我都会问他们,他们在远程帮我改bug,这样我才能保住工作


4.多学习,我看了下我现在的同事,他们没事不会在网上划水,而是都在学习,最敬佩的是旁边那位,一年了从小白,到一名技术骨干,技术成长太快了,他除了学习,每天还不断在看书,我只能说佩服,我很多时候都在刷抖音,我自愧不如,我有罪


5.想办法融入更好的圈子,我之前待得公司都不大,都是外包公司,大家学历都很低,没有一个是985或者211的本科生,大家上班都在聊吃喝嫖赌,主要是聊女人那点事。但是现在我发现身边的同事几年了,没有一个人说过一句脏话,说话总会特别舒服,因为你能感受到他非常尊重你,说话也非常温柔,绝不会听到SEX,tm 这种言词。


6.接受自己的平凡。我以前有很多想法,内心很浮躁,后面发现读书越少的人想法越多,到最后越来越差,债务缠身,本来都是资本的牟利工具,平凡开心更好,把家里的事情处理好,生活上逐渐改善品质,就已经很不错了,在平凡的生活多点浪漫,对未来有一点期待,但不浮夸,我觉得就已经很不错了


7.多多提高自己的综合素质吧,我是一个比较随心的人,但是后面发现,穿的邋遢,说话幼稚,身形不行,走在外面都没自信,何况别人会怎么看你呢,这一点我也在慢慢提高


8.最后我的技术其实很一般,node,vue,react,java,python,php,微信小程序,three.js 这些都有做过,有的都是为了外包业务减少点成本才去学的,但是要说哪一个比较深入,可能就前端的这几个框架,因为天天都在做,偶尔看看掘金的技术文档,但是要说特别深入的,抱歉没有,因为我从误打误撞开始进入这个行业,我的目的不是因为喜欢,而是因为工资高一点,我没有想给要为这个行业带来些什么,我只想活着,我现在觉得我没有特别喜欢做的行业,我不清楚我能在这个行业做多少年,但是只要做,我就把它做好,因为做工作的态度跟自己的喜欢没有关系,做事是做人,自己的工作做好了,下个同事才会很轻松。同时也在尽可能的弥补一些自己的软实力。希望在某一天,有更好的机会,自己能抓得住,自己不会为了自己的能力而自卑!


9.2023-12-21 17:28:53 下班了


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

为什么老家的黄瓜比北京的便宜?普通人应该去经济发达地区谋生

有时候会跟着家人去逛菜市场,有次我岳母说北京的菜真贵,原本在农村老家2、3块的黄瓜到了北京要5、6块一斤,感觉真是土气。 出于好奇,我就开始琢磨这个问题。 我问了财务专业的老婆大人,老婆大人的答案很直接,大概意思是北京的供应链比老家贵的多,比如运输成本、用于卖...
继续阅读 »

有时候会跟着家人去逛菜市场,有次我岳母说北京的菜真贵,原本在农村老家2、3块的黄瓜到了北京要5、6块一斤,感觉真是土气。


出于好奇,我就开始琢磨这个问题。


我问了财务专业的老婆大人,老婆大人的答案很直接,大概意思是北京的供应链比老家贵的多,比如运输成本、用于卖菜的房租成本、还有销售蔬菜的人力成本……然后这些成本都加到蔬菜价格上,自然就高了。


我哈哈一笑,老婆大人的回答自然是不敢出声反驳的,但我可以在心里揣测。我觉得老婆大人说的没问题,但并不深刻,于是我开始浮想联翩……


一 自产自销


在北京生活久了,我发现就蔬菜来说并非不可以自产的。我之前生活的小区,就有销售自产蔬菜的大爷大妈。老婆说是因为供应链比老家的贵,可大爷大妈自产自销不需要供应链啊,老婆的回答就没法解释了。


在我的印象里面,大爷大妈自产的蔬菜也是要比老家的贵的,为什么会如此?


我思考的结果是,大爷大妈是询价定价的。简单来说大爷大妈一开始是不知道定价多少合适的,定的太低心里不得劲,定的太高没人买,所以大爷大妈会去附近菜市场询价,然后定一个比菜市场低的价格,这样既解决了心里不得劲问题,也解决了卖不出去问题。


但这本质上还是因为北京市场上的蔬菜价格比老家的贵。


二 供应链


如果不是自产自销,那就是依靠供应链了。在老家市场销售的黄瓜和北京市场销售的黄瓜,假设有着相同的货源,但背后供应链体系肯定是不同的。


在我们老家有那种大集,就是固定一个时间周期在一个固定地点开展买卖。


大集上的菜农销售蔬菜基本可以忽略掉租房成本,然后因为菜农本身就是销售人员,也省下销售成本,这样整体的供应链成本就下来了,就能够低价销售蔬菜了。


这也就是老婆说的供应链成本不同。


三 边际


边际是经济学核心概念之一,是一种思维方式,就是永远看市场中最后一个人的行为或者最后一个产品的情况。


比如在劳动力市场上,工资不是市场中的平均水平的劳动者决定的,而是最后一个参加劳动的人决定的,要看给他多高的工资他才愿意去做这份工作,同时也要看他有多大的贡献工厂才雇用他,两者相等的时候才是市场里的均衡工资


拉长时间看蔬菜市场中的黄瓜价格会受边际影响。简单来说,蔬菜超市会因为追求最大经济利益调整蔬菜价格,直到找到均衡价格。


结果因为北京的购买者更能容忍价格的波动,导致北京的黄瓜比农村的贵。


四 可替代性


北京的购买者之所以更能容忍价格波动,原因之一是北京生活的人相对老家农村生活的人,在蔬菜这件事上,可替代性弱。


北京生活的人,少有自产自销者;而农村老家的人,多可以自产自销。这虽然不是价格上涨的直接原因,但无形当中推高了均衡价格。


五 购买力


北京的购买者之所以更能容忍价格波动,原因之二是北京的购买力强。


我之前在小区周边的永辉超市看到有机蔬菜,标记35.98/kg,我当时想什么人会买这种蔬菜呢?


蔬菜.jpg


从结果来说,北京购买者购买力强是老家的黄瓜比北京便宜的直接原因


尾声


黄瓜的价格只是一个小的不能再小的缩影,老家的黄瓜之所以比北京便宜,根本原因是老家农村和北京有着根本不同的经济结构


北京这种大城市有更高效的资源利用率,更强的生产能力和更高效的生产效率,结果就是北京创造了更多财富,即使普通人在这里也会有较高收入,高收入下基于边际思维演化的黄瓜价格,形成高的均衡价格。


多年前我曾极力劝我一个朋友来北京,他在四线城市很努力,但始终没能赚大钱。当初我说不出更具说服力的让他来北京的理由,但我认为我现在找到了:他赚钱少可能不是因为他不够努力,而是因为他本来就不在一个发达的经济体里,也就难以享受到比较高的红利。


所以我给普通奋斗者的建议是去经济发达地区谋生吧!


作者:通往自由之路pro
来源:juejin.cn/post/7353233940545323045
收起阅读 »

Activity界面路由的一种简单实现

1. 引言 平时Android开发中,启动Activity是非常常见的操作,而打开一个新Activity可以直接使用Intent,也可以每个Activity提供一个静态的启动方法。但是有些时候使用这些方法并不那么方便,比如:一个应用内的网页需要打开一个原生Ac...
继续阅读 »

1. 引言


平时Android开发中,启动Activity是非常常见的操作,而打开一个新Activity可以直接使用Intent,也可以每个Activity提供一个静态的启动方法。但是有些时候使用这些方法并不那么方便,比如:一个应用内的网页需要打开一个原生Activity页面时。这种情况下,网页的调用代码可能是app.openPage("/testPage")这样,或者是用app.openPage("local://myapp.com/loginPage")这样的方式,我们需要用一种方式把路径和页面关联起来。Android可以允许我们在Manifest文件中配置<data>标签来达到类似效果,也可以使用ARouter框架来实现这样的功能。本文就用200行左右的代码实现一个类似ARouter的简易界面路由。


2. 示例


2.1 初始化


这个操作建议放在Application的onCreate方法中,在第一次调用Router来打开页面之前。


public class AppContext extends Application {
@Override
public void onCreate() {
super.onCreate();
Router.init(this);
}
}

2.2 启动无参数Activity


这是最简单的情况,只需要提供一个路径,适合“关于我们”、“隐私协议”这种简单无参数页面。


Activity配置:


@Router.Path("/testPage")
public class TestActivity extends Activity {
//......
}

启动代码:


Router.from(mActivity).toPath("/testPage").start();
//或
Router.from(mActivity).to("local://my.app/testPage").start();

2.3 启动带参数Activity


这是比较常见的情况,需要在注解中声明需要的参数名称,这些参数都是必要参数,如果启动的时候没有提供对应参数,则发出异常。


Activity配置:


@Router.Path(value = "/testPage",args = {"id", "type"})
public class TestActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//加载布局...

String id = getIntent().getStringExtra("id"); //获取参数
int type = getIntent().getIntExtra("type", 0);//获取参数
}
}

启动代码:


Router.from(mActivity).toPath("/testPage").with("id", "t_123").with("type", 1).start();

2.4 启动带有静态启动方法的Activity


有一些Activity需要通过它提供的静态方法启动,就可以使用Path中的method属性和Entry注解来声明入口,可以提供参数。在提供了method属性时,需要用Entryargs来声明参数。


Activity配置:


@Router.Path(value = "/testPage", method = "open")
public class TestActivity extends Activity {

@Router.Entry(args = {"id", "type"})
public static void open(Activity activity, Bundle args) {
Intent intent = new Intent(activity, NestWebActivity.class);
intent.putExtras(args);
activity.startActivity(intent);
}

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//加载布局...

String id = getIntent().getStringExtra("id"); //获取参数
int type = getIntent().getIntExtra("type", 0);//获取参数
}
}

启动代码:


Router.from(mActivity).toPath("/testPage").with("id", "t_123").with("type", 1).start();

3. API介绍


3.1 Path注解


这个注解只能用于Activity的子类,表示这个Activity需要页面路由的功能。这个类有三个属性:



  • value:表示这个Activity的相对路径。

  • args:表示这个Activity需要的参数,都是必要参数,如果打开页面时缺少指定参数,就会发出异常。

  • method:如果这个Activity需要静态方法做为入口,就将这个属性指定为方法名,并给对应方法添加Entry注解。(注意:这个属性值不为空时,忽略这个注解中的args属性内容)


3.1 Entry注解


这个注解只能用于Activity的静态方法,表示这个方法作为打开Activity的入口。仅包含一个属性:



  • args:表示这个方法需要的参数。


3.2 Router.init方法



  • 方法签名:public static void init(Context context)

  • 方法说明:这个方法用于初始化页面路由表,必须在第一次用Router打开页面之前完成初始化。建议在Application的onCreate方法中完成初始化。


3.3 Rouater.from方法



  • 方法签名:public static Router from(Activity activity)

  • 方法说明:这个方法用于创建Router实例,传入的参数通常为当前Activity。例如,要从AActivity打开BActivity,那么传入参数为AActivity的实例。


3.4 Rouater.to和Rouater.toPath方法



  • 方法签名:




  1. public RouterBuilder to(String urlString)

  2. public RouterBuilder toPath(String path)




  • 方法说明:这个方法用于指定目标的路径,to需要执行绝对路径,而toPath需要指定相对路径。返回的RouterBuilder用于接收打开页面需要的参数。


3.4 RouterBuilder.with方法



  • 方法签名:




  1. public RouterBuilder with(String key, String value)

  2. public RouterBuilder with(String key, int value)




  • 方法说明:这个方法用于添加参数,对应Bundle的各个put方法。目前只有常用的Stringint两个类型。如有需要可自行在RouterBuilder中添加对应的方法。


3.4 RouterBuilder.start方法



  • 方法签名:public void start()

  • 方法说明:这个方法用于打开页面。如果存在路径错误、参数错误等异常情况,会发出对应运行时异常。


4. 实现


import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;

import androidx.annotation.Keep;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

@Keep
public class Router {

public static final String SCHEME = "local";
public static final String HOST = "my.app";
public static final String URL_PREFIX = SCHEME + "://" + HOST;

private static final Map<String, ActivityStarter> activityPathMap = new ConcurrentHashMap<>();

public static void init(Context context) {
try {
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(
context.getPackageName(), PackageManager.GET_ACTIVITIES);

for (ActivityInfo activityInfo : packageInfo.activities) {
Class<?> aClass = Class.forName(activityInfo.name);
Path annotation = aClass.getAnnotation(Path.class);
if (annotation != null && !TextUtils.isEmpty(annotation.value())) {
activityPathMap.put(annotation.value(), (Activity activity, Bundle bundle) -> {
if (TextUtils.isEmpty(annotation.method())) {
for (String arg : annotation.args()) {
if (!bundle.containsKey(arg)) {
throw new IllegalArgumentException(String.format("Bundle does not contains argument[%s]", arg));
}
}
Intent intent = new Intent(activity, aClass);
intent.putExtras(bundle);
activity.startActivity(intent);
} else {
try {
Method method = aClass.getMethod(annotation.method(), Activity.class, Bundle.class);
Entry entry = method.getAnnotation(Entry.class);
if (entry != null) {
for (String arg : entry.args()) {
if (!bundle.containsKey(arg)) {
throw new IllegalArgumentException(String.format("Bundle does not contains argument[%s]", arg));
}
}
method.invoke(null, activity, bundle);
} else {
throw new IllegalStateException("can not find a method with [Entry] annotation!");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public static Router from(Activity activity) {
return new Router(activity);
}

private final Activity activity;

private Router(Activity activity) {
this.activity = activity;
}

public RouterBuilder to(String urlString) {
if (TextUtils.isEmpty(urlString)) {
return new ErrorRouter(new IllegalArgumentException("argument [urlString] must not be null"));
} else {
return to(Uri.parse(urlString));
}
}

public RouterBuilder toPath(String path) {
return to(Uri.parse(URL_PREFIX + path));
}

public RouterBuilder to(Uri uri) {
try {
if (SCHEME.equals(uri.getScheme())) {
if (HOST.equals(uri.getHost())) {
String path = uri.getPath();//note: 二级路径暂不考虑
ActivityStarter starter = activityPathMap.get(path);
if (starter == null) {
throw new IllegalStateException(String.format("path [%s] is not support", path));
} else {
NormalRouter router = new NormalRouter(activity, starter);
for (String key : uri.getQueryParameterNames()) {
if (!TextUtils.isEmpty(key)) {
router.with(key, uri.getQueryParameter(key));
}
}
return router;
}
} else {
throw new IllegalArgumentException(String.format("invalid host : %s", uri.getHost()));
}
} else {
throw new IllegalArgumentException(String.format("invalid scheme : %s", uri.getScheme()));
}
} catch (RuntimeException e) {
return new ErrorRouter(e);
}
}

public static abstract class RouterBuilder {
public abstract RouterBuilder with(String key, String value);

public abstract RouterBuilder with(String key, int value);

public abstract void start();
}


private static class ErrorRouter extends RouterBuilder {
private final RuntimeException exception;

private ErrorRouter(RuntimeException exception) {
this.exception = exception;
}

@Override
public RouterBuilder with(String key, String value) {
return this;
}

@Override
public RouterBuilder with(String key, int value) {
return this;
}

@Override
public void start() {
throw exception;
}
}

private static class NormalRouter extends RouterBuilder {
final Activity activity;
final Bundle bundle = new Bundle();
final ActivityStarter starter;

private NormalRouter(Activity activity, ActivityStarter starter) {
this.activity = Objects.requireNonNull(activity);
this.starter = Objects.requireNonNull(starter);
}

@Override
public RouterBuilder with(String key, String value) {
bundle.putString(key, value);
return this;
}

@Override
public RouterBuilder with(String key, int value) {
bundle.putInt(key, value);
return this;
}

@Override
public void start() {
starter.start(activity, bundle);
}
}

@FunctionalInterface
private interface ActivityStarter {
void start(Activity activity, Bundle bundle);
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Path {
String value();

String method() default "";

String[] args() default {};
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Entry {
String[] args() default {};
}
}

5. 注意



  1. 这个工具的一些功能与ARouter类似,实际项目中建议使用ARouter。如果有特殊需求,例如,页面参数的检查或定制具体打开行为,可以考虑基于这个工具进行修改。

  2. 使用了Pathmethod属性时注意添加对应的混淆设置,避免因混淆而导致找不到对应方法。


作者:乐征skyline
来源:juejin.cn/post/7235639979882463292
收起阅读 »

基于SSE的实时消息推送

背景 小盟 AI 助手项目中需要服务端把 AI 模型回调回来内容,实时推送到客户端,展示给用户;整个流程需要一个能够快速支持上线的服务端推送方案。经过对现有的一些服务端推送方案进行调研,并结合项目的周期、实现成本、用户体验等多方综合考量,最终选择了 ...
继续阅读 »



背景


小盟 AI 助手项目中需要服务端把 AI 模型回调回来内容,实时推送到客户端,展示给用户;整个流程需要一个能够快速支持上线的服务端推送方案。经过对现有的一些服务端推送方案进行调研,并结合项目的周期、实现成本、用户体验等多方综合考量,最终选择了 Server-Sent Events(SSE)方案进行实践。


首先服务端推送,是一种允许应用服务器主动将信息发送到客户端的能力,为客户端提供了实时的信息更新和通知,增强了用户体验。


服务端推送主要基于以下几个诉求:


(1)实时通知:在很多情况下,用户期望实时接收到应用的通知,如新消息提醒、活动提醒等。


(2)节省资源:如果没有服务端推送,客户端需要通过轮询的方式来获取新信息,会造成客户端、服务端的资源损耗。通过服务端推送,客户端只需要在收到通知时做出响应,大大减少了资源的消耗。


(3)增强用户体验:通过服务端推送,应用可以针对特定用户或用户群发送有针对性的内容,如优惠活动、个性化推荐等。这有助于提高用户对应用的满意度和黏性。


方案对比



轮询: 是一种较为传统的方式,客户端定时地向服务端发送请求,询问是否有新数据。服务端只需要检查数据状态,然后将结果返回给客户端。轮询的优点是实现简单,兼容性好;缺点是可能产生较大的延迟,且对服务端资源消耗较高。


长轮询(Long Polling): 轮询的改进版。客户端向服务器发送请求,服务器收到请求后,如果有新的数据,立即返回给客户端;如果没有新数据,服务器会等待一定时间(比如30秒超时时间),在这段时间内,如果有新数据,就返回给客户端,否则返回空数据。客户端处理完服务器返回的响应后,再次发起新的请求,如此反复。长轮询相较于传统的轮询方式减少了请求次数,但仍然存在一定的延迟。   


WebSocket: 一种双向通信协议,同时支持服务端和客户端之间的实时交互。WebSocket 是基于 TCP 的长连接,和HTTP 协议相比,它能实现轻量级的、低延迟的数据传输,非常适合实时通信场景,主要用于交互性强的双向通信。


SSE: 是一种基于 HTTP 协议的推送技术。服务端可以使用 SSE 来向客户端推送数据,但客户端不能通过 SSE 向服务端发送数据。相较于 WebSocket,SSE 更简单、更轻量级,但只能实现单向通信。


图片


图片


小盟 AI 助手项目需要快速上线且保证要用户较好的使用体验。鉴于 SSE 技术的轻量、实现简单、不增加额外的资源成本;当前业务场景也只需要服务端到用户端的单向的字符推送。非常适合项目需要。所以决定使用 SSE 来实现内容推送。****


深入 SSE



SSE 服务端推送,它基于 HTTP 协议,易于实现和部署,特别适合那些需要服务器主动推送信息、客户端只需接收数据的场景。有以下特点:


1、简单:基于 HTTP,无须额外的协议或者类库支持。主流浏览器都支持。


2、事件流:使用"事件流"(Event Stream)将数据从服务器发送到客户端。每个事件都可以包含一个事件标识符、事件类型和数据字段。客户端可以根据这些信息来解析和处理接收到的数据。


3、自动重连:意外断开时会自动尝试重新连接。可以确保了在网络故障或连接中断后能够及时恢复通信,为用户提供连续的数据流。重连时会在 HTTP 头中的Last_Event_ID 带上上一次的数据 ID,便于服务端返回后续数据。


4、单向推送:只能从服务端推送数据到客户端。


图片


SSE 消息体介绍:


图片


SSE消息体示例:


图片


服务端主要使用 Spring,其对 SSE 主要提供了两种支持:



  • Spring WebMVC:传统的基于 Servlet 的同步阻塞编程模型,即 同步模型 Web 框架。

  • Spring WebFlux:异步非阻塞的响应式编程模型,即 异步模型 Web 框架。          


项目基于springboot,所以选择使用前者实现。SseEmitter emitter = new SseEmitter(); 一句代码就可以建立一个 SSE 连接。


实践



后端实现


建立一个SseEmitterManger,统一管理当前服务 SSE 连接的创建、释放以及数据推送。结合 Redis 缓存可实现集群环境 SSE 连接的管理。


核心逻辑如下:



  • 连接池维护,设定一个上限,避免过大,导致内存问题。


static final Map<String, SseEmitter> sseCache =     new ConcurrentHashMap<>(300)          


  • 建立SSE连接,为每个连接建立一个唯一的MsgId,用来维护SSE连接与客户端的关系了;在 Redis 缓存中存入MsgId和当前机器节点的IP和Port,这样可以找到SSE 连接所在的服务结点,然后通过 HTTP 请求转发需要发送的数据到对应的服务节点上进行处理。


sse = new SseEmitter()sseId = "sse_xxx";redisKey= "aisse:" + bosId + "_" + wid ipPort = "10.10.10.10:8080"redis.hset(redisKey, msgId, ipPort)sseCache.put(msgId, sseEmitter);


  • 获取持有连接的 pod ipPort;根据 IP 发起请求。


ipPort = redisUtil.hashGet(redisKey, msgId)


  • 获取当前服务结点的SSE连接,发送数据。


sseEmitter = sseCache.get(msgId)sseEmitter.send(msgJson)          


  • 释放SSE连接


SseEmitter sseEmitter = sseCache.get(msgId);sseEmitter.complete();sseCache.remove(msgId);redisUtil.hashDel(redisKey, msgId);

**核心流程图如下: **  


图片


需要注意的是开启 SSE 连接接口的整个链路都要支持长连接。例如使用 Nginx 则要开启长连接的配置:



  • keepalive 用于控制可连接整个 upstream servers 的 HTTP 长连接的个数,即控制总数。

  • proxy_http_verion 用于控制代理后端链接时使用的 HTTP 版本,默认为 1.0。要想使用长连接,必须配置为 1.1。

  • proxy_set_header 需要设置为 Connection "",否则则发往 upstream servers 的请求中,Connection header 的值将为close,导致无法建立长连接。   


http {        upstream keepAliveService {            server 10.10.131.149:8080;            keepalive 20;        }            server {            listen 80;            server_name keepAliveService;            location /keep-alive/hello {                proxy_http_version 1.1;                proxy_set_header Connection "";                proxy_pass http://keepAliveService;            }        }}

**前端实现 **  


前端可以使用组件 @microsoft/fetch-event-source 来实现。


npm i @microsoft/fetch-event-source
import { fetchEventSource } from '@microsoft/fetch-event-source';let controller = new AbortController(); let eventSource = fetchEventSource('apiUrl', { method: 'POST', headers: { 'Content-Type': 'application/json', 'token': '....' }, signal: controller.signal, body: JSON.stringify({ ... // 传参 }), onopen() { // 建立连接 }, onmessage(event) { // 接收信息 // 成功之后满足某些条件可以使用AbortController关闭连接 controller.abort() eventSource?.close && eventSource.close(); }, onerror() { // 服务异常 controller.abort() eventSource?.close && eventSource.close(); }, onclose() { // 服务关闭 },})

总结



SSE 轻量级的服务端单向推送技术;具有支持跨域、使用简单、支持自动重连等特点。相对于 WebSocket 更加轻量级,如果需求场景客户端和服务端单向通信,那么 SSE 是一个不错的选择。


作者:微盟技术中心
来源:juejin.cn/post/7317325043541032970
收起阅读 »

社会现实告诉我,00后整顿职场就是个笑话

00后整顿职场,也算是我之前的关键词吧。 我硬怼老板要加班费和提成,和他们辩论什么是我的财产,什么是公司的财产。 甚至还能在即将被开除的时候,反将一军把老板开除。 而正是因为这一次把老板开除,让我得到了机会。可以站在了相对于之前更高的位置,来俯瞰整个职场。 也...
继续阅读 »

00后整顿职场,也算是我之前的关键词吧。


我硬怼老板要加班费和提成,和他们辩论什么是我的财产,什么是公司的财产。


甚至还能在即将被开除的时候,反将一军把老板开除。


而正是因为这一次把老板开除,让我得到了机会。可以站在了相对于之前更高的位置,来俯瞰整个职场。


也真正意义上让我感受到了,00后整顿职场,就是一个互联网笑话罢了。


1、职场宫斗,成功上位


我之前在苏州工作,堪称工作中的宫斗,并且在这场宫斗大戏中胜出,将原有的项目负责人开除,成功上位。


而这个项目存在的问题非常多,我就在六月被派遣去项目的总部合肥进行学习,等到打通项目的全部链路后,再回到苏州。


届时我将以这个项目的负责人,重新搭建团队,开展这个项目。所以我在合肥那边,以员工的身份深入各个工作组进行学习。


在市场部,运营部的办公大厅工作过,也在各部门的独立办公室工作过。


我感觉自己像个间谍,一边在以平级的打工人身份和我的同事们相处,一边又以苏州负责人的身份,参与那些领导才能参与的内部会议。


2、内心变化的开端


我在合肥总部工作中,接触了很多躺平摆烂的同事,但这个“躺平摆烂“要加上双引号。


他们是00后,90后,甚至有85后。如果放在三个月前,我可以不假思索地说,他们全都是我最讨厌的人。他们如同牛羊一般任人宰割,上级让加班,他们就加班,有时候加班甚至超过四五个小时也没有怨言。


我甚至从来没听他们感慨过为什么没有加班费。亲眼看着他们被自己的上级用一些与工作无关的鸡毛蒜皮之事骂得狗血淋头,但他们也只会在被骂完之后,背地里吐槽那个领导估计是在家被老婆骂了,才来拿他们泄愤。


我打听了他们的工资,只能说中规中矩,起码不是能让人当牛做马的数字。偶尔我见到一两个有骨气的人,觉得拿这么点钱就应该干这么点事。干不爽就马上离职,但马上就会有下一个人替补他的位置,形成闭环。


我惊讶于怎么有人能惹到这个地步,但后来和他们日渐熟落,我们一起吃饭,一起打游戏,一起下班顺路回家,还参加了他们的生日聚会。我发现他们活得其实真的很洒脱。一切都是随遇而安,下班时间一到,他们就真的可以无忧无虑。


因为他们有一份工资还行的工作,养活自己。他们没有啃老,也没有用卑鄙的手段,去抢想要努力的人应该分到的蛋糕,也压根不去想要赚很多钱,因为没有什么需要太高消费的需求。


加上现在的环境,找到一份可观收入的工作确实很难。所以公司偶尔的加班,领导偶尔的泄愤,这些毕竟还是少数时候的偶尔,也都没有超过他们的心理承受阈值,那也就得过且过了。


所以我们其实都一样,只是个普通人罢了。而像我们这样的普通人,取之不尽,用之不竭。这到底是好事还是坏事呢?


3、复杂的职场生态环境


建立在这个基础上,视觉转换到高层领导们这里。他们当着我的面说,这样的人就是个底层打工仔,缺人就招,加班照旧,心情不好还要扣他们的全勤绩效。


压根就不怕这些底层打工仔闹事,纵使有一两个所谓的决心者辞职,也能在很快时间找到下一位。


两者形成互补,共同铸就了这样恶劣的职场生态环境。但我说职场无法改变,远不止这么一点原因。


在这个项目中,我说好听一些只能算是项目负责人,在此之上还有着项目股东,这还要细分成大股东和小股东。而我所在的项目属于互联网赛道,也就是说需要一些新鲜事物的眼光和思维来对待。


但这些股东们经常提出一些奇怪的意见,就如同用微商时代的卖货思维,来指点直播带货,并且他们是出钱的股东,他们提出的战略方针不容我驳回,因为在他们的光辉历史中,有大量的成功案例,来佐证他们的思路是对的。


我刚开始觉得也有道理。他们能有钱投资,肯定是有什么过人的本领能让他们赚到钱,但是随着相处下来,我发现不过是他们本身家里条件就优越,在九几年就能拿出一百万给他们创业。


他们把这一百万分散到二十个领域,每个投资五万总能撞上那么一两个风口,让他们实现钱生钱。


九几年的五万也算是一笔不少的投资。他们这样的发财经历,让我很难不产生质疑,这不是给我我也行吗?


毕竟他们如果真的有什么过人的本领和远见,也不至于在每次内部开会之前,都要组织喊这样的口号:“好,很好,非常好,越来越好“


甚至试图把这样的口号,带到每一次迎接客户的项目介绍会上。我以自曝式要挟制止他们这个行为,我说如果你们这么干,那我当天就辞职,内部都是自己人,我可以陪你们这样弄,但如果对外这么搞,被录下来说我们是传销,我都不知道怎么辩解。


4、职场中的背锅人


他们就是这样坚信着自己能成功,是因为自己有过人的才华。所以自我洗脑着自己提出的方向没有错。如果出错了,亏损了,那一定是负责人的问题。


但好巧不巧,我就是那个负责人。我已经无数次告诉他们,我们这个项目压根就不需要穿黑丝短裙跳舞的小姐姐。


我甚至写了一篇报告给他们,分析我们的项目为什么不能用擦边这种手段引流。但他们执意要,说这样来流量快,我都有点分不清到底是他们自己想看,还是深信这样做确实是可行。


但如果最后这样还是没成功,导致项目亏损,大概率还是在我身上找原因吧。


面对他们这样的大佬,我心里很清楚,这已经远远不是宫斗了,这也绝对不是靠几个心计,或者有实力撑腰就能取胜上位了。这场权力的游戏,不是我等草民玩得起的。


5、换个思路,创造属于自己的职场


一边是被提供资金,但是瞎指挥的股东们摧残,一边是在有限的预算下,我作为负责人,确实很难做到尊重打工人的内心挣扎,回到苏州我虽然能身居高位,但我终将成为我曾经最鄙视的人。


我不要当这个背锅侠,我也不想在这个环境中,去逐渐接受这样的价值观。


这样看来确实如此,00后整顿职场不过是一场互联网的狂欢罢了。


这个题材的故事,也永远只能发生在职场的最底层。由一群家境优越,体验生活的公子哥和我这种不知好歹的普通人共同出演。


大部分人只是在手机屏幕前把我们当个乐子,成了扣个666,然后一起吃胜利的果实。没成,那就确实是看了个乐子。


或许是因为他们心里也清楚,凭我们压根就做不到。


00后现在确实整顿不了职场,因为社会的资源和命脉还不掌握在00后手上。


但就止步于此了吗?我曾说过我想有一个自己的小工作室,遵守劳动法,双休,按时发工资,交纳五险一金。


是的,换个思路,也许00后不需要整顿职场,而是直接创造属于自己的职场,那么接下来我就要向着这个目标去努力了,毕竟二十年后我也还是00后,不如到时候再来说00后整顿职场吧。


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

RecyclerView还能这样滚动对齐?

前言 RecyclerView要想滚动到指定position,一般有scrollToPosition()和smoothScrollToPosition()两种方式。滚动到指定position后,通常还会要求itemView对齐RecyclerView起始点、中...
继续阅读 »

前言


RecyclerView要想滚动到指定position,一般有scrollToPosition()smoothScrollToPosition()两种方式。滚动到指定position后,通常还会要求itemView对齐RecyclerView起始点、中心点或结束点


熟悉RecyclerView的人应该知道,使用自定义SmoothScroller可以实现平滑滚动到指定position的同时,让itemView和RecyclerView的对齐;而scrollToPosition()方法只能滚动到指定position。那有办法让scrollToPosition()也做到对齐吗?


拆解行为


分析对齐的行为后,可以分为几步



  1. 让目标itemView可见

  2. 计算itemView和目的位置的偏移量

  3. 将itemView移动到目的位置


第一步scrollToPosition()就已经可以实现了,最后一步就是调用scrollBy(),那其实只需要实现第二步计算偏移量,而这可以参考SmoothScroller的实现


平滑滚动


来看下SmoothScroller是怎么做的。通常做法都是自定义LinearSmoothScroller


RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
int preference = LinearSmoothScroller.SNAP_TO_START;// 对齐方式
LinearSmoothScroller smoothScroller = new LinearSmoothScroller(context){
@Override
protected int getHorizontalSnapPreference() {
return preference;
}

@Override
protected int getVerticalSnapPreference() {
return preference;
}
};
smoothScroller.setTargetPosition(targetPosition);
layoutManager.startSmoothScroll(smoothScroller);

简单介绍下几种对齐方式



  • SNAP_TO_START:对齐RecyclerView起始位置

  • SNAP_TO_END:对齐RecyclerView结束位置

  • SNAP_TO_ANY:对齐RecyclerView任意位置,确保itemView在RecyclerView内


接下来看下getVerticalSnapPreference()或者getHorizontalSnapPreference()的返回值是怎么影响到itemView的对齐的。查看LinearSmoothScroller源码发现这两个方法会在onTargetFound()里调用


protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
final int distance = (int) Math.sqrt(dx * dx + dy * dy);
final int time = calculateTimeForDeceleration(distance);
if (time > 0) {
action.update(-dx, -dy, time, mDecelerateInterpolator);
}
}

不难看出,该方法是计算targetView当前要滚动的偏移量和时长,并设置给action。而calculateDxToMakeVisible()calculateDyToMakeVisible()正是我们要找的计算偏移量的方法


由于这两个方法只依赖LayoutManager,所以我们可以将这些代码逻辑复制出来,创建一个Rangefinder类,用于计算偏移量


public class Rangefinder {
private final RecyclerView.LayoutManager mLayoutManager;

public Rangefinder(RecyclerView.LayoutManager layoutManager) {
mLayoutManager = layoutManager;
}

@Nullable
public RecyclerView.LayoutManager getLayoutManager() {
return mLayoutManager;
}

// 计算view在RecyclerView中完全可见所需的垂直偏移量
public int calculateDyToMakeVisible(View view, int snapPreference) {
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
if (layoutManager == null || !layoutManager.canScrollVertically()) {
return 0;
}
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
final int start = layoutManager.getPaddingTop();
final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
return calculateDtToFit(top, bottom, start, end, snapPreference);
}

// 计算view在RecyclerView中完全可见所需的水平偏移量
public int calculateDxToMakeVisible(View view, int snapPreference) {
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
if (layoutManager == null || !layoutManager.canScrollHorizontally()) {
return 0;
}
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin;
final int right = layoutManager.getDecoratedRight(view) + params.rightMargin;
final int start = layoutManager.getPaddingLeft();
final int end = layoutManager.getWidth() - layoutManager.getPaddingRight();
return calculateDtToFit(left, right, start, end, snapPreference);
}

public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd,
@SnapPreference int snapPreference)
{
switch (snapPreference) {
case LinearSmoothScroller.SNAP_TO_START:
return boxStart - viewStart;
case LinearSmoothScroller.SNAP_TO_END:
return boxEnd - viewEnd;
case LinearSmoothScroller.SNAP_TO_ANY:
final int dtStart = boxStart - viewStart;
if (dtStart > 0) {
return dtStart;
}
final int dtEnd = boxEnd - viewEnd;
if (dtEnd < 0) {
return dtEnd;
}
break;
}
return 0;
}
}

有了计算偏移量的方法,接下来就是实现itemView的对齐了


即时滚动


根据上面的拆解步骤,再分析下每一步要做的事情



  1. 调用scrollToPosition()使目标itemView可见。因为该方法最终会requestLayout(),所以要在layout后,才能通过获取到itemView。那么可以post()后调用LayoutManagerfindViewByPosition()方法获取itemView

  2. 参考LinearSmoothScrolleronTargetFound()方法,使用上面的Rangefinder计算itemView和目的位置的偏移量

  3. 调用scrollBy()将itemView移动到目的位置


RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
recyclerView.scrollToPosition(targetPosition);
recyclerView.post(new Runnable() {
@Override
public void run() {
View targetView = layoutManager.findViewByPosition(targetPosition);
if (targetView != null) {
Rangefinder rangefinder = new Rangefinder(layoutManager);
final int dx = rangefinder.calculateDxToMakeVisible(targetView, preference);
final int dy = rangefinder.calculateDyToMakeVisible(targetView, preference);
if (dx != 0 || dy != 0) {
recyclerView.scrollBy(-dx, -dy);
}
}
}
});

至此,我们就实现了即时滚动到position的同时,让itemView和RecyclerView对齐的功能。当然,这也只是测试代码,实际使用还会对上面的逻辑进行封装


测试代码 recyclerView-scroll-demo


参考


作者:benio
来源:juejin.cn/post/7364740313284444186
收起阅读 »

这么炫酷的换肤动画,看一眼你就会爱上

web
实现过程 我们先创建下 vue 项目 npm init vite-app vue3-vite-animation 进入文件夹中 cd vue3-vite-animation 安装下依赖 npm install 启动 npm run dev 重新修改 ...
继续阅读 »

动画.gif


实现过程


我们先创建下 vue 项目


npm init vite-app vue3-vite-animation

进入文件夹中


cd vue3-vite-animation

安装下依赖


npm install

启动


npm run dev

image-20240503171537954.png


重新修改 App.vue


<template>
<div class="info-box">
<div class="change-theme-btn">改变主题</div>
<h1>Element Plus</h1>
<p>基于 Vue 3,面向设计师和开发者的组件库</p>
</div>

</template>

<script setup lang="ts">

</script>



<style>

.change-theme-btn {
width: 80px;
height: 40px;
background-color: #fff;
text-align: center;
line-height: 40px;
color: #282c34;
cursor: pointer;
border-radius: 8px;
border: 2px solid #282c34;
}

.info-box {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>


基本样式出来了,但是页面出现了滚动条,我们需要去掉原有样式


image-20240503175456039.png


src/index.css,里的所有样式都删除了,再到 index.html 中将 bodymargin 属性去掉


<body style="margin: 0;">
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>

接下来,我们来实现下换肤功能


使用 css 变量,先定义下一套黑暗主题、一套白色主题


:root {
--background-color: #fff;
--color: #282c34;
background-color: var(--background-color);
color: var(--color);
}

:root.dark {
--background-color: #282c34;
--color: #fff;
}

再定义点击事件 changeColor,点击 "改变主题" 就会改变主题颜色


classList.toggle 这个方法的第一个参数是类名,第二个参数是布尔值,表示是否添加类


如果第二个参数为 true,则添加类;如果第二个参数为 false,则移除类


<div class="change-theme-btn" @click="changeColor">改变主题</div>

/* 改变颜色 */
const changeColor = () => {
document.documentElement.classList.toggle('dark')
}

image-20240503180914393.png


按钮背景颜色、边框、字体颜色都没有改变


调整下按钮样式,把背景颜色、边框、字体颜色这些都用 css 变量代替


.change-theme-btn {
width: 80px;
height: 40px;
background-color: var(--background-color);
text-align: center;
line-height: 40px;
color: var(--color);
cursor: pointer;
border-radius: 8px;
border: 2px solid var(--color);
}

image-20240503181138545.png


这个效果不是我们想要的,需要一个过渡动画对不对


使用 startViewTransition,这个 API 会生成一个屏幕截图,将新旧屏幕截图进行替换


截图分别对应两个伪元素 ::view-transition-new(root)::view-transition-old(root)


 // 创建一个过渡对象
document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})

可以看到,一个淡入淡出的效果,但是我们需要的是一个圆向外扩散的效果


用剪切效果就可以实现,其中 circle(动画进度 at 动画初始x坐标 动画初始y坐标)


设置动画时间为 1秒,作用在新的伪元素上,也即是作用在新的截图上


const transition = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})

transition.ready.then(() => {
document.documentElement.animate({
clipPath: ['circle(0% at 50% 50%)', 'circle(100% at 100% 100%)']
}, {
duration: 1000,
pseudoElement: '::view-transition-new(root)'
})
})

动画-1714752074132-6.gif


为什么动画效果和预期的不一样


因为,默认的动画效果,把当前动画覆盖了,我们把默认动画效果去掉


/* 隐藏默认的过渡效果 */
::view-transition-new(root),
::view-transition-old(root) {
animation: none;
}

动画-1714752309164-8.gif


效果出来了,但是圆的扩散不是从按钮中心扩散的


那么,通过 ref="btn" 来获取 “改变主题” 按钮的坐标位置


再获取按钮坐标减去宽高,就能得到按钮的中心坐标了


<div ref="btn" class="change-theme-btn" @click="changeColor">改变主题</div>

<script setup>
import { ref } from 'vue';
const btn = ref<any>(null)

/* 改变颜色 */
const changeColor = () => {
// 创建一个过渡对象
const transition = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})

const width = btn.value.getBoundingClientRect().width // 按钮的宽度
const height = btn.value.getBoundingClientRect().height // 按钮的高度
const x = btn.value.getBoundingClientRect().x + width / 2 // 按钮的中心x坐标
const y = btn.value.getBoundingClientRect().y + height / 2 // 按钮的中心y坐标

transition.ready.then(() => {
document.documentElement.animate({
clipPath: [`circle(0% at ${x}px ${y}px)`, `circle(100% at ${x}px ${y}px)`]
}, {
duration: 1000,
pseudoElement: '::view-transition-new(root)',
})
})
}
</script>

扩展,如果,我不要从中心扩展,要从左上角开始动画呢,右上角呢...


我们把按钮放在左上角,看看效果


修改下样式、与模板


<template>
<div ref="btn" class="change-theme-btn" @click="changeColor">改变主题</div>
<div class="info-box">
<h1>Element Plus</h1>
<p>基于 Vue 3,面向设计师和开发者的组件库</p>
</div>

</template>

.info-box {
width: 100vw;
height: calc(100vh - 44px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

动画这个圆的半径不对,导致动画到快末尾的时候,直接就结束了


动画-1714753474905-10.gif


动画的圆的半径 = 按钮中心坐标 到 对角点的坐标


可以使用三角函数计算,两短边平方 = 斜边平方


image-20240504002759638.png


// 计算展开圆的半径
const tragetRadius = Math.hypot(
window.innerWidth - x,
innerHeight - y
)

// 设置过渡的动画效果
transition.ready.then(() => {
document.documentElement.animate({
clipPath: [`circle(0% at ${x}px ${y}px)`, `circle(${tragetRadius}px at ${x}px ${y}px)`]
}, {
duration: 1000,
// pseudoElement
// 设置过渡效果的伪元素,这里设置为根元素的伪元素
// 这样过渡效果就会作用在根元素上
pseudoElement: '::view-transition-new(root)',
})
})

动画-1714754131456-15.gif


如果是右上角呢


.change-theme-btn {
float: right;
width: 80px;
height: 40px;
background-color: var(--background-color);
text-align: center;
line-height: 40px;
color: var(--color);
cursor: pointer;
border-radius: 8px;
border: 2px solid var(--color);
}

动画-1714754468881-23.gif


在右边的话,使用三角函数计算,其中一个短边就不能是 屏幕宽度 - 按钮x坐标,直接是 x 坐标就对了


那要怎么实现呢,直接取 屏幕宽度 - 按钮x坐标 与 按钮x坐标 的最大值就可以了


y 也是同理


const tragetRadius = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y)
)

动画-1714754788538-25.gif


你可以试试其他位置,是否也是可行的


完整代码


<template>
<div ref="btn" class="change-theme-btn" @click="changeColor">改变主题</div>
<div class="info-box">
<h1>Element Plus</h1>
<p>基于 Vue 3,面向设计师和开发者的组件库</p>
</div>

</template>

<script setup lang="ts">
import { ref } from 'vue';
const btn = ref<any>(null)

/* 改变颜色 */
const changeColor = () => {
// 创建一个过渡对象
const transition = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})

const width = btn.value.getBoundingClientRect().width // 按钮的宽度
const height = btn.value.getBoundingClientRect().height // 按钮的高度
const x = btn.value.getBoundingClientRect().x + width / 2 // 按钮的中心x坐标
const y = btn.value.getBoundingClientRect().y + height / 2 // 按钮的中心y坐标

// 计算展开圆的半径
const tragetRadius = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y)
)

// 设置过渡的动画效果
transition.ready.then(() => {
document.documentElement.animate({
clipPath: [`circle(0% at ${x}px ${y}px)`, `circle(${tragetRadius}px at ${x}px ${y}px)`]
}, {
duration: 1000,
// pseudoElement
// 设置过渡效果的伪元素,这里设置为根元素的伪元素
// 这样过渡效果就会作用在根元素上
pseudoElement: '::view-transition-new(root)',
})
})
}
</script>


<style>

:root {
--background-color: #fff;
--color: #282c34;
background-color: var(--background-color);
color: var(--color);
}

:root.dark {
--background-color: #282c34;
--color: #fff;
}

/* 隐藏默认的过渡效果 */
::view-transition-new(root),
::view-transition-old(root) {
animation: none;
}

.change-theme-btn {
float: right;
width: 80px;
height: 40px;
background-color: var(--background-color);
text-align: center;
line-height: 40px;
color: var(--color);
cursor: pointer;
border-radius: 8px;
border: 2px solid var(--color);
}

.info-box {
width: 100vw;
height: calc(100vh - 44px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>

换肤动画源码


小结


换肤功能,主要靠 css 变量 与 classList.toggle


startViewTransition 这个 API 来实现过渡动画效果,注意需要清除默认动画


圆点扩散效果,主要运用剪切的方式进行实现,计算过程运用了三角函数运算


作者:大麦大麦
来源:juejin.cn/post/7363836438935552035
收起阅读 »

从密码到无密码:账号安全进化史(科普向)

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究! 本文是一篇科普文,五一结束了,大家看点轻松的~ 不知道大家在过去半年有没有发现 Github 强制开启了 2FA,而且还不可以关闭的,每次你打开 github 都会提...
继续阅读 »

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!




本文是一篇科普文,五一结束了,大家看点轻松的~


不知道大家在过去半年有没有发现 Github 强制开启了 2FA,而且还不可以关闭的,每次你打开 github 都会提醒你的验证:


Image.png


简单的说,就是打开 Github 进行验证时,只依靠密码验证已经不被允许,你必须打开你手机上的验证软件,把里面随机码输入到 Github 才能完成身份验证,类似于十年前国内 QQ 安全中心的验证。


这是一种双重验证的手段,用于更好的保证我们的账号安全,今天就以此为引,给大家讲讲账号安全相关发展的历史。


第一幕:密码的独角戏 - 脆弱的防线


在互联网的蛮荒时代,密码就像原始人手中的木棍和石头,是守护账号安全的唯一屏障。然而,这道防线却是脆弱不堪的,面对黑客的攻击,如同纸糊的老虎,一戳就破。暴-力-破-解、字-典-攻-击、社会-工程-学手段,都足以让密码这道防线形同虚设。


1. 暴-力-破-解:暴-力-破-解就是通过遍历的方式尝试出你的密码组合,比如银行的六位取款密码实际上只有 46656种组合,利用现在任何一台电脑或者手机的算力都能瞬间算出来,为了对应这种情况,现在几乎所有网站都有密码输入次数限制。


2. 字-典-攻-击:字典-攻击就是利用常用密码来攻破你的密码,比暴力破解效率更好,比如 123456 这个密码就有很多人使用。


3. 社会-工程-学:社会-工程-学说人话就是套你的话,或者调查你的信息,比如在和你沟通的过程中知道了你的手机号、身-份-证号码、生日信息等,因为有大量的人用手机号后六位、身-份-证号后六位或者生日当作自己的密码,所以这种手段的成功率一般会更高。


在当前这个时代,由于互联网各种 App 的涌入,每个人都拥有大量的账号,如何记忆他们成为了一个难题,大量的人选择对所有网站使用同一个密码,这就又造成了账号安全问题。


重复使用密码就像是使用同一把钥匙开启不同的门,一旦一把钥匙被复制,所有的门都将面临危险。


每年,全球都会发生无数起数据泄露事件,大量的用户名和密码被公开曝光。这些泄露的密码成为了黑客攻击的利器,他们可以利用这些密码进行撞库攻击,尝试登录其他网站。


会在不同的网站使用相同的密码将会导致“一损俱损”的局面。一旦一个网站发生数据泄露,黑客便可以利用泄露的密码尝试登录其他网站,从而获取更多个人信息,造成更大的损失。


第二幕:多因素认证 (MFA) 登场 - 多重关卡,层层设防


所以,为了弥补密码的不足,MFA 应运而生,为账号安全加装了多重门锁。除了密码这把“钥匙”,你还需要其他的“通关密语”才能进入:



  • 验证码: 这是国内最常用的方式,甚至几乎所有 App 都已经不需要你记忆账号密码,只需要一个手机验证码即可,国外使用手机验证码的很少,因为他们更多使用邮箱来注册账号,比如我现在在使用的编辑软件Craft 在登录时就要求你提供邮箱验证码,它甚至不能设置密码。

  • 指纹识别: 你的指纹独一无二,所以它就像是你的专属的“魔法印记”,轻轻一按,就能保证你是你。

  • 面部识别: 对着摄像头眨眨眼,你的面容信息也是你的专属印记,苹果手机上甚至使用了虹膜识别来检测你是你,而不是别人。

  • 安全令牌: 一个小巧的硬件设备,可以生成一次性密码,就像古代的“虎符”一样,只有拥有它才能调兵遣将。令牌可以有软件和硬件两种方式,软件就是 Google Auth 这种软件,而硬件则是我们早古互联网时代网上购物常用的网银 U 盾形式。


在开头的时候,我曾提到了 2FA,它和本节的 MFA 听名字非常相似,实际上说的也几乎是一个东西。


2FA 是指:需要两种验证,才能完成整个验证,一般是密码和动态安全令牌。


MFA 是指:需要两种或以上验证方式,才能完成整个验证,一般也是密码和动态安全令牌。


所以在大多数语境下,这俩说的其实是一个东西,有些验证方式将两个验证方式合而为一,比如手机/邮箱验证码。


因为多因子验证的核心是:一个你知道的凭证 和 一个你刚刚才知道的凭证。


我们一开始就知道的凭证往往是邮箱 + 密码,一个刚刚才会知道的凭证往往就是动态安全令牌码了,所以手机验证码登录的方式也是 2FA,还是属于比较方便的那种。


注:我这里说的手机验证码登录是真的发给你验证码,而不是国内的那种手机号一键登录。


第三幕:单点登录 (SSO) 崛起 - 统一管理的钥匙


其实随着 MFA 的出现,安全问题已经不需要太担心了,所以接下来账号安全开始朝着:安全 + 高效的方向开始发展,所以开始出现了 SSO。


SSO 的第一个阶段是内部互信,它的概念最早可以追溯到 1990 年代,随着企业内部网络的发展而兴起。


后来随着互联网的发展,一个公司往往同时拥有多个业务,比如十年前还是百度的天下的时候,我们会同时使用百度知道、百度贴吧、百度网盘这些产品。


你只需要在某一个百度旗下的产品登录一次,打开其他产品的时候往往也会自动识别到你的账号。


比如你在百度贴吧登录了,此时你打开网页版的百度网盘你自动就是已登录状态。


不要以为这是一个 So easy 的操作,它的原理其实是使用你存储在同一个主域名下的 cookie 实现的。


比如百度贴吧的域名是:tieba.baidu.com/,而百度网盘的域名是:pan.baidu.com/,它俩都属于主域名 baidu.com,所以通过携带同域名下cookie 的方式,让同域名下的其他服务也能正确识别当前账号。


具体识别方案一般有两种:



  1. 通过共享 session + cookie 的方式做验证。

  2. 通过获取 cookie 内部跳转到 SSO 做验证。


无论使用哪种方案,携带 cookie 这个操作必不可少,所以这一阶段的 SSO 是基于 Cookie 的。


可能还有一个词大家比较常见:SAML,SAML标准也是用于内部系统互信,做的事和基于 Cookie 的 SSO 都是一样的,所以这里我不再赘述。


第四幕:OAuth 协议的诞生 - 授权管理的桥梁


经历完 SSO 的第一个阶段之后,我们就来到了 SSO 的第二阶段:外部互信


由于 Web 互联网的兴起,这一阶段也被称为基于 Web 的 SSO,这一阶段的代表是OAuth。


你有没有想过,如果我们在所有平台都使用同一个账号多好,就不用在记忆那么多的应用账号密码,减少心智负担。


在国内互联圈地的情况下,这种情况并没有实现,也可以说通过手机号实现了。


但是在国外,Google 账号体系几乎就是事实上的一号通行,你注册一个 Google 账号之后,几乎可以通过这个账号登录所有的网站,这就是 OAtuh 的作用。


想象一下,你拥有许多宝藏,分别存放于不同的宝库中。比如,你在 Facebook 上存储着你的社交关系,在 Google 上存储着你的邮件和文件,在 Spotify 上存储着你的音乐喜好。


现在,你想要使用一个新的游戏应用,而这个应用需要访问你在 Facebook 上的好友列表,以便你能够邀请好友一起玩游戏。


这时,你面临一个两难的选择:



  • 分享密码: 将你的 Facebook 密码告诉游戏应用,让它直接访问你的好友列表。但这存在着巨大的安全风险,一旦游戏应用泄露你的密码,你的所有 Facebook 数据都将暴露无遗。

  • 放弃使用: 由于担心安全问题,你放弃使用这个游戏应用,从而错过了与好友一起游戏的乐趣。


为了解决上面这种问题,Google 等公司在 2010 年发布了 OAuth1.0,由于它存在许多问题,所以又在 2012 年发布了 OAuth2.0。


所以在现如今,几乎所有公司都接入了 Google 的 OAuth 登录,当你在第三方平台想使用 Google 账号登录时,OAuth 协议会引导你到 Google 进行授权。


平台会询问你是否同意授权第三方应用访问你的部分数据 (例如好友列表),如果你同意,平台就会发放一个临时的“通行证”给第三方应用,让它可以访问你的数据,但不会泄露你的密码。


所以 OAuth 的核心是授权而非共享。


第五幕:无密码时代的曙光 - 告别繁琐的密码


我相信当大家看到第四节的时候,大家就会觉得应该就这些了,没有别的新意了,恰恰相反,为了彻底摆脱密码的束缚,世界巨头们正在探索新的“魔法”,那就是无密码


在 2019 年,WebAuthn 标准被 W3C 以建议的形式发布,它是 FIDO 联盟下 FIDO2 的核心组件,旨在减少人们对于密码的依赖。


它带了以下三个好处:



  • 消除密码依赖: 通过使用更加安全的认证方式,例如生物识别技术 (指纹、面部识别) 或安全密钥,消除用户对密码的依赖,降低密码泄露和网络钓鱼攻击的风险。

  • 提升用户体验: 简化登录流程,无需记忆和输入复杂的密码,只需轻触指纹或插入安全密钥,即可完成身份验证。

  • 增强安全性: 使用公钥加密技术,确保用户的认证信息不会被窃取或伪造,有效抵御网络攻击。


如果大家有在 Mac 上的 Safari 浏览器登录苹果账号的经历,就会发现它不需要你输入密码,只需要一次简单的指纹验证:


Image.png


这时你通过验证你的指纹就可以顺利登录成功,这就是基于 WebAuthn 标准的 Passkeys。


目前苹果、谷歌、微软等几乎所有大厂都支持了Passkeys,,由于它也是一个 W3C 标准,所以你可以通过这个网站查看支持列表。


看起来指纹验证就像开头我们说过的 MFA,但是它比 MFA 多了一个东西就是设备,通过生物信息 + 受信设备的方式完成了它的整个认证流程,它拥有两个比较大的特点:



  • 提供了一套标准化的用户界面和用户体验,简化了无密码登录的操作流程。

  • 将用户的登录凭证 (私钥) 存储在用户的设备 (例如手机、电脑) 中,并通过云端服务进行同步,方便用户在不同设备上登录。


说回我们开头的 Github 的 2FA,其实 Github 也接入了它,如果你完成 2FA 之后,之后就可以在浏览器中通过指纹验证登录。


身份认证的未来已来,无密码的出现,为我们在登录授权流程中带来了许多方便~




好了,以上就是本篇文章的全部内容了,希望大家多多点赞支持,我将更快提供更好更优质的内容。


注:本文小标题是借助 AI 能力起的,部分描述也借助了 AI 美化,AI 美化生成内容不会超过 300 字(本文 4000 字),请大家放心食用。


作者:和耳朵
来源:juejin.cn/post/7364764922339065890
收起阅读 »

职场贩卖焦虑,不如调整心态

每当长假期来临之际,出行就变得异常困难,本来是该放松心态的阶段,因为各种各样不好的事情充斥在周围,败坏了本该愉悦的心情,五一还未出行,调补假期的事情就吵的沸沸扬扬,机票的大跌也带来背刺,这个假期注定从焦虑开始,接下来大家还会面对,路上的堵车、景区的人从众、天价...
继续阅读 »
image.png

每当长假期来临之际,出行就变得异常困难,本来是该放松心态的阶段,因为各种各样不好的事情充斥在周围,败坏了本该愉悦的心情,五一还未出行,调补假期的事情就吵的沸沸扬扬,机票的大跌也带来背刺,这个假期注定从焦虑开始,接下来大家还会面对,路上的堵车、景区的人从众、天价的消费,住店的隐私及背刺、以及可能遭遇的不好的天气等等等等;


啊~~~ 难道今天是要来贩卖焦虑吗?


贩卖焦虑吗?其实不然,我更想聊,大环境的变化,如何去调整自身?


大环境


互联网大环境,说说我自身的感受;


加班


加班,相比几年前,感受最深的是,或者说免费加班,越来越多;好像演变成了这家公司好像不搞点 996,都不是互联网公司,不搞点 996,这家公司都不赚钱,不搞点 996 ,这家公司都没有上升空间;


面试很多公司最少都是 995 起步,搞得候选人不接受,好像就吃不苦、没有奋斗精神一样,也许也是,不然为什么 79 的眉笔都会牵涉出工作不努力;但是想过没有,这可能真的会让人认知不够清晰的同学真的去怀疑自己是不是真的不够努力,也真的会让努力的人寒心;


并不是每个人都是幸存者偏差,成功真的不会降临在每个人身上,现实是大部分人不是出生在罗马,而是不是骡就是马,天赋加机遇,各种契机的组合,也许都是百万分之一的幸存率,真的不是努力了,就能有结果的;


但中国人骨子里优良的品质,从日出而作日入而息,自古以来的勤奋,就在基因里面代代传承,现实社会,别说出人头地,能够较好生存都已经不容易了,所以真的,社会多一些宽容,企业多一点人性,如果可以,真的要好好审视现状;


加班真的能带来更好的营收吗?也许可能吧,但是被禁锢的灵魂,你想要他能有多大自驱力,能有多大创造力,能看到前景吗?


内卷


人口大国,求职者甚众,听过最多也是最讽刺的话,你不干多的是有人干,是的,我们每个人都是备胎,我们每个人备胎都很多;


我不知道是从哪里看过一篇言论,大概就是说,程序员这个职业体系,其实大部分是没有什么背景的人,或者是农村人,他们没有好的出路,想要挣钱,想要快速走出原来的贫困圈,他没有太多的选择,他只能苦干;


很讽刺是不是,也许上面的话是被人断章取义的,但是我来自农村,我的感受确实大差不差,你以为干着一份体面的工作,其实在背地里都是牛马,而你没有办法去反驳,因为你没有背景,你要出圈;


所以细细想来,嘈杂的环境,加剧了内卷的诞生,一群本不该竞争的人,一群本该一起取暖的人,一群本来有着共同理想的人,逐渐变成了相互的垫脚石,kpi 的丑态,已经让很多人变成了死道友不是贫道的淡漠。


PUA


PUA,也许是自嘲的人或者不良的氛围或者玩梗,PUA 已经变成日常用语,『牛马们』 开始觉醒,原本作为管理者的手段,上位者心照不宣,突然被公之于众,大家都逐渐去理解你说的话,背后的含义是什么;


『我对你是失望的』『你在低绩效里面是靠前的』『机会已经给到你了,接下来就看你能不能把握了』『我知道你有压力,但你要扛得住压力』


是的,每个人都很珍惜自己的工作,每个人都有理想,每个人都想名利双收,利用人性的欲望去做管理,从本身来说没有什么问题,但是利用人性的弱点,去打压,去PUA,真的不病态吗?


小结


大环境确实存在问题,社会在发展,人类在进步,在快速发展的历程中,总会有一些病态的体系出现,但是在面对这些不定因素的时候,时刻保持理性的头脑,要学会去洞察,要保护自己,在做每一件事情上,不求完美,但求心安,不做别人的摆布工具,做好自我,坚守自我;


关于自我


社会立足,自我认知很重要


职业规划


和很多很多的同学聊过,经历过很多阶段的同学,实习生、初级、中级、高级、资深、专家等等,走技术架构的、走业务架构的、走技术管理的等等;


大部分的同学,其实都不具备职业规划的能力,甚至没有概念,这种真的会很盲目,没有目标、没有方向,一家一家公司换,随性,自由,看起来潇洒、有魄力;


薪资,当然是第一考虑因素,一切不聊到手薪资的都是耍流氓,但是真的不是唯一考虑因素,每个阶段,真的要知道自己需要的是什么,多去看看相关职业规划的书或者前辈的实际历程,当然不要去轻易质疑一个职业人生,理性看待每一件事;


当我们有意识的去按照自己想要走到路线的时候,薪资,我希望不是唯一的理由,当然他依然很重要,他依然是第一要素,不然工作为了什么?为了理想,丢掉面包吗?大可不必,除了自我感动,没有人会为你喝彩;


话说回来,职业规划,一定要重视,他不是今天的目标,也不是你今年的目标,他是你实现你人生目标的一个重要规划之一,每个阶段可能会变,变,很正常,不用自我怀疑,随着你见识的增长,经济的增长,你眼界会不一样,而我们要求的就是不断的是调整规划,为每个阶段的目标去准备,但是在快速奔走的过程中,也请回过头,看看自己的初心;


强大自我


本身我自己是前端,虽然现在不是纯粹的前端开发,但是从来没放弃过对前端技术的关注;


前端很乱,很快,很浮躁,『前端娱乐圈』 也不是一朝得名,很多时候,我也很焦虑,在做开发的时候,我对技术理解很纯粹,就希望用更好的方式去实现,更牛的技术去开发,但是随着了解的越多,越觉得需要了解的也越多,所以在很长的一段时间里,每天我都会查阅大量的资料,为了搞懂一个技术点,可能会去看一整本书,或者几十片技术文章;


我记得有一段时间,我甚至有点偏执,一段代码,甚至项目周期都过去一大半了,我突然发现实现的方式不是很好,很别扭,我直接把所有代码都删掉,甚至包括牵连的老代码,删掉一时爽,接下来就是无尽的痛苦,007 等着你;


换成现在,我不知道还会不会做这种偏执的事情,但是我想,不顺眼的,我照样还是会去优化,对技术的执着,我个人倒是觉得没有太大问题,但是前提别耽误项目进度,否则团队的信任会越来越低;


还有一个阶段,我觉得自己现状已经不是自己想要的了,接受降薪资,去一家可能会给我职业生涯带来有利成长的公司,人们没有办法当即去评判当时做的选择是对或者是错,事后看来,在做评价,已经没有太多意义,人生很长,也是到迟暮之年,这也许也是一段有着非同意义的经历;


我只是站在我自己的角度,去看待或者去如何强大自我,在这个过程中,有偏执,有耽误,也有背离,但是更多的是我自身对自身的调整,不能说我自己做的如何好,只能说,我是一直向着自身变好的方向努力;


关于焦虑


从我个人角度看,职业生涯中,已知和未知的事情太多,初期你会为了自己不够强大而焦虑,到后来你会发现你掌握很多东西,但是依然有很多盲区,技术很广,很难做到都了解;每个阶段都有每个阶段,或者每个环境需要有每个环境掌握的东西;有盲区,也请不要焦虑;


看过余华和罗翔的有意思一个对话:



  • 罗翔问余华老师:给女主人公起名叫林红,是取自南唐后主李煜的那个林花谢了春红,太匆匆,长恨朝来寒雨晚来风,胭脂泪,留人醉。自是人生长恨水长东吗?

  • 余华老师:我真没想这么多,我就是随便起的,现在是了。


你看,作者可能都没想那么多,如果说写的时候,每个都去考究,也许一篇小说,可以写几十年,也许余华老师就是取名字的时候,看到院子前面的林子里面的花红了,就叫林红,别去揣测太多,毕竟*『尤雨溪』*面试 vue 都不见得能通过;


我们能做的就是做好准备,拒绝内耗,理性看待,做每个阶段发力;


最后


环境、自身以及不可控因素,就像假期出行一样,你永远无法去预料即将发生的事情,如果被牵着走,你会发现很多事情,在没有开始之前,就已经寸步难行,不如调整心态,强大自我,无论是职场工作,还是生活,强大的自我,懂得适度调整,所遇之事,可能都会变得不一样,毕竟爱笑的程序员,代码肯定都不会写的很差;


作者:深谷逸风
来源:juejin.cn/post/7363193808522477607
收起阅读 »

background简写,真细啊!

web
背景原因 今天写需求,需要使用background简写属性,心想这还不简单吗,真男人写样式只需要两秒: background: url('./bg.png') no-repeat center contain ; 搞定! 上面设置的依次是 背景图片 背...
继续阅读 »

背景原因


今天写需求,需要使用background简写属性,心想这还不简单吗,真男人写样式只需要两秒:


background:  url('./bg.png') no-repeat center contain ;

搞定!


上面设置的依次是 背景图片 背景平铺模式 背景位置 背景图片是保有其原有的尺寸还是拉伸到新的尺寸。


so easy~


看我ctrl + s 保存代码,编译。


嗯? 怎么不生效? 俺的背景呢?
打开控制台一看,好家伙,压根没生效:


image.png


问题排查


第一反应是这些属性有固定顺序,但是凭我练习两年半的经验,不应该啊,之前也是这样用的啊,遂打开MDN,仔细翻阅....


发现了下面这段话:


image.png


这让我更加确信 写的没毛病啊!!


background-attachment、background-color、background-image、background-position、background-repeat、background-size
这些属性可以以任意顺序书写。


见了鬼了,待我排查两小时(摸鱼...)


原因浮现


在仔细阅读文档后发现,其实在文档的上面,还有另外一段话:


image.png


我恍然大悟,索嘎,以后看文档不能马虎了,得仔细查阅,过于经验主义了,这都是细节啊!


background使用注意事项和总结


其实,使用background时,大部分时候 属性的顺序是可以任意位置书写的,
但是有两个属性有点特殊,那就是background-size和background-position,


当background简写同时有这两个属性时,那么必须background-position在前,background-size在后,且两者只能紧挨着书写并且以 "/"分隔。
例如:


错误: background: url('./bg.png') no-repeat center  contain ; // 没有以 "/"分隔
错误: background: url('./bg.png') center no-repeat contain ; // 没有紧挨着书写
错误: background: url('./bg.png') no-repeat contain / center; //background-size写在了 background-position的前面

正确: background: url('./bg.png') no-repeat center / contain ;


写在最后


其实MDN在关于background的文档最开头的例子中就有写:


image.png


只不过没有用语言描述出来,一般没有认真看很难发现,所以有时候能够静下心来认真查阅文档,真的会发现很多细节(甩锅:这tm是谁写的文档,出来挨打).


作者:可狗可乐
来源:juejin.cn/post/7234825495333158949
收起阅读 »

JavaScript 流程控制语句详解:if语句、switch语句、while循环、for循环等

JavaScript,作为一种广泛使用的编程语言,它的流程控制语句是构建逻辑和实现功能的基础。流程控制语句包括条件语句、循环语句和转向语句,它们是编程中不可或缺的部分。接下来,我们将一一解析这些语句,带你走进JavaScript的世界。一、什么是流程控制语句流...
继续阅读 »

JavaScript,作为一种广泛使用的编程语言,它的流程控制语句是构建逻辑和实现功能的基础。流程控制语句包括条件语句、循环语句和转向语句,它们是编程中不可或缺的部分。

接下来,我们将一一解析这些语句,带你走进JavaScript的世界。

一、什么是流程控制语句

流程控制语句是用来控制程序中语句执行顺序的语句,它们可以影响程序的流程,从而实现不同的逻辑。流程控制语句主要分为以下三类:
Description

顺序结构: 这是最基本的流程控制,代表代码按照书写的顺序从上到下依次执行。通常程序都是从第一行代码开始顺序执行到结束的。

选择结构: 用于根据特定条件来控制代码的执行路径。常见的选择结构包括if、else、if-else if和switch等。这些语句允许程序在满足某些条件时执行特定的代码块,而在其他条件下执行另外的代码块或跳过某些代码。

循环结构: 用于重复执行某段代码直到满足退出条件为止。循环语句包括for、foreach、while和do-while等。通过这些语句,可以实现固定次数的循环或者当某个条件成立时的持续循环。

此外,还有跳转语句如break、continue和return等,它们可以改变正常的控制流程,例如跳出当前循环或者返回函数的结果。

二、条件判断语句

使用条件判断语句可以在执行某个语句之前进行判断,如果条件成立才会执行语句,条件不成立,则语句不执行。

语法一:if(条件表达式){语句…};

执行流程:
if语句在执行时,会先对条件表达式进行求值判断,

  • 如果条件表达式的值为true,则执行if后的语句,

  • 如果条件表达式的值为false,则不会执行if后的语句if语句只能控制紧随其后的那个语句。

如果希望if语句可以控制多条语句,可以将这些语句统一放在代码块中,如果就一条if语句,代码块不是必须的,但在开发中尽量写清楚。

代码演示:

<script>
if(true) console.log('好好学习,天天向上');
// 加上条件运算符 && ||
var a=20;
if(a>10&&a<=20){
alert('a在10-20之间');
alert("4567")
}
</script>

语法二:if…else…语句

语法:

if(条件表达式){
语句....
}else{
语句....
}

执行流程:
当该语句执行时,会先对if后的条件进行判断,

  • 如果该值为true,则执行if后的语句,

  • 如果该值为false,则执行else后的语句,两者选其一执行。

语法三:if…else if…else

语法:

if(条件表达式){
语句....
}else if(条件表达式){

语句....
}else{
语句....
}

执行流程:
当该语句执行时,会从上到下依次对条件表达式进行求值,

  • 如果值为true,则执行当前语句。

  • 如果值为false,则继续向下判断,如果所有的条件都不满意,就执行最后一个else或者不执行,该语句中,只会有一个代码块被执行,一旦代码块执行了, 则直接结束语句。

<script>
var age=16;
/* if(age>=60){
alert("你已经退休了~~~")
}else{
alert("你还没退休~~~")
} */


if(age>=100){
alert("您老高寿呀~~~");
}else if(age>=80){
alert("你也不小了");
} else if(age>=60){
alert("你刚退休呀~~~");
}else if(age>=30){
alert("你已经中年了");
}else if(age>=17){
alert("你刚成年呀~~~");
}else{
alert("你还是个小孩子~~")
};
</script>

三、条件分支语句

switch语句是一种多分支选择结构,它可以根据表达式的值,来选择执行不同的代码块。

语法:switch…case…

switch(条件表达式){
case 表达式:
语句....
break;
case 表达式:
语句....
break;
default:
语句...
break;
}

执行流程:

在执行时,会依次将case后的表达式的值和switch后的条件表达式的值进行全等比较。

  • 如果比较结果为true,则从当前case处开始执行代码,当前case后的所有代码都会执行;

  • 在case的后边跟着一个break关键字,这样可以确保只会执行当前case后的语句,而不会执行其他的case;

  • 如果比较结果为false,则继续向下比较;

  • 如果所有的比较结果都为false,则只执行default后的语句;

注意: switch语句和if语句的功能实际上有重复的,使用switch可以实现if的功能,同样使用if也可以实现switch的功能,所以我们使用时,可以根据自己的习惯选择。
代码演示:

<script>
var num=2;
switch(num){
case 1:
console.log("壹");
//使用break可以退出switch语句
break;
case 2:
console.log("贰");
break;
case 3:
console.log("叁")
break;

default:
console.log("非法数字~~~");
break;
}
</script>

四、循环语句

循环语句,就是让某段代码反复执行。在JavaScript中,主要有for循环、while循环、do…while循环等。

1) while循环

语法:

while(条件表达式){
语句
}

while语句执行流程:

先对条件表示式进行求值判断,如果值为true,则执行循环体,循环体执行完毕以后,继续对表达式进行判断,如果值为false,则终止循环。

2) do…while循环

语法:

do{

语句....

}while(条件表达式)

执行流程:

do…while 语句在执行时,会先执行循环体,循环体执行完毕后,再对while后的条件表示式进行判断,如果结果为true,则继续执行,执行完毕继续判断,如果结果为false,则停止执行。

注意: 实际上以上两个语句功能类似,不同的是while 是先判断后执行,而do…while会先执行后判断,do…while可以保证循环体至少执行一次,而while不行。


想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!点这里前往学习哦!

3)for语句(for循环)

在for循环中,为我们提供了专门的位置,用来放三个表达式。

  • 初始化表达式
  • 条件表达式
  • 更新表达式

for循环的语法:

for(初始化表达式;条件表达式;更新表达式){

语句....

}

for循环的执行流程:

  • 初始化表达式,初始化变量(初始化表达式,只会执行一次);

  • 条件表达式,判断是否执行循环;

  • 如果为true,则执行循环,如果为false,终止循环;

  • 执行更新表达式,更新表达式执行完毕继续重复。

<script>
//第一种写法
for(var i=0;i<10;i++){
alert(i);
}
//第二种写法 for循环中的三个部分都可以省略,也都可以写在外部
var i=0;
for(;i<10;){
alert(i++);
}

//如果在for循环中,不写任何的表达式,只写两个;
//此时循环是一个死循环,会一直执行下去,慎用
for(;;){
alert("hello");
}
</script>

五、break和continue语句

break关键字

可以用来退出switch或循环语句,不能在if语句中使用break和continue,break关键字,会立即终止离它最近的那个循环语句。

continue关键字

可以用来跳过当次循环,同样continue也是默认只会对离它最近的循环起作用。

终止指定循环

可以为循环语句创建一个label(标签),来标识当前的循环。

语法:

label(给起的标签名字):循环语句

使用break语句时,可以在break后跟着一个label,这样break可以结束指定的循环,而不是最近的。

代码演示

<script>

/* for(var i=0;i<5;i++){
console.log(i);
//break;//用来结束for的循环语句,for只会循环一次
if(i==2){
break;//这个break是对整个for循环起作用的
}
} */


/* for (var i = 0; i < 5; i++) {
console.log("@外层循环" + i);
for (var j = 0; j < 5; j++) {
break;//只会结束离他最近的内层循环
console.log("内层循环" + j);
}
} */



/* outer: for (var i = 0; i < 5; i++) {
console.log("@外层循环" + i);
for (var j = 0; j < 5; j++) {
break outer; //指定结束外层的for循环
console.log("内层循环" + j);
}
} */



for (var i = 0; i < 5; i++) {
if (i == 2) {
continue;
}
console.log(i);
}
</script>

JavaScript的流程控制语句,就像是一把魔法棒,它能让我们的代码按照我们的意愿去运行。掌握了这些语句,我们就可以在编程的世界里自由翱翔。

希望这篇文章能帮助你更好地理解和使用JavaScript的流程控制语句,让我们一起在编程的道路上,探索更多的可能性。

收起阅读 »

前端使用a链接下载内容增加loading效果

web
问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。代码如下:// utils.jsconst XLSX = require('xlsx')// 将一个sheet转成最终的exce...
继续阅读 »
  1. 问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。
  2. 代码如下:
// utils.js
const XLSX = require('xlsx')
// 将一个sheet转成最终的excel文件的blob对象,然后利用URL.createObjectURL下载
export const sheet2blob = (sheet, sheetName) => {
sheetName = sheetName || 'sheet1'
var workbook = {
SheetNames: [sheetName],
Sheets: {}
}
workbook.Sheets[sheetName] = sheet
// 生成excel的配置项
var wopts = {
bookType: 'xlsx', // 要生成的文件类型
bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
type: 'binary'
}
var wbout = XLSX.write(workbook, wopts)
var blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' })
// 字符串转ArrayBuffer
function s2ab(s) {
var buf = new ArrayBuffer(s.length)
var view = new Uint8Array(buf)
for (var i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
return buf
}
return blob
}

/**
* 通用的打开下载对话框方法,没有测试过具体兼容性
* @param url 下载地址,也可以是一个blob对象,必选
* @param saveName 保存文件名,可选
*/
export const openDownloadDialog = (url, saveName) => {
if (typeof url === 'object' && url instanceof Blob) {
url = URL.createObjectURL(url) // 创建blob地址
}
var aLink = document.createElement('a')
aLink.href = url
aLink.download = saveName + '.xlsx' || '1.xlsx' // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
var event
if (window.MouseEvent) event = new MouseEvent('click')
else {
event = document.createEvent('MouseEvents')
event.initMouseEvent(
'click',
true,
false,
window,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null
)
}
aLink.dispatchEvent(event)
}

<el-button
@click="clickExportBtn"
>
<i class="el-icon-download"></i>下载数据
</el-button>
<div class="mongolia" v-if="loadingSummaryData">
<el-icon class="el-icon-loading loading-icon">
<Loading />
</el-icon>
<p>loading...</p>
</div>

clickExportBtn: _.throttle(async function() {
const downloadDatas = []
const summaryDataForDownloads = this.optimizeHPPCDownload(this.summaryDataForDownloads)
summaryDataForDownloads.map(summaryItem =>
downloadDatas.push(this.parseSummaryDataToBlobData(summaryItem))
)
// donwloadDatas 数组是一个三维数组,而 json2sheet 需要的数据是一个二维数组
this.loadingSummaryData = true
const downloadBlob = aoa2sheet(downloadDatas.flat(1))
openDownloadDialog(downloadBlob, `${this.testItem}报告数据`)
this.loadingSummaryData = false
}, 2000),

// css
.mongolia {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5rem;
color: #409eff;
z-index: 9999;
}
.loading-icon {
color: #409eff;
font-size: 32px;
}
  1. 解决方案探究:
  • 在尝试了使用 $nextTick、将 openDownloadDialog 改写成 Promise 异步函数,或者使用 async/await、在 openDownloadDialog 中添加 loadingSummaryData 逻辑,发现依旧无法解决问题,因此怀疑是 document 添加新元素与 vue 的 v-if 渲染产生冲突,即 document 添加新元素会阻塞 v-if 的执性。查阅资料发现,问题可能有以下几种:

    • openDownloadDialog 在执行过程中执行了较为耗时的同步操作,阻塞了主线程,导致了页面渲染的停滞。
    • openDownloadDialog 的 click 事件出发逻辑存在问题,阻塞了事件循环(Event Loop)。
    • 浏览器在执行 openDownloadDialog 时,将其脚本任务的优先级设置得较高,导致占用主线程时间片,推迟了其他渲染任务。
    • Vue 的批量更新策略导致了 v-if 内容的显示被延迟。
  • 查阅资料后找到了如下几种方案:

      1. 使用 setTimeout 使 openDownloadDialog 异步执行
      clickExport() {
      this.loadingSummaryData = true;

      setTimeout(() => {
      openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

      this.loadingSummaryData = false;
      });
      }


      1. 对 openDownloadDialog 内部进行优化
      • 避免大循环或递归逻辑
      • 将计算工作分批进行
      • 使用 Web Worker 隔离耗时任务
        • 在编写 downloadWorker.js 中的代码时,要明确这部分代码是运行在一个独立的 Worker 线程内部,而不是主线程中。

            1. 不要直接依赖或者访问主线程的全局对象,比如 window、document 等。这些在 Worker 内都无法直接使用。
            1. 不要依赖 DOM 操作,比如获取某个 DOM 元素。Worker 线程无法访问页面的 DOM。
            1. 代码执行的入口是 onmessage 回调函数,在其中编写业务逻辑。
            1. 和主线程的通信只能通过 postMessage 和 onmessage 发送消息事件。
            1. 代码应该是自包含的,不依赖外部变量或状态。
            1. 可以导入其他脚本依赖,比如用 import 引入工具函数等。
            1. 避免修改或依赖全局作用域,比如定义全局变量等。
        • 总之,编写 downloadWorker.js 时要注意它执行在一个独立环境中,只能通过消息事件与主线程通信。
    // 创建 Worker
    const worker = new Worker('downloadWorker.js');

    // 点击下载时向 Worker 发送消息
    function clickDownload() {

    showLoading();

    worker.postMessage({
    url: fileURL,
    filename: 'report.xlsx'
    });

    worker.onmessage = function(e) {
    // 收到下载完成的消息
    hideLoading();
    }

    }

    // 显示 loading
    function showLoading() {
    loading.style.display = 'block';
    }

    // 隐藏 loading
    function hideLoading() {
    loading.style.display = 'none';
    }

    // downloadWorker.js

    onmessage = function(e) {
    const url = e.data.url;
    const filename = e.data.filename;

    // 创建并点击链接触发下载
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    a.click();

    postMessage('下载完成');
    }

    <div id="loading" style="display:none;">
    Downloading...
    </div>

    <button onclick="clickDownload()">
    Download
    </button>

    <script src="downloadWorker.js"></script>


      1. 使用 requestIdleCallback 进行调度
      clickExport() {
      this.loadingSummaryData = true;

      requestIdleCallback(() => {
      openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

      this.loadingSummaryData = false;
      });
      }
    • 当使用上述方案时发现里面的内容变得异常的慢,于是去问GPT后,得到以下答案:

      使用 requestIdleCallback 来执行 openDownloadDialog 确实可能会需要更多时间。

      因为 requestIdleCallback 会在主线程空闲时才执行回调函数,所以不像 setTimeout 可以直接按指定时间异步执行。

      requestIdleCallback 的优先级较低,只在主线程空闲时才会执行,所以 download 的速度会比 setTimeout 慢一些。

      但使用 requestIdleCallback 的优点是它不会占用主线程时间,可以避免阻塞页面渲染。

      所以需要权衡执行速度和避免阻塞之间的关系:

      • 如果下载速度是优先考虑的因素,可以继续使用 setTimeout 异步执行。
      • 如果避免阻塞渲染更重要,则 requestIdleCallback 是更好的选择。

      偶尔下载任务被延迟不是大问题,但频繁的下载任务阻塞会严重影响页面体验。

      此外,可以结合两者试试:

      setTimeout 先异步调度,然后在回调中使用 requestIdleCallback 避免阻塞。这样就可以兼顾下载速度和避免渲染阻塞了。

    clickExport() {

    this.loadingSummaryData = true;

    setTimeout(() => {

    requestIdleCallback(() => {
    openDownloadDialog(downloadBlob);
    });

    this.loadingSummaryData = false;

    }, 200);

    }


      1. 分析线程模型,找到具体的阻塞点
      • 使用 Performance 工具分析线程
      • debugger 及 console.log 打印关键函数的执行时间
      • 检查是否有非主线程的任务阻塞了主线程
      1. 调整 vue 的批量更新策略
    new Vue({
    config: {
    // 修改批量更新的队列长度
    batchUpdateDuration: 100
    }
    })



作者:李卫泽
来源:juejin.cn/post/7268050036474609683
收起阅读 »

给迷茫的朋友一点建议吧,主要是前端方向的。

背景 前两天分享了一篇自己的个人经历,没想到引来了那么多人共鸣,很多人留下了评论,大部分评论我都认真看了,并且也回复了,有的评论建议真的很棒,对我的帮助很大,很感谢。 今天给评论给整理了一下,再加上自己有一些新的感悟,给大家分享分享,给迷茫的朋友一点建议。有很...
继续阅读 »

背景


前两天分享了一篇自己的个人经历,没想到引来了那么多人共鸣,很多人留下了评论,大部分评论我都认真看了,并且也回复了,有的评论建议真的很棒,对我的帮助很大,很感谢。


今天给评论给整理了一下,再加上自己有一些新的感悟,给大家分享分享,给迷茫的朋友一点建议。有很多东西想说,但是表达能力有限,只能写出一点。


一个30岁老前端的人生经历(学习+工作+婚姻+孩子),给迷茫的朋友一点激励。前端是不是真的不行了

有些没有毕业或者刚毕业的兄弟们,很关心这个问题,因为关乎他们是否要学前端开发。还有一些想转前端开发的兄弟,也很关心这个问题。


关于这个问题,上篇文章有评论开玩笑的说现在入前端,相当于49年入guojun,下面就这个问题我发表一下我的个人看法。


先说一下我的观点,前端确实比以前难很多,工作岗位变少,薪资变低,技术要求也变高了,还没入或准备入前端的,建议真的不要入了,有经验的都不好找工作,更别说没工作经验的了,当然天才除外。


我同学和我朋友两个前端(都是好几年工作经验的)找了几个月都没有合适的工作,面试机会还是有的,但是外包偏多,并且还要降薪,最后都拒了。


他们还发现了一个现象,有时候面试的过程中整体都非常好,hr有要发offer的意思,但是就一直拖着,然后后面突然有一天说不合适了。这种应该是有更好的选择了,以前都是求职者手握多个offer,去挑选公司,现在公司占据主动了,从多个候选人中挑员工了。


后端情况我不清楚,但是前端真的没有以前好就业了,这个时候想入前端的,我真的不太建议,除非你真的特别牛,就算你很牛,但是没有面试机会,也没办法证明啊。


有些已经入前端的新人私信我,让我给他一些提升建议。



  1. 多写多练,熟能生巧,没有别的好办法。先不用去看某某某源码,也不用花很多时间去深入的了解某个库,会用就行,然后从网上找个实战项目跟着做,或者自己从零做个小项目,在实战的过程中可以快速定位到自己薄弱的地方,然后针对自己的问题查漏补缺就行了。我当时从后端转前端时候就是这样的,不一定对所有人都适用,仅供大家参考。

  2. 多总结和复盘。这个习惯让我一直受益,比如今天的文章就是对上一篇文章评论的总结。经常复盘真的能让学习效率变高很多的,还能避免犯同样的错。我的习惯是白天学东西,晚上睡觉前复盘总结一下,这样一来印象很深刻。

  3. 学习的时候,不要怕别人说卷什么的,有些人看到别人学习就说别人卷,反过来自己偷偷的学,这种人就是不想别人追赶上自己。还有一些人自己天天下班打游戏从来不学习,看到同事学习,就在背后说别人卷,这种人就是害怕被别人甩开。为了某个目标去奋斗,我觉得是一件很有意义的事情。比如我吧,天赋一般,家庭一般,就是靠比别人付出的多,才能让一家人过上好一点的生活,只要家人开心,”卷“一点又有什么呢,随别人说去吧。

  4. 定目标。这个对于自律性不够的兄弟有帮助,相信很多人看完某个鸡汤文后,努力个几天,就坚持不下去了,这就是因为没有目标,有时候定完目标还是坚持不下去,可以找人来监督一下。比如我在上篇文章中立的flag,明年写40篇文章,有评论说写质量高的文章,一年肯定写不了那么多。是的,好文章确实不是硬挤出来的,我立这个flag的原因,是我知道自己自律不够,立出来让大家监督的,至于文章质量,我觉得只要用心去写,能帮助到人而不误导人就行了。


小结


没入前端的同学,建议别入了。刚入前端的同学想快速提升自己,多学多练,多总结和复盘,不要怕被别人说卷,给自己定目标。


30岁前端的路


我在flag里写了打算2024年认真看一下react源码,很多大佬建议说现在别搞这些没意义的东西了,搞前端死路一条。


关于大龄前端发展,从评论建议中,我总结了以下几条常用出路,欢迎大家补充。


深入技术


都说前端的天花板就是组长,结合我自身的经历和评论区的留言来看好像确实是这样,我几年前就开始做前端负责人了,其实就是组长,只是管的人多少的问题,cto永远是后端。主要工作就是需求来了,把任务给下面人分一分,就没了,最有挑战的也就是框架基建了吧。


评论里有人说,如果想要继续搞技术的话,可以往全栈方向发展,因为前端很难涉及到公司业务,所以可替代性很高,后端和公司业务结合的比较深一点,而业务才是公司的根本,换一个人很难立马上手,可替代性比前端低很多。


关于全栈我的观点是,大龄前端还是往全栈发展比较好,除了上面的原因外,全栈可以自己做个产品,还可以接私活、做自由职业者,甚至转管理都需要懂点后端技术。


想转全栈,但是又不想学别人语言,node也是可以做后端开发的,现在比较成熟的框架有midway和nest,甚至koa都可以做后端开发。关注我的人应该知道我正在做的fluxy-admin就是一个全栈项目,前端使用的是react,后端使用的是midway,前端上手midway还是很简单的,我一个星期就上手写项目了。


转管理


有一些评论提到了转管理,说实话这个是需要机会的,不是所有人想转就转的,因为公司很少愿意给员工尝试的机会,除非你开发的时候已经表现出了管理方面的天赋,让老板认可你,不然都很难转成功,一是一个萝卜一个坑,老的领导没走你就上不去,而上面领导走了,很多公司可能会从外面招一个新的人空降过来,而不是从下面提拨一个上去。


还有管理也不是什么人都可以做的,有的人搞技术可以,管理真的不太行,比如我,在某家公司,当时管着10几个前端,团队被我带的一团糟,后来我找个副组长帮我管理,我专心搞技术搞框架,团队才慢慢走上正规。


个人觉得想转管理,需要机会和自身有管理才能,这两个条件都很重要。


转产品


有些评论还提到了转产品,我感觉这个还是比较靠谱的,玉伯大佬就是一个成功转型的案例。


个人感觉前端转产品还是有一些优势的,下面是我从网上搜的:



  1. 技术背景:掌握技术知识,能更好地理解产品实现的可能性与限制,与技术团队有效沟通。

  2. 用户体验关注:作为前端开发者,通常会对用户界面和用户体验有较深的理解,有助于设计更好的产品。

  3. 敏捷与迭代理念:熟悉敏捷开发流程和迭代思维,能够快速响应市场变化并调整产品方向。

  4. 问题解决能力:编程经验锻炼了解决复杂问题的能力,这在产品管理中也非常重要。

  5. 沟通桥梁:作为曾经的开发人员,可以成为团队内部技术和非技术成员之间的沟通桥梁。


前端转产品,可以将技术理解和用户体验融入产品管理中,提高产品的竞争力。感觉门槛也不高,只需要有想法,并且现在软件行业那么发达,实在想不到好的点子,还可以借鉴别人产品设计。


甚至以后AI发展的很成熟了,可能就不需要开发了,只需要产品经理把需求描述好,产品就自动生成出来了。


我现在也在往这方面发展,最近一年在公司主导研发低代码平台,从最开始的设计到基本构架实现都是我一个人,框架成熟后,经常和交付直接交流,渐渐往产品的角色发展了。现在工作中不只是只有代码了,经常也会站在产品的角度去思考,做的东西怎么才能更有价值,怎么提高用户体验等。


平时有一些想法在公司产品里用不上,所以打算自己做一个产品,现在有了一些idea,明年应该会去做。这里说明一下,如果有想法想做个产品但找不到合适的后端合伙人,全栈做小应用或独立产品还是很有优势的,呼应了前面说的全栈优势。


搞副业


评论里也有很多人建议搞副业,这个确实也是一条出路,但是我感觉搞副业得有可靠的路子,不能盲目的去搞,不然最后可能血本无归。


我知道前几年有个程序员可以干的并且很好的副业,录视频卖课程,现在随着互联网新人越来越少,这个副业已经不行了。


现在我知道的有个副业还不错,在线帮别人修改简历,因为现在很多人找不到工作,甚至没有面试机会,他们会很焦虑,认为可能是自己简历的问题,会找一些大佬帮忙优化简历,搞这个的提前你得是个大佬,并且有一定的知名度,对于普通人不适用。


现在也有一些人趁着AI的热度,独立开发AI应用,靠这个赚了一些钱,我对这个不太了解,不做评价。


下面是一位大佬的评论,可能比较适合普通人,不过他说的那些我都没有认证,大家请谨慎对待:


image.png


转行


有很多评论认为搞技术没有前途,还是要考公,确实对于刚入行或准备入行的兄弟考公是一个比较好的选择,因为年轻还有很多机会,像我们这种年龄大的,还拖家带口的已经不适合改行了,成本太高了,真经不起折腾。


也有评论说想回老家,说实话我也想老家,但是以我的能力,回老家估计都养不活一家人,只会写代码,回老家能干啥呢。


最后


上面的内容是根据评论内容简单的整理了一下,还加了一些个人看法,大家可以结合自己的情况食用。可能还有更多好的建议因为我文笔有限,没写出来,欢迎大家评论,一起交流。


最后说一下自己吧,没写文章之前,我一直认为作为一个程序员技术最重要的,就像我的简介里说的那样,我唯一会的就是写代码,我不一定会成功,但是我会努力做的更好。看完大家的评论后,内心有过动摇,是不是不应该继续卷技术了,在写这篇文章的时候,我又坚定了,因为搞技术不只是我的工作,这也是我热爱的东西,即使有一天被迫失业不做程序员了,我可能偶尔还会写写代码。不过鸡蛋不能放在一个篮子里的道理我还是知道的,我现在已经在慢慢往产品转,用以保证未来不失业,因为还有一大家子需要我来养啊。共勉。


作者:前端小付
来源:juejin.cn/post/7311932190442635299
收起阅读 »

JSON慢地要命: 看看有啥比它快!

是的, 你没听错! 网络开发中无处不在的数据交换格式JSON, 可能会拖慢你的应用程序. 在这个速度和响应速度至上的世界里, 检查 JSON 的性能影响至关重要, 而我们对此却常常忽略. 在本博客中, 我们将深入探讨 JSON 成为应用程序瓶颈的原因, 并探索...
继续阅读 »


是的, 你没听错! 网络开发中无处不在的数据交换格式JSON, 可能会拖慢你的应用程序. 在这个速度和响应速度至上的世界里, 检查 JSON 的性能影响至关重要, 而我们对此却常常忽略. 在本博客中, 我们将深入探讨 JSON 成为应用程序瓶颈的原因, 并探索更快的替代方案和优化技术, 以确保你的应用程序以最佳状态运行.


JSON 是什么? 为何我要关注这个问题?



JSON 教程 | w3resource


JSON是JavaScript Object Notation的缩写, 是一种轻量级数据交换格式, 已成为Web应用中传输和存储数据的首选. 它的简洁性和人类可读格式使人类和机器都能轻松使用. 但是, 为什么要在Web开发项目中关注 JSON 呢?


JSON 是应用中数据的粘合剂. 它是服务器和客户端之间进行数据通信的语言, 也是数据库和配置文件中存储数据的格式.


JSON 的流行以及人们使用它的原因…


JSON 在Web开发领域的受欢迎程度怎么强调都不为过. 它已成为数据交换的事实标准, 这其中有几个令人信服的原因:


它易于使用!



  1. 人类可读格式: JSON 使用简单明了, 基于文本的结构, 开发人员和非开发人员都能轻松阅读和理解. 这种人类可读的格式增强了协作, 简化了调试.

  2. 语言无关性: JSON 与任何特定的编程语言无关. 它是一种通用的数据格式, 几乎所有现代编程语言都能对其进行解析和生成, 因此它具有很强的通用性.

  3. 数据结构一致性: JSON 使用键值对, 数组和嵌套对象来实现数据结构的一致性. 这种一致性使其具有可预测性, 便于在各种编程场景中使用.

  4. 支持浏览器: 网络浏览器原生支持 JSON, 允许Web应用与服务器进行无缝通信. 这种本地支持极大地促进了 JSON 在Web开发中的应用.

  5. JSON API: 许多网络服务和应用接口默认以 JSON 格式提供数据. 这进一步巩固了 JSON 在Web开发中作为数据交换首选的地位.

  6. JSON Schema: 开发人员可以使用 JSON 模式来定义和验证 JSON 数据的结构, 从而为应用增加了一层额外的清晰度和可靠性.


鉴于这些优势, 难怪全球的开发人员都依赖 JSON 来满足他们的数据交换需求. 然而, 随着我们在本博客的深入探讨, 我们将发现与 JSON 相关的潜在性能挑战, 以及如何有效解决这些挑战.


速度需求


🚀🚀🚀


应用的速度和响应的重要性


在当今快节奏的数字环境中, 应用的速度和响应能力是不可或缺的. 用户希望在Web和移动应用中即时获取信息, 快速交互和无缝体验. 对速度的这种要求是由以下几个因素驱动的:



  1. 用户期望: 用户已习惯于从数字互动中获得闪电般快速的响应. 他们不想等待网页的加载或应用的响应. 哪怕是几秒钟的延迟, 都会导致用户产生挫败感并放弃使用.

  2. 竞争优势: 速度可以成为重要的竞争优势. 反应迅速的应用往往比反应迟缓的应用更能吸引和留住用户.

  3. 搜索引擎排名: 谷歌等搜索引擎将网页速度视为排名因素. 加载速度更快的网站往往在搜索结果中排名靠前, 从而提高知名度和流量.

  4. 转化率: 电子商务网站尤其清楚速度对转化率的影响. 网站速度越快, 转换率越高, 从而增加收入.

  5. 移动性能: 随着移动设备的普及, 对速度的需求变得更加重要. 移动用户的带宽和处理能力往往有限, 因此快速的应用性能是必要的.


JSON 会拖慢我们的应用吗?


现在, 让我们来讨论核心问题: JSON 是否会拖慢我们的应用?


如前所述, JSON 是一种非常流行的数据交换格式. 它灵活, 易用, 并得到广泛支持. 然而, 这种广泛的应用并不意味着它不会面临性能挑战.


某些情况下, JSON 可能是导致应用慢的罪魁祸首. 解析 JSON 数据的过程, 尤其是在处理大型或复杂结构时, 可能会耗费宝贵的毫秒时间. 此外, 低效的序列化和反序列化也会影响应用的整体性能.


在接下来的内容中, 我们将探讨 JSON 成为应用瓶颈的具体原因, 更重要的是, 探讨如何缓解这些问题. 在深入探讨的过程中, 请记住我们的目标不是诋毁 JSON, 而是了解其局限性并发现优化其性能的策略, 以追求更快, 反应更灵敏的应用.



LinkedIn将Protocal Buffers与Rest.li集成以提高微服务性能| LinkedIn工程


JSON 为什么会变慢


尽管 JSON 被广泛使用, 但它也难逃性能挑战. 让我们来探究 JSON 可能会变慢的原因, 并理解为什么 JSON 并不总是数据交换的最佳选择.


1. 解析带来的开销


当 JSON 数据到达应用时, 它必须经过解析过程才能转换成可用的数据结构. 解析过程可能相对较慢, 尤其是在处理大量或深度嵌套的 JSON 数据时.


2. 序列化和反序列化


JSON 要求在从客户端向服务器发送数据时进行序列化(将对象编码为字符串), 并在接收数据时进行反序列化(将字符串转换回可用对象). 这些步骤会带来开销, 影响应用的整体速度.


微服务架构的世界里, JSON 通常用于在服务之间传递消息. 但是, 很关键的是, 我们必须认识到, JSON 消息需要序列化和反序列化, 这两个过程会带来巨大的开销.



在有大量微服务不断通信的场景中, 这种开销可能会增加, 并有可能使应用变慢, 以至于影响用户体验.




我们面临的第二个挑战是, 由于 JSON 的文本性质, 序列化和反序列化的延迟和吞吐量都不理想.
— LinkedIn



1_74sQfiW0SjeFfcTcgNKupw.webp
序列化和反序列化


3. 字符串操作


JSON 基于文本, 在连接和解析等操作中严重依赖字符串操作. 与处理二进制数据相比, 处理字符串的速度会慢一些.


4. 缺乏数据类型


JSON 的数据类型(如字符串, 数字, 布尔值)非常有限. 复杂的数据结构可能需要效率较低的表示法, 从而导致内存使用量增加和处理速度减慢.



5. 冗余


JSON 的人类可读性设计可能会导致冗余. 不需要的键和重复的结构增加了有效载荷的大小, 导致数据传输时间延长.



第一个挑战是 JSON 是一种文本格式, 往往比较冗余. 这导致网络带宽使用量增加, 更高的延迟, 效果并不理想.
— LinkedIn



6. 不支持二进制


JSON 缺乏对二进制数据的本地支持. 在处理二进制数据时, 开发人员通常需要将其编解码为文本, 而这可能会降低效率.


7. 深度嵌套


在某些情况下, JSON 数据可能是深嵌套的, 需要递归解析和遍历. 这种计算复杂性会降低应用的运行速度, 尤其是在没有优化的情况下.


JSON的替代方案


虽然 JSON 是一种通用的数据交换格式, 但由于其在某些情况下的性能限制, 人们开始探索更快的替代格式. 让我们深入探讨其中的一些替代方案, 了解何时以及为何选择它们:


1. Protocol Buffers(protobuf)


Protocal Buffers通常被称为protobuf, 是由谷歌开发的一种二进制序列化格式. 它的设计宗旨是高效, 紧凑和快速. Protobuf 的二进制性质使其在序列化和反序列化方面的速度明显快于 JSON.



  • 何时选择: 当你需要高性能的数据交换时, 尤其是在微服务架构, 物联网应用或网络带宽有限的情况下, 请考虑使用Protobuf.


GitHub - vaishnav-mk/protobuf-example


2. MessagePack


MessagePack 是另一种二进制序列化格式, 以速度快, 结构紧凑而著称. 它比 JSON 更有效率, 同时与各种编程语言保持兼容.



  • 何时选择: 当你需要在速度和跨语言兼容性之间取得平衡时, MessagePack 是一个不错的选择. 它适用于实时应用和对减少数据大小至关重要的情况.


3. BSON (二进制 JSON)


BSON 或二进制 JSON 是一种从 JSON 衍生出来的二进制编码格式. 它保留了 JSON 的灵活性, 同时通过二进制编码提高了性能. BSON 常用于 MongoDB 等数据库.



  • 何时选择: 如果你正在使用 MongoDB, 或者需要一种格式来弥补 JSON 和二进制效率之间的差距, 那么 BSON 是一个很有价值的选择.


4. Apache Avro


Apache Avro 是一个数据序列化框架, 专注于提供一种紧凑的二进制格式. 它基于schema, 可实现高效的数据编解码.



  • 何时选择: Avro 适用于schema演进非常重要的情况, 如数据存储, 以及需要在速度和数据结构灵活性之间取得平衡的情况.


与 JSON 相比, 这些替代方案提供了不同程度的性能改进, 具体选择取决于你的具体使用情况. 通过考虑这些替代方案, 你可以优化应用的数据交换流程, 确保将速度和效率放在开发工作的首位.



JSON, Protobufs, MessagePack, BSON 和 Avro 之间的差异


每个字节都很重要: 优化数据格式


在效率和速度至上的数据交换世界中, 数据格式的选择会产生天壤之别. 本节将探讨从简单的 JSON 数据表示到更高效的二进制格式(如 Protocol Buffers, MessagePack, BSON 和 Avro)的过程. 我们将深入探讨每种格式的细微差别, 并展示为什么每个字节都很重要.


开始: JSON 数据


我们从简单明了的 JSON 数据结构开始. 下面是我们的 JSON 数据示例片段:


{
"id": 1, // 14 bytes
"name": "John Doe", // 20 bytes
"email": "johndoe@example.com", // 31 bytes
"age": 30, // 9 bytes
"isSubscribed": true, // 13 bytes
"orders": [ // 11 bytes
{ // 2 bytes
"orderId": "A123", // 18 bytes
"totalAmount": 100.50 // 20 bytes
}, // 1 byte
{ // 2 bytes
"orderId": "B456", // 18 bytes
"totalAmount": 75.25 // 19 bytes
} // 1 byte
] // 1 byte
} // 1 byte

JSON 总大小: ~ 139 字节


虽然 JSON 用途广泛且易于使用, 但它也有一个缺点, 那就是它的文本性质. 每个字符, 每个空格和每个引号都很重要. 在数据大小和传输速度至关重要的情况下, 这些看似微不足道的字符可能会产生重大影响.


效率挑战: 使用二进制格式减小尺寸



现在, 让我们提供其他格式的数据表示并比较它们的大小:


Protocol Buffers (protobuf):


syntax = "proto3";

message User {
int32 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
bool is_subscribed = 5;
repeated Order orders = 6;

message Order {
string order_id = 1;
float total_amount = 2;
}
}

0A 0E 4A 6F 68 6E 20 44 6F 65 0C 4A 6F 68 6E 20 44 6F 65 65 78 61 6D 70 6C 65 2E 63 6F 6D 04 21 00 00 00 05 01 12 41 31 32 33 03 42 DC CC CC 3F 05 30 31 31 32 34 34 35 36 25 02 9A 99 99 3F 0D 31 02 42 34 35 36 25 02 9A 99 99 3F

Protocol Buffers 总大小: ~ 38 bytes


MessagePack:


(注意:MessagePack 是一种二进制格式, 此处的表示法非人工可读.)


二进制表示(十六进制):


a36a6964000000000a4a6f686e20446f650c6a6f686e646f65406578616d706c652e636f6d042100000005011241313302bdcccc3f0530112434353625029a99993f


MessagePack 总大小: ~34 字节


BSON (二进制 JSON):


(注意:BSON 是一种二进制格式, 此处的表示法非人工可读.)


二进制表示法 (十六进制):


3e0000001069640031000a4a6f686e20446f6502656d61696c006a6f686e646f65406578616d706c652e636f6d1000000022616765001f04370e4940

BSON 总大小: ~ 43 字节


Avro:


(注: Avro使用schema, 因此数据与schema信息一起编码.)


二进制表示法 (十六进制):


0e120a4a6f686e20446f650c6a6f686e646f65406578616d706c652e636f6d049a999940040a020b4108312e3525312e323538323539

Avro 总大小: ~ 32 字节



(这些替代方案的实际字节数可能会有所不同, 提供这些数字只是为了让大家有个大致的了解.)


现在你可能会感到奇怪, 为什么我们的程序会有这么多的字节数?


现在你可能想知道为什么有些格式输出的是二进制, 但它们的大小却各不相同. Avro, MessagePack 和 BSON 等二进制格式具有不同的内部结构和编码机制, 这可能导致二进制表示法的差异, 即使它们最终表示的是相同的数据. 下面简要介绍一下这些差异是如何产生的:


1. Avro:



  • Avro 使用schema对数据进行编码, 二进制表示法中通常包含该schema.

  • Avro 基于schema的编码可提前指定数据结构, 从而实现高效的数据序列化和反序列化.

  • Avro 的二进制格式设计为自描述格式, 这意味着schema信息包含在编码数据中. 这种自描述性使 Avro 能够保持不同版本数据模式之间的兼容性.


2. MessagePack:



  • MessagePack 是一种二进制序列化格式, 直接对数据进行编码, 不包含schema信息.

  • 它使用长度可变的整数和长度可变的字符串的紧凑二进制表示法, 以尽量减少空间使用.

  • MessagePack 不包含schema信息, 因此更适用于schema已预先知道并在发送方和接收方之间共享的情况.


3. BSON:



  • BSON 是 JSON 数据的二进制编码, 包括每个值的类型信息.

  • BSON 的设计与 JSON 紧密相连, 但它增加了二进制数据类型, 如 JSON 缺乏的日期和二进制数据.

  • 与 MessagePack 一样, BSON 不包含schema信息.


这些设计和编码上的差异导致了二进制表示法的不同:



  • Avro 包含模式信息并具有自描述性, 这导致二进制大小稍大, 但提供了schema兼容性.

  • MessagePack 因其可变长度编码而高度紧凑, 但缺乏模式信息, 因此适用于已知模式的情况.

  • BSON 与 JSON 关系密切, 包含类型信息, 与 MessagePack 等纯二进制格式相比, 会增加大小.


总之, 这些差异源于每种格式的设计目标和功能. Avro 优先考虑schema兼容性, MessagePack 注重紧凑性, 而 BSON 则在保持类似 JSON 结构的同时增加了二进制类型. 格式的选择取决于具体的使用情况和要求, 如schema兼容性, 数据大小和易用性.


优化 JSON 性能


JSON 虽然用途广泛, 在Web开发中被广泛采用, 但在速度方面也存在挑战. 这种格式的人类可读性会导致数据负载较大, 处理时间较慢. 因此, 问题出现了: 我们能够怎样优化JSON以使得它更快更高效? 在本文中, 我们将探讨可用于提高 JSON 性能的实用策略和优化方法, 以确保 JSON 在提供应用所需的速度和效率的同时, 仍然是现代 Web 开发中的重要工具.


以下是一些优化 JSON 性能的实用技巧以及代码示例和最佳实践:


1. 最小化数据大小:



  • 使用简短, 描述性的键名: 选择简洁但有意义的键名, 以减小 JSON 对象的大小.


// Inefficient
{
"customer_name_with_spaces": "John Doe"
}

// Efficient
{
"customerName": "John Doe"
}


  • 尽可能缩写:  在不影响清晰度的情况下, 考虑对键或值使用缩写.


// Inefficient
{
"transaction_type": "purchase"
}

// Efficient
{
"txnType": "purchase"
}

2. 明智地使用数组:



  • 最小化嵌套: 避免深度嵌套数组, 因为它们会增加解析和遍历 JSON 的复杂性.


// Inefficient
{
"order": {
"items": {
"item1": "Product A",
"item2": "Product B"
}
}
}

// Efficient
{
"orderItems": ["Product A", "Product B"]
}

3. 优化数字表示:



  • 尽可能使用整数:  如果数值可以用整数表示, 请使用整数而不是浮点数.


// Inefficient
{
"quantity": 1.0
}

// Efficient
{
"quantity": 1
}

4. 消除冗余:



  • 避免重复数据: 通过引用共享值来消除冗余数据.


// Inefficient
{
"product1": {
"name": "Product A",
"price": 10
},
"product2": {
"name": "Product A",
"price": 10
}
}

// Efficient
{
"products": [
{
"name": "Product A",
"price": 10
},
{
"name": "Product B",
"price": 15
}
]
}

5. 使用压缩:



  • 使用压缩算法:  如何可行的话, 使用压缩算法, 比如Gzip 或者Brotli, 以在传输过程中减少JSON负载大小.


// Node.js example using zlib for Gzip compression
const zlib = require('zlib');

const jsonData = {
// Your JSON data here
};

zlib.gzip(JSON.stringify(jsonData), (err, compressedData) => {
if (!err) {
// Send compressedData over the network
}
});

6. 采用服务器端缓存:



  • 缓存 JSON 响应:  实施服务器端缓存, 以便高效地存储和提供 JSON 响应, 减少重复数据处理的需要.


7. 剖析与优化:



  • 剖析性能:  使用剖析工具找出 JSON 处理代码中的瓶颈, 然后优化这些部分.



请记住, 你实施的具体优化措施应符合应用的要求和限制.



真实世界的优化: 在实践中加速


在这一部分, 我们将深入探讨现实世界中遇到 JSON 性能瓶颈并成功解决的应用和项目. 我们将探讨企业如何解决 JSON 的局限性, 以及这些优化为其应用带来的切实好处. 从 LinkedIn 和 Auth0 这样的知名平台到 Uber 这样的颠覆性科技巨头*, 这些示例为我们提供了宝贵的见解, 让我们了解在尽可能利用 JSON 的多功能性的同时提高速度和响应能力的策略.


1. LinkedIn集成Protocol Buffers:


挑战: LinkedIn 面临的挑战是 JSON 的冗长以及由此导致的网络带宽使用量增加, 从而导致延迟增加.
解决方案: 他们在微服务通信中采用了二进制序列化格式 Protocol Buffers 来取代 JSON.
影响: 这一优化将延迟降低了60%, 提高了 LinkedIn 服务的速度和响应能力.


2. Uber的H3地理索引:



  • 挑战: Uber 使用 JSON 表示各种地理空间数据, 但解析大型数据集的 JSON 会降低其算法的速度.

  • 解决方法 他们引入了H3地理索引, 这是一种用于地理空间数据的高效六边形网格系统, 可减少 JSON 解析开销.

  • 影响: 这一优化大大加快了地理空间操作, 增强了 Uber 的叫车和地图服务.


3. Slack的消息格式优化:



  • 挑战: Slack 需要在实时聊天中传输和呈现大量 JSON 格式的消息, 这导致了性能瓶颈.

  • 解决方法 他们优化了 JSON 结构, 减少了不必要的数据, 只在每条信息中包含必要的信息.

  • 影响: 这一优化提高了消息渲染速度, 改善了 Slack 用户的整体聊天性能.


4. Auth0的Protocal Buffers实现:



  • 挑战: Auth0 是一个流行的身份和访问管理平台, 在处理身份验证和授权数据时面临着 JSON 的性能挑战.

  • 解决方案: 他们采用Protocal Buffers来替代 JSON, 以编解码与身份验证相关的数据.

  • 影响: 这一优化大大提高了数据序列化和反序列化的速度, 从而加快了身份验证流程, 并增强了 Auth0 服务的整体性能.


这些真实案例表明, 通过优化策略解决 JSON 的性能难题, 可对应用的速度, 响应和用户体验产生重大积极影响. 它们强调了在各种应用场景中考虑使用替代数据格式和高效数据结构来克服 JSON 相关缓慢的问题的重要性.


总结一下


在开发领域, JSON 是数据交换不可或缺的通用工具. 其人类可读格式和跨语言兼容性使其成为现代应用的基石. 然而, 正如我们在本文中所探讨的, JSON 的广泛应用并不能使其免于性能挑战.


我们在优化 JSON 性能的过程中获得的主要启示是显而易见的:



  • 性能至关重要: 在当今的数字环境中, 速度和响应速度至关重要. 用户希望应用能够快如闪电, 即使是微小的延迟也会导致不满和机会的丧失.

  • 尺寸至关重要: 数据有效载荷的大小会直接影响网络带宽的使用和响应时间. 减少数据大小通常是优化 JSON 性能的第一步.

  • 替代格式: 当效率和速度至关重要时, 探索其他数据序列化格式, 如Protocal Buffers, MessagePack, BSON 或 Avro.

  • 真实世界案例: 从企业成功解决 JSON 速度变慢问题的实际案例中学习. 这些案例表明, 优化工作可以大幅提高应用的性能.


在继续构建和增强Web应用时, 请记住要考虑 JSON 对性能的影响. 仔细设计数据结构, 选择有意义的键名, 并在必要时探索其他序列化格式. 这样, 你就能确保你的应用在速度和效率方面不仅能满足用户的期望, 而且还能超越用户的期望.


在不断变化的Web开发环境中, 优化 JSON 性能是一项宝贵的技能, 它能让你的项目与众不同, 并确保你的应用在即时数字体验时代茁壮成长.


作者:bytebeats
来源:juejin.cn/post/7299353265099423753
收起阅读 »

切!我又不是第一次没人要🤡

我和你一样都经历过 家里蹲 狗都嫌 的尴尬时期,每天早上起来拿着手机不断刷着招聘软件, 海投几百份还是杳无音讯,在BOSS直拒、前程堪忧、失联招聘、猎空之间反复横跳... 还经历了十分灰暗的阶段,焦虑导致出现躯体化反应(头痛、严重失眠、吃不下东西等) 整夜整夜...
继续阅读 »

thumb-1920-651952.jpg


我和你一样都经历过 家里蹲 狗都嫌 的尴尬时期,每天早上起来拿着手机不断刷着招聘软件,


海投几百份还是杳无音讯,在BOSS直拒前程堪忧失联招聘猎空之间反复横跳...


还经历了十分灰暗的阶段,焦虑导致出现躯体化反应(头痛、严重失眠、吃不下东西等)


整夜整夜睡不着,躺下脑子都是工作、面试、人生选择带来的压力


不想出门社交,害怕面试。


其实,我想跟你说:裸辞并不是终点。


1.裸辞/辞职并不是终点


当我扛着我的键盘收拾东西离开工位,第一次对辞职的 “人走茶凉” 有了实感,


下午六点跟对我很好的前辈们告了别,公司离地铁有点远,和往常不一样,天还没黑,有黄昏相伴


三号线还是这么挤 还有点闷。


v2-7ab9c9ebffb2d15971a4ec2a9b1b2fe3_720w.webp


算起来这是第二次辞职,但第一次辞职找了一个礼拜就顺利入职了
,这次好像有点久,今年大家都在说被裁员大环境差,同学领证成家的也不少。


我意识到人与人的节奏不同,而我好像又一次走到了岔路口,上一次这么慌张还是在高考前


即便我从来没后悔过离职这个决定,但还是会因为面试带来的压力感到局促不安


每次离职就像是一场查漏补缺的大考,对勇气,对储蓄,对知识点的大考


唯有拆迁认亲能打破这场突如其来的考验。。。啊不是。。。我想说:


唯有行动能打破僵局!!


R.png


行动!!!去吃个冰淇淋!!。。。果然有灵感了


363b10d3887191d03f7a462610206e4.jpg


短暂的欢愉后,是与台灯的昼夜相守,与简历的交织缠绵


(简历编。。不是。。写不出来呀!!!)


v2-e4d9f181546345313e5e36f15d2e3cf1_r.jpg


反复改了几版之后确实多了一些“打招呼”的机会,但是实际面试机会还是屈指可数呀,


切!又不是第一次没人要🤡,拒绝我的多的去了,得从巴黎排到广州...


继续努力,等待运气,厚积薄发


v2-4039118d48100a401ec76dd9e4d73405_r.jpg


2.当知识脱离了考试,真理和美丽才慢慢浮现


2.1 心态调整(分享一下最近对我有帮助的书)



  • 《见识》 - 吴军


这是第一年出来工作,遇上了很好的领导送我的书,每当迷茫的时候再拿出来翻翻有了不一样的感悟,很多我们看上去非做不可的事情,其实想通了并没有那么重要,无论在职场上还是在生活中,提高效率都需要从拒绝伪工作开始,有些苦是可以不用吃的,苦难并非造就人类



幸福是目的,成功是手段




  • 《意志力》 - 罗伊·鲍迈斯特


技术行业的人都知道学习是个漫长/终身的事情,跟考公考研短期爆发式集中不同,我们更需要坚持长期一点点做下去,我认识到所有人的意志力都是有限的,使用就会消耗,压力也并非与动力画等号,人也跟机器一样需要“充电”和合理分配,每个人的节奏和身体承受能力也不同。



  • 《被讨厌的勇气》 - 岸见一郎、古贺史健编著


在心情动荡的时期,这本书就像开了一盏加热灯一样在一旁无声陪伴,那会我就像婴儿一样无意识地紧紧抓着自己的头发,直到我睁开眼看见了、意识到了,放下禁锢着工作、生活、交友的课题的手,更能轻松地赶路了。



生活的方式千千万,人生的意义,由我自己决定




  • 《法律常识全知道》 - 李桥


读书的时候没有一门跟社会接轨的课程,毕业了也一直专研技术,导致一毕业不知道劳动合同/租房有什么坑,把仲裁和维权看得过于艰难,法律条例密密麻麻 一时间不知从何下手,这本书就很适合我这种来一线城市打工没什么社会经验的小白,用简单的案例植入“NPC游戏”攻略,和《影响力》这本书加一起简直就是进城防骗指南哈哈哈



免费法律援助电话:12348



2.2 前端学习路线图:


roadmap-完整路线.jpeg


各位摸鱼的小伙伴下次见,这篇便是我的2023年终总结:


裸辞不是终点,唯有行动才能打破僵局,当知识脱离了考试,真理和美丽才慢慢浮现。




参考资料:


前端学习路线


作者:慌张的葡萄
来源:juejin.cn/post/7312304122535133220
收起阅读 »

我和我的4年前端生涯

我是一个普普通通的前端开发工程师,除了我的同事,在这个圈子内没人认识我。“前端开发工程师”,老实讲,我很喜欢这个称呼,我对一些圈外朋友总是这么称呼我自己,当然我的内心和认识我的人都知道,我就是一个“写代码的”。初见在2019年,我大专毕业,那时候的学长学姐总和...
继续阅读 »

我是一个普普通通的前端开发工程师,

除了我的同事,在这个圈子内没人认识我。

“前端开发工程师”,老实讲,我很喜欢这个称呼,我对一些圈外朋友总是这么称呼我自己,当然我的内心和认识我的人都知道,我就是一个“写代码的”。

初见

在2019年,我大专毕业,那时候的学长学姐总和我开玩笑说,以后毕业的我们都会去卖保险,跑销售,那时候的我不以为意,以为只是自我调侃,后来发现人家说的是真的,根本不是开玩笑。我在某大厂做了近3个月的电销,那时候我还是我们班工资最高的人,但不得不说这份职业我根本不喜欢。后来,我大学的班花邓某(真的很漂亮)有一次和我聊天,那阵子她在某大厂做审核。她说她打算去培训前端,问我有没有兴趣。那时候的我觉得计算机啊,编程啊都是精英人群,我这等人也能从事吗?我还打趣的问她,参与前端和高考上清华哪个难?(可见那时候的我对这个想都不敢想)

学习

在19年6月毕业后,我就失业了。我根本想不到大专毕业生能从事什么好的工作,也不清楚我能做些什么。后来我一想,那不妨去培训吧,这样就能和班花做同学,有一起接触的空间和机会了!然后我就开始联系她,但是不得不说,她有强烈的拖延症,她根本对这件事没放在心上。那就自学吧!如果我先学会了,然后等她入门的时候,我就可以教她了!她一定对我刮目相看!嘿嘿!我当时是这样想的没错。我首先找到了培训机构,找他们要到了课程大纲,然后自己逐一的根据大纲上的内容,在网上找视频。不得不说,bilibili大学真的很好用。我的html,css,js都是师承的pink老师,他讲的真的很棒,后来vue就跟着180的科比老师学习,不得不说,这两位的视频对我来说有着举足轻重的作用。那时候第一次学习编程,很多东西都是死记硬背的(切记不要这么做),你很难想象到一个人会去背各种元素的作用以及api。那时候学习的过程还是很快乐的,每天都能接触到新知识,在这就是看着那些视频的进度,想象着看完后自己就能毕业的样子,还是很期待的。甚至说,根据视频模仿了一个很小的功能,然后和自己父母分享的时候,他们都在夸我,现在回想起来,那是我从事前端这么多年来最快乐的时光。

毕业

20年9月的时候,大概经历了近10个月的自学,我决定出山找工作了,我来到了广东。当时我具备的技能有html,css,js,vue2,ts。后来就进入了刷面试题的阶段,有很多面试题完全都是死记硬背的,印象最深刻的就是vue2的响应式原理和nextTick原理,完全是根据百度来的台词去阐述。虽然说现在的我让我自己手写这些都不成问题。不得不说,准备的面试题可以准备,但是项目经验,一些细节可真是苦恼了我。一些对项目,业务比较感兴趣的面试官,我通常都会挂在这里,因为没有任何的工作经验。

我还记得,我面试的第一家公司,是在一个星期天,他们需要找开发小程序的人,最后聊完了技术之后;

面试官:“我看你的简历上写着你上一份工作在武汉有着6k/月,是吗?”

我:“是的”(在这里,我和大多数人一样,包装了,美化了自己的简历)

面试官:“据我所知,不到1年工作经验的专科生,是不可能在武汉有6k/月的,当然这只是我个人的片面的看法,江东自古多才俊嘛”(这就是他的原话,我此生都忘不了)

我内心:“尴尬,脸红,我要逃!快让我走!”

我也还记得,我第一次收获的offer的时候,是一家在CBD,25层高楼的公司,在我毕业以前,我一直都以为能在那里工作的人都是精英,商业大佬。这家公司他让我试岗三天,试岗第一天有个题目是让人手写轮播图和使用element做一个分页功能。我还是通过网友,帮我远程控制我的电脑才完成的这个任务,不过很遗憾,我自己选择了离开这家公司,原因嘛,因为我每天挤不上地铁,再者就是,那家公司气氛感觉很压抑。21年的我还很年轻,有这样的果敢和勇气,如果说24年的我,肯定会忍耐这两点。

第一份工作

2021年3月,在经历过长达3个月的求职经历后,我已经被磨练成了面试机器人。终于找到了一家公司,给我的薪水也很不错,至今我都觉得那家公司是宝藏公司,人数500+,我在那里上班1年没有任何加班经历,需求也非常简单,用vue改改东西,写下静态,一周5天能摸鱼3天。就姑且叫那家公司为X公司吧,还有一个特别搞笑的经历,在X面试的时候,老大问我,“你的项目上有分页查询,这个功能是怎么做的?”,我的回答是:“这个功能是前端做的”。真的很难崩有木有!!!他居然要我了!后来在X的日子,开始过起了神仙般的日子,温饱问题总算是解决了,开始每天接触前端这个圈子,和群友聊天,早上刷各种社交软件。 那时候下班了自己也会回家学习,心里也有一个大牛梦,想着自己能不能做出一些很棒的产品开源出去,就像那些社区圈子的名人一样。还记得那时候跟着视频学习写mini-vue框架,也买了很多课程,觉得投资自己是一笔超值的买卖,然而时至今日那些视频都没有看完。在X公司混到了要转正的那一天,还是蛮忐忑的,结果老大把我单独喊出去谈话,到这里已经开始觉得事情不对了。老大说我的表现还不能满足转正,要继续观察一个月,如果说看到这里,可能很多人会说垃圾公司快跑,但我始终为我当时没有赌气离职感到自豪和开心。

ps:有趣的事,在22年的时候我和带我的女同事都离职了,我们聊天的时候说谈起来这件事,她告诉我,关于我是否能转正的问题,是老大先去问她的意见,问我技术怎么样,她把我夸了一顿。当我知道这个事情的时候,泪目了。

第二份工作

在X公司混了1年左右后,说实在的,我的任务实在是太简单了,上班大多数时间都在摸鱼和自学,没有接触到真正的业务。后来就跑路来到了第二家公司,且叫Y公司吧。在这里我接手的一个项目是从0到1实现的,这正是我想要的,能够完整的学习到项目经验,太妙了。那阵子加班的时间就开始多起来,不过也还挺满意的,能学习到真正的业务。不过这种情况持续了大概半年,新鲜感就退去了,我发现我所做的事情,就是“搬砖”。业务上需要用到什么东西,直接搜一个开源的,然后自己管理一下状态,和后端交互一下,结束了,基本上所有的我参与的业务都是这样的模式,我开始懈怠了,早上每天来了先吃半个小时早饭,然后刷新闻,甚至直到下午才开始做活,因为这些事情实在是太无聊了。在下班后,我开始探索代码的世界,开始着手实现自己刚出道的时候的梦想----实现一个开源产品。那时候的我仿佛就是为了写而去写,我根本不知道要写什么,解决什么样的问题,完全就是照着vue,react这种前端框架去抄,而且实现的非常玩具,没坚持多久就放弃了这个天真的想法。

再见

突然有一天,班花邓某突然联系我了,她告诉我她已经辞职去培训了,问我要不要一起。我得承认,收到她消息的时候小鹿乱撞了,我告诉了她我已经从事前端的工作了,她大为开心,说要我帮帮她,有什么不会的就来问我,我很开心,第一次明白被人需要是什么感受。后来她在培训班毕业了,她让我来面试她,不得不说教她的都是什么妖魔鬼怪,我问她对Vue的理解,她居然背文档说,“vue是一款渐进式框架...”。我问她什么是“渐进式”,她就不知道了。那阵子和她交流了很多,虽然不在一个城市,但还是很开心。另外确实漂亮的女生真的在我们这个行业很有优势,她不到1个月就找到工作,而且她提出期望9k,人家面试官给她10k。

WX20240327-161849@2x.png

JS为我做的三件事

  1. 工作

这第一件事就是让我有了生存的技能,找到了一份工作,首先是能生存下来了。说真的,在我参加工作之后,程序员人数膨胀之前,我一直都认为程序员是普通家庭的专科生一项比较好的出路,但现在的话的来说,我可能也会偏向于劝退专科生的萌新吧,这张图是我专科,4年工作经验,计算机专业的求职经历,如果你能承担像这样的挫折感的话,并且你比较自律,热爱编程,我也鼓励你坚持梦想!

WX20240327-132049@2x.png

其实我目前的状态也很难讲,因为这是一个离奇的经历。我们公司最近的效益不太好,可能是我们部门的产品没能带来什么收益,已经开始裁员了,而且很过分的一件事情是,我和我的部门同事,已经3个月没有发薪水了!这件事超级严重,根据过往的经验和数据的话,大概是要凉凉的节奏。

我们部门一个资历比较老的同事,就带头跑路,然后搞的其他人也就跟风,然后吧,好巧不巧,他们问我跑不跑,我深知专科生找工作无比艰难(请参考上面我的BOOS图),我只能相信公司,会好起来的。

这件事无论说到哪里,大家给我的建议都会是立马跑路,然后去仲裁,可能现在看文章的你也是这么想的,但是我很怕,我怕失去工作,我已经适应了现在的环境,我没办法面对接下来的未知挑战,这和我的个人性格有关系,我决定留下来,在赌一下自己的前程,或者等公司裁掉我,没准还有补偿不是吗?

好巧不巧,我们部门资历老的全跑了,老大让我来当老二了,我听到这个消息的时候是懵逼的,老大给我布置了一系列的任务,包括架构一下前端,招人等事宜,我提道工资呢?老大说会给你发的,公司在等机会。也就是典型的加量不加价,其实今天为什么能来更新这篇文章,也是因为我来架构前端,打算在掘金调研一套eslint规则,因为这里的大佬比较多,技术文章质量也还是很高的。

我想到这里,其实觉得我的平台,舞台也是蛮大的,发挥的机会很多,(虽然没发工资)。于是乎我提出了一系列计划上报了老大,包括技术从Vue2转到react18,前端组件库改用storybook等。除了使用react18这套方案被否决了,其余的也都通过了。

说真的,对这份工作的未来也是有极高的不确定性,如果你不敢承受10%的暴跌,那就会错过1%的大涨,不是吗?

  1. 爱情

我在2023年的时候,在某书上认识了一个江西的女生,我们聊的很投缘,我们甚至是一个大学毕业的,她在武汉上的大学。我们有着共同的爱好,经常聊天就是半天的时间就过去了。而且,我们经常分享自己的一些身边事给对方,一些实事的热点新闻的看法,我们也出奇的一致,她在朋友圈发的照片也很好看,有一次,我给她发消息,她过了一段时间没有回复我,我就开始想着,她在干嘛,为什么还没有回,开始焦急,情绪被左右了,我明白了,我确实喜欢他了。

当从朋友,也可能是网友的关系,变成了爱慕的时候,我对她的态度就变了,我开始变得很有占有欲,开始看她某书下的评论有没有其他男孩子,那阵子的我,真的很丑陋。

我开始着急了,我决定更近一步试探一下,我邀请她一起玩某某荣耀,她也答应的很爽快,那时候我还以为我成功了,有戏!没想到,当天,我上号后,她邀请我,我发现!她居然带了她闺蜜和一个男生,我纯纯无语。。。而且游戏过程中,他们三个互动比较多,基本上我就是路人甲,那晚挺失望的反正。

到了她的生日,我说我送你一个生日礼物吧!她也婉拒了,从那时候开始,我们的距离就变得远了。到今天,2024,3,27号,我们大概40多天没说过话了。

到这里,和js有什么关系呢?

我即将告诉你我人生中,做的最小丑的决定

我想的是,女生都比较幕强,我要给她展现一个技术大佬的人设!没错,在某个夜晚我是这么想的,晚上下班后,我上某里云租了一个最便宜的服务器,然后用express + react.js(没错,我个人更喜欢react,这也是我为什么尝试说服领导转react的原因,hhh)花了一晚上时间,写了一个聊天室功能,还内置了许多她喜欢的东西作为背景,icon,logo,然后我部署在了服务器上。

第二天,我立马把域名分享给她,期待她的回复!还俏皮的说,能打败微信吗?我以为这样,她会对我五体投地的佩服,结果她只回了一个 “6”,然后补充“哈哈哈哈哈哈”

那时候我觉得,她是对这种技术不感冒,她只对迪士尼和追星有兴趣,她每周都会去迪士尼和一只小狐狸合影,请注意是每周。而且她会追每一个在她城市开演唱会的人,某杰,某谦,某棋。我以为她是喜欢这些,后来我才发现,她其实是对我没兴趣而已,意识到我越界了,有意疏远我。

  1. 开源,

我用js尝试写过很多开源,也想成为一个为社区有过贡献的人,但后来发现根本没什么用,自己无论写什么,都已经有了前人的库,比我好,比我成熟,比我火。我最近一直在思考,如果我把学习这些,折腾这些的时间用来学习一门其他的事情,比如吉他,或者打游戏,我会不会更快乐?都说1-3年的前端交流群里都在聊技术,4-5年都在聊业务,5年以上的都在聊家庭和钓鱼,看得出,这就是现实,极少有人能从技术中脱颖而出,我也认清了自己。


作者:soloDog
来源:juejin.cn/post/7345760019319373864
收起阅读 »

Android渠道包自动更新

一、背景 转转集团旗下有多款APP产品,随着业务发展,各APP发版频率变高。在持续交付的背景下,渠道包更新存在以下几个效率问题: (1)Android渠道包提交应用市场审核,工作重复&人工成本高   (2)公司目前存在多个APP、需...
继续阅读 »

一、背景


转转集团旗下有多款APP产品,随着业务发展,各APP发版频率变高。在持续交付的背景下,渠道包更新存在以下几个效率问题:


(1)Android渠道包提交应用市场审核,工作重复&人工成本高  


(2)公司目前存在多个APP、需更多人支持,有培训成本


(3)每次发版需要人工通知项目成员渠道包审核进度 


  针对以上问题,我们设计开发了渠道包自动更新后台,用来解决渠道更新的效率问题。


二、方案调研


1、基于业务现状,做了技术调研和逻辑抽象


  不同APP支持的渠道不同,不同渠道更包api不同,如下图:


图片


针对以上调研结果,我们将通用的逻辑统一封装开发,将差异点进行配置,做到灵活配置可扩展。


2、整体的实现方案演变


初期方案,每个应用市场单独提审(需要先选择物料,选好物料后上传包文件,文件上传成功后再点击提交审核),多个应用市场需要重复该操作。


图片


上线运行了一段时间后,发现存在一些问题:单个市场提交步骤繁琐、多个应用市场需要分开多次提交。这些步骤是重复且可简化的,因此我们又对提审的过程做了封装,提供批量上传的入口,简化交互过程,做到一键提审。以下是当前运行的第二版方案:


图片


第二版方案上线后,提审同学只需要在入口处选择要更新的应用市场,然后一键上传全部物料,再点击提审按钮即可提审成功。代码内部会处理具体的逻辑,比如:根据配置规则将物料匹配到对应市场、自动匹配包文件进行提审。


三、方案设计


自动上传包含以下核心模块:



  • APP管理:支持配置多个APP信息,包括转转、找靓机、采货侠等

  • 包管理:支持下载不同渠道,不同版本的包

  • 物料管理:包括历史物料的选择,和新增物料的存储(icon、市场截图)

  • 提交审核:包括包下载、物料下载,支持按照APP配置账号密码提交审核

  • 消息提醒:对提交的结果和审核的结果进行消息通知


图片


实现效果:


提审前信息确认,选择APP,可选择单个或者多个渠道,系统自动选择包地址,用户选择物料后可一键提审多应用市场。操作简单便捷,使用成本低


图片


提审后发送消息通知,便于各方了解渠道的审核结果,对审核异常信息进行及时干预。同时自动存储不同版本的审核记录,方便后续分析。


图片


四、总结


渠道包自动更新功能,节省了大量的提交审核人力成本,打通了Android整体的持续交付过程,降低了人工学习成本。之后我们也会针对各种体验问题进行不断的改进和更新~



作者:转转技术团队
来源:juejin.cn/post/7238917620850147383
收起阅读 »

两个Kotlin优化小技巧,你绝对用的上

大家好,本篇文章仍然聊聊kotlin官方做的一些优化工作,主要包括以下三个方面: 数据对象data object的支持 @Repeatable注解的优化 接下来就带大家介绍下上面三个特性。 一. 数据对象data object的支持 该特性由kotlin1...
继续阅读 »

大家好,本篇文章仍然聊聊kotlin官方做的一些优化工作,主要包括以下三个方面:



  1. 数据对象data object的支持

  2. @Repeatable注解的优化


接下来就带大家介绍下上面三个特性。


一. 数据对象data object的支持


该特性由kotlin1.7.20插件版本提供,并处于实验阶段。



这个特性主要是和原来的object声明的单例类的toString()方法输出有关,在了解这个特性之前,我们先看下下面一个例子:


object Single1

fun main() {
println(Single1)
}

输出:



这个输出本质上就是一个类名、@、地址的拼接,有时候你想要打印输出的仅仅是类名,就得需要重写下toString()方法:


object Single1 {

override fun toString(): String {
return "Single1"
}
}

然后再看一个密封类的例子:


sealed interface Response {

data class Success(val response: String): Response

data class Fail(val error: String): Response

object Loading : Response

}

fun main() {
println(Response.Success("{code: 200}"))
println(Response.Fail("no net"))
println(Response.Loading)
}

输出:



可以看到,大家都是密封子类,但就这个Loading类的输出比较"丑陋",没有上面两个兄弟类的输出简洁清爽。


接下来我们就要介绍下主人公数据对象data object了,这个东西其实使用起来和object一模一样,核心的区别就是前者的toString() 更加简洁。


接下来从一个例子一探究竟:


data object Single2

fun main() {
println(Single2)
}

看下输出:



输出是不是比上面的object Single1更加简单明了。最重要的是在密封类中使用效果更加,我们把上面密封类Loading声明为data object


    data object Loading : Response

看下最终的输出结果:



这下子输出结果是不是清爽更多!!


讲完了应用,我们再java的角度看下其背后的实现机制,相比较于objectdata object会多了下面这三个重写方法:


public final class Single2 {

@NotNull
public String toString() {
return "Single2";
}

public int hashCode() {
return -535782198;
}

public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (!(var1 instanceof Single2)) {
return false;
}

Single2 var2 = (Single2)var1;
}

return true;
}
}

我们需要关心的toString()方法就是直接重写返回了当前的类名。


如果想要使用这个特性,我们只需要增加如下配置即可:


compileKotlin.kotlinOptions {
languageVersion = "1.9"
}

二. @Repeatable注解优化


该特性由kotlin1.6.0插件版本提供优化。



在了解这个特性之前,我们先回忆下@Repeatable这个注解在java中的使用:


如果一个注解在某个方法、类等等上面需要重复使用,那就需要@Repeatable帮助。



  • 首先定义需要重复使用的注解


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(Fruits.class)
public @interface Fruit {
String name();
String color();
}


  • 然后定义注解容器,用来指定可重复使用的注解类型


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Fruits {
Fruit[] value();
}

然后就可以在代码中这样使用:


@Fruits({
@Fruit(name = "apple", color = "red"),
@Fruit(name = "origin", color = "yellow"),
})

public class Detail {
}

大家有没有发现,可重复注解定义起来还是由一丢丢的麻烦,接下来轮到我们kotlin重磅出击了。先看下面一个例子:


@Repeatable 
annotation class Animal(val name: String)

在kotlin中我们只要声明一个需要重复使用的注解即可,kotlin编译器会自动帮助我们生成注解容器@Animal.Container,然后我们就能在代码中这样使用:


@Animal(name = "dog")
@Animal(name = "horse")
public class Detail {
}

是不是非常简单便捷了。


如果你偏要显示指明一个包含注解,也可以,通过以下方式即可实现:


@JvmRepeatable(Animals::class)
annotation class Animal(val name: String)

annotation class Animals(val value: Array)

然后除了上面的使用方式,你在kotlin中还可以这样使用:


@Animals([Animal(name = "dog"), Animal(name = "dog")])
class Detail {
}

请注意:



  1. 如果非要显示声明一个注解容器,其属性的名称一定要为value

  2. 其次,注解容器和可重复性直接不能同时声明在同一个元素上;


另外,其实这个特性kotlin早就支持了,只不过kotlin1.6.0插件版本之前,kotlin这个特性只只支持RetentionPolicy.SOURCE生命周期的注解,并且还和java的可重复注解不兼容。


总结


这两个小技巧相信在大家日常开发中还是比较实用的,希望本篇能对你有所帮助。


参考文章:


Improved string representations for singletons and sealed class hierarchies with data objects


Repeatable annotations with runtime retention for 1.8 JVM target




作者:长安皈故里
来源:juejin.cn/post/7248249730478784569
收起阅读 »

Android Room 数据库的坑

1.Android Room 数据库的坑 在用Room数据库的时候 发现有需要一个字段的条件合到一起去写这个SQL @Query("SELECT * FROM 表名 WHERE 字段A = '1' and 字段B <= :Time " + ...
继续阅读 »

1.Android Room 数据库的坑


在用Room数据库的时候 发现有需要一个字段的条件合到一起去写这个SQL


 @Query("SELECT * FROM 表名 WHERE 字段A = '1' and 字段B <= :Time " +
"
and 字段B >= :Time and 字段C <= :BTime and 字段C >= :BTime " +
"
and '(' || 字段D is null or 字段D = '' || ')'")

List
selectList(String Time, String BTime);


这里面的 “ ||” 是Room里面独特的表达方式 是替代了java里面的“+”号


正常在android中 使用 是这样的


String sql = "SELECT * FROM 表名 WHERE 字段A = '1' and 字段B <= "+传入的参数+" " +
"and 字段B >= "+传入的参数+" and 字段C <= "+传入的参数+" and 字段c >= "+传入的参数+" " +
"and '(' "+" 字段D is null or 字段D = '' "+" ')'"


cursor = db.rawQuery(sql,
null);

而在Room 中 用 “||” 代替了 “+”


2.Android Room 查询语句的坑


@Query("SELECT * FROM 表名 WHERE 字段A = '0' order by id desc")
List
selectList();

假如你正在查询一张表的面的内容,然后忽然跑出来一个异常



# [Android RoomDatabase Cruash "Cursor window allocation of 4194304 bytes failed"](https://stackoverflow.com/questions/75456123/android-roomdatabase-cruash-cursor-window-allocation-of-4194304-bytes-failed)

奔溃日志:


android.database.CursorWindowAllocationException: Could not allocate CursorWindow '/data/user/0/cn.xxx.xxx/databases/xxx.db' of size 2097152 due to error -13.
at android.database.CursorWindow.nativeCreate(Native Method)
at android.database.CursorWindow.(CursorWindow.java:139)
at android.database.CursorWindow.(CursorWindow.java:120)
at android.database.AbstractWindowedCursor.clearOrCreateWindow(AbstractWindowedCursor.java:202)
at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:147)
at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:140)
at yd.d.m(SourceFile:21)
at cn.xxx.control.y.y0(SourceFile:1)
at e5.y.p(SourceFile:230)
at e5.y.l(SourceFile:1)
at e5.y.E(SourceFile:1)
at cn.xxx.cluster.classin.viewmodel.SessionViewModel$d.invokeSuspend(SourceFile:42)

触发原因



  • Room 对应的 Sqlite 数据库,其对 CursorWindows 分配的大小是有限制的,最大为 2M,超过之后会发生上述崩溃闪退现象(偶现且难以复现的 bug


解决方法


需要业务方梳理这块的业务,优化数据库的调用,如果明确知道在一个方法里面会调用多个数据库的方法,需要让 controller 提供新的方法,且这个 controller 层的方法需要添加 @Transaction 进行注解,从而保证在同一个事物内进行数据库操作,以此避免 CursorWindows 大小超过 2M


那么问题来了 @Transaction 这个注解是干嘛的呢


翻译 事务的意思


@Transaction
@Query("SELECT * FROM 表名 WHERE 字段A = '0' order by id desc")
List selectList();

接着 问题完美解决


作者:笨qiao先飞
来源:juejin.cn/post/7273674981959745593
收起阅读 »

告别轮询,SSE 流式传输可太香了!

今天想和大家分享的一个技术是 SSE 流式传输 。如标题所言,通过 SSE 流式传输的方式可以让我们不再通过轮询的方式获取服务端返回的结果,进而提升前端页面的性能。 对于需要轮询的业务场景来说,采用 SSE 确实是一个更好的技术方案。 接下来,我将从 SSE ...
继续阅读 »

今天想和大家分享的一个技术是 SSE 流式传输 。如标题所言,通过 SSE 流式传输的方式可以让我们不再通过轮询的方式获取服务端返回的结果,进而提升前端页面的性能。


对于需要轮询的业务场景来说,采用 SSE 确实是一个更好的技术方案。


接下来,我将从 SSE 的概念、与 Websocket 对比、SSE 应用场景多个方面介绍 SSE 流式传输,感兴趣的同学一起来了解下吧!


什么是 SSE 流式传输


SSE 全称为 Server-sent events , 是一种基于 HTTP 协议的通信技术,允许服务器主动向客户端(通常是Web浏览器)发送更新。


它是 HTML5 标准的一部分,设计初衷是用来建立一个单向的服务器到客户端连接,使得服务器可以实时地向客户端发送数据。


这种服务端实时向客户端发送数据的传输方式,其实就是流式传输。


我们在与 ChatGPT 交互时,可以发现 ChatGPT 的响应总是间断完成。细扒 ChatGPT 的网络传输模式,可以发现,用的也是流式传输。


图片


SSE 流式传输的好处


在 SSE 技术出现之前,我们习惯把需要等待服务端返回的过程称为长轮询。


长轮询的实现其实也是借助 http 请求来完成,一个完整的长轮询过程如下图所示:


图片


从图中可以发现,长轮询最大的弊端是当服务端响应请求之前,客户端发送的所有请求都不会被受理。并且服务端发送响应的前提是客户端发起请求。


前后端通信过程中,我们常采用 ajax 、axios 来异步获取结果,这个过程,其实也是长轮询的过程。


而同为采用 http 协议通信方式的 SSE 流式传输,相比于长轮询模式来说,优势在于可以在不需要客户端介入的情况下,多次向客户端发送响应,直至客户端关闭连接。


这对于需要服务端实时推送内容至客户端的场景可方便太多了!


SSE 技术原理


1. 参数设置

前文说到,SSE 本质是一个基于 http 协议的通信技术。


因此想要使用 SSE 技术构建需要服务器实时推送信息到客户端的连接,只需要将传统的 http 响应头的 contentType 设置为 text/event-stream 。


并且为了保证客户端展示的是最新数据,需要将 Cache-Control 设置为 no-cache 。


在此基础上,SSE 本质是一个 TCP 连接,因此为了保证 SSE 的持续开启,需要将 Connection 设置为 keep-alive 。


Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

完成了上述响应头的设置后,我们可以编写一个基于 SSE 流式传输的简单 Demo 。


2. SSE Demo

服务端代码:


const express = require('express');
const app = express();
const PORT = 3000;

app.use(express.static('public'));

app.get('/events'function(req, res) {
    res.setHeader('Content-Type''text/event-stream');
    res.setHeader('Cache-Control''no-cache');
    res.setHeader('Connection''keep-alive');

    let startTime = Date.now();

    const sendEvent = () => {
        // 检查是否已经发送了10秒
        if (Date.now() - startTime >= 10000) {
            res.write('event: close\ndata: {}\n\n'); // 发送一个特殊事件通知客户端关闭
            res.end(); // 关闭连接
            return;
        }

        const data = { message'Hello World'timestampnew Date() };
        res.write(`data: ${JSON.stringify(data)}\n\n`);

        // 每隔2秒发送一次消息
        setTimeout(sendEvent, 2000);
    };

    sendEvent();
});

app.listen(PORT() => {
    console.log(`Server running on http://localhost:${PORT}`);
});

客户端代码:


<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>SSE Example</title>
</head>

<body>
    <h1>Server-Sent Events Example</h1>
    <div id="messages"></div>

    <script>
        const evtSource = new EventSource('/events');
        const messages = document.getElementById('messages');

        evtSource.onmessage = function(event) {
            const newElement = document.createElement("p");
            const eventObject = JSON.parse(event.data);
            newElement.textContent = "Message: " + eventObject.message + " at " + eventObject.timestamp;
            messages.appendChild(newElement);
        };
    
</script>
</body>
</html>

当我们在浏览器中访问运行在 localhost: 3000 端口的客户端页面时,页面将会以 流式模式 逐步渲染服务端返回的结果:


图片


需要注意的是,为了保证使用 SSE 通信协议传输的数据能被客户端正确的接收,服务端和客户端在发送数据和接收数据应该遵循以下规范:


服务端基本响应格式

SSE 响应主要由一系列以两个换行符分隔的事件组成。每个事件可以包含以下字段:


data:事件的数据。如果数据跨越多行,每行都应该以data:开始。
id:事件的唯一标识符。客户端可以使用这个ID来恢复事件流。
event:自定义事件类型。客户端可以根据不同的事件类型来执行不同的操作。
retry:建议的重新连接时间(毫秒)。如果连接中断,客户端将等待这段时间后尝试重新连接。

字段之间用单个换行符分隔,而事件之间用两个换行符分隔。


客户端处理格式

客户端使用 EventSource 接口监听 SSE 消息:


const evtSource = new EventSource('path/to/sse');
evtSource.onmessage = function(event) {
    console.log(event.data); // 处理收到的数据
};

SSE 应用场景


SSE 作为基于 http 协议由服务端向客户端单向推送消息的通信技术,对于需要服务端主动推送消息的场景来说,是非常适合的:


图片


SSE 兼容性


图片


可以发现,除了 IE 和低版本的主流浏览器,目前市面上绝大多数浏览器都支持 SSE 通信。


SSE 与 WebSocket 对比


看完 SSE 的使用方式后,细心的同学应该发现了:


SSE 的通信方式和 WebSocket 很像啊,而且 WebSocket 还支持双向通信,为什么不直接使用 WebSocket ?


下表展示了两者之间的对比:


特性/因素SSEWebSockets
协议基于HTTP,使用标准HTTP连接单独的协议(ws:// 或 wss://),需要握手升级
通信方式单向通信(服务器到客户端)全双工通信
数据格式文本(UTF-8编码)文本或二进制
重连机制浏览器自动重连需要手动实现重连机制
实时性高(适合频繁更新的场景)非常高(适合高度交互的实时应用)
浏览器支持良好(大多数现代浏览器支持)非常好(几乎所有现代浏览器支持)
适用场景实时通知、新闻feed、股票价格等需要从服务器推送到客户端的场景在线游戏、聊天应用、实时交互应用
复杂性较低,易于实现和维护较高,需要处理连接的建立、维护和断开
兼容性和可用性基于HTTP,更容易通过各种中间件和防火墙可能需要配置服务器和网络设备以支持WebSocket
服务器负载适合较低频率的数据更新适合高频率消息和高度交互的场景

可以发现,SSE 与 WebSocket 各有优缺点,对于需要客户端与服务端高频交互的场景,WebSocket 确实更适合;但对于只需要服务端单向数据传输的场景,SSE 确实能耗更低,且不需要客户端感知


参考文档


developer.mozilla.org/zh-CN/docs/…


作者:veneno
来源:juejin.cn/post/7355666189475954725
收起阅读 »

Android 如何统一处理登录后携带数据跳转到目标页面

需求场景 我们在开发应用的时候经常会遇到先登录,登录成功后再跳转到目标页面。比如商品详情页面我们点击购买必须要先登录,登录完成才能去下单支付。针对这种场景,我们一般有两种做法: 点击购买跳转到登录,登录完成需要用户再次点击购买才能去下单支付页面,这种用户体验...
继续阅读 »

需求场景


我们在开发应用的时候经常会遇到先登录,登录成功后再跳转到目标页面。比如商品详情页面我们点击购买必须要先登录,登录完成才能去下单支付。针对这种场景,我们一般有两种做法:



  1. 点击购买跳转到登录,登录完成需要用户再次点击购买才能去下单支付页面,这种用户体验不是很好。

  2. 点击购买跳转到登录,登录完成直接跳转到下单支付页面。


第一种我们就不谈了产品经理不同意🐶。第二种我们一般是在 onActivityResult 里面获取到登录成功,然后根据 code 跳转到目标页面。这种方式缺点就是我们要在每个页面都处理相同的逻辑还有定义各种 code,如果应用里面很多这种场景也太繁琐了。那有没有统一的方式去处理这种场景就是我们今天的主题了。


封装方式


我们的应用是组件化的,APP 的页面跳转使用了 Arouter。所以我们统一处理使用 Arouter 封装。直接上代码


fun checkLoginToTarget(postcard: Postcard) {//Postcard 是 Arouter 的类
if (User.isLogin()) {
postcard.navigation()
} else {
//不能使用 postcard 切换 path 直接跳转,因为 group 可能不同,所以重新 build
ARouter.getInstance().build(Constant.LOGIN)
.with(postcard.extras)//获取携带的参数重新转入
.withString(Constant.TAGACTIVIFY, postcard.path)//添加目标路由
.navigation()
}
}

//登录成功后在登录页面执行这个方法
fun loginSuccess() {
val intent= intent
val target = intent.getStringExtra(Constant.TAGACTIVIFY)//获取目标路由
target?.apply {
if (isNotEmpty()){
val build = ARouter.getInstance().build(this)
val extras = intent.extras//获取携带的参数
if (extras != null) {
build.with(extras)
}
build.navigation()
}
}
finish()
}

代码加了注释,使用 Kotlin 封装了顶层函数,登录页面在登录成功后跳转到目标页面,针对上面的场景直接调用 checkLoginToTarget 方法。


checkLoginToTarget(ARouter.getInstance().build(Constant.PAY_PAGE).withInt(Constant.GOOD_ID,id))

通过 Arouter 传入下单支付的路由地址,并且携带了商品的 ID,生成了 Postcard 参数。登录成功后能带着商品 ID
直接下单支付了。


最后


如果项目里没有使用路由库可以使用 Intent 封装实现,或者别的路由库也可以用上面的方式去做统一处理。


作者:shortybin
来源:juejin.cn/post/7237386183612530749
收起阅读 »

别做老实人了!这才是 HR 喜欢听的离职原因!

哈喽,大家好 我是 xy👨🏻‍💻。今天我们来聊聊一个面试时超级敏感但又不得不面对的问题——离职原因💢 找工作面试被问离职原因,千万不要直接说出你为什么从上家公司离职的真实原因!!! 当老实人要吃亏的!!! 其实,大部分真实的离职原因都是每家公司或多或少会遇到...
继续阅读 »

哈喽,大家好 我是 xy👨🏻‍💻。今天我们来聊聊一个面试时超级敏感但又不得不面对的问题——离职原因💢



找工作面试被问离职原因,千万不要直接说出你为什么从上家公司离职的真实原因!!!


老实人要吃亏的!!!


其实,大部分真实的离职原因都是每家公司或多或少会遇到的,但如果直接说出来,很可能会让面试官担心你入职后也会因为同样的原因离职哦!💢


😉 所以,作为一个聪明的求职者,我们应该如何回答这个问题呢?


6 个常见的离职原因


首先,让我们来看看这 6 个常见的离职原因❌,以及它们的“禁忌”回答方式吧!🚫


🎯 工资太低

🚫 禁忌回答:因为前公司给的工资太低了,我实在是受不了了。



🎯 加班严重

🚫 禁忌回答:前公司加班太严重了,我身体都垮了。



🎯 领导傻 X

🚫 禁忌回答:前领导简直就是个傻 X,根本不懂管理。



🎯 被裁员

🚫 禁忌回答:我之前的公司裁员,我被无辜波及了。



🎯 无法升职

🚫 禁忌回答:我在前公司干了几年,但一直升不上去。



🎯 同事相处不和谐

🚫 禁忌回答:前公司的同事们都太奇葩了,我实在受不了。



👀 看到了吗?这些回答都是大忌,因为它们会让 HR 觉得你不稳定、有负面情绪、甚至可能是个问题员工。那么,正确的回答方式是什么呢?别急,我们接着往下看!✅


🎯 工资太低

✅ 回答模板:HR 您好!我过去 2 年,在公司和领导的帮助下,我在技术和能力方面有了很大提升,为公司开发的各种工具和库,极大地提高了项目开发效率,领导对我很认可,但公司的薪资结构有一些硬指标,无法满足我的涨薪需求,所以想看看市场上的新机会,寻找一个可以长期稳走发展的平台。



🎯 加班严重

✅ 回答模板:HR 您好!在上家公司,我的工作效率较高,能按时保质保量地完成工作,但前司会定期统计加班时长,并以此进行考核,导致大部分人为了加班而加班,效率非常低,我个人并不反对加班,但这种低效的常态化 996,我并不认可,我希望能在更人性化管理的公司里发挥优势。



🎯 领导傻 X(这个有点难,但我们可以尽量委婉)

✅ 回答模板:HR 您好!因为前公司的工作职贵划分比较固定,每个人负责的业务比较少,对于所负责的开发工作,我已经没有什么新知识可以学的了,我希望职业生涯是一个持续的学习和提升的过程,并且在自己具备了相关业务技能经验后,也想迎接一些新的挑战。



🎯 被裁员

✅ 回答模板:HR 您好!前公司当时在进行业务调整,公司想让我调到其他业务线上,岗位职责也发生了变化,我觉得自己还是很擅长做前端工具链方向的开发,并且也在这块积累了几年经验,还是想继续在这个方向上深耕,因此出来看看机会。



🎯 无法升职

✅ 回答模板:HR 您好!前公司管理比较扁平,虽然一直被领导重用,也负责过多个核心业务,但是职级几年来来一直没有变化。领导也争取过,但由于工作年限等原因,没能通过。我不想安于现状,想找一个更能发挥自己才能的平台。



🎯 同事相处不和谐

✅ 回答模板:HR 您好!我之前在前司能力不错,取得了不错的成绩,但由于分工不太合理,出现了很多扯皮现象和低效沟通,影响到了工作效率,有时甚至会影响整个项目的进度,内耗较大,所以决定离开,希望找一个氛国好团队长期发展。



💡 记住了吗?面试时回答离职原因的关键在于:不要贬低前司和前同事不要让 HR 觉得是你的问题也不要吐槽前司的任何不足。掌握这些要领,相信你在面试中一定能够游刃有余地应对这个问题啦!




作者:前端开发爱好者
来源:juejin.cn/post/7351321275975843867
收起阅读 »

我改进了数据滚动方式!老板直接加薪

web
需求背景 前几天,甲方提了个需求,想让下面的数据循环展示,准备放在他们集团首页给他们领导演示用。 我们领导很重视这个事儿,拍了拍我,语重心长的说,小伙子,好好做。 我啪的一下就兴奋了,老板居然如此器重我,我必当鞠躬尽瘁,减少摸鱼,我要让老板拜倒在精湛的技术下...
继续阅读 »

需求背景


前几天,甲方提了个需求,想让下面的数据循环展示,准备放在他们集团首页给他们领导演示用。



我们领导很重视这个事儿,拍了拍我,语重心长的说,小伙子,好好做。


我啪的一下就兴奋了,老板居然如此器重我,我必当鞠躬尽瘁,减少摸鱼,我要让老板拜倒在精湛的技术下!


于是,我搬出自己的库存代码,仅2min就实现了数据的滚动:


没错,我直接照搬了自己以前写过的文章:JS实现可滚动区域自动滚动展示 - 掘金


就在我准备告诉老板我做完了的时候,我突然想了想,这么快做完,老板一定觉得我没好好做,我以后还怎么升职加薪,赢取白富美?


于是,我连夜研究,终于改进了数据滚动方式,赢得了老板的大饼(以后涨500)。最终效果:



技术方案


技术选型


观察最终效果图,可以发现这其实就是一个数据循环滚动的效果,每条内容之间间隔1000ms,每条出现动的时间为500ms。用术语来说,这就是一个单步停顿滚动效果。


我百度了一下,社区还是有这个实现的现成方案的:vue-seamless-scroll,周下载也还行。



于是,我果断试了试,结果不知道什么原因,并不生效...


既然如此,直接手写一个吧!


实现思路


要实现上述效果其实很简单,如图



我们创造一个含有六个值的数组,每隔一段时间循环更改黄色区域的数据,当黄色区域数据变成最新的时候,红色区域整体向下移动,当有数值超出滚动区域后,在删除这个数据即可。


数据更新


如果不考虑动画,我们的代码应该这么写


<template>
<div class="item-wrap" v-for="(item, index) in animationData">
<!-- 模块内容 -->
</div>

</template>
<script setup lang="ts">
// #假设这是接口请求的10条最新数据
const allCarouseData = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
// #需要轮播的数据
const animationData = ref<any>([])
// *定时器
const animationTimerMeta: any = {
timer: null,
// 这个函数负责设置轮播数据的更新逻辑。
timeFuc() {
let setTimeoutId: any = null
if (this.timer) return
this.timer = setInterval(() => {
// 取轮播数据的第一条id
let firstId = animationData.value[0].id
// 为轮播数据添加最新的第一项数据
let index = allCarouseData.value.findIndex((res: any) => res.id === firstId)
let addIndex = index - 1 < 0 ? allCarouseData.value.length - 1 : index - 1
animationData.value.unshift(allCarouseData.value[addIndex])
setTimeout(() => {
// 删除数组的最后一项
animationData.value.pop()
}, 1000)

}, 1500)
}
}
animationData.value = allCarouseData.value.slice(-5)
animationTimerMeta.timeFuc()
</script>

上述代码的主要功能是:



  1. 从 allCarouseData 中取出最后5个元素作为初始的轮播数据。

  2. 每1.5秒更新一次轮播数据,具体逻辑是:移除当前 animationData 的第一个元素,并从 allCarouseData 中取出前一个元素(如果已经是第一个元素,则取最后一个)添加到 animationData 的开头。

  3. 每1秒从 animationData 的末尾移除一个元素。


上述代码没有实现动画,他的效果是这样的:



动画添加


<template>
<div class="item-wrap" v-for="(item, index) in animationData"
:class="[{ moveToBottom: animationActive }, { show: animationActive && index === 0 }]"
>

<!-- 模块内容 -->
</div>

</template>
<script setup lang="ts">
// #假设这是接口请求的10条最新数据
const allCarouseData = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
// #需要轮播的数据
const animationData = ref<any>([])
// #是否开启动画
const animationActive = ref(false)
// *定时器
const animationTimerMeta: any = {
timer: null,
// 这个函数负责设置轮播数据的更新逻辑。
timeFuc() {
let setTimeoutId: any = null
if (this.timer) return
this.timer = setInterval(() => {
// 取轮播数据的第一条id
let firstId = animationData.value[0].id
// 为轮播数据添加最新的第一项数据
let index = allCarouseData.value.findIndex((res: any) => res.id === firstId)
let addIndex = index - 1 < 0 ? allCarouseData.value.length - 1 : index - 1
animationData.value.unshift(allCarouseData.value[addIndex])
setTimeout(() => {
// 删除数组的最后一项
animationData.value.pop()
}, 1000)

}, 1500)
}
}
animationData.value = allCarouseData.value.slice(-5)
animationTimerMeta.timeFuc()
</script>


@keyframes moveToBottom {
0% {
transform: translateY(-47px);
}

100% {
transform: translateY(0);
}
}

.moveToBottom {
animation: moveToBottom 500ms ease-in-out forwards;
}

@keyframes fadeInFromTop {
0% {
opacity: 0;
transform: translateY(-47px);
}

100% {
opacity: 1;
transform: translateY(0);
color: #683BD6;
}
}

.show {
animation: fadeInFromTop 500ms ease-in-out forwards;
}

上述代码中,为了实现动画效果,采用了动态添加类名的技术方案。


animationData 数组中的元素会按照一定顺序进行显示和隐藏,同时伴随有动画效果。当第一个元素进入视图时,它会应用 fadeInFromTop 动画;其他元素会应用 moveToBottom 动画。通过定时器,元素会定期从 allCarouseData 中获取新的数据并更新 animationData。


代码释义:



  • moveToBottom: 当 animationActive 为真值时,此类名会被添加到 div 上。

  • show: 当 animationActive 为真值且当前元素是数组的第一个元素时,此类名会被添加到 div 上。


CSS 释义:



  • moveToBottom 动画:


定义一个名为 moveToBottom 的关键帧动画,使元素从上方移动到其原始位置。


moveToBottom 类将此动画应用到元素上。



  • fadeInFromTop 动画:


定义一个名为 fadeInFromTop 的关键帧动画,使元素从上方淡入并改变颜色。


show 类将此动画应用到元素上。


通过上述简单的实现方式,就能最终实现我们想要的效果



相比于普通滚动,这种方式看起来要好很多!


结语


要想实现这种单步停帧的效果,其实有很多实现方式,这只是笔者实现的一种,核心逻辑就是动态改变数据、增添类名。如果大家还有更好的方式,也欢迎大家指点。


作者:石小石Orz
来源:juejin.cn/post/7348433631944556555
收起阅读 »

只写后台管理的前端要怎么提升自己

web
本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过(虽然我现在已经心安理得的摆烂),但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。 写优雅的代码 一道面试题 大概两年以前,面试美团的时候...
继续阅读 »

本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过(虽然我现在已经心安理得的摆烂),但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。


写优雅的代码


一道面试题


大概两年以前,面试美团的时候,面试官让我写一道代码题,时间单位转换。具体的题目我忘记了。


原题目我没做过,但是我写的业务代码代码里有类似的单位转换,后端返回一个数字,单位是kb,而我要展示成 KBMB 等形式。大概写一个工具函数(具体怎么写的忘记了,不过功能比这个复杂点):


function formatSizeUnits(kb) {
let units = ['KB', 'MB', 'GB', 'TB', 'PB'];
let unitIndex = 0;

while (kb >= 1024 && unitIndex < units.length - 1) {
kb /= 1024;
unitIndex++;
}

return `${kb.toFixed(2)} ${units[unitIndex]}`;
}

而在此之前,是一个后端同学写的代码(说起来,后台管理系统前端人力不够后端帮忙写好像是常规操作,后端总觉得写前端简单,而我只能说你们那代码写的,一言难尽……),如下:


function formatSizeUnits(kb) {
var result = '';

if (kb < 1024) {
result = kb + ' KB';
} else if (kb < 1024 * 1024) {
result = (kb / 1024).toFixed(2) + ' MB';
} else if (kb < 1024 * 1024 * 1024) {
result = (kb / 1024 / 1024).toFixed(2) + ' GB';
} else {
result = (kb / 1024 / 1024 / 1024).toFixed(2) + ' TB';
}

return result;
}

虽然功能都一样,但是我写在面试的时候写了上面的代码得到了面试官的赞扬,也因此非常顺利的通过了面试。而如果我写成了下面的样子,估计不会有这种效果。


如何提升代码质量


想提升代码质量,最理想的是靠 code review ,但实际上这玩意在大多数公司根本就不存在。其次就只能多看看大佬们写的代码,但是一来后台管理本来就没太大难度很少有大佬,二来很多时候一个后台管理系统就一两个人负责。


还好现在有了各种 AI 工具,可以非常方便的优化自己的代码了。


还是上面的问题,看看 GPT 给的答案


// 提问:写一个工具函数,对文件大小的单位进行转换,加注释
// 回答:下面是一个JavaScript工具函数,它接受文件大小、初始单位、目标单位和小数位数,然后返回目标单位下的文件大小。

/**
* 将文件大小从一个单位转换为另一个单位。
*
* @param {number} size 文件大小。
* @param {string} fromUnit 初始单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {string} toUnit 目标单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {number} [decimalPoint=2] 结果保留的小数位数,默认为2。
* @return {string} 转换后的文件大小,带单位。
*/

function convertFileSize(size, fromUnit, toUnit, decimalPoint = 2) {
// 定义单位与字节之间的转换关系
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
// 获取初始单位和目标单位的索引
const fromIndex = units.indexOf(fromUnit);
const toIndex = units.indexOf(toUnit);

// 如果单位不在列表中,抛出错误
if (fromIndex === -1 || toIndex === -1) {
throw new Error('Invalid units');
}

// 计算初始单位与目标单位之间的转换系数
const exponent = toIndex - fromIndex;
// 计算结果大小
const resultSize = size / Math.pow(1024, exponent);

// 返回格式化后的结果
return parseFloat(resultSize.toFixed(decimalPoint)) + ' ' + toUnit;
}

// 示例使用
console.log(convertFileSize(1, 'GB', 'MB')); // 输出: 1024.00 MB
console.log(convertFileSize(1, 'MB', 'KB')); // 输出: 1024.00 KB
console.log(convertFileSize(1, 'KB', 'B')); // 输出: 1024.00 B
console.log(convertFileSize(1, 'MB', 'GB', 5)); // 输出: 0.00098 GB

还能说什么,我只能说 NB!(经评论区提示,此段代码可能有问题,建议大家在使用AI工具时,重点是借鉴思路,直接使用代码需要谨慎~)


我现在每次写大段逻辑之后都习惯拿给 AI 看看,有什么更好的实现方式,或者用什么设计模式。AI 是非常低成本且高效提升代码质量的工具。


学会封装


一个功能用到了好多次,为什么不封装成组件?一个组件用到了好几个项目,为什么不单独写个npm包?差不多的项目创建了好几个,为什么不封装成脚手架?


你说,没时间,没必要,复制粘贴反而更快。


那你就完全没理解,这么做不一定是为了让工作更快完成,而是可以让你在年年终述职时更有话说(你就算写了一百个表单表格没有写一个脚手架更值得炫耀),如果不会写可以问问 AI。


而当你真正开始封装组件,开始写工具库了,你会发现你需要思考的确实比之前多了。


关注业务


对于前端业务重要吗?


相比于后端来说,前端一般不会太关注业务。就算出了问题大部分也是后端的问题。


但是就我找工作的经验,业务非常重要!


如果你做的工作很有技术含量,比如你在做低代码,你可以面试时讲一个小时的技术难点。但是你只是一个破写后台管理,你什么都没有的说。这个时候,了解业务就成为了你的亮点。


一场面试


还是拿真实的面试场景举例,当时前同事推我字节,也是我面试过N次的梦中情厂了,刚好那个组做的业务和我之前呆的组做的一模一样。



  • 同事:“做的东西和咱们之前都是一样的,你随便走个过场就能过,我在前端组长面前都夸过你了!”

  • 我:“好嘞!”


等到面试的时候:



  • 前端ld:“你知道xxx吗?(业务名词)”

  • 我:“我……”

  • 前端ld:“那xxxx呢?(业务名词)”

  • 我:“不……”

  • 前端ld:“那xxxxx呢??(业务名词)”

  • 我:“造……”


然后我就挂了………………


如何了解业务



  1. 每次接需求的时候,都要了解需求背景,并主动去理解


    我们写一个表格简简单单,把数据展示出来就好,但是表格中的数据是什么意思呢?比如我之前写一个 kafka 管理平台,里面有表格表单,涉及什么 cluster controller topic broker partition…… 我真的完全不了解,很后悔我几年时间也没有耐下心来去了解。


  2. 每次做完一个需求,都需要了解结果


    有些时候,后台管理的团队可能根本没有PM,那你也要和业务方了解,这个功能做了之后,多少人使用,效率提高了吗?数据是怎样的?


  3. 理解需求,并主动去优化


    产品要展示一千条数据,你要考虑要不要分页,不分页会不会卡,要不要上虚拟表格?


    产品要做一个可拖拽表单,你要考虑是否需要拖动,是否需要配置。


    其实很多时候,产品的思维可能会被局限在竞品的实现方式,而前端可以给TA更多选项。在和产品沟通的时候,你不仅是沟通页面的实现,也更能理解业务。



关注源码


说到源码, Vue,React 这些基本是每次必问,也有太多大佬们的总结我就不多说了。


除此之外,我想大家每次面试应该都会被问,你写了什么亮点的项目,困难的项目,你会说什么?哦,都是表单表格,都用 Element UI 实现的,没什么难度。那估计你面试也就到这里了。


那说什么,后台管理就这些啊?!


如果你用了 Element UI 你可以说写了什么自定义 Element UI 表单组件,Element UI 表单源码是什么原理,怎么通信的?用了 Axios 可以说 Axios 的原理,它的拦截器怎么实现的?用了编辑器 diff 那你知道 diff 算法是什么吗?用了 ECharts 那你知道他底层是用什么绘制的吗?用了 husky ?husky的原理是什么?用了Vite,它为什么快?是怎么打包的?写过插件吗?


可能有人觉得我离谱,事实上上面每一个问题都是我在面试中真实被问到的。当然,如果全部去了解,肯定不现实,每个人的精力有限,不可能每个用到的每个工具每个库都去了解原理,不过只要有一两个你去了解过,就可以在面试中引导面试官来问,要学会表现出你擅长的点


至于怎么读源码?以前我不知道,现在嘛…… 为什么不问问神奇的 ChatGPT 呢?



讲一下 Axios 源码中,拦截器是怎么实现的?


Axios 的拦截器实现基于 Axios 的核心原理,即 Axios 实例是一个包含请求和响应拦截器堆栈的对象。当发出请求或接收响应时,Axios 会遍历这些拦截器,并按照添加的顺序执行请求拦截器,以及按照相反的顺序执行响应拦截器。


在 Axios 的源码中,拦截器是通过一个 AxiosInterceptorManager 实例来管理的,它维护了一个拦截器数组。每个拦截器都是一个包含 fulfilledrejected 函数的对象。这两个函数分别对应于拦截器成功处理和拦截器处理出错的情况。


以下是 Axios 拦截器管理器的一个简化版本,展示了其核心实现思路:


class InterceptorManager {
constructor() {
this.handlers = []; // 存储拦截器的数组
}

use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1; // 返回拦截器的ID
}

eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null; // 移除拦截器
}
}

forEach(fn) {
this.handlers.forEach((h) => {
if (h !== null) {
fn(h);
}
});
}
}

在发送请求或接收响应时,Axios 会创建一个 promise 链,并通过 forEach 方法将拦截器中的 fulfilledrejected 函数添加到这个链中。这样,每个拦截器都可以对请求或响应进行处理,然后将结果传递到链的下一个拦截器,或者在出错时结束链的执行。


axios.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

axios.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});

在 Axios 的完整实现中,这个拦截器机制被集成到了 Axios 的请求发送和响应处理流程中。通过这种方式,Axios 可以在发送请求之前和接收响应之后,但在用户定义的 .then.catch 执行之前,插入自定义的逻辑。


请注意,这里提供的代码只是为了说明 Axios 拦截器的实现原理,并不是 Axios 源码的完整复制。如果你对 Axios 的拦截器实现细节感兴趣,建议查看 Axios 的官方 GitHub 仓库中的源码。



前端基建


当我们工作时间久了面试难免会遇到这些问题,前端工程化,前端监控,工作流,部署,性能等等。其实我们在工作中绝大部分时间都在写代码,对于这些不是所有人都有机会接触到,不过这些和所做的业务无关,是我们提升自己很好的一个思路。


技术选型


技术栈选 Vue 还是 React?Vue 选 Vue2 还是 Vue3?组件库选 ElementUI 还是 Ant Design?微前端有没有使用过?打包工具用 Vite 还是 Webpack?有那么多表单怎么实现的,有没有什么表达配置化方案,比如Formily?


对于我这种菜鸡,我这种只写简单的表单表格的人,这些都……无所谓……


image.png

不过为了应对面试我们还是需要了解下未选择技术栈的缺点,和已选择技术栈的优点(有点本末倒置…但是常规操作啦)


Vue 你可以说简单高效轻量级,面试必会问你为什么,你就开始说 Vue 的响应式系统,依赖收集等。


React 你可以说 JSX、Hooks 很灵活,那你必然要考虑 JSX 怎么编译, Hooks 实现方式等。


总体而言,对于技术选型,依赖于我们对所有可选项的理解,做选择可能很容易,给出合理的理由还是需要花费一些精力的。


开发规范


这个方面,在面试的时候我被问到的不多,我们可以在创建项目的时候,配置下 ESlintstylelintprettiercommitlint 等。


前端监控


干了这么多年前端,前端监控我是……一点没做过。


image.png

前端监控,简单来说就是我们在前端程序中记录一些信息并上报,一般是错误信息,来方便我们及时发现问题并解决问题。除此之外也会有性能监控,用户行为的监控(埋点)等。之前也听过有些团队分享前端监控,为了出现问题明确责任(方便甩锅)。


对于实现方案,无论使用第三方库还是自己实现,重要的都是理解实现原理。


对于错误监控,可以了解一下 Sentry,原理简单来说就是通过 window.onerrorwindow.addEventListener('unhandledrejection', ...) 去分别捕获同步和异步错误,然后通过错误信息和 sourceMap 来定位到源码。


对于性能监控,我们可以通过 window.performancePerformanceObserver 等 API 收集页面性能相关的指标,除此之外,还需要关注接口的响应时间。


最后,收集到信息之后,还要考虑数据上报的方案,比如使用 navigator.sendBeacon 还是 Fetch、AJAX?是批量上报,实时上报,还是延迟上报?上报的数据格式等等。


CI/CD


持续集成(Continuous Integration, CI)和 持续部署(Continuous Deployment, CD),主要包括版本控制,代码合并,构建,单测,部署等一系列前端工作流。


场景的工作流有 Jenkins、 Gitlab CI 等。我们可以配置在合并代码时自动打包部署,在提交代码时自动构建并发布包等。


这块我了解不多,但感觉这些工具层面的东西,不太会涉及到原理,基本上就是使用的问题。还是需要自己亲自动手试一下,才能知道细节。比如在 Gitlab CI 中, Pipeline 、 Stage 和 Job 分别是什么,怎么配置,如何在不同环境配置不同工作流等。


了解技术动态


这个可能还是比较依赖信息收集能力,虽然我个人觉得很烦,但好像很多领导级别的面试很愿意问。


比如近几年很火的低代码,很多面试官都会问,你用过就问你细节,你没用过也会问你有什么设计思路。


还有最近的两年爆火的 AI,又或者 Vue React的最新功能,WebAssembly,还有一些新的打包工具 Vite Bun 什么的,还有鸿蒙开发……


虽然不可能学完每一项新技术,但是可以多去了解下。


总结


写了这么多,可能有人会问,如果能回到过去,你会怎么做。


啊,我只能说,说是一回事,做又是另一回事,事实上我并不希望回到过去去卷一遍,菜点没关系,快乐就好,一切都是最好的安排。


image.png

作者:我不吃饼干
来源:juejin.cn/post/7360528073631318027
收起阅读 »

三方接口不动声色将http改为了https,于是开启了我痛苦的一天

早上刚来,就看到仓库那边不停发消息说,我们的某个功能用不了了。赶紧放下早餐加紧看。 原来是调的一个三方接口报错了: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorExc...
继续阅读 »

早上刚来,就看到仓库那边不停发消息说,我们的某个功能用不了了。赶紧放下早餐加紧看。


原来是调的一个三方接口报错了:


javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.ssl.Alert.createSSLException(Alert.java:131)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:353)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:296)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:291)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.checkServerCerts(CertificateMessage.java:652)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.onCertificate(CertificateMessage.java:471)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.consume(CertificateMessage.java:367)
at sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:376)
at sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:444)
at sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:422)
at sun.security.ssl.TransportContext.dispatch(TransportContext.java:183)
at sun.security.ssl.SSLTransport.decode(SSLTransport.java:154)
at sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1279)
at sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1188)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:401)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:373)

查看原因:



由于JVM默认信任证书不包含该目标网站的SSL证书,导致无法建立有效的信任链接。



奥...原来是他们把接口从http改为了https,导致我们获取数据报错了。再看看他们的证书,奥...新的。


image.png


好了,看看我们的逻辑,这其实是一个获取对方生成的PDF文件的接口


PdfReader pdfReader = new PdfReader(url);

url就是他们给的链接,是这行代码报的错。这时候,开始研究,在网上扒拉,找到了初版方案


尝试1


写一个程序专门获取安全证书,这代码有点长,全贴出来影响阅读。我给扔我hithub上了github.com/lukezhao6/I… 将这个文件贴到本地,执行javac InstallCert.java将其进行编译


image.png


编译完长这样:


image.png
然后再执行java InstallCert http://www.baidu.com (这里我们用百度举例子,实际填写的就是你想要获取证书的目标网站)


image.png
报错不用怕,因为它会去检查目标服务器的证书,如果出现了SSLException,表示证书可能存在问题,这时候会把异常信息打印出来。


在生成的时候需要输入一个1


image.png
这样,我们需要的证书文件就生成好了


image.png


这时候,将它放入我们本地的 jdk的lib\security文件夹内就行了


image.png


重启,这时候访问是没有问题了。阶段性胜利。


但是,但是。一顿操作下来,对于测试环境的docker,还有生产环境貌似不能这么操作。 放这个证书文件比较费事。


那就只能另辟蹊径了。


尝试2


搜到了,还有两种方案。



1.通过System.setProperty("javax.net.ssl.trustStore", "你的jssecacerts证书路径");


2.程序启动命令-Djavax.net.ssl.trustStore=你的jssecacerts证书路径 -Djavax.net.ssl.trustStorePassword=changeit



我尝试了第一种,System.setProperty可以成功,但是读不到文件,权限什么的都是ok的。
检查了蛮多地方



  • 路径格式问题

  • 文件是否存在

  • 文件权限

  • 信任库密码

  • 系统属性优先级


貌似都是没问题的,但肯定又是有问题的,因为没起作用。但是想着这样的接口有4个,万一哪天其他三个也改了,我又得来一遍。所以就算研究出来了,还是不能稳坐钓鱼台。有没有一了百了的方法嘞。


尝试3


还真找到了:这个错是因为对方网站的证书不被java信任么,那咱不校验了,直接全部信任。这样就算其他接口改了,咱也不愁。而且这个就是获取pdf,貌似安全性没那么重。那就开搞。


代码贴在了下方,上边的大概都能看懂吧,下方的我加了注释。


URL console = new URL(url);
HttpURLConnection conn = (HttpURLConnection) console.openConnection();
if (conn instanceof HttpsURLConnection) {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, new TrustManager[]{new TrustAnyTrustManager()}, new java.security.SecureRandom());
((HttpsURLConnection) conn).setSSLSocketFactory(sc.getSocketFactory());
((HttpsURLConnection) conn).setHostnameVerifier(new TrustAnyHostnameVerifier());
}
conn.connect();
InputStream inputStream = conn.getInputStream();
PdfReader pdfReader = new PdfReader(inputStream);
inputStream.close();
conn.disconnect();

private static class TrustAnyTrustManager implements X509TrustManager {
//这个方法用于验证客户端的证书。在这里,方法体为空,表示不对客户端提供的证书进行任何验证。
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
//这个方法用于验证服务器的证书。同样,方法体为空,表示不对服务器提供的证书进行任何验证。
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
//这个方法返回一个信任的证书数组。在这里,返回空数组,表示不信任任何证书,也就是对所有证书都不做任何信任验证。
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
}
//这个方法用于验证主机名是否可信。在这里,无论传入的主机名是什么,方法始终返回 true,表示信任任何主机名。这就意味着对于 SSL 连接,不会对主机名进行真实的验证,而是始终接受所有主机名。
private static class TrustAnyHostnameVerifier implements HostnameVerifier {
public boolean verify(String hostname, SSLSession session) {
return true;
}
}

解决了解决了,这样改算是个比较不错的方案了吧。


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

开发需求记录:实现app任意界面弹框与app置于后台时通知

前言 在产品经理提需求时候提到,app在接收到报警信息时候能不能弹出一个弹框,告诉用户报警信息,这个弹框要在app的任意界面能够弹出,并且用户点击详情时候,会跳转到报警详情界面,查看具体信息,当用户将app至于后台的时候,接收到报警信息,app发送通知,当用户...
继续阅读 »

前言


在产品经理提需求时候提到,app在接收到报警信息时候能不能弹出一个弹框,告诉用户报警信息,这个弹框要在app的任意界面能够弹出,并且用户点击详情时候,会跳转到报警详情界面,查看具体信息,当用户将app至于后台的时候,接收到报警信息,app发送通知,当用户点击通知时候,跳转到报警详情界面。
功能大体总结如上,在实现弹框与通知在跳转界面时遇到一些问题,在此记录一下。效果图如下:


开发需求 - 通知与弹框.gif


功能分析


弹框实现,使用DialogFragment。

前后台判断则是,创建一个继承自ActivityLifecycleCallbacks接口和Application的类,继承ActivityLifecycleCallbacks接口是为了前后台判断,继承Application则是方便在基类BaseActivity获取前后台相关数据。

项目原本采用单Activity多Fragment实现,后面因为添加了视频相关功能,改为了多Activity多Fragment。

原单Activity时候,实现比较容易。后面修改为多Activity,就有些头疼,最终用思路是创建基类BaseActivity,后面添加Activity时都要继承基类BaseActivity。使用基类原因是把相同的功能抽取出来,且若每个Activity都自己实现弹框和通知的话太容易出错,也太容易漏下代码了。


代码实现


弹框


在实现继承自DialogFragment的弹框时,需要在onCreateDialog方法内设置dialog的宽高模式以及背景,不然弹框会有默认的边距,导致显示效果与预期不符,未去边距与去掉边距的弹框效果如下:
image.png
关于onCreateDialog的代码如下:


override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = Dialog(requireContext())
dialog.setContentView(R.layout.custom_dialog_layout)
dialog.window?.apply {
setLayout(ViewGr0up.LayoutParams.MATCH_PARENT,ViewGr0up.LayoutParams.MATCH_PARENT)
setBackgroundDrawable(ColorDrawable(Color.parseColor("#88000000")))//去掉DialogFragment的边距
}
dialog.setCancelable(false)
return dialog
}

此外当弹框出现的时候,弹框背景色还会闪烁。这里采用属性值动画设置弹框背景色控件的透明度变换。完整的Dialog代码如下:


class AlarmDialogFragment: DialogFragment() {
private lateinit var binding:CustomDialogLayoutBinding
private var animator:ObjectAnimator? = null

override fun show(manager: FragmentManager, tag: String?) {
try {
super.show(manager, tag)
}catch (e:Exception){
e.printStackTrace()
}
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGr0up?,
savedInstanceState: Bundle?
)
: View? {
binding = CustomDialogLayoutBinding.inflate(inflater)
return binding.root
}

override fun onStart() {
super.onStart()
binding.viewAlarmDialogBg
startAnimation()
}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = Dialog(requireContext())
dialog.setContentView(R.layout.custom_dialog_layout)
dialog.window?.apply {
setLayout(ViewGr0up.LayoutParams.MATCH_PARENT,ViewGr0up.LayoutParams.MATCH_PARENT)
setBackgroundDrawable(ColorDrawable(Color.parseColor("#88000000")))//去掉DialogFragment的边距
}
dialog.setCancelable(false)
return dialog
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
}

override fun onDestroy() {
super.onDestroy()
if(animator?.isStarted == true){
animator?.end()
}
}

private fun initView() {
binding.btnCloseDialog.setOnClickListener {
dismiss()
}

binding.btnDialogNav.setOnClickListener {
if(context is MainActivity){
val bundle = Bundle()
bundle.putString("alarmId","1")
findNavController().navigate(R.id.alarmDetailFragment,bundle)
}else{
val intent = Intent(context,MainActivity::class.java)
intent.putExtra("task","toAlarmDetail")
startActivity(intent)
}
dismiss()
}
}

private fun startAnimation() {
animator = ObjectAnimator.ofFloat(binding.viewAlarmDialogBg, "alpha", 0f, 0.6f, 0f, 0.6f, 0f)
animator?.duration = 1200
animator?.interpolator = AccelerateInterpolator()
animator?.start()
}
}

需要注意地方是,由于弹框还负责跳转,而跳转有两种情况,一种是在ActivityA内,fragmentA与fragmentB间的跳转,这种情况使用findNavController().navigate()方法进行跳转,另一种是ActivityB到另一个ActivityA内的指定FragmentB界面。这种采用startActivity(intent)方式跳转,并且在ActivityA的onStart()的方法使用下面方法。


/** 跳转报警详情界面 */
private fun initToAlarmDetail() {
val task = intent.getStringExtra("task")
if (task == "toAlarmDetail"){
val bundle = Bundle()
bundle.putString("alarmId","1")
findNavController(R.id.fragment_main_table).navigate(R.id.alarmDetailFragment,bundle)
}
}

这样从ActivityB到另一个ActivityA时候,在onStart()方法内会触发上面的initToAlarmDetail()方法,获取跳转里面的信息,在决定具体跳转到哪个Fragment。这里解释的可能不太清楚,可以在Github下载源码看看可能更好理解些。


弹框对应的xml文件代码,可以在Github内查看,可以自己写一个,这个xml比较简单,只是xml代码比较占地方这里就不粘贴了。


前后台判断


关于前后台判断,需要创建一个继承ActivityLifecycleCallbacks和Application的类,这里命名为CustomApplication,在类里面实现ActivityLifecycleCallbacks接口相关方法,此外需要创建下面三个变量,分别表示activity数量,当前activity的名称,是否处于后台,代码如下:


private var activityCount = 0
private var nowActivityName:String? = null
private var isInBackground = true

之后需要在onActivityStarted,onActivityResumed,onActivityStopped方法内进行前后台相关处理,代码如下:


override fun onActivityStarted(activity: Activity) {
activityCount++
if (isInBackground){
isInBackground = false
}
nowActivityName = activity.javaClass.name
}

override fun onActivityStopped(activity: Activity) {
activityCount--
if (activityCount == 0 && !isInBackground){
isInBackground = true
}
}

上面代码可以看出,当触发onActivityStarted方法时候,activityCount数量加一,且app处于前台。之后记录当前activity名称,这里记录activity名称是后面有个功能是app置于后台时候弹出通知,而通知相关操作,为了每个activity都能实现就放在基类执行,而弹出通知并不需要每个继承基类的activity都执行,到时候需要根据根据nowActivityName判断哪个继承了基类的activity执行通知操作。


当触发onActivityStopped方法时候,activityCount数量减一,且当activityCount数量为零时,app置于后台。
CustomApplication完整代码如下:


class CustomApplication: Application(),Application.ActivityLifecycleCallbacks {
companion object{
const val TAG = "CustomApplication"
@SuppressLint("CustomContext")
lateinit var context: Context
}

private var activityCount = 0
private var nowActivityName:String? = null
private var isInBackground = true

fun getNowActivityName(): String? {
return nowActivityName
}

fun getIsInBackground():Boolean{
return isInBackground
}

override fun onCreate() {
super.onCreate()
context = applicationContext
registerActivityLifecycleCallbacks(this)
}

override fun onActivityCreated(activity: Activity, p1: Bundle?) {

}

override fun onActivityStarted(activity: Activity) {
activityCount++
if (isInBackground){
isInBackground = false
}
nowActivityName = activity.javaClass.name
}

override fun onActivityResumed(activity: Activity) {

}

override fun onActivityPaused(activity: Activity) {

}

override fun onActivityStopped(activity: Activity) {
activityCount--
if (activityCount == 0 && !isInBackground){
isInBackground = true
}
}

override fun onActivitySaveInstanceState(activity: Activity, p1: Bundle) {

}

override fun onActivityDestroyed(activity: Activity) {

}
}

弹框与通知弹出


开发中弹框与通知弹出的触发条件是,监听Websocket若有信息过来,app处于前台弹框,处于后台弹通知。这里使用Handler来模拟,弹框弹出比较简单,若有继承了DialogFragment的AlarmDialogFragment类。代码如下:


val dialog = AlarmDialogFragment()
dialog.show(supportFragmentManager,"tag")

通知弹出也不难,若只是弹出通知示例代码如下:


if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel("normal", "Normal", NotificationManager.IMPORTANCE_DEFAULT)
notificationManager?.createNotificationChannel(channel)
}
val notification = NotificationCompat.Builder(this.applicationContext, "normal")
.setContentTitle("标题")
.setContentText("通知次数:${++alarmCount}")
.setSmallIcon(R.drawable.ic_launcher_background)
.setTimeoutAfter(5000)
.setAutoCancel(true)
.build()
notificationManager?.notify(notificationId,notification)

弹框与通知的特殊要求是,能在界面任意地方弹出且跳转到指定界面。弹框跳转相关代码在上面'弹框'部分,下面来说下通知的跳转,点击通知跳转是通过创建PendingIntent后在设置进NotificationCompat的setContentIntent方法内,不过通知跳转与弹框跳转一样需要分两种情况考虑,第一种同一Activity内Fragment与Fragment跳转,这种情况下PendingIntent如下代码所示:


var pendingIntent:PendingIntent? = null
val bundle = Bundle()
bundle.putString("alarmId","1")
pendingIntent = NavDeepLinkBuilder(this)
.setGraph(R.navigation.main_navigation)
.setDestination(R.id.alarmDetailFragment)
.setArguments(bundle)
.createPendingIntent()

上面代码中使用NavDeepLinkBuilder创建了一个PendingIntent,并且使用setGraph()指向使用的导航图,setDestination()则指向目标Fragment。
另一种情况则是ActivityB到另一个ActivityA内的指定FragmentB界面,这种情况下PendingIntent设置代码如下:


val intent = Intent(this@BaseActivity,MainActivity::class.java)
intent.putExtra("task","toAlarmDetail")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
pendingIntent = TaskStackBuilder.create(this@BaseActivity)
.addNextIntentWithParentStack(intent)
.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT)

这种是创建一个跳转到MainActivity的Intent,并添加传递的参数task,接着设置Intent的启动方式,其中Intent.FLAG_ACTIVITY_NEW_TASK,表示启动Activity作为新任务启动,Intent.FLAG_ACTIVITY_CLEAR_TASK,表示清除任务栈中所有现有的Activity。之后调用TaskStackBuilder创建PendingIntent。
上面两种方式创建的PendingIntent可以通过NotificationCompat.setContentIntent(pendingIntent)添加进去,关于通知创建的代码如下:


/** 使用通知 - 通过pendingIntent实现跳转,缺点是任意界面进入报警详情界面,点击返回键只能返回MainFragment */
private fun useNotificationPI() {
var pendingIntent:PendingIntent? = null
if(javaClass.simpleName == "MainActivity"){//主界面
val bundle = Bundle()
bundle.putString("alarmId","1")
pendingIntent = NavDeepLinkBuilder(this)
.setGraph(R.navigation.main_navigation)
.setDestination(R.id.alarmDetailFragment)
.setArguments(bundle)
.createPendingIntent()
}else {//其他界面时候切换后台通知
val intent = Intent(this@BaseActivity,MainActivity::class.java)
intent.putExtra("task","toAlarmDetail")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
pendingIntent = TaskStackBuilder.create(this@BaseActivity)
.addNextIntentWithParentStack(intent)
.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel("normal", "Normal", NotificationManager.IMPORTANCE_DEFAULT)
notificationManager?.createNotificationChannel(channel)
}
val notification = NotificationCompat.Builder(this.applicationContext, "normal")
.setContentTitle("标题")
.setContentText("通知次数:${++alarmCount}")
.setSmallIcon(R.drawable.ic_launcher_background)
.setTimeoutAfter(5000)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.build()
notificationManager?.notify(notificationId,notification)
}

上面代码中if(javaClass.simpleName == "MainActivity"),及第四行代码,该代码用处是当app置于后台时候,pp界面是MainActivity时,pendingIntent使用NavDeepLinkBuilder生成,当是其他Activity时使用TaskStackBuilder生成。之所以这样是因为,在MainActivity的xml,使用了FragmentContainerView用于fragment间跳转,其他的Activity没有FragmentContainerView,因此在生成pendingIntent需要采用不同的方式生成。


这示例代码中,主要涉及到的Avtivity有MainActivity与VideoActivity,MainActivity使用FragmentContainerView,而VideoActivity没有。弹框与通知跳转的界面是AlarmDetailFragment,这个fragment在MainActivity通过Navigation实现导航。


因此在MainActivity界面进入后台时,pendingIntent使用NavDeepLinkBuilder生成,NavDeepLinkBuilder则可以使用导航图中fragment生成深度链接URI,这个URI则可以导航到指定的fragment(关于NavDeepLinkBuilder了解不深入,这里说的可能有错误地方,欢迎大佬指正)。


而VideoActivity界面进入后台时,就需要使用TaskStackBuilder生成一个启动MainActivity的Intent。而在MainActivity的onStart方法内有下面initToAlarmDetail方法,判断跳转时携带参数决定是否跳转到AlarmDetailFragment界面。


/** 跳转报警详情界面 */
private fun initToAlarmDetail() {
val task = intent.getStringExtra("task")
if (task == "toAlarmDetail"){
val bundle = Bundle()
bundle.putString("alarmId","1")
findNavController(R.id.fragment_main_table).navigate(R.id.alarmDetailFragment,bundle)
}
}

至此弹框与通知的功能基本实现,完整的BaseActivity代码如下:


open class BaseActivity: AppCompatActivity() {
companion object{
const val TAG = "BaseActivity"
}
private var alarmCount = 0
private val handler = Handler(Looper.myLooper()!!)
//为了关闭通知,manager放在外面
private val notificationId = 1
private var alarmDialogFragment: AlarmDialogFragment? = null
private var notificationManager:NotificationManager? = null
private var bgServiceIntent:Intent? = null//前台服务

private var nowClassName = ""

/** 弹框定时任务 */
private val dialogRunnable = object : Runnable {
override fun run() {
//在定时方法里面 javaClass.simpleName 不能获取当前所处Activity的名称
if (nowClassName == "VideoActivity"){ //视频界面不弹弹框
CustomLog.d(TAG,"不使用弹框 ${nowClassName}")
}else{
CustomLog.d(TAG,"使用弹框 ${nowClassName}")
useDialog()
handler.postDelayed(this, 10000)
}
}
}

/** 通知定时任务 */
private val notificationRunnable = object :Runnable{
override fun run() {
useNotificationPI()
handler.postDelayed(this,10000)
}
}

override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
initWindow()
return super.onCreateView(name, context, attrs)
}

override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
CustomLog.d(TAG,"onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) 当前类:${javaClass.simpleName}")
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CustomLog.d(TAG,"onCreate(savedInstanceState: Bundle?) 当前类:${javaClass.simpleName}")
initData()
}

override fun onStart() {
super.onStart()
CustomLog.d(TAG,"onStart 当前类:${javaClass.simpleName}")
nowClassName = javaClass.simpleName
handler.postDelayed(dialogRunnable, 3000)
initService()
}

override fun onResume() {
super.onResume()
CustomLog.d(TAG,"onResume 当前类:${javaClass.simpleName}")
}

override fun onRestart() {
super.onRestart()
CustomLog.d(TAG,"onRestart 当前类:${javaClass.simpleName}")
}

override fun onPause() {
super.onPause()
CustomLog.d(TAG,"onPause 当前类:${javaClass.simpleName}")
}

override fun onStop() {
super.onStop()
CustomLog.d(TAG,"onStop 当前类:${javaClass.simpleName}")
val customApplication = applicationContext as CustomApplication
val nowActivityName = customApplication.getNowActivityName()
val activitySimpleName = nowActivityName?.substringAfterLast(".")
CustomLog.d(TAG,"activitySimpleName:$activitySimpleName")
val isInBackground = (this@BaseActivity.applicationContext as CustomApplication).getIsInBackground()
if (isInBackground && activitySimpleName.equals(javaClass.simpleName)){// 处于后台 且 切换至后台app的activity页面名称等于当前基类里面获取activity类名
handler.postDelayed(notificationRunnable,3000)
CustomLog.d(TAG,"使用通知 $nowClassName")
}else{
CustomLog.d(TAG,"关闭所有定时任务 $nowClassName")
closeAllTask()
}
}

override fun onDestroy() {
super.onDestroy()
CustomLog.d(TAG,"onDestroy 当前类:${javaClass.simpleName}")
closeAllTask()
this.stopService(bgServiceIntent)
}

/** 关闭所有定时任务 */
private fun closeAllTask() {
handler.removeCallbacks(dialogRunnable)
handler.removeCallbacks(notificationRunnable)
}

/** 初始化数据 - 关于弹框*/
private fun initData() {
notificationManager = notificationManager ?: this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
alarmDialogFragment = alarmDialogFragment ?: AlarmDialogFragment()
}

/** 使用通知 - 通过pendingIntent实现跳转,缺点是任意界面进入报警详情界面,点击返回键只能返回MainFragment */
private fun useNotificationPI() {
var pendingIntent:PendingIntent? = null
if(javaClass.simpleName == "MainActivity"){//主界面
CustomLog.d(TAG,">>>通知:MainActivity")
val bundle = Bundle()
bundle.putString("alarmId","1")
pendingIntent = NavDeepLinkBuilder(this)
.setGraph(R.navigation.main_navigation)
.setDestination(R.id.alarmDetailFragment)
.setArguments(bundle)
.createPendingIntent()
}else {//其他界面时候切换后台通知
CustomLog.d(TAG,">>>通知:else")
val intent = Intent(this@BaseActivity,MainActivity::class.java)
intent.putExtra("task","toAlarmDetail")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
pendingIntent = TaskStackBuilder.create(this@BaseActivity)
.addNextIntentWithParentStack(intent)
.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel("normal", "Normal", NotificationManager.IMPORTANCE_DEFAULT)
notificationManager?.createNotificationChannel(channel)
}
val notification = NotificationCompat.Builder(this.applicationContext, "normal")
.setContentTitle("标题")
.setContentText("通知次数:${++alarmCount}")
.setSmallIcon(R.drawable.ic_launcher_background)
.setTimeoutAfter(5000)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.build()
notificationManager?.notify(notificationId,notification)
}

/** 弹框使用 - 因为此处涉及到fragment等生命周期,进入其他activity内时候,在前的activity使用useDialog会因为生命周期问题闪退*/
private fun useDialog() {
//弹出多个同种弹框
// alarmDialogFragment = AlarmDialogFragment()
// alarmDialogFragment?.show(supportFragmentManager,"testDialog")

//不弹出多个同种弹框,一次只弹一个,若弹框存在不弹新框
if (alarmDialogFragment?.isVisible == false){//如果不加这一句,当弹框存在时候在调用alarmDialogFragment.show的时候会报错,因为alarmDialogFragment已经存在
alarmDialogFragment?.show(supportFragmentManager,"testDialog")
}else{
//更新弹框内信息
}
}

/** 关闭报警弹框 */
private fun closeAlarmDialog() {
if (alarmDialogFragment?.isVisible == true) {
alarmDialogFragment?.dismiss()//要关闭的弹框
}
}

//状态栏透明,且组件占据了状态栏
private fun initWindow() {
window.statusBarColor = Color.TRANSPARENT
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
}

/** 初始化服务 */
private fun initService() {
CustomLog.d(TAG,"开启前台服务")
bgServiceIntent = bgServiceIntent ?: Intent(this, BackgroundService::class.java)
this.startService(bgServiceIntent)
}
}

总结


只是弹出弹框和通知的话,实现很好实现,中间麻烦地方在于当app使用多个Activity,该怎么实现跳转到指定的界面。当然这里麻烦是,从ActivityB跳转到ActivityA的Fragment,如果是只有一个Activity应该会好办些。个人感觉fragment跳转应该有更好的方式实现希望能和大佬们交流下这种情况下,用什么技术实现。


PS:感觉原生Android在写界面和跳转方面写起来不太方便。不知道大家有便捷的方式吗。


代码地址


GitHub:github.com/SmallCrispy…


作者:卤肉拌面
来源:juejin.cn/post/7260808821659779129
收起阅读 »

无悬浮窗权限实现全局Dialog

有些场景下需要显示一些提示弹窗,但把握不好弹出时机容易先弹出弹窗然后界面马上被杀掉进而看不到提示内容,例如强制下线:客户端退回登录界面并弹出提示弹窗。 如果是直接拿的栈顶activity去弹出,没有将弹窗逻辑写到具体activity中,或不好确定activty...
继续阅读 »

有些场景下需要显示一些提示弹窗,但把握不好弹出时机容易先弹出弹窗然后界面马上被杀掉进而看不到提示内容,例如强制下线:客户端退回登录界面并弹出提示弹窗。


如果是直接拿的栈顶activity去弹出,没有将弹窗逻辑写到具体activity中,或不好确定activty的变化就容易出现这种现象。


由于applicationContext没有AppWindowToken,所以dialog无法使用applicationContext创建,要么就使用windowManager配合WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY使用创建全局悬浮窗。但是这种做法需要申请权限。那么,在没有悬浮权限情况下如何做到让dialog不受栈顶activity变化的影响?


我的想法是通过application.registerActivityLifecycleCallbacks在activity变化时,关闭原来的弹窗,并重新创建一个一样的dialog并显示。


效果演示:


1. 栈顶界面被杀


界面退出

2. 有新界面弹出


界面退出

以下是代码实现:


/**
* @Description 无需悬浮权限的全局弹窗,栈顶activity变化后通过反射重建,所以子类构造方法需无参
*/

open class BaseAppDialog<T : ViewModel>() : Dialog(topActivity!!.get()!!), ViewModelStoreOwner {

companion object {
private val TAG = BaseAppDialog::class.java.simpleName
private var topActivity: WeakReference<Activity>? = null
private val staticRestoreList = linkedMapOf<Class<*>, Boolean>() //第二个参数:是否临时关闭
private val staticViewModelStore: ViewModelStore = ViewModelStore()

@JvmStatic
fun init(application: Application) {
application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
topActivity = WeakReference(activity)
}

override fun onActivityStarted(activity: Activity) {

}

override fun onActivityResumed(activity: Activity) {
topActivity = WeakReference(activity)
val tempList = arrayListOf<BaseAppDialog<*>>()
val iterator = staticRestoreList.iterator()
while (iterator.hasNext()) {
val next = iterator.next()
val topName = (topActivity?.get() ?: "")::class.java.name
if (next.value == true) { //避免onCreate创建的弹窗重复弹出
val newInstance = Class.forName(next.key.name).getConstructor().newInstance() as BaseAppDialog<*>
tempList.add(newInstance)
Log.e(TAG, "重新创建${next.key.name},于$topName")
iterator.remove()
}

}

tempList.forEach {
it.show()
}

if (staticRestoreList.size == 0) {
staticViewModelStore.clear()
}
}

override fun onActivityPaused(activity: Activity) {
}

override fun onActivityStopped(activity: Activity) {

}

override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}

override fun onActivityDestroyed(activity: Activity) {
}
})
}
}


var vm: T? = null

init {
val genericClass = getGenericClass()
if (vm == null) {
(genericClass as? Class<T>)?.let {
vm = ViewModelProvider(this)[it]
}
}

topActivity?.get()?.let {
(it as LifecycleOwner).lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
dismissSilent()
}
})
}
}


//用于栈顶变化时的关闭
private fun dismissSilent() {
super.dismiss()
staticRestoreList.replace(this::class.java, true)
}

override fun show() {
super.show()
staticRestoreList.put(this::class.java, false)
}

override fun dismiss() {
super.dismiss()
staticRestoreList.remove(this::class.java)
}


//获取泛型实际类型
private fun getGenericClass(): Class<*>? {
val superclass = javaClass.genericSuperclass
if (superclass is ParameterizedType) {
val actualTypeArguments: Array<Type>? = superclass.actualTypeArguments
if (!actualTypeArguments.isNullOrEmpty()) {
val type: Type = actualTypeArguments[0]
if (type is Class<*>) {
return type
}
}
}
return ViewModel::class.java
}


//自己管理viewModel以便恢复数据
override fun getViewModelStore(): ViewModelStore {
return staticViewModelStore
}
}

参数传递的话,直接通过修改dialog的viewmodel变量或调用其方法来实现。


class TipDialogVm : ViewModel() {
val content = MutableLiveData<String>("")
}


class TipDialog2 : BaseAppDialog<TipDialogVm>() {

var binding : DialogTip2Binding? = null

init {
binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.dialog_tip2, null, false)
binding?.lifecycleOwner = context as? LifecycleOwner
binding?.vm = vm
setContentView(binding!!.root)

}
}

弹出弹窗


TipDialog2().apply {
vm?.content?.value = "嗨嗨嗨"
}.show()

作者:Abin
来源:juejin.cn/post/7295576843653087266
收起阅读 »