注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

部署完了,样式不生效差点让我这个前端仔背锅

web
大家好,作为今年刚毕业的大聪明(小🐂🐎),知道今年不好就业,费心费力的面试终于进了个小公司,然后我的奇妙之旅开始了。 叮!闹钟响了!牙都不刷了,直接起床出门去上班,身为🐂🐎就要有早起吃草干活的觉悟,所以我是个很合格的食草动物。只不过,下面的信息,让我一天当动...
继续阅读 »

大家好,作为今年刚毕业的大聪明(小🐂🐎),知道今年不好就业,费心费力的面试终于进了个小公司,然后我的奇妙之旅开始了。



叮!闹钟响了!牙都不刷了,直接起床出门去上班,身为🐂🐎就要有早起吃草干活的觉悟,所以我是个很合格的食草动物。只不过,下面的信息,让我一天当动物的好心情都没了。


无标题.png


部署?因为该公司人数比较少,也没有自动化部署操作,只能让前端build一下dist包然后丢给后端,让后端部署,然后该项目也比较急,我刚来,也不好意思@后端,恰巧跟我对接的后端也是刚来不熟悉,所以我就跟负责人继续扯皮拖延时间了。(真是不好意思,假的)↓↓↓


无标题ED.png
终于经过不断努力(后端),部署好了,但是,我的样式乱了,为什么呢?看了一下网络和源代码,样式文件都请求到了,问了后端,一致说是我前端的问题,我懵了,然后快马加鞭查找问题


无标题.png


解决


ok,那我先看打包的dist文件有样式有问题吗,使用npm安装个 http-server 运行打包后的index.html,运行打开运行后的地址,欧克样式没乱,文件引用也没问题,那到底是什么鬼影响的呢?


在我百思不得其解的时候,我打开了神器--csdn,搜索打包dist部署后样式文件不生效,就遇到了这个神文t.csdnimg.cn/WbQyq
欧克,按照博主说的Nginx没有配置这两个东西而导致的,我有点不确定是不是,有点犹犹豫豫的找到后端说帮我在Nginx配置文件的http加下这两个配置,然后样式就好了,完美!后端由于疏忽,没加上就把问题推给我,结果被我扳回一城。


include   mime.types;
default_type application/octet-stream;


  1. include mime.types;



    • 这个指令告诉Nginx去包含一个文件,这个文件通常包含了定义了MIME类型(Multipurpose Internet Mail Extensions)的配置。MIME类型指定了文件的内容类型,例如文本文件、图像、视频等。通过包含 mime.types 文件,Nginx可以识别不同类型的文件并正确地处理它们。

    • 示例:假设 mime.types 文件中定义了 .html 文件为 text/html 类型,Nginx在处理请求时会根据这个定义设置正确的HTTP响应头。



  2. default_type application/octet-stream;



    • 这个指令设置了默认的MIME类型。如果Nginx无法根据文件扩展名或其他方式确定响应的MIME类型时,就会使用这个默认类型。

    • application/octet-stream 是一个通用的MIME类型,表示未知的二进制数据。当服务器无法识别文件类型时,会默认使用这个类型。例如,如果请求的文件没有合适的MIME类型或没有被 mime.types 文件中列出,Nginx就会返回 application/octet-stream 类型。

    • 这种设置对于确保未知文件类型的安全传输很有用,因为浏览器通常会下载这些文件而不是尝试在浏览器中打开它们。




总之,添加 include mime.types; 和 default_type application/octet-stream; 配置后,Nginx能够正确地识别和处理CSS文件的MIME类型,从而确保浏览器能够正确加载和应用CSS样式。


所以,前端仔不能只当前端仔,还是要好好学点服务端的知识,不然锅给你,你还认为这是你该背的锅。



以上是开玩笑的描述,只是为了吸引增加阅读量



作者:世上只有一个唐广君
来源:juejin.cn/post/7388696625689051170
收起阅读 »

一份离职感想

一、和解 人的一生就是不断地与世界和解,以及与自己和解。 今天提离职了,相比于四年前的离职,更加的从容和淡定。所谓,心无外物,云淡风轻。 图:云卷云舒,心随境转 二、当下 今天晚上和一个刚来三周的应届生跑步,聊着聊着就说到了要是回到十八岁就好了的话题。 当...
继续阅读 »

一、和解


人的一生就是不断地与世界和解,以及与自己和解。


今天提离职了,相比于四年前的离职,更加的从容和淡定。所谓,心无外物,云淡风轻


image.png
图:云卷云舒,心随境转




二、当下


今天晚上和一个刚来三周的应届生跑步,聊着聊着就说到了要是回到十八岁就好了的话题。


当然我也谈了我的一些想法, 如果是以前的我也会像你这样,要是回到以前就好了。 但是,现在的我更加从容、更加淡定、更加坚定,以及对事情的理解、看法、态度是那个时候没有的,同时懂得了筛选,也懂得舍弃,对社会的理解,对世界的理解也是那个时候没有的,甚至对自己的看法也更加立体,对自己也更宽容,现在反而会更加欣赏自己。


人生不必时时往过去看,"既往不恋,当下不杂,未来不迎"


41c75840276db6a178aa3a21244a249.jpg


图:操场跑步,有时候也会坐在最高的台阶上,享受这自然的风。


三、接纳


我们还谈到了失恋的话题,我说很多人是没有办法放下的,也没办法抹去,不管是事还是人都一样。


人的一生就像是石块不断垒起来的玛尼堆,有一些人会在特定时间段参与进来,帮你加一块,可能它的颜色、大小、形状、好看程度都不一样,但你不得不承认,那个时间点,那个高度就只有那么一块,无可替代。


与其不见,不如接纳!


a84ae8d6d03d70d359799b4d74ee84d.jpg


图:某次户外徒步


四、选择


大学时的第一部课外读物 ,路遥的《人生》,开篇来自柳青的一段话,至今影响着我。


image.png


刚好昨天生日,有感而发,也时刻警醒着自己。


image.png


图:朋友圈,31岁的记录


五、感受



人这一生就是不断地给自己的感受积累素材



不知什么时候,我开始静下来,用心去感受这个世界。


在吃午餐的时候,变得专心,放下了手机,端起了饭碗,开始用舌头去感受食物原本的味道,就像《小森林》中的女主一样。静下来,都是收获。


image.png


a86c6a0e1f998dc5e12ee155dd4fc53.jpg


图:午餐,三菜一汤


用心去感受,用心去体会,从容和淡然。享受生活,感恩生活。



你所看到的,就是你内心的一种投射,你看到了美好,那么你的内心就是美好的。你看到了恶,那么你的内心就是恶。 内心这块三分宝地,就不要装上一些乱七八糟的东西了。



六、经历



生命的厚度在于去经历一些不一样的事情。



人需要不断去经历一些不一样的事情,而不是将365天过成1天。


“人不是活一辈子,不是活几年几月几天,而是活那么几个瞬间。”——鲍利斯·帕斯捷尔纳克


希望多一些这样怦然心动、与众不同的时刻, 看到黄山的日出、云海,觉得这个世界真的很酷,继续热爱。


image.png


图:黄山风景图


不要等到八十岁了才去做二十岁想做的事情。


七、自己


青年人要有青年人的样子,朝气、热血。


如罗曼罗兰说过:
“世界上只有一种真正的英雄主义,那就是在认清生活的真相后依然热爱生活”


人不要给自己设限,勇敢地做自己,不要被这个世界迷失了自我。


在这个满是焦虑、满是浮躁的社会,更要做到正心正念


image.png


就像我在 500px 网站上说过的一句话:“这个世界没有标准答案,活出自己,不受羁绊”


作者:uzong
来源:juejin.cn/post/7386497602193981481
收起阅读 »

丈母娘,你这是来真的啊?

今年春节在家里呆的时间比往年都要久一些,就算离家不远也会这样感觉,随着年龄的增加,越发思念故土以及父母。 以往回家都比较晚,待着待着就想着回工作的城市。因为回家总会伴随着和二老的争论,而且我那时也很戾气,合不到一块就气,或者进房间睡觉。 要说啥事能值得争论,我...
继续阅读 »

今年春节在家里呆的时间比往年都要久一些,就算离家不远也会这样感觉,随着年龄的增加,越发思念故土以及父母。


以往回家都比较晚,待着待着就想着回工作的城市。因为回家总会伴随着和二老的争论,而且我那时也很戾气,合不到一块就气,或者进房间睡觉。


要说啥事能值得争论,我的确忘了,大概是思想观念以及家里生意的问题产生的。以至于身处异乡也要离开家里不愿意喋喋不休。


随着时间流逝,心态已经发生变化。我内心深处逐渐向着故土,和家里二老的关系也潜移默化,彼此心照不宣的拉近了很多。


二老也不过多问责一些,也不再以长者的心态和我对质。


事实也觉得普通家的孩子成年后就该放权干他自己的事,父母少些干预,有些做法和观念已经帮不上忙。父母与孩子之间往往都会产生很大分歧,尤其是思想观念的矛盾。


每一代都有可能推翻上一代的思想意识。父母与子女和谐,一般父母的思想观念足够前卫能够传承给子女,或者子女的思想观念没有很大差异,认可父母的思想体系。


我们会一起坐下来交谈家里的事情,想好对策处理外事,规划老家祠堂以及屋子的安排;参与布置家里,代替老爹出席各项习俗礼节活动。


从读大学开始到现在就没再给它们制造任何的麻烦和搞砸事情。只见他坐在店里抽着烟,沉默着望向我,迟迟不再发话,只在最后说了一句婚姻的问题。


这个话题别说他了,老家众多的亲戚都会抛出这个尖锐的问题,我说我回家只想休息,逢年过节和老朋友叙旧,去亲戚家串串门,一起乐呵乐呵聊聊家常,聊聊八卦。


95‘ 后教师彭老师


小学同学彭老师,现在是一名县城重点中学的高中老师。她是一名 90 后老师,我在她身上看到了很多老师的缩影。她毕业后第一份工作就是任高一班主任兼任课老师。


图片


▲图/ 学生:???


彭老师很是焦虑很迷茫,害怕自己不能胜任;害怕自己教不好;害怕自己耽误学生的前途。


每一个担心都很沉重,责任肩负在身上,没日没夜的备课,改作业,演练怎么让学生听懂。


图片


▲图/ 在最无能为力的学科遇到了最不想辜负的老师


小城市义务教育向来重视成绩不太注重心理建设,导致问题学生很多。彭老师经常 solo 学生和家长,试图说服他们把重心放在当前阶段要做的事。


尽管生病,喉咙发炎也坚持做着这些事情。


图片


▲图/ 除夕夜,彭老师回学校,顺道提零食带红包给她


图片


▲图/ 她赠了一罐饮料,握着只觉比当时天气还冷


图片
▲图/ 彭老师收到开工红包请吃饭,还给了红包,呜呜呜感动


有时煎熬的对着同事和朋友说,看着自己的希望学生开始堕落真的很难受,说也说不通;面对自己的学生读着读着因家里或者各种的问题而辍学时更无语。


图片


▲图/ 00 后学生在 95 后老师教案涂鸦


忍不住会直接联系家长 solo,开导家长有时候如同开山凿石一样,只有坚持不懈才使得那位学生继续读下去,尽管读大专,也改变了不少走向。


有一天她苦笑的跟我们说,她自己都没怎么谈过恋爱却还要开导失恋的学生,甚至被失恋学生反问:“老师,你谈过恋爱吗?”


???


彭老师所带的班级在 23 年第一批结业。上本率超额完成上面给的任务,这一路上面临学生堕落、早恋、辍学、逃课、网瘾...


彭老师有时候很想放弃摆烂,像那些老道的老师一样,风清云淡,看开一点,上课就上课,喝茶就喝茶,晒太阳就晒太阳。


后来她骑着小电瓶,向我重申了她那坚定的信念,绝不可能摆烂,我要对我的学生负责,要为他们的前途着想,就这么决定了。


我看着她感觉头上出现了一顶为人师表的光环。


图片


实话说,老师的行为无论是怎样的,都会被学生刻在记忆深处,尽管有时不会联系,也会在某一刻回想起,念其良莠。


图片


▲图/ 念大学的学生探望彭老师


她性格一直都很逗比很乐观,走路也喜欢蹦蹦跳跳,还被她的老师和学生嘲笑她走路蹦蹦跳跳,和她玩王者鲁班的走姿如出一辙。


在她学生毕业之后,彭老师回归万年鲁班,但技术依然停留在四五年前大学生时期,经常遭截杀一路“啊”,开疾跑徒走回水晶,也经常被我们和她的学生给护着。


现在见她时还是很逗比,嘴硬心软。脸上刻印出一副班主任的形象,坚定严肃而又亲和。


她说她带完一届之后不当班主任了,太辛苦太累了。后面她又被安排带复读班班主任。


阿姨,你来真的啊?


老家的天气很不错,逢年过节我们经常互串亲戚的门,晒晒太阳,欺负欺负小朋友,欺负过头了说送给他们一份《三年高考五年模拟卷》礼物,他们哭的更厉害。


图片


▲图/帮人带孩子真好玩


春节假期充电的时间正在倒计时,最后赶着串堂弟的门,也就是大叔家,那时他们家里很忙,家族里的会做饭打点的都来帮忙了。


原来是堂弟未来丈母娘查家环节,家里上上下下忙活。


我等闲杂人在巷子里晒着太阳,准备迎接他们。堂弟我和同岁,月份比我小,在温州工作,这几年他老爹操够了心,费了不少钱。龙年又长了一岁,他父母更是着急。


图片


▲图/ 回家路途中天色很震撼,大家都不想说话


闲聊之际,只见一列车齐刷刷地开到门口,不知道的还以为是迎亲来了。


二叔见状连忙赶上去打招呼并指挥停车泊位。只见那丈母娘下车后整理着装,望着周边的房屋装饰,一脸严肃对着二叔说位置有些偏远,绕来绕去的,二叔连忙解释可能走的道不一样,走国道会很顺。


亲友团齐刷刷招呼张罗着进去喝茶,握手,递烟,倒茶。摆了三桌才能坐下,我们自己人在旁边站着观望或帮忙。两方互相寒暄之后,不到十分钟对方便开始切入主题,商量儿女婚姻问题。


丈母娘吐露堂弟家位置偏僻,路道不好走,绕了很多弯才抵达,彩礼需要增加 3w 到 5w,作为她女儿的嫁衣钱。


阿姨,你这是来真的啊?


大叔一边忙着圆场有不同的道路可以走,镇与镇之间来往有很多路可走也方便,一边递烟倒茶使其思量再三。


丈母娘依然坚定不移,重申了一遍,对方亲戚应声附和。


大叔脸色像是喝酒上了头一样,随后陷入沉思,见其态度坚硬,且事已至此,作出退步可以增加 1w - 2w,给到对方女儿身上。


前些天,大叔带着堂弟相亲到女方家,据说对方开价 35.8w 彩礼,回礼是购买房之后支援 10w+,不知其是否商量后的价格。


只见那天大叔来我家喝茶水时带着儿子和未来儿媳去了县城买了“五金”, 4w 左右。


见此事既成,并不买二叔的帐,坚持需要增加 3w,并声称给女儿做嫁衣。


大叔又陷入了思考,心里计算着账面,上个月女儿刚嫁出去,彩礼还没捂热,就要付之东流,是为不甘且又无奈。


场面陷入了安静,对方只管握着茶杯吃着果子等待结果,势必做好了撤离的打算。


姑舅们遂即递烟倒茶聊家常。


堂弟陪同坐着喝茶望着对象低下了头,儿媳妇安静得陪在身边,挽着堂弟的手。


掂量之后大叔同意了对方的要求,双方态度方能缓和很多,继续喝茶,商量事宜,聊家常,聊孩子幸福。


饭后,互相道了别,每人随了红包礼。只觉得对方结婚习俗没有讲明白有点遗憾,但愿不会阻碍他两组建一个幸福的小家庭。


我和表弟坐在沙滩上,对着河扔石子打水漂,谁都不想再提,心里比谁都清楚。却和群里的伙伴嘲笑着自己家的那位是否也要几十个 w。


作者:程序员小榆
来源:juejin.cn/post/7336822951273824282
收起阅读 »

吾辈楷模!国人开源的Redis客户端被Redis官方收购了!

不久前开源圈子里的一则消息在网上引起了一阵关注和讨论。 一个由国人开发者所打造的开源项目被 Redis 公司官方给收购了,作者自己也发了动态,表示感谢项目9年以来的陪伴,同时也希望她未来一切都好。 这个开源项目的名字叫做:ioredis,相信不少小伙伴也用过...
继续阅读 »

不久前开源圈子里的一则消息在网上引起了一阵关注和讨论。


一个由国人开发者所打造的开源项目被 Redis 公司官方给收购了,作者自己也发了动态,表示感谢项目9年以来的陪伴,同时也希望她未来一切都好。



这个开源项目的名字叫做:ioredis,相信不少小伙伴也用过。



目前在GitHub上我们可以看到,ioredis项目的开源地址已经被迁移至 Redis 官方旗下了。



iosredis是国人开发者所打造的一个Redis客户端,基于TypeScript所编写,以健壮性、高性能以及功能强大为特色,并且被很多大公司所使用。



截止到目前,该项目在GitHub上已累计获得超过 13000 个 Star标星和 1000+ Fork。


作者自己曾表示,自己创建这个开源项目的初衷也很简单,那就是当年在这方面并没有找到一个令自己满意的开源库,于是决定自己动手来打造一个,于是就利用闲暇时间,自己从零开发并开源了 ioredis 。


直到2022 年 8 月 30 日,历时整整7年,ioredis 成为了 Node.js 最流行的 Redis 客户端。



而直到如今,这个项目从个人的 side project 到被开源公司官方收购,作者9 年的坚持属实令人佩服,吾辈楷模啊!


而拜访了这位开发者的GitHub后我们会发现,作者非常热衷于创造工具,除了刚被收购的名作ioredis之外,主页还有非常多的开源项目,并且关注量都不低。



而且从作者发的一些动态来看,这也是一个热爱生活的有趣灵魂。



有一说一,个人开源作者真的挺不容易的,像上面这样的个人开源项目被官方收购的毕竟是个例,其实好多个人的开源项目到后期由于各种主客观原因,渐渐都停止更新和维护了。


大家都知道,伴随着这两年互联网行业的寒意,软件产业里的不少环节也受到了波动。行业不景气,连开源项目的主动维护也变得越来越少了。


毕竟连企业也要降本增效,而开源往往并不能带来快速直接的实际效益。付出了如果没有回报,便会很难坚持下去。


而对于一名学习者而言,参与开源项目的意义是不言而喻的,之前咱们这里也曾多次提及。


参与开源项目除了可以提升自身技术能力,收获项目开发经验之外,还可以让自己保持与开源社区其他优秀开发者之间的联系与沟通,并建立自己的技术影响力,另外参与优秀开源项目的经历也会成为自己求职简历上的一大亮点。


所以如果精力允许,利用业余时间来参与或维护一些开源项目,这对技术开发者来说,也是一段难得的经历!


作者:CodeSheep
来源:juejin.cn/post/7345746216150876198
收起阅读 »

我在国企当合同工的那段日子

我的博客原文 @author: 郭瑞峰 @createTime: 2024/06/03 @updateTime: 2024/06/20 心血来潮 25号考完了,非常不理想,果然700页的东西不是一个月能搞完的。不对,我今儿写日志是为了纪念一下我的第一家公司...
继续阅读 »

我的博客原文



@author: 郭瑞峰


@createTime: 2024/06/03


@updateTime: 2024/06/20



心血来潮


25号考完了,非常不理想,果然700页的东西不是一个月能搞完的。不对,我今儿写日志是为了纪念一下我的第一家公司,咋扯到别的了......言归正传,我在第一家公司待了仨年,可能是年纪到了(26岁咋还不退休啊),也可能是留了点感情在,离开前有些百感交集,思来想去还是写一个懒人日志吧,纪念一下我打工的三年光阴吧。
(:з」∠)


743dca683eeab71e90217fa21b2bc09218c024fa8faf6c782d3907ef193af847.png


初说公司


先说一下俺的第一家公司,咱从学校出来就来这儿报道了,公司是国企控股,领导层全是国企员工,其他进公司的员工就是合同工,或者说是国企合同工,能吃公司东西,不是人力外包。
(:з」∠)


成都这边的开发都是围绕着云服务的,包括云操作系统、云桌面系统、云运维系统以及多云系统(我个人喜欢把他称为多个云集成系统),当然全是定制化项目。对,忘说了,公司主要业务是轨道交通行业,做云相关的产品是将轨道行业的运维放在云上面,算是相应国家的两化融合(信息化和工业化)。


对了,得说一下公司待遇,公司给的工资都在平均水平以下,尤其是对应届生而言,社保基数是工资八折(试用期)交的,公积金是12%,没有餐补但自带食堂以及饭卡补助,有些节假日有礼品,至少基础福利还好。


8e814b5959872ab49a56d81bf2ca6f9d1fbf535f95883a1db721d4b54e8ae491.png


项目与业务


我所在的项目组就是多云系统,也算是我认为公司能拿得出手的项目。虽然是集成项目,但它只能集成。好像说了跟没说一样,那说具体点吧,比如说业主那边需要云,但怕私有云厂商垄断坐地起价,所以说一般配额划分为“7/3”、“4/3/3”、“6/4”,这样就有两套云系统,为了用起来顺心就需要一个集成系统,所以说我这个项目组的业务来源就是这样,至于你说的我们集成系统会不会垄断坐地起价,拜托,我们系统只会集成,没有底层设备控制权,坐地起价就直接禁用就行了,就不用这个系统呗,反正资源在另外的云操作系统中。


好了,话题回来,说说项目组开发相关的吧,项目开发受阻有三:与三方厂商沟通、项目代码老旧、随时随地变更的需求。


先说第一点吧,集成系统最大的麻烦就是跟三方厂商沟通,当然测试环境、测试数据获取这类的细节也算三方厂商沟通。因为地铁行业算是智能中国建设的一部分,所以说不光是我,连三方厂商的软件都必须是定制的。开发时候就要等着厂商环境稳定了,有数据了再联调,联调有bug了,再走一轮上面的流程,极大地增加了沟通成本以及开发成本。


20150918092659584.jpg


在沟通,再沟通

其二就是和很多工业软件公司一样,软件项目时间跨度很大,里面东西不知道转手了好多次,缝缝补补式的开发,开发要考虑很多兼容性问题以及自己想办法写补丁。比如说node@6.x.x不支持Object.entries,你就要手动在webpack.base.conf.js写的兼容,问我为啥不配置babel呢,上次改babel配置都是2016年的事儿了。代码要写兼容,久而久之就会忘记什么事封装、抽象,全部遗失在兼容的漩涡中。


就改了一点点.gif


我就改了一点点怎么崩了

其三就是随时随地变更的需求,这里我叠个甲,这个我不是甩锅给产品,虽然是产品改的需求,但产品不是想改就改,一定是业主/客户/上级/领导指示要改的。有需求变动谁都不会安逸,谁都烦,但请把炮火对准,不要误伤友军。频繁调整的需求会不断地消磨激情和热情,模糊项目方向,当然还有临时变卦导致的加班。


写的补丁.gif


一直在变的需求

心态变化


三年工作时间虽然很短,但足以改变心态。原来有些迷茫到彻底迷茫;原来想要搞出一番事业到慢慢得过且过;原来想努力改变世界走到只想躺平加速世界毁灭。


公司的缝缝补补,工作的缝缝补补,项目的缝缝补补,这样的缝缝补补渐渐地缝补在人身伤,人心里。原来就算只有940的显卡也要努力熬夜玩游戏,现在用上3060ti后却只想打开直播看看,就只看看,重新上手玩太耗精力了。至于脱单嘛,自己都这么累了,为啥带着另一个一起累呢?


1a220c1bc42df977bfc783b1d1194378dc5493ad61280f91db826c5a3e7fdc44.png


尾声


本来6月3号说写完的,忙着离职交接以及新公司入职,再加上拖延症又犯了,所以说一直到20号才写完,不过至少咱写完了,能发。


这篇算是自己里程记录,同时也是发牢骚,大家就当笑话看看吧。


8e814b5959872ab49a56d81bf2ca6f9d1fbf535f95883a1db721d4b54e8ae491.png


作者:望远镜
来源:juejin.cn/post/7382121357608321059
收起阅读 »

.zip 结尾的域名很危险吗?有多危险?

Google 于 2023 年 5 月 10 日全面开放了以 .zip 结尾的域名,这一举动引起了安全研究人员和社区的警惕,他们担心该通用顶级域名(gTLD,Generic top-level domains)会被用于创建足以迷惑计算机高手的恶意 URL。 2...
继续阅读 »

Google 于 2023 年 5 月 10 日全面开放了以 .zip 结尾的域名,这一举动引起了安全研究人员和社区的警惕,他们担心该通用顶级域名(gTLD,Generic top-level domains)会被用于创建足以迷惑计算机高手的恶意 URL。


2023 年 5 月 3 日,Google 宣布了包括 .zip.mov 在内的 8 个全新的通用顶级域名:



  • .dad

  • .phd

  • .prof

  • .esq

  • .foo

  • .zip

  • .mov

  • .nexus


并于 5 月 10 日通过 Google Domains 向公众开放注册。



Google Domains 是 Google 提供的一项域名注册和管理服务,支持用户搜索和注册域名



Zip Domain for Malicious Attacks


小心!含有 .zip 的恶意 URL


安全研究员 Bobby Rauch 指出(The Dangers of Google’s .zip TLD),要警惕含有 .zip、Unicode 字符(特别是 U+2044、U+2215 等)以及 @ 符号的恶意 URL。这类恶意 URL 迷惑性极强,甚至能欺骗十分有经验的用户。


若点击 https://google.com@bing.com 这个 URL,实际访问的是 https://bing.com。这是因为根据 RFC 3986 Uniform Resource Identifier (URI): Generic Syntax 的规定,@ 符号之前的 google.com 应识别为用户信息,其后的 bing.com 才是主机名(域名)。我们可以借助常用的编程语言来确认这一点,如利用 PHP 的 parse_url() 函数:


<?php
var_dump(parse_url("https://google.com@bing.com"));
array(3) {
["scheme"]=>
string(5) "https"
["host"]=>
string(8) "bing.com"
["user"]=>
string(10) "google.com"
}

然而,若 @ 之前有正斜杠 /,如 https://google.com/search@bing.com,则浏览器会将 /search@bing.com 部分识别为路径,最终访问的是 https://google.com/ 下的文件 search@bing.com。由于没有这个文件,结果自然是 404。


<?php
var_dump(parse_url("https://google.com/search@bing.com"));
array(3) {
["scheme"]=>
string(5) "https"
["host"]=>
string(10) "google.com"
["path"]=>
string(16) "/search@bing.com"
}

Bobby Rauch 就是利用了上述规则,创建了一个恶意 URL,


https://github.com∕kubernetes∕kubernetes∕archive∕refs∕tags∕@v1271.zip

乍看之下,这个 URL 似乎是用于从 GitHub 上下载 v1271 这个特定版本的 Kubernetes 的链接。但实际上 parse_url() 函数的解析结果显示,真正要访问的域名却是 v1271.zip 而不是 github.com


<?php
var_dump(parse_url("https://github.com∕kubernetes∕kubernetes∕archive∕refs∕tags∕@v1271.zip"));
array(3) {
["scheme"]=>
string(5) "https"
["host"]=>
string(9) "v1271.zip"
["user"]=>
string(63) "github.com?__kubernetes?__kubernetes?__archive?__refs?__tags?__"
}

若你不小心点击了这类域名,那么恭喜你,很可能喜提一个 evil.exe(请注意动画演示中的左下角)。



仅凭肉眼可能难以分辨以下两个 URL 的区别吧:


https://github.com∕kubernetes∕kubernetes∕archive∕refs∕tags∕
https:/
/github.com/kubernetes/kubernetes/archive/refs/tags/

但若调整一下字体,则可以发现端倪,


image-20240625184157641


恶意 URL 中的正斜杠 / 根本不是真正的 /(U+002F),


image-20240625184841985


而是下面这个看起来很像 / 的 Unicode 字符:


image-20240625185113911


由于恶意 URL 中并没有使用真正的 /,因此根据 RFC 3986 的规定,@ 之前的部分github.com∕kubernetes∕kubernetes∕archive∕refs∕tags∕ 尽管看似域名与路径,但实际上却是用户信息(真够长的)。


在刚刚的动画演示中,Bobby Rauch 其实还使用了另一个迷惑人的小伎俩——在电子邮件客户端上,将 @ 的字号大小更改为 1,让这个特殊字符几乎看不到,从而更隐秘地伪装了恶意 URL。


对于由以 .zip 结尾的域名带来的安全隐患,Bobby Rauch 给出的建议是,在单击 URL 之前,先将鼠标悬停在该 URL 上并检查浏览器底部显示真正要访问的 URL。


作者:胡译胡说
来源:juejin.cn/post/7384244866875146290
收起阅读 »

希望你多出去看看,别活在短视频和文字里!

感谢你阅读本文! 这段时间在校友群里看到一些“混得比较好的”同学发言,类似于“5w的月薪很高吗?”,“我身边年薪六七十w的人不少”之类的话,加上偶尔看到一些“年薪百万很简单”的标题党文章或者视频,其实对于我来说,我根本懒得去考证这些是真是假! 但是我觉得有必要...
继续阅读 »

感谢你阅读本文!


这段时间在校友群里看到一些“混得比较好的”同学发言,类似于“5w的月薪很高吗?”,“我身边年薪六七十w的人不少”之类的话,加上偶尔看到一些“年薪百万很简单”的标题党文章或者视频,其实对于我来说,我根本懒得去考证这些是真是假!


但是我觉得有必要去聊一聊!


一、知识的贫乏


首先在说这个问题之前,我想引用罗翔老师的一句话。


一个知识越贫乏的人,就越有莫名的优越感!


一年多以前,我回老家,和以前的高中女同学出来聊天,彼此聊了聊自己现在的工作,然后他问我,“你现在一个月能赚三四万吧!”,我当时惊呆了,我回她:“瞧你说的,捡黄树叶也要赶上秋天呢”,我反问她你现在多少呢,她说两千八,我继续问,“你觉得工资多少才算高?”,她说最起码5万以上吧!我苦笑答:“我的妈呀,怎么都这么厉害呀!”。


事实是怎样的呢?


我们先不把事情说得太远,“脉脉上人均年薪百万”,“抖音上人手一台劳斯莱斯”这些不在叙述范围内,感情咱也不会那么不要脸去吹!


二、大众才是真相


像我们这种普通二本学校的学生现状应该最能接近真相了,往上不谈双一流,往下不谈专科,据我所知,我校2021毕业的学生,如果继续做软件工程的话,现在一个月能拿两万以上的人没几个,还得是一线城市,在一线城市的大多都是一万多,所以一万多就是一个中位数。


不过要注意,软件工程专业毕业后从事本专业的人是很少的,就拿我们班来说,班上50人,但是从事软件的不超过20个,20个还是比较理想的。


那么就有一部分从事其它职业,一部分待业,一部分考公考编。


软件行业在整个市场来说工资高一点,就业相对来说简单一点,虽然近几年来行情越来越差,但是相比于其他行业来说,还是稍微好一点!


从事其他行业的人来说,如果家里有点关系的人,条件好一点的人,可能去到一个单位里面暂时上班,条件不好的,那就出来随便找一个班上,对于销售型的,在广州深圳,大多都是六七千,小城市的话,五千基本上已经很高了。对于待业的,那基本上没收入,考公考编的一般都回到了小县城,随便找个单位临时上班,一个月也就两千来块!


我们就不去分析双一流,专科,中职这些了,所以整体算下来,我们现在的年轻人的收入是很低的。


三、时代特征


努力在这个社会貌似已经不是一个正能量的词了,仿佛已经是一个调侃的词了,就像现在大多女孩子,他现在不会选择一个很努力的男孩子作为伴侣,因为努力后得到回报是一个概率事件,大多会选择有“存货”的人!当然,并不是人人都这样!


社会的发展就是这样,就像森林里面的狼越来越多,那么捕获到猎物的概率就越来越小,这和努力没多大的关系,这是时代特征!


八九十年代别说考上大学,考上中专谋个职位都不难,而现在一砖头下去都能打中几个研究生已经不是什么稀奇事了。


还有现在的经济形势如此严峻,企业和单位的寒冬一直在降临,无数的人蜂拥而至,导致形势更加紧张,本来在夹缝中已经难以生存了,现在变成了针眼!


所以前段时间网红带货主播李佳琦在网上说:“找找自己的原因,工资涨了没涨,有没有认真工作”,是因为的认知出现了谬误,所以才说出了这种言论,而他的成功完全靠努力吗?你怎么看!


四、这和你有鸡毛关系!


浮躁来自于你的认知水平,在这个信息爆炸的时代,如果不能分辨真假是非,那么就很容易陷入浮躁的状态!


网络上和现实中总是充斥着一股“赚钱很容易”的妖风,他们去编造一些故事,制造一些假象来迷惑人的双眼,如果你的甄别能力不够,那么你就会觉得为啥别人那么厉害,自己为啥混成这样,从而陷入浮躁和迷茫之中,当你进入这个状态后,等待你的要么是镰刀,要么是内耗!


做人过程中的一大蠢事就是自己啥也不是的时候,总是去炫耀自己拥有的那些八竿子打不着的人脉和资源,被那些不知真假的事物去影响,去自我否定,当一个人不能独立去思考问题,不站在现实角度去看待问题的时候,那么是永远不可能获得成长的。


五、最后


现实中,很多人都是很窘迫的,赚到钱的人永远在少数,这是时代特征和个人运气所决定的,努力只占了很小一部分,所以别被互联网上的一些妖风所影响!


这个时代我们虽然能决定的东西很少,事物都充满不确定性,但是依然要如罗曼罗兰说的那样“世界上只有一种英雄主义,看清生活的真相依然热爱生活”,正因为充满不确定性,所以才有“赌”的意义!


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

axios中的那些天才代码!看完我实力大涨!

web
axios的两种调用方式 经常调接口的同学一定非常熟悉aixos下面的两种使用方式: axios(config) // 配置式请求 axios({ method: 'post', url: '/user/12345', }); axios.po...
继续阅读 »

axios的两种调用方式


经常调接口的同学一定非常熟悉aixos下面的两种使用方式:



  • axios(config)


// 配置式请求
axios({
method: 'post',
url: '/user/12345',
});


  • axios.post(url, config)


// 简洁的写法
axios.post('/user/12345')

不知道各位大佬有没有思考过这样的问题:



axios到底是个什么东西?我们为什么可以使用这两种方式请求接口呢?axios是怎么设计的?



axios原理简析


为了搞明白上面的问题,我们先按照传统思路仿照axios源码实现一个简单的axios。


手写一个简单的axios


创建一个构造函数


function Axios(config){
this.defaults = config; // 配置对象
this.interceptors = { // 拦截器对象
request:{},
response:{}
}
}

上面的代码中,我们实现了一个基本的Axios类,但它还不具备任何功能。我们现在给它添加功能。


原型上添加方法


Axios.prototype.request = function(config){
console.log('发送Ajax 请求 type:' +config.method)
}
Axios.prototype.get = function(){
return this.request({method:'GET'})
}
Axios.prototype.post = function(){
return this.request({method: 'POST'})
}

上面的代码中,我们在request属性上创建了一个通用的接口请求方法,get和post实际都调用了request,但内部传递了不同的参数,这和axios(config)、axios.post()有异曲同工之妙。


参考aixos的用法, 现在,我们需要创建实例对象


let aixos = new Axios(config)  

创建后的axios包含defaultsinterceptors属性,其对象原型__proto__上(指向Axios的prototype)包含request、get及post方法,因此,我们现在可以使用aixos.post()的方法模拟调用接口了。


但注意,此时aixos只是一个实例对象,不是一个函数!我们似乎也没办法做到改造代码使用aixos(config)的形式调用接口!


aixos是如何实现的呢?


aixos中的天才想法


为了即能使用axios(config)又能使用axios.get(),axios的核心伪逻辑如下


function Axios(config){
this.defaults = config; // 配置对象
this.interceptors = { // 拦截器对象
request:{},
response:{}
}
}

Axios.prototype.request = function(config){
console.log('发送Ajax 请求 type:' +config.method)
}
Axios.prototype.get = function(){
return this.request({method:'GET'})
}
Axios.prototype.post = function(){
return this.request({method: 'POST'})
}

function createInstance(config) {
//注意instance是函数
const instance = Axios.prototype.request;
instance.get = Axios.prototype.get
instance.post = Axios.prototype.post
return instance;
}

let axios = createInstance();

通过上述的伪代码,我们可以知道axios是createInstance()函数的返回值instance



  • instance 是一个函数,因此,axios也是一个函数,可以使用axios(config);

  • instance也是一个对象(js万物皆对象),其原型上有get方法和post方法,因此,我们可以使用axios.post()。




我们看看aixos的源码


aixos的源码实现


 function createInstance(config) {
//实例化一个对象
var context = new Axios(config); //但是不能直接当函数使用

var instance = Axios.prototype.request.bind(context);
//instance 是一个函数,并且可以 instance({}),

//将Axios.prototype 对象中的方法添加到instance函数中,让instance拥有get、post、request等方法属性
Object.keys(Axios.prototype).forEach(key => {
// console.log(key); //修改this指向context
instance[key] = Axios.prototype[key].bind(context);
})
//总结一下,到此instance自身即相当于Axios原型的request方法,
//然后又给instance的属性添加了上了Axios原型的request、get、post方法属性
//然后调用instance自身或instance的方法属性时,修改了this指向context这个Axios实例对象

//为instance函数对象添加属性 default 与 intercetors
Object.keys(context).forEach(key => {
instance[key] = context[key];
})

return instance;
}

可以说,上面的代码真的写的精妙绝伦啊!


注意这里,为什么要修改this的指向


var instance = Axios.prototype.request.bind(context);

首先,requset 是Axios原型对象上的方法,其方法内部的this指向的是其实例化对象context!


Axios.prototype.request = function request(config) {
/*eslint no-param-reassign:0*/
// Allow for axios('example/url'[, config]) a la fetch API
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}

config = mergeConfig(this.defaults, config);

// Set config.method
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}

// Hook up interceptors middleware
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);

this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});

while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}

return promise;
};

因此,如果我们直接使用Axios.prototype.request()就会出现问题,因为这事reques方法内部的this会指向错误,导致函数不能运行,因此,我们必须将this重新指向其实例化对象。


作者:石小石Orz
来源:juejin.cn/post/7387029190620184611
收起阅读 »

后端同事下班早,前端排序我来搞

web
写掘金博客有一小段时间了,我发现了一个秘密。文章阅读量小了吧,很心烦,阅读量大了吧,更心烦。很多特别懂特别会的大哥们就会来“指点”我。感谢这些大哥们的“指点”,使我进步。后来我和群里的小伙伴们讨论了一下,为什么掘金文章会有那么多大哥们来“开心指点”呢?大概是这...
继续阅读 »

写掘金博客有一小段时间了,我发现了一个秘密。文章阅读量小了吧,很心烦,阅读量大了吧,更心烦。很多特别懂特别会的大哥们就会来“指点”我。感谢这些大哥们的“指点”,使我进步。后来我和群里的小伙伴们讨论了一下,为什么掘金文章会有那么多大哥们来“开心指点”呢?大概是这么几种情况:



1 他们爱学习,爱分享,但是即将要被裁员了,所以心情不是太好,怎么办呢?去掘金上指点他们去,让他们知道自己很会很懂;


2 他们在团队中就是翘楚,指点完了团队的人,然后不放心,怕世界不足够完美,反正自己也要被裁员了,有时间,然后补偿没拿够,心情不太好,去给他们指点指点去;


3 他们一直觉得自己不太懂,但是看见文章又想指点指点,所以呢,在家找了3个月工作这段时间,一边学习,然后看大家的文章,学习过程中呢,看哪里觉得不太合适,指点指点,希望趁着阅读量高的文章,好希望有人能发现他们,直接邀约他们入职;


这种人呢,我们总结了一下,他们可以叫“黑哥会”,意思就是黑哥们,比较会,啥都会那种,嗯,希望黑哥会的成员们早日找到心仪的工作,在家闲着不好的。



好啦,在文章正式开始之前呢,告诉大家个好消息,本文点赞,友善评论,友善建议的大哥大姐们,2024年的后半年,一定能够心想事成,工作顺利,家庭和睦,一直到永久。


321... 文本正式开始。


1 未排序的数据


今天早上来了公司,我赶紧喊老张,问:新来的前端妹子这么快就被你搞定啦?听说昨晚你俩10点一起出的公司?是不是,快说。 老张,猛地抬头,问:你咋知道的?我保密工作做这么好。 我说:门口的李大爷说的。你快说说什么情况啊。


老张说:别瞎说,昨天后端下班早,把接口就给妹子了,妹子本来以为调一调接口,传几个参数完事,结果发现后端给的数据没有排序,但看了产品文档,发现,又要根据学生姓名按字母排序,又要根据分数排序,又要根据年龄排序,又要根据日期排序,直接把妹子气的快哭了,所以我就帮他弄了弄。然后就弄到10点了呗,一起出的公司而已,别瞎想。


但是妹子为了感谢我,告诉了我一个好消息,过会儿和你说。我说:你快点说。老张说:你先听我把功能说完,我再告诉你。


你看,后端就一个接口,给的数据大概是这样子:


const users = [
{"name": "小张伟", "age": 19, "score": 55, "dateTime": '2021-03-03 15:33:10'},
{"name": "张三", "age": 22, "score": 65, "dateTime": '2023-03-03 10:10:10'},
{"name": "李四", "age": 30, "score": 87, "dateTime": '2024-04-03 10:10:10'},
{"name": "阿斌", "age": 50, "score": 90, "dateTime": '2021-03-03 10:10:10'},
{"name": "曹小操", "age": 1300, "score": 23, "dateTime": '1021-05-08 10:10:10'},
{"name": "小张灰", "age": 31, "score": 15, "dateTime": '1994-03-04 08:33:10'},
];

2 根据属性排序


这是一个杂乱的json型数组,但是要根据属性进行排序。我们目前做了3种类型的实现


2.1 引入工具库


这里说一个高效便捷功能丰富的前端JS库,首先引入js-tool-big-box工具库。


执行安装命令:



npm install js-tool-big-box



引入dataBox对象,排序的这些公共方法被放到了这个对象下面:


import { dataBox } from 'js-tool-big-box';

2.2 数值型排序


数值型排序呢,就是,你看,age 和 score 都是数值型的,我们把这些归结为一类进行排序。


2.2.1 根据age从小到大的排序

代码如下:


const ageResult1 = dataBox.sortByNumber(users, 'age');
console.log('age是数值型,从小到大,排序后的值为:', ageResult1);

结果如下:


image.png


2.2.2 根据age从大到小的排序

代码如下:


const ageResult2 = dataBox.sortByNumber(users, 'age', 1);
console.log('age是数值型,从大到小,排序后的值为:', ageResult2);

结果如下:


image.png


2.2.3 根据score从低到高的排序

代码如下:


const ageResult3 = dataBox.sortByNumber(users, 'score');
console.log('score是数值型,从低到高,排序后的值为:', ageResult3);

结果如下:


image.png


2.2.4 根据score从高到低的排序

代码如下:


const ageResult4 = dataBox.sortByNumber(users, 'score', 1);
console.log('score是数值型,从大到小,排序后的值为:', ageResult4);

结果如下:


image.png


2.3 中文按字母排序


比如我们的姓名,很多时候需要按字母从A到Z来展示,这个时候就可以用下面这个方法来快速实现:


2.3.1 按字母从A到Z排序

代码如下:


const nameResult1 = dataBox.sortByletter(users, 'name');
console.log('比如name,我们按照字母顺序排序后为:', nameResult1);

结果如下:


image.png


2.3.2 按字母从Z到A排序

代码如下:


const nameResult2 = dataBox.sortByletter(users, 'name', 1);
console.log('比如name,我们按照字母顺序倒序排序后为:', nameResult2);

结果如下:


image.png


2.3.3 注意

需要注意的是,我们这里只是传入了name的属性,如果这个json中有其他中文属性,也是可以使用这个方法进行按字母排序的,很灵活。


2.4 按日期时间排序


比如我们例子中的时间,按时间排序也是非常实用且常见的需求,


2.4.1 按时间从早到晚排序

代码如下:


const timeResult1 = dataBox.sortByTime(users, 'dateTime');
console.log('以时间从早到晚排序后的值为:', timeResult1);

结果如下:


image.png


2.4.2 按时间从晚到早排序

代码如下:


const timeResult2 = dataBox.sortByTime(users, 'dateTime', 1);
console.log('以时间从晚到早排序后的值为:', timeResult2);

结果如下:


image.png


2.4.3 注意

需要注意的是,我们例子中只是传入了dateTime属性,如果json对象中有其他的是时间格式的属性值,也可以把属性传入,就可以进行字段的属性排序啦,很便捷。


3 最后


把效果展示完了,我赶紧催促老张说:你刚才跟我说的好消息呢?老张悄声说:妹子和门口老大爷,还有咱们公司老板都姓李,你品去吧。妹子跟我说了,她跟她爸爸说:这个季度的优秀就是我。我一听也跟着高兴起来,希望看到这篇文章的大哥大姐们,也都能像老张一样,升职加薪,变得越来越优秀




最后告诉你个消息:
js-tool-big-box的npm地址是(js-tool-big-box 的npm地址)


js-tool-big-box的git仓库地址(js-tool-big-box的代码仓库地址)


作者:经海路大白狗
来源:juejin.cn/post/7384419675073789991
收起阅读 »

因为打包太慢,我没吃上午饭

web
事情的起因是这样的: 鄙人呢,在公司负责一个小小的后台管理系统。 这天中午临近饭点的时候,测试小哥坐我工位旁边现场监督我改一个Bug。 Bug本身它倒是不复杂,甚至时隔几天之后我已不记清具体内容是什么。当时只见我是三下C两下V、提交、合并测试分支、登录Jeki...
继续阅读 »

事情的起因是这样的:


鄙人呢,在公司负责一个小小的后台管理系统。


这天中午临近饭点的时候,测试小哥坐我工位旁边现场监督我改一个Bug。


Bug本身它倒是不复杂,甚至时隔几天之后我已不记清具体内容是什么。当时只见我是三下C两下V、提交、合并测试分支、登录Jekins点击deploy,一顿操作如行云流水一般丝滑。


wyzjy.gif


说时迟,那时快。公司给 极品廉价劳动力们 我们安排的午饭送到了,一人一份。但你也知道,这两年经济下行,无人幸免;有的人食不果腹,有的人衣不蔽体, 保不齐谁饿的紧,拿了两份饭,那可就意味着有一个人要饿肚子,我可万万不希望那个倒霉蛋是我。


看着Jekins的deploy进度条,我对测试小哥说:


“你先回去,等会发完了你再看一下,应该没有问题,我先去干饭了”


说罢,我转头看向测试小哥:他面无表情盯着屏幕,似乎是默许了我的提议。于是我便准备起身——


只见他头也不回一手把我按住,缓缓吐出四个字:


“看完再吃”


...


...


...


大约半个小时后,KFC。


我:“我都告诉你了,不会有问题,先干饭,你非不听”


测试小哥:“......”


我:“这下好了吧,上个月的工资还没发,现在又来付费上班”


测试小哥:“我就问你,星期四的这个辣翅,它香不香”


我:“香”


image.png


罪魁祸首


所以,项目到底deploy了多久?


f7c9f47e6cbb14b80745a39c3946832.png


Jekins的记录中可以看到,编译打包环节耗时基本在五六分钟,最近几次成功构建的整体耗时平均在5分50秒。


这个项目本身呢,说大不大,说小也不算小。是个普通CRUD页面居多的管理后台,没有太多其他乱七八糟的东西。如果以页面、组件数量的维度来看:


使用资源管理器在项目的/src目录下通配*.vue可以看到有561个文件


image.png

image.png


说实话,这样的体量打包5-6分钟,属实有点过分。


我又找来公司的一个巨石应用来对比:由于巨石应用历史比较悠久,横跨了多个技术栈(HandleBars模板引擎、使用jQuery的原生HTML、Vue2),不能只看SFC的数量,所以这次就来比较一下,用来存放页面文件的文件夹的体积


image.png


我的项目








image.png


巨石应用


先不算其他的资源,单就页面文件体积已经接近5倍,如果其他东西都算上,打包时间就算没有5倍,一两倍总是要有的吧?


结果呢,时间甚至更短


image.png

好好好,有活干了。为了避免下次面试官问我对webpack做过什么优化时复述那些网上千篇一律的答案,现在就来实际操作一下。



本文真实记录了一次对项目构建耗时、产物体积优化的过程。没有对知识点系统的梳理,主要突出的是思路:面对问题时的解决思路。



日志分析


曾经有位技术能力超强的架构师说过:遇到问题不要慌,先看日志。


既然这么慢的构建过程是发生在Jekins上,那就先来捋一捋Jekins的log,有没有什么值得注意的地方。


这个项目的构建脚本中,抛开(Jekins)工具的准备、从git上拉取代码以及最后的部署这些动作,只看跟前端的打包有关的部分,命令很简单,只有四行:


rm -rf node_modules
rm package-lock.json
npm i
npm run build

在日志中体现如下图:


0c4ebfe0835a4da8d477713af40285d.png


35c974d100d807a636a530608ae602f.png


开局就是一记暴击!


v7m5.gif


11:22:08 + npm i
11:25:59 + added 1863 packages from 1199 contributors balabala...
...
...
11:25:59 + npm run build
...
...

合着这五六分钟的打包时间,安装依赖就占了一大半,阿西巴!


在继续往下进行之前,请允许我先介绍些项目的其他背景:



deploy脚本拉取代码这一步简单来说就是:cd进项目目录 -> git pull -> 切换至要构建的分支


项目的开发人员较少,算我在内三个人


项目的依赖变动频率十分低,以月或数月为单位



背景铺垫完了,开始研究npm i为什么这么耗时,相关的命令有三句:


rm -rf node_modules
rm package-lock.json
npm i

其中npm i这句是必须的,没什么好说的;rm -rf node_modulesrm package-lock.json这两句是变量,挨个做耗时的对比测试。


首先,我在本地使用跟Jekins上相同的node版本(14.16.1),使用相同的npm源(官方源),新建一个目录clone项目代码:



  1. 完整执行三行命令,耗时与Jekins上相差无几

  2. (此时已经有了package-lock.json文件)执行rm -rf node_modules + npm i,耗时极短

  3. (此时已经有了node_modules目录)执行rm package-lock.json + npm i,耗时也极短


第三步其实没什么意义,npm文档中有提到,这种情况就相当于梳理了node_modules的结构并生成了package-lock.json,并没有安装任何东西。
image.png


而步骤一和步骤二之间为何耗时相差比较大,可以参考这篇我觉得npm install流程写的比较好的文章:简单来说就是花费了大量时间去远端获取包信息


结合项目背景一,我们的package-lock.json会提交到仓库,每次肯定都是最新的,所以Jekins deploy脚本中rm package-lock.json这一步属实是没有必要,本地计算完了到Jekins上又来一遍,纯纯的浪费时间。


联系运维哥把测试环境的deploy脚本修改一下,去掉了rm package-lock.json这一句,测试下耗时:


c7085e76a3aed2877d8b6fdfb846fb9.png

如图中下边两次构建,【编译打包】环节时间都去到了2分30秒左右,直接缩短了一半多。部署成功后,在测试环境的页面咔咔一顿点,似乎也没有什么依赖包引起的报错。


效果是不是很显著,你以为这就完了?不不不,图中那条49秒的构建我可不是大意截进去的,伪装成不经意的失误,就是为了丝滑的承上启下


QwN4E.gif


既然这个项目的开发人员很少,而且依赖的添加/更新频率又极低,也就意味着每次npm i所安装的东西,基本都是一样的,既然都一样,为什么我还要rm -rf node_modules再安装?


想象一下你日常本地开发时,如果某次需求要用到一个新的npm包,你一定是先npm install xxx,除非碰到了依赖冲突,否则不会清除node_moduels重新安装。


明明npm提供了梳理依赖树、只做局部更新的逻辑,我们却偏偏每次清除node_modulesinstall,这种行为吧,我感觉就像明明是个Vue项目,却在里边到处使用Document API


哎嘿,我就不用你的响应式,就是玩~


lKGp.gif


冒着被打的风险又私聊了运维哥,把rm -rf node_modules去掉,再发布了一次看看效果


677c1174550bece7d6fabb58a09597a.png

优秀!打包时间从5分多直接干到了50秒,优化率80%+!


本文结束!



在正式结束前,觉得还是有必要补充两点



  1. 各位读者在做打包优化时,部署脚本是否清除package-lock.jsonnode_modules还是主要取决于项目实际情况和团队协作模式,不能因为一味的追求构建速度而导致频繁的构建失败/安装依赖失败[滑稽]

  2. 如果您经过深思熟虑后觉得还是有必要清除package-lock.jsonnode_modules,山人还有一计可供大王优化构建速度:打包时离不开babel,但babel又是个老大难,好在它能缓存转换结果。一般情况下缓存会放在项目目录下的/node_modules/.cache/,那我们把删除node_modules的命令稍微改那么亿点:


    find node_modules/ -mindepth 1 -maxdepth 1 ! -name '.cache' -exec rm -rf {} +


    删除node_modules里面除了/.cache目录以外的其他内容,这样在构建过程中babel还是能使用到之前的缓存。那速度,体验过的都说好!(看babel-loader的缓存文件有多大)


    0716d75b5c769cef10689560947061b.png










全面升级


如果是本着以后不影响吃午饭的目的,那现在确实可以结束了。但我自幼便深受中国四大名句之一来都来了的兄弟句式——干都干了的文化熏陶:既然已经开始了,那就干脆给项目做个全套大保健!


image.png


不过此时我和在座的各位都一样,对打包优化这块着实没什么经验,可以说是毫无头绪。


浅浅百度了下webpack打包优化,有两个工具基本每篇文章都有提到:打包耗时分析speed-measure-webpack-plugin、打包体积分析webpack-bundle-analyzer(vue-cli内置)


目前的痛点是,那就先来个耗时分析试试水。


使用方法还是老样子,自己去查,别人都写的我就不再重复写了


效果如图:


2d1e9f581d75198215a76ae86e410db.png


9f5463a422434e783d851fe6cf13dbc.png


此时因为babeleslint还没有缓存,耗时多是意料之中的;其他的loader或多或少的三两组合,展示了一个module计数和耗时小计,我从中并没有办法获得什么有用的信息;并且多次构建对比发现loader组合的规律和耗时的排名也无迹可寻。在我的认知里:所有被命中的文件会按照loader配置的顺序依次处理,所以面对这样的结果我实在是无计可施。(有会看的朋友可以补充一下)


翻看speed-measure-webpack-plugin的文档,发现有可以打印耗时top N文件的配置项,但开启后再次构建得到的这些文件,同样令我摸不着头脑:一个寥寥数十行的SFC小组件,css-loader耗时竟然能用四到五秒!要知道里边只有一条scoped的样式规则。


无奈只好放弃,看了下项目用的是vue-cli@4.x创建的,对应的webpack@4.x,那就去webpack的文档里逛逛碰碰运气吧!


可惜,福无双至祸不单行。文档里翻了半天,耳熟能详、配置简单的路子,例如babel-loader、eslint-loader的编译缓存多线程打包chunk分割代码混淆压缩tree shaking这些,要么是之前已经被配置过了,要么是webpack内置了。而复杂、高级一些的优化方式,我的项目又用不到...


直到我看到了这里:


image.png

升级webpack简单(呸),npm upgrade webpack嘛,先来搞这个~~


回到项目的package.json里,咦,好奇怪,没有webpack,也没有vue-cli


vue-cli是装在全局的,而webpack是作为依赖的依赖安装的,没有体现在package.json中,所以直接npm upgrade webpack应该是不行的。vue-cli文档提供了一个升级的命令:vue upgrade


既然要升级,干脆全上新的!Node也给他干到20!(我也不知道我当时为什么要这么做,但这为后来的事情埋下了伏笔。。。)


vue-cli升级完,扫了一遍webpack升级指南,发现我项目里的配置文件也没什么好改的,Nice!


image.png


本地浅浅的run了一下server、run了一下build,发现也都OK!那就提交上去在Jekins上试试Node V14o不ok


emmmm...


报错倒是没报错,只是...


4f7cb2e83ffe8afb4db6cd7c5b02221.png

本地build的时候没注意,Jekins上跑才发现,怎么慢了这么多!说好的更新到最新版本均有助于提高性能呢?


再看看这构建物的体积


05d17813e68c984f311cff8868385fb.png


image.png


Hà的我赶紧又本地build了一次,还真让我发现了些东西:似乎build了两次


4e9531cc221e18e94592ac4df18ff6f.png


image.png


按理来说应该只有下边这个print,那上边的legacy bundle又是什么东西?百度上随便那么一搜,应该是不少人都被这么坑过,很容易搜到:这是一种兼容性的构建产物,主要是为了兼容一些很古老版本的浏览器/客户端。想控制也很容易,改package.json里的browserslist字段即可。


这就好办了,这项目是我们公司的内部项目,考虑兼容性?不存在的。


{
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}

配置了之后又试了下,基本恢复到了升级webpack之前的水平,但还是慢一点点...


构建速度的优化这块,实在是没头绪了,明明升级了webpack版本,构建速度却变慢了。


不过刚才提到的两个工具,构建耗时分析的用过了,还有个vue-cli内置的构建体积分析工具没体验;如果需要打包的东西变少了,那构建速度应该也能快一点吧!(吧?)


塑形瘦身


在正式瘦身前,有一个小插曲:


不知道在座的各位,项目里有没有这样的东西


console.log(123123)
// or
console.log('asdfasdf')

我是一个崇尚极简的人,我能接受的底线也就是


console.log('list data: ', data);

仅此而已


你要打印接口返回数据,Network里能看


你要打印函数中某个变量的值,可以打断点


我实在是想不出什么必须console.log的场景


如果你说为了方便线上调试


我能接受的最多也就是按规范打印有意义的log


更别提项目首屏就要翻好几页的无意义log,要知道,大量的console.log也是会影响首屏加载性能


在之前,我通过husky + lint-staged进行过限制,但还是有人以我这个有用这之前不是我写的等等诸多借口绕过了eslint检测,提交了无意义的log。所以这次我最终还是决定,你不仁就休怪我不义,TerserPlugin drop_console走起,本地开发你随便log,只要发到线上我就删掉。


{
plugins: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
pure_funcs: ["console.log", "console.info"],
},
},
})
]
}

毕竟删掉几句console.log,也算瘦身
















接着就webpack-bundle-analyzer走起,vue-cli内置的使用方式是


vue-cli-service build --report

打包后会在你输出的目录里边生成一个report.html,当时的截图找不到了,用语言描述一下就是:从node_moduels里打进去的依赖包,面积直接占了整个屏幕的大概三分之一。那个图网上很容易找到,内容就是打包产物按照体积和来源绘制成一个个的矩形在页面里。


这其实是好事,打进去的依赖包多,我们的可操作空间就大,先拿Vue开个刀。


// vue.config.js
module.exports = {
// ...
configureWebpack: {
externals: {
vue: "Vue",
},
},
// ...
}

也不要忘了把package.json里的vue依赖删除掉、在/public的模版HTML中,通过<script>引入CDN文件。


再打个包看看效果:


ac297e6a4499324cbbf2090d5c9b72d.png

可以看到vue确实咩有了


但在调试的过程中,发现第三方CDN不稳定,时而获取超时


a2e3ed8dfb40b5eb9d36693029e55e9.png

为了保险起见,只得把CDN文件copy到本地/public里来(我司没有自己的CDN或者依赖私仓,正在筹备中)


暂时没什么问题了,下一个就是我们的UI组件库ant-design-vue@1.7.8,按照相同的方式配置一下,不过这次运行后有报错了:


f4f2f6a9b9a3afaaa82b637617803dc.png

可以看到报错是和moment有关系,在antd的文档也找到了原因:如果使用已经构建好的文件,要自行引入moment



为什么antdv不做按需引入?原因有二:



  1. 项目的入口main.js中全量导入了antdv进行注册,页面中直接使用。如果要改成按需引入,要么每个页面里新增按需引入的语句,要么统计使用了哪些组件在main.js里改为按需引入(似乎有plugin解决这个问题,记不太清了)

  2. 按需的这个需,基本等于全量了。。。粗略的扫了一下文档,除了像CommentMentions这种带有互动性质的组件,其他的基本都用上了,所以改按需好像意义也不大



moment的时候国际化有一个小问题,CDN网站一般会提供以下几种文件:



  1. 无国际化的moment主体文件

  2. 带全部语言包的moment主体文件

  3. 单个语言包文件(无功能)


如果没有国际化的需求,那是万万没有必要引入全部语言包的moment。但moment默认是英语,至少需要引入一个中文语言包。碰巧antdv也需要做国际化处理,是相同的问题。


momentantdv的国际化方式很相似:


<a-config-provider :locale="antdLocale" />

moment.locale(momentLocale);

data() {
return {
antdLocale,
momentLocale
}
}

我们只需要知道这个locale运行时的值,把它提取出来就行了。打印后发现其实就是个很简单的key-value对象(不是JSON),在node_modules中的源码里找到 它们复制出来在/public下新建zhCN文件:


window.momentLocale = xxx /* 复制出来的对象 */
window.antdLocale = xxx /* 复制出来的对象 */

image.png


image.png


使用时:


<a-config-provider :locale="antdLocale" />

moment.locale(momentLocale);

data() {
return {
antdLocale: window.antdLocale,
momentLocale: window.momentLocale
}
}

以后如果有别的依赖也有类似的国际化需求,继续向zhCN.js里添加就行。只需要新增一个http请求,就解决了所有依赖的国际化问题。


剔除了antdvmoment之后的report.html


55b3f51ed28b6eb71e2939a26d8e471.png


惊喜的发现,antdvicons也被一起干掉了。


少了这么几个大家伙,此时必须要Jekins上build一波看看效果!


还记得之前把Node给升到20了吗


于是就...报错了...Node版本太低...


image.png


本地切回NodeV14,发现连server也起不来了。。


摸黑前行


预警:这将是一段枯燥且艰难的黑暗时光


搞过的都知道,处理Node版本兼容问题时,如果是需要升级还好;如果是要降级,Node内置的各种包会出现稀奇古怪的报错,而且这些报错还难以trace...



由于这趴的问题实在过于稀奇,甚至在google上都搜不到有用的信息,所以基本都没有截图,但我会尽可能的描述出我对问题的看法。看文字也许你觉得云淡风轻解决起来很轻松,但实际上花费了我接近一整天的时间以及一撮撮掉落的头发...



1. npm run server出现大量的.vue单文件报错


具体的报错信息记不太清,但报错顺序与路由表注册的顺序相符(和动态路由懒加载是两码事,路由懒加载是在运行时访问到页面才会加载对应的chunk,但编译打包时,只要是代码中webpacktrace到的文件,都会被处理)。目测是所有的.vue都有报错,那问题就应该不是出在代码上,而是整体配置上。


翻看vue-loader文档时看到了这个


image.png

升级vue-cli时确实也升级了vue-loader,按照指引配置了下,resolve


2. jsx语法报错


这个问题就有点奇怪了,在升级前,是没有给webpack做过什么支持jsx语法的配置的。升级后,却都报错了。


翻阅了一些资料和支持jsx的解决方案,大部分都是说把SFC<script>加上lang="jsx",里边的内容全部当作jsx解析。这种方式对eslintbabel的配置改动比较大,曾数次尝试无法成功,最后都把所有配置还原重新开始。


image.png


后来灵光一闪,不如直接用刚更新的vue-cli创建一个新项目,看是否支持,如果可行的话,直接把各个配置文件照搬即可。


4799c859068eaebc4f6f4a8db07ac02.png

9221f6f9f1e017649f47b29be1ab01d.png


结果还真可以。babel.config.jsvue.config.js以及package.jsoneslintConfig字段,先全部按照官方脚手架的配置改掉,成功启动之后再挨个把我们自定义的配置添加回去。这过程当中没有出现什么问题,且按下不表,resolve


3. 启动之后,页面白屏报错:(0 , vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent) is not a function


其中resolveComponent也有可能是其他一些Vue3暴露出来的Api,通过打断点观察,推测是Vue内部在初始化的时候出了问题。


不确定是哪里出了问题,但在把之前删除的Vue依赖安装回来(只是开发环境会用到,打包不会打进去)以及把添加的VueLoaderPlugin去掉以后,resolve


迎接黎明


以上这些问题解决以后,已经可以正常启动、打包项目了。但刚才的bundle analyzer进行到一半还没结束,图中的第三方依赖库应该还有一些可以剔除掉的,比如隐藏在一个业务代码chunk里的echarts


7ae1cd4a1c4f0715606a5074554c2a7.png


检索了代码后,发现有按需引入的:


import {xxx, xxx} from "echarts"

也有全量引入的:


import * as echarts from 'echarts';

在分析代码后整理了所使用到的echarts Api和组件,把全量引入改为按需引入,重新打包后发现包体积没有变。我好奇难道echarts只要有一个地方使用了按需引入,其他地方也能自动分析把全量引入改为按需引入?遂把按需引入的也反向改为全量引入重新打包,结果:


5010aca8d7ff78da92b22705416a4c1.png


第二次的改动体积变化了,那就只能说明....


问了写那段代码的同事,果然,在需求迭代的过程当中技术方案变更了,所以那个文件废弃掉没有用了,改了个寂寞...


此时还剩下jquerylodash计划剔除掉,其他的依赖包有一部分已经是比较规范的按需引入,剔除掉改为cdn引入带来的收益不大,当然主要的原因还是因为


jquery:这个npm包有点意思


5794b48e2187021280c39b9a6122da5.png


打进来的是非压缩版本,因为package.json中设置的main就确实是这个,但dist包中明明提供了压缩后的版本。两个版本的体积差距在三倍多,不知包作者的意图是什么


1ed025730a7a1ba28ef75a190cb696e.png


298e66d7e62306a1dc18b97e6b10ac5.png


但最后还是把jquery这个依赖彻底放弃了:整个项目中只有一个远古时期添加的图片预览组件依赖了它,而我们现在开发了样式、功能更为强大的新组件,所以把所有使用到这个组件的地方都改为使用新组件,然后顺带把jquery uninstall了。


lodash:官网本身提供了可按需引入的版本lodash-es,但项目中太多地方都是全量引入的方式在使用


import * as _ from "lodash"

暂且先改成CDN的方式全量引入


至此,bundle analyzer的分析图变成了这样:


8920151ddcf9709147f3a9ab8e094ec.png


三方依赖的chunk已经比包含了echarts的那个业务代码chunk体积还要小。瘦身瘦到这里感觉差不多了,那些更小的依赖包本身体积不大,换成一个http请求也未必是一件划算的事。


然后就还是回到webpack的配置上来,前边一直在琢磨怎么添加配置去做优化,但vue-cli本身已经封装了一套久经考验的配置,不如从这个配置着手,看能否针对我们项目的实际情况做一些修改


获取配置命令(融合了自定义的配置)


vue inspect --mode=production > file-name.js

mode不传的话默认是development。下载下来打开,1400多行猛的一看似乎有点唬人,但实际上有1000行左右都是对样式文件的loader配置。


image.png


粗略的看下vue-cli@5.0.8中有哪些值得注意的配置



  • 解析文件的优先级


// 导入模块时如果不提供文件后缀,同名文件 后缀名的优先级
extensions: [".mjs", ".js", ".jsx", ".vue", ".json", ".wasm"]


  • Hash


optimization: {
realContentHash: false, // 使用非严格的hash计算,减少耗时
}


  • 代码压缩:css使用的是CssMinimizerPluginjs使用的是TerserPlugin


minimizer: [
// 已经内置了js压缩工具terser
new TerserPlugin({
terserOptions: {
compress: {
arrows: false,
collapse_vars: false,
comparisons: false,
computed_props: false,
hoist_funs: false,
hoist_props: false,
hoist_vars: false,
inline: false,
loops: false,
negate_iife: false,
properties: false,
reduce_funcs: false,
reduce_vars: false,
switches: false,
toplevel: false,
typeofs: false,
booleans: true,
if_return: true,
sequences: true,
unused: true,
conditionals: true,
dead_code: true,
evaluate: true,
},
mangle: {
safari10: true, // 代码混淆时兼容使用`let`关键字声明的循环迭代器变量可能会出现无法重复声明let变量的错误。
},
},
parallel: true, // 多进程打包
extractComments: false, // 不将注释单独提取到一个文件中
}),
new CssMinimizerPlugin({
parallel: true,
minimizerOptions: {
preset: [
"default",
{
mergeLonghand: false,
cssDeclarationSorter: false,
},
],
},
}),
]


  • Loader

    • 大量的篇幅编排不同样式文件相关的Loader,分别有csspostcssscsssasslessstylus,按照css moduels in SFC -> SFC style -> normal css modules -> normal css的顺序依次处理。

    • 对于脚本文件,已经开启了多线程转译以及babel缓存功能




{
test: /\.m?jsx?$/,
exclude: [
function () {
/* omitted long function */
},
],
use: [
{
loader:
"path-to-your-project/node_modules/thread-loader/dist/cjs.js",
},
{
loader:
"path-to-your-project/node_modules/babel-loader/lib/index.js",
options: {
cacheCompression: false,
cacheDirectory:
"path-to-your-project/node_modules/.cache/babel-loader",
cacheIdentifier: "1d489a9c",
},
},
],
}


  • Plugin

    • VueLoaderPlugin:已经内置了

    • DefinePlugin:注入编译时的全局配置

    • CaseSensitivePathsPlugin:路径的大小写严格匹配

    • FriendlyErrorsWebpackPlugin:优化报错信息

    • MiniCssExtractPlugin

    • HtmlWebpackPlugin

    • CopyPlugin:配置了info.minimized = true,copy的同时也会压缩

    • ESLintWebpackPlugin:同样开启了缓存




得,不仅没找到有啥可优化的地方,甚至还污染了人自带的配置:


已经内置了TerserPlugin,前边为了打包时去除consoleplugin里边又配置了一次,通过speed-measure-webpack-plugin分析时发现似乎是走了两遍TerserPlugin


只好通过webpack-chain去注入一下,顺便把项目中其他修改webpack配置的地方也改为注入的形式。(使用ConfigureWebpack去改,无法改到已有的TerserPlugin配置):


chainWebpack: (config) => {
config.when(process.env.NODE_ENV === "production", (config) => {
config.devtool(false);
config.optimization.minimizer("terser").tap((args) => {
const compress = args[0].terserOptions.compress;
args[0].terserOptions.compress = {
...compress,
drop_console: true,
pure_funcs: ["console.log", "console.info"],
};
return args;
});
});
config
.externals({
vue: "Vue",
moment: "moment",
"moment/locale/zh-cn": "moment.local",
"ant-design-vue": "antd",
lodash: "_",
})
.resolve.alias
.set("@", path.join(__dirname, "src"))
.set("@worker", path.resolve(__dirname, "public/worker.js"))
.end();
config.plugin("speed-measure").use(SpeedMeasurePlugin);
}

image.png


如果使用ConfigureWebpack


configureWebpack: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
pure_funcs: ["console.log", "console.info"],
},
},
}),
],
},

集成的配置最下方会出现一个新的minimizer数组,不是我们想要的效果


image.png




截止到目前,构建速度变成这样(果然还是没有变更快)


image.png


从项目剔出去的第三方依赖,体积是这么多


928af467649ccc1467ac6476c0d230e.png


不过虽然没打进chunk里去,但还是作为静态依赖在构建产物中,下一步就是搭一个公司内部的简易缓存服务器(有关缓存的内容可以看我上一篇文章)。届时这部分体积才算真正从项目里移除了,不过此时我们还是可以把它视作优化的成果,由于没有对别的类型的资源做什么优化处理,也压根就没什么别的资源,所以只看打包后的js体积的话:


image.png


优化前








image.png


优化后


数据也基本对的上,所以综合来看:



  • 平均构建速度(average full time):从5分50秒减少到54秒,优化率84.48%1 - 54秒 / 3分50秒

  • 脚本构建体积(script size):从5.5M减少到3.6M,优化率35.55%1 - 3.6M / 5.5M






先这样吧,至少下次被问到webpack,多少有点自己的东西能讲,近期可能也不会再更新文章了,原因嘛,你们懂的


image.png


欢迎真诚交流,但如果你来抬杠?阿,对对对~ 你说的都对~


作者:Elecat
来源:juejin.cn/post/7389044903940603945
收起阅读 »

2024 年中总结:务虚年代,逆风飞翔

大家好,我是双越老师,也是 wangEditor 作者 这是一篇散文,没啥结构性,比较随性,想到啥就写点啥吧。 不觉 2024 年过了 6 个月了,你有没有总结一下自己这半年做了啥?除了上班下班,有没有攒下一些钱?有没有收获一些美好的经历?自己的专业技能有没有...
继续阅读 »

大家好,我是双越老师,也是 wangEditor 作者


这是一篇散文,没啥结构性,比较随性,想到啥就写点啥吧。


不觉 2024 年过了 6 个月了,你有没有总结一下自己这半年做了啥?除了上班下班,有没有攒下一些钱?有没有收获一些美好的经历?自己的专业技能有没有得到提升?


如果你日常有积累、有主动去争取什么东西,哪怕每天一点点,积累半年就是一个大成就。如果你什么都没有,或很少,那时间也不会可怜你。


回顾我自己的工作日志,我这半年积累的还不错,比去年要好很多。是的,自由职业这么多年,我一直有记录工作日志的习惯,保持自律。


image.png


首先,我觉得我做的最好一件事儿,不是工作中的,是戒烟。我从 2023.8 开始戒烟,一直到现在 9 个月的时间了。戒烟也是我 2023 年做的最成功的一件事儿,2024 年继续。


戒烟是一个持久的事情,有些人戒 3 年还会复吸,我能继续保持到现在也是非常不容易。未来继续保持,这比工作成绩都重要。


戒烟,第一是保持身体的健康状态,例如现在嗓子清爽,跑步时呼吸通畅;第二是要让自己摆脱对某个东西的强依赖,而恢复自由的状态。因为烟瘾本质上就是尼古丁的戒断反应、就是生理du瘾,戒不掉就永远是它的奴隶。


今年春天开始,我也几乎不喝酒了。这半年聚餐吃饭无数次,但几乎没有喝过酒(偶尔一点点啤酒),我朋友戏称:你这戒烟戒酒,再往后就得戒色了...


我不懂葡萄酒,但那种纯麦啤酒,喝起来很香甜的那种,我还是比较喜欢的,当然得少喝。但日常聚餐的这些工业啤酒、白酒就算了,尤其白酒,本身就是一种反人类的饮料。爱喝白酒的人,都是为了快速获得酒精的刺激,跟吸烟是一个道理,所谓好酒就是既能让你快速获得酒精刺激、又不那么辣嗓子、第二天又不是很难受 —— 第一个目的最重要。


所以,我觉得和烟拜拜了,酒也该再见了,都是一类东西,都是瘾品(虽然我没酒瘾)。古人说的“酒色财气”都归属于“酒”类,因为古代没有烟。


同时,我一直坚持着每日跑步和 15 分钟的力量训练,规律生活早睡早起,所以整体感觉还不错。颈椎病依然还有(只要得了,就无法完全恢复)但不难受,也没啥影响。


再聊聊工作吧。


1月的时候,我实在是不知道干啥了。PS. 其实一整个 2023 年都不知道干啥,太闲了,所以戒烟找点成就感,没想到成功了。


当时的任务就是把我的两门 ke 程更新一下,一门是面试课(这个每年都更新一次),一门是《React仿问卷星 低代码》升级了服务端,使用 Nest.js 写的。


同时也有很多人找我 1v1 面试咨询,我记得春节假期还没结束,就开始预约了。集中忙了大概一个月,每天都和 2-3 个人聊,一边分析简历,一边绞尽脑汁的思考,一边写总结,一边聊,非常累。


但是后来到了 3、4 月发现,今年的行情真的太差了,之前的“金三银四”今年是一点都没体现出来,没体现出任何热度,倒是裁员不断。这和刚刚过去的 6.18 很像,也没体现出任何热度,就和 5 月一样。


大家就像一头一头的死猪,拿开水刺激已经不管用了,无论是公司还是个人。


在这种行情下,我能干啥呢?很多人说自己被裁员,找不到工作,其实我也有相似之处,你们就业环境不好,我也找不到突破口。怎么办呢?


要不我也像其他“讲师”(网红)一样,去深入搞一搞面试服务?开个训练营,多收一些钱,宣传一顿:保证就业率、涨薪多少……


其实能实打实的为学生服务的讲师也挺好的,为你备课,为你准备面试题,为你准备项目,价格公道,这已经很好了。还有一些机构跟骗子也差不多。


有专门的销售催着你,学费 1w 起步,承诺内推、承诺涨薪 xx —— 理性思考,当前这环境,哪儿这好事。


很惭愧,我虽然也是个自由职业,但对圈里的这些事儿,我还真不清楚。咨询了一个朋友才知道具体的事情。


image.png


后来我跟那位同学说:这样,你先让她给你安排内推机会,只要有了面试邀请,你就报名 —— 你看她还理不理你。


我当时想了一个月,我该干嘛呢,最终决定还是踏踏实实的做个项目吧,慢慢开发慢慢积累,做出一个扎实的产品才是长久之道。


而且也是一个真正的壁垒,自己长久积累做出来的项目,别人不可能一下子就做出来。但像面试服务这种东西,想搭建起来是很容易的,没啥壁垒,面试题、考点就那么多,找个技术讲师就齐了。最近有很多前端 A 哥、B 哥、C 哥的,都是这种机制,自己负责发广告招人,然后雇讲师上课。


但做项目做个啥呢?选题和方向也是需要思考的,主动思考是最难的事情。


第一,不能过于简单,技术和业务都得有一定复杂度,而且要是全栈的,前后端都有。也不能过于复杂,一个人搞个淘宝微信也搞不了。


第二,要是真实上线的项目,因为课程项目一直被吐槽为 demo ,所以这次我要做一个不一样的,这样才有差异化。


第三,还要带有一定的话题性和热度,和当前技术发展趋势要吻合。例如今年低代码不再热门了,就不要再用低代码这个话题了。所以我选择了 AI 方向。


第四,要考虑自己擅长的方向,发挥自己擅长的技术点。我擅长富文本编辑器领域,这个要充分利用起来。


于是这个项目就是,使用 Node 全栈 Next.js 开发 AIGC 知识库项目 划水AI ,参考 Notion AI ,基于 GPT 大模型,开发 AI 写作、AI 处理文本,这是当前的热门话题。


现在一期功能(文档管理、富文本编辑器、AI 写作)已经上线试用,二期(团队、分享、多人协同编辑)正在开发中,想加入学习的同学可以找我。



这个项目的研发过程,被我记录成了详细的 wiki 还有代码提交记录,我每做一步就记录一步,遇到一个 bug 我就记录一个 bug —— 这都是宝贵的项目经验。


image.png


现在是一个务虚的时代,务实的人太少了。务虚低成本高收益。务实,成本高,而且你还得能做出来呀!君子只动口,傻子才动手呢。 中国人自古以来就是:圣人坐而论道、舌战群儒,没有去动手做实验的。


我已 36 岁,还能有这种创造力和执行力其实挺不容易的。
30 岁之前大家都精力满满,对未来充满好奇和希望,对技术充满动力和热情,浑身都充满了创造力。但是慢慢熬到 35+ 就容易如老牛一般(王小波说的“被锤的公牛”)被工作生活磨的角都没了。


我最早开始写博客是 2014 年,最早开发 wangEditor 是 2015 年,至今已快 10 年了。即便强如周杰伦,他的创作黄金期也不到 10 年(随便举个例子,不是拿自己和周杰伦比,我也是周杰伦铁粉~)


总结一下。


半年,从一开始的迷茫、没有方向,到后来思考、分析、确定项目,再到后来开发、测试、一期上线。时间一晃而过,但回头一看确实积累了很多东西。


下半年继续,把二期、三期搞定并上线,用一年时间做出一款优秀的产品,教你亮瞎面试官的眼睛!


与君共勉~


作者:前端双越老师
来源:juejin.cn/post/7388891131037614089
收起阅读 »

听说你会架构设计?来,弄一个打车系统

目录引言网约车系统需求设计概要设计详细设计体验优化小结1.引言1.1 台风来袭深圳上周受台风“苏拉”影响,从 9 月 1 日 12 时起在全市启动防台风和防汛一级应急响应。对深圳打工人的具体影响为,当日从下午 4 点起全市实行 “五停”:停工、停业、停市,当日...
继续阅读 »

目录

  1. 引言
  2. 网约车系统
    1. 需求设计
    2. 概要设计
    3. 详细设计
    4. 体验优化
  3. 小结

1.引言

1.1 台风来袭

深圳上周受台风“苏拉”影响,从 9 月 1 日 12 时起在全市启动防台风和防汛一级应急响应。

对深圳打工人的具体影响为,当日从下午 4 点起全市实行 “五停”:停工、停业、停市,当日已经停课、晚上 7 点后停运。

由于下午 4 点停市,于是大部分公司都早早下班。其中有赶点下班的,像这样:

有提前下班的,像这样:

还有像我们这样要居家远程办公的:

1.2 崩溃打车

下午 4 点左右,公交和地铁都人满为患。

于是快下班(居家办公)的时候就想着打个车回家,然而打开滴滴之后:

排队人数 142 位,这个排队人数和时长,让我的心一下就拔凉拔凉的。

根据历史经验,在雨天打上车的应答时间得往后推半个小时左右。更何况,这还是台风天气!

滴滴啊滴滴,你就不能提前准备一下嘛,这个等待时长,会让你损失很多订单分成的。

但反过来想,这种紧急预警,也不能完全怪打车平台,毕竟,车辆调度也是需要一定时间的。在这种大家争相逃命(bushi 的时候,周围的车辆估计也不太够用。

卷起来

等着也是等着,于是就回到公司继续看技术文章。这时我突然想到,经过这次车辆紧急调度之后,如果我是滴滴的开发工程师,需要怎么处理这种情况呢?

如果滴滴的面试官在我眼前,他又会怎么考量候选人的技术深度和产品思维呢?

2. 设计一个“网约车系统”

面试官:“滴滴打车用过是吧!看你简历里写道会架构设计是吧,如果让你设计一个网约车系统,你会从哪些方面考虑呢?”

2.1 需求分析

网约车系统(比如滴滴)的核心功能是把乘客的打车订单发送给附件的网约车司机,司机接单后,到上车点接送乘客,乘客下车后完成订单。

其中,司机通过平台约定的比例抽取分成(70%-80%不等),乘客可以根据第三方平台的信用值(比如支付宝)来开通免密支付,在下车后自动支付订单。用例图如下:

乘客和司机都有注册登录功能,分属于乘客用户模块和司机用户模块。网约车系统的另外核心功能是乘客打车,订单分配,以及司机送单。

2.2 概要设计

网约车系统是互联网+共享资源的一种模式,目的是要把车辆和乘客结合起来,节约已有资源的一种方式,通常是一辆网约车对多个用户。

所以对于乘客和司机来说,他们和系统的交互关系是不同的。比如一个人一天可能只打一次车,而一个司机一天得拉好几趟活。

故我们需要开发两个 APP 应用,分别给乘客和司机打车和接单,架构图如下:

1)乘客视角

如上所示:乘客在手机 App 注册成为用户后,可以选择出发地和目的地,进行打车。

打车请求通过负载均衡服务器,经过请求转发等一系列筛选,然后到达 HTTP 网关集群,再由网关集群进行业务校验,调用相应的微服务。

例如,乘客在手机上获取个人用户信息,收藏的地址信息等,可以将请求转发到用户系统。需要叫车时,将出发地、目的地、个人位置等信息发送至打车系统

2)司机视角

如上图所示:司机在手机 App 注册成为用户并开始接单后,打开手机的位置信息,通过 TCP 长连接定时将自己的位置信息发送给平台,同时也接收平台发布的订单消息。

司机 App 采用 TCP 长连接是因为要定时发送和接收系统消息,若采用 HTTP 推送:

一方面对实时性有影响,另一方面每次通信都得重新建立一次连接会有失体面(耗费资源)。

司机 App:每 3~5 秒向平台发送一次当前的位置信息,包括车辆经纬度,车头朝向等。TCP 服务器集群相当于网关,只是以 TCP 长连接的方式向 App 提供接入服务,地理位置服务负责管理司机的位置信息。

3)订单接收

网关集群充当业务系统的注册中心,负责安全过滤,业务限流,请求转发等工作。

业务由一个个独立部署的网关服务器组成,当请求过多时,可以通过负载均衡服务器将流量压力分散到不同的网关服务器上。

当用户打车时,通过负载均衡服务器将请求打到某一个网关服务器上,网关首先会调用订单系统,为用户创建一个打车订单(订单状态为 “已创建”),并存库。

然后网关服务器调用打车系统,打车系统将用户信息、用户位置、出发地、目的地等数据封装到一个消息包中,发送到消息队列(比如 RabbitMQ),等待系统为用户订单分配司机。

4)订单分配

订单分配系统作为消息队列的消费者,会实时监听队列中的订单。当获取到新的订单消息时,订单分配系统会将订单状态修改为 “订单分配中”,并存库。

然后,订单分配系统将用户信息、用户位置、出发地、目的地等信息发送给订单推送 SDK

接着,订单推送 SDK 调用地理位置系统,获取司机的实时位置,再结合用户的上车点,选择最合适的司机进行派单,然后把订单消息发送到消息告警系统。这时,订单分配系统将订单状态修改为 “司机已接单” 状态。

订单消息通过专门的消息告警系统进行推送,通过 TCP 长连接将订单推送到匹配上的司机手机 App。

5)拒单和抢单

订单推送 SDK 在分配司机时,会考虑司机当前的订单是否完成。当分配到最合适的司机时,司机也可以根据自身情况选择 “拒单”,但是平台会记录下来评估司机的接单效率。

打车平台里,司机如果拒单太多,就可能在后续的一段时间里将分配订单的权重分数降低,影响自身的业绩。

订单分派逻辑也可以修改为允许附加的司机抢单,具体实现为:

当订单创建后,由订单推送 SDK 将订单消息推送到一定的地理位置范围内的司机 App,在范围内的司机接收到订单消息后可以抢单,抢单完成后,订单状态变为“已派单”。

2.3 详细设计

打车平台的详细设计,我们会关注网约车系统的一些核心功能,如:长连接管理、地址算法、体验优化等。

1)长连接的优势

除了网页上常用的 HTTP 短连接请求,比如:百度搜索一下,输入关键词就发起一个 HTTP 请求,这就是最常用的短连接。

但是大型 APP,尤其是涉及到消息推送的应用(如 QQ、微信、美团等应用),几乎都会搭建一套完整的 TCP 长连接通道。

一张图看懂长连接的优势:

图片来源:《美团点评移动网络优化实践》

通过上图,我们得出结论。相比短连接,长连接优势有三:

  1. 连接成功率高
  2. 网络延时低
  3. 收发消息稳定,不易丢失

2)长连接管理

前面说到了长连接的优势是实时性高,收发消息稳定,而打车系统里司机需要定期发送自身的位置信息,并实时接收订单数据,所以司机 App 采用 TCP 长连接的方式来接入系统。

和 HTTP 无状态连接不同的是,TCP 长连接是有状态的连接。所谓无状态,是指每次用户请求可以随意发送到某一台服务器上,且每台服务器的返回相同,用户不关心是哪台服务器处理的请求。

当然,现在 HTTP2.0 也可以是有状态的长连接,我们此处默认是 HTTP1.x 的情况。

而 TCP 长连接为了保证传输效率和实时性,服务器和用户的手机 App 需要保持长连接的状态,即有状态的连接。

所以司机 App 每次信息上报或消息推送时,都会通过一个特定的连接通道,司机 App 接收消息和发送消息的连接通道是固定不变的。

因此,司机端的 TCP 长连接需要进行专门管理,处理司机 App 和服务器的连接信息,架构图如下:

为了保证每次消息的接收和推送都能找到对应通道,我们需要维护一个司机 App 到 TCP 服务器的映射关系,可以用 Redis 进行保存。

当司机 App 第一次登录,或者和服务器断开连接(比如服务器宕机、用户切换网络、后台关闭手机 App 等),需要重连时,司机 App 会通过用户长连接管理系统重新申请一个服务器连接(可用地址存储在 Zookeeper 中),TCP 连接服务器后再刷新 Redis 的缓存。

3)地址算法

当乘客打车后,订单推送 SDK 会结合司机所在地理位置,结合一个地址算法,计算出最适合的司机进行派单。

目前,手机收集地理位置一般是收集经纬度信息。经度范围是东经 180 到西经 180,纬度范围是南纬 90 到北纬 90。

我们设定西经为负,南纬为负,所以地球上的经度范围就是[-180, 180],纬度范围就是[-90,90]。如果以本初子午线、赤道为界,地球可以分成4个部分。

根据这个原理,我们可以先将二维的空间经纬度编码成一个字符串,来唯一标识司机和乘客的位置信息。再通过 Redis 的 GeoHash 算法,来获取乘客附加的所有司机信息。

GeoHash 算法的原理是将乘客的经纬度换算成地址编码字符串,表示在某个矩形区域,通过这个算法可以快速找到同一个区域的所有司机

它的实现用到了跳表数据结构,具体实现为:

将某个市区的一块范围作为 GeoHash 的 key,这个市区范围内所有的司机存储到一个跳表中,当乘客的地理位置出现在这个市区范围时,获取该范围内所有的司机信息。然后进一步筛选出最近的司机信息,进行派单。

4)体验优化

1. 距离算法

作为线上派单,通过距离运算来分配订单效果一定会比较差,因为 Redis 计算的是两点之间的空间距离,但司机必须沿道路行驶过来,在复杂的城市路况下,也许几十米的空间距离行驶十几分钟也未可知。

所以,后续需综合行驶距离(而非空间距离)、司机车头朝向以及上车点进行路径规划,来计算区域内每个司机到达乘客的距离和时间。

更进一步,如果区域内有多个乘客和司机,就要考虑所有人的等待时间,以此来优化用户体验,节省派单时间,提升盈利额。

2. 订单优先级

如果打车订单频繁取消,可根据司机或乘客行为进行判责。判责后给乘客和司机计算信誉分,并告知用户信誉分会影响乘客和司机的使用体验,且关联到派单的优先级。

司机接单优先级

综合考虑司机的信誉分,投诉次数,司机的接单数等等,来给不同信誉分的司机分配不同的订单优先级。

乘客派单优先级

根据乘客的打车时间段,打车距离,上车点等信息,做成用户画像,以合理安排司机,或者适当杀熟(bushi。

PS:目前有些不良打车平台就是这么做的 🐶  甚至之前爆出某打车平台,会根据不同的手机系统,进行差异收费。

4. 小结

4.1 网约车平台发展

目前,全球网约车市场已经达到了数千亿美元的规模,主要竞争者包括滴滴、Uber、Grab 等公司。在中国,滴滴作为最大的网约车平台已经占据了绝大部分市场份额。

网约车的核心商业逻辑比较简单,利益关联方主要为平台、司机、车辆、消费者。

平台分别对接司机、车辆【非必选项,有很多司机是带车上岗】和乘客,通过有效供需匹配赚取整个共享经济链省下的钱。

具体表现为:乘客和司机分别通过网约平台打车和接单,平台提供技术支持。乘客为打车服务付费,平台从交易金额中抽成(10%-30%不等)。

据全国网约车监管信息交互平台统计,截至 2023 年 2 月底,全国共有 303 家网约车平台公司取得网约车平台经营许可。

这些平台一部分是依靠高德打车、百度地图、美团打车为代表的网约车聚合平台;另一部分则是以滴滴出行、花小猪、T3 为代表的出行平台

4.2 网约车平台现状

随着出行的解封,网约车平台重现生机。

但由于部分网约车聚合平台的准入门槛太低,所以在过去一段时间里暴露出愈来愈多的问题。如车辆、司机合规率低,遇到安全事故,产生责任纠纷,乘客维权困难等等。

由于其特殊的模式,导致其与网约车运营商存在责任边界问题,一直游离在法律边缘。

但随着网约车聚合平台的监管不断落地,全国各地都出行了一定的监管条例。

比如某打车平台要求车辆将司机和乘客的沟通记录留档,除了司机与乘客的在线沟通记录必须保存以外,还需要一个语音电话或车载录音转换,留存一段时间备查。

有了这些人性化的监管条例和技术的不断创新,网约车平台或许会在未来的一段时间内,继续蓬勃发展。

后话

面试官:嗯,又专又红,全面发展!这小伙子不错,关注了~


作者:xin猿意码
来源:juejin.cn/post/7275211391102746684
收起阅读 »

何时使用Elasticsearch而不是MySql

MySQL 和 Elasticsearch 是两种不同的数据管理系统,它们各有优劣,适用于不同的场景。本文将从以下几个方面对它们进行比较和分析: 数据模型 查询语言 索引和搜索 分布式和高可用 性能和扩展性 使用场景 数据模型 MySQL 是一个关系型数据...
继续阅读 »

MySQL 和 Elasticsearch 是两种不同的数据管理系统,它们各有优劣,适用于不同的场景。本文将从以下几个方面对它们进行比较和分析:



  • 数据模型

  • 查询语言

  • 索引和搜索

  • 分布式和高可用

  • 性能和扩展性

  • 使用场景


数据模型


MySQL 是一个关系型数据库管理系统(RDBMS),它使用表(table)来存储结构化的数据,每个表由多个行(row)和列(column)组成,每个列有一个预定义的数据类型,例如整数、字符串、日期等。MySQL 支持主键、外键、约束、触发器等关系型数据库的特性,以保证数据的完整性和一致性。


Elasticsearch 是一个基于 Lucene 的搜索引擎,它使用文档(document)来存储半结构化或非结构化的数据,每个文档由多个字段(field)组成,每个字段可以有不同的数据类型,例如文本、数字、布尔、数组等。Elasticsearch 支持动态映射(dynamic mapping),可以根据数据自动推断字段的类型和索引方式。


MySQL 和 Elasticsearch 的数据模型有以下几点区别:



  • MySQL 的数据模型是严格的,需要事先定义好表的结构和约束,而 Elasticsearch 的数据模型是灵活的,可以随时添加或修改字段。

  • MySQL 的数据模型是二维的,每个表只有行和列两个维度,而 Elasticsearch 的数据模型是多维的,每个文档可以有嵌套的对象或数组。

  • MySQL 的数据模型是关系型的,可以通过连接(join)多个表来查询相关的数据,而 Elasticsearch 的数据模型是非关系型的,不支持连接操作,需要通过嵌套文档或父子文档来实现关联查询。



推荐博主开源的 H5 商城项目waynboot-mall,这是一套全部开源的微商城项目,包含三个项目:运营后台、H5 商城前台和服务端接口。实现了商城所需的首页展示、商品分类、商品详情、商品 sku、分词搜索、购物车、结算下单、支付宝/微信支付、收单评论以及完善的后台管理等一系列功能。 技术上基于最新得 Springboot3.0、jdk17,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中间件。分模块设计、简洁易维护,欢迎大家点个 star、关注博主。


github 地址:github.com/wayn111/way…



查询语言


MySQL 使用标准的 SQL 语言来查询和操作数据,SQL 语言是一种声明式的语言,可以通过简洁的语法来表达复杂的逻辑。SQL 语言支持多种查询类型,例如选择(select)、插入(insert)、更新(update)、删除(delete)、聚合(aggregate)、排序(order by)、分组(gr0up by)、过滤(where)、连接(join)等。


Elasticsearch 使用 JSON 格式的查询 DSL(Domain Specific Language)来查询和操作数据,查询 DSL 是一种基于 Lucene 查询语法的语言,可以通过嵌套的 JSON 对象来构建复杂的查询。查询 DSL 支持多种查询类型,例如全文检索(full-text search)、结构化检索(structured search)、地理位置检索(geo search)、度量检索(metric search)等。


MySQL 和 Elasticsearch 的查询语言有以下几点区别:



  • MySQL 的查询语言是通用的,可以用于任何关系型数据库系统,而 Elasticsearch 的查询语言是专用的,只能用于 Elasticsearch 系统。

  • MySQL 的查询语言是字符串形式的,需要拼接或转义特殊字符,而 Elasticsearch 的查询语言是 JSON 形式的,可以直接使用对象或数组表示。

  • MySQL 的查询语言是基于集合论和代数运算的,可以进行集合操作和数学运算,而 Elasticsearch 的查询语言是基于倒排索引和相关度评分的,可以进行全文匹配和相似度计算。


索引和搜索


MySQL 使用 B+树作为主要的索引结构,B+树是一种平衡多路搜索树,它可以有效地存储和检索有序的数据。MySQL 支持主键索引、唯一索引、普通索引、全文索引等多种索引类型,以加速不同类型的查询。MySQL 也支持外部存储引擎,例如 InnoDB、MyISAM、Memory 等,不同的存储引擎有不同的索引和锁机制。


Elasticsearch 使用倒排索引作为主要的索引结构,倒排索引是一种将文档中的词和文档的映射关系存储的数据结构,它可以有效地支持全文检索。Elasticsearch 支持多种分词器(analyzer)和分词过滤器(token filter),以对不同语言和场景的文本进行分词和处理。Elasticsearch 也支持多种搜索类型,例如布尔搜索(boolean search)、短语搜索(phrase search)、模糊搜索(fuzzy search)、通配符搜索(wildcard search)等,以实现不同精度和召回率的检索。


MySQL 和 Elasticsearch 的索引和搜索有以下几点区别:



  • MySQL 的索引是基于数据的值的,可以精确地定位数据的位置,而 Elasticsearch 的索引是基于数据的内容的,可以近似地匹配数据的含义。

  • MySQL 的索引是辅助的,需要手动创建和维护,而 Elasticsearch 的索引是主要的,自动创建和更新。

  • MySQL 的索引是局部的,只针对单个表或列,而 Elasticsearch 的索引是全局的,涵盖所有文档和字段。


分布式和高可用


MySQL 是一个单机数据库系统,它只能运行在一台服务器上,如果服务器出现故障或负载过高,就会影响数据库的可用性和性能。为了解决这个问题,MySQL 提供了多种复制(replication)和集群(cluster)方案,例如主从复制(master-slave replication)、双主复制(master-master replication)、MySQL Cluster、MySQL Fabric 等,以实现数据的冗余和负载均衡。


Elasticsearch 是一个分布式数据库系统,它可以运行在多台服务器上,形成一个集群(cluster)。每个集群由多个节点(node)组成,每个节点可以承担不同的角色,例如主节点(master node)、数据节点(data node)、协调节点(coordinating node)等。每个节点可以存储多个索引(index),每个索引可以划分为多个分片(shard),每个分片可以有多个副本(replica)。Elasticsearch 通过一致性哈希算法(consistent hashing algorithm)来分配分片到不同的节点上,并通过心跳检测(heartbeat check)来监控节点的状态。如果某个节点出现故障或加入集群,Elasticsearch 会自动进行分片的重新分配和平衡。


MySQL 和 Elasticsearch 的分布式和高可用有以下几点区别:



  • MySQL 的分布式和高可用是可选的,需要额外配置和管理,而 Elasticsearch 的分布式和高可用是内置的,无需额外操作。

  • MySQL 的分布式和高可用是基于复制或共享存储的,需要保证数据一致性或可用性之间的权衡,而 Elasticsearch 的分布式和高可用是基于分片和副本的,可以根据需求调整数据冗余度或容错能力。

  • MySQL 的分布式和高可用是静态的,需要手动扩展或缩容集群规模,而 Elasticsearch 的分布式和高可用是动态的,可以自动适应集群变化。


性能和扩展性


MySQL 是一个面向事务(transaction)的数据库系统,它支持 ACID 特性(原子性、一致性、隔离性、持久性),以保证数据操作的正确性和完整性。MySQL 使用锁机制来实现事务隔离级别(isolation level),不同的隔离级别有不同的并发性能和一致性保证。MySQL 也使用缓冲池(buffer pool)来缓存数据和索引,以提高查询效率。MySQL 的性能主要取决于硬件资源、存储引擎、索引设计、查询优化等因素。


Elasticsearch 是一个面向搜索(search)的数据库系统,它支持近实时(near real-time)的索引和查询,以保证数据操作的及时性和灵活性。Elasticsearch 使用分片和副本来实现数据的分布式存储和并行处理,不同的分片数和副本数有不同的写入吞吐量和读取延迟。Elasticsearch 也使用缓存(cache)和内存映射文件(memory-mapped file)来加速数据和索引的访问,以提高搜索效率。Elasticsearch 的性能主要取决于集群规模、分片策略、文档结构、查询复杂度等因素。


MySQL 和 Elasticsearch 的性能和扩展性有以下几点区别:



  • MySQL 的性能和扩展性是有限的,它受到单机资源、锁竞争、复制延迟等因素的限制,而 Elasticsearch 的性能和扩展性是无限的,它可以通过增加节点、分片、副本等方式来水平扩展集群。

  • MySQL 的性能和扩展性是以牺牲搜索能力为代价的,它不能支持复杂的全文检索和相关度评分,而 Elasticsearch 的性能和扩展性是以牺牲事务能力为代价的,它不能保证数据操作的原子性和一致性。

  • MySQL 的性能和扩展性是以提高写入速度为目标的,它优化了数据插入和更新的效率,而 Elasticsearch 的性能和扩展性是以提高读取速度为目标的,它优化了数据检索和分析的效率。


使用场景


MySQL 和 Elasticsearch 适用于不同的使用场景,根据不同的业务需求,可以选择合适的数据库系统或组合使用两者。以下是一些常见的使用场景:



  • 如果需要存储结构化或半结构化的数据,并且需要保证数据操作的正确性和完整性,可以选择 MySQL 作为主要数据库系统。例如,电商网站、社交网络、博客平台等。

  • 如果需要存储非结构化或多样化的数据,并且需要支持复杂的全文检索和相关度评分,可以选择 Elasticsearch 作为主要数据库系统。例如搜索引擎、日志分析、推荐系统等。

  • 如果需要存储和分析大量的时序数据,并且需要支持实时的聚合和可视化,可以选择Elasticsearch作为主要数据库系统。例如,物联网、监控系统、金融市场等。

  • 如果需要同时满足上述两种需求,并且可以容忍一定程度的数据不一致或延迟,可以将 MySQL 作为主数据库系统,并将部分数据同步到 Elasticsearch 作为辅助数据库系统。例如新闻网站、电影网站、招聘网站等。


自此本文讲解内容到此结束,感谢您的阅读,希望本文对您有所帮助。


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

无插件实现一个好看的甘特图

web
效果 预览地址 code.juejin.cn/pen/7272286… 前言 刚好看到这么一个东西,甘特图,然后又发现好像echarts 里面没有这个图形渲染,也去找了一下插件,都不符合我的要求,然后自己就想着看能不能实现一个,然后说干就干,过程还挺复杂的,...
继续阅读 »

效果


预览地址 code.juejin.cn/pen/7272286…


QQ录屏20230828153807 00_00_00-00_00_30.gif


前言


刚好看到这么一个东西,甘特图,然后又发现好像echarts 里面没有这个图形渲染,也去找了一下插件,都不符合我的要求,然后自己就想着看能不能实现一个,然后说干就干,过程还挺复杂的,不过一想清楚了,也就还行。


逻辑


刚开始看着这个图,想到肯定是用表格来实现的,后面开始渲染的时候,发现是我想简单了,这表格是渲染不出来的,也或者是我技术还没够,反正我找了很多相关的插件,都不是用表格去实现的,后面就改变了一些思路,用div去渲染,然后去定位,这么一想,发现事情就简单了许多。


为什么不用表格实现


每点击一个视图切换,表头就需要重新渲染,表头有索引,名称,负责人,如果是表格实现的话,就是如下代码,一看就知道了id=“tableYear”的不好渲染,因为上面有可能是年,也有可能是月,所以肯定是有循环的,但如果一循环,它们没有共同的父容器,怎么渲染呢,也想过表格里面套表格,但那样,样式又实现不了我要的效果。


 <table>
<thead>
<tr>
<th rowspan="2">id</th>
<th rowspan="2">任务名称</th>
<th rowspan="2">负责人</th>
<th colspan="4" id="tableYear">2023-8</th>
</tr>
<tr id="tableDay">
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
</tr>
</thead>
</table>

第一个难点


日期渲染,因为我这表头是动态的,所以要复杂一些,有天数,月数,季度和年度显示的


image.png
点击日视图,就是按天去显示任务,月视图就是按月去显示任务,季视图和年视图同理,
比如说日视图,
先获取当前日期,年月日,然后是想渲染前后几年的


var currentDate=new Date;//当前日期
var currentYear = currentDate.getFullYear();//当前年份

var yearRange = 1; // 前后1年
var startDate = currentYear - yearRange;//前1年
var endDate = currentYear + yearRange;//后1年
var today = currentDate.getDate(); // 获取今天是几号
var currentYear = currentDate.getFullYear();//年
var currentMonth = currentDate.getMonth();//月
var displayedYears = {}; // 用于记录已显示的年份

开始渲染第一排是年月,一年的月份是固定的,所以都是可以写死的,这里有要注意一下的是,就是年月的宽度,因为要根据当月有多少天去计算宽度,所以我要知道,这一年这一月是多少天然后乘以40 下面是相关代码


for (var year = startDate; year <= endDate; year++) {
for (var month = 0; month < 12; month++) {
var lastDay = new Date(year, month + 1, 0).getDate();
var monthElement = $("<p>" + (month + 1) + "月: </p>"); // 创建表示月份的 <p> 元素

for (var day = 1; day <= lastDay; day++) {
dateRange.push(new Date(year, month, day));
}
// 在 .tableYear 中添加年份和月份信息
var yearMonthStr = year + "-" + (month + 1 < 10 ? "0" : "") + (month + 1);
var width = (lastDay * 40)-1 + "px"; // 计算宽度
$(".tableYear").append($("<p class='Gantt-table_header' style='width: " + width + "'>" + yearMonthStr + "</p>"));
}
}

渲染完成,年月以后就是日,我这里也做了一些小的显示,比如说周末深颜色表示,今天也深颜色表示,并且视图要显示在当前,而不是在2022-1-1号这里。


天数渲染


for (var i = 0; i < dateRange.length; i++) {
var currentDate = dateRange[i];
var dayNumber = currentDate.getDay(); // 获取星期几 (0 = 星期日, 1 = 星期一, ...)
var isWeekend = dayNumber === 0 || dayNumber === 6;
var dayText = currentDate.getDate();//获取日
var dayYear = currentDate.getFullYear(); // 获取年份
var dayMonth = currentDate.getMonth(); // 获取月份(注意:月份是从 0 到 11,0 表示一月,11 表示十二月)
var tableCell = $("<p>" + dayText + "</p>");
if (isWeekend) {
tableCell = $("<p class='Gantt-table_weekend'>" + dayText + "</p>");
}
//获取当前时间的年月日,与循环出来的年月日进行循环匹配,
if (dayText === today && dayYear === currentYear && dayMonth === currentMonth ) {
tableCell.addClass("today");
}
$(".tableDay").append(tableCell);
}

视口显示代码


 // 将视口滚动到今天所在的位置
var todayElement = $(".today");
if (todayElement.length > 0) {
var viewportHeight = window.innerHeight || document.documentElement.clientHeight;
var elementHeight = todayElement[0].clientHeight;

var offset = (viewportHeight - elementHeight) / 2;
//平滑滚动参数 smooth auto不滚动
todayElement[0].scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
}

第二个难点


image.png
如果不用表格,怎么去实现这个网格呢,刚开始是想着去渲染,表格对应有多少天,就渲染有多少个p标签,但一想这怎么行,如果任务很多,一条任务就要渲染上千个p标签,太浪费资源了,任务一多,那不直接挂壁。
后面在css里面找到了解决办法


background: repeating-linear-gradient(to right, rgb(221, 221, 221), rgb(221, 221, 221
1px, transparent 1px, transparent 40px) 0% 0% / 40px 100%;

ChatGPT是这样解释的


image.png
然后完美解决问题,不用渲染这么多,一个任务一个div就可以了。


第三个难点


甘特图的核心,那个柱状图的东西。


image.png


柱状图渲染,比如说我提了一个任务,是2023年8月20号开始的,然后到2023年8月25号要完成,那这样就只有五天时间,那渲染肯定是从2023年8月20号开始的,然后到8月25号结束。


我的思路是这样的,显示的宽度是五天,然后一天的宽度是40px,那么这个任务的总宽度就是200px,然后定位,获取到这个任务开始的时候,我这里显示是前后一年的也就是2022-1-1号开始的,然后相减,知道中间相差了多少天,然后再乘以40,得到left的距离,就实现了这个效果。后面的描述也是类似的效果,这样甘特图差不多就完成了。下面是日期相减代码


 function getOffsetDays(startDate, endDate){
var startDateArr = startDate.split("-");
var checkStartDate = new Date();
checkStartDate.setFullYear(startDateArr[0], startDateArr[1], startDateArr[2]);
var endDateArr = endDate.split("-");
var checkEndDate = new Date();
checkEndDate.setFullYear(endDateArr[0], endDateArr[1], endDateArr[2]);
var days = (checkEndDate.getTime() - checkStartDate.getTime())/ 3600000 / 24;
if(startDateArr[0]!=endDateArr[0]){
flag = true;
}
return days;
}

结语


虽然三言两语就讲解完了,但其中还是有很多逻辑的问题,我只是讲解了一下我的一个实现的思路,居体代码请看这


以上就是本篇文章的全部内容,希望对大家的学习有所帮助,以上就是关于无插件实现一个好看的甘特图的详细介绍,如果有什么问题,也希望各位大佬们能提出宝贵的意见。当然也有很多需要优化的地方,我这只是给了一个思路,你们可以去实现很多的功能。


作者:流口水的兔子
来源:juejin.cn/post/7272174836336132132
收起阅读 »

2024年,为啥我不建议应届生再去互联网?

最近快过年了,和还留在成都的一些研究生同学吃了顿饭,其中博士姐姐因为今年刚刚毕业,所以在饭局里面还跟我们谈了一下今年的师弟师妹们的去向。 她说今年虽然就业挺难的,但是师弟师妹们的工作还都挺好的,有去成飞的,有去选调的还有去了一些大国企研究所的。然后我就问今年没...
继续阅读 »

最近快过年了,和还留在成都的一些研究生同学吃了顿饭,其中博士姐姐因为今年刚刚毕业,所以在饭局里面还跟我们谈了一下今年的师弟师妹们的去向。


她说今年虽然就业挺难的,但是师弟师妹们的工作还都挺好的,有去成飞的,有去选调的还有去了一些大国企研究所的。然后我就问今年没有去互联网的吗?她说有哇,有学弟去了美团,钱还开得挺多的,有学弟去了个独角兽做算法,但是也就这两个人去了互联网相关的了。


其实听到这个我还是蛮感慨的,在我毕业的时候互联网还是如日中天,大多数计算机毕业的孩子首选的就是去互联网狠狠的大赚一笔。短短3年间,去互联网的应届生就屈指可数了,一方面是这两年互联网大厂缩招严重,进互联网没有我们当年那么容易。另一方面是,在大环境不容乐观的今天,以及互联网增长见顶的背景下,互联网的工作其实已经不是应届生的首选工作了。


实际上,即使在今年你能过千军万马杀出重围拿到互联网的offer,作为一个过来人我也不是很建议你再去趟互联网这趟浑水。因为,作为一个新人在一个注定下行的行业当中,你可能搭上的不是通往财富自由的快车道,很快你需要考虑的可能就是你还能不能保住你手头的这份工作的问题。


说一个老生常谈的事情,互联网的增长确实见底了,阿里、腾讯、网易的股票最近狂跌,阿里都跌回2014年了,只有抖音还依然坚挺一些但是依然看不到未来成长的空间。从2014年到2024年,正好十年的时间,互联网员工们加班加点996,熬夜爆肝的奋斗,最终的结果尽然是回到了原点。


其实,这个事情也并不奇怪,这些互联网大厂只是坐在电梯里面的人,他们都觉得自己能够取得成功是因为自己在电梯里面做俯卧撑。实际上,跟你在电梯上做啥没有关系,你之所以能够成功只是因为你恰好赶上了这班电梯而已,跟你在里面睡觉还是瞎折腾关系都不大。如今风停了,电梯开始往下走了,作为个体你非要去搭上这个末班车并且期待在踩在早就已经上电梯的这群人的头上的话,那么我只能跟你说,祝你好运了。


其实,作为一名应届生的时候我对职场也没有清醒的认识,以为职场上的同事和学校的同学一样大家和和气气不争不抢的。但是,正是抱着这样的心态我入职了互联网之后的短短一年时间内,才深刻感受到了社会的毒打和职场真实的样貌。所以,我不知道在学校的应届生们有没有做好准备在互联网面对全方位的竞争,这种竞争不仅仅是技术,不仅仅是加班,更是向上管理和领导处理好关系。和国企、外企、体制内不一样,互联网的大多数公司是有强制末尾淘汰的,有些公司甚至连新人保护期都没有,那么你觉得你作为一个活蹦乱跳的应届生,这个名额是老油条扛呢还是你呢?


另外,以前的人扎堆朝互联网冲是因为真的有财富自由的机会的,那时候啥app都没有,张小龙找几个应届生关小黑屋都能写出未来的国民级app微信。16年的字节也还是个小公司,那时候往互联网里面冲的话搞不好真的可以一年能够赚到别人一辈子赚不到的钱,所以去互联网真是一点儿问题都没有。你那时候不去互联网我都会拿着鞭子抽你,劝你上进一点儿!但是都2024年了,市场永远比个人知道一个方向的未来,还是那句话你想创业互联网都拉不到风投的年代,你还能奢望能够实现财富自由吗?


The End

其实作为一名程序员还是挺享受写有趣代码的过程的,也希望做一点儿东西能够被大家认可,所以我劝退互联网但是并不是劝退计算机。


即使是Chatgpt大行其道的今天,我也不认为未来某一天机器能够真正意义上取代程序员,要取代也是从另外一个维度上取代,比如说根据需求直接生成机器码而不是生成代码的这种形式。虽然互联网是一片红海,但是像新的技术VR、物联网、工业软件、芯片和智能机器人等行业,在我们国家还是蕴含着无限机会的。但是,我并不认为去到我上面所说的这些行业工资收入上能够超过现在的互联网大厂给出的工资,我的意思真的有想法的人可以尝试在这些领域去找到自己的一席之地,尤其是在校学生。


你去卷一个注定下山的行业无论它钱给多少都是毫无意义的,因为入职就可能就是你职业生涯的巅峰。相比起来,我觉得华子未来比这些靠着广告赚钱的公司都更有前景,因为是真的有一些核心技术在的。


所以,选择一个还没有走过巅峰的行业,提前布局才是更有未来的职业选择。


作者:浣熊say
来源:juejin.cn/post/7327447632111419443
收起阅读 »

每天都很煎熬,领导派的活太难,真的想跑路了

人在江湖身不由己,无论是领导的亲信还是团队的边缘,都可能遇到这种情况———不得不干一件特别难以推进的事情,茫然无措,不知如何推进。每天陷入焦虑和自我怀疑中…… 这种事情一般有一些共同特点。 结果和目标极其模糊。 需要协调其他团队干活但是对方很不配合。 领导也...
继续阅读 »

人在江湖身不由己,无论是领导的亲信还是团队的边缘,都可能遇到这种情况———不得不干一件特别难以推进的事情,茫然无措,不知如何推进。每天陷入焦虑和自我怀疑中……


这种事情一般有一些共同特点。



  1. 结果和目标极其模糊。

  2. 需要协调其他团队干活但是对方很不配合。

  3. 领导也不知道怎么干


领导往往是拍脑袋提想法,他们也不知道具体如何执行。反过来说,如果领导明确知道怎么做,能亲自指导技术方案、亲自解决关键问题,那问题就好办了,只要跟着领导冲锋陷阵就好了,就不存在烦恼了。


遇到这种棘手的事情,如果自己被夹在中间,真的非常难受啊!


image.png


今天重点聊聊领导拍脑袋、心血来潮想做的那些大事 如果让你摊上了,你该怎么做!


1、提高警惕!逆风局翻盘难!


互联网行业目前处于稳定发展期,很少会出现突然迅猛增长的业务,也很少有公司能够迅速崛起。这是整个行业的大背景。因此,我们应该对任何不确定或模糊的目标表示怀疑,因为它们更有可能成为我们的绊脚石,而不是机遇。即使在王者荣耀这样的游戏里,要逆风翻盘也很困难,更何况在工作中呢。


当领导提出一个棘手的问题时,我们应立刻警惕,这可能不是一个好的机会,而是一个陷阱。我们不应该被领导画的饼所迷惑,而是要冷静客观地思考。哪些目标和结果是难以达到的,这些目标和结果就是领导给我们画的大饼!


领导给出任务后,我们就要努力完成。通常情况下,他们会给我们一大堆任务,需要我们确认各种事情。简而言之,他们只是有个想法,而调研报告和具体实施方案就需要我们去做。


如果领导是一位优秀而谦虚的人,通常在我们完成调研后,会根据调研结果来判断这个想法是否可行。如果不可行,他们会立即放弃,而我们也不会有什么损失。


但是,一旦领导有了一个想法,肯定是希望我们来完成的,即便我们在调研后认为不可行,大多数情况下,他们也不会接受我们的结论!因此,我们的调研工作必须极度认真,如果我们认为不可行,就要清楚地阐述不可行的理由,要非常充分。


这是我们第一次逃离的机会,我们必须重视这次机会,并抓住机会。


2、积极想办法退出


对于这种模糊不靠谱的事情,能避开就避开,不要犹豫。因为这种事情往往占用大量时间,但很难取得显著的成果。对于这种时间周期长、收益低、风险高的事情,最好保持距离。


你还需要忍受巨大的机会成本


在你长期投入这种事情的过程中,如果团队接到更好的项目和需求,那肯定不会考虑你。你只能羡慕别人的机会。


因此,如果可以撤退的话,最好离开这种费力不讨好的活远远的!



子曰:吾日三省吾身,这事能不能不干,这事能不能晚点干,这事能不能推给别人干。



如何摆脱这件事呢?


2.1 借助更高优事情插入,及时抽身


例如,突然出现了一件更为紧急的事情,这就是脱身的机会。与此同时,我们也可以为领导保留一些颜面,因为随着工作的进展,领导也会意识到这件事情的意义不大,很难取得实质成果。但是,如果我们一开始就表示不再继续做这件事,那么领导可能会觉得自己的判断出了问题,失去面子。所以,我们可以寻找一个时机,给领导下台阶。


或者,突然出现了一个需求,与我们目前的重构方案存在冲突。这是一个很好的借口。重构方案和未来产品规划产生了冲突,我们应优先满足产品规划和需求。重构方案需要后续再次评估,以找到更好的解决方案。


2.2 自己规划更重要的事情,并说服领导


当你对系统优化没有想法时,不要怪领导给你找事干。


如果领导有一个系统重构的计划和目标需要你执行,但是你不想干,或者你认为这件事不靠谱。那么你可以思考一个更可行、更有效、更能带来收益的重构方案,并与领导进行汇报。如果领导认为你的计划更加重要且更具可行性,那他可能会放弃自己的想法。


这就是主动转被动的策略。这时你的技术能力将接受考验,你能提出一个更优秀的系统重构方向吗?你能提出一个更佳的系统建设方向吗?


2.3 选择更好的时机做这件事


如果领导让你去做技术重构,而这件事的优先级不如产品需求高,上下游团队也不愿意配合你,而且领导给你的人力和时间资源也不够充裕,你应该怎么办呢?可以考虑与产品需求一起进行技术重构。也就是说,边开发需求,边进行技术重构。这样做有以下好处:可以借助于产品的力量,很自然地协调上下游团队与你一同进行重构。同时也能推动测试同事进行更全面的测试。在资源上遇到的问题,也可以让产品帮助解决。


所以,技术重构最好和产品需求结合起来进行。如果技术重构规模庞大,记得一定要分阶段进行,避免因技术重构导致产品需求延期哦。


2.4 坦诚自己能力不足,暂时无法完成这件事,以后再干行不行


可以考虑向领导坦然承认自己的能力还不足以立即执行这项任务,因此提出先缓一缓,先熟悉一下这个系统的建议。我可以多做一些需求,以此来熟悉系统,然后再进行重构。


我曾经接手一个系统,领导分配给我一个非常复杂的技术重构任务。当时我并没有足够聪明,没有拒绝,而是勉强去做,结果非常不理想,还导致了线上P0级别的事故发生!


新领导告诉我,"先想清楚如何实施,再去行动。盲目地勉强上阵只会带来糟糕的结果。当你对一个系统不熟悉的时候,绝对不能尝试对其进行重构。"


先熟悉系统至少三个月到半年。再谈重构系统!


2.5 拖字诀,拖到领导不想干这件事!


拖到领导不想干的时候,就万事大吉了。


注意这是最消极的策略,运气好,拖着拖着就不用干了。但如果运气不佳,拖延只会让任务在时间上更加紧迫,而且还会招致领导的不满。


使用拖延策略很可能得罪领导,给他们留下不良的印象。


因此,在使用此策略时应谨慎行事!


2.6 退出时毫不犹豫,不要惋惜沉默成本


如果有撤退的机会,一定不要犹豫,不要为自己付出的投入感到遗憾,不要勉强继续前进,也不必试图得到明确的结果。错误的决策只会带来错误的结果。一定要及时止损。


因为我曾经犯过类似的错误,本来有机会撤退,但是考虑到已经付出了很多,想要坚持下去。幸好有一位同事更加冷静,及时制止了我。事后我反思,庆幸及时撤退,否则后果真的不敢想象啊。


3、适当焦虑


每个人都喜欢做确定性的事情,面对不确定的事情每个人都会感到焦虑。为此可能你每天都很焦虑,甚至开始对工作和与领导见面感到厌恶。之所以这个事情让你感到不适,是因为它要求你跳出舒适区。


但是,请记住,适度的焦虑是正常的。告诉自己,这并没有什么大不了的。即使做得不好,顶多被领导责备一下而已。不值得让生活充满焦虑,最重要的是保持身心健康和快乐。


当你沉浸在焦虑中时,可能会对工作和领导感到厌烦。这样一来,你可能会对和领导沟通感到反感。这种情况是可怕的,因为你需要不断和领导沟通才能了解他真正的意图。如果失去了沟通,这个事情肯定不会有好的结果。


因此,一定要保持适度的焦虑。


3.1 沟通放在第一位


面对模糊的目标和结果,你需要反复和领导沟通,逐步确认他的意图。或者在沟通中,让领导他自己逐渐确定的自己的意图。在这方面有几个技巧~


3.2 直接去工位找他


如果在线上沟通,领导回复可能慢,可能沟通不通畅。单独约会议沟通,往往领导比较忙,没空参加。所以有问题可以直接去工位找他,随时找他沟通问题。提高效率


3.3 没听懂的话让领导说清楚


平常时候领导没说清楚,无所谓,影响不大。例如普通的产品需求,领导说的不清楚没关系,找产品问清楚就行。


面对目标不明确的项目,领导的意图就十分重要。因为你除了问领导,问其他人没用。领导就是需求的提出方,你不问领导你问谁。 在这种情况下,没听懂的事情必须要多问一嘴。把领导模糊的话问清楚。


不要怕啰嗦,也不要自己瞎揣摩领导的意图。每个人的想法都不同,瞎猜没用。


3.4 放低姿态


如果领导和你说这件事不用干了,你肯定拍手叫好。很多烦恼,领导一句话,就能帮你摆平!


放低姿态就是沟通时候,该叫苦叫苦,该求助就求助,别把自己当成超人,领导提啥要求都不打折扣的行为完全没必要。可以和领导叫叫苦,可以活跃气氛,让领导多给自己点资源,包括人和时间。


说白了,就是和 领导 “撒娇”。这方面女生比较有优势,男生可能拉不下脸。之前的公司,我真见识过,事情太多,干不完,希望领导给加人,但被领导拒绝。 然后她就哭了,最后还真管用!是个女同事。


男孩子想想其他办法撒娇吧。评论区留下你们的办法!


3.5 维护几个和领导的日常话题


平常如果有机会和领导闲聊天,一定不要社交恐惧啊! 闲聊天很能提升双方的信任关系,可以多想想几个话题。例如车、孩子、周末干啥、去哪旅游了等等。


提升了信任关系,容易在工作中和领导更加融洽。说白了就是等你需要帮忙的时候,领导会多卖你人情!


4 积极想替代方案————当领导提的想法不合理时


积极寻求替代方案,不要被领导的思路局限!引导众人朝着正确的方向前进!


不同领导的水平和对技术问题的认知不尽相同,他们注重整体大局,而员工更注重细节。这种差异导致了宏观和微观层面之间存在信息不对称,再加上个人经验、路径依赖导致的个人偏见,使得领导的想法不一定正确,也不一定能够顺利实施。


就我个人的经历来说,领导要求我进行一次技术重构。由于我对这个项目还不够熟悉,所以我完全按照领导的方案去操作,没有怀疑过。事后回顾,发现这个方案过于繁重,其实只需要调整前端接口就能解决问题,但最终我们却对底层数据库存储、业务代码和接口交互方式进行了全面改变。


最终收益并不高,反而导致了一个严重的故障。既没有获得功劳,也没有得到应有的认可。


事后反思,我意识到我不应该盲目按照领导的方案去执行,而是应该怀着质疑和批判的态度去思考他的方案。多寻求几个备选方案,进行横向比较,找到成本最低、实施最简单的方案。


4.1 汇报材料高大上,实现方案短平快


私底下,可以对老板坦诚这件事,就是没什么搞头。但是对外文章要写得高大上!


技术方案要高大上,实现方案要短平快。


面对不确定的目标、面对不好完成的任务,要适当吹牛逼和画饼。汇报文档可以和实现方案有出入。


模糊的目标,往往难以执行和完成,技术方案越复杂,越容易出问题。本来就没什么收益,还引出一堆线上问题,只能当项目失败的背锅侠,得不偿失。


一定要想办法,把实现方案做的简单。这样有3个好处;



  1. 降低实现难度,减少上线风险。

  2. 缩短开发周期,尽快摆脱这个项目。

  3. 把更多的时间放在汇报材料上。代码没人看!!!


程序员一般情况下习惯于实话实说,如果说假话,一定是被人逼得。


不会写文档?# 写文档不用发愁,1000个互联网常用词汇送给你


不会写技术方案?# 不会画图? 17 张图教你写好技术方案!


5、申请专门的团队攻克难关!


例如重构系统涉及到上下游系统,一个人搞不定的!要向领导寻求帮助,让上下游同事一起干这件事。


让熟悉系统的人跟自己一起做,拉更多的人入伙!多个人一起承担重任! 这种组织上的安排,只能由领导出面解决。


假如别的同事经常打扰你,总让你确认这件事,确认那件事,总让你帮忙梳理文档,你愿意配合吗? 每个人都很忙,没人愿意长期给你干活。


让领导帮忙成立重构小组!然后你可以给每个人都分派任务,比自己独自硬扛,成功概率大很多。


虽然重构的目标不明确,但你可以尝试明确每个人的责任,设置短期的里程碑。例如前三天梳理整理资料,每天开早会, push大家干活。(这样很招人恨!没办法,领导卷的)


5.1 寻求合作的最大公约数


重大项目往往需要多个团队同时配合,即便你申请了专门的小组跟进这件事,但是别人可能出工不出力!


他们不配合的原因在于:不光没有收益,付出还很多。成本和收益不对等,人家不愿意很正常。保持平常心!不要带着脾气看待这件事!


略微想一下就明白,既然你觉得这件事风险高、收益低,难道其他人看不出来吗?


作为项目的负责人推动事情更加困难。当别人不配合时,除了把矛盾上升到上层领导外,还有哪些更好的办法呢?



  1. 平时多和相关同学打好关系。平时奶茶咖啡多送点,吃别人嘴短,到时候求人时候很管事的。

  2. 调动对方的积极性!例如重构系统需要人家配合,但是这件事对他们又没有收益。可以和他们一起头脑风暴,想一下对方系统可以做哪些重构。当双方一拍即合,各取所需时,才能合作融洽。双赢的合作,才能顺利。

  3. 多作妥协。上下游系统的交互边界很难划分,如果交互存在争议,可以适当让步,换取对方的积极合作。完成胜于完美!


总之,涉及多个团队合作时,除了依靠上层领导的强硬干预之外,还要想一些合作共赢的方案!


6、争取更多的资源支持


没有完不成的事情,只要资源充裕,任何事情都是有希望的。当你面临棘手的问题时,除了打起12分的精气神,还要多想想和领导申请资源啊!


最重要的包括人力资源、时间资源。如果空口白牙就要人,可能比较困难。


这需要你在调研阶段深入思考,预想到系统的挑战点,把任务细分,越细越好,然后拿着排期表找领导,要人、要时间。


如果人和时间都不给!可以多试几次,软磨硬泡也是好办法!


此外还有别的办法,例如 ”偷工减料"。你可以和领导沟通,方案中哪些内容不重要,是否可以砍掉。”既然你不给人,砍掉不重要的部分,减少工作量,总可以吧"


除此之外,还可以考虑分期做。信用卡可以分期付款,技术重构当然也可以分期优化!


7、能分期就分期


对于技术重构类工作,一定要想办法分期重构,不要一次性只求大而全!



  1. 越复杂的技术方案越容易出问题!

  2. 越长的开发周期越容易出问题!

  3. 越想一次性完成,越容易忙中出错!


分期的好处自不必说,在设计方案时一定要想如何分期完成。


如果对一个系统不熟悉,建议分期方案 先易后难!先做简单的,逐渐地你对系统会有更深入的理解!


如果对一个系统很熟悉,可以考虑先难后易。先把最困难的完成!后面会轻松很多!


但是我还是建议庞大的重构工作,先易后难!先做简单的,拖着拖着,也许就不需要重构了呢!


8、即便没有功劳但是要收获苦劳


当一件事干成很难的时候,要想办法把损失降到最低。一定要想着先保护自己!别逞能!


工作几年的朋友应该知道,不是所有的项目都能成功!甚至大部分项目在商业上是失败的!做不成一件事很正常!


如果一件事很难办成,功劳就不要想了。但是可以赚一份苦劳。


这要求你能把自己的困难说给领导,例如其他团队不配合!你可以一直和领导反馈,并寻求领导的帮助。


日常工作的内容也要有文档留存。工作以周报形式单独和领导汇报!要让领导知道你每周的进展,向领导传递一个事实:“每一周你都努力地在做事,并且也都及时汇报了,日后干不成,可别只怪我一人啊!”


接到一个烫手山芋,处理起来很难~ 斗智斗勇,所以能躲开还是躲开啊!


9、转变观念:放弃责任心,领导关注的内容重点完成


出于责任心的角度,我们可能认为领导提出的方案并不正确,甚至认为领导给自己派的工作完全没有意义。


你可能认为领导的Idea 不切合实际!


出于责任心,你有你的想法,你有你的原则!你认为系统这样重构更适合!但那又怎样,除非你有足够的理由说服领导,否则改变不了什么。


站在更高的位置能看的更远,一般领导都会争取团队利益最大化。虽然看起来不切实际,但是努力拼一拼,也许能给团队带来更大的利益。这可能是领导的想法!说白了,就是领导想让团队多去冲锋陷阵,多把一些不可能变成可能!


和领导保持节奏,领导更关注哪件事,就尽力把这件事做好! 放弃自己所谓的“责任心”。


10、挑战、机遇、风险并存。


在互联网稳定期,各行各业都在内卷,公司内部更是在内卷!


在没有巨大增量的团队和公司里,靠内卷出成绩是很困难的事情。有时候真的很绝望,每一分钟都想躺平 。


像这种目标不明确、执行方案不明确、结果不明确、需要协调其他团队干活的难事越来越多!风险高、低收益的事情谁都不想干!


但是一旦能做成,对于个人也是极大地锻炼。所以大家不要一味地悲观,遇到这种棘手的事情,多和领导沟通,多想想更优的解决方案。也许能走出一条捷径,取得极大的成果~


作者:五阳
来源:juejin.cn/post/7290469741867565092
收起阅读 »

Easy-Es:像mybatis-plus一样,轻松操作ES

0. 引言 es的java客户端不太友好的语法一直饱受诟病,书写一个查询语句可能需要书写一大串的代码,如果能像mybatis--plus一样,支持比较灵活方便的语句生成器那就好了。 于是为elasticsearch而生的ORM框架Easy-Es诞生了,使用及其...
继续阅读 »

0. 引言


es的java客户端不太友好的语法一直饱受诟病,书写一个查询语句可能需要书写一大串的代码,如果能像mybatis--plus一样,支持比较灵活方便的语句生成器那就好了。


于是为elasticsearch而生的ORM框架Easy-Es诞生了,使用及其方便快捷,今天我们就一起来学习easy-es,对比看看原生java-client方便之处在哪儿。


1. Easy-Es简介


Easy-Es是以elasticsearch官方提供的RestHighLevelClient为基础,而开发的一款针对es的ORM框架,类似于es版的mybatis-plus,可以让开发者无需掌握es复杂的DSL语句,只要会mysql语法即可使用es,快速实现es客户端语法


官方文档:http://www.easy-es.cn/
在这里插入图片描述


2. Easy-Es使用


1、引入依赖


<!-- 引入easy-es最新版本的依赖-->
<dependency>
<groupId>org.dromara.easy-es</groupId>
<artifactId>easy-es-boot-starter</artifactId>
<version>2.0.0-beta3</version>
</dependency>

<!-- 排除springboot中内置的es依赖,以防和easy-es中的依赖冲突-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</exclusion>
<exclusion>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.14.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.14.0</version>
</dependency>

2、添加配置项,这里只配置了几个基本的配置项,更多配置可参考官网文档:easy-es 配置介绍


easy-es: 
# es地址、账号密码
address: 192.168.244.11:9200
username: elastic
password: elastic

3、在启动类中添加es mapper文件的扫描路径


@EsMapperScan("com.example.easyesdemo.mapper")

在这里插入图片描述


4、创建实体类,通过@IndexName注解申明索引名称及分片数, @IndexField注解申明字段名、数据类型、分词器等,更多介绍参考官方文档:essy-es 注解介绍


@IndexName(value = "user_easy_es")
@Data
public class UserEasyEs {

@IndexId(type = IdType.CUSTOMIZE)
private Long id;

private String name;

private Integer age;

private Integer sex;

@IndexField(fieldType = FieldType.TEXT, analyzer = Analyzer.IK_SMART, searchAnalyzer = Analyzer.IK_SMART)
private String address;

@IndexField(fieldType = FieldType.DATE, dateFormat = "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis")
private Date createTime;

private String createUser;

}

5、创建mapper类,继承BaseEsMapper类,注意这里的mapper一定要创建到第3步中设置的mapper扫描路径下com.example.easyesdemo.mapper


public interface UserEsMapper extends BaseEsMapper<UserEasyEs> {
}

6、创建controller,书写创建索引、新增、修改、查询的接口


@RestController
@RequestMapping("es")
@AllArgsConstructor
public class UserEsController {

private final UserEsMapper userEsMapper;

/**
* 创建索引
* @return
*/

@GetMapping("create")
public Boolean createIndex(){
return userEsMapper.createIndex();
}

@GetMapping("save")
public Integer save(Long id){
UserEasyEs user = new UserEasyEs();
user.setId(id);
user.setName("用户"+id);
user.setAddress("江苏省无锡市滨湖区");
user.setAge(30);
user.setSex(1);
user.setCreateUser("admin");
user.setCreateTime(new Date());
Long count = userEsMapper.selectCount(EsWrappers.lambdaQuery(UserEasyEs.class).eq(UserEasyEs::getId, id));
if(count > 0){
return userEsMapper.updateById(user);
}else{
return userEsMapper.insert(user);
}
}

@GetMapping("search")
public List<UserEasyEs> search(String name, String address){
List<UserEasyEs> userEasyEs = userEsMapper.selectList(
EsWrappers.lambdaQuery(UserEasyEs.class)
.eq(UserEasyEs::getName, name)
.match(UserEasyEs::getAddress, address)
);
return userEasyEs;
}

}

7、分别调用几个接口



  • 创建索引
    在这里插入图片描述
    kibana中查询索引,发现创建成功
    在这里插入图片描述

  • 新增接口
    这里新增了4笔
    在这里插入图片描述
    数据新增成功
    在这里插入图片描述

  • 数据查询


在这里插入图片描述
如上便是针对easy-es的简单使用,这里的用法都与mp类似,上手相当简单,不用再写那些复杂的DSL语句了


3. 拓展介绍



  • 条件构造器



上述演示,我们构造查询条件时,使用了EsWrappers来构造条件,用法与mp及其类型,大家根据提示就可以推导出方法如何书写,更详细的使用说明可以查看官方文档:easy-es 条件构造器介绍




  • 索引托管



如果想要自动根据创建的es实体类来创建对应的索引,那么只需要调整索引的托管模式为非手动模式即可,因为这里我不需要自动同步数据,所以选择非平滑模式



easy-es:
global-config:
process_index_mode: not_smoothly


其中三种模式的区别为:
平滑模式:smoothly,索引的创建、数据更新迁移等都由easy-es自动完成
非平滑模式:not_smoothly,索引自动创建,但不会自动迁移数据
手动模式:manual,全部操作由用户手动完成,默认模式




  • 数据同步



如果数据源是来自mysql, 那么建议使用canal来进行同步,canal的使用可在我主页搜索。
其次还有DataX, Logstash等同步工具,当然你也可以使用easy-es提供的CRUD接口,来手动同步数据




  • 日志打印



通过开启日志,可以在控制台打印执行的DSL语句,更加方便我们在开发阶段进行问题排查



logging:
level:
tracer: trace # 开启trace级别日志,在开发时可以开启此配置,则控制台可以打印es全部请求信息及DSL语句,为了避免重复,开启此项配置后,可以将EE的print-dsl设置为false.


  • 聚合查询
    easy-es实现的聚合查询,只要是针对gr0up by这类聚合,也就是es中的Terms aggregation,以及最大值、最小值、平均值、求和,而对于其他类型的聚合,还在不断更新中,但这里大家也需要了解,es的聚合和mysql的聚合完全是不一样的维度和复杂度,es支持非常多的聚合查询,所以其他类型的实现还需要借助RestHighLevelClient来实现


我们利用easy-es来实现下之前书写的聚合案例
在这里插入图片描述


@RestController
@AllArgsConstructor
@RequestMapping("order")
public class OrderEsController {

private final OrderTestEsMapper orderEsMapper;

@GetMapping("search")
public String search(){
SearchResponse search = orderEsMapper.search(EsWrappers.lambdaQuery(OrderTest.class).groupBy(OrderTest::getStatus));

// 包装查询结果
Aggregations aggregations = search.getAggregations();
Terms terms = (Terms)aggregations.asList().get(0);
List<? extends Terms.Bucket> buckets = terms.getBuckets();
HashMap<String,Long> statusRes = new HashMap<>();
buckets.forEach(bucket -> {
statusRes.put(bucket.getKeyAsString(),bucket.getDocCount());
});
System.out.println("---聚合结果---");
System.out.println(statusRes);
return statusRes.toString();
}
}

可以看到实际上的查询语句就一行,而其他的都是对返回结果的封装,因为es本身返回的数据是封装到嵌套的对象中的,所以我们需要对其进行包装


对比原始的查询语句,其易用性上的提升还是很明显的
在这里插入图片描述


4. 总结


至此对easy-es的介绍就结束了,可以看到如果是针对es实现CRUD上,easy-es表现出非常好的便捷性,而在复杂的聚合查询中,仍然还有进步空间,目前还需要借助RestHighLevelClient,但easy-es的出现,为未来提供更好用的ES ORM框架,提供了希望和方向


文中演示代码见:gitee.com/wuhanxue/wu…


作者:wu55555
来源:juejin.cn/post/7271896547594682428
收起阅读 »

一次低端机 WebView 白屏的兼容之路

web
问题 项目:Vite4 + Vue3,APP WebView 项目 页面在 OPPO A5 手机上打不开,页面空白。 最开始是客户端在看,然后发现一个警告,大概也因为没看出什么问题,给到 Web 前端。 相关背景 为了方便描述过程的行为,先做一些相关背景的介绍...
继续阅读 »

问题


项目:Vite4 + Vue3,APP WebView 项目


页面在 OPPO A5 手机上打不开,页面空白。


最开始是客户端在看,然后发现一个警告,大概也因为没看出什么问题,给到 Web 前端。


相关背景


为了方便描述过程的行为,先做一些相关背景的介绍。知道这些背景才能更好的了解问题的复杂。这些在解决问题的过程中始终是干扰因素,在反复调试试错的过程中才梳理总结出来,这里把它们列出来。


使用测试 App,其中有两个入口,一个是本地调试,这个地址是写在 App 里的,也就是要修改这个地址需要客户端重新出包;一个是项目的测试地址,这个地址测试可以进行配置。



修改客户端,重新出包,是很麻烦的,所以尽量避免。


项目配置了 HTTPS 支持,所以开发地址是 https 开头。但是也能启动 http 的地址。


关于项目支持 HTTPS,可以参考之前写的这篇:juejin.cn/post/732783…


之所以要支持 HTTPS 是因为 iOS WebView 只支持 HTTPS 的地址。


而安卓 App WebView 却需要 HTTP 打开,原因是安卓 WebView 反馈不支持本地地址 HTTPS 的方式。但是在本机上用 MuMu 模拟器打开 App,是能打开本地 HTTPS 地址的,之前也尝试过给安卓手机安装根证书,但是还是不行,得到以上反馈。


所以我本地开发安卓在电脑 MuMu 上调试,iOS 可以用手机调试,如果要安卓真机本地调试,需要去掉本地 HTTPS 的支持,使用 HTTP 的地址(自然,iOS 本地调试就不能同时进行了)


快速尝试


拿到问题之后,快速进行问题验证,在 OPPO A5 上,进入 APP 中,打开本地调试,用 HTTP 的方式。发现确实白屏,查看了客户端相关的日志,发现一个警告:



[INFO:CONSOLE(9)] "The key "viewport-fit" is not recognized and ignored.", source: xxx



于是修改 viewport-fit,发现并没有区别,这只是一个提示,应该没有影响。


于是采用最简代码法,排除法,用最简单的页面进行测试,看是否能正常打开,确定 WebView 没有问题。直到确定 script type="module" 引入的 main.ts 的代码没有起作用。


于是,基本上确定 Vite 的开发模式在 OPPO A5 WebView 中有问题。那不支持 ESM,就是兼容性问题?


快速查看解决方案,引入官方插件:@vitejs/plugin-legacy。但是怎么测试呢?要验证兼容性是否生效,只能验证打包构建后的代码,而不是通过本地调试进行测试。那只能发布到测试了,但这样岂不是要改一点就要发布一次,这是没办法进行的。但是第一次,还是发布一下看有没有生效。


不出意料,没那么容易解决!测试地址依然白屏。


如何调试


确定如何方便的调试是解决问题的必要条件。


几天后又开始看这个问题。


浏览器是否能打开页面?


首先在 App 中进行调试是比较麻烦的,需求启动 App,那么能否在浏览器中进行测试呢?很遗憾,期间用手机系统浏览器打开测试地址是正常的,后面打开本地地址也是正常的。所以浏览器和 App WebView 是有区别的。


启动本地服务查看构建后的页面


兼容插件只是解决打包后的构建产物,想要看打包后的效果,于是我想到将打包后的文件起一个 Web 服务,这样就可以打开打包后的页面 index.html,而且手机访问同一网络,扫码就可以打开这个页面。



  • 找了 Chrome 插件 Web Server for Chrome,发现已经不能用了

  • 找了 VS code 插件 Live Server,服务启了,但是有个报错。

  • 换用 http-server,启动服务 xxx:8080。正常打开页面,手机也能访问。


那么考虑我们的实际问题,如何在手机调试呢?将本地调试改成本地起的服务 xxx:8080,看 WebView 能否打开,这样每次修改、打包,生成新的打包后文件,刷新 WebView 就可以了。


但是前面说了,找 APP 出包很麻烦,改一个地址要出个包,费时。还有其他的办法吗?如果把本地启的服务端口改成 5173 不就不用改 App 了吗,可以直接用本地调试来进行测试,突然又想到本地调试的地址是 HTTPS,可是启动的本地服务好像没法改成 HTTPS。


通过测试地址增加本地调试入口


又想到 App 中的本地调试入口本应该做成一个公共页面,里面放上很多可能的入口。这样只需要 APP 改一次,之后想要什么入口,可以自己添加。改这个调试入口还是需要 App 改动,还是麻烦。我可以在项目中增加一个页面 debug.html(因为项目是多页面应用),这样我增加页面\增加调试入口,发布一下测试就生效了,在测试入口就能看到,这样更快。于是做了一个公共页面。


Vite preview


而且突然想到根本不需要自己起一个服务,Vite 项目,Vite preview 就是把打包后的页面启动服务。地址是:xxx:4173。


修改测试地址为本地预览


然而,OPPO A5 WebView 本来就是打不开我们的系统,那么 WebView 打不开测试地址自然也就没法打开我的本地预览了。但是,测试地址是可以配置的,所以为了快速调试,让测试配置了我的本地预览地址 xxx:4173。


这样,终于在 APP WebView 中打开了我本地预览的页面。


如何查看 App WebView 的日志


手机连接电脑,adb 日志:


image1-2.png


看起来这几个报错是正常的,报错信息也说了:



vite: loading legacy chunks, syntax error above and the same error below should be ignored



但是页面没有加载,不知道 WebView 打开页面和页面加载之间发生了什么。


Vite 兼容插件的原理


这期间,反复详细理解原理,是否是插件的使用不对。


用一句话说就是,Vite 兼容插件让构建打包的产物多了传统版本的 chunk 和对应 ES 语言特性的 polyfill,来支持传统浏览器的运行。兼容版的 chunk 只会在传统浏览器中运行。它是如何做到的呢?




  • 通过 script type="module" 中的代码,判断当前浏览器是否是现代浏览器。如果是,设置全局变量:window.__vite_is_modern_browser = true。判断的依据是:



    • import.meta.url;

    • import("_").catch(() => 1);

    • async function* g() { }



  • 通过 script type="module",如果是现代浏览器,直接退出;如果不是,加载兼容文件:

  • 通过 script type="nomodule",加载兼容 polyfill 文件;

  • 通过 script type="nomodule",加载兼容入口文件;


传统浏览器不执行 type="module" 的代码,执行 type="nomodule" 的代码。


现代浏览器执行 type="module" 的代码,不执行 type="nomodule" 的代码。


为什么需要 type="module" 的代码?这里是针对浏览器支持 ESM 却不支持以上 3 个语法的情况,仍然使用兼容模式。


详细可以看参考文章,以及查看打包构建产物。


除了知乎那篇文章,我几乎翻遍了搜到的 vite 兼容 空白 白屏 相关的文章,参考相关的配置。这个插件就是很常规的使用,几乎没有看到有任何特殊的配置或处理。就是生成了兼容的代码,低版本浏览器就能使用而已,似乎没人碰到过我的问题。


尝试解决


前面说了,用手机系统浏览器打开页面,竟然正常。怀疑是不是 WebView 的问题。


WebView 的内核版本


借了几个低端机型,几个安卓 5.x 6.x 的系统,结果手机浏览器都能正常打开。


打印 console.log(navigator.appVersion),WebView 中:


5.0 (Linux; Android 8.1.0; PBAT00 Build/OPM1.171019.026; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36 uniweb/ma75 uniweb-apk-version/0.6.0 uniweb-script-version/0.6.0 uniweb-channel/netease Unisdk/2.1 NetType/wifi os/android27 ngwebview/4.1 package_name/com.netease.sky udid/944046b939d510b1 webview_orbit/1.2(1)


而手机浏览器版本为 Chrome 90,其他手机有 Chrome70。总之,OPPO A5 WebView 的内核 Chrome 版本较低。


Vite 文档对于构建生产版本浏览器兼容性的介绍:


用于生产环境的构建包会假设目标浏览器支持现代 JavaScript 语法。默认情况下,Vite 的目标是能够 支持原生 ESM script 标签支持原生 ESM 动态导入import.meta 的浏览器


原生 ESM script 标签的支持:



原生 ESM 动态导入的支持:



import.meta 的支持:



所以,原来 Chrome 62 支持 ESM,但是不支持其他 2 个。通过日志,也可以知道不支持兼容插件尝试的 3 个语法,因为打印了那句警告,来自 module 的代码位置,window.__vite_is_modern_browser 不为 true。



从 Android 4.4 开始,系统 WebView 使用 Chrome 内核。


手机系统浏览器内核和系统 WebView 不一样,手机系统的 WebView 也可能不是安卓默认的。


兼容生效了吗?


但还是不知道低版本的浏览器兼容性是否生效,当前我们只能确定 Chrome 62 的 WebView 中兼容有问题,那是否在浏览器中就正常呢?或者更低的版本兼容是否生效?(毕竟不支持 ESM 的浏览器版本的代码执行又不一样)


target 配置不对?


target 配置的是目标浏览器,针对这些浏览器生成对应的兼容代码,期间我一直调整 target 的配置,使用 .broswerlistrc 文件配置,target 直接配置,参考不同的配置方案,确保包含了 Chrome 62。


又是如何调试?


想要下载安卓 Chrome 62 进行测试,但是搜了一圈也没找到。


后来想到不一定要手机浏览器进行测试,Chrome 也行。这里面有点思维上的转换,之前我测试只能通过 WebView 进行调试,因为浏览器上没有问题。现在确定了是浏览器 Chrome 版本的问题,那么我们还是可以通过 PC 浏览器进行测试。


于是范围更大一些,找到了 Chrome 的所有历史版本,不得不说,Chrome 提供的下载真是太有用了,对于测试兼容性非常有帮助! 而且我所担心的覆盖现有浏览器版本的问题完全不存在,下载之后直接运行。


安装 Chrome 62,打开页面,果然空白。终于在浏览器复现,确定就是兼容的问题,而不是 WebView 的问题。安装安卓版本的 Chrome 62,也同样复现。


下载 win 的 Chrome 62,虽然在 refs 里找到同版本的记录:xxx。但是没找到同版本的下载,不过也都是 62,应该没问题。下打开页面,打开控制台,和在 adb 中看到的报错一样,只是这里是红色的:



安装更低版本的 Chrome,同样复现,说明不支持 ESM 的兼容也出现问题。同时可以看到那句提示没有了:



过程当然也没那么顺利,下载 Chrome 的过程中,Chrome 62 直接可以运行,下载 Chrome 59 却没法打开。于是又下载了 Chrome 55,mini exe 文件,可以直接打开。


报错到底要不要处理?


通过 adb日志可以看到报错,也可以从打包后的代码看到:对于语法报错是可以忽略的,因为那是预期中的行为。可是之后的代码为什么没执行了呢?


回到现在的问题,这个报错不是不需要处理吗?但是加载了兼容的 js,页面却没有渲染元素。此时隐隐觉得报错可能还是要处理,至少可能最后一个报错有点问题?


但是这个报错实在难以查看,之前我把它当作和前两个报错一样的来源。现在只剩这个报错了,问题是这是打包压缩后的代码,完全不知道真正的问题是什么。


通过请教网友,做了一些尝试:


通过对插件配置:renderModernChunks: false,只生成兼容代码,依然报错。


通过修改 Vite 配置:build.minify: false 不压缩代码,尝试查看报错位置。新的报错:



升级 Vite。新的报错:



所以每次的报错都不一样,越来越奇怪。不过看起来似乎是同一个原因导致的。


在构建源码中调试


通过在构建后的源码中打印,其中 excute 函数中,有两个参数 exports module,但是在其中使用 module.meta 报错,说明其他文件在使用这个方法是并没有传参。看起来像是模块规范的问题(commonJS 和 ES Module)。


ChatGPT


在这期间,也在 ChatGPT 搜素方法:




就尝试了一下 format: 'es',顺便看到有个配置 compact: true,好像也是压缩,就顺手改成 false,这样全部不要压缩,方便看报错。


结果竟然 OK 了,页面打开,没有报错!


是这个配置生效的吗?通过排除,发现竟然是 compact 的原因。这个配置不是 Vite 本身的,是 Vite 使用的 rollup 的配置:




果然是插件冲突的结果。


再搜素 execute,已经没有带参数了:



再次感叹 Webpack 配置工程师



build.sourcemap


后来想到开启 sourcemap 来定位报错的原始文件位置,未开启:




开启 sourcemap:




如果在打包过程中对代码进行了混淆或压缩,可能会导致 Source Map 无法准确映射到原始代码位置。


这就完了?


中午去吃个饭,下午回来,本以为打包发布验证一下就完了,结果测试地址能打开页面了,却和我在电脑浏览器上看到的一样,没有正常加载页面。晴天霹雳!


image21-2.png


说明一下:我们这个项目是 App Hybrid 应用,Web 前端和服务端的通信通过客户端中转,所以在非客户端环境是拿不到服务端的数据的,联调测试都是在 App 中进行的。


但为什么前面一直用浏览器测试呢?虽然数据获取不到,但是如果页面加载出来了,说明打包代码是没问题,所以在前面页面显示了背景图等元素,id="app"中有了内容以后,就是兼容版本的 js 正常执行了。但是再说一次,真实的环境是 WebView,最终的结果还是要看 WebView。


目前在 App 中还是没有显示完整的网页加载过程,又陷入了困境。页面已经没有了报错(除了 Vite 那个可以被忽略的),难道生成的兼容 js 有问题? 如果是具体的兼容代码有问题,那整个项目的代码又如何排查呢?


虽然没有头绪,但是隐隐觉得应该再坚持一下。继续分析,如果已经显示了页面,兼容的 js 已经执行了,那么数据接口请求了吗?


于是联系服务端,查看服务器日志,确定页面加载没有请求接口,说明请求接口的代码没执行。我用高版本 Chrome 查看 Preview(之前没做这一步),果然除了看到页面元素,还进行了请求尝试。于是又运用排除法,一步步打印首页加载的过程。


在这之前,又是如何调试的问题,我们把测试入口改为我本地的 preview 地址。


加入打印日志,本地 build,Preview,打印了日志信息,果然接口请求的部分未执行。排查发现是之前调起 WebView 导航的代码出问题,可能是 jsBridge 还未加载。


但是为什么其他设备没有出现这个问题呢,可能是设备性能较差。于是把相关代码放到 jsBridge 加载完成后执行,修复了这样一个隐藏的 bug。


但是为什么不报错呢??这真的是很不好的体验,之前 Vue router 不报错的问题也类似。


总结


同样,我们再回头看那个最初的报错:



vite: loading legacy chunks, syntax error above and the same error below should be ignored



上面的报错、项目相同的报错可以被忽略。很容易让人忽略了报错,明确提到了下面的报错,但是 Vite 打包中下面没有出现同样的报错了(我目前的打包和其他文章中提到的不一样,比如知乎文章提到的代码);而且相同的报错到底是什么相同,同样是语法报错而已啊。


这句提示值得商榷。


function __vite_legacy_guard() {
import.meta.url;
import("_").catch(() => 1);
async function* g() {};
};

主要是因为出问题的恰恰就是中间的版本,Chrome 62,Vite 让它报错又能正常运行。遇到这种情况少之又少,从权衡上来说,好像也没有问题


本质上,只是在解决一个打包后的文件报错的问题,问题是一开始并没有定位到这个问题,其次是打包后的报错仍然难以定位具体错误位置。然后这其中还涉及到项目自身环境的各种干扰。


几点感悟:



  • 坚持不懈,这是解决问题的唯一原因。

  • 总结熟练调试很重要,要快速找到方便调试的方法。

  • 没有报错是开发的一大痛点。

  • 针对当前的问题更深入的分析原因,更广泛的尝试。

  • 多用 ChatGPT,ChatGPT 的强大在于它没有弱点,没有缺项。


说明


通过这个案例,希望能给大家一点解决问题的启发。是遇到类似的问题时:



  • 了解相关的问题

  • 熟悉相关的概念

  • 学习解决问题的方法

  • 学习调试的方法

  • 坚持的重要性


参考


【原理揭秘】Vite 是怎么兼容老旧浏览器的?你以为仅仅依靠 Babel?


juejin.cn/post/723953…


Chromium History Versions Download ↓


作者:choreau
来源:juejin.cn/post/7386493910820667418
收起阅读 »

研发都认为DBA很Low?我反手一个嘴巴子

前言我用十多年的DBA经验告诉你,如果你作为研发觉得DBA很Low,你是会吃苦头的“你以为DBA就是安装一下数据库,管理一下数据库?你丢个SQL给DBA优化下?你的日志爆满了DBA给你清理一下?DBA帮你安装下中间件?你以为的DBA只是做这些事?”秉持着和平交...
继续阅读 »

前言

我用十多年的DBA经验告诉你,如果你作为研发觉得DBA很Low,你是会吃苦头的

“你以为DBA就是安装一下数据库,管理一下数据库?你丢个SQL给DBA优化下?你的日志爆满了DBA给你清理一下?DBA帮你安装下中间件?你以为的DBA只是做这些事?”

秉持着和平交流的学习态度,我这里精选了几位高赞粉丝的精彩回答

1.救火能力

1.1 调优

IT界并没有一个通行的 ”拳头“ 来判断谁low,谁更low。有时候,研发写的程序,新功能发布后,就出现磁盘IO出现瓶颈了、或者CPU飙高到100%了,但是这个时候,只是表象,只知道Linux机器的资源耗尽了,DBA得先找到资源消耗在哪了,才能进一步分析原因,用数据说话是应用的问题,才能责令程序员整改。

SQL调优是一个复杂的过程,涉及多个方面,包括但不限于SQL语句的编写、索引的使用、表的连接策略、数据库的统计信息、系统资源的利用等。调优的难度取决于多个因素,包括查询的复杂性、数据量、硬件资源、数据库的工作负载和现有的优化策略。

在这里给大家分享一个执行计划变,1个SQL把系统干崩的情景,由于业务用户检索数据范围过大,导致执行计划谓词越界,通过矫正执行计划及开启操作系统大页,服务器DB一直存在的CPU高负载从75%降低到25%!

生产问题,瞬息万变,DBA要同时熟悉业务,并对硬件、网络要精通,要在这样的复杂情况下作出正确的决策,这一点我想难度不小吧。

1.2 高可用

数据库高可用是指DB集群中任何一个节点的故障都不会影响用户的使用,连接到故障节点的用户会被自动转移到健康节点,从用户感受而言, 是感觉不到这种切换。

那么DBA在高可用的配置方面,下面就是某制造业大厂,应用层的链接方式

--jdbc应用端的连接
jdbc:oracle:thin:@(DESCRIPTION =
(ADDRESS_LIST =(ADDRESS = (PROTOCOL = TCP)(HOST = rac1-vip)
(PORT = 1521))(ADDRESS = (PROTOCOL = TCP)(HOST = rac2-vip)
(PORT = 1521))(LOAD_BALANCE = no)(FAILOVER = yes))
(CONNECT_DATA =(SERVER = DEDICATED)(SERVICE_NAME = dbserver)))

那么这种配置FAILOVER = yes,Net会从多个地址中按顺序选择一个地址进行连接,直到连接成功为止,那么就会保证数据库单节点故障,自动的切换,高可用是故障发生的第一个救命的稻草,系统上线前一定要测试好,才能确保数据库的高可用,这期间DBA功不可没!

还有客户要求选择的一套国产数据库支持核心业务,那么作为DBA在选型及业务适配上就发挥作用了,跟研发确认发现应用是兼容PG的,而且客户要求要同时兼容OLAT和OLTP业务,看下以下这套openGauss国产数据库的高可用架构。

1.openGauss高可用:CM
通过配置VIP故障转移,OLTP连接VIP,进行事物交易
同时支持动态配置CM集群故障切换策略和数据库集群脑裂故障恢复策略,
从而能够尽可能确保集群数据的完整性和一致性。

2.写重定向,报表分析业务连接,支持读写分离
主备节点开启控制参数 enable_remote_execute=on之后
通过备库发起的写操作,会重定向到主库执行

2.监控能力

这方面我是最有发言权了,SA一直是我的本职工作,从机房硬件部署、弱电以及数据库的安装实施,很多东西需要依赖于DBA来做,全力保障应用的稳定性,而且监控到的指标随时可以推送到邮件以及微信。这期间我也发现了很多天窗,原来还可以这么干?

2.1 服务器监控

首先监控Linux服务嘛,那肯定是要全方位系统的监控,网络、磁盘、CPU、内存等等,这才叫监控,那么其实给大家推荐一款免费的监控工作

Prometheus提供了从指标暴露,到指标抓取、存储和可视化,以及最后的监控告警等组件。

数据库监控

Zabbix聚焦于帮助用户通过性能优化和功能升级来快速响应业务需求,从而满足客户的高期望值,并提升IT运维人员的生产力。在可扩展性与性能、稳定性与高可用、可观测性几个领域获得持续提升。监控做不好,救火救到老!拿下Zabbix,现在!立刻!马上!!

1.监控Oracle
博客地址:
https://jeames.blog.csdn.net/article/details/126825934
2.监控PostgreSQL
博客地址:
https://jeames.blog.csdn.net/article/details/120300581
3.监控MySQL
博客地址:
https://jeames.blog.csdn.net/article/details/126825934

3 数据源赋能者

从AI、智能化到云迁移和安全性,业务和技术趋势不断重塑DBA在组织中的角色.DBA 群体站在时代的岔路口,国产数据库太多了应该怎么选?DBA 会被云上数据库抛弃吗?应该如何应对新时代挑战?职业终点在哪里?

1.云数据库解决方案
DBA要善于利用云原生保障数据安全和优化成本

2.数据安全与合规
随着数据保护法律的出台、日益严峻的网络攻击,
DBA必须掌握加密、访问控制和审计等技能

3.灾难恢复和业务连续性
随着企业愈加依赖数据的连续性,
快速恢复丢失数据并最大限度地减少停机时间至关重要

4.自动化和脚本编写
自动化和脚本编写对于DBA管理重复性任务和提高效率尤为关键

5.有效的沟通和协作
有效的沟通和协作仍然是DBA的重要技能。
能够向同事清楚地传达技术信息、与跨职能团队合作,
打破IT部门和业务部门之间的信息差,确保数据库的策略与组织目标保持一致。

4.总结

在一个公司写了屎山代码的研发,可以拍拍屁股走人,然后继续去下一个企业再写个屎山。反正不会追着代码跨省找你。而一个搞崩了系统的DBA,这个闯祸经历将成为他的黑历史,并影响到他未来的就业.因为需要专业DBA的好企业,基本都是几百台服务器起步的大项目,难免不会查背景,这就导致DBA如果想干得好,圈子会越来越小,请记住是干得好,不是混得好,混是会出事的。

好了,以上就是我对DBA的理解了,有不足之处还望指正。


作者:IT邦德
来源:juejin.cn/post/7386505099848646710
收起阅读 »

谈谈前端如何防止数据泄漏

web
最近突然发现了一个好玩的事情,部分网站进去的时候几乎都是死的,那种死是区别于我们常见的网站的死:不能选中文字不能复制粘贴文字不能鼠标右键显示选项不能打开控制台……各种奇葩的操作应接不暇,像极了我最初接触的某库。shigen的好奇心直接拉满,好家伙,这是咋做的呀...
继续阅读 »

最近突然发现了一个好玩的事情,部分网站进去的时候几乎都是死的,那种死是区别于我们常见的网站的死:

  • 不能选中文字
  • 不能复制粘贴文字
  • 不能鼠标右键显示选项
  • 不能打开控制台
  • ……

各种奇葩的操作应接不暇,像极了我最初接触的某库。shigen的好奇心直接拉满,好家伙,这是咋做的呀。一顿操作之后,发现这种是为了防止网站的数据泄露(高大上)。在我看来,不是为了装X就是为了割韭菜。

咱废话也不多说,就手动来一个,部分代码参考文章:如何防止网站信息泄露(复制/水印/控制台)

shigen实现的效果是这样的:

将进酒页面

用魔法生成了一个页面,展示的是李白的《将进酒》。我需要的功能有尽可能的全面,禁止复制、选择、调试……

找了很多的方式,最后能自豪的展示出来的功能有:

  • 禁止选择
  • 禁止鼠标右键
  • 禁止复制粘贴
  • 禁止调试资源(刷新页面的方式)
  • 常见的页面水印

那其实也没有特别的技术含量,我就在这里展示了,希望能作为工具类供大家使用。

页面部分

html5+css,没啥好讲的。

 html>
 <html lang="zh-CN">
 
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <style>
         body {
             font-family: "Microsoft YaHei", sans-serif;
             line-height: 1.6;
             padding: 20px;
             text-align: center;
             background-color: #f8f8f8;
        }
 
         .poem-container {
             max-width: 600px;
             margin: 0 auto;
             background-color: #fff;
             padding: 20px;
             border-radius: 8px;
             box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
        }
 
         h1 {
             font-size: 1.5em;
             margin-bottom: 20px;
        }
 
         p {
             text-indent: 2em;
             font-size: 1.2em;
        }
     style>
     <title>李白《将进酒》title>
 head>
 
 <body>
     <div class="poem-container">
         <h1>将进酒h1>
         <p>君不见,黄河之水天上来,奔流到海不复回。p>
         <p>君不见,高堂明镜悲白发,朝如青丝暮成雪。p>
         <p>人生得意须尽欢,莫使金樽空对月。p>
         <p>天生我材必有用,千金散尽还复来。p>
         <p>烹羊宰牛且为乐,会须一饮三百杯。p>
         <p>岑夫子,丹丘生,将进酒,杯莫停。p>
         <p>与君歌一曲,请君为我倾耳听。p>
         <p>钟鼓馔玉不足贵,但愿长醉不复醒。p>
         <p>古来圣贤皆寂寞,惟有饮者留其名。p>
         <p>陈王昔时宴平乐,斗酒十千恣欢谑。p>
         <p>主人何为言少钱,径须沽取对君酌。p>
         <p>五花马,千金裘,呼儿将出换美酒,与尔同销万古愁。p>
     div>
  body>

js部分

禁止选中

 // 防止用户选中
 function disableSelect() {
     // 方式:给body设置样式
     document.body.style.userSelect = 'none';
 
     // 禁用input的ctrl + a
     document.keyDown = function(event) {
         const { ctrlKey, metaKey, keyCode } = event;
         if ((ctrlKey || metaKey) && keyCode === 65) {
             return false;
        }
    }
 };

禁止复制、粘贴、剪切

 document.addEventListener('copy', function(e) {
     e.preventDefault();
 });
 document.addEventListener('cut', function(e) {
     e.preventDefault();
 });
 document.addEventListener('paste', function(e) {
     e.preventDefault();
 });

禁止鼠标右键

 // 防止右键
 window.oncontextmenu = function() {
     event.preventDefault()
     return false
 }

禁止调试资源

这个我会重点分析。

 let threshold = 160 // 打开控制台的宽或高阈值  
 window.setInterval(function() {
     if (window.outerWidth - window.innerWidth > threshold ||
         window.outerHeight - window.innerHeight > threshold) {
         // 如果打开控制台,则刷新页面  
         window.location.reload()
    }
 }, 1000)

这个代码的意思很好理解,当我们F12的时候,页面的宽度肯定会变小的,我们这个时候和屏幕的宽度比较,大于我们设置的阈值,我们就算用户在调试页面了。这也是我目前找到的比较好的方式了。但是,但是,认真思考一下以下问题需要你考虑吗?

  • 页面频繁加载,流量的损失大吗
  • 页面刷新,后端接口频繁调用,接口压力、接口幂等性

所以,我觉得这种方式不优雅,极度的不优雅,但是有没有别的好的解决办法。

加水印

 // 生成水印
 function generateWatermark(keyword = 'shigen-demo') {
     // 创建Canvas元素  
     const canvas = document.createElement('canvas');
     const context = canvas.getContext('2d');
 
     // 设置Canvas尺寸和字体样式  
     canvas.width = 100;
     canvas.height = 100;
     context.font = '10px Arial';
     context.fillStyle = 'rgba(0,0,0,0.1)';
 
     // 绘制文字到Canvas上  
     context.fillText(keyword, 10, 50);
 
     // 生成水印图像的URL  
     const watermarkUrl = canvas.toDataURL();
 
     // 在页面上显示水印图像(或进行其他操作)  
     const divDom = document.createElement('div');
     divDom.style.cssText = `
         position: fixed;
         z-index: 99999;
         top: -10000px;
         bottom: -10000px;
         left: -10000px;
         right: -10000px;
         transform: rotate(-45deg);
         pointer-events: none;
         background-image: url(${watermarkUrl});
     `
;
     document.body.appendChild(divDom);
 }

代码不需要理解,部分的参数去调整一下,就可以拿来就用了。

我一想,我最初接触到这种页面水印的时候,是在很老的OA办公系统,到后来用到了某书,它的app页面充满了水印,包括浏览器端的页面。

所以,我也实现了这个。but,but,有一种技术叫做OCR,大白话讲就是文字识别。我把图片截个图,让某信、某书识别以下,速度和效果那叫一个nice,当然也可能把水印也识别出来了。聪敏的开发者会把水印的颜色和文字的颜色设置成一种,这个时候需要准确的文字那可得下一番功夫了。换句话说,不是定制化的OCR,准确的识别出信息,真的够呛。

还有的很多页面实现了js的数据加密、接口数据加密。但是道高一尺,魔高一丈,各种都是在一种相互进步的。就看实际的业务场景和系统的设计了。


作者:shigen01
来源:juejin.cn/post/7300102080903675915
收起阅读 »

从劝退 flutter_screenutil 聊到不同尺寸 UI 适配的最佳实践

先说优点 💡 先说优点叠个甲,毕竟库本身没有太大问题,往往都是使用的人有问题。 由于是基于设计稿进行屏幕适配的框架,在处理不同尺寸的屏幕时,都可以使用相同的 尺寸数值+单位 ,实现对设计稿等比例的适配,同时保真程度一般很高。 在有设计稿的情况下,只使用 C...
继续阅读 »

先说优点



💡 先说优点叠个甲,毕竟库本身没有太大问题,往往都是使用的人有问题。



由于是基于设计稿进行屏幕适配的框架,在处理不同尺寸的屏幕时,都可以使用相同的 尺寸数值+单位 ,实现对设计稿等比例的适配,同时保真程度一般很高。


在有设计稿的情况下,只使用 Container + GestureDetector 都可以做到快速的开发,可谓是十分的无脑梭哈。


在:只考虑移动端、可以接受使用大屏幕手机看小屏幕 ui、不考虑大字体的模式、被强烈要求还原设计稿、急着开发。的情况下,还是挺好用的。


为什么劝退?



来到我劝退师最喜欢的一个问题,为什么劝退。如果做得不好,瞎搞乱搞,那就是我劝退的对象。



在亲身使用了两个项目并结合群里的各种疑惑,我遇到常见的有如下问题:


如何实现对平板甚至是桌面设备的适配?


由于基于设计稿尺寸,平板、桌面等设备的适配基本上是没法做的,要做也是费力不讨好的事。


千万不要想着说,我通过屏幕宽度断点来使用不同的设计稿,当用户拉动边框来修改页面的宽度时,体验感是很崩溃的。而且三套设计稿要写三遍不同的代码,就更不提了。(这里说三遍代码的原因是,计算 .w .h 的布局,数据会跟随设计稿变化)


如何适配大字体无障碍?


因为大字体缩放在满屏的 .w .h 下,也就是写死了尺寸的情况下,字体由于随系统字体放大,布局是绝对会溢出的。很多项目开发到最后上线才意识到自己有大字体无障碍的用户,甚至某些博客上,使用了一句:


MediaQuery.of(context).copyWith(textScaleFactor: 1.0),

来处理掉自己的用户,强制所有屏幕字体不可缩放。一时的勉强敷衍过去,最后只能等项目慢慢腐烂。


为什么在 1.w 的情况下会很糊?同样是 16.sp 为什么肉眼可见的不一样大?


库的原理很简单,提供了一堆的 api 相对于设计图的宽高去做等比例计算,所以必然存在一个问题,计算结果是浮点数。可是?浮点数有什么问题吗?


梳理一下原理:已知屏幕设计图宽度 sdw 、组件设计图宽度 dw ,根据屏幕实际宽度 sw ,去计算得出组件实际宽度 w


w = sw / sdw * dw

可是设计图的屏幕宽度 sdw 作为分母时,并不能保证总是可以被表示为有限小数。举个例子:库的文档中给的示例是 const Size(360, 690), 的尺寸,如果我需要一个 100.w 会得到多少?在屏幕宽度为 420 的情况下,得到组件宽度应该为 116.6666... 的无限小数


这会导致最终在栅格化时会面临消除小数点像素的锯齿问题。一旦有像素点的偏差,就会导致边缘模糊。


字体对尺寸大小更为敏感,一些非矢量的字体甚至只有几个档位的大小,当使用 14.5、15、15.5 的字体大小时,可能会得到一样的视觉大小,再加上 .sp 去计算一道,误差更是放大。



具体是否会发生在栅格化阶段,哪怕文章有误也无所谓,小数点像素在物理意义上就是不存在的,总是会面临锯齿平滑的处理,导致无法像素级还原 UI。



为什么部分屏幕下会溢出?


我们知道了有小数点问题,那么不得不说起计算机编程常见的一个不等式:


0.1 + 0.2 != 0.3

由于底层表示浮点数本身就有的精度问题,现在让 Flutter 去做这个加法,一样会溢出。考虑以下代码:


    Row(
children: [
SizedBox(width: 60.w),
SizedBox(width: 100.w),
SizedBox(width: 200.w),
],
);

在一个总共宽度 360.w 的设计图上,可能出现了溢出,如果不去使用多个屏幕来调试,根本不会觉得异常,毕竟设计图是这样做的,我也是这样写的,怎么可能有错呢?


然而恰恰是库本身的小数问题,加上编程届常见的底层浮点数精度问题,导致边缘溢出一点点像素。


我使用了 screenutil 为什么和真实的单位 1px 1rem 1dp 的大小不同呢?


哪怕是 .sp 都是基于设计图等比例缩放的,使用 screenutil 就从来不存在真实大小,计算的结果都是基于设计稿的相对大小。就连 .w.h 都没法保证比例相同,导致所有布局优先使用 .w 来编写代码的库,还想保证和真实尺寸相等?


为什么需要响应式 UI?


说个题外话:在面试淘菜菜的时候真的会有点崩不住,他们问如何做好不同屏幕的适配,我说首先这是 UI 出图的问题,如果 UI 出的图是响应式的,那没问题,照着写,闭着眼都能适配。


但是如果设计图不是响应式的,使用 flutter_screenutil 可以做到和设计图高保真等比还原,但是如果做多平台就需要 UI 根据屏幕断点出不同平台的设计图。


面试官立即就打断我说他们的 UI 只会出一份图。我当场就沉默了,然后呢?也不说话了?是因为只有移动端用户,或者说贵公司 UI 太菜了,还是说都太菜了。菜就给我往下学 ⏬


首先 UI 的响应式设计是 UI 的责任


抛开国情不谈,因为国内的 UI 能做到设计的同时,UI 还是响应式的,这样的 UI 设计师很少很少,他们能把主题规范好,约定好,已经是不得了的了。


但即使如此,响应式 UI 设计也还是应该归于 UI 设计中,在设计图中去根据不同的尺寸,拖动验证不同的布局效果是很容易的。在不同的尺寸下,应该怎么调整元素个数,应该如何去布局元素,只有 UI 使用响应式的写法去实现了,UI 和开发之间的无效交流才会减少。


响应式的 UI 可以避免精度问题


早在 19 年我就有幸翻阅了一本 iOS 的 UI 设计规范,当时有个特别的点特别印象深刻:尺寸大小应该为 2 的整数次幂,或者 4 的倍数。因为这样做,在显示和计算上会较为友好。



💡 这其实是有点历史原因的,之前的 UI 在栅格化上做得并不是很好,锯齿化严重也是常态,所以使用可以被 2 整除的尺寸,一方面使用起来只有几个档位,方便调整;另一方面这样的尺寸可以在像素的栅格化上把小数除尽。



举个例子,在屏幕中间显示一个 300 宽度的卡片,和边距 16 的卡片,哪一个更响应式,无疑是后者,前者由于需要计算 300 相对与设计稿屏幕的宽度,后者只需要准确的执行 16 的边距就好,中间的卡片宽度随屏幕的宽度自动变化。


同样的例子,带有 Expanded 布局的 Row 组件,相比直接给定每个子组件尺寸导致精度问题的布局,更能适配不同的屏幕。因为 Row 会先放置固定大小的组件,剩余空间由 Expanded 去计算好传给子组件,原理和 Web 开发中的 flex 布局一样。


响应式布局是通用的规范


如果有 Web 开发经验的,应该会知道 Web 的屏幕是最多变的,但是设计起来也可以很规范,常见的 bootstrap 框架就提到了断点这个观点,指出了当我们去做 UI 适配的时候,需要根据不同的屏幕大小去做适配。同时 flex 布局也是 Web 布局中常用的响应式布局手段。


在设计工具中,响应式 UI 也没有那么遥远,去下载一份 Material Design 的 demo,对里面的组件自由的拉伸缩放,再对比一下自己通过输入尺寸大小拼凑在一起的 UI,找找参数里面哪里有差异。


怎么做响应式 UI


这里直接放一个谷歌大会的演讲,我相信下面的总结其实都可以不用看了,毕竟本实验室没有什么可补充的,但是我们还是通过从外到内、从整体到局部的顺序来梳理一下如何去做一个响应式的 UI,从而彻底告别使用 flutter_screenutil。


http://www.youtube.com/watch?v=LeK…


SafeArea


一个简单的组件,可以确保内部的 UI 不会因为愚蠢的设备圆角、前置挖孔摄像头、折叠屏链接脚、全面屏边框等原因而被意外的裁剪,将重要的内容,显示在“安全区”中。


屏幕断点


让 UI 根据不同的尺寸的窗口变化而变化,首先就要使用 MediaQuery.sizeOf(context);LayoutBuilder() 来实现对窗口的宽度的获取,然后通过不同的屏幕断点,去构建不同情况下的 UI。


其中 LayoutBuilder 还能获取当前约束下的宽度,以实现页面中子区域的布局,比如 Drawer 的宽度,对话框的宽度,导航的宽度。


这里举了个例子,使用媒体查询获得窗口宽度之后,展示不同的 Dialog



写出如此优雅的断点代码只需要三步:



  • 抽象:找到全屏对话框和普通对话框中共同的属性,并将功能页面提取出来。

  • 测量:思考应该使用窗口级别的宽度(MediaQuery),还是某个约束下的宽度(LayoutBuilder)。

  • 分支:编写如上图所示的带有断点逻辑的代码。



GridView


熟悉了移动端的 ListView 布局之后,切换到 GridView 布局并适配到平板、桌面端,是一件十分自然的事,只需要根据情况使用不同的 gridDelegate 属性来设置布局方式,就能简单的适配。


这里一般使用 SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: ) 方法来适配,传入一个期望的最大宽度,使其在任何屏幕上看到的子组件都自然清晰,GridView 会根据宽度计算出合适的一行里合适的列数。


Flex 布局,但是 Flutter 版


前面说过了尽量不要去写固定尺寸的几个元素加起来等于屏幕宽度,没有那么巧合的事情。在 Row/Column 中,善用 Expanded 去展开子组件占用剩余空间,善用 Flexible 去缩紧子组件,最后善用 Spacer 去占用空白,结合 MainAxisAlignment 的属性,你会发现布局是那样的自然。


只有部分组件是固定尺寸的


例如 Icon 一般默认 24,AppBar 和 BottomNavigationBar 高度为 56,这些是写在 MD 设计中的固定尺寸,但是一般不去修改。图片或许是固定尺寸的,但是一般也使用 AspectRatio 来固定宽高比。


我曾经也说过一个普遍的公理,因为有太多初学者容易因为这个问题而出错了。



当你去动态计算宽高的时候,可能是布局思路有问题了。



在大多数情况下,你的布局都不应该计算宽高,交给响应式布局,让组件通过自己的能力去得出自己的位置、约束、尺寸。


举一个遇到过的群友问题,他使用了 stack 布局包裹了应用栏和一个滚动布局,由于SliverAppBar 拉伸后的高度会变化,他想去动态的计算下方的滚动布局的组件起始位置。这个问题就连描述出来都是不可思议的,然后他问我,我应该如何去获取这个 AppBar 的高度,因为我想计算下方组件的高度。(原问题记不清了,但是这样的需求是不成立的)


最后,多看文档


最后补上关于 MD3 设计中,关于布局的文档,仔细学习:


Layout – Material Design 3


最后的最后,响应式布局其实是一个很宽的话题,这里没法三言两语说完,只能先暂时在某些领域劝退使用这个库。任何觉得可能布局困难的需求,都可以发到评论区讨论,下一篇文章我们将根据几个案例来谈谈具体的实践。


作者:优雅实践实验室
来源:juejin.cn/post/7386947074640298038
收起阅读 »

没用的东西,你连个内存泄漏都排查不出来!!

web
背景 (书接上回) ui妹子的无理要求,我通通满足了。但是不出意外的话,意外就出来了。 此功能在上线之后,我们的业务在客户app内使用刷脸的时候会因为内存过高导致app将webview杀死。 然后我们leader爆了,让我排查问题。可是可是,我哪里会排查内存...
继续阅读 »

背景 (书接上回)



  • ui妹子的无理要求,我通通满足了。但是不出意外的话,意外就出来了。

  • 此功能在上线之后,我们的业务在客户app内使用刷脸的时候会因为内存过高导致app将webview杀死。

  • 然后我们leader爆了,让我排查问题。可是可是,我哪里会排查内存泄漏呀。

  • 我:我不会。你自己不会上吗?你tm天天端个茶,抽个烟,翘个二郎腿,色眯眯的看着ui妹妹。

  • 领导:污蔑,你纯粹就是污蔑。我tm现在就可以让你滚蛋,你信吗?

  • 我:我怕你个鸟哦,我还不知道你啥水平,你tm能写出来个防抖节流,我就给你磕头。

  • 领导:hi~ui妹妹,今天过的好吗,来,哥哥这里有茶喝。(此时ui妹妹路过)。你赶快给我干活,以后ui妹妹留给你。

  • 艹!你早这么说不就好了。





开始学习


Chrome devTools查看内存情况




  • 打开Chrome的无痕模式,这样做的目的是为了屏蔽掉Chrome插件对我们之后测试内存占用情况的影响

  • 打开开发者工具,找到Performance这一栏,可以看到其内部带着一些功能按钮,例如:开始录制按钮;刷新页面按钮;清空记录按钮;记录并可视化js内存、节点、事件监听器按钮;触发垃圾回收机制按钮等




简单录制一下百度页面,看看我们能获得什么,如下动图所示:




从上图中我们可以看到,在页面从零到加载完成这个过程中JS Heap(js堆内存)、documents(文档)、Nodes(DOM节点)、Listeners(监听器)、GPU memoryGPU内存)的最低值、最高值以及随时间的走势曲线,这也是我们主要关注的点



看看开发者工具中的Memory一栏,其主要是用于记录页面堆内存的具体情况以及js堆内存随加载时间线动态的分配情况



堆快照就像照相机一样,能记录你当前页面的堆内存情况,每快照一次就会产生一条快照记录




如上图所示,刚开始执行了一次快照,记录了当时堆内存空间占用为33.7MB,然后我们点击了页面中某些按钮,又执行一次快照,记录了当时堆内存空间占用为32.5MB。并且点击对应的快照记录,能看到当时所有内存中的变量情况(结构、占总占用内存的百分比...)





在开始记录后,我们可以看到图中右上角有起伏的蓝色与灰色的柱形图,其中蓝色表示当前时间线下占用着的内存;灰色表示之前占用的内存空间已被清除释放



在得知有内存泄漏的情况存在时,我们可以改用Memory来更明确得确认问题和定位问题


首先可以用Allocation instrumentation on timeline来确认问题,如下图所示:



内存泄漏的场景



  • 闭包使用不当引起内存泄漏

  • 全局变量

  • 分离的DOM节点

  • 控制台的打印

  • 遗忘的定时器


1. 闭包使用不当引起内存泄漏


使用PerformanceMemory来查看一下闭包导致的内存泄漏问题


<button onclick="myClick()">执行fn1函数button>
<script>
function fn1 () {
let a = new Array(10000) // 这里设置了一个很大的数组对象

let b = 3

function fn2() {
let c = [1, 2, 3]
}

fn2()

return a
}

let res = []

function myClick() {
res.
push(fn1())
}
script>


在退出fn1函数执行上下文后,该上下文中的变量a本应被当作垃圾数据给回收掉,但因fn1函数最终将变量a返回并赋值给全局变量res,其产生了对变量a的引用,所以变量a被标记为活动变量并一直占用着相应的内存,假设变量res后续用不到,这就算是一种闭包使用不当的例子



设置了一个按钮,每次执行就会将fn1函数的返回值添加到全局数组变量res中,是为了能在performacne的曲线图中看出效果,如图所示:




  • 在每次录制开始时手动触发一次垃圾回收机制,这是为了确认一个初始的堆内存基准线,便于后面的对比,然后我们点击了几次按钮,即往全局数组变量res中添加了几个比较大的数组对象,最后再触发一次垃圾回收,发现录制结果的JS Heap曲线刚开始成阶梯式上升的,最后的曲线的高度比基准线要高,说明可能是存在内存泄漏的问题

  • 在得知有内存泄漏的情况存在时,我们可以改用Memory来更明确得确认问题和定位问题

  • 首先可以用Allocation instrumentation on timeline来确认问题,如下图所示:




  • 在我们每次点击按钮后,动态内存分配情况图上都会出现一个蓝色的柱形,并且在我们触发垃圾回收后,蓝色柱形都没变成灰色柱形,即之前分配的内存并未被清除

  • 所以此时我们就可以更明确得确认内存泄漏的问题是存在的了,接下来就精准定位问题,可以利用Heap snapshot来定位问题,如图所示:




  • 第一次先点击快照记录初始的内存情况,然后我们多次点击按钮后再次点击快照,记录此时的内存情况,发现从原来的1.1M内存空间变成了1.4M内存空间,然后我们选中第二条快照记录,可以看到右上角有个All objects的字段,其表示展示的是当前选中的快照记录所有对象的分配情况,而我们想要知道的是第二条快照与第一条快照的区别在哪,所以选择Object allocated between Snapshot1 and Snapshot2即展示第一条快照和第二条快照存在差异的内存对象分配情况,此时可以看到Array的百分比很高,初步可以判断是该变量存在问题,点击查看详情后就能查看到该变量对应的具体数据了


以上就是一个判断闭包带来内存泄漏问题并简单定位的方法了


2. 全局变量


全局的变量一般是不会被垃圾回收掉的当然这并不是说变量都不能存在全局,只是有时候会因为疏忽而导致某些变量流失到全局,例如未声明变量,却直接对某变量进行赋值,就会导致该变量在全局创建,如下所示:


function fn1() {
// 此处变量name未被声明
name = new Array(99999999)
}

fn1()


  • 此时这种情况就会在全局自动创建一个变量name,并将一个很大的数组赋值给name,又因为是全局变量,所以该内存空间就一直不会被释放

  • 解决办法的话,自己平时要多加注意,不要在变量未声明前赋值,或者也可以开启严格模式,这样就会在不知情犯错时,收到报错警告,例如


function fn1() {
'use strict';
name = new Array(99999999)
}

fn1()

3. 分离的DOM节点


假设你手动移除了某个dom节点,本应释放该dom节点所占用的内存,但却因为疏忽导致某处代码仍对该被移除节点有引用,最终导致该节点所占内存无法被释放,例如这种情况


<div id="root">
<div class="child">我是子元素div>
<button>移除button>
div>
<script>
let btn = document.querySelector('button')
let child = document.querySelector('.child')
let root = document.querySelector('#root')

btn.
addEventListener('click', function() {
root.
removeChild(child)
})
script>


该代码所做的操作就是点击按钮后移除.child的节点,虽然点击后,该节点确实从dom被移除了,但全局变量child仍对该节点有引用,所以导致该节点的内存一直无法被释放,可以尝试用Memory的快照功能来检测一下,如图所示





同样的先记录一下初始状态的快照,然后点击移除按钮后,再点击一次快照,此时内存大小我们看不出什么变化,因为移除的节点占用的内存实在太小了可以忽略不计,但我们可以点击第二条快照记录,在筛选框里输入detached,于是就会展示所有脱离了却又未被清除的节点对象



解决办法如下图所示:


<div id="root">
<div class="child">我是子元素div>
<button>移除button>
div>
<script>
let btn = document.querySelector('button')

btn.
addEventListener('click', function() {
let child = document.querySelector('.child')
let root = document.querySelector('#root')

root.
removeChild(child)
})

script>


改动很简单,就是将对.child节点的引用移动到了click事件的回调函数中,那么当移除节点并退出回调函数的执行上文后就会自动清除对该节点的引用,那么自然就不会存在内存泄漏的情况了,我们来验证一下,如下图所示:




结果很明显,这样处理过后就不存在内存泄漏的情况了


4. 控制台的打印


<button>按钮button>
<script>
document.querySelector('button').addEventListener('click', function() {
let obj = new Array(1000000)

console.log(obj);
})
script>

我们在按钮的点击回调事件中创建了一个很大的数组对象并打印,用performance来验证一下




开始录制,先触发一次垃圾回收清除初始的内存,然后点击三次按钮,即执行了三次点击事件,最后再触发一次垃圾回收。查看录制结果发现JS Heap曲线成阶梯上升,并且最终保持的高度比初始基准线高很多,这说明每次执行点击事件创建的很大的数组对象obj都因为console.log被浏览器保存了下来并且无法被回收



接下来注释掉console.log,再来看一下结果:


<button>按钮button>
<script>
document.querySelector('button').addEventListener('click', function() {
let obj = new Array(1000000)

// console.log(obj);
})
script>


可以看到没有打印以后,每次创建的obj都立马被销毁了,并且最终触发垃圾回收机制后跟初始的基准线同样高,说明已经不存在内存泄漏的现象了


其实同理 console.log也可以用Memory来进一步验证


未注释 console.log



注释掉了console.log




最后简单总结一下:在开发环境下,可以使用控制台打印便于调试,但是在生产环境下,尽可能得不要在控制台打印数据。所以我们经常会在代码中看到类似如下的操作:



// 如果在开发环境下,打印变量obj
if(isDev) {
console.log(obj)
}


这样就避免了生产环境下无用的变量打印占用一定的内存空间,同样的除了console.log之外,console.errorconsole.infoconsole.dir等等都不要在生产环境下使用



5. 遗忘的定时器



定时器也是平时很多人会忽略的一个问题,比如定义了定时器后就再也不去考虑清除定时器了,这样其实也会造成一定的内存泄漏。来看一个代码示例:



<button>开启定时器button>
<script>

function fn1() {
let largeObj = new Array(100000)

setInterval(() => {
let myObj = largeObj
},
1000)
}

document.querySelector('button').addEventListener('click', function() {
fn1()
})
script>

这段代码是在点击按钮后执行fn1函数,fn1函数内创建了一个很大的数组对象largeObj,同时创建了一个setInterval定时器,定时器的回调函数只是简单的引用了一下变量largeObj,我们来看看其整体的内存分配情况吧:



按道理来说点击按钮执行fn1函数后会退出该函数的执行上下文,紧跟着函数体内的局部变量应该被清除,但图中performance的录制结果显示似乎是存在内存泄漏问题的,即最终曲线高度比基准线高度要高,那么再用Memory来确认一次:




  • 在我们点击按钮后,从动态内存分配的图上看到出现一个蓝色柱形,说明浏览器为变量largeObj分配了一段内存,但是之后这段内存并没有被释放掉,说明的确存在内存泄漏的问题,原因其实就是因为setInterval的回调函数内对变量largeObj有一个引用关系,而定时器一直未被清除,所以变量largeObj的内存也自然不会被释放

  • 那么我们如何来解决这个问题呢,假设我们只需要让定时器执行三次就可以了,那么我们可以改动一下代码:


<button>开启定时器button>
<script>
function fn1() {
let largeObj = new Array(100000)
let index = 0

let timer = setInterval(() => {
if(index === 3) clearInterval(timer);
let myObj = largeObj
index ++
},
1000)
}

document.querySelector('button').addEventListener('click', function() {
fn1()
})
script>

现在我们再通过performancememory来看看还不会存在内存泄漏的问题



  • performance




这次的录制结果就能看出,最后的曲线高度和初始基准线的高度一样,说明并没有内存泄漏的情况




  • memory



这里做一个解释,图中刚开始出现的蓝色柱形是因为我在录制后刷新了页面,可以忽略;然后我们点击了按钮,看到又出现了一个蓝色柱形,此时就是为fn1函数中的变量largeObj分配了内存,3s后该内存又被释放了,即变成了灰色柱形。所以我们可以得出结论,这段代码不存在内存泄漏的问题



简单总结一下: 大家在平时用到了定时器,如果在用不到定时器后一定要清除掉,否则就会出现本例中的情况。除了setTimeoutsetInterval,其实浏览器还提供了一个API也可能就存在这样的问题,那就是requestAnimationFrame




  • 好了好了,学完了,ui妹妹我来了






  • ui妹妹:去你m的,滚远点





好了兄弟们,内存泄漏学会了吗?


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

面包会有的,玫瑰也会有的

前言 在杭州持续半个多月的阴雨中,迎来了“大火收汁”的7月🥵。转眼间这个2024年也过去了一半,我也从大学毕业做了2年的“来杭州讨饭的🐕”了。本来最近是有点忙的,7月底要疗休养,但是突然来了不少活,不过现在要等接口开发好,所以还是来做一下年中暨成为社畜两周年总...
继续阅读 »

前言


在杭州持续半个多月的阴雨中,迎来了“大火收汁”的7月🥵。转眼间这个2024年也过去了一半,我也从大学毕业做了2年的“来杭州讨饭的🐕”了。本来最近是有点忙的,7月底要疗休养,但是突然来了不少活,不过现在要等接口开发好,所以还是来做一下年中暨成为社畜两周年总结了。


减肥(膝盖要紧)


减不动了🤣,之前走路走太多了,把自己走出了滑膜炎,现在多走几千步膝盖就会疼(想想自己养成走路习惯还是因为买 huawei 手环下载的 APP 上面的成就奖牌,当时为了拿成就一天两万多步)。之前减肥是真的快啊,三四个月从 83kg 减到 68kg ,最近 4 个多月没怎么运动了,也就保持在 71kg 左右。家里人也不让我减了,说再减就难看了。


image.png


补牙(双连:一战成名)


去年体检被发现有了一颗蛀牙,今年才有时间去补,第一次体验补牙还挺新奇的(不过很快就不新奇了)。好在只蛀到一点点神经,上了点药阻断一下就补上了。原本以为这种事短时间内不会再体验了,结果 2 个月前在我品尝我的梅干菜肉饼时,我左上的一颗槽牙被神奇地磕掉了一小块😅......好嘛,再次喜提牙科医生的修补打磨。


工作学习(平平淡淡)


我这个小小前端每天也就是砌个div了,这半年没整太多新东西,就是基于公司的已有平台,加加feature、修修bug,把官网PC端和移动端换了一遍样式,现在官网到处是UI加的毛玻璃特效,在我这个 8G 内存小 thinkbook 机子的浏览器上肉眼可见的卡顿🫠(可能因为我的核显不行吧,我加了搜到的translateZ(0)也没体会到加速),没办法可能用户电脑都很强能带的动吧。


不过说了好几个月的基于 umi + qiankun 的新平台在下半年终于是要交付了,又重新过了一遍代码和开发流程。有一说一,真的很麻烦,整个平台根本就不大,完全想不通为什么要上微前端,我们目前开发团队算上外包同学也就3个前端了,而且后续可能就我一个人负责这个平台的前端开发和维护😣。


image.png


工作之外重温了一下之前了解过的 SolidJS、Svelte 和 Tauri,写了几个小 Demo
练练手:



3月份打算今年11月考软考高项的,但是刚看了一个月的书,就通知只有上半年考了🫤。我寻思报名费这么贵,两三个月准备时间岂不是做慈善?!那就明年5月份再考吧,下半年该把书啊什么的再拿出来看了。


吃喝玩乐(还得是家乡的味道)


5月份,带女朋友回了老家徐州玩。感觉物价比起过去涨了好多,尤其是节假日的酒店(不敢想要是高铁也在节假日涨价,我过年还回不回得起家),苏宁广场的绿茶餐厅比杭州in77的还贵。宝莲寺在最近徐州旅游火起来之前我都没听说过😹。羊肉串、菜煎饼、米线、蒸菜、冷面味道还是好吃的👍,不过在家待的几天都是出去下馆子,没怎么吃到家里人做的菜,饭馆基本都是除了几道特色菜好吃,其他都一般般。


fdd3809f1e627f989650a6ace0321cc.jpg


还得是过年时,家里做的好吃🫡。


a56ca2600e021e33c441fb639ff0992.jpg


再秀两张在宝莲寺的情侣照😎


4283b9a12a10226a65e8dbc2b21774d.jpg


展望


女朋友也毕业了,后面就是两个人在杭州打拼了。现在的工作其实挺好的,但是没有机会爬上去,爬不上去就没法在这个房价、物价如此离谱的城市留下。爸妈总是跟我说他们当初在一起的时候什么也没有,后面还是一起打拼出了这个家,面包总会有的。不过我想社会发展到现在,想要的也不一样了,面包要有,玫瑰也要有的吧。


0051bffa936b09233a1c0cba8a34bd4.jpg


hh,再写就要丧起来了。说不准哪天就中彩-票了,什么面包、玫瑰都不是事😇,全都做成鲜花饼😡,硌不坏牙的那种。


作者:HyaCinth
来源:juejin.cn/post/7386848746913366056
收起阅读 »

我是计算机专业研二的学生,我焦虑死了

最近两周,总有大三或研二的同学在微信上跟我说: “学长,我想在明年上半年的时候找个中大厂的实习,以及在秋招的时候拿个好点儿的offer。 但我现在没有项目经验,八股文才刚刚开始看,算法只会刷几十道简单和中等难度的,现在时间一天天过去,我自己并不在学习状态,感觉...
继续阅读 »

最近两周,总有大三或研二的同学在微信上跟我说:


“学长,我想在明年上半年的时候找个中大厂的实习,以及在秋招的时候拿个好点儿的offer。


但我现在没有项目经验,八股文才刚刚开始看,算法只会刷几十道简单和中等难度的,现在时间一天天过去,我自己并不在学习状态,感觉非常焦虑,甚至焦虑得晚上睡不着觉。”


其实,我非常理解同学们有这种焦虑状态,而形成这种状态的原因,我的分析如下。


恐惧来源于未知,而焦虑来自于不可控,大部分又焦虑又没有学习状态的同学,往往都有类似困惑:



  1. 我是双非学历,学了还有用吗?会不会大厂的简历筛选都不通过?

  2. 我现在开始学,一天10个小时这种节奏,还来得及吗?

  3. 我到底应该学习哪些东西,先后顺序是什么,这些东西需要学到什么程度才行?


而正是这种努力了也不一定更好,自己的命运无法掌控的感觉,让他们产生了不安全感和焦虑。


恰恰,大部分处于这种状态的同学,都是学习成绩和技术能力处于中等水平的。


因为按照现在这市场行情,学习成绩和技术能力处于下游的同学,早就已经放飞自我、彻底躺平了。


他们的想法是:毕业后的工作爱找成啥样就找成啥样呗,反正最后有个工作干着就行。现在这经济环境,就算努力了也大概率没什么好的结果,反而希望越大,失望就越大,再给自己累出一身病来,得不偿失。


而在各方面都比较优秀的同学,感到焦虑的人也不是很多,毕竟那份骨子里的自信和从容还是有的。


下面,我就给这类焦虑的同学支支招,用我自己思考的“双标一力”策略来化解这种负面情绪。


“双标一力”,即:制定目标 + 拆解目标 + 执行力。


制定目标


有句话是这样说的,梦想还是要有的,万一实现了呢?


但如果真的把不切实际的梦想作为目标,容易让自己整体的执行落地计划和动作变形,导致自己越来越焦虑,最终彻底摆烂。


BTW:这里所说的目标,是以校招生的身份,拿到心仪公司的offer。


举个例子,如果你是双非二本学历,技术能力也没牛逼到傲视群雄的地步,我不建议你把目标定为互联网大厂。


你可以把目标更切实际地定为,找个福利待遇中上等的公司,保证先上牌桌,再图发展。


如果你不知道如何给自己制定目标,我建议你采用“参照对标”的方式。


比如:你目前的学习成绩和技术储备在学校中排名前 30%的话,可以把去年前20%的学长学姐所去的公司作为目标。毕竟,去年和今年的招聘市场环境相差不大。


为什么今年前30%对去年的前20%呢?很简单,就是留一些空间让你去追赶的。


拆解目标


制定完目标后,你的焦虑感依然是存在的,还是觉得心里没底。


接下来就要看,你想要拿到目标公司的offer,都需要做哪些技术储备,以及需要储备到什么程度了。


举个例子,现在你想拿到互联网大厂的Java后端岗的offer,那以下几个方面的储备是缺一不可的。其中包括:



  1. Java技术栈的相关八股文(一期:Java、Spring生态、MyBatis、MySQL、Redis、JVM、操作系统、计网;二期:ES、MQ、Netty等)。

  2. 500+的算法题。

  3. 两段以上的项目经历,以及熟悉项目中对应的技术点。

  4. 至少3个月的大厂实习经历。


下面我们继续对其进行拆解,以此得出不同时间节点需要做完哪些事情。


我们按照秋招九月份开始,那如果具备上述至少3个月的大厂实习经历的话,那意味着最迟六月初就要去大厂开始实习了。


我们继续往前推算,如果6月份入职实习,那预留一个半月找实习的时间,是比较充裕的,也就是4月中旬。


接下来,我们再看项目、八股文和算法应该如何进行安排。


其中,项目和八股文是有先后顺序的,如果你在没练手过任何项目的情况下,直接去背八股文,那这个过程会非常痛苦。


但反过来,你没有储备任何八股文,但只要掌握Java基本语法和SpringBoot、MyBatis的用法,跟着黑马、尚硅谷、慕课的视频敲两个新手小白项目,那应该是不难的。


而刷算法这件事情,只要你大学期间有些数据结构和算法的底子,那直接开始即可。另外,算法储备会比较耗时,所以越早开始越好。


那整体的拆解路径也就出来了,我们假设从2023年的12月中旬开始,进行如下安排:



  1. 项目 + 算法双管齐下,以二月中旬为期限,在这两个月中,动手敲两个小项目 + 初刷200个算法题。

  2. 在二月中旬到三月中旬期间,以一期八股文为主 + 再储备100个算法题为辅。

  3. 在三月中旬到四月中旬,继续以一期八股文为主 + 简历中项目的技术点为主 + 二刷300道算法题为辅。

  4. 四月中旬到六月初,一期八股文 + 简历中项目的技术点 + 300道算法(三刷、四刷)三管齐下。如果拿到了实习offer,那马上调转方向,在入职实习前的几天里,all in两三个月没碰过的项目,这样到了公司能快速上手。

  5. 六月初到九月初,在公司里好好工作实习,争取可以有亮点经历写在简历上,并巩固好。另外八股文和算法不要停,继续扩充广度,即:一期、二期八股文 + 500算法题。


执行力


这点没什么好说的,干就完了。真正的聪明人,都喜欢心无旁骛地下笨功夫。


我相信,只要你能用心坚持一个月,看着自己之前的学习计划正在如期逐步落地,你的焦虑感就会变为成就感。


结语


希望我写的这些,能够对正在焦虑中的大三、研二的计算机专业同学有所帮助。


作者:托尼学长
来源:juejin.cn/post/7310147188252704787
收起阅读 »

前端超进化-小公司不用自研也能搞基建(全开源工具版)

蛮荒时代快看,这个男人叫小帅,他进了一家只有4个人的信息高科技有限公司,还妄想改变世界。前端只有一个人,所谓的发版,就是直接本地打包,然后代码通过ftp工具扔到服务器上,代码能跑就行。农耕时代这种不靠谱的开发模式进行了一段时间,直到某一天,ftp把服务器搞挂了...
继续阅读 »

蛮荒时代

快看,这个男人叫小帅,他进了一家只有4个人的信息高科技有限公司,还妄想改变世界。

前端只有一个人,所谓的发版,就是直接本地打包,然后代码通过ftp工具扔到服务器上,代码能跑就行。

image.png

农耕时代

这种不靠谱的开发模式进行了一段时间,直到某一天,ftp把服务器搞挂了的同时,本地的文件也丢了。所以他的leader小强意识到是时候需要改变了。

首先他们开始使用了git,代码不再是只存在本地,更是存在了云端。同时新建了dev/prod/maste分支来保证相对于各个环境的独立。 image.png

在某次把dev环境代码打包上传到生产后,为了区分环境和避免再次发生这种事故,小强也开始使用了在线打包工具jenkins, image.png

jenkins的引入也有效的避免了服务器账号外泄的风险,现在的服务器账号相对开发者是黑盒的。 image.png

手工时代

慢慢的,公司的生意做的越来越好,老板已经开始看奔驰了。

前端也从1个人加到了3个人,成立前端技术部,小帅觉得自己快要走上人生巅峰了。人加了3个,问题也越来越多。

第一个重要的问题就是,git分支总冲突。

因为以前没有制定git规范,所以大家很随意的在dev和master上开发和提交代码。

所以约定了git的相关规范,每次用feature分支作为开发分支,feature通过merge合并到dev和上线release分支。

同时使用pre-commit来规范提交的commit信息。

第二个问题,是代码风格的问题。

人多了之后,大家的代码风格不一致了。比如有的人是2空格党,有个是4空格党,有人组件用驼峰命名,有人用大驼峰。为了统一风格,于是决定使用使用统一的风格检查。

同时了为了避免一些低级的语法错误和dirty code,引入了eslint进行代码检查。

通过Prettier对代码风格进行统一和格式化,通过Husky来在提交前进行link检查。在提交后,通过github Ci来进行二次检查。

至此,在手工时代,前端团队完成了代码风格和基础的lint检查。

image.png

工业时代

老板的爸爸原来是顾总,看到儿子创业很开心,给儿子调了5个亿的现金,于是大家开始快马加鞭,突击项目。

前端人数也直接爆炸,来到了20+,各种各样的项目如火如荼的进行,于是各种解决问题的前端方案应运而生。

微前端 single spa/qiankun

20个人的开发量,在一个项目中,代码量爆炸,每次启动打包,都巨慢无比。同时,之前一些零散的项目也需要并入到这个项目中,但是目前主技术栈用的react,零散项目有个用vue,有的用angular。

为了解决这些问题,所以要开始微前端的改造。

整体项目采用了single spa,通过统一的框架底层,将各个不同的项目聚合在一起。

微前端改造后,将整体的大项目拆分成了各自独立的项目。同时各自独立的项目有自己的仓库,各自独立打包。在运行时,各独立项目的停止也不影响其他项目。

image.png

物料库

现在各个项目独立开来,拥有各自的仓库,所以每次有新模块的加入,需要新建仓库,所以模版物料应运而生。

模版物料 degit

类似Vue-cli creat-react 都有做模版物料,里面包含了一个git地址,通过git拉取模版物料。

模版物料里包含了一个项目所需的相关配置,例如打包工具,框架cli,框架主模块,eslint,prettier,示例代码等。每个项目直接进行npm安装即可开始。

我们可以使用gedit来达成此目标

npx degit [git地址]
# 例如 npx degit https://github.com/tinlee/1000-project-demo

业务组件物料 VitePress/storybook

有一些物料我们需要结合业务场景,比如省市区选择器,这种物料通常都具有统一的api,ui,可以用于多个项目的使用。在基础的ui组件的基础上,还结合了业务属性。

这类物料也需要有展示的站点,我们可以使用VitePress/storybook来展示,也可以结合类似Dumi等。这些文档工具都是支持markdown和组件的混用插入。

image.pngimage.png

npm私服 Sinopia

既然相关的业务组件库已经搭建完成,我们是希望不通过外网访问的,所以一个npm私服也必须要有。 这里通过Sinopia来进行私服的搭建。npm私服,在拉取的时候,检测当前服务器没有该包时会从npmjs获取,而上传的时候,只发到私服,而不会提交到npmjs。 image.png

文档工具 Outline

现在组件库很多,相关的技术文档很多,在以往的情况下,公司使用了腾讯/飞书等进行文档管理,但是很多内容不希望外部知道的内容。 所以使用Outline搭建了一个内部的文档工具。 image.png

Mock服务 YApi

以往前端和后端都口头约定,然后通过后端的swagger来生成文档。但是这存在一个问题,文档的生成滞后,前端没办法在正式接口之前进行mock。所以引入YApi,通过在Yapi上填写mock数据,进行数据的模拟。 image.png

jekins升级 docker/k8s

因为以往的构建,都是在一个项目中,现在微前端允许多框架,多环境构建。为了解决环境不统一的问题,引入了docker部署,对于多环境/多机器进行部署。

前端监控 sentry

Sentry 是一套开源的实时的异常收集、追踪、监控系统。这套解决方案由对应各种语言的 SDK 和一套庞大的数据后台服务组成,通过 Sentry SDK 的配置,还可以上报错误关联的版本信息、发布环境。同时 Sentry SDK 会自动捕捉异常发生前的相关操作,便于后续异常追踪。异常数据上报到数据服务之后,会通过过滤、关键信息提取、归纳展示在数据后台的 Web 界面中。 image.png

和平时代

基于上一次的工业大爆发,前端已经趋于稳定,公司业务也趋于稳定,老板的兰博已经停在了楼下。

在逐步的追求效率,追求速度的时期结束后,逐渐迎来了和平发展时期。这时候,团队开始关注自动化和创新。

灰度发布/一键回滚 k8s

各服务厂商都有提供灰度发布平台,结合服务商自己的服务可以对前端代码进行多环境,多场景,多条件的灰度部署及运维。

比如腾讯的服务网格,阿里的serverless等均有各种不同的服务。最基本的部署服务,就是根据k8s部署了多个不同的pod节点,然后根据规则匹配,对灰度环境进行的流量进行管理。 image.png

自动化测试 sonic

基于自动化测试的云平台,可以帮助每次执行真机的自动化测试,保证主流程的准确稳定。 image.png

性能监控/优化 Lighthouse/Sentry

chrome自带的Lighthouse可以进行本地性能的分析,同时搭配Sentry可以进行日志监控。 image.png

低代码/营销搭建 lowcode engine

公司一定时间之后一定需要一套做营销搭建和内部页面组织的工具,阿里开源的 lowcode engine可以在低代码领域进行快速的迭代。 image.png

AI客服 kimi/文心/通义

结合公司的文本资料库,将内容喂给Ai后,可以生成自己的Ai客服,解答一些常见的问题。

image.png

我是天元,立志做1000个有趣的项目的前端,公众号:前端cssandjs

如果你喜欢的话,请点赞,收藏,转发

归寂

已经跟随公司奋斗了几年的小帅,拿着合同终止通知书,看着老板的兰博大牛,觉得是时候应该向社会输送自己了。



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

一个迷茫的25岁前端程序员的自述

一直听说程序员的危机在 35 岁,没想到我的危机从 25 岁就开始了。 我甚至不知道自己是不是 25 岁,也可能是 26 岁,或者 27 岁,1998 年的生日,按照 2023 - 1998 的算法就是 25,按照我老家那边的算法就是 26,也有人说是 27 ...
继续阅读 »

一直听说程序员的危机在 35 岁,没想到我的危机从 25 岁就开始了。


我甚至不知道自己是不是 25 岁,也可能是 26 岁,或者 27 岁,1998 年的生日,按照 2023 - 1998 的算法就是 25,按照我老家那边的算法就是 26,也有人说是 27 ,无所谓了。


看着自己的掘金,上一次沉下心来好好的写一篇文章还是六个月前,那时候为了跳槽找工作,又写网站,又写文章的,过了那阵后来就不行了。


再往后稀稀拉拉的也在草稿箱里写了一些东西,东一榔头西一棒子,但没有一篇能看的,能发布出来的。


今年掘金社区的年终数据肯定不会好看,当初还想着要一年比一年强,结果一年快过去了了也没发布几篇文章,数据也都一般。


就连本文也是硬着头皮强迫自己写的,看看能不能借此调整调整心态。


也看过一些鸡汤文,像什么写给迷茫的xxx啊,写给年轻人啊,说实话没半点哔用,什么让你调整心态,自我提升,学好这个学好那个,我但凡要有那个劲,也不会迷茫了。


技术停滞不前


刚入行的时候,完成工作后剩下的时间,都会去看看技术视频啊,看看别人的博客啊,再或者看看一些三方库的文档和源码啊。


也可能是因为那会实在太菜,连工作都完成不了,就得被迫去提升基础能力。


再后来当了个小组长,虽然小组只有仨人,但是毕竟责任在,能给别人分配分配任务啦,给别人解决点小 bug 啦,再或者以组长的身份整两句啦,说实话稍微有那么一点点优越感,也就是靠着这一点点优越感,天天嚷嚷着要当一个架构师,这也学学那也学学,还看完了一个架构师教程,确实学到了不少东西,实际也用到了不少。


后来到北京,再次成为了一个底层的外包 coder, 每天除了听话和写业务代码也不用管别的。


后面公司又开始搞低代码,这回连代码也不用写了,就天天去系统上拖着玩,写配置,真心没意思。


说心里话,以我现在的能力吧,应付业务肯定是没有问题的,简单的页面基本不用思考随便写了,稍微复杂一点的动动脑筋,遇到困难就上网搜搜解决方案,问问 chatgpt,最不济实在搞不出来了,找个大佬问两句,最后也都能解决。


总结起来就是,能干活吗?能干。 能力强吗?不强。


技术文章收藏了不少,掘金小册也买了,视频也保存了一大堆,各种各样的。现在的状态就是看不下去,视频看一会就走神了,讲的什么完全记不得,还要后退回去再听一遍,文字更是看不进去。没有耐心去看文档,看不明白就开始烦了,觉得自己真傻逼的同时又不愿意去研究。


看视频的时候记不住,也不愿意记笔记。看文字的时候看不懂,也不愿意去实际操作一下。


现在把工作完成了,就摸鱼,看新闻,到处逛逛,刷短视频,真的不愿意学一点。


下班之后的时间就更不用说了,根本不愿意动一点脑子。


我有时以为是不是我生活的太好了,有吃有喝,没有压力。但转念一想现在如果给我大的压力,以我现在的心态能不能支撑的住。


但平时也经常想,自己就是一个破的被人看不起的外包,跟正式员工区别对待的外包,身边比我强的人比比皆是,那些月薪几万的正式员工,还有组长,组长的组长。前进的路很长,但就是不想走,不知道怎么走。


想学习吧,不知道从何学起,确定了要学习的内容吧,学不下去。


博客也不写了,开源仓库也不维护了,是躺平,亦或是摆烂,每天看着窗外发呆也占据了大部分时间。


生活百无聊赖


工作没有动力,生活亦是如此。


我现在工作的地方是北京顺义,住的地方就算是个村子,村子外面就是野外,没有一点城市的车水马龙和霓虹招展,一到黑天除了路灯照到的地方就是黑漆漆,没有广场,也没有广场舞。


每天下班回家就待在家里玩游戏,看电视剧。不知道会不会有人和我一样,玩游戏和看电视本应该是消遣娱乐的事情,但我感觉到的只有无聊,玩游戏的时候无聊,看电视剧的时候也一样。但你又不能什么都不干,你又得干点什么事情来让时间过度到需要睡觉的时候。


饭也不用自己做,公司的食堂一天三顿饭都有,早上要花钱买,一顿六七块钱左右,中午和晚上免费,不吃白不吃,每天六点下班,七点开饭,我都要在公司多摸一小时的鱼。也不用思考吃什么,在食堂供应的几种餐食里面选一个也不是什么困难的事情。


我这个人没有什么兴趣爱好,熬过上班的时间,就是玩游戏,看电视,看直播。也只是消磨时间,谈不上兴致勃勃。就没有上学时候宁可挨打也要偷摸去网吧通宵玩游戏的那个心境了。


其次,也没有什么社交,老家有几个朋友,平时在群里瞎聊两句,过年的时候一起吃个饭,也没别的了。之前在大连有几个朋友,现在也是极不频繁的闲聊两句。到北京以后呢,没朋友,没亲戚,自己一个人住单间,也没有室友。


也没有什么圈子,游戏基本上都是单排,就连游戏交流群都没加一个,有的时候发挥好了,别的人主动加我好友就一起玩两把。


也不爱运动,健身更是无稽之谈,每天最大的运动量就是从家骑自行车到公司,再从公司骑自行车回家,还是因为我不想早起挤公交。


平时极不愿意与陌生人打交道,路上碰见不太熟的都会装没看见。可以说是内向吧,也可以说社恐,甚至说我孤僻也没觉得有什么不妥。


但我一直都是这样吗,好像也不是,我大学时候是学院的辩论队教练,在数百人的场地里演讲,宣讲,打辩论赛,教学,做评委述票都迎刃有余,各个学院都有熟人,一块玩桌游,狼人杀,有很多朋友也爱交朋友。


似乎就是从毕业以后,又或许是分手以后,又或许是来到北京以后,短短的数月时间,感觉自己老了很多岁一样。


你问我是放不下她吗,好像也不是,平时也不怎么想,甚至连长什么样都记不清了。


那是一种什么感觉,大概就是放下她了,但没放下失去她吧。


假期回家,很多人说给我介绍对象,又期待又觉得无所谓,或许他们只是寒暄吧,也或许真的会介绍。不想,不愿意,又或者不耐烦去重新认识一个人。


假期在家的时候,以前不屑一顾的小夜市变成了我的一处乌托邦,太长时间没见过的市井气息,小吃摊前围绕的吃客,卖玩具的老板在尽力展示,还有踩高跷的表演,扭秧歌的队伍。还有一伙一伙的广场舞,我不跳,但我爱看,爱看大爷大妈们乐在其中的表情。


哪一瞬间开始觉得自己是个废物


我在朋友圈问过这个问题,得到的答案基本都是 “每一瞬间”,或许他们只是在说笑,也或许有的人真的这么想。但对我来说不是这样的。


我以前一直都自我感觉挺良好的。或者说自我感觉不错。


大学以前的成绩都是中等偏上水平,成绩还行,也拿奖状什么的,经常被夸奖。


大学也是一个普通本科,比上不足比下有余吧,虽然大学挂了很多科,但恰好赶上疫情,学校降低了毕业要求,顺利毕业了。


毕业以后找不到工作,交了 200 的定金准备去培训公司培训了,然后面试的最后一家告诉我通过了,我就没去培训。


边工作边学习,后来跳槽还当了个小组长,工资翻了接近一倍。


第一个对象在大一认识的,谈了一年。后来这个是疫情期间认识的,谈了三年。


家里条件一般,但是能吃饱饭,父母自给自足也不用我支援。


当时的我工作不愁,感情不愁,身体健康,生活没有压力。说自我感觉良好没有什么问题吧。




从小被灌输的概念都是近朱者赤近墨者黑,身边的人优秀才会激励你进步,但对我来说,身边优秀的人越多越容易摆烂。


以前在小公司当组长的时候更愿意去提升,现在当底层反而想躺平。


当你从最优秀的那些人中的一员成为最差的人中的一个,落差是很大的,体验感是十分差劲的。虽然工资翻了一倍,但生活变差了很多倍。


20 岁的时候单身,觉得无所谓,成天就是玩,30 岁的时候单身就会焦虑,担心自己能不能找到对象,或者说随着年龄越来越大,越担心能不能找到一个好女孩。


无论是分手,还是当外包,其实都还好,虽然说心情不太美妙,但一直也没对自己产生这么大的恶意。


直到国庆放假回家,我叔叔家有个小弟,比我小几岁。他的两个同学来接他同学聚会。俩孩子开着车,拿着伴手礼,奶,酒,还有水果。跟我叔叔我爸他们一起喝酒,言谈举止,人情世故活脱脱是个小大人,几杯白酒下肚啥事没有。来到陌生的村子跟几个陌生的人打麻将一点不紧张,输赢先不说,就这份勇气我就没有。明明我比他们要大几岁,可在他们面前我反而像个小孩子。


就在那一瞬间,我才意识到我是多么的没用,老话讲人比人得死,货比货得扔不是没有道理的,不需要别人来对比,自己都会自愧不如。


驾-照考了好几年了,却连车也不会开,要说买一个二手的便宜的车开,确实能买得起,但是不敢。


三十来岁的人了,别人都开着车到处跑,我还停留在父母去哪,我就跟着去哪的阶段。


我还给自己找借口,人家是租车公司的经理,每天跟各种人打交道,我就是个坐办公室的,没有什么历练。


怪性格也好,赖工作性质也罢。说白了就是没见过什么世面,没有什么阅历,三十来岁的年龄,十几岁孩子的见识。


说一句废物一点也不为过吧。


羡慕这两个字我已经说够了


大学玩的最好的朋友,现在在大连,经常跟我秀,自己有车,有房,有媳妇,媳妇有店,在要孩子。


再看看我自己,没车,没房,没媳妇,在村里租房子,在排位上分。


我虽然表面不屑一顾,说一句厉害坏了,但心里只有羡慕。


正式员工隔三差五发福利,半袖,外套,纪念品,外包什么都没有,只有羡慕。


国庆放假,没抢到高铁票,在公司拼了个车回去。


车是什么牌子,不知道,只知道看起来很好,应该不便宜。同事开车,闲聊中得知他比我小一岁,喊我哥,带着自己对象回家过节。


我坐在后排,闭着眼。坐着比我小一岁,领着对象回家过节,开着看起来不便宜的同事的车,心里只有羡慕。


回家吃婚宴,看着布置好的新房和现场的典礼,没有心思截门要红包,心里只有羡慕。


我从来不羡慕那些有钱人,富二代,也不羡慕那些天才,超能力,我觉得他们离我太远,跟我没有关系。但我真的羡慕明明跟我差不多,但是比我懂事的孩子。


羡慕这两个字,我已经说够了。


写在最后


现在的心态就是活着没意思,又不敢死。


说到底,我现在的生活已经比一部分人要好了,我也知道有很多人羡慕我这样的生活,也有很多不如我的人心态比我要阳光,要积极。


或许我是在无病呻吟,或许我是生在福中不知福,又或许我是闲的没事,但我只是把我内心的真实想法表达出来而已,我无意伤害任何人,包括我自己。


本来想在最后贴上自己的开源项目地址,要一些 star 的,想想还是算了。


就希望自己能快些振作起来,让生活走上正轨吧。


至于这要花费多长时间,我也不得而知,希望很快吧。


作者:一尾流莺
来源:juejin.cn/post/7288300174913159222
收起阅读 »

一种适合H5屏幕适配方案

web
一、动态rem适配方案:适合H5项目的适配方案 1. @media媒体查询适配 首先,我们需要设置一个根元素的基准值,这个基准值通常根据视口宽度进行计算。可以在项目的 CSS 文件中,通过媒体查询动态调整根元素的 font-size。 html { fon...
继续阅读 »

一、动态rem适配方案:适合H5项目的适配方案


1. @media媒体查询适配


首先,我们需要设置一个根元素的基准值,这个基准值通常根据视口宽度进行计算。可以在项目的 CSS 文件中,通过媒体查询动态调整根元素的 font-size


html {
font-size: 16px; /* 默认基准值 */
}
...
@media (min-width: 1024px) {
html {
font-size: 14px; /* 适配较大屏幕 */
}
}
@media (min-width: 1440px) {
html {
font-size: 16px; /* 适配超大屏幕 */
}
}

2. PostCSS 插件(自动转换)实现 px2rem


手动转换 pxrem 可能很繁琐,因此可以使用 PostCSS 插件 postcss-pxtorem 来自动完成这一转换。


2.1 安装 postcss-pxtorem


首先,在项目中安装 postcss-pxtorem 插件:


npm install postcss-pxtorem --save-dev

2.2 配置 PostCSS


然后,在项目根目录创建或编辑 postcss.config.js 文件,添加 postcss-pxtorem 插件配置:


/* postcss.config.cjs  */
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue: 16, // 基准值,对应于根元素的 font-size
unitPrecision: 5, // 保留小数点位数
propList: ['*', '!min-width', '!max-width'], // 排除 min-width 和 max-width 属性
selectorBlackList: [], // 忽略的选择器
replace: true, // 替换而不是添加备用属性
mediaQuery: false, // 允许在媒体查询中转换 px
minPixelValue: 0 // 最小的转换数值
}
}
};
/* vite */
export default defineConfig({
css: {
postcss: './postcss.config.cjs',
}
})

3. 在 CSS/SCSS 中使用 px


在编写样式时,依然可以使用 px 进行布局:


.container {
width: 320px;
padding: 16px;
}

.header {
height: 64px;
margin-bottom: 24px;
}

4. 构建项目


通过构建工具(如 webpack/vite )运行项目时,PostCSS 插件会自动将 px 转换为 rem


image-20240613170746376

5. 可以不用@media媒体查询,动态动态调整font-size


为了实现更动态的适配,可以通过 JavaScript 动态设置根元素的 font-size


/**utils/setRootFontSize**/
function setRootFontSize(): void {
const docEl = document.documentElement;
const clientWidth = docEl.clientWidth;
if (!clientWidth) return;
const baseFontSize = 16; // 基准字体大小
const designWidth = 1920; // 设计稿宽度
docEl.style.fontSize = (baseFontSize * (clientWidth / designWidth)) + 'px';
}
export default setRootFontSize;

/**utils/setRootFontSize**/
/**APP**/
import setRootFontSize from '../utils/setRootFontSize';
import { useEffect } from 'react';

export default function App() {
useEffect(() => {
// 设置根元素的字体大小
setRootFontSize();
// 窗口大小改变时重新设置
window.addEventListener('resize', setRootFontSize);
// 清除事件监听器
return () => {
window.removeEventListener('resize', setRootFontSize);
};
}, []);

return (
<>
<div>
<MyRoutes />
</div>
</>

)
}
/**APP**/

这样,无论视口宽度如何变化,页面元素都会根据基准值动态调整大小,确保良好的适配效果。
通过上述步骤,可以实现布局使用 px,并动态转换为 rem 的适配方案。这个方案不仅使得样式编写更加简洁,还提高了适配的灵活性。


注:如果你使用了 setRootFontSize 动态调整根元素的 font-size,就不再需要使用 @media 查询来调整根元素的字体大小了。这是因为 setRootFontSize 函数已经根据视口宽度动态调整了 font-size,从而实现了自适应。



  1. 动态调整根元素 font-size 的优势

    • 更加灵活:可以实现更加平滑的响应式调整,而不是依赖固定的断点。

    • 统一管理:所有的样式都依赖根元素的 font-size,维护起来更加简单。



  2. @media 媒体查询的优势

    • 尽管不再需要用 @media 查询来调整根元素的 font-size,但你可能仍然需要使用 @media 查询来处理其他的响应式设计需求,比如调整布局、隐藏或显示元素等。




这种方式简化了响应式设计,使得样式统一管理更加简单,同时保留了灵活性和适应性。


6. 效果对比(非H5界面)


图一为界面px 适配,效果为图片,文字等大小固定不变。


图二为动态rem适配:整体随界面扩大而扩大,能够保持相对比例。


Screen-2024-06-13-155704-ezgif.com-video-to-gif-converter


t11b673bcd6119f4e6a5e9509cf


7. Tips



  • 动态rem此方案比较适合H5屏幕适配

  • 注意: PostCSS 转换rem应排除 min-widthmax-widthmin-heightmax-height ,以免影响整体界面


二、其他适配


1. 弹性盒模型(Flexbox)


Flexbox 是一种布局模型,能够轻松地实现响应式布局。它允许元素根据容器的大小自动调整位置和大小。


.container {
display: flex;
flex-wrap: wrap;
}

.item {
flex: 1 1 100%; /* 默认情况下每个元素占满一行 */
}

@media (min-width: 600px) {
.item {
flex: 1 1 50%; /* 在较宽的屏幕上,每个元素占半行 */
}
}

@media (min-width: 1024px) {
.item {
flex: 1 1 33.33%; /* 在更宽的屏幕上,每个元素占三分之一行 */
}
}

2. 栅格系统(Grid System)


栅格系统是一种常见的响应式布局方案,广泛应用于各种框架(如 Bootstrap)。通过定义行和列,可以轻松地创建复杂的布局。


.container {
display: grid;
grid-template-columns: 1fr; /* 默认情况下每行一个列 */
gap: 10px;
}

@media (min-width: 600px) {
.container {
grid-template-columns: 1fr 1fr; /* 在较宽的屏幕上,每行两个列 */
}
}

@media (min-width: 1024px) {
.container {
grid-template-columns: 1fr 1fr 1fr; /* 在更宽的屏幕上,每行三个列 */
}
}

3. 百分比和视口单位


使用百分比(%)、视口宽度(vw)、视口高度(vh)等单位,可以根据视口尺寸调整元素大小。


  /* 示例:百分比和视口单位 */
.container {
width: 100%;
height: 50vh; /* 高度为视口高度的一半 */
}

.element {
width: 50%; /* 宽度为容器的一半 */
height: 10vw; /* 高度为视口宽度的 10% */
}

4. 响应式图片


根据设备分辨率和尺寸加载不同版本的图片,以提高性能和视觉效果。可以使用 srcset 和 sizes 属性。


  <!-- 示例:响应式图片 -->
<img
src="small.jpg"
srcset="medium.jpg 600w, large.jpg 1024w"
sizes="(max-width: 600px) 100vw, (max-width: 1024px) 50vw, 33.33vw"
alt="Responsive Image">


5. CSS Custom Properties(CSS变量)


使用 CSS 变量可以更灵活地定义和调整样式,同时通过 JavaScript 动态改变变量值实现响应式设计。


:root {
--main-padding: 20px;
}

.container {
padding: var(--main-padding);
}

@media (min-width: 600px) {
:root {
--main-padding: 40px;
}
}

作者:奇舞精选
来源:juejin.cn/post/7384265691162886178
收起阅读 »

利用高德地图API实现实时天气

web
前言 闲来无事,利用摸鱼时间实现实时天气的小功能 目录 登录 高德开放平台控制台,如果没有开发者账号,请 注册开发者。 创建 key,进入应用管理,创建新应用,新应用中添加 key,服务平台选择 Web端(JS API)。 获取 key 和密钥 获取当前...
继续阅读 »

前言



闲来无事,利用摸鱼时间实现实时天气的小功能



目录



  1. 登录 高德开放平台控制台,如果没有开发者账号,请 注册开发者

  2. 创建 key,进入应用管理,创建新应用,新应用中添加 key,服务平台选择 Web端(JS API)。

  3. 获取 key 和密钥

  4. 获取当前城市定位

  5. 通过定位获取城市名称、区域编码,查询目标城市/区域的实时天气状况


效果图


这里样式我就不做处理了,地图可以不用做展示,只需要拿到获取到天气的结果,结合自己的样式展示就可以了,未来天气可以结合echarts进行展示,页面效果更佳


image.png


实现



  1. 登录高德开放平台控制台
    image.png

  2. 创建 key



这里应用名称可以随便取(个人建议功能名称或者项目称)



image.png


image.png


3.获取 key 和密钥


image.png


4.获取当前城市定位



首先,先安装依赖



npm install @amap/amap-jsapi-loader --save


或者


pnpm add @amap/amap-jsapi-loader --save



页面使用时引入即可 import AMapLoader from "@amap/amap-jsapi-loader"




/**在index.html引入密钥,不添加会导致某些API调用不成功*/

<script type="text/javascript">window._AMapSecurityConfig =
{securityJsCode: "安全密钥"}</script>

  /** 1.  调用AMapLoader.load方法,通过传入一个对象作为参数来指定加载地图时的配置信息。
* - key: 申请好的Web端开发者Key,是必填项,用于授权您的应用程序使用高德地图API。
* - version: 指定要加载的JSAPI版本,不指定时默认为1.4.15。
* - plugins: 需要使用的插件列表,如比例尺、缩放控件等。
*/

function initMap() {
AMapLoader.load({
key: "申请好的Web端开发者Key", // 申请好的Web端开发者Key,首次调用 load 时必填
version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
plugins: [
"AMap.ToolBar",
"AMap.Scale",
"AMap.HawkEye",
"AMap.MapType",
"AMap.Geolocation",
"AMap.Geocoder",
"AMap.Weather",
"AMap.CitySearch",
"AMap.InfoWindow",
"AMap.Marker",
"AMap.Pixel",
], // 需要使用的的插件列表,如比例尺'AMap.Scale'等
})
.then((AMap) => {
map.value = new AMap.Map("container", {
//设置地图容器id
resizeEnable: true,
viewMode: "3D", //是否为3D地图模式
zoom: 10, //初始化地图级别
center: locationArr.value, //初始化地图中心点位置
});

getGeolocation(AMap);
getCitySearch(AMap, map.value);
})
.catch((e) => {
console.log(e);
});
}

// 浏览器定位
const getGeolocation = (AMap: any) => {
const geolocation = new AMap.Geolocation({
enableHighAccuracy: true, //是否使用高精度定位,默认:true
timeout: 1000, //超过10秒后停止定位,默认:5s
position: "RB", //定位按钮的停靠位置
offset: [10, 20], //定位按钮与设置的停靠位置的偏移量,默认:[10, 20]
zoomToAccuracy: true, //定位成功后是否自动调整地图视野到定位点
});
map.value.addControl(geolocation);
geolocation.getCurrentPosition(function (status: string, result: any) {
if (status == "complete") {
onComplete(result);
} else {
onError(result);
}
});
};

// IP定位获取当前城市信息
const getCitySearch = (AMap: any, map: any) => {
const citySearch = new AMap.CitySearch();
citySearch.getLocalCity(function (
status: string,
result: {
city: string;
info: string;
}
) {
if (status === "complete" && result.info === "OK") {
console.log(
"🚀 ~ file: map-container.vue:88 ~ getCitySearch ~ result:",
result
);
// 查询成功,result即为当前所在城市信息
getWeather(AMap, map, result.city);
}
});
};


onMounted(() => {
initMap();
});

5.通过定位获取城市名称、区域编码,查询目标城市/区域的实时天气状况


const getWeather = (AMap: any, map: any, city: string) => {
const weather = new AMap.Weather();
weather.getLive(
city,
function (
err: any,
data: {
city: string;
weather: string;
temperature: string;
windDirection: string;
windPower: string;
humidity: string;
reportTime: string;
}
) {
if (!err) {
const str = [];
str.push("<h4 >实时天气" + "</h4><hr>");
str.push("<p>城市/区:" + data.city + "</p>");
str.push("<p>天气:" + data.weather + "</p>");
str.push("<p>温度:" + data.temperature + "℃</p>");
str.push("<p>风向:" + data.windDirection + "</p>");
str.push("<p>风力:" + data.windPower + " 级</p>");
str.push("<p>空气湿度:" + data.humidity + "</p>");
str.push("<p>发布时间:" + data.reportTime + "</p>");
const marker = new AMap.Marker({
map: map,
position: map.getCenter(),
});
const infoWin = new AMap.InfoWindow({
content:
'<div class="info" style="position:inherit;margin-bottom:0;background:#ffffff90;padding:10px">' +
str.join("") +
'</div><div class="sharp"></div>',
isCustom: true,
offset: new AMap.Pixel(0, -37),
});
infoWin.open(map, marker.getPosition());
marker.on("mouseover", function () {
infoWin.open(map, marker.getPosition());
});
}
}
);
}

完整代码


 <template>
<div id="container"></div>
</template>

<script setup lang="ts">
import AMapLoader from "@amap/amap-jsapi-loader";
import { ref, onMounted, watch, reactive } from "vue";
const props = defineProps({
search: {
type: String,
default: "杭州市",
},
});

const isFalse = ref(false);

const map = ref<any>(null);
let locationArr = ref<any>();
watch(
() => props.search,
(newValue) => {
console.log("search", newValue);
initMap();
}
);
function initMap() {
AMapLoader.load({
key: "申请好的Web端开发者Key", // 申请好的Web端开发者Key,首次调用 load 时必填
version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
plugins: [
"AMap.ToolBar",
"AMap.Scale",
"AMap.HawkEye",
"AMap.MapType",
"AMap.Geolocation",
"AMap.Geocoder",
"AMap.Weather",
"AMap.CitySearch",
"AMap.InfoWindow",
"AMap.Marker",
"AMap.Pixel",
], // 需要使用的的插件列表,如比例尺'AMap.Scale'等
})
.then((AMap) => {
map.value = new AMap.Map("container", {
//设置地图容器id
resizeEnable: true,
viewMode: "3D", //是否为3D地图模式
zoom: 10, //初始化地图级别
center: locationArr.value, //初始化地图中心点位置
});

getGeolocation(AMap);
getCitySearch(AMap, map.value);
})
.catch((e) => {
console.log(e);
});
}

// 浏览器定位
const getGeolocation = (AMap: any) => {
const geolocation = new AMap.Geolocation({
enableHighAccuracy: true, //是否使用高精度定位,默认:true
timeout: 1000, //超过10秒后停止定位,默认:5s
position: "RB", //定位按钮的停靠位置
offset: [10, 20], //定位按钮与设置的停靠位置的偏移量,默认:[10, 20]
zoomToAccuracy: true, //定位成功后是否自动调整地图视野到定位点
});
map.value.addControl(geolocation);
geolocation.getCurrentPosition(function (status: string, result: any) {
if (status == "complete") {
onComplete(result);
} else {
onError(result);
}
});
};

// IP定位获取当前城市信息
const getCitySearch = (AMap: any, map: any) => {
const citySearch = new AMap.CitySearch();
citySearch.getLocalCity(function (
status: string,
result: {
city: string;
info: string;
}
) {
if (status === "complete" && result.info === "OK") {
console.log(
"🚀 ~ file: map-container.vue:88 ~ getCitySearch ~ result:",
result
);
// 查询成功,result即为当前所在城市信息
getWeather(AMap, map, result.city);
}
});
};

// 天气
const getWeather = (AMap: any, map: any, city: string) => {
const weather = new AMap.Weather();
weather.getLive(
city,
function (
err: any,
data: {
city: string;
weather: string;
temperature: string;
windDirection: string;
windPower: string;
humidity: string;
reportTime: string;
}
) {
console.log("🚀 ~ file: map-container.vue:96 ~ .then ~ data:", data);
if (!err) {
const str = [];
str.push("<h4 >实时天气" + "</h4><hr>");
str.push("<p>城市/区:" + data.city + "</p>");
str.push("<p>天气:" + data.weather + "</p>");
str.push("<p>温度:" + data.temperature + "℃</p>");
str.push("<p>风向:" + data.windDirection + "</p>");
str.push("<p>风力:" + data.windPower + " 级</p>");
str.push("<p>空气湿度:" + data.humidity + "</p>");
str.push("<p>发布时间:" + data.reportTime + "</p>");
const marker = new AMap.Marker({
map: map,
position: map.getCenter(),
});
const infoWin = new AMap.InfoWindow({
content:
'<div class="info" style="position:inherit;margin-bottom:0;background:#ffffff90;padding:10px">' +
str.join("") +
'</div><div class="sharp"></div>',
isCustom: true,
offset: new AMap.Pixel(0, -37),
});
infoWin.open(map, marker.getPosition());
marker.on("mouseover", function () {
infoWin.open(map, marker.getPosition());
});
}
}
);

// 未来4天天气预报
weather.getForecast(
city,
function (err: any, data: { forecasts: string | any[] }) {
console.log(
"🚀 ~ file: map-container.vue:186 ~ getWeather ~ data:",
data
);

if (err) {
return;
}
var strs = [];
for (var i = 0, dayWeather; i < data.forecasts.length; i++) {
dayWeather = data.forecasts[i];
strs.push(
`<p>${dayWeather.date}&nbsp&nbsp${dayWeather.dayWeather}&nbsp&nbsp${dayWeather.nightTemp}~${dayWeather.dayTemp}℃</p><br />`
);
}
}
);
};

function onComplete(data: any) {
console.log("🚀 ~ file: map-container.vue:107 ~ onComplete ~ data:", data);
const lngLat = [data.position.lng, data.position.lat];
locationArr.value = lngLat;
}

function onError(data: any) {
console.log("🚀 ~ file: map-container.vue:113 ~ onError ~ data:", data);
// 定位出错
}

onMounted(() => {
initMap();
});
</script>

<style scoped lang="less">
#container {
padding: 0px;
margin: 0px;
width: 100%;
height: 100%;
}
</style>


作者:快乐是Happy
来源:juejin.cn/post/7316746866040619035
收起阅读 »

MySQL 高级(进阶)SQL 语句

MySQL 高级(进阶)SQL 语句 1. MySQL SQL 语句 1.1 常用查询 常用查询简单来说就是 增、删、改、查 对 MySQL 数据库的查询,除了基本的查询外,有时候需要对查询的结果集进行处理。 例如只取 10 条数据、对查询结果进行排序或分组等...
继续阅读 »

MySQL 高级(进阶)SQL 语句


1. MySQL SQL 语句


1.1 常用查询


常用查询简单来说就是 增、删、改、查


对 MySQL 数据库的查询,除了基本的查询外,有时候需要对查询的结果集进行处理。 例如只取 10 条数据、对查询结果进行排序或分组等等


1、按关键字排序
PS:类比于windows 任务管理器
使用 SELECT 语句可以将需要的数据从 MySQL 数据库中查询出来,如果对查询的结果进行排序,可以使用 ORDER BY 语句来对语句实现排序,并最终将排序后的结果返回给用户。这个语句的排序不光可以针对某一个字段,也可以针对多个字段


(1)语法
SELECT column1, column2, … FROM table_name ORDER BY column1, column2, …


ASC|DESC
ASC 是按照升序进行排序的,是默认的排序方式,即 ASC 可以省略。SELECT 语句中如果没有指定具体的排序方式,则默认按 ASC方式进行排序。


DESC 是按降序方式进 行排列。当然 ORDER BY 前面也可以使用 WHERE 子句对查询结果进一步过滤。


准备工作:


create database k1;
use k1;
create table location (Region char(20),Store_Name char(20));
insert int0 location values('East','Boston');
insert int0 location values('East','New York');
insert int0 location values('West','Los Angeles');
insert int0 location values('West','Houston');

create table store_info (Store_Name char(20),Sales int(10),Date char(10));
insert int0 store_info values('Los Angeles','1500','2020-12-05');
insert int0 store_info values('Houston','250','2020-12-07');
insert int0 store_info values('Los Angeles','300','2020-12-08');
insert int0 store_info values('Boston','700','2020-12-08');

1.2 SELECT


显示表格中一个或数个字段的所有数据记录 语法:SELECT "字段" FROM "表名";


SELECT Store_Name FROM location;

d1.png


SELECT Store_Name FROM Store_Info;

d2.png


1.3 DISTINCT


不显示重复的数据记录


语法:SELECT DISTINCT "字段" FROM "表名";


SELECT DISTINCT Store_Name FROM Store_Info;

d3.png


1.4 AND OR


且 或


语法:SELECT "字段" FROM "表名" WHERE "条件1" {[AND|OR] "条件2"}+ ;


d4.png


1.5 in


显示已知的值的数据记录


语法:SELECT "字段" FROM "表名" WHERE "字段" IN ('值1', '值2', ...);


SELECT * FROM store_info WHERE Store_Name IN ('Los Angeles', 'Houston');

b5.png


1.6 BETWEEN


显示两个值范围内的数据记录


语法:SELECT "字段" FROM "表名" WHERE "字段" BETWEEN '值1' AND '值2';


d6.png


2. 通配符 —— 通常与 LIKE 搭配 一起使用



% :百分号表示零个、一个或多个字符


_ :下划线表示单个字符


'A_Z':所有以 'A' 起头,另一个任何值的字符,且以 'Z' 为结尾的字符串。例如,'ABZ' 和 'A2Z' 都符合这一个模式,而 'AKKZ' 并不符合 (因为在 A 和 Z 之间有两个字符,而不是一个字符)。


'ABC%': 所有以 'ABC' 起头的字符串。例如,'ABCD' 和 'ABCABC' 都符合这个模式。 '%XYZ': 所有以 'XYZ' 结尾的字符串。例如,'WXYZ' 和 'ZZXYZ' 都符合这个模式。


'%AN%': 所有含有 'AN'这个模式的字符串。例如,'LOS ANGELES' 和 'SAN FRANCISCO' 都符合这个模式。


'_AN%':所有第二个字母为 'A' 和第三个字母为 'N' 的字符串。例如,'SAN FRANCISCO' 符合这个模式,而 'LOS ANGELES' 则不符合这个模式。



2.1 LIKE


匹配一个模式来找出我们要的数据记录


语法:SELECT "字段" FROM "表名" WHERE "字段" LIKE {模式};


SELECT * FROM store_info WHERE Store_Name like '%os%';

d7.png


2.2 ORDER BY


按关键字排序


语法:SELECT "字段" FROM "表名" [WHERE "条件"] ORDER BY "字段" [ASC, DESC];


注意ASC 是按照升序进行排序的,是默认的排序方式。 DESC 是按降序方式进行排序


SELECT Store_Name,Sales,Date FROM store_info ORDER BY Sales DESC;

d8.png


3. 函数


3.1数学函数


abs(x)返回 x 的绝对值
rand()返回 0 到 1 的随机数
mod(x,y)返回 x 除以 y 以后的余数
power(x,y)返回 x 的 y 次方
round(x)返回离 x 最近的整数
round(x,y)保留 x 的 y 位小数四舍五入后的值
sqrt(x)返回 x 的平方根
truncate(x,y)返回数字 x 截断为 y 位小数的值
ceil(x)返回大于或等于 x 的最小整数
floor(x)返回小于或等于 x 的最大整数
greatest(x1,x2...)返回集合中最大的值,也可以返回多个字段的最大的值
least(x1,x2...)返回集合中最小的值,也可以返回多个字段的最小的值

SELECT abs(-1), rand(), mod(5,3), power(2,3), round(1.89);
SELECT round(1.8937,3), truncate(1.235,2), ceil(5.2), floor(2.1), least(1.89,3,6.1,2.1);

d9.png


3.2 聚合函数


avg()返回指定列的平均值
count()返回指定列中非 NULL 值的个数
min()返回指定列的最小值
max()返回指定列的最大值
sum(x)返回指定列的所有值之和

SELECT avg(Sales) FROM store_info;

SELECT count(Store_Name) FROM store_info;
SELECT count(DISTINCT Store_Name) FROM store_info;

SELECT max(Sales) FROM store_info;
SELECT min(Sales) FROM store_info;

SELECT sum(Sales) FROM store_info;

d10.png


d11.png


3.3 字符串函数


trim()返回去除指定格式的值
concat(x,y)将提供的参数 x 和 y 拼接成一个字符串
substr(x,y)获取从字符串 x 中的第 y 个位置开始的字符串,跟substring()函数作用相同
substr(x,y,z)获取从字符串 x 中的第 y 个位置开始长度为 z 的字符串
length(x)返回字符串 x 的长度
replace(x,y,z)将字符串 z 替代字符串 x 中的字符串 y
upper(x)将字符串 x 的所有字母变成大写字母
lower(x)将字符串 x 的所有字母变成小写字母
left(x,y)返回字符串 x 的前 y 个字符
right(x,y)返回字符串 x 的后 y 个字符
repeat(x,y)将字符串 x 重复 y 次
space(x)返回 x 个空格
strcmp(x,y)比较 x 和 y,返回的值可以为-1,0,1
reverse(x)将字符串 x 反转

d12.png


如 sql_mode 开启了 PIPES_AS_CONCAT,"||" 视为字符串的连接操作符而非或运算符,和字符串的拼接函数Concat相类似,这和Oracle数据库使用方法一样的


SELECT Region || ' ' || Store_Name FROM location WHERE Store_Name = 'Boston';
SELECT substr(Store_Name,3) FROM location WHERE Store_Name = 'Los Angeles';
SELECT substr(Store_Name,2,4) FROM location WHERE Store_Name = 'New York'

d13.png


SELECT TRIM ([ [位置] [要移除的字符串] FROM ] 字符串);


**[位置]:的值可以为 LEADING (起头), TRAILING (结尾), BOTH (起头及结尾)。 **


[要移除的字符串]:从字串的起头、结尾,或起头及结尾移除的字符串。缺省时为空格。


SELECT TRIM(LEADING 'Ne' FROM 'New York');

SELECT Region,length(Store_Name) FROM location;

SELECT REPLACE(Region,'ast','astern')FROM location;

d14.png


4. GR0UP BY


对GR0UP BY后面的字段的查询结果进行汇总分组,通常是结合聚合函数一起使用的


GR0UP BY 有一个原则



  • 凡是在 GR0UP BY 后面出现的字段,必须在 SELECT 后面出现;

  • 凡是在 SELECT 后面出现的、且未在聚合函数中出现的字段,必须出现在 GR0UP BY 后面


语法:SELECT "字段1", SUM("字段2") FROM "表名" GR0UP BY "字段1";


SELECT Store_Name, SUM(Sales) FROM store_info GR0UP BY Store_Name ORDER BY sales desc;

d15.png


5. 别名


字段別名 表格別名


语法:SELECT "表格別名"."字段1" [AS] "字段別名" FROM "表格名" [AS] "表格別名";


SELECT A.Store_Name Store, SUM(A.Sales) "Total Sales" FROM store_info A GR0UP BY A.Store_Name;

d16.png


6. 子查询


子查询也被称作内查询或者嵌套查询,是指在一个查询语句里面还嵌套着另一个查询语 句。子查询语句是先于主查询语句被执行的,其结果作为外层的条件返回给主查询进行下一 步的查询过滤


连接表格,在WHERE 子句或 HAVING 子句中插入另一个 SQL 语句


语法:SELECT "字段1" FROM "表格1" WHERE "字段2" [比较运算符] #外查询 (SELECT "字段1" FROM "表格2" WHERE "条件"); #内查询


[比较运算符]


可以是符号的运算符,例如 =、>、<、>=、<= ;也可以是文字的运算符,例如 LIKE、IN、BETWEEN


SELECT SUM(Sales) FROM store_info WHERE Store_Name IN
(SELECT Store_Name FROM location WHERE Region = 'West');

SELECT SUM(A.Sales) FROM store_info A WHERE A.Store_Name IN
(SELECT Store_Name FROM location B WHERE B.Store_Name = A.Store_Name);

d17.png


7. EXISTS


用来测试内查询有没有产生任何结果,类似布尔值是否为真 #如果有的话,系统就会执行外查询中的SQL语句。若是没有的话,那整个 SQL 语句就不会产生任何结果。


语法:SELECT "字段1" FROM "表格1" WHERE EXISTS (SELECT \* FROM "表格2" WHERE "条件");


SELECT SUM(Sales) FROM store_info WHERE EXISTS (SELECT * FROM location WHERE Region = 'West');

d18.png


8. 连接查询


准备工作


create database k1;
use k1;
create table location (Region char(20),Store_Name char(20));
insert int0 location values('East','Boston');
insert int0 location values('East','New York');
insert int0 location values('West','Los Angeles');
insert int0 location values('West','Houston');

create table store_info (Store_Name char(20),Sales int(10),Date char(10));
insert int0 store_info values('Los Angeles','1500','2020-12-05');
insert int0 store_info values('Houston','250','2020-12-07');
insert int0 store_info values('Los Angeles','300','2020-12-08');
insert int0 store_info values('Boston','700','2020-12-08');

f1.png


UPDATE store_info SET store_name='Washington' WHERE sales=300;

f2.png


inner join(内连接):只返回两个表中联结字段相等的行


left join(左连接):返回包括左表中的所有记录和右表中联结字段相等的记录


right join(右连接):返回包括右表中的所有记录和左表中联结字段相等的记录


f3.png


8.1 内连接


MySQL 中的内连接就是两张或多张表中同时符合某种条件的数据记录的组合。通常在 FROM 子句中使用关键字 INNER JOIN 来连接多张表,并使用 ON 子句设置连接条件,内连接是系统默认的表连接,所以在 FROM 子句后可以省略 INNER 关键字,只使用 关键字 JOIN。同时有多个表时,也可以连续使用 INNER JOIN 来实现多表的内连接,不过为了更好的性能,建议最好不要超过三个表


(1) 语法 求交集


SELECT column_name(s)FROM table1 INNER JOIN table2 ON table1.column_name = table2.column_name;

SELECT * FROM location A INNER JOIN store_info B on A.Store_Name = B.Store_Name ;

f4.png


内连查询:通过inner join 的方式将两张表指定的相同字段的记录行输出出来


8.2 左连接


左连接也可以被称为左外连接,在 FROM 子句中使用 LEFT JOIN 或者 LEFT OUTER JOIN 关键字来表示。左连接以左侧表为基础表,接收左表的所有行,并用这些行与右侧参 考表中的记录进行匹配,也就是说匹配左表中的所有行以及右表中符合条件的行。


SELECT * FROM location A LEFT JOIN store_info B on A.Store_Name = B.Store_Name ;

f5.png


左连接中左表的记录将会全部表示出来,而右表只会显示符合搜索条件的记录,右表记录不足的地方均为 NULL


8.3 右连接


右连接也被称为右外连接,在 FROM 子句中使用 RIGHT JOIN 或者 RIGHT OUTER JOIN 关键字来表示。右连接跟左连接正好相反,它是以右表为基础表,用于接收右表中的所有行,并用这些记录与左表中的行进行匹配


SELECT * FROM location A RIGHT JOIN store_info B on A.Store_Name = B.Store_Name ;

f6.png


9. UNION ----联集


将两个SQL语句的结果合并起来,两个SQL语句所产生的字段需要是同样的数据记录种类


UNION :生成结果的数据记录值将没有重复,且按照字段的顺序进行排序


语法:[SELECT 语句 1] UNION [SELECT 语句 2];


SELECT Store_Name FROM location UNION SELECT Store_Name FROM store_info;

f7.png


UNION ALL :将生成结果的数据记录值都列出来,无论有无重复


语法:[SELECT 语句 1] UNION ALL [SELECT 语句 2];


SELECT Store_Name FROM location UNION ALL SELECT Store_Name FROM store_info;

f8.png


9.1 交集值


取两个SQL语句结果的交集


SELECT A.Store_Name FROM location A INNER JOIN store_info B ON A.Store_Name = B.Store_Name;

SELECT A.Store_Name FROM location A INNER JOIN store_info B USING(Store_Name);

f9.png


取两个SQL语句结果的交集,且没有重复


SELECT DISTINCT A.Store_Name FROM location A INNER JOIN store_info B USING(Store_Name);

SELECT DISTINCT Store_Name FROM location WHERE (Store_Name) IN (SELECT Store_Name FROM store_info);

SELECT DISTINCT A.Store_Name FROM location A LEFT JOIN store_info B USING(Store_Name) WHERE B.Store_Name IS NOT NULL;

SELECT A.Store_Name FROM (SELECT B.Store_Name FROM location B INNER JOIN store_info C ON B.Store_Name = C.Store_Name) A
GR0UP BY A.Store_Name;

SELECT A.Store_Name FROM
(SELECT DISTINCT Store_Name FROM location UNION ALL SELECT DISTINCT Store_Name FROM store_info) A
GR0UP BY A.Store_Name HAVING COUNT(*) > 1;

f10.png


f11.png


9.2 无交集值


显示第一个SQL语句的结果,且与第二个SQL语句没有交集的结果,且没有重复


SELECT DISTINCT Store_Name FROM location WHERE (Store_Name) NOT IN (SELECT Store_Name FROM store_info);

SELECT DISTINCT A.Store_Name FROM location A LEFT JOIN store_info B USING(Store_Name) WHERE B.Store_Name IS NULL;

SELECT A.Store_Name FROM
(SELECT DISTINCT Store_Name FROM location UNION ALL SELECT DISTINCT Store_Name FROM store_info) A
GR0UP BY A.Store_Name HAVING COUNT(*) = 1;

f12.png


10. case


是 SQL 用来做为 IF-THEN-ELSE 之类逻辑的关键字


语法:


SELECT CASE ("字段名")
WHEN "条件1" THEN "结果1"
WHEN "条件2" THEN "结果2"
...
[ELSE "结果N"]
END
FROM "表名";

"条件" 可以是一个数值或是公式。 ELSE 子句则并不是必须的。


SELECT Store_Name, CASE Store_Name 
WHEN 'Los Angeles' THEN Sales * 2
WHEN 'Boston' THEN 2000
ELSE Sales
END
"New Sales",Date
FROM store_info;

#"New Sales" 是用于 CASE 那个字段的字段名。

f13.png


11. 正则表达式


匹配模式描述实例
^匹配文本的结束字符‘^bd’ 匹配以 bd 开头的字符串
$匹配文本的结束字符‘qn$’ 匹配以 qn 结尾的字符串
.匹配任何单个字符‘s.t’ 匹配任何 s 和 t 之间有一个字符的字符串
*匹配零个或多个在它前面的字符‘fo*t’ 匹配 t 前面有任意个 o
+匹配前面的字符 1 次或多次‘hom+’ 匹配以 ho 开头,后面至少一个m 的字符串
字符串匹配包含指定的字符串‘clo’ 匹配含有 clo 的字符串
p1|p2匹配 p1 或 p2‘bg|fg’ 匹配 bg 或者 fg
[...]匹配字符集合中的任意一个字符‘[abc]’ 匹配 a 或者 b 或者 c
[^...]匹配不在括号中的任何字符‘[^ab]’ 匹配不包含 a 或者 b 的字符串
{n}匹配前面的字符串 n 次‘g{2}’ 匹配含有 2 个 g 的字符串
{n,m}匹配前面的字符串至少 n 次,至多m 次‘f{1,3}’ 匹配 f 最少 1 次,最多 3 次

语法:SELECT "字段" FROM "表名" WHERE "字段" REGEXP {模式};


SELECT * FROM store_info WHERE Store_Name REGEXP 'os';
SELECT * FROM store_info WHERE Store_Name REGEXP '^[A-G]';
SELECT * FROM store_info WHERE Store_Name REGEXP 'Ho|Bo';

f14.png


12. 存储过程


存储过程是一组为了完成特定功能的SQL语句集合。


存储过程在使用过程中是将常用或者复杂的工作预先使用SQL语句写好并用一个指定的名称存储起来,这个过程经编译和优化后存储在数据库服务器中。当需要使用该存储过程时,只需要调用它即可。存储过程在执行上比传统SQL速度更快、执行效率更高。


存储过程的优点


1、执行一次后,会将生成的二进制代码驻留缓冲区,提高执行效率


2、SQL语句加上控制语句的集合,灵活性高


3、在服务器端存储,客户端调用时,降低网络负载


4、可多次重复被调用,可随时修改,不影响客户端调用


5、可完成所有的数据库操作,也可控制数据库的信息访问权限


12.1 创建存储过程


DELIMITER $$							#将语句的结束符号从分号;临时改为两个$$(可以是自定义)
CREATE PROCEDURE Proc() #创建存储过程,过程名为Proc,不带参数
-> BEGIN #过程体以关键字 BEGIN 开始
-> select * from Store_Info; #过程体语句
-> END $$ #过程体以关键字 END 结束
DELIMITER ; #将语句的结束符号恢复为分号

实例


DELIMITER $$							#将语句的结束符号从分号;临时改为两个$$(可以自定义)
CREATE PROCEDURE Proc5() #创建存储过程,过程名为Proc5,不带参数
-> BEGIN #过程体以关键字 BEGIN 开始
-> create table user (id int (10), name char(10),score int (10));
-> insert int0 user values (1, 'cyw',70);
-> select * from cyw; #过程体语句
-> END $$ #过程体以关键字 END 结束
DELIMITER ; #将语句的结束符号恢复为分号

f15.png


12.2 调用存储过程


CALL Proc;

f16.png


12.3 查看存储过程


SHOW CREATE PROCEDURE [数据库.]存储过程名; #查看某个存储过程的具体信息


SHOW CREATE PROCEDURE Proc;

SHOW PROCEDURE STATUS [LIKE '%Proc%'] \G

f17.png


12.4 存储过程的参数


**IN 输入参数:**表示调用者向过程传入值(传入值可以是字面量或变量)


**OUT 输出参数:**表示过程向调用者传出值(可以返回多个值)(传出值只能是变量)


**INOUT 输入输出参数:**既表示调用者向过程传入值,又表示过程向调用者传出值(值只能是变量)


DELIMITER $$				
CREATE PROCEDURE Proc6(IN inname CHAR(16))
-> BEGIN
-> SELECT * FROM store_info WHERE Store_Name = inname;
-> END $$
DELIMITER ;

CALL Proc6('Boston');

f18.png


12.5 修改存储过程


ALTER PROCEDURE <过程名>[<特征>... ]
ALTER PROCEDURE GetRole MODIFIES SQL DATA SQL SECURITY INVOKER;
MODIFIES sQLDATA:表明子程序包含写数据的语句
SECURITY:安全等级
invoker:当定义为INVOKER时,只要执行者有执行权限,就可以成功执行。

12.6 删除存储过程


存储过程内容的修改方法是通过删除原有存储过程,之后再以相同的名称创建新的存储过程。如果要修改存储过程的名称,可以先删除原存储过程,再以不同的命名创建新的存储过程。


DROP PROCEDURE IF EXISTS Proc;		
#仅当存在时删除,不添加 IF EXISTS 时,如果指定的过程不存在,则产生一个错误

f19.png


13. 条件语句


if-then-else ···· end if


mysql> delimiter $$
mysql>
mysql> CREATE PROCEDURE proc8(IN pro int)
->
-> begin
->
-> declare var int;
-> set var=pro*2;
-> if var>=10 then
-> update t set id=id+1;
-> else
-> update t set id=id-1;
-> end if;
-> end $$

mysql> delimiter ;

f20.png


f21.png


14. 循环语句


while ···· end while


mysql> delimiter $$
mysql>
mysql> create procedure proc9()
-> begin
-> declare var int(10);
-> set var=0;
-> while var<6 do
-> insert int0 t values(var);
-> set var=var+1;
-> end while;
-> end $$

mysql> delimiter ;

f22.png


15. 视图表 create view


15.1 视图表概述


视图,可以被当作是虚拟表或存储查询。


视图跟表格的不同是,表格中有实际储存数据记录,而视图是建立在表格之上的一个架构,它本身并不实际储存数据记录。


临时表在用户退出或同数据库的连接断开后就自动消失了,而视图不会消失。
视图不含有数据,只存储它的定义,它的用途一般可以简化复杂的查询。


比如你要对几个表进行连接查询,而且还要进行统计排序等操作,写sql语句会很麻烦的,用视图将几个表联结起来,然后对这个视图进行查询操作,就和对一个表查询一样,很方便。


15.2 视图表能否修改?


首先我们需要知道,视图表保存的是select语句的定义,所以视图表可不可以修改需要视情况而定。



  • 如果 select 语句查询的字段是没有被处理过的源表字段,则可以通过视图表修改源表数据;

  • 如果select 语句查询的字段是被 gr0up by语句或 函数 处理过的字段,则不可以直接修改视图表的数据。


create view v_store_info as select store_name,sales from store_info;

update v_store_info set sales=1000 where store_name='Houston';

f23.png


create view v_sales as select store_name,sum(sales) from store_info gr0up by store_name having sum(sales)>1000;

update v_sales set store_name='xxxx' where store_name='Los Angeles';

f24.png


f25.png


15.3 基本语法


15.3.1 创建视图表


语法
create view "视图表名" as "select 语句";

create view v_region_sales as select a.region region,sum(b.sales) sales from location a 
inner join store_info b on a.store_name = b.store_name gr0up by region;

f26.png


15.4 查看视图表


语法
select * from 视图表名;

select * from v_region_sales;

f27.png


15.5 删除视图表


语法
drop view 视图表名;

drop view v_region_sales;

f28.png


15.6 通过视图表求无交集值


将两个表中某个字段的不重复值进行合并


只出现一次(count =1 ) ,即无交集


通过


create view 视图表名 as select distinct 字段 from 左表 union all select distinct 字段 from 右表;

select 字段 from 视图表名 gr0up by 字段 having count(字段)=1;

#先建立视图表
create viem v_union as select distinct store_name from location union all select distinct store_name from store_info;

f29.png


#再通过视图表求无交集
select store_name from v_union gr0up by store_name having count(*)=1;

f30.png


作者:lc111
来源:juejin.cn/post/7291952951047929868
收起阅读 »

哇塞,新来个架构师,把Nacos注册中心讲得炉火纯青,佩服佩服~~

大家好,我是三友~~ 今天就应某位小伙伴的要求,来讲一讲Nacos作为服务注册中心底层的实现原理 不知你是否跟我一样,在使用Nacos时有以下几点疑问: 临时实例和永久实例是什么?有什么区别? 服务实例是如何注册到服务端的? 服务实例和服务端之间是如何保活的...
继续阅读 »

大家好,我是三友~~


今天就应某位小伙伴的要求,来讲一讲Nacos作为服务注册中心底层的实现原理


不知你是否跟我一样,在使用Nacos时有以下几点疑问:



  • 临时实例和永久实例是什么?有什么区别?

  • 服务实例是如何注册到服务端的?

  • 服务实例和服务端之间是如何保活的?

  • 服务订阅是如何实现的?

  • 集群间数据是如何同步的?CP还是AP?

  • Nacos的数据模型是什么样的?

  • ...


本文就通过探讨上述问题来探秘Nacos服务注册中心核心的底层实现原理。


虽然Nacos最新版本已经到了2.x版本,但是为了照顾那些还在用1.x版本的同学,所以本文我会同时去讲1.x版本和2.x版本的实现


临时实例和永久实例


临时实例和永久实例在Nacos中是一个非常非常重要的概念


之所以说它重要,主要是因为我在读源码的时候发现,临时实例和永久实例在底层的许多实现机制是完全不同的


临时实例


临时实例在注册到注册中心之后仅仅只保存在服务端内部一个缓存中,不会持久化到磁盘


这个服务端内部的缓存在注册中心届一般被称为服务注册表


当服务实例出现异常或者下线之后,就会把这个服务实例从服务注册表中剔除


永久实例


永久服务实例不仅仅会存在服务注册表中,同时也会被持久化到磁盘文件中


当服务实例出现异常或者下线,Nacos只会将服务实例的健康状态设置为不健康,并不会对将其从服务注册表中剔除


所以这个服务实例的信息你还是可以从注册中心看到,只不过处于不健康状态


这是就是两者最最最基本的区别



当然除了上述最基本的区别之外,两者还有很多其它的区别,接下来本文还会提到



这里你可能会有一个疑问



为什么Nacos要将服务实例分为临时实例和永久实例?



主要还是因为应用场景不同


临时实例就比较适合于业务服务,服务下线之后可以不需要在注册中心中查看到


永久实例就比较适合需要运维的服务,这种服务几乎是永久存在的,比如说MySQL、Redis等等



MySQL、Redis等服务实例可以通过SDK手动注册



对于这些服务,我们需要一直看到服务实例的状态,即使出现异常,也需要能够查看时实的状态



所以从这可以看出Nacos跟你印象中的注册中心不太一样,他不仅仅可以注册平时业务中的实例,还可以注册像MySQL、Redis这个服务实例的信息到注册中心



在SpringCloud环境底下,一般其实都是业务服务,所以默认注册服务实例都是临时实例


当然如果你想改成永久实例,可以通过下面这个配置项来完成


spring
  cloud:
    nacos:
      discovery:
        #ephemeral单词是临时的意思,设置成false,就是永久实例了
        ephemeral: false

这里还有一个小细节


在1.x版本中,一个服务中可以既有临时实例也有永久实例,服务实例是永久还是临时是由服务实例本身决定的


但是2.x版本中,一个服务中的所有实例要么都是临时的要么都是永久的,是由服务决定的,而不是具体的服务实例


所以在2.x可以说是临时服务永久服务




为什么2.x把临时还是永久的属性由实例本身决定改成了由服务决定?



其实很简单,你想想,假设对一个MySQL服务来说,它的每个服务实例肯定都是永久的,不会出现一些是永久的,一些是临时的情况吧


所以临时还是永久的属性由服务本身决定其实就更加合理了


服务注册


作为一个服务注册中心,服务注册肯定是一个非常重要的功能


所谓的服务注册,就是通过注册中心提供的客户端SDK(或者是控制台)将服务本身的一些元信息,比如ip、端口等信息发送到注册中心服务端


服务端在接收到服务之后,会将服务的信息保存到前面提到的服务注册表中


1、1.x版本的实现


在Nacos在1.x版本的时候,服务注册是通过Http接口实现的



代码如下



整个逻辑比较简单,因为Nacos服务端本身就是用SpringBoot写的


但是在2.x版本的实现就比较复杂了


2、2.x版本的实现


2.1、通信协议的改变


2.x版本相比于1.x版本最主要的升级就是客户端和服务端通信协议的改变,由1.x版本的Http改成了2.x版本gRPC



gRPC是谷歌公司开发的一个高性能、开源和通用的RPC框架,Java版本的实现底层也是基于Netty来的



之所以改成了gRPC,主要是因为Http请求会频繁创建和销毁连接,白白浪费资源


所以在2.x版本之后,为了提升性能,就将通信协议改成了gRPC


根据官网显示,整体的效果还是很明显,相比于1.x版本,注册性能总体提升至少2倍



虽然通信方式改成了gRPC,但是2.x版本服务端依然保留了Http注册的接口,所以用1.x的Nacos SDK依然可以注册到2.x版本的服务端



2.2、具体的实现


Nacos客户端在启动的时候,会通过gRPC跟服务端建立长连接



这个连接会一直存在,之后客户端与服务端所有的通信都是基于这个长连接来的


当客户端发起注册的时候,就会通过这个长连接,将服务实例的信息发送给服务端


服务端拿到服务实例,跟1.x一样,也会存到服务注册表


除了注册之外,当注册的是临时实例时,2.x还会将服务实例信息存储到客户端中的一个缓存中,供Redo操作


所谓的Redo操作,其实就是一个补偿机制,本质是个定时任务,默认每3s执行一次


这个定时任务作用是,当客户端与服务端重新建立连接时(因为一些异常原因导致连接断开)


那么之前注册的服务实例肯定还要继续注册服务端(断开连接服务实例就会被剔除服务注册表)


所以这个Redo操作一个很重要的作用就是重连之后的重新注册的作用



除了注册之外,比如服务订阅之类的操作也需要Redo操作,当连接重新建立,之前客户端的操作都需要Redo一下



小总结


1.x版本是通过Http协议来进行服务注册的


2.x由于客户端与服务端的通信改成了gRPC长连接,所以改成通过gRPC长连接来注册


2.x比1.x多个Redo操作,当注册的服务实例是临时实例是,出现网络异常,连接重新建立之后,客户端需要将服务注册、服务订阅之类的操作进行重做


这里你可能会有个疑问



既然2.x有Redo机制保证客户端与服务端通信正常之后重新注册,那么1.x有类似的这种Redo机制么?



当然也会有,接下往下看。


心跳机制


心跳机制,也可以被称为保活机制,它的作用就是服务实例告诉注册中心我这个服务实例还活着



在正常情况下,服务关闭了,那么服务会主动向Nacos服务端发送一个服务下线的请求


Nacos服务端在接收到请求之后,会将这个服务实例从服务注册表中剔除


但是对于异常情况下,比如出现网络问题,可能导致这个注册的服务实例无法提供服务,处于不可用状态,也就是不健康


而此时在没有任何机制的情况下,服务端是无法知道这个服务处于不可用状态


所以为了避免这种情况,一些注册中心,就比如Nacos、Eureka,就会用心跳机制来判断这个服务实例是否能正常


在Nacos中,心跳机制仅仅是针对临时实例来说的,临时实例需要靠心跳机制来保活


心跳机制在1.x和2.x版本的实现也是不一样的


1.x心跳实现


在1.x中,心跳机制实现是通过客户端和服务端各存在的一个定时任务来完成的


在服务注册时,发现是临时实例,客户端会开启一个5s执行一次的定时任务



这个定时任务会构建一个Http请求,携带这个服务实例的信息,然后发送到服务端



在Nacos服务端也会开启一个定时任务,默认也是5s执行一次,去检查这些服务实例最后一次心跳的时间,也就是客户端最后一次发送Http请求的时间



  • 当最后一次心跳时间超过15s,但没有超过30s,会把这服务实例标记成不健康

  • 当最后一次心跳超过30s,直接把服务从服务注册表中剔除



这就是1.x版本的心跳机制,本质就是两个定时任务


其实1.x的这个心跳还有一个作用,就是跟上一节说的gRPC时Redo操作的作用是一样的


服务在处理心跳的时候,发现心跳携带这个服务实例的信息在注册表中没有,此时就会添加到服务注册表


所以心跳也有Redo的类似效果


2.x心跳实现


在2.x版本之后,由于通信协议改成了gRPC,客户端与服务端保持长连接,所以2.x版本之后它是利用这个gRPC长连接本身的心跳来保活


一旦这个连接断开,服务端就会认为这个连接注册的服务实例不可用,之后就会将这个服务实例从服务注册表中提出剔除


除了连接本身的心跳之外,Nacos还有服务端的一个主动检测机制


Nacos服务端也会启动一个定时任务,默认每隔3s执行一次


这个任务会去检查超过20s没有发送请求数据的连接


一旦发现有连接已经超过20s没发送请求,那么就会向这个连接对应的客户端发送一个请求


如果请求不通或者响应失败,此时服务端也会认为与客户端的这个连接异常,从而将这个客户端注册的服务实例从服务注册表中剔除


所以对于2.x版本,主要是两种机制来进行保活:



  • 连接本身的心跳机制,断开就直接剔除服务实例

  • Nacos主动检查机制,服务端会对20s没有发送数据的连接进行检查,出现异常时也会主动断开连接,剔除服务实例


小总结


心跳机制仅仅针对临时实例而言


1.x心跳机制是通过客户端和服务端两个定时任务来完成的,客户端定时上报心跳信息,服务端定时检查心跳时间,超过15s标记不健康,超过30s直接剔除


1.x心跳机制还有类似2.x的Redo作用,服务端发现心跳的服务信息不存在会,会将服务信息添加到注册表,相当于重新注册了


2.x是基于gRPC长连接本身的心跳机制和服务端的定时检查机制来的,出现异常直接剔除


健康检查


前面说了,心跳机制仅仅是临时实例用来保护的机制


而对于永久实例来说,一般来说无法主动上报心跳


就比如说MySQL实例,肯定是不会主动上报心跳到Nacos的,所以这就导致无法通过心跳机制来保活


所以针对永久实例的情况,Nacos通过一种叫健康检查的机制去判断服务实例是否活着


健康检查跟心跳机制刚好相反,心跳机制是服务实例向服务端发送请求


而所谓的健康检查就是服务端主动向服务实例发送请求,去探测服务实例是否活着



健康检查机制在1.x和2.x的实现机制是一样的


Nacos服务端在会去创建一个健康检查任务,这个任务每次执行时间间隔会在2000~7000毫秒之间


当任务触发的时候,会根据设置的健康检查的方式执行不同的逻辑,目前主要有以下三种方式:



  • TCP

  • HTTP

  • MySQL


TCP的方式就是根据服务实例的ip和端口去判断是否能连接成功,如果连接成功,就认为健康,反之就任务不健康


HTTP的方式就是向服务实例的ip和端口发送一个Http请求,请求路径是需要设置的,如果能正常请求,说明实例健康,反之就不健康


MySQL的方式是一种特殊的检查方式,他可以执行下面这条Sql来判断数据库是不是主库



默认情况下,都是通过TCP的方式来探测服务实例是否还活着


服务发现


所谓的服务发现就是指当有服务实例注册成功之后,其它服务可以发现这些服务实例


Nacos提供了两种发现方式:



  • 主动查询

  • 服务订阅


主动查询就是指客户端主动向服务端查询需要关注的服务实例,也就是拉(pull)的模式


服务订阅就是指客户端向服务端发送一个订阅服务的请求,当被订阅的服务有信息变动就会主动将服务实例的信息推送给订阅的客户端,本质就是推(push)模式



在我们平时使用时,一般来说都是选择使用订阅的方式,这样一旦有服务实例数据的变动,客户端能够第一时间感知


并且Nacos在整合SpringCloud的时候,默认就是使用订阅的方式


对于这两种服务发现方式,1.x和2.x版本实现也是不一样


服务查询其实两者实现都很简单


1.x整体就是发送Http请求去查询服务实例,2.x只不过是将Http请求换成了gRPC的请求


服务端对于查询的处理过程都是一样的,从服务注册表中查出符合查询条件的服务实例进行返回


不过对于服务订阅,两者的机制就稍微复杂一点


在Nacos客户端,不论是1.x还是2.x都是通过SDK中的NamingService#subscribe方法来发起订阅的



当有服务实例数据变动的时,客户端就会回调EventListener,就可以拿到最新的服务实例数据了


虽然1.x还是2.x都是同样的方法,但是具体的实现逻辑是不一样的


1.x服务订阅实现


在1.x版本的时候,服务订阅的处理逻辑大致会有以下三步:


第一步,客户端在启动的时候,会去构建一个叫PushReceiver的类


这个类会去创建一个UDP Socket,端口是随机的



其实通过名字就可以知道这个类的作用,就是通过UDP的方式接收服务端推送的数据的


第二步,调用NamingService#subscribe来发起订阅时,会先去服务端查询需要订阅服务的所有实例信息


之后会将所有服务实例数据存到客户端的一个内部缓存中



并且在查询的时候,会将这个UDP Socket的端口作为一个参数传到服务端


服务端接收到这个UDP端口后,后续就通过这个端口给客户端推送服务实例数据


第三步,会为这次订阅开启一个不定时执行的任务



之所以不定时,是因为这个当执行异常的时候,下次执行的时间间隔就会变长,但是最多不超过60s,正常是10s,这个10s是查询服务实例是服务端返回的



这个任务会去从服务端查询订阅的服务实例信息,然后更新内部缓存


这里你可能会有个疑问



既然有了服务变动推送的功能,为什么还要定时去查询更新服务实例信息呢?



其实很简单,那就是因为UDP通信不稳定导致的


虽然有Push,但是由于UDP通信自身的不确定性,有可能会导致客户端接收变动信息失败


所以这里就加了一个定时任务,弥补这种可能性,属于一个兜底的方案。


这就是1.x版本的服务订阅的实现



2.x服务订阅的实现


讲完1.x的版本实现,接下来就讲一讲2.x版本的实现


由于2.x版本换成了gRPC长连接的方式,所以2.x版本服务数据变更推送已经完全抛弃了1.x的UDP做法


当有服务实例变动的时候,服务端直接通过这个长连接将服务信息发送给客户端


客户端拿到最新服务实例数据之后的处理方式就跟1.x是一样了


除了处理方式一样,2.x也继承了1.x的其他的东西


比如客户端依然会有服务实例的缓存


定时对比机制也保留了,只不过这个定时对比的机制默认是关闭状态


之所以默认关闭,主要还是因为长连接还是比较稳定的原因


当客户端出现异常,接收不到请求,那么服务端会直接跟客户端断开连接


当恢复正常,由于有Redo操作,所以还是能拿到最新的实例信息的


所以2.x版本的服务订阅功能的实现大致如下图所示



这里还有个细节需要注意


在1.x版本的时候,任何服务都是可以被订阅的


但是在2.x版本中,只支持订阅临时服务,对于永久服务,已经不支持订阅了


小总结


服务查询1.x是通过Http请求;2.x通过gRPC请求


服务订阅1.x是通过UDP来推送的;2.x就基于gRPC长连接来实现的


1.x和2.x客户端都有服务实例的缓存,也有定时对比机制,只不过1.x会自动开启;2.x提供了一个开个,可以手动选择是否开启,默认不开启


数据一致性


由于Nacos是支持集群模式的,所以一定会涉及到分布式系统中不可避免的数据一致性问题


1、服务实例的责任机制


再说数据一致性问题之前,先来讨论一下服务实例的责任机制


什么是服务实例的责任机制?


比如上面提到的服务注册、心跳管理、监控检查机制,当只有一个Nacos服务时,那么自然而言这个服务会去检查所有的服务实例的心跳时间,执行所有服务实例的健康检查任务



但是当出现Nacos服务出现集群时,为了平衡各Nacos服务的压力,Nacos会根据一定的规则让每个Nacos服务只管理一部分服务实例的



当然每个Nacos服务的注册表还是全部的服务实例数据




这个管理机制我给他起了一个名字,就叫做责任机制,因为我在1.x和2.x都提到了responsible这个单词


本质就是Nacos服务对哪些服务实例负有心跳监测,健康检查的责任。


2、CAP定理和BASE理论


谈到数据一致性问题,一定离不开两个著名分布式理论



  • CAP定理

  • BASE理论


CAP定理中,三个字母分别代表这些含义:



  • C,Consistency单词的缩写,代表一致性,指分布式系统中各个节点的数据保持强一致,也就是每个时刻都必须一样,不一样整个系统就不能对外提供服务

  • A,Availability单词的缩写,代表可用性,指整个分布式系统保持对外可用,即使从每个节点获取的数据可能都不一样,只要能获取到就行

  • P,Partition tolerance单词的缩写,代表分区容错性。


所谓的CAP定理,就是指在一个分布式系统中,CAP这三个指标,最多同时只能满足其中的两个,不可能三个都同时满足



为什么三者不能同时满足?


对于一个分布式系统,网络分区是一定需要满足的


而所谓分区指的是系统中的服务部署在不同的网络区域中



比如,同一套系统可能同时在北京和上海都有部署,那么他们就处于不同的网络分区,就可能出现无法互相访问的情况


当然,你也可以把所有的服务都放在一个网络分区,但是当网络出现故障时,整个系统都无法对外提供服务,那这还有什么意义呢?


所以分布式系统一定需要满足分区容错性,把系统部署在不同的区域网络中


此时只剩下了一致性和可用性,它们为什么不能同时满足?


其实答案很简单,就因为可能出现网络分区导致的通信失败。


比如说,现在出现了网络分区的问题,上图中的A网络区域和B网络区域无法相互访问


此时假设往上图中的A网络区域发送请求,将服务中的一个值 i 属性设置成 1



如果保证可用性,此时由于A和B网络不通,此时只有A中的服务修改成功,B无法修改成功,此时数据AB区域数据就不一致性,也就没有保证数据一致性


如果保证一致性,此时由于A和B网络不通,所以此时A也不能修改成功,必须修改失败,否则就会导致AB数据不一致


虽然A没修改成功,保证了数据一致性,AB还是之前相同的数据,但是此时整个系统已经没有写可用性了,无法成功写数据了。


所以从上面分析可以看出,在有分区容错性的前提下,可用性和一致性是无法同时保证的。


虽然无法同时一致性和可用性,但是能不能换种思路来思考一下这个问题


首先我们可以先保证系统的可用性,也就是先让系统能够写数据,将A区域服务中的i修改成1


之后当AB区域之间网络恢复之后,将A区域的i值复制给B区域,这样就能够保证AB区域间的数据最终是一致的了


这不就皆大欢喜了么


这种思路其实就是BASE理论的核心要点,优先保证可用性,数据最终达成一致性。


BASE理论主要是包括以下三点:



  • 基本可用(Basically Available):系统出现故障还是能够对外提供服务,不至于直接无法用了

  • 软状态(Soft State):允许各个节点的数据不一致

  • 最终一致性,(Eventually Consistent):虽然允许各个节点的数据不一致,但是在一定时间之后,各个节点的数据最终需要一致的


BASE理论其实就是妥协之后的产物。


3、Nacos的AP和CP


Nacos其实目前是同时支持AP和CP的


具体使用AP还是CP得取决于Nacos内部的具体功能,并不是有的文章说的可以通过一个配置自由切换。


就以服务注册举例来说,对于临时实例来说,Nacos会优先保证可用性,也就是AP


对于永久实例,Nacos会优先保证数据的一致性,也就是CP


接下来我们就来讲一讲Nacos的CP和AP的实现原理


3.1、Nacos的AP实现


对于AP来说,Nacos使用的是阿里自研的Distro协议


在这个协议中,每个服务端节点是一个平等的状态,每个服务端节点正常情况下数据是一样的,每个服务端节点都可以接收来自客户端的读写请求



当某个节点刚启动时,他会向集群中的某个节点发送请求,拉取所有的服务实例数据到自己的服务注册表中



这样其它客户端就可以从这个服务节点中获取到服务实例数据了


当某个服务端节点接收到注册临时服务实例的请求,不仅仅会将这个服务实例存到自身的服务注册表,同时也会向其它所有服务节点发送请求,将这个服务数据同步到其它所有节点



所以此时从任意一个节点都是可以获取到所有的服务实例数据的。


即使数据同步的过程发生异常,服务实例也成功注册到一个Nacos服务中,对外部而言,整个Nacos集群是可用的,也就达到了AP的效果


同时为了满足BASE理论,Nacos也有下面两种机制保证最终节点间数据最终是一致的:



  • 失败重试机制

  • 定时对比机制


失败重试机制是指当数据同步给其它节点失败时,会每隔3s重试一次,直到成功


定时对比机制就是指,每个Nacos服务节点会定时向所有的其它服务节点发送一些认证的请求


这个请求会告诉每个服务节点自己负责的服务实例的对应的版本号,这个版本号随着服务实例的变动就会变动


如果其它服务节点的数据的版本号跟自己的对不上,那就说明其它服务节点的数据不是最新的


此时这个Nacos服务节点就会将自己负责的服务实例数据发给不是最新数据的节点,这样就保证了每个节点的数据是一样的了。


3.2、Nacos的CP实现


Nacos的CP实现是基于Raft算法来实现的


在1.x版本早期,Nacos是自己手动实现Raft算法


在2.x版本,Nacos移除了手动实现Raft算法,转而拥抱基于蚂蚁开源的JRaft框架


在Raft算法,每个节点主要有三个状态



  • Leader,负责所有的读写请求,一个集群只有一个

  • Follower,从节点,主要是负责复制Leader的数据,保证数据的一致性

  • Candidate,候选节点,最终会变成Leader或者Follower


集群启动时都是节点Follower,经过一段时间会转换成Candidate状态,再经过一系列复杂的选择算法,选出一个Leader




这个选举算法比较复杂,完全值得另写一篇文章,这里就不细说了。不过立个flag,如果本篇文章点赞量超过28个,我连夜爆肝,再来一篇。



当有写请求时,如果请求的节点不是Leader节点时,会将请求转给Leader节点,由Leader节点处理写请求


比如,有个客户端连到的上图中的Nacos服务2节点,之后向Nacos服务2注册服务


Nacos服务2接收到请求之后,会判断自己是不是Leader节点,发现自己不是


此时Nacos服务2就会向Leader节点发送请求,Leader节点接收到请求之后,会处理服务注册的过程


为什么说Raft是保证CP的呢?


主要是因为Raft在处理写的时候有一个判断过程



  • 首先,Leader在处理写请求时,不会直接数据应用到自己的系统,而是先向所有的Follower发送请求,让他们先处理这个请求

  • 当超过半数的Follower成功处理了这个写请求之后,Leader才会写数据,并返回给客户端请求处理成功

  • 如果超过一定时间未收到超过半数处理成功Follower的信号,此时Leader认为这次写数据是失败的,就不会处理写请求,直接返回给客户端请求失败


所以,一旦发生故障,导致接收不到半数的Follower写成功的响应,整个集群就直接写失败,这就很符合CP的概念了。


不过这里还有一个小细节需要注意


Nacos在处理查询服务实例的请求直接时,并不会将请求转发给Leader节点处理,而是直接查当前Nacos服务实例的注册表


这其实就会引发一个问题


如果客户端查询的Follower节点没有及时处理Leader同步过来的写请求(过半响应的节点中不包括这个节点),此时在这个Follower其实是查不到最新的数据的,这就会导致数据的不一致


所以说,虽然Raft协议规定要求从Leader节点查最新的数据,但是Nacos至少在读服务实例数据时并没有遵守这个协议


当然对于其它的一些数据的读写请求有的还是遵守了这个协议。



JRaft对于读请求其实是做了很多优化的,其实从Follower节点通过一定的机制也是能够保证读到最新的数据



数据模型


在Nacos中,一个服务的确定是由三部分信息确定



  • 命名空间(Namespace):多租户隔离用的,默认是public

  • 分组(Gr0up):这个其实可以用来做环境隔离,服务注册时可以指定服务的分组,比如是测试环境或者是开发环境,默认是DEFAULT_GR0UP

  • 服务名(ServiceName):这个就不用多说了


通过上面三者就可以确定同一个服务了


在服务注册和订阅的时候,必须要指定上述三部分信息,如果不指定,Nacos就会提供默认的信息


不过,在Nacos中,在服务里面其实还是有一个集群的概念



在服务注册的时候,可以指定这个服务实例在哪个集体的集群中,默认是在DEFAULT集群下


在SpringCloud环境底下可以通过如下配置去设置


spring
  cloud:
    nacos:
      discovery:
        cluster-name: sanyoujavaCluster

在服务订阅的时候,可以指定订阅哪些集群下的服务实例


当然,也可以不指定,如果不指定话,默认就是订阅这个服务下的所有集群的服务实例


我们日常使用中可以将部署在相同区域的服务划分为同一个集群,比如杭州属于一个集群,上海属于一个集群


这样服务调用的时候,就可以优先使用同一个地区的服务了,比跨区域调用速度更快。


总结


到这,终终终于总算是讲完了Nacos作为注册中心核心的实现原理




作者:zzyang90
来源:juejin.cn/post/7347325319198048283
收起阅读 »

你居然还去服务器上捞日志,搭个日志收集系统难道不香么!

1 ELK日志系统 经典的ELK架构或现被称为Elastic Stack。Elastic Stack架构为Elasticsearch + Logstash + Kibana + Beats的组合: Beats负责日志的采集 Logstash负责做日志的聚合和...
继续阅读 »

1 ELK日志系统


经典的ELK架构或现被称为Elastic Stack。Elastic Stack架构为Elasticsearch + Logstash + Kibana + Beats的组合:



  • Beats负责日志的采集

  • Logstash负责做日志的聚合和处理

  • ES作为日志的存储和搜索系统

  • Kibana作为可视化前端展示


整体架构图:


img


2 EFK日志系统


容器化场景中,尤其k8s环境,用户经常使用EFK架构。F代表Fluent Bit,一个开源多平台的日志处理器和转发器。Fluent Bit可以:



  • 让用户从不同来源收集数据/日志

  • 统一并发到多个目的地

  • 完全兼容Docker和k8s环境



3 PLG日志系统


3.1 Prometheus+k8s日志系统




PLG


Grafana Labs提供的另一个日志解决方案PLG逐渐流行。PLG架构即Promtail + Loki + Grafana的组合:
img


Grafana,开源的可视化和分析软件,允许用户查询、可视化、警告和探索监控指标。Grafana主要提供时间序列数据的仪表板解决方案,支持超过数十种数据源。


Grafana Loki是一组可以组成一个功能齐全的日志堆栈组件,与其它日志系统不同,Loki只建立日志标签的索引而不索引原始日志消息,而是为日志数据设置一组标签,即Loki运营成本更低,效率还提高几个数量级。



Loki设计理念


Prometheus启发,可实现可水平扩展、高可用的多租户日志系统。Loki整体架构由不同组件协同完成日志收集、索引、存储等。


各组件如下,Loki’s Architecture深入了解。Loki就是like Prometheus, but for logs。
img


Promtail是一个日志收集的代理,会将本地日志内容发到一个Loki实例,它通常部署到需要监视应用程序的每台机器/容器上。Promtail主要是用来发现目标、将标签附加到日志流以及将日志推送到Loki。截止到目前,Promtail可以跟踪两个来源的日志:本地日志文件和systemd日志(仅支持AMD64架构)。


4 PLG V.S ELK


4.1 ES V.S Loki


ELK/EFK架构确实强,经多年实际环境验证。存储在ES中的日志通常以非结构化JSON对象形式存储在磁盘,且ES为每个对象都建索引,以便全文搜索,然后用户可特定查询语言搜索这些日志数据。


而Loki数据存储解耦:



  • 既可在磁盘存储

  • 也可用如Amazon S3云存储系统


Loki日志带有一组标签名和值,只有标签对被索引,这种权衡使它比完整索引操作成本更低,但针对基于内容的查询,需通过LogQL再单独查询。


4.2 Fluentd V.S Promtail


相比Fluentd,Promtail专为Loki定制,它可为运行在同一节点的k8s Pods做服务发现,从指定文件夹读取日志。Loki类似Prometheus的标签方式。因此,当与Prometheus部署在同一环境,因为相同的服务发现机制,来自Promtail的日志通常具有与应用程序指标相同的标签,统一标签管理。


4.3 Grafana V.S Kibana


Kibana提供许多可视化工具来进行数据分析,高级功能如异常检测等机器学习功能。Grafana针对Prometheus和Loki等时间序列数据打造,可在同一仪表板上查看日志指标。


参考



作者:JavaEdge在掘金
来源:juejin.cn/post/7295623585364082739
收起阅读 »

程序员想独立赚钱的几个注意点

1、始终保持好奇心,喜欢折腾新鲜事物,并且能够很快付诸于行动,有想法立马行动起来,赶紧把东西搞出来,然后推出去。2、普通人不要沉迷于技术,时刻想着通过技术赚到钱才是最重要的。3、一切以最小的代价赚到钱为第一原则。4、要务实,哪怕是些小的事情,哪怕是别人看不上的...
继续阅读 »

1、始终保持好奇心,喜欢折腾新鲜事物,并且能够很快付诸于行动,有想法立马行动起来,赶紧把东西搞出来,然后推出去。

2、普通人不要沉迷于技术,时刻想着通过技术赚到钱才是最重要的。

3、一切以最小的代价赚到钱为第一原则。

4、要务实,哪怕是些小的事情,哪怕是别人看不上的东西,只要能赚钱就要敢于去做。

5、要聚焦,每个小阶段,踏踏实实做好一件小事情。

6、除了懂产品之外,还要学会做运营,搞流量,做好SEO推广。

7、持续阅读,持续写作。

8、有钱也要谨慎,尽量小成本试错,钱要花在刀刃上。

9、不要迷恋自己的产品,如果做出来的东西有人愿意买走,那就果断出手,可以拿到一笔钱之后继续做其他想做的事情。

10、做运营就离不开做社群运营,引导用户进群,盘活用户,促进成交。

11、只要你的产品有价值,那就可以大胆收费。毕竟程序员除了追求技术也要赚钱养家,也要体面生活。

12、如果没有资本做背景,那就选个小赛道,做个隐形冠军。

13、如果想脱离打工人,想自己独立赚钱,那就是条不确定的路,要敢于面对不确定性。


作者:程序员半支烟
来源:mp.weixin.qq.com/s/3RVdGWpvk6AUqzBIB-4qXQ
收起阅读 »

记 · 在 AI 公司入职一个月的体验与感悟

已经在一家 AI 公司入职了一个月,对坐班有些厌恶的我,没想到有一天也会开始通勤打卡。而经历了这一个月的工作,我对坐班的态度有所转变,开始理解这种工作方式对我的意义。是时候分享入职这期间的工作内容与感受。 背景 直入正题,先说职位背景。该职位的技术要求大致如下...
继续阅读 »

已经在一家 AI 公司入职了一个月,对坐班有些厌恶的我,没想到有一天也会开始通勤打卡。而经历了这一个月的工作,我对坐班的态度有所转变,开始理解这种工作方式对我的意义。是时候分享入职这期间的工作内容与感受。


背景


直入正题,先说职位背景。该职位的技术要求大致如下,仅做参考。


## 任职要求 
1. 本科及以上学历,计算机科学、软件工程等相关专业, 硕士优先;
2. 扎实的 HTML、CSS、JavaScript 基础(vanilla) 功底 ;
3. 熟练使用 React、React Native 和 Next.js 进行前端开发
4. 了解前端性能优化技术,如代码压缩、懒加载等
5. 熟悉前端工程化工具
6. 具备良好的问题解决能力和团队协作精神
7. 熟练阅读英文技术文档
8. 有优异前端项目开发经验者优先

## 加分项:
- 贡献开源社区
- 有 AI 相关项目经验。
- 有前端性能优化和 SEO 优化经验。
- 有良好的产品思维和设计(UI/UX)意识。
- 有同理心思维。
- 具有一定的审美感。

很贴合国外主流的技术栈(至于为何,看后文便知),比较巧的是,我的 Web 全栈学习路线就是偏国外的技术栈。因此在技术栈上,这家公司是我喜欢的,恰巧又是 AI 开发,能让我尝试到一些前沿技术,也正好是想我折腾的。


求职经历


我是 Boss 直聘上找的(这里没给 boss 直聘打广告,我甚至还是第一次使用 boss 直聘),我有想过找人内推,但由于家庭因素被限定在福州这座城市,而内推的所在的城市往往都是那些一线城市,加上我的八股文和算法很不过关(我也很不情愿刷),到时候面试那关估计也不乐观。


因此就在 Boss 上碰碰运气,也顺带体验一下新人都是怎么找工作的。


从五一的时候开始准备简历和项目,在5号开始投简历,投递简历一关我是直接怼着工作经验1-3年的来投,而不是投应届或实习岗。因为我确实有一些工作经验,只不过不是正常的坐班打卡的形式,这在之前的博客中有说到。


在这期间共投了20多家,基本都是已读不回,就更别说投递简历了。后来我才了解到,原来 HR 回复消息是要花钱的,发布一个岗位也是。


唯一回复的还是我现在入职的这家,而且我还投了两份过去,一份是给 HR 的(没回),一份是给技术 leader 的(leader 回了)。


Untitled


面试被鸽


可能是由于当时这个岗位急招的原因,在 boss 直聘上也没多说什么,leader 就约明早 11 点来公司现场初步面试聊天一下。这期间还发生了一个小变故,我到公司了,可联系不上面试官,打了微信电话也无果。待了10来分钟后我就走了,等了约一个小时都没信息,那我大概率是被鸽了,还不提前和我通知一声,然后在boss上留下了这句评价🥲。


Untitled


初入职场,初次面试就这种情况,说真的我当时都有点心灰意冷了,我猜想是不是因为有其他合适的人选,于是就不招我了,就连信息也不给我打一个招呼,相当于把我拉黑似得。随后我就到附近的麦当劳花了 10 元的套餐安慰了一下自己,麦!


开始面试


直到到下午一点多的时候,面试官回复我说当时他们在开会,期间不让携带电子设备。早上就当一面过了,问我下午有没有时间,直接二面技术面(code test)过了就直接拿offer。


这时我才知道,原来早上也仅仅只是我的猜想,但我还是有点不想去了,心情有点不太愉悦,但想了想也懒得计较了,过去就当聊天罢了。到了下午面试问的就偏前端基础、八股文那些问题,其实我回答的巨烂,确实也没好好刷题,也不喜欢刷题,就面试了。自己写代码是由业务环境下驱动的,并从中寻求最佳实践。但好在我的技术面是比较广的,很多前沿的前端相关的工具库或多或少都使用过,也能侃侃而谈,加上个人 blog 和 github 这两个大加分项。就进入到了一个代码考核测试,不限框架,不限规则,使用公司的电脑打开 codesandbox 写一个todo list,前提是不使用任何 AI 工具。


这不正好到了我的强项,之前学某个框架的时候,不知道写什么demo,就写 todo list 来练手😂。恰好这次我就使用 next.js app router + Tailwindcss 的模版并且使用 form 标签的 action 和 use server 来实现新增功能。 能体现出我有在使用 next.js,而且用上了一些新特性,就拿到 offer 了。


1000047893.jpg


听完之后是不是莫名的感觉这个 offer 拿的好像有点莫名其妙的感觉😂,不管怎么样结果是好的就行了。


不过拿到 offer 后,我并没有选择马上入职,经历了一次被鸽的经历,对该公司的印象带有一些怀疑。其次就是这是一家初创 AI 公司,规模不大,从应届生找工作的角度,第一份正式的工作的起点很关键,如果能直接进大厂,后续跳槽到其他公司大概率也不成问题。


但在当地我投递了 20 多家已读不回的情况下,加上这份已有的 offer 不等人(急招),加上我家里人给我推荐的工作内容我并不是很满意,于是思考了两天,最终还是选择入职了这家公司。


薪资


比较令我差异的是我与企业签订的直接劳动合同,可能是因为我直接投递 1-3 年的工作经验,但我此时的身份还是应届生,按理来说我应该是签订实习合同后,转正再签劳动合同,难道说我已经提前转正了?。不过也好,这样和学校的三方协议都可以不用签了,直接给劳动合同便可。


试用期 3 个月,薪资打 8 折。薪资在我当地还算 ok,但对于我而言并不理想。可能是会的比较多(全栈?全干!),加上曾经赚过比这还高上许多的薪资,从内心的角度多少是有些不平衡。不过目前还是试用期,薪资这方面后续也能再谈。


接下来尤为重要的上班体验才是让我觉得没后悔入职这家公司。


上班体验


介绍一下公司部门的办公工具


办公管理:企业微信


团队协作:Slack


任务看板:Trello


代码仓库:Github


代码托管:Vercel


视频会议:Zoom


你会发现除了企业微信,其他的应用都是国外的。怎么看都不是一家国内的企业吧,这是因为我部门的 Leader 是海外留学的,这也就不难理解工具是国外应用,技术栈选型是 React 生态了。


入职的第一周部门开了个小会,就是简单介绍了一下部门的任务职责,每个成员自我介绍。重点是提供一个优质的学习环境,像是技术书籍,电子设备,UI 模版或是技术会议的门票等费用,只要对部门有利,能提升自己,都可以找他报销。


我已经找 Leader 报销了个 magic ui pro,大约 420 块,直接找财务刷卡,付款的感觉是真爽,我是真爱了🥰。


Untitled


几天后,公司来了一个阿里做 B 端低代码开发的同事,也是负责前端开发,这不,我可以间接和这个老哥那学习大厂相关经验,我还正愁着没大厂相关的经验😄。


我询问他来这家公司的原因,他说被裁了,在家接外包一年了,不稳定就准备找工作,恰好这家公司急招,于是就来了。


:::warning 补


端午节后,这位老哥提离职了,原因的话我就不具体说了,可能是因为年龄大了,不适合坐班了。虽然早有预感,但还是有点不舍。因为现在部门的前端重任都在我这了😭


:::


团建


在我入职的第一周周末 Leader 为整个部门安排团建,由于这个部门成立不到 2 周,来的都是新成员,让我们自己组个局,去外面吃个饭。


也是在团建的时候了解到同事的履历一个个都不简单,有 985 的,有海外留学的,有在阿里、网易待过的,还有我这不堪回首的经历 🤡。


后面原定在 61 安排整个公司的团建,但由于天气和周末时间去的人少的因素而取消了,这我就不多说了。


端午之后的第一个工作日的中午,补过端午节部门聚餐的,这我也不多说了。就是怎么感觉这频率有点不太对,然后实际项目产出也还停留在 Spring 1 的阶段,让我有些不自在。


福利


部门每个月都会定一个最佳员工奖,我很荣幸获得部门本月的最佳员工,也感谢部门成员的认可,奖励是 300 元奖金或一日自由假。


Untitled


甚至还有一张奖状,就是这奖状怎么有点像给小学生似的。(事后我才了解到这奖状还是用打印机打印的😂)


Untitled


目前我已经能感受到最大的福利就是那个 magicui 动效库的模版,当然了,这个是要给公司的官网用上的,我也是蹭公司的福,给自己的站点用上了这个动效库。


此外像节日福利,如这次端午节,就是聚餐和发粽子,这也就没什么好说的。


通勤


公司距离我租房的地方只有 2 公里,每日的通勤总时间大约 40 分钟,早上大约 8 点起床,我通常坐公交车到公司附近的早餐店吃个早饭,吃完差不多 8 点 40分~50 分。中午外卖就不说了。下午下班从公交车和走路做个选择,吃完饭回到家。


黑客松


黑客松(hackathon),也称编程马拉松比赛。我是第一次听说过这个词,Leader 给定两个选题一个是打造某市地铁智能出行,另一个是给某商场的提供贴心的购物体验,发挥自己近一个月所学的知识,去创造一个供用户使用的 AI 程序,月底交付,奖金 3000 元/小组,抽签分组。我们当时部门有个人提了一嘴,要不我们两小组自己商量一下,把奖金平分得了😂。


不过对于这个行为,我个人认为目的是为了激励员工之间协同合作,但同时也免不了技术上的内耗,毕竟这个比赛不是我们的主要工作内容。


工作内容


我想肯定有很多人对 AI 开发的刻板印象是要会大模型开发,会懂得微调,会懂得人工智能算法。这个想法也没错,但从开发 AI 应用的角度,其实蛮需要前端的,尤其是会全栈框架的前端。


这里我不得不惊叹 next.js 的生态,很多 AI 相关的例子可以直接从 Vercel 的 AI Template 下学习,预览是否有你需要的功能,Clone 到本地,然后运行项目,对某些部分进行更改。搭建 AI 应用也是异常的快。


仿 AI SDK 网站效果


Leader 下发的一个任务,入职的前两周主要让我熟悉一些怎么使用 next.js 配合 vercel 的 ai sdk 来开发 AI 应用,如怎么调用 openai 的模型,实现一个 ai chatbot。给定了一个任务就是仿造 AI SDK,由于该项目没有开源,自然就只能另辟蹊径。


首先就是仿造页面了,这个作为前端开发,实现起来也算容易,更何况这个这个页面的样式使用 Tailwindcss 编写,直接通过审查元素仿造就行了。


其次在功能实现上,ai sdk 文档都提供了非常完善的解决方案,照着文档将代码稍微改写一下便可,具体的细节就不演示了。


官网首页


两周后开始正式项目开发了,首当其冲的就是官网页。


这里当时 Leader 问我有没有用过 Gatsbyjs,要用这个框架搭建一个官网。我表明我没用过,但我提了一嘴如果要搭建偏内容向的网站,可以考虑 Astro,我愿意折腾一番(我也一直想学 Astro 的)。不过最终在开发时间和成本的商讨下还是选择使用 next.js 来搭建,leader 还顺带给我推荐了一个动效库 magicui,叫我看看里面的案例,看看能不能给官网加点动效。 之后就有了上文提到报销 magicui 的事。


Rag bot


篇幅实在有限,有关 RAG 的不做过多解释,它可以让你的 AI 应用更具有权威性,让数据的来源可靠,而非胡乱生成数据。


RAG 的基本流程就是:



  1. 用户输入提问

  2. 检索:根据用户提问对 向量数据库 进行相似性检测,查找与回答用户问题最相关的内容

  3. 增强:根据检索的结果,生成 prompt。 一般都会涉及 “仅依赖下述信息源来回答问题” 这种限制 llm 参考信息源的语句,来减少幻想,让回答更加聚焦

  4. 生成:将增强后的 prompt 传递给 llm,返回数据给用户


在这个应用开发中,借鉴了 ragbot-starter 这个开源项目,同时向量数据库选用 datastax 公司的Astra DB。


恰好在开发这个应用的期间,我也正好在学习 Langchain.js,所以在数据处理这部分有点得心应手,目前应用还只停留在处理本地文件或用户上传的文件,只需要配置各种 File Loader 便可。


使用 RN 实现 chatbot


先看 Gif 效果。


demo


第一次用 Screen Studio,显示的不是很好,还请见谅,主要就是实现一个流式文本效果。


这里简单说下怎么实现的,就用 react-native-reusables 的模版(React native 的 Shadcn/ui) + react-native-gen-ui 实现的,不过后者的功能比较单一,后续估计要改代码了。代码就不贴了,我怕涉嫌代码泄露(其实已经泄露差不多了)。


收获


要我说最大的收获不是遇到一个氛围不错的公司,遇到一个好 leader,也不是接触 AI 开发从中学到了什么,更不是增进了我的技术栈。而是让我养成良好的习惯,开始正常一日三餐,开始作息规律,开始将工作与生活分离,身体状态也渐渐好了起来。


下图为 5 月的生物作息,基本都保持 0 点前入睡。(不过在我写这篇文章的时候已经两点了🥱)


Untitled


过去几年内我的作息与饮食都非常糟糕,能明显的感觉到状态有所下滑,编写代码的效率和能力也明显不如以前,有些力不从心。今年都快过去一半了,而我仅仅完成了2篇博文的写作,文章的输出效率明显不行😮‍💨。


如今经历了这一个月的坐班生活,可能是因为坐班而改变,也可能是公司的氛围,不管是那种,让我跳出我原有舒适区,重新拾起对新颖事物的兴趣,重新点燃学习某个技术的热情,重新找回了自我。


结尾


在我还没找工作之前,从我几个同届毕业的同学和网友的反馈得知今年的就业环境异常险峻。不仅如此,我还在网络上看到了大量工作者对自身工作的抱怨与不满,这些现象让我在工作前让我对未来的就业前景感到了一些不安。


当我亲身入局感受一番,也不禁开始低声叹气。开始思考是什么原因导致了如今大环境不好的现象,人为的制造就业焦虑,还是当下现实本就如此。当我跳出思考,回到现实难道环境好就一定挣钱多,环境差就一定挣钱少吗?社会似乎并不是这么简单的等式。


我逐渐意识到,无论大环境如何,每个人的努力和选择仍是至关重要。在面对不确定性和挑战时,保持学习和进步的态度,以及寻找自己的核心竞争力,才是应对困境的关键。


真正的职业安全感并不完全来自于外部环境,而是来自于我们自身不断提升的能力和适应变化的灵活性。


作者:愧怍
来源:juejin.cn/post/7379446118990282789
收起阅读 »

超级火爆的前端视频方案 FFmpeg ,带你体验一下~

前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ ffmpeg FFmpeg 是一个开源的、跨平台的多媒体框架,它可以用来录制、转换和流式传输音频和视频。它包括了一系列的库和工具,用于处理多媒体内容,比如 l...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~



ffmpeg


FFmpeg 是一个开源的、跨平台的多媒体框架,它可以用来录制、转换和流式传输音频和视频。它包括了一系列的库和工具,用于处理多媒体内容,比如 libavcodec(一个编解码库),libavformat(一个音视频容器格式库),libavutil(一个实用库),以及 ffmpeg 命令行工具本身。


FFmpeg 被广泛用于各种应用中,包括视频转换、视频编辑、视频压缩、直播流处理等。它支持多种音视频编解码器和容器格式,因此能够处理几乎所有类型的音视频文件。由于其功能强大和灵活性,FFmpeg 成为了许多视频相关软件和服务的底层技术基础。


很多网页都是用 ffmpeg 来进行视频切片,比如一个视频很大,如果通过一个连接去请求整个视频的话,那势必会导致加载时间过长,严重阻碍了用户观感


所以很多视频网站都会通过视频切片的方式来优化用户观感,就是一部分一部分地去加载出来,这样有利于用户的体验



安装 ffmpeg


安装包下载


首先到 ffmpeg 的安装网页:http://www.gyan.dev/ffmpeg/buil…



下载解压后将文件夹改名为 ffmpeg



环境变量配置


环境变量配置是为了能在电脑上使用 ffmpeg 命令行




体验 ffmpeg


先准备一个视频,比如我准备了一个视频,总共 300 多 M



视频切片


并在当前的目录下输入以下的命令


 ffmpeg -i jhys.mkv 
-c:v libx264
-c:a aac
-hls_time 60
-hls_segment_type mpegts
-hls_list_size 0
-f hls
-max_muxing_queue_size 1024
output.m3u8

接着 ffmpeg 会帮你将这个视频进行分片



直到切片步骤执行完毕,我们可以看到视频已经别切成几个片了



在这个命令中:



  • -i input_video.mp4 指定了输入视频文件。

  • -c:v libx264 -c:a aac 指定了视频和音频的编解码器。

  • -hls_time 10 指定了每个 M3U8 片段的时长,单位为秒。在这里,每个片段的时长设置为 10 秒。

  • -hls_segment_type mpegts 指定了 M3U8 片段的类型为 MPEG-TS。

  • -hls_list_size 0 设置 M3U8 文件中包含的最大片段数。这里设置为 0 表示没有限制。

  • -f hls 指定了输出格式为 HLS。

  • -max_muxing_queue_size 1024 设置了最大复用队列大小,以确保输出不会超过指定大小。

  • 最后输出的文件为 output.m3u8


视频播放


创建一个简单的前端项目




可以看到浏览器会加载所有的视频切片





作者:Sunshine_Lin
来源:juejin.cn/post/7361998447908864011
收起阅读 »

写给Java开发的16个小建议

前言 开发过程中其实有很多小细节要去注意,只有不断去抠细节,写出精益求精的代码,从量变中收获质变。 技术的进步并非一蹴而就,而是通过无数次的量变,才能引发质的飞跃。我们始终坚信,只有对每一个细节保持敏锐的触觉,才能绽放出完美的技术之花。 从一行行代码中,我们品...
继续阅读 »

前言


开发过程中其实有很多小细节要去注意,只有不断去抠细节,写出精益求精的代码,从量变中收获质变。


技术的进步并非一蹴而就,而是通过无数次的量变,才能引发质的飞跃。我们始终坚信,只有对每一个细节保持敏锐的触觉,才能绽放出完美的技术之花。


从一行行代码中,我们品味到了追求卓越的滋味。每一个小小的优化,每一个微妙的改进,都是我们追求技艺的印记。我们知道,只有更多的关注细节,才能真正理解技术的本质,洞察其中的玄机。正是在对细节的把握中,我们得以成就更好的技术人生。


耐心看完,你一定会有所收获。


image.png


补充


20230928


针对评论区指出的第14条示例的问题,现已修正。


原来的示例贴在这里,接受大家的批评:


image.png


1. 代码风格一致性


代码风格一致性可以提高代码的可读性和可维护性。例如,使用Java编程中普遍遵循的命名约定(驼峰命名法),使代码更易于理解。


// 不好的代码风格
int g = 10;
String S = "Hello";

// 好的代码风格
int count = 10;
String greeting = "Hello";

2. 使用合适的数据结构和集合


选择适当的数据结构和集合类可以改进代码的性能和可读性。例如,使用HashSet来存储唯一的元素。


// 不好的例子 - 使用ArrayList存储唯一元素
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(1); // 重复元素

// 好的例子 - 使用HashSet存储唯一元素
Set<Integer> set = new HashSet<>();
set.add(1);
set.add(2);
set.add(1); // 自动忽略重复元素

3. 避免使用魔法数值


使用常量或枚举来代替魔法数值可以提高代码的可维护性和易读性。


// 不好的例子 - 魔法数值硬编码
if (status == 1) {
// 执行某些操作
}

// 好的例子 - 使用常量代替魔法数值
final int STATUS_ACTIVE = 1;
if (status == STATUS_ACTIVE) {
// 执行某些操作
}

4. 异常处理


正确处理异常有助于代码的健壮性和容错性,避免不必要的try-catch块可以提高代码性能。


// 不好的例子 - 捕获所有异常,没有具体处理
try {
// 一些可能抛出异常的操作
} catch (Exception e) {
// 空的异常处理块
}

// 好的例子 - 捕获并处理特定异常,或向上抛出
try {
// 一些可能抛出异常的操作
} catch (FileNotFoundException e) {
// 处理文件未找到异常
} catch (IOException e) {
// 处理其他IO异常
}

5. 及时关闭资源


使用完资源后,及时关闭它们可以避免资源泄漏,特别是对于文件流、数据库连接等资源。


更好的处理方式参见第16条,搭配try-with-resources食用最佳


// 不好的例子 - 未及时关闭数据库连接
Connection conn = null;
Statement stmt = null;
try {
conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
stmt = conn.createStatement();
// 执行数据库查询操作
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 数据库连接未关闭
}

// 好的例子 - 使用try-with-resources确保资源及时关闭,避免了数据库连接资源泄漏的问题
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
Statement stmt = conn.createStatement()) {
// 执行数据库查询操作
} catch (SQLException e) {
e.printStackTrace();
}

6. 避免过度使用全局变量


过度使用全局变量容易引发意外的副作用和不可预测的结果,建议尽量避免使用全局变量。


// 不好的例子 - 过度使用全局变量
public class MyClass {
private int count;

// 省略其他代码
}

// 好的例子 - 使用局部变量或实例变量
public class MyClass {
public void someMethod() {
int count = 0;
// 省略其他代码
}
}

7. 避免不必要的对象创建


避免在循环或频繁调用的方法中创建不必要的对象,可以使用对象池、StringBuilder等技术。


// 不好的例子 - 频繁调用方法创建不必要的对象
public String formatData(int year, int month, int day) {
String formattedDate = String.format("%d-d-d", year, month, day); // 每次调用方法都会创建新的String对象
return formattedDate;
}

// 好的例子 - 避免频繁调用方法创建不必要的对象
private static final String DATE_FORMAT = "%d-d-d";
public String formatData(int year, int month, int day) {
return String.format(DATE_FORMAT, year, month, day); // 重复使用同一个String对象
}

8. 避免使用不必要的装箱和拆箱


避免频繁地在基本类型和其对应的包装类型之间进行转换,可以提高代码的性能和效率。


// 不好的例子
Integer num = 10; // 不好的例子,自动装箱
int result = num + 5; // 不好的例子,自动拆箱

// 好的例子 - 避免装箱和拆箱
int num = 10; // 好的例子,使用基本类型
int result = num + 5; // 好的例子,避免装箱和拆箱

9. 使用foreach循环遍历集合


使用foreach循环可以简化集合的遍历,并提高代码的可读性。


// 不好的例子 - 可读性不强,并且增加了方法调用的开销
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
for (int i = 0; i < names.size(); i++) {
System.out.println(names.get(i)); // 不好的例子
}

// 好的例子 - 更加简洁,可读性更好,性能上也更优
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
for (String name : names) {
System.out.println(name); // 好的例子
}

10. 使用StringBuilder或StringBuffer拼接大量字符串


在循环中拼接大量字符串时,使用StringBuilder或StringBuffer可以避免产生大量临时对象,提高性能。


// 不好的例子 - 每次循环都产生新的字符串对象
String result = "";
for (int i = 0; i < 1000; i++) {
result += "Number " + i + ", ";
}

// 好的例子 - StringBuilder不会产生大量临时对象
StringBuilder result = new StringBuilder();
for (int i = 0; i < 1000; i++) {
result.append("Number ").append(i).append(", ");
}

11. 使用equals方法比较对象的内容


老生常谈的问题,在比较对象的内容时,使用equals方法而不是==操作符,确保正确比较对象的内容。


// 不好的例子
String name1 = "Alice";
String name2 = new String("Alice");
if (name1 == name2) {
// 不好的例子,使用==比较对象的引用,而非内容
}

// 好的例子
String name1 = "Alice";
String name2 = new String("Alice");
if (name1.equals(name2)) {
// 好的例子,使用equals比较对象的内容
}

12. 避免使用多个连续的空格或制表符


多个连续的空格或制表符会使代码看起来杂乱不堪,建议使用合适的缩进和空格,保持代码的清晰可读。


// 不好的例子
int a = 10; // 不好的例子,多个连续的空格和制表符
String name = "John"; // 不好的例子,多个连续的空格和制表符

// 好的例子
int a = 10; // 好的例子,适当的缩进和空格
String name = "John"; // 好的例子,适当的缩进和空格

13. 使用日志框架记录日志


在代码中使用日志框架(如Log4j、SLF4J)来记录日志,而不是直接使用System.out.println(),可以更灵活地管理日志输出和级别。


// 不好的例子:
System.out.println("Error occurred"); // 不好的例子,直接输出日志到控制台

// 好的例子:
logger.error("Error occurred"); // 好的例子,使用日志框架记录日志

14. 避免在循环中创建对象


在循环中频繁地创建对象会导致大量的内存分配和垃圾回收,影响性能。尽量在循环外部创建对象,或使用对象池来复用对象,从而减少对象的创建和销毁开销。


// 不好的例子 - 在循环过程中频繁地创建和销毁对象,增加了垃圾回收的负担
for (int i = 0; i < 1000; i++) {
// 在每次循环迭代中创建新的对象,增加内存分配和垃圾回收的开销
Person person = new Person("John", 30);
System.out.println("Name: " + person.getName() + ", Age: " + person.getAge());
}


// 好的例子 - 在循环外部创建对象,减少内存分配和垃圾回收的开销
Person person = new Person("John", 30);
for (int i = 0; i < 1000; i++) {
System.out.println("Name: " + person.getName() + ", Age: " + person.getAge());

// 可以根据需要修改 person 对象的属性
person.setName("Alice");
person.setAge(25);
}


15. 使用枚举替代常量


这条其实和第3条一个道理,使用枚举可以更清晰地表示一组相关的常量,并且能够提供更多的类型安全性和功能性。


// 不好的例子 - 使用常量表示颜色
public static final int RED = 1;
public static final int GREEN = 2;
public static final int BLUE = 3;

// 好的例子 - 使用枚举表示颜色
public enum Color {
RED, GREEN, BLUE
}

16. 使用try-with-resources语句


在处理需要关闭的资源(如文件、数据库连接等)时,使用try-with-resources语句可以自动关闭资源,避免资源泄漏。


// 不好的例子 - 没有使用try-with-resources
FileReader reader = null;
try {
reader = new FileReader("file.txt");
// 执行一些操作
} catch (IOException e) {
// 处理异常
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
// 处理关闭异常
}
}
}

// 好的例子 - 使用try-with-resources自动关闭资源
try (FileReader reader = new FileReader("file.txt")) {
// 执行一些操作
} catch (IOException e) {
// 处理异常
}

总结


这16个小建议,希望对你有所帮助。


技术的路上充满挑战,但要相信,把细节搞好,技术会越来越牛。小小的优化,微小的改进,都能让我们的代码变得更好。


时间飞逝,我们要不断学习,不断努力。每个技术小突破都是我们不懈努力的成果。要用心倾听,用心琢磨,这样才能在技术的道路上越走越远。


从每一次写代码的过程中,我们收获更多。我们要踏实做好每个细节。在代码的世界里,细节是我们的罗盘。


坚持初心,不忘初心!


006APoFYly8hgexvlq6lug306c06c7qd (1).gif


作者:一只叫煤球的猫
来源:juejin.cn/post/7261835383201726523
收起阅读 »

AI 时代计算机专业会涨薪还是降薪

此前,在 2024 年世界政府峰会,英伟达 CEO 黄仁勋在被问及“如果站在科技的前沿,人们到底应该学习什么”时表示:“学计算机的时代过去了,生命科学是未来”。老黄的这个观点再结合现在 AI 的能力越来越强,这让报考计算机专业的考生会担心:未来 AI 时代计算...
继续阅读 »

image.png
此前,在 2024 年世界政府峰会,英伟达 CEO 黄仁勋在被问及“如果站在科技的前沿,人们到底应该学习什么”时表示:“学计算机的时代过去了,生命科学是未来”。老黄的这个观点再结合现在 AI 的能力越来越强,这让报考计算机专业的考生会担心:未来 AI 时代计算机专业会涨薪还是降薪?


未来的事情其实很难预测,我们只能根据一些历史经验来推导一下。


为什么计算机专业相对薪资较高?


通常员工的薪资由两个主要因素决定:1. 创造的价值;2. 技能的稀缺性;


像 Google、Meta、OpenAI 这些公司的程序员工资高,一方面他们创造了很大价值,另一方面他们所做的事情需要一定的技能,而掌握这些技能的人才相对较少。


AI 会让计算机专业薪资更高还是更低?


计算机专业从就业来看是比较广泛的,不仅仅是程序员,还有数据科学、人工智能、QA、产品设计、项目管理、开发管理等等方向,所以不能简单的谈 AI 对计算机专业对薪资的影响,而是对可能的岗位的影响。


对于技术岗位来说 AI 会创造更大价值,管理岗也许会贬值


从创造的价值来看,有了 AI 的加持,可以预见对于技术性的岗位,创造的价值都能更大,比如程序员借助 GitHub Copilot 辅助,生成代码效率会更高;借助 AI,QA 可以更多的让测试自动化起来;产品经理借助 AI,节约了大量写产品设计文档的时间。


但对于一些偏管理的岗位来说,无论是项目管理还是人员管理,在 AI 时代创造的价值可能反而会降低。一方面软件工程方面的进步,像 Scrum 这样的开发流程,项目经理的作用有限;另一方面随着程序开发效率的提升,团队会趋向小型化,有很多善用 AI 的超级个体,沟通成本会大幅下降,不需要太多的管理者。


高级 AI 开发、产品设计技能会更稀缺、基础编程和测试岗位会减少


稀缺性体现在两个方面:1. 这个技能掌握的难度;2. 是不是供小于求


按照 AI 能力的发展趋势来看,目前 AI 在编程还只能是 Copilot(副驾驶)这样的辅助角色,但即使如此,也能普遍提升 20% 左右的效率;几年过后可能就到 50% 了,直到最终替代人类编程。


这也意味着,对于基础编程和测试,掌握的难度会大幅降低,随着 AI 和自动化工具的进步,一些基本的编程任务和软件测试可以通过自动化工具来完成,岗位会减少。


短期来说它还不能马上替代的是:集成 AI 的产品设计、对需求进行分析拆解、复杂项目的架构设计、对复杂项目进行维护这些相对复杂的技能。也就是高级的编程、架构师、产品设计这些岗位,掌握的难度高,不容易被替代。


那么供求关系如何呢?未来 AI 时代,计算机专业相关的岗位是更多了还是更少了?


从去年开始,无论是应用还是服务,都在集成 AI,像苹果和微软,甚至都在操作系统层面为 AI 进行了重构,相应的,这会创造很多新的开发需求,有些类似于当年移动互联网,各个应用、服务都要提供移动版本,产生了很多岗位需求。可以预见中短期,未来 10-20 年以内,主要的服务和应用,都会集成 AI,并且随着 AI 能力的增强,持续的升级完善。这样的升级,会先从科技公司开始,然后再延伸到各个行业。


所以未来 10-20 年,我预计计算机岗位需求还是会和现在差不多,但是技能要求会有些变化,不再纯粹的是传统的编程,还需要对使用 AI、集成 AI 相关的技能要有掌握。这方面对于新从业者还有优势,没有历史包袱,可以很快适应,相反一些不愿意学习新技能的计算机专业从业者,反而学习适应的会差一些。


如果整体供求关系和现在差不多,而 AI 能创造更大价值,未来计算机专业薪资应该会更高,但前提是你得是属于掌握了 AI 技能的人才。


怎么可以让自己赶上 AI 时代的红利也能拿高薪?


不建议你只是为了高薪选择计算机专业


首先不建议你只是为了高薪选择计算机专业,这个行业看起来光鲜其实背后也很残酷,比如加班严重、年龄大了可能会被优化、新技术层出不穷。我见过很多因为高薪选择这个行业,但并喜欢,所以并不会花多少时间去学习去精进自己,几年后再找工作就会比较难。


建议多积累相关项目经验


然后建议多积累项目经验。计算机专业,最终都是要通过软件项目去创造出产品,进而通过产品创造价值。所以想拿高薪,一个基本前提就是你掌握了构建软件项目的部分关键技能,比如说编程、产品设计、测试等。当然如果你想当独立开发者,自己去产品,那要求会更高,除了掌握计算机专业技能,还得要一些营销的能力。


在 AI 时代,找工作对于新人不一定更友好,因为基础岗位很多会被 AI 代替,除了大公司,企业会倾向于招有经验的,这就意味着你能自己先积累经验,让自己更有竞争力。


要积累项目经验,可以参与开源项目,可以做一点给自己或者亲戚朋友用的小产品,可以去公司实习或者找一份相关的工作。


如果不是计算机专业也有机会


无论是不是 AI 时代,对于热爱计算机但是不是计算机专业的人来说,一直都有机会,见过太多非计算机专业自学成才的例子。


AI 时代,学习对新人来说却是要容易很多,比如学习编程,以前一个很大的门槛是没有老师指导、遇到问题没有人帮忙解决,而现在像 ChatGPT、Claude 这些大语言模型,可以随时随地咨询技术问题,遇到技术上的故障也可以帮助解决,让学习比以前容易很多。


即使是计算机专业、已经有几年工作经验的,在 AI 时代也一样需要再学习,因为在 AI 时代,对技能的要求会发生变化,比如你能借助 AI 提升开发效率,不然可能会被那些善用 AI 的同事卷下去;比如你得有能力帮助公司构建出 AI 时代的产品。


最后


如果你报考的是计算机专业,即使未来 AI 时代,也不必担心薪水下降;但未来找工作对新手不一定友好,需要在毕业前通过实习和自学多积累项目经验和 AI 相关技能。


如果你没能报考计算机专业,但是热爱计算机专业,也一样有机会,自学成才的例子很多,尤其在 AI 时代,学习的门槛会更低,花点功夫可以比科班学的还好。


祝今年的考生们都选到自己心仪的专业,毕业拿高薪。


作者:宝玉的工程技术分享
来源:juejin.cn/post/7386290071189635083
收起阅读 »

2024转行前端第6年

前言 上一篇《外行转码农,焦虑到躺平》分享我的从业经历收到大家很多关注,这里继续分享一下我的生活 我是16毕业在厂里干了两年,18年7月转行前端开发。在后面4年,也就是18~22年是很投入的,这几年上完班回到家基本也在学习,学技术考证,可谓说没有自己的生活,这...
继续阅读 »

前言


上一篇《外行转码农,焦虑到躺平》分享我的从业经历收到大家很多关注,这里继续分享一下我的生活


我是16毕业在厂里干了两年,18年7月转行前端开发。在后面4年,也就是18~22年是很投入的,这几年上完班回到家基本也在学习,学技术考证,可谓说没有自己的生活,这几年是我技术进步最快的几年,也是很快乐的几年,大家懂得全身投入做一件事的快乐吧。


近几年,感觉自己很难突破自己的瓶颈了,慢慢将重心转为生活。也要声明一下,这两年我可不是完全躺平,对应工作我还是很负责的去做,只是工作不是我的全部了。分享一下这几年的收获吧


近几年收获


2019



  • 中级软件设计师


2020



  • 基金从业

  • 证券从业


2021



  • pmp

  • 彩铅绘画


2022



  • 计算机专业课程系统学习

  • 背了5000单词


2023



  • 算法 150道

  • 浅浅玩了区块链技术、android、ios开发

  • 中级经济师


2024



  • 缝纫

  • 计划考 中级会计


看书


我之前基本不怎么看书的,后面慢慢与书籍成为了朋友。这几年读了上百本书,主要涉及:哲学、心理学、经济学、文学、历史、政治。这些可是我之前不怎么关注的,曾经的我很封闭,对全球地理概念都不清楚那种。老爸也说我现在知识面扩展了很多。


醒悟


感觉28岁是我的醒悟元年,告别过往原谅也放过自己,生活从新开始吧。


之前的我,对于朋友没有秘密,很依赖别人,后面慢慢懂得自己的痛苦只能自己慢慢消化,我学会了用 “行为认知疗法”治愈自己,很推荐大家去了解。


分享


我把这些放到b站了,大概感兴趣可以去看哦,搜索 liucheng58


读书笔记


最近不开心看的两本书


-《蛤蟆先生去看心理医生》



“PLOM代表了四个英文单词,意思就是‘可怜弱小的我呀’。这个游戏你每局都赢了,也可以说是输了,这取决于你自己的看法。”



把我们的人生当作一次游戏,跳出自我,静静俯视它的发展。个人在这场游戏中,可能就是游戏背景中一颗小小草,在画面中一闪而过,没有人注意到它刚被怪兽踩了一脚。
而自我的人一直盯着想着那颗小草,而忽略了整个游戏的乐趣。世界上有很多有趣的事情,不要总是计较一时的得失——致自己。



“‘成人自我状态’指我们用理性而不是情绪化的方式来行事。它让我们能应对此时此地正在发生的现实状况。”苍鹭回答。“‘成人自我状态’指我们用理性而不是情绪化的方式来行事。它让我们能应对此时此地正在发生的现实状况。”苍鹭回答。



我这两年最大的三个变化

1、遇事不发脾气,冷静思考当下如何解决;

2、如果没法推脱,就怀着快乐的心态去做吧,和那个任性的小孩好好商量,讲解做这件事的好处,不要抱怨的把事情做了,费力不讨好;

3、聆听他人,角度不同想法不同,没有谁对谁错。


-《幸福之路》



一个人应该认真建立自己的意念,让它与理性相信的方向一致,不要让任何非理性的信仰不经过检视就占据自己的心灵。



回头看老一辈很多坚信的观念我们都无法接受,这就是他们小时候接受到的信息刺激。一个人改变自己的观念很难,改变既有的思维模式很难,但是如果能打破它们,我们就是放过了自己。之前看到做法就是每日自检,找出困扰你的想法,分解并破解。


史铁生说他只能躺在床上的时候他无比怀念坐轮椅的时候,只有体验人生角色的酸痛苦辣,才能做到感同身受,仅仅看书冥想很多东西无法吸收,我想这也是成佛要经历81难吧


image.png



一个把注意力转向自我内心的人,会找不到任何值得关注的事;而那些对外界事物兴致勃勃的人,当他偶尔把注意力转移到自己的灵魂,他会发现所有以前采集、累积的各式各样有趣的材料,都已经被转换重组成美丽且有价值的东西



我的理解是:不要太在乎一时的得失、不要经常悔恨自己的错误,其实命运早已注定。我们应该跳出对自身的注视,面临抉择充分收集信息,然后一条路走到黑,然后让它自由发展。其他时候我的精力要跳出自身,去体验大好河山,体验先人深邃精神。



所有需要技巧的工作都有带给人乐趣的共性,只要这技巧有价值和无限的进步空间



就像程序员很多人最开始是喜欢而从事,写代码是个建设的过程,看着代码运行成一个个应用。但是工作3-5年后,很熟练了,很难有进步空间,很多人就怠慢了。最重要的是很多人意识到,再牛赚的钱也是很有限的。


缝纫


最近朋友圈分享自己第一件缝纫作品 ,收到朋友很多赞赏,我感觉我有点飘了。这里我再分享一次


image.png


绘画


我五音不全,但是很喜欢绘画,21年报了个兴趣班,画了一些彩铅,这两年没画了。


买了平板、画笔和procreate软件,后面打算学习一下平板绘画,感觉还是电子的容易保存。后面如果开动了,再分享我的成果。


image.png


猫猫


2020年5月养了第一只中华田园猫,后面养过5只中华田园猫:莞莞、果果、蕉蕉;救助过两只流浪猫:小黄、小白;


小黄是在地下车库别人车底,小白是在公园草丛,两只猫猫都是连续叫了好几天,又怕人那种小猫;后面我蹲了几小时,用捕猫笼给抓住的;它们都是又瘦又脏,养肥了洗了澡,教了它们使用猫砂,后面给它们找了人家。


image.png


做饭


我现在每天都自己带饭,我做饭的效率很高,周末将肉切好,炒两个菜一般半小时可以搞定。


image.png


面食


买了蒸锅、烤箱、电饼铛,做了包子、面包和饼饼,这些也是我很爱吃的


image.png


最后


我的人生有很多遗憾,遗憾成熟太晚、遗憾高考失利没复读,但是现在也放下了,我总要体会我人生角色的起起伏伏,痛苦使我成长。


我的执行力还是差了点,很多想做的都搁置了。自己习惯依赖,随波逐流,总希望别人给自己人生一个规划,这几年也是一个去依赖的过程。


我之前对自己要求很高,现在功利心下降了,做的事情都是随性而行,才发现自己兴趣很多。


我买了中国三维地图和世界三维地图,曾经很想抛开现有的去外面看看,我爸说我幼稚不成熟,这几年这个想法也放弃了,不知道是成熟了还是向生活妥协了。


接下来,我希望自己用一个平静的心态度过此生,不以物喜不以己悲。


希望被大家温柔以待,不要中伤我。




作者:chengliu0508
来源:juejin.cn/post/7349931303787839499
收起阅读 »

外行转码农,焦虑到躺平

下一篇《2024转行前端第6年》展示我躺平后捣鼓的东西 介绍自己 本人女,16年本科毕业,学的机械自动化专业,和大部分人一样,选专业的时候是拍大腿决定的。 恍恍惚惚度过大学四年,考研时心比天高选了本专业top5学校,考研失败,又不愿调剂,然后就参加校招大军。可...
继续阅读 »

下一篇《2024转行前端第6年》展示我躺平后捣鼓的东西


介绍自己


本人女,16年本科毕业,学的机械自动化专业,和大部分人一样,选专业的时候是拍大腿决定的。


恍恍惚惚度过大学四年,考研时心比天高选了本专业top5学校,考研失败,又不愿调剂,然后就参加校招大军。可能外貌+绩点优势,很顺利拿到了很多工厂offer,然后欢欢喜喜拖箱带桶进厂。


每天两点一线生活,住宿吃饭娱乐全在厂区,工资很低但是也没啥消费,住宿吃饭免费、四套厂服覆盖春夏秋冬。


我的岗位是 inplan软件维护 岗位,属于生产资料处理部门,在我来之前6年该岗位一直只有我师傅一个人,岗位主要是二次开发一款外购的软件,软件提供的api是基于perl语言,现在很少有人听过这个perl吧。该岗位可能是无数人眼里的神仙岗位吧,我在这呆了快两年,硬是没写过一段代码...


inplan软件维护 岗位的诞生就是我的师傅开创的,他原本只是负责生产资料处理,当大家只顾着用软件时,他翻到了说明书上的API一栏,然后写了一段代码,将大家每日手工一顿操作的事情用一个脚本解决了,此后更是停不下来,将部门各种excel数据处理也写成了脚本,引起了部门经理的注意,然后就设定了该岗位。


然而,将我一个对部门工作都不了解的新人丢在这个岗位,可想我的迷茫。开始半年师傅给我一本厚厚的《perl入门到精通》英文书籍,让我先学会 perl 语言。(ps:当时公司网络不连外网,而我也没有上网查资料的习惯,甚至那时候对电脑操作都不熟练...泪目)


师傅还是心地很善良很单纯的人,他隔一段时间会检查我的学习进度,然而当他激情澎拜给我讲着代码时,我竟控制不住打起了瞌睡,然后他就不管我了~~此后我便成了部门透明人物,要是一直透明下去就好了。我懒散的工作态度引起了部门主管的关注,于是我成了他重点关注的对象,我的工位更是移到了他身后~~这便是我的噩梦,一不小心神游时,主管的脸不知啥时凑到了我的电脑屏幕上~~~😱


偶然发现我的师傅在学习 php+html+css+js,他打算给部门构建一个网站,传统的脚本语言还是太简陋了。我在网上翻到了 w3scool离线文档 ,这一下子打开了我的 代码人生。后面我的师傅跳槽了,我在厂里呆了两年觉得什么都没学到,也考虑跳槽了。


后面的经历也很魔幻,误打误撞成为了一名前端开发工程师。此时是2018年,算是前端的鼎盛之年吧,各种新框架 vue/react/angular 都火起来了,各种网站/手机端应用如雨后春笋。我的前端之路还算顺利吧,下面讲讲我的经验吧


如何入门


对于外行转码农还是有一定成本的,省心的方式就是报班吧,但是个人觉得不省钱呀。培训班快则3个月,多的几年,不仅要交上万的培训费用,这段时间0收入,对于家境一般的同学,个人不建议报班。


但是现在市场环境不好,企业对你的容忍度不像之前那么高。之前几年行业缺人,身边很多只懂皮毛的人都可以进入,很多人在岗位半年也只能写出简单的页面,逻辑复杂一点就搞不定~~即使被裁了,也可以快速找到下家。这样的日子应该一去不复返了,所以我们还是要具备的实力,企业不是做慈善的,我们入职后还是要对的起自己的一份工资。


讲讲具体怎么入门吧


看视频:


b站上有很多很多免费的视频,空闲之余少刷点段子,去看看这些视频。不要问我看哪个,点击量大的就进去看看,看看过来人的经验,看看对这个行业的介绍。提高你的信息量,普通人的差距最大就在信息量的多少


还是看视频:


找一个系统的课程,系统的学习 html+css+js+vue/react,我们要动手写一些demo出来。可以找一些优秀的项目,自己先根据它的效果自己实现,但后对着源码看看自己的局限,去提升。


做笔记:


对于新人来说,就是看了视频感觉自己会了,但是写起来很是费力。为啥呢?因为你不知道也记不住有哪些api,所以我们在看视频学习中,有不知道的语法就记下来。

我之前的经验就是手动抄写,最初几年抄了8个笔记本,但是后面觉得不是很方便,因为笔记没有归纳,后续整理笔记困难,所以我们完全可以用电子档的形式,这方便后面的归纳修改。


回顾:


我们的笔记做了就要经常的翻阅,温故而知新,经常翻阅我们的笔记,经常去总结,突然有一天你的思维就上升了一个高度。



  • 慢慢你发现写代码就是不停调用api的过程

  • 慢慢你会发现程序里的美感,一个设计模式、一种新思维。我身边很多人都曾经深深沉迷过写代码,那种成就感带来的心流,这是物质享受带来不了的


输出:


就是写文章啦,写文章让我们总结回顾知识点,发现知识的盲区,在这个过程中进行了深度思考。更重要的是,对于不严谨的同学来说,研究一个知识点很容易浅尝则止,写文章驱动自己去更深层系统挖掘。不管对于刚入行的还是资深人士,我觉得输出都是很重要的。


持续提升


先谈谈学历歧视吧,现在很多大厂招聘基本条件就是211、985,对此很是无奈,但是我内心还是认可这种要求的,我对身边的本科985是由衷的佩服的。我觉得他们高考能考上985,身上都是有过人之处的,学习能力差不了。


见过很多工作多年的程序员,但是他们的编码能力无法描述,不管是逻辑能力、代码习惯、责任感都是很差的,写代码完全是应付式的,他们开发的代码如同屎山。额,但是我们也不要一味贬低他人,后面我也学会了尊重每一个人,每个人擅长的东西不一样,他可能不擅长写代码,但是可能他乐观的心态是很多人不及的、可能他十分擅长交际...


但是可能的话,我们还是要不断提高代码素养



  • 广度:我们实践中,很多场景没遇到,但是我们要提前去了解,不要等需要用、出了问题才去研究。我们要具备一定的知识面覆盖,机会是给有准备的人的。

  • 深度:对于现在面试动不动问源码的情况,很多人是深恶痛绝的,曾经我也是,但是当我沉下心去研究的时候,才发现这是有道理的。阅读源码不仅挺高知识的广度,更多让我们了解代码的美感


具体咋做呢,我觉得几下几点吧。(ps:我自己也做的不好,道理都懂,很难做到优秀呀~~~)



  • 扩展广度:抽空多看看别人的文章,留意行业前沿技术。对于我们前端同学,我觉得对整个web开发的架构都要了解,后端同学的mvc/高并发/数据库调优啥的,运维同学的服务器/容器/流水线啥的都要有一定的了解,这样可以方便的与他们协作

  • 提升深度:首先半路出家的同学,前几年不要松懈,计算机相关知识《操作系统》《计算机网络》《计算机组成原理》《数据结构》《编译原理》还是要恶补一下,这是最基础的。然后我们列出自己想要深入研究的知识点,比如vue/react源码、编译器、低代码、前端调试啥啥的,然后就沉下心去研究吧。


职业规划


现在整个大环境不好了,程序员行业亦是如此,身边很多人曾经的模式就是不停的卷,卷去大厂,跳一跳年薪涨50%不是梦,然而现在不同了。寒风凌凌,大家只想保住自己的饭碗(ps:不同层次情况不同呀,很多大厂的同学身边的同事还是整天打了鸡血一般)


曾经我满心只有工作,不停的卷,背面经刷算法。22年下半年市场明显冷下来,大厂面试机会都没有了,年过30,对大厂的执念慢慢放下。


我慢慢承认并接受了自己的平庸,然后慢慢意识到,工作只是生活的一部分。不一定要担任ceo,才算走上人生巅峰。最近几年,我爱上了读书,以前只觉得学理工科还是实用的,后面慢慢发现每个行业有它的美感~


最后引用最近的读书笔记结尾吧,大家好好体会一下论语的“知天命”一词,想通了就不容易焦虑了~~~



自由就是 坦然面对生活,看清了世界的真相依然热爱生活。宠辱不惊,闲看庭前花开花落。去留无意,漫随天外云卷云舒。



image.png




作者:liucheng58
来源:juejin.cn/post/7343138429860347945
收起阅读 »

已老实!公司的代码再也不敢乱改了!

开篇 大家好,我是聪。想必对于很多初入职场,心中怀着无限激情的兄弟们,对于接手老代码都会有很多愤慨,碰到同事的代码十分丑陋应不应该改!我也是这样,我相信有很多人同样有跟我一样的经历。满打满算实习 + 正式工作,我也敲了两年多代码,我今天来说说我自己的看法吧。 ...
继续阅读 »

开篇


大家好,我是聪。想必对于很多初入职场,心中怀着无限激情的兄弟们,对于接手老代码都会有很多愤慨,碰到同事的代码十分丑陋应不应该改!我也是这样,我相信有很多人同样有跟我一样的经历。满打满算实习 + 正式工作,我也敲了两年多代码,我今天来说说我自己的看法吧。


亲身经历


我第一次接手老代码的时候,映入我眼帘的就是侧边栏满页的黄色提示以及代码下面的众多黄色波浪线,以及提交代码时的提示,如下图:


image-20240620084453396.png


我内心 OS:


1)大干一场,把黄色波浪线全干掉!


2)同事这写的也太不优雅了吧,改成我这样!


3)这代码怎么也没格式化,我来 Ctrl + Alt + L 格式化一波!


已老实,求放过


image-20240620095636440.png


干掉黄色波浪线,将代码改 ”优雅“ 结局如下:


1)不声不吭动了同事代码,换来同事怒骂,毕竟人家逻辑写好,然后你按你想法来搞,也没有跟人家商量。


2)后续领导找你加需求,你发现原来之前的代码有妙用,你悔不当初,被扣绩效。


3)格式化后,在项目修改记录上面是你的修改,这代码出问题,负责人先来找你。


说说我的看法


代码能跑不要动


前几日我要在老项目中,新增一点小功能,在新增完功能后,我扫了一眼代码,发现有几处逻辑根本不会执行,比如:抛异常后,执行删除操作类似,我也不会去义愤填膺的去干掉这块代码,毕竟我想到一点!项目都跑七八年没出问题了,能跑就别动它。


代码强迫症不要强加于别人


前几日在某金看见了这样一个沸点:


image-20240620095955748.png

这样的事情其实在小公司经常发生,你觉得它写的不优雅,封装少,可能是别人也有别人的难处,至少不能将自己想法强加于别人,比如领导突然来一个需求,跟你说今天你得完成,然后第二天这个需求,你要这样改、再给我加点新需求上去,你能想到的封装其实只是你冷静下来,而且没有近乎疯狂的迭代需求得到的想法,当你每天都要在原代码上面疯狂按照领导要求修改,可能你会有自己的看法。


新增代码,尽量不影响以前逻辑


image-20240618163649720.png


新增代码的时候,尽量按照以前的规则逻辑来进行,比如我改的一个老项目,使用的公司自己写的一套 SQL 处理逻辑,我总不能说不行!我用不惯这个!我要用 MyBatis!!!!那真的直接被 T 出门口了。


尊重他人代码风格


每个人的代码风格都有所不同,这个很正常,不同厨师的老师教法不一样,做出的味道还不一样呢,没有最好的代码,只有更适合的代码,刚好我就有这样的例子:


我注入 Spring 依赖喜欢用构造注入、用 Lombook 的注解 @RequiredArgsConstructor 注入,我同事喜欢 @Autowired ,我能说他不准用这个吗,这个是人家的习惯,虽然 Spring 也不推荐使用这个,但改不改这个都不会影响公司收益,反而能少一件事情,促进同事友好关系,哈哈哈哈,我是这样认为的。


处理好同事之间的关系


哈哈哈哈这个真的就是人情事故了,你换位想象一下,如果你写的幸幸苦苦的代码,新来的同事或者实习生,来批评你的代码不规范,要 Diss 你,偷偷改你代码,就算他说的超级对,你心里都十分不好受,会想一万个理由去反驳。


我一般如果需求需要改动同事的代码,我会先虚心的向同事请求,xx哥,我这个需求要改动你这边的代码来配合一下,你来帮我一起看看,你这部分的代码这样改合理吗,或者你自己改下你自己的部分,然后我合并一下~ 谢谢 xx哥。


image-20240620102755432.png


最后


希望大家都能遇到与自己志同道合的同事一起快乐的开发~,我经历的可能没有大家伙的多,大家如果还碰见因为改老代码发生的惨案,欢迎大家一起来分享,我也来跟大佬们学习一下~


作者:cong_
来源:juejin.cn/post/7383342927508799539
收起阅读 »

离职前同事将下载大文件功能封装成了npm包,赚了145块钱

web
这几天有个同事离职了,本来那是last day,还有半个小时,但他还在那里勤勤恳恳的写着代码。我很好奇,就问:老张,你咋还不做好准备,都要撤了,还奋笔疾书呐。他说:等会儿和你说。 等了半个小时,他说:走,一起下班。我跟你说个好东西。 我说:好的。 老张一边走...
继续阅读 »

这几天有个同事离职了,本来那是last day,还有半个小时,但他还在那里勤勤恳恳的写着代码。我很好奇,就问:老张,你咋还不做好准备,都要撤了,还奋笔疾书呐。他说:等会儿和你说。



等了半个小时,他说:走,一起下班。我跟你说个好东西。


我说:好的。


老张一边走一边跟我说:公司的下载大文件代码不好。


我说哪里不好了,不是都用了很久了。


他说,那些代码,每次项目需要的时候,还得拷过来拷过去的,有时候拷着拷着就拷丢了,还得去网上现找代码,很不好。


我问:那然后呢?


他说:我这两天把这段代码封装了一下,封装成了npm包。以后,大家就直接调用就可以了,不用重复造轮子,或者担心轮子走丢了。我说那太好了。


他说:我把这个npm包给你,以后你就说自己写的,下个季度晋升的时候,你就说,为公司解决了代码冗余,重复造轮子的问题,而且让下载大文件功能更加便捷,节省开发时间,提升了开发效率。


我说:那怎么好啊,得请你吃个饭啊,你都要走了。不过,你先跟我说说,怎么用这个npm包啊。



下载大文件版


比如我们现有的成形的项目,大家使用axios或者fetch,一定在项目里已经封装好了请求,所以直接调用服务端给的请求地址,获取到blob数据流信息就可以了。但是拿到blob数据流以后,这段代码得四处拷贝,重复造轮子,很不好。所以可以这样使用,高效、便捷。


下载js-tool-big-box工具包


执行安装命令



npm install js-tool-big-box



项目中引入ajaxBox对象,下载文件的公共方法,downFile 在这个对象下面。


import { ajaxBox } from 'js-tool-big-box';

调用实现下载


比如你在项目中已经封装好了axios或者fetch的实现,那么只需要正常发送请求,然后调用方法即可,使用非常方便。


fetch('https://test.aaa.com/getPDF').then(res => res.blob()).then((blob) => {
ajaxBox.downFile(blob, '优乐的美.pdf');
});

在这个方法中,你只要将接口返回的信息流转为blob流,然后传入 downFile 方法中,然后再传入一个参数做为下载后的文件名即可。


fetch请求 + 下载实现版本


我又问他,的确是很多项目里,请求都已经封装好了。但我之前做过一个项目,功能很简单,大部分都是展示类的。但产品在一个详情页,让我加下载功能,我的请求并没有做封装。


然后呢,服务端告诉我,这个下载文件的接口,还需要传入参数params,需要传入headers,你这个方法就不适用了吧?


他想了一下,说。也是可以的,你听我说啊。


定义请求参数们


const url = 'https://test.aaaa.com/getPDF';
const headers = {
'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8',
'CCC-DDD': 'js-tool-big-box-demo-header'
}
const params = {
name: '经海路大白狗',
startDate: '2024-03-05',
endDate: '2024-04-05',
}

你看这些参数了吗?url就是下载文件需要的那个接口,如果是get请求呢,你就按照get形式把参数拼接上去,如果是post形式呢,你就需要后面的这个params变量做为入参数据。如果服务端需要headers呢,你就再将headers定义好,准备往过传。


调用实现


ajaxBox.downFileFetch(url, '相的约奶的茶.mp4', 'get', headers, dataParams);

你看到这个 downFileFetch 方法了吧,他也在 ajaxBox 对象下面。



第一个参数呢,表示服务端接口,如果是get请求呢,就把参数拼接上去;


第二个参数呢,表示下载后文件名,比如 down.pdf 这样;


第三个参数呢,默认是get请求,如果不想写get呢,你就写个null,但是你得写进去,如果服务端要求是个post请求呢,你就写post;
第四个参数呢,就是headers啦,服务端需要你就传过去,不需要你就写个null;
第五个参数呢,如果是psot请求,你就传入json对象过去,如果没有参数,你就不写也行,写个null也行。



我说:你这个工具库真是棒,js-tool-big-box,就是前端JS的一个大盒子啊。他说:是的,里面还有很多特别实用的方法,用了这个工具库后,前端项目可以少些很多公共方法,少引很多第三方库,很不错的。我也要离职了,你在公司就说这是你开发的。


我说:那我得请你吃饭啊。于是,我去买了一瓶茅台王子酒,花了260元,定了两份炒饼,花了30元。


等吃完,我说,你这个工具库可以啊,直接从我这里挣了290元。


他说:看你说的,酒你喝了一半,炒饼你吃了一份。我这顶多也就是145元啊。


看完不过瘾?这里有更全的js-tool-big-box使用指南哦,掘金链接直达(只会Vue的我,一入职就让用React,用了这个工具库,我依然高效 - 掘金 (juejin.cn))




最后告诉你个消息:
js-tool-big-box的npm地址是(js-tool-big-box 的npm地址)


js-tool-big-box的git仓库地址(js-tool-big-box的代码仓库地址)


作者:经海路大白狗
来源:juejin.cn/post/7379524605104848946
收起阅读 »

34岁程序员带全家离开北京的故事

哈喽大家好,我是大圣,正如标题所说,我离开北京了 视频链接 今天来聊一下我在北京的这17年,北京好在哪,以及为什么要选择离开, 以及下一步的打算 想 提前声明,我下面会说一些北京和英国的优点,我个人只在英国生活的一个月,我体验到的优点肯定非常片面,也欢迎评论区...
继续阅读 »

哈喽大家好,我是大圣,正如标题所说,我离开北京了


视频链接


今天来聊一下我在北京的这17年,北京好在哪,以及为什么要选择离开, 以及下一步的打算

提前声明,我下面会说一些北京和英国的优点,我个人只在英国生活的一个月,我体验到的优点肯定非常片面,也欢迎评论区讨论


离开北京的当天我还兴致勃勃的拿着gopro 准备拍个视频,但是下了楼看到北京天,突然很感慨,没了拍视频的兴致,拍几个照留个纪念吧


image.png


image.png


image.png


image.png


image.png


image.png


image.png


北京好在哪


我2007年来北京念书,咱们国家排名前50的那200所高校之一,我们家家庭条件不是特别好,但是北京让我开了眼界


我来自一个国家级贫困县,我跟我哥俩人上大学比较费钱,到我大学毕业那天起,我们家外面还欠着外债,经济虽然窘迫,但是我也感受到了后海步行街的灯红酒绿,鸟巢水立方世界级赛事,毕业之后互联网黄金时代的革命,也正是北京各地的创业咖啡厅里,很多投资人和创业者激情的演讲,让我接触到软件编程,成就了现在的我


可以说一线城市虽然压力很大,但还是给了普通人很多的机会,如果我毕业就在我们老家那边阜阳,或者努努力去省会合肥,生活应该也很棒,我可能就不会有现在这么多选择


北京很好,我在这里工作,学习编程,创业,我33岁的时候好像完成了人生第一阶段的任务,在北京有房有车,我媳妇有北京户口,看起来养娃也没什么问题,那为什么要离开呢


两个原因吧,养娃和个人职业实现


从我媳妇查出来怀孕的那天,我兴奋了一整天,但是到了晚上就开始和媳妇讨论,怎么让他成为一个快乐的孩子,讨论了俩星期吧


我其实已经卷出来了,但是我不想让孩子也走这条路,我觉得比较辛苦,他现在还只是一个胚胎,后面的中高考,考研考公,面试,裁员等等,有没有什么事情能让他快乐一些,以后大学报志愿的时候,也不会只看就业率如何来选择,讨论来 讨论去,最后冒出一个新想法,要不出国试试,都说老外快乐教育


第二个就是我跟媳妇个人的价值实现,我媳妇是低我三届的学妹,我俩处对象那会他还在读研,她学设计的,腾讯当产品经理实习生,为了北京户口去了国企,算是为家庭做了一些贡献,我感觉国企的工作稍显琐碎一些


我在的互联网行业,很刺激很好玩,很严重的35岁裁员危机,最近大家也能看到很多新闻,工资确实高,裁员也猛


我们都希望能有一个有趣的,持久的职业生涯,能够有大量的时间,去学习怎么成为一个合格的父母,少赚一些可以的,但是离开北京,国内的城市好像很难实现


而且我跟我媳妇一直都有一个环游世界的梦想,只是一只没机会实现,所以,出国这两个字 就在我们家出现了


为什么选英国


首先基本大家都有点英语基础,但是学起来很难受,所以真的不想学二外,并且咱们普通人,基本投资移民和咱们也没啥关系,不花钱的工签就是性价比最高的


所以符合这个要求的,能找到程序员工作的美国,新加坡,加拿大,澳洲,英国,新西兰,和一些欧洲的国家


然后不想努力奋斗了,排除了美国和新加坡


支持远程工作,不卷,最好时区和国内有重合,我国内的卖课服务可以继续


当然准备去后,英国有一些优点我觉得可以参考,可能其他国家也有 欢迎补充



  1. 英国好学校还挺多,以后对孩子教育估计不错

  2. 英国没有蚊子蛇,想想夏天就爽

  3. 英国PR拿到后,每年只需要入境一次就可以续,所以我可以五年后继续回国定居,同时保持英国PR,如果孩子以后回国念书,我这几年也不算浪费

  4. 英国有阿森纳,对我来说优势太大了,我已经期待去唱north london forever了,掉点眼泪估计是必须的

  5. 这边的文化生活还是挺丰富的,博物馆,演出之类的

  6. 比较严格的八小时工作制,是真的不加班,而且remote的还挺多的,很多非remote的也是每周去两天左右,有很多的时间来做课程,陪家人,年假也挺多的,我记得是20多天,准备度假的时候再用

  7. 空气我觉得也不错,无论晴天阴天,都没什么发白的雾霾的感觉


不过以上都不是硬性条件,比如美国,加拿大澳大利亚我也会看机会,我也会考虑去奋斗,整体比较随缘, 我之前也分享过一些学英语的内容,然后给英国这边remote了半年后,问了下工签政策,元旦那会就担保过来了,然后体验了一个月,觉得还能接受,然后回国 准备搬家的事


打算


英国当然也有特别多的缺点,比如我一个朋友就因为税务和天气问题,准备去新加坡,也有一个因为职业发展问题准备去北美奋斗,也同时有朋友努力学英语去澳洲,和从澳洲努力准备回国


都是个人选择,主要就是你想过什么样的生活


英国吃的也不咋地,我也不太喜欢这边的酒吧文化,也没有国内安全,所以我也在探索


我在英国还是remote,所以暂时就在伦敦和周边生活,反正5年后拿绿卡,后面需要很努力的带娃,花大量的时间生活,旅游,比如在欧洲多玩一玩


同时继续发展卖课视野,做一个好的讲师,研发远程,或者web3的开发课程,除了国内也开辟一下欧洲市场


伦敦这边公园特别多,我住的两个地方周边溜达十分钟,都有草坪非常好的公园,非常适合遛娃和遛狗


而且也很适合养狗,狗可以坐地铁,火车,非常方便


我家孩子也一岁了,我希望以后我能成为一个合格的父亲把,能够陪他成长,在我的能力范围内,给他多一些选择,多快乐一些


找一些能持续成长的爱好,能够和孩子一起成长,比如踢球网球,重新开始打dota2,黑悟空也准备配个电脑好好玩,回归生活


下一期聊一下英国这边的生活体验,成本啥的吧,开心工作,努力生活
视频版


image.png


image.png


作者:花果山大圣
来源:juejin.cn/post/7380513226155868214
收起阅读 »

一个小公司的技术开发心酸事

背景 长话短说,就是在2022年6月的时候加入了一家很小创业公司。老板不太懂技术,也不太懂管理,靠着一腔热血加上对实体运输行业的了解,加上盲目的自信,贸然开始创业,后期经营困难,最终散伙。 自己当时也是不察,贸然加入,后边公司经营困难,连最后几个月的工资都没给...
继续阅读 »

背景


长话短说,就是在2022年6月的时候加入了一家很小创业公司。老板不太懂技术,也不太懂管理,靠着一腔热血加上对实体运输行业的了解,加上盲目的自信,贸然开始创业,后期经营困难,最终散伙。


自己当时也是不察,贸然加入,后边公司经营困难,连最后几个月的工资都没给发。


当时老板的要求就是尽力降低人力成本,尽快的开发出来App(Android+IOS),老板需要尽快的运营起来。


初期的技术选型


当时就自己加上一个刚毕业的纯前端开发以及一个前面招聘的ui,连个人事、测试都没有。


结合公司的需求与自己的技术经验(主要是前端和nodejs的经验),选择使用如下的方案:



  1. 使用uni-app进行App的开发,兼容多端,也可以为以后开发小程序什么的做方案预留,主要考虑到的点是比较快,先要解决有和无的问题;

  2. 使用egg.js + MySQL来开发后端,开发速度会快一点,行业比较小众,不太可能会遇到一些较大的性能问题,暂时看也是够用了的,后期过渡到midway.js也方便;

  3. 使用antd-vue开发运营后台,主要考虑到与uni-app技术栈的统一,节省转换成本;


也就是初期选择使用egg.js + MySQL + uni-app + antd-vue,来开发两个App和一个运营后台,快速解决0到1的问题。


关于App开发技术方案的选择


App的开发方案有很多,比如纯原生、flutter、uniapp、react-native/taro等,这里就当是的情况做一下选择。



  1. IOS与Android纯原生开发方案,需要新招人,两端同时开发,两端分别测试,这个资金及时间成本老板是不能接受的;

  2. flutter,这个要么自己从头开始学习,要么招人,相对于纯原生的方案好一点,但是也不是最好的选择;

  3. react-native/taro与uni-app是比较类似的选择,不过考虑到熟练程度、难易程度以及开发效率,最终还是选择了uni-app。


为什么选择egg.js做后端


很多时候方案的选择并不能只从技术方面考虑,当是只能选择成本最低的,当时的情况是egg.js完全能满足。



  1. 使用一些成熟的后端开发方案,如Java、、php、go之类的应该是比较好的技术方案,但对于老板来说不是好的经济方案;

  2. egg.js开发比较简单、快捷,个人也比较熟悉,对于新成员的学习成本也很低,对于JS有一定水平的也能很快掌握egg.js后端的开发


中间的各种折腾


前期开发还算顺利,在规定的时间内,完成了开发、测试、上线。但是,老板并没有如前面说的,很快运营,很快就盈利,运营的开展非常缓慢。中间还经历了各种折腾的事情。



  1. 老板运营遇到困难,就到处找一些专家(基本跟我们这事情没半毛钱关系的专家),不断的提一些业务和ui上的意见,不断的修改;

  2. 期间新来的产品还要全部推翻原有设计,重新开发;

  3. 还有个兼职的领导非要说要招聘原生开发和Java开发重新进行开发,问为什么,也说不出什么所以然,也是道听途说。


反正就是不断提出要修改产品、设计、和代码。中间经过不断的讨论,摆出自己的意见,好在最终技术方案没修改,前期的工作成果还在。后边加了一些新的需求:系统升级1.1、ui升级2.0、开发小程序版本、开发新的配套系统(小程序版本)以及开发相关的后台、添加即时通信服务、以及各种小的功能开发与升级;


中间老板要加快进度了就让招人,然后又无缘无故的要开人,就让人很无奈。最大的运营问题,始终没什么进展,明显的问题并不在产品这块,但是在这里不断的折腾这群开发,也真是难受。


明明你已经很努力的协调各种事情、站在公司的角度考虑、努力写代码,却仍然无济于事。


后期技术方案的调整



  1. 后期调整了App的打包方案;

  2. 在新的配套系统中,使用midway.js来开发新的业务,这都是基于前面的egg.js的团队掌握程度,为了后续的开发规范,做此升级;

  3. 内网管理公用npm包,开发业务组件库;

  4. 规范代码、规范开发流程;


人员招聘,团队的管理


人员招聘


如下是对于当时的人员招聘的一些感受:



  1. 小公司的人员招聘是相对比较难的,特别是还给不了多少钱的;

  2. 好在我们选择的技术方案,只要对于JS掌握的比较好就可以了,前后端都要开发一点,也方便人员工作调整,避免开发资源的浪费。


团队管理


对于小团队的管理的一些个人理解:



  1. 小公司刚起步,就应该实事求是,以业务为导向;

  2. 小公司最好采取全栈的开发方式,避免任务的不协调,造成开发资源的浪费;

  3. 设置推荐的代码规范,参照大家日常的代码习惯来制定,目标就是让大家的代码相对规范;

  4. 要求按照规范的流程设计与开发、避免一些流程的问题造成管理的混乱和公司的损失;

    1. 如按照常规的业务开发流程,产品评估 => 任务分配 => 技术评估 => 开发 => 测试 => cr => 上线 => 线上问题跟踪处理;



  5. 行之有效可量化的考核规范,如开发任务的截止日期完成、核心流程开发文档的书写、是否有线上bug、严谨手动修改数据库等;

  6. 鼓励分享,相互学习,一段工作经历总要有所提升,有所收获才是有意义的;

  7. 及时沟通反馈、团队成员的个人想法、掌握开发进度、工作难点等;


最后总结及选择创业公司避坑建议!important



  1. 选择创业公司,一定要确认老板是一个靠谱的人,别是一个总是画饼的油腻老司机,或者一个优柔寡断,没有主见的人,这样的情况下,大概率事情是干不成的;

    1. 老板靠谱,即使当前的项目搞不成,也可能未来在别的地方做出一番事情;



  2. 初了上边这个,最核心的就是,怎么样赚钱,现在这种融资环境,如果自己不能赚钱,大概率是活不下去的@自己;

  3. 抓住核心矛盾,解决主要问题,业务永远是最重要的。至于说选择的开发技术、代码规范等等这些都可以往后放;

  4. 对上要及时反馈自己的工作进度,保持好沟通,老板总是站在更高一层考虑问题,肯定会有一些不一样的想法,别总自以为什么什么的;

  5. 每段经历最好都能有所收获,人生的每一步都有意义。




作者:qiuwww
来源:juejin.cn/post/7257085326471512119
收起阅读 »

关于鸿蒙开发,我暂时放弃了

起因 在最近鸿蒙各种新闻资讯说要鸿蒙不再兼容android之后,我看完了鸿蒙视频,并简单的撸了一个demo。 # 鸿蒙HarmonyOS从零实现类微信app效果第一篇,基础界面搭建 # 鸿蒙HarmonyOS从零实现类微信app效果第二篇,我的+发现页面实...
继续阅读 »

image.png


image.png


起因


在最近鸿蒙各种新闻资讯说要鸿蒙不再兼容android之后,我看完了鸿蒙视频,并简单的撸了一个demo。


企业微信截图_6f8acb94-bd68-4f56-9460-4a59d2370a4a.png



鸿蒙的arkui,使用typescript作为基调,然后响应式开发,对于我这个old android来说,确实挺惊艳的。而且在模拟器中运行起来也很快,写demo的过程鸡血满满,着实很愉快。


后面自己写的文章,也在掘金站点上获得了不错的评价。


企业微信截图_fa34f233-af43-4567-8dac-57ef5666f1bd.png


image.png


打击


今天下午,刚好同事有一个遥遥领先(meta 40 pro),鸿蒙4.0版本


怀着秀操作的想法,在同事手机上运行了起来。very nice。 一切出奇的顺利。


but ...


尼玛,点击的时候,直接卡住不对,黑屏。让人瞬间崩溃。


本着优先怀疑自己的原则,我找了一个官方的demo。 运行起来。


额...


尼玛。还是点击之后卡住了,大概30s之后,才跳转到新的页面。


image.png


这一切,让我熬夜掉的头发瞬间崩溃。


放弃了...


放弃了...


后续


和其他学习鸿蒙的伙伴沟通,也遇到了同样的问题,真机不能运行,会卡线程。但是按下home键,再次回到界面,页面会刷新过来


我个人暂时决定搁置对于鸿蒙开发的学习了,后续如果慢慢变得比较成熟之后,再次接触学习吧。



后续个人计划:




  • 1、还是会持续关注后续版本是否真机能运行,传言api 10对黑屏和真机无法运行的修复了。奈何官方所有渠道的编译器都没有api 10 的模拟器,真机4.0按道理是支持api10,但是还是黑屏,再持续观察吧。插个眼

  • 2、为了贯彻执行持续学习。后续可能会持续更新jetpack compose相关内容,包含且不局限于 compose desktop以及multi platform


最新情报:有网友告知我,在meta60上是运行没问题的,可能是最新版4.0是ok的,那么结论就是目前真机适配不完善


作者:王先生技术栈
来源:juejin.cn/post/7304538094736343052
收起阅读 »

全年免费!环信发布出海创新版,助力泛娱乐创业者扬帆起航

目前,以陌生人社交、直播、语聊、电商等热门场景为代表的社交泛娱乐出海正发展得如火如荼,成为企业新的增长曲线。但随着出海企业增多,海外市场争夺、资源竞争与技术博弈也愈加激烈。为了让更多创业者与创新者获得支持,快速高效地打通出海通道。社交出海的解决方案专家环信于近...
继续阅读 »

目前,以陌生人社交、直播、语聊、电商等热门场景为代表的社交泛娱乐出海正发展得如火如荼,成为企业新的增长曲线。但随着出海企业增多,海外市场争夺、资源竞争与技术博弈也愈加激烈。

为了让更多创业者与创新者获得支持,快速高效地打通出海通道。社交出海的解决方案专家环信于近期正式推出“出海创新版”,全年免费为创业者和开发者提供必要的工具和资源,打造下一个出海独角兽。


环信IM出海创新版,海外能力与热门场景应有尽有

环信IM出海创新版汇聚了丰富齐全的海外能力,并覆盖大多数热门应用场景,助力社交泛娱乐出海开发者和创业者,成为真正吃到螃蟹、拿到红利的人。

目前,热门场景应用覆盖了1V1视频、AI社交、陌生人社交、视频直播、语聊房。海外能力上,则能提供国际版UIKIT、多语言翻译、多语言内容审核、表情贴纸、表情回应、超级社区等,助力开发者快速构建个性化娱乐社交场景。

丰富齐全的海外能力



热门场景应用

超级优惠力度来袭:全年免费+超值生态资源

为了全力支持创业者与开发者更好的开启出海征程,此次环信 IM 出海创新版推出了专属套餐版本,只要参与的企业就能以0元/月,全年免费的价格获取包含IM所有核心功能和服务,详情如下:

立即免费申请:https://www.easemob.com/event/special2024

除此之外,环信还借助自身生态资源和技术优势,为出海企业提供包含实时音视频、内容审核、消息推送、美颜、数字人、超级社区在内的超值生态伙伴助力场景功能大礼包。


在生态资源上,出海企业可以通过环信,与资深出海社交泛娱乐创业者沟通交流、与产品技术大牛创新协作、不定期参加全球创新方向洞见分享、高价值场景交流等。

不仅如此,出海企业还可以加入由声网发起的“超音速计划”,与优秀的创业者们进行深度交流与竞技,还有机会获得投资与孵化、产品和技术支持、市场与品牌展示等机会等。用生态的力量为自己的创业项目赋能,共同探索如何让出海之路走得更顺、走得更远。

行业明星应用背书 环信IM质量服务有保障

目前,星野、Hungry Panda、Autel Maxifix、locklok等社交泛娱乐出海的明星应用都在使用环信IM的服务,并依托环信IM的高并发稳定服务支撑用户的快速增长。


以定位为沉浸式AI内容社区的星野为例,环信IM通过提供高效、稳定、安全的即时通讯服务,帮助星野提高了用户粘性和转化率,推动人与AI智能互动体验共同发展。

对于任何一个社交泛娱乐出海的企业而言,海外资源能力的配置都显得至关重要。环信IM作为一个成熟的即时通讯产品,无论是从技术、资源还是合规上,都有充分的能力为“出海”提供一套完整的技术支持和服务框架。

现在就扬帆起航,加入“环信IM出海创新版”吧!

参考文档:

收起阅读 »

我写了一个程序,让端口占用无路可逃

作为一个 Java 工程师,经常会遇到这么个场景:IDEA 里的程序正在运行,此时直接关闭了 IDEA 而没有先关闭正在运行的服务。 在绝大多数情境下,此方式都无伤大雅,但总有一些抽风的场景运行的程序并没有被正常的关闭,也就导致了重启项目时将会提示 xxxx ...
继续阅读 »

作为一个 Java 工程师,经常会遇到这么个场景:IDEA 里的程序正在运行,此时直接关闭了 IDEA 而没有先关闭正在运行的服务。


在绝大多数情境下,此方式都无伤大雅,但总有一些抽风的场景运行的程序并没有被正常的关闭,也就导致了重启项目时将会提示 xxxx 端口已被占用。


Windows 下此方式解决也十分简单,在命令行输入下述两个命令即可根据端口关闭对应的进程。


# 端口占用进程
netstat -ano | findstr <port>

# 进程关闭
taskkill -PID <pid> -F

虽然说也不麻烦但却很繁杂,试想一下当遇到这种情况下,我需要先翻笔记找出这两个命令,在打开命令行窗口执行,一套连招下来相当影响编程情绪。


因此,我决定写一个程序能够便捷的实现这个操作,最好是带 GUI 页面。


说干就干,整个程序功能其实并不复杂,对于页面的展示要求也不高,我就确定下来了直接通过 Java Swing 实现 GUI 部分。而对于命令执行部分,在 Java 中提供了 Process 类可用于执行命令。


先让我们看下 Process 的作用方式,以最简单的 ping baidu.com 测试为例。


public void demo() {  
ProcessBuilder processBuilder = new ProcessBuilder();
List<String> command = new ArrayList<>();
command.add("ping");
command.add("www.baidu.com");
processBuilder.command(command);

try {
Process process = processBuilder.start();
try (
InputStreamReader ir = new InputStreamReader(process.getInputStream(), "GBK");
BufferedReader br = new BufferedReader(ir)
) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}

运行上述的代码,在控制台可以得到下图结果:


image.png


在上述程序中,ProcessBuilder 用于构建命令,processBuilder.start() 则相当于你敲下回车执行,而执行的结果的则以 IO 流的形式返回,这里通过 readLine() 将返回的结果逐行的形式进行读取。


了解的大概原理之后,剩下的事情就简单了,只需要将之前提到的两个命令以同样的方式通过 Process 执行就可以,再通过 Java Swing 进行一个页面展示就可以。


具体的实现并不复杂,这里就不详细展开介绍,完整的项目代码已经上传到 GitHub,感兴趣的小伙伴可自行前往查看,仓库地址:windows-process


下面主要介绍程序的使用与效果,开始前可以去上述提到的仓库 relase 里将打包完成的 exe 程序下载,下载地址


下载后启动 window process.exe 程序,在启动之后会先弹出下图的提示,这是因为使用了 exe4j 打包程序,选择确认即可。


image.png


选择确认之后即会展示下图页面,列表中展示的数据即 netstat -ano 命令返回的结果,


image.png


在选中列表任意一条进程记录后,会将该进程对应的端口号和 PID 填充至上面的输入框中。


20240630_104641.gif


同时,可在 Port 输入框中输入对应的端口号实现快速查询,若需要停止某个进程,则将点击对应端口进程记录其 PID 会自动填入输入框中,然后单击 Kill 按钮,成功停止进程后将会进行相应的提示。


最后的最后,再臭不要脸的给自己要个赞,觉得不错的可以去 GitHub 仓库上下载下来看看,如果能点个 star 更是万分感谢,这里再贴一下仓库地址:windows-process


image.png


作者:烽火戏诸诸诸侯
来源:juejin.cn/post/7385499574881026089
收起阅读 »

null 不好,我真的推荐你使用 Optional

"Null 很糟糕." - Doug Lea。 Doug Lea 是一位美国的计算机科学家,他是 Java 平台的并发和集合框架的主要设计者之一。他在 2014 年的一篇文章中说过:“Null sucks.”1,意思是 null 很糟糕。他认为 null 是...
继续阅读 »

"Null 很糟糕." - Doug Lea。



Doug Lea 是一位美国的计算机科学家,他是 Java 平台的并发和集合框架的主要设计者之一。他在 2014 年的一篇文章中说过:“Null sucks.”1,意思是 null 很糟糕。他认为 null 是一种不明确的表示,它既可以表示一个值不存在,也可以表示一个值未知,也可以表示一个值无效。这样就会导致很多逻辑错误和空指针异常,给程序员带来很多麻烦。他建议使用 Optional 类来封装可能为空的值,从而提高代码的可读性和健壮性。



"发明 null 引用是我的十亿美元错误。" - Sir C. A. R. Hoare。



Sir C. A. R. Hoare 是一位英国的计算机科学家,他是快速排序算法、Hoare 逻辑和通信顺序进程等重要概念的发明者。他在 2009 年的一个软件会议上道歉说:“I call it my billion-dollar mistake. It was the invention of the null reference in 1965.”,意思是他把 null 引用称为他的十亿美元错误。他说他在 1965 年设计 ALGOL W 语言时,引入了 null 引用的概念,用来表示一个对象变量没有指向任何对象。他当时认为这是一个很简单和自然的想法,但后来发现这是一个非常糟糕的设计,因为它导致了无数的错误、漏洞和系统崩溃。他说他应该使用一个特殊的对象来表示空值,而不是使用 null。


自作者从事 Java 编程一来,就与 null 引用相伴,与 NullPointerException 相遇已经是家常便饭了。


null 引用是一种表示一个对象变量没有指向任何对象的方式,它是 Java 语言中的一个特殊值,也是导致空指针异常(NullPointerException)的主要原因。虽然 null 引用可以用来表示一个值不存在或未知,也可以用来节省内存空间。但是它也不符合面向对象的思想,因为它不是一个对象,不能调用任何方法或属性。


可以看到,null 引用并不好,我们应该尽量避免使用 null,那么我们该怎么避免 null 引用引起的逻辑错误和运行时异常嘞?


其实这个问题 Java 的设计者也知道,于是他们在 Java8 之后设计引入了 Optional 类解决这个问题,本文将给大家详细介绍下 Optional 类的设计目的以及使用方法。



Optional 类是什么?


Optional 类是 java 8 中引入的一个新的类,它的作用是封装一个可能为空的值,从而避免空指针异常(NullPointerException)。Optional 类可以看作是一个容器,它可以包含一个非空的值,也可以为空。Optional 类提供了一些方法,让我们可以更方便地处理可能为空的值,而不需要显式地进行空值检查或者使用 null。



推荐作者开源的 H5 商城项目waynboot-mall,这是一套全部开源的微商城项目,包含三个项目:运营后台、H5 商城前台和服务端接口。实现了商城所需的首页展示、商品分类、商品详情、商品 sku、分词搜索、购物车、结算下单、支付宝/微信支付、收单评论以及完善的后台管理等一系列功能。 技术上基于最新得 Springboot3.0、jdk17,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中间件。分模块设计、简洁易维护,欢迎大家点个 star、关注我。


github 地址:github.com/wayn111/way…



Optional 类的设计


Optional 类的设计是基于函数式编程的思想,它借鉴了 Scala 和 Haskell 等语言中的 Option 类型。Optional 类实现了 java.util.function 包中的 Supplier、Consumer、Predicate、Function 等接口,这使得它可以和 lambda 表达式或者方法引用一起使用,形成更简洁和优雅的代码。


Optional 类被 final 修饰,因此它是一个不可变的类,它有两个静态方法用于创建 Optional 对象。


Optional.empty()


Optional.empty 表示一个空的 Optional 对象,它不包含任何值。


// 创建一个空的 Optional 对象
Optional empty = Optional.empty();

Optional.of(T value)


Optional.of 表示一个非空的 Optional 对象,它包含一个非空的值。


// 创建一个非空的 Optional 对象
Optional hello = Optional.of("Hello");

Optional.ofNullable(T value)


注意,如果我们使用 Optional.of 方法传入一个 null 值,会抛出 NullPointerException。如果我们不确定一个值是否为空,可以使用 Optional.ofNullable 方法,它会根据值是否为空,返回一个相应的 Optional 对象。例如:


// 创建一个可能为空的 Optional 对象
Optional name = Optional.ofNullable("Hello");

Optional 对象的使用方法


Optional 对象提供了一些方法,让我们可以更方便地处理可能为空的值,而不需要显式地进行空值检查或者使用 null。以下是一些常用的方法。


isPresent()


判断 Optional 对象是否包含一个非空的值,返回一个布尔值。


get()


如果 Optional 对象包含一个非空的值,返回该值,否则抛出 NoSuchElementException 异常。



// 使用 isPresent 和 get 方法
Optional name = Optional.ofNullable("tom");
if (name.isPresent()) {
System.out.println("Hello, " + name.get());
} else {
System.out.println("Name is not available");
}
// 输出:Hello tom

ifPresent(Consumer action)


如果 Optional 对象包含一个非空的值,执行给定的消费者操作,否则什么也不做。


// 使用 ifPresent(Consumer action)
Optional name = Optional.ofNullable("tom");
name.ifPresent(s -> {
System.out.println("Hello, " + name.get());
});
// 输出:Hello tom

orElse(T other)


如果 Optional 对象包含一个非空的值,返回该值,否则返回给定的默认值。


// 使用 orElse(T other)
Optional name = Optional.ofNullable(null);
String greeting = "Hello, " + name.orElse("Guest");
System.out.println(greeting);
// 输出:Hello Guest

orElseGet(Supplier supplier)


如果 Optional 对象包含一个非空的值,返回该值,否则返回由给定的供应者操作生成的值。


// 使用 orElseGet(Supplier supplier)
Optional name = Optional.ofNullable(null);
String greeting = "Hello, " + name.orElseGet(() -> "Guset");
System.out.println(greeting);
// 输出:Hello Guset

orElseThrow(Supplier exceptionSupplier)


如果 Optional 对象包含一个非空的值,返回该值,否则抛出由给定的异常供应者操作生成的异常。


// 使用 orElseThrow(Supplier exceptionSupplier)
Optional name = Optional.ofNullable(null);
String greeting = "Hello, " + name.orElseThrow(() -> new NullPointerException("null"));
// 抛出 java.lang.NullPointerException: null 异常

map(Function mapper)


如果 Optional 对象包含一个非空的值,对该值应用给定的映射函数,返回一个包含映射结果的 Optional 对象,否则返回一个空的 Optional 对象。


// 使用 map(Function mapper)
Optional name = Optional.ofNullable("tom");
String greeting = "Hello, " + name.map(s -> s.toUpperCase()).get();
System.out.println(greeting);
// 输出:Hello TOM

flatMap(Function> mapper)


如果 Optional 对象包含一个非空的值,对该值进行 mapper 参数操作,返回新的 Optional 对象,否则返回一个空的 Optional 对象。


// 使用 flatMap(Function> mapper)
Optional name = Optional.ofNullable("tom");
String greeting = name.flatMap(s -> Optional.of("Hello " + s)).get();
System.out.println(greeting);
// 输出:Hello tom

filter(Predicate predicate)


如果 Optional 对象包含一个非空的值,并且该值满足给定的谓词条件,返回包含该值的 Optional 对象,否则返回一个空的 Optional 对象。


// filter(Predicate predicate)
Optional name = Optional.ofNullable("tom");
String greeting = "Hello " + name.filter(s -> !s.isEmpty()).get();
System.out.println(greeting);
// 输出:Hello tom

Java 9 中 Optional 改进


Java 9 中 Optional 类有了一些改进,主要是增加了三个新的方法,分别是 stream()、ifPresentOrElse() 和 or()。这些方法可以让我们更方便地处理可能为空的值,以及和流或其他返回 Optional 的方法结合使用。我来详细讲解一下这些方法的作用和用法。


stream()


这个方法可以将一个 Optional 对象转换为一个 Stream 对象,如果 Optional 对象包含一个非空的值,那么返回的 Stream 对象就包含这个值,否则返回一个空的 Stream 对象。这样我们就可以利用 Stream 的各种操作来处理 Optional 的值,而不需要显式地判断是否为空。我们可以用 stream() 方法来过滤一个包含 Optional 的列表,只保留非空的值,如下所示:


List> list = Arrays.asList(
Optional.empty(),
Optional.of("A"),
Optional.empty(),
Optional.of("B")
);

// 使用 stream() 方法过滤列表,只保留非空的值
List filteredList = list.stream()
.flatMap(Optional::stream)
.collect(Collectors.toList());

System.out.println(filteredList);
// 输出 [A, B]

ifPresentOrElse(Consumer action, Runnable emptyAction)


这个方法可以让我们在 Optional 对象包含值或者为空时,执行不同的操作。它接受两个参数,一个是 Consumer 类型的 action,一个是 Runnable 类型的 emptyAction。如果 Optional 对象包含一个非空的值,那么就执行 action.accept(value),如果 Optional 对象为空,那么就执行 emptyAction.run()。这样我们就可以避免使用 if-else 语句来判断 Optional 是否为空,而是使用函数式编程的方式来处理不同的情况。我们可以用 ifPresentOrElse() 方法来打印 Optional 的值,或者提示不可用,如下所示 :


Optional optional = Optional.of(1);
optional.ifPresentOrElse(
x -> System.out.println("Value: " + x),
() -> System.out.println("Not Present.")
);

optional = Optional.empty();
optional.ifPresentOrElse(
x -> System.out.println("Value: " + x),
() -> System.out.println("Not Present.")
);

// 输出:Value: 1
// 输出:Not Present.

or(Supplier> supplier)


这个方法可以让我们在 Optional 对象为空时,返回一个预设的值。它接受一个 Supplier 类型的 supplier,如果 Optional 对象包含一个非空的值,那么就返回这个 Optional 对象本身,如果 Optional 对象为空,那么就返回 supplier.get() 返回的 Optional 对象。这样我们就可以避免使用三元运算符或者其他方式来设置默认值,而是使用函数式编程的方式来提供备选值。我们可以用 or() 方法来设置 Optional 的默认值,如下所示:


Optional optional = Optional.of("Hello ");
Supplier> supplier = () -> Optional.of("tom");
optional = optional.or(supplier);
optional.ifPresent(x -> System.out.println(x));

optional = Optional.empty();
optional = optional.or(supplier);
optional.ifPresent(x -> System.out.println(x));

// 输出:Hello
// 输出:tom

为什么我推荐你使用 Optional 类


最后我总结一下使用 Optional 类的几个好处:



  1. 可以避免空指针异常,提高代码的健壮性和可读性。

  2. 可以减少显式的空值检查和 null 的使用,使代码更简洁和优雅。

  3. 可以利用函数式编程的特性,实现更灵活和高效的逻辑处理。

  4. 可以提高代码的可测试性,方便进行单元测试和集成测试。


总之,Optional 类是一个非常有用的类,它可以帮助我们更好地处理可能为空的值,提高代码的质量和效率。所以我强烈推荐你在 Java 开发中使用 Optional 类,你会发现它的魅力和好处。


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

搭建个人直播间,实现24小时B站、斗鱼、虎牙等无人直播!

不知道大家平时看不看直播呢?现在有各式各样的直播,游戏直播、户外直播、带货直播、经典电视/电影直播等等。 电视、电影直播是24小时不间断无人直播,如斗鱼/虎牙中的一起看,这种直播要如何实现呢? 其实非常简单,只需要一台服务器和视频资源就能完成。 再借助于直播...
继续阅读 »

不知道大家平时看不看直播呢?现在有各式各样的直播,游戏直播、户外直播、带货直播、经典电视/电影直播等等。


电视、电影直播是24小时不间断无人直播,如斗鱼/虎牙中的一起看,这种直播要如何实现呢?


其实非常简单,只需要一台服务器和视频资源就能完成。


再借助于直播推流工具,如 KPlayer,将电视剧、电影等媒体资源推流到直播间,就能实现24小时无人直播了!



关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。



KPlayer 简介


KPlayer —— ByteLang Studio 设计开发的一款用于在 Linux 环境下进行媒体资源推流的应用程序。


只需要简单的修改配置文件即可达到开箱即用的目的,不需要了解众多推流适配、视频编解码的细节即可方便的将媒体资源在主流直播平台上进行直播。意愿是提供一个简单易上手、扩展丰富、性能优秀适合长时间不间断推流的直播推流场景。


功能特色:



  • 本地/网络视频资源的无缝推流,切换资源不导致断流

  • 可自定义配置的编码参数,例如分辨率、帧率等

  • 自定义多输出源,适合相同内容一次编码多路推流节省硬件资源

  • 提供缓存机制避免相同内容二次编解码,大大降低在循环场景下对硬件资源的消耗

  • 丰富的API接口在运行时对播放行为和资源动态控制

  • 提供基础插件并具备自定义插件开发的能力


项目地址:https://github.com/bytelang/kplayer-go
在线文档:https:/
/docs.kplayer.net/v0.5.8/

安装 KPlayer


KPlayer 支持一键安装、手动安装和 Docker 安装。


一键安装


通过 ssh 进入到你的服务器中,找到合适的目录并运行以下的命令进行下载:


curl -fsSL get.kplayer.net | bash

手动安装(可选)


1、下载压缩包


wget http://download.bytelang.cn/kplayer-v0.5.8-linux_amd64.tar.gz

2、解压压缩包


tar zxvf kplayer-v0.5.8-linux_amd64.tar.gz

安装完成


1、执行 cd kplayer 进入到 kplayer 目录,使用 ll 查看文件列表:


-rw-r--r-- 1 root root 285 3  23 18:23 config.json.example
-rwxr-xr-x 1 root root 27M 7 29 11:12 kplayer


  • config.json.exampleKPlayer 最小化的配置信息示例

  • kplayerKPlayer 服务启动、停止的执行脚本命令


2、使用 ./kplayer 命令查看当前版本


创建配置文件


1、使用 cp 命令重命名并复制一份 config.json.example


cp config.json.example config.json

2、修改配置文件


{
"version": "2.0.0",
"resource": {
"lists": [
"/video/example_1.mp4",
"/video/example_2.mp4"
]
},
"output": {
"lists": [
{
"path": "rtmp://127.0.0.1:1935/push"
}
]
}
}


  • resource.lists 视频资源文件路径

  • output.lists 直播推流地址,在B站、斗鱼、虎牙等直播平台中开启直播后,将会得到推流地址与推流码


开启直播


上传视频


上传视频资源到服务器,并修改 KPlayer 中的 resource.lists 视频路径



❗❗❗注意:直播的媒体文件必须得有平台版权,否则就会被投诉,封禁直播间❗




{
"version": "2.0.0",
"resource": {
"lists": [
"/data/software/movie/WechatMomentScreenshot.mp4",
"/data/software/movie/IT Tools.mp4",
"/data/software/movie/EasyCode.mp4",
"/data/software/movie/TinyRDM.mp4",
"/data/software/movie/Fooocus.mp4",
"/data/software/movie/Stirling-PDF.mp4"
]
},
"output": {
"lists": [
{
"path": "rtmp://127.0.0.1:1935/push"
}
]
}
}
}

获取推流地址



以开启B站直播为例。



1、点击首页直播


2、点击网页右侧的开播设置


3、选择分类,点击开播



前提需要身-份-证和姓名实名认证




4、复制直播间地址


rtmp://live-push.bilivideo.com/live-bvc/?streamname=live_*********_********&key=**************&schedule=rtmp&pflag=1

5、将直播间地址配置到 KPlayer 配置文件中的 output.lists 直播推流地址


{
"version": "2.0.0",
"resource": {
"lists": [
"/data/software/movie/WechatMomentScreenshot.mp4",
"/data/software/movie/IT Tools.mp4",
"/data/software/movie/EasyCode.mp4",
"/data/software/movie/TinyRDM.mp4",
"/data/software/movie/Fooocus.mp4",
"/data/software/movie/Stirling-PDF.mp4"
]
},
"output": {
"lists": [
{
"path": "rtmp://live-push.bilivideo.com/live-bvc/?streamname=live_*********_********&key=**************&schedule=rtmp&pflag=1"
}
]
}
}

运行 KPlayer


执行以下命令启动 KPlayer


./kplayer play start


后台运行 KPlayer


./kplayer play start --daemon

测试访问


打开直播间地址,可以看到已经开始直播了。



斗鱼、虎牙等其他直播平台的直播配置也是类似的流程,只需要获取到平台的直播推流地址,并进行配置即可!可以同时配置多个平台同时进行直播!



配置循环播放


KPlayer 提供了很多的配置项,有资源配置、播放配置等。


如:可以配置循环播放视频,这样就可以保证24小时不间断的循环播放视频。


{
"version": "2.0.0",
"resource": {
"lists": [
"/data/software/movie/WechatMomentScreenshot.mp4",
"/data/software/movie/IT Tools.mp4",
"/data/software/movie/EasyCode.mp4",
"/data/software/movie/TinyRDM.mp4",
"/data/software/movie/Fooocus.mp4",
"/data/software/movie/Stirling-PDF.mp4"
]
},
"output": {
"lists": [
{
"path": "rtmp://live-push.bilivideo.com/live-bvc/?streamname=live_*********_********&key=**************&schedule=rtmp&pflag=1"
}
]
},
## 播放配置
"play": {
"fill_strategy": "ratio",
## 启用推流编码缓存,会生成缓存,命中缓存节约CPU资源
"skip_invalid_resource": true,
"cache_on": true,
# 播放模式为按顺序且循环播放
"play_model": "loop"
}
}


更多的配置信息可参考 KPlayer 提供的文档。



Docker 安装 KPlayer


1、创建缓存目录 /data/software/docker/kplayer/cache


cd /data/software/docker/kplayer
mkdir cache

2、创建配置文件 /data/software/docker/kplayer/config.json


cd /data/software/docker/kplayer
touch config.json

填入配置信息:


{
"version": "2.0.0",
"resource": {
"lists": [
"/data/software/movie/WechatMomentScreenshot.mp4",
"/data/software/movie/IT Tools.mp4",
"/data/software/movie/EasyCode.mp4",
"/data/software/movie/TinyRDM.mp4",
"/data/software/movie/Fooocus.mp4",
"/data/software/movie/Stirling-PDF.mp4"
]
},
"output": {
"lists": [
{
"path": "rtmp://live-push.bilivideo.com/live-bvc/?streamname=live_*********_********&key=**************&schedule=rtmp&pflag=1"
}
]
},
## 播放配置
"play": {
"fill_strategy": "ratio",
## 启用推流编码缓存,会生成缓存,命中缓存节约CPU资源
"skip_invalid_resource": true,
"cache_on": true,
# 播放模式为按顺序且循环播放
"play_model": "loop"
}
}

2、创建 docker-compose.yml


version: "3.3"
services:
kplayer:
container_name: kplayer
volumes:
- "/data/software/movie:/video"
- "/data/software/docker/kplayer/config.json:/kplayer/config.json"
- "/data/software/docker/kplayer/cache:/kplayer/cache"
restart: always
image: "bytelang/kplayer"

3、启动容器


docker-compose up -d 

以上,就是利用服务器搭建个人直播间的全流程,整个步骤不是很复杂。


我们可以利用闲置的服务器,将自己收藏的电影、电视等资源进行全天候直播,每天还能获得一定的收益!



❗❗❗注意:直播的媒体文件必须得有平台版权,否则就会被投诉,封禁直播间❗



最后


推荐的开源项目已经收录到 GitHub 项目,欢迎 Star


https://github.com/chenyl8848/great-open-source-project

或者访问网站,进行在线浏览:


https://chencoding.top:8090/#/


作者:Java陈序员
来源:juejin.cn/post/7385929329640226828
收起阅读 »

使用双异步后,从 191s 优化到 2s

大家好,我是哪吒。 在开发中,我们经常会遇到这样的需求,将Excel的数据导入数据库中。 一、一般我会这样做: 通过POI读取需要导入的Excel; 以文件名为表名、列头为列名、并将数据拼接成sql; 通过JDBC或mybatis插入数据库; 操作起来,...
继续阅读 »

大家好,我是哪吒。


在开发中,我们经常会遇到这样的需求,将Excel的数据导入数据库中。


一、一般我会这样做:



  1. 通过POI读取需要导入的Excel;

  2. 以文件名为表名、列头为列名、并将数据拼接成sql;

  3. 通过JDBC或mybatis插入数据库;



操作起来,如果文件比较多,数据量都很大的时候,会非常慢。


访问之后,感觉没什么反应,实际上已经在读取 + 入库了,只是比较慢而已。


读取一个10万行的Excel,居然用了191s,我还以为它卡死了呢!


private void readXls(String filePath, String filename) throws Exception {
@SuppressWarnings("resource")
XSSFWorkbook xssfWorkbook = new XSSFWorkbook(new FileInputStream(filePath));
// 读取第一个工作表
XSSFSheet sheet = xssfWorkbook.getSheetAt(0);
// 总行数
int maxRow = sheet.getLastRowNum();

StringBuilder insertBuilder = new StringBuilder();

insertBuilder.append("insert int0 ").append(filename).append(" ( UUID,");

XSSFRow row = sheet.getRow(0);
for (int i = 0; i < row.getPhysicalNumberOfCells(); i++) {
insertBuilder.append(row.getCell(i)).append(",");
}

insertBuilder.deleteCharAt(insertBuilder.length() - 1);
insertBuilder.append(" ) values ( ");

StringBuilder stringBuilder = new StringBuilder();
for (int i = 1; i <= maxRow; i++) {
XSSFRow xssfRow = sheet.getRow(i);
String id = "";
String name = "";
for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) {
if (j == 0) {
id = xssfRow.getCell(j) + "";
} else if (j == 1) {
name = xssfRow.getCell(j) + "";
}
}

boolean flag = isExisted(id, name);
if (!flag) {
stringBuilder.append(insertBuilder);
stringBuilder.append('\'').append(uuid()).append('\'').append(",");
for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) {
stringBuilder.append('\'').append(value).append('\'').append(",");
}
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
stringBuilder.append(" )").append("\n");
}
}

List collect = Arrays.stream(stringBuilder.toString().split("\n")).collect(Collectors.toList());
int sum = JdbcUtil.executeDML(collect);
}

private static boolean isExisted(String id, String name) {
String sql = "select count(1) as num from " + static_TABLE + " where ID = '" + id + "' and NAME = '" + name + "'";
String num = JdbcUtil.executeSelect(sql, "num");
return Integer.valueOf(num) > 0;
}

private static String uuid() {
return UUID.randomUUID().toString().replace("-", "");
}

二、谁写的?拖出去,斩了!


优化1:先查询全部数据,缓存到map中,插入前再进行判断,速度快了很多。


优化2:如果单个Excel文件过大,可以采用 异步 + 多线程 读取若干行,分批入库。



优化3:如果文件数量过多,可以采一个Excel一个异步,形成完美的双异步读取插入。



使用双异步后,从 191s 优化到 2s,你敢信?


下面贴出异步读取Excel文件、并分批读取大Excel文件的关键代码。


1、readExcelCacheAsync控制类


@RequestMapping(value = "/readExcelCacheAsync", method = RequestMethod.POST)
@ResponseBody
public String readExcelCacheAsync() {
String path = "G:\\测试\\data\\";
try {
// 在读取Excel之前,缓存所有数据
USER_INFO_SET = getUserInfo();

File file = new File(path);
String[] xlsxArr = file.list();
for (int i = 0; i < xlsxArr.length; i++) {
File fileTemp = new File(path + "\\" + xlsxArr[i]);
String filename = fileTemp.getName().replace(".xlsx", "");
readExcelCacheAsyncService.readXls(path + filename + ".xlsx", filename);
}
} catch (Exception e) {
logger.error("|#ReadDBCsv|#异常: ", e);
return "error";
}
return "success";
}

2、分批读取超大Excel文件


@Async("async-executor")
public void readXls(String filePath, String filename) throws Exception {
@SuppressWarnings("resource")
XSSFWorkbook xssfWorkbook = new XSSFWorkbook(new FileInputStream(filePath));
// 读取第一个工作表
XSSFSheet sheet = xssfWorkbook.getSheetAt(0);
// 总行数
int maxRow = sheet.getLastRowNum();
logger.info(filename + ".xlsx,一共" + maxRow + "行数据!");
StringBuilder insertBuilder = new StringBuilder();

insertBuilder.append("insert int0 ").append(filename).append(" ( UUID,");

XSSFRow row = sheet.getRow(0);
for (int i = 0; i < row.getPhysicalNumberOfCells(); i++) {
insertBuilder.append(row.getCell(i)).append(",");
}

insertBuilder.deleteCharAt(insertBuilder.length() - 1);
insertBuilder.append(" ) values ( ");

int times = maxRow / STEP + 1;
//logger.info("将" + maxRow + "行数据分" + times + "次插入数据库!");
for (int time = 0; time < times; time++) {
int start = STEP * time + 1;
int end = STEP * time + STEP;

if (time == times - 1) {
end = maxRow;
}

if(end + 1 - start > 0){
//logger.info("第" + (time + 1) + "次插入数据库!" + "准备插入" + (end + 1 - start) + "条数据!");
//readExcelDataAsyncService.readXlsCacheAsync(sheet, row, start, end, insertBuilder);
readExcelDataAsyncService.readXlsCacheAsyncMybatis(sheet, row, start, end, insertBuilder);
}
}
}

3、异步批量入库


@Async("async-executor")
public void readXlsCacheAsync(XSSFSheet sheet, XSSFRow row, int start, int end, StringBuilder insertBuilder) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = start; i <= end; i++) {
XSSFRow xssfRow = sheet.getRow(i);
String id = "";
String name = "";
for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) {
if (j == 0) {
id = xssfRow.getCell(j) + "";
} else if (j == 1) {
name = xssfRow.getCell(j) + "";
}
}

// 先在读取Excel之前,缓存所有数据,再做判断
boolean flag = isExisted(id, name);
if (!flag) {
stringBuilder.append(insertBuilder);
stringBuilder.append('\'').append(uuid()).append('\'').append(",");
for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) {
stringBuilder.append('\'').append(value).append('\'').append(",");
}
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
stringBuilder.append(" )").append("\n");
}
}

List collect = Arrays.stream(stringBuilder.toString().split("\n")).collect(Collectors.toList());
if (collect != null && collect.size() > 0) {
int sum = JdbcUtil.executeDML(collect);
}
}

private boolean isExisted(String id, String name) {
return ReadExcelCacheAsyncController.USER_INFO_SET.contains(id + "," + name);
}

4、异步线程池工具类


@Async的作用就是异步处理任务。



  1. 在方法上添加@Async,表示此方法是异步方法;

  2. 在类上添加@Async,表示类中的所有方法都是异步方法;

  3. 使用此注解的类,必须是Spring管理的类;

  4. 需要在启动类或配置类中加入@EnableAsync注解,@Async才会生效;


在使用@Async时,如果不指定线程池的名称,也就是不自定义线程池,@Async是有默认线程池的,使用的是Spring默认的线程池SimpleAsyncTaskExecutor。


默认线程池的默认配置如下:



  1. 默认核心线程数:8;

  2. 最大线程数:Integet.MAX_VALUE;

  3. 队列使用LinkedBlockingQueue;

  4. 容量是:Integet.MAX_VALUE;

  5. 空闲线程保留时间:60s;

  6. 线程池拒绝策略:AbortPolicy;


从最大线程数可以看出,在并发情况下,会无限制的创建线程,我勒个吗啊。


也可以通过yml重新配置:


spring:
task:
execution:
pool:
max-size: 10
core-size: 5
keep-alive: 3s
queue-capacity: 1000
thread-name-prefix: my-executor

也可以自定义线程池,下面通过简单的代码来实现以下@Async自定义线程池。


@EnableAsync// 支持异步操作
@Configuration
public class AsyncTaskConfig {

/**
* com.google.guava中的线程池
*
@return
*/

@Bean("my-executor")
public Executor firstExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("my-executor").build();
// 获取CPU的处理器数量
int curSystemThreads = Runtime.getRuntime().availableProcessors() * 2;
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(curSystemThreads, 100,
200, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(), threadFactory);
threadPool.allowsCoreThreadTimeOut();
return threadPool;
}

/**
* Spring线程池
*
@return
*/

@Bean("async-executor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
// 核心线程数
taskExecutor.setCorePoolSize(24);
// 线程池维护线程的最大数量,只有在缓冲队列满了之后才会申请超过核心线程数的线程
taskExecutor.setMaxPoolSize(200);
// 缓存队列
taskExecutor.setQueueCapacity(50);
// 空闲时间,当超过了核心线程数之外的线程在空闲时间到达之后会被销毁
taskExecutor.setKeepAliveSeconds(200);
// 异步方法内部线程名称
taskExecutor.setThreadNamePrefix("");

/**
* 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略
* 通常有以下四种策略:
* ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
* ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
* ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
* ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用 execute() 方法,直到成功
*/

taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
}


5、异步失效的原因



  1. 注解@Async的方法不是public方法;

  2. 注解@Async的返回值只能为void或Future;

  3. 注解@Async方法使用static修饰也会失效;

  4. 没加@EnableAsync注解;

  5. 调用方和@Async不能在一个类中;

  6. 在Async方法上标注@Transactional是没用的,但在Async方法调用的方法上标注@Transcational是有效的;


三、线程池中的核心线程数设置问题


有一个问题,一直没时间摸索,线程池中的核心线程数CorePoolSize、最大线程数MaxPoolSize,设置成多少,最合适,效率最高。


借着这个机会,测试一下。


1、我记得有这样一个说法,CPU的处理器数量


将核心线程数CorePoolSize设置成CPU的处理器数量,是不是效率最高的?


// 获取CPU的处理器数量
int curSystemThreads = Runtime.getRuntime().availableProcessors() * 2;

Runtime.getRuntime().availableProcessors()获取的是CPU核心线程数,也就是计算资源。



  • CPU密集型,线程池大小设置为N,也就是和cpu的线程数相同,可以尽可能地避免线程间上下文切换,但在实际开发中,一般会设置为N+1,为了防止意外情况出现线程阻塞,如果出现阻塞,多出来的线程会继续执行任务,保证CPU的利用效率。

  • IO密集型,线程池大小设置为2N,这个数是根据业务压测出来的,如果不涉及业务就使用推荐。


在实际中,需要对具体的线程池大小进行调整,可以通过压测及机器设备现状,进行调整大小。


如果线程池太大,则会造成CPU不断的切换,对整个系统性能也不会有太大的提升,反而会导致系统缓慢。


我的电脑的CPU的处理器数量是24。


那么一次读取多少行最合适呢?


测试的Excel中含有10万条数据,10万/24 = 4166,那么我设置成4200,是不是效率最佳呢?


测试的过程中发现,好像真的是这样的。


2、我记得大家都习惯性的将核心线程数CorePoolSize和最大线程数MaxPoolSize设置成一样的,都爱设置成200。


是随便写的,还是经验而为之?


测试发现,当你将核心线程数CorePoolSize和最大线程数MaxPoolSize都设置为200的时候,第一次它会同时开启150个线程,来进行工作。


这个是为什么?


3、经过数十次的测试



  1. 发现核心线程数好像差别不大

  2. 每次读取和入库的数量是关键,不能太多,因为每次入库会变慢;

  3. 也不能太少,如果太少,超过了150个线程,就会造成线程阻塞,也会变慢;


四、通过EasyExcel读取并插入数据库


EasyExcel的方式,我就不写双异步优化了,大家切记陷入低水平勤奋的怪圈。


1、ReadEasyExcelController


@RequestMapping(value = "/readEasyExcel", method = RequestMethod.POST)
@ResponseBody
public String readEasyExcel() {
try {
String path = "G:\\测试\\data\\";
String[] xlsxArr = new File(path).list();
for (int i = 0; i < xlsxArr.length; i++) {
String filePath = path + xlsxArr[i];
File fileTemp = new File(path + xlsxArr[i]);
String fileName = fileTemp.getName().replace(".xlsx", "");
List list = new ArrayList<>();
EasyExcel.read(filePath, UserInfo.class, new ReadEasyExeclAsyncListener(readEasyExeclService, fileName, batchCount, list)).sheet().doRead();
}
}catch (Exception e){
logger.error("readEasyExcel 异常:",e);
return "error";
}
return "suceess";
}

2、ReadEasyExeclAsyncListener


public ReadEasyExeclService readEasyExeclService;
// 表名
public String TABLE_NAME;
// 批量插入阈值
private int BATCH_COUNT;
// 数据集合
private List LIST;

public ReadEasyExeclAsyncListener(ReadEasyExeclService readEasyExeclService, String tableName, int batchCount, List list) {
this.readEasyExeclService = readEasyExeclService;
this.TABLE_NAME = tableName;
this.BATCH_COUNT = batchCount;
this.LIST = list;
}

@Override
public void invoke(UserInfo data, AnalysisContext analysisContext) {
data.setUuid(uuid());
data.setTableName(TABLE_NAME);
LIST.add(data);
if(LIST.size() >= BATCH_COUNT){
// 批量入库
readEasyExeclService.saveDataBatch(LIST);
}
}

@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
if(LIST.size() > 0){
// 最后一批入库
readEasyExeclService.saveDataBatch(LIST);
}
}

public static String uuid() {
return UUID.randomUUID().toString().replace("-", "");
}
}

3、ReadEasyExeclServiceImpl


@Service
public class ReadEasyExeclServiceImpl implements ReadEasyExeclService {

@Resource
private ReadEasyExeclMapper readEasyExeclMapper;

@Override
public void saveDataBatch(List list) {
// 通过mybatis入库
readEasyExeclMapper.saveDataBatch(list);
// 通过JDBC入库
// insertByJdbc(list);
list.clear();
}

private void insertByJdbc(List list){
List sqlList = new ArrayList<>();
for (UserInfo u : list){
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append("insert int0 ").append(u.getTableName()).append(" ( UUID,ID,NAME,AGE,ADDRESS,PHONE,OP_TIME ) values ( ");
sqlBuilder.append("'").append(ReadEasyExeclAsyncListener.uuid()).append("',")
.append("'").append(u.getId()).append("',")
.append("'").append(u.getName()).append("',")
.append("'").append(u.getAge()).append("',")
.append("'").append(u.getAddress()).append("',")
.append("'").append(u.getPhone()).append("',")
.append("sysdate )");
sqlList.add(sqlBuilder.toString());
}

JdbcUtil.executeDML(sqlList);
}
}

4、UserInfo


@Data
public class UserInfo {

private String tableName;

private String uuid;

@ExcelProperty(value = "ID")
private String id;

@ExcelProperty(value = "NAME")
private String name;

@ExcelProperty(value = "AGE")
private String age;

@ExcelProperty(value = "ADDRESS")
private String address;

@ExcelProperty(value = "PHONE")
private String phone;
}



作者:哪吒编程
来源:juejin.cn/post/7315730050577694720
收起阅读 »

SpringBoot统一结果返回,统一异常处理,大牛都这么玩

引言 在开发Spring Boot应用时,我们经常面临着不同的控制器方法需要处理各种不同类型的响应结果,以及在代码中分散处理异常可能导致项目难以维护的问题。你是否曾经遇到过在不同地方编写相似的返回格式,或者在处理异常时感到有些混乱?这些看似小问题的积累,实际上...
继续阅读 »

引言


在开发Spring Boot应用时,我们经常面临着不同的控制器方法需要处理各种不同类型的响应结果,以及在代码中分散处理异常可能导致项目难以维护的问题。你是否曾经遇到过在不同地方编写相似的返回格式,或者在处理异常时感到有些混乱?这些看似小问题的积累,实际上可能对项目产生深远的影响。统一结果返回和统一异常处理并非只是为了规范代码,更是为了提高团队的协作效率、降低项目维护的难度,并使代码更易于理解和扩展。


本文的目的是帮助你更好地理解和应用Spring Boot中的统一结果返回和统一异常处理。通过详细的讨论和实例演示,我们将为你提供一套清晰的指南,让你能够在自己的项目中轻松应用这些技术,提高代码质量,减轻开发压力。


统一结果返回


统一结果返回是一种通过定义通用的返回格式,使所有的响应结果都符合同一标准的方法。这有助于提高代码的一致性,减少重复代码的编写,以及使客户端更容易理解和处理API的响应。统一结果返回不仅规范了代码结构,还能提高团队协作效率,降低项目维护的难度。


接下来让我们一起看看在SpringBoot中如何实现统一结果返回。


1. 定义通用的响应对象


当实现统一结果返回时,需要创建一个通用的响应对象,定义成功和失败的返回情况,并确保在接口中使用这个通用返回对象。


@Setter  
@Getter  
public class ResultResponse<T> implements Serializable {  
    private static final long serialVersionUID = -1133637474601003587L;  

    /**  
     * 接口响应状态码  
     */
  
    private Integer code;  

    /**  
     * 接口响应信息  
     */
  
    private String msg;  

    /**  
     * 接口响应的数据  
     */
  
    private T data;
}    

2. 定义接口响应状态码


统一结果返回的关键之一是规定一套通用的状态码。这有助于客户端更容易地理解和处理 API 的响应,同时也为开发者提供了一致的标准。通常,一些 HTTP 状态码已经被广泛接受,如:



  • 200 OK:表示成功处理请求。

  • 201 Created:表示成功创建资源。

  • 204 No Content:表示成功处理请求,但没有返回任何内容。


对于错误情况,也可以使用常见的 HTTP 状态码,如:



  • 400 Bad Request:客户端请求错误。

  • 401 Unauthorized:未授权访问。

  • 404 Not Found:请求资源不存在。

  • 500 Internal Server Error:服务器内部错误。


除了 HTTP 状态码外,你还可以定义自己的应用程序特定状态码,以表示更具体的情况。确保文档中清晰地说明了每个状态码所代表的含义,使开发者能够正确地解释和处理它们。


public enum StatusEnum {  
    SUCCESS(200 ,"请求处理成功"),  
    UNAUTHORIZED(401 ,"用户认证失败"),  
    FORBIDDEN(403 ,"权限不足"),  
    SERVICE_ERROR(500"服务器去旅行了,请稍后重试"),  
    PARAM_INVALID(1000"无效的参数"),  
    ;  
    public final Integer code;  

    public final String message;  

    StatusEnum(Integer code, String message) {  
        this.code = code;  
        this.message = message;  
    }  

}

3. 定义统一的成功和失败的处理方法


定义统一的成功和失败的响应方法有助于保持代码一致性和规范性,简化控制器逻辑,提高代码复用性,降低维护成本,提高可读性,促进团队协作,以及更便于进行测试。


/**  
 * 封装成功响应的方法  
 * @param data 响应数据  
 * @return reponse  
 * @param <T> 响应数据类型  
 */
  
public static <T> ResultResponse<T> success(T data) {  

    ResultResponse<T> response = new ResultResponse<>();  
    response.setData(data);  
    response.setCode(StatusEnum.SUCCESS.code);  
    return response;  
}  

/**  
 * 封装error的响应  
 * @param statusEnum error响应的状态值  
 * @return  
 * @param <T>  
 */
  
public static <T> ResultResponse<T> error(StatusEnum statusEnum) {  
   return error(statusEnum, statusEnum.message);  
}  

/**  
 * 封装error的响应  可自定义错误信息
 * @param statusEnum error响应的状态值  
 * @return  
 * @param <T>  
 */
  
public static <T> ResultResponse<T> error(StatusEnum statusEnum, String errorMsg) {  
    ResultResponse<T> response = new ResultResponse<>();  
    response.setCode(statusEnum.code);  
    response.setMsg(errorMsg);  
    return response;  
}

4. web层统一响应结果


在web层使用统一结果返回的目的是将业务逻辑的处理结果按照预定的通用格式进行封装,以提高代码的一致性和可读性。


@RestController  
@RequestMapping("user")  
@Validated  
@Slf4j  
public class UserController {  

    private IUserService userService;  

    /**  
     * 创建用户  
     * @param requestVO  
     * @return  
     */
  
    @PostMapping("create")  
    public ResultResponse<VoidcreateUser(@Validated @RequestBody UserCreateRequestVO requestVO){  
        return ResultResponse.success(null);  
    }

    /**  
     * 根据用户ID获取用户信息  
     * @param userId 用户id  
     * @return 用户信息  
     */
  
    @GetMapping("info")  
    public ResultResponse<UserInfoResponseVOgetUser(@NotBlank(message = "请选择用户") String userId){  
        final UserInfoResponseVO responseVO = userService.getUserInfoById(userId);  
        return ResultResponse.success(responseVO);  
    }  

    @Autowired  
    public void setUserService(IUserService userService) {  
        this.userService = userService;  
    }  
}

调用接口,响应的信息统一为:


{
    "code": 200,
    "msg": null,
    "data": null
}

{
    "code": 200,
    "msg": null,
    "data": {
        "userId": "121",
        "userName": "码农Academy"
    }
}

统一结果返回通过定义通用的返回格式、成功和失败的返回情况,以及在控制器中使用这一模式,旨在提高代码的一致性、可读性和可维护性。采用统一的响应格式简化了业务逻辑处理流程,使得开发者更容易处理成功和失败的情况,同时客户端也更容易理解和处理 API 的响应。这一实践有助于降低维护成本、提高团队协作效率,并促进代码的规范化。


统一异常处理


统一异常处理的必要性体现在保持代码的一致性、提供更清晰的错误信息、以及更容易排查问题。通过定义统一的异常处理方式,确保在整个应用中对异常的处理保持一致,减少了重复编写相似异常处理逻辑的工作,同时提供友好的错误信息帮助开发者和维护人员更快地定位和解决问题,最终提高了应用的可维护性和可读性。


1.定义统一的异常类


我们需要定义服务中可能抛出的自定义异常类。这些异常类可以继承自RuntimeException,并携带有关异常的相关信息。即可理解为局部异常,用于特定的业务处理中异常。手动埋点抛出。


@Getter  
public class ServiceException extends RuntimeException{  

    private static final long serialVersionUID = -3303518302920463234L;  

    private final StatusEnum status;  

    public ServiceException(StatusEnum status, String message) {  
        super(message);  
        this.status = status;  
    }  

    public ServiceException(StatusEnum status) {  
        this(status, status.message);  
    }  
}

2.异常处理器


创建一个全局的异常处理器,使用@ControllerAdvice 或者 @RestControllerAdvice注解和@ExceptionHandler注解来捕获不同类型的异常,并定义处理逻辑。


2.1 @ControllerAdvice注解

用于声明一个全局控制器建言(Advice),相当于把@ExceptionHandler@InitBinder@ModelAttribute注解的方法集中到一个地方。常放在一个特定的类上,这个类被认为是全局异常处理器,可以跨足多个控制器。



当时用@ControllerAdvice时,我们需要在异常处理方法上加上@ResponseBody,同理我们的web接口。但是如果我们使用@RestControllerAdvice 就可以不用加,同理也是web定义的接口



2.2 @ExceptionHandler注解

用于定义异常处理方法,处理特定类型的异常。放在全局异常处理器类中的具体方法上。


通过这两个注解的配合,可以实现全局的异常处理。当控制器中抛出异常时,Spring Boot会自动调用匹配的@ExceptionHandler方法来处理异常,并返回定义的响应。


@Slf4j  
@ControllerAdvice  
public class ExceptionAdvice {  

    /**  
     * 处理ServiceException  
     * @param serviceException ServiceException  
     * @param request 请求参数  
     * @return 接口响应  
     */
  
    @ExceptionHandler(ServiceException.class)  
    @ResponseBody  
    public ResultResponse<Void> handleServiceException(ServiceException serviceException, HttpServletRequest request) {  
        log.warn("request {} throw ServiceException \n", request, serviceException);  
        return ResultResponse.error(serviceException.getStatus(), serviceException.getMessage());  
    }  

    /**  
     * 其他异常拦截  
     * @param ex 异常  
     * @param request 请求参数  
     * @return 接口响应  
     */
  
    @ExceptionHandler(Exception.class)  
    @ResponseBody  
    public ResultResponse<VoidhandleException(Exception ex, HttpServletRequest request) {  
        log.error("request {} throw unExpectException \n", request, ex);  
        return ResultResponse.error(StatusEnum.SERVICE_ERROR);  
    }
}    

3.异常统一处理使用


在业务开发过程中,我们可以在service层处理业务时,可以手动抛出业务异常。由全局异常处理器进行统一处理。


@Service  
@Slf4j  
public class UserServiceImpl implements IUserService {  

    private IUserManager userManager;

    /**  
     * 创建用户  
     *  
     * @param requestVO 请求参数  
     */
  
    @Override  
    public void createUser(UserCreateRequestVO requestVO) {  
        final UserDO userDO = userManager.selectUserByName(requestVO.getUserName());  
        if (userDO != null){  
            throw new ServiceException(StatusEnum.PARAM_INVALID, "用户名已存在");  
        }  
    }

    @Autowired  
    public void setUserManager(IUserManager userManager) {  
        this.userManager = userManager;  
    }  
}

@RestController  
@RequestMapping("user")  
@Validated  
@Slf4j  
public class UserController {  

    private IUserService userService;  

    /**  
     * 创建用户  
     * @param requestVO  
     * @return  
     */
  
    @PostMapping("create")  
    public ResultResponse<VoidcreateUser(@Validated @RequestBody UserCreateRequestVO requestVO){  
        userService.createUser(requestVO);  
        return ResultResponse.success(null);  
    }

    @Autowired  
    public void setUserService(IUserService userService) {  
        this.userService = userService;  
    }  
}

当我们请求接口时,假如用户名称已存在,接口就会响应:


{
    "code": 1000,
    "msg": "用户名已存在",
    "data": null
}

统一异常处理带来的好处包括提供一致的异常响应格式,简化异常处理逻辑,记录更好的错误日志,以及更容易排查和解决问题。通过统一处理异常,我们确保在整个应用中对异常的处理方式一致,减少了重复性代码的编写,提高了代码的规范性。简化的异常处理逻辑降低了开发者的工作负担,而更好的错误日志有助于更迅速地定位和解决问题,最终提高了应用的可维护性和稳定性。


其他类型的异常处理


在项目开发过程中,我们还有一些常见的特定异常类型,比如MethodArgumentNotValidExceptionUnexpectedTypeException等,并为它们定义相应的异常处理逻辑。这些特定异常可能由于请求参数校验失败或意外的数据类型问题而引起,因此有必要为它们单独处理,以提供更具体和友好的异常响应。


1.MethodArgumentNotValidException


由于请求参数校验失败引起的异常,通常涉及到使用@Valid注解或者@Validated进行请求参数校验。我们可以在异常处理器中编写@ExceptionHandler方法,捕获并处理MethodArgumentNotValidException,提取校验错误信息,并返回详细的错误响应。


/**  
 * 参数非法校验  
 * @param ex  
 * @return  
 */
  
@ExceptionHandler(MethodArgumentNotValidException.class)  
@ResponseBody  
public ResultResponse<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {  
    try {  
        List<ObjectErrorerrors = ex.getBindingResult().getAllErrors();  
        String message = errors.stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(","));  
        log.error("param illegal: {}", message);  
        return ResultResponse.error(StatusEnum.PARAM_INVALID, message);  
    } catch (Exception e) {  
        return ResultResponse.error(StatusEnum.SERVICE_ERROR);  
    }  
}

当我们使用@Valid注解或者@Validated进行请求参数校验不通过时,响应结果为:


{
    "code": 1000,
    "msg": "请输入地址信息,用户年龄必须小于60岁,请输入你的兴趣爱好",
    "data": null
}


关于@Valid注解或者@Validated进行参数校验的功能请参考:SpringBoot优雅校验参数



2.UnexpectedTypeException


意外的数据类型异常,通常表示程序运行时发生了不符合预期的数据类型问题。一个常见的使用场景是在数据转换或类型处理的过程中。例如,在使用 Spring 表单绑定或数据绑定时,如果尝试将一个不符合预期类型的值转换为目标类型,就可能抛出 UnexpectedTypeException。这通常会发生在将字符串转换为数字、日期等类型时,如果字符串的格式不符合目标类型的要求。


我们可以在异常处理器中编写@ExceptionHandler方法,捕获并处理UnexpectedTypeException,提供适当的处理方式,例如记录错误日志,并返回合适的错误响应。


@ExceptionHandler(UnexpectedTypeException.class)  
@ResponseBody  
public ResultResponse<Void> handleUnexpectedTypeException(UnexpectedTypeException ex,  
                                                        HttpServletRequest request) {  
    log.error("catch UnexpectedTypeException, errorMessage: \n", ex);  
    return ResultResponse.error(StatusEnum.PARAM_INVALID, ex.getMessage());  
}

当发生异常时,接口会响应:


{
    "code": 500,
    "msg": "服务器去旅行了,请稍后重试",
    "data": null
}

3.ConstraintViolationException


javax.validation.ConstraintViolationException 是 Java Bean Validation(JSR 380)中的一种异常。它通常在使用 Bean Validation 进行数据校验时,如果校验失败就会抛出这个异常。即我们在使用自定义校验注解时,如果不满足校验规则,就会抛出这个错误。


@ExceptionHandler(ConstraintViolationException.class)  
@ResponseBody  
public ResultResponse<Void> handlerConstraintViolationException(ConstraintViolationException ex, HttpServletRequest request) {  
    log.error("request {} throw ConstraintViolationException \n", request, ex);  
    return ResultResponse.error(StatusEnum.PARAM_INVALID, ex.getMessage());  
}


案例请参考:SpringBoot优雅校验参数,注册ConstraintValidator示例中的@UniqueUser校验。



4.HttpMessageNotReadableException


表示无法读取HTTP消息的异常,通常由于请求体不合法或不可解析。


@ResponseBody  
@ResponseStatus(HttpStatus.BAD_REQUEST)  
@ExceptionHandler(HttpMessageNotReadableException.class)  
public ResultResponse<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException ex,  
HttpServletRequest request) {  
    log.error("request {} throw ucManagerException \n", request, ex);  
    return ResultResponse.error(StatusEnum.SERVICE_ERROR);  
}

5.HttpRequestMethodNotSupportedException


Spring Framework 中的异常类,表示请求的 HTTP 方法不受支持。当客户端发送了一个使用不被服务器支持的 HTTP 方法(如 GET、POST、PUT、DELETE等)的请求时,可能会抛出这个异常。


@ExceptionHandler({HttpRequestMethodNotSupportedException.class, HttpMediaTypeException.class})  
@ResponseBody  
public ResultResponse<Void> handleMethodNotSupportedException(Exception ex) {  
    log.error("HttpRequestMethodNotSupportedException \n", ex);  
    return ResultResponse.error(StatusEnum.HTTP_METHOD_NOT_SUPPORT);  
}

全局异常处理与局部异常处理在Spring Boot应用开发中扮演不同角色。全局异常处理通过统一的异常处理器确保了整个应用对异常的处理一致性,减少了冗余代码,提高了代码的整洁度。然而,这种方式可能在灵活性上略显不足,无法满足每个具体控制器或业务场景的个性化需求。


相比之下,局部异常处理能够为每个控制器或业务场景提供更具体、灵活的异常处理逻辑,允许定制化的异常响应。这使得在复杂的项目中更容易处理特定的异常情况,同时提供更详细的错误信息。然而,局部异常处理可能带来代码冗余和维护难度的问题,特别是在大型项目中。


在实际应用中,选择全局异常处理还是局部异常处理应根据项目规模和需求进行权衡。对于小型项目或简单场景,全局异常处理可能是一种更简单、合适的选择。而对于大型项目或需要个性化异常处理的复杂业务逻辑,局部异常处理则提供了更为灵活的方案。最佳实践是在项目中根据具体情况灵活使用这两种方式,以平衡一致性和个性化需求。


最佳实践与注意事项


1. 最佳实践



  • 统一响应格式:  在异常处理中,使用统一的响应格式有助于客户端更容易理解和处理错误。通常,返回一个包含错误码、错误信息和可能的详细信息的响应对象。

  • 详细错误日志:  在异常处理中记录详细的错误日志,包括异常类型、发生时间、请求信息等。这有助于快速定位和解决问题。

  • 使用HTTP状态码:  根据异常的性质,选择适当的HTTP状态码。例如,使用HttpStatus.NOT_FOUND表示资源未找到,HttpStatus.BAD_REQUEST表示客户端请求错误等。

  • 异常分类:  根据异常的种类,合理分类处理。可以定义不同的异常类来表示不同的异常情况,然后在异常处理中使用@ExceptionHandler分别处理。

  • 全局异常处理:  使用全局异常处理机制来捕获未被特定控制器处理的异常,以确保应用在整体上的健壮性。


2 注意事项



  • 不滥用异常:  异常应该用于表示真正的异常情况,而不是用作控制流程。滥用异常可能导致性能问题和代码可读性降低。

  • 不忽略异常:  避免在异常处理中忽略异常或仅仅打印日志而不进行适当的处理。这可能导致潜在的问题被掩盖,难以追踪和修复。

  • 避免空的catch块:  不要在catch块中什么都不做,这样会使得异常难以被发现。至少在catch块中记录日志,以便了解异常的发生。

  • 适时抛出异常:  不要过于吝啬地抛出异常,但也不要无谓地滥用。在必要的时候使用异常,例如表示无法继续执行的错误情况。

  • 测试异常场景:  编写单元测试时,确保覆盖异常场景,验证异常的正确抛出和处理。


总结


异常处理在应用开发中是至关重要的一环,它能够提高应用的健壮性、可读性和可维护性。全局异常处理和局部异常处理各有优劣,需要根据项目的规模和需求来灵活选择。通过采用统一的响应格式、详细的错误日志、适当的HTTP状态码等最佳实践,可以使异常处理更为有效和易于管理。同时,注意避免滥用异常、忽略异常、适时抛出异常等注意事项,有助于确保异常处理的质量。在开发过程中,持续关注和优化异常处理,将有助于提高应用的稳定性和用户体验。


本文已收录我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等


作者:码农Academy
来源:juejin.cn/post/7322463748006248459
收起阅读 »

不要再用 StringBuilder 拼接字符串了,来试试字符串模板

引言 字符串操作是 Java 中使用最频繁的操作,没有之一。其中非常常见的操作之一就是对字符串的组织,由于常见所以就衍生了多种方案。比如我们要实现 x + y = ?,方案有如下几种 使用 + 进行字符串拼接 String s = x + " + " + ...
继续阅读 »


引言


字符串操作是 Java 中使用最频繁的操作,没有之一。其中非常常见的操作之一就是对字符串的组织,由于常见所以就衍生了多种方案。比如我们要实现 x + y = ?,方案有如下几种



  • 使用 + 进行字符串拼接


String s = x + " + " + y + " = " + (x + y);


  • 使用 StringBuilder


String s = new StringBuilder()
.append(x)
.append(" + ")
.append(y)
.append(" = ")
.append(x + y)
.toString()


  • String::formatString::formatted 将格式字符串从参数中分离出来


String s = String.format("%2$d + %1$d = %3$d", x, y, x + y);
or
String s = "%2$d + %1$d = %3$d".formatted(x, y, x + y);


  • java.text.MessageFormat


String s = MessageFormat.format("{0} + {1} = {2}", x,y, x + y);

这四种方案虽然都可以解决,但很遗憾的是他们或多或少都有点儿缺陷,尤其是面对 Java 13 引入的文本块(Java 13 新特性—文本块)更是束手无措。


字符串模板


为了简化字符串的构造和格式化,Java 21 引入字符串模板功能,该特性主要目的是为了提高在处理包含多个变量和复杂格式化要求的字符串时的可读性和编写效率。


它的设计目标是:



  • 通过简单的方式表达混合变量的字符串,简化 Java 程序的编写。

  • 提高混合文本和表达式的可读性,无论文本是在单行源代码中(如字符串字面量)还是跨越多行源代码(如文本块)。

  • 通过支持对模板及其嵌入式表达式的值进行验证和转换,提高根据用户提供的值组成字符串并将其传递给其他系统(如构建数据库查询)的 Java 程序的安全性。

  • 允许 Java 库定义字符串模板中使用的格式化语法(java.util.Formatter ),从而保持灵活性。

  • 简化接受以非 Java 语言编写的字符串(如 SQL、XML 和 JSON)的 API 的使用。

  • 支持创建由字面文本和嵌入式表达式计算得出的非字符串值,而无需通过中间字符串表示。


该特性处理字符串的新方法称为:Template Expressions,即:模版表达式。它是 Java 中的一种新型表达式,不仅可以执行字符串插值,还可以编程,从而帮助开发人员安全高效地组成字符串。此外,模板表达式并不局限于组成字符串——它们可以根据特定领域的规则将结构化文本转化为任何类型的对象。


STR 模板处理器


STR 是 Java 平台定义的一种模板处理器。它通过用表达式的值替换模板中的每个嵌入表达式来执行字符串插值。使用 STR 的模板表达式的求值结果是一个字符串。


STR 是一个公共静态 final 字段,会自动导入到每个 Java 源文件中。


我们先看一个简单的例子:


    @Test
public void STRTest() {
String sk = "死磕 Java 新特性";
String str1 = STR."{sk},就是牛";
System.out.println(str1);
}
// 结果.....
死磕 Java 新特性,就是牛

上面的 STR."{sk},就是牛" 就是一个模板表达式,它主要包含了三个部分:



  • 模版处理器:STR

  • 包含内嵌表达式({blog})的模版

  • 通过.把前面两部分组合起来,形式如同方法调用


当模版表达式运行的时候,模版处理器会将模版内容与内嵌表达式的值组合起来,生成结果。


这个例子只是 STR模版处理器一个很简单的功能,它可以做的事情有很多。



  • 数学运算


比如上面的 x + y = ?


    @Test
public void STRTest() {
int x = 1,y =2;
String str = STR."{x} + {y} = {x + y}";
System.out.println(str);
}

这种写法是不是简单明了了很多?



  • 调用方法


STR模版处理器还可以调用方法,比如:


String str = STR."今天是:{ LocalDate.now()} ";

当然也可以调用我们自定义的方法:


    @Test
public void STRTest() {
String str = STR."{getSkStr()},就是牛";
System.out.println(str);
}

public String getSkStr() {
return "死磕 Java 新特性";
}


  • 访问成员变量


STR模版处理器还可以访问成员变量,比如:


public record User(String name,Integer age) {
}

@Test
public void STRTest() {
User user = new User("大明哥",18);
String str = STR."{user.name()}今年{user.age()}";
System.out.println(str);
}

需要注意的是,字符串模板表达式中的嵌入表达式数量没有限制,它从左到右依次求值,就像方法调用表达式中的参数一样。例如:


    @Test
public void STRTest() {
int i = 0;
String str = STR."{i++},{i++},{i++},{i++},{i++}";
System.out.println(str);
}
// 结果......
0,1,2,3,4

同时,表达式中也可以嵌入表达式:


    @Test
public void STRTest() {
String name = "大明哥";
String sk = "死磕 Java 新特性";
String str = STR."{name}的{STR."{sk},就是牛..."}";
System.out.println(str);
}
// 结果......
大明哥的死磕 Java 新特性,就是牛...

但是这种嵌套的方式会比较复杂,容易搞混,一般不推荐。


多行模板表达式


为了解决多行字符串处理的复杂性,Java 13 引入文本块(Java 13 新特性—文本块),它是使用三个双引号(""")来标记字符串的开始和结束,允许字符串跨越多行而无需显式的换行符或字符串连接。如下:


String html = """
<html>
<body>
<h2>skjava.com</h2>
<ul>
<li>死磕 Java 新特性</li>
<li>死磕 Java 并发</li>
<li>死磕 Netty</li>
<li>死磕 Redis</li>
</ul>
</body>
</html>
""";

如果字符串模板表达式,我们就只能拼接这串字符串了,这显得有点儿繁琐和麻烦。而字符串模版表达式也支持多行字符串处理,我们可以利用它来方便的组织html、json、xml等字符串内容,比如这样:


    @Test
public void STRTest() {
String title = "skjava.com";
String sk1 = "死磕 Java 新特性";
String sk2 = "死磕 Java 并发";
String sk3 = "死磕 Netty";
String sk4 = "死磕 Redis";

String html = STR."""
<html>
<body>
<h2>{title}</h2>
<ul>
<li>{sk1}</li>
<li>{sk2}</li>
<li>{sk3}</li>
<li>{sk4}</li>
</ul>
</body>
</html>
"""
;
System.out.println(html);
}

如果决定定义四个 sk 变量麻烦,可以整理为一个集合,然后调用方法生成 <li> 标签。


FMT 模板处理器


FMT 是 Java 定义的另一种模板处理器。它除了与STR模版处理器一样提供插值能力之外,还提供了左侧的格式化处理。下面我们来看看他的功能。比如我们要整理模式匹配的 Switch 表达在 Java 版本中的迭代,也就是下面这个表格


Java 版本更新类型JEP更新内容
Java 17第一次预览JEP 406引入模式匹配的 Swith 表达式作为预览特性。
Java 18第二次预览JEP 420对其做了改进和细微调整
Java 19第三次预览JEP 427进一步优化模式匹配的 Swith 表达式
Java 20第四次预览JEP 433
Java 21正式特性JEP 441成为正式特性

如果使用 STR 模板处理器,代码如下:


    @Test
public void STRTest() {
SwitchHistory[] switchHistories = new SwitchHistory[]{
new SwitchHistory("Java 17","第一次预览","JEP 406","引入模式匹配的 Swith 表达式作为预览特性。"),
new SwitchHistory("Java 18","第二次预览","JEP 420","对其做了改进和细微调整"),
new SwitchHistory("Java 19","第三次预览","JEP 427","进一步优化模式匹配的 Swith 表达式"),
new SwitchHistory("Java 20","第四次预览","JEP 433",""),
new SwitchHistory("Java 21","正式特性","JEP 441","成为正式特性"),
};

String history = STR."""
Java 版本 更新类型 JEP 更新内容
{switchHistories[0].javaVersion()} {switchHistories[0].updateType()} {switchHistories[0].jep()} {switchHistories[0].content()}
{switchHistories[1].javaVersion()} {switchHistories[1].updateType()} {switchHistories[1].jep()} {switchHistories[1].content()}
{switchHistories[2].javaVersion()} {switchHistories[2].updateType()} {switchHistories[2].jep()} {switchHistories[2].content()}
{switchHistories[3].javaVersion()} {switchHistories[3].updateType()} {switchHistories[3].jep()} {switchHistories[3].content()}
{switchHistories[4].javaVersion()} {switchHistories[4].updateType()} {switchHistories[4].jep()} {switchHistories[4].content()}
""";
System.out.println(history);
}

得到的效果是这样的:


Java 版本     更新类型    JEP 更新内容
Java 17 第一次预览 JEP 406 引入模式匹配的 Swith 表达式作为预览特性。
Java 18 第二次预览 JEP 420 对其做了改进和细微调整
Java 19 第三次预览 JEP 427 进一步优化模式匹配的 Swith 表达式
Java 20 第四次预览 JEP 433
Java 21 正式特性 JEP 441 成为正式特性

是不是很丑?完全对不齐,没法看。为了解决这个问题,就可以采用FMT模版处理器,在每一列左侧定义格式:


   @Test
public void STRTest() {
SwitchHistory[] switchHistories = new SwitchHistory[]{
new SwitchHistory("Java 17","第一次预览","JEP 406","引入模式匹配的 Swith 表达式作为预览特性。"),
new SwitchHistory("Java 18","第二次预览","JEP 420","对其做了改进和细微调整"),
new SwitchHistory("Java 19","第三次预览","JEP 427","进一步优化模式匹配的 Swith 表达式"),
new SwitchHistory("Java 20","第四次预览","JEP 433",""),
new SwitchHistory("Java 21","正式特性","JEP 441","成为正式特性"),
};

String history = FMT."""
Java 版本 更新类型 JEP 更新内容
%-10s{switchHistories[0].javaVersion()} %-9s{switchHistories[0].updateType()} %-10s{switchHistories[0].jep()} %-20s{switchHistories[0].content()}
%-10s{switchHistories[1].javaVersion()} %-9s{switchHistories[1].updateType()} %-10s{switchHistories[1].jep()} %-20s{switchHistories[1].content()}
%-10s{switchHistories[2].javaVersion()} %-9s{switchHistories[2].updateType()} %-10s{switchHistories[2].jep()} %-20s{switchHistories[2].content()}
%-10s{switchHistories[3].javaVersion()} %-9s{switchHistories[3].updateType()} %-10s{switchHistories[3].jep()} %-20s{switchHistories[3].content()}
%-10s{switchHistories[4].javaVersion()} %-9s{switchHistories[4].updateType()} %-10s{switchHistories[4].jep()} %-20s{switchHistories[4].content()}
""";
System.out.println(history);
}

输出如下:


Java 版本     更新类型        JEP             更新内容
Java 17 第一次预览 JEP 406 引入模式匹配的 Swith 表达式作为预览特性。
Java 18 第二次预览 JEP 420 对其做了改进和细微调整
Java 19 第三次预览 JEP 427 进一步优化模式匹配的 Swith 表达式
Java 20 第四次预览 JEP 433
Java 21 正式特性 JEP 441 成为正式特性

作者:大明哥_
来源:juejin.cn/post/7323251349302706239
收起阅读 »