注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

iOS 开发:分享一个可以提高开发效率的技巧

iOS
前言 在日常的开发中,要想提高开发效率,重要的是要集中精力,今天来讲一个我自己日常在用的方法,我认为提高了我的开发效率,大家也可以尝试一下。 我们做开发都很讨厌写代码的过程中被打断,可能你在找一个 bug,或者在做一个很难的需求,好不容易有了思路,结果一被打断...
继续阅读 »

前言


在日常的开发中,要想提高开发效率,重要的是要集中精力,今天来讲一个我自己日常在用的方法,我认为提高了我的开发效率,大家也可以尝试一下。


我们做开发都很讨厌写代码的过程中被打断,可能你在找一个 bug,或者在做一个很难的需求,好不容易有了思路,结果一被打断,思路全忘了。所以在进入开发前,我会尽可能的把可能打断我的的因素屏蔽掉。比如我会关掉社交软件(尤其是微信),关掉软件推送。然后每过两个小时左右上一次社交软件,集中去处理消息,处理完了退掉继续工作


使用 Xcode 的时候我会开启全屏模式,这可以帮助我集中注意力,而不会分散其他应用程序的注意力,接下来讲讲如何把 Xcode 和模拟器同时进入全屏模式。


Xcode 和模拟器并行的全屏模式


最终的全屏模式如图所示,整个屏幕只有左边是 Xcode,右边是模拟器(当然你也可以调整顺序)。



这是一个能让你完全专注的环境,不被顶部的菜单栏和底部的程序坞栏内容分散注意力。


设置全屏只需这样操作:

  1. 打开 Xcode 和模拟器

  2. 点击 Xcode 左上角第三个按钮,开启全屏,或者使用快捷键 control + command + F

  3. 点击快捷键 control + ⬆️上箭头 打开程序控制,或者使用触控板上的四个手指向上滑动。

  4. 然后将你的模拟器拖入到屏幕顶部 Xcode 所在的窗口中,当拖动到窗口左侧或者右侧时,会显示一个加号,放置在上面即可

  5. 最后点击 Xcode 和模拟器所在的窗口就完成了



最后


保持专注是写好代码和提高效率的一种途径,我见过一些程序员一边写代码,一边还在用手机刷剧,这种写出的代码质量不可能很高,一心二用的开发效率也是很低的。


保持专注本身就是一种技能,刚开始你可能会觉得不习惯(没有微信消息、没有热点资讯),但当你适应了之后,你就会发现你的代码质量和效率都有一定提升,而省下来的时间足以做更多的事情了。


而且我发现每两个小时集中处理一次消息的策略还可以让处理信息的质量变高,比如以前在写代码的时候来了一条微信消息,你点开之后发现不是很重要,可以稍后再回,就先去写代码了,但是当你写完代码时可能已经忘记了回微信消息的事情(因为这条消息已经是已读状态了)。而集中处理可以把未读消息集中处理掉,不容易遗漏。


作者:iOS新知
链接:https://juejin.cn/post/7279641901526188086
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

月入五万的西二旗人教你如何活得像月薪五千

在网上看到一篇神奇的文章,确实挺发人深思了的,蜜蜂最为珍惜的是蜂蜜,殊不知自身才是养蜂人的宝贵财产 昨天,有个朋友转给我一篇文章。《月入五万的西二旗人教你如何活得像月薪五千》 这篇文章大概写的是西二旗程序员们吃饭穿衣都会选最便宜的,然后把所有剩余的钱都拿来买房...
继续阅读 »

在网上看到一篇神奇的文章,确实挺发人深思了的,蜜蜂最为珍惜的是蜂蜜,殊不知自身才是养蜂人的宝贵财产


昨天,有个朋友转给我一篇文章。《月入五万的西二旗人教你如何活得像月薪五千》 这篇文章大概写的是西二旗程序员们吃饭穿衣都会选最便宜的,然后把所有剩余的钱都拿来买房。 然后朋友也问我了,说在房价下跌的2023年,你怎么看。


这让我想起了我小时候,家里的阳台上,曾经养过的一箱蜜蜂。因为我家靠近公园,有足够的花可以作为蜜源。所以在我的记忆中,家里从来不缺蜂蜜吃,因为家里人会定期戴着防护面罩开箱,把蜜取走。


「不过我们每次取蜜,都会留下一些蜂蜜给蜜蜂过冬,如果收割得太狠,蜜蜂活不下去,就没有长久的蜂蜜吃了。不能涸泽而渔」。 小学的语文课本里,一直在歌颂蜜蜂的勤劳,可我一直有一个疑问,是不是正因为蜜蜂的勤劳,才使得它们变成了我们的绝佳收割对象。


也许,在 「蜜蜂的认知中,蜂箱蜂巢就是他们最大的资产」,但在更高一维度的养蜂人眼中,把蜂箱视为最大资产的 「蜜蜂,才是养蜂人最大的资产」


所以呢?


生活中极尽节俭,并把房子视为最大资产而背负房贷的西二旗人,会不会是更高维度的操盘手眼中的最大资产呢?


一、操盘手的鬼牌


一个年薪百万的西二旗程序员,交掉社保个税后,到手大概70万,然后公司还要额外缴纳十几万的社保。
也就是,一个西二旗程序员每创造110多万的财富,在第一次分配环节,大概还能剩70万。然后,因为他们吃饭穿衣都极为节省,所以这70万,可能又有五六十万投入了楼市。自己还剩下十几万用于生活。


投入楼市的这五六十万,可能有40多万作为土地出让金交给了操盘手,剩下付给了开发商和上下游供应商。


这么看起来,似乎程序员每赚100块钱,就有80多块以社保个税土地出让金等方式回流到了操盘手的手中。自己只剩下十几块用于生活。


但这里其实是有个问题的,西二旗人可以不买新房的。如果他向老北京人买二手房,他的这笔巨大支出,不就回流到了老北京人手中,而没有回到操盘手的手中吗?


我们来看看这个问题,是通过怎样的步骤,被操盘手解决的。


第一步,零成本选址


操盘手首先要选一块只有很少居民的土地,最好土地上没有老北京人的房子。一块荒地那就是最好了,这样就能实现零拆迁成本征用所有土地。 我们看到,北京西二旗,上海张江唐镇,杭州未科,成都天府新区,几十年前,可能都是比较荒的,也几乎都是这个套路。


第二步,引入科技公司入驻


有花才能引来蜜蜂,有工作机会才能引来年轻人。所以如果能有一些政策优惠啥的,把科技大企业引来入驻,也就等同于,引来了大批期待高薪工作的年轻人。 于是,北京西二旗后厂村路成了程序员宇宙中心,上海张江唐镇成为高科技园区。杭州未科变身未来科技城。。。


第三步,开始售卖科技公司周边的土地


当年轻人开始在科技公司上班,就会就近选择可购买的房子。但附近所有的土地,都在操盘手的手中。所以,操盘手拥有绝对的定价权。于是,年轻人以未来的收入为背书,借债购买房产,支付房产的土地出让金。并开始定期还贷。


所以,当西二旗人把西二旗的房子视为他们最大资产的时候,他们不知道的是,房子并不是最大资产,「背上债务的他们,才是别人眼中的最大资产」


通过债务的跨时间周期交易,他们把每年收入的80%以上,以社保个税土地出让金的形式交了出去。但我始终有个顾忌,80%,这样的上交比例是不是太高了。


如果把80%降为50%,也许他们就不需要996,也可以像欧洲人那样去海边晒太阳,时间多了,生育率也会更高。 目前京沪的总和生育率,已是0.7,不但是全国最低,更是全球最低。


如果涸泽而渔的话,会不会生育率提升不起来呢? 但我似乎又发现了一个隐藏的解法,对于蜜蜂我们不能涸泽而渔,「但对于西二旗和张江程序员,其实是可以涸泽而渔的」


二、谁是蜂王,谁是工蜂


在一个蜂群巢穴,是有着明确的分工的。 一个巢穴的蜜蜂分为三种,蜂王,雄蜂,和工蜂。 工蜂是雌蜂但无生育能力,只负责采蜜工作和照顾小蜜蜂。 蜂王不采蜜,只接受工蜂的养料,专职生小蜜蜂。 雄蜂也不采蜜,唯一的工作就是,和蜂王交配。


也就是说,让每个蜂种,都只从事自己最擅长的工作。 「这似乎给了我一些启示」。 虽然京沪的总和生育率已经降到了0.7,是全球最低。但这并不可怕,其实是有解法的。


我们来做个战棋推演。 一个家庭的分工,夫妻当中赚钱多的那个去赚钱,赚钱少的在家照顾孩子,会让这个家庭的效率最大化。 那么,提升全国的生育率,我们如果仅从效率最大化的角度去考虑,也会有两个方向。



  • 方向一,用最少的钱,激励出最多的生育。


从这个方向看,显然,钱应该花在三四线城市。给一线城市居民补贴50万,可能人家也不愿意生,毕竟房价生活成本高。但如果是四线城市,可能给20万,人家就愿意生了。毕竟养育成本低。 所以,基于花钱花在刀刃上的原则,「补贴三四线城市,其拉动生育效果会明显好于一线城市」。补贴一线城市一个孩子的钱,在四线城市可以补贴好几个孩子了。



  • 方向二,激励同样生育成果的前提下,花费最小的代价。


从这个角度,如果一个985高学历,年薪百万的女性,辞职生二胎照顾孩子,每年会损失百万财富的创造。但如果是一个大专学历,年薪5万的女性辞职生二胎照顾孩子,每年只损失5万财富的创造。 也就是说,达成同样生育数量的情况下,代价是完全不同的。


当然,从人文角度,985女当然和大专女享有同等生育权。从个人角度自主生育的话,那当然都没问题,盈亏反正也是自负。但如果说要操盘手额外花钱激励生育的话,从全国总盘子的效率角度考虑,激励大专女,会代价更小。 那如果操盘手只从效率最大化的角度考虑,很显然,应该让一线城市高学历中产尽可能努力工作,并通过 「税收或买房形成的支付转移」,转移到三四线城市去补贴育龄女性生育。等三四线孩子长大,通过高考选拔后,再进入一线城市,开始下一次循环。 从这个角度出发,很明显, 北京西二旗或上海张江的程序员,贡献蜂蜜,低生育率,是工蜂; 三四线城市多子女家庭,获取转移支付的蜂蜜,高生育率,是蜂王。


蜂王的子女长大后,再去一线城市进入新的一次循环。从而一线城市低生育率问题可解。 而现在的真实情况也确实是如此,比如贵州的总和生育率,就是上海的大约三倍。 有人可能会问,蜂王子女长大后去一线,能那么容易留下来么? 答案是,容易的! 因为今天不容易不代表未来不容易,万物皆周期! 按上海如今0.7的总和生育率,每过一代,就会损失2/3的人口。两代过后,90%的人口就没了。


这时候,是急切需要蜂王的后代,来上海补充年轻劳动力的。 我记得20年前,上海还有一些教上海话的电视节目,而今天几乎绝迹。既然两代之后,上海人口就损失90%,那自然上海也就会变成一个完全的普通话城市。


三、北京西二旗和上海张江男的终极宿命


最后一个问题,西二旗程序员,为啥心甘情愿在吃穿上拼命节省,而把大笔的钱投入楼市呢?


答案是,他们认为房子是核心资产


但问题在于,任何资产,或者说财富,其本质,都是对他人劳动的索取权。也就是说,世间的一切资产,不论是房子,股票,货币,黄金,它最终要能兑换成人的劳动,才有意义。


可问题就在于,2020年之后的生育率断崖式下跌了。未来所有的人,都会盯着这仅有的少数年轻人的劳动价值。 这其中,当然也包括操盘手。毕竟操盘手要负责老人养老金,公务员工资,义务教育等一系列花钱的地方。


现在西二旗人每年收入的80%,切切实实通过各种渠道给出去了,然后换来了一套西二旗的大房子。可30年后,如果西二旗人要用这套房子去换取未来年轻人同样的劳动时,操盘手能让他们得逞吗? 操盘手会不会和今天一样,同样划出一块荒地,然后引入30年后的风口科技公司(不知道会不会是超导,人工智能这些,还是更超前的公司),然后把年轻人引到新的地块呢?毕竟只有这样,才能最大化虹吸未来年轻人的劳动价值。


毕竟蜂巢不是资产,采蜜的蜜蜂,才是操盘手最大的资产。 而30年后,目前人口结构处于青壮年期的西二旗,张江,会不会自然衰老为一个以六七十岁年龄结构为主的老龄化社区呢?


如果一个社区,居民都变成了中老年,即便没有操盘手号召,企业出于自身招聘的考虑,也要搬走了。至于搬去哪里,那自然要看操盘手要把年轻人引向哪里。 如果一个社区,没有企业和工作机会,住的都是中老年,那么必然就不存在接盘力量。


这一点,似乎细思极恐。 原住民年轻时花大力气努力购买的房子,最后会变成一个笑话吗? 如果真是如此,那么该社区原住民的终极悲惨宿命,也就是必然的结局了


四、后记


本文无意得罪张江和西二旗的程序员,因为文中所说的逻辑,其实适用于所有在科技新区安家的一二线城市中产。


但因为我自己是一个前淘宝的程序员。想想还是自嘲下自己这个群体吧。


不过确实能反映当下一些问题引发一些思考,当然还是要保持积乐观的生活态度,想到了学生时代 学习的 普希金的一首诗《假如生活欺骗了你》


「假如生活欺骗了你,」


「不要悲伤,不要心急!」


「忧郁的日子里须要镇静:」


「相信吧,快乐的日子将会来临!」


「心儿永远向往着未来;」


「现在却常是忧郁。」


「一切都是瞬息,一切都将会过去;」


「而那过去了的,就会成为亲切的怀恋。」


作者:Android茶话会
来源:juejin.cn/post/7268975896370937893
收起阅读 »

为什么我们总是被赶着走

最近发生了一些事情,让shigen不禁的思考:为什么我们总是被各种事情赶着走。 一 第一件事情就是工作上的任务,接触的是一个老系统ERP,听说是2018年就在线上运行的,现在出现问题了,需要我去修改一下。在这里,我需要记录一下技术背景: ERP系统背景 后端...
继续阅读 »

最近发生了一些事情,让shigen不禁的思考:为什么我们总是被各种事情赶着走。



第一件事情就是工作上的任务,接触的是一个老系统ERP,听说是2018年就在线上运行的,现在出现问题了,需要我去修改一下。在这里,我需要记录一下技术背景:



ERP系统背景

后端采用的是jfinal框架,让我觉得很奇葩的地方有:



  • 接受前端的参数采用的HashMap封装,意味着前端字段传递的值可以为字符串、数字(float double)

  • 仅仅一个金额,可以有多种形式:1111.001,1,111.001

  • 格式化 1.00000100 小数点保存8位,这样的显示被骂了

  • 数据库采用的是oracle,jfinal的ORM工具可以采取任何的类型存入数据表的字段里,我就遇到了‘1.1111’字符串存入到定义为double的字段中

  • 原来的设计者存储金额、数量全部采用 flaot、double,凭空出现0.0000000000000001的小数,导致数量金额对不上

  • 小数位0.00000000001 会在前端显示成1-e10,直接在sql上格式化

  • sql动辄几百行,上千行,各种连表

  • sql还会连接字典表,显示某个值代表的含义

  • ……


前端不知道啥框架,接近于jquery+原生的js



  • 每改一段代码,都需要重启后端服务

  • 各种代码冗余

  • 后端打包一次40分钟+

  • ……


最关键的是:所有的需求口头说,我也是第一次接触,一次需求没理解,被运维的在办公室大声批评:你让用户怎么想?



后来,需求本来要半个月完成,拖了一个月才勉强结束。一次快下班的时候出现了问题,我没有加班,也因为遇到了问题没人帮忙。第二天问进度,没进展,领导叫去看会,说态度不好。后来换组了……



第二件事情就是我的公众号更新问题,我在八月份的时候个自己定了一个目标:公众号不停更。到最近一段时间发现:很难保持每天更新的需求了。因为我接触到的技巧很少,每篇文章的成本也很大。就拿我的某个需求为例,我需要先把代码写出来,测试完成之后再去写文章,这整个过程最低也需要两个小时的时间。成本很大,所以我有一次很难定顶住这个压力,推荐了往期的文章。


我也经常关注一些技术类的博客,看他们写的文章发现部分的博客都是互相抄袭的,很难保持高质量。更多的是在贩卖焦虑,打广告。


我希望我的每一篇文章都是有意义的,都是原创的、有价值的。所以,我也在陷入了矛盾中,成本这么大,我需要改变一下更新的节奏吗?



最后一件事情就是:我感冒了。


事情是这样的,一连几天没有去跑步了,家里的健腹轮也很少去练了,除了每天骑行了5公里外,我基本没有啥运动量。我以为我吃点维生素B、维生素C我的体质就会好一点,大错特错了。


周一发现嗓子有点干痒疼,晚上还加了班,睡觉的时候已经是凌晨一点了。周二就头很晕、带一点发热的症状,我赶紧下午去医院,在前台测了一下体温,直接烧到了28.4摄氏度。血常规检测发现是病毒性感染,买了两盒药回来了。下午一直在睡觉,睡到了十一点。


也在想:难道我的体质真的这么差吗?如果我坚持那几天戴口罩,坚持运动会不会好一些。我想到了我的拖延症。


我的dock栏永远是满的,各种软件经常打开着,Java、数据库,总是有很多的事情要去做,很忙的样子,最后发现没时间去运动了。一次健腹轮的运动不到十分钟,我都没有去行动。



这次的感冒,让我更加的重视起我的健康了,也让我觉得我丧失了主动性,总是被生活赶着走。


所以,提到了这么多,涉及到了任务的规划、任务中的可变因素……我觉得除了计划之外,更多的是需要保持热爱。不仅仅是热爱生活、热爱运动、热爱事业,更是热爱自己拥有的一切,因为:爱你所爱,即使所爱譬如朝露


作者:shigen01
来源:juejin.cn/post/7280740613891981331
收起阅读 »

智能门锁临时密码的简单实现~~

引子 话说新房子装修,安装了遥遥领先智能门锁PRO,最近到了家具进场的阶段。 某日,接到一通电话:“哥,你现在家里有人吗?你的书桌到了。” 原来是快递小哥,我回复他:“家里没人,但是有智能锁,嗯,因为临时密码有时间限制,等下到了再给我回下电话,我把临时密码给你...
继续阅读 »

引子


话说新房子装修,安装了遥遥领先智能门锁PRO,最近到了家具进场的阶段。


某日,接到一通电话:“哥,你现在家里有人吗?你的书桌到了。”


原来是快递小哥,我回复他:“家里没人,但是有智能锁,嗯,因为临时密码有时间限制,等下到了再给我回下电话,我把临时密码给你。”


“好嘞,那到时候联系”


挂断电话,我随手打开手机上的花粉生活APP,但是感觉有点不对劲,我去,设备咋都离线了(后来发现是网络欠费)?我顿时虎躯一震,脑海中浮现了快递小哥到了后发现自己白跑一趟,带着满头大汗、气喘吁吁并且嘴里一顿C语言输出的尴尬场景...


但是我惊喜的发现,门锁的卡片虽然离线但还可以正常进入,我抱着试一试的心态点进去,临时密码竟然可以正常生成,真牛!


于是我点击了生成临时密码...


电话又响起:“哥我到了,把密码给我吧”


我将临时密码给小哥开了门,一切顺利...




实现


这是前段时间亲身经历的一件事,原本以为智能门锁临时密码的功能需要网络支持,服务器生成临时密码给用户,同时下发到门锁里面。现在发现,并不需要门锁联网也可以执行密码验证的操作。
脑海中思考了下,临时密码离线验证这个功能可能是类似这样实现的:



  • 门锁端和服务器端采用相同的规则生成临时密码,并且密码生成规则里面包含了时间这个因素

  • 用户请求临时密码,服务端按照规则生成临时密码返回给用户

  • 用户输入临时密码解锁,门锁按照同样的规则进行校验
    以上实现是一个直觉性的思考,实际编码落地根据不同的需求会有更多的考虑,以我在使用的遥遥领先牌智能门锁Pro为例,下面来做一个简单的实现...


首先,让来看看这款门锁的临时密码有哪些限制条件:


limit12.png


lim22.png


限制条件有:



  • 单个密码有效期为30分钟

  • 有效期内只能使用一次

  • 一分钟内只能添加一个临时密码


根据这些限制条件和前面的思考,密码生成规则可以这样设置:



  • 拼接产品序列号+当前时间字符串,获取拼接后字符串的hashcode,然后对1000000(百万)取余,得到6位数字作为临时密码。并且时间字符串按照yyyy-MM-dd HH:mm 格式,精确到分钟

  • 加入产品序列号的原因是为了让不同门锁在相同时间产生不同的密码,如果只以时间为变量肯定是不安全的

  • 由于门锁生成的限制条件里面约定了一分钟只能添加一个临时密码,因此时间变量也精确到分钟,保证每分钟的临时密码不同,分钟内相同。


然后是实现思路:



  • 用户请求服务端,服务端根据密码生成规则返回一个临时密码

  • 快递小哥拿着临时密码在门锁现场输入

  • 门锁按照临时密码输入的时间点,计算时间点前30分内每一分钟对应的密码,30分钟对应30个临时密码。为什么是30分钟?因为密码30分钟内有效

  • 门锁将快递小哥输入的密码与生成的30个密码进行一一比对,如果有匹配的密码,说明临时密码有效

  • 将输入的临时密码缓存,每次输入密码时都要去缓存里面判断临时密码是否在30分钟内使用过,如果使用过就不能开锁。为什么要判断是否30分钟内使用过?因为有效期内只能使用一次




有了以上思路,下面代码的编写工作就比较简单了,开整...


首先创建三个类:OtherTerminal、SmartLock、PasswordUtils 分别,表示其他可获取密码的终端、门锁以及跟密码相关的工具类


首先是OtherTerminal类,相当于可获取密码的终端,例如我们的手机或者平板,主要功能是调用PasswordUtils工具类根据门锁的序列号和当前时间来获取有效临时密码。



public class OtherTerminal {
private final static String serialNumber = "XiaoHuaSmartLock001";
public static void main(String[] args) {
System.out.println("当前开锁密码:"+PasswordUtils.generate(serialNumber, PasswordUtils.localDateTimeToStr(LocalDateTime.now())));
}
}


接着是SmartLock类


SmartLock的main方法里面等待控制台的输入,并对输入的密码进行验证。验证调用了verify方法。


verify方法的执行逻辑:调用PasswordUtils工具类,获取过去30分钟内每分钟对应的临时密码,判断输入的密码是否在这些临时密码当中。如果存在说明临时密码有效,还需对当前密码在过去30分钟内是否使用进行判断,保证密码只能使用一次。这个判断是通过调用PasswordUtils工具类的getAndSet方法实现的。


如果认证成功,则开锁。否则开锁失败。


// 智能门锁
public class SmartLock {

private final static String serialNumber = "XiaoHuaSmartLock001";
private final static Integer expirationTime = 30;


public static void main(String[] args) {
// 步骤:首先生成过去30分钟内的所有数字

Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
int password = scanner.nextInt();
if (verify(password)) {
System.out.println("开锁成功,当前时间:" + LocalDateTime.now());
} else {
System.out.println("开锁失败,当前时间:" + LocalDateTime.now());
}
}
scanner.close();

}

private static boolean verify(Integer inputPassword) {
// 获取当前时间点以前30分钟内的所有密码
LocalDateTime now = LocalDateTime.now();
LocalDateTime validityPeriod = now.minusMinutes(expirationTime);
List<Integer> validityPeriodPasswords = new ArrayList<>();

while (validityPeriod.isBefore(now.plusMinutes(1L))) {
validityPeriodPasswords.add(PasswordUtils.generate(serialNumber, PasswordUtils.localDateTimeToStr(validityPeriod)));
validityPeriod = validityPeriod.plusMinutes(1L);
}
System.out.println(validityPeriodPasswords);
return validityPeriodPasswords.contains(inputPassword) && PasswordUtils.getAndSet(inputPassword);
}
}

再来看下PasswordUtils工具类,这个类内容较多,分步解释:
首先是生成6位临时密码的generate方法,比较简单。但是这样生成的密码不能以0开头,是缺点!


/**
* 生成一个密码
*
* @return 返回一个六位正整数
*/

public static Integer generate(String serialNumber, String time) {
String toHash = time + serialNumber;
return Math.abs(toHash.hashCode() % 1000000);
}

接着是一个格式化时间的方法,将时间格式化为:yyyy-MM-dd HH:mm。精确到分钟,generate方法的第二个参数time需要调用此方法来保证时间以分钟为单位,这样分钟内生成的密码都是相同的


public static String localDateTimeToStr(LocalDateTime localDateTime) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
return formatter.format(localDateTime);
}

最后是门锁对临时密码的管理:



  • 临时密码存储在一个map对象中:usedPasswordMap

  • 有一个标记对象clearTag用于标记是否应当对usedPasswordMap进行清理操作,用于清理已过期的临时密码

  • 临时密码存在时间大于30分钟,判断为已过期


下面是临时密码过期判断和过期清理的方法


/**
* @param current 当前时间
* @param compare 比较时间
* @return 是否过期
*/

private static boolean expired(long current, long compare) {
Instant endInstant = Instant.ofEpochMilli(current);
LocalDateTime end = LocalDateTime.ofInstant(endInstant, ZoneId.systemDefault());
Instant beginInstant = Instant.ofEpochMilli(compare);
LocalDateTime begin = LocalDateTime.ofInstant(beginInstant, ZoneId.systemDefault());

Duration duration = Duration.between(begin, end);
long actualInterval = switch (PasswordUtils.expirationUnit) {
case SECONDS -> duration.toSeconds();
case MINUTES -> duration.toMinutes();
case HOURS -> duration.toHours();
case DAYS -> duration.toDays();
default -> throw new IllegalArgumentException("输入时间类型不支持");
};
return actualInterval >= (long) PasswordUtils.expirationTime;
}

/**
* 清理过期的密码
*/

private static void clearExpired() {
Iterator<Map.Entry<Integer, Long>> iterator = usedPasswordMap.entrySet().iterator();
Long currentTimestamp = System.currentTimeMillis();
while (iterator.hasNext()) {
Map.Entry<Integer, Long> item = iterator.next();
if (expired(currentTimestamp, item.getValue())) {
iterator.remove();
}
}
}

getAndSet方法:



  • 首先判断是否达到了清理阈值,从而执行是否清理的操作,用于节省资源消耗

  • 从usedPasswordMap中获取当前输入密码是否存在,如果不存在说明密码未使用过,则将当前密码设置到map里面并返回true,否则还要进行进一步的判断,因为可能存在历史密码但是已过期和当前密码重复的情况

  • 若usedPasswordMap中存在当前密码,调用expired方法,如果历史密码过期了说明当前密码有效,并刷新时间戳,否则说明有效期内当前密码已经使用过一次


/**
*
* @param password
* @return false说明密码已经使用过,true则表示密码可以使用
*/

public static boolean getAndSet(Integer password) {
// usedPasswordMap存储的过期密码可能会越来越多,需要定期清理
if (clearTag > clearThreshold) {
if (!usedPasswordMap.isEmpty()) {
clearExpired();
}
clearTag = 0;
}
clearTag++;
Long usedPasswordTimestamp = usedPasswordMap.get(password);
Long currentTimestamp = System.currentTimeMillis();
if (ObjectUtils.isEmpty(usedPasswordTimestamp)) {
usedPasswordMap.put(password, currentTimestamp);
return true;
}
// 到了这里说明密码已经使用过(有效期内,或之前),若使用时间距今在有效期内,说明当期已经使用过,否则是以前使用的
if (expired(currentTimestamp, usedPasswordTimestamp)) {
usedPasswordMap.put(password, currentTimestamp);
System.out.println("密码虽然已使用,但为历史使用,因此当前密码有效");
return true;
}
System.out.println("密码有效期内已使用一次");
return false;
}



验证


我将门锁程序部署到我的服务器上面,并运行。随便输入一个数字,例如123456,返回开锁失败。


image.png


然后本地运行OtherTerminal类获取临时密码:974971


image.png
再去门锁上验证试试:开锁成功!


image.png


最后完整的PasswordUtil工具类的代码贴在这里:


// 密码工具类

public class PasswordUtils {
private static Map<Integer, Long> usedPasswordMap = new HashMap<>();
private final static Integer expirationTime = 30;
private final static TimeUnit expirationUnit = TimeUnit.MINUTES;
private final static Integer clearThreshold = 30;
private static Integer clearTag = 0;

/**
* 获取code状态,并设置到使用code里面
*
* @param password
* @return false说明密码已经使用过,true则表示密码可以使用
*/

public static boolean getAndSet(Integer password) {
// usedPasswordMap存储的过期密码可能会越来越多,需要定期清理
if (clearTag > clearThreshold) {
if (!usedPasswordMap.isEmpty()) {
clearExpired();
}
clearTag = 0;
}
clearTag++;
Long usedPasswordTimestamp = usedPasswordMap.get(password);
Long currentTimestamp = System.currentTimeMillis();
if (ObjectUtils.isEmpty(usedPasswordTimestamp)) {
usedPasswordMap.put(password, currentTimestamp);
return true;
}
// 到了这里说明密码已经使用过(有效期内,或之前),若使用时间距今在有效期内,说明当期已经使用过,否则是以前使用的
if (expired(currentTimestamp, usedPasswordTimestamp)) {
usedPasswordMap.put(password, currentTimestamp);
System.out.println("密码虽然已使用,但为历史使用,因此当前密码有效");
return true;
}
System.out.println("密码有效期内已使用一次");
return false;
}


/**
* @param current 当前时间
* @param compare 比较时间
* @return 是否过期
*/

private static boolean expired(long current, long compare) {
Instant endInstant = Instant.ofEpochMilli(current);
LocalDateTime end = LocalDateTime.ofInstant(endInstant, ZoneId.systemDefault());
Instant beginInstant = Instant.ofEpochMilli(compare);
LocalDateTime begin = LocalDateTime.ofInstant(beginInstant, ZoneId.systemDefault());

Duration duration = Duration.between(begin, end);
long actualInterval;
switch (PasswordUtils.expirationUnit) {
case SECONDS:
actualInterval = duration.toSeconds();
break;
case MINUTES:
actualInterval = duration.toMinutes();
break;
case HOURS:
actualInterval = duration.toHours();
break;
case DAYS:
actualInterval = duration.toDays();
break;
default:
throw new IllegalArgumentException("输入时间类型不支持");
}
return actualInterval >= (long) PasswordUtils.expirationTime;
}

/**
* 清理过期的密码
*/

private static void clearExpired() {
Iterator<Map.Entry<Integer, Long>> iterator = usedPasswordMap.entrySet().iterator();
Long currentTimestamp = System.currentTimeMillis();
while (iterator.hasNext()) {
Map.Entry<Integer, Long> item = iterator.next();
if (expired(currentTimestamp, item.getValue())) {
iterator.remove();
}
}
}

/**
* 生成一个密码
*
* @return 返回一个六位正整数
*/

public static Integer generate(String serialNumber, String time) {
String toHash = time + serialNumber;
return Math.abs(toHash.hashCode() % 1000000);
}

public static String localDateTimeToStr(LocalDateTime localDateTime) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
return formatter.format(localDateTime);
}

}

最后的最后,这种方法生成的密码有个bug,就是30分钟内生成的30个密码里面会有重复的可能性,不过想来发生概率很低,看后续如何优化了。


作者:持剑的青年
来源:juejin.cn/post/7280459667129188387
收起阅读 »

中秋节,我只想回家😭

web
前言 中秋节马上就要到啦,中秋节作为刻入我们DNA里面的团圆佳节,我想大家关心的肯定是团圆了吧 对于普通人回家跟家人团圆可能也就是一张车票而已 但是今年~我就想知道28 29号的票都被谁买去了,我现在连30号的都没买到!!! 这让我怎么团圆啊!!! 忆中秋 还...
继续阅读 »

前言


中秋节马上就要到啦,中秋节作为刻入我们DNA里面的团圆佳节,我想大家关心的肯定是团圆了吧


对于普通人回家跟家人团圆可能也就是一张车票而已


但是今年~我就想知道28 29号的票都被谁买去了,我现在连30号的都没买到!!!


这让我怎么团圆啊!!!


忆中秋


还记得小时候过中秋,就跟过年一样。记忆中的中秋,各家各户外出的人都会从天南地北赶回来,一家人团聚在一起;记忆中的月亮,又圆又亮,坐在门前的小院子里,一起吃着月饼赏着月,已经很开心了。


小时候
听着嫦娥的故事
心里却惦记着月饼

长大了
手里捧着月饼
心里却想着嫦娥

中秋节到了
愿你重拾童年的快乐
点缀幸福的生活

程序员怎么过中秋呢?


当然是以代码来庆祝一下中秋啦,正好还可以参加下中秋创意大赛!但是很难有一些新奇的创意了,那就做一个猜灯谜的小游戏供大家消遣吧哈哈哈。话不多说,开始今天的主题,制作灯谜小游戏,码上掘金会有源码哦,很基础的~


1、游戏简介



  • 玩家在提交答案后,游戏将根据玩家的回答情况给予相应的提示信息。如果答案正确,将显示回答正确的提示,并增加相应的得分;如果答案错误,将显示回答错误的提示,并扣除相应的分数。同时,游戏会记录玩家的最高分数,以便玩家挑战自己的最好成绩。

  • 玩家可以选择继续猜下一道题目,直到回答完所有题目或不再继续。游戏结束后,将显示玩家的得分和最高分数,并提供重新开始和退出游戏的选项。


2、游戏规则



游戏包括多道灯谜题目,每个题目都有一个对应的答案。


玩家需要在输入框中输入自己的答案,并点击提交按钮进行确认。


如果答案正确,将显示相应的提示信息,表示回答正确;如果答案错误,将显示错误提示信息并扣除相应分数。


游戏根据玩家的回答情况给予评分,并记录最高分数。


玩家可以选择重置游戏重新开始,或者退出游戏。



3、游戏设计



  • 定义题目和答案数组,每个元素包含一个题目和对应的答案。

  • 初始化游戏数据,包括当前题目索引、得分和最高分数。

  • 显示当前题目,将题目显示在页面上供用户查看。

  • 用户输入答案后,点击提交按钮。

  • 检查用户答案是否正确,如果正确则增加得分,显示回答正确的提示;如果错误则显示回答错误的提示。

  • 更新最高分数,如果当前得分超过最高分数,则更新最高分数。

  • 显示当前得分和最高分数。

  • 清空输入框,准备接受下一题答案。

  • 判断是否回答完所有题目,若回答完所有题目则显示游戏结束的提示信息,并禁用提交按钮;若未完成则显示下一题。

  • 提供重新开始游戏的功能,重置游戏数据并重新显示第一题。

  • 提供退出游戏的功能,显示退出游戏的提示信息。


4、功能实现


题目和答案的存储



题目和答案的存储可以使用数组来实现,每个元素表示一道题目和对应的答案。例如:



// 定义题目和答案
const questions = [

{ question: '中秋佳节结良缘 (打一城市名)', answer: '重庆' },
{ question: '中秋鼓励消费 (打一成语)', answer: '月下花前' },
{ question: '中秋遥知兄弟赏光处 (打一唐诗目)', answer: '望月怀远' },
{ question: '木兰迷恋中秋夜 (打一成语)', answer: '花好月圆' },
{ question: '中秋渡蜜月 (打一成语)', answer: '喜出望外' }
];

每个元素都是一个对象,包含两个属性:question表示题目,answer表示答案。可以根据实际需要修改题目和答案的内容和数量。


5、游戏展示


话不多说直接上效果 !


作者:优秀稳妥的Zn
来源:juejin.cn/post/7280747221510733878
收起阅读 »

外甥女问我什么是代码洁癖,我是这么回答的...

1. 引言 哈喽,大家好,我是小 ❤,一个在二进制世界起舞的探险家,幻想有一天可以将代码作诗的后台开发。 今天,我要和大家聊聊程序员的神秘技能——重构!别担心,我会用通俗易懂的语言和一些趣味对话来帮助你理解和掌握这个技能,我 8 岁的外甥女听了都说懂。 1.1...
继续阅读 »

1. 引言


哈喽,大家好,我是小 ❤,一个在二进制世界起舞的探险家,幻想有一天可以将代码作诗的后台开发。


今天,我要和大家聊聊程序员的神秘技能——重构!别担心,我会用通俗易懂的语言和一些趣味对话来帮助你理解和掌握这个技能,我 8 岁的外甥女听了都说懂。


1.1 背景


代码开发:



一个月后:



后面有时间了改一改吧(放心,不会有时间的,有时间了也不会改)。


六个月后:



如上,是任何一个开发者都会经历的场景:早期的代码根本不能回顾,不然一定会陷入深深的怀疑,这么烂的代码真是出自自己的手吗?


更何况,目前大部分系统都是协同开发,每个程序员的命名规范、编码习惯都不尽相同,就导致了一个系统代码,多个味道的情况。


重构是什么


妍妍:嘿,舅舅,听说你要分享重构,这又是什么新鲜事?



❤:嗨,妍妍!重构就是改进既有代码的设计,让它更好懂、更容易维护,而不改变它的功能。想象一下,它就像是给代码来了个变美的化妆术,但内在还是那个代码,不会变成"不认识的人"。


为什么要重构


露露:哇,听起来好厉害,那为什么我们要重构呢?



❤:哈哈,好问题,露露!因为代码是活的,一天天在变大,当代码变得难以理解、难以修改时,它就像是一头头重的大象,拖慢了我们前进的步伐。重构就像是给大象减肥,使它更轻盈、更灵活,开发速度也能提升不少!


这和你们有小洁癖,爱收拾房间一样,有代码洁癖的程序员也会经常重构 Ta 们的代码呢!


什么时候要重构


妍妍:听起来有道理,但什么时候才应该使用重构呢?



❤:好问题,妍妍!有以下几种情况:




  • 当你看到代码中有好几处长得一模一样的代码,这时候可以考虑把它们合并成一个,减少冗余。




  • 当你的函数或方法看上去比词典还厚重时,可以把它拆成一些小的部分,更好地理解。




  • 当你要修复一个 bug,但却发现原来的代码结构太复杂,修复变得像解迷一样难时,先重构再修复就是个好主意。




  • 当你要添加新功能,但代码不让你轻松扩展时,也可以先重构,然后再扩展。




重构的步骤


露露:明白了舅舅,那重构的具体步骤是什么呢?



❤:问得好,露露,看来你有认真在思考!接下来让我给你介绍一下重构的基本步骤吧!


2. 如何重构


重构之前,我们需要识别出代码里面的坏味道代码。


所谓坏味道,就是指代码的表面的混乱,和深层次的腐化现象。简单来说,就是感觉不太对劲的代码。


2.1 坏味道代码



在《重构-改善既有代码的设计》一书中,讲述了这二十多种坏味道情况,我们下面将挑选最常见的几种来介绍。


1)方法过长


方法过长是指在一个方法里面做了太多的工作,常常伴随着方法中的语句不在同一个抽象层级,比如 dto 和 service 层代码混合在一起,即逻辑分散。


除此之外,方法过长还容易带来一些额外的问题。


问题1:过多的注释


方法太长会导致逻辑难以理解,需要大量的注释,如果 10 行代码需要 20 行注释,代码很难阅读。特别是读代码的时候,常常需要记住大量的上下文。


问题2:面向过程


面向过程的问题在于当逻辑复杂以后,代码会很难维护。


相反地,我们在代码开发时常常用面向对象的设计思想,即把事物抽象成具有共同特征的对象。


解决思路


解决方法过长时,我们遵循这样一条原则:每当感觉要写注释来说明代码时,就把这部分代码写进一个独立的方法里,并根据这段代码的意图来命名。



方法命名原则:可以概括要做的事,而非怎么做。



2)过大的类


一个类做了太多的事情,比如一个类的实现既包含商品逻辑,又包含订单逻辑。在创建时就会出现太多的实例变量和方法,难以管理。


除此之外,过大的类还容易带来两个问题。


问题1:冗余重复


当一个类里面包含两个模块的逻辑时,两个模块容易产生依赖。这在代码编写的过程中,很容易发生 “你带着我,我看着你” 的问题。


即在两个模块中,都看到了和另一个模块相关的程序结构或相同意图的方法。


问题2:耦合结构不良


当类的命名不足以描述所做的事情时,大概率产生了耦合结构不良的问题,这和我们想要编写 “高内聚,低耦合” 的代码目标相悖而行了。


解决思路


将大类根据业务逻辑拆分成小类,如果两个类之间有依赖,则通过外键等方式关联。当出现重复代码时,尽量合并提出来,程序会变得更简洁可维护。


3)逻辑分散


逻辑分散是由于代码架构层次或者对象层次上有不合理的依赖,通常会导致两个问题:


发散式变化


某个类经常因为不同的原因,在不同的方向上修改。


散弹式修改


发生某种变化时,需要多个类中修改。


4)其它坏味道


数据泥团


数据泥团是指很多数据项混乱地融合在一起,不易复用和扩展。


当许多数据项总是一起出现,并且一起出现时更容易分类。我们就可以考虑将数据按业务封装成数据对象。反例如下:


func AddUser(age int, gender, firstName, lastName string) {}

重构之后:


type AddUserRequest struct {
   Age int
   Gender string
   FirstName string
   LastName string
}
func AddUser(req AddUserRequest) {}

基本类型偏执


在大多数高级编程语言里面,都有基本类型和结构类型。在 Go 语言里面,基本类型就是 int、string、bool 等。


基本类型偏执是指我们在定义对象的变量时,常常不考虑变量的实际业务含义,直接使用基本类型。


反例如下:


type QueryMessage struct {
Role        int         `json:"role"`
Content  string    `json:"content"`
}

重构之后:


// 定义对话角色类型
type MessageRole int

const (
HUMAN     MessageRole = 0
ASSISTANT MessageRole = 1
)

type QueryMessage struct {
Role        MessageRole   `json:"role"`
Content  string               `json:"content"`
}

这是 ChatGPT 问答时的请求字段,我们可以看到对话角色为 int 类型,且 0 表示人类,1 表示聊天助手。


当直接使用 int 来表示对话 Role 时,没办法直接从定义里知道更多信息。


但是用 type MessageRole int 定义后,我们就可以根据常量值很清晰地看出对话角色分为两种:HUMAN & ASSISTANT.


混乱的代码层次调用


我们一般的系统都会根据业务 service、中转控制 controller 和数据库访问 dao 等进行分层。一般 controller 调用 service,service 调用 dao。


如果我们在 controller 直接调用 dao,或者 dao 调用 controller,就会出现层次混乱的问题,就可以进行优化了。


5)坏味道带来的问题


妍妍:舅舅,这些坏味道都需要解决吗,你说的这些坏味道代码会带来什么样的影响呢?


❤:是的,代码里如果坏味道代码太多,会带来四个 “难以”



  • 难以理解:新来的开发同学压根看不懂看人的代码,一个模块看了两个周还不知道啥意思。或许不是开发者的水平不够,可能是代码写的太一言难尽。



  • 难以复用:要么是读都读不懂,或者勉强读懂了却不敢用,担心有什么暗坑。或者系统耦合性严重,难以分离可重用部分。



  • 难以变化:牵一发而动全身,即散弹式修改。动了一处代码,整个模块都快没了。




  • 难以测试:改了不好测,难以进行功能验证。命名杂乱,结构混乱,在测试时可能测出新的问题。




3. 重构技巧


露露:哦,原来是这样啊,那我们可以去除它们吗?


❤:当然可以了!就像你们爱收拾房间一样,每一个有责任心(代码洁癖)的程序员,都会考虑代码重构。


而对于重构问题,业界已经有比较好的思路:通过持续不断地重构将代码中的 "坏味道" 清除掉。


1)命名规范


一个好的命名规范应该符合:



  • 精准描述所做的事情

  • 格式符合通用惯例


约定俗成的惯例


我们拿华为公司内部的 Go 语言的开发规范来举例:


场景约束示例
项目名全部小写,多个单词时用中划线 '-' 分隔user-order
包名全部小写,多个单词时用中划线 '-' 分隔config-sit
结构体名首字母大写Student
接口采用 Restful API 的命名方式,路径最后一部分是资源名词如 [get] api/v1/student
常量名首字母大写,驼峰命名CacheExpiredTime
变量名首字母小写,驼峰命名userName,password

2)重构手法


妍妍:哇,这么多成熟的规范可以用啊!那除了规范,我们还需要注意什么吗?


❤:好问题妍妍!接下来我还会介绍一些常见的重构手法:




  • 提取函数:将一个长长的函数分成小块,更容易理解和复用。




  • 改名字:给变量、函数、类等改个名字,更有意义。




  • 消除冗余:找到相似的代码块,合并它们,减少重复。




  • 搬家:把函数或字段移到更合适的地方,让代码更井然有序。




  • 抽象通用类:把通用功能抽出来,变成一个类,增加代码的可重用性。




  • 引入参数对象:当变量过多时,传入对象,消除数据泥团。




  • 使用卫语句:减少 else 的使用,让代码结构更加清晰。




4. 小结


露露:舅舅,你讲得太有趣了,我感觉我也会重构了!


❤:露露真棒,我相信你!重构的思想无处不在,就像生活中都应该留白一样,你们的人生也会非常精彩的。在编程里,重构可以让代码更美观、更容易读懂,提高开发效率,是程序员都应该掌握的技能。


妍妍:我也会了,我也会了!以后我也要写代码,做代码重构,我还要给舅舅的文章点赞。



❤:哈哈哈,好哒,你们都很棒!就像你们喜欢打扫卫生,爱好画画读诗一样,如果以后你们想写代码,它们也会十分的干净整洁,充满诗情画意。



最后,如果你觉得有所收获,别忘了点赞和在看,让更多的人了解重构的神奇之处,一起进步,一起写出更好的代码!


希望这篇文章对你有所帮助,也希望你能在编程的路上越走越远。感谢大家的支持,我们下次再见!🚀✨


最后


妍妍说:看完的你还不赶紧分享、点赞、加入在看吗?



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

前端监控究竟有多重要?

web
为什么要有前端监控? 一个很现实的原因是bug是不可能被全部测试出来的,由于成本和上线档期的考虑,测试无法做到“面面俱到”,即使时间充裕也总会有这样或那样的bug埋藏在某个角落。 所以一个可靠的前端监控系统可以帮助我们化被动为主动,不再被动的等待客服来找,而是...
继续阅读 »

为什么要有前端监控?


一个很现实的原因是bug是不可能被全部测试出来的,由于成本和上线档期的考虑,测试无法做到“面面俱到”,即使时间充裕也总会有这样或那样的bug埋藏在某个角落。


所以一个可靠的前端监控系统可以帮助我们化被动为主动,不再被动的等待客服来找,而是在问题出现时开发人员可以第一时间知道并解决。并且我们还可以通过监控系统获取用户行为以及跟踪产品在用户端的使用情况,并以监控数据为基础,指明产品优化的方向。


常见的前端监控


前端监控系统大体可以分为四部分



  • 异常监控

  • 用户数据监控

  • 性能监控

  • 异常报警


用户数据监控



数据监控,就是监听用户的行为,可以帮助我们评估和改进用户在使用网站时的体验:




  • PV:PV(page view):即用户访问特定页面的次数,也可以说是页面的浏览量或点击量,

  • UV:访问网站的不同个体或设备数量,而不是页面访问次数

  • 新独立访客:当日的独立访客中,历史上首次访问网站的访客为新独立访客。

  • 跳出次数:跳出指仅浏览了1个页面就离开网站的访问(会话)行为。跳出次数越多则访客对网站兴趣越低或站内入口质量越差。

  • 来访次数:由该来源进入网站的访问(会话)次数。

  • 用户在每一个页面的停留时间

  • 用户通过什么入口来访问该网页

  • 用户在相应的页面中触发的行为

  • 网站的转化率

  • 导航路径分析


统计这些数据是有意义的,我们可以清晰展示前端性能的表现,并依据这些监控结果来进一步优化前端性能。例如,我们可以改善动画效果以在低版本浏览器上兼容,或者采取措施加快首屏加载时间等。这些优化措施不仅可以提高转化率,因为快速加载的网站通常具有更高的转化率,还可以确保我们的网站在多种设备和浏览器上都表现一致,以满足不同用户的需求。最终达到,改善用户体验,提供更快的页面加载时间和更高的性能,增强用户满意度,降低跳出率的目的。


性能监控



性能监控是一种用于追踪和评估网站和性能的方法。它专注于用户在浏览器中与网站互时的性能体验




  • 首次绘制(FP): 全称 First Paint,标记浏览器渲染任何在视觉上不同于导航前屏幕内容之内容的时间点

  • 首次内容绘制(FCP):全称 First Contentful Paint,标记的是浏览器渲染来自 DOM 第一位内容的时间点,该内容可能是文本、图像、SVG 甚至 <canvas> 元素。

  • 首次有效绘制(FMP):全称 First Meaningful Paint,标记的是页面主要内容绘制的时间点,例如视频应用的视频组件、天气应用的天气信息、新闻应用中的新闻条目。

  • 最大内容绘制(LCP):全称 Largest Contentful Paint,标记在可视区“内容”最大的可见元素开始绘制在屏幕上的时间点。

  • 白屏时间

  • http 等请求的响应时间

  • 静态资源整体下载时间

  • 页面渲染时间

  • 页面交互动画完成时间


异常监控



由于产品的前端代码在客户端的执行过程中也会发生异常,因此需要引入异常监控。及时的上报异常情况,这样可以避免线上故障的发生。虽然大部分异常可以通过 try catch 的方式捕获,但是比如内存泄漏以及其他偶现的异常难以捕获。



常见的需要监控的异常包括:



  • Javascript 的异常监控:捕获并报告JavaScript代码中的错误,如未定义的变量、空指针引用、语法错误等

  • 数据请求异常监控:监控Ajax请求和其他网络请求,以便识别网络问题、服务器错误和超时等。

  • 资源加载错误:捕获CSS、JavaScript、图像和其他资源加载失败的情况,以减少页面加载问题。

  • 跨域问题:识别跨域请求导致的问题,如CORS(跨源资源共享)错误。

  • 用户界面问题:监控用户界面交互时的错误,如用户界面组件的不正常行为或交互问题


通过捕获和报告异常,开发团队可以快速响应问题,提供更好的用户体验,减少客户端问题对业务的不利影响


异常报警



前端异常报警是指在网站中检测和捕获异常、错误以及问题,并通过各种通知方式通知开发人员或团队,以便他们能够快速诊断、分析和解决问题。



常见的异常报警方式




  • 邮件通知:通过邮件将异常信息发送给相关人员,通常用于低优先级的问题。




  • 短信或电话通知:通过短信或电话自动通知相关人员,通常用于紧急问题或需要立即处理的问题。




  • 即时消息:使用即时通讯工具如企业微信 飞书或钉钉发送异常通知,以便团队及时协作。




  • 日志和事件记录:将异常信息记录到中央日志,或者监控中台系统,以供后续分析和审计。




报警级别和策略:


异常报警通常有不同的级别和策略,根据问题的紧急性和重要性来确定通知的方式和频率。例如,可以定义以下报警级别:




  • 紧急报警:用于严重的问题,需要立即处理,通常通过短信或电话通知。




  • 警告报警:用于中等级别的问题,需要在短时间内处理,可以通过即时消息或邮件通知。




  • 信息报警:用于一般信息和低优先级问题,通过邮件或即时消息通知。




  • 静默报警:用于临时性问题或不需要立即处理的问题,可以记录到日志而不发送通知。




异常报警是确保系统稳定性和可用性的重要机制。它能够帮助组织及时发现和解决问题,减少停机时间,提高系统的可靠性和性能,从而支持业务运营。异常报警有助于快速识别和响应问题,减少停机时间,提高系统的可用性和性能


介绍完了前端监控的四大部分,现在就来聊聊前端监控常见的几种监控方式。


SDK设计(埋点方案)


前端埋点是一种用于收集和监控网站数据的常见方法


image.png


手动埋点:


手动埋点也称为代码埋点,是通过手动在代码中插入埋点代码(SDK 的函数)的方式来实现数据收集。像腾讯分析(Tencent Analytics)、百度统计(Baidu Tongji)、诸葛IO(ZhugeIO)等第三方数据统计服务商大都采用这种方案,这种方法的优点是:



  • 灵活:开发人员可以根据需要自定义属性和事件,以捕获特定的用户行为和数据。

  • 精确:可以精确控制埋点位置,以确保收集到关键数据。


然而,手动埋点的缺点包括:



  • 工作量大:需要在代码中多次插入埋点代码,工程量较大。

  • 沟通成本高:需要开发、产品和运营之间的频繁沟通,容易导致误差和延迟。

  • 更新迭代成本高:每次有埋点更新或漏埋点都需要重新发布应用程序,成本较高。


可视化埋点:


可视化埋点通过提供可视化界面,允许用户在不编写代码的情况下进行添加埋点。这种方法的优点是:



  • 简单方便:非技术人员也可以使用可视化工具添加埋点,减少了对技术团队的依赖。

  • 实时更新:可以实时更新埋点配置,无需重新上传网站。


然而,可视化埋点的缺点包括:



  • 可定制性受限:可视化工具通常只支持有限的埋点事件和属性,无法满足所有需求。

  • 对控件有限制:可视化埋点通常只适用于特定的UI控件和事件类型。


无埋点:


无埋点是一种自动收集所有用户行为和事件的方法,然后通过后端过滤和分析以提取有用的数据。这种方法的优点是:



  • 全自动:无需手动埋点,数据自动收集,降低了工程量,而且不会出现漏埋和误埋等现象。

  • 全面性:捕获了所有用户行为,提供了完整的数据集。


然而,无埋点的缺点包括:



  • 数据量大:数据量庞大,需要后端过滤和处理,可能增加服务器性能压力。

  • 数据处理复杂:需要处理大量原始数据,提取有用的信息可能需要复杂的算法和逻辑。


作者:zayyo
来源:juejin.cn/post/7280430881964638262
收起阅读 »

闲来无事,拜拜电子财神

web
最近在刷抖音的时候,经常能刷到类似下面这种手机桌面,通过手机小组件功能,搭了一个电子供台。。。    由于最近闲来无事儿,就在想可不可以制作一个类似的网页,功能点有以下这些: 1.类似手机小组件一样的布局 2.点击木鱼一次,可以显示功德加一并且带音效 3.随着...
继续阅读 »

最近在刷抖音的时候,经常能刷到类似下面这种手机桌面,通过手机小组件功能,搭了一个电子供台。。。


  


由于最近闲来无事儿,就在想可不可以制作一个类似的网页,功能点有以下这些:


1.类似手机小组件一样的布局


2.点击木鱼一次,可以显示功德加一并且带音效


3.随着功德点击,香炉上方会有烟雾飘散的效果


4.统计不同省份的功德数据


5.心愿墙功能,


于是说干就干,就开始了开发工作;


经过了 2 个下午的忙碌,完成了前三个功能,有了大概的雏形,就是下面这个样子



开发的过程中也遇到了一些问题


1.在手机上连续点击木鱼时,会导致网页放大


在网上找了一些解决办法,设置 meta 属性


无效,在 ios 的浏览器上没有效果


这个方法类似于写个节流函数,不过这样做就没有连续敲击木鱼的快感了,所以也不行。


最后让我找到了一个插件 fastClick.js,完美解决了问题。只要正常引入,然后加入以下代码即可。


if ("addEventListener" in document) {            document.addEventListener(                "DOMContentLoaded",                function () {                    FastClick.attach(document.body);                },                false            );        }

2.播放木鱼音效延迟问题


通过document.createElement('audio')方式创建 audio 组件,代码如下


var audio = document.createElement('audio') //生成一个audio元素
audio.controls = true //这样控件才能显示出来
audio.src = 'xxxxx' //音乐的路径
document.body.appendChild(audio) //把它添加到页面中
audio.play()

声音是能播放出来了,但是延迟很高,点一下木鱼,过几秒钟后才有音效,所以这个方式 pass 了。还有说可以通过AudioContext API 来播放音效,但是看了一下,感觉写起来有些复杂,也 pass 掉了,最后也是找到了一款合适的插件解决了这个问题。



使用方式也是异常简单


var sound = new Howl({
src: ['sound.mp3']
});

sound.play();

由于有个功能是敲击木鱼后,页面香炉的位置会生成烟雾,自己不太会写,于是又找到了可以一个模拟烟雾的插件,可以在页面任意位置生成烟雾动画


使用时先创建一个 canvas 标签


<canvas id="smoke"></canvas>

然后初始化


let canvas = document.getElementById("smoke");let ctx = canvas.getContext("2d");canvas.width = window.innerWidth;canvas.height = window.innerHeight;party = SmokeMachine(ctx, [230, 230, 230]); // 数组里是颜色 rgb 值

点击木鱼一次,创建一次播放动画


party.start();party.addSmoke(    window.innerWidth / 2,    //烟雾生成的位置,x    window.innerHeight * 0.4, //烟雾生成的位置,y    10 //烟雾大小);

至此烟雾效果就完美实现了。


体验url:财神爷.我爱你


没错,是纯中文域名,中国的神仙就要用中文域名。


未完待续......


作者:yibeicha
来源:juejin.cn/post/7280435142245285946
收起阅读 »

前端又出新框架了,你还学得动吗?

web
最近前端又出来一个新框架/库,名为nue.js。一周前的9.13号提交了第一个commit,到今天已超过2000个star。 翻译一下: Nue 是一个强大的 React、Vue、Next.js、Vite 和 Astro 替代品。它可能会改变您的web开发...
继续阅读 »

最近前端又出来一个新框架/库,名为nue.js。一周前的9.13号提交了第一个commit,到今天已超过2000个star。


官网首页截图


翻译一下:



Nue 是一个强大的 React、Vue、Next.js、Vite 和 Astro 替代品。它可能会改变您的web开发方式。



What is Nue JS?


Nue JS 是一个非常小的(压缩后 2.3kb)JavaScript 库,用于构建 Web 界面。 它是即将推出的 Nue 生态系统的核心。 它就像 Vue.js、React.js 或 Svelte,但没有hooks, effects, props, portals, watchers, provides, injects, suspension 这些抽象概念。了解 HTML、CSS 和 JavaScript 的基础知识,就可以开始了。


用更少的代码构建用户界面


它表示,Nue 最大的好处是你需要更少的代码来完成同样的事情:


同样一个listBox组件,react需要2537行,vue需要1913行,svelte需要1286行,Nue只需要208行,比react小10倍。





仅仅是HTML


Nue 使用基于 HTML 的模板语法:


<div @name="media-object" class="{ type }">
<img src="{ img }">
<aside>
<h3>{ title }</h3>
<p :if="desc">{ desc }</p>
<slot/>
</aside>
</div>

React 和 JSX 声称是“Just JavaScript”,但 Nue 可以被认为是“Just HTML”


按比例构建


Nue 具有出色扩展性的三个原因:



  1. 关注点分离,易于理解的代码比“意大利面条代码”更容易扩展

  2. 极简主义,一百行代码比一千行代码更容易扩展

  3. 人才分离,当 UX 开发人员专注于前端,而 JS/TS 开发人员专注于前端后端时,团队技能就会达到最佳平衡:



解耦样式


Nue不提倡使用 Scoped CSS、样式属性、Tailwind 或其他 CSS-in-JS 体操:



  1. 更多可重用代码:当样式未硬编码到组件时,同一组件可能会根据页面或上下文而看起来有所不同。

  2. 没有意大利面条式代码:纯 HTML 或纯 CSS 比混合意大利面条式代码更容易阅读

  3. 更快的页面加载:通过解耦样式,可以更轻松地从辅助 CSS 中提取主 CSS,并将 HTML 页面保持在关键的14kb 限制以下。


反应式和同构


Nue拥有丰富的组件模型,它允许您使用不同类型的组件创建各种应用程序:



  1. 服务器组件在服务器上呈现。它们可以帮助您构建以内容为中心的网站,无需 JavaScript 即可加载速度更快,并且可以被搜索引擎抓取。

  2. 反应式组件在客户端上呈现。它们帮助您构建动态岛或单页应用程序。

  3. 混合组件部分在服务器端呈现,部分在客户端呈现。这些组件可帮助您构建响应式、SEO 友好的组件,例如视频标签或图片库。

  4. 通用组件在服务器端和客户端上使用相同的方式。


UI库文件


Nue允许您在单个文件上定义多个组件。这是将相关组件组合在一起并简化依赖关系管理的好方法。


<!-- shared variables and methods -->
<script>
import { someMethod } from './util.js'
</script>

<!-- first component -->
<article @name="todo">
...
</article>

<!-- second component -->
<div @name="todo-item">
...
</div>

<!-- third component -->
<time @name="cute-date">
...
</time>

使用库文件,您的文件系统层次结构看起来更干净,并且您需要更少的样板代码将连接的部分连接在一起。他们帮助为其他人打包库。


更简单的工具


Nue JS带有一个简单的render服务器端渲染功能和一个compile为浏览器生成组件的功能。不需要 WebpackVite 等复杂的捆绑程序来控制您的开发环境。只需将 Nue 导入到项目中即可。


如果应用程序因大量依赖项而变得更加复杂,可以在业务模型上使用打包器。Bunesbuild是很棒的高性能选择。


用例


Nue JS是一款多功能工具,支持服务器端和客户端渲染,可帮助您构建以内容为中心的网站和反应式单页应用程序。



  1. UI 库开发:为反应式前端或服务器生成的内容创建可重用组件。

  2. 渐进式增强:Nue JS 是一个完美的微型库,可通过动态组件或“岛”增强以内容为中心的网站

  3. 静态网站生成器:只需将其导入您的项目即可准备渲染。不需要捆绑器。

  4. 单页应用程序:与即将推出的Nue MVC项目一起构建更简单、更具可扩展性的应用程序。

  5. Template Nue:是一个用于生成网站和 HTML 电子邮件的通用工具。


本文参考资料



作者:xintianyou
来源:juejin.cn/post/7280747833371705405
收起阅读 »

iOS小技能:Xcode13的使用技巧

iOS
引言 Xcode13新建项目不显示Products目录的解决方案Xcode13新建的工程恢复从前的Info.plist同步机制的方法自动管理签名证书时拉取更新设备描述文件的方法。 I 显示Products目录的解决方案 问题:Xcode13 新建的项目不显示P...
继续阅读 »

引言


  1. Xcode13新建项目不显示Products目录的解决方案
  2. Xcode13新建的工程恢复从前的Info.plist同步机制的方法
  3. 自动管理签名证书时拉取更新设备描述文件的方法。

I 显示Products目录的解决方案


问题:Xcode13 新建的项目不显示Products目录


解决方式: 修改project.pbxproj 文件的productRefGroup配置信息


效果:

应用场景:Products目录的app包用于快速打测试包。


1.1 从Xcodeeproj 打开project.pbxproj



1.2 修改productRefGroup 的值


将mainGroup 对应的值复制给productRefGroup 的值,按command+s保存project.pbxproj文件,Xcode将自动刷新,Products目录显示出来了。



1.3 应用场景


通过Products目录快速定位获取真机调试包路径,使用脚本快速打包。


打包脚本核心逻辑:在含有真机包路径下拷贝.app 到新建的Payload目录,zip压缩Payload目录并根据当前时间来命名为xxx.ipa。

#!/bin/bash
echo "==================(create ipa file...)=================="
# cd `dirname $0`;
rm -rf ./Target.ipa;
rm -rf ./Payload;
mkdir Payload;
APP=$(find . -type d | grep ".app$" | head -n 1)
cp -rf "$APP" ./Payload;
data="`date +%F-%T-%N`"
postName="$data"-".ipa"
zip -r -q "$postName" ./Payload;
rm -rf ./Payload;
open .
# 移动ipa包到特定目录
mkdir -p ~/Downloads/knPayload
cp -a "$postName" ~/Downloads/knPayload
open ~/Downloads/knPayload
echo "==================(done)=================="
exit;






II 关闭打包合并Info.plist功能


Xcode13之前Custom iOS Target Properties面板和Info.plist的配置信息会自动同步。


Xcode13新建的工程默认开启打包合并Info.plist功能,不再使用配置文件(Info.plist、entitlements),如果需要修改配置,直接在Xcode面板target - Info - Custom iOS Target Propertiesbuild settings中设置。




Projects created from several templates no longer require configuration files such as entitlements and Info.plist files. Configure common fields in the target’s Info tab, and build settings in the project editor.



2.1 设置Info.plist为主配置文件


由于GUI配置面板没有配置文件plist的灵活,不支持查看源代码。所以我们可以在BuildSetting Generate Info.plist File设置为NO,来关闭打包合并功能。



关闭打包合并功能,重启Xcode使配置生效,Custom iOS Target Properties面板的信息以info.plist的内容为准。



每次修改info.plist都要重启Xcode,info.plist的信息才会同步到Custom iOS Target Properties面板。 



2.2 注意事项


注意: 关闭打包合并Info.plist功能 之前记得先手动同步Custom iOS Target Properties面板的信息到Info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>iOS逆向</string>
<key>CFBundleIdentifier</key>
<string>blog.csdn.net.z929118967</string>
<key>CFBundleName</key>
<string>YourAppName</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchScreen</key>
<dict>
<key>UILaunchScreen</key>
<dict/>
</dict>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~iphone</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>


III 自动管理签名证书时如何拉取最新设备描述文件?


方法:根据描述文件的创建时间来删除旧的自动管理证书的描述文件



 



原理:在~/Library/MobileDevice/Provisioning\ Profiles 文件夹中删除之前的描述文件,然后系统检测到没有描述文件则会自动生成一个新的


see also


iOS第三方库管理规范:以Cocoapods为案例



kunnan.blog.csdn.net/article/det…



iOS接入腾讯优量汇开屏广告教程



kunnan.blog.csdn.net/article/det…


作者:公众号iOS逆向
链接:https://juejin.cn/post/7137938695616741407
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

开发没切图怎么办?矢量图标(iconFont)上手指南

iOS
需求: 有时候我们自己想独立开发一些App,但苦恼没有设计给icon切图? 这可怎么办? 今天我们来介绍一种比较高效且高质量的替代方案:使用矢量图标 —— iconFont。 一、iconFont简介 iconFont:是阿里巴巴提供的一个矢量图标库。简单...
继续阅读 »

需求:

有时候我们自己想独立开发一些App,但苦恼没有设计给icon切图?

这可怎么办?

今天我们来介绍一种比较高效且高质量的替代方案:使用矢量图标 —— iconFont



一、iconFont简介



iconFont:是阿里巴巴提供的一个矢量图标库。简单来说,就是可以把icon转换成font,再通过文本展示出来。官网链接

支持:WebiOSAndroid平台使用。



二、iOS端简单使用指南


第一步:


登录iconFont,挑选你需要的icon,并把它们加入购物车,下载代码。

  • 挑选统一风格的icon

    • 全局搜索想要的icon

    • 将需要使用的icon加入到购物车

    • 下载代码




第二步:


解压下载的压缩包,注意demo_index.htmliconFont.ttf文件。打开工程将ttf导入到项目中,并在info.plist中配置。


  • 压缩文件,找到demo_index.htmliconFont.ttf



  • iconFont.ttf文件导入项目:



第三步:


打开demo_index.html预览iconFont所对应的Unicode编码。并在项目中应用。


  • 打开demo_index.html文件


  • swift使用方法如下,用格式\u{编码}使用Unicode编码
//...
label.font = UIFont.init(name: "iconFont", size: 26.0)
label.text = "\u{e658}"
//...

  • Objective-C使用方法如下,用格式\U0000编码使用Unicode编码
//...
label.font = [UIFont fontWithName:@"uxIconFont" size: 34];;
label.text = @"\U0000e658";
//...

这样,在没有设计提供切图的情况下,就可以用LabeliconFont字体代替切图达成ImageView的效果了。


三、iconFont原理


先把icon通过像素点描述成自定义字体(svg格式字体),然后打包成ttf格式的文件,再通过对应的unicode对应到相关的icon


四、可能遇到的一些问题


  • ttf文件导入冲突问题:

由于从iconFont上打包生成的ttf文件,字体名均为“iconFont”,因此从官网上下载的ttf文件,字体名均为“iconFont”。因此多ttf文件引入时,会有冲突。


解决方案:用一些工具修改字体名,再导入多个ttf文件。(记得在info.plist文件里配置)


  • Unicode变化问题:

尽量使用一个账号下载ttf资源,不同的环境下可能会导致生成的Unicode不同。从而给项目替换icon带来成本。


  • 版权问题:

iconFont目前应该不支持商用,除非有特别的许可。
自己独立写一些小项目的时候可以使用。


作者:齐舞647
链接:https://juejin.cn/post/7254107670012543013
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

全方位对比 Postgres 和 MySQL (2023 版)

根据 2023 年 Stack Overflow 调研,Postgres 已经取代 MySQL 成为最受敬仰和渴望的数据库。 随着 Postgres 的发展势头愈发强劲,在 Postgres 和 MySQL 之间做选择变得更难了。 如果看安装数量,MySQL...
继续阅读 »

根据 2023 年 Stack Overflow 调研,Postgres 已经取代 MySQL 成为最受敬仰和渴望的数据库。




随着 Postgres 的发展势头愈发强劲,在 Postgres 和 MySQL 之间做选择变得更难了。


如果看安装数量,MySQL 可能仍是全球最大的开源数据库。




Postgres 则自诩为全球最先进的开源关系型数据库。




因为需要与各种数据库及其衍生产品集成,Bytebase 和各种数据库密切合作,而托管 MySQL 和 Postgres 最大的云服务之一 Google Cloud SQL 也是 Bytebase 创始人的杰作之一。


我们对 Postgres 和 MySQL 在以下几个维度进行了比较:


  • 许可证 License
  • 性能 Performance
  • 功能 Features
  • 可扩展性 Extensibility
  • 易用性 Usability
  • 连接模型 Connection Model
  • 生态 Ecosystem
  • 可运维性 Operability



除非另有说明,下文基于最新的主要版本 Postgres 15 和 MySQL 8.0 (使用 InnoDB)。在文章中,我们使用 Postgres 而不是 PostgreSQL,尽管 PostgreSQL 才是官方名称,但被认为是一个错误的决定




许可证 License


  • MySQL 社区版采用 GPL 许可证。
  • Postgres 发布在 PostgreSQL 许可下,是一种类似于 BSD 或 MIT 的自由开源许可。

即便 MySQL 采用了 GPL,仍有人担心 MySQL 归 Oracle 所有,这也是为什么 MariaDB 从 MySQL 分叉出来。


性能 Performance


对于大多数工作负载来说,Postgres 和 MySQL 的性能相当,最多只有 30% 的差异。无论选择哪个数据库,如果查询缺少索引,则可能导致 x10 ~ x1000 的降级。
话虽如此,在极端的写入密集型工作负载方面,MySQL 确实比 Postgres 更具优势。可以参考下文了解更多:



除非你的业务达到了 Uber 的规模,否则纯粹的数据库性能不是决定因素。像 Instagram, Notion 这样的公司也能够在超大规模下使用 Postgres。


功能 Features


对象层次结构


MySQL 采用了 4 级结构:


  1. 实例
  2. 数据库

Postgres 采用了 5 级结构:


  • 实例(也称为集群)
  • 数据库
  • 模式 Schema

ACID 事务


两个数据库都支持 ACID 事务,Postgres 提供更强大的事务支持。




安全性


Postgres 和 MySQL 都支持 RBAC。


Postgres 支持开箱即用的附加行级安全 (RLS),而 MySQL 需要创建额外的视图来模拟此行为。


查询优化器


Postgres 的查询优化器更优秀,详情参考此吐槽


复制


Postgres 的标准复制使用 WAL 进行物理复制。MySQL 的标准复制使用 binlog 进行逻辑复制。


Postgres 也支持通过其发布/订阅模式进行逻辑复制。


JSON


Postgres 和 MySQL 都支持 JSON。 Postgres 支持的功能更多:


  • 更多操作符来访问 JSON 功能。
  • 允许在 JSON 字段上创建索引。

CTE (Common Table Expression)


Postgres 对 CTE 的支持更全面:


  • 在 CTE 内进行 SELECT, UPDATE, INSERT, DELETE 操作
  • 在 CTE 之后进行 SELECT, UPDATE, INSERT, DELETE 操作

MySQL 支持:


  • 在 CTE 内进行 SELECT 操作
  • 在 CTE 之后进行 SELECT, UPDATE, DELETE 操作

窗口函数 (Window Functions)


窗口帧类型:MySQL 仅支持 Row Frame 类型,允许定义由固定数量行组成的帧;而 Postgres 同时支持 Row Frame 和范围帧类型。


范围单位:MySQL 仅支持 UNBOUNDED PRECEDING 和 CURRENT ROW 这两种范围单位;而 Postgres 支持更多范围单位,包括 UNBOUNDED FOLLOWING 和 BETWEEN 等。


性能:一般来说,Postgres 实现的 Window Functions 比 MySQL 实现更高效且性能更好。


高级函数:Postgres 还支持更多高级 Window Functions,例如 LAG(), LEAD(), FIRST_VALUE(), and LAST_VALUE()。


可扩展性 Extensibility


Postgres 支持多种扩展。最出色的是 PostGIS,它为 Postgres 带来了地理空间能力。此外,还有 Foreign Data Wrapper (FDW),支持查询其他数据系统,pg_stat_statements 用于跟踪规划和执行统计信息,pgvector 用于进行 AI 应用的向量搜索。


MySQL 具有可插拔的存储引擎架构,并诞生了 InnoDB。但如今,在 MySQL 中,InnoDB 已成为主导存储引擎,因此可插拔架构只作为 API 边界使用,而不是用于扩展目的。


在认证方面,Postgres 和 MySQL 都支持可插拔认证模块 (PAM)。


易用性 Usability


Postgres 更加严格,而 MySQL 更加宽容:


  • MySQL 允许在使用 GROUP BY 子句的 SELECT 语句中包含非聚合列;而 Postgres 则不允许。
  • MySQL 默认情况下是大小写不敏感的;而 Postgres 默认情况下是大小写敏感的。
  • MySQL 允许 JOIN 来自不同数据库的表;而 Postgres 只能连接单个数据库内部的表,除非使用 FDW 扩展。

连接模型 Connection Model


Postgres 采用在每个连接上生成一个新进程的方式工作。而 MySQL 则在每个连接上生成一个新线程。因此,Postgres 提供了更好的隔离性,例如,一个无效的内存访问错误只会导致单个进程崩溃,而不是整个数据库服务器。另一方面,进程模型消耗更多资源。因此,在部署 Postgres 时建议通过连接池(如 PgBouncer 或 pgcat)代理连接。


生态 Ecosystem


常见的 SQL 工具都能很好地支持 Postgres 和 MySQL。由于 Postgres 的可扩展架构,并且仍被社区拥有,近年来 Postgres 生态系统更加繁荣。对于提供托管数据库服务的应用平台,每个都选择了 Postgres。从早期的 Heroku 到更新的 Supabase, render 和 Fly.io。


可运维性 Operability


由于底层存储引擎设计问题,在高负载下,Postgres 存在臭名昭著的 XID wraparound 问题。


对于 MySQL,在 Google Cloud 运营大规模 MySQL 集群时,我们遇到过一些复制错误。


这些问题只会在极端负载下发生。对于正常工作负载而言,无论是 Postgres 还是 MySQL 都是成熟且可靠的。数据库托管平台也提供集成备份/恢复和监控功能。


Postgres 还是 MySQL


2023 年了,在 Postgres 和 MySQL 之间做选择仍然很困难,并且经常引起激烈讨论




总的来说,Postgres 有更多功能、更繁荣的社区和生态;而 MySQL 则更易学习并且拥有庞大的用户群体。
我们观察到与 Stack Overflow 结果相同的行业趋势,即 Postgres 在开发者中变得越来越受欢迎。但根据我们的实际体验,精密的 Postgres 牺牲了一些便利性。如果你对 Postgres 不太熟悉,最好从云服务提供商那里启动一个实例,并运行几个查询来上手。有时候,这些额外好处可能并不值得,选择 MySQL 会更容易一些。


同时,在一个组织内部共存 Postgres 和 MySQL 也是很常见的情况。如果需要同时管理 Postgres 和 MySQL 的开发生命周期,可以来了解一下 Bytebase。






💡 你可以访问官网,免费注册云账号,立即体验 Bytebase。


作者:Bytebase
链接:https://juejin.cn/post/7254944931386228796
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

少一点功利主义,多一点傻逼似的坚持

感谢你观看本文,希望在未来的时光中,我们都能找到真正的自己,做真正的自己 坚持只需要一个理由,而放弃则有无数个接口,坚持很难,而放弃就是一刹那的时间,作为普通人的我们,其实只要能坚持做一件事,那么其实是很了不起的,可能它暂时不能给你带来经济价值,但是经过时间的...
继续阅读 »

感谢你观看本文,希望在未来的时光中,我们都能找到真正的自己,做真正的自己


坚持只需要一个理由,而放弃则有无数个接口,坚持很难,而放弃就是一刹那的时间,作为普通人的我们,其实只要能坚持做一件事,那么其实是很了不起的,可能它暂时不能给你带来经济价值,但是经过时间的酝酿,它会迸发处惊人的力量!


不过有一关是很难过的,这一关基本上可以刷掉百分之九十五的人,那就是否有长期主义,是否能够忍受“没有回报”,因为人的本性就是贪婪,而我们从小受到的教育就是“付出就有收获”,所以我们在做每一件事的时候,心里第一反应是我做这件事能给我带来多少收获。


比如读一本书,其实很多时候我们都是带有目的性的,比如觉得事业不顺,人生失意,或者想赚快钱,那么这时候就会去快速翻阅一些诸如《快速致富》的书籍,然后加满鸡血后,第二天依旧是十二点起,起来又卷入精神内耗中,反反复复,最终宝贵是时光!


又比如你看到别人赚到了钱,于是眼睛一红,就问他怎么赚的,别人稍微指点后,你就暗下决心要搞钱,前几天到几个月期间赚了几块钱,你就失落了,你在想,这条路子行不通,于是就放弃了,又去折腾其它的了。


上述的例子是百分之九十的人的真实写照,那么我觉得可以总结为两点:


1.只要没有得到应有的回报,就觉得是损失


2.极强的功利主义


首先对于这一点,我觉得是我们最容易犯的错,比如当一个人说你去坚持做这件事情,一个月会有一千的附加收入,你去做了,而实际上只拿到了50元的收入,这时候你就会极度的不平衡,感到愤怒,你会觉得花了这么多时间才得到50元,老子不干了,实际上你在这个过程中学到的东西远比1000块多,不过你不会觉得,这时候你宁愿去刷短视频,追剧,你也不会去做这件事了。


所以当你心中满是“付出多少就应该得到多少回报”的时候,你不可能做好事,也不会得到更好的回报,因为你心中总是在想“会不会0回报”,“这玩意究竟靠谱不靠谱”,克服这种心态是一件十分难的事情!


第二点,我觉得我们应该少一点功利主义,多一点傻逼似的坚持,这说得有点理想主义了,人本质就是贪婪的,如果赚不到钱,我就不做,对我没好处,我也不会做,我有写文章的习惯其实从大学就开始了,以前没发公众号,之前朋友经常说我,你写的有什么卵用?能赚钱吗?有人看吗?


一开始我还会在乎,在问自己,你干嘛写这些,因为写个人的感悟和生活这种文章确实会有一定的心里压力,朋友说:”你自己都是这个鸟样,有什么资格去给别人说教“,不过随着时间的推移,我不再去在乎这些了。


就单拿写文章这件事来说,虽然没赚到钱,不过在这个过程中,我逐渐不再浮躁,能静下心来写,也结实了朋友,这是一种对自己的总结,对技术的总结,也是一种锻炼,虽然现在文笔依然很差,不过我依然会像一个傻逼一样去坚持。


时间是最奇妙的东西,你的一些坚持一定会在相应的时间点迸发处惊人的力量!


回头想一下,你没写文章,没看书,没学习,没出去看世界,而是拿着个手机躺在床上刷短视频,像个清朝抽鸦片的人一样,那么你又收获了多少呢?


作者:刘牌
链接:https://juejin.cn/post/7278245506719825955
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Vision pro,当一切“眼见为实”

iOS
关于 Vision pro,留存一点感想,或许十年后再来回顾。缺点肯定不少,但是这个产品带来了很有趣的新维度 WWDC直播时,最大的疑问是眼动追踪交互足够准确吗?能即时反馈吗?看过各位媒体的文字或口述体验之后,才知道苹果竟然将这种交互方式做得像来自未来一样,...
继续阅读 »

关于 Vision pro,留存一点感想,或许十年后再来回顾。缺点肯定不少,但是这个产品带来了很有趣的新维度




WWDC直播时,最大的疑问是眼动追踪交互足够准确吗?能即时反馈吗?看过各位媒体的文字或口述体验之后,才知道苹果竟然将这种交互方式做得像来自未来一样,通过你的目光,精准得可以定位到一个小小的字母,随时随地随心而动,简直不可思议。


直播中所展示的三维交互效果,让我想起人类对信息的记录和显示方式。文字、图画刻在石头上,刻在竹简上,纸墨印刷成书册;照片、视频,呈现在手机或者电脑的二维屏幕上。而三维信息,可能从未被如此精确真实地呈现在我们的面前。诚然 3D 电影和许多别的 VR、AR 生产厂商也做了诸多努力和探索,但是那些效果都还不足以 “以假乱真”。从现在开始,消费级的信息记录方式,或许又能上升一个维度。


而直播中最大的震撼其实是迪士尼宣传片的一连串 What if...What if all the things we thought impossible were suddenly possible🤯一连串电影和想象中的角色和景观展现在我的眼前,和现实仿若融为一体。这让我感受到 Vision pro 或许能用一种全新的交互体验,让观众真正地身临其境,沉浸其中,甚至忘记真实和虚幻的界限。


直播接近尾声时,BGM 反复唱着 Be a Dreamer,苹果是个实干的梦想家,他们有足够的底气和积淀去梦想,更用努力和科技,将不可能变成可能,将许许多多科幻片中的梦想带来到 2023 年,用 Vision pro 为所有人铺就了无限大的画布,哦,不是二维的画布,是无限大的梦想空间!


当然,这个空间,目前似乎还只有基础的系统应用,像个刚通水电的毛坯房。他究竟能有怎样的表现,还是得看这些内容生产者开发出怎样的内容。有人诟病苹果在六月份拿出来这样的宣传,却要在明年年初才能售卖。可我相信,过去 APP Store 的成功很可能会在 Vision OS 中再现。WWDC,是苹果开发者大会,即主要面向开发者等专业人士的会议。Apple 召集起这些媒体,摄影师,导演,应用和游戏开发者率先开始了解 Vision pro。这些内容生产者,有他们,就有了 dream maker,造梦人,为普通用户编织光怪陆离的绚烂梦境。


看完各位博主的真机体验,Vision pro 并不是一个取代现有的手机、电脑的产品,这是一个全新的,开创新的体验维度,开创人类新需求的产品。


作为多年的哈迷,感觉现在 Vision pro 的语音输入,手势识别和 3D 交互完全可以让我们拿着魔杖释放咒语,让我们和神奇动物面对面,让我们就像骑着飞天扫帚一样去追踪金色飞贼。因为有了如此先进的科技,魔法世界不再是幻想🥺🤩


更可以想象,无论是工业设计,照片、视频、电视电影还是游戏,都可能会因为这种全新的沉浸式的三维交互体验,而被改写。


你可以和同事一起在虚拟空间中建造模型,模拟生产制造流程。


你可以把与亲朋好友、猫猫狗狗共度的美好时光定格在一片似真似幻的空间。无论何时再回首,他们好像永远在你身边,永不褪色。


电影制作人未来可以使用专门的摄像机制作沉浸式三维电影,在家就能有 100 英尺,接近 30 米的巨幕享受。 篮球比赛你可以选择不同的机位跟踪你喜欢的球星和精彩瞬间。 演唱会你可以在任何地方躺下享受最佳视角和空间音效。


而游戏,新增的交互体验更是给了游戏制作人们无限的想象空间。 操控赛车从北极的冰川到热带的雨林;在枪林弹雨中和队友并肩对抗敌人;在球赛场上面对面激情碰撞。配合上 AI 和语言模型,喜欢的二次元角色仿佛搭着你的肩膀和你耳语;所有的一切,开始“眼见为实”。


Vision pro 的眼部追踪、手势交互和 3D 显示混合现实的完成度,带来了像当年 iPhone 实现多点触控的革命性质变。从技术的进步来说,我个人认为这次的质变可能更加惊艳,更加了不起。但是 3499 美元,加上税可能 3 万人民币。说实话,这不是一个大众消费者能够接受的价格。即使有 air 版本,我感觉可能也需要上万人民币。所以,个人估计,它受欢迎的程度应该会大于等于 mac 小于 iPhone 和 AirPods。


当年 Apple Macintosh 开创了精美的高完成度的计算机图形界面 GUI,让电脑走入消费者群体,可价格太贵,真正让个人电脑普及的是微软;现在,iOS 将手机变成了一个功能强大的多媒体设备,可价格不便宜,真正让千家万户享受到智能手机的是 Android。未来,相比 Vision pro,或许有其他品牌的廉价替代品,更开放的开源混合现实系统,Vision 系列或许都不能获得最大的市场份额。可是由 Vision 所真正掀开的新维度不会被关闭,这个被精心打造出来的梦想空间只会无限延伸,满载着人类的创意和梦想…最后的最后,正如 WWDC 中 Apple 所言:


Be a dreamer. This is just the START.


作者:VickyLu
链接:https://juejin.cn/post/7242186721785708604
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

SwiftUI 入门教程 - 基础控件

iOS
SwiftUI 是 Apple 新推出的一款能快速搭建页面的 framework。它采用的是声明式语法,简洁明了。 而且它是所见即所得的,你写的代码都能通过 Preview 实时的看到效果,这可以很大的节省开发者开发时间。当你开发一个复杂的项目,需要等待几分钟...
继续阅读 »

SwiftUI 是 Apple 新推出的一款能快速搭建页面的 framework。它采用的是声明式语法,简洁明了。


而且它是所见即所得的,你写的代码都能通过 Preview 实时的看到效果,这可以很大的节省开发者开发时间。当你开发一个复杂的项目,需要等待几分钟的时间去编译运行代码,只为了看一个 UILabel 字体大小或者颜色是否改变时,你就能体会到所见即所得的快乐了。


基础控件


当我们新建一个项目,选择 Interface 选择 SwiftUI 时,建好的项目会自带一个 ContentView,这是下面的默认代码:

struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

ContentView 是需要我们根据需求修改代码的部分,下面的 ContentView_Previews 则是为了实时预览的。


Tips:如果注释ContentView_Previews,你会发现预览页面也会消失。


ContentView 代码说明


首先,可以看到 ContentView 有一个 body 的计算属性,该属性代表当前视图的内容。当你实现一个自定义 view 的时候,必须要实现该属性,否则代码会报错。


VStack 代表的是一个垂直布局。里面包含 Image 和 Text,两个控件垂直布局。padding 则代表当前视图外边距的间距。


Text 对应 UILabel


在 SwiftUI 中,用 Text 控件来展示静态文本。下面是它的代码示例:

Text("我是一个文本")
.font(.title)
.foregroundColor(.red)
.frame(width: 100, alignment: .center)
.lineLimit(1)
.background(.yellow)

常用的属性基本就这几个:


  • font:字体。如果想更加细致化的指定字体,可以用 system,.font(.system(size: 16, weight: .light))
  • foregroundColor:字体颜色。
  • frame:控制文本的大小和对齐位置。这个不写的话默认是自适应宽高。如果仅指定宽度就是高度自适应,仅指定高度就是宽度自适应。
  • lineLimit:指定行数,默认为 0,不限制行数。
  • background:用来设置背景。比如背景形状、背景颜色等等。

Tips:SwiftUI 的布局简化了自动布局和弱化了 frame 指定具体数值的布局方式。默认都是自适应的,这一点和 Flutter 类似,大大提高了开发效率。


Image 对应 UIImageView


在 SwiftUI 中,Image 用来展示图像资源。下面是它的示例代码:

Image(systemName: "globe")
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.accentColor)
.background(.red)

常用属性:


  • resizable:可调整大小以适应当前布局。
  • aspectRatio:调整缩放比。
  • foregroundColor、background:参见 Text。

Button 对应 UIButton


在 SwiftUI 中,用 Button 来表示一个按钮。下面是它的示例代码:

Button {
print("点击了按钮")
} label: {
Text("按钮文本")
Image(systemName: "globe")
}
.cornerRadius(10)
.background(.red)
.font(.body)
.border(.black, width: 2)

常用属性:


  • font、foregroundColor、background 等属性与 Text 使用一致。
  • label:用来自定义按钮的文本和图标。
  • cornerRadius:设置圆角。
  • border:设置边框。

总结


本文主要讲解了 SwiftUI 的三个基本控件 Text:用来展示静态文本;Image:用来加载图像资源;Button:用来展示按钮。以及三个控件的基本使用。希望通过此文大家可以对 SwiftUI 的语法有个基本的了解。


作者:冯志浩
链接:https://juejin.cn/post/7239178749153525816
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

中国未来楼市,程序员的小窝购买指南

中国楼市持续火爆,未来趋势如何?中国楼市在过去的几年里一直保持着火爆的态势,无论是房价还是成交量都不断创下新高。本文将从中国楼市的背景介绍、市场分析、原因分析和未来展望等方面进行分析。一、背景介绍中国楼市的发展可以追溯到上世纪80年代,当时城市土地开始实行私有...
继续阅读 »

中国楼市持续火爆,未来趋势如何?

中国楼市在过去的几年里一直保持着火爆的态势,无论是房价还是成交量都不断创下新高。本文将从中国楼市的背景介绍、市场分析、原因分析和未来展望等方面进行分析。

一、背景介绍

中国楼市的发展可以追溯到上世纪80年代,当时城市土地开始实行私有化改革,房地产市场逐渐形成。随着经济的快速发展和城市化进程的加速,中国楼市也迎来了飞速发展的时期。特别是2000年以来,房地产市场逐渐成为国民经济的重要支柱产业,政府也出台了一系列扶持政策,如住房制度改革、住房公积金制度等。

二、市场分析

  1. 投资者热衷于购房

随着人们生活水平的提高和购房政策的宽松,越来越多的投资者热衷于购房。他们将购房视为一种投资手段,认为房价会持续上涨,从而获得更多的收益。这种投资需求的增加也推高了中国楼市的房价。

  1. 房贷违约案例逐渐增多

随着楼市的火爆,越来越多的人选择贷款购房。然而,近年来房贷违约案例逐渐增多,给银行和房地产市场带来了不小的风险。部分购房者由于收入不稳定、贷款利率上升等原因,无法按时偿还房贷,导致违约。

三、原因分析

  1. 政策调控

中国政府对房地产市场的调控政策对市场的影响非常大。例如,政府出台的“国八条”、“限购令”等政策,对楼市进行了严格的调控,使得市场逐渐回归理性。

  1. 利率变化

利率的变化也是影响楼市的重要因素之一。在利率较低的时候,购房者可以获得更低的贷款利率,从而降低了购房成本,提高了购房需求。而在利率较高的时候,购房者的负担加重,购房需求相应减少。

  1. 人口因素

中国拥有庞大的人口基数,这也为房地产市场提供了广阔的需求空间。特别是在城市化进程加速的情况下,大量人口涌入城市,使得城市房屋需求不断增长。

四、未来展望

  1. 政策调整

未来中国政府可能会对楼市政策进行适当调整。一方面,政府将继续加强对房地产市场的监管,抑制房价过快上涨;另一方面,政府可能会出台更加优惠的购房政策,鼓励刚需和改善型购房者购房。

  1. 经济环境的变化

中国经济的发展也可能会对中国楼市产生影响。未来中国经济可能会逐渐转型,从传统的制造业向服务业和高科技产业转型。这种转型可能会导致人们对住房的需求发生变化,对楼市产生一定的影响。

综上所述,中国楼市在经历了一段飞速发展的时期后,目前仍处于较为火热的态势。然而,受到政策调控、利率变化、人口因素等多种因素的影响,楼市也面临一定的挑战。未来,中国楼市将如何在政策调整和经济环境的变化中寻找新的发展方向,值得我们进一步关注和研究。

收起阅读 »

iOS 电商倒计时

iOS
背景 最近项目中,需要做一个如图所示的倒计时控件,上网搜了一圈,发现大家的方法大同小异,都是把倒计时的秒,转换成时分秒然后拼接字符串,见下图 网上大部分采用的方法 juejin.cn/post/684490…  在我的项目中,期望这个倒计时控件的f...
继续阅读 »

背景


最近项目中,需要做一个如图所示的倒计时控件,上网搜了一圈,发现大家的方法大同小异,都是把倒计时的秒,转换成时分秒然后拼接字符串,见下图




网上大部分采用的方法
juejin.cn/post/684490… 



在我的项目中,期望这个倒计时控件的format是可以自定义的,所以计算时分秒这样的方式,对于我的需求是不太灵活的


既然format需要自定义,那么很容易想到一个时间格式处理的类:DateFormatter


思路


后端返回的字段

init_time // 需要倒计时的时长,单位ms
format // 展示的倒计时格式

我们的需求其实非常明确,就是完成一个可以自定义format的倒计时label


那我们拆解一下整个需求:

  • 自定formatlabel
    • Date自定义format显示
    • 指定Date自定义format显示
  • 可以进行倒计时功能
  • 那么我们怎么才能把要倒计时的时长,转换为时分秒呢?

    • 直接计算后端给的init_time,算出是多少小时,多少分钟,多少秒
    • 如果我从每天的零点开始计时,然后把init_time作为偏移量不就是我要倒计时的时间吗,而且这个可以完美解决需要自定义format的问题,Date可以直接通过 DateFormatter转化成字符串 



Date自定义format显示

let df = DateFormatter()

df.dateFormat = "hh:mm:ss"

print("🍀", df.string(from: Date()), "🍀\n\n")

输出:🍀 03:56:28 🍀

指定Date自定义format显示

let df = DateFormatter()

var calendar = Calendar(identifier: .gregorian)

let startOfDate = calendar.startOfDay(for: Date())

df.dateFormat = "hh:mm:ss"

print("🍀", df.string(from: startOfDate), "🍀\n\n")

输出:🍀 12:00:00 🍀

完整功能

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        initCountdownTimer()

        return true

    }

private var timer: DispatchSourceTimer?

private var second = 0

// 单位ms
var delayTime = 0

// 单位ms
var interval = 1000
var initSecound = 10

var format = "hh:mm:ss"

private lazy var startDate: Date = {
var calendar = Calendar(identifier: .gregorian)
        let startOfDate = calendar.startOfDay(for: Date())
        return Date(timeInterval: TimeInterval(initSecound), since: startOfDate)
  }()

  private lazy var df: DateFormatter = {
let df = DateFormatter()
        df.dateFormat = format
        return df
  }()

  func initCountdownTimer() {
        timer = DispatchSource.makeTimerSource(queue: .main)
        timer?.schedule(deadline: .now() + .milliseconds(delayTime), repeating: .milliseconds(interval), leeway: .milliseconds(1))
        timer?.setEventHandler { [weak self] in
            self?.updateText()
            self?.second += 1
        }

        timer?.resume()
    }

    func deinitTimer() {
        timer?.cancel()
        timer = nil
    }

    func updateText() {
        if second == initSecound && second != 0 {
            deinitTimer()
        }
        if second == initSecound {
            return
        }
        let date = Date(timeInterval: -TimeInterval(second + 1), since: startDate)
        let text = df.string(from: date)

        print(text)
    }

输出:
12:00:09
12:00:08
12:00:07
12:00:06
12:00:05
12:00:04
12:00:03
12:00:02
12:00:01
12:00:00

以上整个功能基本完成,但是细心的同学肯定发现了,按道理小时部分应该是00,但是实际是12,这是为什么呢,为什么呢?


我在这里研究了好久,上网查了很多资料


最后去研究了foramt每个字母的意思才知道:

  • h 代表 12小时制

  • H 代表 24小时制,如果想要显示00,把"hh:mm:ss"改成"HH:mm:ss"即可


时间格式符号字段详见


作者:xiAo_Ju
链接:https://juejin.cn/post/7240303252930609207
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

我有一刀,可斩全栈

引言 夜谈性的文章,思考篇幅会比较啰嗦,篇幅基本会以概念、发展、思考、未来这几个内容主题进行,最近结合软环境地狱,再到看到社区的很多未来思考,做一些总结和预测,去年的一些总结,今年基本应验了一部分,希望能起到警示和思考吧。 概念 什么是全栈 全栈(Full-...
继续阅读 »

引言


夜谈性的文章,思考篇幅会比较啰嗦,篇幅基本会以概念、发展、思考、未来这几个内容主题进行,最近结合软环境地狱,再到看到社区的很多未来思考,做一些总结和预测,去年的一些总结,今年基本应验了一部分,希望能起到警示和思考吧。


概念


什么是全栈



全栈(Full-Stack)是指一种解决问题域全局性技术的能力模型。


很多现代项目开发,需要掌握多种技术,以减少沟通成本、解决人手不够资源紧张、问题闭环的问题。全栈对业务的价值很大,如对于整个业务的统筹、技术方案的判断选型、问题的定位解决等,全栈技术能力有重要影响。另外对于各种人才配套不是很齐全的创业公司,全栈能解决各种问题,独挡多面,节省成本,能在早期促进业务快速发展。


技术有两个发展方向,一种是纵向一种是横向的,横向的是瑞士军刀,纵向的是削铁如泥的干将莫邪。这两个方向都没有对与错,发展到一定程度都会相互融合,就好比中国佛家禅修的南顿北渐,其实到了最后,渐悟与顿悟是一样的,顿由渐中来。可以说全栈什么都会,但又什么都不会。



全栈定义


狭义


全栈 = 前端 / 终端 + 后端


广义(问题全域)


全栈 = 呈现端(硬件 + 操作系统(linux/windows/android/ios/..) + 浏览器/宿主环境+端差异【机型、定制】) +H5+小程序(多端统一框架)+ 前端开发/终端开发 + 网络 + 后端开发(架构/算法) + 数据(SQL/NoSQL/半结构/时序/图特性) + 测试 + 运维


+软实力=文档能力+UI能力+业务能力+设计能力+技术视角(前瞻性)选型+不同语言掌握能力+项目管理能力+架构设计能力+客户沟通能力+技术撕逼能力+运营能力


价值


全局性思维


一个交付项目的全周期,除了传统的软件过程,需求调研、规划、商务、合同签订、立项、软件过程、交付、实施运维等,麻雀虽小,五脏俱全,如果对并发、相应、扩展性、并行开发等有硬性要求,软件过程会变得异常复杂,因此后来又拆前端架构、后端架构定向的解决某个领域内的技术规划岗位,因为人力反倒是小问题,要的是快和结果稳定,项目可以迅速肢解投入,每个岗位注重领域和边界问题,以做沟通的核心基础,对于一个团队特别是互联网企业来说,有一个全局性思维的人非常非常重要,这个角色常常会被赋予(产品/项目)或其他Tile,什么事业线、军团之类的,本质上也是对人员的细节化和边界的扩充。
回到本质问题,当人成为问题的时候,以3个人为例,一般开发层的东西,3个合理偏重的 【狭义全栈】,做事的效率和执行沟通结果和3个1+2的分端是完全不同的,一个是以业务块沟通的,一个是以功能块沟通的,一个是对业务块结果负责,一个是对功能块结果负责。


其实刚入职那会儿,就有人和我说,服务是看不到的,端是直面的,这其中有个度的问题,不过度设计、不过度随意,保持需求和设计在合理区间内,有适度的前瞻性即可。
我之前接触的单端普遍会犯在业务不可能的场景下,纯粹讨论逻辑性的问题,导致的无休止的无意义讨论,最终的反思是 我想把这个东西做好, 举个不太恰当的例子叫 "有一种冷,叫妈妈觉得你冷",我把这种归结起来就是不对结果负责,只对自己负责,这也多半是因为岗位边界的问题导致的。


沟通成本


项目越大,沟通成本越高,做过项目管理的都知道,项目中的人力是1+1<2的,人越多效率越低。因为沟通是需要成本的,不同技术的人各说各话,前端和后端是一定会掐架的。每个人都会为自己的利益而战,毫不为己的人是不存在的。


而全栈工程师的沟通成本会主要集中在业务上,因为各种技术都懂,胸有成竹,自己就全做了。即使是在团队协作中,与不同技术人员的沟通也会容易得多,让一个后端和一个前端去沟通,那完全是鸡同鸭讲,更不用说设计师与后端了。但如果有一个人懂产品懂设计懂前端懂后端,那沟通的结果显然不一样,因为他们讲的,彼此都能听得懂,相信经历过(纯业务/纯管理/纯产品)蹂躏过的开发应该有体会。


性价比与结果控制


创业公司不可能像大公司一样,各方面的人才都有。所以需要一个多面手,各种活都能一肩挑,独挡多面的万金油。对于创业公司,不可能说DBA前端后端客户端各种人才全都备齐了,很多工作请人又不饱和,不请人又没法做,外包又不放心质量,所以全栈工程师是省钱的一妙招,大公司不用担心人力,小公司绕不过的就是人力,当人力被卡住,事情被挡住了,独当一面可不只是说说而已,此时的价值就会被凸显,技术解决问题的途径很多样。


这里说个题外话,性价比是对企业的,那对个人来说,意味着个人的能量和价值会放大,如果你细心观察开源的趋势,会发现整体性的项目趋势变多了,而且基本在微小的时候可能只是单人支撑的,这个趋势从百度技术领跑再到阿里转换时有过方向和风格的转换。


困境


说得不好听一点,全栈工程师就是什么都会,什么都不会,但有需求,结果、时间、风险都会被很好的评估,因为思路和理念是完全不同的,全栈天然的就必然会重视执行结果,单端只注重过程,事情做了,坏的结果跟我一点儿关系都没有,其中甘苦,经历了才知道,所以也注定面试是不占优势的,而且全栈根本没有啥标准的划分,也注定游离在小公司才能如鱼得水,当然,如果你的目标是星辰大海,工作自由,这个事就另当别论了。


发展


天下大事分久必合,合久必分,最开始的没有前端,到分出前端,没有安卓/IOS到分出岗位,再到手机端合到前端,pc到前端,”大前端“的概念,不管技术怎么进步或者变化,总归是要为行业趋势负责的,就好比你为300人的企业用户考虑高并发,完全不计较实施和人力成本,很多的事情都是先试水再铺开的,没那么技术死板。


感觉整个软件生态发展至今,提供便利的同时,也用框架把每个人往工具这个方向上在培养,这本就是符合企业利益的事,但减量环境下,螺丝钉的支撑意义被无限的减弱和消磨,很多的单端从业一段时间后,想做事儿,发现另外领域的空白,也开始往横向考虑,这本就是危机思考和方向驱动的结果,一个大周期的循环又开始了,特别是在java国内的一家独大,再到个体开始挣扎的时候,多态的语言开始反噬,反噬的驱动力也从服务器这个层级开始了挣扎,亦如当年的java跨平台先机一样。


前端的框架随着框架的便捷性和易用性越来越完善,其竞争力变得隐形了,回归了工程化问题的解决能力,去年也提过,变化中思考,稳定中死亡,到了思考自己的核心竞争力是什么的时候了,这何尝不是自由工作者的春天。


端扩散


软件的路程发展已经有了很长一段路,概念和业务层级的提升服务有限,自动化、半自动化、AI的概念渐渐的可以走向技术成熟,端的发展又有了去处,只不过这个过程很慎重,需要打通很多封闭的东西,再加上工业信息化的政策加持,单纯的信息录入或者业务系统已经掀不起多大风浪,而纯互联网的金融、物联网也被玩的渣都不剩,突围和再上一层的变革,短时间内,公司级的突破已经很难找到出路,从收缩阵地,裁剪人员可见一斑。


复杂度提升


如果说有确切的变化,那基本就是我机器上的编译器环境和用的工具越来越多样,解决问题的途径和手段越来越多,不再是原来的一个整合ide解决所有问题,这就好比,我原先手上只有木棍,武器用它、做房子用它、生火也用它,挖掘的它所有的应用途径,那有一天,我有了刀、有了席梦思的床、有了大别墅,却因为害怕放着不用。当然,我之前听别人说过一个理论:”只要能解决好结果,哪怕你徒手,我也无所谓“,他站在老板的角度上,至于你是累死也好,花10倍的工作量也好,都无所谓。作为个体来说,既然只要结果,那就别怪我偷工作量了,个体的掌握技能的多样性,背后可是有语言生态支持的,因此复杂度的提升,也带来了生态支持,并非一边倒的情况。


人心异化


我依然怀念头几年的环境,都是集中在解决问题,目标一致,各自解决各自的问题,拼到一起,就是整体结果,各自的同事关系轻松和谐,上线前的交付大家一起搞的1点多,下班宵夜美滋滋,现在端分离和职责明确,天然存在利益冲突,摸鱼划水,撕逼的情况,虽说可能是部分老鼠屎引起的,但谁说这不是热情消退的结果呢,生活归生活,工作归工作,但生活真的归了生活,工作真的只归了工作吗?


思考


全栈的title就跟我参与了xxx开源项目一样,貌似也成为提升竞争力,标签化的一种,架构师、小组长、技术经理、总监,这些title,在离职那一刻其实都毫无意义,有意义的也只是待遇和自身的能力,如果你怀着高title在另外一家公司风生水起的想法,那很多3个月离职的经历,再一家还是3个月,难道不是面试能力和自身的能力出现不对等了嘛,可能是所有的公司都坑,那有没有可能是我们韧性太低,选择不慎呢。


好像刚工作那会儿,经常会被问到职业规划,之后很少被问到,却不停的在想,我能干嘛,今后想干嘛,之后就是无休止的躁动和不停的学习,不停的接项目,不停的用新技术,10年多的坚持,平均12点,找的工作基本也都是相对轻松的,那我能干啥,好像貌似什么也做不了,想法创意不停的被对比否认,找到合适的却不停的为盈利性的项目让路,貌似什么都会,貌似什么都没做成,原本以为是觉得自己修炼不够,没法实现自己的项目,后来发现,其实自己的第二职业,只需要一条路,一往无前的坚持,最终会有结果,尽管这个结果可能不好,但事情实践了,回想起刚工作那会儿”先理顺环节,再开发,还是先出东西再说“的争论,这会儿我完全认同了 ”先结果,再谈未来“


因此,别管什么 ”前端已死“”java已死“,大环境不好,行业低迷,去行动吧,亲手埋葬也许,焕发新生也好,回到内心,做好与行业诀别的决心,背水一战。即便是为了生活被迫转行,也可毫不顾忌的说,努力过,没戏,直面内心,回想起18年看到的新闻,”程序猿直播7天0观众“,我想我能够做的也只能是武装与坚持,至于大环境怎样,行业怎样,到那一天再说吧,套用领导的话”别想那些有的没的,做好自己的事“,至少,我人为,当软件公司不易时,恰恰是个体的机会,当个体的力量开始有竞争力,那全栈的优势会有很好的发挥,这个场景在我有意识的5人实践和2人优势互补中已经得到了长效的验证。


未来


也许从当前的公司离职那天,就是我职业生涯结束那天,我已经做好了心里预期,但我希望可以作为一个自由工作者,这是我后半段反复思考的结果,至于结果怎样,我只能说,预期的努力我已经做了,时机和后续有待生活的刀斩我不屈之心。


PS


认清内心、从容面对,不要有什么鸵鸟心态,事实不逃避,行动不耽误,这是斩龙之刀,破除未知的迷雾,我所能提的也只是从心和认知,没啥发展途径和规划,因为技术的发展,总是未知和充满惊喜的,这也正是它的魅力所在。


最后


我深怕自己本非美玉,故而不敢加以刻苦琢磨,却又半信自己是块美玉,故又不肯庸庸碌碌,与瓦砾为伍。于是我渐渐地脱离凡尘,疏远世人,结果便是一任愤懑与羞恨日益助长内心那恬弱的自尊心。


作者:沈二到不行
来源:juejin.cn/post/7248118049583628344
收起阅读 »

如果写劣质代码是犯罪,那我该判无期

导读 程序员痛恨遇到质量低劣的代码,但在高压环境下,我们常为了最快解决当下需求而忽略代码规范,在无意识中堆积大量债务。我们还观察到许多开发者被迫加班的罪魁祸首便是写低效代码、不重视代码优化。编程路上,欲速则不达。 接下来,我将为各位列举9种我个人工作中高频遇到...
继续阅读 »

导读


程序员痛恨遇到质量低劣的代码,但在高压环境下,我们常为了最快解决当下需求而忽略代码规范,在无意识中堆积大量债务。我们还观察到许多开发者被迫加班的罪魁祸首便是写低效代码、不重视代码优化。编程路上,欲速则不达。 接下来,我将为各位列举9种我个人工作中高频遇到的不整洁代码行为,并提出针对性优化建议。继续阅读~


目录


1 代码风格和可读性


2 注释


3 错误处理和异常处理


4 代码复用和模块化


5 硬编码


6 测试和调试


7 性能优化


8 代码安全性


9 版本控制和协作


10 总结


01、代码风格和可读性



  • 错误习惯


不一致的命名规则:使用多种命名规则,如 camelCase、snake_case 和 PascalCase 等。过长的函数和方法:编写过长的函数和方法,导致代码难以阅读和理解。 过长的行:编写超过50字符的代码行,导致代码难以阅读。

1.1 变量命名不规范


在编程中,变量命名是非常重要的,良好的变量命名能够提高代码的可读性和可维护性。不规范的命名会增加理解难度,以下是一个不规范命名的例子:


int a, b, c; // 不具有描述性的变量名
float f; // 不清楚变量表示的含义

这样的变量命名不仅会降低代码的可读性,还可能会导致变量混淆,增加代码维护的难度。正确的做法应该使用有意义的名称来命名变量。例如:


int num1, num2, result; // 具有描述性的变量名
float price; // 清晰明了的变量名

1.2 长函数和复杂逻辑


长函数和复杂逻辑是另一个常见的错误和坏习惯。长函数难以理解和维护,而复杂逻辑可能导致错误和难以调试。以下是一个长函数和复杂逻辑的案例:


def count_grade(score):
if score >= 90:
grade = 'A'
elif score >= 80:
grade = 'B'
elif score >= 70:
grade = 'C'
elif score >= 60:
grade = 'D'
else:
grade = 'F'

if grade == 'A' or grade == 'B':
result = 'Pass'
else:
result = 'Fail'
return result

在这个例子中,函数 count_grade 包含了较长的逻辑和多个嵌套的条件语句,使得代码难以理解和维护。正确的做法是将逻辑拆分为多个小函数,每个函数只负责一个简单的任务,例如:


def count_grade(score):
grade = get_grade(score)
result = pass_or_fail(grade)
return result
def get_grade(score):
if score >= 90:
return 'A'
elif score >= 80:
return 'B'
elif score >= 70:
return 'C'
elif score >= 60:
return 'D'
else:
return 'F'
def pass_or_fail(grade):
if grade == 'A' or grade == 'B':
return 'Pass'
else:
return 'Fail'

通过拆分函数,我们使得代码更加可读和可维护。


1.3 过长的行


代码行过长,会导致代码难以阅读和理解,增加了维护和调试的难度。例如:


def f(x):
if x>0:return 'positive' elif x<0:return 'negative'else:return 'zero'

这段代码的问题在于,它没有正确地使用空格和换行,使得代码看起来混乱,难以阅读。正确的方法是,我们应该遵循一定的代码规范和风格,使得代码清晰、易读。下面是按照 PEP 8规范改写的代码:


def check_number(x):
if x > 0:
return 'positive'
elif x < 0:
return 'negative'
else:
return 'zero'

这段代码使用了正确的空格和换行,使得代码清晰、易读。


02、注释



  • 错误习惯


缺少注释:没有为代码编写注释,导致其他人难以理解代码的功能和逻辑。 过时的注释:未及时更新注释,使注释与实际代码不一致。 错误注释:注释上并不规范,常常使用一些不合理的注释。



  • 错误的注释




注释是非常重要的,良好的注释可以提高代码的可读性和可维护性。以下是一个不规范的例子:


int num1, num2; // 定义两个变量

上述代码中,注释并没有提供有用的信息,反而增加了代码的复杂度。


03、错误处理和异常处理



  • 错误的习惯


忽略错误:未对可能出现的错误进行处理。 过度使用异常处理:滥用 try...except 结构,导致代码逻辑混乱。 捕获过于宽泛的异常:捕获过于宽泛的异常,如 except Exception,导致难以定位问题。

3.1 忽略错误


我们往往会遇到各种错误和异常。如果我们忽视了错误处理,那么当错误发生时,程序可能会崩溃,或者出现不可预知的行为。例如:


def divide(x, y):
return x / y

这段代码的问题在于,当 y 为0时,它会抛出 ZeroDivisionError 异常,但是这段代码没有处理这个异常。下面是改进的代码:


def divide(x, y):
try:
return x / y
except ZeroDivisionError:
return 'Cannot divide by zero!'

3.2 过度使用异常处理


我们可能会使用异常处理来替代条件判断,这是不合适的。异常处理应该用于处理异常情况,而不是正常的控制流程。例如:


def divide(a, b):
try:
result = a / b
except ZeroDivisionError:
result = float('inf')
return result

在这个示例中,我们使用异常处理来处理除以零的情况。正确做法:


def divide(a, b):
if b == 0:
result = float('inf')
else:
result = a / b
return result

在这个示例中,我们使用条件判断来处理除以零的情况,而不是使用异常处理。


3.3 捕获过于宽泛的异常


捕获过于宽泛的异常可能导致程序崩溃或隐藏潜在的问题。以下是一个案例:


try {
// 执行一些可能抛出异常的代码
} catch (Exception e) {
// 捕获所有异常,并忽略错误}

在这个例子中,异常被捕获后,没有进行任何处理或记录,导致程序无法正确处理异常情况。正确的做法是根据具体情况,选择合适的异常处理方式,例如:


try {
// 执行一些可能抛出异常的代码
} catch (FileNotFoundException e) {
// 处理文件未找到异常
logger.error("File not found", e);
} catch (IOException e) {
// 处理IO异常
logger.error("IO error", e);
} catch (Exception e) {
// 处理其他异常
logger.error("Unexpected error", e);}

通过合理的异常处理,我们可以更好地处理异常情况,增加程序的稳定性和可靠性。


04、错误处理和异常处理



  • 错误的习惯


缺乏复用性:代码冗余,维护困难,增加 bug 出现的可能性。 缺乏模块化:代码耦合度高,难以重构和测试。

4.1 缺乏复用性


代码重复是一种非常常见的错误。当我们需要实现某个功能时,可能会复制粘贴之前的代码来实现,这样可能会导致代码重复,增加代码维护的难度。例如:


   def calculate_area_of_rectangle(length, width):
return length * width

def calculate_volume_of_cuboid(length, width, height):
return length * width * height

def calculate_area_of_triangle(base, height):
return 0.5 * base * height

def calculate_volume_of_cone(radius, height):
return (1/3) * 3.14 * radius * radius * height

上述代码中,计算逻辑存在重复,这样的代码重复会影响代码的可维护性。为了避免代码重复,我们可以将相同的代码复用,封装成一个函数或者方法。例如:


   def calculate_area_of_rectangle(length, width):
return length * width

def calculate_volume(length, width, height):
return calculate_area_of_rectangle(length, width) * height

def calculate_area_of_triangle(base, height):
return 0.5 * base * height

def calculate_volume_of_cone(radius, height):
return (1/3) * 3.14 * radius * radius * height

这样,我们就可以避免代码重复,提高代码的可维护性。


4.2 缺乏模块化


缺乏模块化是一种常见的错误,这样容易造成冗余,降低代码的可维护性,例如:


   class User:
def __init__(self, name):
self.name = name

def save(self):
# 保存用户到数据库的逻辑

def send_email(self, content):
# 发送邮件的逻辑

class Order:
def __init__(self, user, product):
self.user = user
self.product = product

def save(self):
# 保存订单到数据库的逻辑

def send_email(self, content):
# 发送邮件的逻辑
```

此例中,User 和 Order 类都包含了保存和发送邮件的逻辑,导致代码重复,耦合度高。我们可以通过将发送邮件的逻辑提取为一个独立的类,例如:


   class User:
def __init__(self, name):
self.name = name

def save(self):
# 保存用户到数据库的逻辑

class Order:
def __init__(self, user, product):
self.user = user
self.product = product

def save(self):
# 保存订单到数据库的逻辑

class EmailSender:
def send_email(self, content):
# 发送邮件的逻辑

通过把发送邮件单独提取出来,实现了模块化。现在 User 和 Order 类只负责自己的核心功能,而发送邮件的逻辑由 EmailSender 类负责。这样一来,代码更加清晰,耦合度降低,易于重构和测试。


05、硬编码



  • 错误的习惯


常量:设置固定常量,导致维护困难。 全局变量:过度使用全局变量,导致程序的状态难以跟踪。

5.1 常量


在编程中,我们经常需要使用一些常量,如数字、字符串等。然而,直接在代码中硬编码这些常量是一个不好的习惯,因为它们可能会在未来发生变化,导致维护困难。例如:


def calculate_score(score):
if (score > 60) {
// do something}

这里的60就是一个硬编码的常量,导致后续维护困难,正确的做法应该使用常量或者枚举来表示。例如:


PASS_SCORE = 60;
def calculate_score(score):
if (score > PASS_SCORE) {
// do something }

这样,我们就可以避免硬编码,提高代码的可维护性。


5.2 全局变量


过度使用全局变量在全局范围内都可以访问和修改。因此,过度使用全局变量可能会导致程序的状态难以跟踪,增加了程序出错的可能性。例如:


counter = 0
def increment():
global counter
counter +
= 1

这段代码的问题在于,它使用了全局变量 counter,使得程序的状态难以跟踪。我们应该尽量减少全局变量的使用,而是使用函数参数和返回值来传递数据。例如:


def increment(counter):
return counter + 1

这段代码没有使用全局变量,而是使用函数参数和返回值来传递数据,使得程序的状态更易于跟踪。


06、测试和调试



  • 错误的习惯


单元测试:不进行单元测试会导致无法及时发现和修复代码中的错误,增加代码的不稳定性和可维护性。 边界测试:不进行边界测试可能导致代码在边界情况下出现错误或异常。 代码的可测试性:有些情况依赖于当前条件,使测试变得很难。

6.1 单元测试


单元测试是验证代码中最小可测试单元的方法,下面是不添加单元测试的案例:


def add_number(a, b):
return a + b

在这个示例中,我们没有进行单元测试来验证函数 add_number 的正确性。正确示例:


import unittest

def add_number(a, b):
return a + b

class TestAdd(unittest.TestCase):
def add_number(self):
self.assertEqual(add(2, 3), 5)

if __name__ == '__main__': unittest.main()

在这个示例中,我们使用了 unittest 模块进行单元测试,确保函数 add 的正确性。


6.2 边界测试


边界测试是针对输入的边界条件进行测试,以验证代码在边界情况下的行为下面是错误示例:


def is_even(n):
return n % 2 == 0

在这个示例中,我们没有进行边界测试来验证函数 is_even 在边界情况下的行为。正确示例:


import unittest

def is_even(n):
return n % 2 == 0

class TestIsEven(unittest.TestCase):
def test_even(self):
self.assertTrue(is_even(2))
self.assertFalse(is_even(3))

if __name__ == '__main__': unittest.main()

在这个示例中,我们使用了 unittest 模块进行边界测试,验证函数 is_even 在边界情况下的行为。


6.3 可测试性


代码的可测试性我们需要编写测试来验证代码的正确性。如果我们忽视了代码的可测试性,那么编写测试将会变得困难,甚至无法编写测试。例如:


def get_current_time():
return datetime.datetime.now()

这段代码的问题在于,它依赖于当前的时间,这使得我们无法编写确定性的测试。我们应该尽量减少代码的依赖,使得代码更易于测试。例如:


def get_time(now):
return now

这段代码不再依赖于当前的时间,而是通过参数传入时间,这使得我们可以编写确定性的测试。


07、性能优化



  • 错误的习惯


过度优化:过度优化可能会导致代码难以理解和维护,甚至可能会引入新的错误。 合适的数据结构:选择合适的数据结构可以提高代码的性能。

7.1 过度优化


我们往往会试图优化代码,使其运行得更快。然而,过度优化可能会导致代码难以理解和维护,甚至可能会引入新的错误。例如:


def sum(numbers):
return functools.reduce(operator.add, numbers)

这段代码的问题在于,它使用了 functools.reduce 和 operator.add 来计算列表的和,虽然这样做可以提高一点点性能,但是这使得代码难以理解。我们应该在保持代码清晰和易读的前提下,进行适度的优化。例如:


def sum(numbers):
return sum(numbers)

这段代码使用了内置的 sum 函数来计算列表的和,虽然它可能比上面的代码慢一点,但是它更清晰、易读。


7.2 没有使用合适的数据结构


选择合适的数据结构可以提高代码的性能。使用不合适的数据结构可能导致代码执行缓慢或占用过多的内存。例如:


def find_duplicate(numbers):
duplicates = []
for i in range(len(numbers)):
if numbers[i] in numbers[i+1:]:
duplicates.append(numbers[i])
return duplicates

在这个示例中,我们使用了列表来查找重复元素,但这种方法的时间复杂度较高。我们可以使用集合来查找元素。例如:


def find_duplicate(numbers):
duplicates = set()
seen = set()
for num in numbers:
if num in seen:
duplicates.add(num)
else:
seen.add(num)
return list(duplicates)

我们使用了集合来查找重复元素,这种方法的时间复杂度较低。


08、代码安全性



  • 错误的习惯


输入验证:不正确的输入验证可能导致安全漏洞,如 SQL 注入、跨站脚本攻击等。 密码存储:不正确的密码存储可能导致用户密码泄露。 权限控制:不正确的权限控制可能导致未经授权的用户访问敏感信息或执行特权操作。

8.1 输入验证


没有对用户输入进行充分验证和过滤可能导致恶意用户执行恶意代码或获取敏感信息。例如:


import sqlite3
def get_user(username):
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)
user = cursor.fetchone()
conn.close()
return user

在这个示例中,我们没有对用户输入的 username 参数进行验证和过滤,可能导致 SQL 注入攻击。正确示例:


import sqlite3

def get_user(username):
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
query = "SELECT * FROM users WHERE username = ?"
cursor.execute(query, (username,))
user = cursor.fetchone()
conn.close()
return user

在这个示例中,我们使用参数化查询来过滤用户输入,避免了 SQL 注入攻击。


8.2 不正确的密码存储


将明文密码存储在数据库或文件中,或使用不安全的哈希算法存储密码都是不安全的做法。错误示例:


import hashlib

def store_password(password):
hashed_password = hashlib.md5(password.encode()).hexdigest()
# 存储 hashed_password 到数据库或文件中

在这个示例中,我们使用了不安全的哈希算法 MD5 来存储密码。正确示例:


import hashlib
import bcrypt

def store_password(password):
hashed_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
# 存储 hashed_password 到数据库或文件中

在这个示例中,我们使用了更安全的哈希算法 bcrypt 来存储密码。


8.3 不正确的权限控制


没有正确验证用户的身份和权限可能导致安全漏洞。错误示例:


def delete_user(user_id):
if current_user.is_admin:
# 执行删除用户的操作
else:
raise PermissionError("You don't have permission to delete users.")

在这个示例中,我们只检查了当前用户是否为管理员,但没有进行足够的身份验证和权限验证。正确示例:


def delete_user(user_id):
if current_user.is_authenticated and current_user.is_admin:
# 执行删除用户的操作
else:
raise PermissionError("You don't have permission to delete users.")

在这个示例中,我们不仅检查了当前用户是否为管理员,还检查了当前用户是否已经通过身份验证。


09、版本控制和协作



  • 错误的习惯


版本提交信息:不合理的版本提交信息会造成开发人员难以理解和追踪代码的变化。 忽略版本控制和备份:没有备份代码和版本控制的文件可能导致丢失代码、难以追溯错误来源和无法回滚等问题。

9.1 版本提交信息


不合理的版本提交信息可能导致代码丢失、开发人员难以理解等问题。错误示例:


git commit -m "Fixed a bug"

在这个例子中,提交信息没有提供足够的上下文和详细信息,导致其他开发人员难以理解和追踪代码的变化。正确的做法是提供有意义的提交信息,例如:


$ git commit -m "Fixed a bug in calculate function, which caused grade calculation for scores below 60"

通过提供有意义的提交信息,我们可以更好地追踪代码的变化,帮助其他开发人员理解和维护代码。


9.2 忽略版本控制和备份


忽略使用版本控制工具进行代码管理和备份是一个常见的错误。错误示例:


$ mv important_code.py important_code_backup.py
$ rm important_code.py

在这个示例中,开发者没有使用版本控制工具,只是简单地对文件进行重命名和删除,没有进行适当的备份和记录。正确示例:


$ git clone project.git
$ cp important_code.py important_code_backup.py
$ git add .
$ git commit -m "Created backup of important code"
$ git push origin master
$ rm important_code.py

在这个示例中,开发者使用了版本控制工具进行代码管理,并在删除之前创建了备份,确保了代码的安全性和可追溯性。


10、总结


好的代码应该如同一首好文,让人爱不释手。优雅的代码,不仅是功能完善,更要做好每一个细节。


最后,引用韩磊老师在《代码整洁之道》写到的一句话送给大家:



细节之中自有天地,整洁成就卓越代码。


以上是本文全部内容,欢迎分享。


-End-


原创作者|孔垂航


技术责编|刘银松


作者:腾讯云开发者
来源:juejin.cn/post/7257894053902565433
收起阅读 »

听说你会架构设计?来,解释一下为什么错不在李佳琦

1. 引言 大家好,我是小❤,一个漂泊江湖多年的 985 非科班程序员,曾混迹于国企、互联网大厂和创业公司的后台开发攻城狮。 1.1 带货风波 近几天,“带货一哥” 李佳琦直播事件闹得沸沸扬扬,稳占各大新闻榜单前 10 名。 图来源:微博热点,侵删 虽然小❤...
继续阅读 »

1. 引言


大家好,我是小❤,一个漂泊江湖多年的 985 非科班程序员,曾混迹于国企、互联网大厂和创业公司的后台开发攻城狮。


1.1 带货风波


近几天,“带货一哥” 李佳琦直播事件闹得沸沸扬扬,稳占各大新闻榜单前 10 名。



图来源:微博热点,侵删


虽然小❤平时很少看直播,尤其是带货直播。


但奈何不住吃瓜的好奇心重啊!于是就趁着休息的功夫了解了一下,原来这场风波事件起源于前几天的一场直播。


当时,李佳琦在直播间介绍合作产品 “花西子” 眉笔的价格为 79 元时,有网友在评论区吐槽越来越贵了。他直言:哪里贵了?这么多年都是这个价格,不要睁着眼睛乱说,国货品牌很难的,哪里贵了?



图来源:网络,侵删


之后,李佳琦接着表示:有的时候找找自己原因,这么多年了工资涨没涨,有没有认真工作?



图来源:互联网,侵删


小❤觉得,这件事评论区网友说的没错,吐槽一下商品的价格有什么问题呢?我自己平时买菜还挑挑拣拣的,能省一毛是一毛。


毕竟,这个商品的价格也摆在那是不?



图来源:微博热点,侵删


1.2 身份决定立场,立场决定言论


但是,有一说一,从主播的角度呢,我也能理解。毕竟,不同的消费能力,说着自己立场里认可的大实话,也没啥问题。


那问题出在哪呢?


咳咳,两边都没问题,那肯定是评论系统有问题!


一边是年收入十多亿的带货主播,一边是普普通通的老百姓,你评论区为啥不甄别出用户画像,再隔离一下评论?


俗话说:“屁股决定脑袋”,立场不同,言论自然不一样。所以,这个锅,评论系统背定了!


2. 评论系统的特点


正巧,前几天在看关于评论系统的设计方案,且这类架构设计在互联网大厂的面试里出现的频率还是挺高的。所以我们今天就来探讨一下这个热门话题——《海量评论系统的架构设计》。


2.1 需求分析


首先,让我们来了解一下评论系统的特点和主要功能需求。评论系统是网站和应用中不可或缺的一部分,主要分为两种:



  • 一种是列表平铺式,只能发起评论,不能回复;

  • 一种是盖楼式评论,支持无限盖楼回复,可以回复用户的评论。


为了迎合目前大部分网站和应用 App 的需求,我们设计的评论系统采用盖楼式评论


需要满足以下几个功能需求:



评论系统中的观众和主播相当于用户和管理员的角色,其中观众用户可以:



  • 评论发布和回复:用户可以轻松发布评论,回复他人的评论。

  • 点赞和踩:用户可以给评论点赞或踩,以表达自己的喜好。

  • 评论拉取:评论需要按照时间或热度排序,并且支持分页显示。


主播可以:




  • 管理评论:主播可以根据直播情况以及当前一段时间内的总评论数,来判断是否打开 “喜好开关”。




  • 禁言用户:当用户发布了不当言论,或者恶意引流时,主播可以禁言用户一段时间。




  • 举报/删除:系统需要支持主播举报不当评论,并允许主播删除用户的评论。




2.2 非功能需求


除了功能需求,评论系统还需要满足一系列非功能需求,例如应对高并发场景,在海量数据中如何保证系统的稳定运行是一个巨大的挑战。




  • 海量数据:拿抖音直播举例,10 亿级别的用户量,日活约 2 亿,假设平均每 10 个人/天发一条评论,总评论数约 2 千万/天;




  • 高并发量:每秒十万级的 QPS 访问,每秒万级的评论发布量;




  • 用户分布不均匀:某个直播间的用户或者评论区数量,超出普通用户几个数量级;




  • 时间分布不均匀:某个主播可能突然在某个时间点成为热点用户,其评论数量也可能陡增几个数量级。




3. 系统设计


评论系统也具有一个典型社交类系统的特征,可归结为三点:海量数据,高访问量,非均匀性,接下来我们将对评论系统的关键特点和需求做功能设计。


3.1 功能设计


在直播平台或评论系统里,观众可以接收开通提醒,并且评论被回复之后也可以通过手机 App 收到回复消息,所以需要和系统建立 TCP 长连接。


同样地,主播由于要实时上传视频直播流,所以也需要 TCP 连接。架构图如下:



用户或主播上线时,如果是第一次登录,需要从用户长连接管理系统申请一个 TCP 服务器地址信息,然后进行 TCP 连接



不了解 TCP 连接的同学可以看我之前写的这篇文章:听说你会架构设计?来,弄一个打车系统



当观众或主播(统称用户)第一次登录,或者和服务器断开连接(比如服务器宕机、用户切换网络、后台关闭手机 App 等),需要重连时,用户可以通过用户长连接管理系统重新申请一个 TCP 服务器地址(可用地址存储在 Zookeeper 中),拿到 TCP 地址后再发起请求连接到集群的某一台服务器上。


用户系统


用户系统的用户表记录了主播和观众的个人信息,包括用户名、头像和地理位置等信息。


除此之外,用户还需要记录关注信息,比如某个用户关注了哪些直播间。


用户表(user)设计如下:




  • user_id:用户唯一标识




  • name:用户名




  • portrait:头像压缩存储




  • addr:地理位置




  • role:用户角色,观众或主播




直播系统


每次开播后,直播系统通过拉取直播流,和主播设备建立 TCP 长连接。这时,直播系统会记录直播表(live)信息,包括:




  • live_id:一场直播的唯一标识




  • live_room_id:直播间的唯一标识




  • user_id:主播用户ID




  • title:直播主题




参考微博的关注系统,我们可以引入用户关注表(attention),以便用户可以关注直播间信息,并接收其动态和评论通知:



  • user_id:关注者的用户ID。

  • live_room_id:被关注者的直播间ID。


这个表可以用于构建用户和主播之间的社交网络,并实现评论的动态通知。


用户关系表的设计可以支持关注、取消关注和获取关注列表等功能。


在数据库中,使用索引可以提高关系查询的性能。同时,可以定期清理不活跃的关系,以减少存储和维护成本。


评论系统


参考微博的评论系统,我们可以支持多级嵌套评论,让用户能够回复特定评论。


对于嵌套评论的存储,我们可以使用递归结构或层次结构的数据库设计,也可以使用关系型数据库表结构。评论表(comment)字段如下:



  • comment_id:评论唯一标识符,主键。

  • user_id:评论者的用户ID。

  • content:评论内容,可以是文本或富文本。

  • timestamp:评论时间戳。

  • parent_comment_id:如果是回复评论,记录被回复评论的comment_id。

  • live_id:评论所属的直播ID。

  • level:评论级别,用于标识评论的嵌套层级。


除此之外,我们可以根据业务需求添加一些额外字段:如点赞数、踩数、举报数等,以支持更多功能。


推送系统


为了提供及时的评论通知,我们可以设计消息推送系统,当用户收到关注直播间开播,或者有新评论或回复时,系统可以向其发送通知。


通知系统需要支持消息的推送和处理,当直播间关注人数很多或者用户发出了热点评论时,为了保证系统稳定,可以使用消息队列来处理异步任务


此外,在推送时需要考虑消息的去重、过期处理和用户偏好设置等方面的问题。


3.2 性能和安全


除了最基本的功能设计以外,我们还需要结合评论系统的数据量和并发量,考虑如何解决高并发、高性能以及数据安全的问题。


1)高并发处理


评论系统面临着巨大的并发压力,数以万计的用户可能同时发布和查看评论。为了应对这个挑战,我们可以采取以下策略。


分布式架构



采用分布式集群架构,将流量分散到多个服务器上,降低单点故障风险,提升用户的性能体验。


消息队列


引入消息队列,如 Kafka,来处理异步任务。



当直播间开播时,首先获取到关注该直播间的用户,然后将直播间名称、直播主题等信息,放入消息队列。


消息推送系统实时监听消息队列,当获取到开播提醒的 Topic 时,首先从 Redis 获取和用户连接的 TCP 服务器信息,然后将开播消息推送到用户手机上


同样地,当用户评论被回复时,将评论用户名和评论信息通过消息推送系统,也推送到用户手机上。


使用消息队列一方面可以减轻服务器的流量负担,另一方面可以根据用户离线情况,消息推送系统可以将历史消息传入延时队列,当用户重新上线时去拉取这些历史消息,以此提升用户体验。


数据缓存


引入缓存层,如 Redis,用于缓存最新的评论数据,以此减轻数据库负载并提升响应速度。例如,可以根据 LRU 策略缓存直播间最热的评论、用户地理位置等信息,并定时更新。


2)安全和防护


评论系统需要应对敏感词汇、恶意攻击等安全威胁。我们可以采取以下防护措施:


文字过滤


使用文字过滤技术,过滤垃圾评论和敏感词汇。实现时,可以用 Redis 缓存或者布隆过滤器。对比性能,我们这里采用布隆过滤器来实现。


布隆过滤器(Bloom Filter)是一个巧妙设计的数据结构,它的原理是将一个值多次哈希,映射到不同的 bit 位上并记录下来。


当新的值使用时,通过同样的哈希函数,比对各个 bit 位上是否有值:如果这些 bit 位上都没有值,说明这个数不存在;否则,就大概率是存在的。



以上图为例,具体操作流程为:



  1. 假设敏感词汇有 3 个元素{菜狗,尼玛,撒币},哈希函数的个数也设置为 3。我们首先将位数组初始化,将每个位都置为 0。



  1. 然后将集合里的敏感词语通过 3 个哈希函数进行映射,每次映射都会产生一个哈希值,即位数组里的 1.



  1. 当查询词语是否为敏感文字时,用相同的哈希函数进行映射,如果映射的位置有一个不为 1,说明该文字一定不存在于集合元素中。反之,如果 3 个点都为 1,则判定元素存在于集合中。


当然,这可能会产生误判,布隆过滤器一定可以发现重复的值,但也可能将不重复的值判断为重复值。如上图中的 “天气”,虽然都命中了 1,但是它并没有存在于敏感词集合里。


布隆过滤器在处理大量数据时非常有用,比如网页缓存、拼写检查、黑名单过滤等。虽然它有一定的误判率(约为 0.05%),但是其判重的速度和节省空间的优点足以瑕不掩瑜。


用户限制


除了从评论信息上加以限制,我们也可以从用户侧来限制:



  • 用户认证:要求用户登录后才能发布评论,降低匿名评论的风险。

  • 评论限制:根据用户 ID 和直播 ID 进行限流,比如让用户在一分钟之内最多只能发送 10 条的评论。



不知道如何限流的,可以看小❤之前的这篇文章:若我问到高可用,阁下又该如何应对呢?



4. 李佳琦该如何应对?


4.1 文本分析和情感分析


除了可以用布隆过滤器检测出恶意攻击和敏感内容,我们还可以引入文本分析和情感分析技术,使用自然语言处理(NLP)算法来检测不当评论。


并且,通过分析用户的评论内容,可以进行情感分析,以了解用户的情感倾向。



除了算法模块,我们还需要新增一个评论采集系统,定期(比如每天)从数据库里拉取用户的评论数据,传入对象存储服务。


算法模块监听对象存储服务,每天实时拉取训练数据,并获取可用的情感分析和语义理解模型。


当有新的评论出现时,会先调用算法模型,然后根据情感的分析结果来存储评论信息。我们可以在评论表(comment)里面新增一个表示情感正负倾向的字段 emotion,当主播打开喜好开关后,只拉取 emotion 为 TRUE 的评论信息,将“嫌贵的用户”或者 “评价为负面” 的评论设置为不可见。


这样,每次直播时,主播看到的都是情感正向且说话好听的评论,不仅能提升直播激情,还能增加与 “真爱粉” 的互动效果,可谓一箭三雕 🐶


但是,评论调用算法模型势必会牺牲一定的实时性与互动效果,主播也可以在开启直播时可以自己决定是否要打开评论喜好设置,并告知打开后评论会延时一段时间。


4.2 机器学习和推荐算法


除了从主播的角度,评论系统还可以引入机器学习算法来分析用户行为,根据用户的历史评论和喜好。


从观众来说,这可以提高观众的参与度和留存率,增强用户粘性。


从主播来说,可以筛选出真爱粉,脑残粉,甚至死亡芭比粉 🐶。这样,每次主播在直播时,只筛选一部分用户可以发表评论,其余的统统禁言,或者设置为不看用户评论。



除了直播领域,社交领域也经常使用推荐算法来获取评论内容。比如之前有 B 站 UP 主爆出:小红书在同一个帖子下,对女性用户和男性用户展示的评论区是不一样的,甚至评论区是截然相反的观点。


这个小❤没有试验过,大家不妨去看一下😃


5. 小结


目前,评论系统随着移动互联网的直播和社交平台规模不断扩大,许多网站和应用已经实现了社交媒体集成,允许用户使用他们的社交媒体帐户进行评论,增加了互动性和用户参与度。


一些平台也开始使用机器学习和人工智能技术来提供个性化评论推荐,以改善用户体验。


总的来说,评论系统是在线社交和内容互动的重要组成部分,希望看过这篇文章之后,大家以后知道如何应对类似的公关危机,到时候记得回来给我点赞。


什么?你想现在就分享、点赞,加入在看啊!


那你一定是社交领域的优质用户,如果直播间都是你这样的观众,评论系统设计成什么样已经不重要了!Love And Peace ❤



当然,前提是老板们都得时刻反思找找自己的原因,这么多年了有没有认真工作,有没有给打工人涨涨工资 🐶



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

一个烂分页,踩了三个坑!

你好呀,我是歪歪。 前段时间踩到一个比较无语的生产 BUG,严格来说其实也不能算是 BUG,只能说开发同事对于业务同事的需求理解没有到位。 这个 BUG 其实和分页没有任何关系,但是当我去排查问题的时候,我看了一眼 SQL ,大概是这样的: select *...
继续阅读 »

你好呀,我是歪歪。


前段时间踩到一个比较无语的生产 BUG,严格来说其实也不能算是 BUG,只能说开发同事对于业务同事的需求理解没有到位。


这个 BUG 其实和分页没有任何关系,但是当我去排查问题的时候,我看了一眼 SQL ,大概是这样的:



select * from table order by priority limit 1;



priority,就是优先级的意思。


按照优先级 order by 然后 limit 取优先级最高(数字越小,优先级越高)的第一条 ,结合业务背景和数据库里面的数据,我立马就意识到了问题所在。


想起了我当年在写分页逻辑的时候,虽然场景和这个完全不一样,但是踩过到底层原理一模一样的坑,这玩意印象深刻,所以立马就识别出来了。


借着这个问题,也盘点一下我遇到过的三个关于分页查询有意思的坑。


职业生涯的第一个生产 BUG


歪师傅职业生涯的第一个生产 BUG 就是一个小小的分页查询。


当时还在做支付系统,接手的一个需求也很简单就是做一个定时任务,定时把数据库里面状态为初始化的订单查询出来,调用另一个服务提供的接口查询订单的状态并更新。


由于流程上有数据强校验,不用考虑数据不存在的情况。所以该接口可能返回的状态只有三种:成功,失败,处理中。


很简单,很常规的一个需求对吧,我分分钟就能写出伪代码:


//获取订单状态为初始化的数据(0:初始化 1:处理中 2:成功 3:失败)
//select * from order where order_status=0;
ArrayList initOrderInfoList = queryInitOrderInfoList();
//循环处理这批数据
for(OrderInfo orderInfo : initOrderInfoList){
    //捕获异常以免一条数据错误导致循环结束
    try{
        //发起rpc调用
        String orderStatus = queryOrderStatus(orderInfo.getOrderId);
        //更新订单状态
        updateOrderInfo(orderInfo.getOrderId,orderStatus);    
    } catch (Exception e){
        //打印异常
    }
}

来,你说上面这个程序有什么问题?



其实在绝大部分情况下都没啥大问题,数据量不多的情况下程序跑起来没有任何毛病。


但是,如果数据量多起来了,一次性把所有初始化状态的订单都拿出来,是不是有点不合理了,万一把内存给你撑爆了怎么办?


所以,在我已知数据量会很大的情况下,我采取了分批次获取数据的模式,假设一次性取 100 条数据出来玩。


那么 SQL 就是这样的:



select * from order where order_status=0 order by create_time limit 100;



所以上面的伪代码会变成这样:


while(true){
    //获取订单状态为初始化的数据(0:初始化 1:处理中 2:成功 3:失败)
    //select * from order where order_status=0 order by create_time limit 100;
    ArrayList initOrderInfoList = queryInitOrderInfoList();
    //循环处理这批数据
    for(OrderInfo orderInfo : initOrderInfoList){
        //捕获异常以免一条数据错误导致循环结束
        try{
            //发起rpc调用
            String orderStatus = queryOrderStatus(orderInfo.getOrderId);
            //更新订单状态
            updateOrderInfo(orderInfo.getOrderId,orderStatus);    
        } catch (Exception e){
            //打印异常
        }
    }
}

来,你又来告诉我上面这一段逻辑有什么问题?



作为程序员,我们看到 while(true) 这样的写法立马就要警报拉满,看看有没有死循环的风险。


那你说上面这段代码在什么时候退不出来?


当有任何一条数据的状态没有从初始化变成成功、失败或者处理中的时候,就会导致一直循环。


而虽然发起 RPC 调用的地方,服务提供方能确保返回的状态一定是成功、失败、处理中这三者之中的一个,但是这个有一个前提是接口调用正常的情况下。


如果接口调用一旦异常,那么按照上面的写法,在抛出异常后,状态并未发生变化,还会是停留在“初始化”,从而导致死循环。


当年,测试同学在测试阶段直接就测出了这个问题,然后我对其进行了修改。


我改变了思路,把每次分批次查询 100 条数据,修改为了分页查询,引入了 PageHelper 插件:


//是否是最后一页
while(pageInfo.isLastPage){
    pageNum=pageNum+1;
    //获取订单状态为初始化的数据(0:初始化 1:处理中 2:成功 3:失败)
    //select * from order where order_status=0 order by create_time limit pageNum*100,100;
    PageHelper.startPage(pageNum,100);
    ArrayList initOrderInfoList = queryInitOrderInfoList();
    pageInfo = new PageInfo(initOrderInfoList);
    //循环处理这批数据
    for(OrderInfo orderInfo : initOrderInfoList){
        //捕获异常以免一条数据错误导致循环结束
        try{
            //发起rpc调用
            String orderStatus = queryOrderStatus(orderInfo.getOrderId);
            //更新订单状态
            updateOrderInfo(orderInfo.getOrderId,orderStatus);    
        } catch (Exception e){
            //打印异常
        }
    }
}

跳出循环的条件为判断当前页是否是最后一页。


由于每循环一次,当前页就加一,那么理论上讲一定会是翻到最后一页的,没有任何毛病,对不对?


我们可以分析一下上面的代码逻辑。


假设,我们有 120 条 order_status=0 的数据。


那么第一页,取出了 100 条数据:



SELECT * from order_info WHERE order_status=0 LIMIT 0,100;



这 100 条处理完成之后,第二页还有数据吗?


第二页对应的 sql 为:



SELECT * from order_info WHERE order_status=0 LIMIT 100,100;



但是这个时候,状态为 0 的数据,只有 20 条了,而分页要从第 100 条开始,是不是获取不到数据,导致遗漏数据了?


确实一定会翻到最后一页,解决了死循环的问题,但又有大量的数据遗漏怎么办呢?



当时我苦思冥想,想到一个办法:导致数据遗漏的原因是因为我在翻页的时候,数据状态在变化,导致总体数据在变化。


那么如果我每次都从后往前取数据,每次都固定取最后一页,能取到数据就代表还有数据要处理,循环结束条件修改为“当前页即是第一页,也是最后一页时”就结束,这样不就不会遗漏数据了?


我再给你分析一下。


假设,我们有 120 条 order_status=0 的数据,从后往前取了 100 天出来进行出来,有 90 条处理成功,10 条的状态还是停留在“处理中”。


第二次再取的时候,会把剩下的 20 条和这次“处理中”的 10 条,共计 30 条再次取出来进行处理。


确保没有数据遗漏。


后来测试环节验收通过了,这个方案上线之后,也确实没有遗漏过数据了。


直到后来又一天,提供 queryOrderStatus 接口的服务异常了,我发过去的请求超时了。


导致我取出来的数据,每一条都会抛出异常,都不会更新状态。从而导致我每次从后往前取数据,都取到的是同一批数据。


从程序上的表现上看,日志疯狂的打印,但是其实一直在处理同一批,就是死循环了。


好在我当时还在新手保护期,领导帮我扛下来了。


最后随着业务的发展,这块逻辑也完全发生了变化,逻辑由我们主动去调用 RPC 接口查询状态变成了,下游状态变化后进行 MQ 主动通知,所以我这一坨骚代码也就随之光荣下岗。


我现在想了一下,其实这个场景,用分页的思想去取数据真的不好做。


还不如用最开始的分批次的思想,只不过在会变化的“状态”之外,再加上另外一个不会改变的限定条件,比如常见的创建时间:



select * from order where order_status=0 and create_time>xxx order by create_time limit 100;



最好不要基于状态去做分页,如果一定要基于状态去做分页,那么要确保状态在分页逻辑里面会扭转下去。


这就是我职业生涯的第一个生产 BUG,一个低级的分页逻辑错误。


还是分页,又踩到坑


这也是在工作的前两年遇到的一个关于分页的坑。


最开始在学校的时候,大家肯定都手撸过分页逻辑,自己去算总页数,当前页,页面大小啥的。


当时功力尚浅,觉得这部分逻辑写起来是真复杂,但是扣扣脑袋也还是可以写出来。


后来参加工作了之后,在项目里面看到了 PageHelper 这个玩意,了解之后发了“斯国一”的惊叹:有了这玩意,谁还手写分页啊。



但是我在使用 PageHelper 的时候,也踩到过一个经典的“坑”。


最开始的时候,代码是这样的:


PageHelper.startPage(pageNum,100);
List<OrderInfo> list = orderInfoMapper.select(param1);

后来为了避免不带 where 条件的全表查询,我把代码修改成了这样:


PageHelper.startPage(pageNum,100);
if(param != null){
    List<OrderInfo> list = orderInfoMapper.select(param);
}

然后,随着程序的迭代,就出 BUG 了。因为有的业务场景下,param 参数一路传递进来之后就变成了 null。


但是这个时候 PageHelper 已经在当前线程的 ThreadLocal 里面设置了分页参数了,但是没有被消费,这个参数就会一直保留在这个线程上,也就是放在线程的 ThreadLocal 里面。


当这个线程继续往后跑,或者被复用的时候,遇到一条 SQL 语句时,就可能导致不该分页的方法去消费这个分页参数,产生了莫名其妙的分页。


所以,上面这个代码,应该写成下面这个样子:


if(param != null){
    PageHelper.startPage(pageNum,100);
    List<OrderInfo> list = orderInfoMapper.select(param);
}

也是这次踩坑之后,我翻阅了 PageHelper 的源码,了解了底层原理,并总结了一句话:需要保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,否则会污染线程。


在正确使用 PageHelper 的情况下,其插件内部,会在 finally 代码段中自动清除了在 ThreadLocal 中存储的对象。


这样就不会留坑。


这次翻页源码的过程影响也是比较深刻的,虽然那个时候经验不多,但是得益于 MyBatis 的源码和 PageHelper 的源码写的都非常的符合正常人的思维,阅读起来门槛不高,再加上我有具体的疑问,所以那是一次古早时期,尚在新手村时,为数不多的,阅读源码之后,感觉收获满满的经历。


分页丢数据


关于这个 BUG 可以说是印象深刻了。


当年遇到这个坑的时候排查了很长时间没啥头绪,最后还是组里的大佬指了条路。


业务需求很简单,就是在管理页面上可以查询订单列表,查询结果按照订单的创建时间倒序排序。


对应的分页 SQL 很简单,很常规,没有任何问题:



select * from table order by create_time desc limit 0,10;



但是当年在页面上的表现大概是这样的:



订单编号为 5 的这条数据,会同时出现在了第一页和第二页。


甚至有的数据在第二页出现了之后,在第五页又出现一次。


后来定位到产生这个问题的原因是因为有一批数量不小的订单数据是通过线下执行 SQL 的方式导入的。


而导入的这一批数据,写 SQL 的同学为了方便,就把 create_time 都设置为了同一个值,比如都设置为了 2023-09-10 12:34:56 这个时间。


由于 create_time 又是我作为 order by 的字段,当这个字段的值大量都是同一个值的时候,就会导致上面的一条数据在不同的页面上多次出现的情况。


针对这个现象,当时组里的大佬分析明白之后,扔给我一个链接:



dev.mysql.com/doc/refman/…



这是 MySQL 官方文档,这一章节叫做“对 Limit 查询的优化”。


开篇的时候人家就是这样说的:



如果将 LIMIT row_count 和 ORDER BY 组合在一起,那么 MySQL 在找到排序结果的第一行 count 行时就停止排序,而不是对整个结果进行排序。


然后给了这一段补充说明:



如果多条记录的 ORDER BY 列中有相同的值,服务器可以自由地按任何顺序返回这些记录,并可能根据整体执行计划的不同而采取不同的方式。


换句话说,相对于未排序列,这些记录的排序顺序是 nondeterministic 的:



然后官方给了一个示例。


首先,不带 limit 的时候查询结果是这样的:



基于这个结果,如果我要取前五条数据,对应的 id 应该是 1,5,3,4,6。


但是当我们带着 limit 的时候查询结果可能是这样的:



对应的 id 实际是 1,5,4,3,6。


这就是前面说的:如果多条记录的 ORDER BY 列中有相同的值,服务器可以自由地按任何顺序返回这些记录,并可能根据整体执行计划的不同而采取不同的方式。


从程序上的表现上来看,结果就是 nondeterministic。


所以看到这里,我们大概可以知道我前面遇到的分页问题的原因是因为那一批手动插入的数据对应的 create_time 字段都是一样的,而 MySQL 这边又对 Limit 参数做了优化,运行结果出现了不确定性,从而页面上出现了重复的数据。


而回到文章最开始的这个 SQL,也就是我一眼看出问题的这个 SQL:



select * from table order by priority limit 1;



因为在我们的界面上,只是约定了数字越小优先级越高,数字必须大于 0。


所以当大家在输入优先级的时候,大部分情况下都默认自己编辑的数据对应的优先级最高,也就是设置为 1,从而导致数据库里面有大量的优先级为 1 的数据。


而程序每次处理,又只会按照优先级排序只会,取一条数据出来进行处理。


经过前面的分析我们可以知道,这样取出来的数据,不一定每次都一样。


所以由于有这段代码的存在,导致业务上的表现就很奇怪,明明是一模一样的请求参数,但是最终返回的结果可能不相同。


好,现在,我问你,你说在前面,我给出的这样的分页查询的 SQL 语句有没有毛病?



select * from table order by create_time desc limit 0,10;



没有任何毛病嘛,执行结果也没有任何毛病?


有没有给你按照 create_time 排序?


摸着良心说,是有的。


有没有给你取出排序后的 10 条数据?


也是有的。


所以,针对这种现象,官方的态度是:我没错!在我的概念里面,没有“分页”这样的玩意,你通过组合我提供的功能,搞出了“分页”这种业务场景,现在业务场景出问题了,你反过来说我底层有问题?


这不是欺负老实人吗?我没错!



所以,官方把这两种案例都拿出来,并且强调:



在每种情况下,查询结果都是按 ORDER BY 的列进行排序的,这样的结果是符合 SQL 标准的。




虽然我没错,但是我还是可以给你指个路。


如果你非常在意执行结果的顺序,那么在 ORDER BY 子句中包含一个额外的列,以确保顺序具有确定性。


例如,如果 id 值是唯一的,你可以通过这样的排序使给定类别值的行按 id 顺序出现。


你这样去写,排序的时候加个 id 字段,就稳了:



好了,如果觉得本文对你有帮助的话,求个免费的点赞,不过分吧?


作者:why技术
来源:juejin.cn/post/7277187894870671360
收起阅读 »

懂点心理学 - 奶头乐效应

Ivy:今天事情真多,有点小沮丧。 Jimmy:要不一起玩局游戏 Ivy:赞同 然后,游戏一局接着一局玩 🐶 囧 奶头乐是什么 奶头乐泛指那一类让人着迷、低成本又使人满足的低俗娱乐内容。奶头乐理论是用来描述一个设想:由于生产力的不断上升,世界上的一大部...
继续阅读 »

  • Ivy:今天事情真多,有点小沮丧。

  • Jimmy:要不一起玩局游戏

  • Ivy:赞同



然后,游戏一局接着一局玩 🐶



pexels-cottonbro-studio-3945683.jpg



奶头乐是什么


奶头乐泛指那一类让人着迷、低成本又使人满足的低俗娱乐内容。奶头乐理论是用来描述一个设想:由于生产力的不断上升,世界上的一大部分人口将会不用也无法积极参与产品和服务的生产,为了安慰这些人,他们的生活应该被大量的娱乐活动(比如网络、电视和游戏)填满。



奶头乐 - 英文 tittytainmenttitty(奶头)与 entertainment(娱乐)的组合。



奶头乐的应用


奶头乐在我们的生活中扮演着重要的角色,有消极的作用,也有积极的作用。问题在于,我们应该怎么趋利避害?


最近很火的某音秀才和一笑倾城事件,关注的中老年的都开始幻想着如意郎君和贤惠姨婆,这可害惨了 TA 们。本来就是在闲暇时候看的小段子打发打发时间,不料,都变成精神寄托了,深陷泥潭的不在少数...


1080x2267_64f6cf1f8faaf.jpeg


但是,我们也可以把奶头乐的一些属性(比如让人着迷)玩成有利于我们的发展,比如玩具模型组装:工作了一周时间,存够了薪水,为自己买了一份乐高 - 法国巴黎铁塔。在娱乐的同时,又很好地锻炼了我们的动手能力和记忆力。


法国巴黎积木.png


奶头乐效应,可爱但又可恨。衡量它的好坏,就看站在哪个角度来看。然而,趋利避害才是我们在深陷奶头乐效应的时候,需要清醒认识(但是很难,往往是奶头乐之后,才会清醒认识)。


参考



作者:Jimmy
来源:juejin.cn/post/7276694924136087586
收起阅读 »

说说今年的秋招的情况与感受

目前秋招已经过了一段时间,说下我看到的情况和整体感受,仅限于计算机相关专业。 另外,我感受秋招的视角比较特别,今年做了半年的Java面试辅导副业,我是以一种“秋招面试陪跑”的”准老师“身份切入的。 时隔五年,又跟着十几个同学重新经历了一次秋招,有点儿“爷青回”...
继续阅读 »

目前秋招已经过了一段时间,说下我看到的情况和整体感受,仅限于计算机相关专业。


另外,我感受秋招的视角比较特别,今年做了半年的Java面试辅导副业,我是以一种“秋招面试陪跑”的”准老师“身份切入的。


时隔五年,又跟着十几个同学重新经历了一次秋招,有点儿“爷青回”的感觉。


对比去年,依然普天同庆(ai hong bian ye)


很多同学觉得今年疫情阴霾散去,经济复苏,形势一片转好,他们甚至动了冲一冲大厂的念头。但真的到了秋招的时候才发现,24秋招 = 没有迪子的23秋招,转而发下重誓,不进体制誓不为人。


不得不说,那些年,我们一起甩锅的疫情,这次终于自证清白了。


其实,经历了过往十年移动互联网的高速发展,目前流量红利已经见顶,进入到了增长受限的存量时代,这些互联网大厂对于人才的需求远不如以前那么强烈了,甚至从去年就开始一波又一波的“去肥增瘦”。



说完了需求侧,我们再用一张图来说说供给侧,2024年高校毕业生人数达到了1187万人,大概是1000万多点儿的国内毕业生和100多万海归毕业生的总盘子。


理性地思考一下,需求侧的日益饱和 + 供给侧的井喷之势 + 选专业时的追涨杀跌 + 转码时的后知后觉,今年还能形势转好?


我信你个鬼!


再说几个方面的细节:




  • 今年无论你是多牛逼的硕,只要本科不是211 985,有的大厂连笔试机会都不给。




  • 今年的大厂面试官,会因为你没有大厂实习经历而挂掉你,不再培养优秀人才了,希望开箱即用。




  • 今年的技术面真难,如果你回答不好生产环境的压测方案,以及流量激增100倍的解决方案,会直接挂掉你。




  • 今年力扣的算法原题越来越少了,今年的八股文考查源码的越来越多了。




  • 今年貌似没有985保底公司。




  • 3个985本硕目前投了七八十家公司,约面的只有十家,其他的均显示“简历评估中”,感觉企业根本不着急。




你大爷还是你大爷


那种学历牛逼,有大厂实习经历,有参赛获奖经历,技术功底扎实行业内的牛逼人才,依然是大厂offer收割机。



对于这类同学,只要在面试准备期别走偏,只要能正常发挥应有水平,只要别中二地犯了面试官的忌讳,他们能完全摆脱大环境萧条的左右。


接下来他们要做的事情就是选择取舍了,有句话说得还是很有道理的,“选择大于努力,命运大于选择”。


强烈建议,其中的那些能力出众、足够努力,但不善于选择的小镇做题家们,这个时候一定要多问问人,做到谋定后动,行且坚毅。


颈部同学受影响最大


除非整个行业团灭,否则头部同学永远都是稳如泰山的,而离头部同学差一个档位的颈部同学,则受影响最大。


颈部同学的人物画像大概是:



  • 技术储备出色,也有实习经历,但学历并没那么出色的同学。

  • 985本硕,技术储备一般,无实习经历,项目经历出自黑马或尚硅谷的同学。

  • 211本硕,技术储备尚可,有些中小厂实习经历的同学。

  • 名校海归硕,技术储备与中国式校招不match,边吃凉面边扳正认知的同学。

  • 985本硕,技术储备出色,也有实习经历,但沟通能力存在硬伤的同学。


这类同学是过往互联网黄金十年、企业人才扩招的最大受益者,也是现在行业萧条的最大受害者,跟几年前的学长学姐进行比较,则成为了他们最大的精神内耗。


他们会被面试官花式吊打屡屡凉面,他们所泡的池子是汪洋大海,他们阅尽千帆归来却依然0 offer,他们拿到offer的档位和数量会直线下降,他们拿到offer的薪资也没能实现倒挂上届。



有人会说,如果颈部都被影响了,那中部和尾部的同学不是影响更大了吗?


这个未必,中尾部的同学没有那么强的比较心理,在性格上更加随遇而安和知足常乐,甚至早早做好了“大不了转行,干啥不是干”的准备。


这就验证了,忧天的往往不是杞人。


逆商和复盘能力的最好考查


高考虽然可以复读,但浪费一年大好时光的成本过于庞大,因此其“一战定天下”的属性更加强烈。


而秋招面试,你甚至可以在前面挂99次,但只要有一次面试发挥出色,拿到了心仪公司的offer,你就是100%成功的。


因此,在不断的“凉面”和“挂面”中保持心态平和,不抛弃不放弃,认真做好复盘总结,不断完善自己的知识体系,不断提升自己的认知层次,你下次的面试成功率是会叠加的。


记住,乾坤未定,你我皆是黑马,秋招是对逆商和复盘能力的最好考查。



一场与面试官的心理博弈


高考是拿到考卷后的解题模式,秋招虽然也有笔试,但其只是敲门砖,绝不是终极态。


终极态是在两三个小时的面试过程中,迅速得到几个陌生面试官的肯定与认可,这是有很多前期工作需要准备的,往往会涉及到候选人与面试官的心理猜析和博弈,面试话题和节奏控制与反控制。



如果你设计合理,那么面试官会在不知不觉中陷入到你提前安排好的布局中。


这里举一个简历当中项目选型的例子:


有些同学为了充分体现其做的项目有技术含量,硬往简历上放手写RPC框架、消息队列、分布式缓存、仿滴滴打车,仿MySQL RDBMS之类的。


这有些乍一看挺唬人,但其实给自己埋下了不小的坑。因为这种颇具技术含量的项目,最大的问题就是它的深度和广度不收敛,你很难hold住。


所以,除非你确实深谙此道,否则就等着被各家公司的面试官,以各种姿势花式吊打吧。


而那些聪明的,善于与面试官博弈的同学,早就准备好了几个有些难度、但技术深度和广度可控的项目。在面试的时候,他就可以顺理成章地将面试官带入到自己所熟悉的八股文技术点中,最终成为了offer收割机。


写给那些心态崩了的同学




  • 有人说,秋招让他明白,读书的目的只是为了换文凭;




  • 有人说,秋招让他明白,人真的要学会接受自己的普通,要学会取悦自己;




  • 有人说,秋招让他明白,倘若是因为读书而耽误了正事,那么读书就是玩物丧志;




  • 有人说,秋招是应届生的头等大事,一旦错过了秋招,我的人生完蛋了;




对于那些心态崩了的同学,我要说的是:


人生中最辉煌的时刻,绝对不是你功成名就的那天,而是你坠入绝望之谷后,重新燃起挑战人生的欲望,再次义无反顾地踏上征程的那天。


作者:库森学长
来源:juejin.cn/post/7279313746450530315
收起阅读 »

为什么5.225.toFixed(2)!=5.23,令人摸不着头脑的银行家舍入法

web
前言 很多时候,我们在程序中计算数字,得到的结果也许并不和我们想象得一样,在我们大多数人的认知里几乎都是四舍五入法,但是程序中所呈现的好像并不是我们想要的结果。 今天就谈谈程序中的那匪夷所思得银行家舍入法(也会涉及到数字精度问题) 什么是银行家舍入法 银行家舍...
继续阅读 »

前言


很多时候,我们在程序中计算数字,得到的结果也许并不和我们想象得一样,在我们大多数人的认知里几乎都是四舍五入法,但是程序中所呈现的好像并不是我们想要的结果。

今天就谈谈程序中的那匪夷所思得银行家舍入法(也会涉及到数字精度问题)


什么是银行家舍入法


银行家舍入法,也称为四舍六入五留双或四舍六入五成双,是一种在计算机科学和金融领域广泛使用的舍入方法。


具体操作步骤如下:



  1. 如果被修约的数字小于5,则直接舍去;

  2. 如果被修约的数字大于5,则进行进位;

  3. 如果被修约的数字等于5,则需要查看5前面的数字。如果5前面的数字是奇数,则进位;如果5前面的数字是偶数,则舍去5,即修约后末尾数字都成为偶数。特别需要注意的是,如果5的后面还有不为0的任何数,则无论5的前面是奇数还是偶数,均应进位。


以上可以看出银行家舍入法得规则,当为5时,并不是所有得都会向前进一位,所以就可以知道5.225.toFixed(2)为什么不等于5.23了


举例


在浏览器的控制台中,我们可以试着打印一下


image.png
这个时候我们可以看到,哎,好像是符合我们的所认知得四舍五入法了,但是紧接着


image.png
这里看出,怎么又变成这样的了,这还是银行家舍入法呀,为了更严谨再试一下5前面为奇数时得结果


image.png
这里结果又变了,反而是整数大于等于4得正常了,但是小于4得又有些失常了,反而整数为1得总是按照咱们预想的结果在进行,这种结果让我大脑一片混乱,所以这到底是什么原因,导致结果不像是银行家舍入法,也不像是四舍五入法


在我掉了一花西币的头发后,终于想通了,是程序中的精度问题,我们所写的数字并不是表面那么纯粹,再次打印一下看看


image.png
现在可以清楚看出,我们所写的简单的数字后面并不见简单,之所以1.235和1.225使用toFiexd的时候都准确的四舍五入了,都是因为他的后面是多出来了0.0000000000几的数字,然而2.235就没有那么幸运了,所以2.235的0.005就被舍弃了!


解决方法


先说一种可行但不完全可行的解决方法,就是使用Math.round()
首先这个方法确实是js中提供的真正含义上的四舍五入的方法。


image.png
哎,这么一看,确实可行,既然简单的可以,那我们就试着进行复杂运算一下,再保留一下两位小数试试看


image.png
呕吼,错了,按我们正常来算应该是9.77,但却得到了9.76。

要知道程序中存在着精度问题,再我们算来这个式子的结果应该是9.765,但是在程序看来


image.png
可以说是无限趋近于9.765但还没有达到,然后就在Math.round这个方法中给舍弃掉了,这个方法似乎不完全可行


那么另外一招就是可行但有隐式风险的方式,就是在我们所算出来的结果后面添加0.0000000001,这样再让我们看一下结果


image.png
这样可以看出,无论使用哪种方法,都能达到我们所需的结果了,即使使用toFixed有了银行家舍入法的规则,依旧可以按我们所想的一样进行四舍五入,因为当我们加了0.000000001后,即使最后一位等于5了,5后面还有数字,它就会向前进一位,那如果说加了这0.000000001正好等于5然后又触发了银行家舍入法的规则,那只能说算你倒霉,这就是我说为什么会有隐式风险,有风险但很小。


当然还有一个方法就是自己写一个方法来解决这个问题


//有的时候也许传的参数就是计算过后的,无线趋近于5的数,可以根据需求来判断是否传入第二个参数
Number.prototype.myToFixed = function (n, d) {
//进来之后转为字符串 字符串不存在精度问题
const str = this.toString();
const dotIndex = str.indexOf(".");
//如果没有小数点传进来的就是整数,直接使用toFixed传出去
if (dotIndex === -1) {
return this.toFixed(n);
}
//当为小数的时候
const intStr = str.substring(0, dotIndex);
const decStr = str.substring(dotIndex + 1, str.length).split("");
//当大于5时,就进一
if (decStr[n] >= 5) {
decStr[n - 1] = Number(decStr[n - 1]) + 1;
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
} else {
//否则小于五时 先判断是否有第二个参数
if (d) {
//如果有就截取到第二个参数的位置
const newDec = decStr.splice(n, n + d);
let nineSum = 0;
//遍历循环有多少个9
for (let index = 0; index < newDec.length; index++) {
if (index != 0 && newDec[index] == 9) {
nineSum++;
}
}
//判断四舍五入后面的位置 是否为四 并且是否除了4之后全是9 或者 9的位数大于第二个传的参数
if (newDec[0] == 4 && (nineSum >= newDec.length - 2 || nineSum >= d)) {
//条件成立 就按5进一
decStr[n - 1] = Number(decStr[n - 1]) + 1;
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
} else {
//不成立则舍一
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
}
} else {
//没有第二个参数,小于五直接舍一
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
}
}
};

我们再进行测试一下


image.png


image.png
这样就是我们想要的结果了


总结


在程序中,银行家舍入法和数字的精度问题很多时候都会遇见,不论前端还是后端,然而处理这些数据也是比较头疼的事,我所讲的这些也许不能满足所有情况,但大多数情况都是可以处理的。


如果是相对于银行里这种对数字比较敏感的环境,这些参数的处理还需要更加谨慎的处理


写的如有问题,欢迎提出建议


作者:iceCode
来源:juejin.cn/post/7280430881952759862
收起阅读 »

你知道抖音的IP归属地是怎么实现的吗

1.背景 最近刷抖音发现上线了 IP 属地的功能,小伙伴在发表动态、发表评论以及聊天的时候,都会显示自己的 IP 属地信息,其核心意义是让用户更具有真实性,减少虚假欺骗事件。正好最近本人开发获取客户端ip,做一些接口限流,黑白名单等需求功能,顺路就研究了一下怎...
继续阅读 »

1.背景


最近刷抖音发现上线了 IP 属地的功能,小伙伴在发表动态、发表评论以及聊天的时候,都会显示自己的 IP 属地信息,其核心意义是让用户更具有真实性,减少虚假欺骗事件。正好最近本人开发获取客户端ip,做一些接口限流,黑白名单等需求功能,顺路就研究了一下怎么解析IP获取归属地问题。


接下来,就着重讲解一下Java后端怎么实现IP归属地的功能,其实只需要以下两大步骤:


2.获取客户端ip接口


做过web开发都知道,无论移动端还是pc端的请求接口都会被封装成为一个HttpServletRequest对象,该对象包含了客户端请求信息包括请求的地址,请求的参数,提交的数据等等。


如果服务器直接把IP暴漏出去,那么request.getRemoteAddr()就能拿到客户端ip。


但目前流行的架构中,基本上服务器都不会直接把自己的ip暴漏出去,一般前面还有一层或多层反向代理,常见的nginx居多。 加了代理后,相当于服务器和客户端中间还有一层,这时·request.getRemoteAddr()拿到的就是代理服务器的ip了,并不是客户端的ip。所以这种情况下,一般会在转发头上加X-Forwarded-For等信息,用来跟踪原始客户端的ip。


X-Forwarded-For: 这是一个 Squid 开发的字段,只有在通过了HTTP代理或者负载均衡服务器时才会添加该项。 格式为X-Forwarded-For:client1,proxy1,proxy2,一般情况下,第一个ip为客户端真实ip,后面的为经过的代理服务器ip。 上面的代码注释也说的很清楚,直接截取拿到第一个ip。 Proxy-Client-IP/WL- Proxy-Client-IP: 这个一般是经过apache http服务器的请求才会有,用apache http做代理时一般会加上Proxy-Client-IP请求头,而WL-Proxy-Client-IP是他的weblogic插件加上的头。这种情况也是直接能拿到。 HTTP_CLIENT_IP: 有些代理服务器也会加上此请求头。 X-Real-IP: nginx一般用这个。


但是在日常开发中,并没有规范规定用以上哪一个头信息去跟踪客户端,所以都有可能,只能一一尝试,直到获取到为止。代码如下:


@Slf4j
public class IpUtils {

   private static final String UNKNOWN_VALUE = "unknown";
   private static final String LOCALHOST_V4 = "127.0.0.1";
   private static final String LOCALHOST_V6 = "0:0:0:0:0:0:0:1";

   private static final String X_FORWARDED_FOR = "X-Forwarded-For";
   private static final String X_REAL_IP = "X-Real-IP";
   private static final String PROXY_CLIENT_IP = "Proxy-Client-IP";
   private static final String WL_PROXY_CLIENT_IP = "WL-Proxy-Client-IP";
   private static final String HTTP_CLIENT_IP = "HTTP_CLIENT_IP";

   private static final String IP_DATA_PATH = "/Users/shepherdmy/Desktop/ip2region.xdb";
   private static  byte[] contentBuff;
 
  /**
    * 获取客户端ip地址
    * @param request
    * @return
    */
   public static String getRemoteHost(HttpServletRequest request) {
       String ip = request.getHeader(X_FORWARDED_FOR);
       if (StringUtils.isNotEmpty(ip) && !UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           // 多次反向代理后会有多个ip值,第一个ip才是真实ip
           int index = ip.indexOf(",");
           if (index != -1) {
               return ip.substring(0, index);
          } else {
               return ip;
          }
      }
       ip = request.getHeader(X_REAL_IP);
       if (StringUtils.isNotEmpty(ip) && !UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           return ip;
      }
       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getHeader(PROXY_CLIENT_IP);
      }
       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getHeader(WL_PROXY_CLIENT_IP);
      }
       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getRemoteAddr();
      }

       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getHeader(HTTP_CLIENT_IP);
      }

       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getRemoteAddr();
      }
       return ip.equals(LOCALHOST_V6) ? LOCALHOST_V4 : ip;
  }

}


项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用


Github地址github.com/plasticene/…


Gitee地址gitee.com/plasticene3…


微信公众号Shepherd进阶笔记


交流探讨群:Shepherd_126



3.获取ip归属地


通过上面我们就能获取到客户端用户的ip地址,接下来就可以通过ip解析获取归属地了。


如果我们在网上搜索资料教程,大部分都是说基于各大平台(eg:淘宝,新浪)提供的ip库进行查询,不过不难发现这些平台已经不怎么维护这个功能,现在处于“半死不活”的状态,根本不靠谱,当然有些平台提供可靠的获取ip属地接口,但是收费、收费、收费


本着作为一个程序员的严谨:“能白嫖的就白嫖,避免出现要买的是你,不会用也是你的尴尬遭遇”。扯远了言归正传,为了寻求可靠有效的解决方案,只能去看看github有没有什么项目能满足需求,果然功夫不负有心人,发现一个宝藏级项目:ip2region,一个准确率 99.9% 的离线 IP 地址定位库,0.0x 毫秒级查询,ip2region.db 数据库只有数 MB的项目,提供了众多主流编程语言的 xdb 数据生成和查询客户端实现,这里只能说:开源真香,开源万岁。


3.1 Ip2region 特性


标准化的数据格式


每个 ip 数据段的 region 信息都固定了格式:国家|区域|省份|城市|ISP,只有中国的数据绝大部分精确到了城市,其他国家部分数据只能定位到国家,其余选项全部是0。


数据去重和压缩


xdb 格式生成程序会自动去重和压缩部分数据,默认的全部 IP 数据,生成的 ip2region.xdb 数据库是 11MiB,随着数据的详细度增加数据库的大小也慢慢增大。


极速查询响应


即使是完全基于 xdb 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:



  1. vIndex 索引缓存 :使用固定的 512KiB 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在10-20微秒之间。

  2. xdb 整个文件缓存:将整个 xdb 文件全部加载到内存,内存占用等同于 xdb 文件大小,无磁盘 IO 操作,保持微秒级别的查询效率。


IP 数据管理框架


v2.0 格式的 xdb 支持亿级别的 IP 数据段行数,region 信息也可以完全自定义,例如:你可以在 region 中追加特定业务需求的数据,例如:GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据。


99.9% 准确率


数据聚合了一些知名 ip 到地名查询提供商的数据,这些是他们官方的的准确率,经测试着实比经典的纯真 IP 定位准确一些。


ip2region 的数据聚合自以下服务商的开放 API 或者数据(升级程序每秒请求次数 2 到 4 次):



备注:如果上述开放 API 或者数据都不给开放数据时 ip2region 将停止数据的更新服务。


3.2 整合Ip2region客户端进行查询


提供了众多主流编程语言的 xdb 数据生成和查询客户端实现,已经集成的客户端有:java、C#、php、c、python、nodejs、php扩展(php5 和 php7)、golang、rust、lua、lua_c,nginx。这里讲一下java的客户端。


首先我们需要引入依赖:


<dependency>
 <groupId>org.lionsoul</groupId>
 <artifactId>ip2region</artifactId>
 <version>2.6.5</version>
</dependency>

接下来我们需要先去下载数据文件ip2region.xdb到本地,然后基于数据文件进行查询,下面查询方法文件路径改为你本地路径即可,ip2region提供三种查询方式:


完全基于文件的查询


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
   public static void main(String[] args) {
       // 1、创建 searcher 对象
       String dbPath = "ip2region.xdb file path";
       Searcher searcher = null;
       try {
           searcher = Searcher.newWithFileOnly(dbPath);
      } catch (IOException e) {
           System.out.printf("failed to create searcher with `%s`: %s\n", dbPath, e);
           return;
      }

       // 2、查询
       try {
           String ip = "1.2.3.4";
           long sTime = System.nanoTime();
           String region = searcher.search(ip);
           long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
           System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
      } catch (Exception e) {
           System.out.printf("failed to search(%s): %s\n", ip, e);
      }

       // 3、关闭资源
       searcher.close();
       
       // 备注:并发使用,每个线程需要创建一个独立的 searcher 对象单独使用。
  }
}

缓存 VectorIndex 索引


我们可以提前从 xdb 文件中加载出来 VectorIndex 数据,然后全局缓存,每次创建 Searcher 对象的时候使用全局的 VectorIndex 缓存可以减少一次固定的 IO 操作,从而加速查询,减少 IO 压力。


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
   public static void main(String[] args) {
       String dbPath = "ip2region.xdb file path";

       // 1、从 dbPath 中预先加载 VectorIndex 缓存,并且把这个得到的数据作为全局变量,后续反复使用。
       byte[] vIndex;
       try {
           vIndex = Searcher.loadVectorIndexFromFile(dbPath);
      } catch (Exception e) {
           System.out.printf("failed to load vector index from `%s`: %s\n", dbPath, e);
           return;
      }

       // 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。
       Searcher searcher;
       try {
           searcher = Searcher.newWithVectorIndex(dbPath, vIndex);
      } catch (Exception e) {
           System.out.printf("failed to create vectorIndex cached searcher with `%s`: %s\n", dbPath, e);
           return;
      }

       // 3、查询
       try {
           String ip = "1.2.3.4";
           long sTime = System.nanoTime();
           String region = searcher.search(ip);
           long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
           System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
      } catch (Exception e) {
           System.out.printf("failed to search(%s): %s\n", ip, e);
      }
       
       // 4、关闭资源
       searcher.close();

       // 备注:每个线程需要单独创建一个独立的 Searcher 对象,但是都共享全局的制度 vIndex 缓存。
  }
}

缓存整个 xdb 数据


我们也可以预先加载整个 ip2region.xdb 的数据到内存,然后基于这个数据创建查询对象来实现完全基于文件的查询,类似之前的 memory search。


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
   public static void main(String[] args) {
       String dbPath = "ip2region.xdb file path";

       // 1、从 dbPath 加载整个 xdb 到内存。
       byte[] cBuff;
       try {
           cBuff = Searcher.loadContentFromFile(dbPath);
      } catch (Exception e) {
           System.out.printf("failed to load content from `%s`: %s\n", dbPath, e);
           return;
      }

       // 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。
       Searcher searcher;
       try {
           searcher = Searcher.newWithBuffer(cBuff);
      } catch (Exception e) {
           System.out.printf("failed to create content cached searcher: %s\n", e);
           return;
      }

       // 3、查询
       try {
           String ip = "1.2.3.4";
           long sTime = System.nanoTime();
           String region = searcher.search(ip);
           long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
           System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
      } catch (Exception e) {
           System.out.printf("failed to search(%s): %s\n", ip, e);
      }
       
       // 4、关闭资源 - 该 searcher 对象可以安全用于并发,等整个服务关闭的时候再关闭 searcher
       // searcher.close();

       // 备注:并发使用,用整个 xdb 数据缓存创建的查询对象可以安全的用于并发,也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。
  }
}

3.3 springboot整合示例


首先我们也需要像上面一样引入maven依赖。然后就可以基于上面的查询方式进行封装成工具类了,我这里选择了上面的第三种方式:缓存整个 xdb 数据


@Slf4j
public class IpUtils {
   private static final String IP_DATA_PATH = "/Users/shepherdmy/Desktop/ip2region.xdb";
   private static  byte[] contentBuff;

   static {
       try {
           // 从 dbPath 加载整个 xdb 到内存。
           contentBuff = Searcher.loadContentFromFile(IP_DATA_PATH);
      } catch (IOException e) {
           e.printStackTrace();
      }
  }
 
     /**
    * 根据ip查询归属地,固定格式:中国|0|浙江省|杭州市|电信
    * @param ip
    * @return
    */
   public static IpRegion getIpRegion(String ip) {
       Searcher searcher = null;
       IpRegion ipRegion = new IpRegion();
       try {
           searcher = Searcher.newWithBuffer(contentBuff);
           String region = searcher.search(ip);
           String[] info = StringUtils.split(region, "|");
           ipRegion.setCountry(info[0]);
           ipRegion.setArea(info[1]);
           ipRegion.setProvince(info[2]);
           ipRegion.setCity(info[3]);
           ipRegion.setIsp(info[4]);
      } catch (Exception e) {
           log.error("get ip region error: ", e);
      } finally {
           if (searcher != null) {
               try {
                   searcher.close();
              } catch (IOException e) {
                   log.error("close searcher error:", e);
              }
          }
      }
       return ipRegion;
  }

}

作者:shepherd111
来源:juejin.cn/post/7280118836685668367
收起阅读 »

Metal每日分享,不同色彩空间转换滤镜效果

iOS
本案例的目的是理解如何用Metal实现色彩空间转换效果滤镜,转换在不同色彩空间生成的图像; Demo HarbethDemo地址iDay每日分享文档地址 实操代码// 色彩空间转换滤镜 let filter = C7ColorSpace.init(with:...
继续阅读 »

本案例的目的是理解如何用Metal实现色彩空间转换效果滤镜,转换在不同色彩空间生成的图像;




Demo



实操代码

// 色彩空间转换滤镜
let filter = C7ColorSpace.init(with: .rgb_to_yuv)

// 方案1:
ImageView.image = try? BoxxIO(element: originImage, filters: [filter, filter2, filter3]).output()

// 方案2:
ImageView.image = originImage.filtering(filter, filter2, filter3)

// 方案3:
ImageView.image = originImage ->> filter ->> filter2 ->> filter3

效果对比图


  • 不同参数下效果

    实现原理


  • 过滤器


这款滤镜采用并行计算编码器设计.compute(kernel: type.rawValue)

/// 色彩空间转换
public struct C7ColorSpace: C7FilterProtocol {

public enum SwapType: String, CaseIterable {
case rgb_to_yiq = "C7ColorSpaceRGB2YIQ"
case yiq_to_rgb = "C7ColorSpaceYIQ2RGB"
case rgb_to_yuv = "C7ColorSpaceRGB2YUV"
case yuv_to_rgb = "C7ColorSpaceYUV2RGB"
}

private let type: SwapType

public var modifier: Modifier {
return .compute(kernel: type.rawValue)
}

public init(with type: SwapType) {
self.type = type
}
}

  • 着色器

每条通道乘以各自偏移求和得到Y,用Y作为新的像素rgb;

kernel void C7ColorSpaceRGB2Y(texture2d<half, access::write> outputTexture [[texture(0)]],
texture2d<half, access::read> inputTexture [[texture(1)]],
uint2 grid [[thread_position_in_grid]]) {
const half4 inColor = inputTexture.read(grid);

const half Y = half((0.299 * inColor.r) + (0.587 * inColor.g) + (0.114 * inColor.b));
const half4 outColor = half4(Y, Y, Y, inColor.a);

outputTexture.write(outColor, grid);
}

// See: https://en.wikipedia.org/wiki/YIQ
kernel void C7ColorSpaceRGB2YIQ(texture2d<half, access::write> outputTexture [[texture(0)]],
texture2d<half, access::read> inputTexture [[texture(1)]],
uint2 grid [[thread_position_in_grid]]) {
const half4 inColor = inputTexture.read(grid);

const half3x3 RGBtoYIQ = half3x3({0.299, 0.587, 0.114}, {0.596, -0.274, -0.322}, {0.212, -0.523, 0.311});
const half3 yiq = RGBtoYIQ * inColor.rgb;
const half4 outColor = half4(yiq, inColor.a);

outputTexture.write(outColor, grid);
}

kernel void C7ColorSpaceYIQ2RGB(texture2d<half, access::write> outputTexture [[texture(0)]],
texture2d<half, access::read> inputTexture [[texture(1)]],
uint2 grid [[thread_position_in_grid]]) {
const half4 inColor = inputTexture.read(grid);

const half3x3 YIQtoRGB = half3x3({1.0, 0.956, 0.621}, {1.0, -0.272, -0.647}, {1.0, -1.105, 1.702});
const half3 rgb = YIQtoRGB * inColor.rgb;
const half4 outColor = half4(rgb, inColor.a);

outputTexture.write(outColor, grid);
}

// See: https://en.wikipedia.org/wiki/YUV
kernel void C7ColorSpaceRGB2YUV(texture2d<half, access::write> outputTexture [[texture(0)]],
texture2d<half, access::read> inputTexture [[texture(1)]],
uint2 grid [[thread_position_in_grid]]) {
const half4 inColor = inputTexture.read(grid);

const half3x3 RGBtoYUV = half3x3({0.299, 0.587, 0.114}, {-0.299, -0.587, 0.886}, {0.701, -0.587, -0.114});
const half3 yuv = RGBtoYUV * inColor.rgb;
const half4 outColor = half4(yuv, inColor.a);

outputTexture.write(outColor, grid);
}

kernel void C7ColorSpaceYUV2RGB(texture2d<half, access::write> outputTexture [[texture(0)]],
                                texture2d<half, access::read> inputTexture [[texture(1)]],
                                uint2 grid [[thread_position_in_grid]]) {
    const half4 inColor = inputTexture.read(grid);

    const half3x3 YUVtoRGB = half3x3({1.0, 0.0, 1.28033}, {1.0, -0.21482, -0.38059}, {1.0, 2.21798, 0.0});
    const half3 rgb = YUVtoRGB * inColor.rgb;
    const half4 outColor = half4(rgb, inColor.a);

    outputTexture.write(outColor, grid);
}

色彩空间


  • YIQ

在YIQ系统中,是NTSC(National Television Standards Committee)电视系统标准;


  • Y是提供黑白电视及彩色电视的亮度信号Luminance,即亮度Brightness;
  • I代表In-phase,色彩从橙色到青色;
  • Q代表Quadrature-phase,色彩从紫色到黄绿色;



转换公式如下:




  • YUV

YUV是在工程师想要在黑白基础设施中使用彩色电视时发明的。他们需要一种信号传输方法,既能与黑白 (B&W) 电视兼容,又能添加颜色。亮度分量已经作为黑白信号存在;他们将紫外线信号作为解决方案添加到其中。

由于 U 和 V 是色差信号,因此在直接 R 和 B 信号上选择色度的 UV 表示。换句话说,U 和 V 信号告诉电视在不改变亮度的情况下改变某个点的颜色。
或者 U 和 V 信号告诉显示器以牺牲另一种颜色为代价使一种颜色更亮,以及它应该移动多少。
U 和 V 值越高(或负值越低),斑点的饱和度(色彩)就越高。
U 值和 V 值越接近零,颜色偏移越小,这意味着红、绿和蓝光的亮度会更均匀,从而产生更灰的点。
这是使用色差信号的好处,即不是告诉颜色有多少红色,而是告诉红色比绿色或蓝色多多少。
反过来,这意味着当 U 和 V 信号为零或不存在时,它只会显示灰度图像。
如果使用 R 和 B,即使在黑白场景中,它们也将具有非零值,需要所有三个数据承载信号。
这在早期的彩色电视中很重要,因为旧的黑白电视信号没有 U 和 V 信号,这意味着彩色电视开箱后只会显示为黑白电视。
此外,黑白接收器可以接收 Y' 信号并忽略 U 和 V 颜色信号,使 YUV 向后兼容所有现有的黑白设备、输入和输出。
如果彩色电视标准不使用色差信号,这可能意味着彩色电视会从 B& 中产生有趣的颜色 W 广播,否则需要额外的电路将黑白信号转换为彩色。
有必要为色度通道分配较窄的带宽,因为没有可用的额外带宽。
如果某些亮度信息是通过色度通道到达的(如果使用 RB 信号而不是差分 UV 信号,就会出现这种情况),黑白分辨率就会受到影响。

YUV 模型定义了一个亮度分量 (Y),表示物理线性空间亮度,以及两个色度分量,分别称为 U(蓝色投影)和 V(红色投影)。它可用于在 RGB 模型之间进行转换,并具有不同的颜色空间




转换公式如下:




最后


  • 慢慢再补充其他相关滤镜,喜欢就给我点个星🌟吧。

✌️.


作者:茶底世界之下
链接:https://juejin.cn/post/7179397518345093177
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

烟雨蒙蒙的三月

金三银四好像失效了 从去年下半年开始,互联网寒冬就总是萦绕在耳边。大量的公司倒闭、裁员。本以为等疫情过,等春天,等金三银四,一切又会变得好起来。但是站在这个本应该金光闪闪的时刻,却是无人问津。那个我们希望的春天似乎没有到来。 你是否也会像我一样焦虑 从业...
继续阅读 »
金三银四好像失效了


从去年下半年开始,互联网寒冬就总是萦绕在耳边。大量的公司倒闭、裁员。本以为等疫情过,等春天,等金三银四,一切又会变得好起来。但是站在这个本应该金光闪闪的时刻,却是无人问津。那个我们希望的春天似乎没有到来。



你是否也会像我一样焦虑


从业六年多,但是最近将近两年都在中间件行业,技术、履历都算不上优秀。年纪每一年都在增长,而我有随着年纪一起快速增长吗?我想是没有的。年后公司部门会议说到部门发展,领导说我们的产品越发稳定,对于一个中间件来说,客户需要的就是稳定,太多功能对于他们来说是无用的。这就意味着我们的产品到头了。但是这个产品到头我们再做什么呢?没人给我们答案。



要跳出当前的圈子吗


如果在这里看不见曙光,那么在别的地方是不是能有希望呢?春天,万物复苏,楼下如枯骨林立的一排排树干纷纷长出绿芽,迸发生机。我是否可以像沉寂了一整个冬天的枯树一样迎来自己的春天呢?目前看来也是没有的。投了很多家的简历,犹如石沉大海了无音讯。不知道对于别人来说是怎样,但是对我而言,这个三月并不是春天。



会有春天吗


去年我第一次为开源社区贡献了自己的代码,我觉得我变得更好了。疫情也在年底宣布画上句号,春天似乎真的要来了。物理上的春天是到来了,可是那个我们期盼的春天它真的会到来吗?总是在期盼等一等一切就会好转,因为除了等,我们似乎也并没有太多的选择。时代的轮盘一直运转,无数人的命运随之沉浮。我们更多的只能逆来顺受,接受它的变化,并随之拥抱它。可是未知的未来总是让人充满惶恐,看看自己再过两年就三十了,未婚、未育。在本就三十五岁魔咒的行业,总是惴惴不安。我总是思考如果被这个行业抛弃,不做开发我又能做什么呢?如果是你,这个答案会是什么呢?



这个文章应该有个结尾


文章总是需要结尾的,生活不是。生活还需要继续,每个人的答案都需要自己去寻找。在茫然无措的时刻,只能自己去寻找一些解药,在心绪不宁的时候,学习什么也是学不进去的。最近在看《我的解放日记》,能够缓解一些我的焦虑情绪。如果你也需要一些治愈系的剧,也可以去看看它。时代的浪潮推着我们往前,我们惶恐不安,手足无措,这都不是我们的错,我们只能尽力做好能做的。但是那些决定命运的瞬间几乎都是我们不能选择的。能活着就已经很不错了。


作者:山城小辣椒
链接:https://juejin.cn/post/7213557559731028024
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

学习能力必然是职场的核心能力

最近新工作的编程语言换为了Golang,同时也在面试招聘相关岗位的人才。通过简历面试(别人的经历),以及自己的亲身学习经历,真切的感受到学习能力将是未来的一大竞争力。 从面试方面来看,大多数人工作稳定之后便失去了学习能力,以为现在的工作可以长久的干下去。结果,...
继续阅读 »

最近新工作的编程语言换为了Golang,同时也在面试招聘相关岗位的人才。通过简历面试(别人的经历),以及自己的亲身学习经历,真切的感受到学习能力将是未来的一大竞争力。


从面试方面来看,大多数人工作稳定之后便失去了学习能力,以为现在的工作可以长久的干下去。结果,互联网的风停下来之后,市场的需求变了,从单一的编程语言、单一业务的能力变成更加综合的能力,需要的人逐渐变为T型人才甚至π型人才。此时,学习能力就变得更加重要。否则,面临的只能是市场的淘汰。


下面分享一下自己最近三周学习Golang的一些经验和方法,大家可以拿来借鉴的其他学习方面上:


第一、实践。任何的学习都离不开实践。不能够运用到实践中的学习大概率是无效学习,而实践也是学习最有效的手段。在刚开学学习Golang时,找了一份基础语法的文档,花一两个小时看了一遍,知道常见的语法结构怎么用的,便开始搭建项目,写业务功能。其实这样的效果最快,以具体的功能实践来驱动学习,同时把对这方面的手感和思路锻炼出来。


第二、系统学习。单纯动手实践的过程中会掺杂着业务逻辑的实现,学习效率和范围上会有一些局限,属于用到什么学什么,缺点是不够系统。这时还需要一两本书,通读全书,帮助系统的了解这门语言(或某个行业)是怎么运作的,整个生态是什么样的,底层逻辑是怎样的,以便查漏补缺。在系统学习这块,建议以书籍为主,书籍的优势就是方便、快捷、系统、准确。


第三、交流。之前找一个懂的大佬请教和交流不是那么容易。但随着AI的发展,交流形式不仅仅限于大佬了,也可以是GPT。GPT最强大的能力是无所不知,知无不言。当然,对于它提供的结果也需要辩证的去看,某些地方可能会有错误,但大方向基本上是没错的,再辅以佐证,基本上能够解决80%的问题。


如果有机会参与面试,无论是作为面试官或者被面试者,都是一个交流的过程。在相互沟通的过程中了解市场需要什么,市场流行什么。


最后,针对某些问题,还是得去跟大佬交流才行,交流的过程中会碰撞出很多火花来。比如,不断的迭代某个算法,学到更好的实现方式,了解到你不知道的知识点等。曾经,一个字符串截取的功能,与大佬交流了三次,升级了三版,也学到了不同的API的使用方法和特性。


第四,输出。检验是否学会的一个标准就是你能否清晰的给别人描述出来,让别人听得懂。这一条是否很耳熟?对,它就是费曼学法,世界公认的最快的学习法。如果没办法很好的表达,说明这块掌握的还不是很清楚。当然,这个过程中也属于交流,也会拿到别人的反馈,根据别人的反馈来认识到自己的掌握程度和薄弱点。


第五,利用别人的时间。个人的时间总是有限的,不可能什么事情都自己做,也不可能都亲手验证。而作为管理者,最大的技能之一就是靠别人、靠团队来实现目标。那么,一个技术方案是否可行,是否有问题,也可以交给别人来调研、实践、验证。这样,可以让学习的效率并行起来。


另外,我们可能都听说过“一万小时定律”,这个概念是极具迷惑性的,会让你觉得学习任何东西都需要花费大量的时间的。其实不然,一万小时定律指的是学习一个复杂的领域并且成为这个领域的专家。


而我们在生活和实践的过程中,往往不需要什么方面都成为专家,只需要知道、掌握或会用某一领域的知识即可。对于入门一个新领域,一般来说,可能只需要20小时、100小时不等,没有想象中那么难。对于一个懂编程语言的人来说,从零学习另外一门语言,一般也就一两周时间就可以上手了。因此,我们不要对此产生畏惧心理。


上面讲的是学习方法,但最根本的是学习的意愿。你是选择花一年时间学习一门技术,然后重复十年,还是愿意每年都不断的学习迭代自己?两者的结果差距超乎你的想象。


作者:程序新视界
链接:https://juejin.cn/post/7257285697382449189
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

如何治愈拖延症

如何治愈拖延症 背景 最近发现我的拖延症很严重了😭😭,看了一下我的抖音主页,我已经很久没有去跑步了。最近的一次跑步的记录停留在了8月23日,周三。我的这篇文章写在周天的上午,掐指一算,已经有三天晚上没有跑步了。我不大喜欢给自己找借口,没有行动就是没有行动。 ...
继续阅读 »

如何治愈拖延症


背景


最近发现我的拖延症很严重了😭😭,看了一下我的抖音主页,我已经很久没有去跑步了。最近的一次跑步的记录停留在了8月23日,周三。我的这篇文章写在周天的上午,掐指一算,已经有三天晚上没有跑步了。我不大喜欢给自己找借口,没有行动就是没有行动。




就拿我昨天晚上来说吧,吃完饭已经是8点了,这个点没啥问题。和家里通了半小时的电话之后,发现手机没电了,于是又在充电。等到九点的时候,电池的电量还在30%左右,我知道我的手机电池不大行,不足以支撑一个小时,于是就放弃了😅。


但是当早上我坐在电脑前的时候,发现昨天的好多事情都没有完成,今天的事情又得往后推了。越堆积越是多,都喘不过气来了🤥。



哈哈🤭🤭,也不好意思让大家看到下周的推文内容啦,算是提前剧透了😎





我就不断的在思考,为什么我的执行力不行了。我觉得我的代言词就是:一个有思想有行动力的程序员。现在看来,我是一个懒惰、带有严重的拖延症的程序员了。不行,这个问题得治,不然我会更加的焦虑,堆积更多的任务导致更低的效率。


分析


结合这个低效率的周末,我反思了我为什么效率这么低。


🕢推迟开始


我发现我总喜欢做todo list,但是很少去看,也很少去核对一下我当前的进度。总觉得一天的时间很长,我可以先去做别的事情,比如碎片化的短视频、吃吃吃、发呆。于是一件件的本在计划中的事情被不断的推迟了。


⏲时间管理困难


从我8:00起来到晚上的凌晨入睡,减去我个人清洁、做饭、午睡,我剩下的时间大约是10个小时。但是,我一对比下来,我的时间利用率仅仅是40%,相当于我只有4个小时是在满满当当的学习的。我之前的ipad在的时候,我会用潮汐这个软件把我的时间分割成一个小时一个小时的。现在没了,我发现我的时间规划真的出了大问题。


🤖自我控制力下降


我觉得最近一年的时间,我真的太放松自我了。我的技术成长、学习上长进也是微乎其微。我总结下来就是因为我的自控力太差了,或者说没有承受着外界的干扰。因为一个短视频就可以刷上一个小时的短视频,因为一个好物就会不断的逛购物软件......碎片化的时间消耗,最终导致了效率低下。


解决方案


针对以上我总结的问题,我决定对症下药。


🧾明确的计划


我觉得我明确的计划真的很必要。就像我公众号shigen里面给自己定的一个目标一样:



2023年的8月开始,我先给自己定一个小目标:公众号文章不停更





“不停更”的意思是我每天都要更新文章。我的推文里还带了“新闻早知道”栏目,我哪天没更新或者说更新晚了,我就觉得目标没有实现了,新闻也没什么意义了。我觉得日常的计划和这个目标的设定和实现有着相似的地方,我要把我的计划和目标更明确一点。🤔🤔比方说我今天要干嘛,我完成了怎么样了。


优先级


事情分清楚轻重缓急,我记得我在实习的时候,就有一次因为项目要上线和我一点不大紧要的事情次序搞混了,导致晚上加班上线。现在的我也是,很多重要的事情也是放到了最后做甚至只延期了。所以,我的行动之前,得先做最要紧的事情。但是也会混杂一些个人的情绪在里边,比方说明明一件事情很重要,但是自己就是不想做或者说觉得事情很简单,我先做最有意思的事情。很多时候都是这样的,兴趣和意义占据了主导因素,优先级反而不是那么重要了。


抗拒干扰


手机就在我的边上,这很难不因为一个消息或者一个发愣就去拿起手机,一旦拿起来就放不下了。所以,我觉得最好就是把它放在我的抽屉里,然后眼不见就不去想它了。


奖励惩罚机制


最后,我觉得奖罚分明也挺重要的。在这里,我也想起了我在一线的时候,我周末总会有一天去我住的地方隔壁去逛超市,每次的消费金额大约在100-150左右。但是我出去的前提是我的学习目标完成了或者代码写完了。我现在却相反,目标缺少了一个验收和奖惩的过程。我觉得和我更喜欢宅有一点关系了,所以,我也得奖励我自己一下:目标完成了可以去逛超市消费🛒,也可以去骑行🚲;但是没完成,健腹轮😭😭安排上!


好了,以上就是我对于最近的拖延症的分析和解决方式的思考了。也欢迎伙伴们在评论区交流一下自己对于拖延症的看法。


shigen一起,每天不一样!


作者:shigen01
链接:https://juejin.cn/post/7272690326401728547
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Swift - LeetCode - 二叉树的所有路径

iOS
题目 给你一个二叉树的根节点 root,按 任意顺序,返回所有从根节点到叶子节点的路径。 叶子节点 是指没有子节点的节点。 示例 1: 输入:root = [1,2,3,null,5]输出:["1->2->5","1->3"] 示例 2:...
继续阅读 »

题目


给你一个二叉树的根节点 root,按 任意顺序,返回所有从根节点到叶子节点的路径。


叶子节点 是指没有子节点的节点。


示例 1:



  • 输入:root = [1,2,3,null,5]
  • 输出:["1->2->5","1->3"]


示例 2:



  • 输入:root = [1]
  • 输出:["1"]


提示:


  • 树中节点的数目在范围 [1, 100] 内
  • -100 <= Node.val <= 100

方法一:深度优先搜索


思路及解法


最直观的方法是使用深度优先搜索。在深度优先搜索遍历二叉树时,我们需要考虑当前的节点以及它的孩子节点。


  • 如果当前节点不是叶子节点,则在当前的路径末尾添加该节点,并继续递归遍历该节点的每一个孩子节点。
  • 如果当前节点是叶子节点,则在当前路径末尾添加该节点后我们就得到了一条从根节点到叶子节点的路径,将该路径加入到答案即可。

如此,当遍历完整棵二叉树以后我们就得到了所有从根节点到叶子节点的路径。当然,深度优先搜索也可以使用非递归的方式实现,这里不再赘述。


代码

class Solution {
func binaryTreePaths(_ root: TreeNode?) -> [String] {
var paths: [String] = []
constructPaths(root, "", &paths)
return paths
}

func constructPaths(_ root: TreeNode?, _ path: String, _ paths: inout [String]) {
if nil != root {
var path = path
path += String(root!.val)
if nil == root?.left && nil == root?.right {
paths.append(path)
} else {
path += "->"
constructPaths(root?.left, path, &paths)
constructPaths(root?.right, path, &paths)
}
}
}
}

复杂度分析

  • 时间复杂度:(2),其中  表示节点数目。在深度优先搜索中每个节点会被访问一次且只会被访问一次,每一次会对  变量进行拷贝构造,时间代价为 (),故时间复杂度为 (2)

  • 空间复杂度:(2),其中  表示节点数目。除答案数组外我们需要考虑递归调用的栈空间。在最坏情况下,当二叉树中每个节点只有一个孩子节点时,即整棵二叉树呈一个链状,此时递归的层数为 ,此时每一层的  变量的空间代价的总和为 (=1)=(2) 空间复杂度为 (2)。最好情况下,当二叉树为平衡二叉树时,它的高度为 log,此时空间复杂度为 ((log)2)


方法二:广度优先搜索


思路及解法


我们也可以用广度优先搜索来实现。我们维护一个队列,存储节点以及根到该节点的路径。一开始这个队列里只有根节点。在每一步迭代中,我们取出队列中的首节点,如果它是叶子节点,则将它对应的路径加入到答案中。如果它不是叶子节点,则将它的所有孩子节点加入到队列的末尾。当队列为空时广度优先搜索结束,我们即能得到答案。


代码

class Solution {
func binaryTreePaths(_ root: TreeNode?) -> [String] {
var paths: [String] = []
if nil == root {
return paths
}
var node_queue: [TreeNode] = []
var path_queue: [String] = []

node_queue.append(root!)
path_queue.append(String(root!.val))

while !node_queue.isEmpty {
let node: TreeNode? = node_queue.removeFirst()
let path: String = path_queue.removeFirst()

if nil == node?.left && nil == node?.right {
paths.append(path)
} else {
if nil != node?.left {
node_queue.append(node!.left!)
path_queue.append(path + "->" + String(node!.left!.val))
}

if nil != node?.right {
node_queue.append(node!.right!)
path_queue.append(path + "->" + String(node!.right!.val))
}
}
}
return paths
}
}

复杂度分析


  • 时间复杂度:(2),其中  表示节点数目。分析同方法一。

  • 空间复杂度:(2),其中  表示节点数目。在最坏情况下,队列中会存在  个节点,保存字符串的队列中每个节点的最大长度为 ,故空间复杂度为 (2)


作者:晨曦_iOS
链接:https://juejin.cn/post/7133986881594720269
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

交互小组件 — iOS 17

iOS
作为一名 iOS 开发人员,该平台有一些令人兴奋的特性和功能值得探索。 其中,小部件是我的最爱。 小部件已成为 iOS 和 macOS 体验中不可或缺的一部分,并且随着 SwiftUI 中引入的最新功能,它们现在变得更加强大。 在本文中,我们将探讨如何通过交互...
继续阅读 »

作为一名 iOS 开发人员,该平台有一些令人兴奋的特性和功能值得探索。 其中,小部件是我的最爱。 小部件已成为 iOS 和 macOS 体验中不可或缺的一部分,并且随着 SwiftUI 中引入的最新功能,它们现在变得更加强大。 在本文中,我们将探讨如何通过交互性和动画使小组件变得栩栩如生,使它们更具吸引力和视觉吸引力。 我们将深入探讨动画如何与小组件配合使用的细节,并展示新的 Xcode Preview API,它可以实现快速迭代和自定义。 此外,我们将探索如何使用熟悉的控件(如 Button 和 Toggle)向小部件添加交互性,并利用 App Intents 的强大功能。 那么让我们开始吧!


小部件中的交互性
小部件在单独的进程中呈现,它们的视图代码仅在归档期间运行。 为了使小组件具有交互性,我们可以使用 Button 和 Toggle 等控件。 但是,由于 SwiftUI 不会在应用程序的进程空间中执行闭包或改变绑定,因此我们需要一种方法来表示可由小部件扩展执行的操作。 App Intents 为此提供了一个解决方案,允许我们定义可由系统调用的操作。 通过导入 SwiftUI 和 AppIntents,我们可以使用接受 AppIntent 作为参数的 Button 和 Toggle 初始值设定项来执行所需的操作。


现在我们要为现有项目创建小组件。




相应地命名它。 请注意,禁用两个复选框




现在我将使用清单和按钮重写现有代码。

struct Provider: TimelineProvider {  
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry( checkList: Array(ModelData.shared.items.prefix(3)))
}

func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(checkList: Array(ModelData.shared.items.prefix(3)))
completion(entry)
}

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
//var entries: [SimpleEntry] = []

// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let data = Array(ModelData.shared.items.prefix(3))
let entries = [SimpleEntry(checkList: data)]

let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}

struct SimpleEntry: TimelineEntry {
var date: Date = .now

var checkList: [ProvisionModel]
}

struct InteractiveWidgetEntryView : View {
var entry: Provider.Entry

var body: some View {
VStack(alignment: .leading, spacing: 5.0) {
Text("My List")
if entry.checkList.isEmpty{
Text("You've bought all🏆")
}else{
ForEach(entry.checkList) { item in
HStack(spacing: 5.0){

Image(systemName: item.isAdded ? "checkmark.circle.fill":"circle")
.foregroundColor(.green)


VStack(alignment: .leading, spacing: 5){
Text(item.itemName)
.textScale(.secondary)
.lineLimit(1)
Divider()
}
}
}
}
}
.containerBackground(.fill.tertiary, for: .widget)
}
}

struct InteractiveWidget: Widget {
let kind: String = "InteractiveWidget"

var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
InteractiveWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}


提供的代码在 iOS 或 macOS 应用程序中使用 SwiftUI 定义小部件。 让我们分解代码并解释每个部分:

  1. Provider:该结构体符合TimelineProvider协议,负责向widget提供数据。 它包含三个功能:
    • placeholder(in:):此函数返回一个占位符条目,表示首次添加小部件时的外观。 它使用派生自 ModelData.shared.items 的清单数组创建 SimpleEntry。
    • getSnapshot(in:completion:):此函数生成一个表示小部件当前状态的快照条目。 它使用派生自 ModelData.shared.items 的清单数组创建 SimpleEntry。
    • getTimeline(in:completion:):此函数生成小部件的条目时间线。 它使用派生自 ModelData.shared.items 的清单数组创建 SimpleEntry 实例的数组,并返回包含这些条目的时间线。
    1. SimpleEntry:此结构符合 TimelineEntry 协议,表示小部件时间线中的单个条目。 它包含一个表示条目日期的日期属性和一个 checkList 属性,后者是一个 ProvisionModel 项的数组。
    2. InteractiveWidgetEntryView:此结构定义用于显示小部件条目的视图层次结构。 它采用 Provider.Entry 类型的条目作为输入。 在 body 属性内部,它创建一个具有对齐和间距设置的 VStack。 它显示一个标题,并根据 checkList 数组是否为空,显示一条消息或迭代该数组以显示每个项目的信息。
    3. InteractiveWidget:该结构定义小部件本身。 它符合Widget协议并指定了Widget的种类。 它提供了一个 StaticConfiguration,其中包含一个 Provider 实例作为数据提供者,并提供一个 InteractiveWidgetEntryView 作为每个条目的视图。 它还设置小部件的显示名称和描述。
    4. Preview:此代码块用于在开发过程中预览小部件的外观。 它为 .systemSmall 大小的小部件创建预览,并提供 SimpleEntry 实例作为条目。 总的来说,此代码设置了一个使用 SwiftUI 框架显示清单的小部件。 小部件的数据由 Provider 结构提供,条目的视图由 InteractiveWidgetEntryView 结构定义。 InteractiveWidget 结构配置小部件并提供用于开发目的的预览。


还有按钮动作!


Apple 为此推出了 AppIntents!


我已经创建了视图模型和应用程序意图。

struct ProvisionModel: Identifiable{  
var id: String = UUID().uuidString
var itemName: String
var isAdded: Bool = false

}

class ModelData{
static let shared = ModelData()

var items: [ProvisionModel] = [.init(
itemName: "Orange"
), .init(
itemName: "Cheese"
), .init(
itemName: "Bread"
), .init(
itemName: "Rice"
), .init(
itemName: "Sugar"
), .init(
itemName: "Oil"
), .init(
itemName: "Chocolate"
), .init(
itemName: "Corn"
)]
}

提供的代码包括两个数据结构的定义:ProvisionModel 和 ModelData。 以下是每项的解释:


ProvisionModel:该结构表示清单中的一个供应项。 它符合可识别协议,该协议要求它具有唯一的标识符。 它具有以下属性:


id:一个字符串属性,保存使用 UUID 生成的唯一标识符。 每个 ProvisionModel 实例都会有一个不同的 id。


itemName:表示供应项目名称的字符串属性。


isAdded:一个布尔属性,指示该项目是否已添加到清单中。 它使用默认值 false 进行初始化。


ModelData:此类充当数据存储和单例,提供对供应项的共享访问。 它具有以下组件:
共享:ModelData 类型的静态属性,表示类的共享实例。 它遵循单例模式,允许跨应用程序访问同一实例。


items:一个数组属性,包含表示供应项的 ProvisionModel 实例。 该数组使用一组预定义的项目进行初始化,每个项目都使用特定的 itemName 进行初始化。 ModelData.shared 实例提供对此数组的访问。
总的来说,此代码为清单应用程序设置了数据模型。 ProvisionModel 结构定义每个供应项的属性,包括其唯一标识符以及是否已添加到清单中。 ModelData 类提供对供应项列表的共享访问,并遵循单例模式以确保访问和修改数据的一致性。


现在是 appIntent 的时候了!

struct MyActionIntent: AppIntent{  

static var title: LocalizedStringResource = "Toggle Task State"
@Parameter(title: "Task ID")
var id: String
init(){

}

init(id: String){
self.id = id
}

func perform() async throws -> some IntentResult {
if let index = ModelData.shared.items.firstIndex(where: { $0.id == id }) {
ModelData.shared.items[index].isAdded.toggle()

let itemToRemove = ModelData.shared.items[index]
ModelData.shared.items.remove(at: index)

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
ModelData.shared.items.removeAll(where: { $0.id == itemToRemove.id })
}

print("Updated")
}

return .result()
}
}

提供的代码定义了一个名为 MyActionIntent 的结构,该结构符合 AppIntent 协议。 此结构表示在清单应用程序中切换任务状态的意图。 以下是对其组成部分的解释:


title(静态属性):该属性表示操作意图的标题。 它的类型为 LocalizedStringResource,它是用于本地化目的的本地化字符串资源。


id(属性装饰器):该属性用@Parameter装饰,表示需要切换的任务的ID。


init():这是结构的默认初始化程序。 它不执行任何特定的初始化。


init(id: String):此初始化程序允许您使用特定任务 ID 创建 MyActionIntent 实例。


Perform()(方法):AppIntent 协议需要此方法,并执行与 Intent 相关的操作。
以下是其实施细目:
它检查 ModelData.shared.items 数组中是否存在与意图中提供的 ID 匹配的任务。


如果找到匹配项,它将使用toggle() 方法切换任务的isAdded 属性。 这会改变任务的状态。
然后,它创建一个局部变量 itemToRemove 来存储切换的任务。
使用remove(at:)方法和找到任务的索引从ModelData.shared.items数组中删除任务。
延迟 2 秒后,使用removeAll(where:) 和检查匹配 ID 的闭包从 ModelData.shared.items 数组中删除 itemToRemove。


最后,“Updated”被打印到控制台。
return .result():该语句返回一个IntentResult实例,表示intent的完成,没有任何具体的结果值。
总的来说,此代码定义了一个意图,用于执行切换清单中任务状态的操作。 它访问 ModelData 的共享实例,以根据提供的 ID 查找和修改任务。


现在是时候用 AppIntents 替换图像了

Button(intent: MyActionIntent(id: item.id)) {  
Image(systemName: item.isAdded ? "checkmark.circle.fill":"circle")
.foregroundColor(.green)
}
.buttonStyle(.plain)


作者:MrWu
链接:https://juejin.cn/post/7275980024263983164
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

看完这位小哥的GitHub,我沉默了

就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。 而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。 出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。...
继续阅读 »

就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。



而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。



出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。


即:在网页端实现了Windows 12的UI界面和交互效果。


这里也放几张图片感受一下。

登录页面

开始菜单

资源管理器

设置

终端命令行

AI Copilot

其他应用



这个项目的灵感来源于作者之前看到 Windows 12 概念版后深受启发,于是决定做一个Windows12网页版(就像之前的 Windows 11 网页版一样),可以让用户在网络上预先体验 Windows 12。


可以看到,这个项目是一个前端开源项目,而且由标准前端技术(HTML,JS,CSS)来实现,下载代码,无需安装,打开desktop.html即可。



项目包含:

  • 精美的UI设计
  • 流畅丰富的动画
  • 各种高级的功能(相较于网页版)

不仅如此,作者团队对于该项目的后续发展还做了不少规划和畅想。


  • 项目规划


  • 项目畅想


刚上面也说了,项目README里写道该项目的作者是一位14岁的初中生,网名叫星源,曾获得CSP普及组一等奖和蓝桥杯国赛三等奖。


作者出生于2009年,在成都上的小学和初中,目前刚上初三。


这样来看的话,虽说作者年龄很小,不过接触计算机和编程应该非常早,而且对计算机领域的知识和技术应该有着非常浓厚的兴趣。


从作者的个人主页里能看到,技术栈这块涉猎得还挺广泛。



作者自己表示如今上初三了,对于win12这个项目也不会做什么功能的更新了,后续的维护更新将交给其他贡献者成员。



文章的结尾也附上Windows 12网页版体验地址:tjy-gitnub.github.io/win12/desktop.html,感兴趣的同学可以自行体验。


聊到这里不得不说,人与人之间的差距确实挺大的。就像这位小哥,才14岁就已经精通前端技术了。


而14岁的我,当年在干嘛呢?


我想了又想。。


额,我好像在网吧里玩红警。。(手动doge)


作者:CodeSheep
链接:https://juejin.cn/post/7275978708644151354
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

className 还能这么用,你学会了吗

抛出问题 className大家都用过吧,用它在react项目中设置样式。它的用法很简单,除了可以设置一个样式外,react中也可以使用className引入多个类样式。 这次在写项目的时候,碰到一个非常小但是当时却一直解决不了的问题。后面在复盘的时候将它解决...
继续阅读 »

抛出问题


className大家都用过吧,用它在react项目中设置样式。它的用法很简单,除了可以设置一个样式外,react中也可以使用className引入多个类样式。


这次在写项目的时候,碰到一个非常小但是当时却一直解决不了的问题。后面在复盘的时候将它解决了。问题大致是这样的:


有两个活动页,每个活动页上都有一个活动规则图标来弹出活动规则,活动规则图标距离顶部会有一个值。现在问题就是这个活动规则在这两个活动页距离顶部的这个值是不一样的,但是我已经将这个活动规则图标做成了组件,并在这两个活动页里都调用了它,从而导致两个页面的样式会相同。如下图所示:




解决问题


这个问题不算很大,但是属于细节问题。就和我的组长所说的一样,一个项目应该要做到先完成再完美。所以我当时的解决方法是再写一个活动规则组件,只是将距离顶部的值做出修改即可。效果确实是达到了,不过在最后复盘代码的时候,组长注意到了这两个组件,并开始询问我为什么这样做。


组长:Rule_1Rule_2这两个组件是什么意思,我看它们没有很大的区别呀。


我便简单说了一下缘由。


组长接着说:你忘了组件是什么吗?一个CSS样式值不同就大费周章地新增一个组件,这岂不是太浪费了。再去想想其他方案。


通过这一番谈话我想起了组件化思想的运用,发现之前解决的这个小问题解决的并不够好。于是,我就带着组件化思想又来重新完善它。


我重新写了一个demo代码,将主要内容和问题在demo代码中体现出来。下面是原版活动规则组件demo代码,之后的代码都是基于demo代码完成的

import React from "react";
import "./index.css";
const Header = ({ onClick }) => {
return (
<>
<div className="container_hd">
<div
className='affix'
onClick={onClick}
></div>
</div>
</>
);
};
export default Header;

组件化思想


我自己问自己:既然已经写好了一个活动规则组件,为什么仅仅因为一个样式值的不同而去新增一个功能一样的组件?很显然,这种方法是最笨的方案。既然是组件,那就应该要有复用性,或者说只需在原有的基础上稍加改动就可达到效果。


这是样式的问题,因此要从根本上解决问题。单纯地修改 CSS 样式肯定不行,因为两个页面两个不同的样式。


className 运用


className 就不用多介绍了,经常能使用,咱们直接来看如何解决问题。在这里我定义了一个 Value 值,用来区分是在哪个页面的,比如分别有提交页和成功页,我在成功页设置一个 Value 值,,然后将 Value 值传入到活动规则组件,那么在活动规则组件里只需要判断 Value 值是否等于成功页的 Value 值即可。在 className 处做一个三元判断,如下所示:

className={`affix_${Value === "0" ? "main" : "submit"}`}

相当于如果Value等于0的时候类名为affix_main,否则为affix_submit。最后再css将样式完善即可。完整代码可以参考如下:

  • 成功页组件
import Header from "./components/Header";

const Success = () => {
const Value = "0";
return (
<div style={{ backgroundColor: "purple", width: "375px", height: "670px" }}>
<Header Value={Value}></Header>
</div>
);
};

export default Success;

  • 活动规则组件
import React from "react";
import "./index.css";
const Header = ({ onClick, Value }) => {
return (
<>
<div className="container_hd">
<div
className={`affix_${Value === "0" ? "main" : "submit"}`}
onClick={onClick}
></div>
</div>
</>
);
};
export default Header;

  • 活动规则组件样式
.container_hd {
width: 100%;
}
.affix_main {
position: absolute;
top: 32px;
right: -21px;
z-index: 9;
width: 84px;
height: 26px;
background: url('./assets/rule.png');
background-size: contain;
background-repeat: no-repeat;
}
.affix_submit {
position: absolute;
top: 12px;
right: -21px;
z-index: 9;
width: 84px;
height: 26px;
background: url('./assets/rule.png');
background-size: contain;
background-repeat: no-repeat;
}



通过对比效果图可以看出,两者的效果确实发生变化。完成之后,我心里在想:为什么当时就没想出这个简单易行的方案呢?动态判断并设置类名,至少比最开始的新增一个组件的方法高级多了。


总结问题


对于这个问题的解决就这样告一段落了,虽然看起来比较简单(一个动态设置类名),但是通过这个className的灵活使用,让我对className的用法有了更进一步的掌握,也不得不感叹组件化思想的广泛运用,这里最大程度地将组件化思想通过className 发挥出来。


因此,希望通过这个问题,来学会className的灵活用法,并理解好组件化思想。当然如果大家还有更好的解决方案的话,欢迎在评论区告诉我。


作者:一条会coding的Shark
链接:https://juejin.cn/post/7177042554666352697
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

35岁又如何,脚步不停歇,道阻且长,行之将至

前言 公司总会有业务的调整,而自己也会随着业务调整不得不做出一些改变。过去的半年,偶尔会找不到努力的方向。每到需要总结的时候,也总会感叹是否又荒废了时光。在即将快35岁的年纪,发现需要重新审视一下自己,让自己能有进一步的提升,保持足够的竞争力。 思考 所以从...
继续阅读 »


前言


公司总会有业务的调整,而自己也会随着业务调整不得不做出一些改变。过去的半年,偶尔会找不到努力的方向。每到需要总结的时候,也总会感叹是否又荒废了时光。在即将快35岁的年纪,发现需要重新审视一下自己,让自己能有进一步的提升,保持足够的竞争力。



思考


所以从自身多个维度出发,查阅自己的现状和不足,确定好目标以及一些计划。主要包括以下方面,如果大家有类似的疑惑,也可以按照这些方面提升自己。


image.png



技术


     技术是技术人的立身之本,没有技术,其他的都是空谈。


image.png



基础能力


内容现状目标
包括但不限于
- 计算机基础知识(网络、操作系统)
- 编程语言基础和进阶
- ....
评分: 良好
作为计算机专业出身的小镇做题家,学校学习时课程学的还算不错,但是到了工作中,只能用良好评价。最近半年频繁接触的像TCP、UDP的内容,分分钟让自己怀疑大学时期究竟有没有认真学习。
目标:优秀
基础知识的掌握当然要牢固,这是一个程序员专业度的体验。所以,针对遇到的问题,一定要刨根问底,去探究深层的原因。在探索和解惑的过程中,实际也会涉及对基础知识的检阅。


算法


内容现状目标
包括但不限于
- 计算机数据结构与算法
- 常用算法(Leecode题)
- ...
评分:较差
对于算法的考核,在面试中越来越重要,几乎每家公司都会有算法题。 我个人的算法偏差,有些题目只能靠死记硬背,这也间接导致影响了几次面试中的评价。
目标:良好
基于我在算法方面的实际能力,我觉得做到优秀有一些困难。但是对于常见的面试题目和解题方法,一定要掌握


架构


内容现状目标
包括但不限于
- 数据库
- 中间件
- 分布式理论和实践
- 微服务
- ...
评分:良好
每一个程序员都有架构师的梦。但是落实到实际工作中,真正的架构师岗位很少。不过幸运的是,我们所负责的模块,还是有一些机会的。
目标:优秀
对于架构相关的内容要掌握,在自己负责的内容中勇于尝试。做好时刻要进行架构设计的准备。


产品


作为技术人员,与产品应该是统一战线。有些时候,我们很容易陷入技术实现的细节中,而忽略了产品需求的合理性。


image.png



产品思维和数据洞察


内容现状目标
包括但不限于
- 以产品思维分析需求,提出合理合理意见和建议
- 需求的数据收集
- 功能上线后的数据分析
- ....
评分:差
对于我自己,我也是非常缺乏产品化思维的,很多时候还是以功能出发,并未将其产品化。在数据层面,也缺少足够的敏感度。
目标: 良好
要做到优秀非常困难,毕竟我也不打算转产品(手动狗头)。但是还是期望自己能有产品思维,多参与评审设计。同时也要提升自己的数据敏感度,能从数据中分析产品需求。


项目管理


内容现状目标
包括按不限于
- 需求分析
- 需求跟进
- 各方协调
评分: 差
在上一家公司,绝大多数会有专职的PM或者PMO进行项目管理,到后半段进行敏捷迭代以后,会参与部分项目管理的职责。等来到现在的公司,很多事情需要技术牵头处理,所以作为技术也要有项目管理的能力。
目标: 良好
能够作为PM进行项目牵头跟进,遇到问题多向前思考几步,这一方面对于技术人员的软素质提升也是非常有帮助的。


沟通


 在公司里面,我们有很大一部分时间都是在沟通,沟通也是软素质的一种体现。


image.png



沟通技巧


内容现状目标

- 观点表达要清晰
- 要学会倾听
评分:良好目标:优秀
希望自己更要学会倾听,在和别人沟通时,一定不要基于表达自己的观点,尤其是自己非常擅长的领域,也要克制自己急于表达观点。此外,对于一些事情的表达,切记不要斩钉截铁的回复。


情绪管理


内容现状目标
做好情绪管理,避免在沟通中引入情绪,影响沟通的效果。评分:良好
有时候会情绪化解决问题
目标:优秀
最近也在练习冥想,尽量控制自己的情绪表达。


分享


如果需要深刻掌握某个知识,一般可以按照以下步骤,阅读-> 笔记-> 总结-> 写作->分享。
当可以把知识能够分享给其他人,知识才真正属于了自己。


image.png



阅读


内容现状目标

- 技术或非技术书籍
- 技术博客(推荐medium)
- ...
评分:良好
目前会按照一定的计划读一些书籍,每周也会读几篇博客。
目标:优秀
但是这里有一点需要额外注意,那就是英文文章的阅读一定要加强。


写作


内容现状目标

- 对于自己要经常找的内容,要统一记录,快速查找
- 做好总结,选用适当的方式描述(视频、音频、或者图表)
评分:较差
很多时候,对于看到的内容,遇到的问题,总结不够及时,后续反复来找。
目标:良好
该记录的地方一定记录。
要学会用好Xmind等神器。


分享会


内容现状目标

- 团队分享
- 部门分享
- 公司分享
- ...
评分:差
面对面的分享参与非常非常少。
目标:良好
有机会一定要参与,因为每一次参与,也是督促自己认真整理,以及校验自己学习成果的时候。


管理


关于管理,我几乎0经验,只有之前敏捷团队的一些经验。当然,这是不是说明我进步空间大。



目标制定


内容现状目标

- 明确目标
- 目标清单
- 明确计划
- 目标验收
- ....
评分:较差
某些事情的处理,缺乏计划。
目标:良好
对于目标,我觉得可大可小,也可能不是管理目标,但是希望自己针对后面每个工作都按照目标、计划等内容列出来,逐步锻炼吧。


思考和创新



思考


内容现状目标

- 深度思考
- 抽象思考
- 系统思考
- ....
评分:较差目标:良好
遇到问题,三思而后行,尝试往前想3步,利用各种思考方式思考问题。多阅读、多提问、多交流。


创新


内容现状目标

- 技术创新
- 业务创新
评分: 差
对于我自己,很容易墨守成规,不是很容易变通,所以创新方面很弱。
目标:良好这个还是很困难的,无论是技术创新还是业务创新,如果没有足够的涉猎都不足以支撑。但是还是要提醒自己,这是自己非常薄弱的点。


健康


内容现状目标
身体是革命的本钱,在透支身体加班的同时,还是要记得锻炼身体。评分: 良好
近期北京天气开始变好,早晚不是很热,我也开始骑行通勤上班。骑行时可以让自己从另外一个视角看这个城市,真的很舒服。不过由于单趟通勤要20KM,往返40,加上自己比较菜,所以每周目前基本节奏是周一骑到公司,周二骑回家,周三休息一天,周四再骑到公司,周五骑回家。还不能天天骑,慢慢加油吧。
健康工作50年!!!

image.png



后记


从上面的这些维度分析以后,知道自己还有哪些方面需要进一步提升。所以,我每周都会把这周在这些方面所做的内容记录下来。在日常工作中,也会留意这些内容。


image.png


总而言之,还是继续加油吧!


作者:wowojyc艺超
来源:juejin.cn/post/7276352518262947900
收起阅读 »

硬盘坏了,一气之下用 js 写了个恢复程序

web
硬盘坏了,一气之下写了个恢复程序 师傅拯救无望 硬盘已经寄过去超过一周了,一问竟然是还没开始弄??? 再过一周,上来就问我分几个区?我要恢复哪些数据?我要恢复的数据在哪个位置? 那好吧,既然给了钱师傅也都放弃了,我也没什么好寄托希望的了。况且经过这三个...
继续阅读 »

硬盘坏了,一气之下写了个恢复程序


师傅拯救无望


硬盘已经寄过去超过一周了,一问竟然是还没开始弄???


2023-03-24-14-15-16.png


再过一周,上来就问我分几个区?我要恢复哪些数据?我要恢复的数据在哪个位置?


2023-03-24-14-18-50.png


2023-03-24-14-19-30.png


2023-03-24-14-20-05.png


那好吧,既然给了钱师傅也都放弃了,我也没什么好寄托希望的了。况且经过这三个星期的缓解,心情已经平复了很多,就像时光,回不来了就是回不来了。


自救之路


在把硬盘寄过去的时间里,等待师傅的修复结果的时间里,我并没有闲着(在摸鱼)。


经过调研,数据恢复方法通常有:



  • 硬件损坏,对坏的盘进行修复

  • 误删或逻辑错误等,文件扫描修复

  • git 重置恢复


很明显,这些都不适用于我现在的场景。因为师傅能不能修好是未知的,我只是数据盘没了,系统盘还在。由于 vscode 的数据目录空间占比较小,就没有搬迁到数据盘里,这刚好可以为恢复代码提供了可能。


这是因为新版 vscode 有一个时间线功能,这个时间线数据是默认存储在用户目录下的。


我从 C:/Users/love/AppData/Roaming/Code/User/History 目录中确实找到了很多名为 entries.json 的文件,结构如下:


{
// 配置版本
"version": 1,
// 原来文件所在位置
"resource": "file:///d%3A/git2/cloudcmd/.madrun.mjs",
// 文件历史
"entries": [
{
// 历史文件存储的名称
"id": "YFRn.mjs",
"source": "工作区编辑",
// 修改的时间
"timestamp": 1656583915880
},
{
"id": "Vfen.mjs",
"timestamp": 1656585664751
},
]
}

通过上面的文件大概可以看到,每一个时间点的文件都保存在另一个随机命名的文件里。而网上的方法基本都是自己一个个手动到目录里去根据最新的 id 去找对应的文件内容,然后创建文件并把内容复制出来。


这个过程恢复一两个文件还好,但我这可是要恢复整个 git 工作区,大概有几十个项目上千个文件。


这时候当然是在网上找找有没有什么 vscode 数据恢复 相关的工具,很遗憾找了大半天都没有找到。


气死我了,一气之下就自己写个!


恢复程序开发步骤


毕竟只要数据在磁盘上,无非就是一个文件读取操作的问题,还要拿在这水文章,见谅见谅。


首先考虑需求:



  • 我要实现一个自动扫描 vscode 数据目录

  • 然后以原始的目录结构还原出来,不需要我自己去创建文件夹和文件

  • 如果还原的文件最新的那份不是我想要的,我还能根据时间线进行对比和选择

  • 扫描出来有N个项目时,我可以指定只还原某此项目

  • 我可以搜索文件、目录名或文件内容进行还原

  • 为了方便,我还要一个看起来不太丑的操作界面


大概就上面这些吧。


然后考虑实现:


我要实现一个自动扫描 vscode 数据目录


要的就是我自己连数据目录和恢复地址也不需要填写,就能自动恢复的那种。那么就让程序来自动查找数据目录。经过调研,各版本的 vscode 的数据目录一般保存在这些地方:


参考: stackoverflow.com/a/72610691


  - win -- C:\Users\Mark\AppData\Roaming\Code\User\History
- win -- C:\Users\Mark\AppData\Roaming\Code - Insiders\User\History
- /home/USER/.config/VSCodium/User/History/
- C:\Users\USER\AppData\Roaming\VSCodium\User\History

大概有上面这些路径,当然不排除使用者故意把默认位置修改掉这种边缘情况,或者使用者就只想扫描某个数据目录的情况,所以我也要支持手动输入目录:


  let { historyPath, toDir } = req.body
const homeDir = os.userInfo().homedir
const pathList = [
historyPath,
`${homeDir}/AppData/Roaming/Code/User/History/`,
`${homeDir}/AppData/Roaming/Code - Insiders/User/History/`,
`${homeDir}/AppData/Roaming/VSCodium/User/History`,
`${homeDir}/.config/VSCodium/User/History/`,
]
historyPath = (() => {
return pathList.find((path) => path && fs.existsSync(path))
})()
toDir = toDir || normalize(`${process.cwd()}/re-store/`)

然后以原始的目录结构还原出来……


这就需要解析扫描到的时间线文件 entries.json 了。我们先把解析结果放到一个 list 中,以下是一个完整的解析方法。


然后再把列表转换为树型,与硬盘上的状态对应起来,这样便于调试数据和可视化。


function scan({ historyPath, toDir } = {}) {
const gitRoot = `${historyPath}/**/entries.json`

fs.existsSync(toDir) === false && fs.mkdirSync(toDir, { recursive: true })
const globbyList = globby.sync([gitRoot], {})

let fileList = globbyList.map((file) => {
const data = require(file)
const dir = path.parse(file).dir
// entries.json 地址
data.from = file
data.fromDir = dir
// 原文件地址
data.resource = decodeURIComponent(data.resource).replace(
/.*?\/\/\/(.*$)/,
`$1`
)
// 原文件存储目录
data.resourceDir = path.parse(data.resource).dir
// 恢复后的完整地址
data.rresource = `${toDir}/${data.resource.replace(/:\//g, `/`)}`
// 恢复后的目录
data.rresourceDir = `${toDir}/${path
.parse(data.resource)
.dir.replace(/:\//g, `/`)}
`

const newItem = [...data.entries].pop()
// 创建文件所在目录
fs.mkdirSync(data.rresourceDir, { recursive: true })
const binary = fs.readFileSync(`${dir}/${newItem.id}`, {
encoding: `binary`,
})
fs.writeFileSync(data.rresource, binary, { encoding: `binary` })
return data
})

const tree = pathToTree(fileList, { key: `resource` })
return tree
}

为了方便,我还要一个看起来不太丑的操作界面


我们要把文件树的形式展示出来,还要方便切换。后面决定使用 macos 的文件管理器风格,大概如下。


image.png


如果还原的文件最新的那份不是我想要的,我还能根据时间线进行对比和选择


理论上这里应该要做一个像 vscode 对比文件那样,有代码高亮功能,并且把有差异的字符高亮出来。


实际上,这个需求得加钱。


2023-03-24-15-09-25.png


由于界面是在浏览器里的,需要自动打开,浏览器与系统交互需要一个接口,所以我们使用 opener 来自动打开浏览器。


使用 get-port 来自动生成接口服务的端口,避免使用时出现占用。


  const opener = require(`opener`)
const { portNumbers, default: getPort } = await import(`get-port`)
const port = await getPort({ port: portNumbers(3000, 3100) })
const server = express()
server.listen(port, `0.0.0.0`, () => {
const link = `http://127.0.0.1:${port}`
opener(link)
})

封装成工具,我为人人


理论上我根本不需要什么 UI 界面,也不需要配置,因为我的文件都恢复出来了我还花时间去搞毛线?


实际上,万一别人也有这个恢复文件的需要呢?那么他只要运行下面这条命令代码就能立刻恢复到当前目录啦!


npx vscode-file-recovery

这就是恢复后的文件在硬盘里的样子啦:


2023-03-24-15-22-23.png


所有代码位于:



建议收藏,以备不时之需。/手动狗头


作者:程序媛李李李李李蕾
来源:juejin.cn/post/7213994684262826040
收起阅读 »

写在入职九周年这天,讲讲这些年的心路历程

往前翻翻才意识到已经很长很长时间没有写文章,大概具体有多长?感觉上有一光年那么长。 今天,刚好是入职九周年,竟然遇到周末,省了奶茶钱。是的,没错,我在一个窝里面趴了九年。 这些年,彷徨过,迷茫过,孤独过,也充满热血的奋斗过,激情的追求过,有犹豫,有脆弱,也有失...
继续阅读 »

往前翻翻才意识到已经很长很长时间没有写文章,大概具体有多长?感觉上有一光年那么长。


今天,刚好是入职九周年,竟然遇到周末,省了奶茶钱。是的,没错,我在一个窝里面趴了九年。


这些年,彷徨过,迷茫过,孤独过,也充满热血的奋斗过,激情的追求过,有犹豫,有脆弱,也有失落。在悠悠岁月中,能及时不断做出调整,让自己学会享受工作带来的乐趣,学会慢慢成长。在当下浮躁的时代,写些闲言碎语,给诸君放松下心情,缓解压力。


过去的那些年


入职那天,阳光明媚,清风柔和,大厦旁边的道路开满迎春花,连空气都是甜的,让人不由自主地深呼吸,可以闻到花香草香,还有阳光的味道。


那一天也是为数不多来上班较早的一天,哦吼有大草坪,哦哟还有篮球场,这楼还波浪线,牛批,B座这个大厅有点大,有点豪华,牛批牛批,头顶上这看着怎么像熊掌,12345,设计真不孬啊。慢慢的大厅上聚集了很多人,有点吵,咋也没人组织一下呢,大家都很随意的站着等待,陆陆续续有员工来上班。


“XXX”,听到有人喊我名字,吓我一跳,还以为偷看小姐姐被发现了。


“站到靠近电梯入口的最右一列,第一个位置上”,“XX,去站在他后面”,“大家按我叫名字的顺序排好队,咱们准备上楼了”。


那会儿,AI 还不会人脸识别过闸机。


呦呵,这公司真牛批,还有扶手电梯。跟着带路的同学来到三楼五福降中天会议室,一个挺老大的屋子,还有各种数不过来的高大上仪器电子设备,一周后,也是在这里,我和厂长面对面聊聊人生。坐稳扶好后,HR 同学开始入职培训,我摸摸新电脑,摸摸工卡牌,心里美滋滋,想到未来几年,将在这样美妙的环境中度过,喜不胜收,甚至我都闻到了楼下食堂啵啵鱼的香味。


培训刚结束。


“我叫到名字的同学,跟我来。XXX,XX……”,纳尼???中午还管饭??这福利也太好了吧,真不用吧,我自己能找到食堂,再说,你知道我喜欢吃什么吗?“跟着我走,咱们去北门做班车,去另一个办公楼,你们的工位不在这儿。”不在这?还坐班车?what?被外包了?她刚刚喊我了吗???差不多六七个同学,跟着楼长鱼贯而出,下楼,走小路,几分钟后,上了班车。司机大哥,一脚油门,就带着我远离啵啵鱼。


大约10分钟,也有可能是5分钟,或者15分钟,按照现在萝卜快跑无人车的速度,是7分钟。来到一栋圆了咕咚的,长的像长南瓜的楼,它有一个很科技感的名字,“首创空间”,就是这个空间,不仅给我带来技术的成长,还有十几斤的未来多年甩也甩不掉的肥肉。没有啵啵鱼的日子,相见便成为世上最奢侈的愿望。


大约是两年后的初春 ,准确的说,不到两年,记得是二、三月份,北京的PM2.5比较严重的时候,鼻子还不会过敏,也没学会发炎,眼睛也不知道怎么迎风流泪,总之,我们要搬家了。


“科技园”\color{#333333}“科技园”,听听,听听,多好的名字,长得像无穷大与莫比乌斯环,楼顶带跑道,位置也牛批,毗邻猪厂、鹅厂、渣浪,北邻联想,西靠壹号院,远眺百望山,低头写代码,啧啧,美滋滋的日子又来了。当时还没有共享单车,晚饭时蹭着班车和一群小伙伴过去看新工位,喏,不错不错,挺大,位置离厕所不远,不错不错,会议室安静舒适好多个,不错不错。重点来了,食堂大的离谱,还有很多美食,连吃几个月,基本不重样。吃过几次啵啵鱼,与大厦简直天壤之别,怀念。


机会说来就来,几个月后的一天,发生了一件大事。我回到了梦开始的地方,那让人朝思暮想的啵啵鱼,那让人魂牵梦绕的味道,那让人无法忘怀的美妙口感。


清醒一下.gif


命运说变就变,国庆休假回来,食堂换了运营商,我他么……¥#%@%#!@#@!&%


一直没变的:不忘初心,砥砺前行。


曾经觉得自己无所不能,可以改变世界,总幻想像蝴蝶一样扇扇翅膀,亚马逊的雨林就会刮起大风。食堂吃的多了,越来越认识到自己的影响力微乎其微,我们能做到,是把交代的工作做好,做到极致,应该就是对公司最大的回馈了,也对得起日渐增多的白发。


早些年,搞视频直播,课程学习,每天研究各种编解码技术,与视频流打交道,看过不少底层技术原理书籍,探索低延迟的 P2P 技术,枯燥,乏味,也跟不上时代变化,觉得自己会的那些早晚被淘汰,技术乏陈革新的速度超乎想象,而你所负责的,恰恰不是那些与时代贴合度较高的业务,边缘化。


怎么破?


从来没有人限制你,不允许你去学习。\color{red}从来没有人限制你,不允许你去学习。


因为恰巧在做课程的直播、录播,需要特别关注课程内容,主要担心出现线上问题,刚好利用这个契机,了解到很多跨专业,跨部门的业务,当时给自己的宗旨是,“只要有时间,就去听课”,“凡是所学,皆有收获”。前后积累近千小时的学习时长,现在想想,觉得都有些不可能,怎么做到的,是人吗?这是人干的事?


日常工作,专心不摸鱼,积极努力提高工作效率,解决研发任务,配合 peer 做好产品协同。晚饭后,专心研究 HTML大法,通勤路上手机看文档,学 api 用法,学习各种牛批的框架,技巧,逛各大论坛,写博客做积累,与各种人物斯比,每天晚上十点,跑步半小时,上床睡觉,生活很规律。


机缘巧合下,我终于从一个小坑,成功跳到一个大坑,并至今依然在坑中。那天,我想起了啵啵鱼。


16797732_0_final.png


可爱小熊猫,AI 还不会画牙、画手的阶段


一直在变的


团队在变,用两只手数了数,前前后后换了七次 leader,管理风格比五味杂陈还多一味,有的事无巨细,有的不闻不问,有的给你空间让你发挥,有的完全帮不上忙。怎么破?尊重,学习,并努力适应,不断调整心态,适应环境的变化。


业务在变,这么多年数过来,参与过的产品没有一百也有八十了,真正能够长期坚守下来的产品不多,机会可遇不可求,能得一二,实属幸运。把一款产品从零做到一,很容易;再做到十,很难但能够完成;再从十到百,几乎不可能。互联网公司怎么会有这样的产品存在,少之又少。


技能在变,经历过前端技术栈井喷的同学都深有体会,学不动的感受。


时代在变,社会在变,人心也在改变。


曾经多次想过换个环境,换一个坑趴着,毕竟很多机会还是很诱人的。印象最深的一次,是在某年夏天,对手头的工作实在是感到无聊。由于前一年小伙伴们的共同努力,产品架构设计相当完美,今年的工作接近于智力劳动转变为纯人力的重复的机械的体力劳动,对产品建设渐失激情,每天如同行尸走肉般的敲键盘,突然意识到,自己到了职业发展瓶颈期。如何抉择,走或留,临门一脚的事,至于这一脚踢向何方,还未知。


忧思几天后,去找 leader 沟通,好家伙,他让我呆在这里别动,帮他稳住团队,他要撤,一两个月的事。好家伙,你不殿后掩护我们,自己先撂了,还说可以试试带团队,我说大哥,也没几个人呀。他说你还能招兵买马,试试新的角色,体会下不同的视角,很好的机会。坑,绝对的大坑,我他么竟然义不容辞的答应了。


好在,不枉大家这么多年的认可,团队战斗力很强大。


你觉得什么是幸福



  • 有独处的时间

  • 有生活的追求

  • 工作能给你带来乐趣


颐和园.jpg


静悄悄的圆明园东路


前些日子,给娃拿药请了半天假,工作日人不多,十点多就完事了,看看时间地铁回去差不多到公司刚好中午饭。医院出来看到很多小黄车,美团那种新式的自行车,看着很不错,还没体验过,特别想兜几圈。查地图,距离公司有22公里,按照骑行的速度推算,70分钟也差不多到了。打定主意后,书包里翻出俩水煮蛋(鬼知道我为什么早上去公司拿了俩鸡蛋)和一瓶水(鬼使神差的早上往书包放的),算是吃过早饭了。于是一个人,一条狗,开局一把刀,沿着滨河路,经过木樨地,二里河,中关村南大街,北大街,信息路,上地西路回来了。您还别说,就是一个地道。竟然还路过玉渊潭,还遇到了封路限行,静悄悄的圆明园东路,过国图,还有数不清的大学,附中,有那么一瞬间好想回母校去看看,总之,重点是顺路吃到心心念的煎饼果子。


路上给媳妇打电话,这小妞竟然说我疯了,疯了?你懂个屁,这叫幸福。


人生的乐趣


人生的乐趣何在?你的答案和我的答案可能不一样,作为打工人,我知道,肯定不是工作。但似乎又不能没有工作,不工作我们怎么活着?怎么在这个社会上,换取资源,立足于当下,着眼于未来。说回工作,最后悔的事,曾经有那么一小段,人际关系没有处理好,可能造成误会,当时来自于我对某些事情的不表态,默许的态度,十周年前修复它。最快乐的时光,是和大家一起沉浸在技术点的探讨,Bug的跟进定位,发现问题解决问题的成就感;参与产品的规划,出谋划策,影响他人;挑灯夜战,专注于产品的 DDL,为上线争分夺秒的努力前行,感受团队的力量。


这个春天,爬过许多京郊的小山头,站在山顶,凝视着壮丽的景色,总以为自己是秦始皇。不惑之前,去征服贡嘎雪山。


总之,故事太多讲也讲不完,作为一个九年的老东西,我是不会爆金币的。


到结尾了,给点建议吧


建议?给不了给不了,我自己还没活明白。


历史的滚滚车轮中,每个生命都很渺小,时代一直在变,抓住机遇,让自己成长,多读书,沉下心,慢慢来。


16795669_0_final.png


作者:水鳜鱼肥
来源:juejin.cn/post/7222509109948989501
收起阅读 »

我们都有美好的未来

从善待厂毕业了,年终没有,季度奖也没有,好在N+1还有 同一个组的小伙伴吃了最后一顿散伙饭 后来,陆陆续续知道了其他人的动向 继博去了楼上的一家公司,做农民工讨薪的app,再后来听说快成为第一批用户了 阿森去了本地的一个大厂,每天10点他会跟我们讲他下班了 添...
继续阅读 »

从善待厂毕业了,年终没有,季度奖也没有,好在N+1还有


同一个组的小伙伴吃了最后一顿散伙饭


后来,陆陆续续知道了其他人的动向


继博去了楼上的一家公司,做农民工讨薪的app,再后来听说快成为第一批用户了


阿森去了本地的一个大厂,每天10点他会跟我们讲他下班了


添总去了城里,每天朝九晚六


浩宇回了内蒙去放羊


沐川转行不写Java了


文强回了重庆,住了院,听说因为工作生了一场病


我们都还在,都还有美好的未来


作者:think123
来源:juejin.cn/post/7154257335878189087
收起阅读 »

跨域漏洞,我把前端线上搞崩溃了

web
最近迁移前端资源到云厂商时,我遇到了一个奇怪的问题 —— 线上环境某个 CSS 资源跨域了,后果直接导致项目崩溃不可用。大领导直接找到了我的工位,激动的心,颤抖的手,问题原因就是找不到!!! 很奇怪,我已经提前设置了资源允许跨域访问,提测环境、测试环境也已经...
继续阅读 »

最近迁移前端资源到云厂商时,我遇到了一个奇怪的问题 —— 线上环境某个 CSS 资源跨域了,后果直接导致项目崩溃不可用。大领导直接找到了我的工位,激动的心,颤抖的手,问题原因就是找不到!!!


WX20230807-141353@2x.png


很奇怪,我已经提前设置了资源允许跨域访问,提测环境、测试环境也已经正常运行很久了,理论上不应该出现跨域问题。而且更奇怪的是,这个问题只出现在某个 CSS 文件上。


建议大家在阅读本文时结合目录一起查看。本文详细介绍了从跨域问题发现到跨域问题解决的整个过程,文章还简要提到了前端资源链路。结合上下文来看,对处理前端跨域问题具有一定的参考价值。希望对大家有所帮助。


什么是跨域问题?


跨域及其触发条件


跨域是指在 web 开发中,一个网页的源(origin)与另一个网页的源不同,即它们的协议、域名或端口至少有一个不同。跨域问题是由于浏览器的同源策略而产生的,它限制了一个网页中加载的资源只能来自同一个源,以防止恶意网站在未经允许的情况下访问其他网站的数据。


以下情况会触发跨域问题:



  1. 不同域名:当页面的域名与请求的资源的域名不一致时,会触发跨域问题,如从 example.com 页面请求资源来自 api.example.net

  2. 不同协议:如果页面使用了 https 协议加载,但试图请求非 https 资源,也会触发跨域问题。

  3. 不同端口:如果页面加载的是 example.com:3000,但试图请求资源来自 example.com:4000,同样会触发跨域问题。

  4. 不同子域名:即使是不同的子域名也会被认为是不同的源。例如,subdomain1.example.comsubdomain2.example.com 是不同的源。


image.png


跨域问题会影响到浏览器执行以下操作:



  • JavaScript的XMLHttpRequest或Fetch API请求其他源的资源。

  • 通过<img><link><script>等标签加载其他源的资源。

  • 使用CORS(跨源资源共享)机制实现跨域数据传输。


解决跨域的方法


解决跨域问题的方法有多种,具体的选择取决于你的应用场景。以下是一些常见的跨域解决方法:



  1. 跨域资源共享(CORS) :CORS是一种标准机制,通过在服务器端设置响应头来允许或拒绝跨域请求。这是解决跨域问题的最常见方法。

    • 在服务器端设置响应头中的Access-Control-Allow-Origin字段来指定允许访问的域名或使用通配符*表示允许所有域名访问。

    • 其他相关的CORS头,如Access-Control-Allow-MethodsAccess-Control-Allow-Headers,用于控制允许的HTTP方法和请求头。



  2. JSONP(JSON with Padding): 通过动态创建 <script> 标签来实现跨域请求的技术。服务器端返回的数据被包装在一个函数调用中,该函数名由客户端指定。虽然 JSONP 简单易用,但只支持GET请求,由于安全性较差(容易受到跨站脚本攻击),存在安全风险。
    // 客户端代码
    function handleResponse(data) {
    console.log('Received data:', data);
    }

    const script = document.createElement('script');
    script.src = 'https://example.com/api/data?callback=handleResponse';
    document.head.appendChild(script);


  3. 代理服务器:设置一个位于同一域的代理服务器,将跨域请求代理到目标服务器,并将响应返回给客户端。这个方法需要服务器端的额外配置。

  4. 跨文档消息传递: 使用window.postMessage()方法,可以在不同窗口或iframe之间进行跨域通信。

  5. WebSocket: WebSocket是一种双向通信协议,不受同源策略的限制。通过WebSocket,客户端和服务器可以建立持久连接进行跨域通信。

  6. Nginx反向代理: 使用 Nginx 或其他反向代理服务器可以将跨域请求转发到目标服务器,同时解决跨域问题。这种方法适用于前端无法直接控制目标服务器的情况。


每种方法都有其适用的场景和安全考虑,具体的选择取决于项目的需求和架构。


背景与跨域设置


image.png


项目背景介绍


最近我负责了一个前端迁移第三方云(阿里云)的工作,由于这是一个多项目组合成的微前端项目,我考虑在前端迁移中,尽可能统一各个应用项目流程、规范和技术。一是形成统一的规范和方式,二是团队项目各负责人按照方案迁移各自项目时避免因各自不一致导致出现问题。


而在这其中就存在着资源存储和加载不一致的情况,我遇到了三种不同的方法:




  1. 直接使用云存储提供的资源地址


    这是一种常见方式,但也伴随着一些潜在问题。首先,访问云资源可能会有一定的延迟,尤其对于大型文件或数据集。其次,公共云资源地址可能存在安全性和隐私风险,特别是涉及敏感信息时。此外,直接使用OSS资源地址可能导致资源管理分散,难以跟踪和监控资源的使用情况,也可能限制了一些高级功能的实现,如CDN缓存控制、分布式访问控制以及资源日志记录。


    https://company.oss-cn-beijing-internal.company.com/productdata/gateway/prod/assets/index-a12abcbf.css



  2. 使用配置了云存储的CDN域名地址


    这种方式是我比较推荐的方式,然而团队在配置这块链路上存在一些潜在问题。首先 CDN 请求打到了团队内部的一个老服务器,目前老服务器的稳定性不如预期,稳定性方面较差,出现过故障,影响用户体验。前端迁移到第三方云的主要目的之一就是解耦该服务,提供更稳定的前端资源环境。此外,该服务器与其他服务器存在依赖关系,增加了项目的复杂性和不稳定性,解耦是必要的。并且使用这个 CDN 的项目很多,随着时间推移,项目的增加可能会使得该资源地址的维护变得相当复杂。


    https://static.company.com/productdata/gateway/prod/assets/index-a12abcbf.css



  3. 直接加载服务器内部的前端资源


    直接加载服务器内部的前端资源是通过网站域名的代理访问服务器内部资源的一种方法。有几个项目使用这个种方式,这种方式具备简单、快捷等优势。然而,这种方式可能引入网络延迟和性能影响,因为资源请求需要经过团队内部的服务器。同时,存在单点故障的风险,如果内部服务器发生故障或不可用,将导致网站前端资源无法加载,可能对用户造成不便。它也依赖于团队内部的网络环境,需要保持网络的稳定性和可靠性以确保资源能够顺利加载。




为了统一这三种方式并规避潜在问题,我想到了一个综合性的前端资源请求链路方案。通过将 OSS 存储桶、CDN 和网关服务器相互结合,以提升资源分发速度和安全性,同时减轻 OSS 服务器的负载。此外,我还将所有资源引用集中到一个配置文件中,位于网关服务器,以便轻松进行维护和跟踪。(这里只是简要介绍,我将在后续文章分享详细细节


然而,在初步方案制定后,也需要考虑如何处理在同源策略下可能出现的跨域问题。


image.png


前端静态资源跨域设置


我在OSS存储桶的跨域设置中配置了允许跨域头,使得网页可以通过浏览器访问OSS资源,免受同源策略的限制。


image.png


为什么我会选择在 OSS 存储桶配置呢?


主要因为这个存储桶非常整洁,只有两个项目在使用,已经提前简单配置了跨域处理。而且这两个项目后续会按照前端迁移方案进行统一迁移处理,因此我认为直接在 OSS 存储桶配置跨域会更为简洁和可维护,我还和 SRE 老师调整了一下配置(然而,没想到恰恰因为我的这个行为,导致后面出现了跨域问题)。


此外,为了确保安全性,我采取了以下措施:



  • 将项目单独、分批迁移到阿里云 OSS。

  • 在网关服务器中使用nginx进行项目正则匹配,每次迁移就开放一个项目。
    location ~ ^/(gateway|message)/prod/

  • 项目在提测、测试环境都各自运行一段时间(有的甚至在1~2个月)。

  • 在未迁移到正式环境前,各项目按照各自排期计划进行过多次发版。


这些措施是为了确保有问题,可以在提测环境、测试环境中暴露出来。然而,在迁移第3个项目到正式服环境时,出现了问题。。。


奇怪的CSS资源跨域问题


为什么只有某个CSS文件受影响?


跨域问题通常由浏览器的同源策略引起,该策略限制了来自不同源的资源之间的交互。


如果资源有跨域问题,不应该只有某个CSS文件出现跨域问题呀?


3p55k2cus.png


分析后,我发现浏览器中 CSS 资源的返回头中缺少 CORS 头信息,截图如下:


image.png


正常情况下,应该是下图这样:


image.png


这时候我在想不应该呀,我已经在源站 OSS 存储桶配置了允许跨域头,这里的返回头中应用是要携带的,而且别的文件(如html、js)返回头中都是携带了允许跨域,但是为什么只有这个 CSS 资源的就没有呢?





需要注意的是,通常情况下,HTML 文件本身不受同源策略的限制,因此可以从不同源加载 CSS 文件。但如果 CSS 文件中包含引用其他跨域资源(如字体、图片等),那么同源策略仍然会生效,需要特别注意处理这些跨域资源的加载。


问题的深层原因分析


image.png


排除了自身导致的问题


面对这样一个看似简单的跨域问题,我做了一系列的排查和解决过程。首先,我排除了浏览器缓存、资源代码方面以及浏览器本身的问题,并同 SRE 老师否定了前端资源链路(如OSS、CDN)配置错误的可能性。随后,我们还仔细查看了网关日志,但未能发现问题。


一直没找到导致跨域问题出现的原因,我们也想到了直接在网关服务器或 CDN 中强制加入允许跨域头。然而我们一讨论,发现不行,因为 OSS 中已经配置了跨域,强制加入允许跨域头,会出现双重跨域问题;如果移除 OSS 中跨域头也不行,因为已经有两个项目已经直接引用阿里云地址,移除后那两个项目也会出现跨域问题。





寻求阿里云 CDN 运维工程师的帮助


结合我们自己的分析,我们认为是前端资源请求链路的哪个环节出现了问题,但是迟迟找不到原因,于是我们咨询了阿里云 CDN 运维工程师,因为阿里云 CDN 的日志隔天才出来,所以借此希望通过阿里云 CDN 运维老师能够查看下当天的 CDN 日志,从而找到问题。查看日志后,阿里云 CDN 运维老师也只是给出了日志显示一切正常,但随后我们继续沟通。


随后,给到了我们一个关键点:“OSS要能响应跨域头,请求头必须携带 Origin 请求头”。阿里云 CDN 运维老师也说传任何值都可以,但是我多次查看到浏览器请求已经携带了 Origin 请求头。如下图:


image.png


这就奇怪了!此时测试环境提测环境又无法复现 CORS 跨域问题,我们又不能直接在生产环境调试这个问题。


借助工具复现问题


于是我在思考是否能够在提测环境模拟出加载有问题资源的场景。我想到了可以通过拦截浏览器对提测环境的资源请求地址,并将其代理到具有问题的资源地址上来实现这个目的。为了实现这一方案,我使用了一个名为 GoRes 的谷歌浏览器插件。


image(2).png


成功复现,见下图:


3p55k2cus.png


随后,在多次代理调试中,我发现只有在正式服这个项目的资源地址中出现了这个问题。我和 SRE 老师一起再次确认了提测环境、测试环境和正式环境中各自网关服务器和 CDN 域名等的差异性,当然还是没发现问题!





问题逐渐浮现出水面


经过综合分析,我们怀疑 CDN 缓存可能是导致问题的原因。然而,我们无法直接查看缓存的资源,只能再次联系阿里云 CDN 的运维老师。经过多次沟通,我们得知如果客户端在第一次请求 CDN 时没有携带 Origin 请求头,CDN 就不会将 Origin 请求头传递到 OSS,OSS 因此不会响应跨域头,而后续 CDN 便会将没有跨域头的资源内容缓存下来。


这时我才意识到,OSS 内部存在着对 Origin 辨别的跨域处理机制。而在此之前,上传代码资源到 OSS 后,由于是正式环境,为了安全起见测试资源是否上传成功,我直接在浏览器中访问了一个 CSS 文件地址(当时请求到了资源,我还信心满满,丝毫没有注意到还有这么一个坑),但这一步的操作却间接成为了导致跨域问题出现的导火索


通常情况下,当网页加载跨域资源时,由于违反了同源策略,浏览器会自动添加源 Origin 到资源的请求头中。然而,由于我直接请求了 CSS 资源地址,未触发同源策略,浏览器也就没有自动添加 Origin 请求头,导致请求到的 OSS 资源中没有跨域头配置。这也就是为什么 CDN 缓存了没有跨域头的资源。


在网页加载资源时,由于 CDN 缓存了没有跨域头的资源,无论你如何请求,只要 CDN 缓存一直存在,浏览器加载的就是没有跨域头的资源。 因此,这也导致了资源跨域问题的出现。



本来是为了谨慎一点,提前验证资源是否已上传成功的操作,没想到却成为了跨域问题出现的导火索!!!



image.png


这个问题的教训很深刻,让我们意识到必须在向 OSS 请求资源时强制添加 Origin 请求头,以确保今后前端资源的稳定性。否则,这个问题将成为一个定时炸弹。我们决定在网关服务器上分工合作解决这个问题,以杜绝类似情况再次发生。这个经验教训也提醒了我和SRE老师要更加谨慎地处理类似操作,以避免潜在的问题。


如何稳定解决跨域问题


尽管我们已经找到了问题的根源,但是不排除是不是还有其他类似问题,为了保险起见,我决定还是缩小影响范围。在确保测试无问题后,逐步放开所有项目。


SRE 老师负责处理向 OSS 传递Origin请求头的部分,而我负责处理 Nginx location 的正则匹配项目的部分。以下是我们的网关服务器配置:


location ~ ^/(message|dygateway|logcenter)/tice/ { 
set $cors_origin "";
if ($http_origin = "" ) {
set $cors_origin 'static.example.com';
}
if ($http_origin != "") {
set $cors_origin $http_origin;
}
proxy_set_header Origin $cors_origin;
}



  • location ~ ^/(message|dygateway)/tice/:这是一个正则匹配,能更容易地添加或移除项目。




  • proxy_set_header Origin $cors_origin;:如果请求中包含 Origin 头部,它会被直接传递给 OSS;如果没有,它会被设置为一个值后再传递给 OSS。




配置完成后,直接在浏览器中请求下面这个资源地址,你会发现请求头并没有添加上去。这并不是配置出错,而是因为上面我们提到的CDN不仅缓存了资源,还缓存了请求头。


https://static.company.com/productdata/gateway-admin/prod/assets/index-a12abcbf.css

所以我们在这个资源的地址后面拼接了参数,相当于是请求新的 CDN 资源地址,此时可以发现跨域头已经添加上了。


https://static.company.com/productdata/gateway-admin/prod/assets/index-a12abcbf.css?abc=111

image.png


接下来就是在真实项目中测试下,首先在 CDN 后台刷新了有问题的项目资源文件目录,清除掉有跨域问题 CDN 资源缓存后。然后重新刷新浏览器,此时这个文件就成功加上了跨域的请求头,页面访问也正常了。





image.png


image.png


后面我又测试了多次,跨域问题彻底解决。为了避免以后出现类型的问题,所以我又整理了跨域资源共享(CORS)方案,希望对大家有用,请大家接着往下看。


跨域资源共享方案


image.png


跨域资源共享方案是解决前端资源跨域问题的最常见方法,可维护性强,配置简单,可以说这是业界普遍处理前端资源跨域的方式。下面我们将深入探讨三种不同的 CORS 配置方案,并分析各自的优缺点。


OSS存储桶配置跨域


我们都知道 OSS(对象存储服务)是阿里云提供的海量、安全、低成本、高可靠的云存储服务。但其实 OSS 也能设置跨域处理,可以让客户端前端应用从其他域名请求资源。


实施步骤:



  1. 登录阿里云控制台,找到对应的OSS存储桶。

  2. 进入存储桶的管理界面,选择“跨域设置”。

  3. 添加CORS规则,指定允许的来源、方法、头信息等。


image.png


优点:



  • 简单易用:配置简单,通过图形界面即可完成。

  • 安全性高:可以灵活控制允许访问的来源,减少安全风险。


缺点:



  • 依赖云服务商:此方法只适用于使用阿里云OSS的情况,不适用于其他云服务商或自建服务器。


注意:OSS存储桶配置完成跨域后,需要在请求 OSS 存储桶资源时,在请求头中配置 Origin。因为 OSS 内部的机制是 OSS 响应跨域头的前提是必须要携带源站Origin请求头。 建议大家强制配置必传 Origin 请求头,否则容易出现我这次的问题。使用OSS存储桶配置跨域制定方案时,可以参考我在上面的处理:“如何稳定解决跨域问题”。


网关服务器配置跨域


在网关服务器配置跨域,网关服务器通常配置了 Nginx 反向代理服务器。通过配置 Nginx location,可以实现对特定域名的允许跨域支持。


实施步骤:



  1. 修改nginx配置文件(通常位于/etc/nginx/nginx.conf),添加CORS相关配置。

  2. 配置Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers等头信息。


location / {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}

优点:



  • 灵活性高:可以自由配置适应特定需求。

  • 适用性广:适用于各种服务器环境,不依赖特定云服务商。


缺点:



  • 配置复杂:需要熟悉nginx配置。


CDN配置跨域


CDN(内容分发网络)是一种通过将内容缓存到全球各地节点,加速用户访问速度的网络服务。但也能通过 CDN 配置 CORS,可以在边缘节点处实现跨域。


实施步骤:



  1. 登录CDN服务提供商的控制台,找到相应CDN加速域名。

  2. 进入域名配置界面,找到CORS配置选项。

  3. 添加CORS规则,指定允许的来源、方法、头信息等。


image.png


优点:



  • 高性能:CDN 服务通常提供全球分发,可以加速跨域请求,提供更好的性能。

  • 规模化:适用于大规模的Web应用,可支持高并发的跨域请求。


缺点:



  • 成本:使用 CDN 服务可能会产生额外的费用,特别是对于大量的数据传输。

  • 配置复杂性:相对于 OSS 或 Nginx,CDN 的配置可能会更为复杂,需要在控制台进行详细的设置。


注意:腾讯云 CDN 中有专门针对跨域设置的勾选项,只需要选中保存就行。


三种跨域处理方案各有优缺点,选择合适的方案取决于具体的业务需求和技术栈。我上面所说的也只供大家参考,毕竟 CDN、存储桶这种很大程度受限于云平台,这也是我把允许跨域配置在网关服务器的原因之一。可以综合考虑选择合适的方案或者结合多种方案来实现跨域资源共享。


u=2094032080,194978745&fm=30&app=106&f=JPEG.jpeg


结语


前端资源加载问题往往受多种因素的影响,包括 CDN 配置、资源请求链路、云存储配置等。因此,需要全面分析并综合考虑可能出现问题的任何风险点。也要合理使用浏览器插件工具、网络抓包工具和服务器日志分析等工具,可以帮助我们更快速地诊断和解决问题。如果问题复杂或涉及云服务配置,与云厂商的支持团队联系可以提供专业的帮助。


这是我关于资源跨域的一篇文章,里面关于定位问题和跨域方案希望对您有所帮助和参考。如果您需要进一步的协助或有任何问题,请随时提问!


作者:Sailing
来源:juejin.cn/post/7279429009796546623
收起阅读 »

问个事,我就用Tomcat,不用Nginx,行不行!

只用Tomcat,不用Nginx搭建Web服务,行不行?我曾经提出的愚蠢问题,今天详细给自己解释下,为什么必须用Nginx! 不用Nginx,只用Tomcat的Http请求流程 浏览器处理一个Http请求时,会首先通过DNS服务器找到域名关联的IP地址,然后请...
继续阅读 »

只用Tomcat,不用Nginx搭建Web服务,行不行?我曾经提出的愚蠢问题,今天详细给自己解释下,为什么必须用Nginx!


不用Nginx,只用Tomcat的Http请求流程


浏览器处理一个Http请求时,会首先通过DNS服务器找到域名关联的IP地址,然后请求到对应的IP地址。以阿里云域名管理服务为例,一个域名可以最多绑定三个IP地址,这三个IP地址需要是公网IP地址,所以首先需要在三个公网Ip服务器上部署Tomcat实例。


此时我将面临的麻烦如下



  1. 由于DNS域名管理绑定的IP地址有限,最多三个,你如果想要扩容4台Tomcat,是不支持的。无法满足扩容的诉求

  2. 如果你有10个服务,对应10套Tomcat集群,就需要10 * 3台公网Ip服务器。成本还是蛮高的。

  3. 10个服务需要对应10个域名,分别映射到对应的Tomcat集群

  4. 10个域名我花不起这个钱啊!(其实可以用二级域名配置DNS映射)

  5. 公网服务器作为接入层需要有防火墙等安全管控措施,30台公网服务器,网络安全运维,我搞不定。

  6. 公网IP地址需要额外从移动联通运营商或云厂商购买,30个公网IP价格并不便宜。

  7. 前后端分离的情况,Tomcat无法作为静态文件服务器,只能用Nginx或Apache


以上几个问题属于成本、安全、服务扩容等方面。


如果Tomcat服务发布怎么办


Tomcat在服务发布期间是不可用的,在发布期间Http请求打到发布的服务器,就会失败。由于DNS 最多配置3台服务器,也就是发布期间是 1/3 的失败率。 我会被老板枪毙,用加特林


DNS不能自动摘掉故障的IP地址吗?


不能,DNS只是负责解析域名对应的IP地址,他并不知道对应的服务器状态,更不会知道服务器上Tomcat的状态如何。DNS只是解析IP,并没有转发Http请求,所以压根不知道哪台服务器故障率高。更无法自动摘掉IP地址。


我能手动下掉故障的IP地址吗?


这个我能,但是还是会有大量请求失败。以阿里云为例,配置域名映射时,我可以下掉对应的IP地址,但需要指定域名映射的缓存时间,默认10分钟。换句话说,就算你在上线前,摘掉了对应的IP,依然要等10分钟,所有的客户端才会拿到最新的DNS解析地址。


那么把TTL缓存时间改小,可以吗? 可以的,但是改小了,就意味更多的请求被迫从DNS服务器拿最新的映射,整体请求耗时增加,用户体验下降!被老板发现,会骂我。


节点突然挂掉怎么办?


虽然可以在DNS管理后台手动下掉IP地址,但是节点突然宕机、Tomcat Crash等因素导致的突然故障,我是来不及下掉对应IP地址的,我只能打电话告诉老板,“线上服务崩了,你等我10分钟改点东西”。


如果这时候有个软件能 对Tomcat集群健康检查和故障重试,那就太好了。


恰好,这是 Nginx 的长处!


Nginx可以健康检查和故障重试


而Tomcat没有。


例如有两台Tomcat节点,在Nginx配置故障重试策略


upstream test {
server 127.0.0.1:8001 fail_timeout=60s max_fails=2; # Server A
server 127.0.0.1:8002 fail_timeout=60s max_fails=2; # Server B
}

当A节点出现 connect refused时(端口关闭或服务器挂了),说明服务不可用,可能是服务发布,也可能是服务器挂了。此时nginx会把失败的请求自动转发到B节点。 假设第二个请求 请求到A还是失败,正好累计2个失败了,那么Nginx会自动把A节点剔除存活列表 60 秒,然后继续把请求2 转发到B节点进行处理。60秒后,再次尝试转发请求到A节点…… 循环往复,直至A节点活过来……


而这一过程客户端是感知不到失败的。因为两次请求都二次转发到B节点成功处理了。客户端并不会感知到A节点的处理失败,这就是Nginx 反向代理的好处。即客户端不用直连服务端,加了个中间商,服务端的个别节点宕机或发布,对客户端都毫无影响。


而Tomcat只是Java Web容器,并不能做这些事情。


10个服务,10个Tomcat集群,就要10个域名,30个公网IP吗?


以阿里云为例,域名管理后台是可以配置二级域名映射,所以一个公网域名拆分为10个二级域名就可以了。


所以只用Tomcat,不用Nginx。需要1个公网域名,10个二级域名,30台服务器、30个公网IP。


当我和老板提出这些的时候,他跟我说:“你XX疯了,要不滚蛋、要不想想别的办法。老子没钱,你看我脑袋值几个钱,拿去换公网IP吧”。


image.png


心里苦啊,要是能有一个软件,能帮我把一个域名分别映射到30个内网IP就好了。


恰好 Nginx可以!


Nginx 虚拟主机和反向代理


例如把多个二级域名映射到不同的文件目录,例如



  1. bbs.abc.com,映射到 html/bbs

  2. blog.abc.com 映射到 html/blog


http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name http://www.abc.com;
location / {
root html/www;
index index.html index.htm;
}
}

server {
listen 80;
server_name bbs.abc.com;
location / {
root html/bbs;
index index.html index.htm;
}
}

server {
listen 80;
server_name blog.abc.com;
location / {
root html/blog;
index index.html index.htm;
}
}
}

例如把不同的二级域名或者URL路径 映射到不同的 Tomcat集群



  1. 分别定义 serverGroup1、serverGroup2 两个Tomcat集群

  2. 分别把路径group1、group1 反向代理到serverGroup1、serverGroup2


upstream serverGroup1 {                    # 定义负载均衡设备的ip和状态
server 192.168.225.100:8080 ; # 默认权重值为一
server 192.168.225.101:8082 weight=2; # 值越高,负载的权重越高
server 192.168.225.102:8083 ;
server 192.168.225.103:8084 backup; # 当其他非backup状态的server 不能正常工作时,才请求该server,简称热备
}

upstream serverGroup2 { # 定义负载均衡设备的ip和状态
server 192.168.225.110:8080 ; # 默认权重值为一
server 192.168.225.111:8080 weight=2; # 值越高,负载的权重越高
server 192.168.225.112:8080 ;
server 192.168.225.113:8080 backup; # 当其他非backup状态的server 不能正常工作时,才请求该server,简称热备
}

server { # 设定虚拟主机配置
listen 80; # 监听的端口
server_name picture.itdragon.com; # 监听的地址,多个域名用空格隔开
location /group1 { # 默认请求 ,后面 "/group1" 表示开启反向代理,也可以是正则表达式
root html; # 监听地址的默认网站根目录位置
proxy_pass http://serverGroup1; # 代理转发
index index.html index.htm; # 欢迎页面
deny 127.0.0.1; # 拒绝的ip
allow 192.168.225.133; # 允许的ip
}
location /group2 { # 默认请求 ,后面 "/group2" 表示开启反向代理,也可以是正则表达式
root html; # 监听地址的默认网站根目录位置
proxy_pass http://serverGroup2; # 代理转发
index index.html index.htm; # 欢迎页面
deny 127.0.0.1; # 拒绝的ip
allow 192.168.225.133; # 允许的ip
}

error_page 500 502 503 504 /50x.html;# 定义错误提示页面
location = /50x.html { # 配置错误提示页面
root html;
}
}

经过以上的教训,我再也不会犯这么愚蠢的错误了,我需要Tomcat,也需要Nginx。


当然如果钱足够多、资源无限丰富,公网IP、公网服务器、域名无限…… 服务发布,网站崩溃,无动于衷,可以不用Nginx。


作者:他是程序员
来源:juejin.cn/post/7280088532377534505
收起阅读 »

别再用 float 布局了,flex 才是未来!

web
大家好,我是树哥! 前面一篇文章整体介绍了 CSS 的布局知识,其中说到 float 布局是 CSS 不断完善的副产物。而在 2023 年的今天,flex 这种布局方式才是未来!那么今天我们就来学习下 flex 弹性布局。 什么是 Flex 布局? 在经过了长...
继续阅读 »

大家好,我是树哥!


前面一篇文章整体介绍了 CSS 的布局知识,其中说到 float 布局是 CSS 不断完善的副产物。而在 2023 年的今天,flex 这种布局方式才是未来!那么今天我们就来学习下 flex 弹性布局。


什么是 Flex 布局?


在经过了长达 10 年的发展之后,CSS3 才终于迎来了一个简单好用的布局属性 —— flex。Flex 布局又称弹性布局,它使用 flexbox 属性使得容器有了弹性,可以自动适配各种设备的不同宽度,而不必依赖于传统的块状布局和浮动定位。


举个很简单地例子,如果我们想要实现一个很简单左侧定宽,右侧自适应的导航布局,如下图所示。


-w1239


在没有 flex 之前,我们的代码是这么写的。


<div>
<h1>4.1 两栏布局 - 左侧定宽、右侧自适应 - float</h1>
<div class="container">
<div class="left41"></div>
<div class="right41"></div>
</div>
</div>

/** 4.1 两栏布局 - 左侧定宽、右侧自适应 - float **/
.left41 {
float: left;
width: 300px;
height: 500px;
background-color: pink;
}
.right41 {
width: 100%;
height: 500px;
background-color: aquamarine;
}

这种方式不好的地方在于,我们还需要去理解 float 这个概念。一旦需要理解 float 这个概念,我们就会拖出一大堆概念,例如文档流、盒子模型、display 等属性(虽然这些东西确实应该学)。但对于 flex 来说,它就很简单,只需要设置一个伸缩系数即可,如下代码所示。


<div>
<h1>4.2 两栏布局 - 左侧定宽、右侧自适应 - flex</h1>
<div class="container42">
<div class="left42"></div>
<div class="right42"></div>
</div>
</div>

.container42 {
display: flex;
}
.left42 {
width: 300px;
height: 500px;
background-color: pink;
}
.right42 {
flex: 1;
width: 100%;
height: 500px;
background-color: aquamarine;
}

上面的代码里,我们只需要将父级容器设置为 flex 展示形式(display: flex),随后在需要自动伸缩的容器里设置属性即可。上面代码中的 flex: 1 表示其占据所有其他当行所剩的空间。通过这样的方式,我们非常方便地实现了弹性布局。


当然,上面只是一个最简单的例子,甚至还不是很能体现出 flex 的价值。flex 除了在响应式布局方面非常方便之外,它在对齐等方面更加方便,能够极大地降低学习成本、提高工作效率。


Flex 核心概念


对于 Flex 布局来说,其有几个核心概念,分别是:主轴与交叉轴、起始线和终止线、Flex 容器与 Flex 容器项。


主轴和交叉轴


在 Flex 布局中有一个名为 flex-direction 的属性,可以取 4 个值,分别是:



  • row

  • row-reverse

  • column

  • column-reverse


如果你选择了 row 或者 row-reverse,那么主轴(Main Axis)就是横向的 X 轴,交叉轴(Cross Axis)就是竖向的 Y 轴,如下图所示。


主轴是横向的X轴,交叉轴是竖向的Y轴


如果你选择了 column 或者 column-reverse,那么主轴(Main Axis)就变成是竖向的 Y 轴,交叉轴(Cross Axis)就是横向的 X 轴,如下图所示。


主轴是竖向的Y轴,交叉轴是横向的X轴


起始线和终止线


过去,CSS 的书写模式主要被认为是水平的,从左到右的。但现代的布局方式涵盖了书写模式的范围,所以我们不再假设一行文字是从文档的左上角开始向右书写的。


对于不同的语言来说,其书写方向不同,例如英文是从左到右,但阿拉伯文则是从右到左。那么对于这两种语言来说,其xx会有所不同 TODO。举个简单的例子,如果 flex-direction 是 row ,并且我是在书写英文。由于英文是从左到右书写的,那么主轴的起始线是左边,终止线是右边,如下图所示。


-w557


但如果我在书写阿拉伯文,由于阿拉伯文是从右到左的,那么主轴的起始线是右边,终止线是左边,如下图所示。


-w541


在 Flex 布局中,起始线和终止线决定了 Flex 容器中的 Flex 元素从哪个方向开始排列。 举个简单例子,如果我们通过 direction: ltr 设置了文字书写方向是从左到右,那么起始线就是左边,终止线就是右边。此时,如果我们设置的 flex-direction 值是 row,那么 Flex 元素将会从左到右开始排列。但如果我们设置的 flex-direction 值是 row-reverse,那么 Flex 元素将会从右到左开始排列。


在上面的例子中,交叉轴的起始线是 flex 容器的顶部,终止线是底部,因为两种语言都是水平书写模式。但如果有一种语言,它的书写形式是从底部到顶部,那么当设置 flex-direction 为 column 或 column-reverse 时,也会有类似的变化。


Flex 容器与 Flex 元素


我们把一个容器的 display 属性值改为 flex 或者 inline-flex 之后,该容器就变成了 Flex 容器,而容器中的直系子元素就会变为 flex 元素。如下代码所示,parent 元素就是 Flex 容器,son 元素就是 Flex 元素。


<style>
#parent {
display: flex;
}
</style>
<div id="parent">
<div id="son"></div>
</div>

Flex 核心属性


对于 Flex 来说,它有非常多的用法,但核心属性却相对较少。这里我只简单介绍几个核心属性,如果你想了解更多 Flex 的属性,可以去 Mozilla 官网查询,这里给个传送门:flex 布局的基本概念 - CSS:层叠样式表 | MDN


对于 Flex 布局来说,其核心属性有如下几个:



  1. flex-direction 主轴方向

  2. flex 伸缩系数及初始值

  3. justify-content 主轴方向对齐

  4. align-items 交叉轴方向对齐


flex-direction 主轴方向


如上文所介绍过的,flex-direction 定义了主轴的方向,可以取 4 个值,分别是:



  • row 默认值

  • row-reverse

  • column

  • column-reverse


一旦主轴确定了,交叉轴也确定了。主轴和交叉轴与后续的对齐属性有关,因此弄懂它们非常重要!举个很简单的例子,如下的代码将展示下图的展示效果。


.box {
display: flex;
flex-direction: row-reverse;
}

<div class="box">
<div>One</div>
<div>Two</div>
<div>Three</div>
</div>

-w538


如果你将 flex-direction 改成 column-reverse,那么将会变成如下的效果,如下图所示。


-w541


flex 伸缩系数及初始值


前面说到 Flex 布局可以很方便地进行响应式布局,其实就是通过 flex 属性来实现的。flex 属性其实是 flex-grow、flex-shrink、flex-basis 这三个参数的缩写形式,如下代码所示。


flex-grow: 1;
flex-shrink: 1;
flex-basis: 200px;
/* 上面的设置等价于下面 flex 属性的设置 */
flex: 1 1 200px;

在考虑这几个属性的作用之前,需要先了解一下 可用空间 available space 这个概念。这几个 flex 属性的作用其实就是改变了 flex 容器中的可用空间的行为。


假设在 1 个 500px 的容器中,我们有 3 个 100px 宽的元素,那么这 3 个元素需要占 300px 的宽,剩下 200px 的可用空间。在默认情况下,flexbox 的行为会把这 200px 的空间留在最后一个元素的后面。


-w537


如果期望这些元素能自动地扩展去填充满剩下的空间,那么我们需要去控制可用空间在这几个元素间如何分配,这就是元素上的那些 flex 属性要做的事。


flex-basis


flex-basis 属性用于设置 Flex 元素的大小,其默认值是 auto。此时浏览器会检查元素是否有确定的尺寸,如果有确定的尺寸则用该尺寸作为 Flex 元素的尺寸,否则就采用元素内容的尺寸。


flex-grow


flex-grow 若被赋值为一个正整数,flex 元素会以 flex-basis 为基础,沿主轴方向增长尺寸。这会使该元素延展,并占据此方向轴上的可用空间(available space)。如果有其他元素也被允许延展,那么他们会各自占据可用空间的一部分。


举个例子,上面的例子中有 a、b、c 个 Flex 元素。如果我们给上例中的所有元素设定 flex-grow 值为 1,容器中的可用空间会被这些元素平分。它们会延展以填满容器主轴方向上的空间。


但很多时候,我们可能都需要按照比例来划分剩余的空间。此时如果第一个元素 flex-grow 值为 2,其他元素值为 1,则第一个元素将占有 2/4(上例中,即为 200px 中的 100px), 另外两个元素各占有 1/4(各 50px)。


flex-shrink


flex-grow 属性是处理 flex 元素在主轴上增加空间的问题,相反 flex-shrink 属性是处理 flex 元素收缩的问题。如果我们的容器中没有足够排列 flex 元素的空间,那么可以把 flex 元素 flex-shrink 属性设置为正整数,以此来缩小它所占空间到 flex-basis 以下。


与flex-grow属性一样,可以赋予不同的值来控制 flex 元素收缩的程度 —— 给flex-shrink属性赋予更大的数值可以比赋予小数值的同级元素收缩程度更大。


justify-content 主轴方向对齐


justify-content 属性用来使元素在主轴方向上对齐,它的初始值是 flex-start,即元素从容器的起始线排列。justify-content 属性有如下 5 个不同的值:



  • flex-start:从起始线开始排列,默认值。

  • flex-end::从终止线开始排列。

  • center:在中间排列。

  • space-around:每个元素左右空间相等。

  • space-between:把元素排列好之后,剩余的空间平均分配到元素之间。


各个不同的对齐方式的效果如下图所示。


flex-start:


-w454


flex-end:


-w444


center:


-w449


space-around:


-w442


space-between:


-w453


align-items 交叉轴方向对齐


align-items 属性可以使元素在交叉轴方向对齐,它的初始值是 stretch,即拉伸到最高元素的高度。align-items 属性有如下 5 个不同的值:



  • stretch:拉伸到最高元素的高度,默认值。

  • flex-start:按 flex 容器起始位置对齐。

  • flex-end:按 flex 容器结束为止对齐。

  • center:居中对齐。

  • baseline:始终按文字基线对齐。


各个不同的对齐方式的效果如下图所示。


stretch:


-w448


flex-start:


-w439


flex-end:


-w438


center:


-w444


要注意的事,无论 align-items 还是 justify-content,它们都是以主轴或者交叉轴为参考的,而不是横向和竖向为参考的,明白这点很重要。


Flex 默认属性


由于所有 CSS 属性都会有一个初始值,所以当没有设置任何默认值时,flex 容器中的所有 flex 元素都会有下列行为:



  • 元素排列为一行 (flex-direction 属性的初始值是 row)。

  • 元素从主轴的起始线开始。

  • 元素不会在主维度方向拉伸,但是可以缩小。

  • 元素被拉伸来填充交叉轴大小。

  • flex-basis 属性为 auto。

  • flex-wrap 属性为 nowrap。


弄清楚 Flex 元素的默认值有利于我们更好地进行布局排版。


实战项目拆解


看了那么多的 Flex 布局知识点,总感觉干巴巴的,是时候来看看别人在项目中是怎么用的了。


-w1290


上面是我在 CodePen 找到的一个案例,这样的一个布局就是用 Flex 布局来实现的。通过简单的分析,其实我们可以拆解出其 Flex 布局方法,大致如下图所示。


-w1297


首先整体分为两大部分,即导航栏和内容区域,这部分的主轴纵向排列的(flex-direction: column),如上图红框部分。随后在内容区域,又将其分成了左边的导航栏和右边的内容区域,此时这块内容是横向排列的(flex-direction: row),如下上图蓝框部分。


剩下的内容布局也大致类似,其实就是无限套娃下去。由于偏于原因,这里就不继续深入拆解了,大致的布局思路已经说得很清楚了。


有了 Flex 布局之后,貌似布局也变得非常简单了。但纸上得来终觉浅,还是得自己实际动手练练才知道容易不容易,不然就变成纸上谈兵了!


总结


看到这里,关于 Flex 布局的核心点就介绍得差不多了。掌握好这几个核心的知识点,开始去实践练习基本上没有什么太大的问题了。剩下的一些比较小众的属性,等用到的时候再去查查看就足够了。


接下来更多的时间,就是找多几个实战案例实践,唯有实践才能巩固所学知识点。后面有机会,我将分享我在 Flex 布局方面的项目实践。


如果这篇文章对你有帮助,记得一键三连支持我!


参考资料



作者:树哥聊编程
来源:juejin.cn/post/7280054182996033548
收起阅读 »

看完这位小哥的GitHub,我沉默了

web
就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。 而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。 出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。...
继续阅读 »

就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。



而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。



出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。


即:在网页端实现了Windows 12的UI界面和交互效果。


这里也放几张图片感受一下。



  • 登录页面




  • 开始菜单




  • 资源管理器




  • 设置




  • 终端命令行




  • AI Copilot




  • 其他应用



这个项目的灵感来源于作者之前看到 Windows 12 概念版后深受启发,于是决定做一个Windows12网页版(就像之前的 Windows 11 网页版一样),可以让用户在网络上预先体验 Windows 12。


可以看到,这个项目是一个前端开源项目,而且由标准前端技术(HTML,JS,CSS)来实现,下载代码,无需安装,打开desktop.html即可。



项目包含:



  • 精美的UI设计

  • 流畅丰富的动画

  • 各种高级的功能(相较于网页版)


不仅如此,作者团队对于该项目的后续发展还做了不少规划和畅想。



  • 项目规划




  • 项目畅想



刚上面也说了,项目README里写道该项目的作者是一位14岁的初中生,网名叫星源,曾获得CSP普及组一等奖和蓝桥杯国赛三等奖。


作者出生于2009年,在成都上的小学和初中,目前刚上初三。


这样来看的话,虽说作者年龄很小,不过接触计算机和编程应该非常早,而且对计算机领域的知识和技术应该有着非常浓厚的兴趣。


从作者的个人主页里能看到,技术栈这块涉猎得还挺广泛。



作者自己表示如今上初三了,对于win12这个项目也不会做什么功能的更新了,后续的维护更新将交给其他贡献者成员。



文章的结尾也附上Windows 12网页版体验地址:tjy-gitnub.github.io/win12/desktop.html,感兴趣的同学可以自行体验。


聊到这里不得不说,人与人之间的差距确实挺大的。就像这位小哥,才14岁就已经精通前端技术了。


而14岁的我,当年在干嘛呢?


我想了又想。。


额,我好像在网吧里玩红警。。(手动doge)


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

蒙提霍尔问题

最近看韩国电视剧【D.P:逃兵追缉令】里面提到一个有趣的数学概率游戏 -> 蒙提霍尔问题 意思是:参赛者会看见三扇门,其中一扇门的里面有一辆汽车,选中里面是汽车的那扇门,就可以赢得该辆汽车,另外两扇门里面则都是一只山羊,让你任意选择其中一个,然后打开其余...
继续阅读 »



最近看韩国电视剧【D.P逃兵追缉令】里面提到一个有趣的数学概率游戏 -> 蒙提霍尔问题


意思是:参赛者会看见三扇门,其中一扇门的里面有一辆汽车,选中里面是汽车的那扇门,就可以赢得该辆汽车,另外两扇门里面则都是一只山羊,让你任意选择其中一个,然后打开其余两个门中的一个并且是山羊(去掉一个错误答案),这时,让你重新选择。那么你是会坚持原来的选择,还是换选另外一个未被打开过的门呢?


大家可以想一想如果是自己,我们是会换还是不会换?


好了,我当时看到后感觉很有意思,所以我简单写了一套代码,源码贴在下面,大家可以验证一下,先告诉大家,换赢得汽车的概率是2/3,不换赢得汽车的概率是1/3

<header>
<h1>请选择换不换?</h1><button class="refresh">刷新</button>
</header>
<section>
<div class="box">
<h2>1</h2>
<canvas width="300" height="100"></canvas>
<div class="prize">奖品</div>
</div>
<div class="box">
<h2>2</h2>
<canvas width="300" height="100"></canvas>
<div class="prize">奖品</div>
</div>
<div class="box">
<h2>3</h2>
<canvas width="300" height="100"></canvas>
<div class="prize">奖品</div>
</div>
</section>
<span>请选择号码牌</span>
<select name="" id="">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button class="confirm">确认</button>
<span class="confirm-text"></span>
<span class="opater">
<button class="change">换</button>
<button class="no-change">不换</button>
</span>
<p>
<strong>游戏规则:</strong>
<span>
上面有三个号码牌,其中一个号码牌的里面有汽车,选中里面是汽车的号码牌,
你就可以赢得该辆汽车,另外两个号码牌里面则都是一只山羊,
你任意选择其中一个,然后打开其余两个号码牌中的一个并且是山羊(去掉一个错误答案),
这时,你有一个重新选择的机会,你选择换还是不换?
</span>
</p>
.prize {
width: 300px;
height: 100px;
background-color: pink;
font-size: 36px;
line-height: 100px;
text-align: center;
position: absolute;
}

canvas {
position: absolute;
z-index: 2;
}

section {
display: flex;
}

.box {
width: 300px;
height: 200px;
cursor: pointer;
}

.box+.box {
margin-left: 8px;
}

header {
display: flex;
align-items: center;
}

header button {
margin-left: 8px;
height: 24px;
}
p {
width: 400px;
background-color: pink;
}
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function getRandomNumber() {
return Math.random() > 0.5 ? 1 : 2;
}
let a1 = [0, 1, 2]
let i1 = undefined
let i2 = undefined
let isChange = false
const opater = document.querySelector('.opater')
opater.style.display = 'none'
// 随机一个奖品
const prizes = document.querySelectorAll('.prize')
let a0 = [0, 1, 2]
a0 = shuffleArray(a0)
a0.forEach((v,i) => {
const innerText = !!v ? '山羊' : '汽车'
prizes[i].innerText = innerText
})

const canvas = document.querySelectorAll('canvas')
const confirmText = document.querySelector('.confirm-text')
canvas.forEach(c => {
// 使用canvas实现功能
// 1. 使用canvas绘制一个灰色的矩形
const ctx = c.getContext('2d')
ctx.fillStyle = '#ccc'
ctx.fillRect(0, 0, c.width, c.height)
// 2. 刮奖逻辑
// 鼠标按下且移动的时候,需要擦除canvas画布
let done = false
c.addEventListener('mousedown', function () {
if (i1 === undefined) return alert('请先选择号码牌,并确认!')
if (!isChange) return alert('请选择换不换!')
done = true
})
c.addEventListener('mousemove', function (e) {
if (done) {
// offsetX 和 offsetY 可以获取到鼠标在元素中的偏移位置
const x = e.offsetX - 5
const y = e.offsetY - 5
ctx.clearRect(x, y, 10, 10)
}
})
c.addEventListener('mouseup', function () {
done = false
})
})
const confirm = document.querySelector('.confirm')
const refresh = document.querySelector('.refresh')
confirm.onclick = function () {
let select = document.querySelector('select')
const options = Array.from(select.children)
confirmText.innerText = `您选择的号码牌是${select.value},请问现在换不换?`
// 选择后,去掉一个错误答案
// i1是下标
i1 = select.value - 1
// delValue是值
let delValue = undefined
// 通过下标找值
if (a0[i1] === 0) {
delValue = getRandomNumber()
} else {
delValue = a0[i1] === 1 ? 2 : 1
}
// 通过值找下标
i2 = a0.indexOf(delValue)
// 选择的是i1, 去掉的是
const ctx = canvas[i2].getContext('2d')
ctx.clearRect(0, 0, 300, 100)
options.map(v => v.disabled = true)
confirm.style.display = 'none'
opater.style.display = 'inline-block'
}
const change = document.querySelector('.change')
const noChange = document.querySelector('.no-change')
change.onclick = function () {
isChange = true
const x = a1.filter(v => v !== i1 && v !== i2)
confirmText.innerText = `您确认选择的号码牌是${x[0] + 1},请刮卡!`
opater.style.display = 'none'
}
noChange.onclick = function () {
isChange = true
confirmText.innerText = `您确认选择的号码牌是${i1 + 1},请刮卡!`
opater.style.display = 'none'
}
refresh.onclick = function () {
window.location.reload()
}

作者:JoyZ
链接:https://juejin.cn/post/7278684023757553727
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

什么是 HTTP 长轮询?

什么是 HTTP 长轮询? Web 应用程序最初是围绕客户端/服务器模型开发的,其中 Web 客户端始终是事务的发起者,向服务器请求数据。因此,没有任何机制可以让服务器在没有客户端先发出请求的情况下独立地向客户端发送或推送数据。 为了克服这个缺陷,Web 应用...
继续阅读 »

什么是 HTTP 长轮询?


Web 应用程序最初是围绕客户端/服务器模型开发的,其中 Web 客户端始终是事务的发起者,向服务器请求数据。因此,没有任何机制可以让服务器在没有客户端先发出请求的情况下独立地向客户端发送或推送数据。


为了克服这个缺陷,Web 应用程序开发人员可以实施一种称为 HTTP长轮询的技术,其中客户端轮询服务器以请求新信息。服务器保持请求打开,直到有新数据可用。一旦可用,服务器就会响应并发送新信息。客户端收到新信息后,立即发送另一个请求,重复上述操作。


什么是 HTTP 长轮询?


那么,什么是长轮询?HTTP 长轮询是标准轮询的一种变体,它模拟服务器有效地将消息推送到客户端(或浏览器)。


长轮询是最早开发的允许服务器将数据“推送”到客户端的技术之一,并且由于其寿命长,它在所有浏览器和 Web 技术中几乎无处不在。即使在一个专门为持久双向通信设计的协议(例如 WebSockets)的时代,长轮询的能力仍然作为一种无处不在的回退机制占有一席之地。


HTTP 长轮询如何工作?


要了解长轮询,首先要考虑使用 HTTP 的标准轮询。


“标准”HTTP 轮询


HTTP 轮询由客户端(例如 Web 浏览器)组成,不断向服务器请求更新。


一个用例是想要关注快速发展的新闻报道的用户。在用户的浏览器中,他们已经加载了网页,并希望该网页随着新闻报道的展开而更新。实现这一点的一种方法是浏览器反复询问新闻服务器“内容是否有任何更新”,然后服务器将以更新作为响应,或者如果没有更新则给出空响应。浏览器请求更新的速率决定了新闻页面更新的频率——更新之间的时间过长意味着重要的更新被延迟。更新之间的时间太短意味着会有很多“无更新”响应,从而导致资源浪费和效率低下。




上图:Web 浏览器和服务器之间的 HTTP 轮询。服务器向立即响应的服务器发出重复请求。


这种“标准”HTTP 轮询有缺点:


  • 更新请求之间没有完美的时间间隔。请求总是要么太频繁(效率低下)要么太慢(更新时间比要求的要长)。
  • 随着规模的扩大和客户端数量的增加,对服务器的请求数量也会增加。由于资源被无目的使用,这可能会变得低效和浪费。

HTTP 长轮询解决了使用 HTTP 进行轮询的缺点


  1. 请求从浏览器发送到服务器,就像以前一样
  2. 服务器不会关闭连接,而是保持连接打开,直到有数据供服务器发送
  3. 客户端等待服务器的响应。
  4. 当数据可用时,服务器将其发送给客户端
  5. 客户端立即向服务器发出另一个 HTTP 长轮询请求


上图:客户端和服务器之间的 HTTP 长轮询。请注意,请求和响应之间有很长的时间,因为服务器会等待直到有数据要发送。


这比常规轮询更有效率。


  • 浏览器将始终在可用时接收最新更新
  • 服务器不会被永远无法满足的请求所搞垮。

长轮询有多长时间?


在现实世界中,任何与服务器的客户端连接最终都会超时。服务器在响应之前保持连接打开的时间取决于几个因素:服务器协议实现、服务器体系结构、客户端标头和实现(特别是 HTTP Keep-Alive 标头)以及用于启动的任何库并保持连接。


当然,许多外部因素也会影响连接,例如,移动浏览器在 WiFi 和蜂窝连接之间切换时更有可能暂时断开连接。


通常,除非您可以控制整个架构堆栈,否则没有单一的轮询持续时间。


使用长轮询时的注意事项


在您的应用程序中使用 HTTP 长轮询构建实时交互时,需要考虑几件事情,无论是在开发方面还是在操作/扩展方面。


  • 随着使用量的增长,您将如何编排实时后端?
  • 当移动设备在WiFi和蜂窝网络之间快速切换或失去连接,IP地址发生变化时,长轮询会自动重新建立连接吗?
  • 通过长轮询,您能否管理消息队列并如何处理丢失的消息?
  • 长轮询是否提供跨多个服务器的负载平衡或故障转移支持?

在为服务器推送构建具有 HTTP 长轮询的实时应用程序时,您必须开发自己的通信管理系统。这意味着您将负责更新、维护和扩展您的后端基础设施。


服务器性能和扩展


使用您的解决方案的每个客户端将至少每 5 分钟启动一次与您的服务器的连接,并且您的服务器将需要分配资源来管理该连接,直到它准备好满足客户端的请求。一旦完成,客户端将立即重新启动连接,这意味着实际上,服务器将需要能够永久分配其资源的一部分来为该客户端提供服务。当您的解决方案超出单个服务器的能力并且引入负载平衡时,您需要考虑会话状态——如何在服务器之间共享客户端状态?您如何应对连接不同 IP 地址的移动客户端?您如何处理潜在的拒绝服务攻击?


这些扩展挑战都不是 HTTP 长轮询独有的,但协议的设计可能会加剧这些挑战——例如,您如何区分多个客户端发出多个真正的连续请求和拒绝服务攻击?


消息排序和排队


在服务器向客户端发送数据和客户端发起轮询请求之间总会有一小段时间,数据可能会丢失。


服务器在此期间要发送给客户端的任何数据都需要缓存起来,并在下一次请求时传递给客户端。




然后出现几个明显的问题:


  • 服务器应该将数据缓存或排队多长时间?
  • 应该如何处理失败的客户端连接?
  • 服务器如何知道同一个客户端正在重新连接,而不是新客户端?
  • 如果重新连接花费了很长时间,客户端如何请求落在缓存窗口之外的数据?

所有这些问题都需要 HTTP 长轮询解决方案来回答。


设备和网络支持


如前所述,由于 HTTP 长轮询已经存在了很长时间,它在浏览器、服务器和其他网络基础设施(交换机、路由器、代理、防火墙)中几乎得到了无处不在的支持。这种级别的支持意味着长轮询是一种很好的后备机制,即使对于依赖更现代协议(如 WebSockets )的解决方案也是如此。


众所周知,WebSocket 实现,尤其是早期实现,在双重 NAT 和某些 HTTP 长轮询运行良好的代理环境中挣扎。


作者:demo007x
链接:https://juejin.cn/post/7240111396869161020
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

🐞 如何成为一名合格的“中级开发”

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️ 在这个系列里面的上一篇文章中,我跟大家分享了怎么做一个专业的开发者,还有工作中要注意什么事情。 这是我们人生很重要的一步,因为只有学会怎么开始,才能慢慢变优秀,才能一步步往上进步。 如果你是第一次看这个系列,我强烈建...
继续阅读 »

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️


在这个系列里面的上一篇文章中,我跟大家分享了怎么做一个专业的开发者,还有工作中要注意什么事情。


这是我们人生很重要的一步,因为只有学会怎么开始,才能慢慢变优秀,才能一步步往上进步。


如果你是第一次看这个系列,我强烈建议你回去看看我之前写的两篇文章,说不定能对你有帮助。



  1. 🎖️怎么知道我的能力处于什么水平?我该往哪里努力?
  2. 🚗我毕业/转行了,怎么适应我的第一份开发工作?


其实我想写这篇文章已经很久了,可是一直想不出来怎么写,找了很多资料也没用。


确实憋不出来,中间还水了一篇“JavaScript冷饭”文章。天可是天天炒冷饭不好吃啊,写那些水文总会心生愧疚,感觉对不起你们哈哈。


今天,我们继续聊一聊,当我们进入这个角色一两年后,该怎么摆脱“初级”头衔,迈入“中级”阶段呢?😎



注意事项:


我接下来提及的内容可能很多大佬跟我的意见是不同的。


也有可能我的知识有限,我只涵盖了前端开发工程师的部分,对其他岗位的开发工程师不了解,可能我说的指标并不一定能和贵公司考核时所授予给的职称相对应。


我这里说的是衡量开发人员技能、知识和整体能力的一般指标


它会根据所在的领域而变化,比如前端、后端、数据等等都不太一样。


虽然具体的工具、技术甚至架构知识可能有所不同,但是我说的一般原则应该是可以广泛适用的。


如果觉得我说错了,请在评论区交流。😊



🎖️ 中级开发的显著特点:“骄傲”


当你到了中级水平,你心里一定有一个想法。那就是:


我已经学会了我现在做的事情,以及要用的所有东西了!


再说得清楚一点就是:


“我已经完全会用JavaScript了,我对HTML很熟悉了,我对数据库没问题!”


“我已经完全会用Vue了,我也会用Angular开发”


这个时候的“中级开发”,觉得他已经有了这个领域需要的能力了。



我肯定每个人到了中级阶段后肯定会有这种感觉。


可能你觉得我要说的是开玩笑,但是大部分的“中级开发”肯定都经历过这个事情。



当然啦,我想表达的“骄傲”不是贬义词。


因为这个阶段只是我们成长中必须经历的一个阶段。这真的不是一件坏事。


“骄傲”不是一件坏事


我们小时候我们都会觉得,爸爸妈妈什么都不知道,我们才更明白


类似的,当你真正进入进入“中级开发“这个角色,你大概率的就会产生这类“骄傲的情绪”。


当你拥有“骄傲”,你才开始真正走自己的路。这个时候你才真正开始独立思考。


这意味着你已经积累了足够的知识和经验,可以继续精进设计模式、最佳实践等这些学科以拔高你的知识。


简单的东西已经不能吸引你了。


🚩 中级开发应该掌握什么?


现在你是中级开发了,你需要看看自己是不是能做到下面这些事情。


这些“新”的东西可以让中级开发更有经验,也更能帮助团队。


编程能力:


  1. 很清楚不同的系统(API、模块、包等)怎么互相连接
  2. 熟练使用编程工具(IDE、GIT等)
  3. 知道怎么实现一般的需求
  4. 遇到bug的时候,知道从哪里找原因和解决办法
  5. 知道怎么优化代码和重构代码
  6. 知道怎么提高性能
  7. 知道怎么用面向对象的程序设计
  8. 知道常用的软件架构模式(MVC、MVVM、MVP、MVI等)
  9. 知道编程语言的一些特点(函数式编程)
  10. 知道怎么部署系统应用
  11. 知道怎么用数据库索引
  12. 知道怎么用数据库表迁移
  13. 知道怎么用数据库分片技术

社会能力:


  1. 可以偶尔跟产品经理(客户)沟通
  2. 是团队的主力

开始优雅:


  1. 代码模块开始按照设计模式来写
  2. 对烂代码有敏感度和重构能力

等等


📌 对中级开发的一些建议


也许现在在读文章的你已经是一位中级开发的存在了,我现在有一些建议想要分享给你!


找一个自己感兴趣的开发者社区加入


为什么我们常说“好的团队创造个人”呢


因为当你真的参与到了重要或高价值的项目时,你真的比一个人漫无目的地学习更快地获得经验。


而且当你真正在团队中贡献力量地时候,你地团队,你的组长,你的领导都会知道,把事情交给你,你就能把自己做好。


在这个过程中,你能积累经验并在你的团队中声名鹊起(这不是名气,而是知名度),那么当新的机会出现时,你就能很快地把握住。


跳出舒适区


跟我上一篇提到的给初级开发的建议类似,你一定要经常的跳出自己的舒适区,不然你不会有毅力坚持学习。


而且,特别是在互联网行业,学习能力是个硬性指标,如果无法坚持下去,很容易就会被淘汰。


这样做可以开阔你的眼界,让你的知识面更广。最终,你会逐渐掌握开发的技巧,面对这些全新的知识领域时,能更快、更准确地找到重点并掌握它们。


但是只要你坚持下去,未来的你一定会与其他人拉开差距。


找到你的导师


这一点在上一篇我也强调过了。你的开发生涯,不能只靠你自己摸索。


你需要有人给你提供想法并能够从中学习。特别是在“中级开发”阶段。


导师可以帮助你不会在某些技术问题或者人生问题上钻牛角尖,他可以拉你一把,避免你浪费很多时间。


这个人可以是你团队中的某个人。


也可以是网络上开发者社区中认识的某位博主。


找到你信任的人(或者更可能是一群人),你可以跟他们问问题和说想法!


找到可以指导你的导师,让你能够突破当前的认知。你的未来将逐步变得清晰起来。


持续学习


这个没什么好说的,在这内卷的社会中,如果没有润的资本和能力,不如在持续学习中等待破局的机会!




🎉 你觉得怎么样?这篇文章可以给你带来帮助吗?当你处于这个阶段时,你发现什么对你帮助最大?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨


作者:道长王jj
链接:https://juejin.cn/post/7243203041872412731
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS文件系统

iOS
沙盒机制 概念 iOS 沙盒机制是一种安全策略,它将每个应用程序的数据和资源隔离在一个专用目录中,限制了应用程序访问其他应用程序或系统文件的能力,从而保护了用户数据和系统安全. 目录结构 For security purposes, an iOS app’s...
继续阅读 »

沙盒机制


概念


iOS 沙盒机制是一种安全策略,它将每个应用程序的数据和资源隔离在一个专用目录中,限制了应用程序访问其他应用程序或系统文件的能力,从而保护了用户数据和系统安全.


目录结构



For security purposes, an iOS app’s interactions with the file system are limited to the directories inside the app’s sandbox directory. During installation of a new app, the installer creates a number of container directories for the app inside the sandbox directory. Each container directory has a specific role. The bundle container directory holds the app’s bundle, whereas the data container directory holds data for both the app and the user. The data container directory is further divided into a number of subdirectories that the app can use to sort and organize its data. The app may also request access to additional container directories—for example, the iCloud container—at runtime.



👆大概内容讲的是出于安全考虑,iOS应用只能在当前APP的沙盒目录下与文件系统进行交互(读取、创建、删除等).在APP安装时,系统会在当前APP的沙盒目录下创建不同类型不同功能的容器目录(Bundle、Data、iCloud).Data Container又进一步被划分为Documents、Library、temp、System Data.沙盒目录结构如下图所示:




各目录描述如下图所示:




下面介绍一些关于文件系统中常用的API.


创建NSFileManager

// 1.创建实例
NSFileManager *fileManager = [[NSFileManager alloc] init];

// 2.获取单例
NSFileManager *fileManager = [NSFileManager defaultManager];

// 3.自定义
自定义NSFileManager实现一些自定义功能.

NSFileManager有一个delegate(NSFileManagerDelegate)属性,实现该协议的对象能够对文件的拷贝、移动等操作做更多的逻辑处理,同时能在这些操作发生错误时做一些容错的处理.

// 该协议用于控制文件/文件夹,是否允许移动、删除、拷贝.以及允许这些操作发生错误时进行额外的处理.
@protocol NSFileManagerDelegate <NSObject>

// 控制是否允许该操作:
// 以下每种方法都有一个NSURL和NSString的版本,URL和Path同作用的方法只会调用一次,并且优先调用URL的方法.如果两个方法都没实现,则实现系统的默认值YES.
// 其中srcURL/srcPath分别代表原(文件/文件夹)路径URL(file://xxx)/路径(xxx). xxx为完整路径.
// 其中dstURL/dstPath分别代表目标(文件/文件夹)路径URL(file://xxx)/路径(xxx). xxx为完整路径.

// 错误处理:
// 以下每种方法都有一个NSURL和NSString的版本,URL和Path同作用的方法只会调用一次,并且优先调用URL的方法.如果两个方法都没实现,则不处理错误.

@optional

/// Moving an Item

// 在移动文件/文件夹前调用,控制是否允许移动操作
// 如果两个方法都没有实现,系统默认YES即允许移动.
- (BOOL)fileManager:(NSFileManager *)fileManager shouldMoveItemAtURL:(NSURL *)srcURL toURL:(NSURL *)dstURL;
- (BOOL)fileManager:(NSFileManager *)fileManager shouldMoveItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath;

// 移动失败时调用 处理错误信息
- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error movingItemAtURL:(NSURL *)srcURL toURL:(NSURL *)dstURL;
- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error movingItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath;

/// Copy an Item

// 在拷贝文件/文件夹前调用,控制是否允许拷贝操作
// 如果两个方法都没有实现,系统默认YES即允许拷贝.
- (BOOL)fileManager:(NSFileManager *)fileManager shouldCopyItemAtURL:(NSURL *)srcURL toURL:(NSURL *)dstURL;
- (BOOL)fileManager:(NSFileManager *)fileManager shouldCopyItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath;

// 拷贝失败时调用 处理错误信息
- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error copyingItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath;
- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error copyingItemAtURL:(NSURL *)srcURL toURL:(NSURL *)dstURL;

/// Delete an Item

// 在拷贝文件/文件夹前调用,控制是否允许删除操作
// 如果两个方法都没有实现,系统默认YES即允许删除.
- (BOOL)fileManager:(NSFileManager *)fileManager shouldRemoveItemAtPath:(NSString *)path;
- (BOOL)fileManager:(NSFileManager *)fileManager shouldRemoveItemAtURL:(NSURL *)URL;

// 删除失败时调用 处理错误信息
- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error removingItemAtPath:(NSString *)path;
- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error removingItemAtURL:(NSURL *)URL;

@end

考虑线程安全的问题,如果需要实现NSFileManagerDelegate协议,通常是新创建NSFileManager实例或是继承NSFileManager后新建实例,确保一个一个实例仅对应一个delegate.


创建文件/文件夹

/// 文件

// 创建文件,会覆盖已存在的文件内容.
// path: 文件路径.
// data: 文件内容.
// attr: 文件属性.例如:设置文件夹可读写权限、文件夹创建日期中.传入nil,则使用系统默认的配置.
// return: YES:文件存在或创建成功. NO:文件创建失败
- (BOOL)createFileAtPath:(NSString *)path
contents:(NSData *)data
attributes:(NSDictionary<NSFileAttributeKey, id> *)attr;

/// 文件夹

// url: 文件夹路径URL.
// createIntermediate: 是否自动创建目录中不存在的中间目录,如果设置为NO,仅仅会创建路径的最后一级目录,若任意中间路径不存在,该方法会返回NO.同时如果任意中间目录是文件也会失败.
// attributes: nil,则使用系统默认的配置.
// error: 错误信息.
// YES: 文件夹创建成功.YES: createIntermediates为YES且文件夹已存在. NO: 错误发生.
- (BOOL)createDirectoryAtURL:(NSURL *)url
withIntermediateDirectories:(BOOL)createIntermediates
attributes:(NSDictionary<NSFileAttributeKey, id> *)attributes
error:(NSError * _Nullable *)error;

// 同上
- (BOOL)createDirectoryAtPath:(NSString *)path
withIntermediateDirectories:(BOOL)createIntermediates
attributes:(NSDictionary<NSFileAttributeKey, id> *)attributes
error:(NSError * _Nullable *)error;


其他方式写入文件:NSData、NSString、All kinds Of Collections (写入plist).


删除文件/文件夹


删除操作是指将文件/文件夹从指定目录移除掉.

// 以file://xxx的形式删除文件/文件夹.
// srcURL: 原文件/文件夹目录URL
// dstURL: 目标目录URL.
// error: 错误信息.
// return: YES: 移动成功或URL为nil或delegate停止删除操作. NO: 错误发生.
- (BOOL)removeItemAtURL:(NSURL *)URL
error:(NSError * _Nullable *)error;
// 同上,不过传入的是xxx完整路径.
- (BOOL)removeItemAtPath:(NSString *)path
error:(NSError * _Nullable *)error;

// 将文件/文件夹移入到废纸篓,适用于Macos.iOS上使用会失败.
- (BOOL)trashItemAtURL:(NSURL *)url
resultingItemURL:(NSURL * _Nullable *)outResultingURL
error:(NSError * _Nullable *)error;

// 注意:
// 1.如果删除的是文件夹,则会删除文件夹中所有的内容.
// 2.删除文件前会调用delegate的-[fileManager:shouldRemoveItemAtURL:]或-[fileManager:shouldRemoveItemAtPath:]方法,用于控制能否删除.如果都没有实现,则默认可以删除.
// 3.删除失败时会调用delegate的-[fileManager:shouldProceedAfterError:removingItemAtURL:]或-[fileManager:shouldProceedAfterError:removingItemAtPath:]方法,用于处理错误.
// 如果2个方法都没有实现则删除失败.并且删除方法会返回相应的error信息.
// 方法返回YES会认为删除成功,返回NO则删除失败,删除方法接收error信息.

文件/文件夹是否存在


// path: 文件/文件夹路径.
// isDirectory: YES: 当前为文件夹. NO: 当前为文件.
// return: YES: 文件/文件夹存在. NO: 文件/文件夹不存在.
- (BOOL)fileExistsAtPath:(NSString *)path
isDirectory:(BOOL *)isDirectory;

// 同上,不过不能判断当前是文件还是文件夹.
- (BOOL)fileExistsAtPath:(NSString *)path;

遍历文件夹


有时候我们并不知道文件的具体路径,此时就需要遍历文件夹去拼接完整路径.

/// 浅度遍历: 返回当前目录下的文件/文件夹(包括隐藏文件).
// 默认带上了options: NSDirectoryEnumerationSkipsSubdirectoryDescendants.
// NSDirectoryEnumerationIncludesDirectoriesPostOrder无效,因为不会遍历子目录.
- (nullable NSArray<NSString *> *)contentsOfDirectoryAtPath:(NSString *)path
error:(NSError **)error

// url: 文件路径.
// keys: 预请求每个文件属性的key数组.如果不想预请求则传入@[],传nil会有默认的预请求keys.
// options: 遍历时可选掩码.
// error: 错误信息.
- (nullable NSArray<NSURL *> *)contentsOfDirectoryAtURL:(NSURL *)url
includingPropertiesForKeys:(nullable NSArray<NSURLResourceKey> *)keys
options:(NSDirectoryEnumerationOptions)mask
error:(NSError **)error

/// 深度遍历(递归遍历): 返回当前目录下的所有文件/文件夹(包括隐藏文件).
- (nullable NSDirectoryEnumerator<NSString *> *)enumeratorAtPath:(NSString *)path
- (nullable NSDirectoryEnumerator<NSURL *> *)enumeratorAtURL:(NSURL *)url
includingPropertiesForKeys:(nullable NSArray<NSURLResourceKey> *)keys
options:(NSDirectoryEnumerationOptions)mask
errorHandler:(nullable BOOL (^)(NSURL *url, NSError *error))handler
- (NSArray<NSString *> *)subpathsOfDirectoryAtPath:(NSString *)path
error:(NSError * _Nullable *)error;
- (NSArray<NSString *> *)subpathsAtPath:(NSString *)path;

获取文件夹

// iOS基本上都是使用NSUserDomainMask.

// 获取指定目录类型和mask的文件夹.类似于NSSearchPathForDirectoriesInDomains方法.
// 常用的directory:
// NSApplicationSupportDirectory -> Library/Application Support.
// NSCachesDirectory -> /Library/Caches.
// NSLibraryDirectory -> /Library.
- (NSArray<NSURL *> *)URLsForDirectory:(NSSearchPathDirectory)directory
inDomains:(NSSearchPathDomainMask)domainMask

// 获取指定directory & domainMask下的文件夹.
// domain: 不能传入NSAllDomainsMask.
// url: 仅当domain = NSUserDomainMask & directory = NSItemReplacementDirectory时有效.
// shouldCreate: 文件不存在时 是否创建.
- (NSURL *)URLForDirectory:(NSSearchPathDirectory)directory
inDomain:(NSSearchPathDomainMask)domain
appropriateForURL:(NSURL *)url
create:(BOOL)shouldCreate
error:(NSError * _Nullable *)error;
// 同上
FOUNDATION_EXPORT NSArray<NSString *> *NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory directory, NSSearchPathDomainMask domainMask, BOOL expandTilde);
// 获取/temp目录
FOUNDATION_EXPORT NSString *NSTemporaryDirectory(void);
// 获取沙盒根目录
FOUNDATION_EXPORT NSString *NSHomeDirectory(void);

设置和获取文件/文件夹属性

// 获取指定目录下文件/文件夹的属性[NSFileAttributeKey].(https://developer.apple.com/documentation/foundation/nsfileattributekey).
- (nullable NSDictionary<NSFileAttributeKey, id> *)attributesOfItemAtPath:(NSString *)path
error:(NSError **)error;

// 为指定目录文件/文件夹设置属性.
- (BOOL)setAttributes:(NSDictionary<NSFileAttributeKey, id> *)attributes ofItemAtPath:(NSString *)path error:(NSError * _Nullable *)error;

// 基于NSDictionary提供的便利方法 //
@interface NSDictionary<KeyType, ObjectType> (NSFileAttributes)

// 文件大小
- (unsigned long long)fileSize;

// 文件创建日期
- (nullable NSDate *)fileCreationDate;
// 文件修改日期
- (nullable NSDate *)fileModificationDate;

// 文件类型.NSFileAttributeType
- (nullable NSString *)fileType;

// 文件权限掩码
- (NSUInteger)filePosixPermissions; // 位掩码 可见文件 _s_ifmt.h eg:S_IRWXU
/* File mode */
/* Read, write, execute/search by owner */
#define S_IRWXU 0000700 /* [XSI] RWX mask for owner */
#define S_IRUSR 0000400 /* [XSI] R for owner */
#define S_IWUSR 0000200 /* [XSI] W for owner */
#define S_IXUSR 0000100 /* [XSI] X for owner */
/* Read, write, execute/search by group */
#define S_IRWXG 0000070 /* [XSI] RWX mask for group */
#define S_IRGRP 0000040 /* [XSI] R for group */
#define S_IWGRP 0000020 /* [XSI] W for group */
#define S_IXGRP 0000010 /* [XSI] X for group */
/* Read, write, execute/search by others */
#define S_IRWXO 0000007 /* [XSI] RWX mask for other */
#define S_IROTH 0000004 /* [XSI] R for other */
#define S_IWOTH 0000002 /* [XSI] W for other */
#define S_IXOTH 0000001 /* [XSI] X for other */

// 当前文件/文件夹所处的文件系统编号
- (NSInteger)fileSystemNumber;
这两个方法可以拼接文件的引用URL -> file:///.file/id=fileSystemNumber.fileSystemFileNumber
// 当前文件/文件夹在文件系统中的编号
- (NSUInteger)fileSystemFileNumber;

// 是否是隐藏文件
- (BOOL)fileExtensionHidden;
...

@end

移动文件/文件夹


移动操作是将文件从一个位置移动到另一个位置.

// 以file://xxx的形式移动文件/文件夹.
// srcURL: 原文件/文件夹位置URL.
// dstURL: 目标位置URL.
// error: 错误信息.
// return: YES: 移动成功或manager的delegate停止移动操作. NO: 错误发生.
- (BOOL)moveItemAtURL:(NSURL *)srcURL
toURL:(NSURL *)dstURL
error:(NSError * _Nullable *)error;
// 同上,不过传入的是xxx完整路径.
- (BOOL)moveItemAtPath:(NSString *)srcPath
toPath:(NSString *)dstPath
error:(NSError * _Nullable *)error;

// 注意:
// 1.srcURL/srcPath、dstURL/dstPath任意为nil会crash.
// 2.如果目标目录文件已存在会被覆盖.
// 3.移动文件前会调用delegate的-[fileManager:shouldMoveItemAtURL:toURL:]或-[fileManager:shouldMoveItemAtPath:toPath:]方法,用于控制能否移动.如果都没有实现,则默认可以移动.
// 4.移动失败时会调用delegate的-[fileManager:shouldMoveItemAtURL:toURL:]或-[fileManager:shouldMoveItemAtPath:toPath:]方法,用于处理错误.
// 如果2个方法都没有实现则移动失败.并且移动方法会返回相应的error信息.
// 方法返回YES会认为移动成功,返回NO则移动失败,移动方法接收error信息.

拷贝文件/文件夹


拷贝操作是将原文件从一个位置copy到另一个位置,类似于复制粘贴.

// 以file://xxx的形式拷贝文件/文件夹.
// srcURL: 原文件/文件夹/位置URL.
// dstURL: 目标位置URL.
// error: 错误信息.
// return: YES: 拷贝成功或manager的delegate停止拷贝操作. NO: 错误发生.
- (BOOL)copyItemAtURL:(NSURL *)srcURL
toURL:(NSURL *)dstURL
error:(NSError * _Nullable *)error;
// 同上,不过传入的是xxx完整路径.
- (BOOL)copyItemAtPath:(NSString *)srcPath
toPath:(NSString *)dstPath
error:(NSError * _Nullable *)error;

// 注意:
// 1.srcURL/srcPath、dstURL/dstPath任意为nil会crash.
// 2.如果目标目录已经存在则会发生错误.
// 3.拷贝文件前会调用delegate的-[fileManager:shouldCopyItemAtURL:toURL:]或-[fileManager:shouldCopyItemAtPath:toPath:]方法,用于控制能否拷贝.如果都没有实现,则默认可以拷贝.
// 4.拷贝失败时会调用delegate的-[fileManager:shouldProceedAfterError:copyingItemAtURL:toURL:]或-[fileManager:shouldProceedAfterError:copyingItemAtPath:toPath:]方法,用于处理错误.
// 如果2个方法都没有实现则拷贝失败.并且拷贝方法会返回相应的error信息.
// 方法返回YES会认为拷贝成功,返回NO则拷贝失败,拷贝方法接收error信息.

参考资料


  1. developer.apple.com/library/arc…
  2. developer.apple.com/documentati…

好物推荐


  1. OpenSim:用于快速定位模拟器中项目沙盒目录.

  2. Flex:用于真机或模拟器Debug环境下调试.


作者:小白不是小白
链接:https://juejin.cn/post/7276412839585382419
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

大厂人,大厂魂,大厂都是人上人?

大厂,一个贴在互联网人灵魂深处的标签,四舍五入,是互联网人的第二学历。 本文写给所有向往大厂,被大厂(或大厂人) PUA,逃离大厂的人。 Chapter 1: 千万人往 很多人(包括我)选择大厂的理由,无非是下面几点: 更完善的公司制度和明确的上升通道更复杂的...
继续阅读 »

大厂,一个贴在互联网人灵魂深处的标签,四舍五入,是互联网人的第二学历。


本文写给所有向往大厂,被大厂(或大厂人) PUA,逃离大厂的人。


Chapter 1: 千万人往


很多人(包括我)选择大厂的理由,无非是下面几点:


  1. 更完善的公司制度和明确的上升通道
  2. 更复杂的业务/技术问题
  3. 更成熟的基础建设
  4. 更高的工资

一言以蔽之,就是更高的成长空间。当然,也不排除为了工牌(大厂光环)入职的人,尤其是做公众号的、卖课的、运作人设的(绝没有骂我自己的意思)。


不过,大厂也带来了另一面,那就是:


  1. 高情商:「向上管理」;低情商:「学会人际和汇报」
  2. 高情商:「注重深度」;低情商:「螺丝钉」

无论是我自己的日常工作和生活中,还是面试过程中,都会发现我们渐渐被大厂黑话侵蚀,说着自以为是的人话;又或者是螺丝钉当久了,在一个舒适区里跨不出来,知识面狭隘。


当然,保个命,这些都是个人原因,并不具有普遍规律。我也知道大部分想去大厂的人还是希望「至少去过」。


对于希望至少去过的人,在这里给大家一些简单的建议:


  1. 打好基础(专业基本功)
  2. 学会沟通(表达能力)
  3. 有一项优势(一个自豪的项目或者有把握的能力)

当然,你在面试中即使被刷了,也不代表你不够好,可能只是因为「不合适」,有可能是和岗位有所偏差,有可能是和面试官相性太差——比如岗位要招一个小程序专家,但你并没有做过小程序,这很显然就不够合适;又或者面试官就喜欢问算法和八股文,而你擅长系统设计。


但是切记,不要进行简历造假,简历造假不止是学历和工作经历造假,还包括了项目经历的造假,比如这个项目其实根本不是你做的,或者你只开发了其中很小的一个模块,却说自己是整个项目的负责人。这些在项目经历问题的连环拷打下根本无处遁形,甚至还会担上诚信和被拉黑的风险——同时,也不排除有人在网上说自己造假了入职的,但你的面试官,未来会是你的同事、领导,这样水平的同事,真的没关系吗?


Chapter 2: 往昔,风光无限


降本提效导致了大量大厂人的流出,美其名曰人才输送,也导致了越来越多的人吐槽大厂出身的人——「味太重」。因为大厂除了明确的规章制度外,还有一些文化基调——只是口号定的激情澎湃,但往往执行和理解上会出现了一点偏差。


拿我自己来说,我非常讨厌「阿里味」,尽管我认识很多阿里人,有一线的也有级别更高的同学,他们为人处世都是很正常的,但我也确确实实感受过阿里政委,感受过 PUA。


除此以外,一些大厂(可能级别比较高,也可能不高)的同学喜欢把自己的「成功经验」输出给他人,无论是团队运作的经验,或者是系统架构的经验。这种输出是种双刃剑,一方面,确实给大家带来了另一种方案和视野,但是另一方面,如果迷信大厂经验,无脑照搬,可能前方就是万丈深渊。


对于大厂的同学,最忌卖弄和照搬经验,「我以前在 XX」在脉脉是被吐槽的最多的句式之一,为什么被吐槽,我相信不是说他完全不对,而是很有可能是理论并没有结合实践,每个公司或者业务都有自己的特色和基础能力,因此我一而再再而三的在所有文章前面介绍背景,在文末说「没有银弹」,都是为了告诉大家:结合自己的业务思考,而不要一股脑全抄。


其实,这也不是大厂病,即使不是大厂人,你也可能听到一些人喜欢说「我以前在 XX」或者「我当年 XX」,习惯用这样开头的人,可能也是想用一些标签来进行暗示或者明示:「我是专业的」。


但是真正的专业,是不需要通过给自己贴标签来体现的。


更何况可能还会遇到我这种专门跟「权威」对着干的叛逆分子。


所以即使往昔风光无限,也不要把大厂作为自己的标签——毕竟大家都知道,大厂并不是每个人都是非常厉害的,万一装逼翻车,可能人家就会怀疑你是被末位淘汰的了。


这里再告诉大家一个秘密:职级高并不全等于技术水平高,更多的是对你工作的认可,「认可」二字,细细斟酌。


我就比较喜欢这样的标签和介绍:我,敖天羽,打钱!


如果之后有大厂人这么跟你说,表情无限骄傲和怀念,你不妨问问:既然如此,你离职干嘛?


Chapter 3: 逃离,下一站在何方


离开大厂也有许多理由,或许自己不愿离开,但是降本提效;或许是螺丝拧久了想要出去看看外面的世界,毕竟有些项目组可能已经形成了阶级固化,在人才辈出的团队里卷又仿佛看不到头,又成了鸡头凤尾之争。


但是逃离前,请先想清楚,鸡头也有鸡头的痛苦,小厂甚至可能拿不出这么高的薪资,基础设施也不够完备,你将走出一个螺丝钉的舒适区。


至于向上管理?人际关系?最近我想明白了一点,有人的地方就有江湖,无非是你可不可以选择当个侠客,还是只能混帮派的区别。——作为一个邪派分子,很明显我是不乐于混帮派的。


当然,你的下一站,甚至不一定是写代码,也有可能是——公务员、水果摊/奶茶店/超市老板
滴滴司机、外卖小哥,也可能是自媒体、主播等等。


总结


当然,无论怎么样,希望每个人都无悔于自己的选择,也希望大家不要迷信大厂、不要因为大厂的标签当自己是权威,不要为了进大厂不择手段。


本文只是最近遇到的一些事的碎碎念,请勿代入(你代就是你说了算!)


最后,请记住我的标签:我,敖天羽,打钱!


作者:敖天羽
链接:https://juejin.cn/post/7271542820808163383
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

改了 3 个字符,10 倍的沙箱性能提升?!!

确实会慢,但不多 🤪 qiankun2 自发布以来,常被人诟病慢、有性能问题。虽在大部分场景下,这个问题表现的并不明显,不会对应用造成可感知的影响(2m JS 的解析约增加 200ms 耗时,单个函数调用增加耗时可忽略不计)。大部分情况下应用渲染慢,真的就是因...
继续阅读 »

确实会慢,但不多 🤪


qiankun2 自发布以来,常被人诟病慢、有性能问题。虽在大部分场景下,这个问题表现的并不明显,不会对应用造成可感知的影响(2m JS 的解析约增加 200ms 耗时,单个函数调用增加耗时可忽略不计)。大部分情况下应用渲染慢,真的就是因为你的 JS 太大(一个不分片的超大的 bundle),接口响应太长,UI 不够有「弹性」导致的。
但在面临一些 CPU 密集型的 UI 操作时,如图表、超量 DOM 变更(1000以上)等场景,确实存在明显的卡顿现象。所以我们也不好反驳什么,通常的解决方案就是推荐用户关闭沙箱来提升性能。


去年底我们曾尝试过一波优化,虽然略有成效,但整体优化幅度不大,因为有一些必要访问耗时省不掉,最终以失败告终。


重启优化之路 😤


近期有社区用户又提到了这个问题,加之年初的时候「获取」到了一些灵感,中秋假期在家决定对这个问题重新做一次尝试。
我们知道 qiankun 的沙箱核心思路其实是这样的:

const windowProxy = new Proxy(window, traps);

with(windowProxy) {
// 应用代码,通过 with 确保所有的全局变量的操作实际都是在操作 qiankun 提供的代理对象
${appCode}
}

此前主要的性能问题出在应用的代码会频繁的访问沙箱,比如 Symbol.unscopables 在图表场景很容易就达到千万级次的访问。
优化的思路也很简单,就是要减少全局变量在 proxy 里的 lookup 次数。比如可以先缓存起来,后续访问直接走作用域里的缓存即可:

const windowProxy = new Proxy(window, traps);

with(windowProxy) {
+ // 提前将一些全局变量通过 赋值/取值 从 proxy 里缓存下来
+ var undefined = windowProxy.undefined; var Array = windowProxy.Array; var Promise = windowProxy.Promise;
// 应用代码,通过 with 确保所有的全局变量的操作实际都是在操作 qiankun 提供的代理对象
${appCode}
}

看上去很完美,不过手上没有 windows 设备没法验证(M1性能太强测不出来),于是先提了个 pr


验证 👻


假期结束来公司,借了台 windows 设备,验证了一下。
糟了,没效果。优化前跟优化后的速度几乎没有变化。🥲


想了下觉得不应该啊,理论上来讲多少得有点作用才是,百思不得其解。


苦恼之际,突然好像想到了什么,于是做出了下面的修改:

const windowProxy = new Proxy(window, traps);

with(windowProxy) {
+ // 提前将一些全局变量通过 赋值/取值 从 proxy 里缓存下来
- var undefined = windowProxy.undefined; var Array = windowProxy.Array; var Promise = windowProxy.Promise;
+ const undefined = windowProxy.undefined; const Array = windowProxy.Array; const Promise = windowProxy.Promise;
// 应用代码,通过 with 确保所有的全局变量的操作实际都是在操作 qiankun 提供的代理对象
${appCode}
}

改动更简单,就是将 var 声明换成了 const,立马保存验证一把。


直接起飞!


场景 1:vue 技术栈下大 checkbox 列表变更





在有沙箱的情况下,耗时基本与原生框架的一致了。


场景 2:10000 个 dom 插入/变更


在 vue 的 event handler 中做原生的 10000 次 的 for 循环,然后插入/更新 10000 个 dom,记录中间的耗时:

<template>
<div>
<ul>
<li v-for="item in aaa" :key="item">{{ item }}</li>
</ul>
<button @click="test">test</button>
</div>
</template>

<script>
import logo from "@/assets/logo.png";
export default {
data() {
return {
aaa: 1
};
},
methods: {
test() {
console.time("run loop", 10000);

for (let index = 2; index < 1 * 10000; index++) {
this.aaa = index;
}

console.timeLog("run loop", 10000);

this.$nextTick(() => {
// 10000 个 dom 更新完毕后触发
console.timeEnd("run loop", 10000);
});
}
}
};
</script>
 

 

可以看到,这个优化后的提升已经不止 10 倍了,都超过 50 倍了,跟原生的表现基本无异。


如何做到的 🧙


完成最后的性能飞跃,实际上我只改了 3 个字符,就是把 with 里的 var 换成了 const,这是为什么呢?
其实我之前的这篇文章早就告诉了我答案:
ES 拾遗之 with 声明与 var 变量赋值
里面有一个重要的结论:
image.png
因为 windowProxy 里有所有的全局变量,那么我们之前使用 var 去尝试做作用域缓存的方案其实是无效的,声明的变量实际还是在全局的词法环境中的,也就避免不了作用域链的查找。而换成 const,就可以顺利的将变量写到 with 下的词法环境了。


one more thing 😂


至此,如果以后你的应用在微前端场景下表现的不尽如人意,请先考虑:


  1. 是否是应用的打包策略不合理,导致 bundle 过大 js 执行耗时过长
  2. 是否是前置依赖逻辑过多执行过慢(如接口响应),阻塞了页面渲染
  3. 是否是微应用的加载策略不合理,导致过晚的加载
  4. 没有加载过渡动画,只有硬生生的白屏

别再试图甩锅给微前端了,瑞思拜🫡。


作者:支付宝体验科技
链接:https://juejin.cn/post/7153509630155423752
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

比 React 快 30%?Gyron 是怎么做到的。

距离第一个试用版已经过去半年之久,想想就发了这篇文章,该文章起这种标题完全是被迫。之前也相继发表了一些相关的文章但是阅读量和点赞量寥寥无几,这种标题我也是看了一些自媒体发表的文章后学习而来。我也在尝试编写一些高质量的文章,如果你看到这篇文章请嘴下留情。 响应式...
继续阅读 »

距离第一个试用版已经过去半年之久,想想就发了这篇文章,该文章起这种标题完全是被迫。之前也相继发表了一些相关的文章但是阅读量和点赞量寥寥无几,这种标题我也是看了一些自媒体发表的文章后学习而来。我也在尝试编写一些高质量的文章,如果你看到这篇文章请嘴下留情。


响应式


Gyron.js是一款零依赖的响应式框架,和社区中大多数响应式方案一样,Gyron.js也是利用Proxy提供的能力进行的依赖收集和更新,理论上来说只要支持Proxy能力的环境都支持这套响应式的逻辑,只是需要修改(接入)对应的渲染部分。这里也要感谢 Vue 实现的响应式依赖更新方案,我们基本上参考着这套方案实现了自己的 API。


响应式核心部分就是 effect 结构,基本上所有提供出来的 api 都是在 effect 之上实现的,所以我们先来介绍 effect 这个数据结构。


我们先来看一眼 effect 长什么样子

export type Dep = Set<Effect>;

export type EffectScheduler = (...args: any[]) => any;

export type Noop = () => void;

export interface Effect {
// A 等于 self effect
// B 等于 other effect
// deps 是一个 Set 结构的 Effect ,用于存放 A 依赖的 B
// 当A不再使用时需要清除所有依赖 A 的 B
deps: Dep[];
// 在边界情况下强制执行依赖的B
allowEffect: boolean;
// 在首次执行时收集依赖的函数
scheduler: EffectScheduler | null;
// 存放所有手动设置的依赖
wrapper: Noop;
// 当需要更新时执行的函数
run: Noop;
// 当不再使用时清除依赖的B
stop: Noop;
}

effect 中有很多属性,我也在其中注释了每个属性的作用。每个属性都有自己的应用场景,比如 deps 就是用于卸载组件后清除所有组件依赖的数据,避免数据更新后组件被卸载,导致组件更新异常。其实响应式核心分为两个部分,第一个部分就是依赖收集,第二个部分就是响应更新,我们先来看看下面这张图。




上面这张图就是说明两个不同的数据之间的依赖关系,第一块(左上角)表明 variable proxy 2 依赖 variable proxy 1,在 variable proxy 2 中访问 variable proxy 1 时,会触发 variable proxy 1 的自动收集任务,当 variable proxy 1 的值更新后会触发依赖 variable proxy 2 的任务,也就是 run 或者 scheduler。那么我们是通过什么将这两个变量关联在一起的呢?我们引入了一个WeakMap数据effectTracks,用变量作为一个 key 值,然后在变更 variable proxy 1 时从这个模块变量中找到依赖再做更新,也就是图中右边部分。至此,依赖收集和依赖更新都已经完成,接下来,如何对应到组件上面呢?


上面我们介绍了两个响应式变量是如何完成这一整套响应式方案,那么我们把上述的变量变更为组件可不可以呢?答案是可以的。组件是什么?在 Gyron.js 中组件就是一个函数,一个被内装函数包装的函数。那么,组件在初次渲染时如何进行依赖收集呢?在讲解组件的依赖收集之前,我们先讲一讲另外一个模块变量activeEffect,这个变量主要用于组件初次渲染时保存组件的 effect 对象,然后在响应式数据 track 时,获取到组件的 effect 对象保存在上面讲的effectTracks模块变量中,在响应式数据发生变更后触发组件 effect 的 update 方法(也就是 run)来更新组件。这里值得一提的是,所有的更新我们全部都是异步,并且是可选支持中断继续模式的,这部分内容我们接下来再进行介绍。


好了,响应式的核心内容其实并不多,其实如何实现只是冰山一角,最主要的是其中的想法可以应用在你的业务之中,尽量少写一些恶心同事的代码。


任务调度


上面我们讲解了Gyron.js是如何做到响应式更新的,接下来我们说一说多个组件同时更新应该怎么处理呢?如果组件更新阻止了用户操作应该怎么办呢?如何在组件中拿到更新后的DOM呢?这些问题在平时开发中相信大多数开发中都遇到过,其实这些问题可以在编写业务代码时去进行优化,但是不怎么优雅,也可能会导致代码不可读,比如


获取更新后的 DOM

// 获取更新后的 D<DOM>
任务: [update component A, update component B, [update component C, [update component D]]]
等待任务更新完成: A、B、C、D
获取组件D更新后的DOM

为了提高开发效率和用户体验,开发者可以合理选择使用哪种模式更新组件。具体有哪些模式可以选择呢?答案是两种,第一种就是默认模式,组件更新全部在异步队列中完成。第二种模式就是在异步队列中的任务会受到外部状态控制。接下来我们分开讲一讲这两种模式。


第一种模式,我们可以使用Gyron.js暴露的FC方法定义组件,然后正常使用 JSX 去描述 UI,在组件更新时通过暴露的nextRender获取到更新后的 DOM。这是一个很常见的实现方式,这也和 Vue 的nextTick一样。我们重点讲讲另外一种更新模式。


延迟更新:组件更新的前面几步都是一样,有一个异步队列,但是延迟更新模式中有一个priority属性,当组件 effect 拥有这种属性的时候会自动根据这批组件的更新时间,或者用户操作来中断队列中后续任务的更新,当浏览器告诉我们,现在有空闲了可以继续任务时再继续未更新的任务。其实这种模式还可以更进一步,设定一个冷却时间,在冷却时间内再次发现相同的任务直接抛弃上一次相同的任务(根据任务 ID 来区分),这样做可以减少浏览器开销,因为这些任务在下一个周期中肯定会被覆盖。我们有计划的去实现这个内容,但不是现在。


第二种模式的实现完全得益于浏览器提供的 API,让这种模式实现变为可能,也让用户体验得到提升。


其实第二种模式后面的理念可以用在一些大型的编辑场景和文档协作中以此来提升用户体验,这也是在研究 React 的调度任务之后得出的结论。


所以,有人在反驳说看这些源码时没用时可以硬气的告诉他们,看完之后学到了什么。(不过不要盲目的去看,针对具体的问题去研究和借鉴)


复合直观的


如果你是一个 React 的用户,你会发现函数式组件的状态心智负担太高,不符合直觉,直接劝退新手。那么,什么是符合直观的代码?当我的组件依赖更新后组件的内容发生响应的更新即可,这也是响应式的核心。在Gyron.js中编写一个简单的响应式组件会如此简单。

import { FC, useValue } from "gyron";

interface HelloProps {
initialCount: number;
}

const Hello = FC<HelloProps>(({ initialCount = 0 }) => {
const count = useValue(initialCount);
const onClick = () => count.value++;

// Render JSX
return <div onClick={onClick}>{count.value}</div>;
});

上面定义了一个 Hello 的组件,这个组件接受一个参数 initialCount,类型为数字,组件的功能也很简单,当用户点击这个数字然后自增一。而如果要用 React 去实现或者 Vue 去实现这样一个功能,我们应该怎么做呢?


我们用 Vue 去实现一个一样的组件,代码如下(使用了 setup 语法)

<script lang="ts" setup>
import { ref } from "vue";

const props = withDefaults(
defineProps<{
initialCount: number;
}>(),
{
initialCount: 0,
}
);

const count = ref(props.initialCount);
function onClick() {
count.value++;
}
</script>

<template>
<div @click="onClick">{{ count }}</div>
</template>

那么我们用 React 也去实现一个一样的组件,代码如下

import { useState, useCallback } from "react";

export const Hello = ({ initialCount = 0 }) => {
const [count, setCount] = useState(initialCount);

const onClick = useCallback(() => {
setCount(count + 1);
}, [count, setCount]);

console.log("refresh"); // 每点击一次都会打印一次

return <div onClick={onClick}>{count}</div>;
};

好了,上面是不同框架实现的 Hello 组件。这里并不是说其它框架不好,只是我认为在表达上有一些欠缺。Vue2 中需要理解 this,并且没办法让 this 稳定下来,因为它可以在任何地方修改然后还无法被追踪,在 Vue3 中需要理解 setup 和 template 之间的关系,然后实现类型推断需要了解 defineXXX 这种 API。在 React 中想要更新组件需要注意 React 更新机制,比如内部状态何时才是预期的值,在遇到复杂的组件时这往往比较考验开发者的编码水平。


以上,Gyron.js是如何解决这些问题的呢?其实,这完全得益于 babel 的强大能力,让开发者不需要知道编译构建优化的知识也能介入其中,改变源码并能重新构建。如果想了解其中的用法可以去 babel 官网plugin 页面


然后,Gyron.js是如何解决上面提到的问题?我们以上面编写的一个简单组件 Hello 为例,介绍其中到底发生了什么。
首先,我们的组件用 FC 函数进行了一个包装,这里 FC 就好比一个标识符,在 AST 中属于 BinaryExpression 节点,然后函数体的返回值就是JSX.Element。我们有了这个规则,然后在 babel 中就可以根据这个规则定位到组件本身,再做修改。为了解决重复渲染的问题,我们需要把返回值做一些修改,把JSX.Element用一个函数进行包裹再进行返回。具体转换如下:

const Hello = FC(({ numbers }) => {
return <div>{numbers}</div>;
});
// ↓ ↓ ↓ ↓ ↓
const Hello = FC(({ numbers }) => {
return ({ numbers }) => <div>{numbers}</div>;
});


名词解释

组件函数:我们熟知的 JSX 组件

渲染函数:转换后的 JSX 函数,用于标记哪些部分是渲染部分,哪些是逻辑部分。类似于 Vue3 的 setup 和 render 的区别。



这是一个最简单的转换,但是这又引入了另外几个问题。第一,在JSX.Element中的元素内容是组件的参数,但是在下次渲染时取到的是顶层函数中的numbers,为了解决这个问题,我们将顶层函数中的第一个参数作为渲染函数中的第一个参数,然后在渲染函数中访问到的状态就是最新状态。


这其中还有一个问题,我在组件函数中访问 props 状态也无法保证是最新的,这时候就需要使用Gyron.js提供的onBeforeUpdate方法,这个方法会在组件更新之前调用,然后我们需要把组件函数中定义的 props 全部放进这个函数中,然后根据函数的 new props 去更新用户定义的 props。但是真实的使用场景比较复杂,比如可以这样定义({ a, ...b }) => {},将 props 的 a 单独拎出来,然后其余部分全部归纳到 b 中。


举一个简单的例子:

const Hello = FC(({ numbers }) => {
function transform() {
return numbers;
}
return <div>{transform()}</div>;
});
// ↓ ↓ ↓ ↓ ↓
import { onBeforeUpdate as _onBeforeUpdate } from "gyron";
const Hello = FC(({ numbers }) => {
_onBeforeUpdate((_, props) => {
var _props = props;
numbers = _props.numbers;
});
function transform() {
return numbers;
}
return <div>{transform()}</div>;
});

可以看到转换后的组件中多出了一个_onBeforeUpdate方法调用,其作用就是更新组件函数作用域中的 props。



小结:为了让用户在开发中编写符合直观的代码,Gyron.js在背后做了很多事情。这其实也是 babel 在实际项目中的一种使用方法。



极快的 hmr


hmr(hot module replacement )就是模块的热更新,其实这部分功能都是编译工具提供,我们只需要按照他们提供的 API 然后更新我们的组件。

if (module.hot) {
module.hot.accept("./hello.jsx", function (Comp1) {
rerender("HashId", Comp1);
});
}

以上代码我们的插件会自动插入,无需手动引入。(目前还只接入 vite,后续有计划的支持 webpack 等有 hot 模块的工具)


我们大致了解一下这其中发生了什么?首先,我们还是借助 babel 把每一个组件的名字和内容生成的 hash 值作为注释节点存放在模块中,然后通过编译工具获取到所有本模块的组件,然后通过注册 hot 事件重新渲染更新后的组件。


好了,讲解了编译工具提供的功能,这里着重讲解一下Gyron.js是如何做到重新渲染的。首先,我们通过编译工具获取到了组件 Hash 和组件函数,然后通过rerender函数执行重新渲染。那么rerender所需要的数据又是从哪里来的呢?其实,在实例第一次初始化的时候这个数据全部都收集到一个Map<string, Set<Component>>数据结构中,然后再通过Component上的 update 方法执行组件的更新。


SEO 友好


其实这段内容和Gyron.js本身关系不太大,但是没有Gyron.js提供的能力也很难办到。Gyron.js提供了 SSR(Server Side Render)的渲染模式,也就是我们熟知的服务端渲染。其中大致的原理就是服务端将实例渲染成字符串之后返回给浏览器,然后再通过客户端的hydrate功能让“静态”文本变的可响应。


以上是简单的用法,然后大致流程图如下所示:




为了让组件变得更通用,我们在所有组件的 props 上注入了一个变量告诉开发者当前处于何种模式的渲染当中,在服务端渲染当中时不能使用客户端提供的 API,在客户端渲染的过程中不能使用服务端的 API。

const App = ({ isSSR }) => {
// ...
if (!isSSR) {
document.title = "欢迎";
}
};
import { strict as assert } from "node:assert";
const App = ({ isSSR }) => {
// ...
if (isSSR) {
assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, "3"]], 4, 5]);
}
};

这是服务端渲染的方式,还有一种介于服务端渲染和客户端渲染之间,就是完全输出静态资源然后就可以部署到任何机器或者在线平台服务商中,比如app.netlify.comgithub.com等。这里不再介绍 SSR 模式的使用方法,可以去gyron.cc/docs/ssr这里有更详细的介绍。


所见即所得



这里介绍的是官方文档中的在线编辑器,相比于接入其它平台,我们占用的资源更少,功能齐全。



经过一段时间的折腾,终于弄出一个简单版的在线编辑器,支持实时预览、语法纠错、语法高亮、智能跳转等功能。


语言的话目前支持 jsx、tsx、less,并且还支持加载在线资源,比如import { h } from 'https://cdn.jsdelivr.net/npm/gyron'。因为所有数据都不保存在远端,只保存在本地,所以没有使用 standalone 沙盒技术隔离运行环境,也没有防范 xss 攻击。在线编辑器的目标就是让用户可以在线使用,支持用户编辑源代码,支持本地模块导入,支持实时预览,支持多个编辑器运行互不干扰。


目前这个编辑器支持本地编译和服务端编译,本地编译会让首屏加载变慢所以在线编辑器使用的服务端编译。现在,可以访问gyron.cc/explorer这个地址在线体验。


如果使用本地编译,这里面最终的就是需要实现一套本地的虚拟文件系统,让打包工具能够正常访问到对应的本地资源。而在 esbuild 中实现一套虚拟文件系统其实很简单,只需要编写一个插件,然后用 resolve 和 load 两种勾子就可以将本地文件输出到 esbuild 中。

const buildModuleRuntime = {
name: "buildModuleRuntime",
setup(build) {
build.onResolve({ filter: /\.\// }, (args) => {
return {
path: args.path,
namespace: "localModule",
};
});
build.onLoad({ filter: /\.\//, namespace: "localModule" }, async (args) => {
// 具体实现可以去github https://github.com/gyronorg/core/blob/main/packages/babel-plugin-jsx/src/browser.ts
const source = findSourceCode(config.sources, args.path);

if (source) {
const filename = getFileName(args, source.loader);
const result = await transformWithBabel(
source.code,
filename,
main,
true
);
return {
contents: result.code,
};
}
return {
contents: "",
loader: "text",
warnings: [
{
pluginName: "buildModuleRuntime",
text: `Module "${args.path}" is not defined in the local editor`,
},
],
};
});
},
};

然后会输出一个 module 文件,最终只需要将文件塞到 script 中让其运行。


在页面中引用多个编辑器,需要注意的是在不用这个 module 文件后及时删除。可以使用命名空间给 module 加上一个标签,新增和删除都使用这个命名空间作为变量控制当前运行时的资源。


作者:gyron
链接:https://juejin.cn/post/7212550699321917501
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一次嫖娼的结果,被抓走什么流程

最近咨询卖淫嫖娼的又多了起来这个怎么说呢屡禁不止吧,今天跟一次性跟大家讲讲嫖娼的规定处罚和处理办法吧,点赞收藏好,以后就不在讲了,可以告诉你身边的人。第1个、嫖娼将会受到15日以内的拘留第2个、这件事将会受到5000元以下的罚款第3个、是这件事将通知你的配偶,...
继续阅读 »


最近咨询卖淫嫖娼的又多了起来这个怎么说呢屡禁不止吧,今天跟一次性跟大家讲讲嫖娼的规定处罚和处理办法吧,点赞收藏好,以后就不在讲了,可以告诉你身边的人。

第1个、嫖娼将会受到15日以内的拘留
第2个、这件事将会受到5000元以下的罚款
第3个、是这件事将通知你的配偶,即使你没有配偶,这件事也会告诉你的父母或者是你的兄弟姐妹。
第4个、即使你单身,嫖娼这件事也是违法的
第 5个、外地嫖娼处罚会寄回老家
第6个、你如果遇到了14周岁以下的女孩,那么就构成了强奸罪。

那如果案件发生了,公安又会如何处理呢? 老杨跟大家讲一下公安机关对于卖淫嫖娼的办案流程。
(一)传唤至公安机关,公安机关发现涉嫌卖淫、嫖娼的人员,可以当场口头传唤或者使用《传唤证》传唤至公安机关进行讯问。
(二)讯问、查证公安民警对经传唤至公安机关的卖淫 嫌疑人,应及时讯问、查证,但讯问查证的时限不得超过24小时.经讯问、查证,有证据证明一方是以营利为目的,自愿和他人发生性关系,以及另一方是以给付金钱等物质利益为手段,与卖淫者发生性关系的,可以认定为卖淫、嫖娼行为。在讯问、查证时.注意应将双方分别讯问、查证

(三)强制进行性病检查认定为卖淫嫖娼的,公安机关必须强制卖淫嫖娼人员检查性病,以调查和区别卖淫嫖娼人员是否涉嫌“传播性病罪”。强制检查性病的方法可以是:由公安民警将卖淫嫖人员带到性病监测门诊(或者是有皮肤性病科的公立医院)。
所以,有这方面爱好的同学们要注意了,别有侥幸心理,可能下一次被带走的就是你了关注老杨,每天学习实用的法律知识 #法律咨询 #小红书法律知识课堂 #法律求助 #嫖娼 #嫖娼违法 #法律常识

来源:老杨说刑事,小红书2200171265

收起阅读 »

停止编写 API 函数

如果你正在开发一个前端程序,后端使用的 RESTFUL API,那你必须停止为每一个接口编写函数。 RESTFUL API 通常提供在不同实体上执行增删改查(CRUD)操作的一组接口。我们通常在我们的前端项目中为这些每一个接口提供一个函数,这些函数的功能非常的...
继续阅读 »

如果你正在开发一个前端程序,后端使用的 RESTFUL API,那你必须停止为每一个接口编写函数。


RESTFUL API 通常提供在不同实体上执行增删改查(CRUD)操作的一组接口。我们通常在我们的前端项目中为这些每一个接口提供一个函数,这些函数的功能非常的相似,只是为了服务于不用的实体。举个例子,假设我们有这些函数。

// api/users.js

// 创建
export function createUser(userFormValues) {
return fetch('users', { method: 'POST', body: userFormValues });
}

// 查询
export function getListOfUsers(keyword) {
return fetch(`/users?keyword=${keyword}`);
}

export function getUser(id) {
return fetch(`/users/${id}`);
}

// 更新
export updateUser(id, userFormValues) {
return fetch(`/users/${is}`, { method: 'PUT', body: userFormValues });
}

// 删除
export function removeUser(id) {
return fetch(`/users/${id}`, { method: 'DELETE' });
}

类似的功能可能存在于其他实体,例如:城市、产品、类别...但是我们可以用一个简单的函数调用来代替这些函数:

// apis/users.js
export const users = crudBuilder('/users');

// apis/cities.js
export const cities = crudBuilder('/regions/cities');


然后像这样去使用:

users.create(values);
users.show(1);
users.list('john');
users.update(values);
users.remove(1);

你可能会问为什么?有一些很好的理由:


  • 减少了代码行数:你编写的代码,和当你离开公司时其他人维护的代码
  • 强制执行 API 函数的命名约定,这可以增加代码的可读性和可维护性。例如你已经见过的函数名称: getListOfUsersgetCitiesgetAllProductsproductIndexfetchCategories等, 他们都在做相同的事情,那就是“获取实体列表”。使用这种方法,你将始终拥有entityName.list()函数,并且团队中的每个人都知道这一点。

所以,让我们创建crudBuilder()函数,然后再添加一些糖。


一个非常简单的 CRUD 构造器


对于上边的简单示例,crudBuilder()函数将非常简单:

export function crudBuilder(baseRoute) {
function list(keyword) {
return fetch(`${baseRoute}?keyword=${keyword}`);
}
function show(id) {
return fetch(`${baseRoute}/${id}`);
}
function create(formValues) {
return fetch(baseRoute, { method: 'POST', body: formValues });
}
function update(id, formValues) {
return fetch(`${baseRoute}/${id}`, { method: 'PUT', body: formValues });
}
function remove(id) {
return fetch(`${baseRoute}/${id}`, { method: 'DELETE' });
}

return {
list,
show,
create,
update,
remove
};
}

假设约定 API 路径并且给相应实体提供一个路径前缀,他将返回该实体上调用 CRUD 操作所需的所有方法。


但老实说,我们知道现实世界的应用程序并不会那么简单。在将这种方法应用于我们的项目时,有很多事情需要考虑:


  • 过滤:列表 API 通常会提供许多过滤器参数
  • 分页:列表 API 总是分页的
  • 转换:API 返回的值在实际使用之前可能需要进行一些转换
  • 准备:formValues对象在发送给 API 之前需要做一些准备工作
  • 自定义接口:更新特定项的接口不总是${baseRoute}/${id}

因此,我们需要可以处理更多复杂场景的 CRUD 构造器。


高级 CRUD 构造器


让我们通过上述方法来构建一些日常中我们真正使用的东西。


过滤


首先,我们应该在 list输出函数中处理更加复杂的过滤。每个实体列表可能有不同的过滤器并且用户可能应用了其中的一些过滤器。因此,我们不能对应用过滤器的形状和值有任何假设,但是我们可以假设任何列表过滤都可以产生一个对象,该对象为不同的过滤器名称指定了一些值。例如,我们可以过滤一些用户:

const filters = {
keyword: 'john',
createdAt: new Date('2020-02-10')
};

另一方面,我们不知道这些过滤器应该如何传递给 API,但是我们可以假设(跟 API 提供方进行约定)每一个过滤器在列表 API 中都有一个相应的参数,可以以'key=value'URL 查询参数的形式被传递。


因此我们需要知道如何将应用的过滤器转换成相对应的 API 参数来创建我们的 list 函数。这可以通过将 transformFilters 参数传递给 crudBuilder() 来完成。举一个用户的例子:

function transformUserFilters(filters) {
const params = [];
if (filters.keyword) {
params.push(`keyword=${filters.keyword}`);
}
if (filters.createdAt) {
params.push(`create_at=${dateUtility.format(filters.createdAt)}`);
}

return params;
}

现在我们可以使用这个参数来创建 list 函数了。

export function crudBuilder(baseRoute, transformFilters) {
function list(filters) {
let params = transformFilters(filters)?.join('&');
if (params) {
params += '?';
}

return fetch(`${baseRoute}${params}`);
}
}

转换和分页


从 API 接收的数据可能需要进行一些转换才能在我们的应用程序中使用。例如,我们可能需要将 snake_case 转换成驼峰命名或将一些日期字符串转换成用户时区。


此外,我们还需要处理分页。


我们假设来自 API 的分页数据都按照如下格式(与 API 提供者约定):

{
data: [], // 实体对象列表
pagination: {...} // 分页信息
}

因此,我们需要知道如何转换单个实体对象。然后我们可以遍历列表对象来转换他们。为此,我们需要一个 transformEntity 函数作为 crudBuilder 的参数。

export function crudBuilder(baseRoute, transformFilters, transformEntity, ) {
function list(filters) {
const params = transformFilters(filters)?.join('&');
return fetch(`${baseRoute}?${params}`)
.then((res) => res.json())
.then((res) => ({
data: res.data.map((entity) => transformEntity(entity)),
pagination: res.pagination
}));
}
}

list() 函数我们就完成了。


准备


对于 createupdate 函数,我们需要将 formValues 转换成 API 需要的格式。例如,假设我们在表单中有一个 City 的城市选择对象。但是 create API 只需要 city_id。因此,我们需要一个执行以下操作的函数:

const prepareValue = formValue => ({city_id: formValues.city.id});

这个函数会根据用例返回普通对象或者 FormData,并且可以将数据传递给 API:

export function crudBuilder(baseRoute, transformFilters, transformEntity, prepareFormValues) {
function create(formValues) {
return fetch(baseRoute, {
method: 'POST',
body: prepareFormValues(formValues)
});
}
}

自定义接口


在一些少数情况下,对实体执行某些操作的 API 接口不遵循相同的约定。例如,我们不能使用 /users/${id} 来编辑用户,而是使用 /edit-user/${id}。对于这些情况,我们应该指定一个自定义路径。


在这里我们允许覆盖 crud builder 中使用的任何路径。注意,展示、更新、移除操作的路径可能取决于具体实体对象的信息,因此我们必须使用函数并传递实体对象来获取路径。


我们需要在对象中获取这些自定义路径,如果没有指定,就退回到默认路径。像这样:

const paths = {
list: 'list-of-users',
show: (userId) => `users/with/id/${userId}`,
create: 'users/new',
update: (user) => `users/update/${user.id}`,
remove: (user) => `delete-user/${user.id}`
};

最终的 BRUD 构造器


这是创建 CRUD 函数的最终代码。

export function crudBuilder(baseRoute, transformFilters, transformEntity, prepareFormValues, paths) {
function list (filters) {
const path = paths.list || baseRoute;
let params = transformFilters(filters)?.join('&');
if (params) {
params += '?';
}

return fetch(`${path}${params}`)
.then((res) => res.json())
.then(() => ({
data: res.data.map(entity => transformEntity(entity)),
pagination: res.pagination
}));
}
function show(id) {
const path = paths.show?.(id) || `${baseRoute}/${id}`;

return fetch(path)
.then((res) => res.json())
.then((res => transformEntity(res)));
}
function create(formValues) {
const path = paths.create || baseRoute;

return fetch(path, { method: 'POST', body: prepareFormValues(formValues) });
}
function update(id, formValues) {
const path = paths.update?.(id) || `${baseRoute}/${id}`;

return fetch(path, { method: 'PUT', body: formValues });
}
function remove(id) {
const path = paths.remove?.(id) || `${baseRoute}/${id}`;

return fetch(path, { method: 'DELETE' });
}
return {
list,
show,
create,
update,
remove
}
}


Saeed Mosavat: Stop writing API functions


作者:七七喝可乐
链接:https://juejin.cn/post/7147993043923107870
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »