注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

十年码农内功:经历篇

分享工作中重要的经历,可以当小说来看 一、伪内存泄漏排查 1.1 背景 我们原来有一个刚用 C++ 写的业务服务,迟迟不敢上线,原因是内存泄漏问题一直解决不了。现象是,服务上线后,内存一直慢慢每隔几秒上涨4/8KB,直到服务下线。 我刚入职不久,领导让我来查...
继续阅读 »

分享工作中重要的经历,可以当小说来看



一、伪内存泄漏排查


1.1 背景


我们原来有一个刚用 C++ 写的业务服务,迟迟不敢上线,原因是内存泄漏问题一直解决不了。现象是,服务上线后,内存一直慢慢每隔几秒上涨4/8KB,直到服务下线。


我刚入职不久,领导让我来查这个问题,非常具有挑战性,也是领导对我的考察!


1.2 分析


心路历程1:工具分析


使用 Valgrind 跑 N 遍服务,结果中都没有发现内存泄漏,但是有很多没有被释放的内存和很多疑似内存泄漏。实在没有发现线索。


心路历程2:逐个模块排查


工具分析失败,那就挨个模块翻看代码,并且逐个模块写demo验证该模块是否有泄漏(挂 Valgrind),很遗憾,最后还是没有找到内存泄漏。


心路历程3:不抛弃不放弃


这个时候两周快过去了,领导说:“找不到内存泄漏那就先去干别的任务吧”,感觉到一丝凉意,我说:“再给我点时间,快找到了”。这样顶着巨大压力加班加点的跑Valgrind,拿多次数据结果进行对比,第一份跑 10 分钟,第二份跑 20 分钟,看看有哪些差异或异常,寻找蛛丝马迹。遗憾的是还是没有发现内存泄漏在哪。


功夫不负有心人,看了 N 份结果后,对一个队列产生了疑问,它为啥这么大,队列长度 1000 万,直觉告诉我,它不正常。


去代码中找这个队列,确实在初始化的时候设置了 1000 万长度,这个长度太大了。


1.3 定位


进队列需要把虚拟地址映射到物理地址,物理内存就会增加,但是出队列物理内存不会立刻回收,而是保留给程序一段时间(当系统内存紧张时会主动回收),目的是让程序再次使用之前的虚拟地址更快,不需要再次申请物理内存映射了,直接使用刚才要释放的物理内存即可。


当服务启动时,程序在这 1000 万队列上一直不停的进/出队列,有点像貔貅,光吃不拉,物理内存自然会一直涨,直到貔貅跑到了队尾,物理内存才会达到顶峰,开始处在一个平衡点。


图1 中,红色代表程序占用的物理内存,绿色为虚拟内存。



图1


然而每次上线还没等 到达平衡点前就下线了,担心服务内存一直涨,担心出事故就停服务了。解决办法就是把队列长度调小,最后调到了 2 万,再上线,貔貅很快跑到了队尾,达到了平衡点,内存就不再增涨。


其实,本来就没有内存泄漏,这就是伪内存泄漏。


二、周期性事故处理


2.1 背景


我们有一个业务,2019 年到 2020 年间发生四次(1025、0322、0511 和 0629)大流量事故,事故时网络流量理论峰值 3000 Gbps,导致网络运营商封禁入口 IP,造成几百万元经济损失,均没有找到具体原因,一开始怀疑是服务器受到网络攻击。


后来随着事故发生次数增加,发现事故发生时间具有规律性,越发感觉不像是被攻击,而是业务服务本身的流量瞬间增多导致。服务指标都是事故造成的结果,很难倒推出事故原因。


2.2 猜想(大胆假设)


2.2.1 发现事故大概每50天发生一次


清晰记得 2020 年 7 月 15 日那天巡检服务时,我把 snmp.xxx.InErrors 指标拉到一年的跨度,如图2 发现多个尖刺的间距似乎相等,然后我就看了下各个尖刺时间节点,记录下来,并且具体计算出各个尖刺间的间隔记录在下面表格中。着实吓了一跳,大概是 50 天一个周期。并且预测了 8月18日 可能还有一次事故。



图2 服务指标


事故时间相隔天数
2019.09.05-
2019.10.2550天
2019.12.1450天
2020.02.0149天
2020.03.2250天
2020.05.1150天
2020.06.2949天
2020.08.18预计

2.2.2 联想50天与uint溢出有关


7 月 15 日下班的路上,我在想 3600(一个小时的秒数),86400(一天的秒数),50 天,5 x 8 等于 40,感觉好像和 42 亿有关系,那就是 uint(2^32),就往上面靠,怎么才能等于 42 亿,86400 x 50 x 1000 是 40 多亿,这不巧了嘛!拿出手机算了三个数:


2^32                  = 4294967296 
3600 * 24 * 49 * 1000 = 4233600000
3600 * 24 * 50 * 1000 = 4320000000

好巧,2^32 在后面的两个结果之间,4294967296 就是 49 天 16 小时多些,验证了大概每 50 天发生一次事故的猜想。



图3 联想过程


2.3 定位(小心求证)


2.3.1 翻看代码中与时间相关的函数


果然找到一个函数有问题,下面的代码,在 64 位系统上没有问题,但是在 32 位系统上会发生溢出截断,导致返回的时间是跳变的,不连续。图4 是该函数随时间输出的折线图,理想情况下是一条向上的蓝色直线,但是在 32 位系统上,结果却是跳变的红线。


uint64_t now_ms() {
struct timeval t;
gettimeofday(&t, NULL);
return t.tv_sec * 1000 + t.tv_usec / 1000;
}


图4 函数输出


这里解释一下,问题出在了 t.tv_sec * 1000,在 32 位系统上会发生溢出,高于 32 位的部分被截断,数据丢失。不幸的是我们的客户端有一部分是 32 位系统的。


2.3.2 找到出问题的逻辑


继续追踪使用上面函数的逻辑,发现一处问题,客户端和服务端的长链接需要发Ping保活,下一次发Ping时间等于上一次发Ping时间加上 30 秒,代码如下:


next_ping = now_ms() + 30000;

客户端主循环会不断判断当前时间是否大于 next_ping,当大于时发 Ping 保活,代码如下:


if (now_ms() > next_ping) {
send_ping();
next_ping = now_ms() + 30000;
}

那怎么就出现大流量打到服务器呢?举个例子,如图3,假如当前时间是 6月29日 20:14:00(20:14:26 时 now_ms 函数返回 0),now_ms 函数的返回值超级大。


那么 next_ping 等于 now_ms() 加上 30000(30s),结果会发生 uint64 溢出,反而变的很小,这就导致在接下来的 26 秒内,now_ms函数返回值一直大于 next_ping,就会不停发 Ping 包,产生了大量流量到服务端。


2.3.3 客户端实际验证


找到一个有问题的客户端设备,把它本地时间拨回 6月29日 20:13:00,让其自然跨过 20:14:26,发现客户端本地 log 中有大量发送 Ping 包日志,8 秒内发送 2 万多个包。证实事故原因就是这个函数造成的。解决办法是对 now_ms 函数做如下修改:


uint64_t now_ms() {
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return uint64_t(t.tv_sec) * 1000 + t.tv_nsec / 1000 / 1000;
}

2.3.4 精准预测后面事故时间点


因为客户端发版周期比较长,需要做好下次事故预案,及时处理事故,所以预测了后面多次事故。


时间戳(ms)16进制北京时间备注
15719580303360x16E000000002019/10/25 07:00:30历史事故时间
15762529976320x16F000000002019/12/14 00:03:17不确定
15805479649280x170000000002020/02/01 17:06:04不确定
15848429322240x171000000002020/03/22 10:08:52历史事故时间
15891378995200x172000000002020/05/11 03:11:39历史事故时间
15934328668160x173000000002020/06/29 20:14:26历史事故时间
15977278341120x174000000002020/08/18 13:17:14精准预测事故发生
16020228014080x175000000002020/10/07 06:20:01精准预测事故发生
16063177687040x176000000002020/11/25 23:22:48精准预测事故发生

2.4 总结


该事故的难点在于大部分服务端的指标都是事故导致的结果,并且大流量还没有到业务服务,就被网络运营商封禁了 IP;并且事故周期跨度大,50 天发生一次,发现规律比较困难。


发现规律是第一步,重点是能把 50 天和 uint32 的最大值联系起来,这一步是解决该问题的灵魂。



  • 大胆假设:客户端和服务端的代码中与时间相关的函数有问题;

  • 小心求证:找到有问题的函数,别写代码验证,最后通过复现定位问题;


经过不屑努力从没有头绪到逐渐缩小排查范围,最后定位和解决问题。

作者:科英
来源:juejin.cn/post/7252159509837119546

收起阅读 »

程序猿的九年广漂

** 程序猿的九年广漂感悟 广漂第九年了,成为了一名老码农,混迹于各种论坛博客中,我感觉总得写点什么.讲讲技术也好,发泄一下码农的无奈也好.希望对年轻的同行有点帮助吧. 非广东人,但从小在广东生活,一口不标准的粤语,毕业自然而然就就来了广州,总以为广州是最合...
继续阅读 »

**


程序猿的九年广漂感悟


广漂第九年了,成为了一名老码农,混迹于各种论坛博客中,我感觉总得写点什么.讲讲技术也好,发泄一下码农的无奈也好.希望对年轻的同行有点帮助吧.

image.png
非广东人,但从小在广东生活,一口不标准的粤语,毕业自然而然就就来了广州,总以为广州是最合适的城市,无论饮食和文化,跟大多数毕业生一样,总会有一些不着边际的理由漂泊在北上广深,没有过来人指导职业规划,全凭一股初生牛犊不怕虎的拼劲.其实这是一件很可怕且无奈的事情.建议大家多听听张雪峰对大学生的指导.不说太多废话,先讲自己的经历吧.
14-15年毕业季,毕业于很普通的二本母校,在面试公司实在是抬不起头来,后来入职了一家搞微商的公司,一个人干三个人的活,写Android和java后端,天天加班,老板天天喂鸡汤大饼,周末单休,4000大洋的无保障金,还发不出来,年轻的时候只想学技术,希望早日月薪过万.当时住在一个城中村里面,每天来回160分钟的公交地铁,路上都在看慕课网和掘金,这个期间虽然穷且幼稚,但是技术成长很快,相信和大多数刚刚毕业的大学生一样吧,因为想法不多,没有家庭,没有职业规划,才能一心一意的把技术学好.


同时期,那些在老家有规划的同学在干嘛呢,有继承家里工厂的,有直接考试公务员的,他们的人生规划很清晰,有工作然后找老婆,然后家里帮助买房成家,哦对了,我老家附近是二线城市,这一点很重要.

毕业的第一家公司让我像个无头苍蝇,微商公司出来后呢,或多或少也算对我们天朝社会有了一点认知,想找稳定一点的工作,最起码是正常发工资的,很快就找了家超甲级写字楼的中外合资公司,开头就是广东XXXX公司,琶洲那边,直面广东大珠江.每月薪资8500大洋,主攻Android.那时候996不叫流行,叫盛行.


WeChat459ffe44abc76d02a65e224e7348c39a.jpg


期间有几件有意思的事情.

第一件是公司有很多老外,当时有个德国投资商需要几个产品和设计一起对需求,第一天就跟国产同事说,我工作的节奏很快,也会有加班,你们一定要跟上节奏.当时刚刚毕业于西安外语的小妹妹不知道怎么翻译的,回来给我们说的一愣一愣的,一个月后,德国佬全身而退了,还说了一些让我们啼笑皆非的话,说你们中国人加班太严重了,好像不需要自己的私人时间一样.当时还没有’卷”这种说法.跟着德国佬对需求的产品设计回到国人领导的团队后,感叹,跟着德国人做事太舒服了,几乎也没有加班,节奏慢的舒坦;


第二件事,公司后来给我配了个小弟,让我好好带,小弟的薪资是9500.这货是刚刚培训出来的,理论知识牛逼,啥也做不了.一个月后我怕累死自己,于是让他走人了,后来公司又找了大专没有毕业的小伙子,技术很扎实,薪资5000.领导还说给多了,这小伙子很实在,后来一直跟了我很多年,现在想想有点对不住他,一直没有给他带个好坑位.


第三件事,因为写字楼周末是不开中央空调的,所以我们经常加班的冷热不知;但是资本家可不管这些,虽然公司经常不准时发工资,套路可不少,比如招人来试用期过后就裁员,不多不少3个月,反正便宜好用,或者招外包人员来用几天给人充当大公司的样子,然后在绩效方面下功夫,很多很多套路,有时候不得不佩服国人在这方面的造诣.还有经常在午休期间听到老板撕心裂肺的骂人,哈哈哈,那时候想不明白这货是怎么做成老板的,很多年后向明白了,这货才符合国人老板形象.对了,这家公司背靠我们伟大的某种天朝机构.


这家公司是我呆过最差的公司,通宵加班是常有的,你们打工的最长加班时间是多长呢,我是两天两夜....你们肯定很想知道这家公司结局怎样,最老外肯定是撤资走人的,公司最后就剩几个无关紧要的人.你们以为是失败了吗,其实是相当成功的,虽然坑了一波人,但是老板们的豪车豪宅是实现了的,快钱已经到手了,你们细细品....


任职这家中外合资的公司期间,其实说不清是什么人生态度,我除了正常打工还干嘛呢,被朋友忽悠去创业,经常折腾到晚上两三点.年轻就是好呀,好像不用睡觉一样.后来有一件事对我打击很大.我接了一个中国移动的小单子,3万块,每天晚上在家工作到2点左右,刚刚开始还没有什么,一个星期后,突然重感冒,发烧,后来直接请假在家休息了一段时间,也就是这个期间,我的体重从60公斤一下子飙到了65公斤.至于职业规划,买车买房,成家立业啥的,不好意思,8500的月薪,住在见不到阳光的城中村的我,真的不知道自己应该想什么.除了提高技术拼体力以外,我们能想的很少,当然家里也经常叫回去考公务员,但是人出来了,想回去真的很难很难.


这里再说一下同时期的好友,做公务员的朋友,已经在老家买了房子了,有了心爱的姑娘了,虽然自称是房奴,但是公积金已经绰绰有余.接手家里生意的朋友已经做的风生水起.在这里提醒一下刚刚毕业的小码农,家庭的高度以及父母提供的规划很重要.



从那家公司出来以后,我突然想去国企看看,因为那段时间太累了,想要周末,想正常上下班.这里就要提到一家老牌国企.某电视台,做某电视台新闻某电app.当时面试已经通过了,人事谈下来的薪资是1w,很多公司给的薪资都是一点几的w,但是还是想去国企,可能真的是加班怕了吧.一个二十出头的小伙子,经常通宵加班,心里打击还是很大的.但是这家某视台的国企呢,比较让人摸不着头脑,人事已经口头答应录用了,但是offer迟迟没有下来,后来我入职了另外一家公司半个月以后,offer终于来了,人事还介绍了一番,问我什么时候可以入职.后来还发生了一件特别有趣的事情.你以为的国企就很稳定,并没有,一年后,我成为了面试官,他们整个小组的人来求职,是我亲自面试的......


再说说接下来吧,我去了一家上市公司,公司很大,薪资很高,刚刚开始很闲,那时候的我除了学技术以外,终于有时间思考下一步怎么办,这是我遇到的福利最好的一家公司,不过我后来还是离职了,其实任职期间我已经在找下家了.这家公司的项目我认为是没有前途的,后来事实证明确实没有前景,项目很快就裁掉了.这家公司也有一件小插曲,当时公司安排了7个实习生让我带,让我在两个星期内完成一个教育类app,大公司是有各种考核的,每天都要写日报,我当时的前一个星期的日报都是写怎么搭框架,怎么模块化项目, 后来项目经理每天都找到我,她说不关心我做什么框架,什么模块化,他要的是马上实现UI上面的功能,并且她能够体验的到这些功能的完成度,必须要知道我在干什么,对了,这家公司如果使用了第三方库的话,需要提交给上级申请,并且要写评估报告的.对,你没有听错.要求全部使用原生的框架,举个例子,我用 了okhttp,然后被叼了一顿.哈哈哈哈!有一次我去看了一下别人的日报怎么写的,很亮眼睛,全部都是今天我优化了哪里哪里的代码,完成了哪里哪里的注释....


是不是有小伙伴问我为什么不去某大型互联网公司试试,这还用说,任职期间我肯定是有去面试的,某讯面试通过了,但是我不喜欢深圳,放弃了,某易的话,我请了三天假去面试,我印象中最后一轮是某易的某邮箱的组长面的我,拒绝我的理由是因为我不懂C++,当时我挺沮丧的,并且怼了他,简历上就可以看出我不精通C++,电话面试也可以看出我是否精通C++,为什么让我花了几天来面试....他不说话.多年后想了想,其实完全没有必要,一个邮箱app很难吗,拒绝我肯定是其他原因的.谁有话语权就有决定权.出来工作有一点必须要尽快学会成长的,不要对没有意义的事情较劲.


接着讲吧,某上市公司出来之后,我去了一家我当时认为还是比较ok的公司,干了很多年,一直干到公司上市.....我不太想说太多这家公司,怕暴露自己最好的青春都给了这家公司.


说说同时期的好朋友怎么样了吧,某公务员朋友,在家里的帮助下,已经副科,孩子已经打酱油了,他每天聊天的积极性很高,你懂的,家里的帮助下,买了个大房子.另一个接手家庭生意的朋友,已经把公司做到了当地的第一,什么奔驰宝马豪宅,早就实现了,孩子都三个了.也有同学会问,北上广深漂泊的同学呢,他们基本上都回老家了.不好意思,没有太多的人生赢家,大部分的人都是普普通通的.房子始终是我们绕不过的一道坎,尤其是大城市,想通过打工实现大城市安家立业,是一件很难很难的事情,刚刚毕业的程序员小伙伴要考虑清楚这一点哦.


差点忘记了这是一个技术论坛,我们再来聊聊技术吧,可能会有小伙伴好奇我的技术怎么样,ios Android python 设计 都还行,因为永远都有新东西出来,没人敢说自己在新的技术点上多牛,也不敢保证自己永远处于热爱技术的阶段上,至于什么架构师啥的,项目管理什么的,对于目前的我来说就跟煮饭一样简单.目前的阶段处于,什么可以赚钱我就可以学什么,但是大多数的技术都是不赚钱的,你细品...

奉劝新入技术坑的小伙伴,不要纠结于自己学哪种技术,更不要要纠结于哪个技术点谁比较牛逼,你们较劲的东西资本家看着像一个笑话.技术仅仅只是一个工具,先确定好自己要做什么,再决定自己携带什么工具.

WeChat039aa9244fd4185fb0728e11854eeeef.jpg


再说说三十多岁的程序员的未来方向在哪里,先说说找工作吧,我这个阶段一旦被裁员,大概率找不到工作,不要纠结市场环境为什么这么奇怪,你可以怀疑资本家的人品,但是请不要怀疑资本家对市场的把控.不要问我怎么知道,也不要举例反驳我,市场上总有特例,我们只能拥抱市场大环境.这些年来,我带过团队很多,经常招人,有时候都会感叹我出去以后怎么找工作,慢慢的,我现在已经能接受自己失业后送外卖和开滴滴了.对,你没有听错,宇宙的尽头可能是考公,也可能是美团或者饿了么.


说说职业分水岭吧,第一个5年,你跟身边的同学可能差别不大,无非就是结婚与不结婚,但是到了第二个五年,你会慢慢的发现,原来不同的职业不同的城市,幸福感真的不一样,选择远远比努力重要,做一个普通人就是要很努力很努力的,所以不要看不起一毕业就考公的同学,也不要说谁谁谁读大专是混日子啥的,请不要忽视体制带来的便利,体制这个词你细品.


WeChat5fa4443854247035029c1d17309afe99.jpg


写了那么多,来个总结吧,毕竟是小爽文嘛,是要写点个人主观的想法的.技术本身是没有出路的,出路在业务,业务驱动技术的开发.所以不要过多纠结于太多技术点,技术牛逼的人未必能赚多钱,技术牛逼的人不一定能去bat,也可能在缅甸菲律宾或者91啥先生的.对了,我忘了说,这些年副业赚到的钱是打工的好几倍,所以你细品.


一个人是否考虑技术上长远发展,首先要考虑自己的家庭,以及自己的身边的资源.如果有条件毕业就回家考公上岸的话,尽量去试试.大城市的技术之路往往伴随着城中村,出租房,996.所以多花点心思在业务上.希望在座的小伙伴尽快规划好自己的职业道路,我文中提了那么多同时期的朋友,不得不承认他们有更好的家庭资源,我想说的是,要懂得利用自己的家庭资源尽早规划自己的职业道路,如果没有足够的家庭资源,更要尽早规划自己的职业道路.技术不是出路,只是暂时的活路

作者:给朕下跪
来源:juejin.cn/post/7252195699524403259
.且行且珍惜!!!!

收起阅读 »

记一次修改一行代码导致的线上BUG

web
背景介绍 先描述一下需求,要在一个老项目里根据type类型,给一个试题题干组件新增一个class样式,type是在url地址栏上面携带的。简单,一行代码搞定,五分钟部署,十分钟留给测试,然后跟车上线,打卡下班! 《凉凉》送给自己 看标题就知道结果了,第二天下午...
继续阅读 »

1920_1200_20100319011154682575.jpg


背景介绍


先描述一下需求,要在一个老项目里根据type类型,给一个试题题干组件新增一个class样式type是在url地址栏上面携带的。简单,一行代码搞定,五分钟部署,十分钟留给测试,然后跟车上线,打卡下班!


《凉凉》送给自己


看标题就知道结果了,第二天下午现网问题来了,一线反馈某个页面题干不展示了,值班同事排查一圈,找到我说我昨天加的代码报错了!


006Cmetyly1ff16b3zxvxj308408caa8.jpg


惊了,就加了一行业务代码,其他都是样式,测试也通过了,这也能有问题?绩效C打底稳了(为方便写文章,实际判断用变量代替):


<div :class="{'addClass': $route.query.type === 'xx'}">
...
</div>

temp.png
问题其实很简单,$route为undefined了,导致query获取有问题,这让我点怀疑自己,难道这写错了?管不了太多,只能先兼容上线了。


$route && $route.query && $route.query.type

其实是可以用?.简写的,但是这个项目实在不“感动”了,保险写法,解决问题优先。提申请,拉评审,走流程,上线,问题解决,松口气,C是保住了。


问题分析


解决完问题,还要写线上问题分析报告,那只能来扒一扒代码来看看了。首先,这个项目使用的是多页应用,每个页面都是一个新的SPA,我改的页面先叫组件A吧,组件A在页面A里被使用,没问题;组件A同样被页面B使用,报错了。那接下来简单了,看代码:


// 2022-09-26 新增
import App from '@/components/pages/页面A'
import router from '@/config/router.js'
// initApp 为封装的 new Vue
import { initApp, Vue } from '../base-import'
initApp(App, router)

// 2020-10-18 新增
import App from '@/components/pages/页面b'
new Vue({
el: '#app',
components: { App },
template: '<App/>'
})

两个页面的index.js文件,两种写法,一个引用了router,一个没有引用,被这个神仙代码整懵了。然后再看了一下其他页面,也都是两种写法掺着写的,心态崩了。这分析报告只能含着泪写了...


最后总结



  1. 问题不是关键,关键的是代码规范;

  2. 修改新项目之前,最好看一下代码逻辑,有熟悉的同事最好,可以沟通了解一下业务(可以避免部分问题);

  3. 当想优化之前代码的时候,要全面评估,统一优化,上面的写法我也找同事了解了,因为之前写法不满足当时的需求,他就封装了新方法,但是老的没有修改,所以就留了坑;


作者:追风筝的呆子
来源:juejin.cn/post/7252198762625089596
收起阅读 »

什么!一个项目给了8个字体包???

web
🙋 遇到的问题 在一个新项目中,设计统一了项目中所有的字体,并提供了字体包。在项目中需要按需引入这些字体包。 首先,字体包的使用分为了以下几种情况: 无特殊要求的语言使用字体A,阿拉伯语言使用字体B; 加粗、中等、常规、偏细四种样式,AB两种字体分别对应使用...
继续阅读 »

🙋 遇到的问题


在一个新项目中,设计统一了项目中所有的字体,并提供了字体包。在项目中需要按需引入这些字体包。


首先,字体包的使用分为了以下几种情况:



  1. 无特殊要求的语言使用字体A,阿拉伯语言使用字体B;

  2. 加粗、中等、常规、偏细四种样式,AB两种字体分别对应使用 BoldMediumRegularThin 四种字体包;


所以,我现在桌面上摆着 8 个字体包:



  • A-Bold.tff

  • A-Medium.tff

  • A-Regular.tff

  • A-Thin.tff

  • B-Bold.tff

  • B-Medium.tff

  • B-Regular.tff

  • B-Thin.tff


image.png
不同语言要使用不同的字体包,不同粗细也要使用不同的字体包!


还有一个前提是,设计给的设计图都是以字体A为准,所以在 Figma 中复制出来的 CSS 代码中字体名称都是A。


刚接到这个需求时还是比较懵的,一时想不出来怎么样才能以最少的逻辑判断最少的文件下载最少的代码改动去实现在不同情况下自动的去选择对应的字体包。


因为要涉及到语言的判断,最先想到的还是通过 JS,然后去添加相应的类名。但这样也只能判断语言使用A或B,粗细还是解决不了。


image.png


看来还是要用 CSS 解决。


首先我将所有的8个字体先定义好:


@font-face {
font-family: A-Bold;
src: url('./fonts/A-Bold.ttf');
}

/* ... */

@font-face {
font-family: B-Thin;
src: url('./fonts/B-Thin.ttf');
}

image.png


🤲🏼 如何根据粗细程度自动选择对应字体包


有同学可能会问,为什么不直接使用 font-weight 来控制粗细而是用不同的字体包呢?


我们来看下面这个例子,我们使用同一个字体, font-weight 分别设置为900、500、100,结果我们看到的字体粗细是一样的。


对的,很多字体不支持 font-weight 所以我们需要用不同粗细的字体包。


image.png


所以,我们可以通过 @font-face 中的 font-weight 属性来设置字体的宽度:


@font-face {
font-family: A;
src: url('./fonts/A-Bold.ttf');
font-weight: 600;
}
@font-face {
font-family: A;
src: url('./fonts/A-Medium.ttf');
font-weight: 500;
}
@font-face {
font-family: A;
src: url('./fonts/A-Regular.ttf');
font-weight: 400;
}
@font-face {
font-family: A;
src: url('./fonts/A-Thin.ttf');
font-weight: 300;
}

注意,这里我们把字体名字都设为相同的,如下图所示,这样我们就成功的解决了第一个问题:不同粗细也要使用不同的字体包;


image.png


并且,如果我们只是定义而未真正使用时,不会去下载未使用的字体包,再加上字体包的缓存策略,就可以最大程度节省带宽:


image.png


🔤 如何根据不同语言自动选择字体包?


通过张鑫旭的博客找到了解决办法,使用 unicode-range 设置字符 unicode 范围,从而自定义字体包。


unicode-range 是一个 CSS 属性,用于指定字体文件所支持的 Unicode 字符范围,以便在显示文本时选择适合的字体。


它的语法如下:


@font-face {
font-family: "Font Name";
src: url("font.woff2") format("woff2");
unicode-range: U+0020-007E, U+4E00-9FFF;
}

在上述例子中,unicode-range 属性指定了字体文件支持的字符范围。使用逗号分隔不同的范围,并使用 U+XXXX-XXXX 的形式表示 Unicode 字符代码的范围。


通过设置 unicode-range 属性,可以优化字体加载和页面渲染性能,只加载所需的字符范围,减少不必要的网络请求和资源占用。


通过查表得知阿拉伯语的 unicode 的范围为:U+06??, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+10A60-10A7F, U+10A80-10A9F 这么几个区间。所以我们设置字体如下,因为设计以 A 字体为准,所以在 Figma 中给出的样式代码字体名均为 A,所以我们把 B 字体的字体名也设置为 A:


image.png


当使用字体的字符中命中 unicode-rang 的范围时,自动下载相应的字体包。


@font-face {
font-family: A;
src: url('./fonts/A-Bold.ttf');
font-weight: 600;
}

@font-face {
font-family: A;
src: url('./fonts/A-Medium.ttf');
font-weight: 500;
}

@font-face {
font-family: A;
src: url('./fonts/A-Regular.ttf');
font-weight: 400;
}

@font-face {
font-family: A;
src: url('./fonts/A-Thin.ttf');
font-weight: 300;
}

:root {
--ARABIC_UNICODE_RANGE: U+06??, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+10A60-10A7F, U+10A80-10A9F;
}
@font-face {
font-family: A;
src: url('./fonts/B-Bold.ttf');
font-weight: 600;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Medium.ttf');
font-weight: 500;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Regular.ttf');
font-weight: 400;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Thin.ttf');
font-weight: 300;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
p {
font-family: A;
}

总结


遇到的问题:



  1. 两种字体,B 字体为阿拉伯语使用,A 字体其他语言使用。根据语言自动选择。

  2. 根据字宽自动选择相应的字体包。

  3. 可以直接使用 Figma 中生成的样式而不必每次手动改动。

  4. 尽可能节省带宽。


我们通过 font-weight 解决了问题2,并通过 unicode-range 解决了问题1。


并且实现了按需下载相应字体包,不使用时不下载。


Figma 中的代码可以直接复制粘贴,无需任何修改即可根据语言和自宽自动使用相应字体包。




参考资料:http://www.zhangxinxu.com/wordpr

作者:Mengke
来源:juejin.cn/post/7251884086536781880
ess/2…

收起阅读 »

关于Java已死,看看国外开发者怎么说的

博主在浏览 medium 社区时,发现了一篇点赞量 1.5k 的文章,名称叫《Java is Dead — 5 Misconceptions of developers that still think Java is relevant today!》直译过来...
继续阅读 »


博主在浏览 medium 社区时,发现了一篇点赞量 1.5k 的文章,名称叫《Java is Dead — 5 Misconceptions of developers that still think Java is relevant today!》直译过来就是《Java 已死 — 开发人员对 Java 在现代编程语言中的5个误解》。这篇文章可以说是标题党得典范,热度全靠标题蹭 😂。当然本文重点在于文章评论区。作者因为标题党惨着评论区大佬们怒怼,不敢回复。


原文地址:medium.com/@sidh.thoma… Thomas



推荐博主开源的 H5 商城项目waynboot-mall,这是一套全部开源的微商城项目,包含三个项目:运营后台、H5 商城前台和服务端接口。实现了商城所需的首页展示、商品分类、商品详情、商品 sku、分词搜索、购物车、结算下单、支付宝/微信支付、收单评论以及完善的后台管理等一系列功能。 技术上基于最新得 Springboot3.0、jdk17,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中间件。分模块设计、简洁易维护,欢迎大家点个 star、关注博主。


github 地址:github.com/wayn111/way…



下面是文章内容:



人们仍然认为 Java 与当今时代相关,这是一种常见的误解。事实上 Java 是一种正在消亡的编程语言。 Java 一直是世界上使用最广泛、最流行的编程语言之一,但它很快就会面临消亡的危险。如今 Java 拥有庞大而活跃的开发者社区,并且仍然用于广泛的应用程序,包括 Web 开发、移动应用程序开发和企业级软件开发,但 Java 能在未来 10 年生存吗?让我们看看开发者对 Java 有哪些误解:



误解 1:Java 拥有庞大且活跃的开发者社区。世界各地有数百万 Java 开发人员,该语言在开发人员共享知识和资源的在线论坛和社区中占有重要地位。



虽然情况仍然如此,但开发人员转向其他平台和编程语言的速度很能说明问题,我个人也看到开发人员惊慌失措地跳槽。主要问题是 Java 作为一种编程语言还没有现代化,因此它仍然很冗长,通过一个步履蹒跚但极其笨重的类型系统结合了静态和动态类型之间最糟糕的两个世界,并且要求在具有以下功能的 VM 上运行宏观启动时间(对于长时间运行的服务器来说不是问题,但对于命令行应用程序来说是痛苦的)。虽然它现在表现得相当不错,但它仍然无法与 C 或 C++ 竞争,并且只要有一点爱,C#、Go、Rust 和 Python 就可以或将会在该领域超越它。对于现实世界的生产服务器,它往往需要大量的 JVM 调整,而且很难做到正确。



误解 2:Java 的应用范围很广。 Java 不仅仅是一种 Web 开发语言,还用于开发移动应用程序、游戏和企业级软件。这种多功能性使其成为许多不同类型项目的有价值的语言。



Java 不再是移动应用程序开发(尤其是 Android)首选的编程语言。 Kotlin 现在统治着 Android,大多数 Android 开发者很久以前就已经跳槽了。就连谷歌也因为几年前与甲骨文的惨败而放弃了 Java 作为 Android 的事实上的语言。 Java 作为一种 Web 开发语言也早已失去了它的受欢迎程度。就企业开发而言,Java 在大型企业中仍然适用,因为它可靠且稳定。尽管许多初创公司并未将 Java 作为企业软件的首选,但他们正在使用其他替代方案。



误解 3:Java 是基础语言。许多较新的编程语言都是基于 Java 的原理和概念构建的,并且旨在以某种方式与其兼容。这意味着即使 Java 的受欢迎程度下降,它的原理和概念仍然具有相关性。



虽然 Java 确实是许多人开始编程之旅的基础语言,但事实是 Java 仍然非常陈旧且不灵活。最重要的是,与其他现代编程语言相比,它仍然很冗长,这意味着它需要大量代码来完成某些任务。这会使编写简洁、优雅的代码变得更加困难,并且可能需要更多的精力来维护大型代码库。此外,Java 是静态类型的这一事实意味着它可能比动态类型语言更严格且灵活性较差,这可能会让一些开发人员感到沮丧。



误解 4:Java 得到各大公司的大力支持。 Oracle 是维护和支持 Java 的公司,对该语言有着坚定的承诺,并持续投资于其开发和改进。此外,包括 Google 和 Amazon 在内的许多大公司都在其产品和服务中使用 Java。



Oracle 的 Java 市场份额正在快速被竞争对手夺走。见下图:



尽管下图显示甲骨文仍然拥有最大的市场份额,但其份额已减少了一半以上。 2020 年,甲骨文占据了“大约 75% 的 Java 市场”,而现在的份额还不到 35%。


根据 New Relic 的数据,排名第二的是亚马逊,自 2021 年 11 月发布 Java 17 以来,其份额急剧上升,当时其份额几乎与 Eclipse Adoptium 相同。



误解 5:Java 在学校和大学中广泛教授。 Java 是一种流行的编程概念教学语言,经常用于学校和大学的计算机科学课程。这意味着有源源不断的新开发人员正在学习 Java 并熟悉其功能。



这种情况正在发生很大的变化。渴望成为软件开发人员的年轻大学生正在迅速转向其他编程语言。由于对这些其他编程语言的普遍需求,这越来越多地促使学院和大学寻找替代方案。


我知道这是一个有争议的话题。虽然我也认为 Java 是一种彻底改变了软件编写方式的语言,并为其他编程语言树立了可以效仿的基准。但不幸的是,该语言的所有权掌握在公司手中,在没有留下太多财务收益的情况下,该公司没有动力继续改进它。



OK,文章内容就这么多,下面是本文重点!



评论区



喜闻乐见评论区来了 😎,看看国外开发者怎么反驳这篇文章得,本文选取评论点赞量较高得5条评论放在下文。



评论一


来自Migliorabile



作者不知道什么是编程语言、它为什么存在以及它在哪里使用。

仅因为许多程序员都在应用程序中最简单的部分工作,就认为 Java 与 Python 等效,这是完全错误的。

假设自因为使用自行车的人比驾驶采矿机的人多,我就认为自行车比卡特彼勒采矿机更好,这是不对得。



评论二


来自Khalid Hamid



哈哈哈,我想说他甚至可能不是一个程序员,可能会做一些 JavaScript 的事情,即使如此,将 JavaScript 和 TypeScript 归类为两种语言也是没有意义的。

在安卓开发中,他不明白 Kotlin 是什么,虽然它确实有效。



评论三


来自Dan Decker



每次看到这样的文章我都会直接去看评论。(喜闻乐见评论区🤔)



评论四


来自Max Dancona



对于成熟,我有一些话要说。我过去三份工作中有两份是在一些公司开始使用一种性感的新语言(即 ruby 和 python),然后付钱给像我这样的人用 Java 重写他们的应用程序。



评论五


来自Marco Kneubühler



作者似乎不明白编程语言的风格是出于不同的目的而存在的,语言之间进行比较没有意义, 比如拿 sql 或 html/css 与 java 来比?语言是一个丰富的生态系统,我们需要为特定目的选择正确的语言。因此需要多语言开发人员而不是教条主义。



总结


博主这里说下自己得看法,虽然作者对于自己得观点进行了5个误解的阐述,但是博主是并不认同得。



  • 文章的标题就是一个误导性的问题,暗示了 Java 已经不行。事实上 Java 仍然是一门非常流行和强大的编程语言,它在很多领域都有广泛的应用和优势,如移动应用、Web 应用、可穿戴设备、大数据、云计算等。Java 也有不断地更新和改进,引入了很多新的特性和功能,以适应不断变化的技术需求。

  • Java 也有庞大的社区和丰富的资源,为开发者提供了很多支持和帮助。根据 GitHub Octoverse Report 2022,Java 是第三大最受欢迎的语言,仅次于 JavaScript、Python。根据 JetBrains State of Developer Ecosystem 2022,Java 是过去12个月内使用占有率排名第五的语言,占据了 48% 的份额。根据 StackOverflow Developer Survey 2022,最常用的编程语言排行榜中 Java 是排名第六的语言,占据了 33.27% 的份额。这些数据都表明 Java 并没有死亡或不在流行,而是仍然保持着其重要的地位。


GitHub Octoverse Report 2022


JetBrains State of Developer Ecosystem 2022


StackOverflow Developer Survey 2022



  • 文中说 Java 是一门过时和冗长的语言,它没有跟上时代的变化,而其他语言如 Python、JavaScript 和 Kotlin 等都更加简洁和现代化。这个观点忽略了 Java 的设计哲学和目标。Java 是一门成熟、稳定、跨平台、高性能、易维护、易扩展的编程语言,它注重可读性、健壮性和兼容性。Java 的语法可能相对复杂,但它也提供了很多强大的特性和功能,如泛型、注解、枚举、lambda 表达式、流 API、模块化系统等。

  • Java 也没有停止创新和改进,它在近几年引入了很多新的特性和功能,如 Record 类、密封类、模式匹配、文本块、虚拟线程、外部函数和内存API等。其他语言可能在某些方面比 Java 更加简洁或现代化,但它们也有自己的局限和缺点,比如运行速度慢、类型系统弱、错误处理困难等。不同的语言适合不同的场景和需求,并不是说一种语言就可以完全取代另一种语言。


总之,我觉得 Java 在未来会被替代的可能性很小,但也不能掉以轻心,在后端开发领域,Go 已经在逐步蚕食 Java 得份额,今年非常火得 ai 模型领域相关,大部分代码也是基于 Python 编写。Java 需要在保持优势领域地位后持续地创新和改进。



关注公众号【waynblog】每周分享技术干货、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力!


作者:waynaqua
来源:juejin.cn/post/7252127579195736119

收起阅读 »

挤爆服务器,北大法律大模型ChatLaw火了:直接告诉你张三怎么判!

语言大模型不断向垂直行业领域拓展,这次出圈的是北大法律大模型。 大模型又「爆了」。 昨晚,一个法律大模型 ChatLaw 登上了知乎热搜榜榜首。热度最高时达到了 2000 万左右。 这个 ChatLaw 由北大团队发布,致力于提供普惠的法律服务。一方面当前全...
继续阅读 »

语言大模型不断向垂直行业领域拓展,这次出圈的是北大法律大模型。



大模型又「爆了」。


昨晚,一个法律大模型 ChatLaw 登上了知乎热搜榜榜首。热度最高时达到了 2000 万左右。


这个 ChatLaw 由北大团队发布,致力于提供普惠的法律服务。一方面当前全国执业律师不足,供给远远小于法律需求;另一方面普通人对法律知识和条文存在天然鸿沟,无法运用法律武器保护自己。


大语言模型最近的崛起正好为普通人以对话方式咨询法律相关问题提供了一个绝佳契机。



目前,ChatLaw 共有三个版本,分别如下:




  • ChatLaw-13B,为学术 demo 版,基于姜子牙 Ziya-LLaMA-13B-v1 训练而来,中文各项表现很好。但是,逻辑复杂的法律问答效果不佳,需要用更大参数的模型来解决;




  • ChatLaw-33B,也为学术 demo 版,基于 Anima-33B 训练而来,逻辑推理能力大幅提升。但是,由于 Anima 的中文语料过少,问答时常会出现英文数据;




  • ChatLaw-Text2Vec,使用 93w 条判决案例做成的数据集,基于 BERT 训练了一个相似度匹配模型,可以将用户提问信息和对应的法条相匹配。




根据官方演示,ChatLaw 支持用户上传文件、录音等法律材料,帮助他们归纳和分析,生成可视化导图、图表等。此外,ChatLaw 可以基于事实生成法律建议、法律文书。该项目在 GitHub 上的 Star 量达到了 1.1k。



官网地址
http://www.chatlaw.cloud/


论文地址
arxiv.org/pdf/2306.16…


GitHub 地址
github.com/PKU-YuanGro…


目前,由于 ChatLaw 项目太过火爆,服务器暂时崩溃,算力已达上限。该团队正在修复,感兴趣的读者可以在 GitHub 上部署测试版模型。


小编本人也还在内测排队中。所以这里先展示一个 ChatLaw 团队提供的官方对话示例,关于日常网购时可能会遇到的「七天无理由退货」问题。不得不说,ChatLaw 回答挺全的。



不过,小编发现,ChatLaw 的学术 demo 版本可以试用,遗憾的是没有接入法律咨询功能,只提供了简单的对话咨询服务。这里尝试问了几个问题。





其实最近发布法律大模型的不只有北大一家。上个月底,幂律智能联合智谱 AI 发布了千亿参数级法律垂直大模型 PowerLawGLM。据悉该模型针对中文法律场景的应用效果展现出了独特优势。


图源:幂律智能


ChatLaw 的数据来源、训练框架


首先是数据组成。ChatLaw 数据主要由论坛、新闻、法条、司法解释、法律咨询、法考题、判决文书组成,随后经过清洗、数据增强等来构造对话数据。同时,通过与北大国际法学院、行业知名律师事务所进行合作,ChatLaw 团队能够确保知识库能及时更新,同时保证数据的专业性和可靠性。下面我们看看具体示例。


基于法律法规和司法解释的构建示例:



抓取真实法律咨询数据示例:



律师考试多项选择题的建构示例:



然后是模型层面。为了训练 ChatLAW,研究团队在 Ziya-LLaMA-13B 的基础上使用低秩自适应 (Low-Rank Adaptation, LoRA) 对其进行了微调。此外,该研究还引入 self-suggestion 角色,来缓解模型产生幻觉问题。训练过程在多个 A100 GPU 上进行,并借助 deepspeed 进一步降低了训练成本。


如下图为 ChatLAW 架构图,该研究将法律数据注入模型,并对这些知识进行特殊处理和加强;与此同时,他们也在推理时引入多个模块,将通识模型、专业模型和知识库融为一体。


该研究还在推理中对模型进行了约束,这样才能确保模型生成正确的法律法规,尽可能减少模型幻觉。



一开始研究团队尝试传统的软件开发方法,如检索时采用 MySQL 和 Elasticsearch,但结果不尽如人意。因而,该研究开始尝试预训练 BERT 模型来进行嵌入,然后使用 Faiss 等方法以计算余弦相似度,提取与用户查询相关的前 k 个法律法规。


当用户的问题模糊不清时,这种方法通常会产生次优的结果。因此,研究者从用户查询中提取关键信息,并利用该信息的向量嵌入设计算法,以提高匹配准确性。


由于大型模型在理解用户查询方面具有显著优势,该研究对 LLM 进行了微调,以便从用户查询中提取关键字。在获得多个关键字后,该研究采用算法 1 检索相关法律规定。



实验结果


该研究收集了十余年的国家司法考试题目,整理出了一个包含 2000 个问题及其标准答案的测试数据集,用以衡量模型处理法律选择题的能力。


然而,研究发现各个模型的准确率普遍偏低。在这种情况下,仅对准确率进行比较并无多大意义。因此,该研究借鉴英雄联盟的 ELO 匹配机制,做了一个模型对抗的 ELO 机制,以便更有效地评估各模型处理法律选择题的能力。以下分别是 ELO 分数和胜率图:



通过对上述实验结果的分析,我们可以得出以下观察结果


(1)引入与法律相关的问答和法规条文的数据,可以在一定程度上提高模型在选择题上的表现;


(2)加入特定类型任务的数据进行训练,模型在该类任务上的表现会明显提升。例如,ChatLaw 模型优于 GPT-4 的原因是文中使用了大量的选择题作为训练数据;


(3)法律选择题需要进行复杂的逻辑推理,因此,参数量更大的模型通常表现更优。


参考资料



[1]https://www.zhihu.com/question/610072848

[2]https://mp.weixin.qq.com/s/bXAFALFY6GQkL30j1sYCEQ


作者:夕小瑶科技说
来源:juejin.cn/post/7252172628450541624

收起阅读 »

十年码农内功:分布式

分布式协议一口气学完 一、Raft 协议 1.1 基础概念 Raft 算法是通过一切以领导者为准的方式,实现一系列数据的共识和各节点日志的一致。Raft是强领导模型,集群中只能有一个领导者。 成员身份:领导者(Leader)、跟随者(Follower)和候选...
继续阅读 »

分布式协议一口气学完



一、Raft 协议


1.1 基础概念


Raft 算法是通过一切以领导者为准的方式,实现一系列数据的共识和各节点日志的一致。Raft是强领导模型,集群中只能有一个领导者。


成员身份:领导者(Leader)、跟随者(Follower)和候选者(Candidate)。



图1 成员身份



  • 领导者:集群中霸道总裁,一切以我为准,处理写请求、管理日志复制和不断地发送心跳信息。

  • 跟随者:普通成员,处理领导者发来的消息,发现领导者心跳超时,推荐自己成为候选人。

  • 候选人:先给自己投一票,然后请求其他集群节点投票给自己,得票多者成为新的领导者。

  • 任期编号:每任领导者都有任期编号。当领导者心跳超时,跟随者会变成候选人,任期编号 +1,然后发起投票。任期编号小的服从编号大的。

  • 心跳超时:每个跟随者节点都设置了随机心跳超时时间,目的是避免跟随者们同时成为候选人,同时发起投票。

  • 选举超时:每轮选举的结束时间,随机选举超时时间可以避免多个候选人同时结束选举未果,然后同时发起下一轮选举


1.2 领导选举


1.2.1 选举规则



  • 领导者周期性地向跟随者发送心跳消息,告诉跟随者我是领导者,阻止跟随者发变成候选人发起新选举;

  • 如果跟随者的随机心跳超时了,那么认为没有领导者了,推荐自己为候选人,发起新的选举;

  • 在一轮选举中,赢得一半以上选票的候选人,即成为新的领导者;

  • 在一轮选举中,每个节点对每个任期编号的选举只能投出一票,先来先服务原则;

  • 日志完整性高(也就是最后一条日志项对应的任期编号值更大,索引号更大)的跟随者A拒绝给完整性低的候选人B投票,即使B的任期编号大;


1.2.2 选举动画



图2 初始选举



图3 领导者宕机/断网



图4 第一轮选举未果,发起第二轮选举


1.3 日志复制


日志项(Log Entry):是一种数据格式,它主要包含索引值(Log index)、任期编号(Term)和 指令(Command)。



  • 索引值:它是用来标识日志项的,是一个连续的、单调递增的整数值。

  • 任期编号:创建这条日志项的领导者的任期编号。

  • 指令:一条由客户端请求指定的、服务需要执行的指令。



图5 日志信息


1.3.1 日志复制动画



图6 简单日志复制



图7 复杂日志复制


1.3.2 日志恢复


每次选举出来的Leader一定包含在多数节点上最新的已经提交的日志,新的Leader将会覆盖其他节点上不一致的数据。


虽然新Leader一定包括上一个Term的Leader已提交(Committed)日志,但是可能也包含上一个Term的Leader的未提交(Uncommitted)日志。


这部分未提交日志需要转变为Committed,相对比较麻烦,需要考虑Leader多次切换且未完成日志恢复,需要保证最终提案是一致的、确定的,不然就会产生所谓的幽灵复现问题。


为了将上一个Term未提交的日志转为已提交,Raft算法要求Leader当选后立即追加一条Noop的特殊内部日志,并立即同步到其它节点,实现前面未提交日志全部隐式提交。


这样保证客户端不会读到未提交数据,因为只有Noop被大多数节点同意并提交了之后(这样可以连带往期日志一起同步),服务才会对外正常工作;


Noop日志本身是一个分界线,Noop之前的日志被提交,之后的日志将会被丢弃。Noop日志仅包含任期编号和日志索引值,没有指令。


日志“幽灵复现”的场景



图8


第一步,A是领导者,在本地记录4和5日志,并没有提交,然后挂了。



图9


第二步,由于B的日志索引值比C的大,B成为了领导者,仅把日志3同步给了C,然后挂了。



图10


第三步,A恢复了,并且成为了领导者,然后把未提交的日志4和5同步给了B和C(C在A成为了领导者之后、同步日志之前恢复了),然后ABC都提交了日志4和5,就这样原本客户端写失败的日志4和5复活了,进而客户端会读到其认为未提交的日志(实际上集群日志已提交)。


Noop解决日志复现


第一步,同上面一样。


第二步,由于B的日志索引值比C的大,B成为了领导者,这次不仅把日志3同步给了C,还记录了一个Noop日志,并且同步给了C。



图11


第三步,当A恢复了,想成为领导者,发现自己的日志任期编号和日志索引值都不是最大的,即使B挂了也还有C,A也就成为不了领导者,乖乖使用B的日志覆盖自己的日志。


1.4 成员变更


集群成员变更最大的风险是可能同时出现 2 个领导者。比如在成员变更时,节点 A、B 和 C 之间发生了分区错误,节点 A、B 组成旧集群(ABC)中的“大多数”。


而节点 C 和新节点 D、E 组成了新集群(ABCDE)的“大多数”,它们可能会选举出新的领导者(比如节点 C)。结果出现了同时存在 2 个领导者的情况。违反了Raft协议中领导者唯一性原则。



图12 集群(ABC)同时增加节点D和E


最初解决办法是联合共识(Joint Consensus),但实现起来难,后来 Raft 的作者就提出了一种改进后的方法,单节点变更(single-server changes)。


在正常情况下,旧集群的“大多数”和新集群的“大多数”都会有一个重叠的节点。



图13 集群(ABCD)增加新节点E



图14 集群(ABCDE)删除节点A



图15 集群(ABC)增加新节点D



图16 集群(ABCD)删除节点A


需要注意的是,在分区错误、节点故障等情况下,如果并发执行单节点变更,那么就可能出现一次单节点变更尚未完成,新的单节点变更又在执行,导致集群出现 2 个领导者的情况。


二、Gossip 协议


Gossip协议,顾名思义,就像流言蜚语一样,利用一种随机、带有传染性的方式,将信息传播到整个网络中,并在一定时间内,使得系统内的所有节点数据一致。Gossip其实是一种去中心化思路的分布式协议,解决信息在集群中的传播和最终一致性。


2.1 原理


Gossip协议的消息传播主要有两种:反熵(Anti-Entropy)和谣言传播(Rumor-Mongering)。


2.1.1 反熵:节点相对固定,节点数量不多,以固定概率传播所有的数据


每个节点周期性地随机选择其他节点,通过互相交换各自的所有数据来消除两者之间的差异,实现数据的最终一致性。反熵非常可靠,但每次节点两两交换各自的所有数据会带来非常大的通信负担,因此不会频繁使用。通过引入校验和等机制,可以降低需要对比的数据量和传播消息量。


反熵 使用“simple epidemics”方式,其包含两种状态:susceptible和infective,这种模型也称为SI model。处于infective状态的节点代表其有数据更新,并且会将这个数据分享给其他节点;处于susceptible状态的节点代表其并没有收到来自其他节点的更新。



图17 反熵


2.2.2 谣言传播:节点动态变化,节点数量较多,仅传播新到达的数据


当一个节点有了新信息后,这个节点变成活跃状态,并周期性地向其他节点传播新信息。直到所有的节点都知道该新信息。由于节点之间只传播新信息,所以大大减少了通信负担。


谣言传播 使用“complex epidemics”方法,比反熵 多了一种状态:removed,这种模型也称为SIR model。处于removed状态的节点说明其已经接收到来自其他节点的更新,但是其并不会将这个更新分享给其他节点。因为谣言消息会在某个时间标记为removed,然后不会再被传播给其他节点,所以谣言传播有极小概率使得所有节点数据不一致。



图18 谣言传播


一般来说,为了在通信代价和可靠性之间取得折中,需要将这两种方法结合使用。


2.2 通信方式


节点间的交互主要有三种方式:推、拉和推/拉



图19 节点状态


2.2.1 推送模式(push)


节点A随机选择联系节点B,并向其发送自己的信息,节点B在收到信息后比较/更新自己的数据。



图20 推方式


2.2.2 拉取模式(pull)


节点A随机选择联系节点B,从对方获取信息,节点A在收到信息后比较/更新自己的数据。



图21 拉方式


2.2.3 推/拉模式(push/pull)


节点A向选择的节点B发送信息,同时从对方获取信息,节点A和节点B在收到信息后各自比较/更新自己的数据。



图22 推/拉方式


2.3 优缺点



  • 优点

    • 可扩展性(Scalable): Gossip协议是可扩展的,一般需要 O(logN) 轮就可以将信息传播到所有的节点,其中N代表节点的个数。每个节点仅发送固定数量的消息,并且与网络中节点数目无关。在数据传送时,节点并不会等待消息的Ack,所以消息传送失败也没有关系,因为可以通过其他节点将消息传递给之前传送失败的节点。允许节点的任意增加和减少,新增节点的数据最终会与其他节点一致。

    • 容错(Fault-tolerance): 网络中任何节点的重启或者宕机都不会影响 Gossip 消息的传播,具有天然的分布式系统容错特性。

    • 健壮性(Robust): Gossip协议是去中心化的协议,集群中的所有节点都是对等的,没有特殊的节点,所以任何节点出现问题都不会阻止其他节点继续发送消息。任何节点都可以随时加入或离开,而不会影响系统的整体服务质量。

    • 最终一致性(Convergent consistency): Gossip协议实现信息指数级的快速传播,因此在有新信息需要传播时,消息可以快速地发送到全局节点,在有限的时间内能够做到所有节点都拥有最新的数据。



  • 缺点

    • 消息延迟:节点随机向少数几个节点发送消息,消息最终是通过多个轮次的散播而到达全网,不可避免的造成消息延迟。

    • 消息冗余:节点定期随机选择周围节点发送消息,而收到消息的节点也会重复该步骤,不可避免地引起同一节点消息多次接收,增加消息处理压力。




三、参考


作者:科英
来源:juejin.cn/post/7251501954156855352

收起阅读 »

用 node 实战一下 CSRF

web
前言 之前面试经常被问到 CSRF, 跨站请求伪造 大概流程比较简单, 大概就是用户登录了A页面,存下来登录凭证(cookie), 攻击者有诱导受害者打开了B页面, B页面中正好像A发送了一个跨域请求,并把cookie进行了携带, 欺骗浏览器以为是用户的行为...
继续阅读 »

前言


之前面试经常被问到 CSRF, 跨站请求伪造



大概流程比较简单, 大概就是用户登录了A页面,存下来登录凭证(cookie), 攻击者有诱导受害者打开了B页面, B页面中正好像A发送了一个跨域请求,并把cookie进行了携带, 欺骗浏览器以为是用户的行为,进而达到执行危险行为的目的,完成攻击



上面就是面试时,我们通常的回答, 但是到底是不是真是这样呢? 难道这么容易伪造吗?于是我就打算试一下能不能实现


接下来,我们就通过node起两个服务 A服务(端口3000)和B服务(端口4000), 然后通过两个页面 A页面、和B页面模拟一下CSRF。


我们先约定一下 B页面是正常的页面, 起一个 4000 的服务, 然后 A页面为伪造者的网站, 服务为3000


先看B页面的代码, B页面有一个登录,和一个获取数据的按钮, 模拟正常网站,需要登录后才可以获取数据


<body>
<div>
正常 页面 B
<button onclick="login()">登录</button>
<button onclick="getList()">拿数据</button>
<ul class="box"></ul>
<div class="tip"></div>
</div>
</body>
<script>
async function login() {
const response = await fetch("http://localhost:4000/login", {
method: "POST",
});
const res = await response.json();
console.log(res, "writeCookie");
if (res.data === "success") {
document.querySelector(".tip").innerHTML = "登录成功, 可以拿数据";
}
}

async function getList() {
const response = await fetch("http://localhost:4000/list", {
method: "GET",
});

if (response.status === 500) {
document.querySelector(".tip").innerHTML = "cookie失效,请先登录!";
document.querySelector(".box").innerHTML = "";
} else {
document.querySelector(".tip").innerHTML = "";
const data = await response.json();
let html = "";
data.map((el) => {
html += `<div>${el.id} - ${el.name}</div>`;
});
document.querySelector(".box").innerHTML = html;
}
}
</script>

在看B页面的服务端代码如下:


const express = require("express");
const app = express();

app.use(express.json()); // json
app.use(express.urlencoded({ extends: true })); // x-www-form-urlencoded

app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
// 允许客户端跨域传递的请求头
res.header("Access-Control-Allow-Headers", "Content-Type");
next();
});

app.use(express.static("public"));

app.get("/list", (req, res) => {
const cookie = req.headers.cookie;
if (cookie !== "user=allow") {
res.sendStatus("500");
} else {
res.json([
{ id: 1, name: "zhangsan" },
{ id: 2, name: "lisi" },
]);
}
});

app.post("/login", (req, res) => {
res.cookie("user", "allow", {
expires: new Date(Date.now() + 86400 * 1000),
});
res.send({ data: "success" });
});

app.post("/delete", (req, res) => {
const cookie = req.headers.cookie;
if (req.headers.referer !== req.headers.host) {
console.log("should ban!");
}
if (cookie !== "user=allow") {
res.sendStatus("500");
} else {
res.json({
data: "delete success",
});
}
});

app.listen(4000, () => {
console.log("sever 4000");
});

B 服务有三个接口, 登录、获取列表、删除。 再触发登录接口的时候,会像浏览器写入cookie, 再删除或者获取列表的时候,都先检测有没有将指定的cookie传回,如果有就认为有权限


然后我们打开 http://localhost:4000/B.html 先看看B页面功能是否都正常


image.png


我们看到此时 B 页面功能和接口都是正常的, cookie 也正常进行了设置,每次获取数据的时候,都是会携带cookie到服务端校验的


那么接下来我们就通过A页面,起一个3000端口的服务,来模拟一下跨域情况下,能否完成获取 B服务器数据,调用 B 服务器删除接口的功能


A页面代码


  <body>
<div>
伪造者页面 A
<form action="http://localhost:4000/delete" method="POST">
<input type="hidden" name="account" value="xiaoming" />
</form>
<script>
// 这行可以放到控制台执行,便于观察效果
// document.forms[0].submit();
</script>
</div>
<ul class="box"></ul>
<div class="tip"></div>
</body>

A页面服务端代码


  <body>
<div>
伪造者页面 A
<form action="http://localhost:4000/delete" method="POST">
<input type="hidden" name="account" value="xiaoming" />
</form>
<script>
// 这行可以放到控制台输入
// document.forms[0].submit();
</script>
<script src="http://localhost:4000/list"></script>
</div>

</body>

于是在我们 访问 http://localhost:3000/A.html 页面的时候发现, 发现list列表确实,请求到了, 控制台输入 document.forms[0].submit() 时发现,确实删除也发送成功了, 是不是说明csrf就成功了呢, 但是其实还不是, 关键的一点是, 我们在B页面设置cookie的时候, domain设置的是 localhost 那么其实在A页面, 发送请求的时候cookie是共享的状态, 真实情况下,肯定不会是这样, 那么为了模拟真实情况, 我们把 http://localhost:3000/A.html 改为 http://127.0.0.1:3000/A.html, 这时发现,以及无法访问了, 那么这是怎么回事呢, 说好的,cookie 会在获取过登录凭证下, 再次访问时可以携带呢。


image.png


于是,想了半天也没有想明白, 难道是浏览器限制严格进行了限制, 限制规避了这个问题? 难道我们背的面试题是错误的?


有知道的

作者:重阳微噪
来源:juejin.cn/post/7250374485567340603
小伙伴,欢迎下方讨论

收起阅读 »

前端流程图插件对比选型

web
前言 前端领域有多种流程库可供选择,包括但不限于vue-flow、butterfly、JointJS、AntV G6、jsPlumb和Flowchart.js。这些库都提供了用于创建流程图、图形编辑和交互的功能。然而,它们在特性、易用性和生态系统方面存在一些差...
继续阅读 »

Snipaste_2023-07-04_15-49-12.png


前言


前端领域有多种流程库可供选择,包括但不限于vue-flow、butterfly、JointJS、AntV G6、jsPlumb和Flowchart.js。这些库都提供了用于创建流程图、图形编辑和交互的功能。然而,它们在特性、易用性和生态系统方面存在一些差异。


流程图插件汇总


序号名称地址
1vue-flowgithub.com/bcakmakoglu…
2butterflygithub.com/alibaba/but…
3JointJShttp://www.jointjs.com/
4AntV G6antv-2018.alipay.com/zh-cn/g6/3.…
5jsPlumbgithub.com/jsplumb/jsp…
6Flowchart.jsgithub.com/adrai/flowc…

流程图插件分析


vue-flow


简介


vue-flowReactFlow 的 Vue 版本,目前只支持 在Vue3中使用,对Vue2不兼容,目前国内使用较少。包含四个功能组件 core、background、controls、minimap,可按需使用。


使用


Vue FlowVue下流程绘制库。安装:
npm i --save @vue-flow/core 安装核心组件
npm i --save @vue-flow/background 安装背景组件
npm i --save @vue-flow/controls 安装控件(放大,缩小等)组件
npm i --save @vue-flow/minimap 安装缩略图组件

引入组件:
import { Panel, PanelPosition, VueFlow, isNode, useVueFlow } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'
import { MiniMap } from '@vue-flow/minimap'

引入样式:
@import '@vue-flow/core/dist/style.css';
@import '@vue-flow/core/dist/theme-default.css';

优缺点分析


优点:



  1. 轻松上手:内置缩放和平移功能、元素拖动、选择等等。

  2. 可定制:使用自定义节点、边缘和连接线并扩展Vue Flow的功能。

  3. 快速:链路被动更改,仅重新渲染适当的元素。

  4. 工具和组合:带有图形助手和状态可组合函数,用于高级用途。

  5. 附加组件:背景(内置模式、高度、宽度或颜色),小地图(右下角)、控件(左下角)。


缺点:



  1. 仓库迭代版本较少,2022年进入首次迭代。

  2. 国内使用人数少,没有相关技术博客介绍,通过官网学习。


butterfly


简介


Butterfly是由阿里云-数字产业产研部孵化出来的的图编辑器引擎,具有使用自由、定制性高的优势,已支持上百张画布。号称 “杭州余杭区最自由的图编辑器引擎”。


使用



  • 安装


//
npm install butterfly-dag --save


  • 在 Vue3 中使用


<script lang="ts" setup>
import {TreeCanvas, Canvas} from 'butterfly-dag';
const root = document.getElementById('chart')
const canvas = new Canvas({
root: root,
disLinkable: true, // 可删除连线
linkable: true, // 可连线
draggable: true, // 可拖动
zoomable: true, // 可放大
moveable: true, // 可平移
theme: {
edge: {
shapeType: "AdvancedBezier",
arrow: true,
arrowPosition: 0.5, //箭头位置(0 ~ 1)
arrowOffset: 0.0, //箭头偏移
},
},
});
canvas.draw(mockData, () => {
//mockData为从mock中获取的数据
canvas.setGridMode(true, {
isAdsorb: false, // 是否自动吸附,默认关闭
theme: {
shapeType: "circle", // 展示的类型,支持line & circle
gap: 20, // 网格间隙
background: "rgba(0, 0, 0, 0.65)", // 网格背景颜色
circleRadiu: 1.5, // 圆点半径
circleColor: "rgba(255, 255, 255, 0.8)", // 圆点颜色
},
});
});
</script>

<template>
<div class="litegraph-canvas" id="chart"></div>
</template>

优缺点分析


优点:



  1. 轻松上手:基于dom的设计模型大大方便了用户的入门门槛,提供自定义节点,锚点的模式大大降低了用户的定制性。

  2. 多技术栈支持:支持 jquery 基于 dom 的设计,也包含 butterfly-react、butterfly-vue 两种设计。

  3. 核心概念少而精:提供 画布(Canvas)、节点(Node)、线(Edge)等核心概念。

  4. 优秀的组件库支持:对于当前使用组件库来说,可以大量复用现有的组件。


缺点:



  1. butterfly 对 Vue的支持不是特别友好,这跟阿里的前端技术主栈为React有关,butterfly-vue库只支持 Vue2版本。在Vue3上使用需要对 butterfly-drag 进行封装。


JointJS


简介


创建静态图表或完全交互式图表工具,例如工作流编辑器、流程管理工具、IVR 系统、API 集成器、演示应用程序等等。


属于闭源收费项目,暂不考虑。


AntV G6


简介


AntV 是蚂蚁金服全新一代数据可视化解决方案,致力于提供一套简单方便、专业可靠、无限可能的数据可视化最佳实践。G6 是一个图可视化引擎。它提供了图的绘制、布局、分析、交互、动画等图可视化的基础能力。G6可以实现很多d3才能实现的可视化图表。


使用



  • 安装


npm install --save @antv/g6	//安装


  • 在所需要的文件中引入


<template>
/* 图的画布容器 */
<div id="mountNode"></div>
</template>

<script lang="ts" setup>
import G6 from '@antv/g6';
// 定义数据源
const data = {
// 点集
nodes: [
{
id: 'node1',
x: 100,
y: 200,
},
{
id: 'node2',
x: 300,
y: 200,
},
],
// 边集
edges: [
// 表示一条从 node1 节点连接到 node2 节点的边
{
source: 'node1',
target: 'node2',
},
],
};

// 创建 G6 图实例
const graph = new G6.Graph({
container: 'mountNode', // 指定图画布的容器 id
// 画布宽高
width: 800,
height: 500,
});
// 读取数据
graph.data(data);
// 渲染图
graph.render();
</script>



优缺点分析


优点:



  1. 强大的可定制性:G6 提供丰富的图形表示和交互组件,可以通过自定义配置和样式来实现各种复杂的图表需求。

  2. 全面的图表类型支持:G6 支持多种常见图表类型,如关系图、流程图、树图等,可满足不同领域的数据可视化需求。

  3. 高性能:G6 在底层图渲染和交互方面做了优化,能够处理大规模数据的展示,并提供流畅的交互体验。


缺点:



  1. 上手难度较高:G6 的学习曲线相对较陡峭,需要对图形语法和相关概念有一定的理解和掌握。

  2. 文档相对不完善:相比其他成熟的图表库,G6 目前的文档相对较简单,部分功能和使用方法的描述可能不够详尽,需要进行更深入的了解与实践。


jsPlumb


简介


一个用于创建交互式、可拖拽的连接线和流程图的 JavaScript 库。它在 Web 应用开发中广泛应用于构建流程图编辑器、拓扑图、组织结构图等可视化操作界面。


使用


<template>
<div ref="container">
<div ref="sourceElement">Source</div>
<div ref="targetElement">Target</div>
</div>

</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { jsPlumb } from 'jsplumb';

const container = ref<HTMLElement | null>(null);
const sourceElement = ref<HTMLElement | null>(null);
const targetElement = ref<HTMLElement | null>(null);

onMounted(() => {
// 创建 jsPlumb 实例
const jsPlumbInstance = jsPlumb.getInstance();

// 初始化 jsPlumb 实例设置
if (container.value) {
jsPlumbInstance.setContainer(container.value);
}

// 创建连接线
if (sourceElement.value && targetElement.value) {
jsPlumbInstance.connect({
source: sourceElement.value,
target: targetElement.value,
});
}
});
</script>

优缺点分析


优点:



  1. 简单易用:jsPlumb 提供了直观的 API 和丰富的文档,比较容易上手和使用。

  2. 可拓展性:允许开发人员根据自己的需求进行定制和扩展,使其适应不同的应用场景。

  3. 强大的连接功能:jsPlumb 允许创建各种连接类型,包括直线、曲线和箭头等,满足了复杂交互需求的连接效果。
    缺点:

  4. 文档更新不及时:有时候,jsPlumb 的官方文档并没有及时更新其最新版本的特性和用法。

  5. 性能考虑:在处理大量节点、连接线或复杂布局时,jsPlumb 的性能可能受到影响,需要进行优化。


Flowchart.js


简介


Flowchart.js 是一款开源的JavaScript流程图库,可以使用最短的语法来实现在页面上展示一个流程图,目前大部分都是用在各大主流 markdown 编辑器中,如掘金、csdn、语雀等等。


使用


flowchat
start=>start: 开始
end=>end: 结束
input=>inputoutput: 我的输入
output=>inputoutput: 我的输出
operation=>operation: 我的操作
condition=>condition: 确认
start->input->operation->output->condition
condition(yes)->end
condition(no)->operation

优缺点


优点:



  1. 使用方便快捷,使用几行代码就可以生成一个简单的流程图。

  2. 可移植:在多平台上只需要写相同的代码就可以实现同样的效果。


缺点:



  1. 可定制化限制:对于拥有丰富需求的情况下,flowchartjs只能完成相对简单的需求,没有高级的定制化功能。

  2. 需要花费一定时间来学习他的语法和规则,但是flowchartjs的社区也相对不太活跃。


对比分析




  1. 功能和灵活性:



    • Butterfly、G6 和 JointJS 是功能较为丰富和灵活的库。它们提供了多种节点类型、连接线样式、布局算法等,并支持拖拽、缩放、动画等交互特性。

    • Vue-Flow 来源于 ReactFlow 基于 D3和vueuse等库,提供了 Vue 组件化的方式来创建流程图,并集成了一些常见功能。

    • jsPlumb 专注于提供强大的连接线功能,具有丰富的自定义选项和功能。

    • Flowchart.js 则相对基础,提供了构建简单流程图的基本功能。




  2. 技术栈和生态系统:



    • Vue-Flow 是基于 Vue.js 的流程图库,与 Vue.js 生态系统无缝集成。

    • Butterfly 是一个基于 TypeScript 的框架,适用于现代 Web 开发。

    • JointJS、AntV G6 和 jsPlumb 可以与多种前端框架(如Vue、React、Angular等)结合使用。

    • AntV G6 是 AntV 团队开发的库,其背后有强大的社区和文档支持。




  3. 文档和学习曲线:



    • Butterfly、G6 和 AntV G6 都有完善的文档和示例,提供了丰富的使用指南和教程。

    • JointJS 和 jsPlumb 也有较好的文档和示例资源,但相对于前三者较少。

    • Flowchart.js 的文档相对较少。




  4. 兼容性:



    • Butterfly、JointJS 和 G6 库在现代浏览器中表现良好,并提供了兼容低版本浏览器

    • 作者:WayneX
      来源:juejin.cn/post/7251835247595110457
      l>

收起阅读 »

全局唯一ID生成

在Java中,可以使用多种方法生成唯一的ID。下面我将介绍几种常用的方法: UUID(Universally Unique Identifier):UUID是一种128位的唯一标识符。它可以通过java.util.UUID类来生成,使用UUID.rando...
继续阅读 »

在Java中,可以使用多种方法生成唯一的ID。下面我将介绍几种常用的方法:




  1. UUID(Universally Unique Identifier):UUID是一种128位的唯一标识符。它可以通过java.util.UUID类来生成,使用UUID.randomUUID()方法返回一个新的UUID。UUID的生成是基于时间戳和计算机MAC地址等信息,因此几乎可以保证全局唯一性。


    import java.util.UUID;

    public class UniqueIdExample {
    public static void main(String[] args) {
    UUID uuid = UUID.randomUUID();
    String id = uuid.toString();
    System.out.println(id);
    }
    }



  2. 时间戳:可以使用当前时间戳作为唯一ID。使用System.currentTimeMillis()方法可以获取当前时间的毫秒数作为ID值。需要注意的是,时间戳只是在同一台机器上保持唯一性,在分布式系统中可能存在重复的风险。


    public class UniqueIdExample {
    public static void main(String[] args) {
    long timestamp = System.currentTimeMillis();
    String id = String.valueOf(timestamp);
    System.out.println(id);
    }
    }



  3. Snowflake算法:Snowflake是Twitter开源的一种分布式ID生成算法,可以生成带有时间戳、机器ID和序列号的唯一ID。可以使用第三方库(如Twitter的Snowflake)来生成Snowflake ID。Snowflake ID的生成是基于时间序列、数据中心ID和机器ID等参数的。


    import com.twitter.snowflake.SnowflakeIdGenerator;

    public class UniqueIdExample {
    public static void main(String[] args) {
    SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator();
    long id = idGenerator.nextId();
    System.out.println(id);
    }
    }



以上是一些常用的生成唯一ID的方法,每种方法都有自己的特点和适用场景。选择合适的方法要根据具体需求、性能要求

作者:Lemonade22
来源:juejin.cn/post/7250037058684583995
以及系统架构来决定。

收起阅读 »

为什么选择 Next.js 框架?

web
前言 Next.js 框架作为一种强大而受欢迎的工具,为开发人员提供了许多优势和便利。本文将探讨 Next.js 框架的优点,并解释为什么选择 Next.js 是一个明智的决策。 文档:nextjs.org/docs 强大的服务端渲染和静态生成能力: Ne...
继续阅读 »

前言


Next.js 框架作为一种强大而受欢迎的工具,为开发人员提供了许多优势和便利。本文将探讨 Next.js 框架的优点,并解释为什么选择 Next.js 是一个明智的决策。



文档:nextjs.org/docs



强大的服务端渲染和静态生成能力:


Next.js 框架提供了先进的服务端渲染(SSR)和静态生成(SSG)能力,使得我们能够在服务器上生成动态内容并将其直接发送给客户端,从而大大减少首次加载的等待时间。这样可以提高网站的性能、搜索引擎优化(SEO)以及用户体验。


简化的数据获取:


Next.js 提供了简单易用的数据获取方法,例如 getServerSidePropsgetStaticProps,使得从后端获取数据并将其注入到组件中变得非常容易。这种无缝的数据获取流程,可以让开发人员专注于业务逻辑而不用过多关注数据获取的细节。


优化的路由系统:


Next.js 内置了灵活而强大的路由功能,使得页面之间的导航变得简单直观。通过自动化的路由管理,我们可以轻松地构建复杂的应用程序,并实现更好的用户导航体验。


支持现代前端技术栈:


Next.js 是建立在 React 生态系统之上的,因此可以充分利用 React 的强大功能和丰富的社区资源。同时,Next.js 也支持最新的 JavaScript(ES6+)特性,如箭头函数、模块化导入导出、解构赋值等,让开发人员可以使用最新的前端技术来构建现代化的应用。


简化的部署和扩展:


Next.js 提供了轻松部署和扩展应用程序的工具和解决方案。借助 Vercel、Netlify 等平台,我们可以快速将应用程序部署到生产环境,并享受高性能、弹性扩展的好处。Next.js 还支持构建静态站点,可以轻松地将应用部署到 CDN 上,提供更快的加载速度和更好的全球可访问性。


大型社区支持:


Next.js 拥有庞大的开发者社区,其中有许多优秀的开源项目和库。这意味着你可以从社区中获取到大量的学习资源、文档和支持。无论是在 Stack Overflow 上寻求帮助,还是参与讨论,你都能够从其他开发人员的经验中获益。


什么环境下需要选择nextjs框架?


需要服务端渲染或静态生成:


如果你的应用程序需要在服务器端生成动态内容,并将其直接发送给客户端,以提高性能和搜索引擎优化,那么 Next.js 是一个很好的选择。它提供了强大的服务端渲染和静态生成能力,使得构建高性能的应用变得更加简单。


需要快速开发和部署:


Next.js 提供了简化的开发流程和快速部署的解决方案。它具有自动化的路由管理、数据获取和构建工具,可以提高开发效率。借助 Vercel、Netlify 等平台,你可以轻松地将 Next.js 应用部署到生产环境,享受高性能和弹性扩展的好处。


基于 React 的应用程序:


如果你已经熟悉 React,并且正在构建一个基于 React 的应用程序,那么选择 Next.js 是自然而然的。Next.js 是建立在 React 生态系统之上的,提供了与 React 紧密集成的功能和工具。


需要良好的 SEO 和页面性能:


如果你的应用程序对搜索引擎优化和良好的页面性能有较高的要求,Next.js 可以帮助你实现这些目标。通过服务端渲染和静态生成,Next.js 可以在初始加载时提供完整的 HTML 内容,有利于搜索引擎索引和页面的快速呈现。


需要构建现代化的单页应用(SPA):


尽管 Next.js 可以支持传统的多页面应用(MPA),但它也非常适合构建现代化的单页应用(SPA)。你可以使用 Next.js 的路由系统、数据获取和状态管理功能,构建出功能丰富且响应快速的 SPA。


与nextjs相似的框架?


Nuxt.js:


Nuxt.js 是一个基于 Vue.js 的应用框架,提供了类似于 Next.js 的服务端渲染和静态生成功能。它通过使用 Vue.js 的生态系统,使得构建高性能、可扩展的 Vue.js 应用变得更加简单。


Gatsby:


Gatsby 是一个基于 React 的静态网站生成器,具有类似于 Next.js 的静态生成功能。它使用 GraphQL 来获取数据,并通过预先生成静态页面来提供快速的加载速度和良好的SEO。


Angular Universal:


Angular Universal 是 Angular 框架的一部分,提供了服务端渲染的能力。它可以生成动态的 HTML 内容,从而加快首次加载速度,并提供更好的 SEO 和用户体验。


Sapper:


Sapper 是一个基于 Svelte 的应用框架,支持服务端渲染和静态生成。它提供了简单易用的工具和流畅的开发体验,帮助开发者构建高性能的 Sv

作者:嚣张农民
来源:juejin.cn/post/7251875626906599485
elte 应用程序。

收起阅读 »

为什么你非常不适应 TypeScript

web
前言 在群里看到一些问题和言论:为什么你们这么喜欢“类型体操”?为什么我根本学不下去 TypeScript?我最讨厌那些做类型体操的了;为什么我学了没过多久马上又忘了? 有感于这些问题,我想从最简单的一个角度来切入介绍一下 TypeScript,并向大家介绍并...
继续阅读 »

前言


在群里看到一些问题和言论:为什么你们这么喜欢“类型体操”?为什么我根本学不下去 TypeScript?我最讨厌那些做类型体操的了;为什么我学了没过多久马上又忘了?


有感于这些问题,我想从最简单的一个角度来切入介绍一下 TypeScript,并向大家介绍并不是只要是个类型运算就是体操。并在文中介绍一种基本思想作为你使用类型系统的基本指引。


引子


我将从一个相对简单的 API 的设计过程中阐述关于类型的故事。在这里我们可以假设我们现在是一个工具的开发者,然后我们需要设计一个 API 用于从对象中拿取指定的一些 key 作为一个新的对象返回给外面使用。


垃圾 TypeScript


一个人说:我才不用什么破类型,我写代码就是要没有类型,我就是要随心所欲的写。然后写下了这段代码。


declare function pick(target: any, ...keys: any): any

他的用户默默的写下了这段代码:


pick(undefined, 'a', 1).b

写完运行,发现问题大条了,控制台一堆报错,接口数据也提交不上去了,怎么办呢?


刚学 TypeScript


一个人说:稍微检查一下传入类型就好了,别让人给我乱传参数就行。


declare function pick(target: Record<string, unknown>, ...keys: string[]): unknown

很好,上面的问题便不复存在了,API 也是基本可用的了。但是!当对象复杂的时候,以及字段并不是短单词长度的时候就会发现了一个没解决的问题。


pick({ abcdefghijkl: '123' }, 'abcdefghikjl')

从肉眼角度上,我们很难发现这前后的不一致,所以我们为什么要让调用方的用户自己去 check 自己的字段有没有写对呢?


不就 TypeScript


一个人说:这还不简单,用个泛型加 keyof 不就行了。


declare function pick<
T extends Record<string, unknown>
>(target: T, ...keys: keyof T[]): unknown

我们又进一步解决的上面的问题,但是!还是有着相似的问题,虽然我们不用检查 keys 是不是传入的是一个正确的值了,但是我们实际上对返回的值也存在一个类似的问题。


pick({ abcdefghijkl: '123' }, 'abcdefghijkl').abcdefghikjl



  • 一点小小的拓展


    在这里我们看起来似乎是一个很简单的功能,但实际上蕴含着一个比较重要的信息。


    为什么我们之前的方式都拿不到用户传入进来的类型信息呢?是有原因的,当我们设计的 API 的时候,前面的角度是从,如何校验类型方向进行的思考。


    而这里是尝试去通过约定好的一种规则,通过 TypeScript 的隐式类型推断获得到传入的类型,再通过约定的规则转化出一种新的类型约束来对用户的输入进行限制。




算算 TypeScript


一个人说:好办,算出来一个新的类型就好了。


declare function pick<
T extends Record<string, unknown>,
Keys extends keyof T
>(target: T, ...keys: Keys[]): {
[K in Keys]: T[K]
}

到这里已经是对类型的作用有了基础的了解了,能写出来符合开发者所能接受的类型相对友好的代码了。我们可以再来思考一些更特殊的情况:


// 输入了重复的 key
pick({ a: '' }, 'a', 'a')

完美 TypeScript


到这里,我们便是初步开始了类型“体操”。但是在本篇里,我们不去分析它。


export type L2T<L, LAlias = L, LAlias2 = L> = [L] extends [never]
? []
: L extends infer LItem
? [LItem?, ...L2T<Exclude<LAlias2, LItem>, LAlias>]
: never

declare function pick<
T extends Record<string, unknown>,
Keys extends L2T<keyof T>
>(target: T, ...keys: Keys): Pick<T, Keys[number] & keyof T>

const x0 = pick({ a: '1', b: '2' }, 'a')
console.log(x0.a)
// @ts-expect-error
console.log(x0.b)

const x1 = pick({ a: '1', b: '2' }, 'a', 'a')
// ^^^^^^^^
// TS2345: Argument of type '["a", "a"]' is not assignable to parameter of type '["a"?, "b"?] | ["b"?, "a"?]'.
//   Type '["a", "a"]' is not assignable to type '["a"?, "b"?]'.
//     Type at position 1 in source is not compatible with type at position 1 in target.
//       Type '"a"' is not assignable to type '"b"'.

一个相对来说比较完美的 pick 函数便完成了。


总结


我们再来回到我们的标题吧,从我对大多数人的观察来说,很多的人开始来使用 TypeScript 有几种原因:



  • 看到大佬们都在玩,所以自己也想来“玩”,然后为了过类型校验而去写

  • 看到一些成熟的项目在使用 TypeScript ,想参与贡献,参与过程中为了让类型通过而想办法去解决类型报错

  • 公司整体技术栈采用的是 TypeScript ,要用 TypeScript 进行业务编写,从而为了过类型检查和 review 而去解决类型问题


诸如此类的问题还有很多,我将这种都划分为「为了解决类型检查的问题」而进行的类型编程,这也是大多数人为什么非常不适应 TypeScript,甚至不喜欢他的一个原因。这其实对学习 TypeScript 并不是一个很好的思路,在这里我觉得我们需要站在设计者的角度去对类型系统进行思考。我觉得有以下几个角度:



  • 类型检查到位

  • 类型提示友好

  • 类型检查严格

  • 扩展性十足


我们如果站在这几个角度对我们的 API 进行设计,我们可以发现,开发者能够很轻松的将他们需要的代码编写出来,而尽量不用去翻阅文档,查找 example。


希望通过我的这篇分享,大家能对 TypeScript 多一些理解,并参与到生态中来,守护我们的 JavaScript。




2023/06/27 更新



理性探讨,在评论区说什么屎不是屎的,嘴巴臭可以不说话的。


没谁逼着你一定要写最后一种层次的代码,能力不足可以学啊,不喜欢可以不学啊,能达到倒数第二个就已经很棒啊。


最后一种只是给大家看看 TypeScript 的一种可能,而不是说你应该这么做的。


作者:一介4188
来源:juejin.cn/post/7248599585751515173

收起阅读 »

次世代前端视图框架都在卷啥?

web
上图是 State of JavaScript 2022 前端框架满意度排名。前三名分别是 Solid、Svelte、Qwik。我们可以称他们为次世代前端框架的三大代表,前辈是 React/Angular/Vue。 目前 React/Augular/Vue 还...
继续阅读 »

state of JavaScript 2022 满意度排名


上图是 State of JavaScript 2022 前端框架满意度排名。前三名分别是 SolidSvelteQwik。我们可以称他们为次世代前端框架的三大代表,前辈是 React/Angular/Vue
目前 React/Augular/Vue 还占据的主流的市场地位, 现在我们还不知道下一个五年、十年谁会成为主流,有可能前辈会被后浪拍死在沙滩上, 也有可能你大爷还是你大爷。


就像编程语言一样,尽管每年都有新的语言诞生,但是撼动主流编程语言的地位谈何容易。在企业级项目中,我们的态度会趋于保守,选型会偏向稳定、可靠、生态完善的技术,因此留给新技术的生存空间并不多。除非是革命性的技术,或者有大厂支撑,否则这些技术或框架只会停留小众圈子内。



比如有一点革命性、又有大厂支撑的 Flutter。





那么从更高的角度看,这些次时代的前端视图框架在卷哪些方向呢?有哪些是革命性的呢?


先说一下本文的结论:



  • 整体上视图编程范式已经固化

  • 局部上体验上内卷






视图编程范式固化


从 JQuery 退出历史舞台,再到 React 等占据主流市场。视图的编程范式基本已经稳定下来,不管你在学习什么视图框架,我们接触的概念模型是趋同的,无非是实现的手段、开发体验上各有特色:



  • 数据驱动视图。数据是现代前端框架的核心,视图是数据的映射, View=f(State) 这个公式基本成立。

  • 声明式视图。相较于上一代的 jQuery,现代前端框架使用声明式描述视图的结构,即描述结果而不是描述过程。

  • 组件化视图。组件是现代前端框架的第一公民。组件涉及的概念无非是 props、slots、events、ref、Context…






局部体验内卷


回顾一下 4 年前写的 浅谈 React 性能优化的方向,现在看来依旧不过时,各大框架无非也是围绕着这些「方向」来改善。


当然,在「框架内卷」、「既要又要还要」时代,新的框架要脱颖而出并不容易,它既要服务好开发者(开发体验),又要服务好客户(用户体验) , 性能不再是我们选择框架的首要因素。




以下是笔者总结的,次世代视图框架的内卷方向:



  • 用户体验

    • 性能优化

      • 精细化渲染:这是次世代框架内卷的主要战场,它们的首要目的基本是实现低成本的精细化渲染

        • 预编译方案:代表有 Svelte、Solid

        • 响应式数据:代表有 Svelte、Solid、Vue、Signal(不是框架)

        • 动静分离





    • 并发(Concurrent):React 在这个方向独枳一树。

    • 去 JavaScript:为了获得更好的首屏体验,各大框架开始「抛弃」JavaScript,都在比拼谁能更快到达用户的眼前,并且是完整可交互的形态。



  • 开发体验

    • Typescript 友好:不支持 Typescript 基本就是 ca

    • 开发工具链/构建体验: Vite、Turbopack… 开发的工具链直接决定了开发体验

    • 开发者工具:框架少不了开发者工具,从 Vue Devtools 再到 Nuxt Devtools,酷炫的开发者工具未来可能都是标配

    • 元框架: 毛坯房不再流行,从前到后、大而全的元框架称为新欢,内卷时代我们只应该关注业务本身。代表有 Nextjs、Nuxtjs










精细化渲染






预编译方案


React、Vue 这些以 Virtual DOM 为主的渲染方式,通常只能做到组件级别的精细化渲染。而次世代的 Svelte、Solidjs 不约而同地抛弃了 Virtual DOM,采用静态编译的手段,将「声明式」的视图定义,转译为「命令式」的 DOM 操作


Svelte


<script>
let count = 0

function handleClick() {
count += 1
}
</script>

<button on:click="{handleClick}">Clicked {count} {count === 1 ? 'time' : 'times'}</button>

编译结果:


// ....
function create_fragment(ctx) {
let button
let t0
let t1
let t2
let t3_value = /*count*/ (ctx[0] === 1 ? 'time' : 'times') + ''
let t3
let mounted
let dispose

return {
c() {
button = element('button')
t0 = text('Clicked ')
t1 = text(/*count*/ ctx[0])
t2 = space()
t3 = text(t3_value)
},
m(target, anchor) {
insert(target, button, anchor)
append(button, t0)
append(button, t1)
append(button, t2)
append(button, t3)

if (!mounted) {
dispose = listen(button, 'click', /*handleClick*/ ctx[1])
mounted = true
}
},
p(ctx, [dirty]) {
if (dirty & /*count*/ 1) set_data(t1, /*count*/ ctx[0])
if (
dirty & /*count*/ 1 &&
t3_value !== (t3_value = /*count*/ (ctx[0] === 1 ? 'time' : 'times') + '')
)
set_data(t3, t3_value)
},
i: noop,
o: noop,
d(detaching) {
if (detaching) {
detach(button)
}

mounted = false
dispose()
},
}
}

function instance($$self, $$props, $$invalidate) {
let count = 0

function handleClick() {
$$invalidate(0, (count += 1))
}

return [count, handleClick]
}

class App extends SvelteComponent {
constructor(options) {
super()
init(this, options, instance, create_fragment, safe_not_equal, {})
}
}

export default App

我们看到,简洁的模板最终被转移成了底层 DOM 操作的命令序列。


我写文章比较喜欢比喻,这种场景让我想到,编程语言对内存的操作,DOM 就是浏览器里面的「内存」:



  • Virtual DOM 就是那些那些带 GC 的语言,使用运行时的方案来屏蔽 DOM 的操作细节,这个抽象是有代价的

  • 预编译方案则更像 Rust,没有引入运行时 GC, 使用了一套严格的所有权和对象生命周期管理机制,让编译器帮你转换出安全的内存操作代码。

  • 手动操作 DOM, 就像 C、C++ 这类底层语言,需要开发者手动管理内存


使用 Svelte/SolidJS 这些方案,可以做到修改某个数据,精细定位并修改 DOM 节点,犹如我们当年手动操作 DOM 这么精细。而 Virtual DOM 方案,只能到组件这一层级,除非你的组件粒度非常细。








响应式数据


和精细化渲染脱不开身的还有响应式数据


React 一直被诟病的一点是当某个组件的状态发生变化时,它会以该组件为根,重新渲染整个组件子树,如果要避免不必要的子组件的重渲染,需要开发者手动进行优化(比如 shouldComponentUpdatePureComponentmemouseMemo/useCallback)  。同时你可能会需要使用不可变的数据结构来使得你的组件更容易被优化。


在 Vue 应用中,组件的依赖是在渲染过程中自动追踪的,所以系统能精确知晓哪个组件确实需要被重渲染。


近期比较火热的 signal (信号,Angular、Preact、Qwik、Solid 等框架都引入了该概念),如果读者是 Vue 或者 MobX 之类的用户, Signal 并不是新的概念。


按 Vue 官方文档的话说:从根本上说,信号是与 Vue 中的 ref 相同的响应性基础类型。它是一个在访问时跟踪依赖、在变更时触发副作用的值容器。


不管怎样,响应式数据不过是观察者模式的一种实现。相比 React 主导的通过不可变数据的比对来标记重新渲染的范围,响应式数据可以实现更细粒度的绑定;而且响应式的另一项优势是它的可传递性(有些地方称为 Props 下钻(Props Drilling))。






动静分离


Vue 3 就是动静结合的典型代表。在我看来 Vue 深谙中庸之道,在它身上我们很难找出短板。


Vue 的模板是需要静态编译的,这使得它可以像 Svelte 等框架一样,有较大的优化空间;同时保留了 Virtual DOM 和运行时 Reactivity,让它兼顾了灵活和普适性。


基于静态的模板,Vue 3 做了很多优化,笔者将它总结为动静分离吧。比如静态提升、更新类型标记、树结构打平,无非都是将模板中的静态部分和动态部分作一些分离,避免一些无意义的更新操作。


更长远的看,受 SolidJS 的启发, Vue 未来可能也会退出 Vapor 模式,不依赖 Virtual DOM 来实现更加精细的渲染。








再谈编译时和运行时


编译时和运行时没有优劣之分, 也不能说纯编译的方案就必定是未来的趋势。


这几年除了新的编译时的方案冒出来,宣传自己是未来;也有从编译时的焦油坑里爬出来, 转到运行时方案的,这里面的典型代表就是 Taro。


Taro 2.0 之前采用的是静态编译的方案,即将 ’React‘ 组件转译为小程序原生的代码:


Untitled


但是这个转译工作量非常庞大,JSX 的写法千变万化,非常灵活。Taro 只能采用 穷举 的方式对 JSX 可能的写法进行了一 一适配,这一部分工作量很大,实际上 Taro 有大量的 Commit 都是为了更完善的支持 JSX 的各种写法。这也是 Taro 官方放弃这种架构的原因。


也就是说 Taro 也只能覆盖我们常见的 JSX 用法,而且我们必须严格遵循 Taro 规范才能正常通过。


有非常多的局限:



  • 静态的 JSX

  • 不支持高阶组件

  • 不支持动态组件

  • 不支持操作 JSX 的结果

  • 不支持 render function

  • 不能重新导出组件

  • 需要遵循 on*、render* 约束

  • 不支持 Context、Fragment、props 展开、forwardRef

  • ….


有太多太多的约束,这已经不是带着镣铐跳舞了,是被五花大绑了。




使用编译的方案不可避免的和实际运行的代码有较大的 Gap,源码和实际运行的代码存在较大的差别会导致什么?



  • 比较差的 Debug 体验。

  • 比较黑盒。


我们在歌颂编译式的方案,能给我们带来多大的性能提升、带来多么简洁的语法的同时。另一方面,一旦我们进行调试/优化,我们不得不跨越这层 Gap,去了解它转换的逻辑和底层实现。


这是一件挺矛盾的事情,当我们「精通」这些框架的时候,估计我们已经是一个人肉编译器了。


Taro 2.x 配合小程序, 这对卧龙凤雏, 可以将整个开发体验拉到地平线以下。




回到这些『次世代』框架。React/Vue/Angular 这些框架先入为主, 在它们的教育下,我们对前端视图开发的概念和编程范式的认知已经固化。


Untitled


比如在笔者看来 Svelte 是违法直觉的。因为 JavaScript 本身并不支持这种语义。Svelte 要支持这种语义需要一个编译器,而作为一个 JavaScript 开发者,我也需要进行心智上的转换。


而 SolidJS 则好很多,目之所及都是我们熟知的东西。尽管编译后可能是一个完全不一样的东西。



💡 Vue 曾经也过一个名为**响应性语法糖的实验性功能来探索这个方向,但最后由于这个原因**,废弃了。这是一次明智的决定



当然,年轻的次世代的前端开发者可能不这么认为,他们毕竟没有经过旧世代框架的先入为主和洗礼,他们更能接受新的开发范式,然后扛起这些旗帜,让它们成为未来主流。


总结。纯编译的方能可以带来更简洁的语法、更多性能优化的空间,甚至也可以隐藏一些跨平台/兼容性的细节。另一方面,源码和实际编译结果之间的 Gap,可能会逼迫开发者成为人肉编译器,尤其在复杂的场景,对开发者的心智负担可能是翻倍的。


对于框架开发者来说,纯编译的方案实现复杂度会更高,这也意味着,会有较高贡献门槛,间接也会影响生态。








去 JavaScript


除了精细化渲染,Web 应用的首屏体验也是框架内卷的重要方向,这个主要的发展脉络,笔者在 现代前端框架的渲染模式 一文已经详细介绍,推荐大家读一下:


Untitled


这个方向的强有力的代表主要有 Astro(Island Architecture 岛屿架构)、Next.js(React Server Component)、Qwik(Resumable 去 Hydration)。


这些框架基本都是秉承 SSR 优先,在首屏的场景,JavaScript 是「有害」的,为了尽量更少地向浏览器传递 JavaScript,他们绞尽脑汁 :



  • Astro:’静态 HTML‘优先,如果想要 SPA 一样实现复杂的交互,可以申请开启一个岛屿,这个岛屿支持在客户端进行水合和渲染。你可以把岛屿想象成一个 iframe 一样的玩意。

  • React Server Component: 划分服务端组件和客户端组件,服务端组件仅在服务端运行,客户端只会看到它的渲染结果,JavaScript 执行代码自然也仅存于服务端。

  • Qwik:我要直接革了水合(Hydration)的命,我不需要水合,需要交互的时候,我惰性从服务端拉取事件处理器不就可以了…


不得不说,「去 JavaScript」的各种脑洞要有意思多了。






总结


本文主要讲了次世代前端框架的内卷方向,目前来看还处于量变的阶段,并没有脱离现在主流框架的心智模型,因此我们上手起来基本不会有障碍。


作为普通开发者,我们可以站在更高的角度去审视这些框架的发展,避免随波逐流和无意义的内卷。






扩展阅读



作者:荒山
来源:juejin.cn/post/7251763342954512440
收起阅读 »

为了娃的暑期课,老父亲竟然用上了阿里云高大上的 Serverless FaaS!!!

web
起因 事件的起因是,最近家里的俩娃马上要放暑假了,家里的老母亲早早的就规划好了姐姐弟弟的暑期少年宫课程,奈何有些想上个课程一直没有”抢“到课程。平时带娃在少年宫上课的父母可能懂的,一般少年宫的课程都是提前预报名,然后会为了公平起见进行摇号,中者缴费。本来是一件...
继续阅读 »

起因


事件的起因是,最近家里的俩娃马上要放暑假了,家里的老母亲早早的就规划好了姐姐弟弟的暑期少年宫课程,奈何有些想上个课程一直没有”抢“到课程。平时带娃在少年宫上课的父母可能懂的,一般少年宫的课程都是提前预报名,然后会为了公平起见进行摇号,中者缴费。本来是一件比较合理的处理方式,但奈何不住各位鸡娃的父母们的上有政策下有对策的路子。



第一阶段:靠数量提高命中率 ,大家都各自报了很多不同课程,防止因为摇号没摇上,导致落空。我们家也是一样操作~~~。但是这里也会出现另一种状况,当摇号结束,大家缴费期间,有的摇中家长,发现课程多了或者有些课程和课外兴趣班冲突,或者种种其他原因,不想再上暑期课程了,就会取消这门课程。 即时你缴费了,后面也是可以取消的,只是会扣除一些费用。
第二阶段:捡漏,有报多的家长,就有没有抢到合适课程的家长。没错,说的正是我们家 哈哈。在我老婆规划中,我们还有几门课程没有摇中,那这个时候怎么办呢?只能蹲守,人工不定时的登录查课,寄期望于有些家长退课了,我们好第一时间补上去。


当当当,作为一个程序员老父亲,这个时候终于排上用场了~~~,花了一个晚上,写了个定时查询脚本+通知,当有课放出,咱们就通知一下领导(老婆大人)定夺,话说这个小查课定时任务深受领导的高度表扬。
好了起因就是这样,下面我们回到正题,给大家实操下如何使用阿里云的Serverless 函数,来构建这个小定时脚本。


架构


很简单的架构图,只用到了这么几个组件,



  • Serverless FC 无服务器函数,承载逻辑主体

  • OSS 存储中间结果数据

  • RAM是对计算函数赋予角色使其有对应权限。

  • 企业微信机器人,企业微信本身可以随便注册,拉个企业微信群,加入一个群机器人,就可以作为消息触达端。



实践


函数计算FC



本次实操中,我们需要先了解阿里云的函数计算FC几个概念,方便我们后面操作理解:



相关官方资料:基本概念

下面我只列了本次操作涉及到的概念,更详细资料,建议参考官方文档。




  • 服务:服务是函数计算资源管理的单位,是符合微服务理念的概念。从业务场景出发,一个应用可以拆分为多个服务。从资源使用维度出发,一个服务可以由多个函数组成。

  • FC函数:函数计算的资源调度与运行是以函数为单位。FC函数由函数代码和函数配置构成。FC函数 必须从属于服务,同一个服务下的所有函数共享一些相同的设置,例如服务授权、日志配置。函数的相关操作

  • 层:层可以为您提供自定义的公共依赖库、运行时环境及函数扩展等发布与部署能力。您可以将函数依赖的公共库提炼到层,以减少部署、更新时的代码包体积,也可以将自定义的运行时,以层部署在多个函数间共享。

  • 触发器:触发器是触发函数执行的方式。在事件驱动的计算模型中,事件源是事件的生产者,函数是事件的处理者,而触发器提供了一种集中、统一的方式来管理不同的事件源


创建函数



  1. 函数计算FC--> 任务--> 选择创建函数




  1. 配置函数


这里我截了个长屏,来给大家逐个解释



tips: 如果大家也有截长屏需求,推荐chrome 中的插件:Take Webpage Screenshots Entirely - FireShot




  • 函数方式:我的小脚本是python 代码,我直接使用自定义运行环境,如果你想了解这三种方式区别,建议详细阅读这篇文章:函数计算支持的多语言运行时信息

  • 服务名称:我们如果初次创建,选择创建一个服务,然后填入自己设定的服务名字即可

  • 函数代码:这里我选择运行时python 3.9 , 示例代码(代码等我们创建完成之后,再填充自己的代码逻辑)

  • 高级配置: 这里如果是初学者,个人建议尽量选最小配置,因为函数计算是按你使用的资源*次数 收费的, 这里我改成了资源粒度,0.05vCpu 128MB,并发度 1

  • 函数变量:我暂时不需要,就没有设置,如果你需要外部配置一些账号密码,可以使用这种方式来配置

  • 触发器:这里展示出了函数计算的协同作用,可以通过多种云服务产品来进行事件通知触发,我们这里的样例只需要一个定时轮询调度,所以这里我使用了定时触发器,5分钟调用一次。



配置依赖



函数整体创建成功之后,点击函数名称,进入函数详情页



函数代码模块填充本地已经调试好的代码, 测试函数,发现相关依赖并没有,这里我们需要编辑层,来将python相关依赖文件引入, 点击上图中编辑层



我选择的是在线构建依赖层,按照requirements.txt的格式书写,然后就可以在线安装了,很方便。创建成功之后,回到编辑层位置,选择刚开始创建的层,点击确定,既可,这样就不会再报相关依赖缺失了。


配置OSS映射


我的小脚本里,需要存储中间数据,因为函数计算FC本身是无状态的,所以需要借助外部存储,很自然的就会想到用OSS来存储。但是如何正确的使用OSS桶来存储中间数据呢?
官方关于python操作OSS的教程:python 操作 OSS



# -*- coding: utf-8 -*-
import oss2
# 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
auth = oss2.Auth('<yourAccessKeyId>', '<yourAccessKeySecret>')
# Endpoint以杭州为例,其它Region请按实际情况填写。
bucket = oss2.Bucket(auth, 'http://oss-cn-hangzhou.aliyuncs.com', '<yourBucketName>')

这里操作基本都会使用到 AK,SK。 但是基于云上的安全的实践操作以及要定期更换ak,sk来保证安全,尽量不要直接在代码中使用ak, sk来调用api 。那是否有其他更合理的操作方式?
我找到了这篇文章 配置OSS文件系统



函数计算支持与OSS无缝集成。您可以为函数计算的服务配置OSS挂载,配置成功后,该服务下的函数可以像使用本地文件系统一样使用OSS存储服务。



个人推荐这种解决方案



  • 只需要配置对应函数所授权的角色策略中,加上对相应的挂载OSS桶的读写权限

  • 这个操作符合最小粒度的赋予权限,同时也减少代码开发量,python可以像操作本地磁盘一样,操作oss,简直不要太方便~~~

  • 同时也不需要担心所谓的ak sk泄漏风险以及需要定期更换密钥的麻烦,因为就不存在使用ak sk


我最后也是用这种方式,配置了oss文件系统映射到函数运行时的环境磁盘上。


企业微信机器人


企业微信可以直接注册,不需要任何费用,之后两个人拉一个群,添加一个群机器人即可。
可以参考官方文档:如何使用群机器人 来用python 发送群消息,很简单的一段代码既可完成发送消息通知。


wx_url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx-xxxxx-xxxxx"
def sendWechatBotMsg(msg,collagues):
"""艾特指定同事,并发送指定信息"""
data = json.dumps({"msgtype": "text", "text": {"content": msg, "mentioned_list":collagues}})
r = requests.post(wx_url, data, auth=('Content-Type', 'application/json'))

最后效果如图所示:



总结


通过日常生活中的一个小场景,实践了下阿里云的高大上的Serverless FC 服务。个人选择这种无服务器函数计算,也是结合了成本的因素。
给大家对比下两种方案价格:



  • 传统云主机方式:


阿里云官方ECS主机的定价:实例价格信息
最便宜的一档: 1vCPU 1GB 实例, 每个月也要34.2 RMB 还没有包括挂载的磁盘价格 ,以及公网带宽费用




  • Serverless FC


而使用无服务器函数计算服务, 按使用时长和资源计费,像我这种最小资源粒度就可以满足同时调度次数是周期性的,大大消减了费用, 我跑了大概一周的时间 大概花费了 0.16 RMB,哈哈 简直是不能再便宜了。大家感兴趣的也可以动手实践下自己的需求场景。




云计算已经是当下技术人员的必学的一门课程,如果有时间也鼓励大家可以多了解学习,提升自己的专业能力。感兴趣的朋友,如果有任何问题,需要沟通交流也可以添加我的个人微信 coder_wukong,备注:云计算,或者关注我的公众号 WuKongCoder日常也会不定期写一些文章和思考。




如果觉得文章不错,欢迎大家点赞,留言,转发,收藏 谢谢大家,我们下篇文章再会~~~



参考资料


中国唯一入选 Forrester 领导者象限,阿里云 Serverless 产品能力全球第一

函数计算支持的多语言运行时信息

阿里云OSS文档:python 操作 OSS

阿里云函数计算文档:配置OSS文件系统

企业微信文档:如何使用群机器人

让 Serverless 更普惠

Serverless 在阿里云函数计算中的实践


作者:WuKongCoder
来源:juejin.cn/post/7251786717652107301
收起阅读 »

你还在用传统轮播组件吗?来看看遮罩轮播组件

web
背景 最近有一个页面改版的需求,在UI走查阶段,设计师说原来的轮播组件和新版页面UI整体风格不搭,所以要换掉。 这里就涉及到两种轮播组件,一种是传统的轮播组件,一种是设计师要的那种。 传统的轮播组件,大家都见过,原理也清楚,就是把要轮播的图片横向排成一个队列,...
继续阅读 »

背景


最近有一个页面改版的需求,在UI走查阶段,设计师说原来的轮播组件和新版页面UI整体风格不搭,所以要换掉。


这里就涉及到两种轮播组件,一种是传统的轮播组件,一种是设计师要的那种。


传统的轮播组件,大家都见过,原理也清楚,就是把要轮播的图片横向排成一个队列,把他们当成一个整体,每次轮换,其实是把这个队列整体往左平移X像素,这里的X通常就是一个图片的宽度。
这种效果可以参见vant组件库里的swipe组件


而我们设计师要的轮播效果是另外一种,因为我利用端午假期已经做好了一个雏形,所以大家可以直接看Demo


当然你也可以直接打开 腾讯视频APP 首页,顶部的轮播,就是我们设计师要的效果。


需求分析


新式轮播,涉及要两个知识点:



  • 图片层叠

  • 揭开效果


与传统轮播效果一个最明显的不同是,新的轮播效果需要把N张待轮播的图片在Z轴上重叠放置,每次揭开其中的一张,下一张是自然漏出来的。这里的实现方式也有多种,但最先想到的还是用zindex的方案。


第二个问题是如何实现揭开的效果。这里就要使用到css3的新属性mask。
mask是一系列css的简化属性。包括mask-image, mask-position等。
因为mask的系列属性还有一定的兼容性,所以一部分浏览器需要带上-webkit-前缀才能生效。


还有少数浏览器不支持mask属性,退化的情况是轮播必须有效,但是没有轮换的动效。


实现


有了以上的分析,就可以把效果做出来了。核心代码如下:


<script setup lang="ts">
import { ref, onMounted, watch } from "vue";
// 定义属性
const props = defineProps([
'imgList',
'duration',
'transitionDuration',
'maskPositionFrom',
'maskPositionTo',
'maskImageUrl'
]);
// 定义响应式变量
const currentIndex = ref(0);
const oldCurrentIndex = ref(0);
const imgList = ref([...props.imgList, props.imgList[0]]);
const getInitZindex = () => {
const arr = [1];
for (let i = imgList.value.length - 1; i >= 1; i--) {
arr.unshift(arr[0] + 1);
}
return arr;
}
const zIndexArr = ref([...getInitZindex()]);
const maskPosition = ref(props.maskPositionFrom || 'left');
const transition = ref(`all ${props.transitionDuration || 1}s`);
// 设置动画参数
const transitionDuration = props.transitionDuration || 1000;
const duration = props.duration || 3000;

// 监听currentIndex变化
watch(currentIndex, () => {
if (currentIndex.value === 0) {
zIndexArr.value = [...getInitZindex()];
}
maskPosition.value = props.maskPositionFrom || 'left';
transition.value = 'none';
})
// 执行动画
const execAnimation = () => {
transition.value = `all ${props.transitionDuration || 1}s`;
maskPosition.value = props.maskPositionFrom || 'left';
maskPosition.value = props.maskPositionTo || 'right';
oldCurrentIndex.value = (currentIndex.value + 1) % (imgList.value.length - 1);
setTimeout(() => {
zIndexArr.value[currentIndex.value] = 1;
currentIndex.value = (currentIndex.value + 1) % (imgList.value.length - 1);
}, 1000)
}
// 挂载时执行动画
onMounted(() => {
const firstDelay = duration - transitionDuration;
function animate() {
execAnimation();
setTimeout(animate, duration);
}
setTimeout(animate, firstDelay);
})
</script>
<template>
<div class="fly-swipe-container">
<div class="swipe-item"
:class="{'swipe-item-mask': index === currentIndex}"
v-for="(url, index) in imgList"
:key="index"
:style="{ zIndex: zIndexArr[index],
'transition': index === currentIndex ? transition : 'none',
'mask-image': index === currentIndex ? `url(${maskImageUrl})` : '',
'-webkit-mask-image': index === currentIndex ? `url(${maskImageUrl})`: '',
'mask-position': index === currentIndex ? maskPosition: '',
'-webkit-mask-position': index === currentIndex ? maskPosition: '' }"
>

<img :src="url" alt="">
</div>
<div class="fly-indicator">
<div class="fly-indicator-item"
:class="{'fly-indicator-item-active': index === oldCurrentIndex}"
v-for="(_, index) in imgList.slice(0, imgList.length - 1)"
:key="index">
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.fly-swipe-container {
position: relative;
overflow: hidden;
width: 100%;
height: inherit;
.swipe-item:first-child {
position: relative;
}
.swipe-item {
position: absolute;
width: 100%;
top: 0;
left: 0;
img {
display: block;
width: 100%;
object-fit: cover;
}
}
.swipe-item-mask {
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: cover;
-webkit-mask-size: cover;
}
.fly-indicator {
display: flex;
justify-content: center;
align-items: center;
z-index: 666;
position: relative;
top: -20px;
.fly-indicator-item {
margin: 0 5px;
width: 10px;
height: 10px;
border-radius: 50%;
background: gray;
}
.fly-indicator-item-active {
background: #fff;
}
}
}
</style>

这是一个使用 Vue 3 构建的图片轮播组件。在这个组件中,我们可以通过传入一组图片列表、切换动画的持续时间、过渡动画的持续时间、遮罩层的起始位置、遮罩层的结束位置以及遮罩层的图片 URL 来自定义轮播效果。


组件首先通过 defineProps 定义了一系列的属性,并使用 ref 创建了一些响应式变量,如 currentIndexoldCurrentIndeximgListzIndexArr 等。


onMounted 钩子函数中,我们设置了一个定时器,用于每隔一段时间执行一次轮播动画。
在模板部分,我们使用了一个 v-for 指令来遍历图片列表,并根据当前图片的索引值为每个图片元素设置相应的样式。同时,我们还为每个图片元素添加了遮罩层,以实现轮播动画的效果。


在样式部分,我们定义了一些基本的样式,如轮播容器的大小、图片元素的位置等。此外,我们还为遮罩层设置了一些样式,包括遮罩图片的 URL、遮罩层的位置等。


总之,这是一个功能丰富的图片轮播组件,可以根据传入的参数自定义轮播效果。


后续


因为mask可以做的效果还有很多,后续该组件可以封装更多轮播效果,比如从多个方向的揭开效果,各种渐变方式揭开效果。欢迎使用和提建议。


仓库地址:github.com/cunzai

zhuyi…

收起阅读 »

你们公司的官网被搜索引擎收录了吗?

web
前言 前段时间,我司的官网要改版。老板们手一挥,提出了以下几点需求 网站要大气,炫酷,有科技感 图片文字要高大上 注重SEA、SEO优化,用户查找关键字后,我们公司的网站排名要显示在前列 为此,我们还专门买了一个SEO优化的课程,大张旗鼓的学习了一通。至于...
继续阅读 »

1.jpg


前言


前段时间,我司的官网要改版。老板们手一挥,提出了以下几点需求



  • 网站要大气,炫酷,有科技感

  • 图片文字要高大上

  • 注重SEA、SEO优化,用户查找关键字后,我们公司的网站排名要显示在前列


为此,我们还专门买了一个SEO优化的课程,大张旗鼓的学习了一通。至于效果如何,1个月见分晓


那么如何编写 JavaScript 代码以有利于 SEO 和 SEA 呢?


下面仅展示被被谷歌搜索引擎收录的


SEA、SEO优化


保持好的网页结构



  1. 使用语义化的 HTML结构


HTML语义化是指使用恰当的HTML标签来描述网页内容的结构和含义,以提高网页的可读性、可访问性和搜索引擎优化。



  • header: 网站的页眉部分

  • nav: 定义网站的主要导航链接

  • main: 定义页面的主要内容区域,每个页面应该只有一个<main>标签

  • section: 定义页面中的独立区块, 例如文章、产品列表等

  • article: 定义独立的文章内容,通常包含标题、作者、发布日期等信息

  • aside: 定义页面的侧边栏或附属信息区域

  • footer: 网站的页脚部分


<header>
<h1>官网</h1>
<nav>
<ul>
<li><a href="#">首页</a></li>
<li><a href="#">关于我们</a></li>
<li><a href="#">联系我们</a></li>
</ul>
</nav>
</header>

<main>
<div>欢迎来到我们的网站</div>
<p>这里是网站的主要内容。</p>
</main>


<section>
<h2>最新文章</h2>
<article>
<h3>文章标题</h3>
<p>文章内容...</p>
</article>
<article>
...
</article>
</section>


<aside>
<h3>最新消息</h3>
<ul>
<li><a href="#">链接1</a></li>
...
</ul>
</aside>

<article>
<h2>消息1</h2>
<p>文章内容...</p>
</article>


<footer>
<p>版权所有 &copy; 2023</p>
<p>联系我们:info@example.com</p>
</footer>



  1. 提供准确且吸引人的页面标题和描述


准确且简洁的标题和描述,有利于吸引访问者和搜索引擎的注意



  • 页面标题: Title

  • 页面描述: Meta Description


<head>
<title>精美手工艺品——手工制作的独特艺术品</title>
<meta name="description" content="我们提供精美手工艺品的设计与制作,包括陶瓷、木雕、织物等。每件艺术品都是独一无二的,以精湛的工艺和创造力打动您的心灵。欢迎浏览我们的作品集。">
</head>


标题要小于50个字符,描述要小于150字符





  1. 在关键位置使用关键字: 包括标题、段落文本、链接文本和图片的 alt 属性。



    • 段落文本: 自然的使用关键字,有助于搜索引擎收录

    • 链接文本: 使用描述性的链接文本,并在其中包含关键字,这有助于搜索引擎理解链接指向的内容

    • 图片的 alt 属性: 对于每个图像,使用描述性的 alt 属性来说明图像内容,并在其中包含关键字。这不仅有助于视力障碍用户理解图像,还可以提供关键字相关的图像描述给搜索引擎。




<h1>欢迎来到精美手工艺品网店</h1>
<p>我们提供各种精美手工艺品,包括陶瓷、木雕、织物等。每个艺术品都是由我们经验丰富的工匠手工制作而成,展现了精湛的工艺和创造力。</p>
<p>浏览我们的<a href="/products" title="手工艺品产品列表">产品列表</a>,您将发现独特的艺术品,适合作为礼物或收藏。</p>
<img src="product.jpg" alt="陶瓷花瓶 - 手工制作的精美艺术品" />


一个页面要保证有且只有h1标签



使用友好的 URL 结构


使用友好的URL结构是一个重要的优化策略,它可以提升网站的可读性、可维护性和用户体验



  • 使用关键字: 在URL中使用关键字,以便用户和搜索引擎可以更好地理解页面的主题和内容, URL中多个关键词使用连字符字符 "-"进行分隔。

  • 结构层次化: 层次化的URL结构来反映内容的结构和关系

  • 避免使用参数: 尽量避免在URL中使用过多的参数,特别是使用随机字符串或数字作为参数

  • 尽量使用永久链接: 尽可能使用永久链接,避免频繁更改URL

  • 尽量保持URL简洁: 避免过长的URL。短连接更易于分享和记忆


<!-- 不友好的URL -->
https://example.com/index.html?category=7&product=12345
https://example.com/qinghua/porcelain

<!-- 友好的URL -->
https://example.com/porcelain/qinghua
https://example.com/blog/friendly-urls


  1. 重要链接不要用JS


搜索引擎爬虫通常不会执行 JavaScript,并且依赖 JavaScript 的链接可能无法被爬虫正确解析和索引



使用标准的 <a> 标签进行跳转,避免使用 JavaScript 跳转




  1. 使用W3C规范


使用W3C规范是确保你的网页符合Web标准并具有良好可访问性的重要方式


不符合W3C的规范:



  • 未闭合的标签

  • 未正确嵌套的元素

  • 行内元素包裹块状元素


<!-- 未闭合的标签 -->
<p>This is a paragraph with no closing tag.
<!-- 未正确嵌套的元素 -->
<div><p>This paragraph is inside a div but not closed properly.</div></p>
<!-- 行内元素包裹块状元素 -->
<span><p>This paragraph is inside a div but not closed properly.</p></span>

响应式设计和移动优化


Google 现在使用了移动优先索引, 搜索引擎更倾向于优先索引和显示移动友好的网页


使用响应式设计,使你的网页在各种设备上都能正确显示。



  1. 响应式设计:确保网页具有响应式设计,能够适应不同设备的屏幕尺寸

  2. 关注移动友好性:确保网页在移动设备上加载和显示良好


JavaScript使用和加载优化


搜索引擎爬虫通常不会执行 JavaScript,并且在抓取和索引页面时可能会忽略其中的动态内容




  1. 加载时间优化: 通过压缩和合并 JavaScript文件,减小文件大小,以及使用异步加载和延迟加载的方式,可以提高网页的加载速度




  2. 避免使用AJAX技术加载核心内容: 对于核心内容,避免使用 AJAX 或动态加载方式,而是在初始页面加载时就呈现。这样可以确保搜索引擎能够正确抓取和索引核心内容,提高网页的可见性和相关性。




  3. 减少懒加载、瀑布流、上拉刷新、下载加载、点击更多等互动加载: 这些常见的页面优化方式虽然有利于用户体验。但搜索引擎爬虫不会执行 JavaScript,并且在抓取和索引页面时可能会忽略其中的动态内容。




  4. js阻塞后保证页面正常运行: 确保网站在没有 JavaScript 的情况下仍然能够正常运行。这有助于搜索引擎爬虫能够正确索引你的网页内容。




性能和体验优化



  1. 提高网站加载速度: 搜索引擎和用户都更喜欢快速加载的网页,提高页面的转加载速度,会对搜索引擎排名产生积极影响。

  2. 优化移动体验: 在移动设备上,用户的粘性和耐心被放大,优化移动体验,减少用户的流失率,会对移动搜索排名产生积极影响。

  3. 无障碍: 在 Web 开发无障碍性意味着使尽可能多的人能够使用 Web 站点, 增加用户人群的受众,会提高搜索引擎排名


内容更新



  1. 内容持续更新: 搜索引擎比较喜欢新鲜的内容,如果网站内容长期不更新的话,搜索引擎就会厌烦我们的网站。反之,我们频繁的更新新闻、博客等内容,会大大的提高

  2. 网页数量尽可能的多: 尽可能的让网页超过15个,



频繁修改或调整网站结构的话就相当于修改了搜索引擎爬取网站的路径,导致网站即使更新再多的内容也难以得到收录



监测


索引


在浏览器中输入 site:你的地址(此方法仅适合谷歌,百度则直接搜索URL地址)


查看是否被索引



  1. 进入Google Search Console

  2. 进入URL检测工具。

  3. 将需要索引的URL粘贴到搜索框中。

  4. 等待谷歌检测URL。

  5. 点击“请求编入索引”按钮。


image.png


收录


点击网址检查: 如果页面被索引,那么会显示“URL is on Google(URL在谷歌中)”。


image.png


如何去收录


image.png


但是,请求编入收录索引不太可能解决旧页面的索引问题,并且这只是一个最原始的方式,提交链接不能确保你的URL一定被收录,尤其是百度。


参考11个让百度快速收录网站的奇思淫技


总结


持续的优化和监测是关键,以确保你的策略和实践符合不断变化的搜索引擎算法和用户需求。


期待一个月后见分晓啦!


参考文献



  1. 11个让百度快速收录网站的奇思淫技

  2. search

  3. JavaScript与SEO之间的藕断丝连关系<
    作者:高志小鹏鹏
    来源:juejin.cn/post/7251786985535275067
    /a>

收起阅读 »

实现联动滚动

序言 在垂直滑动的过程中可以横向滚动内容。 效果 代码 就一个工具类就行了。可以通过root view向上查找recycleView。自动添加滚动监听。在子view完全显示出来以后,才分发滚动事件。这样用户才能看清楚第一个。 需要一个id资源<?xml...
继续阅读 »

序言


在垂直滑动的过程中可以横向滚动内容。


效果


在这里插入图片描述


代码


就一个工具类就行了。可以通过root view向上查找recycleView。自动添加滚动监听。在子view完全显示出来以后,才分发滚动事件。这样用户才能看清楚第一个。


需要一个id资源

<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="key_rv_scroll_helper" type="id"/>
</resources>

RVScrollHelper

package com.trs.myrb.provider.scroll;

import android.view.View;
import android.view.ViewParent;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import com.trs.myrb.R;
import com.trs.myrb.douyin.action.TRSFunction;


/**
* <pre>
* Created by zhuguohui
* Date: 2023/5/12
* Time: 13:49
* Desc:用于监听recycleView的垂直滚动,然后将垂直滚动转发给其他类。
*
* //在provider中使用
* RVScrollHelper.create(binding.getRoot(), dy -> {
* binding.rvHotVideos.scrollBy(dy,0);
* return null;
* });
* </pre>
*/
public class RVScrollHelper implements View.OnAttachStateChangeListener {
TRSFunction<Integer, Void> scrollCallBack;
View child;
RecyclerView recyclerView;
private int recyclerViewHeight;

public static RVScrollHelper create(View child,TRSFunction<Integer, Void> scrollCallBack){
if(child==null){
return null;
}
Object tag = child.getTag(R.id.key_rv_scroll_helper);
if(!(tag instanceof RVScrollHelper)){
RVScrollHelper helper = new RVScrollHelper(child, scrollCallBack);
tag=helper;
child.setTag(R.id.key_rv_scroll_helper,helper);
}
return (RVScrollHelper) tag;
}

private RVScrollHelper(View child, TRSFunction<Integer, Void> scrollCallBack) {
this.scrollCallBack = scrollCallBack;
this.child = child;
this.child.addOnAttachStateChangeListener(this);

}

@Override
public void onViewAttachedToWindow(View v) {

if(child==null){
return;
}
if (recyclerView == null) {
recyclerView = getRecyclerView(v);
recyclerViewHeight = recyclerView.getHeight();
recyclerView.addOnScrollListener(onScrollListener);
}

}


@Override
public void onViewDetachedFromWindow(View v) {

if(recyclerView!=null) {
recyclerView.removeOnScrollListener(onScrollListener);
recyclerView=null;
}
}

private RecyclerView.OnScrollListener onScrollListener=new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {

}

@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
//只有在view全部显示出来以后,才发送滚动事件
boolean isAllShow=isAllShow();
if(isAllShow){
if(scrollCallBack!=null){
scrollCallBack.call(dy);

}
}
}
};

private boolean isAllShow() {
int top = child.getTop();
int bottom = child.getBottom();
return bottom>=0&&bottom<=recyclerViewHeight;

}

private RecyclerView getRecyclerView(View v) {
ViewParent parent = v.getParent();
while (!(parent instanceof RecyclerView) && parent != null) {
parent = parent.getParent();
}
if(parent!=null){
return (RecyclerView) parent;
}
return null;
}




}


使用


在provider中使用。

  RVScrollHelper.create(holder.itemView, dy -> {
recyclerView.scrollBy(dy, 0);
return null;
});

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

每日一题:Zygote 为什么不采用Binder机制进行IPC通信呢?

在android面试中,我们常会遇到Framework面试相关问题,而今天要分享的就是Zygote 为什么不采用Binder机制进行IPC通信呢? 其主要考察的是程序员对binder的理解和zygote fork的理解。 问题正解: 这个很重要的原因是如果zy...
继续阅读 »

在android面试中,我们常会遇到Framework面试相关问题,而今天要分享的就是Zygote 为什么不采用Binder机制进行IPC通信呢?


其主要考察的是程序员对binder的理解和zygote fork的理解。


问题正解:


这个很重要的原因是如果zygote采用binder 会导致 fork出来的进程产生死锁。


在UNIX上有一个 程序设计的准则:多线程程序里不准使用fork。


这个规则的原因是:如果程序支持多线程,在程序进行fork的时候,就可能引起各种问题,最典型的问题就是,fork出来的子进程会复制父进程的所有内容,包括父进程的所有线程状态。那么父进程中如果有子线程正在处于等锁的状态的话,那么这个状态也会被复制到子进程中。父进程的中线程锁会有对应的线程来进行释放锁和解锁,但是子进程中的锁就等不到对应的线程来解锁了,所以为了避免这种子进程出现等锁的可能的风险,UNIX就有了不建议在多线程程序中使用fork的规则。


在Android系统中,Binder是支持多线程的,Binder线程池有可以有多个线程运行,那么binder 中就自然会有出现子线程处于等锁的状态。那么如果Zygote是使用的binder进程 IPC机制,那么Zygote中将有可能出现等锁的状态,此时,一旦通过zygote的fork去创建子进程,那么子进程将继承Zygote的等锁状态。这就会出现子进程一创建,天生的就在等待线程锁,而这个锁缺没有地方去帮它释放,子进程一直处于等待锁的状态。


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

小白如何快速提升技术水平?我的建议是,多加班

经常在技术社区看到有小白提问如何提升技术水平,我也曾是小白,我也曾经问过这个问题,我也不是什么天赋异禀之人,和大部分人都一样,所以我能够理解小白在问这个问题时的感受,作为过来人,解决这个问题的路径其实是非常清晰的,网上这类的答案也很多,不过看过我文章的读者都应...
继续阅读 »

经常在技术社区看到有小白提问如何提升技术水平,我也曾是小白,我也曾经问过这个问题,我也不是什么天赋异禀之人,和大部分人都一样,所以我能够理解小白在问这个问题时的感受,作为过来人,解决这个问题的路径其实是非常清晰的,网上这类的答案也很多,不过看过我文章的读者都应该能够感觉到我是个比较务实的人,对于冠冕堂皇实际上屁用没有的东西不太感冒,所以我的这篇文章可能跟其他人的不要太一样,我只从实际出发,谈谈我的想法


以下内容对于大部分 0-3年经验的小白都适用,仅针对想靠这行混碗饭吃的人(这类人才是这个行业的主力军),天赋异禀的少数群体看个乐子就行


基础知识必须掌握


ReactVue 大行其道的当下,我见过很多人竟然连基本的 js api 都搞不清楚,方法不知道,参数不清楚,掌握的少的可怜的 js 知识,比如 requestAnimationFrameObject.defineProperty等还都纯粹是因为跟 vuereact扯上了关系面试要用所以才看了两眼,我无法理解如果连基础知识都掌握不了的话,又该如何去 深入 理解这些框架?


实际工作用的确实也是框架,很少有需要用原生 js 去实现什么复杂功能的地方,但这并不代表这不重要,恰恰相反,如果不是急着要找工作的话,比如在校大学生,我认为还是先把基础弄清楚,再顺带着学一学框架,无论是什么框架,都是对于底层技术的封装,reactvue等框架更新的确实很快,很多人都在说学不动了,实际上是你学错了,这些上层的东西根本不需要追着学,它们只是趁手的工具,工具是为你所服务的,但现在你却说工具让你追得很累,这本身就是有问题的


我上大学的时候,那个还是 angular 市场占有率最高,我那个时候也学了 angular,但是现在我再看 angular发现早就不是当初我学的那个东西了,假设我现在需要用到 angular,现在学和一路追着 angular那么多版本更新一个个学下来,有区别吗?没区别,都是工具,只要能实现功能就行了,我管你是 angular还是 react,管你是 vue2.x 还是 vue3.x,反正对着文档一把梭就行了,都是基于 jsdsl封装罢了,无非是玩得花不花而已


你可能会说,现在都在用框架,面试问的也是框架知识,基础知识知不知道有什么关系平时根本用不到啊,我并不认同,我就举个例子,经常看到有人说问



只学过vue,现在工作需要用react,怎么办,react好学吗?



我看了就很迷惑,你要是问我vuereact牛不牛逼,我肯定会伸出大拇指绝对牛逼,但你要问我难不难学,那我不能理解了,不就是一个框架吗有什么难不难好不好学的,我只能猜测这人可能一上来就是跟风学了vue,啥基础知识都不懂,要是老老实实先掌握了基础知识,再顺手学个框架,好好研究下框架与基础之间的通用关系,根本不会问这个问题


我不否认有些小众的东西确实难以理解,但像vuereact这种市场占有率很高的大众化框架,不可能存在什么技术门槛的,管你是React还是 Svelte,一天看文档,两天速通,三天上手写业务就是标准流程


当然了,想深入还是要花费些功夫的,但是恕我直言,以绝大多数人(包括我自己)写的业务屎山的逻辑难度而言,根本不需要什么深度技术,深入学习研究更多的是提升自己的认知水平


为了快速找工作,你可以在什么基础知识都不懂的情况下就跟风学框架,问题不大,毕竟吃饭要紧,但是如果你想把饭碗尽可能拿的更久一点或者更进一步,跳过的东西,你必须要再捡回来


另外,多提一嘴,前端三剑客包括 jscsshtml,虽然后两者的重要程度远不能跟 js 相提并论,但既然是基础知识,都还是需要掌握的,作为一名业务前端,总不能跟别人说你不太懂 css吧?


多看


如何掌握基础知识,首先你得知道有哪些基础知识,你作为一个啥也不懂一片空白的新人,了解这个领域最应该做的事情就是多看,看什么?看书、看视频


看书


关于前端书籍推荐的话题或者书单等,网上已经有很多了,我就不再赘述了,不过我发现有些书单也太特么长了,几十上百本书哪有那么多时间去看,好多我名字都没听过也往上凑,这可能会让很多人打退堂鼓,甚至直接摆烂,先进收藏夹再说,至于什么时候看就再说吧


如果只谈基础知识的,我认为只有两本书是必须要看的,《JavaScript高级程序设计》《JavaScript权威指南》,也就是常说的红宝书和犀牛书,这两本书比较厚,但相比于几十上百本书而言,已经很明确了很清晰了,只需要看这两本就行了,是必须要看的,由于这两本书都是出版了较长时间的书籍,一些更新的内容没有包括在内,那么这部分知识就需要自行去网上获取了,比如 ECMAScript 6 入门


至于其他的几十上百本,说实话,看哪本都行,不是说其他书不重要,哪本书都有其存在的意义,但只是相比于这两本没那么重要,多看肯定比不看强,这两本书是必看的综合基础,其他书是在你在掌握了基础之后,根据你的实际情况,提升在不同细分领域水平的助攻


技术社区上的文章也姑且算是书的一种形式吧,零散的文章用于开阔视野紧跟潮流风向,系列文章用于加深在细分领域的认知


看视频


我知道很多人看不起视频学习的,认为效率太低,除此之外还因为跟培训班扯上关系,也让很多自诩清高的人认为太low拉低自己的层次,所以对于看视频学习持否定态度


首先,我也认为看视频学习确实效率很低,但你得分清是对什么层次的人来说的


对于有了三年(只是大概估个数,少部分人可能只需要一年)以上工作经验的人来说,再看视频学习,那确实效率太低了,但是对于三年以下甚至是初识技术世界的小白来说,看视频的效率不是低,而是高,年限越低,看视频学习的效率越高,直到突破了三年这个节点后,看视频学习的效率才开始低于看书


我就是从小白过来的,我很清楚小白对于技术的认知是什么样的,那真是狗屁不通,让这样的人抱着厚厚的一本书在那看,每个字都认识,可一连起来就不知道为什么这样了,书籍内的世界虽然无比广阔,但无人指引,你跑了半天可能还在绕着圈,天地再大与你何干?


但视频就不同了,视频承载的信息量远低于书籍,但那却能给你更清晰的指引,一步步地告诉你该怎么走,万事开头难,入门的时候慢一点没关系,主要是要能入门,入对门,而不是像无头苍蝇一样乱撞


鄙人就是靠视频入门的,一开始大学里学的是 java,但后来对前端感兴趣,转投前端,只能自学,但又不知如何下手,明知道红宝书和犀牛书是很好的书籍,奈何看着看着就想睡觉,每个字都能看懂,但一合上书就忘了今天看了啥,机缘巧合之下在某专门卖视频课的网站买了视频开始学习,这才逐步进入正轨,一改之前苦大仇恨不得要领,每天跟着视频课都学得很开心,因为能学进脑子里了,每天都能感觉到自己又多进步了一点,如此良性循环才算是入了门,入了门之后,才终于具备了自学的能力,因为知道路在哪里了,知道该怎么走了


视频的信息量低吗?低,无论是以前还是现在我都这么认为


但视频课的效率一定低吗?不一定,对于已经入了门有了足够经验的人来说,效率是低的,因为确实信息密度低嘛,但对于小白来说,重要的是要先入门,哪种方式能先入门哪种方式才是效率高的



掘金小册我认为是介于书籍和视频之间的一种形式,兼顾了二者的部分优点



多写


看了不一定就是会了,必须要亲自上手写才能考验出是否是真的懂了,看视频或者看书,每一页都能看懂,但是真正让你写了却不知道如何下手,那不叫懂,上学时对着答案做题就没有不会的,但没了答案或者换了个形式你就不会了,那不叫会



写什么?写所有你能写的代码,无论是什么代码


写多少?能写多少是多少,写得越多越好



有小白曾问过我,说他看别人写的代码都能看懂也能理解,但是让自己独立写的话,他就不知道该怎么写了,为什么会出现这种情况?我想都不想就知道这人代码写得太少了


这一行绝大部分人干的活,包括一些所谓的大厂员工(比如我),实际上真的都只是搬砖活而已,根本不需要太高的技术含量,也就是远不到比拼智商的时候,只要你智商和别人差得不太多,那么别人能够做到的事情,你肯定也能做到,如果你做不到,只是因为你懒罢了


这么说吧,我认为对于大多数人来说,如果他们有机会从一开始就进入并夕夕过上 997 的工作模式,只要不是真的智商有问题,那么经过了两年的高强度输出之后,技术能力怎么也能达到这个行业的中上水准,这就是多写带来的收益


你一年写得代码比其他人三年写得还多,一些代码编写规范或者设计模式你背都背下来了,水平还能怎么低?


没错,虽然zz不正确,但我依然鼓励加班,但,需要注意前提


前提是你是个初入技术大门的小白(老油条就没必要掺和了,瞎内卷),想快速提升技术水平不怕吃苦,只是想靠这行混碗饭并不天赋异禀,年轻力壮有冲劲,且你在的公司确实能让你学到一些东西。


有些人可能会说,想多写代码不一定非要加班啊,我完全可以自己整个项目自己写,我的看法是,可以当然可以,但恕我直言,你自己捣鼓的那个东西也就是个 Demo,你不面对复杂多变的需求,没有庞大的用户体量,没有苛刻的产品、测试,你是很难碰到有意义的问题的,毕竟自己写的东西自己怎么看都是对的


也有人会说,公司确实清闲,没啥需要加班的怎么办?好办啊,我从业那么多年,我还就没见过完美的项目,绝大多数情况下都是问题一个接一个的屎山,屎山虽然满是毒,但如果你能摸索着尝试改善屎山,在实践中去思考去总结去行动,对你的提升速度可比光看书强得多得多了。但是,必须要注意,自己折腾可以,千万要注意安全,别把跑得好好的屎山给整塌方了


加班是给自己加的,如果你在加班之后,完全感觉不到自己有什么收获只是想骂公司傻x,那么就没必要加这个班了


多思考


如果你能做到前一条的话,再做这一条就是顺理成章的事情了(除非你懒)


我刚入门编程的时候,完全不理解函数是什么,为什么要抽离函数,我只知道人家都说抽离函数好,所以我就抽离,至于为什么抽离、什么时候抽离、怎么抽离,我一概不知,只是照葫芦画瓢,我没有专门去理解这件事,因为我尝试过但是理解不了(就是笨吧),于是就不管了,但忽然有一天我发现我不知道什么时候已经理解这回事了,想了半天,我只能归结于我写代码写多了,自动理解了


但这不是一蹴而就的,这是一个逐渐积累的过程,可能我在每次封装函数的时候都会思考一点,我为什么封装了这个函数,给我带来了哪些好处,那么当我写得代码足够多,封装的函数足够多之后,我就完全理解了这回事


写代码的时候要思考,为什么我要这么写,还有没有更好的写法?相同的逻辑,别人是那样写的,我是这样写的,差别是什么,各自有什么优缺点?这个代码把我坑惨了,要是下次换我来写,我会怎么设计?


不必专门腾出时间来冥想,这些思考完全可以是零碎的、一闪而过的念头,但必须要有这种习惯,当你写得足够多的时候,这些零散的思考汇聚起来终有一天能给你带来别样的启发


小结


绝大部分人(包括我)所能用上的技术,都谈不上高深,并不需要什么天赋,确实就只是熟能生巧的事情,辛苦一两年,把这些熟练掌握,然后后面才有余力去做技术之外的事情,当然,你也会发现,相比于这些事情,

作者:清夜
来源:juejin.cn/post/7251792725069512763
技术是真的单纯和简单

收起阅读 »

开发这么久,gradle 和 gradlew 啥区别、怎么选?

使用 Gradle 的开发者最常问的问题之一便是: gradle 和 gradlew 的区别? 。 这两个都是应用在特定场景的 Gradle 命令。通过本篇文章你将了解到每个命令干了什么,以及如何在两个命令中做选择。 快速摘要 如果你正在开发的项目当中已经包...
继续阅读 »

使用 Gradle 的开发者最常问的问题之一便是: gradlegradlew 的区别?


这两个都是应用在特定场景的 Gradle 命令。通过本篇文章你将了解到每个命令干了什么,以及如何在两个命令中做选择。



快速摘要


如果你正在开发的项目当中已经包含 gradlew 脚本,安啦,可以一直使用它。没有包含的话,请使用 gradle 命令生成这个脚本。


想知道为什么吗,请继续阅读。



gradle 命令


如果你从 Gradle 官网(gradle.org/releases)下载和安装了 Gradle 的话,你便可以使用安装在 bin 路径下的 gradle 命令了。当然你记得将该 bin 路径添加到设备的 PATH 环境变量中。


此后,在终端上运行 gradle 的话,你会看到如下输出:



你会注意到输出里打印了 Gradle 的版本,它对应着你运行的 gradle 命令在设备中的 Gradle 安装包版本。这听起来有点废话,但在谈论 gradlew 的时候需要明确这点,这很重要。


通过这个本地安装的 Gradle,你可以使用 gradle 命令做很多事情,包括:



  • 使用 gradle init 命令创建一个新的 Gradle 项目或者使用 gradle wrapper 命令创建 gradle wrapper 目录及文件

  • 在一个 Gradle 项目内使用 gradle build 命令进行 Gradle 编译

  • 通过 gradle tasks 命令查看当前的 Gradle 项目中支持哪些 task


上述的命令均使用你本地安装的 Gradle 程序,无论你安装的是什么版本。


如果你使用的是 Windows 设备,那么 gradle 命令等同于 gradle.bat,gradlew 命令等同于 gradlew.bat,非常简单。


gradlew 命令


gradlew 命令,也被了解为 Gradle wrapper,与 gradle 命令相比它是略有不同的。它是一个打包在项目内的脚本,并且它参与版本控制,所以当年复制了某项目将自动获得这个 gradlew 脚本。


“可那又如何?”


好吧,如果你这么想。让我告诉你,它有很多重要的优势。


1. 无需本地安装 gradle


gradlew 脚本不依赖本地的 Gradle 安装。在设备上第一次运行的时候会从网络获取 Gradle 的安装包并缓存下来。这使得任何人、在任何设备上,只要拷贝了这个项目就可以非常简单地开始编译。


2. 配置固定的 gradle 版本


这个 gradlew 脚本和指定的 Gradle 版本进行绑定。这非常有用,因为这意味着项目的管理者可以强制要求该项目编译时应当使用的 Gradle 版本。


Gradle 特性并不总是互相兼容各版本的,所以使用 Gradle wrapper 可以确保项目每次编译都能获得一致性的结果。


当然这需要编译项目的人使用 gradlew 命令,如下是在项目内运行 ./gradlew 的示例:



输出和运行 gradle 命令的结果比较相似。但仔细查看你会发现版本不一样,不是上面的 6.8.2 而是 6.6.1


这个差异说重要也重要,说不重要也不重要。


但当使用 gradlew 的话可以免于担心由于 Gradle 版本导致的不一致性,缘自它可以保证所有的团队成员以及 CI 服务端都会使用相同的 Gradle 版本来构建这个项目。


另外,几乎所有使用 gradle 命令可以做的事情,你也可以使用 gradlew来完成。比如编译一个项目就是 ./gradlew build


如果你愿意的话,可以拷贝 示例项目 并来试一下gradlew


gradle 和 gradlew 对比


至此你应该能看到在项目内使用 gradlew 通常是最佳选择。确保 gradlew 脚本受到版本控制,这样的话你以及其他开发者都可以收获如上章节提到的好处。


但是,难道没有任何情况需要使用 gradle 命令了吗?当然有。如果你期望在一个空目录下搭建一个新的 Gradle 项目,你可以使用 gradle init 来完成。这个命令同样会生成 gradlew 脚本。


(如下的表格简单列出两者如何选)可以说,使用 gradlew 确实是 Gradle 项目的最佳实践。

你想做什么?gradle 还是 gradlew?
编译项目gradlew
测试项目gradlew
项目内执行其他 Gradle taskgradlew
初始化一个 Gradle 项目或者生成 Gradle wrappergradle
作者:TechMerger
链接:https://juejin.cn/post/7144558236643885092
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

放弃熬夜,做清晨的霸主

前言 不知道最近大家有没有在 b 站刷到硬核的HeyMatt老师一个视频,标题叫做放弃熬夜,做清晨的霸主(人生效率的巨变)。 抱着随便看看的心情点了进去后,我沉默了并思考了片刻,我决定按照他视频里的建议和方法尝试一下。 在尝试早起将近一个月的时间后,我发现,...
继续阅读 »

前言



  • 不知道最近大家有没有在 b 站刷到硬核的HeyMatt老师一个视频,标题叫做放弃熬夜,做清晨的霸主(人生效率的巨变)

  • 抱着随便看看的心情点了进去后,我沉默了并思考了片刻,我决定按照他视频里的建议和方法尝试一下。

  • 在尝试早起将近一个月的时间后,我发现,我的效率确实是有了质的提升,接下来我会根据HeyMatt老师提到的方法和我一些实践来进行说明,感兴趣的小伙伴欢迎收藏慢慢看。


🕐 极致利用晚上时间的错觉



  • 会不会有很多小伙伴会有这种情况,每天辛勤劳作后,到了11点半大脑就会提示你:累了一天了,要不要放松一下呢?视频里说到,这种大脑暗示的放松大体分为三种:

    • 开始刷视频,打个游戏,借助浅层的刺激感来放松

    • 点个宵夜,搞个小烧烤吃吃,借助食物换取特定心境

    • 想一些过往能够牵动情绪的往事,沉浸在起伏连绵的情绪中



  • 绝了,以上三种我都尝试过,全中,但是作为程序员我还会有其他的几种:

    • 学习知识📖

    • 优化代码💼

    • 加快需求进度,赶需求🏃



  • 我经常会有这种想法,如果晚上11点半到1点半我可以把这些事情做完或者做多一点,那么我的时间就会被延长🕐。

  • 错❌,看了这个视频后我真的悟了,我花掉了N个晚上的两个小时,但是换不回来人生相应的发展,甚至很多质量很差的决策、代码都是在这个时间段产出的。

  • 可能你确实在这晚上获得了很多愉悦感,但是这个愉悦感是没有办法持续的第二天又赖床又想逃避,你会去想我白白浪费了晚上两个小时刷剧,过了一个晚上这个愉悦感在你早上醒来的时候会忽然转化为你的焦虑感

  • 确实是这样的,特别是在周末熬夜的时候,你会潜意识的特别晚睡,第二天让睡眠拉满,直接到中午才起床,但其实这样不是浪费了更多的时间吗?


🤔 三个风险



  • HeyMatt老师提到在熬夜的这些时间,面临了至少三个风险。


时间的消耗不可控



  • 就拿我来举例,我前段时间老是想着公司需求怎么做,需求的方案是不是不完整,是不是有可以优化的点,要修复的Bug怎么定位,怎么解决。

  • 我不自觉的就会想,噢我晚上把它给搞定,那么第二天就可以放下心去陪家人出去走走。

  • 可是事实呢?运气好一点或许可以在2个小时解决1点准时睡觉,但是运气不好时,时间会损耗越来越多,2个半小时,3个小时,4个小时,随着时间的消逝,问题没有解决就会越发焦虑,不禁查看时间已经凌晨3-4点了。

  • 就更不用说以前大学的时候玩游戏,想着赢一局就睡觉,结果一晚上都没赢过...😓


精神方面的损耗



  • 当我们消耗了晚上睡眠时间来工作、来学习、来游戏,那么代价就是你第二天会翻倍的疲惫。

  • 你会不自觉的想要睡久一点,因为这样才能弥补你精神的损耗,久而久之你就会养成晚睡晚起的习惯,试问一下自己有多久没有在周末看过清晨的阳光了?

  • 再说回我,当我前一个晚上没有解决问题带着焦虑躺在床上时,我脑子会不自觉全是需求、Bug,这真的不夸张,我真的睡着了都会梦到我在敲代码。这其实就是一种极度焦虑而缺乏休息的大脑能干出来的事情。

  • 我第二天闹钟响了想起我还有事情没做完,就会强迫自己起床,让自己跟**“想休息的大脑”**打架,久而久之这危害可想而知。


健康维度的损耗



  • 随着熬夜次数的增多,年龄的增长,很多可见或不可见的身体预警就会越来越多,具体有什么危害,去问AI吧,它是懂熬夜的。



🔥 做清晨的霸主



  • 那么怎么解决这些问题呢,其实很简单,把晚上11.30后熬夜的时间同等转化到早上即可,比如11.30-1.30,那么就转化到6.30-8.30,这时候就会有同学问了:哎呀小卢,你说的这么简单,就是起不来呀!!

  • 别急,我们都是程序员,最喜欢讲原理了,HeyMatt老师也将原理告诉了我们。


赖床原理



  • 其实我们赖床起不来的很大一部分原因是自己想太多了。

  • 闹钟一响,你会情不自禁去思考,“我真的要现在起床吗?” “我真的需要这一份需要早起的工作吗?” “我起床之后我需要干什么?” “这么起来会不会很累,要不还是再睡一会,反正今天不用上班?”

  • 这时候咱们大脑就处于一种**“睡眠”“清醒”**的重叠状态,就跟叠buffer一样,大脑没有明确的收到指令是要起床还是继续睡。

  • 当我们想得越多,意识就变得越模糊,但是大脑不愿意去思考,大脑无法清晰地识别并执行指令,导致我们又重新躺下了。


练就早起



  • 在一次采访中,美国作家 Jocko Willink 老师提出了一种早起方法::闹钟一响,你的大脑什么都不要想,也不需要去想,更不用去思考,让大脑一片空白,你只需执行动作即可。

  • 而这个动作其实特别简单,就是坐起来--->站起来--->去洗漱,什么都不用想,只用去做就好。

  • 抱着试一试的心态,我尝试了一下这种方法,并在第二天调整了闹钟到 6:30。第二天闹钟一响,直接走进卫生间刷个牙洗个脸,瞬间清醒了,而且我深刻的感觉到我的专注力精神力有着极大的提升,大脑天然的认为现在是正常起床,你是需要去工作和学习👍。

  • 绝了,这个方法真的很牛*,这种方法非常有效,让我觉得起床变得更容易了,推荐大家都去试试,你会回来点赞的。


克服痛苦



  • 是的没错,上面这种办法是会给人带来痛苦的,在起床的那一瞬间你会感觉仿佛整个房间的温度都骤降了下来,然后,你使劲从被窝里钻出来,脚底下着地的瞬间,你感到冰凉刺骨,就像是被一桶冰水泼醒一样。你感到全身的毛孔都瞬间闭合,肌肉僵硬,瑟瑟发抖,好像一股冰冷的气流刺痛着你的皮肤。

  • 但是这种痛苦是锐减的,在三分钟之后你的痛苦指数会从100%锐减到2%

  • 带着这种征服痛苦的快感,会更容易进入清晨的这两小时的写作和工作中。


✌️ 我得到了什么



  • 那么早起后,我收获了什么呢❓❓


更高效的工作时间



  • 早起可以让我在开始工作前有更多的时间来做自己想做的事情,比如锻炼、读书、学习新技能或者提升自己的专业知识等,这些事情可以提高我的效率专注力,让我在工作时间更加高效。

  • 早起可以让我更容易集中精力,因为此时还没有太多事情干扰我的注意力。这意味着我可以更快地完成任务,更少地分心更少地出错


更清晰的思维



  • 早上大脑比较清醒,思维更加清晰,这有助于我更好地思考解决问题,我不用担心我在早上写的需求方案是否模糊,也能更好的做一些决策

  • 此外,早起还可以让我避免上班前匆忙赶路的情况,减少心理上的紧张压力


更多可支配的时间



  • 早起了意味着早上两个最清醒的时间随便我来支配,我可以用半小时运动,再用10分钟喝个咖啡,然后可以做我喜欢做的事情。

  • 可以用来写代码,可以用来写文章,也可以用来运营个人账号

  • 可以让我有更多的时间规划安排工作,制定更好的工作计划时间管理策略,从而提高工作效率减少压力


更好的身体健康



  • 空腹运动对我来说是必须要坚持的一件事情,早起可以让我有更多的时间来锻炼身体,这对程序员来说非常重要,因为长时间的坐着工作容易导致身体不健康

  • 用来爬楼,用来跑步,用来健身环等等等等,随便我支配,根本不用担心下班完了后缺乏运动量。


👋 写在最后



  • 我相信,我坚持了一年后,我绝对可以成为清晨的霸主,你当然也可以。

  • 而且通过早起不思考这个方法,很多在生活有关于拖延的问题都可以用同样的方式解决,学会克服拖延直接去做,在之后就会庆幸自己做出了正确的决定

  • 如果您觉得这篇文章有帮助到您的的话不妨🍉🍉关注+点赞+收藏+评论+转发🍉🍉支持一下哟~~😛您的支持就是我更新的最大动力。

  • 如果想跟我一起讨论和学习更多的前端知识可以加入我的前端交流学习群,大家一起畅谈天下~~~

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

什么是 HTTP 长轮询?

什么是 HTTP 长轮询? Web 应用程序最初是围绕客户端/服务器模型开发的,其中 Web 客户端始终是事务的发起者,向服务器请求数据。因此,没有任何机制可以让服务器在没有客户端先发出请求的情况下独立地向客户端发送或推送数据。 为了克服这个缺陷,Web 应用...
继续阅读 »

什么是 HTTP 长轮询?


Web 应用程序最初是围绕客户端/服务器模型开发的,其中 Web 客户端始终是事务的发起者,向服务器请求数据。因此,没有任何机制可以让服务器在没有客户端先发出请求的情况下独立地向客户端发送或推送数据。


为了克服这个缺陷,Web 应用程序开发人员可以实施一种称为 HTTP长轮询的技术,其中客户端轮询服务器以请求新信息。服务器保持请求打开,直到有新数据可用。一旦可用,服务器就会响应并发送新信息。客户端收到新信息后,立即发送另一个请求,重复上述操作。


什么是 HTTP 长轮询?


那么,什么是长轮询?HTTP 长轮询是标准轮询的一种变体,它模拟服务器有效地将消息推送到客户端(或浏览器)。


长轮询是最早开发的允许服务器将数据“推送”到客户端的技术之一,并且由于其寿命长,它在所有浏览器和 Web 技术中几乎无处不在。即使在一个专门为持久双向通信设计的协议(例如 WebSockets)的时代,长轮询的能力仍然作为一种无处不在的回退机制占有一席之地。


HTTP 长轮询如何工作?


要了解长轮询,首先要考虑使用 HTTP 的标准轮询。


“标准”HTTP 轮询


HTTP 轮询由客户端(例如 Web 浏览器)组成,不断向服务器请求更新。


一个用例是想要关注快速发展的新闻报道的用户。在用户的浏览器中,他们已经加载了网页,并希望该网页随着新闻报道的展开而更新。实现这一点的一种方法是浏览器反复询问新闻服务器“内容是否有任何更新”,然后服务器将以更新作为响应,或者如果没有更新则给出空响应。浏览器请求更新的速率决定了新闻页面更新的频率——更新之间的时间过长意味着重要的更新被延迟。更新之间的时间太短意味着会有很多“无更新”响应,从而导致资源浪费和效率低下。


HTTP 轮询


上图:Web 浏览器和服务器之间的 HTTP 轮询。服务器向立即响应的服务器发出重复请求。


这种“标准”HTTP 轮询有缺点:



  • 更新请求之间没有完美的时间间隔。请求总是要么太频繁(效率低下)要么太慢(更新时间比要求的要长)。

  • 随着规模的扩大和客户端数量的增加,对服务器的请求数量也会增加。由于资源被无目的使用,这可能会变得低效和浪费。


HTTP 长轮询解决了使用 HTTP 进行轮询的缺点



  1. 请求从浏览器发送到服务器,就像以前一样

  2. 服务器不会关闭连接,而是保持连接打开,直到有数据供服务器发送

  3. 客户端等待服务器的响应。

  4. 当数据可用时,服务器将其发送给客户端

  5. 客户端立即向服务器发出另一个 HTTP 长轮询请求


HTTP 长轮询


上图:客户端和服务器之间的 HTTP 长轮询。请注意,请求和响应之间有很长的时间,因为服务器会等待直到有数据要发送。


这比常规轮询更有效率。



  • 浏览器将始终在可用时接收最新更新

  • 服务器不会被永远无法满足的请求所搞垮。


长轮询有多长时间?


在现实世界中,任何与服务器的客户端连接最终都会超时。服务器在响应之前保持连接打开的时间取决于几个因素:服务器协议实现、服务器体系结构、客户端标头和实现(特别是 HTTP Keep-Alive 标头)以及用于启动的任何库并保持连接。


当然,许多外部因素也会影响连接,例如,移动浏览器在 WiFi 和蜂窝连接之间切换时更有可能暂时断开连接。


通常,除非您可以控制整个架构堆栈,否则没有单一的轮询持续时间。


使用长轮询时的注意事项


在您的应用程序中使用 HTTP 长轮询构建实时交互时,需要考虑几件事情,无论是在开发方面还是在操作/扩展方面。



  • 随着使用量的增长,您将如何编排实时后端?

  • 当移动设备在WiFi和蜂窝网络之间快速切换或失去连接,IP地址发生变化时,长轮询会自动重新建立连接吗?

  • 通过长轮询,您能否管理消息队列并如何处理丢失的消息?

  • 长轮询是否提供跨多个服务器的负载平衡或故障转移支持?


在为服务器推送构建具有 HTTP 长轮询的实时应用程序时,您必须开发自己的通信管理系统。这意味着您将负责更新、维护和扩展您的后端基础设施。


服务器性能和扩展


使用您的解决方案的每个客户端将至少每 5 分钟启动一次与您的服务器的连接,并且您的服务器将需要分配资源来管理该连接,直到它准备好满足客户端的请求。一旦完成,客户端将立即重新启动连接,这意味着实际上,服务器将需要能够永久分配其资源的一部分来为该客户端提供服务。当您的解决方案超出单个服务器的能力并且引入负载平衡时,您需要考虑会话状态——如何在服务器之间共享客户端状态?您如何应对连接不同 IP 地址的移动客户端?您如何处理潜在的拒绝服务攻击?


这些扩展挑战都不是 HTTP 长轮询独有的,但协议的设计可能会加剧这些挑战——例如,您如何区分多个客户端发出多个真正的连续请求和拒绝服务攻击?


消息排序和排队


在服务器向客户端发送数据和客户端发起轮询请求之间总会有一小段时间,数据可能会丢失。


服务器在此期间要发送给客户端的任何数据都需要缓存起来,并在下一次请求时传递给客户端。


HTTP 长轮询 MQ


然后出现几个明显的问题:



  • 服务器应该将数据缓存或排队多长时间?

  • 应该如何处理失败的客户端连接?

  • 服务器如何知道同一个客户端正在重新连接,而不是新客户端?

  • 如果重新连接花费了很长时间,客户端如何请求落在缓存窗口之外的数据?


所有这些问题都需要 HTTP 长轮询解决方案来回答。


设备和网络支持


如前所述,由于 HTTP 长轮询已经存在了很长时间,它在浏览器、服务器和其他网络基础设施(交换机、路由器、代理、防火墙)中几乎得到了无处不在的支持。这种级别的支持意味着长轮询是一种很好的后备机制,即使对于依赖更现代协议(如 WebSockets )的解决方案也是如此。


众所周知,WebSocket 实现,尤其是早期实现,在双重 NAT 和某些 HTTP 长轮询运行良好的代理环境中挣扎。


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

世界那么大,我并不想去看看

回家自由职业有一个半月了,这段时间确实过的挺开心的。 虽然收入不稳定,但小册和公众号广告的收入也足够生活。 我算过,在小县城的家里,早饭 10 元,午饭 60元(按照偶尔改善生活,买份酸菜鱼来算),晚饭 10 元,牛奶零食等 20 元,一天是 100 元。 一...
继续阅读 »

回家自由职业有一个半月了,这段时间确实过的挺开心的。


虽然收入不稳定,但小册和公众号广告的收入也足够生活。


我算过,在小县城的家里,早饭 10 元,午饭 60元(按照偶尔改善生活,买份酸菜鱼来算),晚饭 10 元,牛奶零食等 20 元,一天是 100 元。


一年就是 36500 元。


加上其他的支出,一年生活费差不多 5w 元。


10 年是 50w,40 年就是 200 万元。


而且还有利息,也就是说,我这辈子有 200 万就能正常活到去世了。


我不会结婚,因为我喜欢一个人的状态,看小说、打游戏、自己想一些事情,不用迁就另一个人的感受,可以完全按照自己的想法来生活。


并不是说我现在有 200 万了,我的意思是只要在二三十年内赚到这些就够了。


我在大城市打工,可能一年能赚 50 万,在家里自由职业估计一年也就 20 万,但这也足够了。


而且自由职业能赚到最宝贵的东西:时间。


一方面是我自己的时间,我早上可以晚点起、晚上可以晚点睡、下午困了可以睡个午觉,可以写会东西打一会游戏,不想干的时候可以休息几天。


而且我有大把的时间可以来做自己想做的事情,创造自己的东西。


一方面是陪家人的时间,自从长大之后,明显感觉回家时间很少了,每年和爸妈也就见几天。


前段时间我爸去世,我才发觉我和他最多的回忆还是在小时候在家里住的时候。


我回家这段时间,每天都陪着我妈,一起做饭、吃饭,一起看电视,一起散步,一起经历各种事情。


我买了个投影仪,很好用:



这段时间我们看完了《皓镧传》、《锦绣未央》、《我的前半生》等电视剧,不得不说,和家人一起看电视剧确实很快乐、很上瘾。


再就是我还养了只猫,猫的寿命就十几年,彼此陪伴的时间多一点挺好的:



这些时间值多少钱?没法比较。


回家这段时间我可能接广告多了一点,因为接一个广告能够我好多天的生活费呢。


大家不要太排斥这个,可以忽略。


其实我每次发广告总感觉对不起关注我的人,因为那些广告标题都要求起那种博人眼球的,不让改,就很难受。



小册的话最近在写 Nest.js 的,但不只是 nest。


就像学 java,我们除了学 spring 之外,还要学 mysql、redis、mongodb、rabbitmq、kafka、elasticsearch 等中间件,还有 docker、docker compose、k8s 等容器技术。


学任何别的后端语言或框架,也是这一套,Nest.js 当然也是。


所以我会在 Nest.js 小册里把各种后端中间件都讲一遍,然后会做 5 个全栈项目。


写完估计得 200 节,大概会占据我半年的时间。


这段时间也经历过不爽的事情:





这套房子是我爸还在的时候,他看邻居在青岛买的房子一周涨几十多万,而且我也提到过可能回青岛工作,然后他就非让我妈去买一套。


当时 18 年青岛限购,而即墨刚撤市划区并入青岛,不限购,于是正好赶上房价最高峰买的。


然而后来并没有去住。


这套房子亏了其实不止 100 万。


因为银行定存利息差不多 4%,200 万就是每年 8万利息,5年就是 40万。


但我也看开了,少一百万多一百万对我影响大么?


并不大,我还是每天花那些钱。


相比之下,我爸的去世对我的打击更大,这对我的影响远远大于 100 万。


我对钱没有太大的追求,对很多别人看重的东西也没啥追求。


可能有的人有了钱,有了时间会选择环游中国,环游世界,我想我不会。


我就喜欢宅在家里,写写东西、看看小说、打打游戏,这样活到去世我也不会有遗憾。


我所追求的事情,在我小时候可能是想学医,一直觉得像火影里的纲手那样很酷,或者像大蛇丸那样研究一些东西也很酷。


但近些年了解了学医其实也是按照固定的方法来治病,可能也是同样的东西重复好多年,并不是我想的那样。


人这一辈子能把一件事做好就行。


也就是要找到自己一生的使命,我觉得我找到了:我想写一辈子的技术文章。


据说最高级的快乐有三种来源:自律、爱、创造。


写文章对我来说就很快乐,我想他就是属于创造的那种快乐。


此外,我还想把我的故事写下来,我成长的故事,我和东东的故事,那些或快乐或灰暗的日子,今生我一定会把它们写下来,只是现在时机还不成熟。


世界那么大,我并不想去看看。


我只想安居一隅,照顾好家人,写一辈子的技术文章,也写下自己的故事。


这就是我的平凡之路。


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

两年前进入前端,回顾一下,两年四家公司的经历!!!

前言 在2020年进入前端行业,12月份来到上海,专科,21岁,包装2年经验!!! 正文 刚来上海第一份简历,没有任何人指导,写出了一份这样的简历,现在看来说好听点是青涩,在不好听点就是一眼假,在不好听点,懂得都懂!!! 故事开始了,我拿着这样一份简历在现...
继续阅读 »

前言


在2020年进入前端行业,12月份来到上海,专科,21岁,包装2年经验!!!

正文


image.png



  • 刚来上海第一份简历,没有任何人指导,写出了一份这样的简历,现在看来说好听点是青涩,在不好听点就是一眼假,在不好听点,懂得都懂!!!

  • 故事开始了,我拿着这样一份简历在现在的boss上投递,当时也不知道为什么就敢来上海这种一线城市闯荡,可能是对大城市的向往吧,以为来到大城市就会有所成就(农村的)。说的跑偏了。我在各个区之间奔跑,拿着打印的简历(以前不知道公司会打印,都是自己拿着打印的简历,还是彩印~贵)游走于各个公司,当时也是12月份了,很多公司只是刷KPI,大多时候都会白跑一趟,两个小时的地铁,甚至都见不到技术人员,各种理由 技术出差,技术在开会,公司招到人,又加上自己紧张,就导致面试一直不理想,经常就是十几二十分钟结束。持续了两个星期左右,终于收到了我的第一份Offer,薪资12k,当时觉得给的好高,是一家小公司就我一个前端,当然肯定有一个大佬,因为我没去之前端都是他做的,用的框架是uni,当时只是知道有uni这个东西,当时面试时候也只是说自己不会uni只是了解,但是愿意带我,我就以为会很好,就立马定下来了,于是就租房入职,当时住的是公寓静安区的,老贵了2400,特别小几平米之独卫,只放的下床大小(棺材房)。




第一份工作


image.png



入职后 小小的紧张 当然还有对未来的向往




  • 一些基础的办理入职的流程

  • 熟悉一周代码流程,适应公司环境 (没有公司照片了要不然就上图了)

  • 认识公司员工


当时觉得第一份工作,要赶紧学习,加上uni不会,就想赶紧赶上,于是在第一周里我就把uni框架,还有公司现有的项目摸清楚,然后当时满脑子都是会不会自己表现不好然后过不了试用期,哈哈,确实很让人紧张,于是就在紧张与忙碌学习中很快就到了发工资的日子

附上图db7b0fee2d78ac540501969701147e2.jpg


来上海的第一份工作,第一次发工资,当时都是欣喜和自豪感,甚至还给父母打电话炫耀了好一会(写到这还真是让人怀念不已),于是就这样坚持下去了,第二个月就提前转正了,因为我的leader觉得我还行,学习和适应能力都不错,就给我提前转了正,当时觉得真不错,就决定在这好好干,可是随着时间的变化,因为是小公司不卷也不加班,产品进入了维护期,加上自己完全没有方向,心也慢慢变得平淡,感觉好像掉入了舒适圈。于是当时就产生了可能要离开的想法,但是还是想着先在这个公司做够一年,毕竟我的上司和老板,对我都很好。可是9月份,公司突然决定转型,是不需要开发组了,当时我就决定离开了,于是在10月份放完假后,我就当时提了离职,当然也没有什么交接,我就离开了,于是开始了再次找工作的道路。还是跟刚来上海一样,跑着找工作,也不知道什么是好公司,(当时就已经意识到学历不行了),当然21年10月份,工作机会还是比较多的,所以很快也就很快找到了工作,很开心,因为没工作会让我很焦虑。


第二份工作


image.png
可能是我比较幸运,第二份工作的薪资就到达了14k,但是我很明白,我能力的配不上这份薪资。


第一天入职就领到自己的 macmin,加两个显示屏,当然还有自己的吊牌,虽然没咋带过

附上图


image.png
image.png

公司环境也很好,加上是 ios 安卓 小程序 三端同步开发的(人员情况ios2人,安卓1个,4个前端,4个后端,4个UI,产品和带头的技术就不说了,没有运维,运维后端负责了),我就觉得再累再苦我都坚持下来多学点东西加上公司里都是年轻人(哦,年轻人是指没有40岁以上的),没什么公司沟通成本,而且都很乐意教你点东西。加班情况 肯定是有的 双休基本上是很少,基本都是大小周,一个星期5天加3天班,每次都9点左,右不细说,还好。于是就在这个公司里疯狂学习,学习处理问题,学习产品思维,学习问题分析,学着沟通,每天基本上都算是有收获的一天,代码质量也慢慢变得更加规范,有了更加系统化的进步,当时完全没有对未来的迷茫,身边的人都比我优秀,只有学习才能追的上别人,当时我其实就已经意识到我是一个自己很难独立学习的人,我是一个被生活推着走的人,只有用到才会学到。当然这种环境有好有坏,好的是你可以学习进步,每天都会有收获,不好的是,自卑会充斥着你(这个自卑跟自己的生活有关,小时候没有很好的环境,突然到这种身边人都很优秀对于钱的概念跟你完全不同时,就会发酵的特别快)。所以当时的我就很自卑,但是我也有在适应这种状态。


可是这份工作也是在刚过完年,就失去了,突然公司经营状况不是很好,因为上海这边是研发部的,所以就当时是说让开发部跟运营部合并,可是运营部不在上海,就问我们开发部的谁愿意去合肥,肯定是没有同意,但是当时我是动摇了,因为合肥离我家比较近,家是河南的,当时通知是说,直接到合肥那边报道不用来上海,可是大家都说其实去了也不一定好,而且还要降薪(因为消费水平变了就会降薪)。所以大家都没去。然后就是赔偿问题了,大家都是说N+1,结果公司不愿意乱七八糟的,还去仲裁了,也是我第一次经历,我也不是很懂,就随波逐流,最后也没仲裁上,因为太浪费时间了,加上有人来调节到最后也是赔了半个月工资加点加班费啥的(还是呼吁大家如果真的不合理不要怕麻烦,去保护自己的合法效益,这是你的权力,我是刚转正半年还不到所以就没争啥)。


于是就开始了第三份的找工作之旅,真是颠簸,当时2022年3月份,疫情还没开始爆发,面试机会也还不错,所以也是10天之内找到了工作待遇也跟上家差不多,但是规模就小了很多,首先人数没那么多,但是是个老公司了6年历史了,入职一天我就走了,难顶,因为看到了屎山,哈哈,然后让我接着维护和修正(程序员看见这种东西我想大家都会很难受,特别是在你手上)。


别问为啥换格式了,因为下面的经历跟看小说一样,tmd



于是我就开始了重新奔波,我还没奔波起来,刚准备重新投递简历,第一个星期就说疫情来了,一下子面试机会都少了很多,甚至有的公司已经有小阳人了,开始封街道,封区,然后当时我就想能封多久啊,顶多一个月吧,于是我也暂停了简历的投递,等风波过去在找,结果一封就是3个月啊,三个月,第一个月觉得快了快了,当时还会复习前端知识,第二个月,第三个月直接躺平,每天睡不着,白天吃了睡,日夜颠倒,害,跟做梦一样,当然也没有完全躺平,做了做小区志愿者,就发发物资,守守楼,真是不是因为这件事,我觉得我都废了。在疫情一过我就立马逃离上海回老家,结果老家又封了半个月,于是上半年就过去了,8月份去杭州找工作了,因为不想在上海了,太难受了。结果在杭州半个月找到工作。



image.png


第三份工作


image.png


当时找工作已经很难找到了,因为我入职后发现一摞打印出来的前端岗位的简历,(薪资其实是有涨的13k13薪)也是小公司来的,但不是我一个前端,我当时感觉进去还挺离谱的,因为觉得自己能从这么多人中杀出来,真是幸运。然后更离谱的事情发生了,9月中旬星期四下班之前,突然叫我和另一个后端(其实进去之前我就觉得是要开我们了,因为项目大体开发完了,后续就是迭代跟维护),说公司转型,去拍抖音啥了,为啥就你们俩个开发走,是因为剩下的开发,要把项目结尾一下,还说我们技术没问题,很高效啥的,我当时心累的不行,就说好,就走了。然后过了1个月,测试给我发信息说,公司又来业务了招人呢,招一个前端跟一个后端,我当时想着 真牛啊,真是节省成本的大户,然后后续这个公司就是开人,跟招人的业务了。。。此处省略一万字。。。


再次踏入找工作的征途


10月份的行情


10月份中旬开始投递简历,这时候我就已经见识了什么叫已读不回,就是完全没机会,有机会的面试哪怕你回答的不错,但也会有更好的,因为我去面试的时候完全见到了竞争力,就是全都是来面前端的,岗位只有一个,但是面试的人却滔滔不绝,这时候我就知道你没有学历,那么你连门槛都没有,于是在杭州呆了一个月,就回家了,其实我已经承受不住,那种投了没人回复,无能为力的焦虑感了,每天都很焦虑,也碰到了现在的孔乙己文学,父母让我随便找个工作干着,我却觉得那我学这么长时间的前端干嘛,反正就是各种焦虑,最后也是早早回家过年。


第四份工作


第四份就是2023年年后入职的,目前在职工作,在上海,工作内容就是:后台,跟大屏可视化,目前还没有什么奇怪的事,薪资待遇没以前好了,然后offer也没发,直接入职了,害,主要是不想在漫无目的的游荡。


总结


我是个普通人,一个跟大多数前端或者从事开发一样的普通人,没有年包,没有朝九晚五,没有双休,也不是天赋出众,就是简简单单的普通人,可是某些时候就是想小有坚持,现在的我依然迷茫,在前端这个行业里迷茫,没有方向,不会学习,什么都不精通,只会写业务的工作,看着前端已死的文章,给自己的以后添加更多的焦虑,知道发出来以后看到的会批评我,会说我自己不努力怪谁,确实谁也没怪,只是发出来自己的这两年,确实错了,今年23岁,却也被这两年磨平了23岁的朝气,想了想刚来上海的意气风发,到现在说不出的感觉,总觉失落,却又无可奈何,想听听大家的想法,也想得到大家批评。如果可以也想听听大佬们的经历,每一条评论都会认真回。
以后会更新点技术,但不是大佬那样,那么细就可能我自己用到了记录一下。


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

天涯论坛倒闭,我给天涯续一秒

时代抛弃你,连句招呼都不会打 "时代抛弃你,甚至连句招呼都不会打",成立差不多23年,承载无数人青春回忆的社区就这样悄无声息的落幕了。在那个没有微博抖音的年代,天涯可谓是神一般的存在,当时还没有网络实名制,因此内容包罗万象五花八门,各路大神层出不穷,这里有:盗...
继续阅读 »

时代抛弃你,连句招呼都不会打


"时代抛弃你,甚至连句招呼都不会打",成立差不多23年,承载无数人青春回忆的社区就这样悄无声息的落幕了。在那个没有微博抖音的年代,天涯可谓是神一般的存在,当时还没有网络实名制,因此内容包罗万象五花八门,各路大神层出不穷,这里有:盗走麻花腾qq的黑客大神、高深莫测的民生探讨、波诡云谲的国际形势分析、最前沿最野的明星八卦、惊悚刺激的怪力乱神、脑洞大开的奇人异事 等等,让人眼花缭乱。甚至还有教你在家里养一只活生生的灵宠(见下文玄学类) image.png 今年4月初,天涯官微发布公告,因技术升级和数据重构,暂时无法访问。可直到现在,网站还是打不开。虽然后来,官微略带戏谑和无奈地表示:“我会回来的”。但其糟糕的财务状况预示着,这次很可能真是,咫尺天涯,永不再见了。 image.png


神奇的天涯


当时还在读大一时候就接触到了 天涯,还记得特别喜欢逛的板块是 "莲蓬鬼话"、"天涯国际"。莲蓬鬼话老用户都知道,主要是一些真真假假的怪力乱神的惊险刺激的事情,比如 有名的双鱼玉佩,还有一些擦边的玩意,比如《风雪漫千山人之大欲》,懂得都懂,这些都在 pdf里面自取😁;天涯国际主要是各路大佬分析国际局势,每每看完总有种感觉 "原来在下一盘大棋",还有各种人生经验 比如kk大神对房产的预测,现在看到貌似还是挺准的。还有教你在家里养一只活生生的灵宠,神奇吧。 总共200+篇,这里先做下简单介绍


image.png



关注公众号,回复 「天涯」 海量社区经典文章双手奉上,感受一下昔日论坛的繁华



历史人文类


功底深厚,博古通今,引人入胜,实打实的的拓宽的你的知识面



  • (长篇)女性秘史◆那些风华绝代、风情万种的女人,为你打开女人的所有秘密.pdf

  • 办公室实用暴力美学——用《资治通鉴》的智慧打造职场金饭碗.pdf

  • 《二战秘史》——纵论二战全史——邀你一起与真相贴身肉搏.pdf

  • 不被理解的mzd(密码是123).zip

  • 地缘看世界——欧洲部分-温骏轩.pdf

  • 宝钗比黛玉大八岁!重解红楼全部诗词!血泪文字逐段解释!所有谜团完整公开!.pdf

  • 现代金融经济的眼重看历史-谁是谁非任评说.pdf

  • 蒋介石为什么失掉大陆:1945——1949-flp713.pdf


人生箴言类


开挂一般的人生,有的应该是体制内大佬闲来灌水,那时上网还无需实名



  • 职业如何规划?大城市,小城市,如何抉择?我来说说我的个人经历和思考-鸟树下睡懒觉的猪.pdf

  • kk所有内容合集(506页).pdf

  • 一个潜水多年的体制内的生意人来实际谈谈老百姓该怎么办?.pdf

  • 三年挣850万,你也可以复制!现在新书已出版,书名《我把一切告诉你》.pdf

  • 互联网“裁员”大潮将起:离开的不只是马云 可能还有你.pdf

  • 大鹏金翅明王合集.pdf

  • 解密社会中升官发财的经济学规律-屠龙有术.pdf


房产金融


上帝视角,感觉有的可能是参与制定的人



  • 从身边最简单的经济现象判断房价走势-招招是道.pdf

  • 大道至简,金融战并不复杂。道理和在县城开一个赌场一样。容我慢慢道来-战略定力.pdf

  • 沉浮房产十余载,谈房市心得.pdf

  • 现代金融的本质以及房价-curgdbd.pdf

  • 对当前房地产形势的判断和对一些现象的解释-loujinjing.pdf

  • 中国VS美国:决定世界命运的博弈 -不要二分法 .pdf

  • 大江论经济-大江宁静.pdf

  • 形势转变中,未来哪些行业有前景.pdf

  • 把握经济大势和个人财运必须读懂钱-现代金钱的魔幻之力.pdf

  • 烽烟四起,中美对决.pdf

  • 赚未来十年的钱-王海滨.pdf

  • 一个炒房人的终极预测——调控将撤底失败.pdf


故事连载小说类


小说爱好者的天堂,精彩绝伦不容错过



  • 人之大欲,那些房中术-风雪漫千山.pdf

  • 冒死记录中国神秘事件(真全本).pdf 五星推荐非常精彩

  • 六相十年浩劫中的灵异往事,颍水尸媾,太湖獭淫,开封鬼谷,山东杀坑-御风楼主人.pdf

  • 《内参记者》一名“非传统”记者颠覆你三观的采访实录-有骨难画.pdf

  • 中国式骗局大全-我是骗子他祖宗.pdf

  • 我是一名警察,说说我多年来破案遇到的灵异事件.pdf

  • 一个十年检察官所经历的无数奇葩案件.pdf

  • 宜昌鬼事 (三峡地区巫鬼轶事记录整理).pdf

  • 南韩往事——华人黑帮回忆录.pdf

  • 惊悚灵异《青囊尸衣》(斑竹推荐)-鲁班尺.pdf

  • 李幺傻江湖故事之《戚绝书》(那些湮没在岁月深处的江湖往事)-我是骗子他祖宗.pdf

  • 闲来8一下自己幽暗的成长经历-风雪漫千山.pdf

  • 阴阳眼(1976年江汉轶事).pdf

  • 民调局异闻录-儿东水寿.pdf

  • 我当道士那些年.pdf

  • 目睹殡仪馆之奇闻怪事.pdf


玄学类


怪力乱神,玄之又玄,虽然已经要求建国后不许成精了



  • 请块所谓的“开光”玉,不如养活的灵宠!.pdf

  • 写在脸上的风水-禅海商道.pdf

  • 谶纬、民谣、推背图-大江宁静.pdf

  • 拨开迷雾看未来.pdf

  • 改过命的玄教弟子帮你断别你的网名吉凶-大雨小水.pdf


天涯的败落


内容社区赚钱,首先还是得有人气,这是互联网商业模式的基础。天涯在PC互联网时代,依靠第一节说的几点因素,持续快速的吸引到用户,互联网热潮,吸引了大量的资本进入,作为有超高流量的天涯社区,自然也获得了资本的青睐。营收这块,主要分为两个部分:网络广告营销业务和互联网增值业务收入。广告的话,最大的广告主是百度,百度在2015年前5个月为天涯社区贡献了476万元,占总收入的比重达11.24%;百度在2014年为天涯社区贡献收入1328万元,占比12.76%。广告收入严重依赖于流量,天涯为了获得广告营收,大幅在社区内植入广告位,影响了用户体验,很有竭泽而渔的感觉。 但是在进入移动互联网时代没跟上时代步伐, image.png 2010年底,智能手机的出货量开始超过PC,另外,移动互联网走的是深度垂直创新,天涯还是大而全的综合社区模式,加上运营也不是很高明,一两个没工资的版主,肯定打不过别人公司化的运作,可以看到在细分领域被逐步蚕食:



  • 新闻娱乐,被**「微博、抖音」**抢走;

  • 职场天地,被**「Boss直聘」**抢走;

  • 跳蚤市场,被**「闲鱼、转转」**抢走;

  • 音乐交友,被**「网易云、qq音乐」**抢走;

  • 女性兴趣,被**「小红书」**抢走,等等


强如百度在移动互联网没占到优势,一直蛰伏到现在,在BAT中名存实亡,何况天涯,所以也能理解吧。"海内存知己,天涯若比邻",来到2023年,恐怕只剩物是人非,变成一个被遗忘的角落,一段被尘封的回忆罢了,期待天涯能够度过难关再度重来吧。


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

一次微前端的改造记录

web
前言 由于公司的一些需求,需要去了解 iframe 和 qiankun 两种微前端方案,特此记录一下。 微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独...
继续阅读 »

前言


由于公司的一些需求,需要去了解 iframe 和 qiankun 两种微前端方案,特此记录一下。


微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行、独立开发、独立部署。


iframe


HTML 内联框架元素,能够将另一个 HTML 页面嵌入到当前页面


<iframe src="文件路径"></iframe>

postMessage




  • window.postMessage() 方法可以安全地实现跨源通信:postMessage 讲解




  • 通常来说,对于两个不同页面的脚本,只有当他们页面位于具有相同的协议(通常为 https),端口号(443 为 https 的默认值),以及主机时,这两个脚本才能互相通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全


    otherWindow.postMessage(message, targetOrigin, [transfer]);



  • postMessage 的兼容性




实现思路


整体架构


以我们公司的真实项目为例:


1687167610369.png


一个父站点,很多子站点,不同的子站点即为完全独立的不同的项目
父站点包括:



  1. 公共部分:header 部分、左侧菜单部分都是公共的组件

  2. 主区域部分(子站点区域):需要展示不同子站点的业务部分


父子站点通信(如何展示不同的站点页面)


上面已经介绍过 iframe 和 postMessage:我们通过 iframe 去加载不同项目的线上地址



  1. 我们新建一个通用组件,无论菜单路由指向何处都指向这个组件,渲染这个组件

  2. 在这个组件中监听路由的变化,返回不同的线上地址,让 iframe 去加载对应的内容(公司项目比较老,还是使用的 vue2)


<template>
<div class="container">
<iframe :src="src"></iframe>
</div>
</template>

<script>
export default {
data() {
return {
src: '',
};
},
mounted() {
this.updateIframe(); // 生命周期加载一次,否则页面空白,第一次监听不到
},
watch: {
$route() {
this.updateIframe();
},
},
methods: {
updateIframe() {
// 更新 src
},
},
};
</script>


  1. 菜单以及子站点线上地址怎么来:目前我们的做法是单独的菜单配置,通过接口去拿,配置菜单的时候同事配置好 iframe 线上地址,这样就可以一起拿到了
    image.png

  2. 那我们究竟如何通信呢?
    父站点(补充上面的代码):


<template>
<div class="container">
<iframe :src="src" id="iframe"></iframe>
</div>
</template>
<script>
export default {
data() {
return {
src: '',
};
},
mounted() {
this.getMessage();
this.updateIframe(); // 生命周期加载一次,否则页面空白,第一次监听不到,同样的也可以使用 iframe 的 onload 方法
},
watch: {
$route() {
this.updateIframe();
},
},
methods: {
updateIframe() {
// 更新 src
},
messageCallBack(data) {
// 这边可以接收一些子站点的数据,去做一些逻辑判断,比如要在iframe加载完之后,父站点再去发消息给子站点,不然肯定存在问题
// 可以传递一些信息给子站点
this.postMessage({
data: {},
});
},
postMessage(data) {
document.getElementById('iframe').contentWindow.postMessage(JSON.stringify(data), '*');
},
getMessage() {
window.addEventListener('message', this.messageCallBack);
},
},
};
</script>


  1. 子站点也一样在需要的地方通过 postMessage 去发送或者接受数据(比如我们子站点每次都加载首页,然后接收到路由信息,再在子项目中跳转到对应页面)


需要干掉 iframe 滚动条吗


当然需要,不然多丑,加入以下代码即可去掉:


#app {
-ms-overflow-style: none; /* IE 和 Edge 浏览器隐藏滚动条 */
scrollbar-width: none; /* FireFox隐藏浏览器滚动条 */
}
/* Chrome浏览器隐藏滚动条 */
#app::-webkit-scrollbar {
display: none;
}

弹窗是否能覆盖整个屏幕


UI 不同步,DOM 结构不共享。 iframe 里来一个带遮罩层的弹框,只会在 iframe 区域内,为了好看,我们需要让它在整个屏幕的中间


解决:



  • 使得 iframe 区域的宽高本身就和屏幕宽高相等,子站点内部添加 padding ,使内容区缩小到原本子站点 content 区域;

  • 正常情况下,父站点 header、左侧菜单部分的层级需要高于 iframe 的层级(iframe 不会阻止这些区域的点击);

  • 当用户点了新建按钮,对话框出现的时候,给父项目发送一条消息,让父项目调高 iframe 的层级,遮罩便可以覆盖全屏。


这样的解决的缺点:每次打开弹窗,都得先发送 postMessage 数据,逻辑显得多余,对于新手不友好;可是为了好看,只能这样了。


iframe 方案总结


好用的地方:



  • 业务解耦

  • 技术隔离,vue、react 互不影响

  • 项目拆分,上线快速,对其他项目无影响

  • iframe 的硬隔离使得各个项目的 JS 和 CSS 完全独立,不会产生样式污染和变量冲突


存在的缺点:



  • 布局约束:不给定高度,会塌陷;iframe 内的 div 无法全屏(iframe 标签设置 allow="fullscreen" 属性即可)

  • 不利于 seo,会当成 2 个页面,破坏了语义化 ,对无障碍可访问性支持不好

  • url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用

  • 性能开销,慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程


还需要解决的问题:



  • 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果;至于怎么传,可以根据每个公司得实际情况去定


qiankun


接下来在 iframe 的基础下扩展下 qiankun,在使用方面还是简单的
qiankun 使用指南


父项目中


值得注意的是,我们需要增加一个类型,如果是 qiankun 的项目,需要全部指向新增的 qiankun 组件


<template>
<div>
<div id="microContainer" class="microContainer"></div>
</div>
</template>

<script>
import fetchData from './fetchData'; // 一些逻辑处理
import { loadMicroApp } from 'qiankun';
import { getToken } from '@/...';

export default {
data() {
return {
microRef: null,
};
},
methods: {
fetch(route) {
fetchData(route).then(({ data }) => {
const { name, entry } = data;

this.microRef = loadMicroApp({
name,
entry,
container: '#yourContainer',
props: {
router: this.$router,
data: {
// 一些参数
},
token: getToken(),
},
});
});
},
unregisterApplication() {
this.microAppRef.mountPromise.then(() => this.microAppRef.unmount());
},
},
mounted() {
this.fetch(this.$route);
},
beforeDestroy() {
this.unregisterApplication();
},
};
</script>

子项目中


在 src 目录新增文件 public-path.js


iif (window.__POWERED_BY_QIANKUN__) {
// 动态设置子应用的基础路径
// 使用 window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ 变量来获取主应用传递的基础路径
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

子项目中需要导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用


const render = (parent = {}) => {
if (window.__POWERED_BY_QIANKUN__) {
// 渲染 qiankun 的路由
} else {
// 渲染正常的路由
}
};

//全局变量来判断环境,独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/

export async function bootstrap() {
console.log('react app bootstraped');
}

/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/

export async function mount(props) {
render(props.data);
}

/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/

export async function unmount(props) {
ReactDOM.unmountComponentAtNode(
props.container ? props.container.querySelector('#root') : document.getElementById('root')
);
}

/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/

export async function update(props) {
console.log('update props', props);
}

子站点 webpack 配置


const packageName = require('./package.json').name;

module.exports = {
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd', // 把子应用打包成 umd 库格式,让 qiankun 拿到其 export 的生命周期函数
jsonpFunction: `webpackJsonp_${packageName}`,
},
};

路由方面:qiankun 使用父站点的路由,在子站点获取到路由信息,然后加载不同的组件,如果单独运行子站点,则需要适配自己的路由组件,做一些差异化处理就好了


qiankun 总结



  • qiankun 自带 js/css 沙箱功能

    • js 隔离:Proxy 沙箱,它将 window 上的所有属性遍历拷贝生成一个新的 fakeWindow 对象,紧接着使用 proxy 代理这个 fakeWindow,用户对 window 操作全部被拦截下来,只作用于在这个 fakeWindow 之上

    • css 隔离:ShadowDOM 样式沙箱会被开启。在这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响;



  • qiankun 支持子项目预请求功能

  • 支持复用公共依赖

    • webpack 配置 externals:子项目独立运行时,这些依赖的来源有且仅有 index.html 中的外链 script 标签

    • 可以给子项目 index.html 中公共依赖的 script 和 link 标签加上 ignore 属性;有了这个属性,qiankun 便不会再去加载这个 js/css,而子项目独立运行,这些 js/css 仍能被加载




存在的问题:



  • css 污染问题

    • 全局 CSS:如果子站点中使用了全局 CSS 样式(如直接写在 HTML 中的 标签或通过 引入的外部样式表),这些全局样式可能会影响整个页面,包括父站点和其他子站点。
    • CSS 命名冲突:如果子站点和父站点使用相同的 CSS 类名或样式选择器,它们的样式规则可能会相互覆盖或产生不可预料的效果。为避免冲突,可以采用命名约定或 CSS 模块化来隔离样式。




总结


目前只是初步接入了 qiankun,后续还会基于 qiankun 做一些优化,当然不考虑一些因素的情况下,个人觉得 iframe 依旧是最完美的沙箱隔离,当然目前在我们的项目中,他们是共存的,各有优劣。微前端也是前端工

作者:Breeze
来源:juejin.cn/post/7251495270800752700
程化一个重要的方案。

收起阅读 »

Android常见问题

1.1.Demo为啥手机号验证无法登录? 首先我们将demo跑起来是UI是这个样式的点击4.0.3版本号两下,会出现一个提示 我们点击ok2.切换到这个页面我们点击 服务器配置将在管理后台的appkey填写以后 点击下面的保存这样我们在页面正常按照在环信管理后...
继续阅读 »

1.1.Demo为啥手机号验证无法登录?

首先我们将demo跑起来是UI是这个样式的

点击4.0.3版本号两下,会出现一个提示 我们点击ok
2.
切换到这个页面我们点击 服务器配置

将在管理后台的appkey填写以后 点击下面的保存
这样我们在页面正常按照在环信管理后台申请的 环信id 登录就可以了 (登录方式是账号密码登录)
2.修改会话条目的尺寸宽高 他是属于EaseBaseLayout ,相比EaseChatLayout 他是ChatLayout的父类 关于尺寸大小的设计是存在基本都在父类中


3.集成后环信后,App被其他应用平台下架,厂商反馈是自启动的原因

将此服务去除


4.如何将百度地图切换到高德地图


1.因为百度地图将easeimkit中关于百度地图的集成去掉,改成高德地图;2.在chatfragment中重写位置的点击事件方法startMapLocation或者是直接在EaseChatFragment中直接修改点击事件startMapLocation跳转到高德地图;3.在调用环信api去发送地理位置消息时,传入高德获取到的经纬度。
2..点击位置的点击事件更换 ,demo中的点击事件是在EaseChatFragment下的onExtendMenuItemClick里面官方提供了EaseBaiduMapActivity 这个定位页面。2.修改为高德其实非常简单只需要在ChatFragment操作就可以了2.1修改点击事件在ChatFragment的onExtendMenuItemClick方法中添加2.2 在自己实现高德地图的页面返回定位信息 参数名称不要修改 不然其它地方也要修改2.3接下来在ChatFragment中的onActivityResult中接收定位信息并发送消息走到这里从高德获取的位置消息已经成功发送给好友了 接下来是获取查看好友位置消息2.4 查看位置消息还是在ChatFragment里 通过getCustomChatRow方法LoccationAdapter 继承位置消息展示 重写。
5.播放语音消息语音消息的声音小(不是语音通话)
(1)首先要打开扬声器 如果觉得声音还是比较小

(2)将ui库中调用的原声音量模式修改为媒体音量模式




收起阅读 »

女朋友要我讲解@Controller注解的原理,真是难为我了

背景 女朋友被公司裁员一个月了,和我一样作为后端工程师,最近一直在找工作,面试了很多家还是没有找到工作,面试官问@Controller的原理,她表示一脸懵,希望我能给她讲清楚。之前我也没有好好整理这块知识,这次借助这个机会把它彻底搞清楚。 我们知道Contr...
继续阅读 »

背景


女朋友被公司裁员一个月了,和我一样作为后端工程师,最近一直在找工作,面试了很多家还是没有找到工作,面试官问@Controller的原理,她表示一脸懵,希望我能给她讲清楚。之前我也没有好好整理这块知识,这次借助这个机会把它彻底搞清楚。
太难了.jpeg


我们知道Controller注解的类能够实现接收并处理Http请求,其实在我看Spring mvc模块的源码之前也和我女朋友目前的状态一样,很疑惑,Spring框架是底层是如何实现的,通过使用Controller注解就简单的完成了http请求的接收与处理。


image.png


有疑问就好啊,因为兴趣是最好的老师,如果有兴趣才有动力去弄懂这个技术点。


看过前面的文章的同学就会知道,学习Spring的所有组件,脑袋里要有一个思路,那就是解析组件和运用组件两个流程,这是Spring团队实现组件的统一套路,大家可以回忆一下是不是这么回事。


image.png


一、Spring解析Controller注解


首先我们看看Spring是如何解析Controller注解的,打开源码看看他长啥样??

@Target({ElementType.TYPE})
@Component
public @interface Controller {
String value() default "";
}

发现Controller注解打上了Component的注解,这样Spring做类扫描的时候,发现了@Controller标记的类也会当作Bean解析并注册到Spring容器。
我们可以看到Spring的类扫描器,第一个就注册了Component注解的扫描

//org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider
protected void registerDefaultFilters() {
this.includeFilters.add(new AnnotationTypeFilter(Component.class));
}

这样Spring容器启动完成之后,bean容器中就有了被Controller注解标记的bean实例了。
到这里只是单纯的把Controller标注的类实例化注册到Spring容器,和Http请求接收处理没半毛钱关系,那么他们是怎么关联起来的呢?


二、Spring解析Controller注解标注的类方法


这个时候Springmvc组件中的另外一个组件就闪亮登场了



RequestMappingHandlerMapping



RequestMappingHandlerMapping 看这个名就可以知道他的意思,请求映射处理映射器。
这里就是重点了,该类间接实现了InitializingBean方法,bean初始化后执行回调afterPropertiesSet方法,里面调用initHandlerMethods方法进行初始化handlermapping。


//类有没有加Controller的注解
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

protected void initHandlerMethods() {
//所有的bean
String[] beanNames= applicationContext().getBeanNamesForType(Object.class);

for (String beanName : beanNames) {
Class<?> beanType = obtainApplicationContext().getType(beanName);
//有Controller注解的bean
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
handlerMethodsInitialized(getHandlerMethods());
}

这里把标注了Controller注解的实例全部找到了,然后调用detectHandlerMethods方法,检测handler方法,也就是解析Controller标注类的方法。


private final Map<T, MappingRegistration<T>> registry = new HashMap<>();

protected void detectHandlerMethods(final Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());

if (handlerType != null) {
final Class<?> userType = ClassUtils.getUserClass(handlerType);
//查找Controller的方法
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType));

methods.forEach((method, mapping) -> {
//注册
this.registry.put(mapping,new MappingRegistration<>(mapping,method));

});
}


到这里为止,Spring将Controller标注的类和类方法已经解析完成。现在再来看RequestMappingHandlerMapping这个类的作用,他就是用来注册所有Controller类的方法。


三、Spring调用Controller注解标注的方法


接着还有一个重要的组件RequestMappingHandlerAdapter
它就是用来将请求转换成HandlerMethod,并且完成请求处理的流程。
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter

@Override
public boolean supports(Object handler) {
return handler instanceof HandlerMethod;
}

protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
//请求check
checkRequest(request);
//调用handler方法
mav = invokeHandlerMethod(request, response, handlerMethod);
//返回
return mav;
}

看到这里,就知道http请求是如何被处理的了,我们找到DispatcherServlet的doDispatch方法看看,确实是如此!!


四、DispatcherServlet调度Controller方法完成http请求

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 从注册表查找handler
HandlerExecutionChain mappedHandler = getHandler(request);
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 调用
ModelAndView m = ha.handle(processedRequest, response, mappedHandler.getHandler());
//
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}

DispatcherServlet是Spring mvc的总入口,看到doDispatch方法后,全部都联系起来了。。。
最后我们看看http请求在Spring mvc中的流转流程。


image.png


第一次总结SpringMvc模块,理解不到位的麻烦各位大佬指正。


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

关于WorkManager需要知道的一切

背景WorkManager 是google为Android推出Jetpack开发套件的成员之一,是一个持续化工作的推荐解决方案。 我们在开发过程中通常会遇到一些需要持续化的操作需求,例如,上传文件到服务端,Token过期刷新,定期更新服务端下发的配置等。主要的...
继续阅读 »

背景

WorkManager 是google为Android推出Jetpack开发套件的成员之一,是一个持续化工作的推荐解决方案。 我们在开发过程中通常会遇到一些需要持续化的操作需求,例如,上传文件到服务端,Token过期刷新,定期更新服务端下发的配置等。

主要的用途是:

立即开始执行一个任务

有的任务必须立即开始执行,但这里可能会有个疑问,为什么立即开始执行的任务不直接写代码开始执行,要利用WorkManager呢?这主要归功于WorkManager的一些特性,例如根据约束安排任务,链式化任务等,让你代码写得更简洁,更好的扩展性。

需要长时间定期运行的任务

任务需要长时间定期运行的的情况,例如我们我需要每一个小时上传一次用户日志.

延迟任务

有的任务需要延后一段时间执行.


再继续讲解它如何使用之前先来说说它的几个特性.

特性

约束

提供一些约束条件去执行任务,当约束条件满足后才开始执行,例如,当连接上Wifi, 当设备有足够的电量等条件

可靠性

安排的任务保证能够顺利执行,因为WorkManager内部以SqLite存储任务的执行的情况,为执行的和执行失败的都会重新尝试.

加急任务

可能你会给WorkManager安排很多Task, 某些Task也许优先级比较高,需要立即执行,WorkManager提供加急的特性,可以尽早执行这类Task.

链式任务

某些Task可能需要顺序执行,也可能需要并行执行,WorkManager同样提供这类API满足需求

val continuation = WorkManager.getInstance(context)
    .beginUniqueWork(
        Constants.IMAGE_MANIPULATION_WORK_NAME,
        ExistingWorkPolicy.REPLACE,
        OneTimeWorkRequest.from(CleanupWorker::class.java)
    ).then(OneTimeWorkRequest.from(WaterColorFilterWorker::class.java))
    .then(OneTimeWorkRequest.from(GrayScaleFilterWorker::class.java))
    .then(OneTimeWorkRequest.from(BlurEffectFilterWorker::class.java))
    .then(
        if (save) {
            workRequest<SaveImageToGalleryWorker>(tag = Constants.TAG_OUTPUT)
        } else /* upload */ {
            workRequest<UploadWorker>(tag = Constants.TAG_OUTPUT)
        }
    )

线程的互操作性

无缝继承了Coroutines和RxJava的异步特性,在WorkManager也能使用这类异步的API


实战

说了这么多,下面我们来看看如何使用WorkManager

第一步,添加依赖

dependencies {
    def work_version = "2.8.1"

    // (Java only)
    implementation "androidx.work:work-runtime:$work_version"

    // Kotlin + coroutines
    implementation "androidx.work:work-runtime-ktx:$work_version"

    // optional - RxJava2 support
    implementation "androidx.work:work-rxjava2:$work_version"

    // optional - GCMNetworkManager support
    implementation "androidx.work:work-gcm:$work_version"

    // optional - Test helpers
    androidTestImplementation "androidx.work:work-testing:$work_version"

    // optional - Multiprocess support
    implementation "androidx.work:work-multiprocess:$work_version"
}

第二步,自定义Worker

这里我们需要定义,这个Task需要做什么,创建一个自定义Worker类,这里我们创建一个UploadWorker用于做一些后台上传的操作

class UploadWorker(appContext: Context, workerParams: WorkerParameters):
       Worker(appContext, workerParams) {
   override fun doWork(): Result {
       // 做一些上传操作
       uploadImages()
       // 代表任务执行成功
       return Result.success()
   }
}

在doWork中我们可以写上任务需要执行的代码,当任务结束后需要返回一个Result,这个Result有三个值

Result.success() 任务执行成功

Result.failure() 任务执行失败

Result.retry() 任务需要重试

第三步, 创建一个WorkRequest

当定义完需要做什么后我们需要创建一个WorkRequest去启动这个任务的执行。WorkManager提供了很多灵活的API用于定义任务的启动逻辑,例如是否执行一次还是周期性执行,它的约束条件是什么等。这里演示我们使用OneTimeWorkRequest.

val uploadWorkRequest: WorkRequest =
   OneTimeWorkRequestBuilder<UploadWorker>()
       .build()

第四步, 提交WorkRequest

当创建完成WorkRequest,我们需要把它交给WorkManager去执行

WorkManager
    .getInstance(myContext)
    .enqueue(uploadWorkRequest)

进阶

一次性任务

创建一个简单的一次性任务

val myWorkRequest = OneTimeWorkRequest.from(MyWork::class.java)

如果需要增加一些配置如约束等可以使用builder

val uploadWorkRequest: WorkRequest =
   OneTimeWorkRequestBuilder<MyWork>()
       // 添加额外配置
       .build()
加急工作

WorkManager 执行重要的工作,同时让系统更好地控制对资源的访问。

加急工作具有以下特点:

重要性:加急工作适合对用户重要或由用户启动的任务。

速度:加急工作最适合立即开始并在几分钟内完成的短任务。

配额:限制前台执行时间的系统级配额决定加急作业是否可以启动。

电源管理:电源管理限制(例如省电模式和打瞌睡模式)不太可能影响加急工作。

启动加急工作的方式也非常简单,可以直接调用setExpedited()设置该WorkRequest为一个加急任务

val request = OneTimeWorkRequestBuilder()
    .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
    .build()

WorkManager.getInstance(context)
    .enqueue(request)

这里setExpedited会有一个参数OutOfQuotaPolicy,代表系统配额不足时候,把该任务作为一个普通任务对待.

周期性任务

我们有一些需求例如,备份应用数据,上传日志,下载一些应用配置,需要周期性进行,我们可以定义PeriodicWorkRequest去创建周期性的任务.


val saveRequest =
       PeriodicWorkRequestBuilder<SaveImageToFileWorker>(1, TimeUnit.HOURS)
           .build()

上面这段代码每一小时执行一次任务.

但是这个时间约束也不是固定的,这里定义的时间实际上是最小间隔时间,系统会根据当前系统的情况进行适当调整。

我们还可以定义flexInterval让间隔提前一点


val myUploadWork = PeriodicWorkRequestBuilder<SaveImageToFileWorker>(
       1, TimeUnit.HOURS, // repeatInterval
       15, TimeUnit.MINUTES) // flexInterval
    .build()

这样我们执行任务的时间是repeatInterval - flexInterval,上面代码的任务会在1小时-15分钟的时候执行.

image.png

周期性任务遇上约束条件

当周期性任务遇到一些约束条件不满足的时候将会延迟执行,直到约束条件满足.

关于约束

WorkManager提供了下列的一些约束条件.

NetworkType 网络条件约束,例如只能在连接WIFI的情况下执行.

BatteryNotLow 非电量低约束,在有充足的电量的时候执行.

RequiresCharging 需要充电的时候执行约束

DeviceIdle 在设备无状态时候运行,这样不会对设备的效率产生影响.

StorageNotLow 当设备有有足够的存储空间时候运行

创建一个约束使用Contraints.Builder()并赋值给WorkRequest.Builder().

下面代码展示创建一个约束该任务只会在wifi并且在充电的时候执行.

val constraints = Constraints.Builder()
   .setRequiredNetworkType(NetworkType.UNMETERED)
   .setRequiresCharging(true)
   .build()

val myWorkRequest: WorkRequest =
   OneTimeWorkRequestBuilder<MyWork>()
       .setConstraints(constraints)
       .build()

延迟任务

如果你指定的任务没有约束或者约束已经满足,那么它会立即开始执行,如果想让它有个最少的延迟,可以指定一个最小的延迟执行时间.

下面这个例子展示设置最小10分钟后开始加入队列.

val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
   .setInitialDelay(10, TimeUnit.MINUTES)
   .build()

上面展示的针对OneTimeWorkRequestBuilder同样也适用于PeriodicWorkRequest.

退避策略

如果一个任务失败返回Result.retry(), 你的任务可以在稍等进行重试,这种退避策略可以自定义,这里连个自定义的属性

退避延迟:退避延迟执行下次尝试任务的最少时间,通常我们自定义最少不能低于[MIN_BACKOFF_MILLIS]

退避策略:退避策略可以指定两种一个是LINEAR(线性)和EXPONENTIAL(幂等)

实际上每一个任务都有一个默认的退避策略,缺省的退避策略是EXPONENTIAL和30s的延迟,但是你可以自定义,下面是一个自定义的例子。

val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
   .setBackoffCriteria(
       BackoffPolicy.LINEAR,
       OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
       TimeUnit.MILLISECONDS)
   .build()

Tag任务

每一个任务都可以附加上一个标签,稍后可以使用这个标签找到该任务,取消它或者查看的执行进度. 如果你有一组任务,可以添加同一个标签,可以统一的操作它们。例如使用WorkManager.cancelAllWorkByTag(String)取消所有的任务,使用WorkManager.getWorkInfosByTag(String)返回一个任务信息列表查看当前任务的状态.

下面是一个展示给任务赋值一个标签

val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
   .addTag("cleanup")
   .build()

链式任务

WorkManager还提供了一种定义顺序执行任务或者并发执行任务的方式。

使用这种方式创建任务通过

WorkManager.beginWith(OneTimeWorkRequest)
WorkManager.beginWith(List<OneTimeWorkRequest>)

以上两个方式都会返回一个WorkContinuation.

WorkContinuation随后可以继续调用

then(OneTimeWorkRequest)
then(List<OneTimeWorkRequest>)

执行链式任务

最后可以调用enqueue()去执行你的工作链. 举个例子

WorkManager.getInstance(myContext)
   .beginWith(listOf(plantName1, plantName2, plantName3))
   .then(cache)
   .then(upload)
   .enqueue()

Input Mergers

一个父任务的执行结果可以传递给子任务,例如上面plantName1, plantName2, plantName3执行的结果可以传递

给cache任务,WorkManager使用InputMerger去管理这些多个父任务的输出结果到子任务.

这里有两种不同类型的的InputMerger

  • OverwritingInputMerger 从输入到输出增加所有的key,遇到冲突的情况,覆盖之前的key的值

  • ArrayCreatingInputMerger 从输入到输出增加所有的key,遇到冲突的情况进行创建数组

工作链的状态

当前面的任务阻塞住的时候后面的任务同样也是阻塞状态.

image.png

当前面的任务执行成功后,后面的任务才能继续开始执行

image.png

当一个任务失败进行重试的时候并不会影响并发的任务

image.png

一个任务失败,后面的任务也会是失败状态

image.png

对于取消也是这样

image.png

结语

总的来说WorkManager是一个非常好用的组件,它解决了一些曾经实现起来比较繁琐的功能,例如它的约束执行,我们可以等待有网络时候执行任务。我们利用周期性执行任务功能能够很方便的执行一些诸如刷新token, 定期日志上传等功能.


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

末日终极坐标安卓辅助工具

前言 本工具完全免费,无需联网 本文档只介绍工具的使用方法,有时间再写一篇介绍一下实现细节。 总的来讲本工具可以帮助你时刻知道自己的坐标并知道和宝箱的位置关系,减少资源浪费。 5分钟即可完成100汽油的使用,大大节省时间。 阅读本文档前提是大家是《末日血战》等...
继续阅读 »

前言


本工具完全免费,无需联网


本文档只介绍工具的使用方法,有时间再写一篇介绍一下实现细节。

总的来讲本工具可以帮助你时刻知道自己的坐标并知道和宝箱的位置关系,减少资源浪费。

5分钟即可完成100汽油的使用,大大节省时间。


阅读本文档前提是大家是《末日血战》等同款游戏的玩家。


工具下载安装


链接: pan.baidu.com/s/14GE-713c… 提取码: 29c5


安装工具


工具安装后,桌面会有这个图标。

在打开工具前需进入应用设置页打开这个应用的显示悬浮窗权限图片说明


填入初始坐标


打开工具大致显示是这个样子,坐标初始都是0,那么填入相应的坐标保存就会是这个样子。
14ae0a88098b04a18b0a0c36b400e3c.jpg
可以看到左上角有个小图,这是一个直角坐标系的缩略图,左上角位置是(0,0)右下角位置(301,301),拖拽可以移动位置。
小图中会显示3个红点,一个绿点。绿点表示当前坐标,红点表示终极坐标。绿点的坐标数值是固定显示在左上角的,不随绿点移动


此时可以按返回键退出app,但是不要杀掉应用。


建立坐标系


初次使用,建议可以打开小程序截一张生存之路的全屏图,然后我们打开这张图并横屏显示图片开始操作。当已经熟悉工具如何使用后可以在游戏中进行操作了


建立坐标系

打开图片或者游戏,进入到这个界面,点击左上角悬浮窗上的开始按钮,会看到这样一个界面(没有中间两条直线)
47b446b703476c931a27667de16d706.jpg
我们的目标就是为了建立中间两条直线。


严格按图片指示的顺序操作。尽可能点击在轴线的中心位置
d6bdd46daf0a94960c54bf8918af5f5.png
以上操作只需要执行一次,后续就不需要操作了。
完成以上操作,就得到有两条线的图了。这个时候就完成了建立坐标系。

如果坐标系建立的不太好(比较斜,良好的坐标系有助于减少误差。),可以重新再来直到满意为止。


开始寻找终极坐标


注意观察小地图,找一个我们没有到达的离的近的终极坐标为目标。可以看到x和y的差距。

举个例子,我们当前坐标49,52,刚刚已经在48,52这里取得了一个宝箱,那么下一个目的地选237,29。因为是x坐标相差较大,我们x太小,而y坐标我们的大一点。所以主要的方向是加x,少量的减y。
05c5394993b5a8877db3d81c8ce6425.png
所以我们应该按x轴正方向和y轴负方向这里走,因为x相差较大,所以如果可以的话(有障碍物就走不了)就直接沿着x轴正方向走就好了。


我想游戏玩家应该还是知道要往哪走的,但是容易算错坐标或者根本懒得记,凭运气。那么我们指定往哪走之后,接下来怎么使用这个工具。



  • 1、点击一个位置(我们要让小车开到的位置),这个时候小车不会走,因为我们工具盖住了游戏

  • 2、app回退一下(不是杀掉应用),这时可以发现小车在抖动了,其实就是小车可以走了,再点一下刚才那个位置,小车就会走到那个位置。这样我们就完成了一次移动和坐标记录。小地图当前坐标就会变化。绿点也会移动。

  • 3、小车走完之后,我们再点开始,然后重复1,2 步骤。


补充



  • 1、本工具存在误差,一般每次执行在小车x,y <=|2| 基本100%准确。x,y <= |3| 100个汽油可能会有|2|以内坐标的误差(仅本人测试数据)

  • 2、点击位置尽可能点在地图块的中间,这样可以减少误差。遇到坐标点在路径上,可以进入其中对当前坐标进行校准,当然一般是不需要的。

  • 3、如果遇到了事件,我们就处理完事件后再点开始按钮

  • 4、回退怎么用:右下角回退用途是当我们不想走这一步,可以点回退按钮撤销这一步,然后重新再点一个点。如果还是后悔不想走这一步(这个回退是指回退我们的记录,游戏中的步骤我们肯定是做不到回退的)可以再次回退,但要注意只能回退一步。

  • 5、在工具操作界面(即点击开始后显示坐标线和按钮的那种情况),GETXY按钮下的白色坐标数字是表示当前这一步的行进地图坐标,例如3,2表示向x轴正方向移动3格,y轴正方向移动2格。可以通过这个坐标判断工具计算的坐标是否准确。

  • 6、本次汽油用完后,就可以杀掉辅助工具app了。下次有汽油可以继续直接使用,记住使用过程中的退出都是回退而不是杀掉app

  • 7、如果已经在工具上操作坐标了,但是发现汽油不够了,这个时候最好是买几个汽油仍然走到刚才记录的位置。当然也可以使用回退功能,再重新操作工具点击到你汽油够的位置。


最后


希望大家先熟悉工具流程,可以截一张图去操作,参考上文补充5的说明,通过这个坐标数值可以知道工具记录是否准确。然后再在游戏中操作避免浪费资源。如果通过截图去熟悉工具使用,在正式使用前要核对一下当前坐标是否准确。坐标可以随时矫正。

希望大家游戏愉快,也希望本工具对大家有所帮助。


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

为什么谷歌搜索不支持无限分页

这是一个很有意思却很少有人注意的问题。 当我用Google搜索MySQL这个关键词的时候,Google只提供了13页的搜索结果,我通过修改url的分页参数试图搜索第14页数据,结果出现了以下的错误提示: 百度搜索同样不提供无限分页,对于MySQL关键词,百度...
继续阅读 »

这是一个很有意思却很少有人注意的问题。


当我用Google搜索MySQL这个关键词的时候,Google只提供了13页的搜索结果,我通过修改url的分页参数试图搜索第14页数据,结果出现了以下的错误提示:


Google不能无限分页


百度搜索同样不提供无限分页,对于MySQL关键词,百度搜索提供了76页的搜索结果。


百度不能无限分页


为什么不支持无限分页


强如Google搜索,为什么不支持无限分页?无非有两种可能:



  • 做不到

  • 没必要


「做不到」是不可能的,唯一的理由就是「没必要」。


首先,当第1页的搜索结果没有我们需要的内容的时候,我们通常会立即更换关键词,而不是翻第2页,更不用说翻到10页往后了。这是没必要的第一个理由——用户需求不强烈。


其次,无限分页的功能对于搜索引擎而言是非常消耗性能的。你可能感觉很奇怪,翻到第2页和翻到第1000页不都是搜索嘛,能有什么区别?


实际上,搜索引擎高可用和高伸缩性的设计带来的一个副作用就是无法高效实现无限分页功能,无法高效意味着能实现,但是代价比较大,这是所有搜索引擎都会面临的一个问题,专业上叫做「深度分页」。这也是没必要的第二个理由——实现成本高。


我自然不知道Google的搜索具体是怎么做的,因此接下来我用ES(Elasticsearch)为例来解释一下为什么深度分页对搜索引擎来说是一个头疼的问题。


为什么拿ES举例子


Elasticsearch(下文简称ES)实现的功能和Google以及百度搜索提供的功能是相同的,而且在实现高可用和高伸缩性的方法上也大同小异,深度分页的问题都是由这些大同小异的优化方法导致的。


什么是ES


ES是一个全文搜索引擎。


全文搜索引擎又是个什么鬼?


试想一个场景,你偶然听到了一首旋律特别优美的歌曲,回家之后依然感觉余音绕梁,可是无奈你只记得一句歌词中的几个字:「伞的边缘」。这时候搜索引擎就发挥作用了。


使用搜索引擎你可以获取到带有「伞的边缘」关键词的所有结果,这些结果有一个术语,叫做文档。并且搜索结果是按照文档与关键词的相关性进行排序之后返回的。我们得到了全文搜索引擎的定义:



全文搜索引擎是根据文档内容查找相关文档,并按照相关性顺序返回搜索结果的一种工具



2022-06-08-085125.png


网上冲浪太久,我们会渐渐地把计算机的能力误以为是自己本身具备的能力,比如我们可能误以为我们大脑本身就很擅长这种搜索。恰恰相反,全文检索的功能是我们非常不擅长的。


举个例子,如果我对你说:静夜思。你可能脱口而出:床前明月光,疑是地上霜。举头望明月,低头思故乡。但是如果我让你说出带有「月」的古诗,想必你会费上一番功夫。


包括我们平时看的书也是一样,目录本身就是一种符合我们人脑检索特点的一种搜索结构,让我们可以通过文档ID或者文档标题这种总领性的标识来找到某一篇文档,这种结构叫做正排索引


目录就是正排索引


而全文搜索引擎恰好相反,是通过文档中的内容来找寻文档,诗词大会中的飞花令就是人脑版的全文搜索引擎。


飞花令就是全文搜索


全文搜索引擎依赖的数据结构就是大名鼎鼎的倒排索引(「倒排」这个词就说明这种数据结构和我们正常的思维方式恰好相反),它是单词和文档之间包含关系的一种具体实现形式。


单词文档矩阵


打住!不能继续展开了话题了,赶紧一句话介绍完ES吧!



ES是一款使用倒排索引数据结构、能够根据文档内容查找相关文档,并按照相关性顺序返回搜索结果的全文搜索引擎



高可用的秘密——副本(Replication)


高可用是企业级服务必须考虑的一个指标,高可用必然涉及到集群和分布式,好在ES天然支持集群模式,可以非常简单地搭建一个分布式系统。


ES服务高可用要求其中一个节点如果挂掉了,不能影响正常的搜索服务。这就意味着挂掉的节点上存储的数据,必须在其他节点上留有完整的备份。这就是副本的概念。


副本


如上图所示,Node1作为主节点,Node2Node3作为副本节点保存了和主节点完全相同的数据,这样任何一个节点挂掉都不会影响业务的搜索。满足服务的高可用要求。


但是有一个致命的问题,无法实现系统扩容!即使添加另外的节点,对整个系统的容量扩充也起不到任何帮助。因为每一个节点都完整保存了所有的文档数据。


因此,ES引入了分片(Shard)的概念。


PB级数量的基石——分片(Shard)


ES将每个索引(ES中一系列文档的集合,相当于MySQL中的表)分成若干个分片,分片将尽可能平均地分配到不同的节点上。比如现在一个集群中有3台节点,索引被分成了5个分片,分配方式大致(因为具体如何平均分配取决于ES)如下图所示。


分片


这样一来,集群的横向扩容就非常简单了,现在我们向集群中再添加2个节点,则ES会自动将分片均衡到各个节点之上:


横向扩展


高可用 + 弹性扩容


副本和分片功能通力协作造就了ES如今高可用支持PB级数据量的两大优势。


现在我们以3个节点为例,展示一下分片数量为5,副本数量为1的情况下,ES在不同节点上的分片排布情况:


主分片和副分片的分布


有一点需要注意,上图示例中主分片和对应的副本分片不会出现在同一个节点上,至于为什么,大家可以自己思考一下。


文档的分布式存储


ES是怎么确定某个文档应该存储到哪一个分片上呢?



通过上面的映射算法,ES将文档数据均匀地分散在各个分片中,其中routing默认是文档id。


此外,副本分片的内容依赖主分片进行同步,副本分片存在意义就是负载均衡、顶上随时可能挂掉的主分片位置,成为新的主分片。


现在基础知识讲完了,终于可以进行搜索了。


ES的搜索机制


一图胜千言:


es搜索



  1. 客户端进行关键词搜索时,ES会使用负载均衡策略选择一个节点作为协调节点(Coordinating Node)接受请求,这里假设选择的是Node3节点;

  2. Node3节点会在10个主副分片中随机选择5个分片(所有分片必须能包含所有内容,且不能重复),发送search request;

  3. 被选中的5个分片分别执行查询并进行排序之后返回结果给Node3节点;

  4. Node3节点整合5个分片返回的结果,再次排序之后取到对应分页的结果集返回给客户端。



注:实际上ES的搜索分为Query阶段Fetch阶段两个步骤,在Query阶段各个分片返回文档Id和排序值,Fetch阶段根据文档Id去对应分片获取文档详情,上面的图片和文字说明对此进行了简化,请悉知。



现在考虑客户端获取990~1000的文档时,ES在分片存储的情况下如何给出正确的搜索结果。


获取990~1000的文档时,ES在每个分片下都需要获取1000个文档,然后由Coordinating Node聚合所有分片的结果,然后进行相关性排序,最后选出相关性顺序在990~100010条文档。


深度分页


页数越深,每个节点处理的文档也就越多,占用的内存也就越多,耗时也就越长,这也就是为什么搜索引擎厂商通常不提供深度分页的原因了,他们没必要在客户需求不强烈的功能上浪费性能。


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

【android activity重难点突破】这些知识还不会,面试八成被劝退

Activity作为android四大组件之一,地位就不用多说了吧,该组件看起来是比较简单的,但是也涉及到很多知识点,要想全部理解并在合适的业务场景下使用,也是需要一定的技术沉淀,本文主要是对activity一些重要知识点进行总结整理,可能平时不一定用到,但是...
继续阅读 »

Activity作为android四大组件之一,地位就不用多说了吧,该组件看起来是比较简单的,但是也涉及到很多知识点,要想全部理解并在合适的业务场景下使用,也是需要一定的技术沉淀,本文主要是对activity一些重要知识点进行总结整理,可能平时不一定用到,但是一定要有所了解。


当然这些知识点并没有设计过多源码部分,比如activity的启动流程什么的,主要是零散的知识点,对于activity的启动流程网上文章太多了,后面自己也准备重新梳理下,好记性不如烂笔头,在不断学习整理的过程中,一定会因为某个知识点而豁然开朗。


image.png


1.生命周期


①.两个页面跳转


MainActivity跳转到SecordActivity的生命周期,重点关注MainonPauseonStopSecord几个关键生命周期的顺序,以及从Secord返回时与Main的生命周期的交叉:


image.png


可以发现Main页面的onPause生命周期之后直接执行SecordonCreate,onStart,onResume,所以onPause生命周期内不要执行耗时操作,以免影响新页面的展示,造成卡顿感。


②.弹出Dialog



  • 单纯的弹出Dialog是不会影响Activity的生命周期的;

  • 启动dialog themeActivity的时候,启动的activity只会执行onPause方法,onStop不会执行,被启动的activity会正常走生命周期,back的时候,启动的Activity会对应执行onResume方法;


image.png


③.横竖屏切换



  • AndroidManifest不配置configChanges时,横竖屏切换,会销毁重建Activity,生命周期会重新走一遍;

  • 当ActivityconfigChanges="orientation|screenSize"时,横竖屏切换不会重新走Activity生命周期方法,只会执行onConfigurationChanged方法,如需要可以在此方法中进行相应业务处理;



如横竖屏切换时需要对布局进行适配,可在res下新建layout-portlayout-land目录,并提供相同的xml布局文件,横竖屏切换时即可自动加载相应布局。(前提是未配置configChanges忽略横竖屏影响,否则不会重新加载布局)



④.启动模式对生命周期的影响


1.A(singleTask)启动(startActivity)B(standard),再从B启动A,生命周期如下:


A启动B:A_onPause、B_onCreate、B_onStart、B_onResume、A_onStop


第二步:B_onPause、A_onNewIntent、A_onRestart、A_onStart、A_onResume、B_onStop、B_onDestory


2.A(singleTask)启动A,或者A(singleTop)启动A


A_onPause、A_onNewIntent、A_Resume


3.singleInstance模式的activity


多次启动A(singleInstance),只有第一次会创建一个单独的任务栈(全局唯一),再次启动会调用A_onPause、A_onNewIntent、A_Resume


2.启动模式


Activity的启动模式一直是standardsingleTopsingleTasksingleInstance四种,Android 12新增了singleInstancePerTask启动模式,在这里不一一介绍,仅介绍重要知识点。


①.singleTask


1.Activity是一个可以跨进程、跨应用的组件,当你在 A App里打开 B AppActivity的时候,这个Activity会直接被放进A的Task里,而对于B的Task,是没有任何影响的。


从A应用启动B应用,默认情况下启动的B应用的Activity会进入A应用当前页面所在的任务栈中,此时按home建,再次启动B应用,会发现B应用并不会出现A启动的页面(前提是A应用启动的不是B应用主activity,如果是必然一样),而是如第一次启动一般.


如果想要启动B应用的时候出现被A应用启动的页面,需要设置B应用被启动页的launchmodesingleTask,此时从A应用的ActivityA页面启动B应用的页面ActivityBlaunchmodesingleTask),发现动画切换方式是应用间切换,此时ActivityBActivityA分别处于各自的任务栈中,并没有在一个task中,此时按Home键后,再次点击启动B应用,发现B应用停留在ActivityB页面。


如果想要实现上述效果,除了设置launchmode之外,还可以通过设置allowTaskReparenting属性达到同样的效果,Activity 默认情况下只会归属于一个 Task,不会在多个Task之间跳来跳去,但你可以通过设置来改变这个逻辑,如果你不设置singleTask,而是设置allowTaskReparentingtrue,此时从A应用的ActivityA页面启动B应用的页面ActivityB(设置了allowTaskReparentingtrue),ActivityB会进入ActivityA的任务栈,此时按Home键,点击启动B应用,会进入ActivityB页面,也就是说ActivityBActivityA的任务栈移动到了自己的任务栈中,此时点击返回,会依次退出ActivityB所在任务栈的各个页面,直到B应用退出。


注意:allowTaskReparenting在不同Android版本上表现有所不同,Android9以下是生效的,Android9,10又是失效的,但Android11又修复好了,在使用时一定要好好测试,避免一些因版本差异产生的问题。


②.singleInstance


singleInstance具备singleTask模式的所有特性外,与它的区别就是,这种模式下的Activity会单独占用一个Task栈,具有全局唯一性,即整个系统中就这么一个实例,由于栈内复用的特性,后续的请求均不会创建新的Activity实例,除非这个特殊的任务栈被销毁了。以singleInstance模式启动的Activity在整个系统中是单例的,如果在启动这样的Activity时,已经存在了一个实例,那么会把它所在的任务调度到前台,重用这个实例。


③.singleInstancePerTask


释义:singleInstancePerTask的作用和singleTask几乎一模一样,不过singleInstancePerTask不需要为启动的Activity设置一个特殊的taskAffinity就可以创建新的task,换句话讲就是设置singleInstancePerTask模式的activity可以存在于多个task任务栈中,并且在每个任务栈中是单例的。


多次启动设置singleInstancePerTask模式的Activity并不会多次创建新的任务栈,而是如singleInstance模式一样,把当前Activity所在的任务栈置于前台展示,如果想每次以新的任务栈启动需要设置FLAG_ACTIVITY_MULTIPLE_TASKFLAG_ACTIVITY_NEW_DOCUMENT,使用方式如下:

intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);

此时,每次启动Activity就会单独创建新的任务栈。


注意:测试需要在Android12的真机或者模拟器上,否则默认为Standard模式


3.taskAffinity


taskAffinity可以指定任务栈的名字,默认任务栈是应用的包名,前提是要和singleTask,singleInstance模式配合使用,standardsingleTop模式无效,当app存在多个任务栈时,如果taskAffinity相同,则在最近任务列表中只会出现处于前台任务栈的页面,后台任务栈会“隐藏”在某处,如果taskAffinity不同,最近任务列表会出现多个任务页面,点击某个就会把该任务栈至于前台。


4.清空任务栈


activity跳转后设置FLAG_ACTIVITY_CLEAR_TASK即可清空任务栈,并不是新建一个任务栈,而是清空并把当前要启动的activity置于栈底,使用场景比如:退出登录跳转到登录页面,可以以此情况activity任务栈。

intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK|Intent.FLAG_ACTIVITY_NEW_TASK);


注意:FLAG_ACTIVITY_CLEAR_TASK必须与FLAG_ACTIVITY_NEW_TASK一起使用.



5.Activity.FLAG


FLAG_ACTIVITY_NEW_TASK


FLAG_ACTIVITY_NEW_TASK并不像起名字一样,每次都会创建新的task任务栈,而是有一套复杂的规则来判断:



  • 通过activity类型的context启动,如果要启动的ActivitytaskAffinity与当前Activity不一致,则会创建新的任务栈,并将要启动的Activity置于栈底,taskAffinity一致的话,就会存放于当前activity所在的任务栈(注意启动模式章节第三点taskAffinity的知识点);

  • taskAffinity一致的情况下,如果要启动的activity已经存在,并且是栈根activity,那么将没有任何反应(启动不了要启动的activity)或者把要启动的activity所在的任务栈置于前台;否则如果要启动的activity不存在,将会在当前任务栈创建要启动的activity实例,并入栈;

  • taskAffinity一致的情况下,如果要启动的activity已经存在,但不是栈根activity,依然会重新创建activity示例,并入栈(前提是:要启动的activitylaunchModestandard,意思就是是否会创建新实例会受到launchMode的影响);

  • activitycontext启动activity时(比如在service或者broadcast中启动activity),在android7.0之前和9.0之后必须添加FLAG_ACTIVITY_NEW_TASK,否则会报错(基于android-32的源码,不同版本可能不同):
//以下代码基于android 12
public void startActivity(Intent intent, Bundle options) {
warnIfCallingFromSystemProcess();
final int targetSdkVersion = getApplicationInfo().targetSdkVersion;

//检测FLAG_ACTIVITY_NEW_TASK
if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0
&& (targetSdkVersion < Build.VERSION_CODES.N
|| targetSdkVersion >= Build.VERSION_CODES.P)
&& (options == null
|| ActivityOptions.fromBundle(options).getLaunchTaskId() == -1)) {
//未设置FLAG_ACTIVITY_NEW_TASK,直接抛出异常
throw new AndroidRuntimeException(
"Calling startActivity() from outside of an Activity "
+ " context requires the FLAG_ACTIVITY_NEW_TASK flag."
+ " Is this really what you want?");
}
//正常启动activity
mMainThread.getInstrumentation().execStartActivity(
getOuterContext(), mMainThread.getApplicationThread(), null,
(Activity) null, intent, -1, options);
}


注意:FLAG_ACTIVITY_NEW_TASK的设置效果受到taskAffinity以及其他一些配置的影响,实际使用过程中一定要进行充分测试,并且不同的android版本也会表现不同,极端场景下要仔细分析测试,选择最优方案;




提示:通过adb shell dumpsys activity activities命令可以查看activity任务栈;



6.多进程


正常情况下,app运行在以包名为进程名的进程中,其实android四大组件支持多进程,通过manifest配置process属性,可以指定与包名不同的进程名,即可运行在指定的进程中,从而开启多进程,那么,开启多进程有什么优缺点呢?


多进程下,可以分散内存占用,可以隔离进程,对于比较重的并且与其他模块关联不多的模块可以放在单独的进程中,从而分担主进程的压力,另外主进程和子进程不会相互影响,各自做各自的事,但开启了多进程后,也会带来一些麻烦事,比如会引起Application的多次创建,静态成员失效,文件共享等问题。


所以是否选择使用多进程要看实际需要,我们都知道app进程分配的内存是有限的,超过系统上限就会导致内存溢出,如果想要分配到更多的内存,多进程不失为一种解决方案,但是要注意规避或处理一些多进程引起的问题;


设置多进程的方式:

android:process=":childProcess" //实际上完整的进程名为:包名:childProcess,这种方式声明的属于私有进程。

android:process="com.child.process" //完整的进程名即为声明的名字:com.child.process,这种方式声明的属于全局进程。

7.excludeFromRecents


excludeFromRecents如果设置为true,那么设置的Activity将不会出现在最近任务列表中,如果这个Activity是整个Task的根Activity,整个Task将不会出现在最近任务列表中.


8.startActivityForResult被弃用


使用Activity Result Api代替,使用方式如下:

private val launcherActivity = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()) {
Log.e("code","resultCode = "+it.resultCode)
}

findViewById<Button>(R.id.btn_jump).setOnClickListener {
launcherActivity.launch(Intent(this@MainActivity,SecordActivity::class.java))
}

//要跳转的Activity设置回调数据:
val resultIntent = Intent()
resultIntent.putExtra("dataKey","data value")
setResult(1001,resultIntent)
finish()

关于registerForActivityResult更多请点击这里查看。


9.Deep link


简单理解,所谓Deep Link就是可以通过外部链接来启动app或者到达app指定页面的一想技术,比如可以通过点击短信或者网页中的链接来拉起app到指定页面,以达到提供日活或者其他目的,一般流程是可以通过在manifestactivity标签中配置固定的scheme来实现这种效果,形如:

<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:scheme="jumptest"
android:host="work"
android:port="8801"
android:path="/main"
/>
</intent-filter>

然后在网页中就可以通过如下方式来启动当前activity:

<a href="jumptest://work:8801/main?data=123456">你好</a>


格式 <scheme>://<host>:<port>/<path>?<query>



被启动的app可以通过如下方式拿到传递的参数以及schmea配置项:

val host = schemeIntent.data?.host
val path = schemeIntent.data?.path
val scheme = schemeIntent.data?.scheme
val query = schemeIntent.data?.query
Log.e("scheme","host = $host, path = $path, scheme = $scheme, query = $query")

结果:


image.png



注意:


1.intent-filter与Main主Activity搭配使用时,要单独开启一个intent-filter,否则匹配不到。

2.从android12开始,设置了intent-filter标签后,activity的exported必须设置成true,这个要注意(android12之前,其实添加了intent-filter,系统也会默认设置exported为true)。



①.app link


App link是一种特殊的Deep link,它的作用就是可以使通过网站地址打开app的时候,不需要用户选择使用哪个应用来打开,换种说法就是,我可以设置默认打开次地址的应用,这样一来,就可以直接引导到自己的app。


更多关于App link的可以参考这篇文章,或者看官网介绍


10.setResult和finish的顺序关系


通过startActivityForResult启动activity,通常会在被启动的activity的合适时机调用setResult来回调数据给上一个页面,然后当前页面返回的时候就会回调onActivityResult,这里要注意setResult的调用时机,请一定要在activity的finish()方法之前调用,否则可能不会生效(不会回调onActivityResult)


原因如下:

private void finish(int finishTask) {
if (mParent == null) {
int resultCode;
Intent resultData;
//会在finish的时候把回调数据赋值
synchronized (this) {
resultCode = mResultCode;
resultData = mResultData;
}
···
if (ActivityClient.getInstance().finishActivity(mToken, resultCode, resultData,
finishTask)) {
mFinished = true;
}
} else {
mParent.finishFromChild(this);
}
···
}

//setResult对mResultCode,mResultData赋值
public final void setResult(int resultCode) {
synchronized (this) {
mResultCode = resultCode;
mResultData = null;
}
}


由上述代码可以看出,setResult必须在finish之前赋值,才能够在finish的时候拿到需要callback的数据,以便在合适的时机回调onActivityResult


11.onSaveInstanceState()和onRestoreInstanceState()


activity在非正常情况被销毁的时候(非正常情况:横竖屏切换,系统配置发生变化,内存不足后台activity被回收等),当重新回到该activity,系统会重新实例化该对象,如果没有对页面输入的内容进行保存,就会存在内容丢失的情况,此时可以通过onSaveInstanceState来保存页面数据,在onCreate或者onRestoreInstanceState中对数据进行恢复,形如:

override fun onSaveInstanceState(outState: Bundle) {
outState.putString("SAVE_KEY","SAVE_DATA")
outState.putString("SAVE_KEY","SAVE_DATA2")
super.onSaveInstanceState(outState)
}
//需要判空,savedInstanceState不一定有值
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(null != savedInstanceState){
saveData = savedInstanceState.getString("SAVE_KEY") ?: ""
saveData2 = savedInstanceState.getString("SAVE_KEY2") ?: ""
}
setContentView(R.layout.activity_main)
}

//或者在onRestoreInstanceState恢复数据,无需判空,回调此方法一定有值
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
saveData = savedInstanceState.getString("SAVE_KEY") ?: ""
saveData2 = savedInstanceState.getString("SAVE_KEY2") ?: ""
super.onRestoreInstanceState(savedInstanceState)
}



注意:请使用onSaveInstanceState(outState: Bundle)一个参数的方法,两个参数的方法和ActivitypersistableMode有关。



本文主要对Activity重难点知识进行整理和解释,希望对大家有所帮助,当然难免存在错误,如有发现,希望指正,如果感觉不错,麻烦点个赞,这将给我持续更文以更大的动力,后续如有其他知识点,也会持续更新。


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

浅谈中间人攻击

之前聊了一些前端安全性的问题,好多小伙伴反馈说聊的东西比较浅,很轻松就搞懂了,但实际上,真正生产环境下的网络安全问题多数是由一种或者多种简单的攻击方式混合导致的,所以掌握好基本的网络安全原理还是很有必要的,今天来聊聊中间人攻击。 中间人攻击 所谓的中间人攻击一...
继续阅读 »

之前聊了一些前端安全性的问题,好多小伙伴反馈说聊的东西比较浅,很轻松就搞懂了,但实际上,真正生产环境下的网络安全问题多数是由一种或者多种简单的攻击方式混合导致的,所以掌握好基本的网络安全原理还是很有必要的,今天来聊聊中间人攻击。


中间人攻击


所谓的中间人攻击一般发生在双方通信的过程当中,使用技术手段对通信进行拦截,然后基于通信的中间进行信息的转发,让通信双方错以为双方是在一条私密的信道上进行通信,实际上,整个会话已经被中间人完全掌控,其中的信息自然也是一览无余。


image.png


发生中间人攻击,信息泄露是肯定的了,还可能发生信息的篡改,所以危害比较大。


中间人攻击分析


和其他的安全问题一样,中间人攻击吃准了通信的双方缺乏授信的手段,然后占据双方通信的信道。基于这样的思路,可以想到的中间人攻击策略:


1、wifi欺骗:这个实际上是实现起来难度最小的一种攻击方法,大概的方法是攻击者创建一个周围受信的wifi,比如周围商场或者饭店为名称的wifi,引诱受害者链接wifi访问网络,这个时候数据通过wifi进行访问,那么在wife端很容易可以监控到使用者的信息。


2、HTTPS欺骗:利用大家对https协议的信任,通过一些是是而非的网站,比如:apple.com和Apple.com,或者浏览器识别但是肉眼不识别的特殊字符,比如o、ο,一个是英文的o,一个是希腊字母的omicron,肉眼不可见,但是浏览器确实会区分。


3、SSL劫持:通过给通信一方下发假的(中间人的)证书来阶段通信双方通信,一般以伪造SSL证书来攻击。


4、DNS欺骗:好多小伙伴在进入特殊的内网环境,比如公司的办公网,可能会配置自己的dns问题,比如windows当中的hosts文件,DNS欺骗就是通过修改DNS服务器的解析信息,将要访问的域名解析到中间人的服务器上。


5、电子邮件劫持:这个是近年来听说最多的一种攻击方式(我们公司的邮箱也发生过),这种攻击更需要社会学的知识,比如,以公司财务的类似的邮箱地址发送退税等邮件,诱惑受害者点击攻击链接。


当然攻击方式还有很多,但是上面的5种是我们常见的攻击方式。


思考:


中间人攻击其实已经相当有危害性了,因为这个攻击的发起人在了解技术的同时,对受害人的一些信息也是很了解的,比如:社会关系,家庭住址,对中间人攻击的防御更多的是需要考虑到使用网络的谨慎:


1、不随便链接模式的wife。


2、不要忽略浏览器的安全警告,好多小伙伴完全不在意。


3、不访问一些不好的网站(嘿嘿嘿)


4、定期查看自己的网络访问情况。


5、不要把核心的个人隐私放到计算机的浏览器缓存当中,比如银行卡的支付密码。


今天聊的是纯粹的理论,还是欢迎各位大佬多多指点。


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

我是如何保障亿级用户系统五年0故障

我负责了我们公司几个非常大的平台系统,日均访问量超过了千万级别,用户过亿。其中某个推送系统每天的消息推送量是过亿级别的。尽管流量和用户都巨大,高峰期的请求也非常高,但是这五年来我们没有出现过任何恶性的线上影响用户的定级故障,那我们是怎么样做到的呢?里面有没有一...
继续阅读 »

我负责了我们公司几个非常大的平台系统,日均访问量超过了千万级别,用户过亿。其中某个推送系统每天的消息推送量是过亿级别的。尽管流量和用户都巨大,高峰期的请求也非常高,但是这五年来我们没有出现过任何恶性的线上影响用户的定级故障,那我们是怎么样做到的呢?里面有没有一些值得借鉴的方法可以供大家参考呢?


首先并不是说我们这个系统天生就是稳定的,任何人来维护都不会引发故障,而实际上我们在这五年中也迭代了大量的需求,期间也发生过或大或小的一些变更事故、代码事故,但是都被我们良好的机制应急保障的非常成功,所以没有继续上升成影响多数用户的恶性故障,那么我今天就来跟大家分享一下我是怎么做到的。



对于故障的认识


首先是对于故障稳定性的认知上面。我经历过很多故障,特别是刚开始毕业的时候,由于自己的经验不够成熟,对于系统的故障的认知不够全面,所以导致了一系列影响线上客户的问题,而有一些还被升级成了定级故障。


所以对于大流量高并发的系统来说,最首要就是要建立对系统故障的认知。一个页面一个人访问,100个人访问和1万个人访问,它的影响面是不同的。研发同学对于自己所构建的系统能够影响多少用户应该有一个清晰的评估。比如我现在维护的系统每天的访问量有几千万,所以它的稳定性是至关重要的,稍有不慎可能会引起大面积的用户不可用。


作为研发同学,一定要认识到故障对于用户的体感来说是非常恶劣的,我们这个职责本身就要求我们要敬畏线上进敬畏客户,特别是对于我们这种实时系统,一旦发生了问题,用户可用性就会被打断,有时候造成的影响甚至是无法挽回的。


因此,对故障的认知、对职业的认知,就要求我们不能心存侥幸、马马虎虎、粗糙编码和上线。我们实际上通过各种案例发现,很多一线的研发同学丝毫不尊重用户进而造成引起恶性的线上事故。比如未经测试直接上线、发布后不管不问系统监控情况、业务出现问题后无法联系到相关的开发同学等等。


稳定性治理机制


在完成了自己对于故障的影响面认知程度之外,现在就是到了我们重点环节,就是要建立一整套完整的制度来保障稳定性。



  • 大盘和监控


在整个稳定性的保障里面,我觉得监控和告警是最重要的,因为如果没有了监控和告警,就无异于盲人摸象,整个系统到底有什么问题,问题什么时候会发生。发生了以后是什么样的影响面都不知道的情况下的话,就等于一个瞎子。


所以在系统或者业务上线的时候,就要同时伴随着监控和大盘的上线,我们不允许一个新的模块上线却不存在对应的监控的情况。


一般来说整个监控体系本身应该是比较完善的,有硬件、软件和业务系统的监控指标。也有跟周期相关的大盘的监控指标,比如说和上周的同比,和昨天的同比等等。在很多时候还可以对中间件进行一系列完整的监控,比如说对于数据库的监控,对于缓存的监控,对于PC框架调用的监控等。


还有一些可以针对自己业务单个接口的监控,在一些比较特殊的情况下的话,还有针对关键字的监控,比如可以单独配置监控日志里的NullPoint,用来快速定位到某些具体的问题,目前开源的一些监控系统都具备了这种即时数据收集和展现的能力。


除了监控之外,还要配套的就是报警机制。如果系统出了问题,研发同学第一时间感知不到。监控就等于是白费的,同时根据故障的等级、接口的调用量,我们会配置不同等级的监控,比如说非常紧急的问题,会用电话的方式进行报警。稍微弱一点的可能用群或者用短信的方式进行报警。

【集团报警】[2022/12/28 02:26] mm-orchard-push[hsf消费者成功率]
[C] 共有1台机器[hsf消费者成功率]触发[CRITICAL]报警, 摘要:
* 3x.6x.5x.1xx 当前时间的值: 87.50% < 90%


租户: 应用监控,应用: mm-orchard-push
报警统计:首次触发

报警的通知对象一般是业务的负责人或者固定的值班告警群等。这种报警的目的是能够第一时间让应用的负责人能感知到故障,并且让业务或者应用负责人作为接口人,能快速地找到上下游进行应急处理。当然告警机制本身也是需要演练的,以防止通知机制由于各种原因失灵导致无法及时把问题同步给负责人。比如以前就发生过系统短信欠费导致开发负责人收不到短信的问题发生。



  • 日常值班


还有一个事前预防性的措施就是日常的值班,日常的值班也分了两种,一种是我们的早值班,早值班主要是在8点~10点,这一段时间可能大部分的开发同学都没有来到公司上班的时候,我们会要求至少有一位员工是在线上观察问题。这个观察问题可以是查看系统日志或者获取线上用户的投诉CASE。


这个机制的保障可以监控到一些时间错位的问题。比如我们昨天晚上的发布,客户流量比较少,没有触发用户投诉,到了第二天早上客户大量的访问系统而造成的不可用引起的投诉。早值班处理的问题也是一样,也就是要第一时间感知到故障的发生,能够进行快速的一个止血,突出的也是一个敏捷性。


其次就是我们日常的常规值班,我们产品发布后都会有一些的产品不可用的问题、产品难用的咨询以及线上非预期的问题,那么我们会以一个值班群的方式,让客户或者业务方或者合伙合作伙伴都拉到群里,有一些客户在发现了客系统不可用的时候,第一时间会把不可用的问题提到群内,我们在值班的时候就能够及时快速的去判断这个问题是否是变更引起的故障问题。


不管在早值班还是在日常的答疑群里面,我们碰到这些问题的话,都会评估是否有故障的风险,然后都会尽快的成立故障应急小组,执行相应的预案或者计划。



  • 演练压测


演练和压测是预防故障里面非常重要的一个步骤,也就是通过一些常规性的动作模拟用户的大量请求,可以帮助发现系统的漏洞,把系统的不完善的地方全部暴露出来。我们在压测和演练的时候,一般会选在流量低峰期,既能暴露问题,又不会大面积的影响线上的真实客户。


那为什么要频繁演练呢?那是因为我们整个互联网的系统都是经常会有迭代和更新的需求,当我们某一次演练系统没有问题之后,业务可能又发生了大量的变化,很有可能会有新的故障点或者风险点的注入,那么这个时候通过常规化的演练,就可以更早暴露问题。


我们压测和演练都常规化了,每个月至少执行一次压测或者一次演练,压测一般也会选择核心接口以及本个本代里面新增的重要业务接口。在压测期间,我们会关注到对于上下游的业务分的调用以及自身的性能压力,当压测到极限的时候,发现了当内存、CPU、数据库还是外部依赖的超时的时候,我们会停止压测并记录问题,并最终复盘问题,对于相关的不符合预期的问题就进行一个分析和治理。



  • 技术方案评审


对于如此大流量的系统,我们要求所有的稍微大一点的需求变更,我们都要走完整的技术方案评审。因为有时候一个不合理的架构设计会导致故障频繁并且难以根治,架构的优雅性决定了故障的底线是高是低。


技术方案评审除了对于整个业务的ROI(投入产出比)进行一个通晒和判断之外,我们还会要求技术方案有完整的稳定性方案。


这个稳定性的方案一方面是要求对于现有的技术选型,要评估它是否会引入直接的风险点,比如说我们引进了一些新的缓存系统,那么缓存系统的容量能不能符合要求?缓存系统对我们业务保障的SLA又在多少?


除了对于系统方案的调研之外,我们也要求要有配套的保障的监控体系,比如我们这次引入的业务迭代有没有相关的监控和大盘?


其次就是要有业务开关和灰度策略。我们要求所有的核心功能上线都必须要有开关和灰度的方式,能够充分降低业务风险。而实际上表明我们对于这么大流量的情况下的话,用灰度是非常好的一个方式,灰度实际上就是把整个新的功能暴露在一小批用户并且我们去验证这些小批用户的可用性。


我们很多时候都发现我们在刚刚灰都了一批用户的时候,就发现了异常,我们及时的就会回滚和修复,这样就避免了把所有的用户暴露在故障和不可用的功能里面。



  • 故障应急机制


没有完美的系统,哪怕你的代码编写的再好,你的测试再完善,都可能会有遇到一些突发情况。比如非预期的流量、比如底层的网络超时、比如硬盘故障等。


所以我们成立了故障的应急机制。不管是发生了系统的自动告警,还是用户投诉,我们值班的同学或者业务的负责人能够第一时间感知到这些错误,并且能够快速得升级,按照SOP流程成立应急小组并把故障风险上升到指定的层级。


应急小组的形式往往是一个钉钉群,在必要的时候,我们会直接呼起电话会议,把上下游和受影响的团队都会全部拉上,快速的进行一个故障的初步判断以及止血方案的沟通。


所以我们的应急消防要求的特点就是要敏捷,能够快速的对故障进行响应,因为你只要响应的时间提前一分钟止血,客户受影响的时间就短了一分钟。很多大型公司会有保障制度,比如在指定的时间内完成对故障的处理,可以直接降低故障等级,也体现了公司的文化和价值倡导,即出问题不可怕,如果能快速止血问题,就是值得鼓励的行为。


因此我们在整个部门里面也要求做到1-5-15,也就是1分钟感知到故障5分钟定位的问题15分钟解决问题。当然在实际的过程中很难对于所有的故障都做到1-5-15,但是这是我们系统治理要持续追求的目标。



  • 紧急预案


我们的一些核心功能在上线的时候,我们都要求有紧急的降级预案,比如说当我们上线的功能发现了极端不可用的情况下的话,能否快速的止血?比如我们的产品就有一个非常好的全局降级计划,就是我们的服务端接口或者我们依赖方发生了大规模不可用的情况下的话,我们有一个紧急预案就是可以一键降级的缓存,那么客户就能够直接访问他的客户端缓存,这样的话就给了我们留下了很多时间去检验和修复问题。


紧急预案包含有很多方式,比如对于某些接口设置限流,在无法快速解决问题的时候,可以通过限流来保护系统,尽量把影响面降到最低。



  • 复盘


最后就是故障复盘。我们不能期待我们所有的欲望都是完美无缺的,正如系统一样,我们对于故障的认识和故障的处理也是需要反复迭代升级的。我们要求和鼓励复盘文化,不仅仅对影响到真实用户的问题进行复盘,也对潜在的问题进行复盘。


总结


首先我觉得对于一个研发同学来说,故障可能长期来看是不可避免的,但是我们还是要提升自己的对于故障的认知观,因为我们给客户造成了不可用,就是在一定程度上研发工程师的价值,那么我们应该追求写出非常优异的代码,能够写出非常鲁棒的系统,以及在系统出现了不可预期的问题下我们快速的去恢复用户的体验。


最后也不能因噎废食,不能因为怕引起故障就逃避写代码,这相信也不是公司请我们来的原因。而应该大胆创新、小心试错,在出现问题的时候,积极主动响应和治理,并且持续复盘进步,这就是一名优秀的工程师所要追求的素养。




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

简单回顾5年职业生涯,混子前端也要持续前行

基本情况 我是95年前端小菜鸟一只,目前在深圳一家创业公司担任前端小组长,组内两个都是今年刚毕业的小伙,本身自己就是半桶水了,挺怕耽误别人发展的。   写这篇总结主要是回顾一下自己从大学毕业到工作5年来的一些成长经历和心路历程,希望在...
继续阅读 »

基本情况



我是95年前端小菜鸟一只,目前在深圳一家创业公司担任前端小组长,组内两个都是今年刚毕业的小伙,本身自己就是半桶水了,挺怕耽误别人发展的。 




 写这篇总结主要是回顾一下自己从大学毕业到工作5年来的一些成长经历和心路历程,希望在自我回顾的过程中面对真实的自己,继续找到前行的目标和动力。



(PS:不含技术内容,非常水,JYM可以提前溜)


 学习经历



我一直都是一个没什么长远目标的人,喜欢随遇而安,可能也是因为缺乏主见,在高中前两年极尽自由的干了很多想干的事情,逃自习上网打LOL,上课睡觉看小说等等。 第三年的时候感觉周围的人都很努力,在这个氛围下觉得确实也更专注,加上自己还有点小聪明,拼搏了一年,幸运的考上了本省的一所211大学读计科-网络-信安大类。2年基础2年专业课程,因为上了大学少了老师和父母的管教之后放飞自我,经常熬夜打游戏看小说,以至于挂了高数和大物,然后也没认真学到很深的东西,只是为了应付考试,基本没学到太多干货,基本是吃喝玩乐混,毕业前总是想着回5线老家考个公务员事业单位啥的躺平一下。



大学的时候接触过C、JAVA、Python这些技术,但是都仅限于简单的课设要求 



  • 大一的时候加入过学校的社团,接触过一些校园网的技术,写过一些简单的**HTML+CSS+JQ**的简单页面,就是首页轮播图和一些文章管理的,所以当时就埋下了往前端发展的种子吧。

  •  大三暑假那年出了点状况,改变了初衷,所以为了搞钱开始想到一线城市卷一卷。但是由于没有加入导师的实验室做项目,基本没有项目经验,课设作业很多也是抄宿舍大佬的。

  • 大四开学开始就开始整一些简历准备秋招,那会中软国际来学校招聘,当时也没怎么上网冲浪了解,不知道外包公司对未来职业发展影响这么大(后续补充),只是觉得别的校招笔试都挺难的,当时参加过广州多益网络,VIVO一些公司的笔试都没过,这个过了之后能到深圳并且有机会到华为本部工作,觉得是一个不错的机会,当时没有招聘前端的,只有C和JAVA的后端开发岗位,由于自己JAVA不行选择了C,被分配到华为的交换机框式嵌入式相关的开发部门,当时觉得自己反正也是小白,跟着学习应该是可以的,可是事实证明不是钻研的性格的话很难往下发展,都是后话了


 2017~2018




  • 17年2月春节收假后接到电话说节后要到深圳参加青训营,然后开始实习,去到那边之后和来自不同校招的同期大约100+人一起参加青训营,其实就是各业务线负责人讲一下公司的具体项目的运作流程,开发的开发/测试/上线/运维的一些相关内容,为期一周,然后一起去松山湖骑行玩了一天,就被分配导师带领到各自的业务线进行实习了。




  • 我起初是在东莞那边做交换机命令行的嵌入式开发,三方签的6.5K,实习2.5K不包吃住,所幸东莞租房便宜,单间基本500左右,华为的嵌入式开发慢慢迁移过松山湖那边了,机房需要场地大,在深圳成本高。




  • 我们这边基本是做华为的外包项目,不在华为本部,主要就是开发一些框式交换机的命令行和状态监控相关的一些东西,根据业务需求开发某个模块,由华为SE写文档,MDE细化,我们根据切分的业务模块成为小owner,随后拉相关的人开会澄清,确定周期之后进入开发,刚开始接触项目代码的时候,感觉头非常大,因为嵌入式C的开发和自己在校学习的差别巨大,很多钩子根本不懂是哪块代码引入的,而且因为我们是外包,核心的模块我们是没有权限查看代码的,只能够看的是自己业务模块的仓库,开始都是导师指导我们哪块有做过类似的,让我们把那个函数CV过去,改一些逻辑这样子,开始基本上一天才能写两个功能函数,提交代码的时候git也要由华为的人审核代码才能合入,然后由CI/CD集成生成的软件包,还要通过网口上传到设备,老一些的只能通过烧录的方式,所以开发快但是验证很麻烦;但是还是掌握了git的基本命令和团队开发的基本流程




  • 毕业后直到18年初每天基本都是加班到10点/11点的样子,感觉自己对于C和嵌入式的掌握还是不够,并且没有太大的发展兴趣,而且得知中软基本上招外包很少给涨薪,基本上第二年来的都是倒挂的,毕竟只需要你来做简单的功能模块开发,然后后续相同的类似功能都交给你,比较难/核心的需求基本上都是华为本部做,重复/机械的业务才会外包到中软/软通一类的公司。所以当时就谋划着润,可是又不太想继续搞嵌入式,所以就内部转岗积累一些前端的项目经验,当时面试原型链/闭包之类的都没答上,所以只能平薪到了一个部门做前端开发,当时项目组用的angular1和echarts开发,从18年3~12月就基本是在做中国铁塔某分公司的系统,重新开始做回前端,开始学习到ES6和一些组件开发的思想,彼时vue和react已经比较流行了,但是项目组没有使用,而自己也是比较佛系的人,下了班就是跟兄弟们开黑打本,当时沉迷DNF基本周末都在打乌龟卢克刷SS什么的,也错过了一段飞速发展期




  • 随着当初一起进中软的同学们一个个跳槽到其他公司,薪资都上了10K以上的时候,非常羡慕,但是整简历出去面试的时候,因为大部分公司都是用vue/react,所以当时面试情况不太乐观,决定停一段时间好好学习和复习面试,于是18年底选择了裸辞,当时在出租屋好好浪了一段时间推古剑,然后才开始学习vue,都是在网上一些课程和文档,然后git根据原项目整一些demo这样,然后去成都找了波发小,他校招进的中国工商银行,宇宙行要分配到成都当1年半电话客服,去那边玩了几天,逛了锦里,武侯祠,看了大熊猫,吃了好几顿火锅串串,成都真的是一个很美丽的城市,本来还想去川西玩玩,结果快过年就先回家了~ 其实想想成都还是一座蛮适合程序员发展的城市,和杭州一样是新兴的准一线




 2019~2022




  • 年后进行面试,因为中软的外包工作经历,过了面试也会被这个为由压薪资,说外包2年等于半年之类的,当时贼气,感觉如果有点选择的话刚毕业的同学们少选外包,除非钱给的足,而且基本上是入职即巅峰。




  • 19年就是进入现在这家创业公司,是做工业互联网相关的自研系统,终于薪资涨到了11K,不过是大小周,但是当时公司的技术栈是vue,而且有个5年的老哥带,所以就决定留下,开始上手时也因为都没怎么用过,element,axios,vuex以及很多客户定制的内容,所以那段时间也是飞速进步的一段时间,那位老哥其实vue的使用时间也不长,但是当时确实教了我不少关于vue的相关知识,包括vuex,指令等等。还是很感谢他的,入职后我就被老板让弄看板相关的开发,就是很多echarts图表展示,那段时间啃**echarts**文档很勤,也在社区找了很多custom的例子和特效来弄,痛并快乐着吧。结果7月的时候老哥和老板吵了一架,当场就和我交接了,然后我就莫名其妙成了唯一一个前端,开始整体维护公司的前端项目,包括后台PC,小程序(原生)这些,痛并快乐着,但是确实是磨练人,小公司的好处是一个人又当开发又当测试又兼UI,当时公司的项目也是外包某个公司用vueCLI+webpack3搭的,基本没有内部组件,是一个很粗糙的项目,当时感觉好多功能用element的模块不满足业务场景,包括大数据table和tree会导致卡顿等一系列问题,所以在论坛上找了pl-table,学习到了虚拟列表和可编辑表格的一些处理方式,才算慢慢走上了前端开发的正规,第一年挺潦草的度过了。




  • 20年过年时爆发了疫情,为了怕不能正常回深办公,2月底就提前回深了,3月在家办公了一个多月,当时基本是开会腾讯视频,然后根据客户定制的功能模块用墨刀画UI,然后自己进行开发,但是那段时间我们系统的定位不对,跟很多同行竞争中丢单,所以老板和合伙人也很焦急,随后我们老大(据说是TX10000以内工号的,但是感觉他之前做游戏的,主要用的C++,对现在系统用的Java微服务架构不了解,后来找前同事高级架构师给我们系统重新设计了架构,又进行了一波重构,然后老板又从IBM拉了一位资深顾问,带技术团队的来给我们重新设计了UI和交互,然后从20年底开始进入重构,本来顾问大佬问我能不能用AntDesign,由于当时对vue的支持不行,又不太想切换到react,所以就回绝了,又错失一个成长的机会。20年基本上因为已经习惯了现有的技术,基本上需求都是能够及时完成,所以又进入了舒适圈,除非遇到需要的时候才会主动去搜索相关的信息,所以导致了现在的焦虑与迷茫,这是后话了,可能自己一开始就不是一个合格的程序员吧。哦我们公司每年会给小涨薪1K,这年达到13K。




  • 21年由于重构项目需要扩充人手,我面试了一些人,招了个2年多的开发,和他一起用3个月完成了原系统的重构,这年因为很多客户需要定制开发和类excel表格录入,所以我们用了**handsonTable**这个三方完成,但是这个二次开发不像element等组件好改,基本上是要使用到JSX的语法,所以也“被迫”的跳出舒适区;因为需要打印,又学习了不少打印相关的三方组件,最终还是选择了无预览的**LODOP**,公司业务也开始好转,基本上能够达到盈亏平衡。此时薪资涨到14K,但是相比于同龄,不少同学多次跳槽之后已经有25K往上,虽然当前创业公司薪资少,但是确实有给到我一些股份,并且氛围相对轻松,所以暂时也还没有动的想法,但是感觉到技术已经在止步了,然后开始接触到掘金,看了同龄人的一些技术分享和B站的一些模拟面试,感觉自己和他们比在知识体系上差距有些大,不过彼时已经有了萌生退意的想法,想要回老家省会发展,然后年底考了一个公安厅的事业单位,笔试过了,3人进入面试,最终结构化面试没发挥好,比第一差了0.01分,无缘编制,就只能想想看怎么卷了。




  • 22年初立了一些flag,想要拼一拼大厂,但是组内另一个前端因为某个小程序新需求和老大吵了一大架,然后当天下午就和我交接了T-T,本来还想着他顶我,我好润的,然后今年经济下行的情况下,公司业务居然还有增长,导致了我又变得忙了起来,下班基本就10点了,完全没有学习的动力和精力,前半年就这么过了,6月的时候改了简历,想着去外面尝试一下看看行情如何,结果也一直没有去面试,可能也是还没下定决心改变吧,所以总想着拖延一下,加上公司在人员流失的情况下,为了稳住老员工,开始给我们按项目收款的比例获得奖金,所以薪资有了一些涨幅,所以感觉自己有点像处在温水里的青蛙,因为这个公司也创业4年了,目前看来市场还不够认可,只能算是维持在一个状态,但是自己也慢慢走到了成家立业的年龄,父母的身体也慢慢变差,疫情以来经历了外公的离世,以及好友父亲由于癌症离开,感到得过且过有点不太好,所以想要改变下现有的状态,不要到周末节假日就报复性熬夜,少玩游戏,少追剧,多花一些时间重塑自己的职业技能,好好掌握未熟练的浏览器、JS、工程化、TS、把行业新出的内容应用到现在的项目中,提高开发效率的同时也能进行学校提升。




 回顾总结



回看自己这5年,其实大多数时候还是缺乏思考,没有做好职业规划,所以目前虽然年限到了但是感觉技术水平远没有达到同龄人的水准,当然可能与工作中虽然有记笔记,但是没有整理总结输出,所以学习效果不佳吧。 也可能是大家都比较卷吧,毕竟行业如此,没有人能独善其身,既然决定往下走,那么也只能把缺失的东西找补回来,最近在看《认知觉醒》,里面提到提到人多数时候被本能和情绪控制习惯于做简单不思考的事,缺乏耐心坚持长期有益的事情,颇有感悟,写下此篇回顾也是为了直面自己的过去,然后好好复盘与修正不足,为了变成更好的自己提供一些借鉴,加油吧!


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

再学http-为什么文件上传要转成Base64?

web
1 前言 最近在开发中遇到文件上传采用Base64的方式上传,记得以前刚开始学http上传文件的时候,都是通过content-type为multipart/form-data方式直接上传二进制文件,我们知道都通过网络传输最终只能传输二进制流,所以毫无疑问他们本...
继续阅读 »

1 前言


最近在开发中遇到文件上传采用Base64的方式上传,记得以前刚开始学http上传文件的时候,都是通过content-type为multipart/form-data方式直接上传二进制文件,我们知道都通过网络传输最终只能传输二进制流,所以毫无疑问他们本质上都是一样的,那么为什么还要先转成Base64呢?这两种方式有什么区别?带着这样的疑问我们一起来分析下。


2 multipart/form-data上传


先来看看multipart/form-data的方式,我在本地通过一个简单的例子来查看http multipart/form-data方式的文件上传,html代码如下


<!DOCTYPE html>
<html>
<head>
<title>上传文件示例</title>
<meta charset="UTF-8">
<body>
<h1>上传文件示例</h1>
<form action="/upload" method="POST" enctype="multipart/form-data">
<label for="file">选择文件:</label>
<input type="file" id="file" name="file"><br>
<label for="tx">说明:</label>
<input type="text" id="tx" name="remark"><br><br>
<input type="submit" value="上传">
</form>
</body>
</html>

页面展示也比较简单


image.png


选择文件点击上传后,通过edge浏览器f12进入调试模式查看到的请求信息。

请求头如下
image.png
在请求头里Content-Type 为 multipart/form-data; boundary=----WebKitFormBoundary4TaNXEII3UbH8VKo,刚开始看肯定有点懵,不过其实也不复杂,可以简单理解为在请求体里要传递的参数被分为多部份,每一部分通过分解符boundary分割,就比如在这个例子,表单里有file和remark两个字段,则在请求体里就被分为两部分,每一部分通过boundary=----WebKitFormBoundary4TaNXEII3UbH8VKo来分隔(实际上还要加上CRLF回车换行符,回车表示将光标移动到当前行的开头,换行表示一行文本的结束,也就是新文本行的开始)。需要注意下当最后一部分结尾时需要加多两个"-"结尾。

我们继续来看请求体


image.png
第一部分是file字段部分,它的Content-Type为image/png,第二部分为remark字段部分,它没有声明Content-Type,则默认为text/plain纯文本类型,也就是在例子中输入的“测试”,到这里大家肯定会有个疑问,上传的图片是放在哪里的,这里怎么没看到呢?别急,我猜测是浏览器做了特殊处理,请求体里不显示二进制流,我们通过Filder抓包工具来验证下。


image.png
可以看到在第一部分有一串乱码显示,这是因为图片是二进制文件,显示成文本格式自然就乱码了,这也证实了二进制文件也是放在请求体里。后端使用框架springboot通过MultipartFile接受文件也是解析请求体的每一部分最终拿到二进制流。


@RestController
public class FileController {
// @RequestParam可接收Content-Type 类型为:multipart/form-data 
// 或 application/x-www-form-urlencoded 请求体的内容
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
return "test";
}
}

到此multipart/form-data方式上传文件就分析完了,关于multipart/form-data官方说明可参考 RFC 7578 - Returning Values from Forms: multipart/form-data (ietf.org)


3 Base64上传


在http的请求方式中,文件上传只能通过multipart/form-data的方式上传,这样一来就会有比较大的限制,那有没其他方式可以突破这一限制,也就是说我可以通过其他的请求方式上传,比如application/json?当然有,把文件当成一个字符串,和其他普通参数没什么两样,我们可以通过其他任意请求方式上传。如果转成了字符串,那上传文件就比较简单了,但问题是我们怎么把二进制流转成字符串,因为这里面可能会有很多“坑”,业界一般的做法是通过Base64编码把二进制流转成字符串,那为什么不直接转成字符串而要先通过Base64来转呢?我们下面来分析下。


3.1 Base64编码原理


在分析原理之前,我们先来回答什么是Base64编码?首先我们要知道Base64只是一种编码方式,并不是加解密算法,因此Base64可以编码,那也可以解码,它只是按照某种编码规则把一些不可显示字符转成可显示字符。这种规则的原理是把要编码字符的二进制数每6位分为一组,每一组二进制数可对应Base64编码的可打印字符,因为一个字符要用一个字节显示,那么每一组6位Base64编码都要在前面补充两个0,因此总长度比编码前多了(2/6) = 1/3,因为6和8最小公倍数是24,所以要编码成Base64对字节数的要求是3的倍数(24/8=3字节),对于不足字节的需要在后面补充字节数,补充多少个字节就用多少个"="表示(一个或两个),这么说有点抽象,我们通过下面的例子来说明。

我们对ASCII码字符串"AB\nC"(\n和LF都代表换行)进行Base64编码,因为一共4字节,为了满足是3的倍数需要扩展到6个字节,后面补充了2个字节。


image.png


表3.1


转成二级制后每6位一组对应不同颜色,每6位前面补充两个0组成一个字节,最终Base64编码字符是QUIKQw==,Base64编码表大家可以自行网上搜索查看。


image.png
我们通过运行程序来验证下


image.png
最终得出的结果与我们上面推理的一样。


3.2 Base64编码的作用


在聊完原理之后,我们继续来探讨文件上传为什么要先通过Base64编码转成字符串而不直接转成字符串?一些系统对特殊的字符可能存在限制或者说会被当做特殊含义来处理,直接转成普通字符串可能会失真,因此上传文件要先转成Base64编码字符,不能把二进制流直接字符串。


另外,相比较multipart/form-data Base64编码文件上传比较灵活,它不受请求类型的限制,可以是任何请求类型,因为最终就是一串字符串,相当于请求的一个参数字段,它不像二进制流只能限定multipart/form-data的请求方式,日常开发中,我们用的比较多的是通过apllication/json的格式把文件字段放到请求体,这种方式提供了比较便利的可操作性。


4 总结


本文最后再来总结对比下这两种文件上传的方式优缺点。

(1)multipart/form-data可以传输二进制流,效率较高,Base64需要编码解码,会耗费一定的性能,效率较低。

(2)Base64不受请求方式的限制,灵活度高,http文件二进制流方式传输只能通过multipart/form-data的方式,灵活度低。

因为随着机器性能的提升,小文件通过二进制流传输和字符串传输,我们对这两种方式时间延迟的感知差异并不那么明显,因此大部分情况下我们更多考虑的是灵活性,所以采用Base64编码的情况也就比较多。


作者:初心不改_1
来源:juejin.cn/post/7251131990438264889
收起阅读 »

Web的攻击技术: 别让我看到你网站的缺陷,不然你看我打不打你🍗🍗🍗

简单的 HTTP 协议本身并不存在安全性问题,因此协议本身几乎不会成为攻击的对象。应用 HTTP 协议的服务器和客户端,已经运行在服务器上的 Web 应用等资源才是攻击目标。 在客户端即可篡改请求 在 Web 应用中,从浏览器那接收到的 HTTP 请求的全部内...
继续阅读 »

简单的 HTTP 协议本身并不存在安全性问题,因此协议本身几乎不会成为攻击的对象。应用 HTTP 协议的服务器和客户端,已经运行在服务器上的 Web 应用等资源才是攻击目标。


在客户端即可篡改请求


Web 应用中,从浏览器那接收到的 HTTP 请求的全部内容,都可以在客户端自由地变更、篡改。所以 Web 应用可能会接收到与预期数据不相同的内容。


HTTP 请求报文内加载攻击代码,就能立发起对 Web 应用的攻击,通过 URL 查询字段或表单、HTTP 首部、Cookie 等途径吧攻击代码传入,若这时 Web 应用存在安全漏洞,那内部信息就会遭到窃取,或被攻击者拿到管理权限。
20230701072440


针对 Web 应用的攻击模式


Web 应用的攻击模式有以下两种:



  • 主动攻击;

  • 被动攻击;


以服务器为目标的主动攻击


主动攻击是指攻击者通过直接访问 Web 应用,把攻击代码传入的模式,由于该模式是直接针对服务器上的资源进行攻击,因此攻击者能够访问到那些资源。


主动攻击模式里面具有代表性的攻击是 SQL 注入攻击和 OS 命令注入攻击。


20230701072818


以服务器为目标的被动攻击


被动攻击是指利用圈套策略执行攻击代码的攻击模式,在被动攻击过程中,攻击者不直接对目标 Web 应用访问发起攻击。


被动攻击通常的攻击模式如下所示:



  1. 攻击者诱使用户触发已设置好的陷阱,而陷阱会启动发送已嵌入攻击代码的 HTTP 请求;

  2. 当用户不知不觉中招之后,用户的浏览器或邮件客户端会触发这个陷阱;

  3. 中招后的用户浏览器会把含有攻击代码的 HTTP 请求发送给作为攻击目标的 Web 应用,运行攻击代码;

  4. 执行完攻击代码,存在安全漏洞的 Web 应用会成为攻击者的跳板,可能导致用户所持的 Cookie 等个人信息被窃取,登录状态中的用户权限遭恶意滥用等后果。


被动攻击模式中具有代表性的攻击是跨站脚本攻击和跨站点请求伪造。
20230701074804


利用被动攻击,可发起对原本从互联网上无法直接访问的企业内网等网络的攻击。只要用户踏入攻击者预先设好的陷阱,在用户能够访问到的网络范围内,即使是企业内网也同样会受到攻击。


20230701075024


因输出值转移不完全引发的安全漏洞


实施 Web 应用的安全对策可大致分为以下两部分:



  • 客户端的验证;

  • Web 应用端的验证:

    • 输入值验证;

    • 输出值转义;




20230701075829


因为 JavaScript 代码可以在客户端随便修改或者删除,所以不适合将 JavaScript 验证作为安全的方法策略。保留客户端验证只是为了尽早地辨识输入错误,起到提高 UI 体验的作用。


输入值验证通常是指检查是否是符合系统业务逻辑的数值或检查字符编码等预防对策。


从数据库或文件系统、HTML、邮件等输出 Web 应用处理的数据之际,针对输出做值转义处理是一项至关重要的安全策略。当输出值转义不完全时,会因触发攻击者传入的攻击代码,而给输出对象带来损害。


跨站脚本攻击


跨站脚本攻击(Cross-Site Scripting,XSS)是指通过存在安全漏洞的 Web 网站注册用户的浏览器内运行非法的 HTML 标签或 JavaScript 进行的一种攻击。动态创建的 HTML 部分有可能隐藏这安全漏洞。就这样,攻击者编写脚本设下陷阱,用户在自己的浏览器上运行时,一不小心就会受到被动攻击。


跨站脚本攻击有可能造成一下影响:



  • 利用虚假输入表单骗取用户个人信息;

  • 利用脚本窃取用户的 Cookie 值,被害者在不知情的情况下,帮助攻击者发送恶意请求;

  • 显示伪造的文章或图片;


跨站脚本攻击案例


在动态生成 HTML 处发生


下面以编辑个人信息页面为例讲解跨站脚本攻击,下放界面显示了用户输入的个人信息内容:
20230701090312


确认姐妹按原样显示在编辑解密输入的字符串。此处输入带有山口伊朗这样的 HTML 标签的字符串。


那如果我把输入的内容换成一段 JavaScript 代码呢,阁下又该如何应对?


<script>
alert("落霞与孤鹜齐飞,秋水共长天一色!");
</script>

XSS 是攻击者利用预先设置的陷阱触发的被动攻击


跨站脚本攻击属于被动攻击模式,因此攻击者会事先布置好用于攻击的陷阱。


下图网站通过地址栏中 URI 的查询字段指定 ID,即相当于在表单内自动填写字符串的功能。而就在这个地方,隐藏着可执行跨站脚本攻击的漏洞。
20230701091251


充分熟知此处漏洞特点的攻击者,于是就创建了下面这段嵌入恶意代码的 URL。并隐藏植入事先准备好的欺诈邮件中或 Web 页面内,诱使用户去点击该 URL


浏览器打开该 URI 后,直观感觉没有发生任何变化,但设置好的脚本却偷偷开始运行了。当用户在表单内输入 ID 和密码之后,就会直接发送到攻击者的网站,导致个人登录信息被窃取。


之后,ID 及密码会传给该正规网站,而接下来仍然是按正常登录步骤,用户很难意识到自己的登录信息已遭泄露。


除了在表单中设下圈套之外,下面那种恶意构造的脚本统一能够通过跨站脚本攻击的方式,窃取到用户的
Cookie 信息:


const cookie = document.cookie;

执行上面这段 JavaScript 程序,即可访问到该 Web 应用所处域名下的 Cookie 信息,然后这些信息会发送至攻击者的 Web 网站,记录在它的登录日志中,攻击者就这样窃取到用户的 cookie 信息了。


React 中通过 JSX 语法转义来防止 XSS。在 JSX 语法中,可以通过花括号 {} 插入 JavaScript 表达式。JSX 语法会自动转义被插入到 HTML 标签之间的内容。这意味着任何用户输入的内容都会被转义,以防止恶意脚本的执行。在转义过程中,React 会将特殊字符进行转换,例如将小于号 < 转义为 <、大于号 > 转义为 >、引号 " 转义为 " 等。这样可以确保在渲染时,用户输入的内容被当作纯文本处理,而不会被解析为 HTML 标签或 JavaScript 代码。


SQL 注入攻击


会执行非法 SQL 的 SQL 注入攻击


SQL 注入是指针对 Web 应用使用的数据库,通过运行非法的 SQL 而产生的攻击。该安全隐患有可能引发极大的威胁,有时会直接导致个人信息及机密信息的泄露。


SQL 注入攻击有可能会造成以下等影响:



  • 非法查看或篡改数据库内的数据;

  • 规避认证;

  • 执行和数据库服务器业务关联的程序等;


SQL 注入攻击案例



  • 登录绕过攻击: 攻击者可以在登录表单的用户名和密码字段中插入恶意的 SQL 代码。如果应用程序未对输入进行正确的验证和过滤,攻击者可以通过在用户名字段中输入 ' OR '1'='1 的恶意输入来绕过登录验证,使得 SQL 语句变为:SELECT \* FROM users WHERE username = '' OR '1'='1' AND password = '<输入的密码>',从而成功登录到系统中;

  • 删除或修改数据攻击: 攻击者可以通过注入恶意的 SQL 代码来删除或修改数据库中的数据。例如,攻击者可以在一个表单的输入字段中插入 '; DROP TABLE users;-- 的恶意输入。如果应用程序未正确处理这个输入,攻击者可以成功删除用户表(假设表名为 "users")导致数据丢失;


OS 命令注入攻击


OS 命令注入攻击是指通过 Web 应用,执行非法的操作系统命令达到攻击的目的。只要在能调用 Shell 函数的地方就有存在被攻击的风险。


可以从 Web 应用中通过 Shell 来调用操作系统命令,如果调用 Shell 是存在疏漏,就可以执行插入的非法 OS 命令。


OS 命令注入攻击可以向 Shell 发送命令,让 WindowsLinux 操作系统的命令行启动程序。也就是说,通过 OS 注入攻击可执行 OS 上安装着的各种程序。


OS 注入攻击案例



  • 文件操作攻击: 攻击者可以在应用程序的输入字段中插入恶意的操作系统命令来执行文件操作。例如,如果应用程序在文件上传过程中未对输入进行适当的验证和过滤,攻击者可以在文件名字段中插入 '; rm -rf / ;-- 的恶意输入。如果应用程序在执行文件操作时没有正确处理这个输入,攻击者可能会删除服务器上的所有文件;

  • 远程命令执行攻击: 攻击者可以通过注入恶意的操作系统命令来执行远程系统命令。例如,如果应用程序在一个输入字段中插入 ; ping <恶意 IP 地址> ; 的恶意输入,而没有进行正确的输入验证和过滤,攻击者可以利用该注入漏洞执行远程命令,对目标系统进行攻击或探测;


HTTP 首部注入攻击


HTTP 首部注入攻击是指攻击者通过在响应首部字段内插入换行,添加任意响应首部或主体的一种攻击。属于被动攻击模式。


HTTP 首部注入攻击有可能造成以下一些影响:



  • 设置任何 Cookie 信息;

  • 重定向值任意 URL;

  • 显示任意的主体;


HTTP 首部注入攻击案例


以下是一些 HTTP 首部注入攻击的案例,展示了攻击者是如何利用该漏洞进行攻击的:



  • 重定向攻击: 攻击者可以在 HTTP 响应的 Location 首部中插入恶意 URL,从而将用户重定向到恶意网站或欺骗性的页面。如果应用程序未正确验证和过滤用户输入,并将其直接用作 Location 首部的值,攻击者可以在 URL 中插入换行符和其他特殊字符,添加额外的首部字段,导致用户被重定向到意外的位置;

  • 缓存投毒攻击: 攻击者可以在 HTTP 响应的 Cache-Control 或其他缓存相关首部中插入恶意指令,以欺骗缓存服务器或浏览器,导致缓存数据的污染或泄漏。攻击者可以通过注入换行符等特殊字符来添加额外的首部字段或修改缓存指令,绕过缓存机制或引发信息泄露;

  • HTTP 劫持攻击: 攻击者可以在 HTTP 响应的 LocationRefresh 首部中插入恶意 URL,将用户重定向到恶意网站或欺骗性页面,从而劫持用户的会话或执行其他攻击。通过在响应中插入恶意的 LocationRefresh 值,攻击者可以修改用户的浏览器行为;

  • XSS 攻击: 攻击者可以在 HTTP 响应的 Set-Cookie 或其他首部字段中插入恶意脚本,以执行跨站脚本攻击。如果应用程序未正确过滤和转义用户输入,并将其插入到首部字段中,攻击者可以通过注入恶意代码来窃取用户的会话标识符或执行其他恶意操作;


因会话管理疏忽引发的安全漏洞


会话管理是用来管理用户状态的必备功能,但是如果在会话管理上有所疏忽,就会导致用户的认证状态被窃取等后果。


会话劫持


会话劫持是指攻击者通过某种手段拿到了用户的会话 ID,并非法使用此会话 ID 伪装成用户,达到攻击的目的:
20230701104716


具备人中功能的 Web 应用,使用会话 ID 的会话管理机制,作为管理认证状态的主流方式。会话 ID 中记录客户端的 Cookie 等信息,服务端将会话 ID 与认证状态进行一对一匹配管理。


下面列举了几种攻击者可获得会话 ID 的途径:



  • 通过非正规的生成方法推测会话 ID;

  • 通过窃听或 XSS 攻击盗取会话 ID;

  • 通过会话固定攻击强行获取会话 ID;


会话劫持攻击案例


下面我们以认证功能为例讲解会话劫持。这里的认证功能通过会话管理机制,会将成功认证的用户的会话 ID,保存在用户浏览器的 Cookie 中。
20230701105419


攻击者在得知该 Web 网站存在可跨站攻击的安全漏洞后,就设置好用 JavaScript 调用 document.cookie 以窃取 cookie 信息的陷阱,一旦用户踏入陷阱访问了该脚本,攻击者就能获取含有会话的 IDCookie


攻击者拿到用户的会话 ID 后,往自己的浏览器的 Cookie 中设置该会话 ID,即可伪装成会话 ID 遭窃的用户,访问 Web 网站了。


会话固定攻击


对一切去目标会话 ID 为主动攻击手段的会话劫持而言,会话固定攻击 攻击会强制用户使用指定的会话 ID,属于被动攻击。


会话固定攻击案例


下面我们以认证功能为例讲解会话固定攻击,这个 Web 网站的认证功能,会在认证前发布一个会话 ID,若认证成功,就会在服务器内改变认证状态。


20230701152056


攻击者准备陷阱,先访问 Web 网站拿到会话 ID,此刻,会话 ID 在服务器上的记录仪仍是未认证状态。


攻击者设计好强制用户使用该会话 ID 的陷阱,并等待用户拿着这个会话 ID 前去认证,一旦用户触发陷阱并完成认证,会话 ID 服务器上的状态(用户 A 已认证) 就会被记录下来。


攻击者估计用户差不多已触发陷阱后,再利用之前这个会话 ID 访问网站,由于该会话 ID 目前已是用户 A 已认证状态,于是攻击者作为用户 A 的身份顺利登陆网站。


会话固定攻击预防措施


会话固定攻击利用了应用程序在身份验证和会话管理过程中未正确处理会话标识符的漏洞。为了防止会话固定攻击,开发人员可以采取以下措施:



  1. 生成随机、唯一的会话标识符,并在用户每次登录或创建新会话时重新生成;

  2. 不接受用户提供的会话标识符,而是通过服务器生成并返回给客户端;

  3. 在身份验证之前和之后,对会话标识符进行适当的验证和验证机制;

  4. 设置会话管理策略,包括会话超时时间和注销会话的方式;


通过采取这些预防措施,可以减少会话固定攻击的风险,并提高应用程序的安全性。


跨站点请求伪造


跨站点伪造请求(Cross-Site Require Forgeries,CSRF)攻击是指攻击者通过设置好的陷阱,强制用户对已完成认证的用户进行非预期的个人信息或设定信息等某些状态更新,属于被动攻击。


跨站点请求伪造有可能会造成一下等影响:



  • 利用已通过认证的用户权限更新设定信息等;

  • 利用已通过认证的用户权限购买商品;

  • 利用已通过认证的用户权限在评论区发表言论;


跨站点伪造请求的攻击案例


下面以一个网站的登录访问功能为例,讲解跨站点请求伪造,如下图所示:
20230701160321



  1. 当用户输入账号信息请求登录 A 网站;

  2. A 网站验证用户信息,通过验证后返回给用户一个 cookie;

  3. 在未退出网站 A 之前,在同一浏览器中请求了黑客构造的恶意网站 B;

  4. B 网站收到用户请求后返回攻击性代码,构造访问 A 网站的语句;

  5. 浏览器收到攻击性代码后,在用户不知情的情况下携带 cookie 信息请求了 A 网站。此时 A 网站不知道这是由 B 发起的。那么这时黑客就可以为所欲为了!!!


这首先必须瞒住两个条件:



  • 用户访问站点 A 并产生了 Cookie;

  • 用户没有退出 A 网站同时访问了 B 网站;


CSRF 攻击的防御


当涉及到跨站伪造请求的防御时,一下是一些防御方法和实践:




  1. 验证来源和引用检查:



    • 服务器端应该验证每个请求的来源 Referer 字段和源 Origin 字段以确保请求来自预期的域名或网站。如果来源不匹配,服务器应该拒绝请求。Referer 头部并不是 100% 可靠,因为某些浏览器或网络代理可能会禁用或篡改该字段。因此,Origin 字段被认为是更可靠的验证来源的方式;




  2. CSRF Token:



    CSRF 令牌是一个随机生成的值,嵌入到表单或请求参数中,与用户会话相关联。它的目的是验证请求的合法性,确保请求是由预期的用户发起的,而不是由攻击者伪造的。




    • 在每个表单和敏感操作的请求中,包括一个 CSRF 令牌;

    • 令牌可以作为隐藏字段 input type="hidden" 或请求头,例如 X-CSRF-Token 的一部分发送;

    • 在服务器端,验证请求中的令牌是否与用户会话中的令牌匹配,以确保请求的合法性;




  3. 验证请求的方法: 某些敏感操作应该使用 POSTPUTDELETE 等非幂等方法,而不是 GET 请求。这样可以防止攻击者通过构造图片或链接等 GET 请求来触发敏感操作;




  4. 敏感操作的二次确认: 对于一些敏感操作,例如修改密码、删除账户等,可以在用户执行操作前要求二次确认,以确保用户的意图和授权;




综合采取上述防御措施,可以有效减少跨站伪造请求攻击的风险。然而,没有单一的解决方案可以完全消除跨站伪造请求的威胁,因此建议在开发过程中将安全性作为一个关键考虑因素,并进行全面的安全测试和审查。


参考文献


书籍: 图解HTTP


总结


没有总结,总结个屁,不上网就是

作者:Moment
来源:juejin.cn/post/7251158799318057015
最安全的......

收起阅读 »

程序员的努力有意义吗?

最近,在小灰的知识星球上,有个小伙伴问了一个蛮有意思的问题: 这个问题看起来有些复杂,其实可以归纳为一句话: IT技术更新换代很快,如果我们花费很多年去学习技术,有一天旧技术被淘汰,新技术成为主流,那我们是不是就白学了?我们程序员的努力还有什么意义呢? 不得...
继续阅读 »

最近,在小灰的知识星球上,有个小伙伴问了一个蛮有意思的问题:



这个问题看起来有些复杂,其实可以归纳为一句话:


IT技术更新换代很快,如果我们花费很多年去学习技术,有一天旧技术被淘汰,新技术成为主流,那我们是不是就白学了?我们程序员的努力还有什么意义呢?


不得不说,这个问题困扰着很多程序员,小灰自己也常常在思考。


那么,程序员该不该努力钻研技术的?今天小灰来说一说自己的想法。


先说结论,程序员的努力当然是有效的。


无论是程序员,还是其他大多数凭本事吃饭的职业,个人的成就主要取决于四个因素:努力选择天赋运气。其中天赋和运气是不可控的,因此我们这里只谈论努力和选择。


那么,我们应该选择什么样的方向去努力钻研呢?


在职场上,我们需要掌握的技能有三类:一类是应用技能,一类是底层技能,一类是通用技能


应用技能,就是可以直接用来做事情赚钱的技能,比如Go语言、MySQL技术、Spring框架等等。掌握了这些技能,你可以开发项目,在近期为公司创造价值。但是,这些技术难免会有时效性,很可能过一段时间就不再流行。


举个例子,十几年前兴盛一时的Delphi语言,因为市场的转变,现在已经很少有人使用了。


底层技能,对于程序员行业来说,包括操作系统原理、算法与数据结构,设计模式、架构理论等等。这些技能在短时间内无法让你快速提高生产力,但是却可以让你在职业发展的中长期受益。而且,这些技能的有效期很长,在可见的未来,在程序员的各个细分领域里,一直都有用。


举个例子,无论你是做Java,做Python,做C++,亦或是做前端,算法和数据结构的理念都是互通的。


通用技能,包括沟通能力,情绪控制能力,团队管理能力等等。这些技能不仅对程序员有用,哪怕有一天你不当程序员了,甚至你退休以后,都能给你的工作和生活带来一定的好处。


比如,当你从程序员转行做了产品经理,沟通能力依然能派上用场;当你谈恋爱结婚,未来教育孩子的时候,情绪控制能力同样可以派上用场。


因此,我们在选择某一方向去努力的时候,切记不要一味追逐流行的新技术,那样只会让我们疲于奔命。我们需要在学习应用技能的同时,不断加深底层技能和通用技能的提升,为更远的将来打好基础,全方位进行提升。


当你长期坚持在正确的方向上努力,或许一年两年看不出效果,但经过五年十年,你和同行的能力差距会变得非常显著,而且不会因为技术的更新换代而改变。


比如说,你用Go语言工作了10年,有一天Go语言没人用了,Come语言成了主流语言。那你损失的只是这一项应用技能,而你这些年的底层技能、通用技能并没有白积累。


当你和职场新人一起学习Come语言的时候,你大概率比他学习得更快,因为编程技术之间多少会有一些相通性。


再加上你的算法和设计功底,良好的沟通和管理能力,你一定比新人更有价值。


不过话又说回来,随着人的年龄增长,你一定会有家庭的牵绊、体力的下降等问题,让你在职场上的竞争力有所下降。但这些和工作能力没有关系,并不在我们今天的讨论范围内。


程序员的努力有意义吗?


选择正确的方向,兼顾应用技能、底层技能、通用技能的提升,那我们的努力就必然是有意义的。


小灰的回答就到这里,如果这篇文章对你有所

作者:程序员小灰
来源:juejin.cn/post/7251501954157215800
帮助,欢迎点赞哦~~

收起阅读 »

API接口对于企业数字化转型有哪些重要意义?

当前,有很多企业都在加速进行数字化转型,随着AI等新技术的不断发展,企业的交付和数据管理能力已经无法满足需求。以数据为例,不少企业内部存在数据难题无法解决,一方面数据孤岛使得数据分散且多个业务系统之间难以打通,业务衔接不够顺畅,导致用户使用系统会更加复杂,数字...
继续阅读 »

当前,有很多企业都在加速进行数字化转型,随着AI等新技术的不断发展,企业的交付和数据管理能力已经无法满足需求。以数据为例,不少企业内部存在数据难题无法解决,一方面数据孤岛使得数据分散且多个业务系统之间难以打通,业务衔接不够顺畅,导致用户使用系统会更加复杂,数字化作用失效;另一方面数据共享开放的需求明显,但是数据安全无法保障,部分企业客户倾向于线下提供数据进行共享,会导致数据无法实时更新而且无法控制数据的流向。这两大问题既无法提高企业的业务效能,也影响到数据的安全。因此企业迫切需要进行数字化转型,才能跟上数字化浪潮的发展。

这其中最关键的方式就是使用API接口服务,那么API是如何赋能企业数字化转型的呢?


首先,不少企业内部都储存了海量的高价值数据,API能够帮助打破数据孤岛怪圈,让数据得到有效利用,开发人员能自由访问、组合数字资产,最终实现整体的协同效果,企业数据管理环境的复杂性得到了解决。
其次,API可以构造多个业务相关的接口服务与交付,企业的交付周期大大缩短,同时因为减少了代码量,开发效率得到有效提升,企业内部快速实现了降本增效。
最后,API开放平台能够实现IT资产和运维可视化以及IT资产的安全管控, 促进生态系统的形成,同时开发人员能够更方便地进行实验,创新并响应不断变化的客户需求。

数聚变平台打造了一个深耕新能源领域的API生态平台,目前已经覆盖了数据采集转发、数据集成共享、数据要素开放流通、企业数字化咨询和API全生命周期管理等多个功能模块,有效助力企业实现数字化转型。

收起阅读 »

Flutter如何实现IOC与AOP

在Flutter中实现IOC(Inversion of Control,控制反转)与AOP(Aspect-Oriented Programming,面向切面编程)之前,让我们先来了解一下这两个概念。 IOC(控制反转) 是一种设计原则,它将应用程序的控制权从应...
继续阅读 »

在Flutter中实现IOC(Inversion of Control,控制反转)与AOP(Aspect-Oriented Programming,面向切面编程)之前,让我们先来了解一下这两个概念。


IOC(控制反转) 是一种设计原则,它将应用程序的控制权从应用程序本身转移到外部框架或容器。传统上,应用程序会自己创建和管理对象之间的依赖关系。而在IOC中,对象的创建和管理被委托给一个专门的框架或容器。框架负责创建和注入对象,以实现松耦合和可扩展的架构。通过IOC,我们可以将应用程序的控制流程反转,从而实现更灵活、可测试和可维护的代码。


AOP(面向切面编程) 是一种编程范式,用于将横切关注点(如日志记录、事务管理、性能监控等)从应用程序的主要业务逻辑中分离出来。AOP通过在特定的切入点上织入额外的代码(称为切面),从而实现对这些关注点的统一管理。这种分离和集中的方式使得我们可以在不修改核心业务逻辑的情况下添加、移除或修改横切关注点的行为。


对于Java开发者来说,IOC和AOP可能已经很熟悉了,因为在Java开发中有许多成熟的框架,如Spring,提供了强大的IOC和AOP支持。


在Flutter中,尽管没有专门的IOC和AOP框架,但我们可以利用语言本身和一些设计模式来实现类似的功能。


接下来,我们可以探讨在Flutter中如何实现IOC和AOP的一些常见模式和技术。无论是依赖注入还是横切关注点的管理,我们可以使用一些设计模式和第三方库来实现类似的效果,以满足我们的开发需求


1. 控制反转(IOC):


依赖注入(Dependency Injection):依赖注入是一种将依赖关系从组件中解耦的方式,通过将依赖项注入到组件中,实现控制反转的效果。在Flutter中,你可以使用get_it库来实现依赖注入。下面是一个示例:


import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';

class UserService {
String getUser() => 'John Doe';
}

class GreetingService {
final UserService userService;

GreetingService(this.userService);

String greet() {
final user = userService.getUser();
return 'Hello, $user!';
}
}

void main() {
// 注册依赖关系
GetIt.instance.registerSingleton<UserService>(UserService());
GetIt.instance.registerSingleton<GreetingService>(
GreetingService(GetIt.instance<UserService>()),
);

runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final greetingService = GetIt.instance<GreetingService>();

return MaterialApp(
title: 'IOC Demo',
home: Scaffold(
appBar: AppBar(title: Text('IOC Demo')),
body: Center(child: Text(greetingService.greet())),
),
);
}
}


在上述示例中,我们定义了UserServiceGreetingService两个类。GreetingService依赖于UserService,我们通过依赖注入的方式将UserService注入到GreetingService中,并通过get_it库进行管理。


2. 面向切面编程(AOP):


在Flutter中,可以使用Dart语言提供的一些特性,如Mixin和装饰器(Decorator)来实现AOP。


Mixin:Mixin是一种通过将一组方法和属性混入到类中来实现代码复用的方式。下面是一个示例:


import 'package:flutter/material.dart';

mixin LogMixin<T extends StatefulWidget> on State<T> {
void log(String message) {
print('[LOG]: $message');
}
}

class LogButton extends StatefulWidget {
final VoidCallback onPressed;

const LogButton({required this.onPressed});

@override
_LogButtonState createState() => _LogButtonState();
}

class _LogButtonState extends State<LogButton> with LogMixin {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
log('Button clicked');
widget.onPressed();
},
child: Text('Click Me'),
);
}
}

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'AOP Demo',
home: Scaffold(
appBar: AppBar(title: Text('AOP Demo')),
body: Center(child: LogButton(onPressed: () => print('Button pressed'))),
),
);
}
}



在上面的示例中,我们定义了一个LogMixin,其中包含了一个log方法,用于记录日志。然后我们在_LogButtonState中使用with LogMixin将日志记录功能混入到_LogButtonState中。每次按钮被点击时,会先打印日志,然后调用传入的回调函数。


装饰器:装饰器是一种将额外行为添加到方法或类上的方式。下面是一个示例:


void logDecorator(Function function) {
print('[LOG]: Method called');
function();
}

@logDecorator
void greet() {
print('Hello, world!');
}

void main() {
greet();
}

在Flutter中,虽然没有专门的IOC(控制反转)和AOP(面向切面编程)框架,但我们可以利用一些设计模式和技术来实现类似的效果。


对于IOC,我们可以使用依赖注入(Dependency Injection)的方式实现。依赖注入通过将依赖项注入到组件中,实现了控制反转的效果。在Flutter中,可以借助第三方库如get_itkiwi来管理依赖关系,将对象的创建和管理交由依赖注入框架处理。


在AOP方面,我们可以使用Dart语言提供的Mixin和装饰器(Decorator)来实现类似的功能。Mixin是一种通过将一组方法和属性混入到类中的方式实现代码复用,而装饰器则可以在不修改被装饰对象的情况下,添加额外的行为或改变对象的行为。


通过使用Mixin和装饰器,我们可以在Flutter中实现横切关注点的管理,例如日志记录、性能监测和权限控制等。通过将装饰器应用于关键的方法或类,我们可以在应用程序中注入额外的功能,而无需直接修改原始代码。


需要注意的是,以上仅为一些示例,具体实现方式可能因项目需求和个人偏好而有所不同。在Flutter中,我们可以灵活运用设计模式、第三方库和语言特性,以实现IOC和AOP的效果,从而提升代码的可维护性、可扩展性和重用性。


总结而言,尽管Flutter没有专门的IOC和AOP框架,但我们可以借助依赖注入和装饰器等技术,结合常见的设计模式,构建灵活、可测试和可维护的应用程序。这些技术和模式为开发者提供了良好的开发体验和代码结构。


希望对您有所帮助谢谢!!

作者:北漂十三载
来源:juejin.cn/post/7251032736692600869

收起阅读 »

算法基础:归并排序

上一篇文章介绍了什么是分治思想,今天就来看一下它其中一个继承人-- 归并排序,本章主要介绍归并排序的原理,以及对一个实际问题进行编码。 学习的内容 1. 什么是归并排序 比如我们拿到一个数组,如果想使用归并排序,应该怎么做呢?首先我们将数组从中间切分,分成左...
继续阅读 »

上一篇文章介绍了什么是分治思想,今天就来看一下它其中一个继承人-- 归并排序,本章主要介绍归并排序的原理,以及对一个实际问题进行编码。


学习的内容




1. 什么是归并排序


比如我们拿到一个数组,如果想使用归并排序,应该怎么做呢?首先我们将数组从中间切分,分成左右两个部分,然后对左半部分和右半部分进行排序,两边部分又可以继续拆分,直至子数组中只剩下一个数据位置。


然后就要将拆分的子数组进行合并,合并的时候会涉及到两个数据进行比较,然后按照大小进行排序,以此往上进行合并。


拆分过程


image.png
合并过程


image.png


从上面我们可以看出,我们最终将大的数组拆分成只有单个数据的数组,然后进行合并,在合并过程中比较两个长度为1的数组,进行排序合并成新的子数组,然后依次类推,直至全部排序完成,也就意味着原数组排序完成。


2.代码示例


public class Solution {
   public static void main(String[] args) {
       int[] arr = {1,4,3,2,11};
       sortArray(arr);
       System.out.println(arr);
  }

   public static int[] sortArray(int[] nums) {
       quickSort(nums, 0, nums.length - 1);
       return nums;
  }

   private static void quickSort(int[] nums, int left, int right) {
       if (left >= right) {
           return;
      }
       int partitionIndex = getPartitionIndex(nums, left, right);
       quickSort(nums, left, partitionIndex - 1);
       quickSort(nums, partitionIndex + 1, right);
  }

   private static int getPartitionIndex(int[] nums, int left, int right) {
       int pivot = left;
       int index = pivot + 1;
       for (int i = index; i <= right; i++) {
           if (nums[i] < nums[pivot]) {
               swap(nums, i, index);
               index++;
          }
      }
       swap(nums, pivot, index - 1);
       return index - 1;
  }

   private static void swap(int[] nums, int i, int j) {
       int temp = nums[i];
       nums[i] = nums[j];
       nums[j] = temp;
  }
}




总结


本章简单分析了归并排序的原理以及分享了一个实际案例,无论是归并还是归并算法,对理解递归还是很有帮助的,之前总是靠着想递归流程,复杂点的绕着绕着就晕了,后面会再看一下快速排序,他和本文提到的归并排序都是分治思想,等说完快排,再

作者:花哥编程
来源:juejin.cn/post/7250404077712048165
一起对比两者的区别。

收起阅读 »

如果你的同事还不会配置commit提交规范,请把这篇文章甩给他

前言 首先问问大家在日常工作中喜欢哪种commit提交? git commit -m "代码更新" git commit -m "解决公共样式问题" git commit -m "feat: 新增微信自定义分享" 如果你是第三种,那我觉得你肯定了解过co...
继续阅读 »

前言


首先问问大家在日常工作中喜欢哪种commit提交?


git commit -m "代码更新"

git commit -m "解决公共样式问题"

git commit -m "feat: 新增微信自定义分享"

如果你是第三种,那我觉得你肯定了解过commit提交规范,可能是刷到过同类文章也可能是在工作中受到的要求


我自己是在刚出来实习的一家公司了解到的,依稀记得“冒号要用英文的,冒号后面要接空格...”


虽然我一直保持这种习惯去提交代码,但是后面遇到的同事大部分都是放飞自我的提交,看的我很难受


因此这篇文章就教还不会配置的小伙伴如何配置被业界广泛认可的 Angular commit message 规范以及呼吁大家去使用。


先来了解下commit message的构成


<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

对应的commit记录如下图


微信截图_20230608114515.png




  • type: 必填 commit 类型,有业内常用的字段,也可以根据需要自己定义



    • feat 增加新功能

    • fix 修复问题/BUG

    • style 代码风格相关无影响运行结果的

    • perf 优化/性能提升

    • refactor 重构

    • revert 撤销修改

    • test 测试相关

    • docs 文档/注释

    • chore 依赖更新/脚手架配置修改等

    • workflow 工作流改进

    • ci 持续集成

    • types 类型定义文件更改

    • wip 开发中

    • undef 不确定的分类




  • scope: commit 影响的范围, 比如某某组件、某某页面




  • subject: 必填 简短的概述提交的代码,建议符合 50/72 formatting




  • body: commit 具体修改内容, 可以分为多行, 建议符合 50/72 formatting




  • footer: 其他备注, 包括 breaking changes 和 issues 两部分




git cz使用


只需要输入 git cz ,就能为我们生成规范代码的提交信息。


一、安装工具


npm install -g commitizen // 系统将弹出上述type、scope等来填写
npm install -g cz-conventional-changelog // 用来规范提交信息

ps:如果你是拉取别人已经配置好git cz的项目,记得也要在自己环境安装


然后将cz-conventional-changelog添加到package.json中


commitizen init cz-conventional-changelog --save --save-exact

微信截图_20230608155514.png


二、使用git cz提交


安装完第一步的工具后,就可以使用git cz命令提交代码了


微信图片_20230608092741.png


微信图片_20230608092732.png


如图,输入完git cz命令后,系统将会弹出提交所需信息,只需要依次填写就可以


commitlint使用


如果你不想使用git cz命令去提交代码,还是习惯git commit的方式去提交


那么接下来就教大家怎么在git commit命令或者vscode工具中同样规范的提交代码


一、安装工具


npm install --save-dev husky
npm install --save-dev @commitlint/cli
npm install --save-dev @commitlint/config-conventional

二、配置



  • 初始化husky


npx husky install


  • 添加hooks


npx husky add .husky/commit-msg 'npx --no -- commitlint --edit \$1'


  • 在项目根目录下创建commitlint.config.js,并配置如下


module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-case': [2, 'always', ['lower-case', 'upper-case']],
'type-enum': [2, 'always',[
'feat', // 增加新功能
'fix', // 修复问题/BUG
'style', // 代码风格相关无影响运行结果的
'perf', // 优化/性能提升
'refactor', // 重构
'revert', // 撤销修改
'test', // 测试相关
'docs', // 文档/注释
'chore', // 依赖更新/脚手架配置修改等
'workflow', // 工作流改进
'ci', // 持续集成
'types', // 类型定义文件更改
'wip', // 开发中
'undef' // 不确定的分类
]
]
}
}

三、验证


没配置前能直接提交


微信图片_20230608092753.png


配置之后就会规范提交


微信图片_20230608092757.png


总结


以上两种方式都很简单,几个步骤下来就可以配置好,希望大家都能

作者:这货不是狮子
来源:juejin.cn/post/7243451555930898469
养成一个开发好习惯~

收起阅读 »

Android 内存治理之线程

1、 前言   当我们在应用程序中启动一个线程的时候,也是有可能发生OOM错误的。当我们看到以下log的时候,就说明系统分配线程栈失败了。 java.lang.OutOfMemoryError: pthread_create (1040KB stack) fa...
继续阅读 »

1、 前言


  当我们在应用程序中启动一个线程的时候,也是有可能发生OOM错误的。当我们看到以下log的时候,就说明系统分配线程栈失败了。


java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory


这种情况可能是两种原因导致的。



  • 第一个就是系统的内存不足的时候,我们去启动一个线程。

  • 第二种就是进程内运行的线程总数超过了系统的限制。



  如果是内存不足的情况,需按照堆内存治理的方式来进行解决,检查应用内存泄漏问题并优化,此情况不作为本次讨论的重点。

  本次主要讨论进程内运行的线程总数超过了系统的限制所导致的情况。出现此情况时,我们就需要通过控制并发的线程总数来解决这个问题。


  想要控制并发的线程数。最直接的一种方式就是利用回收的思路,也就是让我们的线程通过串行的方式来执行;一个线程执行完毕之后,再启动下一个线程。这样就能够让并发的线程总数达到一个可控的状态。

  另外一种方式就是通过复用来解决,让同一个线程的实例可以被反复的利用,只创建较少的线程实例,就能完成大量的异步操作。


2、异步任务的方式对比


  对比一下,在安卓平台我们比较常用的开启异步任务的方式中,有哪些是更加有利于我们进行线程总数的控制的。


开启异步任务的方式特点
Thread.start()并行,难以管理
HandlerThread带消息循环的线程,线程内部串行任务(线程复用)
AsyncTask轻量级,串行(3.0以上),可以结合线程池使用
线程池可管理并发数,池化复用线程
Kotlin协程简化异步编程代码,复用线程,提高并发效率
##### 2.1 Thread

  从最简单的直接创建Thread的实例的方式来说起。在Java中这种方式虽然是最简单的去开启一个线程的方式,但是在实际开发中,一旦我们通过这种方式去自己创建 Thread 类的实例,并且调用 start 来开启一个线程的话,所开启的线程会非常的难以调度和管理。这种线程也就是我们平时所说的野线程。所以我们最好不要直接的创建thread类的实例。


2.2 HandlerThread

public class HandlerThread extends Thread { }

  HandlerThread是Thread类的子类,对Thread做了很多便利的封装。它有自己的Loop,它能够进行消息循环,所以就能够做到通过Handler执行异步任务,也能够做到在不同的线程之间,通过Handler进行现成的通讯。我们可以利用Handler的post操作,让我们在一个线程内部串行的执行多个异步任务。从内存的角度来说,也就相当于对线程进行了复用。


2.3 AsyncTask

  AsyncTask是一个相对更加轻量级,专门为了完成执行异步任务,然后返回UI线程更新UI的操作而设计的。对于我们来说,AsyncTask更像是一个任务的概念,而不是一个线程的概念。我们不需要把它当做一个线程去理解。 AsyncTask的本质,其实也是对线程和Handler的封装。



  • Android 1.6前,串行执行,原理:一个子线程进行任务的串行执行;

  • Android 1.6到2.3,并行执行,原理:一个线程数为5的线程池并行执行,但如果前五个任务执行时间过长,会堵塞后续任务执行,故不适合大量任务并发执行;

  • Android 3.0后,串行执行,原理:全局线程池进行串行处理任务;


到了Android 3.0以上版本,默认是串行执行的,但是可以结合线程值来实现有限制的并行。也可以达到一个限制线程总数的目的。


2.4 线程池

  Java语言本身也为我们提供了线程池。线程池的作用就是可以管理并发数,并且能够持续的去复用线程。如果在一个应用内部的全部异步操作,全部都采用线程池的方式来开启的话,那么我们就能够管理我们所有的异步任务了。这样一来,能够大大的降低线程治理的成本。


2.5 Kotlin协程

  在Kotlin中还引入了协程的概念。协程给传统的Java的异步编程带来最大的改变,就是能够让我们更加优雅的去实现异步任务。我们前面所说的这几种异步任务的执行方式,都需要我们额外的去写大量的样本代码。而Kotlin协程就能够做到让我们用写同步代码的方式去写异步代码。


  在语法的层面上,协程的另一个优势就是性能方面。协程能够帮助我们用更少的线程去执行更多的并发任务。同样也降低了我们治理内存的成本。从治理内存的角度来说,用线程池接管线程或者采用协程都是很好的方式。

作者:大神仙
来源:juejin.cn/post/7250357906712854589

收起阅读 »

uniapp开发项目——问题总结

前言 之前使用过uniapp开发微信小程序,但是没有遇到需要兼容H5页面的。因此在使用uniapp开发微信小程序和H5的过程中,遇到了好些问题。 1. button按钮存在黑色边框 使用button标签,在手机上查看存在黑色的边框,设置了border: non...
继续阅读 »

前言


之前使用过uniapp开发微信小程序,但是没有遇到需要兼容H5页面的。因此在使用uniapp开发微信小程序和H5的过程中,遇到了好些问题。


1. button按钮存在黑色边框


使用button标签,在手机上查看存在黑色的边框,设置了border: none;也没有效果。


原因:uniapp的button按钮使用了伪元素实现边框


解决方法: 设置button标签的伪元素为display:none或者boder:none;


button:after{
boder:none;
}

2. 配置反向代理,处理跨域


微信小程序没有跨域问题,如果当前小程序还没有配置服务器域名出现无法请求接口,只需要在微信开发工具勾选不校验合法域名,就可以请求到了


在本地开发环境中,H5页面在浏览器中调试,会出现跨域问题。如果后端不处理,前端就需要配置反向代理,处理跨域


a. 在manifest.json的源码视图中,找到h5的配置位置,配置proxy代理


image.png


注: "pathRewrite"是必要的,告诉连接要使用代理


b.在请求接口中使用


// '/api'就是manifest.json文件配置的devServer中的proxy
uni.request({
url: '/api'+ '接口url',
...
})

c. 配置完,需要重启项目


3. 使用uni.uploadFile()API,上传图片文件


在微信小程序使用该API上传图片没问题,但是在H5页面实现图片上传,后台始终不能获取到上传的文件。


一开始使用uni.chooseImage()API实现从本地相册选择图片或使用相机拍照,成功之后可以返回图片的本地文件路径列表(tempFilePaths)和图片的本地文件列表(tempFiles,每一项是一个 File 对象)


tempFilePaths 在微信小程序中得到临时路径图片,而在浏览器中得到 blob 路径图片。微信小程序使用uni.uploadFile()上传该临时路径图片,可以成功上传,但是H5无法成功(浏览器中的传值方式会显示为payload,不是文件流file)


image.png


f994e37fce7a5d62763f1c015b9553f.png



可能原因:



  1. 使用 uni.uploadFile() 上传 blob 文件给服务端,后端无法获取到后缀名,进而上传失败。


b. uni.uploadFile()上传的文件格式不正确



解决方法:


在H5中上传tempFiles文件,而不是tempFilePaths,并更改uni.uploadFile()上传的格式


H5


image.png


微信小程序


image.png


4. 打包H5


问题:打包出来,部署到线上,页面空白,控制台preview中展示please enable javascript tocontinue


原因:uniapp的打包配置存在问题


解决方法:


a. web配置不选择路由模式、运行的基础路径也不填写(一开始都写了)


image.png


b. "pathRewrite"设置为空(不知道为啥,可能是不需要配置代理了,网站和接口是同一域名)


"proxy" : {
"/api" : {
"target" : "xxx",
"changeOrigin" : true,
"secure" : true,
"pathRewrite" : {}
}
}



注: 之前接口中的'/api'也需要取消


作者:sherlockkid7
来源:juejin.cn/post/7250284959221809209

收起阅读 »

你的密码安全吗?这三种破解方法让你大开眼界!

密码破解,是黑客们最喜欢的玩具之一。当你用“123456”这类简单密码来保护你的账户时,就像裸奔一样,等待着黑客的攻击。所以,今天我们就来聊聊密码破解知识,看看那些常见的密码破解方法,以及如何防范它们。 1、暴力破解 首先,我们来介绍一下最简单、最暴力的密码破...
继续阅读 »

密码破解,是黑客们最喜欢的玩具之一。当你用“123456”这类简单密码来保护你的账户时,就像裸奔一样,等待着黑客的攻击。所以,今天我们就来聊聊密码破解知识,看看那些常见的密码破解方法,以及如何防范它们。


1、暴力破解


首先,我们来介绍一下最简单、最暴力的密码破解方法——暴力破解。


什么是暴力破解密码呢?


简单来说,就是 攻击者通过穷举所有可能的密码组合来尝试猜测用户的密码。如果你的密码太简单或者密码空间较小,那么暴力破解密码的成功几率会增加。


暴力破解密码的一般步骤:


step1:确定密码空间


密码空间是指所有可能的密码组合。密码空间的大小取决于密码的长度和使用的字符集。例如,对于一个只包含数字的4位密码,密码空间就是从0000到9999的所有组合。


step2:逐个尝试密码


攻击者使用自动化程序对密码空间中的每个密码进行逐个尝试。这可以通过编写脚本或使用专门的密码破解工具来实现。攻击者从密码空间中选择一个密码并将其用作尝试的密码。


step3:比对结果


对于每个尝试的密码,攻击者将其输入到目标账户进行验证。如果密码正确,那么攻击者成功破解了密码。否则,攻击者将继续尝试下一个密码,直到找到匹配的密码为止。


那么我们该如何防范暴力破解呢?


方案1:增强密码策略


增强密码策略,即选择强密码。强密码应该包括足够的长度、复杂的字符组合和随机性,以增加密码空间的大小,从而增加破解的难度。比如说,“K3v!n@1234”这样的密码就比“123456”要强得多。


方案2:登录尝试限制


限制登录尝试次数,例如设置最大尝试次数和锁定账户的时间。


方案3:双因素身份验证


我们还可以引入双因素身份验证,要求用户提供额外的验证信息,如验证码、指纹或硬件令牌等。


通过综合使用这些安全措施,我们可以大大减少暴力破解密码的成功几率,并提高账户和系统的安全性。


2、彩虹表攻击


接下来,我们来介绍一种更加高级、更加可怕的密码破解方法——彩虹表攻击。


彩虹表攻击是一种 基于预先计算出的哈希值与明文密码对应关系的攻击方式


攻击者通过预先计算出所有可能的哈希值与对应的明文密码,并将其存储在一个巨大的“彩虹表”中。


当需要破解某个哈希值时,攻击者只需要在彩虹表中查找对应的明文密码即可。


攻击者生成彩虹表时需要耗费大量的计算和存储资源,但一旦生成完成,后续的密码破解速度就会非常快。


彩虹表攻击的基本原理如下:


step1:生成彩虹表


攻击者事先生成一张巨大的彩虹表,其中包含了输入密码的哈希值和对应的原始密码。彩虹表由一系列链条组成,每个链条包含一个起始密码和相应的哈希值。生成彩虹表的过程是耗时的,但一旦生成完成,后续的破解过程会变得非常快速。


step2:寻找匹配


当攻击者获取到被保护的密码哈希值时,他们会在彩虹表中搜索匹配的哈希值。如果找到匹配,就意味着找到了原始密码。


step3:链表查找


如果在彩虹表中没有找到直接匹配的哈希值,攻击者将使用哈希值在彩虹表中进行一系列的链表查找。他们会在链表上依次应用一系列的哈希函数和反向函数,直到找到匹配的密码。


那如何防范呢?


方案1:盐值(Salt)


使用随机盐值对密码进行加密。盐值是一个随机的字符串,附加到密码上,使得每次生成的哈希值都不同。这样即使相同的密码使用不同的盐值生成哈希,也会得到不同的结果,使得彩虹表无效。


方案2:迭代哈希函数


多次迭代哈希函数是指对原始密码进行多次连续的哈希运算的过程。


通常情况下,单次哈希函数的计算速度是相当快的,但它可能容易受到彩虹表等预先计算的攻击。为了增加密码的破解难度,我们可以通过多次迭代哈希函数来加强密码的安全性。


在多次迭代哈希函数中,原始密码会被重复输入到哈希函数中进行计算。每次哈希运算的结果会作为下一次的输入,形成一个连续的链式计算过程。例如,假设初始密码为 "password",哈希函数为 SHA-256,我们可以进行如下的多次迭代哈希计算:



  1. 首先,将初始密码 "password" 输入 SHA-256 哈希函数中,得到哈希值 H1。

  2. 将哈希值 H1 再次输入 SHA-256 哈希函数中,得到哈希值 H2。

  3. 将哈希值 H2 再次输入 SHA-256 哈希函数中,得到哈希值 H3。

  4. 以此类推,进行多次迭代。


通过多次迭代哈希函数,我们可以增加密码破解的难度。攻击者需要对每一次迭代都进行大量的计算,从而大大增加了密码破解所需的时间和资源成本。同时,多次迭代哈希函数也提供了更高的密码强度,即使原始密码较为简单,其哈希值也会变得复杂和难以预测。


需要注意的是,多次迭代哈希函数的次数应根据具体的安全需求进行选择。次数过少可能仍然容易受到彩虹表攻击,而次数过多可能会对系统性能产生负面影响。因此,需要在安全性和性能之间进行权衡,并选择适当的迭代次数。


方案3:长度和复杂性要求


要求用户选择强密码,包括足够的长度、复杂的字符组合和随机性,以增加彩虹表的大小和密码破解的难度。


3、字典攻击


最后,我们来介绍一种基于字典的密码破解方法——字典攻击。


字典攻击是 通过使用一个包含常见单词和密码组合的字典文件来尝试破解密码(这文件就是我们常说的字典)。这种方法比暴力破解要高效得多,因为它可以根据常见密码和单词来进行尝试。


如果你使用了常见单词或者简单密码作为密码,那么字典攻击很有可能会成功。


以下是字典攻击的一般步骤:


step1:收集密码字典


攻击者会收集各种常见密码、常用字词、常见姓名、日期、数字序列等组成的密码字典。字典可以是公开的密码列表、泄露的密码数据库或通过爬取互联网等方式获得。


step2:构建哈希表


攻击者会对密码字典中的每个密码进行哈希运算,并将明文密码与对应的哈希值构建成一个哈希表,方便后续的比对操作。


step3:逐个比对


攻击者使用字典中的密码与目标账户的密码进行逐个比对。对于每个密码,攻击者将其进行哈希运算,并与目标账户存储的哈希值进行比较。如果找到匹配的哈希值,那么密码就被破解成功。


字典攻击的成功取决于密码的强度和字典的质量。


如果用户使用弱密码或常见密码,很容易受到字典攻击的威胁。为了抵御字典攻击,用户应该选择强密码,包括使用足够的长度、复杂的字符组合和随机性,以增加密码的猜测难度。而系统设计者可以使用前文介绍的方式来防止密码被破解,如密码加盐和限制登录尝试次数等。


好啦,今天的分享就到这里啦!希望大家都能保护好自己的账户安全,不要成

作者:陈有余Tech
来源:juejin.cn/post/7250866224429563941
为黑客攻击的目标哦!

收起阅读 »

为啥你的tree的checkbox隐藏的这么艰难

web
场景: 近期在实现一个基于element-ui 的 Tree 组件的场景, 产品要求, 部门的数据,都不要checkbox, 只有节点值为 员工 才显示,而且还要部分员工的checkbox 禁用 element-ui 的 tree 还不支持特定节点的check...
继续阅读 »

场景:


近期在实现一个基于element-ui 的 Tree 组件的场景, 产品要求, 部门的数据,都不要checkbox, 只有节点值为 员工 才显示,而且还要部分员工的checkbox 禁用


element-ui 的 tree 还不支持特定节点的checkbox隐藏功能, 网上大多采用 class 的方式,将第一层的checkbox进行了隐藏, 但是不满足我们的要求


规则:



  • 第一层节点不显示checkbox

  • 后续任意子节点,如果数据为部门 则也不显示 checkbox

  • 后端返回的部分数据,如果人员符合特定规则(根据自己场景来即可),则表现为 禁用 checkbox


实现


数据
treeData.js


export default [
{
"id":1,
"label":"一级 1-是部门",
"depType":1,
"disabled":false,
"children":[
{
"id":4,
"label":"二级 1-1-是部门",
"depType":1,
"disabled":false,
"children":[
{
"id":9,
"label":"三级 1-1-9",
"disabled":false
},
{
"id":25,
"label":"三级 1-1-25",
"disabled":false
},
{
"id":27,
"label":"三级 1-1-27",
"disabled":false
},
{
"id":30,
"label":"三级 1-30",
"disabled":false
},
{
"id":10,
"label":"三级 1-1-2 是部门",
"depType":5,
"disabled":false
}
]
}
]
},
{
"id":2,
"label":"一级 2 部门",
"depType":1,
"disabled":false,
"children":[
{
"id":5,
"label":"二级 2-1 张三",
"disabled":false
},
{
"id":6,
"label":"二级 2-2 李四",
"disabled":false
}
]
},
{
"id":3,
"label":"一级 3 部门",
"depType":1,
"disabled":false,
"children":[
{
"id":7,
"depType":1,
"label":"二级 3-1 王武",
"disabled":false
},
{
"id":8,
"label":"二级 3-2 赵柳",
"disabled":false
}
]
}
]

上述数据,有的有 deptType字段 ,有的节点没有, 这其实是业务场景的特殊规则,有deptType的认为这个节点为部门节点,没有的则为 员工


<template>
<div>
<el-tree
node-key="id"
show-checkbox
:data="treeData"
:render-content="renderContent"
class="tree-box"
@node-expand='onNodeExpand'
>
</el-tree>
<div>

<ul>
<li>一开始的数据结构必须都有 disabled字段, 默认不禁用,设置为 false 否则会出现视图的响应式延迟问题</li>
<li>是否禁用某个节点,根据renderContent 里面的规则来的, 规则是, 只要是部门的维度,就禁用 设置 data.disabled= true</li>
<li>tree的第一层节点隐藏,是通过js控制的</li>
</ul>
</div>
</div>

</template>

<script>
import treeData from './treeData.js'

export default {
name: 'render-content-tree',
data() {
return {
treeData
}
},
mounted() {
let nodes = document.querySelector('.tree-box')
let children = nodes.querySelectorAll('.el-tree-node')

for(let i=0; i< children.length; i++) {
children[i].querySelector('.el-checkbox').style.display = 'none'
}

// 第一层不要checkbox
// 后续根据规则来
},

methods: {
renderContent(h, { node, data, store }) {
// console.log(node, data)

// 如果不是一级节点,并且符合数据的特定要求,比如这里是 id 大于27 的数据,禁用掉
if (node.level !== 1 && data.id > 27) {
data.disabled = true
}

return h('div',
{
// 如果是部门,就将所有的 checkbox 都隐藏
class: data.depType === undefined ? '' : 'dept-node'
},
data.label)
},

setDeptNodeHide() {
let deptNodes = document.querySelectorAll('.dept-node')

for(let i=0; i<deptNodes.length; i++) {
let checkbox = deptNodes[i].parentNode.querySelector('.el-checkbox')

checkbox.style.display = 'none'
}
},

onNodeExpand(data, node, com) {
// console.log(data);
// console.log(node);
// console.log(com);

this.$nextTick(() => {
this.setDeptNodeHide()
})
}
}
}
</script>

image.png


节点初次渲染的效果.png




展开后的效果


image.png


部门节点没有checkbox, 符合特定规则的c

作者:知了清语
来源:juejin.cn/post/7250040492162433081
heckbox 禁用

收起阅读 »

Vue3 如何开发原生(安卓,ios)

Vue3 有没有一款好用的开发原生的工具 1.uniapp 我个人认为uniapp 适合开发小程序之类的,用这个去开发原生应用会存在一些问题 性能限制:由于 Uniapp 是通过中间层实现跨平台,应用在访问底层功能时可能存在性能损失。与原生开发相比,Uni...
继续阅读 »

Vue3 有没有一款好用的开发原生的工具


1.uniapp 我个人认为uniapp 适合开发小程序之类的,用这个去开发原生应用会存在一些问题




  • 性能限制:由于 Uniapp 是通过中间层实现跨平台,应用在访问底层功能时可能存在性能损失。与原生开发相比,Uniapp 在处理大规模数据、复杂动画和高性能要求的应用场景下可能表现较差。




  • 平台限制:不同平台有着各自的设计规范和特性,Uniapp 在跨平台时可能受到一些平台限制。有些平台特有的功能或界面设计可能无法完全实现,需要使用特定平台的原生开发方式来解决。




  • 生态系统成熟度: 相比于原生开发,Uniapp 的生态系统相对较新,支持和资源相对有限。在遇到问题时,可能难以找到完善的解决方案,开发者可能需要花费更多的时间和精力来解决问题。




  • 用户体验差异: 由于不同平台的设计规范和用户习惯不同,使用 Uniapp 开发的应用在不同平台上的用户体验可能存在差异。开发者需要针对每个平台进行特定的适配和调优,以提供更好的用户体验。




  • 功能支持限制: Uniapp 尽可能提供了跨平台的组件和 API,但某些特定平台的功能和接口可能无法完全支持。在需要使用特定平台功能的情况下,可能需要使用原生开发或自定义插件来解决。




  • uni 文档 uniapp.dcloud.net.cn/




2.react 拥有react native 开发原生应用 Vue无法使用 http://www.reactnative.cn/


3.Cordova cordova.apache.org/ 支持原生html js css 打包成 ios android exe dmg


4.ionic 我发现这个框架支持Vue3 angular react ts 构建Android iOS 桌面程序 这不正合我意 ionicframework.com/docs


前置条件


1.安装 java 环境 和 安卓编辑器sdk



安装完成检查环境变量


image.png


image.png


image.png


检查安卓编辑器的sdk 如果没安装就装一下


image.png


image.png


image.png


ionic


npm install -g @ionic/cli

初始化Vue3项目


安装完成后会有ionic 命令


ionic start [name] [template] [options]
# 名称 模板 类型为vue项目
ionic start app tabs --type vue

image.png


npm install #安装依赖

npm run dev 启动测试

image.png


启动完成后自带一个tabs demo


image.png


运行至android 编辑器 调试


npm run build
ionic capacitor copy android

注意检查


image.png


如果没有这个文件 删除android目录 重新执行下面命令


ionic capacitor copy android

预览


ionic capacitor open android

他会帮你打开安卓编辑器


如果报错说丢失sdk 注意检查sdk目录


image.png.


等待编译


image.png


点击上面绿色箭头运行


image.png


热更新


如果要热更新预览App 需要一个安卓设备


一直点击你的版本号就可以开启开发者模式


bd36c9f72990ae5cf2275e7690c7f354.jpg


开启usb调试 连接电脑


8f1085f12207c5107d39dd8d193dadfb.jpg


ionic capacitor run android -l --external

选择刚才的安卓设备


image.png


成功热更新


image.png


20c29c088e7f4f152fe1af0adbc4035f.jpg


作者:小满zs
来源:juejin.cn/post/7251113487317106745
收起阅读 »

从底层理解CAS原语

CAS
什么是硬件同步原语? 为什么硬件同步原语可以替代锁呢?要理解这个问题,你要首先知道硬件同步原语是什么。 硬件同步原语(Atomic Hardware Primitives)是由计算机硬件提供的一组原子操作,我们比较常用的原语主要是CAS和FAA这两种。 CAS...
继续阅读 »

什么是硬件同步原语?


为什么硬件同步原语可以替代锁呢?要理解这个问题,你要首先知道硬件同步原语是什么。


硬件同步原语(Atomic Hardware Primitives)是由计算机硬件提供的一组原子操作,我们比较常用的原语主要是CAS和FAA这两种。


CAS(Compare and Swap),它的字面意思是:先比较,再交换。我们看一下CAS实现的伪代码:

<< atomic >>
function cas(p : pointer to int, old : int, new : int) returns bool {
if *p ≠ old {
return false
}
*p ← new
return true
}


它的输入参数一共有三个,分别是:



  • p: 要修改的变量的指针。

  • old: 旧值。

  • new: 新值。


返回的是一个布尔值,标识是否赋值成功。


通过这个伪代码,你就可以看出CAS原语的逻辑,非常简单,就是先比较一下变量p当前的值是不是等于old,如果等于,那就把变量p赋值为new,并返回true,否则就不改变变量p,并返回false。


这是CAS这个原语的语义,接下来我们看一下FAA原语(Fetch and Add):

<< atomic >>
function faa(p : pointer to int, inc : int) returns int {
int value <- *location
*p <- value + inc
return value
}


FAA原语的语义是,先获取变量p当前的值value,然后给变量p增加inc,最后返回变量p之前的值value。


讲到这儿估计你会问,这两个原语到底有什么特殊的呢?


上面的这两段伪代码,如果我们用编程语言来实现,肯定是无法保证原子性的。而原语的特殊之处就是,它们都是由计算机硬件,具体说就是CPU提供的实现,可以保证操作的原子性。


我们知道, 原子操作具有不可分割性,也就不存在并发的问题。所以在某些情况下,原语可以用来替代锁,实现一些即安全又高效的并发操作。


CAS和FAA在各种编程语言中,都有相应的实现,可以来直接使用,无论你是使用哪种编程语言,它们底层的实现是一样的,效果也是一样的。


接下来,还是拿我们熟悉的账户服务来举例说明一下,看看如何使用CAS原语来替代锁,实现同样的安全性。


CAS版本的账户服务


假设我们有一个共享变量balance,它保存的是当前账户余额,然后我们模拟多个线程并发转账的情况,看一下如何使用CAS原语来保证数据的安全性。


这次我们使用Go语言来实现这个转账服务。先看一下使用锁实现的版本:

package main

import (
"fmt"
"sync"
)

func main() {
// 账户初始值为0元
var balance int32
balance = int32(0)
done := make(chan bool)
// 执行10000次转账,每次转入1元
count := 10000

var lock sync.Mutex

for i := 0; i < count; i++ {
// 这里模拟异步并发转账
go transfer(&balance, 1, done, &lock)
}
// 等待所有转账都完成
for i := 0; i < count; i++ {
<-done
}
// 打印账户余额
fmt.Printf("balance = %d \n", balance)
}
// 转账服务
func transfer(balance *int32, amount int, done chan bool, lock *sync.Mutex) {
lock.Lock()
*balance = *balance + int32(amount)
lock.Unlock()
done <- true
}


这个例子中,我们让账户的初始值为0,然后启动多个协程来并发执行10000次转账,每次往账户中转入1元,全部转账执行完成后,账户中的余额应该正好是10000元。


如果你没接触过Go语言,不了解协程也没关系,你可以简单地把它理解为进程或者线程都可以,这里我们只是希望能异步并发执行转账,我们并不关心这几种“程”他们之间细微的差别。


这个使用锁的版本,反复多次执行,每次balance的结果都正好是10000,那这段代码的安全性是没问题的。接下来我们看一下,使用CAS原语的版本。

func transferCas(balance *int32, amount int, done chan bool) {
for {
old := atomic.LoadInt32(balance)
new := old + int32(amount)
if atomic.CompareAndSwapInt32(balance, old, new) {
break
}
}
done <- true
}


这个CAS版本的转账服务和上面使用锁的版本,程序的总体结构是一样的,主要的区别就在于,“异步给账户余额+1”这一小块儿代码的实现。


那在使用锁的版本中,需要先获取锁,然后变更账户的值,最后释放锁,完成一次转账。我们可以看一下使用CAS原语的实现:


首先,它用for来做了一个没有退出条件的循环。在这个循环的内部,反复地调用CAS原语,来尝试给账户的余额+1。先取得账户当前的余额,暂时存放在变量old中,再计算转账之后的余额,保存在变量new中,然后调用CAS原语来尝试给变量balance赋值。我们刚刚讲过,CAS原语它的赋值操作是有前置条件的,只有变量balance的值等于old时,才会将balance赋值为new。


我们在for循环中执行了3条语句,在并发的环境中执行,这里面会有两种可能情况:


一种情况是,执行到第3条CAS原语时,没有其他线程同时改变了账户余额,那我们是可以安全变更账户余额的,这个时候执行CAS的返回值一定是true,转账成功,就可以退出循环了。并且,CAS这一条语句,它是一个原子操作,赋值的安全性是可以保证的。


另外一种情况,那就是在这个过程中,有其他线程改变了账户余额,这个时候是无法保证数据安全的,不能再进行赋值。执行CAS原语时,由于无法通过比较的步骤,所以不会执行赋值操作。本次尝试转账失败,当前线程并没有对账户余额做任何变更。由于返回值为false,不会退出循环,所以会继续重试,直到转账成功退出循环。


这样,每一次转账操作,都可以通过若干次重试,在保证安全性的前提下,完成并发转账操作。


其实,对于这个例子,还有更简单、性能更好的方式:那就是,直接使用FAA原语。

func transferFaa(balance *int32, amount int, done chan bool) {
atomic.AddInt32(balance, int32(amount))
done <- true
}


FAA原语它的操作是,获取变量当前的值,然后把它做一个加法,并且保证这个操作的原子性,一行代码就可以搞定了。看到这儿,你可能会想,那CAS原语还有什么意义呢?


在这个例子里面,肯定是使用FAA原语更合适,但是我们上面介绍的,使用CAS原语的方法,它的适用范围更加广泛一些。类似于这样的逻辑:先读取数据,做计算,然后更新数据,无论这个计算是什么样的,都可以使用CAS原语来保护数据安全,但是FAA原语,这个计算的逻辑只能局限于简单的加减法。所以,我们上面讲的这种使用CAS原语的方法并不是没有意义的。


另外,你需要知道的是,这种使用CAS原语反复重试赋值的方法,它是比较耗费CPU资源的,因为在for循环中,如果赋值不成功,是会立即进入下一次循环没有等待的。如果线程之间的碰撞非常频繁,经常性的反复重试,这个重试的线程会占用大量的CPU时间,随之系统的整体性能就会下降。


缓解这个问题的一个方法是使用Yield(), 大部分编程语言都支持Yield()这个系统调用,Yield()的作用是,告诉操作系统,让出当前线程占用的CPU给其他线程使用。每次循环结束前调用一下Yield()方法,可以在一定程度上减少CPU的使用率,缓解这个问题。你也可以在每次循环结束之后,Sleep()一小段时间,但是这样做的代价是,性能会严重下降。


所以,这种方法它只适合于线程之间碰撞不太频繁,也就是说绝大部分情况下,执行CAS原语不需要重试这样的场景。


总结


本文讲述了CAS和FAA这两个原语。这些原语,是由CPU提供的原子操作,在并发环境中,单独使用这些原语不用担心数据安全问题。在特定的场景中,CAS原语可以替代锁,在保证安全性的同时,提供比锁更好的性能。


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

一文看懂什么是fork/join

什么是Fork/Join Fork/Join 是JUC并发包下的一个并行处理框架,实现了ExecutorService接口的多线程处理器,它专为那些可以通过递归分解成更细小的任务而设计,最大化的利用多核处理器来提高应用程序的性能。 Fork/Join的运行流程...
继续阅读 »

什么是Fork/Join


Fork/Join 是JUC并发包下的一个并行处理框架,实现了ExecutorService接口的多线程处理器,它专为那些可以通过递归分解成更细小的任务而设计,最大化的利用多核处理器来提高应用程序的性能。


Fork/Join的运行流程大致如下所示:


需要注意的是,图里的次级子任务可以一直分下去,一直分到子任务足够小为止,这里体现了分而治之(divide and conquer) 的算法思想。


工作窃取算法


工作窃取算法指的是在多线程执行不同任务队列的过程中,某个线程执行完自己队列的任务后从其他线程的任务队列里窃取任务来执行。


工作窃取流程如下图所示:


值得注意的是,当一个线程窃取另一个线程的时候,为了减少两个任务线程之间的竞争,我们通常使用双端队列来存储任务。被窃取的任务线程都从双端队列的头部拿任务执行,而窃取其他任务的线程从双端队列的尾部执行任务。


另外,当一个线程在窃取任务时要是没有其他可用的任务了,这个线程会进入阻塞状态以等待再次“工作”。


Fork/Join 实践


前面说Fork/Join框架简单来讲就是对任务的分割与子任务的合并,所以要实现这个框架,先得有任务。在Fork/Join框架里提供了抽象类ForkJoinTask来实现任务。


ForkJoinTask


ForkJoinTask是一个类似普通线程的实体,但是比普通线程轻量得多。


fork()方法:使用线程池中的空闲线程异步提交任务

public final ForkJoinTask<V> fork() {
Thread t;
// ForkJoinWorkerThread是执行ForkJoinTask的专有线程,由ForkJoinPool管理
// 先判断当前线程是否是ForkJoin专有线程,如果是,则将任务push到当前线程所负责的队列里去
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
((ForkJoinWorkerThread)t).workQueue.push(this);
else
// 如果不是则将线程加入队列
// 没有显式创建ForkJoinPool的时候走这里,提交任务到默认的common线程池中
ForkJoinPool.common.externalPush(this);
return this;
}

其实fork()只做了一件事,那就是把任务推入当前工作线程的工作队列里。


join()方法:等待处理任务的线程处理完毕,获得返回值。


我们看下join()的源码:

public final V join() {
int s;
// doJoin()方法来获取当前任务的执行状态
if ((s = doJoin() & DONE_MASK) != NORMAL)
// 任务异常,抛出异常
reportException(s);
// 任务正常完成,获取返回值
return getRawResult();
}

/**
* doJoin()方法用来返回当前任务的执行状态
**/
private int doJoin() {
int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
// 先判断任务是否执行完毕,执行完毕直接返回结果(执行状态)
return (s = status) < 0 ? s :
// 如果没有执行完毕,先判断是否是ForkJoinWorkThread线程
((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
// 如果是,先判断任务是否处于工作队列顶端(意味着下一个就执行它)
// tryUnpush()方法判断任务是否处于当前工作队列顶端,是返回true
// doExec()方法执行任务
(w = (wt = (ForkJoinWorkerThread)t).workQueue).
// 如果是处于顶端并且任务执行完毕,返回结果
tryUnpush(this) && (s = doExec()) < 0 ? s :
// 如果不在顶端或者在顶端却没未执行完毕,那就调用awitJoin()执行任务
// awaitJoin():使用自旋使任务执行完成,返回结果
wt.pool.awaitJoin(w, this, 0L) :
// 如果不是ForkJoinWorkThread线程,执行externalAwaitDone()返回任务结果
externalAwaitDone();
}

我们在之前介绍过说Thread.join()会使线程阻塞,而ForkJoinPool.join()会使线程免于阻塞,下面是ForkJoinPool.join()的流程图:


RecursiveAction和RecursiveTask


通常情况下,在创建任务的时候我们一般不直接继承ForkJoinTask,而是继承它的子类RecursiveAction和RecursiveTask。


两个都是ForkJoinTask的子类,RecursiveAction可以看做是无返回值的ForkJoinTask,RecursiveTask是有返回值的ForkJoinTask。


此外,两个子类都有执行主要计算的方法compute(),当然,RecursiveAction的compute()返回void,RecursiveTask的compute()有具体的返回值。


ForkJoinPool


ForkJoinPool是用于执行ForkJoinTask任务的执行(线程)池。


ForkJoinPool管理着执行池中的线程和任务队列,此外,执行池是否还接受任务,显示线程的运行状态也是在这里处理。


我们来大致看下ForkJoinPool的源码:

@sun.misc.Contended
public class ForkJoinPool extends AbstractExecutorService {
// 任务队列
volatile WorkQueue[] workQueues;

// 线程的运行状态
volatile int runState;

// 创建ForkJoinWorkerThread的默认工厂,可以通过构造函数重写
public static final ForkJoinWorkerThreadFactory defaultForkJoinWorkerThreadFactory;

// 公用的线程池,其运行状态不受shutdown()和shutdownNow()的影响
static final ForkJoinPool common;

// 私有构造方法,没有任何安全检查和参数校验,由makeCommonPool直接调用
// 其他构造方法都是源自于此方法
// parallelism: 并行度,
// 默认调用java.lang.Runtime.availableProcessors() 方法返回可用处理器的数量
private ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory, // 工作线程工厂
UncaughtExceptionHandler handler, // 拒绝任务的handler
int mode, // 同步模式
String workerNamePrefix) { // 线程名prefix
this.workerNamePrefix = workerNamePrefix;
this.factory = factory;
this.ueh = handler;
this.config = (parallelism & SMASK) | mode;
long np = (long)(-parallelism); // offset ctl counts
this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
}

}

WorkQueue
双端队列,ForkJoinTask存放在这里。


runState
ForkJoinPool的运行状态。SHUTDOWN状态用负数表示,其他用2的幂次表示。


当工作线程在处理自己的工作队列时,会从队列首取任务来执行(FIFO);如果是窃取其他队列的任务时,窃取的任务位于所属任务队列的队尾(LIFO)。


ForkJoinPool与传统线程池最显著的区别就是它维护了一个工作队列数组(volatile WorkQueue[] workQueues,ForkJoinPool中的每个工作线程都维护着一个工作队列)。


Fork/Join的使用


上面我们说ForkJoinPool负责管理线程和任务,ForkJoinTask实现fork和join操作,所以要使用Fork/Join框架就离不开这两个类了,只是在实际开发中我们常用ForkJoinTask的子类RecursiveTask 和RecursiveAction来替代ForkJoinTask。


下面我们用一个计算斐波那契数列第n项的例子来看一下Fork/Join的使用:


斐波那契数列数列是一个线性递推数列,从第三项开始,每一项的值都等于前两项之和:


1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89······


如果设f(n)为该数列的第n项(n∈N*),那么有:f(n) = f(n-1) + f(n-2)。

public class FibonacciTest {

class Fibonacci extends RecursiveTask<Integer> {

int n;

public Fibonacci(int n) {
this.n = n;
}

// 主要的实现逻辑都在compute()里
@Override
protected Integer compute() {
// 这里先假设 n >= 0
if (n <= 1) {
return n;
} else {
// f(n-1)
Fibonacci f1 = new Fibonacci(n - 1);
f1.fork();
// f(n-2)
Fibonacci f2 = new Fibonacci(n - 2);
f2.fork();
// f(n) = f(n-1) + f(n-2)
return f1.join() + f2.join();
}
}
}

@Test
public void testFib() throws ExecutionException, InterruptedException {
ForkJoinPool forkJoinPool = new ForkJoinPool();
System.out.println("CPU核数:" + Runtime.getRuntime().availableProcessors());
long start = System.currentTimeMillis();
Fibonacci fibonacci = new Fibonacci(40);
Future<Integer> future = forkJoinPool.submit(fibonacci);
System.out.println(future.get());
long end = System.currentTimeMillis();
System.out.println(String.format("耗时:%d millis", end - start));
}
}

上面例子的输出:



  • CPU核数:4

  • 计算结果:102334155

  • 耗时:9490 ms

  • 需要注意的是,上述计算时间复杂度为O(2^n),随着n的增长计算效率会越来越低,这也是上面的例子中n不敢取太大的原因。


总结


并不是所有的任务都适合Fork/Join框架,比如上面的例子任务划分过于细小反而体现不出效率。因为Fork/Join是使用多个线程协作来计算的,所以会有线程通信和线程切换的开销。


如果要计算的任务比较简单,直接使用单线程会更快一些。但如果要计算的东西比较复杂,计算机又是多核的情况下,就可以充分利用多核CPU来提高计算速度。


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

Kotlin的一些细节与技巧

kotlin在使用过程中有些有着一些小细节需要注意,搞清楚这些能够更加高效的使用Kotlin,下面来介绍一下。 查看字节码 kotlin本质上在编译之后还是会跟java一样生成字节码,as的工具自带查看字节码工具,能让我们看到一些舒服的kotlin操作背后的秘...
继续阅读 »

kotlin在使用过程中有些有着一些小细节需要注意,搞清楚这些能够更加高效的使用Kotlin,下面来介绍一下。


查看字节码


kotlin本质上在编译之后还是会跟java一样生成字节码,as的工具自带查看字节码工具,能让我们看到一些舒服的kotlin操作背后的秘密 image.png 点击生成文件的Decompile 能看到kotlin文件从字节码到java代码后的结果,不过可读性并不是很好 image.png


扩展方法的小坑


Kotlin提供了扩展方法和扩展属性,能够对一些我们无法修改源码的类,增加一些额外的方法和属性 一个很简单的例子,String是JDK提供的类,我们没有办法直接修改它的源码,但是我们又经常会做一些判空、判断长度的操作,在以往使用Java的时候,我们会使用TextUtils.isEmpty来判断,但是有了Kotlin之后,我们可以像下面这样,给String定义一个扩展方法,之后在方法体中,使用this就可以方法到当前的String对象,从而实现**「看起来」**为这个类新增了一个方法的效果,如下所示

fun main() {
    "".isEmpty()
}

fun String.isEmpty(): Boolean {
    return this.length > 0
}

这实际上是Kotlin编译器的魔法,最终它在调用时还是以一个方法的形式,「所以扩展方法并没有真正的为这个类增加新的方法」,而只是让你在写代码时可以像调用方法一样调用工具类,来增加代码的可读性,看下它的字节码 image.png 了解这一原理之后,我们就可以理解在一些特殊case下,Kotlin的扩展为什么表现的有点不符合预期,



  • 扩展类中一样签名的方法,将无效
class People {
    fun run() = println("people run")
}

fun People.run() = println("extend run")

fun main() {
    val people = People()

    people.run()
}
//people run

因为从底层来看,类People自己的方法和扩展方法,方法签名是一样的,Kotlin编译器发现本身有这个方法了,就不会再给你做扩展方法的调用



  • 扩展方法跟随声明时候类型
open class Fruit {
}

class Apple : Fruit() {

}


fun Fruit.printSelf() = println("Fruit")

fun Apple.printSelf() = println("Apple")

fun main() {
    val fruit = A()
    fruit.printSelf()
 //注意这里
    val apple1: Fruit = Apple()
    apple1.printSelf()

    val apple2 = Apple()
    apple2.printSelf()
}
// 输出结果是
// Fruit
// Fruit
// Apple

但是第二个的输出结果却是Fruit,把apple的类型声明成了Fruit,虽然它是一个Apple的实例,但Kotlin编译器又不知道你运行时到底是什么,你声明是Fruit,就给你调用Fruit的扩展方法。


inline来帮你性能优化


在高阶函数在调用时总会创建新的Function对象,当被频繁调用,那么就会有大量的对象被创建,除此之外还可能会有基础类型的装箱拆箱问题,不可避免的就会导致性能问题,为此,「Kotlin为我们提供了inline关键字」。 inline的作用**,内联**,通过inline,我们可以把**「函数调用替换到实际的调用处」**,从而避免Function对象的创建,进一步避免性能损耗,看下代码以及 image.png main方法的调用不再直接调用foo函数,而是把foo函数的函数体直接拷贝了过来进行调用, 不过也不能滥用inline,因为inline是在编译时进行代码的替换,那么就意味着你inline的函数体里的代码,会被替换到每一个调用的地方,从而导致字节码的膨胀,如果对产物对产物大小有严格的要求,需要关注下这个副作用。


借助reified来实现真泛型


在java中我们都知道由于编译时的类型擦除,JVM的泛型其实都是假泛型,如下的代码在编译时往往会报错

fun <T> foo() {
    println(T::class.java) // 会报错
}

但是Kotlin为我们提供了**「reified关键字」,通过这个关键字,我们就可以让上面的代码成功编译并且运行,不过还需要「搭配inline关键字」**

inline fun <reified T> fooReal() {
    println(T::class.java)
}

由于inline会把函数体替换到调用处,调用处的泛型类型一定是确定的,那么就可以直接把泛型参数进行替换,从而达成了「真泛型」的效果,比如使用上面的fooReal

fooReal<String>()
//调用它的打印方法时 替换为String类型
println(String::class.java)

Lateinit 和 by lazy的使用场景


这两个经常会被使用到用来实现变量的延迟初始化,不过二者还是有些区别的



  • lateinit


在声明变量时不知道它的初始值是多少,依赖后续的流程来赋值,可以节省变量判空带来的便利。不过需要确保后续是会对其赋值的,不然会有异常出现



  • lazy


**「一个对象的创建需要消耗大量的资源,而我不知道它到底会不会被用到」**的场景,并且只有在第一次被调用的时候才会去赋值。

fun main() {
    val lazyTest by lazy {
        println("create lazyTest instance")
    }

    println("before create")
    val value = lazyTest
    println("after create")
}
// before create
// create lazyTest instance
// after create

Sequence来提高性能


Kotlin为我们提供了大量的集合操作函数来简化对集合的操作,比如filter、map等,但是这些操作符往往**「伴随着性能的损耗」**,比如如下代码

fun main() {
    val list = (1..20).toList()

    val result = list.filter {
        print("$it ")
        it % 2 == 0
    }.also {
        println()
    }.filter {
        print("$it ")
        it % 5 == 0
    }
    println()
    println(result.toString())
}
// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 
// 2 4 6 8 10 12 14 16 18 20 
// [10, 20]

可以看出,我们定义了一个1~20的集合,然后通过两次调用**「filter」**函数,来先筛选出集合中的偶数,再筛选出集合中的5的倍数,最后得到结果10和20,让我们看下这个舒服的fliter操作符的实现

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

可以看到,每次filter操作都会创建一个新的集合对象,如果你的操作次数很多并且你的集合对象很大,那么就会有额外的性能开销 「如果你对集合的操作次数比较多的话,这时候就需要Sequence来优化性能」

fun main() {
    val list = (1..20).toList()

    val sequenceResult = list.asSequence()
        .filter {
            print("$it ")
            it % 2 == 0
        }.filter {
            print("$it ")
            it % 5 == 0
        }
    
    val iterator = sequenceResult.iterator()
    iterator.forEach {
        print("result : $it ")
    }
}

// 1 2 2 3 4 4 5 6 6 7 8 8 9 10 10 result : 10 11 12 12 13 14 14 15 16 16 17 18 18 19 20 20 result : 20 

对于Sequence,由于它的计算是惰性的,在调用filter的时候,并不会立即计算,只有在调用它的iterator的next方法的时候才会进行计算,并且它并不会像List的filter一样计算完一个函数的结果之后才会去计算下一个函数的结果,「而是对于一个元素,用它直接去走完所有的计算」。 在上面的例子中,对于1,它走到第一个filter里面,不满足条件,直接就结束了,而对于5,它走到第一个filter里面,符合条件,这个时候会继续拿它去走第二个filter,不符合条件,就返回了,对于10,它走到第一个filter里面,符合条件,这个时候会继续拿它去走第二个filter,依然符合条件,最终就被输出了出来


Unit与void的区别


在Kotlin中,如果一个方法没有声明返回类型,那么它的返回类型会被默认设置为**「Unit」,但是「Unit并不等同于Java中的void」**关键字,void代表没有返回值,而Unit是有返回值的,如下

fun main() {
    val foo = foo()
    println(foo.javaClass)
}

fun foo() {

}

// 输出结果:class kotlin.Unit

继续跟进下看看Unit的实现

public object Unit {
    override fun toString() = "kotlin.Unit"
}

在Kotlin中是函数作为一等公民,而不是对象。这一个特性就决定了它可以使用函数进行传递和返回。因此,Kotlin中的高阶函数应用就很广。高阶函数至少就需要一个函数作为参数,或者返回一个函数。如果我们没有在明明函数声明中明确的指定返回类型,或者没有在Lambda函数中明确返回任何内容,它就会返回Unit。 比如 如下实现实际是相同的

fun funcionNoReturnAnything(){
 
}
fun funcionNoReturnAnything():Unit{
 
}

或者是在lambda函数体中最后一个值会作为返回值返回,如果没有明确返回,就会默认返回Unit

view.setOnclickListener{
 
}
view.setOnclickListener{
 Unit 
}

Kotlin的包装类型


kotlin是字节码层面跟java是一样的,但是java中在基础类型有着 **「原始类型和包装类型」**的区别,比如int和Integer,但是在kotlin中我们只有Int这一种类型,那么kotlin编译器是如何做到区分的呢?先看一段kotlin代码以及反编译java之后的代码 image.png 可以看出



  • 对于不可空的基础类型,Kotlin编译器会自动为我们选择使用原始类型,而对于可空的基础类型,Kotlin编译器则会选择使用包装类型

  • 对于集合这种只能传包装类的情况,不论你是传可空还是不可空,都会选择使用包装类型


老生常谈run、let、also、with


run、let、apply、also、with都是Kotlin官方为我们提供的高阶函数,通常对比着4个操作符,



  1. 差异


我们关注receiver、argument、return之间的差异,如图所示 image.png



  1. 场景


image.png 简而言之



  • **「run」**适用于在顶层进行初始化时使用

  • **「let」**在被可空对象调用时,适用于做null值的检查,let在被非空对象调用时,适用于做对象的映射计算,比如说从一个对象获取信息,之后对另一个对象进行初始化和设置最后返回新的对象

  • **「apply」**适用于做对象初始化之后的配置

  • **「also」**适用于与程序本身逻辑无关的副作用,比如说打印日志等


==和===


在Java中我们一般使用==来判断两个对象的引用是否相等,使用equals方法来判断两个**「对象值」**是否相等 但是在Kotlin中,==和equals是相等的用来判断值,使用===来判断两个对象的引用是否相等


高阶函数


kotlin中一等公民是函数,函数也可以作为另一个函数的入参或者返回值,这就是高阶函数。 不过JVM本身是没有函数类型的,那Kotlin是如何实现这种效果的呢?先看段kotlin代码以及反编译了java的代码,一切就一目了然 image.png 我们可以看到,最终foo方法传入的类型是一个Function0类型,然后调用了Function0的invoke方法,继续看下Function0类型

public interface Function0<out R> : Function<R> {
    /** Invokes the function. */
    public operator fun invoke(): R
}

看来魔法就在这里 也就是说如下的两种写法也是等价的

//kotlin
fun main() {
    foo {
        println("foo")
    }
}
//java
public static void main(String[] args) {
    foo(new Function0<Unit>() {
        @Override
        public Unit invoke() {
            System.out.println("foo");
            return Unit.INSTANCE;
        }
    });
}

到这里是不是对高阶函数有着更深刻的认识了呢 Kotlin的高阶函数本质上是通过对函数的抽象,然后在运行时通过创建Function对象来实现的。


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