注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

需要具备哪些技能才算中高级前端?

之前有人问过我,“到底什么样才算中高级前端,需要具备哪些技能才算中高级?”他的本意是让我推荐一下前端的学习路线,然后再问了我这个问题,估计是想看看有哪些技术是晋升中高级前端的关键,提前学习吧。 这里不管是前端、终端还是后台,我觉得是可以统一来讨论的。 有什么标...
继续阅读 »

之前有人问过我,“到底什么样才算中高级前端,需要具备哪些技能才算中高级?”他的本意是让我推荐一下前端的学习路线,然后再问了我这个问题,估计是想看看有哪些技术是晋升中高级前端的关键,提前学习吧。


这里不管是前端、终端还是后台,我觉得是可以统一来讨论的。


有什么标志性的技能或者技术是可以作为中级工程师和高级工程师的分水岭的吗?只要学会了这些技术和技能,就一定可以晋升中高级工程师?我想是没有的。


我分享一下我对初中高级工程师的理解,仅供参考。


初级工程师就是应届毕业生,标志是能够熟练支撑中小型业务需求开发。他可能会支撑所有业务模块的开发,或者非核心业务模块的开发,同时也会支撑基础技术项目的开发。所以,如果使用是否参与基础技术项目来作为判断的话,是不对的。


中级工程师的标志是能够独立负责一个核心模块。成为一个模块负责人,这个模块的所有事情,领导都可以放心交给你的时候,你就是中级工程师了。这个负责模块,不是指能够支撑涉及这个模块相关的需求。而是指,你要:


  • 了解它的全部代码、它的设计原理
  • 了解它在整个系统中的位置、它跟其他模块的关联关系
  • 了解它的各种特性、现状、问题、未来的优化、发展方向
  • 维护好它的文档
  • 可以很好地给其他人、你的领导描述清楚,这个模块的所有内容
  • 负责它的一切

高级工程师的标志是能够负责一个系统成为系统负责人,带领项目成员一起,承担这个系统的所有事情。对比中级工程师,负责的内容更大更加复杂了,但本质没变,就是要综合能力。同时,中级工程师还只是单人作战,如果想要成为高级工程师,一定需要了解团队的力量,并学习如何通过合理的项目管理手段,做好一个复杂系统。


这里中级和高级都提到了“负责”这个词,那具体怎样才算负责,是领导指派给你,让你负责一个核心模块,就算负责了吗?不是的。这里的“负责”是指能够完全胜任,做出让领导满意的成果,让领导非常放心


当然,每家公司对不同职级的能力要求是不一样的,你也可以完全按照上面的能力描述来进行有针对性的学习和成长。


以上就是我对于中高级前端开发的理解,希望能够给你带来一些启发。



【讨论问题】


你是如何理解中高级工程师的呢?


欢迎在评论区分享你的想法,一起讨论。



----------------【END】----------------



【往期文章】


给你介绍一个工具,帮你找到未来的努力方向


《程序员职场工具库》高效工作的神器 —— checklist


2023 年上半年最值得看的一篇文章



欢迎加我v【longyiyiyu】,进行无负担沟通,我会


  • 长期职业发展规划指导
  • 近期工作重点交流
  • 职场解惑
  • 面试辅导

也欢迎关注公众号【潜龙在渊灬】,收获程序员职场相关经验、提升工作效率和职场效能、结交更多人脉。


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

我又听到有人说:主要原因是人不行

在工作中,我们经常把很多问题的原因都归结为三个字:人不行。 曾经有UI同事指着我的鼻子说,你们没有把设计稿百分百还原,是因为你们人不行。 昨天,我又听到一个研发经理朋友说,唉呀,项目干不好,主要是人不行。 哦,我听到这里,有种似曾相识的感觉,于是我详细问了一...
继续阅读 »

在工作中,我们经常把很多问题的原因都归结为三个字:人不行。



曾经有UI同事指着我的鼻子说,你们没有把设计稿百分百还原,是因为你们人不行


昨天,我又听到一个研发经理朋友说,唉呀,项目干不好,主要是人不行


哦,我听到这里,有种似曾相识的感觉,于是我详细问了一下,你的人哪个地方不行。



朋友说,项目上线那天晚上,他们居然不主动留下来值班,下班就走了,自觉意识太差。代码写的很乱,不自测就发到生产环境,一点行业规范都没有。他们……还……反正就是,能不干就不干,能偷懒就偷懒,人不行!



这个朋友,代码写的很好,人品也很好,刚刚当上管理岗,我也没有劝他,因为我知道,劝他没用,反而会激怒他。


当一个人,代码写得好,人品好,他就会以为别人也和他一样。他的管理方式就会是:大家一定要像我这样自觉,不自觉我就生闷气了!


反而,当一个人代码写得差,自觉性不那么强,如果凑巧还有点自知之明,那么因为他很清楚自己是如何糊弄的,因此他才会考虑如何通过管理的方法去促成目标。


我的这些认知,满是血泪史。因为我就经历过了“好人”变“差人”的过程。


因为代码写得好,几乎在每一个公司,干上一段时间,领导都会让我做管理,这在IT行业,叫:码而优则仕


做管理以后,我就发现,并不是所有人都像我一样,也并不是各个部门都各司其职,所谓课程上学的项目流程,只存在于理想状态下。当然,其中原因非常复杂,并不一定就是人不行,也可能是流程制度有问题。比如我上面的朋友,他就没有安排上线必须留人,留什么人,留到几点,什么时候开始,什么标准算是上线完成,完成之后有什么小奖励,这些他都没有强调和干预。


但是,我们无法活在理想中。不能说产品经理的原型逻辑性差,UI的设计稿歪七扭八,我们就建议老板把公司解散吧,你这个公司不适合做软件产品,那样我们就失业了。


你只能是就目前遇到的问题,结合目前手头的仅有的仨瓜俩枣,想办法去解决。可能有些方案不符合常规的思路,但都是解决实际问题特意设置的。


比如我在项目实践中,经常遇到的一点:



产品经理没有把原型梳理明白,就拿出来给开发人员看,导致浪费大家的时间,同时也打击大家的积极性:这样就开始了,这项目能好的了吗?我们也做不完就交给测试!



这种情况,一般我都会提前和产品经理沟通,我先预审,我这关过了,再交给开发看,起码保证不会离大谱。这里面有一个点,产品没有干好自己的活,人不行?他也只有3天时间设计原型。


还有一个问题也经常出现:



即便是产品原型还算可以,评审也过了。让开发人员看原型,他们没有看的。一直到开发了,自己的模块发现了问题,然后开始吐槽产品经理设计的太烂,流程走不通。



这是开发人不行?他们不仔细看,光想着糊弄。其实是他们没有看的重点,你让我看啥,我就是一个小前端,让我看整个平台吗?让我看整个技术架构?Java该用什么技术栈?看前端,你告诉我前端我做哪一模块的功能?此时,我一般都是先分配任务,然后再进行原型评审。如果先把任务分下去,他知道要做这一块,因为涉及自己的利益,会考虑自己好不好实现,就会认真审视原型,多发现问题。这样会避免做的过程中,再返过头来,说产品经理没设计好。已经进入开发了,再回头说产品问题,其实是开发人员不负责,更确切说是开发领导的责任。


