绑定大量的的v-model,导致页面卡顿的解决方案
绑定大量的的v-model,导致页面卡顿的解决方案
设计图如下:
页面布局看着很简单使用element组件,那就完蛋了,因为是大量的数据双向绑定,所以使用组件,延迟非常高,高到什么程度,请求100条数据到渲染到页面上,要10-12s,特别是下拉选择的时候,延迟都在2-3s,人麻了老铁!!!
卡顿的原因很长一段时间都是在绑定v-model,为什么绑定v-model会很卡呢,请求到的每一条数据有14个数据需要绑定v-model,每次一请求就是100个打底,那就是1400个数据需要绑定v-model;而且组件本身也有延迟,所以这个方案不能采用,那怎么做呢?
我尝试采用原生去写,写着写着,哎解决了!!!惊呆了
做完后100条数据页面渲染不超过2s,毕竟还是需要绑定v-model,能在2s内,我还是能接受的吧;选择和输入延迟基本没有
下面就来展示一下我的代码,写的不好看着玩儿就好了:
请求到的数据:
methods这两个事件做的什么事儿呢,就是手动将数据绑定到数据上去也就是row上如图:
当然还有很多解决方案
来源:juejin.cn/post/7392248233222881316
那些年,我在职场中做过的蠢事
大家好,我是程序员马晓博,目前从事前端行业已有5年有余,而近期由于被裁员有一段时间,也开始回顾自己的过往,发现自己以前在职场中,做过不少傻事,这里就写篇文章来记录下。曾经的犯傻已不可避免,索性公之于众,坦然面对。
ps: 像一直戴工牌,故意露个工牌带子在外面就不说了,谁还没年轻过呢?还真谁也别笑话谁哈哈。
自诩正义强出头
这是我在腾讯实习的时候遇到的,故事很简单,有个同事加了一个全局错误捕获的逻辑,导致原本有报错但是能够正常运行的程序,出现了线上 bug。
此时团队间要追究责任,认为是加了全局捕获错误的同事的责任。从现在的视角看,加了全局错误捕获同事自然是有问题的,但是当时的我,非常正义的认为,写 bug 的人才应该承担责任,而为了让代码更健壮写了全局错误捕获的同事是没有错的。
现在回想起当时自己义正严辞的发言,真是太年轻啦。
更何况这事我连当事人都算不上,只能安慰自己:谁还没年轻过呢?
平易近领导
这个事,也是发生在腾讯实习期间。
在来实习之前的我,深受互联网扁平化管理,工位不做区别,这些非常先进的思想影响,到了腾讯之后,听领导说,自己平时喜欢游泳,我一想我也喜欢啊,就直接跑领导工位上问,你平时在哪里游泳?
吓的我的直属上级,直接跑出来拉住我,我给你推荐,我带你游泳。
还有一次是,午休一起打王者荣耀,人多了把我空出来了,我就指导领导玩亚瑟,上来就说你这出装太肉了,没一点伤害,要怎么怎么玩。
我现在还能想起来他当时的眼神,你,是在教我做事?
对于领导,毫无距离感。这应该是很多年轻人都会有的心态,还会认为这就是年轻人的本色,是互联网的特色,而且会对奉承领导的人嗤之以鼻。
当然,互联网公司文化本身也都提倡这种,没有上下级,大家都能上。也就是倡导所谓扁平化管理,工位也是领导普通员工没区别。
曾经的我认为这一切都是问题不大的,就是扁平化,互联网就是不一样,但是透过一些其他行业的人,我虽不介意工位情况,但是难免对其底层所宣扬的扁平化产生一定的怀疑。
ps: 这里放一个其他行业的人的对互联网工位的一些看法,我妹妹(中国移动打过一段时间工)来参观了我的工位之后说了一句话:这是工位?牛马间吧这是,我不以为然。直到我看到了她的工位,差别还真不是一般的大,好歹有隔间。这里应该放一张图,但是我没有,大家自行脑补吧哈哈。
自诩性格真诚直率
可谓是初生牛犊不怕虎吧,看到认为拉垮的代码,就会找同事当面聊,应该怎么怎么样,不应该怎么怎么写。
但是实际上,代码写成你认为的不合理的样子,往往是很多因素导致的,或工期,或对方当时也是初学者,或团队风格,或当时环境,或仅仅是对方对新方案的尝试。
上下文不了解,就开始吐槽。但是实际上人家的代码线上运行毕竟没有问题,没有故障的代码,本身就是一份合格的代码以及对方能力的认证。
而我的当面不友好交流,真是一点礼貌没有。还美名其曰,性格比较直率真诚。
好在当时的同事比较友好,并未计较,还在我后来选房子的时候还提供了很大的帮助,可叹没有仔细聆听对方的教诲终究还是没有十全十美。
夜郎自大而不自知
这是在我工作两年左右时候产生的一种感觉,觉得自己完成业务没有任何压力,而且还承担了一些比较重要的工作,从而有一种觉得自己很行的错觉。
但是当时面试很快就泼了一盆冷水,一般来讲,这个阶段做业务的同学,应对业务开发其实基本都没有什么问题。
但是国内对程序员的面试根本不限于业务,深挖一些知识点,理解其原理才是及格线。
而当时的我就是一直停留在使用阶段,用好本身没有问题,但是奈何不足以应对面试。
当然,心态还是最重要的。半瓶水晃荡而不自知才是最可怕的。
开弓来个回头箭
这个事,说实话有点羞于启齿。
是我工作大概第四个年头发生的,那时我在网易工作有一年多一些。
由于自己做了一个还算比较有技术难度的项目,想要寻求晋升,结果当时的晋升期答辩都结束了自己还不知晓。
心里有闷气,就开始面试找工作,也顺利拿到了几个涨幅非常不错的 offer。
开始跟上级提离职,哈哈,对方聊了下,也答应了。
结果我自己晚上就是睡不着,始终觉得自己这个时候走,是逃避,是逃兵。而且这个时候走,之前的积累就全部白费,新公司还得从零做起。
网上都说开弓没有回头箭。但是我就还是厚着脸皮来个回头箭。
不得不说,这个决定并不算蠢事,我在整件事里最蠢的是没有想好就和上级提了离职,虽然拿了 offer,但是没有想清楚就离职,是非常不成熟的表现。
好在我的上级,也主打一个真诚,也明确说明,想清楚了就行。
接下来一年的合作非常愉快,既有可视化埋点平台这样的业务技术都有挑战的项目,也有团队状态管理方案的产出,顺利在第二年迎来了自己的晋升。
这一次,愚蠢更多是在于自己没有想清楚就开弓,而真诚待人在我看来是双向必杀技,但真诚也为我后来吃亏埋下了种子。
整体而言,在网易的几年,领导,同事,大家都比较真诚,不屑于暗地里去做一些掉份的事情,也让我在职场上,形成了真诚而缺少防范的一个问题,这在我的下一步职业生涯中,给我带来了比较大的打击和跟头。
和同事交往讲真诚
这是我在离开网易后,选择的一家规模比较小的公司。
这时候,我工作已经整整 5年了,但是我过往的经历终究让我缺少了一些对同事的防范,大公司还好,大家相互之间,利益冲突不大,更多的是合作关系,同时由于大家或多或少都有自己的一点点的"骄傲",所以其实并没有遇到一些因为利益冲突而导致的暗箭。
而过往的经历也在告诉我,真诚,并不会带来什么问题。
真诚无错,但是说者无意听者有心。
到新公司之后,也到了该带人的职级,此时,我还是主打真诚,很快就和团队融为一片。
几个关系近的同事和下属,知道我家里买了几套房,知道我平时看的书,知道我平时都在干啥,知道我对生活和工作的态度,知道我在工作上的安排。
这些事情,平时没有什么问题,但是当和有心的同事出现利益冲突的时候,这些事情就成为一把利剑,间接导致我失去了这份工作。
而这些利剑,是我亲手递给了对方。
对职场恶意的容忍
如果说真诚是给别人递了一把利剑,那么自己的容忍和锋芒的隐藏,是我自己收起了盾牌。
我在周围的同事身上,总能看到自己的影子,所以对于他们的恶意,往往有一定程度的容忍,我觉得,年轻人嘛,有点锋芒,很正常。
比如,当他们吹嘘自己写了一篇文章,获得了几个赞的时候,我往往是进行倾听并表示赞赏,虽然几个赞的文章其实真的很简单。又或者公开场合提出质疑,虽然我会讲道理,理可以辩明,但是对于这其中的恶意,我一般会选择包容。
但是就是这一步,自身锋芒的隐藏,在对方眼里却是得寸进尺的机会。
个人觉得,作为级别比对方高的,还是需要适时的漏出自身的锋芒,而不是仅仅倾听加赞赏,同时由于私下交往的密集,更导致对方的肆无忌惮。
从而亲手递给对方利剑,又自己收起盾牌。
只能说,在这条路上,我还是太稚嫩。
最后
以上,就是我个人认为在职场中,做过的一些蠢事。虽然已经工作了五年之久,但是这条路上,还是觉得太过稚嫩,谨以此文,纪念哪些蠢事!
ps: 不知道看完这篇的你,有没有回忆起一些类似的事情呢?欢迎交流哈。
// 还是那句话,都年轻过,谁也别笑话谁~
来源:juejin.cn/post/7357994849386102836
从20k到50k再到2k,聊聊我在互联网干前端的这几年
大家好,我是程序员晓博,目前从事前端行业已经有将近 6年。这六年,从最初的互联网鼎盛时期,到今年是未来十年内最好的一年,再到疫情时期的回暖,再到如今年年都喊寒冬的寒冬。从最初的 20k,到最近的一份 50k 的工作,再到如今的 "政府补贴" 2k,可谓是感悟颇多。
刚好最近 gap 一段时间,有所空闲,就整理下这几年的经历以及我所看到的行业的兴衰。
学生见闻
我是 2011 年读的大学,当时是电子科学与技术这个专业,并非计算机科班出身,更偏向于硬件编程,单片机,嵌入式,FPGA 这些会更多一些。
所以当时对于互联网行业的前端后端,并没有特别明确的概念,也对于 c++, java 这些语言的地位和适用性其实也没有明确的认知。
记得是 2012, 2013 年的时候,学校里经常有 java 培训班的宣传,说实话,那会还看不上 java,虽然自己也不会,但是学校里教的都是 c, c++, java 那会在我看来,更多的是一个 c++ 简化后的语言,所以对于我这个非科班的并没有提起兴趣。
现在回想起来,那时可真是入行的好时期,当然也是风云变换的几年。那会学校流传着一个段子: 你只要会安装 eclipse,就能找到一份美团的工作。而之后的一年,你得开发过自己的 app,才能找到安卓开发工作。
不过当时更多的观念告诉我至少得读个研究生出来,所以我选择了读研而非直接工作。可以说是错过了互联网飞速发展的黄金时期,直接毕业就来到了今年是未来十年里最好的一年。
也就是在研究生阶段,我才慢慢了解到,外界的互联网大厂,其实已经分化出了移动端,前端,后端这样的岗位,当时的前端圈最为活跃,而移动端,后端,似乎都已经定型。而前端圈的新框架此起彼伏,从 react, vue, webpack,还有很多已经消失在历史中的框架。
在当时的就业情况下,前端的工资似乎是最高的,在加上当时的前端圈确实很活跃,而学习起来也比较简单。作为非科班的学生,自学前端上手最快,所以我选择了前端作为自己的就业方向。专业对口的硬件开发就不说了,工资实在是大相径庭。
但是话说回来,硬件开发如今的热门程度,并不亚于当时的软件开发。硬件开发培训,挑战 30w 年薪这样的培训班,在 2022 年左右也出现了,一如当时 2015 年左右 java 开发包就业那样的火热。
不过这个专业,其实还给我带来了一份现在看起来可以称为副业的东西:代写课程设计和毕业设计,因为是比互联网前后端更细分的赛道,所以竞争并不激烈,我还是接到了不少的单子,但是由于自己本身也是学生,所以定价很低,基本按照 100/h 的费用在收,也有做代码复用的整合,但是在硬件这一块,它的售后并不像软件这样,往往需要花费时间帮用户在板子上走通,这一部分是比较花费时间的。也在研究生阶段尝试过转项目的方式来获取收益,但是由于定价过低以及单子并不多的问题,而没有继续。不过如今想来,借助 chatgpt 等 AI 工具,定价确实还能更低(尤其是包含论文的单子)。
第一份实习
出于提升自身竞争力的考虑,我在研究生阶段就开始了边自学,边找实习。好在自学的时间比较早,准备的也比较充分,顺利拿到了腾讯等几家公司的实习。
当时虽说还处于互联网发展的时期,但是竞争其实就已经比较激烈了,没有实习进大厂基本就是 hard 模式,我也是面试了 n 家公司,才拿到的 offer。
不过腾讯这个部门虽然在面试的时候,会问一些比较现代技术的问题,但是实际进去后,是写的 php 和 jquery。我的收获其实并不多,但是简历上好看一点,后来也顺利拿到了转正的 offer。
当时给的薪资应该是 16k 左右,还会加上城市的补贴大概 2k。不过最终因为房价的原因,并没有考虑留在深圳。
ps:我在实习的时候专门考察了深圳腾讯总部附近的房价,好像是 6w,确认过眼神,是掏光 6 个钱包也买不起的房子。不过据说一度涨到了 10w,现在没有再关注了,可能有所下跌吧。
说起来当时还有一个事让我印象比较深刻,也因此对阿里有了一些抵触。就是 2017:众多应届生被阿里毁了 offer。而对于这种事情,阿里给的解释是:拥抱变化。
可能马爸爸从那时就嗅到了危机,但这却是我第一次听说毁应届生 offer,非常败好感。ps: 现在这种毁应届生 offer 的事是非常常见啦。
似乎那时警钟已经敲响,但是我并没有未雨绸缪。
第一份工作
我是 2018 年毕业的,那会北京,上海,都有落户的限制,甚至还有一些积分制等似乎不欢迎应届生去上班的感觉。
那会刚毕业,可谓是心比天高,落个户都这么麻烦,我还不想去呢!还不如人深圳的口号,来了就是深圳人。而当时阿里的总部,就在杭州,而且杭州只要是大学生,立马就能落户,立马能摇号买房。而当时房价也比较亲民 (确认过眼神,是掏光钱包可以买得起的价格)。
所以基本上只找杭州的工作。最终入职了当时比较热门的 p2p 领域的独角兽,51 信用卡。
当时的 51信用卡,可以说是 p2p 领域的一只牛逼独角兽,甚至这家公司的缩写就是 51NB。
不过以我当时的认知,入职 51信用卡,纯粹是因为 20k 的薪资,以及全额报销来回路费。要知道当时 BAT 虽然有报销,但是实际上都有各种限制和上限。
ps: 以我当时的认知,几乎没有任何犹豫,我就关闭了我的副业通道,因为我觉得,精进前端技术,带来的收益更大,毕竟一个月 20k 的收入,更别提还有 4个月加的年终收入了。而这份副业,一方面对主业没有提升,同时还要消耗比较大的精力(主要集中在给学生讲解代码以及售后上),收入也就几千块而且时间比较集中,很难兼顾。
现在回过头来看,真的是误打误撞赶上了 p2p 行业的末班车。起始薪资确实不错,但是很快就来到了国家严控 p2p 行业的开端。
最终,入职当年,就遇到了一波一波的裁员,从开始的 n + 3,到 n + 2 再到 n + 1,可谓是一波一波的裁员。也包括了应届生。一如现在,应届生也还是裁员重灾区。
也因为 51 当时是杭州互联网第一波开启的裁员,还裁了应届生。口碑急转直下,但是很快就迎来了反转,隔壁滴滴,微店等也迅速开启裁员模式,仅仅只有 n + 1。
ps: 在 51 的第一年,是有年会的。第二年,年会倒是有,但是主题就是一句话,今年将是接下来十年内,最好的一年。也是这一年 2019,p2p 彻底宣告结束,51 也出现了警车上门的事件。最终借贷业务转型为依赖于银行的借贷业务。也结束了接近 10% 的储蓄利率时代。
短暂的阿里之旅
2020 年年初,p2p 行业宣告结束叠加疫情之初,悲观情绪四处蔓延。我在 51 的旅程也渐渐走到了尾声。
当时面试了字节和阿里。彼时的字节跳动,在杭州名气和规模还没有如今这么大。权衡之下选择了名气更盛,当时口碑更好的阿里。但是我对于字节的判断,实在是偏差的离谱,看着如今蒸蒸日上的字节,真是后悔莫及。
但是进了阿里,说实话是真有些不适应。
一方面生活上,不提供纸巾,让我颇为诧异,而时不时的团队聚餐竟然是 AA 也让我非常不适应。
当然,对方看我也很奇怪,说了一句话,感觉你是外企来的 (ps: 如今的 51信用卡还是有点小而美的感觉,各项业务依托也还在继续,也有露营等新业务的开拓,老板自由后也还在继续折腾着)。
因为疫情的原因,我并没有经历百阿培训。但是有一本小册子,写着价值观。让我印象深刻的是,"此时此刻,非我莫属" 和 "不难,要你做什么"。
这两句话听起来都没有什么问题,鼓励人奋进并没有任何问题,但是以前的奋斗,伴随着可能的巨大的回报,而我当时的付出与回报,显然已经是大打折扣了。
那时还没有 pua 的说法,但是确实有一些话让我觉得不舒服,比如目标要跳起来才是 3.5,蹦起来才是 3.75,以及业务好和你一点关系都没有,你把业务做好了,也只能给你 3.25。必须得出一些技术项目,才能拿到好绩效。
而那种没有人会点明但是大家都在执行的道理:和我 kpi 有关的就是天,无关的就是已读不回,而已读不回,就是拒绝。更是让当时还非常稚嫩的我想要逃之夭夭。
这些也不能说错,但是这确实和我在 5信用卡当时围着业务转的风格大相径庭。
说回技术,阿里整个集团的基建可以说非常好,反而我所在的这个小前端团队的基建,赶不上 51前端团队的基建。
不论是脚手架,发布系统(比较让我震惊的是我当时团队的发布是自己丢文件到服务器上,测试正式环境的区分还是靠手动维护的一份文件),开发流程,完全赶不上 51当时的丝滑程度。
可以说是对压力的逃避,也可以说是对这种环境的不适应,也可以说是对涨幅的不满,我很快就开启了下一段旅程。
现在回过头来看,当时离职,还是冲动占了大部分。一方面,随着后来业务接触的多了,能够理解当时那个小团队的基建差的原因:主要是业务形态,当时的小团队是 toB 的,要维护的仅仅是一个项目,自然在发布流程上的投入不会太多,而且收益也是远远不及 51 这种移动端几十上百个工程的发布工作来的实在。
而另一方面,所谓的深夜开会,不明说但是心里都清楚的加班氛围,以及唯 kpi 导向的风气,其实也不过是一种生存规则而已。强行说服自己接受也很容易。毕竟人生如戏,适应规则,利用规则,掌握规则,但凡能够想通这一点,当时坚持下来也非常容易。
长达三年的网易之旅
当处于阿里的水深火热之中时,一个在周末就完成了全部流程的网易团队,向我抛出了橄榄枝。
经过短暂的调整,我也就入职了这个团队,不曾想一待就是三年。此时的薪资来到了 30k 左右,但是由于当时这个团队独特的奖金制,月收入会比 base 高出不少。
团队的业务主要是直播,所以 toB 和 toC 的业务都有。基建上也比较完善,发布系统,组件库,脚手架,微前端,等等,相对更为繁荣。
这个团队并没有明确的技术项目的考核,还是以业务为主,大多数人,完成业务开发目标,就能够顺利拿到 3.5 的绩效,同时由于当时直播行业的繁荣,基本都会有一笔不菲的奖金。而技术项目属于锦上添花,确确实实能在最终的绩效上有所体现但是并不多。
但是恰恰是在这样的环境下,组内的同学在相对宽松的氛围下,更热衷于鼓捣技术项目,反而平时对技术的研究及讨论会更多一些。
这三年,也算是见证了业务的兴衰,从开始的营收暴涨开始出海,到最终营收暴跌收缩,到裁员。也不过短短三年。
现在回头来看,在这三年里,对于业务的了解更多的还是停留在表层,虽然当时觉得自己理解业务方的需求了,但是其实内部的很多玩法还是远非仅仅理解需求就能接触到的,什么大 R 运营,"军火商" 等等秀场直播的黑话,我是没有学到一点。
由于组内业务还算比较综合,c端页面的开发,b端后台都有所接触。同时业务之余还会有很多时间去做一些技术项目,比如我负责的 CloudIDE, WebIDE, 可视化埋点项目, 基于 zustand 的状态管理库, 均是这一时期的产物。
整体来讲,这三年不论是工作节奏,还是技术产出,都还算可以。
但是如今回过头来看,似乎这三年,对外界的关注,基本上有了一定的钝感,不像之前,对互联网的各个信息都会去了解看一下。反而这几年,说内敛沉稳也好,说闭门造车也好,说停留在自己的舒适圈内也好,除了技术层面的精进,对于整个行业的发展,都太过闭塞,仿佛只是重复一种舒适的生活过了三年:每天和老婆一起轮流开车上下班,顺便再健个身,住着自己的房子,还着公积金就能覆盖还有结余的带款。
如今回想起来,也正是这三年的经历,让我在技术上有所精进,但是对互联网行业的关注,反而有所下降。同时由于同事间的关系比较简单,也让我在人际交往上变得更加朴素真诚。
半年的小公司之旅
怎么说呢,好像人总是在不稳定的时候追求稳定,在稳定的时候追求不稳定。
所以在结束了网易的三年相对稳定的工作之后,我内心反而变得很躁动,想要去小公司,谋一番事业。
出来看机会之后,才发现外界的环境其实并没有平时了解的那么糟糕,确实不像之前机会那么多,但是确实也还有一些岗位。
在这之中,我选择了在发展业务第二曲线同时又有第一业务支持的说稳定又不稳定的公司 ---- 爱普拉维。
这家公司业务主要集中在海外,所以整体业务情况也还是非常客观。给出的薪资也比较客观,我的薪资也在这一时期,达到了 50k 左右。
不过入职之初,就经历了一些人事变动,如今想来,可以说是警醒,但是我应该是选择性的进行了忽视。心思沉浸在技术和一点点的管理上。
这个团队前端同学并不多,但是业务上除了常规的 h5 和少量的后台项目之外,还会存在一些 chrome 扩展逆向,爬虫项目的存在,而我被招进来的主要任务,也就是 chrome 扩展的逆向和爬虫项目。
在这一期间,我一度沉浸在了技术上的钻研中,从 webpack 的解码逆向,到 puppeteer 爬虫的实现,从 项目秒开的优化,到 svelte 的重构,都是对我之前技术经验的一个补充。也顺利在技术角度上在公司站稳了脚跟。度过了这个公司网上传言的不好过的试用期。
不过终归还是在人际交往上有所欠缺,叠加上公司的业务方向调整,导致了最终今年 1月份的离职。而这,也为我的职场画下了短暂的暂停键。
离职快小半年了
不知不觉离离职已经快小半年了,也顺利领到了失业金,也就是题目中提到的 2k。
这段时间从刚开始的玩乐,到中途的读书写文章,再到一些副业(对于无业人员来讲应该是主业)的探索。焦虑在所难免,未来也还比较迷茫,而其他主业的探索,说实话也没探索出来什么结果。
反倒是这段读书的时间给了我一些收获,一方面是 《穷爸爸富爸爸》中对于资产负债表的解释,我自己也做了一份,还参加了财富流沙盘游戏,对自己的财务状况有了更好的认知。另一方面便是 《认知觉醒》中关于焦虑的说法,一定程度上命中了当下的自己很多。
最后,就用《认知觉醒》中关于焦虑的根源来结束这篇文章吧:想同时做很多事,又想立即看到效果。自己的欲望大于能力,又极度缺乏耐心。人的天性就是避难驱易和急于求成。
ps: 避难驱易,这几个字实在太戳我了,也正是因为避难驱易,所以其实很多之前就想写的文章都拖拖拖,直到认识到是内心的避难驱易之后才开始控制自己开始输出,而也正是输出才让我注意到了自己之前没有注意到的点,才有了这篇文章以及 那些年,我在职场中做过的蠢事。
最后的最后,愿我们都有美好的未来!
来源:juejin.cn/post/7366567675315126281
uni-app 集成推送
研究了几天,终于是打通了uni-app的推送,本文主要针对的是App端的推送开发过程,分为在线推送和离线推送。我们使用uni-app官方推荐的uni-push2.0。官方文档
准备工作:开通uni-push功能
- 勾选uniPush2.0
- 点击"配置"
- 填写表单
关联服务空间说明:
uni-push2.0需要开发者开通uniCloud。不管您的业务服务器是否使用uniCloud,但实现推送,就要使用uniCloud服务器。
- 如果您的后台业务使用uniCloud开发,那理解比较简单。
- 如果您的后台业务没有使用uniCloud,那么也需要在uni-app项目中创建uniCloud环境。在uniCloud中写推送逻辑,暴露一个接口,再由业务后端调用这个推送接口。
在线推送
以上操作配置好了以后,回到HBuilderX。
因为上面修改了manifest.json配置,一定要重新进行一次云打包(打自定义调试基座和打正式包都可以)后才会生效。
客户端代码
我这边后端使用的是传统服务器,未使用云开发。要实现推送,首先需要拿到一个客户端的唯一标识,使用uni.getPushClientId API
链接地址
onLaunch() {
uni.getPushClientId({
success: (res) => {
let push_clientid = res.cid
console.log('客户端推送标识:', push_clientid)
// 保存在全局,可以在进入app登录账号后调用一次接口将设备id传给后端
this.$options.globalData.pushClientId = push_clientid
// 一进来就掉一次接口把push_clientid传给后端
this.$setPushClientId(push_clientid).then(res => {
console.log('[ set pushClientId res ] >', res)
})
},
fail(err) {
console.log(err)
}
})
}
客户端监听推送消息
监听推送消息的代码,需要在收到推送消息之前被执行。所以应当写在应用一启动就会触发的应用生命周期
onLaunch
中。
//文件路径:项目根目录/App.vue
export default {
onLaunch: function() {
console.log('App Launch')
uni.onPushMessage((res) => {
console.log("收到推送消息:",res) //监听推送消息
})
},
onShow: function() {
console.log('App Show')
},
onHide: function() {
console.log('App Hide')
}
}
服务端代码
- 鼠标右击项目根目录,依次执行
- 然后右击uniCloud目录,选择刚开始创建的云服务空间
- 在cloudfunctions目录右击,新建云函数/云对象,命名为uni-push,会创建一个uni-push目录
- 右击uni-push目录,点击 管理公共模块或扩展库依赖,选择uni-cloud-push
- 右击database目录,新建DB Schema,创建这三张表:
opendb-tempdata
,opendb-device
,uni-id-device
,也就是json文件,直接输入并选择相应的模板。
- 修改index.js
'use strict';
const uniPush = uniCloud.getPushManager({appId:"__UNI__XXXX"}) //注意这里需要传入你的应用appId
exports.main = async (event, context) => {
console.log('event ===> ', event)
console.log('context ===> ', context)
// 所有要传的参数,都在业务服务器调用此接口时传入
const data = JSON.parse(event.body || '{}')
console.log('params ===> ', data)
return await uniPush.sendMessage(data)
};
- package.json
{
"name": "uni-push",
"dependencies": {},
"main": "index.js",
"extensions": {
"uni-cloud-push": {}
}
}
- 右击uni-push目录,点击上传部署
- 云函数url化
登录云函数控制台,进入云函数详情
8. postman测试一下接口
没问题的话,客户端将会打印“console.log("收到推送消息:", xxx)”,这一步最好是使用真机,运行到App基座,使用自定义调试基座运行,会在HBuilderX控制台打印。
离线推送
APP离线时,客户端收到通知会自动在通知栏创建消息,实现离线推送需要配置厂商参数。
苹果需要专用的推送证书,创建证书参考链接
安卓需要在各厂商开发者后台获取参数,参考链接
参数配置好了以后,再次在postman测试
注意 安卓需要退出app后,在任务管理器彻底清除进程,才会走离线推送
解决离线推送没有声音
这个是因为各安卓厂商为了避免开发者滥用推送进行的限制,因此需要设置离线推送渠道,查看文档
调接口时需要传一个channel参数
实现离线推送自定义铃声
这个功能只有华为和小米支持
也需要设置channel参数,并使用原生插件,插件地址
注意 使用了原生插件,一定要重新进行一次云打包
- 华为,申请了自分类权益即可
- 小米,在申请渠道时,选择系统铃声,url为
android.resource://安卓包名/raw/铃声文件名(不要带后缀)
来源:juejin.cn/post/7267417057451573304
无框架,跨框架!时隔两年,哈啰Quark Design迎来重大特性升级!
引言
历经1年多迭代,Quarkd 2.0 版本正式发布,这是自 Quarkd 开源以来第二个重大版本。本次升级主要实现了组件外部可以穿透影子Dom,修改组件内部元素的任何样式。
- (迁移后)最新官网:quark-ecosystem.github.io/quarkd-docs
- Github 地址:github.com/hellof2e/qu…
Quark Design 介绍
Quark(夸克) Design 是由哈啰平台 UED 和增长&电商前端团队联合打造的一套面向移动端的跨框架 UI 组件库。与业界第三方组件库不一样,Quark Design 底层基于 Web Components 实现,它能做到一套代码,同时运行在各类前端框架/无框架中。
前端各类框架技术发展多年,很多公司存量前端项目中必定存在各类技术栈。为了解决各类不同技术栈下UI交互统一,我们开发了这套UI组件库。
之前技术瓶颈
熟悉 quarkd 的开发者都知道其底层基因是 Web Components,从而实现了跨技术栈使用。但Web Components 中的 shadow dom 特性决定了其“孤岛”的特性,组件内部是个独立于外部的小世界,外部无法修改组件内部样式,若要修改内部样式,我们在 quarkd 1.x 版本中采用了 CSS 变量的方式来支援这种做法。
但这种做法依旧局限性非常大,你只能修改预设css变量的指定样式,比如你要修改 Dialog 内容中的字体大小/颜色:
![](https://www.imgeek.net/uploads/article/20240717/5132cc60cdaca923ef8a9ee6a5f58f82.jpg)
// 使用组件
<quark-dialog class=“dialog” content="生命远不止连轴转和忙到极限,人类的体验远比这辽阔、丰富得多。"></quark-dialog>
// 内部css源码
:host .quark-dialog-content {
font-size: var(--dialog-content-font-size, 14px);
color: var(--dialog-content-color, "#5A6066");
// ... 其它样式
}
这时候,你需要在组件外部书写:
.dialog {
--dialog-content-font-size: 36px;
--dialog-content-color: red;
}
这种做法会带来一些问题,比如当源码中没有指定的css变量,就意味着你无法通过css变量从外面渗透进入组件内部去修改,比如 dialog conent 内的 font-style
。
升级后
得益于 ::part
CSS 伪元素的特性, 我们将 Quarkd 主要 dom 节点进行改造,升级后,你可以通过如下方式来自定义任何组件样式。
custom-element::part(foo) {
/* 样式作用于 `foo` 部分 */
}
::part
可以用来表示在阴影树中任何匹配 part
属性的元素。
该特性已兼容主流浏览器,详情见:mozilla.org # ::part()
用法示例:
// 使用组件
<quark-dialog class=“dialog” content="生命远不止连轴转和忙到极限,人类的体验远比这辽阔、丰富得多。"></quark-dialog>
.dialog::part(body) {
font-size: 24px;
color: #666;
}
.dialog::part(footer) {
font-size: 14px;
color: #333;
}
其它DEMO地址:stackblitz.com/edit/quarkd…
关于升级
Quarkd 2.x 向下兼容所有 1.x 功能及特性,之前的css变量也被保留,所以使用者可以从1.x直接升级到2.x!
One more thing
假如你也想利用 quarkd 底层能力构建属于自己的跨技术栈组件,欢迎使用:
github.com/hellof2e/qu…
最后
感谢在Quarkd迭代期间作出贡献的朋友们,感谢所有使用quarkd的开发者!
来源:juejin.cn/post/7391753478123864091
zero-privacy——uniapp小程序隐私协议弹窗组件
一. 引言
为规范开发者的用户个人信息处理行为,保障用户的合法权益,自2023年9月15日起,对于涉及处理用户个人信息的小程序开发者,微信要求,仅当开发者主动向平台同步用户已阅读并同意了小程序的隐私保护指引等信息处理规则后,方可调用微信提供的隐私接口。
公告地址:关于小程序隐私保护指引设置的公告
developers.weixin.qq.com/miniprogram…
接下来我们将打造一个保姆级的隐私协议弹窗组件
二. 开发调试基础
划重点,看文档,别说为什么没有效果,没有弹窗
1. 更新用户隐私保护指引
小程序管理员或开发者可以根据具体小程序涉及到的隐私相关接口来更新微信小程序后台的用户隐私保护指引,更新并审核通过后就可以进行相关的开发调试工作。仅有在指引中声明所处理的用户信息,才可以调用平台提供的对应接口或组件。若未声明,对应接口或组件将直接禁用。
- ���知道怎么填写隐私协议,看看文档:用户隐私保护指引设置developers.weixin.qq.com/miniprogram…
- 哪些api需要用户点击同意隐私协议才可以使用的看这里:小程序用户隐私保护指引内容介绍developers.weixin.qq.com/miniprogram…
审核时间有人说十几分钟,我自己的给大家参考一下。
审核通过!审核通过!审核通过后才可以开发调试。
2.配置调试字段 "__usePrivacyCheck__": true
- 在 2023 年 9 月 15 号之前,在 app.json 中配置
"__usePrivacyCheck__": true
后,会启用隐私相关功能,如果不配置或者配置为 false 则不会启用。 - 在 2023 年 9 月 15 号之后,不论 app.json 中是否有配置 usePrivacyCheck,隐私相关功能都会启用。
- 所以在基于uni-app开发时,我们在 2023 年 9 月 15 号之前进行相关开发调试则需要在manifest.json文件mp-weixin中添加
"__usePrivacyCheck__": true
- manifest.json文件源码视图
"mp-weixin" : {
"__usePrivacyCheck__": true
},
3. 配置微信开发工具基础库
将调试基础库改为3.0.0以上。具体路径为:
微信开发者工具->详情->本地设置->调试基础库
以上配置完成后,即可看看效果,我在小程序后台设置了剪切板的隐私接口,果然,已经提示没有隐私授权不能使用了。
三. zero-privacy组件介绍
组件下载地址:ext.dcloud.net.cn/plugin?name…
组件的功能和特点
- 支持 居中弹出,底部弹出
- 不依赖第三方弹窗组件,内置轻量动画效果
- 支持自定义触发条件
- 支持自定义主题色
- 组件中最重要的4个api(只需用到前3个):
- wx.getPrivacySetting 查询隐私授权情况 官方链接
- wx.onNeedPrivacyAuthorization 监听隐私接口需要用户授权事件。 官方链接
- wx.openPrivacyContract 跳转至隐私协议页面 官方链接
- wx.requirePrivacyAuthorize 模拟隐私接口调用,并触发隐私弹窗逻辑 官方链接
四. zero-privacy组件使用方法
在uniapp插件市场直接下载导入 uni_modules
后使用即可
- 最直接看到弹窗效果的测试方法
<template>
<view class="container">
<zero-privacy :onNeed='false'></zero-privacy>
</view>
</template>
注意以上是测试方案,不建议实际开发中按上面的方法使用,推荐以下两种方法
- 在小程序首页等tabbar页面直接处理隐私弹窗逻辑
<template>
<view class="container">
<zero-privacy :onNeed='false' :hideTabBar='true'></zero-privacy>
</view>
</template>
- 在页面点击某些需要用到隐私协议后处理隐私弹窗逻辑
<template>
<view class="container">
<view class="btn" @click="handleCopy">
复制
</view>
<zero-privacy></zero-privacy>
</view>
</template>
- 自定义内容使用
<template>
<view class="container">
<zero-privacy title="测试自定义标题" predesc="协议前内容" privacy-contract-name-custom="<自定义名称及括号>" subdesc="协议后内容协议后内容协议后内容. 主动换行"></zero-privacy>
</view>
</template>
五. zero-privacy组件参数说明
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
position | String | center | 可选 bottom ,从底部弹出 |
color | String | #0396FF | 主颜色: 协议名和同意按钮的背景色 |
bgcolor | String | #ffffff | 弹窗背景色 |
onNeed | Boolean | true | 使用到隐私相关api时触发弹窗,设置为false时初始化弹窗将判断是否需要隐私授权,需要则直接弹出 |
hideTabBar | Boolean | false | 是否需要隐藏tabbar,在首页等tabbar页面使用改弹窗时建议改为true |
title | String | #ffffff | 用户隐私保护提示 |
predesc | String | 使用前请仔细阅读 Ï | 协议名称前的内容 |
subdesc | String | 当您点击同意后,即表示您已理解并同意该条款内容,该条款将对您产生法律约束力。如您拒绝,将无法使用该服务。 | 协议名称后的内容 |
privacyContractNameCustom | String | '' | 自定义协议名称,不传则由小程序自动获取 |
predesc
和 subdesc
的自定义内容,需要主动换行时在内容中添加实体字符
即可
六. zero-privacy组件运行效果
来源:juejin.cn/post/7273803674790150183
java就能写爬虫还要python干嘛?
爬虫学得好,牢饭吃得饱!!!切记!!!
相信大家多少都会接触过爬虫相关的需求吧,爬虫在绝大多数场景下,能够帮助客户自动的完成部分工作,极大的减少人工操作。目前更多的实现方案可能都是以python为实现基础,但是作为java程序员,咱们需要知道的是,以java 的方式,仍然可以很方便、快捷的实现爬虫。下面将会给大家介绍两种以java为基础的爬虫方案,同时提供案例供大家参考。
一、两种方案
传统的java实现爬虫方案,都是通过jsoup的方式,本文将采用一款封装好的框架【webmagic】进行实现。同时针对一些特殊的爬虫需求,将会采用【selenium-java】的进行实现,下面针对两种实现方案进行简单介绍和演示配置方式。
1.1 webmagic
官方文档:webmagic.io/
1.1.1 简介
使用webmagic开发爬虫,能够非常快速的实现简单且逻辑清晰的爬虫程序。
四大组件
- Downloader:下载页面
- PageProcessor:解析页面
- Scheduler:负责管理待抓取的URL,以及一些去重的工作。通常不需要自己定制。
- Pipeline:获取页面解析结果,数持久化。
Spider
- 启动爬虫,整合四大组件
1.1.2 整合springboot
webmagic分为核心包和扩展包两个部分,所以我们需要引入如下两个依赖:
<properties>
<webmagic.version>0.7.5</webmagic.version>
</properties>
<!--WebMagic-->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>${webmagic.version}</version>
</dependency>
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>${webmagic.version}</version>
</dependency>
到此为止,我们就成功的将webmagic引入进来了,具体使用,将在后面的案例中详细介绍。
1.2 selenium-java
1.2.1 简介
selenium是一款浏览器自动化工具,它能够模拟用户操作浏览器的交互。但前提是,我们需要在使用他的机器(windows/linux等)安装上它需要的配置。相比于webmigc的安装,它要繁琐的多了,但使用它的原因,就是为了解决一些webmagic做不到的事情。
支持多种语言:java、python、ruby、javascript等。其使用代码非常简单,以java为例如下:
package dev.selenium.hello;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
public class HelloSelenium {
public static void main(String[] args) {
WebDriver driver = new ChromeDriver();
driver.get("https://selenium.dev");
driver.quit();
}
}
1.2.2 安装
无论是在windows还是linux上使用selenium,都需要两个必要的组件:
- 浏览器(chrome)
- 浏览器驱动 (chromeDriver)
需要注意的是,要确保上述两者的版本保持一致。
下载地址
chromeDriver:chromedriver.storage.googleapis.com/index.html
windows
windows的安装相对简单一些,将chromeDriver.exe下载至电脑,chrome浏览器直接官网下载相应安装包即可。严格保证两者版本一致,否则会报错。
在后面的演示程序当中,只需要通过代码指定chromeDriver的路径即可。
linux
linux安装才是我们真正的使用场景,java程序通常是要部署在linux环境的。所以我们需要linux的环境下安装chrome和chromeDriver才能实现想要的功能。
首先要做的是判断我们的linux环境属于哪种系统,是ubuntu
、centos
还是其他的种类,相应的shell脚本都是不同的。
我们采用云原生的环境,所有的服务均以容器的方式部署,所以要在每一个服务端容器内部安装chrome和chromeDiver。我们使用的是Alpine Linux
,一个轻量级linux发行版,非常适合用来做Docker镜像。
我们可以通过apk --help
去查看相应的命令,我直接给出安装命令:
# Install Chrome for Selenium
RUN apk add gconf
RUN apk add chromium
RUN apk add chromium-chromedriver
上面的内容,可以放在DockerFile文件中,在部署的时候,会直接将相应组件安装在容器当中。
需要注意的是,在Alpine Linux中自带的浏览器是chromium
和chromium-chromedriver
,且版本相应较低,但是足够我们的需求所使用了。
/ # apk search chromium
chromium-68.0.3440.75-r0
chromium-chromedriver-68.0.3440.75-r0
1.2.3 整合springboot
我们只需要在爬虫模块引入依赖就好了:
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
</dependency>
二、三个案例
下面通过三个简单的案例,给大家实际展示使用效果。
2.1 爬取省份街道
使用webmagic进行省份到街道的数据爬取。注意,本文只提供思路,不提供具体爬取网站信息,请同学们自己根据使用选择。
接下来搭建webmagic的架子,其中有几个关键点:
- 创建页面解析类,实现PageProcessor。
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.processor.PageProcessor;
/**
* 页面解析
*
* @author wjbgn
* @date 2023/8/15 17:25
**/
public class TestPageProcessor implements PageProcessor {
@Override
public void process(Page page) {
}
@Override
public Site getSite() {
return site;
}
/**
* 初始化Site配置
*/
private Site site = Site.me()
// 重试次数
.setRetryTimes(3)
//编码
.setCharset(StandardCharsets.UTF_8.name())
// 超时时间
.setTimeOut(10000)
// 休眠时间
.setSleepTime(1000);
}
- 实现PageProcessor后,要重写其方法process(Page page),此方法是我们实现爬取的核心(页面解析)。通常省市区代码分为6级,所以常见的网站均是按照层级区分,我们是从省份开始爬取,即从第三层开始爬取。
- 初始化变量
@Override
public void process(Page page) {
// 市级别
Integer type = 3;
// 初始化结果明细
RegionCodeDTO regionCodeDTO = new RegionCodeDTO();
// 带有父子关系的结果集合
List<Map<String, Object>> list = new ArrayList();
// 页面所有元素集合
List<String> all = new ArrayList<>();
// 页面中子页面的链接地址
List<String> urlList = new ArrayList<>();
}
- 根据不同级别,获取相应页面不同的元素
if (CollectionUtil.isEmpty(all)) {
// 爬取所有的市,编号,名称
all = page.getHtml().css("table.citytable").css("tr").css("a", "text").all();
// 爬取所有的城市下级地址
urlList = page.getHtml().css("table.citytable").css("tr").css("a", "href").all()
.stream().distinct().collect(Collectors.toList());
if (CollectionUtil.isEmpty(all)) {
// 区县级别
type = 4;
all = page.getHtml().css("table.countytable").css("tr.countytr").css("td", "text").all();
// 获取区
all.addAll(page.getHtml().css("table.countytable").css("tr.countytr").css("a", "text").all());
urlList = page.getHtml().css("table.countytable").css("tr").css("a", "href").all()
.stream().distinct().collect(Collectors.toList());
if (CollectionUtil.isEmpty(all)) {
// 街道级别
type = 5;
all = page.getHtml().css("table.towntable").css("tr").css("a", "text").all();
urlList = page.getHtml().css("table.towntable").css("tr").css("a", "href").all()
.stream().distinct().collect(Collectors.toList());
if (CollectionUtil.isEmpty(all)) {
// 村,委员会
type = 6;
List<String> village = new ArrayList<>();
all = page.getHtml().css("table").css("tr.villagetr").css("td", "text").all();
for (int i = 0; i < all.size(); i++) {
if (i % 3 != 1) {
village.add(all.get(i));
}
}
all = village;
}
}
}
}
- 定义一个实体类RegionCodeDTO,用来存放临时获取的code,url以及父子关系等内容:
public class RegionCodeDTO {
private String code;
private String parentCode;
private String name;
private Integer type;
private String url;
private List<RegionCodeDTO> regionCodeDTOS;
}
- 接下来对页面获取的内容(code、name、type)进行组装和临时存储,添加到children中:
// 初始化子集
List<RegionCodeDTO> children = new ArrayList<>();
// 初始化临时节点数据
RegionCodeDTO region = new RegionCodeDTO();
// 解析页面结果集all当中的数据,组装到region 和 children当中
for (int i = 0; i < all.size(); i++) {
if (i % 2 == 0) {
region.setCode(all.get(i));
} else {
region.setName(all.get(i));
}
if (StringUtils.isNotEmpty(region.getCode()) && StringUtils.isNotEmpty(region.getName())) {
region.setType(type);
// 添加子集到集合当中
children.add(region);
// 重新初始化
region = new RegionCodeDTO();
}
}
- 组装页面链接,并将页面链接组装到children当中。
// 循环遍历页面元素获取的子页面链接
for (int i = 0; i < urlList.size(); i++) {
String url = null;
if (StringUtils.isEmpty(urlList.get(0))) {
continue;
}
// 拼接链接,页面的子链接是相对路径,需要手动拼接
if (urlList.get(i).contains(provinceEnum.getCode() + "/")) {
url = provinceEnum.getUrlPrefixNoCode();
} else {
url = provinceEnum.getUrlPrefix();
}
// 将链接放到临时数据子集对象中
if (urlList.get(i).substring(urlList.get(i).lastIndexOf("/") + 1, urlList.get(i).indexOf(".html")).length() == 9) {
children.get(i).setUrl(url + page.getUrl().toString().substring(page.getUrl().toString().indexOf(provinceEnum.getCode() + "/") + 3
, page.getUrl().toString().lastIndexOf("/")) + "/" + urlList.get(i));
} else {
children.get(i).setUrl(url + urlList.get(i));
}
}
- 将children添加到结果对象当中
// 将子集放到集合当中
regionCodeDTO.setRegionCodeDTOS(children);
- 在下面的代码当中将进行两件事儿:
- 处理下一页,通过page的addTargetRequests方法,可以进行下一页的跳转,此方法参数可以是listString和String,即支持多个页面跳转和单个页面的跳转。
- 将数据传递到Pipeline,用于数据的存储,Pipeline的实现将在后面具体说明。
// 定义下一页集合
List<String> nextPage = new ArrayList<>();
// 遍历上面的结果子集内容
regionCodeDTO.getRegionCodeDTOS().forEach(regionCodeDTO1 -> {
// 组装下一页集合
nextPage.add(regionCodeDTO1.getUrl());
// 定义并组装结果数据
Map<String, Object> map = new HashMap<>();
map.put("regionCode", regionCodeDTO1.getCode());
map.put("regionName", regionCodeDTO1.getName());
map.put("regionType", regionCodeDTO1.getType());
map.put("regionFullName", regionCodeDTO1.getName());
map.put("regionLevel", regionCodeDTO1.getType());
list.add(map);
// 推送数据到pipeline
page.putField("list", list);
});
// 添加下一页集合到page
page.addTargetRequests(nextPage);
- 当本次process方法执行完后,将会根据传递过来的链接地址,再次执行process方法,根据前面定义的读取页面元素流程的代码,将不符合type=3的内容,所以将会进入到下一级4的爬取过程,5、6级别原理相同。
- 创建Pipeline,用于编写数据持久化过程。经过上面的逻辑,已经将所需内容全部获取到,接下来将通过pipline进行数据存储。首先定义pipeline,并实现其process方法,获取结果内容,具体存储数据的代码就不展示了,需要注意的是,此处pipeline没有通过spring容器托管,需要调用业务service需要使用SpringUtils进行获取:
public class RegionDataPipeline implements Pipeline{
@Override
public void process(ResultItems resultItems, Task task) {
// 获取service
IXXXXXXXXXService service = SpringUtils.getBean(IXXXXXXXXXService.class);
// 获取内容
List<Map<String, String>> list = (List<Map<String, String>>) resultItems.getAll().get("list");
// 解析数据,转换为对应实体类
// service.saveBatch
}
- 启动爬虫
//启动爬虫
Spider.create(new RegionCodePageProcessor(provinceEnum))
.addUrl(provinceEnum.getUrl())
.addPipeline(new RegionDataPipeline())
//此处不能小于2
.thread(2).start()
2.2 爬取网站静态图片
爬取图片是最常见的需求,我们通常爬取的网站都是静态的网站,即爬取的内容都在网页上面渲染完成的,我们可以直接通过获取页面元素进行抓取。
可以参考下面的文章,直接拉取网站上的图片:juejin.cn/post/705138…
针对获取到的图片网络地址,直接使用如下方式进行下载即可:
url = new URL(imageUrl);
//打开连接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
//设置请求方式为"GET"
conn.setRequestMethod("GET");
//超时响应时间为10秒
conn.setConnectTimeout(10 * 1000);
//通过输入流获取图片数据
InputStream is = conn.getInputStream();
2.3 爬取网站动态图片
在2.2中我们可以很快地爬取到对应的图片,但是在另外两种场景下,我们获取图片将会不适用上面的方式:
- 需要拼图,且多层的gis相关图片,此种图片将会在后期进行复杂的图片处理(按位置拼接瓦片,多层png图层叠加),才能获取到我们想要的效果。
- 动态js加载的图片,直接无法通过css、xpath获取。
所以在这种情况下我们可以使用开篇介绍的selenium-java来解决,本文使用的仅仅是截图的功能,来达到我们需要的效果。具体街区全屏代码如下所示:
public File getItems() {
// 获取当前操作系统
String os = System.getProperty("os.name");
String path;
if (os.toLowerCase().startsWith("win")) {
//windows系统
path = "driver/chromedriver.exe";
} else {
//linux系统
path = "/usr/bin/chromedriver";
}
WebDriver driver = null;
// 通过判断 title 内容等待搜索页面加载完毕,间隔秒
try {
System.setProperty("webdriver.chrome.driver", path);
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.addArguments("--headless");
chromeOptions.addArguments("--no-sandbox");
chromeOptions.addArguments("--disable-gpu");
chromeOptions.addArguments("--window-size=940,820");
driver = new ChromeDriver(chromeOptions);
// 截图网站地址
driver.get(UsaRiverConstant.OBSERVATION_POINT_URL);
// 休眠用于网站加载
Thread.sleep(15000);
// 截取全屏
File screenshotAs = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
return screenshotAs;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
driver.quit();
}
}
如上所示,我们获取的是整个页面的图片,还需要对截取的图片进行相应的剪裁,保留我们需要的区域,如下所示:
public static void cutImg(InputStream inputStream, int x, int y, int width, int height, OutputStream outputStream) {//图片路径,截取位置坐标,输出新突破路径
InputStream fis = inputStream;
try {
BufferedImage image = ImageIO.read(fis);
//切割图片
BufferedImage subImage = image.getSubimage(x, y, width, height);
Graphics2D graphics2D = subImage.createGraphics();
graphics2D.drawImage(subImage, 0, 0, null);
graphics2D.dispose();
//输出图片
ImageIO.write(subImage, "png", outputStream);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
三、小结
通过如上两个组件的简单介绍,足够应付在java领域的大多数爬取场景。从页面数据、到静态网站图片,在到动态网站的图片截取。本文以提供思路为主,原理请参考相应的官方文档。
爬虫学得好,牢饭吃得饱!!!切记!!!
来源:juejin.cn/post/7267532912617177129
领导让前端实习生在网页上添加一个长时间不操作锁定电脑的功能
前情提要
大约一个月前,公司的医疗管理系统终于完工上线。后面一个周一,领导叫大家开会,说后面没有项目进来了,用不了这么多开发人员,原地宣布裁员。再后一周后,花 2000 招了个实习生,工作内容为系统维护。
工作内容
领导:由于我们工作内容很简单,事情轻松,基本就在页面上加加按钮就行,所以工资相对较少一些,是否接受?
实习生小李:能开实习证明吗?
领导:能的。
实习生小李:好的,谢谢老板。
领导:什么时候能入职?
实习生小李:现在。
工作来源
医疗系统是一个比较数据敏感的系统,现在医院那边需要添加一个十分钟时间没有在系统进行操作,则锁定电脑的功能,使用者再次使用时,必须输入密码。客户那边在系统对接群里发出需求时,并没有人回复(PS:人都裁完了),然后老板回复到:好的。
工作安排
领导:小李,我们有个医疗系统,需要添加锁屏功能,你处理一下,两天时间应该没问题吧?
实习生小李:(思索片刻)好的,有代码吗
(4小时之后)
领导:有的,我找下
(第二天10点)
实习生小李:王总,代码找到了没有
(第二天12点)
领导:没代码改不了吗?
实习生小李:(瑟瑟发抖)我试试
(第二天14点)
实习生小李:王总,是那种长时间不操作就锁定系统的功能吗
领导:是的
实习生小李:多久不操作才锁
领导:十分钟,锁了需要输入密码才能使用
实习生小李:但是我们医疗系统没有密码功能
领导:客户电脑有密码啊
实习生小李:是锁电脑系统吗
领导:对
实习生小李:(若有所思)我试试
实现过程
实习生小李:魔镜魔镜,我们有个医疗系统,需要做一个十分钟不操作电脑,就锁定用户电脑系统的功能,在没有源代码的情况下如何实现?
魔镜:好的,在没有源代码的情况下为医疗系统添加十分钟不操作电脑就锁定用户电脑系统的功能,可以使用 sys-shim 实现。
第一步,创建一个目录例如 medical-system
,目录里有以下两个文件:
package.json
文件用来配置 sys-shimpreload.js
用来向医疗系统添加功能
第二步
在 package.json 中编写内容如下
{
"browserArguments": "--disable-web-security --allow-running-insecure-content ",
"form": {
"right": "1300",
"bottom": "800"
},
"page": "https://www.baidu.com/"
}
- browserArguments 用来指定浏览器参数,这里配置为允许跨域以方便注入代码
- form 用来控制窗口如何显示,这里表示窗口大小
- page 表示医疗系统的页面
在 preload.js 中编写内容如下
new Promise(async function () {
window.main = await new window.Sys({ log: true })
// 设置倒计时时间,为了测试方便,这里改为 30 秒
const TIMEOUT = 0.5 * 60 * 1000;
// 声明一个变量来存储 setTimeout 的引用
let timeoutId = null;
// 定义一个函数来重置倒计时并在2分钟后打印日志
function startInactivityCheck() {
// 清除之前的倒计时(如果有的话)
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
// 设置一个新的倒计时
timeoutId = setTimeout(function() {
// 锁定系统
window.main.native.sys.lock()
}, TIMEOUT);
}
// 为 body 元素添加点击事件监听器
document.body.addEventListener('click', function() {
console.log("检测到点击事件,重新开始计时。");
// 重置倒计时
startInactivityCheck();
});
// 初始化倒计时
startInactivityCheck();
})
sys.lock()
方法用于锁定操作系统。
第三步,生成应用程序
npx sys-shim pack --input medical-system
运行该命令后,会在当前目录生成一个名为 medical-system.exe 的可执行文件。它封装了医疗系统这个 web 程序,并在里面添加了锁屏功能。
pack
指定表示打包应用--input
参数表示要打包的目录
--input 参数也可以是线上的网页,比如:
npx sys-shim pack --input https://www.baidu.com/
即可获取一个可以调用操作系统 api 的 web 应用。
交付反馈
用户:以前我们还需要进入浏览器输入网址才能进入系统,现在直接在桌面上就能进入,并且还有安全锁屏功能,非常好!
领导:小李干得不错,但没有在规定的时间内完成,但由于客户反馈不错,就不扣你的考核分了。
实习生小李:(不得其解)谢谢老板。
后记
不知不觉,又到了周五,这是公司技术分享会的时候。当前公司技术人员只有实习生小李,由小李负责技术分享。
宽旷的会议室里,秘书、领导、小李三人面面相觑,小李强忍住尴尬,开始了自己的第一次技术分享:
实习生小李:感谢领导给我的工作机会,在这份工作里,我发现了 sys-shim 这个工具,它可以方便的在已有的 web 页面中添加系统 api,获取调用操作系统层面功能的能力,比如关机、锁屏。
领导:(好奇)那他可以读取电脑上的文件吗?
实习生小李:可以的,它可以直接读取电脑上的文件,例如电脑里面的文档、照片、视频等。
突然领导脸色一黑,看了一眼秘书,并关闭了正在访问的医疗系统,然后在技术分享考核表上写下潦潦草草的几个字:考核分-5
。
续集:托领导大福!前端实习生用 vue 随手写了个系统修复工具,日赚 300
提示
大家可以直接运行这个命令生成 app 体验:
npx sys-shim pack --input https://www.baidu.com/
生成后的 app 可以右键解压,看到内部结构。如果遇到问题,可以在这里提交,方便追溯,我会及时解答的。
参考
来源:juejin.cn/post/7373831659470880806
领导被我的花式console.log吸引了!直接写入公司公共库!
文章的效果,大家可以直接只用云vscode实验一下:juejin.cn/post/738875…
背景简介
这几天代码评审,领导无意中看到了我本地代码的控制台,被我花里胡哨的console打印
内容吸引了!
老板看见后,说我这东西有意思,花里胡哨的,他喜欢!
但是随即又问我,这么花里胡哨的东西,上生产会影响性能吧?我自信的说:不会,代码内有判断的,只有开发环境会打印
!
老板很满意,于是让我给其他前端同事分享一下,讲解下实现思路!最终,这个方法还被写入公司的公用utils库里,供大家使用!
console简介
console 是一个用于调试和记录信息的内置对象, 提供了多种方法,可以帮助开发者输出各种信息,进行调试和分析。
console.log()
用于输出一般信息,大家应该在熟悉不过了。
console.info() :
输出信息,与 console.log 类似,但在某些浏览器中可能有不同的样式。
console.warn() :
输出警告信息,通常会以黄色背景或带有警告图标的样式显示。
console.error() :
输出错误信息,通常会以红色背景或带有错误图标的样式显示。
console.table() :
以表格形式输出数据,适用于数组和对象。
例如:
const users = [
{ name: '石小石', age: 18 },
{ name: '刘亦菲', age: 18 }
];
console.table(users);
通过上述介绍,我们可以看出,原生的文本信息、警告信息、错误信息、数组信息打印出来的效果都很普通,辨识度不高!现在我们通过console.log来实现一些花里花哨的样式!
技术方案
console.log()
console.log() 可以接受任何类型的参数,包括字符串、数字、布尔值、对象、数组、函数等。最厉害的是,它支持占位符!
常用的占位符:
- %s - 字符串
- %d or %i - 整数
- %f - 浮点数
- %o - 对象
- %c - CSS 样式
格式化字符串
console.log() 支持类似于 C 语言 printf 函数的格式化字符串。我们可以使用占位符来插入变量值。
const name = 'Alice';
const age = 30;
console.log('Name: %s, Age: %d', name, age); // Name: Alice, Age: 30
添加样式
可以使用 %c 占位符添加 CSS 样式,使输出内容更加美观。
console.log('%c This is a styled message', 'color: red; font-size: 20px;');
自定义样式的实现,其实主要是靠%c 占位符添加 CSS 样式实现的!
实现美化的信息打印
基础信息打印
我们创建一个prettyLog方法,用于逻辑编写
// 美化打印实现方法
const prettyLog = () => {
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
console.log(
`%c ${title} %c ${text} %c`,
`background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
`border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
'background:transparent'
);
};
// 基础信息打印
const info = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Info' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#909399');
};
return {
info
};
};
上述代码定义了一个 prettyLog 函数,用于美化打印信息到控制台。通过自定义样式,输出信息以更易读和美观的格式呈现。
我们使用一下看看效果
// 创建打印对象
const log = prettyLog();
// 不带标题
log.info('这是基础信息!');
//带标题
log.info('注意看', '这是个男人叫小帅!');
info 方法用于输出信息级别的日志。它接受两个参数:textOrTitle 和 content。如果只提供一个参数,则视为内容并设置默认标题为 Info;如果提供两个参数,则第一个参数为标题,第二个参数为内容。最后调用 prettyPrint 方法进行输出。
错误信息打印
const prettyLog = () => {
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
// ...
};
const info = (textOrTitle: string, content = '') => {
// ...
};
const error = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Error' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#F56C6C');
};
// retu;
return {
info,
error,
};
};
// 创建打印对象
const log = prettyLog();
log.error('奥德彪', '出来的时候穷 生活总是让我穷 所以现在还是穷。');
log.error('前方的路看似很危险,实际一点也不安全。');
成功信息与警告信息打印
// 美化打印实现方法
const prettyLog = () => {
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
console.log(
`%c ${title} %c ${text} %c`,
`background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
`border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
'background:transparent'
);
};
const info = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Info' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#909399');
};
const error = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Error' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#F56C6C');
};
const warning = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Warning' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#E6A23C');
};
const success = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Success ' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#67C23A');
};
// retu;
return {
info,
error,
warning,
success
};
};
// 创建打印对象
const log = prettyLog();
log.warning('奥德彪', '我并非无路可走 我还有死路一条! ');
log.success('奥德彪', '钱没了可以再赚,良心没了便可以赚的更多。 ');
实现图片打印
// 美化打印实现方法
const prettyLog = () => {
// ....
const picture = (url: string, scale = 1) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const c = document.createElement('canvas');
const ctx = c.getContext('2d');
if (ctx) {
c.width = img.width;
c.height = img.height;
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, c.width, c.height);
ctx.drawImage(img, 0, 0);
const dataUri = c.toDataURL('image/png');
console.log(
`%c sup?`,
`font-size: 1px;
padding: ${Math.floor((img.height * scale) / 2)}px ${Math.floor((img.width * scale) / 2)}px;
background-image: url(${dataUri});
background-repeat: no-repeat;
background-size: ${img.width * scale}px ${img.height * scale}px;
color: transparent;
`
);
}
};
img.src = url;
};
return {
info,
error,
warning,
success,
picture
};
}
// 创建打印对象
const log = prettyLog();
log.picture('https://nimg.ws.126.net/?url=http%3A%2F%2Fdingyue.ws.126.net%2F2024%2F0514%2Fd0ea93ebj00sdgx56001xd200u000gtg00hz00a2.jpg&thumbnail=660x2147483647&quality=80&type=jpg');
上述代码参考了其他文章:Just a moment...
url可以传支持 base64,如果是url链接,图片链接则必须开启了跨域访问才能打印
实现美化的数组打印
打印对象或者数组,其实用原生的console.table比较好
const data = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 }
];
console.table(data);
当然,我们也可以伪实现
const table = () => {
const data = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 }
];
console.log(
'%c id%c name%c age',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;'
);
data.forEach((row: any) => {
console.log(
`%c ${row.id} %c ${row.name} %c ${row.age} `,
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;'
);
});
};
但是,我们无法控制表格的宽度,因此,这个方法不太好用,不如原生。
仅在开发环境使用
// 美化打印实现方法
const prettyLog = () => {
//判断是否生产环境
const isProduction = import.meta.env.MODE === 'production';
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
if (isProduction) return;
// ...
};
// ...
const picture = (url: string, scale = 1) => {
if (isProduction) return;
// ...
};
// retu;
return {
info,
error,
warning,
success,
picture,
table
};
};
我们可以通过import.meta.env.MODE 判断当前环境是否为生产环境,在生产环境,我们可以禁用信息打印!
完整代码
// 美化打印实现方法
const prettyLog = () => {
const isProduction = import.meta.env.MODE === 'production';
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
if (isProduction) return;
console.log(
`%c ${title} %c ${text} %c`,
`background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
`border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
'background:transparent'
);
};
const info = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Info' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#909399');
};
const error = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Error' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#F56C6C');
};
const warning = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Warning' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#E6A23C');
};
const success = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Success ' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#67C23A');
};
const table = () => {
const data = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 }
];
console.log(
'%c id%c name%c age',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;'
);
data.forEach((row: any) => {
console.log(
`%c ${row.id} %c ${row.name} %c ${row.age} `,
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;'
);
});
};
const picture = (url: string, scale = 1) => {
if (isProduction) return;
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const c = document.createElement('canvas');
const ctx = c.getContext('2d');
if (ctx) {
c.width = img.width;
c.height = img.height;
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, c.width, c.height);
ctx.drawImage(img, 0, 0);
const dataUri = c.toDataURL('image/png');
console.log(
`%c sup?`,
`font-size: 1px;
padding: ${Math.floor((img.height * scale) / 2)}px ${Math.floor((img.width * scale) / 2)}px;
background-image: url(${dataUri});
background-repeat: no-repeat;
background-size: ${img.width * scale}px ${img.height * scale}px;
color: transparent;
`
);
}
};
img.src = url;
};
// retu;
return {
info,
error,
warning,
success,
picture,
table
};
};
// 创建打印对象
const log = prettyLog();
来源:juejin.cn/post/7371716384847364147
听说去哪儿混合办公了? 聊聊程序员如何找到远程工作和好处
哈喽大家好,这两天看到去哪儿开始混合办公了,作为远程工作的支持者我表示很开心,终于有大厂全面开始支持远程员工,去哪儿的邮件截图是这么说的
去年开始现在一些团队做小范围的尝试,好评如潮,有的同学利用通勤的时间减肥,有的同学可以回家陪家人,家人对工作的支持度变高,而且工作效率一点都没下降,所以去哪儿在7月开始,每周三周五可以居家办公,无需申请
远程的好处非常明显,尤其是在一线城市,通勤的一个半小时就省下了,生活幸福度直线提高,你出去旅游周四就可以出发,如果五天都可以居家就是完全远程,你可以回老家省会还有北京一线的收入,老板节省下了组办公室的费用
因为远程无论是对员工满意度,还是老板的成本控制都很友好,混合或者远程办公在海外已经比较流行了,基本海外所有的招聘网站都有是否远程的选项,你可以过滤只看支持远程的,你搜工作就有一个选项是坐班,混合还是远程的,不像国内boss直聘,默认都是要通勤的,哪怕跟我说支持远程,也是只是面试可以远程
比如英国这边混合办公室基本操作,好一些的就会全员远程,比如这边大厂,Meta是每周去两天还是三天,我给忘了,我觉得国内以后支持远程的会越来越多,越来越多的小老板会抛弃自己奴隶主的思想,必须得盯着你干活,反而会考虑更真实的办公室成本
对远程最大的批评就是会降低工作效率,所谓的见面沟通效率才是最高的,确实有一些场景面对面效率最高,但是扪心自问,你现在开会的效率真的高吗,大公司动辄就一小时的会议,而且你首先就在通勤上浪费了一小时,你一天的效率可能很高吗 ,腾讯会议的AI总结功能比以前人工写的会议纪要不知道好多少倍
而一个人能有更多的时间照顾家庭后,闲暇时间才会产生创意,尝试新的工具和沟通方式等等,可能会让效率有非常大的提高
远程我就可以工作的同事带家人出游,远程久了你换工作就会只考虑远程的,再也不想挤地铁了,虽然现在通勤还是主流,尤其是还有马斯克这种非常反对远程的人,但是我最近聊的一些创业公司和小公司,基本都支持混合办公的
说了这么多远程办公的好处,喜欢工作和生活平衡的你可能已经蠢蠢欲动了,那远 程工作需要哪些能力呢,以及如何找到一个远程工作呢
其实远程工作有很多最佳实践,比较典型的有37signals这家公司,这家公司坚持小而美,写出来ruby on rails这种框架,有一套书叫重来,有三本,第二本就叫remote,比较系统的介绍远程工作文化的方方面面
包括远程工作的好处,可以逃离大城市的房价,更好的work life balance,更自在的生活,还反驳了一些对远程文化的批评,比如觉得坐在一起才能效率高,家里干扰大等等
更重要的介绍了如何更好的远程协作,这是需要学习的技能,比如如何可视化你的工作进度,高效的沟通,怎么管理远程员工的效率,还有远程人如何更好的生活,非常推荐
还有就是有一个公司叫gitlab,这个更厉害,这是一家美国上市公司,应该是第一个招股书里没有办公地址的,从老板到实习生,全员都散落在世界各地办公,关于大型公司如何实践远程文化,他们有一个专门的文档,质量非常高,主要是关于如何管理远程团队,还有很重要的远程开会技巧
地址和上面重来的电子书,评论区好像没法发链接,要不加我吧,我研究下怎么发给大家
那最后如何找到一个远程工作呢,其实之前我也分享过,这里简单总结下
首先程序员是非常适合远程的,所有的代码任务都可以在线完成,通过git管理代码,腾讯会议或者zoom开会,飞书钉钉slack等工作聊天等等
就像前面所述国内远程机会比较少,而且很多国内的老板哪怕远程也依然是监控心态,比如要求你开摄像头或者响应速度也挺难受,所以我觉得比如你想明年找个远程,那现在就优先学英语,程序员怎么学英语大家感兴趣以后可以专门聊,大概方法就是不要学英语,而是用英语学习编程就可以了
希望大家都能尝试和探索混合或者远程工作的新体验,能够拥有一个更加自在的职场,在努力工作的同时,可以有时间陪家人探索世界
来源:juejin.cn/post/7392116075674927131
同为情怀程序员,给博客园提供几个救园思路
博客园是老牌的技术社区,近来因为各种原因,导致社区运营岌岌可危。
笔者也是一个情怀型程序员,但是在这几年的创业生涯中,也摸爬滚打,养成了一些商业思路。
这个世界,就是一个肉弱强食,尔虞我诈的,不要做一个纯粹的情怀主义者,也不要做一个老实人。
那么下面呢,基于笔者的认知和思路,给博客园提供一些救园思路。
降本增效
个人认为,这是非常核心,非常重要的问题。
收入骤减,那么我们的开销成本,也要跟着降低,才能维持更久的运转。
比如关停不必要的网站功能,减少服务器的运行开支。
作为一个技术社区,主要的金钱投入,除了人员开支,办公场地之外,应该就是服务器的运行成本了吧。
那么相应的,人员开支,办公费用,场地,该减少的减少,该换场地的就换场地。
这是降低成本,控制支出最直观的方式了。
那么接下来呢,就是如何提高收入了。
外包接单
这是博客园正在做的,当时我看到官方提成只拿5%(原文我没找到了,如有错漏,请指正),这个提成,太低了。
我自己也做接单撮合,运营2-3个月,撮合成功30-40单,平台提成是15%。
那么这个提成,市场行情是多少呢?20%-30%。
5%,说实话,太少了。
接单撮合,有一个问题非常重要:接单,客户,技术,三方都要能赚到钱
。
你拿5%,唉,还是程序员思维,还是在做情怀。
不要这样,商业就是商业,程序员作为最终实施的一方,确实出力最多,最苦,最累。
平台少拿点,给技术多一点,非常好,但是,我的个人建议是不能低于15%,为什么呢?
如果有额外的介绍人,可以给介绍人5%或一半(7.5%),这样才有余地。
如果平台赚不到钱,那么如何运营好接单撮合这个项目呢?
广告接入
这个问题,在博客园7月15日的文章中已经说明了。
我的感觉是,令人肃然起敬,但是违背社会规律。
可以这么说,为了博客园的存亡问题,至少90%以上的用户,是不反感广告的。
但是,目前来看,似乎只有网站的运营者反对广告。。。
只要不做成某C开头的社区,看个电视剧,广告比剧集还长。
所以,我突然发现,这一整个事件的根本原因是什么?居然是人的执念。。可怕。
凭博客园的流量,接入广告,分分钟救园,大家又能愉快地玩耍。
不要考验人性
笔者曾经说过一句话,在网上广为流传。
博客园是一个程序员社区。
通过卖社区周边产品,还是通过情怀变现,下策,下策啊!
既然要商业化,那就不要过多考虑情怀,要从商业的角度去思考问题。
所谓的商业,并不是变成一个尔虞我诈的商人,有太多的既盈利,又给用户带来有价值,有意义的参考,大家可以想想有哪些。
而且,不管是会员,还是周边,这根本不是一个“我尽心尽力服务多年,当我落难的时候,大家挺身而出
”的问题。
大家受益于博客园,博客园也因为所有用户而收益,这是一个相互作用。
绝大多数用户,仅仅只是看客,真正写文章,在博客园出人头地的,少之又少。
社会和技术的发展,日新月异,技术内容,也远不及当年那么必需。
周边也不是必需,会员也是一次性的,根本不具备持续性。
救了这次,下次呢?所以还得从长计议。
产品推广分成
现在,有各种“严选”,“甄选”,“优品”。
博客园也可以做严选,甄选产品。
通过合作的模式,降低产品价格,获得产品提成,也不失为一个好地方方式。
比如,某某软件,某某产品,以低于官网价格的拿货价,平台推广卖给光大社区用户。
这样,用户购买价格更低,产品方销量更好,博客园平台也赚到了钱,也是一个三方共赢的方案。
比卖会员,卖周边好。
这种方式,选品品类更多,更接近用户刚需。
甚至可以发起投票,让用户选择拼团购买什么产品,软件,工具。
既可以积累社区凝聚力,也能让用户买到真正的,需要的东西,平台也赚到了钱。
舆论维护
要倾听用户的声音,目前技术圈,对博客园的这段时间运营情况,可以说非常不满。
鱼皮的这篇文章我觉得就说得不错,这是具备真正生意头脑的创业者思路。
如果一意孤行,还是程序员的传统思路,最终很可能连口碑也坏了,三思。
网站出售
最后呢,当然,是最坏的情况,万不得已,也可以卖掉博客园,获得一笔不菲的收入。
当然,这是下下之策。
希望博客园可以挺过这次难关,不论如何,这么有情怀的社区,真的不多见了。
如果它真的消失了,那将是技术圈的重大损失。
来源:juejin.cn/post/7392071328520994816
fabric.js实战
一、业务需求
- 给定一个图片作为参考
- 可配置画笔颜色、粗细
- 可通过手势生成实际轨迹
- 可通过手势来圈选轨迹
- 可撤销、删除轨迹
- 可操作轨迹
- 可缩放、拖动
- 背景网格,且背景网格可缩放和拖动
- 参考图片可缩放、偏移
- 手势处理系统,单指绘制、双指缩放、三指拖动
- 需要一个禁止绘制区域,方便用户在平板上有手掌的支撑区域
二、技术选型
因为涉及到轨迹
、操作轨迹
两点,
svg无法满足大量且复杂的轨迹,canvas没法操作轨迹,所以从 fabricjs/konva 中选型,
因 fabricjs 使用人数更多,所以采取了其作为技术选型。
三、fabric 原理
- 通过其内置的几何对象来创建图形
- 维护一个对象树
- 将对象树通过 canvas-api 绘制在实际的 canvas 上
因此,fabric 能做非常多的优化手段
- 已渲染的节点可通过
子canvas
做缓存 - 对比新旧对象树能做差值更新
- 虚拟画布,类似于虚拟滚动,只渲染可视化的区域
四、模块拆分
- header:与业务相关
- toolBar:工具栏
- sidebar:与业务相关
- canvas:绘制画板
五、架构设计
六、问题收集
6.1 性能问题
- 圈选是实时的,即判断一个多边形是否相交于或包含于一条复杂轨迹,因为使用了射线法,当遇到大量轨迹的时候,可能会卡顿。目前做了多重优化手段,比如函数节流、先稀疏复杂轨迹的点、然后判断图形的占位区域是否相交、然后判断图形的线段之间是否相交、然后判断是否包含关系。
- 轨迹的实时生成,在一长串touchmove事件中,使用一个初始化的polyline,后续更改其点集,这样只需要实例化一个对象,性能高。
- touchmove回调里执行复杂的逻辑,这会阻塞touchmove的触发频率,我们将touchmove里的回调通过settimeout放到异步队列中,这样就剥离了touchmove
事件层
和 回调函数处理层
,这样touchmove的触发频率就不会被影响
6.2 禁止绘制区域
该需求无法实现,因为当我们手掌放在平板上时,会触发系统级别的误触识别算法,阻止所有的触摸事件,所以我们没办法在页面上实现该功能。
touch事件
一个屏幕上可以有多个touch触摸点,这些触摸点绑定的target是可以多个的
touchEvent对象有如下重要属性
- targetTouches:只在当前target(比如某div)上触发的touch触摸点
- touches:在屏幕上触发的所有touch触目点
特别注意点:
- 没有类似鼠标的mouseout事件,所以你一开始是点在div上的,然后移出div的范围后,依旧触发touchmove事件
平板调试手段-chrome浏览器
- 平板安装chrome移动版
- 电脑安装chrome + chrome插件:inspect devices
- 平板开启开发者选项,然后允许usb调试
- usb链接平板和chrome
- 平板和电脑都打开chrome
- 电脑启动插件,然后就能控制平板的chrome,并且对该chrome访问的网页进行调试
- 很好用哇
来源:juejin.cn/post/7278931998650744869
实现小红书响应式瀑布流
前言
瀑布流布局,不管是在pc端还是手机端都很常见,但是我们通常都是列固定。今天来实现一下小红书的响应式瀑布流。后面有仓库地址。
正文
还是先来看看效果
原理:
对每一个item都使用绝对定位,left和top都是0,最后根据容器大小、item的height通过计算来确定item的transform值
接下来从易到难来解析一下实现
初始化数据
列表怎么可以没有数据,先来初始化一下数据
确定列数及列大小
由于是响应式,我们要去监听列表容器的大小变化并记录容器宽度,这样才能做出相应的处理
根据监听得到的容器大小信息,我们可以确定每行个数
和每一个item的宽度
确定列表中item位置
确定item的位置,那么我们只需要确定transform
值就可以了,这也是整个实现的核心。我们还需要解决几个问题
- 对还不知道item的高度,怎么确定
- 我们希望把新的item放置在最低高度的旧item下方,这样全部渲染完每一列的高度才不会相差很多。
item放置的原理图,放置在当前最低高度的下面
更新item高度
当我们第一次运行的时候,每一个item的高度一定都是随机生成的,现在我们要确定item的实际高度。在这里我们还可优化一下,使用懒加载和底部加载,提升性能
。这两个在这里就不讲了,不懂的可以去搜一下。
下面代码一共两个作用
- 记录容器滚动值,传递给每一个
item
,用于判断是否加载图片。 - 判断是否请求添加数据
根据滚动值判断是否加载图片,加载图片后触发父亲更新高度函数
父亲接受到新的高度并更新高度,然后去重新计算transform
值和item高度
完整代码
结语
感兴趣的可以去试试
来源:juejin.cn/post/7270160291411886132
明明 3 行代码即可轻松实现,Promise 为何非要加塞新方法?
给前端以福利,给编程以复利。大家好,我是大家的林语冰。
00. 观前须知
地球人都知道,JS 中的异步编程是 单线程 的,和其他多线程语言的三观一龙一猪。因此,虽然其他语言的异步模式彼此互通有无,但对 JS 并不友好,比如 Actor 模型等。
这并不是说 JS 被异步社区孤立了,只是因为 JS 天生和多线程八字不合。你知道的,要求 JS 使用多线程,就像要求香菜恐惧症患者吃香菜一样离谱。本质上而言,这是刻在 JS 单线程 DNA 里的先天基因,直接决定了 JS 的“异步性状”。有趣的是,如今 JS 也变异出若干多线程的使用场景,只是比较非主流。
ES6 之后,JS 的异步编程主要基于 Promise
设计,比如人气爆棚的 fetch
API 等。因此,最新的 ES2024 功能里,又双叒叕往 Promise
加塞了新型静态方法 Promise.withResolvers()
,也就见怪不怪了。
问题在于,我发现这个新方法居然只要 3 行代码就能实现!奥卡姆剃刀原则告诉我们, 若无必要,勿增实体。那么这个鸡肋的新方法是否违背了奥卡姆剃刀原则呢?我决定先质疑、再质疑。
当然,作为应试教育的漏网之鱼,我很擅长批判性思考,不会被第一印象 PUA。经过三天三夜的刻意练习,机智如我发现新方法果然深藏不露。所以,本期我们就一起来深度学习 Promise
新方法的技术细节。
01. 静态工厂方法
Promise.withResolvers()
源自 tc39/proposal-promise-with-resolvers
提案,是 Promise
类新增的一个 静态工厂方法。
静态的意思是,该方法通过 Promise
类调用,而不是通过实例对象调用。工厂的意思是,我们可以使用该方法生成一个 Promise
实例,而无须求助于传统的构造函数 + new
实例化。
可以看到,这类似于 Promise.resolve()
等语法糖。区别在于,传统构造函数实例化的对象状态可能不太直观,而这里的 promise
显然处于待定状态,此外还“买一送二”,额外附赠一对用于改变 promise
状态的“变态函数” —— resolve()
和 reject()
。
ES2024 之后,该方法可以作为一道简单的异步笔试题 —— 请你在一杯泡面的时间里,实现一下 Promise.withResolvers()
。
如果你是我的粉丝,根本不慌,因为新方法的基本原理并不复杂,参考我下面的实现,简单给面试官表演一下就欧了。
可以看到,这个静态工厂方法的实现难点在于,如何巧妙地将变态函数暴露到外部作用域,其实核心逻辑压缩后有且仅有 3 行代码。
这就引发了本文开头的质疑:新方法是否多此一举?难道负责 JS 标准化的 tc39 委员会也有绩效考核,还是确实存在某些不为人知的极端情况?
02. 技术细节
通过对新方法进行苏格拉底式的“灵魂拷问”和三天三夜的深度学习,我可以很有把握地说,没人比我更懂它。
首先,与传统的构造函数实例化不同,新方法支持无参构造,我们不需要在调用时传递任何参数。
可以看到,构造函数实例化要求传递一个执行器回调,偷懒不传则直接报错,无法顺利实例化。
其次,变态函数的设计更加自由。
可以看到,传统的构造函数中,变态函数能且仅能作为局部变量使用,无法在构造函数外部调用。而新方法同时返回实例及其变态函数,这意味着实例和变态函数处于同一级别的作用域。
那么,这个设计上的小细节有何黑科技呢?
假设我们想要一个 Promise
实例,但尚未知晓异步任务的所有细节,我们期望先将变态函数抽离出来,再根据业务逻辑灵活调用,请问阁下如何应对?
ES2024 之前,我们可以通过 作用域提升 来“曲线救国”,举个栗子:
可以看到,这种方案的优势在于,诉诸作用域提升,我们不必把所有猫猫放在一个薛定谔的容器里,在构造函数中封装一大坨“代码屎山”;其次,变态函数不被限制在构造函数内部,随时随地任你调用。
该方案的缺陷则在于,某些社区规范鼓励“const
优先”的代码风格,即 const
声明优先,再按需修改为 let
声明。
这里的变态函数被迫使用 let
声明,这意味着存在被愣头青意外重写的隐患,但为了缓存赋值,我们一开始就不能使用 const
声明。从防御式编程的角度,这可能不太鲁棒。
因此,Promise.withResolvers()
应运而生,该静态工厂方法允许我们:
- 无参构造
const
优先- 自由变态
03. 设计动机
在某些需要封装 Promise
风格的场景中,新方法还能减少回调函数的嵌套,我把这种代码风格上的优化称为“去回调化”。
举个栗子,我们可以把 Node 中回调风格的 API 转换为 Promise
风格,以 fs
模块为例:
可以看到,由于使用了传统的构造函数实例化,在封装 readFile()
的时候,我们被迫将其嵌套在构造函数内部。
现在,我们可以使用新方法来“去回调化”。
可以看到,传统构造函数嵌套的一层回调函数就无了,整体实现更加扁平,减肥成功!
粉丝请注意,很多 Node API 现在也内置了 Promise
版本,现实开发中不需要我们手动封装,开箱即用就欧了。但是这种封装技巧是通用的。
举个栗子,瞄一眼 MDN 电子书搬运过来的一个更复杂的用例,将 Node 可读流转换为异步可迭代对象。
可以看到,井然有序的代码中透露着一丝无法形容的优雅。我脑补了一下如何使用传统构造函数来实现上述功能,现在还没缓过来......
04. 高潮总结
从历史来看,Promise.withResolvers()
并非首创,bluebird 的 Promise.defer()
或 jQuery 的 $.defer()
等库就提供了同款功能,ES2024 只是换了个名字“新瓶装旧酒”,将其标准化为内置功能。
但是,Promise.withResolvers()
的标准化势在必行,比如 Vite 源码中就自己手动封装了同款功能。
无独有偶,Axios、Vue、TS、React 等也都在源码内部“反复造轮子”,像这种回头率超高的代码片段我们称之为 boilerplate code(样板代码)。
重复乃编程之大忌,既然大家都要写,不如大家都别写,让 JS 自己写,牺牲小我,成全大家。编程里的 DRY 原则就是让我们不要重复,因为很多 bug 就是重复导致的,而且不好统一管理和维护,《ES6 标准入门教程》科普的 魔术字符串 就是其中一种反模式。
兼容性方面,我也做过临床测试了,主流浏览器广泛支持。
总之,Promise.withResolvers()
通过将样板代码标准化,达到了消除重复的目的,原生实现除了性能更好,是一个性价比较高的静态工厂方法。
参考文献
- GitHub:github.com/tc39/propos…
- MDN:developer.mozilla.org/en-US/docs/…
- bluebird:bluebirdjs.com/docs/deprec…
粉丝互动
本期话题是:你觉得新方法好评几颗星,为什么?你可以在本文下方自由言论,文明科普。
欢迎持续关注“前端俱乐部”,给前端以福利,给编程以复利。
坚持阅读的小伙伴可以给自己点赞!谢谢大家的点赞,掰掰~
来源:juejin.cn/post/7391745629876469760
显示器分辨率的小知识
数字化时代,显示器是我们日常生活和工作中不可或缺的一部分。
无论是在电脑、手机、平板还是电视上,我们都依赖显示器来呈现图像和文字。然而,对于许多人来说,显示器分辨率这一概念可能并不十分清晰。
分辨率是影响显示器性能和视觉效果的关键因素之一。它决定了图像的清晰度、细节和整体观感。
因此,了解显示器分辨率的知识对于我们选择和使用显示设备至关重要。
本文将向大家介绍关于显示器分辨率的一些小知识,希望能够帮助大家更好地选择合适的显示器,并提升在使用显示设备时的体验。
1. 常用的分辨率
代号 | 分辨率 | 备注 |
---|---|---|
720p | 1280 x 720 | 也被称为 HD,高清 |
1080p | 1920 x 1080 | 也被称为 FULL HD,全高清 |
1440p | 2560 x 1440 | 也被称为 QHD,Quad HD |
2160p | 3840 x 2160 | 也被称为 4K |
4320p | 7680 x 4320 | 也被称为 8K |
2. 一些术语
关于显示器,最常见的三个术语就是:
2.1. 刷新率
刷新率(Refresh rate
)是指屏幕硬件每秒刷新以显示图像的速率,通常以赫兹(Hz
)为单位。
刷新率是指显示器的能力,简单理解就是每秒屏幕能切换多少个图像。
刷新率越高的显示器,显示的视频越流畅。
不过,由于人眼有视觉暂留的能力,一般60Hz左右的液晶屏已经很流畅了。
2.2. 帧速率
帧速率(Frame rate
)是指视频或游戏每秒传输的图像帧数,通常以FPS
(每秒帧数)为单位。
帧速率一般取决于视频或者游戏本身,与显示器关系不大。
帧速率越高,视频和游戏的清晰度和流畅度越高,当然,占用的硬盘空间也越大,对显卡要求也越高。
在实际使用中,如果帧速率高于刷新率,可能会出现屏幕撕裂等现象,因为显示器无法完全跟上图像的更新速度。
而如果刷新率高于帧速率的话,对显示影响不大,但是对显示器来说,有点大材小用。
因此,刷新率和帧速率的匹配和协调对于获得最佳的视觉体验,以及购买显示器时考虑性价比至关重要。
2.3. 纵横比
纵横比(Aspect ratio
)概念比较简单,是指水平像素数与垂直像素数的比率。
对于视频和游戏,一般可以调节输出的纵横比;对于显示器,也可以通过调节像素来显示不同的纵横比。
视频或游戏与显示器的纵横比匹配的时候,显示效果最佳,图像不会变形。
这也是为什么很多视频在手机上竖屏看的时候,只会集中在中间显示,上下很多部分都是黑屏,
就是因为视频的纵横比在竖屏上的纵横比不匹配,只能缩小在中间那部分显示。
换成横屏观看,视频才能完全展开。
3. 容易混淆的概念
关于纵横比和刷新,有2个概念可能我们平时容易混淆。
3.1. 4:3 和 16:9
这两种纵横比常常被误会成差不多,甚至是一样的,但是细算起来,它们的差距还挺大。
对于4:3 的纵横比意味着图像中每 4 个宽度单位就有 3 个高度单位,
最终显示出来,屏幕宽度比长度增加了 33%。
而16:9 的纵横比意味着图像中每 16 个宽度单位就有 9 个高度单位,
最终显示出来,屏幕宽度比长度增加了 78%。
3.2. 1080i 和 1080p
i 代表隔行扫描,通过照亮屏幕上的奇数像素和偶数像素,然后将它们的结果拼接在一起以获得最终图像,容易闪烁。
p 代表逐行扫描,对图像以逐行平滑的方式拼接,有效防止屏幕闪烁。
1080p比1080i的显示效果更加的清晰和细腻,因为1080p
是后来改进的技术,现在的显示器用 i 的方式已经不多了。
来源:juejin.cn/post/7302268383315148827
关于我在HarmonyOS中越陷越深这件事...
前言
上次发文已是2023年,在上一篇 前端的春天!拥抱HarmonyOS4.0🤗 - 掘金 (juejin.cn)一文中我介绍了一些鸿蒙OS知识,此文一出大家的看法也层出不穷,笔者持开放的态度对待大家对于新生态的看法。在2024年的今天,我想来说说这几个月我有哪些思考和行动。
在短短几个月的时间里,HarmonyOS已经来到了Next版本,迎来属于鸿蒙的春天。俗话说光说不练假把式,实践是试金石,我深知在做开发的这一行只有不断试错,反复的验证,才能创造新的轮子,创造力一个人无法被替代的根本。
我写这篇文章的目的不在于极力推荐大家去学习这项技术,更多的是以一个求学者的角度去阐述自己对新技术学习的心路历程。
为什么学习鸿蒙?
迷茫
笔者是25届的学生,对于学生来说最多的是时间和学习热情,自己也曾经经历过一段时间的专业方向选择困惑期,或许当人越迷茫的时候越容易听信别人的话吧,好与坏是相对的,分人也分时间,在合适的时间选择做了合适的事情这就没有什么问题了,至少学习鸿蒙这件事情对我来说,无论将来何时都会让我记忆犹新。
渴望
在学校里老师会告诉你成熟的解决方案,会告诉你应该这样做,不应该那样做,你仿佛一个机器人,进行一些机械系的学习,时间太急,急到我们只能应付相对的课程考试与学习,内容太多,多到我们最后仅靠老师给出的精简知识点去实际开发项目。这显然不是我想要的学习方式和结果...
动力源泉
有人说:这不就是Vue、React、Flutter、就是个缝合怪....
面对互联网高速发展的今天,各家博采众长,相互吸收优秀的开发思想已不是一件新鲜的事情了。
我自己学习的方向是大前端,加上之前开发的项目都是web的与小程序相关的,自己一直想尝试结合之前开发的项目开发一个基于HarmonyOS的App,听到“一次开发多端部署”这句话让我眼前一亮(很可惜这里的多端部署在4.0的开放版本是不支持的)。在接触鸿蒙的第一天,犹如我第一次接触前端开发,那种所见即所得的开发体验让我从内心里竟有了一丝“自信”,但也恰恰是这种“自信”也逐渐将我推入了深渊
坎坷与前行
在我真正尝试开发一个鸿蒙App的时候是在2023年底,我希望通过我所学的东西去做一个完整的东西并参加 2024年的计算机设计大赛。
在十二月份的那几周,我不断的使用Figma进行原型的绘制,与指导老师探讨功能、确定交互逻辑,期间我也参考了大量的App类设计准则,最后发现鸿蒙的ArkUI是具有工业审美的(至少是符合我的想法),这使得我不必耗费太多的精力在从0到1的去做一些组件,仅需适配设计规范上所涉及到的即可,将更多的精力放在逻辑的完整性。
在开发过程中遇到了各种形形色色的问题,例如:http请求封装upload组件无法拿到回调、地图功能无法使用的解决方案、websocket连接不上、创建时间、地理位置编码......
所幸所有问题都有解决办法,只是过程真的很痛苦,反复尝试、不断验证,我很喜欢在夜晚写代码,天空越黑星星越亮,当空气都变得安静时,我的内心反而会激发一种向上的力量来支撑我,可能是因为自己太想进步了吧(hhhh),在开发的App的日子里,每天都很崩溃,但是我的老师、朋友也都在鼓励我,我又不太想都付出这么多了又轻易放弃......
在2024年的4月,我去看了武汉的樱花,距离比截止还有七天不到,因为我实在撑不住了,在这个时间点,与其逼自己一把,不如放自己一马,于是和朋友相约武汉一起赏樱......
在五月,我得知自己的鸿蒙原生应用拿到了省一等奖,内心是非常激动的,但同时有一些失落的是,我无缘继续参与今年七月的国赛,因为赛制名额原因,我无法被上推。
比赛结束后,我开始准备投递简历实习,但最终都石沉大海,行业现状让我十分焦虑,我时常觉得自己能力不足......
星河璀璨,紧接着HarmonyOS Next 正式面世,我内心不断在问自己,难道我就所有的努力都要止步于此了吗?
....
学习现状
六月我的一位学习伙伴邀请我和他们一起开发研究院的一款基于ArkUI-X的软件,这将对我来说是一个非常宝贵的机会,几个月的时间,兜兜转转回到了我梦开始的地方......
Harmony Next 正式beta发布已过去半个多月了,这期间我了解到了很多之前没有学习到的新东西,鸿蒙提供了3w+的api,这些api有什么用?我打个比方,你要做满汉全席首先得要食材,其次需要烹饪技巧。而鸿蒙他会为你提供所需的所有食材,但是,你要做松鼠桂鱼还是佛跳墙,完全取决于你自己!至于烹饪技巧,鸿蒙开设了相关的做菜视频,你可以从中学习。
笔者也看到了许多鸿蒙原生开发者,一起交流关于鸿蒙的技术问题,在这里我附上一个宝藏鸿蒙优秀案例仓库
HarmonyOS NEXT应用开发案例集
感悟
真正的强大不是对抗,而是允许和接受,接纳挫折,接纳无常,接纳情绪,接纳不同,当你允许一切发生之后,就会不再那么尖锐,会渐渐变得柔和。
Per aspera ad astra. 没有人能熄灭满天星光,鸿蒙让我见证了从星光微微到星河璀璨,它教会我的不是一项技术,更多的是教会我如何去解决问题,去思考问题,当问题没有解决方案的时候,是否自己能够结合现有资源去提出自己的想法,并不断进行验证与总结。
路上会有风
会有浪漫
会有悲伤
会有孤独
也会有无尽的星辰与希望
来源:juejin.cn/post/7390956576180109312
原来Optional用起来这么清爽!
前言
大家好,我是捡田螺的小男孩。
最近在项目中,看到一段很优雅的代码,用Optional 来判空的。我贴出来给大家看看:
//遍历打印 userInfoList
for (UserInfo userInfo : Optional.ofNullable(userInfoList)
.orElse(new ArrayList<>())) {
//print userInfo
}
这段代码因为Optional的存在,优雅了很多,因为userInfoList
可能为null,我们通常的做法,是先判断不为空,再遍历:
if (!CollectionUtils.isEmpty(userInfoList)) {
for (UserInfo userInfo:userInfoList) {
//print userInfo
}
}
显然,Optional让我们的判空更加优雅啦、
- 关注公众号:捡田螺的小男孩(很多后端干货文章)
1. 没有Optional,传统的判空?
如果只有上面这一个例子的话,大家会不会觉得有点意犹未尽呀。那行,田螺哥再来一个。
假设有一个订单信息类,它有个地址属性。
要获取订单地址的城市,会有这样的代码:
String city = orderInfo.getAddress().getCity();
这块代码会有啥问题呢?是的,可能报空指针问题!为了解决空指针问题,一般我们可以这样处理:
if (orderInfo != null) {
Address address = orderInfo.getAddress();
if (address != null) {
String city = address.getCity();
}
}
这种写法显然有点丑陋。为了更加优雅一点,我们可以使用Optional
String city = Optional.ofNullable(orderInfo)
.map(Order::getAddress)
.map(Address::getCity)
.orElseThrow(() ->
new IllegalStateException("OrderInfo or Address is null"));
这样是不是优雅一点,好了这例子也介绍完了。你们知道,田螺哥很细的。当然,是指写文章很细哈
有些伙伴,可能第一眼看那个Optional
优化后的代码有点生疏。因此,接下来,给介绍Optional
相关API
。
2. Optional API简介
2.1 ofNullable(T value)、empty()、of(T value)
因为我们上面的例子,使用到了 Optional.ofNullable(T value)
,第一个函数就讲它啦。源码如下:
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
如果value
为null,就返回 empty()
,否则返回 of(value)
函数。接下来,我们看Optional的empty()
和 of(value)
函数
public final class Optional<T> {
private static final Optional<?> EMPTY = new Optional<>();
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
显然, empty()
函数的作用就是返回EMPTY
对象。
而of(value)
函数会返回Optional的构造函数
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
对于 Optional的构造函数:
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
- 当value值为空时,会报
NullPointerException
。 - 当value值不为空时,能正常构造
Optional
对象。
2.2 orElseThrow(Supplier<? extends X> exceptionSupplier)、orElse(T other) 、orElseGet(Supplier<? extends T> other)
上面的例子,我们用到了orElseThrow
.orElseThrow(() -> new IllegalStateException("OrderInfo or Address is null"));
那我们先来介绍一下它吧:
public final class Optional<T> {
private final T value;
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
很简单就是,如果value
不为null
,就返回value
,否则,抛出函数式exceptionSupplier
的异常。
一般情况,跟orElseThrow
函数功能相似的还有orElse(T other)
和 orElseGet(Supplier<? extends T> other)
public T orElse(T other) {
return value != null ? value : other;
}
对于orElse
,如果value
不为null
,就返回value
,否则返回 other
。
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
对于orElseGet
,如果value
不为null
,就返回value
,否则返回执行函数式other
后的结果。
2.3 map 和 flatMap
我们上面的例子,使用到了map(Function<? super T, ? extends U> mapper)
Optional.ofNullable(orderInfo)
.map(Order::getAddress)
.map(Address::getCity)
我们先来介绍一下它的:
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}
public boolean isPresent() {
return value != null;
}
其实这段源码很简答,先是做个空值检查,接着就是value的存在性检查,最后就是应用函数并返回新的
Optional```
跟.map
相似的,还有个flatMap
,如下:
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}
可以发现,它两差别并不是很大,主要就是体现在入参所接受类型不一样。
2.4 isPresent 和ifPresent
我们在使用Optional的过程中呢,有些时候,会使用到isPresent
和ifPresent
,他们有点像,一个就是判断value值是否为空,另外一个就是判断value值是否为空,再去做一些操作。比如:
public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}
即判断value值是否为空,然后做一下函数式的操作。
举个例子,这段代码:
if(userInfo!=null){
doBiz(userInfo);
}
用了isPresent
可以优化为:
Optional.ofNullable(userInfo)
.ifPresent(u->{
doBiz(u);
});
优雅永不过时,嘻嘻~
来源:juejin.cn/post/7391065678546386953
我毕业俩月,就被安排设计了公司第一个负载均衡方案,真头大
前言
Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。
今天我想和大家聊一段我自己毕业初期的一段经历,当领导给你安排了无从下手的任务,难以解决的技术问题时,该怎么办?
本文会从我的一段经历出发,分析在初入职场、刚刚成为一个程序的时候,遇到自己无解决的问题,和自己的一些解决问题的思考。
如果你工作不满三年,一定要往下看,相信一定会对你有帮助。但如果你已经工作3年以上,那么你应该不会遇到下面的问题啦,但也欢迎你往下看,也许会有不同的收获。
先讲故事
每个人都有自己的职场新人期,这个阶段你会敏感、迷茫。
记得在毕业的2个月后,我的组长给我布置了一个任务,实现服务的热部署。
热部署是什么?
热部署是一种技术,它允许在应用程序运行时不中断地更新或替换软件组件,如代码或配置。这种技术主要应用于Web应用程序和分布式系统中,旨在减少停机时间,提高开发效率,并增强应用程序的可用性。
那时候我们的服务器是单实例,每次升级,一定要先停掉服务,替换war包,然后重启tomcat。想升级,必须要在半夜客户下班的时候升级。
作为一个职场小白,技术小白,第一次接到这种任务的时候,我整个人是一脸懵逼的。
那时候报错的堆栈信息我都看不懂,还得请教组里的同事,CRUD还没整明白呢。就要去解决这个架构问题,可想而知我那时候有多崩溃。
其实选择让我一个毕业2个月的应届生来做,能发现两个基本的背景
- 公司没有标准化的解决方案,没有人知道你的解决方案是否对错。
- 包括我的组长、组内老同事在内,也没有人了解如何在生产环境实现热部署,没人可以提供帮助。
因此在这件事的起步阶段,非常的困难。最重要的是,我还不会调研行业的标准方案是什么,刚毕业的我只能埋头苦干,不断试错。
持续的没有进展,也让我内心非常焦虑。多次周会上被问及这件事情,也完全不知道如何汇报进展,因为我自己也不知道,到底什么时候能解决。
记得那时候上厕所、接水的时候,我都是低头不语,装作看不见,生怕他问我一句:“那件事情的进度怎么样了?”
现在想起自己那时候的无助和不安,我都会微微一笑。
很庆幸是在计算机行业,即使是用百度,也能够搜到大把的信息,那段时间也了解到了像Nginx、Redis这样的中间件,最终也是用Nginx,实现了最基本的目标,可以在保证客户使用的情况下,正常的升级我们的系统了。
是的,毕业后的第一家公司的负载均衡和不停机服务发布,是由一个小小毕业生来做的,前期内心有多煎熬,上线的那一刻就有多自豪。
故事讲完了,我相信很多职场人,都会面临这样的场景吧,或许是一个功能不知道怎么去实现,又或者一个方案不知道如何去设计,不敢面对,不敢说出来。
为什么会这样
在职场中,我们大概可以用四个阶段概括
- 新人期
- 发展期
- 成熟期
- 衰退期
上面我自己的一小段经历,遇到了新人期中最容易面临的问题,技能上的迷茫,和职场上的迷茫。
技能上的迷茫
我们可能发现自己有好多东西不会,什么都要学,却不知道自己该如何入手。
在大厂里,周围人看起来都是大牛,可以看着他们侃侃而谈,虽然自己只能茫然四顾,但起码有着标杆和榜样。
而在普通传统行业,身边连个会的人都没有,自己的摸索更像是盲人摸象。
职场上的迷茫
不知道事情该办成什么样,领导会不会不满意,对我的考核会不会有影响。
又或者主动说了有困难,领导会不会觉得我能力不足?
面对多重困难,我该如何汇报自己的进度?没有进度怎么办。
思考、建立正确的认知
界定问题,比问题本身要重要
技术日新月异,新技术是学不完的,解决问题的方式更是多种多样的。所有成熟的方案,都不是一蹴而就的,而是通过发现问题、解决问题一步步优化完善来的。
所以我们有一个清晰的认知,我们很难得到最优的解决方案,而是把注意力放在问题本身,看看我们到底要解决什么问题。
例如我一开始不知道热部署的含义,但我知道的是,公司想解决服务的不停机升级问题。那么我最终引入了Nginx,通过反向代理,部署多个服务就好了。提供的解决方案,完成了预期,我认为对于一个新人来讲就是合格的。
PS:事实上我在使用了Nginx完成了负载均衡后,我的组长曾和我说,你这个方案感觉不太行,不是领导想要的效果。但点到为止了,也没有什么指导,或者改进措施,最终也用这个方案在生产去部署。
注重当下
职场规则是明确的,可以在员工手册、职级要求中看到。但职场中的潜规则是不明确的,你无法摸清领导最真实的想法。
为什么这么说呢?
- 领导可能就是想历练你,给你有挑战的事情,想看看你做到什么程度,你的能力边界在哪里
- 领导对你的预期就是能够解决,也不需要他的指导
不同的想法,对你的要求截然不同。
历练你的时候,即使事情没有结果,领导也不会多说什么,或许这就是一个当下受限于公司运维、资源而难以做到的事情。
但领导相信你能够解决问题的时候,你必须要尽量完成,不然确实会影响到对你的一些看法。
对于职场新手期,你短时间内是无法建立一个良好的向上沟通渠道的(如何建立向上沟通渠道,我们后面再说)。所以对你而言,与其揣摩想法, 不如界定好问题,然后注重当下,解决好遇到的每一个困难。
两个方法
目标拆分
有一个关于程序员的经典段子:这个工作已经做完了80%,剩下的**20%**还要用和前面的一样时间。
遇到无从下手,不知如何去解决的问题,是怎么给出一个可执行的分解。
重点来了,拆分的,一定要可执行。每个人对于可执行的区分,是有很大不同的。不同的地方在于可执行的定义,你是否能清楚地知道这个问题该如何解决。
比如文章开头的故事,如果时间回到那一刻,我会这么列出计划
- 了解什么是热部署,方案调研
- 了解Nginx是什么,学会使用基本的命令
- 多个服务之间如何同步信息,比如上一秒在A服务,下一秒在B服务
- 进行验证,线上部署
学会借力
手里有把锤子,看见什么都是钉子。
我们更倾向于用我们手里的某种工具,去解决所有问题。
在遇到问题时,我们的大脑会根据以往经验做出预判,从而形成思维定势,使得我们倾向于使用熟悉的工具或方法来解决问题。然而,并不是所有问题都是钉子,一旦碰到超出我们经验边界的事情,我们可能会束手无策。
还是分享一段经历:
在字节的时候,我遇到过一个问题场景,就是如何保证集群服务器的本地缓存,保持一致。
直白来讲,我要开一个500人的会议,我准备了500份资料,那么在资料可能会修改的情况下,如何保证大家手里资料,都是最新的?
加载、更新,我能想到的就是用消息队列广播,系统收到消息的时候,每一台服务器都走一遍加载逻辑。
但有一个问题我解决不了,几百台服务器同时请求,如何解决突发的压力问题呢?如果500个人同时去找会议组织人打印,排队不说了,打印机得忙冒烟。
我给身边的一个技术大拿说了我的问题,他说,你可以用公司的一个中间件啊,数据只需要一次加载,然后服务器去下载就好了。是哈,用一个超级打印机打印出500份,分发下去就好了,不用每个人亲自来取呀。
就是几句点拨,同步给我了这一个信息,我了解了一下这个中间件,完美解决了我的问题。
因此,学会借力。找到公司大佬,找到网上的大佬,买杯咖啡、发个红包,直接了当的说出你的问题,咨询解决方案,从更高层次,降维打击你的问题。
当然,一定要在自己思考完、没有结果的情况下,再去请教,能够自己研究明白的,比如使用相关的,不要去麻烦别人。
说在最后
文章到这里就要结束啦,很感谢你能看到最后。
职场初期遇到无从下手的任务时,我们应该建立正确的认知,界定问题,并注重当下。
也分享给你了两个行之有效的方法,目标拆分和学会借力,当然,这一切都离不开你的行动和坚持,这不是方法,而是一个技术人最基本的素质,所以就不多说啦。
不知道你在职场中初期遇到无从下手的问题时,你是怎么处理的呢,欢迎你在评论区和我分享,也希望你点赞、评论、收藏,让我知道对你有所收获,这对我来说很重要。也欢迎你加我的wx:Ldhrlhy10,让我做你的垫脚石,帮你解决你遇到的问题呀,欢迎一起交流~
来源:juejin.cn/post/7362905254725648438
失业的七个月,失去了很多很多,一个普通的不能再普通的人的年中总结
开篇
这不是一篇技术的文章。
第一阶段 裸辞后的两个半月
介绍下自己的情况,坐标上海,双非院校前端打工人,目前是有三年的工作经验,在23年的年底裸辞了,有一个女朋友,本来异地,她在22年10月来到了上海,选择相信我。
在刚刚裸辞的时候,我信心满满的在各个平台投递着简历,给自己做了一个规划:“先投外包的,投小公司来练手,最后一鸣惊人进入大厂,走向人生巅峰!”当时已经可以在各大论坛上看到,前端已死啊之类的标题,但从实际感受来说,好像并没有那么夸张,程序员还是比较高薪的职业。已读不回是挺常见的,但实际面试还是比较多的,并不像大家在网上说的感觉前端开发都要找不到工作了。在这三个月里,我BOSS上沟通了400个人左右,面试的有11家,其中有5个外包。这样的情况在现在的我看来真有点属于暴殄天物了,只沟通了400多个公司就约了11家面试,后来的我才知道这就是我找工作最顺利的阶段,也是我最浪费机会的阶段。由于没有一个正确的认知,以及一个具体的规划和行动,我面试没有尽力去准备,再加上父亲住院,经常往医院跑,一来一回基本一天就没了,11家面试只有一家进入了二轮复面,甚至没有一家进入到谈薪阶段。可当时的我始终是抱着一个比较乐观的态度,还会天天和我的女朋友吐槽一些公司,从来没有认真的找过自己的问题,天真的认为只要我好好准备一下,就一定可以拿下offer,还和她保证我过年前一定能找到工作!她也十分信任我,可后续的发展就愈发的不可控了。
第二阶段 字节面试一轮结束 失业的第四个月
因为之前的面试都没有一个很好的反馈,渐渐的我有点开始着急,也已经渐渐处于摆烂的状态投递简历,就是海投,不看公司介绍,只要是个公司我就打个招呼,有回复我就发个简历,以至于在投递简历的过程中,都不知道何时自己投了字节的一个岗位,然后还约到了面试,我一时间来了信心,约了两周后的字节一轮面试。这两周我没有打开招聘软件,处于一个孤注一掷的状态,实际上就是抱着侥幸心理想碰碰运气,这期间我看了一周的技术课程,浅浅的背了一些八股文,但并没有对之前所有面试的经历进行一个总结,找找自己的问题。之后时间很快就到了面试的当天,这天我突然感觉自己差的东西有很多,但还是硬着头皮去面了,结果可想而知,一轮没有通过。我当时非常沮丧,给女朋友打了一个电话说自己没有通过,她还是非常鼓励我,不给我压力。她工作也很忙,基本就是早上8点到晚上11点的上班强度,但还是一直很有耐心的和我说没事,没有工作也没事,等你想找工作的时候再找也来得及。实际这个时候我就应该醒悟了,自己并不是什么技术大牛,只是一个顶着一个前端工程师光环的cv工程师,一个凭借一些错误的自我认知,就觉得自己未来一定会更好的愚蠢、不可理喻的愚蠢的人,况且我已经不算年轻,是一个快要奔三的大龄青年了,还抱着这种天真,不成熟的想法,但这也是后话了,当时的我还是挫败了几天后继续着躺平的生活,还大言不惭的和我的女朋友说着我三四月份一定可以找到一份合适的工作,做出承诺,但却没有匹配承诺的态度和行为,这时候的女朋友已经对我有了一些意见,但她没有明确的和我说,怕给我压力,只是感觉她的工作状态逐渐不对了,疲惫和内耗占据了她大部分空闲时间,她第一次跟我说了,要不试试别的岗位吧,前端的工作这么不好找,不如换个方向,但这句话我没有放在心上。因为报了健身班,我的健身教练已经开始为我着急了,他说要不来干健身教练吧,我带你入门,我也只当是玩笑话了。
第三阶段 裸辞后的六月半
不出意外的,在金三银四过去之后,我仍然没有找到工作,面试机会也几乎没有了,一个月可能有一两个外包的面试,但自己因为不想考虑外包,在某一家外包面完挫败感的驱使下,屏蔽了BOSS上大多数的外包公司,自此我的BOSS上再也没有一点水花。后来在我认为是缘分的加持下,我的父母和她的父母在上海碰到了,然后顺利的吃了饭,见了面,我爸妈给了我的女朋友见面礼一万零一的红包,寓意万里挑一。我的父母是很普通的农民,一辈子都跟种地打交道,不善言辞,一整个饭局,基本就是我的哥哥和她的爸妈和她在聊天,我在饭桌上也一言不发,可能是怕她的父母问及我工作的问题,也可能是因为哥哥是个很优秀的人,他对于我的期望很高,每次他在场的时候我都会避免开口,以免被抓到会被教育的点。总之饭局也很快就结束了,她的父母对我也没有什么过分的要求,也没有着急让我找工作,说着安慰我的话,现在市场环境不好,找不到工作很正常,利用这段时间学习,以后还有很多机会,你还年轻呢,不要害怕。现在的我几乎是流着眼泪打出当时阿姨跟我说的话,她们一家都是很讲道理,也会为别人考虑的人,但我还是辜负了他们的信任,让他们失望了。这期间面试机会少之又少,我这时候已经渐渐由摆烂,变得慌张的不知所措了,现在发生的情况我之前完全没有考虑到,随着存款一点一点的被社保、房租、吃喝消耗,女朋友的状态也愈发变得消极,我内心陷入了很痛苦的境地。但这并没有让我去总结面试的问题,只是一味的投着简历,看着八股文,等待面试。在六月初,女朋友第二次和我商量,如果前端的工作不好找就换个别的干干吧,这时我已经有点想逃避社会了,想要避免外出,除了每天去健身房,剩下时间几乎都呆在小小的出租屋房间。但我还是答应了她,在6月15号之后我就找其他岗位的工作。接下来几天,有了两个面试,还有原来公司的人找我想让我帮忙做个项目,因为甲方报价还没有,所以这个事情就相当于没有后续,但实际我还是心存幻想,想走更平坦的路。接着时间很快就到了6月13号,她问我计划还实行吗,我说当然实行啊,不过想等一下这个项目的事情,她第一次发泄了自己的情绪,说自己要等到什么时候啊!6月14号,她说她好累,不想等了,想分手。晚上沟通中我没有任何的话可以讲出口,一直沉默,气氛一直很凝重,6月15日,我整理了一些我的问题,想和她聊聊,她上班回来之后很平静的和我说了分手,说了本身对我的期望也没有那么高,说只需要有一个工作,或者我努力的心就可以。我说了我的很多问题,保证我自己会改,但为时已晚,因为她已经给我了足够的时间,但我没有珍惜。她把礼金退给了我,我搬了出去,故事在这就画了句号。
现在
痛苦总是后知后觉,并且悄悄击碎你的防线。父亲身体愈发不好,在知道我分手之后,去医院检查身体的时候晕倒了,进入急诊的那一刻,看着急诊中的病人,看着意识模糊的父亲,我的世界一下就崩塌了。好在父亲没有大碍,但精神状态不是很好。我没有一个最坏的预期,并且时刻都在回避问题,不敢直面自己,承认自己的弱小以及愚蠢,还渐渐看不到身边人做出的牺牲和努力。我浑浑噩噩的过着这几天,这几天是我第一次正面的和哥哥进行了我认为的平等的沟通,我第一次很认真的听他讲话,不再排斥,也第一次觉得他说的其实都对,他的话第一次在我眼里不是说教,而是关心和爱护。我发现了自己的很多问题,他还教会了我一个人生态度:凡事抱最大的希望,尽最大的努力,做最坏的打算。我的世界开始慢慢重建起来,我发现了自己之前的工作内容很简单,基于vue2、antdv、Echarts的后台管理系统,图表,小程序。我发现我没有规划,总是走一步看一步,并且还带着莫名的自信。我发现我辜负了很多人的期待,让自己也很失望。我发现了很多很多,我不知道这次裸辞还会持续的给我带来什么影响,但生活总是要继续,一手好牌最终打的稀烂,但已经没有时间让我继续消沉下去,2024年有可能是我这一生都不会忘记的。
结语
这七个月,让我失去了很多很多,失去了工作,失去了信任,失去了挚爱,失去了···这篇文章主要目的不是为了卖惨,或者贩卖焦虑,只是一个普通的不能再普通的人的一段时间的总结,是对可能已经逝去爱情的怀念。未来还会有很多很多意外的事情发生,我需要做的是积极的去面对,能做的是适应环境,找自己的不足,及时补救,不要一切都来不及的时候才后悔,才反思。我想要和林克一样,忍受孤独,努力变强,去找到她。
来源:juejin.cn/post/7382892371224461352
教你做事,uniapp ios App 打包全流程
背景
使用uniapp 开发App端,开发完成后,ios端我们需要上架到App Store,在此之前,我们需要将App先进行打包。
在HubilderX中,打包ios App我们需要四个东西,分别是:
- Bundle ID
- 证书私钥密码
- 证书私钥文件
- 证书profile文件
下面,我将一步步讲解,如何获取以上文件。
加入苹果开发者
- 使用iPhone或iPad 在App Store 下载 Apple Developer
- 进入App
- 点击底部【账户】
- 点击立即注册
- 填写资料(填写的信息要与你的苹果账号对应,因为这个App需要双重认证)
- 填完信息和资料后点击订阅
- 付费(需要给你的手机添加付款方式)
- 付费成功
- 成功加入苹果开发者计划
生成p12证书和证书私钥密码
步骤:CSR文件 ➡️ cer文件 ➡️ p12文件
- 进入Apple Developer官网,登录成功后,点击顶部导航栏的【账户】,在【账户】页面点击【证书】
- 进入到【Certificates, Identifiers & Profiles】页面,点击+号,开始注册证书
- 选择【iOS Distribution (App Store and Ad Hoc)】再点击【Continue】
- 上传证书签名(CSR文件)
下面会教大家如何生成CSR文件:
- 打开Mac上的【钥匙串访问】App
- 依次选择App顶上菜单栏的【钥匙串访问】➡️【证书助理】➡️【从证书颁发机构请求证书…】
- 打开弹窗,填写两个邮件、常用名称,选择存储到磁盘,点击【继续】
- 存储到桌面,得到【CSR文件】
- 回到网页,选择并上传刚刚生成的【CSR文件】,点击【Continue】
- 到这里【cer文件】就生成好了,点击【Download】下载到桌面
- 得到【cer文件】
接下来我们要根据这个【cer文件】导出生成为【p12文件】
- 双击打开【cer文件】,Mac会自动打开【钥匙串访问】,选中左侧登录 ➡️ 我的证书 ➡️ 证书文件,找到这个【cer证书】
- 此时证书是未受信任,双击该证书,在弹窗中展开【信任】,选择【始终信任】,然后关闭输入密码保存,证书就改成受信任了
- 右键选中该证书,在菜单中选择【导出】
- 输入密码,即【证书私钥密码】(该密码就是HbuilderX发行打包App时,填写的【证书私钥密码】),之后再输入电脑密码
- 最终得到【p12证书】
生成Bundle ID
- 回到页面(Certificates, Identifiers & Profiles),选择【Identifiers】,点击+号
- 选择【App IDs】,点击【Continue】
- 选择【App】,点击【Continue】
- 填写描述和Bundle ID,ID格式如:com.domainname.appname
- 下面的功能如果有需要的话,需要勾选上
- 比如你的App需要Apple登录的话,则需要勾选【Sign In with Apple】
- 设置完成后,点击右上角的【Continue】,【Bundle ID】就生成好了
生成profile文件
- 回到页面(Certificates, Identifiers & Profiles),选择【Profiles】,点击+号
- 选择【App Store】,点击【Continue】
- 选择上一步生成的【身份标识】,点击【Continue】
- 选择第一步生成的【Certificates证书】,点击【Continue】
- 设置【配置文件名称】,点击【Generate】生成
- 点击【Download】下载【profile文件】
- 得到【profile文件】
到这里,【Bundle ID】、【p12文件】【证书私钥密码】、【profile文件】就生成好了,可以去HbuilderX打包ios App了
HbuilderX 打包ios App
- 填入配置和文件
- 点击【打包】,即可生成App
到这一步,iOS App就生成好了。
来源:juejin.cn/post/7264939254290579495
程序员的这10个坏习惯,你中了几个?超过一半要小心了
前言
一些持续关注过我的朋友大部分都来源于我的一些资源分享和一篇万字泣血斩副业的劝诫文,但今年年后开始我有将近4个月没有再更新过。
有加过我好友的朋友私聊我问过,有些回复了有些没回复。
想通过这篇文章顺便说明一下个人的情况,主要是给大家的一些中肯的建议。
我的身体
今年年前公司福利发放的每人一次免费体检,我查出了高密度脂蛋白偏低,因为其他项大体正常,当时也没有太在意。
但过完年后的第一个月,我有一次下午上班忽然眩晕,然后犯恶心,浑身发软冒冷汗,持续了好一阵才消停。
当时我第一感觉就是颈椎出问题了?毕竟这是程序员常见的职业病。
然后在妻子陪伴下去医院的神经内科检查了,结果一切正常。
然后又去拍了片子看颈椎什么问题,显示第三节和第四节有轻微的增生,医生说其实没什么,不少从事电脑工作的人都有,不算是颈椎有大问题。
我人傻了,那我这症状是什么意思。
医生又建议我去查下血,查完后诊断出是血脂偏高,医生说要赶紧开始调理身体了,否则会引发更多如冠心病、动脉粥样硬化、心脑血管疾病等等。
我听的心惊胆战,没想到我才34岁就会得上老年病。
接下来我开始调理自己的作息和生活,放弃一些不该强求的,也包括工作之余更新博客,分享代码样例等等。
4个月的时间,我在没有刻意减肥的情况下体重从原先152减到了140,整个人也清爽了许多,精力恢复了不少。
所以最近又开始主动更新了,本来是总结了程序员的10个工作中的不良习惯。
但想到自己的情况,决定缩减成5个,另外5个改为程序员生活中的不良习惯,希望能对大家有警示的作用。
不良习惯
1、工作
1)、拖延症
不到最后一天交差,我没有压力,绝不提前完成任务,从上学时完成作业就是这样,现在上班了,还是这样,我就是我,改不了了。
2)、忽视代码可读性
别跟我谈代码注释,多写一个字我认你做die,别跟我谈命名规范,就用汉语拼音,怎样?其他人读不懂,关我什么事?
3)、忽视测试
我写一个单元测试就给我以后涨100退休金,那我就写,否则免谈。接口有问题你前端跟我说就行了发什么脾气,前后端联调不就这样吗?!
4)、孤立自己
团队合作不存在的,我就是不合群的那个,那年我双手插兜,全公司没有一个对手。
5)、盲目追求技术新潮
晚上下班了,吃完饭打开了某某网,看着课程列表中十几个没学完的课程陷入了沉默,但是首页又出现了一门新课,看起来好流行好厉害耶,嗯,先买下来,徐徐图之。
2、生活
1)、缺乏锻炼和运动
工作了一天,还加班,好累,但还是得锻炼,先吃完饭吧,嗯,看看综艺节目吧,嗯,再看看动漫吧,嗯,还得学习一下新技术吧,嗯,是手是得洗澡了,嗯,还要洗衣服,咦,好像忘记了什么重要的事情?算了,躺床上看看《我家娘子不对劲》慢慢入睡。
2)、加班依赖症
看看头条,翻翻掘金,瞅瞅星球,点点订阅号,好了,开始工作吧,好累,喝口水,上个厕所,去外面走走,回来了继续,好像十一点半了,快中午了,待会儿吃什么呢?
午睡醒了,继续干吧,看看头条,翻翻掘金,瞅瞅星球,点点订阅号,好了,开始工作吧,好累,喝口水,上个厕所,去外面走走,回来了继续,好像5点半了,快下班了,任务没完成。
算了,加加班,争取8点之前搞定。
呼~搞定了,走人,咦,10点了。
3)、忽视饮食健康
早上外卖,中午外卖,晚上外卖,哇好丰富耶,美团在手,简直就是舌尖上的中国,晚上再来个韩式炸鸡?嗯,来个韩式甜辣酱+奶香芝士酱,今晚战个痛快!
4)、缺乏社交活动
好烦啊,又要参加公司聚会,聚什么餐,还不是高级外卖,说不定帮厨今天被大厨叼了心情不好吐了不少唾沫在里面,还用上完厕所摸了那里没洗的手索性搅了一遍,最后在角落里默默看着你们吃。
吃完饭还要去KTV?继续喝,喝不死你们,另外你们唱得很好听吗?还不是看谁嗷的厉害!
谁都别跟我说话,尤其是领导,离我越远越好,唉,好想回去,这个点B站该更新了吧,真想早点看up主们嘲讽EDG。
5)、没有女朋友
张三:我不是不想谈恋爱,是没人看得上我啊,我也不好意思硬追,我也要点脸啊,现在的女孩都肿么了?一点暗示都不给了?成天猜猜猜,我猜你MLGB的。
李四:家里又打电话了,问在外面有女朋友了没,我好烦啊,我怎么有啊,我SpringCloudAlibaba都没学会,我怎么有?现在刚毕业的都会k8s了,我不学习了?不学习怎么跳槽,不跳槽工资怎么翻倍,不翻倍怎么买房,不买房怎么找媳妇?
王五:亲朋好友介绍好多个了,都能凑两桌麻将了,我还是没谈好,眼看着要30了,我能咋整啊,我瞅她啊。破罐破摔吧,大不了一个人过呗,多攒点钱以后养老,年轻玩个痛快,老了早点死也不亏,又不用买房买车结婚受气还得养娃,多好啊,以后两脚一蹬我还管谁是谁?
总结
5个工作坏习惯,5个生活坏习惯,送给我亲爱的程序员们,如果你占了一半,真得注意点了,别给自己找借口,你不会对不起别人,只是对不起自己。
喜欢的小伙伴们,麻烦点个赞,点个关注,也可以收藏下,以后没事儿翻出来看看哈。
来源:juejin.cn/post/7269375465319415867
你的 Flutter 项目异常太多是因为代码没有这样写
以前在团队里 Review 过无数代码,也算是阅码无数的人了,解决过的线上异常也数不胜数,故对如何写出健状性的代码有一些微小的见解。刚好最近闲来得空(其实是拖延症晚期)把之前躺在备忘录里的一些小记整理了一下,希望能对你有一些启发。
Uri 对象的使用
在 Dart 语言中 Uri 类用于表示 URIs(网络地址、文件地址或者路由),它内部能自动处理地址的模式与的百分号编解码。在实际开发过程我们经常直接使用字符串进行拼接 URIs,然而这种方式会给地址的高级处理带来不便甚至隐式异常。
/// 假设当前代码为一个内部系统,[outsideInput] 变量是外部系统传入内部的字符串变量
/// 你无法限定 [outsideInput] 的内容,如果变量包含非法字符(如中文),整个地址非法
final someAddress = 'https://www.special.com/?a=${outsideInput}';
/// 为了保持 URI 的完整性你可能会这样做
final someAddress = 'https://www.special.com/?a=${Uri.decodeFull(outsideInput)}';
/// 如有多个外部输入变量你又需要这样做
final someAddress = 'https://www.special.com/?a=${Uri.encodeFull(outsideInput)}&b=${Uri.encodeFull(outsideInput1)}&c=${Uri.encodeFull(outsideInput2)}';
直接使用字符串来拼接 URI 地址会带来非常多的限制,你需要关注地址中拼接的每个部分的合法性,并且在处理复杂逻辑时需要更冗长为的处理。
/// 如果我们需要在另一个系统中对 [someAddress] 地址的参数按条件进行添加
if (conditionA) {
someAddress = 'https://www.special.com/?a=${Uri.encodeFull(outsideInput)}';
} else if (conditionB) {
someAddress = 'https://www.special.com/?a=${Uri.encodeFull(outsideInput)}&b=${otherVariable}';
} else {
someAddress = 'https://www.special.com';
}
如果使用 Uri 可以简化绝大多数对 URIs 的处理,同时限定类型对外部有更明确的确定性,因此针对 URIs 需要做如下约定:
在任何系统中都不应直接拼接 URIs 字符串,应当构造 URI 对象作为参数或返回值。
/// 生成 Uri 对象
final someAddress = Uri(path: 'some/sys/path/loc', queryParameters: {
'a': '${outsideInput}', // 非法参数将自动百分号编码
'b': '${outsideInput1}', // 不用对每个参数单独进行编码
if (conditionA) 'c': '${outsideInput2}', // 条件参数更为简洁
});
类型转换
Dart 中可以使用 is
进行类型判断,as
进行类型转换。 同时,使用 is
进行类型判断成功后会进行隐性的类型转换。示例如下:
class Animal {
void eat(String food) {
print('eat $food');
}
}
class Bird extends Animal {
void fly() {
print('flying');
}
}
void main() {
Object animal = Bird();
if (animal is Bird) {
animal.fly(); // 隐式类型转换
}
(animal as Animal).eat('meat'); // 强制类型转换一旦失败就会抛异常
}
由于隐式的类型转换存在,is
可以充当 as
的功能,同时 as
进行类型失败会抛出异常。
所以日常开发中建议使用 is
而不是 as
来进行类型转换。 is
运算符允许更安全地进行类型检查,如果转换失败,也不会抛出异常。
void main() {
dynamic animal = Bird();
if (animal is Bird) {
animal.fly();
} else {
print('转换失败');
}
}
List 使用
collection package 的使用
List 作为 Dart 中的基础对象使用广范,由于其本身的特殊性,如使用不当极易导致异常,从而影响业务逻辑。典型示例如下:
List<int> list = [];
// 当 List 为空时访问其 first 会抛异常
list.first
// 同理访问 last 也会抛异常
list.last
// 查找对象时没有提供 orElse 也会抛异常
list.firstWhere((t) => t > 0);
// List 对象其它会抛异常的访问还有
list.single
list.lastWhere((t) => t > 0)
list.singleWhere((t) => t > 0)
所以如果没有前置判断条件,所有对 List 的访问均需替换为 collection
里对应的方法。
import 'package:collection/collection.dart';
List<int> list = [];
list.firstOrNull;
list.lastOrNull;
list.firstWhereOrNull((t) => t > 0);
list.singleOrNull;
list.lastWhereOrNull((t) => t > 0);
list.singleWhereOrNull((t) => t > 0);
取元素越界
在 Dart 开发时,碰到数组越界或者访问数组中不存在的元素情况时,会导致运行时错误,如:
List<int> numbers = [0, 1, 2];
print(numbers[3]); // RangeError (index): Index out of range: index should be less than 3: 3
你可以使用使用 try-catch
来捕获异常,但由于数组取值这样的基础操作往往遍布在项目的各个角落,try-catch
在这样的情况下使用起来会比较繁琐,而且并不是所有的取值都会导致异常,所以往往越界的问题只有真正出现了才发现。
好在,我们可以封装一个 extension
来简化数组越界的问题:
extension SafeGetList<T> on List<T> {
T? tryGet(int index) =>
index < 0 || index >= this.length ? null : this[index];
}
使用时:
final list = <int>[];
final single = list.tryGet(0) ?? 0;
由于 tryGet
返回值类型为可空(T?
) ,外部接收时需要进行空判断或者赋默认值,这相当于强迫开发者去思考值不存在的情况,如此减少了异常发生的可能,同时在业务上也更加严谨。
当然还有另一种方案,可以继承一个 ListMixin
的自定义类:SafeList
,其代码如下:
class SafeList<T> extends ListMixin<T> {
final List<T?> _rawList;
final T defaultValue;
final T absentValue;
SafeList({
required this.defaultValue,
required this.absentValue,
List<T>? initList,
}) : _rawList = List.from(initList ?? []);
@override
T operator [](int index) => index < _rawList.length ? _rawList[index] ?? defaultValue : absentValue;
@override
void operator []=(int index, T value) {
if (_rawList.length == index) {
_rawList.add(value);
} else {
_rawList[index] = value;
}
}
@override
int get length => _rawList.length;
@override
T get first => _rawList.isNotEmpty ? _rawList.first ?? defaultValue : absentValue;
@override
T get last => _rawList.isNotEmpty ? _rawList.last ?? defaultValue : absentValue;
@override
set length(int newValue) {
_rawList.length = newValue;
}
}
使用:
final list = SafeList(defaultValue: 0, absentValue: 100, initList: [1,2,3]);
print(list[0]); // 正常输出: 1
print(list[3]); // 越界,输出缺省值: 100
list.length = 101;
print(list[100]); // 改变数组长度了,输出默认值: 0
以上两种方案均可以解决越界的问题,第一个方案更简洁,第二个方案略复杂且侵略性也更强但好处是可以统一默认值、缺省值,具体使用哪种取决于你的场景。
ChangeNotifier 使用
ChangeNotifier 的属性访问或方法调用
ChangeNotifier
及其子类在 dispose
之后将不可使用,dispose
后访问其属性(hasListener
)或方法(notifyListeners
)时均不合法,在 Debug 模式下会触发断言异常;
// ChangeNotifier 源码
bool get hasListeners {
// 访问属性时会进行断言检查
assert(ChangeNotifier.debugAssertNotDisposed(this));
return _count > 0;
}
void dispose() {
assert(ChangeNotifier.debugAssertNotDisposed(this));
assert(() {
// dispose 后会设置此标志位
_debugDisposed = true;
return true;
}());
_listeners = _emptyListeners;
_count = 0;
}
static bool debugAssertNotDisposed(ChangeNotifier notifier) {
assert(() {
if (notifier._debugDisposed) { // 断言检查是否 dispose
throw FlutterError(
'A ${notifier.runtimeType} was used after being disposed.\n'
'Once you have called dispose() on a ${notifier.runtimeType}, it '
'can no longer be used.',
);
}
return true;
}());
return true;
}
在 dispose
后访问属性或调用方法通常出现在异步调用的场景下,由其是在网络请求之后刷新界面。典型场景如下:
class PageNotifier extends ChangeNotifier {
dynamic pageData;
Future<voud> beginRefresh() async {
final response = await API.getPageContent();
if (!response.success) return;
pageData = response.data;
// 接口返回之后此实例可能被 dispose,从而导致异常
notifyListeners();
}
}
为使代码逻辑更加严谨,增强整个代码的健状性:
ChangeNotifier
在有异步的场景情况下,所有对 ChangeNotifier
属性及方法的访问都需要进行是否 dispose
的判断。
你可能会想到加一个 hasListeners
判断:
class PageNotifier extends ChangeNotifier {
dynamic pageData;
Future<voud> beginRefresh() async {
final response = await API.getPageContent();
if (!response.success) return;
pageData = response.data;
// Debug 模式下 hasListeners 依然可能会抛异常
if (hasListeners) notifyListeners();
}
}
如上所述 hasListeners
内部仍然会进行是否 dispose
的断言判断,所以 hasListeners
仍然不安全。
因此正确的做法是:
// 统一定义如下 mixin
mixin Disposed on ChangeNotifier {
bool _disposed = false;
bool get hasListeners {
if (_disposed) return false;
return super.hasListeners;
}
@override
void notifyListeners() {
if (_disposed) return;
super.notifyListeners();
}
@override
void dispose() {
_disposed = true;
super.dispose();
}
}
// 在必要的 ChangeNotifier 子类混入 Disposed
class PageNotifier extends ChangeNotifier with Disposed {
Future<voud> beginRefresh() async {
final response = await API.getPageContent();
if (!response.success) return;
pageData = response.data;
// 异步调用不会异常
notifyListeners();
}
}
ChangeNotifier 禁止实例复用
ChangeNotifier
在各种状态管理模式中一般都用于承载业务逻辑,初入 Flutter 的开发者会受原生开发的思维模式影响可能会将 ChangeNotifier
实例进行跨组件复用。典型的使用场景是购物车,购物车有加/减商品、数量管理、折扣管理、优惠计算等复杂逻辑,将 ChangeNotifier
单个实例复用甚至单例化能提高编码效率。
但单个 ChangeNotifier
实例在多个独立的组件或页面中使用会造成潜在的问题:复用的实例一旦在某个组件中被意外 dispose
之后就无法使用,从而影响其它组件展示逻辑并且这种影响是全局的。
@override
void initState() {
super.initState();
// 添加监听
ShoppingCart.instance.addListener(_update);
}
@override
void dispose() {
// 正确移除监听
ShoppingCart.instance.removeListener(_update);
// 如果哪个实习生不小心在组件中这样移除监听,将产生致命影响
// ShoppingCart.instance.dispose();
super.dispose();
}
因此在 Flutter 开发中应禁止 ChangeNotifier
实例对外跨组件直接复用,如需跨组件复用应借助provider
、get_it
等框架将 ChangeNotifer
子类实例对象置于顶层;
void main() {
runApp(
MultiProvider(
providers: [
Provider<Something>.value(ShoppingCart.instance),
],
child: const MyApp(),
)
);
}
如果你非得要 「单例化」 自定义 ChangeNotifier
子类实例,记得一定要重新 dispose
函数。
Controller 使用
在 Flutter 中大多数 Controller
都直接或间接继承自 ChangeNotifier
。为使代码逻辑更加严谨,增强整个代码的健状性,建议:
所有 Controller
需要显式调用 dispose
方法,所有自定义 Controller
需要重写或者添加 dispose
方法。
// ScrollController 源码
class ScrollController extends ChangeNotifier {
//...
}
// 自定义 Controller 需要添加 dispose 方法
class MyScrollController {
ScrollController scroll = ScrollController();
// 添加 dispose 方法
void dispose() {
scroll.dispose();
}
}
ChangeNotifierProvider 使用
ChangeNotifierProvider
有两个构造方法:
ChangeNotifierProvider.value({value:})
ChangeNotifierProvider({builder:})
使用 value
构造方法时需要注意:value
传入的是一个已构造好的 ChangeNotifier
子类实例,此实例不由 Provider
内构建,Provider
不负责此实例的 dispose
。
虽然这个差异在 Provider 文档中有重点说明,但仍然有不少开发人员在写代码的过程中混用,故在此再次强调
因此开发人员在使用 ChangeNotifierProvider.value
时为使代码逻辑更加严谨,增强整个代码的健状性,培养良好的开发习惯开发人员需践行以下规范:
使用 ChangeNotifierProvider.value
构造方法时传入的实例一定是一个已构建好的实例,你有义务自行处理此实例的 dispose
。使用 ChangeNotifierProvider(builder:)
构造方法时你不应该传入一个已构建好的实例,这会导致生命周期混乱,从而导致异常。
你需要这样做
MyChangeNotifier variable;
void initState() {
super.initState();
variable = MyChangeNotifier(); // 提前构建实例
}
void build(BuildContext context) {
return ChangeNotifierProvider.value(
value: variable, // 已构建好的实例
child: ...
);
}
void dispose() {
super.dispose();
variable.dispose(); // 主动 dispose
}
你不能这样做
MyChangeNotifier variable;
void initState() {
super.initState();
variable = MyChangeNotifier();
}
void build(BuildContext context) {
// create 对象的生命周期只存在于 Provider 树下,此处应不直接使用此实例
return ChangeNotifierProvider(
create: (_) => variable,
child: ...
);
}
避免资源释放遗忘
在 Flutter 中有很多需要主动进行资源释放的类型,包含但不限于:Timer
、StreamSubscription
、ScrollController
、TextEditingController
等,另外很多第三方库存在需要进行资源释放的类型。
如此多的资源释放类型管理起来是非常麻烦的,一旦忘记某个类型的释放很会造成整个页面的内存泄漏。而资源的创建一般都位于 initState
内,资源释放都位于 dispose
内。
为了减小忘记资源释放的可能性,dispose
应为 State
内的第一个函数并尽可能的将 initsate
紧跟在 dispose
后
这样在代码 Review 时可以从视觉上一眼看出来资源释放是否被遗忘。
Bad
final _controller = TextEditingController();
late Timer _timer;
void initState() {
super.initState();
_timer = Timer(...);
}
Widget build(BuildContext context) {
return SizedBox(
child: // 假设此处为简单的登录界面,也将是一串很长的构建代码
);
}
void didChangeDependencies() {
super.didChangeDependencies();
// 又是若干行
}
// dispose 函数在 State 末尾,与 initState 大概率会超过一屏的距离
// 致使 dispose 需要释放的资源与创建的资源脱节
// 无法直观看出是否漏写释放函数
void dispose() {
_timer.cancell();
super.dispose();
}
Good
final _controller = TextEditingController();
late Timer _timer;
// 属性后第一个函数应为 dispose
void dispose() {
_controller.dispose();
_timer.cancell();
super.dispose();
}
// 中间不要插入其它函数,紧跟着写 initState
void initState() {
super.initState();
_timer = Timer(...);
}
上面推荐的写法也可以用在自定义的 ChangeNotifer
子类中,将 dispose
函数紧在构造函数后,有利于释放遗漏检查。
由于创建资源与释放资源在不同的函数内,因此存在一种情况:为了释放资源不得不在 State
内加一个变量以便于在 dipose
函数中引用并释放,即便此资源仅在局部使用。
典型场景如下:
late CancelToken _token;
Future<void> _refreshPage() async {
// _token 只在页面刷新的函数中使用,却不得不加一个变量来引用它
_token = CancelToken();
Dio dio = Dio();
Response response = await dio.get(url, cancelToken: _token);
int code = response.statusCode;
// ...
}
void dispose() {
super.dispose();
_token.cancel();
}
这样的场景在一个页面内可能有多处,相同的处理方式使用起来就略显麻烦了,也容易导致遗忘。因此推荐如下写法:
// 创建下面的 Mixin
mixin AutomaticDisposeMixin<T extends StatefulWidget> on State<T> {
Set<VoidCallback> _disposeSet = Set<VoidCallback>();
void autoDispose(VoidCallback callabck) {
_disposeSet.add(callabck);
}
void dispose() {
_disposeSet.forEach((f) => f());
_disposeSet.removeAll();
super.dispose();
}
}
class _PageState extends State<Page> with AutomaticDisposeMixin {
Future<void> _refreshPage() async {
final token = CancelToken();
// 添加到自动释放队列
autoDispose(() => token.cancel());
Dio dio = Dio();
Response response = await dio.get(url, cancelToken: token);
int code = response.statusCode;
// ...
}
}
当然也这种用法不限于局部变量,同样也可以在 initState
内进行资源声明的同时进行资源释放,这种写法相对来讲更加直观,更不易遗漏资源释放。
final _controller = TextEditingController();
void initState() {
super.initState();
_timer = Timer(...);
autoDispose(() => _timer.cancel());
autoDispose(() => _controller.dispose());
}
StatefulWidget 使用
State 中存在异步刷新
在开发过程中简单的页面或组件通常直接使用 StatefulWidget
进行构建,并在 State
中实现状态逻辑。因此 State
不可避免可能会存在异步刷新的场景。但异步结束时当前 Widget 可能已经从当前渲染树移除,直接刷新当前 Widget 可能导致异常。典型示例如下:
class SomPageState extends State<SomePageWidget> {
PageData _data;
Future<void> _refreshPage() async {
// 异步可能是延时、接口、文件读取、平台状态获取等
final response = await API.getPageDetaile();
if (!response.success) return;
// 直接界面刷新页面可能会导致异常,当前 Widget 可能已从渲染树移除
setState((){
_data = response.data;
});
}
}
为使代码逻辑更加严谨,增强整个代码的健壮性,培养良好的开发习惯,建议:
在 State
里异步刷新 UI 时需要进行 mounted
判断,确认当前 Widget
在渲染树中时才需要进行界面刷新否则应忽略。
Future<void> _refreshPage() async {
// 异步可能是接口、文件读取、状态获取等
final response = await API.getPageDetaile();
if (!response.success) return;
// 当前 Widget 存在于渲染树中才刷新
if (!mounted) return;
setState((){
_data = response.data;
});
}
上面的 mounted
判断可能会存在于所有 State
中又或者一个 State
里有多个异步 setState
调用,每个调用都去判断过于繁锁,因此更推荐如下写法:
// 统一定义如下 mixin
mixin Stateable<T extends StatefulWidget> on State<T> {
@override
void setState(VoidCallback fn) {
if (!mounted) return;
super.setState(fn);
}
}
// 在存在异步刷新的 State 中 with 如上 mixin
class SomPageState extends State<SomePageWidget> with Stateable {
//...
}
来源:juejin.cn/post/7375882178012577802
为什么常说,完成比完美更重要
前言
Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。
最近学习了12个生财思维,受益匪浅,但是纸上得来终觉浅,绝知此事要躬行,没有亲身实践,怎么能更好的理解呢?
单纯的学习,尤其是思维工具类的学习,只看但不实践,是不会有太好的效果的。
课程中的案例虽然真实,但是每个人的眼界、能力不同,所以案例对自己只能开开眼,但自己对于思维模式的理解却不会有太多的帮助。
为了更好的理解每一个生财思维,我决定根据每一个生财思维去复盘过去十年间遇到的机遇,看看自己错过了什么,有抓住了什么,然后把学习过程中的思考重新整理出来。
今天想和大家分享的是迭代思维,希望对你有所帮助。
迭代思维
什么是迭代思维
迭代是什么意思,一种重复反馈过程的活动,每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值。
迭代对于一个程序员来讲并不陌生,甚至很多公司把版本发布,都成为“迭代”。
如何真正的用好迭代思维?
主要有三步
- 确定目标,比如在软件开发中,我们首先要知道,我们目标是什么,是收入、用户数,还是流量。
- 找到迭代方法,这个依然很常见,利用上一篇文章讲的对标思维,参考领域成功的高手,看看他们用了什么工具、方法。
- 持续改进,每次哪怕只改进一小点,让每一步走的更踏实。
先完成,再完美
一周上线的新系统
软件开发是离不开迭代的过程的,就算是你设计了完美的框架,也会在各类需求的轰炸中不得不进行迭代满足用户需求。
在上家公司,有一次领导安排一个新业务启动,于是要单独启动一个项目。
我不知道大家平常对于一个新项目的开发需要多久,那时候我的能力较弱,开发语言也不熟悉,以往的经验是,从技术设计、框架搭建、代码开发、测试这一套流程下来,我觉着这个项目起码也要1个月左右才能完成吧。
但是经过领导们评估,最终决定这个系统的开发时间是5天,留两天时间自测,说实话我是持着怀疑的态度,硬着头皮接了下来。
最终项目用了一周多的时间就上线了,虽然时长报警,缺乏监控,代码性能不够高,但快速验证了业务的可行性。
我承认刚上线的系统并不是一个完美的系统,甚至有问题时发下连日志都没打印。
但是,发现问题排查困难,所以先不停的完善日志打印。
接着觉着发现问题太慢了,补充了监控和报警,异常情况第一时间就能感知。
性能不够,响应时间长,花了3天时间优化性能瓶颈点。
业务不断扩充,代码扩展性不好,优化了1周的时间,进行了一部分重构。
就是随着一段段时间的投入,一次次的发版上线,最终服务趋于稳定,也成了业务的一部分收入来源。
不断练习的写作
如果还能想到一个例子,那这件事就是写作。
去年年底决定开始写作时,发布的那篇文章,写完就直接发布了,很明显,数据非常不好。
内容很少分段,也没有配图,更不用说加黑、二级标题这些了。
先从内容开始,对比很多流量好的文章,都有一个共性,就是没有一大段话,而是都进行了分段。为了让大家阅读更加轻松,把一大段话,拆成几个小段,看来更加清爽。
添加了分段之后,又发现如果正片内容都是文字,一样让大家阅读压力很大,所以在其中搭配上配图,效果会更好。
后面,改进了文字排版,开始带有一级标题、二级标题。
再后来,学习如何取标题、如何选题,还建立了自己的写作模版,开头和结尾,还添加上了引导关注的文案。
就用这种方式,在掘金和公众号上,也写出了一些数据比较好的内容,掘金的创作登记,从lv3,也升级到了lv5。
而迭代思维,也会在写作上,持续应用下去。
不去开始,必定失败
迭代思维,主要适用于哪类场景呢?
当你有一件事情、一个项目时,因为内心没有完美的方案,迟迟没有行动时,就需要用到迭代思维了。
记得我发布的第一篇文章,可以是2022年了,然而下一次再次发布,已经隔了1年多的时间。
相隔时间这么久的原因,就是因为我在那个时候,我发现我写出的文章和别人差距太大了,别人的文章洋洋洒洒一两千字,标题吸引人,然而我自己的文章即没有深度,又没有自己的感触。
我在和一篇优秀的文章做比较时,我完全不知道应该如何做,才能写的像别人一样好。
于是,遇到问题不做了,睡大觉。这一睡,就白白荒废了1年的时间。
迭代思维,首先要避免完美思维,先开始,然后最重要的是小步快跑。
记得在学习写作的时候,听到老师的一句话,让我记忆犹新,也再次分享给大家。
想都是问题,做才有答案。
说在最后
好了,文章到这里就要结束了,总结一下。
迭代思维从概念上看其实不难,只需要三步即可,确定目标,找到迭代方法,持续改进。
来源:juejin.cn/post/7390134326871080972
美团多场景建模的探索与实践
本文介绍了美团到家/站外投放团队在多场景建模技术方向上的探索与实践。基于外部投放的业务背景,本文提出了一种自适应的场景知识迁移和场景聚合技术,解决了在投放中面临外部海量流量带来的场景数量丰富、场景间差异大的问题,取得了明显的效果提升。希望能给大家带来一些启发或帮助。
1 引言
美团到家Demand-Side Platform(下文简称DSP)平台,主要负责在美团外部媒体上进行商品或者物料的推荐和投放,并不断优化转化效果。随着业务的不断发展与扩大,DSP对接的外部渠道越来越丰富、展示形式越来越多样,物料展示场景的差异性愈发明显(如开屏、插屏、信息流、弹窗等)。
例如,用户在午餐时间更容易点击【某推荐渠道下】【某App】【开屏展示位】的快餐类商家的物料而不是【信息流展示位】的啤酒烧烤类商家物料。场景间差异的背后本质上是用户意图和需求的差异,因此模型需要对越来越多的场景进行定制化建设,以适配不同场景下用户的个性化需求。
业界经典的Mixture-of-Experts架构(MoE,如MMoE、PLE、STAR[1]等)能一定程度上适配不同场景下用户的个性化需求。这种架构将多个Experts的输出结果通过一个门控网络进行权重分配和组合,以得到最终的预测结果。早期,我们基于MoE架构提出了使用物料推荐渠道进行场景划分的多场景建模方案。然而,随着业务的不断壮大,场景间的差异越来越大、场景数量也越来越丰富,这版模型难以适应业务发展,不能很好地解决DSP背景下存在的以下两个问题:
- 负迁移现象:以推荐渠道为例,由于不同推荐渠道的流量在用户分布、行为习惯、物料展示形式等方面存在差异,其曝光数、点击率也不在同一个数量级(如下图1所示,不同渠道间点击率相差十分显著),数据呈现典型的“长尾”现象。如果使用推荐渠道进行多场景建模的依据,一方面模型会更倾向于学习到头部渠道的信息,对于尾部渠道会存在学习不充分的问题,另一方面尾部渠道的数据也会给头部渠道的学习带来“噪声”,导致出现负迁移。
- 数据稀疏难以收敛:DSP会在外部不同媒体上进行物料展示,而用户在访问外部媒体时,其所处的时空背景、上下文信息、不同App以及物料展示位等信息共同构成了当前的场景,这样的场景在十万的量级,每个场景的数据又十分稀疏,导致模型难以在每个场景上得到充分的训练。
在面对此类建模任务时,业界现有的方法是在不同场景间进行知识迁移。例如,SAML[2]模型采用辅助网络来学习场景的共享知识并迁移至各场景的独有网络;ADIN[3]和SASS[4]模型使用门控单元以一种细粒度的方式来选择和融合全局信息到单场景信息中。然而,在DSP背景中复杂多变的流量背景下,场景差异性导致了场景数量的急剧增长,现有方法无法在巨量稀疏场景下有效。
因此,在本文中我们提出了DSP背景下的自适应场景建模方案(AdaScene, Adaptive Scenario Model),同时从知识迁移和场景聚合两个角度进行建模。AdaScene通过控制知识迁移的程度来最大化不同场景共性信息的利用,并使用稀疏专家聚合的方式利用门控网络自动选择专家组成场景表征,缓解了负迁移现象;同时,我们利用损失函数梯度指导场景聚合,将巨大的推荐场景空间约束到有限范围内,缓解了数据稀疏问题,并实现了自适应场景建模方案。
2 自适应场景建模
在本节开始前,我们先介绍多场景模型的建模方式。多场景模型采用输入层 Embedding + 混合专家(Mixture-of-Experts, MoE)的建模范式,其中输入信息包括了用户侧、商家侧以及场景上下文特征。多场景模型的损失由各场景的损失聚合而成,其损失函数形式如下:
其中,为场景数量,为各场景的损失权重值。
我们提出的AdaScene自适应场景模型主要包含以下2个部分:场景知识迁移(Knowledge Transfer)模块以及场景聚合(Scene Aggregation)模块,其模型结构如下图2所示。场景知识迁移模块自适应地控制不同场景间的知识共享程度,并通过稀疏专家网络自动选择 K 个专家构成自适应场景表征。场景聚合模块通过离线预先自动化衡量所有场景间损失函数梯度的相似度,继而通过最大化场景相似度来指导场景的聚合。
该模型结构的整体损失函数如以下公式所示:
其中,为每个场景组的损失函数所对应的系数,为第个场景组下的的场景数量,为某种场景组的划分方式。
下面,我们分别介绍自适应场景知识迁移和场景聚合的建模方案。
2.1 自适应场景知识迁移
在多场景建模中,场景定义方式决定了场景专家的学习样本,很大程度上影响着模型对场景的拟合能力,但无论采用哪种场景定义方式,不同场景间用户分布都存在重叠,用户行为模式也会有相似性。
为提升不同场景间共性的捕捉能力,我们从场景特征和场景专家两个维度探索场景知识迁移的方法,在以物料推荐渠道×App×展示形态作为多场景建模Base模型的基础上,构建了如下图3所示的自适应场景知识迁移模型(Adaptive Knowledge Transfer Network, AKTN)。该模型建立了场景共享参数与私有参数的知识迁移桥梁,能够自适应地控制知识迁移的程度、缓解负迁移现象。
- 场景特征适配:通过Squeeze-and-Excitation Network[5]构建场景适应层(Scene Adaption Layer),其结构可表示为,其中表示全连接层,为激活函数。由于不同场景对原始特征的关注程度存在较大差异,该层能够根据不同场景的信息生成原始特征的权重,并利用这些权重对输入特征进行相应的变换,实现场景特定的个性化输入表征,提高模型的场景信息捕捉能力。
- 场景知识迁移:使用GRU门控单元构建场景知识迁移层(Scene Transfer Layer)。GRU门控单元通过场景上下文信息对来自全局场景专家和当前场景专家的信息流动进行控制,筛选出符合当前场景的有用信息;并且,该结构能以层级方式进行堆叠,不断对场景输出进行修正。
场景特征适配在输入层根据场景信息对不同特征进行权重适配,筛选出当前场景下模型最关注的特征;场景知识迁移在隐层专家网络中进行知识迁移,控制共享专家中共性信息向场景独有信息的流动,使得场景共性信息得以传递。
这两种知识迁移方式互为补充、相辅相成,共同提升多场景模型的预估能力。我们对比了不同模块的实验效果,具体结果如下表1所示。可以看出,引入场景知识迁移和特征权重优化在头部、尾部渠道都能带来一定提升,其中尾部小流量场景上(见下表1子场景2、3)有更为明显的提升,可见场景知识迁移缓解了场景之间的负迁移现象。
相关研究和实践表明[6][7][8],稀疏专家网络对于提高计算效率和增强模型效果非常有用。因此,我们在AKTN模型的基础上,在专家层进一步优化多场景模型。具体的,我们将场景知识迁移层替换为自动化稀疏专家选择方法,通过门控网络从大规模专家中选取与当前场景最相关的个构成自适应场景表征,其选择过程如下图4所示:
在实践中,我们通过使用可微门控网络对专家进行有效组合,以避免不相关任务之间的负迁移现象。同时大规模专家网络的引入扩大了多场景模型的选择空间,更好地支持了门控网络的选择。考虑到多场景下的海量流量和复杂场景特征,在业界调研的基础上对稀疏专家门控网络进行了探索。
具体而言,我们对以下稀疏门控方法进行了实践:
- 方法一:通过散度衡量子场景与各专家之间的相似度,以此选择与当前场景最匹配的个专家。在实现方式上,使用场景*专家的二维矩阵计算相似性,并通过散度选择出最适合的个专家。
- 方法二:每个子场景配备一个专家选择门控网络,个场景则有个门控网络。对于每个场景的门控网络,配备个单专家选择器[9],每个单专家选择器负责从个专家中选择一个作为当前场景的专家(为Experts个数)。在实践中,为提高训练效率,我们对单专家选择器中权重较小的值进行截断,保证每个单专家选择器仅选择一个专家。
在离线实验中,我们以物料推荐渠道 * 展示形态作为场景定义,对上述稀疏门控方法进行了尝试,离线效果如下表2所示:
可以看出,基于软共享机制的专家聚合方法能够更好地通过所激活的相同专家网络对各场景之间的知识进行共享。相较于常见的以截断方式为主的门控网络,使用二进制编码的方式使得其在不损失其他专家网络信息的同时,能够更好地收敛到目标专家数量,同时其可微性使得其在以梯度为基础的优化算法中训练更加稳定。
同时,为了验证稀疏门控网络能否有效区分不同场景并捕捉到场景间差异性,我们使用=16个专家中选择=7个的例子,对验证集中不同场景下各专家的利用率、选择专家的平均权重进行了可视化分析(如图5-图7所示),实验结果表明该方法能够有效地选择出不同的专家对场景进行表达。
例如,图6中KP_1更多地选择第5个专家,而KP_2更倾向于选择第15个专家。并且,不同场景对各专家的使用率以及选择专家的平均权重也有着明显的差异性,表明该方法能够捕捉到细分场景下流量的差异性并进行差异化的表达。
实验证明,在通过大规模专家网络对每个场景进行建模的同时,基于软共享机制的专家聚合方法能够更好地通过所激活的相同专家网络对各场景之间的知识进行共享。 同时,为了进一步探索Experts个数对模型性能的影响,我们在方法二的基础上通过调整专家个数和topK比例设计了多组对比实验,实验结果如下表3所示:
从实验数据可以看出,大规模的Experts结构会带来正向的离线收益;并且随着选取专家个数比例的增加(表3横轴),模型整体的表现效果也有上升的趋势。
2.2 自适应场景聚合
理想情况下,一条请求(流量)可以看作一个独立的场景。但如引言所述,随着DSP业务持续发展,不同的物料展示渠道、形式、位置等持续增加,每个场景的数据十分稀疏,我们无法对每个细分场景进行有效训练。因此,我们需要对各个推荐场景进行聚类、合并。我们使用场景聚合的方法对此问题进行求解,通过衡量所有场景间的相似度,并最大化该相似度来指导场景的聚合,解决了数据稀疏导致难以收敛的问题。具体的,我们将该问题表示为:
其中表示某种分组方式,为场景在分组内与其他场景的总体相似度。在将个场景聚合成个场景组的过程中,我们需要找到使得场景间整体相似度最大的分组方式。
因此,我们在2.1节场景知识迁移模型的基础上,增加了场景聚合部分,提出了基于Two-Stage策略进行训练的场景聚合模型:
- Stage 1:基于相似度衡量方法对各场景的相似度进行归纳,并以最大化分组场景的相似度为目标找到各场景的最优聚合方式(如Scene1与Scene 4可聚合为场景组合Scene Gr0up SGA);
- Stage 2:基于Stage 1得到的场景聚合方式,以交叉熵损失为目标函数最小化各场景下的交叉熵损失。
其中,Stage 2与2.1节中所述一致,本节主要针对Stage 1进行阐述。我们认为,一个有效的场景聚合方法应该能自适应地应对流量变化的趋势,能够发现场景之间的内在联系并依据当前流量特点自动适配聚合方法。我们首先想到的是从规则出发,将人工先验知识作为场景聚合的依据,按照推荐渠道、展示形式以及两者叉乘的方式进行了相应迭代。然而这类场景聚合方式需要可靠的人工经验来支撑,且在应对海量流量时不能迅速捕捉到其中的变化。
因此,我们对场景之间关系的建模方法进行了相关的探索。首先,我们通过离线训练时场景之间的表征迁移和组合训练来评估场景之间的影响,但这种方式存在组合空间巨大、训练耗时较长的问题,效率较低。
在多任务的相关研究中[10][11][12][13],使用梯度信息对任务之间的关系进行建模是一种有效的方法。类似的在多场景模型中,能够根据各场景损失函数的梯度信息对场景间的相似度进行建模,因此我们采用多专家网络并基于梯度信息自动化地对场景之间的相似度进行求解,模型示意如下图8所示:
基于上述思路,我们对场景之间的关系建模方法进行了以下尝试:
1. Gradient Regulation
基于梯度信息能够对场景信息进行潜在表示这一认知,我们在损失函数中加入各场景损失函数关于专家层梯度距离的正则项,整体的损失函数如下所示,该正则项的系数表示场景之间的相似度,为常见的评估梯度之间距离的方法,比如,距离。
2. Lookahead Strategy
3. Meta Weights
Lookahead Strategy该方法对场景间的关系进行了显式建模,但是这种根据损失函数的变化计算场景相关系数的策略存在着训练不稳定、波动较大的现象,无法像Gradient Regulation这一方法对场景相似度进行求解。
因此,我们引入了场景间的相关性系数矩阵(meta weights),结合前两种方法对该问题进行如下建模,通过场景的数据对其与其他场景的相关性系数进行更新,同时基于该参数对全局的参数模型进行优化。针对这种典型的两层优化问题,我们基于MAML[14]方法进行求解,并将meta weights作为场景间的相似度。
我们以推荐渠道和展示形式(是否开屏)的多场景模型作为Base,对上述3种方法做了探索。为了提高训练效率,我们在设计 Stage 1 模型时做了以下优化:
我们对每个方法的GAUC进行了比较,实验效果如下表4所示。相较于人工规则,基于梯度的场景聚合方法都能带来效果的明显提升,表明损失函数梯度能在一定程度上表示场景之间的相似性,并指导多场景进行聚合。
为了更全面的展现场景聚合对于模型预估效果的影响,我们选取Meta Weights进行分组数量的调优实验,具体的实验结果如下表5所示。可以发现:随着分组数的增大,GAUC提升也越大,此时各场景间的负迁移效应减弱;但分组超过一定数量时,场景间总体的相似度减小,GAUC呈下降趋势。
此外,我们对Meta Weigts方法中部分场景间的关系进行了可视化分析,分析结果如下图9所示。以场景作为坐标轴,图中的每个方格表示各场景间的相似度,颜色的深浅表示渠道间的相似程度大小。
从图中可以发现,以渠道和展示形式为粒度的细分场景下,该方法能够学习到不同场景间的相关性,例如A渠道下的信息流(s16)与其他场景的相关性较低,会将其作为独立的场景进行预估,而B渠道下的开屏展示(s9)与C渠道开屏展示(s8)相关性较高,会将其聚合为一个场景进行预估,同时该相似度矩阵不是对称的,这也说明各场景间相互的影响存在着差异。
3 总结与展望
通过多场景学习的探索和实践,我们深入挖掘了推荐模型在不同场景下的建模能力,并分别从场景知识迁移、场景聚合方向进行了尝试和优化,这些尝试提供了更好的理解和解释推荐模型对不同类型流量和场景的应对能力。然而,这只是多场景学习研究的开始,后续我们会探索并迭代以下方向:
- 更好的场景划分方式:当前多场景的划分主要还是依据渠道(渠道*展示形态)作为流量的划分方式,未来会在媒体、展示位、媒体*时间等维度上进行更详细地探索;
- 端到端的流量聚合方式:在进行流量聚合时,使用了Two-Stage的策略进行聚合。然而,这种方式不能充分地利用流量数据中相关的信息。因此,需要探索端到端的流量场景聚合方案将更直接和有效地提高推荐模型的能力。
结合多场景学习,在未来的研究中将不断探索新的方法和技术,以提高推荐模型对不同场景和流量类型的建模能力,创造更好的用户体验以及商业价值。
4 作者简介
王驰、森杰、树立、文帅、尹华、肖雄等,均来自美团到家事业群/到家研发平台。
5 参考文献
- [1] STAR:Sheng, Xiang-Rong, et al. "One model to serve all: Star topology adaptive recommender for multi-domain ctr prediction." Proceedings of the 30th ACM International Conference on Information & Knowledge Management. 2021.
- [2] SAML:Chen, Yuting, et al. "Scenario-aware and Mutual-based approach for Multi-scenario Recommendation in E-Commerce." 2020 International Conference on Data Mining Workshops (ICDMW). IEEE, 2020.
- [3] ADIN:Jiang, Yuchen, et al. "Adaptive Domain Interest Network for Multi-domain Recommendation." Proceedings of the 31st ACM International Conference on Information & Knowledge Management. 2022.
- [4]SASS:Zhang, Yuanliang, et al. "Scenario-Adaptive and Self-Supervised Model for Multi-Scenario Personalized Recommendation." Proceedings of the 31st ACM International Conference on Information & Knowledge Management. 2022.
- [5] Squeeze-and-Excitation:Hu, Jie, Li Shen, and Gang Sun. "Squeeze-and-excitation networks." Proceedings of the IEEE conference on computer vision and pattern recognition. 2018.
- [6] 美团外卖推荐情境化智能流量分发的实践与探索
- [7] PaLM:ai.googleblog.com/2022/04/pat…
- [8] GLaM:proceedings.mlr.press/v162/du22c.…
- [9] 单专家选择器:arxiv.org/abs/2106.03…
- [10] HOA:proceedings.mlr.press/v119/standl…
- [11] Gradient Affinity:proceedings.neurips.cc/paper/2021/…
- [12] SRDML:dl.acm.org/doi/abs/10.…
- [13] Auto-Lambda:arxiv.org/abs/2202.03…
- [14] MAML:arxiv.org/abs/1703.03…
来源:juejin.cn/post/7278597227785551883
终于找到一个比较好用的前端国际化方案了
在开发Vue/React应用,一直对现有的多语言方案不是很满足,现在终于出了一个比较满意好用的了。
本节以标准的Nodejs
应用程序为例,简要介绍VoerkaI18n
国际化框架的基本使用。
vue
或react
应用的使用流程也基本相同,可以参考Vue集成和React集成。
myapp
|--package.json
|--index.js
在本项目的所有支持的源码文件中均可以使用t
函数对要翻译的文本进行包装,简单而粗暴。
// index.js
console.log(t("中华人民共和国万岁"))
console.log(t("中华人民共和国成立于{}",1949))
t
翻译函数是从myapp/languages/index.js
文件导出的翻译函数,但是现在myapp/languages
还不存在,后续会使用工具自动生成。voerkai18n
后续会使用正则表达式对提取要翻译的文本。
第一步:安装命令行工具
安装@voerkai18n/cli
到全局。
> npm install -g @voerkai18n/cli
> yarn global add @voerkai18n/cli
> pnpm add -g @voerkai18/cli
第二步:初始化工程
在工程目录中运行voerkai18n init
命令进行初始化。
> voerkai18n init
上述命令会在当前工程目录下创建languages/settings.json
文件。如果您的源代码在src
子文件夹中,则会创建在src/languages/settings.json
settings.json
内容如下:
{
"languages": [
{
"name": "zh",
"title": "zh"
},
{
"name": "en",
"title": "en"
}
],
"defaultLanguage": "zh",
"activeLanguage": "zh",
"namespaces": {}
}
上述命令代表了:
- 本项目拟支持
中文
和英文
两种语言。 - 默认语言是
中文
(即在源代码中直接使用中文) - 激活语言是
中文
(代表当前生效的语言)
注意:
- 可以修改该文件来配置支持的语言、默认语言、激活语言等。可支持的语言可参阅语言代码列表。
voerkai18n init
是可选的,voerkai18n extract
也可以实现相同的功能。- 一般情况下,您可以手工修改
settings.json
,如定义名称空间。 voerkai18n init
仅仅是创建languages
文件,并且生成settings.json
,因此您也可以自己手工创建。- 针对
js/typescript
或react/vue
等不同的应用,voerkai18n init
可以通过不同的参数来配置生成ts
文件或js
文件。 - 更多的
voerkai18n init
命令的使用请查阅这里
第三步:标识翻译内容
接下来在源码文件中,将所有需要翻译的内容使用t
翻译函数进行包装,例如下:
import { t } from "./languages"
// 不含插值变量
t("中华人民共和国")
// 位置插值变量
t("中华人民共和国{}","万岁")
t("中华人民共和国成立于{}年,首都{}",1949,"北京")
t
翻译函数只是一个普通函数,您需要为之提供执行环境,关于t
翻译函数的更多用法见这里
第四步:提取文本
接下来我们使用voerkai18n extract
命令来自动扫描工程源码文件中的需要的翻译的文本信息。 voerkai18n extract
命令会使用正则表达式来提取t("提取文本")
包装的文本。
myapp>voerkai18n extract
执行voerkai18n extract
命令后,就会在myapp/languages
通过生成translates/default.json
、settings.json
等相关文件。
- translates/default.json : 该文件就是从当前工程扫描提取出来的需要进行翻译的文本信息。所有需要翻译的文本内容均会收集到该文件中。
- settings.json: 语言环境的基本配置信息,包含支持的语言、默认语言、激活语言等信息。
最后文件结构如下:
myapp
|-- languages
|-- settings.json // 语言配置文件
|-- translates // 此文件夹是所有需要翻译的内容
|-- default.json // 默认名称空间内容
|-- package.json
|-- index.js
如果略过第一步中的voerkai18n init
,也可以使用以下命令来为创建和更新settings.json
myapp>voerkai18n extract -D -lngs zh en de jp -d zh -a zh
以上命令代表:
- 扫描当前文件夹下所有源码文件,默认是
js
、jsx
、html
、vue
文件类型。 - 支持
zh
、en
、de
、jp
四种语言 - 默认语言是中文。(指在源码文件中我们直接使用中文即可)
- 激活语言是中文(即默认切换到中文)
-D
代表显示扫描调试信息,可以显示从哪些文件提供哪些文本
第五步:人工翻译
接下来就可以分别对language/translates
文件夹下的所有JSON
文件进行翻译了。每个JSON
文件大概如下:
{
"中华人民共和国万岁":{
"en":"<在此编写对应的英文翻译内容>",
"de":"<在此编写对应的德文翻译内容>",
"jp":"<在此编写对应的日文翻译内容>",
"$files":["index.js"] // 记录了该信息是从哪几个文件中提取的
},
"中华人民共和国成立于{}":{
"en":"<在此编写对应的英文翻译内容>",
"de":"<在此编写对应的德文翻译内容>",
"jp":"<在此编写对应的日文翻译内容>",
"$files":["index.js"]
}
}
我们只需要修改该文件翻译对应的语言即可。
重点:如果翻译期间对源文件进行了修改,则只需要重新执行一下voerkai18n extract
命令,该命令会进行以下操作:
- 如果文本内容在源代码中已经删除了,则会自动从翻译清单中删除。
- 如果文本内容在源代码中已修改了,则会视为新增加的内容。
- 如果文本内容已经翻译了一部份了,则会保留已翻译的内容。
总之,反复执行voerkai18n extract
命令是安全的,不会导致进行了一半的翻译内容丢失,可以放心执行。
第六步:自动翻译
voerkai18n
支持通过voerkai18n translate
命令来实现调用在线翻译服务进行自动翻译。
>voerkai18n translate --appkey <在百度翻译上申请的密钥> --appid <在百度翻译上申请的appid>
在项目文件夹下执行上面的语句,将会自动调用百度的在线翻译API
进行翻译,以现在的翻译水平而言,您只需要进行少量的微调即可。关于voerkai18n translate
命令的使用请查阅后续介绍。
第七步:编译语言包
当我们完成myapp/languages/translates
下的所有JSON语言文件
的翻译后(如果配置了名称空间后,每一个名称空间会对应生成一个文件,详见后续名称空间
介绍),接下来需要对翻译后的文件进行编译。
myapp> voerkai18n compile
compile
命令根据myapp/languages/translates/*.json
和myapp/languages/settings.json
文件编译生成以下文件:
|-- languages
|-- settings.json // 语言配置文件
|-- idMap.js // 文本信息id映射表
|-- index.js // 包含该应用作用域下的翻译函数等
|-- storage.js
|-- zh.js // 语言包
|-- en.js
|-- jp.js
|-- de.js
|-- formatters // 自定义扩展格式化器
|-- zh.js
|-- en.js
|-- jp.js
|-- de.js
|-- translates // 此文件夹包含了所有需要翻译的内容
|-- default.json
|-- package.json
|-- index.js
第八步:导入翻译函数
第一步中我们在源文件中直接使用了t
翻译函数包装要翻译的文本信息,该t
翻译函数就是在编译环节自动生成并声明在myapp/languages/index.js
中的。
import { t } from "./languages"
因此,我们需要在需要进行翻译时导入该函数即可。
但是如果源码文件很多,重次重复导入t
函数也是比较麻烦的,所以我们也提供了一个babel/vite
等插件来自动导入t
函数,可以根据使用场景进行选择。
第九步:切换语言
当需要切换语言时,可以通过调用change
方法来切换语言。
import { i18nScope } from "./languages"
// 切换到英文
await i18nScope.change("en")
// 或者VoerkaI18n是一个全局单例,可以直接访问
await VoerkaI18n.change("en")
i18nScope.change
与VoerkaI18n.change
两者是等价的。
一般可能也需要在语言切换后进行界面更新渲染,可以订阅事件来响应语言切换。
import { i18nScope } from "./languages"
// 切换到英文
i18nScope.on("change",(newLanguage)=>{
// 在此重新渲染界面
...
})
//
VoerkaI18n.on("change",(newLanguage)=>{
// 在此重新渲染界面
...
})
@voerkai18n/vue和@voerkai18n/react提供了相对应的插件和库来简化重新界面更新渲染。
第十步:语言包补丁
一般情况下,多语言的工程化过程就结束了,voerkai18n
在多语言实践考虑得更加人性化。有没有经常发现这样的情况,当项目上线后,才发现:
- 翻译有误
- 客户对某些用语有个人喜好,要求你更改。
- 临时要增加支持一种语言
一般碰到这种情况,只好重新打包构建工程,重新发布,整个过程繁琐而麻烦。 现在voerkai18n
针对此问题提供了完美的解决方案,可以通过服务器来为应用打语言包补丁
和动态增加语言
支持,而不需要重新打包应用和修改应用。
方法如下:
- 注册一个默认的语言包加载器函数,用来从服务器加载语言包文件。
import { i18nScope } from "./languages"
i18nScope.registerDefaultLoader(async (language,scope)=>{
return await (await fetch(`/languages/${scope.id}/${language}.json`)).json()
})
- 将语言包补丁文件保存在Web服务器上指定的位置
/languages/<应用名称>/<语言名称>.json
即可。 - 当应用启动后会自动从服务器上加载语言补丁包合并,从而实现动为语言包打补丁的功能。
- 利用该特性也可以实现动态增加临时支持一种语言的功能
来源:juejin.cn/post/7275944565885485116
前段时间面试了一些人,有这些槽点跟大家说说
大家好,我是拭心。
前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。
简历书写和自我介绍
- 今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备
- 去年工作经历都是小公司的还有几个进了面试,今年基本没有,在 HR 第一关就被刷掉了
- 这种情况的,一定要走内推,让内推的人跟 HR 打个招呼:这人技术不错,让用人部门看看符不符合要求
- 用人部门筛简历也看学历经历,但更关注这几点:过去做了什么项目、项目经验和岗位对不对口、项目的复杂度怎么样、用到的技术栈如何、他在里面是什么角色
- 如果项目经历不太出彩,简历上可以补充些学习博客、GitHub,有这两点的简历我都会点开仔细查看,印象分会好很多
- 现在基本都视频面试,面试的时候一定要找个安静的环境、体态认真的回答。最好别用手机,否则会让人觉得不尊重!
- 我面过两个神人,一个在马路上边走边视频;另一个聊着聊着进了卫生间,坐在马桶上和我讲话(别问我怎么知道在卫生间的,他努力的声音太大了。。。)
- 自我介绍要自然一点,别像背课文一样好吗亲。面试官不是考你背诵,是想多了解你一点,就当普通聊天一样自然点
- 介绍的时候不要过于细节,讲重点、结果、数据,细节等问了再说
- 准备介绍语的时候问问自己,别人可以得到什么有用的信息、亮点能不能让对方快速 get 到
- 实在不知道怎么介绍,翻上去看第 4 点和第 5 点
- 出于各种原因,很多面试官在面试前没看过你的简历,在你做自我介绍时,他们也在一心二用 快速地浏览你的简历。所以你的自我介绍最好有吸引人的点,否则很容易被忽略
- 你可以这样审视自己的简历和自我介绍:
a. 整体:是否能清晰的介绍你的学历、工作经历和技能擅长点
b. 工作经历:是否有可以证明你有能力、有结果的案例,能否从中看出你的能力和思考
c. 技能擅长点:是否有岗位需要的大部分技能,是否有匹配工作年限的复杂能力,是否有区别于其他人的突出点
面试问题
- 根据公司规模、岗位级别、面试轮数和面试官风格,面试的问题各有不同,我们可以把它们简单归类为:项目经历、技能知识点和软素质
- 一般公司至少有两轮技术面试 + HR 面试,第一轮面试官由比岗位略高一级的人担任,第二轮面试官由用人部门领导担任
- 不同轮数考察侧重点不同。第一轮面试主要确认简历真实性和基础技术能力,所以主要会围绕项目经历和技能知识点;第二轮面试则要确认这个人是否适合岗位、团队,所以更偏重过往经历和软素质
项目经历
项目经历就是我们过往做过的项目。
项目经历是最能体现一个程序员能力的部分,因此面试里大部分时间都在聊这个。
有朋友可能会说:胡说,为什么我的面试大部分时候都是八股文呢?
大部分都是八股文有两种可能:要么是初级岗位、要么是你的经历没什么好问的。哦还有第三种可能,面试官不知道问什么,从网上搜的题。
在项目经历上,面试者常见的问题有这些:
- 不重要的经历占比过多(比如刚毕业的时候做的简单项目花了半页纸)
- 经历普通,没有什么亮点(比如都是不知名项目,项目周期短、复杂度低)
- 都是同质化的经历,看不出有成长和沉淀(比如都是 CRUD、if visible else gone)
出现这种情况,是因为我们没有从面试官的角度思考,不知道面试的时候对方都关注什么。
在看面试者的项目经历时,面试官主要关注这三点:
1. 之前做的项目有没有难度
2. 项目经验和当前岗位需要的是否匹配
3. 经过这些项目,这个人的能力有哪些成长
因此,我们在日常工作和准备面试时,可以这样做:
- 工作时有意识地选择更有复杂度的,虽然可能花的时间更多,但对自己的简历和以后发展都有好处
- 主动去解决项目里的问题,解决问题是能力提升的快车道,解决的问题越多、能力会越强
- 解决典型的问题后,及时思考问题的本质是什么、如何解决同一类问题、沉淀为文章、记录到简历,这些都是你的亮点
- 经常复盘,除了公司要求的复盘,更要做自己的复盘,复盘这段时间里有没有成长
- 简历上,要凸显自己在项目面试的挑战、解决的问题,写出自己如何解决的、用到什么技术方案
- 投简历时,根据对方业务类型和岗位要求,适当的调整项目经历里的重点,突出匹配的部分
- 面试时,要强调自己在项目里的取得的成果、在其中的角色、得到什么可复制的经验
技能知识点
技能知识点就是我们掌握的编程语言、技术框架和工具。
相较于项目经历,技能知识点更关键,因为它决定了面试者是否能够胜任岗位。
在技能知识点方面,面试者常见的问题有这些:
- 不胜任岗位:基础不扎实,不熟悉常用库的原理
- 技术不对口:没有岗位需要的领域技术
- 技术过剩:能力远远超出岗位要求
第一种情况就是我们常说的“技术不行”。很多人仅仅在工作里遇到不会的才学习,工作多年也没有自己的知识体系,在面试的时候很容易被基础知识点问倒,还给自己找理由说“我是高级开发还问这么细节的,面试官只会八股文”。框架也是浅尝辄止,会用就不再深入学了,这在面试的时候也很容易被问住。
第二种情况,是岗位工作内容属于细分领域,但面试者不具备这方面的经验,比如音视频、跨端等。为了避免这种情况,我们需要打造自己的细分领域技能,最好有一个擅长的方向,越早越好。
第三种情况简单的来说就是“太贵了”。有时候一些资深点的开发面试被挂掉,并不是因为你的能力有问题,而是因为岗位的预算有限。大部分业务需求都是增删改查和界面展示,并不需要多复杂的经验。这种情况下,要么再去看看更高级的岗位,要么降低预期。
在我面试的人里,通过面试的都有这些特点:
- 技术扎实:不仅仅基础好,还有深度
- 解决过复杂的问题:项目经验里除了完成业务需求,也有做一些有挑战的事
有些人的简历上只写项目经历不写技能知识点,对此我是反对的,这样做增加了面试官了解你的成本。问项目经历的目的还是想确认你有什么能力,为什么不直接明了的写清楚呢?
软素质
这里的「软素质」指面试时考察的、技术以外的点。
程序员的日常工作里,除了写代码还需要做这些事:
- 理解业务的重点和不同需求的核心点,和其他同事协作完成
- 从技术角度,对需求提出自己的思考和建议,反馈给其他人
- 负责某个具体的业务/方向,成为这个方面所有问题的处理者
因此,面试官或者 HR 还会考察这些点,以确保面试者具备完成以上事情的能力:
- 理解能力和沟通表达能力
- 业务能力
- 稳定性
第一点是指面试者理解问题和讲清楚答案的能力。遇到过一些面试者,面试的时候过于紧张,讲话都讲不清楚,这种就让人担心“会不会是个社恐”、“工作里该不会也这样说不清楚吧”;还有的人爱抢答,问题都没听明白就开始抢答,让人怀疑是不是性格太急躁太自大;还有的人过于能讲,但讲不到重点,东扯西扯,让人对他的经历和理解能力产生了怀疑。
第二点是指在实现业务目标的过程中可以提供的能力。 业务发展是需要团队共同努力的,但有的人从来没这么想过,觉得自己上班的任务就是写代码,来什么活干什么活,和外包一样。
业务发展中可能有各种问题。定方向的领导有时候会过于乐观、跨部门协作项目可能会迟迟推进不动、产品经理有时候也会脑子进水提无用需求、质量保障的测试同学可能会大意漏掉某个细节测试。这个时候,程序员是否能够主动站出来出把力,帮助事情向好的方向发展,就很重要了。
遇到过一些面试者,在一家公司干了好几年,问起来业务发展情况语焉不详,让人感觉平时只知道写代码;还有的面试者,说起业务问题抱怨指责一大堆,“领导太傻逼”、“产品经理尽提蠢需求”,负能量满满😂。
第三点是指面试者能不能在一家公司长久干下去。 对于级别越高的人,这点要求就越高,因为他的离开对业务的发展会有直接影响。即使级别不高,频繁换工作也会让人对你有担心:会不会抗压能力很差、会不会一不涨工资就要跑路。一般来说,五年三跳就算是临界线,比这个频繁就算是真的“跳的有点多”。
针对以上这三点,我们可以这样做:
- 面试时调整心态,当作普通交流,就算不会也坦然说出,不必过于紧张
- 回答问题时有逻辑条理,可以采用类似总分总的策略
- 工作时多关注开发以外的事,多体验公司产品和竞品,在需求评审时不摸鱼、多听听为什么做、思考是否合理、提出自己的想法
- 定好自己的职业规划(三年小进步、五年大进步),在每次换工作时都认真问问自己:下一份工作能否帮助自己达到目标
总结
好了,这就是我前段时间面试的感悟和吐槽。
总的来说,今年找工作的人不少,市面上的岗位没有往年那么多。如果你最近要换工作,最好做足准备。做好后面的规划再换、做好准备再投简历、经历整理清楚再面试。
来源:juejin.cn/post/7261604248319918136
使用Tauri快速搭建桌面项目
什么是 Tauri
Tauri 是一个跨平台的 GUI
框架,与 Electron
的理念相似。Tauri 的前端部分同样基于 Web 技术,但它的后端则采用了 Rust
语言。Tauri 可以创建体积更小、运行更快且更加安全的跨平台桌面应用。
与 Electron
不同,Tauri 并没有内置 Chromium
,因此打包后的应用体积要比 Electron
小很多,启动速度更快,内存和 CPU 占用率也更低。
然而,由于没有内置 Chromium
,Tauri 使用系统原生的 WebView 来渲染网页,这可能导致不同系统之间的页面表现存在差异。同时,Tauri 的后端需要使用 Rust
进行开发,这对前端开发人员来说可能会有一定的上手成本。
好在 Tauri 已经为我们封装了大部分 API,即使不懂 Rust
,也可以开发出一款简单的应用。
预先准备
我们以 macOS 为例:
1. 首先安装 Xcode 命令行工具
在终端中执行以下命令:
xcode-select --install
如果已经安装过 Xcode 命令行工具,则可以直接进行下一步。
2. 安装 Rust
在 macOS 上安装 Rust,请打开终端并输入以下命令:
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
安装成功后,终端将显示以下内容:
Rust is installed now. Great!
请确保重新启动终端以使更改生效。
快速开始
创建项目
Tauri 官方提供了多种项目模板,可以快速搭建项目:
# pnpm
pnpm create tauri-app
# npm
npm create tauri-app
# yarn
yarn create tauri-app
按照提示选择自己喜欢的模板。
这里我们选择 react
开发前端页面。
一路回车后,打开项目文件夹。执行安装依赖命令:pnpm i
。依赖安装完成后,执行 pnpm tauri dev
命令启动项目。这时便会启动一个应用,如下图所示:
开发
Tauri 的开发非常容易上手,我们先来看下项目文件结构:
是不是和 Vite 的目录结构一样?
没错,这就是一个常规的 Vite 目录结构,唯一的区别是增加了一个 src-tauri
文件夹,这里面是 Rust
部分的代码,也就是后端代码。
打包
首先,我们需要修改默认的包标识符,位置在 src-tauri > tauri.conf.json > tauri > bundle > identifier
。
这里我们随便填写一个标识符 com.example.app
,保存,然后执行命令:pnpm tauri build
就可以正常打包了。
tauri.conf.json
文件是我们的应用配置文件,包含了应用的基本信息。
打包完成后,就可以在 tauri-app/src-tauri/target/release/bundle
目录下找到我们的应用。
现在我们只构建了 macOS 下的应用。
打开之后就可以看到我们的应用了。
参考文档
来源:juejin.cn/post/7388842078798823433
DDD项目落地之充血模型实践 | 京东云技术团队
背景:
充血模型是DDD分层架构中实体设计的一种方案,可以使关注点聚焦于业务实现,可有效提升开发效率、提升可维护性;
1、DDD项目落地整体调用关系
调用关系图中的Entity为实体,从进入领域服务(Domin)时开始使用,直到最后返回。
2、实体设计
充血模型是实体设计的一种方法,简单来说,就是一种带有具体行为方法和聚合关联关系的特殊实体;
关于实体设计,需要明白的关键词为:领域服务->聚合->聚合根->实体->贫血模型->充血模型
聚合与聚合根:
聚合是一种关联关系,而聚合根就是这个关系成立的基础,没有聚合根,这个聚合关系就无法成立;
举个例子,存在3个实体:用户、用户组、用户组关联关系,这3个实体形成的关联关系就是聚合,而用户实体就是这个聚合中的聚合根;
实体:
定义在领域层,是领域层的重要元素,从领域划分到工程实践落地,都应该围绕实体进行,DDD中的实体和数据库表不只是1对1关系,可能是1对多或者仅为内存中的对象;
贫血模型:
实体不带有任何行为方法,也不带有聚合关联关系,作用基本相当于值对象(ValueObject),仅作为值传递的对象,和传统三层项目架构中的实体具有相同作用,不建议使用。补充说明:一般我们使用的DTO就可以被当做是值对象
充血模型:
实体中带有具有行为方法和聚合关联关系,行为方法是说create、save、delete等封装了一类可以指代行为的方法,比如在用户实体对象中具有用户组实体的引用,这样当我们需要操作用户组时,只通过用户实体进行操作就可以。
工程实践中,建议采用充血模型,好处是隐藏胶水代码,提升代码可读性,使关注点聚焦于业务实现。
充血模型在实践中的问题:
行为代码量过多,导致实体内部臃肿膨胀,难以阅读,难以维护,对于这种问题,我们需要根据实体行为的代码量多少来采取不同的解决方案。
解决方案:
场景1:行为不会导致实体臃肿的情况下,在实体中完成行为定义
public CooperateServicePackageConfig save() {
// 直接调用基础设施层进行保存
cooperateServicePackageConfigRepository.save(this);
return this;
}
场景2:行为导致实体臃肿的情况下,采用外部定义行为的方式,核心思想是借助其他类实现行为代码定义,将臃肿代码外移,保留干净的实体行为:
1)创建工具类,将某个实体中的行为定义其中,实体负责调用该工具类
public CooperateServicePackageConfig save() {
// 将处理过程放在工具类中
ServicePackageSaveUtils.save(this);
return this;
}
2)创建新实体,将该实体的使用场景明确至某个细分行为,比如一个聚合根(ExampleEntity)的保存可能涉及到5个实体的保存,那么我们定义一个ExampleSaveEntity实体,专门用来处理该聚合下的保存行为
实践经验:
1、关于spring bean注入:充血模型在实体中使用静态注入方法实现。例:
private LabelInfoRepository labelInfoRepository = ApplicationContextUtils.getBean(LabelInfoRepository.class);
2、充血模型的实体序列化,排除非必要属性,在一些redis对象缓存时可能会用到。例:
// 使用注解排除序列化属性
@Getter(AccessLevel.NONE)
private LabelInfoRepository labelInfoRepository = ApplicationContextUtils.getBean(LabelInfoRepository.class);
// 使用注解排除序列化属性
@JSONField(serialize = false)
private ServicePackageConfig servicePackageConfig;
// 使用注解排除序列化 get 方法
@Transient
@JSONField(serialize = false)
public static CooperateServicePackageRepositoryQuery getAllCodeQuery(Long contractId) {
CooperateServicePackageRepositoryQuery repositoryQuery = new CooperateServicePackageRepositoryQuery();
repositoryQuery.setContractIds(com.google.common.collect.Lists.newArrayList(contractId));
repositoryQuery.setCode(RightsPlatformConstants.CODE_ALL);
return repositoryQuery;
}
3、利用Set方法建立聚合绑定关系。例:
public void setServiceSkuInfos(List<ServiceSkuInfo> serviceSkuInfos) {
if (CollectionUtils.isEmpty(serviceSkuInfos))
{
return;
}
this.serviceSkuInfos = serviceSkuInfos;
List<String> allSkuNoSet = serviceSkuInfos
.stream()
.map(one -> one.getSkuNo())
.collect(Collectors.toList());
String skuJoinStr = Joiner.on(GlobalConstant.SPLIT_CHAR).join(allSkuNoSet);
this.setSkuNoSet(skuJoinStr);}
作者:京东健康 张君毅
来源:京东云开发者社区
来源:juejin.cn/post/7264235181778190373
学会Grid之后,我觉得再也没有我搞不定的布局了
说到布局很多人的感觉应该都是恐惧,为此很多人都背过一些很经典的布局方案,例如:圣杯布局
、双飞翼布局
等非常耳熟的名词;
为了实现这些布局我们有很多种实现方案,例如:table布局
、float布局
、定位布局
等,当然现在比较流行的肯定是flex布局
;
flex布局
属于弹性布局,所谓弹性也可以理解为响应式布局
,而同为响应式布局的还有Grid布局
;
Grid布局
是一种二维布局,可以理解为flex布局
的升级版,它的出现让我们在布局方面有了更多的选择,废话不多说,下面开始全程高能;
本篇不会过多介绍
grid
的基础内容,更多的是一些布局的实现方案和一些小技巧;
常见布局
所谓的常见布局只是我们在日常开发中经常会遇到的布局,例如:圣杯布局
、双飞翼布局
这种名词我个人觉得不用太过于去在意;
因为这类布局最后的解释都会变成几行几列
,内容在哪一行哪一列,而这些就非常直观的对标了grid
的特性;
接下来我们来一起看看一些非常常见的布局,并且用grid
来实现;
1. 顶部 + 内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-rows: 60px 1fr;
height: 100vh;
}
.header {
background-color: #039BE5;
}
.content {
background-color: #4FC3F7;
}
.header,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="content">Content</div>
</body>
</html>
2. 顶部 + 内容 + 底部
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-rows: 60px 1fr 60px;
height: 100vh;
}
.header {
background-color: #039BE5;
}
.content {
background-color: #4FC3F7;
}
.footer {
background-color: #039BE5;
}
.header,
.content,
.footer {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="content">Content</div>
<div class="footer">Footer</div>
</body>
</html>
这里示例和上面的示例唯一的区别就是多了一个
footer
,但是我们可以看到代码并没有多少变化,这就是grid
的强大之处;
可以看
码上掘金
的效果,这里的内容区域是单独滚动的,从而实现了header
和footer
固定,内容区域滚动的效果;
实现这个效果也非常简单,只需要在
content
上加上overflow: auto
即可;
3. 左侧 + 内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-columns: 240px 1fr;
height: 100vh;
}
.left {
background-color: #039BE5;
}
.content {
background-color: #4FC3F7;
}
.left,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="left">Left</div>
<div class="content">Content</div>
</body>
</html>
这个示例效果其实和第一个是类似的,只不过是把
grid-template-rows
换成了grid-template-columns
,这里就不提供码上掘金
的示例了;
4. 顶部 + 左侧 + 内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-rows: 60px 1fr;
grid-template-columns: 240px 1fr;
height: 100vh;
}
.header {
grid-column: 1 / 3;
background-color: #039BE5;
}
.left {
background-color: #4FC3F7;
}
.content {
background-color: #99CCFF;
}
.header,
.left,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="left">Left</div>
<div class="content">Content</div>
</body>
</html>
这个示例不同点在于
header
占据了两列,这里我们可以使用grid-column
来实现,grid-column
的值是start / end
,例如:1 / 3
表示从第一列到第三列;
如果确定这一列是占满整行的,那么我们可以使用
1 / -1
来表示,这样如果后续变成顶部 + 左侧 + 内容 + 右侧
的布局,那么header
就不需要修改了;
5. 顶部 + 左侧 + 内容 + 底部
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-areas:
"header header"
"left content"
"left footer";
grid-template-rows: 60px 1fr 60px;
grid-template-columns: 240px 1fr;
height: 100vh;
}
.header {
grid-area: header;
background-color: #039BE5;
}
.left {
grid-area: left;
background-color: #4FC3F7;
}
.content {
grid-area: content;
background-color: #99CCFF;
}
.footer {
grid-area: footer;
background-color: #6699CC;
}
.header,
.left,
.content,
.footer {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="left">Left</div>
<div class="content">Content</div>
<div class="footer">Footer</div>
</body>
</html>
这个示例的小技巧是使用了
grid-template-areas
,使用这个属性可以让我们通过代码来直观的看到布局的样式;
这里的值是一个字符串,每一行代表一行,每个字符代表一列,例如:
"header header"
表示第一行的两列都是header
,这里的header
是我们自己定义的,可以是任意值;
定义好了之后就可以在对应的元素上使用
grid-area
来指定对应的区域,这里的值就是我们在grid-template-areas
中定义的值;
在
码上掘金
中的效果可以看到,左侧的菜单和内容区域都是单独滚动的,这里的实现方式和第二个示例是一样的,只需要需要滚动的元素上加上overflow: auto
即可;
响应式布局
响应式布局指的是页面的布局会随着屏幕的大小而变化,这里的变化可以是内容区域大小可以自动调整,也可以是页面布局随着屏幕大小进行自动调整;
这里我就用掘金的页面来举例,这里只提供一个思路,所以不会像上面那样提供那么多示例;
1. 基础布局实现
移动端布局
以移动端的效果开始,掘金的移动端的布局就是上面的效果,这里我简单的将页面分为了三个部分,分别是
header
、navigation
、content
;
注:这里不是要
100%
还原掘金的页面,只是为了演示grid
布局,具体页面结构和最后实现的效果会有非常大的差异,这里只会实现一些基础的布局;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-areas:
"header"
"navigation"
"content";
grid-template-rows: 60px 48px 1fr;
height: 100vh;
}
.header {
grid-area: header;
background-color: #039BE5;
}
.navigation {
grid-area: navigation;
background-color: #4FC3F7;
}
.content {
grid-area: content;
background-color: #99CCFF;
}
.header,
.navigation,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
</body>
</html>
iPad布局
这里是需要借助媒体查询来实现的,在媒体查询中只需要调整一下
grid-template-rows
和grid-template-columns
的值即可;
由于这里的效果是上面一个的延伸,为了阅读体验会移除上面相关的
css
代码,只保留需要修改的代码;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.right {
display: none;
background-color: #6699CC;
}
@media (min-width: 1000px) {
body {
grid-template-areas:
"header header"
"navigation navigation"
"content right";
grid-template-columns: 1fr 260px;
}
.right {
grid-area: right;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
<div class="right">Right</div>
</body>
</html>
PC端布局
和上面处理方式相同,由于
Navigation
移动到了左侧,所以还要额外的修改一下grid-template-areas
的值;
这里就可以体现
grid
的强大之处了,我们可以简单的修改grid-template-areas
就可以实现一个完全不同的布局,而且代码量非常少;
为了居中显示内容,我们需要在左右两侧加上一些空白区域,可以简单的使用
.
来实现,这里的.
表示一个空白区域;
由于内容的宽度基本上是固定的,所以留白区域简单的使用
1fr
进行占位即可,这样就可以平均的分配剩余的空间;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
@media (min-width: 1220px) {
body {
grid-template-areas:
"header header header header header"
". navigation content right .";
grid-template-columns: 1fr 180px minmax(0, 720px) 260px 1fr;
grid-template-rows: 60px 1fr;
}
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
<div class="right">Right</div>
</body>
</html>
完善一些细节
最终的布局大概就是上图这样,这里主要处理的各个版块的间距和响应式内容区域的大小,这里的处理方式主要是使用
column-gap
和一个空的区域进行占位来实现的;
这里的
column-gap
表示列与列之间的间距,值可以是px
、em
、rem
等基本的长度属性值,也可以使用计算函数,但是不能使用弹性值fr
;
空区域进行占位留间距其实我并不推荐,这里只是演示
grid
布局可以实现的一些功能,具体的实现方式还是要根据实际情况来定,这里我更推荐使用margin
来实现;
完整代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-areas:
"header header header"
"navigation navigation navigation"
". . ."
". content .";
grid-template-columns: 1fr minmax(0, 720px) 1fr;
grid-template-rows: 60px 48px 10px 1fr;
column-gap: 10px;
height: 100vh;
}
.header {
grid-area: header;
background-color: #039BE5;
}
.navigation {
grid-area: navigation;
background-color: #4FC3F7;
}
.content {
grid-area: content;
background-color: #99CCFF;
}
.right {
display: none;
background-color: #6699CC;
}
@media (min-width: 1000px) {
body {
grid-template-areas:
"header header header header"
"navigation navigation navigation navigation"
". . . ."
". content right .";
grid-template-columns: 1fr minmax(0, 720px) 260px 1fr;
}
.right {
grid-area: right;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
}
@media (min-width: 1220px) {
body {
grid-template-areas:
"header header header header header"
". . . . ."
". navigation content right .";
grid-template-columns: 1fr 180px minmax(0, 720px) 260px 1fr;
grid-template-rows: 60px 10px 1fr;
}
}
.header,
.navigation,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
<div class="right">Right</div>
</body>
</html>
简单复刻版
以
码上掘金
上的效果来说已经完成了大部分的布局和一些效果,目前来说就是还差一些交互,还有一些细节上的处理,感兴趣的同学可以自行完善;
异型布局
异性布局指的是页面中的元素不是按照常规的流式布局进行排版,又或者说不规则的布局,这里我简单的列出几个布局,来看看grid
是如何实现的;
1. 照片墙
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
background: #f2f3f5;
overflow: auto;
}
body {
display: grid;
grid-template-columns: repeat(12, 100px);
grid-auto-rows: 100px;
place-content: center;
gap: 6px;
height: 100vh;
}
.photo-item {
width: 200px;
height: 200px;
clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}
</style>
</head>
<body>
</body>
<script>
function randomColor() {
return '#' + Math.random().toString(16).substr(-6);
}
let row = 1;
let col = 1;
for (let i = 0; i < 28; i++) {
const div = document.createElement('div');
div.className = 'photo-item';
div.style.backgroundColor = randomColor();
div.style.gridRow = `${row} / ${row + 2}`;
div.style.gridColumn = `${col} / ${col + 2}`;
document.body.appendChild(div);
col += 2;
if (col > 11) {
row += 1;
col = row % 2 === 0 ? 2 : 1;
}
}
</script>
</html>
这是一个非常简单的照片墙效果,如果不使用
grid
的话,我们大概率是会使用定位去实现这个效果,但是换成grid
的话就非常简单了;
而且代码量是非常少的,这里就不提供
码上掘金
的 demo 了,感兴趣的同学可以将代码复制下来自行查看效果;
2. 漫画效果
在漫画中有很多类似这种不规则的漫画框,如果使用定位的话,那么代码量会非常大,而且还需要计算一些位置,使用
grid
的话就非常简单了;
可以看到这里还有一个气泡文字显示的效果,按照页面书写顺序,气泡是不会显示的,这里我们可以使用
z-index
来实现,这里的z-index
值越大,元素就越靠前;
而且气泡文字效果也是通过
grid
来进行排版的,并没有使用其他的布局来实现,代码量也并不多,感兴趣的同学可以自行查看;
3. 画报效果
在一个画报中,我们经常会看到文字和图片混合排版的效果,由于这里直接使用的是渐变的背景,而且我的文字都是随意进行排列的,没有什么规律,所以看起来会比较混乱;
在画报效果中看似文字排版非常混乱不规则,但是实际上设计师在设计的时候也是会划分区域的,当然用定位也是没问题的,但是使用
grid
的话就会简单很多;
我这里将页面划分为
12 * 12
区域的网格,然后依次对不同的元素进行单独排列和样式的设置;
流式布局
流式布局指的是页面的内容会随着屏幕的大小而变化,流式布局也可以理解为响应式布局;
但是不同于响应式布局的是,流式布局的布局不会像响应式布局那样发生变化,只是内容会随着轴进行流动;
通常这种指的是grid-template-columns: repeat(auto-fit, minmax(0, 1fr))
这种;
直接看效果:
这里有两个关键字,一个是
auto-fit
,还有一个是auto-fill
,在行为上它们是相同的,不同的是它们在网格创建的不同,
就像上面图中看到的一样,使用
auto-fit
会将空的网格进行折叠,可以看到他们的结束colum
的数字都是6
;
像我们上面的实例中不会出现这个问题,因为我们使用了响应式单位
fr
,只有使用固定单位才会出现这个现象;
感兴趣的同学可以将
minmax(200px, 1fr)
换成200px
尝试;
对比 Flex 布局
在我上面介绍了这么多的布局场景和案例,其实可以很明显的发现一件事,那就是我使用grid
进行的布局基本上都是大框架;
当然上面也有一些布局使用flex
也是可以实现的,但是我们再换个思路,除了flex
可以做到上面的一些布局,float
布局、table
布局、定位布局其实也都能实现;
不同的是float
布局、table
布局、定位布局基本上都是一些hack
的方案,就拿table
布局来说,table
本身就是一个html
标签,作用就是用来绘制表格,被拿来当做布局的一种方案也是迫不得已;
而web布局
发展到现在的我们有了正儿八经可以布局的方案flex
,为什么又要出一个grid
呢?
grid
的出现绝对不是用来替代flex
的,在我上面的实现的一些布局案例中,也可以看到我还会使用flex
;
我个人理解的是使用grid
进行主体的大框架的搭建,flex
作为一些小组件的布局控制,两者搭配使用;
flex
能实现一些grid
不好实现的布局,同样grid
也可以实现flex
实现困难的布局;
本身它们的定位就不痛,flex
作为一维布局的首选,grid
定位就是比flex
高一个维度,它的定位是二维布局,所以他们之间没有必要进行对比,合理使用就好;
总结
上面介绍的这么多基于grid
布局实现的布局方案,足以看出grid
布局的强大;
grid
布局的体系非常庞大,本文只是梳理出一些常见的布局场景,通过grid
布局去实现这些布局,来体会grid
带来的便利;
可能需要完全理解我上面的全部示例需要对grid
有一定的了解才可以,但是都看到这里了,不妨去深挖一下;
grid
布局作为一项强大的布局技术,有望在未来继续发展,除了我上面说到的布局,grid
还有很多小技巧来实现非常多的布局场景;
碍于我的见识和文笔的限制,我这次介绍grid
肯定是有很多不足的,但是还是希望这篇文章能为你对于布局相关能有新的认识;
来源:juejin.cn/post/7310423470546354239
域名还能绑定动态IP?真是又涨见识了,再也不用购买固定IP了!赶快收藏
大家好,我是冰河~~
一般家庭网络的公网IP都是不固定的,而我又想通过域名来访问自己服务器上的应用,也就是说:需要通过将域名绑定到动态IP上来实现这个需求。于是乎,我开始探索实现的技术方案。
通过在网上查阅一系列的资料后,发现阿里云可以做到实现动态域名解析DDNS。于是乎,一顿操作下来,我实现了域名绑定动态IP。这里,我们以Python为例实现。
小伙伴们注意啦:Java版源码已提交到:github.com/binghe001/m…
好了,说干就干,我们开始吧,走起~~
阿里云DDNS前置条件
- 域名是在阿里云购买的
- 地址必须是公网地址,不然加了解析也没有用
通过阿里云提供的SDK,然后自己编写程序新增或者修改域名的解析,达到动态解析域名的目的;主要应用于pppoe拨号的环境,比如家里设置了服务器,但是外网地址经常变化的场景;再比如公司的pppoe网关,需要建立vpn的场景。
安装阿里云SDK
需要安装两个SDK库,一个是阿里云核心SDK库,一个是阿里云域名SDK库;
阿里云核心SDK库
pip install aliyun-python-sdk-core
阿里云域名SDK库
pip install aliyun-python-sdk-domain
阿里云DNSSDK库
pip install aliyun-python-sdk-alidns
设计思路
- 获取阿里云的accessKeyId和accessSecret
- 获取外网ip
- 判断外网ip是否与之前一致
- 外网ip不一致时,新增或者更新域名解析记录
实现方案
这里,我直接给出完整的Python代码,小伙伴们自行替换AccessKey和AccessSecret。
#!/usr/bin/env python
#coding=utf-8
# 加载核心SDK
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ClientException
from aliyunsdkcore.acs_exception.exceptions import ServerException
# 加载获取 、 新增、 更新、 删除接口
from aliyunsdkalidns.request.v20150109 import DescribeSubDomainRecordsRequest, AddDomainRecordRequest, UpdateDomainRecordRequest, DeleteDomainRecordRequest
# 加载内置模块
import json,urllib
# AccessKey 和 Secret 建议使用 RAM 子账户的 KEY 和 SECRET 增加安全性
ID = 'xxxxxxx'
SECRET = 'xxxxxx'
# 地区节点 可选地区取决于你的阿里云帐号等级,普通用户只有四个,分别是杭州、上海、深圳、河北,具体参考官网API
regionId = 'cn-hangzhou'
# 配置认证信息
client = AcsClient(ID, SECRET, regionId)
# 设置主域名
DomainName = 'binghe.com'
# 子域名列表 列表参数可根据实际需求增加或减少值
SubDomainList = ['a', 'b', 'c']
# 获取外网IP 三个地址返回的ip地址格式各不相同,3322 的是最纯净的格式, 备选1为 json格式 备选2 为curl方式获取 两个备选地址都需要对获取值作进一步处理才能使用
def getIp():
# 备选地址:1, http://pv.sohu.com/cityjson?ie=utf-8 2,curl -L tool.lu/ip
with urllib.request.urlopen('http://www.3322.org/dyndns/getip') as response:
html = response.read()
ip = str(html, encoding='utf-8').replace("\n", "")
return ip
# 查询记录
def getDomainInfo(SubDomain):
request = DescribeSubDomainRecordsRequest.DescribeSubDomainRecordsRequest()
request.set_accept_format('json')
# 设置要查询的记录类型为 A记录 官网支持A / CNAME / MX / AAAA / TXT / NS / SRV / CAA / URL隐性(显性)转发 如果有需要可将该值配置为参数传入
request.set_Type("A")
# 指定查记的域名 格式为 'test.binghe.com'
request.set_SubDomain(SubDomain)
response = client.do_action_with_exception(request)
response = str(response, encoding='utf-8')
# 将获取到的记录转换成json对象并返回
return json.loads(response)
# 新增记录 (默认都设置为A记录,通过配置set_Type可设置为其他记录)
def addDomainRecord(client,value,rr,domainname):
request = AddDomainRecordRequest.AddDomainRecordRequest()
request.set_accept_format('json')
# request.set_Priority('1') # MX 记录时的必选参数
request.set_TTL('600') # 可选值的范围取决于你的阿里云账户等级,免费版为 600 - 86400 单位为秒
request.set_Value(value) # 新增的 ip 地址
request.set_Type('A') # 记录类型
request.set_RR(rr) # 子域名名称
request.set_DomainName(domainname) #主域名
# 获取记录信息,返回信息中包含 TotalCount 字段,表示获取到的记录条数 0 表示没有记录, 其他数字为多少表示有多少条相同记录,正常有记录的值应该为1,如果值大于1则应该检查是不是重复添加了相同的记录
response = client.do_action_with_exception(request)
response = str(response, encoding='utf-8')
relsult = json.loads(response)
return relsult
# 更新记录
def updateDomainRecord(client,value,rr,record_id):
request = UpdateDomainRecordRequest.UpdateDomainRecordRequest()
request.set_accept_format('json')
# request.set_Priority('1')
request.set_TTL('600')
request.set_Value(value) # 新的ip地址
request.set_Type('A')
request.set_RR(rr)
request.set_RecordId(record_id) # 更新记录需要指定 record_id ,该字段为记录的唯一标识,可以在获取方法的返回信息中得到该字段的值
response = client.do_action_with_exception(request)
response = str(response, encoding='utf-8')
return response
# 删除记录
def delDomainRecord(client,subdomain):
info = getDomainInfo(subdomain)
if info['TotalCount'] == 0:
print('没有相关的记录信息,删除失败!')
elif info["TotalCount"] == 1:
print('准备删除记录')
request = DeleteDomainRecordRequest.DeleteDomainRecordRequest()
request.set_accept_format('json')
record_id = info["DomainRecords"]["Record"][0]["RecordId"]
request.set_RecordId(record_id) # 删除记录需要指定 record_id ,该字段为记录的唯一标识,可以在获取方法的返回信息中得到该字段的值
result = client.do_action_with_exception(request)
print('删除成功,返回信息:')
print(result)
else:
# 正常不应该有多条相同的记录,如果存在这种情况,应该手动去网站检查核实是否有操作失误
print("存在多个相同子域名解析记录值,请核查后再操作!")
# 有记录则更新,没有记录则新增
def setDomainRecord(client,value,rr,domainname):
info = getDomainInfo(rr + '.' + domainname)
if info['TotalCount'] == 0:
print('准备添加新记录')
add_result = addDomainRecord(client,value,rr,domainname)
print(add_result)
elif info["TotalCount"] == 1:
print('准备更新已有记录')
record_id = info["DomainRecords"]["Record"][0]["RecordId"]
cur_ip = getIp()
old_ip = info["DomainRecords"]["Record"][0]["Value"]
if cur_ip == old_ip:
print ("新ip与原ip相同,不更新!")
else:
update_result = updateDomainRecord(client,value,rr,record_id)
print('更新成功,返回信息:')
print(update_result)
else:
# 正常不应该有多条相同的记录,如果存在这种情况,应该手动去网站检查核实是否有操作失误
print("存在多个相同子域名解析记录值,请核查删除后再操作!")
IP = getIp()
# 循环子域名列表进行批量操作
for x in SubDomainList:
setDomainRecord(client,IP,x,DomainName)
# 删除记录测试
# delDomainRecord(client,'b.jsoner.com')
# 新增或更新记录测试
# setDomainRecord(client,'192.168.3.222','a',DomainName)
# 获取记录测试
# print (getDomainInfo(DomainName, 'y'))
# 批量获取记录测试
# for x in SubDomainList:
# print (getDomainInfo(DomainName, x))
# 获取外网ip地址测试
# print ('(' + getIp() + ')')
Python脚本的功能如下:
- 获取外网ip地址。
- 获取域名解析记录。
- 新增域名解析记录。
- 更新域名解析记录。
- 删除域名解析记录 (并不建议将该功能添加在实际脚本中)。
- 批量操作,如果记录不存在则添加记录,存在则更新记录。
另外,有几点需要特别说明:
- 建议不要将删除记录添加进实际使用的脚本当中。
- 相同记录是同一个子域名的多条记录,比如 test.binghe.com。
- 脚本并没有验证记录类型,所以同一子域名下的不同类型的记录也会认为是相同记录,比如:有两条记录分别是 test.binghe.com 的 A 记录 和 test.binghe.com 的 AAAA 记录,会被认为是两条相同的 test.binghe.com 记录.如果需要判定为不同的记录,小伙伴们可以根据上述Python脚本自行实现。
- 可以通过判断获取记录返回的 record_id 来实现精确匹配记录。
最后,可以将以上脚本保存为文件之后,通过定时任务,来实现定期自动更新ip地址。
来源:juejin.cn/post/7385106262009004095
我为什么选择Next.js+Supabase做全栈开发
作为一名前端工程师,选择合适的技术栈对项目的成功至关重要,我最近一个星期尝试了下这两个技术栈的组合,大概在一个星期就写了一个小 SAAS,总共 10 多个页面。在本文中,我将分享为什么我选择Next.js 14和Supabase作为全栈开发的首选组合,并通过最新的代码示例和比较数据,直观地展示这个选择带来的诸多优势。
Next.js 14: 现代React应用的革新框架
默认服务器组件的优势
Next.js 14默认使用服务器组件,这对于提升性能和开发体验至关重要。
例如,一个简单的服务器组件如下:
// app/page.js
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export default async function Home() {
const data = await getData()
return <div>Welcome to {data.name}div>
}
在这个例子中,Home
组件是一个异步的服务器组件,它可以直接进行数据获取,而无需使用useEffect或getServerSideProps。
App Router: 更强大的路由系统
Next.js 14采用了新的App Router,提供了更灵活和直观的路由方式:
app/
page.js // 对应路由 /
about/
page.js // 对应路由 /about
posts/
[id]/
page.js // 对应路由 /posts/1, /posts/2, 等
Server Actions: 无需API路由的表单处理
Next.js 14引入了Server Actions,允许我们直接在服务器上处理表单提交,无需单独的API路由:
// app/form.js
export default function Form() {
async function handleSubmit(formData) {
'use server'
// 在服务器上处理表单数据
const name = formData.get('name')
// ...处理逻辑
}
return (
<form action={handleSubmit}>
<input type="text" name="name" />
<button type="submit">Submitbutton>
form>
)
}
这个能力好用到哭,不用再写API路由了,直接在页面上处理表单提交。代码简单了不止一点点。
Supabase: 开源Firebase替代品的崛起
数据库即服务的便利性
Supabase提供了PostgreSQL数据库即服务,使用起来非常简单:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('YOUR_SUPABASE_URL', 'YOUR_SUPABASE_KEY')
// 插入数据
const { data, error } = await supabase
.from('users')
.insert({ name: 'John', email: 'john@example.com' })
实时功能的强大支持
Supabase的实时订阅功能让实现实时更新变得轻而易举:
import { useEffect } from 'react'
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('YOUR_SUPABASE_URL', 'YOUR_SUPABASE_KEY')
function RealtimeData() {
useEffect(() => {
const channel = supabase
.channel('*')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'users' }, payload => {
console.log('New user:', payload.new)
})
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [])
return <div>Listening for new users...div>
}
身份认证和授权的简化
Supabase内置的身份认证系统大大简化了用户管理:
const { data, error } = await supabase.auth.signUp({
email: 'example@email.com',
password: 'example-password',
})
Next.js 14 + Supabase: 完美的全栈开发组合
开发效率的显著提升
结合Next.js 14和Supabase,我们可以快速构建全功能的Web应用。以下是一个简单的例子,展示了如何在Next.js 14的服务器组件中使用Supabase:
// app/posts/page.js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('YOUR_SUPABASE_URL', 'YOUR_SUPABASE_KEY')
export default async function Posts() {
const { data: posts } = await supabase.from('posts').select('*')
return (
<div>
{posts.map(post => (
<div key={post.id}>{post.title}div>
))}
div>
)
}
这个例子展示了Next.js 14服务器组件如何与Supabase无缝集成,直接在服务器端获取数据并渲染。
与其他技术栈的对比
为了更直观地展示Next.js 14+Supabase的优势,我们来看一个更新后的比较表格:
特性 | Next.js 14+Supabase | MERN Stack | Firebase | Django |
---|---|---|---|---|
默认服务器组件 | ✅ | ❌ | ❌ | N/A |
App Router | ✅ | ❌ | ❌ | ❌ |
Server Actions | ✅ | ❌ | ❌ | ✅ |
实时数据库 | ✅ | 需配置 | ✅ | 需配置 |
SQL支持 | ✅ (PostgreSQL) | ❌ (默认NoSQL) | ❌ (NoSQL) | ✅ |
身份认证 | ✅ | 需配置 | ✅ | ✅ |
学习曲线 | 中 | 中 | 低 | 高 |
全栈JavaScript | ✅ | ✅ | ✅ | ❌ |
开源 | ✅ | ✅ | ❌ | ✅ |
选型优势的直观感受
- 开发速度:使用Next.js 14+Supabase,你可以在几小时内搭建起一个包含用户认证、数据库操作和实时更新的全栈应用。
- 代码量减少:得益于Next.js 14的服务器组件和Supabase的简洁API,代码量可以减少40%-60%。
- 性能提升:通过Next.js 14的默认服务器组件和自动代码分割,页面加载速度可以提升40%-70%。
- 学习成本:虽然新概念(如服务器组件)需要一定学习时间,但整体学习曲线比传统全栈开发更平缓,2-3周即可上手。
- 维护简化:单一语言(TypeScript)贯穿全栈,加上Next.js的文件约定和Supabase的声明式API,大大减少了维护的复杂度。
- 可扩展性:Supabase基于PostgreSQL,为未来的扩展提供了更多可能性,而Next.js的渐进式框架特性也允许逐步采用高级功能。
一些想法
Next.js 14和Supabase是现代全栈开发的最佳选择,它们的结合提供了前所未有的开发体验和性能优势。如果你正在寻找一个全栈开发的新方向,不妨试试Next.js 14和Supabase,相信你会爱上这个组合。而且 supabase 学了也很划算,即便你想做 react native,Flutter,他都可以作为你坚实的后端。
来源:juejin.cn/post/7389925676520226825
大厂都在”偷偷“用语义化标签,你却还在div?
引言
在我们日常浏览网页的时候,通常会看到各种各样的内容,如文字、图片、视频等。这些内容背后都有一个共同的语言,那就是HTML(超文本标记语言)。HTML是构建网页的基础,它就像建筑物的框架,决定了网页的基本结构和布局。
然而,仅仅有结构是不够的。如果网页只是简单地用一些基础标签堆砌而成,那么浏览器、搜索引擎甚至我们自己在后期维护时,都会感到非常吃力。
这时候,HTML的语义化标签就显得尤为重要。语义化标签不仅能使网页结构更加清晰,还能帮助搜索引擎更好地理解和索引网页内容。
什么是HTML语义化标签?
HTML语义化标签,就是那些带有特定含义的标签,它们告诉浏览器和搜索引擎,每一部分内容是什么。这就好比是给每个内容部分都贴上了一个清晰的标签,让所有人都能明白这个部分是用来做什么的。
举个例子,假设你在看一本书,书的封面、目录、章节标题等都是明确标示出来的,这样你就能快速找到自己想看的部分。同样,HTML语义化标签也是为了让网页的内容更加明晰易懂。比如:
<header>
标签用来定义网页的头部内容,通常包含导航栏、Logo等信息;<nav>
标签专门用于定义导航链接,这样搜索引擎就能更好地理解网站的结构;<article>
标签用于定义独立的内容,比如一篇新闻文章或者博客帖子。- ……
通过使用这些语义化标签,不仅提高了网页的可读性和维护性,还能帮助搜索引擎更准确地抓取和排名内容,从而提升网站的SEO效果。
为什么使用语义化标签?
使用HTML语义化标签有很多好处,它们不仅能让代码更清晰,还能带来实际的效果和便利。
- 提高网页的可读性和结构化
- 语义化标签让HTML代码更加直观,其他开发者在阅读和维护代码时,可以快速理解每个部分的作用。这有助于团队合作和项目的长期维护。
- 有助于搜索引擎优化(SEO)
- 搜索引擎通过爬虫程序抓取网页内容,并根据网页的结构和内容进行索引。使用语义化标签可以帮助搜索引擎更好地理解网页的层次和重点内容,从而提升网站在搜索结果中的排名。
- 无障碍支持
- 语义化标签对辅助技术(如屏幕阅读器)非常重要,它们可以帮助视障用户更好地理解网页内容。例如,使用
<nav>
标签可以让屏幕阅读器快速跳转到导航部分。 - 还记得浏览器内置的“沉浸阅读器”吗?它们也大多基于语义化标签提供服务。例如掘金的文章都是用
<article>
标签包裹的,所以你可以在掘金文章页面启用沉浸阅读器,而且精准的获取了文章的主体内容。
- 语义化标签对辅助技术(如屏幕阅读器)非常重要,它们可以帮助视障用户更好地理解网页内容。例如,使用
另外不得不说,目前苹果对语义化标签的使用是最炉火纯青的。怪不得都说苹果优雅,现在算是在前端上见识到了这个细节怪……
其实还有很多大厂都在使用,但都是偷偷地使用。它们没有全局使用语义化标签,而是在特定的关键位置使用语义化标签来 “谄媚” 一下搜索引擎或浏览器提供的无障碍功能。
所以我相信很多人还是非常支持div一把梭的,只要老板不限制,想怎么做就怎么做。不过如果你也能学习大厂,在漫天div下加一点语义化标签的小巧思,骗过搜索引擎和浏览器,这不是很香吗?
所以,本文着重介绍那些搜索引擎和浏览器有特别支持的语义化标签,搞定他们就搞定了一大半!
常用的语义化标签
搜索引擎钟爱的语义化标签
搜索引擎(如Google、Bing等)特别关注某些HTML语义化标签,因为这些标签能够帮助它们更好地理解网页的结构和内容,从而改进搜索结果的质量。
以下是一些被搜索引擎特别关注的语义化标签:
<header>
- 搜索引擎会识别
<header>
标签中的内容,通常包括页面的标题、导航链接等,有助于理解网页的整体结构和主要部分。
- 搜索引擎会识别
<nav>
<nav>
标签标示出导航链接区域,帮助搜索引擎理解网站的链接结构和页面之间的关系,有助于内部链接的优化。
<article>
<article>
标签表示独立的内容块,如新闻文章、博客帖子等。搜索引擎会特别关注这些标签,认为其包含主要的内容。
<footer>
<footer>
标签包含页脚内容,通常包括版权信息、联系信息等,搜索引擎会利用这些信息来补充网页的相关性数据。
<main>
<main>
标签标示出页面的主要内容区域,帮助搜索引擎更快地定位和抓取主要内容,而忽略导航栏、页脚等次要部分。
浏览器的无障碍功能
现代浏览器具备许多无障碍功能(accessibility features),这些功能可以帮助有特殊需求的用户更好地浏览网页。
以下是一些关键的无障碍功能:
- 屏幕阅读器支持
- 屏幕阅读器是一种软件工具,可以将网页内容转换为语音或盲文,帮助视障用户浏览网页。语义化标签可以极大地提升屏幕阅读器的效率和准确性。例如,
<nav>
标签可以让用户快速跳转到导航部分,而<article>
标签则可以帮助用户找到主要的文章内容。
- 屏幕阅读器是一种软件工具,可以将网页内容转换为语音或盲文,帮助视障用户浏览网页。语义化标签可以极大地提升屏幕阅读器的效率和准确性。例如,
- 键盘导航
- 无障碍浏览器允许用户通过键盘进行导航,语义化标签如
<header>
、<nav>
、<main>
、<footer>
等,可以帮助键盘用户快速跳转到页面的不同部分,提高浏览效率。
<header>
<h1>网站标题</h1>
</header>
<nav>
<!-- 导航内容 -->
</nav>
<main>
<h2>主要内容标题</h2>
<p>这是主要内容区域。</p>
</main>
<footer>
<p>版权所有 © 2024 公司名称</p>
</footer>
- 无障碍浏览器允许用户通过键盘进行导航,语义化标签如
- 高对比度模式
- 一些浏览器提供高对比度模式,帮助视觉有障碍的用户更容易阅读内容。使用正确的语义化标签和良好的结构,可以确保在高对比度模式下内容的可读性和可访问性。
<section>
<h2>章节标题</h2>
<article>
<h3>文章标题</h3>
<p>文章内容...</p>
</article>
<aside>
<h3>附加内容</h3>
<p>例如广告或链接...</p>
</aside>
</section>
- ARIA(可访问性富互联网应用)标签
- 虽然ARIA标签不是HTML语义化标签的一部分,但它们可以补充HTML标签,提供更多的无障碍信息。例如,
aria-label
、aria-labelledby
等属性可以为非文本元素提供文本描述,帮助辅助技术更好地解释内容。
<button aria-label="关闭">X</button>
<div role="dialog" aria-labelledby="dialogTitle" aria-describedby="dialogDescription">
<h2 id="dialogTitle">对话框标题</h2>
<p id="dialogDescription">对话框内容描述。</p>
</div>
- 虽然ARIA标签不是HTML语义化标签的一部分,但它们可以补充HTML标签,提供更多的无障碍信息。例如,
语义化标签的实际应用
为了更好地理解语义化标签的使用方法,让我们通过一个具体的案例来展示它们的实际应用。
假设我们要创建一个简单的博客页面,包含标题、导航栏、文章内容、侧边栏和页脚。下面是一个示例代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>我的博客</title>
<style>
body { font-family: Arial, sans-serif; }
header, nav, article, aside, footer { margin: 20px; padding: 10px; border: 1px solid #ccc; }
nav ul { list-style-type: none; padding: 0; }
nav ul li { display: inline; margin-right: 10px; }
aside { float: right; width: 30%; }
article { float: left; width: 65%; }
</style>
</head>
<body>
<header>
<h1>我的博客</h1>
<nav>
<ul>
<li><a href="#home">首页</a></li>
<li><a href="#about">关于我</a></li>
<li><a href="#contact">联系我</a></li>
</ul>
</nav>
</header>
<section>
<article>
<h2>文章标题</h2>
<p>这里是文章的正文内容。</p>
</article>
<aside>
<h2>侧边栏</h2>
<p>这里是一些附加内容,比如广告或链接。</p>
</aside>
</section>
<footer>
<p>版权所有 Dikkoo; 2024 我的博客</p>
</footer>
</body>
</html>
回顾一下
在这个案例中,我们使用了多个语义化标签来组织页面内容:
<header>
包含网站的标题和导航栏。<nav>
用于定义导航链接区域。<section>
用于分隔主要内容区域,包含文章和侧边栏。<article>
定义了独立的文章内容。<aside>
包含附加内容,如侧边栏。<footer>
包含页面的底部信息。
怎样合理运用语义化标签?
为了充分发挥HTML语义化标签的优势,以下是一些最佳实践建议:
- 规划页面结构,提前设计
- 在编写HTML之前,先绘制页面的结构图,明确各部分的功能和内容。根据设计选择合适的语义化标签,这样可以避免在编写过程中频繁修改结构。
- 保持代码简洁
- 语义化标签旨在使代码更清晰,因此应尽量避免过度嵌套标签和使用多余的标签。使用语义化标签替代大量的
<div>
和<span>
,使代码更加简洁和易读。
- 语义化标签旨在使代码更清晰,因此应尽量避免过度嵌套标签和使用多余的标签。使用语义化标签替代大量的
- 合理嵌套标签
- 语义化标签应按照其语义进行嵌套。例如,将
<nav>
放在<header>
内,表示导航是头部的一部分;将<section>
和<article>
合理地嵌套在一起,表示内容的层次结构。
- 语义化标签应按照其语义进行嵌套。例如,将
- 遵循HTML规范
- 确保使用语义化标签时符合HTML规范,不要滥用标签。例如,不要将
<header>
标签用在每个段落中,而应仅用于页面或章节的头部。
- 确保使用语义化标签时符合HTML规范,不要滥用标签。例如,不要将
来源:juejin.cn/post/7388056946121113637
虽然炒股赚了十多万,但差点倾家荡产!劝你别入坑
今天,五阳哥不打算聊技术,而是聊一下炒股的话题。我自认为在这方面有发言权,自述一个程序员的炒股经历。
2019年,我开始涉足股市,在2021年中旬为了购房,将持有的股票全部卖出,赚了十多万元。在最高峰时期,我获利超过了二十多万元,但后来又回吐了一部分利润。虽然我的炒股成绩不是最出色的,但也超过了很多人。因为大多数股民都是亏损的,能够在股市长期盈利的人真的是凤毛麟角。
股市中普遍流传的七亏二平一赚的说法并不只是传闻,事实上,现实中的比例更加残酷,能够长期赚钱的人可能连10%都达不到。
接下来,我想谈谈我的炒股经历和心路历程,与大家分享一下我的内心体验,为那些有意向或正在炒股的朋友提供一些参考。希望劝退大家,能救一个是一个!
本文倒叙描述,先聊聊最后的疯狂和偏执!
不甘失败,疯狂上杠杆
股市有上涨就有下跌,在我卖出以后,股市继续疯涨了很多。当时长春高新,我是四百一股买入,六百一股就卖出了,只赚了2万。可是在我卖出去的两个月以后,它最高涨到了一千。相当于我本可以赚六万,结果赚了两万就跑了。
我简直想把大腿拍烂了,这严重的影响了我的认知。我开始坚信,这只股票和公司就是好的,非常牛,是我始乱终弃,我不应该早早抛弃人家。 除了悔恨,我还在期盼它下跌,好让我再次抄底,重新买入,让我有重新上车的机会!
终于这只股票后来跌了10%,我觉得跌的差不多了,于是我开始抄底买入!抄底买入的价格在900一股(复权前)。
没想到,这次抄底是我噩梦的开始。我想抄他的底,他想抄我的家!
这张图,完美的诠释了我的抄底过程。地板底下还有底,深不见底,一直到我不再敢抄底为止。一直抄到,我天天睡不着觉!
当时我九百多一股开始抄底买入,在此之前我都是100股,后来我开始投入更多的资金在这只股票上。当时的我 定下了规矩,鸡蛋不能放在一个篮子里;不能重仓一只股票,要分散投资;这些道理我都明白,但是真到了节骨眼上,我不想输,我想一把赢回来,我要抄底,摊平我的成本。
正所谓:高位加仓,一把亏光。之前我赚的两万块钱,早就因为高位加仓,亏回去了。可是我不甘心输,我想赢回来。当时意识不到也不愿意承认:这就是赌徒心理。
后来这只股票,从1000,跌倒了600,回调了40%。而我已经被深深的套牢。当时我盈利时,只买了1股。等我被套牢时,持有了9股。 按照1000一股,就是九十万。按照600一股,就是54万。
我刚毕业,哪来的那么多钱!
我的钱,早就在800一股的时候,我就全投进去了,我认为800已经算是底了吧,没想到股价很快就击穿了800。
于是我开始跟好朋友借钱。一共借了10万,商量好借一年,还他利息。后来这10万块钱,也禁不住抄底,很快手里没钱了,股价还在暴跌。我已经忘记当时亏多少钱了,我当时已经不敢看账户了,也不敢细算亏了多少钱!
于是,我又开始从支付宝和招商银行借贷,借钱的利率是相当高的,年利息在6%以上。当时一共借了30万。但是股价还不见底,我开始焦虑的睡不着觉。
不光不见底,还在一直跌,我记得当时有一天,在跌了很多以后,股价跌停 -10%。当时的我已经全部资金都投进去了,一天亏了5万,我的小心脏真的要受不了了。跌的我要吐血! 同事说,那天看见我的脸色很差,握着鼠标手还在发抖!
跌成这样,我没有勇气打开账户…… 我不知道什么时候是个头,除了恐惧只有恐惧,每天活在恐惧之中。
我盘算了一下,当时最低点的我,亏了得有二十多万。从盈利六万,一下子到亏二十多万。只需要一个多月的时间。
我哪里经历过这些,投资以来,我都是顺风顺水的,基本没有亏过钱,从来都是挣钱,怎么会成这个样子。
当时的我,没空反思,我只希望,我要赚回来!我一定会赚回来,当时能借的支付宝和招行都已经借到最大额度了…… 我也没有什么办法了,只能躺平。
所以股价最低点的时候,基本都没有钱加仓。
侥幸反弹,但不忍心止盈
股价跌了四个月,这是我人生极其灰暗的四个月。后来因为种种原因,股价涨回来了,当时被传闻的事情不攻自破,公司用实际的业绩证明了自己。
股价开始慢慢回暖,后来开始凶猛的反弹,当时的��一直认为:股价暴跌时我吃的所有苦,所有委屈,我都要股市给我补回来!
后来这段时间,股价最高又回到了1000元一股(复权前)。最高点,我赚了二十多万,但是我不忍心止盈卖出。
我觉得还会继续涨,我还在畅想:公司达到,万亿市值。
我觉得自己当时真的 失了智了。
结婚买房,卖在最高点
这段时间,不光股市顺丰顺水,感情上也比较顺利,有了女朋友,现在是老婆了。从那时起,我开始反思自己的行为,我开始意识到,自己彻彻底底是一个赌徒。
因为已经回本了,也赚了一点钱,我开始不断的纠结要不要卖出,不再炒股了。
后来因为两件事,第一件是我姐姐因为家里要做小买卖,向我借钱。 当时的我,很纠结,我的钱都在股市里啊,借她钱就得卖股票啊,我有点心疼。奈何是亲姐,就借了。
后来我盘算着,不对劲。我还有贷款没还呢,一共三十万。我寻思,我从银行借钱收6%的利息,我借给别人钱,我一分利息收不到。 我TM 妥妥的冤大头啊。
不行,我要把贷款全部还上,我Tm亏大了,于是我逐渐卖股票。一卖出便不可收拾。
我开始担心,万一股价再跌回去,怎么办啊。我和女朋友结婚时,还要买房,到时候需要一大笔钱,万一要是被套住了,可怎么办啊!
在这这样的焦虑之下,我把股票全部都卖光了!
冥冥之中,自有天意。等我卖出之后的第二周,长春高新开启了下一轮暴跌,而这一轮暴跌之后,直至今日,再也没有翻身的机会。从股价1000元一股,直至今天 300元一股(复权前是300,当前是150元)。暴跌程度大达 75%以上!
全是侥幸
我觉得我是幸运的,如果我迟了那么一步!假如反应迟一周,我觉得就万劫不复。因为再次开启暴跌后,我又会开始赌徒心理。
我会想,我要把失去的,重新赢回来!我不能现在卖,我要赢回来。再加上之前抄底成功一次,我更加深信不疑!
于是我可能会从1000元,一路抄底到300元。如果真会如此,我只能倾家荡产!
不是每个人都有我这么幸运,在最高点,跑了出去。 雪球上之前有一个非常活泼的用户, 寒月霖枫,就是因为投资长春高新,从盈利150万,到亏光100万本金,还倒欠银行!
然而这一切,他的家人完全不知道,他又该如何面对家人,如何面对未来的人生。他想自杀,想过很多方式了结。感兴趣的朋友可以去 雪球搜搜这个 用户,寒月霖枫。
我觉得 他就是世界上 另一个自己。我和他完全类似的经历,除了我比他幸运一点。我因为结婚买房和被借钱,及时逃顶成功,否则我和他一样,一定会输得倾家荡产!
我觉得,自己就是一个赌狗!
然而,在成为赌狗之前,我是非常认真谨慎对待投资理财的!
极其谨慎的理财开局
一开始,我从微信理财通了解到基金,当时2019年,我刚毕业两年,手里有几万块钱,一直存在活期账户里。其中一个周末,我花时间研究了一下理财通,发现有一些债券基金非常不错。于是分几批买了几个债券基金,当时的我对于理财既谨慎又盲目。
谨慎的一面是:我只敢买债券基金,就是年利息在 5%上下的。像股票基金这种我是不敢买的。
盲目的一面是:我不知道债券基金也是风险很大的,一味的找利息最多的债券基金。
后来的我好像魔怔了,知道了理财这件事,隔三差五就看看收益,找找有没有利息更高的债券基金。直到有一天,我发现了一个指数基金,收益非常稳定。
是美股的指数基金,于是我买了1万块钱,庆幸的是,这只指数基金,三个月就赚了八百多,当时的我很高兴。那一刻,我第一次体会到:不劳而获真的让人非常快乐!
如饥似渴的学习投资技巧
经过一段时间的理财,我对于理财越来越熟悉。
胆子也越来越大,美股的指数基金赚了一点钱,我害怕亏回去,就立即卖了。卖了以后就一直在找其他指数基金,这时候我也在看国内 A股的指数基金,甚至行业主题的基金。
尝到了投资的甜头以后,我开始花更多的时间用来 找基。我开始从方方面面评估一只基金。
有一段时间,我特别自豪,我在一个周末,通过 天天基金网,找到了一个基金,这只基金和社保投资基金的持仓 吻合度非常高。当时的我思想非常朴素, 社保基金可是国家队,国家管理的基金一定非常强,非常专业,眼光自然差不了。这只基金和国家队吻合度如此高,自然也差不了。
于是和朋友们,推荐了这只基金。我们都买了这只基金,而后的一个月,这只基金涨势非常喜人,赚了很多钱,朋友们在群里也都感谢我,说我很厉害,投资眼光真高!
那一刻,我飘飘然……
我开始投入更多的时间用来理财。下班后,用来学习的时间也不学习了,开始慢慢的过度到学习投资理财。我开始不停地 找基。当时研究非常深入,我会把这只基金过往的持仓记录,包括公司都研究到。花费的时间也很多。
我也开始看各种财经分析师对于股市的分析,他们会分析大盘何时突破三千点,什么时候股市情绪会高昂起来,什么行业主题会热门,什么时候该卖出跑路了。
总之,投资理财,可以学习的东西多种多样!似乎比编程有趣多了。
换句话说:我上头了
非常荒谬的炒股开局
当时我还是非常谨慎地,一直在投资基金,包括 比较火爆的 中欧医疗创新C 基金,我当时也买了。当时葛兰的名气还很响亮呢。后来股市下行,医疗股票都在暴跌,葛兰的基金 就不行了,有句话调侃:家里有钱用不完,中欧医疗找葛兰。腰缠万贯没人分,易方达那有张坤。
由此可见,股市里难有常胜将军!
当时的我,进入股市,非常荒谬。有一天,前同事偷偷告诉我,他知道用友的内幕,让我下午开盘赶紧买,我忙追问,什么内幕,他说利润得翻五倍。 我寻思一下,看了一眼用友股票还在低位趴着,心动了。于是我中午就忙不迭的线上开户,然后下午急匆匆的买了 用友。 事后证明,利润不光没有翻五倍,还下降了。当然在这之前,我早就跑了,没赚着钱,也没咋亏钱。
当时的我,深信不疑这个假的小道消息,恨不得立即买上很多股票。害怕来不及上车……
自从开了户,便一发不可收拾,此时差2个月,快到2019年底!席卷全世界的病毒即将来袭
这段时间,股市涨势非常好,半导体基金涨得非常凶猛! 我因为初次进入股市,没有历史包袱,哪个股票是热点,我追哪个,胆子非常大。而且股市行情非常好,我更加相信,自己的炒股实力不凡!
换句话说:越来越上头,胆子越来越大。 学习编程,学个屁啊,炒股能赚钱,还编个屁程序。
刚入股市,就赶上牛市,顺风顺水
2019年底到2020年上半年,A股有几年不遇的大牛市,尤其是半导体、白酒、医疗行业行情非常火爆。我因为初入股市,没有历史包袱,没有锚点。当前哪个行业火爆,我就买那个,没事就跑 雪球 刷股票论坛的时间,比上班的时间还要长。
上班摸鱼和炒股 是家常便饭。工作上虽然不算心不在焉,但是漫不经心!
在这之前,我投入的金额不多。最多时候,也就投入了10万块钱。当时基金收益达到了三万块。我开始飘飘然。
开始炒股,也尝到了甜头,一开始,我把基金里的钱,逐渐的转移到股市里。当时的我给自己定纪律。七成资金投在基金里,三成资金投在股市里。做风险平衡,不能完全投入到风险高的股市里。
我自认为,我能禁得住 炒股这个毒品。
但是逐渐的,股票的收益越来越高,这个比例很快就倒转过来,我开始把更多资金投在股市中,其中有一只股票,我非常喜欢。这只股票后来成为了很多人的噩梦,成为很多股民 人生毁灭的导火索!
长春高新 股票代码:000661。我在这只股票上赚的很多,后来我觉得股市涨了那么多,该跌了吧,于是我就全部卖出,清仓止盈。 当时的我利润有六万,我觉得非常多了,我非常高兴。
其中 长春高新 一只股票的利润在 两万多元。当时这是我最喜欢的一只股票。我做梦也想不到,后来这只股票差点让我倾家荡产……
当时每天最开心的事情就是,打开基金和证券App,查看每天的收益。有的时候一天能赚 两千多,比工资还要高。群里也非常热闹,每个人都非常兴奋,热烈的讨论哪个股票涨得好。商业互吹成风……
换句话说:岂止是炒股上头,我已经中毒了!
之后就发生了,上文说的一切,我在抄底的过程中,越套越牢……
总结
以上都是我的个人真实经历。 我没有谈 A 股是否值得投资,也不评论当前的股市行情。我只是想分享自己的个人炒股经历。
炒股就是赌博
我想告诉大家,无论你在股市赚了多少钱,迟早都会还回去,越炒股越上头,赚的越多越上头。
赌徒不是一天造成的,谁都有赢的时候,无论赚多少,最终都会因为人性的贪婪 走上赌徒的道路。迟早倾家荡产。即使你没有遇到长春高新,也会有其他暴跌的股票等着你!
什么🐶皮的价值投资! 谈价值投资,撒泡尿照照自己,你一个散户,你配吗?
漫漫人生路,总会错几步。股市里错几步,就会让你万劫不复!
”把钱还我,我不玩了“
”我只要把钱赢回来,我就不玩了“
这都是常见的赌徒心理,奉劝看到此文的 程序员朋友,千万不要炒股和买基金。
尤其是喜欢打牌、打德州扑克,喜欢买彩-票的 赌性很强的朋友,一定要远离炒股,远离投资!
能救一个是一个!
来源:juejin.cn/post/7303348013934034983
扒一扒uniapp是如何做ios app应用安装的
为何要扒
因为最近有移动端业务的需求,用uniapp做了ios、Android双端的app应用,由于没有资质上架AppStore和test flight,所以只能使用苹果的超签(需要ios用户提供uuid才能加入测试使用,并且只支持100人安装使用)。打包出来生成的是一个ipa包,并不能直接安装,要通过爱思助手这类的应用装一下ipa包。但交付到客户手上就有问题了,还需要电脑连接助手才能安装,那岂不是每次安装新版什么的,都要打开电脑搞一下。因此,才有了这次的扒一扒,目标就是为了解决只提供一个下载链接用户即可下载,不用再通过助手类应用安装ipa包。
开干
官方模板
先打开uniapp云打包一下项目看看
复制地址到移动端浏览器打开看看
这就对味了,都知道ios是不能直接打开ipa文件进行安装的,接下来就研究下这个页面的执行逻辑。
开扒
F12打开choromdevtools,ctrl+s保存网页html。
保存成功,接下来看看html代码(样式代码删除了)
<!DOCTYPE html>
<!-- saved from url=(0077)https://ide.dcloud.net.cn/build/download/2425a4b0-4229-11ee-bd1b-67afccf2f6a7 -->
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
</head>
<body>
<br><br>
<center>
<a class="button" href="itms-services://?action=download-manifest&url=https://ide.dcloud.net.cn/build/ipa-xxxxxxxxxxx.plist">点击安装</a>
</center>
<br><br>
<center>注意:只提供包名为io.dcloud.appid的包的直接下载安装,如果包名不一致请自行搭建下载服务器</center>
</body>
</html>
解析
从上面代码可以看出,关键代码就一行也就是a标签的href地址("itms-services://?action=download-manifest&url=ide.dcloud.net.cn/build/ipa-x…")
先看看itms-services是什么意思,下面是代码开发助手给的解释
大概意思就是itms-services是苹果提供给开发者一个的更新或安装应用的协议,用来做应用分发的,需要指向一个可下载的plist文件地址。
什么又是plist呢,这里再请我们的代码开发助手解释一下
对于没接触过ios相关开发的,连plist文件怎么写都不知道,既然如此,那接下来就来扒一下dcloud的pilst文件,看看官方是怎么写的吧。
打开浏览器,copy一下刚刚扒下来的html文件下a标签指向的地址,复制url后面plist文件的下载地址粘贴到浏览器保存到桌面。
访问后会出现
别担心,这时候直接按ctrl+s可以直接保存一个plist.xml文件,也可以打开devtools查看网络请求,找到ipa开头的请求
直接新建一个plist文件,cv一下就好,我这里就选择保存它的plist.xml文件,接下来康康文件里到底是什么
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>https://bdpkg-aliyun.dcloud.net.cn/20230824/xxxxx/Pandora.ipa?xxxxxxxx</string>
</dict>
<dict>
<key>kind</key>
<string>display-image</string>
<key>needs-shine</key>
<false/>
<key>url</key>
<string>https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/uni.png</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>kind</key>
<string>software</string>
<key>bundle-identifier</key>
<string>xxxxx</string>
<key>title</key>
<string>HBuilder手机应用</string>
</dict>
</dict>
</array>
</dict>
</plist>
直接抓重点,这里存你存放ipa包的地址
这里改你应用的昵称
这里改图标
因篇幅限制,想了解plist的自行问代码助手或者搜索引擎。
为我所用
分析完了,如何为我所用呢,首先按照分析上扒下来的plist文件修改下自身应用的信息,并且需要服务器存放ipa文件,这里我选择了unicloud,开发者可以申请一个免费的空间(想了解更多的自己去dcloud官网看看,说多了有打广告嫌疑),替换好大概如下:
将plist文件放到服务器上后,拿到plist的下载地址,打开扒下来的html,将a标签上的url切换成plist文件的下载地址,如图:
可以把页面上没用的信息都删掉,保存,再把html放到服务器上,用户访问这个地址,就可以直接下载描述文件安装ipa包应用了(记得需要添加用户的uuid到开发者账号上),其实至此需求已经算是落幕了,但转念想想还是有点麻烦,于是又优化了一下,将a标签中的href信息,直接加载到二维码上供用户扫描便可直接下载,相对来说更方便一点,于是我直接打开草料,生成了一个二维码,至此,本次扒拉过程结束,需求落幕!
来源:juejin.cn/post/7270799565963149324
听一听比尔盖茨老人家怎么看待 AI 革命
最近发现比尔盖茨还在写文章,确实了不起,68 岁的老人家还在坚持输出,除了写文章,比尔盖茨还致力于教育、医疗和卫生等慈善工作,奋斗在一线,看来美国人也延迟退休啊 😅
原文《AI 将彻底改变计算机的使用方式》 大约一万多字,从宏观的角度讲述了 AI 的形态,并且提及了 AI 将影响的四个行业,最后再讲到目前面临的技术问题和非技术问题,整体文章深入浅出,非常值得一读。
本文做了一些删减,并尝试用自己的话去解读,更加符合中文读者的语义,其中的一些观点仅供参考,大家自由评判之,毕竟比尔先生还预测过计算机只需要 768kb 内存足以,而现在 8 个 G 都不够下饭的。
未来是智能体
首先比尔先生回忆了和保罗·艾伦一起创立微软公司的感觉,然后语重心长讲到,虽然经过了几十年的更迭,但是计算机还是比较蠢笨的,你要完成一个任务,得先选择某一个 app,比如用微软的 Word,去画一个商业的草图,但是这个 app 不能帮你发送邮件、分享自拍、分析数据、计划一个聚会,或者买电影票,要完成上述这些事情,要么找一个亲密的朋友,要么有一个个人助理。
胆大的预测,在下一个五年,AI 将彻底重构,你不需要再用为了处理不同任务,而去使用不同的 app。你可以用任何语言,直接告诉你的设备,你想要做什么。AI 借由丰富的理解能力,为你做个性化的服务和响应。在不远的将来,每个人都能够拥有远超当前科技的个性化的人工智能助手。
这种软件形态,比尔先生思考了 30 多年,但是最近两年来才成为现实,这种形态叫做“智能体”(Agent),能够用自然语言回复,并且基于用户的背景知识去完成不同的任务。
智能体不只是改变每个人和计算机的交互,他们同样颠覆了整个软件工业,这是从命令行输入、图形化交互以来,计算机交互最大的变革。
这里就可以总结了,比尔先生认同「All in One」的观点,一个智能体,处理你的一切事务,并且用发展的眼光看待,在之后技术继续蓬勃发展,智能体将登基,成为新的软件形态之主。
区分机器人和智能体
现在很多公司,做出来的产品根本算不上智能体,只能叫做机器人(Bot),机器人内嵌在某一个 app,借由 AI 的能力处理一些特定的任务,比如文本润色、扩写等,他们不会记住你用过多少次,也不会记住你的喜好,只能冰冷冷的机器人。
智能体不一样,更聪明,更主动,在你询问它们意见之前,就能够给到你合适的建议。它们能够跨 app 处理任务,记录你的行为,识别你的动机和意图,随着时间的推移,它们会慢慢变得更好,更加准确的给你提供信息和建议。
举个例子 🌰,你想要来一场特种兵旅行,机器人只能根据你的预算,给你定位酒店。而智能体,了解你近年来的旅行资料,能够推断出你是想要找一个距离景点近一点的还是远一点的,来为你推荐合适的酒店,还能根据你的兴趣和倾向,为你规划行程、预定餐厅。
AI 智能体,还有最让人激动的能力,那就把现在一些昂贵的服务价格给打下来,在这四个领域中,医疗健康、教育、生产力以及娱乐购物,智能体将大展拳脚。
医疗健康
目前,AI 的作用局限于处理一些非医疗任务,比如,就诊时候录音,然后生成报告给医生检查回顾。接下来会真正的转变,智能体能够帮助病人去做一个基础的伤病分类,获取关于处理健康问题的建议,决定是否需要去做进一步治疗。这些智能体还能够帮助医疗人员做决定,使之更加的高效。
现在已经有类似的 app 了,比如 Glass Health,能够分析病人摘要、提供建议诊断给医生做参考。
有了这些帮助病人和医疗人员的智能体,才是真正利于处在贫困的国家地区,那些贫瘠地方的老百姓们,甚至从来都没见过医生。比尔先生大义 🫡
这类「临床医学智能体」普及的速度可能会比较慢,毕竟这事关生死。人们需要看到这些健康智能体真正起到作用的证据,才能够接受它们。虽然健康智能体可能不完美,会犯错,但是,人类也同样不完美,会犯错。
教育
十多年来,比尔先生一直对借助软件帮助学生学习、让老师工作更轻松这样的事情很上心,软件不会替代教师工作,它只能对他们的工作进行补充,比如对学生做到个性化教育,从批改作业的压力中解放,还有其他种种。
现在已经有初步的进展,那就是一些基于文本的教育智能体,他们能够解释二元方程、提供练习数学题等等,但这还仅仅只是初步能力,接下来智能体还会解锁更多的能力。
确实,AI 学习辅导这个需求确实不错,之前热搜有这么一个家长给孩子辅导的问题
通过 AI 可以给到很好的学习启发:
生产力
生产力这方面已经卷得飞起了,微软以及为 Word、Outlook 等其他 app 集成了 Copilot(副驾驶),谷歌也在做类似的事情,也把 Bard 集合在自家的生产力 app 中。这些 Copilot 可以做很多事情,比如把文本转换为 PPT,回答表格问题,总结电子邮件内容等等。
当然,智能体能做到还有更多,如果你有一个商业想法,智能体会做一份商业计划书,然后基于此创建一份展示汇报,还能根据你的内容插入生成合适的图片。
娱乐购物
好吧,AI 可能帮你挑选电视频道、推荐电影、书、电视剧等等。比如,最近比尔盖茨投资了一家创业公司 Pix,用问答的方式推荐电影。虾皮也有一个基于 AI 的「DJ」,它能够根据你的喜好播放歌曲,并且还能和你交流,甚至会喊你的名字。
对科技行业的冲击波
总而言之,智能体最终能够帮助到我们生活的方方面,这对整个软件行业和社会的影响将会是深远的。
在计算领域,我们常谈论的「平台」,比如安卓、iOS 还有 Windows,是目前 app 和服务赖以存在的基础,而智能体将会成为下一个平台。
创建一个 app 或者服务,你不需要知道如何如变成或者图形设计,你只需要告诉你的智能体你想要做什么,它就能够编码、设计界面、创建 logo,然后发布到 app 到在线商店上,OpenAI 的 GPTs 能让我们一窥未来,GPTs 可以让非开发人员创建并分享自己的的智能体。
推荐下 starflow.tech,可以直接体验 GPTs
没有哪一家公司可以垄断智能体生意,因为未来会有多款不同 AI 引擎可供使用。现在,智能体只能依赖于其他软件,比如 Word 和 Excel,但是最终,他们将会独立运行。现在他们可能是免费的,但以后,你会为这些聪明高效的智能体付费,那么商业逻辑将改变,公司不再需要迎合广告公司而恶心用户,而是真正地为用户量身打造智能体。
在这些聪明但又复杂的智能体落地成为现实之前,还有大量的技术问题需要解决。
技术挑战
至今还没有人搞清楚智能体的底层存储结构是怎么样的,要创建一个个性化的智能体,我们需要一种新型数据库,它能把记录你的兴趣和关系的微妙之处,在保障隐私的情况下还能够快速查询信息。目前向量数据库是一种,或许之后还会有其他更好的呢。
另一个开放的问题就是一个用户大概需要和多少智能体打交道呢?你的个性化智能体会被分为医疗智能体和数学教师智能体吗?如果是的话,你是希望这些智能体彼此能够协作,还是在各自领域保持独立?
智能体的形态会是怎么样的呢,是手机、眼睛、项链、徽章,甚至是全息投影?这里比尔先生推测,现阶段最适合的是耳机,它能够听取你的声音,然后通过耳机回复你,其他的好处是,它还能调节音量、屏蔽周围噪音。
这里面还有其他种种技术挑战存在
1️⃣ 智能体之间互相交流的标准协议?
2️⃣ 智能体的价格要怎么打到每个人都能够用得起?
3️⃣ 用户少量提示词和智能体的准确回复之间如何取得平衡?
4️⃣ 如何减少幻觉,特别是在医疗这种特别重要的场景下?
5️⃣ 如何确保智能体不会伤害 or 歧视人类?
6️⃣ 如何确保智能体不会越权进行犯罪?
在不远的将来,智能体会迫使人类去思考,我们这么做是为了什么?想象一下,一个足够优秀的智能体存在,我们基本不需要工作了,那么每个人还需要接受高水平的教育吗?在未来可能是这样的,人们怎么消磨他们的时间?在所有答案都是已知的情况下,每个人还想要去上学吗?每个人都有大量的空闲时间,你还能有一个安全和繁荣的社会吗?
不过到这个时间点还很早,但至少目前,智能体正在走来,在接下来的几年,他们将彻底改变我们的生活。
来源:juejin.cn/post/7312736427326504996
怎样实现每次页面打开时都清除本页缓存?
"```markdown
每次页面加载时清除本页缓存可以通过多种方式实现,具体方法取决于需要的粒度和数据类型。以下是一些常见的技术:
使用meta标签(HTML):
<meta http-equiv=\"cache-control\" content=\"no-cache, no-store, must-revalidate\">
<meta http-equiv=\"pragma\" content=\"no-cache\">
<meta http-equiv=\"expires\" content=\"0\">
使用JavaScript:
// 清除整个页面缓存
window.location.reload(true);
// 清除特定资源的缓存
const url = 'https://example.com/style.css';
fetch(url, {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
}).then(response => {
// 处理响应
});
// 清除localStorage
localStorage.clear();
// 清除sessionStorage
sessionStorage.clear();
使用HTTP头信息(服务端设置):
// Express.js 示例
app.use((req, res, next) => {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
next();
});
使用框架或库功能:
例如,React中可以通过key属性强制重新渲染组件来清除缓存:
function App() {
const [key, setKey] = useState(0);
const resetPage = () => {
setKey(prevKey => prevKey + 1);
};
return (
<div key={key}>
{/* 页面内容 */}
<button onClick={resetPage}>重置页面</button>
</div>
);
}
清除浏览器缓存:
用户可以手动清除浏览器缓存来达到相同的效果。这通常通过浏览器设置或开发者工具的Network面板来实现。
综上所述,实现每次页面加载时清除本页缓存可以根据具体情况选择合适的方法。无论是通过HTML标签、JavaScript代码、服务器端设置还是框架功能,都可以有效地控制和管理页面的缓存行为,确保用户获得最新和最准确的内容。
来源:juejin.cn/post/7389643363160965130
接口不能对外暴露怎么办?
在业务开发的时候,经常会遇到某一个接口不能对外暴露,只能内网服务间调用的实际需求。
面对这样的情况,我们该如何实现呢?
1. 内外网接口微服务隔离
将对外暴露的接口和对内暴露的接口分别放到两个微服务上,一个服务里所有的接口均对外暴露,另一个服务的接口只能内网服务间调用。
该方案需要额外编写一个只对内部暴露接口的微服务,将所有只能对内暴露的业务接口聚合到这个微服务里,通过这个聚合的微服务,分别去各个业务侧获取资源。
该方案,新增一个微服务做请求转发,增加了系统的复杂性,增大了调用耗时以及后期的维护成本。
2. 网关 + redis 实现白名单机制
在 redis 里维护一套接口白名单列表,外部请求到达网关时,从 redis 获取接口白名单,在白名单内的接口放行,反之拒绝掉。
该方案的好处是,对业务代码零侵入,只需要维护好白名单列表即可;
不足之处在于,白名单的维护是一个持续性投入的工作,在很多公司,业务开发无法直接触及到 redis,只能提工单申请,增加了开发成本;
另外,每次请求进来,都需要判断白名单,增加了系统响应耗时,考虑到正常情况下外部进来的请求大部分都是在白名单内的,只有极少数恶意请求才会被白名单机制所拦截,所以该方案的性价比很低。
3. 方案三 网关 + AOP
相比于方案二对接口进行白名单判断而言,方案三是对请求来源进行判断,并将该判断下沉到业务侧。避免了网关侧的逻辑判断,从而提升系统响应速度。
我们知道,外部进来的请求一定会经过网关再被分发到具体的业务侧,内部服务间的调用是不用走外部网关的(走 k8s 的 service)。
根据这个特点,我们可以对所有经过网关的请求的header里添加一个字段,业务侧接口收到请求后,判断header里是否有该字段,如果有,则说明该请求来自外部,没有,则属于内部服务的调用,再根据该接口是否属于内部接口来决定是否放行该请求。
该方案将内外网访问权限的处理分布到各个业务侧进行,消除了由网关来处理的系统性瓶颈;
同时,开发者可以在业务侧直接确定接口的内外网访问权限,提升开发效率的同时,增加了代码的可读性。
当然该方案会对业务代码有一定的侵入性,不过可以通过注解的形式,最大限度的降低这种侵入性。
具体实操
下面就方案三,进行具体的代码演示。
首先在网关侧,需要对进来的请求header添加外网标识符: from=public
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Override
public Mono < Void > filter ( ServerWebExchange exchange, GatewayFilterChain chain ) {
return chain.filter(
exchange.mutate().request(
exchange.getRequest().mutate().header('id', '').header('from', 'public').build())
.build()
);
}
@Override
public int getOrder () {
return 0;
}
}
接着,编写内外网访问权限判断的AOP和注解
@Aspect
@Component
@Slf4j
public class OnlyIntranetAccessAspect {
@Pointcut ( '@within(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)' )
public void onlyIntranetAccessOnClass () {}
@Pointcut ( '@annotation(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)' )
public void onlyIntranetAccessOnMethed () {
}
@Before ( value = 'onlyIntranetAccessOnMethed() || onlyIntranetAccessOnClass()' )
public void before () {
HttpServletRequest hsr = (( ServletRequestAttributes ) RequestContextHolder.getRequestAttributes()) .getRequest ();
String from = hsr.getHeader ( 'from' );
if ( !StringUtils.isEmpty( from ) && 'public'.equals ( from )) {
log.error ( 'This api is only allowed invoked by intranet source' );
throw new MMException ( ReturnEnum.C_NETWORK_INTERNET_ACCESS_NOT_ALLOWED_ERROR);
}
}
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OnlyIntranetAccess {
}
最后,在只能内网访问的接口上加上@OnlyIntranetAccess注解即可
@GetMapping ( '/role/add' )
@OnlyIntranetAccess
public String onlyIntranetAccess() {
return '该接口只允许内部服务调用';
}
4. 网关路径匹配
在DailyMart项目中我采用的是第四种:即在网关中进行路径匹配。
该方案中我们将内网访问的接口全部以前缀/pv开头,然后在网关过滤器中根据路径找到具体校验器,如果是/pv访问的路径则直接提示禁止外部访问。
使用网关路径匹配方案不仅可以应对内网接口的问题,还可以扩展到其他校验机制上。
譬如,有的接口需要通过access_token进行校验,有的接口需要校验api_key 和 api_secret,为了应对这种不同的校验场景,只需要再实现一个校验类即可,由不同的子类实现不同的校验逻辑,扩展非常方便。
来源:juejin.cn/post/7389092138900717579
Spring Boot集成pf4j实现插件开发功能
1.什么是pf4j?
一个插件框架,用于实现插件的动态加载,支持的插件格式(zip、jar)。
核心组件
- **Plugin:**是所有插件类型的基类。每个插件都被加载到一个单独的类加载器中以避免冲突。
- **PluginManager:**用于插件管理的所有方面(加载、启动、停止)。您可以使用内置实现作为JarPluginManager, ZipPluginManager, DefaultPluginManager(它是一个JarPluginManager+ ZipPluginManager),或者您可以从AbstractPluginManager(仅实现工厂方法)开始实现自定义插件管理器。
- **PluginLoader:**加载插件所需的所有信息(类)。
- **ExtensionPoint:**是应用程序中可以调用自定义代码的点。这是一个java接口标记。任何 java 接口或抽象类都可以标记为扩展点(实现ExtensionPoint接口)。
- **Extension:**是扩展点的实现。它是一个类上的 Java 注释
场景
有一个spring-boot
实现的web应用,在某一个业务功能上提供扩展点,用户可以基于SDK实现功能扩展,要求可以管理插件,并且能够在业务功能扩展点处动态加载功能。
2.代码工程
实验目的
实现插件动态加载,调用 卸载
Demo整体架构
- pf4j-api:定义可扩展接口。
- pf4j-plugins-01:插件项目,可以包含多个插件,需要实现 plugin-api 中定义的接口。所有的插件jar包,放到统一的文件夹中,方便管理,后续只需要加载文件目录路径即可启动插件。
- pf4j-app:主程序,需要依赖 pf4j-api ,加载并执行 pf4j-plugins-01 。
pf4j-api
导入依赖
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j</artifactId>
<version>3.0.1</version>
</dependency>
自定义扩展接口,集成 ExtensionPoint ,标记为扩展点
package com.et.pf4j;
import org.pf4j.ExtensionPoint;
public interface Greeting extends ExtensionPoint {
String getGreeting();
}
打包给其他项目引用
pf4j-plugins-01
如果你想要能够控制插件的生命周期,你可以自定义类集成 plugin 重新里面的方法
/*
* Copyright (C) 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pf4j.demo.welcome;
import com.et.pf4j.Greeting;
import org.apache.commons.lang.StringUtils;
import org.pf4j.Extension;
import org.pf4j.Plugin;
import org.pf4j.PluginWrapper;
import org.pf4j.RuntimeMode;
/**
* @author Decebal Suiu
*/
public class WelcomePlugin extends Plugin {
public WelcomePlugin(PluginWrapper wrapper) {
super(wrapper);
}
@Override
public void start() {
System.out.println("WelcomePlugin.start()");
// for testing the development mode
if (RuntimeMode.DEVELOPMENT.equals(wrapper.getRuntimeMode())) {
System.out.println(StringUtils.upperCase("WelcomePlugin"));
}
}
@Override
public void stop() {
System.out.println("WelcomePlugin.stop()");
}
@Extension
public static class WelcomeGreeting implements Greeting {
@Override
public String getGreeting() {
return "Welcome ,my name is pf4j-plugin-01";
}
}
}
打成jar或者zip包,方便主程序加载
pf4j-app
加载插件包
package com.et.pf4j;
import org.pf4j.JarPluginManager;
import org.pf4j.PluginManager;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.nio.file.Paths;
import java.util.List;
@SpringBootApplication
public class DemoApplication {
/* public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}*/
public static void main(String[] args) {
// create the plugin manager
PluginManager pluginManager = new JarPluginManager(); // or "new ZipPluginManager() / new DefaultPluginManager()"
// start and load all plugins of application
//pluginManager.loadPlugins();
pluginManager.loadPlugin(Paths.get("D:\\IdeaProjects\\ETFramework\\pf4j\\pf4j-plugin-01\\target\\pf4j-plugin-01-1.0-SNAPSHOT.jar"));
pluginManager.startPlugins();
/*
// retrieves manually the extensions for the Greeting.class extension point
List<Greeting> greetings = pluginManager.getExtensions(Greeting.class);
System.out.println("greetings.size() = " + greetings.size());
*/
// retrieve all extensions for "Greeting" extension point
List<Greeting> greetings = pluginManager.getExtensions(Greeting.class);
for (Greeting greeting : greetings) {
System.out.println(">>> " + greeting.getGreeting());
}
// stop and unload all plugins
pluginManager.stopPlugins();
//pluginManager.unloadPlugins();
}
}
3.测试
运行DemoApplication.java 里面的mian函数,可以看到插件加载,调用以及卸载情况
4.引用
来源:juejin.cn/post/7389912762045251584
看清裁员,你就没什么好焦虑的了
前言
Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。
前一阵子,组里面悄悄地走了两个同事,大家对此讳莫如深,原因自然不言而喻,被裁员了。
虽然裁员这件事情,在各种媒体上已经见怪不怪。但是近几年,即使是在北京,所在的公司确实还没遇到过这个情况。
本以为裁员这件事只存在于快节奏的大城市,但真正发生发生在二线城市的时候,我还是觉着心里蛮不舒服的。
因为裁员这个事情,对于个体来说,影响的确不小。
经济影响,裁员对个人的第一个影响就是收入中断。别看程序员收入比其他行业可能高一些,但是收入断层的压力依然很大。如果有房贷、车贷,更是雪上加霜。
职业发展,失去当前职位对程序员的职业发展产生不小影响,长期失业会导致技术技能的滞后,特别是在快速发展的科技行业,技术更新换代迅速。最重要的是,即使是短期的空白期,也可能使程序员在寻找新工作时,简历上的gap year也可能被HR视为一个警示信号。
心理压力,被裁员或许会经历自我怀疑、焦虑和抑郁等情绪,尤其是如果他们认为裁员是因为自己技能不足或表现不佳。这种心理压力可能会进一步影响他们在新工作中的表现和求职过程中的自信心。
所以看到身边的同事走了,我心里认为这真的是一件很糟糕的事情,从个人角度看,我认为裁员是不应该发生的。
但事实上愿望与真相的确不符合,我们常常拒绝接受真相。因为接受真相意味着改变,而改变可能会带来痛苦。
但是认清客观规律,才能认清自己,接受自己,不要固守你对事物"应该"是什么样的看法 这将使你无法了解真实的情况。
裁员存在很久了
太阳底下无新鲜事,裁员潮这件事情,早就不是第一次了。客观来讲,裁员这件事是的确是符合客观规律的,我们先回顾前面几次大的裁员潮吧,挑三个来讲。
90年代国有企业改革
1987年确定了改革开放,改革开放初期,逐步推行市场化改革,打破计划经济体制。
许多国有企业效率低下,国有企业迎来改革,负债累累,生产过剩。为了提高经济效率,政府推行国有企业改革,裁减冗员、减少亏损企业数量。
叠加政策推动,1997年党的十五大提出“抓大放小”政策,即保留和发展大型国有企业,改制或关闭小型亏损企业。
2008年全球金融危机
2008年爆发的全球金融危机导致外需急剧下降,许多出口导向型企业受到重创,全球经济衰退。
企业利润下降,为了控制成本,许多企业不得不裁员。
危机暴露了中国经济过度依赖外需和低附加值产业的问题,促使政府推动经济结构调整。
2019年互联网行业的大量裁员潮
从2018年下半年开始,不少互联网公司的裁员就已经开始,百度、阿里、腾讯、京东等大企业更是成为人们关注的焦点。
那是像摩拜、滴滴、美团等互联网行业,都是靠大量烧钱进行维持的,由于长期烧钱,投资者热情散去,各路资本也变的谨慎了。
是的,其实互联网裁员潮2019年就已经逐步开始了,互联网行业经过10多年的高速发展,慢慢走向了平稳期,互联网红利期已经过去,资本寒冬到来。
裁员背后的规律
企业战略与内部因素
成本控制与财务压力
企业在面临财务压力时,裁员通常是首选的成本控制手段之一。
通过减少员工数量,企业可以直接降低工资支出和相关福利成本。尤其是在利润率下降或财务报表不佳时,裁员成为企业迅速改善财务状况的一种方式。
比如最近理想汽车大裁员,背后就是因为纯电市场推广不顺,销售目标不及预期。
战略转型与业务重组
企业在战略转型或业务重组过程中,往往会调整其人力资源配置。例如,当企业从传统业务转向新兴技术领域时,原有的一些岗位可能不再需要,从而导致裁员。又或者,企业合并或收购也会带来很多人员问题。
比如4月底,马斯克裁撤了整个超充团队,与企业的战略是相关的,欧美充电桩市场竞争很激烈,国内的超充体系技术革新也很快,特斯拉超充的竞争力明显减弱,其实主要就是盈利问题吧。
效率提升与自动化
随着技术的进步,企业会不断寻求通过自动化和技术创新来提升效率。自动化工具和人工智能的应用可以显著减少对人力的依赖,从而导致部分职位的消失。尤其是在重复性高、技术含量低的岗位上,自动化的影响尤为明显。
这个例子就太多了,不一一列举了。
市场环境与外部因素
经济周期与市场波动
宏观经济的周期性波动对企业的经营状况有着深远影响。在经济衰退或市场需求下降时,企业往往会通过裁员来应对收入减少和利润率下滑的挑战。反之,在经济繁荣期,企业则可能增加招聘以满足业务扩张的需求。
最简单的例子,在互联网高速发展期,1年p6、3年p7根本不是梦,后端只要有过简单的开发经验,找工作根本不是问题。
目前中国经济增速放缓、消费升级趋势减弱、人口红利消失等因素的影响,中国互联网市场的需求增长趋于饱和或下降。再叠加就业市场饱和,企业对学历、经验提出了更多的要求。
行业竞争与市场压力
激烈的市场竞争也会促使企业通过裁员来保持竞争力。
当企业面临市场份额的争夺和利润率的压力时,降低运营成本成为必然选择。通过优化人力资源配置,企业可以在价格战或市场扩张中保持灵活性和优势。
目前用户规模停滞,智能手机普及率饱和,互联网用户规模增长趋于停滞,由增量市场变为存量市场,互联网获客成本越来越高。
政策变动与法规影响
政府政策和法规的变化也可能引发企业的裁员行为。例如,税收政策的调整、劳动法规的变化或贸易政策的波动,都会影响企业的运营成本和市场策略。在这种情况下,企业可能通过裁员来适应新的政策环境和经营压力。
就像目前互联网行业规范和监管愈加严格,更加注重合规,因此互联网行业也会收到影响。
技术发展与行业趋势
技术创新与产业升级
技术创新是推动产业升级的重要动力,也直接影响着就业结构的变化。新技术的应用往往带来生产方式的变革,导致传统岗位的减少和新兴岗位的增加。企业在追求技术领先的过程中,不可避免地会进行人力资源的调整。
其实,每一轮的市场繁荣,都是由技术创新带来的,例如3G时代叠加iPhone这样的智能手机,才打开了移动互联网快速发展的时期,才有了阿里 All in 无线的战略。
而现在正在快速发展的AI大模型,就是新一轮技术创新的开始,AI的可能性太大了。
行业趋势
数字化转型和远程办公的普及也在改变企业的用工模式。数字化工具的应用使得部分岗位可以通过外包或灵活用工的方式来完成,从而减少了全职员工的需求。远程办公的兴起则使企业有更多的选择来优化人力资源配置。
怎么做
通过之前的裁员历史,还有裁员背后的一些深层次原因,你应该能对裁员这件事背后的规律,有了一定的了解。
我们每个人都是时代中微不足道的一粒沙子,那么从个人角度,我们应该如何应对呢?
调整心态
年年涨薪,按时晋升机会等等这些互联网黄金时期的待遇,在当前可能不再是标配了,想要做到这一点,你需要在职场中付出比别人多几倍的努力,大家或许应该深有体会。
那么跳槽也不一定有涨薪,甚至工作时间长的人、base高的人,跳槽可能还会面临降薪。
所以一定要调整预期,如果岗位未来还有不错的发展空间,降薪也值得去。
还有就是要做好选择,不能既要(收入、光环),还要(轻松、自由)。
稳住基本盘
技术&业务能力
其实有挺多朋友加我,也不乏一些工作只有一两年的朋友,他们会感到焦虑,比如工作不好找、大厂不好进,是不是需要更换赛道。但是针对于工作年限不久的朋友,我始终给出的建议就是要先深耕技术,做好当前工作,这是作为一个技术人最基本的要求,也是立身之本。
无论市场环境如何,只要自己的技术或者业务能力在线,能够解决工作中遇到的问题,能够产生价值,那么你一定就会持续有一定竞争力。
身体
身体才是奋斗的本钱,长期加班、久坐,大家可能都会有或多或少的一些小毛病。
之所以把身体也列出来,是因为当下我真的感觉很容易精力不足,在持续大量学习的时候,很容易感到疲倦,下班之后就会什么也不想干,窝在沙发一晚上。
在你想做一件事情的时候,良好的身体状况才能保证你能够全身心的投入进去。
财富积累
良好的财富积累,既能提供经济缓冲,也可以增加自己的心里安全感。
如果想做职业转换时,有足够的储蓄也可以让你有更多的时间和资源去学习一些新技能。
提前探索
开头既然说了,国企也曾有过裁员潮,那么在互联网行业,我们大概率也很难干到退休了。
所以你需要提前探索自己可以长期耕耘的方向,并且对自己有一个清晰的自我认知。
比如经常问自己几个问题:
- 你觉得自己的内驱力是什么?喜欢做什么事情?或者自己希望自己的人生是一个怎样的状态。
- 你擅长什么技能?或者说有什么资源?比如技术、运营、销售、沟通等
- 你喜欢做的事情和擅长的技能资源等,比如健身、拍视频、写作
说在最后
好了,文章到这里就要结束了。
所以你看,由于企业战略、市场环境、技术趋势的影响,都有可能成为裁员的原因。所以我们不要认为裁员是自身能力不足,甚至影响自己的情绪。
面对裁员与互联网里不断宣扬的“35岁危机”,与其担忧焦虑,不如先调整心态,稳住自己当下的基本盘。
最重要的是提前探索,找到自己喜欢的事情,去探索自己的能力边界。
来源:juejin.cn/post/7378321582399914003
使用 uni-app 开发 APP 并上架 IOS 全过程
教你用 uni-app 开发 APP 上架 IOS 和 Android
介绍
本文记录了我使用uni-app开发构建并发布跨平台移动应用的全过程,旨在帮助新手开发者掌握如何使用uni-app进行APP开发并最终成功上架。通过详细讲解从注册开发者账号、项目创建、打包发布到应用商店配置的每一步骤,希望我的经验分享能为您提供实用的指导和帮助,让您在开发之旅中少走弯路,顺利实现自己的应用开发目标。
环境配置
IOS 环境配置
注册开发者账号
如果没有开发者账号需要注册苹果开发者账号,并且加入 “iOS Developer Program”,如果是公司项目那么可以将个人账号邀请到公司的项目中。
获取开发证书和配置文件
登录Apple Developer找到创建证书入口
申请证书的流程可以参考Dcloud官方的教程,申请ios证书教程
开发证书和发布证书都申请好应该是这个样子
创建App ID
创建一个App ID。App ID是iOS应用的唯一标识符,稍后你会在uni-app项目的配置文件中使用它。
配置测试机
第一步打开开发者后台点击Devices
第二步填写UDID
第三步重新生成开发证书并且勾选新增的测试机,建议一次性将所有需要测试的手机加入将来就不用一遍遍重复生成证书了
Android 环境配置
生成证书
Android平台签名证书(.keystore)生成指南: ask.dcloud.net.cn/article/357…
uni-app 项目构建配置
基础配置
版本号versionCode 前八位代表年月日,后两位代表打包次数
APP 图标设置
APP启动界面配置
App模块配置
注意这个页面用到什么就配置什么不然会影响APP审核
App隐私弹框配置
注意根据工业和信息化部关于开展APP侵害用户权益专项整治要求应用启动运行时需弹出隐私政策协议,说明应用采集用户数据,这里将详细介绍如何配置弹出“隐私协议和政策”提示框
详细内容可参考Uni官方文档
注意!androidPrivacy.json不要添加注释,会影响隐私政策提示框的显示!!!
在app启动界面配置勾选后会在项目中自动添加androidPrivacy.json文件,可以双击打开自定义配置以下内容:
{
"version" : "1",
"prompt" : "template",
"title" : "服务协议和隐私政策",
"message" : " 请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/> 你可阅读<a href="https://xxx.xxx.com/userPolicy.html">《服务协议》</a>和<a href="https://xxxx.xxxx.com/privacyPolicy.html">《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。",
"buttonAccept" : "同意并接受",
"buttonRefuse" : "暂不同意",
"hrefLoader" : "system|default",
"backToExit" : "false",
"second" : {
"title" : "确认提示",
"message" : " 进入应用前,你需先同意<a href="https://xxx.xxxx.com/userPolicy.html">《服务协议》</a>和<a href="https://xxx.xxxx.com/userPolicy.html">《隐私政策》</a>,否则将退出应用。",
"buttonAccept" : "同意并继续",
"buttonRefuse" : "退出应用"
},
"disagreeMode" : {
"loadNativePlugins" : false,
"showAlways" : false
},
"styles" : {
"backgroundColor" : "#fff",
"borderRadius" : "5px",
"title" : {
"color" : "#fff"
},
"buttonAccept" : {
"color" : "#22B07D"
},
"buttonRefuse" : {
"color" : "#22B07D"
},
"buttonVisitor" : {
"color" : "#22B07D"
}
}
}
我的隐私协议页面是通过vite打包生成的多入口页面进行访问,因为只能填一个地址所以直接使用生产环境的例如:xxx.xxxx.com/userPolicy.…
构建打包
使用HBuilderX进行云打包
IOS打包
构建测试包
第一步 点击发行->原生app云打包
第二步配置打包变量
运行测试包
打开HbuildX->点击运行->运行到IOS App基座
选择设备->使用自定义基座运行
构建生产包
和构建测试包基本差不多,需要变更的就是ios证书的profile文件和密钥证书
构建成功后的包在dist目录下release文件夹中
上传生产包
上传IOS安装包的方式有很多我们选择通过transporter软件上传,下载transporter并上传安装包
确认无误后点击交付,点击交付后刷新后台,一般是5分钟左右就可以出现新的包了。
App store connect 配置
上传截屏
只要传6.5和5.5两种尺寸的就可,注意打包的时候千万不能勾选支持ipad选项,不然这里就会要求上传ipad截屏
填写app信息
配置发布方式
自动发布会在审核完成后直接发布,建议选手动发布
配置销售范围
配置隐私政策
配置完之后IOS就可以提交审核了,不管审核成功还是失败Apple都会发一封邮件通知你审核结果
安卓打包
构建测试包
构建的包在dist/debug目录下
运行测试包
如果需要运行的话,点击运行 -> 运行到Android App底座
构建生产包
构建后的包在dist目录下release文件夹中
构建好安卓包之后就可以在国内的各大手机厂商的应用商店上架了,由于安卓市场平台五花八门就不给大家一一列举了。
参考链接:
结语
本文介绍了使用uni-app开发并发布跨平台移动应用的完整流程,包括注册开发者账号、项目创建、打包发布以及应用商店配置,帮助开发者高效地将应用上架到iOS和Android平台。感谢您的阅读,希望本文能对您有所帮助。
来源:juejin.cn/post/7379958888909029395
我真的不想再用mybatis和其衍生框架了选择自研亦是一种解脱
我真的不想再用mybatis和其衍生框架了选择自研亦是一种解脱
文档地址 xuejm.gitee.io/easy-query-…
GITHUB地址 github.com/xuejmnet/ea…
GITEE地址 gitee.com/xuejm/easy-…
为什么要用orm
众所邹知orm的出现让本来以sql实现的复杂繁琐功能大大简化,对于大部分程序员而言一个框架的出现是为了生产力的提升.。dbc定义了交互数据库的规范,任何数据库的操作都是只需要满足jdbc规范即可,而orm就是为了将jdbc的操作进行简化。我个人“有幸”体验过.net和java的两个大orm,只能说差距很大,当然语言上的一些特性也让java在实现orm上有着比较慢的进度,譬如泛型的出现,lambda的出现。
一个好的orm我觉得需要满足以下几点
- 强类型,如果不支持强类型那么和手写sql没有区别
- 能实现80%的纯手写sql的功能,好的orm需要覆盖业务常用功能
- 支持泛型,“如果一个orm连泛型都不支持那么就没有必要存在”这是一句现实但是又很残酷的结论,但是泛型会大大的减少开发人员的编写错误率
- 不应该依赖过多的组件,当然这并不是orm特有的,任何一个库其实依赖越少越不易出bug
其实说了这么多总结一下就是一个好的orm应该有ide的提示外加泛型约束帮助开发可以非常顺滑的把代码写下去,并且错误部分可以完全的在编译期间提现出来,运行时错误应该尽可能少的去避免。
为什么放弃mybatis
首先如果你用过其他语言的orm那么再用java的mybatis就像你用惯了java的stream然后去自行处理数据过滤,就像你习惯了kotlin的语法再回到java语法,很难受。这种难受不是自动挡到手动挡的差距,而且自动挡到手推车的差距。
xml
配置sql也不知道是哪个“小天才”想出来的,先不说写代码的时候java代码和xml代码跳来跳去,而且xml下>
,<
必须要配合CDATA
不然xml解析就失败,别说转义,我写那玩意在加转义你确定让我后续看得眼睛不要累死吗?美名其曰xml和代码分离方便维护,但是你再怎么方便修改了代码一样需要重启,并且因为代码写在xml里面导致动态条件得能力相对很弱。并且我也不知道mybatis为什么天生不支持分页,需要分页插件来支持,难道一个3202年的orm了还需要这样吗,很难搞懂mybatis的作者难道不写crud代码的吗?有些时候简洁并不是偷懒的原因,当然也有可能是架构的问题导致的。
逻辑删除的功能我觉得稍微正常一点的企业一定都会有这个功能,但是因为使用了myabtis,因为手写sql,所以常常会忘记往sql中添加逻辑删除字段,从而导致一些奇奇怪怪的bug需要排查,因为这些都是编译器无法体现的错误,因为他是字符串,因为mybatis把这个问题的原因指向了用户,这一点他很聪明,这个是用户的错误而不是框架的,但是框架要做的就是尽可能的将一些重复工作进行封装隐藏起来自动完成。
可能又会有一些用户会说所见即所得这样我才能知道他怎么执行了,但是现在哪个orm没有sql打印功能,哪个orm框架执行的sql和打印的sql是不一样的,不是所见即所得。总体而言我觉得mybatis
充其量算是sqltemlate,比sqlhelper好的地方就是他是参数化防止sql注入。当然最主要的呀一点事难道java程序员不需要修改表,不需要动表结构,不需要后期维护的吗还是说java程序员写一个项目就换一个地方跳槽,还是说java程序员每个方法都有单元测试。我在转java后理解了一点,原来这就是你们经常说的java加班严重,用这种框架加班不严重就有鬼了。
为什么放弃mybatis衍生框架
有幸在201几年再网上看到了mybatis-plus
框架,这块框架一出现就吸引了我,因为他在处理sql的方式上和.net的orm很相似,起码都是强类型,起码不需要java文件和xml文件跳来跳去,平常50%的代码也是可以通过框架的lambda表达式来实现,我个人比较排斥他的字符串模式的querywrapper
,因为一门强类型语言缺少了强类型提示,在编写代码的时候会非常的奇怪。包括后期的重构,当然如果你的代码后续不需要你维护那么我觉得你用哪种方式都是ok的反正是一次性的,能出来结果就好了。
继续说mybatis-plus
,因为工作的需要再2020年左右针对内部框架进行改造,并且让mybatis-plus支持强类型gr0up by,sum,min,max,any等api。
这个时候其实大部分情况下已经可以应对了,就这样用了1年左右这个框架,包括后续的update的increment
,decrement
update table set column=column-1 where id=xxx and column>1
全部使用lambda强类型语法,可以应对多数情况,但是针对join始终没有一个很好地方法。直到我遇到了mpj
也就是mybatis-plus-join
,但是这个框架也有问题,就是这个逻辑删除在join的子表上不生效,需要手动处理,如果生效那么在where上面,不知道现在怎么样了,当时我也是自行实现了让其出现在join的on后面,但是因为实现是需要实现某个接口的,所以并没有pr代码.
首先定义一个接口
public interface ISoftDelete {
Boolean getDeleted();
}
//其中join mapper是我自己的实现,主要还是`WrapperFunction`的那段定义
@Override
public Scf4jBaseJoinLinq<T1,TR> on(WrapperFunction<MPJAbstractLambdaWrapper<T1, ?>> onFunction) {
WrapperFunction<MPJAbstractLambdaWrapper<T1, ?>> join= on->{
MPJAbstractLambdaWrapper<T1, ?> apply = onFunction.apply(on);
if(ISoftDelete.class.isAssignableFrom(joinClass)){
SFunction deleted = LambdaHelper.getFunctionField(joinClass, "deleted", Boolean.class);
apply.eq(deleted,false);
}
return apply;
};
joinMapper.setJoinOnFunction(query->{
query.innerJoin(joinClass,join);
});
return joinMapper;
}
虽然实现了join
但是还是有很多问题出现和bug。
- 比如不支持vo对象的返回,只能返回数据库对象自定义返回列,不然就是查询所有列
- 再比如如果你希望你的对象update的时候填充null到数据库,那么只能在entity字段上添加,这样就导致这个字段要么全部生效要么全部不生效.
- 批量插入不支持默认居然是foreach一个一个加,当然这也没关系,但是你真的想实现批处理需要自己编写很复杂的代码并且需要支持全字段。而不是null列不填充
MetaObjectHandler
,支持entity
的insert
和update
但是不支持lambdaUpdateWrapper
,有时候当前更新人和更新时间都是需要的,你也可以说数据库可以设置最后更新时间,但是最后修改人呢?- 非常复杂的动态表名,拜托大哥我只是想改一下表名,目前的解决方案就是try-finally每次用完都需要清理一下当前线程,因为tomcat会复用线程,通过threadlocal来实现,话说pagehelper应该也是这种方式实现的吧
当然其他还有很多问题导致最终我没办法忍受,选择了自研框架,当然我的框架自研是参考了一部分的freesql和sqlsuagr的api,并且还有java的beetsql的实现和部分方法。毕竟站在巨人的肩膀上才能看的更远,不要问我为什么不参考mybatis的,我觉得mybatis已经把简单问题复杂化了,如果需要看懂他的代码是一件很得不偿失的事情,最终我发现我的选择是正确的,我通过参考beetsql
的源码很快的清楚了java这边应该需要做的事情,为我编写后续框架节约了太多时间,这边也给beetsql
打个广告https://gitee.com/xiandafu/beetlsql
自研orm有哪些特点
easy-query
一款无任何依赖的java全新高性能orm支持 单表 多表 子查询 逻辑删除 多租户 差异更新 联级一对一 一对多 多对一 多对多 分库分表(支持跨表查询分页等) 动态表名 数据库列高效加解密支持like crud拦截器 原子更新 vo对象直接返回
文档地址 xuejm.gitee.io/easy-query-…
GITHUB地址 github.com/xuejmnet/ea…
GITEE地址 gitee.com/xuejm/easy-…
- 强类型,可以帮助团队在构建和查询数据的时候拥有id提示,并且易于后期维护。
- 泛型可以控制我们编写代码时候的一些低级错误,比如我只查询一张表,但是where语句里面可以使用不存在上下文的表作为条件,进一步限制和加强表达式
- easy-query提供了三种模式分别是lambda,property,apt proxy其中lambda表达式方便重构维护,property只是性能最好,apt proxy方便维护,但是重构需要一起重构apt文件
单表查询
//根据条件查询表中的第一条记录
List<Topic> topics = easyQuery
.queryable(Topic.class)
.limit(1)
.toList();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t LIMIT 1
<== Total: 1
//根据条件查询id为3的集合
List<Topic> topics = easyQuery
.queryable(Topic.class)
.where(o->o.eq(Topic::getId,"3").eq(Topic::geName,"4")
.toList();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t WHERE t.`id` = ? AND t.`name` = ?
==> Parameters: 3(String),4(String)
<== Total: 1
多表
Topic topic = easyQuery
.queryable(Topic.class)
//join 后面是双参数委托,参数顺序表示join表顺序,可以通过then函数切换
.leftJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
.where(o -> o.eq(Topic::getId, "3"))
.firstOrNull();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t LEFT JOIN t_blog t1 ON t.`id` = t1.`id` WHERE t.`id` = ? LIMIT 1
==> Parameters: 3(String)
<== Total: 1
List<BlogEntity> blogEntities = easyQuery
.queryable(Topic.class)
//join 后面是双参数委托,参数顺序表示join表顺序,可以通过then函数切换
.innerJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
.where((t, t1) -> t1.isNotNull(BlogEntity::getTitle).then(t).eq(Topic::getId, "3"))
//join查询select必须要带对应的返回结果,可以是自定义dto也可以是实体对象,如果不带对象则返回t表主表数据
.select(BlogEntity.class, (t, t1) -> t1.columnAll())
.toList();
==> Preparing: SELECT t1.`id`,t1.`create_time`,t1.`update_time`,t1.`create_by`,t1.`update_by`,t1.`deleted`,t1.`title`,t1.`content`,t1.`url`,t1.`star`,t1.`publish_time`,t1.`score`,t1.`status`,t1.`order`,t1.`is_top`,t1.`top` FROM t_topic t INNER JOIN t_blog t1 ON t.`id` = t1.`id` WHERE t1.`title` IS NOT NULL AND t.`id` = ?
==> Parameters: 3(String)
<== Total: 1
子查询
```java
//SELECT * FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ?
Queryable<BlogEntity> subQueryable = easyQuery.queryable(BlogEntity.class)
.where(o -> o.eq(BlogEntity::getId, "1"));
List<Topic> x = easyQuery
.queryable(Topic.class).where(o -> o.exists(subQueryable.where(q -> q.eq(o, BlogEntity::getId, Topic::getId)))).toList();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t WHERE EXISTS (SELECT 1 FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ? AND t1.`id` = t.`id`)
==> Parameters: false(Boolean),1(String)
<== Time Elapsed: 3(ms)
<== Total: 1
//SELECT t1.`id` FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ?
Queryable<String> idQueryable = easyQuery.queryable(BlogEntity.class)
.where(o -> o.eq(BlogEntity::getId, "123"))
.select(String.class, o -> o.column(BlogEntity::getId));//如果子查询in string那么就需要select string,如果integer那么select要integer 两边需要一致
List<Topic> list = easyQuery
.queryable(Topic.class).where(o -> o.in(Topic::getId, idQueryable)).toList();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t WHERE t.`id` IN (SELECT t1.`id` FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ?)
==> Parameters: false(Boolean),123(String)
<== Time Elapsed: 2(ms)
<== Total: 0
自定义逻辑删除
//@Component //如果是spring
public class MyLogicDelStrategy extends AbstractLogicDeleteStrategy {
/**
* 允许datetime类型的属性
*/
private final Set<Class<?>> allowTypes=new HashSet<>(Arrays.asList(LocalDateTime.class));
@Override
protected SQLExpression1<WherePredicate<Object>> getPredicateFilterExpression(LogicDeleteBuilder builder,String propertyName) {
return o->o.isNull(propertyName);
}
@Override
protected SQLExpression1<ColumnSetter<Object>> getDeletedSQLExpression(LogicDeleteBuilder builder, String propertyName) {
// LocalDateTime now = LocalDateTime.now();
// return o->o.set(propertyName,now);
//上面的是错误用法,将now值获取后那么这个now就是个固定值而不是动态值
return o->o.set(propertyName,LocalDateTime.now())
.set("deletedUser",CurrentUserHelper.getUserId());
}
@Override
public String getStrategy() {
return "MyLogicDelStrategy";
}
@Override
public Set<Class<?>> allowedPropertyTypes() {
return allowTypes;
}
}
//为了测试防止数据被删掉,这边采用不存在的id
logicDelTopic.setId("11xx");
//测试当前人员
CurrentUserHelper.setUserId("easy-query");
long l = easyQuery.deletable(logicDelTopic).executeRows();
==> Preparing: UPDATE t_logic_del_topic_custom SET `deleted_at` = ?,`deleted_user` = ? WHERE `deleted_at` IS NULL AND `id` = ?
==> Parameters: 2023-04-01T23:15:13.944(LocalDateTime),easy-query(String),11xx(String)
<== Total: 0
差异更新
- 要注意是否开启了追踪
spring-boot
下用@EasyQueryTrack
注解即可开启
- 是否将当前对象添加到了追踪上下文 查询添加
asTracking
或者 手动将查询出来的对象进行easyQuery.addTracking(Object entity)
TrackManager trackManager = easyQuery.getRuntimeContext().getTrackManager();
try{
trackManager.begin();
Topic topic = easyQuery.queryable(Topic.class)
.where(o -> o.eq(Topic::getId, "7")).asTracking().firstNotNull("未找到对应的数据");
String newTitle = "test123" + new Random().nextInt(100);
topic.setTitle(newTitle);
long l = easyQuery.updatable(topic).executeRows();
}finally {
trackManager.release();
}
==> Preparing: UPDATE t_topic SET `title` = ? WHERE `id` = ?
==> Parameters: test1239(String),7(String)
<== Total: 1
关联查询
一对一
学生和学生地址
//数据库对像查询
List<SchoolStudent> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolStudentAddress).asTracking().disableLogicDelete())
.toList();
//vo自定义列映射返回
List<SchoolStudentVO> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolStudentAddress).asTracking().disableLogicDelete())
.select(SchoolStudentVO.class,o->o.columnAll()
.columnInclude(SchoolStudent::getSchoolStudentAddress,SchoolStudentVO::getSchoolStudentAddress))
.toList();
多对一
学生和班级
//数据库对像查询
List<SchoolStudent> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolClass))
.toList();
//自定义列
List<SchoolStudentVO> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolClass))
.select(SchoolStudentVO.class,o->o
.columnAll()
.columnInclude(SchoolStudent::getSchoolClass,SchoolStudentVO::getSchoolClass,s->s.column(SchoolClassVO::getId))
)
.toList();
//vo自定义列映射返回
List<SchoolStudentVO> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolClass))
.select(SchoolStudentVO.class,o->o
.columnAll()
.columnInclude(SchoolStudent::getSchoolClass,SchoolStudentVO::getSchoolClass)
)
.toList();
一对多
班级和学生
//数据库对像查询
List<SchoolClass> list1 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolStudents))
.toList();
//vo自定义列映射返回
List<SchoolClassVO> list1 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolStudents))
.select(SchoolClassVO.class,o->o.columnAll()
.columnIncludeMany(SchoolClass::getSchoolStudents,SchoolClassVO::getSchoolStudents))
.toList();
多对多
班级和老师
List<SchoolClass> list2 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolTeachers,1))
.toList();
List<SchoolClassVO> list2 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolTeachers))
.select(SchoolClassVO.class,o->o.columnAll()
.columnIncludeMany(SchoolClass::getSchoolTeachers,SchoolClassVO::getSchoolTeachers))
.toList();
动态报名
List<BlogEntity> blogEntities = easyQuery.queryable(BlogEntity.class)
.asTable(a -> "aa_bb_cc")
.where(o -> o.eq(BlogEntity::getId, "123")).toList();
==> Preparing: SELECT t.`id`,t.`create_time`,t.`update_time`,t.`create_by`,t.`update_by`,t.`deleted`,t.`title`,t.`content`,t.`url`,t.`star`,t.`publish_time`,t.`score`,t.`status`,t.`order`,t.`is_top`,t.`top` FROM aa_bb_cc t WHERE t.`deleted` = ? AND t.`id` = ?
==> Parameters: false(Boolean),123(String)
<== Total: 0
List<BlogEntity> blogEntities = easyQuery.queryable(BlogEntity.class)
.asTable(a->{
if("t_blog".equals(a)){
return "aa_bb_cc1";
}
return "xxx";
})
.where(o -> o.eq(BlogEntity::getId, "123")).toList();
==> Preparing: SELECT t.`id`,t.`create_time`,t.`update_time`,t.`create_by`,t.`update_by`,t.`deleted`,t.`title`,t.`content`,t.`url`,t.`star`,t.`publish_time`,t.`score`,t.`status`,t.`order`,t.`is_top`,t.`top` FROM aa_bb_cc1 t WHERE t.`deleted` = ? AND t.`id` = ?
==> Parameters: false(Boolean),123(String)
<== Total: 0
List<BlogEntity> x_t_blog = easyQuery
.queryable(Topic.class)
.asTable(o -> "t_topic_123")
.innerJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
.asTable("x_t_blog")
.where((t, t1) -> t1.isNotNull(BlogEntity::getTitle).then(t).eq(Topic::getId, "3"))
.select(BlogEntity.class, (t, t1) -> t1.columnAll()).toList();
==> Preparing: SELECT t1.`id`,t1.`create_time`,t1.`update_time`,t1.`create_by`,t1.`update_by`,t1.`deleted`,t1.`title`,t1.`content`,t1.`url`,t1.`star`,t1.`publish_time`,t1.`score`,t1.`status`,t1.`order`,t1.`is_top`,t1.`top` FROM t_topic_123 t INNER JOIN x_t_blog t1 ON t1.`deleted` = ? AND t.`id` = t1.`id` WHERE t1.`title` IS NOT NULL AND t.`id` = ?
==> Parameters: false(Boolean),3(String)
<== Total: 0
最后
感谢各位看到最后,希望以后我的开源框架可以帮助到您,如果您觉得有用可以点点star,这将对我是极大的鼓励
更多文档信息可以参考git地址或者文档
文档地址 xuejm.gitee.io/easy-query-…
GITHUB地址 github.com/xuejmnet/ea…
来源:juejin.cn/post/7259926933008908325
压缩炸弹,Java怎么防止
一、什么是压缩炸弹,会有什么危害
1.1 什么是压缩炸弹
压缩炸弹(ZIP)
:一个压缩包只有几十KB,但是解压缩后有几十GB,甚至可以去到几百TB,直接撑爆硬盘,或者是在解压过程中CPU飙到100%造成服务器宕机。虽然系统功能没有自动解压,但是假如开发人员在不细心观察的情况下进行一键解压(不看压缩包里面的文件大小),可导致压缩炸弹爆炸。又或者压缩炸弹藏在比较深的目录下,不经意的解压缩,也可导致压缩炸弹爆炸。
以下是安全测试几种经典的压缩炸弹
graph LR
A(安全测试的经典压缩炸弹)
B(zip文件42KB)
C(zip文件10MB)
D(zip文件46MB)
E(解压后5.5G)
F(解压后281TB)
G(解压后4.5PB)
A ---> B --解压--> E
A ---> C --解压--> F
A ---> D --解压--> G
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
压缩炸弹(也称为压缩文件炸弹、炸弹文件)是一种特殊的文件,它在解压缩时会迅速膨胀成极其庞大的文件,可能导致系统资源耗尽、崩溃或磁盘空间耗尽。
压缩炸弹的原理是利用文件压缩算法中的重复模式和递归压缩的特性。它通常是一个非常小的压缩文件,但在解压缩时会生成大量的重复数据,导致文件大小迅速增长。这种文件的设计意图是迫使系统进行大量的解压缩操作,以消耗系统资源或填满磁盘空间。
压缩炸弹可能对系统造成严重的影响,包括系统崩溃、资源耗尽、拒绝服务攻击等。因此,它被视为一种恶意的计算机攻击工具,常常被用于恶意目的或作为安全测试中的一种工具。
1.2 压缩炸弹会有什么危害
graph LR
A(压缩炸弹的危害)
B(资源耗尽)
C(磁盘空间耗尽)
D(系统崩溃)
E(拒绝服务攻击)
F(数据丢失)
A ---> B
A ---> C
A ---> D
A ---> E
A ---> F
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
压缩炸弹可能对计算机系统造成以下具体的破坏:
资源耗尽
:压缩炸弹在解压缩时会生成大量的重复数据,导致系统的CPU、内存和磁盘资源被迅速占用。这可能导致系统反应迟缓、无法响应用户的请求,甚至系统崩溃。磁盘空间耗尽
:由于压缩炸弹的膨胀特性,它可能在解压缩过程中填满磁盘空间。这会导致系统无法写入新的数据,造成磁盘空间耗尽,影响系统的正常运行。系统崩溃
:当一个压缩炸弹被解压缩时,系统可能由于资源耗尽或磁盘空间耗尽而崩溃。这可能导致系统重启或需要进行紧急修复,造成数据丢失或系统不可用的情况。拒绝服务攻击
:大规模的解压缩操作可能消耗大量系统资源,导致系统无法正常提供服务。这被恶意攻击者利用,用于进行拒绝服务攻击,使目标系统无法响应合法用户的请求。数据丢失
:在某些情况下,压缩炸弹可能会导致系统文件或数据的损坏或丢失。这可能发生在磁盘空间被耗尽时,写入操作无法成功完成的情况下。
重要提示
:压缩炸弹可能对计算机系统造成不可逆的损害,请不要尝试创建、传播或使用压缩炸弹,以保护计算机和网络的安全。
二、怎么检测和处理压缩炸弹,Java怎么防止压缩炸弹
2.1 个人有没有方法可以检测压缩炸弹?
有一些方法可以识别和处理潜在的压缩炸弹,以防止对系统造成破坏。以下是一些常见的方法:
graph LR
A(个人检测压缩炸弹)
B(安全软件和防病毒工具)
C(文件大小限制)
D(文件类型过滤)
A ---> B --> E(推荐)
A ---> C --> F(太大的放个心眼)
A ---> D --> G(注意不认识的文件类型)
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
安全软件和防病毒工具(推荐)
:使用最新的安全软件和防病毒工具可以帮助检测和阻止已知的压缩炸弹。这些工具通常具备压缩文件扫描功能,可以检查文件是否包含恶意的压缩炸弹。文件大小限制
:设置对文件大小的限制可以帮助防止解压缩过程中出现过大的文件。通过限制解压缩操作的最大文件大小,可以减少对系统资源和磁盘空间的过度消耗。文件类型过滤
:识别和过滤已知的压缩炸弹文件类型可以帮助阻止这些文件的传输和存储。通过检查文件扩展名或文件头信息,可以识别潜在的压缩炸弹,并阻止其传输或处理。
2.2 Java怎么防止压缩炸弹
在java中实际防止压缩炸弹的方法挺多的,可以采取以下措施来防止压缩炸弹:
graph LR
A(Java防止压缩炸弹)
B(解压缩算法的限制)
C(设置解压缩操作的资源限制)
D(使用安全的解压缩库)
E(文件类型验证和过滤)
F(异步解压缩操作)
G(安全策略和权限控制)
A ---> B
A ---> C
A ---> D
A ---> E
A ---> F
A ---> G
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
解压缩算法的限制
:限制解压缩算法的递归层数和重复模式的检测可以帮助防止解压缩过程无限膨胀。通过限制递归的深度和检测重复模式,可以及时中断解压缩操作并避免过度消耗资源。设置解压缩操作的资源限制
:使用Java的java.util.zip
或java.util.jar
等类进行解压缩时,可以设置解压缩操作的资源限制,例如限制解压缩的最大文件大小、最大递归深度等。通过限制资源的使用,可以减少对系统资源的过度消耗。使用安全的解压缩库
:确保使用的解压缩库是经过安全验证的,以避免存在已知的压缩炸弹漏洞。使用官方或经过广泛验证的库可以减少受到压缩炸弹攻击的风险。文件类型验证和过滤
:在解压缩之前,可以对文件进行类型验证和过滤,排除潜在的压缩炸弹文件。通过验证文件的类型、扩展名和文件头信息,可以识别并排除不安全的压缩文件。异步解压缩操作
:将解压缩操作放在异步线程中进行,以防止阻塞主线程和耗尽系统资源。这样可以保持应用程序的响应性,并减少对系统的影响。安全策略和权限控制
:实施严格的安全策略和权限控制,限制用户对系统资源和文件的访问和操作。确保只有受信任的用户或应用程序能够进行解压缩操作,以减少恶意使用压缩炸弹的可能性。
2.2.1 使用解压算法的限制来实现防止压缩炸弹
在前面我们说了Java防止压缩炸弹的一些策略,下面我将代码实现通过解压缩算法的限制
来实现防止压缩炸弹。
先来看看我们实现的思路
:
graph TD
A(开始) --> B[创建 ZipFile 对象]
B --> C[打开要解压缩的 ZIP 文件]
C --> D[初始化 zipFileSize 变量为 0]
D --> E{是否有更多的条目}
E -- 是 --> F[获取 ZIP 文件的下一个条目]
F --> G[获取当前条目的未压缩大小]
G --> H[将解压大小累加到 zipFileSize 变量]
H --> I{zipFileSize 是否超过指定的大小}
I -- 是 --> J[调用 deleteDir方法删除已解压的文件夹]
J --> K[抛出 IllegalArgumentException 异常]
K --> L(结束)
I -- 否 --> M(保存解压文件) --> E
E -- 否 --> L
style A fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style H fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style I fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style J fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style K fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style L fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style M fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
实现流程说明如下:
- 首先,通过给定的
file
参数创建一个ZipFile
对象,用于打开要解压缩的 ZIP 文件。 zipFileSize
变量用于计算解压缩后的文件总大小。- 使用
zipFile.entries()
方法获取 ZIP 文件中的所有条目,并通过while
循环逐个处理每个条目。 - 对于每个条目,使用
entry.getSize()
获取条目的未压缩大小,并将其累加到zipFileSize
变量中。 - 如果
zipFileSize
超过了给定的size
参数,说明解压后的文件大小超过了限制,此时会调用deleteDir()
方法删除已解压的文件夹,并抛出IllegalArgumentException
异常,以防止压缩炸弹攻击。 - 创建一个
File
对象unzipped
,表示解压后的文件或目录在输出文件夹中的路径。 - 如果当前条目是一个目录,且
unzipped
不存在,则创建该目录。 - 如果当前条目不是一个目录,确保
unzipped
的父文件夹存在。 - 创建一个
FileOutputStream
对象fos
,用于将解压后的数据写入到unzipped
文件中。 - 通过
zipFile.getInputStream(entry)
获取当前条目的输入流。 - 创建一个缓冲区
buffer
,并使用循环从输入流中读取数据,并将其写入到fos
中,直到读取完整个条目的数据。 - 最后,在
finally
块中关闭fos
和zipFile
对象,确保资源的释放。
实现代码工具类
:
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* 文件炸弹工具类
*
* @author bamboo panda
* @version 1.0
* @date 2023/10
*/
public class FileBombUtil {
/**
* 限制文件大小 1M(限制单位:B)[1M=1024KB 1KB=1024B]
*/
public static final Long FILE_LIMIT_SIZE = 1024 * 1024 * 1L;
/**
* 文件超限提示
*/
public static final String FILE_LIMIT_SIZE_MSG = "The file size exceeds the limit";
/**
* 解压文件(带限制解压文件大小策略)
*
* @param file 压缩文件
* @param outputfolder 解压后的文件目录
* @param size 限制解压之后的文件大小(单位:B),示例 3M:1024 * 1024 * 3L (FileBombUtil.FILE_LIMIT_SIZE * 3)
* @throws Exception IllegalArgumentException 超限抛出的异常
* 注意:业务层必须抓取IllegalArgumentException异常,如果msg等于FILE_LIMIT_SIZE_MSG
* 要考虑后面的逻辑,比如告警
*/
public static void unzip(File file, File outputfolder, Long size) throws Exception {
ZipFile zipFile = new ZipFile(file);
FileOutputStream fos = null;
try {
Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
long zipFileSize = 0L;
ZipEntry entry;
while (zipEntries.hasMoreElements()) {
// 获取 ZIP 文件的下一个条目
entry = zipEntries.nextElement();
// 将解缩大小累加到 zipFileSize 变量
zipFileSize += entry.getSize();
// 判断解压文件累计大小是否超过指定的大小
if (zipFileSize > size) {
deleteDir(outputfolder);
throw new IllegalArgumentException(FILE_LIMIT_SIZE_MSG);
}
File unzipped = new File(outputfolder, entry.getName());
if (entry.isDirectory() && !unzipped.exists()) {
unzipped.mkdirs();
continue;
} else if (!unzipped.getParentFile().exists()) {
unzipped.getParentFile().mkdirs();
}
fos = new FileOutputStream(unzipped);
InputStream in = zipFile.getInputStream(entry);
byte[] buffer = new byte[4096];
int count;
while ((count = in.read(buffer, 0, buffer.length)) != -1) {
fos.write(buffer, 0, count);
}
}
} finally {
if (null != fos) {
fos.close();
}
if (null != zipFile) {
zipFile.close();
}
}
}
/**
* 递归删除目录文件
*
* @param dir 目录
*/
private static boolean deleteDir(File dir) {
if (dir.isDirectory()) {
String[] children = dir.list();
//递归删除目录中的子目录下
for (int i = 0; i < children.length; i++) {
boolean success = deleteDir(new File(dir, children[i]));
if (!success) {
return false;
}
}
}
// 目录此时为空,可以删除
return dir.delete();
}
}
测试类
:
import java.io.File;
/**
* 文件炸弹测试类
*
* @author bamboo panda
* @version 1.0
* @date 2023/10
*/
public class Test {
public static void main(String[] args) {
File bomb = new File("D:\temp\3\zbsm.zip");
File tempFile = new File("D:\temp\3\4");
try {
FileBombUtil.unzip(bomb, tempFile, FileBombUtil.FILE_LIMIT_SIZE * 60);
} catch (IllegalArgumentException e) {
if (FileBombUtil.FILE_LIMIT_SIZE_MSG.equalsIgnoreCase(e.getMessage())) {
FileBombUtil.deleteDir(tempFile);
System.out.println("原始文件太大");
} else {
System.out.println("错误的压缩文件格式");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
三、总结
文件炸弹是一种恶意的计算机程序或文件,旨在利用压缩算法和递归结构来创建一个巨大且无限增长的文件或文件集
合。它的目的是消耗目标系统的资源,如磁盘空间、内存和处理能力,导致系统崩溃或无法正常运行。文件炸弹可能是有意制造的攻击工具,用于拒绝服务(DoS)攻击或滥用资源的目的。
文件炸弹带来的危害极大,作为开发人员,我们必须深刻认识到文件炸弹的危害性,并始终保持高度警惕,以防止这种潜在漏洞给恐怖分子以可乘之机。
总而言之,我们作为开发人员,要深刻认识到文件炸弹的危害性,严防死守,不给恐怖分子任何可乘之机。通过使用安全工具、限制文件大小、及时更新软件、定期备份数据以及加强安全意识,我们可以有效地防止文件炸弹和其他恶意活动对系统造成损害。
在中国,网络安全和计算机犯罪问题受到相关法律法规的管理和监管
。以下是一些中国关于网络安全和计算机犯罪方面的法律文献,其中也涉及到文件炸弹的相关规定:
- 《中华人民共和国刑法》- 该法律规定了各种计算机犯罪行为的法律责任,包括非法控制计算机信息系统、破坏计算机信息系统功能、非法获取计算机信息系统数据等行为,这些行为可能涉及到使用文件炸弹进行攻击。
- 《中华人民共和国网络安全法》- 该法律是中国的基本法律,旨在保障网络安全和维护国家网络空间主权。它规定了网络安全的基本要求和责任,包括禁止制作、传播软件病毒、恶意程序和文件炸弹等危害网络安全的行为。
- 《中华人民共和国计算机信息系统安全保护条例》- 这是一项行政法规,详细规定了计算机信息系统安全的保护措施和管理要求。其中包含了对恶意程序、计算机病毒和文件炸弹等威胁的防范要求。
来源:juejin.cn/post/7289667869557178404
百亿补贴为什么用 H5?H5 未来会如何发展?
23 年 11 月末,拼多多市值超过了阿里。我想写一篇文章《百亿补贴为什么用 H5》,没有动笔;24 年新年,我想写一篇《新的一年,H5 会如何发展》,也没有动笔。
眼看着灵感就要烂在手里,我决定把两篇文章合为一篇,与大家分享。当然,这些分析预测只是个人观点,如果你有不同的意见,欢迎在评论区讨论交流。
拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。
百亿补贴为什么用 H5
我们先看两张图,在 Android 手机开发者模式下,开启显示布局边界,可以看到「百亿补贴」是一个完整大框,说明「百亿补贴」在 App 内是 H5;拷贝分享链接,在浏览器打开,可以看到资源中有 react 名字的 js 文件,说明「百亿补贴」技术栈大概率是 React。
不只是拼多多,我特地确认了,京东、淘宝的的「百亿补贴」技术栈也是 H5。
那么,为什么电商巨头会在「百亿补贴」这种重要活动上选择 H5 呢?用 H5 有什么好处呢?
H5 技术已经成熟
第一个原因,也是最基础的原因,就是 H5 技术已经成熟,能够完整地实现功能。具体来说:
浏览器兼容性不断提高
自 2008 年 HTML5 草案发布以来,截止 2024 年,HTML5 已有 16 年历史。16 年间,主流浏览器对 HTML5、CSS3 和 JavaScript 的标准语法兼容性一直持续改进,22 年微软更是亲手盖上了 IE 棺材板。虽然 Safari(iOS 浏览器)的兼容性仍然备受诟病,但总体来说兼容成本已经变得可以接受。
主流框架已经成熟
前端最主流的两大框架 Vue 和 React 已经成熟。它们的成熟体现在多个方面:
- 从时间的角度看,截止 2024 年,React 已经发布了 11 年,而 Vue 已经发布了 10 年。经过多年的发展,前端开发者已经非常熟悉 React 和 Vue,能熟练地应用它们进行开发。
- 从语法的角度看,自 React16.8 发布 Hooks,以及 Vue3 发布 Composition API 以来,两大框架语法基本稳定,不再有大的变化。前端开发者可以更加专注于业务逻辑,无需过多担心框架语法的变动。
- 从未来发展方向看,React 目前致力于推广 React Server Component 1;Vue 则在尝试着无 VDom 的 Vapor 方向,并计划利用 Rust 重写 Vite 2。这表明旧领域不再有大的颠覆,两大框架已经正寻求新的发展领域。
混合开发已经成熟
混合开发是指将原生开发(Android 和 iOS)和 Web 开发结合起来的一种技术。简而言之,它将 H5 嵌入到移动应用内部运行。近些年,业界对混合开发的优势和缺陷已经有清晰的认识,并针对其缺陷进行了相应的优化。具体来说:
- 混合开发的优势包括开发速度快、一套代码适配 Android 和 iOS,以及实现代码的热更新。这意味着程序员能更快地编写跨平台应用,及时更新应用、修复缺陷;
- 混合开发的缺陷则是性能较差、加载受限于网络。针对这个缺陷,各大 App、以及云服务商如阿里云 3 和腾讯云 4 都推出了自己的离线包方案。离线包方案可以将网页的静态资源(如 HTML、CSS、JS、图片等)缓存到本地,用户访问 H5 页面时,可以直接读取本地的离线资源,从而提升页面的加载速度。可以说,接入离线包后,H5 不再有致命缺陷。
前端基建工具已经成熟
近些年来,业界最火的技术话题之一,就是用 Rust 替代前端基建,包括:用 Rust 替代 Webpack 的 Rspack;用 Rust 替代 Babel 的 SWC;用 Rust 替代 Eslint 的 OxcLint 等等。
前端开发者对基建工具抱怨,已经从「这工具能不能用」,转变为「这工具好不好用」。这种「甜蜜的烦恼」,只有基建工具成熟后才会出现。
综上所述,浏览器的兼容性提升、主流框架的成熟、混合开发的发展和前端基建工具的完善,使 H5 完全有能力承载「百亿补贴」业务。
H5 开发成本低
前文我们已经了解到,成熟的技术让 H5 可以实现「百亿补贴」的功能。现在我们介绍另一个原因——H5 开发成本低。
「百亿补贴」需要多个 H5
「百亿补贴」的方式,是一个常住的 H5,搭配上多个流动的 H5。(「常住」和「流动」是我借鉴「常住人口」和「流动人口」造的词)
- 常住 H5 链接保持不变。站外投放的链接基本都是常住 H5 的,站内首页入口链接也是常住 H5 的,这样方便用户二次访问。
- 流动 H5 链接位于常住 H5 的不同位置,比如头图、侧边栏等。时间不同、用户不同、算法不同,流动 H5 的链接都会不同,流动 H5 可以区分用户,方便分发流量。
具体来看,拼多多至少有三个流量的分发点,第一个是可点击的头图,第二个是列表上方的活动模块,第三个是右侧浮动的侧边栏,三者可以投放不同的链接。最近就分别投放 3.8 女神节链接、新人链接和品牌链接:
「百亿补贴」需要及时更新
不难想到,每到一个节日、每换一个品牌,「百亿补贴」就需要更新一次。
有时还需要为一些品牌定制化 H5 代码。如果使用其他技术栈,排期跟进通常会比较困难,但是使用 H5 就能够快速迭代并上线。
H5 投放成本低
我们已经「百亿补贴」使用 H5 技术栈的两个原因,现在来看第三个原因——H5 适合投放。
拼多多的崛起过程中,投放到其他 App 的链接功不可没。早期它通过微信等社交平台「砍一刀」的模式,低成本地吸引了大量用户。如今,它通过投放「百亿补贴」策略留住用户。
H5 的独特之处,在于它能够灵活地在多个平台上进行投放,其他技术栈很难有这样的灵活性。即使是今天,抖音、Bilibili 和小红书等其他 App 中,「百亿补贴」的 H5 链接也随处可见。
拼多多更是将 H5 这种灵活性发挥到极致,只要你有「百亿补贴」的链接,你甚至可以在微信、飞书、支付宝等地方直接查看「百亿补贴」 H5 页面。
综上所述,能开发、能快速开发、且开发完成后能大量投放,是「百亿补贴」青睐 H5 的原因。
H5 未来会如何发展
了解「百亿补贴」选择 H5 的原因后,我们来看看电商巨头对 H5 未来发展的影响。我认为有三个影响:
H5 数量膨胀,定制化要求苛刻
C 端用户黏性相对较低,换一个 App 的成本微不足道。近年 C 端市场增长缓慢,企业重点从获取更多的新客变成留住更多的老客,很难容忍用户丢失。因此其他企业投放活动 H5 时,企业必须也投放活动 H5,电商活动 H5 就变得越来越多。
这个膨胀的趋势不仅仅存在于互联网巨头的 App 中,中小型应用也不例外,甚至像 12306、中国移动、招商银行这种工具性极强的应用也无法幸免。
随着市场的竞争加剧,定制化要求也变得越来越苛刻,目的是让消费者区分各种活动。用互联网黑话来说,就是「建立用户心智」。在可预见的未来,尽管电商活动 H5 结构基本相同,但是它们的外观将变得千差万别、极具个性。
SSR 比例增加,CSR 占据主流
在各家 H5 数量膨胀、竞争激烈的情况下,一定会有企业为提升 H5 的秒开率接入 SSR,因此 SSR 的比例会增加。
但我认为 CSR 依然会是主流,主要是因为两个原因:
- SSR 需要额外的服务器费用,包括服务器的维护、扩容等。这对于中小型公司来说是一个负担。
- SSR 对程序员技术水平要求比 CSR 更高。SSR 需要程序员考虑更多的问题,例如内存泄露。使用 CSR 在用户设备上发生内存泄露,影响有限;但是如果在服务器上发生内存泄露,则是会占用公司的服务器内存,增加额外的成本和风险。
因此,收益丰厚、技术雄厚的公司才愿意使用 SSR。
Monorepo 比例会上升,类 Shadcn UI 组件库也许会兴起
如前所述,H5 的数量膨胀,代码复用就会被着重关注。我猜测更多企业会选择 Monorepo 管理方式。所谓 Monorepo,简单来说,就是将原本应该放到多个仓库的代码放入一个仓库,让它们共享相同的版本控制。这样可以降低代码复用成本。
定制化要求苛刻,我猜测社区中类似 Shadcn UI 的 H5 组件库或许会兴起。现有的 H5 组件库样式太单一,即使是 Shadcn UI,也很难满足国内 H5 的定制化需求。然而,Shadcn UI 的基本思路——「把源码下载到项目」,是解决定制化组件难复用的问题的好思路。因此,我认为类似 Shadcn 的 H5 组件库可能会逐渐兴起。
总结
本文介绍了我认为「百亿补贴」会选用 H5 的三大原因:
- H5 技术已经成熟
- H5 开发成本低
- H5 投放成本低
以及电商巨头对 H5 产生的三个影响:
- 数量膨胀,定制化要求苛刻
- SSR 比例增加,CSR 占据主流
- Monorepo 比例增加,类 Shadcn UI 组件库也许会兴起
总而言之,H5 开发会越来越专业,对程序员要求会越来越高。至于这种情况是好是坏,仁者见仁智者见智,欢迎大家在评论区沟通交流。
拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。
Footnotes
来源:juejin.cn/post/7344325496983732250
一个项目代码讲清楚DO/PO/BO/AO/E/DTO/DAO/ POJO/VO
在现代软件架构中,不同类型的类扮演着不同的角色,共同构成了一个清晰、模块化和可维护的系统。以下是对实体类(Entity)、数据传输对象(DTO)、领域对象(Domain Object)、持久化对象(Persistent Object)、业务对象(Business Object)、应用对象(Application Object)、数据访问对象(Data Access Object, DAO)、服务层(Service Layer)和控制器层(Controller Layer)的总体介绍:
不同领域作用
POJO (Plain Old Java Object)
- 定义:POJO 是一个简单的Java对象,它不继承任何特定的类或实现任何特定的接口,除了可能实现
java.io.Serializable
接口。它通常用于表示数据,不包含业务逻辑。 - 案例的体现:
UserEntity
类可以看作是一个POJO,因为它主要包含数据字段和标准的构造函数、getter和setter方法。UserDTO
类也是一个POJO,它用于传输数据,不包含业务逻辑。
VO (Value Object)
- 定义:VO 是一个代表某个具体值或概念的对象,通常用于表示传输数据的结构,不涉及复杂的业务逻辑。
VO(View Object)的特点:
- 展示逻辑:VO通常包含用于展示的逻辑,例如格式化的日期或货币值。
- 用户界面相关:VO设计时会考虑用户界面的需求,可能包含特定于视图的属性。
- 可读性:VO可能包含额外的描述性信息,以提高用户界面的可读性。
实体类(Entity)
- 作用:代表数据库中的一个表,是数据模型的实现,通常与数据库表直接映射。
- 使用场景:当需要将应用程序的数据持久化到数据库时使用。
数据传输对象(DTO)
- 作用:用于在应用程序的不同层之间传输数据,特别是当需要将数据从服务层传输到表示层或客户端时。
- 使用场景:进行数据传输,尤其是在远程调用或不同服务间的数据交换时。
领域对象(Domain Object)
- 作用:代表业务领域的一个实体或概念,通常包含业务逻辑和业务状态。
- 使用场景:在业务逻辑层处理复杂的业务规则时使用。
持久化对象(Persistent Object)
- 作用:与数据存储直接交互的对象,通常包含数据访问逻辑。
- 使用场景:执行数据库操作,如CRUD(创建、读取、更新、删除)操作。
业务对象(Business Object)
- 作用:封装业务逻辑和业务数据,通常与领域对象交互。
- 使用场景:在业务逻辑层实现业务需求时使用。
应用对象(Application Object)
- 作用:封装应用程序的运行时配置和状态,通常不直接与业务逻辑相关。
- 使用场景:在应用程序启动或运行时配置时使用。
数据访问对象(Data Access Object, DAO)
- 作用:提供数据访问的抽象接口,定义了与数据存储交互的方法。
- 使用场景:需要进行数据持久化操作时,作为数据访问层的一部分。
服务层(Service Layer)
- 作用:包含业务逻辑和业务规则,协调应用程序中的不同组件。
- 使用场景:处理业务逻辑,执行业务用例。
控制器层(Controller Layer)
- 作用:处理用户的输入,调用服务层的方法,并返回响应结果。
- 使用场景:处理HTTP请求和响应,作为Web应用程序的前端和后端之间的中介。
案例介绍
- 用户注册:
- DTO:用户注册信息的传输。
- Entity:用户信息在数据库中的存储形式。
- Service Layer:验证用户信息、加密密码等业务逻辑。
- 商品展示:
- Entity:数据库中的商品信息。
- DTO:商品信息的传输对象,可能包含图片URL等不需要存储在数据库的字段。
- Service Layer:获取商品列表、筛选和排序商品等。
- 订单处理:
- Domain Object:订单的业务领域模型,包含订单状态等。
- Business Object:订单处理的业务逻辑。
- DAO:订单数据的持久化操作。
- 配置加载:
- Application Object:应用程序的配置信息,如数据库连接字符串。
- API响应:
- Controller Layer:处理API请求,调用服务层,返回DTO作为响应。
案例代码
视图对象(VO)
一个订单系统,我们需要在用户界面展示订单详情:
// OrderDTO - 数据传输对象
public class OrderDTO {
private Long id;
private String customerName;
private BigDecimal totalAmount;
// Constructors, getters and setters
}
// OrderVO - 视图对象
public class OrderVO {
private Long id;
private String customerFullName; // 格式化后的顾客姓名
private String formattedTotal; // 格式化后的总金额,如"$1,234.56"
private String orderDate; // 格式化后的订单日期
// Constructors, getters and setters
public OrderVO(OrderDTO dto) {
this.id = dto.getId();
this.customerFullName = formatName(dto.getCustomerName());
this.formattedTotal = formatCurrency(dto.getTotalAmount());
this.orderDate = formatDateTime(dto.getOrderDate());
}
private String formatName(String name) {
// 实现姓名格式化逻辑
return name;
}
private String formatCurrency(BigDecimal amount) {
// 实现货币格式化逻辑
return "$" + amount.toString();
}
private String formatDateTime(Date date) {
// 实现日期时间格式化逻辑
return new SimpleDateFormat("yyyy-MM-dd").format(date);
}
}
实体类(Entity)
package com.example.model;
import javax.persistence.*;
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
// Constructors, getters and setters
public UserEntity() {}
public UserEntity(String username, String password) {
this.username = username;
this.password = password;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
数据传输对象(DTO)
package com.example.dto;
public class UserDTO {
private Long id;
private String username;
// Constructors, getters and setters
public UserDTO() {}
public UserDTO(Long id, String username) {
this.id = id;
this.username = username;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
领域对象(Domain Object)
package com.example.domain;
public class UserDomain {
private Long id;
private String username;
// Business logic methods
public UserDomain() {}
public UserDomain(Long id, String username) {
this.id = id;
this.username = username;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
// Additional domain-specific methods
}
领域对象通常包含业务领域内的概念和逻辑。在订单系统中,这可能包括订单状态、订单项、总价等。
package com.example.domain;
import java.util.List;
public class OrderDomain {
private String orderId;
private List items; // 订单项列表
private double totalAmount;
private OrderStatus status; // 订单状态
// Constructors, getters and setters
public OrderDomain(String orderId, List items) {
this.orderId = orderId;
this.items = items;
this.totalAmount = calculateTotalAmount();
this.status = OrderStatus.PENDING; // 默认状态为待处理
}
private double calculateTotalAmount() {
double total = 0;
for (OrderItemDomain item : items) {
total += item.getPrice() * item.getQuantity();
}
return total;
}
// 业务逻辑方法,例如更新订单状态
public void processPayment() {
// 处理支付逻辑
if (/* 支付成功条件 */) {
this.status = OrderStatus.PAYMENT_COMPLETED;
}
}
// 更多业务逻辑方法...
}
持久化对象(Persistent Object)
package com.example.model;
public class UserPersistent extends UserEntity {
// Methods to interact with persistence layer, extending UserEntity
}
业务对象(Business Object)
package com.example.service;
public class UserBO {
private UserDomain userDomain;
public UserBO(UserDomain userDomain) {
this.userDomain = userDomain;
}
// Business logic methods
public void performBusinessLogic() {
// Implement business logic
}
}
OrderBO
业务对象通常封装业务逻辑,可能包含领域对象,并提供业务操作的方法。
package com.example.service;
import com.example.domain.OrderDomain;
public class OrderBO {
private OrderDomain orderDomain;
public OrderBO(OrderDomain orderDomain) {
this.orderDomain = orderDomain;
}
// 执行订单处理的业务逻辑
public void performOrderProcessing() {
// 例如,处理订单支付
orderDomain.processPayment();
// 其他业务逻辑...
}
// 更多业务逻辑方法...
}
应用对象(Application Object)
package com.example.config;
public class AppConfig {
private String environment;
private String configFilePath;
public AppConfig() {
// Initialize with default values or environment-specific settings
}
// Methods to handle application configuration
public void loadConfiguration() {
// Load configuration from files, environment variables, etc.
}
// Getters and setters
}
数据访问对象(Data Access Object)
package com.example.dao;
import com.example.model.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserDAO extends JpaRepository {
// Custom data access methods if needed
UserEntity findByUsername(String username);
}
- OrderDAO
DAO 提供数据访问的抽象接口,定义了与数据存储交互的方法。在Spring Data JPA中,可以继承JpaRepository
并添加自定义的数据访问方法。
package com.example.dao;
import com.example.domain.OrderDomain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface OrderDAO extends JpaRepository { // 主键类型为String
// 自定义数据访问方法,例如根据订单状态查询订单
List findByStatus(OrderStatus status);
}
服务层(Service Layer)
package com.example.service;
import com.example.dao.UserDAO;
import com.example.dto.UserDTO;
import com.example.model.UserEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserDAO userDAO;
@Autowired
public UserService(UserDAO userDAO) {
this.userDAO = userDAO;
}
public UserDTO getUserById(Long id) {
UserEntity userEntity = userDAO.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
return convertToDTO(userEntity);
}
private UserDTO convertToDTO(UserEntity entity) {
UserDTO dto = new UserDTO();
dto.setId(entity.getId());
dto.setUsername(entity.getUsername());
return dto;
}
// Additional service methods
}
OrderService
服务层协调用户输入、业务逻辑和数据访问。它使用DAO进行数据操作,并可能使用业务对象来执行业务逻辑。
package com.example.service;
import com.example.dao.OrderDAO;
import com.example.domain.OrderDomain;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class OrderService {
private final OrderDAO orderDAO;
@Autowired
public OrderService(OrderDAO orderDAO) {
this.orderDAO = orderDAO;
}
public List findAllOrders() {
return orderDAO.findAll();
}
public OrderDomain getOrderById(String orderId) {
return orderDAO.findById(orderId).orElseThrow(() -> new RuntimeException("Order not found"));
}
public void processOrderPayment(String orderId) {
OrderDomain order = getOrderById(orderId);
OrderBO orderBO = new OrderBO(order);
orderBO.performOrderProcessing();
// 更新订单状态等逻辑...
}
// 更多服务层方法...
}
控制器层(Controller Layer)
package com.example.controller;
import com.example.dto.UserDTO;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public UserDTO getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
// Additional controller endpoints
}
总结
这些不同类型的类和层共同构成了一个分层的软件架构,每一层都有其特定的职责和功能。这种分层方法有助于降低系统的复杂性,提高代码的可维护性和可扩展性。通过将业务逻辑、数据访问和用户界面分离,开发人员可以独立地更新和测试每个部分,从而提高开发效率和应用程序的稳定性。
历史热点文章
- 命令模式(Command Pattern):网络爬虫任务队列实战案例分析
- 迭代器模式(Iterator Pattern):电商平台商品分类浏览实战案例分析
- 中介者模式(Mediator Pattern):即时通讯软件实战案例分析
- 备忘录模式(Memento Pattern):游戏存档系统实战案例分析
- 状态模式(State Pattern):电商平台订单状态管理实战案例分析
- 责任链模式(Chain of Responsibility Pattern):电商平台的订单审批流程实战案例分析
- 访问者模式(Visitor Pattern):电商平台商品访问统计实战案例分析
- 工厂方法模式(Factory Method Pattern): 电商多种支付实战案例分析
- 抽象工厂模式(Abstract Factory Pattern):多风格桌面应用实战案例分析
- 建造者模式(Builder Pattern): 在线订单系统实战案例分析
- 原型模式(Prototype Pattern): 云服务环境配置实战案例分析
- 适配器模式(Adapter Pattern):第三方支付集成实战案例分析
- 装饰器模式(Decorator Pattern):电商平台商品价格策略实战案例分析
- 单例模式(Singleton Pattern):购物车实战案例分析
来源:juejin.cn/post/7389212915302105098
七年前的一个思维,彻底改变了我的程序员职场轨迹
前言
Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。
最近学习了12个生财思维,受益匪浅,但是纸上得来终觉浅,绝知此事要躬行,没有亲身实践,怎么能更好的理解呢?
单纯的学习,尤其是思维工具类的学习,只看但不实践,是不会有太好的效果的。
课程中的案例虽然真实,但是每个人的眼界、能力不同,所以案例对自己只能开开眼,但自己对于思维模式的理解却不会有太多的帮助。
为了更好的理解每一个生财思维,我决定根据每一个生财思维去复盘过去十年间遇到的机遇,看看自己错过了什么,有抓住了什么,然后把学习过程中的思考重新整理出来。
希望对你有所帮助。
对标思维
什么是对标思维?
对标思维,对,对比。标,标杆。
什么是标杆,领域里面做的好的,拿到成绩的,就是标杆。
如何真正的运用好对标思维?
分成三步:
- 第一步,找到对标,通过搜索、付费都可以找到领域内出色的人。
- 第二步,研究他的成功路径,比如他做了哪些事情,学习了哪些知识,遇到了什么障碍, 又是如何解决的,或者关注他有什么好的特质,值得你去学习。
- 第三步,复盘自己的做事路径和方法,对比标杆的方法、思维、路径,针对性的改进自己。
对标思维,每一个人都在下意识的应用到。
比如:
- 技术方案设计,我们对标大厂去设计我们的方案,为什么大厂要这样设计
- 身边的同事去了大厂,我们对标他,去看看他作对了什么,学习了什么知识
- 接手新的工作,先去看看别人是怎么做的
对标并不是对比,对比着重于把两个或多个事物放在一起,比较它们之间的相同点和不同点。
而对标更强调以优秀对象为目标进行追赶和超越
对标思维的应用
大学毕业后,我选择留在了老家,我的室友毕业去了北京。他去做了互联网,而我只能选择传统信息企业。
北京的生活节奏与二线城市很不一样,上学时我们一起开黑打游戏,毕业后,只剩下了留在济南的几个人,还能一起开黑娱乐,室友因为工作原因,工作日下班几乎无法和我们打游戏。
后来无意间得知,他的薪资比我高出一倍之多,我感到很震惊。(此时我无意间找到了对标)
同样的大学,同样的专业,相差不大的成绩,在我知道后,我不禁陷入思考,为什么收入会有着如此大的差距?
上面提到,对标并不是对比,如果仅仅从薪资角度来对比,那我们之间的差距无疑也太大了。
运用对标思维,我发现他做的对的事情,是去了北京。17年的互联网行业依然如日中天,大城市的机会、薪资,都是二线城市无法比较的,彼时只要有一些热门技术的经验,哪怕是培训机构出来的,都很容易找到工作。
我不禁反思,为什么当时的我毕业时,为什么没有作出这个选择?
大学同学,大概都来源于五湖四海,而我的室友的家在东北,他对我们所在大学的城市,一定是不会有太多的感情的,所以对他而言,毕业后,自然是像北京这样的大城市,有着更大的吸引力。
而彼时的我,还处于一个舒适区里。我从上学开始就没离开过济南,外面的城市对我而言是陌生的,而在家不用租房,上下班有车开不需要坐地铁,周末有朋友一起吃饭喝酒,上班时间因为城市不大所以也不远,因此缺少了闯荡的勇气。
当我认识到这一点后,便暗暗下决心要出去闯闯看看,随后没过多久,我就在没有任何准备的情况下,裸辞去了北京。
当然因为缺少准备,我吃了不少苦头,不过这个都是后话了,有兴趣的也可以看看我之前的文章。
这件事已经过去了7年之久,我依然记忆犹新,从对标、思考、选择的路径,可以很顺畅的把这件事情完整的回忆出来,因为我在不经意间用了对标思维,做出了很大的改变,也让我认识了更多的朋友,去了更大规模的公司。
看到这里,我猜你可能心里会想,你这个思考路径,并不一定有太大的参考价值呀。你的对标不用你寻找,就在你身边,并且薪资对比如此明显,你自然很容易就会去思考如何去改变。
但是我想说,想主动应用对标思维,没有这么简单。
故事继续。
找到目标
从小带着我玩到大的哥哥,也是程序员,我选择程序员行业,也是受到了我哥哥的影响。
在那个我还没有进入社会,沉浸在大学的美好时光中的时候,哥哥就已经从腾讯跳槽到阿里,并且在工作的城市买房、定居了。
其实我哥哥也不只一次的和我讲过,提前准备好面试内容,参加校招,可能会比毕业后再进入大厂的难度要小上很多。
但我始终没有听进去,在学习了一些内容之后,便放弃了。
理论上,我在上学的时候,就已经有了这么好的一个榜样,我如果早早去对标哥哥的工作路径,了解大厂的面试标准,毕业就选择进入大厂,我几乎可以少走4年的弯路,但我还是在毕业后,选择留在了济南。
瑞·达利欧在《原则》书中提到,
个人进化过程(即我在上一条描述的循环)通过5个不同的步骤发生。如果你能把那5件事都做好,你几乎肯定可以成功。
这五步大概是:
1.有明确的目标。
2.找到阻碍你实现这些目标的问题,并且不容忍问题。
3.准确诊断问题,找到问题的根源。
4.规划可以解决问题的方案。
5.做一切必要的事来践行这些方案,实现成果。
我们就聊第一步,有明确的目标。
大四那年我在干什么?尝试着考了研,尝试着去了一家公司实习,但大部分时间还是在打游戏。错过了秋招春招,最后毕业才拿到一家offer,只能选择入职。
大学期间,我很明显是没有目标的。比如,毕业后想去什么样的公司,想拥有多少收入,去哪个城市发展。
因此即使有再好的榜样在我身边,我也无动于衷。
但当我工作之后,发现了技术上的差距,发现了工资上的差距,发现了工作环境上的差距,自己不想再浑浑噩噩下去,因此才有了改变的动力。
因此让心里埋下一个想法的种子,找到目标,才是运用思维工具的第一步。
具体如何找到目标,进行拆分,可以看看我之前的这篇文章。
说在最后
好了,文章到这里就要结束了,总结一下。
对标思维从概念上看其实不难,只需要三步即可找到对标,研究路径,复盘改进。
但是并非有了这个思维就可以立即应用,还是要有明确的目标,知道自己想要什么,才能更好的利用对标思维。
欢迎你在评论区和我分享,也希望你点赞、评论、收藏,让我知道对你有所收获,这对我来说很重要。也欢迎你加我的wx:Ldhrlhy10,一起交流~
本篇文章是第42篇原创文章,2024目标进度42/100,欢迎有趣的你,关注我。
来源:juejin.cn/post/7388488504055955492
我去,怎么http全变https了
项目场景:
在公司做的一个某地可视化项目。
部署采用的是前后端分离部署,图片等静态资源请求一台minio服务器。
项目平台用的是http
图片资源的服务器用的是https
问题描述
在以https请求图片资源时,图片请求成功报200。
【现象1】: 继图片后续的请求,后续此域名和子域名下的的url均由http变为https
【现象2】: 界面阻塞报错,无法交互
原因分析:
经过现象查阅,发现出现该现象与浏览器的HSTS有关。
什么是HSTS ?
HTTP的Strict-Transport-Security
(HSTS)请求头是一种网络安全机制,用于告诉浏览器仅通过HTTPS与服务器通信,而不是HTTP。它的作用主要有以下几点:
- 防止协议降级攻击:当浏览器接收到HSTS响应头后,它会将该网站添加到HSTS列表中,并在后续的访问中强制使用HTTPS,即使用户或攻击者尝试通过HTTP访问该网站,浏览器也会自动将其重定向到HTTPS。
- 减少中间人攻击的风险:通过确保所有通信都通过加密的HTTPS进行,可以降低中间人攻击(MITM)的风险,因为攻击者无法轻易地截获或篡改传输的数据。
- 提高网站的安全性:HSTS可以作为网站安全策略的一部分,帮助保护用户的敏感信息,如登录凭据、支付信息等。
- 简化安全配置:对于网站管理员来说,HSTS可以减少需要维护的安全配置,因为浏览器会自动处理HTTPS的重定向。
- 提高用户体验:由于浏览器会自动处理重定向,用户不需要担心访问的是HTTP还是HTTPS版本,可以更顺畅地浏览网站。
HSTS的配置可以通过max-age
指令来设置浏览器应该记住这个策略的时间长度,还可以使用includeSubDomains
指令来指示所有子域名也应该遵循这个策略。此外,还有一个preload
选项,允许网站所有者将他们的网站添加到浏览器的预加载HSTS列表中,这样用户在第一次访问时就可以立即应用HSTS策略。
于是在我发现该相关的响应头确有此物
解决方案:
那就取决于服务器是在哪里设置的该请求头。可能是在Nginx
,Lighttpd
,PHP
等等,将该响应头配置去除
来源:juejin.cn/post/7382386471272448035
再有人问你WebSocket为什么牛逼,就把这篇文章发给他!
点赞再看,Java进阶一大半
2008年6月诞生了一个影响计算机世界的通信协议,原先需要二十台计算机资源才能支撑的业务场景,现在只需要一台,这得帮"抠门"老板们省下多少钱,它就是大名鼎鼎的WebSocket协议。很快在下一年也就是2009年的12月,Google浏览器就宣布成为第一个支持WebSocket标准的浏览器。
WebSocket的推动者和设计者就是下面的Michael Carter,他设计的WebSocket协议技术现在每天在全地球有超过20亿的设备在使用。
逮嘎猴,我是南哥。
一个Java进阶的领路人,今天指南的是WebSocket,跟着南哥我们一起Java进阶。
本文收录在我开源的《Java进阶指南》中,一份帮助小伙伴们进阶Java、通关面试的Java学习面试指南,相信能帮助到你在Java进阶路上不迷茫。南哥希望收到大家的 ⭐ Star ⭐支持,这是我创作的最大动力。GitHub地址:github.com/hdgaadd/Jav…。
1. WebSocket概念
1.1 为什么会出现WebSocket
面试官:有了解过WebSocket吗?
一般的Http请求我们只有主动去请求接口,才能获取到服务器的数据。例如前后端分离的开发场景,自嘲为切图仔实际扮猪吃老虎的前端大佬找你要一个配置信息
的接口,我们后端开发三下两下开发出一个RESTful
架构风格的API接口,只有当前端主动请求,后端接口才会响应。
但上文这种基于HTTP的请求-响应模式并不能满足实时数据通信的场景,例如游戏、聊天室等实时业务场景。现在救世主来了,WebSocket作为一款主动推送技术,可以实现服务端主动推送数据给客户端。大家有没听说过全双工、半双工的概念。
全双工通信允许数据同时双向流动,而半双工通信则是数据交替在两个方向上传输,但在任一时刻只能一个方向上有数据流动
HTTP通信协议就是半双工,而数据实时传输需要的是全双工通信机制,WebSocket采用的便是全双工通信。举个微信聊天的例子,企业微信炸锅了,有成百条消息轰炸你手机,要实现这个场景,大家要怎么设计?用iframe、Ajax异步交互技术配合以客户端长轮询不断请求服务器数据也可以实现,但造成的问题是服务器资源的无端消耗,运维大佬直接找到你工位来。显然服务端主动推送数据的WebSocket技术更适合聊天业务场景。
1.2 WebSocket优点
面试官:为什么WebSocket可以减少资源消耗?
大家先看看传统的Ajax长轮询和WebSocket性能上掰手腕谁厉害。在websocket.org网站提供的Use Case C
的测试里,客户端轮询频率为10w/s,使用Poling长轮询每秒需要消耗高达665Mbps,而我们的新宠儿WebSocet仅仅只需要花费1.526Mbps,435倍的差距!!
为什么差距会这么大?南哥告诉你,WebSocket技术设计的目的就是要取代轮询技术和Comet技术。Http消息十分冗长和繁琐,一个Http消息就要包含了起始行、消息头、消息体、空行、换行符,其中请求头Header非常冗长,在大量Http请求的场景会占用过多的带宽和服务器资源。
大家看下百度翻译接口的Http请求,拷贝成curl命令是非常冗长的,可用的消息肉眼看过去没多少。
curl ^"https://fanyi.baidu.com/mtpe-individual/multimodal?query=^%^E6^%^B5^%^8B^%^E8^%^AF^%^95&lang=zh2en^" ^
-H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" ^
-H "Accept-Language: zh-CN,zh;q=0.9" ^
-H "Cache-Control: max-age=0" ^
-H "Connection: keep-alive" ^
-H ^"Cookie: BAIDUID=C8FA8569F446CB3F684CCD2C2B32721E:FG=1; BAIDUID_BFESS=C8FA8569F446CB3F684CCD2C2B32721E:FG=1; ab_sr=1.0.1_NDhjYWQyZmRjOWIwYjI3NTNjMGFiODExZWFiMWU4NTY4MjA2Y2UzNGQwZjJjZjI1OTdlY2JmOThlNzk1ZDAxMDljMTA2NTMxYmNlM1OTQ1MTE0ZTI3Y2M0NTIzMzdkMmU2MGMzMjc1OTRiM2EwNTJQ==; RT=^\^"z=1&dm=baidu.com&si=b9941642-0feb-4402-ac2b-a913a3eef1&ss=ly866fx&sl=4&tt=38d&bcn=https^%^3A^%^2F^%^2Ffclog.baidu.com^%^2Flog^%^2Fweirwood^%^3Ftype^%^3Dp&ld=ccy&ul=jes^\^"^" ^
-H "Sec-Fetch-Dest: document" ^
-H "Sec-Fetch-Mode: navigate" ^
-H "Sec-Fetch-Site: same-origin" ^
-H "Sec-Fetch-User: ?1" ^
-H "Upgrade-Insecure-Requests: 1" ^
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" ^
-H ^"sec-ch-ua: ^\^"Not/A)Brand^\^";v=^\^"8^\^", ^\^"Chromium^\^";v=^\^"126^\^", ^\^"Google Chrome^\^";v=^\^"126^\^"^" ^
-H "sec-ch-ua-mobile: ?0" ^
-H ^"sec-ch-ua-platform: ^\^"Windows^\^"^" &
而WebSocket是基于帧传输的,只需要做一次握手动作就可以让客户端和服务端形成一条通信通道,这仅仅只需要2个字节。我搭建了一个SpringBoot集成的WebSocket项目,浏览器拷贝WebSocket的Curl命令十分简洁明了,大家对比下。
curl "ws://localhost:8080/channel/echo" ^
-H "Pragma: no-cache" ^
-H "Origin: http://localhost:8080" ^
-H "Accept-Language: zh-CN,zh;q=0.9" ^
-H "Sec-WebSocket-Key: VoUk/1sA1lGGgMElV/5RPQ==" ^
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" ^
-H "Upgrade: websocket" ^
-H "Cache-Control: no-cache" ^
-H "Connection: Upgrade" ^
-H "Sec-WebSocket-Version: 13" ^
-H "Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits"
如果你要区分Http请求或是WebSocket请求很简单,WebSocket请求的请求行前缀都是固定是ws://
。
2. WebSocket实践
2.1 集成WebSocket服务器
面试官:有没动手实践过WebSocket?
大家要在SpringBoot使用WebSocket的话,可以集成spring-boot-starter-websocket
,引入南哥下面给的pom依赖。
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
dependencies>
感兴趣点开spring-boot-starter-websocket
依赖的话,你会发现依赖所引用包名为package jakarta.websocket
。这代表SpringBoot其实是集成了Java EE开源的websocket项目。这里有个小故事,Oracle当年决定将Java EE移交给Eclipse基金会后,Java EE就进行了改名,现在Java EE更名为Jakarta EE。Jakarta是雅加达的意思,有谁知道有什么寓意吗,评论区告诉我下?
我们的程序导入websocket依赖后,应用程序就可以看成是一台小型的WebSocket服务器。我们通过@ServerEndpoint可以定义WebSocket服务器对客户端暴露的接口。
@ServerEndpoint(value = "/channel/echo")
而WebSocket服务器要推送消息给到客户端,则使用package jakarta.websocket
下的Session对象,调用sendText
发送服务端消息。
private Session session;
@OnMessage
public void onMessage(String message) throws IOException{
LOGGER.info("[websocket] 服务端收到客户端{}消息:message={}", this.session.getId(), message);
this.session.getAsyncRemote().sendText("halo, 客户端" + this.session.getId());
}
看下getAsyncRemote
方法返回的对象,里面是一个远程端点实例。
RemoteEndpoint.Async getAsyncRemote();
2.2 客户端发送消息
面试官:那客户端怎么发送消息给服务器?
客户端发送消息要怎么操作?这点还和Http请求很不一样。后端开发出接口后,我们在Swagger填充参数,点击Try it out
,Http请求就发过去了。
但WebSocket需要我们在浏览器的控制台上操作,例如现在南哥要给我们的WebSocket服务器发送Halo,JavaGetOffer
,可以在浏览器的控制台手动执行以下命令。
websocket.send("Halo,JavaGetOffer");
实践的操作界面如下。
来源:juejin.cn/post/7388025457821810698
前端开发中过度封装的现象与思考
前言
作为公司内的一名高级前端码喽,大大小小也封装过了不少组件和功能,我逐渐意识到封装并非全是优点,也会存在一些不可忽视的潜在劣势。
在项目中,我们急切地对各种功能和 UI 进行封装,却在不经意间忽略了封装可能带来的额外成本与潜在问题。比如,在之前的一个项目中,为了实现一个看似简单的列表展示功能,我将数据获取、渲染逻辑以及交互处理都塞进了一个繁杂的组件中。后续当需要对列表的某一特定功能进行细微调整时,由于封装的过度复杂,修改工作变得极为棘手,耗费了大量时间去梳理内部的逻辑关系。
还有一次我在对一个表单验证功能的封装时,为追求过高的通用性,添加了过多的配置选项和繁杂的验证规则。这不但增加了代码量,还使得新加入团队的成员在使用时感到困惑,理解和运用这个封装的成本大幅提高。如果让我在写标准代码和学习过度封装的组件之间做选择,我绝对毫不犹豫的选择写标准代码。
一、前端功能封装的优势
- 可以提高代码复用性
在众多项目中,常碰到类似的数据请求、表单验证等功能需求。将这些功能封装成独立的函数或模块,能极大提升代码的复用程度。例如,我们成功封装了一个通用的数据获取函数,在不同页面中仅需传入各异的参数,就能顺利获取所需数据,无需反复编写请求逻辑。 - 有效增强代码的可维护性
封装后的功能代码相对独立,当需要对功能进行修改或优化时,只需在封装的模块内操作,不会对其他使用该功能的部分产生任何影响。如此一来,代码的维护工作变得更加清晰、易于掌控。 - 大幅提升代码的可读性
通过为封装的功能赋予清晰、有意义的函数名和详尽的参数说明,其他开发者能够迅速理解其功能和使用方式,这样也极大提高团队协作的效率。写到这里我突然想起曾经在一个屎山项目中看到过的aaaa、Areyouok、jiashizheng等变量和函数名,我花了好久的时间才把它们修改正常...
二、前端功能封装的劣势
- 事极必反
有时为追求极致的封装效果,可能会对一些简单且复用频率不高的功能进行封装,这反倒会增加代码的复杂程度和理解成本。例如,一个仅仅用于计算两个数之和的简单功能,若过度封装,可能会令后续的开发者感到迷茫。
代码示例:
function add(a, b) {
return a + b;
}
// 过度封装
function complexAdd(a, b) {
if (typeof a!== 'number' || typeof b!== 'number') {
throw new Error('输入必须为数字');
}
const result = a + b;
// 一些额外的复杂逻辑
return result;
}
在一个小型项目中,仅仅为了计算两个数字的和,使用了复杂的封装函数
complexAdd
,导致新同事在理解和使用时花费了过多时间,而原本简单的add
函数就能满足需求。 - 可能隐藏底层实现细节
过度封装或许会让使用功能的开发者对其内部实现一无所知。当问题出现时,可能需要耗费更多时间去理解封装内部的逻辑,进而影响问题的排查和解决效率。
三、UI 二次封装的优势
- 成功统一风格和交互
在大型项目中,保障 UI 的一致性至关重要。通过对基础 UI 组件进行二次封装,能够明确统一的样式、交互行为和响应式规则。例如,对按钮组件进行二次封装,设定不同状态下的颜色、尺寸和点击效果。
- 显著提高开发效率
开发人员能够直接运用封装好的 UI 组件,迅速搭建页面,无需在样式和交互的调整上耗费大量时间。
- 方便后期维护和更新
当需要对 UI 进行整体风格的调整或优化时,只需修改封装的组件,所有使用该组件的页面都会自动更新,大幅减少了维护的工作量。
四、UI 二次封装的劣势
- 过度封装的危害
- 增加不必要的代码量和复杂度,致使应用的加载性能降低。例如,一个简单的输入框组件,如果过度封装了很多复杂的逻辑和样式,可能会使代码体积过大。
- 可能引入过多的抽象层次,让代码变得难以理解和调试。复杂的封装结构可能让开发者在排查 UI 问题时感到无从下手。
- 过度复杂的封装在频繁的渲染和更新操作中,可能会导致性能瓶颈,影响用户体验。
代码示例:
// 过度封装的输入框组件
class OverlyComplexInput extends React.Component {
constructor(props) {
super(props);
this.state = { value: '' };
}
handleChange = (e) => {
// 复杂的处理逻辑
this.setState({ value: e.target.value });
// 更多的额外操作
}
render() {
return (
<input
value={this.state.value}
onChange={this.handleChange}
// 过多的样式和属性设置
/>
);
}
}
在一个性能要求较高的页面中,使用了过度封装的输入框组件,导致页面加载缓慢,用户输入时出现明显的卡顿。
- 灵活性受限
过于严格的封装可能限制了开发者在特定场景下对 UI 进行个性化定制的能力。有时候,某些页面可能需要独特的样式或交互效果,而过度封装的组件无法满足这些特殊需求。 - 版本兼容性问题
当对封装的 UI 组件进行更新时,可能会与之前使用该组件的页面产生兼容性问题。新的版本可能改变了组件的行为、样式或接口,导致使用旧版本组件的页面出现显示异常或功能失效。
所以在实际的开发过程中,我们需要权衡封装带来的好处和潜在的问题。封装应该是有针对性的,基于实际的复用需求和项目的规模。同时,要保持封装的适度性,避免过度封装带来的负面影响。只有这样,才能真正提高前端开发的效率和质量。
来源:juejin.cn/post/7387731346733121551
MySQL 9.0 创新版发布,大失所望。。
大家好,我是程序员鱼皮。2024 年 7 月 1 日,MySQL 发布了 9.0 创新版本。区别于我们大多数开发者常用的 LTS(Long-Term Support)长期支持版本,创新版本的发布会更频繁、会更快地推出新的特性和变更,可以理解为 “尝鲜版”,适合追求前沿技术的同学体验。
我通过阅读官方文档,完整了解了本次发布的新特性,结果怎么说呢,唉,接着往下看吧。。。
下面鱼皮带大家 “尝尝鲜”,来看看 MySQL 9.0 创新版本有哪些主要的变化。
新特性
1、Event 相关 SQL 语句可以被 Prepared
在 MySQL 中,事件(Events)是一种可以在预定时间执行的调度任务,比如定期清理数据之类的,就可以使用事件。
MySQL 9.0 对事件 SQL 提供了 Prepared 支持,包括:
- CREATE EVENT
- ALTER EVENT
- DROP EVENT
prepared 准备语句是一种预编译的 SQL 语句模板,可以在执行时动态地传入参数,从而提高查询的性能和安全性。
比如下面就是一个准备语句,插入的数据可以动态传入:
PREPARE stmt_insert_employee FROM 'INSERT INTO employees (name, salary) VALUES (?, ?)';
2、Performance Schema 新增 2 张表
MySQL 的 Performance Schema 是一个用于监视 MySQL 服务器性能的工具。它提供了一组动态视图和表,记录了 MySQL 服务器内部的活动和资源使用情况,帮助开发者进行性能分析、调优和故障排除。
本次新增的表:
- variables_metadata 表:提供关于系统变量的一般信息。包括 MySQL 服务器识别的每个系统变量的名称、作用域、类型、范围和描述。此表的 MIN_VALUE 和 MAX_VALUE 列旨在取代已弃用的 variables_info 表的 MIN_VALUE 和 MAX_VALUE 列。
- global_variable_attributes 表:提供有关服务器分配给全局系统变量的属性-值对的信息。
3、SQL 语句优化
现在可以使用以下语法将 EXPLAIN ANALYZE(分析查询执行计划和性能的工具)的 JSON 输出保存到用户变量中:
EXPLAIN ANALYZE FORMAT=JSON INTO @variable select_stmt
随后,可以将这个变量作为 MySQL 的任何 JSON 函数的 JSON 参数使用。
4、向量存储
AI 的发展带火了向量数据库,我们可以利用向量数据库存储喂给 AI 的知识库和文档。
虽然 MySQL 官方更新日志中并没有提到对于向量数据存储的支持,但是网上有博主在 MySQL 9.0 社区版中进行了测试,发现其实已经支持了向量存储,如图:
在此之前,MySQL 推出过一个专门用于分析处理和高性能查询的数据库变体 HeatWave,本来以为只会在 HeatWave 中支持向量存储,没想到社区版也能使用。如果是真的,那可太好了。
5、其他
此外,还优化了 Windows 系统上 MySQL 的安装和使用体验。
废弃和移除
1)在 MySQL 8.0 中,已移除了在 MySQL 8.0 中已废弃的 mysql_native_password 认证插件,并且服务器现在拒绝来自没有 CLIENT_PLUGIN_AUTH 能力的旧客户端程序的 mysql_native 认证请求。为了向后兼容性,mysql_native_password 仍然在客户端上可用;客户端内置的认证插件已转换为动态加载插件。
这些更改还涉及移除以下服务器选项和变量:
- --mysql-native-password 服务器选项
- --mysql-native-password-proxy-users 服务器选项
- default_authentication_plugin 服务器系统变量
2)Performance Schema 中 variables_info 表的 MIN_VALUE 和 MAX_VALUE 列现在已废弃,并可能在将来的 MySQL 版本中移除。开发者应该改为使用 variables_metadata 表的 MIN_VALUE 和 MAX_VALUE 列。
3)ER_SUBQUERY_NO_1_ROW 已从忽略包含 IGNORE 关键字的语句的错误列表中移除。这样做的原因如下:
- 忽略这类错误有时会导致将 NULL 插入非空列(对于未转换的子查询),或者根本不插入任何行(使用 subquery_to_derived 的子查询)。
- 当子查询转换为与派生表联接时,行为与未转换查询不同。
升级到 9.0 后,如果包含 SELECT 语句的 UPDATE、DELETE 或 INSERT 语句使用了包含多行结果的标量子查询,带有 IGNORE 关键字的语句可能会引发错误。
总结
看了本次 MySQL 9.0 创新版的更新,说实话,大失所望。在这之前,网上有很多关于 MySQL 9.0 版本新特性的猜测,结果基本上都没有出现。毕竟距离 MySQL 上一次发布的大版本 8.0 已经时隔 6 年,本来以为这次 MySQL 会有一些王炸的新特性,结果呢,本次除了修复了 100 多个 Bug 之外,几乎没啥对开发者有帮助的点。别说没帮助了,我估计很多同学在看这篇文章前都没接触过这些有变更的特性。
我们最关注的,无非就是使用难度、成本和性能提升对吧,最好是什么代码都不用改,直接升级个数据库的版本,性能提升个几倍,还能跟老板吹一波牛皮。
你看看隔壁的 PostgreSQL,这几年,都已经从 11 更新到 17 版本了,AI 时代人家也早就能通过插件支持存储向量数据了。MySQL 你这真的是创新么?
最后,MySQL 9.0 创新版本的下载地址我就不放了,咱还是老老实实用 5.7 和 8.0 版本,MySQL 的新版本,还有很长一条路要走呀!
来源:juejin.cn/post/7387999151411920931
初中都没念完的我,是怎么从IT这行坚持下去的...
大家好,我是一名二线(伪三线,毕竟连续两年二线城市了)的程序员。
现阶段状态在职,28岁,工作了10年左右,码农从事了5年左右,现薪资9k左右。如文章标题所说,初二辍学,第一学历中专,自己报的成人大专。
在掘金也看了不少经历性质的文章,大多都是很多大牛的文章,在大城市的焦虑,在大厂的烦恼,所以今天换换口味,看一看我这个没有学历的二线的程序员的经历。
1.辍学
我是在初二的时候辍学不上的,原因很简单,太二笔了。
现在想来当时的我非常的der,刚从村里的小学出来上中学之后(我还是年级第7名进中学,殊不知这就是我这辈子最好的成绩了),认为别人欺负我我就一定要还回来,完全不知道那是别人的地盘,嚣张的一批,不出意外就被锤了,但是当时个人武力还是很充沛的,按着一个往地上锤,1V7的战绩也算可以了。自此之后,我就开始走上了不良的道路,抽烟喝酒打架,直到中专毕业那天。
我清楚的记得我推着电车望着天,心里只想着一个问题,我毕业了,要工作了,我除了打游戏还会什么呢,我要拿什么生存呢...
这是当时我心里真实的想法,我好像就在这一刻、这一瞬间长大了。
2.深圳之旅
因为我特别喜欢玩游戏,而且家里电脑总是出问题,所以我就来到了我们这当地的一个电脑城打工,打了半年工左右想学习一下真正的维修技术,也就是芯片级维修,毅然决然踏上了深圳的路。
在深圳有一家机构叫做迅维的机构,还算是在业内比较出名的这么一个机构,学习主板显卡的维修,学习电路知识,学习手机维修的技术。现在的我想想当时也不太明白我怎么敢自己一个人就往深圳冲,家里人怎么拦着我都没用,当时我就好像着了魔一样必须要去...
不过在深圳的生活真的很不错,那一年的时光仍旧是我现在非常怀念的,早晨有便宜好吃的肠粉、米粉、甜包,中午有猪脚饭、汤饭、叉烧饭,晚上偶尔还会吃一顿火锅,来自五湖四海的朋友也是非常的友好,教会了我很多东西,生活非常的不错。
3.回家开店
为什么说我工作了10年左右呢,因为我清楚记得我18岁那年在本地开了一个小店,一个电脑手机维修的小店。现在想想我当时也是非常的二笔,以下列举几个事件:
- 修了一个显示器因为没接地线烧了,还跟人家顾客吵了一架。
- 修苹果手机翘芯片主板线都翘出来了,赔了一块。
- 自己说过要给人家上门保修,也忘了,人家一打电话还怼了一顿。
- 因为打游戏不接活儿。
以上这几种情况比比皆是,哪怕我当时这么二笔也是赚了一些钱,还是可以维持的,唯一让我毅然决然转行的就是店被偷了,大概损失了顾客机器、我的机器、图纸、二手电脑等一系列的商品,共计7万元左右,至今仍没找回!
4.迷茫
接下来这三年就是迷茫的几年了,第一件事就是报成人大专,主要从事的行业就杂乱无章了,跟我爸跑过车,当过网吧网管,超市里的理货员,但是这些都不是很满意,也是从这时候开始接触了C和C++开始正式踏入自学编程的路,直到有一次在招聘信息里看到java,于是在b站开始自学java,当时学的时候jdk还是1.6,学习资料也比较古老,但是好歹是入了门了。
5.入职
在入门以后自我感觉非常良好,去应聘了一个外包公司,当时那个经理就问了我一句话,会SSM吗,我说会,于是我就这么入职了,现在想想还是非常幸运的。
当时的我连SSM都用不明白,就懂一些java基础,会一些线程知识,前端更是一窍不通,在外包公司这两年也是感谢前辈带我做一些项目,当时自己也是非常争气,不懂就学,回去百度、b站、csdn各种网站开始学习,前端学习了H5、JS、CSS还有一个经典前端框架,贤心的Layui。
干的这两年我除了学习态度非常认真,工作还是非常不在意,工作两年从来没有任何一个月满勤过,拖延症严重,出现问题从来就是逃避问题,职场的知识是一点也不懂,当时的领导也很包容我,老板都主持了我的婚礼哈哈哈。但是后来我也为我的嚣张买了单,怀着侥幸心理喝了酒开车,这一次事情真真正正的打醒了我,我以后不能这样了...
6.第二家公司
在第二家公司我的态度就变了很多很多 当时已经25岁了,开始真真正正是一个大人了,遵纪守法,为了父母和家人考虑,生活方面也慢慢的好了起来(在刚结婚两年和老婆经常吵架,从这时候开始到现在没有吵过任何架了就),生活非常和睦。工作方面也是从来不迟到早退,听领导的安排,认真工作,认真学习,认识了很多同行,也得到了一些人的认可,从那开始才开始学习springboot、mq、redis、ES一些中间件,学习了很多知识,线程知识、堆栈、微服务等一系列的知识,也算是能独当一面了。但好景不长,当时我的薪资已经到13K左右了,也是因为我们部门的薪资成本、服务器成本太大,入不敷出,公司决定代理大厂的产品而不是自研了,所以当时一个部门就这么毕业了...
7.现阶段公司
再一次找工作就希望去一些自研的大公司去做事情了,但是也是碍于学历,一直没有合适的,可以说是人厌狗嫌,好的公司看不上我,小公司我又不想去,直到在面试现在公司的时候聊得非常的好,也是给我个机会,说走个特批,让我降薪入职,算上年终奖每个月到手大概10k(构成:9k月薪,扣除五险一金到手7.5k,年终奖27k,仨月全薪,所以每个月到手10k),我也是本着这个公司非常的大、非常的稳定、制度非常健全、工作也不是很忙也就来了,工作至今。
总结
- 任何时候想改变都不晚,改变不了别人改变自己。
- 面对问题绝对不能逃避,逃避没有任何用,只有面对才能更好的继续下去。
- 不要忘了自己为什么踏入这行,因为我想做游戏。
- 解决问题不要为了解决而解决,一定要从头学到尾,要不然以后出现并发问题无从下手。
- 任何事情都要合规合法。
- 工作了不要脱产做任何事情,我是因为家里非常支持,我妈至今都难以相信我能走到今天(我认为我大部分是运气好,加上赶上互联网浪潮的尾巴)。
- 最重要的,任何事情都没有家人重要,想回家就回家吧,挣钱多少放一边,IT行业找个副业还是非常简单的,多陪陪他们!
来源:juejin.cn/post/7309645869644480522