注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

30岁以后如何提升自己的收入?

最近,和我打球几个美团的小伙伴,基本都是30岁出头,多数都刚结婚生娃,开始步入中年,面对互联网正在进行的重大变革,有些焦虑,更有些迷茫。 我永远坚信:选择比奋斗更重要,因为奋斗只体现你的勤奋与毅力,而选择则彰显你的智慧与信念。我是18年步入30岁,转眼已经五年...
继续阅读 »

最近,和我打球几个美团的小伙伴,基本都是30岁出头,多数都刚结婚生娃,开始步入中年,面对互联网正在进行的重大变革,有些焦虑,更有些迷茫。


我永远坚信:选择比奋斗更重要,因为奋斗只体现你的勤奋与毅力,而选择则彰显你的智慧与信念。我是18年步入30岁,转眼已经五年,借此回顾一下自己所做的思考和选择,希望能帮助到他人,也能为自己接下来的选择明确方向。


30多岁的人,多数人上有老下有小,既是国家的中坚力量,更是家庭的支柱。我们既要为国家创造财富,更要增加自己小家的收入。


受面向对象编程的启发,我觉得面向提升收入来展开本文,文章的结构和层级会更好,也会更吸引人,而且能够切实帮助到读者。


确保身心健康


身体是革命的本钱,没有良好的身心状态,即使我们赚了很多钱,也得花钱治病。这些年,我头顶的头发稀疏了很多,特别是最近这一两年,因为两个娃比较小,经常需要半夜起床哄娃,经常得不到充分的休息,肠胃也出现了一些问题。


为了解决身体上出现的问题,我主要通过中医、跑长跑和打篮球来调理和强身健体。在中医方面,自学了一些基础理论,比如背下了黄帝内经的上古天真论,避免选择医院或按摩店时,被人忽悠。


这两年,去三甲医院找中医开了三次中药,喝完一个疗程后,感觉帮助不大,倒是在小区的按摩店,通过揉肚子,配合艾灸,能有一定效果。总的来说,我肠胃的问题,主要是吃水果没有节制和吃饭太快引起的,要想根治,得从习惯入手。


除了饮食习惯,还得保持锻炼的习惯,所以22年初,我给自己定了三个习惯:每周至少跑10公里强体魄,每周至少读一本好书启智慧,每周至少做一次公益得快乐。


对了跑步,挺难坚持的,所以我会发动我老婆监督,一次做不到就罚款2万元给她。对于读书,结合育儿的需要,我买了一百本育儿书,正好自己有两个娃,学完就能用得上。而对于公益,我有时会带着大儿子去公园拣烟头,有时会自己到寺庙做义工。


成为技术专家


如果是本科毕业,到了30岁的时候,应该有七八年的工作经验了,硕士的话,也有五六年了,此时不管你愿不愿意,都会成为团队的技术骨干,甚至是架构师。


我是从28岁就开始带团队,在面试30岁左右的候选人时,如果他们还不能成为某个领域的技术专家,我基本上只会意思聊十多分钟,当然如果候选人,应聘的是外包岗位,可能会酌情考虑。


那么怎么样,可以称为技术专家呢,用美团最新职级标准来看的话,就是L7+的同学,差不多对应阿里P7的同学。


我参与过多个公司职级标准的制定,记得曾经和某个HR讨论时,她提出要我用一句话来总结,那就是:不仅能独立完成架构设计、技术难题公关和带领其他研发完成技术实现,而且能从整个研发流程出发提升整体的研发质量、效率和用户体验。


在此基础上,对于前端同学,如果想要获得高薪,就得需要花时间专研一些特定方向的技术,比如图形学(具备自研3D渲染引擎的能力)、音视频处理(掌握WebAssembly技术,深入研究opencv、ffmpeg等)、端智能(需要掌握深度学习及其模型的相关知识)。


从我认识的朋友来看,资深的web图形学技术专家和音视频处理专家,年薪能有150万左右,而端智能前端同学去做的比较少,多数是有算法背景的同学,年薪也有120万起。


从美团合并通道的导向来看,对于技术专家,既有广度的要求,更有特定领域技术深度的要求,你掌握的特定技术越不可替代,越容易拿到高薪。


转型做管理


不管处于什么阶段,管理都是我们需要面对的。从踏入职场开始,我们首先要做好自我管理,高效人士的七个习惯,前三个都是和自我管理有关的。


其次,我们需要做好向上管理,不管是我遇到的几个领导,还是我自己,都是比较喜欢和欢迎,下属做好充分准备好,能够积极主动地约我们,聊聊自己的困惑、工作上的思考和改进建议等。


再次,我们需要做好同事管理,如果不能很好地融入团队及企业文化,不仅自己开展工作比较困难,而且在需要裁员时,这样的同学都是会优先考虑要裁掉的。


最后,对于30岁左右的同学,即使不是实线管理者,通常也需要带着多个同学一起完成工作,就不得不强化自己向下管理的能力。像在美团,我们提拔一个同学做leader的时候,往往会先给几个同学或项目让该同学负责,看看其是否合适,合适的同学,有机会时就会优先考虑他,否则就重新招聘。


互联网研发的流动性很大,对于30岁左右的同学,不管当前有没有向下管理的机会,我都建议大家平时多去观察和思考,从职业发展的角度看,即使得跳槽,也要争取有一段向下管理的实践,否则35岁之后,好的就业机会就非常少了。


寻找副业


程序员可以选择的副业有很多种,以下是一些建议:



  • 写作:撰写技术文章或教程,发布在博客、公众号、知乎等平台上。通过广告和付费阅读等方式,可以获得一定的收入。

  • 创立个人品牌:通过积累经验和作品,创立自己的个人品牌,提高个人影响力。这可以为你带来更多商业机会,如演讲、顾问、培训等。

  • 接私活:在业余时间为其他公司或个人完成项目,如网站开发、小程序、APP 等。你可以在一些平台(如猪八戒、实现网、码市等)上找到相关项目,或者通过朋友和同事介绍获得更多机会。

  • 做个人博客或开源项目:通过分享自己的技术经验和心得,吸引更多人关注并建立个人品牌。这可以为你带来一些广告收入和合作机会。同时,参与开源项目可以提高你的技术水平,也有助于拓展人际关系。

  • 网络兼职:利用自己的技能在一些在线教育平台上教授前端课程,如慕课网、极客时间等。你还可以在一些问答平台(如知乎、Stack Overflow)上回答问题,帮助他人解决问题,提高自己的知名度。

  • 开发移动应用:如果你对移动开发有兴趣,可以尝试开发自己的应用,上架到应用商店。通过广告和内购等方式,你可以获得持续的收入。

  • 开发小游戏:如果你对游戏开发有兴趣,可以尝试开发自己的小游戏,发布在微信小程序、抖音等平台上。通过广告和内购等方式,你可以获得持续的收入。


总之,程序员可以选择的副业方向很多,关键在于发掘自己的兴趣和优势,并付诸实践。同时,副业也需要长期坚持和投入,才能获得稳定的收益。


敢于创业


程序员的尽头是什么?有人说,程序员尽头就是不做程序员。那么,不做程序员又能做什么?


信息时代,为90后提供了更多的机会和资源,让他们拥有良好的教育背景和丰富的知识储备,更好地掌握专业知识和技能,为创业打下坚实的基础。


众多创业者在创业前期,或因受到“偶像”或“故事”激励,从而走上创业之路,比如点燃雷军内心的那本《硅谷之火》,他为此认定创业是自己要走的路。


其实,人生的各个阶段都有不同的人激励我创业,以前卓越教育的校长给予我很多工作和人生方向上的指引。但我始终坚信“创业需要发自内心”,我不会因为看到某个人的故事就热血澎湃。


创业者在任何社会的群体中都是少数派,不到 1% 。就算中国最好的理工类大学也没有很多学生创业,反而他们会选择去留学、当科学家、成为公务员。创业要去无人之境、蛮荒之地,要去开创一个新的事业,往往是不被大众认可的。


程序员创富相对比较容易,是因为现在世界上最有价值的都是科技公司,程序员先天离这些公司的核心价值最近。比尔·盖茨、Larry Page、李彦宏、马化腾、张一鸣都是程序员,科技公司预估有超过一半的老板都有程序员背景,从概率的角度来看,程序员创富比其他职业更容易一点。


原因也比较简单,如果是销售人员担任 CEO,他们还得招几个程序员来构建自己的核心竞争力。而程序员作为公司创业的核心,可以不依赖别人就可以启动创业项目,且只要有两个人就可以启动了。


程序员创业有优势,但也并不是适合所有类型的项目。其中,科技创新的项目显然技术人员做 CEO 最为合适,比如研发搜索引擎,不管美国还是中国,CEO 基本上都是技术背景,因为搜索引擎是技术驱动的领域。


然而,我看见了 500~1000 个程序员创业,有 90% 的失败率,多数是回去上班接着打工去了,还有 9% 拥有一个小公司“不死不活”,每年有几十万到一两百万的收入,对个人来讲,算是创业成功。


但从 VC 投资或者个人事业追求的角度,年营业额上了 1,000 万以上,不管是技术驱动、产品驱动、销售驱动型的公司,都是 1% 以下的比例。


程序员创业要闯三关:



  • 首先是技术关,通常这是程序员最容易闯的一关,因为程序员创业肯定会找相对熟悉的领域去做。

  • 其次业务观,有一定挑战。因为做 2C 要能获客,做 2B 要能搞定客户。逻辑思维能力也很重要,但不是全部。2C 比较适合逻辑思维能力,程序员背景的人肯定能搞得很明白。2B 获客更多是软技能,其中包括察言观色、判断对方角色、决策链决策逻辑等,并不是所有的程序员都能做得好。

  • 最后是组织关。公司人很少的时候,20人以内每个人都认识,不太需要过多的管理机制。但如果公司到了 100 人甚至更多,组织能力没到,人越多效率越低,这是非常大的障碍。


所以程序员除了固有的理性思维能力之外,还要能培养跟人打交道的能力,培养个人魅力,同时对组织管理要有敬畏之心。


总结


转眼,我已过30岁有五年了。从本科毕业到军校培训担任班长,到下到连队当排长管理40多人,然后退出现役成为小白程序员,然后在29岁时成为高级技术经理,管理20多人团队,收入也从月薪9k涨到了50k。


30岁以后,尝试过成为图形学和端智能领域的技术专家,也短暂创过业,上班时接过私活,也投入很多精力搞副业,最后美团的合同到期后,选择了先离职休养一段时间。


目前的计划是,再休息一个月,然后决定继续上班,还是基于副业去创业。


作者:三一习惯
来源:juejin.cn/post/7291936473078775843
收起阅读 »

Android:这个需求搞懵了,产品说要实现富文本回显展示

一、前言 不就展示一个富文本吗,有啥难的,至于这么大惊小怪吗,哎,各位老铁,莫慌,先看需求,咱们再一探究竟。 1、大致需求 要求,用户内容编辑页面,实现图文混排,1、图片随意位置插入,并且可长按拖动排序;2、图片拖动完成之后,上下无内容,则需要空出输入位置,有...
继续阅读 »

一、前言


不就展示一个富文本吗,有啥难的,至于这么大惊小怪吗,哎,各位老铁,莫慌,先看需求,咱们再一探究竟。


1、大致需求


要求,用户内容编辑页面,实现图文混排,1、图片随意位置插入,并且可长按拖动排序;2、图片拖动完成之后,上下无内容,则需要空出输入位置,有内容,则无需空出;3、内容支持随意位置插入;4、以富文本的形式传入后台;5、解析富文本,回显内容。


2、大致效果图



实现这个需求倒不是很难,直接一个RecyclerView就搞定了,无非就是使用ItemTouchHelper,再和RecyclerView绑定之后,在onMove方法里实现Item的位置转换,当然需要处理一些图片和输入框之间的逻辑,这个不是本篇文章的重点,以后再说一块。


效果的话,我又单独的写了一个Demo,和项目中用到的一样,具体效果如下:



获取富文本的方式也是比较的简单,无论文本还是图片,最终都是存到集合中,我们直接遍历集合,给图片和文字设置对应的富文本标签即可,具体的属性,比如宽高,颜色大小,可以自行定义,大致如下:


/**
* AUTHOR:AbnerMing
* INTRODUCE:返回富文本数据
*/

fun getRichContent(): String {
val endContent = StringBuffer()
mRichList.forEach {
if (it.isPic == 0) {
//图片
endContent.append("<img src="" + it.image + ""/>")
} else {
//文本
endContent.append("<p>" + it.text + "</p>")
}
}
return endContent.toString()
}

以上的各个环节,不管怎么说,还是比较的顺利,接下来就到了我们今日的话题了,富文本我们是传上去了,但是如何回显呢?


二、富文本回显分析


回显有两种情况,第一种是编辑之后,可以保存至草稿,下次再编辑时,需要回显;第二种情况是,内容已经发布了,可以再次编辑内容。


具体的草稿回显有多种方式,我们不是使用RecyclerView实现的吗,直接保存列表数据就可以了,可以使用本地或者数据库形式的存储方式,不管使用哪种,实现起来绝非难事,回显的时候也是以集合的形式传入RecyclerView即可。


内容已经发布过的,这才是探究的重点,由于接口返回的是富文本信息,一开始无脑想到的是,富文本信息还得要解析里边的内容,着实麻烦,想着每次发布成功之后在本地存储一份数据,在编辑的时候,根据约定好的标识去存储的数据里找,确实可以实现,但是忽略了这是网络数据,是可以更换设备的,换个设备,数据从哪取呢?哈哈,这种投机取巧的方案,实在是不可取。


那没办法了,解析富文本呗,然后逐次取出图片和内容,再封装成集合,回显到RecyclerView中即可。


三、富文本解析


以下是发布成功后,某个字段的富文本信息,我们拿到之后,需要回显到编辑的页面,也就是自定义的RecyclerView中,老铁们,你们的第一解决方案是什么?


<p>我是测试内容</p><p>我是测试内容12333</p><img src="https://www.vipandroid.cn/ming/image/gan.png"/><p>我是测试内容88888</p><p>我是测试内容99999999</p><img src="https://www.vipandroid.cn/ming/image/zao.png"/>

我们最终需要拿到的数据,如下,只有这样,我们才能逐一封装到集合,回显到列表中。


    我是测试内容
我是测试内容12333
https://www.vipandroid.cn/ming/image/gan.png
我是测试内容88888
我是测试内容99999999
https://www.vipandroid.cn/ming/image/zao.png

字符串截取呗,我相信这是大家的第一直觉,以什么方式截取,才能拿到标签里的内容呢?可以负责任的告诉大家,截取是可以实现的,需要实现的逻辑有点多,我简单的举一个截取的例子:


            content = content.replace("<p>", "")
val split = content.split("</p>")
val contentList = arrayListOf<String>()
for (i in split.indices) {
val pContent = split[i]
if (TextUtils.isEmpty(pContent)) {
continue
}
if (pContent.contains("img")) {
//包含了图片
val imgContent = pContent.split("/>")
for (j in imgContent.indices) {
val img = imgContent[j]
if (img.contains("img")) {
//图片,需要再次截取
val index = img.indexOf(""")
val last = img.lastIndexOf("""
)
val endImg = img.substring(index + 1, last)//最终的图片内容
contentList.add(endImg)
} else {
//文本内容
contentList.add(img)
}
}
} else {
contentList.add(pContent)
}
}

截取的方式有很多种,但是无论哪一种,你的判断是少不了的,为了取得对应的内容,不得不多级嵌套,不得不一而再再而三的进行截取,虽然实现了,但是其冗余了代码,丢失了效率,目前还是仅有两种标签,如果说以后的富文本有多种标签呢?想想都可怕。


有没有一种比较简洁的方式呢?必须有,那就是正则表达式,需要解决两个问题,第一、正则怎么用?第二,正则表达式如何写?搞明白这两条之后,获取富文本中想要的内容就很简单了。


四、Kotlin中的正则使用


说到正则,咱就不得不聊聊Java中的正则,这是我们做Android再熟悉不过的,一般也是最常用的,基本代码如下:


    String str = "";//匹配内容
String pattern = "";//正则表达式
Pattern r = Pattern.compile(pattern);
Matcher m = r.matcher(str);
System.out.println(m.matches());

获取匹配内容的话,取对应的group即可,这个例子太多了,就不单独举了,除了Java中提供的Api之外,在Kotlin当中,也提供了相关的Api,使用起来也是无比的简单。


在Kotlin中,我们可以使用Regex这个对象,主要用于搜索字符串或替换正则表达式对象,我们举几个简单的例子。


1、判定是否包含某个字符串,containsMatchIn


     val regex = Regex("Ming")//定义匹配规则
val matched = regex.containsMatchIn("AbnerMing")//传入内容
print(matched)

输出结果


    true

2、匹配目标字符串matches


     val regex = """[A-Za-z]+""".toRegex()//只匹配英文字母
val matches1 = regex.matches("abcdABCD")
val matches2 = regex.matches("12345678")
println(matches1)
println(matches2)

输出结果


    true
false

3、返回首次出现指定字符串find


    val time = Regex("""\d{4}-\d{1,2}-\d{1,2}""")
val timeValue= time.find("今天是2023-6-28,北京,有雨,请记得带雨伞!")?.value
println(timeValue)

输出结果


    2023-6-28

4、返回所有情况出现目标字符串findAll


     val time = Regex("""\d{4}-\d{1,2}-\d{1,2}""")
val timeValue = time.findAll(
"今天是2023-6-28,北京,有雨,请记得带雨伞!" +
"明天是2023-6-29,可能就没有雨了,具体得等到后天2023-6-30日才能知晓!"
)
timeValue.forEach {
println(it.value)
}

输出结果


    2023-6-28
2023-6-29
2023-6-30

ok,当然了,里面还有许多方法,比如替换,分割等,这里就不介绍了,后续有时间补一篇,基本上常用的就是以上的几个方法。


五、富文本使用正则获取内容


一个富文本里的标签有很多个,显然我们都需要进行获取里面的内容,这里肯定是要使用findAll这个方法了,但是,我们该如何设置标签的正则表达式呢?


我们知道,富文本中的标签,都是有左右尖括号组成的,比如<p></p>,<a></a>,当然也有单标签,比如<img/>,<br/>等,那这就有规律了,无非就是开头<开始,然后是不确定字母,再加上结尾的>就可以了。


1、标签精确匹配


比如有这样一个富文本,我们要获取所有的<p></p>标签。


 <div>早上好啊</div><p>我是一个段落</p><a>我是一个链接</a><p>我是另一个一个段落</p>

我们的正则表达式就如下:


  <p.*?>(.*?)</p>

什么意思呢,就是以<p开头,</p>结尾,这个点. 是 除换行符以外的所有字符,* 为匹配 0 次或多次,? 为0 次或 1 次匹配,之所以开头这样写<p.*?>而不是<p>,一个重要的原因就是需要匹配到属性或者空格,要不然富文本中带了属性或空格,就无法匹配了,这个需要注意!


基本代码


         val content = "<div>早上好啊</div><p>我是一个段落</p><a>我是一个链接</a><p>我是另一个一个段落</p>"
val matchResult = Regex("""<p.*?>(.*?)</p>""").findAll(content)
matchResult.forEach {
println(it.value)
}

运行结果


   <p>我是一个段落</p>
<p>我是另一个一个段落</p>

看到上面的的结果,有的老铁就问了,我要的是内容啊,怎么把标签也返回了,这好像有点不对吧,如果说我们只要匹配到的字符串,目前是对的,但是想要标签里的内容,那么我们的正则需要再优化一下,怎么优化呢,就是增加一个开始和结束的位置,内容的开始位置是”<“结束位置是”>“,如下图



我们只需要更改下起始位置即可:


匹配内容


     val content = "<div>早上好啊</div><p>我是一个段落</p><a>我是一个链接</a><p>我是另一个一个段落</p>"
val matchResult = Regex("""(?<=<p>).*?(?=</p>)""").findAll(content)
matchResult.forEach {
println(it.value)
}

运行结果


    我是一个段落
我是另一个一个段落

2、所有标签进行匹配


有了标签精确匹配之后,针对富文本里的所有的标签内容匹配,就变得很是简单了,无非就是要把上边案例中的p换成一个不确定字母即可。


匹配内容


     val content = "<div>早上好啊</div><p>我是一个段落</p><a>我是一个链接</a><p>我是另一个一个段落</p>"
val matchResult = Regex("""(?<=<[A-Za-z]*>).+?(?=</[A-Za-z]*>)""").findAll(content)
matchResult.forEach {
println(it.value)
}

运行结果


    早上好啊
我是一个段落
我是一个链接
我是另一个一个段落

3、单标签匹配


似乎已经满足我们的需求了,因为富文本中的内容已经拿到了,封装到集合之中,传递到列表中即可,但是,以上的正则似乎只针对双标签的,带有单标签就无法满足了,比如,我们再看下初始我们要匹配的富文本,以上的正则是匹配不到img标签里的src内容的,怎么搞?


 <p>我是测试内容</p><p>我是测试内容12333</p><img src="https://www.vipandroid.cn/ming/image/gan.png"/><p>我是测试内容88888</p><p>我是测试内容99999999</p><img src="https://www.vipandroid.cn/ming/image/zao.png"/>

很简单,单标签单独处理呗,还能咋弄,多个正则表达式,用或拼接即可,属性值也是这样的获取原则,定位开始和结束位置,比如以上的img标签,如果要获取到src中的内容,只需要定位开始位置”src="“,和结束位置”"“即可。


匹配内容


    val content =
"<p>我是测试内容</p><p>我是测试内容12333</p><img src="https://www.vipandroid.cn/ming/image/gan.png"/><p>我是测试内容88888</p><p>我是测试内容99999999</p><img src="https://www.vipandroid.cn/ming/image/zao.png"/>"
val matchResult =
Regex("""((?<=<[A-Za-z]*>).+?(?=</[A-Za-z]*>))|((?<=src=").+?(?="))""").findAll(content)
matchResult.forEach {
println(it.value)
}

运行结果


    我是测试内容
我是测试内容12333
https://www.vipandroid.cn/ming/image/gan.png
我是测试内容88888
我是测试内容99999999
https://www.vipandroid.cn/ming/image/zao.png

这不就完事了,简简单单,心心念念的数据就拿到了,拿到富文本标签内容之后,再封装成集合,回显到RcyclerView中就可以了,这不很easy吗,哈哈~


点击草稿,我们看下效果:



六、总结


在正向的截取思维下,正则表达式无疑是最简单的,富文本,无论是标签匹配还是内容以及属性,都可以使用正则进行简单的匹配,轻轻松松就能搞定,需要注意的是,不同属性的匹配规则是不一样的,需要根据特有的情况去分析。


作者:程序员一鸣
来源:juejin.cn/post/7249604020875984955
收起阅读 »

三分钟教会你微信炸一炸,满屏粑粑也太可爱了!

相信这个特效你和你的朋友(或对象)一定玩过 当你发送一个便便的表情,对方如果扔一个炸弹表情,就会立刻将这个便便炸开,实现满屏粑粑的“酷炫”画面。可谓是“臭味十足”,隔着屏幕都能感受到来自微信爸爸的满满恶意。 不清楚大家对这个互动设计怎么看,反正一恩当时是喜欢...
继续阅读 »

相信这个特效你和你的朋友(或对象)一定玩过

请添加图片描述

当你发送一个便便的表情,对方如果扔一个炸弹表情,就会立刻将这个便便炸开,实现满屏粑粑的“酷炫”画面。可谓是“臭味十足”,隔着屏幕都能感受到来自微信爸爸的满满恶意。


不清楚大家对这个互动设计怎么看,反正一恩当时是喜欢的不行,拉着朋友们就开始“炸”得不亦乐乎。


同样被虏获芳心的设计小哥哥在玩到尽兴后,突然灵感大发,连夜绘制出了设计稿,第二天就拉上产品和研发开始脑暴。


“微信炸💩打动我的一点是他满屏的设计,能够将用户强烈的情绪抒发出来;同时他能够与上一条表情进行捆绑,加强双方的互动性。”设计小哥哥声情并茂道。


“所以,让我们的表情也‘互动’起来吧!”


这不,需求文档就来了:



改掉常见的emoji表情发送方式,替换为动态表情交互方式。即,

当用户发送或接收互动表情,表情会在屏幕上随机分布,展示一段时间后会消失。

用户可以频繁点击并不停发送表情,因此屏幕上的表情是可以非常多且重叠的,能够将用户感情强烈抒发。

请添加图片描述

(暂用微信的聊天界面进行解释说明,图1为原样式,图2是需求样式)



这需求一出,动态表情在屏幕上的分布方案便引起了研发内部热烈讨论:当用户点击表情时,到底应该将表情放置在屏幕哪个位置比较好呢?


最直接的做法就是完全随机方式:取0到屏幕宽度和高度中随机值,放置表情贴纸。但这么做的不确定因素太多,比如存在一定几率所有表情都集中在一个区域,布局边缘化以及最差的重叠问题。因此简单的随机算法对于用户的体验是无法接受的。


在这里插入图片描述


我们开始探索新的方案:

因为目前点的选择依赖于较多元素,比如与屏幕已有点的间距,与中心点距离以及屏幕已有点的数目。因此最终决定采用点权随机的方案,根据上述元素决策出屏幕上可用点的优度,选取优度最高的插入表情。


基本思路


维护对应屏幕像素的二维数组,数组元素代指新增图形时,图形中心取该点的优度。

采用懒加载的方式,即每次每次新增图形后,仅记录现有方块的位置,当需要一个点的优度时再计算。


遍历所有方块的位置,将图形内部的点优度全部减去 A ,将图形外部的点按到图形的曼哈顿距离从 0 到 max (W,H),映射,减 0 到 A * K2。

每次决策插入位置时,随机取 K + n * K1 个点,取这些点中优度最高的点为插入中心

A, K, K1, K2 四个常数可调整



一次选择的复杂度是 n * randT,n 是场上方块数, randT 是本次决策需要随机取多少个点。 从效率和 badcase来说,这个方案目前最优。



在这里插入图片描述


代码展示



```cpp
#include <iostream>
#include <vector>
using namespace std;

const int screenW = 600;
const int screenH = 800;

const int kInnerCost = 1e5;
const double kOuterCof = .1;

const int kOutterCost = kInnerCost * kOuterCof;

class square
int x1;

int x2;
int y1;
int y2;
};

int lineDist(int x, int y, int p){
if (p < x) {
return x - p;
} else if (p > y) {
return p - y;
} else {
return 0;
}
}

int getVal(const square &elm, int px, int py){
int dx = lineDist(elm.x1, elm.x2, px);
int dy = lineDist(elm.y1, elm.y2, py);
int dist = dx + dy;
constexpr int maxDist = screenW + screenH;
return dist ? ( (maxDist - dist) * kOutterCost / maxDist ) : kInnerCost;
}

int getVal(const vector<square> &elmArr, int px, int py){
int rtn = 0;
for (auto elm:elmArr) {
rtn += getVal(elm, px, py);
}
return rtn;
}

int main(void){

int n;
cin >> n;

vector<square> elmArr;
for (int i=0; i<n; i++) {
square cur;
cin >> cur.x1 >> cur.x2 >> cur.y1 >> cur.y2;
elmArr.push_back(cur);
}


for (;;) {
int px,py;
cin >> px >> py;
cout << getVal(elmArr, px, py) << endl;
}

}

优化点



  1. 该算法最优解偏向边缘。因此随着随机值设置越多,得出来的点越偏向边缘,因此随机值不能设置过多。

  2. 为了解决偏向边缘的问题,每一个点在计算优度getVal需要加上与屏幕中心的距离 * n * k3


效果演示


最后就是给大家演示一下最后的效果啦!

请添加图片描述

圆满完成任务,收工,下班!


作者:李一恩
来源:juejin.cn/post/7257410685118677048
收起阅读 »

Android一秒带你定位当前页面Activity

前言 假设有以下路径 在过去开发时,我们在点击多层页面的后,想知道当前页面的类名是什么,以上图下单页面为例,我们首先 1、查找首页的搜索酒店按钮的ID XML布局中找到首页的搜索酒店按钮的ID:假设按钮的ID是 R.id.bt_search_hotel ...
继续阅读 »

前言


假设有以下路径


image.png
在过去开发时,我们在点击多层页面的后,想知道当前页面的类名是什么,以上图下单页面为例,我们首先



  • 1、查找首页的搜索酒店按钮的ID

    • XML布局中找到首页的搜索酒店按钮的ID:假设按钮的ID是 R.id.bt_search_hotel



  • 2、从首页Activity中查找按钮的点击事件

    • 假设你有一个点击事件处理器方法 onSearchHotelClick(View view),你可以在首页Activity中找到这个方法的实现



  • 3、进入下一个酒店列表页面Activity

    • 在点击事件处理方法中,启动酒店列表页面的Activity,示例参数值:




Intent intent = new Intent(this, HotelListActivity.class);
startActivity(intent);


  • 4、若多个RecyclerView,需要找到RecyclerView的ID,并在适配器中处理点击事件

    • 在酒店列表页面的XML布局中找到RecyclerView的ID:假设RecyclerView的ID是 R.id.rvHotel

    • 在适配器中处理点击事件,示例参数值




rvHotel.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(View view, int position) {
// 处理点击事件,启动酒店详情页面的Activity
Intent intent = new Intent(context, HotelDetailActivity.class);
intent.putExtra("hotel_id", hotelList.get(position).getId());
startActivity(intent);
}
});


  • 在酒店详情页面中找到XML中预定按钮的ID,并处理点击事件:

    • 在酒店详情页面的XML布局中找到预定按钮的ID:假设按钮的ID是 R.id.stv_book

    • 在详情页面Activity中找到预定按钮的点击事件处理方法,示例参数值




Button bookButton = findViewById(R.id.bookButton);
bookButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 处理点击事件,启动下单页面的Activity
Intent intent = new Intent(DetailActivity.this, OrderActivity.class);
startActivity(intent);
}
});

上面我们发现存在两个问题:



  1. 在定位Activity这个过程中可能会消耗大量的时间和精力,特别是在页面层级较深或者页面结构较为复杂的情况下。

  2. 我们点击某个属性的时候,有时候想知道当前属性的id是什么,然后去做一些逻辑或者赋值等,我们只能去找布局,如果布局层次深,又会浪费大量的时间去定位属性


如果我们能够在1s快速准确地获取当前Activity的类名,那么在项目开发过程中将起到关键性作用,节省了大量时间,减少了开发中的冗余工作。开发人员的开发流程将更加高效,能更专注于业务逻辑和功能实现,而不用花费过多时间在页面和属性定位上


为什么要实现一秒定位当前页面Activity



  • 优化了Android应用程序的性能,实现了快速的页面定位,将当前Activity的定位时间从秒级缩短至仅1秒

  • 提高了开发效率,允许团队快速切换页面和快速查找当前页面的类名,减少了不必要的开发时间浪费

  • 这一优化对项目推进产生了显著影响,提高了整体开发流程的高效性,使我们能够更专注于业务逻辑的实现和功能开发


使用的库是:AsmActualCombat



  • AsmActual利用ASM技术将合规插件会侵入到编译流程中, 插件会把App中所有系统敏感API或属性替换为SDK的收口方法 , 从而解决直接使用系统方法时面临的隐私合规问题


AsmActualCombat库的使用


使用文档链接:github.com/Peakmain/As…


How To


旧版本添加方式


ASM插件依赖
Add it in your root build.gradle at the end of repositories:


buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath "io.github.peakmain:plugin:1.1.4"
}
}

apply plugin: "com.peakmain.plugin"

拦截事件sdk的依赖



  • Step 1. Add the JitPack repository to your build file
    Add it in your root build.gradle at the end of repositories:


   allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}


  • Step 2. Add the dependency


   dependencies {
implementation 'com.github.Peakmain:AsmActualCombat:1.1.5'
}

新版本添加方式


settings.gradle


pluginManagement {
repositories {
//插件依赖
maven {
url "https://plugins.gradle.org/m2/"
}
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
//sdk仓库
maven { url 'https://jitpack.io' }
}
}

插件依赖


根目录下的build.gradle文件


plugins {
//插件依赖和版本
id "io.github.peakmain" version "1.1.4" apply false
}

sdk版本依赖


implementation 'com.github.Peakmain:AsmActualCombat:1.1.5'

使用


我们只需要在application的时候调用以下即可


SensorsDataAPI.init(this);
SensorsDataAPI.getInstance().setOnUploadSensorsDataListener((state, data) -> {
switch (state) {
case SensorsDataConstants.APP_START_EVENT_STATE:
//$AppStart事件
case SensorsDataConstants.APP_END__EVENT_STATE:
//$AppViewScreen事件
break;
case SensorsDataConstants.APP_VIEW_SCREEN__EVENT_STATE:
if (BuildConfig.DEBUG) {
Log.e("TAG", data);
}
StatisticsUtils.statisticsViewHeader(
GsonUtils.getGson().fromJson(data, SensorsEventBean.class));
break;
case SensorsDataConstants.APP_VIEW_CLICK__EVENT_STATE:
if (BuildConfig.DEBUG) {
Log.e("TAG", data);
}
SensorsEventBean sensorsEventBean =
GsonUtils.getGson().fromJson(data, SensorsEventBean.class);
StatisticsUtils.statisticsClickHeader(sensorsEventBean);
break;
default:
break;

}
});

随后我们点击按钮在控制台便可以看到效果



  • 页面埋点


image.png



  • 点击埋点


image.png


总结



  • 是不是很简单呢,只需要简单配置即可1s实现定位当前页面Activity的类名是什么,不需要再花费大量的时间去查找当前页面的类名。

  • 当然,AsmActualCombat项目不仅仅可以实现全埋点、定位当前Activity类名功能,还可以拦截隐私方法调用的拦截哦。

  • 如果大家觉得项目或者文章对你有一点点作用,欢迎点赞收藏哦,非常感谢


作者:peakmain9
来源:juejin.cn/post/7289047550741397564
收起阅读 »

团队的效率在于规范和沟通,而不仅仅在于技术

感谢你阅读本文! 初入职场的时候,总觉得很多事情没必要做,因为不仅浪费时间,而且还繁琐,因为人面对一件事的时候,如果自己能够快速解决,那么就不愿意再介入第三人,因为会花费更多的时间,加上大多人从内心出发是不太愿意去沟通的! 但是我们永远要相信的是,无论你这个人...
继续阅读 »

感谢你阅读本文!


初入职场的时候,总觉得很多事情没必要做,因为不仅浪费时间,而且还繁琐,因为人面对一件事的时候,如果自己能够快速解决,那么就不愿意再介入第三人,因为会花费更多的时间,加上大多人从内心出发是不太愿意去沟通的!


但是我们永远要相信的是,无论你这个人心再细,技术再牛,你总会有想不到的地方,而这些盲区大概率就是造成日后出问题的导火索!


下面我们就来聊一聊规范、沟通、技术!


规范


我上一次裸辞,上级和我聊的时候,我说了两点原因!


第一是我不想在当前的领域继续干下去了,因为我知道这个领域对我来说已经很不利了,如果再继续干下去,那只能是温水煮青蛙,最终害了自己!


第二就是规范问题,这点其实在之前我也有反馈过,不过一直都没有真正去实施,在提了离职后,谈话的时候我又去反复说这个问题!


因为之前我们线上出现的很多问题就是因为不规范造成的,我记得当时除了研发,我还负责部署,因为他们没有在测试环境测好,到了线上环境就出大问题了,恢复数据都没用,后面停服一天才恢复好。


为啥会出现这种问题!


1.职责划分不清


这点的话还是和公司的规模有关,如果公司团队比较小,那么开发就不得不身兼数职,从扫地干到CTO都行。


我们部门虽然人不多,但是麻雀虽小五脏俱全,不过遗憾的是,根本没去划分好职责,站在最前面的也是比较容易背锅的,很多时候任务倾斜特别严重。


2.没有严格按照流程来走


一个团队里面如果没有严格的流程,那么就会问题百出,特别是达到一定的规模后,有一些我们看似没必要的流程,是因为自己觉得麻烦,但是站在管理的视角,就显得尤为重要。


严格的流程是稳定和安全的保障,如果因为懒惰或者“方便”而去省略流程,那么终有一日会付出N倍的代价!


所以一个明确的规范可以帮助团队成员了解他们的职责和期望。这可以减少混乱和误解,从而提高团队的效率。规范也可以确保所有的工作都按照相同的标准进行,从而提高产品或服务的质量。


沟通


一个技术再牛逼的团队,如果不能做到有效的沟通,那么也是一盘散沙,一个人再强的人,如果不能让别人听懂他说的话,那么也是寸步难行!


沟通除了会议上要尽力把自己想表达的表达清楚,最重要的还是私下的沟通,因为会议上的东西大多都需要进行再次更改,这时候线下个人与个人之间的沟通就变得更加重要。


基本上百分之八十的问题都是沟通不到位造成的,很多时候你觉得你想的是对的,那是因为你还没有去很了解这个事物,这时候你其实就处于一个信息茧房里,所以一定是会出现问题的。


有效的沟通是任何团队成功的关键。通过沟通,团队成员可以分享信息,解决问题,协调工作,以及建立和维护良好的工作关系。缺乏有效的沟通可能会导致误解,冲突,以及工作效率的降低。


技术


技术和赚钱的关系,就是艺术和赚钱的关系。不卖座的戏只能当成兴趣。


技术是服务于项目,而项目依赖于团队,很多时候我们总是去痴迷各种新技术,不管成熟不成熟,适合不适合,往上面堆就行了,但是如果不去考虑团队的兼容性,不考虑是否好维护,那么只会自找麻烦。


热爱新技术,追去新技术是没错的,但是要根据实际情况来,并不是你的系统一定要设计成分布式,微服务,云原生,对于有些项目,QPS 50都没有,硬是要去设计成分布式,不仅花费了大量的成本,而且维护成本也高,实际上一个单体项目只要设计得好,对于中小型应用完全够用,性能比分布式的好。


合适永远比先进好,特别不是技术驱动的公司,jsp依然能够赚得盆满钵满。


但是并不是技术就不重要了,特别对于从事技术的人来说,这是安身立命之本,只有技术够硬,在脱离平台后才不会焦虑,平台能力永远不算能力,那可能是自己运气好,而脱离平台后依然能够走下去,这才是真正的能力!


总结


规范和沟通不论对于任何行业都是必须的,只有在规范和沟通中生产,产品的质量才能得到保证,团队的效率才能得到提升,技术则驱动产品进步,虽然不是必须,但是如果想在时代的进程中不被淘汰,那么技术是不可或缺的!


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

服务:简聊微内核结构

1 简介:微内核架构 微内核架构是指内核的一种精简形式,将通常与内核集成在一起的系统服务层被分离出来,变成可以根据需求加入选,达到系统的可扩展性、更好地适应环境要求。 微内核:内核管理着所有的系统资源,在微内核中用户服务和内核服务在不同的地址空间中实现。 该结...
继续阅读 »

1 简介:微内核架构


微内核架构是指内核的一种精简形式,将通常与内核集成在一起的系统服务层被分离出来,变成可以根据需求加入选,达到系统的可扩展性、更好地适应环境要求。


微内核:内核管理着所有的系统资源,在微内核中用户服务和内核服务在不同的地址空间中实现。


该结构是向最初并非设计为支持它的系统添加特定功能的最佳方式。


此体系结构消除了对应用程序可以具有的功能数量的限制。我们可以添加无限的插件(例如Chrome浏览器有数百个插件,称为扩展程序)


2 一个简单例子


微内核架构(也称为插件结构)通常用于实现可做为第三方产品下载的应用程序。此结构在内部业务程序很常见。


实际上,它可以被嵌入到其他模式中,例如分层体系中。典型的微内核架构有两个组件:核心系统和插件模块


	plug-in                  plug-in
core system
plug-in plug-in

漂亮一点的图


new_微内核架构.png


由上图可知,微内核架构也被称为插件架构模式(Plug-inArchitecture Patterm),通常由内核系统和插件组成的原因。


核心系统包括使系统正确运行的最小业务逻辑。可以通过连接插件组件添加更多功能,扩展软件功能。就像为汽车添加涡轮以提高动力。


轮圈37.png


插件组件可以使用开放服务网关计划(OSGi),消息传递,Web服务或对象实例化进行连接。
需要注意的是,插件组件是独立的组件,是为扩展或增强核心系统的功能,不应与其他组件形成依赖。


常见的系统结构使用微内核的如:嵌入式Linux、L4、WinCE。



  • 优缺点说明


微服务在应用程序和硬件的通信中,内核进程和内存管理的极小的服务,而客户端程序和运行在用户空间的服务通过消息的传递来建立通信,它们之间不会有直接的交互。


这样微内核中的执行速度相对就比较慢了,性能偏低这是微内核架构的一个缺点。


微内核系统结构相当清晰,有利于协作开发;微内核有良好的移植性,代码量非常少;微内核有相当好的伸缩性、扩展性。


3 小结


(1)微内核架构难以进行良好的整体化优化。

由于微内核系统的核心态只实现了最基本的
系统操作,这样内核以外的外部程序之间的独立运行使得系统难以进行良好的整体优化。


(2)微内核系统的进程间通信开销也较单一内核系统要大得多。

从整体上看,在当前硬件条件下,微内核在效率上的损失小于其在结构上获得的收益。


(3)通信损失率高。

微内核把系统分为各个小的功能块,从而降低了设计难度,系统的维护与修改也容易,但通信带来的效率损失是一个问题。


作者:楽码
来源:juejin.cn/post/7291468863396708413
收起阅读 »

关于我调部门感觉又重新面试一次这件事,做出知识总结

web
前言 这篇文章的起因是,当时上周部门调整,要调动到其他部门,最开始我以为就走个流程意思意思,一点准备都没有。没想到,去其他部门还经过了3面,感觉挺正式的,在这期间问的问题有些令我印象深刻,发现了许多不足吧,我是去年毕业的,工作了1年多了,本来以为一些基础知识...
继续阅读 »

前言



这篇文章的起因是,当时上周部门调整,要调动到其他部门,最开始我以为就走个流程意思意思,一点准备都没有。没想到,去其他部门还经过了3面,感觉挺正式的,在这期间问的问题有些令我印象深刻,发现了许多不足吧,我是去年毕业的,工作了1年多了,本来以为一些基础知识掌握的差不多了,路还远着,还得学啊!本来那天我还准备一下班就回去玩战地2042,免费周啊!啪的一下兴趣全无,总结一下知识吧,指不定什么时候用上(手动狗头)



节流


节流是指一定时间内只触发一次函数调用,如果在指定时间内多次触发,执行第一次,其他的触发将会被忽略,直到过了设定的时间间隔才触发。


function throttle (fn,delay) {
let timer;
retrun function (...args) {
if(!timer) {
fn(this,args)
timer = settimeout(()=>{
timer=null
},delay)
}
}
}

防抖


防抖是在函数调用后,在指定时间间隔后才触发一次。如果在这个时间间隔内再次触发函数,将重新计时,直到过了设定的时间间隔才会触发最后一次函数调用。


function debounce (fn,delay) {
let timer;
retrun function (...args) {
if(timer) {
clearTimetout(timer)
}
timer = settimeout(()=>{
fn(this,args)
timer=null
},delay)
}
}

数据扁平化


数组


function flatter(arr) {
let result = []
for(let i =0;i<arr.length;i++) {
if(Array.isArray(arr[i]) {
result = result.concat(flatter(arr[i]))
} esle {
result.push(arr[i])
}
}
return result
}

去重


const arr1 = [...new Set(arr)]

const arr1 = arr.map((item,index)=>{
return arr.indexof(item)==index
})

查找字符串中出现最多的字符


当时手写了一半,str.split(item).length应该还要-1才是当前字符出现的次数


  const str = ref<string>('sdfgsgdd');
const fn = (str: string) => {
const arr = Array.from(str);
let maxCount = 0;
let mostFrequentChar = '';
const Nsome = [...new Set(arr)];
Nsome.forEach((item) => {
const count = str.split(item).length - 1;
if (count > maxCount) {
maxCount = count;
mostFrequentChar = item;
}
});
console.log('出现最多的次数,字符', maxCount, mostFrequentChar);
};

闭包及其应用场景


我的回答是:
函数里面嵌套函数,并且内部函数引用了外部函数的变量,就是函数能访问其作用域外的变量


应用场景:
我的回答其中之一是:vueX中状态共享是使用了闭包,节流,防抖
但在 Vuex 中,闭包主要用于封装和共享状态,而不是用于访问和操作外部函数的变量。它使用了闭包的概念,但不是严格意义上的闭包。


1.模块化开发 2.回调函数 3.延迟执行(节流,防抖)


原型&原型链及其应用场景



  1. 原型(Prototype):



  • 每个 JavaScript 对象都有一个原型(prototype),它是一个对象。

  • 对象的原型用于共享属性和方法,当我们访问一个对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端。

  • 原型可以通过 proto 属性访问,也可以通过 Object.getPrototypeOf() 方法获取。



  1. 原型链(Prototype Chain):



  • 原型链是由对象的原型组成的链式结构,它用于实现对象之间的继承。

  • 当我们访问一个对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端。

  • 原型链的顶端是 Object.prototype,它是所有对象的原型。


应用场景:



  • 继承:通过原型链,我们可以实现对象之间的继承,一个对象可以继承另一个对象的属性和方法。这样可以避免重复定义和维护相似的代码,提高代码的重用性和可维护性。

  • 共享属性和方法:通过原型链,我们可以将属性和方法定义在原型上,从而实现对象之间的共享。这样可以节省内存空间,避免重复创建相同的属性和方法。

  • 扩展原生对象:通过修改原型链,我们可以扩展 JavaScript 的原生对象,为其添加新的方法和属性。这样可以为原生对象添加自定义的功能,满足特定的需求。


在没有class之前,js是怎么做面向对象的


没答出来,只知道js可以通过class实现面向对象,然后又被问在没有class之前,js是怎么做面向对象的。这也是原型链的应用场景之一,可能是前面原型链的应用场景没说这个,想给我一个提示。


在没有class关键字之前,JavaScript使用原型继承来实现面向对象编程。
javaScript 中的每个对象都有一个原型(prototype),原型是一个对象,它包含了共享的属性和方法。当我们访问一个对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端。


通过原型链,我们可以实现对象之间的继承和共享属性和方法。下面是一个使用原型继承的示例:


// 创建一个构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 在构造函数的原型上定义方法
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};
// 创建一个 Person 对象
const person1 = new Person('Alice', 25);
// 调用对象的方法
person1.sayHello(); // 输出 "Hello, my name is Alice and I am 25 years old."

node是什么,express是什么,node服务中的中间件是用来干什么的


Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,可以用于构建高性能的网络应用程序。它允许使用 JavaScript 在服务器端运行代码,而不仅仅局限于在浏览器中运行。


Express 是一个基于 Node.js 的 Web 应用程序框架,它提供了一组简洁而灵活的功能,用于构建 Web 应用程序和 API。Express 提供了路由、中间件、模板引擎等功能,使得构建 Web 应用程序变得更加简单和高效。


中间件的作用是增强和扩展 Node.js 服务的功能,使得处理请求和响应的过程更加灵活和可定制。通过使用中间件,可以将常见的功能模块化,提高代码的可维护性和可重用性。


Express 提供了一些内置的中间件,同时也支持自定义中间件。您可以使用内置的中间件,如 express.json()、express.urlencoded() 来处理请求体的解析,或者编写自己的中间件来满足特定的需求。


你h5怎么处理兼容性


因为是vite+v3项目,vite官方有推荐的插件库,在插件库中有一个关于浏览器兼容支持的插件:@vitejs/plugin-legacy


插件@vitejs/plugin-legacy的作用是为打包后的文件提供传统浏览器兼容性支持



  1. 首先安装插件:npm i @vitejs/plugin-legacy -D

  2. 然后在vite.config.js中配置


import legacyPlugin from '@vitejs/plugin-legacy'
export default defineConfig( {
plugins: [
legacyPlugin({
targets:['chrome 52'], // 需要兼容的目标列表,可以设置多个
additionalLegacyPolyfills:['regenerator-runtime/runtime'] // 面向IE11时需要此插件
})
]
})

rem,px,em这些有什么区别




  1. px(像素):px 是绝对单位,表示屏幕上的一个物理像素点。它是最常用的单位,具有固定的大小,不会根据其他因素而改变。例如,font-size: 16px; 表示字体大小为 16 像素。




  2. rem(根元素字体大小的倍数):rem 是相对单位,相对于根元素(即 元素)的字体大小。如果根元素的字体大小为 16 像素,那么 1rem 就等于 16 像素。如果根元素的字体大小为 20 像素,那么 1rem 就等于 20 像素。通过设置根元素的字体大小,可以方便地调整整个页面的大小。例如,font-size: 1.5rem; 表示字体大小为根元素字体大小的 1.5 倍。




  3. em(相对于父元素字体大小的倍数):em 也是相对单位,相对于父元素的字体大小。如果父元素的字体大小为 16 像素,那么 1em 就等于 16 像素。如果父元素的字体大小为 20 像素,那么 1em 就等于 20 像素。通过设置父元素的字体大小,可以影响其子元素的大小。例如,font-size: 1.2em; 表示字体大小为父元素字体大小的 1.2 倍。




总结来说,px 是绝对单位,不会随其他因素改变;rem 是相对于根元素字体大小的倍数,可以方便地调整整个页面的大小;em 是相对于父元素字体大小的倍数,可以影响子元素的大小。


在实际使用中,可以根据需求选择合适的单位。对于响应式设计,使用 rem 可以方便地调整整个页面的大小;对于局部样式,可以使用 px 或 em 来控制具体的大小。


你工作中遇到了什么坑或者解决什么让自己印象深刻的问题



  • element-plus的el-table表格的二次封装(可以使用tsx)

  • el-table表格的动态合并

  • h5 ios时调起键盘会把整个布局往上推

  • h5调用封装的app JSbrige完成返回

  • 登录的拼图验证

  • h5嵌套在微信小程序中时,由我们h5跳到三方提供的安全验证h5页面,返回时,本地存储的东西没了

  • 利用git hooks+husky+eslint完成前端代码规范和提交规范

  • 银行卡拖拽排序,把排完的顺序返回服务端


上面这些都是我解决了,也不仅仅只有这些,回头想了了下明明自己有很多可以说的,在当时就说了2,3个,然后负责人问我还有吗时,我卡壳了,居然不知道还要说什么。后面我感觉也是根据这个展开来问的


V2混入和V3的hooks,为什么V3要改成hooks的方式


感觉应该是问hooks的好处吧?反正我是答的不太对的,以下是总结:


Vue 3 引入了 Composition API(包括 setup 函数和 hooks),这是一个新的方式来组织和复用代码,与 Vue 2 的混入(mixins)有所不同。

混入在 Vue 2 中被广泛使用,它们允许你在多个组件之间共享行为。然而,混入有一些问题:



  1. 命名冲突:如果混入和组件有相同的方法或数据属性,可能会导致冲突。

  2. 来源不明:当一个组件使用了多个混入时,可能很难确定一个特定的方法或数据属性来自哪个混入。

  3. 复杂性:混入可以包含生命周期钩子、方法、数据等,这可能会增加理解和维护组件的复杂性。

    相比之下,Vue 3 的 Composition API(包括 hooks)提供了一种更灵活、更可控的方式来组织和复用代码:

  4. 更好的逻辑复用和代码组织:你可以将相关的代码(如数据、方法和生命周期钩子)组织在一起,而不是强制按照 Vue 的选项(data、methods、created 等)来组织代码。

  5. 更好的类型推断:对于使用 TypeScript 的项目,Composition API 提供了更好的类型推断。

  6. 更清晰的来源:每个函数和响应式数据的来源都非常明确,因为它们都是从特定的 hook 或 setup 函数返回的。
    因此,虽然 Vue 3 仍然支持混入,但推荐使用 Composition API 来组织和复用代码。


vue3中怎么封装一个自定义指令



  • 通过app.directive()方法注册指令,该方法接受两个参数,第一个参数是指令的名称,第二个参数是一个对象,包含指令的各个生命周期的钩子函数

  • 然后我们就可以在生命周期的钩子函数中定义指令的行为,根据指令的需求,在相应的生命周期钩子函数中编写逻辑代码


什么情况下会使用自定义指令


我的回答是:想要操作dom元素时并且这种类似情况经常出现,如节流和防抖指令,就是给dom加上disabled。按钮权限指令,给当前按钮dom一个显示和隐藏


拖拽排序


拖拽排序的实现原理主要涉及一下几个步骤:



  • 1.监听拖拽事件: 浏览器提供了一系列的拖拽事件,设置draggable="true"



    1. 开始拖拽:当用户开始拖拽一个元素时,会触发 dragstart 事件。在这个事件的处理函数中,我们可以通过 传入的dragstart(e,index) ,中的index来设置当前被拖拽元素的下标。





    1. 拖拽过程:当用户拖拽元素时,会不断触发 dragover 事件。在这个事件的处理函数中,我们需要调用 event.preventDefault 方法来阻止浏览器的默认行为,否则无法触发 拖拽 事件。





    1. 拖拽到另一个元素区域时:当用户拖拽到另一个元素时,会触发 dragenter 事件。在这个事件的处理函数中,我们可以通过 dragente(e,index)方法来获取拖拽到的元素的下标,然后根据获取的两下标来更新列表的排序。




表格动态合并


element-plus表格合并(例如前两列合并) | 耀耀切克闹 (yaoyaoqiekenao.com)


模拟new实例创建的过程



  • 1.创建了新对象并将._proto_指向构造函数.prototype

  • 2.将this指向新创建的对象

  • 3.返回新对象


function newSimulator() {
//1.创建新对象
const obj = new Object()
//2.设置_proto_为构造函数prototype
const constructor = [].shift.call(arguments)
obj._proto_ = constructor.prototype
//3.this指向新对象,也就是改变this的指向
const ret = constructor.apply(obj,arguments)
//4.返回对象或this
return typeof ret = 'object' ? ret : obj
}

冒泡排序


const arr = [1,7,9,2,3,5]
for(let i=0;i<arr.length;i++){
for(let j=0;j<arr.length-i-1;j++){
let a = []
if(arr[j]<arr[j+1]){
a =arr[j]
arr[j]=arr[j+1]
arr[j+1]=a
}
}
}


深拷贝


1.使用 JSON 序列化和反序列化


const obj={
arr:[1,2]
}
const clone = JSON.parse(JSON.stringify(obj))

2.使用递归完成深拷贝


这种方式通过递归地遍历原始对象,并对该对象的的属性进行逐一的深拷贝,以创建一个原对象的独立副本。


function deepCloneObject(obj) {
if(obj ===null||typeof object !='object') {
return obj
}
const clone = Array.isArray(obj)?[]:{}
for(let key in obj) {
if(object.prototype.hasOwnProperty.call(obj,key))
clone[key] = deepClone(obj[key])
}
retrun clone
}

函数柯里化


函数柯里化是一种将具有多个参数的函数转换为逐个应用参数的函数序列的技术。通过柯里化,我们可以将一个函数的多个参数转化为一系列嵌套的函数调用。


柯里化的优点是可以创建可复用的函数模板。通过部分应用来生成新的函数。这样可以更灵活地使用函数,并且可以方便的创建更专注于特定功能的函数。
简单的函数柯里化例子:


function add(x) {
return function(y) {
return x + y;
}
}

// 使用柯里化的add函数
var add5 = add(5);
console.log(add5(3)); // 输出 8
console.log(add5(7)); // 输出 12

封装一下


function curry(fn) {
return function curried(...args) {
if(args.length>=fn.length) {
return fn.apply(this,args)
} else {
return function(...moreArgs) {
return curried.apply(this,args.concat(moreArgs))
}
}
}
}

数组API的实现


forEach


Array.portotype.my_forEach = function(callback) {
for(let i=0;i<this.length;i++) {
callback(this[i],i,this)
}
}

map


Array.portotype.my_map = function(callback) {
let res= []
for(let i=0;i<this.length;i++) {
callback(this[i],i,this)&&res.push( callback(this[i],i,this))
}
return res
}

filter


Array.portotype.my_filter = function(callback) {
let res= []
for(let i=0;i<this.length;i++) {
callback(this[i], i, this) && res.push(this[i])
}
return res
}

前端模块化


问:你讲讲前端模块化吧
答:模块化的开发方式可以提高代码复用率,方便进行代码的管理,通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。


问:模块化有哪几种标准?
答:目前流行的js模块化规范有CommonJS、AMD、CMD以及Es6的模块系统


问:ES Modules 和CommonJS的一些区别
答:
1.使用语法层面,CommonJs是通过modules.exports,exports导出,require导入;ES Modules则是export导出,import导入
2.CommonJs是运行时加载模块,EsModules是在静态编译期间就确定模块的依赖
3.EsModulse在编译期间会将所有import提升到顶部,CommonJs不会提升require
4.CommonJs导出是一个值拷贝,会对加载结果进行缓存,一但内部再修改这个值,则不会同步到外部。ESModule是导出的一个引用,内部修改可以同步到外部
5. CommonJs中顶层的this指向这个模块本身,而ESModule中顶层this指向undefined
6. CommonJS加载的是整个模块,将所有的接口全部加载进来,ESModule可以单独加载其中的某个接口


vue的数据双向绑定的原理


vue的响应式原理是采用‘发布-订阅’的设计模式结合object.defineProperty()劫持各个属性的getter和setter,在数据发生变动时通过调用Deo.notity函数发布订阅给观察者watcher,让其更新响应的视图。


虚拟dom


虚拟dom是用来表现真实dom结果的javaScript对象树,是构建在浏览器真实dom上的抽象层,虚拟dom是可以直接在内存中操作的,可以通过diff算法来新旧dom的差异,将最终变化应用到真实dom上


diff算法


diff算法又称虚拟Dom的周界算法,vue的diff算法是通过深度优先、先序遍历的方式进行的,它将前后两个虚拟Dom树进行逐层比较,当找到某一层不一样的节点时,停止下降,然后比较这些节点的子节点,当所有的子节点都完成比较之后,算法会由下至上进行回溯,此过程被称为执行patch操作。在执行patch操作时,Vue对于不同类型的节点的更新方式也不同,对于元素节点可以更新他的属性和子节点;对于文本节点,只能更新它的文本内容;对于每个子节点,如果key值相同,可以进行复用或者重新排序,或者将其他的节点移动到这个位置。


vue中nextTick的理解及作用


使用场景描述:更改一个数据,导致dom元素的width发生了更改,但又要获取这个更新后的dom元素的width,可以用nextTick
vue2 中的nextTick是在下次Dom更新循环之后执行回调函数,并且是作为vue实例的方法调用的


this.$nextTick(() => { // 组件的DOM已经更新完毕,可以进行相应操作 // ... });

Vue 3的nextTick作为一个独立的函数导入,返回一个Promise,并且可以直接传递回调函数作为参数。这些变化使得Vue 3中的nextTick更加灵活和易于使用。


// Vue 3 
import { nextTick } from 'vue';
nextTick(() => { // 在下次DOM更新循环之后执行 });

vue在实例挂载的过程中发生了什么?




  1. 实例化:首先,Vue.js会创建一个新的Vue实例。在这个过程中,Vue.js会设置实例的各种属性和方法,包括数据对象、计算属性、方法、指令等。




  2. 编译模板:Vue.js会将模板编译成渲染函数。模板就是包含Vue特定语法的HTML代码。编译过程中,Vue.js会解析模板中的指令(如v-if、v-for等)和插值表达式(如{{ message }}),并将它们转换为JavaScript代码。




  3. 创建虚拟DOM:渲染函数会被调用,生成一个虚拟DOM树。虚拟DOM是对真实DOM的轻量级表示,它可以更高效地处理DOM的更新。




  4. 挂载:最后,Vue.js会将虚拟DOM渲染为真实DOM,并将其挂载到指定的元素上。这个过程通常在调用vm.$mount()方法或者在实例化Vue时传入el选项后发生。




  5. 更新:当数据变化时,Vue.js会重新执行渲染函数,生成新的虚拟DOM,并与旧的虚拟DOM进行对比(这个过程称为diff)。然后,Vue.js会根据diff结果,以最小的代价更新真实DOM。




这个过程中还会触发一系列的生命周期钩子,如created、mounted等,开发者可以在这些钩子中执行自己的代码。


vue2中data是一个函数而不是对象的原因


data之所以是一个函数,是因为一个组件可能会多处调用,而每一次调用就会执行data函数并返回新的数据对象,这样,可以避免多处调用之间的数据污染


vue2中给对象添加新属性界面页面不刷新


vue2是用Object.defineProperty实现数据响应式,而后面新增的属性,并没有通过Object.defineProperty设置成响应式数据,所以页面没变化,常用解决方式:



  • Vue.set()

  • Object.assign()

  • $forcecUpdated()


Vue SSR的实现原理


vue.js的ssR是一种在服务器上预渲染Vue.js应用程序的技术。



  1. 服务器接收请求:当服务器接收一个请求时,它会创建一个新的Vue实例。

  2. 创建渲染器:使用vue-server-renderer包创建一个渲染器。

  3. 渲染页面:服务器使用渲染器将Vue实例渲染为Html字符串。

  4. 发送响应:服务器将渲染后的Html字符串作为响应发送给客户端。

  5. 客户端接收响应:客户端接收到服务器的响应后,将HTML字符串解析为DOM并显示给用户。

  6. 激活(Hydration): Vue在客户端创建一个新的Vue实例,将其挂载到服务器收到的Dom上


keep-alive的使用


keep-alive的主要作用是缓存路由组件,以提高性能


<router-view v-slot="{ Component }">  
<keep-alive :include="permissionStore.keepAliveName">
<component :is="Component" :key="$route.path" />
</keep-alive>

</router-view>



  1. router-view是 Vue Router 的一个组件,用于渲染当前路由对应的组件。




  2. v-slot="{ Component }" 是一个插槽,用于获取当前路由对应的组件。




  3. keep-alive 是 Vue 的一个内置组件,用于缓存组件,避免重复渲染。




  4. :include="permissionStore.keepAliveName" 是 的一个属性,表示只有名称在 permissionStore.keepAliveName 中的组件会被缓存。




  5. 是一个动态组件,:is="Component" 表示组件的类型由 Component 决定,:key="$route.path" 表示每个路由路径对应一个唯一的组件实例。




Vue项目中有封装axios吗?主要是封装哪方面的?



  • 1.封装前需要和后端协商好一些约定,请求头,状态码,请求时间....

  • 2.设置接口请求前缀:根据开发、测试、生产环境的不同,前缀需要加以区分

  • 3.移除重复的请求,如果请求在pending中,提示'操作太频繁,请稍后再试'

  • 4.用map结构根据相应状态码处理错误信息

  • 5.请求拦截,若headers中没有token的,移除请求

  • 6.响应拦截器,例如服务端返回的message中有'message',提示'请求超时,请刷新网页重试'

  • 7.请求方法的封装,封装get、post请求方法,使用起来更为方便


css预处理器


css预处理器扩充了css语言,增加了诸如变量、混合(mixin)、函数等功能,让css更易维护、方便。本质上。预处理是css的超集。包含一套自定义的语法及一个解析器,根据这些语法定义自己的样式规则,这些规则最终会通过解析器编译生成对应的css文件。


如何实现上拉加载


image.png
触底公式:


scrollTop + clientHeight >= scrollHeight

简单实现:


    let clientHeight = document.documentElement.clientHeight;//浏览器高度
let scrollHigiht = documnet.body.scrollHeight;//元素内容高度的度量,包括由于溢出导致的视图中不可见内容
let scrollTop = documnet.body.scrollTop; //滚动视窗的高度距离`window`顶部的距离
let distance = 50; //距离视窗还用50的时候,开始触发;

if ((scrollTop + clientHeight) >= (scrollHeight - distance)) {
console.log("开始加载数据");
}

如何实现下拉刷新


关于下拉刷新的原生实现,主要分成三步:



  1. 监听原生touchstart事件,记录其初始位置的值,e.touches[0].pageY;

  2. 监听原生touchmove事件,记录并计算当前滑动的位置值与初始位置值的差值,大于0表示向下拉动,并借助CSS3的3. translateY属性使元素跟随手势向下滑动对应的差值,同时也应设置一个允许滑动的最大值

  3. 监听原生touchend事件,若此时元素滑动达到最大值,则触发callback,同时将translateY重设为0,元素回到初始位置。


封装和使用JSBrige




  1. 定义协议:首先,需要定义一种协议,用于约定H5页面与App之间的通信规则。这可以是一组自定义的URL Scheme或JavaScript函数。




  2. 注册事件监听:在H5页面中,通过JavaScript代码注册事件监听器,用于接收来自App的消息或回调。可以使用window.addEventListener或其他类似的方法来监听特定的事件。




  3. 发送消息给App:在H5页面中,通过调用JSBridge提供的方法,将消息发送给App。这可以是通过修改URL Scheme的方式,或者调用App提供的JavaScript接口。




  4. 处理App的消息或回调:在App原生代码中,通过监听URL Scheme或执行JavaScript代码的方式,接收来自H5页面的消息或回调。根据协议约定,处理相应的逻辑或调用相应的功能。




  5. 回调结果给H5页面:在App原生代码中,根据协议约定,将处理结果或回调信息发送回H5页面。可以通过修改URL Scheme的方式,或者调用H5页面中注册的JavaScript回调函数。




个人博客


耀耀切克闹 (yaoyaoqiekenao.com)


gitHub


DarknessZY (zhangyao) (github.com)


作者:耀耀切克闹灬
来源:juejin.cn/post/7291834381315719220
收起阅读 »

丈母娘说:有编制的不如搞编程的

10月17日百度世界大会召开,据说文心大模型又牛X了,综合水平相比GPT4毫不逊色,真是个让人激动的消息,国产大模型的进展可以说是日新月异,我这耳朵边一直响彻四个字:遥遥领先。 不过今天我关注的重点不是什么大模型,而是发布会上的一件趣事:相亲。这大模型和相亲有...
继续阅读 »

10月17日百度世界大会召开,据说文心大模型又牛X了,综合水平相比GPT4毫不逊色,真是个让人激动的消息,国产大模型的进展可以说是日新月异,我这耳朵边一直响彻四个字:遥遥领先。


不过今天我关注的重点不是什么大模型,而是发布会上的一件趣事:相亲。这大模型和相亲有什么关系呢?给大家说关系密切,各位男女光棍们一定要抓住这个机会。


话说在百度世界大会上出现了一位神秘的阿姨,她既不懂AI,也不懂编程,那她来干什么呢?这位阿姨拿着厚厚一叠的宣传页,见到长的有点像李彦宏的帅哥,就赶紧贴身向前,就着宣传页向对方一顿输出,热情开朗,情真意切,搞得小哥哥们都有点不好意思了。


这位阿姨在干什么呢?


原来这位阿姨是给自己的女儿来相亲的。阿姨的女儿是99年的,职业是小学老师,按说这个年龄、这个身份也应该不愁嫁吧。阿姨是怎么想的呐?


阿姨说,这可比那个什么相亲公园靠谱多了,都是高智商的人,都是搞AI的,就有编制的也不如搞编程的,这将来挣钱的一定是搞AI的。男怕入错行,女怕嫁错郎,这男人入对了行,将来挣钱源源不断!


图片


我想这位阿姨一定是受到了张雪峰老师的启发,张老师在之前的直播中就给女孩子的家长们建议过,孩子学习不好,报个计算机专业,但是进去的主要目的不是学计算机,而是找个聪明的小哥哥,以后的生活比较有着落,原因相信大家都懂,挣钱多呗!


记得以前相亲,丈母娘都是看你有没有房,还有人说中国的房价都是丈母娘搞上去的。现在房价涨不动了,以后可能还会跌跌不休,我想其中的一个原因可能是丈母娘的眼光变高了,钱财当然很重要,但是你脑子不够也不行啊,得能挣钱才行,否则就是坐吃山空,不得长久。


再看看各位网友对这件事的看法:



阿姨政策吃的透透的,风口看的准准的!


大妈打算盘的声音,我在合肥都听到了!


阿姨超有远见!


看人家妈妈的行动力,行动起来啊妈妈



图片图片


看到这里的各位男女光棍们,各位愁娶愁嫁的家长们,赶紧行动起来吧,每年甚至每周都有很多这样的技术大会,网上搜索一大堆,大会上有男有女,大部分都是高科技人才,将来前途和钱途都不可限量。多去看看,多去走走,说不定就能找到对眼的意中人,这个概率可是很高的,是经过丈母娘认可的。


最后,丈母娘也不是好糊弄的!大家一定要有真才实学。给同学们一些建议,打好基础,打好基础,技术不是东拼西凑就可以做好的,勿在浮沙建高台,容易倒;另外多实践多总结,多沟通多交流。


作者:萤火架构
来源:juejin.cn/post/7291847731459096627
收起阅读 »

如何在Java项目中实现漂亮的日志输出

  日志是开发过程中不可或缺的一部分,它可以帮助我们追踪代码的执行过程、排查问题以及监控系统运行状况。然而,大多数开发人员在编写日志时往往只关注于输出必要的信息,而忽略了日志的可读性和美观性。本文将介绍如何在Java项目中实现漂亮的日志输出,提供一些实用的技巧...
继续阅读 »

  日志是开发过程中不可或缺的一部分,它可以帮助我们追踪代码的执行过程、排查问题以及监控系统运行状况。然而,大多数开发人员在编写日志时往往只关注于输出必要的信息,而忽略了日志的可读性和美观性。本文将介绍如何在Java项目中实现漂亮的日志输出,提供一些实用的技巧和建议。



image.png



1. 使用合适的日志框架



  Java有许多优秀的日志框架可供选择,如Log4j、Logback和java.util.logging等。选择一个适合你项目需求的日志框架是实现漂亮日志输出的第一步。这些框架提供了丰富的配置选项,可以帮助你控制日志的格式和输出方式。这里对几个日志框架做一下简单的介绍。


Log4j


  Log4j是一个Java日志处理的框架,用于在Java应用程序中处理日志记录。它提供了一种灵活的方式来记录日志信息,并允许开发者根据需要配置日志输出的格式和目标。


  在Log4j中,主要有三个组件:Logger、Appender和Layout。Logger用于记录日志信息,Appender用于定义日志的输出目标,例如控制台、文件、数据库等,Layout用于定义日志的输出格式。


  以下是一个简单的Log4j代码示例:


import org.apache.log4j.Logger;  

public class MyApp {
// 获取Logger实例
final static Logger logger = Logger.getLogger(MyApp.class);

public static void main(String[] args) {
// 记录不同级别的日志信息
logger.debug("Debugging information");
logger.info("Informational message");
logger.warn("Warning");
logger.error("Error occurred");
logger.fatal("Fatal error occurred");
}
}

  在这个示例中,我们首先导入了Logger类,然后通过Logger.getLogger(MyApp.class)获取了一个Logger实例。在main方法中,我们使用Logger实例记录了不同级别的日志信息,包括Debug、Info、Warn、Error和Fatal。


Logback


  Logback是Log4j的改进版本,是SLF4J(Simple Logging Facade for Java)下的一种日志实现。与Log4j相比,Logback具有更高的性能和更灵活的配置。


  Logback的组件包括Logger、Appender、Encoder、Layout和Filter,其中Logger是最常用的组件。Logger分为rootLogger和nestedLogger,rootLogger是所有Logger的根,nestedLogger则是rootLogger的子级。Logger之间有五个级别,从高到低依次为ERROR、WARN、INFO、DEBUG和TRACE,级别越高,日志信息越重要。


  以下是一个简单的Logback代码示例:


import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;

public class MyClass {
private static final Logger logger = LoggerFactory.getLogger(MyClass.class);
public void myMethod() {
logger.debug("Debug message");
logger.info("Info message");
logger.warn("Warn message");
logger.error("Error message");
}
}

logging


  java.util.logging是Java平台的核心日志工具。


  java.util.logging由Logger、Handler、Filter、Formatter等类和接口组成。其中Logger是日志记录器,用于记录日志信息;Handler是处理器,用于处理日志信息;Filter是过滤器,用于过滤不需要记录的日志信息;Formatter是格式化器,用于格式化日志信息。


  这里介绍的日志框架,在项目当中运用的比较多的是Log4j、Logback,从基本的配置上没有太大的差异,大家也可以根据项目需求选择使用。



2. 定义清晰的日志级别



  在Java项目中,定义清晰的日志级别是非常重要的,以便在调试、监控和解决潜在问题时有效地记录和理解系统行为。下面是一些建议,可以帮助你定义清晰的日志级别:



  1. 了解常见的日志级别:Java中常见的日志级别包括DEBUG、INFO、WARN、ERROR和FATAL。每个级别都有特定的含义和用途,首先要了解这些级别的含义。

  2. 根据项目需求确定日志级别:在定义日志级别时,需要考虑项目的需求和目标。例如,对于一个简单的演示应用程序,可能不需要记录过多的调试信息。但对于一个复杂的业务系统,可能需要详细的调试信息来跟踪和解决潜在的问题。根据项目的重要性和规模来确定每个级别的日志信息是否必要。

  3. 默认级别设置:为项目设置一个默认的日志级别。这通常是INFO级别,用于记录系统的常规操作信息。

  4. 根据模块或功能设置日志级别:为每个模块或功能设置不同的日志级别。这有助于在特定部分出现问题时快速定位问题原因。例如,对于数据库模块,可以将其日志级别设置为DEBUG,以便记录详细的数据库操作信息。

  5. 日志级别继承:在一个日志级别下定义的日志信息,应该继承到其所有子级别中。这意味着,如果某个日志信息被设置为WARN级别,那么该信息应该同时出现在WARN、ERROR和FATAL日志中。

  6. 日志信息清晰明了:在记录日志信息时,要确保信息清晰明了,包含必要的细节。例如,对于错误信息,要包含错误类型、发生错误的方法和时间戳等信息。

  7. 日志轮转和清理:及时对日志进行轮转和清理,避免日志文件过大而影响系统性能。可以设置一个合适的大小限制或时间间隔,对旧的日志文件进行归档和清理。

  8. 培训开发人员:为开发人员提供关于如何使用日志系统的培训,确保他们了解如何记录适当的日志信息以及如何利用日志级别进行过滤。

  9. 参考最佳实践:可以参考一些关于日志编写的最佳实践指南,例如Log4j的官方文档,以获取更多关于如何定义清晰日志级别的建议。


  定义清晰的日志级别对于Java项目来说非常重要。通过了解常见的日志级别、根据项目需求确定级别、设置默认级别、按模块或功能划分级别、继承级别、记录清晰明了的日志信息、及时轮转和清理以及培训开发人员等措施,可以帮助你在项目中实现定义清晰、易于理解和使用的日志级别。



3. 格式化日志输出



  下面以Log4j为例,介绍如何格式化日志输出。


1,引入Log4j依赖


  在Maven项目中,可以在pom.xml文件中添加以下依赖:


<dependency>  
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.x.x</version>
</dependency>

2. 配置日志格式


  在log4j2.xml配置文件中,可以使用PatternLayout类来配置日志格式。例如,以下配置将日志输出为每行包含时间戳、日志级别、线程名和消息的格式:


<?xml version="1.0" encoding="UTF-8"?>  
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

  其中,%d表示时间戳,%t表示线程名,%-5level表示日志级别(使用五个字符的宽度),%logger{36}表示最长为36个字符的Logger名称,%msg表示消息。在配置文件中可以根据需要调整格式。


3. 在代码中使用Log4j记录日志


在Java代码中,可以使用以下语句记录日志:


import org.apache.logging.log4j.LogManager;  
import org.apache.logging.log4j.Logger;

public class MyClass {
private static final Logger logger = LogManager.getLogger(MyClass.class);

public static void main(String[] args) {
logger.debug("Debug message");
logger.info("Info message");
logger.warn("Warn message");
logger.error("Error message");
}
}

  在输出结果中,可以看到每条日志信息都符合之前配置的格式。可以使用不同的配置文件来调整日志格式,以满足不同的需求。



4. 日志轮转和切割



  志切割和轮转在Log4j中主要通过两种策略实现:基于大小(Size-based)和基于日期时间(Time-based)。


1. 基于大小的日志切割和轮转


  这种策略是当日志文件达到指定大小时,会进行切割或轮转。例如,你可以设置当日志文件达到100MB时进行轮转。


<RollingFile name="File" fileName="logs/app.log" filePattern="logs/app-%d{yyyy-MM-dd}.log.gz">  
<PatternLayout>
<pattern>%d %p %c{1.} [%t] %m%n</pattern>
</PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
<DefaultRolloverStrategy max="20"/>
</RollingFile>

  在上述配置中,当app.log文件达到100MB时,它会被切割并存储为app-yyyy-MM-dd.log.gz。并且最多保留20个这样的文件。


2. 基于日期时间的日志切割和轮转


  这种策略是当达到指定的日期时间时,进行日志切割或轮转。例如,你可以设置每天凌晨1点进行轮转。


<RollingFile name="File" fileName="logs/app.log" filePattern="logs/app-%d{yyyy-MM-dd}.log.gz">  
<PatternLayout>
<pattern>%d %p %c{1.} [%t] %m%n</pattern>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
</Policies>
<DefaultRolloverStrategy max="30"/>
</RollingFile>

  在上述配置中,每天凌晨1点,app.log文件会被切割并存储为app-yyyy-MM-dd.log.gz。并且最多保留30个这样的文件。


  注意:<DefaultRolloverStrategy max="20"/> 或 <TimeBasedTriggeringPolicy interval="1"/> 中的数字可以根据你的实际需要进行调整。



5. 日志过滤器(Filter)的使用



  Log4j中的过滤器(Filter)用于在日志事件发生之前对其进行一些条件判断,以决定是否接受该事件或者更改该事件。这可以让你根据特定的条件过滤日志输出,例如只打印错误级别以上的日志,或者根据线程ID、请求ID等过滤日志。


  在Log4j 2中,你可以通过配置文件(例如log4j2.xml)来为日志事件指定过滤器。以下是一个使用Log4j 2的XML配置文件中的过滤器的示例:


<?xml version="1.0" encoding="UTF-8"?>  
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
<Filters>
<ThresholdFilter level="ERROR"/>
<MarkerFilter marker="FLOW" onMatch="DENY"/>
<MarkerFilter marker="EXCEPTION" onMatch="DENY"/>
</Filters>
</Console>
</Appenders>
<Loggers>
<Root level="all">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

  在这个例子中,我们使用了三个过滤器:


  (1). ThresholdFilter只接受级别为ERROR或更高级别的日志事件。


  (2). 第一个MarkerFilter会拒绝任何带有"FLOW"标记的日志事件。


  (3). 第二个MarkerFilter会拒绝任何带有"EXCEPTION"标记的日志事件。


  另外,你还可以创建自定义的过滤器,只需实现org.apache.logging.log4j.core.filter.Filter接口即可。然后你可以在配置文件中通过指定类全名来使用你的过滤器。


  对于更复杂的过滤需求,你还可以使用Condition元素,它允许你使用Java代码来决定是否接受一个日志事件。不过,请注意,因为这可能会影响性能,所以应谨慎使用。


  下面是实际项目中打印的日志,大家可以根据项目的需求满足日志打印的需求。


image.png



总结



  通过选择合适的日志框架、定义清晰的日志级别、格式化日志输出、添加时间戳和线程信息、使用日志分级以及处理异常和堆栈跟踪,我们可以实现在Java项目中打印漂亮的日志。漂亮的日志输出不仅可以提高代码的可读性,还可以帮助我们更好地理解和跟踪代码的执行过程,从而提高开发效率和系统稳定性。


拓展阅读


# 35岁愿你我皆向阳而生


# 深入解读Docker的Union File System技术


# 说一说注解@Autowired @Resource @Reference使用场景


# 编写Dockerfile和构建自定义镜像的步骤与技巧


# 说一说Spring中的单例模式


# MySQL的EXPLAIN用法


# Spring的Transactional: 处理事务的强大工具


作者:mikezhu
来源:juejin.cn/post/7291675889381031990
收起阅读 »

HashMap扩容机制跟你的工作真的不相关吗?

说来话长,这事还得从我第一份工作说起,那时候纯纯大菜鸡一个,啥也不会,工作中如履薄冰,举步维艰,满屏荒唐码,一把辛酸泪😭 再说句题外话,如果你是中高级程序员,建议您划走离开,否则这篇文章可能会浪费您的宝贵时间☺️ 那年 OK,书归正传,得益于本人工作态度良好...
继续阅读 »

说来话长,这事还得从我第一份工作说起,那时候纯纯大菜鸡一个,啥也不会,工作中如履薄冰,举步维艰,满屏荒唐码,一把辛酸泪😭



再说句题外话,如果你是中高级程序员,建议您划走离开,否则这篇文章可能会浪费您的宝贵时间☺️


那年


OK,书归正传,得益于本人工作态度良好,同事和领导都给予了我很大的帮助,只记得那是18年的平常打工人的一天,我写了如下很多打工人都会写,甚至每天都在写的代码(当时的具体代码已经记不清了,现在大概模拟一下案发场景):


    /**
* 从Order对象中获取id属性并包装成List返回
*
* @param orderList Student列表
* @return idList
*/

public List<Long> getOrderIds(List<Order> orderList) {
List<Long> ids = new ArrayList<>();
for (Order order : orderList) {
ids.add(order.getId());
}
return ids;
}


对没错,用Stream流可以一行代码解决这个问题,但当时受限于我们使用的JDK还是1.6和1.7,你懂得



我的直属领导看了我的代码后首先问我,你知道ArrayList初始化容量是多少吗?他是怎么扩容的?


我:。。。。。
img


这俩问题对现在的程序员来说兼职就是小菜一碟,不值一提,但对当时的我来说,可就有亿点难度了,之前面试之前依稀在那个博客上看别人写过,于是乎我就照着脑袋里模糊不清的知识点模棱两可的回答了这俩问题,emmm,


于是乎我领导就跟我说,既然你知道List容量不够会扩容,扩容会带来性能损耗(这个日后再细说,先说正事)那么你应该这么写,来避免它扩容呢?


    public List<Long> getOrderIds(List<Order> orderList) {
List<Long> ids = new ArrayList<>(orderList.size());
for (Order order : orderList) {
ids.add(order.getId());
}
return ids;
}


千万不要小看这些细节哦



听君一席话,如听一席话,于是我悟了,


从那以后再有类似集合初始化的场景,明确知道容量的场景我都会初始化的时候传入构造参数,避免其扩容,无法知道确切容量的时候也会预估一下容量 尽可能的避免减少扩容次数。


去年


时间来到2022年,去年,我已经不是当年的那个懵懵懂懂愣头青了,坐我旁边的一个哥们(技术比我当年强多了去了),他写了一段初始化HashMap的代码也传入了一个初始容量,代码如下:


    public Map<Long, Order> xxx(List<Order> orderList) {
Map<Long, Order> orderMap = new HashMap<>(orderList.size());
for (Order order : orderList) {
orderMap.put(order.getId(), order);
}
return orderMap;
}

img


敲黑板,重点来了,前面铺垫了那么多,就是为了说这事


历史惊奇在这一天重演,只不过负责问问题的是我


img


Q: 咳咳~HashMap的初始容量是16,放第几个个元素的时候会触发扩容呢(这题简单)


A: 元素个数超过16x0.75=12的时候进行扩容呗,扩容为16x2=32


Q: 既然容量为16,只能存12个元素,超过就会扩容,那么你写的new HashMap<>(orderList.size()) 这个能防止扩容吗?


A: emmm,不能


Q: 那初始化容量应该设置多少呢?


A: ……


Q: 16x0.75=12这个计算公式中, 初始容量变成未知假设为N 需存放的元素个数为20 Nx0.75=20N 是多少?(这大概就是经典的大学数学题吧)


A: 20➗0.75呗, 26.666 四舍五入27个, 设置容量为27,可以存放20个元素并且不触发扩容


img


所以正确的代码应该这么写: new HashMap<>((int) (orderList.size / 0.75 + 1))


别问为啥要+1,问就是因为小数转成int不会四舍五入直接舍弃小数点后的部分


一次轻松的对话就此结束


来看下大佬们是怎么写的


google的guava包 这是一个非常常用的java开发工具包,我从里面真的学到了很多(后续单独开篇文章记录一下)


//入口
HashMap<String, String> map= Maps.newHashMapWithExpectedSize(20);

public static <K extends @Nullable Object, V extends @Nullable Object>
HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {
return new HashMap<>(capacity(expectedSize));
}

// 看这里看这里, 还考虑了一些其他的情况,专业!
static int capacity(int expectedSize) {
if (expectedSize < 3) {
checkNonnegative(expectedSize, "expectedSize");
return expectedSize + 1;
}
if (expectedSize < Ints.MAX_POWER_OF_TWO) {
// This is the calculation used in JDK8 to resize when a putAll
// happens; it seems to be the most conservative calculation we
// can make. 0.75 is the default load factor.
return (int) ((float) expectedSize / 0.75F + 1.0F);
}
return Integer.MAX_VALUE; // any large value
}

大佬写的代码就是专业! img


org.apache.curator包 无意之间发现的,实现有点意思


//这里写法一样
HashMap<String, String> map = Maps.newHashMapWithExpectedSize(20);


public static <K, V> HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {
return new HashMap(capacity(expectedSize));
}

//看这里 看这里 expectedSize + expectedSize / 3
static int capacity(int expectedSize) {
if (expectedSize < 3) {
CollectPreconditions.checkNonnegative(expectedSize, "expectedSize");
return expectedSize + 1;
} else {
return expectedSize < 1073741824 ? expectedSize + expectedSize / 3 : Integer.MAX_VALUE;
}
}

expectedSize + expectedSize / 3 说实话第一次看到这段代码的时候还是有点懵的,wocc这是啥写法,后来用几个数值带入计算了一下,还真是那么回事 👍🏻👍🏻


Hutool工具包


//入口
HashMap<String, String> map = MapUtil.newHashMap(20);

public static <K, V> HashMap<K, V> newHashMap(int size) {
return newHashMap(size, false);
}

//看这里, 平平无奇,什么档次?代码跟我写的一样,😄
public static <K, V> HashMap<K, V> newHashMap(int size, boolean isOrder) {
int initialCapacity = (int)((float)size / 0.75F) + 1;
return (HashMap)(isOrder ? new LinkedHashMap(initialCapacity) : new HashMap(initialCapacity));
}


说实话这个实现相较前者来说就显得不那么细了,居然跟我写的一样。。。


image-20231019160132153

这件事情带来的思考


说起HashMap的知识点,晚上的文章博客简直满天飞,大家现在谁还不能说上几句,但是! 后来在我面试的很多初中级开发时,我问他们准备往Map中存放20个元素,初始化容量设置多少不会触发扩容 时,基本上很少有人能答上来,10个人当中差不多有一个能回答上来?为什么会这样呢? 明明这些人是懂的初始容量16,超过出初始容量的75%会触发扩容,反过来问一下就不会了~😒 这充分说明了,学习要融会贯通举一反三,要细!!!


那段代码现在怎么写


据说JDK都出到21了,最近没怎么关注过~
不过JDK8已经流行很久了,那段代码用JDK8应该这么写:



  • list


public List<Long> getOrderIds(List<Order> orderList) {
List<Long> ids = new ArrayList<>(orderList.size());
for (Order order : orderList) {
ids.add(order.getId());
}
return ids;
}

//一行代码搞定,简洁明了
public List<Long> getOrderIds(List<Order> orderList) {
return orderList.stream().map(Order::getId).collect(Collectors.toList());
}

通过StreamCollectors.toList()来返回一个崭新的List,难道就没人好奇他这个List创建的时候有没有指定容量呢?如过不指定,在上面说到的那些明确知道存放容量的场景里岂不是要白白的扩容耗费性能???


答案是:NO 我们看下来Collectors.toList()的实现


public static <T>
Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
(left, right) -> { left.addAll(right); return left; },
CH_ID);
}

static class CollectorImpl<T, A, R> implements Collector<T, A, R> {
private final Supplier<A> supplier;
private final BiConsumer<A, T> accumulator;
private final BinaryOperator<A> combiner;
private final Function<A, R> finisher;
private final Set<Characteristics> characteristics;

CollectorImpl(Supplier<A> supplier,
BiConsumer<A, T> accumulator,
BinaryOperator<A> combiner,
Function<A,R> finisher,
Set<Characteristics> characteristics) {
this.supplier = supplier;
this.accumulator = accumulator;
this.combiner = combiner;
this.finisher = finisher;
this.characteristics = characteristics;
}

CollectorImpl(Supplier<A> supplier,
BiConsumer<A, T> accumulator,
BinaryOperator<A> combiner,
Set<Characteristics> characteristics) {
this(supplier, accumulator, combiner, castingIdentity(), characteristics);
}

CollectorImplCollectors中的一个内部类,构造函数的第一个参数是Supplier<A> supplier这是一个函数式接口,就是说你得传给我一个实现,告诉我应该如何去创建一个集合,上面是这么传参的ArrayList::new, 这个写法其实就是new ArrayList(), 看到没!他并没有指定集合容量哦~~~


那么如果想提前指定好集合容量应该怎么写呢? 不卖关子了,直接贴代码了,写个B博客,真TM累死个人😌


public List<Long> getOrderIds(List<Order> orderList) {
return orderList.stream().map(Order::getId).collect(Collectors.toCollection(() -> new ArrayList<>(orderList.size())));
}

这就行了,看下Collectors.toCollection()的源码


public static <T, C extends Collection<T>>
Collector<T, ?, C> toCollection(Supplier<C> collectionFactory) {
return new CollectorImpl<>(collectionFactory, Collection<T>::add,
(r1, r2) -> { r1.addAll(r2); return r1; },
CH_ID);
}

这和Collectors.toList()基本上市一样的,只不过Collectors.toCollection()把如何创建集合的这个步骤抽象起来叫给我们开发者来个性化实现了,是不是又学到了一招~~~(#^.^#)



  • map


public Map<Long, Order> xxx(List<Order> orderList) {
Map<Long, Order> orderMap = new HashMap<>(orderList.size());
for (Order order : orderList) {
orderMap.put(order.getId(), order);
}
return orderMap;
}


//这点破代码用Stream也是分分钟搞定
public Map<Long, Order> xxx(List<Order> orderList) {
return orderList.stream().collect(Collectors.toMap(Order::getId, Function.identity(), (k1,k2) -> k1));
}

和上面的List一样,这玩意初始化Map的时候也没有指定容量


public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}

Map的创建也是通过一个函数式接口Supplier<M> mapSupplier定义的,传的参数是 HashMap::new,这也是一个方法引用,写法等于 new hashMap(), 想指定容量怎么办呢? 看代码


    public Map<Long, Order> xxx(List<Order> orderList) {
return orderList.stream()
.collect(Collectors.toMap(Order::getId, Function.identity(), (k1,k2) -> k1,
() -> new HashMap<>((int) (orderList.size() / 0.75 + 1))));
}

这个写法我们同样自己掌控如何创建需要的Map,容量自己定~


写在末尾


没啥要写的了,就这吧,累挺
img


作者:码塞顿开
来源:juejin.cn/post/7291828982558425142
收起阅读 »

01.你为什么需要学习K8S

前言 在"云原生"、"应用上云"火热的这几年,相信大家或多或少都听说过K8S这个可以称得上是容器编排领域事实的存在。 可以看出在2017年之后,K8S热度远超容器编排领域的另外两个工具Mesos和Docker Swarm,并将它们甩开了几十条街,成为了整个容...
继续阅读 »

前言



在"云原生"、"应用上云"火热的这几年,相信大家或多或少都听说过K8S这个可以称得上是容器编排领域事实的存在。


可以看出在2017年之后,K8S热度远超容器编排领域的另外两个工具MesosDocker Swarm,并将它们甩开了几十条街,成为了整个容器编排领域的龙头。



随着现在越来越多的企业把业务系统上云之后,大部分的服务都运行在Kubernetes环境中,可以说Kubernetes已经成为了云、运维和容器行业最炙手可热的工具,这也是为什么需要学习Kubernetes最重要的原因。


目前,AWS、Azure、Google、阿里云、腾讯云等主流公有云提供的是基于Kubernetes的容器服务。Rancher、CoreOS、IBM、Mirantis、Oracle、Red Hat、VMWare等无数厂商也在大力研发和推广基于Kubernetes的PaaS产品。


目前国内容器服务平台做的比较好的有腾讯云容器服务TKE阿里云容器服务ACK,它们都是基于K8S做的二开,有兴趣的读者可以自己了解和尝试使用。


K8S是什么?


K8S是单词Kubernetes的缩写,这个单词在古希腊语中是 [舵手] 的意思,之所以简称其为K8S,是因为'K'字母与'S'字母之间隔着八个单词,为了简便称呼,于是有了K8S这个简称。


K8S起初是Google内部的一个名为Borg的系统,据说Google有超过二十亿的容器运行在Borg上,在积累了十几年的经验之后,Google在2014年重写并开源了该项目,改名为Kubernetes


K8S在基于容器部署的方式上,提供了一个弹性分布式的框架,支持服务发现与负载均衡、存储、自动部署回滚、自动计算与调度、自动扩缩容等等一系列操作,目的是方便开发者不再需要关注服务运行细节,K8S能够自动进行容器与Pod调度、扩缩容、自动重建等等操作,保证服务尽可能健康的运行。


一句话来概括:K8S解放了开发者的双手,能够最大程度的让部署的服务健康运行,同时能够接入很多第三方工具(如服务监控、数据采集等等),满足开发者的定制化需求。


部署演变之路



传统部署时代


在互联网开发早期,开发者会在物理服务器上直接运行应用程序。以一个Go Web程序举例,很典型的一个部署方式是首先在本地编译好对应的二进制文件,之后上传到服务器,然后运行应用。


由于无法限制在物理服务器中运行的应用程序资源使用,因此会导致资源分配问题。例如,如果在同一台物理服务器上运行多个应用程序,则可能会出现一个应用程序占用大部分资源的情况,从而导致其他应用程序的性能下降。


虚拟化部署时代


为了解决上述问题,虚拟化技术被引入了。虚拟化技术允许你在单个物理服务器上运行多个虚拟机(VM)。虚拟化能够使应用程序在不同VM之间被彼此隔离,且能提高一定的安全性,因为一个应用程序的信息不能被另一应用程序随意访问。


虚拟化能够更好地利用物理服务器的资源,并且因为可以轻松地添加或者更新应用程序,而因此可以具有更高的扩缩容性,以及降低硬件成本等等的好处。通过虚拟化,可以将一组物力资源呈现为可丢弃的虚拟机集群。每个VM是一台完整的计算机,在虚拟化硬件之上运行所有的组件,包括自身的操作系统Guest OS


容器部署时代


容器类似于VM,但是具有更轻松的隔离特性,使得容器之间可以共享操作系统Host OS,并且容器不会像VM那样虚拟化硬件,例如打印机等等,只是提供一个服务的运行环境。



通常一台物理机只能运行十几或者数十个VM,但是可以启动成千上万的容器。因此,容器和VM比起来是更加轻量级的,且具有和VM一样的特性:每个容器都具有自己的文件系统、CPU、内存、进程空间等。


我们可以简单理解为:一个VM已经是一台完整的计算机了,而容器只是提供了一个服务能够运行的所有环境。


同时,因为容器与基础架构分离,因此可以跨云和OS发行版本进行移植。


容器部署具有以下优势



  • 敏捷部署:比起VM镜像,提高了容器镜像创建的简便性和效率。

  • DEVOPS:由于镜像的不可变性,可以通过快速简单的回滚,提供可靠并且频繁的容器镜像构建和部署。

  • 开发与运维的隔离:在构建、发布的时候创建应用程序容器镜像,而不是在部署的时候,从而将应用程序和基础架构分离。

  • 松耦合:符合微服务架构思想,应用程序被分解成一个个小服务运行在不同的容器中,可以动态部署和管理。

  • 软件/硬件层面隔离:通过namespace实现操作系统层面的隔离,如隔离不同容器之间的文件系统、进程系统等等;通过cgroup实现硬件层面的隔离,提供物理资源上的隔离,避免某些容器占用过多的物理资源CPU、Memory、IO影响到其他容器中的服务质量。


容器时代之后:Serveless


容器阶段之后,虚拟化仍然还在不断演化和衍生,产生了Serveless这个概念。


Serveless英文直译过来的意思是无服务器,这不代表着它真的不需要服务器,而是说服务器对用户不可见了,服务器的维护、管理、资源分配等操作由平台开发商自行维护。一个Serveless很经典的实现就是云函数,即最近火热的FAAS(Function As A Service),函数即服务。


Serveless并不是一个框架或者工具,它本质上是一种软件架构思想,即:用户无需关注应用服务运行的底层资源,比如CPU、Memory、IO的状况,只需要关注自身的业务开发。


Serveless具有以下特点



  • 无穷弹性计算能力:服务应该做到根据请求数量自动水平扩容实例,并且平台开发商应该提供无限的扩容能力。

  • 无需服务器:不需要申请和运维服务器。

  • 开箱即用:无需做任何适配,用户只需要关注自身业务开发,并且能够做到精确的按量计费。


强大的K8S


想像一个场景,假设我们现在把一个微服务架构的程序部署在成百上千个容器上,这些容器分部在不同的机器上,这个时候管理这些容器是一件非常让人头疼的事情。


让我们想想管理这些容器可能会碰到的问题,例如:



  1. 某个容器发生故障,这个时候我们是不是该启动另一个容器?

  2. 某台机器负载过高,那么我们之后的容器是不是不能部署在这台机器上?

  3. 某个服务请求量突增,我们是不是应该多部署几个运行该服务的容器?

  4. 如果某些容器之间需要相互配合怎么办?比如容器A需要容器B的资源,所以容器A一定要在容器B之后运行。

  5. 运行多个容器时,我怎么做到它们的运行结果是原子性的?即要么全部成功,或者全部失败。亦或者如果某一个容器失败,我能够不断重启这个容器以达到我的预期状态。


以上问题,都可以交给K8S来解决,它提供了一系列的功能来帮助我们轻松管理和编排容器,以达到我们的预期状态,同时因为它本身也是一个分布式高可用的组件,所以无需担心K8S出问题。


K8S官方文档这么描述它的功能:



  • 服务发现和负载均衡 Kubernetes 可以使用 DNS 名称或自己的 IP 地址来暴露容器。 如果进入容器的流量很大, Kubernetes 可以负载均衡并分配网络流量,从而使部署稳定。

  • 存储编排 Kubernetes 允许你自动挂载你选择的存储系统,例如本地存储、公共云提供商等。

  • 自动部署和回滚 你可以使用 Kubernetes 描述已部署容器的所需状态, 它可以以受控的速率将实际状态更改为期望状态。 例如,你可以自动化 Kubernetes 来为你的部署创建新容器, 删除现有容器并将它们的所有资源用于新容器。

  • 自动完成装箱计算 你为 Kubernetes 提供许多节点组成的集群,在这个集群上运行容器化的任务。 你告诉 Kubernetes 每个容器需要多少 CPU 和内存 (RAM)。 Kubernetes 可以将这些容器按实际情况调度到你的节点上,以最佳方式利用你的资源。

  • 自我修复 Kubernetes 将重新启动失败的容器、替换容器、杀死不响应用户定义的运行状况检查的容器, 并且在准备好服务之前不将其通告给客户端。

  • 密钥与配置管理 Kubernetes 允许你存储和管理敏感信息,例如密码、OAuth 令牌和 SSH 密钥。 你可以在不重建容器镜像的情况下部署和更新密钥和应用程序配置,也无需在堆栈配置中暴露密钥


什么人需要学习K8S


运维/运开工程师


随着部署模式的演变,现在企业的应用几乎都以容器的方式在开发、测试、生产环境中运行。掌握基于K8S的容器编排工具的运维、开发能力将成为运维/运开工程师的核心竞争力。


软件开发人员


随着开发模式的演变,基于容器的微服务架构已经成为了开发应用首选的架构,而K8S是运行微服务应用的理想平台,市场会需要一批掌握K8S的软件开发人员。


GO开发人员


GO高级开发基本只有两个方向:高级服务端开发工程师和云原生工程师,其中云原生岗位会比高级服务端开发工程师更多。


这里的云原生主要是做DockerPrometheusKubernetes等云原生工具方向等等开发,这也是因为CNCF基金会的一系列产品基本都是使用Go语言写的,Go开发工程师相比于其他人员拥有天然优势。


总结


到这里,每天十分钟轻松入门K8S的01篇: 《你为什么需要学习K8S就结束了》 ,后续会持续更新相关文章,带大家了解K8S架构、K8S组件、如何搭建K8S集群、各种K8S对象、K8S高级特性、K8S-API等等内容。


欢迎大家点赞、收藏、催更~


作者:安妮的心动录
来源:juejin.cn/post/7291513540025434169
收起阅读 »

做个清醒的程序员之要不要做程序员

阅读时长约9.6分钟;共计2411个字 作为这个系列的正篇开端,我们聊一个很应景的话题——要不要做程序员。 说到这个话题,就要把时间往前推到高考后填报志愿的时刻,那个时刻可以说是大部分人首次面对职业规划问题。 在很久之前,我写过一篇关于填报志愿的文章,不妨先来...
继续阅读 »

阅读时长约9.6分钟;共计2411个字


作为这个系列的正篇开端,我们聊一个很应景的话题——要不要做程序员。


说到这个话题,就要把时间往前推到高考后填报志愿的时刻,那个时刻可以说是大部分人首次面对职业规划问题。


在很久之前,我写过一篇关于填报志愿的文章,不妨先来回顾一下。


在那篇文章中,我指出填报志愿的优先原则是城市首选,院校次之,专业末位。


如此选择是有理由的,城市意味着圈子,意味着机会,意味着眼界。院校意味着平均水准,意味着知名度,意味着基本盘。专业意味着兴趣,意味着专长,意味着就业。


虽然我把专业放在末尾,但这并不意味着个人的兴趣不重要。而是当我们有了圈子,有了机会,有了平均水准之后,可以通过辅修或者转专业来进修自己喜欢的知识。


举个例子,比如一个人的高考成绩可以选择一所211或985院校,但专业只能服从调剂。也可以选一所普通院校,专业随便选。这二者之间,其实我更推荐前者。通过转专业或者辅修,最终可以收获知名院校的自己喜欢的专业的毕-业-证书。而后者最终只能收获普通院校的,自己喜欢的专业的毕-业-证书。如果有这样两份不同的学历放在HR面前,如果你是HR,你也会更倾向于选择前者吧?


这就是为啥选专业要放在选学校后面的原因。


再说城市与学校,这个就很好理解了。毕业后,大部分人都会选择参加工作。如果去的城市就业环境不好,机会少,行业内的大佬也不在此地聚集。就算个人再努力,和那些经常和行业大牛接触的人相比,日子久了,差距就会逐渐拉大。这是眼界、格局的差异,不是单纯的能力就可以弥补的。


简短地回顾完旧文章,我们把话题拉回来。就是要不要走软件开发这条路呢?


诚然,在这个问题上,我曾经没有丝毫犹豫,因为我根本就没想过甚至还有点抵触走这条路。


我最初的想法其实是做设计,图形图像方面,或是做视频剪辑。后来又想着做网络工程师,自学了一段时间的思科认证。直到后来快毕业的时候,有培训机构的讲师来做宣讲会,我稀里糊涂地就上了Android App开发这条船。也许是运气爆棚,我还真的挺适合走这条路。


但话说回来,为什么我会在大学期间对自己的未来有那么多的不确定呢?为什么不能坚定地走一条路呢?因为这个专业就不适合我,从一开始报得就有问题。


我填报志愿的时候是师范大学的信息工程,我依稀记得自己就是冲着这个名字选的,没怎么看都有哪些学科。根本就没料到会学什么单片机、电路原理、汇编语言之类的。这些我完全不感兴趣,学习成绩自然也很一般。虽然狗屎运一般地还拿了一次奖学金,但要说心里话,那就是:“这TM学的都是什么玩意”。我这个专业就一个班,而且是全学校唯一一个工科。我有时候就在想,我为啥报了这个专业,以至于荒废了四年大部分的时间。


而且我总共没怎么挂科,然而C语言挂了。所以当时的我怎么也想不到自己会做软件开发,其实这也注定了我也许做不到金字塔顶的那一小撮人。


所以,我特别希望今年的考生,特别是看到我这篇文章的考生,报志愿的时候一定要清醒一些,别像我似的。没有目标感的日子,真的不好过。


另一方面,也是我想表达的重点,就是如果我喜欢做的事情,不挣钱,或是就业前景非常不好,怎么办?


诚然,我当时报这个专业,或多或少是因为这个世界未来的时代将会是信息时代,这一点是毋庸置疑的。没错,选学校、选专业时,考虑的一个因素就是就业。但我认为,就业确实该考虑,但完全不用以它为导向。更多的,还是看个人的擅长领域,只要不是特别离谱就行。


举个例子,小X不喜欢编程,但迫于就业,想多挣些钱,走了编程这条路。刚开始的时候还不错,薪水在同学圈里不算低,自己也因为实现了多挣些钱的目的而开心。但随着时间的推移,他发现薪水的涨幅变慢了。更要命的是,由于自己根本就不喜欢编程,甚至会抵触工作。总是想:“要不是为了钱,老子早就辞职了”。终于有一天,遇到裁员潮。等到他在出去求职的时候,发现同龄人比自己强好多,自己在职场上几乎没什么竞争力。再加上年龄增大,薪酬高的能力够不上,薪酬低的不想去。陷入非常尴尬的境地。


另一个人,小Y,特别喜欢编程。第一份工作的工资或许没有小X高,但他干得很开心,因为他喜欢他的工作内容。而人一旦从事自己真心喜欢的事,就会变得非常积极主动。所以薪资很快就涨到了和小X差不多的水平,但他依然还是很积极地工作。随着他的薪水不断增多,生活水平慢慢地越来越好,他能在更舒适的环境中工作和学习。后来他发现,金钱对他来说不是第一要务,实现人生理想才是。于是他更加积极,甚至把自己所学分享给他人,决定做个对社会有贡献的人。


你看,这就是喜欢和不喜欢的区别,这里面的小Y其实就是我。


发自内心的喜欢,是工作积极主动的重要条件之一。对于真正喜欢的事业,做起来是会非常开心,非常投入,甚至还不觉得累,甚至还是不计回报的。在这种情形下,没有理由做不好。既然能做好,必然就会受到公司的青睐,不用为找不到工作发愁,从而让赚钱成为顺便的事儿。


所以,我的观点,在决定要不要走软件开发者这条路之前,不妨问问自己的内心:我真的喜欢这个行业吗?我真的具备这个行业从业者应有的素养:强大的自学能力、工作中的自律、缜密的逻辑等等吗?我愿意为了可能的加班,牺牲休闲时光吗?我愿意熬夜发版,牺牲睡眠吗?我愿意承担有可能秃如其来的迷人发型吗?……


但是,如果你和这个行业优秀的前辈们那样,希望用键盘,生产那些改善人们生活乃至改变世界的产品;是终身学习者,对新技术、新领域保持好奇;务实,不相信道听途说,善于用实践来检验真理;能和难题死磕到底……那么,非常欢迎你,成为我们的同行。


当然,一旦做了选择,那就没什么可说的。不再犹豫,风雨兼程。十余年的工作经验教会我一个朴实、简单却有奇效的道理——坚持。在坚持面前,一切困难都将不再可怕。而能坚持的人,便是手持利刃的勇士,必能披荆斩棘。


好了,说到这,就有点鸡汤的意味了,我就不给大家打鸡血了。


重复一下重点:走,抑或是不走软件开发这条路。要充分考虑自己的兴趣、擅长以及个性,切勿只考虑就业。况且,四年后的事,谁都说不准。


作者:萧文翰
来源:juejin.cn/post/7217621436512469052
收起阅读 »

很多人找不到人生的意义,但这不妨碍他们快乐的度过一生

”愿世间没有肿瘤,没有疾病,没有痛苦“ 朋友圈里前同事的一条消息惊醒了我,震惊的情绪驱散……了睡前的疲惫,上一次有这种情绪是听到同学的噩耗~ 。 每当悲伤的情绪涌上心头,生死之间的恐惧与震惊会让我禁不住的思考我现在的人生是不是有意义的人生。我会想人生的意义在...
继续阅读 »

”愿世间没有肿瘤,没有疾病,没有痛苦“



朋友圈里前同事的一条消息惊醒了我,震惊的情绪驱散……了睡前的疲惫,上一次有这种情绪是听到同学的噩耗~ 。


每当悲伤的情绪涌上心头,生死之间的恐惧与震惊会让我禁不住的思考我现在的人生是不是有意义的人生。我会想人生的意义在哪里?努力学习,努力拼搏的意义在哪里?仿佛一切努力都不值得~ 在死亡面前一切努力都不值得,反而是笑话。


如果,我是说如果,知道自己死亡的那一天是哪一天,我相信很多人会有另一种不同的活法。


每个人的命运掌握在不同的人手里,有的人掌握在父母手里,有的人掌握在孩子手里,有的人掌握在死神手里,有的人掌握在病魔手里,甚至有的人的命运掌握在酒后驾车司机的手里。


五年前的冬天,我刚毕业一年多,在公司里如鱼得水,每天的状态元气满满,业余时间还会搞开源项目。那个时候还没有体会到北漂的孤独,更几年后,看不到未来的时候对未来的绝望。


给我留下深刻印象的是,那年冬天回到老家,让我第一次感受到,好好活着是一件很难的事。


“林涛, 死了”。四个字就好像晴天霹雳一样在脑袋上炸开。我不记得我爸说话时的语气和表情,我只记得震惊和恐惧的情绪在我脑子里爆炸,好像有人用手掐着我的脖子,我呆住了。


林涛(化名)


愣了一会,才听见我爸说,“林涛出车祸了,和朋友去喝酒,喝完酒一起开车去KTV,路上出事儿的,他坐副驾驶,没系安全带,被甩出去了。”


林涛是我十几年前的发小,那时候天天腻在一起玩,后来渐渐不联系了。偶尔在老家见到,也只是打个招呼……


以后的几年里,时不时的会想起这件事。我第一次意思到,在死神眼里,人人平等,包括90后。


我在想,死亡是我们这个年纪该承受的吗?


如今朋友圈里的消息又让我想到这一切,我又开始思考,人生的意义在哪里?史铁生说,死是一件不必急于求成的事。是啊,任何一个健康的人都不会急于求成这件事。时不时出现的死神仿佛在向我们说:你命由天不由你。


这种情绪时不时的困扰我,每次都会有几天,过几天我又会有 “我命由我不由天”的感觉。没错,过几天就会支棱起来。


我也在想,为什么能支棱起来?主要是命运之神没有狠狠地捶打我。除此之外,我想应该有对美好未来的期待。


当我问老婆,你人生的意义是什么?


“为什么要问人生的意义?” 我老婆反问我。“问这个问题有什么意义,我只想和你快快乐乐的过一辈子。”


是我又矫情了……


我们不再是为赋新词强说愁的少年,我们已经长大,生活工作中有太多的酸甜苦辣,与其再去寻找人生的意义,不如静下心来,想想自己还有哪些人生的愿望,人生的牵挂。


人生苦短,及时行乐。


与其追寻意义,不如快快乐乐的过好每一天。


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

和一个做直播的朋友聊了聊

昨天,昨天和滨江的一个朋友聊了聊,他是那边的一个公司产品负责人,也算是核心合伙人的角色之一,他们的公司是做直播业务的,大概有七八十人的团队,开发人员大概是30人左右,占比35%左右,其中里面还有一个CTO角色,或者说技术总监的角色,其他的全部都是干活的小兵和小...
继续阅读 »

昨天,昨天和滨江的一个朋友聊了聊,他是那边的一个公司产品负责人,也算是核心合伙人的角色之一,他们的公司是做直播业务的,大概有七八十人的团队,开发人员大概是30人左右,占比35%左右,其中里面还有一个CTO角色,或者说技术总监的角色,其他的全部都是干活的小兵和小组长之类的。


我们主要聊到了两个不同规模的公司的工作模式的问题,因为我所在的是阿里巴巴应该是非常典型的超大型互联网公司,而他们公司这个人数刚好是属于小型的互联网公司。


他的公司主要是做直播业务的,大家都很熟悉诸如抖音快手这样的直播平台,这么小的公司怎么能做好一个直播平台呢?那他们的业务模式也非常的经典,那就是做一些非常小众的网红和用户产品。



一、直播市场的长尾用户


他描述了一下他自己的一些对于直播和用户的一些观点和理解,比如说现在众所周知的类似于抖音这样极大的平台,有超级大的网红IP,也有无数的粉丝。但是国内互联网用户基数非常之大,存在非常多的长尾用户,比如一些粉丝想在平台上获得一些娱乐感和交互感,这个是抖音这种大平台所满足不了的。另外一方面有大量的尾部网红在抖音这种大平台上面往往也拿不到任何的流量,所以他们也需要一种更小的平台,有充足的流量扶持。


在这个背景下就有了针对这些长尾用户的一些小的直播平台,那在小的直播平台上,哪怕你再小的网红,你都会有一些流量上面的倾斜,对于用户来说,在抖音上给大V打赏几万可能主播都不会理你,但是你在小平台上直接给主播进行打赏交互,就会变得更加的简单和高效。毕竟我们可以想象一下,很多花不起大价钱的“屌丝”用户,可能在这种小平台上面砸个几百几千,可能就能够约网红出来吃个饭,聊个天什么的。一些尾部网红也是一样,长期在抖音中大平台上面基本上没有流量,也没人关注和在意,但是到小平台上面可能就有比较多的几十个,甚至几百个粉丝过来和你交互和聊天打赏,很容易形成一个正反馈。


所以对于刚刚起步的网红来说,在这种小平台上面去发展,获得自己的正反馈和积累初步的影响力是非常的必要的。那对于一些没有太多钱、时间又空闲的粉丝们来说,对于小平台上面也能够有一个快速的通道去接触到这些主播或者兴趣相同的朋友。


于此同时,各行各业,蚊子肉都是大平台吃不到也不想吃的,这类长尾用户是大的平台是往往无法覆盖的,也是看不上的,所以给了这些小型的平台很多的发展空间,这个就是非常典型的一种长尾生态形式。也非常符合之前我所推荐的那本书叫做《长尾理论》,这种小平台因为它的边际成本是非常的低的,所以它可以在各个地方花钱去投放广告,吸引长尾客流,主打各种形式的娱乐化的直播并从中抽佣。


我们也可以看到这种平台本身也不大可能做的非常大,一方面它可能在形式和内容上面都可能走一些擦边或者灰色的方式,另外一方面对他们自己来说,他们也不想做的做大,做大以后以后就会面临着更加复杂的问题,比如监管问题。所以很多这种小型的平台活的非常的滋润,从来没想着做大做强,而是在自己的一亩三分细分领域里深耕,现金流反而还比大平台的还更加的充足。


他们公司在前两年就寻求上市,因为经济的原因中止,但这也就说明他这种模式实际上非常赚钱,现金流是非常的稳定的。


二、快进快出的用人理念


除了这种非常好的商业模式之外,另外一个讨论点就是我们工作模式上面的最大的区别。他提到了他们公司的员工的离职率是非常高的,基本上几个月、半年可能就大量的技术人员离职汰换。这个也很简单,她说对于新招聘的员工来说,如果半个月上不了手的情况下的话,就会在试用期里面就会解聘掉,主打就是追求实用主义,员工拿来即用没有培养一说。对于一个小的技术公司来说,它的成本控制的非常的严格,如果员工短时间内不能上手的情况下的话,对他们来说是没有任何价值的,所以对于员工都采用快进快出这样的方式,完全不像我们大平台大企业,可能给到一个员工的成长时间,短则三个月,大长则半年,一年。而小公司完全吃不消这种巨大的人力培养成本。


另外就是对于他们一些比较资深的工程师来说,工龄时间也不会太长,因为他们给不了员工的一个向上的晋升通道。当个员工工作了两年到三年,技术能力各方面能力都提高了,以后也没办法往上升或者持续加薪,因为毕竟上面只有一个技术合伙人,总不能把这个技术合伙人给顶下去吧,所以他们大部分的员工工作了两年到三年之后,技术能力上面都有非常大的成长之后,往往就会跳出这个小厂去寻求其他的大厂机会。


然后他们公司本身对于技术的追求也不深,大部分完全采用的是“拿来即用”的原则,他说在早期的时候做平台还会去找一些开源源码自己来部署,到了现在大部分能力都有非常成熟的第三方厂家来支持,他们公司技术人员只要做集成和包装就可以了。现在据我所知,类似于阿里云这样的云平台,已经把整个云计算API、网络直播的API,甚至很多底层技术全部做的非常好,都打包成SDK或者封装成API,所以上层业务方只要购买服务后把API包装一下,封装就可以直接使用了,五分钟生成一个直播平台APP已经没有任何问题了。



以我的理解,一个正常的工作了半年到一年的同学,我觉得在这种SDK或者API的加成下,就应该在一个星期内能创建出来一个直播平台APP了。所以很明显在这种基础能力非常强大的情况下,他们公司就会可以把成本压的更小,他们可以随时的去调整自己的业务方向和迭代,基本上几周就会有一个小版本迭代或者出全新的APP。


我问了一下,他们有没有一个知名的应用市场APP,给我的答案是他们开发成了很多非常小的一些APP,然后在应用市场上面去打广告引流,用户量和粉丝量都不算大,明显就能看到这种模式主打一个灵活、主打分布式。


三、反脆弱的商业形式


所以相对于小厂和中厂来说,不管从业务模式上还是从技术架构上,还是从经营理念上完全不可同日而语。但不得不说,我觉得正如我们的自然界生态系统一样,有些时候很微小的生物往往能够在漫长的生态环境中存活下来,比如蟑螂老鼠,而有一些庞然大物,诸如恐龙猛犸象这样的大体积的生物,反而还容忍不了生态气候的变化而灭绝。


而对于他这样小的一些经济体,几十个人,有自己的一些核心的产品模式,并且能够快速的迭代,对成本控制严格,对经济变化敏感,反而还能够存活到各个不同的周期里面,所以这我觉得也是一种值得我们羡慕的地方。这也是知名作家塔勒布在他的《反脆弱》一书里提到的一种形式,这种公司反而具备更强的反脆弱性,当经济越差,他们不仅不受影响,反而反弹变得更强壮、盈利性更强。


最后一步来说,对于程序员来说,根据自己的兴趣、爱好、能力水平,在当前的经济周期找到一个比较合适自己的平台,能够锻炼到自己的能力,不管是从技术还是从业务经营,产品各个方面都有所成长,那对自己来说就是好事。对于创业者来说也未必要盯着非常大的市场,动不动就来个规模效应,有时候去做这种非常小微公司和长尾市场,往往活得会更加的滋润和惬意。


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

RTX 4090也被禁售了?

游戏也不能玩了? 谁也没有想到,美国针对 AI 大模型技术卡脖子的争端,竟然砍到了玩家头上。 本周二,美国商务部放出最严对华出口管制规定,H800 等关键 AI 加速器成为制裁的焦点,然而消息曝出还没几个小时,人们发现各大电商平台上的高端消费级显卡 GeFo...
继续阅读 »

游戏也不能玩了?



谁也没有想到,美国针对 AI 大模型技术卡脖子的争端,竟然砍到了玩家头上。


本周二,美国商务部放出最严对华出口管制规定,H800 等关键 AI 加速器成为制裁的焦点,然而消息曝出还没几个小时,人们发现各大电商平台上的高端消费级显卡 GeForce RTX 4090 也下架了。


此前一系列对于 AI 芯片的限制已经让人愤怒,这次美国对消费级产品的打击直接影响到了更多人,事件瞬间登上了热搜。


很多人一觉醒来就在问:发生什么事了?



突如其来的消息引发了人们的讨论。游戏圈的人纷纷表示,没想到玩个游戏也能被美国制裁。也有玩生成式 AI 的人说,本来还想攒钱买块显卡跑 AI 画图,现在也不用等了。


但一纸蛮横无理的「禁令」并不能让生活停止,冷静下来之后,很多人进行了分析。首先,更严格的限制会让国内火热的生成式 AI 领域受到影响。


有评论解读认为,按照美国商务部的新规来看,以后出口芯片都要按计算器算一下,芯片的总体性能和性能密度是限制点,因此 RTX 4080 和 AMD 的 7900 XTX 也是有被禁风险的。 



也有人表示,不赚钱让出市场是不可能的,看上次 H800 的做法,英伟达马上就要出 4080Ti 了。



就在我们在计算什么游戏必须要用 4090 才能跑得顺畅的时候,这场闹剧很快又发展到了新的阶段。


禁了,但没完全禁:美商务部澄清


昨天,美国商务部工业和安全局(BIS)更新对华出口管制规定,进一步收紧 AI 芯片等领域的限制。新规显示,此次限制的核心对象是先进计算半导体、半导体制造设备和超级计算机项目,更加严格地限制了中国购买重要的高端芯片。


仔细解读这份新文件可以发现,新规则修改了需要受限的高级芯片的认定规则(参数)。比如删除了「互连带宽」作为识别受限芯片的参数,还设置了一个新的「性能密度阈值」(这个标准更为变通和灵活)来作为参数。



美国政府高级官员说,如果数据中心芯片超过了去年 10 月设定的「性能门槛」,或者超过了以每平方毫米计算的新的「性能密度门槛」基准,美国将限制数据中心芯片的出口。想要向中国或其他禁运地区出口芯片的公司必须通知美国政府。


更新后的禁令还将更多公司列入到「实体清单」,包括两大国产 GPU 显卡厂商摩尔线程、壁仞。


另外在新规定中,香港和澳门地区也在管制区域的范围内。


英伟达在 2022 年 9 月已被禁止向中国出售 A100、H100 等高端 AI 加速器。在当时,英伟达立即设计出了效能、互联性能稍差,但相比前一代提升明显的 A800、H800,作为「特供」中国的替代品。然而在更严格的限制之下,这些产品也被列为禁止出口。


这种管制对于国内科技公司和大模型前沿技术发展的影响,目前还难以分析,但事件的另一个利益相关方英伟达不可避免地再次陷入了被动。


作为上市公司,英伟达立刻提交给美国证监会(SEC)一份报告,主要声明了以下几点:这些限制适用该公司的 A100、A800、H100、H800、L40、L40S 和 RTX 4090 芯片,同时也影响了与这些芯片一起销售的整个系统,包括 DGX 和 HGX 系统。


另外,这些限制可能会损害英伟达按时完成新产品开发的能力。



公告地址:http://www.sec.gov/Archives/ed…


英伟达自己说了,这些你叫得上名的芯片都会受到影响,未来的计划也被打乱了。消息一出,英伟达股价遭遇了大幅缩水,AMD、英特尔也随之下跌,券商也纷纷调低了对于此类公司股价的未来预期。


与此同时,我们看到了在国内的电商平台,RTX 4090 纷纷下架的情景。


消费级显卡被列在了先进计算项目的管制名单里,这并不像是一件正常的事,美国商务部很快对 ECCN 3A090 的高科技出口管制政策做出了澄清。


关于该政策中对 NVIDIA GeForce RTX 4090 显卡的禁令,美国商务部允许对消费性应用的芯片进行出口豁免。不过这里也有争议,并没有明确指出是否包括 RTX 4090。



文件来源:http://www.bis.doc.gov/index.php/d…


这意味着 GeForce RTX 4090 显卡可以在中国(包括香港和澳门)的消费市场进行零售,但不允许将 RTX 4090 芯片用于商业和生产用途。


考虑到目前主流厂商的显卡成品均不是在大陆组装的,新限制对于消费级显卡的影响似乎可以认为是几乎没有。所以此处把问题归于黄牛囤货,是不无道理的。


RTX 4090「算力过高」,5090 怎么办?


所以作为一个玩家,我们就不用担心显卡被禁了,但以后怎么办?


自去年 9 月黄仁勋在 GTC 大会上推出 RTX40 系显卡,已经过去了一年多时间。RTX 4090 相较于 3090Ti 性能翻了一倍,光线追踪性能则提升了 4 倍。



不过,12999 元的起售价似乎让 RTX 4090 在市场上有点卖不动,所以有人调侃称此次 RTX 4090 可能被禁售的传闻是为了用涨价折抵销量的伎俩。


暂且不说被澄清可以消费使用的 RTX 4090,很多人更担忧的是,近来一直爆料不断的下一代旗舰机显卡 RTX 5090 未来会不会步其「前辈」的后尘呢?


据此前消息,RTX 5090 的整体性能会比 4090 提升 70%,其中:




  • CUDA 内核数增加 50%,达到 24576 个




  • 内存带宽增加 52%




  • 缓存增加 78%




  • 频率提高 15%




显然,RTX 5090 的算力很强,强到让人担心次一级的 5080 是不是也在美国商务部那个「算力密度」的范围内。毕竟黄仁勋可是说出过自家芯片每代性能都翻倍这种话的。



图源:videocardz.com/newz/nvidia…


目前看来对于出口的限制仅限数据中心使用的产品,纯粹的民用消费级产品则不受影响。


但不到一年的时间就已经两次修改,以后不好说 —— 不过禁售大众商品完全就是自损一千给助攻的行为,拱手让出消费级市场,是英伟达、AMD 等芯片公司不想看到的事。


尤其对于英伟达这个在中国拥有巨量需求的 GPU 厂商,他肯定不希望因为可能的禁售失掉占其全球营收近一半的中国市场(仅就 2023 财年来说)。未来对于国际芯片厂商和国内科技公司来说,日子可能会更不好过。


从另一个角度看,美国对于高端 AI 加速器的限制,让我们对于发展自身硬件技术有了更强的紧迫感。


或许在限制不断加码的背景之下,会有国内的芯片厂商把握住机遇。


参考内容:


http://www.sec.gov/ix?doc=/Arc…


http://www.zhihu.com/question/62…


作者:机器之心
来源:juejin.cn/post/7291672829135765513
收起阅读 »

因为月薪过高,我的工资发放失败了。。。

0x0. 剧情概要 码农卫师傅,在忙碌地写了一个月 bug 后,迟迟未能收到工资,紧接着又经历了20元巨款的不翼而飞。这究竟是道德的沦丧,还是人性的扭曲?欢迎收看第996期《走近科学之消失的血汗钱》。 对打工人来说,最重要的东西是工资,如果没工资,谁愿意打工?...
继续阅读 »

0x0. 剧情概要


码农卫师傅,在忙碌地写了一个月 bug 后,迟迟未能收到工资,紧接着又经历了20元巨款的不翼而飞。这究竟是道德的沦丧,还是人性的扭曲?欢迎收看第996期《走近科学之消失的血汗钱》。


对打工人来说,最重要的东西是工资,如果没工资,谁愿意打工?

至于你干不干,我反正是不干,我只是一个脱离了高级趣味的打工仔,眼里只有低俗的钱。上班不图钱,图啥?图老板画的饼来填饱肚子吗?亦或是图理想和未来?我特么还图小鹏特斯拉呢。

每月8号,是我司的发薪日,这次,我能如期等来工资,只等来了财务的通知,告诉我工资发放失败了。真是活久见,小刀揦屁股,开了眼了。

就这样,工作11余年,我第一次走上了「讨薪」之路。在经历了一波三折后,终于在3天后拿到工钱了。那么,为啥工资会发放失败呢?背后的原因令人暖心:

因为我工资太高了,被银行拦截了

是有点搞笑,你可能不信,但你还是要相信。反正事情就是这么个事情,情况就是这么个情况。

这篇文章主要记录「讨薪」的过程,以及一些碎碎念。对废话没兴趣的老哥,可以先行离开了。

0x1. 案发经过

过去10年,我的工资卡一直都是招行的,入账有短信提醒,因此,我从未主动查过工资是否到账。现在的公司用中国银行,短信提醒居然要收费,没钱,不开。

我的房贷还款日也是8号,我都是等工资到账后,再转账到房贷卡。但是,工资到账时间不固定,我又懒得去查询,为了避免错过还房贷,就设置了个每月8号重复的日历事件,17点提醒我查工资并转账。

时间来到9月8日,这天是个星期五,17点,手机准时响起,打开中行 APP,余额为0。虽然有点奇怪,但也没放在心上,毕竟腾讯也有过晚上10点多才发工资的情况。

半个多小时后,收到财务的消息,告诉我因为工资卡上没钱,导致工资发放失败了,需要转点钱过去,等银行处理后重新发送工资。不理解,余额为0跟收工资有啥关系?财务表示这是银行的口径,可能是卡被冻结了,以后都需要保持卡上有钱才能发工资。

确实,我的工资卡余额常年为0,个人习惯的原因,每次工资到账后,我都全额转到招行卡里消费用。

现在,死循环了,因为没收到工资,所以余额为0;又因为余额为0,导致收不到工资。难怪我同事说:

月光族,连工资都不配收到了

虽然觉得扯淡,我还是从招行转了20元到中行。很快啊,招行提示转账成功,也扣款了。此时,更离奇的事情发生了,中行 APP 依然显示余额为0,也没有任何异常提示,多次刷新后,涛声依旧。

这就超出我的认知了,人不能赚到自己认知以外的钱,这没毛病,但是,你也不能把我认知以内的钱卷走啊。

我得反思下了,为什么别人都能收到工资,就我收不到?先找找自己的原因,去年从帝都到霸都,从大厂到小厂,从组长到小兵,工资断崖式下跌,年终奖比 HR 的承诺少了一半。现在,一年多过去了,工资一毛没涨,公司股价跌了33%,我到底有没有认真工作?


WTF! 现在是属于我的工资和转账没收到,我特么反思个鸡毛,赶紧去找银行要钱。

0x2. 出师未捷

致电中行客服,在听了一堆电脑播放的废话后,终于接通人工客服,告诉我需要设置电话银行密码才能继续。设置完成后,回不到人工客服了。。。

重新拨打,又听了一遍废话,跟客服重述了一遍问题。他帮我查了一下,耗时仅2分钟,告诉我卡片一切正常,也没有查到那20元的转账记录。如果想继续追究的话,带上身-份-证,人肉前往柜台办理即可。

Good idea,我咋就没想到呢?我想他可能是忘了,此时是18:15,银行已经关门1个多小时了。。。

周六,银行错峰休息,开户行不上班。周日早上,前往银行,取号,7个人排队,2个窗口。前面的大爷大妈基本都是存钱取钱,挺慢的,等了近半个小时。

窗口1,跟工作人员描述了一遍我的问题,只见他一顿操作猛如虎,然后轻声细语(此处为贬义词,对方声音小到几乎听不见)地告诉我,银行卡没问题,一切正常。而且,近期没有交易记录,最近的一笔还是半个多月前的。让我再转账1元试试,从之,从招行转了1元。

我艹屮艸芔茻,闹鬼了,这次成功收到了。中行 APP 上赫然显示着余额1元,打脸了,尼玛,为什么?!


我问他为啥工资和周五转的20元都没能收到?依然答复说卡是正常的,顺便给我抛出了两个问题:

  1. 你们公司是不是没发工资?
  2. 你是不是银行卡号输错了?

大哥,你这是在侮辱我的智商啊,把我给逗笑了。首先,我司虽然不如贵行财大气粗,但按时发工资还是能保证的。其次,卡号都保存在招行的通讯录,根本不需要输入,而且卡号错误,根本就转不过去啊。

我憋着一肚子气,告诉他,都不可能。所以,我这卡到底是咋了?我要做什么才能确保工资顺利入账?答复曰,那就不知道了,而且你刚才转账也成功了,要不你再问问招行?

行吧,居然连柜台都解决不了,也明显感觉他的敷衍。临走前,我向他表达了我的不爽:

你们中行真的是辣鸡

当然,这跟他无关,他跟我一样,只是个打工仔而已,没必要跟我掰扯,就当我在放屁了,头都没抬一下。

0x3. 卷土重来

问题没有解决,我只能骂骂咧咧地走出银行,坐上心爱的小摩托,打算用心中的怒火来发动它。

虽然我知道问题肯定在中行,还是抱着死马当活马医的想法,打开招行 APP,看看能否发现什么蛛丝马迹。又转了1元过去,又成功了,但20元依然没有退回。

四处点了点,在转账的进度查询里,发现了这么一段:


有了这个报文 ID,中行不能再甩锅给招行了吧?山穷水尽疑无路,柳暗花明又一村,古人诚不我欺,遂折返银行。

再次取号,这次变成12个人等待了。为了减少等待时间,找到大堂经理,告知来意,出示报文号,问能否帮忙查下。答曰查不了,需要到窗口,在报文系统中查询。

只能老实回去排队,百无聊赖地玩着手机,仿佛多次听到「工资」一词,便抬头四处看看。

窗口2,正在办业务是个小伙,卡一直正常使用,突然就收不到转账了。我赶紧竖起耳朵,仔细「偷听」了起来,无巧不成书,他碰到了跟我一样的问题。为他办业务的是位小姑娘,明显感觉比窗口1的大哥有耐心,至少花的时间比我长。

希望再次冉起,我这急性子,没等小伙的业务办完,立刻跑到窗口2,一顿狂吠:

我也是工资没收到,但是旁边的窗口告诉我说查不到原因,肯定是你们银行对我的卡做了什么,bala,bala...

喷完后,感觉自己有点失态了,身边多了一圈不明真相的吃瓜群众。冷静了下,我说我有招行的报文 ID,能否定位问题?

此时,从窗口3探出了一个头,说查不了,可以等小伙的业务办完后,让窗口2的姑娘帮忙看下我的卡的故障。此时,我才发现窗口2的姑娘是位实习生,窗口3可能是实习生的上级或者导师,她们貌似在结对办公。

又过了5分钟,小伙完事了。我赶紧上前,先解释插队的原因,我和刚才的小伙的问题一样,但窗口1的大哥没能解决,能否帮我看看?还没等窗口2的姑娘开口,窗口3直接问我一个「高压线」的问题:

你月薪超过 X 了吗?

点头,又问我是否周五发的工资,再次点头。她立刻说到,那是碰到同样的问题了,现在已经解决了,她们的工作群里也在讨论此事:

周五,银行批量将部分账户的日收款限额调整为 X 元,如果月薪超过 X,会发放失败。收到客户反馈后,已经批量调整为 Y 元了

然后,她让我回去等财务重新发工资即可。呃,Y 也不够啊,给我调整到 Z 吧。

她们略带吃惊地看着我,估计觉得我在吹牛,问我哪个公司的,能有这么高的工资?我说了个名字,窗口3表示没听过,实习生倒是知道,只是不知道合肥有分舵。毕竟高新区的公司,高薪是入驻的必要条件🐶

我收入确实比 Y 高,但离 Z 还差的远,Z 是我在腾讯时的收入。虽然短时间内涨薪是不可能的,但梦想还是要有的,否则如何装逼?

之后,领导授权实习生帮我调整了限额,并说不敢保证银行不会再次批量调整。然后,实习生羡慕地说道:

你们工资好高啊,我就没这个烦恼

啊,装逼装大了,赶紧说没有没有,我是怕年终奖超了,就搞个大点的数字,省的到时又要跑一趟。

5分钟后,调整完毕,这是调整后的回执:


当时没仔细看,事后发现这个回执上只有红框中的3个限额是 Z,但从字面上看,肯定不是入账限额。我猜测这是个隐藏的设置项,不会体现在回执上。


之后,她给我留了个银行的电话,如果以后再碰到这个问题,直接打电话就行,不需要再人肉过来了。


此时,还有个问题:



我的20元巨款去哪儿了?



她回复我说,因为刚刚才调整了限额,调整生效后应该会退回招行,或者重新入账,先回去等两天。如果没动静,再过来看看。


OK,从之。


0x4. 落袋为安


周一上午,我将了解到的 X、Y、Z 信息告诉财务,供他参考,以便其他同事能去银行准确地办理所需的业务。


周一下午,财务问我工资是否到账,他那边显示成功了。打开中行 APP,余额 = 工资 + 22,20元是早上08:45收到的。


完结🍭,撒花🎉,鼓掌👏🏻。等等,还有个大大的疑问:



我司大部分的人工资应该都超过了 X,为什么只有少数几个人受到了影响?



关于这个问题,我也咨询了那位领导,她表示不清楚,只是猜测跟反诈有关。我觉得很可能是真的,因为这张卡平时从来不用,只在发工资当天有进账,又在很短的时间内就全部转出,像极了骗子收到赃款立刻转走的行为。再叠加长期余额为0的因素,估计被风控识别为电信诈骗的账户了。


其实,我在几年前就碰到过类似的事,只是追债太麻烦了,我尝试过两次,放弃了,至今都没把钱要回来。事情是这样的:


我在《如何用1个2手计算器换3台 Mac》系列文章中提到,我靠着 WP8 应用赚了几千美刀。2015年,WP8 嗝屁,开发者账户里还有几百刀,彼时外汇无法入账境内银行卡了,之前是没问题的,就全提现到了 Paypal 了。


国内支持 Paypal 的地方太少,我不会海淘,提现到银行卡的手续费巨贵,就那么放着了。


2019年底,了解到 Paypal 提现到香港卡的手续费很低,只要超过1千港币,就不要手续费了。有这好事,赶紧梭哈,All Out,全部提现。


然后,悲剧了,Paypal 显示提现成功,但银行始终没收到。


先致电银行,对方表示查不到入账记录,需要联系 Paypal。电话 Paypal,对方说被美帝的什么机构拦截了,让我提供一堆英文材料证明这是合法收入。当时工作比较忙,之后没多久,那啥开始了,就没心思管这事了。


去年10月底,腾讯股价跌破200,之前看不上的几百刀,感觉也是一笔不小的数字了。再次致电 Paypal,想看看现在的流程是否简单些,把钱要回来。


因为年代久远,几经周转,PayPal 给我发了站内信,说是可能涉及跨国犯罪的洗钱,被拦截了。具体的条例是31 CFR Part 590 - Transnational Criminal Organizations Sanctions Regulations,需要去 OFAC 申请什么许可证,有问题联系 cncsdoc#paypal.com。


有戏,马上搞起,申请 OFAC 许可证的页面长这样:




好家伙,这么多内容要填,而且多达8步,不知道要怎么填,邮件咨询 cncsdoc 是否有模板参考,杳无音讯。算了,几百刀而已,比起梭哈 All In 在腾讯上的亏损,毛都算不上,责任全在美方。


所以,梭哈并不是好主意,无论是 All In 还是 All Out。做人留一线,日后好相见。


话说回来,比起 Paypal,这次中行的经历虽然也不太爽,但除了浪费时间和口舌,实际上并没有损失什么。


换个角度,我费了老大劲才查到银行卡的异常原因,这其实是一件好事。互联网也类似,为了避免敏感信息泄露,给用户的提示语,有时需要模糊化处理。例如流量因为广告作弊被封禁,触碰到风控系统的哪条红线,肯定不会明确告知的。


中行 APP 和电话客服都没能查到异常,连窗口1的大哥都没能搞定。这也是为了避免真正的犯罪分子轻松获知阈值 X,从而绕过风控。那么,是否会有犯罪分子看了这篇文章,主动去窗口要求提高限额的阈值呢?他敢?正好自投罗网,可狱而不可求啊。


PS.



阈,读音同玉,读作 yù;把阈值写成阀值,以及把阈值读作 fá 值的都是异类



所以,虽然我是此次误杀的受害者之一,也给我带来了诸多不便,还是给中行点个赞。现在电信诈骗太猖獗,宁可错杀,不可漏杀,也没毛病。


0x5. 一些启发


虽然没有实际损失,但是用户体验很不好,从开发角度看,这件事也给我带来了一些启发:



  1. 在定位 bug 原因时,经验固然重要,态度也很重要,即所谓 owner 意识;例如限额的 bug,虽然窗口1的大哥是 owner,但最终是态度端正的实习生和经验丰富的领导帮忙解决的。

  2. 涉及 C/S 交互的代码,Log 中至少要有 ID,可以是哈希或加密的,也许是定位 bug 的救命稻草;例如转账的报文 ID,虽然没能用上,如果数额很大,我猜肯定能查到。

  3. try...catch 时,如果出现 exception,考虑再来一发,尤其是网络相关的,也许会被路由到另一条畅通的路径;例如我被路由到了实习生那,finaly return true。

  4. 如果必须牺牲部分用户,应该利用多种数据源交叉验证,尽可能避免错杀;例如,查询下给我的付款的账号,就知道这是工资,可以放行,除非宗旨是宁可错杀,不可漏杀。


另外,为了避免出事后找不到人,互联网有个不成文的规定:



临近节假日,非必要不上线



我做客户端的都知道,更别说后端了。据我了解,金融系统的后端和数据库更新,多是在大半夜进行。周五,白天,批量设置用户的入账额度,真就视金钱如粪土,骚!




虽然从电信反诈的角度,中行做的没错,也是有必要的。但是,从码农的角度看,还是有值得改进的地方,以减少对普通人的影响。试想,万一被拦截的是救命钱呢?间隔50多个小时才到账,生命等的起吗?医院愿意等吗?点到为止,不展开了。


0x6. 努力与回报


行文至此,本应该结束了,但按照应试作文的惯例,还是得故作深沉的再 BB 几句,以此来升华主题,彰显作者的真知灼见,轻松获得高分。我来个画蛇添足,这件事因工资而起,为了首尾呼应,也以工资结束吧。


说到工资,我在旧文《鹅厂组长,北漂10年,有房有车,做了一个违背祖宗的决定》中提到,去年从北京回到合肥,经历了断崖式的降薪。即便如此,现在的收入在合肥还是算高的。因为当我说 Y 不够时,两位工作人员的表情不像是装的,居然连银行的人都没见过这么多钱🐶。


这不是凡尔赛,更不值得炫耀,有没有一种可能,是合肥的收入太低了,使得平平无奇的工资也能鹤立鸡群了。合肥,作为网红二线城市,号称「最牛风投」城市,也诞生了诸多网红,在提高工资水平一事上,任重而道远。


就个人来说,如何才能获得高薪呢?没有答案,但我知道仅靠努力工作肯定是不行的,相比努力,运气可能更重要。如长者所说:



一个人的命运,当然要靠个人的奋斗,也要考虑历史的进程



条条大道通罗马,有人出生就在罗马,没有绝对的公平。互联网的起薪,可能是绝大多数行业的天花板。


我在之前的旧文中多次提到,一命二运三风水。拿我自己来说,所谓的「高薪」,努力的作用只占30%不到,更多的还是因为身在互联网行业,毕业时又赶上了移动互联网的起飞。所以,虽然我很羡慕因为房子或公司股票增值而暴富的,但也没有觉得他们比我更牛逼,他们只是运气比我好,仅此而已。




人贵在自知之明,不以物喜,不以己悲。不能因为自己赶上风口先富了起来,就讥讽别人没有努力工作。更不必因为收入不高而妄自菲薄,他们可能只是运气好而已。在此,引用下罗翔老师的话:



人不应该相信天道酬勤,因为如果你相信天道酬勤,会很容易走向骄傲或者虚无。


当你成功的时候,你觉得这一切都是靠你努力拼搏得来的,你就配拥有这一切,所以你就瞧不起那些失败的人。但是当你努力最后依然失败,依然是一事无成,你又会陷入一种极大的抱怨,你会觉得天道不公。



当然,罗老师不是鼓励躺平,他还说了:



人生中95%的事情可能是我们自己决定不了的,但是我们依然要用5%的努力去撬动那95%你无法决定的事情



虽然我的关注者不过200来人,我决定不了谁会打开此文,更决定不了有多少人会读完。但我还是花了近两周的时间,努力修改措辞,力求使得文章更通顺,同时尽量有趣点。这既是对读者的尊重,也是对自己时间的尊重。至于读者是否买账,那就不是我能决定的了,正所谓:



岂能尽如人意,但求问心无愧



以上,是我自己的一些想法,如有异议,欢迎留言讨论。


作者:野生的码农
链接:https://juejin.cn/post/7282666872217157643
收起阅读 »

3个bug,让老板亏了北京1套房。。。

0x0. 背景介绍 再过几天就是 1024 程序员节了,提前祝广大程序员工友们节日快乐,少写 bug,早点下班回家,不熬夜,尽量 delay 秃头的上线时间😭 上篇文章《因为月薪过高,我的工资发放失败了。。。》中,我分享了中行的骚操作导致我收不到工资的故事。简...
继续阅读 »

0x0. 背景介绍


再过几天就是 1024 程序员节了,提前祝广大程序员工友们节日快乐,少写 bug,早点下班回家,不熬夜,尽量 delay 秃头的上线时间😭


上篇文章《因为月薪过高,我的工资发放失败了。。。》中,我分享了中行的骚操作导致我收不到工资的故事。简单的说,就是中行的码农老哥上线了一个 bug,误伤了普通用户,将正常的银行卡标记为风险账户导致入账失败。


这个 bug 看似没有带来实际损失,但是浪费了客户、客服、柜台人员的大量时间,这些都是成本。更重要的是,中行损失了潜在的高净值客户,某网友撰文吐槽此事,试图搞个大新闻,居然获得了几万的阅读。万一读者里有未来的首富,发誓不跟中行做生意,中行怎么也得损失几个小目标吧🐶。


作为码农,我们和 bug 的相处时间可能比另一半都多,毕竟咱们就是以写 bug 为生。写代码赚大钱的故事,大家见的多了,尤以逼乎和卖卖为甚。可能是大多数开发离钱太远,亦或是因为家丑不可外扬,网上鲜有人分享因为 bug 亏大钱的事故。


恰好,我做过日入过亿的大项目,脸皮也足够厚,本文分享3个我亲身经历的简单 bugs,简单到只需几秒钟就能修复。但是它们带来了巨额的亏损,足够在北京四环全款买一套100平的房子,甚至更多。


面币思过


对了,上文有老哥留言说我废话太多了,这里稍微解释下,我的个人简介里有写的:



本人主业是讲段子,副业才是写 bug



所以,为了避免文章过于枯燥,本文,我依然会按照自己的风格,用「废话」的方式来回答:



bug 产生的原因是什么?为什么没测试出来?给用户带来了什么影响?如何修复?耗时多久?如何避免?



如前所述,都是非常简单的 bug,并没有什么深度和难度,只想看干货的老哥,恕难满足,超市里应该有:


干货


声明:本文内容,毫无虚构,如有雷同,纯属雷同。


0x1. 挤兑的代价


若干年前,北京,12月的某天,23点,-10℃,骑摩托刚到家不久,正坐在暖气片上加热被冻的冰凉的屁股,接到同事电话:



合作团队 X 部门说我们最近几个月的 CDN 带宽陡增,每个月有近千万的成本



千万每月?我以为我听错了,他又重复了一遍,我蹭地一下站了起来,连呼三声卧槽,差点整个人都凉了。彼时,临近年底,老板正在分配年终奖,如果真要支付这么多成本,还发啥年终奖啊,部门都可以就地解散了。


稍后,同事又补充道,这是折扣前的成本,折扣后应该会少很多,具体需要拉上相关同事详细计算。罢了,事已至此,先睡觉吧。


次日,找到相关同事简单讨论了下,基本确定了原因。我们的产品是 SDK,先说下背景:




  1. 不久之前的某个版本增加了功能 A,功能 A 需要用到一些配置 C

  2. 为了能让用户体验更好,SDK 初始化时会主动从 CDN 下载配置 C



最近,我们完善了功能 A,配置 C 的体积也增大了数倍。同时,为了配合推广功能 A,我们做了一次运营活动,鼓励更多用户升级到最新版本。于是,在用户数量和配置 C 的体积双双陡增的情况下,带来了 CDN 流量的暴涨。


雪上加霜的是,一些宿主 APP 用黑科技对抗 ROM,力求做到「保活」,导致 APP 短时间内多次被系统干掉又自动重启,引发 SDK 初始化并下载配置 C。


另外,CDN 的计费是按照当天的峰值带宽来的,24小时内,哪怕波峰只持续了1秒,当天的成本也是按照最高点的带宽来核算的,如下图就是按照接近80的带宽来计算:


CDN 带宽示意图


再考虑一下我们平时使用手机的习惯,有明显的3个高峰期:




  1. 06:30 ~ 08:30

  2. 11:30 ~ 13:00

  3. 18:00 ~ 21:00



这3个高峰期与我们观察到的 CDN 带宽曲线非常吻合,而且早晚高峰远高于午高峰。虽然配置文件 C 并不大,但是海量的用户一股脑地同时请求 CDN,直接将瞬时带宽推上天了,进而导致核算成本超高。就像今年初的硅谷银行,因为储户的大量挤兑,直接把它给干倒闭了。


原因找到了,解决就简单了,各个击破之:




  1. 找到流量占大头的宿主 APP,与开发者沟通,配置其不请求 CDN,带宽直接降低 90%

  2. 确定根本不需要功能 A 的宿主 APP,配置其不请求 CDN,带宽再次降低 50%

  3. 减小配置 C 的文件体积,精简、移除不必要的内容

  4. 削峰填谷,优化下载策略,平滑 CDN 带宽曲线



前两步在当天就完成了,第3步和第4步是逐步完善的,最终带宽稳定在优化前的5%左右。


我猜,肯定有读者质疑,为何要在 SDK 初始化时就请求 CDN 下载配置?应该先请求某个后台 CGI 接口,由后台决定是否需要下载或更新配置。这就是另一个话题了,历史原因,前后端的合作比较拧巴,许多本该后端完成的工作,下放到客户端了,导致技术方案很山寨。后来通过两次请求 CDN 迂回实现了这个功能:




  1. 先以某个固定 URL 请求 CDN,得到配置文件 C 的 URL,URL 中有 C 的哈希

  2. 如果 URL 中的哈希与本地配置文件的哈希不同,再次请求 CDN,下载配置文件 C



问题虽然解决了,已经产生的带宽费用怎么办?部门间结算是按季度进行的,但是负责基建的 X 部门,未能及时告知我们带宽异常情况,造成了带宽的浪费。彼时,降本增效尚未开始,经过与 X 部门的协商,对方减免了我们近几个月的带宽费用。


这个问题持续了几个月,粗略的估算,即使打折,实际消耗也有数百万了。虽然 X 部门没要钱,看似是我们赚了,但最终肯定是小马哥给报销,亏的他只能坐公交了。


小马哥坐公交


0x2. 最贵的字符


不久后,轰轰烈烈的降本增效运动开始席卷整个公司。如何降本?最简单粗暴的方法就是开猿节流:


开猿节流


幸运的是,我所在的部门一直有盈利,没有采取这种低级的手段。不开猿,就只能节流了。在解决上面的 bug 后,我们就开始尝试使用不同手段来优化各种机器成本,包括 CDN 带宽、磁盘存储、CPU 资源等等。尤其是 CDN 带宽,每天上班都会看一眼,防止又出事了。


几个月后,优化初见成色,着实为部门省下了一大笔钱。距离当初定的优化目标,每天都在更近一步,心中甚是喜悦。然鹅,快乐的日子总是短暂的,在准备将这份喜悦分享给老板的前夕,出岔子了。


某天,我突然被 Y 部门的人拉到一个群,询问其 CDN 上的某个文件 F 是否归属我的部门。在得到我肯定的答复后,他们说其 CDN 上 99% 的流量来自文件 F,让我们赔付近几个月消耗的数百万元,同时不排除追溯历史费用。


我屮艸芔茻!Yesterday Once More?稍作冷静,直觉告诉我不可能有这么多钱,因为 F 的使用方式如下:




  1. 应用中自带一份 F,程序启动时会加载 F

  2. 当且仅当本地的 F 与 CDN 上的不一致时,才会重新下载



我们两到三个月才会更新一次 F,理论上,只有在更新 F 时才会产生 CDN 流量,费用最多只有他说的 1/10。彼时,大家都在「降本」,我不敢懈怠,为了尽快把锅甩出去,赶紧找经验丰富的同学的帮忙排查。


很快啊,锅就回来了,因为某处多了一个字符,导致 CDN 带宽暴涨。先暂停1分钟,能猜到可能的原因吗?


---------- 我是没用的分隔符 ----------


问题出在上面的第 2 步,假设 CDN_FILE_HASH 是 CDN 上的文件 F 的哈希值,由后台的 CGI 接口返回给客户端。整个流程的伪代码如下:


 CDN_FILE_HASH = get_cdn_file_hash_from_cgi();
 if (CDN_FILE_HASH != localFile.hash()){
  downloadFile();
 }

简单的 debug 了下,cgi 返回的 CDN_FILE_HASH 比预期多了个换行符 \n,这就导致了 if 语句始终为真。于是,应用每次启动时,都会重新下载 F。谁说人不能两次踏入同一条河流的?这跟第1个 bug 不是一毛一样吗👀。


我们每次在更新 F 后,会将其哈希写入另一个配置文件 H。在收到客户端的请求时,后台读取 H 的内容,返回给客户端。后台相关代码自上线后就没动过,所以多出来的 \n 只能是来自文件 H。


之前,我们是先人肉更新 CDN 上的 F,再将其哈希写入 H,每次都需要在 Web 上填一堆东西,比较麻烦。为了增效,就写了个脚本,一键更新 F 并将其哈希写入 H,真爽!


不用说,肯定是写文件的地方有问题,伪代码如下。暂停1分钟,看出 bug 了吗?


 def write_F_hash_to_H(F)
  with open('H', 'w') as H:
  print(F.hash(), file=H)

---------- 我是没用的分隔符 ----------


对 python 熟悉的小伙伴应该看出来了,print 会自动追加换行符(默认为\n),而 JAVA 只有 println 才会追加:


print


就这样,价值数百万的换行符诞生了,这是我见过的最贵的 bug 了,这亏钱速度,大 A 看了都要落泪😭。


修复也极其简单,将 print 函数的 end 参数赋值为空字符串即可。当务之急是减小损失,遂立刻人肉删除文件 H 中的换行符,CDN 流量瞬间就跌下来了:


仅亏损95%的股票走势


之后的 CDN 带宽走势与上图箭头右侧非常相似,这是我去年中买的一支股票,买在箭头所示的地方,两个月前割了,仅亏损95%😎。


现在,同样的问题又来了,已产生的几百万的费用咋办?彼时,各部门都在「降本」,我们之前那套说辞不好使了,对方坚称要赔付。经过几轮「友好」的争吵与互相问候,几番讨价还价,赔付自 bug 发生日期之后的费用即可,分期付款。


上面提到,我每天都会看一眼 CDN 带宽,这条大鱼为啥还会漏网呢?这就说来话长了:



很久之前,我们也隶属于 Y 部门,CDN 自然也是同一个。


后来,组织架构调整,我们被“赶出” Y 部门,自立门户了。



因为文件 F 非常重要,为了保证存量客户端版本不受影响,分家的时候,F 没有迁移,仍然保留在原 CDN 上。后续 F 有更新,还是上传到原 CDN。正常情况下,F 的带宽非常小,可能 Y 部门没发现或者懒得计较,放任我们白嫖了。


我没有权限查看 Y 部门的 CDN 的监控面板,亦不了解那段历史,直到因为 bug 暴雷,我才知道是 Y 部门在替我们「负重前行」。


bug 持续了两个多月,粗略估算,我们支付的费用,足够正常情况下使用好几年了。真的是应了那句:



所有命运馈送的礼物,早已在暗中标好了价格



不用说,最终又是老板一个人承担了所有


小马哥承担


0x3. 狸猫换太子


前面的 bug,根本原因都是技术方案不完善和细节考虑不周所致,简单的说,是自己人的锅。但,即使把代码写的完美无瑕,就一定能正确运行吗?


11年前,我是个刚出道不久的小菜鸟,接手了一个偶现 bug,涉案金额可以忽略不计。业务逻辑非常简单,如下:




  1. 客户端 POST 本地数据库中的数据至服务端,服务端返回响应 rsp

  2. 如果上传成功,客户端删除本地已上传数据;否则,再重试一次



零星有几个深圳的用户反馈我们的 APP 消耗了很多流量,最终的排查结果,嘿,您猜怎么着?高情商的说法是「涨见识了」,低情商的说法是「操」!


还有这种操作


为了复现这个问题,我尝试了三个运营商的手机卡,在GPRS,EDGE,CDMA 1x,3G等多种网络条件下测试,流量完全正常。因为缺少必要的 log,只能从代码入手,初步怀疑可能有 bug 的地方:




  1. 上传成功,但本地数据删除失败,导致每次重复上传旧数据

  2. 重试逻辑不严谨,如果上传失败,可能多次重试,浪费流量



喊上导师一起仔细读了几遍代码,确定代码没问题。之后的细节记不清了,最终是在深圳同事的协助下,找到了复现的严苛条件:



深圳,中国电信手机卡,数据流量上网,选择 CTWAP 接入点



复现时,远程 debug 发现,客户端每次都走到了上传失败的分支,但手机的网络是正常的,也能 ping 通我们的域名。


猜测是客户端或服务端收到的数据有问题,亦或二者皆有,用 tcpdump 在客户端和服务端分别抓包,很快确定了:




  1. 服务端收到了正确的数据,返回的 rsp 是 gzip 压缩的 JSON 串

  2. 但是,客户端收到的 rsp 与服务端发出的不一致,有些字节被篡改了



看到这,可能有读者会说,这不就是「HTTP 劫持」吗?某些无良运营商,利用 iFrame 在网页上插个「性感荷官,在线发牌」的广告,祖传手艺了。


根本原因肯定是 HTTP 中间人攻击,篡改了数据,但我觉得更像是运营商 CTWAP 的 bug,因为以下两种修改都可以收到正确的 rsp,说明并非运营商刻意为之:




  1. 手机上将接入点改为 CTNET

  2. rsp 不启用 gzip 压缩



在选择 CTWAP 接入点时,手机的 HTTP 请求都会被转发到电信内网的代理服务器 10.0.0.200:80。怀疑是深圳部署的 proxy 有 bug,识别不了 gzip 格式,或者某些二进制字节被误判为「非法」字符,出于好心,就顺手帮我们改了。大概类似这个15年前的笑话吧,异曲同工之妙,我不知道「绿坝」是啥:



因为「绿坝-花季护航」的屏蔽,华为官网的「24口交换机」已改名为「24嘴交换机」



再说回 bug,现在原因很明显了:



上传成功 -> 服务端返回 rsp -> CTWAP 网关篡改 rsp -> 客户端认为上传失败 -> 重试 -> 再次失败 -> 下次满足条件时继续上传...



修复也很简单,因为 gzip 压缩前的 JSON 也就几十个字节,没必要压缩。正好客户端使用的 HTTPClient 可以自动识别 rsp 是否被 gzip 压缩并正确处理,所以只需要服务端关闭 gzip 压缩即可。


彼时 3G 已经普及了,大多数电信手机的默认接入点都是 CTNET,加上网速也不快,所以实际的影响非常有限。当然,针对受影响的用户,我们如实赔付了其损失的流量费。同时,也将这个问题报给了深圳电信,至于是否修复,就不得而知了。


这大概就是「人在家中坐,锅从天上来」吧,所以,bug 不可怕,可怕的是没 bug,那必定是有 bug🐶


0x4. 总结 & 反思


所谓「常在河边走,哪有不湿鞋」,即使经验丰富的大佬,也能写出匪夷所思的 bug,例如臭名昭著的goto fail漏洞:


CVE-2014-1266


这段代码存在于 iOS 7.0.6 之前,其正式编号为CVE-2014-1266,会导致非法的 SSL 证书也能被接受,有极大的安全隐患,详细分析见dwheeler.com/essays/appl…


所以,也没啥好总结的,就像每次版本发布之后,无论针对 bug 的「批斗大会」开的多么成功,码农的反思多么深刻,下次一定还会有 bug,正如黑格尔所说的:



人类从历史中学到的唯一教训,就是人类无法从历史中学到任何教训



虽然无法完全杜绝 bug,但一向好为人师的我,还是想 BB 三句,仅针对本文分享的几个 bug:



  1. 提高成本意识:客户端开发大多都没有机器成本的概念,包括我自己,我们需要尽可能优化网络请求次数和数据量,这些都是💰啊,除非你是帮老板解决钱花不完的烦恼🐶

  2. 增加白盒测试:程序正确运行,不代表没 bug,本文第2个 bug,如果有白盒测试,上线前一定能发现每次都下载 F 的问题。至于哪里用白盒,没有标准,与💰强相关的地方,优先考虑

  3. 一切皆有可能:当出现 bug 时,如果无论如何也找不到原因,也许真的就不是自己代码的 bug,先拖一拖吧,也许它自己就好了,尤其 Android,太多玄学的事情了。。。


再啰嗦一句与技术无关的,发展才是硬道理,随着发展,把蛋糕做大,很多问题会自动解决或可以忽略。例如第1个 bug,降本增效之前,地主家还有点余粮,直接给我们免了;第2个 bug,虽然之前一直是白嫖,但彼时业务在高速增长期,CDN 那点钱比起赚到的钱来说可以忽略不计,所以 Y 部门一直也没计较。就酱,点到为止。


最后,用专治八阿哥的雍正帝镇楼,保佑码农兄弟姐妹们碰到 bug 时都能迎刃而解:


专治八阿哥的雍正


对了,欢迎大家关注我的同名公众号,所有的文章都是首发在公众号,因为他的审核速度最快。


野生的码农.png


作者:野生的码农
来源:juejin.cn/post/7291550018922577920
收起阅读 »

分享一个Java小项目:Java实现超级马里奥的冒险之旅

引言超级马里奥,这个名字对于游戏迷来说一定不陌生。它是一款经典的游戏系列,以一个勇敢的水管工人——马里奥为主角,讲述了他在蘑菇王国中的冒险故事。在这个充满挑战和刺激的游戏中,玩家需要控制马里奥跳跃、躲避障碍物,并与邪恶的蘑菇和食人花敌人战斗,最终抵达城堡的胜利...
继续阅读 »

引言

超级马里奥,这个名字对于游戏迷来说一定不陌生。它是一款经典的游戏系列,以一个勇敢的水管工人——马里奥为主角,讲述了他在蘑菇王国中的冒险故事。在这个充满挑战和刺激的游戏中,玩家需要控制马里奥跳跃、躲避障碍物,并与邪恶的蘑菇和食人花敌人战斗,最终抵达城堡的胜利之地。

游戏目标

在这款游戏中,我们的目标是通过控制马里奥完成三个关卡的挑战。每个关卡都有不同的难度和障碍物,玩家需要灵活运用跳跃技巧和反应能力,才能成功通关。同时,消灭普通砖块还可以赚取积分,增加游戏的趣味性和挑战性。

Java实现

为了实现这个经典的游戏,我们将使用Java编程语言进行开发。Java是一种功能强大且广泛使用的编程语言,它具有丰富的图形界面库和游戏开发工具,非常适合用于制作平台跳跃类游戏。

在实现过程中,我们可以使用Java的Swing库来创建游戏的图形界面,包括游戏窗口、角色、背景等元素。同时,我们还需要处理用户的输入操作,例如键盘按键的监听和处理,以便玩家能够控制马里奥的移动和跳跃。

此外,我们还需要考虑游戏的物理引擎和碰撞检测机制,以确保马里奥能够与障碍物和敌人进行正确的交互。这可以通过使用Java的物理引擎库或自己编写相应的算法来实现。

总结

通过使用Java编程语言和相关库,我们可以成功地实现一个经典的超级马里奥小游戏。这将是一个非常有趣和有挑战性的项目,不仅可以锻炼我们的编程技能,还能够让我们体验到游戏开发的乐趣。让我们一起踏上这段冒险之旅吧!

收起阅读 »

Vue 实现 PDF 导出功能

web
笨文旨在通过 html2canvas 和 jspdf,先将页面的 html 转成 canvas,再将 canvas 转成 pdf,同时解决了分页截断的问题。 安装依赖 yarn add html2canvas yarn add jspdf 思路 通过网上的一...
继续阅读 »

笨文旨在通过 html2canvas 和 jspdf,先将页面的 html 转成 canvas,再将 canvas 转成 pdf,同时解决了分页截断的问题。


安装依赖


yarn add html2canvas
yarn add jspdf

思路


通过网上的一些教程,初步实现了 html 转 pdf 的功能,将一整个 DOM 元素放进去,虽然可以粗糙实现,但是出现了很多地方被分页截断的情况,这个时候就需要在某一张图片被截断时,将其自动切换到下一页中。


1.拆解父节点


所以第一步:拆解父节点,一行一行细分为很多子节点,循环遍历这些子节点,累加这些子节点的高度,如果超出了 a4 纸(210*297)的高度,则分页。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function oneNodeMultipleChildren(title, node) {
html2Canvas(node, {
scale: 2, // 清晰度
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4"); // 以mm为单位
let position = 0; // 页面偏移
let contentWidth = canvas.width; // 转换成canvas后的宽度
let contentHeight = canvas.height; // 转换成canvas后的高度
let proportion = 210 / node.offsetWidth; // html缩小至a4纸大小时的比例
let currentHeight = 0; // 当前高度
let imgWidth = 210; // canvas缩小至a4纸大小时的宽度
let imgHeight = (210 / contentWidth) * contentHeight; // canvas缩小至a4纸大小时的高度
let pageData = canvas.toDataURL("image/jpeg", 1.0); // 将canvas转成图片

for (let j = 0; j < node.children.length; j++) {
let childHeight = (node.children[j].offsetHeight + 8) * proportion; // 页面中每行的间距 margin-bottom: 8px

if (currentHeight + childHeight > 297) {
// 如果加上这个子节点后内容超出a4纸高度,就从当前位置开始分页
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight; // 这一页放了多少高度的内容,下一页就从这个高度开始偏移
if (position >= -contentHeight) {
PDF.addPage(); // 添加新pdf页
}
currentHeight = childHeight; // 下一页第一个元素的高度
} else {
currentHeight += childHeight;
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight); // 最后一页
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight); // 在当前pdf页添加图片
PDF.setFillColor(255, 255, 255); // 遮挡的颜色
PDF.rect(0, currentHeight, 210, Math.ceil(297 - currentHeight), "F"); // 添加空白遮挡
// PDF.rect参数分别为:起始横坐标、起始纵坐标、绘制宽度、绘制高度、填充色
}

2.合并父节点


经过上述步骤,一个父节点多个子节点,并且每个子节点独占一行的布局可以实现分页,那要是有很多父节点呢?就需要遍历每个父节点,合并所有子节点,进行分页截断。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function exportPdf(title, id) {
let content = document.querySelector(`#${id}`);
let first = content.firstElementChild.firstElementChild;
let second = content.lastElementChild;
oneNodeMultipleChildren(title, content, [first, second]);
}

export function oneNodeMultipleChildren(title, content, nodes) {
html2Canvas(content, {
scale: 2,
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4");
let position = 0;
let contentWidth = canvas.width;
let contentHeight = canvas.height;
let proportion = 200 / content.offsetWidth;
let currentHeight = 0;
let imgWidth = 200;
let imgHeight = (200 / contentWidth) * contentHeight;
let pageData = canvas.toDataURL("image/jpeg", 1.0);

for (let i = 0; i < nodes.length; i++) {
// 根据传入的父节点数量进行循环,遍历父节点,合并所有子节点
for (let j = 0; j < nodes[i].children.length; j++) {
let childHeight = (nodes[i].children[j].offsetHeight + 8) * proportion;

if (currentHeight + childHeight > 287) {
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight;
if (position >= -contentHeight) {
PDF.addPage();
}
currentHeight = childHeight;
} else {
currentHeight += childHeight;
}
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight); // 在当前pdf页添加图片
PDF.setFillColor(255, 255, 255); // 遮挡的颜色
PDF.rect(0, currentHeight, 210, Math.ceil(297 - currentHeight), "F"); // 添加空白遮挡
}

3.每行多个元素


这个时候新的问题出现了,由于页面布局为 flex 布局,不同缩放下,每行的元素数量会出现变化。所以我们获取第一个子元素与 a4 纸宽度关系,如果为 n 倍,那后面 n-1 个子元素的高度不进行累加。


warning 注意
这里只解决了一行 n 个子元素宽度相等,且近似等于 a4 纸宽度的 1/n 的情况。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function exportAssetPdf(title, id) {
let content = document.querySelector(`#${id}`);
let first = content.firstElementChild.firstElementChild;
let second = content.lastElementChild;
oneNodeMultipleChildren(title, content, [first, second]);
}

export function oneNodeMultipleChildren(title, content, nodes) {
html2Canvas(content, {
scale: 2,
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4");
let position = 0;
let contentWidth = canvas.width;
let contentHeight = canvas.height;
let proportion = 200 / content.offsetWidth;
let currentHeight = 0;
let imgWidth = 200;
let imgHeight = (200 / contentWidth) * contentHeight;
let pageData = canvas.toDataURL("image/jpeg", 1.0);
let sameIndex = 1;
let widthX = 1;

for (let i = 0; i < nodes.length; i++) {
for (let j = 0; j < nodes[i].children.length; j++) {
let childHeight = (nodes[i].children[j].offsetHeight + 8) * proportion;
let childWidth = nodes[i].children[j].offsetWidth * proportion;
if (sameIndex === 1) {
widthX = Math.round(200 / childWidth);
}
if (sameIndex < widthX) {
childHeight = 0;
sameIndex++;
} else {
sameIndex = 1;
}

if (currentHeight + childHeight > 287) {
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight;
if (position >= -contentHeight) {
PDF.addPage();
}
currentHeight = childHeight;
} else {
currentHeight += childHeight;
}
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight); // 在当前pdf页添加图片
PDF.setFillColor(255, 255, 255); // 遮挡的颜色
PDF.rect(0, currentHeight, 210, Math.ceil(297 - currentHeight), "F"); // 添加空白遮挡
}

4.添加左右间距和页眉页脚


为了美化 pdf 布局,上下左右留白,就需要添加左右间距和页眉页脚:减少 html 缩小至 a4 纸大小时的比例和 canvas 缩小至 a4 纸大小时宽高,增加偏移量,并对页眉页脚进行空白遮挡。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function exportAssetPdf(title, id) {
let content = document.querySelector(`#${id}`);
let first = content.firstElementChild.firstElementChild;
let second = content.lastElementChild;
oneNodeMultipleChildren(title, content, [first, second]);
}

export function oneNodeMultipleChildren(title, fNode, sNode) {
html2Canvas(fNode, {
scale: 2,
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4");
let position = 0;
let contentWidth = canvas.width;
let contentHeight = canvas.height;
let proportion = 200 / fNode.offsetWidth; // 减少10mm
let currentHeight = 0;
let imgWidth = 200; // 减少10mm
let imgHeight = (200 / contentWidth) * contentHeight; // 减少10mm
let pageData = canvas.toDataURL("image/jpeg", 1.0);
let sameIndex = 1;
let widthX = 1;

for (let i = 0; i < sNode.length; i++) {
for (let j = 0; j < sNode[i].children.length; j++) {
let childHeight = (sNode[i].children[j].offsetHeight + 8) * proportion;
let childWidth = sNode[i].children[j].offsetWidth * proportion;
if (sameIndex === 1) {
widthX = Math.round(200 / childWidth); // 减少10mm
}
if (sameIndex < widthX) {
childHeight = 0;
sameIndex++;
} else {
sameIndex = 1;
}

if (currentHeight + childHeight > 287) {
// 减小10mm
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight;
if (position >= -contentHeight) {
PDF.addPage();
}
currentHeight = childHeight;
} else {
currentHeight += childHeight;
}
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 5, position + 5, imgWidth, imgHeight); // 增加偏移量
PDF.setFillColor(255, 255, 255);
PDF.rect(0, 0, 210, 4, "F"); // 添加页眉遮挡
PDF.rect(0, currentHeight + 5, 210, Math.ceil(292 - currentHeight), "F"); // 添加页脚遮挡
}

成果展示


不同缩放下导出 PDF 对比:


每行一个子元素


每行多个子元素


作者:虚惊一场
来源:juejin.cn/post/7291142504123875364
收起阅读 »

如何创建五彩纸屑效果

web
前言 很多网站会在一些按钮上面加上不同的动画效果,这些动感的效果能够更加容易的创建具有视觉吸引力的用户界面。 本文将介绍一个小巧的 JavaScript 库,它能够用非常短的时间以及简短的代码量创建我们想要的五彩纸屑效果 简单使用 我们可以通过 npm 安装或...
继续阅读 »

前言


很多网站会在一些按钮上面加上不同的动画效果,这些动感的效果能够更加容易的创建具有视觉吸引力的用户界面。


本文将介绍一个小巧的 JavaScript 库,它能够用非常短的时间以及简短的代码量创建我们想要的五彩纸屑效果


简单使用


我们可以通过 npm 安装或从 cdn 导入两种方式来使用这个库。


这里我采用的是导入的方式。


在你导入完成这个库之后,我们需要一个按钮


<button onclick="myClick()">button</button>

最简单的特效只需要在点击函数当中调用 confetti 函数即可


function myClick () {
confetti()
}

动画4.gif


细节配置参数


通过传入 options 属性,我们可以在特效上自定义很多我们需要的部分,下面是部分配置属性的作用,后面我们会挑出部分属性来展示一下效果:


属性名作用
particleCount飞出的纸屑的数量,默认 50
angle飞出的纸屑的角度 90 是向上,默认 90
spread飞出的纸屑偏离中心的角度,默认 45
startVelocity飞出的纸屑的初始速度,默认 45
decay飞出的纸屑的减速度,范围 0-1 之间,默认 0.9
gravity重力,默认 1
decay飞出的纸屑的减速度,范围 0-1 之间,默认 0.9
flat是否关闭纸屑的翻转,默认 false
ticks纸屑消失速度,默认 200
origin对象,设置发射纸屑的原点,有 x y 两个参数,取值都是 0 - 1 对应上下边缘,默认 0.5 0.5
colors数组:十六进制格式的颜色字符串数组
shapes数组:纸屑的形状
scalar每个五彩纸屑粒子的比例,默认 1
zIndex纸屑的z-index,默认 100
disableForReducedMotion禁用五彩纸屑,默认 false

根据上面的参数,你可以很容易的定义自己想要的效果,下面我们随意定义部分例子:


<body>
  <button onclick="myClick1()">左倾斜</button>
  <button onclick="myClick2()">全是黑色</button>
  <button onclick="myClick3()">数量很多多多</button>
 <script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.0/dist/confetti.browser.min.js"></script>
  <script>
    function myClick1 () {
      confetti({
        angle: 135,
      });
    }
    function myClick2 () {
      confetti({
        colors: ['#000000']
      });
    }
    function myClick3 () {
      confetti({
        particleCount: 500,
      });
    }
 
</script>

</body>

动画4 1.gif


详细定义纸屑形状


在上面的配置当中我们已经可以定义纸屑的大部分配置,其中 shapes 可以用于定义纸屑的形状,官方为我们预留了三个形状 'square', 'circle', 'star' 分别对应 方形,圆形,星星,这个字段传入的是一个数组,这个数组中元素的数量,决定了这个形状在所有纸屑中的比例,比如说你要是传入了 ['square', 'circle', 'star'] 那么三种形状就都是占比三分之一。


除了通过官方预留的形状,我们还可以通过两个函数来进行形状自定义,分别是 confetti.shapeFromPathconfetti.shapeFromText


confetti.shapeFromPath


这个函数可以传入一个对象,对象中存在一个 pathkey,这个就决定了创建出来的形状长什么样子


下面的代码可以创建一个三角形的纸屑


var triangle = confetti.shapeFromPath({ path: 'M0 10 L5 0 L10 10z' });

confetti({
shapes: [triangle],
});

confetti.shapeFromText


这个函数同样传入一个对象,对象存在 textscalartext 可以传入任何文字,甚至是一些文字表情也可以使用, scalar 配合 optionsscalar 使用,两个相差过大会导致字体变得很模糊。


下面的代码你就可以创建一个字符串纸屑


var scalar = 2;
var pineapple = confetti.shapeFromText({ text: '🍍🍍🍍', scalar });
confetti({
shapes : [pineapple],
scalar,
});

逻辑事件


纸屑在生成的时候,我们可以会需要一些事件,比如说在我们想要的时候清除掉屏幕上还未消失的纸屑,又或者在一次纸屑彻底消失的时候执行某些逻辑。


消除纸屑


我们可以通过调用 confetti.reset(); 来消除屏幕上的纸屑


监听纸屑消失事件


在我们调用 confetti() 的时候会返回一个 promise 对象,这个对象将会在纸屑完全消失的时候回调。


自定义纸屑产生的位置


在上面的例子当中,我们可能会发现,纸屑都只能在屏幕的正中心产生,这是因为 options 里的 origin 属性的默认值,我们可以通过一些方式来自定义这个产生的位置


动态设置 origin


我们可以通过动态的来设置这个值来做到自定义产生的位置。


在点击事件当中,我们能够拿到当前的点击对象,通过这个 event 对象以及 window 上获取到可视区域的宽高,我们不难算出当前按钮相对于左右的位置。


const windowHeight =
  window.innerHeight ||
  document.documentElement.clientHeight ||
  document.body.clientHeight;
// 获取浏览器高度
const windowWidth =
  window.innerWidth ||
  document.documentElement.clientWidth ||
  document.body.clientWidth;
// 获取浏览器宽度
const origin = {
x: event.pageX/windowWidth,
y: event.pageY/windowHeight,
}
// 获取比例

自定义 canvas


官方支持我们创建自定义画布的  confetti 实例,这样创建的  confetti 不会超出定义的这个画布的范围,这在某些时候可能会起到很重要的作用。


并且官方强调了,一个画布最好只和一个  confetti 实例做关联,所以说当我们创建多个  confetti 实例的时候,就也需要创建多个画布。


因为是自定义添加的画布,所以我们需要在不需要的时候手动去删除这个画布,避免产生多余的元素。


var myCanvas = document.createElement('canvas');
var container = document.querySelector('.container')
container.appendChild(myCanvas)
var myConfetti = confetti.create(myCanvas).then(()=>{
container.removeChild(myCanvas)
});

然后设定位置的操作我这里就不多做了,只需要定义这个 canvas 的生成位置就好了。


示例代码



作者:14332223
来源:juejin.cn/post/7290769553572397056
收起阅读 »

如果回到过去,我会这样告诫我自己

如题,一些牢骚。希望对年轻的你有点帮助。 # 勇敢点 提前规划,在大学的时候,你就应该开始“面向大厂”规划自己的职业路线,争取校招进入大厂实习。 不要整天宅在图书馆,看什么 Linux 内核、Unix 编程艺术、Unix 网络编程、计算机的构造与解释、不要学那...
继续阅读 »

如题,一些牢骚。希望对年轻的你有点帮助。


# 勇敢点


提前规划,在大学的时候,你就应该开始“面向大厂”规划自己的职业路线,争取校招进入大厂实习。


不要整天宅在图书馆,看什么 Linux 内核、Unix 编程艺术、Unix 网络编程、计算机的构造与解释、不要学那么多编程语言,Python、Java、Perl、Ruby、JavaScript、PHP、Go、C++…


多刷题,一定要争取去大厂,你一定可以,只要你勇敢点。


在现实生活中,人们往往依靠勇气而不是智慧去取得领先的地位。


# 不要呆在非一线城市


这里机会非常少,不是你不行,不要自卑,不要内耗,这不是你的问题,你应该去能够发挥你价值的地方。


虽然那里生活和工作节奏快。但是现在国内互联网企业都一个德性,都在模仿“狼性”文化,说着一样的互联网黑话,一样是内卷,去一线城市、一线大厂赚更多钱不好吗?


为了钱,不寒碜。


# 不要只关注技术,花点时间了解世界


不要只关注技术!别老是看那些技术类的书,你压根记不住,也很少有实践的机会。


多看点别的书,多了解自己、了解人类、了解世界、了解政治、了解经济/商业的运作原理和底层逻辑。提前布局、提前投资


世事洞明皆学问,不要穷极一生都是为了钱而工作,成为钱的奴隶,一直被恐惧和欲望支配。


推荐图书:人类简史、纳瓦尔宝典、富爸爸穷爸爸、黑客与画家


# 接受不完美的自己和代码


不要追求完美,你很普通,接受自己的平庸吧。


你不可能什么都精通,把精力花在自己核心竞争力上。


就像系统总有改不完的 bug,接受不完美的自己,学会放弃。


不要单打独斗、 尽量和更聪明的人共事。在矮子里面当将军,不如在巨人里面做士兵。


# 了解你的公司


不要只关注你眼前的这颗螺丝钉。



  • 你的公司是做什么的?核心竞争力是什么?

  • 公司的管理模式是什么?为什么要这样管理?规范的目的又是什么?

  • 公司的商业模式是什么?靠什么赚钱?怎么卖出去?

  • 公司的用户是谁?给用户创造了什么价值?

  • 公司的技术架构是怎样?

  • 公司的组织架构为什么设计?团队之间又是怎么协作的

  • 我努力加班赚的钱,最后进谁兜里?凭什么是他?



难道你不好奇?不八卦吗?


# 多积攒人脉


某些关键时刻,他们能捞你一把。同时你也要努力成为别人的有价值的人脉。


这其实并不需要你付出真感情,而应该把它当作资产。


# 可以认清现实,但是要保持批判精神


宏观的大环境个人是无法干预的,我们只能去适应。当然适应并不意味着委屈求全,每个人有选择的权利,当你无法接受公司的工作环境,不能接受公司的价值观,我们是可以选择跳出来的,而且越早越好。


我们的适应能力很强,同样能够适应糟糕的问题,然后置之不理。就比如人类的嗅觉,古人云”入芝兰之室,久而不觉其香;入鲍鱼之肆,久而不觉其臭”。


就比如笔者所在公司最近开始抓考勤打卡了,一开始内心十分抵触,现在也慢慢‘适应’了,也没有之前的抱怨,但我知道这对我来说并不是一件好事。


既要认清现实,保持批判精神,否则将一成不变。


# 关注战略设计


大部分程序员都是实现者,即战术实现者。很多时候,我们都不知道我们的工作的价值是什么。


因此我们也要关注战略设计,保持对一切事情的好奇心,尝试突破自己的职能边界,没人会阻止你,也很少人会给你机会。


# 效率从来不是一个人的事情,伟大的项目也是如此


不要相信小说、传记里面的孤胆英雄。


前几年关于 10 倍程序员也很多讨论,比如极客时间 10X 程序员工作法, 这些教程总结了很多务实的提效方法论。


总的来说,提高效率从来不是一个人的事情,另外程序员的主要工作‘编码’ 也仅仅只占整个研发流程的 20% ~ 30%。


# 不要什么都亲力亲为,学会外包


让你的能力和知识可以复制和传递,比如 CodeReview,技术写作,写好文档。


培养得力的助手,或者更好的方式是招揽比你更聪明的人。


# 不要轻信什么最佳实践


没有绝对正确的东西,没有放之四海皆准的东西。


学习它们,然后忘掉


# 问题的维度


不要只关注吃掉眼前的棋子,从更高的维度去解决问题。


举个例子



  • 问题域。有些问题不一定就要在技术层面解决,可能在产品层面、战略层面就能规避掉。

  • 解决域。另外,提升抽象的高度,在解决问题时能否举一反三?覆盖更多场景?


# 不要被奴役


有房一族(来源:富爸爸穷爸爸)



  • 鼓励抱怨,但也要解决问题。

  • 不要为了钱而工作,让钱为你工作。

  • 工作不是为了写代码,让代码为你工作。

  • 把自己当成一家公司去经营

  • 不要被雇主奴役,你和他们是雇佣合作关系,不是奴隶关系,不要被 PUA

  • 不要被机器奴役。人类创造编程语言是为了服务人类,而不是服务机器。不要追求那些反人类的奇技淫巧,也不要自以为掌握了一门底层、学习曲线陡峭的编程语言而沾沾自喜,不符合人类心智的技术迟早被淘汰。


# 不管你喜不喜欢,在中国你还是得要学会“管理”


金字塔


在国内 IT 打工人的体系更像是军队管理,俗话说就是吃年轻饭的。


尽管未来的趋势是分工的精细化,管理者也是占少数。为什么我就不能当个平庸的程序员呢?



  • 我们所处的社会主导集体主义,自然也会滋生对权力的崇拜,很多人对这个金字塔尖趋之若鹜。权利也意味着‘成功’

  • 平庸很容易被取代。而熟练工并没有壁垒,你能干别人也能干。商业是逐利,在高度内卷的市场下,为什么就不能选择跟便宜、精力更旺盛的年轻人呢?


你面前可能有几条路,创业、技术专家、管理,不管是哪条路都是很艰难,管理在很多人看来是顺理成章。


或者,现在就想想,你不搞 IT 还能干什么?提前做好投资


# 精心炮制的故事


这个世界的秩序是由精心炮制的故事组成,而且大部分人都相信它。


保持怀疑的姿势,可以让你跳出游戏


# 直面你的恐惧


为什么你会社恐?


为什么几天后的一个会议会让你忐忑不安?


为什么你那么在乎别人的看法?


为什么你总是感觉到焦虑?


为什么你不敢说出你的真实想法?


为什么你会情不自禁地与别人做比较,然后妄自菲薄?


为什么你会恐惧?如果人生是一场游戏呢?


# 编程的本质是抽象


编程是一门抽象艺术。


把现实世界的业务抽象成二维表,数据结构,对象关系、业务流程。


前端页面抽象成组件,低代码,DSL,本质上都是抽象的艺术。


抽象需要发挥人的主观能动性,人与人的差距就体现在这里。而工具通过学习一般都能掌握,很难建立壁垒


# 不要自我感动



  • 只有功劳,没有苦劳。

  • 选择比努力更重要。

  • 大部分企业并不在乎你代码写得多多漂亮,而在于你能不能真正创造价值。


# 继续保持专注


远离那些垃圾。


# 总会有差评


不管是多好的产品都会有差评。


# 不要随波逐流


我发现,程序员群体大部分是比较‘安分守己’的,身边很多典型的例子,早早地结婚,早早地生子,996 的工作,电子产品自由,开着特斯拉,在老家可能还有套套牢的房。


人生的轨迹和父辈其实不会有太大的区别。


这真是你想要的人生吗?


# 降低欲望,你会更快乐


所有的痛苦都来源于欲望,远离一切成功学,回头是岸


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

实习到毕业一年的回忆:工作旅程

前两天和实习那会的同事一起吃饭,聊到了他们那会刚毕业两三年的工作收入,问完我之后说,“你刚毕业一年的起点太高了,税后五位数,而且还是大专学历,这在外面根本找不到薪资那么多的工作”,“这一切还是得感谢你们几个人,如果不是前年你们收留了我,估计我都不干这行跑去流水...
继续阅读 »

前两天和实习那会的同事一起吃饭,聊到了他们那会刚毕业两三年的工作收入,问完我之后说,“你刚毕业一年的起点太高了,税后五位数,而且还是大专学历,这在外面根本找不到薪资那么多的工作”,“这一切还是得感谢你们几个人,如果不是前年你们收留了我,估计我都不干这行跑去流水线拧螺丝了”,我点头说道。


21年六月在学校投了上百份简历,面试收到了几个offer,但是实习工资给的太少,不是2.5k或者3k,这对于那时年少轻狂的我怎么可能接受呢,果断拒绝,快月底临近毕业找不到工作的我越来越慌了,后来约了一家线上面试并且通过了,实习工资150一天,正常每个月能拿3.3k,有节假日的情况下只能拿到不到2.8k的可怜工资。但命运真的很神奇,因为这家实习公司,结识了能够在职场上帮助到我的良师益友。


实习公司所在的写字楼


21年十月认识了一位朋友介绍的女生,可能是好久没和女生接触过,我变得不怎么会和女生聊天了,只记得我和她打了两个月的王者,基本上天天玩,还都是玩的人机,后来不知道啥原因就凉凉了,当然两个月也没见过面。当然因为这个事搞的我心烦意乱,工作没法工作,21年底,22年初,也就是元旦期间,我向公司提出离职,电话裸辞,直接就不去公司了,给老板整的一脸懵逼。22年一月中旬,公司聚餐邀请了已经离职的我,晚上酒喝起兴的我,在同事的劝说下,我向老板表明了我想回到公司的意向,后来如愿以偿的回到了公司,此时,我的工资不是150一天了,而且达到了惊人的4.5k每月。


上班路上的金鸡湖大道


22年六月临近毕业,在实习公司沉淀了一年,我觉得时机已经成熟是时候走了,鼓起勇气和老板说了离职,老板同意了。这个时候我还不知道未来的一年,我还会和他们经常聚餐,一起聊行业、工作、生活。甚至今天的这份工作也得益于他们。


离职后,准备去南京发展,当时在常州的同学那暂住了几天,闲的没事干就投了几份简历玩玩,面试了两家都收到了offer,一家给政府做erp系统的公司给了7.5k,另一家是上市公司的外包给了8k,随后我就不想去南京了,选择了那家外包公司,在那前几个月基本上天天没事,过的相当的安逸,每天晚上下班后,5:30准备到球场,后来我换了个组长,我开始做MES系统了,第一个系统我身份是打杂的,给另一个同事当助手,后来做的系统,我开始当主力开发。22年底,工作干的十分不顺心,萌生了离职的想法,向外包公司的部门经理提了涨薪,他只给涨500块钱,我觉得也没必要留下了,所性直接离职,此时我还没有转正,所以我直接在一周到走人。


再次离职后,我选择回到老家休息一段时间,思考一下第二年该去往何处。在家乡待了近四十天,基本上没有碰过电脑,我到处的玩,打球,打游戏,泡澡,感觉已经废了。


过年前几天,我开始慌了,于是我重新打开我的小米笔记本,打开了熟悉又陌生的IDEA,学习了几个开源框架,背了一些面试题,准备年后去外地找工作。


CIM开源框架


大年初三,我早早的买好火车票去往常州,准备在常州找一份工作,可惜我找了近一周,一份工作也没有找到,于是我将目光看向南京和老东家所在的苏州。我联系了实习公司一个同事现在所在的公司,于是他将我内推到了现在的这个公司,他向上面的人担保我肯定没有问题,所以我直接跳过了面试,也就是在这个公司,因为我代码写的好,所以我两次加薪达到了税后五位数。


23年五月二十号,公司安排我去西安出差两周,这是我人生第一次出差,见到了网络上所谓的甲方,值得我纪念一下。


飞机上的云层
仓库


如今,那位内推我的同事,也就是我第一份实习公司的同事,他要走了,去了一家做大数据的公司,领导让我开始学习做管理,以后带新人做项目,我只能说尽力而为。


对于像我这样学历不高的人而言,个人觉得代码不是技术架构,而是人情世故,人脉是人生宝贵的一笔财富。


浪子花梦


上班摸鱼写于2023年7月12日11点。


作者:浪子花梦
来源:juejin.cn/post/7254572372137410597
收起阅读 »

使用a标签下载文件

web
引言 HTML中  <a>  元素(或称锚元素)可以通过它的 href 属性创建通向其他网页、文件、电子邮件地址、同一页面内的位置或任何其他 URL 的超链接。 <a> 中的内容应该指明链接的目标。如果存在 href 属性,当 <...
继续阅读 »

引言


HTML中  <a>  元素(或称元素)可以通过它的 href 属性创建通向其他网页、文件、电子邮件地址、同一页面内的位置或任何其他 URL 的超链接。


<a> 中的内容应该指明链接的目标。如果存在 href 属性,当 <a> 元素聚焦时按下回车键就会激活它。


本文主要讲解如何通过a标签来下载文件。


download属性


浏览器将链接的 URL 视为下载资源。可以使用或不使用 filename 值:




  • 如果没有指定值,浏览器会从多个来源决定文件名和扩展名:



    • Content-DispositionHTTP 标头。

    • URL的最后一段。

    • 媒体类型。来自 Content-Type 标头,data: URL的开头,或 blob: URL 的 Blob.type




  • filename:决定文件名的值。/ 和 \ 被转化为下划线(_)。文件系统可能会阻止文件名中其他的字符,因此浏览器会在必要时适当调整文件名。





备注:



  • download 只在同源 URL或 blob:data: 协议起作用。

  • 浏览器对待下载的方式因浏览器、用户设置和其他因素而异。在下载开始之前,可能会提示用户,或者自动保存文件,或者自动打开。自动打开要么在外部应用程序中,要么在浏览器本身中。

  • 如果 Content-Disposition 标头的信息与 download 属性不同,产生的行为可能不同:

  • 如果文件头指定了一个 filename,它将优先于 download 属性中指定的文件名。

  • 如果标头指定了 inline 的处置方式,Chrome 和 Firefox 会优先考虑该属性并将其视为下载资源。旧的 Firefox 浏览器(版本 82 之前)优先考虑该标头,并将内联显示内容。



下载方式


1. 直接使用a标签的href属性指定文件的URL


可以在a标签中使用href属性指定文件的URL,点击链接时会直接下载文件。


<a href="file_url">Download</a>

优点:简单易用,只需在a标签中指定文件的URL即可。


缺点:无法控制下载文件的名称和保存位置。


2. 使用download属性指定下载文件的名称


可以在a标签中使用download属性指定下载文件的名称,点击链接时会将文件以该名称保存到本地。


<a href="file_url" download="file_name">Download</a>

优点:可以控制下载文件的名称。


缺点:无法控制下载文件的保存位置。


3. 将文件数据转为Blob进行下载


当需要将文件数据转为Blob或Base64进行下载时,可以使用以下方法:


1. 将文件数据转为Blob进行下载


function downloadFile(data, filename, type) {
const blob = new Blob([data], { type: type });
const url = URL.createObjectURL(blob);

const link = document.createElement('a');
link.href = url;
link.download = filename;

document.body.appendChild(link);
link.click();

document.body.removeChild(link);
URL.revokeObjectURL(url);
}

function fileToBlob(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onloadend = () => {
resolve(new Blob([reader.result], { type: file.type }));
};

reader.onerror = reject;

reader.readAsArrayBuffer(file);
});
}

// 使用示例
const fileData = ...; // 文件数据
const fileName = 'example.txt';
const fileType = 'text/plain';

fileToBlob(fileData).then(blob => {
downloadFile(blob, fileName, fileType);
});

首先,我们定义了一个名为downloadFile的函数,它接受三个参数:文件数据(data),文件名(filename)和文件类型(type)。 在函数内部,我们使用Blob构造函数将文件数据和类型传递给它,从而创建一个Blob对象。然后,我们使用URL.createObjectURL()方法创建一个URL,该URL指向Blob对象。 接下来,我们创建一个<a>元素,并设置其href属性为之前创建的URL,并将下载属性设置为指定的文件名。然后将该元素添加到文档的body中。 最后,我们模拟用户点击该链接进行下载,并在完成后清理相关资源。


在使用时,我们首先调用fileToBlob函数将文件数据转换为Blob对象。该函数返回一个Promise对象,在Promise的resolve回调中返回了转换后的Blob对象。 然后,在Promise的回调中调用了downloadFile函数来进行下载。


2. 将文件数据转为Base64进行下载


function downloadBase64File(base64Data, filename, type) {
const byteCharacters = atob(base64Data);
const byteArrays = [];

for (let i = 0; i < byteCharacters.length; i++) {
byteArrays.push(byteCharacters.charCodeAt(i));
}

const blob = new Blob([new Uint8Array(byteArrays)], { type: type });
const url = URL.createObjectURL(blob);

const link = document.createElement('a');
link.href = url;
link.download = filename;

document.body.appendChild(link);
link.click();

document.body.removeChild(link);
}

function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onloadend = () => {
resolve(reader.result.split(',')[1]);
};

reader.onerror = reject;

reader.readAsDataURL(file);
});
}

// 使用示例
const fileData = ...; // 文件数据
const fileName = 'example.txt';
const fileType = 'text/plain';

fileToBase64(fileData).then(base64Data => {
downloadBase64File(base64Data, fileName, fileType);
});

首先,我们定义了一个名为downloadBase64File的函数,它接受三个参数:Base64字符串(base64Data),文件名(filename)和文件类型(type)。 在函数内部,我们首先将Base64字符串解码为字节数组,并将其存储在byteArrays数组中。然后,我们使用这些字节数组创建一个Blob对象,并使用URL.createObjectURL()方法创建一个URL。 接下来,我们创建一个<a>元素,并设置其href属性为之前创建的URL,并将下载属性设置为指定的文件名。然后将该元素添加到文档的body中。 最后,我们模拟用户点击该链接进行下载,并在完成后清理相关资源。


在使用时,我们首先调用fileToBase64函数将文件数据转换为Base64字符串。该函数返回一个Promise对象,在Promise的resolve回调中返回了转换后的Base64字符串。 然后,在Promise的回调中调用了downloadBase64File函数来进行下载。


总结


您可以根据需要选择将文件数据转为Blob或Base64进行下载。如果您已经有文件数据,可以使用fileToBlob函数将其转为Blob对象并进行下载。如果您希望将文件数据转为Base64进行下载,可以使用fileToBase64函数将其转为Base64字符串,并使用downloadBase64File函数进行下载。


作者:前端俊刚
来源:juejin.cn/post/7291200719683502120
收起阅读 »

前端实现蜂巢布局思路

web
效果图如下 上图的组成可以分为两部分,一个为底图六边形的形成,一个为内容六边形的形成。 要生成对应的六边形,首先要获得需要绘制的六边形的中心坐标。 观察不难得出结论,以中心的六边形为基点,第二圈很明显能排6个,第三圈能排12个,以此进行类推。 这里可以以中...
继续阅读 »

效果图如下


image.png
上图的组成可以分为两部分,一个为底图六边形的形成,一个为内容六边形的形成。


要生成对应的六边形,首先要获得需要绘制的六边形的中心坐标。


观察不难得出结论,以中心的六边形为基点,第二圈很明显能排6个,第三圈能排12个,以此进行类推。


image.png


这里可以以中心点为坐标原点[0,0],以[1,0], [1,1],[-1,1],[-1,0],[-1,-1],[1,-1]这种形式来表示第二圈在轴线上的六个六边形的中心点关系(这是一种形式,并非真实的坐标系坐标)。


// 第二圈时的对应圆上的坐标值
const pixiArr = [
[1, 0],
[1, 1],
[-1, 1],
[-1, 0],
[-1, -1],
[1, -1]
]

// 根据圈数生成对应轴线上的坐标
function generatePixiArrByWeight(weight) {
if (weight === 2) {
return pixiArr
}
const multiple = weight - 1
const tempPixiArr = pixiArr.map((x) => {
return [x[0] * multiple, x[1] * multiple]
})
return tempPixiArr
}

进一步观察,可知,第三圈时两条发散的轴中间夹了一个六边形,第四圈时两条发散的轴中间夹了两个六边形,依次类推。


六条发散轴上的六边形中心点坐标是最容易计算的,不过要计算三圈及其开外的,就得有那么一点点的数学基础,知道sin60度cos60度的意思。


const sin60 = Math.sin(Math.PI / 3)
const cos60 = Math.cos(Math.PI / 3)

有了上面的铺垫后就可以开始了,定义一个函数,传入的参数为六边形总个数和六边形的边长


// 生成六边形中心坐标
function getHexagonCoordinateArrayByTotal(total = 0, radius = 0){
// 1、获取圈数weight
if (total === 0) return []
let tierList = [] // 用于存放每圈的个数
let tierIndex = 0
while (total > 0) {
if (tierIndex === 0) {
tierList.push(1)
total = total - 1
} else {
let n = 6 * tierIndex
total = total - n
if (total < 0) {
tierList.push(total + n)
} else {
tierList.push(n)
}
}
tierIndex++
}
const weight = tierList.length
// 2、根据圈数去获取coordinateArray坐标列表
// getHexagonCoordinateArrayByWeight:根据圈数和边长返回对应的坐标点
const weight = tierList.length
let coordinateArray = []
for (let i = 0; i < weight; i++) {
if (i + 1 === weight) {
coordinateArray = [
...coordinateArray,
...getHexagonCoordinateArrayByWeight(i + 1, radius).slice(
0,
tierList[weight - 1]
)
]
} else {
coordinateArray = [
...coordinateArray,
...getHexagonCoordinateArrayByWeight(i + 1, radius)
]
}
}

return coordinateArray
}

有个getHexagonCoordinateArrayByWeight需要实现其,方式为


function _abs(val = 0) {
return Math.abs(val)
}

function getHexagonCoordinateArrayByWeight(weight = 1, radius = 0) {
if (weight === 0) return []
if (weight === 1) return [[0, 0]]
const addNum = weight - 2
const addArr = generatePixiArrByWeight(weight)
const hypotenuse = radius * sin60 * 2 // 两倍的边心距长度
let offsetArr = []
let offsetX
let offsetY
for (let i = 0; i < addArr.length; i++) {
const t = addArr[i]
if (t[1] !== 0) {
offsetX = t[0] * hypotenuse * cos60
offsetY = t[1] * hypotenuse * sin60
} else {
offsetX = t[0] * hypotenuse
offsetY = 0
}
offsetArr.push([offsetX, offsetY])
}
const tempOffsetArr = JSON.parse(JSON.stringify(offsetArr))
let resArr = new Array(6 * (weight - 1))
let lineArr = []
for (let i = 0; i < 6; i++) {
let lindex = i * (weight - 1)
resArr[lindex] = tempOffsetArr[i]
lineArr.push(lindex)
}
// 利用已知的六个发散轴上的中心坐标点推出剩余的中心坐标点
if (addNum > 0) {
for (let i = 0; i < 6; i++) {
let s = tempOffsetArr[i]
let e = i + 1 === 6 ? tempOffsetArr[0] : tempOffsetArr[i + 1]
let si = lineArr[i]
let sp = addNum + 1
let fx
let fy
if (i === 0) {
fx = (s[0] - e[0]) / sp
fy = (e[1] - s[1]) / sp
}
if (i === 1) {
fx = (_abs(s[0]) + _abs(e[0])) / sp
fy = 0

}
if (i === 2) {
fx = (_abs(e[0]) - _abs(s[0])) / sp
fy = (_abs(s[1]) - _abs(e[1])) / sp
}
if (i === 3) {
fx = (_abs(s[0]) - _abs(e[0])) / sp
fy = (_abs(e[1]) - _abs(s[1])) / sp
}
if (i === 4) {
fx = (_abs(s[0]) + _abs(e[0])) / sp
fy = 0
}
if (i === 5) {
fx = _abs(s[0]) / sp
fy = (_abs(e[1]) - _abs(s[1])) / sp
}
let mr = []
for (let j = 0; j < addNum; j++) {
if (i === 0 || i === 1) {
mr.push([s[0] - fx * (j + 1), s[1] + fy * (j + 1)])
}
if (i === 2) {
mr.push([s[0] - fx * (j + 1), s[1] - fy * (j + 1)])
}
if (i === 3) {
mr.push([s[0] + fx * (j + 1), s[1] - fy * (j + 1)])
}
if (i === 4) {
mr.push([s[0] + fx * (j + 1), s[1] - fy * (j + 1)])
}
if (i === 5) {
mr.push([s[0] + fx * (j + 1), s[1] - fy * (j + 1)])
}
}
mr.forEach((x, index) => {
resArr[si + index + 1] = x
})
}
}
return resArr
}

至此,生成六边形中心坐标点的方法完成。
有了中心坐标生成方式之后,就可以使用Konva这种辅助绘图的库来进行效果绘制了。


作者:前端_六一
来源:juejin.cn/post/7291125785796018236
收起阅读 »

过度设计的架构师们,应该拿去祭天

我发现一个非常有趣的现象。 十多年前,那时“美女”这个称谓还是非常稀缺值钱的,被这么称呼的女性同胞占比,也就是不到10%的样子。 后来就愈发不可收拾了,只要是个女的活的,下至5岁上至50岁的,99%都被人称呼过“美女”。 当然,现在互联网行业的架构师,也越来越...
继续阅读 »

我发现一个非常有趣的现象。


十多年前,那时“美女”这个称谓还是非常稀缺值钱的,被这么称呼的女性同胞占比,也就是不到10%的样子。


后来就愈发不可收拾了,只要是个女的活的,下至5岁上至50岁的,99%都被人称呼过“美女”。


当然,现在互联网行业的架构师,也越来越“美女化”了,基本上有个两三年工作经验的,带两三个应届生负责过一两个QPS不过十,用户量不过千的小系统的,把项目用SSM框架给搭建起来的,也都成架构师了。



而这些所谓的“架构师”们,如果仅仅是title上的改动,平时工作中该撸代码就撸代码,该摸鱼看网页就看网页,其实也真的没什么。


最最最最怕的就是,他们觉得自己的身份已经变了,是时候该体现出自己作为系统架构师价值的时候了,那一切就会变得不可收拾了。


这些架构师们体现价值的方式当然是做架构设计。按照他们的话说,系统架构要具备前瞻性、灵活性、复用性、伸缩性、可维护性、可扩展性、低耦合性、高内聚性、可移植性。当然,基本上90%都是过度设计。



下面让我们来细数一下,那些年,我所经历过的过度设计。



名副其实的微服务


不久前我面试过一个中小厂架构师,看他的简历上赫然写着,“主导XX系统从单体服务往微服务架构演进工作”。


然后我问他的问题是:“详细说下微服务拆分这件事情,包括:微服务拆分的原因、时机和拆分后的粒度。”


这个架构师说的第一句话就把我雷到了:“微服务拆分的粒度,我认为越细越好,不然为什么叫微服务呢?而且,现在的一个很小的微服务,随着业务的持续迭代演进,未来都有可能变得非常庞大,我们做架构设计的,必须要具备前瞻性。”


他接着说:“我们的微服务不但按照业务模型进行的拆分,而且我还按照controller层、service层和dao层也做了拆分,这样可以提升代码复用性,你想用我哪层的代码,就可以调用我哪层的API。”


最终,一个单体服务就被他拆分成了这样。



我问他:“微服务的‘三个火枪手原则’了解吗?”


他摇了摇头,说不清楚。


我心里感慨到,今年阿里云和腾讯云业绩能不能达标,全看这类架构师的了,他们是真费机器啊。


3个库和300张表


去年,跟一个三方公司临时组建了一个项目组,共同开发孵化A项目。


项目联调期间,我跟三方公司的小A说:“我刚调用了你们项目的XX接口,新增了20条交易数据,你看看你们接口的业务处理正常吗?数据库里面有这20条数据吗?”


小A说:“好的,稍等,我看看。”


20分钟过去了,我问小A看得怎么样了。


小A说:“业务处理是正常的,数据我正在一条条找,20条已经找到17条了,我在找剩下的3条。”


我听得有些懵逼,问小A:”你直接从你们订单表里,不能一下子看到20分钟前写入的20条数据吗?为什么还需要一条条找啊?“


小A说:”我们的架构师老张,按照每天三百万订单的数据增量,做了一个五年架构规划,已经分好了3个库和300张表。我现在正在根据他的路由规则,一条条地找这些数据。“



满城尽是大中台


呵呵,忽如一夜春风来,满城尽是大中台。


2015年福厂正式提出了“大中台、小前台”的中台战略,通过将原本分散到各个业务的支持部门,比如技术部门、数据部门集中到一起,进行快速的服务迭代,以期更高效地支撑前线,大幅降低支持部门的重复投资建设。



三年后,各个大小互联网公司纷纷跟进,争相建设自己家的中台,也就在这时,某独角兽公司的架构师老范过来找我取经。


我跟老范说:“你们的两个主业务是机票和酒店,业务差别太大了,且创新孵化业务并不多,并不适合中台策略。”


老范说:“不,中台这个我们一定要搞,因为既是研发团队的政治任务,也是我个人的技术追求。”


半年后,我问老范搞得怎么样了,老范说:“唉,讨论了半年哪些职责属于大中台,哪些职责属于小前端,现在还没讨论明白呢。”


无处不在的消息队列


福厂收购了某公司,在收购后的一次技术交流中,我听到对方公司的首席架构师说:“MQ是个好东西,能异步处理,能消峰,能解耦,还是应该在项目中多用用的。”



后来发现,大首席架构师的下级执行力真强,MQ真的在他们的项目中无处不在:



  • 发短信验证码的场景用MQ,且其生产者和消费者是同一个服务,就为了用MQ异步调用短信服务的SDK;

  • 打业务日志的场景用MQ,且其生产者和消费者是同一个服务,就是为了用MQ异步打一行日志;

  • TPS个位数的约课场景用MQ,且其生产者和消费者是同一个服务,其美名曰进行消峰;

  • 各服务间的通信基本上80%都用了MQ,而不是RPC,其美名曰系统解耦;


牛逼Class!


遍地开花的多级缓存


对,对,还是上次的那个首席架构师,他除了爱用消息队列外,还特别喜欢用缓存,而且是Guava Cache + Redis的多级缓存。



据同事说,这种多级缓存策略在这位首席架构师的熏陶下,已经遍布了OA系统、公司官网、消息中心、结算系统、供应链系统、CRM系统。


首席架构师说:“缓存不仅能提升性能,还能帮助数据库抗压,提升系统可用性,绝对是个好东西,应该多用一用。”


然后,公司的系统就经常发生多种缓存的数据与数据库的数据一致性问题。


首席架构师又说:“任何架构都是有利有弊的,但只要利大于弊就好,不要太在意。”


设计模式的流毒


记得我刚上班不久,组内有一个架构师同事,写的代码巨复杂,各种技巧、设计模式、高级语法满天飞,还沾沾自喜的给我们炫耀。



一次Code Review的时候,我嘴欠问他这里咋这么设计,他就鄙视的说:“你连这个都不知道,这是设计模式中的建造者模式啊。”



当时觉得他好牛逼,而我好low。


以后,每次进行Code Review,只要看到其他同事代码里有几个if else,架构师同事就质问道:“为什么不用策略模式优化if else?”


当然,还有其他的质问,类似于:这块为什么不用抽象工厂模式?这块为什么不用代理模式?这块为什么不用观察者模式?


后来我们就给他起了个外号,叫“设模”(se mo)。


多多益善的复杂关系


前面说的那些架构师们,他们过度设计所带来的后果是浪费服务器和研发资源,但架构师老邓不一样,他的过度设计是浪费表。


之前见过某在线教育公司设计的表结构,基本上所有表之间的外键关系都是按照多对多方式设计的,也就是加一个中间的关系映射表。



有的我是可以理解的,比如:



  • 一个学生会出现在多个不同的班级里,而一个班级里也会有不同的学生;

  • 一个学生可以学习多门课程,而每门课程又会对多个学生进行学习;

  • 一个学生可以上多个老师的课,而一个老师又可以教多个学生;


但是,但是。



  • 一个学生可以有多个考试成绩,难道一个考试成绩还能属于多个学生吗?

  • 一个学生有多个课程的课时余额,难道一个课时余额还能属于多个学生吗?


老邓说:“万一以后业务变化了呢?一切皆有可能啊。”


数据库的可移植性


还在上大学的时候,在CSDN上看某著名架构师在极力强调数据库的可移植性。



我记得当时的原话大概是:



  • Hibernate的HQL可以帮我们保证不同数据库之间的移植性,比如:MySQL中的limit和Oracle中的rownum。

  • 为什么不能写存储过程?一个重要的原因就是业务逻辑放到数据库里会导致数据库移植成本变大。

  • 程序内尽量采用标准SQL语法,因为我们要考虑将来的移植风险。


当时听了,觉得这个大架构师简直就是YYDS。然后我工作了这么多年,也没遇到过一次数据库移植。


无间道版的数据校验


我厂某团队的架构师老李素以严谨著称,其经常放在嘴边的一句话就是:“工程师不仅仅是一项有创造性的职业,也是一门严谨审慎的职业。”


这话说的确实没毛病,我也看过他们团队的工程代码,程序的边界处理、异常处理和容错处理做得都特别好,入参校验也是特别细致入微。


就像老李所说的那样:“All input is evil。”


不,等等,入参校验没问题,但怎么从数据库里读出来的数据,为什么还要再校验一遍?难道不是在写入的时候校验吗?


老李面无表情地说:“如果数据库中的数据,并没有经过应用程序处理,而是不知道被谁直接改库了呢?”


卧槽,这是泥马数据校验无间道版吗?



疯魔成活的配置化


还是上面的那个架构师老李,他要求团队代码中带数字的地方,全部走配置中心,这样可以不发布代码就直接进行修改。



然后,我就看到了这样的现象:



  • 如果某个HashMap的size大于0,则进行xxxx,这个0写到了配置中心里。

  • 如果用户性别等于1(男性),则进行男装推荐,这个1写到了配置中心里。

  • 如果商品状态等于2(已下线),则进行xxxx,这个2写到了配置中心里。


配置中心啊,你的责任真的好重大。


总结


遇到这种类型的架构师,真的特别想把他们祭天了,因为我是Kiss原则的忠实拥趸。



Keep it simple,stupid,即:保持简单、愚蠢。


保持简单就能让系统运行更好,更容易维护扩展,越是资深的人,越明白这个道理。


作者:库森学长
来源:juejin.cn/post/7287144182967107638
收起阅读 »

职场坐冷板凳的那些日子

曾经有一段职场生涯,坐了很长时间的冷板凳,也正是那段经历,彻底改变了整个职场生涯。今天这篇文章聊聊自己曾经的经历,也聊聊如果在职场中被坐了冷板凳该咋办。 关于冷板凳 有人的地方就有江湖。而这个江湖中是否性情相同,是否因某些事(或利益)产生矛盾,都可能造成职场坐...
继续阅读 »

曾经有一段职场生涯,坐了很长时间的冷板凳,也正是那段经历,彻底改变了整个职场生涯。今天这篇文章聊聊自己曾经的经历,也聊聊如果在职场中被坐了冷板凳该咋办。


关于冷板凳


有人的地方就有江湖。而这个江湖中是否性情相同,是否因某些事(或利益)产生矛盾,都可能造成职场坐冷板凳的情况。


冷板凳常见于上级对下级的打压。一般手段就是让你无所事事或安排一些边缘性的事务,不怎么搭理你,从团队层面排挤你,甚至否定你或PUA你,别人也不敢跟你沟通,以至于让你在团队中形成孤立的的状态。


根据矛盾或冲突的不同,冷板凳的程度也不同。常见的有:浅层次的冲突,可进行修复;不可调和,无法修复;中间的灰度状态。


通常根据具体情况,判断程度,有没有可能或必要修复,再决定下一步的行动。


第一,可修复的冷板凳


有很多同学,特别是技术人,在职场上有时候特别的“刚”,为了某个技术点跟领导争的面红耳赤的,导致被坐冷板凳。


比如有同学曾有这样的经历:领导已经拍板的决定,他很刚的去跟领导据理力争,导致起了冲突,大吵一架,领导也下不来台。随后领导好几天没搭理他。


针对这种情况,一般也就是一顿火锅的事,找领导主动沟通,重拾信任。甚至可能会出现不打不相识的情况。当然,一顿火锅不够还可以两顿。


第二,清场性质的冷板凳


这种情况常见于业绩或能力不达标,已经是深层次的矛盾,一般会空降过来一个领导,故意将其边缘化。属于清场接替工作性质的,基本上无法修复。


针对这种情况,看清局势,准备好找下家就是了。如果做得好,准备好交接工作,给彼此一个体面。毕竟,很多事情我们是无法改变的。


第三,灰度状态的冷板凳


以上两个常见都比较极端,而大多数情况下都是灰度状态的,大的可能性就是一直僵持着。这时作为下属的人,一般建议主动去沟通、修复。


如果阅历比较浅,看不出中间的微妙关系以及深层次的冲突点,就请人帮你看看,听听别人的建议和决策。再决定值不值得修复,要不要修复。


我的冷板凳


曾经我在一家公司坐的冷板凳属于第三种,但却把这个冷板凳坐到了极致。下面就讲讲我曾经的故事。


跟着一个领导到一家新公司,本来领导带领技术部门的,但由于内部斗争的失利,去带产品团队了,而我也归属到他对手的手下了。这种情况下,冷板凳是坐定了,但也不至于走人。


被新领导安排了一个很边缘的业务:对接和维护一套三方的系统。基本上处于不管不问,开会不带,接触不到核心,也与其他人无交流的状态。起初这种状态非常难受,人毕竟是社群动物,需要一个归属感和存在感的。


但慢慢的,自己找到了一些属于自己的乐趣。


首先,没人管没人问,那就可以自己掌控节奏和状态了。看他们天天加班到凌晨一两点,而自己没人管,六七点就下班了。最起码在那段持续疯狂加班的岁月里,自己保住了头发。那位大领导后来加班太多,得了重病,最终位置也没保住。


其次,有了大把的时间。上班几乎没人安排工作,于是上班的时间完全自己安排。三方服务商安排了对接人,好歹自己作为甲方,于是天天就跟服务商的技术沟通,询问他们系统的设计实现,技术栈什么的。


在那段岁月里,完成了几个改变后续职场生涯的事项。


事项一:那时Spring Boot 1.5刚刚发布,公司的技术栈还没用上,但服务商的这套系统已经用上了。感觉这玩意太好用了,于是疯狂的学学习。因为当初的学习,后来出版了书籍《Spring Boot技术内幕》那本书。


事项二:写技术博客,翻译技术文档,录技术视频。服务商的系统中还用到了规则引擎,当时市面上没有相关的中文资料。于是边跟对方技术沟通,边翻译英文文档,写博客。后来,还把整理的文档录制成视频,视频收入有几万块吧。


这算是自己第一次尝试翻译文档、录制教学视频,而且这个领域网络上后续的很多技术文章都是基于我当初写文章衍生出来的。最近,写的第二本书便是关于规则引擎的,坐等出版了。


事项三:学习新技术,博客输出。当时区块链正火爆时。由于有大量的时间,于是就研究起来了,边研究边写技术博客。也是在这个阶段,养成了写技术博客的习惯。


因为区块链的博客,也找到了下家工作。同时写了CSDN当时类似极客时间的“Chat”专栏,而且是首批作者。也尝试搞了区块链的知识星球。后来,因为区块链的工作,做了第一次公开课的分享。还是因为区块链相关,与别人合著了一本书,解释了出版社的老师,这也是走上出书之路的开始。


因为这次冷板凳,让职场生涯变得极其丰富,也扭转了大的方向,发展了副业,接触了不同行业领域的人。


最后的小结


在职场混,遇到坐冷板凳的情况不可避免,但如何化解,如何抉择却是一个大学问。尽量主动沟通,毕竟找工作并不容易,也不能保证下家会更好。同时,解决问题,也是人生成长的一部分,所以,尽量尝试化解。


但如果矛盾真的不可调和或持续僵持,那么就更好做好决策,选择对自己最有利的一面。


曾在朋友圈发过这样一段话,拿来与大家分享:


“始终难守樊登讲过的一句话:人生成长最有效的方法,就是无论命运把你抛在任何一个点上,你就地展开做力所能及的事情。


如果还要加上一句,那就是:还要占领制高点。与君共勉~”


作者:程序新视界
来源:juejin.cn/post/7267107420655583292
收起阅读 »

和AI网聊10分钟被骗430万,真实诈骗案震惊全网,官方:AI诈骗成功率接近100%

防不胜防,10分钟就被AI骗走430万! 这是这两天震惊全网的真实诈骗案件。 据包头警方发布,一公司老板接到朋友的微信视频电话,由于长相和声音确认都是“本人”,他丝毫没有怀疑就把钱打了过去。 结果一问朋友,对方根本不知道此事。这人才知道,原来诈骗者DeepF...
继续阅读 »
防不胜防,10分钟就被AI骗走430万!

这是这两天震惊全网的真实诈骗案件。


据包头警方发布,一公司老板接到朋友的微信视频电话,由于长相和声音确认都是“本人”,他丝毫没有怀疑就把钱打了过去。


图片


结果一问朋友,对方根本不知道此事。这人才知道,原来诈骗者DeepFake了他朋友的面部和声音。


消息一出,直接冲上热搜第一。网友们纷纷表示:离大谱啊!不敢接电话了。


图片


也有人提出质疑:AI这么好训练?这需要掌握个人的大量信息吧。


图片


不过,虽说是看上去离谱的小概率事件,但据相关统计,AI技术新骗局来袭后,诈骗成功率竟接近100%


图片


毕竟连那些直播卖货“杨幂”“迪丽热巴”、B站歌手“孙燕姿”“林俊杰”都不是真的。


图片

图源抖音@娱乐日爆社,疑似直播间用杨幂的AI换脸带货

10分钟被AI骗走430万


据微信号平安包头介绍,一个月前,福州市一科技公司法人代表郭某,突然接到好友的微信视频。


聊天过程中,这个“好友”透露,自己朋友在外地投标,需要430万保证金,且公对公账户过账,所以想用郭某公司的账户走一下账。


背景介绍之后,“好友”就找郭某要了银行卡号,而后甩出一张银行转账底单的截图告诉郭某,已经把钱打到了郭某的账户上。


结果因为已经视频聊天以及各种“物证”,郭某并没有过多怀疑,甚至也没去核实钱款是否到账。


几分钟之后郭某就分两笔将钱打了个过去,本想去跟好友报备一下:“事情已经办妥”。


然而,好友缓缓打出来一个问号。


图片


好在郭某反应比较快,第一时间报警。于是在警方和银行联动下,仅用时10分钟就成功拦截了330多万元被骗资金。


有网友表示,AI正成为骗子高手新一代工具。


图片


还有网友调侃道,我没钱,哪个都骗不了我。(等等,好友有钱也不行doge)


图片


而在这起案件背后,核心涉及了AI换脸以及语音合成这两个技术。


大众所熟知的AI换脸方面,现在即便一张2D照片,就能让口型动起来。据此前新华每日电讯消息,合成一个动态视频,成本也仅在2元至10元。


当时涉案嫌疑人表示:“客户”往往是成百上千购买,牟利空间巨大。


至于面向更精准、更实时的大型直播平台,视频实时换脸的全套模型购买价格在3.5万元,而且不存在延迟、也不会有bug。


至于像语音合成方面,技术效果也是越来越逼真,新模型和开源项目涌现。


前段时间,微软新模型VALL·E炸场学术圈:只需3秒,就可以复制任何人的声音,甚至连环境背景音都能模仿。


而具备语音合成功能的工具Bark,更是曾登顶GitHub榜首,除了声色接近真人,还能加入背景噪音和高度拟真的笑声、叹息和哭泣声等。


在各类社交网络上,各种小白教程也层出不穷。


图片


要是结合虚拟摄像头,可能就更加防不胜防。


只需一个软件应用程序,就可以在视频通话中使用任意视频资源。


图片

图源微博@哑巴

点击接通后,对方完全不会看到播放、暂停视频等具体操作,只会看到视频播放的效果,“接通后看到的就是美女了”:


图片

图源微博@哑巴

这样一来,不仅视频可以通过虚拟摄像头,随意拍摄甚至更换,甚至连说话方式都可以真人定制:


图片

图源微博@哑巴

核心技术门槛的降低,也就给了犯罪分子可乘之机。


AI新骗局成功率接近100%


事实上,AI加持下的新型网络诈骗,并不止这一种操作。


无论是国内还是国外,都有不少用AI换脸的诈骗案例,小到在网络购物、兼职刷单等方面骗点小钱,大到冒充客服、投资理财人员等身份,获取银行卡账号密码直接转一大笔账,都有出现。


在国内,据南京警方消息,此前就出现过一起被QQ视频AI诈骗3000元的案例。


当事人小李表示,自己的大学同学小王通过QQ跟自己借3800元,称自己很着急,因为表姐住院了。


小李怀疑了小王的身份,而小王很快给她传来了一个4~5秒左右的动态QQ视频,不仅背景在医院,而且还打了声招呼。


这让小李打消了疑虑并转了3000元,然而随后发现对方已经将她删除拉黑,发现视频原来是AI伪造的。


图片


目前包括北京反诈、武汉市反电信网络诈骗中心等官方公众号平台,都警告了AI技术新骗局的严重性,甚至表示“诈骗成功率接近100%”。


图片


可别以为这些诈骗现象只在国内出现,国外的语音诈骗同样花样百出。


一种方式是用AI合成语音骗取电话转账


据Gizmodo介绍,英国最近就同样发生了一起涉及金额高达22万英镑(折合人民币约192.4万元)的诈骗案件。


一位当地能源公司的CEO,在不知情的情况下被骗子“DeepFake”了自己的声音。随后,骗子用他这段声音,电话转账了22万英镑到自己的匈牙利账户。


据CEO表示,他后来自己听到这段AI合成语音时都惊讶了,因为这段语音不仅模仿出了他平时说话的语调,甚至还带有一点他的口癖,有点像是某种“微妙的德国口音”。


另一种则是用合成语音冒充亲友身份


据nbc15报道,美国一位名叫Jennifer DeStefano的母亲,最近接到了一个自称是“绑匪”的诈骗电话,对方称劫持了她15岁的女儿,要求这位母亲交出100万美元的赎金。


电话那头传来了女儿的“呼救声”,不仅声音、就连哭声都非常相似。幸运的是她的丈夫及时证明了女儿是安全的,这次诈骗才没能成功。


现在,不仅是诈骗,在AI技术加持下,就连杨幂、迪丽热巴们都在今天引发了热议。


图片


原来,这是商家们想出的新“搞钱之道”,那就是在直播的时候,用AI换脸等技术“Deepfake”一下杨幂、迪丽热巴、Angelababy等明星的脸,这样大家就会误以为是明星本人在带货,从而拉升直播流量。


然而,这类行为目前还不能被直接判定为侵权。据21世纪经济报道,北京云嘉律师事务所律师赵占领表示:



平台对于平台内商家的侵权行为不承担直接侵权的责任,而是否构成帮助侵权,主要是看平台对于商家的侵权行为是否属于明知或应知。


但对于如何判定平台对用户、权利人的投诉是否明知或应知,在一般情况下很难认定。



图片


显然,在AI技术越来越火热的当下,相关法律也还需要进一步完善。


One More Thing


就在昨天夜里,最近火爆全网的“AI孙燕姿”本人,也就是歌手孙燕姿,出来回应了。


她发布了一篇名为《我的AI》的英文版文章,这是她团队的中文翻译全文:


图片


然后,网友们看到后的评论是酱婶的:


图片

图源微信@南方都市报

参考链接:

[1]mp.weixin.qq.com/s/Ije3MyQxN…

[2]mp.weixin.qq.com/s/kcbNlaFe_…

[3]gizmodo.com/deepfake-ai…

[4]http://www.nbc15.com/2023/04/10/…

[5]weibo.com/1796087453/…

[6]weibo.com/1420862042/…


—  —


作者:量子位
来源:juejin.cn/post/7236935835631190077
收起阅读 »

前端调取摄像头并实现拍照功能

web
前言: 最近接到的一个需求十分有意思,设计整体实现了前端仿微信扫一扫的功能。整理了一下思路,做一个分享。 tips: 如果想要实现完整扫一扫的功能,你需要掌握一些前置知识,这次我们先讲如何实现拍照并且保存的功能。 一. window.navigator 你...
继续阅读 »

前言: 最近接到的一个需求十分有意思,设计整体实现了前端仿微信扫一扫的功能。整理了一下思路,做一个分享。


tips: 如果想要实现完整扫一扫的功能,你需要掌握一些前置知识,这次我们先讲如何实现拍照并且保存的功能。


一. window.navigator




  1. 你想调取手机的摄像头,首先你得先检验当前设备是否有摄像设备,window 身上自带了一个 navigator 属性,这个对象有一个叫做 mediaDevices 的属性是我们即将用到的。




  2. 于是我们就可以先设计一个叫做 checkCamera 的函数,用来在页面刚开始加载的时候执行。。

    image.png




  3. 我们先看一下这个对象有哪些方法,你也许会看到下面的场景,会发现这个属性身上只有一个值为 nullondevicechange 属性,不要怕,真正要用的方法其实在它的原型身上。
    image.png




  4. 让我们点开它的原型属性,注意下面这两个方法,这是我们本章节的主角。

    image.png




  5. 我们到这一步只是需要判断当前设备是否有摄像头,我们先调取 enumerateDevices 函数来查看当前媒体设备是否存在。它的返回值是一个 promise 类型,我们直接用 asyncawait 来简化一下代码。
    image.png
    image.png
    从上图可以看出,我的电脑有两个音频设备和一个视频设备,那么我们就可以放下进行下一步了。




二. 获取摄像头




  1. 接下来就需要用到上面提到的第二个函数,navigator.getUserMedia。这个函数接收一个对象作为参数,这个对象可以预设一些值,来作为我们请求摄像头的一些参数。




  2. 这里我们的重点是 facingMode 这个属性,因为我们扫一扫一般都是后置摄像头
    image.png
    当你执行了这个函数以后,你会看到浏览器有如下提示:

    image.png




  3. 于是你高兴的点击了允许,却发现页面没有任何变化。

    image.png




  4. 这里你需要知道,这个函数只是返回了一个媒体流信息给你,你可以这样简单理解刚刚我们干了什么,首先浏览器向手机申请我想用一下摄像头可以吗?在得到了你本人的确认以后,手机将摄像头的数据线递给了浏览器,:“诺,给你。”




  5. 浏览器现在仅仅拿到了一根数据线,然而浏览器不知道需要将这个摄像头渲染到哪里,它不可能自动帮你接上这根线,你需要自己找地方接上这根数据线。




  6. 这里不卖关子,我们需要请到我们的 Video 标签。我没听错吧?那个播放视频的 video 标签?没错,就是原生的 video 标签。




  7. 这里创建一个 video 标签,然后打上 ref 来获取这个元素。

    image.png




  8. 这里的关键点在于将流数据赋值给 video 标签的 srcObject 属性。就好像你拿到了数据线,插到了显示器上。

    (tips: 这里需要特别注意,不是 video.src 而是 video.srcObject 请务必注意)

    image.png




  9. 现在你应该会看到摄像头已经在屏幕上展示了,这里是我用电脑前置摄像头录制的一段视频做成了gif。(脉动请给我打钱,哼)
    camera.gif




三. 截取当前画面




  1. 这里我随手写了一个按钮当作拍摄键,接下来我们将实现点击这个按钮截取当前画面。

    image.png




  2. 这里你需要知道一个前提,虽然我们现在看到的视频是连贯的,但其实在浏览器渲染的时候,它其实是一帧一帧渲染的。就像宫崎骏有些动漫一样,是一张一张手写画。




  3. 让我们打开 Performance 标签卡,记录一下打开掘金首页的过程,可以看到浏览器的整个渲染过程其实也是一帧一帧拼接到一起,才完成了整个页面的渲染。

    11.gif




  4. 知道了这个前提,那么举一反三,我们就可以明白,虽然我们现在已经打开了摄像头,看到的视频好像是在连贯展示,但其实它也是一帧一帧拼到一起的。那现在我们要做的事情就非常明了,当我按下按钮的时候,想办法将 video 标签当前的画面保存下来。




  5. 这里不是特别容易想到,我就直接说答案了,在这个场景,我们需要用到 canvas 的一些能力。不要害怕,我目前对 canvas 的使用也不是特别熟练,今天也不会用到特别复杂的功能。




  6. 首先创建一个空白的 canvas 元素,元素的宽高设置为和 video 标签一致。

    image.png




  7. 接下来是重点: 我们需要用到 canvasgetContext 方法,先别着急头晕,这里你只需要知道,它接受一个字符串 "2d" 作为参数就行了,它会把这个画布的上下文返回给你。
    tips 如果这里还不清楚上下文的概念,也不用担心,这里你就简单理解为把这个 canvas 这个元素加工了一下,帮你在它身上添加了一些新的方法而已。)
    image.png




  8. 在这个 ctx 对象身上,我们只需要用到一个 drawImage 方法即可,不需要关心其它属性。

    image.png




  9. 感觉参数有点多?没关系,我们再精简一下,我们只需要考虑第二个用法,也就是5参数的写法。(sx,sy 是做裁切用到的,本文用不到,感兴趣可以自行了解。)

    image.png




  10. 这里先简单解释一下 dxdy 是什么意思。在 canvas 里也存在一个看不见的坐标系,起点也是左上角。设想你想在一个 HTMLbody 元素里写一个距离左边距离 100px 距离顶部 100px的画面,是不是得写 margin-left:100px margin-top:100px 这样的代码?没错,这里的 dydx 也是同样的道理。

    image.png




  11. 我们再看 dwidth,和 dheight,从这个名字你就能才出来,肯定和我们将要在画笔里画画的元素的宽度和高度有关,是的,你猜的没错,它就好像你设置一个 div 元素的高度和宽度一样,代表着你将在画布上画的截图的宽高属性。




  12. 现在只剩下第一个参数还没解释,这里直接说答案,我们可以直接将 video 标签填进去,ctx 会自动将当前 video 标签的这一帧画面填写进去。现在按钮的代码应该是这个样子。


    function shoot() {
    if (!videoEl.value || !wrapper.value) return;
    const canvas = document.createElement("canvas");
    canvas.width = videoEl.value.videoWidth;
    canvas.height = videoEl.value.videoHeight;
    //拿到 canvas 上下文对象
    const ctx = canvas.getContext("2d");
    ctx?.drawImage(videoEl.value, 0, 0, canvas.width, canvas.height);
    wrapper.value.appendChild(canvas);//将 canvas 投到页面上
    }



  13. 测试一下效果。

    112.gif




四. 源码


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

const wrapper = ref<HTMLDivElement>();
const videoEl = ref<HTMLVideoElement>();

async function checkCamera() {
const navigator = window.navigator.mediaDevices;
const devices = await navigator.enumerateDevices();
if (devices) {
const stream = await navigator.getUserMedia({
audio: false,
video: {
width: 300,
height: 300,
// facingMode: { exact: "environment" }, //强制后置摄像头
facingMode: "user", //前置摄像头
},
});
if (!videoEl.value) return;

videoEl.value.srcObject = stream;
videoEl.value.play();
}
}

function shoot() {
if (!videoEl.value || !wrapper.value) return;
const canvas = document.createElement("canvas");
canvas.width = videoEl.value.videoWidth;
canvas.height = videoEl.value.videoHeight;
//拿到 canvas 上下文对象
const ctx = canvas.getContext("2d");
ctx?.drawImage(videoEl.value, 0, 0, canvas.width, canvas.height);
wrapper.value.appendChild(canvas);
}

onMounted(() => {
checkCamera();
});
</script>
<template>
<div ref="wrapper" class="w-full h-full bg-red flex flex-col items-center">
<video ref="videoEl" />
<div
@click="shoot"
class="w-100px leading-100px text-center bg-black text-30px"
>
拍摄
</div>
</div>
</template>



五. 总结


实现拍照的整体思路其实很简单,仅仅需要了解到视频其实也是一帧一帧画面构成的,而 canvas 恰好有捕捉当前帧的能力。


预告:在下一篇会讲解如何实现扫一扫的功能,需要用到插件,感兴趣的同学可以先预习一下功课。🎁二维码扫码插件


趁热打铁🧭:前端实现微信扫一扫的思路


作者:韩振方
来源:juejin.cn/post/7289662055183597603
收起阅读 »

前端实现微信扫一扫的思路

web
前言: 在有了获取手机摄像头权限并且记录当前画面的前置知识以后,我们现在已经可以进行下一步实现一个仿微信扫一扫的功能了。 tips: 如果你是直接看的本文,对如何打开摄像头拍照这个功能还不太熟悉,请移步 🎁前端如何打开摄像头拍照。这是你阅读本篇的必需...
继续阅读 »

前言: 在有了获取手机摄像头权限并且记录当前画面的前置知识以后,我们现在已经可以进行下一步实现一个仿微信扫一扫的功能了。


tips: 如果你是直接看的本文,对如何打开摄像头拍照这个功能还不太熟悉,请移步 🎁前端如何打开摄像头拍照。这是你阅读本篇的必需前置知识。


一. 效果预览


这里先简单放一下整体界面效果,接下来带大家一步一步分析其中的功能如何实现。


2.gif


本篇将重点讲解多张二维码识别的处理场景。


二. 简单了解二维码




  1. 现在流行使用的二维码是 qrcode,其中 qr 两个字母其实就是 quick response 的缩写,简单来说就是快速响应的意思。三个角用来定位,黑点表示二进制的1,白色点代表0。(这里感兴趣可以自行了解) 它的本质其实就是将一个地址链接利用某种约定好的规则隐藏到一个图片当中,

    image.png




  2. 我们可以利用 chrome 自带的创建当前网站二维码的功能快速体验一下。
    qr.gif




  3. 你可以用手机自带的二维码扫码软件扫一下这个二维码,它将会将你引导到我掘金的个人主页。

    qrcode_juejin.cn.png




  4. 细心的你可能会发现二维码下面已经给你提示了你准备保存的链接地址,现在你观察一下浏览器地址栏是否正对应下面这个地址呢?
    image.png




三. 实现扫码本地图片功能




  1. 我们不需要深入了解这个二维码的转换规则,我们可以直接选用现有的插件即可完成识别功能。 这里我们选用 antfu 大佬的轮子。这里我们不过多介绍,你只需要它可以识别出图片中的二维码即可。如果感兴趣,这是具体仓库地址 qr-sanner-wechat




  2. 首先安装 npm i qr-scanner-wechat




  3. 它的使用方法也十分简单,这个依赖导出了一个方法,我们直接引入这个方法即 import { scan } from 'qr-scanner-wechat




  4. 这个函数可以接收一个 image 元素或者 canvas 元素作为参数,并且返回一个 promise 类型的值。




  5. 我们先来测试最简单的,传入一个 image 元素,利用 input 标签的 type=file 属性,就可以从本地选择图片,比较基础的知识,不过多赘述,代码如下。


    function getImageFromLocal(e: Event) {
    const inputEl = e.target as HTMLInputElement;
    if (!inputEl) return;
    console.log("inputEl.files", inputEl.files);
    }



    然后我们可以通过 input 元素绑定的 onChange 回调中拿到 input 元素身上的 files 属性,就可以获取到刚刚我们选择的文件信息。

    ee.gif




  6. 但是目前这个数据对象我们还无法使用,需要借助 URL.createObjectUrl 方法来创建一个普通的 url 地址。

    image.png




  7. 当拿到这个 url 地址以后该如何使用呢?🤔
    image.png




  8. 一个熟悉的老朋友,有请 img 标签出场,👏,我们只需要将 img 标签的 src 替换成刚刚保存的 url 地址即可。

    image.png

    现在整体效果应该是这样的:

    code.gif




  9. 有了 img 元素,我们直接将这个元素赋值给 qr-scanner-wechat 插件提供的 scan 函数即可。

    image.png




  10. 我们来测试一下整体流程。

    qw.gif




  11. 可以看到,scan 函数返回了一个对象,这个对象身上有两个十分重要的属性。一个叫做 rect (rectangle 长方形的单词缩写),这个属性描述了这个插件在什么位置扫描到了二维码,另外一个属性就是 text,也就是这个图片上隐藏的字符串地址

    image.png




  12. 这里我们再讲解一下 rect 属性,因为后面的功能需要你对这个属性有比较清晰的理解。我们对比一个现实世界的例子。当你掏出手机扫描二维码的时候,往往并不会正好对准一个二维码的图片,或者会遇到一个图片中存在两个二维码的情况,如下图:

    image.png




  13. 这个 qr-scanner 插件会帮你把二维码所在整张图片的相对位置告诉你,因为这个插件每次调用 scan 函数只会返回一次结果。并不是说图片上有两个二维码,它的识别结果就会有两个,所以说这个 qr-scanner 插件的识别效果也并不是百分之一百准确的。




四. 理清思路




  1. 说了这么多,那么这个 rect 我们该如何利用起来呢?别着急,我们先理清思路再动手写代码,到了目前这一步会出现两种情况。

    image.png




  2. 第一种是图片压根就没有二维码,这个简单,提示用户重新放置图片即可。




  3. 关键点就在于第二张情况,当图片上扫码到存在一个二维码后,我们该如何判断是否存在第二个或多个维码呢?

    image.png




  4. 我们看一下微信的实现效果,当只有一张二维码的时候,它会直接跳转,当有多个二维码的时候,它会将整个页面暂停,并且提示用户有两张二维码,请点击选择一个进行跳转。

    image.png




  5. 但是我们上面提到了,scan 函数每次只会扫描一次图片,返回一个识别结果,它并不能准确知道图片上到底有几个二维码。那放到现实生活我们会怎么做呢?




  6. 举个例子,假如我们现在掏出手机扫一扫的功能,现在给你的图片上有两个二维码,但是我明确的知道我就想扫第二个,你会怎么做?

    image.png




  7. 这不是很简单的道理吗?我拿手挡住第一个二维码不就可以了吗?

    image.png




  8. 那么利用同样的思路,我们可以再扫描到一张二维码的时候,想办法把当前识别到的这个二维码位置给遮挡住,然后将被遮挡后的照片传递给 scan 函数再次扫描。




  9. 那么整个过程就是,我们首先将完整的照片传给 scan,然后 scan 觉得第一张二维码比较帅,就先识别了它。(tips: 这里需要提醒一下,scan 有时候会觉得第二张二维码比较帅,那我就识别第二张二维码,要注意的它的顺序性是随机的)

    image.png




  10. 然后我们想办法盖上遮挡物,然后将这个图片传给 scan,让它再次确认是否有第二个二维码。

    image.png




  11. 在哪覆盖?还记不记 rect 属性保留有这个二维码的位置信息?现在的问题就转变为如何覆盖了?




  12. 这里需要用到 canvas 元素的一丢丢基础知识,这是 mdn canvas 基础知识的介绍,十分简单的就画出了一个绿色长方体。

    image.png

    ctx.filleRect可以接收四个参数,分别是相对于画布起始轴的 xy 的距离。

    简单来讲就可以理解为每一个 canvas 就相当于一个独立的 HTML 文件,也有自己的独立坐标系系统,x,y 就相当于 margin,至于后面两个参数,其实就代表着你要画的长方形宽度高度
    image.png




13.那这不巧了吗,scan 的返回值 rect 恰好就有这几个值。

image.png



  1. 话不多说,马上开始实践。⛽️


五. 处理存在多张二维码的图片




  1. 注意: 以下内容我统一选用从本地照片上传作为演示,从摄像头获取图片是同样的道理,详细介绍请移步 🎁前端如何打开摄像头拍照。在下面的讲解过程,我会默认你已经阅读了前置知识。




  2. 这里我就继续沿用之前提到的图片,我将他们拼接到了一张图片上。

    二.png




  3. 下面应该是你目前从本地选择二维码图片识别的代码。


    async function getImageFromLocal(e: Event) {
    const inputEl = e.target as HTMLInputElement;
    if (!inputEl) return;
    if (!inputEl.files?.length) return;
    const image = inputEl.files[0];
    const url = URL.createObjectURL(image);
    src.value = url;
    const result = await scan(imgEl.value!);
    console.log("result", result);
    }



  4. 接下来我们需要先创建一个 canvas 来将当前的照片拷贝到画布上,然后准备利用得到的 rect 信息在这个 canvas 元素上绘画。

    image.png




  5. 为了方便展示,我调用 appendChildren 方法将这个 canvas 元素打印到了界面上。

    1.gif




  6. 然后用 resultrect坐标宽度信息,去调用我们提到的 canvasfillStyle fillRect 方法。

    image.png

    下面是目前实现的效果:

    1.gif




  7. 注意scan 函数不仅仅可以接受 imgElment 作为扫描的参数,它还可以接受 canvas 元素作为扫描的参数。聪明的你看到这里,或许已经猜到我们下一步准备做什么了。




  8. 那么此时我们就可以将这个已经被黑色涂鸦覆盖过的 canvas 进行二次扫描。(暂时不要考虑代码的优雅性,这里只是更清晰的说明我们在干什么,之后我们会封装几个方法,然后整理一下代码)

    image.png

    让我们再看一下效果:

    2.gif




  9. 通过多次重复上面的操作,就可以将图片上所有的二维码都尽量识别出来。

    image.png

    现在实现的效果:

    11.gif

    同时图片上相对应的识别内容也全都被正确的被获取到了。

    image.png




  10. 此时我们创建一个 Map 来保存这些数据。Mapkey 就是 text ,对应的 value 就是 rect 坐标信息。

    image.png

    image.png




六. 弹出可以点击的小蓝块




  1. 有了坐标信息和位置信息,并且我们的 canvasimg 元素的坐标轴系统是一一对应的,那么我们就可以写一个函数来遍历这个 resultMap,然后根据位置信息在 img 元素所在的 div 上打印出我们想要的样式。




  2. 首先在 img 元素外面包一层 div,打上 ref 叫做 imgWrapper 。因为之后我们要用它当作小蓝块的定位元素,所以先设置 position:relative

    image.png




  3. 绘画代码如下,都是基础的方法,不再过多赘述。


    //多个二维码时添加动态小蓝点
    function draw() {
    resultMap.forEach((rect, link) => {
    if (!imgWrapper.value) return;
    const dom = document.createElement("div");
    const { x, y, width, height } = rect;
    const _x = (x || 0) + width / 2 - 20;
    const _y = (y || 0) + height / 2 - 20;
    dom.className = "blue-chunk";
    dom.style.width = "40px";
    dom.style.height = "40px";
    dom.style.background = "#2ec1cc";
    dom.style.position = "absolute";
    dom.style.zIndex = "9999999";
    dom.style.top = _y + "px";
    dom.style.left = _x + "px";
    dom.style.color = "#fff";
    dom.style.textAlign = "center";
    dom.style.borderRadius = "100px";
    dom.style.borderBlockColor = "#fff";
    dom.style.borderColor = "unset";
    dom.style.borderRightStyle = "solid";
    dom.style.borderWidth = "3px";
    dom.style.animation = "scale-animation 2s infinite";
    dom.addEventListener("click", () => {
    console.log(link);
    });
    imgWrapper.value.appendChild(dom);
    });
    }



  4. 然后再 for 循环以后开始绘画小蓝块。

    image.png




  5. 让我们预览一下现在的效果:

    112.gif




  6. 让我们测试一下相对应的点击事件

    3.gif




七. 源码





八.总结


本篇文章的关键点就是讲解了我在实现处理多张二维码的场景时的思路,利用 canvas 遮挡识别过的二维码这个思路是pbk-bin大佬最先想到的,在实现这个需求以后还是很感叹这个思路的巧妙。👏


再次特别感谢pbk-bin🎁~


如果文章对你有帮助,不妨赠人玫瑰,手有余香,预计将会在下篇更新较为完整的微信扫一扫界面和功能。


作者:韩振方
来源:juejin.cn/post/7290813210276724771
收起阅读 »

看了我项目中购物车、订单、支付一整套设计,同事也开始悄悄模仿了...

在我的mall电商实战项目中,有着从商品加入购物车到订单支付成功的一整套功能,这套功能的设计与实现对于有购物需求的网站来说,应该是一套通用设计了。今天给大家介绍下这套功能设计,涵盖购物车、生成确认单、生成订单、取消订单以及支付成功回调等内容,希望对大家有所帮助...
继续阅读 »

在我的mall电商实战项目中,有着从商品加入购物车到订单支付成功的一整套功能,这套功能的设计与实现对于有购物需求的网站来说,应该是一套通用设计了。今天给大家介绍下这套功能设计,涵盖购物车、生成确认单、生成订单、取消订单以及支付成功回调等内容,希望对大家有所帮助!



mall项目简介


这里还是简单介绍下mall项目吧,mall项目是一套基于 SpringBoot + Vue + uni-app 的电商系统(Github标星60K),采用Docker容器化部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能,功能很强大!



后台管理系统演示



前台商城项目演示



功能设计



这里介绍下从商品加入购物车到订单支付成功的整个流程,涵盖流程的示意图和效果图。



流程示意图


以下是从商品加入购物车到订单支付成功的流程图。



流程效果图


以下是从商品加入购物车到订单支付成功的效果图,可以对照上面的流程示意图查看。



数据库设计


为了支持以上购物流程,整个订单模块的数据库设计如下。



设计要点



接下来介绍下整个购物流程中的一些设计要点,涵盖加入购物车、生成确认单、生成订单、取消订单以及支付成功回调等内容。



加入购物车


功能逻辑


用户将商品加入购物车后,可以在购物车中查看到商品。购物车的主要功能就是存储用户选择的商品信息及计算购物车中商品的优惠。



购物车优惠计算流程



相关注意点



  • 购物车中商品优惠金额不会在购物车中体现,要在生成确认单时才会体现;

  • 由于商品优惠都是以商品为单位来设计的,并不是以sku为单位设计的,所以必须以商品为单位来计算商品优惠;

  • 代码实现逻辑可以参考mall项目中OmsPromotionServiceImpl类的calcCartPromotion方法。


生成确认单


功能逻辑


用户在购物车页面点击去结算后进入生成确认单页面。确认单主要用于用户确认下单的商品信息、优惠信息、价格信息,以及选择收货地址、选择优惠券和使用积分。



生成确认单流程



相关注意点



  • 总金额的计算:购物车中所有商品的总价;

  • 活动优惠的计算:购物车中所有商品的优惠金额累加;

  • 应付金额的计算:应付金额=总金额-活动优惠;

  • 代码实现逻辑可以参考mall项目中OmsPortalOrderServiceImpl类的generateConfirmOrder方法。


生成订单


功能逻辑


用户在生成确认单页面点击提交订单后生成订单,可以从订单详情页查看。生成订单操作主要对购物车中信息进行处理,综合下单用户的信息来生成订单。



下单流程



相关注意点




  • 库存的锁定:库存从获取购物车优惠信息时就已经从pms_sku_stock表中查询出来了,lock_stock字段表示锁定库存的数量,会员看到的商品数量为真实库存减去锁定库存;




  • 优惠券分解金额的处理:对全场通用、指定分类、指定商品的优惠券分别进行分解金额的计算:



    • 全场通用:购物车中所有下单商品进行均摊;

    • 指定分类:购物车中对应分类的商品进行均摊;

    • 指定商品:购物车中包含的指定商品进行均摊。




  • 订单中每个商品的实际支付金额计算:原价-促销优惠-优惠券抵扣-积分抵扣,促销优惠就是购物车计算优惠流程中计算出来的优惠金额;




  • 订单号的生成:使用Redis来生成,生成规则:8位日期+2位平台号码+2位支付方式+6位以上自增id;




  • 优惠券使用完成后需要修改优惠券的使用状态;




  • 代码实现逻辑可以参考mall项目中OmsPortalOrderServiceImpl类的generateOrder方法。




取消订单


功能逻辑


订单生成之后还需开启一个延时任务来取消超时的订单,用户也可以在订单未支付的情况下直接取消订单。



订单取消流程



相关注意点



  • 代码实现逻辑可以参考mall项目中OmsPortalOrderServiceImpl类的cancelOrder方法。


支付成功回调


功能逻辑


前台用户订单支付完成后,第三方支付平台需要回调支付成功接口。



支付成功回调流程



相关注意点



  • 代码实现逻辑可以参考mall项目中OmsPortalOrderServiceImpl类的paySuccess方法。


总结


今天给大家介绍了mall项目中整套购物流程的功能设计,其实对于很多网站来说都需要这么一套功能,说它是通用功能也不为过。从本文中大家可以看到,mall项目的整套购物流程设计的还是比较严谨的,考虑到了方方面面,如果你对mall项目整套购物流程实现感兴趣的话可以学习下mall项目的代码。


项目源码地址


github.com/macrozheng/…


作者:MacroZheng
来源:juejin.cn/post/7290931758787313725
收起阅读 »

工作中时间都去哪了——《重要的两小时》选读 P1

———— # 本篇是:程序员成长-杂谈分享05 最近看了一本讲关于如何科学休息的书,里面有些观点写的很精辟,忍不住想要摘抄下来一直品读,并与各位分享~ 当我们端起第一杯咖啡时,就会用掌上设备检查电子邮箱,看一眼又有谁给我们加上了一条待办事项。邮件一封封被打开...
继续阅读 »

———— # 本篇是:程序员成长-杂谈分享05


最近看了一本讲关于如何科学休息的书,里面有些观点写的很精辟,忍不住想要摘抄下来一直品读,并与各位分享~



当我们端起第一杯咖啡时,就会用掌上设备检查电子邮箱,看一眼又有谁给我们加上了一条待办事项。邮件一封封被打开,压力也在一点点地累积,每一封邮件里都包含着我们明知自己不可能迅速完成的要求。于是我们只好把这些邮件标记为“未读”,留待“晚点再说”。而在脑海中,我们会把这些邮件扔到昨晚(昨晚离开办公室时明明已经很晚了) 没做完的那堆工作里。还有更多的邮件等着你回,更多的电话等着你打,更多的表格等着你填。而所有这一切都需要你立即集中精力去做。


实际上,在我们能着手进行真正重要的工作之前一-真正重要的工作也实在太多了-还需要打起精神解决许多事。我们经常工作一整天:先是在办公室里,回家后还得照顾家人、打扫房间、缴各种费用,有时甚至要一直忙到上床睡觉。简单来说,就是要做的事情太多,时间永远不够用.



卧槽。。这说的不就是我吗??感觉事情一件接着一件,每一件都不是马上就能完成的,要找人推进、要填表、要拟一段话等等;回到家后就是吃饭、洗碗、遛狗、洗澡、睡觉、家务等等,没有什么空余时间哇!



你想成为哪一个富兰克林?那个给自己的爱好和社交休闲留出时间、不断产生新的兴趣的富兰克林,还是那个比同行竞争者更勤勉、高效、受人尊重又有钱的生意人的家伙?在如今这个时代,似乎没有足够的时间能让你做到两者兼备,所以我们只能选择要么享受生活,要么成功。告诉你们一个好消息: 这个选择根本不存在。只有当我们错误地以为高效完全依赖于挤出足够多的时间工作时,我们才会面对这种选择的压力。



作者以富兰克林为例子,说了他是如何闲暇的度过时间的:作家、发明家、印刷家、哲学家、政治家、邮政局长、外交家,都是空闲时间给他的引导~


上面这段话我震惊了,我们原以为不能兼顾空闲时间和忙碌的工作,其实是不对的,空闲的时间可以与高效工作并存,这个高效的工作关键点不在于:挤压时间或者延长时间!或者说这个本来是跟时间无关的!时间是什么?思考我们对时间的感知。



身体动作还有可能影响你的情绪,并影响你对其他人的想法和意愿的理解。研究表明,如果你在评价某人时做了个敌对性的手势一-比如竖中指,你就很可能会把对方看成敌人,因为身体的动作事先已经暗示了这种敌对的倾向。或者,回想一下你是怎么学习的--这当然得依靠记忆力,但这并不是像往电脑上装软件或下载文件那样,把记忆植入大脑之中;相反,你得逐步培养记忆力,要花时间来改变神经元结构,让它们更容易相互激活。这也许能够解释为什么在考试前一晚突击复习效果远不如在数天之内一步步地进行复习一-如果你想记得久一点的话。



我们的行为会影响思想,这个毋庸置疑;同样,我们的思维习惯、行为方式会影响对时间的判断!!



心理学和神经学的科研成果已经告诉我们,我们该在何时、以何种方法创造出大脑的高效工作时间。在本书中,我将会详细分享五种看似简单的策略,对那些大忙人来说,它们能极为有效地帮助他们实现每日的“高效两小时”


1.意识到你的抉择点。只要你开始一项任务,基本上就会处于自动工作的状态,这样就很难改变你的工作方向。因此,要利用好不同任务交接时的那一刻--在这种时候,你能够选择下一步该做什么,然后把精力放到接下来最重要的任务上。


2.管理你的心理能量。需要高度自控力或专注度的任务可能会迅速消耗你的能量,而那些令你情绪化的任务则会让你不在状态。所以要学会按照这些任务的不同要求和恢复时间来安排任务。


3.不要与分心做无意义的缠斗。要学会引导自己的注意力。人的注意力系统天生就是会四处走神、不断更新目标,而不是永远专注的。不让自己分心,就像是阻断海浪一样毫无用处。了解你大脑的工作方式,才能帮助你在分心后迅速而有效地回到手头的工作上。


4.利用你的身心联系。注意你的运动和饮食方式,让它们保证你能完成某个短期内的目标。你可以在闲暇时再随心所欲地运动、吃东西。


5.让你的工作环境为你所用。去了解什么样的环境因素能够帮助你达到最佳状态,并学会如何随之调节环境。只要你知道了什么会让你分心,什么能预先让你的大脑进入创造和冒险状态,你就能调整自己的工作环境,从而实现高效能的工作。



基于科学,本书的核心观点分为这 5 点!


目前只读到了第一点,就大有感触,接着后面会说~~



大部分情况下,我们都处于自动模式下一-我们的所思、所感、所为都是按照无意识的常规程序进行的。所谓无意识,指的是思维或大脑并非有意识去做事。当然,我并不是指我们的所有行为都是不假思索的,我的意思是,我们对许多行为都习以为常并且非常熟练,所以几乎不需要有意识地监控它们。 虽然我在前言中强调过我们的大脑并不是电脑,也不能永远按照预期持之以恒地工作,然而在某种意义上,我们的大脑与电脑又非常相似:我们所做的几乎所有事情--从用牙线剔牙,到花一整天时间回复邮件--都是按照神经的常规程序来做的。这种神经程序和电脑程序其实并无太大差别,它引导着我们的思想、感受和行为。在某种程度上,我们是在自动地完成这些常规程序,而不会有意识地去考虑或反思这么做到底有没有道理。我们一旦开始了一项神经性常规活动,就会像电脑程序一样一直运行下去,直到完成任务,或是被打断。如果你开始用牙线剔牙,很可能直到你剔完牙,都不会意识到自己究竟完成了多少个繁复的步骤才把牙剔干净。如果你在到达办公室几分钟后就开始查阅电子邮件,很可能在你不知不觉的时候,就已经开始条件反射似的打开、阅读、回复一封邮件,然后是下一封,再下一封......也许直到你的同事打断你,拉着你一起去吃午餐时,你才会停下来。但是,当你一大早去办公室前,很可能是打算完成其他工作的,但一旦开始回复邮件,你的神经性常规程序就开始运行,而你却没办法停下来。除非有什么事情打断你,才能让你从这种状态下脱身。



作者举了一个极为生动的例子:道格在各种工作任务中挣扎,做很多工作都是无意识的!


这也就解释了:为什么我们会感觉忙了一天,又啥也没做?但实际上,事情一件接着一件也没停下来过!



我们每天所做的大部分事情一般不经大脑,只是由我们的习惯指引着,几乎不需要什么有意识的思考。这并不是件坏事。就像杜西格所解释的,我们的习惯很有存在的必要,它们能够节省大脑的能量。我们要让自己的大脑从这些行为中解放出来,才能解决不断产生的新问题。再举个例子,当我们克服了不会跳交谊舞的难题之后,就能按习惯动作进行,接下来才有精力和舞伴聊天。但你要是想在第一次学探戈时跟人聊天,那绝对会是一场灾难--我们需要有意识地关注自己的舞步。想象一下,如果我们必须集中注意力完成每一个行为--比如我们的每一步该落在何处一-那我们还能做什么呀! 实际上,每一天都是由一系列习惯性的神经常规程序组成的,我们一般称这些程序为“任务”: 早上起床、穿好衣服去上班、搭乘各种交通工具到工作地点、打开电脑、回复电子邮件、吃午餐、参加员工大会、跑步、做晚饭、洗漱然后上床睡觉。问题就在于,我们经常会从一个任务跳到另一个任务,却不仔细想想下一步最好该做什么。我们只是条件反射似的做出反应,或者是跟着直觉走,不管它们到底对不对,这就导致了无数时间和能量就这么白白地浪费了。



太棒了,说的就是我,周一到周五,工作的我就像机器人一样!很多事不用思考,全凭直觉!



我的经验告诉我,大家都习惯于在这种时刻--或者说抉择点一匆忙略过,好去做某些让你觉得自己“很高效”的事。匆忙地掠过一个抉择点一-也就是不同任务的间隙--也许能给你省出五分来钟的时间,但是,完成不该完成的任务可能会浪费掉一个钟头。不过,这五分钟的确会让你惴惴不安,因为在这种时刻,我们能够非常清楚地意识到每秒的流逝,而在那浪费掉的一小时中,因为我们基本上处于自动模式所以不会觉得难受。可惜的是,很多人都把时间浪费在了那些并不重要,或者在那个时间段根本不能好好完成的工作上了。 另一个困难就是,既然我们会如此频繁地进入自动模式,那么每天其实并没有太多机会能让我们有意识地决定下一步该完成什么工作。所以,意识到这些抉择点并抓住它们就显得极为重要。在接下来的几页 里,我会告诉你该如何做,但首先,让我们来了解神经常规程序是如何工作的,为什么人们会如此轻易地误用抉择点,这对我们大有裨益。



这一段内容比标题有含金量的多的多,我的工作中正是会不自觉的进入自动模式,在不断的工作任务中穿插,当下决定做下一件事的时候,很随意;往往这种随意决定会带来更多工作负担,比如事情的联系、对任务的专注等等,从一件事跳到另一件事,总需要额外的成本。



在学术界有一种理论影响范围很广,它认为人类在很多方面都是“认知上的懒惰者”。在其他条件完全相同的情况下,我们倾向于选择在思维层面阻力最小的那条路。正因为那些无意识的、已经很熟练的神经性常规程序相对而言更容易完成,而那些需要慎重考虑的、有意识的抉择则需要更多思维上的努力,所以,作为认知上的懒惰者,只要有可能,我们就会更倾向于依赖自动的神经性常规行为,而不是有意识的抉择。 在一步步完成神经性常规程序时,人人都会进入一种忘我的状态在韦氏词典里,关于“忘我”这个词有这样一条定义:“一种状态,在此状态下你不会意识到周围发生的事,因为你正在想着其他事。”如果你正在准备一份PPT,就很可能意识不到有两个同事正站在你的座位附近;如果你正在认真阅读一份报告,也许就意识不到自己饿了,或者已经到了午餐时间。当神经性常规程序在运行的时候,你既不会有太多的自我意识,也不太容易意识到在这个程序之外发生的一切。 但当常规程序结束的时候 (例如,当你剔完牙或者读完报告时)或者被什么人或事打断之后 (例如,当你正在准备PPT时,一位同事来向你求助关于某个项目的问题) ,自我意识就会浮现出来。从沉溺于神经性常规程序到停止这一程序,这之间的转变很可能会让你感到不适。



太对了,我们不关注“做决策”,而通过自动模式去做任务,是因为:我们在认知上的懒惰,需要在很多矛盾的事情下做判断,这是一种挑战,如果不愿意做,不花时间多考虑抉择,那么只会:南辕北辙,再次进入盲目的自动工作中;



例如,对着电脑屏幕看一封电子邮件和转过头听你妻子讲她的聚会计划 (我还从来没有这个福气) ,这两个动作就是相互矛盾的。这是两个相矛盾的常规活动,其中一个让你像僵尸一样沉浸在与收件人的虚拟对话中,而另一个则要求你回应并参与一场跟你配偶进行的现实对话。 这两种行为--盯着电脑和转过去面对我们的配偶一-是相互矛盾的,所以才会需要这种有意识地评估并做出决定的能力来帮助我们解决这种冲突。当察觉到某种冲突需要我们注意时,我们大脑中的某个特殊区域--前扣带皮质区--就会变得活跃起来。有些学者认为这一区域相当于某种警告系统,能够唤醒我们进行有意识的思维活动。有意识的反思似乎只是一种权宜之计,只有当我们更加无意识的活动导致了相互矛盾的行为,需要我们做出决定时,这种反思才会活跃起来。由此可知,抉择点通常是作为冲突一-无意识的自动行为之间相冲突、行为和目标之间相冲突一-的结果而出现的。在这样的时刻,我们往往发觉自已在被拉扯向不同的方向。



工作和生活中,经常遇到这样的矛盾:两个任务有冲突,自己就会被拉扯到不同的方向!!



正因为抉择点通常出现在矛盾的时刻,所以它们往往会令人不快。在前面的例子里,边构思边写邮件,与转过头面对你的配偶,听听她想要讲些什么,这两个任务如果分开来,你大概都很乐意去做:但一旦你必须从两者之间选择其一,我敢打赌,一两次之后你就会觉得烦躁,并发现抉择点真是令人不快。 在自我意识更加警醒的这些时刻,我们也开始注意到其他的各种事件,比如那些我们本来打算做却忘记做的事情,又比如时间的流逝。努力地控制正在做的事可能会让你觉得辛苦**。一项研究表明,你越是需要 注意自己的思绪、情感和行为,你就越觉得时间的流逝很缓慢。然而,这种不够“高产”的时间流逝,并不意味着你就浪费了很多时间。这只意味着我们恰巧更能意识到时间的存在而已。我认识的大多数人,当有很多事情要做的时候,如果他们意识到时间在流逝而自己却毫无进展,他们都会感到焦虑或充满愧疚。正因为这些抉择点会令人不舒服,我们往往才选择尽快跳过它们。



这就是核心:为什么我们专注于一项工作,一到两个小时,觉得很快就过去了!而在多个任务切换,多种任务矛盾、各种问题冲突的情况下,时间又慢,人又有情绪上的急躁,就会很累了!!



而这,往往正是让事情搞砸的地方。



真相了!!


面对任务矛盾的时候,疏于做决策,轻易进入自动模式,会被拉扯、会让对时间的感知变慢、会有情绪、会感觉很累!




P1 是前 20 页部分,今天暂且先读到这儿~~


作者:掘金安东尼
来源:juejin.cn/post/7291245033519398949
收起阅读 »

2023年震撼!Java地位摇摇欲坠?Java在TIOBE排行榜滑坡至历史最低!

一、Java掉到历史最低 从2023年6月开始Java掉到历史最低排到第4位 2023年10月tiobe编程语言排行榜,Java仍然还是排到了第4位,C# 和 Java 之间的差距从未如此之小。 top 10 编程语言1988年~2023年历史排名 引用...
继续阅读 »

一、Java掉到历史最低


从2023年6月开始Java掉到历史最低排到第4位



2023年10月tiobe编程语言排行榜,Java仍然还是排到了第4位,C# 和 Java 之间的差距从未如此之小。



top 10 编程语言1988年~2023年历史排名



引用tiobe官网上TIOBE Software 首席执行官的话:


10 月头条:C# 越来越接近 Java


C# 和 Java 之间的差距从未如此之小。目前,差距仅为 1.2%,如果保持这种趋势,C# 将在大约 2 个月的时间内超越 Java。在所有编程语言中,Java 的跌幅最大,为 -3.92%,C# 的涨幅最大,为 +3.29%(每年)。这两种语言一直在相似的领域中使用,因此二十多年来一直是竞争对手。Java 受欢迎程度下降的主要原因是 Oracle 在 Java 8 之后决定引入付费许可模式。微软在 C# 上采取了相反的做法。过去,C#只能作为商业工具Visual Studio的一部分。如今,C# 是免费且开源的,受到许多开发人员的欢迎。Java 的衰落还有其他原因。首先,Java 语言的定义在过去几年中没有发生太大变化,而其完全兼容的直接竞争对手 Kotlin 更易于使用且免费。——TIOBE Software 首席执行官 Paul Jansen


二、编程语言排行榜


编程语言排行榜是一种用来衡量编程语言的流行度或受欢迎程度的指标,它通常会根据一些数据或标准来对编程语言进行排序和评价。不同的编程语言排行榜可能会有不同的数据来源、计算方法和评估标准,因此它们的结果也可能会有所差异。


目前,最知名和权威的编程语言排行榜之一是 TIOBE 编程社区指数,它由成立于 2000 年 10 月位于荷兰埃因霍温的 TIOBE Software BV 公司创建和维护。TIOBE 编程社区指数通过对网络搜索引擎中涉及编程语言的查询结果数量进行计算,来衡量各种编程语言的受欢迎程度。TIOBE 编程社区指数每个月都会更新一次,并且每年还会评选出一门年度编程语言,表示该门语言在当年的排名中上升幅度最大。除了 TIOBE 编程社区指数之外,还有一些其他的编程语言排行榜,以下是列举的一些编程语言排行榜。


1、TIOBE编程语言排行榜


TIOBE是一家荷兰的编程软件质量评估公司,每月发布一份编程语言排行榜。它使用搜索引擎查询结果、开发者社区活跃度和其他指标来评估编程语言的受欢迎程度。



2023年10月TIOBE编程语言排行榜



2、Stack Overflow开发者调查


Stack Overflow每年进行一次开发者调查,其中包括有关最受欢迎编程语言的信息。Stack Overflow 开发者调查是最权威的编程语言排行榜之一,该调查可以反映全球开发者对编程语言的喜好和使用情况。在选择编程语言时,可以参考该调查的结果,但也需要根据自己的实际需求和开发环境进行综合考虑。



连续三年最受欢迎编程语言排名,可以明显的看出Java的占比在逐年的降低



3、GitHub编程语言趋势榜


GitHub提供了一个编程语言趋势页面,显示了开发者在GitHub上使用的编程语言趋势。虽然这不是正式的排行榜,但反映了实际的开发趋势。


GitHub在趋势榜比较前的基本者是Python或Go的项目



GitHub官网已经去掉了top的排名榜只保留了趋势榜,由一些GitHub的爱好者和贡献者创建和维护的www.github-zh.com的GitHub中文社区网站,是非官方github网站,它旨在为中文用户提供GitHub的相关资讯、教程、交流和协作平台,还可以查到Github项目排行榜。



三、展望Java


可以看到各种编程语言排行榜的数据,虽然会存在片面的情况,但也大体能表现出Java的地位在下降,遥想当年Java是排行榜霸榜老大哥。



虽然Java明显下降,或许正如TIOBE首席执行官说的“Java 受欢迎程度下降的主要原因是 Oracle 在 Java 8 之后决定引入付费许可模式。微软在 C# 上采取了相反的做法。”在这个开放的世界里真正的开源而不是利用开源来测试付费项目才能真正的让大家推崇。


Java的许可模式变化导致用户流失。自从Java 8之后,甲骨文公司决定对Java的商业使用收取费用,这使得一些企业和开发者转向其他免费或开源的语言,如C#、Python等 。


Java的竞争对手不断发展和创新,提供了更多的选择和优势。例如,C#在.NET平台上不断完善和扩展,支持跨平台、混合开发、WebAssembly等技术 ;Python在数据科学、人工智能、Web开发等领域有着广泛的应用和生态 ;Kotlin作为Android官方推荐的语言,兼容Java,并提供了更多的语法糖和功能 。




Java虽然在编程语言排行榜上有所下降,但并不意味着Java就没有前途和价值。Java仍然是一门成熟、稳定、高效、跨平台的语言,拥有庞大的用户群和丰富的生态系统。Oracle作为Mysql、Java等重量级项目的拥有者,也在不断地改进和创新Java,让Java能够适应时代的变化和需求。包括Java 17的免费、Kafka/Spring Boot新版本最低的Java版本为17、Java 21引入协程等,都是Oracle在努力让Java保持竞争力和活力的例证 。



未来在不断的变化,说不定马斯克的美女机器人就真的造出来了。。。


当然,我们也不能忽视其他编程语言的发展和优势,我们应该保持开放和学习的心态,了解不同语言的特点和适用场景,选择最合适的语言来解决问题。编程语言只是工具,重要的是我们能够用它们创造出有价值的产品和服务。


作者:玄明Hanko
来源:juejin.cn/post/7290849115721285667
收起阅读 »

可别小看了一边写代码嘴里一边叨咕的同事,人家可能用的是小黄鸭调试法

什么,鸭子还能调试代码?什么神奇的鸭子啊。 当然不是了,是鸭子帮你调试,那好像也有点儿厉害。 初听感觉是傻子,再听感觉是玄学。 什么是小黄鸭调试法 当然不是鸭子调试代码了,也不是鸭子帮你调试,其实还是靠你自己的。 小黄鸭调试法(Rubber Duck Deb...
继续阅读 »

什么,鸭子还能调试代码?什么神奇的鸭子啊。


当然不是了,是鸭子帮你调试,那好像也有点儿厉害。


初听感觉是傻子,再听感觉是玄学。



什么是小黄鸭调试法


当然不是鸭子调试代码了,也不是鸭子帮你调试,其实还是靠你自己的。


小黄鸭调试法(Rubber Duck Debugging)是一种常用于解决编程问题的技巧,不是代码技术层面的技巧。


大致的调试过程是这样的:



  1. 首先你写好了代码,或者有些逻辑一直写不出来,然后很有自信或者不自信;

  2. 然后你找到一只鸭子,玩具鸭子,或者任意一个电脑旁边的物件;

  3. 最后,把你的代码的逻辑尽量详细的讲个上一步找到的对象,比如一只玩具鸭子;

  4. 通过讲解的过程,你很有可能发现代码上的漏洞,有时候还能发现隐藏的漏洞;



你还可以拉过旁边的人,对着他讲,前提是保证别人不会打你。


这个过程更像是一种review的过程,而且是那种非常具体的review,只不过是自己 review 自己的代码或逻辑。


它的核心是通过将问题或逻辑用语言描述出来,在这个过程中找到解决问题的线索。


虽然这个方法听起来可能有点奇怪,但它在实际中确实能够帮助很多人解决问题。解释问题的过程可能会强迫你慢下来,更仔细地思考,从而找到之前忽略的问题点。


另外,在进行这一些列操作的过程中,尽量保证周围没有人,不然别人可能觉得你是个傻子。


当然了,这个操作你可以在心里默默进行,也是一样的效果。


各位平时工作中有没有遇见过有人使用小黄鸭调试法呢?我看到这个概念的时候想了一下,好像还真碰到过。之前有同事在那儿写代码,一边写嘴里一边叨咕,也不知道在说啥,还开玩笑说这是不是你们这个星座的特质(某个星座)。


现在想想,人家当时用的是不是小黄鸭调试法呀,只恨当初孤陋寡闻,没有问清楚啊。


内在原理


小黄鸭调试法的内在原理其实是涉及到认知心理学中的一些概念的,并不真的是玄学和沙雕行为。


认知外部化


这是小黄鸭调试法的核心。当你将问题从内心中的思考状态转移到外部表达时,你会更加仔细地思考问题。解释问题需要你将问题的细节和步骤以清晰的语言描述出来,这个过程可以帮助你整理思路,更好地理解问题。


问题表达


描述问题的过程可以迫使你更具体地考虑问题。将问题分解为不同的部分,逐步地解释代码的执行流程,有助于你更好地理解代码中可能的缺陷或错误。


观察问题:


当你通过语言表达问题时,可能会注意到之前忽略的细节。这可能是因为你在描述问题时需要更仔细地审查代码和逻辑,从而让你注意到潜在的问题点。


听觉和口头处理


讲述问题的过程涉及到将问题从书面表达转化为口头表达。听觉和口头处理可以帮助你以不同的方式来理解问题,可能会在你的大脑中触发新的洞察力。


认知切换:


与代码一起工作时,你可能会一直陷入相同的思维模式中,难以看到问题。通过将问题从代码中抽离出来,并通过描述来关注它,你会进行认知切换,从而能够以不同的角度审视问题。


总结起来其实很简单,如果一个知识点你理解了,你一定能给别人讲出来,或者写出来,而且别人能够理解。如果你在讲的时候发现有模棱两可的地方,那说明你还没有百分百理解。


就像我们平时写技术文章一样,有时候碰到一些细节写半天也写不清楚,那就是还没有完全理解。


作者:古时的风筝
来源:juejin.cn/post/7290932061174022159
收起阅读 »

🤔公司实习生居然还在手动切换node版本?

web
前段时间看了实习生的新项目,发现他启动不了项目,因为node版本太低,我让他去用nvm来管理node的版本,然后看到他切换版本的时候是这样的,先用nvm下载安装目标的node版本,然后在把安装好的node版本替换掉原先的node路径下的node_modules...
继续阅读 »

前段时间看了实习生的新项目,发现他启动不了项目,因为node版本太低,我让他去用nvm来管理node的版本,然后看到他切换版本的时候是这样的,先用nvm下载安装目标的node版本,然后在把安装好的node版本替换掉原先的node路径下的node_modules,而不是用命令行进行版本切换,才发现原来他使用nvm来切换node版本虽然显示切换成功,但全局的node版本一直是不变的,所以才用文件覆盖的方式强制进行解决,对于这个问题进行解决并且梳理



可以直接跳到第四步查看解决方案


1️⃣ 安装nvm


where nvm

找不到nvm路径的朋友可以用这个命令来找到nvm的安装路径,默认的安装路径都是差不多的


image.png


2️⃣ 查看目前node版本


可以看到目前的版本是node V16.14.2


image.png


3️⃣ nvm安装目标node版本



nvm的主要作用就是用来切换node的版本,为什么要切换node的版本,就是因为你的当前node版本和你要启动的项目不兼容,有两种可能,要么是你的项目太旧,你的node版本相对来说比较高,需要用到向下兼容;另外一种可能就是你项目用到的node比较新,你需要进行升级



先安装需要安装的目标版本,用isntall来安装你需要的对应node版本


image.png


回到你的nvm安装路径,就可以看到你已经安装的各种版本的node文件夹
image.png


当然也可以用命令行


nvm list

image.png


4️⃣ nvm切换到目标node版本


切换到目标node版本使用nvm use


nvm use

查看目前nvm安装了哪些版本 然后use来进行切换


image.png


到切换的时候发现了问题,这里无论怎么切换,node的版本依然不会变


image.png


可以看到我用的use来切换到15的版本,但是再次查看nvm的node历史版本,可以看到还是位于16.14.2的node版本,明明就是这么顺利的问题,出了一个让人摸不到头脑的事情


5️⃣寻找问题


既然nvm切换版本已经成功,那么为什么node版本不会变,有没有可能根本改的不是同一个node,或者是存在两个node,直到我打开环境变量一看,为啥会存在两个node的路径,可能的原因就是之前的node版本没有删除,node -v一直输出的是安装前的node


image.png


原来已经安装了一个node的,全局的node指向的node路径和nvm切换node的路径是不一样的


nvm切换的node是基于他文件夹中的nodejs


image.png


image.png



点进去看你会发现他也是有一个node.exe的程序的,所以问题是已经找到的了,目前系统上出现了两个node,并且nvm切换的node版本并不是全局的node,因为环境变量已经指向了旧的node,他的版本不会改变,那么nvm去怎么切换都是没有用的



6️⃣解决方案


image.png


看了网上的一些解决方案都是要在nvm中新建两个文件夹的方式来解决,但是其实直接把nodejs删除也是一个很直接的办法,先通过where node找到当前的node的安装目录,直接进行删除


image.png


最后是通过把另外一个目录的node进行删除,重新看一下node的安装路径,也就是重新执行一下 where node


image.png


可以看到在nvm配置正确的情况下是能直接指向nvm下的node的


最后重新切换一下node的版本,也就是上文的操作


image.png


PS


我指的手动切换是nvm下载node版本之后手动去替换node_modules,原来大家觉得用nvm use也是手动替换(确实是我的问题),经过评论区广大jym指正,可以尝试一下volta这个工具来进行切换版本,真正做到不用手动替换,后续我会亲自去体验一下并且发文,感谢评论区小伙伴


🙏 感谢您花时间阅读这篇文章!如果觉得有趣或有收获,请关注我的更新,给个喜欢和分享。您的支持是我写作的最大动力!


作者:一只大加号
来源:juejin.cn/post/7291096702021304354
收起阅读 »

OpenHarmonyMeetup2023深圳站圆满举办

10月15日,OpenHarmonyMeetup2023城市巡回深圳站活动,在深圳市科学馆隆重举办。活动由OpenHarmony项目群工作委员会主办,深圳市科技传播促进会承办,深圳市科学馆、触觉智能、广鸿会协办,电子发烧友、深开鸿、鸿湖万联、开鸿智谷提供支持,...
继续阅读 »

10月15日,OpenHarmonyMeetup2023城市巡回深圳站活动,在深圳市科学馆隆重举办。活动由OpenHarmony项目群工作委员会主办,深圳市科技传播促进会承办,深圳市科学馆、触觉智能、广鸿会协办,电子发烧友、深开鸿、鸿湖万联、开鸿智谷提供支持,吸引了深圳本地开发者与企业的广泛参与。本次深圳站Meetup重点围绕“OpenHarmony正当时-生态及技术应用”为主题做精彩深度分享。

OpenHarmonyMeetup深圳站签到现场

在深圳市科学技术协会的指导下,本次活动得到OpenHarmony项目群工作委员会批准,由深圳市科技传播促进会秘书长郝立芳作为出品人,携五位开源领域专家分别从六个维度进行了议题分享,并举办了以”开源论道“为主题的深度话题探讨。主要聚焦OpenHarmony技术及生态发展、OpenHarmony开源硬件探索、开源科普与开源人才培养、OpenHarmony产品商业应用场景、OpenHarmony商显领域发展、标准系统OpenHarmony硬件方案落地场景等维度,让更多开发者了解OpenHarmony项目,影响推动更多拥有热情和创造力的技术开发者加入到社区。

OpenHarmonyMeetup2023深圳站活动合影

深圳市科学技术协会党组成员、副主席石兴中出席并致辞,指出未来技术开源的发展以及当下推动OpenHarmony生态建设的重要性,从多方面鼓励和肯定了活动意义,同时也表示深圳市科学技术协会在学术交流和技术沙龙方面有很多支持政策,希望OpenHarmonyMeetup深圳站这样的活动能成为自主创新大讲堂等深圳市科协品牌活动的重要内容之一,成为深圳创新之城的技术交流活动亮丽名牌。深圳科学馆副馆长宋欠芽、科普部部长柳进材、深圳媒体研究会副秘书长胡志方共同出席本次活动。

深圳市科学技术协会党组成员、副主席石兴中发表致辞

第一个议题由来自广鸿会创始人、OpenHarmonyLoongArchSIG组长连志安带来《OpenHarmony生态及技术展望》主题演讲,重点介绍了万物互联时代下的技术变革、OpenHarmony技术架构和未来发展方向。连志安表示,软件是推动技术变革的重要推动力,在万物互联的时代背景下,以面向全场景、分布式的操作系统是构筑信息技术革命的基石。OpenHarmony作为新一代操作系统,以其三大技术特性,支撑万物互联产业的发展。

广鸿会创始人、OpenHarmonyLoongArchSIG组长连志安做主题分享

第二个议题由来自深圳市科技传播促进会秘书长郝立芳带来《OpenHarmony开源硬件的探索》主题演讲,介绍了开源大师兄项目、基于OpenHarmony的火星基地课程研发与设计,以及开源科普与开源人才培养整体体系建设。郝立芳表示,中小学科技教育启迪创新是主流,将开源原则和技术融入开源人才培养,培育技术人才从娃娃抓起,呼吁更多的开发者关注到青少年开发板的应用,为提升新一代开发者参与开源的能力而努力。

深圳市科技传播促进会秘书长郝立芳做主题分享

第三个议题由来自深开鸿生态合作总监王旭带来《OpenHarmony践行科技数字未来》主题演讲,介绍了基于OpenHarmony打造的KaihongOS在实际商业应用场景的落地,以KaihongOS构建数字化底座,赋能智慧城市、智慧港口等商用领域落地,展示OpenHarmony技术特性能力。

深开鸿生态合作总监王旭做主题分享

第四个议题由湖南开鸿智谷数字产业发展有限公司副总裁李传钊和开鸿智谷应用开发专家艾彬共同带来《OpenHarmony UI建设初探》主题演讲,他们从OpenHarmony能够在UI/UX层面做到什么样的高度、开发一套定制化UI又会面临哪些困难、跨设备的UI又有哪些关注点等问题出发,分享了OpenHarmony系统的UI建设关键技术特点使用和成果。

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

湖南开鸿智谷数字产业发展有限公司应用开发专家艾彬做主题分享

第五个议题由来自鸿湖万联开发总监何波带来《OpenHarmony在商显领域的创新思路分享》主题演讲,介绍基于OpenHarmony的商显解决方案及商显发展成果、趋势与展望。何波表示,期待OpenHarmony未来越来越好,生态越来越繁荣,同时为商显领域带来更多的发展,促进各个厂商通力合作,共同打造全新的基于物联网世界的商显新天地。

鸿湖万联开发总监何波做主题分享

第六个议题由来自深圳触觉智能科技有限公司副总经理朱经波带来《OpenHarmony硬件生态解决方案》主题演讲,介绍了标准系统OpenHarmony硬件方案落地场景,分享了基于OpenHarmony的当前硬件生态。朱经波表示,新的OS在传统硬件平台上将会焕发不一样的活力,触觉智能从微小处着手,打造高性价比的创客开发板和适合行业级的硬件系统解决方案,包含教育、智慧矿山、三防平板、手持终端、金融政务、工业控制器等,让更多创客加入到OpenHarmony的生态开发中来,让更多的软件开发公司充分聚焦行业及业务应用软件而不必考虑稳定可靠硬件设计的烦恼。

深圳触觉智能科技有限公司副总经理朱经波做主题分享

在本次活动最后,还举行了以“开源论道”为主题的圆桌论坛,围绕开源技术创新、从业方向、人才培养等话题,深入探讨了开源技术的发展与生态共建创新模式。

OpenHarmonyMeetup2023深圳站圆桌会议互动环节

OpenHarmony是由开放原子开源基金会(OpenAtomFoundation)孵化及运营的开源项目,目标是面向全场景、全连接、全智能时代,基于开源的方式,搭建下一代智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。OpenHarmony开源三年来,社区快速成长。截至2023年9月底,已有50家共建单位、累计超过5900名贡献者,累计产出超一亿行代码;累计已有156个厂家的413款产品通过兼容性测评,覆盖金融、超高清、教育、商显、工业、警务、城市、交通、医疗等领域。OpenHarmony社区已成为“下一代智能终端操作系统根社区”,携手共筑万物互联的底座,使能千行百业的数字化转型。

OpenHarmony社区,下一代智能终端操作系统根社区,扎根于和开发者面对面近距离交流,吸纳更多具备热情和创造力的技术开发者加入到社区,我们期待更多具备组织能力、领导能力的技术开发者和我们一起为OpenHarmony共同发声。如果你对开源充满激情,请带着你的活动方案联系我们,我们将为你提供更多活动能量,让你更加贴近OpenHarmony。

收起阅读 »

推荐一款“自学编程”的宝藏网站!详解版~(在线编程练习,项目实战,免费Gpt等)

🌟云端源想学习平台,一站式编程服务网站🌟云端源想官网传送门⭐📚精品课程:由项目实战为导向的视频课程,知识点讲解配套编程练习,让初学者有方向有目标。🎯📈课程阶段:每门课程都分多个阶段进行,由浅入深,很适合零基础和有基础的金友们自由选择阶段进行练习学习。🌈🎯章节实...
继续阅读 »

🌟云端源想学习平台,一站式编程服务网站🌟


云端源想官网传送门


📚精品课程:由项目实战为导向的视频课程,知识点讲解配套编程练习,让初学者有方向有目标。🎯


📈课程阶段:每门课程都分多个阶段进行,由浅入深,很适合零基础和有基础的金友们自由选择阶段进行练习学习。🌈


🎯章节实战:每一章课程都配有完整的项目实战,帮助初学者巩固所学的理论知识,在实战中运用知识点,不断积累项目经验。🔥


💼项目实战:企业级项目实战和小型项目实战结合,帮助初学者积累实战经验,为就业打下坚实的基础,成为实战型人才。🏆


729cc0d1da3852fe1d8a0eb81a6b357b.png


🧩配套练习:根据课程小节设置配套选择练习或编程练习,帮助学习者边学边练,让学习事半功倍。💪


💻在线编程:支持多种编程语言,创建Web前端、Java后端、PHP等多种项目,开发项目、编程练习、数据存储、虚拟机等都可在线使用,并可免费扩展内存,为你创造更加简洁的编程环境。🖥


🤝协作编程:可邀请站内的好友、大佬快速进入你的项目,协助完成当前项目。与他人一起讨论交流,快速解决问题。👥


📂项目导入导出:可导入自己在在线编辑过的项目再次编辑,完成项目后也可以一键导出项目,降低试错成本。🔗


🤖AI协助编程:AI智能代码协助完成编程项目,随时提问,一键复制代码即可使用。💡

ddcdaae2cab0bac546d4d195018866f0.png


🔧插件工具:在使用在线编程时,可在插件工具广场使用常用的插件,安装后即可使用,帮助你提高编程效率,带来更多便捷。🛠


📞一对一咨询:编程过程中,遇到问题随时提问,网站1V1服务(在职程序员接线,不是客服),实时解决你在项目中遇到的问题。📬


🛠工具广场:提供一些好用的在线智能工具,让你能够快速找到各种实用工具和应用。覆盖了多个领域,包括智能AI问答等。🔍


910fa68dda93dd2594530d0e5af268c7.pngd419dacb74e778686c40897862075ac8.png

收起阅读 »

推荐一款“自学编程”的宝藏网站!详解版~(在线编程练习,项目实战,免费Gpt等)

🌟云端源想学习平台,一站式编程服务网站🌟 云端源想官网传送门 ⭐ 📚精品课程:由项目实战为导向的视频课程,知识点讲解配套编程练习,让初学者有方向有目标。🎯 📈课程阶段:每门课程都分多个阶段进行,由浅入深,很适合零基础和有基础的金友们自由选择阶段进行练习学...
继续阅读 »

🌟云端源想学习平台,一站式编程服务网站🌟


云端源想官网传送门


📚精品课程:由项目实战为导向的视频课程,知识点讲解配套编程练习,让初学者有方向有目标。🎯


📈课程阶段:每门课程都分多个阶段进行,由浅入深,很适合零基础和有基础的金友们自由选择阶段进行练习学习。🌈


🎯章节实战:每一章课程都配有完整的项目实战,帮助初学者巩固所学的理论知识,在实战中运用知识点,不断积累项目经验。🔥


💼项目实战:企业级项目实战和小型项目实战结合,帮助初学者积累实战经验,为就业打下坚实的基础,成为实战型人才。🏆




🧩配套练习:根据课程小节设置配套选择练习或编程练习,帮助学习者边学边练,让学习事半功倍。💪


💻在线编程:支持多种编程语言,创建Web前端、Java后端、PHP等多种项目,开发项目、编程练习、数据存储、虚拟机等都可在线使用,并可免费扩展内存,为你创造更加简洁的编程环境。🖥


🤝协作编程:可邀请站内的好友、大佬快速进入你的项目,协助完成当前项目。与他人一起讨论交流,快速解决问题。👥


📂项目导入导出:可导入自己在在线编辑过的项目再次编辑,完成项目后也可以一键导出项目,降低试错成本。🔗


🤖AI协助编程:AI智能代码协助完成编程项目,随时提问,一键复制代码即可使用。💡



🔧插件工具:在使用在线编程时,可在插件工具广场使用常用的插件,安装后即可使用,帮助你提高编程效率,带来更多便捷。🛠


📞一对一咨询:编程过程中,遇到问题随时提问,网站1V1服务(在职程序员接线,不是客服),实时解决你在项目中遇到的问题。📬


🛠工具广场:提供一些好用的在线智能工具,让你能够快速找到各种实用工具和应用。覆盖了多个领域,包括智能AI问答等。🔍



收起阅读 »

在Flutter上封装一套类似电报的图片组件

前言 最近项目需要封装一个图片加载组件,boss让我们实现类似电报的那种效果,直接上图: 就像把大象装入冰箱一样,图片加载拢共有三种状态:loading、success、fail。 首先是loading,电报的实现效果是底部展示blur image, 上面盖...
继续阅读 »

前言


最近项目需要封装一个图片加载组件,boss让我们实现类似电报的那种效果,直接上图:


581697505195_.pic.jpg


就像把大象装入冰箱一样,图片加载拢共有三种状态:loading、success、fail。


首先是loading,电报的实现效果是底部展示blur image, 上面盖了个progress indicator。blur image有三方库可以实现:flutter_thumbhash | Flutter Package (pub.dev),但是这个库有个bug: 它使用到了MemoryImage, 并且MemoryImage的bytes参数每次都是重新生成的,因而无法使用缓存。所以上面的progress刷新时底部的blur image都会不停闪烁。


//MemoryImage
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is MemoryImage
&& other.bytes == bytes
&& other.scale == scale;
}

@override
int get hashCode => Object.hash(bytes.hashCode, scale);

笔者覆写了equals和hashcode方法,通过listEquals方法来比较bytes,考虑到thumb_hash一般数据量都比较小估计不会有性能问题。
也有人给了个一次性比较8个byte的算法【StackOverflow摘抄】😄


/// Compares two [Uint8List]s by comparing 8 bytes at a time.
bool memEquals(Uint8List bytes1, Uint8List bytes2) {
if (identical(bytes1, bytes2)) {
return true;
}

if (bytes1.lengthInBytes != bytes2.lengthInBytes) {
return false;
}

// Treat the original byte lists as lists of 8-byte words.
var numWords = bytes1.lengthInBytes ~/ 8;
var words1 = bytes1.buffer.asUint64List(0, numWords);
var words2 = bytes2.buffer.asUint64List(0, numWords);

for (var i = 0; i < words1.length; i += 1) {
if (words1[i] != words2[i]) {
return false;
}
}

// Compare any remaining bytes.
for (var i = words1.lengthInBytes; i < bytes1.lengthInBytes; i += 1) {
if (bytes1[i] != bytes2[i]) {
return false;
}
}

return true;
}

图片加载和取消重试


电报在loading的时候可以手动取消下载,这个在Flutter官方Image组件和cached_network_iamge组件都是不支持的,因为在设计者看来既然图片加载失败了,那重试也肯定还是失败(By design)。
extended_image库对cancel和retry做了支持,这里要给作者点赞👍🏻


取消加载


加载图片是通过官方http库来实现的, 核心逻辑是:


final HttpClientRequest request = await httpClient.getUrl(resolved);
headers?.forEach((String name, String value) {
request.headers.add(name, value);
});
final HttpClientResponse response = await request.close();
if (timeLimit != null) {
response.timeout(
timeLimit!,
);
}
return response;

返回的response是个Stream对象,通过它来获取图片数据


final Uint8List bytes = await consolidateHttpClientResponseBytes(
response,
onBytesReceived: chunkEvents != null
? (int cumulative, int? total) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: cumulative,
expectedTotalBytes: total,
));
}
: null,
);

图片加载进度就是通过ImageChunkEvent来获取的,cumulative代表当前已加载的长度,total是总长度,所有图片加载库都是通过它来显示进度的。所以,如何取消呢?这里就需要用到Flutter异步的一个API了:


Future.any(<Future<T>>[Future cancelTokenFuture, Future<Uint8List> imageLoadingFuture])

在加载的时候除了加载图片数据的Future,我们再额外生成一个Future,当需要取消加载的时候只需要后者抛出Error那加载就会直接终止,extended_image就是这么做的:


class CancellationTokenSource {
CancellationTokenSource._();

static Future<T> register<T>(
CancellationToken? cancelToken, Future<T> future) {
if (cancelToken != null && !cancelToken.isCanceled) {
final Completer<T> completer = Completer<T>();
cancelToken._addCompleter(completer);

///CancellationToken负责管理cancel completer
return Future.any(<Future<T>>[completer.future, future])
.then<T>((T result) async {
cancelToken._removeCompleter(completer);
return result;
}).catchError((Object error) {
cancelToken._removeCompleter(completer);
throw error;
});
} else {
return future;
}
}
}

这种取消机制有个问题:虽然上层会捕获抛出的异常终止加载,但是网络请求还是会继续下去直到加载完图片所有数据,我于是翻看了Flutter的API,发现上面提到的解析HttpResponse的方法consolidateHttpClientResponseBytes有个注释:


/// The `onBytesReceived` callback, if specified, will be invoked for every
/// chunk of bytes that is received while consolidating the response bytes.
/// If the callback throws an error, processing of the response will halt, and
/// the returned future will complete with the error that was thrown by the
/// callback. For more information on how to interpret the parameters to the
/// callback, see the documentation on [BytesReceivedCallback].

即onBytesReceived方法如果抛出异常那么就会终止数据传输,所以可以根据chunkEvents是否alive来判断是否需要继续传输,如果不需要就直接抛出异常,从而终止http请求。


重试


图片加载有两种重试:第一种是自动重试,笔者遇到了一个connection closed before full header was received错误,而且是高概率出现,目前没有好的解决办法,加上自动重试机制后好了很多。


第二种就是手动重试,自动重试达到阈值后还是失败,手动触发加载。我这里主要讲第二种,在电报里的展示效果是这样:


591697507850_.pic.jpg


这里卡了我好久,主要是我对Flutter的ImageCache了解不深入导致的,首先看几个问题:


1. 页面有一张图片加载失败,退出页面重新进来图片会自动重新加载吗?


答案是不一定,Flutter图片缓存存储的是ImageStreamController对象,这个对象里有一个FlutterErrorDetails? _currentError;属性,当加载图片失败后_currentError会被赋值,所以退出后重进页面虽然会导致页面重新加载,但是获取到的缓存对象有Error,那就会直接进入fail状态。
缓存的清理是个很复杂的问题, ImageStreamCompleter的清理逻辑主要靠两个属性:_listeners_keepAliveHandles


List<ImageStreamListener> _listeners = [];

@mustCallSuper
void _maybeDispose() {
if (!_hadAtLeastOneListener || _disposed || _listeners.isNotEmpty || _keepAliveHandles != 0) {
return;
}

_currentImage?.dispose();
_currentImage = null;
_disposed = true;
}

_listerners的add和remove时机和Image组件有关


/// image.dart
/// 加载图片
void _resolveImage() {
......
final ImageStream newStream =
provider.resolve(createLocalImageConfiguration(
context,
size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null,
));
_updateSourceStream(newStream);
}

void _updateSourceStream(ImageStream newStream) {
......
/// 向ImageStreamCompleter注册Listener
_imageStream!.addListener(_getListener());
}

既然有了_listeners那为什么还需要_keepAliveHandles属性呢,原因就是在image组件所在页面不在前台时会移除注册的listerner,如果没有_keepAliveHandles属性那缓存可能会被错误清理:


@override
void didChangeDependencies() {
_updateInvertColors();
_resolveImage();

if (TickerMode.of(context)) {
///页面在前台的时候获取最新的ImageStreamCompleter对象
_listenToStream();
} else {
///页面不在前台移除Listener
_stopListeningToStream(keepStreamAlive: true);
}
super.didChangeDependencies();
}

回到最开始的问题:如果加载失败的图片组件在其他页面不存在,那image组件dispose的时候就会清理掉缓存,第二次进入该页面的时候就会重新加载。反之,如果其他页面也在使用该缓存,那二次进入的时候就会直接fail。


一个很好玩的现象是,假如两个页面在加载同一张图片,那么其中一个页面图片加载失败另外一个页面也会同步失败。


2. 判定加载的是同一张图片


这里的相同很重要,因为它决定了ImageCache的存储,比如笔者自定义一个NetworkImage:


class _NetworkImage extends ImageProvider<_NetworkImage> {

_NetworkImage(this.url);

final String url;

@override
ImageStreamCompleter loadImage(NetworkImage key, ImageDecoderCallback decode);

@override
Future<ExtendedNetworkImageProvider> obtainKey();
}

obtainKey一般都会返回SynchronousFuture<_NetworkImage>(this),它代表的是ImageCache使用的键,ImageCache判断当前是否存在缓存的时候会拿Key和缓存的所有键进行比对,这个时候equals和hashcode就开始起作用了:


@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is _NetworkImage
&& other.url == url;
}

@override
int get hashCode => Object.hash(url);

因为我们需要支持取消加载,所以最初我考虑加上cancel token到相同逻辑的判定,但是这会导致同一张图片被不停重复加载,缓存完全失效。


为了解决上面的问题,我对ImageCache起了歪脑筋:能不能在没有加载成功的时候允许并行下载,但是只要有一张图片成功,那后续都可以复用缓存?
如果要实现这个效果,那就必须缓存下所有下载成功的ImageProvider或者对应的CancelToken。下载成功的监听好办,在MultiFrameImageStreamCompleter加个监听就完事。难的是缓存消除的时机判断,ImageCache的缓存机制很复杂(_pendingImages,_cacheImage,_liveImages),并且没有缓存移除的回调。


最终,我放弃了这个方案,不把cancel token放入ImageProvider的比较逻辑中。


3. 实现图片重新加载


首先,我给封装的图片组件加了个reloadFlag参数,当需要重新加载的时候+1即可:


@override
void didUpdateWidget(OldWidget old) {
if(old.reloadFlag != widget.reloadFlag) {
_resolveImage();
}
}

但是,这个时候不会起作用,因为之前失败的缓存没被清理,ImageProvider的evict方法可实现清理操作。


4. 多图状态管理


我在适配折叠屏的时候发现了一个场景:多页面下载相同图片时有时无法联动,首先看cancel:



  • A页面加载图片时使用CancelToken A,新建缓存

  • B页面使用CancelToken B, 复用缓存


B的CancelToken完全没用到,所以是cancel不了的。为了解决这个问题,我创建了一个CancelTokenManager,按需生成CancelToken,并在加载成功或失败时清理掉。


然后是重试,多图无法同时触发重试,虽然可以复用同一个ImageStreamCompleter对象,但ImageStream对象却是Image组件单独生成的,所以只能借助状态管理框架或者事件总线来实现同步刷新。


作者:芭比Q达人
来源:juejin.cn/post/7290732297427107895
收起阅读 »

你是先就业还是先择业?

就业的”就“不是让你将就   是不是大家常常听到家里人唠叨的一句话:“有工作就行了啊,别那么挑剔,你都这么大了,还指望着家里养着你啊” 。还是老师说:“我们要建立优先就业再择业的就业观,不能一直不去就业呀”什么的叭叭叭。   其实某方面来说他们并没有说错,我们...
继续阅读 »

就业的”就“不是让你将就


  是不是大家常常听到家里人唠叨的一句话:“有工作就行了啊,别那么挑剔,你都这么大了,还指望着家里养着你啊” 。还是老师说:“我们要建立优先就业再择业的就业观,不能一直不去就业呀”什么的叭叭叭。


  其实某方面来说他们并没有说错,我们已经成年了,需要独立自主。在漂亮国,到了18岁好像都要分家了吧。不过我们在中国,中国的国情肯定和漂亮国不一样。除此之外中国家庭从小的哭穷式教育,估计让许多孩子都想自己经济独立吧。这个现象导致了大家认为有工作就行了,我管他什么工作呢。


  但是从自身职业发展来讲,这是对自己极其不负责的表现,往往许多人的第一份工作就决定了人生轨迹,不论是以后决定发展的城市,还是以后工作的方向,其实已经早已埋下种子。你说你可以换工作啊,可以跳槽啊,现实往往会打醒你,你以为你没了应届生身份,凭着你那不到一年的工作经验,人家企业看中你什么。所以我们要就业要为自己,同时也要为自己的未来负责,我们要慎之又慎。所以我们要就业不过的选择自己合适的就业不能盲目就业,家长的思想大部分过时了,停留在了上了大学就有好工作的时代。我们只能参照而不能按部就班,对于老师,大部分是为了提高学校就业率完成指标而已,不必要太大理会,当然和你关系好的老师除外,但是相反如果和你关系好他一定会不会让你草草就业的。


u=1343747016,2016950934&fm=253&fmt=auto&app=138&f=JPEG.webp


择业的”择“也别太择



钱多事少离家近,位高权重责任轻。睡觉睡到自然醒,数钱数到手抽筋。



  说完就业再谈谈择业,相信上面这句话大家都听过,这简直就是梦中情职,所以择业在我看来无非四种:离家近的、工资高的、自己感兴趣的、清闲的。这四种涵盖了大部分职业了吧。所以我们怎么择业,选择一个适合自己的职位对于未来发展是事半功倍的。


  大家选择职业的时候不知道是从哪方面来选择的,首先离家近,相信很多女生都是考虑这个优先吧,感觉男生就是喜欢仗剑走天涯那种🤣。然后考虑清闲的,想想你二十几岁的年龄你还要工作四五十年可能,选个清闲一点的职业不过分吧,最好就是考一个公务员事业编了,实在不行就去央企国企了,当然这种工作大家都想去,虽然工资不高但是福利好啊。再者就是兴趣了,把自己的兴趣培养成自己的职业也是可以的,大学就是很好的时间,选那种课比较少的专业,这里不得不再次吐槽大学课程的无用性。然后自己选一个自己喜欢的职业,比如摄影、博主什么的。不过当喜欢的事变成职业很多人也就不喜欢了,比如电竞职业选手他们天天十几个小时训练打游戏,他们下班还会想打游戏嘛🤣。就是坚持很重要。再再者,有的人说自己天生无感对什么都没兴趣,那么恭喜你和我一样🤣,就是什么的不是很感兴趣,也不讨厌,那么我建议搞钱,选个高薪的职业搞到足够的钱就退休了,当初就是看程序员薪资高入行了,对钱总感兴趣了吧。总而言之择业择业选择自己合适的再就业。


  鱼和熊掌不可兼得。选择离家近的就得忍受小镇的慢节奏,没有快速的地铁,没有好玩的游乐场,有的只是街坊邻居的互相寒暄,没有夜晚的灯红酒绿,只有晚上八九点就安静的大街。选择清闲的公务员,那么就要懂的人情世故,还有每个月几千块钱的工资。选择自己感兴趣的,那么就得忍受孤独,经得起自我怀疑要有坚定的勇气。高工资的不用多说了吧,996,007时间就是金钱,加班是常态,通宵也是偶尔。所以没有哪份职业好坏,选择自己合适的,加油奋斗吧!


作者:过了三分之二年河东
来源:juejin.cn/post/7216729979622883389
收起阅读 »

北京十年,来深圳了

离开北京是计划 2013年去的北京,至今整十年,来去匆匆。 几年前就计划好了,赶在孩子上幼儿园之前离开北京,选一个城市定居。 给孩子一个稳定的环境,在这儿上学成长,建立稳定的、属于他自己的朋友圈。人一生中最珍贵的友谊都是在年少无知、天真烂漫的时候建立的。 我们...
继续阅读 »


离开北京是计划


2013年去的北京,至今整十年,来去匆匆。


几年前就计划好了,赶在孩子上幼儿园之前离开北京,选一个城市定居。


给孩子一个稳定的环境,在这儿上学成长,建立稳定的、属于他自己的朋友圈。人一生中最珍贵的友谊都是在年少无知、天真烂漫的时候建立的。


我们希望孩子从他有正式的社交关系开始-幼儿园阶段,尽早适应一个省市的教育理念和节奏,不要等到中小学、甚至高中阶段突然的打断孩子的节奏,插班到一个陌生的班级。他同时要面临环境和学业的压力,不是每个孩子都能很快调整过来的。


我自己小学阶段换了好几次学校,成绩的波动很明显,不希望孩子再面临同样的风险。


另一方面,基于我们年龄的考虑,也要尽快离开,岁数太大了,换城市跳槽不一定能找到合适的岗位。


19年,基于对移动端市场的悲观,我开始考虑换一个技术方向。2020年公司内转岗,开始从事图形相关技术开发,计划2023年离开北京,是考虑要留给自己3年的时间从零开始积累一个领域的技术。


来深圳市是意外


这几年一直在关注其他城市的"落户政策"、"互联网市场"、"房价"、"政府公共服务"。有几个城市,按优先级:杭州、广州、武汉、深圳。这些都是容易落户的城市,我们想尽快解决户口的困扰。


看几组数据:




2023年5月份数据


可以看到,杭州的房价排在第6位,但是收入和工作机会排进前4,所以首选杭州,性价比之王。


广州的房价和工作收入都排第5,中策。


武汉的工作机会排进前10,但是房价在10名开外,而且老家在那边,占尽地利,下策。


深圳的房价高的吓人,和这个城市提供的医疗、教育太不匹配,下下策。


最后选择深圳是形势所逼,今年行情史上最差,外面的机会很少。我和老婆都有机会内部转岗到深圳,所以很快就决定了。


初识深圳


来之前做了基本的调研,深圳本科45岁以内 + 1个月社保可以落户。我公司在南山,老婆的在福田,落户只能先落到对应的区。


我提前来深圳,一个星期租好了房子,确定了幼儿园。


老婆步行15分钟到公司,孩子步行500米到幼儿园,我步行 + 地铁1小时到公司。


福田和南山的教育资源相对充足,有些中小学名校今年都招不满,租房也能上,比龙华、宝安、龙岗等区要好很多。


听朋友说,在龙华一个很差的公立小学1000个小孩报名,只有500个学位。


有不少房东愿意把学位给租户使用,办理起来也不麻烦,到社区录入租房信息即可。和北京一样,采取学区划分政策,按积分排名录取,非常好的学校也要摇号碰运气。


租房


中介小哥陪我看了三四天房子,把这一片小区都看了个遍。考虑近地铁、近幼儿园、有电梯、装修良好等因素。


我本来想砍200房租,中介说先砍400,不行再加。结果我说少400,房东直接说好。我原地愣住了,之前排练的戏份都用不上了,或许今年行情不好,租房市场也很冷淡吧。


小区后面是小山,比较安静。


小区附近-0


小区附近-1


小区附近-2


小区附近-3


外出溜达,路过一所小学


深圳的很多小区里都有泳池
小区-泳池


夜晚的深圳,高楼林立,给人一种压迫感,和天空格格不入。明亮的霓虹灯,和北京一样忙碌。


晚上8点的深圳


晚上10点的深圳


对教育的看法



幸运的人一生都被童年治愈,不幸的人一生都在治愈童年--阿德勒



身边的朋友,有不少对孩子上什么学校有点焦虑,因为教育和高考的压力,有好友极力劝阻我来深圳。我认为在能力的范围内尽力就好,坦然面对一切。


焦虑是对自己无能为力的事情心存妄念。 如果一个人能坦然面对结果,重视当下,不虚度每一分每一秒,人生就不应该有遗憾。人生是来看风景的,结局都是一把灰,躺在盒子里,所以不要太纠结一定要结果怎么样。


学校是培养能力的地方,学历决定一个人的下限,性格和价值观决定上限,你终究成要为你想成为的人,不应该在自我介绍时除了学历能拿出手,一无是处。


不少人不能接受孩子比自己差。可是并没有什么科学依据能证明下一代的基因一定优于上一代吧,或许他们只是不能接受孩子比他们差,他们没有面子,老无所依。我天资一般,我也非常能接受孩子天资平庸,这是上天的旨意。


有些父母根本没有做好家庭教育,试图通过卷学校、一次性的努力把培养的责任寄托于学校。挣钱是成就自己,陪伴是成就孩子,成功的父母居中取舍。


陪伴是最好的家庭教育,如果因为工作而疏忽了孩子,我认为这个家庭是失败的,失败的家庭教育会导致家庭后半生矛盾重重,断送了全家人的幸福。


一个人缺少父爱,就缺少勇敢和力量,缺少母爱就缺少细腻与温和,孩子的性格很容易不健全。除非他自己很有天赋,能自己走出童年的阴影。


因为他长大面对的社会关系是复杂的,他需要在性格层面能融入不同的群体。性格不健全的孩子更容易走向偏激、自私、虚伪、或者懦弱,很多心理学家都是自我治疗的过程中,成为心理学大师。


一个人的一生中,学历不好带来的困扰是非常局部的,但是性格带来的问题将困扰其一生,包括工作、交朋结友、娶妻生子,并且还会传染给下一代。


榜样是最好的教育方法,没有人会喜欢听别人讲大道理,言传不如身教。有些人自己过的很可怜,拼命去鸡娃,那不是培养孩子,那是转移压力,过度投资,有赌棍的嫌疑。你自己过的很苦逼,你如何能说服孩子人生的意义是幸福?鸡娃的尽头是下一代鸡娃。


你只有自己充满能量,积极面对人生,你的孩子才会乐观向上;你只有自己持续的阅读、成长,你的孩子才会心悦诚服的学习;你只有自己做到追求卓越,你的孩子才会把优秀当成习惯。


不要给孩子传递一种信号,人生是苦的,要示范幸福的能力,培养孩子积极地入世观。


作者:sumsmile
来源:juejin.cn/post/7248199693934985272
收起阅读 »

2023年,28岁技术人关于职业和生活的思考

2023.9.29中秋节早晨,4个月的宝宝在旁边陪伴着我,记录了28岁一路以来的变化与成长。 关于我 5年前端,现定级为中级前端工程师,有一个很可爱的儿子,小名甜筒🍦,同时还有一个富有智慧的妻子,没错!我用了智慧一词,因为她在能力、为人处事、情绪管控以及子女...
继续阅读 »

2023.9.29中秋节早晨,4个月的宝宝在旁边陪伴着我,记录了28岁一路以来的变化与成长。



关于我


5年前端,现定级为中级前端工程师,有一个很可爱的儿子,小名甜筒🍦,同时还有一个富有智慧的妻子,没错!我用了智慧一词,因为她在能力、为人处事、情绪管控以及子女教育方面优于一般人,这是我的荣幸~


职业发展


关于中级这个头衔,我并不是很满意,在我的规划中,五年作为一个成长的阶梯,仅仅在前端这个领域,应该是要成长为高级开发,毕竟人生能有几个黄金五年呢?


晋升


在我前三年的工作经历中,的确走了一些坎坷,在第二年的时候就已经担任了某私企前端leader的角色,在技术并不是特别拔尖的情况下去做管理工作,属实有点困难,毕竟要兼顾的东西多了,难免有些手忙脚乱,加上当时作为一个技术人的性格胜任这份工作,在沟通上还是吃了蛮多亏,说白了就是需要情商,即做事让人感动,说话让人舒服。关于前半句,我是感动了自己,而后半句呢,虽然没有拉仇恨,但还是差强人意!


感悟


当然了,犯错伴随着成长,在我看来这个过程中最大的收获大于自己的情绪管理思维方式提升。


作为技术人内敛的性格在遇到问题时容易产生自我怀疑,内耗随之产生,而《非暴露沟通》给了我很多启发,不管是在工作或者家庭关系处理中增加了不少润滑油,以至于后面认识我的朋友给我贴了标签:情绪稳定逆商高


其实,更大的成长在于第四年,2022~2023年经历了父亲病痛、职业生涯选择和组建家庭。


2022年初,我背上了房贷,那时候的薪资不高,只有16k左右,而房贷是一个非常吉利的数字:6666,与此同时父亲生病几个月期间让我意识到自己抗风险能力太低,加上自己除了在技术领域关注甚多,对于其他方面一窍不通,谈时事新闻,我不关注;谈房子车子,我不懂;谈商业逻辑,那真是咸菜炖豆腐,不必多言!


于是对自己的职业生涯重新定位,除了技术管理这条路,我还能够做什么?对于家人,我又有多少时间陪伴?最终,我选择了离职,在一年空窗中,我尝试了其他非技术领域的项目,跟客户谈合作,跟人打交道,到处出差谈客户,而正是在这段经历中,我懂得了几点:


人性不可挑战:利益能够驱使人变的险恶,有些人为了金钱,不断打破自己的原则,触碰别人的底线,最后两败俱伤!


人的一生都在为自己的认知买单:我在房价最高的时候选择了入手,是因为听信于别人说现在不买以后房价更高了啊,谁谁谁买了不久之后涨了xxx倍啊,在没有多选择对比几个楼盘之后就入手,利率5.85%,同时还被忽悠可以返佣金,大概有三四万,当时还眼前一亮,但只是口头约定,并没有白纸黑字立据,随后就比较随意签了一大堆合同,其中包括首付贷利率10%,同时销售承诺免3年物业费,猴子吞大象,亏他张得开嘴,唯独就这次办事没有录音,最后签合同时一口咬定没有这回事,吃了哑巴亏;还有补充协议,写着房产证要在交房后720天之后才能拿到,这是最致命的,真是谢天谢地谢广坤


与此同时,做技术也是一样,某个阶段因为自身技术能力、认知缺陷在技术选型或方案选择上存在误差,在一段时间之后产生了历史债务,以往业务在小程序框架选型上就栽了跟头,在没有做过多的调研之后选择了一个组内成员比较熟悉的框架,在后面的业务发展中逐渐显得鸡肋,如不支持多端,框架出现断崖式更新,社区不活跃等等问题,所以在技术选型方面,我们需要考虑包括但不限于以下因素:业务发展方向、行业解决方案对比、解决了什么问题、组内成员的熟悉度、社区活跃度等等。


重回技术


看了不少行业,见了不少人,对于一些思维框架也在实际中得到验证,掌握一些思维模型在解决工作生活问题更加快捷,比如MECE法则结构化思维透过现象看本质逻辑链思维等等,站在更高的视角看问题,我重新选择技术道路,在互联网大裁员环境下,基础薪资涨幅也超过40%。着力于以下几点:



  • 对于接触新的技术,一上来撸文档或看视频,是一种低效并且不持久的学习方式,零碎的知识点就像是每一个神经元,相互没有连接成网络最终会形成一盘散沙,大脑并不擅长处理这种结构。而要多方面去渗透理解,如发展历史,着重解决哪些问题,相比其他类似技术,优势在哪?从这些角度入手,容易形成自己的知识框架。例如,在node生态中,express解决了什么问题?之后为什么又诞生孪生兄弟koa,两者有什么区别?后起之秀nest凭借什么能够脱颖而出,成为目前最流行企业级框架之一?按照这个逻辑链,梳理形成属于自己的知识体系。

  • 源动力,一切行动背后都应该有充分的理由。我的努力工作目的就是在大城市有属于自己的家,想让家人过得更好,让下一代起点高一些,同时摆脱原生家庭因素的影响。在我看来,即便做最基础的工作,都应该思考为什么而做。


生活


生活的琐碎会消磨人的意识,磨平人的棱角,正是因为这样,乐观看待未来的心态决定了一个人成长的速度。


就在前一晚还跟妻子争吵了一番,源于我的原生家庭并没有给予她应该有的尊重和对待,受到的不公平对待,感觉到委屈,这是我作为中间人失责的一方面。


过去的一年中,我们遭受了别人的诋毁、否定和质疑,但最后我们还是走向了婚姻的殿堂,还有了一个很乖、很可爱的宝宝,这是我们最大的欣慰。时间从来不语,但它会回答所有问题!


生活的逐渐变好源于家庭每一个成员的共同努力,很不庆幸在我原生家庭中存在一些负能量的人,她们的内耗能够将家庭其他成员的能量都消耗殆尽,当然啦,我无法改变这些人,但可以通过运动、鸡汤和励志电影不断让自己保持高能量状态,同时减少或不接触这类人,愿我们都能够成为高能量场的人。


总结与展望


失败是成功之母,总结是成功之父。复盘是我成长最大的途径,极客时间中也有关于该话题的专栏,推荐一看。同时,在我看来经历、改变是最宝贵的财富,是我心智成熟的一方面,源于《谁动了我的奶酪?》


在不久后的将来,当宝宝看到这篇文章的时候,他可以清楚地知道爸爸的过去以及此时所感所悟,所言所想,为他以后的道路再点亮一颗星星⭐️。


作者:寻找奶酪的mouse
来源:juejin.cn/post/7283749823429247013
收起阅读 »

程序员入行感触二三事

引言 好久没有发感触了,之前一直在做讲师授课,接触了好多入门的程序员,有很多感触,但是在做讲师的时候有时候不方便说,在做开发又开始忙,所以就沉淀下来了,忽然今天收到了之前一个学习的小伙伴的消息,心里有些触动,本人也不是一个特别喜欢发朋友圈的人,但是总感觉想说点...
继续阅读 »

引言


好久没有发感触了,之前一直在做讲师授课,接触了好多入门的程序员,有很多感触,但是在做讲师的时候有时候不方便说,在做开发又开始忙,所以就沉淀下来了,忽然今天收到了之前一个学习的小伙伴的消息,心里有些触动,本人也不是一个特别喜欢发朋友圈的人,但是总感觉想说点啥(矫情了,哈哈),所以写写做一个回顾吧。


编程行业从开始到现在被贴上了很多标签: 幸苦,掉头发,工资高,不愁工作等等,这些有好有坏,但是总结起来大多数人对编程行业的认知是:


1、需要一定的学历,尤其对数学和英语要求很高。


2、工作比较累,加班是便饭。


3、收入很可观,10k轻轻松松。


4、岗位比较多,是一个搞高级技术(嘿嘿嘿,之前一个家长和我聊的)的行业。


当然还有很多,但是就是上面这些认知让好多毕业迷茫、家境一般、工作遇到问题的人,把编程行业作为了一个全新开始的选择。于是,就有了市场,有了市场很快就有了资本,有了资本很快就有了营造焦虑氛围的营销策略,然后就有各种各样在掩盖在光鲜下的问题,又得真的很无奈,那么今天就聊聊吧。


问题


1、社会是你做程序员的第一绊脚石


啥意思,啥叫做社会,这里的社会不是一个群居的结构,而是人情世故(嘿嘿嘿),好多小伙伴是转行过来的,老话说的好,人往高处走,水往低处流,大部分转行的小伙伴不是来自于大家认知当中更好的行业(比如:公务员,医生,律师..嘿嘿嘿,扯远了),甚至编程本行业的也很少(程序员自学的能力还是很不错的),所以大家在学习之前就已经在社会上摸爬滚打了很久,久历人情,好处是好沟通,不好的地方就是真的把人情世故看的比技术更重要了,这一点可能拉低这些小伙伴70%的学习效果,你要明白,程序员这个行业确实也有人情世故,但前提是大家可以在一个水平上,这个水平可以是技术,也可以是职级,但是如果开头就这么琢磨的话,没有一个扎实的编程基础,真的很难立足在这个行业。没有必要的谦让,习惯性的差不多损耗了太多的学习效果了,既然选择编程,首先请把技术学好,哪怕是基础(当然那个行业也会有浑水摸鱼的,但是对于转行的小伙伴来说,概率太低了)


2、学历重要,学力也很重要


编程行业是一个需要终生学习的行业,不论是前端,后端,测试,运维还是其他岗位,如果在做技术就一定需要学习,好多人会说学历不够所以干不了编程,但是在我个人的眼里,学历确实重要,但是并没有完全限制你进入编程行业,因为:


(1)任何行业都是有完整的岗位结构的,需要的高精尖人才是需要的,但是普通的岗位也少不了,编程行业也是如此,有些岗位的学历要求不是很高。


(2)在编程行业除了那些竞争激烈的大厂,自考学历是有一定的市场和认可程度的


但是,在学历背后的学力就不是这样一个概念了,这里想表述的是学习能力,包括:


(1)专注能力,好多小伙伴如果之前有一定的社会经历或者在大学过的比较懒散,在没有聊到学历之前,先决条件就是能静下心来学习,但是很多小伙伴专注力根本不达标,听课走神,练习坐不住...(其实个人感觉任何一个行业,能静下心来做,并且活下来的都不会很差)


(2)学习习惯,这里不贬低学历低的小伙伴,但是不能否认的是,参加高考后获得一个高学历的小伙伴能力不谈,但是99%都有一个很好的学习习惯。比如不会在学习的时候把手机放到旁边,科学的记笔记,有效的复习和预习等等,所以在担心学历之前,请先培养好自己的学习习惯(个人建议,如果真的没有一个好的学习习惯,那么学习的时候就不要在眼前出现多余的东西分散注意力,比如: 课桌上除了听课的电脑,不要有其他的,之前见过的容易分散注意力的:手机,水杯,指尖陀螺,魔方....)


3、不要在没有选择能力的时候做出选择


这里想聊的是一些学习恐慌的小伙伴的惯性,好多小伙伴在选择了一种学习方式(买书,看视频,加入培训班)之后,还会进行类比学习,比如:买了Python的一本基础书,然后再大数据或者小伙伴的推荐下又买了另外一本,或者参加了培训班,又去看其他的教学视频,这些对小白同学的学习伤害会很大,因为,本身对技术没有全面的理解,不同的书,不同的教程传递的教学方法是不一样的,混着来有点像老家喝酒掺着喝,白酒不醉,啤酒不醉,白加啤那么就不一定了(很大概率会醉),所以小白同学最总要的不是再学习的过程当中进行对比,而是可以最快最稳的完成基础感念的学习,在自己脑子当中有了基础概念再做选择。


当然了,还有很多,一次也聊不完,之后有时间再聊吧,今天就先写这么多,欢迎大家讨论交流。


作者:老边
来源:juejin.cn/post/7174259081484763173
收起阅读 »

大厂996三个月,我曾迷失了生活的意义,努力找回中

作为一个没有啥家底的小镇做题家,在去年选Offer阶段时,工作强度是我最不看重的一个点,当时觉得自己年轻、身体好、精神足,996算起来一天不过12个小时,去掉吃饭时间,一天也就9到10个小时,完全没有任何问题,对当时热衷于外企、国企、考公的同学非常的不理解,于...
继续阅读 »

作为一个没有啥家底的小镇做题家,在去年选Offer阶段时,工作强度是我最不看重的一个点,当时觉得自己年轻、身体好、精神足,996算起来一天不过12个小时,去掉吃饭时间,一天也就9到10个小时,完全没有任何问题,对当时热衷于外企、国企、考公的同学非常的不理解,于是毫不犹豫的签了一个外界风评不太佳但是包裹给的相对多的Offer,然后便有了现在的心酸感悟。


入职前的忐忑


在一波三折终于拿到学位证后,怀着忐忑的心入职了。忐忑的原因主要是入职之前我并不知道我要入职什么部门,很怕我一个只会Java的后端被分配去写C++,毕竟Java是最好的语言,打死不学C++(手动狗头)。 也担心被分配到一个没有啥业务复杂度、数据复杂度的业务部门,每天CRUD迎接着产品的一个又一个需求,最后活成了CRUD Boy没有什么技术沉淀。又担心去和钱相关的部门,害怕自己代码出Bug,导致公司产生资损。


就这样忐忑着听完了入职当日上午的培训,中午便被我的mentor接走了,很不幸,我被我的mentor告知,我恰好被分在了和钱最直接相关的部门,我的心陡然沉重起来。


这里给出我最诚挚的建议,选Offer最好选一个实习过的部分,次之就是签了三方后去实习一段时间,如果发现部门的味儿不对或者和自己八字不合,此时还有机会跑路,以很小的代价换一家公司,不然毕业后入职就宛如开盲盒,万一遇到了很不适应的部门,交了社保、几个月的工作经验,到了市场上可谓是爹不疼娘不爱,比着校招时候的境遇差太多了。


熟悉项目的第一个月


得益于和钱相关,我部门的需求都不是很紧急,领导的态度是宁愿需求不上,也不能上出问题。所以每一个需求都没有说产品火急火燎的推动着要上线,都是稳扎稳打的在做,给予了开发比较充足的自测时间。但是呢,另外一方面,由于部门的业务领域比较底层,所以接手的项目往往都已经有了两三年的历史,相信写过代码的同学都知道,写代码的场景中最痛苦的那就是读懂别人的代码然后在别人的基础上进行开发,尤其是读懂几年前已经几经几手的项目代码。


第一个月刚进入公司,只是在熟悉项目代码,没有啥需求上的压力,相对来说还是比较轻松。遇到不熟悉的直接问靠谱的mentor,mentor也给热心的解答还是很幸运的。每天吃完公司订的盒饭。下楼转悠一圈就觉得美滋滋。


这个时候其实觉得,996不过如此嘛,好像也没有啥压力,真不搞不明白有啥可怕的。


进入开发状态的第二个月


在熟悉的差不多后,我就开始慢慢接手业务需求了,坦白的说,由于我接手的项目比较成熟,新接入业务需求往往不需要做什么开发工作,只需要做一些配置项,需求就完成了。 然而呢,作为一个几年的老项目,当然是处处埋的有彩蛋,你永远不知道哪里就会给你来一个惊喜。于是呢,我的工作开始变成,寻找代码中的彩蛋,搞明白各个配置项的含义,以及他们究竟是怎么组合的,然后和上下游联合对数据,发现数据对不上,就需要再埋进项目中一丝一缕的分析。


这个时候已经有些许的压力了,如果因为自己成为整个需求的卡点,那太过意不去了。于是开始每天勤勤恳恳,吃盒饭也没有那么香了,饭后散步的脚步也不再那么的愉悦,这时候开始感受到了肩上的压力。


本来我是坚决第一个离开工位下班,决心整治职场的人,但是往往在debug的路上,不经历的就把下班时间延长了一点又一点。而又由于在北京、上海这种大城市,住在公司旁边往往是一种奢望,导致我每天有较长的通勤时间。工作日一天下来,差不多就是晚上回去睡觉,早上醒来没有多久就出门赶地铁。


日复一日,就有一种流水线上螺丝钉的麻木感,周末往往一觉睡醒就结束了,感觉日子很重复,没有一些自己生活过的痕迹。


努力调整状态的第三个月


积极主动,是《高效能人士的七个习惯》中的第一个习惯,也是我印象最深的一个习惯。既然困难无法克服,那么咱们就要主动解决。
alt


工作中,努力开拓自己的视野,搭理好手中的一亩三分地的同时,仰头看看上游,低头往往下游,对他们的业务也多一些学习,理清楚自己工作的业务价值,同时呢,在当好一名螺丝钉之外,也尝试着找出整个流水线的可优化点和风险点,尝试着给出自己的解决方案,同时积极梳理已有的项目代码的技术难点,是如何通过配置化来应对复杂的业务场景,是如何通过自动重试保证数据一致性。


生活中,周末即使比较累了,但是努力也不再宅在家中,一刷手机一整天,而是尝试着做一些比较有挑战或者更有回忆的事情。比如沿着黄浦江骑行。
alt


比如自己下厨做几个菜


alt


比如邀请三五好友玩个桌游


alt


比如通过图书馆借一些杂书来消遣


alt


对后来人想说的话


部门与部门之间的差异,很有可能比公司之间的都要大,选择Offer尽可能的选一个实习过的、或者比较熟悉的部门,能有效避免开盲盒踩雷的风险。没有绝对完美的公司,即使好评如潮的外企、券商类公司,我仍然有一些不幸运的同学,遇到了很卷的部门,平时要自愿加班或者在公司“学习”。


即使遇到了困境,也需要保持积极良好的心态,退一万步想,即使工作丢了,但是咱们的身心健康不能丢。为了这几斗米,伤了身体,是非常得不偿失的。


在选Offer的时候尽量一步到位,以终为始,如果目标瞄定了二线城市,其实我个人不太建议为了某些原因比如对大厂技术的热衷、对一线城市繁华的向往而选择当北漂沪漂,漂泊在外的日子都比较苦,而且吃这种苦往往是没有啥意义的。



我是日暮与星辰之间,一名努力学习成长的后端练习生,创作不易,求点赞、求关注、求收藏,如果你有什么想法或者求职路上、工作路上遇到了什么问题,欢迎在评论区里和我一起交流讨论。



作者:日暮与星辰之间
来源:juejin.cn/post/7159960105759277070
收起阅读 »

谈谈成长

背景 距离毕业到已经三年多了, 距离实习到现在已经三年半了, 在主管的建议下, 8月17号在公司的研发部门做了 一场关于成长的分享, 从工作方式到技术能力提升对自己三年的成长进行了一个复盘, 幸运的是分享得到的反 馈非常好, 老板看了将录屏在整个研发部进行公布...
继续阅读 »

背景


距离毕业到已经三年多了, 距离实习到现在已经三年半了, 在主管的建议下, 8月17号在公司的研发部门做了
一场关于成长的分享, 从工作方式到技术能力提升对自己三年的成长进行了一个复盘, 幸运的是分享得到的反
馈非常好, 老板看了将录屏在整个研发部进行公布并且推荐大家观看, 这是部门内分享活动以来第一次公示与推荐录
屏的情况, 我自己也感觉非常感慨, 毕业以来, 一直朝着“一年入门, 三年高工、五年资深、七年架构”的目
标前进, 有时候回过头来会发现这一路还是挺有意思的, 借着这样一个平台, 以文字的形式描述出来, 期望
能够给前行路上的各位有一定的帮助


下面的描述中, 为了不暴露个人信息, 对公司名称的描述统一替换为 XX, 涉及到人名的我都会进行打码


01.png


一、为什么做这个分享(why)


1.1、原因一


主管说从实习加入XX三年半以来, 看到我成长了很多, 推荐我可以做一下关于个人成长这一块的分享


1.2、原因二


我个人仔细的想了一下, 三年半前加入XX, 当时公司才400多人, 没有一个正式的java团队(公司主c#), 三年半的时间, 公司发展到了750多人, 我经历了java团队从0到1的完整过程(目前已经有3个java团队, 加起来有接近30个java开发), 并且在这三年半的时间, 公司的后端业务项目开始从.net往java迁移, 有大量的重构项目和新的大型项目, 在此期间, 经历了可能有10个以上从0到1的大型项目的落地, 并且这些项目中我个人承担了绝大部分的核心功能开发, 在这些项目的锻炼下, 不管是技术还是工作方式方面, 都有学习到非常多的知识, 借着这样的机会复盘一下, 继续完善自己, 我主观认为这样的经历可能会有一定的参考价值, 期望能够通过这样的一个分享, 能够给一些可能遇到瓶颈或者跟我遇到一样问题的同学一些启发


二、工作方式上的成长及建议


2.1、需求方案的决策


Pre: 遇到问题 / 需求我应该怎么做, 还没有独当一面的能力


Now: 遇到问题 / 需求我思考可以怎么做, 并从自己的角度出发提出方案, 标识每个方案的优缺点, 询问应该怎么做(即使提出的方案都不是最优解, 但是有自己的思考, 并且在遇到更合适的方案时能有一个对比学习)


截图是两个例子, 在我实现功能的时候, 有不同的方案, 在把握不准哪种方案比较合适的情况下, 向主管进行询问, 对可行的方案的优缺点进行分析, 在主管多年的经验下确定最终的实施方案


02.png


03.png


2.2、需求功能的实现和上线


Pre: 需求处理完, 简单的自测或者压根就不自测, 测试流程走完以后发上线就不管了(在工作过程中其实有遇到许多同事也是这样), 在刚来实习以及刚毕业那段时间, 一个功能编码完以后测试反馈了许多的bug问题, 改了一个旧的出现一个新的, 之前有听过一个段子, 测试听到开发说的最多的是什么(我没改代码 / 网络问题 / 你再试试), 这个段子就在我身上出现了, 当时改一个功能, 连续几次都没改好(因为改完后没有充分验证就提测了), 见下图(2021年的聊天记录)


Now: 需求处理完, 充分的自测, 提测以后多次跟进测试同学的测试情况, 上线后跟进线上是否正常使用, 找产品进行验收流程


Suggest: owner意识、需求从哪里开始就从哪里结束, 回调产生事件的人, 形成闭环


owner意识是主管在小组中提出来的一个工作方式, 是说一个开发, 在多人协作的场景下, 不能只关注自己那一块的功能, 需要对整个流程有充分的了解, 以一个owner的角色参与到项目开发中, 跟进其他端的对接, 把控整个项目的进度, 要做到这个其实是很难的, 需要花费更多的精力, 但是一旦做到了, 对整个项目就有一种非常清晰的感觉, 能够更好的完成多人协作项目的落地, 需求从哪里开始就从哪里结束, 每次一个需求处理完以后, 我会严格按照 自测 -> 提测 -> 联调 -> 督促产品验收 -> 灰度环境发布及验收 -> 正式环境发布及验收 -> 同步所有有关开发自己的功能的发布情况


在这样一个闭环的链路中, 功能的落地和稳定有了非常明显的提高


04.png


2.3、句句有回应, 事事有着落


Pre: 忙的时候忘记回消息, 事情多的时候忘记一些临时分配的事情, 遇到的坑重复跳


Now: todoList、QQ置顶, 多检查(防止多次返工), 防止重复的问题重复出现(错别字)


todoList:
我是通过一个txt文件来进行一个记录的(当然有更合适的, 只是用习惯了), 会记录手上的需求有哪些, 每一个的进度是怎么样的, 上线流程是怎么样的, 我给自己定的上线流程中分为以下几步, 并且在上线的过程中严格的按照下面的流程进行操作, 这样极大的提高了上线的稳定性(特别是一些大版本的发布, 有这样的严格流程下, 稳定性有了非常明显的提高)


1、是否有旧数据需要处理


2、是否有sql脚本需要执行


3、是否依赖于其他人的功能


4、是否有单独服务器灰度的需求, 有些业务需要单独一台服务器来进行灰度, 然后通过nginx将部分流量转发过来验收


5、开始灰度, 灰度后产品验收, 全部负载发布完毕后回调需求开始的人, 形成闭环


QQ置顶: 我们公司是采用QQ作为沟通工具的(可能是因为很早之前就是用这个, 即使现在有企业微信, 但是沟通这一块大家都不太倾向于切到企业微信), 当在外面或者遇到其他比较紧急事情时有人找或者发消息, 可能看完就忘记回了, 于是我养成一个习惯, 有人发消息没时间回的, 第一时间置顶, 等有空后再来跟进, 跟进完后取消置顶


防止重复的问题重复出现: 我们需要写周报, 刚开始的时候有出现错别字、格式不正确、周报字数过多的情况(主管规定周报不能超过250个字), 后面为了避免这种情况, 我每次写完都会阅读两遍内容, 防止之前产生的问题重复发生, 每个人都有粗心的时候, 也不能保证不出现错误, 但是我们可以通过一些方式方法来尽量的避免错误的重复发生


2.4、学习别人优秀的地方


人无完人, 比如我有时候说话会比较直接(好吧, 我是直男....), 那时主管就让我在沟通这一块多跟斜对面的同事学习一下, 后面我就观察他的沟通方式, 然后自己从模仿开始, 慢慢的也学会了怎么进行有效的沟通、委婉的拒绝等, 学习别人优秀的地方也是成长中比较重要的一环


2.5、批量接口的重要性


Pre: 对需求的时候遇到一个问题就问一次, 效率低, 刚来实习的时候, 不熟悉业务, 当时接触到一块比较复杂的涉及到购物车优惠计算的业务, 当时跟对方讨论的时候, 遇到一个不懂的就直接去问, 多次打断他的工作, 效率非常低


Now: 对整个需求进行梳理, 整理所有的疑问一次性请教, 效率提高了很多, 也不会容易打断别人的工作(反到现在我经常被别人打断=,=看到了曾经的我....)


2.6、做的事太简单, 没有挑战性


今年带了一个实习生, 刚开始给他分配的工作都是比较简单的, 主要是修bug、小需求, 但是即使是这样简单的功能, 他做的也是磕磕碰碰, 比如代码规范不达标、空指针异常判断不全面、业务涉及到的点想的不全面等等许多瑕疵, 多次需要我来进行一个兜底, 后面他跟HR反馈说太简单没有挑战性, 于是我就分配了一个稍微大的一点需求给他, 结果做的惨不忍睹, 后面他实习结束时这个需求才做到一半, 我转给其他同事帮忙接手....结果那个同事看到代码后对我吐槽了许久....


当时收到反馈后, 我及时的找他进行沟通, 我的观点是他做的每一个简单的需求, 需求的完整稳定上线都是为了给主管建立一个做事靠谱的印象, 如果做事不靠谱, 经常需要他人来进行兜底, 那么谁也不敢把复杂重要的任务交给你做, 公司很多业务都涉及到商家的钱, 一旦这种重要的业务出问题, 那么会给公司造成巨大损失, 其实仔细想想, 自己实习那会也差不多, 想要做很厉害的项目, 用很流行的技术, 但是如果自己给人的感觉是不靠谱的, 那么主管自然就不敢把这些项目交给我了, 明白了这一点后, 当时我毕业一年给主管的目标是一个需求功能下来, 不管大小, 测试反馈的bug不能超过3个, 一个月产生的线上事故不能超过1次, 我努力做到了这一点, 线上事故几乎没发生过, 随之而来的是小组中绝大部分核心的业务、基础组件的开发都由我来处理, 以及一波大的涨薪


2.7、内卷


维基百科: 原是一个社会学概念,指一种文化模式发展到一定水平后,无法突破自身,只能在内部继续发展、复杂化的过程。大约从2018年开始,“内卷”一词在中国大陆变得广为人知,并引申表示付出大量努力却得不到等价的回报,必须在竞争中超过他人的社会文化,包含了恶性竞争、逐底竞争等更为负面的含义。


我们公司965几乎不加班, 所以我们6点会有比较充足的时间, 我一般去楼下吃完饭以后就会回到公司学习, 三年半的时间阅读了大量的源码书籍、学习了许多知识, 个人认为, 深入学习技术, 提高自己其实不属于内卷, 无意义的加班, 内耗等才是内卷, 其他小组也有人说我比较卷, 但是我们小组(包括主管)都是知道我晚上下班后是在公司学习的, 也很少会加班, 借着分享的机会我也跟大家澄清了这样的情况, 并且我们小组的氛围并没有因为我下班后的学习而导致整个小组都晚下班的情况(公司7点左右就基本空了.....)


三、技术能力上的成长及建议


3.1、如何学习


一、我学习一门未接触过的技术时, 会先看视频学习, 建立基本认识, 并且能够从讲师身上学到一些经验, 即先学会简单的使用
二、在了解了基本使用, 并且用起来的情况下, 我会查找跟该技术有关的权威书籍, 对权威书籍的学习是为了建立完整的知识体系


这个学习方式是我从大学以来就保持的, 而我认为这也是对我来说是最合适的学习方式


3.2、打地基-基础知识的重要性


基础知识对一个程序员来说是非常重要的, 个人认为基础越扎实的同学往往在学习技术的时候会吸收的更快, 并且也能够走的更远


一、数据结构, 我学习数据结构的时候, 会手写每个数据结构, 即使是最难的红黑树, 我也手写出来了(当时在大学的时候花了一个下午就为了写一个新增节点和删除节点的方法), 对于数据结构的学习, 我推荐: 恋上数据结构 这套视频, 讲的非常好, 大家如果有兴趣的话可以各显神通的去找找=,=


二、计算机网络, 计算机网络我是通过看视频加书籍的方式来学习的, 视频推荐: 韩立刚, B站就能搜到, 讲的通俗易懂, 我推荐了几个朋友看, 都反馈非常棒, 书籍推荐 计算机网络 第六版(考研408专用)


三、操作系统, 对操作系统的学习, 能够让我们在了解JVM、以及一些底层知识的时候(比如CAS、volatile、synchronize等原理)能够更加的顺利, 他们都是依赖于操作系统相关的知识来的, 视频我推荐: 哈工大的计算机操作系统, B站能搜到, 书籍推荐计算机操作系统(考研408专用)


四、汇编语言, 如果有看过深入理解Java虚拟机这本书, 那么里面就有出现跟汇编相关的话术, 如果对汇编有所了解, 能够亲身的体验到寄存器操作、中断的原理等, 这些在学习操作系统等知识的时候必然会遇到的话术, 视频我推荐推荐: 小甲鱼, B站就能搜到, 书籍我推荐(汇编语言(第3版) 王爽)


五、设计模式, 刚开始写代码的时候, 会一个方法写很多逻辑, 就像流水账一样, 一直写下去, 没有考虑复用等情况, 通过学习设计模式, 我们可以写出更加优雅的代码, 模板方法、单例、工厂等模式的使用能够使得我们的代码阅读性更高、扩展性更强, 学会了设计模式的情况下, 再去看自己之前写的代码就会发现还能写的更好! 并且有了这个知识的基础上, 我们去看一些框架源码的时候会更加顺利, 框架源码用到设计模式的时候命名都是通俗易懂的, 看到名字就知道用了什么模式, 就像程序员之前互相沟通一样, 这个我没有看视频, 我看的是 HeadFirst设计模式 这本书籍, 通过一些生动形象的例子, 把设计模式讲活了...


3.3、创造核心竞争力-不停留在只会用的地步


java开发往往离不开spring的生态系统, 框架开发出来就是给人更加方便开发功能用的, 如果仅仅会用, 那么在遇到一些问题的时候会无从下手, 三年半的时候, 我阅读了spring、springmvc、mybatis、springboot、springcloud等框架的源码, 通过书籍加视频的方式深入的了解了这些框架的原理, 看这些框架源码的时候, 不纠结于一些边线知识, 只管主线流程, 了解主线流程后, 我发现后续遇到问题时, 我能非常自信的跟进源码去排查问题, 在第四章节中我会整理每一个框架我都是通过哪些书籍来深入学习的


3.4、学以致用-尝试输出(github / 博客 / 分享)


学习一个知识, 如果仅仅看了一遍书 / 看了一遍视频, 那么可能过几天就会忘记了, 一般我是通过看视频 -> 记笔记 -> 看书 -> 对书中的知识点进行整理笔记, 笔记采用类似于给他人讲解的方式来记录 -> 将笔记记录在github 或者 以博客的形式分享出来, 在这样的链路下, 我每一步都能更加深刻的学习到知识点, 有时候看书看懂了不代表真懂了, 真正用笔记来描述的时候会发现是磕磕碰碰的, 与此同时, 将这些磕磕碰碰的知识去再次学习, 那么对整个知识点就会有更加全新的认识, 大家也可以看到, 我的掘金的博客是从2020年就开始写了, 都是我个人的口头描述转为文字描述


3.5、有枪不用和无枪可用


在掌握了工作中需要的知识点的情况下, 我们需要去学习流行的技术, 防止自己落伍, 技术的迭代更新是非常快的, 学习这些技术, 往往会给自己带来意想不到的结果


一年前公司我深入的去研究了eureka、zuul等springcloud组件的原理, 后面幸运的是, 公司有一个私有化部署的项目, 主管的计划是用微服务来搭建, 这个项目需要考虑到客户的资源,
有些客户可能预算比较高, 我们就可以提供一套完整的微服务来运行, 有些客户预算比较低, 那么可能最多就跑3-4个java项目, 于是主管的要求是我们的微服务功能, 需要能够满足上述的情况,
能够非常方便的将一个或者多个服务合并成一个服务, 并且自由搭配


正是因为我有对这些组件的深入了解, 我从源码层次提供了一套实现方案, 并且是最简单的实现方案, 主要的原理就是控制bean的加载(打包的时候一起打包, 但是不加载到内存)以及内部
rpc调用时的扩展(利用回环地址来尽可能的忽略http请求的花销), 如果我没有对这一块有所掌握, 那么我可能就失去了这样一个非常好的锻炼机会了


四、从成长的曲线来看侧重点


05.png


作者:zhongshenglong
来源:juejin.cn/post/7277489569958936588
收起阅读 »

茶百道全链路可观测实战

作者:山猎 茶百道是四川成都的本土茶饮连锁品牌,创立于 2008 年 。经过 15 年的发展,茶百道已成为餐饮标杆品牌,全国门店超 7000 家,遍布全国 31 个省市,实现中国大陆所有省份及各线级城市的全覆盖。2021 年 3 月 31 日,在成渝餐·饮峰会...
继续阅读 »

作者:山猎


茶百道是四川成都的本土茶饮连锁品牌,创立于 2008 年 。经过 15 年的发展,茶百道已成为餐饮标杆品牌,全国门店超 7000 家,遍布全国 31 个省市,实现中国大陆所有省份及各线级城市的全覆盖。2021 年 3 月 31 日,在成渝餐·饮峰会中,茶百道斩获“2021 成渝餐·饮标杆品牌奖”。2021 年 8 月,入选艾媒金榜(iiMedia Ranking)最新发布《2021 年上半年中国新式茶饮品牌排行 Top15》。2023 年 6 月 9 日,新茶饮品牌“茶百道”获得新一轮融资,由兰馨亚洲领投,多家知名投资机构跟投,估值飙升至 180 亿元。


今年 4 月,茶百道在成都总部举行了品牌升级发布会,宣布门店数突破 7000 家。根据中国连锁经营协会的数据,截至 2020 年、2021 年以及 2022 年 12 月 31 日,茶百道门店数量分别为 2,240 间、5,070 间以及 6,532 间,疫情并没有拖慢其扩张步伐。


随着业务规模的急速扩展,茶百道全面加速推进数字化转型战略。 但由于茶百道部分早期业务系统由外部 SaaS 服务商提供,无法满足线上业务高速增长所带来的大规模、高并发、弹性扩展、敏捷性、可观测等要求。为了满足线上线下门店客户需求与业务增长需要,针对店务、POS、用户交易、平台对接、门店管理、餐饮制作等核心链路服务,茶百道选择全面自研与阿里云云原生能力相结合,推动容器化、微服务化、可观测能力全面升级。


云原生化的业务价值


茶饮行业面临着市场竞争的压力和内部运营效率的提升需求。为了应对这些挑战,阿里云与茶百道一起完成云原生上云的转型,开启数字化的新征程。


采用容器和微服务技术实现了应用的轻量化和高可移植性。让企业可以更灵活地部署、扩展应用,快速响应市场需求,使得企业能够实现应用的高可用性和弹性扩展能力,无论面对突发的高峰访问量还是系统故障,都能保持业务的稳定运行。


引入了持续交付和持续集成的开发方式,帮助企业实现了快速迭代和部署。通过自动化的流程,企业能够更快地推出新功能和产品,与市场保持同步,抢占先机。


云原生的上云转型不仅带来了更高的安全性、可用性和可伸缩性,也提升了企业的创新能力和竞争力。


云原生带来的可观测挑战


茶百道作为业务高速发展的新兴餐饮品牌,每天都有海量的在线订单,这背后是与互联网技术的紧密结合,借助极高的数字化建设支撑茶百道庞大的销售量。因此,对于业务系统的连续性与可用性有着非常严苛的要求,以确保交易链路核心服务的稳定运行。特别是在每日高峰订餐时段、营销活动、突发热点事件期间,为了让用户有顺畅的使用体验,整个微服务系统的每个环节都需要保证在高并发大流量下的服务质量。


完善的全链路可观测平台以及 APM  ( Application Performance Management )工具,是保障业务连续性与可用性的前提。在可观测技术体系建设上,茶百道技术团队经历过比较多探索。全面实现容器化之前,茶百道在部分微服务系统上接入了开源 APM 工具,并进行超过一年时间的验证,但最终没有能够推广到整个微服务架构中,主要有这几个方面的原因:




  • 指标数据准确度与采样率之间的平衡难以取舍


    适当的采样策略是解决链路追踪工具成本与性能的重要手段,如果 APM 工具固定使用 100% 链路全采集,会带来大量重复链路信息被保存。在茶百道的庞大微服务系统规模下,100% 链路采集会造成可观测平台存储成本超出预期,而且在业务高峰期还会对微服务应用本身的性能带来一定影响。但开源工具在设定采样策略的情况下,又会影响指标数据准确度,使错误率、P99 响应时间等重要可观测指标失去观测与告警价值。




  • 缺少高阶告警能力


    开源工具在告警方面实现比较简单,用户需要自行分别搭建告警处理及告警分派平台,才能实现告警信息发送到 IM 群等基本功能。由于茶百道微服务化后的服务模块众多、依赖复杂。经常因为某个组件的异常或不可用导致整条链路产生大量冗余告警,形成告警风暴。造成的结果就是运维团队疲于应付五花八门且数量庞大的告警信息,非常容易遗漏真正用于故障排查的重要消息。




  • 故障排查手段单一


    开源 APM 工具主要基于 Trace 链路信息帮助用户实现故障定位,对于简单的微服务系统性能问题,用户能够快速找到性能瓶颈点或故障源。但实际生产环境中的很多疑难杂症,根本没有办法通过简单的链路分析去解决,比如 N+1 问题,内存 OOM,CPU 占用率过高,线程池打满等。这样就对技术团队提出了极高要求,团队需要深入了解底层技术细节,并具备丰富 SRE 经验的工程师,才能快速准确的定位故障根源。




接入阿里云应用实时监控服务 ARMS


在茶百道系统架构全面云原生化的过程中,茶百道技术团队与阿里云的工程师深入探讨了全链路可观测更好的落地方式。


ARMS 应用监控作为阿里云云原生可观测产品家族的重要成员,提供线程剖析、智能洞察、CPU & 内存诊断、告警集成等开源 APM 产品不具备的能力。在阿里云的建议下,茶百道技术团队尝试着将一个业务模块接入 ARMS 应用监控。


由于 ARMS 提供了容器服务 ACK 环境下的应用自动接入,只需要对每个应用的 YAML 文件增加 2 行代码就自动注入探针,完成整个接入流程。经过一段时间试用,ARMS 应用监控提供的实战价值被茶百道的工程师不断挖掘出来。茶百道同时使用了阿里云性能测试产品 PTS,来实现日常态和大促态的容量规划。因为ARMS和 PTS 的引入,茶百道日常运维与稳定性保障体系也发生了众多升级。


围绕 ARMS 告警平台构建应急响应体系


由于之前基于开源产品搭建告警平台时,经常遇到告警风暴的问题,茶百道对于告警规则的配置是非常谨慎的,尽可能将告警目标收敛到最严重的业务故障上,这样虽然可以避免告警风暴对 SRE 团队的频繁骚扰,但也会让很多有价值的信息被忽略,比如接口响应时间的突增等。


其实对于告警风暴问题,业界是有一整套标准解法的,其中涉及到去重、压缩、降噪、静默等关键技术,只是这些技术与可观测产品集成上存在一定复杂度,很多开源产品并没有在这个领域提供完善方案。


这些告警领域的关键技术,在 ARMS 告警平台上都有完整功能。以事件压缩举例,ARMS 提供基于标签压缩和基于时间压缩两种压缩方式。满足条件的多条事件会被自动压缩成为一条告警进行通知(如下图所示)。


图片
图: 基于标签压缩


图片
图:基于时间压缩


配合 ARMS 告警平台所提供的多种技术手段,可以非常有效的解决告警风暴的问题,因此茶百道技术团队开始重视告警的使用,逐步丰富更多的告警规则,覆盖应用接口、主机指标、JVM 参数、数据库访问等不同层面。


通过企业微信群进行对接,使告警通知实现 ISTM 流程的互动,当值班人员收到告警通知后,可以直接通过 IM 工具进行告警关闭、事件升级等能力,快速实现告警处理。(如下图所示)


图片
图:监控告警事件的智能化收敛与通告


灵活开放的告警事件处置策略满足了不同时效、场景的需求。茶百道在此基础上参考阿里巴巴安全生产最佳实践,开始构建企业级应急响应体系。将业务视角的应急场景作为事件应急处置的核心模型,通过不同告警级别,识别与流转对应的故障处理过程。这些都是茶百道在全面云原生化后摸索出的经验,并显著提升生产环境服务质量。


引入采样策略


从链路信息中提取指标数据,是所有 APM 工具的必备功能。不同于开源产品简单粗暴的指标提取方式,ARMS 应用监控使用端侧预聚合能力,捕捉每一次真实请求,先聚合,后采样,再上报,提供精准的指标监控。确保在采样策略开启的情况下,指标数据依然与真实情况保持一致。


图片
图:ARMS 端侧预聚合能力


为了降低 APM 工具带来的应用性能损耗,茶百道对大部分应用采取 10% 采样率,对于 TPS 非常高的应用则采取自适应采样策略,进一步降低高峰期应用性能损耗。通过实测,在业务高峰期,ARMS 应用监控造成的应用性能损耗比开源产品低 30% 以上且指标数据准确性可信赖, 比如接口级别的平均响应时间、错误数等指标都可以满足生产级业务需求。


图片
图:接口级别指标数据


异步链路自动埋点*


在 Java 领域存在异步线程池技术,以及众多开源异步框架,比如 RxJava、Reactor Netty、Vert.x 等。相较于同步链路,异步链路的自动埋点与上下文透传的技术难度更大。开源产品对主流异步框架的覆盖度不全,在特定场景下存在埋点失败问题,一旦出现这样的问题,APM 工具最重要的链路分析能力就难以发挥作用。


在这种情况下,需要开发者自行通过 SDK 手工埋点,以保证异步链路的上下文透传。这就会造成巨大的工作量且难以在团队内部大面积、快速推广。


ARMS 对主流的异步框架都实现了支持,无需任何业务代码上的侵入就能够异步链路上下文透传,即使对一些异步框架的特定版本没有及时支持,只要用户侧提出需求,ARMS 团队就能在新版本的探针中补齐。使用 ARMS 应用监控之后,茶百道技术团队直接将此前异步框架手工埋点代码进行了清理,大幅度减少维护工作量。


图片


图:异步调用的链路上下文


更高阶应用诊断技术的运用


在埋点覆盖度足够高的情况下,传统 APM 工具和链路跟踪工具能够帮助用户快速确定链路的哪一个环节(也就是Span)存在性能瓶颈,但需要更进一步排查问题根源时,就无法提供更有效的帮助了。


举一个例子,当系统 CPU 占用率显著提升时,是否因某个业务方法疯狂的消耗 CPU 资源所导致?这个问题对于大多数的 APM 产品而言,都是难以办法解决的。因为单从链路视图无法知晓每个环节的资源消耗情况。茶百道的工程师在使用开源工具时,曾多次遇到类似问题,当时只能凭借经验去猜测,再去测试环境反复对比来彻底解决,虽然也试过一些 Profiling 工具,但使用门槛比较高,效果不是很好。


ARMS 应用监控提供了 CPU & 内存诊断能力,可以有效发现 Java 程序中因为 CPU、内存和 I/O 导致的瓶颈问题,并按照方法名称、类名称、行号进行细分统计,最终协助开发者优化程序、降低延迟、增加吞吐、节约成本。CPU & 内存诊断可以在需要排查特定问题时临时开启,并通过火焰图帮助用户直接找到问题根源。在一次生产环境某应用 CPU 飙升场景中,茶百道的工程师通过 CPU & 内存诊断一步定位到问题是由一个特定业务算法所导致。


图片
图:通过火焰图分析 CPU 时间


此外,对于线上的业务问题,还可以通过 ARMS 提供的 Arthas 诊断能力在线排查。Arthas 作为诊断 Java 领域线上问题诊断利器,利用字节码增强技术,可以在不重启 JVM 进程的情况下,查看程序运行情况。


虽然 Arthas 使用有一定门槛,需要投入比较多精力进行学习,但茶百道的工程师非常喜欢使用这个工具。针对“到底符合哪种特殊的数据导致某业务异常”此类问题,没有比 Arthas 更方便的排查工具了。


图片


阶段性成果


经过 2 个月时间的调研与对比,茶百道决定全面从开源可观测平台转向 ARMS,从开源压测平台转向 PTS,并在团队内部进行推广。**随着使用的不断深入,ARMS 所提供的智能洞察、线程池分析等高阶可观测能力也逐步被茶百道的技术团队应用于日常运维中,线上问题排查效率相比之前也有了数倍提升。


在可观测产品本身的使用成本上,虽然表面上 ARMS 相比开源产品有所提高,但这是建立在开源方案数据单写,以及存在单点故障的情况下。其实茶百道的技术团队也非常清楚,之前的开源方案是存在高可用性隐患的,某个组件的故障会导致整个可观测方案不可用。只是大家对于开源方案提供的可观测能力并没有重度使用,所以才没有足够重视。所以综合来看,ARMS 整体成本并不会高于开源方案。


利用 ARMS 能力,茶百道实现了可观测指标采样率百分百覆盖,链路全采集,监控数据准确率大幅提供,能够快速实现业务故障的自动发现,有效的配合敏态业务发展。


故障发生后,监控系统需要第一时间通知相关人员,做初步定位,ARMS 告警告警能力实现了 ChatOps 能力,基于 IM 工具,快速触达相关人员,并且提供初步定位能力,是故障的响应能力大幅提升。


故障的快速恢复,对于控制业务影响至关重要,ARMS 利用全链路 Trace 能力,快速定位具体应用、接口、方法、慢sql等,是故障快速恢复的关键助手。茶百道技术团队负责人表示: “在与开源方案成本持平的前提下,ARMS 丰富且全面的全栈观测与告警能力,使茶百道快速建立运维观测与响应能力,故障恢复效率提升 50% 以上,故障恢复耗时****缩短 50%,真正做到用可观测为业务迅猛发展保驾护航。”


故障的预防收敛,在稳定性体系建设中是投入产出比极高的,PTS 利用全国流量施压的能力,和秒级监控能力,验证站点容量并定位性能瓶颈。茶百道在业务上线前,充分对单应用和全链路做压测,累计压测 800 余次,在上线前做到了性能问题的收敛,避免演进为线上故障。


下阶段目标


在可观测领域,Prometheus + Grafana 是指标数据存储、计算、查询、展示的事实标准,ARMS 产品家族提供托管加强的 Prometheus 和 Grafana 服务。ARMS 应用监控生成的指标数据也会自动保存到托管版 Prometheus 中,并预置数张 Grafana 大盘。茶百道的工程师们正在基于 Prometheus 和 Grafana,将应用层指标、关键业务指标、云服务指标进行结合,开发多维度可观测大盘。


在不久的将来,茶百道就会建立覆盖业务层、用户体验层、应用服务层、基础设置层、云服务层的统一可观测技术体系,为千万级用户同时在线的大规模微服务系统实现稳定性保障。


作者:阿里云云原生
来源:juejin.cn/post/7289767547329970231
收起阅读 »

几条有助于提高开发者学习效率的小建议

时间就像海绵中的水,挤一挤总还是有的! 思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。 作者:毅航😜 前言 作为程序员不知道你是否有过这样的感受,每天光是应对产品经理天马行空般的需求就已经筋疲力尽了,每天下班后只想静静地躺着,但面对越来...
继续阅读 »

时间就像海绵中的水,挤一挤总还是有的!



思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜





前言


作为程序员不知道你是否有过这样的感受,每天光是应对产品经理天马行空般的需求就已经筋疲力尽了,每天下班后只想静静地躺着,但面对越来越严峻的大环境以及越来越卷的后浪,你内心仿佛又有个声音在不断提醒你要坚持学习。但当你准备学习时又发现自己似乎也没什么头绪,索性就随便点开一篇博客,或是随便找本书随便看看。等过段时间再回头一看,看似当下学了很多,但过几天真正用到时却发现前几天看的东西好像又忘得差不多了,周而复始,不断在奋进摆烂间周旋。


这些问题其实笔者也曾经历过,为了克服这些问题,我也曾阅读过很多有关学习的博客及书籍,同时也做了很多尝试,最终也算找到一条适合自身的学习方案。所以笔者今天想谈一谈笔者是如何来学习新技术的,希望对你的学习、工作有所启发。


树立正确战略方向,避免南辕北辙



做一件事的关键在于树立正确的目标



众所周知,程序员总是需要面对各种层出不穷的新框架,而当接触一款全新框架时,你通常会如何做呢?当接触一款全新的框架时,通常会先花一点时间来考虑如下两个问题:



  1. 通过学习我期待能达到一个水平

  2. 如何衡量我对于这个框架的掌握程度?


接下来,不妨听听我为什么会在学习伊始先考虑这两个问题。早些的时候,当我听闻项目中要用到新框架时,总是会闷头去网上搜寻与其相关的博客、书籍。然后,拼命利用各种空余时间来读这些内容,给别人的感觉就是我很,其实我内心之道,只不过是我菜罢了!


但就是这样似乎也没比别人厉害到哪去,通过阅读可能我确实会比别人多掌握一些奇技淫巧,但实际工作场景中基本不会用到,等过段时间某个场景确实需要了,我也忘得差不多了。最后,也只能借助搜索引擎来解决。费时费力,最终却收效甚微。


后来,我就开始反思自己的行为是否正确。经过反思,我逐渐意识到工作中像我这样接到任务就闷头干的傻小子不在少数。事实上,闷头干这个行为只是你完成目标的一种手段,而不是你的战略
此处的战略你可以理解为是你的目标,也即你的方向南辕北辙的故事想必大家都曾听过,如果一个人方向选取的错了,再怎么努力也是徒劳。


笔者所思考的那两个问题恰恰就是在考虑学习的目标。即通过学习我们对于这个知识点应该掌握到什么程度,而这个程度又该通过什么指标来量化。 当有了量化的指标后,便能准确衡量我们对于一个新技术的掌握情况。


事实上,当你明确自己学习的目标后,你对于所要做的事也才能有着更加清晰的认识,进而你所采取的行动也才能更加精准,这样你也不至于类似出现南辕北辙


就像学习一款新框架最初你的目标就应该是了解框架的基本使用方式,在这个目标指引下你要做的就是找寻与其相关的实用性文档,同时为了实现这一目标你也就不至于翻看解析其源码的专业性文档!


拆分目标,寻找可行的最小单元



对目标进行拆分,分清任务轻重缓急



当明确我们的目标后,我们下一步要做就是对目标进行拆分。这一过程中,我们不断地把大目标拆分为一个个切实可行的最小单元。接下来,便以笔者当初制定学习Spring源码的过程为例,来看看笔者是如何一步步地来将一个大目标拆分具体可行的可执行单元


某段时间内,笔者曾制定下一个深耕Spring源码的目标,期待通过学习能实现从会用熟悉背后原理跃迁,而衡量这一目标是否达到的标准就是能否在不翻越任何博客的情况下自己总结出Spring的相关知识点。基于这一目标,笔者对目标其进行了拆分。


首先,对目标模块进行拆分。要知道,在没有人手把手带领学习的情况下,从零开始学习Spring源码是非常困难的一件事。所以,笔者最开始最小行动单元是从Spring最基础的实用方式开始入手,也即从分析Spring最开始使用的ClassPathXmlApplicationContext开始分析。这一点从笔者之前写的从简单的配置文件开始,重新审视Spring的上下文环境就能看出端倪。这也是笔者这些年逐渐形成的习惯,即在分析时总是会从最基础的入手,然后不断深入。


当对目标进行拆分后,下一步就是确定每个模块所需要掌握的知识。还是以读Spring源码的来分析,笔者将其拆分为容器、资源加载、扩展点、DI、AOP等模块,对于其中的容器来说具体就是要分析Spring中的容器结构,也即对BeanFactory家族的分析,因为这部分就是Spring容器的核心内容。当然你还可以进行细分,直至将一个抽象的事物不断细化,直到拆分为以一个切实可行的步骤在停止。


当目标经历过前面的拆分,下一步就是行动了!对于怎么行动这就取决于每个人的自驱力了,这个并没有一个统一的标准,笔者在这里想再谈一谈是如何挤出时间来学习的。


合理规划时间,日拱一卒



时间就像海绵里的水, 只要愿意挤,总还是有的。



当明确了学习的目标后,下一步就是行动。


在如今这个快节奏的时间似乎成了最宝贵的资源,每天应付完工作,回到家稍微休息一下,吃个饭,洗漱完,似乎就又该上床睡觉了。而且即使有空闲时间,打游戏肯定比学习更快乐,难道要牺牲为数不多的娱乐时间来学习吗?答案肯定是否定,不妨来看看笔者每天是如何来挤时间学习的。


笔者通常七点起床,洗漱完,吃个早餐差不多七点半,然后出门去等公交。从家到公交这段过程中我通常会打开一个技术类相关的视频,然后边听边走。到车站差不多需要十五分钟左右,在站台等车差不多又是十几分钟。这就差不多二十多分钟的时间了,如果视频开倍速的话,这段时间正好可以看完一段三十多分钟左右的视频。


坐公交去公司的话差不多又二十多分钟,在公交车上的这段时间,我通常会打开备忘录对刚才视频的内容进行总结和提炼,差不多八点二十就可到公司,然后开启一天的工作。笔者的公司一般六点多就可以下班了,下班的路上将继续重复早上的行为,这样每天差不多通过通勤我可以挤出一个多小时的学习时间。


到家后通常我会再花十几分钟时间对一天学习的内容进行一个回顾和总结,之后做饭、洗碗,然后打游戏娱乐,大概十点多便洗漱上床多睡觉,这就是我平凡的一天。周末的时候通常会再花一个多小时,将本周学习的内容整理成相关的文档,以备后续查看和回顾。


不难发现,我一天其实都在挤时间来学习。经过笔者长时间的时间,笔者发现这种通过挤时间的方式可以充分可利用通勤的闲暇时间,也并没有因为学习而放弃我所有的休闲娱乐,更没有在累成狗的时候强行“打鸡血”逼着自己去学,相反我很适应现在这样的生活方式,因为一切都在自己的可控范围内。


当然这只是我个人的经历,可能不具有一般性,因为每个公司的作息和个人通勤时间肯定有着很大的差异。这里笔者分享自己的经历更多的是想说: 在当下这个快节奏的时代内,当从学校离开的那一刻起,我们就再也不会有像学生时代那样大段时间来学习了,所以只能从日常中来挤出时间学习。


总结


最后,在分享笔者一条笔者多年以来笔者一直坚持贯彻的一条学习法则,即学习一项新技术时,首先,以视频为入口,然后,以业界公认的名书继续深入理解;最后,以社交圈的同行或网上社区为输出交流。


作者:毅航
来源:juejin.cn/post/7290813210277036067
收起阅读 »

后遗症:年轻人现状

前言 作为一个人间观察员,同时我也是年轻人里面的一份子,我可以感觉到在生活的方方面面都能感觉到一些病态的现状。 1、首先跟我们日常相关的,结婚、买房买车,很多人的想法这是一笔很大的花销,另外工作在这几年经济比较差的环境下也比较难找,然后涨幅也没有那么高了;我...
继续阅读 »

前言




作为一个人间观察员,同时我也是年轻人里面的一份子,我可以感觉到在生活的方方面面都能感觉到一些病态的现状。


1、首先跟我们日常相关的,结婚、买房买车,很多人的想法这是一笔很大的花销,另外工作在这几年经济比较差的环境下也比较难找,然后涨幅也没有那么高了;我发现基本大家的话题都离不开这些哈哈,可能是所谓的人生大事。


2、在讨论上面话题的时候,或者在朋友圈看到别人发的东西,我能感觉身边很多人蛮焦虑,有些甚至抑郁


3、不止是心情上出现问题,蛮多年轻人身体健康上也出现问题,像最近的脆皮大学生的梗。当然这里更多是打工人的身体健康问题,毕竟每天坐着看电脑,运动少,然后吃着各种外卖,再加上工作压力啥的。


总结:可以感受到这个社会充斥着金钱至上的味道,即使你在温饱的情况也会觉得很难受,为啥没有暴富。金钱就像那个萝卜悬在我们面前鞭笞我们前进,彷佛偷下懒就是不对的,像李某人:你要想下自身的原因?有没有努力赚钱?


很多人在这种社会环境下,或者氛围下,身心疲惫,然后你再上网刷一下,更焦虑了,哈哈。当我尝试分析各种各种原因,包括客观上、主观上的问题,发现没法解决现状问题。


什么原因导致?




这就引出以前两个名人的分歧,就是周树人、胡适,周树人是偏向人性的唤醒,就是他认为人太过冷漠了,一群人围观“坏人”的砍头,对于跟自己不相关的事情漠不关心;而胡适认为是环境导致人们出现这种情况,所以两人出现分歧。


目前我认为环境问题占大头,在很长一段的历史里面,压制人的天性,控制你的说话,人人自危,在这种环境下生存的人就会出现冷漠的心态,也就是说是对应的产物。我们现在看到的还是表面的问题,至于本质的问题是什么呢?这就得谈到《共产党宣言》。


我是在读《毛泽东笔记精讲》的时候,描述了早期对乌托邦的向往,在失败之后通过反复看了《共产党宣言》很多遍,然后系统学习共产主义,调整了方向。我那时就好奇究竟里面讲了什么,怎么从乌托邦主义转辩证唯物主义的,其实里面就很显露的暴露出资本主义带来的现状。


《共产党宣言》




image.png


image.png


里面首先提到一点,历史是一部阶级斗争的博弈,在封建时代,资本主义就是打压对象,后来随着先进的生产力出现,资本主义开始扩张,通过对外取代落后的生产力来达到资本递增的需求,所以资本主义占据了上风。


但是随着生产力的发展,机器的普及,人们的工作开始变得细分化,劳动量越大(按我理解是对重复劳动会更多,相当于人也变成一台机器,不断重复,以前搞一个东西时间是长了些,但是一天工作量就那么些,需要各个环节的切换,当你只剩下某个步骤的时候,就是一台重复的机器)。


资本也会让整个社会趋向盲目性,因为它本身就有复利的目的,比如说100w一年存银行拿到多少钱,第二年把所有钱再作为本金进行一个复利的操作。当生产力无法突破的时候,或者说市场过于饱和的时候,开始出现了危机,它不仅影响人们的日常生活,还有相处方式,当人与人的关系只剩下交易、利益的时候,再也没有其他关系,这就是当今的社会风气,就是大家都向钱看,而它恰恰描述在《共产党宣言》里面。



当一座大厦缺乏精神支柱、价值体系的时候,它建的越高倒得越快



待完善


是的,这个宣言极大鼓舞人们追随共产主义,也给出一定的理论指导,在我看来有些地方还需要完善,首先目前的生产力没有达到这么高的水平,记得刘强东在谈到ai的时候,如果机器人可以代替普通人干活,那么我们真正离共产主义的那个时代不远了;另外按需分配的规则也需要完善,是按照以前一个人多少粮票、多少住房来衡量呢?我们的思维是被现状局限了,是不是以后的物质跟现在的水一样,相对来说是非常充裕的,我们真正按照需求来要这个量,但是这里面也有人性的问题,当然如果物质已经丰富到这种情况,也没有人在意多少。


另外在那个时代,每个人都能满足基本的需求,工作都让机器去做,那人们在做什么?这就有点科幻了,是不是未来我们可以去探索外太空呢?这确实是一个有意思的目标哈哈


未来的方向


1、生产力的提升是一个很重要的基础


2、当然这个时候去制定完善的按需分配机制也不合理,需要达到上面前提条件


共产主义是解决当前资本主义带来一系列的问题的方法,但是当前的条件还达不到。


学习的东西


唯心主义、辩证唯物主义


当我在看《毛泽东笔记精讲》的时候,里面提到了早期有段时间推崇乌托邦思想,后来体系接触社会主义之后,追随辩证唯物主义。那么我就很好奇,唯心主义、辩证唯物主义 区别很大吗?


这里可以举个例子,比如说我要做一个大项目,按照我以往的经验,我会去参考业界的过往经验,然后去规划当前符合公司情况的架构。问题来了,当社会主义在初创的时候,哪有借鉴的例子,所以它出现了唯心主义,通过自己对社会主义的理解,进行蓝图,思想概念的构造,然后进行实施,这就是早期德国的社会主义乌托邦。


它的问题是会朝着预想的方向去进展,重心是你自己yy出来的,辩证唯物主义是需要借助历史,来反推未来的发展,它需要依靠现实的demo来总结规律,这像不像我们易经,这是唯物主义。


它忽略了什么,人的主观能动性,人的意志往往可以克服种种困难,所以是辩证唯物主义。



坚持实践是检验真理的唯一标准



物理上客观条件是主要的决定因素,而心理上、精神上是一大变量,是影响因素;我们可以看到在长征途中,做好了心理上的宣导,才能战胜困难,这是在《毛泽东自述》谈到的心理建设的重要性。


关于现状的想法




1、解决温饱问题,基本的物质经济


2、哪些事情对你有意义


当你满足温饱问题之后,我认为应该更多思考什么是你真正要创造的东西,还是一百年后,几百年后,人们对这个少年的印象就是这是个打工人,资本的复利的炮灰,不这不是我想要的,所以我一直在抽时间去思考历史上有哪些精神、哪些处事方式值得我们学习的,然后提出自己的方法论。


当你在获得收获然后分享那一刻是幸福,是骄傲的,历史就是一本厚厚的书籍,有些记载了每个朝代的精英集团,有些承载了那个时代的思想,它是现代人前行的路灯。


3、建设自己的精神世界


知识体系重建,价值体系重建,精神世界重建,这是重点方向。


作者:大鸡腿同学
来源:juejin.cn/post/7289397650385731641
收起阅读 »

面试官:如何判断两个数组的内容是否相等

web
题目 给定两个数组,判断两数组内容是否相等。 不使用排序 不考虑元素位置 例: [1, 2, 3] 和 [1, 3, 2] // true [1, 2, 3] 和 [1, 2, 4] // false 思考几秒:有了😀😀 1. 直接遍历✍ 直接遍历第...
继续阅读 »

题目


给定两个数组,判断两数组内容是否相等。



  • 不使用排序

  • 不考虑元素位置


例:


[1, 2, 3] 和 [1, 3, 2] // true
[1, 2, 3] 和 [1, 2, 4] // false


思考几秒:有了😀😀


1. 直接遍历✍



  • 直接遍历第一个数组,并判断是否存在于在第二个数组中

  • 求差集, 如果差集数组有长度,也说明两数组不等(个人感觉比上面的麻烦就不举例了)


const arr1 =  ["apple", "banana", 1]
const arr2 = ["apple", 1, "banana"]

function fn(arr1, arr2) {
// Arrary.some: 有一项不满足 返回false
// Arrary.indexOf: 查到返回下标,查不到返回 -1
if (arr1.length !== arr2.length) {
return false;
}
return !arr1.some(item => arr2.indexOf(item)===-1)
}

fn(arr1,arr2) // true


  • 细心的小伙伴就会发现:NaN 会有问题


const arr1 =  ["apple", "banana", NaN]
const arr2 = ["apple", NaN, "banana"]

function fn(arr1, arr2) {
if (arr1.length !== arr2.length) {
return false;
}
return !arr1.some(item => arr2.indexOf(item)===-1)
}

fn(arr1,arr2) // false


Arrary.prototype.indexOf() 是使用的严格相等算法 => NaN值永远不相等


Array.prototype.includes() 是使用的零值相等算法 => NaN值视作相等




  • 严格相等算法: 与 === 运算符使用的算法相同

  • 零值相等不作为 JavaScript API 公开, -0和0 视作相等,NaN值视作相等,具体参考mdn文档:


image.png



  • 使用includes


const arr1 =  ["apple", "banana", NaN]
const arr2 = ["apple", NaN, "banana"]

function fn(arr1, arr2) {
if (arr1.length !== arr2.length) {
return false;
}
return !arr1.some(item => !arr2.includes(item))
}

fn(arr1,arr2) // true

使用includes 确实可以判断NaN了,如果数组元素有重复呢?


// 重复的元素都是banana
const array1 = ["apple", "banana", "cherry", "banana"];
const array2 = ["banana", "apple", "banana", "cherry"];
// 或者
// 一个重复的元素是banana, 一个是apple
const array1 = ["apple", "banana", "cherry", "banana"];
const array2 = ["banana", "apple", "apple", "cherry"];


由上可知:这种行不通,接下来看看是否能从给数组元素添加标识入手


2. 把重复元素标识编号✍


这个简单:数组 元素重复 转换成val1, val2


function areArraysContentEqual(arr1, arr2) {
if (arr1.length !== arr2.length) {
return false;
}

// 重复数组元素 加1、2、3
const countArr1 = updateArray(arr1)
const countArr2 = updateArray(arr2)

/**
*
* @param {*} arr 数组 元素重复 转换成val1, val2
* @returns
*/

function updateArray(arr) {
const countMap = new Map();
const updatedArr = [];

for (const item of arr) {
if (!countMap.has(item)) {
// 如果元素是第一次出现,直接添加到结果数组
countMap.set(item, 0);
updatedArr.push(item);
} else {
// 如果元素已经出现过,添加带有编号的新元素到结果数组
const count = countMap.get(item) + 1;
countMap.set(item, count);
updatedArr.push(`${item}${count}`);
}
}
return updatedArr;
}
const flag = countArr1.some(item => !countArr2.includes(item))
return !flag
}

const array1 = ["apple", "banana", "cherry", "banana"];
const array2 = ["banana", "apple", "banana", "cherry"];

areArraysContentEqual(array1, array2) // true

// 其实这种存在漏洞的
const array3 = ["apple", "banana", "cherry", "banana", 1, '1', '1' ];
const array4 = ["banana", "apple", "banana", "cherry", '1', 1, 1];
// 应该是false
areArraysContentEqual(array3, array4) // true

因为把判断的 转为了字符串 updatedArr.push(${item}${count}) 所以出问题了


3. 统计元素次数(最终方案)✍


function areArraysContentEqual(arr1, arr2) {
if (arr1.length !== arr2.length) {
return false;
}

// 创建计数对象,用于记录每个元素在数组中的出现次数
const countMap1 = count(arr1)
const countMap2 = count(arr2)

// 统计数组中的元素出现次数
function count(arr = []) {
const resMap = new Map();
for (const item of arr) {
resMap.set(item, (resMap.get(item) || 0) + 1);
}
return resMap
}
// 检查计数对象是否相等
for (const [key, count] of countMap1) {
if (countMap2.get(key) !== count) {
return false;
}
}

return true;
}

const array1 = ["apple", "banana", "cherry", "banana", 1, '1', '11', 11];
const array2 = ["banana", "apple", "banana", "cherry", '1', 1, '11', 11];

areArraysContentEqual(array1, array2) // true


注意事项


这个题需要注意:



  • 先判断长度,长度不等 必然不等

  • 元素可重复

  • 边界情况考虑

    • '1' 和 1 (Object的key是字符串, Map的key没有限制)

    • NaN

    • null undefined




结语:


如果本文对你有收获,麻烦动动发财的小手,点点关注、点点赞!!!👻👻👻


因为收藏===会了


如果有不对、更好的方式实现、可以优化的地方欢迎在评论区指出,谢谢👾👾👾


作者:程序员小易
来源:juejin.cn/post/7290786959441117243
收起阅读 »

如何将电脑上的“小电影”隐藏为一张图片?这波操作绝了!!

大家好,我是冰河~~ 最近,有很多小伙伴想跟我学渗透。平时时间确实太忙了,除了要研发公司项目外,写公号,写博客,录视频,写书稿,维护开源项目,几乎占据了我全部的业余时间。目前确实没有太多的时间教大家,今天,就暂时给大家分享一个小技巧吧,如何彻底隐藏电脑中的“小...
继续阅读 »

大家好,我是冰河~~


最近,有很多小伙伴想跟我学渗透。平时时间确实太忙了,除了要研发公司项目外,写公号,写博客,录视频,写书稿,维护开源项目,几乎占据了我全部的业余时间。目前确实没有太多的时间教大家,今天,就暂时给大家分享一个小技巧吧,如何彻底隐藏电脑中的“小电影”,让你的女朋友再也不能发现你电脑中的小秘密!


实现效果:你女朋友打开文件是一张图片,你打开却是各种“小电影”~~


好了,我们开始吧!


首先,准备好一张图片,还有一个对你来说的很重要的“电影”文件夹,如图所示。



电影文件夹中的内容如下所示。



接下来,将电影文件夹压缩为1.rar文件,如下所示。



然后新建一个名称为copy_image.bat的脚本文件,文件内容如下所示。


copy 1.jpg/b+1.rar=2.jpg


双击运行copy_image.bat的脚本文件,会生成一张2.jpg文件,如下所示。



接下来,只保留2.jpg文件,其他文件和文件夹全部删除。



可以看到,就只剩下这个图片了,我们打开这张图片。



可以看到,它确实只是一张图片。那么问题来了:我们要看“小电影”怎么办? 接下来,是重点。


如果你想看里面的“小电影”,那只需要把图片的后缀名从.jpg修改为.rar,如下所示。



双击打开2.rar文件,如下所示。



可以看到,里面都是你珍藏多年的“小电影”啦。为了保险起见,看完,还是把文件的后缀名改回.jpg吧 ~~


你学会了吗?欢迎在文末留言讨论~~


好了,今天的分享就到这里,我是冰河,我们下期见~~


作者:冰_河
来源:juejin.cn/post/7290741663643254836
收起阅读 »

领导说我工作 3 年了只会 CRUD

在老东家工作 3 年了,公司的业务和技术栈相对熟练得差不多了。 领导觉得我能够委以重任,便把一个新项目交给我负责,另外指派一名同事协助我。 项目的重点在于数据的交互比较多,以及每天大量的数据同步和批量操作,不能出错。 队友建议以短、平、快为主,能够使用已有现成...
继续阅读 »

在老东家工作 3 年了,公司的业务和技术栈相对熟练得差不多了。


领导觉得我能够委以重任,便把一个新项目交给我负责,另外指派一名同事协助我。


项目的重点在于数据的交互比较多,以及每天大量的数据同步和批量操作,不能出错。


队友建议以短、平、快为主,能够使用已有现成的技术就用现成的技术。直接面向过程开发是人们最为舒适,是人为本能的习惯。由于他有这一种能够处理好的决心,便把数据批量操作这块委托于他。


查看了以往公司现成一些写法,一部分是直接面向 SQL 写法批量插入,面对增量同步则先查出,存在的更新,不存在的插入。一部分是通过 Kafka 和后台任务原子操作。


理论上这么操作结果也能成,但是看到修改记录,我就知道面临的需求变了很多变化很快,导致大量的更改。私底下询问负责人也了解出了太多问题,原本一劳永逸赶紧写完结果反而投入了更多的精力和时间。


出于预防心理,也对那位同事进行了提醒并且加以思考再下手。


不到一个月,我们就把项目上线了,并且没有出现数据上的错误,得到了领导的表扬。


我们也提前收场,做一些小的优化,其余时间在摸鱼。


一段时间之后,麻烦便接踵而至,其一就是开始数据量暴增,那位同事在做增量同步时进行了锁表操作,批量操作需要一些时间,在前台读取时出现响应超时。


其二就是增量同步要调整,以主库或第三方来源库为主,出现数据更新和删除的需要同步操作。


同事目前的主力放在了新项目上,把一些零散的时间用来调整需求和 bug,结果越处理,bug 出现的越多,不是数量过多卡死就是变量不对导致数据处理不对。


于是到了某一时刻终于爆发,领导找到我俩,被痛批一顿,工作这么久就只会 CRUD 操作,来的实习生都会干的活,还养你们干什么。


当然,要复盘的话当然有迹可循。我想碰见这种情况还真不少,首次开发项目时一鼓作气,以“短、平、快” 战术面向过程开发,短时间内上线。


但是,一个软件的生命周期可不止步于上线,还要过程运维以及面对变化。


导致在二次开发的时候就脱节了,要么当时写法不符合现有业务,要么改动太多动不动就割到了大动脉大出血,要么人跑了...


所以我们会采用面向对象,抽象化编程,就是用来保稳定,预留一部分来应付变化,避免牵一发而动全身。


挨完骂,也要开始收拾烂摊子。


于是我打算重新组装一个通用的方法,打算一劳永逸。


首先我们定义一个接口通用思维 IDbAsyncBulk。由于源码已经发布到了github,所以一些注释写成了英文,大致也能看出蹩脚英文的注释。


public interface IDbAsyncBulk
    {
        /// <summary>
        /// default init.
        /// use reflect to auto init all type, to lower case database fileds,and  default basic type.
        /// if ignore some fileds,please use DbBulk,Ignore property to remarkable fileds.
        /// if other operating,need user-defined to init operate.
        /// </summary>
        /// <typeparam name="T">Corresponding type</typeparam>
        Task InitDefaultMappings<T>();

        /// <summary>
        /// batch operating
        /// </summary>
        /// <typeparam name="T">will operate object entity type.</typeparam>
        /// <param name="connection_string">database connecting string.</param>
        /// <param name="targetTable">target table name. </param>
        /// <param name="list">will operate data list.</param>
        Task CopyToServer<T>(string connection_string, string targetTable, List<T> list);

        /// <summary>
        /// batch operating
        /// </summary>
        /// <typeparam name="T">will operate object entity type.</typeparam>
        /// <param name="connection">database connecting string.need to check database connecting is openning.
        /// if nothing other follow-up operate, shouldn't cover this connecting.</param>
        /// <param name="targetTable">target table name.</param>
        /// <param name="list">will operate data list.</param>
        Task CopyToServer<T>(DbConnection connection, string targetTable, List<T> list);

        /// <summary>
        /// renew as it exists,insert as it not exists.
        /// follow up : 
        /// 1.create temporary table
        /// 2.put data into temporary table.
        /// 3.merge data to target table.
        /// </summary>
        /// <typeparam name="T">data type</typeparam>
        /// <param name="connection_string">connecting string</param>
        /// <param name="keys">mapping orignal table and target table fileds,need primary key and data only,if not will throw error.</param>
        /// <param name="targetTable">target table name.</param>
        /// <param name="list">will operate data list.</param>
        /// <param name="tempTable">put data into temporary table,default name as 'target table name + # or _temp'</param>
        /// <param name="insertmapping">need to insert column,if is null,just use Mapping fileds,in order to avoid auto-create column</param>
        /// <param name="updatemapping">need to modify column,if is null,just use Mapping fileds</param>
        Task MergeToServer<T>(string connection_string, List<string> keys, string targetTable, List<T> list, string tempTable = null, List<string> insertmapping = null, List<string> updatemapping = null);

        /// <summary>
        /// renew as it exists,insert as it not exists.
        /// follow up : 
        /// 1.create temporary table
        /// 2.put data into temporary table.
        /// 3.merge data to target table.
        /// </summary>
        /// <typeparam name="T">data type</typeparam>
        /// <param name="connection">database connecting string.need to check database connecting is openning.</param>
        /// <param name="keys">mapping orignal table and target table fileds,need primary key and data only,if not will throw error.</param>
        /// <param name="targetTable">target table name.</param>
        /// <param name="list">will operate data list.</param>
        /// <param name="tempTable">put data into temporary table,default name as 'target table name + # or _temp'</param>
        /// <param name="insertmapping">need to insert column,if is null,just use Mapping fileds,in order to avoid auto-create column</param>
        /// <param name="updatemapping">need to modify column,if is null,just use Mapping fileds</param>
        Task MergeToServer<T>(DbConnection connection, List<string> keys, string targetTable, List<T> list, string tempTable = null, List<string> insertmapping = null, List<string> updatemapping = null);

        /// <summary>
        ///  batch update operating。
        /// 1.create temporary table
        /// 2.put data into temporary table.
        /// 3.merge data to target table.
        /// </summary>
        /// <typeparam name="T">data type</typeparam>
        /// <param name="connection_string">connecting string</param>
        /// <param name="where_name">matching 'where' compare fileds.</param>
        /// <param name="update_name">need to update fileds.</param>
        /// <param name="targetTable">target table name</param>
        /// <param name="list">will operate data list.</param>
        /// <param name="tempTable">put data into temporary table,default name as 'target table name + # or _temp'</param>
        Task UpdateToServer<T>(string connection_string, List<string> where_name, List<string> update_name, string targetTable, List<T> list, string tempTable = null);

        /// <summary>
        ///  batch update operating。
        /// 1.create temporary table
        /// 2.put data into temporary table.
        /// 3.merge data to target table.
        /// </summary>
        /// <typeparam name="T">data type</typeparam>
        /// <param name="connection_string">connecting string</param>
        /// <param name="where_name">matching 'where' compare fileds.</param>
        /// <param name="update_name">need to update fileds.</param>
        /// <param name="targetTable">target table name</param>
        /// <param name="list">will operate data list.</param>
        /// <param name="tempTable">put data into temporary table,default name as 'target table name + # or _temp'</param>
        /// <param name="createtemp"> create temporary table or not </param>
        Task UpdateToServer<T>(DbConnection connection, List<string> where_name, List<string> update_name, string targetTable, List<T> list, string tempTable = nullbool createtemp = true);

        /// <summary>
        /// renew as it exists,insert as it not exists.original table not exist and  target table exist will remove.
        /// 1.create temporary table
        /// 2.put data into temporary table.
        /// 3.merge data to target table.
        /// 4.will remove data that temporary data not exist and target table exist.
        /// </summary>
        /// <typeparam name="T">data type</typeparam>
        /// <param name="connection_string">connecting string</param>
        /// <param name="keys">mapping orignal table and target table fileds,need primary key and data only,if not will throw error.</param>
        /// <param name="targetTable">target table name</param>
        /// <param name="list">will operate data list.</param>
        /// <param name="tempTable">put data into temporary table,default name as 'target table name + # or _temp'</param>
        /// <param name="insertmapping">need to insert column,if is null,just use Mapping fileds,in order to avoid auto-create column</param>
        /// <param name="updatemapping">need to modify column,if is null,just use Mapping fileds</param>
        Task MergeAndDeleteToServer<T>(string connection_string, List<string> keys, string targetTable, List<T> list, string tempTable = null, List<string> insertmapping = null, List<string> updatemapping = null);

        /// <summary>
        /// renew as it exists,insert as it not exists.original table not exist and  target table exist will remove.
        ///  1.create temporary table
        /// 2.put data into temporary table.
        /// 3.merge data to target table.
        /// 4.will remove data that temporary data not exist and target table exist.
        /// </summary>
        /// <typeparam name="T">data type</typeparam>
        /// <param name="connection">database connecting string.need to check database connecting is openning.</param>
        /// <param name="keys">mapping orignal table and target table fileds,need primary key and data only,if not will throw error.</param>
        /// <param name="targetTable">target table name</param>
        /// <param name="list">will operate data list.</param>
        /// <param name="tempTable">put data into temporary table,default name as 'target table name + # or _temp'</param>
        /// <param name="insertmapping">need to insert column,if is null,just use Mapping fileds,in order to avoid auto-create column</param>
        /// <param name="updatemapping">need to modify column,if is null,just use Mapping fileds</param>
        Task MergeAndDeleteToServer<T>(DbConnection connection, List<string> keys, string targetTable, List<T> list, string tempTable = null, List<string> insertmapping = null, List<string> updatemapping = null);

        /// <summary>
        /// create temporary table
        /// </summary>
        /// <param name="tempTable">create temporary table name</param>
        /// <param name="targetTable">rarget table name</param>
        /// <param name="connection">database connecting</param>
        Task CreateTempTable(string tempTable, string targetTable, DbConnection connection);
    }

解释几个方法的作用:



InitDefaultMappings:初始化映射,将目标表的字段映射到实体,在批量操作时候会根据反射进行一一匹配表字段;


CopyToServer:批量新增,在符合数据表结构时批量复制到目标表,采用官方 SqlBulkCopy 类结合实体简化操作。


MergeToServer:增量同步,需指定唯一键,存在即更新,不存在则插入。支持指定更新字段,指定插入字段。


UpdateToServer:批量更新,需指定 where 条件,以及更新的字段。


MergeAndDeleteToServer:增量同步,以数据源和目标表进行匹配,目标表存在的则更新,不存在的则插入,目标表存在,数据源不存在则目标表移除。


CreateTempTable:创建临时表。



增加实体属性标记,用来标记列名是否忽略同步数据,以及消除数据库别名,大小写的差异。


 /// <summary>
    /// 数据库批量操作标记,用于标记对象属性。
    /// </summary>
    public class DbBulkAttribute : Attribute
    {
        /// <summary>
        /// 是否忽略。忽略则其余属性不需要设置,不忽略则必须设置Type。
        /// </summary>
        public bool Ignore { getset; }

        /// <summary>
        /// 列名,不设置则默认为实体字段名小写
        /// </summary>
        public string ColumnName { getset; }

    }

实现类,目前仅支持 SqlServer 数据库,正在更新 MySql 和 PGSql 中。然后需要定义BatchSize(default 10000)、BulkCopyTimeout (default 300)、ColumnMappings,分别是每批次大小,允许超时时间和映射的字段。


/// <summary>
    /// sql server batch
    /// </summary>
    public class SqlServerAsyncBulk : IDbAsyncBulk
    {
        /// <summary>
        /// log recoding
        /// </summary>
        private ILogger _log;
        /// <summary>
        ///batch insert size(handle a batch every time )。default 10000。
        /// </summary>
        public int BatchSize { getset; }
        /// <summary>
        /// overtime,default 300
        /// </summary>
        public int BulkCopyTimeout { getset; }
        /// <summary>
        /// columns mapping
        /// </summary>
        public Dictionary<stringstring> ColumnMappings { getset; }
        /// <summary>
        /// structure function
        /// </summary>
        /// <param name="log"></param>
        public SqlServerAsyncBulk(ILogger<SqlServerAsyncBulk> log)
        {
            _log = log;
            BatchSize = 10000;
            BulkCopyTimeout = 300;
        }
        
        //...to do

使用上也非常的简便,直接在服务里注册单例模式,使用的时候直接依赖注入。


 //if you use SqlServer database, config SqlServerAsyncBulk service.
services.AddSingleton<IDbAsyncBulk, SqlServerAsyncBulk>();

public class BatchOperate
{
  private readonly IDbAsyncBulk _bulk;
  public BatchOperate(IDbAsyncBulk bulk)
  {
    _bulk = bulk;
  }
}

以 user_base 表举两个实例,目前测试几十万数据也才零点几秒。


 public async Task CopyToServerTest()
        {
            var connectStr = @"Data Source=KF009\SQLEXPRESS;Initial Catalog=MockData;User ID=xxx;Password=xxx";
            await _bulk.InitDefaultMappings<UserBaseModel>();
            var mock_list = new List<UserBaseModel>();
            for (var i = 0; i < 1000; i++) {
                mock_list.Add(new UserBaseModel
                {
                    age = i,
                    birthday = DateTime.Now.AddMonths(-i).Date,
                    education = "本科",
                    email = "xiaoyu@163.com",
                    name = $"小榆{i}",
                    nation = "
",
                    nationality="
中国"
                });
            }
            await _bulk.CopyToServer(connectStr, "
user_base", mock_list);
        }

public async Task MergeToServerTest()
        {
            var connectStr = @"Data Source=KF009\SQLEXPRESS;Initial Catalog=MockData;User ID=sa;Password=root";
            await _bulk.InitDefaultMappings<UserBaseModel>();
            var mock_list = new List<UserBaseModel>();
            for (var i = 0; i < 1000; i++)
            {
                mock_list.Add(new UserBaseModel
                {
                    age = i,
                    birthday = DateTime.Now.AddMonths(-i).Date,
                    education = "本科",
                    email = "mock@163.com",
                    name = $"小榆{i}",
                    nation = "汉",
                    nationality = "中国"
                });
            }
            var insertMapping = new List<string> { "birthday""education""age""email""name""nation""nationality" };
            var updateMapping = new List<string> { "birthday""education""age""email"};
            await _bulk.MergeToServer(connectStr,new List<string> {"id"}, "user_base", mock_list,null, insertMapping, updateMapping);
        

到这里,也已经完成了批量数据操作啦,不用再面对大量的sql操作啦。面向 sql 开发一时确实爽,但是面临变化或者别人接手的时候,是很痛苦的。


具体实现细节内容过多,篇幅有限暂时不全部展示,有兴趣或者尝试的伙伴可以进 github 进行参考。



github👉:github.com/sangxiaoyu/… 💖



作者:桑小榆呀
来源:juejin.cn/post/7290361767141376057
收起阅读 »