一旦听到“人不行”的时候,我就会想到一位老领导。


他在我心中的是神一般的存在,在我看来,他有着化腐朽为神奇的力量。


有一次,我们给市场人员做了一个开通业务的APP:上面是表单输入,下面是俩按钮,左边是立即开通,右边是暂时保存。后来,市场同事经常找我们:能不能把我已开通的业务,改为暂时保存,我点错了。这点小事还闹到公司大会上讨论,众人把原因归为市场推广的同事人不行:没有上过学?不认识字?开不开通自己分不清吗?


此事持续了很久,闹得不愉快。甚至市场部和研发部出现了对立的局面,市场部说研发部不支持销售,研发部说市场部销售不利乱甩锅。


我老领导知道后,他就去了解,不可能啊,成年人了,按钮老按错,肯定有问题。原来,客户即便是有合作意向,也很少有立即开通的,他们都会调查一下这个公司的背景,然后再联系市场人员开通。两个按钮虽然是左右平分,但是距离很近。于是,他把软件改了,立即开通按钮挪到上边,填完信息后,顺势点击暂时保存,想开通得滑到上面才能点击。此后,出错的人就少了。




后来,行政部又有人抱怨员工人不行。发给员工的表格填的乱七八糟,根本不认真。有一项叫:请确认是否没有错误_____。明明没有错误,但是很多人都填了“否”。尽管反复强调,一天说三遍,依然有人填错,没有基本的职场素质。


老领导,他又去了解。他把表格改了,“是否没有错误”改为“全对”,空格改为打钩。后来,填错的现象明显少了。




很多事情,我们都想以说教来控制形势。比如反复强调,多次要求,我嗓子都喊哑了。因为不管是区分按钮,还是填写表格,你不是个傻子,你的能力是可以做到的,不应该出错,出了错你就是人不行。而老领导总是以人性来控制,知道你是懒散的,肯定不愿意认真付出,因此设置一个流水线,让你随着预设的轨迹被迫走一圈。下线后,居然发现自己合格了,甚至自己都变成人才了。用他的话说就是:流程弥补能力不足。



当归因为人不行时,其实分两种情况:别人不行自己不行


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

离职交接,心态要好

话说今年经历了几次项目交接?主动和被动的都算! 01 实在是没想到,都到年底快收尾的时候,还要突然接手离职人员的项目; 不断拉扯和管理内心情绪,避免原地裂开; 年度中再次经历突发的交接事宜,并且团队要在极短的时间内完成所有事项的交接流程; 毫无征兆的变动必然...
继续阅读 »

话说今年经历了几次项目交接?主动和被动的都算!




01



实在是没想到,都到年底快收尾的时候,还要突然接手离职人员的项目;


不断拉扯和管理内心情绪,避免原地裂开;


年度中再次经历突发的交接事宜,并且团队要在极短的时间内完成所有事项的交接流程;


毫无征兆的变动必然会引起一系列问题,最直接的就是影响团队现有节奏进度,需要重新调整和规划;


人员的小规模变动,对部门甚至公司产生的影响是显而易见的,道理都懂;


但是从理性上思考,这个问题并非是无解的,是可以在各个团队中,进行内部消化的;


而人力减少带来的成本降低,以及确保公司的可持续,这是极具确定性的,也是核心目的;


所以感性上说,这个梦幻的职场,可能真的是"爱了";



02



如果是常规情况下的离职流程,交接并不是一件复杂的事情,因为有时间有心情来处理这事,好聚好散;


然而最骚的是,奇袭一般的裁员手段,几分钟谈话结束直接走人;


丝毫不顾及由此带来的影响,认定留下的人应该兜底相应的责任,实现无缝接坑;


当然并不是什么公司都有底气这么做的,大部分还是在裁员通知后,留有一定的时间处理交接事项;


对于交的过程是否有质量,完全看接的一方是否聪明;


从感性上分析,都已经被裁了自然要牢牢把握摸鱼的机会,根本不会在意交出的事项谁来维护,不反越防线就不错了;


而压力会直接传送后闪现到接的人正上方;



03



面对被动离职的交接,确实很难妥善处理,情绪化容易导致事情变质,能真正理性对待的并不多;


交接涉及到三方的核心利益:公司、交出人、接手人,不同角度对待这件事件,态度完全不同;


公司,并不关心交接的质量,只要项目有人兜底即可;


交出方,感性上说直接敷衍交接单上的流程即可,并不在意后续的影响;


接手方,项目交接完成后的第一责任人,可能会关心项目的质量状况;


至于说接手的人能否有时间,有能力,有心情接下这种天降大任,可能除了自己以外,不到出问题的时候关注的很少;


因为项目交接过程没有处理好,从而导致后续的事故与甩锅,情绪化的现象并不少见;


如果是在内部矛盾突出的团队中,由此引发的离职效应也并不少见;



04



人的情绪真的是很奇怪,能让复杂的事情变的简单,也能让简单的事情变的离谱;


情绪上头的时候,事情本身是否真的复杂就已经不太重要了;


接手方最大的问题在于吃力不讨好,如果接了一个质量奇差的项目,意味之后很长一段时间内,工作状态都会陷入混乱的节奏中;


对于大部分研发团队来说,都是存在排期规划的,如果被交接的项目横插一脚,重新调规划影响面又偏大;


向上反馈,多半是回答一句:自行消化;


何谓自行消化,就是占用空闲时间处理,比如下班后,比如周末,比如摸鱼,这些都是对工作情绪的持续伤害;


最终兜底的个人或者团队,可能需要带着夜宵去公司搬砖;



05



吐槽归吐槽,裂开归裂开,成熟的搬砖人不该表现出明显的情绪化;


先捋一捋在面对离职交接时的注意事项,虽然说离职后有一个过渡期,但是真正涉及交接的时间通常一周左右;


作为接手一方,自然期待的是各种文档齐全,对于坑坑洼洼的描述足够清楚;


然而对于被离职的交出方,会带着若隐若现的情绪化状态,很难用心处理交接事项,能不挖坑就已经是良心队友了;


接手方作为后续的兜底人员,兜不住就是一地鸡毛;


如果兜住了呢?那是职责所在、理所应当、不要多想、安心搬砖;



06



面对项目交接,这种隔三差五个月就会突发的事,完全可以用一套固定的模式和节奏去执行;


强烈建议:不排斥、不积极、不情绪化;


但是在处理的过程中要理性且严谨,这样可以规避掉许多可能出现的麻烦,毕竟签了交接单,从此该项目问题根本甩不开;


职场几年,在多次"交"与"接"的角色转换过程中,总结以下几点是研发需要注意的;


P1:文档,信息的核心载体;


不管项目涉及多少文档,照单全收;


如果文档严重缺失甚至没有,直接在交接单上写明情况,并且得加粗划重点展示;


文档和项目的维护极有可能是线性不相关,但是手有文档心里不慌,因为方便后续再把项目交接给其他人;


所以,敷衍一时爽,出事火葬场;



07



P2:代码工程,坑与不坑全看此间;


