注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

为什么年轻人要珍惜机会窗口

今天来跟大家分享一下什么是机会窗口以及为什么要珍惜机会窗口?首先从我个人的经验出发,我觉得不管是在学习,在职业,在投资,现在社会各个方面都是有很多非常好的机会的。但是这些好的机会又不经常有,那到底如何定义好机会,又如何抓住机会?那这里面先说一下什么叫好的机会。...
继续阅读 »


今天来跟大家分享一下什么是机会窗口以及为什么要珍惜机会窗口?首先从我个人的经验出发,我觉得不管是在学习,在职业,在投资,现在社会各个方面都是有很多非常好的机会的。但是这些好的机会又不经常有,那到底如何定义好机会,又如何抓住机会?那这里面先说一下什么叫好的机会。


什么是好机会


就以职业的成长性来说,互联网整个行业的二十年蓬勃发展就是极好的一个机会,大概从20年起到如今这个时间段都有一个非常好的机会,那指的就是哪怕你的能力稍微弱一点,你都能够在这个机会里面找到自己的红利。比如我有很多稍微找我几届的同事或者主管,他们可能在学历或者能力方面都没有特别高,但是正因为赶上了红利,他们的晋升特别快,拿到了股票也特别多,我好几个同事基本上在上海或者杭州都有两三套房,并且还有大量的现金。甚至有一些大专的同事,都拿到大量的股票,接近财富自由。


所以这种机会窗口是整个行业变革,整个现代社会发展带来的,它打开了一扇可以改变命运的窗口。这种时间窗口相对来说会比较长,特别是相对一个人的职业三十年来说。而且这种行业的机会,可能就有持续五年或者十年这样的时间。而在这样的机会窗口内,你不管是哪个点入局都能吃到一定的发展红利。


比如我记得早个五六年,很多人在找工作的时候,往往会纠结于去百度还是腾讯或者是阿里,但实际上我们发现站在更高,更长远的角度来说,他们选择任何一个公司收获到的都非常的丰厚,相比现在的毕业生,哪怕是双985可能也是无法找到一份工作,想想那时候是不是很幸福?在这种大背景下,在机会窗口来临的时候,你选错了,选的不是很好,都没有关系,你都能够收获到足够的红利,最多就是你赚50万还是100万的区别,而时代没有的话,上限就是赚10万。


除了这个例子之外,还有一个红利机会点就是房地产。我知道在差不多2005年~2018年这个时间段里面,只要你买房基本上都是赚的,所以我很多同学往往都有一个非常巨大的认知论,就认为他买房赚钱是因为他牛逼,他地段选的好,户型选的好,他完全归因于他买的房价大涨是因为眼光好,怎么样怎么样才能赚到钱,而实际上这只是时代给他的红利而已,其实再往回倒个七八年你在哪里买房都是赚的。但实际上以我的经验来看,不管那个时候,哪怕你在小城市买一套房子,涨幅可能都是两三倍的。


所以当时的眼光和认知和选择能力确实会决定了你的资产增值多少,但是只要在那个红利周期内,你做的选择大概率都不会太差,这也是雷军所说,站在风口上的猪也可以飞起来,说的就是这个道理。



这就是整个时代给我们的窗口,这个窗口可能会给的特别大,而且很多时候在这个周期里面,你根本感觉不到这是时代给你的机会,你只是做你正常的操作,到了指定的时间去指定的公司,去选合适热门专业,去买认为合适的房子,你觉得很自然,但实际上从后面再看,你会发现你在十年前做的选择和十年后做的选择成本、难度以及你付出的代价完全不一样。同样是89平米的房子,放在2010年就是3000一平米,放在现在就是8万一平米。同样是去阿里巴巴,以前大专就行,现在本硕985都直接被Pass。


上面说的都是比较大的机会,那我再说一个相对来说比较小的窗口。这些非常大的机会窗口还是依赖于各种不同不一样的大背景,但是有很多机会并没有像这种时代给的机会一样,可以有长达五年,十年你可以认真去选,你可以去大胆的犯错和试错,选错了你重新再来一次就可以了,但是我们在实际工作里面,我们碰到的一些机会点,其实时间窗口非常的短。如果你稍微不慎,可能就错过了这个机会,而等待下一个机会就不知道猴年马月了,所以我们就要在这个地方要抓住那稍纵即逝的机会窗口。



我举一个例子,比如说这两年是低代码的元年,而这个时候如果你之前刚好一直在从事低代码或者低代码相关的工作,那么到了这两年,你的议价空间是非常大的,因为很多公司都在如火如荼的去做这块的业务,在短时间内是没有办法慢慢培养出或者招聘到这类专才,所以往往公司愿意溢价去花费大价钱去购买和招聘相关的同学,所以这个时候如果你抓住了机会,你可以得到一个很高的议价,比如说层级直接变高了一层或者你的总包直接变成了两倍,甚至非常有机会作为骨干负责人拉起一支团队,那么你进入管理岗位也就水到渠成了。


为什么机会有窗口


而这种机会窗口往往只有半年,一年或者最多两年,因为到了一两年之后,有很多的同学也感知到了这个先机,往往就会把自己的精力投到这一块来,那么意味着供需就发生了变化,供应方就会越来越多,那么就使得需求方有溢价的能力,这个时候到了两年之后可能就完全拉平了,这个低代码行业跟其他行业变得完全一样,甚至再往后人才堆积的更加的过分,你可能连这个机会都没有了,只剩下被选择的命运。历史历代,都演绎着完全相同的剧本。


到了直播行业也是一样,在直播刚刚兴起的时候,如果你恰巧做的是相关业务,这个时候你跳过去往往会能够涨薪特别高,工资的幅度也是特别高,所以在这个时候你有充分的议价权,但是窗口我们也知道往往只有几年,而且在互联网这么变化快的情况下的话,时间可能会进一步缩短,比如这两年已经到了直播的红海,基本上该用直播的用户已经到顶了,这个时候虽然还有大把的招聘,但需求实际上已经是强弩之末了。


随着人口红利到底的时候,我们所谓的互联网这些机会的窗口实际上已经是没了,变得普普通通的一份职业而已,而且这个时候入局往往有可能会遭受灭顶之灾,比如说最近就听说到整个直播行业要整顿,一旦业务发生了整顿,对人才的需求的调整就会变得非常的明显,往往再激烈一点可能就会快速裁员,不要说红利了,拿到的全部是负债。


再往小的一些说,可能针对每个人的职业窗口也是不一样的,比如说对于有些大企业,有一些管理的岗位,但往往是因为原管理的同学离职或者新增的岗位,这个时候会有短时间的招聘名额来等待这个位置,而一旦你错过了这个机会以后,这个位置没了以后,可能这个坑位就不需要人了。这个时候不是你能力好不好的问题,是有没有坑位的问题。


所以好机会往往只是一瞬间而已,很多同学担心稳定性,希望在一个地方一直苟着求稳定,这个其实跟体制内没有任何的区别。风险和收益从哲学层面上来说,都是相对的,或者说没有决定的风险,也没有决定的稳定,风险和稳定阶段性只能取其一,长期看稳定和风险是互相转化的。我经常听到有人说大厂稳定,但是实际上我们在分析背后的原因,大厂稳定本身就是个伪命题。又稳定,又高薪,又轻松,这是不可能的。所以我称之为「工作不可能的三角特点」。


但很多人说我能否要里面的两个因素,我要稳定要高薪但是我愿意加班吃苦。


对不起,这个其实也是不可能的。我们可以站在企业的角度来考虑一下,一旦我这个工作特别的高薪又稳定的情况下的话,那虽然你干的很苦,但我始终在人力成本特别充分的情况下的话,公司能找到更好的替代者来。同样的工作量,但是花更少的钱来解决,说白了大部分所谓的高薪岗位没有什么严格的技术壁垒。


所以我们说过的,站在更大的角度来说,互联网也是一个机会窗口,因为过了这个窗口之后,可能你想加班加点熬夜,你可能都拿不到这样的一个薪水和待遇。


如何抓住机会窗口


反而换一个角度来说,我们一定要抓住这样的机会窗口,这样的机会窗口可以给我们的发展带来一个质的变化,当然也有很多时候我们会做一些错误的选择,比如说我们找到了一个我们认为好的机会,但实际上这个机会是有问题的,比如说我去了某一个创业公司,原本以为会有巨大的发展,但是后面倒闭了。当然这个也是一种博弈,这里面非常考核一个同学的综合的认知能力、选择能力和纠错能力。不仅要判断能否找到合适的机会,还要在碰到了困难的时候能够去快速的去纠错。


从我的例子来看,如敢于去挑战这种新机会的同学,哪怕其中有一些不如意的变动,但是大概率他的结果大概率不会太差。比如我有个同学从集团跳槽到蚂蚁国际,呆了一年就觉得部门有问题,后面又去了字节头条,现在也非常稳定。还有一个同学出去创业,也不顺利,但是后面又折腾成了另外一个大型公司的高级主管。


反而是事事求稳,稳住某一个大厂,稳住某一个职位,稳住每一个薪水,到了最后往往收益会越来越小,直到最后完全被动。整体上来看,整个社会会把更多的报酬分向于这些敢于挑战,敢于冒险,敢于拼搏的人的,而不会把大量的资源分享到又稳定,又顽固,又不愿意改变的这群人,这是当前社会的游戏规则。这个在大数据上面完全是合理的,只不过落到每个人的头上的尺度和比例会有点不一样。


所以站在我现在的角度上来看,我觉得所有的想向上奋进的同学都应该主动抓住变革的机会。因为这个好机会可能对在你的人生来说,几十年可能就这么一两次,甚至有些都是完全为你量身定做的机会,如果你一旦错过了以后,可能你抓住下一个机会的成本和代价就变得会非常的大。



尤其是年轻人更应该去折腾,因为你的试错的成本会非常低,当你发现了你的错误决策以后,你能够快速的去更正,去变化,所以在年轻的时候往往就应该多折腾一点,善于去准备好去等待好的机会,如果机会来了,大胆的出击。


作者:ali老蒋
来源:juejin.cn/post/7296865632166805513
收起阅读 »

绝对超乎你想象,程序员一辈子到底能挣多少钱?

薪资追求 作为资深码农,对这个行业还是比较了解的。 大部分年轻程序员可能都只是想着如何提高技术,为拿到更好的Offer尔努力,很多码农到30岁后才突然发现,程序员高薪也就那么回事,自己好像也并没有存到什么钱,结婚生子,房贷,车贷,个人健康问题好像一样也没落下。...
继续阅读 »

薪资追求


作为资深码农,对这个行业还是比较了解的。


大部分年轻程序员可能都只是想着如何提高技术,为拿到更好的Offer尔努力,很多码农到30岁后才突然发现,程序员高薪也就那么回事,自己好像也并没有存到什么钱,结婚生子,房贷,车贷,个人健康问题好像一样也没落下。其实回头看,互联网高薪也就那么几年,大部分没赶上的跟其它专业的差距也没那么大,甚至35失业后可能还不如同时期传统行业的同学,毕竟码农离开了平台,个人能发挥的空间很有限。


image.png
薪水档位


避免盲目的埋头学技术,努力工作,我们可以提前算一下,大部分程序员一辈子到底能挣多少钱。从毕业22岁算起,不同路线的码农的职业生涯可能有所区别,我们以互联网聚集的一线城市为例来看下各种情况:


第一档:


毕业即一线大厂,年薪30w-50w;普遍的能干三年,薪资区间在:120w-150w;


普遍三年一跳或不跳,一般不跳30%涨幅,跳槽可能有50%涨幅,年薪40w-55w再干两年能拿:80w-110w;


一般五年会有一波比较大的涨幅,算50%,年薪加股票一般能翻一倍,年薪区间会在80w-120w;能到这个段  位都是相对稳定,一般股票需要四年拿完,就按四年算,能拿到:320w-480w;


这个阶段算下来,大部分已经组建家庭区域稳定,31岁左右也不大会频繁变动,职位基本在P7以上,个人利益与公司利益绑定,如果自身没有想法,不创业基本能保证年薪区间在100w-200w,以40岁为终点来算基本能再多拿:900w-1800w;


总体的薪水在:120w+80w+320w+900w=1420w 到 150w+110w+480w+1800w=2540w;


image.png


第二档:


毕业二线大厂,准大厂,年薪15w-25w;普遍干三年,薪资区间在:45w-75w;


普遍三年一跳或不跳,一般不跳30%涨幅,跳槽可能有50%涨幅,年薪20w-40w再干两年能拿:40w-80w;


一般五年会有一波比较大的涨幅,算50%,这个段位基本没有股票,很多公司会卡薪水上限,年薪区间会在30w-50w;社招对标P7基本会在6年经验以上,所以这里普遍能再干两年拿:60w-100w;


这个档位工作7到8年,是大厂比较喜欢的人群,大部分能到P7,加上股票年薪能到60w-120w,一般四年拿完,算下来能多拿:240w-480w;


整体算下来这个段位基本也到33岁左右,稳定来讲大部分人的职级稳定到P7,P8,考虑年龄因素薪水幅度不会太大,维持70w-120w,假设能稳定干到38岁,可以拿到:350w-600w;


总体的薪水在:45w+40w+60w+240w+350w约700w 到 75w+80w+100w+480w+600w=1300w;


第三档:


毕业优质中小公司,年薪10w-15w;普遍干三年,薪资区间在:24w-45w;


普遍三年,五年靠跳槽涨薪,三年涨30%,干两年能拿:25w-40w;


五年跳槽涨50%,年薪30w-45w,干三年多拿:90w-140w;


30岁左右,比较尴尬,能冲一下P7进去也是大头兵,薪资也不会高出很多,年薪基本40w-70w;假设能稳定干到35岁;可以拿到:200w-350w;


总体的薪水在:24w+25w+90w+200w约350w 到 45w+40w+140w+350w约580w;


第四档:


毕业一般公司,年薪6w-12w;普遍干三年,薪资区间在:18w-36w;


起薪低,一般三年能翻一倍,起薪高的30%-50%涨幅,年薪:12w-20w,干两年能拿:24w-40w;


五年跳槽涨50%,年薪30w-45w,干三年多拿:90w-140w;


同第三档差不多,30岁左右,比较尴尬,能冲一下P7进去也是大头兵,薪资也不会高出很多,年薪比第三档第一点基本30w-50w;假设能稳定干到35岁;可以拿到:150w-250w;


总体的薪水在:18w+24w+90w+150w约280w 到 36w+40w+140w+250w约480w;


薪水总结


按照二八法则来看,普通人对应第三档到第四档,向上突破空间有限,也就是说80%以上的程序员在一线城市工作到35岁最多也就能挣500w左右,所以要找到自己的定位,面对一线城市的房价理性做出选择,而且税后的薪水能存下来的最多也就250w左右,所以一线互联网码农最合适的安家还是二线城市。


image.png


作者:sharloon
来源:juejin.cn/post/7295594732948308006
收起阅读 »

开发者,独立吧!

很多技术人在做了几年一线开发后,都会面临职业生涯上的一个选择——是继续走技术路线?还是转管理路线? 事实上,这个「烦恼」有点肤浅,是把自己置于打工者地位而产生的;当因被裁或公司倒闭等而失业时,你是做技术的还是做管理的,又有何差别? 真正该去选择的是继续做打工者...
继续阅读 »

很多技术人在做了几年一线开发后,都会面临职业生涯上的一个选择——是继续走技术路线?还是转管理路线?


事实上,这个「烦恼」有点肤浅,是把自己置于打工者地位而产生的;当因被裁或公司倒闭等而失业时,你是做技术的还是做管理的,又有何差别?


真正该去选择的是继续做打工者还是「晋级」为创业者?若选择后者的话,对于热爱技术的技术人来说,做个独立开发者是最为适合的!


独立开发者


独立开发者也好,创业者也罢,无论现实层面的处境怎样,更重要的是精神层面的思维方式的转变——精神可以改变并扭曲现实。


就好像一个人要想独立,首先得精神独立,做到在精神、情感上不强烈地依赖他人,其次再谈经济独立——由内而外地去独立。


作为一个普通人,若说与原生家庭进行切割是人生中的第一次独立,那么不与某一家具体的公司绑定就是人生中的第二次独立。


成为独立开发者或其他什么类型的创业者,就是第二次独立的象征。


一般特征


从「独立开发者」这个称谓可以看出,这是一群以软件开发为主要技能,且具备变卖该技能及相关成果的能力的个人。


这就意味着,不能固步自封而要保持开放,不仅专业能力要达到甚至超越一般水准,还要学习并思考如何去做运营、市场营销和商务谈判等。


独立开发者有两样东西是可出售的:用于从事软件开发活动的劳动时间;自己研发的开发套件或其他数字产品。


作为独立开发者,除了用技术手段解决问题所带来的快感之外,也追求利润最大化;因而,单纯出售劳动时间只能是过渡,最终还是得以出售自研产品为重。


同一个独立开发者,在出售劳动时间时,看起来像常规的打工者一样;而当出售自研产品时,则会变得如同商人一般。


出售劳动时间做技术支持的话,雇佣形式可以是全职或兼职,工作方式可以为远程或驻场,较为灵活多变。


心态差异


常会听到有人说在工作中要了解业务,有创业者心态,懂得客户第一什么的——这听起来很「政治正确」,令人感到被 PUA 了。


如果是作为打工者,这话是自己公司或者网上哪个领导说的,那妥妥的是 PUA 无疑,就是一种压榨——既没业务发展决定权,又无公司分红,仅有微薄的工资而已。


但若是独立开发者,对那几个「要求」的理解就不一样了——


给别人打工是将自己的时间和服务作为商品进行出售,公司及其他员工都是自己的客户,无所谓同事、领导,也没有「阶级」之分,一视同仁,皆为合作关系。


自己的职责就是交付符合要求的代码及文档等,若去深入了解业务或公司及其他员工的客户,那也只是为了提高自己的服务质量,以便争取到更多更久的合作。


这才真的是「创业者心态」——所做的一切是为了自己的生意和事业而非他人的,通过成就他人的生意和事业而成就自己的。


基本能力


合格的专业能力自不用说,这是做独立开发者的基本中的基本,总不能用三脚猫的功夫和态度去坑别人吧?


发掘客户


能够有源源不断的合作机会是最重要的,否则独立开发者这条路将不可持续,发掘新的客户就是其中一个途径。


确立自己的个人品牌并借由自媒体塑造相应的形象是非常重要的手段,但不应为了流量去写没啥含金量的文章或拍垃圾视频,而要体现自己是善于独立思考的,以及专业性。


自媒体的主要作用是广撒网,培养「信徒」,再借助互联网与人际网络的力量,将自己的触角延伸到潜在客户那里去。


这是以坚实的专业能力为基础,需要长期用心投入经营的事情——



社区影响力决定的是你能把自己的正确想法放大多少倍出去,这种能力,能争取一定要争取,毛主席教导我们:宣传阵地你不占领,别人就要占领。但扩大社区影响力的同时,一定要牢记,它是个双刃剑,如果没有与之相称的实力,很可能伤到自己。


徐飞《程序员的自我营销》



然而,自媒体上的内容是较为零散琐碎的,常常需要多看些文章或视频才可能把这个人的大致轮廓给勾勒出来。


尤其是对潜在客户这目的性较强的群体而言,想要知道这个独立开发者是否满足合作要求,得花费更多的时间与精力去研究他。


若此时有个专门用于商务的「个人名片」,针对潜在客户提纲挈领地告知他们自己具有哪些优势、是个怎样的人、都提供什么服务等,将会提高合作概率——


「个人名片」网站首页


上图为我的「个人名片」(访问 yaol.in 查看),是最近为走独立开发者这条路而特意重新设计并改版的。


留住客户


从陌生人那里发掘新客户的成本较高,留住已经合作过的客户会好很多;当然,留住客户并不意味着要去无原则地跪舔,而是建立健康的长期合作关系。


在第一次选择合作时,客户是相信了独立开发者所塑造出的形象;这第一次的合作就是验金石,只有在双方都觉得合作愉快时才会有继续合作的机会。


要让对方感到愉快,除了专业能力被认可之外,还得令人觉得靠谱、讲诚信,想客户之所想,急客户之所急——客户第一。


这样一来,不仅多了「回头客」,他们还很有可能会自发地带来新客户,从而逐渐地形成口碑效应。


道理很简单,然而对很多人来说做起来并没那么容易。


总结


要想一直做个技术人,独立开发者是最适合的最终归宿,它更倾向于是一种思维方式,而非实际上是否在某个公司里全职上班。


独立开发者需要借助自媒体去营销自己,并用「个人名片」提高合作概率,以这两者为自己从陌生人中带来客户。


从陌生人中发掘新客户的成本较高,不如以自己过硬的专业能力、诚信的为人及优质的服务赢得「回头客」,没准儿他们还会带来新客户。


本文是以「独立开发者」为主题的文章系列的第一篇,主要从出售劳动时间的角度去阐述,日后会根据实践心得编写更多文章,欢迎关注~


作者:欧雷殿
来源:juejin.cn/post/7297152068782473243
收起阅读 »

程序员的副业发展

前言 之前总有小伙伴问我,现在没有工作,或者想在空闲时间做一些程序员兼职,怎么做,做什么,能赚点外快 因为我之前发别的文章的时候有捎带着说过一嘴我做一些副业,这里就说一下我是怎么做的,都做了什么 希望能对你有些帮助~ 正文 学生单 学生单是我接过最多的,已经写...
继续阅读 »

前言


之前总有小伙伴问我,现在没有工作,或者想在空闲时间做一些程序员兼职,怎么做,做什么,能赚点外快


因为我之前发别的文章的时候有捎带着说过一嘴我做一些副业,这里就说一下我是怎么做的,都做了什么


希望能对你有些帮助~


正文


学生单


学生单是我接过最多的,已经写了100多份毕设,上百份大作业了,这里给大家介绍一下


python这种的数据处理的大作业也很多,但是我个人不太会,所以没结过,我只说我做过的


我大致做过几种单子,最多的是学生的单子,分为大作业单子毕设单子


大作业单一般指一个小作业,比如:



  • 几个web界面(大多是html、css、js)

  • 一个全栈的小demo,大多是jsp+SSM或者vue+springboot,之所以不算是毕设是因为,页面的数目不多,数据库表少,而且后端也很简单


我不知道掘金这里能不能说价格,以防万一我就不说大致价格了,大家想参考价格可以去tb或者咸鱼之类的打听就行


然后最多的就是毕设单子,一般就是一个全栈的项目



  • 最多的是vue+springboot的项目,需求量特别大,这里说一下,之前基本都是vue2的项目,现在很多学校要求vue3了,但是大部分商家vue3的模板很少,所以tb上接vue3的项目要么少,要么很贵,所以我觉得能接vue3和springboot项目的可以打一定的价格战,vue2的市面上价格差不多,模板差不多,不好竞争的

  • 少数vue+node的全栈项目,一般是express或者koa,价格和springboot差不多,但是需求量特别少

  • uni+vue+springboot的项目,其实和vue+springboot项目差不多,因为单纯的vue+springboot项目太多了,所以现在很多人要求做个uni手机端,需求量适中

  • .net项目,信管专业的学生用.net的很多,需求量不少,有会的可以考虑一下


这是我接过的比较多的项目,数据库我没有单说,基本上都是MySQL,然后会要求几张表,以及主从表有几对,这就看客户具体要求了



需要注意的点:大部分你得给客户配环境,跑程序,还是就是毕设一般是要求论文的,有论文的会比单纯程序赚的多,但是一定要注意对方是否要求查重,如果要求查重,一般是不建议接的,一般都是要求维普和知网查重,会要了你的老命。还有需要注意的是,学生单子一般是需要答辩的,你可以选择是否包答辩,当然可以调整价格,但是你一旦包答辩,你的微信在答辩期间就不会停了。你永远不知道他们会有怎样的问题



商业单


商业单有大有小,小的跟毕设差不多,大的需要签合同


我接的单子大致就一种,小程序+后台管理+后端,也就是一个大型的全栈项目,要比学生单复杂,而且你还要打包、部署、上线,售后,有一个周期性,时间也比较长


72761aa2847097aa719f2c9728dc560.jpg


image.png


ff5d9aaae6207ab8cbbe847c73cbd36.jpg


9e157d5ddab294d3214fa1d8ece07dc.jpg


为了防止大家不信,稍微放几个聊天记录,是这两个月来找的,也没有给自己打广告,大家都是开发者,开发个小程序有什么打广告,可吹的(真的是被杠怕了)


技术栈有两种情况:自己定客户定


UI也有两种情况:有设计图的无设计图的(也就是自己设计)


基本上也就是两种客户:懂技术的客户,不懂技术的客户


指定技术栈的我就不说了,对于不指定技术栈的我大致分为两种



  • 小程序端:uni/小程序原生、后台:vue、后端:云开发

  • 小程序端:uni/小程序原生、后台:vue、后端:springboot


这取决于预算,预算高的就用springboot、预算不高的就云开发一把嗦,需要说的是并不是说云开发差,其实现在云开发已经满足绝大部分的需求,很完善了,而springboot则是应用广泛,客户后期找别人接手更方便


对于没有UI设计图的,我会选择去各种设计网站去找一些灵感


当项目达到一定金额,会签署合同,预付定金,这是对双方的一种保障


其实在整个项目中比较费劲的是沟通,不是单独说与客户的沟通,更多的是三方沟通,作为上线的程序,需要一些资料手续,这样就需要三方沟通,同时还有一定的周期,可能会被催


讲解单


当然,有的时候人家是有程序的,可能是别人代写的,可能是从开源扒下来的,这个时候客户有程序,但是看不懂,他们可能需要答辩,所以会花钱找人给他们梳理一下,讲一讲, 这种情况比较简单,因为不需要你去写代码,但是需要你能看懂别人的代码


这种情况不在少数,尤其是在小红书这种单子特别多,来钱快,我一般是按照小时收费


cb519bce3fedc451116b659f6cb7388.jpg


e4531c4d8d6527208a03e1dcc6ede32.jpg


aef2baeabe8859caac59fd7ae0b456c.jpg


知识付费这东西很有意思,有时候你回答别人的一些问题,对方也会象征性地给你个几十的红包


接单渠道


我觉得相对于什么单,大家更在意的是怎么接单,很多人都接不到单,这才是最难受的


其实对此我个人并没有太好的建议的方法,我认为最重要的,还是你的交际能力,你在现实中不善于交际,网络上也不善于交际,那就很难了


因为我之前是在学校,在校期间干过一些兼职,所以认识的同学比较多,同时自身能力还可以,所以会有很多人来找,然后做完之后,熟人之间会慢慢介绍,人就越来越多,所以我不太担心能否接单这件事,反而是单太多,自己甚至成立一个小型工作室去接单


如果你是学生的话,一定要在学校积累客户,这样会越来越多,哪怕是现在我还看到学校的各种群天天有毕业很多年以及社会人士来打广告呢,你为什么就不可以呢


当然但是很多人现在已经不是学生了,也不知道怎么接触学生,那么我给大家推荐另外的道路



  • 闲鱼接单

  • 小红书接单


大部分学生找的写手都会比较贵,这种情况下,很多学生都会选择去上面的两个平台去货比三家,那么你的机会就来了


有人说不行啊,这种平台发接单帖子就被删了,那么你就想,为什么那么多人没被删,我也没被删,为什么你被删除了


其次是我最不推荐的一种接单方式:tb写手


为什么不推荐呢,其实就是tb去接单,然后会在tb写手群外包给写手,也就是tb在赚你的差价


这种感觉很难受,而且赚的不多,但是如果你找不到别的渠道,也可以尝试一下


最后


我只是分享一下自己接单的方式,但是说实在的,接一个毕设单或者是商业单其实挺累的,不是说技术层面的,更多的是心累,大家自行体会吧,而且现在商场内卷严重,甚至有人200、300就一个小程序。。。


所以大家要想,走什么渠道,拿什么竞争


另外,像什么猪八戒这种的外包项目的网站,我只是见过,但是没实际用过,接过,所以不好评价


希望大家赚钱顺利,私单是一种赚钱的方式,但是是不稳定的,一定还是要以自己本身的工作为主,自行判断~


作者:Shaka
来源:juejin.cn/post/7297124052174848036
收起阅读 »

听说前端出大事儿了

web
最近这两天,在前端圈最火的图片莫过于下面这张了。 这是一段 React 代码,就算你完全没用过 React 也没关系,一眼看过去就能看到其中最敏感的一句代码,就是那句 SQL 。 咱们把这端代码简化一下,大概就是下面这个样子。

最近这两天,在前端圈最火的图片莫过于下面这张了。



这是一段 React 代码,就算你完全没用过 React 也没关系,一眼看过去就能看到其中最敏感的一句代码,就是那句 SQL 。
咱们把这端代码简化一下,大概就是下面这个样子。




意思就是在页面上点击一个叫做「提交」的按钮,触发一个 formAction(提交表单)的动作。这有点看到了当年 JSP 和 PHP 的味道了。这还不是最神奇的,最厉害的是提交表单要执行的动作不是一个接口请求,而是直接执行一条 SQL 。使用 use server标签,标示这是一个服务端端执行的方法。



一时间竟分不出这到底是前端还是后端了。


这么发展下去,React 就是妥妥的全栈语言了。此时的 PHP 在旁边笑而不语,还说我不是世界上最好的语言,你们终究还是会活成我的样子。



自从前后端分离以来,前端框架可谓是百花齐放,一片繁荣。最早的是 Angular,然后就是 React 和 Vue,到现在基本都是 Vue 和 React 的天下了。


如果你用过原生的 JavaScript 或者 JQuery,那就能感受到 React 或者 Vue 的出现,完全改变了前端的开发方式。


React 目前的最新版本是 18,支持 ES(ECMAScript) 和TS(TypeScript),除了画界面和写CSS之外,完全可以把它当做一个面向对象的语言工具使用。


这次支持执行执行后端 SQL 的特性是 Next.js 开放的,Next.js 是 在React 框架上再次高度封装的一个框架。有点像 Spring Boot与 Spring 的关系,Spring 好比是 React,Spring Boot 就是 Next.js。


本来好好的前端,为什么要直接支持写 SQL 呢,这也并不是无迹可寻的。前两年,React 就推出了React Server Components 。大致的意思就是说这是一种服务器端组件,为了提高性能,由服务器直接渲染,渲染出来的结果通过元数据的形式发给前端 React,React 拿到元数据后与现有的 UI 树合并,最终由浏览器渲染。


React 官方是大力推荐 Next.js 的,有了官方推荐加上本身已经支持的服务器端组件,Next.js 不知道是出于什么样的目的,竟然直接支持执行服务端方法了。之前要通过 HTTP 请求,现在直接就跳过这一步了。


说实话,站在一个前端框架的视角上,加上我本身是一个后端开发,我是有一点看不懂这个操作了。服务端组件还能理解,毕竟开发效率和性能要兼顾,这无可厚非。


但是直接支持服务端执行,是技术的轮回(照着PHP的方向)还是技术的变革呢,此时的 Next.js 就像是一个站在十字路口的汽车,油都加满了,就看各位开发者驾驶员开着它往哪边走了。


反正依我看来,我是觉得前端框架越简单越好。原因很简单,搞这么复杂,我都快不会用了。



不光是我看不懂,毕竟咱是个后端外行,不是专业的。但是前端同学也是一片调侃,调侃的大致意思就是 React Next.js 啥都能干,既然连后端都能整了,那其他的也能全栈了。


比如有人调侃给 Next.js 赋能 AI,使用 use ai,直接 prompt 编程了。



还有赋能 k8s 的



以及赋能二进制编程的



最厉害的,还有赋能删库跑路的。



调侃归调侃,既然口子已经开了,就会有过来吃螃蟹的人,至于之后会变成什么样子,只能拭目以待了。


作者:古时的风筝
来源:juejin.cn/post/7296384298902929417

挖掘潜力 拥抱挑战 第二届OpenHarmony技术大会OS内核及视窗分论坛召开

2023年11月4日,第二届开放原子开源基金会OpenHarmony技术大会在北京成功举办,本届大会的主题为“技术筑生态,智联赢未来”,旨在通过技术手段,为全场景、全连接、全智能时代,基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的...
继续阅读 »

2023年11月4日,第二届开放原子开源基金会OpenHarmony技术大会在北京成功举办,本届大会的主题为“技术筑生态,智联赢未来”,旨在通过技术手段,为全场景、全连接、全智能时代,基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。

作为本届大会最为引人瞩目的分论坛之一,“OS内核及视窗分论坛”在10位顶尖的专家、教授、研究员的深入探讨中圆满落幕。论坛的核心议题围绕如何深度挖掘操作系统内核及视窗的潜能,以更好地助力客户和用户拥抱多元化的行业挑战。 为了实现这一目标,OpenHarmony始终积极寻求系统内核及视窗的优化和迭代,全面降低系统负载,高效构建系统壁垒,更需秉持开放的态度,广纳开发者创新思维,多方聆听领袖的意见,以促进多方合作和共同进步。

为此,OS内核及视窗分论坛特邀了OpenHarmony技术指导委员会委员、华为OS内核实验室主任贾宁和华为操作系统技术专家、菲尔兹实验室主任付明担任出品人,同时邀请深开鸿未来研究院基础架构首席研究员李宝,清华大学计算机系副教授陆游游,上海交通大学助理研究员杜东,华为OS内核技术专家魏勇军,华为终端图形图像资深技术专家黄然,湖南开鸿智谷数字产业发展有限公司副总裁李传钊,厦门大学信息学院教授陈中贵,华为OS视窗架构师杨程云,中国科学院软件研究所开源基础设施组负责人、高级工程师郑森文以及北京航空航天大学虚拟现实技术与系统国家重点实验室副教授高阳10位专家学者共同参与,带来了极具价值的技术内容分享。

出品人:OpenHarmony技术指导委员会委员、华为OS内核实验室主任贾宁

出品人:华为操作系统技术专家、菲尔兹实验室主任付明

深开鸿未来研究院基础架构首席研究员李宝指出,深开鸿通过混合部署技术为OpenHarmony引入实时特性,将OpenHarmony应用到更多关键行业,保障关键任务的实时运行。OpenHarmony 致力于成为万物互联时代的技术底座,深开鸿基于OpenHarmony结合虚拟化技术打造面向更多关键行业的操作系统KaihongOS,在为各行业提供分布式、AI、UI框架等特性的同时也提供了机器人、工业控制等场景需要的实时计算环境,并积极探索基于KaihongOS搭建便捷高效的混合部署统一编程基础设施,构建千行百业的统一数字底座。

深开鸿未来研究院基础架构首席研究员李宝主题分享

清华大学计算机系副教授陆游游对软硬件协同的闪存存储系统进行了深入的探讨。闪存为存储系统带来了颠覆性的革新,清华大学的研究团队在闪存新架构、并发一致性和近数据计算等方向都取得突破性的研究成果,包括软件存储系统架构的变革、I/O并行架构的创新和新型近数据计算I/O栈。这些研究成果为闪存存储系统带来了巨大的性能提升。

清华大学计算机系副教授陆游游主题分享

上海交通大学助理研究员杜东《低时延分布式共享设备研究探索》的报告提出了面向万物智联的应用场景和生态,突破异构分布式操作系统前沿挑战的解决方案。分布式设备共享与协同是万物智联场景操作系统下的重要能力,相比传统分布式共享内存(DSM)等经典技术,分布式设备面临的设备异构、操作系统的驱动框架耦合等挑战。为了应对这些挑战,杜老师提出了用户态框架层的分布式(远端)设备、内核态用户态混合分布式设备、基于新型硬件的设备池等架构方案,这些方案在易用性、兼容性和性能等多方面都取得了一定的突破。

上海交通大学助理研究员杜东主题分享

作为华为OS内核的技术专家,魏勇军带来了《极致性能打造持续流畅的基础内核底座》的主题分享,详细阐述了作为高性能底座的OpenHarmony内核是如何构建Qos智能感知调度、swap内存管理增强、F2FS以及分布式文件系统等核心特性,同时为关键业务提供资源的优先供给,助力OpenHarmony持续构筑流畅低功耗和极致性能的竞争力。

华为OS内核技术专家魏勇军主题分享

华为终端图形图像资深技术专家黄然先生认为,游戏生态是用户对操作系统性能衡量的最直接方式,对终端操作系统至关重要。他介绍了主流游戏的三大引擎Cocos、Unity、Unreal对于游戏而言的重要性,与大家同步了OpenHarmony与游戏引擎厂商合作在游戏垂类生态构筑情况和取得的一些阶段性成果。并号召社区力量更多参与共建游戏生态,共同打造OpenHarmony的游戏体验竞争力。

华为终端图形图像资深技术专家黄然主题分享

湖南开鸿智谷数字产业发展有限公司副总裁李传钊先生,通过结合OpenHarmony商业项目中的真实案例实践经验,分享了OpenHarmony在UI/UX层面创新性突破。他介绍了OpenHarmony在交通行业、教育行业应用中UI/UX的创新技术特点和使用成果,充分帮助开发者和技术同仁了解OpenHarmony商业落地时跨设备UI过程中的实际的问题,以及彻底解决定制化在OpenHarmony UI体验上的痛点的实践。

湖南开鸿智谷数字产业发展有限公司副总裁李传钊主题分享

厦门大学信息学院教授陈中贵老师认为,在OpenHarmony的世界里,视窗图形技术可以创造无限可能,为世界带来更多美好的变化。在OS视窗领域中,矢量图形技术非常重要,它可以利用基于数学方程表达的几何图元来描述图像,相比于位图它具有分辨率无关、文件小、易于编辑和打印质量高等优点,从而广泛应用于文字、图标和图形设计等。然而矢量图的绘制相对复杂,在显示前需要经过一个光栅化的过程,而在一般的视窗系统中通常包含大量的矢量图形,所以矢量图形的绘制效率对视窗效率影响非常大。高质量高效率的矢量图形绘制是OS视窗重点关键技术之一。

厦门大学信息学院教授陈中贵主题分享

除了对矢量图形绘制等视窗关键技术的探讨,华为OS视窗架构师杨程云博士还带来了《2D绘制引擎发展及演进趋势》的分享。在该分享中,我们看到支撑操作系统图形用户界面的基础底座的2D绘制引擎技术发演进轨迹,以及其性能及效率表现对GUI应用交互体验的巨大影响。近年来随着终端设备显示分辨率、屏幕刷新率的持续提升以及应用对UI界面高画质的不断追求,2D绘制引擎面临的性能挑战日益凸显。因此,通过构建下一代2D绘制引擎来解决当前挑战是迫在眉睫的工作。

华为OS视窗架构师杨程云主题分享

中国科学院软件研究所开源基础设施组负责人、高级工程师郑森文老师认为,视窗决定了各类应用程序显示的性能、质量以及图形的响应和反馈,是用户操作和交互中首先能直观感受到的重要环节。尤其是在当前这样一个交互技术极速发展、智联场景多样化、定制化的时代背景下,OpenHarmony视窗系统有机会通过开放式创新,进一步加强了用户的交互体验,成为新一代智联交互显示应用的抓手。

中国科学院软件研究所开源基础设施组负责人、高级工程师郑森文主题分享

最后,北京航空航天大学虚拟现实技术与系统国家重点实验室副教授高阳老师,在分享中谈到虚拟现实领域的OS视窗。高阳老师认为高可置信物理仿真与虚实人机交互是支撑实时场景建模与新型人机交互技术的算法核心,无论是增强现实还是元宇宙等应用,都需要实现高效动态场景模拟与智能人机交互。虚实融合物理仿真与人机交互将对相关物理模型、AI技术融合、数值算法优化关键技术等进行深入研究,为新型虚实融合空间操作系统的高效可视内容呈现方法和智能人机交互模式研究提供了新的思路。

北京航空航天大学虚拟现实技术与系统国家重点实验室副教授高阳主题分享

随着10位专家学者的精彩分享,第二届OpenHarmony技术大会“OS内核及视窗分论坛”完美收官,与此同时,我们也清晰的看到了操作系统内核与视窗的发展路径,深入洞察了未来的发展规划。未来,在多方的协同下,OpenHarmony将攻克技术难关,同时构建技术壁垒,助力分布式全场景协同的开源操作系统底座与生态系统蓬勃发展。

收起阅读 »

OpenHarmony技术俱乐部亮相第二届OpenHarmony技术大会, 揭榜课题科研共建OpenHarmony

11月4日,以“技术筑生态,智联赢未来”为主题的第二届开放原子开源基金会OpenHarmony技术大会在北京圆满召开,汇聚了技术领袖、权威专家与高校师生共同探讨操作系统未来技术发展方向。其中,17所OpenHarmony技术俱乐部高校成员作为未来技术生力军,在...
继续阅读 »

11月4日,以“技术筑生态,智联赢未来”为主题的第二届开放原子开源基金会OpenHarmony技术大会在北京圆满召开,汇聚了技术领袖、权威专家与高校师生共同探讨操作系统未来技术发展方向。其中,17所OpenHarmony技术俱乐部高校成员作为未来技术生力军,在大会主论坛共绘璀璨星图。本次大会还重磅发布了2023 OpenHarmony年度课题,并由OpenHarmony技术俱乐部专家学者共同揭晓。希望通过OpenHarmony技术俱乐部的不断建设和壮大,能够引领操作系统领域的产学研互动,并吸引更多的高校师生参与OpenHarmony的科研共建工作。

17所高校加入技术俱乐部,开源人才储备日益雄厚

OpenHarmony是由开放原子开源基金会(OpenAtom Foundation)孵化及运营的开源项目,目标是面向全场景、全连接、全智能时代、基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。自诞生之初,OpenHarmony就遵循着“共建、共治、共享”的原则,坚持“生态统一、开放共赢”的理念,积极投入OpenHarmony的技术生态与人才生态共建中。

在人才方面,OpenHarmony时刻关注人才和开发者培养,通过产教融合实践,为生态发展注入新动力。OpenHarmony不仅为院校提供全流程服务,支持产业人才培养闭环,助力院校专业建设,还正式发布开发者论坛及开发者成长计划,为开发者提供全方位的赋能支持和帮助。此外,早在2022年7月,OpenHarmony发起了“技术俱乐部计划”,进一步推动产学研合作,加速技术创新和应用。

OpenHarmony“技术俱乐部计划”本着“开展创新工作,成就一流人才”的宗旨,鼓励广大高校师生积极参与OpenHarmony技术研究,承接相关科研课题,展开创新探索;同时,OpenHarmony还通过举办直播课、城市技术论坛等系列线上线下活动,加强社区技术专家与高校师生的互动交流。此外,OpenHarmony技术俱乐部还会定期组织各种形式的技术竞赛、挑战赛,在全国范围内识别优秀创新项目与创新人才。通过这一计划,OpenHarmony邀请专家走进校园开展指导与交流,让师生走进OpenHarmony生态并获得成长,从而全面建设起OpenHarmony社区与高校的桥梁,繁荣OpenHarmony学术生态,打造开源生态与高校结合的标杆。

截至目前,已有上海交通大学、北京理工大学、大连理工大学、兰州大学、武汉大学、东南大学、复旦大学、北京邮电大学、重庆大学、西安交通大学、湖南大学、华中科技大学、浙江大学、西安电子科技大学、南开大学、北京航空航天大学、电子科技大学共17所高校加入OpenHarmony技术俱乐部,为OpenHarmony技术与生态发展储备了坚实的人才基础。

OpenHarmony技术俱乐部高校成员

高校技术俱乐部齐亮相,人才生态日益繁荣

11月4日,来自这17所高校的OpenHarmony技术俱乐部代表也亮相第二届OpenHarmony技术大会现场,并参与了OpenHarmony技术俱乐部发起的“璀璨星空”点亮仪式。现场,OpenHarmony技术俱乐部主任和代表们一起放置“星形灯“,霎时间一颗颗代表OpenHarmony技术俱乐部成员的“星星”接连亮起,汇聚成一片璀璨星空。这寓意着操作系统生态的未来主力军将如同“繁星点点”,通过对创新技术的不懈追求与钻研,以恰同学少年的昂扬姿态,推动下一代终端操作系统社区的发展与繁荣。

OpenHarmony技术俱乐部“星空璀璨”点亮仪式

随后,OpenHarmony项目群技术指导委员会主席陈海波和各高校院级领导,共同发布了OpenHarmony年度课题。随着现场环状灯带亮起并向中间汇聚,“科研共建OpenHarmony”美好寓意跃然眼前,年度课题也一并揭晓。随后,现场的高校教师代表则逐一揭榜各自的意向年度课题,他们将以这些课题为方向,在智能终端操作系统领域展开深入研究。

2023 OpenHarmony年度课题发布仪式

今年4月以来,OpenHarmony已陆续在上海、武汉、长沙、北京和张掖等城市举办了OpenHarmony城市技术论坛,与学术界和产业界技术领袖共同探讨OpenHarmony的未来技术趋势。在OpenHarmony技术俱乐部的点亮仪式环节结束后,大会主论坛还特别举办了OpenHarmony城市技术论坛致谢环节,向上海、武汉、长沙、北京、张掖五个城市技术论坛的出品人及嘉宾致以诚挚的谢意。

OpenHarmony向城市技术论坛出品人颁发致谢纪念

根深才能叶茂,众行方能致远。OpenHarmony在技术人才生态领域的共建举措,已吸引越来越多的高校师生加入到社区生态中。在OpenHarmony技术俱乐部之外,OpenHarmony也吸引了社区内大量技术专家与贡献者的加入。当前,OpenHarmony已有6200+共建者、51家共建单位,贡献代码行数超过1亿行。其中2023年新增代码超过900万行,10家单位志愿成为2023年度“百人代码贡献单位”,OpenHarmony社区已成为下一代智能终端操作系统根社区。也期待未来有越来越多的高等院校师生和技术专家加入OpenHarmony生态,为生态的构建、发展与繁荣注入新动力。

收起阅读 »

首届OpenHarmony竞赛训练营结营颁奖,75所高校学子助力建设开源生态

由OpenAtomOpenHarmony(以下简称“OpenHarmony”)项目群工作委员会和OpenHarmony项目群技术指导委员会主办的首届OpenHarmony竞赛训练营,历时2个月,吸引了来自上海交通大学、复旦大学、电子科技大学、湖南大学、北京理工...
继续阅读 »

由OpenAtomOpenHarmony(以下简称“OpenHarmony”)项目群工作委员会和OpenHarmony项目群技术指导委员会主办的首届OpenHarmony竞赛训练营,历时2个月,吸引了来自上海交通大学、复旦大学、电子科技大学、湖南大学、北京理工大学、四川大学、华中科技大学、中北大学等75个赛队共200+学生报名参与,其中重点本科学院覆盖85%,11月3日经过一天激烈的决赛角逐共有7个赛队脱颖而出。

在11月4日“技术筑生态,智联赢未来”第二届开放原子开源基金会OpenHarmony技术大会上,OpenHarmony项目群工作委员会和OpenHarmony项目群技术指导委员会的专家出席仪式并为12个获奖团队颁奖。

OpenHarmony竞赛训练营合影

构筑开源未来,培育学生英才

OpenHarmony是由开放原子开源基金会(OpenAtomFoundation)孵化及运营的开源项目,目标是面向全场景、全连接、全智能时代、基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。行业创新,人才先行。为培养更多应用型人才和产业需求有效链接,吸引更多的高校师生参与到OpenHarmony的开发和应用中,今年OpenHarmony项目群工作委员会和OpenHarmony项目群技术指导委员会联合创新推出首届OpenHarmony竞赛训练营。训练营以实战竞赛+赋能培训的模式,帮助高校师生充分掌握并利用OpenHarmony进而实现行业需求和三方库补齐,推动OpenHarmony生态系统的建设和发展,促进技术创新和知识共享,为行业发展注入新的活力和动力。

本届OpenHarmony竞赛训练营分为报名、作品开发、赋能培训、提交作品、作品公示、决赛等环节。其中,赋能培训环节包含技术赋能、线上直播培训、线下核心城市路演培训。主办方邀请了OpenHarmony行业使能专家、三方库领域专家、TSC专家和高校老师作为技术指导和评委,为参赛者提供专业的技术指导、培训、项目评审,以提高其作品的质量和竞争力。

此外,OpenHarmony竞赛训练营鼓励参赛者在作品开发过程中积极参与OpenHarmony开源社区和拉瓦尔社区,利用社区丰富的文档、代码、开发工具等资源优化作品。同时训练营配备了专门的社区小助手为参赛选手解决问题,以积极互动问答的方式和开源合作的精神促进OpenHarmony生态系统的健康发展。

动手实践成果初现,开源英才未来可期

以终为始,着眼实际。训练营旨在鼓励高校人才以OpenHarmony为技术底座去解决更加具体的问题,此次获奖的团队作品涌现出很多创新亮点,充分体现了开源生态蓬勃共建的良好势头和巨大潜力。

一等奖花落选择行业使能赛队的华中科技大学“名称暂定队”赛队,他们提交的病床巡检终端作品,能够更快速、方便地获取病床上病人的关键数据,从而提升巡检效率,帮助实现医疗行业需求,为OpenHarmony行业生态系统的建设和发展作出了突出贡献。

金陵科技学院“我是个rapper”赛队、上海交通大学“Almony”赛队荣获二等奖。作为本次竞赛的黑马,“我是个rapper”赛队提交的参赛作品是一款轻量的图表绘制组件库,提供了丰富的图表类型和灵活的定制选项,使用户能够轻松地创建出精美、直观的图表,可以增强OpenHarmony在数据可视化方面的能力。

在本次比赛中,荣获三等奖的队伍分别是:华中科技大学的“宵宫世界第一”赛队、“阁下又将如何应队”赛队以及“1024队”。而上海交通大学的“Almony”赛队则获得了创新奖。获得优秀奖的队伍分别是:西安交通大学的“西安交通大学2队”、武汉大学的“小乖乖”、大连理工大学的“6舍622队”以及“男2舍708”、兰州大学的“咕咕队伍”。

值得一提的是,本次OpenHarmony竞赛训练营三方库赛道的多个作品达到了合入OpenHarmony社区主干的要求,完成门禁审核后,将提供给所有开发者访问和使用。

OpenHarmony项目群技术指导委员会主席陈海波为获奖赛队颁发一等奖

OpenHarmony项目群技术指导委员会委员武延军

OpenHarmony项目管理委员会主席任革林颁发二等奖

OpenHarmony项目群技术指导委员会委员贾宁

OpenHarmony项目管理委员会委员李锋颁发三等奖

OpenHarmony项目群技术指导委员会委员臧斌宇为获奖赛队颁发创新奖

OpenHarmony项目群技术指导委员会委员张兆生

OpenHarmony知识体系组组长王治文颁发优秀奖

我们正迎来最好的万物智联时代,同时也伴随着诸多挑战。躬身入局、合力共建将是智赢未来的关键所在。未来OpenHarmony竞赛训练营将带来更多领域的命题和更强大多元的技术指导和支持,多方位为高校师生赋能,吸引更多OpenHarmony参与者、贡献者、共创者,构筑开源生态美好未来。

收起阅读 »

构建系统安全“堡垒” OpenHarmony技术大会OS安全分论坛意义深远

随时工业4.0时代的到来,数据已成为企业数字化、智能化发展的基石。然而,面对呈爆发式增长的数据量,如何确保系统安全、防止数据被窃取,已成为全球用户关注的焦点。在这样的背景下,2023年11月4日举办的第二届开放原子开源基金会OpenHarmony技术大会OS安...
继续阅读 »

随时工业4.0时代的到来,数据已成为企业数字化、智能化发展的基石。然而,面对呈爆发式增长的数据量,如何确保系统安全、防止数据被窃取,已成为全球用户关注的焦点。在这样的背景下,2023年11月4日举办的第二届开放原子开源基金会OpenHarmony技术大会OS安全分论坛上,众多专家学者就系统安全、数据安全、大模型安全以及软件安全分析等方向展开了热烈探讨,并且针对OS安全所关注的前沿技术和实践应用提出了建设性意见,一同为OpenHarmony提倡的万物智联筑起了坚实的安全保障。华为副首席科学家、终端BG/车BU首席安全架构师付天福担任论坛出品人。

论坛汇聚了中国科学院信息工程研究所二级研究员、副总师、“百人计划学者”李凤华,上海交通大学教授、OpenHarmony技术俱乐部主任夏虞斌,华为系统安全实验室主任、系统安全首席专家谭寅,清华大学网络研究院副院长、副教授、博士生导师张超(临时时间冲突委托朱文宇博士),OpenHarmony安委会产业安全使能与标准化工作组副组长翟世俊,华为OS内核安全专家、形式化验证助理科学家李屹,北京中科微澜科技有限公司CEO杨牧天,北京邮电大学副教授、博士生导师梁洪亮,中国科学院软件研究所特别研究助理凌祥等十位技术学者,共同探讨了OpenHarmony安全的前沿技术和未来发展方向。

OS安全分论坛现场

首先,中国科学院信息工程研究所二级研究员、副总师、“百人计划学者”李凤华在《数据要素流通的安全挑战与对策》分享中介绍了万物智慧互联是推动数据广泛传播的关键驱动力,促进了数据泛在共享,使得数字经济高度依赖于数据要素的流通。然而,随着数据从传统的共享模式演变为要素流通,现有的安全技术无法满足数据多轮交易的安全需求,因此急需构建一套全面的解决方案,以确保数据要素流通的安全性。李老师提出隐私计算整体的一套理论体系和技术方案,正确利用不同技术解决泛在共享环境中不同环节的数据安全与隐私保护问题。

付天福补充说明,OpenHarmony原生提供了全套数据生命周期的创建、存储、流通、使用和销毁的安全机制,为数据成为数字经济最重要的生产要素,提供了基础保障能力。

中国科学院信息工程研究所二级研究员、副总师、“百人计划学者”李凤华主题分享

随后,华为副首席科学家、终端BG/车BU首席安全架构师付天福谈到,安全是OpenHarmony内嵌在基因里的核心架构,为了应对大数据时代的安全挑战, OpenHarmony 提供了对数据整个生命周期的标记、加密、隔离、访问控制等全套机制,为数据在虚拟世界建立了一套完备的保障措施。他表示,在智能汽车时代,安全是最为核心的竞争力,安全是最大的豪华。智能辅助驾驶安全已经成为OpenHarmony赋能汽车行业的独特优势,而网络安全对功能安全的影响也日益增大,OpenHarmony基于分级安全架构,天生就适合智能汽车这样的分布式计算机体系,充分发挥了强安全部件的能力为整车提供保护,为智能汽车提供了网络安全上的领先解决方案。

华为副首席科学家、终端BG/车BU首席安全架构师付天福主题分享

上海交通大学教授、OpenHarmony技术俱乐部主任夏虞斌在报告中提出了“智能终端操作系统的个人数据处理与保护”的新思路。该方法利用人工智能和机密计算等新技术,实现对个人数据的存储、使用和保护。他指出,智能终端作为个人的数字世界主要入口,记录了大量的个人数据,合理利用这些数据将带来效率的极大提升。通过提供一个端云一体化的机密计算能力,既满足了AI时代对数据驱动模型下的智能化广泛应用,又保障了个人隐私不泄露企业数据不泄密,OpenHarmony有望成为大模型的入口,为个人数据的安全和隐私提供更强大的保障。

上海交通大学教授、OpenHarmony技术俱乐部主任夏虞斌主题分享

华为系统安全实验室主任、系统安全首席专家谭寅在报告中提出了面向OpenHarmony的软硬协同系统安全底座的构筑方法。当前操作系统安全技术已经取得了长足的进展,但随着软件功能复杂性和体量急剧提升,攻击面也持续扩大。即使采用了大量的系统安全防护方法,还是避免不了高危漏洞被利用并最终导致系统被攻破的情况。在此背景下,面向OpenHarmony体系化构筑安全底座,突破关键安全技术,从机制上打造OpenHarmony的安全架构就尤为重要。本报告抛砖引玉介绍当前在OpenHarmony安全防护的工作,以期促进系统安全领域学者与研究人员共同构筑OpenHarmony安全。

华为系统安全实验室主任、系统安全首席专家谭寅主题分享

清华大学网络研究院副院长、副教授、博士生导师张超老由于临时时间冲突,委托博士生朱文宇博士代为分享了题为《闭源软件安全智能化分析》的主题报告,深入探讨了在安全对抗场景下闭源软件分析的挑战,并指出了传统分析技术的局限性。他强调,提取和恢复闭源程序的结构和语义信息对分析其安全性具有重要意义,但由于编译过程中的信息损失,二进制程序分析相较于源码分析更具挑战性。令人欣喜的是,张超分享了团队近期的研究成果,展示了流行的大语言模型在二进制程序分析中的显著效果。这一创新方法为闭源软件安全智能化分析提供了新的思路和解决方案,为未来的软件安全研究开辟了广阔的前景。

清华大学网络研究院副院长、副教授、博士生导师张超委托朱文宇博士主题分享

OpenHarmony安委会产业安全使能与标准化工作组副组长翟世俊对OpenHarmony发行版证通电子的LightBeeOS获得CCRCEAL4+认证做了介绍,介绍了中国网络安全审查技术与认证中心(CCRC)评估保障级(EAL)认证情况,CCRCEAL4+认证对操作系统的安全提出了非常高的要求,需要对系统的安全架构进行全面严格的审核。首先对LightBeeOS的安全功能进行全面测评,并结合详细设计、源码审计、渗透测试、现场审核等对LightBeeOS安全进行全面评估,证明LightBeeOS以及OpenHarmony具有较高的安全水平,为万物互联期待提供了一套坚实的底座,生态厂商可基于OpenHarmony开发更多高安应用场景的发行版OS,聚焦业务场景,减少对安全的巨大投入。

OpenHarmony安委会产业安全使能与标准化工作组副组长 翟世俊 主题分享

本次论坛同时也邀请到了证通电子公司董事、安全支付事业部总经理、OpenHarmony研究院院长程胜春先生,程总表示:拿通用的整套操作系统,而且是国产自主的基于OpenHarmony的操作系统进行这么高级别的安全认证我们是第一次,这次LightBeeOS通过EAL4+认证,我们感到非常开心也深受鼓舞,非常感谢在整个认证过程当中,各位专家老师们给予证通电子的宝贵意见与帮助。证通电子LightBeeOS通过Eal4+认证,首先证明我们OpenHarmony数字底座本身的具有基础的安全能力;其次,融合证通在金融安全上的安全组件等安全技术优势,整体上让这样的一个发行版操作系统信息安全保障能力达到了较高水平。

证通电子董事、安全支付事业部总经理程胜春先生和付天福、翟世俊博士合影

在《操作系统视角看大模型安全挑战》的主题演讲中,华为OS内核安全专家、形式化验证助理科学家李屹指出,操作系统是软件栈的底座,操作系统的安全性在各种场景都具有举足轻重的意义。随着大模型等AI应用的广泛兴起,数据安全正在面临前所未有的挑战。所以,他认为行业有必要站在操作系统的视角,来观察我们将如何应对这些挑战。李博提出了以隔离、跟踪、协同一体化的思路,希望以操作系统为底座,以数据安全为第一原则,逐步构建可信的原生智能,为大模型时代构建坚实的数据和系统安全底座。

华为OS内核安全专家、形式化验证助理科学家李屹主题分享

北京中科微澜科技有限公司CEO杨牧天谈到了建设安全可靠的OpenHarmony生态软件仓库的重要性。他认为,尽管OpenHarmony生态的软件数量众多,但如何确保它们的安全性仍然是一个关键问题。因此,他介绍了OHPM在OpenHarmony生态中三方软件管理方面的完善安全运营机制,旨在预防和处理开源三方库常见的风险,保障OpenHarmony生态的安全可靠发展。

北京中科微澜科技有限公司CEO杨牧天主题分享

北京邮电大学副教授、博士生导师梁洪亮在《软件缺陷漏洞分析》的主题演讲中分享了他的深刻见解。他认为,软件缺陷或漏洞广泛存在,攻击者可以利用它们窃取信息或控制系统。因此,在部署之前,检测和分析软件中的缺陷或漏洞至关重要。鉴于传统测试方法效率的局限,梁老师提出了一种序列引导的多目标混合模糊测试的方案LeoFuzz,通过创新的技术手段,大大提升了软件系统的缺陷的发现效率和准确率。为开源社区软件安全分析提供了一种高效的解决思路,为安全的OpenHarmony生态安全助力。

北京邮电大学副教授、博士生导师梁洪亮主题分享

中国科学院软件研究所特别研究助理凌祥在主题演讲中强调了正确使用OpenHarmony等开源操作系统的API对系统稳定性和安全性的重要性。由于API更新频繁且使用复杂,开发者容易发生误用,导致系统安全漏洞。因此,他介绍了一种基于自动挖掘API路径模式的API误用缺陷检测方法APP-Miner,此方法在针对开源操作系统和大型软件系统的缺陷检测中发挥了显著效果,发现了隐藏很深的多个Linux内核API误用缺陷。希望类似的创新方案能为社区安全治理助力,共同推动开源软件社区安全发展。

中国科学院软件研究所特别研究助理凌祥主题分享

随着万物智联时代的到来,设备联网数量不断增加,技术发展带来的大模型的机遇,随之而来的安全和隐私问题也日益突出。大量的数据将被传输和存储,包括敏感数据如医疗记录、银行信息和公司机密。这些数据的泄露可能会导致巨大的经济损失和信任危机。因此,OpenHarmony将充分发挥其操作系统的安全优势,从数据安全、系统安全、AI安全、软件安全分析等各个维度,打造坚固的“堡垒”,构建分布式全场景协同的开源操作系统底座与生态系统,以保障万物智联产业的繁荣发展。

收起阅读 »

Coremail重磅发布2023年Q3企业邮箱安全性报告

10月25日,Coremail邮件安全联合北京中睿天下信息技术有限公司发布《2023年第三季度企业邮箱安全性研究报告》。2023年第三季度企业邮箱安全呈现出何种态势?作为邮箱管理员,我们又该如何做好防护?一、国内垃圾邮件激增,环比增长31.63%根据Corem...
继续阅读 »

10月25日,Coremail邮件安全联合北京中睿天下信息技术有限公司发布2023年第三季度企业邮箱安全性研究报告》。2023年第三季度企业邮箱安全呈现出何种态势?作为邮箱管理员,我们又该如何做好防护?

一、国内垃圾邮件激增,环比增长31.63%

根据Coremail邮件安全人工智能实验室(以下简称“AI实验室”)数据,2023 年Q3国内企业邮箱用户共收到近 亿封的垃圾邮件,环比增长 7.89%,同比去年同期增长 0.91%尤其是国内垃圾邮件激增,环比增长31.63%

经 AI 实验室分析,在 TOP100 接收列表中,教育领域收到的垃圾邮件高达 2.41 亿封,环比上涨13.8%,持续处于前列。

二、境内钓鱼邮件数量激增,首次超过境外

2023 年Q3,全国的企业邮箱用户共收到钓鱼邮件高达 8606.4 万封,同比激增 47.14%,环比也有 23.67%的上升。从总的钓鱼邮件数量来看,境内和境外的钓鱼邮件都呈现增长趋势。但在 2023 年第三季度,境内钓鱼邮件的数量显著增长,超过了境外钓鱼邮件的数量。

Coremail 邮件安全人工智能实验室发现黑产越来越多利用国内的云平台的监管漏洞发送钓鱼邮件,这对国内云服务提供商而言是巨大的挑战

三、Q3垃圾邮件呈现多元化趋势

2023 年 Q3 的垃圾邮件呈现出多元化的趋势,利用各种语言、主题和策略来达成发送垃圾邮件的目的,包括测试邮件、多语言内容、退税和通知等,数量巨大,层出不穷。

而钓鱼邮件常伪装为系统通知或补贴诈骗,这增加了账户被劫和数据泄露的风险。钓鱼邮件主题常利用紧迫性日常相关性模糊性专业性来吸引受害者,建议用户对此类钓鱼邮件保持高度警惕。

四、关键发现:基于邮件的高级威胁

1横向钓鱼攻击

横向钓鱼攻击直接利用了人们的信任关系,已经成为组织面临的重大威胁,而生成型 AI 为攻击者提供了更加强大的工具,使得这些攻击更加难以防范。

以下为 Coremail 在第三季度的横向钓鱼 (也称为内域钓鱼邮件)的检测和拦截数据分析解读:

① 嵌入式钓鱼 URL 的利用:高达 95%的横向钓鱼攻击使用嵌入钓鱼 URL 的邮件。

② 攻击频率:平均每月,约 25%的组织或企业会遭受一次横向钓鱼攻击。

③ 检测挑战:79%的横向钓鱼邮件需要动态分析嵌入的钓鱼URL,这增加了检测的复杂性和时间成本。

④ 更高的威胁等级:接收横向钓鱼邮件的人员的中招率上升了 200%。

2、商业电子邮件欺诈

商业电子邮件欺诈(BEC)涉及网络罪犯伪装成高管或受信任的供应商,以操纵员工转移资金或敏感信息。

针对商业电子邮件欺诈,以下为 Coremail 在第三季度的数据分析解读:

① 账号失陷与社交工程:高达 90%的 BEC 攻击与账户失陷同时发生,而 9%采用社交工程方法。

② 攻击方法:BEC 攻击主要侧重于直接诈骗钱财或信息。

③ 仿冒策略:85%的 BEC 攻击使用以下仿冒策略。

④ 邮件内容分析:70%的邮件为“银行信息变更请求”,15%为催促付款,12%为银行信息变更。

基于 AI 的新威胁

当然,BEC 攻击不仅仅是技术挑战,它更多的是一个人为问题。这类攻击强调了员工培训和安全意识的重要性,因为员工是这类攻击的第一道防线。同时,技术如双因素身份验证、邮件过滤防护和 AI 驱动的安全工具可以提供额外的防护。

五、新措施:监控,响应与安全意识

邮件作为企业沟通的主要方式,不幸地成为了许多网络威胁的首要入口。鉴于此,维护邮件安全不仅是技术问题,还涉及到组织的多个层面。以下分析了邮件安全厂商、邮箱管理员和用户在邮件安全中的作用以及他们分别在监控、响应和安全意识三个方面的关键角色。

1、组织安全的关键挑战

① 员工的安全意识

员工经常成为安全的最弱环节。安全意识方面的缺乏、不够严格的密码策略、轻率地点击可疑链接或不当地处理敏感信息,都可能导致严重的安全事件。

② 威胁响应流程

一个好的安全响应不仅要能有效地解决问题,还要迅速执行。然而,许多组织的反馈机制和响应矩阵的复杂性导致了繁琐的流程,最终导致效率低下和暴露更多风险。

2Coremail 针对性解决方案

① 利用 LLM 进行用户报告的预分类

为了应对迫在眉睫的网络威胁,Coremail 策略性地利用了大语言模型(LLM)即时预分类用户报告的电子邮件。通过 LLM 系统进行即时评估,安全团队可以迅速优先处理威胁,确保高风险邮件得到及时处理。这不仅极大地提高了威胁管理的效率,而且显著降低了由于延迟响应而可能出现的损害风险。

② 让用户成为安全架构的一部分

对于 Coremail 来说,用户不仅仅是被动的实体,而是安全生态系统中的主动参与者。用户是企业安全中的重要角色。通过培养用户主动报告潜在威胁的文化,不仅强化了安全防御,而且增强了用户的安全意识,从而减轻了管理负担。

如上图是“仿冒发信人,仿冒系统通知”的钓鱼漏判响应处理案例的流程。这个流程中,积极的用户参与、即时的邮件威胁响应以及管理员和邮件厂商的紧密合作,得以确保邮件系统的安全性和邮件威胁管理效率。

收起阅读 »

SQL中的DDL(数据定义)语言:掌握数据定义语言的关键技巧!

DDL(Data Definition Language),是用于描述数据库中要存储的现实世界实体的语言。前面我们介绍了数据库及SQL语言的相关概念和基础知识,本篇文章我们来重点讲述DDL(数据定义语言的语法格式)的相关内容以及DDL的常用语句。一、DDL介绍...
继续阅读 »

DDL(Data Definition Language),是用于描述数据库中要存储的现实世界实体的语言。

前面我们介绍了数据库及SQL语言的相关概念和基础知识,本篇文章我们来重点讲述DDL(数据定义语言的语法格式)的相关内容以及DDL的常用语句。

一、DDL介绍

这里我们先回顾一下前面讲过的SQL语言的概念:SQL(Structured Query Language),即结构化查询语言,是在关系型数据库(诸如Mysql、SQL Server、Oracle等)里进行相关操作的标准化语言,可以根据sql的作用分为以下几种类型:

下面再来看DDL语言是什么:

DDL,全称为Data Definition Language,即数据定义语言。它是SQL语言的重要组成部分,主要用于定义和管理数据库的结构。

二、DDL语言能做什么?

通过DDL,我们可以创建、修改和删除数据库、表、视图等对象。

创建数据库: 使用CREATE DATABASE语句,我们可以创建一个新的数据库。

删除数据库: 使用DROP DATABASE语句,我们可以删除一个已经存在的数据库。

创建表: 使用CREATE TABLE语句,我们可以在数据库中创建新的表。

** 删除表:**使用DROP TABLE语句,我们可以删除一个已经存在的表。

修改表结构: 使用ALTER TABLE语句,我们可以修改已经存在的表的结构,如添加、删除或修改字段等。

三、什么是数据库对象

数据库对象是数据库的组成部分,常见的有以下几种:

1、表(Table )

数据库中的表与我们日常生活中使用的表格类似,它也是由行(Row) 和列(Column)组成的。

Description

列由同类的信息组成,每列又称为一个字段,每列的标题称为字段名。行包括了若干列信息项。一行数据称为一个或一条记录,它表达有一定意义的信息组合。一个数据库表由一条或多条记录组成,没有记录的表称为空表。每个表中通常都有一个主关键字,用于唯一确定一条记录。

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

2、索引(Index)

索引是根据指定的数据库表列建立起来的顺序。它提供了快速访问数据的途径,并且可监督表的数据,使其索引所指向的列中的数据不重复。

Description

3、视图(View)

视图看上去同表似乎一模一样,具有一组命名的字段和数据项,但它其实是一个虚拟的表,在数据库中并不实际存。视图是由查询数据库表产生的,它限制了用户能看到和修改的数据。

Description

4、图表(Diagram)

图表其实就是数据库表之间的关系示意图。利用它可以编辑表与表之间的关系。

Description

5、缺省值(Default)

缺省值是当在表中创建列或插入数据时,对没有指定其具体值的列或列数据项赋予事先设定好的值。

Description

6、规则(Rule)

规则是对数据库表中数据信息的限制,它限定的是表的列。

7、触发器(Trigger)

触发器是一个用户定义的SQL事务命令的集合。当对一个表进行插入、更改、删除时,这组命令就会自动执行。

Description

8、存储过程(Stored Procedure)

存储过程是为完成特定的功能而汇集在一起的一组SQL 程序语句,经编译后存储在数据库中的SQL程序。

Description

9、用户(User)

所谓用户就是有权限访问数据库的人。

四、DDL常用语句

4.1 数据库相关

1)查看所有数据库

格式:show databases;

2)创建数据库

格式:create database 数据库名 charset=utf8;

举例:

#创建一个名为test的数据库
#create database 库名;
create database test;
#创建一个名为test的数据库并指定字符集和编码格式
create database test default charset utf8 collate utf8_general_ci;

3)查看数据库信息

格式:show create database 库名;

**4)删除数据库 **

格式:drop database 数据库名;

举例:

#删除test数据库
drop database test;

5)使用数据库

执行表相关和数据库相关的SQL语句之前必须先使用了某个数据库

格式:use 数据库名;

举例:

use test;

4.2 表相关

1)创建表

格式:create table 表名(字段1名 类型,字段2名 类型,…)

举例:

create table person(name varchar(50),age int);
create table person(name varchar(50),age int);
create table stydent(name varchar(50),chinese int ,math int, english int)charset=utf8;
创建一个员工表emp 保存名字,工资和工作
create table emp(name varchar(50),salary int,job varchar(20));

2)查询所有表

格式:show tables;

3)查询表信息

格式:show create table 表名;

举例:

show create table emp;

4)查询表字段

格式:desc 表名; (description)

5)修改表名

格式:rename table 原名 to 新名;

举例:

rename table stydent to stu;

6)删除表

格式:drop table 表名;

4.3 alter表操作相关

1)添加表字段

格式(最后面添加):alter table 表名 add 字段名 类型;

格式(最前面添加):alter table 表名 add 字段名 类型 first;

在xxx字段后面添加:alter table 表名 add 字段名 类型 after 字段名;

举例:

alter table emp add gender gender varchar(5);
alter table emp add id int first;
alter table emp add dept varchar(20) after name;

2)删除表字段

格式:alter table 表名 drop 字段名;

举例:

alter table emp drop dept;

3)修改表字段

格式:alter table 表名 change 原名 新名 新类型;

举例:

alter table emp change job dept varchar(10);

4)修改列属性

格式:alter table 表名 modify 列名 新列属性

举例(只有MySQL是这样写的):

alter table student modify age int;

关于DDL常用语句就讲这么多了,尽管现在有许多图形化工具可以替代传统的SQL语句进行操作,同时在Java等语言中也可以使用数据库,但对于SQL各类语句的了解仍然非常重要。

收起阅读 »

人民网《外企谈信心》| Denodo:加强数据管理技术合作 护航数字经济高质量发展

本文摘自:人民网责编:王震、吕骞近日,Denodo技术公司首席执行官兼创始人AngelViña做客人民网时表示,中国拥有巨大的经济体量,吸引了众多大型全球公司来华设置分支机构,且中国企业怀有“从中国走向全球”的愿望,种种因素使得开辟中国市场成为以全球化为使命的...
继续阅读 »

本文摘自:人民网

责编:王震、吕骞

近日,Denodo技术公司首席执行官兼创始人AngelViña做客人民网时表示,中国拥有巨大的经济体量,吸引了众多大型全球公司来华设置分支机构,且中国企业怀有“从中国走向全球”的愿望,种种因素使得开辟中国市场成为以全球化为使命的Denodo公司的“必然的决策”。

微信图片_20231103092343.jpg

本期嘉宾:Denodo 全球 CEO 兼创始人Angel Viña


【采访摘要】

主持人:作为Denodo公司创始人您如何看待中国市场?Denodo选择来中国发展的缘由又有哪些?

Angel Viña:进入中国是一个必然的决策:

1. 因为我们现在有超过150家全球客户在中国有分支机构,Denodo管理数据的方式可以确保他们更好地运营全球业务。

2. 中国是全球第二大经济体,有很多公司在“从中国走向全球”,这是我们重点合作发展的一类客户。

3. Denodo是一个国际化的组织,中国是我们全球化布局的重要区域,是公司发展战略的重要一步。


主持人:近年中国出台了一系列政策,保障外资企业在华发展。您认为这一系列政策的出台将对外资企业在华发展起到什么作用?

Angel Viña:中国市场有很多积极的变化,包括新的外资政策以及创造稳定、互信的营商环境等。政府牵头进行咨询调研,公开征询外资企业的反馈,我们愿积极参与。

在一系列新法规中,跨境数据管理占到了相当的篇幅,数据交换、数据跨境、数据安全等。全球公司,无论是在华运营的跨国公司,还是出海拓展全球市场的中国公司,都有信息整合的需求。需要完整的数据来支持更明智的决策并实现公司更高效的运营,从而提供更好的客户体验。这些法规规定了数据跨境传输的方式,而Denodo的逻辑数据管理技术在这一场景中可以起到重要作用,确保数据使用的安全合规。

此外,我们会与中国本土丰富的人才网络对接,尤其是与中国的顶级研究中心合作。利用本地优势资源来支持我们的全球拓展,才能更好地服务国际公司和中国出海公司。

中国在不断加强知识产权保护,为像Denodo这样的知识密集型外资公司带来保障。同时我们希望与政府加强信任与合作,因为政府是Denodo在全球各地区的重要使用者,而信任需要依托于规范和监管,我们会积极拥护当地政策。


主持人:如今很多技术,如大数据、云计算、人工智能、区块链等正在加速创新,日益融入经济社会发展全过程。在您看来,数据管理在数字经济发展中发挥着怎样的作用?外国企业参与中国数字经济发展可以享受哪些红利?

Angel Viña:业务高层如今十分注重数据的价值,数据资产化的重要性已得到普遍的认知。用好数据才能真正地做好业务运营,政府部门才能更好地提供社会服务。任何交易、决策、工业系统或制造系统的自动化流程都离不开数据。数据技术是当今首席执行官最注重的技术,做好数据管理是第一要务。

数字经济需要构建在一个强大而统一的数据基础之上。Denodo就是用来帮助组织将多个数据资产统一为一个业务目标,创建统一的数据视图,以便业务用户能够使用数据。

各种新兴技术也都基于数据,如区块链主要用于交易的可追溯性,而不同的数据层都存储在不同的节点中,基于数据才能真正构建个性化的业务应用来支持不同行业和领域的应用场景。对于AI,数据是机器学习流程的核心,生成式AI也是如此,用AI算法构建的任何一个应用都需要一个连接数据资产的桥梁。对于制造业自动化,从机器人到各种先进技术,具备数据管理能力才能创建一个敏捷灵活的数据基础,才能实现所有现代化技术。


主持人:您认为中国企业在数字化转型中面临哪些困难?Denodo如何充分发挥自身优势助力中国企业数字化转型发展?

Angel Viña:第一点,数据的使用率。尽管数据成熟度会有或多或少的差异,但是全球的数据挑战都大同小异。拥有数据资产不是最重要的,重要的是理解数据可以如何使用。因为数据激增的现象非常普遍,数据量在未来两到三年内会以两位数的增速继续增长,甚至达到十几倍以上的增长。其次是数据碎片化的问题,数据普遍分散在许多不同的数据存储库或数据孤岛中。最后就是数据的使用,据统计,全球采集到的数据只有不到4%被使用。挖掘更多数据集的价值,才能获得更多更好的信息来支撑决策。我们也可称之为“数据民主化”,但最终目的是为了让数据更容易被用户访问和使用。

业务部门能够更容易地获得所需的数据,这就是Denodo的独特技术可以帮助中国公司的事情——显著提升数据的使用率。但在Denodo的功能之上,我认为需要来自政府或公司高层管理者对数据文化的贯彻才能真正提升数据的使用,更快地获取客户,更好地改善公民生活,改善公司的客户体验,提高运营效率和员工生产力。这些都需要对数据交易的可见性,在数据生态系统中实现数据使用。

第二点,安全和合规。在增加数据用量的同时,还需要对使用过程加以控制。我们用逻辑或虚拟化的方式来管理数据,就是为了在底层的物理数据存储和运算系统之上创建一个支持层,用来执行管控,确保数据安全,执行数据治理策略来真正地帮助企业把数据交付给正确的用户,或授权正确的用户来使用数据。控制数据使用的过程非常重要,可以确保数据交易的公开可见。一个组织每年可能有上百万或者上亿的数据交易,很多数据查询都可以实现秒级的执行,因此确保数据以安全、合法的方式被正确地使用至关重要。

第三点,对于在华运营的外国公司以及出海的中国企业如何才能解决跨境数据管理和数据分享的问题。各个国家对于数据隐私、数据主权、数据资产管控等都有强有力的立法约束,数据安全与国家安全密不可分。而我们提供的是一种真正能实现数据可操作性,同时又确保其符合跨境、跨国数据法规的方式,对全球公司来说意义重大。他们需要整合数据,做好跨全球数据的安全使用,才能更好地管理如全球人力资源部等关键职能部门的信息交换,或者更好地服务全球客户,Denodo在这方面有丰富的经验。


主持人:在您看来,数字化如何赋能中国企业更好地走出去,与世界共享合作与发展机遇?

Angel Viña:数字化是必经之路,没有数字化转型就没有未来。领导者与落后者的区别就在于数字化交付水平,也可称之为数据成熟度、数字化或者自动化。中国企业对数字化高度重视,非常清楚要想“走出去”并在全球获得竞争力,数据成熟度是一个关键指标。在这一过程中Denodo可以贡献我们的专长,这是我们多年来为世界最顶级最复杂的公司服务而积累的实践经验。20多年来始终帮助顶尖的国际公司建立数据成熟度和数据基础,让他们以更有竞争力的方式来运营和拓展现代业务。

图片5.png

嘉宾简介

Angel Viña是 Denodo Technologies 的创始人兼首席执行官,同时也是公司董事会主席。他负责监督 Denodo 整体企业战略、业务运营和全球扩张。在他的领导下,Denodo 已发展为全球数据管理的领导者,有 800 名员工和 1000 位客户,在 30 个国家/地区设有 25 个办事处。Angel 在开始 IT 职业生涯前已积淀深厚的学术基础,他于 1987 年获得马德里技术大学电信工程博士学位,随后在美国加利福尼亚大学洛杉矶分校和斯坦福大学担任博士后研究访问学者,并在多所大学担任教授。


关于Denodo

Denodo是数据管理领域的领导者。屡获殊荣的Denodo平台是领先的数据集成、管理和交付平台,使用逻辑方法实现自助式商业智能、数据科学、混合/多云数据集成和企业数据服务。Denodo的客户覆盖30多个行业的大型企业和中型市场公司。这些客户实现了超过400%的投资回报率和数百万美元的收益,在不到半年的时间内就获得回报。更多信息,请访问 denodo.com.cn。

收起阅读 »

Denodo全球CEO兼创始人Angel Viña访华 共襄中国经济数字化转型新机遇

近日,全球领先的企业数据管理服务公司Denodo全球CEO兼创始人Angel Viña叶苏斯先生携总部及亚太区高管团队访华。在为期一周的访问中,Angel Viña叶苏斯先生一行与政府机构及战略合作伙伴共商未来数据管理的新高度和新起点,共同探索Denodo助力...
继续阅读 »

近日,全球领先的企业数据管理服务公司Denodo全球CEO兼创始人Angel Viña叶苏斯先生携总部及亚太区高管团队访华。在为期一周的访问中,Angel Viña叶苏斯先生一行与政府机构及战略合作伙伴共商未来数据管理的新高度和新起点,共同探索Denodo助力中国经济数字化转型的新机遇。

图片1.png


据悉,来自西班牙的Denodo平台是领先的数据集成、管理和交付平台,使用逻辑方法实现自助式商业智能、数据科学、混合/多云数据集成和企业数据服务。目前,Denodo的客户覆盖30多个行业的大型企业和中型市场公司,拥有800多名员工和1000多家客户,在30个国家/地区设有25个办事处。


密集访问,共话在华发展机遇

北京市副市长靳伟先生、商务部投资促进局局长刘殿勋先生、中国国际商会秘书长孙晓先生、中国发展研究基金会副秘书长俞建拖先生等政府机构代表会见了Angel Viña团队,就数字科技外资企业在华发展及促进产业地方合作等议题进行了深入交流。商务部投资促进局刘殿勋局长表示,投资促进局将发挥专业机构的作用更大力度吸引和利用外资。中国数字科技领域市场发展前景广阔,欢迎Denodo把握中国市场机遇,深化在华投资合作,加强创新和绿色发展,为推动中外企业合作共赢做出贡献。

图片2.png


对此,Angel Viña先生表达了Denodo在华投资与发展的决心。他表示,尽管经过不平凡的三年,中国经济依旧展现出强大的韧性。政府在出台吸引外商投资相关政策和切实优化外商在华运营环境方面不懈努力,蓬勃发展的数字经济和各组织对数据的重视程度,让他对中国巨大的市场潜力充满信心。Denodo会持续加码在中国的投资发展和技术能力储备,为在华运营的跨国公司和出海拓展的中国企业提供安全、合规、高效的跨境数据管理平台。

Angel Viña先生一行还拜访了西班牙驻华大使馆,与大使拉斐尔·德兹卡拉尔·德马扎雷多(Rafael Dezcallar de Mazarredo)先生和商务参赞阿方索·诺列加·戈麦斯(Alfonso Noriega Gomez)先生进行会谈。


合作研讨,共探数据管理未来

访华期间,Angel Viña先生一行还与包括国家区块链技术创新中心、中国信息通信研究院、清华大学等专业学术和科研机构等在内的科研院校、机构积极接洽,探索数据管理技术发展,以及讨论该领域人才引入对接的可能性。

在清华大学和中国信息通信研究院等举办的一系列“数据编织(Data Fabric)”专家研讨会上Angel Viña先生作为特别嘉宾发表精彩演讲,并吸引众人的关注。他感叹于中国庞大的优秀人才储备,希望与各学术科研机构加深合作,为数据管理领域的技术标准制定和行业化数据解决方案贡献Denodo的专业知识和洞察。

图片3.png


在访问中国最大的IT服务提供商神州数码集团时,Angel Viña先生与神州数码集团总裁王冰峰先生及其核心管理团队进行了深入交流。对神州数码集团 “数云融合”的最新战略深表赞赏,希望通过双方密切的本地协作,为各行业大型企业和政府机构提供数字化转型的最佳产品和服务。

图片4.png


“此次访华所见证的技术创新水平给我留下了深刻的印象。中国是一个经济大国,也是数据大国、创新大国。”Angel Viña先生总结此次访华收获时如是说。在接受人民网专访时他还提出,Denodo进入中国是一个必选项,而不是可选项。Denodo是一家全球性公司,但在各个区域也要成为一家本地化公司,才能真正满足不同市场独特的需求,期待Denodo独特的逻辑数据管理能力能够为中国的数字化经济发展增添新色!

图片5.png

(图:来源人民网专访)

收起阅读 »

符号绑定的另一种打开方式

懒加载和非懒加载iOS对于引用的外部符号,分为Lazy Symbol和Non-Lazy Symbol,分别存储在__DATA,__got节和__DATA,__la_symbol_ptr节。Non-Lazy Symbol符号在dyld加载模块的时候,就会将真实的...
继续阅读 »

懒加载和非懒加载

iOS对于引用的外部符号,分为Lazy SymbolNon-Lazy Symbol,分别存储在__DATA,__got节和__DATA,__la_symbol_ptr节。

Non-Lazy Symbol符号在dyld加载模块的时候,就会将真实的函数地址写入到对应的地址中,实现绑定。而Non-Lazy Symbol则会在第一次调用该函数的时候,为其动态寻找真实函数地址并进行绑定。

facebook基于符号绑定机制,写出了hook神器fishhook,通过查找符号指针并替换,从而达到hook效果!!!

然而,基于模块检测反hook,却甚是烦人。你可能会有反反hook来应付,但是它也有可能会有反反反hook来对付你~~~

那么,怎么才能终结这场hook与反hook的心理战呢?

Mach-O View 分析动态符号绑定过程

简单分析一下Lazy Symbol的绑定过程:



这里以NSLog为例:

可以看出,符号NSLog所指向的地址为:0x0000000100006474。

转化为文件偏移为:0x0000000100006474 - 0x100008078 + 32888 = 0x6474;

到文件偏移为0x6474的位置查看:

这是一段可执行代码,地址0x6474处的意思是:读取0x647c位置处的四个字节的数据(0x1d),保存到w16寄存器。然后无条件跳转到0x645c(这里的地址,全部都是指文件偏移)。


这段代码,光这么看其实看不出什么,但是如果去调试的话,就会发现,这段代码实际上是在调用dyld_stub_binder为懒加载符号绑定真实地址。而刚刚在0x6474处的代码获取到的四字节的数据,实际上是符号绑定信息的偏移:


0xc428+0x1d = 0xc445

也就是说,动态绑定NSLog所需要的数据,就存储在0xc445处。

那么,理论上来说,如果我们尝试着修改这里的数据,是不是就会改变符号的查找的过程呢?

实践

想的再多,都不如动手操作!!!

新建一个工程,书写如下代码(main.m):

__attribute__((constructor)) static void entry(int argc,char *argv[],char **apple,char **executablepath,struct mach_header_64 **mh_ptr){

if (!strncmp(argv[0], "aaa", 3)) {
printf("the same!!");
}
}

并按照如上方式,查找到函数strncmp的Lazy Binding Info,做如下修改:


修改后:


编写动态库并注入到可执行文件:


__attribute__((visibility("default"))) int strncmq(const char *__s1, const char *__s2, size_t __n);

int strncmq(const char *__s1, const char *__s2, size_t __n){
      printf("hook:%s\nhook:%s",__s1,__s2);
       return strncmp(__s1, __s2, __n);
}

重签名运行!!


发现已经替换成功了!!!

但是,用ida或者hopper分析一下二进制文件,会发现调用的还是原来的strncmp符号:


说明如果进行模块检测的话,还是可以检测出来的~因为虽然符号查找替换了,但是实际上"外套"还是strncmp。所以,继续把外套也修改了!!!

修改这两个处:


修改后:


总结

对于动态绑定的外部引用符号,能动手脚的地方确实很多!!!

收起阅读 »

iOS应用砸壳

应用商店下载的app,都是进过加密过的,用hopper或者ida完全分析不了。那是不是就没办法了呢? 其实不然,解铃还须系铃人,要想得到解密后的文件,还是要依靠苹果爸爸啊!!! 首先,我们知道,加密后的应用,如果不解密的话,苹果自己都不知道怎么去解析可执行文件...
继续阅读 »

应用商店下载的app,都是进过加密过的,用hopper或者ida完全分析不了。那是不是就没办法了呢?
其实不然,解铃还须系铃人,要想得到解密后的文件,还是要依靠苹果爸爸啊!!!
首先,我们知道,加密后的应用,如果不解密的话,苹果自己都不知道怎么去解析可执行文件,所以当设备运行应用时,加载进内存中的数据,肯定是经过解密后的数据,因此咱们只需把内存里的对应的解密部分dump下来即可。
这里介绍一个砸壳工具:dumpdecrypted
该工具的原理就是在程序运行之后注入一个动态库,然在内存中dump下解密之后的部分。
原理和实现不赘述,想进一步了解,可以查看源码。
这里简单讲一下如何用该工具进行解密app:
1、运行源码,生成动态链接库。
解压下载下来的dumpdecrypted-master.zip到当前目录;打开终端,cd到该目录。


在该目录下直接运行make:


如果运行不成功,报错找不到文件。运行xcode-select --print-path ,查看目录是否指向/Applications/Xcode.app/Contents/Developer。如果不是,则用xcode-select -s /Applications/Xcode.app/Contents/Developer修改一下。再执行make。
运行成功,会在当前目录生成dumpdecrypted.dylib动态库。
2、注入动态库
ssh登录进手机,用ps命令查看目标app所在的目录。


记住这个目录。待会儿有用。
再用cycript获取目标app的沙盒目录。


获取到目标app的沙盒目录之后,退出cycriptcontrol+D),将第一步得到的dumpdecrypted.dylib,导入到沙盒目录(导入方法有很多,scp、PP助手、ifiles等)。
cd到沙盒目录,运行DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib /var/mobile/Containers/Bundle/Application/DDFA8DC8-F19A-4A6F-932B-22E8170BB22D/HDXinHuaDict.app/HDXinHuaDict:


看到一系列的+就说明运行成功,在沙盒目录下就会有被解密的二进制文件。



收起阅读 »

🔥🔥🔥15万奖池,环信 IM+AI编程挑战赛开启报名!

大赛背景即时通讯已成为现代生活中不可或缺的一部分,近年来人工智能的迅猛发展也为即时通讯带来了前所未有的智能化体验。高度拟人的陪伴型机器人,全能实用的服务型机器人,高效智能的对话机器人,AI 与即时通讯的结合创新,为用户提供了众多智慧化的沟通方式。借助 AI 模...
继续阅读 »


大赛背景

即时通讯已成为现代生活中不可或缺的一部分,近年来人工智能的迅猛发展也为即时通讯带来了前所未有的智能化体验。高度拟人的陪伴型机器人,全能实用的服务型机器人,高效智能的对话机器人,AI 与即时通讯的结合创新,为用户提供了众多智慧化的沟通方式。借助 AI 模型的强大能力,即时通讯正迎来全新的场景和体验。

本次「未来已来·创新无限 - 环信 IM+AI编程挑战赛」由环信举办,旨在鼓励开发者、编程爱好者和创新者们发挥创意,融合即时通讯与人工智能技术,重新定义人们的沟通方式,创造出更智能、更便捷、更有趣的沟通体验。无论您是热衷于编程技术,还是对即时通讯与人工智能融合充满好奇,本次编程挑战赛都将是您展示才华和创新的绝佳机会。

本次大赛已正式开启报名!让我们一起开创未来,用编程点亮「 IM+AI 」的交融之美!

赛程安排


赛题介绍

参赛作品须基于环信即时通讯IM(SDK或Demo),自由调用业内领先的AI大模型能力,实现具有应用场景和创新功能的AI+IM的应用。鼓励大家在本次挑战赛中发挥奇思妙想的创意, 展现花样丰富、智能化的即时通讯体验及应用场景。

以下场景及功能描述供参考:

社交场景 | AI聊天机器人、AI游戏陪玩、虚拟人物、

  • AI聊天机器人:闲聊、恋爱、角色扮演、心理咨询、情感陪伴等。
  • AI游戏陪玩: 根据玩家游戏习惯、兴趣爱好等信息,自动生成陪玩建议和游戏策略,使玩家在游戏中获得更加个性化的体验。

电商场景| 智能客服、精准营销、AI模特换装、虚拟主播

  • 智能客服:有效引导用户购买、使用产品、智能回复、处理订单和退货需求等。
  • 精准推荐:根据用户喜好,提供个性化商品描述、推荐语。
  • AI模特换装、虚拟主播

医疗场景| 智慧导诊、智能在线问诊、诊室听译机器人

  • 智慧导诊:安排预约、患者回访、发通知、收集信息,帮助患者找最近的就诊医院等。
  • 智能在线问诊:AI医生线上辅助接诊,辅助医师诊断和开方。
  • 诊室听译机器人:将医患沟通实时录音,结构化处理病情数据后自动生成病历。

教育场景| AI助教、智能教学、个性化教育

  • AI助教:智能化的处理学生反馈、教师评估、行政协助等基础功能。
  • 智能教学:智能解答和辅导学生问题,帮助学生更好理解和掌握知识。
  • 个性化教育:根据学生学习情况和反馈,自动分析和评估学生的学习成绩,能力,兴趣,提供针对性的学习计划,教学建议和改进措施。

金融保险| AI虚拟代理或顾问

  • AI虚拟代理或顾问:自动处理提交索赔,提供状态更新,执行其他基本任务等。

软件/工具 |会议助手AI洞察、群未读信息摘要、群分享链接摘要

  • 会议助手AI洞察:实时分析在线会议内容,生成会议关键点及摘要,会议结束后智能生成会议结论及待办。
  • 未读信息摘要: 群内大量未读信息,智能生成摘要,方便快速获取信息。
  • 分享链接摘要: 群内分享的海量信息智能生成摘要。

奖项设置


参赛流程

1 、点击 报名链接 完成报名
2 、扫码添加“环信冬冬”为好友,回复【队伍名+报名时填写的姓名】进群

3 、作品开发 开发应用,编写规范文档、帮助文档(包含应用介绍、应用截图、代码模块说明等);

4、 于12月10日前,根据作品提交流程,上传代码及相关文档,完成作品提交。

5、 等待评审结果

作品提交

  1. Fork 本次创新赛官方作品提交仓库至你的个人GitHub 仓库

  2. Clone 你的 GitHub 仓库代码「https://github.com/你的GitHub名/Easemob2023-Innovation-Challenge」

  3. 在本地的「Innovation- Challenge」文件夹下新创建个人项目文件夹,命名格式为“队伍名-作品名“,将参赛作品的相关文件与代码放置在该文件夹内,勿要“感染”其他文件或者文件夹

  4. 最后通过 Pull Request 将作品内容推送至官方仓库

  5. Review 通过,合入本次比赛的主分支。

参赛规则

  • 参赛者可以以个人或团队形式参赛,团队参赛请指定一名联系人

  • 竞赛所使用的编程语言和开发平台不限,参赛者可根据自身喜好和技术背景选择适合的开发平台。

  • 参赛作品不限平台,不限领域。移动端、Web端、PC端、小程序等均可。

  • 比赛期间可随时报名和提交作品,所有参赛作品必须为原创,不得抄袭他人的代码或设计。

  • 请注意作品截止提交时间,将作品内容在规定时间内推送至官方仓库,否则将不参与作品评选。

  • 参赛作品须保证原创性,不违反相关法律法规,不存在任何法律或合规风险,作品中使用的素材(包括但不限于开源代码、图片、视频等)不存在版权问题。

评选规则


活动支持

收起阅读 »

【Java集合】单列集合Collection常用方法详解,不容错过!

嗨~ 今天的你过得还好吗?路途漫漫终有一归幸与不幸都有尽头🌞在上篇文章中,我们简单介绍了下Java 集合家族中的成员,那么本篇文章,我们就来看看 Java在单列集合中,为我们提供的一些方法,以及单列集合的常用遍历玩法,一起来进入学...
继续阅读 »


嗨~ 今天的你过得还好吗?

路途漫漫终有一归

幸与不幸都有尽头

🌞

在上篇文章中,我们简单介绍了下Java 集合家族中的成员,那么本篇文章,我们就来看看 Java在单列集合中,为我们提供的一些方法,以及单列集合的常用遍历玩法,一起来进入学习吧。

在Java基础中我们也学过,在类实现接口后,该类就会将接口中的抽象方法继承过来,此时该类需要重写该抽象方法,完成具体的逻辑。


Collection 常用功能

Collection 是所有单列集合的父接口,因此在Collection 中定义了单列集合(List 和 Set)通用的一些方法,这些方法可用于操作所有的单列集合。

20190428183815681.png

1.1 方法如下:

image.png

打开api文档,我们可以看到Collection 在 java.util 下,我们通过练习来演示下这些方法的使用:


640 (28).gif


1.2方法演示

public class Demo1Collection {    

       public static void main(String[] args) {

           //创建集合对象

           //使用多态的形式 定义
           Collection<String> person = new ArrayList<>();
           //输出不是一个对象地址,所以说重写了toString 方法

           System.out.println(person);

   

   //        boolean add(Object o) 向集合中添加一个元素
   //        返回值是一个boolean值,一般可以不用接收
           person.add("科比");
           person.add("姚明");
           person.add("库里");

           person.add("麦迪");

           //添加完我们在输出一下这个集合

           System.out.println(person);


   //        boolean remove(Object o) 删除该集合中指定的元素

   //        返回 集合中存在元素,删除元素,返回true;集合中不存在,删除返回false

           boolean res1 = person.remove("科比");

           boolean res2 = person.remove("奥尼尔");

           System.out.println("res1=" +res1);

           System.out.println("res2=" +res2);

   //        boolean isEmpty() 判断该集合是否为空

           boolean empty = person.isEmpty();
           System.out.println("empty=" + empty);    
   //        boolean contains(Object o) 判断该集合中是否包含某个元素
           boolean contains = person.contains("麦迪");

           System.out.println("contains=" + contains);

   //        int size() 获取该集合元素个数

           int size = person.size();

           System.out.println("size = " + size);

   

   //        public Object[] toArray() 把集合总的元素,存储到数组中

           Object[] personArray = person.toArray();

           for (int i = 0; i < personArray.length; i++) {

               System.out.println("数组--" + personArray[i]);
           }    
   //        void clear() 删除该集合中的所有元素,但是集合还存在
           person.clear();
           System.out.println(person);    
   
           //通过多态的方式,如果我们把arrayList 换成HashSet,发现也能使用,这就是我们实现接口的好处
       }
   }

注意:有关Collection中的方法不止上面这些,其他方法可以自行查看API学习。


查询集合中的元素-Iterator 迭代器

2.1 Iterator 接口

在程序开发中,经常需要遍历集合中的所有元素,就是要看看里面所有的元素,那我们怎么办呢? 

20201216223250385.png

针对这种需求,JDK 专门提供了一个接口: java.util.Iterator。该接口也是 Java集合中的一员,但它与 Collection、Map 接口有所不同,Collection 接口 与 Map 接口 主要用于存储元素,而 Iterator 主要用于迭代访问(即 遍历) Collection中的元素,因为Iterator 对象也被称为迭代器。 


下面介绍一下迭代器的概念:

迭代即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续再判断,如果还有就再取出来。一直把集合中的所有元素全部取出。这种方式专业术语称为迭代。


通过文档,我们可以看到 Iterator 是一个接口,我们无法直接使用,而需要使用Iterator接口的实现类对象,通过Collection接口中的 Iterator()方法,就可以返回迭代器的实现类对象:

public Iterator iterator():  获取结合对应的迭代器,用来遍历集合中的元素。

通过API文档,我们可以看到 Collection 中 Itrerator 接口的常用方法如下:

  • public E next()  返回迭代的下一个元素

  • public boolean hasNext() 如果仍有元素可以迭代,则返回true

image.png

接下来我们通过案例学习,如何使用Iterator 迭代集合中的元素:

 /**
    *  Iterator 迭代器使用
    */

   public class Demo1Iterator {    
       public static void main(String[] args) {            /**
            * 使用步骤:
            * 1. 使用集合中的方法 iterator() 获取迭代器的实现类对象,使用Iterator接口接收 (使用接口接收返回值,这就是我们说的多态)
            * 2. 使用Iterator接口中的方法 hashNext() 判断有没有下一个元素
            * 3. 使用Iterator接口中的方法 next() 取出集合中的下一个元素
            *
            */

           Collection<String> ball = new ArrayList<>();
           ball.add("篮球");
           ball.add("足球");
           ball.add("排球");
           ball.add("乒乓球");    
           //我们来获取一个迭代器,多态
           Iterator<String> iterator = ball.iterator();    
           //判断
           boolean b = iterator.hasNext();
           System.out.println("是否有元素--" + b);            //取出
           String next = iterator.next();
           System.out.println("元素--" + next);    
           //判断
           boolean b1 = iterator.hasNext();
           System.out.println("是否有元素--" + b1);            //取出
           String next1 = iterator.next();
           System.out.println("元素--" + next1);    
           //判断
           boolean b2 = iterator.hasNext();
           System.out.println("是否有元素--" + b2);            //取出
           String next2 = iterator.next();
           System.out.println("元素--" + next2);    
           //判断
           boolean b3 = iterator.hasNext();
           System.out.println("是否有元素--" + b3);            //取出
           String next3 = iterator.next();
           System.out.println("元素--" + next3);    
           //判断
           boolean b4 = iterator.hasNext();
           System.out.println("是否有元素--" + b4);            //取出
   //        String next4 = iterator.next();
   //        System.out.println("元素--" + next4);
           //如果没有元素,在取的话,会报一个NoSuchElementException 的错误
   
   
           /**
            *
            * 代码优化 上面这些步骤是一个重复的过程,我们可以使用循环来优化,那我们选择哪种来呢
            * 我们说 知道元素个数,使用for
            * 不知道元素个数,使用while
            *
            * 那当前我们迭代器的个数,我们不知道,所以使用while循环,而我们的hasNext 就可以作为
            * while的条件来判断
            *
            */

           while (iterator.hasNext()) {
               String ballResult = iterator.next();
               System.out.println("--优化--" + ballResult);
           }
   
       }
   }

分析: 

在进行集合元素取出时,如果集合中已经没有元素了,还继续使用迭代器的next方法,将会发生java.util.NoSuchElementException 没有集合元素的错误。


640 (28).gif


2.2 迭代器的实现原理

我们在之前的案例中已经完成了Iterator 遍历集合的整个过程。当遍历集合时,首先通过调用集合的iterator() 方法获得迭代器对象,然后使用hasNext()方法判断集合中是否存在下一个元素,如果存在,则调用next()方法将元素取出,否则说明已经到达集合末尾,停止遍历元素。

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

Itearator 迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素,为了让初学者能更好的理解迭代器的工作原理,接下来通过一个图例来演示 Iterator 对象迭代元素的过程:

Description

在获取迭代器的实现类对象是,会把索引指向集合的-1位置,也就是在调用Iterator的next方法之前,迭代器的索引位于第一个元素之前,不指向任何元素。

  • 当第一次调用迭代器的next方法后,迭代器的索引会向后移动一位,指向第一个元素并将该元素返回,

  • 当再次调用next方法时,迭代器的索引会指向第二个元素并将该元素返回,

  • 以此类推,直到hasNext方法返回false,表示到达了集合的末尾,终止对元素的遍历。


查询集合中的元素-增强for

3.1  概念

增强for循环(也称为 for each 循环)是JDK5以后出来的一个高级for循环,专门用来遍历数组和集合的。 通过api文档,Collection 继承了一个Iterable 接口 ,而实现这个接口允许对象成为 “foreach” 语句目标,也就是所有的单列集合都可以使用增强for。 

它的内部原理其实是个Iterator 迭代器,只是用for循环的方式来简化迭代器的写法,所以在遍历的过程中,不能对集合中的元素进行增删改查操作。

格式:

for (元素类型 元素名 : 集合名或数组) {
       访问元素
   }

它用于遍历 Collection 和数组。通常只进行遍历元素,不在遍历的过程中对集合元素进行增删操作。


640 (28).gif


3.2 练习1:遍历数组

public class Demo2Foreach {    
       public static void main(String[] args) {    
           int[] array = {1,2,3,4,5};    
           for (int i : array) {
               System.out.println("--数组元素--" + i);    
               if (i == 2) {
                   i = 19;
               }
           }    
           //在增强for中修改 元素,并不能赋值
           System.out.println(Arrays.toString(array));
       }
   }

640 (28).gif


3.3 练习2:遍历集合

public class Demo3Foreach {    
       public static void main(String[] args) {
           Collection<String> ball = new ArrayList<>();
           ball.add("篮球");
           ball.add("足球");
           ball.add("排球");    
           for (String s : ball) {
               System.out.println("---" + s);
           }    
           //相对于Iterator遍历方式,增强for 简化了很多,所以优先使用该方式。
       }
   }

新for 循环必须有被遍历的目标。目标只能是Collection 或者是数组。仅仅作为遍历操作出现。


总结

本篇中主要介绍了单列集合接口Collection为我们提供的常用接口,也通过代码的方式带大家体会了一下。在后面的内容中为大家介绍了如何把单列集合中的内容查看出来(遍历),通过讲解一些底层的原理,让大家感受了一下迭代器的使用。


当然集合的遍历不仅仅限于这两种方式,例如java8为我们提供的流式遍历集合,希望大家下去后自己也能搜搜相关的遍历方式,尝试使用一下,ok,本文就到这里了。



收起阅读 »

作为前端,这几个关于console的小知识点,你知道吗

web
在我们实际开发中呢,经常会遇到把一个变量打印到控制台,看一下它的结果的情况 就比如下面这个形式的对象: const obj = { "err_no": 0, "err_msg": "success", "data": { "user_ba...
继续阅读 »

在我们实际开发中呢,经常会遇到把一个变量打印到控制台,看一下它的结果的情况



就比如下面这个形式的对象:


const obj = {
"err_no": 0,
"err_msg": "success",
"data": {
"user_basic": {
"university": {},
"major": {}
},
"user_counter": {},
"user_growth_info": {}
}
}

我们一般会使用 console.log() 看一下它的值: console.log(obj)


image.png


我们点击这个按钮可以一层层的展开这个对象:


image.png


除了 console.log() 外,根据实际情况我们还可以使用下面几种。


console.dir


我们还可以使用 console.dir()。在使用它输出 JS 数据类型数据的时候它和使用 console.log() 的效果差不多:


image.png


我们展开这个对象,可以查看我们想看的数据:


image.png


当我们想打印出个某个 DOM 对象时就不一样了,使用 console.log() 输出的是这个 DOM 元素:


image.png


使用 console.dir() 输出的是这个 DOM 对象:


image.png


JSON.stringify()


我们还可以使用 console.log() 配合 JSON.stringify()


console.log(JSON.stringify(obj, null, 4))

运行效果如下:


image.png


可以看到,这里以字符串的形式将这个对象输出在了控制台。


console.table


我们还可以使用 console.table(),它会以一种表格的形式来输出结果:


image.png


可以看到,这样看着还是很整齐的。


如果我们要打印的是一个数组的话,使用 console.table() 输出数据,看起来会更方便一些:


const arr = ['a', 'b', 'c']
console.table(arr)

image.png


还有,输出多个数据的使用使用 console.table() 也有利于查看数据,如:


const a = 'a', b = 'b', c = 'c'
console.table({a, b, c})

效果如下:


image.png


consle.time 和 console.timeEnd


还有,在我们开发的过程中,有时候需要去看一段代码执行到底消耗了多少时间,我们可以使用 console.time()consle.timeEnd() 包裹想要测试运行时间的代码,比如下面这段代码:


function test() {
for (let i = 0; i < 10000; i++) { }
}

console.time()
test()
console.timeEnd()

运行代码,可以看到控制台输出了这段代码在本机大概的一个运行时间:


image.png



作者:程序员黑豆
来源:juejin.cn/post/7292969465298567187
收起阅读 »

只有一年前端经验,简历到底该怎么写?

Hello,大家好,我是雷布斯。 但不少同学还是会问“我是工作了 x 年的前端,该怎么改呢?”,“我觉得我的项目就是没有亮点,该怎么挖掘呢?”之类的问题。 那我们今天继续用一个真实的例子,给大家分享该用什么样的思路去优化自己的简历。 提前说明,参与简历修改分享...
继续阅读 »

Hello,大家好,我是雷布斯。


但不少同学还是会问“我是工作了 x 年的前端,该怎么改呢?”,“我觉得我的项目就是没有亮点,该怎么挖掘呢?”之类的问题。


那我们今天继续用一个真实的例子,给大家分享该用什么样的思路去优化自己的简历。


提前说明,参与简历修改分享的张同学,于今年 4 月参加了我们的面试全流程辅导,修改意见由我们团队的 uncle13 老师提供,关键信息已打码,并且已经获得该同学的授权来进行分享。


下面将按照简历中的模块顺序逐一进行分析。


个人简介 & 教育经历



这位同学可能是使用的简历模板,这两部分的内容都写的很简洁。


那么问题来了:



  • 简历右上角的二维码,扫出来是某简历网站,而且要求登录后才能查看电子简历,操作这么繁琐,放到纸质版的简历中是有什么目的呢?

  • 部分内容不标注清楚,面试官无法快速了解你,比如南京是现居地还是籍贯?zxxxxx.top是个人博客还是项目网站?


所以这部分可以优化下:



  • 去掉二维码

  • 把关键信息标注清楚。比如个人博客:https://xxxx.top

  • 教育经历如果没有太大的竞争力,可以考虑一块放到个人信息中,样式上会好看,改成:陕西理工大学 | 本科 | 网络工程


专业技能



简历中的专业技能是展示自己适合特定职位的能力和经验的重要方式。


但这位同学写的中规中矩,没有什么特色。


一般来说,面试官会从这个模块,了解到面试者的擅长的技术栈,熟练使用的工具等等。


编写清晰、明确的专业技能部分,有助于大家更好地展示自己的专业知识和经验。
但大家还是需要主要,对于自己写的所有关键字,都需要好好准备,面试官可以从任何一个感兴趣的方向进行提问。


针对这位同学的呢绒,可以这样进行优化:



  1. 熟练掌握HTML+CSS。熟练掌握弹性布局、网格布局以及响应式布局,能够快速实现Web端和移动端页面搭建。

  2. 熟练掌握 JavaScript,能脱离框架进行原生开发,熟悉 TypeScript

  3. 熟练使用 React 框架以及相关生态技术,能够独立完成项目搭建和项目部署,熟悉相关框架原理。

  4. 了解 vue 框架技术,有相关实践经验。

  5. 熟悉浏览器原理以及计算机网络相关技术,在性能优化方面有实践经验。

  6. 了解 Node.js 和常用模块,在此基础上能够使用 ExpressWeb 框架进行简单的服务器应用程序开发。

  7. 了解微信小程序原生开发,能够使用小程序原生开发进行开发。

  8. 对前端工程模块化有一定理解,熟悉 webpackvite 等打包工具及其日常开发配置,可以从 0 到 1 独立搭建项目,并优化构建流程。


大家发现没有,同样是描写熟悉的技术,但经过优化后,立马就变得高大上了。


工作经历



接着来看“工作经历”的模块。


其实比较推荐的介绍方式是,写清楚自己在公司的岗位、工作时间、个人的职责,同时也推荐加上公司的业务简介,不用太详细,1 - 2 句话简单介绍下就行。


在公司的个人职责就不要写的太简单了,你完全可以在这个模块,向面试官展示你的职业发展路径,工作经验和成果等等信息。


还有就是工作经历尽量用倒序的方式来组织,把最近的一份工作写在最上面。


还是以张同学的工作经历为例,我们可以这样修改:



  • 南京xx有限公司(前端开发工程师)20xx年xx月 - 至今


工作内容:



  1. 主要负责xx系统、xx系统等项目的业务开发工作,使用Vue+ElementUI技术栈。

  2. 通过使用虚拟列表等技术优化大列表性能,加快页面加载速度,提高用户体验。

  3. 参与各类项目,包括SPA应用、PC端和移动端的开发。

  4. 深入了解并熟练应用Webpack、ES6、Vue生命周期和组件通信等前端技术,持续提升自身技能水平。

  5. 在团队中积极沟通和合作,负责项目的需求分析、设计和开发,并对项目进展情况进行有效的跟踪和反馈。



  • 西安xxxx有限公司(实习前端开发)20xx年xx月 - 20xx年xx月


工作内容:



  1. 负责xx平台部分功能模块的前端开发工作。

  2. 和后端工程师配合完成接口联调,确保数据交互正常。

  3. 在实习期间积累了从需求分析到代码实现的全流程开发经验。


项目经历


“项目经历”和“工作经历”都是简历的重点。


之前分享的文章也介绍了很多挖掘项目亮点的方法,我们今天也不再过多介绍了。


现在直接来看张同学的例子。


项目一:生鲜到家



首先还是项目顺序的问题,最近的项目写在前面。


写的很简洁,一看就像是个人练手项目。但商城项目真的烂大街了,而且个人职责也完全体现不出这个项目的难度和亮点。


这样的项目,面试官真的是一点问的兴趣都没有。


不过这位同学附上了项目的访问地址,做了私有化的部署,能挽回一些印象分。


我们可以这样调整:



  • 项目名称:生鲜到家


项目简介:生鲜到家是一个在线购物商城,实现了商铺选品、购物计费、购物车、购物下单等功能。


项目地址xxxx.top/


技术栈:Vue3,Vuex,Vue-router,Axios,Mock.js


主要职责



  1. 负责整个项目前端生产环境的整体搭建(仓库创建、项目搭建、工程发布)以及项目管理。

  2. 采用 vue 技术生态开发了核心业务模块(购物车、商品选择等),并针对业务做了一些公共组件的抽离:通信、错误处理、图片优化以及其他业务组件的二次封装。

  3. 使用 Mock.js 进行数据模拟,并封装 Axios 发送请求,减少对后端接口的依赖。成功实现了前后端分离开发并独立部署上线。

  4. 使用 Vue3 组合式 API 进行项目逻辑的编写,提高了代码复用性和可维护性。同时,在模块化开发过程中,成功将代码行数降低了 20% 以上。

  5. 利用 Vue-router 路由钩子函数实现系统角色权限控制,并且统一了前端路由的交互模式。

  6. 结合业务以及 UI 规范封装了 css 原子化组件,组件开发效率提升了 30%

  7. 开发过程学了XX、XX、XX、并沉淀了相关文档 ,在社区与大家分享。


大家要注意,职责中的量化数据,可以让自己的工作更具体、更形象,也更有说服力。


项目二:xx旅行



其实看这个项目的介绍,也像是个人做的 demo。我们还是基于这个项目,再来介绍一种项目介绍方式。



  • xx旅行


项目简介:xx旅行是一款基于微信云开发模式的城市旅游小程序,包括景点浏览、预定、支付、评价等功能,并且配有一个后台管理系统。


主要职责



  1. 使用微信小程序云开发工具,从0到1完成了小程序的开发,实现了用户注册、登录、浏览、预定、支付、评价等核心功能。

  2. 利用Vue框架搭建前端页面,并使用 Koa2 搭建后台管理系统,实现了对景点信息、订单信息、评价信息等的管理和统计。

  3. 对小程序的性能进行了优化,提高了小程序界面响应速度和加载速度,缩短了用户等待时间。

  4. 借助微信云函数,实现了客户端和服务端之间的交互,提高了小程序的稳定性和可靠性。

  5. 配置小程序的各种环境变量,例如AppID、密钥等,成功将小程序发布上线。


项目成果



  1. 完成的小程序共计有 15个页面,为 xx 位用户提供了景点介绍服务。

  2. 对小程序进行的性能优化,平均响应时间由原来的 2s 降低至 500ms,提高了小程序流畅度和稳定性。

  3. 在后台的开发中,广泛应用 Vue 框架和 ElementUI 组件库,提高了代码的可读性和可维护性。


可以看到从项目简介、主要职责和项目成果三个方面对项目进行介绍,结构清晰,内容也很丰富。


张同学还有几个项目,就不再这儿一一列举了。


最后


相信看到了这儿的同学,都有不少收获,修改简历的技能又有所增长,仿佛知道了怎么去做简历的优化。


如果有的同学还要抱怨,“道理我都懂,但我怎么还是改不好自己的简历?”,那么推荐这样的同学报名我们的辅导服务,大厂导师一对一辅导,绝对物超所值。


作者:前端时代周刊
来源:juejin.cn/post/7254081954517073980
收起阅读 »

你知道 XHR 和 Fetch 的区别吗?

web
现如今,网站开发普遍采用前后端分离的模式,数据交互成为了不可或缺的关键环节。在这个过程中,XHR 和 Fetch API 是两种最常见的方法,用于从 Web 服务器获取数据。XHR 是一种传统的数据请求方式,而 Fetch API 则代表了现代 Web 开发的...
继续阅读 »

现如今,网站开发普遍采用前后端分离的模式,数据交互成为了不可或缺的关键环节。在这个过程中,XHRFetch API 是两种最常见的方法,用于从 Web 服务器获取数据。XHR 是一种传统的数据请求方式,而 Fetch API 则代表了现代 Web 开发的新兴标准。接下来,我们将一同深入学习它们的使用方法和适用场景。


XMLHttpRequest


XMLHttpRequest,通常简称为 XHR。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。XMLHttpRequest 在 AJAX 编程中(比如 jquery)被大量使用。



AJAX :异步 JavaScript 和 XML。许多人容易把它和 jq 的 ajax 混淆。它是一个技术统称,本身不是一种技术。



特点



  1. 异步请求:XHR 允许进行异步请求,它可以在后台执行,而不会阻止页面的其他操作。

  2. 支持跨域请求:通过服务器端设置允许跨域请求,从不同域的服务器获取数据。

  3. 事件驱动:提供了 onloadonerroronprogress 等一系列事件来监听请求的状态变化。

  4. 灵活性:提供了对请求头、响应头以及请求方法的完全控制,使其非常灵活。


工作原理


XHR 的工作原理主要为:



  1. 创建 XHR 对象实例:通过new XMLHttpRequest()创建一个 XHR 对象。

  2. 配置请求:使用open()方法设置请求方法(GET、POST 等)、URL,以及是否要异步执行请求。

  3. 设置回调函数:设置事件处理程序来处理请求完成、成功、失败等不同的状态。

  4. 发起请求:使用send()方法发送请求。

  5. 处理响应:在事件处理程序中处理响应数据,通常使用responseTextresponseXML来访问响应内容。


// 创建一个新的XHR对象
const xhr = new XMLHttpRequest();

// 配置请求
xhr.open("GET", "https://api.baidu.com/test", true);

// 设置响应处理函数
xhr.onload = function() {
if (xhr.status === 200) {
// 请求成功
const responseData = xhr.responseText;
console.log("成功获取数据:", responseData);
} else {
// 请求失败
console.error("请求失败,状态码:" + xhr.status);
}
};

// 发起请求
xhr.send();

XHR 的响应处理通常在onreadystatechange事件处理程序中完成。在上面的例子中,我们等待 XHR 对象的状态变为 4(表示请求完成)并且 HTTP 状态码为 200(表示成功响应)时,解析响应数据。


Fetch API


Fetch 是一种现代的数据网络请求 API,它旨在解决 XHR 的一些问题,提供了更强大、更灵活的方式来处理 HTTP 请求。可以理解为 XMLHttpRequest 的升级版。


特点



  1. Promise 风格:Fetch API 使用 Promise 对象来处理异步请求,使代码更具可读性和可维护性。

  2. 更简单的语法:相较于 XHR,Fetch API 的语法更加简单明了,通常只需要几行代码来完成请求。

  3. 默认不接受跨域请求:为了安全性,Fetch API 默认不接受跨域请求,但可以通过 CORS(跨域资源共享)来进行配置。

  4. 更现代的架构:Fetch API 是建立在 PromiseStream 之上的,支持更灵活的数据处理和流式传输。


工作原理


Fetch 的工作原理主要为:



  1. 使用fetch()函数创建请求:传入要请求的 URL,以及可选的配置参数,例如请求方法、请求头等。

  2. 处理响应:fetch()返回一个 Promise,您可以使用.then()链式调用来处理响应数据,例如使用.json()方法解析 JSON 数据或.text()方法获取文本数据。

  3. 错误处理:您可以使用.catch()方法来捕获任何请求或响应的错误。

  4. 使用async/await:如果需要,您还可以使用async/await来更清晰地处理异步操作。


Fetch API 的特性和简单的语法使它在许多前端项目中成为首选工具。然而,它也有一些限制,例如不支持同步请求,因此需要谨慎使用。


fetch("https://api.baidu.com/test")
.then(response => {
if (!response.ok) {
throw new Error("请求失败,状态码:" + response.status);
}
return response.json();
})
.then(data => {
// 请求成功,处理响应数据
console.log("成功获取数据:", data);
})
.catch(error => {
// 请求失败,处理错误
console.error(error);
});

XHR 和 Fetch 的对比


XHR 和 Fetch 都用于进行 HTTP 请求,但它们之间存在一些关键区别:



  • 语法: Fetch 使用 Promise,更直观和易于理解。

  • 跨域请求: Fetch 在跨域请求方面更灵活,支持 CORS。

  • 流式传输: Fetch 支持可读流,适用于大文件下载。

  • 维护性: Fetch 更容易维护和扩展。


常用库和插件


基于 XHR 封装的库



  • jquery:一个 JavaScript 库,提供了用于处理 DOM 操作、事件处理和 XHR 请求的便捷方法。

  • axios:一个流行的 HTTP 请求库,基于 XHR 开发,支持浏览器和 Node.js。


基于 fetch 封装的库



  • redaxios:它具有与 axios 类似的 API,但更轻量级且适用于现代 Web 开发。

  • umi-request:由 Umi 框架维护的网络请求库,提供了强大的拦截器、中间件和数据转换功能。


总结


XMLHttpRequest (XHR) 和 Fetch API 都是前端开发中用于进行数据请求的有力工具。XHR 在传统项目中仍然有用,而 Fetch API 则在现代 Web 开发中越来越流行。具体选择哪个工具取决于项目的需求和开发团队的偏好,希望本文对你有帮助!


作者:王绝境
来源:juejin.cn/post/7295551704816189467
收起阅读 »

以订单退款流程为例,聊聊如何优化策略模式

如果有人问你什么是策略模式?你可以尝试这样回答 策略模式是一种行为设计模式,它允许在运行时根据不同的情况选择不同的算法策略。这种模式将算法的定义与使用的代码分离开来,使得代码更加可读、可维护和可扩展。 在策略模式中,通常有一个抽象的策略接口,定义了一系列可以...
继续阅读 »

如果有人问你什么是策略模式?你可以尝试这样回答



策略模式是一种行为设计模式,它允许在运行时根据不同的情况选择不同的算法策略。这种模式将算法的定义与使用的代码分离开来,使得代码更加可读、可维护和可扩展。


在策略模式中,通常有一个抽象的策略接口,定义了一系列可以被不同策略实现类所实现的方法。在使用时,可以根据需要选择合适的具体策略类,并通过接口进行调用。



虽然解释了策略模式,但是面试官可能会认为你在吊书袋,完全没有自己的理解。我因为被领导卷到了,所以对策略模式有一些其他的理解,接下来我从业务中台实际遇到的问题出发,谈谈怎么被领导卷到,谈谈我对策略模式的优化和理解。


在业务中台,可以根据具体情况选择合适的策略类来执行相应的业务逻辑,从而满足不同业务方的要求。


由于各个业务方的业务逻辑存在差异性和相似性,编写中台代码时需要考虑这些差异性,并预留扩展点以供不同业务方根据自身需求提供策略类。在这种情况下,可以应用策略模式。


image.png


当业务方有特殊的业务逻辑时,只需要添加新的策略实现类即可,不需要修改现有的代码,方便了后续的维护和扩展。


然而在我们在应用策略模式时遇到了几个问题


遇到的实际问题


由于业务中台逻辑非常复杂,每个业务线的业务场景都很难保证完全一样,在代码实践中我们的系统出现了非常庞大复杂的策略类,将大量业务逻辑整合到同一个策略中,这导致系统能力复用非常困难。


举个例子,在退款校验逻辑中 业务场景 A,(售卖红包,类似于美团饿了么会员)。 需要判断如下校验逻辑,如果命中,则不允许退款!




  1. 订单是否在生效期,过期不允许退




  2. 订单是否在已使用,已使用不可退




  3. 订单如果是某类特殊商品订单,则不可退




  4. 如果超过当天最大退款次数,则不可退。




于是我们在编写代码时,就新定义 ConcreteStrategyA ,将以上 4 个业务逻辑放到策略类中。


过一段时间,又出现了业务场景 B(售卖红包,类似于美团饿了么会员)。它的校验逻辑和A 大同小异




  1. 订单是否在生效期,过期不允许退




  2. 订单售卖的红包完全使用不可退,部分使用可以退。




  3. 如果超过当天最大退款次数,则不可退。




业务场景B 相比A 而言,少校验了 “特殊商品订单不可退”的逻辑,同时增加了部分使用可以退的逻辑。


如何设计代码实现两种业务场景的校验逻辑呢? 这是非常具体的问题,如果是你如何实现呢?


完全独立的策略类


我们在最开始写代码时,分别独立实现了 ConcreteStrategyA、ConcreteStrategyB。两者的校验逻辑各自独立,没有代码复用。


此时的系统类图如下


image.png


这种实现方式是大量的代码拷贝,例如退款次数限制、生效期校验等代码都大量重复拷贝了。


后来我们发现了这个问题,将相关校验方法抽到父类中。


继承共同的父类


为了更好的复用校验策略,我们将校验生效期的方法、校验退款次数的方法抽取到共同的父级策略类。由具体的子策略类继承BaseStrategy 父策略类,在校验逻辑中,使用父级的校验方法。


如下图的类图所示
image.png


在相当长的一段时间里,我们认为这已经是最优的实现方式了。


但是被领导卷到了!


image.png


被卷到了


“新增业务场景时,为什么校验逻辑都是复用的原有能力,还需要新增扩展类,还需要开发代码呢?” 领导这样问道。


我尝试回答领导的问题:“开发这段代码,并不算太难,只需要增加一个扩展类就可以了!”


“你数数现在有多少个扩展类了?” 领导似乎有些生气,


我一看扩展类的数量,被吓到了,已经有了15个扩展类。这一刻真的非常尴尬,平时领导从不亲自看代码。估计是突然心血来潮。从表情来看,他好像很生气,估计是代码没看懂!


“我看到这部分代码时,想要查看具体的策略类,但Idea直接刷出了15个策略类……为什么会有这么多的扩展类呢?” 领导进一步补充道。


场面僵住了,我也没什么好办法……。当然领导向来的传统是:只提问题,不给解决办法! 我只能自己想解决办法!如何解决策略类过多、扩展类膨胀的问题呢?


image.png


策略类过多怎么办!


当业务场景非常多,业务逻辑非常复杂时,确实会出现非常多的策略类。但领导的问题是,现有业务场景有很多相似性,某些新增业务场景和原有业务场景类似,校验逻辑也是类似的,为什么还需要新增扩展类呢?


经过深思熟虑,让我发现问题所在!


策略类粒度太粗,导致系统复用难


目前我们的系统设计是每个业务场景都有单独的策略,这是大多数人认可的做法。不同的业务场景需要不同的策略。


然而,仔细思考一下我们会发现,不同业务场景的退款校验逻辑实际上是由一系列细分校验逻辑组合而成的,退款校验逻辑在不同的业务场景中是可以被重复使用的。


为什么不能将这些细分退款校验逻辑抽象为策略呢?例如,将过期不允许退款、已生效不可退款、超过最大退款次数不允许退款等校验逻辑抽象成为校验策略类。


各个业务场景组合多个校验策略,这样新增业务场景时,只需要组合多个校验策略即可。


如何组合校验策略


首先需要抽象校验策略接口: VerifyStrategy


classDiagram
class VerifyStrategy{
+void verify(VerifyContext context);
}

然后定义 VerifyScene 类


classDiagram
class VerifyScene{
+ Biz biz;
+ List<VerifyStrategy> strategies;
}

如何把对应具体策略类配置到VerifyScene中呢?本着 ”能不开发代码,就不要开发代码,优先使用配置!“的原则,我们选择使用Spring XML 文件配置。(在业务中台优先使用配置而非硬编码,否则这个问题不好回答。“业务方和中台都需要开发,为啥走你中台?”)


使用Spring XML 组合校验策略


在Spring XML文件中,可以声明VerifyScene类的Bean,初始化属性时,可以引用相关的校验策略。


 <bean name="Biz_A_Strategy" p:biz="A" class="com.XX.VerifyScene">
<property name="strategies">
<list>
<ref bean="checkPeriodVerifyStrategy"/> <!--校验是否未过期-->
<ref bean="checkUsageInfoVerifyStrategy"/> <!--校验使用情况-->
<ref bean="checkRefundTimeVerifyStrategy"/><!--校验退款次数-->
</list>
</property>
</bean>

当需要新增业务场景时,首先需要评估现有的校验策略是否满足需求,不满足则新增策略。最终在XML文档中增加 VerifyScene 校验场景,引用相关的策略类。


这样新增业务场景时,只要校验逻辑是复用的,就无需新增扩展类,也无需开发代码,只需要在XML中配置策略组合即可。


在XML文档中可以添加注释,说明当前业务场景每一个校验单元的业务逻辑。在某种程度上,这个XML文档就是所有业务的退款校验的业务文档。甚至无需再写文档说明每个业务场景的退款策略如何如何~


和领导汇报以后,领导很是满意。对业务方开始宣称,我们的中台系统支持零开发,配置化接入退款能力。


结束了吗?没有 ,我们后来想到更加优雅的方式。


使用Spring Configuration 和 Lamada


Spring 提供了@Bean注解注入Bean,我们可以使用Spring @Bean方式声明校验策略类


@Bean
public VerifyStrategy checkPeriodVerifyStrategy(){
return (context)->{
//校验生效期
};
}

通过以上方式,可以把checkPeriodVerifyStrategy 校验策略注入到Spring中,spring beanName就是方法名checkPeriodVerifyStrategy。


在Spring XML中可以使用 <ref bean="checkPeriodVerifyStrategy"/> 引用这个bean。


并且当点击XML中beanName时,可以直接跳转到 被@Bean修饰的checkPeriodVerifyStrategy方法。这样在梳理校验流程时,可以很方便地查看代码


点击这个BeanName,会跳转到对应的方法。(付费版Idea支持,社区版 Idea 不支持这个特性)
image.png


总结


总结几个问题




  1. 策略模式目的是:根据不同的业务场景选择不同的策略来执行相应的逻辑




  2. 策略模式一定要进行细化,通过组合多个细分策略模式为一个更大的策略,避免使用继承方案。




  3. 使用Spring XML 组合多个策略模式,可以避免开发。减少新增策略类




  4. 使用Spring Configuration @Bean 将策略类注入Spring 更加优雅




作者:他是程序员
来源:juejin.cn/post/7295010992122101801
收起阅读 »

从码农到工匠-怎么写好一个方法

感谢你阅读本文 今天我们来分享一下怎么怎么写好一个方法,我记得大四的时候彪哥推荐我去读《代码精进之路,从码农到工匠》这本书,那时候由于项目经验不多,所以对于书中很多东西没有感同身受,所以不以为然。 后面做的项目越来越多,从痛苦中不断思考,也看了一些优秀开源框架...
继续阅读 »

感谢你阅读本文


今天我们来分享一下怎么怎么写好一个方法,我记得大四的时候彪哥推荐我去读《代码精进之路,从码农到工匠》这本书,那时候由于项目经验不多,所以对于书中很多东西没有感同身受,所以不以为然。


后面做的项目越来越多,从痛苦中不断思考,也看了一些优秀开源框架的实现,逐渐对于开始理解书中的思想。


下文只列举了书中的的“方法封装”,“参数”和“方法短小”这三个,其他的没有列举,因为我觉得其实只要这三点我们执行到位,写出来的代码虽然谈不上优秀,但是维护性和可读性一定会大大提高。


封装的艺术


我们在学习面向对象编程时就知道面向对象的几大特征是抽象,继承,封装,多态,所以学会合理的封装是一件很重要的事情,这也是我们每个程序员应该有的意识。


判断封装


在方法中,总是难免会有很多判断,判断越多,程序就会变得越复杂,但是又不能不判断,我们可以使用设计模式来改善判断很多的方法,使用设计模式来优化判断,比如策略模式,但是其实它并没有消除判断,换句话说,判断是消除不了,我们能做的只能是让判断的代码可读,下面我们来看一段代码。


如下是一个判断,对文件小于1G,类型为txt,文件名中包含user_click_data或者super_user_click_data的文件可以进行处理


对于这个判断,虽然条件并不复杂,但是在代码中就显得“不清洁”,特别是随着系统的不断迭代,加入的人员不断增多,那么判断将会越来越多,如果开发人员加上清晰的注释,那么就比较容易读懂,但是并不是谁都有这样的习惯。


之前我遇到的代码中,有些一个方法中四五十个判断,而且判断还特别复杂,各种&&,||等一大堆,读起来十分费劲,对于这样的代码,遇到真的会让人抓狂。


如何解决呢?


封装判断就是一个好的方案,上面的判断代码我们就可以进行封装,如下。



上面我判断我们将其抽出来作为一个单独的方法,在加上可理解的方法名,那么别人在读你代码的时候就很容易读懂,也保证了代码的清洁,当然,方法命名是一件很难的事情,不过如果命名得不够准确,我们可以加上清晰的注释来解决。


虽然上面这段代码看似没啥改变,但是随着系统的不断复杂,就能体现出它的作用了,我经常看到别人在主流程的代码中加入很多复杂的判断,在我刚开始写代码的时候,也做过这种事,很多时候如果注释不清,命名不规范,上下文不清晰,那么读起来就很痛苦,而且整个方法看起来让人特别难受。


所以,一个看似很小的改动,却能在时间的作用下显得如此重要,保持这样一个习惯,不仅是对自己负责,也是对他人负责。


参数问题


参数问题也是我们应该去考虑的问题,当在一个方法中,如果参数特别多是很不友好的,我之前碰到过一个方法有十五六个参数,首先这么多个参数,在传递的时候,如果使用对IDEA使用比较熟悉,那么可以使用快捷键查看下一个参数是什么类型,如果使用不熟悉,那么就需要点进方法中去看,这是一个体力活。


加上参数越多,维护成本就越高,比如我需要加一个参数,但是这个参数并不是必须的,有些方法用到,有些不用,但是由于使用的是同一个方法,所以调用这个方法的地方都需要加上这个参数,调用的地方越多,成本就越大,如果是一个RPC接口,修改的成本更大。


复杂的参数封装成类。


对于复杂的参数,推荐封装成类然后进行传递,在添加或者减少字段的时候是在类中进行,没有对形参进行增加或者减少,对调用此方法的地方基本没任何影响,我们只需要在需要添加或者减少这个参数的地方进行操作,不需要的地方就保持不动。


方法尽量保持短小


方法短小更容易让人阅读和维护,当我去阅读别人的代码的时候,有些类有七八千行代码,一个方法有五六百行代码的时候,其实内心是奔溃的!


当然,在我刚开始写代码的时候,我也不懂,所以一个方法从来不去优化,也是一个方法一口气干到底,我记得当时给老师做一个项目,一个类我在Controller层直接写了几百行,Service层都不用,还有写Vue,也不会去使用组件和封装函数,所以一个Vue文件几千行,当需要修改的时候,十分麻烦,找一个方法和变量也特别费力。


记得上一个星期,我和前端同事联调,我看他去找一个方法找了足足三四分钟,因为项目十分庞大,加上设计有很大的缺陷,我看他们一个VUE文件可达到八九千行,所以才出现这个问题。


其实无论对于前端还是后端,Java还是golang等等,代码的短小都比较容易去阅读和维护,如果发现方法越写越大,那我们就要考虑去提取,去拆分了,《代码精进之路》作者推荐Java一个方法不要超过20行,不过也不是绝对的。


对于像Spring这种特别复杂的框架,因为不断升级和迭代,所以有些方法也变得特别长,代码也变得可读性不那个高,但是也不影响它是Java生态中使用最广泛,最强大的框架。


只不过对于我们普通人来说,因为水平肯定没有Spring团队的强,所以保证方法的可读性是十分重要的。


今天的分享就到这里,感谢你的观看,我们下期见。


参考:《代码精进之路,从码农到工匠》weread.qq.com/web/reader/…?


作者:刘牌
来源:juejin.cn/post/7295237661617995828
收起阅读 »

开发一个简单的管理系统,前端选择 Vue 还是 React?

web
在前端开发的世界中,React和Vue都是非常流行的JavaScript库,它们都提供了许多有用的功能来帮助开发者构建高质量的用户界面。然而,在我个人的开发经验中,相比于React,我更喜欢使用Vue。接下来讲讲我的实践经验。 我们在低代码开发领域探索了多年...
继续阅读 »

在前端开发的世界中,React和Vue都是非常流行的JavaScript库,它们都提供了许多有用的功能来帮助开发者构建高质量的用户界面。然而,在我个人的开发经验中,相比于React,我更喜欢使用Vue。接下来讲讲我的实践经验。



我们在低代码开发领域探索了多年,从2014 开始研发低代码前端渲染,到 2018 年开始研发后端低代码数据模型,发布了JNPF快速开发平台。


JNPF是一个Vue2/Vue3搭建的低代码数据可视化开发平台,将图表或页面元素封装为基础组件,无需编写代码即可完成业务需求。


前端采用的是Vue、Element-UI…;后端采用Java(.net)、Springboot…;使用门槛低,支持分布式、k8s集群部署,适用于开发复杂的业务管理系统(ERP、MES等);采用可视化组件模式可以有效地扩展不同的业务功能,并方便实现各种业务需求,且不会导致系统臃肿,若想使用某个组件,按需引入即可,反之亦然。



低代码平台的前端框架采用Vue的优势有哪些?




  •  Vue是组件化开发,减少代码的书写,使代码易于理解。




  •  最突出的优势在于可以对数据进行双向绑定。




  •  相比较传统的用超链接进行页面的切换与跳转,Vue使用的是路由,不用刷新页面。




  •  Vue是单页应用,加载时不用获取所有的数据和dom,提高加载速度,优化了用户体验。




  •  Vue的第三方组件库丰富,低代码平台能够获得更多的支持和资源。




JNPF-Web-Vue3 的技术栈介绍


JNPF 快速开发平台的 Vue3.0 版本是基于 Vue3.x、Vue-router4.x、Vite4.x、Ant-Design-Vue3.x、TypeScript、Pinia、Less 的后台解决方案,采用 Pnpm 包管理工具,旨在为中大型项目做开发,提供开箱即用的解决方案。前端同时适配Vue2/Vue3技术栈。


以下对各项技术做简单的拓展介绍:


(1)Vue3.x

Vue3.x 作为一款领先的 JavaScript 框架,通过响应式数据绑定和组件化架构实现高效的应用开发。相较于 Vue2.x,在大规模应用场景下,Vue3.x 的渲染速度提升了近 3 倍,初始化速度提升了 10 倍以上,这不仅为我们提供了更出色的用户体验,也为企业应用的开发和维护提供了极大的便利。


此外,它所支持Composition API 可以更加灵活地实现代码复用和组件化,让我们的代码更加可读、可维护。总而言之,Vue3 在许多方面都进行了改进,包括更好的性能、更少的代码大小和更好的开发体验。


(2)Vue-router4.x

Vue-router4.x 作为 Vue.js 框架中的路由管理器,具备出色的性能和扩展性,为开发者提供了一种高效而灵活的前端路由解决方案。Vue Router 主要用于构建单页应用程序,允许创建可导航的Web 应用,使您可以轻松地构建复杂的前端应用。


(3)Vite4.x

一个基于 ES Module 的 Web 应用构建工具。作为一种全新的开发模式,Vite 相对于Webpack 更加出色,内置了许多优化手段,包括 HMR、代码分割、CSS 提取、缓存策略等,从而在保证开发速度的前提下,为应用程序的加载速度和性能提供了极致的保障。此外,它还支持快速的冷启动、模块化的打包方式以及自动化的多页面构建等特性,极大的提升了前端开发效率。


(4)Ant-Design-Vue3.x

一款基于 Vue3.x 的企业级 UI 组件库,旨在帮助开发者快速搭建出高质量、美观且易用的界面。不同于其他类似的组件库,Ant-Design-Vue3.x 更注重用户体验和可定制性,提供了一整套视觉、交互和动画设计解决方案,结合灵活的样式配置,可以满足大部分项目的UI 需求,帮助开发者事半功倍。


(5)TypeScript

TypeScript 作为一种静态类型的 JavaScript 超集,不仅完美兼容 JavaScript,还提供了强大的静态类型约束和面向对象编程特性,极大地提升了代码的可读性和重用性。TypeScript拥有强大的类型系统,可以帮助开发者在代码编写阶段发现潜在的错误,减少未知错误发生概率,并提供更好的代码补全和类型检查。这一特性让团队协作更加高效,同时也降低了维护代码的成本。


(6)Pinia

Pinia 是 Vue3.x 的状态管理库,基于 Vue3.x 的 Composition API 特性,为开发者提供了清晰、直观、可扩展和强类型化的状态管理方案,可以更好地管理应用数据和状态。无论是在小型项目还是庞大的企业级应用中,我们都可以依靠这个强大的状态管理库来迅速构建出高质量的应用。


(7)Less

一种 CSS 预处理器,能够以更便捷、灵活的方式书写和管理样式表。通过 Less,开发者可以使用变量、嵌套规则、混合、运算、函数等高级功能,使得样式表的编写更加简单、易于维护。使用 Less 不仅可以提高 CSS 开发效率,还可以生成更快、更小的 CSS 文件,从而减少网站加载时间,提升网站性能。


(8)Pnpm

Pnpm 作为一种快速、稳定、安全的包管理工具,它能够帮助我们管理 JavaScript 包的依赖关系,通过采用更为精简的数据存储结构,极大地减少冗余数据的存储,从而有效地节省磁盘空间。


其他亮点


作为一款基于SpringBoot+Vue3的全栈开发平台,满足微服务、前后端分离架构,基于可视化流程建模、表单建模、报表建模工具,快速构建业务应用,平台即可本地化部署,也支持K8S部署。


引擎式软件快速开发模式,除了上述功能,还配置了图表引擎、接口引擎、门户引擎、组织用户引擎等可视化功能引擎,基本实现页面UI的可视化搭建。内置有百种功能控件及使用模板,使得在拖拉拽的简单操作下,也能大限度满足用户个性化需求。


如果你是一名开发者,可以试试我们研发的JNPF开发平台。基于低代码充分利用传统开发模式下积累的经验,高效开发。


最后,给予一点建议


关于Vue,简单易上手,官方的文档很清晰,易于使用,同时它拥有更好的新能且占据的空间相比其他框架更少,同时vue的学习曲线是很平滑的,所以这是我为什么推荐优先学习vue的原因,对于新手来说易上手,快速帮助新手熟悉一些中小型的项目,但是对于大型的项目,这就要说到Vue响应机制上的问题了,大型项目的state(状态)是特别多的,这时watcher也会很多,进而导致卡顿。


对于React,主要是适应大型项目,由于React灵活的结构和可扩展性,相比Vue对于大型项目的适配性更高,此外其跨浏览器兼容、模块化、单项数据流等都是其优点,但是与Vue相反的就是它的学习曲线是陡峭的,由于复杂的设置过程,属性,功能和结构,它需要深入的知识来构建应用程序,这对于新手来说是不太适合作为一个入门级别的框架。


作者:冲浪中台
来源:juejin.cn/post/7295565904405790761
收起阅读 »

用1100天做一款通用的管理后台框架

web
前言 去年年底,我写了一篇《如何做好一款管理后台框架》的文章,这是我对开发 Fantastic-admin 这款基于 Vue 的中后台管理系统框架两年多时间的一个思考与总结。 很意外这么一篇标题平平无奇的文章能收获 30k 的浏览以及 600 多个收藏,似乎大...
继续阅读 »

前言


去年年底,我写了一篇《如何做好一款管理后台框架》的文章,这是我对开发 Fantastic-admin 这款基于 Vue 的中后台管理系统框架两年多时间的一个思考与总结。


很意外这么一篇标题平平无奇的文章能收获 30k 的浏览以及 600 多个收藏,似乎大家对这种非干货的文章也挺感兴趣。于是在这个三年的时间点上(没错,也就是1100天),我打算继续出来和大家唠唠,这一年我又做了些什么事,或者说,如何把一款好的后台框架变得通用?


题外话:如果你对我以前的文章感兴趣,可以点我头像进入主页查看;如果你期待我以后的文章,也可以点个关注。


痛点


因为 Fantastic-admin 是基于 Element Plus 这款 UI 组件库进行开发的,于是今年我陆陆续续被问到一些问题:



  • 以后会有 Ant Design Vue 版本么?会有 Naive UI 版本么?会有 …… 版本么?

  • 我们公司/团队有一套内部的 UI 组件库,可以在 Fantastic-admin 里使用么?会和 Element Plus 有冲突么?

  • 我们有一些老项目希望迁移到 Fantastic-admin 上来,但 UI 组件库用的不是 Element Plus ,有什么办法么?



类似的问题一多,我也在思考一个问题:我的这款框架是不是被 Element Plus 绑架了?如果开发者在做技术选型的时候,因为 UI 组件库不符合预期,而将我的框架筛掉,这是我不希望看到的结果。


基于这个潜在隐患,我开始计划对框架进行转型。


方案


方案一


既然开发者对 UI 组件库有各自的偏好,我又想拉拢这部分开发者,那是不是多出几套不同 UI 组件库版本的就可以了呢?没错,这是我最开始冒出来的念头。


我参考了一些同类产品的做法,尽管它们把不同 UI 组件库版本做得很像,但在使用体验过程中,还是会带来操作上的割裂感。并且因为无法抹平不同 UI 组件库在 API 上的差异,导致在框架功能上,不同版本之间也会有一些差异。



你可以分别对比左右或者上下两张图,包括左侧导航栏的样式、导航收起/展开按钮的位置、右侧项目配置中提供的功能等,都能明显发现它们的差异。


虽然这可能不是什么大问题,但我认为视觉风格上的统一是能帮助产品提高识别度的。就比如上面 4 款基于不同 UI 组件库开发的后台框架,虽然它们属于同一个产品,但如果我不告诉你,你未必能通过图片确定它们师出同门。


其次就是后台框架提供的功能不统一,这里面有一定的原因是因为 UI 组件库导致的。试想一个场景,如果你要从 Element Plus 版本的后台,迁移到 Ant Design Vue 版本的后台,框架的配置文件是否能原封不动的复制过去?如果导航(路由)数据是后端返回的,数据结构能否保持完全一致,后端无需做任何修改?因为不同 UI 组件库对菜单组件的使用方式是完全不同的,比如 Element Plus 是需要手动拼装的,而 Naive UI 则是数据驱动的,只需要传入一个树形结构的数据给组件即可。如果数据结构无法保证一致,就会增加迁移和学习的成本。


最后就是我的一点私心,因为多一个 UI 组件库的版本,势必会占据我更多的业余时间,如果同时维护 4、5 个版本,那我大概下班后的所有时间都要投入到其中,并且如果未来又有新的 UI 组件库成为流行,那就又多一个版本的维护,这并不是一个可持续发展的方案。


方案二


既然上一个方案不符合我的期望,于是我开始思考,框架本身能不能不依赖这些 UI 组件库?如果框架本身不依赖于三方的 UI 组件库,那开发者不就可以根据需要自行引入想要的组件库了么。



就如上图,主/次导航和顶栏是属于框架的部分,而这部分其实并没有用到太多 UI 组件库提供的组件,以 Element Plus 举例,我统计了一下目前 Fantastic-admin 用到的组件:



  • Menu 菜单(主/次导航)

  • Breadcrumb 面包屑(顶栏)

  • Popover 气泡卡片(顶栏右侧的工具栏)

  • Dropdown 下拉菜单(顶栏右侧的工具栏)

  • Drawer 抽屉(应用配置)

    • Message 消息提示

    • Button 按钮

    • Input 输入框

    • Radio 单选框

    • Select 选择器

    • Switch 开关

    • …(等等表单类组件)




可以看到,虽然抽屉组件里用了很多表单类的组件,但这部分组件都是在应用配置里使用的,而应用配置这个模块,主要是方便在线测试框架提供的各种功能,在实际业务开发中,是完全不需要这个模块的。



所以初步算下来,后台框架真正依赖于 Element Plus 实现的组件就只有 4 个:



  • Menu 菜单

  • Breadcrumb 面包屑

  • Popover 气泡卡片

  • Dropdown 下拉菜单


那我为什么不找一些独立的第三方插件替代呢?是的,这是我第二个方案,就是找一些独立的插件替换 UI 组件库中的组件。但问题也立马迎面而来,就是偌大一个 Github ,居然找不到符合我需求和审美的插件。


比如菜单插件,我希望它和 Element Plus 里的菜单组件在功能上没有太大差异,支持水平/垂直模式、支持折叠收起、支持设置默认激活菜单、支持默认展开等。


比如面包屑插件,或许是因为这个插件功能太简单,并且大部分 UI 组件库都有提供,在 Github 能搜到独立的面包屑插件很少,搜到的也基本上是 N 年前的上传的,既没有人维护,风格样式也很丑。


这个方案似乎也行不通……吗?


方案三


虽然方案二在实施的第一步就扑街了,但有一点思路还是正确的,就是让框架本身不依赖于三方 UI 组件库。既然网上搜不到合适的插件,那我为什么不自己写一个呢。


比如面包屑,这是一个很简单的功能,任何前端初学者应该都可以写一个面包屑组件。


而气泡卡片和下拉菜单我没有计划自己写,因为找到了一个还不错的插件 Floating Vue,它由 Vue 团队核心人员开发并维护,并且最重要的是它支持自定义样式,意味着我可以将它魔改成想要的样子,尽可能和我的框架在视觉风格上保持统一。


最后一个比较难啃的骨头就是菜单,因为找不到合适的替代品,自己写的话又比较有挑战,虽然我有一点实现思路,但不多。当然最终还是决定自己写一个,因为觉得三方 UI 组件库这么多,实在写不出来我就去读他们源码,总不能每一个源码我都读不懂吧。


这 4 个组件的替换方案确定后,剩下就是抽屉组件和它里面的一些表单组件了,这些要怎么解决呢?这会我想到了 Headless UI ,它是完全无样式的 UI 组件库,通过与 Tailwind CSS / UnoCSS 集成使用,可以快速构建出属于自己风格的组件。


但是 Headless UI 提供的组件非常有限,并不能覆盖我需要的表单组件。不过它的设计给了我启发。表单组件我并不需要非常复杂的功能,原生的表单控件其实就能满足我的使用需求,只是原生的样式比较丑,和我想要的风格不统一,那我只需要给他们定制一套统一的风格就可以了,也就写一套原子化的 CSS 样式。


于是,方案敲定,开始实操。


实操


我决定从易到难开始处理,因为这样在初期能快速看到进度推进,也避免一上来就被一个菜单功能卡住好几天,甚至十几天都没有进展,打击到自己的信心。


1. 面包屑


和预期一样,并没有什么难度,很轻松就实现了。只不过目前还是保持和 Element Plus 一样的使用方式,就是需要手动拼装,后期计划改成数据驱动的使用方式。



2. 气泡卡片 & 下拉菜单


这部分参考了 nuxt/devtoolsFloating Vue 的自定义样式,以及 nuxt/ui 中下拉菜单的样式风格,最终形成了我自己满意的风格



3. 抽屉


使用了 Headless UI 中的 Dialog 组件,因为它和抽屉组件有相同的交互方式,它们都是在遮罩层上展示内容,只不过 Dialog 更多时候是居中展示,而抽屉则是在左右两侧展示。


其次在使用过程中,发现 Headless UI 中的 Transition 组件是一个惊喜。虽然 Vue 本身就有提供 <transition> 组件用于处理过渡动画,但有一个场景会比较难处理,官方的描述是:



This technique is needed when you want to coordinate different animations for different child elements – for example, fading in a Dialog's backdrop, while at the same time sliding in the contents of the Dialog from one side of the screen.
当您要为不同的子元素协调不同的动画时,就需要使用这种技术,例如,在淡入对话框背景的同时,从屏幕的一侧滑入对话框的内容。



这说的不就是抽屉组件么?于是按照官方的示例,修改了整体风格,最终效果也就出来了。



4. 表单组件


之前的计划是修改原生表单控件的样式,但在开发过程中发现会有一定的局限性。比如 <select> 无法控制弹出选项框的样式,我的解决办法就是用 Floating Vue 封装模拟一个 select 组件。


同时也在开发过程中发现了一些被遗漏组件,于是边做边补,最终大概做了 10 多个组件。虽然看着不少,它们都秉持着最小可用的状态。什么意思呢?就是我不会给它们设计太多的 API ,因为它们的定位和三方 UI 组件库不同,它们只要满足框架本身使用即可,用不到的 API 不会进行开发。并且使用上也不会有太大负担,如果不是对框架进行二次开发,开发者是可以完全不用关注这部分组件。



5. 菜单


菜单组件确实是个难啃的骨头,我差不多用了 3 周的晚上时间去开发。


第一周,按照自己的思路徒手撸,做到一半卡壳,做不下去了;


第二周,开始看 Element Plus 、Naive UI 、Ant Design Vue 里菜单的源码;



Ant Design Vue 的没看懂,放弃;


Naive UI 的看到一半发现核心实现被作者封装到 treemate 这个独立包中了,虽然这个包是开源的,目的也是针对树形结构的一体化解决方案。但我粗略看了一遍文档,感觉有点大材小用,因为它有很多 API 我是用不到的,而我对菜单组件又有一些自己的想法,不确定是否它这个包能否满足我的需求,放弃;


最后选择看 Element Plus 的,通过在本地一点点打印数据,大概理解了实现思路,但组件递归调用,父子组件通过 provide / inject 传递数据和函数的方式,数据状态的变动也是一层层向上级组件通知,直到通知到顶层组件,在我看来有点不太优雅,如果数据能统一在顶层组件里操作就好了。其次我的计划是写一个数据驱动的菜单组件,而不是像 Element Plus 需要手动拼装的,所以虽然我大致看懂了 Element Plus 菜单组件是怎么实现的,但在我自己实现的时候,还是有很大的不同,能参考的代码并不多。


这部分的开发总结,我可能会在以后单独写一篇文章详细说说,因为这部分也是整个方案中唯一的难点。



第三周,因为实现思路大致有了,所以开发上就没有太多的卡壳,最终结果也还不错,基本达到了我的需求。


同时因为组件完全可控,顺带解决了之前使用 Element Plus 菜单组件上无法解决的 bug ,比如当菜单收起时,弹出的悬浮菜单如果数量过多,超出屏幕高度,超出的部分就无法查看了,就像这样:



但是现在则会有滚动条,使用体验上更舒服。



验证


至此,我的后台框架已经摆脱对 Element Plus 的依赖,接下来就需要验证一下是否可以方便的替换成其他 UI 组件库。


我分别用 Ant Design Vue 、Arco Design Vue 、Naive UI 、TDesign 这四款热度比较高的组件库进行了验证:















Ant Design Vue Arco Design Vue Naive UI TDesign

结果还是很满意的,都能够顺利替换,并且替换过程并没有花费很多时间,一个小时内就可以替换成功。



由于登录页这个特殊的存在,替换组件库后是需要对其用到的 Element Plus 组件进行手动修改的,这部分会比较花时间,因为会涉及到表单验证之类的东西,不同组件库的写法差异还是比较大的。



详细的替换步骤可以在 Fantastic-admin 官方文档里找到。


回顾


让我们重新看下一开始的痛点是否都解决了么:




  • 以后会有 Ant Design Vue 版本么?会有 Naive UI 版本么?会有 …… 版本么?



    虽然不会有,但可以自己动手,根据教程将默认的 Element Plus 替换成你想要的 UI 组件库就可以了





  • 我们公司/团队有一套内部的 UI 组件库,可以在 Fantastic-admin 里使用么?会和 Element Plus 有冲突么?



    不会有冲突,现在可以彻底移除 Element Plus ,安装并使用自己的 UI 组件库





  • 我们有一些老项目希望迁移到 Fantastic-admin 上来,但 UI 组件库用的不是 Element Plus ,有什么办法么?



    可以用 Fantastic-admin 源码先进行 UI 组件库的替换,之后再将老项目的业务代码逐部迁移





除了解决这些痛点,甚至还有新收获:




  • 帮助公司/企业打造视觉风格统一的产品,提高产品辨识度



    大公司可能有不止一个项目团队,不同项目团队的技术偏好可能无法完全统一,导致开发的后台长得也千变万化。但即使在这种情况下,使用 Fantastic-admin 依旧可以保持整体视觉风格上的统一。





  • 近乎于 0 的上手成本



    因为后台框架始终都只有一套,开发者不会因为切换 UI 组件库后,要重新了解后台框架的使用





  • 维护成本更低,产品生命周期更长



    这一点是对我自己说的,不管未来会出现多少个新的 UI 组件库,我都不需要去新增一个版本进行单独维护;或者 Element Plus 如果有一天停止维护了,我的产品也不会因此进入了死亡倒计时





总结


文章写到这里,差不多就结束了,虽然阅读一遍可能只花了不到10分钟,但为了做成这件事,我大概从今年 6 月份就开始构思了,也是花了蛮多的精力,所以很感谢你的耐心。


当一款产品做到第 4 个年头,周围大部分同类产品都进入到半停更的状态,这一年里我经常思考如何延长产品的生命周期,如何让更多人来使用,而这篇文章就是对我自己今年的一个总结,也是一份答卷,希望大家能喜欢。


另外,Fantastic-admin V4.0 已经正式发布,感兴趣的朋友可以来看看,或许你的下一个项目,就可以用上了。


作者:Hooray
来源:juejin.cn/post/7295624857432850468
收起阅读 »

终结屏幕适配这个话题

物理像素、逻辑像素、百分比适配 日常开发中,接触到最多的屏幕相关的单位,分别是物理像素(px),逻辑像素(dp, point)。 那物理像素和逻辑像素的区别是? 这里以一张 3x4px 的图片举例。假设该图片放置在 5x6px 的设屏幕中。如下图所示。 此时...
继续阅读 »

物理像素、逻辑像素、百分比适配


日常开发中,接触到最多的屏幕相关的单位,分别是物理像素(px)逻辑像素(dp, point)


那物理像素和逻辑像素的区别是?


这里以一张 3x4px 的图片举例。假设该图片放置在 5x6px 的设屏幕中。如下图所示。



此时想象这个图片放置在 `10*12px` 的屏幕中会是怎样呢。对比如下,会发现该图片放置在分辨率更高的屏幕中会变得非常狭小。

image.png
继续我们的例子,如果该屏幕想要保证图片能跟前面的低分辨率的设备显示效果一致的话,则图片的宽高应增加1倍的大小。即设备需要2倍的像素比例dpr(device percent ratio)。这样图片3*4逻辑像素的尺寸的图片在高分辨率设备中可以映射成6*8物理像素,而在低分辨率的设备(像素比例时1的设备),则3*4逻辑像素的图片映射为3*4物理像素的图片。


这是逻辑像素的大致机制。逻辑像素会根据目标设备的分辨率和尺寸计算出设备的缩放比例。逻辑像素出现是为了让不同分辨率的设备中显示相同的内容能取得大致相同的效果,当然逻辑像素并不是这样简单的百分比换算。


在Android中这个逻辑像素是dp,而ios中则是pt。在android中dp的换算公式中具体换算公式想了解的可以点击下面链接了解。
betterprogramming.pub/cracking-an…


在Android开发中将不同分辨率设备的中的物理像素比率进行如下分类。所以假设设备是230dpi的话也以hdpi1.5倍进行换算。所以这跟百分比的换算是不太一样的。以“微信”应用举例。


底部的Tab(微信、通讯录、发现,我),假设设计图中屏幕的宽度是375dp,根据tab均分,单个tab为93.75。你如果通过水平布局指定宽度为93.75逻辑像素的话则会发现出来的效果在某些手机上并不是均分的。


如下图类似微信界面运行在Iphone 14 pro。此时应该用百分比进行适配,即在不同的分辨率中基于设计图的尺寸进行等比例换算。如:设计图的分辨率为375*812,而显示设备的分辨率为1080*1920,则设计图上1像素相当于目标设备1 ✖️ “显示设备基于设计图的比例(1080/375=2.88)”像素,即 1✖️2.88=2.88像素。这就是百分比适配。对比下图可以发现逻辑像素适配的“我”是偏左的。


image.png


image.png


百分比适配是一种根据设计图的尺寸和设备的分辨率,以百分比的方式进行换算和适配的方法。通过计算设计图上的像素与目标设备分辨率的比例,可以得到百分比像素的值,从而实现在不同分辨率的设备上保持一致的布局和显示效果。


但是百分比并不是万能的。如下图逻辑像素适配和百分比像素适配的对比。在列表中,百分比布局则会出现一个问题。你会发现在大尺寸高分辨率的设备中,列表中的每一项都特别大。则如果用逻辑像素(dp、pt)则是这样。使用逻辑像素能充分发挥大屏的优势,屏幕越大显示的内容更多。


image.png


什么时候应该用逻辑像素,百分比像素。


具体什么时候应该用逻辑像素和百分比像素适配,取决于设计图UI。根据不同设计意图决定何种方案。大部分情况下使用逻辑像素不会出现什么问题,列表item必定使用逻辑像素。但是什么时候应该用百分比像素呢?


举个例子:



ps: 例子中我会以百分比像素表示将设计图像素根据不同分辨率设备等比例换算的像素。即1百分比像素= 1✖️ [(设计图分辨率)/ (目标设备分辨率)]。



下面是一个“购买成功”的UI图。中间有个票根信息。票根信息有个票根背景图片。


标注图中的屏幕分辨率为 393*852


image.png


这里票根信息UI应该用逻辑像素还是百分比像素适配呢?


通过标注图能明显看出票根信息在宽度上固定需要占用一定比例。所以这里宽度应该为 353百分比像素 。为了宽高比例正确,故高度也应为 346百分比像素 。注意这里高度的 346百分比像素 也应该是基于屏幕宽度 393 的百分比像素。即 目标设备屏幕宽度 * 346 / 393


因为整个票根的宽高都为百分比适配,则里面子部件的摆放、间距都应按照百分比的方式进行适配。不然则会出现子部件没法像标注图那样正确对齐的情况。


总结


物理像素(px)是屏幕上的实际物理点,表示屏幕上显示内容的最小单位。逻辑像素(dp、pt)是开发中使用的抽象单位,与物理像素的关系由设备的像素密度决定。


逻辑像素是开发中使用的抽象单位,它们与物理像素之间有一个映射关系。在不同的设备上,逻辑像素的布局和大小是相对统一的。使用逻辑像素可以让开发者在不同分辨率的设备上保持一致的布局和显示效果。


百分比适配是一种根据设计图的尺寸和设备的分辨率,以百分比的方式进行换算和适配的方法。通过计算设计图上的像素与目标设备分辨率的比例,可以得到百分比像素的值,从而实现在不同分辨率的设备上保持一致的布局和显示效果。


一般情况下,使用逻辑像素可以保持在不同设备上显示内容的一致性和最佳效果,特别是在涉及列表和大屏幕显示的情况下,需要根据设计图,决定使用何种方案。可以通过先分析使用逻辑像素思考是否合理,再考虑百分比适配的情况。在一些特定的设计需求下,如背景图片的铺满屏幕、比例布局等,可以考虑使用百分比适配来实现更精确的布局和显示效果。


作者:淹没
来源:juejin.cn/post/7294853623849812002
收起阅读 »

如何用Compose TV写电视桌面

写在前面 Compose TV 最近出来已经有一段时间,对电视开发支持的非常好,比如标题,横向/纵向列表,焦点等. 下图为最终效果成品。 Demo源码地址 整体UI框架搭建 标题(TabRow) + NatHost(内容切换) + 内容(TvLazyColu...
继续阅读 »

写在前面


Compose TV 最近出来已经有一段时间,对电视开发支持的非常好,比如标题,横向/纵向列表,焦点等.


下图为最终效果成品。



Demo源码地址


整体UI框架搭建


标题(TabRow) + NatHost(内容切换) + 内容(TvLazyColumn)



标题-TabRow



val tabs = listof("我的", "影视", "应用")

TabRow(
selectedTabIndex = selectedTabIndex,
indicator = { tabPositions, isActivated ->
// 移动的白色色块
TopBarMoveIndicator(...
}
) {
tabs.forEachIndexed { index, title ->
Tab(
// colors设置了 默认,上焦,选中的颜色
colors = TabDefaults.pillIndicatorTabColors(
contentColor = Color.White,
focusedContentColor = Color.Black,
selectedContentColor = Color.White,
)
...
) {
Text(...)
}
}
}

移动的白色色块,这里只是我写的Demo,都是可以自定义的.


fun TopBarMoveIndicator(
currentTabPosition: DpRect,
isFocused: Boolean
)
{
val width by animateDpAsState(targetValue = currentTabPosition.width, label = "width")
val height = if (isFocused) currentTabPosition.height else 2.dp
val leftOffset by animateDpAsState(targetValue = currentTabPosition.left, label = "leftOffset")
// 有焦点的时候,是矩形,无焦点的时候,是下划线.
val moveShape = if (isFocused) ShapeDefaults.ExtraLarge else ShapeDefaults.ExtraSmall

Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(Alignment.BottomStart)
.offset(leftOffset, currentTabPosition.top)
.width(width)
.height(height)
.background(color = Color.White, shape = moveShape)
.zIndex(-1f)
)
}

NatHost(内容切换) + 内容(TvLazyColumn)


内容切换


NatHost 功能类似 ViewPager,对 "我的","影视","应用" 几个 页面内容进行切换.


NavHost(
...
builder = {
composable(...) { // 我的
// 我的野蛮
}
composable(...) {// 影视
// 影视页面
}
composable(...) { // 应用
// 应用页面
}
}
)

内容布局


TvLazyColumn 与 LazyColumn 功能是差不多的,纵向布局,就不过多赘述,具体看谷歌的开发文档,网上相关视频教程 或 看DEMO源码.


TvLazyColumn(
...
) {
item {
ImmersiveList(...) // 沉浸式列表
}
item {
TvLazyRow(...) // 热门推荐
}
item {
TvLazyRow(...)
}
item {
TvLazyRow(...) // 豆瓣高分
TvLazyRow(...)
}
item {
TvLazyRow(...) // 预热抢先看
}
... ...
}

TvLazyColumn的相关参数,记住这个参数 pivotOffsets,它是设置滚动的位置的,比如设置滚动一直在中间位置.


fun TvLazyColumn(
modifier: Modifier = Modifier,
state: TvLazyListState = rememberTvLazyListState()
,
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
userScrollEnabled: Boolean = true,
pivotOffsets: PivotOffsets = PivotOffsets(),
content: TvLazyListScope.() -> Unit
)

TvLazyRow + Item


TvLazyColumn 每行又包含了 TvLazyRow 横向布局 (如果是固定的几个,可以用 Row。


自定义的布局可以用 Surface 包含的,几个关键属性, Scale(放大),Border(边框),Glow(阴影)。



TvLazyRow(...) {
items(...) { ...
Surface(
onClick = {//点击事件}
scale = ClickableSurfaceDefaults.scale(...),
border = ClickableSurfaceDefaults.border(...),
glow = ClickableSurfaceDefaults.glow(...)
) {
// 你自定义的卡片内容,比如 图片(AsyncImage) + 文本(Text)
}
}
}

我Demo里面用的是 谷歌提供的一个包含 图片+文本的控件 StandardCardLayout


ImmersiveList 沉浸式列表

有点类似 爱奇艺,腾讯,哔哩哔哩等电视应用这种列表.


ImmersiveList(
modifier = Modifier.onGloballyPositioned { currentYCoord = it.positionInWindow().y },
background = {
// 背景图片内容
}
) {
// 布局内容
// 大标题 + 详情
// TvLazyRow
}

TV其它控件推荐


Carousel 轮播界面



TvLazyVerticalGrid/TvLazyHorizontalGrid


ModalNavigationDrawer抽屉式导航栏


ListItem


分辨率适配


TV开发涉及分辨率适配问题,Compose 也能很简单的处理此问题,无论你在1920x1080,还是1280x720等分辨率下,无缝切换,毫无压力.


val displayMetrics = LocalContext.current.resources.displayMetrics
val fontScale = LocalDensity.current.fontScale
val density = displayMetrics.density
val widthPixels = displayMetrics.widthPixels
val widthDp = widthPixels / density
val display = "density: $density\nwidthPixels: $widthPixels\nwidthDp: $widthDp"
KLog.d("display:$display")
CompositionLocalProvider(
LocalDensity provides Density(
density = widthPixels / 1920f,
fontScale = fontScale
)
) {
// 我们写的Compose主界面布局
}

参考资料


What's new with TV and intro to Compose


Android TV 上使用 Jetpack Compose


Compose TV官方设计文档


JetStreaamCompose TV demo


Compose TV demo


写在后面


近几年Android推出了很多东西,我的心尖尖是 MVI,flow(完爆Rxjava),Compose>>>


TV开发的发展,一开始是 RecycleView,要去解决焦点,优化等问题,后来是Leanback,到现在的Compose TV(开发速度提升了很多很多).


我也真的很喜欢Compose的写法,简单明了,强烈推荐Compose TV开发电视,我相信谷歌,能将Compose性能优化的越来越好.


最后一篇TV开发的文章了,以后搞车载相关去了.


作者:冰雪情缘long
来源:juejin.cn/post/7294907512444010559
收起阅读 »

Android使用Hilt依赖注入,让人看不懂你代码

前言 之前接手的一个项目里有些代码看得云里雾里的,找了半天没有找到对象创建的地方,后来才发现原来使用了Hilt进行了依赖注入。Hilt相比Dagger虽然已经比较简洁,但对初学者来说还是有些门槛,并且网上的许多文章都是搬自官网,入手容易深入难,如果你对Hilt...
继续阅读 »

前言


之前接手的一个项目里有些代码看得云里雾里的,找了半天没有找到对象创建的地方,后来才发现原来使用了Hilt进行了依赖注入。Hilt相比Dagger虽然已经比较简洁,但对初学者来说还是有些门槛,并且网上的许多文章都是搬自官网,入手容易深入难,如果你对Hilt不了解或是想了解得更多,那么接下来的内容将助力你玩转Hilt。


通过本篇文章,你将了解到:




  1. 什么是依赖注入?

  2. Hilt 的引入与基本使用

  3. Hilt 的进阶使用

  4. Hilt 原理简单分析

  5. Android到底该不该使用DI框架?



1. 什么是依赖注入?


什么是依赖?


以手机为例,要组装一台手机,我们需要哪些部件呢?

从宏观上分类:软件+硬件。

由此我们可以说:手机依赖了软件和硬件。

而反映到代码的世界:


class FishPhone(){
val software = Software()
val hardware = Hardware()
fun call() {
//打电话
software.handle()
hardware.handle()
}
}
//软件
class Software() {
fun handle(){}
}
//硬件
class Hardware() {
fun handle(){}
}

FishPhone 依赖了两个对象:分别是Software和Hardware。

Software和Hardware是FishPhone的依赖(项)。


什么是注入?


上面的Demo,FishPhone内部自主构造了依赖项的实例,考虑到依赖的变化挺大的,每次依赖项的改变都要改动到FishPhone,容易出错,也不是那么灵活,因此考虑从外部将依赖传进来,这种方式称之为:依赖注入(Dependency Injection 简称DI)

有几种方式:




  1. 构造函数传入

  2. SetXX函数传入

  3. 从其它对象间接获取



构造函数依赖注入:


class FishPhone(val software: Software, val hardware: Hardware){
fun call() {
//打电话
software.handle()
hardware.handle()
}
}

FishPhone的功能比较纯粹就是打电话功能,而依赖项都是外部传入提升了灵活性。


为什么需要依赖注入框架?


手机制造出来后交给客户使用。


class Customer() {
fun usePhone() {
val software = Software()
val hardware = Hardware()
FishPhone(software, hardware).call()
}
}

用户想使用手机打电话,还得自己创建软件和硬件,这个手机还能卖出去吗?

而不想创建软件和硬件那得让FishPhone自己负责去创建,那不是又回到上面的场景了吗?


你可能会说:FishPhone内部就依赖了两个对象而已,自己负责创建又怎么了?


解耦


再看看如下Demo:


interface ISoftware {
fun handle()
}

//硬件
interface IHardware {
fun handle()
}

//软件
class SoftwareImpl() : ISoftware {
override fun handle() {}
}

//硬件
class HardwareImpl : IHardware {
override fun handle() {}
}

class FishPhone() {
val software: ISoftware = SoftwareImpl()
val hardware: IHardware = HardwareImpl()
fun call() {
//打电话
software.handle()
hardware.handle()
}
}

FishPhone 只关注软件和硬件的接口,至于具体怎么实现它不关心,这就达到了解耦的目的。
既然要解耦,那么SoftwareImpl()、HardwareImpl()就不能出现在FishPhone里。

应该改为如下形式:


class FishPhone(val software: ISoftware, val hardware: IHardware) {
fun call() {
//打电话
software.handle()
hardware.handle()
}
}

消除模板代码


即使我们不考虑解耦,假若HardwareImpl里又依赖了cpu、gpu、disk等模块:


//硬件
class HardwareImpl : IHardware {
val cpu = CPU(Regisgter(), Cal(), Bus())
val gpu = GPU(Image(), Video())
val disk = Disk(Block(), Flash())
//...其它模块
override fun handle() {}
}

现在仅仅只是三个模块,若是依赖更多的模块或者模块的本身也需要依赖其它子模块,比如CPU需要依赖寄存器、运算单元等等,那么我们就需要写更多的模板代码,要是我们只需要声明一下想要使用的对象而不用管它的创建就好了。


class HardwareImpl(val cpu: CPU, val gpu: GPU, val disk: Disk) : IHardware {
override fun handle() {}
}

可以看出,下面的代码比上面的简洁多了。




  1. 从解耦和消除模板代码的角度看,我们迫切需要一个能够自动创建依赖对象并且将依赖注入到目标代码的框架,这就是依赖注入框架

  2. 依赖注入框架能够管理依赖对象的创建,依赖对象的注入,依赖对象的生命周期

  3. 使用者仅仅只需要表明自己需要什么类型的对象,剩下的无需关心,都由框架自动完成



先想想若是我们想要实现这样的框架需要怎么做呢?

相信很多小伙伴最朴素的想法就是:使用工厂模式,你传参告诉我想要什么对象我给你构造出来。

这个想法是半自动注入,因为我们还要调用工厂方法去获取,而全自动的注入通常来说是使用注解标注实现的。


2. Hilt 的引入与基本使用


Hilt的引入


从Dagger到Dagger2再到Hilt(Android专用),配置越来越简单也比较容易上手。

前面说了依赖注入框架的必要性,我们就想迫不及待的上手,但难度可想而知,还好大神们早就造好了轮子。

以AGP 7.0 以上为例,来看看Hilt框架是如何引入的。


一:project级别的build.gradle 引入如下代码:


plugins {
//指定插件地址和版本
id 'com.google.dagger.hilt.android' version '2.48.1' apply false
}

二:module级别的build.gradle引入如下代码:


plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
//使用插件
id 'com.google.dagger.hilt.android'
//kapt生成代码
id 'kotlin-kapt'
}
//引入库
implementation 'com.google.dagger:hilt-android:2.48.1'
kapt 'com.google.dagger:hilt-compiler:2.48.1'

实时更新最新版本以及AGP7.0以下的引用请参考:Hilt最新版本配置


Hilt的简单使用


前置步骤整好了接下来看看如何使用。


一:表明该App可以使用Hilt来进行依赖注入,添加如下代码:


@HiltAndroidApp
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
}
}

@HiltAndroidApp 添加到App的入口,即表示依赖注入的环境已经搭建好。


二:注入一个对象到MyApp里:

有个类定义如下:


class Software {
val name = "fish"
}

我们不想显示的构造它,想借助Hilt注入它,那得先告诉Hilt这个类你帮我注入一下,改为如下代码:


class Software @Inject constructor() {
val name = "fish"
}

在构造函数前添加了@Inject注解,表示该类可以被注入。

而在MyApp里使用Software对象:


@HiltAndroidApp
class MyApp : Application() {
@Inject
lateinit var software: Software

override fun onCreate() {
super.onCreate()
println("inject result:${software.name}")
}
}

对引用的对象使用@Inject注解,表示期望Hilt帮我将这个对象new出来。

最后查看打印输出正确,说明Software对象被创建了。


这是最简单的Hilt应用,可以看出:




  1. 我们并没有显式地创建Software对象,而Hilt在适当的时候就帮我们创建好了

  2. @HiltAndroidApp 只用于修饰Application



如何注入接口?


一:错误示范
上面提到过,使用DI的好处之一就是解耦,而我们上面注入的是类,现在我们将Software抽象为接口,很容易就会想到如下写法:


interface ISoftware {
fun printName()
}

class SoftwareImpl @Inject constructor(): ISoftware{
override fun printName() {
println("name is fish")
}
}

@HiltAndroidApp
class MyApp : Application() {
@Inject
lateinit var software: ISoftware

override fun onCreate() {
super.onCreate()
println("inject result:${software.printName()}")
}
}

不幸的是上述代码编译失败,Hilt提示说不能对接口使用注解,因为我们并没有告诉Hilt是谁实现了ISoftware,而接口本身不能直接实例化,因此我们需要为它指定具体的实现类。


二:正确示范

再定义一个类如下:


@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
abstract fun bindSoftware(impl: SoftwareImpl):ISoftware
}



  1. @Module 表示该类是一个Hilt的Module,固定写法

  2. @InstallIn 表示模块在哪个组件生命周期内生效,SingletonComponent::class指的是全局

  3. 一个抽象类,类名随意

  4. 抽象方法,方法名随意,返回值是需要被注入的对象类型(接口),而参数是该接口的实现类,使用@Binds注解标记,



如此一来我们就告诉了Hilt,SoftwareImpl是ISoftware的实现类,于是Hilt注入ISoftware对象的时候就知道使用SoftwareImpl进行实例化。
其它不变运行一下:
image.png


可以看出,实际注入的是SoftwareImpl。



@Binds 适用在我们能够修改类的构造函数的场景



如何注入第三方类


上面的SoftwareImpl是我们可以修改的,因为使用了@Inject修饰其构造函数,所以可以在其它地方注入它。

在一些时候我们不想使用@Inject修饰或者说这个类我们不能修改,那该如何注入它们呢?


一:定义Provides模块


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():Hardware {
return Hardware()
}
}



  1. @Module和@InstallIn 注解是必须的

  2. 定义object类

  3. 定义函数,方法名随意,返回类型为我们需要注入的类型

  4. 函数体里通过构造或是其它方式创建具体实例

  5. 使用@Provides注解函数



二:依赖使用

而Hardware定义如下:


class Hardware {
fun printName() {
println("I'm fish")
}
}

在MyApp里引用Hardware:

在这里插入图片描述


虽然Hardware构造函数没有使用@Inject注解,但是我们依然能够使用依赖注入。


当然我们也可以注入接口:


interface IHardware {
fun printName()
}

class HardwareImpl : IHardware {
override fun printName() {
println("name is fish")
}
}

想要注入IHardware接口,需要定义provides模块:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():IHardware {
return HardwareImpl()
}
}


@Provides适用于无法修改类的构造函数的场景,多用于注入第三方的对象



3. Hilt 的进阶使用


限定符


上述 ISoftware的实现类只有一个,假设现在有两个实现类呢?

比如说这些软件可以是美国提供,也可以是中国提供的,依据上面的经验我们很容易写出如下代码:


class SoftwareChina @Inject constructor() : ISoftware {
override fun printName() {
println("from china")
}
}

class SoftwareUS @Inject constructor() : ISoftware {
override fun printName() {
println("from US")
}
}

@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
abstract fun bindSoftwareCh(impl: SoftwareChina):ISoftware

@Binds
abstract fun bindSoftwareUs(impl: SoftwareUS):ISoftware
}

//依赖注入:
@Inject
lateinit var software: ISoftware

兴高采烈的进行编译,然而却报错:
image.png


也就是说Hilt想要注入ISoftware,但不知道选择哪个实现类,SoftwareChina还是SoftwareUS?没人告诉它,所以它迷茫了,索性都绑定了。


这个时候我们需要借助注解:@Qualifier 限定符注解来对实现类进行限制。

改造一下:


@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
@China
abstract fun bindSoftwareCh(impl: SoftwareChina):ISoftware

@Binds
@US
abstract fun bindSoftwareUs(impl: SoftwareUS):ISoftware
}

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class US

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class China

定义新的注解类,使用@Qualifier修饰。

而后在Module里,分别使用注解类修饰返回的函数,如bindSoftwareCh函数指定返回SoftwareChina来实现ISoftware接口。


最后在引用依赖注入的地方分别使用@China @US修饰。


    @Inject
@US
lateinit var software1: ISoftware

@Inject
@China
lateinit var software2: ISoftware

此时,虽然software1、software2都是ISoftware类型,但是由于我们指定了限定符@US、@China,因此最后真正的实现类分别是SoftwareChina、SoftwareUS。



@Qualifier 主要用在接口有多个实现类(抽象类有多个子类)的注入场景



预定义限定符


上面提及的限定符我们还可以扩展其使用方式。

你可能发现了,上述提及的可注入的类构造函数都是无参的,很多时候我们的构造函数是需要有参数的,比如:


class Software @Inject constructor(val context: Context) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}
//注入
@Inject
lateinit var software: Software

这个时候编译会报错:

image.png
意思是Software依赖的Context没有进行注入,因此我们需要给它注入一个Context。


由上面的分析可知,Context类不是我们可以修改的,只能通过@Provides方式提供其注入实例,并且Context有很多子类,我们需要使用@Qualifier指定具体实现类,因此很容易我们就想到如下对策。

先定义Module:


@Module
@InstallIn(SingletonComponent::class)
object MyContextModule {
@Provides
@GlobalContext
fun provideContext(): Context? {
return MyApp.myapp
}
}

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class GlobalContext

再注入Context:


class Software @Inject constructor(@GlobalContext val context: Context?) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context?.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}

可以看出,借助@Provides和@Qualifier,可以实现全局的Context。

当然了,实际上我们无需如此麻烦,因为这部分工作Hilt已经预先帮我们弄了。

与我们提供的限定符注解GlobalContext类似,Hilt预先提供了:


@Qualifier
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
public @interface ApplicationContext {}

因此我们只需要在需要的地方引用它即可:


class Software @Inject constructor(@ApplicationContext val context: Context?) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context?.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}

如此一来我们无需重新定义Module。




  1. 除了提供Application级别的上下文:@ApplicationContext,Hilt还提供了Activity级别的上下文:@ActivityContext,因为是Hilt内置的限定符,因此称为预定义限定符。

  2. 如果想自己提供限定符,可以参照GlobalContext的做法。



组件作用域和生命周期


Hilt支持的注入点(类)


以上的demo都是在MyApp里进行依赖,MyApp里使用了注解:@HiltAndroidApp 修饰,表示当前App支持Hilt依赖,Application就是它支持的一个注入点,现在想要在Activity里使用Hilt呢?


@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {

除了Application和Activity,Hilt内置支持的注入点如下:
image.png


除了Application和ViewModel,其它注入点都是通过使用@AndroidEntryPoint修饰。



注入点其实就是依赖注入开始的点,比如Activity里需要注入A依赖,A里又需要注入B依赖,B里又需要注入C依赖,从Activity开始我们就能构建所有的依赖



Hilt组件的生命周期


什么是组件?在Dagger时代我们需要自己写组件,而在Hilt里组件都是自动生成的,无需我们干预。
依赖注入的本质实际上就是在某个地方悄咪咪地创建对象,这个地方的就是组件,Hilt专为Android打造,因此势必适配了Android的特性,比如生命周期这个Android里的重中之重。

因此Hilt的组件有两个主要功能:




  1. 创建、注入依赖的对象

  2. 管理对象的生命周期



Hilt组件如下:
image.png


可以看出,这些组件的创建和销毁深度绑定了Android常见的生命周期。

你可能会说:上面貌似没用到组件相关的东西,看了这么久也没看懂啊。

继续看个例子:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():IHardware {
return HardwareImpl()
}
}

@InstallIn(SingletonComponent::class) 表示把模块安装到SingletonComponent组件里, SingletonComponent组件顾名思义是全局的,对应的是Application级别。因此安装的这个模块可在整个App里使用。


问题来了:SingletonComponent是不是表示@Provides修饰的函数返回的实例是同一个?

答案是否定的。


这就涉及到组件的作用域。


组件的作用域


想要上一小结的代码提供全局唯一实例,则可用组件作用域注解修饰函数:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
@Singleton
fun provideHardware():IHardware {
return HardwareImpl()
}
}

当我们在任何地方注入IHardware时,获取到的都是同一个实例。

除了@Singleton表示组件的作用域,还有其它对应组件的作用域:

image.png


简单解释作用域:

@Singleton 被它修饰的构造函数或是函数,返回的始终是同一个实例

@ActivityRetainedScoped 被它修饰的构造函数或是函数,在Activity的重建前后返回同一实例

@ActivityScoped 被它修饰的构造函数或是函数,在同一个Activity对象里,返回的都是同一实例

@ViewModelScoped 被它修饰的构造函数或是函数,与ViewModel规则一致




  1. Hilt默认不绑定任何作用域,由此带来的结果是每一次注入都是全新的对象

  2. 组件的作用域要么不指定,要指定那必须和组件的生命周期一致



以下几种写法都不符合第二种限制:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
@ActivityScoped//错误,和组件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}

@Module
@InstallIn(ActivityComponent::class)
object HardwareModule {
@Provides
@Singleton//错误,和组件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}

@Module
@InstallIn(ActivityRetainedComponent::class)
object HardwareModule {
@Provides
@ActivityScoped//错误,和组件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}

除了修饰Module,作用域还可以用于修饰构造函数:


@ActivityScoped
class Hardware @Inject constructor(){
fun printName() {
println("I'm fish")
}
}

@ActivityScoped表示不管注入几个Hardware,在同一个Activity里注入的实例都是一致的。


构造函数里无法注入的字段


一个类的构造函数如果被@Inject注入,那么构造函数的其它参数都需要支持注入。


class Hardware @Inject constructor(val context: Context) {
fun printName() {
println("I'm fish")
}
}

以上代码是无法编译通过的,因为Context不支持注入,而通过上面的分析可知,我们可以使用限定符:


class Hardware @Inject constructor(@ApplicationContext val context: Context) {
fun printName() {
println("I'm fish")
}
}

这就可以成功注入了。


再看看此种场景:


class Hardware @Inject constructor(
@ApplicationContext val context: Context,
val version: String,
) {
fun printName() {
println("I'm fish")
}
}

很显然String不支持注入,当然我们可以向@ApplicationContext 一样也给String提供一个@Provides和@Qualifier注解,但可想而知很麻烦,关键是String是动态变化的,我们确实需要Hardware构造的时候传入合适的String。


由此引入新的写法:辅助注入


class Hardware @AssistedInject constructor(
@ApplicationContext val context: Context,
@Assisted
val version: String,
) {

//辅助工厂类
@AssistedFactory
interface Factory{
//不支持注入的参数都可以放这,返回值为待注入的类型
fun create(version: String):Hardware
}

fun printName() {
println("I'm fish")
}
}

在引用注入的地方不能直接使用Hardware,而是需要通过辅助工厂进行创建:


@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {
private lateinit var binding: ActivitySecondBinding
@Inject
lateinit var hardwareFactory : Hardware.Factory

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySecondBinding.inflate(layoutInflater)
setContentView(binding.root)

val hardware = hardwareFactory.create("3.3.2")
println("${hardware.printName()}")
}
}

如此一来,通过辅助注入,我们还是可以使用Hilt,值得一提的是辅助注入不是Hilt独有,而是从Dagger继承来的功能。


自定义注入点


Hilt仅仅内置了常用的注入点:Application、Activity、Fragment、ViewModel等。

思考一种场景:小明同学写的模块都是需要注入:


class Hardware @Inject constructor(
val gpu: GPU,
val cpu: CPU,
) {
fun printName() {
println("I'm fish")
}
}

class GPU @Inject constructor(val videoStorage: VideoStorage){}

//显存
class VideoStorage @Inject constructor() {}

class CPU @Inject constructor(val register: Register) {}

//寄存器
class Register @Inject() constructor() {}

此时小刚需要引用Hardware,他有两种选择:




  1. 使用注入方式很容易就引用了Hardware,可惜的是他没有注入点,仅仅只是工具类。

  2. 不选注入方式,则需要构造Hardware实例,而Hardware依赖GPU和CPU,它们又分别依赖VideoStorage和Register,想要成功构造Hardware实例需要将其它的依赖实例都手动构造出来,可想而知很麻烦。



这个时候适合小刚的方案是:



自定义注入点



方案实施步骤:

一:定义入口点


@InstallIn(SingletonComponent::class)
interface HardwarePoint {
//该注入点负责返回Hardware实例
fun getHardware(): Hardware
}

二:通过入口点获取实例


class XiaoGangPhone {
fun getHardware(context: Context):Hardware {
val entryPoint = EntryPointAccessors.fromApplication(context, HardwarePoint::class.java)
return entryPoint.getHardware()
}
}

三:使用Hardware


        val hardware = XiaoGangPhone().getHardware(this)
println("${hardware.printName()}")

注入object类


定义了object类,但在注入的时候也需要,可以做如下处理:


object MySystem {
fun getSelf():MySystem {
return this
}
fun printName() {
println("I'm fish")
}
}

@Module
@InstallIn(SingletonComponent::class)
object MiddleModule {
@Provides
@Singleton
fun provideSystem():MySystem {
return MySystem.getSelf()
}
}
//使用注入
class Middleware @Inject constructor(
val mySystem:MySystem
) {
}

4. Hilt 原理简单分析


@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {}

Hilt通过apt在编译时期生成代码:


public abstract class Hilt_SecondActivity extends AppCompatActivity implements GeneratedComponentManagerHolder {

private boolean injected = false;

Hilt_SecondActivity() {
super();
//初始化注入监听
_initHiltInternal();
}

Hilt_SecondActivity(int contentLayoutId) {
super(contentLayoutId);
_initHiltInternal();
}

private void _initHiltInternal() {
addOnContextAvailableListener(new OnContextAvailableListener() {
@Override
public void onContextAvailable(Context context) {
//真正注入
inject();
}
});
}

protected void inject() {
if (!injected) {
injected = true;
//通过manager获取组件,再通过组件注入
((SecondActivity_GeneratedInjector) this.generatedComponent()).injectSecondActivity(UnsafeCasts.<SecondActivity>unsafeCast(this));
}
}
}

在编译期,SecondActivity的父类由AppCompatActivity变为Hilt_SecondActivity,因此当SecondActivity构造时就会调用父类的构造器监听create()的回调,回调调用时进行注入。



由此可见,Activity.onCreate()执行后,Hilt依赖注入的字段才会有值



真正注入的过程涉及到不少的类,都是自动生成的类,有兴趣可以对着源码查找流程,此处就不展开说了。


5. Android到底该不该使用DI框架?


有人说DI比较复杂,还不如我直接构造呢?

又有人说那是你项目不复杂,用不到,在后端流行的Spring全家桶,依赖注入大行其道,Android复杂的项目也需要DI来解耦。


从个人的实践经验看,Android MVVM/MVI 模式还是比较适合引入Hilt的。
image.png


摘抄官网的:现代Android 应用架构

通常来说我们这么设计UI层到数据层的架构:


class MyViewModel @Inject constructor(
val repository: LoginRepository
) :ViewModel() {}

class LoginRepository @Inject constructor(
val rds : RemoteDataSource,
val lds : LocalDataSource
) {}

//远程来源
class RemoteDataSource @Inject constructor(
val myRetrofit: MyRetrofit
) {}

class MyRetrofit @Inject constructor(
) {}

//本地来源
class LocalDataSource @Inject constructor(
val myDataStore: MyDataStore
) {}

class MyDataStore @Inject constructor() {}

可以看出,层次比较深,使用了Hilt简洁了许多。


本文基于 Hilt 2.48.1

参考文档:

dagger.dev/hilt/gradle…

developer.android.com/topic/archi…

repo.maven.apache.org/maven2/com/…


作者:小鱼人爱编程
来源:juejin.cn/post/7294965012749320218
收起阅读 »

记一次使用babel做代码转换的经历

web
前言 前不久刚刚将公司项目中的静态图片资源放到阿里云oss服务器上,同时删除了项目中的图片资源,成功为项目瘦身。 这不,今天就来了一个私有化部署的需求,需要将现有的项目单独部署到客户那边的服务器上,而且客户还只使用内网,这也就导致使用阿里云访问的图片资源全部访...
继续阅读 »

前言


前不久刚刚将公司项目中的静态图片资源放到阿里云oss服务器上,同时删除了项目中的图片资源,成功为项目瘦身。


这不,今天就来了一个私有化部署的需求,需要将现有的项目单独部署到客户那边的服务器上,而且客户还只使用内网,这也就导致使用阿里云访问的图片资源全部访问不通,还得拿到本地来。


得,谁让咱们天生就是找事的好手呢,那整吧。


方案对比


既然来活了,那咱们首先得先确定下这个事怎么做?有以下几个方案:


方案一: 发挥中华民族的优良传统,勤劳,即手动将全部的静态资源引用处替换为本地引用,想想手就疼


方案二: 将偷懒运用到极致,将静态资源全部放到public/assets目录下(Vite项目中public目录下的文件会被直接复制到打包目录下),同时修改资源引用的统一前缀为 /assets,即可引用到该静态资源。目测几分钟就能完成


方案三: 写个脚本,自动完成 1 操作,瞬间手就不疼了,但是脑壳开始疼了


对比下这三个方案的优缺点,选出最优解



方案一


优点:简单


缺点:手疼且低效


方案二


优点:省时、省力


缺点:需要考虑打包后的引用路径,同时因为文件都是直接复制到包中的,并没有经过hash处理,浏览器会缓存该文件,后续如果文件修改,不能第一时间反应再客户端。


方案三


优点:高效、一劳永逸、文件会经过Vite处理,生成带有hash值的新文件,没有缓存问题


缺点:这个脚本有点难写,涉及代码转换和项目文件扫描等知识,脑壳疼



最终,本着一劳永逸的想法,我选择了方案三。


过程


整体思路:



  1. 将全部静态资源引用汇总到统一文件中,方便管理及代码分析

  2. 使用代码转换工具将上面的文件内容转换为使用 import 导入的方式


静态资源汇总


所有的静态资源引用散布在项目的各个文件中,这不利于代码分析,也不利于代码转化,所以,第一步就是将散布在项目各个文件中的静态资源引用汇总到一个文件中,方便管理、代码分析、代码转化。


这一步是纯体力活,一次劳动,收益无穷。


最终静态资源汇总文件应该是这样的:


import { ASSETS_PREFIX } from './constants';

const contactUs = `${ASSETS_PREFIX}/login/contact_us.png`;
const userAvatar = `${ASSETS_PREFIX}/login/default_avatar.png`;
const loginBg = `${ASSETS_PREFIX}/login/login_bg.jpg`;

export {
contactUs,
userAvatar,
loginBg,
}


  1. 一个静态资源对应一个变量,一个变量对应一个静态资源路径

  2. 静态资源路径必须使用模版字符串统一前缀,便于后续做替换

  3. 统一导出


代码转换


静态资源全部会送完毕后,接下来就是做代码分析及转换。


我们的目标其实就是将上面的代码转换到下面这种:


import contactUs from '@/assets/login/contact_us.png';
import userAvatar from '@/assets/login/default_avatar.png';
import loginBg from '@/assets/login/login_bg.jpg'

export {
contactUs,
userAvatar,
loginBg,
}

既然涉及代码转换,很自然的就能想到使用babel做转换。


先来简单说下babel做代码转换的过程:



  1. 使用 @babel/parser 将代码解析为抽象语法树(AST: 表示当前代码结构的js对象)

  2. 找到标识为 const 的变量,拿出该变量,并将其后对应的变量内容拿出来,将模版字符串中的变量替换为@/assets,得到新静态资源本地路径(@/assets/login/contact_us.png)

  3. 组合 import 的 AST 对象,并使用该对象替换原来的 const 相关的AST

  4. 使用 @babel/generator 将新的AST转换为代码输出到对应文件中


代码如下:


import { parse } from '@babel/parser';
import generate from '@babel/generator';
import fs from 'fs';

// 静态资源汇总文件
let imgInfoFilePath = 'src/scripts/assets.ts';
// 要替换为的静态资源路径前缀
let replaceToCode = '@/assets';

function babelTransformCode() {
logInfo(`开始转换 ${imgInfoFilePath} 文件`);
try {
const code = fs.readFileSync(imgInfoFilePath, 'utf-8');

// 解析AST
const ast = parse(code, { sourceType: 'module', plugins: ['typescript'] });

// 遍历const声明节点
ast.program.body.forEach(node => {
if (node.type === 'VariableDeclaration') {
// 构建导入声明
const importDecl = {
type: 'ImportDeclaration',
specifiers: [],
source: {
type: 'StringLiteral',
},
};

node.declarations.forEach(decl => {
// 存储变量名
const localName = decl.id.name;
// 组装import路径
const filePath = `${replaceToCode}${decl?.init?.quasis?.[1]?.value?.raw}`;
// 组装import结构
importDecl.specifiers.push({
type: 'ImportDefaultSpecifier',
local: {
type: 'Identifier',
name: localName,
},
});

// 修改初始化为相对路径
importDecl.source.value = filePath;
});

// 用importDecl替换原变量声明节点
Object.assign(node, importDecl);
}
});

// 最终代码
const result = generate.default(ast, {}, code);
// 备份原文件
fs.renameSync(imgInfoFilePath, `${imgInfoFilePath}.bak`);
// 代码输出
fs.writeFileSync(imgInfoFilePath, result.code);
} catch (error: any) {
logError(error);
}
}

这样,代码就转换完成了。


这样转换完后,ts文件中相关的静态资源引用就替换完成了,但是css文件中的静态资源引用还没有被转换。


因为css文件中的静态资源路径都是完整路径,不存在其中掺杂变量的情况,所以我们只需要找到所有的css文件,并将其中的路径前缀统一替换为@/assets 即可。


import { globSync } from 'glob';
import fs from 'fs';

let replaceStr = 'https://xxxxx.xxxx.xxxxx';
let replaceToCode = '@/assets';

function replaceHttpsCode() {
try {
// 扫描文件
const files = globSync('./src/**/*.{scss,css}', { ignore: 'node_modules/**' });

files.forEach((file: string) => {
// 读取文件内容
let content = fs.readFileSync(file, 'utf8');

// 替换匹配到的字符串
content = content.replace(replaceStr, replaceToCode);

// 写入文件
fs.writeFileSync(file, content);
});

logSuccess('转换完成');
} catch (error: any) {
logError(error);
}
}


  1. 使用 glob 扫描当前目录下的scss、css文件。

  2. 读取文件内容,并使用replace方法替换掉静态资源路径

  3. 写入文件,完成转换


至此,代码全部转换完成。


封装成工具包


因为多个项目都会涉及静态资源转换的问题,所以我将此脚本封装为npm包,并提炼了 transform build 命令,只需执行该命令,即可完成资源转换,以下是源码分享:


cli.ts


import { Command } from 'commander';
import { version } from '../package.json';
import buildAction from './transform';
const program = new Command();

program
.command('build')
.description('transform assets and code')
.option(
'--replaceStr <path>',
'[string] 需要全局替换的字符串,默认值: https://zkly-fe-resource.oss-cn-beijing.aliyuncs.com/safeis-web-manage',
)
.option('--imgInfoFilePath <path>', '[string] 统一的静态资源文件路径 默认值: src/scripts/assets.ts')
.option('--replaceToCode <path>', '[string] 替换为的代码 默认值: @/assets')
.option('--assetsDir <path>', '[string] 静态资源文件目录 默认值: src/assets')
.action(options => {
buildAction(options);
});

program.version(version);

program.parse();

transfrom.ts


import { parse } from '@babel/parser';
import generate from '@babel/generator';
import chalk from 'chalk';
import { globSync } from 'glob';
import fs from 'fs';

interface Options {
replaceStr?: string;
imgInfoFilePath?: string;
replaceToCode?: string;
assetsDir?: string;
}

let replaceStr = 'https://zkly-fe-resource.oss-cn-beijing.aliyuncs.com/safeis-web-manage';
let imgInfoFilePath = 'src/scripts/assets.ts';
let replaceToCode = '@/assets';
let assetsDir = './src/assets';

function checkAssetsDir() {
logInfo('检查 src/assets 目录是否存在');

if (!fs.existsSync(assetsDir)) {
logError('assets 目录不存在,请先联系相关人员下载对应项目的静态资源文件,并放置在 src/assets 目录下');
} else {
logSuccess('assets 目录存在');
}
}

function babelTransformCode() {
logInfo(`开始转换 ${imgInfoFilePath} 文件`);
try {
const code = fs.readFileSync(imgInfoFilePath, 'utf-8');

// 解析AST
const ast = parse(code, { sourceType: 'module', plugins: ['typescript'] });

// 遍历VariableDeclarator节点
ast.program.body.forEach(node => {
if (node.type === 'VariableDeclaration') {
// 构建导入声明
const importDecl = {
type: 'ImportDeclaration',
specifiers: [],
source: {
type: 'StringLiteral',
},
};

// @ts-ignore
node.declarations.forEach(decl => {
// @ts-ignore
const localName = decl.id.name;

// @ts-ignore
const filePath = `${replaceToCode}${decl?.init?.quasis?.[1]?.value?.raw}`;

// @ts-ignore
logInfo(`替换 ${replaceStr}${decl?.init?.quasis?.[1]?.value?.raw}${filePath}`);

// 构建导入规范
// @ts-ignore
importDecl.specifiers.push({
type: 'ImportDefaultSpecifier',
local: {
type: 'Identifier',
name: localName,
},
});

// 修改初始化为相对路径
// @ts-ignore
importDecl.source.value = filePath;
});

// 用importDecl替换原变量声明节点
Object.assign(node, importDecl);
}
});

// 最终代码
// @ts-ignore
const result = generate.default(ast, {}, code);

logInfo(`备份 ${imgInfoFilePath} 文件为 ${imgInfoFilePath}.bak`);

fs.renameSync(imgInfoFilePath, `${imgInfoFilePath}.bak`);

fs.writeFileSync(imgInfoFilePath, result.code);

logSuccess(`转换 ${imgInfoFilePath} 成功`);
} catch (error: any) {
logError(error);
}
}

function replaceHttpsCode() {
logInfo('开始转换 其余文件中引用https导入的静态资源');

try {
// 扫描文件
const files = globSync('./src/**/*.{vue,js,ts,scss,css}', { ignore: 'node_modules/**' });

files.forEach((file: string) => {
// 读取文件内容
let content = fs.readFileSync(file, 'utf8');

if (content.includes(replaceStr)) {
logInfo(`替换 ${file} 中的 ${replaceStr}${replaceToCode}`);
}

// 替换匹配到的字符串
content = content.replace(replaceStr, replaceToCode);

// 保存文件
fs.writeFileSync(file, content);
});

logSuccess('转换完成');
} catch (error: any) {
logError(error);
}
}

function logInfo(info: string) {
console.log(chalk.gray(`[INFO] - 🆕 ${info}`));
}

function logSuccess(info: string) {
console.log(chalk.green(`[SUCCESS] - ✅ ${info}`));
}

function logError(info: string) {
console.log(chalk.red(`[ERROR] - ❌ ${info}`));
}

export default function main(options: Options) {
replaceStr = options.replaceStr || replaceStr;
imgInfoFilePath = options.imgInfoFilePath || imgInfoFilePath;
replaceToCode = options.replaceToCode || replaceToCode;
assetsDir = options.assetsDir || assetsDir;

checkAssetsDir();
babelTransformCode();
replaceHttpsCode();
}

作者:程序员小杨v1
来源:juejin.cn/post/7295276751595798580
收起阅读 »

Flutter开发者,需要会原生吗?-- Android 篇

前言:随着Flutter在国内移动应用的成熟度,大部分企业都开始认可Flutter的可持续发展,逐步引入Flutter技术栈。 由此关于开发人员的技能储备问题,会产生一定的疑问。今天笔者将从我们在OS中应用Flutter的各种玩法,聊聊老生常谈的话题:Flut...
继续阅读 »

前言:随着Flutter在国内移动应用的成熟度,大部分企业都开始认可Flutter的可持续发展,逐步引入Flutter技术栈。

由此关于开发人员的技能储备问题,会产生一定的疑问。今天笔者将从我们在OS中应用Flutter的各种玩法,聊聊老生常谈的话题:Flutter开发者到底需不需要懂原生平台?



缘起


《Flutter开发者需要掌握原生Android吗?》

这个话题跟Flutter与RN对比Flutter会不会凉同属一类,都是前两年社群最喜欢争论的话题。激烈的讨论无非是观望者太多,加之Flutter不成熟,在使用过程中会遇到不少坑。


直到今年3.7.0、3.10.0相继发布,框架改进和社区的丰富,让更多人选择拥抱Flutter,关于此类型的话题才开始沉寂下来。很多招聘网站也直接出现了Flutter开发这个岗位,而且技能也不要求原生,甚至加分项前端的技能。似乎Flutter开发者在开发过程中很少用到原生的技能,然而事实绝非如此。


我专攻Flutter有3年了,期间Android、iOS、Windows应用做过不少,Web、Linux也都略有研究;这次我将直接从Android平台出发,用切身经历来论述下:Flutter开发者,真的需要懂Android。


Flutter只是个UI框架


打开一个Flutter的项目,我们可以看到整个应用其实是基于一个Activity运行的,属于单页应用。


package com.wxq.test

import io.flutter.embedding.android.FlutterActivity

class MainActivity: FlutterActivity() {
}

Activity继承自FlutterActivity,FlutterActivityonCreate内会创建FlutterActivityAndFragmentDelegate


// io/flutter/embedding/android/FlutterActivity.java
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
switchLaunchThemeForNormalTheme();

super.onCreate(savedInstanceState);
// 创建代理,ActivityAndFragment都支持哦
delegate = new FlutterActivityAndFragmentDelegate(this);
delegate.onAttach(this); // 这个方法创建引擎,并且将context吸附上去
delegate.onRestoreInstanceState(savedInstanceState);

lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);

configureWindowForTransparency();

// 设置Activity的View,createFlutterView内部也是调用代理的方法
setContentView(createFlutterView());
configureStatusBarForFullscreenFlutterExperience();
}

这个代理将会通过engineGr0up管理FlutterEngine,通过onAttach创建FlutterEngine,并且运行createAndRunEngine方法


// io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java
void onAttach(@NonNull Context context) {
ensureAlive();

if (flutterEngine == null) {
setupFlutterEngine();
}

if (host.shouldAttachEngineToActivity()) {

Log.v(TAG, "Attaching FlutterEngine to the Activity that owns this delegate.");
flutterEngine.getActivityControlSurface().attachToActivity(this, host.getLifecycle());
}
platformPlugin = host.providePlatformPlugin(host.getActivity(), flutterEngine);

host.configureFlutterEngine(flutterEngine);
isAttached = true;
}

@VisibleForTesting
/* package */ void setupFlutterEngine() {
Log.v(TAG, "Setting up FlutterEngine.");

// 省略处理引擎缓存的代码
String cachedEngineGr0upId = host.getCachedEngineGr0upId();
if (cachedEngineGr0upId != null) {
FlutterEngineGr0up flutterEngineGr0up =
FlutterEngineGr0upCache.getInstance().get(cachedEngineGr0upId);
if (flutterEngineGr0up == null) {
throw new IllegalStateException(
"The requested cached FlutterEngineGr0up did not exist in the FlutterEngineGr0upCache: '"
+ cachedEngineGr0upId
+ "'");
}

// *** 重点 ***
flutterEngine =
flutterEngineGr0up.createAndRunEngine(
addEntrypointOptions(new FlutterEngineGr0up.Options(host.getContext())));
isFlutterEngineFromHost = false;
return;
}

// Our host did not provide a custom FlutterEngine. Create a FlutterEngine to back our
// FlutterView.
Log.v(
TAG,
"No preferred FlutterEngine was provided. Creating a new FlutterEngine for"
+ " this FlutterFragment.");

FlutterEngineGr0up group =
engineGr0up == null
? new FlutterEngineGr0up(host.getContext(), host.getFlutterShellArgs().toArray())
: engineGr0up;
flutterEngine =
group.createAndRunEngine(
addEntrypointOptions(
new FlutterEngineGr0up.Options(host.getContext())
.setAutomaticallyRegisterPlugins(false)
.setWaitForRestorationData(host.shouldRestoreAndSaveState())));
isFlutterEngineFromHost = false;
}

再调用onCreateView创建SurfaceView或者外接纹理TextureView,这个View就是Flutter的赖以绘制的画布。


// io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java
@NonNull
View onCreateView(
LayoutInflater inflater,
@Nullable ViewGr0up container,
@Nullable Bundle savedInstanceState,
int flutterViewId,
boolean shouldDelayFirstAndroidViewDraw)
{
Log.v(TAG, "Creating FlutterView.");
ensureAlive();

if (host.getRenderMode() == RenderMode.surface) {
FlutterSurfaceView flutterSurfaceView =
new FlutterSurfaceView(
host.getContext(), host.getTransparencyMode() == TransparencyMode.transparent);

// Allow our host to customize FlutterSurfaceView, if desired.
host.onFlutterSurfaceViewCreated(flutterSurfaceView);

// Create the FlutterView that owns the FlutterSurfaceView.
flutterView = new FlutterView(host.getContext(), flutterSurfaceView);
} else {
FlutterTextureView flutterTextureView = new FlutterTextureView(host.getContext());

flutterTextureView.setOpaque(host.getTransparencyMode() == TransparencyMode.opaque);

// Allow our host to customize FlutterSurfaceView, if desired.
host.onFlutterTextureViewCreated(flutterTextureView);

// Create the FlutterView that owns the FlutterTextureView.
flutterView = new FlutterView(host.getContext(), flutterTextureView);
}

flutterView.addOnFirstFrameRenderedListener(flutterUiDisplayListener);
// 忽略一些代码...
return flutterView;
}

由此可见,Flutter的引擎实际上是运行在Android提供的View上,这个View必然是设置在Android的组件上,可以是Activity、Framgent,也可以是WindowManager。

这就给我们带来了很大的可塑性,只要你能掌握这套原理,混合开发就随便玩了。


Android,是必须的能力


通过对Flutter运行机制的剖析,我们很明确它就是个单纯的UI框架,惊艳的跨端UI都离不开Android的能力,这也说明Flutter开发者不需要会原生注定走不远

下面几个例子,也可以充分论证这个观点。


一、Flutter插件从哪里来


上面讲述到的原理,Flutter项目脚手架已经帮我们做好,但这只是UI绘制层面的;实际上很多Flutter应用,业务能力都是由Pub.dev提供的,随着社区框架的增多,开发者大多时候是感知不到需要Android能力的。

然而业务的发展是迅速的,我们开始需要很多pub社区并不支持的能力,比如:getMetaDatagetMacAddressreboot/shutdownsendBroadcast等,这些能力都需要我们使用Android知识,以编写插件的形式,提供给Flutter调用。

Flutter Plugin在Dart层和Android层都实现了MethodChannel对象,同一个Engine下,只要传入一致的channelId字符串,就能建立双向的通道互相传输基本类型数据。


class FlutterNativeAbilityPlugin : FlutterPlugin, MethodCallHandler {
private var applicationContext: Context? = null

private lateinit var channel: MethodChannel

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
applicationContext = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_native_ability")
channel.setMethodCallHandler(this)
}

class MethodChannelFlutterNativeAbility extends FlutterNativeAbilityPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('flutter_native_ability');
}

发送端通过invokeMethod调用对应的methodName,传入arguments;接收端通过实现onMethodCall方法,接收发送端的invokeMethod操作,执行需要的操作后,通过Result对象返回结果。


@override
Future<String> getMacAddress() async {
final res = await methodChannel.invokeMethod<String>('getMacAddress');
return res ?? '';
}

@override
Future<void> reboot() async {
await methodChannel.invokeMethod<String>('reboot');
}

"getMacAddress" -> {
Log.i(TAG, "onMethodCall: getMacAddress")
val macAddress = CommonUtils().getDeviceMac(applicationContext)
result.success(macAddress)
}
"reboot" -> {
Log.i(TAG, "onMethodCall: reboot")
beginToReboot(applicationContext)
result.success(null)
}

ps:invokeMethod和onMethodCall双端都能实现,都能作为发送端和接收端。


二、Flutter依赖于Android机制,得以“横行霸道”


目前我们将Flutter应用于OS的开发,这需要我们不单是从某个独立应用去思考。很多应用、服务都需要从整个系统业务去设计,在以下这些需求中,我们深切感受到:Flutter跟Android配合后,能发挥更大的业务价值。



  • Android服务运行dart代码,广播接收器与Flutter通信


我们很多服务需要开机自启,这必须遵循Android的机制。通常做法是:接收开机广播,在广播接收器中启动Service,然后再去运行DartEngie,执行跨平台的代码;


class MyTestService : Service() {

private lateinit var engineGr0up: FlutterEngineGr0up

override fun onCreate() {
super.onCreate()
startForeground()

engineGr0up = FlutterEngineGr0up(this)
// initService是Flutter层的方法入口点
val dartEntrypoint = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"initService"
)
val flutterEngine = engineGr0up.createAndRunEngine(this, dartEntrypoint)
// Flutter调用Native方法的 MethodChannel 也初始化一下,调用安装接口需要
FlutterToNativeChannel(flutterEngine, this)
}
}

同时各应用之间需要通信,这时我们也会通过Broadcat广播机制,在Android的广播接收器中,通过MechodChannel发送给Flutter端。


总而言之,我们必须 遵循系统的组件规则,基于Flutter提供的通信方式,将Android的消息、事件等发回给Flutter, 带来的跨端效益是实实在在的!



  • 悬浮窗需求


悬浮窗口在视频/直播场景下用的最多,当你的应用需要开启悬浮窗的时候,Flutter将完全无法支持这个需求。

实际上我们只需要在Android中创建一个WindowManager,基于EngineGround创建一个DartEngine;然后创建flutterView,把DartEngine吸附到flutterView上,最后把flutterView Add to WindowManager即可。


private lateinit var flutterView: FlutterView
private var windowManager = context.getSystemService(Service.WINDOW_SERVICE) as WindowManager
private val inflater =
context.getSystemService(Service.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private val metrics = DisplayMetrics()

@SuppressLint("InflateParams")
private var rootView = inflater.inflate(R.layout.floating, null, false) as ViewGr0up

windowManager.defaultDisplay.getMetrics(metrics)
layoutParams.gravity = Gravity.START or Gravity.TOP

windowManager.addView(rootView, layoutParams)

flutterView = FlutterView(inflater.context, FlutterSurfaceView(inflater.context, true))
flutterView.attachToFlutterEngine(engine)

engine.lifecycleChannel.appIsResumed()

rootView.findViewById<FrameLayout>(R.id.floating_window)
.addView(
flutterView,
ViewGr0up.LayoutParams(
ViewGr0up.LayoutParams.MATCH_PARENT,
ViewGr0up.LayoutParams.MATCH_PARENT
)
)
windowManager.updateViewLayout(rootView, layoutParams)


  • 不再局限单页应用


最近我们在升级应用中,遇到一个比较尴尬的需求:在原有OTA功能下,新增一个U盘插入本地升级的功能,希望升级能力和UI都能复用,且互不影响各自流程。


如果是Android项目很简单,把升级的能力抽象,通过多个Activity管理自己的业务流程,互不干扰。但是Flutter项目属于单页应用,不可能同时展示两个路由页面各自处理,所以也必须 走Android的机制,让Flutter应用同时运行多个Activity。


我们在Android端监听了U盘的插入事件,在需要本地升级的时候直接弹出Activity。Activity是继承FlutterActivity的,通过<metadata>标签指定方法入口点。与MainActivity运行main区分开,然后通过重写getDartEntrypointArgs方法,把必要的参数传给Flutter入口函数,从而独立运行本地升级的业务,而且UI和能力都能复用。


class LocalUpgradeActivity : FlutterActivity() {
}

<activity
android:name=".LocalUpgradeActivity"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:theme="@style/Theme.Transparent"
android:windowSoftInputMode="adjustResize">

<meta-data
android:name="io.flutter.Entrypoint"
android:value="runLocalUpgradeApp" />
<!-- 这里指定Dart层的入口点-->
</activity>

override fun getDartEntrypointArgs(): MutableList<String?> {
val filePath: String? = intent?.getStringExtra("filePath")
val tag: String? = intent?.getStringExtra("tag")
return mutableListOf(filePath, tag)
}

至此,我们的Flutter应用不再是单页应用,而且所有逻辑和UI都将在Flutter层实现!


总结


我们遵循Android平台的机制,把逻辑和UI都尽可能的交给Flutter层,让其在跨平台上发挥更大的可能性,在落地过程确实切身体会到Android的知识是何等的重要!

当然我们的应用场景可能相对复杂,一般应用也许不会有这么多的应用组合;但无论Flutter如何完善,社区更加壮大,它都离不开底层平台的支持。

作为Flutter开发者,有精力的情况下,一定要多学各个平台的框架和能力,让Flutter、更让自己走的更远!


作者:Karl_wei
来源:juejin.cn/post/7295571705689423907
收起阅读 »

正式变实习?掌趣科技这波操作太恶心

掌趣科技作为国内老牌游戏上市公司,按道理应该是不错的,然而拿到了他家的 Offer 之后,却感觉套路满满,非常失望!不知道,大家是否有类似的经历? 1.Offer 展示 2.面试题真题 问项目: 讲讲你最好的一个项目怎么实现的 ? 问八股: JVM 的执行...
继续阅读 »

掌趣科技作为国内老牌游戏上市公司,按道理应该是不错的,然而拿到了他家的 Offer 之后,却感觉套路满满,非常失望!不知道,大家是否有类似的经历?


1.Offer 展示



2.面试题真题


问项目:


讲讲你最好的一个项目怎么实现的 ?


问八股:



  1. JVM 的执行流程?

  2. JVM 是怎么找到一个类的?

  3. JVM 的内存布局?

  4. Java 虚拟机是怎么进行垃圾回收的?

  5. Java 有可能产生内存泄漏吗,举例? 内存泄露会导致什么问题?

  6. GC 的过程, 内存是怎么分配的, 是一片一片的使用呢, 还是一大块的使用? 还是想 C/C++ 那样零散的内存呢?怎么解决内存碎片问题?

  7. Java 中你都用过什么容器?HashMap 和 HashTable 的区别?ConcurrentHashMap 怎么保证线程安全问题的?

  8. 你用过 Java 的多线程锁吗?ReentrantLock 和 Synchronized 的区别?你了解什么是悲观锁?什么是乐观锁吗?

  9. Java 是怎么实现多态的? Java 中能多继承吗? Java 中的注解怎么实现的?

  10. Java 虚拟机是怎么找到一个方法,一个类,怎么根据注解找到对应的方法

  11. Java 中反射的作用,反射怎么实现的?

  12. 反射可以实现注解吗?

  13. 你了解自动拆箱装箱吗? 拆箱装箱怎么实现的?

  14. 项目中有碰见过死锁的问题吗? 什么是死锁? 只锁一个对象会产生死锁吗?

  15. Netty 用过吗?了解过吗?

  16. MySQL 有几种索引? 按字段特性分,按物理存储分呢? 索引底层有几种实现呢? MySQL 的有哪几种存储引擎? InnoDB 和 MyISAM 有什么区别?有什么优缺点?

  17. 做一个查询操作的时候,使用哪种索引?

  18. 什么叫回表查询? 如果没有创建主键,怎么进行回表查询?

  19. 什么叫索引覆盖?

  20. 一个表是索引越多越好,还是越少越好?

  21. 你了解什么叫表锁?什么叫行锁吗?什么情况下使用行锁?什么情况下使用表锁?

  22. 如果一个表的写操作比较多,是加行锁比较好,还是加表锁比较好?

  23. 你能说一下事务的隔离级别吗?

  24. Redis 数据库怎么是实现持久化的 ?Redis 里边都有哪些数据类型? ZSet 怎么用?

  25. 什么是缓存雪崩、缓存击穿、缓存穿透,分别怎么解决?

  26. 做过 Java 的网络编程吗?给我说一下 Socket 套接字的创建流程?

  27. 讲一讲三次握手,四次挥手的流程?TCP 和 UDP 的区别?

  28. 使用 UDP 来传输数据,怎么来保证他的可靠性,就像你刚刚说的后发先至问题?



PS:面试问的还挺细的,但因为做足了准备,所以这些面试题基本都拿下了。


以上问题来自学员的整理,在此感谢。



3.套路来了


拿到 Offer 本来是件开心的事,然而当聊完入职事项之后,整个人就不好了,来看看具体的经过吧:


问题1:正式工作变实习?


秋招明明投递的是正式工作,然而拿到 Offer 之后,HR 却必须先让去实习 5 个月。


投递详情如下:



明确是秋招正式岗位,而非实习。


正常的逻辑应该是拿到 Offer 之后,等明年毕业之后先去公司报道,只有 3 个月试用期,试用期没问题就转为正式员工了。



问题2:实习薪资


实习也是按照实习的工资,而非正式工资的 80% 发放的。


问题3:可能不通过&不谈正式薪资


HR 说明年四月实习期过了之后,再谈正式薪资,言外之意,如果实习期没过,那就不要你了,也就不用谈薪资了。那么请问,投递正式岗位又有什么意义呢?


问题4:实习时间超长


通常实习时间也就是 2-3 个月,而掌趣要求 11 月中旬去实习,至少实习到明年 4 月份,这个实习的时间未免要太长了。


小结


作为国内老牌上市公司,竟然以找正式工作的幌子把入选人悄悄转为实习生,这件事对有经验的人来说,一眼就知道怎么回事,然而对于涉世未深,刚步入社会的年轻人来说,却是满满的套路,还未感受生活的美好,就经历了人心的险恶。


人在做天在看,希望某些公司不要有这样的骚操作,招实习就是招实习,招正式员工就是招正式员工,不要混为一谈,更不要暗箱操作,更不要欺负那些涉世未深的年轻人。


作者:Java中文社群
来源:juejin.cn/post/7292946512585162806
收起阅读 »

登录页面一些有趣的css效果

web
前言 今天无意看到一个登录页,input框focus时placeholder上移变成label的效果,无聊没事干就想着自己来实现一下,登录页面能做文章的,普遍的就是按钮动画,title的动画,以及input的动画,这是最终的效果图(如下), 同时附上预览页以及...
继续阅读 »

前言


今天无意看到一个登录页,inputfocusplaceholder上移变成label的效果,无聊没事干就想着自己来实现一下,登录页面能做文章的,普遍的就是按钮动画,title的动画,以及input的动画,这是最终的效果图(如下), 同时附上预览页以及实现源码


919c40a2a264f683ab5e74e8a649ac5.png


title 的动画实现


首先描述一下大概的实现效果, 我们需要一个镂空的一段白底文字,在鼠标移入时给一个逐步点亮的效果。
文字镂空我们可以使用text-stroke, 逐步点亮只需要使用filter即可


text-stroke


text-stroke属性用于在文本的边缘周围添加描边效果,即文本字符的外部轮廓。这可以用于创建具有描边的文本效果。text-stroke属性通常与-webkit-text-stroke前缀一起使用,因为它目前主要在WebKit浏览器(如Chrome和Safari)中支持


text-stroke属性有两个主要值:



  1. 宽度(width) :指定描边的宽度,可以是像素值、百分比值或其他长度单位。

  2. 颜色(color) :指定描边的颜色,可以使用颜色名称、十六进制值、RGB值等。


filter


filter是CSS属性,用于将图像或元素的视觉效果进行处理,例如模糊、对比度调整、饱和度调整等。它可以应用于元素的背景图像、文本或任何具有视觉内容的元素。


filter属性的值是一个或多个滤镜函数,这些函数以空格分隔。以下是一些常见的滤镜函数和示例:




  1. 模糊(blur) : 通过blur函数可以实现模糊效果。模糊的值可以是像素值或其他长度单位。


    .blurred-image {
    filter: blur(5px);
    }



  2. 对比度(contrast) : 通过contrast函数可以调整对比度。值为百分比,1表示原始对比度。


    .high-contrast-text {
    filter: contrast(150%);
    }



  3. 饱和度(saturate) : 通过saturate函数可以调整饱和度。值为百分比,1表示原始饱和度。


    .desaturated-image {
    filter: saturate(50%);
    }



  4. 反色(invert) : 通过invert函数可以实现反色效果。值为百分比,1表示完全反色。


    .inverted-text {
    filter: invert(100%);
    }



  5. 灰度(grayscale) : 通过grayscale函数可以将图像或元素转换为灰度图像。值为百分比,1表示完全灰度。


    .gray-text {
    filter: grayscale(70%);
    }



  6. 透明度(opacity) : 通过opacity函数可以调整元素的透明度。值为0到1之间的数字,0表示完全透明,1表示完全不透明。


    .semi-transparent-box {
    filter: opacity(0.7);
    }



  7. 阴影(drop-shadow) :用于在图像、文本或其他元素周围添加阴影效果。这个属性在 CSS3 中引入,通常用于创建阴影效果,使元素看起来浮在页面上或增加深度感


    drop-shadow(<offset-x> <offset-y> <blur-radius>? <spread-radius>? <color>?)

    各个值的含义如下:



    • <offset-x>: 阴影在 X 轴上的偏移距离。

    • <offset-y>: 阴影在 Y 轴上的偏移距离。

    • <blur-radius> (可选): 阴影的模糊半径。默认值为 0。

    • <spread-radius> (可选): 阴影的扩散半径。默认值为 0。

    • <color> (可选): 阴影的颜色。默认值为当前文本颜色。




filter属性的支持程度因浏览器而异,因此在使用时应谨慎考虑浏览器兼容性。


实现移入标题点亮的效果


想实现移入标题点亮的效果我们首先需要两个通过定位重叠的span元素,一个做镂空用于展示,另一个作为
hover时覆盖掉镂空元素,并通过filter: drop-shadow实现光影效果,需要注意的是这里需要使用inline元素实现效果。


title-animation.gif


input 的动画实现


input的效果比较简单,只需要在focusspan(placeholder)上移变成span(label)同时给inputborder-bottom做一个底色的延伸,效果确定了接着就看看实现思路。


input placeholder 作为 label


使用div作为容器包裹inputspanspan首先绝对定位到框内,伪装为placeholder, 当input状态为focus提高spantop值,即可伪装成label, 这里有两个问题是:




  1. 当用户输入了值的时候,span并不需要恢复为之前的top, 这里我们使用css或者js 去判断都可以, js就是拿到输入框的值,这里不多做赘述,css 有个比较巧妙的做法, 给input required属性值设置为required, 这样可以使用css:valid伪类去判断input是否有值。




  2. 由于span层级高于input,当点击span时无法触发input的聚焦,这个问题我们可以使用pointer-events: none; 来解决。pointer-events 是一个CSS属性,用于控制元素是否响应用户的指针事件(例如鼠标点击、悬停、触摸等)。这个属性对于控制元素的可交互性和可点击性非常有用。


    pointer-events 具有以下几个可能的值:



    1. auto(默认值):元素会按照其正常行为响应用户指针事件。这是默认行为。

    2. none:元素不会响应用户的指针事件,就好像它不存在一样。用户无法与它交互。

    3. visiblePainted:元素在绘制区域上响应指针事件,但不在其透明区域上响应。这使得元素的透明部分不会响应事件,而其他部分会。

    4. visibleFill:元素在其填充区域上响应指针事件,但不在边框区域上响应。

    5. visibleStroke:元素在其边框区域上响应指针事件,但不在填充区域上响应。

    6. painted:元素会在其绘制区域上响应指针事件,包括填充、边框和透明区域。

    7. fill:元素在其填充区域上响应指针事件,但不在边框区域上响应。

    8. stroke:元素在其边框区域上响应指针事件,但不在填充区域上响应。




pointer-events 属性非常有用,特别是在创建交互性复杂的用户界面时,可以通过它来控制元素的响应区域。例如,你可以使用它来创建自定义的点击区域,而不仅仅是元素的边界。它还可以与其他CSS属性和JavaScript事件处理程序结合使用,以创建特定的交互效果。


input border bottom 延伸展开效果


效果比较简单,input被聚焦的时候,一个紫色的边从中间延伸覆盖白色的底边即可。 在使用一个span作为底部的边, 初始不可见, focus时从中间向两边延伸直至充满, 唯一头痛的就是怎么从中间向两边延伸,这里可以使用transform变形,首先使用transform: scaleX(0);达到不可见的效果, 然后设置变形原点为中间transform-origin: center;,这样效果就可以实现了


input 的动画实现效果


input-animation.gif


按钮的动画实现


关于按钮的动画很多,我们这里就实现一个移入的散花效果,移入时发散出一些星星,这里需要使用到动画去实现了,首先通过伪类创建一些周边元素,这里需要用到 background-image(radial-gradient)


background-image(radial-gradient)


background-image 属性用于设置元素的背景图像,而 radial-gradient 是一种 CSS 渐变类型,可用于创建径向渐变背景。这种径向渐变背景通常以一个中心点为基础,然后颜色渐变向外扩展,形成一种放射状的效果。


radial-gradient 的语法如下:


background-image: radial-gradient([shape] [size] at [position], color-stop1, color-stop2, ...);


  • [shape]: 可选,指定渐变的形状。常用的值包括 "ellipse"(椭圆)和 "circle"(圆形)。

  • [size]: 可选,指定渐变的大小。可以是长度值或百分比值。

  • at [position]: 可选,指定渐变的中心点位置。

  • color-stopX: 渐变的颜色停止点,可以是颜色值、百分比值或长度值。


按钮移入动画效果实现


btn-animation.gif


结尾


css 能实现的效果越来越多了,遇到有趣的效果,可以自己想想实现方式以及动手实现一下,思路毕竟是思路,具体实现起来说不定会遇到什么坑,逐步解决问题带来的成就感满足感还是很强的。


作者:刘圣凯
来源:juejin.cn/post/7294908459002331171
收起阅读 »

HarmonyOS开发:基于http开源一个网络请求库

前言 网络封装的目的,在于简洁,使用起来更加的方便,也易于我们进行相关动作的设置,如果,我们不封装,那么每次请求,就会重复大量的代码逻辑,如下代码,是官方给出的案例: // 引入包名 import http from '@ohos.net.http'; //...
继续阅读 »

前言


网络封装的目的,在于简洁,使用起来更加的方便,也易于我们进行相关动作的设置,如果,我们不封装,那么每次请求,就会重复大量的代码逻辑,如下代码,是官方给出的案例:


// 引入包名
import http from '@ohos.net.http';

// 每一个httpRequest对应一个HTTP请求任务,不可复用
let httpRequest = http.createHttp();
// 用于订阅HTTP响应头,此接口会比request请求先返回。可以根据业务需要订阅此消息
// 从API 8开始,使用on('headersReceive', Callback)替代on('headerReceive', AsyncCallback)。 8+
httpRequest.on('headersReceive', (header) => {
console.info('header: ' + JSON.stringify(header));
});
httpRequest.request(
// 填写HTTP请求的URL地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
"EXAMPLE_URL",
{
method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
// 开发者根据自身业务需要添加header字段
header: {
'Content-Type': 'application/json'
},
// 当使用POST请求时此字段用于传递内容
extraData: {
"data": "data to send",
},
expectDataType: http.HttpDataType.STRING, // 可选,指定返回数据的类型
usingCache: true, // 可选,默认为true
priority: 1, // 可选,默认为1
connectTimeout: 60000, // 可选,默认为60000ms
readTimeout: 60000, // 可选,默认为60000ms
usingProtocol: http.HttpProtocol.HTTP1_1, // 可选,协议类型默认值由系统自动指定
}, (err, data) => {
if (!err) {
// data.result为HTTP响应内容,可根据业务需要进行解析
console.info('Result:' + JSON.stringify(data.result));
console.info('code:' + JSON.stringify(data.responseCode));
// data.header为HTTP响应头,可根据业务需要进行解析
console.info('header:' + JSON.stringify(data.header));
console.info('cookies:' + JSON.stringify(data.cookies)); // 8+
// 取消订阅HTTP响应头事件
httpRequest.off('headersReceive');
// 当该请求使用完毕时,调用destroy方法主动销毁
httpRequest.destroy();
} else {
console.info('error:' + JSON.stringify(err));
// 取消订阅HTTP响应头事件
httpRequest.off('headersReceive');
// 当该请求使用完毕时,调用destroy方法主动销毁。
httpRequest.destroy();
}
}
);

以上的案例,每次请求书写这么多代码,在实际的开发中,是无法承受的,所以基于此,封装是很有必要的,把公共的部分进行抽取包装,固定不变的参数进行初始化设置,重写基本的请求方式,这是我们封装的基本宗旨。


我们先看一下封装之后的调用方式:


异步请求


Net.get("url").requestString((data) => {
//data 为 返回的json字符串
})

同步请求


const data = await Net.get("url").returnData<string>(ReturnDataType.STRING)
//data 为 返回的json字符串

装饰器请求


@GET("url")
private getData():Promise<string> {
return null
}

封装之后,不仅使用起来更加的便捷,而且还拓展了请求类型,满足不同需求的场景。


本篇的文章内容大致如下:


1、net库主要功能点介绍


2、net库快速依赖使用


3、net库全局初始化


4、异步请求介绍


5、同步请求介绍


6、装饰器请求介绍


7、上传下载介绍


8、Dialog加载使用


9、相关总结


一、net库主要功能点介绍


目前net库一期已经开发完毕,har包使用,大家可以看第二项,截止到发文前,所支持的功能如下:


■ 支持全局初始化


■ 支持统一的BaseUrl


■ 支持全局错误拦截


■ 支持全局头参拦截


■ 支持同步方式请求(get/post/delete/put/options/head/trace/connect)


■ 支持异步方式请求(get/post/delete/put/options/head/trace/connect)


■ 支持装饰器方式请求(get/post/delete/put/options/head/trace/connect)


■ 支持dialog加载


■ 支持返回Json字符串


■ 支持返回对象


■ 支持返回数组


■ 支持返回data一层数据


■ 支持上传文件


■ 支持下载文件


□ 数据缓存开发中……


二、net库快速依赖使用


私服和远程依赖,由于权限和审核问题,预计需要等到2024年第一季度面向所有开发者,所以,只能使用本地静态共享包和源码 两种使用方式,本地静态共享包类似Android中的aar依赖,直接复制到项目中即可,目前源码还在优化中,先暴露静态共享包这一使用方式。


本地静态共享包har包使用


首先,下载har包,点击下载


下载之后,把har包复制项目中,目录自己创建,如下,我创建了一个libs目录,复制进去



引入之后,进行同步项目,点击Sync Now即可,当然了你也可以,将鼠标放置在报错处会出现提示,在提示框中点击Run 'ohpm install'。


需要注意,@app/net,是用来区分目录的,可以自己定义,比如@aa/bb等,关于静态共享包的创建和使用,请查看如下我的介绍,这里就不过多介绍。


HarmonyOS开发:走进静态共享包的依赖与使用


查看是否引用成功


无论使用哪种方式进行依赖,最终都会在使用的模块中,生成一个oh_modules文件,并创建源代码文件,有则成功,无则失败,如下:



三、net库全局初始化


推荐在AbilityStage进行初始化,初始化一次即可,初始化参数可根据项目需要进行选择性使用。


Net.getInstance().init({
baseUrl: "https://www.vipandroid.cn", //设置全局baseurl
connectTimeout: 10000, //设置连接超时
readTimeout: 10000, //设置读取超时
netErrorInterceptor: new MyNetErrorInterceptor(), //设置全局错误拦截,需要自行创建,可在这里进行错误处理
netHeaderInterceptor: new MyNetHeaderInterceptor(), //设置全局头拦截器,需要自行创建
header: {}, //头参数
resultTag: []//接口返回数据参数,比如data,items等等
})

1、初始化属性介绍


初始化属性,根据自己需要选择性使用。


属性类型概述
baseUrlstring一般标记为统一的请求前缀,也就是域名
connectTimeoutnumber连接超时,默认10秒
readTimeoutnumber读取超时,默认10秒
netErrorInterceptorINetErrorInterceptor全局错误拦截器,需继承INetErrorInterceptor
netHeaderInterceptorINetHeaderInterceptor全局请求头拦截器,需继承INetHeaderInterceptor
headerObject全局统一的公共头参数
resultTagArray接口返回数据参数,比如data,items等等

2、设置请求头拦截


关于全局头参数传递,可以通过以上的header参数或者在请求头拦截里均可,如果没有同步等逻辑操作,只是固定的头参数,建议直接使用header参数。


名字自定义,实现INetHeaderInterceptor接口,可在netHeader方法里打印请求头或者追加请求头。


import { HttpHeaderOptions, NetHeaderInterceptor } from '@app/net'

class MyNetHeaderInterceptor implements NetHeaderInterceptor {
getHeader(options: HttpHeaderOptions): Promise<Object> {
//可以进行接口签名,传入头参数
return null
}
}

HttpHeaderOptions对象


返回了一些常用参数,可以用于接口签名等使用。


export class HttpHeaderOptions {
url?: string //请求地址
method?: http.RequestMethod //请求方式
header?: Object //头参数
params?: Object //请求参数
}

3、设置全局错误拦截器


名字自定义,实现INetErrorInterceptor接口,可在httpError方法里进行全局的错误处理,比如统一跳转,统一提示等。


import { NetError } from '@app/net/src/main/ets/error/NetError';
import { INetErrorInterceptor } from '@app/net/src/main/ets/interceptor/INetErrorInterceptor';

export class MyNetErrorInterceptor implements INetErrorInterceptor {
httpError(error: NetError) {
//这里进行拦截错误信息

}
}

NetError对象


可通过如下方法获取错误code和错误描述信息。


/*
* 返回code
* */

getCode():number{
return this.code
}

/*
* 返回message
* */

getMessage():string{
return this.message
}

四、异步请求介绍


1、请求说明


为了方便数据的针对性返回,目前异步请求提供了三种请求方法,在实际的 开发中,大家可以针对需要,选择性使用。


request方法


Net.get("url").request<TestModel>((data) => {
//data 就是返回的TestModel对象
})

此方法,针对性返回对应的data数据对象,如下json,则会直接返回需要的data对象,不会携带外层的code等其他参数,方便大家直接的拿到数据。


{
"code": 0,
"message": "数据返回成功",
"data": {}
}

如果你的data是一个数组,如下json:


{
"code": 0,
"message": "数据返回成功",
"data": []
}

数组获取


Net.get("url").request<TestModel[]>((data) => {
//data 就是返回的TestModel[]数组
})

//或者如下

Net.get("url").request<Array<TestModel>>((data) => {
//data 就是返回的TestModel数组
})

可能大家有疑问,如果接口返回的json字段不是data怎么办?如下:


举例一


{
"code": 0,
"message": "数据返回成功",
"items": {}
}

举例二


{
"code": 0,
"message": "数据返回成功",
"models": {}
}

虽然网络库中默认取的是json中的data字段,如果您的数据返回类型字段有多种,如上json,可以通过全局初始化resultTag进行传递或者局部setResultTag传递即可。


全局设置接口返回数据参数【推荐】


全局设置,具体设置请查看上边的全局初始化一项,只设置一次即可,不管你有多少种返回参数,都可以统一设置。


 Net.getInstance().init({
resultTag: ["data", "items", "models"]//接口返回数据参数,比如data,items等等
})

局部设置接口返回数据参数


通过setResultTag方法设置即可。


Net.get("")
.setResultTag(["items"])
.request<TestModel>((data) => {

})

requestString方法


requestString就比较简单,就是普通的返回请求回来的json字符串。


Net.get("url").requestString((data) => {
//data 为 返回的json字符串
})

requestObject方法


requestObject方法也是获取对象,和request不同的是,它不用设置返回参数,因为它是返回的整个json对应的对象, 也就是包含了code,message等字段。


Net.get("url").requestObject<TestModel>((data) => {
//data 为 返回的TestModel对象
})

为了更好的复用共有字段,你可以抽取一个基类,如下:


export class ApiResult<T> {
code: number
message: string
data: T
}

以后就可以如下请求:


Net.get("url").requestObject<ApiResult<TestModel>>((data) => {
//data 为 返回的ApiResult对象
})

回调函数

回调函数有两个,一个成功一个失败,成功回调必调用,失败可选择性调用。


只带成功


Net.get("url").request<TestModel>((data) => {
//data 为 返回的TestModel对象
})

成功失败都带


Net.get("url").request<TestModel>((data) => {
//data 为 返回的TestModel对象
}, (error) => {
//失败
})

2、get请求


 Net.get("url").request<TestModel>((data) => {
//data 为 返回的TestModel对象
})

3、post请求


Net.post("url").request<TestModel>((data) => {
//data 为 返回的TestModel对象
})

4、delete请求


 Net.delete("url").request<TestModel>((data) => {
//data 为 返回的TestModel对象
})

5、put请求


Net.put("url").request<TestModel>((data) => {
//data 为 返回的TestModel对象
})

6、其他请求方式


除了常见的请求之外,根据系统api所提供的,也封装了如下的请求方式,只需要更改请求方式即可,比如Net.options。


OPTIONS
HEAD
TRACE
CONNECT

7、各个方法调用


除了正常的请求方式之外,你也可以调用如下的参数:


方法类型概述
setHeadersObject单独添加请求头参数
setBaseUrlstring单独替换BaseUrl
setParamsstring / Object / ArrayBuffer单独添加参数,用于post
setConnectTimeoutnumber单独设置连接超时
setReadTimeoutnumber单独设置读取超时
setExpectDataTypehttp.HttpDataType设置指定返回数据的类型
setUsingCacheboolean使用缓存,默认为true
setPrioritynumber设置优先级 默认为1
setUsingProtocolhttp.HttpProtocol协议类型默认值由系统自动指定
setResultTagArray接口返回数据参数,比如data,items等等
setContextContext设置上下文,用于下载文件
setCustomDialogControllerCustomDialogController传递的dialog控制器,用于展示dialog

代码调用如下:


Net.get("url")
.setHeaders({})//单独添加请求头参数
.setBaseUrl("")//单独替换BaseUrl
.setParams({})//单独添加参数
.setConnectTimeout(10000)//单独设置连接超时
.setReadTimeout(10000)//单独设置读取超时
.setExpectDataType(http.HttpDataType.OBJECT)//设置指定返回数据的类型
.setUsingCache(true)//使用缓存,默认为true
.setPriority(1)//设置优先级 默认为1
.setUsingProtocol(http.HttpProtocol.HTTP1_1)//协议类型默认值由系统自动指定
.setResultTag([""])//接口返回数据参数,比如data,items等等
.setContext(this.context)//设置上下文,用于上传文件和下载文件
.setCustomDialogController()//传递的dialog控制器,用于展示dialog
.request<TestModel>((data) => {
//data 为 返回的TestModel对象
})

五、同步请求介绍


同步请求需要注意,需要await关键字和async关键字结合使用。


 private async getTestModel(){
const testModel = await Net.get("url").returnData<TestModel>()
}

1、请求说明


同步请求和异步请求一样,也是有三种方式,是通过参数的形式,默认直接返回data层数据。


返回data层数据


和异步种的request方法类似,只返回json种的data层对象数据,不会返回code等字段。


 private async getData(){
const data = await Net.get("url").returnData<TestModel>()
//data为 返回的 TestModel对象
}

返回Json对象


和异步种的requestObject方法类似,会返回整个json对象,包含code等字段。


 private async getData(){
const data = await Net.get("url").returnData<TestModel>(ReturnDataType.OBJECT)
//data为 返回的 TestModel对象
}

返回Json字符串


和异步种的requestString方法类似。


private async getData(){
const data = await Net.get("url").returnData<string>(ReturnDataType.STRING)
//data为 返回的 json字符串
}

返回错误


异步方式有回调错误,同步方式如果发生错误,也会直接返回错误,结构如下:


{
"code": 0,
"message": "错误信息"
}

除了以上的错误捕获之外,你也可以全局异常捕获,


2、get请求



const data = await Net.get("url").returnData<TestModel>()

3、post请求



const data = await Net.post("url").returnData<TestModel>()

4、delete请求



const data = await Net.delete("url").returnData<TestModel>()

5、put请求



const data = await Net.put("url").returnData<TestModel>()

6、其他请求方式


除了常见的请求之外,根据系统api所提供的,也封装了如下的请求方式,只需要更改请求方式即可,比如Net.options


OPTIONS
HEAD
TRACE
CONNECT

7、各个方法调用


除了正常的请求方式之外,你也可以调用如下的参数:


方法类型概述
setHeadersObject单独添加请求头参数
setBaseUrlstring单独替换BaseUrl
setParamsstring / Object / ArrayBuffer单独添加参数,用于post
setConnectTimeoutnumber单独设置连接超时
setReadTimeoutnumber单独设置读取超时
setExpectDataTypehttp.HttpDataType设置指定返回数据的类型
setUsingCacheboolean使用缓存,默认为true
setPrioritynumber设置优先级 默认为1
setUsingProtocolhttp.HttpProtocol协议类型默认值由系统自动指定
setResultTagArray接口返回数据参数,比如data,items等等
setContextContext设置上下文,用于下载文件
setCustomDialogControllerCustomDialogController传递的dialog控制器,用于展示dialog

代码调用如下:


const data = await Net.get("url")
.setHeaders({})//单独添加请求头参数
.setBaseUrl("")//单独替换BaseUrl
.setParams({})//单独添加参数
.setConnectTimeout(10000)//单独设置连接超时
.setReadTimeout(10000)//单独设置读取超时
.setExpectDataType(http.HttpDataType.OBJECT)//设置指定返回数据的类型
.setUsingCache(true)//使用缓存,默认为true
.setPriority(1)//设置优先级 默认为1
.setUsingProtocol(http.HttpProtocol.HTTP1_1)//协议类型默认值由系统自动指定
.setResultTag([""])//接口返回数据参数,比如data,items等等
.setContext(this.context)//设置上下文,用于上传文件和下载文件
.setCustomDialogController()//传递的dialog控制器,用于展示dialog
.returnData<TestModel>()
//data为 返回的 TestModel对象

六、装饰器请求介绍


网络库允许使用装饰器的方式发起请求,也就是通过注解的方式,目前采取的是装饰器方法的形式。


1、请求说明


装饰器和同步异步有所区别,只返回两种数据类型,一种是json字符串,一种是json对象,暂时不提供返回data层数据。 在使用的时候,您可以单独创建工具类或者ViewModel或者直接使用,都可以。


返回json字符串


@GET("url")
private getData():Promise<string> {
return null
}

返回json对象


@GET("url")
private getData():Promise<TestModel> {
return null
}

2、get请求


@GET("url")
private getData():Promise<TestModel> {
return null
}

3、post请求


@POST("url")
private getData():Promise<TestModel> {
return null
}

4、delete请求


@DELETE("url")
private getData():Promise<TestModel> {
return null
}

5、put请求


@PUT("url")
private getData():Promise<TestModel> {
return null
}

6、其他请求方式


除了常见的请求之外,根据系统api所提供的,也封装了如下的请求方式,只需要更改请求方式即可,比如@OPTIONS。


OPTIONS
HEAD
TRACE
CONNECT

当然,大家也可以使用统一的NET装饰器,只不过需要自己设置请求方法,代码如下:


@NET("url", { method: http.RequestMethod.POST })
private getData():Promise<string> {
return null
}

7、装饰器参数传递


直接参数传递


直接参数,在调用装饰器请求时,后面添加即可,一般针对固定参数。


@GET("url", {
baseUrl: "", //baseUrl
header: {}, //头参数
params: {}, //入参
connectTimeout: 1000, //连接超时
readTimeout: 1000, //读取超时
isReturnJson: true//默认false 返回Json字符串,默认返回json对象
})
private getData():Promise<string> {
return null
}

动态参数传递


动态参数适合参数可变的情况下传递,比如分页等情况。


@GET("url")
private getData(data? : HttpOptions):Promise<string> {
return null
}

调用时传递


private async doHttp(){
const data = await this.getData({
baseUrl: "", //baseUrl
header: {}, //头参数
params: {}, //入参
connectTimeout: 1000, //连接超时
readTimeout: 1000, //读取超时
isReturnJson: true//默认false 返回Json字符串,默认返回json对象
})
}

装饰器参数传递


使用DATA装饰器,DATA必须在上!


@DATA({
baseUrl: "", //baseUrl
header: {}, //头参数
params: {}, //入参
connectTimeout: 1000, //连接超时
readTimeout: 1000, //读取超时
isReturnJson: true//默认false 返回Json字符串,默认返回json对象
})
@GET("url")
private getData():Promise<string> {
return null
}

七、上传下载介绍


1、上传文件


Net.uploadFile("")//上传的地址
.setUploadFiles([])//上传的文件 [{ filename: "test", name: "test", uri: "internal://cache/test.jpg", type: "jpg" }]
.setUploadData([])//上传的参数 [{ name: "name123", value: "123" }]
.setProgress((receivedSize, totalSize) => {
//监听上传进度
})
.request((data) => {
if (data == UploadTaskState.COMPLETE) {
//上传完成
}
})

方法介绍


方法类型概述
uploadFilestring上传的地址
setUploadFilesArray上传的文件数组
setUploadDataArray上传的参数数组
setProgress回调函数监听进度,receivedSize下载大小, totalSize总大小
request请求上传,data类型为UploadTaskState,有三种状态:START(开始),COMPLETE(完成),ERROR(错误)

其他方法


删除上传进度监听

uploadRequest.removeProgressCallback()

删除上传任务

uploadRequest.deleteUploadTask((result) => {
if (result) {
//成功
} else {
//失败
}
})

2、下载文件


Net.downLoadFile("http://10.47.24.237:8888/harmony/log.har")
.setContext(EntryAbility.context)
.setFilePath(EntryAbility.filePath)
.setProgress((receivedSize, totalSize) => {
//监听下载进度
})
.request((data) => {
if (data == DownloadTaskState.COMPLETE) {
//下载完成
}
})

方法介绍


方法类型概述
downLoadFilestring下载的地址
setContextContext上下文
setFilePathstring下载后保存的路径
setProgress回调函数监听进度,receivedSize下载大小, totalSize总大小
request请求下载,data类型为DownloadTaskState,有四种状态:START(开始),COMPLETE(完成),PAUSE(暂停),REMOVE(结束)

其他方法


移除下载的任务

    downLoadRequest.deleteDownloadTask((result) => {
if (result) {
//移除成功
} else {
//移除失败
}
})

暂停下载任务

downLoadRequest.suspendDownloadTask((result) => {
if (result) {
//暂停成功
} else {
//暂停失败
}
})

重新启动下载任务

downLoadRequest.restoreDownloadTask((result) => {
if (result) {
//成功
} else {
//失败
}
})

删除监听下载进度

downLoadRequest.removeProgressCallback()

八、Dialog加载使用



1、定义dialog控制器


NetLoadingDialog是net包中自带的,菊花状弹窗,如果和实际业务不一致,可以更换。


private mCustomDialogController = new CustomDialogController({
builder: NetLoadingDialog({
loadingText: '请等待...'
}),
autoCancel: false,
customStyle: true
})

2、调用传递控制器方法


此方法会自动显示和隐藏dialog,如果觉得不合适,大家可以自己定义即可。


setCustomDialogController(this.mCustomDialogController)

九、相关总结


开发环境如下:


DevEco Studio 4.0 Beta2,Build Version: 4.0.0.400

Api版本:9

hvigorVersion:3.0.2

目前呢,暂时不支持缓存,后续会逐渐加上,大家在使用的过程中,需要任何的问题,都可以进行反馈,都会第一时间进行解决。


作者:程序员一鸣
来源:juejin.cn/post/7295397683397181450
收起阅读 »

工作六年,看到这样的代码,内心五味杂陈......

工作六年,看到这样的代码,内心五味杂陈...... 那天下午,看到了令我终生难忘的代码,那一刻破防了...... 🔊 本文记录那些年的 Java 代码轶事 ヾ(•ω•`)🫥 故事还得从半年前数据隔离的那个事情说起...... 📖一、历史背景 1.1 数据隔...
继续阅读 »

工作六年,看到这样的代码,内心五味杂陈......


那天下午,看到了令我终生难忘的代码,那一刻破防了......



🔊 本文记录那些年的 Java 代码轶事



ヾ(•ω•`)🫥 故事还得从半年前数据隔离的那个事情说起......


📖一、历史背景


1.1 数据隔离


预发,灰度,线上环境共用一个数据库。每一张表有一个 env 字段,环境不同值不同。特别说明: env 字段即环境字段。如下图所示:
image.png


1.2 隔离之前


🖌️插曲:一开始只有 1 个核心表有 env 字段,其他表均无该字段;
有一天预发环境的操作影响到客户线上的数据。 为了彻底隔离,剩余的二十几个表均要添加上环境隔离字段。


当时二十几张表已经大量生产数据,隔离需要做好兼容过渡,保障数据安全。


1.3 隔离改造


其他表历史数据很难做区分,于是新增加的字段 env 初始化 all ,表示预发线上都能访问。以此达到历史数据的兼容。


每一个环境都有一个自己独立标志;从 application.properties 中读该字段;最终到数据库执行的语句如下:


SELECT XXX FROM tableName WHERE env = ${环境字段值} and ${condition}

1.4 隔离方案


最拉胯的做法:每一张表涉及到的 DO、Mapper、XML等挨个添加 env 字段。但我指定不能这么干!!!


image.png


具体方案:自定义 mybatis 拦截器进行统一处理。 通过这个方案可以解决以下几个问题:



  • 业务代码不用修改,包括 DO、Mapper、XML等。只修改 mybatis 拦截的逻辑。

  • 挨个添加补充字段,工程量很多,出错概率极高

  • 后续扩展容易


1.5 最终落地


在 mybatis 拦截器中, 通过改写 SQL。新增时填充环境字段值,查询时添加环境字段条件。真正实现改一处即可。 考虑历史数据过渡,将 env = ${当前环境}
修改成 en 'all')


SELECT xxx FROM ${tableName} WHERE env in (${当前环境},'all') AND ${其他条件}

具体实现逻辑如下图所示:


image.png



  1. 其中 env 字段是从 application.properties 配置获取,全局唯一,只要环境不同,env 值不同

  2. 借助 JSqlParser 开源工具,改写 sql 语句,修改重新填充、查询拼接条件即可。链接JSQLParser


思路:自定义拦截器,填充环境参数,修改 sql 语句,下面是部分代码示例:


@Intercepts(
{@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})}
)

@Component
public class EnvIsolationInterceptor implements Interceptor {
......
@Override
public Object intercept(Invocation invocation) throws Throwable {
......
if (SqlCommandType.INSERT == sqlCommandType) {
try {
// 重新 sql 执行语句,填充环境参数等
insertMethodProcess(invocation, boundSql);
} catch (Exception exception) {
log.error("parser insert sql exception, boundSql is:" + JSON.toJSONString(boundSql), exception);
throw exception;
}
}

return invocation.proceed();
}
}

一气呵成,完美上线。


image.png


📚二、发展演变


2.1 业务需求


随着业务发展,出现了以下需求:



  • 上下游合作,我们的 PRC 接口在匹配环境上与他们有差异,需要改造


SELECT * FROM ${tableName} WHERE bizId = ${bizId} and env in (?,'all')


  • 有一些环境的数据相互相共享,比如预发和灰度等

  • 开发人员的部分后面,希望在预发能纠正线上数据等


2.2 初步沟通


这个需求的落地交给了来了快两年的小鲜肉。 在开始做之前,他也问我该怎么做;我简单说了一些想法,比如可以跳过环境字段检查,不拼接条件;或者拼接所有条件,这样都能查询;亦或者看一下能不能注解来标志特定方法,你想一想如何实现......


image.png


(●ˇ∀ˇ●)年纪大了需要给年轻人机会。


2.3 勤劳能干


小鲜肉,没多久就实现了。不过有一天下午他遇到了麻烦。他填充的环境字段取出来为 null,看来很久没找到原因,让我帮他看看。(不久前也还教过他 Arthas 如何使用呢,这种问题应该不在话下吧🤔)


2.4 具体实现


大致逻辑:在需要跳过环境条件判断的方法前后做硬编码处理,同环切面逻辑, 一加一删。填充颜色部分为小鲜肉的改造逻辑。


image.png


大概逻辑就是:将 env 字段填充所有环境。条件过滤的忽略的目的。


SELECT * FROM ${tableName} WHERE env in ('pre','gray','online','all') AND ${其他条件}

2.5 错误原因


经过排查是因为 API 里面有多处对 threadLoal 进行处理的逻辑,方法之间存在调用。 简化举例: A 和 B 方法都是独立的方法, A 在调用 B 的过程,B 结束时把上下文环境字段删除, A 在获取时得到 null。具体如下:


image.png


2.6 五味杂陈


当我看到代码的一瞬间,彻底破防了......


image.png


queryProject 方法里面调用 findProjectWithOutEnv,
在两个方法中,都有填充处理 env 的代码。


2.7 遍地开花


然而,这三行代码,随处可见,在业务代码中遍地开花.......


image.png


// 1. 变量保存 oriFilterEnv
String oriFilterEnv = UserHolder.getUser().getFilterEnv();

// 2. 设置值到应用上下文
UserHolder.getUser().setFilterEnv(globalConfigDTO.getAllEnv());

//....... 业务代码 ....

// 3. 结束复原
UserHolder.getUser().setFilterEnv(oriFilterEnv);

image.png


改了个遍,很勤劳👍......


2.8 灵魂开问


image.png


难道真的就只能这么做吗,当然还有......



  • 开闭原则符合了吗

  • 改漏了应该办呢

  • 其他人遇到跳过的检查的场景也加这样的代码吗

  • 业务代码和功能代码分离了吗

  • 填充到应用上下文对象 user 合适吗

  • .......


大量魔法值,单行字符超500,方法长度拖几个屏幕也都睁一眼闭一只眼了,但整这一出,还是破防......


内心涌动😥,我觉得要重构一下。


📒三、重构一下


3.1 困难之处


在 mybatis intercept 中不能直接精准地获取到 service 层的接口调用。 只能通过栈帧查询到调用链。


3.2 问题列表



  • 尽量不要修改已有方法,保证不影响原有逻辑;

  • 尽量不要在业务方法中修改功能代码;关注点分离;

  • 尽量最小改动,修改一处即可实现逻辑;

  • 改造后复用能力,而不是依葫芦画瓢地添加这种代码


3.3 实现分析



  1. 用独立的 ThreadLocal,不与当前用户信息上下文混合使用

  2. 注解+APO,通过注解参数解析,达到目标功能

  3. 对于方法之间的调用或者循环调用,要考虑优化


同一份代码,在多个环境运行,不管如何,一定要考虑线上数据安全性。


3.4 使用案例


改造后的使用案例如下,案例说明:project 表在预发环境校验跳过


@InvokeChainSkipEnvRule(skipEnvList = {"pre"}, skipTableList = {"project"})


@SneakyThrows
@GetMapping("/importSignedUserData")
@InvokeChainSkipEnvRule(skipEnvList = {"pre"}, skipTableList = {"project"})
public void importSignedUserData(
......
HttpServletRequest request,
HttpServletResponse response)
{
......
}

在使用的调用入口处添加注解。


3.5 具体实现



  1. 方法上标记注解, 注解参数定义规则

  2. 切面读取方法上面的注解规则,并传递到应用上下文

  3. 拦截器从应用上下文读取规则进行规则判断


image.png


注解代码


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface InvokeChainSkipEnvRule {

/**
* 是否跳过环境。 默认 true,不推荐设置 false
*
* @return
*/

boolean isKip() default true;

/**
* 赋值则判断规则,否则不判断
*
* @return
*/

String[] skipEnvList() default {};

/**
* 赋值则判断规则,否则不判断
*
* @return
*/

String[] skipTableList() default {};
}


3.6 不足之处



  1. 整个链路上的这个表操作都会跳过,颗粒度还是比较粗

  2. 注解只能在入口处使用,公共方法调用尽量避免


🤔那还要不要完善一下,还有什么没有考虑到的点呢? 拿起手机看到快12点的那一刻,我还是选择先回家了......


📝 四、总结思考


4.1 隔离总结


这是一个很好参考案例:在应用中既做了数据隔离,也做了数据共享。通过自定义拦截器做数据隔离,通过自定注解切面实现数据共享。


4.2 编码总结


同样的代码写两次就应该考虑重构了



  • 尽量修改一个地方,不要写这种边边角角的代码

  • 善用自定义注解,解决这种通用逻辑

  • 可以妥协,但是要有底线

  • ......


4.3 场景总结


简单梳理,自定义注解 + AOP 的场景


场景详细描述
分布式锁通过添加自定义注解,让调用方法实现分布式锁
合规参数校验结合 ognl 表达式,对特定的合规性入参校验校验
接口数据权限对不同的接口,做不一样的权限校验,以及不同的人员身份有不同的校验逻辑
路由策略通过不同的注解,转发到不同的 handler
......

自定义注解很灵活,应用场景广泛,可以多多挖掘。


4.4 反思总结



  • 如果一开始就做好技术方案或者直接使用不同的数据库是否可以解决这个问题

  • 是否可以拒绝那个所谓的需求

  • 先有设计再有编码


4.5 最后感想


image.png



在这个只讲业务结果,不讲技术氛围的环境里,突然有一些伤感;身体已经开始吃不消了,好像也过了那个对技术较真死抠的年纪; 突然一想,这么做的意义又有多大呢?



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

背调,程序员入职的紧箍咒

首先说下,目前,我的表哥自己开一家小的背调公司,所以我在跟他的平时交流中,了解到了背调这个行业的一些信息。 今天跟大家分享出来,给大家在求职路上避避坑。 上周的某天,以前的阿里同事小李跟我说,历经两个月的面试,终于拿到了开水团的offer。我心里由衷地替他高兴...
继续阅读 »

首先说下,目前,我的表哥自己开一家小的背调公司,所以我在跟他的平时交流中,了解到了背调这个行业的一些信息。


今天跟大家分享出来,给大家在求职路上避避坑。


上周的某天,以前的阿里同事小李跟我说,历经两个月的面试,终于拿到了开水团的offer。我心里由衷地替他高兴,赶紧恭喜了他,现在这年头,大厂的offer没这么好拿的。


又过了两周,小张沮丧地跟我说,这家公司是先发offer后背调,结果背调之后,offer GG了,公司HR没有告知他具体原因,只是委婉地说有缘自会再相见。(手动狗头)


我听了,惋惜之余有些惊讶,问了他具体情况。


原来,小李并没有在学历上作假,也没有做合并或隐藏工作经历的事。


他犯错的点是,由于在上家公司,他跟他老板的关系不好,所以他在背调让填写上级领导信息的时候,只写了上级领导的名字,电话留的是他一个同事的。


我听后惋惜地一拍脑门儿,说:“你这么做之前,怎么也不问我一下啊?第三方背调公司进行手机号和姓名核实,都是走系统的,秒出结果。而且,这种手机号的机主姓名造假,背调结果是亮红灯的,必挂。”


小李听后,也是悔得肠子都青了,没办法,只能重新来过了。


我以前招人的时候,遇到过一次这样的情况,当时有个候选人面试通过,发起背调流程。一周后,公司HR给了我一份该候选人背调结果的pdf,上面写着:


“候选人背调信息上提供,原公司上级为郭xx,但经查手机号主为王xx,且候选人原公司并无此人。”


背调结果,红灯,不通过。


基本面


学历信息肯定不能造假,这个大家应该都清楚,学信网不是吃素的,秒出结果。


最近两份工作的入离职时间不要出问题,这个但凡是第三方背调,近两份工作是必查项,而且无论是明察还是暗访,都会查得非常仔细,很难钻空子的。


再有就是刚才说的,手机号和人名要对上,而且这个人确实是在这家公司任职的。


大家耳熟能详的大厂最好查,背调公司都有人才数据库的,而且圈子里的人也好找。再有就是,随便找个内部员工,大厂的组织结构在内部通讯软件里都能看到的。


小厂难度大一些,如果人才数据库没有的话,背调员会从网上找公司电话,然后打给前台,让前台帮忙找人。但有的前台听了会直接挂断电话。


薪资方面,不要瞒报,一般背调公司会让你打印最近半年或一年的流水,以及纳税信息。


直接上级


这应该也是大家最关心的问题之一。


马云曾经说过:离职无非两种原因,钱没给够,心委屈了。而心委屈了,绝大多数都跟自己的直接上级有关。


如果在背调的时候,担心由于自己跟直接上级关系不好,从而导致背调结果不利的话,可以尝试以下三种方式。


第一,如果你在公司里历经了好几任领导的话,可以留关系最好的那任领导的联系方式,这个是在规则允许范围内的。


第二,如果你的直接上级只是一个小组长,而你跟大领导(类似于部门负责人)关系还可以的话,可以跟大领导沟通一下,然后背调留他的信息。像这个,一般背调公司不会深究的。


就像我的那个表哥,背调公司的老板所说的:“如果一个腾讯员工,马化腾都出来给他做背调了,那我们还能说什么呢?”


第三,如果前两点走不通的话,还可以坦诚地跟HR沟通一次,说明跟上级之间确实存在一些问题,原因是什么什么。


比如:我朋友遇到了这种情况,公司由于经营不善而裁员,老板竟然无耻地威胁我朋友,如果要N+1赔偿的话,背调就不会配合。


如果你确实不是责任方的话,一般HR也能理解。毕竟都是打工人,何苦相互为难呢。


你还可以这么加上一句:“我之前工作过的公司,您背调哪家都可以,我的口碑都很好的,唯独这家有些问题。”


btw:还有一些朋友,背调的时候留平级同事的真实电话和姓名,用来冒充领导,这个是有风险的。但是遇到背调不仔细的公司,也能通过。通过概率的话,一半一半吧。


就像我那个朋友所说:“现在人力成本不便宜,如果公司想盈利的话,我的背调员一天得完成5个背调,平均不到两个小时一个。你总不能希望他们个个都是名侦探柯南吧。”


信用与诉讼


一般来讲,背调的标准套餐还包括如下内容:金融违规、商业利益冲突、个人信用风险和有限民事诉讼。其中后两个大家尽量规避。


个人信用风险包括:网贷/逾期风险、反欺诈名单和欠税报告。


网贷这块,当时我有一个同事,2021年的时候,拿了4个offer,结果不明不白地都挂在了背调上,弄得他很懵逼。


当他问这三家公司HR原因的时候,HR都告诉他不便透露。


最后,他动用身边人脉,才联系上一家公司的HR出来吃饭,HR跟他说:“以后网贷不要逾期,尤其是不同来源的网贷多次逾期。”


同事听了,这才恍然大悟。


欠税这个,就更别说了,呵呵,大家都懂,千万别心存侥幸。


再说说劳动仲裁和民事诉讼。


现在有些朋友确实法律意识比较强,受到不公正待遇了,第一想法就是“我要去仲裁”,仲裁不满意了,就去打官司。


首先我要说的是,劳动仲裁是查不到的,所以尽量在这一步谈拢解决。


但民事诉讼在网上都是公开的,而且第三方背调公司也是走系统的,一查一个准儿。如果非必要的话,尽量不要跟公司闹到这一步。


如果真遇到垃圾公司或公司里的垃圾人,第一个想法应该是远离,不要让他们往你身上倒垃圾。


尤其是你主动跟公司打官司这种,索要个加班费、年终奖什么的,难免会让新公司产生顾虑,会不会我offer的这名候选人,以后也会有对簿公堂的一天。


结语


现在这大市场行情,求职不易,遇到入职前背调更是如履薄冰,希望大家都能妥善处理好,一定要避免节外生枝的情况发生,不要在距离成功一米的距离倒下。


最后,祝大家工作顺利,纵情向前,人人都能拿到自己满意的offer,开开心心地入职。


作者:库森学长
来源:juejin.cn/post/7295160228879204378
收起阅读 »

迷茫的前端们

web
前端已死!这个声音从年初开始,持续了小半年。紧接着,一堆的前端前辈出来说,前端不会死。哈哈哈,我觉得这个话题很有意思。也来蹭个热点。嗯,现在已经不算热点了,这个话题已经凉了。 最近想法很多,总想写点东西,就拿这个话题开始吧。希望能对大家有帮助。 什么情况 首先...
继续阅读 »

前端已死!这个声音从年初开始,持续了小半年。紧接着,一堆的前端前辈出来说,前端不会死。哈哈哈,我觉得这个话题很有意思。也来蹭个热点。嗯,现在已经不算热点了,这个话题已经凉了。


最近想法很多,总想写点东西,就拿这个话题开始吧。希望能对大家有帮助。


什么情况


首先,要看看为什么会有这个观点。原因也很简单,自从去年开始,各大公司都开始裁员,然后HC也开始减少。但是因为前几年互联网工资收入高,每年毕业的应届生那是一年比一年多。前端培训班也火热的不行,每年都向社会输送大面积的前端开发。


所以,从供需角度看,前端开发找工作,尤其是刚毕业和刚培训毕业的前端开发,今年找工作就是地狱模式。所以有人说“前端已死”,自然很快就能获得共鸣


另外的声音


然后,有意思的来了。“前端已死”这个话题火了后,各大前端自媒体,前端前辈都出来发声。观点出奇的一致:前端不会死。一下子就把前端已死的观点给压下去了。这种社区声音180度的大转弯也是少见。


到底死没死


说完现象,我说下我的观点。



  1. “前端已死”的声音能获得共鸣,说明前端已经发展到了一个关键的时间点。

  2. 只要还有页面,还有小程序,前端就不会死。

  3. 不会死,并不是说能活的很好。


行业周期


互联网在行业分类中属于第三产业,也就是服务业。服务业的特点就是周期性。如果所有服务业的周期是往下走的,互联网也好不了。三年疫情,大家都知道,服务业已经被折腾的不行了,即使今年放开了,大家还敢开线下店吗?被互联网服务的行业大部分都过的不好,互联网能好吗。


当然,后面随着经济复苏,互联网也会慢慢恢复景气。但是这里还有另外一个点,由于互联网前几年工资都太高了,年年倒挂,导致选计算机专业的人越来越多。以大学四年计算,后面3年每年毕业的计算机专业的学生,还是会持续增加的。


所以呢,从供需角度,技术互联网恢复景气了,互联网招聘未必会恢复景气。

当然了,我觉得,后面几年,不会像今年这么卷,投10份简历,连个面试机会都没有。(预测这种东西,很容易打脸,大家看看就好)


另外,就前端开发而言,我感觉好多东西都很成熟了。最近看技术社区的文章,感觉很久没有看到很新的内容了(也许是推荐系统搞得信息茧房)。大家都在几个领域里面深挖。如果没有新的方向出来,真的很难容纳这么多的求职者。后面看看AI能不能创新出一波职位吧。


所以呢,从互联网行业和前端行业来看,前端已经发展到了一个关键点,后面如果没有新的方向出来,即使互联网恢复景气,求职也不会像以前那么轻松了。

但是,毕竟还有那么多业务需要页面,需要小程序,这块的需求还是在的,所以还是会有前端开发的。


技术周期


从我毕业,基本上都在做互联网,期间其实经历过好几轮技术迭代。比如最早的Flash, 到PHP, PC端的JQuery。你很难在招聘网站上找到要求这些技术的岗位了。

随着前端的发展,技术栈也一直在变化。但是,大家想想,现在流行的React 和 Vue,已经用了多久了。如果还是只会React,Vue,是不是说明已经很久没有成长了。后面会不会有新的技术栈出来,比如VR出来后,是不是还用React开发?

如果还是继续用React,Vue,那么和行业周期里面说的一样,前端不会死,但是不会像以前那么活的好。


新方向在哪里


其实聊了这么多,本质就是一个供需。供需这个角度真的太强大了,能解释很多问题。如果未来没有一个新的大方向出现,那么在岗位需求变化不大的情况下,每年大量学生毕业,也就是供应增多,求职困难是必然的。

那么会不会有新方向出现呢,目前看,有几个可能的方向:



  1. AI。从去年底开始,AI持续火热,目前虽然有退烧,但是AI的趋势已经明确。后面要看的是,AI是不是需要大量的前端工作。这块大家可以说说自己的判断。

  2. VR元宇宙。Meta搞元宇宙,差点把自己搞死,现在苹果也出VR设备,观察下能不能把这个行业带起来。大家说有没有可能。

  3. Web3。去年年初,Web3还很火了的,现在好像,嗯,一般般。后面能不能再次爆发,也要继续观察。大家也可以说说自己的开发。


扯一句


最后扯一句。我看了好多前端前辈对“前端已死”的看法,都说前端不会死,会死的都是初级前端,前端要持续学习,要让自己不可替代。说实话,我觉得这都是屁话。写代码的谁不是从初级开始的呢,现在是完全不给初级的机会,断档了!写代码的人,已经是最爱学习的那一批人了吧,永远在学习新东西。还有让自己不可替代,公司会想办法让你可替代,后面聊一聊不可替代这个话题,我也有很多想说的。


结束


后面会逐步把掌握的前端知识以及职场知识沉淀下来。 如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。


作者:写代码的浩
来源:juejin.cn/post/7253437782333669434
收起阅读 »

协程-来龙去脉

首先必须声明,此文章是观看油管《KotlinConf 2017 - Introduction to Coroutines by Roman Elizarov》的感想,如何可以,更建议您去观看这个视频而不是阅读本篇文章 代码的异步控制 举个例子,假设你要去论坛...
继续阅读 »

首先必须声明,此文章是观看油管《KotlinConf 2017 - Introduction to Coroutines by Roman Elizarov》的感想,如何可以,更建议您去观看这个视频而不是阅读本篇文章


代码的异步控制


举个例子,假设你要去论坛上发布消息,你必须先获取token以表明你的身份,然后创建一个消息,最后去发送它


//这是一个耗时操作
fun requestToken():Token = {
... //block to wait to receive token
returen token
}

//这也是一个耗时操作
fun creatMessage() : Message ={
... //block to wait to creat Message
returen token
}

fun sendMessage(token :Token,message:Message)

fun main() {
val token = requestToken()
val meassage = creatMessage()
sendMessage(token,message)
}


这种情况显然不符合我们的现实情况,我们不可能在没有拿到token就等待在那里,我们可以开启一个线程去异步执行



fun requestToken():Token = {
... //new thread to request token
returen token
}

像这样一个任务我们就需要创建一个线程,creatMessage与requestToken可以并行进行,因此我们需要再次创建一个线程去执行creatMessage,从而创建两个线程。现在我们的手机性能很很高,我们可以创建一个,两个,甚至是一百个个线程去执行任务,但是到达一千个,一万个呢,恐怕手机的不足以支撑。
怎么解决这样的问题呢。我们只需要建立一种通知机制,在token返回后告诉我们,我们再继续完成creatMessage,进而sendMessage。这也就是callback方式



fun requestTokenCallback(callback : (Token) -> Unit) {
... //block to wait to receive token
callback.invoke(token)
}

fun creatMessageCallback : (Message) -> Unit){
... //nblock to wait to creat Message
callback.invoke(message)
}

fun sendMessage(token :Token,message:Message)

fun main() {
//创建一个线程
Thead {
requestTokenCallback { token ->
creatMessageCallback { message ->
{
sendMessage(token, message)
}
}
}

}

}


这仅仅是一个简单的案例,就产生了如此多的嵌套和连续的右括号,在实际业务中往往更为复杂,比如请求失败或者一些异常情况,甚至是一些特定的业务操作,想想这样叠加下去,简直是灾难。
如何解决这种问题呢,java中有一个CompleteFuture,正如其名,它能够异步处理任务,以期获取未来的结果去处理,我们只需要允诺我们将来在某个时间点一定会返回某种类型的数据,我们就可以以预知未来的方式使用他,


//创建一个线程去异步执行
fun requestToken() :CompletableFuture<Token> = ...

//创建一个线程去异步执行
fun creatMessage(token) : CompletableFuture<Send> = ...

fun sendMessage(send :Send)

fun main() {
requestToken()
.thenCompose{token -> creatMessage(token)}
.thenAccept{send -> sendMessage(send)}
}


令人头疼的代码已不复存在,我们可以自由组合我们任务
creatMessage的调用方式和sendMessage并不相同,kotlin为了统一这两种调用,产生了一个suspend 关键字,它能够像拥有魔法一样,让世界的时间暂停,自己又不会暂停,然后去执行自己的任务,执行完成之后,时间恢复,任务继续执行



suspend fun requestToken() :Token = ...

suspend fun creatMessage() : message = ...


fun sendMessage(token :Token,message:Message)

fun main() {
val token = requestToken()
val meassage = creatMessage()
sendMessage(token,message)
}


执行这段代码的时候,会发生编译错误,因为main函数只是一个普通的函数,并没有被suspend标记,当requestToken使时间暂停的同时,主程序也时间暂停了,那这与最开始的阻塞方法有什么不一样的呢


协程Coroutine


本文要介绍的是协程,协程是什么呢,在我看来,协程就是一个容器,让suspend标记的函数可以运行,可以开启魔法 ,让它在时间暂停的同时,并不影响主线程


public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,//创建容器的上下文
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T//拥有魔法的函数
)
: Deferred<T> {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy) //创建协程
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
coroutine.start(start, coroutine, block) //启动魔法开关
return coroutine
}

public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext, //创建容器的上下文
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit //拥有魔法的函数
)
: Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy) //创建协程
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block) //启动魔法开关,启动协程
return coroutine
}

我们可以通过Deferred.await() 获取将来的值T。自此,我们有了新的代码



suspend fun requestToken() :Token = ...

suspend fun creatMessage() : message = ...


fun sendMessage(token :Token,message:Message)

fun main() {
val token = async {requestToken()}
val meassage = async{creatMessage()}
sendMessage(token.await() ,message.await() )
}


实际业务中,我们的任务并不是一直都是需要返回结果的,所以还有另一种容器,只需要去执行


public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
)
: Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}


轻量级线程


协程被视为轻量级线程,轻量在哪呢?


实际上,async与launch 并没有什么魔法,只是将封装好的任务交由线程池去执行,所以suspend标记的函数可以任意暂停


协程只是代码层级的概念,操作系统对于此是无感知的,但是线程作为cpu调度的基本单位,创建和调用是很重的,需要中断机制去进行调度,消耗很多额外的资源,所以协程被视为轻量级线程。


上面的代码中launch 与async 都是CoroutineScope 的函数,那么CoroutineScope 是什么呢
这个就是协程运行的温床,也可说是运行的基础,也就是作用域。创建处一个协程,必须有一个管理容器去管理协程的创建,分发,调度,这就是CoroutineScope。


有一种常见的需求是网络执行完成切换UI线程去执行,因此,我们需要在创建容器的时候需要一个参数,去声明协程到底在线程池中执行,UI线程池还是普通线程池,


val scope = CoroutineScope(Dispatchers.IO) //IO线程池去处理
val scope = CoroutineScope(Dispatchers.Main) // UI线程去处理

题外


阅读到这里可以知道suspend实际上就是一个callback封装, 对于其如何将标记函数转化为可挂起恢复的,可浏览# Kotlin Vocabulary | 揭秘协程中的 suspend 修饰符,个人觉得讲的不错


在推荐的这篇文章中,可以看到使用了状态机,其目的是为了节省Continuation对象的创建,可以借鉴学习


关于我


一个希望友友们能提出建议的代码下毒糕手


作者:小黑不黑
来源:juejin.cn/post/7294852698460373004
收起阅读 »

一亿数据大表,我们是如何做分页的

本文是基于我们公司的情况做的大表分页方案简单记录,不一定适合所有业务场景,大家感兴趣的读一下,可以在评论区讨论,技术交流,文明用语。 最近在做一个功能,有一张大概一亿的数据需要做分页查询,所以存在深分页的问题,也就是越靠后的页访问起来就会越慢,所以需要对这个功...
继续阅读 »

本文是基于我们公司的情况做的大表分页方案简单记录,不一定适合所有业务场景,大家感兴趣的读一下,可以在评论区讨论,技术交流,文明用语。


最近在做一个功能,有一张大概一亿的数据需要做分页查询,所以存在深分页的问题,也就是越靠后的页访问起来就会越慢,所以需要对这个功能进行优化。


对于深分页问题,一般都是采用传递主键ID的做法来解决,也就是每次查询下一页数据时,把上一页数据中最大的ID传递给下一页,这样在查询某一页时就能利用主键索引来快速查询了。


但是由于历史原因,那张一亿数据的大表没有主键ID,-_-,所以这是需要解决的问题之一。


另外,就算能利用主键索引快速查询分页数据,但是毕竟是一亿数据,最终查询比较靠后的页数时,因为数据量大就算走索引最后还是会比较慢,所以就需要分表了。


我们先使用SQL脚本的方式把一亿数据迁移到分表,我们按照时间季度进行分表,比如2022q1、2022q2、2023q1这种方式来进行分表,这样每张分表的数据就不会太多了,大概1000万左右,并且在创建分表时顺便给分表添加主键ID字段,然后在迁移数据时需要按照原始大表的交易时间进行升序排序,保证ID的自增顺序与交易时间顺序是一致的。


不过这种方案存在两个问题,第一个问题就是查询分页时跨分表了怎么办,比如我查第10页数据时,一部分数据在2023q1,一部分数据在2023q2,首先在查询时本身就需要选择年份和季度,所以直接就确定了对应的是哪张分表,查询时根据年份和季度直接定位到分表,然后再进行分页查询,所以不会出现跨表,这个问题就自然而然解决了。


另外,每张表的主键ID要不要连续,比如2022q1的主键是1-1000000,那么2022q1的主键要不要是1000001-2000000,其实我个人觉得需要这么设计,因为两个分表的数据逻辑上其实一张表,所以主键最好不冲突,但是考虑到实现难度,暂时没有采取,而是每张分表的主键ID都是从1开始的,这种方案在目前看来是没有问题的,因为每次都只会从一张分表中查询数据,不会出现跨表的情况。


另外,在设计时也想到过能不能直接用交易时间做为主键来进行分表,但是想到交易时间的精度是秒,所以很有可能出现交易时间相同的记录,这样在做分页时可能会出现下一页数据和上一页数据有重复的,所以还是需要单独设计一个主键ID。


继续思考,假如这张一亿数据的大表要做分页,但是不根据年份和季度做查询来分页,而就是直接分页,那又该如何呢?


首先,肯定还是得利用分表和主键索引,我的思路是,先给原始表添加主键ID并生成自增ID,然后再按主键ID进行分表,分表记录数也可以控制在1000万左右,前端查询时仍然按ID来查分页就可以了,但是此时就存在跨表的问题,比如每页如果是20条,查第10页数据时,如果从分表一只查出了5条,那么后端接口判断5小于20,就尝试从分表二中继续查。


当然了,如果既有复杂的查询条件,又需要进行分页,那用关系型数据库就不太好做了,就可以考虑ES之类的了。


大家有什么更好的经验或思路,欢迎不吝赐教。


我是爱分享技术的大都督周瑜,欢迎关注我的公众号:Hoeller。公众号里有更多高质量干货系列文章和精品面试题。


记得点赞、分享哦!!!


作者:爱读源码的大都督
来源:juejin.cn/post/7294823722807017499
收起阅读 »

分享下最近找工作的行情

国庆前开始陆续随便投了一点公司试了下行情,确实不很乐观,但是当时还没有特别强烈的感觉。 目前这国庆之后再投了几天的感受特别强烈,就是基本凉凉的节奏。 目前感受下来,招人的公司有限,上海这边就字节、PDD、美团、小红书、得物这些在大量招人,在招聘平台脉脉、拉勾、...
继续阅读 »

国庆前开始陆续随便投了一点公司试了下行情,确实不很乐观,但是当时还没有特别强烈的感觉。


目前这国庆之后再投了几天的感受特别强烈,就是基本凉凉的节奏。


目前感受下来,招人的公司有限,上海这边就字节、PDD、美团、小红书、得物这些在大量招人,在招聘平台脉脉、拉勾、BOSS上基本上也是这些有反馈,另外猎头基本上推的也就是得物、字节这些公司,大部分都是得物。


总结下来的情况就是,要么就是一线大厂在招人,要么就是在这个经济不好的情况下业务做得好的、没什么影响的在招人,比如得物了。


朋友问了一圈,一些公司有HC,但是在招聘平台上都是没有声音的。


华为朋友告诉我这个学历只能去OD,985都在干OD。


字节告诉我卷死来,心脏和字节只能有一个跳动。


腾讯有少量HC,但是也是一直在裁员,很不稳定,米哈游也一样。


饿了么还有一些HC,但是也是在一边裁员,一边有些部门有少量HC。


SAP目前没HC,也是裁员。


联想也没有HC,早就没了。


携程和一些其他的小一点的公司,还有一些HC,一线开发,但是好像薪资不是很高的样子,没有细问了。


诸如此类。。。


还有一些小点的公司,去年还在招人,问我要不要过去,今年普遍都在裁员,没有HC了。


还有一些在家办公的在招人,这个你懂的,但是也非常的卷。


大致情况市场就是这样,要么去卷,要么失业的节奏。


想找钱多活少的,目前看起来是非常不现实了,甚至于想找个工作都非常困难。


听说大厂现在都要卡学历、业务、大厂经历,三高匹配至少两项。


总体来看,暂时没有什么好的出路,还在继续看。。。


有好公司欢迎拉我入坑。。。


就这样。


作者:艾小仙
来源:juejin.cn/post/7288540474058457125
收起阅读 »

如何在网页中展示源代码

web
如何在网页中展示你的源代码 如下图所示: 在做技术说明文档 或者 组件库 的时候,经常需要在网页中引用代码示例,但是直接写又很丑,所以我们来试试如何在网页中展示自己的源代码 第一步: 自定义 vite插件 首先需要一个自定义插件用来转换 vue 的自定义块 ...
继续阅读 »

如何在网页中展示你的源代码


如下图所示:


1698137911654.png


在做技术说明文档 或者 组件库 的时候,经常需要在网页中引用代码示例,但是直接写又很丑,所以我们来试试如何在网页中展示自己的源代码


第一步: 自定义 vite插件


首先需要一个自定义插件用来转换 vue 的自定义块


在vite.config.ts文件里


import fs from 'fs'
import {baseParse} from '@vue/compiler-core'
const vueDemoPlugin = {
name: "vue-block-demo",
transform(code, path) {
if (!/vue&type=demo/.test(path)) {
return;
}
const filePath = path.split("?")[0];
//异步读取文件内容,并转为string类型
const file = fs.readFileSync(filePath).toString();
//将读取到的文件中的自定义快渲染为AST
const parsed = baseParse(file).children.find((n) => n.tag === "demo");
//读取自定义模块中的文本内容
const title = parsed.children[0].content;
//将读取文件中的自定义块切分,并转为字符串类型
const main = file.split(parsed.loc.source).join("").trim();
//以JSON数据类型返回
return `export default Comp => {
Comp.__sourceCode = ${JSON.stringify(main)}
Comp.__sourceCodeTitle = ${JSON.stringify(title)}
}`
;
},
};
export default defineConfig({
plugins: [vue(), vueDemoPlugin],
})

第二步:在要展示的源代码文件里面加上一个自定义块


比如我要展示 SwitchDemo01.vue 文件


<template> 加上 <demo> 自定义块,里面内容写上 title


<demo>常规用法</demo>
<template>
<hx-switch v-model="value1" />
<hx-switch
v-model="value2"
class="ml-2"
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
/>
</template>

<script lang="ts" setup>
import { HxSwitch } from 'hx-gulu-ui';
import { ref } from 'vue'

const value1 = ref(true)
const value2 = ref(true)
</script>

第三步: 在页面展示文件里面引用


如: showSwitch.vue 文件:


<template>
<div class="doc-page">
<h2>Switch 组件示例 </h2>
<p class="doc-page-desc">表示两种相互对立的状态间的切换,多用于触发「开/关」。</p>
<div class="demo">
<h3>{{ SwitchDemo01.__sourceCodeTitle }}</h3>
<p class="doc-page-usage">绑定 `v-model` 到一个 `Boolean` 类型的变量。 可以使用 `--el-switch-on-color` 属性与 `--el-switch-off-color` 属性来设置开关的背景色</p>
<div class="demo-component">
<SwitchDemo01></SwitchDemo01>
</div>
<div class="demo-actions">
<Button>查看代码</Button>
</div>
<div class="demo-code">
<pre>{{ SwitchDemo01.__sourceCode }}</pre>
</div>
</div>

</div>
</template>

<script lang="ts" setup>
import SwitchDemo01 from '@/components/switch/SwitchDemo01.vue'

// __sourceCode 这里面是源文件去除了 <demo> 外的所有代码
console.log('SwitchDemo01', SwitchDemo01.__sourceCode)

// __sourceCodeTitle 这里面是 <demo>常规用法</demo> 里面的文字
console.log('SwitchDemo01', SwitchDemo01.__sourceCodeTitle)

</script>

<style lang="scss" scoped>
@import './style.scss';

</style>

此时,已经可以在代码上显示源文件代码了,但是代码没有任何样式,很丑怎么办呢?


第四步: 引入 prismjs




  • prismjs 是代码主题的插件



    • 官网

    • 安装: npm i prismjs




  • 调用


    import 引入 好像有问题,只支持 require('prismjs'),同时在window属性下 添加了 Prismjs属性,大家可以自己试一下


    <script setup lang='ts'>
    import 'prismjs'
    import 'prismjs/themes/prism-okaidia.min.css'
    const Prism = (window as any).Prism

    const code = `var data = 1;`;
    const html = Prism.highlight(code, Prism.languages.javascript, 'javascript');
    </script>

    示例:




<template>
<div class="doc-page">
<h2>Switch 组件示例 </h2>
<p class="doc-page-desc">表示两种相互对立的状态间的切换,多用于触发「开/关」。</p>
<div class="demo">
<h3>{{ SwitchDemo01.__sourceCodeTitle }}</h3>
<p class="doc-page-usage">绑定 `v-model` 到一个 `Boolean` 类型的变量。 可以使用 `--el-switch-on-color` 属性与 `--el-switch-off-color` 属性来设置开关的背景色</p>
<div class="demo-component">
<SwitchDemo01></SwitchDemo01>
</div>
<div class="demo-actions">
<Button>查看代码</Button>
</div>
<div class="demo-code">
<pre class="language-html" v-html="html"></pre>
</div>
</div>

</div>
</template>

<script lang="ts" setup>
import SwitchDemo01 from '@/components/switch/SwitchDemo01.vue'

import 'prismjs'
import 'prismjs/themes/prism-okaidia.min.css'
const Prism = (window as any).Prism

const html = computed(() => {
return Prism.highlight(SwitchDemo01.__sourceCode, Prism.languages.html, 'html')
})

</script>

<style lang="scss" scoped>
@import './style.scss';

</style>

最终效果如下图所示:


1698140953360.png


作者:Blink46
来源:juejin.cn/post/7293348981664399397
收起阅读 »

技术人创业是怎么被自己短板KO的

这几天搜索我的聊天记录,无意中看到了一个两年前的聊天消息。那是和我讨论出海业务的独立开发网友,后来又去创业的业界精英。顺手一搜,当初他在我的一个开发者群也是高度活跃的人群之一,还经常私下给我提建议,人能力很强,做出海矩阵方向的项目做的很拿手。但是在后来忽然就没...
继续阅读 »

这几天搜索我的聊天记录,无意中看到了一个两年前的聊天消息。那是和我讨论出海业务的独立开发网友,后来又去创业的业界精英。顺手一搜,当初他在我的一个开发者群也是高度活跃的人群之一,还经常私下给我提建议,人能力很强,做出海矩阵方向的项目做的很拿手。但是在后来忽然就没消息了,虽然人还留在群里。


我好奇点开了他的朋友圈,才知道他已经不做独立开发了,而且也(暂时)不在 IT 圈里玩了,去帮亲戚家的服装批发业务打打下手,说是下手,应该也是二当家级别了,钱不少,也相对安稳。朋友圈的画风以前是IT行业动态,出海资讯现在是销售文案和二维码。


和他私下聊了几句,他跟我说他现在过的也还好,人生路还长着呢,谈起了自己在现在这行做事情的经历,碎碎念说了不少有趣的事情,最后还和我感慨说:“转行后感觉脑子灵活了很多”,我说那你写程序的时候脑子不灵活吗,他发了个尴尬而不失礼貌的表情,“我以前技术搞多了,有时候死脑筋。”


这种话我没少听过,但是从一个认识(虽然是网友)而且大跨度转行的朋友这里说出来,就显得特别有说服力。尤其了解了他的经历后,想写篇文章唠叨下关于程序员短板的问题,还有这种短板不去补强,会怎么一步步让路越走越窄的。


现在离职(或者被离职)的程序员越来越多了,程序员群体,尤其是客户端程序员这个群体,只要能力过得去,都有全栈化和业务全面化的潜力。尤其是客户端程序员,就算是在公司上班时,业余时间写写个人项目,发到网上,每个月赚个四到五位数的副业收入也是可以的。


再加上在公司里遇到的各种各样的窝囊事,受了无数次“煞笔领导”的窝囊气,这会让一些程序员产生一种想法,我要不是业余时间不够,不然全职做个项目不就起飞了?


知道缺陷在哪儿,才能扬长避短,所以我想复盘一下,程序员创业,在主观问题上存在哪些短板。(因为说的是总体情况,也请别对号入座)


第一,认死理。


和代码,协议,文档打交道多了,不管自己情愿不情愿,人多多少少就有很强的“契约概念”,代码的世界条理清晰,因果分明,1就是1,0就是0,在这样的世界里呆多了,你要说思维方式不被改变,那是不可能的 --- 而且总的来说,这种塑造其实是好事情。要不然也不会有那么多家长想孩子从小学编程了。(当然了,家长只是想孩子学编程,不是做程序员。)


常年埋头程序的结果,很容易让技术人对于社会上很多问题的复杂性本质认识不到位,恐惧,轻视,或者视而不见,总之,喜欢用自己常年打磨的逻辑能力做一个推理,然后下一个简单的结论。用毛爷爷的话说,是犯了形而上的毛病。


例如,在处理iOS产品上架合规性一类问题时,这种毛病暴露的就特别明显。


比如说相信一个功能别的产品也是这么做的,也能通过审核,那自己照着做也能通过。但是他忽略了这种判断背后的条件是,你的账号和别的账号在苹果眼里分量也许不同的,而苹果是不会把这件事写在文档上的。


如果只是说一说不要紧,最怕的是“倔”,要不怎么说是“认死理”呢。


第二,喜欢拿技术套市场。


​这个怎么理解呢,就是有追求的技术人喜欢研究一些很强的技术,但是研究出来后怎么用,也就是落实到具体的应用场景,就很缺点想象力了。


举个身边有意思的例子,有个技术朋友花了三年时间业余时间断断续续的写,用 OpenGL 写了一套动画效果很棒的 UI 引擎,可以套一个 View 进去后定制各种酷炫的动画效果。做出来后也不知道用来干嘛好,后来认识了一个创业老板,老板一看你这个效果真不错啊,你这引擎多少钱我买了,朋友也没什么概念,说那要不五万卖你。老板直接钱就打过去了。后来老板拿给手下的程序员维护,用这套东西做了好几个“小而美”定位的效率工具,简单配置下就有酷炫的按钮动画效果,配合高级的视觉设计逼格拉满,收入怎么样我没问,但是苹果在好几个国家都上过推荐。


可能有人要说,那这个程序员哥哥没有UI帮忙啊,对,是这个理,但是最根本的问题是,做小而美工具这条路线,他想都没想到,连意识都意识不到的赚钱机会,怎么可能把握呢?有没有UI帮忙那是实现层的门槛而已。


第三,不擅长合作。


为什么很多创业赚到小钱(马化腾,李彦宏这些赚大钱就不说了,对我们大部分人没有参考价值)而且稳定活下来的都是跑商务,做营销出身的老板。


他们会搞钱。


他们会搞钱,是​因为他们会搞定人,投资人,合伙人,还有各种七七八八的资源渠道。


大部分人,在创业路上直接卡死在这条路线上了。


投资人需要跑,合作渠道需要拉,包括当地的税务减免优惠,创业公司激励奖金,都需要和各种人打交道才能拿下来。


那我出海总行了吧,出海就不用那么麻烦了吧。不好意思,出海的合作优势也是领先的,找海外的自媒体渠道合作,给产品提曝光。坚持给苹果写推荐信,让自家产品多上推荐。你要擅长做这些,就不说比同行强一大截,起码做出好产品后创业活下来的希望要高出不少,还有很多信息差方法论,需要进圈子才知道。



--- 


我说的这些,不是贬损也不是中伤,说白了,任何职业都有自己的短板,也就是我们说的职业病,本来也不是什么大不了的事情。只是我们在大公司拧螺丝的时候,被保护的太好了。


只是创业会让一个人的短处不断放大,那是因为你必须为自己的选择负责了,没人帮你擦屁股了背锅了。所以短板才显得那么刺眼。


最后说一下,不是说有短板就会失败,谁没点短处呢。写出来只是让自己和朋友有更好的自我认知,明白自己的长处在哪,短处在哪。


最后补一个,左耳朵耗子的事情告诉我们,程序员真的要保养身子,拼到最后其实还是拼身体,活下来才有输出。


作者:风海铜锣
来源:juejin.cn/post/7238443713873199159
收起阅读 »

使用小程序中的 observe 实现数据监听

web
小程序开发中,数据的监听和响应是非常重要的。为了更方便地监听数据的变化,小程序提供了 observe 方法。本文将详细介绍如何在小程序中使用 observe 实现数据监听,以及一些常见的应用场景。 什么是 observe? observe 是小程序中的一个方法...
继续阅读 »

小程序开发中,数据的监听和响应是非常重要的。为了更方便地监听数据的变化,小程序提供了 observe 方法。本文将详细介绍如何在小程序中使用 observe 实现数据监听,以及一些常见的应用场景。


什么是 observe


observe 是小程序中的一个方法,用于监听数据的变化并触发相应的回调函数。它可以用于监听页面数据、组件数据以及其他数据对象。


如何使用 observe


监听页面数据


在页面的 .js 文件中,可以使用 Page 函数中的 data 中的 observe 字段来监听数据的变化。以下是一个示例:


// pages/index/index.js
Page({
data: {
count: 0,
},

// 监听 count 数据的变化
observe: {
'count': function (newVal, oldVal) {
console.log('count 的值从 ' + oldVal + ' 变为 ' + newVal);
}
},

// 增加 count 值的函数
increaseCount() {
this.setData({
count: this.data.count + 1,
});
},
});

在上述示例中,我们在 observe 字段中定义了一个监听器,当 count 数据发生变化时,会触发相应的回调函数。


监听组件数据


在小程序的组件中,也可以使用 observe 来监听组件数据的变化。以下是一个示例:


// components/my-component/my-component.js
Component({
data: {
message: 'Hello, World!',
},

methods: {
changeMessage() {
this.setData({
message: 'New Message!',
});
},
},

// 监听 message 数据的变化
observe: {
'message': function (newVal, oldVal) {
console.log('message 的值从 ' + oldVal + ' 变为 ' + newVal);
}
},
});

在组件的 observe 字段中同样定义了一个监听器,用于监听 message 数据的变化。


常见应用场景


1. 数据绑定


observe 可以用于在数据变化时自动更新视图,实现数据绑定。这对于构建响应式的页面和组件非常有用。


2. 数据校验


通过监听数据的变化,可以在数据变化时进行校验,确保数据的合法性。例如,监听输入框中的文本变化并验证其格式。


3. 事件通知


当某个数据变化时,可以通过 observe 触发相关的事件,通知其他部分的代码执行相应的操作。


4. 数据持久化


在某些情况下,需要将数据持久化到本地存储或服务器,可以在数据变化时触发数据保存操作。


注意事项和最佳实践


在使用 observe 进行数据监听时,有一些注意事项和最佳实践需要考虑:




  1. 数据引用类型的监听:当监听对象是引用类型(例如对象或数组)时,需要注意对象的引用是否发生变化。observe 监听的是对象的引用,而不是对象内部属性的变化。如果需要监听对象内部属性的变化,可以使用深度监听或手动触发。




  2. 避免过多监听器:不要过度使用 observe,因为过多的监听器可能会导致性能问题。只监听那些真正需要监控的数据。




  3. 监听器的性能开销:监听器的回调函数在数据变化时会被频繁调用,因此要确保回调函数的执行效率较高,以避免影响应用的性能。




  4. 避免循环引用:在监听器回调函数中不要再次修改被监听的数据,以防止循环引用和无限循环触发监听器。




  5. 生命周期管理:在页面或组件销毁时,要记得取消监听以防止内存泄漏。可以在 onUnload 生命周期中取消监听。




onUnload() {
this.setData({
observe: null, // 取消监听
});
}

通过谨慎使用 observe,你可以实现有效的数据监听和响应,提高小程序应用的可维护性和性能。


结语


observe 是小程序中非常有用的功能,它允许你监听数据的变化并执行相应的操作。无论是在页面中还是在组件中,都可以使用 observe 来实现数据的监听和响应。通过合理利用 observe,你可以构建更加动态和交互性的小程序应用。


作者:依旧_99
来源:juejin.cn/post/7295237661618438196
收起阅读 »

这个故事有点长 - 苏州

这个故事有点长 - 苏州 👉故事的开始 之前已经去过苏州了,只是当时觉得园林都一样,就只去了拙耕园、留园、山塘街、观前街、平江路、平门古城墙、金鸡湖,印象最深刻的就是,当初围绕着金鸡湖走了一半,累了打车回去了。 最近心情有点郁闷😠,决定去走走,思来想去,就发...
继续阅读 »

这个故事有点长 - 苏州



👉故事的开始


之前已经去过苏州了,只是当时觉得园林都一样,就只去了拙耕园留园山塘街观前街平江路平门古城墙金鸡湖,印象最深刻的就是,当初围绕着金鸡湖走了一半,累了打车回去了。


最近心情有点郁闷😠,决定去走走,思来想去,就发现苏州还有地方没去,于是乎就去了,提前一天定了酒店,住的花居酒店,挺不错的,停车位很多,免费停车,其他酒店都是40一天,由于姑苏区车辆限行,就在姑苏通上申请了,结果都回来了,申请才通过。


🚗第一天


由于没有那么远的距离,九点才从上海出发,大概十一点左右到的酒店,然后再楼下简单吃了顿饭,就驱车去了虎丘,在停车场还有人指导我倒车入库,然后就一直推荐旅游套餐,说虎丘+XXX好几个地方只需100元,车接车送,很划算,不能单买虎丘的,于是乎果断在美团上买了70的门票。







大概逛了三四个小时,期间卖了一块网红雪糕,宝塔形状的。五点左右就回去了,休息了一个小时,然后就打车去金鸡湖音乐喷泉,是个妹子开的车,然后在车上聊了起来,期间还为我表演了一次丝滑插车,下车时还让我给她了一个好评。七点半开始,我六点半左右就到了,结果还是没有抢到最佳观看位置,都太卷了,人很多,与外滩有一拼。




在等待期间,认真观察了苏州的“大裤衩”,期间总共变了三种颜色,期初是文字,后来变成了图案。





从金鸡湖回来, 坐地铁一号线去了观前街,第二次来了,故地重游,感觉就是不一样,又拍了两张照片😄,还是那么的热闹 。




🌅第二天


一觉睡到了十点,吃了早饭去狮子林,搜了一下,距离只有一公里多一些,果断又打车了,门票40元,中间蹭了导游的一些讲解,又看到了九狮台,能看的出来是九头狮子吗?



接下来就是假山群,里面有很多山洞,据说女儿国就在这里拍的,当初乾隆皇帝就在这里完了很久,直呼真有趣,于是就有了真趣亭




之后在附近吃了松鼠桂鱼,然后就回去了,一路都在与天气赛跑,一段下雨一段晴。




🚩起点既是终点


时间过得好快,兜兜转转了一圈,从哪来又回了哪去,晚安🌜🌛。



这个故事有点长 - 故事还在继续🏃🏃🏃.......



作者:小小愿望
来源:juejin.cn/post/7294130755840540735
收起阅读 »

用canvas画出一片星空

web
前言 由于最近用了挺多Echarts的,所以突然想学习学习它的底层原理Canvas。Canvas对于我们前端来说是一个非常强大的工具,它可以实现各种复杂的图形和动画效果,我们如果能够熟练掌握它,我们就可以做很多炫酷的效果。 Canvas 介绍 首先我们来介绍介...
继续阅读 »

前言


由于最近用了挺多Echarts的,所以突然想学习学习它的底层原理CanvasCanvas对于我们前端来说是一个非常强大的工具,它可以实现各种复杂的图形和动画效果,我们如果能够熟练掌握它,我们就可以做很多炫酷的效果。


Canvas 介绍


首先我们来介绍介绍CanvansCanvasHTML5提供的一个绘图API,它允许通过JavaScriptHTML元素创建和操作图形。Canvas提供的是一个矩形画布,我们可以在上面绘制各种图形、动画与交互效果。


功能


上面了解了Canvas是什么之后, 我们再来和大家展开说说Canvas 一些主要功能:



  1. 画布:Canvas提供了一个矩形的画布区域, 我们可以通过HTML中的<canvas>标签来创建并通过设置画布的宽高来确定绘图区域的大小。

  2. 绘画API:Canvas提供了丰富的绘图API,包括绘制路径、直线、曲线、矩形、圆形、文本等。 我们使用这些API来创建各种图形,并自定义样式、颜色、透明度等属性。

  3. 动画:Canvas可以与JavaScript的动画函数结合使用,实现动态的图形效果。通过在每一帧中更新画布上的内容,能创建平滑的动画效果。

  4. 图像处理:Canvas可以加载和绘制图像。我们可以使用Canvas的API对图像进行裁剪、缩放、旋转等操作,从而实现图像处理的功能。

  5. 事件处理:Canvas支持鼠标和触摸事件的处理,我们可以通过监听这些事件来实现交互效果,例如点击、拖拽、缩放等等。


注意:Canvas 绘制的内容是即时生成的,它不会被浏览器缓存,所以每次页面加载Canvas都需要重新绘制,所以我们在使用时需要考虑性能问题。


星空


在介绍完Canvas之后,我们再来用CanvasJS结合,实现一片星空的效果。


第一步,我们先创建好html 结构,代码如下:


  <div class="landscape">
</div>
<canvas id="canvas"></canvas>
<div class="filter"></div>
<script src="./round_item.js"></script>

现在是没有效果的,我们在给它加上css代码,将其美化,同时我们再在css上加一些动画效果,完整代码如下:


* {
margin: 0;
padding: 0;
}
html,
body {
width: 100%;
height: 100%;
}

body {
background: linear-gradient(to bottom, #000 0%, #5788fe 100%);

}

.landscape {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url(./star/xkbg.png);
background-repeat: repeat-x;
background-size: 1000px 250px;
background-position: center bottom;
}
.filter{
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 2;
background: #fa7575;
animation: colorChange 30s ease-in infinite;
}
/* 渐变动画*/
@keyframes colorChange {
0%,100%{
opacity: 0;
}
50%{
opacity: 0.7;
}

}

这时候,我们看到的效果是这样的:


1698327282484.gif


这效果好像只有黄昏的颜色变化,少了星空。那这最后一步,就该我们Canvas上场了,我们要用Canvas画出来一片星空,并配合css的颜色变化,实现一个夜晚到清晨的感觉。


我们 js 代码这样写:


//创建星星的函数
function RoundItem(index, x, y, ctx) {
this.index = index
this.x = x
this.y = y
this.ctx = ctx
this.r = Math.random() * 2 + 1
this.color = 'rgba(255,255,255,1)'
}
// 绘制
RoundItem.prototype.draw = function () {
this.ctx.fillStyle = this.color //指定颜色
this.ctx.beginPath() // 开始绘制
this.ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false) // 绘制圆形
this.ctx.closePath() // 结束绘制
this.ctx.fill() //填充形状
}
//移动
RoundItem.prototype.move = function () {
this.y -= 0.5
this.draw()
}


let canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
round = [],
initRoundPopulation = 200; //星星的个数

const WIDTH = document.documentElement.clientWidth,
HEIGHT = document.documentElement.clientHeight;

canvas.width = WIDTH
canvas.height = HEIGHT

init()
// setInterval(animate, 1700)
animate()
function init() {
for (var i = 0; i < initRoundPopulation; i++) {
round[i] = new RoundItem(i, Math.random() * WIDTH, Math.random() * HEIGHT, ctx)
round[i].draw()
}
}
function animate() {
ctx.clearRect(0, 0, WIDTH, HEIGHT) //清除画布
for (let star of round) {
star.move()
}
requestAnimationFrame(animate) //通过刷新帧数来调用函数
}


将上述代码添加上之后,我们也就完成星星的绘制并添加到了页面上,最终的效果如下:
1698327645625.gif


到这里,我们就用canvas 画出了一片美丽的星空。同时我们也看到了canvas的强大功能,感兴趣的小伙伴可以深入的了解它更多的用法哦。


作者:潘小七
来源:juejin.cn/post/7294103091019612212
收起阅读 »

怎么办,代码发布完出问题了

作者:蒋静 前言 回滚是每个开发者必须熟悉的操作,它的重要性不言而喻,必要的时候我们可以通过回滚减少错误的代码对用户影响的时间。回滚的方式有很多种,方式有好也有坏,比如说使用 git 仓库回滚有可能会覆盖其他业务的代码,不稳定,构建产物的回滚最安全,便于优先...
继续阅读 »

作者:蒋静



前言


回滚是每个开发者必须熟悉的操作,它的重要性不言而喻,必要的时候我们可以通过回滚减少错误的代码对用户影响的时间。回滚的方式有很多种,方式有好也有坏,比如说使用 git 仓库回滚有可能会覆盖其他业务的代码,不稳定,构建产物的回滚最安全,便于优先解决线上问题。


构建部署之“痛”


我的几段公司的工作经历:



  1. 第一段经历,是在一个传统的公司,没有运维,要我们自己登录一个跳板机,把文件部署到服务器,非常麻烦。

  2. 第二段经历,是在一个初创公司,基建几乎没有,前端的规模也很小,发布就是打个 zip 包发给运维,运维去上线。但是久而久之,运维也就不耐烦了。

  3. 后来去了稍微大些的公司,构建、部署有一套比较完善的体系,在网页上点击按钮就可以了。


那么构建部署是如何实现的呢?下面我要来介绍古茗的部署和回滚代码机制。


发布分析


我们的最终目的是发布上线,我们发布的是什么呢?是一条分支,所以我们需要先创建一条分支(更加规范的步骤应该是:基于某个需求和某个应用去拉一条分支)。在分支上开发完我们就可以进行发布的操作啦!


这个时候我们就可以操作发布,我们填写需要的配置项后就可以点击发布按钮了。但是肯定不能让所有人随随便便就发布成功,所以我们要进行一些前置校验。比如说你有没有发布的权限、代码有没有冲突、是不是节假日或非发布窗口期、这个应用有没有正在被发布。。。等等的校验,总之就是确保代码是可以被你发布的。


然后我们的发布平台就会叫 Jenkins 拿着仓库信息、分支信息,以及其他等等的配置信息去仓库拉取代码了,拉到代码之后根据不同类型的应用进行区分,进行编译打包(这个过程不同应用之间是不同的),生成对应的产物。


1. 容器化发布


image.png



注:图中Wukong是我们自研DevOps平台



容器化发布发布的是镜像,镜像 id 代表了这次发布和这个镜像的关联关系。回滚的时候只需要找到这次发布对应的 id ,运维脚本根据这个 docker 的 id 找到 docker 镜像,直接部署这次 docker 镜像,做到回滚。由于发布的是 docker 的镜像,不仅可以保证产物是相同的,发布还很快。


容器化之前的发布:先找到对应的发布,根据这次发布找到对应的 tag,然后打包发布,但是这样只能保证业务代码是相同的,不能保证机器环境、打包机的环境、依赖的版本、打包的产物等等是一样的,并且需要的时间比容器化的方式慢得多。


2. oss发布


image.png


oss 发布和容器化发布流程的区别在于不用打包镜像而是将js、css等资源传到了 oss。通过 oss 发布的应用,只需要记住版本和 oss 上面资源路径的对应关系就可以了。


例如在我们这里的实现是:每次发布完成之后会记下有 hash 的 manifest 的地址,点击回滚后会根据发布 id 找到当次的产物,通过 oss 将 manifest 内容替换为有hash 的,从而就切换了访问的资源(html 的 manifest 地址不变,改变的是 manifest 文件的内容)。


3. 小程序


image.png
钉钉小程序的回滚就比较简单了,一般在我们点击回滚之后,内部会通过 http 接口调用小程序的 api 传递需要回滚的版本好后即回滚完成。或者你也可以选择手动到开发者后台的历史版本点回滚。
例如: open.dingtalk.com/document/or…


未来展望


有了完善的部署回滚机制,我们的产研团队才能有更好的交付体验。工作中的业务价值在我们整个交付内容占比应当是比较高的,而不应当把大量的时间花费在处理部署等流程上,让我们能够更快的去完成业务交付。


更好更稳定的回滚方式,能够让我们做到出现问题时快速恢复。这样才能保证一个较低的试错成本。


对于古茗来说,我认为一个很大的优势是,我们的规模不算很大,可以更好地做好研发流程对应的工具服务的统一,打通研发流程的各个流程,每个环节之间更好地进行串联,更好的助力业务发展。


作者:古茗前端团队
来源:juejin.cn/post/7295160228878106650
收起阅读 »

2D的雪碧图已经够炫了,那么3D的呢?

web
前言 前2篇文章,分别介绍了dat.gui和纹理贴图,老是理论没有实战也是没有什么意思的,今天我们就来着手一个小案例,赶紧实现起来,让你的博客更加炫酷! 这个案例包含了tweenjs动画库的使用,该动画库已在three.js中内置,路径为: examples/...
继续阅读 »

output-16_6_11.gif


前言


前2篇文章,分别介绍了dat.gui纹理贴图,老是理论没有实战也是没有什么意思的,今天我们就来着手一个小案例,赶紧实现起来,让你的博客更加炫酷!


这个案例包含了tweenjs动画库的使用,该动画库已在three.js中内置,路径为: examples/jsm/libs/tween.module.js,使用起来也是比较简单。


初始化


老样子,场景、相机、渲染器三要素初始化,并导入需要的插件库,插件库都已在three中内置:


import * as THREE from 'three';
// tween动画库
import TWEEN from 'three/addons/libs/tween.module.js';
//通过轨迹球控件TrackballControls 我们可以实现场景的旋转、缩放、平移等功能
import { TrackballControls } from 'three/addons/controls/TrackballControls.js'
// 雪碧图
import { CSS3DRenderer, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js'

// 定义场景、相机、渲染器
let scene, camera, renderer;

// 初始化
init()
// 渲染
animate();

function init() {
// 透视相机 远端距离最好设大一点 不然会展示不全
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
// 相机位置
camera.position.set(600, 400, 1500);
// 相机朝向位置
camera.lookAt(0, 0, 0);

// 场景
scene = new THREE.Scene();

// 渲染画布
renderer = new CSS3DRenderer();
renderer.setSize(innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
}

// 渲染
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera)
}

CSS3DSprite创建521个水球


// 定义小球数量
const particlesTotal = 512;
// 定义位置
const positions = [];
// 定义物体
const objects = []
const image = document.createElement('img');
image.addEventListener('load', () => {
for (let i = 0; i < particlesTotal; i++) {
// cloneNode() 方法可创建指定的节点的精确拷贝
const object = new CSS3DSprite(image.cloneNode())
// 随机分布位置 -2000 2000的立方体内
object.position.x = Math.random() * 4000 - 2000,
object.position.y = Math.random() * 4000 - 2000,
object.position.z = Math.random() * 4000 - 2000,
scene.add(object)

objects.push(object);
}
})
image.src = './static/img/sprite.png';


上面的代码中,我们创建了img标签,并使用CSS3DSpriteHTML元素转化为threejs的CSS3精灵模型,类似与转换成了three中的网格,并随机分布在-2000,2000的立方体中。


看下效果:


three04-1.jpg


添加控制器


关于控制器,前面也已经介绍过啦,通过控制器,我们就可以改变相机的位置,观察不同角度的物体。


// 定义控制器
let controls;
controls = new TrackballControls( camera, renderer.domElement );

// 渲染
function animate() {
...
controls.update();
...
}

注意哦,controls.update需要防止在animate中,每帧执行。


有了控制器。我们就可以实现交互啦:


output-15_37_12.gif


让小球按规律放大缩小


让小球按照正弦时间,放大缩小,即有一种闪烁的效果:


const time = performance.now();

for (let i = 0, l = objects.length; i < l; i++) {
const object = objects[i];
const scale = Math.sin((Math.floor(object.position.x) + time) * 0.002) * 0.3 + 1;
object.scale.set(scale, scale, scale);
}

output-15_37_59.gif


让小球生成特定图形


生成矩形,对应的每个小球坐标:


const amount = 8;
const separationCube = 150;
const offset = ( ( amount - 1 ) * separationCube ) / 2;

for ( let i = 0; i < particlesTotal; i ++ ) {

const x = ( i % amount ) * separationCube;
const y = Math.floor( ( i / amount ) % amount ) * separationCube;
const z = Math.floor( i / ( amount * amount ) ) * separationCube;

positions.push( x - offset, y - offset, z - offset );

}

tween.js使用


const position = {x: 0,y: 0};
;//创建一段tween动画
const tween = new TWEEN.Tween(position)
//经过2秒,position对象的x和y属性分别从零变化为100、50
tween.to({x: 100,y: 50}, 2000);
//tween动画开始执行
tween.start();
// 动画效果 类似annimation
tween.easing()
// 完成时执行的钩子,里面可以继续执行下一个操作
tween.onComplete()

使杂乱的小球变成矩形


import * as THREE from 'three';
// tween动画库
import TWEEN from 'three/addons/libs/tween.module.js';
//通过轨迹球控件TrackballControls 我们可以实现场景的旋转、缩放、平移等功能
import { TrackballControls } from 'three/addons/controls/TrackballControls.js'
// 雪碧图
import { CSS3DRenderer, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js'

// 定义场景、相机、渲染器
let scene, camera, renderer;
// 定义控制器
let controls;

// 定义小球数量
const particlesTotal = 512;
// 定义位置
const positions = [];
// 定义物体
const objects = []
let current = 0;
// 初始化
init()
// 渲染
animate();

function init() {
// 透视相机 远端距离最好设大一点 不然会展示不全
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
// 相机位置
camera.position.set(600, 400, 1500);
// 相机朝向位置
camera.lookAt(0, 0, 0);


scene = new THREE.Scene();

const image = document.createElement('img');
image.addEventListener('load', () => {
for (let i = 0; i < particlesTotal; i++) {
// cloneNode() 方法可创建指定的节点的精确拷贝
const object = new CSS3DSprite(image.cloneNode())
// 随机分布位置 -2000 2000的立方体内
object.position.x = Math.random() * 4000 - 2000,
object.position.y = Math.random() * 4000 - 2000,
object.position.z = Math.random() * 4000 - 2000,
scene.add(object)

objects.push(object);
}
transition()
})
image.src = './static/img/sprite.png';

// cube
const amount = 8;
const separationCube = 150;
const offset = ((amount - 1) * separationCube) / 2;

for (let i = 0; i < particlesTotal; i++) {

const x = (i % amount) * separationCube;
const y = Math.floor((i / amount) % amount) * separationCube;
const z = Math.floor(i / (amount * amount)) * separationCube;

positions.push(x - offset, y - offset, z - offset);

}

// 渲染画布
renderer = new CSS3DRenderer();
renderer.setSize(innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);

controls = new TrackballControls(camera, renderer.domElement);

}


// 动画
function transition() {
const offset = current * particlesTotal * 3;
const duration = 2000;
for (let i = 0, j = offset; i < particlesTotal; i++, j += 3) {
const object = objects[i];
new TWEEN.Tween(object.position)
.to({
x: positions[j],
y: positions[j + 1],
z: positions[j + 2]
}, Math.random() * duration + duration)
.easing(TWEEN.Easing.Exponential.InOut)
.start();
}
new TWEEN.Tween(this)
.to({}, duration * 3)
.onComplete(transition)
.start();
current = (current + 1) % 4;
}

// 渲染
function animate() {
requestAnimationFrame(animate);
controls.update();
TWEEN.update();

// 让小球按照正弦时间,放大缩小
const time = performance.now();

for (let i = 0, l = objects.length; i < l; i++) {

const object = objects[i];
const scale = Math.sin((Math.floor(object.position.x) + time) * 0.002) * 0.3 + 1;
object.scale.set(scale, scale, scale);

}
renderer.render(scene, camera)
}

杂乱的小球变成多种形态完整代码


限制文件大小啦,没法完全展示,大家自行运行看看吧!


output-15_49_5.gif


<div id="container"></div>
<script type="module">
import * as THREE from 'three';
// tween动画库
import TWEEN from 'three/addons/libs/tween.module.js';
//通过轨迹球控件TrackballControls 我们可以实现场景的旋转、缩放、平移等功能
import { TrackballControls } from 'three/addons/controls/TrackballControls.js'
// 雪碧图
import { CSS3DRenderer, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js'

// 定义场景、相机、渲染器
let scene, camera, renderer;
// 定义控制器
let controls;

// 定义小球数量
const particlesTotal = 512;
// 定义位置
const positions = [];
// 定义物体
const objects = []
let current = 0;
// 初始化
init()
// 渲染
animate();

function init() {
// 透视相机 远端距离最好设大一点 不然会展示不全
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
// 相机位置
camera.position.set(600, 400, 1500);
// 相机朝向位置
camera.lookAt(0, 0, 0);


scene = new THREE.Scene();

const image = document.createElement('img');
image.addEventListener('load', () => {
for (let i = 0; i < particlesTotal; i++) {
// cloneNode() 方法可创建指定的节点的精确拷贝
const object = new CSS3DSprite(image.cloneNode())
// 随机分布位置 -2000 2000的立方体内
object.position.x = Math.random() * 4000 - 2000,
object.position.y = Math.random() * 4000 - 2000,
object.position.z = Math.random() * 4000 - 2000,
scene.add(object)

objects.push(object);
}
transition()
})
image.src = './static/img/sprite.png';

// Plane
const amountX = 16;
const amountZ = 32;
const separationPlane = 150;
const offsetX = ((amountX - 1) * separationPlane) / 2;
const offsetZ = ((amountZ - 1) * separationPlane) / 2;
for (let i = 0; i < particlesTotal; i++) {
const x = (i % amountX) * separationPlane;
const z = Math.floor(i / amountX) * separationPlane;
const y = (Math.sin(x * 0.5) + Math.sin(z * 0.5)) * 200;
positions.push(x - offsetX, y, z - offsetZ);
}

// Cube
const amount = 8;
const separationCube = 150;
const offset = ((amount - 1) * separationCube) / 2;
for (let i = 0; i < particlesTotal; i++) {
const x = (i % amount) * separationCube;
const y = Math.floor((i / amount) % amount) * separationCube;
const z = Math.floor(i / (amount * amount)) * separationCube;
positions.push(x - offset, y - offset, z - offset);
}

// Random
for (let i = 0; i < particlesTotal; i++) {
positions.push(
Math.random() * 4000 - 2000,
Math.random() * 4000 - 2000,
Math.random() * 4000 - 2000
);
}

// Sphere
const radius = 750;
for (let i = 0; i < particlesTotal; i++) {
const phi = Math.acos(- 1 + (2 * i) / particlesTotal);
const theta = Math.sqrt(particlesTotal * Math.PI) * phi;
positions.push(
radius * Math.cos(theta) * Math.sin(phi),
radius * Math.sin(theta) * Math.sin(phi),
radius * Math.cos(phi)
);
}

// 渲染画布
renderer = new CSS3DRenderer();
renderer.setSize(innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
controls = new TrackballControls(camera, renderer.domElement);
}


// 动画
function transition() {
const offset = current * particlesTotal * 3;
const duration = 2000;
for (let i = 0, j = offset; i < particlesTotal; i++, j += 3) {
const object = objects[i];
new TWEEN.Tween(object.position)
.to({
x: positions[j],
y: positions[j + 1],
z: positions[j + 2]
}, Math.random() * duration + duration)
.easing(TWEEN.Easing.Exponential.InOut)
.start();
}
new TWEEN.Tween(this)
.to({}, duration * 3)
.onComplete(transition)
.start();
current = (current + 1) % 4;
}

// 渲染
function animate() {
requestAnimationFrame(animate);
controls.update();
TWEEN.update();

// 让小球按照正弦时间,放大缩小
const time = performance.now();

for (let i = 0, l = objects.length; i < l; i++) {

const object = objects[i];
const scale = Math.sin((Math.floor(object.position.x) + time) * 0.002) * 0.3 + 1;
object.scale.set(scale, scale, scale);

}
renderer.render(scene, camera)
}

</script>

作者:八月十八
来源:juejin.cn/post/7294301361835147290
收起阅读 »

你眼中的失业潮是怎样的?

逛 V 站刷到一个北漂的码农,他的生存指南直接让我破防,但也不得不佩服他的生存能力和心态。 先介绍下这位老哥的情况,普通本科非科班,9 年 IOS 开发经验,其中 1 年 RN,一年 flutter。北漂,名下无车,老婆和孩子都在北京,目前孩子在读幼儿园。 上...
继续阅读 »

图片


逛 V 站刷到一个北漂的码农,他的生存指南直接让我破防,但也不得不佩服他的生存能力和心态。


先介绍下这位老哥的情况,普通本科非科班,9 年 IOS 开发经验,其中 1 年 RN,一年 flutter。北漂,名下无车,老婆和孩子都在北京,目前孩子在读幼儿园。


上半年已经被优化裁员了,更新简历重新找工作,整体情况就是一周打招呼几十次,只成功投了 3 份简历,0 次面试。


实在不敢想象几个月没工作心态会崩成啥样。虽然没有做好心理准备,但也没有别的办法了。


社保代缴违法了也断缴了,每个月剩下 3k 左右,够孩子上两个月的幼儿园。


失业一段时间之后,注册了滴滴代驾,等待审核通过之后打算买一个折叠自行车。


发现北京的滴滴代驾已经没有名额了,只能等缺人的时候才能继续走下一步操作。


图片


然后注册了美团众包,新手第一天还得培训,培训完才能接单。


之后便开始做起了外卖员,第一次跑从早上 5 点到 7 点 50 分,跑了 6 单 ,15km 的路程,一共赚了 44 元,如果扣除保险的就是 41 元,时薪则是 16.4 元。


由于刚开始,并不累,只是觉得有点意思,目前心态还算好。有一个好处就是,不会觉得挣钱很容易了,坐办公室一天就是几百上千。


之所以会优先选择代驾和外卖的原因,是因为今年的行情的确很糟糕,找了几个月的工作还没找到,感觉这次的找工作周期并不短。


另一个原因是时间比较自由,不会影响我学习和面试。开发这行我也比较热爱,不愿意丢弃这份工作。


图片


并不是说找工作期间完全找不到工作。而是找的工作降薪太多,从企业的角度来说不稳定的因素也有。自己也不甘心,一旦降下去很难说下次按上次的高薪去涨。


由于北京代驾排队人数较多,暂时没有开放名额,所以入职了美团和小鸟。


从 5 月份 12 号开始跑,一开始基本上早上 5 点到 7 点 20 这段时间,然后回家接孩子上幼儿园。


只跑早上,是因为白天还得学习,后来发现没一个面试,索性白天时间段也开始跑了。


5 月份跑单统计下来,美团 275 单,一共 1900 元,扣了 180 元,有一次上午的单子已经餐损了,结果下午又流转到我这新手的头上了,由于没有经验不会操作导致餐损和违规算到我头上了,反馈给客服也不顶用。


蜂鸟 44 单,一共 280 元,扣除 20 元的保险和超时扣费。


每小时基本 20 元左右,叠加活动奖励差不多能到 26.7 元一小时,基本上 1元/km。这个 km 不止是单纯商家到顾客的距离,还要加上骑手接单时的位置到商家的距离。


所用的工具就是一辆最高 25km/h 的国标电动车。所以同时最多挂3单,那些车速快的我看到能接 9 单。没解速是因为觉得外卖只是暂时的,解速以后带孩子骑行就不安全了。


跑外卖整体的收获就是,觉得挣钱不容易,远不如之前坐办公室一天就好几百。甚至觉得跑外卖的时候还期盼下雨,因为会有天气补贴,1 单有 0.5-1 元的补贴,自己想想都觉得心酸。


但也没有那么焦虑了,虽然不够房租和日常开销,但好歹能够也算有收入。其实内心也是有点焦虑,不过在家里就是不能表现得太明显。


我比较喜欢开车,所以还是会去跑代驾。由于滴滴排队太慢了,到现在还在排队中,就先加入了别的代驾。


做代驾前,我做了一些功课。在抖音上搜各种档式,包括鸡腿挡、怀挡、旋钮挡等。还有各种车的启动方式,比如宝马、奔驰、路虎、特斯拉等。


心理建设还是有的,毕竟是开别人的车,怕刮蹭出事故。自己第一次上线接单前足足鼓励了半小时。


我没有买电动车和自行车,靠地铁和共享单车。因此接单我都跑市里,从 2 环到 4 环,跑偏僻地方的概率很小,不至于给我干到廊坊回不来。


刚开始的时候  14 点就出门,但是基本上 19 点开始才可能有单,再加上后来回家晚,第二天可能要补觉,所以基本上就是晚上 20 点到 凌晨。


图片


且家里住的比较远,基本上到家都在凌晨1点半以上,最晚曾到凌晨4点。


整体收入,没接过 200 元以上得大单,所以时薪在 0-120 元左右。由于单量不稳定,有时候要等个把小时才来单,现在每天差不多几十到 100元 不等。


有一次接了一个 300+ 的大单,预计开车 1 小时多点,但是被取消了,有点心痛!


与外卖相比大概率好点,除非代驾没单或者外卖有活动。


近两个月找工作情况,一共3个面试,其中2 个内推,1 个网投。一个非大厂,终面挂,2 个大厂,分别一面挂和二面挂。


目前已经降薪 30% 在找了,但是还没有找到,笑哭。


同时也在继续开发自己的 APP,由于苹果账号到期了所以现在还没搜到。目前正在做得内容有 商品码识别、小组件和内购,希望掌握技能的同时增加收入。


看到这里,我默默的问一句,大家眼中的失业潮是怎样的?


看了不少的消息,基本上有一个共同的意识就是,环境不太好,都在挣扎做一些副业之类挣额外的收入来维持。


另外代驾和外卖数量逐渐开始到达红线的消息传来,也不是空穴来潮。


我所在的开发团队,也都有一个共同意识,就是做好留一手的准备,该考驾-照的考驾-照,该练体力的练体力,该做其他准备的做其他准备。


公司个月都在汇报信息化建设程度,我们疯狂给他们开发系统,开发工具,省人省力省成本。


做到了一定程度竟然听到在清算人力,听到之后不禁打了个寒颤,至少我也是帮凶了吧。


文中主人公 V 站地址👉:http://www.v2ex.com/t/956789


作者:桑小榆呀
来源:juejin.cn/post/7293804880707059750
收起阅读 »

重复请求优化

web
设想一种场景,有两个组件,这两个组件在初始化阶段,都需要调用同一个 API 接口去获取数据。为了防止请求冗余,可以把两个组件的请求都挪到父组件中,由父组件统一调用一次请求,然后再将响应的数据结果传给两个子组件。这种方法应该是最常见的,不过它也有一个局限性条件:...
继续阅读 »

设想一种场景,有两个组件,这两个组件在初始化阶段,都需要调用同一个 API 接口去获取数据。为了防止请求冗余,可以把两个组件的请求都挪到父组件中,由父组件统一调用一次请求,然后再将响应的数据结果传给两个子组件。这种方法应该是最常见的,不过它也有一个局限性条件:两个组件必须有一个共同的祖先组件,如果这两个组件是同级的兄弟组件倒也还好,如果非同级,那么数据的传参就会有些麻烦了。那么还有其他办法吗?当然是有的。


我们可以换个思路,每个组件还是保持原有的业务逻辑不变,从请求接口处做文章。既然是同一个接口调用了两次,而且还是返回了相同的请求结果,那么不妨在第一次时调用成功时,就把请求结果缓存起来,等到第二次再调用时,直接返回缓存的数据。按照这个思路可以写出第一版的代码(这里用了 TS 方便查看参数的类型):


/**
* 缓存请求的响应结果
* 把发起请求的 Promise 对象挂载在原型对象上
* @param request 请求函数
*/

function cacheRequest<T>(request: (...args: any[]) => Promise<T>) {
const cache = Symbol("cache")
return function (...args: any[]): Promise<T> {
if (!request.prototype[cache]) {
request.prototype[cache] = request(...args)
}
return request.prototype[cache]
}
}


  • 首先 cacheRequest 函数,需要接收一个参数 requestrequest 是一个返回结果为 Promise 对象的函数。cacheRequest 执行完后返回一个新的匿名函数。

  • 然后,在匿名函数的内部,先判断 request 的原型对象上是否有 cache(这里的 cache 使用了 Symbol 类型,确保键名唯一)。也即,是否有缓存过的请求结果,如果没有,说明是第一次调用,则将 request 的执行结果存到缓存里。如果有缓存,则直接返回缓存。

  • 可以看到,缓存也是一个 Promise 类型。在同时调用多次请求时,只要在第一次调用执行后,已经把 Promise 存到缓存里了,后续的请求返回的也是缓存里的 Promise,从而保证多个请求都指向同一个 Promise ,也即只会调用一次 API 接口。

  • 这里需要注意一点,由于需要往 request 的原型对象上挂载缓存,所以 request 不能是箭头函数。因为箭头函数没有 this,也就意味着没有原型对象。


小测一下:


function cacheRequest(request) {
const cache = Symbol("cache")
return function (...args) {
if (!request.prototype[cache]) {
request.prototype[cache] = request(...args)
}
return request.prototype[cache]
}
}

const request = function () {
return new Promise(resolve => {
console.log("fetch request")
setTimeout(resolve, 2000)
})
}

const newRequest = cacheRequest(request)

newRequest()
newRequest()
newRequest()

version1.png


可以看到虽然 newRequest 调用了三次,但是 fetch request 只打印了一次,也就是说 request 只调用了一次,符合预期!但是,最后一次 newRequest 的调用,是在 3 秒后调用的,也是走的缓存,没有重新执行。仔细思考一下,后续无论什么时候调用 newRequest 都会使用缓存里的数据,不会重新调用请求了,这显然是不合理的。我们还需要加个缓存的过期时间,超过这个时间,就重新发起新的请求。第二版如下:


/**
* 缓存请求的响应结果
* 把发起请求的 Promise 对象挂载在原型对象上
* 保证在 cacheTime 时间间隔内的多次请求,只会调用一次
* @param request 请求函数
* @param cacheTime 最大缓存时间(单位毫秒)
*/

export function cacheRequest<T>(request: (...args: any[]) => Promise<T>, cacheTime = 1000) {
const cache = Symbol("cache")
const lastTime = Symbol("lastTime")
return function (...args: any[]): Promise<T> {
if (!request.prototype[cache] || Date.now() - request.prototype[lastTime] >= cacheTime) {
request.prototype[cache] = request(...args)
request.prototype[lastTime] = Date.now()
}
return request.prototype[cache]
}
}


  • 首先,cacheRequest 新增一个入参 cacheTime,用于设置过期时间,默认为 1 秒。

  • 其次,在原型对象上新增了一个 lastTime 属性,用来记录最后一次调用的时间。

  • 当缓存为空,或者当前时间距离上一次调用时间超过缓存过期时间时,更新 cachelastTime


再来小测一下:


function cacheRequest(request) {
const cache = Symbol("cache")
const lastTime = Symbol("lastTime")
return function (...args) {
if (!request.prototype[cache] || Date.now() - request.prototype[lastTime] >= cacheTime) {
request.prototype[cache] = request(...args)
request.prototype[lastTime] = Date.now()
}
return request.prototype[cache]
}
}

const request = function () {
return new Promise(resolve => {
console.log("fetch request")
setTimeout(resolve, 2000)
})
}

const newRequest = cacheRequest(request)

newRequest()
newRequest()
setTimeout(newRequest, 3000)

version2.png


这一次,fetch request 打印了两次,符合预期,完美!


作者:showlotus
来源:juejin.cn/post/7294597695478333476
收起阅读 »

接手了一个外包开发的项目,我感觉我的头快要裂开了~

嗨,大家好,我是飘渺。 最近,我和小伙伴一起接手了一个由外包团队开发的微服务项目,这个项目采用了当前流行的Spring Cloud Alibaba微服务架构,并且是基于一个“大名鼎鼎”的微服务开源脚手架(附带着模块代码截图,相信很多同学一看就能认出来)。然而,...
继续阅读 »

嗨,大家好,我是飘渺。


最近,我和小伙伴一起接手了一个由外包团队开发的微服务项目,这个项目采用了当前流行的Spring Cloud Alibaba微服务架构,并且是基于一个“大名鼎鼎”的微服务开源脚手架(附带着模块代码截图,相信很多同学一看就能认出来)。然而,在这段时间里,我受到了来自"外包"和"微服务"这双重debuff的折磨。


image-20231016162237399


今天,我想和大家分享一下我在这几天中遇到的问题。希望这几个问题能引起大家的共鸣,以便在未来的微服务开发中避免再次陷入相似的困境。


1、服务模块拆分不合理


绝大部分网上的微服务开源框架都是基于后台管理进行模块拆分的。然而在实际业务开发中,应该以领域建模为基础来划分子服务。


目前的服务拆分方式往往是按照团队或功能来拆分,这种不合理的拆分方式导致了服务调用的混乱,同时增加了分布式事务的风险。


2、微服务拆分后数据库并没拆分


所有服务都共用同一个数据库,这在物理层面无法对数据进行隔离,也导致一些团队为了赶进度,直接读取其他服务的数据表。


这里不禁要问:如果不拆分数据库,那拆分微服务还有何意义?


3、功能复制,不是双倍快乐


在项目中存在一个基础设施模块,其中包括文件上传、数据字典、日志等基础功能。然而,文件上传功能居然在其他模块中重复实现了一遍。就像这样:


image-20231017185809403


4、到处都是无用组件堆彻


在项目的基础模块中,自定义了许多公共的Starter,并且这些组件在各个微服务中被全都引入。比如第三方登录组件、微信支付组件、不明所以的流程引擎组件、验证码组件等等……


image.png


拜托,我们已经有自己的SSO登录,不需要微信支付,还有自己的流程引擎。那些根本用不到的东西,干嘛要引入呢?


5、明显的错误没人解决


这个问题是由上面的问题所导致的,由于引入了一个根本不需要的消息中间件,项目运行时不断出现如下所示的连接异常。


image-20231013223714103


项目开发了这么久,出错了这么久,居然没有一个人去解决,真的让人不得不佩服他们的忍受力。


6、配置文件一团乱麻


你看到服务中这一堆配置文件,是不是心里咯噔了一下?


image-20231017190214587


或许有人会说:"没什么问题呀,按照不同环境划分不同的配置文件”。可是在微服务架构下,已经有了配置中心,为什么还要这么做呢?这不是画蛇添足吗?


7、乱用配置中心


项目一开始就明确要使用Apollo配置中心,一个微服务对应一个appid,appid一般与application.name一致。


但实际上,多个服务却使用了相同的appid,多个服务的配置文件还塞在了同一个appid下。


更让人费解的是,有些微服务又不使用配置中心。


8、Nacos注册中心混乱


由于项目有众多参与的团队,为了联调代码,开发人员在启动服务时不得不修改配置文件中Nacos的spring.cloud.nacos.discovery.group属性,同时需要启动所有相关服务。


这导致了两个问题:一是某个用户提交了自己的配置文件,导致其他人的服务注册到了别的group,影响他人的联调;二是Nacos注册中心会存在一大堆不同的Gr0up,查找服务变得相当麻烦。


其实要解决这个问题只需要重写一下网关的负载均衡策略,让流量调度到指定的服务即可。据我所知,他们使用的开源框架应该支持这个功能,只是他们不知道怎么使用。


9、接口协议混乱


使用的开源脚手架支持Dubbo协议和OpenFeign调用,然而在我们的项目中并不会使用Dubbo协议,微服务之间只使用OpenFeign进行调用。然而,在对外提供接口时,却暴露了一堆支持Dubbo协议的接口。


10、部署方式混乱


项目部署到Kubernetes云环境,一般来说,服务部署到云上的内部服务应该使用ClusterIP的方式进行部署,只有网关服务需要对外访问,网关可以通过NodePort或Ingress进行访问。


这样做可以避免其他人或服务绕过网关直接访问后端微服务。


然而,他们的部署方式是所有服务都开启了NodePort访问,然后在云主机上还要部署一套Nginx来反向代理网关服务的NodePort端口。


image-20231016162150035


结语


网络上涌现着众多微服务开源脚手架,它们吸引用户的方式是将各种功能一股脑地集成进去。然而,它们往往只是告诉你“如何集成”却忽略了“为什么要集成”。


尽管这些开源项目能够在学习微服务方面事半功倍,但在实际微服务项目中,我们不能盲目照搬,而应该根据项目的实际情况来有选择地裁剪或扩展功能。这样,我们才能更好地应对项目的需求,避免陷入不必要的复杂性,从而更加成功地实施微服务架构。


最后,这个开源项目你们认识吗?


image-20231017190633190



关注公众号 Java日知录 获取更多精彩文章



作者:飘渺Jam
来源:juejin.cn/post/7291480666087964732
收起阅读 »

某月薪5万的朋友关于处理BUG的心得

引言 大家好,我是亿元程序员,一位有着8年游戏行业经验的主程。 今天我想和大家分享一个有趣的故事。这个故事的主角是我的一个朋友,他是一位资深的软件工程师,月薪高达5万。你可能会觉得这个收入水平的人应该过着无忧无虑的生活,但是他告诉我,他的工作压力非常大,尤其是...
继续阅读 »

引言


大家好,我是亿元程序员,一位有着8年游戏行业经验的主程。


今天我想和大家分享一个有趣的故事。这个故事的主角是我的一个朋友,他是一位资深的软件工程师,月薪高达5万。你可能会觉得这个收入水平的人应该过着无忧无虑的生活,但是他告诉我,他的工作压力非常大,尤其是在处理BUG的时候。


BUG是什么


手下留情,我不是你们要找的BUG


BUG,这个词在软件开发中是非常常见的。它是计算机程序中的一个错误或问题,可能会导致程序无法正常运行。对于软件工程师来说,找到并修复这些BUG是一项非常重要的任务


心得


高手支招


这位朋友告诉我,他在处理BUG的过程中,有以下几点心得





  1. 耐心:找到并修复BUG需要非常强的耐心。有时候,你可能需要花费几个小时,甚至几天的时间来追踪一个BUG的来源。这需要你有足够的耐心去面对这个问题。




  2. 细心:在处理BUG的过程中,你需要非常细心。因为BUG可能隐藏在程序的任何一个地方,只有通过仔细的检查,才能找到它。




  3. 创新:有时候,传统的解决方法可能无法解决某个BUG。这时候,你需要有创新的思维,去寻找新的解决方案。




  4. 团队协作:处理BUG并不是一个人的事情,而是需要整个团队的协作。每个人都有自己的专长,只有通过团队的协作,才能更有效地解决问题。




  5. 不断学习:技术是不断发展的,新的编程语言、新的开发工具、新的开发方法会不断出现。作为软件工程师,你需要不断学习,才能跟上技术的发展。





我这位朋友的心得让我深受启发。他告诉我,虽然处理BUG的过程可能会很艰难,但是当你成功解决一个问题的时候,那种成就感是无法用言语表达的


我的看法


patience is key in life


首先,让我们来谈谈耐心。在游戏开发中,找出BUG的来源是一项非常复杂的任务。你可能需要查看大量的代码,甚至需要反复测试多次,才能找到问题的根源。这个过程可能会非常耗时,但是没有耐心的话,你可能会错过一些重要的线索。因此,耐心是处理BUG的第一步


细心


其次细心也是非常重要的。在编写代码的过程中,程序员可能会犯一些错误,这些错误可能会导致程序无法正常运行。这些错误可能是语法错误,也可能是逻辑错误。只有通过仔细的检查,才能找到这些错误。因此,细心是处理BUG的第二步


创新


接下来,我们来谈谈创新。在游戏开发中,传统的解决方法可能无法解决某些复杂的问题。这时候,你需要有创新的思维,去寻找新的解决方案。例如,你可以尝试使用新的编程语言或者新的开发工具来解决问题。因此,创新是处理BUG的第三步


团队协作


然后,我们来谈谈团队协作。在游戏开发中,处理BUG并不是一个人的事情,而是需要整个团队的协作。每个人都有自己的专长,只有通过团队的协作,才能更有效地解决问题。例如,设计师可以帮助程序员理解用户的需求,测试人员可以帮助程序员发现程序的问题。因此,团队协作是处理BUG的第四步


不断学习


最后,我们来谈谈不断学习。技术是不断发展的,新的编程语言、新的开发工具、新的开发方法会不断出现。作为游戏开发者,你需要不断学习,才能跟上技术的发展。只有这样,你才能更好地处理BUG。因此,不断学习是处理BUG的最后一步


结语


总的来说,处理BUG是一项需要耐心、细心、创新、团队协作和不断学习的任务。虽然这个过程可能会很艰难,但是当你成功解决一个问题的时候,那种成就感是无法用言语表达的。


希望这些心得能对你们有所帮助


在哪里可以看到如此清晰的思路,快跟上我的节奏!关注我,和我一起了解游戏行业最新动态,学习游戏开发技巧。


我是"亿元程序员",一位有着8年游戏行业经验的主程。在游戏开发中,希望能给到您帮助, 也希望通过您能帮助到大家。


AD:笔者线上的小游戏《重力迷宫球》《贪吃蛇掌机经典》《填色之旅》大家可以自行点击搜索体验。


作者:亿元程序员
来源:juejin.cn/post/7294563074936356875
收起阅读 »

废掉一个程序员的十大铁律,你中招了吗?

写在前面 废掉一个人,其实还是挺简单的。最简单的两种方式就是:让他忙,一直忙;让他闲,一直闲。 一个人开始废掉的标志 终日懒散,无所事事;没有目标,没有规划;不想上班,只想打游戏;除了这些,可能下一个将要废掉的人,就是看似每天都很忙碌的你! 冰河在多年的从业...
继续阅读 »

写在前面


废掉一个人,其实还是挺简单的。最简单的两种方式就是:让他忙,一直忙;让他闲,一直闲。


一个人开始废掉的标志


终日懒散,无所事事;没有目标,没有规划;不想上班,只想打游戏;除了这些,可能下一个将要废掉的人,就是看似每天都很忙碌的你!


图片


冰河在多年的从业生涯中,总结出10条序员如何让自己废掉的铁律。大家对号入座。


如何让自己更快的废掉?


图片


忙得要死


有人说,废掉一个程序员最隐蔽的方式,就是让他忙到没有时间成长。每天定的计划特别满,正常工作时间完不成的,必须留下来加班。程序员每天都要很努力的工作,不停的coding,写业务代码。每天都非常忙碌,甚至周末都不能休息。有些公司“996”还不行,还要弄个“007”出来,程序员完全无法留出时间自我总结和成长。


冰河见过太多这样的人了,工作几年了,只会写CRUD。为哈?因为他太忙了,忙到没有时间提升自己。很多工作三年的程序员就只会CRUD。跳槽面试时,一旦被问到性能调优、高并发、Dubbo、SpringCloud等底层的原理和技术时,基本就歇菜了。


闲得要命


有人说,废掉一个程序员最快的方式,是让他闲着,让他没有方向感,不知道干啥。这类程序员也不会规划自己的时间,只是被动的接受别人的安排。时间久了,忘记了自己是干啥的,成功的废掉了自己。


拖延症


这类程序员就是自己把自己拖死的。工作任务不重,但是每天会加班到很晚,开始觉得很勤奋,但是细想,就这点工作不至于加班这么久吧?一问才知道,这是拖延症的表现。有些程序员做事情喜欢拖延,工作前要先聊会QQ,聊会微信,看看微博,刷刷朋友圈。总觉得时间还早,晚点再干吧。久而久之,养成了难以戒掉的拖延症。


吃老本


不及时更新自己的技术栈,拒绝接受新技术。总觉得自己的技术很牛X,殊不知,你的技术已经赶不上时代的潮流了,刚毕业的小学生掌握的技术都比你先进。


不会规划


对自己的职业生涯没有规划,未来朝着哪些方向努力,完全不知道。当一天和尚,撞一天钟。一旦公司或者企业出现变故,这类程序员基本没戏。


没有目标


这点与不会规划有些雷同,但是更加强调的是目标感。没有目标感,无法更好的驱动自己去完成相应的事项,无法让自己更聚焦到优先级高的事项中,即使再忙,也是眉毛胡子一把抓,最后累苦了自己,还拖累了别人。


自我感觉良好


这类程序员就是自我感觉啥都会,将自己封闭在自身的世界中,不愿意与他人沟通和交流,觉得自己都是对的,就这么干!美其名曰:自我良好!实际上,啥也不是。久而久之,不废才怪!


专注度不够


做事情注意力不集中,写两行代码,看看手机、刷刷微博和朋友圈,无法让自己专注的完成手上的任务。时间久了,养成了三心二意的习惯。


知识面欠缺


不愿拓宽自己的技术栈,发自内心愿意当一颗小小的螺丝钉,不愿意尝试新技术和新业务,守着自己的一亩三分地,反反复复就那些CRUD操作。


啥都想学


这类程序员本质上就是没有一个方向,对自己的职业生涯没有规划,啥都想学,一会想学Java,一会想学Python,一会又想学Go,最后又想学大数据。到头来,却是啥也没学会。


以上这些,希望小伙伴们有则改之,无则加勉~~


最后,除上面10条铁律外,还有一点就是 改需求 也能让程序员死得快些!


好了,今天就到这儿吧,小伙伴们,你们知道哪些能够让程序员更快废掉的方式呢?请在文末留言,说出你的观点。我是冰河,我们下期见~~


作者:冰_河
来源:juejin.cn/post/7294853623849435170
收起阅读 »