注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

请不要“妖魔化”外包岗位!——一文带你正确认识外包

一、背景一转眼,又到了金三银四的跳槽&求职季。在IT行业,跳槽就离不开一个词,那就是外包。可以说,每一位IT人都接触过外包,甚至参与其中。而多数IT职场萌新,都面临着大厂外包,还是小公司的绝望抉择。虽然很多人虽然抵制外包,但他们往往对外包只有比较直观、...
继续阅读 »

一、背景

一转眼,又到了金三银四的跳槽&求职季。
在IT行业,跳槽就离不开一个词,那就是外包。可以说,每一位IT人都接触过外包,甚至参与其中。而多数IT职场萌新,都面临着大厂外包,还是小公司的绝望抉择。虽然很多人虽然抵制外包,但他们往往对外包只有比较直观、碎片的认识。
网上针对IT外包的资料,很少很少,而且大多比较零碎。我恰巧对外包算比较有经验(经历详见我之前的 从零到阿里的三年)。
所以我想谈一谈外包。希望能给需要的小伙伴,一些参考与帮助。

二、分析

1.什么是外包

为了更好地分析,我们需要了解什么是外包。
外包是一种将目标,委托给其他组织的管理模型。
外包有很多种,如项目外包、产品外包、工程外包等等。而我们最为关心的,则是人力资源外包。
这样说比较抽象,我来举个例子。

  • 项目外包:为了完成某个项目,出于进度、成本,甚至是风险转移的考量,将项目拆分一部分(如非核心部分)交给其他个人/组织。比如猪八戒网上的一些项目,就是这样的项目。
  • 产品外包:多数出于成本考量,将部分产品外包给其他个人/组织。比如战神5将部分场景、人物模型外包给外部团队完成。
  • 工程外包:多数出于成本、风险、进度等考量,将工程交给其他组织。比如包工头承诺完成大楼的墙壁粉刷等。
  • 人力外包:多数出于成本(也有是对上层政策的对策)的考量,将员工合同签署到其他人力资源公司等。比如国内IT行业的中软国际,员工与中软国际签合同,但却在阿里、大搜车等公司驻场工作(也很多有与目标公司分开的情况)。

2.二方外包VS三方外包

我们有时候会听到招聘人员说自己是二方外包,或者直接说自己不是外包,只是合同签署公司有所差别,和正式员工没有什么区别。抛开那种没底线的欺骗行为,到底什么是二方外包呢?它与三方外包的差别是什么?

最直接的区别,三方外包的合同都是与独立的第三方人力资源公司签署,二方外包的合同是与目标公司的关联公司(多为控股子公司)签署。
这里直接举个例子吧。我是一个即将成为盒马外包的开发人员。

  • 三方外包:我和一家与盒马不入股的中软国际签署合同。
  • 二方外包:我和一家由盒马控股的上嘉签署合同。

透过现象看本质。这两种合同的签署方式,直接决定了你和目标公司(如盒马)的关系。
盒马无法直接管理三方外包,甚至说两者解除合作关系后,你就无法在盒马工作了,所以盒马对三方外包员工的信任会比较低。体现到实际表现中,就是三方外包员工的权限总是很有限。另外,三方外包员工即使无法在盒马工作,也可以被三方外包公司派遣到支付宝等其他公司。所以,盒马与三方外包公司基本都是把三方外包员工视为商品,人力资源商品。
但是盒马有权管理二方外包,所以在工作上会更加信任二方外包员工。而且这个二方外包只会服务于盒马,所以在盒马会在一定程度上把二方外包员工当自己的正式员工看待。

搞清楚了外包员工与目标公司的关系(合同关系),自然就清楚了同样是外包,为什么二方比三方有着更好的待遇&机会。

3.外包的优点

虽然很多开发人员都抵制外包,但实际情况则是依旧有大量开发,选择加入到外包这个圈子。
这说明,外包一定是有好处的。所以,我简单归类了三点好处。

a.面试门槛

外包的面试门槛,相对大厂要低很多。尤其一些初级岗位,真的是有手就行。
原因很简单,有三点:

  • 对于三方外包公司而言,外包员工都是商品,商品越多,公司越赚钱。所以三方外包公司一定会极力帮助你通过面试,包括但不限于给面试资料、透露考题等。
  • 目标公司的面试官大多也不会太重视。而且面试内容,相较于正式员工,更多集中于实用技能,不会出于潜力考虑,询问诸如项目管理、业务思考深度等问题。
  • 即使一家公司翻车了,三方外包公司还会推荐别的公司。概率学上来说,总会通过的。囧

所以,在穷途末路时可以考虑先去外包混一混,别把自己饿死。

b.薪资水平

可能很多人并不知道目标公司给外包公司的合同价。一般来说,你和三方外包公司谈到的最高价,再提升30%~50%,便是目标公司给外包公司的合同价。之所以这么高,是因为正式员工的福利待遇比外包好太多了,比如十六薪、旅游、商业保险等。另外,目标公司政策上会卡住正式员工HC的。
三方外包员工的薪资上限是由级别确定的。而这个级别是面试过程中,目标公司面试官确定的。
你看懂了嘛?看出来什么了嘛?机智的小伙伴已经看到薪资大幅提升的方法了。
是的。只要你确定了你的级别,那么无论你之前薪资多低,你都可以和三方外包公司要这个级别的最高薪资。
因为对于三方外包公司而言,当你级别确定后,目标公司就会给出一个确定的合同价格,为你付钱给三方外包公司。所以,只要你的要价没超出三方外包公司对这个级别设定的薪资上限,他们就一定会和你签合同。毕竟多一个合同,就多赚一份钱。
而在正式员工中,多数情况下,HRG都会以应聘者上一份工作涨薪30%左右为上限。因为再高的价格就得走审批了,流程会比较麻烦。所以除非你非常优秀,否则薪资上限就在那里摆着。

这里说一下我当初的情况,我当初的薪资就是从11k x 12,直接跳到20k x 12。

其实从从零到阿里的三年 的相关经历就可以看出这三个月不到的三方外包经历,帮我实现了短短三个月从11k x 12,到20k x 12,再到24.5k x 16近三倍多工资的巨大跳跃。

当时入职时的级别是资深开发工程师。但是入职一个月后,又被调整为技术专家。所以,我也不确定20k x 12是针对资深开发工程师级别,还是技术专家级别。另外,外包公司的技术专家,大家看着开心就行了。

所以,外包是可以实现薪资的大幅提升的一种方式。

c.学习机会

很多人知道外包的种种不好,但还是选择去外包,这是为什么呢?因为很多人,包括我在内,都相信外包有接触大佬,接触复杂系统、接触大型项目的学习机会。
有一说一,外包虽然没有招聘人员提的那么好学习机会,但却是有一定的学习机会。你可以接触到大佬的代码、架构图,甚至负责项目。
不过,所谓的学习机会完全取决于目标团队。目标团队给你多少文档权限、给你多少代码权限,以及你与目标团队的协作方式,都极大影响了这个学习机会。不得不说,二方和三方的学习机会相差是非常大的。

说一下我在二方,也就是在盒马的情况。首先,我要感谢我所在的团队,尤其是我的一二级主管对我非常照顾,给了我很多机会。非常感谢。由于团队与二级主管(P8)的开明,作为二方的我几乎享有正式员工的所有权限。只要能开的权限,都对我放开审批通过权限。而由于一级主管(P7)的信任与支持,我甚至拥有超出一般员工的项目机会、业务沟通、管理提升。不过,随着二方员工的权限抵达边界、上升渠道卡死,以及最重要的一二级主管离去,我也在近两年的工作后后选择离开。

所以,外包是可以有学习机会的,但取决于所在的团队

4.外包的缺点

说完优点,接下来说说缺点。
虽然大部分人都抵制外包,但是很多人,尤其是萌新,并不清楚外包的主要缺点。
我这里简单归纳一下。

a.工作碎片化

外包的工作内容,大多十分碎片化,甚至是机械化。
因为如果这个工作内容真的很完整、成块儿,那正式工就做掉了。正式工做掉的理由有两个:

  • 完整工作内容有利于他,去构建业务认知。
  • 完整内容拆分出来外包,需要进行进行大量的沟通与团队协作,不利于整体效率。

那么有没有办法避免碎片化呢?答案是有的。一方面可以表现出自身能力,获取正式团队信任,从而获取更完整工作内容。另一方面,从碎片内容中找到联系,构建自身认知体系,从而让碎片化的内容,不再碎片。这个是职业通用技能,实操是有一定困难的,有机会可以聊聊。

工作内容的碎片化,就带来了两个最直观的后果:

  • 提升困难:工作内容的碎片化,导致自身在技术和业务上难以提升,进而影响晋升、转正等,乃至后续可能的面试。
  • 缺乏重量级面试项目:由于工作内容很零碎,面试时无法进行整合,从而获得一个较为完整&复杂的项目。进而导致无法在面试中很好地表现自身实力,影响后续面试结果。

b.缺乏上升通道

这里辟谣一波,许多外包都说有转正机会。实际情况是几乎等于零。 其中,三方外包更是可以直接和零划等号。
三方外包的转正,往往就是给个内推机会,然后和面试官会熟悉一些。然而这些都没什么价值。内推的机会,简直不要太好找的说。现在的大厂,大部分人才招聘,都是技术部员工直接内推的。至于面试官的熟悉,只能说大多数情况下那只会让你的面试更加困难。
至于二方外包的转正,大多也会对绩效、贡献等诸多方面有要求,还需要一二级主管进行推荐。另外,还需要经历审批,以及转正答辩。

靠谱的晋升通道,可能也只有外包公司自己的晋升通道了。具体,我也不太清楚。毕竟我在两家外包公司都是技术最高级了。不过,外包公司的外包员工技术级别,我见过最高的是技术专家,公司宣称对标阿里p6+。

那么到底有没有外包转正式?答案是有的。我见过盒马测试团队的二方外包晋升正式成功了。然而,人家根本没接受,直接跑到支付宝某部门了。

所以,现实是能从外包晋升正式员工的人,水平早超出那条线了,完全可以走正常社招流程。当时作为外包的我,当然也很关注这方面啦。我当时咨询了很多人,得出的结论是,同一个人,晋升的得到的薪资待遇,会比社招得到的待遇低很多。结论就是,能外包晋升正式工,也别走晋升途径,直接去走社招途径。 当然,如果和这个团队离不开什么,与团队有了较深的羁绊,那就没办法了。

所以,外包终究不是归宿晋升通道窄,且晋升性价比很低。

c.温水煮青蛙

很多人都知道外包工作不是最终归宿,为什么还有那么多人一条路走到黑,最后黯然离开?答案很简单,就是 温水煮青蛙 。
一方面,外包的工作往往两极分化,要么一堆碎片化事情,要么无所事事。这对于有一定能力的小伙伴,摸鱼不要太容易。外包的工作考核很是简单,尤其数量最多的三方外包。另一方面,外包的薪资还是说得过去的。属于那种虽然买房买车会有点吃力,但是日常生活还是可以过得比较潇洒的。总结一下,活少钱多压力小,就像温水一般,将外包员工们慢慢麻醉,最终死去等到公司抛弃了自己,才发现自己已经失去了市场竞争力了。

d.心理压力

多数外包,都会承受着低人一等的压力。
这个压力,往往不是来自周边的小伙伴,而是来自周边的环境,甚至是来自外包员工自己。

压力来源于:

  • 外部:
    • 狗牌:狗牌是心理压力的最大来源。阿里的狗牌是可以很容易区分正式员工和外包员工的,每次进门刷卡,就会感到压力,那种与众不同的压力。我就见过很多外包的朋友,从正式员工那里获得正式狗牌壳,替换自己的外包员工狗牌壳。说实话,我当时也感觉一些压力。但我一直都没有这样做,因为我坚信我并不比那些正式员工差,我只是欠缺一个自己给自己的机会。不过平时开会,我也只是把狗牌放兜里。囧
    • 福利待遇:大厂的福利待遇不错,但其中许多福利待遇是外包享受不到的。比如旅游,三方外包基本无法享受免费跟团旅游的。结果就是大家在谈论旅游,你只能默默敲电脑。还有文化衫,很多时候公司的文化衫,是不会算着外包的文化衫的。看着周边都穿上了统一样式的文化衫,只有你格格不入地穿着自己的衣服,心里压力陡增。
    • 权限:正式员工的权限与外包员工的权限存在很大差异。当主管在群里发了一篇内部技术论坛的帖子,表示需要大家观摩学习。结果你发现自己没有权限访问&申请,心态简直裂开。
  • 自我:人或多或少,都会有自我中心的倾向。比如有一天你平地摔了一跤,你周边的小伙伴一周不到就忘了。结果你为此纠结了好几个月,认为很是丢人。很多敏感的小伙伴,甚至会把一些正常的行为动作,解读出别的含义。比如主管看到你来了,切换了一下屏幕,你就认为主管在向你隐瞒什么。

这种心理压力,虽然有可能使当事人寻求突破。但更多人是被这种压力,压得喘不过来气。
一方面需要团队与主管的关心&照顾(这里再次感谢我的一二级主管),另一方面需要靠自我。靠自我,主要依据自己对现状的清醒认识(自己在外包的目的等,详见下),以及自己的职业规划(明确的职业规划,可以大幅减少自我焦虑)。

5.如何选择外包

虽然外包听着不好听,但是依然有大量的人进入外包,那么该如何选择外包岗位呢?
多数人选择外包,无非三类:

  • 作为临时的工作
  • 作为跳薪的踏板
  • 作为提升的一步

这里就这三类主要目的,谈一下该如何选择外包岗位。

a.临时工作

如果是临时工作,建议直接在三个月内离开
一方面脱离试用期,离开手续会很麻烦。更重要的是心理上也会有感情,存在“要不就这里”的心理。另一方面一旦超过三个月,在大厂背调环节,则会被列入考察。对公司&团队而言,早点离开,总比半年后离开,更容易接受(无论是培养、情感,抑或是损失)。
临时工作,那就选择试用期在三个月及以上。在此基础上,还需要考虑:

  • 试用期薪资高:不仅仅是工资,而且是试用期工资。因为一些外包offer,试用期薪资打八折,而有的外包offer则不然。
  • 试用期个人时间多:这点就比较油滑了。首先临时工作,意味着后面需要跳槽,那么自然需要不少时间去准备面试。其次,有的工作试用期工作量会比较少。最后,有的工作虽然工作量不少,但是缺乏对外包员工的管控,完全可以上班准备面试。囧

b.跳薪踏板

如果是跳薪踏板,建议早日落实,并且选择月薪高的。
绝大多数情况下,外包薪资涨薪高,仅适用于月薪25k以下。因为正常开发外包,是有月薪25k的一条线的。阿里这边几个BU的二方外包,封顶25k月薪,但一般可以13 + 3。所以,如果想拿外包做跳薪踏板,那就要趁早。
之所以选择月薪高的offer,是因为二方外包,以及大厂关注的是月薪,默认给16个月薪资。这时候,你的月薪很高,很容易就在第二次跳槽时直接月薪&年薪月份数双成长。操作得当,年薪可以快速上升数倍。不过薪资高,有时候也是一种负担,有机会会谈谈这个问题。话说后面,要不写一个用来快速涨薪的文章?囧
薪资的话题,比较敏感,我就先说这么多。剩下的,自行领悟哈。只能告诉你们,现在知道&执行这个操作的人,不多。/doge

c.自我提升

如果是自我提升,建议和正式员工在一起办公,并且积极主动
如果只是为了见识见识复杂系统,那完全可以考虑项目外包,不需要考虑人力资源外包。所以所谓的自我提升,更多是为了学习大佬们在面对复杂系统、技术难题、项目管理等问题时的处理手段、方法论、思想等。而当你的工作地点并没有和正式员工在一起,这一切都是泡影。也许可以摸鱼摸得很开心,但是对自我提升,毫无作用。

分开这种事儿并不是我们能决定的。那么有两种选择,一个是转换主次目的,随时准备跳槽。另一个是让主管想办法,把你捞过去。之前我们这边主管为了让同团队的三方外包可以和大团队一起工作,直接每周申请外部访客。最后是打了一个到P10的申请,将这位三方外包员工,留在了园区工作。

在一起工作,只是提供了学习&成长的可能,更多的是需要你积极主动地参与其中。前面说过外包的温水煮青蛙,重要的一点是外包很容易摸鱼,表现出来的,就是不积极主动。很多人成为外包员工后,就想少做一些工作,感觉做多了就亏了。。。其实主动争取工作,一方面有机会获取到更多更有价值的工作内容。另一方面可以展现自身主观能动性,进而让主管给你更多机会。另外,面对外包员工,多数主管并不会太关心其成长。所以这就需要外包员工自己去努力获取更多资源、更多信息、更多成长。其实无论是否为外包员工,都需要积极主动,才可以获得更多成长。只是外包员工更需要这个,因为几乎没有人去推你
具体如何在工作中更好地成长,我后面会写一些相关的文章,就不在这里展开了。

三、总结

这篇文章,先介绍了外包的概念,包括二方外包与三方外包的区别。进而分析了外包工作的优缺点。最后进一步分析外包工作的选择,如何选择,如何面对等。
希望这一篇文章,能够帮助到那些接触到外包工作机会,甚至已经是外包员工的小伙伴。

来源:cnblogs.com/Tiancheng-Duan/p/16002433.html

收起阅读 »

2021年 IT 圈吃瓜指南

来源:InfoQ













来源:InfoQ

再谈如何写好技术文档

— 1 —搞清楚主谓宾— 2 —不滥用代词、过渡词和标点符号— 3 —多用强势动词,少用形容词和副词— 5 —正确使用段落— 6 —适当使用列表和表格—&nbs...
继续阅读 »

参加工作时间久一点的工程师应该有这样一个体会:自己平时代码写得再多再好,可一旦要用文档去描述或者表达某一个事情或者问题时,都感觉非常困难,无从下手,不知道自己该写什么不该写什么;或者费了九牛二虎之力写出来的东西没法满足要求,需要再三去修改调整。这其中的主要原因我归纳有两点:

  1. 思维方式固化。大部分人平时代码写得太多,文字类型的表述又写得太少。而代码和文字明显是两种不同的思维方式,在代码里陷得太深,不容易跳出来;

  2. 本身文字表达能力有限。这个跟写代码一样,有人代码质量高、bug少;有人水平低、bug自然就多。

以上两点其实都可以通过平时多练、多写、多梳理的方式去弥补,比如周期性的博客总结和记录。但是,如果你能刻意系统性地去补充一些关于“技术型写作”的理论知识,一定能够事半功倍。这就像我们刚学编程时,一顿学、一顿模仿,但是总感觉缺了点什么,自己再努力发现深度还是不够。

这时候,我们需要做的是看一本高质量的经典书籍,书籍能帮我们梳理知识点、总结各种碰到过的问题,从理论上解答我们心中各种疑惑,将之前的野路子“正规化”。

下面我根据平时的一些积累,将技术型写作的理论知识归纳成10个要点。

  1. 搞清楚主谓宾

  2. 不滥用代词、过渡词和标点符号

  3. 多用强势动词,少用形容词和副词

  4. 正确使用术语

  5. 正确使用段落

  6. 适当使用列表和表格

  7. 一图胜千言

  8. 统一样式和风格

  9. 把握好整体文档结构

  10. 明确文档的目标群体


 
1 
搞清楚主谓宾


文档主要由段落组成,段落由句子组成,而大部分句子又由“主谓宾”组成(可能有些场合省略了,但是读者可以通过上下文轻松get到)。主谓宾是主干骨架,其他内容可以看作是句子的修饰,主干骨架是决定句子是否垮掉的主要原因。

现在很多人可能已经忘记了句子的基本构成,毕竟以汉语为母语的人,大概率是不太会关心这些“细节”,就像说英语的国家可能不太关心am、is、are一样,你说哪个人家都理解。

但是,文档中的一句话读起来是否别扭,大多数时候是由句子构成决定的。在不考虑文档上下文的情况下,如果一个句子能包含正确的主语、谓语和宾语(可选),那么它读起来至少是很顺口的。下面举一个明显搞不清主谓宾的例子:

传统图像处理算法,通过计算烟火颜色特征,极易受烟火周围环境相近颜色干扰而造成误检。

尽管你能读懂作者想要表达的意思,但是这句话读起来还是太别扭。“传统图像处理算法”应该算是主语,后面的“通过……”这句不完整,“极易受……干扰”这句还可以,“……造成误检”算是谓语宾语,但是这里用错了动词,为什么是“算法造成误检”,难道不是“周围环境相近颜色干扰造成误检”吗?

这句话的主干内容是:算法极易受……影响而……。正确的表述应该类似下面这样:

因为传统图像处理算法通过计算烟火颜色特征去识别烟火,所以它极易受烟火周围环境相近颜色干扰而出现误检。

我们用过渡词(因为……所以……)将原来的句子拆成了前后两个部分,前面部分的主语是“传统图像处理算法”,谓宾是“识别烟火”;后半部分的主语是“它”,谓宾是“出现误检”。经过调整后,前后两个部分的主语是同一个:传统图像处理算法。下面再直观看一下修改之后的句子主干骨架:

<因为><传统图像处理算法>通过计算烟火颜色特征去<识别烟火>, <所以><它>极易受烟火周围环境相近颜色干扰而<出现误检>。

如果你觉得用“因为……所以……”不太好,那么可以再换一种表述:

传统图像处理算法通过计算烟火颜色特征去识别烟火,烟火周围环境相近颜色的干扰极易造成误检。

第一句还是跟之前一样,主语是“传统图像处理算法”,第二句主语变成了“干扰”,谓宾是“造成误检”。下面我们直观地看一下修改之后的句子主干骨架:

<传统图像处理算法>通过计算烟火颜色特征去<识别烟火>, 烟火周围环境相近颜色的<干扰>极易<造成误检>。

最后再举一个错误的例子:

由于误报率与漏报率很高,因此不管是否有真实事件发生都会去留意,也会有规定的日程定点巡查视频任务。

上面这个句子的作者完全没搞懂谁是主语,谁是谓语。感兴趣的童鞋可以试着修改一下,改成你认为正确的表述。


 
2 
不滥用代词、过渡词和标点符号


不滥用代词和过渡词

中文文档中的代词主要有:你、我、他、她、它、其、前者、后者、这样、那样、如此等等,过渡词主要有:因为/所以、不但/而且、首先/然后等等。下面这张表格列举了一些常见的代词和过渡词及其常用场合:

序号
类型
名称
常用场合举例
1
代词

C语言中引入了“指针”的概念,作用是为了能够提升内存访问速度。
2代词
后者
C语言发明于1970年代,C++语言发明于1980年代,后者主要引入了面向对象思想。
3代词

指针能够提升程序访问内存的速度,但特点仍存在一些缺陷。
4代词

C语言的一大特性是指针,这就像C++语言和的面向对象思想一样。
5
过渡词
因为/所以
因为神经网络可以自动提取数据特征,所以基于神经网络的深度学习技术中不再有传统意义上的“特征工程”这一概念。
6
过渡词
首先/然后
首先我们要保证有足够多的训练数据,然后我们再选择一个适合该问题的神经网络模型。

表2-1 代词和过渡词举例

代词和过渡词就像标点符号一样,容易被滥用。代词滥用主要体现在作者在使用它们的时候并没有搞清楚它们代表的究竟是谁,是前一句的主语、还是前一句的宾语或者干脆是前一整句话?

过渡词滥用主要体现在作者在使用它们的时候并没有搞清楚前后两句话的逻辑关系,是递进还是转折或者是因果?(过渡词滥用频率要低很多,毕竟搞清楚前后句子逻辑的难度要小)接下来举几个滥用代词和过渡词的例子:

C++语言发明于1980年代,它支持“指针”和“面向对象(Object-Oriented)”两个特性,其价值在计算机编程语言历史上数一数二。

上面这个句子中出现了两个代词“它”和“其”,抛开句子内容本身对错不论,第二个代词指向的对象其实并不明确,“其”指的是“指针”、“面向对象”还是“C++语言”?或者是指“C++语言同时支持……两个特性”这个陈述?像这种有歧义的场合,我们应该少用代词,尽量用具体的主语去代替:

C++语言发明于1980年代,它支持“指针”和“面向对象(Object-Oriented)”两个特性,C++的价值在计算机编程语言历史上数一数二。

如果你一定要用代词,那么调整一下可能更好:

C++语言发明于1980年代,它同时支持“指针”和“面向对象(Object-Oriented)”两个特性,这个价值在计算机编程语言历史上数一数二。

再读一读,你是不是没有感觉到歧义了?我们在“支持”前面增加了一个“同时”,然后将代词换成了“这个”,现在这个代词指的是“C++语言同时支持...两个特性”这个陈述,修改后整个句子的意思更明确。

我们再来看另外一个滥用代词的例子:

该模块主要负责对视频进行解码,输出单张YUV格式的图片,并对输出的图片进行压缩和裁剪,前者基于Resize方法来完成,后者基于Crop()方法完成。

对于大部分人来讲,上面这段没什么问题。代词“前者”指的是压缩、“后者”指的是裁剪,原因很简单,因为单词Resize对应的是压缩、单词Crop对应的是裁剪。

但是这段话如果拿给没有任何知识背景的人去读(大概率可能是找不到这种人),恐怕会存在歧义,主要原因是代词前面提到了很多东西,“前者”和“后者”指向不明确,到底是指“解码”、“输出单张图片”还是后面的“压缩”和“裁剪”?下面这样调整后,整段话的意思更加明确:

该模块主要负责对视频进行解码,输出单张YUV格式的图片,并对输出的图片进行压缩和裁剪,压缩基于Resize方法来完成,裁剪基于Crop()方法完成。

我们去掉了代词,直接用具体的主语来代替,句子意思非常明确。如果你一定要使用代词,那么也可以这样调整:

该模块主要负责对视频进行解码,输出单张YUV格式的图片。同时,它还对输出的图片进行压缩和裁剪,前者基于Resize()方法完成,后者基于Crop()方法完成。

上面这段话还是使用了代词“前者”/“后者”,但是我们修改了标点符号,并且增加了一个过渡词“同时……”,这样做的目的是让读者知道虽然整段话说的是同一个东西,但是前后的句子已经分开了,为我们后面使用代词做好准备。

好的,现在我们来总结一下在技术型文档编写过程中使用代词时的一些有价值经验:

  1. 代词可以指它前面出现过的名词、短语甚至整个句子,但是一定是前面出现过的;

  2. 代词的位置和它要指向的目标最好不要隔得太远,1~3句话之内,超过就不要用了;

  3. 代词的作用是减少小范围内某些词汇或句子重复出现的频率,要用到恰到好处;

  4. 代词前面出现的混淆目标如果太多,一定要重新调整句子,确保代词指向无歧义。

不滥用标点符号

接下来我们再看另一个,标点符号的滥用要普遍很多,其主要原因是:标点符号的使用并没有非常明确的对错之分。至少对大部分人而言,使用句号还是逗号其实并没有什么严格的评判标准,只要不出现“一逗到底”的极端情况,其余大概率都OK。下面这张表格是我根据以往经验,总结出来的应用于技术型写作时中文标点符号使用规则:

序号
符号
写法
使用场合
1逗号

前后两句话关联性比较大,阅读时停顿时间短。
2句号

前后两句话关联性比较小,阅读时停顿时间稍长。
3
分号

前后两句话地位相对平等,句子的内容和格式基本保持一致。比如列表中,如果每项是一个句子或者短语,那么第1至第N-1项结尾使用分号,第N项结尾使用句号。
4
冒号

技术型文档中,冒号一般用在需要引入重要内容的场合。比如当你需要插入一张表格或者一张图片时,需要提前做一个提醒(下表列举了常见的代词和过渡词:),提醒结束时补充一个冒号。
5
括号
()、【】
()一般用于解释性的场合,负责对名词或者句子的补充解释。【】用得比较少,我一般用于需要增加醒目标记的名词或短语中。
6
顿号

一般可以用在枚举名词或者短语的场合。
7
问号
不用多解释。
8
引号
“”、‘’
一般用于标记特殊名词、专用名词、短语,或需要重点突出的名词或短语。
9
分隔号
/
一般用于成对出现的名词(举例:因为/所以、首先/然后等等都是过渡词),或者根据文档上下文来判断地位差不多的相近词(举例:算法的好坏直接影响最终报表中误报/误报率那一栏)。
10
破折号
——
用得不多。
11
省略号
……
不用多解释。
12
感叹号

技术型文档不是写小说,用得不多。
13
书名号
《》、<>
不用多解释。

表2-2 常用标点符号

上面这张表格基本涵盖了常用的中文标点符号,其中有一小部分在技术型文档中不太常见,比如感叹号、破折号,这些符号多多少少带有某种感情色彩,不太适合用于技术型文档编写。前面已经简单概括了一下各个符号的使用场合,下面挑几个容易出错的再一一详细说明:

C++语言发明于1980年代,它衍生自C语言,主要引入了“面向对象(Object-Oriented)”思想,面向对象思想强调对数据的封装和对功能的复用,此特性有利于开发者对代码的维护和扩展,目前,大部分计算机编程语言已经支持了面向对象特性。

上面这段话属于典型的“一逗到底”的例子。作者从C++语言说到了面向对象思想,最后总结大部分计算机编程语言都支持面向对象。我们如果将整段话拆开来看,其实它想表述的是3个内容,每个内容之间最好使用句号,停顿时间稍长一些。我们调整之后的效果是:

C++语言发明于1980年代,它衍生自C语言,主要引入了“面向对象(Object-Oriented)”思想。面向对象思想强调对数据的封装和对功能的复用,此特性有利于开发者对代码的维护和扩展。目前,大部分计算机编程语言已经支持了面向对象特性。

接下来我们再看看分号的使用。根据我个人经验,分号常用在列表场合,下面举一个例子说明:

下面是“将大象装入冰箱”的具体步骤:

  1. 打开冰箱门;

  2. 将大象装进冰箱;

  3. 关上冰箱门。

上面是一个有序列表,列表中的各项内容是一个短语。当列表中各项内容是短语或者句子的时候,除最后一项之外其余项目结尾一般都使用分号(注意,同一个列表中各项的格式最好都保持一致,要么都是短语,要么都是单个的名词,这个后面专门讲列表的时候会提到)。如果列表中各项内容只是一个名词时,那么结尾就可以不用标点符号:

下面是“可以被装进冰箱”的动物:

  • 狗子

  • 大象

  • 猴子

  • 鹦鹉

上面是一个无序列表,列表中的各项内容是一个名词,这时候名词结尾处不需要添加任何标点符号。

我们最后再来看一下小括号的使用场合。在技术型文档中,小括号主要用于对前面的名词、短语或者句子进行补充说明,比如当文档中出现缩写词汇时,我们会在它的后面增加一个小括号,在括号里面注明该缩写词汇的全称。下面举一个使用小括号对缩写词汇解释说明的例子:

API(Application Program Interface)是系统对外提供的访问接口,使用者可以按照API文档中的接口定义去访问系统中的数据,并与它做一些交互。

上面这段话主要讲API是什么、可以干什么。它是Application Program Interface三个单词的简称,为了让读者更清楚该术语的定义,作者可以选择在第一个“API”出现的位置增加一个小括号,并将术语全称补充进来,之后的整个文档无需再重复该操作(后面会单独提到术语全称和简称的运用规则)。

除了能对缩写词汇进行解释说明之外,小括号还可以用于对前面整个句子进行补充说明,再看下面这个例子:

它是Application Program Interface三个单词的简称,为了让读者更清楚该术语的定义,作者可以选择在第一个“API”出现的位置增加一个小括号,并将术语全称补充进来,之后的整个文档无需再重复该操作(后面会单独提到术语全称和简称的运用规则)。

上面这段话其实前面已经出现过,最后小括号里面的内容主要是为了对它前面一句话进行补充。如果补充性说明内容太长,比如要好几句话才能起到补充的作用,那么这个时候我们就不应该再使用小括号了,可以考虑调整句子结构,然后将补充性的内容当作段落主体的一部分。

关于代词、过渡词以及标点符号滥用的内容就讲到这里,其中有一些内容是我个人的写作喜好,其实并没有非常明确的对错之分,比如前面讲到列表中分号的使用,很多人这时候可能选择使用句号。

大家可以根据自己的判断去处理这种模棱两可的场景,当然一些比较确定的规则,比如当列表项只有名词的时候,列表项结尾不要使用任何标点符号,这一点还是比较确定的。


 
3 
多用强势动词,少用形容词和副词


强势动词和主动语句

很多人可能第一次听到“强势动词”这个说法,陌生还难以理解。如果将它翻译成英文,对应的单词应该是“Strong Verbs”,意思是强有力的动词,你可以理解为:听起来动作幅度大、冲击力强的那一类动词。打个比方,假如“走”是弱势动词,那么“跳”就是强势动词;假如拿刀“切”是弱势动词,那么拿刀“砍”就是强势动词。下面这张表格列举了一些强势/弱势动词的例子:

序号
弱势动词
(可考虑)强势动词
1
走过去
跳过去
2
切肉
砍肉
3
出现异常
抛出异常
4
程序退出
程序崩溃
5内存增长
内存泄漏
6找不到日志文件
日志文件丢失
7客户提出质疑
客户投诉
8
任务未完成
任务延期
9角色权限是由管理员设置的
管理员控制角色权限
10
系统无法正常使用API返回的结果
系统无法正常解析API返回的结果

表3-1 强势/弱势动词对比

上面列出了10对强势/弱势动词,我们观察可以发现:弱势动词一般无法正确表达问题/事情的真实情况。在技术型文档编写过程中,虽然我们不能借助词汇使用、句子构成以及标点符号等手段去传递感情倾向,但是也不能掩盖真实准确的内容表达。

在提到强势动词时,我们还要注意“主动语句”和“被动语句”的区别。在技术型文档编写过程中,应该尽量少使用被动语句。下面这张表格列举了一些主动/被动语句的例子:

序号
被动语句
(可考虑)主动语句
1
角色权限是由管理员控制的
管理员控制角色权限
2
API结果无法被系统正常解析
系统无法正常解析API结果
3
图像特征是通过CNN逐步降维的方式提取的
CNN通过逐步降维的方式提取图像特征
4
这种检测效果无法被客户接受
客户无法接受这种检测效果
5
经过研发排查发现,这个现象是正常的(*)
经过研发排查发现,这个属于正常现象

表3-2 主动/被动语句对比

上面表中第5项(带*号)严格来讲不算被动语句,但是在技术型写作过程中,我们应该避免使用“……是……的。”这种句式,该句式太过口语化。尽量少用被动语句的原因有以下三个:

  1. 读起来麻烦。读者读到被动语句时,需要先在脑子里将其转换一下再去理解;

  2. 难以理解。读者有时候很难分清被动语句中的真实主语(甚至可能省略了主语);

  3. 字数多。被动语句一般更长、字数更多。

那么被动语句是不是完全不让用了呢?当然不是。仔细的读者可能已经观察到了前面在举例的时候我们有这样一段话:

C++语言<发明于>1980年代,它支持“指针”和“面向对象(Object-Oriented)”两个特性,C++的价值在计算机编程语言历史上数一数二。

上面第一句中的“……于”其实就是被动语句,像“C++语言发明于……”、“该文档编辑于……”这些都算被动语句,由于宾语(这里是C++语言)更重要,所以默认省略了真实主语(某某发明C++语言,可是某某在这里不太重要)。这类句子结构有一个特点就是:宾语比真实主语重要,所以放到句子的开头位置。

少用形容词和副词

技术型文档讲究的是一个“准”字,它不像小说、散文之类的文学作品带有很强的感情色彩,也不同于网络博客可以掺杂一些非正式词汇,更不能跟Marketing Speech(营销话语)一样常常夸大其词。为了做好前面说的“准”,技术型文档应该尽量少用形容词和副词,因为这些词语大部分都属于“主观”表达。下面举几个使用形容词和副词的例子:

为了保证系统运行更高效,他们尝试尽可能压缩图片尺寸,事实证明这个尝试非常成功。这样的工作看似简单,却蕴含着高技术含量。

上面这段话使用了好几个副词和形容词,比如“尽可能”、“非常”、“高”。如果是技术型文档,这段话建议调整为:

为了提高系统运行效率,他们将图片尺寸压缩到原来的1/3,系统响应速度提升2倍。

我们用具体的数值替换了原来的形容词和副词,并且直接删掉了最后一句话,最后一句话在技术型文档中起不到任何作用。下面这张表格列举了部分形容词和副词使用不恰当的场合:

序号
形容词/副词
(可考虑)调整为
1
经过优化,接口响应速度提升明显
经过优化,接口响应速度提升2倍
2
很多人反应现场误报很多
数据统计发现,现场误报率为11%
3
大部分客户投诉说系统很不好用
最近一个月有超过50个客户投诉说系统不好用
4
升级依赖库后,该函数运行很快
将依赖库升级到2.3.1版本后,该函数执行时间缩短到100ms以内
5
研发同事很辛苦,每天加班很晚
研发同事很辛苦,每天23:00之后才下班

表3-3 形容词/副词使用不恰当举例

最后,我们来总结一下:

  1. 优先使用方便读者阅读理解的动词和句式(强势动词和主动语句);

  2. 尽量少用形容词和副词,用具体数值代替、或者调整句子表述。


 
4 
正确使用术语


这里提到的术语分两种:一种是计算机领域通用的专业术语,像SDK、面向对象、TCP/IP、微服务等等这些名词,它们基本已经被大众接受和理解,我们在编写文档的时候不能随意再重新去命名、调整或者改变拼写(将“TCP/IP”写成“Tcp/ip”)。

另外一种是当前文档需要定义的术语,这种术语只有在当前文档上下文中才有效。我们在编写技术型文档时,通过自己的判断,如果认为文档读者缺乏对相关术语(不管是前面哪一种)的理解,我们都应该在文档靠前位置给出对术语的解释说明,也就是我们平时文档中常见的“名词解释”。

序号名词说明
1SDKSoftware Development Kit,软件开发包,开发者基于该工具包开发更丰富的高层应用。
2内存泄漏通过new/malloc等方法申请的内存在使用完后未被及时释放,程序运行内存占用越来越高。
3面向对象强调对数据和功能的封装,提升代码的可复用性、可扩展性以及灵活性。
4FVM(*)Front Video Manager,前端视频管理服务,负责视频接入、分发等业务。
5视频大数据标签服务(*)对视频进行结构化处理,生成结构化标签,并对外提供标签检索等功能。

表4-1 名词解释举例(*为自定义术语)

有些文档可能篇幅比较短,并不是传统意义上的需求设计类文档,比如对某个线上问题分析的结果汇报、对某个模型检测效果的验证报告、或者研发阶段性的工作总结。这些文档由于本身内容就不多,大部分可能直接进入主题,这时候如果还要在文档中专门增加一块名词解释的版块(并且总共也就一两个术语),就显得比较突兀。

另外一种对术语进行解释说明的方式是用我们前面提到的小括号,我们可以在术语后面增加一个小括号,然后在括号里添加补充说明。这种方式很便捷,但是只适合简单的场景,比如在小括号里面补充术语的全称或者简称,或者只做简单的解释说明。如果对一个术语的解释内容很长,就不太适合用这个方法,下面举一个错误的例子:

当视频离线时,FVM(Front Video Manager,前端视频管理服务,负责视频接入、分发等业务。)会产生一条告警记录,并存入节点数据库。

上面这个术语解释内容太长,不太适合使用小括号的方式,这种情况要么在文档正文中专门对FVM进行解释,要么在小括号中只给出FVM的英文全称即可:

当视频离线时,FVM(Front Video Manager)会产生一条告警记录,并存入节点数据库。

使用小括号去做术语解释还需要注意一点的是:只需要在术语第一次出现的时候做一次解释即可,不需要重复多次。下面举一个重复的错误例子:

当视频离线时,FVM(Front Video Manager)会产生一条告警记录,并存入节点数据库。之后节点数据库会将该条告警记录同步到平台数据库,平台FVM(Front Video Manager)检测到有新的告警记录时,会通过消息中间件通知业务系统,业务系统随后将告警信息以短信(或钉钉)的方式通知到用户。

上面对术语FVM的解释重复了两次,这种做法是错误的,第二次我们可以直接去掉。

有些术语存在全称和简称,我们熟悉的SDK全称是“Software Development Kit”,但是现在基本没有人再去使用它的全称。像这种简称已经被大众熟知的术语,我们就不能再标新立异的去用它的全称。

另外一些在文档中自定义的术语,文档作者为了便于阅读可能也会提供一个简写的版本,在这种情况下,文档前后应该保持一致,即:要么整篇文档都用全称,要么都用简称,尽量做到一致。下面举一个全称简称使用不一致的例子:

IVA(Intelligent Video Analytics,智能视频分析)服务主要负责视频解码、模型推理、目标跟踪以及目标行为分析,该服务是整个系统中最复杂的一个模块。智能视频分析服务由张三团队开发完成,一共耗时6个月,人力成本开销120万。

上面这段话中,前半部分作者使用“IVA”简称(小括号中做了全称说明),但是在后面一句话中作者又使用了全称“智能视频分析”,这种做法没有遵循统一原则。不仅同一段落应该保持统一,整篇文档也应该做到统一,术语在文档中第一次出现时是简称,那么整篇文档都应该用简称,反之亦然。

最后我们来总结一下,在技术型文档中使用术语时需要注意的一些事项:

  1. 文档读者不熟悉的术语(包括通用术语和文档自定义术语)都应该有解释说明;

  2. 小括号只适合简短的术语解释场合,括号里的内容不能太长(一两句短语之内);

  3. 任何方式的术语解释只需要有一次即可(术语第一次出现时),不要解释多次;

  4. 术语的全称和简称要保持使用一致,要么整篇文档都用全称、要么都用简称;

  5. 对于计算机领域的通用专业术语,需要沿用主流用法,不要随意再去调整。


 
5 
正确使用段落


单一职责

与面向对象编程中“类的单一职责原则”一样,文档中的句子(特指以句号结尾的一句话)、段落也应该遵循“单一职责原则”。前面讲标点符号的时候已经提到过,同一段话中前后关联性不大的两句话之间用句号,这样可以保证每句话想要表达的是相对独立的内容。

段落也一样,一个段落只陈述一个主题,可以保证段落的句子不会太多、内容不会太长,便于读者阅读和理解。下面举一个段落使用错误的例子:

Excel提供一个组织数据的高效方法。我们可以将Excel想象成一个有行和列的二维表格,每一行代表一个独立的实体,每一列代表该实体的不同属性。Excel还具备数学功能,比如计算平均值和方差等数学操作。如果你想使用Excel来记录图书信息,那么每一行代表不同的书本,每一列代表书本的属性,比如书的名称、价格以及出版社等等信息。

上面这段话的第一句已经明确了段落主题:Excel能高效地组织数据。可是,这段话中间却穿插了一个不相干的句子,说Excel具备数学功能,能够做一些数学操作,这句话显然跟本段主题不一致,我们需要将其去掉:

Excel提供一个组织数据的高效方法。我们可以将Excel想象成一个有行和列的二维表格,每一行代表一个独立的实体,每一列代表该实体的不同属性。如果你想使用Excel来记录图书信息,那么每一行代表不同的书本,每一列代表书本的属性,比如书的名称、价格以及出版社等等信息。

好的开头语

除了要保证段落的“单一职责”之外,我们还需要给每个段落一句“好的”开头语。那么什么是好的开头语呢?好的开头语要能让读者读完之后就能猜到文档作者在本段中想要陈述的主题,其实就是概括性的句子。

还是以上面那段话为例子,它的第一句话“Excel提供一个组织数据的高效方法”其实就是很好的开头语,它提示本段内容主要讲Excel如何高效地组织数据。如果我们将上面那段话的开头调整一下,那么效果明显就差了很多:

Excel由许许多多的单元格组成,每个单元格可以包含不同的内容。我们可以将Excel想象成一个有行和列的二维表格,每一行代表一个独立的实体,每一列代表该实体的不同属性。如果你想使用Excel来记录图书信息,那么每一行代表不同的书本,每一列代表书本的属性,比如书的名称、价格以及出版社等等信息。

读者读完上面第一句话后,可能还是很懵,需要读完整段话才能明白文档作者在本段中想要表达的意思。段落的开头语可以通过提炼段落内容得到,我们可以在段落写完之后回过头提炼一句话作为本段的开头语,下面这段话描述代码中循环语句的作用:

目前几乎所有的计算机编程语言都支持循环语句,例如,我们可以编写代码来判断一个用户命令行输入是否等于“quit”(退出命令),如果需要判断100万次,那就创建一个循环,让判断逻辑代码运行100万次。

上面的这段话本身没什么问题,主要介绍循环语句的功能和应用场合。但是如果我们提炼一下,在段落开头增加一个更好的开头语,效果可能会提升很多:

循环语句会多次运行同一个代码块,直到不再满足循环条件为止。目前几乎所有的计算机编程语言都支持循环语句,例如,我们可以编写代码来判断一个用户命令行输入是否等于“quit”(退出命令),如果需要判断100万次,那就创建一个循环,让判断逻辑代码运行100万次。

上面开头第一句话就说清楚了循环结构的特点,读者读完第一句话基本就知道整段内容要讲什么。一个好的开头语能够节省读者的时间,因为并不是每个读者都有兴趣去阅读整段的内容,开头语可以给读者“是否继续读下去”一个参考。

控制段落长度

控制段落长度并没有一个明确的标准,它只是一个非常主观的说法。如果文档中某个段落内容太长(比如那种一段话就占半页Word),作者自己应该反复阅读几次再对段落做一些精简,这样既可以节省读者的时间,大概率也能提升意思表达的准确性。

同样,也不太建议文档频繁出现小段落,比如整段内容只有一两句话那种,这个时候可以考虑段落合并或者稍微扩充一下内容。

最后我们来总结一下,在技术型文档中如何正确使用段落:

  1. 一个段落只负责讲一个内容,两个不同的主题应该拆分成两个段落去陈述;

  2. 尽量为每个段落增加一个“好的”开头语,能够清晰表达(或暗示)本段的主题;

  3. 要控制好段落内容长短,“不长不短”根据自己经验(比如不超5~7个句子)。


 
6 
适当使用列表和表格


文字相对来讲其实是一种效率比较低的表达方式。如果你想让人快速地去理解你要表达的意思,图片应该是最好的一种方式,但是图片有一个缺点就是:有时候它只能从宏观上去表达,无法体现其中细节。

当我们想要尽可能直观地去陈述内容,又想尽可能多的包含细节时,我们可以考虑使用列表或者表格。有些读者非常抵触大段大段的文字(尤其在技术型文档中),一种改进方法是前面提到的“控制段落长度”,尽量让段落内容精简、单一;再一个就是看看段落内容是否能以列表或者表格的方式去呈现,这种方式可以给人“严谨、清晰”的感觉。

使用列表

列表简单来讲就是将你原来用段落方式呈现的内容改用项目(Item)的方式去呈现,一般它主要用于枚举、过程描述或者要点归纳等场合。列表中的各项可以是名词、短语,甚至是句子,各项目之间有严格顺序要求的列表叫“有序列表”,相反并没有严格顺序要求的列表叫“无序列表”。下面是以段落的方式陈述小张今天所做的事情:

白天在公司上班期间,小张一共修复了7个bug,做了3个代码合并(评审),并和项目经理讨论了前天提的新需求。晚上回到家后,小张先做饭,然后给儿子洗澡,23:30上床睡觉。

上面这段话本身没什么问题,用了合理的标点符号和过渡词,读起来清晰明了。但是,如果在技术型文档编写中,能将这段话改用列表的方式呈现,起到的效果会更好:

张白天在公司:

  • 修复了7个bug;

  • 做了3个代码合并(评审);

  • 和项目经理讨论前天提的新需求。

晚上回到家后:

  1. 做晚饭;

  2. 给儿子洗澡;

  3. 23:30上床睡觉。

我们将原来的一段话拆成了两个列表,并在每个列表前面做了一个“引入说明”(以冒号结束),介绍了接下来列表的背景上下文。第一个列表是无序列表,因为原文并没有突出强调小张白天在公司每项工作之间的前后关系(无顺序要求),只是一个归纳统计;第二个列表是一个有序列表,原文很明显强调了小张晚上回家之后做事的先后顺序(最后一项还给出了具体时间)。

在技术型文档中,合理地运用列表这种方式去呈现内容可以给人一种“逻辑严谨、思路清晰”的感觉,让读者更相信你讲的内容。

在使用列表时,我们应该确保列表中各项内容结构一致,即:要么都是名词,要么都是短语,要么都是句子。这个原则既能保证你使用列表的初衷(逻辑严谨、思路清晰),也能让读者读起来更舒服。下面是一个错误使用列表的示范:

影响系统检测准确性的因素有:

  • 模型;

  • 产品开通过程中,工程师对算法参数校准程度;

  • 应用现场是否有灯光照明。

上面列表一共包含3项,每项的内容结构各不相同,第一项是一个名词,第二项是一个句子,第三项是一个短语。我们将结构统一后,可以调整为下面这样:

影响系统检测准确性的因素有:

  • 模型的复杂性;

  • 部署时对算法参数校准的程度;

  • 应用现场是否有灯光照明。

上面是将列表中各项内容修改为短语,我们还可以换另外一种方式:

影响系统检测准确性的因素有:

  • 模型类型

  • 校准程度

  • 环境亮度

上面是将列表中各项内容修改为名词,由于是名词,每项结尾处不使用任何标点符号(参见前面专门讲标点符号的章节)。下面是对列表运用的总结:

  1. 列表一般用于枚举、过程描述、要点归纳等场合;

  2. 需要强调顺序的时候应该使用有序列表,其余视情况而定;

  3. 列表中各项内容结构应保持一致,都是名词、短语或者句子;

  4. 每个列表前面尽量添加一个明确的“引入说明”,以冒号结束。

使用表格

表格其实跟面向对象有一定联系,大部分时候表格中的一行相当于一个对象,表格中的列相当于对象的属性(字段),表格和面向对象组织数据的方式本质上是一致的。技术型文档中表格一般用来组织与数字有关的内容,当然也有例外,就像前面章节中用到的表格,纯粹是为了组织文本内容。

下面是在技术型文档中,使用表格时可以参考的一些经验:

  1. 组织数字相关内容时,一定要用表格(大部分人可能已经有这个意识);

  2. 组织结构化类型的文本内容时,尽量用表格;

  3. 每个表格都应该配一个表格标题,简要说明表格内容;

  4. 文档中的表格应具备一致的样式和风格,比如标题字体、背景填充等。

在技术型文档中使用表格组织文本内容时,需要控制每个单元格的文本长度。一般情况下建议单元格中只使用短语,如果必须要用段落,也应该控制段落中句子数量(一般建议不超过2~3句)。下面是错误使用表格来组织文本内容的示范:

序号
语言
介绍
1
C
C语言由贝尔实验室发明于1969至1973年,是一种编译型计算机编程语言。它运行速度快、效率高、使用灵活,常被用于计算机底层驱动以及各种语言编译器的开发。C语言是一种面向过程的编程语言,同时它的语法相对来讲较复杂,新人入门门槛比较高。
2
C++
C++语言发明于1979年,是一种编译型计算机编程语言。它衍生自C语言,继承了C语言的一些特性,比如使用指针直接访问内存,同时它也支持面向对象编程,提升了代码的可复用性、可扩展性以及灵活性。由于C++继承了C的大部分语法,再加上本身具备复杂的类型系统以及泛型编程等语言特性,新人入门门槛也比较高。
3
Python
Python语言发明于1991年,是一种解释型计算机编程语言,因此运行速度相对要慢。Python除了支持面向对象编程之外,还支持函数式编程,它语法简单,更贴近人类自然语言,新人入门门槛较低。Python是目前人工智能领域最热门的语言,对应的工具库非常丰富。

表6-1 三种编程语言介绍

上面是以表格的形式来介绍C、C++以及Python三种编程语言,但是在“介绍”那一列中的文本内容太长,我们可以换一种表达方式:

C
C++
Python
由AT&T 贝尔实验室发明于1969至1973年
由BJarne Struistrup发明于1979年
由Guido van Rossum发明于1991年
语法比较复杂,新人入门门槛高
语法比较复杂,新人入门门槛较高
语法简单,贴近人类自然语言,新人入门门槛低
编译型语言
编译型语言
解释型语言
支持面向过程编程
支持面向过程、面向对象编程
支持面向过程、面向对象、函数式编程
偏底层、运行速度快、使用灵活
继承了C语言的一些特性,在其基础之上还支持面向对象等特性
语法简单,学习难度低
一般用于驱动、编译器、嵌入式或者其他偏向硬件层面的开发
一般用于游戏前后端、PC客户端的开发
一般用于数据科学、人工智能相关开发

表6-2 C vs C++ vs Python

上面表格一共还是3列,但是现在每列代表一种编程语言,列中的每个单元格是对该语言的描述,描述内容都比较精简。如果你想继续补充内容,可以对应地增加行即可。

表格的组织方式有多种多样,行可以变成列、列可以变成行,并没有严格的限制。我们只需要找一个适合自己的方式,比如上面这种每列代表一种语言,是因为该场景需要介绍的编程语言只有三种,如果数量再多点(或者数量不确定,后期会继续增加),那么表格宽度就不太够、这种组织方式就不再合适。


 
7 
一图胜千言


人类在发明文字媒介之前,用的是图形符号。图像(或图形、图片)是所有内容表达方式中最直观的一种,同时也能提升读者的阅读兴趣。有人专门做过研究:在文档中增加图像能提升读者对文档的喜爱程度,不管这个图像跟文档内容本身是否有关系(https://reurl.cc/RjkrK6)。

也就是说,哪怕在文档中插入无关紧要的图像,读者也更愿意去尝试阅读文档中其他的内容。我们平时看别人演示PPT时,如果发现整页都是文字描述,大概率就不会有认真去听的欲望。下面是一段对双向链表的文字描述:

双向链表也叫双链表,是链表的一种。它的每个数据节点中都有两个指针,分别指向直接后继节点和直接前驱节点。所以,从双向链表中的任意一个节点开始,我们都可以很方便地访问它的前驱节点和后继节点。在应用双向链表时,我们一般构造双向循环链表,链表首尾相连。

上面这段描述双向链表的文字本身已经非常清晰,对数据结构有一定基础的人看完文字基本就能理解双向链表的结构和应用场合(基于它的特点)。但是,如果是一个零基础的小白来看这段话,可能效果就不会太好(尤其如果这段话是作为PPT中的内容,大概不会再有更多的内容补充)。如果我们在这段话后面增加一个插图,来直观告诉读者双向链表长什么样:

双向链表也叫双链表,是链表的一种。它的每个数据节点中都有两个指针,分别指向直接后继节点和直接前驱节点。所以,从双向链表中的任意一个节点开始,我们都可以很方便地访问它的前驱节点和后继节点。在应用双向链表时,我们一般构造双向循环链表,链表首尾相连。下图是双向链表结构示意图:


图1 双向链表结构

上面的文本配合图片,能让读者更加直观的理解双向链表的结构特点。当文档中的文本和图片同时出现时,读者大概率会先看图片,然后再结合文字去理解,加快文档阅读速度。

可抽象也可具体

技术型文档中的插图不一定都得是流程图、架构图、或者结构设计图这种非常具体的技术相关图片,还可以是抽象的、能形象表达文档主题的图片。下面是在技术型文档中使用卡通和漫画图片的示例:

示例1:

Gitlab中有Label和Tag两个概念。


为了便于区分,这里将Label翻译成“标签”,将Tag翻译成“标记”(在有些地方这两个单词翻译并没有严格的差异)。Gitlab中标签的作用是为了分类、快速地检索和过滤,用户能通过标签来直观的管理Issues,比如to-do、bug等等。

标记的主要作用是为了归档,给Commit取一个形象的别名,后期快速定位和查找。GitLab中创建标记可以理解为“做记号”,建立索引。一般推荐为标记定义一个有意义的名称,比如以版本号为名,当我们要发布1.0版本,对应的标记名称可以是“v1.0”,如果我们要发布2.0预览版,那么对应的标记名称可以是“2.0-pre”。

示例2:

源码版本控制系统(Source Code Version Control System)主要负责对源代码版本进行管理,涉及到代码提交、撤销、比对、分支管理、代码合并等功能。源码管理是软件开发过程中非常重要的一个环节,它能有效保证软件代码质量。


图1 团队协作

源码管理并不是软件开发周期的全部,整个软件开发周期涉及到多个流程、多个团队(多人)协作完成,包括立项/结项、进度/任务管理、需求/设计、bug管理、测试、集成上线等环节。

突出图中重点

当我们想为文档添加图片时,单张图片包含的内容不宜太过复杂,图片应该能准确地表达意思。如果一张图太过复杂、或者包含了一些可能引起歧义的部分,我们可以尝试以下两种改进方式:

  1. 将复杂的图拆开,一张图对应一个局部细节;

  2. 在图片中将重点区域标记出来,让读者可以一眼就发现重点。

在技术型文档中插入复杂的系统架构图很常见,这种时候建议遵循“先宏观,再具体”的原则,循序渐进。我们不要一上来就放一张大图,还想将所有的细节都包含进去,这种想法不太现实,这不仅对你画图的技能要求很高,读者看完也容易一脸懵。下面这张图太过复杂:

整个视频分析系统由3大服务组成,分别是Intelligent Video Analytics、Front Video Service以及Distribute Load Balance,这3大服务一共包含15个子模块。下面是视频分析系统结构:


图1 视频分析系统结构

上面这个例子中插入的这张图既想描述3大服务之间的交互关系、又想描述各个服务内部子模块之间的交互关系(上面只是示意图,实际情况可能比这个更复杂)。文档读者碰到这种情况可能会产生两个感觉:一是图太复杂了,很难看懂,有些地方迫于空间原因字号还小;二是我需要重点关注的点在哪里?如果遵循前面提到的“先宏观,再具体”的原则,上面这个例子可以调整为:

整个视频分析系统由3大服务组成,分别是Intelligent Video Analytics、Front Video Service以及Distribute Load Balance。下面是视频分析系统中各服务之间的关系:


图1 视频分析系统服务交互

其中,Intelligent Video Analytics服务主要负责对视频解码、推理以及行为分析等结构化操作。该服务内部一共包含9个子模块,模块之间的关系见下图:


图2 Intelligent Video Analytics服务子模块交互

Front Video Service服务主要负责视频接入、分发、配置管理等功能。该服务内部一共包含3个子模块……

另外一种情况,插入的图片中包含了不相干内容,文档作者又没有给出醒目的标记,读者看完不清楚关注重点在哪里。下面是错误的示例:

GitLab中的Release功能主要用来对仓库中的代码以及其他一些相关资料文件进行归档,通常用于版本发布。当有新版本发布时,用户可以基于对应的Commit创建一个Tag标记,给它一个合理的名字,比如“v1.0-pre”(代表发布1.0预览版),然后再基于该Tag发布版本。

后期,其他人可以通过Release菜单快速浏览、检索项目版本发布记录以及对应时间点的相关代码和资料。用户可以在GitLab主界面的左侧菜单中找到Release功能入口:


图1 Gitlab中Release菜单

上面图片在介绍Release功能时给出的图片中包含的菜单项太多,为了让读者更直观看懂图片关注点,可以将图片调整如下(左右两种都可以):

GitLab中的Release功能主要用来对仓库中的代码以及其他一些相关资料文件进行归档,通常用于版本发布。当有新版本发布时,用户可以基于对应的Commit创建一个Tag标记,给它一个合理的名字,比如“v1.0-pre”(代表发布1.0预览版),然后再基于该Tag发布版本。

后期,其他人可以通过Release菜单快速浏览、检索项目版本发布记录以及对应时间点的相关代码和资料。用户可以在Gitlab主界面的左侧菜单中找到Release功能入口:


图1 Gitlab中Release菜单

有准确的图标题

图片是为了读者能够更直观地理解文档内容,但是图片毕竟不是文字,不同的人对同一张图片理解可能存在差异,尤其对于那种不包含任何文字的图片。因此,在文档中插入任何图片时,我们应该为它定义一个合适、贴切的标题。图标题一般是一个名词或者短语,作用跟前面讲到的表格标题一样,协助读者理解图片所要表达的含义。


 
8 
统一样式和风格


文档的样式和风格其实跟我们写代码一样,写代码要遵守统一的代码风格(变量命名、换行规则等等),写文档也应该遵守统一的文档风格。公司或者组织一般都有自己的文档风格规范,规范会定义好正文/标题字体字号、页眉页脚、页边距、行间距、段前段后间距等等,按照规范写出来的文档风格基本就能保持一致。

对于没有规范可用的场合,文档作者可以根据自己的偏好执行即可,保证整篇文档的内容遵守相同的风格,比如文档开头和文档结尾的段落间距、列表样式、对齐方式都应该保持一致。本篇文档的主要规范定义如下:

  1. 页边距上下左右2cm;

  2. 标题18号华文仿宋,正文12号宋体,正文中表格/图标题12号华文仿宋;

  3. 段前/段后间距0.5,段落行间距1.5倍,段落首行对齐不空格;

  4. 表格、图片居中对齐,图标题在图片下方、表格标题在表格上方。

还有另外一些比较重要的样式定义,比如列表样式(本篇文档中每个列表外面套了一个表格,表格无左右边框),还比如本篇文档涉及到了很多举例和示范,所有的举例示范都在表格中,并且表格有自己的样式(字体字号、背景颜色等等)。


 
9 
把握好整体文档结构


把握好整体文档结构是一件非常困难的事情,这个其实跟前面讲到的文档内容本身没什么关系。文档作者在动笔之前需要有一个宏观的构思,需要在脑子里先将文档大纲梳理一遍,一级标题可以是什么、二级标题又可以是什么,然后考虑将合适的内容放到对应的章节中去。

优秀的作者在正式动手之前,可能已经有了很长一段时间的思考准备,尤其对于那种非常复杂的文档。但是这种方式对一些人来讲可能不太现实,难度太大。那么这时候就只能考虑另外一种方式,动手之前先在白纸上打草稿,列出来文档大纲,然后不断修改和调整,直到满意为止。

其实不管上面哪种方式,文档结构考验的是作者组织内容的思维能力。对于一些需求、设计类型的“主流”技术型文档,考验的是作者对软件需求、系统架构的理解深度,该写什么不该写什么,写到什么程度,这些都需要作者考虑清楚,这类型的文档一般有标准的模板可以参考,大家平时写得/见得也比较多。

对于另外一些“非主流”类型的技术型文档,比如对某个线上问题的分析报告、技术/原型调研类文档,这些文档一般规模比较小、也没什么参考标准,全靠作者自己去组织。

下面就以“对某个用户需求做技术性反馈”为例,抛砖引玉,简单描述一下技术型文档结构应该如何去组织:

场景说明:

视频分析系统中,客户要求在事件录像文件中对涉事车辆目标(或区域)进行高亮标框显示,视频录像在播放时会有一个醒目的多边形提醒用户具体事件发生位置。客户懂一些技术相关知识,要求公司技术研发团队针对该需求给出合理的需求反馈,如果需求可实现,评估工作难度;如果需求不可实现,说明具体原因。

根据上面场景说明,该需求并非硬性要求。甲方提出了一个想法,并且非常贴心地考虑到了乙方是否具备条件实现,希望给出一个实质性的答复。公司技术团队在写反馈说明文档之前,应该考虑以下两个问题:

  1. 如果正常响应该需求,具体的方案是什么、难点/风险点各是什么;

  2. 如果不能正常响应该需求,具体原因是什么,是否有可替代方案、替代方案是什么。

也就是说,不管最终团队是否响应该需求,我们在文档中都要有非常实质性的内容,不应该是空话、套话。下面就以“不响应”为例,描述文档应该包含哪些内容:

序号
节标题名称
主要内容
1
背景说明
用自己的话将客户的需求完整描述一遍,不要有任何偏差,表明我方已认真理解过原始需求。
2
已有录像逻辑
详细描述系统中目前已有的事件录像逻辑。因为我们本次是不响应该需求,所以对后面不响应有利的内容一定要着重强调(要突出已有录像逻辑的优势)。
3
录像标框逻辑
详细描述在事件录像文件中对涉事目标(或区域)进行高亮标框的逻辑。注意这里按照理想逻辑去描述,不用考虑任何外在限制。
4
录像标框难点
结合第3点,重点归纳、整理出在录像文件中标框的难点,比如需要对每一路进行解码再去叠加图形、视频画面不能压缩否则影响分辨率等等,这些对设备性能要求非常高,会增加硬件成本。
5
解决方案一 (不计代价去响应)
按照理想逻辑去响应,但是要提出前提条件或者代价,比如单台设备分析路数降低到原来的一半,硬件成本是原来的2本。(其实就是要排除这个方案)
6
解决方案二 (可替代方案)
提出一种可替代的方案,可以满足客户最开始提出的“有醒目标记提醒用户”。比如当视频录像播放时,可以在播放器上面叠加一个高亮方框,能够大概标记涉事车辆目标(或区域)。同时,强调该方案的优势(比如工作周期短、对成本无影响)。
7
结论
其实根据前面的描述,只要认真读完文档的人基本都能知道结论是什么、应该选哪个方案。但是这里还是要书面写上,根据前面的描述,解决方案二有更大的优势,建议采用方案二。

需要注意的是,“响应”或者“不响应”的决定很多时候不在技术团队或者写这个文档的人手里。虽然文档中的内容应该为最终的结论服务,但是总体上不应该有偏差。


 
10 
明确文档的目标群体


文档的目标群体是谁?这个其实应该是写文档最开始就需要明确的东西,面对不同的群体,我们文档的内容、结构包括内容描述程度都会不同。尽早确定读者有助于在构思阶段就明确文档内容边界,哪些该写、哪些不该写,该写的又应该如何去写,这些都是编写文档的大方向。

作者:周智,前微软(中国)Windows工程院员工,目前从事于深度学习计算机视觉相关工作,交通安防领域的视频目标检测、跟踪和行为分析。

收起阅读 »

不幸言中,“核酸码”打不开.....那就聊聊为什么我觉得要挂的原因吧!

周四晚上的时候,看到消息说4月9日起要采用新的核酸检查系统,要推出一个新的码,叫:核酸码。当晚就有很多网友发现随申办上已经有入口了,但点进去是报错的:但是因为还没投入真正使用,所以也没啥大的反馈,大家就瞎讨论了技术栈和这个错误可能的原因啥的。我也顺带瞎扯了一句...
继续阅读 »

周四晚上的时候,看到消息说4月9日起要采用新的核酸检查系统,要推出一个新的码,叫:核酸码

当晚就有很多网友发现随申办上已经有入口了,但点进去是报错的:

但是因为还没投入真正使用,所以也没啥大的反馈,大家就瞎讨论了技术栈和这个错误可能的原因啥的。

我也顺带瞎扯了一句:可能会出性能问题(因为我一直觉得国内擅长Hibernate的开发者比较少)


谁想到,今天在获取核酸码的时候真的碰到各种困难,在获取核酸码的时候,就一直刷不出来,有时候显示人多,有时候504错误:

上面我是12点尝试的,后来16、17点还看到很多朋友圈反应各种卡住,刷不出来。


可能这个系统确实太赶了,所以没做好?不过这个谁知道呢?作为一名技术博主就不瞎猜了。

顺手分享一下为什么我觉得用spring data jpa,很可能会挂?

先说说常规国内用的比较多的技术MyBatis,因为大家都是用直接写SQL的方式来实现数据读写的,这个时候团队里DBA、数据库专家、或者实力强点的开发,往往自己已经能够把SQL执行优化到比较好的地步了。这个是否能做好,与我们对SQL、Java这些知识的掌握程度有关

而当我们用Spring Data JPA这样的框架时候,开发者在框架的帮助下,好多SQL都被隐藏了,喜欢些Java代码来替代SQL的开发过程是挺爽的,但也因为这个原因,他可能并不知道最终自己写的代码真正会执行的SQL具体是怎么样的。

这的时候对于优化就带了很大的难度,对于专业DBA来说,他一般都是不具备Spring Data JPA代码到SQL转化的认识,他是很难帮你做静态分析的。而开发者一侧也有这个问题,如果不是很熟悉Hibernate的话,就很容易写出低性能的代码(不代表框架实现的低性能,核心还是使用姿势的问题)。

所以,我一直建议在高并发系统中对数据访问框架的选型一定要慎重,不是说Spring Data JPA不行,而是需要有熟悉的人来把握(特别提这点的原因是国好多是半调子)。不然就比较容易出现性能问题,但是MyBatis的话,对于国内开发者来说,因为直接写SQL,所以还是相对还是更容易理解和把控一些。

好了,借今天核酸码的现象,跟大家聊聊这两个框架的想法,不知道你是否认同?欢迎留言区说说你的观点。

来源:https://mp.weixin.qq.com/s/43bE8juIRQbQLO3vBTUKWA

收起阅读 »

面试官:知道 Flutter 生命周期?下周来入职!

作为一名移动端开发工程师,刚接触 Flutter 的时候,一定会有这样的疑问:Flutter 的生命周期是怎么样的?是如何处理生命周期的?我的 onCreate()[Android] 在哪里?viewDidLoad()[iOS] 呢? 我的业务逻辑应该放在哪里...
继续阅读 »

作为一名移动端开发工程师,刚接触 Flutter 的时候,一定会有这样的疑问:Flutter 的生命周期是怎么样的?是如何处理生命周期的?我的 onCreate()[Android] 在哪里?viewDidLoad()[iOS] 呢? 我的业务逻辑应该放在哪里处理?初始化数据呢?希望看了这篇文章后,可以对你有一点小小的帮助。


安卓


如果你是一名安卓开发工程师,那么对于 Activity 生命周期肯定不陌生



  • onCreate

  • onStart

  • onResume

  • onPause

  • onStop

  • onDestroy


android_life_cycle


iOS


如果你是一名 iOS 开发工程师,那么 UIViewController 的生命周期肯定也已经很了解了。



  • viewDidLoad

  • viewWillAppear

  • viewDidAppear

  • viewWillDisappear

  • viewDidDisappear

  • viewDidUnload


ios_life_cycle


Flutter


知道了 Android 和 iOS 的生命周期,那么 Flutter 呢?有和移动端对应的生命周期函数么?如果之前你对 Flutter 有一点点了解的话,你会发现 Flutter 中有两个主要的 Widget:StatelessWidget(无状态)StatefulWidget(有状态)。本篇文章我们主要来介绍下 StatefulWidget,因为它有着和 Android 和 iOS 相似的生命周期。


StatelessWidget


无状态组件是不可变的,这意味着它们的属性不能变化,所有的值都是最终的。可以理解为将外部传入的数据转化为界面展示的内容,只会渲染一次。
对于无状态组件生命周期只有 build 这个过程。无状态组件的构建方法通常只在三种情况下会被调用:小组件第一次被插入树中,小组件的父组件改变其配置,以及它所依赖的 InheritedWidget 发生变化时。


StatefulWidget


有状态组件持有的状态可能在 Widget 生命周期中发生变化,是定义交互逻辑和业务逻辑。可以理解为具有动态可交互的内容界面,会根据数据的变化进行多次渲染。实现一个 StatefulWidget 至少需要两个类:



  • 一个是 StatefulWidget 类。

  • 另一个是 Sate 类。StatefulWidget 类本身是不可变的,但是 State 类在 Widget 生命周期中始终存在。StatefulWidget 将其可变的状态存储在由 createState 方法创建的 State 对象中,或者存储在该 State 订阅的对象中。


StatefulWidget 生命周期



  • createState:该函数为 StatefulWidget 中创建 State 的方法,当 StatefulWidget 被创建时会立即执行 createState。createState 函数执行完毕后表示当前组件已经在 Widget 树中,此时有一个非常重要的属性 mounted 被置为 true。

  • initState:该函数为 State 初始化调用,只会被调用一次,因此,通常会在该回调中做一些一次性的操作,如执行 State 各变量的初始赋值、订阅子树的事件通知、与服务端交互,获取服务端数据后调用 setState 来设置 State。

  • didChangeDependencies:该函数是在该组件依赖的 State 发生变化时会被调用。这里说的 State 为全局 State,例如系统语言 Locale 或者应用主题等,Flutter 框架会通知 widget 调用此回调。类似于前端 Redux 存储的 State。该方法调用后,组件的状态变为 dirty,立即调用 build 方法。

  • build:主要是返回需要渲染的 Widget,由于 build 会被调用多次,因此在该函数中只能做返回 Widget 相关逻辑,避免因为执行多次而导致状态异常。

  • reassemble:主要在开发阶段使用,在 debug 模式下,每次热重载都会调用该函数,因此在 debug 阶段可以在此期间增加一些 debug 代码,来检查代码问题。此回调在 release 模式下永远不会被调用。

  • didUpdateWidget:该函数主要是在组件重新构建,比如说热重载,父组件发生 build 的情况下,子组件该方法才会被调用,其次该方法调用之后一定会再调用本组件中的 build 方法。

  • deactivate:在组件被移除节点后会被调用,如果该组件被移除节点,然后未被插入到其他节点时,则会继续调用 dispose 永久移除。

  • dispose:永久移除组件,并释放组件资源。调用完 dispose 后,mounted 属性被设置为 false,也代表组件生命周期的结束。


不是生命周期但是却非常重要的几个概念


下面这些并不是生命周期的一部分,但是在生命周期中起到了很重要的作用。



  • mounted:是 State 中的一个重要属性,相当于一个标识,用来表示当前组件是否在树中。在 createState 后 initState 前,mounted 会被置为 true,表示当前组件已经在树中。调用 dispose 时,mounted 被置为 false,表示当前组件不在树中。

  • dirty:表示当前组件为脏状态,下一帧时将会执行 build 函数,调用 setState 方法或者执行 didUpdateWidget 方法后,组件的状态为 dirty。

  • clean:与 dirty 相对应,clean 表示组件当前的状态为干净状态,clean 状态下组件不会执行 build 函数。


stateful_widget_lifecycle 生命周期流程图


上图为 flutter 生命周期流程图


大致分为四个阶段



  1. 初始化阶段,包括两个生命周期函数 createState 和 initState;

  2. 组件创建阶段,包括 didChangeDependencies 和 build;

  3. 触发组件多次 build ,这个阶段有可能是因为 didChangeDependencies、 setState 或者 didUpdateWidget 而引发的组件重新 build ,在组件运行过程中会多次触发,这也是优化过程中需要着重注意的点;

  4. 最后是组件销毁阶段,deactivate 和 dispose。


组件首次加载执行过程


首先我们来实现下面这段代码(类似于 flutter 自己的计数器项目),康康组件首次创建是否按照上述流程图中的顺序来执行的。



  1. 创建一个 flutter 项目;

  2. 创建 count_widget.dart 中添加以下代码;


import 'package:flutter/material.dart';

class CountWidget extends StatefulWidget {
CountWidget({Key key}) : super(key: key);

@override
_CountWidgetState createState() {
print('count createState');
return _CountWidgetState();
}
}

class _CountWidgetState extends State<CountWidget> {
int _count = 0;
void _incrementCounter() {
setState(() {
print('count setState');
_count++;
});
}

@override
void initState() {
print('count initState');
super.initState();
}

@override
void didChangeDependencies() {
print('count didChangeDependencies');
super.didChangeDependencies();
}

@override
void didUpdateWidget(CountWidget oldWidget) {
print('count didUpdateWidget');
super.didUpdateWidget(oldWidget);
}

@override
void deactivate() {
print('count deactivate');
super.deactivate();
}

@override
void dispose() {
print('count dispose');
super.dispose();
}

@override
void reassemble() {
print('count reassemble');
super.reassemble();
}

@override
Widget build(BuildContext context) {
print('count build');
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'$_count',
style: Theme.of(context).textTheme.headline4,
),
Padding(
padding: EdgeInsets.only(top: 100),
child: IconButton(
icon: Icon(
Icons.add,
size: 30,
),
onPressed: _incrementCounter,
),
),
],
),
);
}
}

上述代码把 StatefulWidget 的一些生命周期都进行了重写,并且在执行中都打印了标识,方便看到函数的执行顺序。



  1. 在 main.dart 中加载该组件。代码如下:


import 'package:flutter/material.dart';

import './pages/count_widget.dart';

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);

final String title;

@override
_MyHomePageState createState() {
return _MyHomePageState();
}
}

class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: CountWidget(),
);
}
}

这时 CountWidget 作为 MyHomePage 的子组件。我们打开模拟器,开始运行。在控制台可以看到如下日志,可以看出 StatefulWidget 在第一次被创建的时候是调用下面四个函数。


flutter: count createState
flutter: count initState
flutter: count didChangeDependencies
flutter: count build

点击屏幕上的 ➕ 按钮,_count 增加 1,模拟器上的数字由 0 变为 1,日志如下。也就是说在状态发生变化的时候,会调用 setStatebuild 两个函数。


flutter: count setState
flutter: count build

command + s 热重载后,日志如下:


flutter: count reassemble
flutter: count didUpdateWidget
flutter: count build

注释掉 main.dart 中的 CountWidget,command + s 热重载后,这时 CountWidget 消失在模拟器上,日志如下:


class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
// body: CountWidget(),
);
}
}

flutter: count reassemble
flutter: count deactivate
flutter: count dispose

经过上述一系列操作之后,通过日志打印并结合生命周期流程图,我们可以很清晰的看出各生命周期函数的作用以及理解生命周期的几个阶段。
相信很多细心的同学已经发现了一个细节,那就是 build 方法在不同的操作中都被调用了,下面我们来介绍什么情况下会触发组件再次 build。


触发组件再次 build


触发组件再次 build 的方式有三种,分别是 setStatedidChangeDependenciesdidUpdateWidget


1.setState 很好理解,只要组件状态发生变化时,就会触发组件 build。在上述的操作过程中,点击 ➕ 按钮,_count 会加 1,结果如下图:


set_state


2.didChangeDependencies,组件依赖的全局 state 发生了变化时,也会调用 build。例如系统语言等、主题色等。


3.didUpdateWidget,我们以下方代码为例。在 main.dart 中,同样的重写生命周期函数,并打印。在 CountWidget 外包一层 Column ,并创建同级的 RaisedButton 做为父 Widget 中的计数器。


class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);

final String title;

@override
_MyHomePageState createState() {
print('main createState');
return _MyHomePageState();
}
}

class _MyHomePageState extends State<MyHomePage> {
int mainCount = 0;

void _changeMainCount() {
setState(() {
print('main setState');
mainCount++;
});
}

@override
void initState() {
print('main initState');
super.initState();
}

@override
void didChangeDependencies() {
print('main didChangeDependencies');
super.didChangeDependencies();
}

@override
void didUpdateWidget(MyHomePage oldWidget) {
print('main didUpdateWidget');
super.didUpdateWidget(oldWidget);
}

@override
void deactivate() {
print('main deactivate');
super.deactivate();
}

@override
void dispose() {
print('main dispose');
super.dispose();
}

@override
void reassemble() {
print('main reassemble');
super.reassemble();
}

@override
Widget build(BuildContext context) {
print('main build');
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: <Widget>[
RaisedButton(
onPressed: () => _changeMainCount(),
child: Text('mainCount = $mainCount'),
),
CountWidget(),
],
),
);
}
}

重新加载 app,可以看到打印日志如下:


father_widget_create_state


flutter: main createState
flutter: main initState
flutter: main didChangeDependencies
flutter: main build
flutter: count createState
flutter: count initState
flutter: count didChangeDependencies
flutter: count build

可以发现:



  • 父组件也经历了 createStateinitStatedidChangeDependenciesbuild 这四个过程。

  • 并且父组件要在 build 之后才会创建子组件。


点击 MyHomePage(父组件)的 mainCount 按钮 ,打印如下:


flutter: main setState
flutter: main build
flutter: count didUpdateWidget
flutter: count build

点击 CountWidget 的 ➕ 按钮,打印如下:


flutter: count setState
flutter: count build

可以说明父组件的 State 变化会引起子组件的 didUpdateWidget 和 build,子组件自己的状态变化不会引起父组件的状态改变


组件销毁


我们重复上面的操作,为 CountWidget 添加一个子组件 CountSubWidget,并用 count sub 前缀打印日志。重新加载 app。


注释掉 CountWidget 中的 CountSubWidget,打印日志如下:


flutter: main reassemble
flutter: count reassemble
flutter: count sub reassemble
flutter: main didUpdateWidget
flutter: main build
flutter: count didUpdateWidget
flutter: count build
flutter: count sub deactivate
flutter: count sub dispose

恢复到注释前,注释掉 MyHomePage 中的 CountWidget,打印如下:


flutter: main reassemble
flutter: count reassemble
flutter: count sub reassemble
flutter: main didUpdateWidget
flutter: main build
flutter: count deactivate
flutter: count sub deactivate
flutter: count sub dispose
flutter: count dispose

因为是热重载,所以会调用 reassembledidUpdateWidgetbuild,我们可以忽略带有这几个函数的打印日志。可以得出结论:
父组件移除,会先移除节点,然后子组件移除节点,子组件被永久移除,最后是父组件被永久移除。


Flutter App Lifecycle


上面我们介绍的生命周期主要是 StatefulWidget 组件的生命周期,下面我们来简单介绍一下和 app 平台相关的生命周期,比如退出到后台。


我们创建 app_lifecycle_state.dart 文件并创建 AppLifecycle,他是一个 StatefulWidget,但是他要继承 WidgetsBindingObserver。


import 'package:flutter/material.dart';

class AppLifecycle extends StatefulWidget {
AppLifecycle({Key key}) : super(key: key);

@override
_AppLifecycleState createState() {
print('sub createState');
return _AppLifecycleState();
}
}

class _AppLifecycleState extends State<AppLifecycle>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
print('sub initState');
}

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// TODO: implement didChangeAppLifecycleState
super.didChangeAppLifecycleState(state);
print('didChangeAppLifecycleState');
if (state == AppLifecycleState.resumed) {
print('resumed:');
} else if (state == AppLifecycleState.inactive) {
print('inactive');
} else if (state == AppLifecycleState.paused) {
print('paused');
} else if (state == AppLifecycleState.detached) {
print('detached');
}
}

@override
Widget build(BuildContext context) {
print('sub build');
return Container(
child: Text('data'),
);
}
}

didChangeAppLifecycleState 方法是重点,AppLifecycleState 中的状态包括:resumedinactivepauseddetached 四种。


didChangeAppLifecycleState 方法的依赖于系统的通知(notifications),正常情况下,App是可以接收到这些通知,但有个别情况下是无法接收到通知的,比如用户关机等。它的四种生命周期状态枚举源码中有详细的介绍和说明,下面附上源码以及简单的翻译说明。


app_life_cycle_state



  • resumed:该应用程序是可见的,并对用户的输入作出反应。也就是应用程序进入前台。

  • inactive:应用程序处于非活动状态,没有接收用户的输入。在 iOS 上,这种状态对应的是应用程序或 Flutter 主机视图在前台非活动状态下运行。当处于电话呼叫、响应 TouchID 请求、进入应用切换器或控制中心时,或者当 UIViewController 托管的 Flutter 应用程序正在过渡。在 Android 上,这相当于应用程序或 Flutter 主机视图在前台非活动状态下运行。当另一个活动被关注时,如分屏应用、电话呼叫、画中画应用、系统对话框或其他窗口,应用会过渡到这种状态。也就是应用进入后台。

  • pause:该应用程序目前对用户不可见,对用户的输入没有反应,并且在后台运行。当应用程序处于这种状态时,引擎将不会调用。也就是说应用进入非活动状态。

  • detached:应用程序仍然被托管在flutter引擎上,但与任何主机视图分离。处于此状态的时机:引擎首次加载到附加到一个平台 View 的过程中,或者由于执行 Navigator pop,view 被销毁。


除了 app 生命周期的方法,Flutter 还有一些其他不属于生命周期,但是也会在一些特殊时机被观察到的方法,如 didChangeAccessibilityFeatures(当前系统改变了一些访问性活动的回调)didHaveMemoryPressure(低内存回调)didChangeLocales(用户本地设置变化时调用,如系统语言改变)didChangeTextScaleFactor(文字系数变化) 等,如果有兴趣的话,可以去试一试。


总结


本篇文章主要介绍了 Widget 中的 StatefulWidget 的生命周期,以及 Flutter App 相关的生命周期。但是要切记,StatefulWidget 虽好,但也不要无脑的所有 Widget 全都用它,能使用 StatelessWidget 还是要尽量去使用 StatelessWidget(仔细想一下,这是为什么呢?)。好啦,看完本篇文章,你就是 Flutter 初级开发工程师了,可以去面试了(狗头保命)。


最后


真正坚持到最后的人,往往靠的不是短暂的激情,而是恰到好处的喜欢和投入。你还那么年轻,完全可以成为任何你想要成为的样子!


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

Flutter 蒙层控件

功能说明 新手引导高亮蒙层 图片进度条 使用说明 Import the packages: import 'package:flutter_mask_view/flutter_mask_view.dart'; show height-light mask ...
继续阅读 »

功能说明



  • 新手引导高亮蒙层

  • 图片进度条


使用说明


Import the packages:


import 'package:flutter_mask_view/flutter_mask_view.dart';

show height-light mask for newer:


 Scaffold(
body: Stack(
children: [
//only display background for demo
Image.asset(ImagesRes.BG_HOME),

//config
HeightLightMaskView(
//控件大小
maskViewSize: Size(720, 1080),
//蒙层颜色
backgroundColor: Colors.blue.withOpacity(0.6),
//高亮区域颜色
color: Colors.transparent,
//设置高亮区域形状,如果width = height = radius 为圆形,否则矩形
rRect: RRect.fromRectAndRadius(
Rect.fromLTWH(100, 100, 50, 50),
Radius.circular(50),
),
)
],
),
)

more:


          HeightLightMaskView(
maskViewSize: Size(720, 1080),
backgroundColor: Colors.blue.withOpacity(0.6),
color: Colors.transparent,
//自定义蒙层区域形状
pathBuilder: (Size size) {
return Path()
..moveTo(100, 100)
..lineTo(50, 150)
..lineTo(150, 150);
},
//在蒙层上自定义绘制内容
drawAfter: (Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.red
..strokeWidth = 15
..style = PaintingStyle.stroke;
canvas.drawCircle(Offset(150, 150), 50, paint);
},
//是否重绘,默认return false, 如果使用动画,此返回true
rePaintDelegate: (CustomPainter oldDelegate){
return false;
},
)

Display



create image progress bar:


      ImageProgressMaskView(
size: Size(360, 840),
//进度图片
backgroundRes: 'images/bg.png',
//当前进度
progress: 0.5,
//蒙层形状,内置以下两种蒙层:
//矩形蒙层:PathProviders.sRecPathProvider
//水波蒙层(可配置水波高度和密度):PathProviders.createWaveProvider

//自定义进度蒙层
pathProvider: PathProviders.createWaveProvider(60, 100),
),
)

PathProviders.sRecPathProvider:



PathProviders.createWaveProvider:



与动画联动:


class _MaskTestAppState extends State<MaskTestApp>
with SingleTickerProviderStateMixin {
late AnimationController _controller;

@override
void initState() {
_controller =
AnimationController(duration: Duration(seconds: 5), vsync: this);
_controller.forward();
super.initState();
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Stack(
alignment: Alignment.center,
children: [
ImageProgressMaskView(
size: Size(300, 300),
backgroundRes: ImagesRes.IMG,
progress: _controller.value,
pathProvider: PathProviders.createWaveProvider(60, 40),
rePaintDelegate: (_) => true,
),
Text(
'${(_controller.value * 100).toInt()} %',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
fontSize: 30,
),
)
],
);
},
),
),
);
}
}

Result:


case 1:



case 2: (png)



仓库地址


PUB


Github


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

你会写注释吗?

前言有一本书叫《代码整洁之道》,不知你看过没?初次听闻此书,并未激发我的阅读欲。再次听闻,不免心想:代码竟还整洁之道?我倒要瞧瞧,怎么个整洁法。我是怀着试探地心看了这本书,结果收获了满脑子糟糕的代码。天呐!这代码我貌似一句也看不懂,幸好还有文字,尚可宽慰我这颗...
继续阅读 »

前言

有一本书叫《代码整洁之道》,不知你看过没?

初次听闻此书,并未激发我的阅读欲。再次听闻,不免心想:代码竟还整洁之道?我倒要瞧瞧,怎么个整洁法。

我是怀着试探地心看了这本书,结果收获了满脑子糟糕的代码。天呐!这代码我貌似一句也看不懂,幸好还有文字,尚可宽慰我这颗被代码撞乱的心,于是咬咬牙读了下去。

这本书里面讲了很多代码整洁之道,关于有意义的命名、函数、注释、格式、错误处理、边界等共十七大篇章。如果你感兴趣,可以去看看。我只是粗略地看了一下,因为有些我也看不大明白。特别是当某些代码脱离了计算机而存在的时候,我好像不认识它们了,它们变得异常陌生。恕我我孤陋寡闻了,哎。

尽管如此,此书第四章中,关于“注释”的代码整洁之道,却给我留下了异常深刻的印象。Why? 因为里面关于注释的观点刷新了我的认知,与我的思想产生了一点点灵魂的碰撞,并且说服了我,还驱动我写下了这篇文章。

一、被注释吸引

下面是“注释”篇章的开头两段,特意贴了上来,因为我就是被这样的开头吸引了。希望它能带给你一点点启发。


不知你读完以上两段,作何感想?

我的感想是:如果你的代码写得足够优秀,是不需要过多注释的。注释最多可以证明糟糕的代码。

额,此刻我很想找一个捂脸的表情。与此同时,我在脑海里迅速地回忆了一遍注释之于我的心历路程:从最初知道“注释”这么个神奇玩意儿时的欣喜,到步步沦陷“注释”的魔爪,以致如今看着满屏的代码,不写点儿注释都感觉空落落的......

收回来,继续品。 作者开篇的观点约莫如下:

  • 注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败

  • 如果你发现自己需要写注释,再想想是否有办法翻盘,用代码来表达

  • 注释会撒谎,代码在变动演化,但注释不能总是跟着走

  • 只有代码是唯一准确的信息来源

注意,作者用来了“失败”一词。你无法找到表达自我的恰当方法,所以就要用注释,这并不值得庆祝。当然,这并不意味作者就完全否定了注释的价值,程序员应当负责将代码保持在可维护、有关联、精确的高度。只不过作者更倾向于把力气用在写清楚代码上,直接保证无须编写注释,或者花心思减少注释量。

二、好的注释

有些注释是必须的,作者列举了一些值得写的注释。

  • 公司代码规范要求编写与法律有关的注释

  • 提供基本信息的注释

  • 对意图的解释

  • 阐释:把晦涩难明的参数或返回值的意义翻译为某种可读形式

  • 警示:用于警告其他程序员会出现某种后果的注释

  • TODO 注释:是一种程序员认为应该做,但由于某些原因目前还没做的工作

  • 放大:放大某种看起来不合理之物的重要性的注释

  • 公共 API 中的 Javadoc

尽管如此,作者一再强调:唯一真正好的注释是你想办法不去写的注释。足见作者对注释之深恶痛疾,对糟糕代码之嫌弃,对代码整洁要求之高。你可以细品。

三、坏的注释

果然是有代码洁癖的人,作者用了更多的篇幅来描述坏的注释。

  • 喃喃自语:因为过程需要就添加注释,就是无谓之举

  • 多余的注释:并不比代码本身提供更多的信息,甚至比读代码所花时间长

  • 误导性注释:写出不够精确的注释误导读者

  • 循规式注释:每个函数都要有 Javadoc 或每个变量都要有注释的规则愚不可及

  • 日志式注释:每次编辑代码时,在模块开始处添加一条注释,应当全部删除

  • 废话注释:喋喋不休,废话连篇的注释,一旦代码修改,将变成一堆谎言

  • 能用函数或变量时就别用注释:建议重构代码,删掉注释

  • 位置标记:在源代码中标记某个特别位置,多数实属无理又鸡零狗碎

  • 括号后面的注释:如果你发现自己想标记右括号,其实应该做的是缩短函数

  • 归属与署名:源代码控制系统是这类信息最好的归属地

  • 注释掉的代码:注释掉的代码堆积在一起,就像酒瓶底的渣滓一般

  • HTML 注释:源代码注释中的 HTML 标记是一种厌物

  • 非本地信息:假如你一定要写注释,请确保它描述了离它最近的代码

  • 信息过多:别在注释中添加有趣的历史性话题或无关的细节描述

  • 不明显的关系:注释及其描述的代码之间的联系应该显而易见

  • 函数头:短函数不需要太多的描述,选个好的函数名胜于写函数头注释

一言以蔽之:用整理代码的决心替代创造废话的冲动吧。 你会发现自己成为更优秀、更快乐的程序员。

小结

作者把“注释”拎出来,说了这么多,最终还是回归到了代码本身。

那如何才能写出整洁的代码呢?如果你不明白整洁对代码的意义,尝试去写整洁代码就毫无意义。如果你明白糟糕的代码给你带来的代价,你就会明白,花时间保持代码整洁不但有关效率,还有关生存。争取让营地比你来时更干净吧!

最后,贴上书中震撼我的一隅,希望它能指引你逐渐走向代码整洁之道,与君共勉!



作者:linwanxia
来源:https://juejin.cn/post/7083029096615116837

收起阅读 »

你确定(a == 1 && a == 2 && a == 3)不能为true?

前言最近遇到一个非常有意思的面试题: JavaScript中有没有可能让(a== 1 && a ==2 && a==3)返回true?讲真刚看到这题的时候,我是用这种眼神看面试官的:你TM逗我呢? 尊重一下我可行?没10年脑血栓...
继续阅读 »

前言

最近遇到一个非常有意思的面试题: JavaScript中有没有可能让(a== 1 && a ==2 && a==3)返回true?

讲真刚看到这题的时候,我是用这种眼神看面试官的:你TM逗我呢? 尊重一下我可行?没10年脑血栓问不出这玩意,


但看他一脸"贱笑",一副你一定答不出来的感觉,我觉得此事定不简单...


障眼法我TM给跪了

咱们先不管面试官的意图是什么,具体考察的是什么知识,先来看看几种奇特的解法。

解法1:隐藏字符 + if

const if = () => !0
const a = 9

if(a == 1 && a == 2 && a == 3)
{
 console.log('前端胖头鱼') // 前端胖头鱼
}

眼见为虚


我觉得此时你和我一样,在严重怀疑自己怕是个假前端if也能被改写?a明明是9却可以等于1、2、3


别急,这其实是一个障眼法,只是取巧蒙蔽了我们的双眼,请看下图


真相大白if的后面有个隐藏字符,本质上是声明了一个无论输入啥都返回true函数,而下面的代码块,更是和这个函数没半毛钱关系,怎么样都会执行!!!

{
 console.log('前端胖头鱼') // 前端胖头鱼
}

所以通过构造一个看似重写了if的代码块,仿佛真的实现了题目,实在是太骚了!!!

解法2:隐藏字符 + a变量

有了上面的经验,接下来的解法,你也不会感到奇怪了。

const aᅠ = 1
const a = 2
const ᅠa = 3

if (aᅠ == 1 && a == 2 && ᅠa == 3) {
 console.log('前端胖头鱼') // 前端胖头鱼
}


解法3:隐藏字符 + 数字变量

既然可以伪造三个a变量,那也可以伪造三个123变量嘛

const a = 1
const ᅠ1 = a
const ᅠ2 = a
const ᅠ3 = a

if (a == ᅠ1 && a == ᅠ2 && a == ᅠ3) {
 console.log('前端胖头鱼') // 前端胖头鱼
}

大千世界,果然眼见为虚啊!!!


再来一种奇特的解法

上面几种解法本质上都没有使 a == 1 && a == 2 && a == 3true,不过是障眼法,大家笑笑就好啦!接下来我要认真起来了...

解法4:“with”

MDN上映入眼帘的是一个警告,仿佛他的存在就是个错误,我也从来没有在实际工作中用过他,但他却可以用来解决这个题目。


let i = 1

with ({
 get a() {
   return i++
}
}) {
 if (a == 1 && a == 2 && a == 3) {
   console.log('前端胖头鱼')
}
}

聪明的你甚至都不用我解释代码啥意思了。

隐式转换成解题的关键

上面给出的4种解法多少有点歪门邪道的意思,为了让面试官死心,接下来的才是正解之道,而JS中的隐式转换规则大概也是出这道题的初衷。

隐式转换部分规则

JS中使用==对两个值进行比较时,会进行如下操作:

  1. 将两个被比较的值转换为相同的类型。

  2. 转换后(等式的一边或两边都可能被转换)再进行值的比较。

比较的规则如下表(mdn


从表中可以得到几点信息为了让(a == 1),a只有这几种:

  1. a类型为String,并且可转换为数字1('1' == 1 => true

  2. a类型为Boolean,并且可转换为数字1 (true == 1 => true)

  3. a类型为Object,通过转换机制后,可转换为数字1 (请看下文

对象转原始类型的"转换机制"

规则1和2没有什么特殊的地方,我们来看看3:

对象转原始类型,会调用内置的[ToPrimitive]函数,逻辑大致如下:

  1. 如果有Symbol.toPrimitive方法,优先调用再返回,否则进行2。

  2. 调用valueOf,如果可以转换为原始类型,则返回,否则进行3。

  3. 调用toString,如果可以转换为原始类型,则返回,否则进行4。

  4. 如果都没有返回原始类型,会报错。

const obj = {
 value: 1,
 valueOf() {
   return 2
},
 toString() {
   return '3'
},
[Symbol.toPrimitive]() {
   return 4
}
}

obj == 4 // true
// 您可以将Symbol.toPrimitive、toString、valueOf分别注释掉验证转换规则

解法5: Symbol.toPrimitive

我们可以利用隐式转换规则3完成题目(看完答案你就知道为什么啦!

const a = {
 i: 1,
[Symbol.toPrimitive]() {
   return this.i++
}
}
// 每次进行a == xxx时都会先经过Symbol.toPrimitive函数,自然也就可以实现a依次递增的效果
if (a == 1 && a == 2 && a == 3) {
 console.log('前端胖头鱼') // 前端胖头鱼
}

解法6: valueOf vs toString

当然也可以利用valueOftoString

let a = {
 i: 1,
 // valueOf替换成toString效果是一样的
 // toString
 valueOf() {
   return this.i++
}
}

if (a == 1 && a == 2 && a == 3) {
 console.log('前端胖头鱼') // 前端胖头鱼
}

解法7:Array && join

数组对象在进行隐式转换时,同样符合规则3,只是在toString时还会调用join方法。所以也可以从这里下手

let a = [1, 2, 3]

a.join = a.shift

if (a == 1 && a == 2 && a == 3) {
 console.log('前端胖头鱼') // 前端胖头鱼
}

数据劫持亦是一条出路

通过隐式转换我们做出了3种让a == 1 && a == 2 && a == 3返回true的方案,聪明的你一定想到另一种思路,数据劫持,伟大的Vue就曾使用数据劫持赢得了千万开发者的芳心,我们也试试用它来解决这道面试题

解法8:Object.defineProperty

通过劫持window对象,每次读取a属性时,都给_a 增加1

let _a = 1
Object.defineProperty(window, 'a', {
 get() {
   return _a++
}
})

if (a == 1 && a == 2 && a == 3) {
 console.log('前端胖头鱼') // 前端胖头鱼
}

解法9:Proxy

当然还有另一种劫持数据的方式,Vue3也是将响应式原理中的数据劫持Object.defineProperty换成了Proxy

let a = new Proxy({ i: 1 }, {
get(target) {
return () => target.i++
}
})

if (a == 1 && a == 2 && a == 3) {
console.log('前端胖头鱼') // 前端胖头鱼
}

最后

希望能一直给大家分享实用、基础、进阶的知识点,一起早早下班,快乐摸鱼。

作者:前端胖头鱼
来源:https://juejin.cn/post/7079936779914051615

收起阅读 »

作为一名前端,该如何理解Nginx?

大家好,我叫小杜杜,作为一名小前端,只需要好好写代码,至于部署相关的操作,我们通常接触不到,正所谓专业的人干专业的事,我们在工作中并不需要去配置,但这并不代表不需要了解,相信大家都多多少少听过nginx,所以今天就聊聊,还请大家多多支持~Nginx是什么?Ng...
继续阅读 »

大家好,我叫小杜杜,作为一名小前端,只需要好好写代码,至于部署相关的操作,我们通常接触不到,正所谓专业的人干专业的事,我们在工作中并不需要去配置,但这并不代表不需要了解,相信大家都多多少少听过nginx,所以今天就聊聊,还请大家多多支持~


Nginx是什么?

Nginx (engine x) 是一个轻量级、高性能的HTTP反向代理服务器,同时也是一个通用代理服务器(TCP/UDP/IMAP/POP3/SMTP),最初由俄罗斯人Igor Sysoev编写。

简单的说:

  • Nginx是一个拥有高性能HTTP和反向代理服务器,其特点是占用内存少并发能力强,并且在现实中,nginx的并发能力要比在同类型的网页服务器中表现要好

  • Nginx专为性能优化而开发,最重要的要求便是性能,且十分注重效率,有报告nginx能支持高达50000个并发连接数

正向代理和反向代理

Nginx 是一个反向代理服务器,那么反向代理是什么呢?我们先看看什么叫做正向代理

正向代理:局域网中的电脑用户想要直接访问网络是不可行的,只能通过代理服务器(Server)来访问,这种代理服务就被称为正向代理。

就好比我们俩在一块,直接对话即可,但如果我和你分隔两地,我们要想对话,必须借助一个通讯设备(如:电话)来沟通,那么这个通讯设备就是"代理服务器",这种行为称为“正向代理”

那么反向代理是什么呢?

反向代理:客户端无法感知代理,因为客户端访问网络不需要配置,只要把请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据,然后再返回到客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器地址,隐藏了真实服务器IP地址。

在正向代理中,我向你打电话,你能看到向你打电话的电话号码,由电话号码知道是我给你打的,那么此时我用虚拟电话给你打过去,你看到的不再是我的手机号,而是虚拟号码,你便不知道是我给你打的,这种行为变叫做"反向代理"。

在以上述的例子简单的说下:

  • 正向代理:我通过我的手机(proxy Server)去给你打电话,相当于我和我的手机是一个整体,与你的手机(Server)是分开的

  • 反向代理:我通过我的手机(proxy Server)通过软件转化为虚拟号码去给你打电话,此时相当于我的手机和你的手机是一个整体,和我是分开的

负载均衡

负载均衡:是高可用网络基础架构的关键组件,通常用于将工作负载分布到多个服务器来提高网站、应用、数据库或其他服务的性能和可靠性。

如果没有负载均衡,客户端与服务端的操作通常是:客户端请求服务端,然后服务端去数据库查询数据,将返回的数据带给客户端


但随着客户端越来越多,数据,访问量飞速增长,这种情况显然无法满足,我们从上图发现,客户端的请求和相应都是通过服务端的,那么我们加大服务端的量,让多个服务端分担,是不是就能解决这个问题了呢?

但此时对于客户端而言,他去访问这个地址就是固定的,才不会去管那个服务端有时间,你只要给我返回出数据就OK了,所以我们就需要一个“管理者“,将这些服务端找个老大过来,客户端直接找老大,再由老大分配谁处理谁的数据,从而减轻服务端的压力,而这个”老大“就是反向代理服务器,而端口号就是这些服务端的工号。


向这样,当有15个请求时,反向代理服务器会平均分配给服务端,也就是各处理5个,这个过程就称之为:负载均衡

动静分离

当客户端发起请求时,正常的情况是这样的:


就好比你去找客服,一般先是先说一大堆官方的话,你问什么,他都会这么说,那么这个就叫静态资源(可以理解为是html,css)

而回答具体的问题时,每个回答都是不同的,而这些不同的就叫做动态资源(会改变,可以理解为是变量)

在未分离的时候,可以理解为每个客服都要先说出官方的话,在打出具体的回答,这无异加大了客服的工作量,所以为了更好的有效利用客服的时间,我们把这些官方的话分离出来,找个机器人,让他代替客服去说,这样就减轻了客服的工作量。

也就是说,我们将动态资源和静态资源分离出来,交给不同的服务器去解析,这样就加快了解析的速度,从而降低由单个服务器的压力


安装 Nginx

关于 nginx 如何安装,这里就不做过多的介绍了,感兴趣的小伙伴看看这篇文章:【Linux】中如何安装nginx

这里让我们看看一些常用的命令:

  • 查看版本:./nginx -v

  • 启动:./nginx

  • 关闭:./nginx -s stop(推荐) 或 ./nginx -s quit

  • 重新加载nginx配置:./nginx -s reload

Nginx 的配置文件

配置文件分为三个模块:

  • 全局块:从配置文件开始到events块之间,主要是设置一些影响nginx服务器整体运行的配置指令。(按道理说:并发处理服务的配置时,值越大,可支持的并发处理量越多,但此时会受到硬件、软件等设备等的制约)

  • events块:影响nginx服务器与用户的网络连接,常用的设置包括是否开启对多workprocess下的网络连接进行序列化,是否允许同时接收多个网络连接等等

  • http块:如反向代理和负载均衡都在此配置

location 的匹配规则

共有四种方式:

    location[ = | ~ | ~* | ^~ ] url {
   
  }
复制代码
  • =精确匹配,用于不含正则表达式的url前,要求字符串与url严格匹配,完全相等时,才能停止向下搜索并处理请求

  • ^~:用于不含正则表达式的url前,要求ngin服务器找到表示url和字符串匹配度最高的location后,立即使用此location处理请求,而不再匹配

  • ~最佳匹配,用于表示url包含正则表达式,并且区分大小写。

  • ~*:与~一样,只是不区分大小写

注意:

  • 如果 url 包含正则表达式,则不需要~ 作为开头表示

  • nginx的匹配具有优先顺序,一旦匹配上就会立马退出,不再进行向下匹配

End

关于具体的配置可以参考:写给前端的nginx教程

致此,有关Nginx相关的知识就已经完成了,相信对于前段而言已经足够了,喜欢的点个赞👍🏻支持下吧(● ̄(エ) ̄●)


作者:小杜杜
来源:https://juejin.cn/post/7082655545491980301

收起阅读 »

我用 nodejs 爬了一万多张小姐姐壁纸

前言哈喽,大家好,我是小马,为什么要下载这么多图片呢? 前几天使用 uni-app + uniCloud 免费部署了一个壁纸小程序,那么接下来就需要一些资源,给小程序填充内容。爬取图片首先初始化项目,并且安装 axios 和 ch...
继续阅读 »

前言

哈喽,大家好,我是小马,为什么要下载这么多图片呢? 前几天使用 uni-app + uniCloud 免费部署了一个壁纸小程序,那么接下来就需要一些资源,给小程序填充内容。

爬取图片

首先初始化项目,并且安装 axios 和 cheerio

npm init -y && npm i axios cheerio

axios 用于爬取网页内容,cheerio 是服务端的 jquery api, 我们用它来获取 dom 中的图片地址;const axios = require('axios')

const cheerio = require('cheerio')

function getImageUrl(target_url, containerEelment) {
let result_list = []
const res = await axios.get(target_url)
const html = res.data
const $ = cheerio.load(html)
const result_list = []
$(containerEelment).each((element) => {
result_list.push($(element).find('img').attr('src'))
})
return result_list
}

这样就可以获取到页面中的图片 url 了。接下来需要根据 url 下载图片。

如何使用 nodejs 下载文件

方式一:使用内置模块 ‘https’ 和 ‘fs’

使用 node js 下载文件可以使用内置包或第三方库完成。

GET 方法用于 HTTPS 来获取要下载的文件。 createWriteStream() 是一个用于创建可写流的方法,它只接收一个参数,即文件保存的位置。Pipe()是从可读流中读取数据并将其写入可写流的方法。const fs = require('fs')

const https = require('https')

// URL of the image
const url = 'GFG.jpeg'

https.get(url, (res) => {
// Image will be stored at this path
const path = `${__dirname}/files/img.jpeg`
const filePath = fs.createWriteStream(path)
res.pipe(filePath)
filePath.on('finish', () => {
filePath.close()
console.log('Download Completed')
})
})

方式二:DownloadHelper
npm install node-downloader-helper

下面是从网站下载图片的代码。一个对象 dl 是由类 DownloadHelper 创建的,它接收两个参数:

  1. 将要下载的图像。
  2. 下载后必须保存图像的路径。

File 变量包含将要下载的图像的 URL,filePath 变量包含将要保存文件的路径。const { DownloaderHelper } = require('node-downloader-helper')


// URL of the image
const file = 'GFG.jpeg'
// Path at which image will be downloaded
const filePath = `${__dirname}/files`

const dl = new DownloaderHelper(file, filePath)

dl.on('end', () => console.log('Download Completed'))
dl.start()

方法三: 使用 download

是 npm 大神 sindresorhus 写的,非常好用

npm install download

下面是从网站下载图片的代码。下载函数接收文件和文件路径。const download = require('download')


// Url of the image
const file = 'GFG.jpeg'
// Path at which image will get downloaded
const filePath = `${__dirname}/files`

download(file, filePath).then(() => {
console.log('Download Completed')
})

最终代码

本来想去爬百度壁纸,但是清晰度不太够,而且还有水印等,后来, 群里有个小伙伴找到了一个 api,估计是某个手机 APP 上的高清壁纸,可以直接获得下载的 url,我就直接用了。

下面是完整代码

const download = require('download')
const axios = require('axios')

let headers = {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
}

function sleep(time) {
return new Promise((reslove) => setTimeout(reslove, time))
}

async function load(skip = 0) {
const data = await axios
.get(
'http://service.picasso.adesk.com/v1/vertical/category/4e4d610cdf714d2966000000/vertical',
{
headers,
params: {
limit: 30, // 每页固定返回30条
skip: skip,
first: 0,
order: 'hot',
},
}
)
.then((res) => {
return res.data.res.vertical
})
.catch((err) => {
console.log(err)
})
await downloadFile(data)
await sleep(3000)
if (skip < 1000) {
load(skip + 30)
} else {
console.log('下载完成')
}
}

async function downloadFile(data) {
for (let index = 0; index < data.length; index++) {
const item = data[index]

// Path at which image will get downloaded
const filePath = `${__dirname}/美女`

await download(item.wp, filePath, {
filename: item.id + '.jpeg',
headers,
}).then(() => {
console.log(`Download ${item.id} Completed`)
return
})
}
}

load()

上面代码中先要设置 User-Agent 并且设置 3s 延迟, 这样可以防止服务端阻止爬虫,直接返回 403。

直接 node index.js 就会自动下载图片了。

爬取运行中

来源:https://juejin.cn/post/7078206989402112037

收起阅读 »

React18正式版发布,未来发展趋势是?

2022年3月29号,React18正式版发布。从v16开始,React团队就在普及并发的概念。在v18的迭代过程中(alpha、Beta、RC),也一直在科普并发特性,所以正式版发布时,已经没有什么新鲜特性。本文主要讲解v18发布日志中透露的一些未来发展趋势...
继续阅读 »

2022年3月29号,React18正式版发布。

v16开始,React团队就在普及并发的概念。在v18的迭代过程中(alpha、Beta、RC),也一直在科普并发特性,所以正式版发布时,已经没有什么新鲜特性。

本文主要讲解v18发布日志中透露的一些未来发展趋势。

欢迎加入人类高质量前端框架研究群,带飞

开发者可能并不会接触到并发特性

React对增加API是很慎重的。从13年诞生至今,触发更新的方式都是this.setState

而引入并发概念后,光是与并发相关的API就有好几个,比如:

  • useTransition

  • useDeferredValue

甚至出现了为并发兜底的API(即并发情况下,不使用这些API可能会出bug),比如:

  • useSyncExternalStore

  • useInsertionEffect

一下多出这么多API,还不是像useState这种不使用不行的API,况且,并发这一特性对于多数前端开发者都有些陌生。

你可以代入自己的业务想想,让开发者上手使用并发特性有多难。

所以,在未来用v18开发的应用,开发者可能并不会接触到并发特性。这些特性更可能是由各种库封装好的。

比如:startTransition可以让用户在不同视图间切换的同时,不阻塞用户输入。

这一API很可能会由各种Router实现,再作为一个配置项开放给开发者。

万物皆可Suspense

对于React来说,有两类瓶颈需要解决:

  • CPU的瓶颈,如大计算量的操作导致页面卡顿

  • IO的瓶颈,如请求服务端数据时的等待时间

其中CPU的瓶颈通过并发特性的优先级中断机制解决。

IO的瓶颈则交给Suspense解决。

所以,未来一切与IO相关的操作,都会收敛到Suspense这一解决方案内。

从最初的React.lazy到如今仍在开发中的Server Components,最终万物皆可Suspense

这其中有些逻辑是很复杂的,比如:

  • Server Components

  • 新的服务端渲染方案

所以,这些操作不大可能是直接面向开发者的。

这又回到了上一条,这些操作会交由各种库实现。如果复杂度更高,则会交由基于React封装的框架实现,比如Next.jsRemix

这也是为什么React团队核心人物Sebastian会加入Next.js

可以说,React未来的定位是:一个前端底层操作系统,足够复杂,一般开发者慎用。

而开发者使用的是基于该操作系统实现的各种上层应用

总结

如果说v16之前各种React Like库还能靠体积、性能优势分走React部分蛋糕,那未来两者走的完全是两条赛道,因为两者的生态不再兼容。

未来不再会有React全家桶的概念,桶里的各个部件最终会沦为更大的框架中的一个小模块。

当前你们业务里是直接使用React呢,还是使用各种框架(比如Next.js)?

作者:魔术师卡颂
来源:https://juejin.cn/post/7080719159645962271 收起阅读 »

高并发之伪共享和缓存行填充(缓存行对齐)(@Contended)

1.使用缓存行(Cache Line)填充前后对比伪共享和缓存行填充,我们先看一个例子,让大家感受一下了解底层知识后,你的代码可以快到起飞的感jio: 在类中定义看似无用的成员属性,速度有质的提升。 如下是未使用缓存行(Cache Line)填充方法运行的结果...
继续阅读 »

1.使用缓存行(Cache Line)填充前后对比

伪共享和缓存行填充,我们先看一个例子,让大家感受一下了解底层知识后,你的代码可以快到起飞的感jio: 在类中定义看似无用的成员属性,速度有质的提升。 如下是未使用缓存行(Cache Line)填充方法运行的结果,可以看到耗时是3579毫秒:

而在其变量x的前后加上7个long类型到变量(在变量x前56Byte,后面也是56Byte,这就是缓存行填充,下面章节会详细介绍),当然这个14个变量是不会在代码中被用到的,但是为什么速度会提升将近2倍呢,如下图所示,可以看到耗时为1280毫秒:


ps:上面两个截图中的完整代码见
章节5,大家也可以直接跳转到章节去看下完整的代码。

为什么会这么神奇,这里为先提前说下结论,具体的大家可以往后看。

  • 缓存一致性是根据缓存行(Cache line)为单元来进行同步的,即缓存中的传输单元为缓存行,一个缓存行大小通常为64Byte;

  • 缓存行的内容一发生变化,就需要进行缓存同步;

  • 所以虽然用到的不是同一个数据,但是他们(数据X和数据Y)在同一个缓存行中,缓存行的内容一发生变化,就需要进行缓存同步,这个同步是需要时间的。

2.内存、缓存与寄存器之间如何传输数据

为什么会这样呢?前面我们提到过缓存一致性的问题,见笔者该篇博文:“了解高并发底层原理”,面试官:讲一下MESI(缓存一致性协议)吧,点击文字即可跳转。 其中内存、缓存与寄存器之间的关系图大致如下:


硬盘中的可执行文件加载到寄存器中进行运算的过程如下:

  1. 硬盘中的可执行文件(底层存储还是二进制的)加载到内存中,操作系统为其分配资源,变成了一个进程A,此时还没有跑起来;

  2. 过了一段时间之后,CPU0的时间片分配给了进程A,此时CPU0进行线程的装载,然后把需要用到的数据先从内存中读取到缓存中,读取的单元为一个缓存行,其大小现在通常为64字节(记住这个缓存行大小为64字节,这个非常重要,在后面会多次用到这个数值)。

  3. 然后数据再从缓存中读取到寄存器中,目前缓存一般为三级缓存,这里不具体画出。

  4. 寄存器得到了数据之后送去ALU(arithmetic and logic unit)做计算。

这里说一下为什么要设计三级缓存:

  • 电脑通过使用时钟来同步指令的执行。时钟脉冲在一个固定的频率(称为时钟频率)。当你买了一台1.5GHz的电脑,1.5GHz就是时钟频率,即每秒15亿次的时钟脉冲,一次完整的时钟脉冲称为一个周期(cycle),时钟并不记录分和秒。它以不变的速率简单跳动。

  • 其主要原因还是因为CPU方法内存消耗的时间太长了,CPU从各级缓存和内存中读取数据所需时间如下:

CPU访问大约需要的周期(cycle)大约需要的时间
寄存器1 cycle0ns
L1 Cache3—4 cycle1ns
L2 Cache10—20 cycle3ns
L3 Cache40—45 cycle15ns
内存60—90ns

3.缓存中数据共享问题(真实共享和伪共享)

3.1 真实共享(不同CPU的寄存器中都到了同一个变量X)

首先我们先说数据的真实共享,如下图,我们在CPU0和CPU1中都用到了数据X,现在不考虑数据Y。


如果不考虑缓存一致性,会出现如下问题: 在多线程情况下,此时由两个cpu同时开始读取了long X =0,然后同时执行如下语句,会出现如下情况:

int X = 0;
X++;

刚开始,X初始化为0,假设有两个线程A,B,

  1. A线程在CPU0上进行执行,从主存加载X变量的数值到缓存,然后从缓存中加载到寄存器中,在寄存器中执行X+1操作,得到X的值为1,此时得到X等于1的值还存放在CPU0的缓存中;

  2. 由于线程A计算X等于1的值还存放在缓存中,还没有刷新会内存,此时线程B执行在CPU1上,从内存中加载i的值,此时X的值还是0,然后进行X+1操作,得到X的值为1,存到CPU1的缓存中,

  3. A,B线程得到的值都是1,在一定的时间周期之后刷新回内存

  4. 写回内存后,两次X++操作之后,其值还是1;

可以看到虽然我们做了两次++X操作,但是只进行了一次加1操作,这就是缓存不一致带来的后果。

如何解决该问题:

  • 具体的我们可以通过MESI协议(详情见笔者该篇博文:blog.csdn.net/MrYushiwen/…)来保证缓存的一致性,如上图最中间的红字所示,在不同寄存器的缓存中,需要考虑数据的一致性问题,这个需要花费一定的时间来同步数据,从而达到缓存一致性的作用。

3.2伪共享(不同CPU的寄存器中用到了不同的变量,一个用到的是X,一个用到的是Y,并且XY在同一个缓存行中)

  • 缓存一致性是根据缓存行(Cache line)为单元来进行同步的,即缓存中的传输单元为缓存行,一个缓存行大小通常为64Byte;

  • 缓存行的内容一发生变化,就需要进行缓存同步;

  • 在3.1中,我们在寄存器用到的数据是同一个X,他们肯定是在同一个缓存行中的,这个是真实的共享数据的,共享的数据为X。

  • 而在3.2中,不同CPU的寄存器中用到了不同的变量,一个用到的是X,一个用到的是Y,但是变量X、Y在同一个缓存行中(一次读取64Byte,见3.1中的图),缓存一致性是根据缓存行为单元来进行同步的,所以虽然用到的不是同一个数据,但是他们(数据X和数据Y)在同一个缓存行中,他们的缓存同步也需要时间。


4.伪共享解决办法(缓存行填充或者使用@Contended注解)

4.1.缓存行填充

如章节一所示,我们可以在x变量前后进行缓存行的填充,:

public volatile long A,B,C,D,E,F,G;
public volatile long x = 1L;
public volatile long a,b,c,d,e,f,g;

添加后,3.2章节中的截图将会变成如下样子:


不论如何进行缓存行的划分,包括x在内的连续64Byte,也就是一个缓存行不可能存在变量Y,同样变量Y所在的缓存行不可能存在x,这样就不存在伪共享的情况,他们之间就不需要考虑缓存一致性问题了,也就节省了这一部分时间。

4.2.Contended注解

在Java 8中,提供了@sun.misc.Contended注解来避免伪共享,原理是在使用此注解的对象或字段的前后各增加128字节大小的padding,使用2倍于大多数硬件缓存行的大小来避免相邻扇区预取导致的伪共享冲突。我们目前的缓存行大小一般为64Byte,这里Contended注解为我们前后加上了128字节绰绰有余。 注意:如果想要@Contended注解起作用,需要在启动时添加JVM参数-XX:-RestrictContended 参数后 @sun.misc.Contended 注解才有。

然而在java11中@Contended注解被归类到模块java.base中的包jdk.internal.vm.annotation中,其中定义了Contended注解类型。笔者用的是java12,其注解如下:


加上该注解,如下,也能达到缓存行填充的效果


5.完整代码(利用缓存行填充和没用缓存行填充)

大家自己也可以跑一下如下代码,看利用缓存行填充后的神奇效果。

5.1没用缓存行填充代码如下:

package mesi;

import java.util.concurrent.CountDownLatch;

/**
* @Author: YuShiwen
* @Date: 2022/2/27 2:52 PM
* @Version: 1.0
*/

public class NoCacheLineFill {

   public volatile long x = 1L;
}

class MainDemo {

   public static void main(String[] args) throws InterruptedException {
       // CountDownLatch是在java1.5被引入的,它是通过一个计数器来实现的,计数器的初始值为线程的数量。
       // 每当一个线程完成了自己的任务后,调用countDown方法,计数器的值就会减1。
       // 当计数器值到达0时,它表示所有的线程已经完成了任务,然后调用await的线程就可以恢复执行任务了。
       CountDownLatch countDownLatch = new CountDownLatch(2);

       NoCacheLineFill[] arr = new NoCacheLineFill[2];
       arr[0] = new NoCacheLineFill();
       arr[1] = new NoCacheLineFill();

       Thread threadA = new Thread(() -> {
           for (long i = 0; i < 1_000_000_000L; i++) {
               arr[0].x = i;
          }
           countDownLatch.countDown();
      }, "ThreadA");

       Thread threadB = new Thread(() -> {
           for (long i = 0; i < 100_000_000L; i++) {
               arr[1].x = i;
          }
           countDownLatch.countDown();
      }, "ThreadB");

       final long start = System.nanoTime();
       threadA.start();
       threadB.start();
       //等待线程A、B执行完毕
       countDownLatch.await();
       final long end = System.nanoTime();
       System.out.println("耗时:" + (end - start) / 1_000_000 + "毫秒");

  }
}

5.2利用缓存行填充代码如下:

package mesi;

import java.util.concurrent.CountDownLatch;

/**
* @Author: YuShiwen
* @Date: 2022/2/27 3:45 PM
* @Version: 1.0
*/

public class UseCacheLineFill {

   public volatile long A, B, C, D, E, F, G;
   public volatile long x = 1L;
   public volatile long a, b, c, d, e, f, g;
}

class MainDemo01 {

   public static void main(String[] args) throws InterruptedException {
       // CountDownLatch是在java1.5被引入的,它是通过一个计数器来实现的,计数器的初始值为线程的数量。
       // 每当一个线程完成了自己的任务后,调用countDown方法,计数器的值就会减1。
       // 当计数器值到达0时,它表示所有的线程已经完成了任务,然后调用await的线程就可以恢复执行任务了。
       CountDownLatch countDownLatch = new CountDownLatch(2);

       UseCacheLineFill[] arr = new UseCacheLineFill[2];
       arr[0] = new UseCacheLineFill();
       arr[1] = new UseCacheLineFill();

       Thread threadA = new Thread(() -> {
           for (long i = 0; i < 1_000_000_000L; i++) {
               arr[0].x = i;
          }
           countDownLatch.countDown();
      }, "ThreadA");

       Thread threadB = new Thread(() -> {
           for (long i = 0; i < 1_000_000_000L; i++) {
               arr[1].x = i;
          }
           countDownLatch.countDown();
      }, "ThreadB");

       final long start = System.nanoTime();
       threadA.start();
       threadB.start();
       //等待线程A、B执行完毕
       countDownLatch.await();
       final long end = System.nanoTime();
       System.out.println("耗时:" + (end - start) / 1_000_000 + "毫秒");

  }
}

作者:YuShiwen
来源:https://juejin.cn/post/7083030159304949767

收起阅读 »

人人为我,我为人人——环信开发者“𠈌”计划邀你加入!

各位亲爱的环友们~环信技术社区及官方支持群自组建以来涌现了不少不分昼夜,互帮互助,无私帮助他人解决问题的热心网友,看似不经意的“顺手答一下”“刚好遇到过”,于被帮助的人都是雪中炭,暗室灯,绝渡舟的存在~~为鼓励这些默默发光的环友同时壮大帮帮团队伍,环信推出“𠈌...
继续阅读 »

各位亲爱的环友们~

环信技术社区及官方支持群自组建以来涌现了不少不分昼夜,互帮互助,无私帮助他人解决问题的热心网友,看似不经意的“顺手答一下”“刚好遇到过”,于被帮助的人都是雪中炭,暗室灯,绝渡舟的存在~~为鼓励这些默默发光的环友同时壮大帮帮团队伍,环信推出
“𠈌”计划。

“𠈌”计划以传递人人为我,我为人人的开发者互助精神为目标,将程序员自由开放和共享精神发扬光大。赠人玫瑰,手有余香,帮助他人沉淀自己的技术力量,现诚邀广大开发者积极加入!

包括但不限于在技术社区和官方支持群里解答IM集成及以外的所有开发问题。


从四月开始,每月底在本社区技术支持群(环信官方技术支持群1、2、3)分别选出3~5名当月积极帮助他人解决问题的网友(非环信员工)给予一定的福利奖励。
同时把Ta送上环信月度优秀群友墙~!颁发优秀环友徽章一枚,自本月起,累计上墙次数超过3次的环友们将拥有神秘的年度大奖~



 福利奖励标准 

IMGeek论坛:积极回复每月新发问题(集成问题,开发问题,bug解决等),帮助坛友解决问题——回帖总条数top5


技术支持群:受企业微信群统计功能限制,每月由以下各群内的环信支持小哥哥小姐姐们提名。



*以上暂为每月评选标准,评选方式后期会慢慢优化,以更客观数据为依据,贯彻公平公正的原则,坚持获选者0争议0质疑的宗旨,众望所归。
*环信员工不参与以上排名或提名。


 福利大礼包 

环信礼包:含环信定制周边、月优秀环友徽章、京东卡、其他随机盲盒




“每月优秀环友”在次月10日前揭晓并发放奖励。


如果您特别了解环信IM集成及相关问题解答,欢迎加入答疑方队。

想加入官方技术支持群的朋友,请联系环信冬冬通过审核后进群。



收起阅读 »

跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企业...
继续阅读 »
前言
跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

我们在开发flutter应用的时候编写代码,要么是同步代码,要么是异步代码。那么什么是同步什么是异步呢?

  • 同步代码就是正常编写的代码块
  • 异步代码就是Future,async等关键字修饰的代码块

一、时机不同

他们区别于运行时机不同,同步代码先执行,异步代码后执行,即使你的同步代码写在最后,那也是你的同步代码执行,之后运行你的异步代码。

二、机制不同

异步代码运行在 event loop中,类似于Android里的Looper机制,是一个死循环,event loop不断的从事件队列里取事件然后运行。

event loop循环机制

如图所示,事件存放于队列中,loop循环执行 运行图 Dart的事件循环如下图所示。循环中有两个队列。一个是微任务队列(MicroTask queue),一个是事件队列(Event queue)。 在这里插入图片描述 事件队列包含外部事件,例如I/O, Timer,绘制事件等等。 微任务队列则包含有Dart内部的微任务,主要是通过scheduleMicrotask来调度。

  1. 首先处理所有微任务队列里的微任务。
  2. 处理完所有微任务以后。从事件队列里取1个事件进行处理。
  3. 回到微任务队列继续循环。

Dart要先把所有的微任务处理完,再处理一个事件,处理完之后再看看微任务队列。如此循环。

例子:

8个微任务
2个事件

Dart-->执行完8个微任务
Dart-->执行完1个事件
Dart-->查看微任务队列
Dart-->再执行完1个事件
done

异步执行

那么在Dart中如何让你的代码异步执行呢?很简单,把要异步执行的代码放在微任务队列或者事件队列里就行了。

可以调用scheduleMicrotask来让代码以微任务的方式异步执行

    scheduleMicrotask((){
print('a microtask');
});

可以调用Timer.run来让代码以Event的方式异步执行

   Timer.run((){
print('a event');
});

Future异步执行

创建一个立刻在事件队列里运行的Future:

Future(() => print('立刻在Event queue中运行的Future'));

创建一个延时1秒在事件队列里运行的Future:

Future.delayed(const Duration(seconds:1), () => print('1秒后在Event queue中运行的Future'));

创建一个在微任务队列里运行的Future:

Future.microtask(() => print('在Microtask queue里运行的Future'));

创建一个同步运行的Future:

Future.sync(() => print('同步运行的Future'));

这里要注意一下,这个同步运行指的是构造Future的时候传入的函数是同步运行的,这个Future通过then串进来的回调函数是调度到微任务队列异步执行的。

有了Future之后, 通过调用then来把回调函数串起来,这样就解决了"回调地狱"的问题。

Future(()=> print('task'))
.then((_)=> print('callback1'))
.then((_)=> print('callback2'));

在task打印完毕以后,通过then串起来的回调函数会按照链接的顺序依次执行。 如果task执行出错怎么办?你可以通过catchError来链上一个错误处理函数:

 Future(()=> throw 'we have a problem')
.then((_)=> print('callback1'))
.then((_)=> print('callback2'))
.catchError((error)=>print('$error'));

上面这个Future执行时直接抛出一个异常,这个异常会被catchError捕捉到。类似于Java中的try/catch机制的catch代码块。运行后只会执行catchError里的代码。两个then中的代码都不会被执行。

既然有了类似Java的try/catch,那么Java中的finally也应该有吧。有的,那就是whenComplete:


Future(()=> throw 'we have a problem')
.then((_)=> print('callback1'))
.then((_)=> print('callback2'))
.catchError((error)=>print('$error'))
.whenComplete(()=> print('whenComplete'));

无论这个Future是正常执行完毕还是抛出异常,whenComplete都一定会被执行。

结果执行

把如上的代码在dart中运行看看输出

 print('1');
var fu1 = Future(() => print('立刻在Event queue中运行的Future'));
Future future2 = new Future((){
print("future2 初始化任务");
});
print('2');
Future.delayed(const Duration(seconds:1), () => print('1秒后在Event queue中运行的Future'));
print('3');
var fu2 = Future.microtask(() => print('在Microtask queue里运行的Future'));
print('4');
Future.sync(() => print('同步运行的Future')).then((value) => print('then同步运行的Future'));
print('5');
fu1.then((value) => print('then 立刻在Event queue中运行的Future'));
print('6');
fu2.then((value) => print('then 在Microtask queue里运行的Future'));
print('7');
Future(()=> throw 'we have a problem')
.then((_)=> print('callback1'))
.then((_)=> print('callback2'))
.catchError((error)=>print('$error'));
print('8');
Future(()=> throw 'we have a problem')
.then((_)=> print('callback1'))
.then((_)=> print('callback2'))
.catchError((error)=>print('$error'))
.whenComplete(()=> print('whenComplete'));
print('9');
Future future4 = Future.value("立即执行").then((value){
print("future4 执行then");
}).whenComplete((){
print("future4 执行whenComplete");
});
print('10');


future2.then((_) {
print("future2 执行then");
future4.then((_){
print("future4 执行then2");
});

});

输出

I/flutter (29040): 1
I/flutter (29040): 2
I/flutter (29040): 3
I/flutter (29040): 4
I/flutter (29040): 同步运行的Future
I/flutter (29040): 5
I/flutter (29040): 6
I/flutter (29040): 7
I/flutter (29040): 8
I/flutter (29040): 9
I/flutter (29040): 10
I/flutter (29040): 在Microtask queue里运行的Future
I/flutter (29040): thenMicrotask queue里运行的Future
I/flutter (29040): then同步运行的Future
I/flutter (29040): future4 执行then
I/flutter (29040): future4 执行whenComplete
I/flutter (29040): 立刻在Event queue中运行的Future
I/flutter (29040): then 立刻在Event queue中运行的Future
I/flutter (29040): future2 初始化任务
I/flutter (29040): future2 执行then
I/flutter (29040): future4 执行then2
I/flutter (29040): we have a problem
I/flutter (29040): we have a problem
I/flutter (29040): whenComplete
I/flutter (29040): 1秒后在Event queue中运行的Future

输出说明:

  • 先输出同步代码,再输出异步代码
  • 通过then串联起的任务会在主要任务执行完立即执行
  • Future.sync是同步执行,then执行在微任务队列中
  • 通过Future.value()函数创建的任务是立即执行的
  • 如果是在whenComplete之后注册的then,那么这个then的任务将放在microtask执行

Completer

Completer允许你做某个异步事情的时候,调用c.complete(value)方法来传入最后要返回的值。最后通过c.future的返回值来得到结果,(注意:宣告完成的complete和completeError方法只能调用一次,不然会报错)。 例子:

test() async {
Completer c = new Completer();
for (var i = 0; i < 1000; i++) {
if (i == 900 && c.isCompleted == false) {
c.completeError('error in $i');
}
if (i == 800 && c.isCompleted == false) {
c.complete('complete in $i');
}
}

try {
String res = await c.future;
print(res); //得到complete传入的返回值 'complete in 800'
} catch (e) {
print(e);//捕获completeError返回的错误
}
}


收起阅读 »

跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate

前言 跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企...
继续阅读 »

前言


跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

Dart是单线程的,Dart提供了Isolate,isolate提供了多线程的能力。但作为多线程能力的,却内存不能共享。但同样的内存不能共享,那么就不存在锁竞争问题。


举个例子来展示作用


如果一段代码执行事件很长,flutter如何开发。
基本页面代码(一段代码)


ElevatedButton(
child: Text("登录"),
onPressed: () {
执行运行代码();
}

延时代码块


String work(int value){
print("work start");
sleep(Duration(seconds:value));
print("work end");
return "work complete:$value";
}

第一种:直接执行运行代码(延时5秒)


  执行运行代码() {
work(5);
}

结果:
5秒卡的死死的


第二种:async执行运行代码(延时5秒)


  执行运行代码() async{
work(5);
}

结果:
5秒依旧卡的死死的


------------------------------------------------我是分割线--------------------------------------------------



why?在dart中,async不是异步计算么?(循环机制下篇讲)因为我们仍旧是在同一个UI线程中做运算,异步只是说我可以先运行其他的,等我这边有结果再返回,但是,我们的计算仍旧是在这个UI线程,仍会阻塞UI的刷新,异步只是在同一个线程的并发操作。



第三种:ioslate执行运行代码(延时5秒)



但是由于dart中的Isolate比较重量级,UI线程和Isolate中的数据的传输比较复杂,因此flutter为了简化用户代码,在foundation库中封装了一个轻量级compute操作。



  执行运行代码() async{
var result = await compute(work, 5);
print(result);
}

结果:
居然不卡顿了


使用说明



compute的使用还是有些限制,它没有办法多次返回结果,也没有办法持续性的传值计算,每次调用,相当于新建一个隔离,如果调用过多的话反而会适得其反。我们需要根据不同的业务选择用compute和isolate




Future work(int value) async{
//接收消息管道
ReceivePort rp = new ReceivePort();
//发送消息管道
SendPort port = rp.sendPort;
Isolate isolate = await Isolate.spawn(workEvent, port);
//发送消息管道2
final sendPort2 = await rp.first;
//返回应答数据
final answer = ReceivePort();
sendPort2.send([answer.sendPort, value]);
return answer.first;
}

void workEvent(SendPort port) {
//接收消息管道2
final rPort = ReceivePort();
SendPort port2 = rPort.sendPort;
// 将新isolate中创建的SendPort发送到主isolate中用于通信
port.send(port2);

rPort.listen((message) {
final send = message[0] as SendPort;
send.send(work(5));
});
}

基本方法


    //恢复 isolate 的使用
isolate.resume(isolate.pauseCapability);

//暂停 isolate 的使用
isolate.pause(isolate.pauseCapability);

//结束 isolate 的使用
isolate.kill(priority: Isolate.immediate);

//赋值为空 便于内存及时回收
isolate = null;


两个进程都双向绑定了消息通信的通道,即使新的Isolate中的任务完成了,它的进程也不会立刻退出,因此,当使用完自己创建的Isolate后,最好调用isolate.kill(priority: Isolate.immediate);将Isolate立即杀死。



用Future还是isolate?


future使用场景:



  • 代码段可以独立运行而不会影响应用程序的流畅性


isolate使用场景:



  • 繁重的处理可能要花一些时间才能完成

  • 网络加载大图

  • 图片处理
收起阅读 »

跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企业...
继续阅读 »

前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

与java&kotlin不同的是,dart中有一个特殊的关键字mixin(mix-in),用这个关键字的类被其他类(包含)的时候,其他类就拥有了该类的方法。这样代码不通过继承(extend)就可以重用。


场景来展示mixin如何使用


由于在java&kotlin中经常性的用extent & implements 并不知道mixin是如何使用,那么我举几个特殊的例子来帮助大家理解


场景用例


在这里插入图片描述
如上uml图所示
鸟作为父类,鸟必备的技能为(下蛋和走路),而作为其子类的大雁和麻雀可以飞行,企鹅却不能飞行。
那么飞行却成为个别鸟类的技能,如果在父类中定义实现飞,那在企鹅中就多了个空实现。如果定义一个接口实现飞,那么在能飞的鸟类中就必须都要重新编写飞的代码。如何让这一切变得容易呢。
那么我们用混入(with)来实现如下代码:


abstract class Bird{

void walk() { print('我会走路'); }
void xiadan() { print('我会下蛋'); }
}

abstract class Fly{
void fly() { print('我会飞'); }
}

//大雁

class Dayan extends Bird with Fly {}

//企鹅

class Qier extends Bird {}

如果 Fly 类 不希望作为常规类被使用,使用关键字 mixin 替换 class 。


mixin Fly{
void fly() { print('我会飞'); }
}

如果 Fly 类 只希望限定于鸟类去使用,那么需要加入如下关键字


mixin Fly on Bird{
void fly() { print('我会飞'); }
}

mixin特点



  1. mixin 没有构造函数,不能被实例化

  2. 可以当做接口使用,class 混入之后需要实现

  3. 可以使用on 指定混入的类类型,如果不是报错。

  4. 如果with后的多个类中有相同的方法,如果当前使用类重写了该方法,就会调用当前类中的方法。如果当前使用类没有重写了该方法,则会调用距离with关键字最远类中的方法。


调用顺序展示


简单顺序调用


如果with后的多个类中有相同的方法,如果当前使用类重写了该方法,就会调用当前类中的方法。如果当前使用类没有重写了该方法,则会调用距离with关键字最远类中的方法。


abstract class First {
void doPrint() {
print('First');
}
}

abstract class Second {
void doPrint() {
print('Second');
}
}

class Father {
void doPrint() {
print('Father');
}
}

class Son extends Father with First,Second {

}

调用:


	Son son = Son();
son.doPrint();

打印:


Second

重写后调用


class Son extends Father with First,Second {
void doPrint() {
print('Son');
}
}

调用:


	Son son = Son();
son.doPrint();

打印:


Son

带有父类方法调用


class Father {
void init() {
print('Father init');
}
}
mixin FirstMixin on Father {
void init() {
print('FirstMixin init start');
super.init();
print('FirstMixin init end');
}
}

mixin SecondMixin on Father {
void init() {
print('SecondMixin init start');
super.init();
print('SecondMixin init end');
}
}


class Son extends Father with FirstMixin, SecondMixin {

@override
void init() {
print('Son init start');
super.init();
print('Son init end');
}
}

调用:


  Son().init();

打印:


flutter: Son init start
flutter: SecondMixin init start
flutter: FirstMixin init start
flutter: Father init
flutter: FirstMixin init end
flutter: SecondMixin init end
flutter: Son init end

说明






















方式类型说明
withmixin混入该类内容
with onmixin混入该类内容,但必须是特点的类型

特别注意


mixin 可以on多个类,但with时候之前的类必须已经有相关的实现


mixin Mix on Mix1,Mix2{ }
收起阅读 »

Java好用的时间类,别在用Date了

前言假设你想获取当前时间,那么你肯定看过这样的代码public static void main(String[] args) { Date date = new Date(System.currentTimeMillis()); Syste...
继续阅读 »

前言

假设你想获取当前时间,那么你肯定看过这样的代码

public static void main(String[] args) {

Date date = new Date(System.currentTimeMillis());
System.out.println(date.getYear());
System.out.println(date.getMonth());
System.out.println(date.getDate());
}

获取年份,获取月份,获取..日期?
运行一下

121
9
27

怎么回事?获取年份,日期怎么都不对,点开源码发现

/**
* Returns a value that is the result of subtracting 1900 from the
* year that contains or begins with the instant in time represented
* by this <code>Date</code> object, as interpreted in the local
* time zone.
*
* @return the year represented by this date, minus 1900.
* @see java.util.Calendar
* @deprecated As of JDK version 1.1,
* replaced by <code>Calendar.get(Calendar.YEAR) - 1900</code>.
*/
@Deprecated
public int getYear() {
return normalize().getYear() - 1900;
}

原来是某个对象值 减去了 1900,注释也表示,返回值减去了1900,难道我们每次获取年份需要在 加上1900?注释也说明了让我们 用Calendar.get()替换,并且该方法已经被废弃了。点开getMonth()也是一样,返回了一个0到11的值。getDate()获取日期?不应该是getDay()吗?老外的day都是sunday、monday,getDate()才是获取日期。再注意到这些api都是在1.1的时候被废弃了,私以为是为了消除getYear减去1900等这些歧义。收~

Calendar 日历类

public static void main(String[] args) {

Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int dom = calendar.get(Calendar.DAY_OF_MONTH);
int doy = calendar.get(Calendar.DAY_OF_YEAR);
int dow = calendar.get(Calendar.DAY_OF_WEEK);
int dowim = calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH);
System.out.println(year+"年"+ month+"月");
System.out.println(dom+"日");
System.out.println(doy+"日");
System.out.println(dow+"日");
System.out.println(dowim);
}

打印(运行时间2021年10月27日 星期三 晴)

2021年9月
27日
300日
4日
4

问:月份怎么是上个月的?
答:是为了计算方便,月是0到11之间的值。
问:计算方便?
答:比如月份从1月开始,增加一个月,12月+1=13,没有13月。假设取余,(12+1)=1 正好为1月,那11月增加一个月,(11+1)=0,这就有问题了。所以为了计算方便1月,返回了0值。date.getMonth()也是一个道理。 问:那下面的DAY_OF_XXX 又是什么意思?
答:猜!根据结果猜。
Calendar.DAY_OF_MONTH 在这个月 的这一天
Calendar.DAY_OF_YEAR 在这一年 的这一天
Calendar.DAY_OF_WEEK 在这一周 的这一天
Calendar.DAY_OF_WEEK_IN_MONTH 在这一个月 这一天在 第几周
到这里 Calendar.DAY_OF_WEEK 为什么是 4 ,你肯定也猜到了
Calendar.HOUR
Calendar.HOUR_OF_DAY
Calendar.SECOND
...其他的 你肯定也会用了

LocalDate 本地日期类

LocalDate localDate = LocalDate.now();
System.out.println("当前日期:"+localDate.getYear()+" 年 "+localDate.getMonthValue()+" 月 "+localDate.getDayOfMonth()+"日" );

//结果
当前日期:2021 年 10 月 27日

也可以通过 LocalDate.of(年,月,日)去构造

LocalDate pluslocalDate = localDate.plusDays(1);//增加一天
LocalDate pluslocalDate = localDate.plusYears(1);//增加一年

其他api

LocalDate.isBefore(LocalDate);
LocalDate.isAfter();
LocalDate.isEqual();

也就是对两个日期的判断,是在前、在后、或者相等。

LocalTime 本地时间类

LocalTime localTime = LocalTime.now();
System.out.println("当前时间:"+localTime.getHour()+"h "+localTime.getSecond()+"m "+localTime.getMinute()+"s" );

LocalDate和LocalTime 都有类似作用的api
LocalDate.plusDays(1) 增加一天
LocalTime.plusHours(1) 增加一小时 等等~
其他api

LocalTime.isBefore(LocalTime);
LocalTime.isAfter();

对两个时间的判断。肯定碰到过一个需求,今天离活动开始时间还剩多少天。

LocalDateTime 本地日期时间类

public final class LocalDateTime ...{

private final LocalDate date;

private final LocalTime time;
}

LocalDateTime = LocalDate + LocalTime 懂的都懂

Instant 类

Instant 是瞬间,某一时刻的意思

Instant.ofEpochMilli(System.currentTimeMillis())
Instant.now()

通过Instant可以创建一个 “瞬间” 对象,ofEpochMilli()可以接受某一个“瞬间”,比如当前时间,或者是过去、将来的一个时间。
比如,通过一个“瞬间”创建一个LocalDateTime对象

LocalDateTime now = LocalDateTime.ofInstant(
Instant.ofEpochMilli(System.currentTimeMillis()),ZoneId.systemDefault());

System.out.println("当前日期:"+now.getYear()+" 年 "+now.getMonthValue()+" 月 "+now.getDayOfMonth()+"日" )

Period 类

Period 是 时期,一段时间 的意思
Period有个between方法专门比较两个 日期 的

LocalDate startDate = LocalDateTime.ofInstant(
Instant.ofEpochMilli(1601175465000L), ZoneId.systemDefault()).toLocalDate();//1601175465000是2020-9-27 10:57:45
Period p = Period.between(startDate, LocalDate.now());

System.out.println("目标日期距离今天的时间差:"+p.getYears()+" 年 "+p.getMonths()+" 个月 "+p.getDays()+" 天" );

//目标日期距离今天的时间差:1 年 1 个月 1 天

看一眼源码

public static Period between(LocalDate startDateInclusive, LocalDate endDateExclusive) {
return startDateInclusive.until(endDateExclusive);
}

public Period until(ChronoLocalDate endDateExclusive) {
LocalDate end = LocalDate.from(endDateExclusive);
long totalMonths = end.getProlepticMonth() - this.getProlepticMonth(); // safe
int days = end.day - this.day;
if (totalMonths > 0 && days < 0) {
totalMonths--;
LocalDate calcDate = this.plusMonths(totalMonths);
days = (int) (end.toEpochDay() - calcDate.toEpochDay()); // safe
} else if (totalMonths < 0 && days > 0) {
totalMonths++;
days -= end.lengthOfMonth();
}
long years = totalMonths / 12; // safe
int months = (int) (totalMonths % 12); // safe
return Period.of(Math.toIntExact(years), months, days);
}

他只接受两个LocalDate对象,对时间的计算,算好之后返回Period对象

Duration 类

Duration 是 期间 持续时间 的意思 上代码

LocalDateTime end = LocalDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneId.systemDefault());
LocalDateTime start = LocalDateTime.ofInstant(Instant.ofEpochMilli(1601175465000L), ZoneId.systemDefault());
Duration duration = Duration.between(start, end);

System.out.println("开始时间到结束时间,持续了"+duration.toDays()+"天");
System.out.println("开始时间到结束时间,持续了"+duration.toHours()+"小时");
System.out.println("开始时间到结束时间,持续了"+duration.toMillis()/1000+"秒");

可以看到between也接受两个参数,LocalDateTime对象,源码是对两个时间的计算,并返回对象。

对象转换

再贴点api

//long -> LocalDateTime
LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault())

//String -> LocalDateTime
DateTimeFormatter dateTimeFormatter1 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime.parse("2021-10-28 00:00:00", dateTimeFormatter1);

//LocalDateTime -> long
LocalDateTime对象.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();

//LocalDateTime -> String
DateTimeFormatter dateTimeFormatter1 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime对象.format(dateTimeFormatter1)

对象转换几乎都涵盖了,里面有个时区对象,这个一般用默认时区。

总结

用LocalDate、LocalTime、LocalDateTime代替了Date类。Date管日期,Time管时间
LocalDateTime = LocalDate + LocalTime
Period 只能用LocalDate
Duration 持续时间,所以LocalDate、LocalTime、LocalDateTime 都能处理
至于Calendar 日历类,这里面的api,都是针对日历的,比如这个月的第一天是星期几。
总体来说,都是api的使用,非常清晰,废弃date.getMonth()等,使用localDate.getMonthValue()来获取几月,更易理解,更易贴合使用。代码都贴在了github上了


作者:回眸婉约
链接:https://juejin.cn/post/7024389549652443172
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

飞机上一般是什么操作系统?安全不 ?

首先,请大家为“3·21”东航MU5735坠机事故,默哀1分钟,再接着看本文 !来正文 。。。科普文 !航空软件其实并不神秘,从计算机架构上来说,同普通的计算机系统并无太大区别,都是由处理器、总线、I/O设备、存储设备、网络设备、通讯设备、操作系统和应用软件所...
继续阅读 »

首先,请大家为“3·21”东航MU5735坠机事故,默哀1分钟,再接着看本文 !


来正文 。。。科普文 !

航空软件其实并不神秘,从计算机架构上来说,同普通的计算机系统并无太大区别,都是由处理器、总线、I/O设备、存储设备、网络设备、通讯设备、操作系统和应用软件所构成的。仅仅是为了满足很高指标的可靠性、健壮性和实时性,而采用了另一套东西而已。

1、波音-787、AH-64用的操作系统是VxWorks

VxWorks官网http://www.windriver.com/products/vxworks/

2、B-2、F-16、F-22、F-35、空客-380使用的操作系统均是Integrity-178B


Integrity-178B官网https://www.ghs.com/products/safety_critical/integrity-do-178b.html

类似波音-787、空客-380、空客-350内部设备之间是使用以太网的一种变体来互联的,叫AFDX,在应用软件这一层,同普通的以太网程序没有任何区别。扩展:10个关键词,了解MU5735搜寻最新进展

3、过去这些设备经常使用ADA语言来编写,现在为了降低成本,在F-35项目上已经改为使用C++了


F-35项目的C++编程规范http://www.stroustrup.com/JSF-AV-rules.pdf

F-35的微处理器是PowerPC指令集的,为了保证可靠性,采用的编译器也是同普通的编译器不太一样。编译器也是有可能出现bug的,为了保障源代码同编译出来的目标代码完全一致,避免编译器的bug造成问题,在JSF项目内部的软件开发中,经常使用CompCert编译器。这个编译器只能编译C99,但是可靠性极高。扩展:远程控制系统

要知道,近几年全球范围内飞机失事发生的次数不少。据不完全统计,每年全球大约有4000万次的飞机起落,而我国的飞机失事率一直处于非常低的水平。此前中国已经连续12年没有发生过重大民航事故了,而上一次坠机事故还是发生在2010年8月24日,河南航空的伊春空难,当时坠毁的机型为ERJ-190。另外,搜索公众号Java架构师技术后台回复“Spring”,获取一份惊喜礼包。

截至目前,东航坠机已经过去24小时了。总体来说,无论大家讨论什么因素导致的,都不具有肯定性的说法,包括为什么急速骤降,最后垂直坠落,飞机本身有没有问题,是不是操作系统出了故障,有没有遭遇极端天气影响等等,这一切都是属于未知数。

任何空难发生都是悲剧的,事故真实原因还需要等待官方调查结论、依靠黑匣子等来解开谜团。


参考来源:

1. VxWorks官方网站

http://www.windriver.com/products/vxworks/

2. Integrity-178B的官方网站

https://www.ghs.com/products/safety_critical/integrity-do-178b.html

3. 《F-35项目的C++编程规范》PDF

http://www.stroustrup.com/JSF-AV-rules.pdf

来源:科技曼

收起阅读 »

Flutter 与原生通信的三种方式

Flutter 与原生之间的通信依赖灵活的消息传递方式 应用的Flutter部分通过平台通道(platform channel)将消息发送到其应用程序的所在的宿主(iOS或Android)应用(原生应用) 宿主监听平台通道,并接收该消息。然后它会调用该...
继续阅读 »

Flutter 与原生之间的通信依赖灵活的消息传递方式




  • 应用的Flutter部分通过平台通道(platform channel)将消息发送到其应用程序的所在的宿主(iOS或Android)应用(原生应用)




  • 宿主监听平台通道,并接收该消息。然后它会调用该平台的 API,并将响应发送回客户端,即应用程序的 Flutter 部分




Flutter 与原生存在三种交互方式




  • MethodChannel:用于传递方法调用(method invocation)通常用来调用 native 中某个方法




  • BasicMessageChannel:用于传递字符串和半结构化的信息,这个用的比较少




  • EventChannel:用于数据流(event streams)的通信。有监听功能,比如电量变化之后直接推送数据给flutter端




三种 Channel 之间互相独立,各有用途,但它们在设计上却非常相近。每种 Channel 均有三个重要成员变量:




  • name: String类型,代表 Channel 的名字,也是其唯一标识符




  • messager:BinaryMessenger 类型,代表消息信使,是消息的发送与接收的工具




  • codec: MessageCodec 类型或 MethodCodec 类型,代表消息的编解码器




具体使用



  • 首先分别创建 Native 工程和 Flutter Module。我这里是以 iOS 端和 Flutter 通信为例,创建完 iOS 工程后,需要通过 CocoaPods 管理 Flutter Module。


截屏2021-11-27 下午3.09.28.png



  • 然后在 iOS 工程里面创建 Podfile ,然后引入 Flutter Module ,具体代码如下:


platform :ios,'11.0'
inhibit_all_warnings!

#flutter module 文件路径
flutter_application_path = '../flutter_module'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'Native_iOS' do

install_all_flutter_pods(flutter_application_path)

end

注意: flutter_application_path 这个是 Flutter 工程的路径,我是原生项目和 Flutter在一个目录下



  • 最后在终端 pod install 一下,看是否能正常引入 Flutter Module。这样就可以在iOS工程里面导入#import <Flutter/Flutter.h>

一、MethodChannel的使用


这里写的代码实现了以下功能


1.实现了点击原生页面的按钮跳转到 Flutter 页面,在 Flutter 点击返回按钮能正常返回原生页面


2.实现在Flutter页面点击当前电量,从原生界面传值到 Flutter 页面


原生端代码


@property (nonatomic, strong)FlutterEngine *flutterEngine;

@property (nonatomic, strong)FlutterViewController *flutterVC;

@property (nonatomic, strong)FlutterMethodChannel *methodChannel;

- (void)viewDidLoad {
    [super viewDidLoad];

   //隐藏了原生的导航栏
    self.navigationController.navigationBarHidden = YES;

    UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 80, 80)];
    btn.backgroundColor = [UIColor redColor];
    [btn addTarget:self action: @selector(onBtnClick) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn];

    self.flutterVC = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil];
//创建channel
    self.methodChannel = [FlutterMethodChannel methodChannelWithName:@"methodChannel" binaryMessenger:self.flutterVC.binaryMessenger];

}

- (void)onBtnClick {

    //告诉Flutter对应的页面
//Method--方法名称,arguments--参数
    [self.methodChannel invokeMethod:@"EnterFlutter" arguments:@""];

//push进入Flutter页面
    [self.navigationController pushViewController:self.flutterVC animated:YES];

    __weak __typeof(self) weakSelf = self;
//监听Flutter发来的事件
    [self.methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
//响应从Flutter页面发送来的方法
        if ([call.method isEqualToString:@"exit"]) {
            [weakSelf.flutterVC.navigationController popViewControllerAnimated:YES];
        } else if ([call.method isEqualToString:@"getBatteryLevel"]) {
//传值回Flutter页面
            [weakSelf.methodChannel invokeMethod:@"BatteryLevel" arguments:@"60%"];
        }
    }];
}

//创建引擎,真正在项目中,引擎可以定义为一个单例。这样处理防止在原生里面存在多引擎,是非常占有内存的
- (FlutterEngine *)flutterEngine {
    if (!_flutterEngine) {
        FlutterEngine * engine = [[FlutterEngine alloc] initWithName:@"flutterEngin"];
        if (engine.run) {
            _flutterEngine = engine;
        }
    }
    return _flutterEngine;
}

Flutter 端代码


class _MyHomePageState extends State<MyHomePage> {

String batteryLevel = '0%';
//定义通道
final MethodChannel _methodhannel =
const MethodChannel('com.pages.your/native_get');

@override
void initState() {
super.initState();

//Flutter端监听发送过来的数据
_methodhannel.setMethodCallHandler((call) {
if (call.method == 'EnterFlutter') {
print(call.arguments);
} else if (call.method == 'BatteryLevel') {
batteryLevel = call.arguments;
}
setState(() {});
return Future(() {});
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
children: [
ElevatedButton(
onPressed: () {
//发送消息给原生
_methodhannel.invokeListMethod('exit');
},
child: Text('返回'),
),
ElevatedButton(
onPressed: () {
//发送消息给原生
_oneChannel.invokeListMethod('getBatteryLevel');
},
child: Text('当前电量${batteryLevel}'),
),
],
),
),
);
}
}

二、BasicMessageChannel的使用


它是可以双端通信的,Flutter 端可以给 iOS 发送消息,iOS 也可以给 Flutter 发送消息。这段代码实现了在 Flutter 中的 TextField 输入文字,在 iOS 端能及时输出。


原生端代码


需要在上面代码的基础上增加 MessageChannel ,并接收消息和发送消息


@property (nonatomic, strong) FlutterBasicMessageChannel *messageChannel;

self.messageChannel = [FlutterBasicMessageChannel messageChannelWithName:@"messgaeChannel" binaryMessenger:self.flutterVC.binaryMessenger];

[self.messageChannel setMessageHandler:^(id _Nullable message, FlutterReply  _Nonnull callback) {

        NSLog(@"收到Flutter的:%@",message);
    }];

Flutter 端代码


//需要创建和iOS端相同名称的通道
final messageChannel =
const BasicMessageChannel("messgaeChannel", StandardMessageCodec());

监听消息


messageChannel.setMessageHandler((message) {
print('收到来自iOS的$message');
return Future(() {});
});

发送消息


messageChannel.send(str);

三、EventChannel的使用


只能是原生发送消息给 Flutter 端,例如监听手机电量变化,网络变化,传感器等。


我这里在原生端实现了一个定时器,每隔一秒发送一个消息给 Flutter 端,模仿这个功能。


原生端代码


记得所在的类要实现这个协议 FlutterStreamHandler


//定义属性
//通道
@property (nonatomic, strong) FlutterEventChannel *eventChannel;
//事件回调
@property (nonatomic, copy) FlutterEventSink events;
//用于计数
@property (nonatomic, assign) NSInteger count;

//初始化通道
self.eventChannel = [FlutterEventChannel eventChannelWithName:@"eventChannel" binaryMessenger:self.flutterVC.binaryMessenger];

[self.eventChannel setStreamHandler:self];

//调用创建定时器
[self createTimer];

//创建定时器
- (void)createTimer {

    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector: @selector(timeStart) userInfo:nil repeats:YES];
}

//发送消息
- (void)timeStart{

    self.count += 1;
    NSDictionary *dic = [NSDictionary dictionaryWithObject:@(self.count) forKey:@"count"];
    if (self.events != nil) {
        self.events(dic);
    }
}

//代表通道已经建好,原生端可以发送数据了
- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(FlutterEventSink)eventSink {

    self.events = eventSink;
    return nil;
}

//代表Flutter端不再接收
- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments {

    self.events = nil;
    return nil;
}

Flutter 端代码


//创建通道
final EventChannel eventChannel = const EventChannel('eventChannel');

//开始监听数据
eventChannel.receiveBroadcastStream().listen((event) {
print(event.toString());
});

以上就是iOS原生和Flutter通信的三种方式,消息传递是异步的,这确保了用户界面在消息传递时不会被挂起。


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

在外企工作真的爽吗?

从参加工作至今,我觉得,我做过最幸运的事情包括下面三个:在IT圈混的人都知道,做程序员,在一家公司能干够5年,绝对算的上老员工。如果还不跳槽,很多人会觉得你要么缺乏实力,要么没有奋斗精神。因此,在我加入外企的第5个年头,我也动了换工作的心思。而在这之后,照顾家...
继续阅读 »
最近互联网行业裁员的消息,把大家搞的忧心忡忡。对于在换工作或找工作的小伙伴,犹豫是否要到互联网公司了。剩下就是外企、国企和传统行业了,最近外企因为work life balance的口碑比较好,很多小伙伴都挺憧憬的,那么外企到底怎么样呢?下面给大家精选了一下网友在外企的体验文章,给大家了解一下过来人的体会。
我是一名程序员,从30岁加入外企,至今已经11年。
从参加工作至今,我觉得,我做过最幸运的事情包括下面三个:
  1. 娶了现在的老婆。
  2. 房价暴涨之前买了房。
  3. 30岁之后进入外企。
在IT圈混的人都知道,做程序员,在一家公司能干够5年,绝对算的上老员工。如果还不跳槽,很多人会觉得你要么缺乏实力,要么没有奋斗精神。因此,在我加入外企的第5个年头,我也动了换工作的心思。
面试一圈下来,也收到了几个不错的offer。于是也下定了离开的念头,连离职报告都写好了,就差推开领导的门递上去了。而这时,家里突然有人生病,需要我照顾,也打断了我换工作的计划。
而在这之后,照顾家人的这段时间。它彻底改变我对工作,对人生的看法。
因为家人生病的急,没有在第一时间来的及请假。事情处理后,才在短信里给领导请了假,但心里是忐忑的。因为,觉得领导可能会为难,毕竟公司也不是他家开的。但没想到他很快就回复了我:
“照顾好家人,工作的事你就不要担心了。另外,注意休息,保重身体!”
当时读了,我一个大男人,心里暖暖的,鼻子甚至有点酸楚。
通过这件事,使我真切的感受到这是一个充满温情的公司。这种温情,在人生的特殊时刻,至关重要。
如果我在一家制度严苛,领导不近人情的公司。家人生病的这段时间,估计我很可能要被迫离职了。这样,在我人生最灰暗,经济压力最大的时间点上。我反而丢了工作,没了收入。也许,生活一下子就把我彻底击倒了。
而从那之后的5年。随着时间的推移,我身体的变化,我愈加的觉出,我当时没离开这家公司是多么正确的一个决定。
人生特别荒谬的一点是,当你30岁精力旺盛的时候。你既无法想象,也无法理解一个40岁油腻中年男,对家庭和健康的感受。于是,你一边鄙视着别人稳定的生活,一边高呼着奋斗的口号要去外边闯荡,去赢取别人眼中的财务自由。
但眨眼间,等你真到40岁,期待的财务自由并没有到来。但精力已大不如前,健康问题此起彼伏的时候,就会刻骨的体会到什么叫做人生的无力感。但是,你却已经没了退路。
人到40,当你再也加不动班,却也找不到一个温情公司收留的时候,你怎么办?这个时候,你终于认识到了公务员、国企、外企的好处,但你却没有了机会。
于是,在人生最艰难的节骨点,你被吊在了哪里,没有了出路。
但这能怪谁呢?人总不能在所有的时候,把所有的好处都占了吧,这并不公平!
今年我41岁,按部就班的在公司上班, 也常被人嘲笑没出息,缺乏“奋斗”的激情。但这世界上,是谁规定“奋斗”的人生就更高尚呢?
对我来说,理想生活就是现在这样:
  1. 没有996。
  2. 一年20天年假。
  3. 家庭和生活完美平衡。
  4. 工资虽然没那么高,但也够有尊严的生活。
  5. 同事之间和和气气,互相尊重。
  6. ...
至于好不好,不同的年龄,不同的经历,自会有不同的感受。
“如人饮水,冷暖自知”。
作者:铅刀一割、沈世钧
来源:https://www.zhihu.com/question/299766610
该死的知乎,不知道为啥非要给我推荐这个帖子“在外企工作爽吗?”,下面这个回答看起来更刺激。

爽。
上周刚刚从前东家离职。
离职原因不是我跳槽,而是集团我们的业务条线退出中国,被迫离职。
这是我进的第一家外企,也坚定了我之后(大概率)只在外企找活的信念。
首先上一个我的工作台位镇楼。
200多平的办公室,只有5个员工。

我司隶属一家欧洲集团,我所在的子公司从事非常小众的环境权益交易。
这在欧美已经是成熟产业,但在国内还处于起步阶段。
加之持有这些资产的多为能源国企(涉及国有资产流失等口径问题),所以总部也是保持“试水”的态度设立了我司。
公司地处使馆区,与集团另外一个规模较大的兄弟公司在同一层。
楼下一众网红咖啡馆、餐厅,交通十分便利。
每天上下班不打卡,大家基本是10点左右陆续到,6点左右陆续走。
但你有事,三点走也没人管你。
进公司就是15天年假(社会工龄十年以上则是20天),每多呆一年,增加一天年假,封顶25天。
如果生病了,和老板说一下就好,所以我从来没有关心过自己有几天病假……
我有个同事妈妈得了癌症,有一阵子天天3点左右就走了,大家也都十分理解(当然他不久之后陪家里人出国治病了,也就直接离职了)。
另外一个同事自己生病,整整一年时间没有来上班(名义上是在家办公,其实她的活都是亚太同事分摊了),领着全额工资,住的单人病房也是公司的商业保险全包的。
薪水一般。
和我bat的同学比……人活着何必给自己找不痛快?
但站在性价比角度来看还行。
来源:https://mp.weixin.qq.com/s/fAW1V9ZJNBHWX0M4vONC 4A
收起阅读 »

优秀的后端应该有哪些开发习惯?

前言毕业快三年了,前后也待过几家公司,碰到各种各样的同事。见识过各种各样的代码,优秀的、垃圾的、不堪入目的、看了想跑路的等等,所以这篇文章记录一下一个优秀的后端 Java 开发应该有哪些好的开发习惯。拆分合理的目录结构受传统的 MVC 模式影响,传统做法大多是...
继续阅读 »

前言

毕业快三年了,前后也待过几家公司,碰到各种各样的同事。见识过各种各样的代码,优秀的、垃圾的、不堪入目的、看了想跑路的等等,所以这篇文章记录一下一个优秀的后端 Java 开发应该有哪些好的开发习惯。

拆分合理的目录结构

受传统的 MVC 模式影响,传统做法大多是几个固定的文件夹 controller、service、mapper、entity,然后无限制添加,到最后你就会发现一个 service 文件夹下面有几十上百个 Service 类,根本没法分清业务模块。正确的做法是在写 service 上层新建一个 modules 文件夹,在 moudles 文件夹下根据不同业务建立不同的包,在这些包下面写具体的 service、controller、entity、enums 包或者继续拆分。



等以后开发版本迭代,如果某个包可以继续拆领域就继续往下拆,可以很清楚的一览项目业务模块。后续拆微服务也简单。

封装方法形参

当你的方法形参过多时请封装一个对象出来...... 下面是一个反面教材,谁特么教你这样写代码的!

public void updateCustomerDeviceAndInstallInfo(long customerId, String channelKey,
                  String androidId, String imei, String gaId,
                  String gcmPushToken, String instanceId) {}

写个对象出来

public class CustomerDeviceRequest {
  private Long customerId;
  //省略属性......
}

为什么要这么写?比如你这方法是用来查询的,万一以后加个查询条件是不是要修改方法?每次加每次都要改方法参数列表。封装个对象,以后无论加多少查询条件都只需要在对象里面加字段就行。而且关键是看起来代码也很舒服啊!

封装业务逻辑

如果你看过“屎山”你就会有深刻的感触,这特么一个方法能写几千行代码,还无任何规则可言......往往负责的人会说,这个业务太复杂,没有办法改善,实际上这都是懒的借口。不管业务再复杂,我们都能够用合理的设计、封装去提升代码可读性。下面贴两段高级开发(假装自己是高级开发)写的代码

@Transactional
public ChildOrder submit(Long orderId, OrderSubmitRequest.Shop shop) {
  ChildOrder childOrder = this.generateOrder(shop);
  childOrder.setOrderId(orderId);
  //订单来源 APP/微信小程序
  childOrder.setSource(userService.getOrderSource());
  // 校验优惠券
  orderAdjustmentService.validate(shop.getOrderAdjustments());
  // 订单商品
  orderProductService.add(childOrder, shop);
  // 订单附件
  orderAnnexService.add(childOrder.getId(), shop.getOrderAnnexes());
  // 处理订单地址信息
  processAddress(childOrder, shop);
  // 最后插入订单
  childOrderMapper.insert(childOrder);
  this.updateSkuInventory(shop, childOrder);
  // 发送订单创建事件
  applicationEventPublisher.publishEvent(new ChildOrderCreatedEvent(this, shop, childOrder));
  return childOrder;
}
@Transactional
public void clearBills(Long customerId) {
  // 获取清算需要的账单、deposit等信息
  ClearContext context = getClearContext(customerId);
  // 校验金额合法
  checkAmount(context);
  // 判断是否可用优惠券,返回可抵扣金额
  CouponDeductibleResponse deductibleResponse = couponDeducted(context);
  // 清算所有账单
  DepositClearResponse response = clearBills(context);
  // 更新 l_pay_deposit
  lPayDepositService.clear(context.getDeposit(), response);
  // 发送还款对账消息
  repaymentService.sendVerifyBillMessage(customerId, context.getDeposit(), EventName.DEPOSIT_SUCCEED_FLOW_REMINDER);
  // 更新账户余额
  accountService.clear(context, response);
  // 处理清算的优惠券,被用掉或者解绑
  couponService.clear(deductibleResponse);
  // 保存券抵扣记录
  clearCouponDeductService.add(context, deductibleResponse);
}

这段两代码里面其实业务很复杂,内部估计保守干了五万件事情,但是不同水平的人写出来就完全不同,不得不赞一下这个注释,这个业务的拆分和方法的封装。一个大业务里面有多个小业务,不同的业务调用不同的 service 方法即可,后续接手的人即使没有流程图等相关文档也能快速理解这里的业务,而很多初级开发写出来的业务方法就是上一行代码是 A 业务的,下一行代码是 B业务的,在下面一行代码又是 A 业务的,业务调用之间还嵌套这一堆单元逻辑,显得非常混乱,代码还多。

判断集合类型不为空的正确方式

很多人喜欢写这样的代码去判断集合

if (list == null || list.size() == 0) {
return null;
}

当然你硬要这么写也没什么问题......但是不觉得难受么,现在框架中随便一个 jar 包都有集合工具类,比如 org.springframework.util.CollectionUtilscom.baomidou.mybatisplus.core.toolkit.CollectionUtils 。 以后请这么写

if (CollectionUtils.isEmpty(list) || CollectionUtils.isNotEmpty(list)) {
return null;
}

集合类型返回值不要 return null

当你的业务方法返回值是集合类型时,请不要返回 null,正确的操作是返回一个空集合。你看 mybatis 的列表查询,如果没查询到元素返回的就是一个空集合,而不是 null。否则调用方得去做 NULL 判断,多数场景下对于对象也是如此。

映射数据库的属性尽量不要用基本类型

我们都知道 int/long 等基本数据类型作为成员变量默认值是 0。现在流行使用 mybatisplus 、mybatis 等 ORM 框架,在进行插入或者更新的时候很容易会带着默认值插入更新到数据库。我特么真想砍了之前的开发,重构的项目里面实体类里面全都是基本数据类型。当场裂开......

封装判断条件

public void method(LoanAppEntity loanAppEntity, long operatorId) {
if (LoanAppEntity.LoanAppStatus.OVERDUE != loanAppEntity.getStatus()
        && LoanAppEntity.LoanAppStatus.CURRENT != loanAppEntity.getStatus()
        && LoanAppEntity.LoanAppStatus.GRACE_PERIOD != loanAppEntity.getStatus()) {
  //...
  return;
}

这段代码的可读性很差,这 if 里面谁知道干啥的?我们用面向对象的思想去给 loanApp 这个对象里面封装个方法不就行了么?

public void method(LoanAppEntity loan, long operatorId) {
if (!loan.finished()) {
  //...
  return;
}

LoanApp 这个类中封装一个方法,简单来说就是这个逻辑判断细节不该出现在业务方法中。

/**
* 贷款单是否完成
*/
public boolean finished() {
return LoanAppEntity.LoanAppStatus.OVERDUE != this.getStatus()
        && LoanAppEntity.LoanAppStatus.CURRENT != this.getStatus()
        && LoanAppEntity.LoanAppStatus.GRACE_PERIOD != this.getStatus();
}

控制方法复杂度

推荐一款 IDEA 插件 CodeMetrics ,它能显示出方法的复杂度,它是对方法中的表达式进行计算,布尔表达式,if/else 分支,循环等。


点击可以查看哪些代码增加了方法的复杂度,可以适当进行参考,毕竟我们通常写的是业务代码,在保证正常工作的前提下最重要的是要让别人能够快速看懂。当你的方法复杂度超过 10 就要考虑是否可以优化了。

使用 @ConfigurationProperties 代替 @Value

之前居然还看到有文章推荐使用 @Value 比 @ConfigurationProperties 好用的,吐了,别误人子弟。列举一下 @ConfigurationProperties 的好处。

  • 在项目 application.yml 配置文件中按住 ctrl + 鼠标左键点击配置属性可以快速导航到配置类。写配置时也能自动补全、联想到注释。需要额外引入一个依赖 org.springframework.boot:spring-boot-configuration-processor


  • @ConfigurationProperties 支持 NACOS 配置自动刷新,使用 @Value 需要在 BEAN 上面使用 @RefreshScope 注解才能实现自动刷新

  • @ConfigurationProperties 可以结合 Validation 校验,@NotNull、@Length 等注解,如果配置校验没通过程序将启动不起来,及早的发现生产丢失配置等问题。

  • @ConfigurationProperties 可以注入多个属性,@Value 只能一个一个写

  • @ConfigurationProperties 可以支持复杂类型,无论嵌套多少层,都可以正确映射成对象

相比之下我不明白为什么那么多人不愿意接受新的东西,裂开......你可以看下所有的 springboot-starter 里面用的都是 @ConfigurationProperties 来接配置属性。

推荐使用 lombok

当然这是一个有争议的问题,我的习惯是使用它省去 getter、setter、toString 等等。

不要在 AService 调用 BMapper

我们一定要遵循从 AService -> BService -> BMapper,如果每个 Service 都能直接调用其他的 Mapper,那特么还要其他 Service 干嘛?老项目还有从 controller 调用 mapper 的,把控制器当 service 来处理了。。。

尽量少写工具类

为什么说要少写工具类,因为你写的大部分工具类,在你无形中引入的 jar 包里面就有,String 的,Assert 断言的,IO 上传文件,拷贝流的,Bigdecimal 的等等。自己写容易错还要加载多余的类。

不要包裹 OpenFeign 接口返回值

搞不懂为什么那么多人喜欢把接口的返回值用 Response 包装起来......加个 code、message、success 字段,然后每次调用方就变成这样

CouponCommonResult bindResult = couponApi.useCoupon(request.getCustomerId(), order.getLoanId(), coupon.getCode());
if (Objects.isNull(bindResult) || !bindResult.getResult()) {
throw new AppException(CouponErrorCode.ERR_REC_COUPON_USED_FAILED);
}

这样就相当于

  1. 在 coupon-api 抛出异常

  2. 在 coupon-api 拦截异常,修改 Response.code

  3. 在调用方判断 response.code 如果是 FAIELD 再把异常抛出去......

你直接在服务提供方抛异常不就行了么。。。而且这样一包装 HTTP 请求永远都是 200,没法做重试和监控。当然这个问题涉及到接口响应体该如何设计,目前网上大多是三种流派

  • 接口响应状态一律 200

  • 接口响应状态遵从HTTP真实状态

  • 佛系开发,领导怎么说就怎么做

不接受反驳,我推荐使用 HTTP 标准状态。特定场景包括参数校验失败等一律使用 400 给前端弹 toast。下篇文章会阐述一律 200 的坏处。

写有意义的方法注释

这种注释你写出来是怕后面接手的人瞎么......

/**
* 请求电话验证
*
* @param credentialNum
* @param callback
* @param param
* @return PhoneVerifyResult
*/

要么就别写,要么就在后面加上描述......写这样的注释被 IDEA 报一堆警告看着蛋疼

和前端交互的 DTO 对象命名

什么 VO、BO、DTO、PO 我倒真是觉得没有那么大必要分那么详细,至少我们在和前端交互的时候类名要起的合适,不要直接用映射数据库的类返回给前端,这会返回很多不必要的信息,如果有敏感信息还要特殊处理。

推荐的做法是接受前端请求的类定义为 XxxRequest,响应的定义为 XxxResponse。以订单为例:接受保存更新订单信息的实体类可以定义为 OrderRequest,订单查询响应定义为 OrderResponse,订单的查询条件请求定义为 OrderQueryRequest

尽量别让 IDEA 报警

我是很反感看到 IDEA 代码窗口一串警告的,非常难受。因为有警告就代表代码还可以优化,或者说存在问题。 前几天捕捉了一个团队内部的小bug,其实本来和我没有关系,但是同事都在一头雾水的看外面的业务判断为什么走的分支不对,我一眼就扫到了问题。

因为 java 中整数字面量都是 int 类型,到集合中就变成了 Integer,然后 stepId 点上去一看是 long 类型,在集合中就是 Long,那这个 contains 妥妥的返回 false,都不是一个类型。

你看如果注重到警告,鼠标移过去看一眼提示就清楚了,少了一个生产 bug。

尽可能使用新技术组件

我觉得这是一个程序员应该具备的素养......反正我是喜欢用新的技术组件,因为新的技术组件出现必定是解决旧技术组件的不足,而且作为一个技术人员我们应该要与时俱进~~ 当然前提是要做好准备工作,不能无脑升级。举个最简单的例子,Java 17 都出来了,新项目现在还有人用 Date 来处理日期时间...... 都什么年代了你还在用 Date

结语

本篇文章简单介绍我日常开发的习惯,当然仅是作者自己的见解。暂时只想到这几点,以后发现其他的会更新。

作者:暮色妖娆丶
来源:https://juejin.cn/post/7072252275002966030

收起阅读 »

纯后端如何写前端?我用了低代码平台

我是3y,一年CRUD经验用十年的markdown程序员👨🏻‍💻常年被誉为优质八股文选手花了几天搭了个后台管理页面,今天分享下我的搭建过程,全文非技术向,就当跟大家吹吹水吧。1、我的前端技术老读者可能知道我是上了大学以后,才了解什么是编程。在这之前,我对编程一...
继续阅读 »

我是3y,一年CRUD经验用十年的markdown程序员👨🏻‍💻常年被誉为优质八股文选手

花了几天搭了个后台管理页面,今天分享下我的搭建过程,全文非技术向,就当跟大家吹吹水吧。


1、我的前端技术

老读者可能知道我是上了大学以后,才了解什么是编程。在这之前,我对编程一无所知,甚至报考了计算机专业之后也未曾了解过它是做什么的。

在大一的第一个学期,我印象中只开了一门C++的编程课(其他的全是数学)。嗯,理所当然,我是听不懂的,也不知道用来干什么。


刚进大学的时候,我对一切充满了未知,在那时候顺其自然地就想要进几个社团玩玩。但在众多社团里都找不到我擅长的领域,等快到截止时间了。我又不想大学期间什么社团都没有参加,最后报了两个:乒乓球社团和计算机协会

这个计算机协会绝大多数的人员都来自于计算机专业,再后来才发现这个协会的主要工作就是给人「重装系统」,不过这是后话啦。

当时加入计算机协会还需要满足一定的条件:师兄给了一个「网站」我们这群人,让我们上去学习,等到国庆回来后看下我们的学习进度再来决定是否有资格加入。

那个网站其实就是对HTML/CSS/JavaScript入门教程,是一个国外的网站,具体的地址我肯定是忘了。不过那时候,我国庆闲着也没事干,于是就开始学起来了。我当时的进度应该是学到CSS,能简单的页面布局和展示图片啥的

刚开始的时候,觉得蛮有趣的:我改下这个代码,字体的颜色就变了,图片就能展示出来了。原来我平时上网的网站是这样弄出来的啊! (比什么C++有趣多了)

国庆后回来发现:考核啥的并不重要,只要报名了就都通过了。


有了基本的认知后,我对这个也并不太上心,没有持续地学下去。再后来,我实在是太无聊,就开始想以后毕业找工作的事了,自己也得在大学充实下自己,于是我开始在知乎搜各种答案「如何入门编程」。

在知乎搜了各种路线并浪费了大量时间以后,我终于开始看视频入门。我熬完了JavaSE基础之后,我记得我是看方立勋老师入门的JavaWeb,到前端的课程以后,我觉得前端HTML/CSS/JavaScript啥的都要补补,于是又去找资源学习(那时候信奉着技多不压身)。

印象中是看韩顺平老师的HTML/CSS/JavaScript,那时候还手打代码的阶段,把我看得一愣一愣的(IDE都不需要的)。随着学习,发现好像还得学AJAX/jQuery,于是我又去找资源了,不过我已经忘了看哪个老师的AJAXjQuery课程。

在这个学习的过程中,我曾经用纯HTML/CSS/JavaScript跟着视频仿照过某某网站,在jQuery的学习时候做过各种的轮播图动画。还理解了marginpadding的区别。临近毕业的时候,也会点BootStrap来写个简单的页面(丑就完事了)


等我进公司了以后,技术架构前后端是分离的,虽然我拉了前端的代码,但我看不懂,期间我也没学。以至于我两年多是没碰过前端的,我对前端充满着敬畏(刚毕业那段时间,前端在飞速发展

2、AUSTIN前端选型

从我筹划要写austin项目的时候,我就知道我肯定要写一个「后台管理页面」,但我迟迟没下手。一方面是我认为「后端」才是我的赛道,另一方面我「前端」确实菜,不想动手。

我有想过要不找个小伙伴帮我写,但是很快就被我自己否定了:还得给小伙伴提需求,算了


当我要面临前端的时,我第一时间就想到:肯定是有什么框架能够快速搭建出一个管理页面的。我自己不知道,但是,我的朋友圈肯定是有人知道的啊。于是,我果断求助:


我被安利了很多框架,简单列举下出场率比较高的。

:大多数我只是粗略看了下,没有仔细研究。若有错误可以在评论区留言,轻喷

2.1 renren-fast

官网文档:http://www.renren.io/guide#getdo…


它这个框架是前后端分离的,后端还可以生成对应的CRUD代码,前端基于vueelement-ui开发。

当时其实我有点想选它的,但考虑到我要再部署个后端,还得学点vue,我就搁置了

2.2 RuoYi

官方文档:doc.ruoyi.vip/ruoyi/


RuoYi给我安利的也很多,这个貌似最近非常火?感觉我被推荐了以后,到处都能看到它的身影。

我简单刷了下文档,感觉他做的事比renren-fast要多,文档也很齐全,但是没找到我想要的东西:我打开一个文档,我希望能看到它的系统架构,系统之间的交互或者架构层面上的东西,但我没快速找到。

项目齐全和复杂对我来说或许并不是一件好事,很可能意味着我的学习成本可能会更大。于是,我也搁置着。

2.3 Vue相关

vue-element-admin

官方文档:panjiachen.github.io/vue-element…


Vue Antd Admin

官方文档:iczer.gitee.io/vue-antd-ad…


Ant Design Pro

官方文档:pro.antdv.com/docs/gettin…


这几个项目被推荐率也是极高的,从第一行介绍我基本就知道需要去学Vue的语法,奈何我太懒了,搁置着。

2.4 layui

有好几小伙伴们听说我会jQuery,于是给我推荐了layui。我以前印象中好像听过这个框架,但一直没了解过他。但是,当我搜到它的时候,它已经不维护了


GitHub地址:github.com/sentsin/lay…

我简单浏览下文档,其实它也有对应的一套”语法“,需要一定的学习成本,但不高。


第一感觉有点类似我以前写过的BootStrap,我对这不太感冒,感觉如果要接入可能还是需要自己写比较多的代码。

2.5 其他

还有些小伙伴推荐或者我看到的文章推荐:x-admin/D2admin/smartchart/JEECG-BOOT/Dcat-admin/iview-admin等等等,在这里面还有些依赖着PHP/Python

总的来说,我还是觉得这些框架有一定的学习成本(我真的是懒出天际了)。可能需要我去部署后端,也可能需要我学习前端的框架语法,也可能让我学Vue

看到这里,可能你们很好奇我最后选了什么作为austin的前端,都已经被我筛了这么多了。在公布之前,我想说的是:如果想要页面好看灵活性高还是得学习Vue。从上面我被推荐的框架中,好多都是在Vue的基础上改动的,并且我敢肯定:还有很多基于Vue且好用的后台是我不知道的。

:我这里指代跟我一样不懂前端的(如果本身就已经懂前端,你说啥都对)


3、AMIS框架

我最后选择了amis作为austin的前端。这个框架在我朋友圈只有一个小伙伴推荐,我第一次打开文档的时候,确实惊艳到我了


文档地址:baidu.gitee.io/amis/zh-CN/…

它是一个低代码前端框架:amis 的渲染过程是将 json 转成对应的 React 组件

我花了半天粗略地刷了下文档,大概知道了JSON的结构(说实话,他这个文档写得挺可以的),然后我去GitHub找了一份模板,就直接开始动手了,readme十分简短。


GitHub:github.com/aisuda/amis…

这个前端低代码工具还有个好处就是可以通过可视化编辑器拖拉生成JSON代码,将生成好的代码直接往自己本地一贴,就完事了,确实挺方便的。


可视化编辑器的地址:aisuda.github.io/amis-editor…

4、使用感受

其实没什么好讲的,无非就是在页面上拖拉得到一个页面,然后调用API的时候看下文档的姿势。

在这个过程中我也去看了下这个框架的评价,发现百度内部很多系统就用的这个框架来搭建页面的,也看到Bigo也有在线上使用这个框架来搭建后台。有一线/二线公司都在线上使用该框架了,我就认为问题不大了。

总的来说,我这次搭建austin后台实际编码时间没多少,都在改JSON配置和查文档。我周六下午2点到的图书馆,新建了GitHub仓库,在6点闭馆前就已经搭出个大概页面了,然后在周日空闲时间里再完善了几下,感觉可以用了

austin-amis仓库地址:github.com/ZhongFuChen…

在搭建的过程中,amis低代码框架还是有地方可吐槽的,就是它的灵活性太低。我们的接口返回值需要迎合它的主体结构,当我们如果有嵌套JSON这种就变得异常难处理,表单无法用表达式进行回显等等。

它并不完美,很可能需要我用些奇怪的姿势妥协,不要问我接口返回的时候为啥转了一层Map


不管怎么说,这不妨碍我花了极短的时间就能搭出一个能看的后台管理页面(CRUD已齐全)


5、总结

目前搭好的前端能用,也只能用一点点,后面会逐渐完善它的配置和功能的。我后面有链路追踪的功能,肯定要在后台这把清洗后的数据提供给后台进行查询,但也不会花比较长的篇幅再来聊前端这事了。

我一直定位是在后端的代码上,至于前端我能学,但我又不想学。怎么说呢,利益最大化吧。我把学前端的时间花在学后端上,或许可能对我有更大的受益。现在基本前后端分离了,在公司我也没什么机会写前端。

下一篇很有可能是聊分布式定时任务框架上,我发现我的进度可以的,这个季度拿个4.0应该问题不大了。

都看到这里了,点个赞一点都不过分吧?我是3y,下期见。


austin项目源码Gitee链接:gitee.com/austin

austin项目源码GitHub链接:github.com/austin


作者:Java3y
来源:https://juejin.cn/post/7076231399669235725

收起阅读 »

你最少用几行代码实现深拷贝?

前言深度克隆(深拷贝)一直都是初、中级前端面试中经常被问到的题目,网上介绍的实现方式也都各有千秋,大体可以概括为三种方式:JSON.stringify+JSON.pars e, 这个很好理解;全量判断类型,根据类型做不同的处理2的变型,简化类型判断过程前两种比...
继续阅读 »

前言

深度克隆(深拷贝)一直都是初、中级前端面试中经常被问到的题目,网上介绍的实现方式也都各有千秋,大体可以概括为三种方式:

  1. JSON.stringify+JSON.pars e, 这个很好理解;

  2. 全量判断类型,根据类型做不同的处理

  3. 2的变型,简化类型判断过程

前两种比较常见也比较基础,所以我们今天主要讨论的是第三种。

阅读全文你将学习到:

  1. 更简洁的深度克隆方式

  2. Object.getOwnPropertyDescriptors()api

  3. 类型判断的通用方法

问题分析

深拷贝 自然是 相对 浅拷贝 而言的。 我们都知道 引用数据类型 变量存储的是数据的引用,就是一个指向内存空间的指针, 所以如果我们像赋值简单数据类型那样的方式赋值的话,其实只能复制一个指针引用,并没有实现真正的数据克隆。

通过这个例子很容易就能理解:

const obj1 = {
   name: 'superman'
}
const obj2 = obj1;
obj1.name = '前端切图仔';
console.log(obj2.name); // 前端切图仔

所以深度克隆就是为了解决引用数据类型不能被通过赋值的方式 复制 的问题。

引用数据类型

我们不妨来罗列一下引用数据类型都有哪些:

  • ES6之前: Object, Array, Date, RegExp, Error,

  • ES6之后: Map, Set, WeakMap, WeakSet,

所以,我们要深度克隆,就需要对数据进行遍历并根据类型采取相应的克隆方式。 当然因为数据会存在多层嵌套的情况,采用递归是不错的选择。

简单粗暴版本

function deepClone(obj) {
   let res = {};
   // 类型判断的通用方法
   function getType(obj) {
       return Object.prototype.toString.call(obj).replaceAll(new RegE xp(/\[|\]|object /g), "");
  }
   const type = getType(obj);
   const reference = ["Set", "WeakSet", "Map", "WeakMap", "RegExp", "Date", "Error"];
   if (type === "Object") {
       for (const key in obj) {
           if (Object.hasOwnProperty.call(obj, key)) {
               res[key] = deepClone(obj[key]);
          }
      }
  } else if (type === "Array") {
       console.log('array obj', obj);
       obj.forEach((e, i) => {
           res[i] = deepClone(e);
      });
  }
   else if (type === "Date") {
       res = new Date(obj);
  } else if (type === "RegExp") {
       res = new RegExp(obj);
  } else if (type === "Map") {
       res = new Map(obj);
  } else if (type === "Set") {
       res = new Set(obj);
  } else if (type === "WeakMap") {
       res = new WeakMap(obj);
  } else if (type === "WeakSet") {
       res = new WeakSet(obj);
  }else if (type === "Error") {
       res = new Error(obj);
  }
    else {
       res = obj;
  }
   return res;
}

其实这就是我们最前面提到的第二种方式,很傻对不对,明眼人一眼就能看出来有很多冗余代码可以合并。

我们先进行最基本的优化:

合并冗余代码

将一眼就能看出来冗余的代码合并下。

function deepClone(obj) {
   let res = null;
   // 类型判断的通用方法
   function getType(obj) {
       return Object.prototype.toString.call(obj).replaceAll(new RegExp(/\[|\]|object /g), "");
  }
   const type = getType(obj);
   const reference = ["Set", "WeakSet", "Map", "WeakMap", "RegExp", "Date", "Error"];
   if (type === "Object") {
       res = {};
       for (const key in obj) {
           if (Object.hasOwnProperty.call(obj, key)) {
               res[key] = deepClone(obj[key]);
          }
      }
  } else if (type === "Array") {
       console.log('array obj', obj);
       res = [];
       obj.forEach((e, i) => {
           res[i] = deepClone(e);
      });
  }
   // 优化此部分冗余判断
   // else if (type === "Date") {
   //     res = new Date(obj);
   // } else if (type === "RegExp") {
   //     res = new RegExp(obj);
   // } else if (type === "Map") {
   //     res = new Map(obj);
   // } else if (type === "Set") {
   //     res = new Set(obj);
   // } else if (type === "WeakMap") {
   //     res = new WeakMap(obj);
   // } else if (type === "WeakSet") {
   //     res = new WeakSet(obj);
   // }else if (type === "Error") {
   //   res = new Error(obj);
   //}
   else if (reference.includes(type)) {
       res = new obj.constructor(obj);
  } else {
       res = obj;
  }
   return res;
}

为了验证代码的正确性,我们用下面这个数据验证下:

const map = new Map();
map.set("key", "value");
map.set("ConardLi", "coder");

const set = new Set();
set.add("ConardLi");
set.add("coder");

const target = {
   field1: 1,
   field2: undefined,
   field3: {
       child: "child",
  },
   field4: [2, 4, 8],
   empty: null,
   map,
   set,
   bool: new Boolean(true),
   num: new Number(2),
   str: new String(2),
   symbol: Object(Symbol(1)),
   date: new Date(),
   reg: /\d+/,
   error: new Error(),
   func1: () => {
       let t = 0;
       console.log("coder", t++);
  },
   func2: function (a, b) {
       return a + b;
  },
};
//测试代码
const test1 = deepClone(target);
target.field4.push(9);
console.log('test1: ', test1);

执行结果:


还有进一步优化的空间吗?

答案当然是肯定的。

// 判断类型的方法移到外部,避免递归过程中多次执行
const judgeType = origin => {
   return Object.prototype.toString.call(origin).replaceAll(new RegExp(/\[|\]|object /g), "");
};
const reference = ["Set", "WeakSet", "Map", "WeakMap", "RegExp", "Date", "Error"];
function deepClone(obj) {
   // 定义新的对象,最后返回
    //通过 obj 的原型创建对象
   const cloneObj = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

   // 遍历对象,克隆属性
   for (let key of Reflect.ownKeys(obj)) {
       const val = obj[key];
       const type = judgeType(val);
       if (reference.includes(type)) {
           newObj[key] = new val.constructor(val);
      } else if (typeof val === "object" && val !== null) {
           // 递归克隆
           newObj[key] = deepClone(val);
      } else {
           // 基本数据类型和function
           newObj[key] = val;
      }
  }
   return newObj;
}

执行结果如下:


  • Object.getOwnPropertyDescriptors() 方法用来获取一个对象的所有自身属性的描述符。

  • 返回所指定对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。

具体解释和内容见MDN

这样做的好处就是能够提前定义好最后返回的数据类型。

这个实现参考了网上一位大佬的实现方式,个人觉得理解成本有点高,而且对数组类型的处理也不是特别优雅, 返回类数组。

我在我上面代码的基础上进行了改造,改造后的代码如下:

function deepClone(obj) {
   let res = null;
   const reference = [Date, RegExp, Set, WeakSet, Map, WeakMap, Error];
   if (reference.includes(obj?.constructor)) {
       res = new obj.constructor(obj);
  } else if (Array.isArray(obj)) {
       res = [];
       obj.forEach((e, i) => {
           res[i] = deepClone(e);
      });
  } else if (typeof obj === "Object" && obj !== null) {
       res = {};
       for (const key in obj) {
           if (Object.hasOwnProperty.call(obj, key)) {
               res[key] = deepClone(obj[key]);
          }
      }
  } else {
       res = obj;
  }
   return res;
}

虽然代码量上没有什么优势,但是整体的理解成本和你清晰度上我觉得会更好一点。那么你觉得呢?

最后,还有循环引用问题,避免出现无线循环的问题。

我们用hash来存储已经加载过的对象,如果已经存在的对象,就直接返回。

function deepClone(obj, hash = new WeakMap()) {
   if (hash.has(obj)) {
       return obj;
  }
   let res = null;
   const reference = [Date, RegExp, Set, WeakSet, Map, WeakMap, Error];

   if (reference.includes(obj?.constructor)) {
       res = new obj.constructor(obj);
  } else if (Array.isArray(obj)) {
       res = [];
       obj.forEach((e, i) => {
           res[i] = deepClone(e);
      });
  } else if (typeof obj === "Object" && obj !== null) {
       res = {};
       for (const key in obj) {
           if (Object.hasOwnProperty.call(obj, key)) {
               res[key] = deepClone(obj[key]);
          }
      }
  } else {
       res = obj;
  }
   hash.set(obj, res);
   return res;
}

总结

对于深拷贝的实现,可能存在很多不同的实现方式,关键在于理解其原理,并能够记住一种最容易理解和实现的方式,面对类似的问题才能做到 临危不乱,泰然自若。 上面的实现你觉得哪个更好呢?欢迎大佬们在评论区交流~


作者:前端superman
来源:https://juejin.cn/post/7075351322014253064

收起阅读 »

为了快乐的摸鱼,专门写了个网站!

这是鄙人做的网站,目的呢原本是为了摸鱼,把产品那边整的页面快速构建出来,咱们公司用的是比较老的vue2版本,组件库是ant-design-vue,做的系统是一些中规中矩的企业用的办公系统,所以页面都是千篇一律。作为卑微的996社畜,不想被肆无忌惮的压榨,于是有...
继续阅读 »

直接进入主题: demo

这是鄙人做的网站,目的呢原本是为了摸鱼,把产品那边整的页面快速构建出来,咱们公司用的是比较老的vue2版本,组件库是ant-design-vue,做的系统是一些中规中矩的企业用的办公系统,所以页面都是千篇一律。作为卑微的996社畜,不想被肆无忌惮的压榨,于是有一天,我就琢磨着通过拖拉拽的方式把组件模块组合起来,能快速的响应产品那边朝令夕改的无理要求。

经过将近一个月的鼓捣,小破站也在命运多舛中慢慢走向成熟。

先简单介绍吧,显而易见的操作界面:传统的页眉,低调却不失风采;左侧的手风琴列表,简约而不简单;中控是一个设计器,有了它你可以写出一个出色的网页,而不需要写一行代码(少量代码还是必要的)!





本小破站还做了国际化、自适应,能基本满足常规的企业系统界面需求,比如传统的ERP/HR/SDM等后台管理系统,页面的顶部有一个下拉框,里面有默认的几个示例,都是通过这种拖拽方式做出来的。

有一个地方需要特别说明,就是组件提供的事件回调函数提供w,w,w,vm这两个全局参数。w表示当前window全局对象,w表示当前window全局对象,w表示当前window全局对象,vm则代表全局vm对象,也就是this。通过这两个参数,是可以简单的写出组件间调用的方法的 (可以看看test#table这个例子)。当然,涉及更复杂一点的业务逻辑,则需要做更多的代码复用,以及watch监听等等,这部分功能的话暂时还没有想好怎么实现。

组件有基本的antd组件、echarts组件,还有vue-3d-model组件,为了更方便的编辑属性和代码,用了bin-ace-editor,有了这些大佬们的轮子,转起来确实快乐。

功能还在逐步完善中,最近也没很多时间去写,总之有时间就去补充,日积月累的完善吧。

PS:

小破站带宽是乞丐版的1Mb,鄙人已经尽力做了cdn加速,希望不卡

第一次打开会自动生成3个示例,放在localStorage里面

感谢阅读 ^_^


作者:AllenThomas
来源:https://juejin.cn/post/7077743139934437406

收起阅读 »

95年女程序员内心的感受

作为一个95年女程序员,2022也是我从事前端的第5个年头了,期间换过2个公司,想和大家分享下真实感受。坐标: 东北 故事开始 95年出生的女生,大专学历,大学专业为“电子商务”, 因为接触到网页设计与制作以及PS等课程,从而对通过代码写过网页产生了兴趣,东...
继续阅读 »

作为一个95年女程序员,2022也是我从事前端的第5个年头了,期间换过2个公司,想和大家分享下真实感受。坐标: 东北



故事开始


95年出生的女生,大专学历,大学专业为“电子商务”, 因为接触到网页设计与制作以及PS等课程,从而对通过代码写过网页产生了兴趣,东西做出来觉得很有成就感,于是实习期间抱着期待的心情投了很多关于网页设计和网页制作相关的工作,但是更多等来的是培训机构的电话,被告知不通过培训时找不到工作的,学习的东西太少不够用于工作,于是阴差阳错的去了培训机构学习了前端开发...


第一个公司


第一家公司是我2017年3月初入职的,是一个外包公司,由于是mini型公司,没有前端,所以是一个UI小姐姐面试的我,刚好我懂一点设计,聊的比较来,并没有问我太多技术问题就让我去上班了,就这样开启了我的第一份工作...


感受:公司很小,人员很nice,成长很快,初创型企业真的很锻炼人,当只有我一个前端面临问题时,真的很无助,但也因此锻炼我发现问题解决问题的能力,不会就学,不懂就百度,慢慢的也开始能够独当一面,虽然经常加班,但是我很感谢这一份经历,虽然工资低也很累但是也收获了巨大成长


离职原因:后来公司被一个大公司收购成为了一个小部门,因为拖欠工资没办法维持现在的生活就离开了...


第二个公司


第二家公司是2018年入职,是一个自研产品的公司,研发人员上百个,当然前端人员自然也很多啦,想要重点说说感受


感受:公司很大,制度很多,流程相对规范,比如git提交的规范、自动构建、内部脚手架等,不过我发现明显的问题,就是分工不均匀,有的项目组天天加班,持续半年加班,但是有的项目组,每天除了摸鱼还是摸鱼,比如我们组,很多项目都流产了,就很多时候都没有事情,最多的就是逛逛B站,偶尔自学,看看新闻时事....


离职:目前尚未离职,在纠结中。


焦虑


在这个公司快4年了,技术一直停滞,生活的很安逸,但是有点温水煮青蛙的感觉,刚开始会自学,慢慢的也就不太想自学了,好像整个人都堕落了,没有刚开始工作的冲劲了... 算一算我也27岁了,虽说未来的事情说不准,但是确实30岁以后的的程序员确实找工作相对会难一点,尤其是女程序员会考虑到结婚生子的问题,年龄大了确实也会受限,所以现在在纠结到底从事什么工作,是转行还是重新努力学习前端知识,从事前端开发工作,会发现很迷茫,换行or继续前端....


很后悔浪费了这4年的光阴,希望后面的程序员弟弟妹妹要珍惜自己现在的时光,程序员本身就业时间就相对其他行业短暂一点,对于我们来说每一天都很重要,4年了足够我们做很多很多事情了,如果对开发不是很感兴趣也可以提前根据自己的兴趣来规划职业


作者:M77星球
来源:https://juejin.cn/post/7076377003057741838
收起阅读 »

前端无痛刷新Token

前端无痛刷新Token这个需求场景很常见,几乎很多项目都会用上,之前项目也实现过,最近刚好有个项目要实现,重新梳理一番。需求对于需要前端实现无痛刷新Token,无非就两种:请求前判断Token是否过期,过期则刷新请求后根据返回状态判断是否过期,过期则刷新处理逻...
继续阅读 »

前端无痛刷新Token

这个需求场景很常见,几乎很多项目都会用上,之前项目也实现过,最近刚好有个项目要实现,重新梳理一番。

需求

对于需要前端实现无痛刷新Token,无非就两种:

  1. 请求前判断Token是否过期,过期则刷新

  2. 请求后根据返回状态判断是否过期,过期则刷新

处理逻辑

实现起来也没多大差别,只是判断的位置不一样,核心原理都一样:

  1. 判断Token是否过期

    1. 没过期则正常处理

    2. 过期则发起刷新Token的请求

      1. 拿到新的Token保存

      2. 重新发送Token过期这段时间内发起的请求

重点:

  • 保持Token过期这段时间发起请求状态(不能进入失败回调)

  • 把刷新Token后重新发送请求的响应数据返回到对应的调用者

实现

  1. 创建一个flag isRefreshing 来判断是否刷新中

  2. 创建一个数组队列retryRequests来保存需要重新发起的请求

  3. 判断到Token过期

    1. isRefreshing = false的情况下 发起刷新Token的请求

      1. 刷新Token后遍历执行队列retryRequests

    2. isRefreshing = true 表示正在刷新Token,返回一个Pending状态的Promise,并把请求信息保存到队列retryRequests

import axios from "axios";
import Store from "@/store";
import Router from "@/router";
import { Message } from "element-ui";
import UserUtil from "@/utils/user";

// 创建实例
const Instance = axios.create();
Instance.defaults.baseURL = "/api";
Instance.defaults.headers.post["Content-Type"] = "application/json";
Instance.defaults.headers.post["Accept"] = "application/json";

// 定义一个flag 判断是否刷新Token中
let isRefreshing = false;
// 保存需要重新发起请求的队列
let retryRequests = [];

// 请求拦截
Instance.interceptors.request.use(async function(config) {
 Store.commit("startLoading");
 const userInfo = UserUtil.getLocalInfo();
 if (userInfo) {
   //业务需要把Token信息放在 params 里面,一般来说都是放在 headers里面
   config.params = Object.assign(config.params ? config.params : {}, {
     appkey: userInfo.AppKey,
     token: userInfo.Token
  });
}
 return config;
});

// 响应拦截
Instance.interceptors.response.use(
 async function(response) {
   Store.commit("finishLoading");
   const res = response.data;
   if (res.errcode == 0) {
     return Promise.resolve(res);
  } else if (
     res.errcode == 30001 ||
     res.errcode == 40001 ||
     res.errcode == 42001 ||
     res.errcode == 40014
  ) {
   // 需要刷新Token 的状态 30001 40001 42001 40014
   // 拿到本次请求的配置
     let config = response.config;
   //   进入登录页面的不做刷新Token 处理
     if (Router.currentRoute.path !== "/login") {
       if (!isRefreshing) {
           // 改变flag状态,表示正在刷新Token中
         isRefreshing = true;
       //   刷新Token
         return Store.dispatch("user/relogin")
          .then(res => {
           // 设置刷新后的Token
             config.params.token = res.Token;
             config.params.appkey = res.AppKey;
           //   遍历执行需要重新发起请求的队列
             retryRequests.forEach(cb => cb(res));
           //   清空队列
             retryRequests = [];
             return Instance.request(config);
          })
          .catch(() => {
             retryRequests = [];
             Message.error("自动登录失败,请重新登录");
               const code = Store.state.user.info.CustomerCode || "";
               // 刷新Token 失败 清空缓存的用户信息 并调整到登录页面
               Store.dispatch("user/logout");
               Router.replace({
                 path: "/login",
                 query: { redirect: Router.currentRoute.fullPath, code: code }
              });
          })
          .finally(() => {
               // 请求完成后重置flag
             isRefreshing = false;
          });
      } else {
         // 正在刷新token,返回一个未执行resolve的promise
         // 把promise 的resolve 保存到队列的回调里面,等待刷新Token后调用
         // 原调用者会处于等待状态直到 队列重新发起请求,再把响应返回,以达到用户无感知的目的(无痛刷新)
         return new Promise(resolve => {
           // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
           retryRequests.push(info => {
               // 将新的Token重新赋值
             config.params.token = info.Token;
             config.params.appkey = info.AppKey;
             resolve(Instance.request(config));
          });
        });
      }
    }
     return new Promise(() => {});
  } else {
     return Promise.reject(res);
  }
},
 function(error) {
   let err = {};
   if (error.response) {
     err.errcode = error.response.status;
     err.errmsg = error.response.statusText;
  } else {
     err.errcode = -1;
     err.errmsg = error.message;
  }
   Store.commit("finishLoading");
   return Promise.reject(err);
}
);

export default Instance;


作者:沐夕花开
来源:https://juejin.cn/post/7075348765162340383

收起阅读 »

基于JDK的动态代理原理分析

基于JDK的动态代理原理分析 这篇文章解决三个问题: What 动态代理是什么 How 动态代理怎么用 Why 动态代理的原理 动态代理是什么? 动态代理是代理模式的一种具体实现,是指在程序运行期间,动态的生成目标对象的代理类(直接加载在内存中的字节码文件...
继续阅读 »

基于JDK的动态代理原理分析


这篇文章解决三个问题:



  1. What 动态代理是什么

  2. How 动态代理怎么用

  3. Why 动态代理的原理


动态代理是什么?


动态代理是代理模式的一种具体实现,是指在程序运行期间,动态的生成目标对象的代理类(直接加载在内存中的字节码文件),实现对目标对象所有方法的增强。通过这种方式,我们可以在不改变(或无法改变)目标对象源码的情况下,对目标对象的方法执行前后进行干预。


动态代理怎么用?


首先,准备好我们需要代理的类和接口,因为JDK的动态代理是基于接口实现的,所以被代理的对象必须要有接口


/**
* SaySomething接口
*/

public interface SaySomething {

   public void sayHello();

   public void sayBye();
}

/**
* SaySomething的实现类
*/

public class SaySomethingImpl implements SaySomething {
   @Override
   public void sayHello() {
       System.out.println("Hello World");
  }

   @Override
   public void sayBye() {
       System.out.println("Bye Bye");
  }
}

按照动态代理的用法,需要自定义一个处理器,用来编写自定义逻辑,实现对被代理对象的增强。


自定义的处理器需要满足以下要求:



  • 需要实现InvocationHandler,重写invoke方法,在invoke方法中通过加入自定义逻辑,实现对目标对象的增强。

  • 需要持有一个成员变量,成员变量的是被代理对象的实例,通过构造参数传入。(用来支持反射调用被代理对象的方法)

  • 需要提供一个参数为被代理对象接口类的有参构造。(用来支持反射调用被代理对象的方法)


/**
* 自定义的处理器,用来编写自定义逻辑,实现对被代理对象的增强
*/

public class CustomHandler implements InvocationHandler {

   //需要有一个成员变量,成员变量为被代理对象,通过构造参数传入,用来支持方法的反射调用。
   private SaySomething obj;
   
   //需要有一个有参构造,通过构造函数将被代理对象的实例传入,用来支持方法的反射调用
   public CustomHandler(SaySomething obj) {
       this.obj = obj;
  }

   /**
    * proxy:动态生成的代理类对象com.sun.proxy.$Proxy0
    * method:被代理对象的真实的方法的Method对象
    * args:调用方法时的入参
    */

   @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       //目标方法执行前的自定义逻辑处理
       System.out.println("-----before------");

       //执行目标对象的方法,使用反射来执行方法,反射需要传入目标对象,此时用到了成员变量obj。
       Object result = method.invoke(obj, args);

       //目标方法执行后的自定义逻辑处理
       System.out.println("-----after------");
       return result;
  }
}

这样我们就完成了自定义处理器的编写,同时在invoke方法中实现对了代理对象方法的增强,被代理类的所有方法的执行都会执行我们自定义的逻辑。


接下来,需要通过Proxy,newProxyInstance()方法来生成代理对象的实例,并进行方法调用测试。


public class JdkProxyTest {
   public static void main(String[] args) {
       //将生成的代理对象的字节码文件 保存到硬盘
       System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

       //被代理对象的实例
       SaySomething obj = new SaySomethingImpl();
       //通过构造函数,传入被代理对象的实例,生成处理器的实例
       InvocationHandler handler = new CustomHandler(obj);
       //通过Proxy.newProxyInstance方法,传入被代理对象Class对象、处理器实例,生成代理对象实例
       SaySomething proxyInstance = (SaySomething) Proxy.newProxyInstance(obj.getClass().getClassLoader(),
                                                                          new Class[]{SaySomething.class}, handler);
       //调用生成的代理对象的sayHello方法
       proxyInstance.sayHello();
       System.out.println("===================分割线==================");
       //调用生成的代理对象的sayBye方法
       proxyInstance.sayBye();
  }
}

image.png
运行main方法,查看控制台,大功告成。至此,我们已经完整的完成了一次动态代理的使用。


动态代理的原理


生成的proxyInstance对象到底是什么,为什么调用它的sayHello方法会执行CustomerHandler的invoke方法呢?


直接贴上proxyInstance的字节码文件,我们就会恍然大悟了...


//$Proxy0是SaySomething的实现类,重写了sayHello和sayBye方法
public final class $Proxy0 extends Proxy implements SaySomething {
   private static Method m1;
   private static Method m3;
   private static Method m2;
   private static Method m4;
   private static Method m0;

   public $Proxy0(InvocationHandler var1) throws {
       super(var1);
  }

   static {
       try {
           m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
           m3 = Class.forName("com.example.demo.hanmc.proxy.jdk.SaySomething").getMethod("sayHello");
           m2 = Class.forName("java.lang.Object").getMethod("toString");
           m4 = Class.forName("com.example.demo.hanmc.proxy.jdk.SaySomething").getMethod("sayBye");
           m0 = Class.forName("java.lang.Object").getMethod("hashCode");
      } catch (NoSuchMethodException var2) {
           throw new NoSuchMethodError(var2.getMessage());
      } catch (ClassNotFoundException var3) {
           throw new NoClassDefFoundError(var3.getMessage());
      }
  }
 
   //实现了接口的sayHello方法,在方法内部调用了CustomerHandler的invoke方法,同时传入了Method对象,
   //所以在CustomerHandler对象中可以通过mathod.invovke方法调用SyaSomthing的sayHello方法
   public final void sayHello() throws {
       try {
           //h是父类Proxy中的InvocationHandler对象,其实就是我们自定义的CustomHandler对象
           super.h.invoke(this, m3, (Object[])null);
      } catch (RuntimeException | Error var2) {
           throw var2;
      } catch (Throwable var3) {
           throw new UndeclaredThrowableException(var3);
      }
  }

   public final void sayBye() throws {
       try {
           super.h.invoke(this, m4, (Object[])null);
      } catch (RuntimeException | Error var2) {
           throw var2;
      } catch (Throwable var3) {
           throw new UndeclaredThrowableException(var3);
      }
  }
   public final int hashCode() throws {
      //忽略内容
  }
   public final boolean equals(Object var1) throws {
      //忽略内容
  }
   public final String toString() throws {
      //忽略内容
  }
}

看到了生成的代理对象的字节码文件,是不是一切都明白你了,原理竟然如此简单^_^


作者:82年咖啡
来源:https://juejin.cn/post/7079720742899843080
收起阅读 »

你已经是个成熟的前端了,应该学会破解防盗链了

今天一早打开微信,就看到国产github——gitee崩了。 Issue列表里面全是反馈图片显示异常,仔细一看,原来是图床的防盗链。 场景复现 之前没用过gitee,火速去建了一个账号试验一下。 我在我的gitee中上传一张图片,在gitee本站里面显示是正...
继续阅读 »

今天一早打开微信,就看到国产github——gitee崩了。



Issue列表里面全是反馈图片显示异常,仔细一看,原来是图床的防盗链。


场景复现


之前没用过gitee,火速去建了一个账号试验一下。


我在我的gitee中上传一张图片,在gitee本站里面显示是正常的。


1-1.png


右键复制这张图片的地址,放到一个第三方的在线编辑器中,发现图片变成gitee的logo了



什么是防盗链


防盗链不是一根链条,正确的停顿应该是防·盗链——防止其他网站盗用我的链接。


我把图片上传到gitee的服务器,得到了图片的链接,然后拿着这个链接在第三方编辑器中使用,这就是在“盗用”——因为这张图片占用了gitee的服务器资源,却为第三方编辑器工作,gitee得不到好处,还得多花钱。


如何实现防盗链


要实现防盗链,就需要知道图片的请求是从哪里发出的。可以实现这一功能的有请求头中的originrefererorigin只有在XHR请求中才会带上,所以图片资源只能借助referer。其实gitee也确实是这么做的。


通过判断请求的referer,如果请求来源不是本站就返回302,重定向到gitee的logo上,最后在第三方网站引用存在gitee的资源就全变成它的logo了。


可以在开发者工具中看到第三方网站请求gitee图片的流程:



  1. 首先请求正常的图片,但是没有返回200,而是302重定向,其中响应头中的location就是要重定向去向的地址;

  2. 接着浏览器会自动请求这个location,并用这个返回结果代替第一次请求的返回内容;


最后,我们的图片在第三方网站就变成gitee的logo了。


如何破解防盗链


想让gitee不知道我在盗用,就不能让他发现请求的来源是第三方,只要把referer藏起来就好,可以在终端尝试这段代码:


curl 'https://images.gitee.com/uploads/images/2022/0326/155444_dc9923a4_10659337.jpeg' \
-o noReferer.jpg

这段👆代码的意思是请求这张jpg图片资源,把返回结果以noReferer.jpg这个名称保存在当前目录下,并且没有带上referer,测试结果是图片正常保存下来了。


就像加上了gitee本站的referer一样可以正常请求👇:


curl 'https://images.gitee.com/uploads/images/2022/0326/155444_dc9923a4_10659337.jpeg' \
-H 'referer: https://gitee.com' \
-o fromGitee.jpg

而在第三方网站请求的效果就像这段👇代码


curl 'https://images.gitee.com/uploads/images/2022/0326/155444_dc9923a4_10659337.jpeg' \
-H 'referer: https://editor.mdnice.com/' \
-o otherReferer.png

带上了第三方网站的标识https://editor.mdnice.com最终无法正常下载。


gitee做的不够完善吗


测试完上面的三段代码,不知道你会不会疑惑,gitee为什么不把“请求来源不能是第三方网站”的策略改成“请求来源必须是本站点”呢?换句话说,控制referer不能为空,只要是空就重定向。


因为在浏览器的地址栏中直接输入这个图片的url,然后回车,发起的请求是没有referer字段的,在这种场景下如果还是返回gitee的logo,就显得不太合理了。



图片的url:https://images.gitee.com/uploads/images/2022/0326/155444_dc9923a4_10659337.jpeg



图片看不到了,现在怎么办


如果你的个人搭建的博客里面用了很多存在gitee的图片,你可以在html的head部分加上这样一行


<meta name="referrer" content="no-referrer" />


或者


<img referrer="no-referrer|origin|unsafe-url" src="{item.src}"/>


来阻止请求因带上站点来源而被重定向成gitee的logo。


如果你是博客的访问者,可以借助一个chrome小插件ModHeader,把referer给“擦掉”



这样第三方站点就可以正常访问啦~


1-2.png


结语


上面提到的解决方式只是开个玩笑,临时恢复使用可以。但还是要慢慢把图片迁移到自己的服务器才最可靠。


作者:前端私教年年
来源:https://juejin.cn/post/7079705713781506079 收起阅读 »

【集成攻略】手把手教你环信对接离线推送,再搞不定把你头打掉

前提条件1.macOS系统,安装了xcode,并且配置好了cocoapods第三方管理工具2.有苹果开发者账号3.有环信开发者账号(注册地址:https://console.easemob.com/user/register)在苹果开发者中心创建项目,注册推送...
继续阅读 »

前提条件

1.macOS系统,安装了xcode,并且配置好了cocoapods第三方管理工具

2.有苹果开发者账号

3.有环信开发者账号

(注册地址:https://console.easemob.com/user/register


在苹果开发者中心创建项目,注册推送证书.

1.登录苹果开发者中心.

https://developer.apple.com/

(请用自己的苹果开发者账号)





2.苹果开发者中心创建 - Identifiers.

(name - empushdemo )

(identifier - com.yyytp.empushdemo )











3.钥匙串 - 从证书颁发机构请求证书

(本机证书)
















4.针对刚创建的bundle id开通并注册Certificates push 证书

(注册 可以在开发和生产双环境下使用的推送证书)













5.安装证书到本机,并导出 push - p12 

(这里需要格外注意操作步骤!不能展开!!!要闭合的状态导出!!!)

(因为申领的证书是双环境的,所以导出的p12文件直接复制成双份即可)

(开发证书名称 : yyytp_empush_apns_dev)

(生产证书名称 : yyytp_empush_apns_pro)

(密码 : 不告诉你)




==========
提示:解决证书不受信任的问题

如果在《钥匙串访问》中出现"证书不受信任"的警告时,可以去苹果官方网站下载G4证书,并双击打开即可

“证书不受信任”的图片样例


苹果官方网站链接:

https://www.apple.com/certificateauthority/

苹果官方网站需要下载的G4证书截图说明:


苹果官方解释:
苹果全球开发者关系中级证书的版本将于2023年2月7日到期,续订版本将于2030年2月20日到期。更新后的版本用于签署2021 1月28日之后颁发的新软件签名证书。剩余服务证书将于2022年1月27日更新。
为苹果平台开发的苹果开发者计划和苹果开发者企业计划的成员需要下载更新的证书,并遵循以下说明。
苹果开发者企业计划的成员需要在所有使用2020年9月1日之后生成的企业iOS分发证书进行代码签名的机器上安装续订的证书。
对于2021 1月28日之后生成的所有软件签名证书,由在Xcode中拥有个人帐户的开发人员和iOS大学开发人员计划成员提供的,也需要进行此更新。
新的中间证书由Xcode 11.4.1或更高版本自动下载,可在证书颁发机构页面上下载。通过验证过期日期设置为2030,确认安装了正确的中间证书。

注:本解决方案来自简书平台的博主AndyLiYL
原文链接:
https://www.jianshu.com/p/2697ed4f6e41

==========


后续补充:导出时必须使用[英文+数字+下划线]字符集内字符.不能使用中文和其他特殊符号







以上是在苹果开发者中心做了两件事

1.创建了bundleid为com.yyytp.empushdemo的app

2.创建推送证书 apns-2.cer 并导出了 (.p12) 证书,复制为2份,分别命名为 yyytp_empush_apns_dev yyytp_empush_apns_pro 密码是 123456

其中两份p12证书内容是完全一样的,只是命名不同,都适用于开发和生产环境,不过为了后期导入环信后台时方便辨识


===========分割线===========================


在环信console后台创建一个appkey,顺带创建一个测试username


1.登录环信console后台:https://console.easemob.com/user/login




2.创建appkey

(app_name : showpushdemo )





3.创建一个测试账号

(测试账号 : emtest 密码 1 )







========分割线=======================


在环信console后台中上传推送证书(.p12文件)

注意!!!是在刚才创建的appkey(1168171101115760#showpushdemo)下创建证书!!!

(这里需要注意的细节是:证书名不能有中文和其他特殊符号!!! 建议 字母 + 下划线)

(所以我会采用 yyytp_empush_apns_dev / yyytp_empush_apns_pro 这样的命名方式)





最终效果




=========分割线=======================


在代码中实现

1.创建项目

em_push_showdemo

2.集成环信SDK

pod 'HyphenateChat' , '3.9.0'




3.配置项目





4.代码部分如下:





下面代码是AppDelegate.m的所有代码,可直接复制粘贴

下面代码是AppDelegate.m的所有代码,可直接复制粘贴

下面代码是AppDelegate.m的所有代码,可直接复制粘贴


//
// AppDelegate.m
// em_push_showdemo
//
// Created by flower on 2022/3/14.
//

#import "AppDelegate.h"
#import
#import

@interface AppDelegate ()

@end

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
/*
1.注册环信SDK
2.注册推送
注册成功之后 绑定至环信SDK
3.登录账号
*/

[self _registerEMCHAT];
[self _registerSysPush];
[self _loginEMCHAT];
return YES;
}

- (void)_registerEMCHAT{
EMOptions *options = [EMOptions optionsWithAppkey:@"1168171101115760#showpushdemo"];
options.apnsCertName = @"yyytp_empush_apns_dev";
options.isAutoLogin = false;
options.usingHttpsOnly = true;
[EMClient.sharedClient initializeSDKWithOptions:options];
}

- (void)_registerSysPush{
[UNUserNotificationCenter.currentNotificationCenter
requestAuthorizationWithOptions:
UNAuthorizationOptionBadge|
UNAuthorizationOptionSound|
UNAuthorizationOptionAlert
completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (granted) {
dispatch_async(dispatch_get_main_queue(), ^{
[UIApplication.sharedApplication registerForRemoteNotifications];
});
}
}];
}

- (void)_loginEMCHAT{
[EMClient.sharedClient loginWithUsername:@"emtest" password:@"1" completion:^(NSString *aUsername, EMError *aError) {
if (aError) {
NSLog(@"登录失败");
}else{
NSLog(@"登录成功");
//下面这个updatePushDisplayStyle是设置显示效果,有两种显示效果可以设置.详情可查看枚举值(EMPushDisplayStyleSimpleBanner)的定义
[EMClient.sharedClient.pushManager updatePushDisplayStyle:EMPushDisplayStyleSimpleBanner completion:^(EMError * _Nonnull aError) {
}];
}
}];
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{
NSLog(@"绑定成功");
dispatch_async(dispatch_get_main_queue(), ^{
[EMClient.sharedClient bindDeviceToken:deviceToken];
});
}

- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error{
}



@end


4.运行至手机,运行完成后,退出APP,发送消息测试推送.

收起阅读 »

跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企业...
继续阅读 »

前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

如何开发一款易用的,并且可以扩展的空页面呢?那么今天我将带领大家手把手开发一款可扩展的空页面。

开发前注意事项

1、定义好空页面状态 2、可扩展思想(用抽象或基类替代实体) 3、抽离出空页面的结构

空页面展示

在这里插入图片描述

开始搭建

一、页面分析

空页面需要元素有:

  1. 展示图片
  2. 展示文案
  3. 展示刷新按钮

页面功能点:

  1. 文案可自定义
  2. 图片可自定义
  3. 按钮可隐藏

wiget作用范围:

  1. 可包裹其他widget
  2. 不包裹其他widget

二、定义状态

2.1 几种状态

enum EmptyStatus {
fail, //失败视图
loading, //加载视图
nodata, //没有数据视图
none //没有状态
}

没有状态该空页面就隐藏掉

2.2 空页面刷新回调

abstract class IEmptyRefresh{

void pressedReload();

}

2.3 定义copy类(复用做准备)&定义空接口(抽离要扩展的方法)

abstract class Copyable {
T copy();
}
abstract class IEmpty implements Copyable{
IEmptyRefresh? iEmptyRefresh;
Widget? diyImage; // 自定义图片替换
Widget? diyText;// 自定义文案替换
Widget? image();

Widget? text();

Widget? refresh();
}

2.4 空页面实现类

默认加载中页面

class DefaultLodingPage extends IEmpty{

@override
Widget? text() {
return diyText??Text(
LibEmptyManager.instance.libEmptyPageLoding,
style: TextStyle(fontSize: LibEmptyManager.instance.textSize, color: AppTheme.instance.textColor()),
);
}

@override
Widget? image() {
return null;
}

@override
Widget? refresh() => null;

@override
IEmpty copy() {
return DefaultLodingPage()
..diyImage = diyImage
..diyText = diyText
..iEmptyRefresh=iEmptyRefresh;
}


}
默认空页面

class DefaultEmptyPage extends IEmpty{

@override
Widget? text() {
return diyText??Text(
LibEmptyManager.instance.libEmptyPageNoData,
style: TextStyle(fontSize: LibEmptyManager.instance.textSize, color: AppTheme.instance.textColor()),
);
}

@override
Widget? image() {
return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: diyImage??Icon(LibEmptyManager.instance.imageNoData,color: AppTheme.instance.imageColor(),size: LibEmptyManager.instance.imageSize,),
);
}

@override
Widget? refresh() => null;

@override
IEmpty copy() {
return DefaultEmptyPage()
..diyImage = diyImage
..diyText = diyText
..iEmptyRefresh=iEmptyRefresh;;
}


}
默认网络失效页

class DefaultNetWorkError extends IEmpty {
@override
Widget? text() {
return diyText??Text(
LibEmptyManager.instance.libEmptyPageNetError,
style: TextStyle(fontSize: LibEmptyManager.instance.textSize, color: AppTheme.instance.textColor()),
);
}

@override
Widget? image() {
return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: diyImage??Icon(LibEmptyManager.instance.imageNetWork,color: AppTheme.instance.imageColor(),size: LibEmptyManager.instance.imageSize,),
);
}

@override
Widget? refresh() {
return Padding(
padding: const EdgeInsets.only(top: 20),
child: Padding(
padding: const EdgeInsets.only(left: 20,right: 20),
child: ElevatedButton(onPressed: () => iEmptyRefresh?.pressedReload(),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(AppTheme.instance.btnBackColor()),
shape: MaterialStateProperty.all(const StadiumBorder()),
)
, child: Text(LibEmptyManager.instance.libRefresh,style: TextStyle(fontSize: LibEmptyManager.instance.libRefreshSize,color: AppTheme.instance.btnTextColor())),),
),
);
}

@override
IEmpty copy() {
return DefaultNetWorkError()
..diyImage = diyImage
..diyText = diyText
..iEmptyRefresh=iEmptyRefresh;;
}
}

2.5 空页面管理类

可进行外部配置


class LibEmptyManager{
IEmpty emptyPage = DefaultEmptyPage();
IEmpty loadingPage = DefaultLodingPage();
IEmpty netWorkError = DefaultNetWorkError();

late LibEmptyConfig libEmptyConfig;

LibEmptyManager._();

static final LibEmptyManager _instance = LibEmptyManager._();

static LibEmptyManager get instance {
return _instance;
}

2.6 核心逻辑

判断状态,并进行类型拷贝,并增加自定义参数

switch(widget.layoutType){
case EmptyStatus.none:
visable = true;
break;
// return widget.child;
case EmptyStatus.fail:
iEmpty = LibEmptyManager.instance.netWorkError.copy()
..diyText = widget.networkText
..diyImage = widget.networkImage
;
break;
case EmptyStatus.nodata:
iEmpty = LibEmptyManager.instance.emptyPage.copy()
..diyText = widget.emptyText
..diyImage = widget.emptyImage
;
break;
case EmptyStatus.loading:
iEmpty = LibEmptyManager.instance.loadingPage;
break;
default:
iEmpty = LibEmptyManager.instance.emptyPage.copy()
..diyText = widget.emptyText
..diyImage = widget.emptyImage
;
}

如果是包裹类型需要stack进行包装

return Stack(
children: [
Offstage(
offstage: !visable,
child: widget.child,
),
Offstage(
offstage: visable,
child: Container(
width: double.infinity,
color: AppTheme.instance.backgroundColor(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: _listEmpty(iEmpty),
),
),),
],
);

判断是否有网,有网的话,就刷新,没网的话,就提示


@override
void pressedReload() async
{
bool isConnectNetWork = await isConnected();
if(isConnectNetWork){
widget.refresh.call();
}else{
TipToast.instance.tip(LibLocalizations.getLibString().libNetWorkNoConnect!,tipType: TipType.error);
}
}

// 是否有网
Future isConnected() async {
var connectivityResult = await (Connectivity().checkConnectivity());
return connectivityResult != ConnectivityResult.none;
}

组装empty


List _listEmpty(IEmpty? iEmpty) {
List tempEmpty = [];
if(iEmpty!=null){
Widget? image = iEmpty.image();
Widget? text = iEmpty.text();
Widget? refresh = iEmpty.refresh();
if(image!=null){
tempEmpty.add(image);
}
if(text!=null){
tempEmpty.add(text);
}
if(refresh!=null){
tempEmpty.add(refresh);
}

}
return tempEmpty;
}

三、空页面widget实现完整代码

class LibEmptyView extends StatefulWidget{
Widget? child;
EmptyStatus layoutType;
VoidCallback refresh;


Widget? networkImage;Widget? networkText;
Widget? emptyImage;Widget? emptyText;

LibEmptyView({Key? key, this.child,required this.refresh,required this.layoutType,this.networkImage,this.networkText, this.emptyImage,this.emptyText}) : super(key: key);

@override
State createState() => _LibEmptyViewState();

}

class _LibEmptyViewState extends State implements IEmptyRefresh{
//控制器

@override
Widget build(BuildContext context) {
IEmpty? iEmpty;
bool visable = false;
switch(widget.layoutType){
case EmptyStatus.none:
visable = true;
break;
case EmptyStatus.fail:
iEmpty = LibEmptyManager.instance.netWorkError.copy()
..diyText = widget.networkText
..diyImage = widget.networkImage
;
break;
case EmptyStatus.nodata:
iEmpty = LibEmptyManager.instance.emptyPage.copy()
..diyText = widget.emptyText
..diyImage = widget.emptyImage
;
break;
case EmptyStatus.loading:
iEmpty = LibEmptyManager.instance.loadingPage;
break;
default:
iEmpty = LibEmptyManager.instance.emptyPage.copy()
..diyText = widget.emptyText
..diyImage = widget.emptyImage
;
}
iEmpty?.iEmptyRefresh = this;



return Stack(
children: [
Offstage(
offstage: !visable,
child: widget.child,
),
Offstage(
offstage: visable,
child: Container(
width: double.infinity,
color: AppTheme.instance.backgroundColor(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: _listEmpty(iEmpty),
),
),),
],
);
}

@override
void pressedReload() async{
bool isConnectNetWork = await isConnected();
if(isConnectNetWork){
widget.refresh.call();
}else{
TipToast.instance.tip(LibLocalizations.getLibString().libNetWorkNoConnect!,tipType: TipType.error);
}
}


// 是否有网
Future isConnected() async {
var connectivityResult = await (Connectivity().checkConnectivity());
return connectivityResult != ConnectivityResult.none;
}
}

List _listEmpty(IEmpty? iEmpty) {
List tempEmpty = [];
if(iEmpty!=null){
Widget? image = iEmpty.image();
Widget? text = iEmpty.text();
Widget? refresh = iEmpty.refresh();
if(image!=null){
tempEmpty.add(image);
}
if(text!=null){
tempEmpty.add(text);
}
if(refresh!=null){
tempEmpty.add(refresh);
}

}
return tempEmpty;
}

四、空页面widget使用代码

包裹使用 (代码中的webview封装参见:跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview

LibEmptyView(
layoutType: status,
refresh: () {

status = EmptyStatus.none;
_innerWebPageController.reload();

},

child: InnerWebPage(widget.url,titleCallBack: (title){
setState(() {
urlTitle = title;
});
},javascriptChannels: widget._javascriptChannels,urlIntercept: widget._urlIntercept,onInnerWebPageCreated: (innerWebPageController){
_innerWebPageController = innerWebPageController;
widget._javascriptChannels?.webPageCallBack = webPageCallBack;
widget._urlIntercept?.webPageCallBack = webPageCallBack;
},onWebResourceError: (error){
setState(() {
status = EmptyStatus.fail;
});
},),
),
));

非包裹使用

if(_status == EmptyStatus.none){
return _listViewUi.call(_allReportItems);
}else{
var empty = LibEmptyView(
layoutType: _status,
refresh: () {
_status = EmptyStatus.loading;
LibLoading.show();
_refreshCenter.refreshData();
},networkImage: networkImage,networkText: networkText,emptyImage: emptyImage,emptyText: emptyText,);
if(builder!=null){
return builder.call(context,empty);
}else{
return empty;
}
}

感谢大家阅读我的文章

收起阅读 »

Flutter桌面端开发——复制和粘贴内容

复制和粘贴这个功能,一般系统都自带,简单的按几个键就能完成。但是有时候我们想要自己在应用中集成这个功能,或者想在用户复制文字后不使用粘贴操作,就让复制的内容直接出现在我们的应用中。想要实现该功能,就可以用我今天介绍的几个插件。 screen_capturer ...
继续阅读 »

复制和粘贴这个功能,一般系统都自带,简单的按几个键就能完成。但是有时候我们想要自己在应用中集成这个功能,或者想在用户复制文字后不使用粘贴操作,就让复制的内容直接出现在我们的应用中。想要实现该功能,就可以用我今天介绍的几个插件。


screen_capturer


这个方法是用来截取屏幕的。本来想写一期介绍截屏插件的,但是找了一圈只找到这个适用于桌面端,只写这一个插件篇幅又太短,所以直接加了进来。


安装🛠


点击screen_capturer获取最新版本。以下是在编写本文章时的最新版本:


screen_capturer: ^0.1.0

使用🥟


该插件的主体是 ScreenCapturer.instance ,其下有3个方法:isAccessAllowed 、requestAccess 和 capture。


isAccessAllowed 和 requestAccess仅在 macOS中适用,分别用来检测和申请截图的权限。


await ScreenCapturer.instance.requestAccess();  // 申请权限
bool _isAllowed = await ScreenCapturer.instance.isAccessAllowed (); // 检测是否拥有权限

我们截图的目的是把图片显示出来,所以在正式截图前先定义个图片路径的参数:


String _image;

接下来介绍截图的主要方法 capture,该方法可以传递2个参数:



  • String? imagePath:该属性为必填,传递一个图片保存的路径和名称

  • bool silent:设置是否开启截屏的提示音


String _imageName = '${DateTime.now().millisecondsSinceEpoch}.png';  // 设置图片名称
String _imagePath = 'C:\\Users\\ilgnefz\\Pictures\\$_imageName'; // 设置图片保存的路径
CapturedData? _capturedData = await ScreenCapturer.instance.capture(imagePath: _imagePath);
if (_capturedData != null) {
_image = _capturedData.imagePath;
setState(() {});
} else {
BotToast.showText(text: '截图被取消了');
}

1


通过运行可以发现,这里调用的其实是系统的截图功能,然后将截取的图片进行了保存。和windows自带的截图又有些差别,自带的只会将截图保存到剪切板中。


screen_text_extractor


安装🛠


点击screen_text_extractor获取最新版本。以下是在编写本文章时的最新版本:


screen_text_extractor: ^0.1.0

使用🥟


该插件的主体是 ScreenTextExtractor.instance ,其下有7个方法:



  1. isAccessAllowed:检测是否有进行相关操作的权限,仅macOS

  2. requestAccess:申请进行相关操作的权限,仅macOS

  3. extractFromClipboard:从剪切板提取内容

  4. extractFromScreenSelection:从选择的屏幕提取

  5. simulateCtrlCKeyPress:模拟 Ctrl + c ,返回一个布尔值


前面4个仅macOS使用的方法和screen_capturer一样,这里就不多赘述。后面3个方法将会返回一个 Future 的 ExtractedData 对象。


我们先定义一个String对象用来显示获取到的内容:


String _text = '获取的内容将会在这里🤪';

获取剪切板内容


ExtractedData data = await ScreenTextExtractor.instance.extractFromClipboard();
_text = data.text;
setState((){});

我们先在 windows 中按 windows键 + v 来调出剪切板,清空


无标题


使用该方法看一下:


1


我们将会得到一个空白的内容。为了更好的用户体验,我们可以添加个条件。


if (data.text!.isEmpty) {
BotToast.showText(text: '剪切板什么都没有🤨');
} else {
_text = data.text!;
setState(() {});
}

我们现在复制一段内容:


无标题


然后看看效果:


2


获取成功😀,但是剪切板除了能存储文本,还是能存储图片的。


1


但是ExtractedData只有个text属性,我们来看下会发生什么:


3


直接为空了😶


获取选区内容


ExtractedData data = await ScreenTextExtractor.instance.extractFromScreenSelection(
useAccessibilityAPIFirst: false, // 使用辅助功能API,仅macOS
);
if (data.text!.isEmpty) {
BotToast.showText(text: '剪切板什么都没有🤨');
} else {
_text = data.text!;
setState(() {});
}

👻该方法如果是在windows端,返回的就是extractFromClipboard()方法的结果,在macOS端和Linux端暂时无法演示😪


pasteboard


安装🛠


点击pasteboard获取最新版本。以下是在编写本文章时的最新版本:


pasteboard: ^0.0.2

使用🥟


该插件中的 Pasteboard 对象一共拥有4个方法:



  • image:复制图片

  • file:复制文件/文本

  • writeImage:粘贴图片

  • writeFile:粘贴文件/文本


复制粘贴文本


当然,第一步先定义一个用来存储结果的变量:


String _text = '还没粘贴任何内容';

定义一个文本控制器,用来获取输入的内容:


final TextEditingController _controller = TextEditingController();

接下来使用 pasteboard 来实现复制和粘贴的功能:




  • 复制文本


    void _copyText() async {
    if (_controller.text.isEmpty) {
    BotToast.showText(text: '啥都没输入,你要我复制什么🥴');
    } else {
    final lines = const LineSplitter().convert(_controller.text);
    await Pasteboard.writeFiles(lines);
    }
    }



  • 粘贴文本


    void _pastText() async {
    final results = await Pasteboard.files();
    if (results.isNotEmpty) {
    _text = result.toString();
    setState(() {});
    } else {
    BotToast.showText(text: '我什么都不能给你,因为我也咩有😭');
    }
    }



我们先来试一下,不用复制直接直接粘贴会发生什么。此时我的剪切板有一条内容:


无标题


来看看效果:


1


我们可以发现,它并不能读取我们剪切板的内容。试下复制再粘贴:


2


通过测试可以知道,最终的结果是一个数组。我们再来看看剪切板有没有记录:


无标题


这里其实用的是上面同一张图,因为没有变化所以就没再截图了。


通过以上内容,我们可以发现,pasteboard 的复制粘贴是和系统隔开的。


复制粘贴文件


其实代码可以不用修改,但是为了更好的显示,我们还是修改以下:


void _pastText() async {
final results = await Pasteboard.files();
if (results.isNotEmpty) {
_text = '';
for (final result in results) {
_text += '$result\n';
}
setState(() {});
} else {
BotToast.showText(text: '我什么都不能给你,因为我也咩有😭');
}
}

在这里,我使用了 url_launcher 插件,用来打开系统的文件浏览器。代码如下:


void _openExplorer() async {
const _filePath = r'C:\Users\ilgnefz\Pictures';
final Uri _uri = Uri.file(_filePath);
await launch(_uri.toString());
}

来看看效果:


image


图片本质上也是文件,可以直接使用上面的方法进行复制粘贴。所以关于图片的方法就不讲解了


(🤫ps: 其实是我使用官方例子的方法,用Base64图片进行测试,发现无法得到想要的结果。使用了官方的例子也是一样。复制图片的方法需要传递一个Uint8List参数,虽然可以使用其他方法转换,但是就变得麻烦了。以后我会出一篇关于用 CustomPaint 绘制图片的文章,里面会用到将图片转换成Uint8List对象的方法)。


clipboard


安装🛠


点击clipboard获取最新版本。以下是在编写本文章时的最新版本:


clipboard: ^0.1.3

使用🥟


该插件拥有4个方法:



  • controlC:模仿 cttr + c 键,复制

  • controlC:模仿 cttr + v 键,粘贴

  • copy:复制

  • paste:粘贴


先来看看前面两个方法:


void _useCtrC() async {
if (_controller.text.isEmpty) {
BotToast.showText(text: '啥都没输入,你要我复制什么🥴');
} else {
await FlutterClipboard.controlC(_controller.text);
}
}

void _useCtrV() async {
ClipboardData result = await FlutterClipboard.controlV();
_text = result.text.toString();
setState(() {});
}

使用 controlV 会返回一个 ClipboardData 对象。


4


后面两个方法和前面的唯一不同,就是返回的是一个 String 对象:


void _useCopy() async {
if (_controller.text.isEmpty) {
BotToast.showText(text: '啥都没输入,你要我复制什么🥴');
} else {
await FlutterClipboard.copy(_controller.text);
}
}

void _usePaste() async {
_text = await FlutterClipboard.paste();
setState(() {});
}

5


我们打开系统的剪切板可以发现,以上复制的内容都会被记录。我们试一下不按复制看能不能直接读取剪切板的信息进行粘贴:


6


试试 paste 方法:


7


🛫OK,以上就是这篇文章的全部内容,仅针对插件的当前版本,并不能保证适用于以后插件用法的更新迭代。


最后,感谢 leanflutterMixin Network 两个团队还有 samuelezedi 对以上插件的开发和维护😁。本应用代码已上传至 githubgitee,有需要的可以下载下来查看学习。


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

Flutter好用的轮子推荐03:一套精美实用的内置动画弹窗组件

前言 Flutter 是 Google 开源的应用开发框架,仅通过一套代码就能构建支持Android、iOS、Windows、Linux等多平台的应用。Flutter的性能非常高,拥有120fps的刷新率,也是目前非常流行的跨平台UI开发框架。 本专栏为大家收...
继续阅读 »

前言


Flutter 是 Google 开源的应用开发框架,仅通过一套代码就能构建支持Android、iOS、Windows、Linux等多平台的应用。Flutter的性能非常高,拥有120fps的刷新率,也是目前非常流行的跨平台UI开发框架。


本专栏为大家收集了Github上近70个优秀开源库,后续也将持续更新。希望可以帮助大家提升搬砖效率,同时祝愿Flutter的生态越来越完善🎉🎉。


正文


一、🚀 轮子介绍



  • 名称:awesome_dialog

  • 概述:一个简单易用的内置动画弹窗

  • 出版商:marcos930807@gmail.com

  • 仓库地址:awesomeDialogs

  • 推荐指数: ⭐️⭐️⭐️⭐️

  • 常用指数: ⭐️⭐️⭐️⭐️

  • 效果预览:


gif.gif


二、⚙️ 安装及使用


dependencies:
awesome_dialog: ^2.1.2

import 'package:awesome_dialog/awesome_dialog.dart';

三、🔧 常用属性






















































































































































































属性类型描述
dialogTypeDialogType设置弹窗类型
customHeaderWidget设置自定义标题(如果设置了,DiaologType将被忽略。)
widthdouble弹窗最大宽度
titleString弹窗标题
descString弹窗描述文本
bodyWidget弹窗主体,如果设置了此属性,标题和描述将被忽略。
contextBuildContext@required
btnOkTextString确认按钮的文本
btnOkIconIconData确认按钮的图标
btnOkOnPressFunction确认按钮事件
btnOkColorColor确认按钮颜色
btnOkWidget创建自定义按钮,以上确认按钮相关属性将被忽略
btnCancelTextString取消按钮的文本
btnCancelIconIconData取消按钮的图标
btnCancelOnPressFunction取消按钮事件
btnCancelColorColor取消按钮颜色
btnCancelWidget创建自定义按钮,以上取消按钮相关属性将被忽略
buttonsBorderRadiusBorderRadiusGeometry按钮圆角
dismissOnTouchOutsidebool点击外部消失
onDissmissCallbackFunction弹窗关闭回调
animTypeAnimType动画类型
aligmentAlignmentGeometry弹出方式
useRootNavigatorbool使用根导航控制器而不是当前根导航控制器,可处理跨界面关闭弹窗。
headerAnimationLoopbool标题动画是否循环播放
paddingEdgeInsetsGeometry弹窗内边距
autoHideDuration自动隐藏时间
keyboardAwarebool键盘弹出内容被遮挡时是否跟随移动
dismissOnBackKeyPressbool控制弹窗是否可以通过关闭按钮消失
buttonsBorderRadiusBorderRadiusGeometry按钮圆角
buttonsTextStyleTextStyle按钮文字风格
showCloseIconbool是否显示关闭按钮
closeIconWidget关闭按钮图标
dialogBackgroundColorColor弹窗背景色
borderSideBorderSide整个弹窗形状

四、🗂 示例


1.带有点击动画的按钮


animatedButton-2.gif


AnimatedButton(
color: Colors.cyan,
text: '这是一个带有点击动画的按钮',
pressEvent: () {},
)

2.固定宽度并带有确认 / 取消按钮的提示框


fixedWidthAndButtons.gif


AnimatedButton(
text: '固定宽度并带有确认 / 取消按钮的提示框',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.INFO_REVERSED,
borderSide: const BorderSide(
color: Colors.green,
width: 2,
),
width: 380,
buttonsBorderRadius: const BorderRadius.all(
Radius.circular(2),
),
btnCancelText: '不予理会',
btnOkText: '冲啊!',
headerAnimationLoop: false,
animType: AnimType.BOTTOMSLIDE,
title: '提示',
desc: '一个1级bug向你发起挑衅,是否迎战?',
showCloseIcon: true,
btnCancelOnPress: () {},
btnOkOnPress: () {},
).show();
});

3.自定义按钮样式的问题对话框


questionDialogWithCustomButtons.gif


AnimatedButton(
color: Colors.orange[700],
text: '具有自定义按钮样式的问题对话框',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.QUESTION,
headerAnimationLoop: false,
animType: AnimType.BOTTOMSLIDE,
title: '触发额外剧情',
desc: '发现一名晕倒在草丛的路人,你会?',
buttonsTextStyle: const TextStyle(color: Colors.black),
btnCancelText: '拿走他的钱袋',
btnOkText: '救助',
showCloseIcon: true,
btnCancelOnPress: () {},
btnOkOnPress: () {},
).show();
});

4.无按钮的信息提示框


noHeaderDialog.gif


AnimatedButton(
color: Colors.grey,
text: '无按钮的信息提示框',
pressEvent: () {
AwesomeDialog(
context: context,
headerAnimationLoop: true,
animType: AnimType.BOTTOMSLIDE,
title: '提示',
desc:
'你救下路人,意外发现他是一位精通Flutter的满级大佬,大佬为了向你表示感谢,赠送你了全套Flutter的学习资料...',
).show();
});

5.警示框


warningDialog.gif


AnimatedButton(
color: Colors.orange,
text: '警示框',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.WARNING,
headerAnimationLoop: false,
animType: AnimType.TOPSLIDE,
showCloseIcon: true,
closeIcon: const Icon(Icons.close_fullscreen_outlined),
title: '警告',
desc: '意外发现bug的窝点,你准备?',
btnCancelOnPress: () {},
onDissmissCallback: (type) {
debugPrint('Dialog Dissmiss from callback $type');
},
btnCancelText: '暂且撤退',
btnOkText: '发起战斗',
btnOkOnPress: () {},
).show();
});

6.错误提示框


errorDialog.gif


AnimatedButton(
color: Colors.red,
text: '错误提示框',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.ERROR,
animType: AnimType.RIGHSLIDE,
headerAnimationLoop: true,
title: '挑战失败',
desc: '你寡不敌众,败下阵来,(回到出生点后,拿出大佬赠送的全套学习资料,立志学成后报仇血恨... )',
btnOkOnPress: () {},
btnOkIcon: Icons.cancel,
btnOkColor: Colors.red,
).show();
});

7.成功提示框


successDialog.gif


AnimatedButton(
color: Colors.green,
text: '成功提示框',
pressEvent: () {
AwesomeDialog(
context: context,
animType: AnimType.LEFTSLIDE,
headerAnimationLoop: false,
dialogType: DialogType.SUCCES,
showCloseIcon: true,
title: '挑战成功',
desc: '经过三天三夜的苦战,你成功消灭了所有的bug',
btnOkOnPress: () {
debugPrint('OnClcik');
},
btnOkIcon: Icons.check_circle,
onDissmissCallback: (type) {
debugPrint('Dialog Dissmiss from callback $type');
},
).show();
});

8.不带顶部动画的弹窗


noHeaderDialog.gif


AnimatedButton(
color: Colors.cyan,
text: '不带顶部动画的弹窗',
pressEvent: () {
AwesomeDialog(
context: context,
headerAnimationLoop: false,
dialogType: DialogType.NO_HEADER,
title: 'No Header',
desc:'Dialog description here...',
btnOkOnPress: () {
debugPrint('OnClcik');
},
btnOkIcon: Icons.check_circle,
).show();
});

9.自定义内容弹窗


customBodyDialog.gif


AnimatedButton(
color: Colors.purple,
text: '自定义内容弹窗',
pressEvent: () {
AwesomeDialog(
context: context,
animType: AnimType.SCALE,
dialogType: DialogType.INFO,
body: const Center(
child: Text(
'If the body is specified, then title and description will be ignored, this allows to further customize the dialogue.',
style: TextStyle(fontStyle: FontStyle.italic),
),
),
title: 'This is Ignored',
desc: 'This is also Ignored',
).show();
});

10.自动隐藏弹窗


autoHideDialog.gif


AnimatedButton(
color: Colors.grey,
text: '自动隐藏弹窗',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.INFO,
animType: AnimType.SCALE,
title: 'Auto Hide Dialog',
desc: 'AutoHide after 2 seconds',
autoHide: const Duration(seconds: 2),
).show();
});

11.测试弹窗


testingDialog.gif


AnimatedButton(
color: Colors.blue,
text: '测试弹窗',
pressEvent: () {
AwesomeDialog(
context: context,
keyboardAware: true,
dismissOnBackKeyPress: false,
dialogType: DialogType.WARNING,
animType: AnimType.BOTTOMSLIDE,
btnCancelText: "Cancel Order",
btnOkText: "Yes, I will pay",
title: 'Continue to pay?',
desc:'Please confirm that you will pay 3000 INR within 30 mins. Creating orders without paying will create penalty charges, and your account may be disabled.',
btnCancelOnPress: () {},
btnOkOnPress: () {},
).show();
});

12.文本输入弹窗


bodyWithInput.gif


AnimatedButton(
color: Colors.blueGrey,
text: '带有文本输入框的弹窗',
pressEvent: () {
late AwesomeDialog dialog;
dialog = AwesomeDialog(
context: context,
animType: AnimType.SCALE,
dialogType: DialogType.INFO,
keyboardAware: true,
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
Text('Form Data',
style: Theme.of(context).textTheme.headline6,),
const SizedBox(height: 10,),
Material(
elevation: 0,
color: Colors.blueGrey.withAlpha(40),
child: TextFormField(
autofocus: true,
minLines: 1,
decoration: const InputDecoration(
border: InputBorder.none,
labelText: 'Title',
prefixIcon: Icon(Icons.text_fields),
),
),
),
const SizedBox(height: 10,),
Material(
elevation: 0,
color: Colors.blueGrey.withAlpha(40),
child: TextFormField(
autofocus: true,
keyboardType: TextInputType.multiline,
minLines: 2,
maxLines: null,
decoration: const InputDecoration(
border: InputBorder.none,
labelText: 'Description',
prefixIcon: Icon(Icons.text_fields),
),
),
),
const SizedBox(height: 10,),
AnimatedButton(
isFixedHeight: false,
text: 'Close',
pressEvent: () {
dialog.dismiss();
},
)
],),
),
)..show();
});

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

从0-1背包问题看动态规划的几种方式

0-1 knapsack Problem Statement Given the weights and profits of ‘N’ items, we are asked to put these items in a knapsack that has ...
继续阅读 »

0-1 knapsack


Problem Statement


Given the weights and profits of ‘N’ items, we are asked to put these items in a knapsack that has a capacity ‘C’. The goal is to get the maximum profit from the items in the knapsack. Each item can only be selected once, as we don’t have multiple quantities of any item.


Items: { Apple, Orange, Banana, Melon }
Weights: { 2, 3, 1, 4 }
Profits: { 4, 5, 3, 7 }
Knapsack capacity: 5
Output: 10

暴力解法


每个 Item 都可以放或者不放,可以对每个元素一次进行递归


export function bruteforce(
profits: number[],
weights: number[],
capacity: number
) {
const recursive = (i: number, c: number, p: number): number => {
if (i >= profits.length) return p;

return Math.max(
c + weights[i] > capacity
? p
: recursive(i + 1, c + weights[i], p + profits[i]),
recursive(i + 1, c, p)
);
};

const ans = recursive(0, 0, 0);
return ans;
}

时间复杂度:O(2n)


空间复杂度:O(n)


Top-Down DP


稍微变一个 recursive 的逻辑,recursive 函数表示,把 0~i 范围内的 Item 装入 capacity 为 c 的背包中,能够获得的最大的 profit


image.png


可以看到有重复的分支,我们需要做的就是在这个基础上记忆


export function topDown(
profits: number[],
weights: number[],
capacity: number
) {
const dp: Record<string, number> = {};

const recursive = (i: number, c: number, p: number): number => {
if (i >= profits.length) return p;

const key = `${i}_${c}`;

if (dp[key]) {
return dp[key];
}

const ans = Math.max(
c - weights[i] >= 0
? recursive(i + 1, c - weights[i], p + profits[i])
: p,
recursive(i + 1, c, p)
);

return (dp[key] = ans);
};

const ans = recursive(0, capacity, 0);
return ans;
}

时间复杂度:O(n * c),因为记忆化后,最多有 n * c 个子问题


空间复杂度:O(n * c + n)


Bottom-Up DP


dp 记录对于前 i 个元素,当 capacity 为 c 时,能获得的最大 profit,那么


dp[i][c] = max (
// 不取当前元素
dp[i-1][c],
// 取当前元素
profits[i] + dp[i-1][c-weights[i]]
)

图解如下:


image.png


export function bottomUp(
profits: number[],
weights: number[],
capacity: number
) {
const N = profits.length;
const dp: number[][] = new Array(N)
.fill(0)
.map(() => new Array(capacity + 1).fill(0));

// 初始化
for (let i = 0; i < N; i++) dp[i][0] = 0;
for (let c = 0; c <= capacity; c++) {
if (weights[0] <= c) dp[0][c] = profits[0];
}

// dp
for (let i = 1; i < N; i++) {
for (let c = 1; c <= capacity; c++) {
dp[i][c] = Math.max(
dp[i - 1][c],
c < weights[i] ? 0 : profits[i] + dp[i - 1][c - weights[i]]
);
}
}

return dp[N - 1][capacity];
}

时间复杂度:O(n * c)


空间复杂度:O(n * c)


Bottom-Up DP 优化


在计算第 i 个元素的过程中,只需要用到前一次的 dp[c] and dp[c-weight[i]] ,所以,在空间复杂度上我们可以做优化,可以使用同一个数组进行前后两次的记忆


如果 capacity 从 c ~ 0 ,而不是 0 ~ c 循环,去修改 dp[i][c ~ capacity],可以确保 dp[i][0 ~ c-1] 的值是前一次的,但是,如果按照之前的反过来,计算 dp[i][c] 的时候,dp[i][0 ~ c-1] 已经变成第 i 次的值在存储,所以行不通


export function bottomUp2(
profits: number[],
weights: number[],
capacity: number
) {
const N = profits.length;
const dp: number[] = new Array(capacity + 1).fill(0);

// 初始化
for (let c = 0; c <= capacity; c++) {
if (weights[0] <= c) dp[c] = profits[0];
}

// dp
for (let i = 1; i < N; i++) {
for (let c = capacity; c >= 0; c--) {
dp[c] = Math.max(
dp[c],
c < weights[i] ? 0 : profits[i] + dp[c - weights[i]]
);
}
}

return dp[capacity];
}

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

七大跨域解决方法原理

前言 大家好,我是林三心。用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初衷。 咱们做前端的,平时跟后端对接接口那是必须的事情,但是可能很多同学忽略了一个对接过程中可能会发生的问题——跨域,那跨域到底是啥呢?为什么会跨域呢?又怎么才能解决呢...
继续阅读 »

前言


大家好,我是林三心。用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初衷。


咱们做前端的,平时跟后端对接接口那是必须的事情,但是可能很多同学忽略了一个对接过程中可能会发生的问题——跨域,那跨域到底是啥呢?为什么会跨域呢?又怎么才能解决呢?


截屏2021-10-01 上午7.16.06.png


为什么跨域?


image.png


为什么会出现跨域问题呢?那就不得不讲浏览器的同源策略了,它规定了协议号-域名-端口号这三者必须都相同才符合同源策略


截屏2021-10-01 上午8.50.11.png


如有有一个不相同,就会出现跨域问题,不符合同源策略导致的后果有



  • 1、LocalStorge、SessionStorge、Cookie等浏览器内存无法跨域访问

  • 2、DOM节点无法跨域操作

  • 3、Ajax请求无法跨域请求


注意点:一个IP是可以注册多个不同域名的,也就是多个域名可能指向同一个IP,即使是这样,他们也不符合同源策略


截屏2021-10-01 上午9.02.55.png


跨域的时机?


跨域发生在什么时候呢?我考过很多位同学,得到了两种答案



  • 1、请求一发出就被浏览器的跨域报错拦下来了(大多数人回答)

  • 2、请求发出去到后端,后端返回数据,在浏览器接收后端数据时被浏览器的跨域报错拦下来


那到底是哪种呢?我们可以验证下,咱们先npm i nodemon -g,然后创建一个index.js,然后nodemon index起一个node服务


// index.js  http://127.0.0.1:8000

const http = require('http');

const port = 8000;

http.createServer(function (req, res) {
const { query } = urllib.parse(req.url, true);
console.log(query.name)
console.log('到后端喽')
res.end(JSON.stringify('林三心'));
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

再创建一个index.html,用来写前端的请求代码,咱们就写一个简单的AJAX请求


// index.html  http://127.0.0.1:5500/index.html
<script>
//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数
ajax.open('get', 'http://127.0.0.1:8000?name=前端过来的林三心');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
console.log(ajax.responseText);//输入相应的内容
}
}

</script>
复制代码

截屏2021-10-01 下午1.37.01.png


最终,前端确实是跨域报错了。但这不是结果,我们要想知道是哪一个答案,关键在于看后端的node服务那里有没有输出,就一目了然了。所以,答案2才是对的。


截屏2021-10-01 下午1.38.52.png


截屏2021-10-01 下午1.41.51.png


同域情况 && 跨域情况?


前面提到了同源策略,满足协议号-域名-端口号这三者都相同就是同域,反之就是跨域,会导致跨域报错,下面通过几个例子让大家巩固一下对同域和跨域的认识把!


截屏2021-10-01 上午9.24.38.png


解决跨域的方案


跨域其实是一个很久的问题了,对应的解决方案也有很多,一起接着往下读吧!!!


JSONP


前面咱们说了,因为浏览器同源策略的存在,导致存在跨域问题,那有没有不受跨域问题所束缚的东西呢?其实是有的,以下这三个标签加载资源路径是不受束缚的



  • 1、script标签:<script src="加载资源路径"></script>

  • 2、link标签:<link herf="加载资源路径"></link>

  • 3、img标签:<img src="加载资源路径"></img>


而JSONP就是利用了scriptsrc加载不受束缚,从而可以拥有从不同的域拿到数据的能力。但是JSONP需要前端后端配合,才能实现最终的跨域获取数据


JSONP通俗点说就是:利用script的src去发送请求,将一个方法名callback传给后端,后端拿到这个方法名,将所需数据,通过字符串拼接成新的字符串callback(所需数据),并发送到前端,前端接收到这个字符串之后,就会自动执行方法callback(所需数据)。老规矩,先上图,再上代码。


截屏2021-10-01 下午1.22.08.png


后端代码


// index.js  http://127.0.0.1:8000

const http = require('http');
const urllib = require('url');

const port = 8000;

http.createServer(function (req, res) {
const { query } = urllib.parse(req.url, true);
if (query && query.callback) {
const { name, age, callback } = query
const person = `${name}今年${age}岁啦!!!`
const str = `${callback}(${JSON.stringify(person)})` // 拼成callback(data)
res.end(str);
} else {
res.end(JSON.stringify('没东西啊你'));
}
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500/index.html

const jsonp = (url, params, cbName) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
window[cbName] = (data) => {
resolve(data)
document.body.removeChild(script)
}
params = { ...params, callback: cbName }
const arr = Object.keys(params).map(key => `${key}=${params[key]}`)
script.src = `${url}?${arr.join('&')}`
document.body.appendChild(script)
})
}

jsonp('http://127.0.0.1:8000', { name: '林三心', age: 23 }, 'callback').then(data => {
console.log(data) // 林三心今年23岁啦!!!
})
复制代码

截屏2021-10-01 下午1.47.29.png



JSONP的缺点就是,需要前后端配合,并且只支持get请求方法



WebSocket


WebSocket是什么东西?其实我也不怎么懂,但是我也不会像别人一样把MDN的资料直接复制过来,因为复制过来相信大家也是看不懂的。


我理解的WebSocket是一种协议(跟http同级,都是协议),并且他可以进行跨域通信,为什么他支持跨域通信呢?我这里找到一篇文章WebSocket凭啥可以跨域?,讲的挺好


截屏2021-10-01 下午10.02.39.png


后端代码


先安装npm i ws


// index.js  http://127.0.0.1:8000
const Websocket = require('ws');

const port = 8000;
const ws = new Websocket.Server({ port })
ws.on('connection', (obj) => {
obj.on('message', (data) => {
data = JSON.parse(data.toString())
const { name, age } = data
obj.send(`${name}今年${age}岁啦!!!`)
})
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500/index.html


function myWebsocket(url, params) {
return new Promise((resolve, reject) => {
const socket = new WebSocket(url)
socket.onopen = () => {
socket.send(JSON.stringify(params))
}
socket.onmessage = (e) => {
resolve(e.data)
}
})
}
myWebsocket('ws://127.0.0.1:8000', { name: '林三心', age: 23 }).then(data => {
console.log(data) // 林三心今年23岁啦!!!
})
复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


Cors


Cors,全称是Cross-Origin Resource Sharing,意思是跨域资源共享,Cors一般是由后端来开启的,一旦开启,前端就可以跨域访问后端。


为什么后端开启Cors,前端就能跨域请求后端呢?我的理解是:前端跨域访问到后端,后端开启Cors,发送Access-Control-Allow-Origin: 域名 字段到前端(其实不止一个),前端浏览器判断Access-Control-Allow-Origin的域名如果跟前端域名一样,浏览器就不会实行跨域拦截,从而解决跨域问题。


截屏2021-10-01 下午6.41.11.png


后端代码


// index.js  http://127.0.0.1:8000

const http = require('http');
const urllib = require('url');

const port = 8000;

http.createServer(function (req, res) {
// 开启Cors
res.writeHead(200, {
//设置允许跨域的域名,也可设置*允许所有域名
'Access-Control-Allow-Origin': 'http://127.0.0.1:5500',
//跨域允许的请求方法,也可设置*允许所有方法
"Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
//允许的header类型
'Access-Control-Allow-Headers': 'Content-Type'
})
const { query: { name, age } } = urllib.parse(req.url, true);
res.end(`${name}今年${age}岁啦!!!`);
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500/index.html
//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数
ajax.open('get', 'http://127.0.0.1:8000?name=林三心&age=23');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
console.log(ajax.responseText);//输入相应的内容
}
}
复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


截屏2021-10-01 下午7.10.57.png


Node接口代理


还是回到同源策略,同源策略它只是浏览器的一个策略而已,它是限制不到后端的,也就是前端-后端会被同源策略限制,但是后端-后端则不会被限制,所以可以通过Node接口代理,先访问已设置Cors的后端1,再让后端1去访问后端2获取数据到后端1,后端1再把数据传到前端


截屏2021-10-01 下午8.46.28.png


后端2代码


// index.js  http://127.0.0.1:8000

const http = require('http');
const urllib = require('url');

const port = 8000;

http.createServer(function (req, res) {
console.log(888)
const { query: { name, age } } = urllib.parse(req.url, true);
res.end(`${name}今年${age}岁啦!!!`)
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

创建一个index2.js,并nodmeon index2.js


后端1代码


// index2.js  http://127.0.0.1:8888

const http = require('http');
const urllib = require('url');
const querystring = require('querystring');
const port = 8888;

http.createServer(function (req, res) {
// 开启Cors
res.writeHead(200, {
//设置允许跨域的域名,也可设置*允许所有域名
'Access-Control-Allow-Origin': 'http://127.0.0.1:5500',
//跨域允许的请求方法,也可设置*允许所有方法
"Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
//允许的header类型
'Access-Control-Allow-Headers': 'Content-Type'
})
const { query } = urllib.parse(req.url, true);
const { methods = 'GET', headers } = req
const proxyReq = http.request({
host: '127.0.0.1',
port: '8000',
path: `/?${querystring.stringify(query)}`,
methods,
headers
}, proxyRes => {
proxyRes.on('data', chunk => {
console.log(chunk.toString())
res.end(chunk.toString())
})
}).end()
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500

//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数,动态的传递参数starName到服务端
ajax.open('get', 'http://127.0.0.1:8888?name=林三心&age=23');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
console.log(ajax.responseText);//输入相应的内容
}
}
复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


Nginx


其实NginxNode接口代理是一个道理,只不过Nginx就不需要我们自己去搭建一个中间服务


截屏2021-10-01 下午8.47.40.png


先下载nginx,然后将nginx目录下的nginx.conf修改如下:


    server{
listen 8888;
server_name 127.0.0.1;

location /{
proxy_pass 127.0.0.1:8000;
}
}
复制代码

最后通过命令行nginx -s reload启动nginx


后端代码


// index.js  http://127.0.0.1:8000

const http = require('http');
const urllib = require('url');

const port = 8000;

http.createServer(function (req, res) {
const { query: { name, age } } = urllib.parse(req.url, true);
res.end(`${name}今年${age}岁啦!!!`);
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500

//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数,动态的传递参数starName到服务端
ajax.open('get', 'http://127.0.0.1:8888?name=林三心&age=23');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
console.log(ajax.responseText);//输入相应的内容
}
}
复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


postMessage


场景:http://127.0.0.1:5500/index.html页面中使用了iframe标签内嵌了一个http://127.0.0.1:5555/index.html的页面


虽然这两个页面存在于一个页面中,但是需要iframe标签来嵌套才行,这两个页面之间是无法进行通信的,因为他们端口号不同,根据同源策略,他们之间存在跨域问题


那应该怎么办呢?使用postMessage可以使这两个页面进行通信


截屏2021-10-01 下午9.28.53.png


// http:127.0.0.1:5500/index.html

<body>
<iframe src="http://127.0.0.1:5555/index.html" id="frame"></iframe>
</body>
<script>
document.getElementById('frame').onload = function () {
this.contentWindow.postMessage({ name: '林三心', age: 23 }, 'http://127.0.0.1:5555')
window.onmessage = function (e) {
console.log(e.data) // 林三心今年23岁啦!!!
}
}
</script>
复制代码

// http://127.0.0.1:5555/index.html

<script>
window.onmessage = function (e) {
const { data: { name, age }, origin } = e
e.source.postMessage(`${name}今年${age}岁啦!!!`, origin)
}
</script>
复制代码

document.domain && iframe


场景:a.sanxin.com/index.htmlb.sanxin.com/index.html之间的通信


其实上面这两个正常情况下是无法通信的,因为他们的域名不相同,属于跨域通信


那怎么办呢?其实他们有一个共同点,那就是他们的二级域名都是sanxin.com,这使得他们可以通过document.domain && iframe的方式来通信


截屏2021-10-01 下午9.58.55.png


由于本菜鸟暂时没有服务器,所以暂时使用本地来模拟


// http://127.0.0.1:5500/index.html

<body>
<iframe src="http://127.0.0.1:5555/index.html" id="frame"></iframe>
</body>
<script>
document.domain = '127.0.0.1'
document.getElementById('frame').onload = function () {
console.log(this.contentWindow.data) // 林三心今年23岁啦!!!
}
</script>
复制代码

// http://127.0.0.1:5555/index.html

<script>
// window.name="林三心今年23岁啦!!!"
document.domain = '127.0.0.1'
var data = '林三心今年23岁啦!!!';
</script>

复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


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

画一手好的架构图是码农进阶的开始

1.前言 你是否对大厂展示的五花八门,花花绿绿的架构设计图所深深吸引,当我们想用几张图来介绍下业务系统,是不是对着画布不知从何下手?作为技术扛把子的筒子们是不是需要一张图来描述系统,让系统各个参与方都能看的明白?如果有这样的困惑,本文将介绍一些画图的方...
继续阅读 »

1.前言

你是否对大厂展示的五花八门,花花绿绿的架构设计图所深深吸引,当我们想用几张图来介绍下业务系统,是不是对着画布不知从何下手?作为技术扛把子的筒子们是不是需要一张图来描述系统,让系统各个参与方都能看的明白?如果有这样的困惑,本文将介绍一些画图的方法论,让技术图纸更加清晰。

2. 架构的定义

  • 系统架构是概念的体现,是对物/信息的功能与形式元素之间的对应情况所做的分配,是对元素之间的关系以及元素同周边环境之间的关系所做的定义;

  • 架构就是对系统中的实体以及实体之间的关系所进行的抽象描述,是一系列的决策;

  • 架构是结构和愿景.

在TOGAF企业架构理论中, 架构是从公司战略层面,自顶向下的细化的一部分,从战略=> 业务架构=>应用/数据/技术架构,当然老板层关注的是战略与业务架构,我们搬砖的需要聚焦到应用/数据/技术架构这一层。


  • 业务架构: 由业务架构师负责,也可以称为业务领域专家、行业专家,业务架构属于顶层设计,其对业务的定义和划分会影响组织架构和技术架构;

  • 应用架构: 由应用架构师负责,需要根据业务场景需要,设计应用的层次结构,制定应用规范、定义接口和数据交互协议等。并尽量将应用的复杂度控制在一个可以接受的水平,从而在快速的支撑业务发展的同时,在保证系统的可用性和可维护性的同时,确保应用满足非功能属性的要求如性能、安全、稳定性等。

  • 技术架构: 描述了需要哪些服务;选择哪些技术组件来实现技术服务;技术服务以及组件之间的交互关系;

  • 数据架构: 描述了数据模型、分布、数据的流向、数据的生命周期、数据的管理等关系;

3.架构图的分类

系统架构图是为了抽象的表示软件系统的整体轮廓和各个组件之间的相互关系和约束边界,以及软件系统的物理部署和软件系统的演进方向的整体视图。好的架构图可以让干系人理解、遵循架构决策,就需要把架构信息传递出去。那么,画架构图是为了:解决沟通障碍/达成共识/减少歧义。比较流行的是4+1视图和C4视图。

3.1 4+1视图

3.1.1 场景视图

用于描述系统的参与者与功能用例间的关系,反映系统的最终需求和交互设计,通常由用例图表示;


3.1.2 逻辑视图

用于描述系统软件功能拆解后的组件关系,组件约束和边界,反映系统整体组成与系统如何构建的过程,通常由UML的组件图和类图来表示。


3.1.3 物理视图

用于描述系统软件到物理硬件的映射关系,反映出系统的组件是如何部署到一组可计算机器节点上,用于指导软件系统的部署实施过程。


3.1.4 处理流程视图

用于描述系统软件组件之间的通信时序,数据的输入输出,反映系统的功能流程与数据流程,通常由时序图和流程图表示。


3.1.5 开发视图

开发视图用于描述系统的模块划分和组成,以及细化到内部包的组成设计,服务于开发人员,反映系统开发实施过程。


5 种架构视图从不同角度表示一个软件系统的不同特征,组合到一起作为架构蓝图描述系统架构。

3.2 C4视图

下面的案例来自C4官网,然后加上了一些笔者的理解。


C4 模型使用容器(应用程序、数据存储、微服务等)、组件和代码来描述一个软件系统的静态结构。这几种图比较容易画,也给出了画图要点,但最关键的是,我们认为,它明确指出了每种图可能的受众以及意义。

3.2.1 语境图(System Context Diagram)

用于描述要我们要构建的系统是什么,用户是谁,需要如何融入已有的IT环境。这个图的受众可以是开发团队的内部人员、外部的技术或非技术人员。


3.2.2 容器图(Container Diagram)

容器图是把语境图里待建设的系统做了一个展开描述,主要受众是团队内部或外部的开发人员或运维人员,主要用来描述软件系统的整体形态,体现了高层次的技术决策与选型,系统中的职责是如何分布的,容器间是如何交互的。


3.2.3 组件图(Component Diagram)

组件图是把某个容器进行展开,描述其内部的模块,主要是给内部开发人员看的,怎么去做代码的组织和构建,描述了系统由哪些组件/服务组成,了组件之间的关系和依赖,为软件开发如何分解交付提供了框架。


4.怎么画好架构图

上面的分类是前人的经验总结,图也是从网上摘来的,那么这些图画的好不好呢?是不是我们要依葫芦画瓢去画这样一些图?先不去管这些图好不好,我们通过对这些图的分类以及作用,思考了一下,总结下来,我们认为,明确这两点之后,从受众角度来说,一个好的架构图是不需要解释的,它应该是自描述的,并且要具备一致性和足够的准确性,能够与代码相呼应。

4.1 视图的受众

在画出一个好的架构图之前, 首先应该要明确其受众,再想清楚要给他们传递什么信息 ,所以,不要为了画一个物理视图去画物理视图,为了画一个逻辑视图去画逻辑视图,而应该根据受众的不同,传递的信息的不同,用图准确地表达出来,最后的图可能就是在这样一些分类里。那么,画出的图好不好的一个直接标准就是:受众有没有准确接收到想传递的信息。

4.2 视图的元素区分

可以看到架构视图是由方框和线条等元素构成,要利用形状、颜色、线条变化等区分元素的含义,避免混淆。架构是一项复杂的工作,只使用单个图表来表示架构很容易造成莫名其妙的语义混乱。

让我们一起画出好的架构图!

参考资料


作者:代码的色彩
来源:https://juejin.cn/post/7062662600437268493

收起阅读 »

CSS性能优化的8个技巧

我们都知道对于网站来说,性能至关重要,CSS作为页面渲染和内容展现的重要环节,影响着用户对整个网站的第一体验。因此,与其相关的性能优化是不容忽视的。对于性能优化我们常常在项目完成时才去考虑,经常被推迟到项目的末期,甚至到暴露出严重的性能问题时才进行性能优化,相...
继续阅读 »

我们都知道对于网站来说,性能至关重要,CSS作为页面渲染和内容展现的重要环节,影响着用户对整个网站的第一体验。因此,与其相关的性能优化是不容忽视的。

对于性能优化我们常常在项目完成时才去考虑,经常被推迟到项目的末期,甚至到暴露出严重的性能问题时才进行性能优化,相信大多数人对此深有体会。

笔者认为,为了更多地避免这一情况,首先要重视起性能优化相关的工作,将其贯穿到整个产品设计与开发中。其次,就是了解性能相关的内容,在项目开发过程中,自然而然地进行性能优化。最后,也是最最重要的,那就是从现在开始实施优化。

推荐大家阅读下奇舞周刊之前推的《嗨,送你一张Web性能优化地图》1这篇文章,能够帮助大家对性能优化需要做的事以及需要考虑的问题形成一个整体的概念。

本文将会详细介绍CSS性能优化相关的技巧,笔者将它们分为实践型建议型两类,共8个小技巧。实践型技巧能够快速地应用在项目中,能够很好地提升性能,也是笔者经常使用的,建议大家尽快在项目中实践。建议型技巧中,有的可能对性能影响并不显著,有的平时大家也并不会那么用,所以笔者不会着重讲述,读者们可以根据自身情况了解一下即可。

在正式开始之前,需要大家对于浏览器的工作原理2有些一定的了解,需要的小伙伴可以先简单了解下。

下面我们开始介绍实践型的4个优化技巧,先从首屏关键CSS开始。

1. 内联首屏关键CSS(Critical CSS)

性能优化中有一个重要的指标——首次有效绘制(First Meaningful Paint,简称FMP)即指页面的首要内容(primary content)出现在屏幕上的时间。这一指标影响用户看到页面前所需等待的时间,而内联首屏关键CSS(即Critical CSS,可以称之为首屏关键CSS)能减少这一时间。

大家应该都习惯于通过link标签引用外部CSS文件。但需要知道的是,将CSS直接内联到HTML文档中能使CSS更快速地下载。而使用外部CSS文件时,需要在HTML文档下载完成后才知道所要引用的CSS文件,然后才下载它们。所以说,内联CSS能够使浏览器开始页面渲染的时间提前,因为在HTML下载完成之后就能渲染了。

既然内联CSS能够使页面渲染的开始时间提前,那么是否可以内联所有的CSS呢?答案显然是否定的,这种方式并不适用于内联较大的CSS文件。因为初始拥塞窗口3存在限制(TCP相关概念,通常是 14.6kB,压缩后大小),如果内联CSS后的文件超出了这一限制,系统就需要在服务器和浏览器之间进行更多次的往返,这样并不能提前页面渲染时间。因此,我们应当只将渲染首屏内容所需的关键CSS内联到HTML中

既然已经知道内联首屏关键CSS能够优化性能了,那下一步就是如何确定首屏关键CSS了。显然,我们不需要手动确定哪些内容是首屏关键CSS。Github上有一个项目Critical CSS4,可以将属于首屏的关键样式提取出来,大家可以看一下该项目,结合自己的构建工具进行使用。当然为了保证正确,大家最好再亲自确认下提取出的内容是否有缺失。

不过内联CSS有一个缺点,内联之后的CSS不会进行缓存,每次都会重新下载。不过如上所说,如果我们将内联后的文件大小控制在了14.6kb以内,这似乎并不是什么大问题。

如上,我们已经介绍了为什么要内联关键CSS以及如何内联,那么剩下的CSS我们怎么处理好呢?建议使用外部CSS引入剩余CSS,这样能够启用缓存,除此之外还可以异步加载它们。

2. 异步加载CSS

CSS会阻塞渲染,在CSS文件请求、下载、解析完成之前,浏览器将不会渲染任何已处理的内容。有时,这种阻塞是必须的,因为我们并不希望在所需的CSS加载之前,浏览器就开始渲染页面。那么将首屏关键CSS内联后,剩余的CSS内容的阻塞渲染就不是必需的了,可以使用外部CSS,并且异步加载。

那么如何实现CSS的异步加载呢?有以下四种方式可以实现浏览器异步加载CSS。

第一种方式是使用JavaScript动态创建样式表link元素,并插入到DOM中。

// 创建link标签
const myCSS = document.createElement( "link" );
myCSS.rel = "stylesheet";
myCSS.href = "mystyles.css";
// 插入到header的最后位置
document.head.insertBefore( myCSS, document.head.childNodes[ document.head.childNodes.length - 1 ].nextSibling );

第二种方式是将link元素的media属性设置为用户浏览器不匹配的媒体类型(或媒体查询),如media="print",甚至可以是完全不存在的类型media="noexist"。对浏览器来说,如果样式表不适用于当前媒体类型,其优先级会被放低,会在不阻塞页面渲染的情况下再进行下载。

当然,这么做只是为了实现CSS的异步加载,别忘了在文件加载完成之后,将media的值设为screenall,从而让浏览器开始解析CSS。

<link rel="stylesheet" href="mystyles.css" media="noexist" onl0ad="this.media='all'">

与第二种方式相似,我们还可以通过rel属性将link元素标记为alternate可选样式表,也能实现浏览器异步加载。同样别忘了加载完成之后,将rel改回去。

<link rel="alternate stylesheet" href="mystyles.css" onl0ad="this.rel='stylesheet'">

上述的三种方法都较为古老。现在,rel="preload"5这一Web标准指出了如何异步加载资源,包括CSS类资源。

<link rel="preload" href="mystyles.css" as="style" onl0ad="this.rel='stylesheet'">

注意,as是必须的。忽略as属性,或者错误的as属性会使preload等同于XHR请求,浏览器不知道加载的是什么内容,因此此类资源加载优先级会非常低。as的可选值可以参考上述标准文档。

看起来,rel="preload"的用法和上面两种没什么区别,都是通过更改某些属性,使得浏览器异步加载CSS文件但不解析,直到加载完成并将修改还原,然后开始解析。

但是它们之间其实有一个很重要的不同点,那就是使用preload,比使用不匹配的media方法能够更早地开始加载CSS。所以尽管这一标准的支持度还不完善,仍建议优先使用该方法。

该标准现在已经是候选标准,相信浏览器会逐渐支持该标准。在各浏览器的支持度如下图所示。


从上图可以看出这一方法在现在的浏览器中支持度不算乐观,不过我们可以通过loadCSS6进行polyfill,所以支持不支持,这都不是事儿。

3. 文件压缩

性能优化时有一个最容易想到,也最常使用的方法,那就是文件压缩,这一方案往往效果显著。

文件的大小会直接影响浏览器的加载速度,这一点在网络较差时表现地尤为明显。相信大家都早已习惯对CSS进行压缩,现在的构建工具,如webpack、gulp/grunt、rollup等也都支持CSS压缩功能。压缩后的文件能够明显减小,可以大大降低了浏览器的加载时间。

4. 去除无用CSS

虽然文件压缩能够降低文件大小。但CSS文件压缩通常只会去除无用的空格,这样就限制了CSS文件的压缩比例。那是否还有其他手段来精简CSS呢?答案显然是肯定的,如果压缩后的文件仍然超出了预期的大小,我们可以试着找到并删除代码中无用的CSS

一般情况下,会存在这两种无用的CSS代码:一种是不同元素或者其他情况下的重复代码,一种是整个页面内没有生效的CSS代码。对于前者,在编写的代码时候,我们应该尽可能地提取公共类,减少重复。对于后者,在不同开发者进行代码维护的过程中,总会产生不再使用的CSS的代码,当然一个人编写时也有可能出现这一问题。而这些无用的CSS代码不仅会增加浏览器的下载量,还会增加浏览器的解析时间,这对性能来说是很大的消耗。所以我们需要找到并去除这些无用代码。

当然,如果手动删除这些无用CSS是很低效的。我们可以借助Uncss7库来进行。Uncss可以用来移除样式表中的无用CSS,并且支持多文件和JavaScript注入的CSS。

前面已经说完了实践型的4个优化技巧,下面我们介绍下建议型的4个技巧

1. 有选择地使用选择器

大多数朋友应该都知道CSS选择器的匹配是从右向左进行的,这一策略导致了不同种类的选择器之间的性能也存在差异。相比于#markdown-content-h3,显然使用#markdown .content h3时,浏览器生成渲染树(render-tree)所要花费的时间更多。因为后者需要先找到DOM中的所有h3元素,再过滤掉祖先元素不是.content的,最后过滤掉.content的祖先不是#markdown的。试想,如果嵌套的层级更多,页面中的元素更多,那么匹配所要花费的时间代价自然更高。

不过现代浏览器在这一方面做了很多优化,不同选择器的性能差别并不明显,甚至可以说差别甚微。此外不同选择器在不同浏览器中的性能表现8也不完全统一,在编写CSS的时候无法兼顾每种浏览器。鉴于这两点原因,我们在使用选择器时,只需要记住以下几点,其他的可以全凭喜好。

  1. 保持简单,不要使用嵌套过多过于复杂的选择器。

  2. 通配符和属性选择器效率最低,需要匹配的元素最多,尽量避免使用。

  3. 不要使用类选择器和ID选择器修饰元素标签,如h3#markdown-content,这样多此一举,还会降低效率。

  4. 不要为了追求速度而放弃可读性与可维护性。

如果大家对于上面这几点还存在疑问,笔者建议大家选择以下几种CSS方法论之一(BEM9,OOCSS10,SUIT11,SMACSS12,ITCSS13,Enduring CSS14等)作为CSS编写规范。使用统一的方法论能够帮助大家形成统一的风格,减少命名冲突,也能避免上述的问题,总之好处多多,如果你还没有使用,就赶快用起来吧。

Tips:为什么CSS选择器是从右向左匹配的?

CSS中更多的选择器是不会匹配的,所以在考虑性能问题时,需要考虑的是如何在选择器不匹配时提升效率。从右向左匹配就是为了达成这一目的的,通过这一策略能够使得CSS选择器在不匹配的时候效率更高。这样想来,在匹配时多耗费一些性能也能够想的通了。

2. 减少使用昂贵的属性

在浏览器绘制屏幕时,所有需要浏览器进行操作或计算的属性相对而言都需要花费更大的代价。当页面发生重绘时,它们会降低浏览器的渲染性能。所以在编写CSS时,我们应该尽量减少使用昂贵属性,如box-shadow/border-radius/filter/透明度/:nth-child等。

当然,并不是让大家不要使用这些属性,因为这些应该都是我们经常使用的属性。之所以提这一点,是让大家对此有一个了解。当有两种方案可以选择的时候,可以优先选择没有昂贵属性或昂贵属性更少的方案,如果每次都这样的选择,网站的性能会在不知不觉中得到一定的提升。

3. 优化重排与重绘

在网站的使用过程中,某些操作会导致样式的改变,这时浏览器需要检测这些改变并重新渲染,其中有些操作所耗费的性能更多。我们都知道,当FPS为60时,用户使用网站时才会感到流畅。这也就是说,我们需要在16.67ms内完成每次渲染相关的所有操作,所以我们要尽量减少耗费更多的操作。

3.1 减少重排

重排会导致浏览器重新计算整个文档,重新构建渲染树,这一过程会降低浏览器的渲染速度。如下所示,有很多操作会触发重排,我们应该避免频繁触发这些操作。

  1. 改变font-sizefont-family

  2. 改变元素的内外边距

  3. 通过JS改变CSS类

  4. 通过JS获取DOM元素的位置相关属性(如width/height/left等)

  5. CSS伪类激活

  6. 滚动滚动条或者改变窗口大小

此外,我们还可以通过CSS Trigger15查询哪些属性会触发重排与重绘。

值得一提的是,某些CSS属性具有更好的重排性能。如使用Flex时,比使用inline-blockfloat时重排更快,所以在布局时可以优先考虑Flex

3.2 避免不必要的重绘

当元素的外观(如color,background,visibility等属性)发生改变时,会触发重绘。在网站的使用过程中,重绘是无法避免的。不过,浏览器对此做了优化,它会将多次的重排、重绘操作合并为一次执行。不过我们仍需要避免不必要的重绘,如页面滚动时触发的hover事件,可以在滚动的时候禁用hover事件,这样页面在滚动时会更加流畅。

此外,我们编写的CSS中动画相关的代码越来越多,我们已经习惯于使用动画来提升用户体验。我们在编写动画时,也应当参考上述内容,减少重绘重排的触发。除此之外我们还可以通过硬件加速16和will-change17来提升动画性能,本文不对此展开详细介绍,感兴趣的小伙伴可以点击链接进行查看。

最后需要注意的是,用户的设备可能并没有想象中的那么好,至少不会有我们的开发机器那么好。我们可以借助Chrome的开发者工具进行CPU降速,然后再进行相关的测试,降速方法如下图所示。


如果需要在移动端访问的,最好将速度限制更低,因为移动端的性能往往更差。

4. 不要使用@import

最后提一下,不要使用@import引入CSS,相信大家也很少使用。

不建议使用@import主要有以下两点原因。

首先,使用@import引入CSS会影响浏览器的并行下载。使用@import引用的CSS文件只有在引用它的那个css文件被下载、解析之后,浏览器才会知道还有另外一个css需要下载,这时才去下载,然后下载后开始解析、构建render tree等一系列操作。这就导致浏览器无法并行下载所需的样式文件。

其次,多个@import会导致下载顺序紊乱。在IE中,@import会引发资源文件的下载顺序被打乱,即排列在@import后面的js文件先于@import下载,并且打乱甚至破坏@import自身的并行下载

所以不要使用这一方法,使用link标签就行了。

总结

至此,我们介绍完了CSS性能优化的4个实践型技巧和4个建议型技巧,在了解这些技巧之后,CSS的性能优化从现在就可以开始了。不要犹豫了,尽快开始吧。

参考文章

  1. Efficiently Rendering CSS

  2. How to write CSS for a great performance web application

  3. CSS performance revisited: selectors, bloat and expensive styles

  4. Avoiding Unnecessary Paints

  5. Five CSS Performance Tools to Speed up Your Website

  6. How and Why You Should Inline Your Critical CSS

  7. Render blocking css

  8. Modern Asynchronous CSS Loading

  9. Preload

作者:奇舞精选 · 高峰
来源:https://juejin.cn/post/6844903649605320711

收起阅读 »

你要懂的单页面应用和多页面应用

单页面应用(SinglePage Web Application,SPA)只有一张Web页面的应用,是一种从Web服务器加载的富客户端,单页面跳转仅刷新局部资源 ,公共资源(js、css等)仅需加载一次,常用于PC端官网、购物等网站如图:单页面应用结构视图多页...
继续阅读 »

单页面应用(SinglePage Web Application,SPA)

只有一张Web页面的应用,是一种从Web服务器加载的富客户端,单页面跳转仅刷新局部资源 ,公共资源(js、css等)仅需加载一次,常用于PC端官网、购物等网站

如图:


单页面应用结构视图

多页面应用(MultiPage Application,MPA)

多页面跳转刷新所有资源,每个公共资源(js、css等)需选择性重新加载,常用于 app 或 客户端等

如图:


多页面应用结构视图

具体对比分析:

单页面应用(SinglePage Web Application,SPA)多页面应用(MultiPage Application,MPA)
组成一个外壳页面和多个页面片段组成多个完整页面构成
资源共用(css,js)共用,只需在外壳部分加载不共用,每个页面都需要加载
刷新方式页面局部刷新或更改整页刷新
url 模式a.com/#/pageone a.com/#/pagetwoa.com/pageone.html a.com/pagetwo.html
用户体验页面片段间的切换快,用户体验良好页面切换加载缓慢,流畅度不够,用户体验比较差
转场动画容易实现无法实现
数据传递容易依赖 url传参、或者cookie 、localStorage等
搜索引擎优化(SEO)需要单独方案、实现较为困难、不利于SEO检索 可利用服务器端渲染(SSR)优化实现方法简易
试用范围高要求的体验度、追求界面流畅的应用适用于追求高度支持搜索引擎的应用
开发成本较高,常需借助专业的框架较低 ,但页面重复代码多
维护成本相对容易相对复杂


作者:boxser
来源:https://juejin.cn/post/6844903512107663368

收起阅读 »

千万别小瞧九宫格 一道题就能让候选人原形毕露!

前言 据不完全统计(其实就统计了自己身边的朋友和同事),在刨除抖音或快手这一类短视频 APP 后,每天在手机上花费时间最长的就是刷微博和逛朋友圈。 在刷微博和逛朋友圈的时候经常会看到这种东西: 它有一个高大上的名字:九宫格。 顾名思义,九宫格通常为如图这种三...
继续阅读 »

前言


据不完全统计(其实就统计了自己身边的朋友和同事),在刨除抖音或快手这一类短视频 APP 后,每天在手机上花费时间最长的就是刷微博和逛朋友圈。


在刷微博和逛朋友圈的时候经常会看到这种东西:



它有一个高大上的名字:九宫格。
顾名思义,九宫格通常为如图这种三行三列的布局。


微信客户端就用到了这种布局方式:



大家最熟悉的朋友圈也采用了九宫格:



还有微博:



它在移动端的运用十分的广泛,而且不仅仅是在移动端的运用,它甚至还运用到了一些面试题中,因为九宫格可以很好的考察面试者的 CSS 功底。


边距九宫格


九宫格通常分为两种,一种是边距九宫格,另一种是边框九宫格。


边距九宫格就是朋友圈那种每张图都带有一定边距的那种:


这种其实反而更简单一些,因为不涉及到边框问题,像这种几行几列的布局用网格布局(grid)简直再合适不过了。


但考虑到大家普遍对网格不太熟悉,所以咱们用同样适合几行几列的表格布局来实现,为什么不用万能的弹性盒子(flex)来做呢?因为下面那道面试题就是用flex实现的,不想用两个一样的布局来实现,为了美观一点,这里使用了一个中文渐变色的库:chinese-gradient,来看代码:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- 在这里用link标签引入中文渐变色 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/chinese-gradient">
<style>
/* 清除默认样式 */
* { padding: 0; margin: 0; }

/* 全屏显示 */
html, body, ul { height: 100% }

/* 父元素 */
ul {
/* 给个合适的宽度 */
width: 100%;

/* 清除默认样式 */
list-style: none;

/* 令其用table方式去显示 */
display: table;

/* 设置间距 */
border-spacing: 3px
}

/* 子元素 */
li {
/* 令其用table-row方式去显示 */
display: table-row
}

/* 孙子元素 */
div {
/* 令其用table-cell方式去显示 */
display: table-cell;

/* 蓝色渐变 */
background: var(--湖蓝)
}
</style>
</head>
<body>
<ul>
<li>
<div></div>
<div></div>
<div></div>
</li>
<li>
<div></div>
<div></div>
<div></div>
</li>
<li>
<div></div>
<div></div>
<div></div>
</li>
</ul>
</body>
</html>
复制代码

运行结果:



可以看到在 DOM 结构上我们并没有用到 <table>、<tr>、<td> 这类传统表格元素,因为在这种情况下只是用到了表格的那种几行几列而已。但实际上九宫格并不是表格,所以为了符合 W3C 的语义化标准,我们采用了其他的 DOM 元素。



在有些适合使用表格布局但又不是表格的情况下,可以利用 display 属性来模仿表格的行为:




  • display: table;相当于把元素的行为变成<table></table>

  • display: inline-table;相当于把元素的行为变成行内元素版的<table></table>

  • display: table-header-group;相当于把元素的行为变成<thead></thead>

  • display: table-row-group;相当于把元素的行为变成<tbody></tbody>

  • display: table-footer-group;相当于把元素的行为变成<tfoot></tfoot>

  • display: table-row;相当于把元素的行为变成<tr></tr>

  • display: table-column-group;相当于把元素的行为变成<colgroup></colgroup>

  • display: table-column;相当于把元素的行为变成<col></col>

  • display: table-cell;相当于把元素的行为变成<td></td><th></th>

  • display: table-caption;相当于把元素的行为变成<caption></caption>


边框九宫格


可能大家看了前面的内容觉得:就这?这么简单还想让人原形毕露?


那咱们来看这么一道题:



要求如下:



  • 边框九宫格的每个格子中的数字都要居中

  • 鼠标经过时边框和数字都要变红

  • 点击九宫格会弹出对应的数字


看起来还是没什么大不了对不对?是不是觉得就是把九宫格加个边框就行了?如果你是这么想的话,那么你写出来的九宫格将会变成这样:



是不是跟想象中的好像不太一样?为什么会这样呢?




因为给每个盒子加入了边框以后,在有边距的情况下看起来都挺正常的,但要将他们合并在一起的话相邻的两个边框就会贴合在一起,肉眼看起来就是一个两倍粗的边框:



那么怎么解决这个问题呢?


解法1


不是相邻的两个边框合并在一起会变粗吗?那么最简单粗暴的办法就是让两个相邻的盒子的其中一个的相邻边不显示边框不就完了!就像这样:



这么做完全可以实现,绝对没毛病。但这种属于笨方法,如果给换成四宫格、六宫格、十二宫格,那么又要重新去想一下该怎么实现,而且写出来的代码也比较冗余,几乎每个盒子都要给它定义一个不同的样式。


如果去参加面试的时候这么实现出来,面试官也不会给你满分,甚至可能连个及格分都不会给。但毕竟算是实现出来了,总比那些没实现出来的强点,不会给零分的。


解法2


上面那种实现方式要给每一个盒子都写一套不同的样式,而且还不适合别的像六宫格、十二宫格这类,代码冗余、可复用性差。


那么怎么才能每个盒子只用到一个样式,并且同样还适用于别的宫格呢?来看看这个思路:



但是仔细一看经不起推敲啊:整个九宫格最右边和最下边的边框都没有了!其实只要咱们在父元素上再加上右侧和下侧的边框即可:



而且并不一定非得是这个方向的,别的方向也可以实现啊,比如酱婶儿的:



酱婶儿的:



还有酱婶儿的:



这种方式不管你是4、6、9还是12宫格,只需在子元素上加一个样式即可,然后再在父元素上加一个互补的边框样式。


解法3


上面那种解法其实已经可以了,但还不是最完美的,那么它都有哪些问题呢?




  • 首先,虽然换成别的宫格也可以复用,但都只适合"满"的情况。比如像朋友圈,最大就是九宫格对吧?但用户可以不是每次都发满九张照片,有可能发7张、有可能发五张,这样的话就会露馅(所以朋友圈采用的是边距九宫格而不是边框九宫格)。




  • 其次,它并不适合这道面试题,因为这道面试题的要求是在鼠标移入时边框变红,而上面那种解法会导致每个盒子的边框都不完整,所以当鼠标移入时效果会变成这样:





那么怎么样才能完美的解出这道题呢?首先每个盒子的边框不能再给它缺斤少两了,但那又会回到最初的那个问题上去:



有的面试题就是这样,在你苦思冥想的时候怎么也想不出来,但是稍微给点思路立马就能明白!


其实就是每个盒子都给它一个负边距,边距的距离恰巧就是边框的粗细,这样后面一个盒子就会"叠加"在前面那个盒子的边框上,我们来写一个粗点的半透明边框演示一下:



中间那些颜色变深了的就是叠在一起的边框,由于是半透明,所以叠在一起时颜色会变深。


不过一些比较细心的朋友可能会纳闷:既然所有盒子都用负边距向左上角移动了,岂不是九宫格不会处在原来的位置上了,没错是这样的!所以我们需要让最左边那一排和最上面那一排不要有负边距,这时候就要考察候选人的CSS水平了,看看他/她能不能够灵活运用伪类选择器:每一行的第一个,应该怎么写?



  • :nth-child(1), :nth-child(4), :nth-child(7)


这样也能实现,不过更好的方式是写成这样:



  • :nth-child(3n+1)


最上面那一排负边距可以不用管,因为如果页面上的九宫格往左边移动了,哪怕只有一两像素,也会导致和页面上的版面无法对齐,而往上移动个一两像素的话谁也看不出来。


但如果要写的话大多数人想的可能是这样:



  • :first-child, :nth-child(2), :nth-child(3)


而更好的方式是这样:



  • :nth-child(-n+3)


每个宫格内的数字要居中,这里推荐用grid,因为九宫格可以用flex去实现,但里面的内容还继续用它去实现的话就体现不出你技术的全面性了,而且在居中这一方面grid可以做到比flex代码更少,即使你对grid不感兴趣,那么只需记住这一固定用法即可:


父元素 {
display: grid;

/* 令其子元素居中 */
place-items: center;
}
复制代码

点击这里查看更多实现居中布局的方式


里面的内容解决了,外面的九宫格咱们来用万能的flex去实现,flex默认是一维布局,但如果仅支持一维的话就不会称之为万能的flex了,思路是这样的,假如每一个宫格宽高为100 x 100,九宫格加起来是300 x 300,每三个就让它换行,这样就可以考察到候选人对flex的灵活运用的程度了:


父元素 {
width: 300px;

/* 设置为flex布局 */
display: flex;

/* 设置换行 */
flex-flow: wrap;
}

子元素 {
width: 100px;
height: 100px;

border: 1px solid black;
}
复制代码

看起来没毛病对不对?实际上确是每行只有两个宫格就会换行,因为加了边框以后子元素的宽高就变成了102 x 102了,三个的话就已经超过了300,所以还没到三个就开始换行了,这时候就考察到候选人的盒模型了:


子元素 {
width: 100px;
height: 100px;

border: 1px solid black;

/* 设置盒模型 */
box-sizing: border-box;
}
复制代码

这样即使加了边框,宽高也还是100,刚好能满3个就换行,想象一下如果你是面试官,直接问盒模型是不是显得很low,但是就这一个小小的九宫格立马就能区分出这个候选人的水平如何。


再接下来就是鼠标移入时边框和里面的内容一起变红,这有啥难的,不就是:


:hover {
/* 红色字体 */
color: red;

/* 红色边框 */
border: 1px solid red;
}
复制代码

还是那句话,这样确实能实现,但如果在咱们写js的过程中像red这种多处地方使用的值是不是一般都会给它设置成变量啊?那么这里要写CSS变量?也可以,但有一个更好的变量叫做currentColor,这个属性可以把它理解成一个内置变量,就像js里的innerWidth(window.innerWidth)一样,不用定义自然就是一个变量。


CSS变量不同的是它取的是自身或父元素上的color值,而且它的兼容性还更好,可以一直兼容到IE9


如果你觉得纳闷:这单词这么长,还不如直接写个red多方便啊,那么请别忘了color是可以继承的!如果在一个外层元素中定义了一个颜色,里面的子元素都可以继承,用JS来控制的话只需要获取外层DOM元素然后修改它的color样式即可。


currentColor作为一个变量,可以用在 border、box-shadow、background、linear-gradient() 等一大堆的 CSS 属性上…甚至连svg中的 fill 和 stroke 都可以使用这个变量,它能做的事情很多,这里为了不跑题就先不展开讲,有兴趣的可以去搜一下。


:hover {
/* 红色字体 */
color: red;

/* 红色边框 */
border: 1px solid;
}
复制代码

修改后的代码如上,为什么没有currentColor?那是因为如果你不写的话,默认就是currentColor,这个关键字代表的就是你当前的color值。



大多数的候选人可能都不会写成这样,如果你作为面试官的话最好是适当的提示一下,看他能不能说出currentColor这个变量或者CSS变量



然后就是点击每个宫格弹出对应的数字,这个考察的是事件冒泡和事件代理:


父元素.addEventListener('click', e => alert(e.target.innerText))
复制代码

你可以观察一下候选人是把事件绑定在父元素上还是一个个的绑定在子元素上,这个问题按理说基本上都不会错。但如果发现候选人一个个把事件绑定在子元素上了,那就可以到此为止了,也不用浪费时间再去问别的问题了,可以十分装B的来一句:行,你的情况我已基本了解了,回去等通知吧!


接下来我们再来写一下完整一点的代码,以便引出下一个问题:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
/* 清除默认样式 */
* { padding: 0; margin: 0; }

/* 全屏显示 */
html, body { height: 100% }

body {
/* 网格布局 */
display: grid;

/* 子元素居中 */
place-items: center;
}

/* 父元素 */
ul {
width: 300px;

/* 清除默认样式 */
list-style: none;

/* 设置为flex布局 */
display: flex;

/* 设置换行 */
flex-flow: wrap;
}

/* 子元素 */
li {
/* 显示为网格布局 */
display: grid;

/* 子元素水平垂直居中 */
place-items: center;

/* 宽高都是100像素 */
width: 100px;
height: 100px;

/* 设置盒模型 */
box-sizing: border-box;

/* 设置1像素的边框 */
border: 1px solid black;

/* 负边距 */
margin: -1px 0 0 -1px;
}

/* 第1、4、7个子元素 */
li:nth-child(3n+1) {
/* 取消左负边距 */
margin-left: 0
}

/* 前三个子元素 */
li:nth-child(-n+3) {
/* 取消上负边距 */
margin-top: 0
}

/* 当鼠标经过时 */
li:hover {
/* 红色字体 */
color: red;

/* 红色边框 */
border: 1px solid;
}
</style>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
</ul>
<script>
// 选择ul元素
const ul = document.getElementsByTagName('ul')[0]

// 监听ul元素的点击事件
ul.addEventListener('click', e => alert(e.target.innerText))
</script>
</body>
</html>
复制代码

运行结果:



想知道为什么会这样吗?因为当前这个边框被后面的宫格压住了嘛!那么只需要当鼠标经过时不让后面的压住就好了(调高层级)。


说到调高层级,大家首先想到的可能就是z-index了,这个属性用的最多的地方可能就是绝对定位和固定定位了。但其实很少有人知道,z-index不是只能用在position: xxx的,万能的弹性盒子(display:flex)也是支持z-index的:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
/* 清除默认样式 */
* { padding: 0; margin: 0; }

/* 全屏显示 */
html, body { height: 100% }

body {
/* 网格布局 */
display: grid;

/* 子元素居中 */
place-items: center;
}

/* 父元素 */
ul {
width: 300px;

/* 清除默认样式 */
list-style: none;

/* 设置为flex布局 */
display: flex;

/* 设置换行 */
flex-flow: wrap;
}

/* 子元素 */
li {
/* 显示为网格布局 */
display: grid;

/* 子元素水平垂直居中 */
place-items: center;

/* 宽高都是100像素 */
width: 100px;
height: 100px;

/* 设置盒模型 */
box-sizing: border-box;

/* 设置1像素的边框 */
border: 1px solid black;

/* 负边距 */
margin: -1px 0 0 -1px;
}

/* 第1、4、7个子元素 */
li:nth-child(3n+1) {
/* 取消左负边距 */
margin-left: 0
}

/* 前三个子元素 */
li:nth-child(-n+3) {
/* 取消上负边距 */
margin-top: 0
}

/* 当鼠标经过时 */
li:hover {
/* 红色字体 */
color: red;

/* 红色边框 */
border: 1px solid;

/* 调高层级 */
z-index: 1;
}
</style>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
</ul>
<script>
// 选择ul元素
const ul = document.getElementsByTagName('ul')[0]

// 监听ul元素的点击事件
ul.addEventListener('click', e => alert(e.target.innerText))
</script>
</body>
</html>
复制代码

运行结果:



结语


没想到这么一个看似不起眼的九宫格一下子就能考察这么多内容吧!如果面试的时候直接问:



  • 你对 flex 了解的怎么样

  • 当元素的外边距为负值时会有什么样的行为

  • 请实现一下水平垂直居中

  • 了解过 grid 吗

  • 谈一下你对盒模型的理解

  • 说一下事件绑定和事件冒泡

  • CSS3的伪类选择器用的怎么样

  • 当页面元素重叠时如何控制哪个在上哪个在下

  • 在CSS中如何运用变量


直接这么问的话既浪费口舌,又显得很low,而且还不能筛选出真正能够灵活运用技术的候选人。


因为这些问题都不难,一般来说都能答出来,但具体能不能灵活运用就不一定了,而这一道九宫格,就像一面照妖镜一样,瞬间让人原形毕露!


如果你是候选人的话,那么一定要好好练习一下这道题。


如果是面试官的话,那么也推荐你用这道题来考察候选者的技术水平,如果能非常完美的做出来,那么基本上就不用再问其他的CSS题目了,日常开发所用到的样式基本难不倒他/她了,可以直接上JS面试题了。


但如果没做出来也不一定就代表这个人水平不行,可以试着提示一下候选者,然后再问一下其他的CSS题来确定一下此人的水平。


作者:手撕红黑树
来源:https://juejin.cn/post/6886770985060532231
收起阅读 »

仅靠H5标签就能实现收拉效果

前言 最近做项目时碰到这么一个需求: 这有点类似于手风琴效果,但不一样的是很多手风琴效果是同一时间内只能有一个展开,而这个是各个部分独立的,你展不展开完全不会影响我的展开与否。其实这种效果简直再普遍不过了,网上随便一搜就出来一大堆。但不一样的是,我在接到这个...
继续阅读 »

前言


最近做项目时碰到这么一个需求:



这有点类似于手风琴效果,但不一样的是很多手风琴效果是同一时间内只能有一个展开,而这个是各个部分独立的,你展不展开完全不会影响我的展开与否。其实这种效果简直再普遍不过了,网上随便一搜就出来一大堆。但不一样的是,我在接到这个需求的时候突然想起来很久以前看过张鑫旭大佬的一篇文章,模糊的记得那篇文章里说过有个什么很方便的 CSS 属性能够实现这一效果,不用像咱们平时实现的那些展开收起那样写很多的代码,于是就来到他的博客里面一顿搜,找了半天终于发现原来是我记错了,并不是什么 CSS3 属性,而是 HTML5 标签!


details


想要非常轻松的实现一个收拉效果,需要用到三个标签,分别是:<details><summary>以及随意


随意是什么意思?意思是什么标签都可以?


咱们先只写一个<details>标签来看看页面上会出现什么:


<details></details>
复制代码

运行结果:



可以看到非常有意思的一个现象:我们明明什么文字都没有写,但页面上却出现了详细信息这四个字,因为如果你在标签里没有写<summary>的话,浏览器会自动给你补上一个<summary>详细信息</summary>,那有人可能奇怪了,怎么补的是中文呢?那老外不写<summary>的话也会来一个<summary>详细信息</summary>?其实是这样:



现代浏览器经常偷偷获取用户隐私信息,包括但不仅限于用人工智能判断屏幕前的用户是中国人还是外国人,然后根据用户的母语来动态向<summary>标签里加入不同语言的'详细信息'这几个字。




开个玩笑,其实是根据你当前操作系统的语言来判断的,要是你把系统语言改成其它语言的话出现的就不再是'详细信息'这几个中文字符了。


那如果我们在<details>标签里写了<summary>呢?


<details>
<summary>公众号:</summary>
</details>
复制代码

运行结果:



可以看到<summary>里面的文字就会在三角箭头旁边的标题位置展示出来,可是我们展开三角箭头发现里面什么内容也没有,那么内容写在哪呢?


只需写在<summary>的后面就可以了,那是不是还要写个固定标签呢?比如什么<describe>之类的,其实在<summary>之后无论写什么标签都可以,当然必须得是合法的 HTML 标签啊,比如我们写个<h1>标签来试试看:


<details>
<summary>公众号:</summary>
<h1>前端学不动</h1>
</details>
复制代码

运行结果:



再换个别的标签试试:


<details>
<summary>公众号:</summary>
<button>前端学不动</button>
</details>
复制代码

运行结果:



看!我们仅用了三个标签就完成了一个最简单的收拉效果!以前在网上看到类似的效果要么就是 getElementById 获取到 DOM 元素,然后添加 onclick 事件控制下方元素的 style 属性,要么就是纯 CSS 实现,写几个单选按钮配合兄弟选择器来控制后方元素的显隐,抑或是 CSS 与 JS 相结合来实现的,但仅靠 HTML 标签来实现这一效果还是非常清新脱俗的!并且十分简洁、非常节约代码量、也更加直观易于理解。


深入测试


既然<summary>标签后面写什么都行,那么可不可以写很多个标签呢?我们来测试一下:


<details>
<summary>公众号:</summary>
<button>前端学不动</button>
<span>前端学不动</span>
<h1>前端学不动</h1>
<a href="#">前端学不动</a>
<strong>前端学不动</strong>
</details>
复制代码

运行结果:



那展开收起那部分的内容只能放在<summary>标签之后吗?如果放它前面呢:


<details>
<button>前端学不动</button>
<span>前端学不动</span>
<h1>前端学不动</h1>
<a href="#">前端学不动</a>
<strong>前端学不动</strong>
<summary>公众号:</summary>
</details>
复制代码

运行结果:



效果居然一模一样,看来展开收起的那部分应该是在<details>标签内部的除<summary>标签之外的所有内容。那如果写两个<summary>标签呢:


<details>
<button>前端学不动</button>
<span>前端学不动</span>
<h1>前端学不动</h1>
<a href="#">前端学不动</a>
<strong>前端学不动</strong>
<summary>公众号:</summary>
<summary>summary</summary>
</details>
复制代码

运行结果:



可以看到只有第一个出现的<summary>标签是真正的summary,后续出现的其他所有标签(包括其它的<summary>)都是展开收起的那部分。


既然所有标签都可以,那么也包括<details>咯?


<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
复制代码

运行结果:



这玩意有点意思,利用这种嵌套写法可以轻松实现编辑器左侧的那些文件区的效果。


加入样式


虽然可以很轻松、甚至在不用写 CSS 代码的情况下就实现展开收起效果,但毕竟不写 CSS 只是实现了个最基础的乞丐版效果,很多人都不想要点击的时候出现的那个轮廓:



在谷歌浏览器和 Safari 浏览器下都会出现这个轮廓,火狐就没有这玩意,咱们只需要给<summary>标签设置 outline 属性就可以了,一般如果你的项目引入了抹平浏览器样式间差异的 reset.css 文件的话,就不用写这个 CSS 了,为了方便同时观看 HTML、CSS 和 JS,我们来用 Vue 的格式来写代码:


<template>
<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
</template>

<style>
summary { outline: none }
</style>
复制代码

运行结果:



这样看起来就舒服多啦!但是还有个问题:那个三角箭头太傻大黑粗了,一般我们很少会用这样的箭头,而且我们也不一定非得让它在左边待着,那么怎么修改箭头的样式呢?


在谷歌浏览器以及 Safari 浏览器下我们需要用::-webkit-details-marker伪元素,在火狐浏览器下我们要用::-moz-list-bullet伪元素,比如我们想让它别那么傻大黑粗:


<template>
<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
</template>

<style>
summary { outline: none }

/* 谷歌、Safari */
::-webkit-details-marker {
transform: scale(.5);
color: gray
}

/* 火狐 */
::-moz-list-bullet { color: gray }
</style>
复制代码

运行结果:



是不是没那么傻大黑粗了,不过有时我们不想要这个三角形的箭头,想要的是自己自定义的箭头,那么我们就需要先把这个默认的三角给隐藏掉:


<template>
<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
</template>

<style>
summary { outline: none }

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }
</style>
复制代码

运行结果:



这回箭头没了,我们只需要在<summary>标签里写个箭头就好了,可以用::before::after伪元素,也可以直接在里面写个<img>标签,为了让大家能够直接复制代码到 Vue 环境里运行,在这里我们就不用图片了,直接手写<svg>


<template>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
project
</summary>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
html
</summary>
index.html
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
css
</summary>
reset.css
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
js
</summary>
main.js
</details>
</details>
</template>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
fill: none;
stroke: gray
}
</style>
复制代码

运行结果:



箭头是变成自定义的了,但是方向却不智能了,不能像原生箭头那样展开收起时会自动改变方向,但是<details>这个标签好就好在它在展开是会自动在标签里添加一个open属性:



我们可以利用它的这一特点,用属性选择器来让<svg>标签进行旋转:


<template>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
project
</summary>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
html
</summary>
index.html
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
css
</summary>
reset.css
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
js
</summary>
main.js
</details>
</details>
</template>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

[open] > summary > svg { transform: none }
</style>
复制代码

运行结果:



用 JS 控制 open 属性


既然展开时会自动给<details>标签添加一个open属性,那如果我们用 JS 手动给<details>标签添加或删除open属性,<details>标签会随之展开收起吗?


比如我们用定时器,每隔1秒就自动展开一个,同时收起上一个已被展开过的标签:


<template>
<details v-for="({title, content}, index) of list" :key="title" :open="openIndex === index">
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
{{ content }}
</details>
</template>

<script>
import { defineComponent, ref, onBeforeUnmount } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: 'index.html'
}, {
title: 'css',
content: 'reset.css'
}, {
title: 'js',
content: 'main.js'
}]

const openIndex = ref(-1)

const interval = setInterval(() => openIndex.value === list.length
? openIndex.value = 0
: openIndex.value++
, 1000)

onBeforeUnmount(() => clearInterval(interval))

return { list, openIndex }
})
</script>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

[open] > summary > svg { transform: none }
</style>
复制代码

运行结果:



既然能靠控制open属性来控制元素的展开收起,那么手风琴效果也很好实现了:只需要保证在当前列表中仅有一个<details>标签有open属性,点击别的标签时就去掉另一个标签的open属性即可:


<template>
<details
v-for="({title, content}, index) of list"
:key="title"
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
{{ content }}
</details>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: 'index.html'
}, {
title: 'css',
content: 'reset.css'
}, {
title: 'js',
content: 'main.js'
}]

const openIndex = ref(-1)

const onChange = ({ target }, i) => target.open && (openIndex.value = i)

return { list, openIndex, onChange }
})
</script>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

[open] > summary > svg { transform: none }
</style>
复制代码

运行结果:




⚠️需要注意的是,在<details>标签展开收起时会触发一个 toggle 事件,和 click、mousemove 等事件用法一致,也会接收一个 event 对象的参数,event.target 是当前触发事件的 DOM,也就是<details>,它会有一个.open属性,值为 true 或 false,代表是否展开收起。



加入动画


那么接下来离一个理想的手风琴效果只差最后一步了:过渡动画


但过渡动画这里有坑,我们先来分析一下思路:在平时就给<details>标签里的内容区(除第一个出现的

标签以外的内容)写上:max-height: 0;

然后在 open 时用属性选择器 [open] 配合后代选择器来给内容区加上 max-height: xxx; 的代码,这样平时在收起时高度就是0,等出现 open 属性时就会慢慢过渡到我们定义的最大高度:


<template>
<details
v-for="({title, content}, index) of list"
:key="title"
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
<ul>
<li v-for="doc of content" :key="doc">{{ doc }}</li>
</ul>
</details>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: ['index.html', 'banner.html', 'login.html', '404.html']
}, {
title: 'css',
content: ['reset.css', 'header.css', 'banner.css', 'footer.css']
}, {
title: 'js',
content: ['index.js', 'main.js', 'javascript.js']
}]

const openIndex = ref(-1)

const onChange = ({ target }, i) => target.open && (openIndex.value = i)

return { list, openIndex, onChange }
})
</script>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

details > ul {
max-height: 0;
margin: 0;
overflow: hidden;
}

[open] > summary > svg { transform: none }
[open] > ul { max-height: 120px }
</style>
复制代码

运行结果:



如果用谷歌浏览器打开的话居然看不到任何的过渡效果!但用火狐打开就有效果:



估计是浏览器的 bug,既然过渡动画(transition)在不同浏览器之间表现不一致,那关键帧动画(keyframes)呢?


<template>
<details
v-for="({title, content}, index) of list"
:key="title"
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
<ul>
<li v-for="doc of content" :key="doc">{{ doc }}</li>
</ul>
</details>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: ['index.html', 'banner.html', 'login.html', '404.html']
}, {
title: 'css',
content: ['reset.css', 'header.css', 'banner.css', 'footer.css']
}, {
title: 'js',
content: ['index.js', 'main.js', 'javascript.js']
}]

const openIndex = ref(-1)

const onChange = ({ target }, i) => target.open && (openIndex.value = i)

return { list, openIndex, onChange }
})
</script>

<style lang="scss">
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

details > ul {
max-height: 0;
margin: 0;
overflow: hidden;
}

[open] {
> summary > svg { transform: none }
> ul { animation: open .2s both }
}

@keyframes open {
to { max-height: 120px }
}
</style>
复制代码

运行结果:



可以看到关键帧动画在各大浏览器的行为都是一致的,推荐大家使用关键帧动画。


收起动画


上面那种效果已经完全足够满足我们的日常开发需求了,但它仍然有一个小小的遗憾,那就是:收起的时候没有任何的动画效果。



这是因为<details>的行为是靠着 open 属性控制内容显示或隐藏,你可以简单的把它的隐藏理解为display: block;display: none;,虽然这么说可能并不准确,但却非常有助于我们理解<details>的行为:在展开时display: block;突然显示,既然显示了就可以有时间展示我们的展开动画。但在收起时display: none;是突然消失,根本没时间展示我们的收起动画。



那么怎么才能解决这个问题呢?答案就是更改 DOM 结构,我们把原本放在<details>里面那部分需要展开收起的内容元素移到<details>标签的外面去,但一定要在它的后一位,这样就可以方便我们用兄弟选择器配合属性选择器来控制外部元素的显隐了,在<details>标签有 open 属性时我们就让它的后面一个元素用动画展开,没有 open 属性时我们就让后一个元素用动画收起:


<template>
<template v-for="({title, content}, index) of list" :key="title">
<details
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
</details>
<ul>
<li v-for="doc of content" :key="doc">{{ doc }}</li>
</ul>
</template>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: ['index.html', 'banner.html', 'login.html', '404.html']
}, {
title: 'css',
content: ['reset.css', 'header.css', 'banner.css', 'footer.css']
}, {
title: 'js',
content: ['index.js', 'main.js', 'javascript.js']
}]

const openIndex = ref(-1)

const onChange = ({ target }, i) => target.open && (openIndex.value = i)

return { list, openIndex, onChange }
})
</script>

<style lang="scss">
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

ul {
max-height: 0;
margin: 0;
transition: max-height .2s;
overflow: hidden
}

[open] {
> summary > svg { transform: none }
+ ul { max-height: 120px }
}
</style>
复制代码

运行结果:



结语


如果你的项目不需要这些花里胡哨的动画效果,完全可以只靠 H5 标签去实现,根本不必再去关心展开收起的逻辑了,只需要写一些样式代码就可以了,比如写成暗黑模式:



你的 CSS 只需要专注于暗黑模式本身就够了,是不是很省心呢?


同时这个收拉效果也并不仅仅只适用于手风琴,很多地方都可以用到它,比如这种:


但唯一比较遗憾的事就是这个标签不支持 IE:



不过好在别的浏览器支持的都不错,如果你的项目不需要兼容 IE 的话就请尽情的享受<details>标签所带来的便利吧!


作者:手撕红黑树
来源:https://juejin.cn/post/6912374170743472135
收起阅读 »

后端一次给你10万条数据,如何优雅展示,面试官到底考察我什么

背景面试题:后台传给前端十万条数据,你作为前端如何渲染到页面上?回答者A:我有句话不知当讲不当讲,这什么鬼需求。回答者B:滚,后端,我不要这样的数据,你就不能分页给我吗。回答C:10万条数据这怎么展示,展示了也看不完啊。分析:面试官既然能这么问,我们从技术的角...
继续阅读 »

背景

面试题:后台传给前端十万条数据,你作为前端如何渲染到页面上?

回答者A:我有句话不知当讲不当讲,这什么鬼需求。

回答者B:滚,后端,我不要这样的数据,你就不能分页给我吗。

回答C:10万条数据这怎么展示,展示了也看不完啊。

分析:

面试官既然能这么问,我们从技术的角度出发,探索一下这道题,上手操作了一下:

function loadAll(response) {
  var html = "";
  for (var i = 0; i < 100000; i++) {
      html += "<li>title:" + '我正在测试'+[i] + "</li>";
  }
          $("#content").html(html);
}

在chorme浏览器下面 非常卡顿,刷新页面数据非常卡顿,渲染页面大概花掉10秒左右的时间,卡顿非常明显,性能瓶颈是在将html字符串插入到文档中这个过程上, 也就是性能瓶颈是在将html字符串插入到文档中这个过程上,也就是$("#content").html(html); 这句代码的执行, 毕竟有10万个li元素要被挺入到文档里面, 页面渲染速度缓慢也在情理之中。

解决方案

既然一次渲染10万条数据会造成页面加载速度缓慢,那么我们可以不要一次性渲染这么多数据,而是分批次渲染, 比如一次10000条,分10次来完成, 这样或许会对页面的渲染速度有提升。 然而,如果这13次操作在同一个代码执行流程中运行,那似乎不但无法解决糟糕的页面卡顿问题,反而会将代码复杂化。 类似的问题在其它语言最佳的解决方案是使用多线程,JavaScript虽然没有多线程,但是setTimeout和setInterval两个函数却能起到和多线程差不多的效果。 因此,要解决这个问题, 其中的setTimeout便可以大显身手。 setTimeout函数的功能可以看作是在指定时间之后启动一个新的线程来完成任务。

ajax 请求。。。。

function loadAll(response) {
  //将10万条数据分组, 每组500条,一共200组
  var groups = group(response);
  for (var i = 0; i < groups.length; i++) {
      //闭包, 保持i值的正确性
      window.setTimeout(function () {
          var group = groups[i];
          var index = i + 1;
          return function () {
              //分批渲染
              loadPart( group, index );
          }
      }(), 1);
  }
}

//数据分组函数(每组500条)
function group(data) {
  var result = [];
  var groupItem;
  for (var i = 0; i < data.length; i++) {
      if (i % 500 == 0) {
          groupItem != null && result.push(groupItem);
          groupItem = [];
      }
      groupItem.push(data[i]);
  }
  result.push(groupItem);
  return result;
}
var currIndex = 0;
//加载某一批数据的函数
function loadPart( group, index ) {
  var html = "";
  for (var i = 0; i < group.length; i++) {
      var item = group[i];
      html += "<li>title:" + item.title + index + " content:" + item.content + index + "</li>";
  }
  //保证顺序不错乱
  while (index - currIndex == 1) {
      $("#content").append(html);
      currIndex = index;
  }
}

思考:

面试官为啥会问这样的问题呢?现实中会有这样的需求吗? 我们从技术的角度思考,其实就是考察setTimetout的知识点。面试官就是换汤不换药。当然,其实这道题还有其他的解决方案,可以在评论区讨论学习。


作者:zz
来源:https://juejin.cn/post/6986237263164211207

收起阅读 »

前端人员不要只知道KFC,你应该了解 BFC、IFC、GFC 和 FFC

前言说起KFC,大家都知道是肯德基🍟,但面试官问你什么是BFC、IFC、GFC和FFC的时候,你是否能够像回答KFC是肯德基时的迅速,又或者说后面这些你根本就没听说过,作为一名前端开发工程师,以上这些FC(Forrmatting Context)你都得知道,而...
继续阅读 »

前言

说起KFC,大家都知道是肯德基🍟,但面试官问你什么是BFC、IFC、GFC和FFC的时候,你是否能够像回答KFC是肯德基时的迅速,又或者说后面这些你根本就没听说过,作为一名前端开发工程师,以上这些FC(Forrmatting Context)你都得知道,而且必须得做到像肯德基这样印象深刻。下面我将会带大家一起揭开这些FC的真面目,如果你已经了解的请奖励自己一顿肯德基~(注意文明用语,这里别用语气词😂)

FC的全称是:Formatting Contexts,译作格式化上下文,是W3C CSS2.1规范中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用。

CSS2.1中只有BFC和IFC,CSS3中才有GFC和FFC。

如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~

前置概念

在学习各种FC之前,我们先来了解几个基本概念:

Box(CSS布局基本单位)

简单来讲,我们看到的所有页面都是由一个个Box组合而成的,元素的类型和display属性决定了Box的类型。

  • block-level Box: 当元素的 CSS 属性 displayblock, list-itemtable 时,它是块级元素 block-level。块级元素(比如<p>)视觉上呈现为块,竖直排列。 每个块级元素至少生成一个块级盒(block-level Box)参与 BFC ,称为主要块级盒(principal block-level box)。一些元素,比如<li>,生成额外的盒来放置项目符号,不过多数元素只生成一个主要块级盒。

  • Inline-level Box: 当元素的 CSS 属性 display 的计算值为inline,inline-blockinline-table 时,称它为行内级元素。视觉上它将内容与其它行内级元素排列为多行。典型的如段落内容,有文本或图片,都是行内级元素。行内级元素生成行内级盒(inline-level boxes),参与行内格式化上下文 IFC 。

  • flex container: 当元素的 CSS 属性 display 的计算值为 flexinline-flex ,称它为弹性容器display:flex这个值会导致一个元素生成一个块级(block-level)弹性容器框。display:inline-flex这个值会导致一个元素生成一个行内级(inline-level)弹性容器框。

  • grid container:*当元素的 CSS 属性 display 的计算值为 gridinline-grid,称它为*栅格容器

块容器盒(block container box)

只包含其它块级盒,或生成一个行内格式化上下文(inline formatting context),只包含行内盒的叫做块容器盒子

也就是说,块容器盒要么只包含行内级盒,要么只包含块级盒。

块级盒(block-level Box)是描述元素跟它的父元素与兄弟元素之间的表现。

块容器盒(block container box)描述元素跟它的后代之间的影响。

块盒(BLock Boxes)

同时是块容器盒的块级盒称为块盒(block boxes)

行盒(Line boxes)

行盒由行内格式化上下文(inline formatting context)产生的盒,用于表示一行。在块盒里面,行盒从块盒一边排版到另一边。 当有浮动时, 行盒从左浮动的最右边排版到右浮动的最左边。

OK,了解完上面这些概念,我们再来看我们本篇文章的重点内容(终于要揭开各种FC的庐山真面目了,期待~)

BFC(Block Formatting Contexts)块级格式化上下文

什么是BFC?

BFC 全称:Block Formatting Context, 名为 块级格式化上下文

W3C官方解释为:BFC它决定了元素如何对其内容进行定位,以及与其它元素的关系和相互作用,当涉及到可视化布局时,Block Formatting Context提供了一个环境,HTML在这个环境中按照一定的规则进行布局。

如何触发BFC?

  • 根元素或其它包含它的元素

  • 浮动 float: left/right/inherit

  • 绝对定位元素 position: absolute/fixed

  • 行内块display: inline-block

  • 表格单元格 display: table-cell

  • 表格标题 display: table-caption

  • 溢出元素 overflow: hidden/scroll/auto/inherit

  • 弹性盒子 display: flex/inline-flex

BFC布局规则

  • 内部的Box会在垂直方向,一个接一个地放置。

  • Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠。

  • 每个元素的margin box的左边, 与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。

  • BFC的区域不会与float box重叠。

  • BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。

  • 计算BFC的高度时,浮动元素也参与计算

BFC应用场景

解决块级元素垂直方向margin重叠

我们来看下面这种情况:

<style>
 .box{
   width:180px;
   height:180px;
   background:rosybrown;
   color:#fff;
   margin: 60px auto;
}
</style>
<body>
   <div class="box">nanjiu</div>
   <div class="box">南玖</div>
</body>

按我们习惯性思维,上面这个box的margin-bottom60px,下面这个box的margin-top也是60px,那他们垂直的间距按道理来说应该是120px才对。(可事实并非如此,我们可以来具体看一下)

bfc1.png 从图中我们可以看到,两个box的垂直间距只有60px,并不是120px!

这种情况下的margin边距为两者的最大值,而不是两者相加,那么我们可以使用BFC来解决这种margin塌陷的问题。

<style>
 .box{
   width:180px;
   height:180px;
   background:rosybrown;
   color:#fff;
   margin: 60px auto;
}
 .outer_box{
   overflow: hidden;
}
</style>
<body>
   <div class="outer_box">
       <div class="box">nanjiu</div>
   </div>
   <div class="box">南玖</div>
</body>

bfc2.png 由上面可以看到,我们通过给第一个box外面再包裹一层容器,并触发它形成BFC,此时的两个box就不属于同一个BFC了,它们的布局互不干扰,所以这时候他们的垂直间距就是两者间距相加了。

解决高度塌陷问题

我们再来看这种情况,内部box使用float脱离了普通文档流,导致外层容器没办法撑起高度,使得背景颜色没有显示出来。

<style>
 .box{
   float:left;
   width:180px;
   height:180px;
   background:rosybrown;
   color:#fff;
   margin: 60px;
}
 .outer_box{
   background:lightblue;
}
</style>
<body>
   <div class="outer_box">
       <div class="box">nanjiu</div>
       <div class="box">南玖</div>
   </div>
</body>

bfc3.png 从这张图,我们可以看到此时的外层容器的高度为0,导致背景颜色没有渲染出来,这种情况我们同样可以使用BFC来解决,可以直接为外层容器触发BFC,我们来看看效果:

<style>
 .box{
   float:left;
   width:180px;
   height:180px;
   background:rosybrown;
   color:#fff;
   margin: 60px;
}
.outer_box{
 display:inline-block;
 background:lightblue;
}
</style>
<body>
   <div class="outer_box">
       <div class="box">nanjiu</div>
       <div class="box">南玖</div>
   </div>
</body>

bfc4.png

清除浮动

在早期前端页面大多喜欢用浮动来布局,但浮动元素脱离普通文档流,会覆盖旁边内容:

<style>
.aside {
 float: left;
 width:180px;
 height: 300px;
 background:lightpink;
}
 .container{
   width:500px;
   height:400px;
   background:mediumturquoise;
}
</style>
<body>
   <div class="outer_box">
       <div class="aside">nanjiu</div>
       <div class="container">南玖</div>
   </div>
</body>

bfc5.png 我们可以通过触发后面这个元素形成BFC,从而来清楚浮动元素对其布局造成的影响

<style>
.aside {
 float: left;
 width:180px;
 height: 300px;
 background:lightpink;
}
 .container{
   width:500px;
   height:400px;
   background:mediumturquoise;
   overflow: hidden;
}
</style>
<body>
   <div class="outer_box">
       <div class="aside">nanjiu</div>
       <div class="container">南玖</div>
   </div>
</body>

bfc6.png

IFC(Inline Formatting Contexts)行内级格式化上下文

什么是IFC?

IFC全称:Inline Formatting Context,名为行级格式化上下文

如何触发IFC?

  • 块级元素中仅包含内联级别元素

形成条件非常简单,需要注意的是当IFC中有块级元素插入时,会产生两个匿名块将父元素分割开来,产生两个IFC。

IFC布局规则

  • 在一个IFC内,子元素是水平方向横向排列的,并且垂直方向起点为元素顶部。

  • 子元素只会计算横向样式空间,【padding、border、margin】,垂直方向样式空间不会被计算,【padding、border、margin】。

  • 在垂直方向上,子元素会以不同形式来对齐(vertical-align)

  • 能把在一行上的框都完全包含进去的一个矩形区域,被称为该行的行框(line box)。行框的宽度是由包含块(containing box)和与其中的浮动来决定。

  • IFC中的line box一般左右边贴紧其包含块,但float元素会优先排列。

  • IFC中的line box高度由 CSS 行高计算规则来确定,同个IFC下的多个line box高度可能会不同。

  • inline boxes的总宽度少于包含它们的line box时,其水平渲染规则由 text-align 属性值来决定。

  • 当一个inline box超过父元素的宽度时,它会被分割成多个boxes,这些boxes分布在多个line box中。如果子元素未设置强制换行的情况下,inline box将不可被分割,将会溢出父元素。

IFC应用场景

元素水平居中

当一个块要在环境中水平居中时,设置其为inline-block则会在外层产生IFC,通过text-align则可以使其水平居中。

<style>
/* IFC */
 .text_container{
   width: 650px;
   border: 3px solid salmon;
   margin-top:60px;
   text-align: center;
}
 strong,span{
   /* border:1px solid cornflowerblue; */
   margin: 20px;
   background-color: cornflowerblue;
   color:#fff;
}
</style>
<body>
   <div class="text_container">
       <strong>众里寻他千百度,南玖需要你关注</strong>
       <span>蓦然回首,那人却在,南玖前端交流群</span>
   </div>
</body>

ifc1.png

多行文本水平垂直居中

创建一个IFC,然后设置其vertical-align:middle,其他行内元素则可以在此父元素下垂直居中。

<style>
.text_container{
 text-align: center;
 line-height: 300px;
 width: 100%;
 height: 300px;
 background-color: turquoise;
 font-size: 0;
}
 p{
   line-height: normal;
   display: inline-block;
   vertical-align: middle;
   background-color: coral;
   font-size: 18px;
   padding: 10px;
   width: 360px;
   color: #fff;
}
</style>
<body>
 <div class="text_container">
   <p>
    东风夜放花千树,更吹落,星如雨。宝马雕车香满路。凤箫声动,玉壶光转,一夜鱼龙舞。蛾儿雪柳黄金缕,笑语盈盈暗香去。
     <strong>众里寻他千百度,蓦然回首,那人却在,灯火阑珊处。</strong>
   </p>
 </div>
</body>

ifc2.png

GFC(Grid Formatting Contexts)栅格格式化上下文

什么是GFC?

GFC全称:Grids Formatting Contexts,名为网格格式上下文

简介: CSS3引入的一种新的布局模型——Grids网格布局,目前暂未推广使用,使用频率较低,简单了解即可。 Grid 布局与 Flex 布局有一定的相似性,都可以指定容器内部多个项目的位置。但是,它们也存在重大区别。 Flex 布局是轴线布局,只能指定"项目"针对轴线的位置,可以看作是一维布局。Grid 布局则是将容器划分成"行"和"列",产生单元格,然后指定"项目所在"的单元格,可以看作是二维布局。Grid 布局远比 Flex 布局强大。

如何触发GFC?

当为一个元素设置display值为grid或者inline-grid的时候,此元素将会获得一个独立的渲染区域。

GFC布局规则

通过在网格容器(grid container)上定义网格定义行(grid definition rows)网格定义列(grid definition columns)属性各在网格项目(grid item)上定义网格行(grid row)和网格列(grid columns)为每一个网格项目(grid item)定义位置和空间(具体可以在MDN上查看)

GFC应用场景

任意魔方布局

这个布局使用用GFC可以轻松实现自由拼接效果,换成其他方法,一般会使用相对/绝对定位,或者flex来实现自由拼接效果,复杂程度将会提升好几个等级。

<style>
.magic{
display: grid;
grid-gap: 2px;
width:300px;
height:300px;
}
.magic div{
border: 1px solid coral;
}
.m_1{
grid-column-start: 1;
grid-column-end: 3;
}
.m_3{
grid-column-start: 2;
grid-column-end: 4;
grid-row-start: 2;
grid-row-end: 3;
}
</style>
<body>
<div>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<div>6</div>
<div>7</div>
</div>
</body>

gfc1.png

FFC(Flex Formatting Contexts)弹性格式化上下文

什么是FFC?

FFC全称:Flex Formatting Contexts,名为弹性格式上下文

简介: CSS3引入了一种新的布局模型——flex布局。 flex是flexible box的缩写,一般称之为弹性盒模型。和CSS3其他属性不一样,flexbox并不是一个属性,而是一个模块,包括多个CSS3属性。flex布局提供一种更加有效的方式来进行容器内的项目布局,以适应各种类型的显示设备和各种尺寸的屏幕,使用Flex box布局实际上就是声明创建了FFC(自适应格式上下文)

如何触发FFC?

display 的值为 flexinline-flex 时,将生成弹性容器(Flex Containers), 一个弹性容器为其内容建立了一个新的弹性格式化上下文环境(FFC)

FFC布局规则

  • 设置为 flex 的容器被渲染为一个块级元素

  • 设置为 inline-flex 的容器被渲染为一个行内元素

  • 弹性容器中的每一个子元素都是一个弹性项目。弹性项目可以是任意数量的。弹性容器外和弹性项目内的一切元素都不受影响。简单地说,Flexbox 定义了弹性容器内弹性项目该如何布局

⚠️注意:FFC布局中,float、clear、vertical-align属性不会生效。

Flex 布局是轴线布局,只能指定"项目"针对轴线的位置,可以看作是一维布局。Grid 布局则是将容器划分成"行"和"列",产生单元格,然后指定"项目所在"的单元格,可以看作是二维布局。Grid 布局远比 Flex 布局强大。

FFC应用场景

这里只介绍它对于其它布局所相对来说更方便的特点,其实flex布局现在是非常普遍的,很多前端人员都喜欢用flex来写页面布局,操作方便且灵活,兼容性好。

自动撑开剩余高度/宽度

看一个经典两栏布局:左边为侧边导航栏,右边为内容区域,用我们之前的常规布局,可能就需要使用到csscalc方法来动态计算剩余填充宽度了,但如果使用flex布局的话,只需要一个属性就能解决这个问题:

calc动态计算方法:

<style>
.outer_box {
width:100%;
}
.aside {
 float: left;
 width:180px;
 height: 300px;
 background:lightpink;
}
.container{
 width:calc(100% - 180px);
 height:400px;
 background:mediumturquoise;
 overflow: hidden;
}
</style>
<body>
<div class="outer_box">
       <div class="aside">nanjiu</div>
       <div class="container">南玖</div>
   </div>
</body>

ffc.gif 使用FFC:

<style>
.outer_box {
 display:flex;
width:100%;
}
.aside {
 float: left;
 width:180px;
 height: 300px;
 background:lightpink;
}
.container{
 flex: 1;
 height:400px;
 background:mediumturquoise;
 overflow: hidden;
}
</style>
<body>
<div class="outer_box">
       <div class="aside">nanjiu</div>
       <div class="container">南玖</div>
   </div>
</body>

ffc2.gif

总结

一般来说,FFC能做的事情,通过GFC都能搞定,反过来GFC能做的事通过FFC也能实现。 通常弹性布局使用FFC,二维网格布局使用GFC,所有的FFC与GFC也是一个BFC,在遵循自己的规范的情况下,向下兼容BFC规范。

现在所有的FC都介绍完了,了解清楚的去奖励自己一顿KFC吧😄~

推荐阅读

作者:南玖
来源:https://juejin.cn/post/7072174649735381029

收起阅读 »

经验复盘-力扣刷题给我带来了什么?

起因 虽然在去年跳槽前有刷过一段时间的力扣,但是一直都没有将自己的刷题的过程记录下来。直到2021年5月,掘金举办了一个Java 刷题打卡的活动,我才开始将自己的第一篇刷题文章发布在掘金👇 后面掘金又举办了2021年6月更文挑战活动,活动大奖要求有 30 篇...
继续阅读 »

起因


虽然在去年跳槽前有刷过一段时间的力扣,但是一直都没有将自己的刷题的过程记录下来。直到2021年5月,掘金举办了一个Java 刷题打卡的活动,我才开始将自己的第一篇刷题文章发布在掘金👇


image.png


后面掘金又举办了2021年6月更文挑战活动,活动大奖要求有 30 篇文章,最终这次活动中我输出了 23 篇力扣刷题的文章。


至今为止,我在掘金上已发表了 124 个力扣题解,有兴趣的话可以关注我的专栏:力扣刷题


image.png


我是怎么做的?


1. 代码存储


为了更方便的调试代码以及存储自己写的测试用例,所以我在Github上建了一个私人库用于存储代码。这样做的好处就是:随时随地获取你的历史代码、而且可以保留自己写的测试用例。我非常推荐你也使用仓库来存储自己的解题代码!


image.png


2.模仿和总结


刚开始刷题的时候我只会简单的暴力求解,所以在提交第一题的时候击败率连 10% 都不到,后来通过看官方题解发现了可以使用 HashMap 用空间换取时间(因 HashMap 中链表节点超过8时会转为红黑树)。


所以在刚开始很长一段时间我都是 题目看完 -> 思考15分钟,没有思路 -> 看官方题解,但经过了一段时间后我发现自己的提升很小。就像是一种脑子会了,手没会的感觉,下次碰到类似的题目还是不会做。


后来经过不断的摸索,我刷题的步骤大概是这样的:



  1. 看题目,列出题目中的重点

  2. 想清楚解题的关键和思路(碰到链表或二叉树相关的题目,我一般都会在草稿纸上画一下)

  3. 实在不会的可以看一下官方题解,但是不抄官方的代码

  4. 运行调试:写好代码后,将力扣的测试用例写在main方法中测试(像一些二叉树的题目就需要根据官方的二叉树数组自己转为 TreeNode 了)

  5. 提交代码,然后再在本地调试不通过的测试用例(一般来说需要调试二十分钟左右,不要着急!)

  6. 直到通过所有的测试用例


经过这段时间的刷题,我也有了一些小感悟:



  • 算法的本质就是利用空间换区时间

  • 有时候看到数据结构就能猜到用什么算法:例如碰到 二叉树 就会想到 递归、碰到 截取字符串 会想到 滑动窗口、碰到大问题可以分解的情况会想到 动态规划

  • 使用 HashMap 替代数组或链表,可有效降低时间复杂度

  • 遍历链表时一般都需要临时指针指向头结点

  • ......


我收获了什么?


虽然刷题碰到的一些算法在实际业务中无法很好落地,但是刷题能够对数据结构有更深刻的理解调试能力的提升(先将步骤分解,然后寻找非预期的输入或输出)、锻炼思维能力、以及对代码执行的效率更敏感了(减少时间复杂度)。


实际项目如何运用?



下面我举得这个例子就可以很好的体现:递归算法的运用和提高执行效率。



在常见的业务系统中,不可避免的会需要动态菜单和权限的功能。动态菜单本质上是一个 ,而某个菜单路由下面的权限就像是树中的 叶子节点。但是数据库并不能够存储树这样的结构,那怎么办呢?


常规的树形结构的路由菜单如下所示:


image.png


设计的数据库表如下所示:


parentId:表示父节点的Id  
menuType:当类型为 C 标识为路由下的权限,即二叉树中的叶子节点

image.png


实际业务需要将查找出来的列表转为树形结构返回给前端,这怎么实现呢?大致的思路如下所示:


    /**
* list转为树结构
*/
private List<MenuBO> list2Tree(List<MenuBO> list, Integer pId) {
List<MenuBO> tree = new ArrayList<>();
Iterator<MenuBO> it = list.iterator();
while (it.hasNext()) {
MenuBO m = it.next();
if (m.getParentId() == pId) {
tree.add(m);
// 已添加的元素删除掉
it.remove();
}
}
// 寻找子元素
tree.forEach(n -> n.setChildren(list2Tree(list, n.getId())));
return tree;
}

这段代码的注释比较清楚,就是找到原列表中所有 pid 相同的元素放入新列表,并将新列表设为孩子节点,直到所有节点都遍历完成。又因为一行记录只会成为树中的一个节点,故每个元素只需遍历一次。所以再将元素放入到新列表后,就在原列表中将此元素移除。算法复杂度从 O(N*logN) 变为了 0(N),大大提升了执行效率。


总结


去年的这个时候我是零基础开始刷题的,刚开始写一个中等难度的题目需要一两个小时,再加上写完题目后还要输出一篇文章,一般都要搞到凌晨左右。但现在我简单题已经可以重拳出击了,中等难度的题目解题的时间更少了,至于困难题还是需要看题解。


从我一年多刷题的经历来看,我总结出来两句话:



  1. 万事开头难

  2. 实践才是检验真理的唯一标准!


所以行动起来吧!就现在!



最后我想感谢一下稀土掘金!来这里不仅能刷沸点段子,还能写文章拿奖品。不得不说,掘金的活动实在是太多了(PS:奖品也很多)!


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

Flutter终极大杀器,一个它顶四个库!

每次新建Flutter项目,是我最痛苦的时间,每次去pub仓库找库,再复制粘贴到 pubspec.yaml ,再执行 flutter pub get 。这套操作往往需要重复十几次。毕竟Flutter大到路由,状态管理,小到工具类,国际化都需要库来支持,等找齐这...
继续阅读 »

每次新建Flutter项目,是我最痛苦的时间,每次去pub仓库找库,再复制粘贴到 pubspec.yaml ,再执行 flutter pub get 。这套操作往往需要重复十几次。毕竟Flutter大到路由,状态管理,小到工具类,国际化都需要库来支持,等找齐这些东西,终于可以准备开发的时候,半天已经过去了。


所幸,我在pub仓库发现了它,GetX,这玩意是真的牛皮,使用它大大小小开发了四五个项目后,确定了稳定性和性能后,决定进行分享一波。


本文只是简单分享,GetX没有官方文档,只有github的README,所以我结合自己的经验,整理了一份。


github


gitee


GetX为何物?




  • GetX 是 Flutter 上的一个轻量且强大的解决方案:高性能的状态管理、智能的依赖注入和便捷的路由管理。




  • GetX 有3个基本原则:



    • 性能: GetX 专注于性能和最小资源消耗。GetX 打包后的apk占用大小和运行时的内存占用与其他状态管理插件不相上下。如果你感兴趣,这里有一个性能测试

    • 效率: GetX 的语法非常简捷,并保持了极高的性能,能极大缩短你的开发时长。

    • 结构: GetX 可以将界面、逻辑、依赖和路由完全解耦,用起来更清爽,逻辑更清晰,代码更容易维护。




  • GetX 并不臃肿,却很轻量。如果你只使用状态管理,只有状态管理模块会被编译,其他没用到的东西都不会被编译到你的代码中。它拥有众多的功能,但这些功能都在独立的容器中,只有在使用后才会启动。




GetX能干什么?


GetX包含的功能:



  • 状态管理

  • 路由

  • 国际化

  • 工具类

  • IDE拓展

  • 工程化Cli

  • ......


GetX的优点?


对于一个菜鸟来说,它最大的优点当然是 简单易上手


举几个例子:


状态管理


创建一个 Controller 管理你的状态变量


class Controller extends GetxController{
var count = 0.obs;
increment() => count++;
}

然后直接使用


class Home extends StatelessWidget {

@override
Widget build(context) {

// 使用Get.put()实例化你的类,使其对当下的所有子路由可用。
final Controller c = Get.put(Controller());

return Scaffold(
// 使用Obx(()=>每当改变计数时,就更新Text()。
appBar: AppBar(title: Obx(() => Text("Clicks: ${c.count}"))),

// 用一个简单的Get.to()即可代替Navigator.push那8行,无需上下文!
body: Center(child: ElevatedButton(
child: Text("Go to Other"), onPressed: () => Get.to(Other()))),
floatingActionButton:
FloatingActionButton(child: Icon(Icons.add), onPressed: c.increment));
}
}

class Other extends StatelessWidget {
// 你可以让Get找到一个正在被其他页面使用的Controller,并将它返回给你。
final Controller c = Get.find();

@override
Widget build(context){
// 访问更新后的计数变量
return Scaffold(body: Center(child: Text("${c.count}")));
}
}

你只需要 put 一个 Controller 后,再将 widget 包裹在 Obx 中,这样就将 count 绑定在了你的 widget 中,只要 count 发生改变, widget 就很跟着更新。



注意,Controller是与Widget解耦的,只有进行了put才会进行绑定,所以是局部状态还是全局状态完全由你自己决定。



路由


GetX的路由最大的特点就是,不需要 context ,直接使用即可


导航到新页面


Get.to(NextScreen());

用别名导航到新页面。


Get.toNamed('/details');

要关闭snackbars, dialogs, bottomsheets或任何你通常会用Navigator.pop(context)关闭的东西。


Get.back();

进入下一个页面,但没有返回上一个页面的选项(用于闪屏页,登录页面等)。


Get.off(NextScreen());

进入下一个页面并取消之前的所有路由(在购物车、投票和测试中很有用)。


Get.offAll(NextScreen());

国际化


GetX的国际化尤其简单,我们只需要新建一个 Translations


import 'package:get/get.dart';

class Messages extends Translations {
@override
Map<String, Map<String, String>> get keys => {
'zh_CN': {
'hello': '你好 世界',
},
'de_DE': {
'hello': 'Hallo Welt',
}
};
}

并且将你的 MaterialApp 更改为 GetMaterialApp ,并绑定上刚刚创建的 Translations 类。



不用担心,GetMaterialApp支持所有MaterialApp的接口,它们是兼容的



return GetMaterialApp(
translations: Messages(), // 你的翻译
locale: Locale('zh', 'CN'), // 将会按照此处指定的语言翻译
fallbackLocale: Locale('en', 'US'), // 添加一个回调语言选项,以备上面指定的语言翻译不存在
);

然后直接使用


Text('title'.tr);

是的,你只需要在字符串后面加上 .tr 即可使用国际化功能


GetX Cli是何物?


GetX Cli是一个命令行脚本,类似vue cli,但更强大一些,它可以做到:



  • 创建项目

  • 项目工程化

  • 生成Model

  • 生成page

  • 生成view

  • 生成controller

  • 自定义controller模板

  • 生成翻译文件

  • ......


想要使用GetX Cli,你需要安装dart环境或者Flutter环境


然后直接安装即可使用


pub global activate get_cli 
# or
flutter pub global activate get_cli

创建项目


get create project:my_project

这个命令会调用 flutter create ,然后再执行 get init


项目工程化


get init

生成page


get create page:home

生成controller


get create controller:dialogcontroller on home

生成view


get create view:dialogview on home

生成model


get generate model on home with assets/models/user.json

更多详细命令去看文档


IDE拓展



结语


祝大家在编程路上飞黄腾达,越走越远。


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

Flutter桌面端开发——选择读取本地文件

file_selector 安装🛠 点击file_selector获取最新版本。以下是在编写本文章时的最新版本: file_selector: ^0.8.4+1 👻注意:在开发 macOS 端的程序时,还需要额外的操作,具体可以查看这里 了解🧩 在 file_...
继续阅读 »

file_selector


安装🛠


点击file_selector获取最新版本。以下是在编写本文章时的最新版本:


file_selector: ^0.8.4+1

👻注意:在开发 macOS 端的程序时,还需要额外的操作,具体可以查看这里


了解🧩


在 file_selector 插件中,我们需要了解对象 XTypeGroup 和方法 openFiles。


XTypeGroup的作用是使用给定的标签和文件扩展名创建一个新组,没有提供任何类型选项的组表示允许任何类型。


XTypeGroup 可以传入5个参数:



  • String? label:用来自己作区分

  • List<String>? extensions:过滤非描述的后缀文件,默认加载全部类型的文件

  • List<String>? mimeTypes:主要针对 Linux 系统下的文件类型

  • List<String>? macUTIs:主要针对 mac 系统下的文件类型

  • List<String>? webWildCards:主要针对网页开发时的类型


openFile 方法返回一个 XFile 对象,可以传入3个参数:



  • List acceptedTypeGroups = const []:接受的类型组,传入一个XTypeGroup列表

  • String? initialDirectory:初始化文件夹。打开文件对话框以加载文件并返回文件路径

  • String? confirmButtonText:弹窗“打开”按钮的显示文字


openFiles 方法返回 XFile 对象列表,传入的参数和 openFile 一样。


使用🥩


选择的文件我们最后需要读取出来,读取需要接受一个 String 类型的路径:


String path = '';

这里先以选择图片为例,我们需要先定义一个 XTypeGroup 对象:


final xType = XTypeGroup(label: '图片', extensions: ['jpg', 'png']);

选择单张图片


打开弹窗选取单个文件,使用 openFile 方法:


final XFile? file = await openFile(acceptedTypeGroups: [xType]);

将获取到的 XFile 对象的路径值传给 path。当然,并不是每次打开弹窗都会选择图片,所以需要判断一下:


if (file != null) {
path = file.path;
setState((){});
} else {
BotToast.showText(text: '你不选择图片打开干啥😤');
}

image


openFile 方法中还有两个属性,我们修改试一下:


final XFile? file = await openFile(
acceptedTypeGroups: [xType],
initialDirectory: r'C:\Users\ilgnefz\Pictures',
confirmButtonText: '嘿嘿嘿',
);

image


initialDirectory 属性貌似没用😕去看了官方的例子,也没用到过这个参数,以后就忽略它吧。


选择多张图片


选取多张图片,我们就需要定义一个路径的数组了:


final List<String> paths = []

XTypeGroup 对象和刚才的一样就行,重要的是使用 openFiles 方法:


final List<XFile> files = await openFiles(acceptedTypeGroups: [xType]);

将获取到的文件路径列表赋值给 paths:


if (file != null) {
paths.addAll(files.map((e) => e.path).toList());
setState((){});
} else {
BotToast.showText(text: '你不选择图片打开干啥😤');
}

好了,来看看效果如何


image


读取文本文件


读取文本文件,我们需要获取文件的名称和内容:


final String title = '';
final String content = '';

再更改一下 XTypeGroup 对象就行:


final XTypeGroup xType = XTypeGroup(label: '文本', extensions: ['txt']);
final XFile? file = await openFile(acceptedTypeGroups: [xType]);

将获取到的 XFile 对象的属性赋值给我们定义的对象:


if (file != null) {
title = file.name;
content = await file.readAsString();
setState((){});
} else {
BotToast.showText(text: '打开了个寂寞🙄');
}

image


存储文本文件


存储文本需要用到 XFile 对象中的 fromData 方法。让我们来看看这个方法中需要传入什么参数:



  • Uint8List bytes:存储的主要内容

  • String? mimeType:文件的 mine 类型

  • String? name:文件名?测试了毫无用处😑

  • int? length:不知道是什么的长度,反正无法截取内容😑

  • DateTime? lastModified:最后修改文件的时间

  • String? path:文件保存的路径?测试了毫无效果😑

  • CrossFileTestOverrides? overrides:覆盖CrossFile的某些方法用来测试


(以上几个参数要是有朋友测试出来了,可以告知一下😁)


在存储文本文件前,我们需要先知道应该存储在哪个文件夹:


final String? path = await getSavePath();

然后再把 XFile.fromData 需要的参数放进去:


if (path != null) {
// 将内容编码成utf8
final Uint8List fileData = const Utf8Encoder().convert(content);
const String fileMimeType = 'text/plain';
final XFile xFile = XFile.fromData(
fileData,
mimeType: fileMimeType,
name: title,
);
await xFile.saveTo(path);
} else {
BotToast.showText(text: '给你个眼神自己体会😑');
}

5


获取文件夹路径


读取文件夹路径需要使用 getDirectoryPath 方法:


final String? path = await getDirectoryPath();
if (path != null) {
title = '目录';
content = path;
setState((){});
}

6


file_picker


安装🛠


点击file_picker获取最新版本。以下是在编写本文章时的最新版本:


file_picker: ^4.5.1

使用🥩


先定义一个默认的路径:


String path = '';

选择单个文件


选择单个文件需要用到 pickFiles 方法,该方法可以传入10个参数:



  • String? dialogTitle:弹窗的标题

  • String? initialDirectory:初始化的文件夹

  • FileType type = FileType.any:文件的类型

  • List<String>? allowedExtensions:允许的文件后缀名称

  • dynamic Function(FilePickerStatus)? onFileLoading:监听文件选择的状态

  • bool allowCompression = true:是否允许压缩

  • bool allowMultiple = false:是否允许选择多个文件

  • bool withData = false:如果为true,选取的文件将在内存中立即以“Uint8List”的形式提供其字节数据,如果您选择它进行服务器上传或类似操作,这将很有用。但是,请记住,如果您允许多个选择或选择大文件,则在 IO(iOS 和 Android)上启用此功能可能会导致内存不足问题。请改用 [withReadStream]。在 web 上默认为 true,其他为 false

  • bool withReadStream = false:拾取的文件将以 [Stream<List>] 的形式提供其字节数据,这对于上传和处理大文件很有用

  • bool lockParentWindow = false:是否将子窗口(文件选择器窗口)一直停留在 Flutter 窗口的前面,直到它关闭(如模态窗口)。此参数仅适用于 Windows


FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result != null) {
File file = File(result.files.single.path!);
path = file.path;
setState((){});
}

image


我们试着添加一些参数:


FilePickerResult? result = await FilePicker.platform.pickFiles(
dialogTitle: '我的地盘我做主',
initialDirectory: r'C:\Users\ilgnefz\Pictures\Saved Pictures',
type: FileType.image,
);

8


initialDirectory 又没起作用😑


选择多个文件


定义一个接受所有路径的数组:


final List<String> paths = [];

FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: true,
);
if (result != null) {
paths = result.files.map((e) => e.path!).toList();
setState((){});
}

读取文件信息


通过以上的方法,我们会得到一个 PlatformFile 对象:


FilePickerResult? result = await FilePicker.platform.pickFiles();
PlatformFile file = result.files.single;

该对象有以下几个属性:



  • name:文件名称

  • size:文件大小,以字节为单位

  • bytes:此文件的字节数据。如果您想操作其数据或轻松上传到其他地方,则特别有用。 在常见问题解答中查看此处 一个关于如何使用它在网络上上传的示例。

  • extension:文件后缀

  • path:文件路径

  • identifier:原始文件的平台标识符,是指 Android 上的 Uri 和 iOS 上的 NSURL。其他为null

  • readStream:将文件内容转换成流读取


1


存储文件


存储文件需要使用 saveFile 方法,该方法有可以传入6个参数:



  • String? dialogTitle:同 pickFiles 方法

  • String? fileName:存储文件的名字

  • String? initialDirectory:同 pickFiles 方法

  • FileType type = FileType.any:同 pickFiles 方法

  • List ? allowedExtensions:同 pickFiles 方法

  • bool lockParentWindow = false:同 pickFiles 方法


String? outputFile = await FilePicker.platform.saveFile();

(⊙o⊙)…这个方法连保存内容的参数都没有,诶!就是玩😄。官方说这个方法没有实际意义。


获取文件夹路径


获取文件夹需要使用 getDirectoryPath 方法,可以传入3个参数:



  • String? dialogTitle:同 pickFiles 方法

  • bool lockParentWindow = false:同 pickFiles 方法

  • String? initialDirectory:同 pickFiles 方法


final String title = '';
final String content = '';

String? dir = await FilePicker.platform.getDirectoryPath();
if (path != null) {
title = '目录';
content = path;
setState((){});
}

9


🛫OK,以上就是这篇文章的全部内容,仅针对插件的当前版本,并不能保证适用于以后插件用法的更新迭代。


最后,感谢 flutter 团队和 miguelpruivo 对以上插件的开发和维护😁。本应用代码已上传至 githubgitee,有需要的可以下载下来查看学习。


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

MySQL模糊查询再也用不着 like+% 了

前言 我们都知道 InnoDB 在模糊查询数据时使用 "%xx" 会导致索引失效,但有时需求就是如此,类似这样的需求还有很多,例如,搜索引擎需要根基用户数据的关键字进行全文查找,电子商务网站需要根据用户的查询条件,在可能需要在商品的详细介绍中进行查找,这些都不...
继续阅读 »

前言


我们都知道 InnoDB 在模糊查询数据时使用 "%xx" 会导致索引失效,但有时需求就是如此,类似这样的需求还有很多,例如,搜索引擎需要根基用户数据的关键字进行全文查找,电子商务网站需要根据用户的查询条件,在可能需要在商品的详细介绍中进行查找,这些都不是B+树索引能很好完成的工作。


通过数值比较,范围过滤等就可以完成绝大多数我们需要的查询了。但是,如果希望通过关键字的匹配来进行查询过滤,那么就需要基于相似度的查询,而不是原来的精确数值比较,全文索引就是为这种场景设计的。


全文索引(Full-Text Search)是将存储于数据库中的整本书或整篇文章中的任意信息查找出来的技术。它可以根据需要获得全文中有关章、节、段、句、词等信息,也可以进行各种统计和分析。


在早期的 MySQL 中,InnoDB 并不支持全文检索技术,从 MySQL 5.6 开始,InnoDB 开始支持全文检索。


倒排索引


全文检索通常使用倒排索引(inverted index)来实现,倒排索引同 B+Tree 一样,也是一种索引结构。它在辅助表中存储了单词与单词自身在一个或多个文档中所在位置之间的映射,这通常利用关联数组实现,拥有两种表现形式:



  • inverted file index:{单词,单词所在文档的id}

  • full inverted index:{单词,(单词所在文档的id,再具体文档中的位置)}


MarkerHub


上图为 inverted file index 关联数组,可以看到其中单词"code"存在于文档1,4中,这样存储再进行全文查询就简单了,可以直接根据 Documents 得到包含查询关键字的文档;而 full inverted index 存储的是对,即(DocumentId,Position),因此其存储的倒排索引如下图,如关键字"code"存在于文档1的第6个单词和文档4的第8个单词。



相比之下,full inverted index 占用了更多的空间,但是能更好的定位数据,并扩充一些其他搜索特性。



MarkerHub


全文检索


创建全文索引


1、创建表时创建全文索引语法如下:

CREATE TABLE table_name ( id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, author VARCHAR(200), 
title VARCHAR(200), content TEXT(500), FULLTEXT full_index_name (col_name) ) ENGINE=InnoDB;
复制代码

输入查询语句:


SELECT table_id, name, space from INFORMATION_SCHEMA.INNODB_TABLES
WHERE name LIKE 'test/%';
复制代码

MarkerHub


上述六个索引表构成倒排索引,称为辅助索引表。当传入的文档被标记化时,单个词与位置信息和关联的DOC_ID,根据单词的第一个字符的字符集排序权重,在六个索引表中对单词进行完全排序和分区。


2、在已创建的表上创建全文索引语法如下:

CREATE FULLTEXT INDEX full_index_name ON table_name(col_name);
复制代码

使用全文索引


MySQL 数据库支持全文检索的查询,全文索引只能在 InnoDB 或 MyISAM 的表上使用,并且只能用于创建 char,varchar,text 类型的列。


其语法如下:


MATCH(col1,col2,...) AGAINST(expr[search_modifier])
search_modifier:
{
    IN NATURAL LANGUAGE MODE
    | IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION
    | IN BOOLEAN MODE
    | WITH QUERY EXPANSION
}
复制代码

全文搜索使用 MATCH() AGAINST()语法进行,其中,MATCH()采用逗号分隔的列表,命名要搜索的列。AGAINST()接收一个要搜索的字符串,以及一个要执行的搜索类型的可选修饰符。全文检索分为三种类型:自然语言搜索、布尔搜索、查询扩展搜索,下面将对各种查询模式进行介绍。


Natural Language


自然语言搜索将搜索字符串解释为自然人类语言中的短语,MATCH()默认采用 Natural Language 模式,其表示查询带有指定关键字的文档。


接下来结合demo来更好的理解Natural Language


SELECT
    count(*) AS count 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( 'MySQL' );
复制代码

MarkerHub


上述语句,查询 title,body 列中包含 'MySQL' 关键字的行数量。上述语句还可以这样写:


SELECT
    count(IF(MATCH ( title, body ) 
    against ( 'MySQL' ), 1, NULL )) AS count 
FROM
    `fts_articles`;
复制代码

上述两种语句虽然得到的结果是一样的,但从内部运行来看,第二句SQL的执行速度更快些,因为第一句SQL(基于where索引查询的方式)还需要进行相关性的排序统计,而第二种方式是不需要的。


还可以通过SQL语句查询相关性:


SELECT
    *,
    MATCH ( title, body ) against ( 'MySQL' ) AS Relevance 
FROM
    fts_articles;
复制代码

MarkerHub


相关性的计算依据以下四个条件:



  • word 是否在文档中出现

  • word 在文档中出现的次数

  • word 在索引列中的数量

  • 多少个文档包含该 word


对于 InnoDB 存储引擎的全文检索,还需要考虑以下的因素:



  • 查询的 word 在 stopword 列中,忽略该字符串的查询

  • 查询的 word 的字符长度是否在区间 [ innodb_ft_min_token_size,innodb_ft_max_token_size] 内


如果词在 stopword 中,则不对该词进行查询,如对 'for' 这个词进行查询,结果如下所示:


SELECT
    *,
    MATCH ( title, body ) against ( 'for' ) AS Relevance 
FROM
    fts_articles;
复制代码

MarkerHub


可以看到,'for'虽然在文档 2,4中出现,但由于其是 stopword ,故其相关性为0


参数 innodb_ft_min_token_sizeinnodb_ft_max_token_size 控制 InnoDB 引擎查询字符的长度,当长度小于 innodb_ft_min_token_size 或者长度大于 innodb_ft_max_token_size 时,会忽略该词的搜索。在 InnoDB 引擎中,参数 innodb_ft_min_token_size 的默认值是3,innodb_ft_max_token_size的默认值是84


Boolean


布尔搜索使用特殊查询语言的规则来解释搜索字符串,该字符串包含要搜索的词,它还可以包含指定要求的运算符,例如匹配行中必须存在或不存在某个词,或者它的权重应高于或低于通常情况。


例如,下面的语句要求查询有字符串"Pease"但没有"hot"的文档,其中+和-分别表示单词必须存在,或者一定不存在。


select * from fts_test where MATCH(content) AGAINST('+Pease -hot' IN BOOLEAN MODE);
复制代码

Boolean 全文检索支持的类型包括:



  • +:表示该 word 必须存在

  • -:表示该 word 必须不存在

  • (no operator)表示该 word 是可选的,但是如果出现,其相关性会更高

  • @distance表示查询的多个单词之间的距离是否在 distance 之内,distance 的单位是字节,这种全文检索的查询也称为 Proximity Search,如 MATCH(context) AGAINST('"Pease hot"@30' IN BOOLEAN MODE)语句表示字符串 Pease 和 hot 之间的距离需在30字节内

  • >:表示出现该单词时增加相关性

  • <:表示出现该单词时降低相关性

  • ~:表示允许出现该单词,但出现时相关性为负

  • * :表示以该单词开头的单词,如 lik*,表示可以是 lik,like,likes

  • " :表示短语


下面是一些demo,看看 Boolean Mode 是如何使用的。


demo1:+ -


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( '+MySQL -YourSQL' IN BOOLEAN MODE );
复制代码

上述语句,查询的是包含 'MySQL' 但不包含 'YourSQL' 的信息


MarkerHub


demo2: no operator


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( 'MySQL IBM' IN BOOLEAN MODE );
复制代码

上述语句,查询的 'MySQL IBM' 没有 '+','-'的标识,代表 word 是可选的,如果出现,其相关性会更高


MarkerHub


demo3:@


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( '"DB2 IBM"@3' IN BOOLEAN MODE );
复制代码

上述语句,代表 "DB2" ,"IBM"两个词之间的距离在3字节之内


MarkerHub


demo4:> <


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( '+MySQL +(>database <DBMS)' IN BOOLEAN MODE );
复制代码

上述语句,查询同时包含 'MySQL','database','DBMS' 的行信息,但不包含'DBMS'的行的相关性高于包含'DBMS'的行。


MarkerHub


demo5: ~


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( 'MySQL ~database' IN BOOLEAN MODE );
复制代码

上述语句,查询包含 'MySQL' 的行,但如果该行同时包含 'database',则降低相关性。


MarkerHub


demo6:*


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( 'My*' IN BOOLEAN MODE );
复制代码

上述语句,查询关键字中包含'My'的行信息。


MarkerHub


demo7:"


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( '"MySQL Security"' IN BOOLEAN MODE );
复制代码

上述语句,查询包含确切短语 'MySQL Security' 的行信息。


MarkerHub


Query Expansion


查询扩展搜索是对自然语言搜索的修改,这种查询通常在查询的关键词太短,用户需要 implied knowledge(隐含知识)时进行,例如,对于单词 database 的查询,用户可能希望查询的不仅仅是包含 database 的文档,可能还指那些包含 MySQL、Oracle、RDBMS 的单词,而这时可以使用 Query Expansion 模式来开启全文检索的 implied knowledge通过在查询语句中添加 WITH QUERY EXPANSION / IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION 可以开启 blind query expansion(又称为 automatic relevance feedback),该查询分为两个阶段。



  • 第一阶段:根据搜索的单词进行全文索引查询

  • 第二阶段:根据第一阶段产生的分词再进行一次全文检索的查询


接着来看一个例子,看看 Query Expansion 是如何使用的。


-- 创建索引
create FULLTEXT INDEX title_body_index on fts_articles(title,body);
复制代码

-- 使用 Natural Language 模式查询
SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH(title,body) AGAINST('database');
复制代码

使用 Query Expansion 前查询结果如下:


MarkerHub


-- 当使用 Query Expansion 模式查询
SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH(title,body) AGAINST('database' WITH QUERY expansion);
复制代码

使用 Query Expansion 后查询结果如下:


MarkerHub


由于 Query Expansion 的全文检索可能带来许多非相关性的查询,因此在使用时,用户可能需要非常谨慎。


删除全文索引


1、直接删除全文索引语法如下:

DROP INDEX full_idx_name ON db_name.table_name;
复制代码

2、使用 alter table 删除全文索引语法如下:

ALTER TABLE db_name.table_name DROP INDEX full_idx_name;
复制代码


来源:juejin.cn/post/6989871497040887845





推荐3个原创springboot+Vue项目,有完整视频讲解与文档和源码:


【dailyhub】【实战】带你从0搭建一个Springboot+elasticsearch+canal的完整项目



【VueAdmin】手把手教你开发SpringBoot+Jwt+Vue的前后端分离后台管理系统



【VueBlog】基于SpringBoot+Vue开发的前后端分离博客项目完整教学



作者:MarkerHub
来源:https://juejin.cn/post/7072257652784365599
收起阅读 »

被尤雨溪推荐,这款开箱即用的Vue3组件库做对了什么

相信很多开发者都有过这样的想法:因为对某个技术栈或明星开源项目感兴趣,产生了开发拓展方向的新项目的想法与实践,同时也希冀于这个全新的开源项目也能如同别的优质开源项目一样受到关注,只是并非每个项目都能登上热门,获得高额 star 数。 不过,今天马建仓介绍的这...
继续阅读 »

相信很多开发者都有过这样的想法:因为对某个技术栈或明星开源项目感兴趣,产生了开发拓展方向的新项目的想法与实践,同时也希冀于这个全新的开源项目也能如同别的优质开源项目一样受到关注,只是并非每个项目都能登上热门,获得高额 star 数。



不过,今天马建仓介绍的这款开源项目的开发者,就曾在过去一年里实现了从零到一的华丽逆袭,让我们一起来瞧瞧这究竟是什么宝藏项目。


Varlet 是一个基于 Vue3 开发的 Material 风格移动端组件库,并在今年的 Vue JS Live 上被 Vue 的作者尤雨溪推荐。然而自这个项目诞生的时间不到一年。


从 Varlet 作者的某技术博客上得知,作者是一位专科毕业、在无锡工作的四川前端开发。去年,因所属单位打算开发某个与 Vue3 相关的组件库,机缘巧合下,作者自告奋勇包揽下这个活。然而,公司却因成本、投资回报等原因并不打算提供支持,随后作者搭档两位好友决心继续坚持下去。



这个组件库是基于 Material Design 的设计进行规范的,在此期间作者与合作的小伙伴们共同参考社区成品以及结合国内开发者感兴趣的 api 。对于为何选择 Material,作者在官方文档中这样描述:



在早期的移动端设备中,大色块以及强烈对比色,对显示设备要求很高,同时非线性动画和水波纹对 GPU 有一定要求。 导致 Material 风格并没有在移动端浏览器环境下有很好的体验,更多选择更扁平朴素的风格投入产品。 但随着现代设备和新的 js 框架运行时处理的效率的逐步提升,浏览器有了更多的空闲时间和能力去处理动画效果,Material Design 将会给应用带来更好的体验。



经历了多次的反复推敲之后,组件库隐约有了个雏形。打这时起, Varlet 也正式开源,并采用 MIT 开源许可证。



之后的日子里,Varlet 不仅获得阮一峰老师的推荐,同时也得到了国外开源技术社区的认可,其中 Vite 核心团队的 Antfu 大神也接受了这个组件库的 PR。不久前,在 Vue3 的 2021 年度总结分享会上,尤雨溪大神也推荐了 Varlet 。前段时间,在 Gitee 上开源的 varlet-ui 项目经过评估,也获得了Gitee的推荐,项目地址:gitee.com/varlet/varl…


那么 Varlet 究竟有着怎样的魅力,吸引着这么多大神与优质平台的推广呢?




从特性上看



  • 提供50个高质量通用组件

  • 组件十分轻量

  • 由国人开发,完善的中英文文档和后勤保障

  • 支持按需引入

  • 支持主题定制

  • 支持国际化

  • 支持 webstorm,vscode 组件属性高亮

  • 支持 SSR

  • 支持 Typescript

  • 确保90%以上单元测试覆盖率,提供稳定性保证

  • 支持暗黑模式


如何安装与部署


CDN


varlet.js 包含组件库的所有样式和逻辑, 因此只需引入即可。


<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue@next"></script>
<script src="https://cdn.jsdelivr.net/npm/@varlet/ui/umd/varlet.js"></script>
<script>
  const app = Vue.createApp({
    template: '<var-button>按钮</var-button>'
  })
  app.use(Varlet).mount('#app')
</script>
复制代码

Webpack/Vite


# 通过 npm、yarn 或 pnpm 安装

# npm
npm i @varlet/ui -S

# yarn
yarn add @varlet/ui

# pnpm
pnpm add @varlet/ui
复制代码

import App from './App.vue'
import Varlet from '@varlet/ui'
import { createApp } from 'vue'
import '@varlet/ui/es/style.js'

createApp(App).use(Varlet).mount('#app')
复制代码

如何引入?



手动引入


每一个组件都是一个 Vue 插件,并由组件逻辑和样式文件组成,如下方式进行手动引入使用。


import { createApp } from 'vue'
import { Button } from '@varlet/ui'
import '@varlet/ui/es/button/style/index.js'

createApp().use(Button)
复制代码

自动引入


所有在模板中的组件,都会被 unplugin-vue-components 插件自动扫描,插件会自动引入组件逻辑和样式文件并注册组件。


# 安装插件

# npm
npm i unplugin-vue-components -D

# yarn
yarn add unplugin-vue-components -D

# pnpm
pnpm add unplugin-vue-components -D
复制代码

Vue Cli


// vue.config.js
const Components = require('unplugin-vue-components/webpack')
const { VarletUIResolver } = require('unplugin-vue-components/resolvers')

module.exports = {
  configureWebpack: {
    plugins: [
      Components({
        resolvers: [VarletUIResolver()]
      })
    ]
  }
}
复制代码

Vite


// vite.config.js
import vue from '@vitejs/plugin-vue'
import components from 'unplugin-vue-components/vite'
import { VarletUIResolver } from 'unplugin-vue-components/resolvers'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    vue(),
    components({
      resolvers: [VarletUIResolver()]
    })
  ]
})
复制代码

注意


完成配置后如下使用即可


<template>
  <var-button>默认按钮</var-button>
</template>
复制代码

如何切换主题


该项目提供了暗黑模式的主题,暗黑模式的优势在于在弱光环境下具有更高的可读性。



<var-button block @click="toggleTheme">切换主题</var-button>
复制代码

import dark from '@varlet/ui/es/themes/dark'
import { StyleProvider } from '@varlet/ui'

export default {
  setup() {
    let currentTheme
    
    const toggleTheme = () => {
      currentTheme = currentTheme ? null : dark
      StyleProvider(currentTheme)
    }
    
    return { toggleTheme }
  }
}
复制代码

注入组件库推荐的文字颜色和背景颜色变量来控制整体颜色


body {
  transition: background-color .25s;
  color: var(--color-text);
  background-color: var(--color-body);
}
复制代码

样式展示




在线编辑地址


前往下列网址:varlet.gitee.io/varlet-ui/#…


点击界面右上方:


作者:Gitee
来源:https://juejin.cn/post/7075162881498562590
收起阅读 »

平时的工作如何体现一个人的技术深度?

今天在公司内网看到一个讨论帖,原文如下:平时的工作如何体现一个人的技术深度?平时工作中很多时候需求细而碎的,如何在工作中积累技术深度?又如何体现一个人的技术深度? 思考:做需求与做需求的差异 再回答问题之前,我想先抛开「技术深度」这次词,讲讲做需求这件事,说说...
继续阅读 »

今天在公司内网看到一个讨论帖,原文如下:

平时的工作如何体现一个人的技术深度?

平时工作中很多时候需求细而碎的,如何在工作中积累技术深度?又如何体现一个人的技术深度?

思考:做需求与做需求的差异


再回答问题之前,我想先抛开「技术深度」这次词,讲讲做需求这件事,说说我对做需求的理解。每一个程序员都是从刚毕业做需求开始,为什么有的人逐渐成为大牛,主导大型技术项目或走向团队管理岗位,而有的人一直还在做需求。我觉得这里面的差异在于,每一个对做需求这件事的理解有所不同。


这里面的差异在于,你是抱着一种什么样的心态去完成这个需求,是把这个需求做到极致,还是只是当做任务完成这个需求,达到产品想要的功能就行。这两个表面上看似差不多其实相差极大,差异在于,你有没有站在更高的角度,希望这件事做到完美。从需求角度有没有思考产品设计当中的缺陷,能不能反向为产品设计提供建议,从技术角度能不能做到高质量高兼容性无bug,以及下次再有类似的需求能不能快速高效率的迭代。


用一句话来描述就是,能不能跳出自己是一个程序员是一个被动执行人的角色,而是将自己当做产品当做技术负责人的心态去做这件事


业务需求该怎么做


知易行难,如果一开始做不到,那就先着眼小事,关注细节,从需求开始的需求评审,编写技术方案文档,设计文档,到开发的代码注释,结构设计,保证高质量,完善无漏洞的代码逻辑,再到异常埋点,指标监控,线上可用性运维等等,认真对待一个需求的每一个环节。


当你自认为已经做好整个流程的每一件小事之后,接下来可以 通过深入细节,思考整个流程是否存在问题。做需求过程中沟通协作的有没有问题,流程规范的有没有问题,机制环节哪方面问题,代码公共基础能力的是否有缺失,开发过程中你所遇到的问题是不是一个通用问题,能不能抽象出一个公共库解决大家的问题,能不能制定一个SOP的解决方案流程,亦或是提炼出一个最佳实践在组内外分享经验。


通过这些一件件小事来锻炼自己解决问题的能力,以及更深层级的发现问题的能力。再通过不断的发现问题,思考问题出现的原因,拿出解决方案,最终落地解决了自己或组内或协作方的问题,锻炼自己的综合能力逐步慢慢成长。


再说「技术深度」


说了这么多,你可能会说,这跟我问的技术深度有什么关系?我想说:抛开业务需求谈技术深度都是耍流氓


举一个例子,数据可视化方面3D three.js,视频直播方面的编解码压缩,客户端安全方面的攻防渗透,每一个都是有技术深度的事情,但问题是即使你掌握了这些领域拥有了非常高的技术深度之后呢,不能应用于业务需求,不能解决产品急迫要解决的问题,不能完成你老板的OKR,达成部门的战略目标,还是英雄无用武之地(当然你也可以选择一个可以用得上的团队,那是就是另外一回事了)。


由于这些单点的有技术深度的事情,不能为你带来直观和显而易见的 「回报」(也就是颜如玉 黄金屋与金榜题名),也就间接的打击了积极性(当然自己对某门技术感兴趣而钻研的不再本次讨论之列)。所以提升自己的技术深度,最好的方式还是在公司业务中,发现有深度的事,然后去在攻克这个问题的过程中,提升了自己的技术深度,即跟随公司业务的发展的同时自身也获得了成长,你用技术能力为公司解决了业务发展过程中的问题,自然也就从公司获得了该有的回报。这是一个ROI投入产出比最高的获得技术深度的方式。


获取做有深度事情的授权


当想明白获取技术深度的路径之后,接下来要解决的就是,如何让领导给你安排有技术深度的事情?


业务发展中有很多有技术深度有技术难度的事情,为什么领导要安排你来做这件事呢?你凭什么让领导觉得你 「有能力」「有意愿」 来完成这件事?能力与意愿是作为领导在分配工作当中的最重要的两个决策项(有机会的话我会再写一篇,从管理视角是如何做分工的)。既然你能提问如何积累技术深度,我相信你一定是有强烈意愿的,那么剩下的就是如何让领导认为你有完成这个有技术深度的事情的能力?关于这个问题,可以参考我之前写的一篇回答:如何管理leader对你的能力预期? 最简单来讲就是我在前面讲的,你能不能在开发需求中做到深度思考,追求极致,精益求精,有责任心,有主人翁意识与主R意识,在每件小事中能做到 「自闭环」,之后才会逐步让你承担更大范围更高挑战更大深度的事情,形成正向循环。


这也是我前面为什么要先重点强调做好每一件小事的重要性。


技术深度不是唯一标准


作为一个程序员,在职业生涯的初期,确实是以技术深度也就是技术能力作为最大的衡量标准。但随着职业生涯的发展,职级从L5到L8,站在从公司角度,对一个人的需求,也会从能完成一个业务需求,变成能带领一帮人完成一个更大的维度的需求,能不能为组织解决问题,为事业部达成战略目标,对人的要求的重心也会慢慢发生变化,这种变化可以参考公司的职级能力模型体系的雷达图。


所以一味的追求积累技术深度就跑偏了,但不是说技术深度不重要,技术能力是作为程序员的安身立命之本,但是在积累技术深度的同时,也需要学习锻炼技术深度以外的能力。具体到底是什么其他能力,这就够再展开好几篇文章的篇幅了,今天在这就不细说了,有机会可以谈谈我对这方面的理解。


最后


故不积跬步无以至千里,不积小流无以成江海。先从做好每一件小事开始,把每个业务需求做到120分,深度思考,发现问题,解决问题,逐步建立起靠谱有责任心技术牛的人设,逐步负责有技术难度的事情,跟随公司业务发展积累自己的业务领域经验与技术深度,从而获得双赢的回报。


这是我对如何积累技术深度这件事的理解,或许会有一些片面和偏激,毕竟不是谁都有一个能知人善任的好领导,不是谁都能遇到一个快速发展的业务,不是谁都能遇到有技术难度与技术挑战的场景,无论我怎么说,都会有幸存者偏差的存在。


努力与机遇并存,机遇可遇不可求,所以我们能做的事,就是学会正确做事的思路和方法,并为之坚持不懈的践行它。知易行难,学会方法容易,坚持践行难于上青天。自己该做的都做好了,机遇来了就可以抓住,即使抓不住,你也有了「选择的能力」,有了选择更好机遇更好公司的能力


以上均为个人主观且片面的看法,欢迎批评讨论~~。


作者:沧海月明FE
来源:https://juejin.cn/post/7073001183123603470 收起阅读 »

Flutter好用的轮子推荐02:拥有炫酷光影效果的拟态风格UI套件

前言 Flutter 是 Google 开源的应用开发框架,仅通过一套代码就能构建支持Android、iOS、Windows、Linux等多平台的应用。Flutter的性能非常高,拥有120fps的刷新率,也是目前非常流行的跨平台UI开发框架。 本专栏为大家收...
继续阅读 »

前言


Flutter 是 Google 开源的应用开发框架,仅通过一套代码就能构建支持Android、iOS、Windows、Linux等多平台的应用。Flutter的性能非常高,拥有120fps的刷新率,也是目前非常流行的跨平台UI开发框架。


本专栏为大家收集了Github上近70个优秀开源库,后续也将持续更新。希望可以帮助大家提升搬砖效率,同时祝愿Flutter的生态越来越完善🎉🎉。


正文


一、🚀 轮子介绍




  • 名称:flutter_neumorphic




  • 概述:易用的拟态风格UI套件,几乎可以在任何App中使用它。




  • 作者:idean Team




  • 仓库地址:Flutter-Neumorphic




  • 推荐指数: ⭐️⭐️⭐️⭐️⭐️




  • 常用指数: ⭐️⭐️⭐️⭐️⭐️




  • 效果预览:




flutter_logo_small.gif


二、⚙️ 安装及使用


dependencies:
flutter_neumorphic: ^3.0.3

import 'package:flutter_neumorphic/flutter_neumorphic.dart';

三、🔧 常用属性


1.基本



















































属性描述
LightSource特定于theme或小组件的光源,用于投射浅色/深色阴影
shape拟态容器中使用的曲线形状
Depth小组件与父组件的垂直距离
Intensity光的强度,它影响阴影的颜色
SurfaceIntensity组件表面的明暗效果
Color拟态组件的默认颜色
Accent拟态组件的选中颜色,例如复选框
Variant拟态组件的次要颜色
BoxShape拟态组件形状
Border边框

2.Shapes


image.png


四、🗂 内置组件介绍


tips:为了更直观的展示效果,本文案例已将组件和背景设置为同一色值的浅灰色。


1.Neumorphic


一个基本的拟态容器组件,可根据光源、高度(深度)添加浅色/深色渐变的容器。


container.gif


NeumorphicStyle(
depth: 3,
lightSource: LightSource.left,
color: Colors.grey[200],
),
child: const SizedBox(
width: 200,
height: 200,
),
)

2.NeumorphicButton


拟态按钮,默认按下有高度变化及震动反馈


button-2.gif


NeumorphicButton(
style: NeumorphicStyle(
boxShape: NeumorphicBoxShape.roundRect(
BorderRadius.circular(12),
),
color: Colors.grey[200],
shape: NeumorphicShape.flat,
),
child: Container(
color: Colors.grey[200],
width: 100,
height: 25,
child: const Center(
child: Text('Click me'),
),
),
onPressed: () {},
)

3.NeumorphicRadio


单选按钮


radio.gif


class NeumorphicButtonWidget extends StatefulWidget {
const NeumorphicButtonWidget({Key? key}) : super(key: key);
@override
State<NeumorphicButtonWidget> createState() => _NeumorphicButtonWidgetState();
}

class _NeumorphicButtonWidgetState extends State<NeumorphicButtonWidget> {
int _groupValue = 1;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
getChild('A', 1),
const SizedBox(width: 12),
getChild('B', 2),
const SizedBox(width: 12),
getChild('C', 3),
],
);
}
Widget getChild(String str, int value) {
return NeumorphicRadio(
child: Container(
color: Colors.grey[200],
height: 50,
width: 50,
child: Center(
child: Text(str))),
value: value,
groupValue: _groupValue,
onChanged: (value) {
setState(() {
_groupValue = value as int;
});
});
}}

4.NeumorphicCheckbox


多选按钮


checkbox.gif


class NeumorphicCheckboxWidget extends StatefulWidget {
const NeumorphicCheckboxWidget({Key? key}) : super(key: key);
@override
State<NeumorphicCheckboxWidget> createState() => _NeumorphicCheckboxWidgetState();
}

class _NeumorphicCheckboxWidgetState extends State<NeumorphicCheckboxWidget> {
bool check1 = false;
bool check2 = false;
bool check3 = false;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const SizedBox(width: 12),
NeumorphicCheckbox(
value: check1,
onChanged: (value) {
setState(() {
check1 = value;
});
},
),
const SizedBox(width: 12),
NeumorphicCheckbox(
value: check2,
onChanged: (value) {
setState(() {
check2 = value;
});
},
),
const SizedBox(width: 12),
NeumorphicCheckbox(
value: check3,
onChanged: (value) {
setState(() {
check3 = value;
});
},
),
],
);
}
}

5.NeumorphicText


拟态文字


text.png


NeumorphicText(
'Flutter',
textStyle: NeumorphicTextStyle(
fontSize: 80,
fontWeight: FontWeight.w900,
),
style: NeumorphicStyle(
depth: 3,
lightSource: LightSource.left,
color: Colors.grey[200],
),
)

6.NeumorphicIcon


拟态图标


icon.png


NeumorphicIcon(
Icons.public,
size: 180,
style: NeumorphicStyle(
depth: 3,
lightSource: LightSource.left,
color: Colors.grey[200],
),
);

7.material.TextField


拟态文本输入框


textfield.png


Neumorphic(
margin: const EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 4),
style: NeumorphicStyle(
depth: NeumorphicTheme.embossDepth(context),
boxShape: const NeumorphicBoxShape.stadium(),
color: Colors.grey[200]),
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 18),
child: const TextField(
decoration: InputDecoration.collapsed(hintText: 'NeumorphicTextField'),
),
);

8.NeumorphicSwitch


拟态开关


switch.gif


class NeumorphicSwitchWidget extends StatefulWidget {
const NeumorphicSwitchWidget({Key? key}) : super(key: key);
@override
State<NeumorphicSwitchWidget> createState() => _NeumorphicSwitchWidgetState();
}
class _NeumorphicSwitchWidgetState extends State<NeumorphicSwitchWidget> {
bool isChecked = false;
bool isEnabled = true;
@override
Widget build(BuildContext context) {
return NeumorphicSwitch(
style: NeumorphicSwitchStyle(
trackDepth: 3,
activeThumbColor: Colors.grey[200], // 开启时按钮颜色
activeTrackColor: Colors.green, // 开启时背景颜色
inactiveThumbColor: Colors.green, // 关闭时按钮颜色
inactiveTrackColor: Colors.grey[200], // 关闭时背景颜色
),
isEnabled: isEnabled,
value: isChecked,
onChanged: (value) {
setState(() {
isChecked = value;
});
},
);
}}

9.NeumorphicToggle


拟态滑动选择器


toggle.gif


class NeumorphicToggleWidget extends StatefulWidget {
const NeumorphicToggleWidget({Key? key}) : super(key: key);
@override
State<NeumorphicToggleWidget> createState() => _NeumorphicToggleWidgetState();
}
class _NeumorphicToggleWidgetState extends State<NeumorphicToggleWidget> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return NeumorphicToggle(
height: 50,
style: NeumorphicToggleStyle(
backgroundColor: Colors.grey[200],
),
selectedIndex: _selectedIndex,
displayForegroundOnlyIfSelected: true,
children: [
ToggleElement(
background: const Center(
child: Text(
"This week",
style: TextStyle(fontWeight: FontWeight.w500),
)),
foreground: const Center(
child: Text(
"This week",
style: TextStyle(fontWeight: FontWeight.w700),
)),
),
ToggleElement(
background: const Center(
child: Text(
"This month",
style: TextStyle(fontWeight: FontWeight.w500),
)),
foreground: const Center(
child: Text(
"This month",
style: TextStyle(fontWeight: FontWeight.w700),
)),
),
ToggleElement(
background: const Center(
child: Text(
"This year",
style: TextStyle(fontWeight: FontWeight.w500),
)),
foreground: const Center(
child: Text(
"This year",
style: TextStyle(fontWeight: FontWeight.w700),
)),
)
],
thumb: Neumorphic(
style: NeumorphicStyle(
boxShape: NeumorphicBoxShape.roundRect(
const BorderRadius.all(Radius.circular(12))),
),
),
onChanged: (value) {
setState(() {
_selectedIndex = value;
});
},
);
}}

10.NeumorphicSlider


拟态滑动控制器


slider.gif


class NeumorphicSliderWidget extends StatefulWidget {
const NeumorphicSliderWidget({Key? key}) : super(key: key);
@override
State<NeumorphicSliderWidget> createState() => _NeumorphicSliderWidgetState();
}
class _NeumorphicSliderWidgetState extends State<NeumorphicSliderWidget> {
double num = 0;
@override
Widget build(BuildContext context) {
return NeumorphicSlider(
min: 8,
max: 75,
value: num,
onChanged: (value) {
setState(() {
num = value;
});
},
);
}
}

11.NeumorphicProgress


拟态百分比进度条


progress.gif


NeumorphicProgress(
height: 20,
percent: 0.5,
);

12.NeumorphicProgressIndeterminate


渐进式进度条


indeterminate.gif


NeumorphicProgressIndeterminate(
height: 10,
);

13.NeumorphicBackground


拟态背景,可以使用Radius裁剪屏幕


image.png


class NeumorphicPageView extends StatelessWidget {
const NeumorphicPageView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return NeumorphicBackground(
borderRadius: const BorderRadius.all(Radius.circular(130)),
child: Scaffold(
backgroundColor: Colors.grey[200],
));
}
}

14.NeumorphicApp


使用拟态设计的应用程序。可以处理主题、导航、本地化等


void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return NeumorphicApp(
debugShowCheckedModeBanner: false,
themeMode: ThemeMode.light,
title: 'Flutter Neumorphic',
home: FullSampleHomePage(),
);
}
}

15.NeumorphicAppBar


拟态导航条


app_bar.png


五、🏠 使用案例


image.png


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

Flutter桌面端开发——发送本地悬浮通知?

在我用的大部分桌面端中,发送本地悬浮通知的软件可以说是屈指可数。但是,这不妨碍到我们学习✊,奋斗说不定以后就能用到呢! 在选择该使用哪些插件开发桌面端的时候,由 lijy91 主导的 LeanFlutter 可以说是帮了很大的忙,有需要的可以自己去看看。 接下...
继续阅读 »

在我用的大部分桌面端中,发送本地悬浮通知的软件可以说是屈指可数。但是,这不妨碍到我们学习✊,奋斗说不定以后就能用到呢!


在选择该使用哪些插件开发桌面端的时候,由 lijy91 主导的 LeanFlutter 可以说是帮了很大的忙,有需要的可以自己去看看。


接下来要介绍的两个发送通知的插件,也是从 LeanFlutter 下的 awesome-flutter-desktop 仓库中找的。


local_notifier


安装🛠


点击local_notifier获取最新版本。以下是在编写本文章时的最新版本:


local_notifier: ^0.1.1

👻注意:在开发 Linux 端的程序时,还需要额外的操作,具体可以查看这里


使用🍖


要想实现发送通知的功能,需要用到一个实例化的 LocalNotifier 对象:


final localNotifier = LocalNotifier.instance;

如果你想全局使用,可以把该行代码放到一个自定义的全局类里。


要想编辑想发送的内容,需要用到 LocalNotification 类。实例化该类可以传入5个参数:



  • String? identifier:用来当做通用唯一识别码

  • required String title:发送通知的标题,一般是软件名称

  • String? subtitle:发送的通知内容的标题

  • String? body:发送的内容的主体

  • bool silent = false:在发送通知时是否静音


final notification = LocalNotification(
identifier: '12345',
title: '古诗鉴赏从',
subtitle: '桃夭 - 佚名〔先秦〕',
body: '桃之夭夭,灼灼其华。之子于归,宜其室家。\n桃之夭夭,有蕡其实。之子于归,宜其家室。\n桃之夭夭,其叶蓁蓁。之子于归,宜其家人。',
silent: false,
);
localNotifier.notify(notification);

现在我们就能愉快地发送一条自定义的通知了🎉


local-notifier


我们发现,其中其实只有title参数是必传的,我们就试一下只传入这个参数:


final notification = LocalNotification(
title: '古诗鉴赏从',
);
localNotifier.notify(notification);

local-notifier1


我们发现,只传 title 参数,它会自动将 title 的参数值赋值给 subtitle,而 body 参数会以“新通知”代替。
好了,以上就是 local_notifier 目前的全部功能,如果你只是简单的发送一条提示使用该插件完全够用。


win_toast


安装🛠


点击win_toast获取最新版本。以下是在编写本文章时的最新版本:


win_toast: ^0.0.2

使用🍖


在全局使用该插件,需要在app初始化时初始化。在某个页面使用,只要在页面初始化时初始化就行。


初始化时需要传递3个参数:



  • required String appName:程序名称

  • required String productName:产品名称

  • required String companyName:公司名称


await WinToast.instance().initialize(
appName: '第一个Desktop应用',
productName: '第一个Desktop应用',
companyName: 'Hiden Intelligence',
);

没写过原生,插件作者贴出Pick a unique AUMID that will identify your Win32 app告诉我们为什么要填这些内容👀,想了解的可以看一下。


要想发送一条通知,需要使用 showToast 方法,该方法有5个参数可以传:




  • required ToastType type:传入toast显示的类型,一共有8种:



    • imageAndText01至imageAndText04

    • text01至text04


    至于这些类型的异同,可以点这里👀




  • required String title:通知显示的标题




  • String subtitle = '':通知显示的主要内容




  • String imagePath = '':选择 imageAndText 类型时,要显示的图片




  • List actions = const []:显示通知中的按钮




使用text类型


先定义几个变量和常量:


Toast? toast;
final List<String> _title = 'Shining For One Thing(《一闪一闪亮星星》影视剧歌曲) - 赵贝尔';
final List<String> _subtitle = 'I fall in love\nI see your love\n遇见你才发现\n我在等你到来';
final List<String> _actione = ['上一首', '播放/暂停', '下一首'];

先来看看只传入文字的 text01 类型:


toast = await WinToast.instance().showToast(
type: ToastType.text01,
title: _title,
actions: _actione,
);

👻注意:当使用 ToastType.text01 或 ToastType.imageAndText01 时不能传入 subtitle 参数。


1


再来看看只传入文字的 text02 类型:


toast = await WinToast.instance().showToast(
type: ToastType.text02,
title: _title,
subtitle: _subtitle,
actions: _actione,
);

2


用了一下 ToastType.text03 和 ToastType.text04,发现显示的效果和 ToastType.text02 没有差别。大家可以自己试试。


使用imageAndText类型


修改一下常量的值(非必要):


Toast? toast;
final List<String> _title = '又下雨了,你的心情怎么样?';
final List<String> _subtitle = '偷偷告诉你,明天就天晴了😏\n好雨知时节,当春乃发生。随风潜入夜,润物细无声。野径云俱黑,江船火独明。晓看红湿处,花重锦官城。';
final List<String> _actione = ['不开森😭', '只想睡觉🥱', '非常高兴😃'];

还需要传入一张图片,目前无法得知应该传入图片的路径怎么填,所以先准备一张资源图片传入它的相对路径:


final String _imagePath = 'assets/images/pdx.jpg';

来看看 imageAndText01 类型:


toast = await WinToast.instance().showToast(
type: ToastType.imageAndText01,
title: _titles * 3,
imagePath: _imagePath,
actions: _action,
);

3


😲嗯?我们传入的图片怎么没显示?换个网络图片的链接试试:


final String _imagePath = 'https://gitee.com/ilgnefz/image-house/raw/master/images/pdx.jpg';

发现效果还是一样的。通过查看文档里的第一个链接中的例子,可以发现,这里需要传入图片的绝对路径。


那在 Flutter 中怎么获取文件的绝对路径呢🤔?当然,可以直接在 Android Studio 中选中图片点右键的 Copy Path,但是程序被打包安装后就不一定在这个位置了。学过Node.js,在里面获取文件的绝对路径要用到 Path 模块,那么 Flutter 是否也用同样的插件。打开 pub.dev 搜索,还真有。复制到 pubspec.yaml 进行安装,报错,告诉我们 Flutter Desktop 中已经集成了该插件,但是版本不一样。😀那不就好办了,第一步导入:


import 'package:path/path.dart' as path;

path 中没有 __dirname 方法,可以通过path. 查看提示,发现有一个 current的方法。虽然我们不知道这个方法是干什么的,但也不妨试试。修改 imagePath 为如下代码:


final String _imagePath = path.join(path.current, 'assets/images/pdx.jpg');

3


成功🎉🎉🎉🎉🎉


接下来使用 imageAndText02 类型来看看:


toast = await WinToast.instance().showToast(
type: ToastType.imageAndText02,
title: _titles,
subtitle: _subtitle,
imagePath: _imagePath,
actions: _action,
);

3


imageAndText03 和 imageAndText04 显示的效果也和 imageAndText02 无差别,这里就不放图了。


大家可能已经发现,通知中的3个按钮是由 actions 参数决定的,但是这个参数传入的是 String 类型,那我们要怎么才能获取到用户对这些按钮的点击事件呢?


前期我们定义了一个toast对象用来赋值,接下来就要用到这个参数:


if (toast != null) {
toast.eventStream.listen((event) {
if (event is ActivatedEvent) {
print(event);
}
});
}

在这里我们会获得一个 event 对象,通过打印会发现该对象下面只有一个属性actionIndex,返回的是 int? 类型。通过该属性,我们就可以获取到用户点击的是第几个按钮:


WinToast.instance().bringWindowToFront(); // 用户点击后关闭弹窗通知
BotToast.showText(text: '你当前的状态是${_action[event.actionIndex!]}');

4


知道了用户点击的是哪个按钮,接下来编写事件的代码就容易了。


🛫OK,以上就是这篇文章的全部内容,仅针对插件的当前版本,并不能保证适用于以后插件用法的更新迭代。本人只处于对代码的实践部分,如某些内容的概念或叫法出错还请指正🙏。


最后,感谢 lijy91boyan01 对以上插件的维护和开发😁。本程序相关代码已上传至 githubgitee,有需要的可以下载下来查看学习。


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

Android 实现卡片堆叠,钱包管理效果(带动画)

先上效果图 源码 github.com/woshiwzy/Ca… 实现原理: 1.继承LinearLayout 2.重写onLayout,onMeasure 方法 3.利用ValueAnimator 实施动画 4.在动画回调中requestLayout 实现...
继续阅读 »

先上效果图


result.gif


源码
github.com/woshiwzy/Ca…


实现原理:


1.继承LinearLayout

2.重写onLayout,onMeasure 方法

3.利用ValueAnimator 实施动画

4.在动画回调中requestLayout 实现动画效果


思路:


1.用Bounds 对象记录每一个CardView 对象的初始位置,当前位置,运动目标位置


2.点击时计算出对应的view以及可能会产生关联运动的View的运动的目标位置,从当前位置运动到目标位置,然后以这2个位置作为动画参数实施ValueAnimator动画,在动画回调中触发onLayout,达到动画的效果。


重写adView 方法,确保新添加的在这里确保所有的子view 都有一个初始化的bounds位置


   @Override
public void addView(View child, ViewGroup.LayoutParams params) {
super.addView(child, params);
Bounds bounds = getBunds(getChildCount());
}

确保每个子View的测量属性宽度填满父组件



boolean mesured = false;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mesured == true) {//只需要测量一次
return;
}
mesured = true;
int childCount = getChildCount();
int rootWidth = getWidth();
int rootHeight = getHeight();
if (childCount > 0) {
View child0 = getChildAt(0);
int modeWidth = MeasureSpec.getMode(child0.getMeasuredWidth());
int sizeWidth = MeasureSpec.getSize(child0.getMeasuredWidth());

int modeHeight = MeasureSpec.getMode(child0.getMeasuredHeight());
int sizeHeight = MeasureSpec.getSize(child0.getMeasuredHeight());

if (childCount > 0) {
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
childView.measure(MeasureSpec.makeMeasureSpec(sizeWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(sizeHeight, MeasureSpec.EXACTLY));
int top = (int) (i * (sizeHeight * carEvPercnet));
getBunds(i).setTop(top);
getBunds(i).setCurrentTop(top);
getBunds(i).setLastCurrentTop(top);
getBunds(i).setHeight(sizeHeight);
}

}

}
}

重写onLayout 方法是关键,是动画触发的主要目的,这里layout参数并不是写死的,而是计算出来的(通过ValueAnimator 计算出来的)


@Override
protected void onLayout(boolean changed, int sl, int st, int sr, int sb) {
int childCount = getChildCount();
if (childCount > 0) {
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
int mWidth = view.getMeasuredWidth();
int mw = MeasureSpec.getSize(mWidth);
int l = 0, r = l + mw;
view.layout(l, getBunds(i).getCurrentTop(), r, getBunds(i).getCurrentTop() + getBunds(i).getHeight());
}
}
}

源码


github:
github.com/woshiwzy/Ca…


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