接到手里的项目,是否会导致情绪崩塌,全看项目代码工程的质量,遇上一堆烂摊子,心情会持续的跌跌跌,然后裂开;


直接把人打包送走的情况也并不少见;


如果代码工程质量极高,架构设计稳定,组件集成比较常规,分包井然有序,悬着的情绪可以适当下落;


P3:库表设计,就怕没注释;


对于数据库层面的设计,与代码工程和业务文档三者相辅相成,把握其中的主线逻辑即可;


但前提是表的设计得有清晰的注释,如果是纯中式英文混搭拼音,且缺乏注释,必然会成为解决问题的最佳卡点;


P4:核心接口,应当关注细节;


从项目的核心业务中选出2-3个复杂的接口读一读;需要将注意点放在细节逻辑上,给内心积蓄一丢丢解决问题的底气;


熟悉接口的基本思路:请求从客户端发出,业务服务的处理逻辑,对数据层面的影响,最终响应的主体;



08



P5:遗留问题,考验职场关系的时候到了;


公司一片祥和的时候,员工之间还可以做做样子;


但是已经走到了一别两宽的地步,从感性上来说只要不藏着掖着就行,还想窥探别人安稳摸鱼的秘密,确实想的不错;


老练的开发常干的事,为了解决某个问题临时上线一段代码,处理好后关闭触发的入口,但是会保留代码主体;


这还算常规操作,最骚的是在本地写一段脚本工具解决线上的问题;


这些隐藏的接口和脚本只有开发的人自己清楚,如果不给个说明文档,这不单是挖坑,还顺手倒了一定比例的水进行混合;


P6:结尾事项,寒暄几句还是要的;


安全意识好的公司,会对员工的账号权限做好备份,以便离职时快速处理,不会留下风险隐患;


在所有权限关闭之后,接手人就可以在交接单上完成签字仪式;


交接完成后还是得适当的寒暄几句,万一接了个坑,转头就得再联系也不稀奇,所以职场留一线方便语音再连线;



09



年度收到的离职交接,已经累计好几份,对这种事情彻底麻了;


事来了先兜着,等兜不住的时候自然会有解决办法;


抗拒与烦躁都不会影响流程的持续推进,这种心态需要自己用清醒的意识不断的说服自己;


最后想探讨一个话题,跟项目前负责人联系,用什么话术请教问题,才能显得不卑不亢?



END


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

Swift - 闭包

iOS
定义 闭包是一个自包含的函数代码块,可以在代码中被传递和引用。闭包可以捕获和存储其所在上下文中任意常量和变量的引用**。 闭包的语法有三种形式:全局函数、嵌套函数和闭包表达式。 全局函数是一个有名字但不会捕获任何值的闭包潜逃函数是一个有名字并可以捕获其封闭函数...
继续阅读 »

定义


  • 闭包是一个自包含的函数代码块,可以在代码中被传递和引用
  • 闭包可以捕获和存储其所在上下文中任意常量和变量的引用**。

闭包的语法有三种形式:全局函数、嵌套函数和闭包表达式。


  • 全局函数是一个有名字但不会捕获任何值的闭包
  • 潜逃函数是一个有名字并可以捕获其封闭函数域内值的闭包
  • 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包

闭包表达式


闭包表达式的一般形式

{ (parameters) -> return type in

statements

}

以数组的sorted(by:)方法为例

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})


写成一行

names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2})

根据上下文推断类型


  • sorted(by:)方法被一个字符串数组调用,Swift 可以推断其参数和返回值的类型,因此其参数必须是 (String, String) -> Bool
  • 这意味着(String, String) 和 Bool 类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头(->)和围绕在参数周围的括号也可以被省略:
names.sorted(by: { s1, s2 in return s1 > s2})

单表达式闭包的隐式返回


  • 单行表达式闭包可以通过省略 return 关键字来隐式返回单行表达式的结果
names.sorted(by: { s1, s2 in s1 > s2})

参数名称缩写


  • Swift 自动为内联闭包提供了参数名称缩写功能,你可以直接通过 $0$1$2 来顺序调用闭包的参数,以此类推。
  • 闭包接受的参数的数量取决于所使用的缩写参数的最大编号。
  • in 关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:
names.sorted(by: {s1 > s2})

运算符方法


  • Swift 的 String 类型定义了关于大于号(>)的字符串实现,其作为一个函数接受两个 String 类型的参数并返回 Bool 类型的值。而这正好与 sorted(by:) 方法的参数需要的函数类型相符合。因此,你可以简单地传递一个大于号,Swift 可以自动推断找到系统自带的那个字符串函数的实现:
names.sorted(by: >)

尾随闭包


尾随闭包是一种特殊的闭包语法,它可以在函数调用的括号外部以简洁的方式提供闭包作为函数的最后一个参数。
使用尾随闭包的优势在于增加了代码的可读性和简洁性。当闭包作为函数的最后一个参数时,将闭包放在括号外部,可以使函数调用更加清晰,更接近于自然语言的阅读顺序。

func calculate(a: Int, b: Int, closure: (Int, Int) -> Int) {
let result = closure(a, b)
print(result)
}

// 调用函数时使用尾随闭包
calculate(a: 5, b: 3) { (x, y) -> Int in
return x + y
}

// 如果闭包只包含一个表达式,可以省略 return 关键字
calculate(a: 5, b: 3) { (x, y) in
x + y
}

// 省略参数的类型和括号
calculate(a: 5, b: 3) { x, y in
x + y
}

// 使用 $0, $1 等缩写形式代替参数名
calculate(a: 5, b: 3) {
$0 + $1
}


如果一个函数接受多个闭包,需要省略第一个尾随闭包的参数标签,并为其余尾随闭包添加标签。



值捕获


闭包可以在其被定义的上下文中捕获常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。



可以捕获值的闭包最简单的形式是嵌套函数,也就是定义在其他函数的函数体内的函数。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。



注意:



如果将闭包赋值给一个类实例的属性,并且该闭包通过访问该实例或其成员捕获了该实例,将会造成一个循环引用。



捕获列表


默认情况下,闭包会捕获附近作用域中的常量和变量,并使用强引用指向它们。你可以通过一个捕获列表来显示指定它的捕获行为。


捕获列表在参数列表之前,由中括号括起来,里面是由逗号分隔的一系列表达式。一旦使用了捕获列表,就必须使用in关键字,即使省略了参数名、参数类型和返回类型。


捕获列表中的项会在闭包创建时被初始化。每一项都会用闭包附近作用域中的同名常量或者变量的值初始化。例如下面的代码实例中,捕获列表包含a而不包含b,这将导致这两个变量有不同的行为。

var a = 0
var b = 0
let closure = { [a] in
print(a, b)
}

a = 10
b = 10
closure()
// 打印“0 10”

如果捕获列表中的值是类类型,可以使用weakunowned来修饰它,闭包会分别用弱引用、无主引用来捕获该值:

myFunction { print(self.title) }                    // 隐式强引用捕获
myFunction { [self] in print(self.title) } // 显式强引用捕获
myFunction { [weak self] in print(self!.title) } // 弱引用捕获
myFunction { [unowned self] in print(self.title) } // 无主引用捕获

