注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

一个30岁前端老社畜的人生经历

web
前言 在掘金多年,我一直是一个读者,从事前端快8年了,每天都在看一些视频和资料以及别人的日记,零零碎碎我也做过一些笔记,但是都不成体系。这些笔记至今留存在各种应用上,写了就再也没打开过,还是没有养成习惯,我希望能坚持下去,为自己的人生添加一点历史,等以后老了,...
继续阅读 »

前言


在掘金多年,我一直是一个读者,从事前端快8年了,每天都在看一些视频和资料以及别人的日记,零零碎碎我也做过一些笔记,但是都不成体系。这些笔记至今留存在各种应用上,写了就再也没打开过,还是没有养成习惯,我希望能坚持下去,为自己的人生添加一点历史,等以后老了,我还能证明我的青春有过一些记录,偶尔回味也会是一件比较幸福的事情。


近些年,感觉社会戾气挺重的,特别是疫情的时候,抖音里面的那些评论很让人糟心,现在年轻人也逐渐选择躺平,也是对社会的卷系妥协,随着经济的下滑,一般学校的研究生可能都很难找到一个比较ok的工作,更别提本科或者大专,作为学历真的拿不出手的我,更加焦虑。


从业前端快8年了,做过很多类型的项目,小到一般的H5展示网页,大到区块链应用、智能能源项目;其实回过头来看,没有什么大的成就感,我的从业经验只获得过一次奖杯,就是吃苦耐劳奖一个镀金的大手指,那还是我4年前在一家外包公司连续工作48小时做一个小程序上线后,老板看我确实辛苦,于是发了一个这个奖杯给我,后面被我娃摔坏了,就啥也没有了。


2023


2023年其实回头来看,收获并不是很大,归纳下来也没有几条:


  1. 今年非全研究生在读了

  2. 今年提交了入党申请

  3. 第二套房子装修完成

  4. 小孩来到了身边读书(之前在农村读幼儿园)

  5. 工作中学会了Vue3,能用Java做开发,同时更了解了业务方面的知识

  6. 开始了写作的习惯

  7. 跑了5场马拉松


2024 展望


2024年还有几天就到了,我希望每一年都能有点收获吧,立几个flag:


  1. 带妈妈旅游一次

  2. 成为党员积极分子

  3. 提高Java方面的基础知识,以及three 3D方面的能力

  4. 看不低于5本技术书籍,至少写30篇技术文章

  5. 还清自己的个人债务,当然不包含房贷

  6. 跑5场马拉松


行业展望


目前行业有些自媒体在唱衰,说前端已死,但是我觉得没这么悲观,国家多次强调往智能方向发展,各行业的智能得依托计算机才能智能,像什么智慧制造,智慧能源,智慧农村等等都需要计算机技术来运算和展示。前端只是比以前的要求会高一些了,在5,6年前对前端技术要求没有那么高,大家0基础都可以参与,但如今可能不行了,我觉得这是一个好事,要求高一点,薪资也会高一些。淘汰的,就是一开始就不适合这个行业的人。我目前在一家大型央企工作,还是算较为稳定,但同时也需要不断学习,因为或许某一天的淘汰人选就会是我,社会是残酷的,混日子终究不是一个好的方式。


个人真实经历分享


给大家分享一下我的个人真实经历,与君共勉。


我出生在一个普通的农村家庭,初中开始接触老虎机,高中接触网吧,17岁前没有出过县城,是个十足的井底之蛙,父亲几十年一直在外地工作,一年就回家两三次,从小就我妈妈一个人带我,她做装修的,每天早上7点就骑车外出上班了,下午5点回家,我在家做好饭菜等她回家吃了后,她就马上去田里土里种庄稼,喂了很多牲畜,高中毕业前一直都是这样(不过高中我住校,就我妈妈和我妹在家),我妈妈非常节约,从我记事起,每年只有过年她才会舍得给自己买一两件衣服,因为她觉得过年要穿新的衣服,寓意者新的一年有好的开始,从来没给自己买过首饰,也从来没有烫染过头发,也从来没有赌博过,但同时我父亲其实并不是有责任心的人,基本从不过问家里,以及我的学习。


我的学习打小从小就不好,学习生涯当了两个月的劳动委员,这就是我的荣耀,因为我觉得我小时就是sb,在干啥完全不知道,在学校就是为了吃那一顿饭,和同学天天玩,初中要毕业就被学校各种“”“好言相劝” 去中专学技术,学会拿高薪,实际上是为了赚中专学校的回扣。中专后面又把我们送去富士康,天天12个小时流水线,学校也是为了赚富士康的回扣,我的学业就是这样被卖来卖去,突然觉得有些可悲。这也是普遍读书不行的农村孩子的现状。


我的第一份工作是从2013年开始的,到现在已经差不多10年了,那就做一个时间线看看我的悲催往事吧,这也是我第一次对外讲


2013-2014.02


毕业季,和同学们坐着学校包的几辆大巴车,开到了成都郫县的富士康厂区,哪个时候富士康才在这里建厂,每天的工作就是搬东西,从另外一个地方往厂区里面搬,后面正式开工就开始了每天12个小时的白夜班交替,本来身体从小就弱,经常生病,在富士康就是上班,生病,加上富士康十三跳以及厂区经常出事,我和同学晚上提着东西,连夜翻墙走的,对,真是翻墙走的,后面线长给我打电话,我说我已经不在成都了。不过线长是我老乡,还是跟我没算旷工,算正常离职。 这里不得不说一句,在厂里,一个芝麻官都的官威都不得了,我实在看不惯,加上没前途,才走的。那时候天天12小时到手工资3500,自己上班赚钱还挺潇洒的,下班就去麻辣烫,一人吃饱全家不饿,和同学们啤酒小菜吃着,真是潇洒,厂区还有来自五湖四海的同龄妹子,都是中专生,还是挺快乐的,因为大家年龄相同,就是吃喝玩乐。自然半年才没存什么钱,灰溜溜回了老家,被我妈骂了一顿。


总结:富士康收获: 吃喝玩乐,此刻我的人生规划一无所知


2014.2-2015.07


回了老家,每天早上我妈6点就起来做饭、洗衣服、扫地等等,我起来烧柴,跟猪熬糠羹,喂猪,经常都是公鸡还没叫,我们都忙活一阵了,坚持了十天我就受不了了,因为我得承认,我出去工作后我变懒了,但是每天晚上很晚才睡,因为我在成都买了一个山寨的洛基亚手机,我开始在QQ聊天了,枯燥的生活我受不了,我要出去上班,我就去了重庆。就我妈和我妹两个人在家,这里我的说一下,我去重庆了,我妹才读幼儿园,我妈每天已经非常忙了,我妹从小就是邻居照看,她是位留守的老人,她每天给我妹妹吃好喝好的,比我奶奶好了太多,因为我妈性格很强势,和奶奶性格不合,我奶奶从来没照顾过我和我妹,都在伯伯家带他们的孩子,我妈妈经常晚上8.9点才从田里回家,我妹都是在邻居婆婆那儿吃饭睡觉,前几年她去世了,我妈妈哭了好久,因为她是我家的大贵人,现在每次走到她的坟前,我们都会去跪拜她。现在想起来,我妈太伟大了,她一生都是这么勤劳,吃苦。


到重庆了,上了一年多的厂,其实也是浑浑噩噩的,没有学历,只有在厂里做检验员,一个月2400的工资,入不敷出,因为当时听说主管也是中专学习,干了10多年才当主管,主管才5200一月工资,我觉得没前途,加上厂里玩的好的同事也走了,我也就走了


收获:C1驾-照, 成人高考专科录取通知书, iSO9O001,iSO14001 两个体系证书


2015.7-2016.7


这一年我就像做梦一样,2015年3月去学校报到,认识了班花我老婆,然后就开始交往,然后10月的时候,检查到怀孕了,过年就去了她的老家,因为怀孕了,也就准备结婚的事情了,同学们简直惊叹,纷纷问我怎么办到的?我才23岁,当父亲完全没概念,不过这也满足了我家人的愿望,穷人家里早当家,就在这一年,我妈妈存了一辈子的钱就被我花完了。10月检查怀孕,11月孩子她妈跟我父母說了要了买房买车的事情,我妈妈非常反对,后面我外婆对我妈说:你就这一个儿子,你都不帮他,以后他不恨你吗? 我妈妈想了几晚她咬咬牙还是同意了,过年去了女方家,她父母挺喜欢我的,我妈妈第二年年初付了房子首付26万,后面装修8万,买车8万,结婚7万,对没听错,全是我妈出的,她平时在农村做装修,有的时候包工,一个月7,8000有的时候包工一万多一个月工资,省吃俭用,全部存下的,都被我全部榨干了,好在岳父岳母没有要我一分彩礼,还给我2万块钱装修,他们也是农村人,也是吃了很多苦,2万得他们在工地干很久了,他们在老家为我们办了十多桌,请了一村的人来吃饭。


我妈后面才跟我说,这么多钱,我爸只出了一万块钱,我现在都不可思议,他在外面这么多年的钱去哪儿了? 但我也不恨他,毕竟每个人想法不同,他没有义务要给我出这些费用,不过好在之前房子一个平方8000,算是重庆比较贵的房子了,现在26000一个平方,算是赚了一些,有了一个家庭的财产保障,之前还要贵一点,现在房地产不行降了一些。


2016年7月后我出来也是误打误撞的进入了计算机这个行业,我之前压根就不了解这个行业,是看的招聘网站,招聘信息写的5000的工资,我那时候才3000多,在做销售,简直是高薪了,结果去了才知道,原来是计算机培训学校,耐不住那个美女姐姐各种软磨硬泡,我还是去学了计算机,当然,钱还是我妈跟我出的,因为也是孩子她妈跟我妈说这个行业好,比上厂强,我妈才听了她的,要是我说,那根本不管用。


收获:买房,结婚,买车,装修,好像所有的大事这段时间都基本完成了,虽然都是我妈出的钱


2016.7-2018.7


2016年10月孩子出生了,我也从培训学校出来工作了一段时间,培训机构学了4个月,时间都忙家里的事情去了,所以一毕业面试了20多家公司,都被打击了,每次都想放弃,但是回到家,看到家人,我都心里说不出的滋味,为此也哭了好多次,孩子她妈跟着我这些年,没买过一件超过300的衣服,全是淘宝的几十块一件的,我妈妈为了我在农村不管工作有多远,天气有多冷,都要去工作,我觉得我就是累赘,那时我24岁,我压力可能已经超出了我的极限,房贷3000,孩子每个月2000,车子和物业1000,还有生活费,每个月花销都要8000,有的时候孩子一生病就可能要一万以上,我后面找到一份工作4500,是切图仔,每天就jQuery,才稍微帮家里分担了一下,其实压力全部都在我妈妈哪儿,我妈妈为了我,操碎了不少心。


2018年我拿到了大专学历,然后随即开始了报名成人高考本科,孩子她妈就没有报名了,她觉得女孩子大专就够了,加上家里也没钱


收获:
1.当父亲了,压力更大了。我必须得成熟一点了,在前端行业算是正式入行了,通过自己每天工作之外,在各种QQ群里聊天拉业务,我的外快收入也逐渐多了起来,虽然很多时候工作到2点,但是总算是跟家庭减少一点压力,虽然期间换了3家工作,但是我的工资也高了一些,月薪到手9000了,加上外快时多时不多,一个月平均有个1.3的收入了;我也有一点点经济带家人去自驾游了(不过只有两次)
2.成人高考本科录取通知书


2018.7-2020.7


这一年通过我经常在QQ群聊天的好友介绍,我到了一家外包公司(他当时也拿了回扣,但是我也很感激他。因为他教我怎么面试,跟我出面试题),因为通过了客户的面试,我厚着脸皮开到了1.6一个月的工资。到手14k,我当时说我拿这么多,家里人都以为骗他们,不过等发第一个月工资的时候,他们觉得我以前选择计算机是对的,我妈妈也多了很多笑容,这个时候小孩也是大了,妈妈一个人带着孩子读幼儿园,我和孩子他妈在重庆上班,我妈也在上班,家庭算是好了起来,大家笑容也多了起来。当时加上的我的外快业务,一年也能赚个6,7万,因为大家知道我在做这块,后面一些朋友陆续的给我介绍,我也会给他们相对满足的回扣。平均一个月收入已经超过2万了,不过有点不厚道的是,我上班没事也在做外包。


收获:自己随着年龄的增加,人的心态也在发生变化,随着收入多了起来,脸上的笑容也多了,家庭矛盾也少了,日子也越来越有奔头了


2020.7-2023.10


2021年因为公司被客户从人力资源池给移除了,我们没有资格做客户的业务了,我随即也面临着失业,我28岁了,其实我还是很恐慌的,因为家庭开支这么大,加上我长期做外包,技术底子很薄弱,可能失业找不到这么高的工资,所以我很担忧,工作随便都是全日制本科起,我一个半罐子学历,能干点啥,但是后面客户对我的技术能力还有做事能力还算比较认可的,给我推荐了另外还在资源池的外包公司,但是我都不去,我觉得外包没有前途,同时他们也开不起16k的工资(虽然技术不咋的,但是现在是这工资,让我转到其他外包公司才13k,我也心有不甘啊),最后客户他们把我转进了客户内部,于是我一个中专生进入了体制内,不过在进入之前,各部长对我的学历还是有一定质疑,不过我的直系领导以自身名誉担保,我还是通过他们的几轮面试,最终成功进入。进入到体制内,身边的同事都是985的博士,研究生,还有都是留学回来的,也有一些清华北大北航的研究生,其实还是很自卑的,大家学历这么高,有的时候不得不承认,他们的专业素养,学术知识,脑回路都比较灵活,他们的英语都非常厉害,有的同事28岁都上中央台了,太强了,我妈妈非常担心我的学历,怕我一在公司一犯错就被开除,其实有的时候她还是多虑了,我也在尽力 的追赶他们,希望差距尽量小一点点。所以在2023年我拿到了非全的研究生录取通知书,继续读软件工程。在公司也申请入了党,因为他们全是党员,我在公司负责两个部门的前端管理工作,也在带一些校招的研究生,同时我也在2022年5月买了第二套房,首付42万,其他非要加上差不多47万,为此把车卖了一万五,凑点首付钱,我妈妈又出了26万,再一次吧我妈给榨干了,这次我爸也没有出一分钱。不过还在我是组合贷,每个月只出商业贷,每个月出差不多2000的房贷,第一套房贷也还有20多万就还完了,2023年我孩子读小学了,我妈妈也来重庆带孩子了,为此她没有继续在做装修了,每天接送小孩子上学放学,中间有两个小时去一家店里打扫卫生,每个月2500的收入。我在每个月给他2000多生活费,虽然她不太适应城市的生活,觉得城市的人会看不起她是农村人,走哪儿都不会用导航,但是她慢慢的还是习惯了,城市的人并没有觉得自己高人一等,她还算是过得比较快乐,现在我的收入在重庆来说还算OK,外快也有,但是我也不想太累了,我想把时间利用在学习上,因为同事们都很强,我尽量向他们看齐,


收获:本科毕-业-证,非全研究生录取通知书,稳定的工作,第二套房子首付+装修(因为旁边学校更好一点)


最后


今年30了,孩子已经7岁了,我已经开始享受十天的年假了。其实我已经算是走了很多路,深夜哭了很多次,第二天依旧怀揣着斗志,我数次回想我这30年的发展,其实都过得不是很灿烂,或许平凡的就是这样,一无所有的农村人,只能靠父母,如果父母靠不住,那自己也开心点,随着父母的年龄越来越大,我的压力也变大了,他们很多时候会征求我的意见,我也要拿钱支持他们了,也有一些感悟:


1.每年还是得有一个目标,细分到每个月,每一周去完成它,如果没有目标,那就认真的把每一件事情尽量做好,贵人每个人都会遇到,只是看能否抓住,可能会是工作中,生活中的某一个,他愿意提携你一下,真的能少走很多弯路,我的经验告诉我,我有两三次都有贵人帮助我,只是我没有把握住,就像之前一起做区块链,一起做电商的公司老板就很喜欢我,因为我比较踏实,没攻击性,人老实。但是我还是太年轻,很多时候做事不够成熟,就这样和机会擦肩而过,他们现在已经是财富自由了


2.与人交流,说话适可而止,充分尊重他人,聊天中尽量带点幽默,学习一下话题的扩展


3.没事多扩展一下人脉,我才开始培训机构出来,基本没学,全靠在QQ群聊天的人带的我,怎么学,我每次遇到问题我都会问他们,他们在远程帮我改bug,这样我才能保住工作


4.多学习,我看了下我现在的同事,他们没事不会在网上划水,而是都在学习,最敬佩的是旁边那位,一年了从小白,到一名技术骨干,技术成长太快了,他除了学习,每天还不断在看书,我只能说佩服,我很多时候都在刷抖音,我自愧不如,我有罪


5.想办法融入更好的圈子,我之前待得公司都不大,都是外包公司,大家学历都很低,没有一个是985或者211的本科生,大家上班都在聊吃喝嫖赌,主要是聊女人那点事。但是现在我发现身边的同事几年了,没有一个人说过一句脏话,说话总会特别舒服,因为你能感受到他非常尊重你,说话也非常温柔,绝不会听到SEX,tm 这种言词。


6.接受自己的平凡。我以前有很多想法,内心很浮躁,后面发现读书越少的人想法越多,到最后越来越差,债务缠身,本来都是资本的牟利工具,平凡开心更好,把家里的事情处理好,生活上逐渐改善品质,就已经很不错了,在平凡的生活多点浪漫,对未来有一点期待,但不浮夸,我觉得就已经很不错了


7.多多提高自己的综合素质吧,我是一个比较随心的人,但是后面发现,穿的邋遢,说话幼稚,身形不行,走在外面都没自信,何况别人会怎么看你呢,这一点我也在慢慢提高


8.最后我的技术其实很一般,node,vue,react,java,python,php,微信小程序,three.js 这些都有做过,有的都是为了外包业务减少点成本才去学的,但是要说哪一个比较深入,可能就前端的这几个框架,因为天天都在做,偶尔看看掘金的技术文档,但是要说特别深入的,抱歉没有,因为我从误打误撞开始进入这个行业,我的目的不是因为喜欢,而是因为工资高一点,我没有想给要为这个行业带来些什么,我只想活着,我现在觉得我没有特别喜欢做的行业,我不清楚我能在这个行业做多少年,但是只要做,我就把它做好,因为做工作的态度跟自己的喜欢没有关系,做事是做人,自己的工作做好了,下个同事才会很轻松。同时也在尽可能的弥补一些自己的软实力。希望在某一天,有更好的机会,自己能抓得住,自己不会为了自己的能力而自卑!


9.2023-12-21 17:28:53 下班了


作者:超级666
来源:juejin.cn/post/7314877697996947482
收起阅读 »

什么,你还不会 vue 表格跨页多选?

web
前言看背景就知道,国服第一薇恩,欢迎组队! 言归正传在我们日常项目开发中,经常会有表格跨页多选的需求,接下来让我们用 el-table 示例一步步来实现这个需求。动手开发在线体验codesandbox.io/s/priceless…常规版本...
继续阅读 »

前言

看背景就知道,国服第一薇恩,欢迎组队! 言归正传

在我们日常项目开发中,经常会有表格跨页多选的需求,接下来让我们用 el-table 示例一步步来实现这个需求。

动手开发

在线体验

codesandbox.io/s/priceless…

常规版本

本部分只写了一些重点代码,心急的彦祖可以直接看 性能进阶版

  1. 首先我们需要初始化一个选中的数组 checkedRows
this.checkedRows = []
  1. 在触发选中的时候,我们就需要把当前行数据 push 到 checkedRows,否则就需要剔除对应行
"multipleTable" @select="handleSelectChange">
handleSelectChange (val, row) {
const checkedIndex = this.checkedRows.findIndex(_row => _row.id === row.id)
if (checkedIndex > -1) {
// 选中剔除
this.checkedRows.splice(checkedIndex, 1)
} else {
// 未选中压入
this.checkedRows.push(row)
}
}
  1. 实现换页的时候的回显逻辑
this.data.forEach(row=>{
const checkedIndex = this.checkedRows.findIndex(_row => _row.id === row.id)
if(checkedIndex>-1) this.$refs.multipleTable.toggleRowSelection(row,true)
})

效果预览

让我们看下此时的效果

2023-08-08 20.03.52.gif

完整代码



<script>
export default {
data () {
return {
currentPage: 1,
checkedRows: [],
pageSize: 10,
totalData: Array.from({ length: 1000 }, (_, index) => {
return {
date: '2016-05-03',
id: index,
name: '王小虎' + index
}
})
}
},
computed: {
tableData () {
const { currentPage, totalData, pageSize } = this
return totalData.slice((currentPage - 1) * pageSize, currentPage * pageSize)
}
},
methods: {
currentChange (page) {
this.currentPage = page
this.tableData.forEach(row => {
const checkedIndex = this.checkedRows.findIndex(_row => _row.id === row.id)
if (checkedIndex > -1) this.$refs.multipleTable.toggleRowSelection(row, true)
})
},
handleSelectChange (val, row) {
const checkedIndex = this.checkedRows.findIndex(_row => _row.id === row.id)
if (checkedIndex > -1) {
this.checkedRows.splice(checkedIndex, 1)
} else {
this.checkedRows.push(row)
}
},
handleSelectAllChange (val) {
this.tableData.forEach(row => {
this.handleSelectChange(null, row)
})
}
}
}
script>

性能进阶版

性能缺陷分析

优秀的彦祖们,应该发现以上代码的性能缺陷了

1.handleSelectChange 需要执行一个 O(n) 复杂度的循环

2.currentChange 的回显逻辑内部, 有一个 O(n^2) 复杂度的循环

想象一下 如果场景中勾选的行数达到了 10000 行, 每页显示 100 条

那么我们每次点击换页 最坏情况就要执行 10000 * 100 次循环,这是件可怕的事...

重新设计数据结构

其实我们没必要把 checkedRows 设计成一个数组

我们可以设计成一个 map,这样读取值就只需要 O(1)复杂度

Object 和 Map 的选择

此时应该有 彦祖会好奇,为什么要搞一个 Map 而不是 Object呢?

其实要弄清楚这个问题,我们必须要知道他们之间的区别,网上的文章非常多,也介绍的非常详细

但有一点,是很多文章没有提及的,那就是 Map 是有序的,Object 是无序的

比如有个需求要获取 第一个选中行,最后一个选中行,那么我们利用 Map 实现就非常简单。

其次 我们可以用 size 方法轻松获取 选中行数量

改造代码

1.改造 checkedRows

this.crossPageMap = new Map()

2.修改选中逻辑(核心代码)

handleSelectChange (val, row) {
// 实现了 O(n) 到 O(1) 的提升
const checked = this.crossPageMap.has(row.id)
if (checked) {
this.crossPageMap.delete(row.id)
} else {
this.crossPageMap.set(row.id, row)
}
}

3.修改换页回显逻辑

currentChange (page) {
this.currentPage = page
// 实现了 O(n^2) 到 O(n) 的提升
this.tableData.forEach(row => {
const checked = this.crossPageMap.has(row.id)
if (checked) this.$refs.multipleTable.toggleRowSelection(row, true)
})
}

完整代码



<script>
export default {
data () {
return {
currentPage: 1,
crossPageMap: new Map(),
pageSize: 10,
totalData: Array.from({ length: 1000 }, (_, index) => {
return {
date: '2016-05-03',
id: index,
name: '王小虎' + index
}
})
}
},
computed: {
tableData () {
const { currentPage, totalData, pageSize } = this
return totalData.slice((currentPage - 1) * pageSize, currentPage * pageSize)
}
},
methods: {
currentChange (page) {
this.currentPage = page
this.tableData.forEach(row => {
const checked = this.crossPageMap.has(row.id)
if (checked) this.$refs.multipleTable.toggleRowSelection(row, true)
})
},
handleSelectChange (val, row) {
const checked = this.crossPageMap.has(row.id)
if (checked) {
this.crossPageMap.delete(row.id)
} else {
this.crossPageMap.set(row.id, row)
}
},
handleSelectAllChange (val) {
this.tableData.forEach(row => {
const isChecked = this.crossPageIns.isChecked(row)
if (val.length === 0) {
// 取消全选 只有选中的需要改变状态
if (isChecked) this.crossPageIns.onRowSelectChange(row)
} else {
// 全选 只有未选中的才需要改变状态
if (!isChecked) this.crossPageIns.onRowSelectChange(row)
}
})
}
}
}
script>

抽象业务逻辑

以上就是完整的业务代码部分,但是为了复用性。

我们考虑可以把其中的逻辑抽象成一个CrossPage

设计 CrossPage 类

接收以下参数

`data` - 行数据
`key` - 行数据唯一值
`max` - 最大选中行数
`toggleRowSelection` - 切换行数据选中/取消选中的方法

提供以下方法

`onRowSelectChange` - 外部点行数据点击的时候调用此方法
`onDataChange` - 外部数据变化的时候调用此方法
`clear` - 清空所有选中行
`isChecked` - 判断当前行是否选中

构造器大致代码 如下

constructor (options={}) {
this.crossPageMap = new Map()
this.key = options.key || 'id'
this.data = options.data || []
this.max = options.max || Number.MAX_SAFE_INTEGER
this.toggleRowSelection = options.toggleRowSelection
if(typeof this.toggleRowSelection !== 'function') throw new Error('toggleRowSelection is not function')
}

设置私有crossPageMap

彦祖们,问题来了,我们把crossPageMap挂载到实例上,那么外部就可以直接访问修改这个变量。

这可能导致我们内部的数据逻辑错乱,所以必须禁止外部访问。

我们可以使用 # 修饰符来实现私有属性,具体参考

developer.mozilla.org/zh-CN/docs/…

完整代码

  • CrossPage.js
/**
* @description 跨页选择
* @param {Object} options
* @param {String} options.key 行数据唯一标识
* @param {Function} options.toggleRowSelection 设置行数据选中/取消选中的方法,必传
*/

export const CrossPage = class {
#crossPageMap = new Map();
constructor (options={}) {
this.key = options.key || 'id'
this.data = options.data || []
this.max = options.max || Number.MAX_SAFE_INTEGER
this.toggleRowSelection = options.toggleRowSelection
if(typeof this.toggleRowSelection !== 'function') throw new Error('toggleRowSelection is not function')
}
get keys(){
return Array.from(this.#crossPageMap.keys())
}
get values(){
return Array.from(this.#crossPageMap.values())
}
get size(){
return this.#crossPageMap.size
}
clear(){
this.#crossPageMap.clear()
this.updateViews()
}
isChecked(row){
return this.#crossPageMap.has(row[this.key])
}
onRowSelectChange (row) {
if(typeof row !== 'object') return console.error('row is not object')
const {key,toggleRowSelection} = this
if(this.isChecked(row)) this.#crossPageMap.delete(row[key])
else {
this.#crossPageMap.set(row[key],row)
if(this.size>this.max){
this.#crossPageMap.delete(row[key])
toggleRowSelection(row,false)
}
}
}
onDataChange(list){
this.data = list
this.updateViews()
}
updateViews(){
const {data,toggleRowSelection,key} = this
data.forEach(row=>{
toggleRowSelection(row,this.isChecked(row))
})
}
}

写在最后

未来想做的还有很多

  •  利用requestIdleCallback 提升单页大量数据的 toggleRowSelection 渲染效率
  •  提供默认选中项的配置
  •  ...

欢迎彦祖们 贡献宝贵代码

个人能力有限 如有不对,欢迎指正🌟 如有帮助,建议小心心大拇指三连🌟

彩蛋

宁波团队还有一个hc, 带你海鲜自助。 欢迎彦祖们私信😚


作者:前端手术刀
来源:juejin.cn/post/7264898713646153780
收起阅读 »

Android 如何统一处理登录后携带数据跳转到目标页面

需求场景 我们在开发应用的时候经常会遇到先登录,登录成功后再跳转到目标页面。比如商品详情页面我们点击购买必须要先登录,登录完成才能去下单支付。针对这种场景,我们一般有两种做法: 点击购买跳转到登录,登录完成需要用户再次点击购买才能去下单支付页面,这种用户体验...
继续阅读 »

需求场景


我们在开发应用的时候经常会遇到先登录,登录成功后再跳转到目标页面。比如商品详情页面我们点击购买必须要先登录,登录完成才能去下单支付。针对这种场景,我们一般有两种做法:



  1. 点击购买跳转到登录,登录完成需要用户再次点击购买才能去下单支付页面,这种用户体验不是很好。

  2. 点击购买跳转到登录,登录完成直接跳转到下单支付页面。


第一种我们就不谈了产品经理不同意🐶。第二种我们一般是在 onActivityResult 里面获取到登录成功,然后根据 code 跳转到目标页面。这种方式缺点就是我们要在每个页面都处理相同的逻辑还有定义各种 code,如果应用里面很多这种场景也太繁琐了。那有没有统一的方式去处理这种场景就是我们今天的主题了。


封装方式


我们的应用是组件化的,APP 的页面跳转使用了 Arouter。所以我们统一处理使用 Arouter 封装。直接上代码


fun checkLoginToTarget(postcard: Postcard) {//Postcard 是 Arouter 的类
if (User.isLogin()) {
postcard.navigation()
} else {
//不能使用 postcard 切换 path 直接跳转,因为 group 可能不同,所以重新 build
ARouter.getInstance().build(Constant.LOGIN)
.with(postcard.extras)//获取携带的参数重新转入
.withString(Constant.TAGACTIVIFY, postcard.path)//添加目标路由
.navigation()
}
}

//登录成功后在登录页面执行这个方法
fun loginSuccess() {
val intent= intent
val target = intent.getStringExtra(Constant.TAGACTIVIFY)//获取目标路由
target?.apply {
if (isNotEmpty()){
val build = ARouter.getInstance().build(this)
val extras = intent.extras//获取携带的参数
if (extras != null) {
build.with(extras)
}
build.navigation()
}
}
finish()
}

代码加了注释,使用 Kotlin 封装了顶层函数,登录页面在登录成功后跳转到目标页面,针对上面的场景直接调用 checkLoginToTarget 方法。


checkLoginToTarget(ARouter.getInstance().build(Constant.PAY_PAGE).withInt(Constant.GOOD_ID,id))

通过 Arouter 传入下单支付的路由地址,并且携带了商品的 ID,生成了 Postcard 参数。登录成功后能带着商品 ID
直接下单支付了。


最后


如果项目里没有使用路由库可以使用 Intent 封装实现,或者别的路由库也可以用上面的方式去做统一处理。


作者:shortybin
来源:juejin.cn/post/7237386183612530749
收起阅读 »

如果启动一个未注册的Activity

简述 要启动未注册的Activity主要是要逃避AMS的检测,思路是,检测前要启动的Activity换成注册的,检测通过了,再在启动前换回来。这里主要是两个点。检测前,hookAMS。检测后hookHandler。hook点有很多尽量找静态变量、单例和publ...
继续阅读 »

简述


要启动未注册的Activity主要是要逃避AMS的检测,思路是,检测前要启动的Activity换成注册的,检测通过了,再在启动前换回来。这里主要是两个点。检测前,hookAMS。检测后hookHandler。hook点有很多尽量找静态变量单例public


hookAMS


1、android 11举例,启动acitivty是在ATMS中(11之前是AMS,这个自己可以去适配)


image.png


2、拿到ATMS的代理。


3、然后ATMS整个动态代理在startActivity之前将Intent 偷梁换柱


4、换成已经注册的Activity之后记得原目标Acitivty存起来,在骗完AMS之后换回来


 
public static void hookAMS() {
// 10之前
try {
Class<?> clazz = Class.forName("android.app.ActivityTaskManager");
Field singletonField = clazz.getDeclaredField("IActivityTaskManagerSingleton");

singletonField.setAccessible(true);
Object singleton = singletonField.get(null);




Class<?> singletonClass = Class.forName("android.util.Singleton");
Field mInstanceField = singletonClass.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
Method getMethod = singletonClass.getMethod("get");
Object mInstance = getMethod.invoke(singleton);

Class IActivityTaskManagerClass = Class.forName("android.app.IActivityTaskManager");

Object mInstanceProxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{IActivityTaskManagerClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

if ("startActivity".equals(method.getName())) {
int index = -1;

// 获取 Intent 参数在 args 数组中的index值
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
index = i;
break;
}
}
// 生成代理proxyIntent -- 孙悟空(代理)的Intent
Intent proxyIntent = new Intent();
// 这个包名是宿主的
proxyIntent.setClassName("com.leo.amsplugin",
ProxyActivity.class.getName());

// 原始Intent能丢掉吗?保存原始的Intent对象
Intent intent = (Intent) args[index];
proxyIntent.putExtra(TARGET_INTENT, intent);

// 使用proxyIntent替换数组中的Intent
args[index] = proxyIntent;
}

// 原来流程
return method.invoke(mInstance, args);
}
});

// 用代理的对象替换系统的对象
mInstanceField.set(singleton, mInstanceProxy);
} catch (Exception e) {
e.printStackTrace();
}
}

hookHandler


hookAMS完成,欺骗了AMS,接下来要把Intent中的原目标扶起回正位,
启动Activity要用handler,我们从这里hook吧


1、Activtiy thread 中的handler用来启动activity class H extends Handler


2、handlerMessage中的EXECUTE_TRANSACTION(159)来启动activity


3、
final ClientTransaction transaction = (ClientTransaction) msg.obj;--包含Intent


mTransactionExecutor.execute(transaction);--执行启动


launchActivityItem中有Intent,而ta继承于ClientTransactionItem,而ClientTransaction中包含List<ClientTransactionItem>


4、所以我只要拿到msg就可以拿到Intent
msg.obj --> ClientTransaction --> List mActivityCallbacks(LaunchActivityItem)
--> private Intent mIntent 替换


image.png


5、handlerMessage(MSG)之前有个callback也可以拿到msg。则会callback是一个接口,如果重写这个接口可就可重新handlerMessage这个方法,然后操作msg。


6、ActivityThread当中,Handler的构建没有传参数。


...//去ActivityThread.java里看
@UnsupportedAppUsage
final H mH = new H();
...
class H extends Handler //也没写构造方法

...//去Handler.java里看

@Deprecated
public Handler() {
this(null, false);
}

7、实际上callback是看,那么我自己替换系统的call就可以啦


8、那我通过反射拿Handler中的mCallback


 public void hoodHandler() {
try {
Class<?> clazz = Class.forName("android.app.ActivityThread");
Field activityThreadField = clazz.getDeclaredField("sCurrentActivityThread");
activityThreadField.setAccessible(true);
Object activityThread = activityThreadField.get(null);

Field mHField = clazz.getDeclaredField("mH");
mHField.setAccessible(true);
final Handler mH = (Handler) mHField.get(activityThread);

Field mCallbackField = Handler.class.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);

mCallbackField.set(mH, new Handler.Callback() {

@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case 159:
// msg.obj = ClientTransaction
try {
// 获取 List<ClientTransactionItem> mActivityCallbacks 对象
Field mActivityCallbacksField = msg.obj.getClass()
.getDeclaredField("mActivityCallbacks");
mActivityCallbacksField.setAccessible(true);
List mActivityCallbacks = (List) mActivityCallbacksField.get(msg.obj);

for (int i = 0; i < mActivityCallbacks.size(); i++) {
// 打印 mActivityCallbacks 的所有item:
//android.app.servertransaction.WindowVisibilityItem
//android.app.servertransaction.LaunchActivityItem

// 如果是 LaunchActivityItem,则获取该类中的 mIntent 值,即 proxyIntent
if (mActivityCallbacks.get(i).getClass().getName()
.equals("android.app.servertransaction.LaunchActivityItem")) {
Object launchActivityItem = mActivityCallbacks.get(i);
Field mIntentField = launchActivityItem.getClass()
.getDeclaredField("mIntent");
mIntentField.setAccessible(true);
Intent proxyIntent = (Intent) mIntentField.get(launchActivityItem);

// 获取启动插件的 Intent,并替换回来
Intent intent = proxyIntent.getParcelableExtra(TARGET_INTENT);
if (intent != null) {
mIntentField.set(launchActivityItem, intent);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
break;
}
return false;
}
});
} catch (Exception e) {
e.printStackTrace();
}

}

总结


一个分为两步


1、hookAMS主要就是逃避ams检测,让ams检测的是一个已经注册了的activity。


2、hookHandler在生成activity之前再把activity换回来。


所以一定要熟悉动态代理,反射和Activity的启动流程。


主要通过hook,核心在于hook点


插桩
1、尽量找 静态变量 单利
2、public


动态代理


AMS检测之前我改下


image.png


作者:KentWang
来源:juejin.cn/post/7243272599769055292
收起阅读 »

Android ReyclerView分割线竟然暗藏算法

前言 事情是这样的,前段时间正好有个RecyclerView用GridLayoutManager实现网格布局的需求,然后要做分割线,一般这种都是信手捏来的东西,然后我发现这个分割线竟然对不齐。 当然,如果要实现这样的功能,会有很多种方法,包括在itemView...
继续阅读 »

前言


事情是这样的,前段时间正好有个RecyclerView用GridLayoutManager实现网格布局的需求,然后要做分割线,一般这种都是信手捏来的东西,然后我发现这个分割线竟然对不齐。

当然,如果要实现这样的功能,会有很多种方法,包括在itemView加margin、padding等等,都能有办法去实现分割线的效果,但是我这种人就是非要弄清楚其中的问题才舒服。


结论


因为涉及到算法,可能要讲得比较多,所以先说说最终的结论,先看看效果


image.png


就是实现这种有分割线并均分布局的效果,我研究到最后发现竟然不是简单一两句代码能解决的,其中还暗藏玄机。这里我处理这个问题会涉及一个算法,所以最终会得到一个公式,我不能保证我的公式是最优的解法,如果有其它更好的公式也可以留言告诉我。


1. 简单的处理分割线


我这里的场景是ItemView是填充,意思就是填充除了分割线以外的布局。


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/purple_200"
android:orientation="vertical">

</LinearLayout>

假如我一开始要做分割线,我简单的去做,会是这样的效果


rv.addItemDecoration(object : ItemDecoration() {

override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
)
{
outRect.left = 60
}
})


image.png


然后你会很自然而然的想这个做


rv.addItemDecoration(object : ItemDecoration() {

override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
)
{
val pos = parent.getChildAdapterPosition(view)
if (pos != 0) {
outRect.left = 60
}
}
})


然后你会发现此时的布局不均分,第一个item更多


image.png


注意,我这里的处理问题思路是必须用分割线处理,不然用一些方法确实能更快做到,比如上面的情况,我加个padding也能做,但我这里的思路是要完全用outRect去处理这个问题


看到上面的效果和想象中的不同,没关系,我换个思路,我左右都加间距


rv.addItemDecoration(object : ItemDecoration() {

override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
)
{
val pos = parent.getChildAdapterPosition(view)
outRect.left = 30
outRect.right = 30
}
})

image.png


可以看到是均分了,但是如果我的场景需要首尾两个item贴边,那这样就不合适,但是你可能会很快的想到这样做,去判断首尾Item


image.png


恭喜你,又失败了,可以看到布局又不是均分了。如果你一直按照这样的简单思路去想,是无法处理这个问题的,因为他不是一个简单的公式就能解决的,所以简单的去思考,也只是浪费时间。


首先需要的是理解他的原理


2. 设置分割线getItemOffsets方法的原理


这里简单讲,不是看源码,而是通过图片去分析(我就简单画点图,可能不是很标准,将就着看)


image.png


红色是内容,白色是间距,如果不设置的话,红色的区域就是整个白色,可以抽象的理解成它是往内去缩的,所以如果第一个Item不设置Left,最后一个Item不设置Right,他的效果就会是这样


image.png


这就是上面Demo的最后一种情况,这里给你们看一个很有意思的现象,假如我的代码这样写(在3列的情况下)


rv.addItemDecoration(object : ItemDecoration() {

override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
)
{
val pos = parent.getChildAdapterPosition(view)
if (pos == 0) {
outRect.right = 40
} else if (pos == 2) {
outRect.left = 40
} else {
outRect.left = 20
outRect.right = 20
}
}
})


image.png


可以看到这样就均分了,是不是很神奇,其实这里用图来画出来是这样的


image.png


间距是由一个Item更大的间距加上一个Item略小的间距实现的。


你可能会想,懂了,除了首尾之外,其他的就是填一半间距。真的这么简单吗,可以看看4个效果,同样的代码如果把列数从3个变成4个


image.png


你就会发现,中间的分割线会更小一点,你可以算算看,左边的分割线是 40 + 20,而中间的分割线是 20 + 20 ,所以不同。 所以我说这个问题不会这个简单


其实当时我处理不了又比较赶时间,我就去google查,找了几个老哥的代码直接拷贝下来用发现用不了,所以我看深入去思考这个问题。


3. 真正的实现分割线均分布局的操作


来了,重点来了,通过上面的原理你能知道,其实就是把首尾两个Item应该多出的间距,平均分配到每个分割线。但是它不会是一个简单的计算,会是一个偏复杂的问题,数据问题。


当我把他变成数学问题,这个问题就是,我给出固定的分割线宽度,你需要分割线宽度相同,Item的宽度也相同,注意是两个相同,这是一个解题的条件


这个问题如果从正向去解释,我觉得很难说清楚,所以我从反向来解答,假如我有10列(我这里为了方便,先用一行来举例


image.png


图画得不太准,因为准的不好手动画,假设看成间距和Item宽度都相同。我10列那就是有9个间距(9个分割线),假设每条间距是10


那我是不是可以这样分:


间距1:(L1)9 + (R1)1

间距2:(L2)8 + (R2)2

间距3:(L3)7 + (R3)3

间距4:(L4)6 + (R4)4

间距5:(L5)5 + (R5)5

间距6:(L6)4 + (R6)6

间距7:(L7)3 + (R7)7

间距8:(L8)2 + (R8)8

间距9:(L9)1 + (R9)9


他们的间距都不同,但是他们加起来都是10,这是满足了第一个条件,分割线间距相同,还有一个条件,他们的Item宽要相同


从上面的原理我们知道,Item的最终宽度就是总宽度减去左右间距的宽度。Item1的左间距是0,右间距是9,它的宽度是AllWeidth - 9 ,Item2的左间距是1,右间距是8,它的宽度是AllWeidth - (8+1),和Item1是相同的,你可以算算其他的,也是相同的。所以这样就能达到一个均分的效果。


OK,我们来凑公式。上面说过,其实这种场景就相当于10个分割线的间距,把其中一个间距分成每一份去加到其他的间距中,而每一份其实就是最小的份,你看看上面的10列,每一份就是1,所以得出一个公式


min = space / n


然后有了最小,我们还需要算出一个最大的Item的间距,从间距相等我们得知


max = space - min


等理解这两个公式之后,我们再往下看。假设我就拿前面2个Item做分析


L1 = 0 // 最左边的Item没左间距这个应该很容易理解吧

R1 = max // 从上面的模型你能看出,Item1的右间距是最大间距

L2 = space - R1 // 根据间距相等这个条件,R1确认了,L2自然就确认

R2 = max - L2 // 这个是什么意思呢,这个是保证Item的宽度相同,从这个条件根据L2来算出R2。简单来说就是根据第一个Item你知道总间距,你后面的Item也要根据左右间距加起来得到的总间距相等


后一个值要根据前一个值的结果算出,是不是很熟悉,介不是某大厂特别喜欢考的动态规划吗?我见过直接算法题的动态规划,倒是第一次见结合到代码场景里面的,没想到一个小小的RecyclerView能玩这么花。


动态规划,老熟人了,我们能根据上面的分析推出一个公式


        rv.addItemDecoration(object : ItemDecoration() {

override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
val pos = parent.getChildAdapterPosition(view)

val min : Float = space / n
val max = space - min

if (pos == 0) {
outRect.right = max.toInt()
} else if (pos == (n - 1)) {
outRect.left = max.toInt()
} else {
var index = 1
var oldLeft = 0
var oldRight = max
while (index <= pos) {
val left = space - oldRight
val right = max - left
oldLeft = left.toInt()
oldRight = right
index++
}
outRect.left = oldLeft
outRect.right = oldRight.toInt()
}

}

})


这里的pos == 0这些判断是在我只有1行的前提下才这么演示的,实际别这么写。

现在分析下代码,space是间距宽度,n是列数,min和max上面分析过了,pos == 0只有右边间距并且为max,pos是最后一个只有左边间距并且为max,这个就不用解释了,主要是最后的else


当前的Item的间距需要根据前一个Item的间距算出,所以这里我用了循环,holdLef和oldRight表示前一个Item的左间距和右间距。然后就是用我们推出的公式去计算


Ln = space - R(n-1)

Rn = max - Ln


可以看看效果


image.png


image.png


image.png


image.png


image.png


可以看到是均分的啦。


优化


本来不想说pos == 0这个判断的,我怕有人直接拉代码出问题说我。上面的代码pos == 0只是为了方便演示1行的情况,如果我在多行用


image.png


所以正常使用判断要改下



if (pos % n == 0) {
outRect.right = max.toInt()
} else if ((pos + 1) % n == 0) {
outRect.left = max.toInt()
} else {
var index = 1
var oldLeft = 0
var oldRight = max
while (index <= (pos % n) ) {
val left = space - oldRight
val right = max - left
oldLeft = left.toInt()
oldRight = right
index++
}
outRect.left = oldLeft
outRect.right = oldRight.toInt()
}


size为10,n为5


image.png


size为8,n为3


image.png


除此之外,还可以看出这个算法的复杂度是O(m*n)


因为getItemOffsets是一个循环,里面的while又是一个循环,所以这里可以优化,我有一个想法,可以用hashmap通过空间来换时间,而且你会发现超过n/2的Item都是之前反着的,所以用hashmap的话你只需要记录第一个行的一半Item的间距,我觉得还是很不错的


还要注意一点,计算时要用Float,最后再转Int,否则全程用Int算可能有点偏差


总结


首先写这篇文章的目的是觉得这其中的算法非常有意思,这个动态规划的过程要推导出这个公式,整个推导的过程能在这其中感受到开发的快乐,所以记住这个公式


L0 = 0

R0 = max

Ln = space - R(n-1)

Rn = max - Ln


其次,我也不敢保证我这个是最佳的解法、最佳的公式,但是我测试目前来看是没问题,所以想用的话可以直接把代码拷去用,当然通过其他的方式也是能处理的,不一定要把思维限制在必须使用ItemDecoration去实现。


解算法的过程是痛苦的,但是解出来之后,那就非常的爽


作者:流浪汉kylin
来源:juejin.cn/post/7314142205684776998
收起阅读 »

20行js就能实现逐字显示效果???-打字机效果

web
效果演示 横版 竖版 思路分析 可以看到文字是一段一段的并且独占一行,使用段落标签p表示一行 一段文字内,字是一个一个显示的,所以这里每一个字都用一个span标签装起来 每一个字都是从透明到不透明的过渡效果,使用css3的过渡属性transition让每...
继续阅读 »

效果演示


横版


原生JavaScript实现逐字显示效果(打字机效果)插图


竖版


原生JavaScript实现逐字显示效果(打字机效果)插图1


思路分析



  1. 可以看到文字是一段一段的并且独占一行,使用段落标签p表示一行

  2. 一段文字内,字是一个一个显示的,所以这里每一个字都用一个span标签装起来

  3. 每一个字都是从透明到不透明的过渡效果,使用css3的过渡属性transition让每个字都从透明过渡到不透明


基本结构


HTML基本结构


<div id="container"></div>

这里只需要一个容器,其他的结构通过js动态生成


CSS


#container {
/* 添加这行样式=>文字纵向从右往左显示 */
/* 目前先不设置,后面可以取消注释 */
/* writing-mode: vertical-rl; */
}
#container span {
/* 这里opacity先设置为1,让其不透明,可以看到每一步的效果 */
/* 写完js之后到回来改为0 */
opacity: 1;
transition: opacity 0.5s;
}

文本数据


const data = ['清明时节闹坤坤,', '路上行人梳中分;', '借问荔枝何处有,', '苏珊遥指蔡徐村。']

使用数组存放文本数据,一个元素代表一段文字


创建p标签


使用for/of循环遍历数组创建对应个数的p标签,添加到html页面中


const data = ['清明时节闹坤坤,', '路上行人梳中分;', '借问荔枝何处有,', '苏珊遥指蔡徐村。']
// 获取dom元素
const container = document.querySelector('#container')
// for/of循环遍历数组
for (const item of data) {
// 打印每一个item => 数组的每一个元素
console.log(item)
// 创建p标签
const p = document.createElement('p')
// 将p标签插入到container
container.append(p)
}

item代表数组的每一个元素,也就是每一段文字,所以会创建4个p标签


原生JavaScript实现逐字显示效果(打字机效果)插图2


原生JavaScript实现逐字显示效果(打字机效果)插图3


与数组元素数量对应的p标签就生成好了


接下来就是将每一个元素里面的文本添加到span标签中


创建span标签


为每一个字创建一个span标签,然后让span标签的内容等于对应的字,再将每一个生成的span插入到p标签


本节代码


// 遍历item的每一个字
for (let i = 0; i < item.length; i++) {
// 创建span
let span = document.createElement('span')
// span的内容等于item的每一个字
span.innerHTML = item[i]
// 将span插入到p标签中
p.append(span)
}

合并后代码


const data = ['清明时节闹坤坤,', '路上行人梳中分;', '借问荔枝何处有,', '苏珊遥指蔡徐村。']
// 获取dom元素
const container = document.querySelector('#container')
// for/of循环遍历数组
for (const item of data) {
// 打印每一个item => 数组的每一个元素
console.log(item)
// 创建p标签
const p = document.createElement('p')
// 遍历item的每一个字
for (let i = 0; i < item.length; i++) {
// 创建span
let span = document.createElement('span')
// span的内容等于item的每一个字
span.innerHTML = item[i]
// 将span插入到p标签中
p.append(span)
}
// 将p标签插入到container
container.append(p)
}

原生JavaScript实现逐字显示效果(打字机效果)插图4


此时已经完成了渲染数组,并将数组的每一个元素的文字渲染到单独的span中


接下来就要让每一个文字做到从看不见到看的见的效果


添加透明度过渡效果


将css样式中的opacity由1改为0


因为每个字的出现时间不一样,所以不能直接在循环的时候直接添加过渡效果,添加以下代码,让span标签在添加到p标签前也添加到新数组中


const arr = []
// 将span也添加到新数组中
arr.push(span)

最后遍历arr数组,为每一个元素添加一个过渡延迟效果


// 延时1毫秒等待上方循环渲染完成
setTimeout(() => {
// 遍历arr数组的每一个元素
arr.forEach((item, index) => {
// 给每一个元素添加过渡延迟属性
item.style.transitionDelay = `${index * 0.2}s`
// 将透明度设置为不透明
item.style.opacity = 1
})
}, 1)

最后的最后将css样式中的opacity改为0,让所有的字透明


#container span {
opacity: 0;
transition: opacity 0.5s;
}

完整js代码


const data = ['清明时节闹坤坤,', '路上行人梳中分;', '借问荔枝何处有,', '苏珊遥指蔡徐村。']
const arr = []
// 获取dom元素
const container = document.querySelector('#container')
// for/of循环遍历数组
for (const item of data) {
// 打印每一个item => 数组的每一个元素
console.log(item)
// 创建p标签
const p = document.createElement('p')
// 遍历item的每一个字
for (let i = 0; i < item.length; i++) {
// 创建span
let span = document.createElement('span')
// span的内容等于item的每一个字
span.innerHTML = item[i]
// 将span插入到p标签中
p.append(span)
// 将span也添加到新数组中
arr.push(span)
}
// 将p标签插入到container
container.append(p)
}
// 延时1毫秒等待上方循环渲染完成
setTimeout(() => {
// 遍历arr数组的每一个元素
arr.forEach((item, index) => {
// 给每一个元素添加过渡延迟属性
// 让每一个字都比前一个字延时0.2秒的时间
item.style.transitionDelay = `${index * 0.2}s`
// 将透明度设置为不透明
item.style.opacity = 1
})
}, 1)

至此,已经完成了逐字显示的效果,最后介绍一个css属性


writing-mode


使用这个属性可以改变文字方向,实现纵向从左往右或从右往左显示


以下摘自mdn文档


writing-mode 属性定义了文本水平或垂直排布以及在块级元素中文本的行进方向。为整个文档设置该属性时,应在根元素上设置它(对于 HTML 文档,应该在 html 元素上设置)


horizontal-tb

对于左对齐(ltr)文本,内容从左到右水平流动。对于右对齐(rtl)文本,内容从右到左水平流动。下一水平行位于上一行下方。


vertical-rl

对于左对齐(ltr)文本,内容从上到下垂直流动,下一垂直行位于上一行左侧。对于右对齐(rtl)文本,内容从下到上垂直流动,下一垂直行位于上一行右侧。


vertical-lr

对于左对齐(ltr)文本,内容从上到下垂直流动,下一垂直行位于上一行右侧。对于右对齐(rtl)文本,内容从下到上垂直流动,下一垂直行位于上一行左侧。


作者:AiYu
来源:juejin.cn/post/7271165389692960828
收起阅读 »

前端使用a链接下载内容增加loading效果

web
问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。代码如下:// utils.js const XLSX = require('xlsx') // 将一个sheet转成最终的ex...
继续阅读 »
  1. 问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。
  2. 代码如下:
// utils.js
const XLSX = require('xlsx')
// 将一个sheet转成最终的excel文件的blob对象,然后利用URL.createObjectURL下载
export const sheet2blob = (sheet, sheetName) => {
sheetName = sheetName || 'sheet1'
var workbook = {
SheetNames: [sheetName],
Sheets: {}
}
workbook.Sheets[sheetName] = sheet
// 生成excel的配置项
var wopts = {
bookType: 'xlsx', // 要生成的文件类型
bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
type: 'binary'
}
var wbout = XLSX.write(workbook, wopts)
var blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' })
// 字符串转ArrayBuffer
function s2ab(s) {
var buf = new ArrayBuffer(s.length)
var view = new Uint8Array(buf)
for (var i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
return buf
}
return blob
}

/**
* 通用的打开下载对话框方法,没有测试过具体兼容性
* @param url 下载地址,也可以是一个blob对象,必选
* @param saveName 保存文件名,可选
*/

export const openDownloadDialog = (url, saveName) => {
if (typeof url === 'object' && url instanceof Blob) {
url = URL.createObjectURL(url) // 创建blob地址
}
var aLink = document.createElement('a')
aLink.href = url
aLink.download = saveName + '.xlsx' || '1.xlsx' // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
var event
if (window.MouseEvent) event = new MouseEvent('click')
else {
event = document.createEvent('MouseEvents')
event.initMouseEvent(
'click',
true,
false,
window,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null
)
}
aLink.dispatchEvent(event)
}

"clickExportBtn"
>
<i class="el-icon-download">i>下载数据

<div class="mongolia" v-if="loadingSummaryData">
<el-icon class="el-icon-loading loading-icon">
<Loading />
el-icon>
<p>loading...p>
div>

clickExportBtn: _.throttle(async function() {
const downloadDatas = []
const summaryDataForDownloads = this.optimizeHPPCDownload(this.summaryDataForDownloads)
summaryDataForDownloads.map(summaryItem =>
downloadDatas.push(this.parseSummaryDataToBlobData(summaryItem))
)
// donwloadDatas 数组是一个三维数组,而 json2sheet 需要的数据是一个二维数组
this.loadingSummaryData = true
const downloadBlob = aoa2sheet(downloadDatas.flat(1))
openDownloadDialog(downloadBlob, `${this.testItem}报告数据`)
this.loadingSummaryData = false
}, 2000),

// css
.mongolia {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5rem;
color: #409eff;
z-index: 9999;
}
.loading-icon {
color: #409eff;
font-size: 32px;
}
  1. 解决方案探究:
  • 在尝试了使用 $nextTick、将 openDownloadDialog 改写成 Promise 异步函数,或者使用 async/await、在 openDownloadDialog 中添加 loadingSummaryData 逻辑,发现依旧无法解决问题,因此怀疑是 document 添加新元素与 vue 的 v-if 渲染产生冲突,即 document 添加新元素会阻塞 v-if 的执性。查阅资料发现,问题可能有以下几种:

    • openDownloadDialog 在执行过程中执行了较为耗时的同步操作,阻塞了主线程,导致了页面渲染的停滞。
    • openDownloadDialog 的 click 事件出发逻辑存在问题,阻塞了事件循环(Event Loop)。
    • 浏览器在执行 openDownloadDialog 时,将其脚本任务的优先级设置得较高,导致占用主线程时间片,推迟了其他渲染任务。
    • Vue 的批量更新策略导致了 v-if 内容的显示被延迟。
  • 查阅资料后找到了如下几种方案:

      1. 使用 setTimeout 使 openDownloadDialog 异步执行
      clickExport() {
      this.loadingSummaryData = true;

      setTimeout(() => {
      openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

      this.loadingSummaryData = false;
      });
      }
      1. 对 openDownloadDialog 内部进行优化
      • 避免大循环或递归逻辑
      • 将计算工作分批进行
      • 使用 Web Worker 隔离耗时任务
        • 在编写 downloadWorker.js 中的代码时,要明确这部分代码是运行在一个独立的 Worker 线程内部,而不是主线程中。

            1. 不要直接依赖或者访问主线程的全局对象,比如 window、document 等。这些在 Worker 内都无法直接使用。
            1. 不要依赖 DOM 操作,比如获取某个 DOM 元素。Worker 线程无法访问页面的 DOM。
            1. 代码执行的入口是 onmessage 回调函数,在其中编写业务逻辑。
            1. 和主线程的通信只能通过 postMessage 和 onmessage 发送消息事件。
            1. 代码应该是自包含的,不依赖外部变量或状态。
            1. 可以导入其他脚本依赖,比如用 import 引入工具函数等。
            1. 避免修改或依赖全局作用域,比如定义全局变量等。
        • 总之,编写 downloadWorker.js 时要注意它执行在一个独立环境中,只能通过消息事件与主线程通信。

          // 创建 Worker
          const worker = new Worker('downloadWorker.js');

          // 点击下载时向 Worker 发送消息
          function clickDownload() {

          showLoading();

          worker.postMessage({
          url: fileURL,
          filename: 'report.xlsx'
          });

          worker.onmessage = function(e) {
          // 收到下载完成的消息
          hideLoading();
          }

          }

          // 显示 loading
          function showLoading() {
          loading.style.display = 'block';
          }

          // 隐藏 loading
          function hideLoading() {
          loading.style.display = 'none';
          }

          // downloadWorker.js

          onmessage = function(e) {
          const url = e.data.url;
          const filename = e.data.filename;

          // 创建并点击链接触发下载
          const a = document.createElement('a');
          a.href = url;
          a.download = filename;
          a.click();

          postMessage('下载完成');
          }

          id="loading" style="display:none;">
          Downloading...





      1. 使用 requestIdleCallback 进行调度
      clickExport() {
      this.loadingSummaryData = true;

      requestIdleCallback(() => {
      openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

      this.loadingSummaryData = false;
      });
      }
    • 当使用上述方案时发现里面的内容变得异常的慢,于是去问GPT后,得到以下答案:

      使用 requestIdleCallback 来执行 openDownloadDialog 确实可能会需要更多时间。

      因为 requestIdleCallback 会在主线程空闲时才执行回调函数,所以不像 setTimeout 可以直接按指定时间异步执行。

      requestIdleCallback 的优先级较低,只在主线程空闲时才会执行,所以 download 的速度会比 setTimeout 慢一些。

      但使用 requestIdleCallback 的优点是它不会占用主线程时间,可以避免阻塞页面渲染。

      所以需要权衡执行速度和避免阻塞之间的关系:

      • 如果下载速度是优先考虑的因素,可以继续使用 setTimeout 异步执行。
      • 如果避免阻塞渲染更重要,则 requestIdleCallback 是更好的选择。

      偶尔下载任务被延迟不是大问题,但频繁的下载任务阻塞会严重影响页面体验。

      此外,可以结合两者试试:

      clickExport() {

      this.loadingSummaryData = true;

      setTimeout(() => {

      requestIdleCallback(() => {
      openDownloadDialog(downloadBlob);
      });

      this.loadingSummaryData = false;

      }, 200);

      }

      setTimeout 先异步调度,然后在回调中使用 requestIdleCallback 避免阻塞。这样就可以兼顾下载速度和避免渲染阻塞了。

      1. 分析线程模型,找到具体的阻塞点
      • 使用 Performance 工具分析线程
      • debugger 及 console.log 打印关键函数的执行时间
      • 检查是否有非主线程的任务阻塞了主线程
      1. 调整 vue 的批量更新策略
      new Vue({
      config: {
      // 修改批量更新的队列长度
      batchUpdateDuration: 100
      }
      })


作者:李卫泽
来源:juejin.cn/post/7268050036474609683
收起阅读 »

实现丝滑的无缝滚动轮播图

web
一. 目标效果 二. 实现思路 使用Animate API或者CSS动画的方式都可以,我选择的是Animate API。 实现无缝滚动的一般思路 Translate位移+无限循环动画。但是这样会有一个小问题,就是在动画结束又开始的一瞬间会闪烁一下,不是很完美...
继续阅读 »

一. 目标效果


2023-12-15-13-59-18.gif


二. 实现思路


使用Animate API或者CSS动画的方式都可以,我选择的是Animate API。


实现无缝滚动的一般思路


Translate位移+无限循环动画。但是这样会有一个小问题,就是在动画结束又开始的一瞬间会闪烁一下,不是很完美。


解决方法


复制一份数据, 原来的1份数据变成2份数据。然后动画的关键帧设置位移的终点为50%,这样每次动画的结束帧就在数据的中间位置, 注意如果数据之间有间距的话,还要加上间距的一半。这样即可实现无限滚动,并且足够丝滑。3


三. 实现


核心代码


以下代码示例都使用React框架。


  // 使用Web animate Api 添加动画
useEffect(() => {
if (!container.current) return;
// 获取gap值
const gap = getComputedStyle(container.current).gap.split('px')[0] ?? 0;

// 滚动容器(container)的50%宽度 + 滚动容器的50%gap值
// 如果不加滚动容器的50%gap值, 在动画结束又开始的瞬间会跳一下
const translateX = container.current.clientWidth / 2 + Number(gap) / 2;
if (isNaN(translateX)) {
throw new Error('translateX is NaN!');
}

// 定义关键帧, 执行动画
let keyframes: Keyframe[] = [];
if (type === 'rtl') {
keyframes = [
{
transform: 'translateX(0)',
},
{
transform: `translateX(-${translateX}px)`,
},
];
} else if (type === 'ltr') {
keyframes = [
{
transform: `translateX(-${translateX}px)`,
},
{
transform: 'translateX(0)',
},
];
}

animation.current = container.current.animate(keyframes, {
duration,
easing: 'linear',
iterations: Infinity,
});
}, []);

return (
// 使用context传递store和dispatch
<SwiperContext.Provider value={{ store, dispatch }}>
<div className={classNames(['w-full overflow-x-hidden', wrapperClassName])}>
{/* 使用inline-flex代替flex,让ul的宽度被子元素撑开 */}
<ul
className={classNames(['inline-flex flex-nowrap gap-5', className])}
style={style}
ref={container}
>

{/* 类似于HOC的效果, 为Item组件添加_key */}
{children.map((child) =>
cloneElement(child as ReactElement, {
_key: (child as ReactElement).key ?? '',
})
)}

{/* 实现无缝滚动, 复制一组子元素进行占位 */}
{children.map((child) =>
cloneElement(child as ReactElement, {
_key: (child as ReactElement).key ?? '',
})
)}
</ul>
</div>
</SwiperContext.Provider>

);

其中type只是为了区分在x轴上的滚动方向而已,根据方向应用不同的动画。动画的实例使用useRef()去保存。
方便后续调用此动画实例进行动画的暂停和播放。


完整代码


SwiperBox.tsx


import { useHover } from 'ahooks';
import classNames from 'classnames';
import { isNaN, isUndefined } from 'lodash-es';
import {
cloneElement,
CSSProperties,
ReactElement,
ReactNode,
useContext,
useEffect,
useRef,
} from 'react';

import { SwiperContext } from './swiper-context';
import useSwiperReducer, { SwiperActions } from './use-swiper-reducer';

interface SwiperBoxProp {
/**
* 轮播方向
*
* @type {('ltr' | 'rtl')}
* @memberOf SwiperBoxProp
*/

type: 'ltr' | 'rtl';

/**
* 子节点
*
* @type {ReactNode[]}
* @memberOf SwiperBoxProp
*/

children: ReactNode[];

/**
* 类名
*
* @type {string}
* @memberOf SwiperBoxProp
*/

className?: string;

/**
* 外层节点类名
*
* @type {string}
* @memberOf SwiperBoxProp
*/

wrapperClassName?: string;

/**
* 节点样式
*
* @type {CSSProperties}
* @memberOf SwiperBoxProp
*/

style?: CSSProperties;

/**
* 动画持续时间
*
* @type {EffectTiming['duration']}
* @memberOf SwiperBoxProp
*/

duration?: EffectTiming['duration'];

/**
* 鼠标悬停时触发
* @type {boolean} isHovering 是否悬停
* @type {string} key 节点key
*
* @memberOf SwiperBoxProp
*/

hoverOnChange?: (isHovering: boolean, key: string) => void;
}

/**
* 无限循环、无缝轮播组件
* 使用这个组件必须通过gap的方式(eg: gap-4)来设置滚动项之间的距离, 不能使用margin的方式, 不然无缝滚动会有问题
*/

function SwiperBox(prop: SwiperBoxProp) {
const {
type,
className,
wrapperClassName,
style,
children,
duration = 3000,
hoverOnChange,
} = prop;
const [store, dispatch] = useSwiperReducer();
const { activeKey } = store;
// 滚动容器
const container = useRef<HTMLUListElement>(null);
// 动画实例
const animation = useRef<Animation | null>(null);

// activeKey改变时通知外部组件
useEffect(() => {
hoverOnChange &&
!!Object.keys(activeKey).length &&
hoverOnChange(activeKey.isHovering, activeKey.key);
}, [activeKey]);

// 获取所有的key值并存储
useEffect(() => {
dispatch(
SwiperActions.updateKeys(children.map((child) => (child as ReactElement).key ?? ''))
);
}, []);

// 使用Web animate Api 添加动画
useEffect(() => {
if (!container.current) return;
// 获取gap值
const gap = getComputedStyle(container.current).gap.split('px')[0] ?? 0;

// 滚动容器(container)的50%宽度 + 滚动容器的50%gap值
// 如果不加滚动容器的50%gap值, 在动画结束又开始的瞬间会跳一下
const translateX = container.current.clientWidth / 2 + Number(gap) / 2;
if (isNaN(translateX)) {
throw new Error('translateX is NaN!');
}

// 定义关键帧, 执行动画
let keyframes: Keyframe[] = [];
if (type === 'rtl') {
keyframes = [
{
transform: 'translateX(0)',
},
{
transform: `translateX(-${translateX}px)`,
},
];
} else if (type === 'ltr') {
keyframes = [
{
transform: `translateX(-${translateX}px)`,
},
{
transform: 'translateX(0)',
},
];
}

animation.current = container.current.animate(keyframes, {
duration,
easing: 'linear',
iterations: Infinity,
});
}, []);

// 鼠标移入动画暂停/播放
useEffect(() => {
if (!animation.current) return;
if (isUndefined(activeKey.isHovering)) return;

if (activeKey.isHovering) {
animation.current.pause();
} else {
animation.current.play();
}
}, [activeKey]);

return (
// 使用context传递store和dispatch
<SwiperContext.Provider value={{ store, dispatch }}>
<div className={classNames(['w-full overflow-x-hidden', wrapperClassName])}>
{/* 使用inline-flex代替flex,让ul的宽度被子元素撑开 */}
<ul
className={classNames(['inline-flex flex-nowrap gap-5', className])}
style={style}
ref={container}
>

{/* 类似于HOC的效果, 为Item组件添加_key */}
{children.map((child) =>
cloneElement(child as ReactElement, {
_key: (child as ReactElement).key ?? '',
})
)}

{/* 实现无缝滚动, 复制一组子元素进行占位 */}
{children.map((child) =>
cloneElement(child as ReactElement, {
_key: (child as ReactElement).key ?? '',
})
)}
</ul>
</div>
</SwiperContext.Provider>

);
}

interface SwiperBoxItemProp {
children: ReactNode;
// 唯一标识, React不会将key转发到组件中, 因此自定义一个唯一的_key
_key?: string;
}

function SwiperBoxItem(prop: SwiperBoxItemProp) {
const { children, _key } = prop;

const container = useRef<HTMLLIElement>(null);
const context = useContext(SwiperContext);

// 鼠标hover
const onEnter = () => {
context && _key && context.dispatch(SwiperActions.onEnter(true, _key));
};

// 鼠标退出hover
const onLeave = () => {
context && _key && context.dispatch(SwiperActions.onLeave(false, _key));
};

useHover(container, {
onEnter,
onLeave,
});

return (
<li
ref={container}
className="transition-transform duration-500 ease-out hover:scale-105"
>

{children}
</li>

);
}

const SwiperWithAnimation = {
Box: SwiperBox,
Item: SwiperBoxItem,
};

export default SwiperWithAnimation;


swiper-context.ts


import { createContext, Dispatch } from 'react';

import { SwiperAction, SwiperState } from './use-swiper-reducer';

export type SwiperContextType = {
store: SwiperState;
dispatch: Dispatch<SwiperAction>;
};
export const SwiperContext = createContext<SwiperContextType | null>(null);


useSwiperReducer.ts


import { useReducer } from 'react';

export interface SwiperState {
activeKey: { isHovering: boolean; key: string };
totalKeys: string[];
}

export type SwiperAction<T = any> = {
type: string;
payload: T;
};

export const SwiperActions = {
onEnter: (isHovering: boolean, key: string) => ({
type: 'onEnter',
payload: { isHovering, key },
}),
onLeave: (isHovering: boolean, key: string) => ({
type: 'onLeave',
payload: { isHovering, key },
}),
updateKeys: (keys: string[]) => ({
type: 'update_keys',
payload: keys,
}),
};

export default function useSwiperReducer() {
const initialState: SwiperState = {
activeKey: {} as SwiperState['activeKey'],
totalKeys: [] as SwiperState['totalKeys'],
};

const reducer = (store: SwiperState, { type, payload }: SwiperAction): SwiperState => {
switch (type) {
case 'onEnter':
return {
...store,
activeKey: payload,
};
case 'onLeave':
return {
...store,
activeKey: payload,
};
case 'update_keys':
return {
...store,
totalKeys: payload,
};
default:
return store;
}
};

const [store, dispatch] = useReducer(reducer, initialState);

return [store, dispatch] as const;
}


四、如何使用


import SwiperWithAnimation from '@/components/swiper-box/SwiperBox';
import { uniqueId } from 'lodash-es';


const DATA = new Array(2).fill(0).map(() => uniqueId('data'));
/**
* 测试页面
*/

export default function TestPage() {
// 鼠标hover事件
const hoverOnChange = (isHovering: boolean, key: string) => {
console.log('isHovering: ', isHovering);
console.log('key: ', key);
};

return (
<div>
<SwiperWithAnimation.Box
type="ltr"
wrapperClassName="py-9 m-auto !w-[600px] border border-red-200"
className="gap-8"
hoverOnChange={hoverOnChange}
>

{DATA.map((data) => (
<SwiperWithAnimation.Item key={data}>
<div className="f-c h-[300px] w-[300px] rounded-lg bg-theme-primary">
<div className="text-2xl">{data}</div>
</div>
</SwiperWithAnimation.Item>
))}
</SwiperWithAnimation.Box>
</div>

);
}


作者:In74
来源:juejin.cn/post/7312421872414818331
收起阅读 »

我的2023年,人到中年波波折折、起起伏伏

今年2023年了,感觉还活在2021年,可再过半个月就2024年了,是时候回顾一下今年发生的所有事。在2023年经历了被优化、全职带小孩、带小孩期间寻找兼职、后面又成功上岸新公司,历历在目! 被优化 2023年3月份,所属上家公司优化人员,很不幸成了其中一员,...
继续阅读 »

今年2023年了,感觉还活在2021年,可再过半个月就2024年了,是时候回顾一下今年发生的所有事。在2023年经历了被优化、全职带小孩、带小孩期间寻找兼职、后面又成功上岸新公司,历历在目!


被优化


2023年3月份,所属上家公司优化人员,很不幸成了其中一员,好在公司给了补尝,正常N+1,这家公司待了1年半多的时间,说实话,要不是工资达到了预期,我肯定不愿意继续待这里,对于中年的我,钱会是首位。


在这里也顺便抱怨一下这家公司我的直属负责人,每天基本见不到人,一年到头只有在谈绩效的时候才会聊几句,对于下属的情况丝毫不关心,非常不负责,垃圾田宇!记得在上家公司的时候,项目开发的最后一个项目,公司采用了开源项目的基础架构,但是开源项目是5年前开发的,js加一堆坑代码,谁看谁知道,对于我来说,一个好的基础架构一定会有一个愉快的开发体验,于是在项目开发初始,基于基础架构实现原理,用几天时间重新架构的新的方案,用目前流行ts+react+antd方式进行项目开发,把文档写好,优势利弊写完,然后发群里(部门群),然后就没然后了,对于这种躺平的部门真的fuck了,领导不负责,造就了下属更加无所谓。


绩效靠与领导关系,领导抽烟,于是那几个抽烟的同志绩效普遍就好,一个私企里的小部门都搞的这么恶心人!


重拾心态,为下一步认真考虑


其实早在上家公司的时候就有找下一家的打算了,持续了大概一个多月,但是行情是真的不行,基本石沉大海,期间因为我老婆也想出去试试,老婆全职在家带娃,于是就与老婆互换角色,她出去打工赚钱,我在家全职带娃。没小宝宝的可能不知道,有了小宝宝后,其实家庭会出现矛盾,矛盾来源于如何带好小孩,简单的说就是教育问题,我的宗旨是让小孩自我成长,自己的事情自己做,妈妈不这样小,小孩还小(已上大班了),吃饭、洗脸、洗澡、睡觉以及最基本的生活常识,妈妈都盯着、帮着,上幼儿园后,基本每两周生一次病,然后在家休养两周,每两周不断循环,我看着烦,我带的话,保活就可以,虽然后面我带的3个月期间有小感冒,但是幼儿园都坚持上着,没请一天假。


找兼职赚钱


在家带娃的期间,就想实践一下能不能通过兼职赚钱,帮企业主开发软件,赚取最基本的生活费。事实证明,这一步还是行的通的,在3个多月期间,兼职了好几个前端开发任务,包括外包任务、直接与老板洽谈、程序员客栈接单,在这期间赚的钱并没有上班时赚的多,但是至少不会让自己闲着,算了一下,大概赚了5万左右,最后一份兼职是与老板直接洽谈,非常感谢博库老板的信任,这份情我会记心上的。


重新找工作


时间大概来到了8月份,那个时候还做着兼职,但是老婆的工作并不顺利,她公司是新成立的部门,由于业绩没达到公司的要求,被迫全部解散,这一次的工作对于她来说也算是让她认清工作很难,尤其是对于全职妈妈来说,找到一份好工作更加不可能,我也清楚这对于她来说确实很难。


在8月份又开始了疯狂的投递简历(当然兼职期间也会偶尔投一下),两周过去,毫无消息、三周、四周,在8月下旬的时候终于接到了目前公司的面试通知以及另外一家,面试对我来说真不难,在过去的几年,除了大厂,小公司的面试基本能过,这两家公司也顺利的通过了面试,但是另外一家公司给的薪资没有目前这家公司的高,遂放弃。


入职的这家公司也是降薪加入(相对上家),但是也算是满足了,毕竟当前环境也确实不好找。


入职新公司


8月30号办理了入职手续。


有好多前端开发er,最主要有很多好相处的同事,到目前已经过去了快5个月了,时间过的可真快。


2023年最值得说的是:


因为ai的爆发,我在离职期间也报名了相关的课程,花了4000多,也算是入门ai了吧,但是因为现在工作了,也还是没有后续。。。


第二个就是背单词了,希望自己能够背下7000个单词,明年11月份能去参加一次雅思考试,为什么要参加雅思考试,为了有机会能够run出去


2024年期许:


明年一定要考一次雅思!


作者:莹石
来源:juejin.cn/post/7313941093876138038
收起阅读 »

Android 布局优化,看过来 ~

屏幕刷新机制 基本概念 刷新率:屏幕每秒刷新的次数,单位是 Hz,例如 60Hz,刷新率取决于硬件的固定参数。 帧率:GPU 在一秒内绘制操作的帧数,单位是 fps。Android 采用的是 60fps,即每秒 GPU 最多绘制 60 帧画面,帧率是动态变化...
继续阅读 »

屏幕刷新机制


基本概念



  • 刷新率:屏幕每秒刷新的次数,单位是 Hz,例如 60Hz,刷新率取决于硬件的固定参数。

  • 帧率:GPU 在一秒内绘制操作的帧数,单位是 fps。Android 采用的是 60fps,即每秒 GPU 最多绘制 60 帧画面,帧率是动态变化的,例如当画面静止时,GPU 是没有绘制操作的,帧率就为0,屏幕刷新的还是 buffer 中的数据,即 GPU 最后操作的帧数据。


显示器不是一次性将画面显示到屏幕上,而是从左到右边,从上到下逐行扫描,顺序显示整屏的一个个像素点,不过这一过程快到人眼无法察觉到变化。以 60 Hz 刷新率的屏幕为例,这一过程的耗时: 1000 / 60 ≈ 16.6ms。


屏幕刷新的机制大概就是: CPU 执行应用层的测量,布局和绘制等操作,完成后将数据提交给 GPU,GPU 进一步处理数据,并将数据缓存起来,屏幕由一个个像素点组成,以固定的频率(16.6ms)从缓冲区中取出数据来填充像素点。


画面撕裂


如果一个屏幕内的数据来自两个不同的帧,画面会出现撕裂感。屏幕刷新率是固定的,比如每 16.6ms 从 buffer 取数据显示完一帧,理想情况下帧率和刷新率保持一致,即每绘制完成一帧,显示器显示一帧。但是 CPU 和 GPU 写数据是不可控的,所以会出现 buffer 里有些数据根本没显示出来就被重写了,即 buffer 里的数据可能是来自不同的帧,当屏幕刷新时,此时它并不知道 buffer 的状态,因此从 buffer 抓取的帧并不是完整的一帧画面,即出现画面撕裂。


那怎么解决这个问题呢?Android 系统采用的是 双缓冲 + VSync


双缓冲:让绘制和显示器拥有各自的 buffer,GPU 将完成的一帧图像数据写入到 BackBuffer,而显示器使用的是 FrameBuffer,当屏幕刷新时,FrameBuffer 并不会发生变化,当 BackBuffer 准备就绪后,它们才进行交换。那什么时候进行交换呢?那就得靠 VSync。


VSync:当设备屏幕刷新完毕后到下一帧刷新前,因为没有屏幕刷新,所以这段时间就是缓存交换的最佳时间。此时硬件屏幕会发出一个脉冲信号,告知 GPU 和 CPU 可以交换了,这个就是 Vsync 信号。


掉帧


有时,当布局比较复杂,或者设备性能较差的时候,CPU 并不能保证在 16.6ms 内就完成绘制,这里系统又做了一个处理,当正在往 BackBuffer 填充数据时,系统会将 BackBuffer 锁定。如果到了 GPU 交换两个 Buffer 的时间点,你的应用还在往 BackBuffer 中填充数据,会发现 BackBuffer 被锁定了,它会放弃这次交换。
这样做的后果就是手机屏幕仍然显示原先的图像,这就是所谓的掉帧。


优化方向


如果想要屏幕流畅运行,就必须保证 UI 全部的测量,布局和绘制的时间在 16.6ms 内,因为人眼与大脑之间的协作无法感知超过 60fps 的画面更新,也就是 1000 / 60Hz = 16.6ms,也就是说超过 16.6ms 用户就会感知到卡顿。


层级优化


层级越少,View 绘制得就越快,常用有两个方案。



  • 合理使用 RelativeLayout 和 LinearLayout:层级一样优先使用 LinearLayout,因为 RelativeLayout 需要考虑视图之间的相对位置关系,需要更多的计算和更高的系统开销,但是使用 LinearLayout 有时会使嵌套层级变多,这时就应该使用 RelativeLayout。

  • 使用 merge 标签:它会直接将其中的子元素添加到 merge 标签 Parent 中,这样就不会引入额外的层级。它只能用在布局文件的根元素,不能在 ViewStub 中使用 merge 标签,当需要 inflate 的布局本身是由 merge 作为根节点的话,需要将其置于 ViewGr0up 中,设置 attachToRoot 为 true。


一个布局可以重复利用,当使用 include 引入布局时,可以考虑 merge 作为根节点,merge 根节点内的布局取决于include 这个布局的父布局。编写 XML 时,可以先用父布局作为根节点,然后完成后再用 merge 替换,方便我们预览效果。


merge_layout.xml


<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">


<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello" />


<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="World" />


</merge>

父布局如下:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">


<include layout="@layout/merge_layout" />

</LinearLayout>

如果需要通过 inflate 引入 merge_layout 布局文件时,可以这样引入:


class MyLinearLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {

init {
LayoutInflater.from(context).inflate(R.layout.merge_layout, this, true)
}
}

第一个参数为 merge 布局文件 id,第二个参数为要将子视图添加到的 ViewGr0up,第三个参数为是否将加载好的视图添加到 ViewGr0up 中。


需要注意的是,merge 标签的布局,是不能设置 padding 的,比如像这样:


<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="30dp">


<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello" />


<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="World" />


</merge>

上面的这个 padding 是不会生效的,如果需要设置 padding,可以在其父布局中设置。


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="30dp"
tools:context=".MainActivity">


<include layout="@layout/merge_layout" />

</LinearLayout>

ViewStub


ViewStub 是一个轻量级的 View,一个看不见的,并且不占布局位置,占用资源非常小的视图对象。可以为 ViewStub 指定一个布局,加载布局时,只有 ViewStub 会被初始化,当 ViewStub 被设置为可见或 inflate 时,ViewStub 所指向的布局会被加载和实例化,可以使用 ViewStub 来设置是否显示某个布局。


ViewStub 只能用来加载一个布局文件,且只能加载一次,之后 ViewStub 对象会被置为空。适用于某个布局在加载后就不会有变化,想要控制显示和隐藏一个布局文件的场景,一个典型的场景就是我们网络请求返回数据为空时,往往要显示一个默认界面,表明暂无数据。


view_stub_layout.xml


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">


<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher" />


<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="no data" />


</LinearLayout>

通过 ViewStub 引入


<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">


<data>

<variable
name="click"
type="com.example.testapp.MainActivity.ClickEvent" />

</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">


<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{click::showView}"
android:text="show" />


<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{click::hideView}"
android:text="hide" />


<ViewStub
android:id="@+id/default_page"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/view_stub_layout" />


</LinearLayout>
</layout>

然后在代码中 inflate,这里通过按钮点击来控制其显示和隐藏。


class MainActivity : AppCompatActivity() {

private var viewStub: ViewStub? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.click = ClickEvent()
viewStub = binding.defaultPage.viewStub
if (!binding.defaultPage.isInflated) {
viewStub?.inflate()
}
}

inner class ClickEvent {
// 后面 ViewStub 已经回收了,所以只能用 GONE 和 VISIBLE
fun showView(view: View) {
viewStub?.visibility = View.VISIBLE
}

fun hideView(view: View) {
viewStub?.visibility = View.GONE
}
}
}

过度绘制


过度绘制是指屏幕上的某个像素在同一帧的时间内被绘制了多次,在多层次重叠的 UI 结构中,如果不可见的 UI 也在做绘制操作,就会导致某些像素区域被绘制了多次,从而浪费了 CPU 和 GPU 资源。


我们可以打开手机的开发人员选项,打开调试 GPU 过度绘制的开关,就能通过不同的颜色区域查看过度绘制情况。我们要做的,就是尽量减少红色,看到更多的蓝色。



  • 无色:没有过度绘制,每个像素绘制了一次。

  • 蓝色:每个像素多绘制了一次,蓝色还是可以接受的。

  • 绿色:每个像素多绘制了两次。

  • 深红:每个像素多绘制了4次或更多,影响性能,需要优化,应避免出现深红色区域。


优化方法



  • 减少不必要的背景:比如 Activity 往往会有一个默认的背景,这个背景由 DecorView 持有,当自定义布局有一个全屏的背景时,这个 DecorView 的背景对我们来说是无用的,但它会产生一次 Overdraw,可以干掉。


window.setBackgroundDrawable(null)


  • 自定义 View 的优化:在自定义 View 的时候,某个区域可能会被绘制多次,造成过度绘制。可以通过 canvas.clipRect 方法指定绘制区域,可以节约 CPU 与 GPU 资源,在 clipRect 区域之外的绘制指令都不会被执行。


AsyncLayoutInflater


setContentView 函数是在 UI 线程执行的,其中有一系列的耗时动作:XML 的解析,View 的反射创建等过程都是在 UI 线程执行的,AsyncLayoutInflater 就是把这些过程以异步的方式执行,保持 UI 线程的高响应。


implementation 'androidx.asynclayoutinflater:asynclayoutinflater:1.0.0'

class TestActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AsyncLayoutInflater(this).inflate(R.layout.activity_test, null) { view, _, _ ->
setContentView(view)
}
}
}

这样,将 UI 的加载过程迁移到了子线程,保证了 UI 线程的高响应,使用时需要特别注意,调用 UI 一定要等它初始化完成之后,不然可能会产生崩溃。


Compose


Jetpack Compose 相对于传统的 XML 布局方式,具有更强的可组合性,更高的效率和更佳的开发体验,相信未来会成为 Android UI 开发的主流方式。


传统的 XML 布局方式是基于声明式的 XML 代码编写的,使用大量的 XML 标签来描述 UI 结构,XML 文件通过解析和构建生成 View 对象,并将它们添加到 View 树中。在 Compose 中,UI 代码被组织成可组合的函数,每个函数都负责构建某个具体的 UI 元素,UI 元素的渲染是由 Compose 运行时直接管理的,Composable 函数会被调用,以计算并生成当前 UI 状态下的最终视图。


作者:阿健君
来源:juejin.cn/post/7221811522740256823
收起阅读 »

开发需求记录:实现app任意界面弹框与app置于后台时通知

前言 在产品经理提需求时候提到,app在接收到报警信息时候能不能弹出一个弹框,告诉用户报警信息,这个弹框要在app的任意界面能够弹出,并且用户点击详情时候,会跳转到报警详情界面,查看具体信息,当用户将app至于后台的时候,接收到报警信息,app发送通知,当用户...
继续阅读 »

前言


在产品经理提需求时候提到,app在接收到报警信息时候能不能弹出一个弹框,告诉用户报警信息,这个弹框要在app的任意界面能够弹出,并且用户点击详情时候,会跳转到报警详情界面,查看具体信息,当用户将app至于后台的时候,接收到报警信息,app发送通知,当用户点击通知时候,跳转到报警详情界面。
功能大体总结如上,在实现弹框与通知在跳转界面时遇到一些问题,在此记录一下。效果图如下:


开发需求 - 通知与弹框.gif


功能分析


弹框实现,使用DialogFragment。

前后台判断则是,创建一个继承自ActivityLifecycleCallbacks接口和Application的类,继承ActivityLifecycleCallbacks接口是为了前后台判断,继承Application则是方便在基类BaseActivity获取前后台相关数据。

项目原本采用单Activity多Fragment实现,后面因为添加了视频相关功能,改为了多Activity多Fragment。

原单Activity时候,实现比较容易。后面修改为多Activity,就有些头疼,最终用思路是创建基类BaseActivity,后面添加Activity时都要继承基类BaseActivity。使用基类原因是把相同的功能抽取出来,且若每个Activity都自己实现弹框和通知的话太容易出错,也太容易漏下代码了。


代码实现


弹框


在实现继承自DialogFragment的弹框时,需要在onCreateDialog方法内设置dialog的宽高模式以及背景,不然弹框会有默认的边距,导致显示效果与预期不符,未去边距与去掉边距的弹框效果如下:
image.png
关于onCreateDialog的代码如下:


override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = Dialog(requireContext())
dialog.setContentView(R.layout.custom_dialog_layout)
dialog.window?.apply {
setLayout(ViewGr0up.LayoutParams.MATCH_PARENT,ViewGr0up.LayoutParams.MATCH_PARENT)
setBackgroundDrawable(ColorDrawable(Color.parseColor("#88000000")))//去掉DialogFragment的边距
}
dialog.setCancelable(false)
return dialog
}

此外当弹框出现的时候,弹框背景色还会闪烁。这里采用属性值动画设置弹框背景色控件的透明度变换。完整的Dialog代码如下:


class AlarmDialogFragment: DialogFragment() {
private lateinit var binding:CustomDialogLayoutBinding
private var animator:ObjectAnimator? = null

override fun show(manager: FragmentManager, tag: String?) {
try {
super.show(manager, tag)
}catch (e:Exception){
e.printStackTrace()
}
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGr0up?,
savedInstanceState: Bundle?
)
: View? {
binding = CustomDialogLayoutBinding.inflate(inflater)
return binding.root
}

override fun onStart() {
super.onStart()
binding.viewAlarmDialogBg
startAnimation()
}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = Dialog(requireContext())
dialog.setContentView(R.layout.custom_dialog_layout)
dialog.window?.apply {
setLayout(ViewGr0up.LayoutParams.MATCH_PARENT,ViewGr0up.LayoutParams.MATCH_PARENT)
setBackgroundDrawable(ColorDrawable(Color.parseColor("#88000000")))//去掉DialogFragment的边距
}
dialog.setCancelable(false)
return dialog
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
}

override fun onDestroy() {
super.onDestroy()
if(animator?.isStarted == true){
animator?.end()
}
}

private fun initView() {
binding.btnCloseDialog.setOnClickListener {
dismiss()
}

binding.btnDialogNav.setOnClickListener {
if(context is MainActivity){
val bundle = Bundle()
bundle.putString("alarmId","1")
findNavController().navigate(R.id.alarmDetailFragment,bundle)
}else{
val intent = Intent(context,MainActivity::class.java)
intent.putExtra("task","toAlarmDetail")
startActivity(intent)
}
dismiss()
}
}

private fun startAnimation() {
animator = ObjectAnimator.ofFloat(binding.viewAlarmDialogBg, "alpha", 0f, 0.6f, 0f, 0.6f, 0f)
animator?.duration = 1200
animator?.interpolator = AccelerateInterpolator()
animator?.start()
}
}

需要注意地方是,由于弹框还负责跳转,而跳转有两种情况,一种是在ActivityA内,fragmentA与fragmentB间的跳转,这种情况使用findNavController().navigate()方法进行跳转,另一种是ActivityB到另一个ActivityA内的指定FragmentB界面。这种采用startActivity(intent)方式跳转,并且在ActivityA的onStart()的方法使用下面方法。


/** 跳转报警详情界面 */
private fun initToAlarmDetail() {
val task = intent.getStringExtra("task")
if (task == "toAlarmDetail"){
val bundle = Bundle()
bundle.putString("alarmId","1")
findNavController(R.id.fragment_main_table).navigate(R.id.alarmDetailFragment,bundle)
}
}

这样从ActivityB到另一个ActivityA时候,在onStart()方法内会触发上面的initToAlarmDetail()方法,获取跳转里面的信息,在决定具体跳转到哪个Fragment。这里解释的可能不太清楚,可以在Github下载源码看看可能更好理解些。


弹框对应的xml文件代码,可以在Github内查看,可以自己写一个,这个xml比较简单,只是xml代码比较占地方这里就不粘贴了。


前后台判断


关于前后台判断,需要创建一个继承ActivityLifecycleCallbacks和Application的类,这里命名为CustomApplication,在类里面实现ActivityLifecycleCallbacks接口相关方法,此外需要创建下面三个变量,分别表示activity数量,当前activity的名称,是否处于后台,代码如下:


private var activityCount = 0
private var nowActivityName:String? = null
private var isInBackground = true

之后需要在onActivityStarted,onActivityResumed,onActivityStopped方法内进行前后台相关处理,代码如下:


override fun onActivityStarted(activity: Activity) {
activityCount++
if (isInBackground){
isInBackground = false
}
nowActivityName = activity.javaClass.name
}

override fun onActivityStopped(activity: Activity) {
activityCount--
if (activityCount == 0 && !isInBackground){
isInBackground = true
}
}

上面代码可以看出,当触发onActivityStarted方法时候,activityCount数量加一,且app处于前台。之后记录当前activity名称,这里记录activity名称是后面有个功能是app置于后台时候弹出通知,而通知相关操作,为了每个activity都能实现就放在基类执行,而弹出通知并不需要每个继承基类的activity都执行,到时候需要根据根据nowActivityName判断哪个继承了基类的activity执行通知操作。


当触发onActivityStopped方法时候,activityCount数量减一,且当activityCount数量为零时,app置于后台。
CustomApplication完整代码如下:


class CustomApplication: Application(),Application.ActivityLifecycleCallbacks {
companion object{
const val TAG = "CustomApplication"
@SuppressLint("CustomContext")
lateinit var context: Context
}

private var activityCount = 0
private var nowActivityName:String? = null
private var isInBackground = true

fun getNowActivityName(): String? {
return nowActivityName
}

fun getIsInBackground():Boolean{
return isInBackground
}

override fun onCreate() {
super.onCreate()
context = applicationContext
registerActivityLifecycleCallbacks(this)
}

override fun onActivityCreated(activity: Activity, p1: Bundle?) {

}

override fun onActivityStarted(activity: Activity) {
activityCount++
if (isInBackground){
isInBackground = false
}
nowActivityName = activity.javaClass.name
}

override fun onActivityResumed(activity: Activity) {

}

override fun onActivityPaused(activity: Activity) {

}

override fun onActivityStopped(activity: Activity) {
activityCount--
if (activityCount == 0 && !isInBackground){
isInBackground = true
}
}

override fun onActivitySaveInstanceState(activity: Activity, p1: Bundle) {

}

override fun onActivityDestroyed(activity: Activity) {

}
}

弹框与通知弹出


开发中弹框与通知弹出的触发条件是,监听Websocket若有信息过来,app处于前台弹框,处于后台弹通知。这里使用Handler来模拟,弹框弹出比较简单,若有继承了DialogFragment的AlarmDialogFragment类。代码如下:


val dialog = AlarmDialogFragment()
dialog.show(supportFragmentManager,"tag")

通知弹出也不难,若只是弹出通知示例代码如下:


if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel("normal", "Normal", NotificationManager.IMPORTANCE_DEFAULT)
notificationManager?.createNotificationChannel(channel)
}
val notification = NotificationCompat.Builder(this.applicationContext, "normal")
.setContentTitle("标题")
.setContentText("通知次数:${++alarmCount}")
.setSmallIcon(R.drawable.ic_launcher_background)
.setTimeoutAfter(5000)
.setAutoCancel(true)
.build()
notificationManager?.notify(notificationId,notification)

弹框与通知的特殊要求是,能在界面任意地方弹出且跳转到指定界面。弹框跳转相关代码在上面'弹框'部分,下面来说下通知的跳转,点击通知跳转是通过创建PendingIntent后在设置进NotificationCompat的setContentIntent方法内,不过通知跳转与弹框跳转一样需要分两种情况考虑,第一种同一Activity内Fragment与Fragment跳转,这种情况下PendingIntent如下代码所示:


var pendingIntent:PendingIntent? = null
val bundle = Bundle()
bundle.putString("alarmId","1")
pendingIntent = NavDeepLinkBuilder(this)
.setGraph(R.navigation.main_navigation)
.setDestination(R.id.alarmDetailFragment)
.setArguments(bundle)
.createPendingIntent()

上面代码中使用NavDeepLinkBuilder创建了一个PendingIntent,并且使用setGraph()指向使用的导航图,setDestination()则指向目标Fragment。
另一种情况则是ActivityB到另一个ActivityA内的指定FragmentB界面,这种情况下PendingIntent设置代码如下:


val intent = Intent(this@BaseActivity,MainActivity::class.java)
intent.putExtra("task","toAlarmDetail")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
pendingIntent = TaskStackBuilder.create(this@BaseActivity)
.addNextIntentWithParentStack(intent)
.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT)

这种是创建一个跳转到MainActivity的Intent,并添加传递的参数task,接着设置Intent的启动方式,其中Intent.FLAG_ACTIVITY_NEW_TASK,表示启动Activity作为新任务启动,Intent.FLAG_ACTIVITY_CLEAR_TASK,表示清除任务栈中所有现有的Activity。之后调用TaskStackBuilder创建PendingIntent。
上面两种方式创建的PendingIntent可以通过NotificationCompat.setContentIntent(pendingIntent)添加进去,关于通知创建的代码如下:


/** 使用通知 - 通过pendingIntent实现跳转,缺点是任意界面进入报警详情界面,点击返回键只能返回MainFragment */
private fun useNotificationPI() {
var pendingIntent:PendingIntent? = null
if(javaClass.simpleName == "MainActivity"){//主界面
val bundle = Bundle()
bundle.putString("alarmId","1")
pendingIntent = NavDeepLinkBuilder(this)
.setGraph(R.navigation.main_navigation)
.setDestination(R.id.alarmDetailFragment)
.setArguments(bundle)
.createPendingIntent()
}else {//其他界面时候切换后台通知
val intent = Intent(this@BaseActivity,MainActivity::class.java)
intent.putExtra("task","toAlarmDetail")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
pendingIntent = TaskStackBuilder.create(this@BaseActivity)
.addNextIntentWithParentStack(intent)
.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel("normal", "Normal", NotificationManager.IMPORTANCE_DEFAULT)
notificationManager?.createNotificationChannel(channel)
}
val notification = NotificationCompat.Builder(this.applicationContext, "normal")
.setContentTitle("标题")
.setContentText("通知次数:${++alarmCount}")
.setSmallIcon(R.drawable.ic_launcher_background)
.setTimeoutAfter(5000)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.build()
notificationManager?.notify(notificationId,notification)
}

上面代码中if(javaClass.simpleName == "MainActivity"),及第四行代码,该代码用处是当app置于后台时候,pp界面是MainActivity时,pendingIntent使用NavDeepLinkBuilder生成,当是其他Activity时使用TaskStackBuilder生成。之所以这样是因为,在MainActivity的xml,使用了FragmentContainerView用于fragment间跳转,其他的Activity没有FragmentContainerView,因此在生成pendingIntent需要采用不同的方式生成。


这示例代码中,主要涉及到的Avtivity有MainActivity与VideoActivity,MainActivity使用FragmentContainerView,而VideoActivity没有。弹框与通知跳转的界面是AlarmDetailFragment,这个fragment在MainActivity通过Navigation实现导航。


因此在MainActivity界面进入后台时,pendingIntent使用NavDeepLinkBuilder生成,NavDeepLinkBuilder则可以使用导航图中fragment生成深度链接URI,这个URI则可以导航到指定的fragment(关于NavDeepLinkBuilder了解不深入,这里说的可能有错误地方,欢迎大佬指正)。


而VideoActivity界面进入后台时,就需要使用TaskStackBuilder生成一个启动MainActivity的Intent。而在MainActivity的onStart方法内有下面initToAlarmDetail方法,判断跳转时携带参数决定是否跳转到AlarmDetailFragment界面。


/** 跳转报警详情界面 */
private fun initToAlarmDetail() {
val task = intent.getStringExtra("task")
if (task == "toAlarmDetail"){
val bundle = Bundle()
bundle.putString("alarmId","1")
findNavController(R.id.fragment_main_table).navigate(R.id.alarmDetailFragment,bundle)
}
}

至此弹框与通知的功能基本实现,完整的BaseActivity代码如下:


open class BaseActivity: AppCompatActivity() {
companion object{
const val TAG = "BaseActivity"
}
private var alarmCount = 0
private val handler = Handler(Looper.myLooper()!!)
//为了关闭通知,manager放在外面
private val notificationId = 1
private var alarmDialogFragment: AlarmDialogFragment? = null
private var notificationManager:NotificationManager? = null
private var bgServiceIntent:Intent? = null//前台服务

private var nowClassName = ""

/** 弹框定时任务 */
private val dialogRunnable = object : Runnable {
override fun run() {
//在定时方法里面 javaClass.simpleName 不能获取当前所处Activity的名称
if (nowClassName == "VideoActivity"){ //视频界面不弹弹框
CustomLog.d(TAG,"不使用弹框 ${nowClassName}")
}else{
CustomLog.d(TAG,"使用弹框 ${nowClassName}")
useDialog()
handler.postDelayed(this, 10000)
}
}
}

/** 通知定时任务 */
private val notificationRunnable = object :Runnable{
override fun run() {
useNotificationPI()
handler.postDelayed(this,10000)
}
}

override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
initWindow()
return super.onCreateView(name, context, attrs)
}

override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
CustomLog.d(TAG,"onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) 当前类:${javaClass.simpleName}")
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CustomLog.d(TAG,"onCreate(savedInstanceState: Bundle?) 当前类:${javaClass.simpleName}")
initData()
}

override fun onStart() {
super.onStart()
CustomLog.d(TAG,"onStart 当前类:${javaClass.simpleName}")
nowClassName = javaClass.simpleName
handler.postDelayed(dialogRunnable, 3000)
initService()
}

override fun onResume() {
super.onResume()
CustomLog.d(TAG,"onResume 当前类:${javaClass.simpleName}")
}

override fun onRestart() {
super.onRestart()
CustomLog.d(TAG,"onRestart 当前类:${javaClass.simpleName}")
}

override fun onPause() {
super.onPause()
CustomLog.d(TAG,"onPause 当前类:${javaClass.simpleName}")
}

override fun onStop() {
super.onStop()
CustomLog.d(TAG,"onStop 当前类:${javaClass.simpleName}")
val customApplication = applicationContext as CustomApplication
val nowActivityName = customApplication.getNowActivityName()
val activitySimpleName = nowActivityName?.substringAfterLast(".")
CustomLog.d(TAG,"activitySimpleName:$activitySimpleName")
val isInBackground = (this@BaseActivity.applicationContext as CustomApplication).getIsInBackground()
if (isInBackground && activitySimpleName.equals(javaClass.simpleName)){// 处于后台 且 切换至后台app的activity页面名称等于当前基类里面获取activity类名
handler.postDelayed(notificationRunnable,3000)
CustomLog.d(TAG,"使用通知 $nowClassName")
}else{
CustomLog.d(TAG,"关闭所有定时任务 $nowClassName")
closeAllTask()
}
}

override fun onDestroy() {
super.onDestroy()
CustomLog.d(TAG,"onDestroy 当前类:${javaClass.simpleName}")
closeAllTask()
this.stopService(bgServiceIntent)
}

/** 关闭所有定时任务 */
private fun closeAllTask() {
handler.removeCallbacks(dialogRunnable)
handler.removeCallbacks(notificationRunnable)
}

/** 初始化数据 - 关于弹框*/
private fun initData() {
notificationManager = notificationManager ?: this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
alarmDialogFragment = alarmDialogFragment ?: AlarmDialogFragment()
}

/** 使用通知 - 通过pendingIntent实现跳转,缺点是任意界面进入报警详情界面,点击返回键只能返回MainFragment */
private fun useNotificationPI() {
var pendingIntent:PendingIntent? = null
if(javaClass.simpleName == "MainActivity"){//主界面
CustomLog.d(TAG,">>>通知:MainActivity")
val bundle = Bundle()
bundle.putString("alarmId","1")
pendingIntent = NavDeepLinkBuilder(this)
.setGraph(R.navigation.main_navigation)
.setDestination(R.id.alarmDetailFragment)
.setArguments(bundle)
.createPendingIntent()
}else {//其他界面时候切换后台通知
CustomLog.d(TAG,">>>通知:else")
val intent = Intent(this@BaseActivity,MainActivity::class.java)
intent.putExtra("task","toAlarmDetail")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
pendingIntent = TaskStackBuilder.create(this@BaseActivity)
.addNextIntentWithParentStack(intent)
.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel("normal", "Normal", NotificationManager.IMPORTANCE_DEFAULT)
notificationManager?.createNotificationChannel(channel)
}
val notification = NotificationCompat.Builder(this.applicationContext, "normal")
.setContentTitle("标题")
.setContentText("通知次数:${++alarmCount}")
.setSmallIcon(R.drawable.ic_launcher_background)
.setTimeoutAfter(5000)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.build()
notificationManager?.notify(notificationId,notification)
}

/** 弹框使用 - 因为此处涉及到fragment等生命周期,进入其他activity内时候,在前的activity使用useDialog会因为生命周期问题闪退*/
private fun useDialog() {
//弹出多个同种弹框
// alarmDialogFragment = AlarmDialogFragment()
// alarmDialogFragment?.show(supportFragmentManager,"testDialog")

//不弹出多个同种弹框,一次只弹一个,若弹框存在不弹新框
if (alarmDialogFragment?.isVisible == false){//如果不加这一句,当弹框存在时候在调用alarmDialogFragment.show的时候会报错,因为alarmDialogFragment已经存在
alarmDialogFragment?.show(supportFragmentManager,"testDialog")
}else{
//更新弹框内信息
}
}

/** 关闭报警弹框 */
private fun closeAlarmDialog() {
if (alarmDialogFragment?.isVisible == true) {
alarmDialogFragment?.dismiss()//要关闭的弹框
}
}

//状态栏透明,且组件占据了状态栏
private fun initWindow() {
window.statusBarColor = Color.TRANSPARENT
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
}

/** 初始化服务 */
private fun initService() {
CustomLog.d(TAG,"开启前台服务")
bgServiceIntent = bgServiceIntent ?: Intent(this, BackgroundService::class.java)
this.startService(bgServiceIntent)
}
}

总结


只是弹出弹框和通知的话,实现很好实现,中间麻烦地方在于当app使用多个Activity,该怎么实现跳转到指定的界面。当然这里麻烦是,从ActivityB跳转到ActivityA的Fragment,如果是只有一个Activity应该会好办些。个人感觉fragment跳转应该有更好的方式实现希望能和大佬们交流下这种情况下,用什么技术实现。


PS:感觉原生Android在写界面和跳转方面写起来不太方便。不知道大家有便捷的方式吗。


代码地址


GitHub:github.com/SmallCrispy…


作者:卤肉拌面
来源:juejin.cn/post/7260808821659779129
收起阅读 »

css 实现 'X' 号的显示(close关闭 icon), 并支持动画效果

web
最近项目上要实现一个小 'x' 的关闭样式, 今天记录一下处理过程 先看效果 HTML DOM 元素说明 要渲染内容必须有 dom 节点, 这里我们使用 span 作为容器, 然后所有的处理都基于它进行处理 <span class="close-x"&...
继续阅读 »

最近项目上要实现一个小 'x' 的关闭样式, 今天记录一下处理过程


先看效果


HTML DOM 元素说明


要渲染内容必须有 dom 节点, 这里我们使用 span 作为容器, 然后所有的处理都基于它进行处理


<span class="close-x">span>

第一步, 设置 close-x 的样式


@closeXSize: 20px; // 大小/尺寸
@closeXLine: 2px; // 线条宽度
.close-x {
position: relative;
display: inline-block;
width: @closeXSize;
height: @closeXSize;
cursor: pointer;
}


  • 通过使用 closeXSize closeXLine, 方便对尺寸进行调整

    渲染出来大概是这样的
    image.png


第二步, 通过伪元素 before after 画两条线


.close-x {
// ...

&::before, &::after {
position: absolute;
left: 50%;
width: @closeXLine;
height: 100%;
margin-left: (@closeXLine / -2);
content: '';
background: #000;
}
}


  • margin-left 的设置是为了处理'线'的自身宽度

    渲染出来大概是这样的
    image.png


第三步, 分别设置旋转角度


.close-x {
// ...

&::before {
transform: rotate(-45deg);
}

&::after {
transform: rotate(45deg);
}
}

渲染出来大概是这样的, 基本上就完成了
image.png


继续优化, 锦上添花



  • 先来定义一个动画, 动画的意思是这样的:

    • 当为 0% 时旋转角为 0 度,

    • 当为 100% 时旋转角为 360 度




@keyframes rotating {
0% {
transform: rotate(0deg);
}

100% {
transform: rotate(360deg);
}
}

持续旋转


.rotate-infinite {
animation: rotating .3s infinite linear;
}

// 使用方式 增加类 rotate-infinite
//

加载时旋转一次


.rotate-one {
animation: rotating .3s linear;
}

// 使用方式 增加类 rotate-one
//

hover 时旋转一次


.rotate-hover:hover {
.rotate-one();
}

// 使用方式 增加类 rotate-hover
//

选中时旋转


.rotate-active:active {
.rotate-infinite();
}

// 使用方式 增加类 rotate-active
//

纯JS实现


function addCloseX(content) {
const closeXSize = 20;
const closeXLine = 2;

const closeXWrap = document.createElement('div');
closeXWrap.style.cssText = `
position: relative;
display: inline-block;
width:
${closeXSize}px;
height:
${closeXSize}px;
cursor: pointer;
`
;

const baseStyle = `
display: block;
height: 100%;
width:
${closeXLine}px;
margin: auto;
background: #000;
`
;
const xLineOne = document.createElement('i');
xLineOne.style.cssText = baseStyle + `
transform: rotate(45deg);
`
;
const xLineTwo = document.createElement('i');
xLineTwo.style.cssText = baseStyle + `
margin-top: -100%;
transform: rotate(-45deg);
`
;
closeXWrap.appendChild(xLineOne);
closeXWrap.appendChild(xLineTwo);

content.appendChild(closeXWrap);
}

addCloseX(document.getElementById('close'))

需要提供一下注入的位置, 以上示例需要我们提供这样的 dmo 节点:


<div id="close">div>


  • 这种方式没有使用样式表, 所有的样式都使用了行内样式的方式实现的

  • 因为只用到了行内样式, 所以没办法使用伪元素, 故引入了两个 i 标签代替


结束


相关文档


CSS 实现圆(环)形进度条


作者:洲_
来源:juejin.cn/post/7263069805254197307
收起阅读 »

一文洞彻:Application为啥不能作为Dialog的context?

大家好,相信大家在使用Dialog时,都有一个非常基本的认知:就是Dialog的context只能是Activity,而不能是Application,不然会导致弹窗崩溃:这个Exception几乎属于是每个Android开发初学者都会碰到的,但是。前几天研究项...
继续阅读 »

大家好,相信大家在使用Dialog时,都有一个非常基本的认知:就是Dialog的context只能是Activity,而不能是Application,不然会导致弹窗崩溃:

这个Exception几乎属于是每个Android开发初学者都会碰到的,但是。

前几天研究项目代码发现  Application作为Dialogcontext竟然不会崩溃?!!这句话说出来和本篇文章标题严重不符哈,这不是赤裸裸的打脸了吗。先别急,请大家跟着我的脚步,相信阅读完本篇文章就可以解答目前你心目中最大的两个疑惑:

  1. 如标题所言,为啥Application无法作为Dialog的context并导致崩溃?
  2. 项目中为啥又发现,Application作为Dialog的context可以正常显示弹窗?

一. 窗口(包括Activity和Dialog)如何显示的?

这里怕有些童鞋不了解窗口(包括Activity和Dialog的)的显示流程,先简单的介绍下:

不管是Activity界面的显示还是DIalog的窗口显示,都会调用到WindowManagerImpl#addView()方法,这个方法经过一连续调用,会走到ViewRootImpl#setView()方法中。

在这个方法中,我们最终会调用到IWindowSession#addToDisplayAsUser()方法,这个方法是一个跨进程的调用,经过一番折腾,最终会执行到WMS的addWindow()方法。

在这个方法中会将窗口的信息进行保存管理,并且对于窗口的信息进行校验,比如上面的崩溃信息:“BadTokenException: Unable to add window”就是由于在这个方法中检验失败导致的;另外也是在这个方法中将窗口和Surface、Layer绘制建立起了连接(这句话说的可能不标准,主要对这块了解不多,懂得大佬可以评论分享下)。

接着开始在ViewRootImpl#setView()执行requestLayout()方法,开始进行渲染绘制等。

有了上面的简单介绍,接下来我们就开始先分析为啥Application作为Dialog的context会异常。

二. 窗口离不开的WindowManagerImpl

上面也说了,窗口只要显示,就得借助WindowManagerImpl#addView()方法,而WindowManagerImpl创建流程在ApplicationActivity的差异,就是Application作为Dialogcontext会异常的核心原因

我们就从下面方法作为入口进行分析:

context.getSystemService(WINDOW_SERVICE)

1. Application下WindowManagerImpl的创建

对于Application而言,getSystemService()方法的调用,最终会走到父类ContextWrapper中:

而这个mBase属性对应的类为ContextImpl对象,对应ContextImpl#getSystemService():

对应SystemServiceRegistry#getSystemService

SYSTEM_SERVICE_FETCHERS是一个Map集合,对应的key为服务的名称,value为服务的实现方式:

Android会在SystemServiceRegistry初始化的时候将各种服务以及服务的实现方法注册到这个集合中:

接下来看下咱们关心的WindowManager服务的注册方式:

到了这里,咱们就明白了,调用context.getSystemService(WINDOW_SERVICE)会返回一个WindowManagerImpl对象,核心点就在于WindowManagerImpl的构造函数,可以看到构造函数只传入了一个ContextImpl对象,我们看下其构造方法:

本篇文章重要的地方来了:通过这种方法创建的WindowManagerImpl对象,其mParentWindow属性是null的

2. Activity下WindowManagerImpl的创建

Activity重写了getSystemService()方法:

而mWindowManager属性的赋值是发生在Activity#attach()方法中:

这个mWindow属性对应的类型为Window类型(其唯一实现类为大家耳熟能详的PhoneWindow,其创建时机和Activity创建的时机是一起的),走进去看下:

经过一层层的调用,最终咱们的WindowManager是通过WindowManagerImpl#createLocalWindowManager创建的,并且参数传入的是当前的Window对象,即PhoneWindow。

可以看到,该方法最终帮助咱们创建了WindowManagerImpl对象,关键点是其mParentWindow属性的值为上面传入的PhoneWindow,不为null

小结:

Activity获取到的WindManager服务,即WindowManagerImpl的mParentWindow属性不为空,而Application获取的mParentWindow属性为null。

文章开头我们简单介绍了窗口的显示流程,同时又知道实现窗口添加的关键类WindowManagerImpl的来头,有了这些铺垫,接下来我们就对窗口的显示进行一个比较深入的分析。

三. 深入探究窗口的显示流程

这里我们就从WindowManagerGlobal#addView()方法说起,它是WindowManagerImpl#addView()方法的真正实现者。

WindowManagerImpl#addView():

WindowManagerGlobal#addView():

这一分析,就进入到了本篇文章最重要的一个方法的分析,如上面红框所示。

前面我们有讲过,对于Application获取的WindowManagerImpl,其mParentWindow属性为null,而Activity对应的mParentWindow不为null。

  1. 如果当前为Activity的窗口,或者借助Activity作为Context显示的Dialog窗口,其会走入到方法adjustLayoutParamsForSubWindow()中,对应的实现类为Window

type为窗口的类型,对于Activity的窗口还是对于Dialog的窗口,其对应类型为都为2(TYPE_APPLICATION),所以最终都会走到红框中的位置,最终给window对应的layoutparam对象的token属性赋值为mAppToken

这个mAppToken可以简单理解为窗口的一种凭证,它是AMS在startActivity流程的时候被初始化的,然后传递给应用侧,最终再用来WMS进行窗口检验的其中在AMS的startActivity流程中,会将这个AppToken作为key,并构造一个WindowToken对象作为value,写入到 DisplayContent#mTokenMap集合中,这部分详细的源码分析可以参考文章:Android高工面试(难度:四星):为什么不能使用 Application Context 显示 Dialog?

  1. 如果当前为application作为context显示的Dialog,mParentWindow为null,那就走不到adjustLayoutParamsForSubWindow()方法中,自然其Window#LayoutParam#token属性就是null。

咱们再次回到WindowManagerGlobal#addView()方法中,接下来会走到ViewRootImpl#setView()方法中,这个方法里最终会调用下面方法完成窗口真正的添加:

其中这个mWindowSession对应是一个Binder对象,对应类型为IWindowSession,其真正的实现位于system_server侧的Session类,所以这里会发生跨进程通信,并将window的LayoutParam类型参数进行传入,我们继续看下Session#addToDiaplayAsUser方法:

mService对应的实现类WindowManagerService,所以我们看下该类的addWindow方法:

# WindowManagerService
final HashMap mWindowMap = new HashMap<>();

public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
InputChannel outInputChannel, InsetsState outInsetsState,
InsetsSourceControl[] outActiveControls)
{

WindowState parentWindow = null;
final int type = attrs.type;
//1.
if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
parentWindow = windowForClientLocked(null, attrs.token, false);
//...
}
//2.
final boolean hasParent = parentWindow != null;
WindowToken token = displayContent.getWindowToken(
hasParent ? parentWindow.mAttrs.token : attrs.token);
//3.
if (token == null) {
if (!unprivilegedAppCanCreateTokenWith(parentWindow, callingUid, type,
rootType, attrs.token, attrs.packageName)) {
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
}

final WindowState win = new WindowState(this, session, client, token, parentWindow,
appOp[0], attrs, viewVisibility, session.mUid, userId,
session.mCanAddInternalSystemWindow);
}

# DiaplayConent
private final HashMap mTokenMap = new HashMap();

WindowToken getWindowToken(IBinder binder) {
return mTokenMap.get(binder);
}

上面的代码是经过精简后的。

  1. 前面有提到,Dialog的窗口类型为2,所以不满足if的条件,自然parentWindow无法赋值,即为null;
  2. 这里hasParent自然就是false,调用方法getWindowToken()传入的参数就是应用侧Window#LayoutParam#token属性,其中借助前面分析,如果Application作为Dialog的context,这个token值是null;

    看下getWindowToken()方法,它会将上面的传入token作为key,从DisplayContent#mTokenMap这个集合中获取值,什么时候写入值呢:前面有提到过,在startActivity的流程中,会向这个集合中写入值。而这个传入的token就是之前startActivity流程中,写入到DisplayContent#mTokenMap这个集合中的key,所以自然是能够获取到对应的value,即WindowToken类型属性token不为null,自然走不到3处标记的条件分支中,窗口校验通过。

  3. 而Application作为Dialog的context时,传入的token是null,自然是无法获取到值,WindowToken 类型属性token为null,走到if分支中,会返回WindowManagerGlobal.ADD_BAD_APP_TOKEN ,当应用侧检测到返回值为这个时,就会出现文章一开头说的BadTokenException异常

到了这里,相信你就明白了,为啥Application作为Dialog的context会导致崩溃,关键的分析就是上面的内容;

四. 不让Application作为Dialog的context崩溃?

根据上面的分析结果,Application作为Dialog的context崩溃的真正原因就是应用侧传过来的LayoutParam#token对象是null的,既然这样,那我们在应用侧给Dialog的Window#LayoutParam#token属性赋值为Activity的Window#LayoutParam#token属性,就可以避免这场悲剧发生了,可以看到下面能正常显示弹窗:

但是还是不建议大家这样做哈,毕竟如果在Dialog中使用到了这个Application的context进行Activity的跳转等其他未知行为,估计就会出现其他的幺蛾子了哈。

五. 总结

本篇文章涉及到的源码有点多,重点在于以下几个地方:

  1. Activity和Application获取WindowManager在应用侧服务的区别;
  2. 将窗口添加到WMS侧,Activity和Application下WindowManagerImpl传参token的区别;
  3. WMS中对应窗口类型以及传入的token是否为null进行的一番检验,已经检验不通过导致应用侧发生BadTokenException异常;

希望本篇文章能对你有所帮助,有什么需要交流的也欢迎下评论中留言,感谢阅读。

参考文章

Android高工面试(难度:四星):为什么不能使用 Application Context 显示 Dialog?


作者:长安皈故里
来源:juejin.cn/post/7314125877486616615
收起阅读 »

一次面试让我真正认识了 “:visible.sync”

web
面试官提出了一个很实际的问题:如何封装一个不需要在每个父组件中都定义关闭方法的全局对话框组件,类似于 Element UI 的 el-dialog。这篇技术文章将带你了解如何实现这样的组件,并解释 :visible.sync 这个 Vue 2 的语法糖。 如何...
继续阅读 »

面试官提出了一个很实际的问题:如何封装一个不需要在每个父组件中都定义关闭方法的全局对话框组件,类似于 Element UI 的 el-dialog。这篇技术文章将带你了解如何实现这样的组件,并解释 :visible.sync 这个 Vue 2 的语法糖。


如何封装一个类似 el-dialog 的全局对话框组件


el-dialog 是 Element UI 中的一个常用对话框组件,它提供了一种简洁的方式来展示模态框。在我们自己的项目中,我们可能也会需要封装一个自定义的、功能类似的对话框组件。


步骤一:创建 MyDialog 组件


在 src/components 目录下创建 MyDialog.vue 文件:


<template>
<!-- 对话框的 HTML 代码结构如上所示 -->
</template>

<script>
export default {
// 组件逻辑如上所示
};
</script>

<style scoped>
/* 对话框的样式如上所示 */
</style>

步骤二:在 main.js 中全局注册


在 main.js 文件中,导入 MyDialog 并全局注册这个组件:


import Vue from 'vue';
import MyDialog from './components/MyDialog.vue';

Vue.component('my-dialog', MyDialog);

步骤三:在父组件中使用 .sync 修饰符


要想让 MyDialog 组件的显示状态能够通过父组件控制,但不需要在每个父组件中定义方法来关闭对话框,我们可以使用 Vue 的 .sync 修饰符。


在父组件中,你可以这样使用 MyDialog 组件:


<template>
<my-dialog :title="'自定义对话框'" :visible.sync="dialogVisible">
<!-- 对话框的内容 -->
</my-dialog>
</template>

<script>
export default {
data() {
return {
dialogVisible: false
};
}
// 无需定义关闭对话框的方法
};
</script>

理解 .sync 修饰符


.sync 是 Vue 2 中的一个定制的语法糖,用于创建双向绑定。通常来说,Vue 使用单向数据流,即数据的变化是单向传播的。通过 .sync,子组件可以通过事件更新父组件的状态,使数据的变更成为双向的。


当你在父组件的一个属性上加了 .sync 时,实际上 Vue 会自动更新父组件的状态,当子组件触发了一个特定命名形式的事件(update:xxx)时。


示例:MyDialog 组件


在 MyDialog 组件中,当用户点击关闭按钮时,组件需要通知父组件更新visible属性。这可以通过在子组件内部触发 update:visible 事件来实现:


methods: {
handleClose() {
this.$emit('update:visible', false);
}
}

结论


通过正确使用 Vue 的 .sync 修饰符和事件系统,我们可以轻松地封装和使用类似于 el-dialog 的全局对话框组件,而无需在每个使用它的父组件中定义关闭方法。这种方式使代码更加干净、可维护,并遵循 Vue 的设计原则。


以这种方法,你可以增强组件的可重用性与可维护性,同时学习到 Vue.js 高级组件通信的实用技巧。


注意:在 Vue 3 中,.sync 已经被废弃。相同功能可以通过 v-model 或其它定制的事件和属性实现。所以,确保你的实现与你使用的 Vue 版本相一致。


作者:超级vip管理员
来源:juejin.cn/post/7314493016497635368
收起阅读 »

技术资讯:Firefox浏览器即将被淘汰?

大家好,我是大澈! 本文约1200+字,整篇阅读大约需要2分钟。 1. 资讯速览 最近,我在网上看到一篇文章,文章说的是Firefox 正处于危险边缘,可能很快就会被淘汰。 当时看到这句话,我感到非常震惊,曾经三巨头之一的火狐浏览器,怎么突然就会被淘...
继续阅读 »

大家好,我是大澈!


本文约1200+字,整篇阅读大约需要2分钟。


1. 资讯速览


最近,我在网上看到一篇文章,文章说的是Firefox 正处于危险边缘,可能很快就会被淘汰。


当时看到这句话,我感到非常震惊,曾经三巨头之一的火狐浏览器,怎么突然就会被淘汰了呢?


还记得当年,火狐算是我除了一些国产浏览器和IE之外,最早使用的浏览器了。最初的印象,就是一个简洁且充满高级感的小狐狸,支持很多好用的音视频插件,并且可以在手机和电脑上同步使用,很方便!


图片


文章中提到,根据美国政府网站的开发指南,如果 Firefox 的市场份额低于 2%,那么美国政府的网站可以不再兼容 Firefox。


而在过去90天,访问美国政府网站的浏览器中 Chrome 占 49%,Safari 占 34.8%,Edge 8.5%,Firefox 只有 2.2 %—— 已经非常接近临界点。


如果 Firefox 失去了美国政府网站的支持,会影响到无数企业,就像多米诺骨牌倒下一样,会导致 Firefox 一点点走向被淘汰的边缘。


图片


2. 资讯详述


确实,不知道从什么时候开始,我们Web开发人员现在都已普及使用了谷歌浏览器,以及系统自带的Edge和Safari,慢慢忘记了Firefox


2.1 Firefox流量一直呈下降趋势


先看一组文章中提供的流量趋势图:


图片


如图所示,Firefox 流量在 2009 年 11 月达到了 31.82% 的峰值,然后开始长期下滑,几乎与 Chrome 的崛起成正比。


谷歌的使用率从 2009 年 1 月的 1.37% 飙升至 2020 年 9 月的峰值 66.34%,此后又回落至最新数据中“微不足道”的 62.85%


很鲜明的对比,很震惊!


虽然这些数字反映了全球趋势,但美国的具体情况并没有真正更好。事实上,由于 iPhone 在美国非常受欢迎,Safari 吸引了大量用户,这也损害了 Firefox。


其实,在国内也是如此。


2.2 为什么Firefox会被Chrome超越


我觉得,有两个重要因素导致 Chrome 超过 Firefox:


兼容性和开发者支持:Chrome 在过去几年中积极推动 Web 标准和新技术的发展,并得到了许多开发者的支持。一些网站和 Web 应用程序可能更倾向于在 Chrome 上进行优化和测试,导致在 Chrome 中获得更好的性能和用户体验。


公司强大和平台支持:Chrome 作为强大Google公司的产品,且广泛支持不同的操作系统和设备。Chrome 在 Windows、macOS、Linux 以及移动设备上都有可用的版本,相比之下,Firefox 的市场份额在移动设备上相对较小。


图片


2.3 怎么正确去看待此事


Firefox 是由 Mozilla 组织维护和开发。Mozilla 组织致力于推动 Web 标准、隐私保护和开放性,Firefox 也提供了一些独特的功能和扩展,以满足用户的需求。


虽然 Chrome 浏览器在市场份额上比 Firefox 更具领先优势,但 Firefox 仍然在许多方面保持着竞争力。


所以,不能一味的去说,Firefox浏览器即将被淘汰了。作为工具而言,我们仍可以因为某些方面,坚定放心地去选择使用Firefox。


或许,这也是对过去做个交代吧。


结语


建立这个平台的初衷:



  • 打造一个专注于前端功能问题的问答平台,让大家高效搜索处理同样问题。

  • 通过不断积累问题,一起练习逻辑思维,并顺便学习相关知识点。

  • 遇到有共鸣的问题,与众多同行朋友们一起讨论,一起沉淀成长。

  • 为了给功能问题专栏添加乐趣,增设技术资讯、实用干货两个新专栏。

作者:程序员大澈
来源:juejin.cn/post/7314558335818465307
收起阅读 »

Java中“100==100”为true,而"1000==1000"为false?

前言 今天跟大家聊一个有趣的话题,在Java中两个Integer对象做比较时,会产生意想不到的结果。 例如: Integer a = 100; Integer b = 100; System.out....
继续阅读 »

前言


今天跟大家聊一个有趣的话题,在Java中两个Integer对象做比较时,会产生意想不到的结果。


例如:


Integer a = 100;
Integer b = 100;
System.out.println(a==b);

其运行结果是:true。


而如果改成下面这样:


Integer a = 1000;
Integer b = 1000;
System.out.println(a==b);

其运行结果是:false。


看到这里,懵了没有?


为什么会产生这样的结果呢?


1 Integer对象


上面例子中的a和b,是两个Integer对象。


而非Java中的8种基本类型。


8种基本类型包括:



  • byte

  • short

  • int

  • long

  • float

  • double

  • boolean

  • char


Integer其实是int的包装类型。


在Java中,除了上面的这8种类型,其他的类型都是对象,保存的是引用,而非数据本身。


Integer a = 1000;
Integer b = 1000;

可能有些人认为是下面的简写:


Integer a = new Integer(1000);
Integer b = new Integer(1000);

这个想法表面上看起来是对的,但实际上有问题。


在JVM中的内存分布情况是下面这样的:图片在栈中创建了两个局部变量a和b,同时在堆上new了两块内存区域,他们存放的值都是1000。


变量a的引用指向第一个1000的地址。


而变量b的引用指向第二个1000的地址。


很显然变量a和b的引用不相等。


既然两个Integer对象用==号,比较的是引用是否相等,但下面的这个例子为什么又会返回true呢?


Integer a = 100;
Integer b = 100;
System.out.println(a==b);

不应该也返回false吗?


对象a和b的引用不一样。


Integer a = 1000;
Integer b = 1000;

其实正确的简写是下面这样的:


Integer a = Integer.valueOf(1000);
Integer b = Integer.valueOf(1000);

在定义对象a和b时,Java自动调用了Integer.valueOf将数字封装成对象。图片而如果数字在low和high之间的话,是直接从IntegerCache缓存中获取的数据。


图片Integer类的内部,将-128~127之间的数字缓存起来了。


也就是说,如果数字在-128~127,是直接从缓存中获取的Integer对象。如果数字超过了这个范围,则是new出来的新对象。


文章示例中的1000,超出了-128~127的范围,所以对象a和b的引用指向了两个不同的地址。


而示例中的100,在-128~127的范围内,对象a和b的引用指向了同一个地址。


所以会产生文章开头的运行结果。


为什么Integer类会加这个缓存呢?


答:-128~127是使用最频繁的数字,如果不做缓存,会在内存中产生大量指向相同数据的对象,有点浪费内存空间。


Integer a = 1000;
Integer b = 1000;

如果想要上面的对象a和b相等,我们该怎么判断呢?


2 判断相等


在Java中,如果使用==号比较两个对象是否相等,比如:a==b,其实比较的是两个对象的引用是否相等。


很显然变量a和b的引用,指向的是两个不同的地址,引用肯定是不相等的。


因此下面的执行结果是:false。


Integer a =  Integer.valueOf(1000);
Integer b = Integer.valueOf(1000);
System.out.println(a==b);

由于1000在Integer缓存的范围之外,因此上面的代码最终会变成这样:


Integer a =  new Integer(1000);
Integer b = new Integer(1000);
System.out.println(a==b);

如果想要a和b比较时返回true,该怎么办呢?


答:调用equals方法。


代码改成这样的:


Integer a = Integer.valueOf(1000);
Integer b = Integer.valueOf(1000);
System.out.println(a.equals(b));

执行结果是:true。


其实equals方法是Object类的方法,所有对象都有这个方法。图片它的底层也是用的==号判断两个Object类型的对象是否相等。


不过Integer类对该方法进行了重写:图片它的底层会先调用Integer类的intValue方法获取int类型的数据,然后再通过==号进行比较。


此时,比较的不是两个对象的引用是否相等,而且比较的具体的数据是否相等。


我们使用equals方法,可以判断两个Integer对象的值是否相等,而不是判断引用是否相等。


最近我建了新的技术交流群,打算将它打造成高质量的活跃群,欢迎小伙伴们加入。


总结


Integer类中有缓存,范围是:-128~127


Integer a = 1000;

其实默认调用了Integer.valueOf方法,将数字转换成Integer类型:


Integer a = Integer.valueOf(1000);

如果数字在-128~127之间,则直接从缓存中获取Integer对象。


如果数字在-128~127之外,则该方法会new一个新的Integer对象。


我们在判断两个对象是否相等时,一定要多注意:



  1. 判断两个对象的引用是否相等,用==号判断。

  2. 判断两个对象的值是否相等,调用equals方法判断。


作者:苏三说技术
来源:juejin.cn/post/7314365638557777930
收起阅读 »

主管让我说说 qiankun 是咋回事😶

web
前言 最近乙方要移交给我们开发的一个项目的代码,其中前端用到了 qiankun 微前端技术,因为第一版代码之前让我看过,写过基础开发文档,然后主管昨天就找我问了一下,本来以为就是问下具体概念和开发,没想到问起了是怎么实现的🥲,之前了解 qiankun 也就是看...
继续阅读 »

前言


最近乙方要移交给我们开发的一个项目的代码,其中前端用到了 qiankun 微前端技术,因为第一版代码之前让我看过,写过基础开发文档,然后主管昨天就找我问了一下,本来以为就是问下具体概念和开发,没想到问起了是怎么实现的🥲,之前了解 qiankun 也就是看了下开发配置,并没有去关注具体实现,一下子给我难住了。后面又给我留下了几个问题,让我去了解了解,琢磨琢磨,这篇文章就是记一下自己 search 到的一些知识和自己的理解,可能有很多问题,期待JY们指正。


QA


Q:父应用和子应用可以在不同的nginx上吗?


A:可以,父子应用既可以在同一个nginx也可以在不同的nginx上。


Q:从SLB过来的请求是先到父应用再路由到子应用?


A:不是,父应用在运行时,通过 fetch 拿到子应用的 html 文件上的 js、css 依赖(import-html-entry),划出一个独立容器(sandbox)运行子应用,所有子应用都是运行在父应用这个基座上的“应用级组件”,子应用成为了父应用的一部分,子应用中配置的代理不会生效,父子应用共享同一个网络环境,都运行在同一个IP上,请求都从同一个IP发出,子应用的所有网络请求都通过父应用配置的代理转发。


Q:父应用和子应用通信?(是不是通过网络通信)


A:qiankun的父子应用通信不是通过网络通信。


父子应用通信是直接通过浏览器存储或者内存等,例如路由的 query、localStorage、eventBus 或者qiankun提供的全局状态管理工具都可以管理。


子应用挂载时,也可以类似React组件通过props传递具体数据和父应用中改变数据的函数,也可以传递一个全局状态,其包含变量修改和监听变化的函数,父子应用都可以监听变量的变化和修改变量。


Nginx配置


父应用上的 nginx 配置类似本地文件中的 proxy 代理配置,在父应用上分别配置每个子应用的 html 文件所在的地址(资源代理),和子应用的后端接口地址(请求代理)。


export default {
"/root-app": {
target: "https://xxx.xxx.com:xxxx/",
changeOrigin: true,
},

// child1
// 资源代理
"/child1/": {
target: "https://xxx.xxx.com:xxxx/",
changeOrigin: true,
},
// 接口代理
"/child1-api/": {
target: "https://xxx.xxx.com:xxxx/",
changeOrigin: true,
},
// ......
};

不允许主应用跨域访问微应用,做法就是将主应用服务器上一个特殊路径的请求全部转发到微应用的服务器上,即通过代理实现“微应用部署在主应用服务器上”的效果。


例如,主应用在 A 服务器,微应用在 B 服务器,使用路径 /app1 来区分微应用,即 A 服务器上所有 /app1 开头的请求都转发到 B 服务器上。此时主应用的 Nginx 代理配置为:


/app1/ {
proxy_pass http://www.b.com/app1/;
proxy_set_header Host $host:$server_port;
}

演示图


资源文件


从子应用 html 上解析出 js 和 css 加载到父应用基座未命名文件 (2).png


网络请求


未命名文件 (1).png


核心


应用的加载


qiankun 的一个重要的依赖库 import-html-entry ,其功能是主应用拉取子应用 html 中的 js 和 css 文件并加载到父应用基座,css 嵌入到 html,js放在内存中在适当时机 eval 运行


应用的隔离与通信


通过 sandbox 进行 js 和 css 隔离。


js 隔离

js 隔离通过给全局 window 一个 proxy 包裹传递进来,子应用的 js 运行在 proxy 上,子应用卸载时,proxy 跟着清除,这样避免了污染真正的 window,另外对于不支持 proxy 的浏览器,没有 polyfill 方案,qiankun 采用 snapshot 快照方案,保存子应用挂载前的 window 状态,在子应用卸载时,恢复到挂载前的状态,但这种解决方案无法处理基座上同时挂载多个子应用的情景;


css 隔离

css 隔离通过 shadowdom,将子应用的根节点挂载到 shadowdom 中,shadowdom 内部的样式并不会影响全局样式,但是有个缺点,很多组件库的类似弹窗提醒组件会把 dom 提升到顶层,这样注定会污染到全局的样式;


qiankun 的一个实验性解决方案,类似 vue 的 scoped 方案/css-module,给子应用的 css 变量装饰一下(一般是hash),这样避免来避免子应用的样式污染到全局。


彻底解决:约定主子应用完全使用不同的 css 命名; react 的 css-in-js 方案;使用 postcss 全局加变量;全部写 tailwindcss ......


通信

父子应用通信是直接通过浏览器存储或者内存等,例如路由的 query、localStorage、eventBus 或者qiankun提供的全局状态管理工具都可以管理,简单来说就是全局变量。


子应用挂载时,也可以类似React组件通过props传递具体数据和父应用中改变数据的函数,也可以传递一个全局状态,其包含变量修改和监听变化的函数,父子应用都可以监听变量的变化和修改变量。


理解


子应用是可以独立开发、独立部署、独立运行的应用,但在父应用上并不是“独立”运行,而是父应用通过网络动态 fetch 到子应用的 html 文件,然后解析出 html 上的 js 和 css 依赖,处理后加载到父应用基座,将子应用作为自己的一个特殊组件加载渲染到一个“独立沙箱容器”中。


问题



  • 多应用模块共享、代码复用问题没有解决。父子应用如果存在相同依赖,在子应用加载时,是不是还是会去重新加载一遍?

  • 子应用 css 隔离仍存在问题,不支持 proxy 的浏览器无法支持多个子应用同时加载的情形;

  • 当前项目是否真的大到需要使用微前端来增加开发和维护复杂度;

  • 根据我的搜索,qiankun 对于 vite 构建的项目支持度貌似不够,而我们最新项目基本都是通过 vite 构建,可能会有问题。


作者:HyaCinth
来源:juejin.cn/post/7314196310647423039
收起阅读 »

寒冬,拒绝薪资倒挂

写在前面 今天翻看小 🍠 的时候,无意发现两组有趣数据: 一个是,互联网大厂月薪分布: 另一个是,国内互联网大厂历年校招薪资与福利汇总: 中概互联网的在金融市场的拐点。 是在 2021 年,老美出台《外国公司问责法案》开始的。 那时候,所有在美上市的中概...
继续阅读 »

写在前面


今天翻看小 🍠 的时候,无意发现两组有趣数据:


一个是,互联网大厂月薪分布:


月薪分布


另一个是,国内互联网大厂历年校招薪资与福利汇总:


研发


算法


中概互联网的在金融市场的拐点。


是在 2021 年,老美出台《外国公司问责法案》开始的。


那时候,所有在美上市的中概股面临摘牌退市,滴滴上市也被叫停。



拐点从资本市场反映到劳动招聘市场,是有滞后性的,如果没有 ChatGPT 的崛起,可能寒冬还会来得更凛冽些 ...


时代洪流的走向,我们无法左右,能够把握的,只有做好自己。


如何在寒冬来之不易的机会中,谈好待遇,拒绝薪资倒挂 🙅🏻‍♀️🙅


一方面:减少信息差,在谈判的中后期,多到职场类社区论坛(牛客/小红书/脉脉/offershow)中,了解情况


另一方面:增加自身竞争力,所有技巧在绝对实力面前,都不堪一击,如果能在笔面阶段,和其他候选人拉开足够差距,或许在后续博弈中,需要知道的套路就会越少


增强自身竞争力,尤其是走校招路线的小伙伴,建议从「算法」方面进行入手。


下面给大家分享一道常年在「字节跳动」题库中霸榜的经典题。


题目描述


平台:LeetCode


题号:25


给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。


k 是一个正整数,它的值小于或等于链表的长度。


如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。


示例 1:


输入:head = [1,2,3,4,5], k = 2

输出:[2,1,4,3,5]

示例 2:
img


输入:head = [1,2,3,4,5], k = 3

输出:[3,2,1,4,5]

提示:



  • 列表中节点的数量在范围 sz

  • 1<=sz<=50001 <= sz <= 5000

  • 0<=Node.val<=10000 <= Node.val <= 1000

  • 1<=k<=sz1 <= k <= sz


进阶:



  • 你可以设计一个只使用常数额外空间的算法来解决此问题吗?

  • 你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。


迭代(哨兵技巧)


哨兵技巧我们在前面的多道链表题讲过,让三叶来帮你回忆一下:


做有关链表的题目,有个常用技巧:添加一个虚拟头结点(哨兵),帮助简化边界情况的判断。


链表和树的题目天然适合使用递归来做。


但这次我们先将简单的「递归版本」放一放,先搞清楚迭代版本该如何实现。


我们可以设计一个翻转函数 reverse


传入节点 root 作为参数,函数的作用是将以 root 为起点的 kk 个节点进行翻转。


当以 root 为起点的长度为 kk 的一段翻转完成后,再将下一个起始节点传入,直到整条链表都被处理完成。


当然,在 reverse 函数真正执行翻转前,需要先确保节点 root 后面至少有 kk 个节点。


我们可以结合图解再来体会一下这个过程:


假设当前样例为 1->2->3->4->5->6->7k = 3
640.png


然后我们调用 reverse(cur, k),在 reverse() 方法内部,几个指针的指向如图所示,会通过先判断 cur 是否为空,从而确定是否有足够的节点进行翻转:


然后先通过 while 循环,将中间的数量为 k - 1 的 next 指针进行翻转:


最后再处理一下局部的头结点和尾结点,这样一次 reverse(cur, k) 执行就结束了:


回到主方法,将 cur 往前移动 k 步,再调用 reverse(cur, k) 实现 k 个一组翻转:


Java 代码:


class Solution {
public ListNode reverseKGr0up(ListNode head, int k) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode cur = dummy;
while (cur != null) {
reverse(cur, k);
int u = k;
while (u-- > 0 && cur != null) cur = cur.next;
}
return dummy.next;
}
// reverse 的作用是将 root 后面的 k 个节点进行翻转
void reverse(ListNode root, int k) {
// 检查 root 后面是否有 k 个节点
int u = k;
ListNode cur = root;
while (u-- > 0 && cur != null) cur = cur.next;
if (cur == null) return;
// 进行翻转
ListNode tail = cur.next;
ListNode a = root.next, b = a.next;
// 当需要翻转 k 个节点时,中间有 k - 1 个 next 指针需要翻转
while (k-- > 1) {
ListNode c = b.next;
b.next = a;
a = b;
b = c;
}
root.next.next = tail;
root.next = a;
}
}

C++ 代码:


class Solution {
public:
ListNode* reverseKGr0up(ListNode* head, int k) {
ListNode* dummy = new ListNode(-1);
dummy->next = head;
ListNode* cur = dummy;
while (cur != NULL) {
reverse(cur, k);
int u = k;
while (u-- > 0 && cur != NULL) cur = cur->next;
}
return dummy->next;
}
// reverse 的作用是将 root 后面的 k 个节点进行翻转
void reverse(ListNode* root, int k) {
// 检查 root 后面是否有 k 个节点
int u = k;
ListNode* cur = root;
while (u-- > 0 && cur != NULL) cur = cur->next;
if (cur == NULL) return;
// 进行翻转
ListNode* tail = cur->next;
ListNode* a = root->next, *b = a->next;
// 当需要翻转 k 个节点时,中间有 k - 1 个 next 指针需要翻转
while (k-- > 1) {
ListNode* c = b->next;
b->next = a;
a = b;
b = c;
}
root->next->next = tail;
root->next = a;
}
};

Python 代码:


class Solution:
def reverseKGr0up(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
# reverse 的作用是将 root 后面的 k 个节点进行翻转
def reverse(root, k):
# 检查 root 后面是否有 k 个节点
u, cur = k, root
while u > 0 and cur:
cur = cur.next
u -= 1
if not cur: return
# 进行翻转
tail = cur.next
a, b = root.next, root.next.next
# 当需要翻转 k 个节点时,中间有 k - 1 个 next 指针需要翻转
while k > 1:
c, b.next = b.next, a
a, b = b, c
k -= 1
root.next.next = tail
root.next = a

dummy = ListNode(-1)
dummy.next = head
cur = dummy
while cur:
reverse(cur, k)
u = k
while u > 0 and cur:
cur = cur.next
u -= 1
return dummy.next

TypeScript 代码:


function reverseKGr0up(head: ListNode | null, k: number): ListNode | null {
// reverse 的作用是将 root 后面的 k 个节点进行翻转
const reverse = function(root: ListNode | null, k: number): void {
// 检查 root 后面是否有 k 个节点
let u = k, cur = root;
while (u-- > 0 && cur != null) cur = cur.next;
if (cur == null) return;
// 进行翻转
let tail = cur.next, a = root.next, b = a.next;
// 当需要翻转 k 个节点时,中间有 k - 1 个 next 指针需要翻转
while (k-- > 1) {
let c = b.next;
b.next = a;
a = b;
b = c;
}
root.next.next = tail;
root.next = a;
};
let dummy = new ListNode(-1);
dummy.next = head;
let cur = dummy;
while (cur != null) {
reverse(cur, k);
let u = k;
while (u-- > 0 && cur != null) cur = cur.next;
}
return dummy.next;
};


  • 时间复杂度:会将每个节点处理一遍。复杂度为 O(n)O(n)

  • 空间复杂度:O(1)O(1)


递归


搞懂了较难的「迭代哨兵」版本之后,常规的「递归无哨兵」版本写起来应该更加容易了。


需要注意的是,当我们不使用「哨兵」时,检查是否足够 kk 位,只需要检查是否有 k1k - 1nextnext 指针即可。


代码:


class Solution {
public ListNode reverseKGr0up(ListNode head, int k) {
int u = k;
ListNode p = head;
while (p != null && u-- > 1) p = p.next;
if (p == null) return head;
ListNode tail = head;
ListNode prev = head, cur = prev.next;
u = k;
while (u-- > 1) {
ListNode tmp = cur.next;
cur.next = prev;
prev = cur;
cur = tmp;
}
tail.next = reverseKGr0up(cur, k);
return prev;
}
}

C++ 代码:


class Solution {
public:
ListNode* reverseKGr0up(ListNode* head, int k) {
int u = k;
ListNode* p = head;
while (p != NULL && u-- > 1) p = p->next;
if (p == NULL) return head;
ListNode* tail = head;
ListNode* prev = head, *cur = prev->next;
u = k;
while (u-- > 1) {
ListNode* tmp = cur->next;
cur->next = prev;
prev = cur;
cur = tmp;
}
tail->next = reverseKGr0up(cur, k);
return prev;
}
};

Python 代码:


class Solution:
def reverseKGr0up(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
u = k
p = head
while p and u > 1:
p = p.next
u -= 1
if not p: return head

tail = prev = head
cur = prev.next
u = k
while u > 1:
tmp = cur.next
cur.next = prev
prev, cur = cur, tmp
u -= 1
tail.next = self.reverseKGr0up(cur, k)
return prev

TypeScript 代码:


function reverseKGr0up(head: ListNode | null, k: number): ListNode | null {
let u = k;
let p = head;
while (p != null && u-- > 1) p = p.next;
if (p == null) return head;
let tail = head, prev = head, cur = prev.next;
u = k;
while (u-- > 1) {
let tmp = cur.next;
cur.next = prev;
prev = cur;
cur = tmp;
}
tail.next = reverseKGr0up(cur, k);
return prev;
};


  • 时间复杂度:会将每个节点处理一遍。复杂度为 O(n)O(n)

  • 空间复杂度:只有忽略递归带来的空间开销才是 O(1)O(1)


更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉


作者:宫水三叶的刷题日记
来源:juejin.cn/post/7314263159116628009
收起阅读 »

🙁 分手、黑中介、中毒、亲人接连去世、丢失爱串、边缘化|2023年终总结

❄ 2023 总结 今年,是目前为止的我人生中最黑暗的一年。 我被边缘了 今年1月份,公司整体进行组织架构调整,所有人全部打乱,部门全部重新划分(我们公司每年都会进行架构调整,一般都是微调,但是今年年初是一次整体的大规模调整)。 最后我被划分到了一个偏业务...
继续阅读 »

image.png


❄ 2023 总结



今年,是目前为止的我人生中最黑暗的一年。



我被边缘了


今年1月份,公司整体进行组织架构调整,所有人全部打乱,部门全部重新划分(我们公司每年都会进行架构调整,一般都是微调,但是今年年初是一次整体的大规模调整)。


最后我被划分到了一个偏业务的部门。这么说吧,就好比在饭店当司机,其实压根这个部门基本不需要开发,却分配了两个开发。而有的部门需要开发却一个开发没有。


这次架构调整相当于被“边缘化”了,让我看看谁是倒霉蛋?哦,是我啊🤡


image.png

换完部门其实我就想跑路了,但是考虑到我工作三年每次都是一年一跳(其实真的就是每次工作内容太捞了,迫不得已跑路),对我的履历不太好,再加上现在形势不好,所以还是觉得先干着坐等下次架构调整😷


炸裂分手


对,你没看错 “炸裂分手”。毫不夸张,谈了三年多将近4年,明白一个道理:“不合适的两个人,就算走到了一起,结果注定是无趣或分离”


和一般的情侣和平分手、或者闹矛盾分手不同。我们是吵得不可开交,然后冷战默契分手。如今如同仇人一般。


image.png

即便如此,我对她的感情还是非常深的,从小到大我没有失眠过,自从今年分手后,太多次无法入睡,脑子胡思乱想。无数次想过找她复合,但是我是比较偏理性的人(MBTI是ISTJ),我慎重考虑,即使复合可能依然解决不了不合适的问题。


所以。放下吧,绕过她也饶过我...


image.png
image.png

(ps:jym给我介绍个对象🤪,详情看沸点以前发的相亲贴https://juejin.cn/pin/7248502542996275259


image.png

亲人接连去世


今年真的是诸事不顺,4月份的时候,我大舅去世了,原因是村里停电了...


我大舅患有肺病,一直都随身携带呼吸机。好几年了,听我妈说,看x光片,可以看到整个肺上全是孔,想想就让人心疼。


今年过年的时候我还和大舅单独聊了会儿,看着他说话有气无力,插着呼吸机的样子,哎...


4月份,突然看到我妈在我家群里说,大舅去世了,如晴天霹雳。因为村里停电了,呼吸机无法工作...


我妈这边的亲戚,我大舅是第一个去世的...


我大舅出殡完的一个礼拜后,只一个礼拜


我家群里再次发消息,我二大爷去世了...


遗憾的是,在我大舅去世那几天,我回去之后请客几个离得近的亲戚吃饭,我二大爷说他下午上班中午要睡觉所以没来。


最后一面没有见到...


其实有很多细节,我不想过于赘述,也不方便在网上诉说。


黑中介我***


来北京三年,一年一换房。


前两年一直都很顺利,今年换房可算是倒了大霉。


image.png

我先分享下我的换房小技巧😃,瞅瞅有没有志同道合的小伙伴:



  • 打开北京地铁线路图,然后根据公司所在的线路,我最多接受倒两趟地铁。挑选几个离的不是很远,并且又相对比较偏,房价可能比较低的地铁站;

  • 然后在地图上放大看每个地铁站附近有哪些小区,拿个小本本记上;

  • 然后在安居客上分别搜索这些小区看看价位,只是看看价位,全tn是假图。

  • 挑选几个感觉性价比高的小区加几个中介,然后就让他带着看指定小区的房源;


我的秘籍倾囊相授了,换你个点赞不过分吧哈哈


回归正题,我按照我的套路找到个小区,然后中介带我看了一个半小时房源,我挑了两间有点犹豫,然后我说我回去考虑好了告诉他,下午给他答复(此时1点40)。


黑中介:“你先把钱付了,你回去慢慢想,下午三点前给我个答复,我好给你留房”


image.png

本来我坚决不同意,说我先考虑好再付钱,架不住这个🐶一直狂吠,诱导我先交钱。然后我就真先把钱付了,然后我按照约定3点前告诉他选择了哪间。他也及时给我回复了“OK”。


于是我安心打起了游戏。四点半的时候,黑中介打电话过来告诉我房被别人定了...


image.png

我都不用脑子想就知道,这比绝对没有在回复我后就在软件上给我定房,导致被别人抢先了,不然不可能过了一个半小时才回我。


黑中介:“这个房子虽然被抢了,但是钱不能退你,因为你本来是要定的,这间没了你就得选另一间”


好好好,这么玩是吧,你就活吧,谁能活得过你啊🤬


真是青蛙喝茶叶————你也算个人?


我尼玛...于是我开始了讨还定金之路


110报警,查他个人信息、公司信息威慑他、不断给他的三个手机号打电话以及微信电话骚扰他、下班线下逮捕他拖住他电瓶车不让他走影响他工作。我可谓是无所不用其极,能用的招全使上了,最后好歹退了我一半多点...


详细过程比这恶心多了,感兴趣的可以翻看我以前发的沸点juejin.cn/pin/7249925…


我找好新房子之后,去退我的旧房子。中介又以“不续租需要提前15天告知”为由妄想不退我押金。但是在15天前我问过他什么时候到期,他不告诉我。。。然后我自己查的合同。不过我拖住他好说歹说终于退还了我押金,扣了200卫生费。


食物中毒


好像是八月份吧,我和朋友去吃“鲜辣道鱼火锅”。刚吃完放下筷子,坐了不到两分钟,我突然一瞬间就想吐,真的是一瞬间。因为在店里,吐出来就当场社死,我拼尽全力憋到眼泪都出来了都难以控制,然后我抓紧出去准备上商场卫生间去吐,还好走了一路,到了卫生间缓和了很多,没有吐出来。


之后一下午我都处于眩晕、反胃的状态,我朋友也是,而且他还拉了两次肚子(当天我没拉)。因为当时在店里的时候他还没什么症状,我以为是我自己的事,所以就走了。


重磅的来了,第二天一早,我俩都开始疯狂拉肚子,都给我拉虚脱了,化身喷射战士,抓紧买了蒙脱石散才平息了战斗。而我的朋友,比我更加严重,硬是拉了一早上...


image.png

我朋友中午去了医院,检查只是肠胃炎。还好不是真食物中毒,估计就是食材不新鲜...


我两次和商家沟通索要赔偿,但是商家连饭钱都不给退,只同意把就医的钱退了,本来我准备周末上门索要赔偿(上门的话我估计99%会赔偿,因为毕竟是个连锁店,在店里面闹起来对他们影响不好)。但是周末我朋友有事回家了,而我又离那个店比较远,地铁两小时。我是个懒比,自己一个人懒得跑了,所以这件事就这么过去了...


image.png

对了,我在315上面投诉了他们店,但是没有通过,忘记原因了,好像是投诉的店不归他们管。


痛失爱串


成年男人三大爱好:抽烟喝酒盘串,前两个就算了,今年培养了个新爱好:盘串😄。


我先后一共买了三串:紫金鼠、老型猴头、菩提根。


其中我最爱的就是我的猴头,其他俩我都基本没咋盘过,但是猴头是整天不离手,工作、地铁、吃饭都在盘玩。


不过我主打一个随心,没那么讲究,所以前期被我盘花了,但是它仍然是我的爱串。


悲痛的是,周末去滑了个雪,把我盘了半年的爱串给弄丢了(我估计可能是还设备的时候脱手套给带下来了没注意,但是坐的团队的车,我也没时间去找了😭),啊啊啊啊,我的宝~下面就是它的最后一张遗照了😭😭😭


WechatIMG945.jpeg

我还想着过了年给它上个小配饰呢😭


Completed && unfinished


这一年浑浑噩噩,太多负能量的事让我一直处于不太有冲劲的状态。总结这一年,感觉浪费了好多时间,并没有完成太多自己定下的目标。


Completed



  • 从c站来到掘金,持续发文。参加了码上掘金掘金比赛、创意更文比赛获奖;


1702275665490.jpg

  • 第一次开始接了几个外包项目。战线太长,好累,甲方程序员是真能装逼🤢,我真想哐哐给他两拳;

  • 参与蓝桥杯出题,不过目前题目还在调整中;

  • 实践了一些技术;

  • 接触AI,参与了几个AI项目,玩了玩AIGC。现在都离不开gpt了🤤,以前有问题先想百度谷歌,现在直接gpt,启动!

  • 看完了红楼梦,感触颇深;

  • 打了这么多年游戏,终于打了个国标;


111.png

  • 培养了两个新爱好:盘串、滑雪。


unfinished



  • 本来想申请写小册的,想了个方向和大纲都列好了,但是没审核通过。原因是已经有此类的小册了...

  • 想业余时间跳跳舞,我非常喜欢跳舞,大学跳了三年hiphop。但是太懒了,学了一个简单的舞之后就没再跳了;

  • 找对象未果;

  • 减肥三天打鱼两天晒网。我并不胖,但是由于之前吃了太多零食,上半年竟然查出有轻至中度脂肪肝,所以我想控制饮食加轻锻炼恢复下健康。


⛳ 思考


今年经历了很多事情,引发了我很多思考。无论是工作上,感情上,生活上...我都对自己进行了反思。


对于感情,后期我丧失了太多耐心,可能是谈恋爱前期积攒了太多的怨气(性格三观不合适,我总是不理解她的想法做法)。虽然后面都在慢慢变好,但是经常吵架翻旧账,我总把不合适挂在口头上...

现在想想,依然觉得我们两个确实不合适,但是经过了三年的磨合,其实性格、思想等方面已经慢慢步入一个轨道,或许如果我心平气和一点,看开点,多点耐心,就不会发展到这一步...

我有点遗憾,但也有点庆幸;

遗憾的是有太多美好回忆,并且是校园恋爱,最终没能走到一起;

庆幸的是不合适的人如果最终步入婚姻,依然会有很多矛盾,两个人都过的不舒服,而这些只有分开了才能想明白。


对于亲戚去世,今年只是个开始,因为我的家族是个大家庭,这么说吧,我妈这边亲戚以前拍了张全家福,上面有将近70个人。

我们家族不仅人多,而且非常和谐,所有亲戚关系都特别好,每年无论多忙都必须聚一聚。

既享受了大家庭的美满,也得迎来亲人们的迟暮。

今年我第一次看了红楼梦,感受到了那种盛极而衰的凄凉。

“可见世上万般,好便是了,了便是好”


对于工作,今年国庆假期和几个社会上打拼的朋友吃饭,听他们说他们的故事。因为我属于比较内向的人,工作中也不会表现自己,经常就是自己做了很多优化却没人知道😂

我朋友就教育我让我学点人情世故,不能太死板。我听着有些许道理。他们还教我国庆假期回去给领导带点礼物,平时节日嘘寒问暖一下,虽然有点刻意,但大家都是成年人了,懂得都懂~

哈哈哈,这辈子我能学会圆滑吗🤪


今天我有个特别焦虑的点,因为今年过后我就工作三年多了,都说工作3、5、7年是一个分界线,总感觉自己在原地踏步...


🌻 2024 展望


有一位智者说过:“生性乐观的人,懂得在逆境中找到光明;生性悲观的人,却常因愚蠢的叹气,而把光明给吹熄了。当你懂得生活的乐趣,就能享受生命带来的喜悦。”他还告诉我们,“烦恼重的人,芝麻小事都会困住他;想解脱的人,天大的事情都束缚不了他。”



明年,希望一切都会好起来💖



暂且定了几个flag,明年回头看看能完成几个:



  • 涨薪or跳槽

  • 拿到c站博客专家的证书

  • 读2本编程书籍

  • 找一个girlfriend

  • 坚持锻炼

  • 打游戏开麦,生活中尝试和陌生人交流


人总是要进步的,一直原地踏步活着有什么意思呢?你说是吧,彦祖亦菲😄


作者:前端阿彬
来源:juejin.cn/post/7314207903414796299
收起阅读 »

环信IM Android端实现华为推送详细步骤

首先我们要参照华为的官网去完成 以下两个配置都是华为文档为我们提供的1.https://developer.huawei.com/consumer/cn/doc/HMSCore-Guides/android-config-agc-000000105017013...
继续阅读 »

首先我们要参照华为的官网去完成 以下两个配置都是华为文档为我们提供的

1.https://developer.huawei.com/consumer/cn/doc/HMSCore-Guides/android-config-agc-0000001050170137#section19884105518498 

2.https://developer.huawei.com/consumer/cn/doc/HMSCore-Guides/android-integrating-sdk-0000001050040084

3.在环信上传华为的配置信息IM推送上传方式->打开管理后台->进入到即使通讯中上传证书(不是即时推送)




4.信息在华为的:将信息添加到(3)的位置 记得检查下前面的信息是否有存在空格有的话删除掉


5.客户端绑定华为证书 注意:客户端设置的appkey 一定要和上传证书对应key 保持一致




6.客户端导入环信提供HMSPushHelper类 

百度网盘地址:链接: https://pan.baidu.com/s/1EehWKyl3uauB5Z43C5wBbw

提取码: 8888

在环信登录成功以后调用



7.添加HMSPushService



8.清单文件注册华为的appid

<meta-data        android:name="com.huawei.hms.client.appid"        android:value="appid=109911253" />  

参考文档:

环信官方Demo下载:https://www.easemob.com/download/demo

IMGeek社区支持:https://www.imgeek.net/

收起阅读 »

数据库连接神器:JDBC的基本概述、组成及工作原理全解析!

JDBC(Java DataBase Connectivity)是一种用于执行SQL语句的 Java API,是Java和数据库之间的一个桥梁,是一个规范而不是一个实现,能够交给数据库执行SQL语句。在信息化时代,数据库已经成为了存储和管理数据的重要工具。而J...
继续阅读 »

JDBC(Java DataBase Connectivity)是一种用于执行SQL语句的 Java API,是Java和数据库之间的一个桥梁,是一个规范而不是一个实现,能够交给数据库执行SQL语句。

在信息化时代,数据库已经成为了存储和管理数据的重要工具。而Java作为一种广泛使用的编程语言,其与数据库的交互就显得尤为重要。JDBC就是为了解决这个问题而生的。通过JDBC,我们可以在Java程序中轻松地执行SQL语句,实现对数据库的增删改查操作。今天我们就来聊一聊JDBC的相关概念。

一、JDBC简介

概念:

JDBC(Java DataBase Connectivity) :Java数据库连接技术。
具体讲就是通过Java连接广泛的数据库,并对表中数据执行增、删、改、查等操作的技术。如图所示:

Description

本质上,JDBC的作用和图形化客户端的作用相同,都是发送SQL操作数据库。差别在图形化界面的操作是图形化、傻瓜化的,而JDBC则需要通过编码(这时候不要思考JDBC代码怎么写,也不要觉得它有多难)完成图形操作时的效果。

也就是说,JDBC本质上也是一种发送SQL操作数据库的client技术,只不过需要通过Java编码完成。

作用:

  • 通过JDBC技术与数据库进行交互,使用Java语言发送SQL语句到数据库中,可以实现对数据的增、删、改、查等功能,可以更高效、安全的管理数据。
  • JDBC是数据库与Java代码的桥梁(链接)。

二、JDBC的组成

JDBC是由一组用Java语言编写的类和接口组成,主要有驱动管理、Connection接口、Statement接口、ResultSet接口这几个部分。

Description

Connection 接口

定义:在 JDBC 程序中用于代表数据库的连接,是数据库编程中最重要的一个对象,客户端与数据库所有的交互都是通过connection 对象完成的。

Connection conn = DriverManager.getConnection(url,user,password);

常见方法:

  • createStatement() :创建向数据库发送的sql的statement对象。

  • prepareStatement(sql) :创建向数据库发送预编译sql的PrepareSatement对象。

  • prepareCall(sql) :创建执行存储过程的callableStatement对象。(常用)

  • setAutoCommit(boolean autoCommit):设置事务是否自动提交。

//关闭自动提交事务  
setAutoCommit(false);
//关闭后需要手动打开提交事务
  • commit() : 在链接上提交事务。

  • rollback() : 在此链接上回滚事务。

Statement 接口

  • statement:由createStatement创建,用于发送简单的SQL语句(不带参数)。
Statement st = conn.createStatement();
  • PreparedStatement :继承自Statement接口,是Statement的子类,可发送含有参数的SQL语句。效率更高,并且可以防止SQL注入,建议使用。

  • PreparedStatement ps = conn.prepareStatement(sql语句);

PreparedStatement 的优势:
Statement会使数据库频繁编译SQL,可能造成数据库缓冲区溢出。PreparedStatement 可对SQL进行预编译,从而提高数据库的执行效率。
并且PreperedStatement对于sql中的参数,允许使用占位符的形式进行替换,简化sql语句的编写,可以避免SQL注入的问题。

  • CallableStatement:继承自PreparedStatement接口,由方法 prepareCall创建,用于调用存储过程。

常见方法:

  • executeQuery(String sql) :用于向数据发送查询语句。

  • executeUpdate(String sql) :用于向数据库发送insert、update或delete语句。

  • execute(String sql):用于向数据库发送任意sql语句。

  • addBatch(String sql):把多条sql语句放到一个批处理中。

  • executeBatch():向数据库发送一批sql语句执行。

ResultSet 接口

ResultSet:用于代表Sql语句的执行结果。

Resultset封装执行结果时,采用的类似于表格的方式,ResultSet 对象维护了一个指向表格数据行的游标,初始的时候,游标在第一行之前,调用ResultSet.next() 方法,可以使游标指向具体的数据行,进行调用方法获取该行的数据。

常用方法:

  • ResultSet.next() :移动到下一行;

  • ResultSet.Previous() :移动到前一行

  • ResultSet.absolute(int row):移动到指定行

  • ResultSet.beforeFirst():移动resultSet的最前面

  • ResultSet.afterLast():移动resultSet的最后面

在这里给大家分享一下【云端源想】学习平台,无论你是初学者还是有经验的开发者,这里都有你需要的一切。包含课程视频、在线书籍、在线编程、一对一咨询等等,现在功能全部是免费的,

点击这里,立即开始你的学习之旅!

三、JDBC的工作原理

JDBC的工作原理可以分为以下几个步骤:

Description

1、加载并注册JDBC驱动:
这是建立数据库连接的第一步,我们需要先加载JDBC驱动,然后通过DriverManager的registerDriver方法进行注册。

2、建立数据库连接:
通过DriverManager的getConnection方法,我们可以建立与数据库的连接。

3、创建Statement对象:
通过Connection对象的createStatement方法,我们可以创建一个Statement对象,用于执行SQL语句。

4、执行SQL语句:
通过Statement对象的executeQuery或executeUpdate方法,我们可以执行SQL语句,获取结果或者更新数据库。

5、处理结果:
对于查询操作,我们需要处理ResultSet结果集;对于更新操作,我们不需要处理结果。

6、关闭资源:
最后,我们需要关闭打开的资源,包括ResultSet、Statement和Connection。

下面,我们来看一个简单的JDBC使用示例。假设我们要查询为"students"的表中的所有数据:

Description

四、面向过程的实现过程

1.在pom.xml中引入mysql的驱动文件

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.32</version>
  1. 加载驱动类
Class.forName("com.mysql.cj.jdbc.Driver");
  1. 建立Java同数据库中间的连接通道
String url = "jdbc:mydql://locallhost:3306/test";//test是数据库名称
String user = "root";
String password = "root";
Connection conn = DriverManager.getConnection(url,user,password);
  1. 产生负责’传递命令’的‘传令官’对象
String sql ="insert into emp values(null,"苏醒","歌手",7956,now(),79429,6799,30,1)";
PrepareStement ps = conn.prepareStement(sql);
  1. 接收结果集(只有查询有结果集)
int row = ps.excuteUpdate();//交由MySQL执行命令
System.out.println(row + "行受到影响!");
  1. 关闭连接通道
ps.close();
conn.close();

参数的传递方式

//键盘赋值
private static Scanner scan;
static{
scan = new Scanner(System.in);
}

拼接字符串方式

Description

占位符方式 ‘?’

Description

使用占位符的好处:

  • 可以有效避免SQL注入问题

  • 可以自动根据复制时的数据类型来决定是否引入"

删除

  • 物理删除

Description

  • 逻辑删除

Description

查询操作

  • 全查询

Description

  • 按ID查询

Description

五、面向对象(JDBC)的实现方式

面向对象是指将多个功能查分成多个包进行对数据库的增删改查(CRUD)操作。

db包作用

db包中只需要一个类–>DBManager类,这个类的主要作用就是负责管理数据的的连接。

Description

bean包作用

一般和数据库中的表对应,bean包中的类一般都和表名相同,首字母大写,驼峰命名。

Description

dao包作用

DAO是Data Access Object数据访问接口,一般以bean包的类名为前缀,以DAO结尾,负责执行CRUD操作,一个dao类负责一个表的CRUD,也可以说成是对一个bean类的CRUD(增删改查)。

public class EmpDAO(){


// 一般对于删除操作,都是进行更新状态将之隐藏
public void delete(int id){
try{
conn = DBManager.getConnection();
String sql = "update emp set state = 0 where empNo = "+ id;
ps = conn.prepareStatement(sql);
ps.executeUpdate();
}catch(ClassNotFoundException e){
e.printStackTrace();
}catch(SQLException e){
e.printStackTrace();
}finally{
DBManager.closeConn(conn, ps);
}
}

//存储
public void save(Emp emp) {


try {
conn = DBManager.getConnection();
String sql = "insert into emp values(null,?,?,?,now(),?,?,?,1)";
ps = conn.prepareStatement(sql);


ps.setString(1, emp.getEname());
ps.setString(2, emp.getJob());
ps.setInt(3, emp.getMgr());
ps.setDouble(4, emp.getSal());
ps.setDouble(5, emp.getComm());
ps.setInt(6, emp.getDeptNo());
ps.executeUpdate();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {


DBManager.closeConn(conn, ps);
}
}

//更新--修改
public void update(Emp emp) {
try {
conn = DBManager.getConnection();
String sql = "update emp set ename=?,job=?,mgr=?,sal=?,comm=?,deptNo=? where empNo=?";
ps = conn.prepareStatement(sql);
ps.setString(1, emp.getEname());
ps.setString(2, emp.getJob());
ps.setInt(3, emp.getMgr());
ps.setDouble(4, emp.getSal());
ps.setDouble(5, emp.getComm());
ps.setInt(6, emp.getDeptNo());
ps.setInt(7, emp.getEmpNo());
ps.executeUpdate();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBManager.closeConn(conn, ps);
}
}
//单条信息查询--按ID查询--将填写的信息填写在emp属性里中,然后将emp
public Emp findEmpByNo(int id) {
Emp emp = new Emp();


try {
conn = DBManager.getConnection();
String sql = "select * from emp where empno=? and state = 1";
ps = conn.prepareStatement(sql);
ps.setInt(1, id);
rs = ps.executeQuery();
if (rs.next()) {
//取出第一列的值赋给empNO
emp.setEmpNo(rs.getInt(1));
emp.setEname(rs.getString(2));
emp.setJob(rs.getString(3));
emp.setMgr(rs.getInt(4));
emp.setHireDate(rs.getString(5));
emp.setSal(rs.getDouble(6));
emp.setComm(rs.getDouble(7));
emp.setDeptNo(rs.getInt(8));
emp.setState(1);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {


DBManager.closeConn(conn, ps);
}
return emp;
}


//全表查询--集合
public List<Emp> findAllEmp() {
List<Emp> list = new ArrayList<>();


try {
conn = DBManager.getConnection();
String sql = "select * from emp where state = 1";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()) {
Emp emp = new Emp();//每循环一次new一个新对象,给对象付一次值
emp.setEmpNo(rs.getInt(1));
emp.setEname(rs.getString(2));
emp.setJob(rs.getString(3));
emp.setMgr(rs.getInt(4));
emp.setHireDate(rs.getString(5));
emp.setSal(rs.getDouble(6));
emp.setComm(rs.getDouble(7));
emp.setDeptNo(rs.getInt(8));
emp.setState(1);
list.add(emp);//循环一次在集合中增加一条数据
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBManager.closeConn(conn, ps);
}


return list;


}
}

多表联查


//多表联查
//方法一:
public List<Emp> findAllEmp2(){
List<Emp> list = new ArrayList<>();
try {
conn = DBManager.getConnection();
String sql = "select * from emp e left join Dept d on e.deptNo = d.deptNo where state =1";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()){
Emp emp = new Emp();
emp.setEmpNo(rs.getInt(1));
emp.setEname(rs.getString(2));
emp.setJob(rs.getString(3));
emp.setMgr(rs.getInt(4));
emp.setHireDate(rs.getString(5));
emp.setSal(rs.getDouble(6));
emp.setComm(rs.getDouble(7));
emp.setDeptNo(rs.getInt(8));
emp.setState(1);
emp.setDname(rs.getString(11));
emp.setLoc(rs.getString(12));
list.add(emp);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBManager.closeConn(conn, ps,rs);
}
return list;
}


//方法二:
public List<Emp> findAllEmp3(){
List<Emp> list = new ArrayList<>();


try {
conn = DBManager.getConnection();
String sql = "select * from emp e left join Dept d on e.deptNo = d.deptNo where state =1";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()){
Emp emp = new Emp();
emp.setEmpNo(rs.getInt(1));
emp.setEname(rs.getString(2));
emp.setJob(rs.getString(3));
emp.setMgr(rs.getInt(4));
emp.setHireDate(rs.getString(5));
emp.setSal(rs.getDouble(6));
emp.setComm(rs.getDouble(7));
emp.setDeptNo(rs.getInt(8));
emp.setState(1);
Dept dept = new Dept();
dept.setDeptNo(rs.getInt(10));
dept.setDname(rs.getString(11));
dept.setLoc(rs.getString(12));
emp.setDept(dept);//引入dept表
list.add(emp);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBManager.closeConn(conn, ps,rs);
}


return list;
}
}

以上就是JDBC的基本概述了,JDBC这个看似复杂的技术,其实是一个非常实用的工具。

只要你掌握了它,就可以轻松地处理和管理你的数据。无论你是一位经验丰富的程序员,还是一位刚刚入门的新手,我都强烈推荐你学习和使用JDBC。相信我,当你掌握了JDBC,你会发现它为你的工作和学习带来了极大的便利。

收起阅读 »

指纹人脸登验

一、安卓原生指纹识别在 Android 平台上实现原生指纹识别可以使用 Android 系统提供的 FingerprintManager 类。以下是在 Android 平台上实现原...
继续阅读 »

一、安卓原生指纹识别

在 Android 平台上实现原生指纹识别可以使用 Android 系统提供的 FingerprintManager 类。以下是在 Android 平台上实现原生指纹识别的简单步骤:

1. 检查设备是否支持指纹识别:在你的应用中,你可以通过以下代码来检查设备是否支持指纹识别:

FingerprintManager fingerprintManager = (FingerprintManager) getSystemService(Context.FINGERPRINT_SERVICE);  

if (!fingerprintManager.isHardwareDetected()) {
    // 设备不支持指纹识别
}

if (!fingerprintManager.hasEnrolledFingerprints()) {
    // 没有注册指纹
}

2. 实现指纹识别功能:当设备支持指纹识别且用户已经注册了指纹时,你可以使用以下代码来实现指纹识别功能:

FingerprintManager.AuthenticationCallback authenticationCallback = new FingerprintManager.AuthenticationCallback() {  
    @Override
    public void onAuthenticationError(int errMsgId, CharSequence errString) {
        // 指纹认证错误
    }

    @Override
    public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
        // 指纹认证需要帮助
    }

    @Override
    public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
        // 指纹认证成功
    }

    @Override
    public void onAuthenticationFailed() {
        // 指纹认证失败
    }
};

FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(yourCipher);

fingerprintManager.authenticate(cryptoObject, cancellationSignal, 0, authenticationCallback, null);

在上面的代码中,yourCipher 是你要用于加密的密码或密钥的 Cipher 对象,cancellationSignal 是用于取消指纹认证的信号。authenticationCallback 中包含了指纹认证过程中的回调方法,你可以在这些方法中处理指纹认证的结果和错误情况。

以上是在 Android 平台上实现原生指纹识别的简单步骤。需要注意的是,指纹识别功能需要在 AndroidManifest.xml 文件中

二、安卓原生人脸识别

在 Android 平台上实现原生人脸识别可以使用 Android 系统提供的 FaceManager 或者 Camera2 API。以下是使用 FaceManager 实现人脸识别的主要代码:

1. 检查设备是否支持人脸识别:你可以通过以下代码来检查设备是否支持人脸识别:

FaceManager faceManager = (FaceManager) getSystemService(Context.FACE_SERVICE);  

if (!faceManager.isHardwareDetected()) {
    // 设备不支持人脸识别
}

if (!faceManager.hasEnrolledTemplates()) {
    // 没有注册人脸模板
}

2. 实现人脸识别功能:当设备支持人脸识别且用户已经注册了人脸模板时,你可以使用以下代码来实现人脸识别功能:

FaceManager.AuthenticationCallback authenticationCallback = new FaceManager.AuthenticationCallback() {  
    @Override
    public void onAuthenticationError(int errMsgId, CharSequence errString) {
        // 人脸认证错误
    }

    @Override
    public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
        // 人脸认证需要帮助
    }

    @Override
    public void onAuthenticationSucceeded(FaceManager.AuthenticationResult result) {
        // 人脸认证成功
    }

    @Override
    public void onAuthenticationFailed() {
        // 人脸认证失败
    }
};

faceManager.authenticate(null, cancellationSignal, 0, authenticationCallback, null, handler);

在上面的代码中,cancellationSignal 是用于取消人脸认证的信号,authenticationCallback 中包含了人脸认证过程中的回调方法,你可以在这些方法中处理人脸认证的结果和错误情况。

除了使用 FaceManager,你还可以使用 Camera2 API 来获取摄像头数据并进行人脸检测与识别。这需要使用相机预览功能以及图像处理技术来实现人脸检测和识别。

总的来说,实现原生人脸识别涉及到硬件的支持和权限的管理,同时需要根据具体的业务需求来选择合适的实现方式。希望以上信息对你有所

三、flutter指纹识别

在 Flutter 中实现安卓指纹识别可以使用 local_auth 插件。以下是如何在 Flutter 应用中实现安卓指纹识别的简单步骤:

1. 首先,在你的 pubspec.yaml 文件中添加 local_auth 插件的依赖:

dependencies:  
  local_auth: ^1.1.6

然后运行以下命令获取依赖:

flutter pub get  

2. 接下来,在你的 Dart 代码中使用 local_auth 插件来请求指纹识别:

import 'package:flutter/material.dart';  
import 'package:local_auth/local_auth.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final LocalAuthentication localAuth = LocalAuthentication();

  Future<void_authenticate() async {
    bool authenticated = false;
    try {
      authenticated = await localAuth.authenticateWithBiometrics(
        localizedReason: '扫描指纹以进行身份验证',
        useErrorDialogs: true,
        stickyAuth: true,
      );
    } catch (e) {
      print(e);
    }
    if (authenticated) {
      // 指纹认证成功
      print('指纹认证成功');
    } else {
      // 指纹认证失败
      print('指纹认证失败');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('指纹识别示例'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: _authenticate,
          child: Text('进行指纹识别'),
        ),
      ),
    );
  }
}

在此示例中,我们在 MyHomePage 的 build 方法中创建了一个按钮,当用户点击按钮时调用 _authenticate 方法进行指纹识别。在 _authenticate 方法中,我们使用 local_auth 插件来请求指纹识别,并根据认证结果打印相应的消息。

请注意,为了运行安卓指纹识别,你需要在项目的 AndroidManifest.xml 文件中添加指

四、flutter人脸识别

在 Flutter 中实现安卓人脸识别同样可以使用 local_auth 插件。该插件提供了与指纹识别类似的方式来请求进行人脸识别。以下是在 Flutter 中实现安卓人脸识别的简单步骤:

1. 首先,在你的 pubspec.yaml 文件中添加 local_auth 插件的依赖(如果已添加,可以跳过此步骤):

dependencies:  
  local_auth: ^1.1.6

然后运行以下命令获取依赖:

flutter pub get  

2. 接下来,更新你的 Dart 代码以使用 local_auth 插件来请求人脸识别:

import 'package:flutter/material.dart';  
import 'package:local_auth/local_auth.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final LocalAuthentication localAuth = LocalAuthentication();

  Future<void_authenticate() async {
    bool authenticated = false;
    try {
      authenticated = await localAuth.authenticateWithBiometrics(
        localizedReason: '进行人脸识别以进行身份验证',
        useErrorDialogs: true,
        stickyAuth: true,
        biometricOnly: true,
      );
    } catch (e) {
      print(e);
    }
    if (authenticated) {
      // 人脸认证成功
      print('人脸认证成功');
    } else {
      // 人脸认证失败
      print('人脸认证失败');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('人脸识别示例'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: _authenticate,
          child: Text('进行人脸识别'),
        ),
      ),
    );
  }
}

在此示例中,我们在 MyHomePage 的 build 方法中创建了一个按钮,当用户点击按钮时调用 _authenticate 方法进行人脸识别。在 _authenticate 方法中,我们使用 local_auth 插件来请求人脸识别,

KeyguardManager

KeyguardManager 是 Android 系统中用于管理设备锁屏状态的类。通过 KeyguardManager,你可以获取设备的锁屏状态信息,管理键盘锁和密码锁,以及控制设备的解锁和锁定操作。以下是 KeyguardManager 的一些主要功能:

  1. 获取 KeyguardManager 实例:
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
  1. 检查设备的当前锁屏状态:
if (keyguardManager.isKeyguardSecure()) {
// 设备已设置了安全锁屏方式(比如 PIN、图案、密码锁等)
} else {
// 设备没有设置安全锁屏方式
}
  1. 请求设备的解锁:
if (keyguardManager.isKeyguardSecure()) {
Intent intent = keyguardManager.createConfirmDeviceCredentialIntent("Title", "Description");
if (intent != null) {
startActivityForResult(intent, REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS);
}
}

在上面的代码中,createConfirmDeviceCredentialIntent 方法可以创建一个用于验证设备解锁凭据的 Intent,你可以通过启动这个 Intent 来请求设备的解锁操作。

KeyguardManager 还有其他方法,比如管理锁定屏幕、设置锁定屏幕的超时时间等。使用 KeyguardManager 可以帮助你在应用中实现更安全的锁屏管理功能。

KeyStore

KeyStore 是 Android 系统中用于存储密钥(Key)和证书(Certificate)的类。KeyStore 允许你在安全的存储区域保存私钥和受信任的证书,以便在应用中使用加密和认证功能。

以下是 KeyStore 的一些主要功能:

  1. 创建或打开 KeyStore
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);

在上面的代码中,我们使用 KeyStore.getInstance 方法来获取 KeyStore 实例,并指定了存储类型为 "AndroidKeyStore"keyStore.load(null) 方法会加载默认的安装在 Android 设备上的密钥和证书。如果你希望自定义 KeyStore 的存储类型,可以使用其他类型的 KeyStore,比如 "PKCS12"。

  1. 生成或导入密钥:
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
KeyGenParameterSpec.Builder keyGenParameterSpecBuilder = new KeyGenParameterSpec.Builder(
alias,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setDigests(KeyProperties.DIGEST_SHA256)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
.setUserAuthenticationRequired(true);

keyPairGenerator.initialize(keyGenParameterSpecBuilder.build());
KeyPair keyPair = keyPairGenerator.generateKeyPair();

在上面的代码中,我们使用 KeyPairGenerator 来生成密钥对,并通过 KeyGenParameterSpec.Builder 设置密钥生成的参数,然后调用 generateKeyPair 生成密钥对并保存到 KeyStore 中。

  1. 获取密钥:
PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, null);
PublicKey publicKey = keyPair.getPublic();

通过调用 keyStore.getKey 方法,你可以从 KeyStore 中获取保存的私钥和公钥。这些密钥可以用于加密、解密、数字签名等操作。

通过 KeyStore 的功能,可以实现在安全的存储区域保存和管理应用所需的密钥和证书,确保这些敏感信息的安全

参考

Android 指纹识别(给应用添加指纹解锁) - 掘金 (juejin.cn)


作者:whysqwhw
来源:juejin.cn/post/7313589252172087330

收起阅读 »

Vue实现一个textarea幽灵建议功能

web
不知道你有没有发现Bing AI聊天有个输入提示功能,在用户输入部分内容时后面会给出灰色提示文案。用户只要按下tab键就可以快速添加提示的后续内容。我将这个功能称为幽灵建议。接下来我将用Vue框架来实现这个功能。 布局样式 布局使用label标签作为容器,这...
继续阅读 »

不知道你有没有发现Bing AI聊天有个输入提示功能,在用户输入部分内容时后面会给出灰色提示文案。用户只要按下tab键就可以快速添加提示的后续内容。我将这个功能称为幽灵建议。接下来我将用Vue框架来实现这个功能。



布局样式


布局使用label标签作为容器,这样即使建议内容在上层,也不会影响输入框的输入。


<label class="container">
<textarea></textarea>
<div class="ghost-content"></div>
</label>

样式需要确保输入框与建议内容容器除了颜色外都要一致。建议内容可以通过z-index: -1置于输入框底部,但要注意输入框必须是透明背景。


.container {
position: relative;
display: block;
width: 300px;
height: 200px;
font-size: 14px;
line-height: 21px;
}
.container textarea {
width: 100%;
height: 100%;
padding: 0;
border: 0;
font: inherit;
color: #212121;
background-color: #fff;
outline: none;
}
.ghost-content {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
color: #212121;
opacity: 0.3;
}

显示逻辑


显示逻辑比较简单,当输入框中显示输入内容时,找到匹配的内容后将其显示在建议容器中。以下是代码示例:


import { ref } from 'vue'

const content = ref('') // 输入框内容
const ghostContent = ref('') // 建议内容
const suggestions = ['你好啊', '怎么学编程'] // 建议列表

const handleInput = () => {
ghostContent.value = '' // 内容变化时,清空建议
// 如果为空或者建议内容改变,则不进行后续匹配
if (content.value === '') {
return
}
const suggestion = suggestions.find((item) => item.startsWith(content.value))
if (suggestion) {
ghostContent.value = suggestion
}
}

const handleTabKeydown = () => {
// 监听tab键按下,将输入框内容设置为建议内容,同时清空建议内容
content.value = ghostContent.value
ghostContent.value = ''
}

按照以上代码的写法,已经可以实现幽灵建议的功能了。但还存在一个小问题,输入框内容和建议内容的重叠部分会显得比较粗。因此,最好将重叠部分的文字颜色设置为透明。我的解决方法是使用span标签来包裹重叠部分的内容,然后将span的文字样式设置为透明。此外,为了表示可以使用tab键,我在末尾添加了符号。改进后的代码如下:


// 重复部分省略
// ...
const ghostHTML = ref('') // 建议内容HTML
const handleInput = () => {
ghostContent.value = ''
ghostHTML.value = ''
if (content.value === '' || fromSuggestion) {
fromSuggestion && (fromSuggestion = false)
return
}
const suggestion = suggestions.find((item) => item.startsWith(content.value))
if (suggestion) {
ghostContent.value = suggestion
ghostHTML.value = suggestion.replace(content.value, `<span>${content.value}</span>`) + ' →' // 显示内容替换
}
}

const handleTabKeydown = () => {
content.value = ghostContent.value
ghostContent.value = ''
ghostHTML.value = ''
}

最后,补充一下HTML代码。


<label class="container">
<textarea v-model="content" @input="handleInput" @keydown.tab.prevent="handleTabKeydown"></textarea>
<div class="ghost-content" v-html="ghostHTML"></div>
</label>


  • 我们需要阻止tab按下的默认事件,按下tab键会导致切换到其他元素,使输入框失去焦点;

  • 使用v-html来绑定HTML内容。


作者:60岁咯
来源:juejin.cn/post/7273674732448120895
收起阅读 »

谈谈国内前端的三大怪啖

web
因为工作的原因,我和一些外国前端开发有些交流。他们对于国内环境不了解,有时候会问出一些有趣的问题,大概是这些问题的启发,让我反复在思考一些更为深入的问题。今天聊三个事情:小程序微前端模块加载小程序每个行业都有一把银座,当坐上那把银座时,做什么便都是对的。“我们...
继续阅读 »

因为工作的原因,我和一些外国前端开发有些交流。他们对于国内环境不了解,有时候会问出一些有趣的问题,大概是这些问题的启发,让我反复在思考一些更为深入的问题。

今天聊三个事情:

  • 小程序
  • 微前端
  • 模块加载

小程序

每个行业都有一把银座,当坐上那把银座时,做什么便都是对的。

“我们为什么需要小程序?”

第一次被问到这个问题,是因为一个法国的同事。他被派去做一个移动端业务,刚好那个业务是采用小程序在做。于是一个法国小哥就在被痛苦的中文文档和黑盒逻辑中来回折磨着 🤦。

于是,当我们在有一次交流中,他问出了我这个问题:我们为什么需要小程序?

说实话,我试图解释了 19 年国内的现状,以及微信小程序推出时候所带来的便利和体验等等。总之,在我看来并不够深刻的见解。

即便到现在为止,每次当我使用小程序的时候,依旧会复现这个问题。在 ChatGPT 11 月份出来的时候,我也问了它这个很有国内特色的问题:

看起来它回答的还算不错,至少我想如果它来糊弄那些老外,应该会比我做的更好些。

但如果扪心自问,单从技术上来讲。以上这些事情,一定是有其他方案能解决的。

所以从某种程度上来看,这更像是一场截胡的商业案例:

应用市场

全世界的互联网人都知道应用市场是非常有价值的事情,可以说操作系统最值钱的部分就在于他们构建了自己的应用市场。

只要应用在这里注册发行,雁过拔毛,这家公司就是互联网世界里的统治阶级,规则的制定者。

反之则需要受制于人,APP 做的再大,也只是应用市场里的一个应用,做的好与坏还得让应用商店的评判。

另外,不得不承认的是,一个庞大如苹果谷歌这样的公司,他们的应用商店对于普通国内开发者来说,确实是有门槛的。

在国内海量的 APP 需求来临之前,能否提供一个更低成本的解决方案,来消化这些公司的投资?

毕竟不是所有的小企业都需要 APP,其实他们大部分需求 Web 就可以解决,但是 Web 没牌面啊,做 Web 还得砸搜索的钱才有流量。(某度搜索又做成那样...)

那做操作系统?太不容易,那么多人都溺死在水里了,这水太深。

那有没有一种办法可以既能构建生态,又有 APP 的心智,还能给入驻企业提供流量?

于是,在 19 年夏天,滨海大厦下的软件展业基地里,每天都在轮番播放着,做 XX小程序,拥抱下一个风口...

全新体验心智

小程序用起来挺方便的。

你有没有想过,这些美妙感觉的具体都来自哪些?以及这些真的是 Web 技术本身无法提供的吗?

  1. 靠谱感,每个小程序都有约束和规范,于是你可以将他们整整齐齐的陈放在你的列表里,仿佛你已经拥有了这一个个精心雕琢的作品,相对于一条条记不住的网页地址和鱼龙混杂的网页内容来说,这让你觉得小程序更加的有分量和靠谱。
  2. 安全感,沉浸式的头部,没有一闪而过的加载条,这一切无打扰的设计,都让你觉得这是一个在你本地的 APP,而不是随时可能丢失网页。你不会因为网速白屏而感到焦虑,尽管网络差的时候,你的 KFC 依旧下不了单 😂
  3. 沉浸感,我不知道是否打开网页时顶部黑黑的状态栏是故意留下的,还是不小心的... 这些限制都让用户非常强烈的意识到这是一个网页而不是 APP,而小程序中虽然上面也存在一个空间的空白,但是却可以被更加沉浸的主题色和氛围图替代。网页这个需求做不了?我不信。
H5小程序
  1. 顺滑感,得益于 Native 的容器实现,小程序在所有的视图切换时,都可以表现出于原生应用一样的顺滑感。其实这个问题才是在很多 Hybrid 应用中,主要想借助客户端来处理和解决的问题。类似容器预开、容器切换等技术是可以解决相关问题的,只是还没有一个标准。

我这里没有提性能,说实话我不认为性能在小程序中是有优势的(Native 调用除外,如地图等,不是一个讨论范畴)。作为普通用户,我们感受到的只是离线加载下带来的顺滑而已。

而上述提到的许多优势,这对于一个高品质的 Web 应用来说是可以做得到的,但注意这里是高品质的 Web 应用。而这种“高品质”在小程序这里,只是入驻门槛而已。

心智,这个词,听起来很黑话,但却很恰当。当小程序通过长期这样的筛选,所沉淀出来一批这样品质的应用时。就会让每个用户即便在还没打开一个新的小程序之前,也有不错体验的心理预期。这就是心智,一种感觉跟 Web 不一样,跟 APP 有点像的心智。

打造心智,这件事情好像就是国内互联网企业最擅长做的事情,总是能从一些细微的差别中开辟一条独立的领域,然后不断强化灌输本来就不大的差异,等流量起来再去捞钱变现。


我总是感觉现在的互利网充斥着如何赚钱的想法,好像永远赚不够。“赚钱”这个事情,在这些公司眼里就是圈人圈地抢资源,看看谁先占得先机,别人有的我也得有,这好像是最重要的事情。

很少有企业在思考如何创造些没有的市场,创造些真正对技术发展有贡献,对社会发展有推动作用的事情。所以在国内互联网圈也充斥着一种奇怪的价值观,有技术的不一定比赚过钱的受待见。

管你是 PHP 还是 GO,管你是在做游戏还是直播带货,只要赚到钱就是高人。并且端的是理所应当、理直气壮,有些老板甚至把拍摄满屋子的程序员为自己打工作为一种乐趣。什么情怀、什么优雅、什么愿景,人生就俩字:搞钱。

不是故意高雅,赚钱这件事情本身不寒碜,只是在已经赚到盆满钵满、一家独大的时候还在只是想着赚更多的钱,好像赚钱的目的就是为了赚钱一样,这就有点不合适。企业到一定程度是要有社会责任的,龙头企业每一个决定和举措,都有会影响接下来的几年里这个行业的价值观走向。

当然也不是完全没有好格局的企业,我非常珍惜每一个值得尊重的中国企业,来自一个蔚来车主。

小程序在商业上固然是成功的,但吃的红利可以说还是来自 网页 到 应用 的心智变革。将本来流向 APP 的红利,截在了小程序生态里。

但对于技术生态的发展却是带来了无数条新的岔路,小程序的玩法就决定了它必须生长在某个巨型应用里面,不论是用户数据获取、还是 API 的调用,其实都是取决于应用容器的标准规范。

不同公司和应用之间则必然会产生差异,并且这种差异是墒增式的差异,只会随着时间的推移越变越大。如果每个企业都只关注到自己业务的增长,无外部约束的话,企业必然会根据自己的业务发展和政策需要,选择成本较低的调整 API,甚至会故意制造一些壁垒来增加这种差异。

小程序,应该是 浏览器 与 操作系统 的融合,这本应该是推动这两项技术操刀解决的事情。

微前端

qiankun、wujie、single-spa 是近两年火遍前端的技术方案,同样一个问题:我们为什么需要微前端?

我不确定是否每个在使用这项技术的前端都想清楚了这个问题,但至少在我面试过的候选人中,我很少遇到对自己项目中已经在使用的微前端,有很深的思考和理解的人。

先说下我的看法:

  1. 微前端,重在解决项目管理而不在用户体验。
  2. 微前端,解决不了该优化和需要规范的问题。
  3. 微前端,在挽救没想清楚 MPA 的 SPA 项目。

没有万能银弹

银色子弹(英文:Silver Bullet),或者称“银弹”“银质子弹”,指由纯银质或镀银的子弹。在欧洲民间传说及19世纪以来哥特小说风潮的影响下,银色子弹往往被描绘成具有驱魔功效的武器,是针对狼人、吸血鬼等超自然怪物的特效武器。后来也被比喻为具有极端有效性的解决方法,作为杀手锏、最强杀招、王牌等的代称。

所有技术的发展都是建立在前一项技术的基础之上,但技术依赖的选择过程中一定需要保留第一性原理的意识。

当 React、Vue 兴起,当 打包技术(Webpack) 兴起,当 网页应用(SPA) 兴起,这些杰出的技术突破都在不同场景和领域中给行业提供了新的思路、新的方案。

不知从何时开始,前端除了 div 竟说不出其他的标签(还有说 View 的),项目中再也不会考虑给一个通用的 class 解决通用样式问题。

不知从何时开始,有没有权限需要等到 API 请求过后才知道,没有权限的话再把页面跳转过去申请。

不知从何时开始,大家的页面都放在了一个项目里,两个这样的巨石应用融合竟然变成了一件困难的事。

上面这些不合理的现状,都是在不同的场景下,不思考适不适合,单一信奉 “一招吃遍天” 下演化出的问题。

B 端应用,是否应该使用 SPA? 这其实是一个需要思考的问题。

微前端从某种程度上来讲,是认准 SPA 了必须是互联网下一代应用标准的产物,好像有了 SPA 以后,MPA 就变得一文不值。甭管页面是移动端的还是PC的;甭管页面是面对 C 端的还是 B 端的;甭管一个系统是有 20 个页面还是 200 个页面,一律行这套逻辑。

SPA 不是万能银弹,React 不是万能银弹,Tailwind 不是万能银弹。在新技术出现的时候,保持热情也要保持克制。

ps. 我也十分痛恨 React 带来的这种垄断式的生态,一个 React 组件将 HTML 和 Style 都吃进去,让你即使在做一个移动端的纯展示页面时,也需要背上这些称重的负担。

质疑 “墨守成规”,打开视野,深度把玩,理性消费。

分而治之

分治法,一个很基本的工程思维。

在我看来在一个正常商业迭代项目中的主要维护者,最好不要超过 3 个人,注意是主要维护者(Maintainer) 。

你应该让每个项目都有清晰的责任人,而不是某行代码,某个模块。责任人的理解是有归属感,有边界感的那种,不是口头意义上的责任人。(一些公司喜欢搞这种虚头巴脑的事情,什么连坐…)

我想大部分想引入微前端的需求都是类似 如何更好的划分项目边界,同时保留更好的团队协同。

比如 导航菜单 应该是独立收口独立管理的,并且当其更新时,应该同时应用于所有页面中。类似的还有 环境变量、时区、主题、监控及埋点。微前端将这些归纳在主应用中。

而具体的页面内容,则由对应的业务进行开发子应用,最后想办法将路由关系注册进主应用即可。

当然这样纯前端的应用切换,还会出现不同应用之间的全局变量差异、样式污染等问题,需要提供完善的沙箱容器、隔离环境、应用之间通信等一系列问题,这里不展开。

当微前端做到这一部分的时候,我不禁在想,这好像是在用 JavaScript 实现一个浏览器的运行容器。这种本应该浏览器本身该做的事情,难道 JS 可以做的更好?

只是做到更好的项目拆分,组织协同的话,引入后端服务,由后端管控路由表和页面规则,将页面直接做成 MPA,这个方案或许并不比引入微前端成本高多少。

体验差异

从 SPA 再回 MPA,说了半天不又回去了么。

所以不防想想:在 B端 业务中使用 SPA 的优势在哪里?

流畅的用户体验:

这个话题其实涵盖范围很广,对于 SPA 能带来的 “流畅体验”,对于大多数情况下是指:导航菜单不变,内容变化 发生变化,页面切换中间不出现白屏

但要做到这个点,其实对于 MPA 其实并没有那么困难,你只需要保证你的 FCP 在 500ms 以内就行。

以上的页面切换全部都是 MPA 的多页面切换,我们只是简单做了导航菜单的 拆分 和 SWR,并没有什么特殊的 preload、prefetch 处理,就得到了这样的效果。

因为浏览器本身在页面切换时会在 unload 之前先 hold 当前页面的视图不变,发起一下一个 document 的请求,当页面的视图渲染做到足够快和界面结构稳定就可以得到这样的效果。

这项浏览器的优化手段我找了很久,想找一篇关于它的博文介绍,但确实没找到相关内容,所以 500ms 也是我的一个大致测算,如果你知道相关的内容,可以在评论区补充,不胜感激。

所以从这个角度来看,浏览器本身就在尽最大的努力做这些优化,并且他们的优化会更底层、更有效的。

离线访问 (PWA)

SPA 确实会有更好的 PWA 组织能力,一个完整的 SPA 应用甚至可以只针对编译层做改动就可以支持 PWA 能力。

但如果看微前端下的 SPA 应用,需要支持 PWA 那就同样需要分析各子应用之间的元数据,定制 Service Worker。这种组织关系和定制 SW,对于元数据对于数据是来自前端还是后端,并不在意。

也就是说微前端模式下的 PWA,同样的投入成本,把页面都管理在后端服务中的 MPA 应用也是可以做到相同效果的。

项目协同、代码复用

有人说 SPA 项目下,项目中的组件、代码片段是可以相互之间复用的,在 MPA 下就相对麻烦。

这其实涉及到项目划分的领域,还是要看具体的需求也业务复杂度来定。如果说整个系统就是二三十个页面,这做成 SPA 使用前端接管路由高效简单,无可厚非。

但如果你本身在面对的是一个服务于复杂业务的 B 端系统,比如类似 阿里云、教务系统、ERP 系统或者一些大型内部系统,这种往往需要多团队合作开发。这种时候就需要跟完善的项目划分、组织协同和系统整合的方案。

这个时候 SPA 所体现出的优势在这样的诉求下就会相对较弱,在同等投入的情况下 MPA 的方案反而会有更少的执行成本。

也不是所有项目一开始就会想的那么清楚,或许一开始的时候就是个简单的 SPA 项目,但是随着项目的不断迭代,才变成了一个个复杂的巨石应用,现在如果再拆出来也会有许多迁移成本。引入微前端,则可以...

这大概是许多微前端项目启动的背景介绍,我想说的是:对于屎山,我从来不信奉“四两拨千斤”

如果没有想好当下的核心问题,就引入新的“银弹”解决问题,只会是屎山雕花。

项目协同,抽象和复用这些本身不是微前端该解决的问题,这是综合因素影响下的历史背景问题。也是需要一个个资深工程师来把控和解决的核心问题,就是需要面对不同的场景给出不同的治理方案。

这个道理跟防沙治沙一样,哪有那么多一蹴而就、立竿见影的好事。

模块加载

模块加载这件事情,从玉伯大佬的成名作 sea.js 开始就是一个非常值得探讨的问题。在当时 jQuery 的时代里,这是一个绝对超前的项目,我也在实际业务中体会过在无编译的环境下 sea.js 的便捷。

实际上,不论是微前端、低代码、会场搭建等热门话题离不开这项技术基础。

import * from * 我们每天都在用,但最终的产物往往是一个自运行的 JS Bundle,这来自于 Webpack、Vite 等编译技术的发展。让我们可以更好的组织项目结构,以构建更复杂的前端应用。

模块的概念用久了,就会自然而然的在遇到浏览器环境中,遇到动态模块加载的需求时,想到这种类似模块加载的能力。

比如在遇到会场千奇百怪的个性化营销需求时,能否将模块的 Props 开放出来,给到非技术人员,以更加灵活的方式让他们去做自由组合。

比如在低代码平台中,让开发者自定义扩展组件,动态的将开发好的组件注册进低代码平台中,以支持更加个性的需求。

在万物皆组件的思想影响下,把一整个完整页面都看做一个组件也不是不可以。于是在一些团队中,甚至提倡所有页面都可以搭建、搭建不了的就做一个大的页面组件。这样及可以减少维护运行时的成本,又可以统一管控和优化,岂不美哉。

当这样大一统的“天才方案”逐渐发展成为标准后,也一定会出现一些特殊场景无法使用,但没关系,这些天才设计者肯定会提供一种更加天才的扩展方案出来。比如插件,比如扩展,比如 IF ELSE。再后来,就会有性能优化了,而优化的 追赶对象 就是用原来那种简单直出的方案。

有没有发现,这好像是在轮回式的做着自己出的数学题,一道接一道,仿佛将 1 + 1的结论重新演化了一遍。

题外话,我曾经自己实现过一套通过 JSON Schema 描述 React 结构的 “库” ,用于一个低代码核心层的渲染。在我的实现过程中,我越发觉得我在做一件把 JSX 翻译成 JS 的事情,但 JSX 或者 HTML 本身不就是一种 DSL 么。为什么一定要把它翻译成 JSON 来传输呢?或者说这样的封装其本身有意义么?这不就是在做 PHP、.Net 直接返回带有数据的 HTML Ajax 一样的事情么。

传统的浏览器运行环境下要实现一个模块加载器,无非是在全局挂载一个注册器,通过 Script 插入一段新的 JS,该 JS 通过特殊的头尾协议,将运行时的代码声明成一个函数,注册进事先挂载好的注册器。

但实际的实现场景往往要比这复杂的多,也有一些问题是这种非原生方式无法攻克的问题。比如全局注册器的命名冲突;同模块不同版本的加载冲突;并发加载下的时序问题;多次模块加载的缓存问题 等等等等等...

到最后发现,这些事情好像又是在用 JS 做浏览器该做的事情。然而浏览器果然就做了,,Vite 就主要采用这种模式实现了它 1 年之内让各大知名项目切换到 Vite 的壮举。

“但我们用不了,有兼容性问题。”

哇哦,当我看着大家随意写出的 display: grid 样式定义,不禁再次感叹人们对未知的恐惧。

import.meta 的兼容性是另外一个版本,是需要 iOS12 以上,详情参考:caniuse.com/?search=imp…

试想一下,现在的低代码、会场搭建等等各类场景的模块加载部分,如果都直接采用 ESM 的形式处理,这对于整个前端生态和开发体验来说会有多么大的提升。

模块加载,时至今日,本来就已经不再需要 loader。 正如 seajs 中写的:前端模块化开发那点历史

历史不是过去,历史正在上演。随着 W3C 等规范、以及浏览器的飞速发展,前端的模块化开发会逐步成为基础设施。一切终究都会成为历史,未来会更好。

结语

文章的结尾,我想感叹另外一件事,国人为什么一定要有自己的操作系统?为什么一定需要参与到一些规范的制定中?

因为我们的智慧需要有开花的土壤,国内这千千万开发者的抱负需要有地方释放。

如果没有自己掌握核心技术,就是只能在问题出现的时候用另类的方式来解决。最后在一番折腾后,发现更底层的技术只要稍稍一改就可以实现的更好。这就像三体中提到的 “智子” 一样,不断在影响着我们前进的动力和方向。

不论是小程序、微前端还是模块加载。试想一下,如果我们有自己的互联网底蕴,能决定或者影响操作系统和浏览器的底层能力。这些 “怪啖” 要么不会出现,要么就是人类的科技创新。

希望未来技术人不用再追逐 Write Once, Run Everywhere 的事情...


作者:YeeWang
来源:juejin.cn/post/7267091810366488632
收起阅读 »

QR码是怎么工作的?

web
原文链接: typefully.com/DanHollick/… 作者:Dan Hollick 你有想过QR码是如何工作的吗? 我也没有想过,但是它真的很低调很迷人~ 【警告】这里有一些非常书呆子的东西👇 ) QR码是由丰田的一个子公司发明的,目的是为了在整...
继续阅读 »

原文链接: typefully.com/DanHollick/…


作者:Dan Hollick


你有想过QR码是如何工作的吗?


我也没有想过,但是它真的很低调很迷人~


【警告】这里有一些非常书呆子的东西👇 )


image.png


QR码是由丰田的一个子公司发明的,目的是为了在整个制造过程中跟踪零件信息。


之前出现的条形码被证明是不足够的 - 它们只能从特定的角度读取,并且相对于他们的大小来说,并不能储存很多的数据。


那么 QR 码不只解决了这些问题


image.png


QR 码最独一无二的地方在于这些正方体,这些正方体被称为“查找器”,这些正方体帮助了你的阅读器检测到码的存在。


第四个小的正方体,被称作对齐模式,它是用来定向代码的,使它可以在任何角度呈现,阅读器仍然哪个方向是向上的。


image.png


你可能从来都没有注意过,但是每个 QR 码都有这些叫做定时模式的黑白相间的点。


这些黑白相间的点告诉阅读器单个模块有多大以及整个 QR 码有多大 -- 也就是版本。


image.png


版本一:最小的。
版本四十:最大的。


image.png


关于格式的信息被存在查找器旁边的两个条纹中。


它被存储了两次,所以即使QR码被部分遮挡,它也是可读的。(你会注意到这是一个反复出现的主题。)


image.png


它存储了三个重要的信息片段



  1. 掩码(Mask)

  2. 纠错级别

  3. 纠错格式


我知道这听起来很无聊,但是实际上,他还是很有意思的(doge


image.png


首先,纠错 - 这是什么玩意?


从本质上讲,它规定了在 QR 码中存储多少冗余信息,以确保即使部分信息丢失也能保持可读。


image.png


这真的很酷好吗 - 如果您的代码在户外,您可以选择更高的冗余级别,以确保它在模糊的时候也能正常工作。


试试下面这个二维码


image.png


第二个,这个 mask,这个是个什么东西?


首先你需要知道,QR 阅读器在黑色区域和白色区域的数量一样的时候工作的最好。


但是数据可能无法发挥作用,因此使用掩码来平衡。


image.png


当掩版应用于QR码时,任何落在掩码暗部的东西都会被反转。


白色区域变成黑色,黑色区域变成白色。


image.png


有8种标准模式,一个接一个地应用。使用达到最佳结果的模式,并存储该信息,以便读者可以不应用掩码。


最后呢,就到了我们的实际数据的部分。


奇怪的是,数据从右下角开始,然后像图中那样蜿蜒而上。


从哪开始几乎不重要了,因为它可以从每个角度读取。


image.png


这里的第一个信息块告诉读者数据是以什么模式编码的,第二个告诉它长度。


在我们的例子中,每个字符占用8位块,或者称为字节,共有24个字节。


image.png


在我们的数据之后还有一些剩余的空间。这是存储纠错信息的地方,以便在部分模糊的情况下可以读取。它的工作方式实际上非常非常复杂,所以我把它省略了。基本上就是这样!


image.png


对于制作 QR 码的书呆子来说,一个有意思的事实是: QR码最酷的事情是发明QR码的Denso Wave公司从未行使他们的专利,并且免费发布这项技术!


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

商品 sku 在库存影响下的选中与禁用

web
分享一下,最近使用 React 封装的一个 Skus 组件,主要用于处理商品的sku在受到库存的影响下,sku项的选中和禁用问题; 需求分析 需要展示商品各规格下的sku信息,以及根据该sku的库存是否为空,判断是否禁用该sku的选择。 以下讲解将按照我的 ...
继续阅读 »

分享一下,最近使用 React 封装的一个 Skus 组件,主要用于处理商品的sku在受到库存的影响下,sku项的选中和禁用问题;


需求分析


需要展示商品各规格下的sku信息,以及根据该sku的库存是否为空,判断是否禁用该sku的选择。


sku-2.gif

以下讲解将按照我的 Skus组件 来,我这里放上我组件库中的线上 demo 和码上掘金的一个 demo 供大家体验;由于码上掘金导入不了组件库,我就上传了一份开发组件前的一份类似的代码,功能和代码思路是差不多的,大家也可以自己尝试写一下,可能你的思路会更优;


线上 Demo 地址


码上掘金



传入的sku数据结构


需要传入的商品的sku数据类型大致如下:


type SkusProps = { 
/** 传入的skus数据列表 */
data: SkusItem[]
// ... 其他的props
}

type SkusItem = {
/** 库存 */
stock?: number;
/** 该sku下的所有参数 */
params: SkusItemParam[];
};

type SkusItemParam = {
name: string;
value: string;
}

转化成需要的数据类型:


type SkuStateItem = {
value: string;
/** 与该sku搭配时,该禁用的sku组合 */
disabledSkus: string[][];
}[];

生成数据


定义 sku 分类


首先假装请求接口,造一些假数据出来,我这里自定义了最多 6^6 = 46656 种 sku。


sku-66.gif

下面的是自定义的一些数据:


const skuData: Record<string, string[]> = {
'颜色': ['红','绿','蓝','黑','白','黄'],
'大小': ['S','M','L','XL','XXL','MAX'],
'款式': ['圆领','V领','条纹','渐变','轻薄','休闲'],
'面料': ['纯棉','涤纶','丝绸','蚕丝','麻','鹅绒'],
'群体': ['男','女','中性','童装','老年','青少年'],
'价位': ['<30','<50','<100','<300','<800','<1500'],
}
const skuNames = Object.keys(skuData)

页面初始化



  • checkValArr: 需要展示的sku分类是哪些;

  • skusList: 接口获取的skus数据;

  • noStockSkus: 库存为零对应的skus(方便查看)。


export default () => {
// 这个是选中项对应的sku类型分别是哪几个。
const [checkValArr, setCheckValArr] = useState<number[]>([4, 5, 2, 3, 0, 0]);
// 接口请求到的skus数据
const [skusList, setSkusList] = useState<SkusItem[]>([]);
// 库存为零对应的sku数组
const [noStockSkus, setNoStockSkus] = useState<string[][]>([])

useEffect(() => {
const checkValTrueArr = checkValArr.filter(Boolean)
const _noStockSkus: string[][] = [[]]
const list = getSkusData(checkValTrueArr, _noStockSkus)
setSkusList(list)
setNoStockSkus([..._noStockSkus])
}, [checkValArr])

// ....

return <>...</>
}

根据上方的初始化sku数据,生成一一对应的sku,并随机生成对应sku的库存。


getSkusData 函数讲解


先看总数(total)为当前需要的各sku分类的乘积;比如这里就是上面传入的 checkValArr 数组 [4,5,2,3]120种sku选择。对应的就是 skuData 中的 [颜色前四项,大小前五项,款式前两项,面料前三项] 即下图的展示。


image.png

遍历 120 次,每次生成一个sku,并随机生成库存数量,40%的概率库存为0;然后遍历 skuNames 然后找到当前对应的sku分类即 [颜色,大小,款式,面料] 4项;


接下来就是较为关键的如何根据 sku的分类顺序 生成对应的 120个相应的sku。


请看下面代码中注释为 LHH-1 的地方,该 value 的获取是通过 indexArr 数组取出来的。可以看到上面 indexArr 数组的初始值为 [0,0,0,0] 4个零的索引,分别对应 4 个sku的分类;



  • 第一次遍历:


indexArr: [0,0,0,0] -> skuName.forEach -> 红,S,圆领,纯棉


看LHH-2标记处: 索引+1 -> indexArr: [0,0,0,1];



  • 第二次遍历:


indexArr: [0,0,0,1] -> skuName.forEach -> 红,S,圆领,涤纶


看LHH-2标记处: 索引+1 -> indexArr: [0,0,0,2];



  • 第三次遍历:


indexArr: [0,0,0,2] -> skuName.forEach -> 红,S,圆领,丝绸


看LHH-2标记处: 由于已经到达该分类下的最后一个,所以前一个索引加一,后一个重新置为0 -> indexArr: [0,0,1,0];



  • 第四次遍历:


indexArr: [0,0,1,0] -> skuName.forEach -> 红,S,V领,纯棉


看LHH-2标记处: 索引+1 -> indexArr: [0,0,1,1];



  • 接下来的一百多次遍历跟上面的遍历同理


image.png
function getSkusData(skuCategorys: number[], noStockSkus?: string[][]) {
// 最终生成的skus数据;
const skusList: SkusItem[] = []
// 对应 skuState 中各 sku ,主要用于下面遍历时,对 product 中 skus 的索引操作
const indexArr = Array.from({length: skuCategorys.length}, () => 0);
// 需要遍历的总次数
const total = skuCategorys.reduce((pre, cur) => pre * (cur || 1), 1)
for(let i = 1; i <= total; i++) {
const sku: SkusItem = {
// 库存:60%的几率为0-50,40%几率为0
stock: Math.floor(Math.random() * 10) >= 4 ? Math.floor(Math.random() * 50) : 0,
params: [],
}
// 生成每个 sku 对应的 params
let skuI = 0;
skuNames.forEach((name, j) => {
if(skuCategorys[j]) {
// 注意:LHH-1
const value = skuData[name][indexArr[skuI]]
sku.params.push({
name,
value,
})
skuI++;
}
})
skusList.push(sku)

// 注意: LHH-2
indexArr[indexArr.length - 1]++;
for(let j = indexArr.length - 1; j >= 0; j--) {
if(indexArr[j] >= skuCategorys[j] && j !== 0) {
indexArr[j - 1]++
indexArr[j] = 0
}
}

if(noStockSkus) {
if(!sku.stock) {
noStockSkus.at(-1)?.push(sku.params.map(p => p.value).join(' / '))
}
if(indexArr[0] === noStockSkus.length && noStockSkus.length < skuCategorys[0]) {
noStockSkus.push([])
}
}
}
return skusList
}

Skus 组件的核心部分的实现


初始化数据


需要将上面生成的数据转化为以下结构:


type SkuStateItem = {
value: string;
/** 与该sku搭配时,该禁用的sku组合 */
disabledSkus: string[][];
}[];

export default function Skus() {
// 转化成遍历判断用的数据类型
const [skuState, setSkuState] = useState<Record<string, SkuStateItem>>({});
// 当前选中的sku值
const [checkSkus, setCheckSkus] = useState<Record<string, string>>({});

// ...
}

将初始sku数据生成目标结构


根据 data (即上面的假数据)生成该数据结构。


第一次遍历是对skus第一项进行的,会生成如下结构:


const _skuState = {
'颜色': [{value: '红', disabledSkus: []}],
'大小': [{value: 'S', disabledSkus: []}],
'款式': [{value: '圆领', disabledSkus: []}],
'面料': [{value: '纯棉', disabledSkus: []}],
}

第二次遍历则会完整遍历剩下的skus数据,并往该对象中填充完整。


export default function Skus() {
// ...
useEffect(() => {
if(!data?.length) return
// 第一次对skus第一项的遍历
const _checkSkus: Record<string, string> = {}
const _skuState = data[0].params.reduce((pre, cur) => {
pre[cur.name] = [{value: cur.value, disabledSkus: []}]
_checkSkus[cur.name] = ''
return pre
}, {} as Record<string, SkuStateItem>)
setCheckSkus(_checkSkus)

// 第二次遍历
data.slice(1).forEach(item => {
const skuParams = item.params
skuParams.forEach((p, i) => {
// 当前 params 不在 _skuState 中
if(!_skuState[p.name]?.find(params => params.value === p.value)) {
_skuState[p.name].push({value: p.value, disabledSkus: []})
}
})
})

// ...接下面
}, [data])
}

第三次遍历主要用于为每个 sku的可点击项 生成一个对应的禁用sku数组 disabledSkus ,只要当前选择的sku项,满足该数组中的任一项,该sku选项就会被禁用。之所以保存这样的一个二维数组,是为了方便后面点击时的条件判断(有点空间换时间的概念)。


遍历 data 当库存小于等于0时,将当前的sku的所有参数传入 disabledSkus 中。


例:第一项 sku(红,S,圆领,纯棉)库存假设为0,则该选项会被添加到 disabledSkus 数组中,那么该sku选择时,勾选前三个后,第四个 纯棉 的勾选会被禁用。


image.png
export default function Skus() {
// ...
useEffect(() => {
// ... 接上面
// 第三次遍历
data.forEach(sku => {
// 遍历获取库存需要禁用的sku
const stock = sku.stock!
// stockLimitValue 是一个传参 代表库存的限制值,默认为0
// isStockGreaterThan 是一个传参,用来判断限制值是大于还是小于,默认为false
if(
typeof stock === 'number' &&
isStockGreaterThan ? stock >= stockLimitValue : stock <= stockLimitValue
) {
const curSkuArr = sku.params.map(p => p.value)
for(const name in _skuState) {
const curSkuItem = _skuState[name].find(v => curSkuArr.includes(v.value))
curSkuItem?.disabledSkus?.push(
sku.params.reduce((pre, p) => {
if(p.name !== name) {
pre.push(p.value)
}
return pre
}, [] as string[])
)
}
}
})

setSkuState(_skuState)
}, [data])
}

遍历渲染 skus 列表


根据上面的 skuState,生成用于渲染的列表,渲染列表的类型如下:


type RenderSkuItem = {
name: string;
values: RenderSkuItemValue[];
}
type RenderSkuItemValue = {
/** sku的值 */
value: string;
/** 选中状态 */
isChecked: boolean
/** 禁用状态 */
disabled: boolean;
}

export default function Skus() {
// ...
/** 用于渲染的列表 */
const list: RenderSkuItem[] = []
for(const name in skuState) {
list.push({
name,
values: skuState[name].map(sku => {
const isChecked = sku.value === checkSkus[name]
const disabled = isChecked ? false : isSkuDisable(name, sku)
return { value: sku.value, disabled, isChecked }
})
})
}
// ...
}

html css 大家都会,以下就简单展示了。最外层遍历sku的分类,第二次遍历遍历每个sku分类下的名称,第二次遍历的 item(类型为:RenderSkuItemValue),里面会有sku的值,选中状态和禁用状态的属性。


export default function Skus() {
// ...
return list?.map((p) => (
<div key={p.name}>
{/* 例:颜色、大小、款式、面料 */}
<div>{p.name}</div>
<div>
{p.values.map((sku) => (
<div
key={p.name + sku.value}
onClick={() =>
selectSkus(p.name, sku)}
>
{/* classBem 是用来判断当前状态,增加类名的一个方法而已 */}
<span className={classBem(`sku`, {active: sku.isChecked, disabled: sku.disabled})}>
{/* 例:红、绿、蓝、黑 */}
{sku.value}
</span>
</div>
))}
</div>
</div>

))
}

selectSkus 点击选择 sku


通过 checkSkus 设置 sku 对应分类下的 sku 选中项,同时触发 onChange 给父组件传递一些信息出去。


const selectSkus = (skuName: string, {value, disabled, isChecked}: RenderSkuItemValue) => {
const _checkSkus = {...checkSkus}
_checkSkus[skuName] = isChecked ? '' : value;
const curSkuItem = getCurSkuItem(_checkSkus)
// 该方法主要是 sku 组件点击后触发的回调,用于给父组件获取到一些信息。
onChange?.(_checkSkus, {
skuName,
value,
disabled,
isChecked: disabled ? false : !isChecked,
dataItem: curSkuItem,
stock: curSkuItem?.stock
})
if(!disabled) {
setCheckSkus(_checkSkus)
}
}

getCurSkuItem 获取当前选中的是哪个sku



  • isInOrder.current 是用来判断当前的 skus 数据是否是整齐排列的,这里当成 true 就好,判断该值的过程就不放到本文了,感兴趣可以看 源码


由于sku是按顺序排列的,所以只需按顺序遍历上面生成的 skuState,找出当前sku选中项对应的索引位置,然后通过 就可以直接得出对应的索引位置。这样的好处是能减少很多次遍历。


如果直接遍历原来那份填充所有 sku 的 data 数据,则需要很多次的遍历,当sku是 6^6 时, 则每次变换选中的sku时最多需要 46656 * 6 (data总长度 * 里面 sku 的 params) 次。


const getCurSkuItem = (_checkSkus: Record<string, string>) => {
const length = Object.keys(skuState).length
if(!length || Object.values(_checkSkus).filter(Boolean).length < length) return void 0
if(isInOrder.current) {
let skuI = 0;
// 由于sku是按顺序排列的,所以索引可以通过计算得出
Object.keys(_checkSkus).forEach((name, i) => {
const index = skuState[name].findIndex(v => v.value === _checkSkus[name])
const othTotal = Object.values(skuState).slice(i + 1).reduce((pre, cur) => (pre *= cur.length), 1)
skuI += index * othTotal;
})
return data?.[skuI]
}
// 这样需要遍历太多次
return data.find(s => (
s.params.every(p => _checkSkus[p.name] === getSkuParamValue(p))
))
}

isSkuDisable 判断该 sku 是否是禁用的


该方法是在上面 遍历渲染 skus 列表 时使用的。



  1. 开始还未有选中值时,需要校验 disabledSkus 的数组长度,是否等于该sku参数可以组合的sku总数,如果相等则表示禁用。

  2. 判断当前选中的 sku 还能组成多少种组合。例:当前选中 红,S ,而 isSkuDisable 方法当前判断的 sku 为 款式 中的 圆领,则还有三种组合 红\S\圆领\纯棉红\S\圆领\涤纶红\S\圆领\丝绸

  3. 如果当前判断的 sku 的 disabledSkus 数组中存在这三项,则表示该 sku 选项会被禁用,无法点击。


const isCheckValue = !!Object.keys(checkSkus).length

const isSkuDisable = (skuName: string, sku: SkuStateItem[number]) => {
if(!sku.disabledSkus.length) return false
// 1.当一开始没有选中值时,判断某个sku是否为禁用
if(!isCheckValue) {
let checkTotal = 1;
for(const name in skuState) {
if(name !== skuName) {
checkTotal *= skuState[name].length
}
}
return sku.disabledSkus.length === checkTotal
}

// 排除当前的传入的 sku 那一行
const newCheckSkus: Record<string, string> = {...checkSkus}
delete newCheckSkus[skuName]

// 2.当前选中的 sku 一共能有多少种组合
let total = 1;
for(const name in newCheckSkus) {
if(!newCheckSkus[name]) {
total *= skuState[name].length
}
}

// 3.选中的 sku 在禁用数组中有多少组
let num = 0;
for(const strArr of sku.disabledSkus) {
if(Object.values(newCheckSkus).every(str => !str ? true : strArr.includes(str))) {
num++;
}
}

return num === total
}

至此整个商品sku从生成假数据到sku的选中和禁用的处理的核心代码就完毕了。还有更多的细节问题可以直接查看 源码 会更清晰。


作者:滑动变滚动的蜗牛
来源:juejin.cn/post/7313979106890842139
收起阅读 »

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

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

背景



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

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

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

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





我该如何应对


先看我实现的


b0nh2-9h1qy.gif


在看看设计想要的


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



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

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

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

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



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



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



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

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

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

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


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





而我就像是
1b761c13b4439463a77ac8abf563677d.png


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



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



代码实现


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


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

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

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

行吧,咱就这样吧




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

入职新公司一周了

前言 也是找了许久的工作,终于在这周入职新公司了。 本文就是想简单分享一下自己离职、找工作的经历和入职新公司一周后的一些感想吧。 离职 其实我在上一家公司就呆了几个月,但是整个过程中内耗非常严重,常常焦虑到失眠,有次甚至失眠到半夜四点起来跑步缓解焦虑。 后面觉...
继续阅读 »

前言


也是找了许久的工作,终于在这周入职新公司了。


本文就是想简单分享一下自己离职、找工作的经历和入职新公司一周后的一些感想吧。


离职


其实我在上一家公司就呆了几个月,但是整个过程中内耗非常严重,常常焦虑到失眠,有次甚至失眠到半夜四点起来跑步缓解焦虑。


后面觉得这样下去也不是办法,咬咬牙就裸辞了(大伙们还是不要学我,多少是有些任性了)。


提出辞职的时候其实同事和领导也有挽留,提出可以让我休息几天,调整放松一下。不过当时觉得既然提出了辞职,再呆下去其实也不好,最后还是走了。


离职后还是会基本每天写代码保持手感


离职后其实并没有完全放松摆烂,还是会在网上看一些知识点,每天抽点时间出来写写学习记录和学点新东西。因为我怕自己太放松的话,不利于后续复习找工作,所以还是会写点东西保持手感。


重新找工作,面试机会很少


说实话,面试机会真的很少,在招聘软件上和人事沟通,基本都是已读不回。找工作全程有回复的可能都是一些外包公司,但是其实我个人感觉我是没什么机会去这些外包公司的,一方面个人不太愿意去外包,另一方面就是其实外包大概率不会要我这种非科班的且工作经验不足三年的(2021年毕业的),外包人事和你聊天基本就是三连问:


请问你是四年全日制、学信网可查、本科学历吗?请问你的上一份工作离职原因是什么?请问你上一份工作的薪资以及你的期望薪资是多少?


然后你给他一顿回复,接下来就是问你要个学信网截图,就没有后续了。


找工作过程本来就很煎熬,后来看到新闻说今年除夕不放假,心里也想过要不直接现在收拾东西回家过年算了,工作什么的过完年再说,但是我哥和我大学舍友都劝我耐下心继续找找,加上我离职并没有告诉家人,最后还是没直接回家,而是试着再找一个月试试。


转机 - 11月


前面在招聘软件上问了几百个都没几个面试机会,已经是聊怕了。开始以为是自己的打招呼语不对,引不起对面的兴趣,所以期间招呼语改了很多次,从开始使用默认的,到对应着不同岗位需要的技能着重介绍对应的能力,但是都没什么用。


期间可能就面了两个还是三个吧,都是那种公司的唯一一个前端要走了,然后让这个唯一的前端走之前帮忙面试一个替代者,这些面试的时候问的问题相对都难点,还会问一些计算机网络和运维的东西,都是毫无意外都挂掉了。


但是不知道为什么,到了11月中下旬,面试机会好像多了起来,好像后面11月后面的三周里,基本每周都有两三个面试,虽然大多都不太满意(我不满意对面,估计对面也不满意我,哈哈哈),不过起码面试机会多了起来,具体什么原因我也不清楚,可能是11月离职的人特别多?


其中有一个面试就是我后面入职的公司,也是随机投的公司。开始人事问我有没有在外包公司呆过,我说没有(还真没有)。又问了下到岗时间,我看了下才发现公司就在我出租屋附近,走路就十分钟的路程,连忙说我就住附近可以随时入职。然后问我能不能简单说一下自身的优缺点,我面对这些问题已经不太想回答了,因为前面回答过很多个类似的问题,但是都没后续了,所以当时也没回复,但是没想到的是晾了人事一天后,她又找我了,让我加一下微信约一下面试时间。


加微信后先是给我发了一些公司的相关网站和介绍,让我先了解一下,有问题可以问,后续没什么问题的话就和我约个具体的面试时间。我其实也没有太细看,直接说可以约面试,然后就约了两天后的面试。


面试前的准备


前面面试了好几家都是卡在性能优化和亮点或者难点这些问题上,说实话自己前面的工作也就是个业务仔,具体确实没有什么太能说的亮点,所以这些问题都答得不好。


后面自己简单系统过了下webpack,最后联系自己实际工作的项目,简单总结了一下性能优化方面的知识点,面试前暗暗想着千万不能再在这里卡住啦~


后面回头想想,其实实际工作中,即使自己没做一些有难点或亮点的东西,公司其他领导或同事肯定或多或少都会做的,你只需要将公司的东西消化掉,转换为自己的东西,将一些知识点串联起来就好。


面试当天


面试当天,提前一个小时出门,但是没想到路程真就和导航说的一样,走路十分钟就到了,因为很紧张,所以我也没有提前上去公司,就在楼下等。期间一遍又一遍看自己辞职后在有道云上的总结,希望自己能表现好一点。


等到面试时间差不多了,坐电梯上到公司,才发现一层楼都是他们的。刚进去的时候他们正在开分享会,用广播分享的,公司环境也不错,人事小姐姐带我到了一个房间,说因为其他会议室正在使用,所以暂时只能带我到直播间这里面试了,我嘴上回复说好的好的,其实心里想的是公司居然还有直播间的啊?厉害厉害。


人事小姐姐给我装了一杯水,让我稍等下,她去叫面试官,我说好。等她走后,我努力让自己平复心情,准备接下来的面试。


等了一会,来了两个面试官,简单介绍了下就开始面试了。


其实那天整个技术面试面了一个多小时,但是太具体的问题也记不起了,基本都是问的一些知识点,没怎么聊项目。对于我自己来说,我其实更希望他问知识点的,因为我自认为自己这些知识点自己还是比较容易应对的,整个过程我也觉得我的发挥很好,基本所有问题都能答并且答得还算可以(自我感觉),当面试过程中进入了自己的舒适区,就会觉得一切都是很顺其自然,问啥都能立即给出回复。


技术面面完,两位面试官让我在这稍等一下,他们去和人事沟通一下。这个时候我觉得就是有戏了,等两位面试官走出房间后,我自己松了一口气,发现自己身上都在冒汗,自己对于面试还是太紧张了。


后面人事小姐姐进来,又和我聊了一个小时,个人觉得这个人事小姐姐还是很专业的,而且她给我一种很舒服的感觉,笑起来很好看,聊的过程中还是很放松的。聊完后人事小姐姐说如果这两轮面试我都通过的话,最后还会有一个总监面,结果会在一到三个工作日通知我。


我心想已经面了两轮了,技术面+人事面,后面还要有总监面啊?不过也没什么办法,走之前点头示意说好的,希望能和你成为同事,然后就回去等通知了~


等待三面通知


前面两轮面完,其实已经感觉元气大伤了哈哈,很少试过面试两个多小时的,感觉精力都耗尽了。虽然我个人觉得能面这么久的,应该都是很有戏了,但是offer没到手前还是不心安。


面完回到家,躺了一会,又继续准备面试了,因为第二天也有一个面试,不过这个面试就不细说了,大概就是他们是一个需要出差的岗位,我不太愿意,就没后续了。


后续就是我的心思都在这个公司上了,已经不想再投简历了,一心在等待三面通知,等了两天,人事小姐姐终于在微信联系我了,先是恭喜我过了前面两轮面试,约我下一周的周一进行总监三面。


三面以及接受offer


当时三面,来面我的不是技术总监,而是另外一条线的主管,他说是因为技术总监刚好那天请假了,所以才暂时换了一个面试官。


不过其实三面也没问什么,简历聊了十分钟就结束了,问问加班怎么看之类的,给我的感觉更多是已经要我了,只是走个流程让总监见见我。


面完后也还是说一到三个工作日给我通知,回去路上我看了看招聘软件,发现招聘软件上我和这个公司的聊天记录没有了,在搜一下这个公司,发现招聘的岗位也没了,吓得我以为他们已经招了其他人了,小心翼翼在微信上问了下人事小姐姐,不过还好,人事小姐姐说心仪人选就是我,只是他们内部还要讨论一下,如无意外的话后续会和我讨论offer。


至此心里的石头终于放下了~


后面也是简单安排了下背调,然后就给我发offer了,本来想找个周四或者周五入职,这样入职后上两天班有个周末缓冲一下,不过那边给安排了周一,我也没多说什么,想着找到工作了,就这样接受吧~


入职当天


个人性格有点奇怪,每次要去到一个新环境时,前一晚都会失眠,这次也一样,失眠到凌晨两三点才睡着,倒也不是说害怕新环境,这种感觉也说不出具体,反正就是会莫名其妙多想。


入职当天九点半前要到公司报道,我大概九点十五分出门,步行走路十分钟就到了,心想以后午睡也能回家躺了(不过这也还是太理想了,午休一个半小时跑回家,来回也要消耗20分钟路程,得不偿失)。


到公司后,人事小姐姐带我交接一些入职文件和签订对应合同后,带我逛一下公司环境和介绍下需要对接的领导,到工位后给我发了一份入职礼包和一个新的联想笔记本。入职礼包包括抱枕、纸质笔记本和几个书签,都是带有公司logo的,然后其他事情就是在飞书上交代了。


所有的入职相关文档都在飞书上,人事小姐姐和直属主管给我发了入职后需要观看的相关文档,第一天的内容就是看看文档和配置环境这样。


另外就是入职有个环节是要到自己的部门群和公司大群发个自我介绍,这也是行例公事了,之前入职的公司都有这一步。我在群里简单介绍了一下自己,哪里人、职位、什么爱好啥。


我说我爱好是打篮球,立马被同事拉进公司的篮球群了,说公司每周五会有篮球活动,有空可以参与一下;


我说我喜欢的歌手是陈奕迅,立马有人问我要不要一起去看演唱会,我只能无奈说没抢到票;


我说我喜欢的作家是韩寒,虽然他很久没出书了,居然也有人说犹记得当年看的韩寒第一个作品就是《卧梅》......


已经感受到新同事们的热情,希望后面也能好好相处~


入职一周的一些感想


入职一周了,主要可能有两个点想说说的:


第一点就是入职当天知道的,当天下午没什么事干的时候,飞书上面收到私聊信息,说是公司的实习生,岗位是前端,他也入职不久,所以想和我交个朋友。


我当然非常乐意,就和他闲聊了一下,才发现原来他是大二的,我心想大二就已经出来实习了吗?回想自己大二还在宿舍玩加里奥玩得天昏地暗,不免有点惭愧。


如果你问我对大学生有什么建议,我的建议是,如果你的目标是毕业后工作而不是考公考研这种,那么就尽早找实习吧,特别是对于学历一般的学生来说,实习经验才是找工作的敲门砖~


至于该怎么规划,怎么找实习,怎么提升自己的能力这些问题,我个人其实给不了太多建议,因为当年自己也没有好好想过这些问题,所以我也不在这里误人子弟了,还是要自己多多向有经验的长辈或者查阅相关资料。


第二点,对自己说的,也是对各位入职工作一两年的大伙们说的:没有白费的努力,你所有的付出,可能都会在你以后某个时间点给你带来意想不到的回报。


我是从两个方面感受到这些的:面试和入职后的工作安排。


我从上家离职后,基本每天都有保持在有道云笔记上写每日计划和记录每日知识点,或多或少,可能昨天学习了很多,写了很多笔记,也有可能今天偷懒了,没有怎么记录,都没关系的,主要是坚持。我也常常怀疑自己这样经常记录到底有什么用,但是里面记录的知识点,最后都在我面试的时候,给予了我很大的帮助。


到后面入职后,我接到的第一个需求就是要需要搭建两个新项目,刚好我在离职后有私下学习并搭建过对应相关技术栈的项目,并且记录了比较多的笔记,所以在上手搭建项目的时候,我可以直接从我笔记中查阅对应问题的解决办法,相当于无缝衔接了。


如果我当时没有学习这一部分内容,也不是说不能从0开始百度,但是学过后,至少给了我很大的底气,自己做过的东西起码不会太过慌张。


所以伙伴们,自己现在正在努力的事情,即使短期内见不到成果,也请你不要着急,努力不会白费的。


最后


其实写到这里,我翻看上面的内容,总还是感觉自己还有很多东西想要表达的,但是都没能表达出来,比如说面试的一些具体准备、面试时碰到一些什么问题、入职后如何开展工作等等,后续如果有机会,再详细写写相关内容吧。


最后希望自己能够顺利融入新公司,努力提升个人能力~


我是一名非科班的普通二本前端程序员,期待和大家一起成长~


作者:3iggins
来源:juejin.cn/post/7310786554488881190
收起阅读 »

我在美团三年的前端工程化实践回顾

web
时间过得真快,从20年9月加入美团,转眼已经三年了。在美团的这几年,我应该有接近一半的时间,在做前端工程化相关的工作。 三年,正好合同已经到期,也到了离开的时候,最近相对不忙,正好回顾一下自己做前端工程化的一些思考与踩过的坑。 对前端工程的理解 前端技术的演进...
继续阅读 »

时间过得真快,从20年9月加入美团,转眼已经三年了。在美团的这几年,我应该有接近一半的时间,在做前端工程化相关的工作。


三年,正好合同已经到期,也到了离开的时候,最近相对不忙,正好回顾一下自己做前端工程化的一些思考与踩过的坑。


对前端工程的理解


前端技术的演进


在谈前端工程这个概念之前,我们先回顾一下2000年以后前端技术的演进,主要分四个阶段:



  • 页面开发阶段(2000~2009) :在ECMAScript 2009发布之前,很多前端工作都是以单页面开发为主,需要重点解决兼容性问题,靠工具库提高效率,代表技术如:jQuery、ExtJS等。

  • 模块化开发阶段(2009~2015) :以模块化开发为主,要解决性能问题,靠构建工具和UI框架提高效率,特别是基于Node.js的各种前端工具,代表技术如:Angular、React、Less、Gulp等。

  • 应用开发阶段(2015~2022) :以应用开发为主,要解决工程化问题,靠自动化工具和跨平台提高效率,代表技术如:Webpack、React Native、Flutter等。

  • 智能辅助开发阶段(2022以后) :将前端工程化与 AI 结合,将重复冗余的流程通过智能化实现开发提效,实现智能代码生成、评审、智能编写单测、代码语言转化等。


软件工程的三要素


同时,我们也需要了解一下软件工程的概念。1983年IEEE是这么定义的:软件工程是软件开发、运行、维护和修复软件的系统方法。


基于此,软件界一些前辈提出了软件工程的三要素:



  • 方法:是完成软件开发的各项任务的技术方法,为软件开发提供“如何做”的技术。

  • 工具:为运用方法而提供的自动的或半自动的软件工程的支撑环境。

  • 过程:是为了获得高质量的软件所需要完成的一系列任务的框架。


前端工程化的定义


前端工程化这个词,是国内前端圈子2018年前后才出现的,大概的意思是将(后端已经比较成熟的)许多软件工程概念、实践、工具引入前端开发,提升开发效率。


关于前端工程化的定义,众说纷纭。我们团队在21年初,经过三个多月的调研和讨论,才形成了一个大家都能认可的定义:在前端开发和运维过程中,以降低成本、提高效率、保障质量为目的,通过一系列规范、工具、流程(分别对应软件工程中的方法、工具和过程)作为手段的实践体系。


前端工程化的演进


美团由于业务广泛,大大小小的前端团队得有30个以上,每个团队的业务场景不同,都会建设或采用一套合适的前端工程方案,但其演进过程,一般都会经历以下阶段:



  • 工具化:以针对各自业务场景开发脚手架为主,内置常用的前端组件库,提供代码格式检查、埋点及监控等插件,提升项目初始化的效率。

  • 规范化:面向完成需求的整个研发流程,梳理需求管理、视觉交互设计、评审、开发、联调、测试验收、上线部署和质量监控等相关的规范,进一步建设工具来约束研发过程中的不确定性。

  • 平台化:将支撑研发的有关工具和系统聚合起来,通过套件和插件的设计模式,实现对不同场景的支撑,支持在线初始化项目,横向打通研发的整体链路。

  • 体系化:紧跟前沿技术,集成低代码、在线IDE、代码智能生成或推荐等能力,建设需求、设计、研发、运营一体化的云开发平台。


中后台项目的工程化实践


工程化演进的动力,源于业务复杂度的增加及团队规模的扩大。我在美团工作过的两个部门,都是属于基础研发平台的,在我加入后,所在前端团队需要开发维护的中后台项目都在变多(第一个部门有60多个,第二个部门也有10多个),团队规模也进一步扩大(第一个部门30多人,第二个部门10多人)。


在第二个部门的工程化,主要借助我在第一个部门的前端工具建设,进行定制化应用。因此,着重介绍一下我在第一个部门的前端工程化实践。


工具建设


团队的工具建设,开始于2018年,为了建设美团私有云平台,需要收拢美团基础研发平台所有 IaaS、PaaS 产品,预期两年内会有几十个增量项目接入,我们需要提供高效、稳定的前端支持。急需解决的问题有:



  • 缺乏研发工具。 这一时期,我们的开发手段还比较原始,业务强相关的大量重复工作难以避免,如:前端工程的搭建,接入统一通用的SDK。

  • 机器资源不足。 当时的前端项目还是直接部署在机器上,团队能申请的机器资源有限,难以承接即将接入的大量项目。


针对这两个问题,首先我们建设了自己脚手架工具,并统一了研发流程:



  • 项目模板:我们提供了两套模板用于初始化项目,一套适用于接入私有云平台(面向美团所有研发,需要统一的顶导、侧导,对视觉交互要求高,上线管控严格),一套适用于普通的后台管理(只是部门内少数研发使用,重在快速实现功能)。

  • 研发工具:通过一套自研的中后台组件库把控整体的视觉交互,并提供私有云平台本地开发调试的代理转发工具,解决接口请求的鉴权问题。

  • 集成服务工具:提供了将本地静态资源发布到云存储和接入公司埋点监控等服务的工具,简化和统一了不同项目接入相同的服务。


其次,我们升级了静态资源部署方案:团队的前端大多数项目都是纯静态页面,可以使用云存储代替机器存储,从而解放大多数机器资源。故而我们基于 s3plus 对象存储,研发配套的部署工具,实现了静态 web 项目的无服务架构。


规范制定


到 2020 年的时候,我们需要支持 80 多个基础技术中后台项目的前端工作,当时团队支持项目上存在以下 2 个问题:



  • 无规范,协作难。 随着团队规模的扩大,各个小组的规范和工具出现分叉,同类项目有多套规范及协作工具,跨项目及跨职能协作同学认知和上手成本高,跨项目协作或人员调动阻力大。

  • 工具分散,存在内耗。 团队共存多套同类工具,低水平轮子多,维护成本高;工具没有形成生态,不能发挥规模效应,效率提升有限。


首先,我们联合多个小组接口人,共同梳理标准规范,并通过标准宣讲,拉齐各职能角色的认知,最终形成了六个大类(分别为需求、设计、研发、发布、架构和运维)、26个小类的文字版规范。


然后,我们联合多个部门的前端物料接口人,基于中后台项目前端界面的常见场景,制定了统一的设计规范,从零建设区块和页面模板库,整合已有的基础组件、业务组件和项目模板,形成了完整的前端物料体系。


接着,我们把发布工具从 Plus 平台迁移至 DevTools 流水线,并且通过 WebStatic 平台进行静态网站托管。这样的好处是,发布规则可以通过流水线定制,加入标准化监测度量等工具,从而实现卡控;流水线运行的容器天然可以作为中转站,将前端资源发布S3,解放了机器;WebStatic 平台接管静态网站托管,可以让我们省去复杂重复的路由配置。


最后,我们采用“普法”和“执法”并重的原则,首先通过课程分享和改造宣讲,普及并对齐标准化的价值,完成团队“普法”过程。“执法”前,我们基于标准沉淀多种一键接入工具降低接入成本;无法自动接入的标准,官方给出最佳实践及预计改造时长,协助业务同学排期;“执法”中,提供了检查工具,用于标准校验并收集项目标准化数据,帮助标准化持续运营。


同时,我们把规范标准区分“强制”、“推荐”两个等级。存量项目只需遵守“强制”等级,不影响项目进度的前提下达成团队标准下限;对于增量项目,提供工具高效遵循全部标准。


搭建平台


2021年团队引入了大量前端外包同学,原本的研发工具及增量项目的服务搭建对于外包同学,都有较高的学习成本,因此这一年我们将提效的重点放在了研发工具的体验优化,以及发布规范、架构规范的配套工具落地。主要解决如下问题:



  • 架构规范难学习。项目从创建到上线,需要对接代码仓库、Appkey、发布工具、资源托管服务及网关配置等,涉及基础服务产品多,申请及系统切换操作复杂,即使工作经验丰富,也未必熟悉项目所需的全部中间件。

  • 部分研发工具难上手,体验较差。研发套件中包含工具类型繁多,建设初期,文档完善程度参差不齐,高频使用的物料工具对于新人上手也不够友好。


我们决定通过建设研发工作台落地架构规范,通过自动化的研发流程串联降低新人学习成本,快速搭建增量项目;同时,为解决研发套件使用体验问题,我们同步建设了 VSCode IDE插件,集成高频使用的插件、物料使用等工具,降低学习和切换成本,增强用户的使用体验。


同时,为满足不同业务场景的定制需求,我们将各场景研发流程抽象成 「场景模板」, 它是最佳实践的载体,前文中的自动化创建流程就是基于场景模板来串联。


场景模板由初始化模板(生成项目基础结构),研发工具插件(CLI 层面的插件 preset),基础服务配置方案组成,每部分可以灵活配置,一定程度上满足不同业务场景的定制需求,团队工程负责人可以按需定制自己的场景模板。


平台化以后,我们的前端工程方案就可以满足公司更多部门接入使用,发挥更大的价值。比如,我转岗的部门在推进工程化的时间,基于这套方案,只需结合视觉项目的特点,替换前端物料、生成项目的脚手架即可。


形成体系


2022年外包团队规模和产品规模即将进一步扩大,然而当前工具对于效率的提升也逐渐出现瓶颈,我们期望对当前的主要业务场景,即对中后台业务,进行深度提效的探索。另一方面,现有大部分规范已有配套工具保障,但前置的需求以及后置的运维环节依然没有形成闭环,我们期望平台能有更沉浸式的体验,建设中后台场景的体系化解决方案。需要解决以下问题:



  • 提效瓶颈,研发提效工具待加强: 分析业务现状和参考业界,传统编码(ProCode)的辅助工具完整性和易用性需持续加强,业界的低代码(LowCode)实践也很适用于我们的中后台场景,我们也将在这一领域探索建设。

  • 需求、运维规范未闭环: 当前的平台能够串联从创建项目到部署的研发流程,但是前置的需求、设计管理和后置的运维规范还不完善,对于相关工具(如项目管理和监控工具)的应用也没有形成标准。


我们希望将研发工作台打造为云开发平台,通过集成在线 IDE 开发工具和低代码自动化研发工具,对于中后台场景深度提效;同时也要与项目管理、设计平台、监控等平台加深协作与融合,串联前置的需求设计环节与后置的运维环节,形成中后台场景的体系化开发平台。


sdk项目的工程化实践


2022年初,为了从北京换到深圳定居,我换了部门,在新的部门,需要开发维护的npm包比之前多了很多。如果没有统一的工程标准,不仅开发维护的效率很低,同时SDK的易用性也会比较差。


过去一年,我参与了多个SDK项目的开发维护工作,同时前两年也参与了面向公司的中后台前端项目的工程建设,于是我将这些实践和采坑经验进行总结,形成了一套前端sdk项目的工程标准。


业务项目和SDK项目的区别


通过表格对比可以看到,业务项目和SDK的项目还是有较大区别的,除了有一些公共标准可以复用外,SDK项目需要增加打包构建、发npm包、多包管理、文档管理和门户建设等相关的工程标准。


类型产品要求使用方式技术栈
业务项目更加注重功能的实现,如果是C端项目,还需要关注首屏。多数都过网页链接使用。以Vue/React框架开发为主(需要非常熟悉相关框架的api),还会涉及HTML、CSS,多数使用Webpack进行打包,一般不用考虑测试。
SDK项目更加关注稳定性、兼容性、性能、包大小和使用文档。一般通过npm包安装到项目中或在html 文件中以cdn嵌入脚本使用。纯JS/TS开发为主(需要深入了解编程语言特性,比如类的创建、继承和各种封装),除了组件库基本不涉及HTML和CSS,多数使用Rollup打包和使用Jest进行单元测试。

工程方案


我们基于业务项目制定的规范,对于有差异的部分,比如依赖包管理方案、目录结构、文件命名、本地开发等,制定了新的规范;而对于没有包含的部分,比如文档管理、官网建设、发布npm包及其权限管理等,补充了新的规范。


我们基于前面建设的云开发平台,提供了一个面向 SDK 开发的场景模板来创建项目:创建成功后,会自动注册前端appkey,创建仓库并使用sdk的项目模板初始化项目,把sdk官网的静态资源接入webstatic,并接入发布流水线,提供默认域名供用户访问。


相比之前的方案,这个方案不仅开发调试及发布验证更加方便,还能提供默认的域名访问该sdk网站,让用户可以快速查看相关的接入文档和教程,体验sdk提供的功能。


相关说明


整个项目使用vite工具进行构建,使用rollup 进行打包,打包成功后,即可通过本地或流水线发布到公司的私有npm。


我们没有使用lerna进行多包管理,而是使用了pnpm的方案,所以要求必须本地安装pnpm,然后通过pnpm安装package.json文件中引入的npm包。


只需配置根目录下的pnpm-workspace.yaml文件即可,示例如下:


packages:
- 'packages/**'
- site

在packages目录下,一个子目录对应为一个子npm包。site 目录为sdk 对应的官网代码,本地开发时,可以在site 中的某个页面,引入packages中的某个包进行源码调试。


在根目录执行 npm start 就可以打开sdk 官网了,然后跳转到demo 演示的页面,修改site目录下对应页面或者packages下对应的npm包,即可开始进行SDK的开发调试了。


所有的文档管理相关代码,都在site 目录下的src目录,如果需要更新文档,直接在markdown 目录编辑对应的文档即可。


如果发布了新的sdk,需要验证sdk的可用性,需要先将site目录下package.json文件对应的npm包修改为最新的版本,提交后远程仓库后,再选择对应流水线发布到自己想验证的环境。


总结


在美团工作的三年,在技术和视野上,对我的帮助都很大,接触的领导和身边的小伙伴都很优秀,有些工作是我对最终结果负责,有些我只是重在参与。


我们会把事情分为业务开发和框架(工具)开发:



  • 业务开发主要是实现产品需求,要对业务有深入了解,掌握团队所用到的技术栈和工具。

  • 框架(工具)开发主要是为提升业务开发效率而开发的框架或工具,框架是把系统可复用层抽象出来,如网络层,存储层等,工具是研发过程中效率工具,如自动化测试工具,持续交付工具等。


通常上系统是有多个模块组成,那么会有一个从复杂到简单的拆解过程,既然系统有分层架构,那我们会按照每个人技术水平来安排不同复杂度的工作。我们要不断提升两个标准,一是通过对人才的培养提高上限,二是通过工程工具建设提升团队下限。


工程化永远是围绕着质量、体验和效率三个维度进行建设,来保证高效、高质量地完成业务需求,减少跨项目、跨团队的协作成本。但前端工程化不是万金油,它是在特定时期面对特定场景的解决方案。


平台体系的建设往往会被业务结构和技术架构所约束,要尽量结合团队的业务场景和技术现状来制定合理的解决方案,避免仅凭个人的技术思考来主观驱动,所以还是要结合自身组织特点,先清楚地认识自己所处的阶段,再去实践并验证。


老王(王慧文)在演讲曾提到过:“不要为自己设限”,所以前端工程师在前端工程化中,应该积极承担业务工程化建设或工程工具建设工作。《论语》中说道:“工欲善其事,必先利其器”,所以面对复杂工程,我们要学会用工具来提升效率,使复杂问题简单化。


2023年我的主要精力都在做前端智能化,在工程化上的投入比较少,但是我相信借助AI,前端工程化一定会迎来重大的变革。


作者:三一习惯
来源:juejin.cn/post/7268533072995598347
收起阅读 »

前端学哪些技能饭碗越铁收入还高

web
随着经济的下行以及移动互联网发展趋于成熟,对软件开发人员的需求大大减少,互联网行业所有的公司都在降本增效,合并通道,降薪裁员的新闻层出不穷。 但相比其他行业,互联网行业的从业者薪资还是比较可观的,但要求也比之前高了很多,需要大家掌握更多的技能和在某些技术领域深...
继续阅读 »

随着经济的下行以及移动互联网发展趋于成熟,对软件开发人员的需求大大减少,互联网行业所有的公司都在降本增效,合并通道,降薪裁员的新闻层出不穷。


但相比其他行业,互联网行业的从业者薪资还是比较可观的,但要求也比之前高了很多,需要大家掌握更多的技能和在某些技术领域深耕。


本文,我们就聊聊,掌握了哪些技能,能让前端同学,收入高且稳定。


端智能


首推的是端智能,很多行业大咖都认为,随着ChatGPT的横空出世,开启了第四次工业革命,很多产品都可以用大模型重做一遍。当前,我创业的方向,也和大模型有关。


当前的大模型主要还跑在云端,但云端的成本高,大模型的未来在端智能,这也是小米创始人雷军在今年一次发布会上提出的观点。


在2023年8月14日的雷军年度演讲中,雷军宣布小米已经在手机端跑通13亿参数的大模型,部分场景效果媲美云端。


目前,端上大模型的可行性和前景已经得到了业内的普遍认可,国内外各个科技大厂在大模型的端侧部署领域均开始布局,目前大量工程已在PC端、手机端实现大模型的离线部署,更有部分App登陆应用商店,只需下载即可畅通无阻地对话。


我们相信,在不久的将来,端上大模型推理将会成为智能应用的重要组成部分,为用户带来更加便捷、智能的体验。


我在美团从零研发了web端智能推理引擎,当时立项时,就给老板画饼,美团每天的几百亿推理调用,如果有一半用端智能替代的话,每年能为公司节省上亿元。


要想掌握端智能,需要学习深度学习的基本知识,还要掌握图形学和C++编程,通过webgl或webassembly 技术实现在Web端执行深度学习算法。


图形学


前面提到的端智能,只是涉及到了图形学中的webgl计算,但图形学的未来在元宇宙,通过3D渲染,实现VR、AR、XR等各种R。


计算机图形学是一门快速发展的领域,涵盖了三维建模、渲染、动画、虚拟现实等众多技术和应用。在电影、广告、游戏等领域中,计算机图形学的应用已经非常广泛。


熟练使用threejs开发各种3D应用,只能算是入门。真正的图形学高手,不仅可以架构类似3D家装软件的大型应用,而且能掌握渲染管线的底层原理,熟练掌握各种模型格式和解决各种软件,进行模型转换遇到的各种兼容问题。


随着计算机硬件和算法的不断进步,计算机图形学正迎来新的发展趋势。


首先是实时渲染与逼真度提升



  • 实时渲染技术:随着游戏和虚拟现实的兴起,对实时渲染的需求越来越高。计算机图形学将继续致力于研发更高效的实时渲染算法和硬件加速技术,以实现更逼真、流畅的视觉效果。

  • 光线追踪与全局照明:传统的实时渲染技术在光照模拟方面存在挑战。计算机图形学将借助光线追踪等技术,实现更精确的全局照明效果,提升场景的真实感和细节表现。


其次是虚拟与增强现实的融合



  • 混合现实技术:计算机图形学将与传感器技术、机器视觉等相结合,推动虚拟现实与增强现实的融合发展。通过实时感知和交互,用户可以在真实世界中与虚拟对象进行互动,创造更沉浸式的体验。

  • 空间感知与虚拟对象定位:计算机图形学将致力于解决空间感知和虚拟对象定位的挑战。利用深度学习、摄像头阵列等技术,实现高精度的空间感知和虚实融合,为虚拟与增强现实应用带来更自然、精确的交互方式。


再次是计算机图形学与人工智能的融合



  • 生成对抗网络(GAN)在图形生成中的应用:GAN等人工智能技术为计算机图形学带来了新的创作手段。通过训练模型生成逼真的图像和场景,计算机图形学能够更便捷地创建大量内容,并提供个性化的用户体验。

  • 计算机图形学驱动的虚拟人物与角色生成:结合计算机图形学和人工智能技术,研究人员正在努力开发高度逼真的虚拟人物和角色生成方法。这将应用于游戏、影视等领域,带来更具情感表达和交互性的虚拟角色。


最后是可视化分析与科学研究。



  • 一是大数据可视化:随着大数据时代的到来,计算机图形学在可视化分析方面扮演着关键角色。通过创新的可视化方法和交互技术,研究人员能够更深入地理解和分析庞大而复杂的数据集,揭示潜在的模式和趋势。

  • 二是科学数据可视化:计算机图形学在科学研究中的应用也日益重要。通过将科学数据转化为可视化形式,研究人员能够更直观地理解复杂的数据模式和关系,加快对科学问题的洞察和发现。这种可视化分析有助于领域如天文学、生物学、气象学等的研究进展。


工程提效


其实,过去三年我在美团的工作,至少有一半的精力是做和工程提效相关的事情。当然,也做了降本的事情,从零搭建外包团队。


就像我之前总结的文章:我在美团三年的前端工程化实践回顾 中提到那样,前端工程提效,一般会按照工具化、标准化、平台化和体系化进行演进。


相比前面的端智能和图形学,除了建设低代码平台和WebIDE有点技术难度,其他更多需要的是沟通、整合资源的能力,既要有很强的项目管理能力,又要人产品思维。


前两个方向做好了我们一般称为技术专家,而工程提效则更偏管理者,未来可以成为高管或自己创业。


总结


端智能和图形学,我在美团都尝试过进行深耕,但自己性格外向,很难坐得住。工程提效做得也一般,主要是因为需要换城市而换了部门,没有机缘继续推进,在美团很难往上走,所以只能尝试自己创业。


作者:三一习惯
来源:juejin.cn/post/7310143510103064585
收起阅读 »

人生似乎总是碌碌无为2023年终章

最近瞎忙,断更已久,环境如此,也无法避免这种情况。每个人的成长及其心路历程不一样,而我却走的尤为的崎岖。因为各种因素放弃了太多,这么一想不免有点矫揉造作了,时至今日,也只能无悔每一步选择,套用我经常开解自己的一句话“每一种选择都有选择它的原因,哪怕自己并不知道...
继续阅读 »

最近瞎忙,断更已久,环境如此,也无法避免这种情况。每个人的成长及其心路历程不一样,而我却走的尤为的崎岖。因为各种因素放弃了太多,这么一想不免有点矫揉造作了,时至今日,也只能无悔每一步选择,套用我经常开解自己的一句话“每一种选择都有选择它的原因,哪怕自己并不知道为什么”。


从20年开始觉得自己需要抢救一下开始,慢慢的读了特别多的书,无论是电子小说,还是各种成长、锻炼、心理层面都读。也不是说完全没有用,只是吸收不了,看小说,会发现小说其实是和现实环境关联的,懂了一些人情世故,锻炼可以以正确的方式缓解修复情绪而不是暴饮暴食,心理层面的更多是感受自我。反正杂七杂八的都看,微信读书上,加入书架的书已经好几百本了,读了也会200本了,这其实也是一种缓解自我焦虑的一种方式吧。


不记得是哪本书了,告诉了我,自我分为多层的,一层是本能层,一层是意识层,意识层更多是指经过大脑思考的,而不是基于肠胃、身体感受器官的本能反应,23年中旬的时候,我才开始用意识层去感受身体的每一次反馈,这和冥想带来的感觉非常像,但是我冥想却不行。当自我感受到很多反馈的时候,慢慢的理解了之前自己的很多莫名其妙的行为,比如生气、暴饮暴食、做一些莫名其妙的决定等。当然现在也不能完全清楚,只是开辟了一个新的方向,比如说压力比较大的时候,自我可能会过滤掉高压力的感受反馈,但是通过身体的感受,比如说唾液的分泌情况,肠胃的状态,心率,皮肤的弹性等等,就会发现自己处于自我高压状态。这似乎和中医很类似,人是一个整体,大脑调配身体的各个器官的功能运作,在本能的趋利避害的情况下,却会过滤掉一些东西。而我尝试打破这一屏障的方式却是整了一场半麻的肠胃镜手术,我称之为 触神 ,当时肠胃感觉不太好,又没有什么大的情绪触发条件,这么一趟下来,感觉自己终于进入了身体感受的状态,之前自我感受不到肠胃的信号,或者说是过滤掉了肠胃的信号,这种心理层面的通过身体体现的状态叫做代偿失调,就是说别人CPU或者冷暴力或自己压力过大的时候就容易出现的问题。


学习类的书籍也看了不少,这也是我选择 触神 的原因之一。我很喜欢的一句话是 了心苦而不起苦 ,感受自己其实是可以减缓这种苦的。非小说类的从1.5倍速的听到1.3倍的听,然后是边听边看,到开始整理笔记,这一步走了2年,也开始喜欢上了这种记笔记的写作方式,当然还是走了很多弯路,比如说,专门去学习如何写作,完全是搞错了方向。


正文


2023年11月的时候,微信读书推荐了一本书《只管去做》。这也是这篇笔记的由来,里面很多知识点很多书籍都看过,但只有这一次是记住了的。



  • 人的精力是有限的,所以需要找到最想做的事情这,而不是全都要。

  • 想要的做到的一定得出自内心,也就是上面说的自身感受里面渴望做到的事情。

  • 结合精力有限理论,将目标进行分解,分解到天,这个过程中就可以知道有哪些难题。

  • 尽可能的预想到所有意外出入,然后提供解决方案,这能有效的避免出现意外情况导致目标被放弃的情况。

  • 仪式感很重要。

  • 承受痛苦远比改变容易。但感受痛苦更容易做出改变。


那么对我而言应该怎么做呢?前一段时间,我似乎许下过宏愿,写完Android fw 基础blog,但是当我实践起来的时候,因为时间不够,基础不扎实,一篇blog 往往需要3到4个小时,同时需要进行知识的串联,但是一周能有几个写笔记的3到4个小时呢?于是这个就暂停了,同时我将事情想简单了,比如说,AMS,WMS,PMS,view的绘制流程,刷新,事件分发,handler,binder这些根本就不是一篇就可以写完的,得拆开,这也就导致,我只写了几篇就放弃了。


然后还有减肥,目前穿鞋178,170多斤,所谓的正方形人类,我1到2年前就说要减到140或130,但是最多就减到了160,然后持续反弹,除开感觉减肥痛苦以外,最直观的问题就是没有形成计划,没有按计划执行,目标就是减肥,那么今天减和明天减感觉没有多少区别,而且没有考虑到精力因素,就是莽,就导致了精力不够的时候减肥,然后身体为了维持精力,就吃得多和减少其他开销了。


现在呢,这本书看了快一个月了,通过半个月的仔细研读,开始琢磨与自己契合的方式,规划目标,现在目标就包含了很多方面了,而不是单纯的学习或减肥了。


休闲娱乐


嗯,主要感觉自己是一个很简单的人,没有啥爱好,当然也是这几年一直在折腾折磨把之前的兴趣爱好干没了,现在兴趣爱好就很简单了。



  • 骑自行车到处晃,在CD还是喜欢骑一环和二环。

  • 看书,不是技术类的书籍,就是杂七杂八的看。

  • 写笔记或者blog 输出


当然了,还有一些其他的,比如说刷微博,看各种视频小说啥的,现在也没有觉得这些不好,这反而是生活的一部分。


骑自行车


现在的规划是,每周骑一次一环或二环。下班骑自行车回家不算娱乐,这个算运动。考虑的是当我减肥到160 斤的时候,我就买一个便宜的自行车。


其他休闲娱乐


看书主要是集中到早上,早上地铁上可以边听边看,几分钟一个站,还是对心神消耗蛮大的,所以不看技术类的,看一些简单碎片化的东西。


中午吃完饭也基本上刷手机。晚上回家地铁上也可以刷手机,但是晚上回家地铁上就可以刷微博,整点不需要大脑参与的东西。


写blog啥的,主要是集中在周末或者晚上,到家直接洗澡,洗完澡精力基本上就可以恢复一些,就可以搞学习或输出blog了。


学习成长


现阶段,学习的重心就不是知识面的广度了,而是知识的深度,学习还是两个方向,一是通过视频学习,一个是重新学习基础知识。这个还没有规划好,还需要斟酌。但是时间安排上,一般只能是晚上和周末了,通勤路上不适合聚精会神的思考。咋说呢,约束自己的精力之后,看书和听书就更注重吸收了,也慢慢发现代码世界和社会的区别,这是一个人性世界,只有尊重和理解了人性,才可以尝试理解别人为什么怎么做。


身体健康


人的精力是有限的,所以,我将减肥锻炼放到了早上,基本上每天早上7点醒,喝点水,花10到20分钟进行简单运动,比如拉伸,深蹲,哑铃,卷腹等,反正是一些不出门就可以运动的事情。反正得先起来,现阶段的目标就是每天起来,培养好运动的习惯。结合上下班一些快走,这一周还是减掉了1千克,成果显著,早上起来运动还有一个好处就是肌肉活跃了,整天都精力十分旺盛,这也促使了我每天走到了1万多步吧。


工作


现在养成了一个习惯,那就是先拆解工作任务,把工作任务分解到功能的最小单位,然后搞一个表格,做完一项勾一项,做到过程中也会发现没有分解到位的,又会添加到表格里面。技术方向的工作一般都是连续性的,就先分解成大的板块,那么天的任务就有了,然后基于大的板块分解为小的板块,那么一天的详细事情就出来了,按着表格一步步的执行即可,这个有点像WBS的拆解执行,通过这种工作上的反馈也可以修正自身的很多东西,也不再是想一出是一出了。


终章


从上面写到这里,其实整体的规划都围绕一个点,那就是精力。以一天的精力进行分解,去做那些确实适合自己的事情,也慢慢的拒绝了一些内耗。这和我之前追求的无咎何其相似,只是更上了一个思想层次吧。


但愿你我都可以无愧于心。


作者:luoye呀
来源:juejin.cn/post/7314178434338766858
收起阅读 »

Android Tab吸顶 嵌套滚动通用实现方案✅

很多应用的首页都会有一些嵌套滚动、Tab吸顶的布局,尤其是一些生鲜类应用,例如 朴朴超市、大润发优鲜、盒马等等。 在 Android 里面,滚动吸顶方式通常可以通过 CoordinatorLayout + AppBarLayout + Collapsin...
继续阅读 »

很多应用的首页都会有一些嵌套滚动、Tab吸顶的布局,尤其是一些生鲜类应用,例如 朴朴超市、大润发优鲜、盒马等等。





在 Android 里面,滚动吸顶方式通常可以通过 CoordinatorLayout + AppBarLayout + CollapsingToolbarLayout + NestedScrollView 来实现,但是 AppBarLayoutBehavior fling
无法传递到
NestedScrollView,快速来回滑动偶尔也会有些抖动,导致滚动不流畅。


另外对于头部是一些动态列表的,还是更希望通过 RecyclerView 来实现,那么嵌套的方式变为:RecyclerView + ViewPager + RecyclerView,那么就需要处理好 RecyclerView 的滑动冲突问题。


如果 ViewPager 的 RecyclerView 内部还嵌套一层 ViewPager,例如一些广告Banner图,那么事件处理也会更加复杂。本文将介绍一种通用的嵌套滚动方案,既可以实现Tab的吸顶,又可以单纯实现的两个垂直 RecyclerView 嵌套(主要场景是:尾部的recyclerview可以实现容器级别的复用,例如往多个列表页的尾部嵌套一个相同样式的推荐商品列表,如下图所示)。


nested2.jpg


代码库地址:github.com/smuyyh/Nest…


目前已应用到线上,如有一些好的建议欢迎交流交流呀~~


核心思路:



  • 父容器滑动到底部之后,触摸事件继续交给子容器滑动

  • 子容器滚动到顶部之后,触摸事件继续交给父容器滑动

  • fling 在父容器和子容器之间传递

  • Tab 在屏幕中间,切换 ViewPager 之后,如果子容器不在顶部,需要优先处理滑动


代码实现:


ParentRecyclerView


因为触摸事件首先分发到父容器,所以核心的协调逻辑主要由父容器实现,子容器只需要处理 fling 传递即可。


public class ParentRecyclerView extends RecyclerView {

private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();

/**
* fling时的加速度
*/

private int mVelocity = 0;

private float mLastTouchY = 0f;

private int mLastInterceptX;
private int mLastInterceptY;

/**
* 用于向子容器传递 fling 速度
*/

private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
private int mMaximumFlingVelocity;
private int mMinimumFlingVelocity;

/**
* 子容器是否消耗了滑动事件
*/

private boolean childConsumeTouch = false;
/**
* 子容器消耗的滑动距离
*/

private int childConsumeDistance = 0;

public ParentRecyclerView(@NonNull Context context) {
this(context, null);
}

public ParentRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public ParentRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}

private void init() {
ViewConfiguration configuration = ViewConfiguration.get(getContext());
mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();

addOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == SCROLL_STATE_IDLE) {
dispatchChildFling();
}
}
});
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mVelocity = 0;
mLastTouchY = ev.getRawY();
childConsumeTouch = false;
childConsumeDistance = 0;

ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
if (isScrollToBottom() && (childRecyclerView != null && !childRecyclerView.isScrollToTop())) {
stopScroll();
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
childConsumeTouch = false;
childConsumeDistance = 0;
break;
default:
break;
}

try {
return super.dispatchTouchEvent(ev);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (isChildConsumeTouch(event)) {
// 子容器如果消费了触摸事件,后续父容器就无法再拦截事件
// 在必要的时候,子容器需调用 requestDisallowInterceptTouchEvent(false) 来允许父容器继续拦截事件
return false;
}
// 子容器不消费触摸事件,父容器按正常流程处理
return super.onInterceptTouchEvent(event);
}

/**
* 子容器是否消费触摸事件
*/

private boolean isChildConsumeTouch(MotionEvent event) {
int x = (int) event.getRawX();
int y = (int) event.getRawY();
if (event.getAction() != MotionEvent.ACTION_MOVE) {
mLastInterceptX = x;
mLastInterceptY = y;
return false;
}
int deltaX = x - mLastInterceptX;
int deltaY = y - mLastInterceptY;
if (Math.abs(deltaX) > Math.abs(deltaY) || Math.abs(deltaY) <= mTouchSlop) {
return false;
}

return shouldChildScroll(deltaY);
}

/**
* 子容器是否需要消费滚动事件
*/

private boolean shouldChildScroll(int deltaY) {
ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
if (childRecyclerView == null) {
return false;
}
if (isScrollToBottom()) {
// 父容器已经滚动到底部 且 向下滑动 且 子容器还没滚动到底部
return deltaY < 0 && !childRecyclerView.isScrollToBottom();
} else {
// 父容器还没滚动到底部 且 向上滑动 且 子容器已经滚动到顶部
return deltaY > 0 && !childRecyclerView.isScrollToTop();
}
}

@Override
public boolean onTouchEvent(MotionEvent e) {
if (isScrollToBottom()) {
// 如果父容器已经滚动到底部,且向上滑动,且子容器还没滚动到顶部,事件传递给子容器
ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
if (childRecyclerView != null) {
int deltaY = (int) (mLastTouchY - e.getRawY());
if (deltaY >= 0 || !childRecyclerView.isScrollToTop()) {
mVelocityTracker.addMovement(e);
if (e.getAction() == MotionEvent.ACTION_UP) {
// 传递剩余 fling 速度
mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
float velocityY = mVelocityTracker.getYVelocity();
if (Math.abs(velocityY) > mMinimumFlingVelocity) {
childRecyclerView.fling(0, -(int) velocityY);
}
mVelocityTracker.clear();
} else {
// 传递滑动事件
childRecyclerView.scrollBy(0, deltaY);
}

childConsumeDistance += deltaY;
mLastTouchY = e.getRawY();
childConsumeTouch = true;
return true;
}
}
}

mLastTouchY = e.getRawY();

if (childConsumeTouch) {
// 在同一个事件序列中,子容器消耗了部分滑动距离,需要扣除掉
MotionEvent adjustedEvent = MotionEvent.obtain(
e.getDownTime(),
e.getEventTime(),
e.getAction(),
e.getX(),
e.getY() + childConsumeDistance, // 更新Y坐标
e.getMetaState()
);

boolean handled = super.onTouchEvent(adjustedEvent);
adjustedEvent.recycle();
return handled;
}

if (e.getAction() == MotionEvent.ACTION_UP || e.getAction() == MotionEvent.ACTION_CANCEL) {
mVelocityTracker.clear();
}

try {
return super.onTouchEvent(e);
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}

@Override
public boolean fling(int velX, int velY) {
boolean fling = super.fling(velX, velY);
if (!fling || velY <= 0) {
mVelocity = 0;
} else {
mVelocity = velY;
}
return fling;
}

private void dispatchChildFling() {
// 父容器滚动到底部后,如果还有剩余加速度,传递给子容器
if (isScrollToBottom() && mVelocity != 0) {
// 尽量让速度传递更加平滑
float mVelocity = NestedOverScroller.invokeCurrentVelocity(this);
if (Math.abs(mVelocity) <= 2.0E-5F) {
mVelocity = (float) this.mVelocity * 0.5F;
} else {
mVelocity *= 0.46F;
}
ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
if (childRecyclerView != null) {
childRecyclerView.fling(0, (int) mVelocity);
}
}
mVelocity = 0;
}

public ChildRecyclerView findNestedScrollingChildRecyclerView() {
if (getAdapter() instanceof INestedParentAdapter) {
return ((INestedParentAdapter) getAdapter()).getCurrentChildRecyclerView();
}
return null;
}

public boolean isScrollToBottom() {
return !canScrollVertically(1);
}

public boolean isScrollToTop() {
return !canScrollVertically(-1);
}

@Override
public void scrollToPosition(final int position) {
if (position == 0) {
// 父容器滚动到顶部,从交互上来说子容器也需要滚动到顶部
ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
if (childRecyclerView != null) {
childRecyclerView.scrollToPosition(0);
}
}

super.scrollToPosition(position);
}
}

ChildRecyclerView


子容器主要处理 fling 传递,以及滑动到顶部时,允许父容器继续拦截事件。


public class ChildRecyclerView extends RecyclerView {

private ParentRecyclerView mParentRecyclerView = null;

/**
* fling时的加速度
*/

private int mVelocity = 0;

private int mLastInterceptX;

private int mLastInterceptY;

public ChildRecyclerView(@NonNull Context context) {
this(context, null);
}

public ChildRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public ChildRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}

private void init() {
setOverScrollMode(OVER_SCROLL_NEVER);

addOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == SCROLL_STATE_IDLE) {
dispatchParentFling();
}
}
});
}

private void dispatchParentFling() {
ensureParentRecyclerView();
// 子容器滚动到顶部,如果还有剩余加速度,就交给父容器处理
if (mParentRecyclerView != null && isScrollToTop() && mVelocity != 0) {
// 尽量让速度传递更加平滑
float velocityY = NestedOverScroller.invokeCurrentVelocity(this);
if (Math.abs(velocityY) <= 2.0E-5F) {
velocityY = (float) this.mVelocity * 0.5F;
} else {
velocityY *= 0.65F;
}
mParentRecyclerView.fling(0, (int) velocityY);
mVelocity = 0;
}
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mVelocity = 0;
}

int x = (int) ev.getRawX();
int y = (int) ev.getRawY();
if (ev.getAction() != MotionEvent.ACTION_MOVE) {
mLastInterceptX = x;
mLastInterceptY = y;
}

int deltaX = x - mLastInterceptX;
int deltaY = y - mLastInterceptY;

if (isScrollToTop() && Math.abs(deltaX) <= Math.abs(deltaY) && getParent() != null) {
// 子容器滚动到顶部,继续向上滑动,此时父容器需要继续拦截事件。与父容器 onInterceptTouchEvent 对应
getParent().requestDisallowInterceptTouchEvent(false);
}
return super.dispatchTouchEvent(ev);
}

@Override
public boolean fling(int velocityX, int velocityY) {
if (!isAttachedToWindow()) return false;
boolean fling = super.fling(velocityX, velocityY);
if (!fling || velocityY >= 0) {
mVelocity = 0;
} else {
mVelocity = velocityY;
}
return fling;
}

public boolean isScrollToTop() {
return !canScrollVertically(-1);
}

public boolean isScrollToBottom() {
return !canScrollVertically(1);
}

private void ensureParentRecyclerView() {
if (mParentRecyclerView == null) {
ViewParent parentView = getParent();
while (!(parentView instanceof ParentRecyclerView)) {
parentView = parentView.getParent();
}
mParentRecyclerView = (ParentRecyclerView) parentView;
}
}
}


效果


有 Tab





无 Tab,两个 RecyclerView 嵌套





作者:LeBron_Six
来源:juejin.cn/post/7312338839695081499
收起阅读 »

树形列表翻页,后端: 搞不了搞不了~~

web
背景 记得几年前做了一个报告,报告里面加载的是用户的历年作品还有会员信息,然后按照年月倒序展示出来,其中历年作品都要将作品的封面展示出来。一开始这个报告到没啥问题,而且一个时间轴下来感觉挺好,有用户的作品、会员记录、关注以及粉丝记录很全面。直到最近忽然有一批用...
继续阅读 »

背景


记得几年前做了一个报告,报告里面加载的是用户的历年作品还有会员信息,然后按照年月倒序展示出来,其中历年作品都要将作品的封面展示出来。一开始这个报告到没啥问题,而且一个时间轴下来感觉挺好,有用户的作品、会员记录、关注以及粉丝记录很全面。直到最近忽然有一批用户说一进到这个报告页面就卡住不动了,上去一查发现不得了,都是铁杆用户,每年作品都几百个,导致几年下来,这个报告返回了几千个作品,包含上千的图片。


问题分析


上千的图片,肯定会卡,首先想到的是做图片懒加载。这个很简单,使用一个vue的全局指令就可以了。但是上线发现,没啥用,dom节点多的时候,懒加载也卡。


然后就问服务端能不能支持分页,服务端说数据太散,连表太多,树形结构很难做分页。光查询出来就已经很费劲了。


没办法于是想了一下如何前端来处理掉。


思路



  1. 由于是app中的嵌入页面,首先考虑通过滚动进行分页加载。

  2. 一次性拿了全部的数据,肯定不能直接全部渲染,我们可以只渲染一部分,比如第一个节点,或者前几个节点。

  3. 随着滚动一个节点一个节点或者一批一批的渲染到dom中。


实现


本文仅展示一种基于vue的实现


1. 容器

设计一个可以进行滚动翻页的容器 然后绑定滚动方法OnPageScrolling



<style lang="less" scoped>

.study-backup {

overflow-x: hidden;

overflow-y: auto;

-webkit-overflow-scrolling: touch;

width: 100%;

height: 100%;

position: relative;

min-height: 100vh;

background: #f5f8fb;

box-sizing: border-box;

}

</style>

<template>

<section class="report" @scroll="OnPageScrolling($event)">

</section>

</template>



2.初始化数据

这里定义一下树形列表的数据结构,实现初始化渲染,可以渲染一个树节点,或者一个树节点的部分子节点



GetTreeData() {

treeapi

.GetTreeData({ ... })

.then((result) => {

// 处理结果

const data = Handle(result)

// 这里备份一份数据 不参与展示

this.backTreeList = data.map((item) => {

return {

id: item.id,

children: item.children

}

})

// 这里可以初始化为第一个树节点

const nextTree = this.backTreeList[0]

const nextTansformTree = nextTree.children.splice(0)

this.treeList = [{

id: nextTree.id,

children: nextTansformTree

}]

// 这里可以初始化为第一树节点 但是只渲染第一个子节点

const nextTree = this.backTreeList[0]

const nextTansformTree = nextTree.children.splice(0, 1)

this.treeList = [{

id: nextTree.id,

children: nextTansformTree

}]

})

},


3.滚动加载

这里通过不断的把 backTreeList 的子节点转存入 treeList来实现分页加载。



OnPageScrolling(event) {

const container = event.target

const scrollTop = container.scrollTop

const scrollHeight = container.scrollHeight

const clientHeight = container.clientHeight

// console.log(scrollTop, clientHeight, scrollHeight)

// 判断是否接近底部

if (scrollTop + clientHeight >= scrollHeight - 10) {

// 执行滚动到底部的操作

const currentReport = this.backTreeList[this.treeList.length - 1]

// 检测匹配的当前树节点 treeList的长度作为游标定位

if (currentReport) {

// 判断当前节点的子节点是否还存在 如果存在则转移到渲染树中

if (currentReport.children.length > 0) {

const transformMonth = currentReport.children.splice(0, 1)

this.treeList[this.treeList.length - 1].children.push(

transformMonth[0]

)

// 如果不存在 则寻找下一树节点进行复制 同时复制下一节点的第一个子节点 当然如果寻找不到下一树节点则终止翻页

} else if (this.treeList.length < this.backTreeList.length) {

const nextTree = this.backTreeList[this.treeList.length]

const nextTansformTree = nextTree.children.splice(0, 1)

this.treeList.push({

id: nextTree.id,

children: nextTansformTree

})

}

}

}

}


4. 逻辑细节

从上面代码可以看到,翻页的操作是树copy的操作,将备份树的子节点转移到渲染树中



  1. copy备份树的第一个节点到渲染树,同时将备份树的第一个节点的子节点的第一个节点转移到渲染树的第一个节点的子节点中

  2. 所谓转移操作,就是数组splice操作,从一颗树中删除,然后把删除的内容插入到另一颗树中

  3. 由于渲染树是从长度1开始的,所以我们可以根据渲染树的长度作为游标和备份树进行匹配,设渲染树的长度为当前游标

  4. 根据当前游标查询备份树,如果备份树的当前游标节点的子节点不为空,则进行转移

  5. 如果备份树的当前游标节点的子节点为空,则查找备份树的当前游标节点的下一节点,设为下一树节点

  6. 如果找到了备份树的当前游标节点的下一节点,扩展渲染树,将下一树节点复制到渲染树,同时将下一树节点的子节点的第一节点复制到渲染树

  7. 循环4-6,将备份树完全转移到渲染树,完成所有翻页


扩展思路


这个方法可以进行封装,将每次复制的节点数目和每次复制的子节点数目作为传参,一次可复制多个节点,这里就不做展开


作者:CodePlayer
来源:juejin.cn/post/7270503053358612520
收起阅读 »

当接口要加入新方法时,我后悔没有早点学设计模式了

假设系统中有一个接口,这个接口已经被10个实现类实现了,突然有一天,新的需求来了,其中5个实现类需要实现同一个方法。然后你就在接口中添加了这个方法的定义,想着一切都很完美。 当你在接口和其中5个实现类中加完这个方法后,一编译。不妙啊,另外那 5 个实现类报错了...
继续阅读 »

假设系统中有一个接口,这个接口已经被10个实现类实现了,突然有一天,新的需求来了,其中5个实现类需要实现同一个方法。然后你就在接口中添加了这个方法的定义,想着一切都很完美。


当你在接口和其中5个实现类中加完这个方法后,一编译。不妙啊,另外那 5 个实现类报错了,没实现新加的这个方法。要知道,接口中的方法定义必须要在实现类中实现才行,缺一个都编译不过。


这时候你耳边突然响起了开发之初的老前辈跟你说的话:“这几个实现以后可能差距越来越大,接口中可能会加方法,注意留口子”。



现在咋整


假设之前的接口是这样的,只有吃饭和喝水两个方法。


public interface IUser {

/**
* 吃饭啊
*/

void eat();

/**
* 喝水啊
*/

void drink();
}

现在有 5 个实现类厉害了,要加一个 play() 方法。


既然情况已经这样了,现在应该怎么处理。


破罐子破摔吧,走你


不管什么接口不接口的了,哪个实现类要加,就直接在那个实现类里加吧,接口还保持之前的样子不动,仍然只有吃饭和喝水两个方法,play 方法就直接加到 5 个实现类中。


public class UserOne implements IUser{

@Override
public void eat() {
System.out.println("吃饭");
}

@Override
public void drink() {
System.out.println("喝水");
}

public void play() {
System.out.println("玩儿");
}
}

虽然可以实现,但是完全背离了当初设计接口的初衷,本来是照着五星级酒店盖的,结果盖了一层之后,上面的变茅草屋了。


从此以后,接口是接口,实现类是实现类,基本上也就没什么关系了。灵活性倒是出来了,以后想在哪个实现类加方法就直接加了。



再加一个接口行不


还是有点儿追求吧,我新加一个接口行不行。之前的接口不动,新建一个接口,这个接口除了包含之前的两个方法外,再把 play 方法加进去。


这样一来,把需要实现 play 方法的单独在弄一个接口出来。就像下面这样 IUser是之前的接口。IUserExtend接口是新加的,加入了 play() 方法,需要实现 play() 方法的实现类改成实现新的IUserExtend接口,只改几个实现关系,改动不是很大嘛,心满意足了。



但是好景不长啊,过了几天,又要加新方法了,假设是上图的 UserOneUserNine要增加方法,怎么办呢?



假如上天再给我一次机会


假如上天再给我一次重来的机会,我会对自己说:“别瞎搞,看看设计模式吧”。


适配器模式


适配器模式可以通过创建一个适配器类,该适配器类实现接口并提供默认实现,然后已有的实现类可以继承适配器类而不是直接实现接口。这样,已有的实现类不需要修改,而只需要在需要覆盖新方法的实现类中实现新方法。



不是要加个 play() 方法吗,没问题,直接在接口里加上。


public interface IUser {
void eat();
void drink();
void play();
}

适配器类很重要,它是一个中间适配层,是一个抽象类。之前不是实现类直接 implements 接口类吗,而现在适配器类 implements 接口类,而实现类 extends 适配器类。


在适配器类可以给每个方法一个默认实现,当然也可以什么都不干。


public abstract class UserAdapter implements IUser {
@Override
public void eat() {
// 默认实现
}

@Override
public void drink() {
// 默认实现
}

@Override
public void play() {
// 默认实现
}
}

public class UserNine extends UserAdapter {
@Override
public void eat() {
System.out.println("吃饭");
}

@Override
public void drink() {
System.out.println("喝水");
}

@Override
public void play() {
System.out.println("玩儿");
}
}

public class UserTen extends UserAdapter {
@Override
public void eat() {
System.out.println("吃饭");
}

@Override
public void drink() {
System.out.println("喝水");
}
}

调用方式:


IUser userNine = new UserNine();
userNine.eat();
userNine.drink();
userNine.play();

IUser userTen = new UserTen();
userTen.eat();
userTen.drink();

这样一来,接口中随意加方法,然后在在适配器类中添加对应方法的默认实现,最后在需要实现新方法的实现类中加入对应的个性化实现就好了。


策略模式


策略模式允许根据不同的策略来执行不同的行为。在这种情况下,可以将新方法定义为策略接口,然后为每个需要实现新方法的实现类提供不同的策略。


把接口改成抽象类,这里面 eat() 和 drink() 方法不变,可以什么都不做,实现类里想怎么自定义都可以。


而 play() 这个方法是后来加入的,所以我们重点关注 play() 方法,策略模式里的策略就用在 play() 方法上。


public abstract class AbstractUser {

IPlayStrategy playStrategy;

public void setPlayStrategy(IPlayStrategy playStrategy){
this.playStrategy = playStrategy;
}

public void play(){
playStrategy.play();
}

public void eat() {
// 默认实现
}

public void drink() {
// 默认实现
}
}

IPlayStrategy是策略接口,策略模式是针对行为的模式,玩儿是一种行为,当然了,你可以把之后要添加的方法都当做行为来处理。


我们定一个「玩儿」这个行为的策略接口,之后不管你玩儿什么,怎么玩儿,都可以实现这个 IPlayStrategy接口。


public interface IPlayStrategy {

void play();
}

然后现在做两个实现类,实现两种玩儿法。


第一个玩儿游戏的实现


public class PlayGameStrategy implements IPlayStrategy{

@Override
public void play() {
System.out.println("玩游戏");
}
}

第二个玩儿足球的实现


public class PlayFootballStrategy implements IPlayStrategy{
@Override
public void play() {
System.out.println("玩儿足球");
}
}

然后定义 AbstractUser的子类


public class UserOne extends AbstractUser{
@Override
public void eat() {
//自定义实现
}

@Override
public void drink() {
//自定义实现
}
}

调用方式:


public static void main(String[] args) {
AbstractUser userOne = new UserOne();
// 玩儿游戏
userOne.setPlayStrategy(new PlayGameStrategy());
userOne.play();
// 玩儿足球
userOne.setPlayStrategy(new PlayFootballStrategy());
userOne.play();
}

整体的类关系图大概是这个样子:



最后


通过适配器模式和策略模式,我们即可以保证具体的实现类实现共同的接口或继承共同的基类,同时,又能在新增功能(方法)的时候,尽可能的保证设计的清晰。不像之前那种破罐子破摔的方式,接口和实现类几乎脱离了关系,每个实现类,各玩儿各的。


作者:古时的风筝
来源:juejin.cn/post/7313418992310976549
收起阅读 »

公司敏感数据被上传Github,吓得我赶紧改提交记录

大家好,我是小富~ 说个事吧!最近公司发生了一个事故,有同事不小心把敏感数据上传到了GitHub上,结果被安全部门扫描出来了。这件事导致公司对所有员工进行了一次数据安全的培训。对于这个事我相信,有点工作经验的人都不会故意去上传这些敏感文件,多数应该是误操作导致...
继续阅读 »

大家好,我是小富~


说个事吧!最近公司发生了一个事故,有同事不小心把敏感数据上传到了GitHub上,结果被安全部门扫描出来了。这件事导致公司对所有员工进行了一次数据安全的培训。对于这个事我相信,有点工作经验的人都不会故意去上传这些敏感文件,多数应该是误操作导致的。


这个事件也给了提了个醒,我平时会写博客用GitHub比较多,吓得我赶紧对自己所有的GitHub仓库进行了排查,庆幸没有提交过敏感信息的记录。但我注意到在过往的提交记录中,有使用公司的Git账号信息提交过代码,TMD这就很难受了。


图中信息均为假数据,切勿当真


避免后续产生不必要的麻烦,我决定修改一下提交记录中涉及公司的信息。



注意:以下操作只限于用在自己的Git仓库,别在公司的项目里秀,切记!



设置用户信息


Git进行版本控制的时候,每次的代码提交记录中都包含用户的用户名和邮箱,这些信息在你进行每一次提交时都会被记录下来。我们保不齐会错误地使用了错误的信息,或者需要改用另一个邮箱地址。那这种情况,我们就需要更改我们提交记录中的用户名和邮箱。


可以通过全局设置或者特定仓库设置两种方式来修改我们提交时的用户信息。


全局


全局设置可以影响所有的代码提交。如果你在全局范围内设置了用户名和邮箱后,除非你在特定的项目中覆盖这个设置,否则这个设置会作为默认设置应用于所有的提交。


git config --global user.name "程序员小富"
git config --global user.email "邮箱信息"

你可以通过如下的命令来查看Git的全局配置:


git config --global -l

特定仓库


如果你只想修改某个特定仓库的用户信息,可以在特定仓库的根目录下进行如下操作,Git会将设置得用户名和邮箱仅应用于当前仓库。


git config user.name "程序员小富"
git config user.email "邮箱信息"

篡改提交记录


单条修改


Git提供了amend命令,可以用来修改最新的提交记录。注意,这个命令只会修改最近一次的提交,它能实现以下的功能:



  • 修改提交信息

  • 添加漏掉的文件到上一次的提交中

  • 修改之前提交的文件


用法


它的使用方法比较简单,直接替换用户名、邮箱信息,或者如果已经修改了仓库的用户信息,直接执行命令重置。


# 替换用户名、邮箱信息
git commit --amend --author="{username} <{email}>" --no-edit

#
如果已经修改了仓库的用户信息,直接执行命令重置
git commit --amend --reset-author --no-edit

看到最近一次提交的用户名是xiaofu,不是我的个人信息程序员小富,使用amend命令修改一下。



效果


执行命令后最近一次的提交信息从xiaofu变更到了程序员小富,更改成功和预期的效果一致。


git commit --amend --author="程序员小富 <515361725@qq.com>" --no-edit


修改完成之后,别忘了推送到远程仓库。


 git push origin master

批量修改


Git官网提供了很多种修改提交记录信息的方法,这里主要介绍下filter-branch,它可以通过脚本的方式批量修改历史提交记录信息。


filter-branch 它能实现如下的功能,正好符合我们要批量修改历史提交记录中用户、邮箱的需求。



  • 全局修改邮箱地址;

  • 从每一个提交中移除一个文件;

  • 使一个子目录做为新的根目录


用法


历史提交记录中有很多用户名xiaofu提交的记录,现在使用filter-branch批量将他们改写成程序员小富



以下是官网提供的脚本,其逻辑很简单:如果遇到用户名为xiaofu的提交记录,将该提交记录的用户名和邮箱修改为程序员小富515361725@qq.com


git filter-branch --commit-filter '
if [ "$GIT_AUTHOR_NAME" = "xiaofu" ];
then
GIT_AUTHOR_NAME="程序员小富";
GIT_AUTHOR_EMAIL="515361725@qq.com";
git commit-tree "$@";
else
git commit-tree "$@";
fi'
HEAD

为了方便操作,创建一个脚本modifyCommit.sh,放在项目的根目录执行。


chmod +x modifyCommit.sh
sh modifyCommit.sh

执行脚本后稍作等待,出现如下的输出说明已经在执行修改操作了。



执行完毕看到历史提交记录中的用户名xiaofu全都变更成了程序员小富,说明脚本生效了。



如果没有修改成功,可以再次执行,但会出现错误提示A previous backup already exists in refs/original/,说明已经执行过了,执行以下命令清除缓存即可再次执行。



git filter-branch -f --index-filter 'git rm --cached --ignore-unmatch Rakefile' HEAD

修改完成之后,别忘了推送到远程仓库。


 git push origin master

GitHub工具


管理GitHub项目,我推荐大家使用GitHub官方的Git客户端工具GitHub Desktop,这个工具专门用来管理GitHub仓库,洁面简洁使用也很方便,主打一个轻量。



而且在提交代码时,如果用户信息与当前账号GitHub信息不一致,还会有提示这样就不怕误用其他信息提交了。



总结


如果大家平时会维护自己的GitHub仓库,建议一定一定要仔细的检查提交的代码,像注释里的公司邮箱信息、代码包路径中的公司标识,凡事涉及公司信息的数据一概去除,不要惹一些不必要的麻烦,数据泄漏这种重可大可小不是闹着玩的。


还有GitHub别留太多的个人信息,手机号邮箱就别放了,头像也别傻乎乎的放个自己大头贴,给自己留点回旋的余地。核心思工作和生活要隔离!!!


我是小富~ 下期见


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

这样做产品,死是早晚的事!

昨天和在北京的朋友聊天,他了解到之前我做过餐饮的SAAS系统,于是问我这一块是否还能分到一杯羹! 说实话,我觉得没机会,特别是对于一家小公司来说,基本上没机会,甚至连入场券都拿不到! 这不禁让我想起几年前认识的一个小公司,给他们兼职开发的两款SAAS产品,一款...
继续阅读 »

image.png
昨天和在北京的朋友聊天,他了解到之前我做过餐饮的SAAS系统,于是问我这一块是否还能分到一杯羹!


说实话,我觉得没机会,特别是对于一家小公司来说,基本上没机会,甚至连入场券都拿不到!


这不禁让我想起几年前认识的一个小公司,给他们兼职开发的两款SAAS产品,一款是连锁酒店系统,一款则是餐饮系统。


他们的酒店系统,现在在我看来依然是很牛逼的,我也去看过一些市面上的解决方案,但是依然没有他们的牛逼。


不过残酷的是,最近半年来,他们好像一套也没有卖出去,如果我没猜错的话,这几年下来,他们应该没有卖出多少套。


其实几年前我和他们协同开发,听了他们的一些想法,我就预见他们很难打出去。


因为我发现他去做了一些看似很完美,但是不是必须的功能,而且还花了大量时间去做,当时我觉得这完全就是鸡肋,现在看来是鸡骨头。


说白了,就是定位不明确,想做一个大而全方案,但是这对于一个小公司初创团队来说,这是很致命的,特别是资金不充足的情况下去干这事!


下面从几个方面去看问题。


1.定位不明确


理想一定是会被现实啪啪打脸的,当想去做一个产品的时候,不要觉得自己做得很全很大就能赢得市场,这简直是痴人说梦。


特别是在行业竞争如此之大的情况下,大公司早都入局了,人家的解决方案比你强大,价格比你便宜,售后比你全,你拿什么去拼?


当时我问他,为啥要做餐饮解决方案,你觉得你从技术,价格,服务这些方面,你有哪里比得上客如云,微盟,美团这些巨头,他说别管那么多,东西做出来自然有办法!


现在里面过去了,基本上没有任何推进。


这肯定是定位出问题了啊,不要觉得你手上有产品就能赚钱,如果是这样,那还需要销售干嘛。


对于小公司来说,大家都是技术出身,没有营销经验,就算做出产品来,也只能摆着看,如果要请销售团队,公司又支撑不起,显然矛盾了!


所以就尽量别去做这类似的产品,应该去做一些能解决别人痛点的小而美的解决方案。


就像微信公众号刚兴起的那几年,因为公众号自带的编辑器很难用,有一个人就做了一个小编辑器出来,赚得盆满钵满。


看似冷门,但是垂直!


2.陷入大而全的误区


接着上面的说。


后面有人看到看到了这个红利,就进军去做,他们希望做出更强大,功能更全的编辑器,结果花了大量时间去做,最后产品出来了,但是市场已经被别人抢了先机,最终不得不死。


这就是迷恋大而全的后果!


其实开源就是一个很好避免大而全的方案。


在开源领域,先做出一个小而美的产品,把影响力传播开,然后根据用户的需求不断迭代,这时候不是人去驱动产品了,而是需求去驱动产品。


这样做出来的产品不仅能避免出现很多无用的功能,还能节约很多的成本!


一定要让用户的需求来驱动产品的发展,而不是靠自己的臆想去决定做什么产品!


老罗当年在做锤子科技的时候,我觉得他就陷入了想去做一个大而全的产品,还陷入自己以为的漩涡,所以耗费了很多资金去研发TNT,所以导致失败。


如果那时候致力于去做好坚果系列,那么结局可能大不一样!


3.没有尝到甜头,你怎敢去做!


在我们贵州本土,有一个技术大佬,他一开始做了一个门户系统的解决方案,后续就有人来找他,说要购买他的系统,他从里面尝到了甜头!


于是就在这个领域持续深耕,最终形成了一套强大的解决方案。现在他的解决方案已经遍布全国。


他们公司基本上就是靠门户系统的解决方案来维持的。


所以,做一个产品,只有自己尝到甜头了,再去深耕,形成一套解决方案,那么成功率就会变得越高。


特别对于小公司来说,这是很重要的!


4.总结


做产品一定要忌讳大而全,也不要陷入只要我做出来了,无论如何都能分一杯羹,这是不现实的。


市场上到处是饿狼潜伏,你不过是一只小羊羔,怎么生存?


用最少的成本开发出一个小而美的解决方案,然后拿出去碰一碰,闻到味道了,再不断进击,这样成功率就高一点,即使失败了代价也不高。


今天的分享就到这里!


作者:追梦人刘牌
来源:juejin.cn/post/7313887095415324672
收起阅读 »

听说蚂蚁的职级调整了

上周三听说蚂蚁的职级调整了,让我们来看一下具体的改革方案: 简单地说,就是把原来的 PN 级一拆二,拆成 2N 和 2N+1 级。 从本质上来看,就是把原来扁平化的宽职级变多了,相当于 double 了。 那职级变多有什么好处呢? 第一点是职级更有区分度了。...
继续阅读 »

上周三听说蚂蚁的职级调整了,让我们来看一下具体的改革方案:


插图1.png


简单地说,就是把原来的 PN 级一拆二,拆成 2N 和 2N+1 级。


从本质上来看,就是把原来扁平化的宽职级变多了,相当于 double 了。


那职级变多有什么好处呢?


第一点是职级更有区分度了。因为职级变多了。扁平化的职级会造成一个问题是某个职级的人会停留在这个职级很久,比如有的 P6 停留了 2 年,有的 P6 可能停留了 4 年,在现有的职级体系就比较难区分了。


第二点是职级晋升难度降低了。因为职级少,晋升难度自然就会高,每一次晋升都是一个坎。职级变多了,就变得不稀有了,那晋升难度自然就下降了,晋升的频次也会变高了。


相信有不少小伙伴应该都体验过答辩失败的经历,甚至更悲催的是,有人连续晋升好几次都失败了。如此高难度的晋升会造成以下问题:



  • 打击积极性。晋升这么难,那就开始躺平吧,反正努力也没用。

  • 劝退人才。晋升太黑暗了,此处不留爷自有留爷处。

  • 扩大内耗。每次准备答辩都要好几个月,心太累了。为了答辩,必须卷项目造轮子,太没意义了。


现在职级变多了,晋升难度就可以降低了,晋升成功还有调薪,这大家不就有盼头了吗?


所以,职级体系改革的本质目的就是为了激励大家,让大家卷起来,拿到好绩效,就可以晋升了。


这让我回想起之前腾讯的职级改革,先来看下:


插图2.png


腾讯的职级改革比较早,可以看到,这个职级调整并没有增多职级的数量,好像就单纯换了个名字,这不是改革了个寂寞?


其实不然,这里最大的变化就是取消了大职级的概念,统一职级体系,本质也是把那个坎干掉了。


之前在腾讯,T 族一般来说,会有两个坎:



  • 2-3 晋升到 3-1,从 2 升到 3,是个非常大的坎,因为 3 级是高级工程师。2-3 之前的晋升,全部都是部门内部说了算,基本是到了年限就能升,而且很优秀的同学还能跳级。但从晋升 3-1 开始就“突然”变难了,因为要去通道答辩了,所以这是一个非常大的坎。有很多人,都是卡在 3-1 这个坎的,曾经看到有人连续晋升失败了 8 次的,他的坚持我也是很佩服。

  • 3-3 晋升到 4-1 是另外一个坎,因为 4 级是专家工程师。对于大部分程序员来说,基本 3-3 就是天花板了,晋升 4-1 就需要业务的支持,但是哪有那么多的好业务呢?所以基本大家到了 3-3 就可以开始选择躺平,或者跳槽了。


因此,腾讯抹平了大职级的差异之后,这两个坎就没了。具体的表现是:



  • 晋升 3-1(T9) 的坎没了。在 T12 之前的所有职级晋升,都下放到部门,不需要通道答辩了。听说现在是这样,反正我走的时候,是晋升 T9 下放部门了,但晋升 T10 还需要答辩。

  • 晋升 T12 的难度降低了。我走之前,后台的几个大佬全部都陆续晋升 T12 了。


所以,以后大家就卷绩效就行了,不需要再卷项目卷答辩卷 PPT 了,绩效好了,就什么都有啦!


作者:潜龙在渊灬
来源:juejin.cn/post/7313979404993069094
收起阅读 »

作为曾经新东方的技术人,说说我跟孙东旭的两三事

我是2019年年底入职的新东方在线,当时也是孙东旭(Jack)任命为新东方在线CEO不到一年的样子。 那时正是K12在线教育打得如火如荼之际,好未来、猿辅导、作业帮群雄逐鹿,新东方也是不甘示弱地布局线上,招聘了不少产研的人才。 Jack大部分精力都放在业务团队...
继续阅读 »

我是2019年年底入职的新东方在线,当时也是孙东旭(Jack)任命为新东方在线CEO不到一年的样子。


那时正是K12在线教育打得如火如荼之际,好未来、猿辅导、作业帮群雄逐鹿,新东方也是不甘示弱地布局线上,招聘了不少产研的人才。


Jack大部分精力都放在业务团队上,与产研团队的接触不算频繁,但也还是有些接触的。


下面回忆一下,我跟Jack的几次接触,给大家带来一些不同的视角进行解读。


初次饭局


那次产研的同事正在新东方南楼开会,会差不多开完的时候,Jack从门外走了进来,衣着朴素,面带微笑,微笑中透着善意,以至于当时我根本不知道,原来他就是公司的大老板。


随后Jack开始讲话,是给在座的同事进行鼓劲儿,大意就是在线教育迎来是史上大爆发的时代,作为老牌巨头的新东方一定会牛逼,一定会在这个时代的大潮中有所斩获。


不得不说,真的是人不可貌相。Jack的讲话风格,完全延续了新东方老师的教学风格,金句频出,出口成章,完全与所谓的“官僚、官腔”不沾边儿。


接下来我们每个人做自我介绍,介绍完后Jack也是自来熟地跟我们每个人都一一聊两句。到了我的时候,调侃了两句我的英文名,戏称“Tony”老师,哈哈哈。


会议的尽头,当然少不了的就是饭局。如果没记错的话,当时应该去的是离公司很近的颐和雅苑,Jack带来了很多瓶的茅台。


听同事说,Jack酒量不大,但是非常喜欢喝酒。


转正述职


公司的试用期是半年,因此在职五个多月的时候,就要准备转正述职资料。当时我老板跟我明确说了,在我述职的时候,Jack肯定会参加。


我那时听到这个消息,真的愁坏了,因为给非技术出身的CEO进行述职,讲述我在这半年中的工作成果,是个不小的挑战。


现在我还记得,那份转正述职的PPT我整整写了半个月。甚至有的关键页,真的是一杯茶、一根烟,一张PPT写一天。


述职当天,我的前面还有一个同事,在他进行述职的时候,我又一遍又一遍地过我PPT中的内容。


终于到我了,记得刚进会议室的门,Jack就热情地招呼我坐下,告诉我别紧张。后来整个述职过程中,Jack很认真地听着,一直从开始听到最后,能看出来的是Jack还算满意。


现在还记得当时Jack说的几句话,大概意思就是好好干,主流的互联网大厂对贡献卓著的员工,有非常完善的晋升机制,这个新东方也会有。


总之,整体的转正述职,我是诚惶诚恐、如履薄冰地进去,如释重负、如沐春风地出来了。


还是饭局


这次饭局的阵容更强大,就连俞老师也来参加了,当时是为了给一个刚刚加入的学科副总裁接风。


记得那次去的是一家人民大学附近的,俞老师非常喜欢的江苏菜馆,吃了各种各样的鱼。


饭局中,俞老师很健谈,说了很多话,大家依次跟俞老师进行合影。哈哈哈,我相信应该有很多人都把合影转发朋友圈了。


后来俞老师有事,饭局还没进行完就离开了。


随后Jack开始活跃气氛,唱了一首许巍的歌——《那一年》。哦,对了,他说记不住歌词,所以对着手机中的歌词唱的。


我也是许巍的歌迷,这首歌并不算许巍的热门歌曲,但当时听Jack唱起来,感觉还挺好听的。


最后,Jack说相信这位学科副总裁到来后,一定会把公司的股价推向新高,相信一年后股价能达到70——100。


当然,后来新东方在线没有达到,但是东方甄选达到了。


东方甄选


再后来就是2021年夏天,那次史无前例的双减了,产研团队走了将近90%的人,我也离开了。


记得临走的时候,我给Jack微信留言告别,Jack回复是:“谢谢,感谢你给公司做出的贡献,一起经历一切都还历历在目,希望你未来越来越好。”


后来从前同事那里得知,俞老师把公司转型的方向定成了直播带货。


我之前从来没看过直播带货,有次是处于好奇前公司现状的原因,去东方甄选的直播间看了看。


当时应该是2021年的年底,董宇辉还没火,当时正好是Jack在直播间带着别的主播进行直播。


还是那种熟悉的语气,熟悉的讲话风格,金句频出、挥洒自如。只不过讲话场合,从以前的会议室变成了直播间,K12的直播课程变成了大米鸡蛋。


结语


有感而发,随便写写,也算是纪念一下曾经在新东方的两年吧。


作者:托尼学长
来源:juejin.cn/post/7313839469047971876
收起阅读 »

2023再见,2024你好😉

时间如白驹过隙,一眨眼,2023已经到了最后一站,这一年的旅途中,有欢笑,有遗憾,有收获,有失去,此时此刻,即将执笔记录下过去一年的风景,邀君共赏~ 欢笑 熟悉我的朋友一定知道,我之前的工作经历其实是非常曲折的,经历过三家欠薪的公司,要回了两家,还有一家已经...
继续阅读 »

时间如白驹过隙,一眨眼,2023已经到了最后一站,这一年的旅途中,有欢笑,有遗憾,有收获,有失去,此时此刻,即将执笔记录下过去一年的风景,邀君共赏~


image.png


欢笑


熟悉我的朋友一定知道,我之前的工作经历其实是非常曲折的,经历过三家欠薪的公司,要回了两家,还有一家已经放弃了,因此自己也经历过非常多的换工作经历,为了应付面试,自己也要花费大量的时间和精力去准备,整个的过程其实是非常辛苦和苦涩的


image.png


在去年年底的时候跳槽到了现在这家公司,领导非常nice,业务也非常有前景,最最关键的是没有欠薪了!让我能踏踏实实,开开心心地工作一整年,我觉得这就是2023年带给我最快乐的事情!也希望各位朋友不要遇到欠薪的公司,都能有个顺顺利利的职业生涯!


遗憾


时常听到一句话:“人生里遗憾才是常态,成功不过是刹那的欢愉”,少时的我不懂其中含义,随着年岁的渐长,慢慢领悟了其中的含义,我的理解就是 要学会接受自己的普通


其实今年我在业余时间做了如下几件事情:



  • 开通并维护自己的公众号 (前端千里码)

  • 买了树莓派

  • 参加了软考


其实一开始开通公众号,还幻想过自己能有几千上万的粉丝,能打造一个具有影响力的公众号,但经过自己一年的“打造”,粉丝数达到了惊人的11人(还包括我老婆),自己也放下了念想,现在就只把它当做一个掘金之外发表文章的地方,彻底佛系了


至于买树莓派的原因,自己一开始也是想着能打造一个智能家居系统,但自己由于没有嵌入式相关的基础,自己也没有恒心与毅力去学习,至此已经彻底成了一个 “吃灰派”了


软考今年我参加的是 软件设计师,一开始我是觉得自己能很轻松过的,毕竟自己就是干这行的,于是跟以往备考一样,提前一个月开始,断断续续看视频,然后做了几套真题的选择题,之后就充满信心地去考试了,但真正在考场上的时候,我才发现很多题都很陌生,当时就预感不太好了,结果上周出的成绩,上午47,下午36,就这样以失败告终


image.png


其实今年这些遗憾的事情也并没有打击到我,因为我知道失败是必然的,成功才是偶然,况且自己也只是一个普通人,跌倒了再爬起来,拍拍灰,继续前行才是最重要的,因为 既然选择了远方,便只顾风雨兼程


结语


很喜欢一句话:我已不再年轻,岁月给了我智慧,2023里,自己也经历了很多,收获了很多,当然也充满遗憾,但我想这就是人生吧,人生百味不过如此,愿我们所有人在2024年里能蒸蒸日上,让欢乐的事情多些,再多些吧!


作者:Mr_Carl
来源:juejin.cn/post/7311623836013641782
收起阅读 »

被遗忘的 PHP 更新到 V8.3 了

还记得当年喊过的口号吗?PHP 宇宙第一! PHP 8.3 在前几天就已经被发布了,提供了一些新功能,包括 Override 属性、JSON 验证和类型化类常量。 最新版本的主要功能包括: #[\Override] 属性:验证用该属性标记的方法是否确实有被重...
继续阅读 »


还记得当年喊过的口号吗?PHP 宇宙第一!


PHP 8.3 在前几天就已经被发布了,提供了一些新功能,包括 Override 属性、JSON 验证和类型化类常量。


最新版本的主要功能包括:



  • #[\Override] 属性:验证用该属性标记的方法是否确实有被重写的父方法。在删除父方法的情况下,它可以防止键入错误或重构;这种情况目前会引发致命错误。

  • 类型化的类常量:在类、接口或特性中声明的常量现在可以被类型化。这个特性的 RFC 解析说为改进 PHP 的类型系统真的投入了无数光阴和汗水。PHP 和 JavaScript 一样,是一种动态语言;但对强类型的需求也在不断增加,并逐渐增加了对它的一些支持。

  • Json_validate()函数:它检查字符串是否是有效的 Json (JavaScript对象表示法),并且比 Json_decode()更有效。

  • 只读属性的深度克隆:这项功能是解决 PHP 支持只读属性的“严重缺陷”的提议的一部分。非只读类可以扩展只读类的提议没有得到批准,但在克隆过程中重新初始化只读属性的能力得到了批准,解决了该提议所说的“阻碍任何非基本用例的主要不便”。在__clone()方法执行期间,重新初始化只可能一次。


工具开发商 JetBrains 已经快速推出 PhpStorm IDE 新版本并支持了 PHP8.3。


PHP 的新版本大约每 12 个月发布一次,之前的版本是 2022 年 12 月的 8.2。每个版本都有两年的活跃支持和一年的安全支持,因此 2020 年 11 月 26 日发布的 8.0 即将失去安全支持,尽管操作系统供应商可能有自己的支持策略,其中包括 PHP。


长期不受支持的 PHP 版本通常会继续被老项目使用,今年早些时候的一份报告称,大多数部署都处于这种状态。



W3Techs 报告称,PHP 是迄今为止部署最广泛的服务器端应用程序运行时,尽管使用它的开发人员数量最近几年一直在下降。


从年度 StackOverflow 报告等调查来看,开发人员的 PHP 使用率似乎在下降。2023 年,数据显示有 18.58% 的开发人员反映他们用 PHP 编写代码,而 2022 年和 2020 年分别为 20.87% 和 26.2%。


即使 PHP 已经过了高光时刻,它仍然很受欢迎,并且是 WordPress 使用的默认语言,根据 W3Techs 的数据,WordPress 在 43% 的网站上运行。更令人惊讶的是,我们知道的网站中,服务器端编程语言 PHP 占了76.7%。


作者:ENG八戒
来源:juejin.cn/post/7312818409472655386
收起阅读 »

你不会还在useEffect中请求数据吧

web
使用React Query代替useEffect获取数据的优势与对比 在构建现代React应用时,我们经常需要从后端API获取数据来渲染界面。传统的方式是使用React的useEffect钩子结合fetch或axios等HTTP请求库来完成数据获取和状态管理。...
继续阅读 »

使用React Query代替useEffect获取数据的优势与对比


在构建现代React应用时,我们经常需要从后端API获取数据来渲染界面。传统的方式是使用React的useEffect钩子结合fetchaxios等HTTP请求库来完成数据获取和状态管理。然而,随着React Query的出现,获取和同步服务器状态的方式得到了显著的改进。本文将详细介绍使用React Query代替useEffect获取数据的原因,并通过示例对比两种方式在代码层面的不同,在最后总结React Query的优势。


传统方式:使用useEffect获取数据


在没有使用React Query之前,我们通常会这样获取数据:


import React, { useState, useEffect } from 'react';

function MyComponent() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch('https://my-api/data');
const result = await response.json();
setData(result);
} catch (error) {
setError(error);
}
setIsLoading(false);
};

fetchData();
}, []);

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{JSON.stringify(data)}</div>;
}

这段代码虽然能工作,但存在几个问题:缺乏缓存策略、复杂的错误处理、不自动的数据更新、重复的数据请求等。


使用React Query改进数据获取


接下来,看看React Query如何为我们解决上述问题和简化代码:


import React from 'react';
import { useQuery } from 'react-query';

async function fetchData() {
const response = await fetch('https://my-api/data');
return response.json();
}

function MyComponent() {
const { data, isLoading, error } = useQuery('data', fetchData);

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{JSON.stringify(data)}</div>;
}

在这个改进后的版本中,我们用useQuery钩子来代理数据加载。这行代码做了很多工作:它自动进行数据请求,处理加载状态和错误状态,还负责缓存和更新数据。


使用React Query的原因


简化的状态管理


React Query内部处理了数据的加载(isLoading)、数据更新(isFetching)、错误(error)状态管理,这使得开发者无需手动设置这些状态。


自动化的数据缓存和无效化


React Query还提供了出色的数据缓存策略。默认情况下,当组件卸载再重新挂载时,React Query会使用旧的缓存数据,同时在后台静默地为你刷新数据,保证数据的新鲜度。


更好的错误和重试处理


通过对错误状态的内部管理,React Query提供了错误捕获的机制并允许自动重试功能。这比手动实现要简单得多。


优化请求节省带宽


React Query会自动去重和合并并发的查询请求,减少不必要的网络请求,节省宽带。


React Query的优势


总结来说,React Query的主要优势包括:



  • 自动化:管理请求生命周期(查询、缓存、更新、重试)无需手动编写代码。

  • 减少样板代码:少写很多状态处理的逻辑,让代码简洁易维护。

  • 性能提升:智能缓存和数据更新策略,更少的重新渲染。

  • 鲁棒性:更健壮的错误处理和重试逻辑。

  • 开箱即用:丰富的功能如后台获取、分页、无限加载等。


在创建现代化的React应用程序时,React Query提供了一种更智能、更高效和简单的方法来处理数据获取和同步,这也是越来越多的React开发者选择它的原因。


React Query


下面将详细介绍React Query的功能,以及它如何在一个实际的场景中被使用。我们将构建一个用户列表的应用,这个应用将展示用户数据、支持数据刷新、加载更多用户以及处理错误重试。


项目准备


首先,确保已经在React项目中安装了React Query:


npm install react-query

或者


yarn add react-query

功能概览



  • 数据获取 (useQuery): 用于获取数据并提供状态管理,比如loading, error, data。

  • 缓存与背景更新 (staleTimecacheTime): 确定数据保持新鲜的时间,以及未被使用时保持在缓存中的时间。

  • 自动重试 (retry): 当请求失败时,自动进行重试。

  • 分页和加载更多 (页码或游标): 当我们需要分页或者无限加载数据时使用。

  • 数据预加载 (queryClient.prefetchQuery): 加载关键数据以提升用户体验。

  • 数据变异 (useMutation): 提交数据至服务器,并更新本地缓存。


示例应用


获取用户列表


我们使用useQuery钩子来获取用户数据。这个钩子会自动发起请求并监听数据状态。


import { useQuery } from 'react-query';

const fetchUsers = async (page = 0) => {
const response = await fetch(`https://my-api/users?page=${page}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
};

function Users() {
const { data, error, isLoading, isFetching } = useQuery('users', fetchUsers);

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;

return (
<>
{data.users.map(user => (
<div key={user.id}>{user.name}</div>
))}
{isFetching ? <span>Updating...</span> : null}
</>

);
}

自动刷新和背景更新


React Query可以配置数据自动刷新的时间,我们可以设置staleTime来避免不必要的后台更新,同时让我们的数据保持最新。


const { data } = useQuery('users', fetchUsers, {
staleTime: 5 * 60 * 1000 // 每5分钟更新一次数据
});

自动重试


如果请求失败,React Query可以自动尝试重新获取数据:


const { data } = useQuery('users', fetchUsers, {
retry: 2 // 请求失败会尝试2次重试
});

分页和加载更多


对于需要加载更多数据的情况,我们可以使用React Query的页码或游标方法来实现:


const { data, fetchNextPage, hasNextPage } = useInfiniteQuery(
'users',
({ pageParam = 0 }) => fetchUsers(pageParam),
{
getNextPageParam: (lastPage, allPages) => lastPage.nextPage,
}
);

// ...

<button onClick={() => fetchNextPage()} disabled={!hasNextPage}>
Load More
</button>


加载更多的按钮会根据hasNextPage来判断是否还有更多数据可以加载。


数据预加载


我们可以在用户的鼠标悬浮到某个按钮上时提前获取数据:


const queryClient = useQueryClient();

// ...

<button
onMouseEnter={() =>
queryClient.prefetchQuery('more-users-data', fetchAdditionalUsers)}
>
Show More Users
</button>


数据变异


当需要提交数据到服务端时,我们可以使用useMutation来处理:


import { useMutation, useQueryClient } from 'react-query';

const addUser = async (newUser) => {
const response = await fetch(`https://my-api/users`, {
method: 'POST',
body: JSON.stringify(newUser)
});
if (!response.ok) {
throw new Error('Could not add user');
}
return response.json();
};

function AddUser() {
const queryClient = useQueryClient();
const mutation = useMutation(addUser, {
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries('users');
}
});

return (
<button
onClick={() =>
{
const newUser = { name: 'New User' };
mutation.mutate(newUser);
}}
>
Add User
</button>

);
}

当我们向服务端增加一个新用户时,使用useMutation并提供一个成功的回调,该回调通过调用queryClient.invalidateQueries来标记用户列表的缓存为无效,以便它可以自动重新获取最新的用户列表。


总结React Query的优势


通过上述示例,我们可以看到React Query提供了强大且灵活的功能来处理数据的获取、缓存、更新、预加载、变异等操作。它大大简化了数据同步和状态管理的复杂性,使开发者可以专注于构建交互式的用户界面,而不必担心数据操作的底层细节。此外,React Query的自动重试和智能缓存策略可以提高应用的健壮性和用户的体验。


最后,简要地复习一下React Query的优势:



  1. 内置缓存功能:React Query 为获取的数据提供缓存机制,这意味着当组件重新渲染或者同用户交互时,相同的数据正在加载,不需要再次发起网络请求,可以直接从缓存中获取数据。这减少了不必要的网络请求,提高了应用的效率。

  2. 错误处理和错误重试:在处理异常数据时,错误处理和错误和错误重试在其他较繁琐。React Query 提供了强化的方式来处理这些状态,简化了开发者的工作。

  3. 优化数据获取:React Query 会自动合并重复的查询请求,并将它们批量处理。这意味着如果多个组件请求相同的数据,React Query 只会发送一次网络请求,并且将数据分发给所有请求的组件。

  4. 简洁高效和提高内存性能:通过减少不必要的网络请求和优化数据处理,React Query 可以帮助节省带宽并提高应用的响应性能。

  5. 数据同步:在复杂的应用中,保持组件间数据的同步是一个挑战。React Query 通过其高层机制,帮助保持不同组件间数据的一致性。


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

flutter chat UI again flutter 漂亮聊天UI界面实现

flutter 漂亮聊天UI界面实现 flutter chat UI  之前写了一个聊天界面,但是只是花架子,并不能使用,无法点击,无法活动,并且由于时间问题也没有完全完成,右侧的聊天界面没有实现。现在,我准备完成一个比较美观且能使用的聊天界面。 寻找聊天界面...
继续阅读 »

flutter 漂亮聊天UI界面实现 flutter chat UI


 之前写了一个聊天界面,但是只是花架子,并不能使用,无法点击,无法活动,并且由于时间问题也没有完全完成,右侧的聊天界面没有实现。现在,我准备完成一个比较美观且能使用的聊天界面。


寻找聊天界面模板


 先找一个美观的模板来模仿吧。找模板的标准是简介、美丽、大方、清新。


1.png


 这次选的是一个比较简洁的界面,淡蓝色为主色,横向三个大模块排列开来,有设置界面、好友列表、聊天界面,就选定用这个了。


chatUI聊天界面实现


整体分析


 最外层使用横向布局,分别放置三个大组件,每个组件里面使用竖向布局来放置各种按钮、好友列表、聊天界面。每个组件里面的细节我们边实现边学习。


外层框架


 我们先实现最外边的框架。用SelectionArea包裹所有后续组件,实现所有文字可以选定。Selection现在有了官方的正式支持,该功能补全了Flutter长时间存在Selection异常等问题,尤其是在Web框架下经常会有选择文本时与预期的行为不匹配的情况。接着用Row水平布局组件来包裹三大块细分功能组件,代码里先用text组件代替。这样框架就设置好了。


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false, //去掉右上角debug标识
theme: ThemeData(
//主题设置
primarySwatch: Colors.blue,
),
home: const SelectionArea(
//子组件支持文字选定 3.3新特性
child: Scaffold(
//子组件
body: MyAppbody(),
),
),
);
}
}

class MyAppbody extends StatelessWidget {
const MyAppbody({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Row(
//水平布局
children: const <Widget>[
//子组件
Expanded(
flex: 1, //空间占比
child: Text("按钮组件"), ),

Expanded(
flex: 1, //空间占比
child: Text("好友列表组件"), ),

Expanded(
flex: 3, //空间占比
child: Text("聊天框组件"), ),

],
),
);
}
}

 效果图:


2.png


第一个模块设计


 新建一个fistblock文件夹放置我们的第一个模块代码,实现代码分块抽离。还是先写大框架,外围放置竖向排列组件Column,然后再依次放进去头像模块和设置模块。Column是垂直布局,在Y轴排列,也就是纵轴上的排列方式,可以使其包含的子控件按照垂直方向排列,Column是Widget的容器,存放的是一组Widget,而Container里面一次只能存放一个child。


import 'package:flutter/material.dart';
class FistBlockMain extends StatelessWidget {
const FistBlockMain({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
////竖直布局
children: const <Widget>[
//子组件
Expanded(
flex:1, //空间占比
child: Text("头像"),
),

Expanded(
flex: 1, //空间占比
child: Text("设置"),
),

Expanded(
flex:1, //空间占比
child: Text("帮助"),
),

],
),
);
}
}

 效果图:


3.png


头像模块实现


 头像模块我们之前也实现过,现在可以直接拿来用,例子里在线状态小圆点在右上角,这里我们依旧利用Badge实现小圆点,同时圆点位置可以自由设置,我比较习惯放在右下角,当然,你也可以通过设置Badge的position参数改变位置。Badge是flutter的插件,flutter也有很多其他的优秀的插件可以使用,有了插件的帮忙,我们可以很方便的实现各种功能。


class User extends StatelessWidget {
const User({super.key});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Badge(
//头部部件
//通知小圆点
badgeColor: Colors.green, //小圆点颜色
position: const BadgePosition(
start: 35, top: 35, end: 0, bottom: 0), //小圆点显示位置
borderSide:
const BorderSide(color: Colors.white, width: 1.5), //外层白色圆圈框框
child: const CircleAvatar(
//图片圆形剪裁
radius: 25, //圆形直径,(半径)?
backgroundColor: Colors.white, //背景颜色设置为白色
backgroundImage: AssetImage(
"images/5.jpeg", //图片
),
),
),
title: const Text(//标题
"George",
style: TextStyle(
fontSize: 15, //字体大小
fontWeight: FontWeight.bold, //字体加粗
),
),
);
}
}

 效果图:


4.png


第一模块蓝色背景模块实现


 写完头像模块突然想起来,第一模块的蓝色背景还没实现呢,现在来实现一个蓝色的背景。因为是背景,所以应该用层叠Stack组件。背景颜色用Container的decoration来设置,实际使用BoxDecoration实现背景颜色盒子的设置,同时还需要设置阴影。BoxDecoration类提供了多种绘制盒子的方法,这个盒子有边框、主体、阴影组成,盒子的形状可能是圆形或者长方形。如果是长方形,borderRadius属性可以控制边界的圆角大小。


class FistBlockMain extends StatelessWidget {
const FistBlockMain({super.key});
@override
Widget build(BuildContext context) {
return Stack(children: <Widget>[
const Backgroud(),
Column(
//竖直布局
children: const <Widget>[
//子组件
Expanded(
flex: 1, //空间占比
child: User(),
),

Expanded(
flex: 1, //空间占比
child: Text("设置"),
),
Expanded(
flex: 1, //空间占比
child: Text("帮助"),
),
],
),
]);
}
}

class Backgroud extends StatelessWidget {
const Backgroud({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Color.fromARGB(220, 100, 149, 237),
boxShadow: [
BoxShadow(
color: Color.fromARGB(220, 100, 149, 237),
blurRadius: 30, //阴影模糊程度
spreadRadius: 1 //阴影扩散程度
)
],
),
);
}
}

 效果图:


5.png


 模板的这个颜色我找了半天也没找到,后来就找个相似的先用着,但是总是看起来没有原来的好看。当个程序员难道还需要懂美术和艺术吗。。。


按钮模块实现


 接着要实现若干带图标的按钮了。模板是一个带图标的按钮,我们用TextButton.icon组件实现。按钮能被选定会影响操作体验,这里使用SelectionContainer是他不能被选中。外层使用Column布局依次放置按钮组件。使用Padding调整间距,是他更好看一些。图标和文字大小都是可以设置的。通过Text组件的TextStyle设置文字的颜色、大小,这里我们使用白色的文字。图标使用Icon组件实现,直接使用Icons.lock_clock内置的icon图标。按钮的onPressed和autofocus需要设置,这样的话点击按钮才会有动画显示。Padding组件再一次使用,这个组件我感觉很好用,可以通过他进一步调整部件的位置,进行美化。


class Buttonblock extends StatelessWidget {
const Buttonblock({super.key});
@override
Widget build(BuildContext context) {
return SelectionContainer.disabled(//选定失效
child: Column(
children: <Widget>[
//子组件
Padding(
padding: const EdgeInsets.fromLTRB(0, 20, 0, 20),
child: TextButton.icon(
icon: const Icon(
size: 22,
Icons.lock_clock,
color: Colors.white,
), //白色图标
label: const Text(
"Timeline",
style: TextStyle(
fontSize: 14, //字体大小
fontWeight: FontWeight.bold, //字体加粗
color: Colors.white //白色文字
),
),
onPressed: (){},//点击事件
autofocus: true,
),
),

Padding(
padding: const EdgeInsets.fromLTRB(0, 20, 0, 20),
child: TextButton.icon(
icon: const Icon(
size: 22,
Icons.message,
color: Colors.white,
), //白色图标
label: const Text(
"Message",
style: TextStyle(
fontSize: 14, //字体大小
fontWeight: FontWeight.bold, //字体加粗
color: Colors.white //白色文字
),
),
onPressed: () {
},
autofocus: true,
),
),
],
),
);
}
}


 效果图:


6.png


按钮点击弹窗showDialog实现


 是按钮当然需要被点击,点击之后我们可以弹一个窗给用户进行各种操作。这里用showDialog实现弹窗。在TextButton.icon的onPressed下实现一个点击弹窗操作。在Flutter里有很多的弹出框,比如AlertDialog、SimpleDialog,调用函数是showDialog。对话框也是一个UI布局,通常会包含标题、内容,以及一些操作按钮。这里实现一个最简单的对话框,如果有需求可以在这个基础上进行修改。


 Padding(
padding: const EdgeInsets.fromLTRB(0, 20, 0, 20),
child: TextButton.icon(
icon: const Icon(
size: 22,
Icons.message,
color: Colors.white,
), //白色图标
label: const Text(
"Message",
style: TextStyle(
fontSize: 14, //字体大小
fontWeight: FontWeight.bold, //字体加粗
color: Colors.white //白色文字
),
),
onPressed: () {//点击弹框
showDialog<void>(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
title: const Text('选择'),
children: <Widget>[
SimpleDialogOption(
child: const Text('选项 1'),
onPressed: () {
Navigator.of(context).pop();
},
),
SimpleDialogOption(
child: const Text('选项 2'),
onPressed: () {//点击事件
Navigator.of(context).pop();
},
),
],
);
},
).then((val) {
});
},
autofocus: true,
),
),

 效果图:


7.png


第二个模块设计


 第二个模块是两部分,上边部分是一个在线状态展示区域,下边部分是好友列表,中间有一道分隔线。所以第二部分外层使用Column竖直布局组件,结合Stack组件做一个背景色。Stack可以容纳多个组件,以叠加的方式摆放子组件,后者居上,覆盖上一个组件。Stack也是可以存放一组Widget的组件。


class SecondBlockMain extends StatelessWidget {
const SecondBlockMain({super.key});
@override
Widget build(BuildContext context) {
return
Stack(children: <Widget>[
const Backgroud(),
Column(
//竖直布局
children: const <Widget>[
//子组件
Expanded(
flex: 1, //空间占比
child: Text("上边"),
),
Expanded(
flex:4, //空间占比
child: Text("下边"),
),
],
),
]);
}
}

第二个模块灰色背景颜色实现


 仔细看第二部分发现也是有背景颜色的和阴影的,只不过很浅,不容易看出来。刚才已经实现了带阴影的背景,稍微改一下颜色就可以了,依旧要结合Stack组件。BoxShadow的两个参数blurRadius和spreadRadius经常使用,其中blurRadius是模糊半径,也就是阴影半径,SpreadRadius是阴影膨胀数值,也就是阴影面积扩大几倍。


class Backgroud extends StatelessWidget {
const Backgroud({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: const Color.fromARGB(255, 238, 235, 235).withOpacity(0.6),
boxShadow: [
BoxShadow(
color: const Color.fromARGB(255, 204, 203, 203).withOpacity(0.5),
blurRadius: 20, //阴影模糊程度
spreadRadius: 20 ,//阴影扩散程度
offset:const Offset(20,20), //阴影y轴偏移量
)
],
),
);
}
}

 效果图:


8.png


在线状态展示区域实现


 本来想着放一个图片在这个位置就好了,这样简单。但是如果拖动界面,改变小大,那么图片就会变形,很不美观。所以利用横向布局组件Row放在外层,里面包裹Badge组件实现小圆点,通过position、badgeColor等组件调整圆点位置和颜色。


class Top extends StatelessWidget {
const Top({super.key});
@override
Widget build(BuildContext context) {
return Row(children: [
Expanded(
flex: 1,
child: ListTile(
leading: Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 0, 0),
child: Badge(
//小圆点
badgeColor: Colors.orange, //小圆点颜色
position: const BadgePosition(
start: -70, top: 0, end: 0, bottom: 0), //小圆点显示位置
borderSide: const BorderSide(
color: Colors.white, width: 1.5), //外层白色圆圈框框
child: const Text(
//标题
"Family",
style: TextStyle(
fontSize: 12, //字体大小
fontWeight: FontWeight.bold, //字体加粗
color: Colors.black),
),
),
),
)),
Expanded(
flex: 1,
child: ListTile(
leading: Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 0, 0),
child: Badge(
//小圆点
badgeColor: Colors.cyan, //小圆点颜色
position: const BadgePosition(
start: -70, top: 0, end: 0, bottom: 0), //小圆点显示位置
borderSide: const BorderSide(
color: Colors.white, width: 1.5), //外层白色圆圈框框
child: const Text(
//标题
"Friend",
style: TextStyle(
fontSize: 12, //字体大小
fontWeight: FontWeight.bold, //字体加粗
color: Colors.black),
),
),
),
)),
]);
}
}

 效果图:


9.png


好友列表实现


 好友列表之前也实现过,这次在以前的基础上修改。我们使用ListView组件实现列表,ListView是最常用的可滚动组件之一,它可以沿一个方向线性排布所有子组件。底层使用Column结合ListTile组件,ListTile结合CircleAvatar可以实现圆形头像效果,同时也可以设置主副标题,设置 focusColor改变鼠标悬停时列表颜色,


List listData = [  {"title": 'First', "imageUrl": "images/1.jpg", "description": '09:15'},  {"title": 'Second', "imageUrl": "images/2.jpg", "description": '13:10'},];

class FriendList extends StatelessWidget {
const FriendList({super.key});
@override
Widget build(BuildContext context) {
return ListView(
children: listData.map((value) {//重复生成列表
return Column(
children: <Widget>[
ListTile(
onTap: (){},
hoverColor: Colors.black,// 悬停颜色
focusColor: Colors.white,//聚焦颜色
autofocus:true,//自动聚焦
leading: CircleAvatar(//头像
backgroundImage: AssetImage(value["imageUrl"]),
),
title: Text(
value["title"],
style: const TextStyle(
fontSize: 25, //字体大小
color: Colors.black),
),
subtitle: Text(value["description"])),
const Padding(
padding: EdgeInsets.fromLTRB(70, 10, 0, 30),
child: Text(
maxLines: 2,
"There are moments in life when you miss someone so much that you just want to pick them from your dreams and hug them for real!",
style: TextStyle(
fontSize: 12,
height: 2, //字体大小
color: Colors.grey),
),
)
],
);
}).toList(), //注意这里要转换成列表,因为listView只接受列表
);
}
}


 效果图:


10.png


第三个模块设计


 现在来第三个模块,聊天界面。分析模板布局,从上到下依次是一个搜索框,分隔线,聊天主界面,输入框,表情、视频、语音工具栏和发送按钮。我们,从上到下把他分成四个小部分来实现,外层使用Column组件。


class ThirdBlockMain extends StatelessWidget {
const ThirdBlockMain({super.key});
@override
Widget build(BuildContext context) {
return Stack(children: <Widget>[
Column(
//竖直布局
children: const <Widget>[
//子组件
Text("1"),
Divider(
height: 0.5,
indent: 20.0,
color: Colors.grey,
),
Text("2"),
Divider(
height: 0.5,
indent: 20.0,
color: Colors.grey,
),
Text("3"),
Text("4"),

],
),
]);
}
}


 效果图:


11.png


搜索框实现


 之前实现过搜索框,直接拿过来改一改。外层添加一个SizedBox组件来控制一下搜索框的大小和位置。


class SearchWidget extends StatefulWidget {

const SearchWidget(
{Key? key,
this.height,
this.width,
this.hintText,
this.onEditingComplete})
: super(key: key);

@override
State<SearchWidget> createState() => _SearchWidgetState();
}

class _SearchWidgetState extends State<SearchWidget> {
var controller = TextEditingController();
@override
void initState() {
super.initState();
}

@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constrains) {
return SizedBox(
width: 400,
height: 40,
child: TextField(
controller: controller, //控制器
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search), //头部搜索图标
filled: true,
fillColor: Colors.grey.withAlpha(50), // 设置输入框背景色为灰色,并设置透明度
hintText: "Search people",
hintStyle: const TextStyle(color: Colors.grey, fontSize: 14),
contentPadding: const EdgeInsets.only(bottom: 20),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15), //圆角边框
borderSide: BorderSide.none,
),
suffixIcon: IconButton(
//尾部叉叉图标
icon: const Icon(
Icons.close,
size: 17,
),
onPressed: clearKeywords, //清空操作
splashColor: Theme.of(context).primaryColor,
)),
),
);
});
}
}

 效果图:


12.png


聊天,信息发送界面实现


 因为我的这个并不能真的实现聊天,所以就先放text组件在这把吧,后边再进一步完善。这里简单做一些美化操作,输入框不需要背景颜色,图标需要设置成蓝色,同时调节两个模块的长宽高来适应屏幕。输入框使用TextField,与搜索框使用一致。这里要用到StatefulWidget来完成情况输入框的操作。


class ChatUi extends StatelessWidget {
const ChatUi({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
width: 100,
height: 400,
child: Text(""),
);
}
}

class InPutUi extends StatefulWidget {

const InPutUi(
{Key? key,
this.height,
this.width,
this.hintText,
this.onEditingComplete})
: super(key: key);

@override
State<InPutUi> createState() => _InPutUi();
}

class _InPutUi extends State<InPutUi> {
var controller = TextEditingController();
@override
void initState() {
super.initState();
}

@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constrains) {
return TextField(
controller: controller, //控制器
decoration: InputDecoration(
filled: true,
fillColor: Colors.white.withAlpha(50), // 设置输入框背景色为灰色,并设置透明度
hintText: "Write something...",
hintStyle: const TextStyle(color: Colors.grey, fontSize: 14),
contentPadding: const EdgeInsets.only(bottom: 20),
border: const OutlineInputBorder(
borderSide: BorderSide.none,
),
suffixIcon: IconButton(
color: Colors.blue,
//尾部叉叉图标
icon: const Icon(
Icons.send,
size: 16,
),
onPressed: clearKeywords, //清空操作
splashColor: Theme.of(context).primaryColor,
)),

);
});
}
}

 效果图:


13.png


底部工具界面实现


 最后来实现底部工具栏。外层使用横向布局来依次放入带图标按钮。这里用到IconButton、MaterialButton两种组件来实现按钮,一种是图标按钮,一种是普通按钮,之前已经实现过,拿来就可以用了。外围使用Padding组件进行填充,方便后期调整每个组件的位置,使它更好看一点。



class Bottom extends StatelessWidget {
const Bottom({super.key});
@override
Widget build(BuildContext context) {
return Row(children: [
Padding(
padding: const EdgeInsets.fromLTRB(30, 0, 0, 0),
child: IconButton(
icon: const Icon(Icons.mood),
tooltip: 'click IconButton',
onPressed: () {},
),
),
Padding(
padding: const EdgeInsets.fromLTRB(580, 20, 0, 22),
child: MaterialButton(
height: 35,
color: Colors.blue,
onPressed: () {}, //点击事件
autofocus: true,
child: const Text(
'Send',
style: TextStyle(
fontSize: 12, //字体大小
fontWeight: FontWeight.bold, //字体加粗
color: Colors.white),
),
),
),
]);
}
}



 效果图:


14.png


总结


 到这里基本上就完成了, 当然,他是不能实际使用的,因为点击、数据交互等功能还没实现,因为我还不会。后期再边学边写吧。


 模板图:


1.png


 完成图:
14.png
 自己实现的与模板还是差距很大的。自己的看起来就没那么美观,我应该去学学美术了,一点艺术细胞都没有。


作者:头好晕呀
来源:juejin.cn/post/7232274061283115045
收起阅读 »

深入探究npm run的底层原理

web
起因 某一天,我正在很悠闲的喝着小茶,无所事事浏览着技术文章,忽然看到一篇文章成功的吸引了我的眼球——你真的了解npm run吗,看完之后打开了一扇新世界的大门。 我陷入了沉思之后,有那么一瞬间感觉到了不可思议。惯性思维下,我们已经习惯性的认为命令就是要通过n...
继续阅读 »

起因


某一天,我正在很悠闲的喝着小茶,无所事事浏览着技术文章,忽然看到一篇文章成功的吸引了我的眼球——你真的了解npm run吗,看完之后打开了一扇新世界的大门。


我陷入了沉思之后,有那么一瞬间感觉到了不可思议。惯性思维下,我们已经习惯性的认为命令就是要通过npm run的方式进行执行,而没有进一步思考为什么是这样的。大多数可能跟我一样,第一反应是:难道不是因为在 package.json 中定义了各种的 script 命令,然后我们通过npm run xxx的方式进行执行吗?


揭开事情的迷雾,我们抱着打破沙锅问到底的心态来思考一下:


我们为什么要执行 npm run start 命令,而不是直接执行 react-scripts start 呢?


难道不是因为使用方便吗


这是我的第一反应,但是,实际上可能不是的。为了验证一下两者的区别,我立马跑去打开我的测试项目,分别使用了两种方式。意外的发现直接执行命令的方式竟然报错了,而通过npm run 的方式执行的时候一如既往的Ok。


image.png


为什么命令会不存在呢


报错信息很明显,直接执行命令的时候,系统报错:react-scripts 命令不存在。


这个时候我就感到有点不可思议了。


既然命令不存在,凭什么npm run就可以执行呢?


不知道大家在windows上安装node的时候,需要将node配置到系统环境变量里面去了,然后我们可以全局通过 node -v 来验证node是否安装成功和查询当前node版本信息。难道说 npm run 的玄机跟node配置过环境变量有关系吗?


抱着怀疑的态度,我在node文件夹中一通翻找(我是基于nvm进行node管理的,可能跟直接使用node的目录结构有所出入),终于找到了问题的关键信息:安装依赖的时候,会在这里创建几个命令文件。


image.png


经过几次反复的安装、删除依赖操作之后,终于确认了我的想法。每次我们通过 npm i xxx -g 安装某个依赖的时候,除了在node下的node_modules文件夹中安装对应的依赖包之外,还会在node下创建这个依赖的可执行文件(对应不同的环境,会有好几个不同的命令文件)。


这个时候,我忽然想起来了linux上的操作,在linux上安装全局依赖的时候,我们安装完依赖之后,还需要手动创建软连接。两相印证,事实的真相已经很明显了。


    ln -s /usr/local/src/nodejs/bin/node /usr/local/bin/node

ln -s /usr/local/src/nodejs/bin/npm /usr/local/bin/npm

安装依赖的时候,会在bin目录下创建一个对应的可行性文件,这个其实就跟我们node文件夹下创建的这个npm文件夹的性质是一样的。


npm run 命令可执行总结


我们经过一番摸索终于弄清楚了,这里我们再一起来总结一下:



  1. 我们安装依赖的时候,在对应的文件夹下创建了对应的可行性命令;

  2. 我们执行npm run命令的时候会在当前目录中查找相关命令,如果找到的话,直接运行对应的命令;

  3. 如果没有找到的话,会到全局的node文件夹下查找相关的命令,如果找到的话,直接运行对应的命令;

  4. 如果依然没有找到的话,就会报错误信息了


为什么会创建多个可执行文件呢


前面我们说到了,创建的可执行文件是有多个。细心的你可能已经注意到其中的一个可执行文件xxx.cmd了,它的类型很明显已经告诉我们它是什么了 —— Windows 命令脚本。大胆的猜测一下:另外几个分别对应的是不同环境的可执行命令,比方说:没有文件后缀的可执行文件,其实就是我们前面说到的在linux中安装的软链接的方式。


我们大致看一下其中一个cross-env.cmd的可执行命令的内容(假装可以看得懂)。


    @ECHO off
SETLOCAL
CALL :find_dp0

IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)

"%_prog%" "%dp0%\node_modules\cross-env\src\bin\cross-env.js" %*
ENDLOCAL
EXIT /b %errorlevel%
:find_dp0
SET dp0=%~dp0
EXIT /b

虽然看不懂,但是其中很重要的一个点我们其实还是可以猜出来的 "%_prog%" "%dp0%\node_modules\cross-env\src\bin\cross-env.js" %*将可执行命令 cross-env 指向对应的依赖的bin文件 bin\cross-env.js。


这里其实变相的给我们解释了另外一个问题。


为什么安装依赖可以创建可执行命令呢


在依赖的package.json中配置了bin属性,定义了可执行命令的名字和可执行命令的文件,当我们通过npm安装依赖的时候,npm就会根据声明的bin属性来创建对应的可执行文件。


    "bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
},

相信看到这里之后,大家应该心里已经很清楚npm run的底层原理了。


打完收功


好了,有关npm run的内容暂时就这么多了,希望对大家有所帮助。


欢迎大家在下方进行留言交流。


作者:花开花落花中妖
来源:juejin.cn/post/7313203461705580580
收起阅读 »

董老师的话充满力量——手写call、apply、bind

web
前言: 大家好,我是小瑜。最近在网上看到了东方甄选和董宇辉的小作文事件,我也一直是众多吃瓜群众的一员,看完后董宇辉俞敏洪的联合直播,心中也有很多感触。 董宇辉老师说,一放假,就喜欢往家里跑,因为接近土地可以感到踏实。一个千万网红这种纯粹质朴的精神着实让人很贴切...
继续阅读 »

前言:


大家好,我是小瑜。最近在网上看到了东方甄选和董宇辉的小作文事件,我也一直是众多吃瓜群众的一员,看完后董宇辉俞敏洪的联合直播,心中也有很多感触。


董宇辉老师说,一放假,就喜欢往家里跑,因为接近土地可以感到踏实。一个千万网红这种纯粹质朴的精神着实让人很贴切。


特别是董老师的另外一番话:你必须人生中有一段经历是自己走过去的。你充满了痛苦,然后充满了孤独,但这个东西叫做成长,好的生活和幸福的经历是不能带来成长的,所以能见到很多人,四五十岁看着还很幼稚,说明他从来没有受到苦,能让你成长的东西,就是让你反思的东西,因为在历史的长河中进化是痛苦的,逼不得已,人才会进步很成长,所以成长都不快乐。但同时也恭喜你一直在成长。


在学习以及编写这篇文章的时候,我也是痛苦的,同时也有所收获。接下来给大家分享this指向以及手写call、apply、bind。


在这之间给大家简单举几个例子说明下this指向的不同


普通函数调用


// 谁调用就是谁, 直接调用window
function sayHi() {
console.log(this); // window
}
sayHi() // === window.sayHi()

对象中的方法调用


const obj = {
name: 'zs',
objSayHi() {
console.log(this) // obj
setTimeout(() => {
console.log(this, 'setTimeout'); // obj
}, 1000),
function inner() {
console.log(this); // window
}
inner()
},
qwe: () => console.log(this) // window
}
obj.objSayHi()
obj.qwe()

obj.objSayHi() => obj



  • 因为是 **obj **对象调用,所以 **this **指向 **obj **这个对象


obj.qwe() => window



  • 对于箭头函数 qwe,它捕获的是定义时外部的 this 上下文。在浏览器中全局范围内的箭头函数 qwethis 指向的是全局对象 window(或者是全局的 this,具体取决于执行上下文)。


inner() => window



  • inner() 函数是通过常规函数声明方式定义的。在 JavaScript 中,常规函数声明方式中的 this 在严格模式下指向 undefined,而在非严格模式下(例如浏览器环境中),this 指向全局对象(在浏览器中通常是 window 对象)。因此,当 inner() 函数在 objSayHi() 方法内部被调用时,其 this 指向全局对象 window


setTimeout => obj



  • objSayHi 方法中,setTimeout 中的回调函数使用了箭头函数。箭头函数内部的 this 会捕获最近的普通函数(非箭头函数)的 this 值,也就是 objSayHi 被调用时的 this。因此,setTimeout 中的箭头函数捕获到的 this 值指向的是 obj 对象。


总结:浏览器环境中, 谁调用this指向谁,但是箭头函数的this义是外部的 this 上下文。通过常规函数声明方式定义this指向window。其他关于this指向可以参考这张图


image.png


修改this指向


call


第1个参数为this,第2-n为传入该函数的参数


function myThis1(name, age) {
console.log(this);
console.log(name);
console.log(age);
}
const obj = {
name: 'zs',
age: 18
}
myThis1.call(obj, 'ls', 20) // {name:"zs",age:18} ls 18

apply


第1个参数为this,第2-n已数组的方式传递


function myThis1(name, age) {
console.log(this);
console.log(name);
console.log(age);
}
const obj = {
name: 'zs',
age: 18
}
myThis1.apply(obj, ['王五', 18]) // {name:"zs",age:18} 王五 18

bind


bind() 方法创建一个新函数,当调用该新函数时,它会调用原始函数并将其 this 关键字设置为给定的值,同时,还可以传入一系列指定的参数


function myThis1(name, age) {
console.log(this);
console.log(name);
console.log(age);
}
const obj = {
name: 'zs',
age: 18
}

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/92cfe20fc6374374bacf97bcc3d31ac6~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=814&h=308&s=145955&e=png&b=fafafa)
const fn = myThis1.bind(obj, '赵六',30)
fn() // {name:"zs",age:18} 赵六 30

手写call函数


要求实现


const obj = {
name: 'zs',
age: 20
}
function myFn(a, b, c, d, e) {
console.log(`大家好,我的名字叫${this.name} 我今年${this.age}岁了`, a, b, c, d, e);
}
myFn.myCall(obj, 1, 2, 3, 4, 5)

简单思路:



  1. ** **原本并不存在 **myCall **方法,那么如何去创建这个方法?

  2. 如何让函数内部的 **this **为 某个对象?

  3. 如何将调用时传入的参数传入到 **myFn **函数中?


实现思路1:通过函数原型的方式,给原型添加 myCall 方法,这样通过原型链就可以使用


Function.prototype.myCall = function () {
console.log('myCall被调用了');
}
myFn.myCall()


实现思路2:在myCall调用的时候将obj传入到函数中,并根据谁调用this就指向谁的原则给对象添加this方法并执行


首先可以打印看一下thisArg,this 分别是什么


const obj = {
name: 'zs',
age: 20
}
Function.prototype.myCall = function (thisArg) {
console.log('myCall被调用了',thisArg,this);
}
function myFn(a, b, c, d, e) {
console.log(`大家好,我的名字叫${this.name} 我今年${this.age}岁了`, a, b, c, d, e);
}
myFn.myCall(obj)

image.png
很明显 **thisArg 就是 obj **对象 而 this就是 myFn 这个函数,那么就可以根据谁调用this就指向谁的原则,将obj这个对象也就是 **thisArg **添加 myCall 方法 = this


  Function.prototype.myCall = function (thisArg) {
console.log('myCall被调用了', thisArg, this);
thisArg.myCall = this
thisArg.myCall()
}
function myFn(a, b, c, d, e) {
console.log(`大家好,我的名字叫${this.name} 我今年${this.age}岁了`, a, b, c, d, e);
}
const obj = {
name: 'zs',
age: 20
}
myFn.myCall(obj, 1, 2, 3, 4, 5)

此时就发现,this已经成功指向了这个obj对象,但是还差参数没有传递,接下去就去实现


image.png


实现思路3:利用剩余参数加展开运算符传入参数


  Function.prototype.myCall = function (thisArg,...args) {
console.log('myCall被调用了', thisArg, this);
thisArg.myCall = this
thisArg.myCall(...args)
}
function myFn(a, b, c, d, e) {
console.log(`大家好,我的名字叫${this.name} 我今年${this.age}岁了`, a, b, c, d, e);
}
const obj = {
name: 'zs',
age: 20
}
myFn.myCall(obj, 1, 2, 3, 4, 5)

此时就基本上可以完成了,还有一点优化,就是查看** obj 发现 myCall **是一直存在的,因为之前通过给原型添加方法,希望的是使用完成后将myCall方法删除,这里只需要 在 **myCall 最后再添加一句 delete thisArg.myCall **即可


优化: 增加返回值并 利用 Symbol 动态生成唯一的属性名


Function.prototype.myCall = function (thisArg, ...args) {
const key = Symbol()
thisArg[key] = this
const res = thisArg[key](...args)
delete thisArg[key]
return res
}

手写apply


apply 方法同理 call 只是第二个参数需要改为数组


Function.prototype.myApply = function (thisArg, args) {
console.log(args);
const key = Symbol()
thisArg[key] = this
const res = thisArg[key](args)
delete thisArg[key]
return res
}
const obj = {
name: 'zs',
age: 20
}
function myFn(args) {
const div = `大家好,我的名字叫${this.name} 我今年${this.age}岁了,${args.toString()}`
return div
}
const res = myFn.myApply(obj, [1, 2, 3, 4, 5])
console.log(res);

手写bind


Function.prototype.myBind = function (thisArg, ...args) {
const fn = this
return function (...args1) {
const allArgs = [...args, ...args1]
// 判断是否为new的构造函数
if (new.target) {
return new fn(...allArgs)

} else {
return fn.call(thisArg, allArgs)
}
}
}
const obj = {
name: 'zs',
age: 20
}
function myFn(...arg) {
console.log(`大家好,我的名字叫${this.name} 我今年${this.age}岁了,${arg}`);
const div = `大家好,我的名字叫${this.name} 我今年${this.age}岁了,${arg}`
return div
}
const res = myFn.myBind(obj, '1')
console.log(res('122'));

作者:不知名小瑜
来源:juejin.cn/post/7313135267572121612
收起阅读 »

回望我在谷歌的 18 年

最近有篇文章很火,是一名谷歌前员工写的《Reflecting on 18 years at Google》,翻译过来给大家看看。 我于 2005 年 10 月加入谷歌,18 年后,我递交了辞呈。上周,我结束了在谷歌的最后一段日子。 对于能够亲历谷歌上市初期的...
继续阅读 »

最近有篇文章很火,是一名谷歌前员工写的《Reflecting on 18 years at Google》,翻译过来给大家看看。




我于 2005 年 10 月加入谷歌,18 年后,我递交了辞呈。上周,我结束了在谷歌的最后一段日子。


对于能够亲历谷歌上市初期的时光,我感到非常幸运;不同于大多数公司,与通常的看法相反,从基层工程师到高层管理者,谷歌的员工都真心致力于做正确的事情。经常被嘲讽的口号“不作恶”实际上是当时公司的核心原则(这在很大程度上是对当时像微软这样的同行,将利润放在客户和人类整体利益之上的做法的一种反抗)。


我见证了谷歌因为真心想为社会做出贡献而遭到的诸多误解和批评。


比如谷歌图书项目。围绕 Chrome 和搜索功能的诸多批评,尤其是那些关于广告利益冲突的指控,大都是毫无根据的(令人惊讶的是,巧合和错误有时会被误解为恶意)。我经常看到隐私倡导者以损害用户利益的方式反对谷歌的计划。


这些争议对全球产生了深远的影响;其中最让人烦恼的是,现在我们不得不面对的那些无意义的 cookie 警告。看到团队努力推进对世界有益的想法,却因为不优先考虑谷歌的短期利益而遭到公众的冷嘲热讽,这让我感到非常失望。



[2011 年,谷歌园区的 Charlie's patio。图像已被处理,移除了其中的人物]


早期的谷歌也是一个极佳的工作环境。高层领导每周都会坦诚地回答问题,或者坦率地解释无法回答的原因(比如因为法律原因或某些话题过于敏感)。Eric Schmidt 会定期向全公司介绍董事会的讨论情况。产品的成败都会客观地呈现出来,成就被庆祝,失败被深入分析,目的是为了吸取教训,而非推卸责任。公司有清晰的愿景,对于任何偏离这一愿景的行为都会给出解释。在 Netscape 实习期间,我曾经历过 Dilbert 式的管理,所以谷歌员工的整体能力和专业素养让我感到格外耳目一新。


在 Google 工作的最初九年,我的主要工作是致力于 HTML 及相关标准的开发。我的目标是做对网络最有益的事,因为这也符合 Google 的利益(我被明确指示忽视 Google 的直接利益)。这份工作是我之前在 Opera Software 公司时开始的延续。Google 为这项工作提供了极好的支持。我领导的团队名义上是 Google 的开源团队,但实际上我拥有完全的自主权(在此要特别感谢 Chris DiBona)。我大部分时间都是在 Google 园区的各个建筑中用笔记本电脑工作,有几年时间我甚至几乎没用过我的固定办公桌。


然而,随着时间的推移,Google 的企业文化也出现了一些变化。


例如,尽管我很赞赏 Vic Gundotra 的热情和他对 Google+ 最初的清晰、明确的愿景,但当项目进展不顺时,我对他给出明确回答的能力产生了怀疑。他还开始在 Google 内部设置壁垒,比如将某些建筑只限 Google+ 团队使用,这与早期 Google 完全透明的文化不同。另一个例子是 Android 团队,他们虽然是通过收购加入的,但从未完全融入 Google 的文化。Android 团队的工作与生活的平衡不佳,相比 Google 的其他部门,他们的透明度较低,更多地关注于追赶竞争对手,而不是解决用户的真实问题。


我在 Google 的最后九年投入到了 Flutter 项目上。


回想起来,我在 Google 最美好的回忆之一就是 Flutter 项目初期的日子。Flutter 是 Larry Page 在 Alphabet 成立前不久发起的几个大胆实验项目之一,属于老 Google 时代的产物。我们的运作方式更像是一家初创公司,更多的是在探索我们要做的事情,而不仅仅是设计。


Flutter 团队深受年轻时代 Google 文化的影响,比如我们重视内部透明、工作与生活的平衡以及基于数据的决策(Tao Dong 及其 UXR 团队在这方面提供了极大的帮助)。我们从一开始就保持着极高的开放性,这也让我们更容易围绕这一项目建立起一个健康的开源社区。


多年来,Flutter 也很幸运地拥有了出色的领导团队,比如创始技术领导 Adam Barth、产品经理 Tim Sneath 和工程经理 Todd Volkert。



在 Flutter 的早期发展阶段,我们并未完全遵循工程领域的最佳实践。


举个例子,我们当时既没有编写测试,文档资料也寥寥无几。这幅白板曾是我们设计核心 Widget、RenderObject 和 dart:ui 层的唯一“设计文档”,它帮助我们快速起步,但随后也让我们付出了不小的代价。


Flutter 在一个与外界几乎隔绝的“泡沫”中成长,这个“泡沫”使其与 Google 同期的变化保持了距离。Google 的企业文化逐步退化。决策的重心从原本的用户利益转变为 Google 的利益,最终演变为决策者个人利益。公司内部的透明度也随之消失。


过去,我总是满怀期待地参加每一次公司大会,希望了解公司的最新动向。然而如今,我甚至能预测出公司高层的标准答案。


至今,我在 Google 内部找不到任何人能明确地阐述 Google 的愿景。员工士气跌至谷底。如果你询问湾区的心理治疗师,他们会告诉你,他们的 Google 客户普遍对公司感到不满。


接着,Google 开始了裁员。这次裁员是一场不必要的错误,源于公司短视地追求股价季度增长,背离了其长期战略——即便短期内有所损失,也要优先考虑长期成功(即“不作恶”原则的精髓)。裁员带来的影响是隐蔽而深远的。


在此之前,员工或许还会专注于用户体验或公司利益,相信只要做对的事情,即使超出了自己的职责范围,最终也会得到回报。但裁员之后,员工无法再相信公司会支持他们,从而大幅减少了冒险尝试。职责被严格限定,知识成了用来保护自己的“武器”,因为在裁员的阴影下,变得不可替代是唯一的自保策略。这种对管理层的不信任,以及管理层对员工信任的缺失,都在 Google 的愚蠢公司政策中得到体现。


回想 2004 年,Google 的创始人曾在华尔街上坚称:“Google 不是一家传统公司,我们也不打算变成那样。”但如今,那个 Google 已经一去不复返了。


谷歌目前面临的问题很多都与 Sundar Pichai 缺乏前瞻性领导力有关,他似乎对维护早期谷歌的文化特色不太感兴趣。这种情况下,我们看到不太称职的中层管理人员逐渐增多。比如 Jeanine Banks,她管理着一个涵盖 Flutter、Dart、Go 和 Firebase 等多种项目的部门。


尽管她的部门名义上有个战略,但即便我想分享也做不到,因为我自己都搞不清楚这个战略具体是什么,即使听了好几年她的描述。她对团队的了解非常有限,经常提出些毫无意义、不切实际的要求。她对待工程师的方式就像对待工具一样,任意调动他们的岗位,而不考虑他们的技能。她对于建设性的反馈也是完全不理不睬。据我所知,其他团队的领导更擅长政治游戏,他们找到了应对她的方法,适时提供必要信息,以保持她不干扰团队工作。


作为见证过谷歌辉煌时期的人,我对这种现状感到非常失望。


不过,谷歌仍有很多优秀人才。我有幸与 Flutter 团队的很多杰出成员合作,比如 JaYoung Lee、Kate Lovett、Kevin Chisholm、Zoey Fan、Dan Field 等,实在太多了,没法一一列举(对不起,我本该提到每个人的)。


近年来,我开始向谷歌的员工提供职业建议,也因此认识了许多公司内的优秀人才。我认为挽救谷歌并非不可能。这需要从高层开始进行一些重大调整,需要有人能够带着明确的长期愿景,利用谷歌的资源为用户创造价值,而不是只盯着 CFO 办公室。我依然坚信谷歌的使命(组织世界上的信息,使其普遍可访问和有用)仍有很大的发展空间。如果有人愿意引导谷歌进入未来二十年,专注于为人类带来最大利益,而不是只关注股价的短期波动,那么谷歌完全有能力实现伟大的成就。


但我觉得时间不等人。谷歌的文化正在逐渐恶化,如果不及时纠正,这种情况最终将无法逆转。因为那些能够起到道德指南针作用的人,正是那些不愿加入没有道德指南针的组织的人。


作者:ENG八戒
来源:juejin.cn/post/7313910763978162216
收起阅读 »

一名过了更年期的中年DBA的2023失业总结 | 我还想当一名工程师

笔者介绍 -------------------截至2023年12月,已经失业8个月 笔者自我介绍,IT入行10余年,人在羊城,有房贷有车贷,家里一女一子,2023年喜获公司毕业通知,正式2023年4月底毕业,获得N+1赔偿。自以为自己读过几本书,IT各个方面...
继续阅读 »

笔者介绍


-------------------截至2023年12月,已经失业8个月


笔者自我介绍,IT入行10余年,人在羊城,有房贷有车贷,家里一女一子,2023年喜获公司毕业通知,正式2023年4月底毕业,获得N+1赔偿。自以为自己读过几本书,IT各个方面,系统、网络、数据库、应用方面略有小懂,找一份工作没有任何问题,正式6月份出来找工作。


没有想到400多封简历都是已读不回,截至11月底,大概投出600个简历,只有6个面试的机会, 将近过年,把自己的心路历程好好总结,期望 2024年工作踏入新征。


关于生理更年期


2024年后,我的简历就是40岁,用香港无厘头的说法表达,我已经过了更年期,综合指数都在下降, 内分泌再也不旺盛,外分泌严重失调。上有风竹残年的70老母,下有两个嗷嗷待哺的吞金兽,相信这样一份简历摆在HR的桌子上,大机会率对方心里存在一个疑问,这个人已经过了更年期,尚能饭否?心里各种疑问和不信任!


以前听过IT行业35岁魔咒的想法,笔者的亲自经历,过了35岁真的是被人区别对待。其中一个面试,面试官问我有没有足够的精力通宵达旦的加班干活做事?能不能在指定的时间范围内把功课赶出来? 学习最新的IT技术应用工作实践行不行有没有、能不能、行不行连续的推进强烈语气把我镇住了,我口头说可以,心想你能给具体的例子场景验证测试吗?对方一脸坏笑,很明显这是针对40的区别对待,在这个年龄层次上,给你打上了这个有色眼镜标签,更年期的中年人不行,不能、没有


我回想了一下10年前年轻力壮、血气方刚的自己,那时候 身体状况与现状没有多大差异,除了腹部6块腹肌组成一个腹肌之外。学习的奔头一直保持,无论是工作里还是工作外都会刻意进行某方面的学习,无论是业务还是技术,客户需求还是技术追踪。怎么就不行、不能、没有!


可能性的一个原因,社会上的一些职位相对能力,精力更加重要,没有家庭的人即使能力差点,但是精力可以补缺。的确,笔者几年前已经往保温杯投放枸杞了。每一年,笔者都会被拉入到某一个群里面,某位同学脑出血了,某位老师肾出毛病了,需要同学的帮助。担心自己某一天也会这样,笔者的脸皮薄,真的遇上大病大难也是想从自我解决,而不是寻求网民的捐赠,那种感觉像是求人施舍。


自我解决是我一厢情愿的想法,如果真的遇上大病大难,可能也会屈下膝盖寻找外界力量帮助吧,毕竟。。。。。 我已经过了更年期了。


关于求职失败


我投了600多份简历,只有6家单位约我面试,求职600个单位,6家面试单位失败,依然失业中。


有人对我说,不是我的能力不行,而是大环境不好,地产行业崩了!间接影响其它行业,同时世界经济也不好,大潮影响着一波又一波的人。


但是我的想法,不是有6家单位约我面试,为什么没有抓住机会, 从这点上来看,也是我的能力不够好,没有做好准备。求职失败,不是环境的问题,而是我个人的问题,更有助于下次的面试吧。复盘6家单位的面试过程: A单位、B单位、C单位、D单位、E单位、F单位。


A单位性质是外包,前面笔者投了400多个简历,终于有一份面试的机会了,笔者激动万分。笔者简述一下,由于笔者鞍前马后,售前、售中、售后都有做过,有人建议我把简历拆成两个,分别是 售前和项目实施,事实上笔者的工作是两份都交替参与。拆成两个的好处是,简历更容易筛选通过。


我接受了这个建议,把简历拆了以售前的经历得到一个面试的机会,在面试的过程中,突然脑袋进水了,我是售前应该以售前的角度来说话的,不应该是技术实施。说话的时候,有诸多不确定性,结果二轮的时候给筛选了。


B单位也是外包,这个没有话说了,甲方对于年龄有区别对待,面试完B单位后,我的总结是我的能力是大多数,应该往少数的方向上开拓。


C单位是一家互联网公司,对方问了关于数据库监控方案、批量数据库管理、数据库内存优化等方面的问题,回答不够完美。针对一个问题,至少有3个点的输出。


D单位是一家大公司,严格来说我是通过了,对方HR说一个月的时间入职,结果一个月后,说我的入职卡在预算上,不知道1月份还是2月份是不是新的预算。


E单位是外包,作为乙方入驻甲方场地,甲方没有问我很深的技术问题或者详细的业务问题 ,对答方面没有很大的失误。我总结是没有把我的项目经历形成数字化表达,更好的输出自己。


F单位是外包,对方是大厂的资深DBA,问了我mysql7到 mysql8的区别,主要是挖根从深处出发的, 正好是我的薄弱的地方。


根据面试中遇到的问题,反思自己的不足之处,再加强加强,2024重新找新的机会。


关于职业发展


除了IT,我还能做什么? 快递?外卖?网约车 ?


我还尝试换了一下赛道,朋友介绍一份工作与研学活动有关,我去和老板深入交流了一下,工作性质前期有很多导游的工作。


想到这个要耗费我大量的精力,而且我要把原来的都要抛弃掉,拿的是一份微薄的工资,笔者再三思考,还是拒绝了。


至于快递,外卖以及网约车,更是不可能!这种一眼看到头的事业,纯粹与体力和坚持有关,好歹笔者也是一个接受过九年义务高等教育的正儿八经的大专生,就不要和人家抢生意。


除非到了山穷水尽那一步,否则我是不会换跑道的。那么我在IT这个领域细分干啥 的? 国产数据库!


我在一家国产数据库跑道上跑了好几年,现在登记注册的国产数据库厂商大小有288家,我了解数据库的原理、概念、应用实践,我围绕数据库售前、售中、售后做了好几年,写过文章,写过视频。


燕过留声,人过留名,一直想写一篇国产数据库相关的书,除了给个人利益之外,另外等儿子长大了,我可以说,你老爸虽然在社会没有什么大名气,但是也是写了一本书出来。


可能每个人的社会等级不同,有些人是校长,有些人是教授,有些人是老师,他们的都是离不开讲台,站在上面教书育人。销售能力分高低,但是销售经理和销售员的相性都是共通的,他们都是销售。 我是一名工程师,工程师能够创造,除了本职工作,也可以选择下笔写书,还可以搞搞自媒体,写写公众号,做做测试实验,搞搞课程,没有必要和司机、外卖他们去抢饭碗。


关于失业修行


失业犹如落水的人,不停在水里挣扎,期望有一天可以上岸重回职场,因为我还在水里,所以我没有资格教说用什么样的方法上岸。临近年底,职位更加空缺,我也没有投简历了。


有一段晚上总是睡不着,感觉工作是世界的全部,整个世界都支离破碎了,这个时间看了一本《相信》的书,京东前副总裁渐冻人蔡磊所著,到了生命最后不多的时刻,他依然顽强向命运战斗,阅后幡然醒悟。我只是小小的失业!在无迹的生活我给自己制定科学、自律、写作、思考、运动的基本定律。


科学


科学是对自己专业、行业、产品、技术的长、宽、高的认识。 这是一个很广泛的范围,我圈了三个点,三个点代表三个标签,分别是国产数据库、数据治理、数据化转型,这些都是信息行业与国计民生相关的,给自己制造了以下话题。



  • 当今主流的数据库使用操作及技术原理

  • 国产数据库与主流数据库的使用差距

  • 国际主流数据库技术及最新产品

  • 金融与电信的业务应用场景细分

  • OceanBase的CCIE认证


关于当今主流的数据库使用操作及技术原理国产数据库与主流数据库的使用差距,必须要通过实践认识才能找出两者的规律,否则会带有很多以偏概全的观点。这个意味着我要从输入端、输出端编写模拟自动化程序,保障问题 复现并反复去验证。


关于国际主流数据库技术及最新产品,最主流的技术依然是美国马首是瞻,很多东西要向老美去学习。卡内基梅隆大学的数据库课程就很不错。


金融与电信的业务应用场景细分 国产数据库的来源于信创,同样也有业务上的驱动,市场 上与数据库紧密有关的是金融行业和电信行业,了解客户的诉求、痛点极有帮助。


OceanBase的CCIE认证 个人认为这个证书未来对就业大有帮助


自律



  • 每天早上7点半之前起床,中午睡觉只睡半个小时,每天晚上1点前要睡觉。

  • 晚上不吃宵夜

  • 一个星期要自己亲自做一次菜。

  • 每周亲自安排带小孩出外做一次研学活动


写作



  • 基于经济驱动的写作

  • 基于实验测试的写作

  • 基于写书的写作

  • 基于目的总结的写作


思考



  • 世界与行业

  • 大脑与世界

  • 自我执行力监测


运动



  • 每天坚持锻炼半个小时


最后


一个家庭是什么样子的,就看父母是什么样的,父母是什么样的,就决定小孩是什么样子。为了这两个小可爱,我会努力向阳的方向生长,给他们做出一个榜样。


image.png


作者:angryart
来源:juejin.cn/post/7313760536713019402
收起阅读 »

云音乐自研客户端UI自动化项目 - Athena

背景 网易云音乐是一款大型的音乐平台App,除了音乐业务外,还承接了直播、K歌、mlog、长音频等业务。整体的P0、P1级别的测试用例多达 3000 多个,在现代互联网敏捷高频迭代的情况下,留给测试回归的时间比较有限,云音乐目前采用双周迭代的模式,具体如下图所...
继续阅读 »





背景


网易云音乐是一款大型的音乐平台App,除了音乐业务外,还承接了直播、K歌、mlog、长音频等业务。整体的P0、P1级别的测试用例多达 3000 多个,在现代互联网敏捷高频迭代的情况下,留给测试回归的时间比较有限,云音乐目前采用双周迭代的模式,具体如下图所示:


image


每个迭代仅给测试留 1.5 天的回归测试时间,在此背景下,云音乐采用了一种折中的方式,即挑选一些核心链路的核心场景进行回归测试,不做全量回归。这样的做法实际是舍弃了一些线上质量为代价,这也导致时不时的会有些低级的错误带到线上。


在这样的背景下我们的测试团队也尝试了一些业内的UI自动化框架,但是整体的执行结果离我们的预期差距较大,主要体现在用例录入成本、用例稳定性、执行效率、执行成功率等维度上,为此我们希望结合云音乐的业务和迭代特点,并参考业内框架的优缺点设计一套符合云音乐的自动化测试框架。


核心关注点


接下来我们来看下目前自动化测试主要关心点:



  • 用例录入成本


即用例的生成效率,因为用例的基数比较庞大,并且可预见的是未来用例一定会一直膨胀,所以对于用例录入成本是我们非常关注的点。目前业内的自动化测试框架主要有如下几种方式:



  1. 高级或脚本语言



高级或脚本语言在使用门槛上过高,需要用例录入同学有较好的语言功底,几乎每一条用例都是一个程序,即使是一位对语言相对熟悉的测试同学,每日的生产用例条数也都会比较有限;




  1. 自然语言


场景: 验证点击--点击屏幕位置
当 启动APP[云音乐]
而且 点击屏幕位置[580,1200]
而且 等待[5]
那么 全屏截图
那么 关闭App


如上这段即为一个自然语言描述的例子,自然语言在一定程度上降低了编程门槛,但是自然语言仍然避免不了程序开发调试的过程,所以在效率仍然比较低下;




  1. ide 工具等



AirTest 则提供了ide工具,利用拖拽的能力降低了元素查找的编写难度,但是仍然避免不了代码编写的过程,而且增加了环境安装、设备准备、兼容调试等也增加了一些额外的负担。




  1. 操作即用例



完全摒弃手写代码的形式,用所见操作即所得的用例录制方式。此方式没有编程的能力要求,而且录入效率远超其他三种方式,这样的话即可利用测试外包同学快速的将用例进行录入。目前业内开源的solopi即采用此方式。



如上分析,在用例录入维度,也只有录制回放的形式是能满足云音乐的诉求。



  • 用例执行稳定性


即经过版本迭代后,在用例逻辑和路径没有发生变化的情况下,用例仍然能稳定执行。


理论上元素的布局层次或者位置发生变化都不应该影响到用例执行,特别是一些复杂的核心场景,布局层次和位置是经常发生变化的,如果导致相关路径上的用例执行都不再稳定,这将是一场灾难(所有受到影响的用例都将重新录入或者编辑,在人力成本上将是巨大的)。


这个问题目前在业内没有一套通用的行之有效的解决方案,在Android 侧一般在写UI界面时每个元素都会设置一个id,所以在Android侧可以依据这个id进行元素的精准定位;但是iOS 在写UI时不会设置唯一id,所以在iOS侧相对通用的是通过xpath的方式去定位元素,基于xpath就会受到布局层次和位置变化的影响。



  • 用例执行效率


即用例完整执行的耗时,这里耗时主要体现在两方面:



  1. 用例中指令传输效率



业内部分自动化框架基于webdriver驱动的c/s模型,传输和执行上都是以指令粒度来的,所以这类方式的网络传输的影响就会被放大,导致整体效率较低;




  1. 用例中元素定位的效率



相当一部分框架是采用的黑盒方式,这样得通过跨进程的方式dump整个页面,然后进行遍历查找;



用例执行效率直接决定了在迭代周期内花费在用例回归上的时间长短,如果能做到小时级别回归,那么所有版本(灰度、hotfix等)均能在上线前走一遍用例回归,对线上版本质量将会有较大帮助。



  • 用例覆盖度


即自动化测试框架能覆盖的测试用例的比例,这个主要取决于框架能力的覆盖范围和用例的性质。比如在视频播放场景会有视频进度拖拽的交互,如果框架不具备拖拽能力,这类用例就无法覆盖。还有些用例天然不能被自动化覆盖,比如一些动画场景,需要观察动画的流畅度,以及动画效果。


自动化框架对用例的覆盖度直接影响了人力的投入,如果覆盖度偏低的话,没法覆盖的用例还是得靠人工去兜底,成本还是很高。所以在UI自动化框架需要能覆盖的场景多,这样才能有比较好的收益,业内目前优秀的能做到70%左右的覆盖度。



  • 执行成功率


即用例执行成功的百分比,主要有两方面因素:



  1. 单次执行用例是因为用例发生变化导致失败,也就是发现了问题;

  2. 因为一些系统或者环境的因素,在用例未发生改变的情况下,用例执行失败;


所以一个框架理想的情况下应该是除了用例发生变化导致的执行失败外,其他的用例应该都执行成功,这样人为去验证失败用例的成本就会比较低。


业内主流框架对比


在分析了自动化框架需要满足的这些核心指标后,对比了业内主流的自动化测试框架,整体如下:


维度UIAutomatorXCUITestAppiumSmartAutoAirTestSolopi
录入成本使用Java编写用例,门槛高使用OC语言编写,门槛高使用python/java编写用例,门槛高,且调试时间长自然语言编写,但是理解难度和调试成本仍然高基于ide+代码门槛高操作即用例,成本低
执行稳定性较高一般一般一般一般较高
执行效率较高较高一般一般一般较高
系统支持单端(安卓)单端(iOS)单端(安卓)

注:因用例覆盖度和执行成功率不光和自动化框架本身能力相关,还关联到配套能力的完善度(接口mock能力,测试账号等),所以没有作为框架的对比维度


整体对比下来,没有任何一款自动框架能满足我们业务的诉求。所以我们不得不走上自研的道路。


解决思路


再次回到核心的指标上来:


用例录入成本:我们可以借鉴solopi的方式(操作即用例),Android已经有了现成的方案,只需要我们解决iOS端的录制回放能力即可。


用例执行稳定性:因为云音乐有曙光埋点(自研的一套多端统一的埋点方案),核心的元素都会绑定双端统一的点位,所以可以基于此去做元素定位,在有曙光点的情况下使用曙光点,如果没有曙光点安卓则降级到元素唯一id去定位,iOS则降级到xpath。这样即可以保证用例的稳定性,同时在用例都有曙光点的情况下,双端的用例可以达到复用的效果(定义统一的用例描述格式即可)。


用例执行效率:因为可以采用曙光点,所以在元素定位上只要我们采用白盒的方式,即可实现元素高效的定位。另外对于网络传输问题,我们采用以用例粒度来进行网络传输(即接口会一次性将一条完整的用例下发到调度机),即可解决指令维度传输导致的效率问题。


用例覆盖度&执行成功率:在框架能力之余,我们需要支持很多的周边能力,比如首页是个性化推荐,对于这类场景我们需要有相应的网络mock能力。一些用例会关联到账号等级,所以多账号系统支持也需要有。为了方便这些能力,我们在用例的定义上增加了前置条件和后置动作和用例进行绑定。这样在执行一些特定用例时,可以自动的去准备执行环境。


在分析了这些能力都可以支持之后,我们梳理了云音乐所有的用例,评估出来我们做完这些,是可以达到70%的用例覆盖,为此云音乐的测试团队和大前端团队合作一起立了自动化测试项目- Athena


设计方案


用例双端复用,易读可编辑


首先为了达到双端用例可复用,设计一套双端通用的用例格式,同时为了用例方便二次编辑,提升其可读性,我们采用json的格式去定义用例。
eg:


image


Android端设计


因为 Solopi 有较好的录制回放能力,并且有完整的基于元素id定位元素的能力,所以这部分我们不打算重复造轮子,而是直接拿来主义,基于 Solopi 工程进行二次开发,集成曙光相关逻辑,并且支持周边相关能力建设即可。因为 Solopi 主要依赖页面信息,基于 Accessibility 完全能满足相关诉求,所以 Solopi 是一个黑盒的方案,我们考虑到曙光相关信息透传,以及周边能力信息透传,所以我们采用了白盒的方式,在 app 内部会集成一个 sdk,这个 sdk 负责和独立的测试框架 app 进行通讯。
架构图如下:
image


iOS 端设计


iOS 在业内没有基于录制回放的自动化框架,并且其他的框架与我们的目标差距均较大,所以在 iOS 侧,我们是从 0 开始搭建一整套框架。其中主要的难点是录制回放的能力,在录制时,对于点击、双击、长按、滑动分别 hook 的相关 api 方法,对于键盘输入,因为不在 app 进程,所以只能通过交互工具手动记录。在回放时,基于 UIEvent 的一些私有 api 方法实现 UI 组件的操作执行。


在架构设计上,iOS 直接采用 sdk 集成进测试 app 的白盒形式,这样各种数据方便获取。同时在本地会起一个服务用于和平台通讯,同时处理和内嵌 sdk 的指令下发工作。


image


双端执行流程


整体的录制流程如下:


image


回放流程:


image


录制回放效果演示:



接口mock能力


对于个性推荐结果的不确定性、验证内容的多样性,我们打通了契约平台(接口 mock 平台),实现了接口参数级别的方法 mock,精准配置返回结果,将各个类型场景一网打尽。主要步骤为,在契约平台先根据要 mock 的接口配置相应参数和返回结果,产生信息二维码,再用客户端扫码后将该接口代表,在该接口请求时会在请求头中添加几个自定义的字段,网关截获这些请求后,先识别自定义字段是否有 mock 协议,若有,则直接导流到契约平台返回配置结果。


mock 方案:


image


平台


saturn 平台作为自动化操作的平台,将所有和技术操作、代码调度的功能均在后台包装实现,呈现给用户的统一为交互式操作平台的前端。包括用例创建更改、执行机创建编辑、执行机执行、自定义设备、定时执行任务等功能;


image


image


问题用例分析效率


在用例执行时,我们会记录下相应操作的截图、操作日志以及操作视频为执行失败的用例提供现场信息。通过这些现场信息,排查问题简单之极,提缺陷也极具说服力,同时在问题分析效率上也极高。


image


私有化云机房建设


云音乐通过参考 android 的 stf、open-atx-server 等开源工程,结合自身业务特点,实现了即可在云端创建分发任务、又即插即用将设备随时变为机房设备池设备的平台,对 android 和 iOS 双端系统都支持云端操作,且具备去中心化的私有化部署能力。


image


私有化机器池:


image


整体架构


image


落地情况


在框架侧,我们的录入效率对比如下:


image


用例执行效率:


image


目前在云音乐中,已经对客户端 P0 场景的用例进行覆盖,并且整体覆盖率已经达到 73%。双端的执行成功率超过 90%。


具体覆盖情况:


image


具体召回的用例情况:


image


对于迭代周期中,之前 1.5天 大概投入 15人日 进行用例归回,现在花 0.5天,投入约 6人日,提效超过 60%


现在 Athena 不光用在云音乐业务用例回归,在云音乐的其他业务中也在推广使用。


总结


本文介绍了云音乐在UI自动化测试上的一站式解决方案,采用录制的方式解决录制门槛高、效率低下的问题,在回放过程中前置准备用例执行环境以及结合曙光埋点提升用例执行的稳定性,并且会保留执行过程中的现场信息以便后续溯因。最后通过私有云部署,在云端即可统一调度Android和iOS设备来执行任务。目前该套方案在云音乐所有业务线均已覆盖,我们未来会在自动化测试方面继续探索和演进,争取积累更多的经验与大家交流分享。


作者:网易云音乐技术团队
来源:juejin.cn/post/7313501001788964898
收起阅读 »

把Fragment变成Composable踩坑

把Fragment变成Composable踩坑 Why 在编写Compose时候如果遇到需要加载其他Fragment就比较麻烦,而且很多时候这种Fragment还是xml或者第三方SDK提供的。下面提供一些解决方案。 Option 1 google也意识到这个...
继续阅读 »

把Fragment变成Composable踩坑


Why


在编写Compose时候如果遇到需要加载其他Fragment就比较麻烦,而且很多时候这种Fragment还是xml或者第三方SDK提供的。下面提供一些解决方案。


Option 1


google也意识到这个问题,所以提供了AndroidViewBinding,可以把Fragment通过包装成AndroidView,就可以在Composable中随意使用了。AndroidViewBinding在组合项退出组合时会移除 fragment。


官方文档:Compose 中的 fragment


//源码
@Composable
fun <T : ViewBinding> AndroidViewBinding(
factory: (inflater: LayoutInflater, parent: ViewGr0up, attachToParent: Boolean) -> T,
modifier: Modifier = Modifier,
update: T.() -> Unit = {} //view inflate 完成时候回调
)
{ ...


  • 首先需要添加ui-viewbinding依赖,并且开启viewBinding


// gradle
buildFeatures {
...
viewBinding true
}
...
implementation("androidx.compose.ui:ui-viewbinding")


  • 创建xml布局,在android:name="MyFragment"添加Fragment的名字和包名路径


<androidx.fragment.app.FragmentContainerView
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/fragment_container_view"
  android:layout_height="match_parent"
  android:layout_width="match_parent"
  android:name="com.example.MyFragment" />



  • 在Composable函数中如下调用,如果您需要在同一布局中使用多个 fragment,请确保您已为每个 FragmentContainerView 定义唯一 ID。


@Composable
fun FragmentInComposeExample() {
    AndroidViewBinding(MyFragmentLayoutBinding::inflate) {
        val myFragment = fragmentContainerView.getFragment<MyFragment>()
        // ...
    }
}


这种方式默认支持空构造函数的Fragment,如果是带有参数或者需要arguments传递数据的,需要改造成调用方法传递或者callbak方式,官方建议使用FragmentFactory。



class MyFragmentFactory extends FragmentFactory {
@NonNull
@Override
public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) {
Class extends Fragment> clazz = loadFragmentClass(classLoader, className);
if (clazz == MainFragment.class) {
//这次处理传递参数
return new MainFragment(anyArg1, anyArg2);
} else {
return super.instantiate(classLoader, className);
}
}
}

//使用
getSupportFragmentManager().setFragmentFactory(fragmentFactory)

请参考此文:FragmentFactory :功能详解&使用场景


Option 2


如果我们可以new Fragment或者有fragment实例,如何加载到Composable中呢。


思路:fragmentManager把framgnt add之后,fragment自己getView,然后包装成AndroidView即可。修改下AndroidViewBinding源码就可以得到如下代码:


@Composable
fun FragmentComposable(
fragment: Fragment,
modifier: Modifier = Modifier,
update: (Fragment) -> Unit = {}
)
{
val fragmentTag = remember { mutableStateOf(fragment.javaClass.name) }
val localContext = LocalContext.current

AndroidView(
modifier = modifier,
factory = { context ->
require(!fragment.isAdded) { "fragment must not attach to any host" }
(localContext as? FragmentActivity)?.supportFragmentManager
?.beginTransaction()
?.setReorderingAllowed(true)
?.add(fragment, fragmentTag.value)
?.commitNowAllowingStateLoss()
fragment.requireView()
},
update = { update(fragment) }
)

DisposableEffect(localContext) {
val fragmentManager = (localContext as? FragmentActivity)?.supportFragmentManager
val existingFragment = fragmentManager?.findFragmentByTag(fragmentTag.value)
onDispose {
if (existingFragment != null && !fragmentManager.isStateSaved) {
// If the state isn't saved, that means that some state change
// has removed this Composable from the hierarchy
fragmentManager
.beginTransaction()
.remove(existingFragment)
.commitAllowingStateLoss()
}
}
}
}

Issue Note


其实里面有个巨坑。如果你的Fragment中还通过fragmentManager进行了navigation的实现,你会发现你的其他Fragment生命周期会异常,返回了却onDestoryView,onDestory不回调。



  • 方案1中 官方建议把所有的子Fragment通过childFragmentManager来加载,这样子Fragment依赖与父对象,当父亲被回退出去后,子类Fragment全部自动销毁了,会正常被childFragmentManager处理生命周期。

  • 方案1中 Fragment嵌套需要用FragmentContainerView来包装持有。下面是源码解析,只保留了核心处理的地方


@Composable
fun <T : ViewBinding> AndroidViewBinding(
factory: (inflater: LayoutInflater, parent: ViewGr0up, attachToParent: Boolean) -> T,
modifier: Modifier = Modifier,
update: T.() -> Unit = {}
)
{
// fragmentContainerView的集合
val fragmentContainerViews = remember { mutableStateListOf<FragmentContainerView>() }
val viewBlock: (Context) -> View = remember(localView) {
{ context ->
...
val viewBinding = ...
fragmentContainerViews.clear()
val rootGr0up = viewBinding.root as? ViewGr0up
if (rootGr0up != null) {
//递归找到 并且加入集合
findFragmentContainerViews(rootGr0up, fragmentContainerViews)
}
viewBinding.root
}
}

...
//遍历所有找到View每个都注册一个 DisposableEffect用来处理销毁
fragmentContainerViews.fastForEach { container ->
DisposableEffect(localContext, container) {
// Find the right FragmentManager
val fragmentManager = parentFragment?.childFragmentManager
?: (localContext as? FragmentActivity)?.supportFragmentManager
// Now find the fragment inflated via the FragmentContainerView
val existingFragment = fragmentManager?.findFragmentById(container.id)
onDispose {
if (existingFragment != null && !fragmentManager.isStateSaved) {
// If the state isn't saved, that means that some state change
// has removed this Composable from the hierarchy
fragmentManager.commit {
remove(existingFragment)
}
}
}
}
}
}

思考和完善


很多时候我们的业务很复杂改动Fragment的导航方式成本很高,如何无缝兼容呢。于是有了如下思考



  • 加载这个Composable Fragment之前可能还有Fragment加载和导航,需要单独的FragmentManager
    val parentFragment = remember(localView) {
    try {
    // 需要依赖 implementation "androidx.fragment:fragment-ktx:1.6.2"
    localView.findFragment<Fragment>().takeIf { it.isAdded }
    } catch (e: IllegalStateException) {
    // findFragment throws if no parent fragment is found
    null
    }
    }
    val localContext = LocalContext.current
    //如果有还有父Fragment就使用childFragmentManager,
    //如果没有说明是第一个Fragment用supportFragmentManager
    val fragmentManager = parentFragment?.childFragmentManager
    ?: (localContext as? FragmentActivity)?.supportFragmentManager
    //加载Composable Fragment
    val fragment = ...
    fragmentManager
    ?.beginTransaction()
    ?.setReorderingAllowed(true)
    ?.add(id, fragment, fragment.javaClass.name)
    ?.commitAllowingStateLoss()


  • 子Fragment若用parentFragment childFragmentManager管理,不需要额外处理

  • 子Fragment若用parentFragment fragmentManager管理,需要监听的出入堆栈,在Composable销毁时候处理所有堆栈中的子fragment
    val attachListener = remember {
    FragmentOnAttachListener { _, fragment ->
    Log.d("FragmentComposable", "fragment: $fragment")
    }
    }
    fragmentManager?.addFragmentOnAttachListener(attachListener)


  • 实际操作中parentFragmentManager实现的子Fragment导航,中间会发生popback,如何防止出栈的Fragment出现内存泄露问题
    val fragments = remember { mutableListOf<WeakReference<Fragment>>() }
    FragmentOnAttachListener { _, fragment ->
    Log.d("FragmentComposable", "fragment: $fragment")
    fragments += WeakReference(fragment)
    }


  • 实际操作中 beginTransaction().remove(childFragment)只会执行子fragment的onDestoryView方法,onDestory不触发,原来是加载子fragment用了addToBackStack,需要调用popBackStack
    DisposableEffect(localContext) {
    val fragmentManager = ...
    onDispose {
    //回退栈到AndroidView的Fragment
    fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
    }
    }



Final Option


import android.widget.FrameLayout
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.viewinterop.AndroidView
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentOnAttachListener
import androidx.fragment.app.findFragment
import java.lang.ref.WeakReference

/**
* Make fragment as Composable by AndroidView
*
* @param fragment fragment
* @param fm add fragment by FragmentManager, can be childFragmentManager
* @param update The callback to be invoked after the layout is inflated.
*/

@Composable
fun <T : Fragment> FragmentComposable(
modifier: Modifier = Modifier,
fragment: T,
update: (T) -> Unit = {}
)
{
val localView = LocalView.current
// Find the parent fragment, if one exists. This will let us ensure that
// fragments inflated via a FragmentContainerView are properly nested
// (which, in turn, allows the fragments to properly save/restore their state)
val parentFragment = remember(localView) {
try {
localView.findFragment<Fragment>().takeIf { it.isAdded }
} catch (e: IllegalStateException) {
// findFragment throws if no parent fragment is found
null
}
}

val fragments = remember { mutableListOf<WeakReference<Fragment>>() }

val attachListener = remember {
FragmentOnAttachListener { _, fragment ->
Log.d("FragmentComposable", "fragment: $fragment")
fragments += WeakReference(fragment)
}
}

val localContext = LocalContext.current

DisposableEffect(localContext) {
val fragmentManager = parentFragment?.childFragmentManager
?: (localContext as? FragmentActivity)?.supportFragmentManager
fragmentManager?.addFragmentOnAttachListener(attachListener)

onDispose {
fragmentManager?.removeFragmentOnAttachListener(attachListener)
if (fragmentManager?.isStateSaved == false) {
fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
fragments
.filter { it.get()?.isRemoving == false }
.reversed()
.forEach { existingFragment ->
Log.d("FragmentComposable", "remove:${existingFragment.get()}")
fragmentManager
.beginTransaction()
.remove(existingFragment.get()!!)
.commitAllowingStateLoss()
}
}
}
}

AndroidView(
modifier = modifier,
factory = { context ->
FrameLayout(context).apply {
id = System.currentTimeMillis().toInt()
require(!fragment.isAdded) { "$fragment must not attach to any host" }
val fragmentManager = parentFragment?.childFragmentManager
?: (localContext as? FragmentActivity)?.supportFragmentManager
fragmentManager
?.beginTransaction()
?.setReorderingAllowed(true)
?.replace(this.id, fragment, fragment.javaClass.name)
?.commitAllowingStateLoss()
fragments.clear()
}
},
update = { update(fragment) }
)
}

注意事项



  • 使用上面的代码加载的Fragment(父),若里面导航子Fragment,必须使用parentFragment一样fragmentManager 或者 parentFragment的childFragmentManager

  • 如果子Fragment使用了FragmentActivity?.supportFragmentManager,而parentFragment.fragmentManager不是这个,就会导致子Fragment的生命周期异常。


转载声明


未授权禁止转载和二次修改发布(最近发现有人搬运我的文章,并且改为自己原创,脸都不要了。)如果上面的代码有Bug,请在评论区留言。


作者:forJrking
来源:juejin.cn/post/7312266765123272744
收起阅读 »

Android TextView中那些冷门好用的用法

介绍 TextView 是 Android 开发中最常用的小部件之一。它用于在屏幕上显示文本。但是,TextView 有几个较少为人知的功能,对开发人员非常有用。在本博客文章中,我们将探讨其中的一些功能。 自定义字体 默认情况下,TextView 使用系统字体...
继续阅读 »

介绍


TextView 是 Android 开发中最常用的小部件之一。它用于在屏幕上显示文本。但是,TextView 有几个较少为人知的功能,对开发人员非常有用。在本博客文章中,我们将探讨其中的一些功能。


自定义字体


默认情况下,TextView 使用系统字体显示文本。但其实我们也可以导入我们自己的字体文件在 TextView 中使用自定义字体。这可以通过将字体文件添加到资源文件夹(res/font 或者 assets)并在 TextView 上以编程方式设置来实现。


要使用自定义字体,我们需要下载字体文件(或者自己生成)并将其添加到资源文件夹中。然后,我们可以使用setTypeface()方法在TextView上以编程方式设置字体。我们还可以在 XML 中使用android:fontFamily或者android:typeface属性设置字体。需要注意的是,在 XML 中使用 typeface 的方式只能使用系统预设的字体并且仅对英文字符有效,如果TextView的文本内容是中文的话这个属性设置后将不会有任何效果。


以下是 Android TextView 自定义字体的代码示例:



  1. 将字体文件添加到 assets 或 res/font 文件夹中。

  2. 通过以下代码设置字体:


// 字体文件放到 assets 文件夹的情况
Typeface tf = Typeface.createFromAsset(getAssets(), "fonts/myfont.ttf");
TextView tv = findViewById(R.id.tv);
tv.setTypeface(tf);

// 字体文件放到 res/font 文件夹的情况, 需注意的是此方式在部分低于 Android 8.0 的设备上可能会存在兼容性问题
val tv = findViewById<TextView>(R.id.tv)
val typeface = ResourcesCompat.getFont(this, R.font.myfont)
tv.typeface = typeface

在上面的示例中,我们首先从 assets 文件夹中创建了一个新的 Typeface 对象。然后,我们使用 setTypeface() 方法将该对象设置为 TextView 的字体。


在上面的示例中,我们将字体文件命名为 “myfont.ttf”。我们可以将其替换为要使用的任何字体文件的名称。


自定义字体是 TextView 的强大功能之一,它可以帮助我们创建具有独特外观和感觉的应用程序。另外,我们也可以通过这种方法实现自定义图标的绘制。


AutoLink


AutoLink 可以自动检测文本中的模式并将其转换为可点击的链接。例如,如果 TextView 包含电子邮件地址或 URL,则 AutoLink 将识别它并使其可点击。此功能使开发人员无需手动创建文本中的可点击链接。


要在 TextView 上启用 AutoLink,您需要将autoLink属性设置为emailphoneweball。您还可以使用Linkify类设置自定义链接模式。


以下是一个Android TextView AutoLink代码使用示例:


<TextView
android:id="@+id/tv3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="web"
android:textColorLink="@android:color/holo_red_dark"
android:text="这是我的个人博客地址: http://www.geektang.cn" />


在上面的示例中,我们将 autoLink 属性设置为 web ,这意味着 TextView 将自动检测文本中的 URL 并将其转换为可点击的链接。我们还将 text 属性将文本设置为 这是我的个人博客地址: http://www.geektang.cn 。当用户单击链接时,它们将被带到 http://www.geektang.cn 网站。另外,我们也可以通过 textColorLink 属性将 Link 颜色为我们喜欢的颜色。


AutoLink是一个非常有用的功能,它可以帮助您更轻松地创建可交互的文本。


对齐模式


对齐模式允许您通过在单词之间添加空格将文本对齐到左右边距,这使得文本更易读且视觉上更具吸引力。您可以将对齐模式属性设置为 inter_wordinter_character


要使用对齐模式功能,您需要在 TextView 上设置 justificationMode 属性。但是,此功能仅适用于运行 Android 8.0(API 级别 26)或更高版本的设备。


以下是对齐模式功能的代码示例:


<TextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This is some sample text that will be justified."
android:justificationMode="inter_word"/>

在上面的示例中,我们将 justificationMode 属性设置为 inter_word 。这意味着 TextView 将在单词之间添加空格,以便将文本对齐到左右边距。


以下是对齐模式功能的显示效果示例:


image.png
同样一段文本,上面的设置 justificationMode 为 inter_word ,是不是看起来会比下面的好看一些呢?这个属性一般用于多行英文文本,如果只有一行文本或者文本内容是纯中文字符的话,不会有任何效果。


作者:GeekTR
来源:juejin.cn/post/7217082232937283645
收起阅读 »

Swift Rust Kotlin 三种语言的枚举类型比较

本文比较一下 Swift Rust Kotlin 三种语言中枚举用法的异同。 定义比较 基本定义 Swift: enum LanguageType { case rust case swift case kotlin } Rus...
继续阅读 »

本文比较一下 Swift Rust Kotlin 三种语言中枚举用法的异同。


定义比较


基本定义



  • Swift:


enum LanguageType {
case rust
case swift
case kotlin
}


  • Rust:


enum LanguageType {
Rust,
Swift,
Kotlin,
}


  • Kotlin:


enum class LanguageType {
Rust,
Swift,
Kotlin,
}

三种语言都使用 enum 作为关键字,Swift 枚举的选项叫 case 一般以小写开头,Rust 枚举的选项叫 variant 通常以大写开头,Kotlin 枚举的选项叫 entry 以大写开头。kotlin 还多了一个 class 关键字,表示枚举类,具有类的一些特殊特性。在定义选项时,Swift 需要一个 case 关键字,而另外两种语言只需要逗号隔开即可。


带值的定义


三种语言的枚举在定义选项时都可以附带一些值。



  • 在 Swift 中这些值叫关联值(associated value),每个选项可以使用同一种类型,也可以使用不同类型,可以有一个值,也可以有多个值,值还可以进行命名。这些值在枚举创建时再进行赋值:


enum Language {
case rust(Int)
case swift(Int)
case kotlin(Int)
}

enum Language {
case rust(Int, String)
case swift(Int, Int, String)
case kotlin(Int)
}

enum Language {
case rust(year: Int, name: String)
case swift(year: Int, version: Int, name: String)
case kotlin(year: Int)
}

let language = Language.rust(2015)


  • 在 Rust 中这些值也叫关联值(associated value),和 Swift 类似每个选项可以定义一个或多个值,使用相同或不同类型,但不能直接进行命名,同样也是在枚举创建时赋值::


enum Language {
Rust(u16),
Swift(u16),
Kotlin(u16),
}

enum Language {
Rust(u16, String),
Swift(u16, u16, String),
Kotlin(u16),
}

let language = Language::Rust(2015);

如果需要给属性命名可以将关联值定义为匿名结构体:


enum Language {
Rust { year: u16, name: String },
Swift { year: u16, version: u16, name: String },
Kotlin { year: u16 },
}


  • 在 Kotlin 中选项的值称为属性或常量,所有选项的属性类型和数量都相同(因为它们都是枚举类的实例),而且值需要在枚举定义时就提供


enum class Language(val year: Int) {
Rust(2015),
Swift(2014),
Kotlin(2016),
}

所以这些值如果是通过 var 定义的就可以修改:


enum class Language(var year: Int) {
Rust(2015),
Swift(2014),
Kotlin(2016),
}

val language = Language.Kotlin
language.year = 2011

范型定义


Swift 和 Rust 定义枚举时可以使用范型,最常见的就是可选值的定义。



  • Swift:


enum Option<T> {
case none
case some(value: T)
}


  • Rust:


enum Option<T> {
,
Some(T),
}

但是 Kotlin 不支持定义枚举时使用范型。


递归定义


Swift 和 Rust 定义枚举的选项时可以使用递归,即选项的类型是枚举自身。



  • Swift 定义带递归的选项时需要添加关键字 indirect:


enum ArithmeticExpression {
case number(Int)
indirect case addition(ArithmeticExpression, ArithmeticExpression)
indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}


  • Rust 定义带递归的选项时需要使用间接访问的方式,包括这些指针类型: Box, Rc, Arc, &&mut,因为 Rust 需要在编译时知道类型的大小,而递归枚举类型的大小在没有引用的情况下是无限的。


enum ArithmeticExpression {
Number(u8),
Addition(Box<ArithmeticExpression>, Box<ArithmeticExpression>),
Multiplication(Box<ArithmeticExpression>, Box<ArithmeticExpression>),
}

Kotlin 同样不支持递归枚举。


模式匹配


模式匹配是枚举最常见的用法,最常见的是使用 switch / match / when 语句进行模式匹配:



  • Swift:


switch language {
case .rust(let year):
print(year)
case .swift(let year):
print(year)
case .kotlin(let year):
print(year)
}


  • Rust:


match language {
Language::Rust(year) => println!("Rust was first released in {}", year),
Language::Swift(year) => println!("Swift was first released in {}", year),
Language::Kotlin(year) => println!("Kotlin was first released in {}", year),
}


  • Kotlin:


when (language) {
Language.Kotlin -> println("Kotlin")
Language.Rust -> println("Rust")
Language.Swift -> println("Swift")
}

也可以使用 if 语句进行模式匹配:



  • Swift:


if case .rust(let year) = language {
print(year)
}

// 或者使用 guard
guard case .rust(let year) = language else {
return
}
print(year)

// 或者使用 while case
var expression = ArithmeticExpression.multiplication(
ArithmeticExpression.multiplication(
ArithmeticExpression.number(1),
ArithmeticExpression.number(2)
),
ArithmeticExpression.number(3)
)
while case ArithmeticExpression.multiplication(let left, _) = expression {
if case ArithmeticExpression.number(let value) = left {
print("Multiplied \(value)")
}
expression = left
}


  • Rust:


if let Language::Rust(year) = language {
println!("Rust was first released in {}", year);
}

// 或者使用 let else 匹配
let Language::Rust(year) = language else {
return;
};
println!("Rust was first released in {}", year);

// 还可以使用 while let 匹配
let mut expression = ArithmeticExpression::Multiplication(
Box::new(ArithmeticExpression::Multiplication(
Box::new(ArithmeticExpression::Number(2)),
Box::new(ArithmeticExpression::Number(3)),
)),
Box::new(ArithmeticExpression::Number(4)),
);

while let ArithmeticExpression::Multiplication(left, _) = expression {
if let ArithmeticExpression::Number(value) = *left {
println!("Multiplied: {}", value);
}
expression = *left;
}

Kotlin 不支持使用 if 语句进行模式匹配。


枚举值集合


有时需要获取枚举的所有值。



  • Swift 枚举需要实现 CaseIterable 协议,通过 AllCases() 方法可以获取所有枚举值。同时带有关联值的枚举无法自动实现 CaseIterable 协议,需要手动实现。


enum Language: CaseIterable {
case rust
case swift
case kotlin
}

let cases = Language.AllCases()


  • Rust 没有内置获取所有枚举值的方法,需要手动实现,另外带有关联值的枚举类型提供所有枚举值可能意义不大:


enum Language {
Rust,
Swift,
Kotlin,
}

impl Language {
fn all_variants() -> Vec<Language> {
vec![
Language::Rust,
Language::Swift,
Language::Kotlin,
]
}
}


  • Kotlin 提供了 values() 方法获取所有枚举值,同时支持带属性和不带属性的:


val allEntries: Array<Language> = Language.values()

原始值


枚举类型还有一个原始值,或者叫整型表示的概念。



  • Swift 可以为枚举的每个选项提供原始值,在声明时进行指定。


Swift 枚举的原始值可以是以下几种类型:



  1. 整数类型:如 Int, UInt, Int8, UInt8 等。

  2. 浮点数类型:如 Float, Double

  3. 字符串类型:String

  4. 字符类型:Character


enum Language: Int {
case rust = 1
case swift = 2
case kotlin = 3
}

带关联值的枚举类型不能同时提供原始值,而提供原始值的枚举可以直接从原始值创建枚举实例,不过实例是可选类型:


let language: Language? = Language(rawValue: 1)

在 Swift 中,提供枚举的原始值主要有以下作用:



  1. 数据映射:原始值允许枚举与基础数据类型(如整数或字符串)直接关联。这在需要将枚举值与外部数据(例如从 API 返回的字符串)匹配时非常有用。

  2. 简化代码:使用原始值可以简化某些操作,例如从原始值初始化枚举或获取枚举的原始值,而无需编写额外的代码。

  3. 可读性和维护性:在与外部系统交互时,原始值可以提供清晰、可读的映射,使代码更容易理解和维护。



  • Rust 使用 #[repr(…)] 属性来指定枚举的整数类型表示,并为每个变体分配一个可选的整数值,带和不带关联值的枚举都可以提供整数表示。


#[repr(u32)]
enum Language {
Rust(u16) = 2015,
Swift(u16) = 2014,
Kotlin(u16) = 2016,
}

在 Rust 中,为枚举提供整数表示(整数值或整数类型标识)主要有以下作用:



  1. 与外部代码交互:整数表示允许枚举与 C 语言或其他低级语言接口,因为这些语言通常使用整数来表示枚举。

  2. 内存效率:指定整数类型可以控制枚举占用的内存大小,这对于嵌入式系统或性能敏感的应用尤为重要。

  3. 值映射:通过整数表示,可以将枚举直接映射到整数值,这在处理像协议代码或状态码等需要明确值的情况下很有用。

  4. 序列化和反序列化:整数表示简化了将枚举序列化为整数值以及从整数值反序列化回枚举的过程。



  • Kotlin 没有单独的原始值概念,因为它已经提供了属性。


方法实现


三种枚举都支持方法实现。



  • Swift


enum Language {
case rust(Int)
case swift(Int)
case kotlin(Int)

func startYear() -> Int {
switch self {
case .kotlin(let year): return year
case .rust(let year): return year
case .swift(let year): return year
}
}
}


  • Rust


enum Language {
Rust(u16),
Swift(u16),
Kotlin(u16),
}

impl Language {
fn start_year(&self) -> u16 {
match self {
Language::Rust(year) => *year,
Language::Swift(year) => *year,
Language::Kotlin(year) => *year,
}
}
}


  • Kotlin


enum class Language(var year: Int) {
Rust(2015),
Swift(2014),
Kotlin(2016);

fun yearsSinceRelease(): Int {
val year: Int = java.time.Year.now().value
return year - this.year
}
}

它们还支持对协议、接口的实现:



  • Swift


protocol Versioned {
func latestVersion() -> String
}

extension Language: Versioned {
func latestVersion() -> String {
switch self {
case .kotlin(_): return "1.9.0"
case .rust(_): return "1.74.0"
case .swift(_): return "5.9"
}
}
}


  • Rust


trait Version {
fn latest_version(&self) -> String;
}

impl Version for Language {
fn latest_version(&self) -> String {
match self {
Language::Rust(_) => "1.74.0".to_string(),
Language::Swift(_) => "5.9".to_string(),
Language::Kotlin(_) => "1.9.0".to_string(),
}
}
}


  • Kotlin


interface Versioned {
fun latestVersion(): String
}

enum class Language(val year: Int): Versioned {
Rust(2015) {
override fun latestVersion(): String {
return "1.74.0"
}
},
Swift(2014) {
override fun latestVersion(): String {
return "5.9"
}
},
Kotlin(2016) {
override fun latestVersion(): String {
return "1.9.0"
}
};
}

这三者对接口的实现还有一个区别,Swift 和 Rust 可以在枚举定义之后再实现某个接口,而 Kotlin 必须在定义时就完成对所有接口的实现。不过它们都能通过扩展增加新的方法:



  • Swift


extension Language {
func name() -> String {
switch self {
case .kotlin(_): return "Kotlin"
case .rust(_): return "Rust"
case .swift(_): return "Swift"
}
}
}


  • Rust


impl Language {
fn name(&self) -> String {
match self {
Language::Rust(_) => "Rust".to_string(),
Language::Swift(_) => "Swift".to_string(),
Language::Kotlin(_) => "Kotlin".to_string(),
}
}
}


  • Kotlin


fun Language.name(): String {
return when (this) {
Language.Kotlin -> "Kotlin"
Language.Rust -> "Rust"
Language.Swift -> "Swift"
}
}

内存大小



  • Swift 的枚举大小取决于其最大的成员和必要的标签空间。若包含关联值,枚举的大小会增加以容纳这些值。可以使用 MemoryLayout 来估算大小。


enum Language {
case rust(Int, String)
case swift(Int, Int, String)
case kotlin(Int)
}

print(MemoryLayout<Language>.size) // 33


  • Rust 中的枚举大小通常等于其最大变体的大小加上一个用于标识变体的额外空间。使用 std::mem::size_of 来获取大小,提供了整型表示的枚举类型大小等于整型值大小加上最大变体大小,同时还要考虑内存对齐的因素。


enum Language1 {
Rust,
Swift,
Kotlin,
}
println!("{} bytes", size_of::<Language1>()); // 1 bytes

#[repr(u32)]
enum Language2 {
Rust,
Swift,
Kotlin,
}
println!("{} bytes", size_of::<Language2>()); // 4 bytes

#[repr(u32)]
enum Language3 {
Rust(u16),
Swift(u16),
Kotlin(u16),
}
println!("{} bytes", size_of::<Language3>()); // 8 bytes

对于上面例子中 Language3 的内存大小做一个简单解释:



  • 枚举的变体标识符(因为 #[repr(u32)])占用 4 字节。

  • 最大的变体是一个 u16 类型,占用 2 字节。

  • 可能还需要额外的 2 字节的填充,以确保整个枚举的内存对齐,因为它的对齐要求由最大的 u32 决定。


因此,总大小是 4(标识符)+ 2(最大变体)+ 2(填充)= 8 字节。



  • Kotlin:在 JVM 上,Kotlin 枚举的大小包括对象开销、枚举常量的数量和任何附加属性。Kotlin 本身不提供直接的内存布局查看工具,需要依赖 JVM 工具或库。


兼容性


Swift 枚举有时需要考虑与 Objective-C 的兼容,Rust 需要考虑与 C 的兼容。



  • 在 Swift 中,要使枚举兼容 Objective-C,你需要满足以下条件:



  1. 原始值类型:Swift 枚举必须有原始值类型,通常是 Int,因为 Objective-C 不支持 Swift 的关联值特性。

  2. 遵循 @objc 协议:在枚举定义前使用 @objc 关键字来标记它。这使得枚举可以在 Objective-C 代码中使用。


    @objc
    enum Language0: Int {
    case rust = 1
    case swift = 2
    case kotlin = 3

    func startYear() -> Int {
    switch self {
    case .kotlin: return 2016
    case .rust: return 2015
    case .swift: return 2014
    }
    }
    }


  3. 限制:使用 @objc 时,枚举不能包含关联值,必须是简单的值列表。


这样定义的枚举可以在 Swift 和 Objective-C 之间交互使用,适用于混合编程环境或需要在 Objective-C 项目中使用 Swift 代码的场景。



  • Rust 枚举与 C 兼容,只需要使用 #[repr(C)] 属性来指定枚举的内存布局。这样做确保枚举在内存中的表示与 C 语言中的枚举相同。


#[repr(C)]
enum Language0 {
Rust,
Swift,
Kotlin,
}

注意:在 Rust 中,你不能同时在枚举上使用 #[repr(C)]#[repr(u32)]。每个枚举只能使用一个 repr 属性来确定其底层的数据表示。如果你需要确保枚举与 C 语言兼容,并且具有特定的基础整数类型,你应该选择一个符合你需求的 repr 属性。例如,如果你想让枚举在内存中的表示与 C 语言中的 u32 类型的枚举相同,你可以使用 #[repr(u32)]



  • 在 Kotlin 中,枚举类(Enum Class)是与 Java 完全兼容的。Kotlin 枚举可以自然地在 Java 代码中使用,反之亦然。


作者:镜画者
来源:juejin.cn/post/7313589252172120098
收起阅读 »