在捕获列表中,也可以将任意表达式的值绑定到一个常量上。该表达式会在闭包被创建时进行求值,闭包会按照制定的引用类型来捕获表达式的值:

// 以弱引用捕获 self.parent 并赋值给 parent
myFunction { [weak parent = self.parent] in print(parent!.title) }

解决闭包的循环强引用


在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用。捕获列表定义了闭包体内捕获一个或者多个引用类型的规则。跟解决两个类实例间的循环强引用一样,声明每个捕获的引用为弱引用或无助引用,而不是强引用。应当根据代码关系来决定使用弱引用还是无主引用。


使用规则

  • 在闭包和捕获的实例总是互相引用并且同时销毁时,将闭包内的捕获定义为无主引用

  • 相反,在被捕获的引用可能会变为nil,将闭包内的捕获定义为弱引用,弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为nil。这使我们可以在闭包体内检查它们是否存在


注意



如果被捕获的实例绝对不会变为nil,应该使用无主引用,而不是弱引用。



闭包是引用类型


无论你将函数和闭包赋值给一个常量还是变量,你实际上都是将常量或变量的值设置为对应函数或闭包的引用


逃逸闭包


当一个闭包作为参数传到一个函数中,但是这个闭包在函数之后才被执行,称该闭包从函数中逃逸


在参数名之前标注@escaping指明这个闭包是允许逃逸出这个函数。


一种能使闭包"逃逸"出函数的方法是,将这个闭包包存在一个函数外部定义的变量中。例子:很多异步操作的函数接受一个闭包参数作为completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。这种情况下,闭包需要"逃逸"出函数,因为闭包需要在函数返回之后被调用:

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}

注意



将一个闭包标记为 @escaping 意味着你必须在闭包中显式地引用 self



自动闭包


自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// 打印“Now serving Ewa!”

总结


Swift 的闭包有以下几个主要的知识点:


  1. 闭包表达式(Closure Expressions):闭包表达式是一种在简短的几行代码中完成自包含的功能代码块。比如数组的排序方法 sorted(by:)
  2. 尾随闭包(Trailing Closures):如果你需要将一个很长的闭包表达式作为一个函数的最后一个参数,使用尾随闭包是很有用的。尾随闭包是一个书写在函数或方法的括号之后的闭包表达式。
  3. 值捕获(Value Capturing):闭包可以在其定义的上下文中捕获和存储任何常量和变量的引用。这就是所谓的闭包的值捕获特性。
  4. 闭包是引用类型(Closures Are Reference Types):无论你将函数/方法或闭包赋值给一个常量还是变量,你实际上都是将引用赋值给了一个常量或变量。如果你对这个引用进行了修改,那么它将影响原始数据。
  5. 逃逸闭包(Escaping Closures):一个闭包可以“逃逸”出被定义的函数并在函数返回后被调用。逃逸闭包通常存储在定义了该闭包的函数的外部。
  6. 自动闭包(Autoclosures):自动闭包能让你延迟处理,因为代码段不会被执行直到你调用这个闭包。自动闭包很有用,用来包装那些需要被延迟执行的代码。

Swift 闭包和OC Block


相似点:


  1. 都是可以捕获和存储其所在上下文的变量和常量的引用的代码块。
  2. 都可以作为参数传递给函数或方法,或者作为函数或方法的返回值。
  3. 都可以在代码块中定义局部变量和常量。
  4. 都可以访问其被创建时所处的上下文环境。

区别:


  1. 语法:Swift 的闭包语法更简洁明了,使用大括号 {} 来定义闭包,而 Objective-C 的 Block 语法相对复杂,使用 ^ 符号和大括号 ^{} 来定义 Block。
  2. 内存管理:Objective-C 的 Block 对捕获的对象默认使用强引用,需要注意避免循环引用;而 Swift 的闭包对捕获的变量默认使用强引用,但通过使用捕获列表(capture list)可以实现对捕获变量的弱引用或无引用。
  3. 类型推断:Swift 的闭包对于参数和返回值的类型具有类型推断的能力,可以省略类型注解;而 Objective-C 的 Block 需要明确指定参数和返回值的类型。
  4. 逃逸闭包:Swift 可以将闭包标记为 @escaping,表示闭包可能会在函数返回之后才被调用;而 Objective-C 的 Block 默认是可以在函数返回后被调用的。

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

盘点那些国际知名黑客(上篇)

iOS
电影中的黑客仅靠一部电脑就可以窃取别人的信息,利用自己高超的技术让公司甚至国家都胆战心惊。“黑客”原指热心于计算机技术、水平高超的电脑高手,但逐渐区分为黑帽、白帽、灰帽。这些术语源自美国流行文化的老式西部电影,其中主角戴白色或浅色帽子,反派戴黑色帽子。黑帽黑客...
继续阅读 »

电影中的黑客仅靠一部电脑就可以窃取别人的信息,利用自己高超的技术让公司甚至国家都胆战心惊。“黑客”原指热心于计算机技术、水平高超的电脑高手,但逐渐区分为黑帽、白帽、灰帽。这些术语源自美国流行文化的老式西部电影,其中主角戴白色或浅色帽子,反派戴黑色帽子。

  • 黑帽黑客以“利欲”为目标,通过破解、入侵去获取不法利益或发泄负面情绪。
    • 灰帽黑客以“昭告”为目标,透过破解、入侵炫耀自己所拥有的高超技术。
    • 白帽黑客以“改善”为目标,破解某个程序作出修改,透过入侵去提醒设备的系统管理者其安全漏洞,有时甚至主动予以修补。


白帽黑客大多是电脑安全公司的雇员,抑或响应招测单位的悬赏,通常是在合法的情况下攻击某系统,而黑帽黑客同时也被称作“Cracker”(溃客),打着黑客的旗帜做不光彩的事情。接下来我们为大家介绍一下世界上非常厉害的顶级黑客。


“互联网之子”亚伦·斯沃茨 



2013 年 1 月 11 日,年仅26 岁的互联网奇才亚伦斯沃茨自杀身亡。他的一生都在为互联网的信息自由而努力。亚伦·斯沃茨 被称作计算机天才、互联网时代的普罗米修斯。但在这些光环的背后,是美国政府为他定下的 13 项重罪指控和最高 35 年的监禁。



1986年亚伦出生在一个程序员之家。3岁学会编程,12岁创建了一个知识共享网站,叫做 The info,功能和维基百科一样,但比维基百科早了 5 年。15岁参与制订了CC协议。18岁入学斯坦福,20岁辍学创业与Reddit项目的两位创始人合伙开公司,并创建了Reddit网站。 Reddit在当时的影响力不断扩大,成为最受欢迎的网站之一。后来,雅伦卖掉Reddit网站,赚了 100 万美元,在他 20 岁那年成为百万富翁。



亚伦参与构建了RSS,这是博客时代的工具,能让用户订阅自己感兴趣的博客,当订阅更新的时候,用户会收到邮件提醒。彼时的亚伦沉浸在互联网程序世界的理想主义美梦里,他希望自己能像他的偶像万维网的发明人蒂姆·博纳斯·李那样,让互联网回归自由、共享的初心。亚伦对赚钱并不感兴趣,他的梦想是追求一个更宏大的目标——互联网知识的自由和共享。



一次机会,亚纶了解到一个名为PACER的网站,它是一个存放法庭电子记录的系统,每看一页里面的内容,联邦政府需要收取 8 美分的管理费用。这项业务每年能带给政府超过 100亿美元的收入。亚纶认为,这些联邦法庭记录的材料本就属于公众,应当免费向公众开放。于是他编写了一个程序,抓取了超过 2000 万页的PACER资料,并将它们投放到公共资源网上,供大家免费阅读,这一举动相当于直接减少了美国司法系统200万美元的收入,PACER也在巨大的舆论压力下逐渐免费。



亚伦有一个“开放图书馆”的梦想,他认为实体的图书馆限制了知识的传播,而互联网是连接书籍、读者、作者、纸张与思想最好的载体。他在08年发表的《开放获取游击队宣言》中写道:信息就是力量,但就像所有力量一样,有些人只想将其占为己有。世界上大多数的期刊都被类似Elsevier、JSTOR这样的巨头垄断,每阅读一篇文献都需要支付一定数量的费用。亚伦想帮助更多的人平等地享受这些知识。于是他通过自己高超的黑客技术,利用麻省理工学院的校园网络免费端口从JSTOR下载了 480 万篇论文,相当于整个文献数据库的80% 。



亚伦毫无意外地被警察逮捕,但由于并未用论文牟利,JSTOR放弃了对他的指控。但马萨诸塞州检察长坚持起诉雅伦违反了1986年的计算机欺诈与滥用法。若罪名成立,亚伦将面临35年的监禁和100万美元的巨额罚款。亚伦拒绝认罪他选择与美国政府斗争。在这期间,他积极参与到各种推动知识共享的运动中,传播他关于知识共享的理念。



2012 年9月 12 日,联邦检察官提出了一份替换起诉书,增加了电子欺诈、非经授权访问计算机等罪名,从原来的 4 项重罪指控变成了 13 项。2013 年1月,雅伦在布鲁克林的公寓中上吊自杀,结束了自己的生命。这一年,他26岁。他死后,超过5万人在白宫网站上请愿,要是起诉亚伦的检察官辞职,维基百科以黑屏为他悼念。



亚伦认为知识共享能提高全人类的智慧,信息共享、言论自由才是真正的平等。在他死后,黑客入侵了麻省理工官网,抗议这个被视为黑客起源地的学府对于亚伦的无所作为。麻省理工的标题页被改为亚伦在2008 年写下的《开放获取游击队宣言》宣言中鼓励每一个网络用户行动起来,阻止商人与政客将网络私有化。



2013 年3月,亚伦被追授詹姆斯麦迪逊奖,用以表彰他捍卫公众的知情权所作出的贡献。


“世界头号黑客”凯文·米特尼克



凯文·米特尼克曾说:“巡游五角大楼,登录克里姆林宫,进出全球所有计算机系统,摧垮全球金融秩序和重建新的世界格局,谁也阻挡不了我们的进攻,我们才是世界的主宰。”



如果说谁的人生像小说一样精彩,那一定当属凯文·米特尼克。他出生于美国洛杉矶,是第一个被美国联邦调查局通缉的黑客,号称“世界头号黑客”。



20世纪80年代,他因多次入侵美国联邦调查局的中央电脑系统等而被逮捕三次。米特尼克的所作所为与人们所熟知的犯罪不同,他所做的一切似乎都不是为了钱,他曾破坏了40多家的安全系统,只是为了表明他“有能力做到”。



2000年,米特改邪归正,成为了一名白帽黑客,成功创办了米特尼克安全咨询公司,专门世界500强企业做网络咨询工作。2023年7月16日去世,享年59岁。


“C语言之父”丹尼斯·里奇



“丹尼斯·里奇一点也不家喻户晓,但是如果你有一台显微镜,能在电脑里看到他的作品,你会发现里面到处都是他的作品。”



丹尼斯·里奇(Dennis Ritchie)是美国计算机科学家,被称为“C语言之父”“Unix之父”。20世纪60年代,丹尼斯·里奇和肯·汤普逊参与了贝尔实验室Multics系统的开发。在开发期间,肯·汤普逊开发了游戏【空间旅行】,但当时的系统不给力,游戏运行速度很慢。



然而不久之后贝尔实验室撤出了Multics计划,里奇和汤普逊利用一台旧的迷你计算机Digital PDP-7,1969年的圣诞节Unix系统诞生了。最初的Unix内核使用B语言编写,为了更好开发Unix,1973年,里奇以B语言为基础发展出C语言,在它的主体设计完成后,他和汤普森又用它完全重写了Unix。



随着计算机的发展,编程语言层出不穷,但无论如何翻涌,都无法改变C语言在编程界德高望重的地位,C++、Java、C#都是在C语言的基础上衍生出来的。而如今诸多流行的操作系统也是在Unix的基础上开发的,如Linux、MacOS甚至最流行的手机系统Android。


丹尼斯·里奇发明的C语言联合Unix操作系统,构建了当代计算机世界的钢筋水泥。正是因为C语言和Unix系统这两项成就,里奇成为了许多编程爱好者膜拜的对象。


“Linux之父”林纳斯·托瓦兹



“Given enough eyeballs,all bugs are shallow.”【很多双眼睛盯着的代码,bug无处藏身】


1991年Linus开发了Linux操作系统,在最初几年里,Linux并没有得到太多关注。但随着互联网的普及,如今的linux已经成为全球最受欢迎的操作系统之一,被广泛应用于服务器、移动设备、家庭电脑和超级计算机等领域。



Linux的诞生充满了偶然,林纳斯经常用他的终端仿真器去访问大学主机上的新闻组和邮件,为了方便读写和下载文件,他自己编写了磁盘驱动程序和文件系统。这些在后来成为了Linux第一个内核的雏形,那时的他年仅21岁。



我们能够看到如今日渐壮大的Linux,但也不难发现,在成功的Linux背后,有着几十年如一日的持之以恒,有着对高质量代码的坚持,更是有着合作的。林纳斯没有建立组织,仅仅通过吸引全球数以万计的自由开发者免费贡献就完成了项目。Linux不仅仅是一个代码项目,也是一种互联网出现以后的新的协作方式——开源模式。


写在最后


现在国家很重视网络安全建设,网络安全已经成为了很多高校的一级学科,因此通过正常学习即可进入网络安全行业,大家一定要遵纪守法,效仿黑客们的行为做一些非法的黑客攻击行为,下期我们将继续为大家送上其他几位世界著名黑客的传奇故事,请大家保持关注哦。


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

🔥🔥🔥996已明确违法,从此拒绝精神内耗!

之前一个禅道用户说,他在国外工作时主动加过两次班,然而被上司慰问了。上司特别严肃地跟他说:“请你不要再加班了,这让我很困扰。我们不加班,而且我无法向我的上司解释你为什么要加班,工作做不完可以明天做,工作只是你一天的一部分,利用好这8小时就可以了。” 对内卷严重...
继续阅读 »

之前一个禅道用户说,他在国外工作时主动加过两次班,然而被上司慰问了。上司特别严肃地跟他说:“请你不要再加班了,这让我很困扰。我们不加班,而且我无法向我的上司解释你为什么要加班,工作做不完可以明天做,工作只是你一天的一部分,利用好这8小时就可以了。”


对内卷严重的公司来说:一天干8小时怎么够?全天all in的状态才是我想要的。于是996疯狂盛行。


冷知识:“996”已严重违反法律规定。


早在2021年8月,最高法、人社部就曾联合发布超时加班典型案例,明确“工作时间为早9时至晚9时,每周工作6天”的内容,严重违反法律关于延长工作时间上限的规定,应认定为无效。


最近两会期间,全国政协委员蒋胜男也在提案中表示,应加强劳动法对劳动者的休息权保护。


由此,新的一波讨论已然来袭。


一、“996”带来了什么?



产品没有核心价值,缺乏核心竞争力,害怕落后于竞争激烈的市场……越来越多的管理者选择用加班、拉长工作时间来弥补技术创新的匮乏。


这种高强度的996工作制,侵占了我们的“充电”时间,甚至让我们丧失对新事物的接收力和思考能力;高强度的工作压力+长期的加班、熬夜、不规律饮食,给身体带来了沉重的负担;在忙碌了一周之后,感受到的是前所未有的迷茫与疲倦,精神内耗愈发严重


而对于企业来说,当员工沦为“执行工具”,原本的创新型发展却变成闭门造车,所以只能不停地加班、拉长工作时间,以产出更多的成果。长此以往,就形成了一种恶性循环。


在普遍“苦996久矣”的环境下,“8小时工作制”的推崇便显得尤为可贵。


二、“8小时工作制”从何而来?


8小时工作制,不应成为一个冷知识。《中华人民共和国劳动法》第三十六条规定:国家实行劳动者每日工作时间不超过8小时,平均每周工作时间不超过44小时的工时制度


8小时工作制的提出,要感谢来自英国的Robert Owen。1817年,他提出了“8小时工作制”,也就是将一天分成3等分,8小时工作、8小时娱乐、8小时休息。在当时一周普遍工作时间超过80个小时的情况下,这种要求简直是天方夜谭。


而8小时工作制得到推行,应归功于福特汽车品牌的创始人亨利·福特。1914年1月,福特公司宣布将员工的最低薪资从每天的2.34美元涨到5美元,工作时间减少至每天8小时。这项计划将会使福特公司多支付1000万美元。



在增加了员工薪资后,最直观的是员工流动率的下降。员工的稳定以及对操作的愈发熟练,增加了生产效率,从而降低成本、提高产量。最后,福特公司只用了两年时间,就将利润增加了一倍。


1926年,福特公司又宣布将员工的工作时间改为每周5天、每天8小时。亨利·福特用实际行动证明了增加工作收入、减少工作时间,对公司来说是可以实现正向创收的。


随后,8小时工作制才开始逐渐普及。随着Z时代的到来,更多新型职场状态也已经诞生。


液态职场早已到来,你准备好了吗?


三、液态职场是什么?



1)“3+2”混合办公模式


早在2022年,全国人大代表黄细花提交了建议,呼吁可推广“3+2”混合办公模式,允许员工每周可选择1-2天在家远程办公。黄细花还表示,推广“3+2”混合办公制,提高员工工作效率的同时,减轻年轻群体的生活压力,减少城市通勤压力。对女性员工而言,弹性的办公时间能让她们更好地平衡工作和生活。混合办公制对企业、员工和社会都将产生深远影响。


于是,不少企业开始了行动。携程推出了“3+2”混合办公模式的新政策:从 2022年3月起,允许员工每周三、周五在家远程办公。


2)四天半工作制


乐视也紧随其后,推出“四天半工作制”,每周三弹性工作半天。


3)“上4休3”的工作制


微软日本公司,也早在2019年8月曾宣布,公司开始试运行每周“上4休3”的工作制度,即每周五、六、日休息3天,周五所有办公室全部关闭。


不管是8小时工作制还是上4休3”,其实本质上都一样:都是为了迎合当下的现状,打破固有传统的工作模式,寻找更加多元化的新型职场状态,让员工能够充分休息,提升效率和创造力,也能节省企业开支,最终双方获益。


这世界变化太快了,上一秒还在“996”中疯狂内卷,下一秒就已经有先行者去探索更适合的工作节奏。液态职场时代已经到来,你准备好了吗?


四、提高工作效率,大胆对996说不!


作为打工人,不管是996还是8小时工作制,虽然都不是我们能决定的,但我们可以用法律来维护自己的权利,学会说“不”。利用好这8小时,发挥出自己的价值,提高自身的创新能力和效率,是为了更有底气的说“不”!这样才能保证企业与员工之间形成一个正向循环。如何利用好8小时?给大家分享几个提高工作效率的小技巧:

  1. 保持桌面整洁,减少其他事物对工作专注度的干扰;

  2. 巧用看板,可视化工作任务,便于进行任务管理;

  3. 排列优先级,按照任务的重要紧急程度,尽量避免并行多个任务;

  4. 随时记录工作中的创意和灵感

  5. 将重复、机械的工作自动化,解放双手;

  6. 定期复盘:不断改进与优化;

  7. 培养闭环思维:凡事有交代,件件有着落,事事有回音。


工作本应是我们热爱的样子。当我们还沉浸在无休止的工作与忙碌中,被疲惫、彷徨等负面情绪包围,开始精神内耗时,是时候明确拒绝996了!


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

iOS实现宽度不同无限轮播图

iOS
背景 项目中需要实现一个不同宽度的图片的无限轮播图效果,而且每次滚动,只滚到下一个图片。由于业界实现的轮播图效果都是等宽图片,所以需要重新根据“以假乱真”的原理,设计一款不同宽度的轮播效果; 演示效果 底部是个collectionView,顶部盖了个透明的sc...
继续阅读 »

背景


项目中需要实现一个不同宽度的图片的无限轮播图效果,而且每次滚动,只滚到下一个图片。由于业界实现的轮播图效果都是等宽图片,所以需要重新根据“以假乱真”的原理,设计一款不同宽度的轮播效果;


演示效果


底部是个collectionView,顶部盖了个透明的scrollView,传入的数据源是:

NSArray *imageWidthArray = @[@(200), @(60), @(120)];



实现思路


  1. 传入一个存储图片宽度的数组,计算出屏幕可见的个数,比如下图,假如可见数为3个;

  2. 左、右两侧各有2个灰块,用于实现以假乱真的数据;(两侧各需生成的灰块数=屏幕可见数-1)

    • 比如当前看到123,左滑会滚到231,再左滑会滚到312,此时设置contentOffset,切到前面那个312;
    • 比如当前看到123,右滑会滚到312,再右滑会滚到231,此时设置contentOffset,切到后面那个231;
    1. 为了性能方面的考虑,使用的是collectionView;
    2. 关于每次滚动,只滚到下一个,实现方式则是在collectionView上面盖一个scrollView,设置其isPagingEnabled = YES; scrollView里面的页数和数据源保持一致(方便计算滚到哪个page);





完整的代码实现


Github Demo


ViewController:

#import "ViewController.h"
#import "MyCollectionViewCell.h"

#define padding 10.f
#define margin 16.f
#define scrollViewWidth (self.view.bounds.size.width - 2 * margin)
#define scrollViewHeight 200.f

@interface ViewController ()<UIScrollViewDelegate, UICollectionViewDelegate, UICollectionViewDataSource>

@property (nonatomic, strong) UICollectionView *collectionView;
@property (nonatomic, strong) UIScrollView *topScrollView;
@property (nonatomic, strong) UIPageControl *pageControl;
@property (nonatomic, strong) NSArray *imageWidthArray; // 用户传入,图片宽度数组
@property (nonatomic, assign) NSInteger canSeeViewCount; // 屏幕最多可见几个view
@property (nonatomic, strong) NSMutableArray *imageWidthMuArray;
@property (nonatomic, strong) NSMutableArray *imageContentOffsetXArray;
@property (nonatomic, strong) NSMutableArray *currentPageMuArray;

@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupViewWithImageWidthArray:@[@(200), @(60), @(120)]];
//    [self setupViewWithImageWidthArray:@[@(150), @(80),@(60), @(120)]];
}

-(void)setupViewWithImageWidthArray:(NSArray *)imageWidthArray {
    // 根据机型宽度,计算屏幕可见数量
    self.canSeeViewCount = imageWidthArray.count;
    CGFloat checkWidth = 0;
    for (NSInteger i = 0; i < imageWidthArray.count; i ++) {
        checkWidth += [imageWidthArray[i] floatValue];
        if (checkWidth >= scrollViewWidth) {
            self.canSeeViewCount = i + 1;
        }
    }

    self.imageWidthArray = imageWidthArray;
    self.imageContentOffsetXArray = [NSMutableArray arrayWithCapacity:self.imageWidthArray.count];

    // 插入头尾数据(前后插入可见数-1个)、生成currentPageMuArray
    self.imageWidthMuArray = [NSMutableArray array];
    self.currentPageMuArray = [NSMutableArray array];
    for (NSInteger i = self.imageWidthArray.count - (self.canSeeViewCount - 1); i < self.imageWidthArray.count; i ++) {
        [self.imageWidthMuArray addObject:self.imageWidthArray[i]];
        [self.currentPageMuArray addObject:@(i)];
    }
    [self.imageWidthMuArray addObjectsFromArray:self.imageWidthArray];

    for (NSInteger i = 0; i < self.imageWidthArray.count; i ++) {
        [self.currentPageMuArray addObject:@(i)];
    }

    for (NSInteger i = 0; i < (self.canSeeViewCount - 1); i ++) {
        [self.imageWidthMuArray addObject:self.imageWidthArray[i]];
        [self.currentPageMuArray addObject:@(i)];
    }

    CGFloat collectionViewContentSizeWidth = 0;
    for (NSInteger i = 0; i < self.imageWidthMuArray.count; i ++) {
        CGFloat imageWidth = [self.imageWidthMuArray[i] floatValue];
        if ( i > 0) {
            collectionViewContentSizeWidth += padding;
        }
        [self.imageContentOffsetXArray addObject:@(collectionViewContentSizeWidth)];
        collectionViewContentSizeWidth += imageWidth;
    }

    // collectionView
    UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
    flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    flowLayout.minimumInteritemSpacing = padding;
    flowLayout.minimumLineSpacing = padding;

    UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(margin, 100, scrollViewWidth, scrollViewHeight) collectionViewLayout:flowLayout];
    [collectionView registerClass:[MyCollectionViewCell class] forCellWithReuseIdentifier:@"MyCollectionViewCell"];
    collectionView.dataSource = self;
    collectionView.delegate = self;
    collectionView.bounces = NO;
    collectionView.showsHorizontalScrollIndicator = NO;
    collectionView.backgroundColor = [UIColor brownColor];
    [self.view addSubview:collectionView];
    collectionView.contentSize = CGSizeMake(collectionViewContentSizeWidth, 0);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [collectionView setContentOffset:CGPointMake([self.imageContentOffsetXArray[self.canSeeViewCount - 1] floatValue], 0)];
    });
    self.collectionView = collectionView;

    // topScrollView
    UIScrollView *topScrollView = [[UIScrollView alloc] initWithFrame:collectionView.frame];
    topScrollView.showsHorizontalScrollIndicator = NO;
    [topScrollView setPagingEnabled:YES];
    topScrollView.backgroundColor = [UIColor clearColor];
    topScrollView.delegate = self;
    topScrollView.bounces = NO;
    [self.view addSubview:topScrollView];
    self.topScrollView = topScrollView;
    topScrollView.contentSize = CGSizeMake(self.imageWidthMuArray.count * scrollViewWidth, 0);
    [topScrollView setContentOffset:CGPointMake((self.canSeeViewCount - 1) * scrollViewWidth, 0)];

    // pageControl
    CGFloat pageControlHeight = 50.f;
    UIPageControl *pageControl = [[UIPageControl alloc] initWithFrame:CGRectMake(margin, 100 + scrollViewHeight-pageControlHeight, scrollViewWidth, pageControlHeight)];
    pageControl.numberOfPages = self.imageWidthArray.count;
    pageControl.currentPage = 0;
    [self.view addSubview:pageControl];
    self.pageControl = pageControl;
}

#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (scrollView == self.collectionView) {
        return;
    }

    // 页面整数部分
    NSInteger floorPageIndex = floor(scrollView.contentOffset.x / scrollView.frame.size.width);

    // 小数部分
    CGFloat pageRate = scrollView.contentOffset.x / scrollView.frame.size.width - floor(scrollView.contentOffset.x / scrollView.frame.size.width);
    CGFloat imageContentOffsetX = [self.imageContentOffsetXArray[floorPageIndex] floatValue];
    CGFloat imageWidth = [self.imageWidthMuArray[floorPageIndex] floatValue];
    self.collectionView.contentOffset = CGPointMake(imageContentOffsetX + (imageWidth + 10.f) * pageRate, 0);
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    NSInteger rightIndex = (self.canSeeViewCount - 1) + (self.imageWidthArray.count) - 1;
    NSInteger leftIndex = (self.canSeeViewCount - 1) - 1;

    // 右边卡到尾时
    if (self.collectionView.contentOffset.x == [self.imageContentOffsetXArray[rightIndex] floatValue]) {
        [self.collectionView setContentOffset:CGPointMake([self.imageContentOffsetXArray[leftIndex] floatValue], 0)];
    }

    // 左边卡到头时
    else if (self.collectionView.contentOffset.x == 0) {
        [self.collectionView setContentOffset:CGPointMake([self.imageContentOffsetXArray[self.imageWidthArray.count] floatValue], 0)];
    }

    // 右边卡到尾时
    if (self.topScrollView.contentOffset.x == scrollViewWidth * rightIndex) {
        [self.topScrollView setContentOffset:CGPointMake(scrollViewWidth * leftIndex, 0)];
    }

    // 左边卡到头时
    if (self.topScrollView.contentOffset.x == 0) {
        [self.topScrollView setContentOffset:CGPointMake(scrollViewWidth * self.imageWidthArray.count, 0)];
    }

    // 设置currentPage
    NSInteger floorPageIndex = floor(scrollView.contentOffset.x / scrollView.frame.size.width);
    self.pageControl.currentPage = [self.currentPageMuArray[floorPageIndex] intValue];
}

#pragma mark - UICollectionViewDelegate, UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return self.imageWidthMuArray.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    MyCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"MyCollectionViewCell" forIndexPath:indexPath];
    cell.labelText = [NSString stringWithFormat:@"%.0f", [self.imageWidthMuArray[indexPath.item] floatValue]];
    return cell;
}

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    return CGSizeMake([self.imageWidthMuArray[indexPath.item] floatValue], scrollViewHeight);
}

@end

MyCollectionViewCell:

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface MyCollectionViewCell : UICollectionViewCell

@property (nonatomic, copy) NSString *labelText;

@end

NS_ASSUME_NONNULL_END
#import "MyCollectionViewCell.h"
#import "Masonry.h"

@interface MyCollectionViewCell()

@property (nonatomic, strong) UILabel *label;

@end

@implementation MyCollectionViewCell

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self setupUI];
    }
    return self;
}

- (void)setupUI {
self.backgroundColor = [UIColor grayColor];
    UILabel *label = [[UILabel alloc] init];
    label.textAlignment = NSTextAlignmentCenter;
    label.font = [UIFont boldSystemFontOfSize:18];
    [self.contentView addSubview:label];
    [label mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.contentView);
    }];
    self.label = label;
}

- (void)setLabelText:(NSString *)labelText {
    _labelText = labelText;
    self.label.text = labelText;
}

-(void)prepareForReuse {
    [super prepareForReuse];
    self.label.text = @"";
}
@end

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

环信FCM推送详细步骤

集成FCM推推送
准备的地址有 :https://firebase.google.com
1.firebase官网选择我们自己创建的项目

2.点到这个设置按键

3.我们打开到项目设置->常规 拉到最下面有一个“您的应用” 点击下载json文件,json文件的使用是客户端放在安卓项目的app目录下

4.首先环信需要的信息有 项目设置中-> 服务账号 生成新的私钥 生成的文件我们要上传到环信的管理后台证书部分(V1)

5.点击上传证书会选择你下载的文件,注意!! 名称是由你设置的项目名称的json文件 并不是 google-services.json
6.项目名称 是你的发送者ID 这个id 我们在firebase官网中的项目设置-〉常规 -〉您的项目->的项目编号就是您的SenderID 填写到环信官网即可 另外客户端的 google-services.json 这个文件 打开后 project number 也是SenderID

7.将我们下载好的 google-services.json 文件放到app的目录下 (文件获取可以反回步骤3 查看)

8.打开build的根目录添加 :
buildscript {
dependencies {
// classpath 'com.android.tools.build:gradle:7.2.2'
classpath 'com.google.gms:google-services:4.3.8'
}
}

9.build.gradle.app部分添加:
implementation platform('com.google.firebase:firebase-bom:28.4.1')
implementation 'com.google.firebase:firebase-messaging'

10.对应好appkey 以及我们的客户端初始化fcm的senderID

11.在登陆前 初始化以后 添加以下代码:
EMPushHelper.getInstance().setPushListener(new PushListener() {
@Override
public void onError(EMPushType pushType, long errorCode) {
EMLog.e("PushClient", "Push client occur a error: " + pushType + " - " + errorCode);
}

@Override
public boolean isSupportPush(EMPushType pushType, EMPushConfig pushConfig) {
if(pushType==EMPushType.FCM)
{
return GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(MainActivity.this)
== ConnectionResult.SUCCESS;
}
return super.isSupportPush(pushType, pushConfig);
}
});

12.登陆成功后的第一个页面添加 :
if(GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(NewAcitivty.this) != ConnectionResult.SUCCESS) {
return;
}
FirebaseMessaging.getInstance().getToken().addOnCompleteListener(new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
if (!task.isSuccessful()) {
EMLog.d("PushClient", "Fetching FCM registration token failed:"+task.getException());
return;
}
// 获取新的 FCM 注册 token
String token = task.getResult();
EMClient.getInstance().sendFCMTokenToServer(token);
}
});

13.清单文件注册sevices 主要是为了继承FCM的服务 必要操作!

添加代码: 重写onMessageReceived
收到消息后 就在这个方法中 自己调用 本地通知 因为fCM的推送只有唤醒
public class FireBaseservice extends FirebaseMessagingService {
@Override
public void onMessageReceived(@NonNull RemoteMessage message) {
super.onMessageReceived(message);
if (message.getData().size() > 0) {
String alter = message.getData().get("alter");
Log.d("", "onMessageReceived: " + alter);
}

}
@Override
public void onNewToken(@NonNull String token) {
Log.i("MessagingService", "onNewToken: " + token);
// 若要对该应用实例发送消息或管理服务端的应用订阅,将 FCM 注册 token 发送至你的应用服务器。
if(EMClient.getInstance().isSdkInited()) {
EMClient.getInstance().sendFCMTokenToServer(token);
}
}
}
14.准备测试 这个时候我们就要验证我们的成果了 首先要看自己登录到环信后 是否有绑定证书 借用环信的即时推送功能查看是否有绑定证书
这个时候看到登录了证书还是没有绑定上 那肯定是客户端出现问题了

15.检查错误 看到提示了com.xxxx.play 安装 这个是因为 你的设备没有打开 VPN 或者VPN不稳定,所以你首先要确定VPN打开并且 稳定 然后我们在重新登录测试

16.这个时候我们在借用即时推送查看 看看有没有绑定到环信 看到该字样就证明你的证书已经绑定上了 直接杀掉进程离线 测试离线推送,(一定要在清单文件注册的谷歌服务中 重新的onMessageReceived 中写入本地通知展示 不然fcm的推送只有唤醒)

升级Xcode 15后,出现大量Duplicate symbols问题的解决方案

升级到Xcode 15后,原先Xcode14可以编译的项目出现大量Duplicate symbols,且引用报错指向同一个路径(一般为Framework)下的同一个文件。经过查找相关资料,查到可通过在Xcode -> Target -> Build...
继续阅读 »

升级到Xcode 15后,原先Xcode14可以编译的项目出现大量Duplicate symbols,且引用报错指向同一个路径(一般为Framework)下的同一个文件。经过查找相关资料,查到可通过

在Xcode -> Target -> Build Setting -> Other Linker Flags 添加一行"-ld64"

即可解决该问题

原因是Xcode15采用了新的链接器(Linker),被称作“ld_prime”。新的连接器有诸多好处,尤其是对合并库的支持方面,具体可以查看WWDC 2023 SESSION 10268 Meet mergeable libraries.。然而,链接器的升级可能会出现不兼容老库的情况出现。遇到这种情况,可以通过恢复旧的连接器来解决这个问题。从Other Linker Flags添加"-ld64"后,就会覆盖Xcode编译时选择的链接器,因此可以正常访问。

收起阅读 »

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
收起阅读 »