大厂都在”偷偷“用语义化标签,你却还在div?
引言
在我们日常浏览网页的时候,通常会看到各种各样的内容,如文字、图片、视频等。这些内容背后都有一个共同的语言,那就是HTML(超文本标记语言)。HTML是构建网页的基础,它就像建筑物的框架,决定了网页的基本结构和布局。
然而,仅仅有结构是不够的。如果网页只是简单地用一些基础标签堆砌而成,那么浏览器、搜索引擎甚至我们自己在后期维护时,都会感到非常吃力。
这时候,HTML的语义化标签就显得尤为重要。语义化标签不仅能使网页结构更加清晰,还能帮助搜索引擎更好地理解和索引网页内容。
什么是HTML语义化标签?
HTML语义化标签,就是那些带有特定含义的标签,它们告诉浏览器和搜索引擎,每一部分内容是什么。这就好比是给每个内容部分都贴上了一个清晰的标签,让所有人都能明白这个部分是用来做什么的。
举个例子,假设你在看一本书,书的封面、目录、章节标题等都是明确标示出来的,这样你就能快速找到自己想看的部分。同样,HTML语义化标签也是为了让网页的内容更加明晰易懂。比如:
<header>
标签用来定义网页的头部内容,通常包含导航栏、Logo等信息;<nav>
标签专门用于定义导航链接,这样搜索引擎就能更好地理解网站的结构;<article>
标签用于定义独立的内容,比如一篇新闻文章或者博客帖子。- ……
通过使用这些语义化标签,不仅提高了网页的可读性和维护性,还能帮助搜索引擎更准确地抓取和排名内容,从而提升网站的SEO效果。
为什么使用语义化标签?
使用HTML语义化标签有很多好处,它们不仅能让代码更清晰,还能带来实际的效果和便利。
- 提高网页的可读性和结构化
- 语义化标签让HTML代码更加直观,其他开发者在阅读和维护代码时,可以快速理解每个部分的作用。这有助于团队合作和项目的长期维护。
- 有助于搜索引擎优化(SEO)
- 搜索引擎通过爬虫程序抓取网页内容,并根据网页的结构和内容进行索引。使用语义化标签可以帮助搜索引擎更好地理解网页的层次和重点内容,从而提升网站在搜索结果中的排名。
- 无障碍支持
- 语义化标签对辅助技术(如屏幕阅读器)非常重要,它们可以帮助视障用户更好地理解网页内容。例如,使用
<nav>
标签可以让屏幕阅读器快速跳转到导航部分。 - 还记得浏览器内置的“沉浸阅读器”吗?它们也大多基于语义化标签提供服务。例如掘金的文章都是用
<article>
标签包裹的,所以你可以在掘金文章页面启用沉浸阅读器,而且精准的获取了文章的主体内容。
- 语义化标签对辅助技术(如屏幕阅读器)非常重要,它们可以帮助视障用户更好地理解网页内容。例如,使用
另外不得不说,目前苹果对语义化标签的使用是最炉火纯青的。怪不得都说苹果优雅,现在算是在前端上见识到了这个细节怪……
其实还有很多大厂都在使用,但都是偷偷地使用。它们没有全局使用语义化标签,而是在特定的关键位置使用语义化标签来 “谄媚” 一下搜索引擎或浏览器提供的无障碍功能。
所以我相信很多人还是非常支持div一把梭的,只要老板不限制,想怎么做就怎么做。不过如果你也能学习大厂,在漫天div下加一点语义化标签的小巧思,骗过搜索引擎和浏览器,这不是很香吗?
所以,本文着重介绍那些搜索引擎和浏览器有特别支持的语义化标签,搞定他们就搞定了一大半!
常用的语义化标签
搜索引擎钟爱的语义化标签
搜索引擎(如Google、Bing等)特别关注某些HTML语义化标签,因为这些标签能够帮助它们更好地理解网页的结构和内容,从而改进搜索结果的质量。
以下是一些被搜索引擎特别关注的语义化标签:
<header>
- 搜索引擎会识别
<header>
标签中的内容,通常包括页面的标题、导航链接等,有助于理解网页的整体结构和主要部分。
- 搜索引擎会识别
<nav>
<nav>
标签标示出导航链接区域,帮助搜索引擎理解网站的链接结构和页面之间的关系,有助于内部链接的优化。
<article>
<article>
标签表示独立的内容块,如新闻文章、博客帖子等。搜索引擎会特别关注这些标签,认为其包含主要的内容。
<footer>
<footer>
标签包含页脚内容,通常包括版权信息、联系信息等,搜索引擎会利用这些信息来补充网页的相关性数据。
<main>
<main>
标签标示出页面的主要内容区域,帮助搜索引擎更快地定位和抓取主要内容,而忽略导航栏、页脚等次要部分。
浏览器的无障碍功能
现代浏览器具备许多无障碍功能(accessibility features),这些功能可以帮助有特殊需求的用户更好地浏览网页。
以下是一些关键的无障碍功能:
- 屏幕阅读器支持
- 屏幕阅读器是一种软件工具,可以将网页内容转换为语音或盲文,帮助视障用户浏览网页。语义化标签可以极大地提升屏幕阅读器的效率和准确性。例如,
<nav>
标签可以让用户快速跳转到导航部分,而<article>
标签则可以帮助用户找到主要的文章内容。
- 屏幕阅读器是一种软件工具,可以将网页内容转换为语音或盲文,帮助视障用户浏览网页。语义化标签可以极大地提升屏幕阅读器的效率和准确性。例如,
- 键盘导航
- 无障碍浏览器允许用户通过键盘进行导航,语义化标签如
<header>
、<nav>
、<main>
、<footer>
等,可以帮助键盘用户快速跳转到页面的不同部分,提高浏览效率。
<header>
<h1>网站标题</h1>
</header>
<nav>
<!-- 导航内容 -->
</nav>
<main>
<h2>主要内容标题</h2>
<p>这是主要内容区域。</p>
</main>
<footer>
<p>版权所有 © 2024 公司名称</p>
</footer>
- 无障碍浏览器允许用户通过键盘进行导航,语义化标签如
- 高对比度模式
- 一些浏览器提供高对比度模式,帮助视觉有障碍的用户更容易阅读内容。使用正确的语义化标签和良好的结构,可以确保在高对比度模式下内容的可读性和可访问性。
<section>
<h2>章节标题</h2>
<article>
<h3>文章标题</h3>
<p>文章内容...</p>
</article>
<aside>
<h3>附加内容</h3>
<p>例如广告或链接...</p>
</aside>
</section>
- ARIA(可访问性富互联网应用)标签
- 虽然ARIA标签不是HTML语义化标签的一部分,但它们可以补充HTML标签,提供更多的无障碍信息。例如,
aria-label
、aria-labelledby
等属性可以为非文本元素提供文本描述,帮助辅助技术更好地解释内容。
<button aria-label="关闭">X</button>
<div role="dialog" aria-labelledby="dialogTitle" aria-describedby="dialogDescription">
<h2 id="dialogTitle">对话框标题</h2>
<p id="dialogDescription">对话框内容描述。</p>
</div>
- 虽然ARIA标签不是HTML语义化标签的一部分,但它们可以补充HTML标签,提供更多的无障碍信息。例如,
语义化标签的实际应用
为了更好地理解语义化标签的使用方法,让我们通过一个具体的案例来展示它们的实际应用。
假设我们要创建一个简单的博客页面,包含标题、导航栏、文章内容、侧边栏和页脚。下面是一个示例代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>我的博客</title>
<style>
body { font-family: Arial, sans-serif; }
header, nav, article, aside, footer { margin: 20px; padding: 10px; border: 1px solid #ccc; }
nav ul { list-style-type: none; padding: 0; }
nav ul li { display: inline; margin-right: 10px; }
aside { float: right; width: 30%; }
article { float: left; width: 65%; }
</style>
</head>
<body>
<header>
<h1>我的博客</h1>
<nav>
<ul>
<li><a href="#home">首页</a></li>
<li><a href="#about">关于我</a></li>
<li><a href="#contact">联系我</a></li>
</ul>
</nav>
</header>
<section>
<article>
<h2>文章标题</h2>
<p>这里是文章的正文内容。</p>
</article>
<aside>
<h2>侧边栏</h2>
<p>这里是一些附加内容,比如广告或链接。</p>
</aside>
</section>
<footer>
<p>版权所有 Dikkoo; 2024 我的博客</p>
</footer>
</body>
</html>
回顾一下
在这个案例中,我们使用了多个语义化标签来组织页面内容:
<header>
包含网站的标题和导航栏。<nav>
用于定义导航链接区域。<section>
用于分隔主要内容区域,包含文章和侧边栏。<article>
定义了独立的文章内容。<aside>
包含附加内容,如侧边栏。<footer>
包含页面的底部信息。
怎样合理运用语义化标签?
为了充分发挥HTML语义化标签的优势,以下是一些最佳实践建议:
- 规划页面结构,提前设计
- 在编写HTML之前,先绘制页面的结构图,明确各部分的功能和内容。根据设计选择合适的语义化标签,这样可以避免在编写过程中频繁修改结构。
- 保持代码简洁
- 语义化标签旨在使代码更清晰,因此应尽量避免过度嵌套标签和使用多余的标签。使用语义化标签替代大量的
<div>
和<span>
,使代码更加简洁和易读。
- 语义化标签旨在使代码更清晰,因此应尽量避免过度嵌套标签和使用多余的标签。使用语义化标签替代大量的
- 合理嵌套标签
- 语义化标签应按照其语义进行嵌套。例如,将
<nav>
放在<header>
内,表示导航是头部的一部分;将<section>
和<article>
合理地嵌套在一起,表示内容的层次结构。
- 语义化标签应按照其语义进行嵌套。例如,将
- 遵循HTML规范
- 确保使用语义化标签时符合HTML规范,不要滥用标签。例如,不要将
<header>
标签用在每个段落中,而应仅用于页面或章节的头部。
- 确保使用语义化标签时符合HTML规范,不要滥用标签。例如,不要将
来源:juejin.cn/post/7388056946121113637
虽然炒股赚了十多万,但差点倾家荡产!劝你别入坑
今天,五阳哥不打算聊技术,而是聊一下炒股的话题。我自认为在这方面有发言权,自述一个程序员的炒股经历。
2019年,我开始涉足股市,在2021年中旬为了购房,将持有的股票全部卖出,赚了十多万元。在最高峰时期,我获利超过了二十多万元,但后来又回吐了一部分利润。虽然我的炒股成绩不是最出色的,但也超过了很多人。因为大多数股民都是亏损的,能够在股市长期盈利的人真的是凤毛麟角。
股市中普遍流传的七亏二平一赚的说法并不只是传闻,事实上,现实中的比例更加残酷,能够长期赚钱的人可能连10%都达不到。
接下来,我想谈谈我的炒股经历和心路历程,与大家分享一下我的内心体验,为那些有意向或正在炒股的朋友提供一些参考。希望劝退大家,能救一个是一个!
本文倒叙描述,先聊聊最后的疯狂和偏执!
不甘失败,疯狂上杠杆
股市有上涨就有下跌,在我卖出以后,股市继续疯涨了很多。当时长春高新,我是四百一股买入,六百一股就卖出了,只赚了2万。可是在我卖出去的两个月以后,它最高涨到了一千。相当于我本可以赚六万,结果赚了两万就跑了。
我简直想把大腿拍烂了,这严重的影响了我的认知。我开始坚信,这只股票和公司就是好的,非常牛,是我始乱终弃,我不应该早早抛弃人家。 除了悔恨,我还在期盼它下跌,好让我再次抄底,重新买入,让我有重新上车的机会!
终于这只股票后来跌了10%,我觉得跌的差不多了,于是我开始抄底买入!抄底买入的价格在900一股(复权前)。
没想到,这次抄底是我噩梦的开始。我想抄他的底,他想抄我的家!
这张图,完美的诠释了我的抄底过程。地板底下还有底,深不见底,一直到我不再敢抄底为止。一直抄到,我天天睡不着觉!
当时我九百多一股开始抄底买入,在此之前我都是100股,后来我开始投入更多的资金在这只股票上。当时的我 定下了规矩,鸡蛋不能放在一个篮子里;不能重仓一只股票,要分散投资;这些道理我都明白,但是真到了节骨眼上,我不想输,我想一把赢回来,我要抄底,摊平我的成本。
正所谓:高位加仓,一把亏光。之前我赚的两万块钱,早就因为高位加仓,亏回去了。可是我不甘心输,我想赢回来。当时意识不到也不愿意承认:这就是赌徒心理。
后来这只股票,从1000,跌倒了600,回调了40%。而我已经被深深的套牢。当时我盈利时,只买了1股。等我被套牢时,持有了9股。 按照1000一股,就是九十万。按照600一股,就是54万。
我刚毕业,哪来的那么多钱!
我的钱,早就在800一股的时候,我就全投进去了,我认为800已经算是底了吧,没想到股价很快就击穿了800。
于是我开始跟好朋友借钱。一共借了10万,商量好借一年,还他利息。后来这10万块钱,也禁不住抄底,很快手里没钱了,股价还在暴跌。我已经忘记当时亏多少钱了,我当时已经不敢看账户了,也不敢细算亏了多少钱!
于是,我又开始从支付宝和招商银行借贷,借钱的利率是相当高的,年利息在6%以上。当时一共借了30万。但是股价还不见底,我开始焦虑的睡不着觉。
不光不见底,还在一直跌,我记得当时有一天,在跌了很多以后,股价跌停 -10%。当时的我已经全部资金都投进去了,一天亏了5万,我的小心脏真的要受不了了。跌的我要吐血! 同事说,那天看见我的脸色很差,握着鼠标手还在发抖!
跌成这样,我没有勇气打开账户…… 我不知道什么时候是个头,除了恐惧只有恐惧,每天活在恐惧之中。
我盘算了一下,当时最低点的我,亏了得有二十多万。从盈利六万,一下子到亏二十多万。只需要一个多月的时间。
我哪里经历过这些,投资以来,我都是顺风顺水的,基本没有亏过钱,从来都是挣钱,怎么会成这个样子。
当时的我,没空反思,我只希望,我要赚回来!我一定会赚回来,当时能借的支付宝和招行都已经借到最大额度了…… 我也没有什么办法了,只能躺平。
所以股价最低点的时候,基本都没有钱加仓。
侥幸反弹,但不忍心止盈
股价跌了四个月,这是我人生极其灰暗的四个月。后来因为种种原因,股价涨回来了,当时被传闻的事情不攻自破,公司用实际的业绩证明了自己。
股价开始慢慢回暖,后来开始凶猛的反弹,当时的��一直认为:股价暴跌时我吃的所有苦,所有委屈,我都要股市给我补回来!
后来这段时间,股价最高又回到了1000元一股(复权前)。最高点,我赚了二十多万,但是我不忍心止盈卖出。
我觉得还会继续涨,我还在畅想:公司达到,万亿市值。
我觉得自己当时真的 失了智了。
结婚买房,卖在最高点
这段时间,不光股市顺丰顺水,感情上也比较顺利,有了女朋友,现在是老婆了。从那时起,我开始反思自己的行为,我开始意识到,自己彻彻底底是一个赌徒。
因为已经回本了,也赚了一点钱,我开始不断的纠结要不要卖出,不再炒股了。
后来因为两件事,第一件是我姐姐因为家里要做小买卖,向我借钱。 当时的我,很纠结,我的钱都在股市里啊,借她钱就得卖股票啊,我有点心疼。奈何是亲姐,就借了。
后来我盘算着,不对劲。我还有贷款没还呢,一共三十万。我寻思,我从银行借钱收6%的利息,我借给别人钱,我一分利息收不到。 我TM 妥妥的冤大头啊。
不行,我要把贷款全部还上,我Tm亏大了,于是我逐渐卖股票。一卖出便不可收拾。
我开始担心,万一股价再跌回去,怎么办啊。我和女朋友结婚时,还要买房,到时候需要一大笔钱,万一要是被套住了,可怎么办啊!
在这这样的焦虑之下,我把股票全部都卖光了!
冥冥之中,自有天意。等我卖出之后的第二周,长春高新开启了下一轮暴跌,而这一轮暴跌之后,直至今日,再也没有翻身的机会。从股价1000元一股,直至今天 300元一股(复权前是300,当前是150元)。暴跌程度大达 75%以上!
全是侥幸
我觉得我是幸运的,如果我迟了那么一步!假如反应迟一周,我觉得就万劫不复。因为再次开启暴跌后,我又会开始赌徒心理。
我会想,我要把失去的,重新赢回来!我不能现在卖,我要赢回来。再加上之前抄底成功一次,我更加深信不疑!
于是我可能会从1000元,一路抄底到300元。如果真会如此,我只能倾家荡产!
不是每个人都有我这么幸运,在最高点,跑了出去。 雪球上之前有一个非常活泼的用户, 寒月霖枫,就是因为投资长春高新,从盈利150万,到亏光100万本金,还倒欠银行!
然而这一切,他的家人完全不知道,他又该如何面对家人,如何面对未来的人生。他想自杀,想过很多方式了结。感兴趣的朋友可以去 雪球搜搜这个 用户,寒月霖枫。
我觉得 他就是世界上 另一个自己。我和他完全类似的经历,除了我比他幸运一点。我因为结婚买房和被借钱,及时逃顶成功,否则我和他一样,一定会输得倾家荡产!
我觉得,自己就是一个赌狗!
然而,在成为赌狗之前,我是非常认真谨慎对待投资理财的!
极其谨慎的理财开局
一开始,我从微信理财通了解到基金,当时2019年,我刚毕业两年,手里有几万块钱,一直存在活期账户里。其中一个周末,我花时间研究了一下理财通,发现有一些债券基金非常不错。于是分几批买了几个债券基金,当时的我对于理财既谨慎又盲目。
谨慎的一面是:我只敢买债券基金,就是年利息在 5%上下的。像股票基金这种我是不敢买的。
盲目的一面是:我不知道债券基金也是风险很大的,一味的找利息最多的债券基金。
后来的我好像魔怔了,知道了理财这件事,隔三差五就看看收益,找找有没有利息更高的债券基金。直到有一天,我发现了一个指数基金,收益非常稳定。
是美股的指数基金,于是我买了1万块钱,庆幸的是,这只指数基金,三个月就赚了八百多,当时的我很高兴。那一刻,我第一次体会到:不劳而获真的让人非常快乐!
如饥似渴的学习投资技巧
经过一段时间的理财,我对于理财越来越熟悉。
胆子也越来越大,美股的指数基金赚了一点钱,我害怕亏回去,就立即卖了。卖了以后就一直在找其他指数基金,这时候我也在看国内 A股的指数基金,甚至行业主题的基金。
尝到了投资的甜头以后,我开始花更多的时间用来 找基。我开始从方方面面评估一只基金。
有一段时间,我特别自豪,我在一个周末,通过 天天基金网,找到了一个基金,这只基金和社保投资基金的持仓 吻合度非常高。当时的我思想非常朴素, 社保基金可是国家队,国家管理的基金一定非常强,非常专业,眼光自然差不了。这只基金和国家队吻合度如此高,自然也差不了。
于是和朋友们,推荐了这只基金。我们都买了这只基金,而后的一个月,这只基金涨势非常喜人,赚了很多钱,朋友们在群里也都感谢我,说我很厉害,投资眼光真高!
那一刻,我飘飘然……
我开始投入更多的时间用来理财。下班后,用来学习的时间也不学习了,开始慢慢的过度到学习投资理财。我开始不停地 找基。当时研究非常深入,我会把这只基金过往的持仓记录,包括公司都研究到。花费的时间也很多。
我也开始看各种财经分析师对于股市的分析,他们会分析大盘何时突破三千点,什么时候股市情绪会高昂起来,什么行业主题会热门,什么时候该卖出跑路了。
总之,投资理财,可以学习的东西多种多样!似乎比编程有趣多了。
换句话说:我上头了
非常荒谬的炒股开局
当时我还是非常谨慎地,一直在投资基金,包括 比较火爆的 中欧医疗创新C 基金,我当时也买了。当时葛兰的名气还很响亮呢。后来股市下行,医疗股票都在暴跌,葛兰的基金 就不行了,有句话调侃:家里有钱用不完,中欧医疗找葛兰。腰缠万贯没人分,易方达那有张坤。
由此可见,股市里难有常胜将军!
当时的我,进入股市,非常荒谬。有一天,前同事偷偷告诉我,他知道用友的内幕,让我下午开盘赶紧买,我忙追问,什么内幕,他说利润得翻五倍。 我寻思一下,看了一眼用友股票还在低位趴着,心动了。于是我中午就忙不迭的线上开户,然后下午急匆匆的买了 用友。 事后证明,利润不光没有翻五倍,还下降了。当然在这之前,我早就跑了,没赚着钱,也没咋亏钱。
当时的我,深信不疑这个假的小道消息,恨不得立即买上很多股票。害怕来不及上车……
自从开了户,便一发不可收拾,此时差2个月,快到2019年底!席卷全世界的病毒即将来袭
这段时间,股市涨势非常好,半导体基金涨得非常凶猛! 我因为初次进入股市,没有历史包袱,哪个股票是热点,我追哪个,胆子非常大。而且股市行情非常好,我更加相信,自己的炒股实力不凡!
换句话说:越来越上头,胆子越来越大。 学习编程,学个屁啊,炒股能赚钱,还编个屁程序。
刚入股市,就赶上牛市,顺风顺水
2019年底到2020年上半年,A股有几年不遇的大牛市,尤其是半导体、白酒、医疗行业行情非常火爆。我因为初入股市,没有历史包袱,没有锚点。当前哪个行业火爆,我就买那个,没事就跑 雪球 刷股票论坛的时间,比上班的时间还要长。
上班摸鱼和炒股 是家常便饭。工作上虽然不算心不在焉,但是漫不经心!
在这之前,我投入的金额不多。最多时候,也就投入了10万块钱。当时基金收益达到了三万块。我开始飘飘然。
开始炒股,也尝到了甜头,一开始,我把基金里的钱,逐渐的转移到股市里。当时的我给自己定纪律。七成资金投在基金里,三成资金投在股市里。做风险平衡,不能完全投入到风险高的股市里。
我自认为,我能禁得住 炒股这个毒品。
但是逐渐的,股票的收益越来越高,这个比例很快就倒转过来,我开始把更多资金投在股市中,其中有一只股票,我非常喜欢。这只股票后来成为了很多人的噩梦,成为很多股民 人生毁灭的导火索!
长春高新 股票代码:000661。我在这只股票上赚的很多,后来我觉得股市涨了那么多,该跌了吧,于是我就全部卖出,清仓止盈。 当时的我利润有六万,我觉得非常多了,我非常高兴。
其中 长春高新 一只股票的利润在 两万多元。当时这是我最喜欢的一只股票。我做梦也想不到,后来这只股票差点让我倾家荡产……
当时每天最开心的事情就是,打开基金和证券App,查看每天的收益。有的时候一天能赚 两千多,比工资还要高。群里也非常热闹,每个人都非常兴奋,热烈的讨论哪个股票涨得好。商业互吹成风……
换句话说:岂止是炒股上头,我已经中毒了!
之后就发生了,上文说的一切,我在抄底的过程中,越套越牢……
总结
以上都是我的个人真实经历。 我没有谈 A 股是否值得投资,也不评论当前的股市行情。我只是想分享自己的个人炒股经历。
炒股就是赌博
我想告诉大家,无论你在股市赚了多少钱,迟早都会还回去,越炒股越上头,赚的越多越上头。
赌徒不是一天造成的,谁都有赢的时候,无论赚多少,最终都会因为人性的贪婪 走上赌徒的道路。迟早倾家荡产。即使你没有遇到长春高新,也会有其他暴跌的股票等着你!
什么🐶皮的价值投资! 谈价值投资,撒泡尿照照自己,你一个散户,你配吗?
漫漫人生路,总会错几步。股市里错几步,就会让你万劫不复!
”把钱还我,我不玩了“
”我只要把钱赢回来,我就不玩了“
这都是常见的赌徒心理,奉劝看到此文的 程序员朋友,千万不要炒股和买基金。
尤其是喜欢打牌、打德州扑克,喜欢买彩-票的 赌性很强的朋友,一定要远离炒股,远离投资!
能救一个是一个!
来源:juejin.cn/post/7303348013934034983
扒一扒uniapp是如何做ios app应用安装的
为何要扒
因为最近有移动端业务的需求,用uniapp做了ios、Android双端的app应用,由于没有资质上架AppStore和test flight,所以只能使用苹果的超签(需要ios用户提供uuid才能加入测试使用,并且只支持100人安装使用)。打包出来生成的是一个ipa包,并不能直接安装,要通过爱思助手这类的应用装一下ipa包。但交付到客户手上就有问题了,还需要电脑连接助手才能安装,那岂不是每次安装新版什么的,都要打开电脑搞一下。因此,才有了这次的扒一扒,目标就是为了解决只提供一个下载链接用户即可下载,不用再通过助手类应用安装ipa包。
开干
官方模板
先打开uniapp云打包一下项目看看
复制地址到移动端浏览器打开看看
这就对味了,都知道ios是不能直接打开ipa文件进行安装的,接下来就研究下这个页面的执行逻辑。
开扒
F12打开choromdevtools,ctrl+s保存网页html。
保存成功,接下来看看html代码(样式代码删除了)
<!DOCTYPE html>
<!-- saved from url=(0077)https://ide.dcloud.net.cn/build/download/2425a4b0-4229-11ee-bd1b-67afccf2f6a7 -->
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
</head>
<body>
<br><br>
<center>
<a class="button" href="itms-services://?action=download-manifest&url=https://ide.dcloud.net.cn/build/ipa-xxxxxxxxxxx.plist">点击安装</a>
</center>
<br><br>
<center>注意:只提供包名为io.dcloud.appid的包的直接下载安装,如果包名不一致请自行搭建下载服务器</center>
</body>
</html>
解析
从上面代码可以看出,关键代码就一行也就是a标签的href地址("itms-services://?action=download-manifest&url=ide.dcloud.net.cn/build/ipa-x…")
先看看itms-services是什么意思,下面是代码开发助手给的解释
大概意思就是itms-services是苹果提供给开发者一个的更新或安装应用的协议,用来做应用分发的,需要指向一个可下载的plist文件地址。
什么又是plist呢,这里再请我们的代码开发助手解释一下
对于没接触过ios相关开发的,连plist文件怎么写都不知道,既然如此,那接下来就来扒一下dcloud的pilst文件,看看官方是怎么写的吧。
打开浏览器,copy一下刚刚扒下来的html文件下a标签指向的地址,复制url后面plist文件的下载地址粘贴到浏览器保存到桌面。
访问后会出现
别担心,这时候直接按ctrl+s可以直接保存一个plist.xml文件,也可以打开devtools查看网络请求,找到ipa开头的请求
直接新建一个plist文件,cv一下就好,我这里就选择保存它的plist.xml文件,接下来康康文件里到底是什么
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>https://bdpkg-aliyun.dcloud.net.cn/20230824/xxxxx/Pandora.ipa?xxxxxxxx</string>
</dict>
<dict>
<key>kind</key>
<string>display-image</string>
<key>needs-shine</key>
<false/>
<key>url</key>
<string>https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/uni.png</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>kind</key>
<string>software</string>
<key>bundle-identifier</key>
<string>xxxxx</string>
<key>title</key>
<string>HBuilder手机应用</string>
</dict>
</dict>
</array>
</dict>
</plist>
直接抓重点,这里存你存放ipa包的地址
这里改你应用的昵称
这里改图标
因篇幅限制,想了解plist的自行问代码助手或者搜索引擎。
为我所用
分析完了,如何为我所用呢,首先按照分析上扒下来的plist文件修改下自身应用的信息,并且需要服务器存放ipa文件,这里我选择了unicloud,开发者可以申请一个免费的空间(想了解更多的自己去dcloud官网看看,说多了有打广告嫌疑),替换好大概如下:
将plist文件放到服务器上后,拿到plist的下载地址,打开扒下来的html,将a标签上的url切换成plist文件的下载地址,如图:
可以把页面上没用的信息都删掉,保存,再把html放到服务器上,用户访问这个地址,就可以直接下载描述文件安装ipa包应用了(记得需要添加用户的uuid到开发者账号上),其实至此需求已经算是落幕了,但转念想想还是有点麻烦,于是又优化了一下,将a标签中的href信息,直接加载到二维码上供用户扫描便可直接下载,相对来说更方便一点,于是我直接打开草料,生成了一个二维码,至此,本次扒拉过程结束,需求落幕!
来源:juejin.cn/post/7270799565963149324
听一听比尔盖茨老人家怎么看待 AI 革命
最近发现比尔盖茨还在写文章,确实了不起,68 岁的老人家还在坚持输出,除了写文章,比尔盖茨还致力于教育、医疗和卫生等慈善工作,奋斗在一线,看来美国人也延迟退休啊 😅
原文《AI 将彻底改变计算机的使用方式》 大约一万多字,从宏观的角度讲述了 AI 的形态,并且提及了 AI 将影响的四个行业,最后再讲到目前面临的技术问题和非技术问题,整体文章深入浅出,非常值得一读。
本文做了一些删减,并尝试用自己的话去解读,更加符合中文读者的语义,其中的一些观点仅供参考,大家自由评判之,毕竟比尔先生还预测过计算机只需要 768kb 内存足以,而现在 8 个 G 都不够下饭的。
未来是智能体
首先比尔先生回忆了和保罗·艾伦一起创立微软公司的感觉,然后语重心长讲到,虽然经过了几十年的更迭,但是计算机还是比较蠢笨的,你要完成一个任务,得先选择某一个 app,比如用微软的 Word,去画一个商业的草图,但是这个 app 不能帮你发送邮件、分享自拍、分析数据、计划一个聚会,或者买电影票,要完成上述这些事情,要么找一个亲密的朋友,要么有一个个人助理。
胆大的预测,在下一个五年,AI 将彻底重构,你不需要再用为了处理不同任务,而去使用不同的 app。你可以用任何语言,直接告诉你的设备,你想要做什么。AI 借由丰富的理解能力,为你做个性化的服务和响应。在不远的将来,每个人都能够拥有远超当前科技的个性化的人工智能助手。
这种软件形态,比尔先生思考了 30 多年,但是最近两年来才成为现实,这种形态叫做“智能体”(Agent),能够用自然语言回复,并且基于用户的背景知识去完成不同的任务。
智能体不只是改变每个人和计算机的交互,他们同样颠覆了整个软件工业,这是从命令行输入、图形化交互以来,计算机交互最大的变革。
这里就可以总结了,比尔先生认同「All in One」的观点,一个智能体,处理你的一切事务,并且用发展的眼光看待,在之后技术继续蓬勃发展,智能体将登基,成为新的软件形态之主。
区分机器人和智能体
现在很多公司,做出来的产品根本算不上智能体,只能叫做机器人(Bot),机器人内嵌在某一个 app,借由 AI 的能力处理一些特定的任务,比如文本润色、扩写等,他们不会记住你用过多少次,也不会记住你的喜好,只能冰冷冷的机器人。
智能体不一样,更聪明,更主动,在你询问它们意见之前,就能够给到你合适的建议。它们能够跨 app 处理任务,记录你的行为,识别你的动机和意图,随着时间的推移,它们会慢慢变得更好,更加准确的给你提供信息和建议。
举个例子 🌰,你想要来一场特种兵旅行,机器人只能根据你的预算,给你定位酒店。而智能体,了解你近年来的旅行资料,能够推断出你是想要找一个距离景点近一点的还是远一点的,来为你推荐合适的酒店,还能根据你的兴趣和倾向,为你规划行程、预定餐厅。
AI 智能体,还有最让人激动的能力,那就把现在一些昂贵的服务价格给打下来,在这四个领域中,医疗健康、教育、生产力以及娱乐购物,智能体将大展拳脚。
医疗健康
目前,AI 的作用局限于处理一些非医疗任务,比如,就诊时候录音,然后生成报告给医生检查回顾。接下来会真正的转变,智能体能够帮助病人去做一个基础的伤病分类,获取关于处理健康问题的建议,决定是否需要去做进一步治疗。这些智能体还能够帮助医疗人员做决定,使之更加的高效。
现在已经有类似的 app 了,比如 Glass Health,能够分析病人摘要、提供建议诊断给医生做参考。
有了这些帮助病人和医疗人员的智能体,才是真正利于处在贫困的国家地区,那些贫瘠地方的老百姓们,甚至从来都没见过医生。比尔先生大义 🫡
这类「临床医学智能体」普及的速度可能会比较慢,毕竟这事关生死。人们需要看到这些健康智能体真正起到作用的证据,才能够接受它们。虽然健康智能体可能不完美,会犯错,但是,人类也同样不完美,会犯错。
教育
十多年来,比尔先生一直对借助软件帮助学生学习、让老师工作更轻松这样的事情很上心,软件不会替代教师工作,它只能对他们的工作进行补充,比如对学生做到个性化教育,从批改作业的压力中解放,还有其他种种。
现在已经有初步的进展,那就是一些基于文本的教育智能体,他们能够解释二元方程、提供练习数学题等等,但这还仅仅只是初步能力,接下来智能体还会解锁更多的能力。
确实,AI 学习辅导这个需求确实不错,之前热搜有这么一个家长给孩子辅导的问题
通过 AI 可以给到很好的学习启发:
生产力
生产力这方面已经卷得飞起了,微软以及为 Word、Outlook 等其他 app 集成了 Copilot(副驾驶),谷歌也在做类似的事情,也把 Bard 集合在自家的生产力 app 中。这些 Copilot 可以做很多事情,比如把文本转换为 PPT,回答表格问题,总结电子邮件内容等等。
当然,智能体能做到还有更多,如果你有一个商业想法,智能体会做一份商业计划书,然后基于此创建一份展示汇报,还能根据你的内容插入生成合适的图片。
娱乐购物
好吧,AI 可能帮你挑选电视频道、推荐电影、书、电视剧等等。比如,最近比尔盖茨投资了一家创业公司 Pix,用问答的方式推荐电影。虾皮也有一个基于 AI 的「DJ」,它能够根据你的喜好播放歌曲,并且还能和你交流,甚至会喊你的名字。
对科技行业的冲击波
总而言之,智能体最终能够帮助到我们生活的方方面,这对整个软件行业和社会的影响将会是深远的。
在计算领域,我们常谈论的「平台」,比如安卓、iOS 还有 Windows,是目前 app 和服务赖以存在的基础,而智能体将会成为下一个平台。
创建一个 app 或者服务,你不需要知道如何如变成或者图形设计,你只需要告诉你的智能体你想要做什么,它就能够编码、设计界面、创建 logo,然后发布到 app 到在线商店上,OpenAI 的 GPTs 能让我们一窥未来,GPTs 可以让非开发人员创建并分享自己的的智能体。
推荐下 starflow.tech,可以直接体验 GPTs
没有哪一家公司可以垄断智能体生意,因为未来会有多款不同 AI 引擎可供使用。现在,智能体只能依赖于其他软件,比如 Word 和 Excel,但是最终,他们将会独立运行。现在他们可能是免费的,但以后,你会为这些聪明高效的智能体付费,那么商业逻辑将改变,公司不再需要迎合广告公司而恶心用户,而是真正地为用户量身打造智能体。
在这些聪明但又复杂的智能体落地成为现实之前,还有大量的技术问题需要解决。
技术挑战
至今还没有人搞清楚智能体的底层存储结构是怎么样的,要创建一个个性化的智能体,我们需要一种新型数据库,它能把记录你的兴趣和关系的微妙之处,在保障隐私的情况下还能够快速查询信息。目前向量数据库是一种,或许之后还会有其他更好的呢。
另一个开放的问题就是一个用户大概需要和多少智能体打交道呢?你的个性化智能体会被分为医疗智能体和数学教师智能体吗?如果是的话,你是希望这些智能体彼此能够协作,还是在各自领域保持独立?
智能体的形态会是怎么样的呢,是手机、眼睛、项链、徽章,甚至是全息投影?这里比尔先生推测,现阶段最适合的是耳机,它能够听取你的声音,然后通过耳机回复你,其他的好处是,它还能调节音量、屏蔽周围噪音。
这里面还有其他种种技术挑战存在
1️⃣ 智能体之间互相交流的标准协议?
2️⃣ 智能体的价格要怎么打到每个人都能够用得起?
3️⃣ 用户少量提示词和智能体的准确回复之间如何取得平衡?
4️⃣ 如何减少幻觉,特别是在医疗这种特别重要的场景下?
5️⃣ 如何确保智能体不会伤害 or 歧视人类?
6️⃣ 如何确保智能体不会越权进行犯罪?
在不远的将来,智能体会迫使人类去思考,我们这么做是为了什么?想象一下,一个足够优秀的智能体存在,我们基本不需要工作了,那么每个人还需要接受高水平的教育吗?在未来可能是这样的,人们怎么消磨他们的时间?在所有答案都是已知的情况下,每个人还想要去上学吗?每个人都有大量的空闲时间,你还能有一个安全和繁荣的社会吗?
不过到这个时间点还很早,但至少目前,智能体正在走来,在接下来的几年,他们将彻底改变我们的生活。
来源:juejin.cn/post/7312736427326504996
怎样实现每次页面打开时都清除本页缓存?
"```markdown
每次页面加载时清除本页缓存可以通过多种方式实现,具体方法取决于需要的粒度和数据类型。以下是一些常见的技术:
使用meta标签(HTML):
<meta http-equiv=\"cache-control\" content=\"no-cache, no-store, must-revalidate\">
<meta http-equiv=\"pragma\" content=\"no-cache\">
<meta http-equiv=\"expires\" content=\"0\">
使用JavaScript:
// 清除整个页面缓存
window.location.reload(true);
// 清除特定资源的缓存
const url = 'https://example.com/style.css';
fetch(url, {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
}).then(response => {
// 处理响应
});
// 清除localStorage
localStorage.clear();
// 清除sessionStorage
sessionStorage.clear();
使用HTTP头信息(服务端设置):
// Express.js 示例
app.use((req, res, next) => {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
next();
});
使用框架或库功能:
例如,React中可以通过key属性强制重新渲染组件来清除缓存:
function App() {
const [key, setKey] = useState(0);
const resetPage = () => {
setKey(prevKey => prevKey + 1);
};
return (
<div key={key}>
{/* 页面内容 */}
<button onClick={resetPage}>重置页面</button>
</div>
);
}
清除浏览器缓存:
用户可以手动清除浏览器缓存来达到相同的效果。这通常通过浏览器设置或开发者工具的Network面板来实现。
综上所述,实现每次页面加载时清除本页缓存可以根据具体情况选择合适的方法。无论是通过HTML标签、JavaScript代码、服务器端设置还是框架功能,都可以有效地控制和管理页面的缓存行为,确保用户获得最新和最准确的内容。
来源:juejin.cn/post/7389643363160965130
接口不能对外暴露怎么办?
在业务开发的时候,经常会遇到某一个接口不能对外暴露,只能内网服务间调用的实际需求。
面对这样的情况,我们该如何实现呢?
1. 内外网接口微服务隔离
将对外暴露的接口和对内暴露的接口分别放到两个微服务上,一个服务里所有的接口均对外暴露,另一个服务的接口只能内网服务间调用。
该方案需要额外编写一个只对内部暴露接口的微服务,将所有只能对内暴露的业务接口聚合到这个微服务里,通过这个聚合的微服务,分别去各个业务侧获取资源。
该方案,新增一个微服务做请求转发,增加了系统的复杂性,增大了调用耗时以及后期的维护成本。
2. 网关 + redis 实现白名单机制
在 redis 里维护一套接口白名单列表,外部请求到达网关时,从 redis 获取接口白名单,在白名单内的接口放行,反之拒绝掉。
该方案的好处是,对业务代码零侵入,只需要维护好白名单列表即可;
不足之处在于,白名单的维护是一个持续性投入的工作,在很多公司,业务开发无法直接触及到 redis,只能提工单申请,增加了开发成本;
另外,每次请求进来,都需要判断白名单,增加了系统响应耗时,考虑到正常情况下外部进来的请求大部分都是在白名单内的,只有极少数恶意请求才会被白名单机制所拦截,所以该方案的性价比很低。
3. 方案三 网关 + AOP
相比于方案二对接口进行白名单判断而言,方案三是对请求来源进行判断,并将该判断下沉到业务侧。避免了网关侧的逻辑判断,从而提升系统响应速度。
我们知道,外部进来的请求一定会经过网关再被分发到具体的业务侧,内部服务间的调用是不用走外部网关的(走 k8s 的 service)。
根据这个特点,我们可以对所有经过网关的请求的header里添加一个字段,业务侧接口收到请求后,判断header里是否有该字段,如果有,则说明该请求来自外部,没有,则属于内部服务的调用,再根据该接口是否属于内部接口来决定是否放行该请求。
该方案将内外网访问权限的处理分布到各个业务侧进行,消除了由网关来处理的系统性瓶颈;
同时,开发者可以在业务侧直接确定接口的内外网访问权限,提升开发效率的同时,增加了代码的可读性。
当然该方案会对业务代码有一定的侵入性,不过可以通过注解的形式,最大限度的降低这种侵入性。
具体实操
下面就方案三,进行具体的代码演示。
首先在网关侧,需要对进来的请求header添加外网标识符: from=public
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Override
public Mono < Void > filter ( ServerWebExchange exchange, GatewayFilterChain chain ) {
return chain.filter(
exchange.mutate().request(
exchange.getRequest().mutate().header('id', '').header('from', 'public').build())
.build()
);
}
@Override
public int getOrder () {
return 0;
}
}
接着,编写内外网访问权限判断的AOP和注解
@Aspect
@Component
@Slf4j
public class OnlyIntranetAccessAspect {
@Pointcut ( '@within(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)' )
public void onlyIntranetAccessOnClass () {}
@Pointcut ( '@annotation(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)' )
public void onlyIntranetAccessOnMethed () {
}
@Before ( value = 'onlyIntranetAccessOnMethed() || onlyIntranetAccessOnClass()' )
public void before () {
HttpServletRequest hsr = (( ServletRequestAttributes ) RequestContextHolder.getRequestAttributes()) .getRequest ();
String from = hsr.getHeader ( 'from' );
if ( !StringUtils.isEmpty( from ) && 'public'.equals ( from )) {
log.error ( 'This api is only allowed invoked by intranet source' );
throw new MMException ( ReturnEnum.C_NETWORK_INTERNET_ACCESS_NOT_ALLOWED_ERROR);
}
}
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OnlyIntranetAccess {
}
最后,在只能内网访问的接口上加上@OnlyIntranetAccess注解即可
@GetMapping ( '/role/add' )
@OnlyIntranetAccess
public String onlyIntranetAccess() {
return '该接口只允许内部服务调用';
}
4. 网关路径匹配
在DailyMart项目中我采用的是第四种:即在网关中进行路径匹配。
该方案中我们将内网访问的接口全部以前缀/pv开头,然后在网关过滤器中根据路径找到具体校验器,如果是/pv访问的路径则直接提示禁止外部访问。
使用网关路径匹配方案不仅可以应对内网接口的问题,还可以扩展到其他校验机制上。
譬如,有的接口需要通过access_token进行校验,有的接口需要校验api_key 和 api_secret,为了应对这种不同的校验场景,只需要再实现一个校验类即可,由不同的子类实现不同的校验逻辑,扩展非常方便。
来源:juejin.cn/post/7389092138900717579
Spring Boot集成pf4j实现插件开发功能
1.什么是pf4j?
一个插件框架,用于实现插件的动态加载,支持的插件格式(zip、jar)。
核心组件
- **Plugin:**是所有插件类型的基类。每个插件都被加载到一个单独的类加载器中以避免冲突。
- **PluginManager:**用于插件管理的所有方面(加载、启动、停止)。您可以使用内置实现作为JarPluginManager, ZipPluginManager, DefaultPluginManager(它是一个JarPluginManager+ ZipPluginManager),或者您可以从AbstractPluginManager(仅实现工厂方法)开始实现自定义插件管理器。
- **PluginLoader:**加载插件所需的所有信息(类)。
- **ExtensionPoint:**是应用程序中可以调用自定义代码的点。这是一个java接口标记。任何 java 接口或抽象类都可以标记为扩展点(实现ExtensionPoint接口)。
- **Extension:**是扩展点的实现。它是一个类上的 Java 注释
场景
有一个spring-boot
实现的web应用,在某一个业务功能上提供扩展点,用户可以基于SDK实现功能扩展,要求可以管理插件,并且能够在业务功能扩展点处动态加载功能。
2.代码工程
实验目的
实现插件动态加载,调用 卸载
Demo整体架构
- pf4j-api:定义可扩展接口。
- pf4j-plugins-01:插件项目,可以包含多个插件,需要实现 plugin-api 中定义的接口。所有的插件jar包,放到统一的文件夹中,方便管理,后续只需要加载文件目录路径即可启动插件。
- pf4j-app:主程序,需要依赖 pf4j-api ,加载并执行 pf4j-plugins-01 。
pf4j-api
导入依赖
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j</artifactId>
<version>3.0.1</version>
</dependency>
自定义扩展接口,集成 ExtensionPoint ,标记为扩展点
package com.et.pf4j;
import org.pf4j.ExtensionPoint;
public interface Greeting extends ExtensionPoint {
String getGreeting();
}
打包给其他项目引用
pf4j-plugins-01
如果你想要能够控制插件的生命周期,你可以自定义类集成 plugin 重新里面的方法
/*
* Copyright (C) 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pf4j.demo.welcome;
import com.et.pf4j.Greeting;
import org.apache.commons.lang.StringUtils;
import org.pf4j.Extension;
import org.pf4j.Plugin;
import org.pf4j.PluginWrapper;
import org.pf4j.RuntimeMode;
/**
* @author Decebal Suiu
*/
public class WelcomePlugin extends Plugin {
public WelcomePlugin(PluginWrapper wrapper) {
super(wrapper);
}
@Override
public void start() {
System.out.println("WelcomePlugin.start()");
// for testing the development mode
if (RuntimeMode.DEVELOPMENT.equals(wrapper.getRuntimeMode())) {
System.out.println(StringUtils.upperCase("WelcomePlugin"));
}
}
@Override
public void stop() {
System.out.println("WelcomePlugin.stop()");
}
@Extension
public static class WelcomeGreeting implements Greeting {
@Override
public String getGreeting() {
return "Welcome ,my name is pf4j-plugin-01";
}
}
}
打成jar或者zip包,方便主程序加载
pf4j-app
加载插件包
package com.et.pf4j;
import org.pf4j.JarPluginManager;
import org.pf4j.PluginManager;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.nio.file.Paths;
import java.util.List;
@SpringBootApplication
public class DemoApplication {
/* public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}*/
public static void main(String[] args) {
// create the plugin manager
PluginManager pluginManager = new JarPluginManager(); // or "new ZipPluginManager() / new DefaultPluginManager()"
// start and load all plugins of application
//pluginManager.loadPlugins();
pluginManager.loadPlugin(Paths.get("D:\\IdeaProjects\\ETFramework\\pf4j\\pf4j-plugin-01\\target\\pf4j-plugin-01-1.0-SNAPSHOT.jar"));
pluginManager.startPlugins();
/*
// retrieves manually the extensions for the Greeting.class extension point
List<Greeting> greetings = pluginManager.getExtensions(Greeting.class);
System.out.println("greetings.size() = " + greetings.size());
*/
// retrieve all extensions for "Greeting" extension point
List<Greeting> greetings = pluginManager.getExtensions(Greeting.class);
for (Greeting greeting : greetings) {
System.out.println(">>> " + greeting.getGreeting());
}
// stop and unload all plugins
pluginManager.stopPlugins();
//pluginManager.unloadPlugins();
}
}
3.测试
运行DemoApplication.java 里面的mian函数,可以看到插件加载,调用以及卸载情况
4.引用
来源:juejin.cn/post/7389912762045251584
看清裁员,你就没什么好焦虑的了
前言
Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。
前一阵子,组里面悄悄地走了两个同事,大家对此讳莫如深,原因自然不言而喻,被裁员了。
虽然裁员这件事情,在各种媒体上已经见怪不怪。但是近几年,即使是在北京,所在的公司确实还没遇到过这个情况。
本以为裁员这件事只存在于快节奏的大城市,但真正发生发生在二线城市的时候,我还是觉着心里蛮不舒服的。
因为裁员这个事情,对于个体来说,影响的确不小。
经济影响,裁员对个人的第一个影响就是收入中断。别看程序员收入比其他行业可能高一些,但是收入断层的压力依然很大。如果有房贷、车贷,更是雪上加霜。
职业发展,失去当前职位对程序员的职业发展产生不小影响,长期失业会导致技术技能的滞后,特别是在快速发展的科技行业,技术更新换代迅速。最重要的是,即使是短期的空白期,也可能使程序员在寻找新工作时,简历上的gap year也可能被HR视为一个警示信号。
心理压力,被裁员或许会经历自我怀疑、焦虑和抑郁等情绪,尤其是如果他们认为裁员是因为自己技能不足或表现不佳。这种心理压力可能会进一步影响他们在新工作中的表现和求职过程中的自信心。
所以看到身边的同事走了,我心里认为这真的是一件很糟糕的事情,从个人角度看,我认为裁员是不应该发生的。
但事实上愿望与真相的确不符合,我们常常拒绝接受真相。因为接受真相意味着改变,而改变可能会带来痛苦。
但是认清客观规律,才能认清自己,接受自己,不要固守你对事物"应该"是什么样的看法 这将使你无法了解真实的情况。
裁员存在很久了
太阳底下无新鲜事,裁员潮这件事情,早就不是第一次了。客观来讲,裁员这件事是的确是符合客观规律的,我们先回顾前面几次大的裁员潮吧,挑三个来讲。
90年代国有企业改革
1987年确定了改革开放,改革开放初期,逐步推行市场化改革,打破计划经济体制。
许多国有企业效率低下,国有企业迎来改革,负债累累,生产过剩。为了提高经济效率,政府推行国有企业改革,裁减冗员、减少亏损企业数量。
叠加政策推动,1997年党的十五大提出“抓大放小”政策,即保留和发展大型国有企业,改制或关闭小型亏损企业。
2008年全球金融危机
2008年爆发的全球金融危机导致外需急剧下降,许多出口导向型企业受到重创,全球经济衰退。
企业利润下降,为了控制成本,许多企业不得不裁员。
危机暴露了中国经济过度依赖外需和低附加值产业的问题,促使政府推动经济结构调整。
2019年互联网行业的大量裁员潮
从2018年下半年开始,不少互联网公司的裁员就已经开始,百度、阿里、腾讯、京东等大企业更是成为人们关注的焦点。
那是像摩拜、滴滴、美团等互联网行业,都是靠大量烧钱进行维持的,由于长期烧钱,投资者热情散去,各路资本也变的谨慎了。
是的,其实互联网裁员潮2019年就已经逐步开始了,互联网行业经过10多年的高速发展,慢慢走向了平稳期,互联网红利期已经过去,资本寒冬到来。
裁员背后的规律
企业战略与内部因素
成本控制与财务压力
企业在面临财务压力时,裁员通常是首选的成本控制手段之一。
通过减少员工数量,企业可以直接降低工资支出和相关福利成本。尤其是在利润率下降或财务报表不佳时,裁员成为企业迅速改善财务状况的一种方式。
比如最近理想汽车大裁员,背后就是因为纯电市场推广不顺,销售目标不及预期。
战略转型与业务重组
企业在战略转型或业务重组过程中,往往会调整其人力资源配置。例如,当企业从传统业务转向新兴技术领域时,原有的一些岗位可能不再需要,从而导致裁员。又或者,企业合并或收购也会带来很多人员问题。
比如4月底,马斯克裁撤了整个超充团队,与企业的战略是相关的,欧美充电桩市场竞争很激烈,国内的超充体系技术革新也很快,特斯拉超充的竞争力明显减弱,其实主要就是盈利问题吧。
效率提升与自动化
随着技术的进步,企业会不断寻求通过自动化和技术创新来提升效率。自动化工具和人工智能的应用可以显著减少对人力的依赖,从而导致部分职位的消失。尤其是在重复性高、技术含量低的岗位上,自动化的影响尤为明显。
这个例子就太多了,不一一列举了。
市场环境与外部因素
经济周期与市场波动
宏观经济的周期性波动对企业的经营状况有着深远影响。在经济衰退或市场需求下降时,企业往往会通过裁员来应对收入减少和利润率下滑的挑战。反之,在经济繁荣期,企业则可能增加招聘以满足业务扩张的需求。
最简单的例子,在互联网高速发展期,1年p6、3年p7根本不是梦,后端只要有过简单的开发经验,找工作根本不是问题。
目前中国经济增速放缓、消费升级趋势减弱、人口红利消失等因素的影响,中国互联网市场的需求增长趋于饱和或下降。再叠加就业市场饱和,企业对学历、经验提出了更多的要求。
行业竞争与市场压力
激烈的市场竞争也会促使企业通过裁员来保持竞争力。
当企业面临市场份额的争夺和利润率的压力时,降低运营成本成为必然选择。通过优化人力资源配置,企业可以在价格战或市场扩张中保持灵活性和优势。
目前用户规模停滞,智能手机普及率饱和,互联网用户规模增长趋于停滞,由增量市场变为存量市场,互联网获客成本越来越高。
政策变动与法规影响
政府政策和法规的变化也可能引发企业的裁员行为。例如,税收政策的调整、劳动法规的变化或贸易政策的波动,都会影响企业的运营成本和市场策略。在这种情况下,企业可能通过裁员来适应新的政策环境和经营压力。
就像目前互联网行业规范和监管愈加严格,更加注重合规,因此互联网行业也会收到影响。
技术发展与行业趋势
技术创新与产业升级
技术创新是推动产业升级的重要动力,也直接影响着就业结构的变化。新技术的应用往往带来生产方式的变革,导致传统岗位的减少和新兴岗位的增加。企业在追求技术领先的过程中,不可避免地会进行人力资源的调整。
其实,每一轮的市场繁荣,都是由技术创新带来的,例如3G时代叠加iPhone这样的智能手机,才打开了移动互联网快速发展的时期,才有了阿里 All in 无线的战略。
而现在正在快速发展的AI大模型,就是新一轮技术创新的开始,AI的可能性太大了。
行业趋势
数字化转型和远程办公的普及也在改变企业的用工模式。数字化工具的应用使得部分岗位可以通过外包或灵活用工的方式来完成,从而减少了全职员工的需求。远程办公的兴起则使企业有更多的选择来优化人力资源配置。
怎么做
通过之前的裁员历史,还有裁员背后的一些深层次原因,你应该能对裁员这件事背后的规律,有了一定的了解。
我们每个人都是时代中微不足道的一粒沙子,那么从个人角度,我们应该如何应对呢?
调整心态
年年涨薪,按时晋升机会等等这些互联网黄金时期的待遇,在当前可能不再是标配了,想要做到这一点,你需要在职场中付出比别人多几倍的努力,大家或许应该深有体会。
那么跳槽也不一定有涨薪,甚至工作时间长的人、base高的人,跳槽可能还会面临降薪。
所以一定要调整预期,如果岗位未来还有不错的发展空间,降薪也值得去。
还有就是要做好选择,不能既要(收入、光环),还要(轻松、自由)。
稳住基本盘
技术&业务能力
其实有挺多朋友加我,也不乏一些工作只有一两年的朋友,他们会感到焦虑,比如工作不好找、大厂不好进,是不是需要更换赛道。但是针对于工作年限不久的朋友,我始终给出的建议就是要先深耕技术,做好当前工作,这是作为一个技术人最基本的要求,也是立身之本。
无论市场环境如何,只要自己的技术或者业务能力在线,能够解决工作中遇到的问题,能够产生价值,那么你一定就会持续有一定竞争力。
身体
身体才是奋斗的本钱,长期加班、久坐,大家可能都会有或多或少的一些小毛病。
之所以把身体也列出来,是因为当下我真的感觉很容易精力不足,在持续大量学习的时候,很容易感到疲倦,下班之后就会什么也不想干,窝在沙发一晚上。
在你想做一件事情的时候,良好的身体状况才能保证你能够全身心的投入进去。
财富积累
良好的财富积累,既能提供经济缓冲,也可以增加自己的心里安全感。
如果想做职业转换时,有足够的储蓄也可以让你有更多的时间和资源去学习一些新技能。
提前探索
开头既然说了,国企也曾有过裁员潮,那么在互联网行业,我们大概率也很难干到退休了。
所以你需要提前探索自己可以长期耕耘的方向,并且对自己有一个清晰的自我认知。
比如经常问自己几个问题:
- 你觉得自己的内驱力是什么?喜欢做什么事情?或者自己希望自己的人生是一个怎样的状态。
- 你擅长什么技能?或者说有什么资源?比如技术、运营、销售、沟通等
- 你喜欢做的事情和擅长的技能资源等,比如健身、拍视频、写作
说在最后
好了,文章到这里就要结束了。
所以你看,由于企业战略、市场环境、技术趋势的影响,都有可能成为裁员的原因。所以我们不要认为裁员是自身能力不足,甚至影响自己的情绪。
面对裁员与互联网里不断宣扬的“35岁危机”,与其担忧焦虑,不如先调整心态,稳住自己当下的基本盘。
最重要的是提前探索,找到自己喜欢的事情,去探索自己的能力边界。
来源:juejin.cn/post/7378321582399914003
使用 uni-app 开发 APP 并上架 IOS 全过程
教你用 uni-app 开发 APP 上架 IOS 和 Android
介绍
本文记录了我使用uni-app开发构建并发布跨平台移动应用的全过程,旨在帮助新手开发者掌握如何使用uni-app进行APP开发并最终成功上架。通过详细讲解从注册开发者账号、项目创建、打包发布到应用商店配置的每一步骤,希望我的经验分享能为您提供实用的指导和帮助,让您在开发之旅中少走弯路,顺利实现自己的应用开发目标。
环境配置
IOS 环境配置
注册开发者账号
如果没有开发者账号需要注册苹果开发者账号,并且加入 “iOS Developer Program”,如果是公司项目那么可以将个人账号邀请到公司的项目中。
获取开发证书和配置文件
登录Apple Developer找到创建证书入口
申请证书的流程可以参考Dcloud官方的教程,申请ios证书教程
开发证书和发布证书都申请好应该是这个样子
创建App ID
创建一个App ID。App ID是iOS应用的唯一标识符,稍后你会在uni-app项目的配置文件中使用它。
配置测试机
第一步打开开发者后台点击Devices
第二步填写UDID
第三步重新生成开发证书并且勾选新增的测试机,建议一次性将所有需要测试的手机加入将来就不用一遍遍重复生成证书了
Android 环境配置
生成证书
Android平台签名证书(.keystore)生成指南: ask.dcloud.net.cn/article/357…
uni-app 项目构建配置
基础配置
版本号versionCode 前八位代表年月日,后两位代表打包次数
APP 图标设置
APP启动界面配置
App模块配置
注意这个页面用到什么就配置什么不然会影响APP审核
App隐私弹框配置
注意根据工业和信息化部关于开展APP侵害用户权益专项整治要求应用启动运行时需弹出隐私政策协议,说明应用采集用户数据,这里将详细介绍如何配置弹出“隐私协议和政策”提示框
详细内容可参考Uni官方文档
注意!androidPrivacy.json不要添加注释,会影响隐私政策提示框的显示!!!
在app启动界面配置勾选后会在项目中自动添加androidPrivacy.json文件,可以双击打开自定义配置以下内容:
{
"version" : "1",
"prompt" : "template",
"title" : "服务协议和隐私政策",
"message" : " 请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/> 你可阅读<a href="https://xxx.xxx.com/userPolicy.html">《服务协议》</a>和<a href="https://xxxx.xxxx.com/privacyPolicy.html">《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。",
"buttonAccept" : "同意并接受",
"buttonRefuse" : "暂不同意",
"hrefLoader" : "system|default",
"backToExit" : "false",
"second" : {
"title" : "确认提示",
"message" : " 进入应用前,你需先同意<a href="https://xxx.xxxx.com/userPolicy.html">《服务协议》</a>和<a href="https://xxx.xxxx.com/userPolicy.html">《隐私政策》</a>,否则将退出应用。",
"buttonAccept" : "同意并继续",
"buttonRefuse" : "退出应用"
},
"disagreeMode" : {
"loadNativePlugins" : false,
"showAlways" : false
},
"styles" : {
"backgroundColor" : "#fff",
"borderRadius" : "5px",
"title" : {
"color" : "#fff"
},
"buttonAccept" : {
"color" : "#22B07D"
},
"buttonRefuse" : {
"color" : "#22B07D"
},
"buttonVisitor" : {
"color" : "#22B07D"
}
}
}
我的隐私协议页面是通过vite打包生成的多入口页面进行访问,因为只能填一个地址所以直接使用生产环境的例如:xxx.xxxx.com/userPolicy.…
构建打包
使用HBuilderX进行云打包
IOS打包
构建测试包
第一步 点击发行->原生app云打包
第二步配置打包变量
运行测试包
打开HbuildX->点击运行->运行到IOS App基座
选择设备->使用自定义基座运行
构建生产包
和构建测试包基本差不多,需要变更的就是ios证书的profile文件和密钥证书
构建成功后的包在dist目录下release文件夹中
上传生产包
上传IOS安装包的方式有很多我们选择通过transporter软件上传,下载transporter并上传安装包
确认无误后点击交付,点击交付后刷新后台,一般是5分钟左右就可以出现新的包了。
App store connect 配置
上传截屏
只要传6.5和5.5两种尺寸的就可,注意打包的时候千万不能勾选支持ipad选项,不然这里就会要求上传ipad截屏
填写app信息
配置发布方式
自动发布会在审核完成后直接发布,建议选手动发布
配置销售范围
配置隐私政策
配置完之后IOS就可以提交审核了,不管审核成功还是失败Apple都会发一封邮件通知你审核结果
安卓打包
构建测试包
构建的包在dist/debug目录下
运行测试包
如果需要运行的话,点击运行 -> 运行到Android App底座
构建生产包
构建后的包在dist目录下release文件夹中
构建好安卓包之后就可以在国内的各大手机厂商的应用商店上架了,由于安卓市场平台五花八门就不给大家一一列举了。
参考链接:
结语
本文介绍了使用uni-app开发并发布跨平台移动应用的完整流程,包括注册开发者账号、项目创建、打包发布以及应用商店配置,帮助开发者高效地将应用上架到iOS和Android平台。感谢您的阅读,希望本文能对您有所帮助。
来源:juejin.cn/post/7379958888909029395
我真的不想再用mybatis和其衍生框架了选择自研亦是一种解脱
我真的不想再用mybatis和其衍生框架了选择自研亦是一种解脱
文档地址 xuejm.gitee.io/easy-query-…
GITHUB地址 github.com/xuejmnet/ea…
GITEE地址 gitee.com/xuejm/easy-…
为什么要用orm
众所邹知orm的出现让本来以sql实现的复杂繁琐功能大大简化,对于大部分程序员而言一个框架的出现是为了生产力的提升.。dbc定义了交互数据库的规范,任何数据库的操作都是只需要满足jdbc规范即可,而orm就是为了将jdbc的操作进行简化。我个人“有幸”体验过.net和java的两个大orm,只能说差距很大,当然语言上的一些特性也让java在实现orm上有着比较慢的进度,譬如泛型的出现,lambda的出现。
一个好的orm我觉得需要满足以下几点
- 强类型,如果不支持强类型那么和手写sql没有区别
- 能实现80%的纯手写sql的功能,好的orm需要覆盖业务常用功能
- 支持泛型,“如果一个orm连泛型都不支持那么就没有必要存在”这是一句现实但是又很残酷的结论,但是泛型会大大的减少开发人员的编写错误率
- 不应该依赖过多的组件,当然这并不是orm特有的,任何一个库其实依赖越少越不易出bug
其实说了这么多总结一下就是一个好的orm应该有ide的提示外加泛型约束帮助开发可以非常顺滑的把代码写下去,并且错误部分可以完全的在编译期间提现出来,运行时错误应该尽可能少的去避免。
为什么放弃mybatis
首先如果你用过其他语言的orm那么再用java的mybatis就像你用惯了java的stream然后去自行处理数据过滤,就像你习惯了kotlin的语法再回到java语法,很难受。这种难受不是自动挡到手动挡的差距,而且自动挡到手推车的差距。
xml
配置sql也不知道是哪个“小天才”想出来的,先不说写代码的时候java代码和xml代码跳来跳去,而且xml下>
,<
必须要配合CDATA
不然xml解析就失败,别说转义,我写那玩意在加转义你确定让我后续看得眼睛不要累死吗?美名其曰xml和代码分离方便维护,但是你再怎么方便修改了代码一样需要重启,并且因为代码写在xml里面导致动态条件得能力相对很弱。并且我也不知道mybatis为什么天生不支持分页,需要分页插件来支持,难道一个3202年的orm了还需要这样吗,很难搞懂mybatis的作者难道不写crud代码的吗?有些时候简洁并不是偷懒的原因,当然也有可能是架构的问题导致的。
逻辑删除的功能我觉得稍微正常一点的企业一定都会有这个功能,但是因为使用了myabtis,因为手写sql,所以常常会忘记往sql中添加逻辑删除字段,从而导致一些奇奇怪怪的bug需要排查,因为这些都是编译器无法体现的错误,因为他是字符串,因为mybatis把这个问题的原因指向了用户,这一点他很聪明,这个是用户的错误而不是框架的,但是框架要做的就是尽可能的将一些重复工作进行封装隐藏起来自动完成。
可能又会有一些用户会说所见即所得这样我才能知道他怎么执行了,但是现在哪个orm没有sql打印功能,哪个orm框架执行的sql和打印的sql是不一样的,不是所见即所得。总体而言我觉得mybatis
充其量算是sqltemlate,比sqlhelper好的地方就是他是参数化防止sql注入。当然最主要的呀一点事难道java程序员不需要修改表,不需要动表结构,不需要后期维护的吗还是说java程序员写一个项目就换一个地方跳槽,还是说java程序员每个方法都有单元测试。我在转java后理解了一点,原来这就是你们经常说的java加班严重,用这种框架加班不严重就有鬼了。
为什么放弃mybatis衍生框架
有幸在201几年再网上看到了mybatis-plus
框架,这块框架一出现就吸引了我,因为他在处理sql的方式上和.net的orm很相似,起码都是强类型,起码不需要java文件和xml文件跳来跳去,平常50%的代码也是可以通过框架的lambda表达式来实现,我个人比较排斥他的字符串模式的querywrapper
,因为一门强类型语言缺少了强类型提示,在编写代码的时候会非常的奇怪。包括后期的重构,当然如果你的代码后续不需要你维护那么我觉得你用哪种方式都是ok的反正是一次性的,能出来结果就好了。
继续说mybatis-plus
,因为工作的需要再2020年左右针对内部框架进行改造,并且让mybatis-plus支持强类型gr0up by,sum,min,max,any等api。
这个时候其实大部分情况下已经可以应对了,就这样用了1年左右这个框架,包括后续的update的increment
,decrement
update table set column=column-1 where id=xxx and column>1
全部使用lambda强类型语法,可以应对多数情况,但是针对join始终没有一个很好地方法。直到我遇到了mpj
也就是mybatis-plus-join
,但是这个框架也有问题,就是这个逻辑删除在join的子表上不生效,需要手动处理,如果生效那么在where上面,不知道现在怎么样了,当时我也是自行实现了让其出现在join的on后面,但是因为实现是需要实现某个接口的,所以并没有pr代码.
首先定义一个接口
public interface ISoftDelete {
Boolean getDeleted();
}
//其中join mapper是我自己的实现,主要还是`WrapperFunction`的那段定义
@Override
public Scf4jBaseJoinLinq<T1,TR> on(WrapperFunction<MPJAbstractLambdaWrapper<T1, ?>> onFunction) {
WrapperFunction<MPJAbstractLambdaWrapper<T1, ?>> join= on->{
MPJAbstractLambdaWrapper<T1, ?> apply = onFunction.apply(on);
if(ISoftDelete.class.isAssignableFrom(joinClass)){
SFunction deleted = LambdaHelper.getFunctionField(joinClass, "deleted", Boolean.class);
apply.eq(deleted,false);
}
return apply;
};
joinMapper.setJoinOnFunction(query->{
query.innerJoin(joinClass,join);
});
return joinMapper;
}
虽然实现了join
但是还是有很多问题出现和bug。
- 比如不支持vo对象的返回,只能返回数据库对象自定义返回列,不然就是查询所有列
- 再比如如果你希望你的对象update的时候填充null到数据库,那么只能在entity字段上添加,这样就导致这个字段要么全部生效要么全部不生效.
- 批量插入不支持默认居然是foreach一个一个加,当然这也没关系,但是你真的想实现批处理需要自己编写很复杂的代码并且需要支持全字段。而不是null列不填充
MetaObjectHandler
,支持entity
的insert
和update
但是不支持lambdaUpdateWrapper
,有时候当前更新人和更新时间都是需要的,你也可以说数据库可以设置最后更新时间,但是最后修改人呢?- 非常复杂的动态表名,拜托大哥我只是想改一下表名,目前的解决方案就是try-finally每次用完都需要清理一下当前线程,因为tomcat会复用线程,通过threadlocal来实现,话说pagehelper应该也是这种方式实现的吧
当然其他还有很多问题导致最终我没办法忍受,选择了自研框架,当然我的框架自研是参考了一部分的freesql和sqlsuagr的api,并且还有java的beetsql的实现和部分方法。毕竟站在巨人的肩膀上才能看的更远,不要问我为什么不参考mybatis的,我觉得mybatis已经把简单问题复杂化了,如果需要看懂他的代码是一件很得不偿失的事情,最终我发现我的选择是正确的,我通过参考beetsql
的源码很快的清楚了java这边应该需要做的事情,为我编写后续框架节约了太多时间,这边也给beetsql
打个广告https://gitee.com/xiandafu/beetlsql
自研orm有哪些特点
easy-query
一款无任何依赖的java全新高性能orm支持 单表 多表 子查询 逻辑删除 多租户 差异更新 联级一对一 一对多 多对一 多对多 分库分表(支持跨表查询分页等) 动态表名 数据库列高效加解密支持like crud拦截器 原子更新 vo对象直接返回
文档地址 xuejm.gitee.io/easy-query-…
GITHUB地址 github.com/xuejmnet/ea…
GITEE地址 gitee.com/xuejm/easy-…
- 强类型,可以帮助团队在构建和查询数据的时候拥有id提示,并且易于后期维护。
- 泛型可以控制我们编写代码时候的一些低级错误,比如我只查询一张表,但是where语句里面可以使用不存在上下文的表作为条件,进一步限制和加强表达式
- easy-query提供了三种模式分别是lambda,property,apt proxy其中lambda表达式方便重构维护,property只是性能最好,apt proxy方便维护,但是重构需要一起重构apt文件
单表查询
//根据条件查询表中的第一条记录
List<Topic> topics = easyQuery
.queryable(Topic.class)
.limit(1)
.toList();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t LIMIT 1
<== Total: 1
//根据条件查询id为3的集合
List<Topic> topics = easyQuery
.queryable(Topic.class)
.where(o->o.eq(Topic::getId,"3").eq(Topic::geName,"4")
.toList();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t WHERE t.`id` = ? AND t.`name` = ?
==> Parameters: 3(String),4(String)
<== Total: 1
多表
Topic topic = easyQuery
.queryable(Topic.class)
//join 后面是双参数委托,参数顺序表示join表顺序,可以通过then函数切换
.leftJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
.where(o -> o.eq(Topic::getId, "3"))
.firstOrNull();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t LEFT JOIN t_blog t1 ON t.`id` = t1.`id` WHERE t.`id` = ? LIMIT 1
==> Parameters: 3(String)
<== Total: 1
List<BlogEntity> blogEntities = easyQuery
.queryable(Topic.class)
//join 后面是双参数委托,参数顺序表示join表顺序,可以通过then函数切换
.innerJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
.where((t, t1) -> t1.isNotNull(BlogEntity::getTitle).then(t).eq(Topic::getId, "3"))
//join查询select必须要带对应的返回结果,可以是自定义dto也可以是实体对象,如果不带对象则返回t表主表数据
.select(BlogEntity.class, (t, t1) -> t1.columnAll())
.toList();
==> Preparing: SELECT t1.`id`,t1.`create_time`,t1.`update_time`,t1.`create_by`,t1.`update_by`,t1.`deleted`,t1.`title`,t1.`content`,t1.`url`,t1.`star`,t1.`publish_time`,t1.`score`,t1.`status`,t1.`order`,t1.`is_top`,t1.`top` FROM t_topic t INNER JOIN t_blog t1 ON t.`id` = t1.`id` WHERE t1.`title` IS NOT NULL AND t.`id` = ?
==> Parameters: 3(String)
<== Total: 1
子查询
```java
//SELECT * FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ?
Queryable<BlogEntity> subQueryable = easyQuery.queryable(BlogEntity.class)
.where(o -> o.eq(BlogEntity::getId, "1"));
List<Topic> x = easyQuery
.queryable(Topic.class).where(o -> o.exists(subQueryable.where(q -> q.eq(o, BlogEntity::getId, Topic::getId)))).toList();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t WHERE EXISTS (SELECT 1 FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ? AND t1.`id` = t.`id`)
==> Parameters: false(Boolean),1(String)
<== Time Elapsed: 3(ms)
<== Total: 1
//SELECT t1.`id` FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ?
Queryable<String> idQueryable = easyQuery.queryable(BlogEntity.class)
.where(o -> o.eq(BlogEntity::getId, "123"))
.select(String.class, o -> o.column(BlogEntity::getId));//如果子查询in string那么就需要select string,如果integer那么select要integer 两边需要一致
List<Topic> list = easyQuery
.queryable(Topic.class).where(o -> o.in(Topic::getId, idQueryable)).toList();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t WHERE t.`id` IN (SELECT t1.`id` FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ?)
==> Parameters: false(Boolean),123(String)
<== Time Elapsed: 2(ms)
<== Total: 0
自定义逻辑删除
//@Component //如果是spring
public class MyLogicDelStrategy extends AbstractLogicDeleteStrategy {
/**
* 允许datetime类型的属性
*/
private final Set<Class<?>> allowTypes=new HashSet<>(Arrays.asList(LocalDateTime.class));
@Override
protected SQLExpression1<WherePredicate<Object>> getPredicateFilterExpression(LogicDeleteBuilder builder,String propertyName) {
return o->o.isNull(propertyName);
}
@Override
protected SQLExpression1<ColumnSetter<Object>> getDeletedSQLExpression(LogicDeleteBuilder builder, String propertyName) {
// LocalDateTime now = LocalDateTime.now();
// return o->o.set(propertyName,now);
//上面的是错误用法,将now值获取后那么这个now就是个固定值而不是动态值
return o->o.set(propertyName,LocalDateTime.now())
.set("deletedUser",CurrentUserHelper.getUserId());
}
@Override
public String getStrategy() {
return "MyLogicDelStrategy";
}
@Override
public Set<Class<?>> allowedPropertyTypes() {
return allowTypes;
}
}
//为了测试防止数据被删掉,这边采用不存在的id
logicDelTopic.setId("11xx");
//测试当前人员
CurrentUserHelper.setUserId("easy-query");
long l = easyQuery.deletable(logicDelTopic).executeRows();
==> Preparing: UPDATE t_logic_del_topic_custom SET `deleted_at` = ?,`deleted_user` = ? WHERE `deleted_at` IS NULL AND `id` = ?
==> Parameters: 2023-04-01T23:15:13.944(LocalDateTime),easy-query(String),11xx(String)
<== Total: 0
差异更新
- 要注意是否开启了追踪
spring-boot
下用@EasyQueryTrack
注解即可开启
- 是否将当前对象添加到了追踪上下文 查询添加
asTracking
或者 手动将查询出来的对象进行easyQuery.addTracking(Object entity)
TrackManager trackManager = easyQuery.getRuntimeContext().getTrackManager();
try{
trackManager.begin();
Topic topic = easyQuery.queryable(Topic.class)
.where(o -> o.eq(Topic::getId, "7")).asTracking().firstNotNull("未找到对应的数据");
String newTitle = "test123" + new Random().nextInt(100);
topic.setTitle(newTitle);
long l = easyQuery.updatable(topic).executeRows();
}finally {
trackManager.release();
}
==> Preparing: UPDATE t_topic SET `title` = ? WHERE `id` = ?
==> Parameters: test1239(String),7(String)
<== Total: 1
关联查询
一对一
学生和学生地址
//数据库对像查询
List<SchoolStudent> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolStudentAddress).asTracking().disableLogicDelete())
.toList();
//vo自定义列映射返回
List<SchoolStudentVO> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolStudentAddress).asTracking().disableLogicDelete())
.select(SchoolStudentVO.class,o->o.columnAll()
.columnInclude(SchoolStudent::getSchoolStudentAddress,SchoolStudentVO::getSchoolStudentAddress))
.toList();
多对一
学生和班级
//数据库对像查询
List<SchoolStudent> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolClass))
.toList();
//自定义列
List<SchoolStudentVO> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolClass))
.select(SchoolStudentVO.class,o->o
.columnAll()
.columnInclude(SchoolStudent::getSchoolClass,SchoolStudentVO::getSchoolClass,s->s.column(SchoolClassVO::getId))
)
.toList();
//vo自定义列映射返回
List<SchoolStudentVO> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolClass))
.select(SchoolStudentVO.class,o->o
.columnAll()
.columnInclude(SchoolStudent::getSchoolClass,SchoolStudentVO::getSchoolClass)
)
.toList();
一对多
班级和学生
//数据库对像查询
List<SchoolClass> list1 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolStudents))
.toList();
//vo自定义列映射返回
List<SchoolClassVO> list1 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolStudents))
.select(SchoolClassVO.class,o->o.columnAll()
.columnIncludeMany(SchoolClass::getSchoolStudents,SchoolClassVO::getSchoolStudents))
.toList();
多对多
班级和老师
List<SchoolClass> list2 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolTeachers,1))
.toList();
List<SchoolClassVO> list2 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolTeachers))
.select(SchoolClassVO.class,o->o.columnAll()
.columnIncludeMany(SchoolClass::getSchoolTeachers,SchoolClassVO::getSchoolTeachers))
.toList();
动态报名
List<BlogEntity> blogEntities = easyQuery.queryable(BlogEntity.class)
.asTable(a -> "aa_bb_cc")
.where(o -> o.eq(BlogEntity::getId, "123")).toList();
==> Preparing: SELECT t.`id`,t.`create_time`,t.`update_time`,t.`create_by`,t.`update_by`,t.`deleted`,t.`title`,t.`content`,t.`url`,t.`star`,t.`publish_time`,t.`score`,t.`status`,t.`order`,t.`is_top`,t.`top` FROM aa_bb_cc t WHERE t.`deleted` = ? AND t.`id` = ?
==> Parameters: false(Boolean),123(String)
<== Total: 0
List<BlogEntity> blogEntities = easyQuery.queryable(BlogEntity.class)
.asTable(a->{
if("t_blog".equals(a)){
return "aa_bb_cc1";
}
return "xxx";
})
.where(o -> o.eq(BlogEntity::getId, "123")).toList();
==> Preparing: SELECT t.`id`,t.`create_time`,t.`update_time`,t.`create_by`,t.`update_by`,t.`deleted`,t.`title`,t.`content`,t.`url`,t.`star`,t.`publish_time`,t.`score`,t.`status`,t.`order`,t.`is_top`,t.`top` FROM aa_bb_cc1 t WHERE t.`deleted` = ? AND t.`id` = ?
==> Parameters: false(Boolean),123(String)
<== Total: 0
List<BlogEntity> x_t_blog = easyQuery
.queryable(Topic.class)
.asTable(o -> "t_topic_123")
.innerJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
.asTable("x_t_blog")
.where((t, t1) -> t1.isNotNull(BlogEntity::getTitle).then(t).eq(Topic::getId, "3"))
.select(BlogEntity.class, (t, t1) -> t1.columnAll()).toList();
==> Preparing: SELECT t1.`id`,t1.`create_time`,t1.`update_time`,t1.`create_by`,t1.`update_by`,t1.`deleted`,t1.`title`,t1.`content`,t1.`url`,t1.`star`,t1.`publish_time`,t1.`score`,t1.`status`,t1.`order`,t1.`is_top`,t1.`top` FROM t_topic_123 t INNER JOIN x_t_blog t1 ON t1.`deleted` = ? AND t.`id` = t1.`id` WHERE t1.`title` IS NOT NULL AND t.`id` = ?
==> Parameters: false(Boolean),3(String)
<== Total: 0
最后
感谢各位看到最后,希望以后我的开源框架可以帮助到您,如果您觉得有用可以点点star,这将对我是极大的鼓励
更多文档信息可以参考git地址或者文档
文档地址 xuejm.gitee.io/easy-query-…
GITHUB地址 github.com/xuejmnet/ea…
来源:juejin.cn/post/7259926933008908325
压缩炸弹,Java怎么防止
一、什么是压缩炸弹,会有什么危害
1.1 什么是压缩炸弹
压缩炸弹(ZIP)
:一个压缩包只有几十KB,但是解压缩后有几十GB,甚至可以去到几百TB,直接撑爆硬盘,或者是在解压过程中CPU飙到100%造成服务器宕机。虽然系统功能没有自动解压,但是假如开发人员在不细心观察的情况下进行一键解压(不看压缩包里面的文件大小),可导致压缩炸弹爆炸。又或者压缩炸弹藏在比较深的目录下,不经意的解压缩,也可导致压缩炸弹爆炸。
以下是安全测试几种经典的压缩炸弹
graph LR
A(安全测试的经典压缩炸弹)
B(zip文件42KB)
C(zip文件10MB)
D(zip文件46MB)
E(解压后5.5G)
F(解压后281TB)
G(解压后4.5PB)
A ---> B --解压--> E
A ---> C --解压--> F
A ---> D --解压--> G
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
压缩炸弹(也称为压缩文件炸弹、炸弹文件)是一种特殊的文件,它在解压缩时会迅速膨胀成极其庞大的文件,可能导致系统资源耗尽、崩溃或磁盘空间耗尽。
压缩炸弹的原理是利用文件压缩算法中的重复模式和递归压缩的特性。它通常是一个非常小的压缩文件,但在解压缩时会生成大量的重复数据,导致文件大小迅速增长。这种文件的设计意图是迫使系统进行大量的解压缩操作,以消耗系统资源或填满磁盘空间。
压缩炸弹可能对系统造成严重的影响,包括系统崩溃、资源耗尽、拒绝服务攻击等。因此,它被视为一种恶意的计算机攻击工具,常常被用于恶意目的或作为安全测试中的一种工具。
1.2 压缩炸弹会有什么危害
graph LR
A(压缩炸弹的危害)
B(资源耗尽)
C(磁盘空间耗尽)
D(系统崩溃)
E(拒绝服务攻击)
F(数据丢失)
A ---> B
A ---> C
A ---> D
A ---> E
A ---> F
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
压缩炸弹可能对计算机系统造成以下具体的破坏:
资源耗尽
:压缩炸弹在解压缩时会生成大量的重复数据,导致系统的CPU、内存和磁盘资源被迅速占用。这可能导致系统反应迟缓、无法响应用户的请求,甚至系统崩溃。磁盘空间耗尽
:由于压缩炸弹的膨胀特性,它可能在解压缩过程中填满磁盘空间。这会导致系统无法写入新的数据,造成磁盘空间耗尽,影响系统的正常运行。系统崩溃
:当一个压缩炸弹被解压缩时,系统可能由于资源耗尽或磁盘空间耗尽而崩溃。这可能导致系统重启或需要进行紧急修复,造成数据丢失或系统不可用的情况。拒绝服务攻击
:大规模的解压缩操作可能消耗大量系统资源,导致系统无法正常提供服务。这被恶意攻击者利用,用于进行拒绝服务攻击,使目标系统无法响应合法用户的请求。数据丢失
:在某些情况下,压缩炸弹可能会导致系统文件或数据的损坏或丢失。这可能发生在磁盘空间被耗尽时,写入操作无法成功完成的情况下。
重要提示
:压缩炸弹可能对计算机系统造成不可逆的损害,请不要尝试创建、传播或使用压缩炸弹,以保护计算机和网络的安全。
二、怎么检测和处理压缩炸弹,Java怎么防止压缩炸弹
2.1 个人有没有方法可以检测压缩炸弹?
有一些方法可以识别和处理潜在的压缩炸弹,以防止对系统造成破坏。以下是一些常见的方法:
graph LR
A(个人检测压缩炸弹)
B(安全软件和防病毒工具)
C(文件大小限制)
D(文件类型过滤)
A ---> B --> E(推荐)
A ---> C --> F(太大的放个心眼)
A ---> D --> G(注意不认识的文件类型)
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
安全软件和防病毒工具(推荐)
:使用最新的安全软件和防病毒工具可以帮助检测和阻止已知的压缩炸弹。这些工具通常具备压缩文件扫描功能,可以检查文件是否包含恶意的压缩炸弹。文件大小限制
:设置对文件大小的限制可以帮助防止解压缩过程中出现过大的文件。通过限制解压缩操作的最大文件大小,可以减少对系统资源和磁盘空间的过度消耗。文件类型过滤
:识别和过滤已知的压缩炸弹文件类型可以帮助阻止这些文件的传输和存储。通过检查文件扩展名或文件头信息,可以识别潜在的压缩炸弹,并阻止其传输或处理。
2.2 Java怎么防止压缩炸弹
在java中实际防止压缩炸弹的方法挺多的,可以采取以下措施来防止压缩炸弹:
graph LR
A(Java防止压缩炸弹)
B(解压缩算法的限制)
C(设置解压缩操作的资源限制)
D(使用安全的解压缩库)
E(文件类型验证和过滤)
F(异步解压缩操作)
G(安全策略和权限控制)
A ---> B
A ---> C
A ---> D
A ---> E
A ---> F
A ---> G
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
解压缩算法的限制
:限制解压缩算法的递归层数和重复模式的检测可以帮助防止解压缩过程无限膨胀。通过限制递归的深度和检测重复模式,可以及时中断解压缩操作并避免过度消耗资源。设置解压缩操作的资源限制
:使用Java的java.util.zip
或java.util.jar
等类进行解压缩时,可以设置解压缩操作的资源限制,例如限制解压缩的最大文件大小、最大递归深度等。通过限制资源的使用,可以减少对系统资源的过度消耗。使用安全的解压缩库
:确保使用的解压缩库是经过安全验证的,以避免存在已知的压缩炸弹漏洞。使用官方或经过广泛验证的库可以减少受到压缩炸弹攻击的风险。文件类型验证和过滤
:在解压缩之前,可以对文件进行类型验证和过滤,排除潜在的压缩炸弹文件。通过验证文件的类型、扩展名和文件头信息,可以识别并排除不安全的压缩文件。异步解压缩操作
:将解压缩操作放在异步线程中进行,以防止阻塞主线程和耗尽系统资源。这样可以保持应用程序的响应性,并减少对系统的影响。安全策略和权限控制
:实施严格的安全策略和权限控制,限制用户对系统资源和文件的访问和操作。确保只有受信任的用户或应用程序能够进行解压缩操作,以减少恶意使用压缩炸弹的可能性。
2.2.1 使用解压算法的限制来实现防止压缩炸弹
在前面我们说了Java防止压缩炸弹的一些策略,下面我将代码实现通过解压缩算法的限制
来实现防止压缩炸弹。
先来看看我们实现的思路
:
graph TD
A(开始) --> B[创建 ZipFile 对象]
B --> C[打开要解压缩的 ZIP 文件]
C --> D[初始化 zipFileSize 变量为 0]
D --> E{是否有更多的条目}
E -- 是 --> F[获取 ZIP 文件的下一个条目]
F --> G[获取当前条目的未压缩大小]
G --> H[将解压大小累加到 zipFileSize 变量]
H --> I{zipFileSize 是否超过指定的大小}
I -- 是 --> J[调用 deleteDir方法删除已解压的文件夹]
J --> K[抛出 IllegalArgumentException 异常]
K --> L(结束)
I -- 否 --> M(保存解压文件) --> E
E -- 否 --> L
style A fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style H fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style I fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style J fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style K fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style L fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style M fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
实现流程说明如下:
- 首先,通过给定的
file
参数创建一个ZipFile
对象,用于打开要解压缩的 ZIP 文件。 zipFileSize
变量用于计算解压缩后的文件总大小。- 使用
zipFile.entries()
方法获取 ZIP 文件中的所有条目,并通过while
循环逐个处理每个条目。 - 对于每个条目,使用
entry.getSize()
获取条目的未压缩大小,并将其累加到zipFileSize
变量中。 - 如果
zipFileSize
超过了给定的size
参数,说明解压后的文件大小超过了限制,此时会调用deleteDir()
方法删除已解压的文件夹,并抛出IllegalArgumentException
异常,以防止压缩炸弹攻击。 - 创建一个
File
对象unzipped
,表示解压后的文件或目录在输出文件夹中的路径。 - 如果当前条目是一个目录,且
unzipped
不存在,则创建该目录。 - 如果当前条目不是一个目录,确保
unzipped
的父文件夹存在。 - 创建一个
FileOutputStream
对象fos
,用于将解压后的数据写入到unzipped
文件中。 - 通过
zipFile.getInputStream(entry)
获取当前条目的输入流。 - 创建一个缓冲区
buffer
,并使用循环从输入流中读取数据,并将其写入到fos
中,直到读取完整个条目的数据。 - 最后,在
finally
块中关闭fos
和zipFile
对象,确保资源的释放。
实现代码工具类
:
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* 文件炸弹工具类
*
* @author bamboo panda
* @version 1.0
* @date 2023/10
*/
public class FileBombUtil {
/**
* 限制文件大小 1M(限制单位:B)[1M=1024KB 1KB=1024B]
*/
public static final Long FILE_LIMIT_SIZE = 1024 * 1024 * 1L;
/**
* 文件超限提示
*/
public static final String FILE_LIMIT_SIZE_MSG = "The file size exceeds the limit";
/**
* 解压文件(带限制解压文件大小策略)
*
* @param file 压缩文件
* @param outputfolder 解压后的文件目录
* @param size 限制解压之后的文件大小(单位:B),示例 3M:1024 * 1024 * 3L (FileBombUtil.FILE_LIMIT_SIZE * 3)
* @throws Exception IllegalArgumentException 超限抛出的异常
* 注意:业务层必须抓取IllegalArgumentException异常,如果msg等于FILE_LIMIT_SIZE_MSG
* 要考虑后面的逻辑,比如告警
*/
public static void unzip(File file, File outputfolder, Long size) throws Exception {
ZipFile zipFile = new ZipFile(file);
FileOutputStream fos = null;
try {
Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
long zipFileSize = 0L;
ZipEntry entry;
while (zipEntries.hasMoreElements()) {
// 获取 ZIP 文件的下一个条目
entry = zipEntries.nextElement();
// 将解缩大小累加到 zipFileSize 变量
zipFileSize += entry.getSize();
// 判断解压文件累计大小是否超过指定的大小
if (zipFileSize > size) {
deleteDir(outputfolder);
throw new IllegalArgumentException(FILE_LIMIT_SIZE_MSG);
}
File unzipped = new File(outputfolder, entry.getName());
if (entry.isDirectory() && !unzipped.exists()) {
unzipped.mkdirs();
continue;
} else if (!unzipped.getParentFile().exists()) {
unzipped.getParentFile().mkdirs();
}
fos = new FileOutputStream(unzipped);
InputStream in = zipFile.getInputStream(entry);
byte[] buffer = new byte[4096];
int count;
while ((count = in.read(buffer, 0, buffer.length)) != -1) {
fos.write(buffer, 0, count);
}
}
} finally {
if (null != fos) {
fos.close();
}
if (null != zipFile) {
zipFile.close();
}
}
}
/**
* 递归删除目录文件
*
* @param dir 目录
*/
private static boolean deleteDir(File dir) {
if (dir.isDirectory()) {
String[] children = dir.list();
//递归删除目录中的子目录下
for (int i = 0; i < children.length; i++) {
boolean success = deleteDir(new File(dir, children[i]));
if (!success) {
return false;
}
}
}
// 目录此时为空,可以删除
return dir.delete();
}
}
测试类
:
import java.io.File;
/**
* 文件炸弹测试类
*
* @author bamboo panda
* @version 1.0
* @date 2023/10
*/
public class Test {
public static void main(String[] args) {
File bomb = new File("D:\temp\3\zbsm.zip");
File tempFile = new File("D:\temp\3\4");
try {
FileBombUtil.unzip(bomb, tempFile, FileBombUtil.FILE_LIMIT_SIZE * 60);
} catch (IllegalArgumentException e) {
if (FileBombUtil.FILE_LIMIT_SIZE_MSG.equalsIgnoreCase(e.getMessage())) {
FileBombUtil.deleteDir(tempFile);
System.out.println("原始文件太大");
} else {
System.out.println("错误的压缩文件格式");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
三、总结
文件炸弹是一种恶意的计算机程序或文件,旨在利用压缩算法和递归结构来创建一个巨大且无限增长的文件或文件集
合。它的目的是消耗目标系统的资源,如磁盘空间、内存和处理能力,导致系统崩溃或无法正常运行。文件炸弹可能是有意制造的攻击工具,用于拒绝服务(DoS)攻击或滥用资源的目的。
文件炸弹带来的危害极大,作为开发人员,我们必须深刻认识到文件炸弹的危害性,并始终保持高度警惕,以防止这种潜在漏洞给恐怖分子以可乘之机。
总而言之,我们作为开发人员,要深刻认识到文件炸弹的危害性,严防死守,不给恐怖分子任何可乘之机。通过使用安全工具、限制文件大小、及时更新软件、定期备份数据以及加强安全意识,我们可以有效地防止文件炸弹和其他恶意活动对系统造成损害。
在中国,网络安全和计算机犯罪问题受到相关法律法规的管理和监管
。以下是一些中国关于网络安全和计算机犯罪方面的法律文献,其中也涉及到文件炸弹的相关规定:
- 《中华人民共和国刑法》- 该法律规定了各种计算机犯罪行为的法律责任,包括非法控制计算机信息系统、破坏计算机信息系统功能、非法获取计算机信息系统数据等行为,这些行为可能涉及到使用文件炸弹进行攻击。
- 《中华人民共和国网络安全法》- 该法律是中国的基本法律,旨在保障网络安全和维护国家网络空间主权。它规定了网络安全的基本要求和责任,包括禁止制作、传播软件病毒、恶意程序和文件炸弹等危害网络安全的行为。
- 《中华人民共和国计算机信息系统安全保护条例》- 这是一项行政法规,详细规定了计算机信息系统安全的保护措施和管理要求。其中包含了对恶意程序、计算机病毒和文件炸弹等威胁的防范要求。
来源:juejin.cn/post/7289667869557178404
百亿补贴为什么用 H5?H5 未来会如何发展?
23 年 11 月末,拼多多市值超过了阿里。我想写一篇文章《百亿补贴为什么用 H5》,没有动笔;24 年新年,我想写一篇《新的一年,H5 会如何发展》,也没有动笔。
眼看着灵感就要烂在手里,我决定把两篇文章合为一篇,与大家分享。当然,这些分析预测只是个人观点,如果你有不同的意见,欢迎在评论区讨论交流。
拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。
百亿补贴为什么用 H5
我们先看两张图,在 Android 手机开发者模式下,开启显示布局边界,可以看到「百亿补贴」是一个完整大框,说明「百亿补贴」在 App 内是 H5;拷贝分享链接,在浏览器打开,可以看到资源中有 react 名字的 js 文件,说明「百亿补贴」技术栈大概率是 React。
不只是拼多多,我特地确认了,京东、淘宝的的「百亿补贴」技术栈也是 H5。
那么,为什么电商巨头会在「百亿补贴」这种重要活动上选择 H5 呢?用 H5 有什么好处呢?
H5 技术已经成熟
第一个原因,也是最基础的原因,就是 H5 技术已经成熟,能够完整地实现功能。具体来说:
浏览器兼容性不断提高
自 2008 年 HTML5 草案发布以来,截止 2024 年,HTML5 已有 16 年历史。16 年间,主流浏览器对 HTML5、CSS3 和 JavaScript 的标准语法兼容性一直持续改进,22 年微软更是亲手盖上了 IE 棺材板。虽然 Safari(iOS 浏览器)的兼容性仍然备受诟病,但总体来说兼容成本已经变得可以接受。
主流框架已经成熟
前端最主流的两大框架 Vue 和 React 已经成熟。它们的成熟体现在多个方面:
- 从时间的角度看,截止 2024 年,React 已经发布了 11 年,而 Vue 已经发布了 10 年。经过多年的发展,前端开发者已经非常熟悉 React 和 Vue,能熟练地应用它们进行开发。
- 从语法的角度看,自 React16.8 发布 Hooks,以及 Vue3 发布 Composition API 以来,两大框架语法基本稳定,不再有大的变化。前端开发者可以更加专注于业务逻辑,无需过多担心框架语法的变动。
- 从未来发展方向看,React 目前致力于推广 React Server Component 1;Vue 则在尝试着无 VDom 的 Vapor 方向,并计划利用 Rust 重写 Vite 2。这表明旧领域不再有大的颠覆,两大框架已经正寻求新的发展领域。
混合开发已经成熟
混合开发是指将原生开发(Android 和 iOS)和 Web 开发结合起来的一种技术。简而言之,它将 H5 嵌入到移动应用内部运行。近些年,业界对混合开发的优势和缺陷已经有清晰的认识,并针对其缺陷进行了相应的优化。具体来说:
- 混合开发的优势包括开发速度快、一套代码适配 Android 和 iOS,以及实现代码的热更新。这意味着程序员能更快地编写跨平台应用,及时更新应用、修复缺陷;
- 混合开发的缺陷则是性能较差、加载受限于网络。针对这个缺陷,各大 App、以及云服务商如阿里云 3 和腾讯云 4 都推出了自己的离线包方案。离线包方案可以将网页的静态资源(如 HTML、CSS、JS、图片等)缓存到本地,用户访问 H5 页面时,可以直接读取本地的离线资源,从而提升页面的加载速度。可以说,接入离线包后,H5 不再有致命缺陷。
前端基建工具已经成熟
近些年来,业界最火的技术话题之一,就是用 Rust 替代前端基建,包括:用 Rust 替代 Webpack 的 Rspack;用 Rust 替代 Babel 的 SWC;用 Rust 替代 Eslint 的 OxcLint 等等。
前端开发者对基建工具抱怨,已经从「这工具能不能用」,转变为「这工具好不好用」。这种「甜蜜的烦恼」,只有基建工具成熟后才会出现。
综上所述,浏览器的兼容性提升、主流框架的成熟、混合开发的发展和前端基建工具的完善,使 H5 完全有能力承载「百亿补贴」业务。
H5 开发成本低
前文我们已经了解到,成熟的技术让 H5 可以实现「百亿补贴」的功能。现在我们介绍另一个原因——H5 开发成本低。
「百亿补贴」需要多个 H5
「百亿补贴」的方式,是一个常住的 H5,搭配上多个流动的 H5。(「常住」和「流动」是我借鉴「常住人口」和「流动人口」造的词)
- 常住 H5 链接保持不变。站外投放的链接基本都是常住 H5 的,站内首页入口链接也是常住 H5 的,这样方便用户二次访问。
- 流动 H5 链接位于常住 H5 的不同位置,比如头图、侧边栏等。时间不同、用户不同、算法不同,流动 H5 的链接都会不同,流动 H5 可以区分用户,方便分发流量。
具体来看,拼多多至少有三个流量的分发点,第一个是可点击的头图,第二个是列表上方的活动模块,第三个是右侧浮动的侧边栏,三者可以投放不同的链接。最近就分别投放 3.8 女神节链接、新人链接和品牌链接:
「百亿补贴」需要及时更新
不难想到,每到一个节日、每换一个品牌,「百亿补贴」就需要更新一次。
有时还需要为一些品牌定制化 H5 代码。如果使用其他技术栈,排期跟进通常会比较困难,但是使用 H5 就能够快速迭代并上线。
H5 投放成本低
我们已经「百亿补贴」使用 H5 技术栈的两个原因,现在来看第三个原因——H5 适合投放。
拼多多的崛起过程中,投放到其他 App 的链接功不可没。早期它通过微信等社交平台「砍一刀」的模式,低成本地吸引了大量用户。如今,它通过投放「百亿补贴」策略留住用户。
H5 的独特之处,在于它能够灵活地在多个平台上进行投放,其他技术栈很难有这样的灵活性。即使是今天,抖音、Bilibili 和小红书等其他 App 中,「百亿补贴」的 H5 链接也随处可见。
拼多多更是将 H5 这种灵活性发挥到极致,只要你有「百亿补贴」的链接,你甚至可以在微信、飞书、支付宝等地方直接查看「百亿补贴」 H5 页面。
综上所述,能开发、能快速开发、且开发完成后能大量投放,是「百亿补贴」青睐 H5 的原因。
H5 未来会如何发展
了解「百亿补贴」选择 H5 的原因后,我们来看看电商巨头对 H5 未来发展的影响。我认为有三个影响:
H5 数量膨胀,定制化要求苛刻
C 端用户黏性相对较低,换一个 App 的成本微不足道。近年 C 端市场增长缓慢,企业重点从获取更多的新客变成留住更多的老客,很难容忍用户丢失。因此其他企业投放活动 H5 时,企业必须也投放活动 H5,电商活动 H5 就变得越来越多。
这个膨胀的趋势不仅仅存在于互联网巨头的 App 中,中小型应用也不例外,甚至像 12306、中国移动、招商银行这种工具性极强的应用也无法幸免。
随着市场的竞争加剧,定制化要求也变得越来越苛刻,目的是让消费者区分各种活动。用互联网黑话来说,就是「建立用户心智」。在可预见的未来,尽管电商活动 H5 结构基本相同,但是它们的外观将变得千差万别、极具个性。
SSR 比例增加,CSR 占据主流
在各家 H5 数量膨胀、竞争激烈的情况下,一定会有企业为提升 H5 的秒开率接入 SSR,因此 SSR 的比例会增加。
但我认为 CSR 依然会是主流,主要是因为两个原因:
- SSR 需要额外的服务器费用,包括服务器的维护、扩容等。这对于中小型公司来说是一个负担。
- SSR 对程序员技术水平要求比 CSR 更高。SSR 需要程序员考虑更多的问题,例如内存泄露。使用 CSR 在用户设备上发生内存泄露,影响有限;但是如果在服务器上发生内存泄露,则是会占用公司的服务器内存,增加额外的成本和风险。
因此,收益丰厚、技术雄厚的公司才愿意使用 SSR。
Monorepo 比例会上升,类 Shadcn UI 组件库也许会兴起
如前所述,H5 的数量膨胀,代码复用就会被着重关注。我猜测更多企业会选择 Monorepo 管理方式。所谓 Monorepo,简单来说,就是将原本应该放到多个仓库的代码放入一个仓库,让它们共享相同的版本控制。这样可以降低代码复用成本。
定制化要求苛刻,我猜测社区中类似 Shadcn UI 的 H5 组件库或许会兴起。现有的 H5 组件库样式太单一,即使是 Shadcn UI,也很难满足国内 H5 的定制化需求。然而,Shadcn UI 的基本思路——「把源码下载到项目」,是解决定制化组件难复用的问题的好思路。因此,我认为类似 Shadcn 的 H5 组件库可能会逐渐兴起。
总结
本文介绍了我认为「百亿补贴」会选用 H5 的三大原因:
- H5 技术已经成熟
- H5 开发成本低
- H5 投放成本低
以及电商巨头对 H5 产生的三个影响:
- 数量膨胀,定制化要求苛刻
- SSR 比例增加,CSR 占据主流
- Monorepo 比例增加,类 Shadcn UI 组件库也许会兴起
总而言之,H5 开发会越来越专业,对程序员要求会越来越高。至于这种情况是好是坏,仁者见仁智者见智,欢迎大家在评论区沟通交流。
拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。
Footnotes
来源:juejin.cn/post/7344325496983732250
一个项目代码讲清楚DO/PO/BO/AO/E/DTO/DAO/ POJO/VO
在现代软件架构中,不同类型的类扮演着不同的角色,共同构成了一个清晰、模块化和可维护的系统。以下是对实体类(Entity)、数据传输对象(DTO)、领域对象(Domain Object)、持久化对象(Persistent Object)、业务对象(Business Object)、应用对象(Application Object)、数据访问对象(Data Access Object, DAO)、服务层(Service Layer)和控制器层(Controller Layer)的总体介绍:
不同领域作用
POJO (Plain Old Java Object)
- 定义:POJO 是一个简单的Java对象,它不继承任何特定的类或实现任何特定的接口,除了可能实现
java.io.Serializable
接口。它通常用于表示数据,不包含业务逻辑。 - 案例的体现:
UserEntity
类可以看作是一个POJO,因为它主要包含数据字段和标准的构造函数、getter和setter方法。UserDTO
类也是一个POJO,它用于传输数据,不包含业务逻辑。
VO (Value Object)
- 定义:VO 是一个代表某个具体值或概念的对象,通常用于表示传输数据的结构,不涉及复杂的业务逻辑。
VO(View Object)的特点:
- 展示逻辑:VO通常包含用于展示的逻辑,例如格式化的日期或货币值。
- 用户界面相关:VO设计时会考虑用户界面的需求,可能包含特定于视图的属性。
- 可读性:VO可能包含额外的描述性信息,以提高用户界面的可读性。
实体类(Entity)
- 作用:代表数据库中的一个表,是数据模型的实现,通常与数据库表直接映射。
- 使用场景:当需要将应用程序的数据持久化到数据库时使用。
数据传输对象(DTO)
- 作用:用于在应用程序的不同层之间传输数据,特别是当需要将数据从服务层传输到表示层或客户端时。
- 使用场景:进行数据传输,尤其是在远程调用或不同服务间的数据交换时。
领域对象(Domain Object)
- 作用:代表业务领域的一个实体或概念,通常包含业务逻辑和业务状态。
- 使用场景:在业务逻辑层处理复杂的业务规则时使用。
持久化对象(Persistent Object)
- 作用:与数据存储直接交互的对象,通常包含数据访问逻辑。
- 使用场景:执行数据库操作,如CRUD(创建、读取、更新、删除)操作。
业务对象(Business Object)
- 作用:封装业务逻辑和业务数据,通常与领域对象交互。
- 使用场景:在业务逻辑层实现业务需求时使用。
应用对象(Application Object)
- 作用:封装应用程序的运行时配置和状态,通常不直接与业务逻辑相关。
- 使用场景:在应用程序启动或运行时配置时使用。
数据访问对象(Data Access Object, DAO)
- 作用:提供数据访问的抽象接口,定义了与数据存储交互的方法。
- 使用场景:需要进行数据持久化操作时,作为数据访问层的一部分。
服务层(Service Layer)
- 作用:包含业务逻辑和业务规则,协调应用程序中的不同组件。
- 使用场景:处理业务逻辑,执行业务用例。
控制器层(Controller Layer)
- 作用:处理用户的输入,调用服务层的方法,并返回响应结果。
- 使用场景:处理HTTP请求和响应,作为Web应用程序的前端和后端之间的中介。
案例介绍
- 用户注册:
- DTO:用户注册信息的传输。
- Entity:用户信息在数据库中的存储形式。
- Service Layer:验证用户信息、加密密码等业务逻辑。
- 商品展示:
- Entity:数据库中的商品信息。
- DTO:商品信息的传输对象,可能包含图片URL等不需要存储在数据库的字段。
- Service Layer:获取商品列表、筛选和排序商品等。
- 订单处理:
- Domain Object:订单的业务领域模型,包含订单状态等。
- Business Object:订单处理的业务逻辑。
- DAO:订单数据的持久化操作。
- 配置加载:
- Application Object:应用程序的配置信息,如数据库连接字符串。
- API响应:
- Controller Layer:处理API请求,调用服务层,返回DTO作为响应。
案例代码
视图对象(VO)
一个订单系统,我们需要在用户界面展示订单详情:
// OrderDTO - 数据传输对象
public class OrderDTO {
private Long id;
private String customerName;
private BigDecimal totalAmount;
// Constructors, getters and setters
}
// OrderVO - 视图对象
public class OrderVO {
private Long id;
private String customerFullName; // 格式化后的顾客姓名
private String formattedTotal; // 格式化后的总金额,如"$1,234.56"
private String orderDate; // 格式化后的订单日期
// Constructors, getters and setters
public OrderVO(OrderDTO dto) {
this.id = dto.getId();
this.customerFullName = formatName(dto.getCustomerName());
this.formattedTotal = formatCurrency(dto.getTotalAmount());
this.orderDate = formatDateTime(dto.getOrderDate());
}
private String formatName(String name) {
// 实现姓名格式化逻辑
return name;
}
private String formatCurrency(BigDecimal amount) {
// 实现货币格式化逻辑
return "$" + amount.toString();
}
private String formatDateTime(Date date) {
// 实现日期时间格式化逻辑
return new SimpleDateFormat("yyyy-MM-dd").format(date);
}
}
实体类(Entity)
package com.example.model;
import javax.persistence.*;
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
// Constructors, getters and setters
public UserEntity() {}
public UserEntity(String username, String password) {
this.username = username;
this.password = password;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
数据传输对象(DTO)
package com.example.dto;
public class UserDTO {
private Long id;
private String username;
// Constructors, getters and setters
public UserDTO() {}
public UserDTO(Long id, String username) {
this.id = id;
this.username = username;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
领域对象(Domain Object)
package com.example.domain;
public class UserDomain {
private Long id;
private String username;
// Business logic methods
public UserDomain() {}
public UserDomain(Long id, String username) {
this.id = id;
this.username = username;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
// Additional domain-specific methods
}
领域对象通常包含业务领域内的概念和逻辑。在订单系统中,这可能包括订单状态、订单项、总价等。
package com.example.domain;
import java.util.List;
public class OrderDomain {
private String orderId;
private List items; // 订单项列表
private double totalAmount;
private OrderStatus status; // 订单状态
// Constructors, getters and setters
public OrderDomain(String orderId, List items) {
this.orderId = orderId;
this.items = items;
this.totalAmount = calculateTotalAmount();
this.status = OrderStatus.PENDING; // 默认状态为待处理
}
private double calculateTotalAmount() {
double total = 0;
for (OrderItemDomain item : items) {
total += item.getPrice() * item.getQuantity();
}
return total;
}
// 业务逻辑方法,例如更新订单状态
public void processPayment() {
// 处理支付逻辑
if (/* 支付成功条件 */) {
this.status = OrderStatus.PAYMENT_COMPLETED;
}
}
// 更多业务逻辑方法...
}
持久化对象(Persistent Object)
package com.example.model;
public class UserPersistent extends UserEntity {
// Methods to interact with persistence layer, extending UserEntity
}
业务对象(Business Object)
package com.example.service;
public class UserBO {
private UserDomain userDomain;
public UserBO(UserDomain userDomain) {
this.userDomain = userDomain;
}
// Business logic methods
public void performBusinessLogic() {
// Implement business logic
}
}
OrderBO
业务对象通常封装业务逻辑,可能包含领域对象,并提供业务操作的方法。
package com.example.service;
import com.example.domain.OrderDomain;
public class OrderBO {
private OrderDomain orderDomain;
public OrderBO(OrderDomain orderDomain) {
this.orderDomain = orderDomain;
}
// 执行订单处理的业务逻辑
public void performOrderProcessing() {
// 例如,处理订单支付
orderDomain.processPayment();
// 其他业务逻辑...
}
// 更多业务逻辑方法...
}
应用对象(Application Object)
package com.example.config;
public class AppConfig {
private String environment;
private String configFilePath;
public AppConfig() {
// Initialize with default values or environment-specific settings
}
// Methods to handle application configuration
public void loadConfiguration() {
// Load configuration from files, environment variables, etc.
}
// Getters and setters
}
数据访问对象(Data Access Object)
package com.example.dao;
import com.example.model.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserDAO extends JpaRepository {
// Custom data access methods if needed
UserEntity findByUsername(String username);
}
- OrderDAO
DAO 提供数据访问的抽象接口,定义了与数据存储交互的方法。在Spring Data JPA中,可以继承JpaRepository
并添加自定义的数据访问方法。
package com.example.dao;
import com.example.domain.OrderDomain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface OrderDAO extends JpaRepository { // 主键类型为String
// 自定义数据访问方法,例如根据订单状态查询订单
List findByStatus(OrderStatus status);
}
服务层(Service Layer)
package com.example.service;
import com.example.dao.UserDAO;
import com.example.dto.UserDTO;
import com.example.model.UserEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserDAO userDAO;
@Autowired
public UserService(UserDAO userDAO) {
this.userDAO = userDAO;
}
public UserDTO getUserById(Long id) {
UserEntity userEntity = userDAO.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
return convertToDTO(userEntity);
}
private UserDTO convertToDTO(UserEntity entity) {
UserDTO dto = new UserDTO();
dto.setId(entity.getId());
dto.setUsername(entity.getUsername());
return dto;
}
// Additional service methods
}
OrderService
服务层协调用户输入、业务逻辑和数据访问。它使用DAO进行数据操作,并可能使用业务对象来执行业务逻辑。
package com.example.service;
import com.example.dao.OrderDAO;
import com.example.domain.OrderDomain;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class OrderService {
private final OrderDAO orderDAO;
@Autowired
public OrderService(OrderDAO orderDAO) {
this.orderDAO = orderDAO;
}
public List findAllOrders() {
return orderDAO.findAll();
}
public OrderDomain getOrderById(String orderId) {
return orderDAO.findById(orderId).orElseThrow(() -> new RuntimeException("Order not found"));
}
public void processOrderPayment(String orderId) {
OrderDomain order = getOrderById(orderId);
OrderBO orderBO = new OrderBO(order);
orderBO.performOrderProcessing();
// 更新订单状态等逻辑...
}
// 更多服务层方法...
}
控制器层(Controller Layer)
package com.example.controller;
import com.example.dto.UserDTO;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public UserDTO getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
// Additional controller endpoints
}
总结
这些不同类型的类和层共同构成了一个分层的软件架构,每一层都有其特定的职责和功能。这种分层方法有助于降低系统的复杂性,提高代码的可维护性和可扩展性。通过将业务逻辑、数据访问和用户界面分离,开发人员可以独立地更新和测试每个部分,从而提高开发效率和应用程序的稳定性。
历史热点文章
- 命令模式(Command Pattern):网络爬虫任务队列实战案例分析
- 迭代器模式(Iterator Pattern):电商平台商品分类浏览实战案例分析
- 中介者模式(Mediator Pattern):即时通讯软件实战案例分析
- 备忘录模式(Memento Pattern):游戏存档系统实战案例分析
- 状态模式(State Pattern):电商平台订单状态管理实战案例分析
- 责任链模式(Chain of Responsibility Pattern):电商平台的订单审批流程实战案例分析
- 访问者模式(Visitor Pattern):电商平台商品访问统计实战案例分析
- 工厂方法模式(Factory Method Pattern): 电商多种支付实战案例分析
- 抽象工厂模式(Abstract Factory Pattern):多风格桌面应用实战案例分析
- 建造者模式(Builder Pattern): 在线订单系统实战案例分析
- 原型模式(Prototype Pattern): 云服务环境配置实战案例分析
- 适配器模式(Adapter Pattern):第三方支付集成实战案例分析
- 装饰器模式(Decorator Pattern):电商平台商品价格策略实战案例分析
- 单例模式(Singleton Pattern):购物车实战案例分析
来源:juejin.cn/post/7389212915302105098
七年前的一个思维,彻底改变了我的程序员职场轨迹
前言
Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。
最近学习了12个生财思维,受益匪浅,但是纸上得来终觉浅,绝知此事要躬行,没有亲身实践,怎么能更好的理解呢?
单纯的学习,尤其是思维工具类的学习,只看但不实践,是不会有太好的效果的。
课程中的案例虽然真实,但是每个人的眼界、能力不同,所以案例对自己只能开开眼,但自己对于思维模式的理解却不会有太多的帮助。
为了更好的理解每一个生财思维,我决定根据每一个生财思维去复盘过去十年间遇到的机遇,看看自己错过了什么,有抓住了什么,然后把学习过程中的思考重新整理出来。
希望对你有所帮助。
对标思维
什么是对标思维?
对标思维,对,对比。标,标杆。
什么是标杆,领域里面做的好的,拿到成绩的,就是标杆。
如何真正的运用好对标思维?
分成三步:
- 第一步,找到对标,通过搜索、付费都可以找到领域内出色的人。
- 第二步,研究他的成功路径,比如他做了哪些事情,学习了哪些知识,遇到了什么障碍, 又是如何解决的,或者关注他有什么好的特质,值得你去学习。
- 第三步,复盘自己的做事路径和方法,对比标杆的方法、思维、路径,针对性的改进自己。
对标思维,每一个人都在下意识的应用到。
比如:
- 技术方案设计,我们对标大厂去设计我们的方案,为什么大厂要这样设计
- 身边的同事去了大厂,我们对标他,去看看他作对了什么,学习了什么知识
- 接手新的工作,先去看看别人是怎么做的
对标并不是对比,对比着重于把两个或多个事物放在一起,比较它们之间的相同点和不同点。
而对标更强调以优秀对象为目标进行追赶和超越
对标思维的应用
大学毕业后,我选择留在了老家,我的室友毕业去了北京。他去做了互联网,而我只能选择传统信息企业。
北京的生活节奏与二线城市很不一样,上学时我们一起开黑打游戏,毕业后,只剩下了留在济南的几个人,还能一起开黑娱乐,室友因为工作原因,工作日下班几乎无法和我们打游戏。
后来无意间得知,他的薪资比我高出一倍之多,我感到很震惊。(此时我无意间找到了对标)
同样的大学,同样的专业,相差不大的成绩,在我知道后,我不禁陷入思考,为什么收入会有着如此大的差距?
上面提到,对标并不是对比,如果仅仅从薪资角度来对比,那我们之间的差距无疑也太大了。
运用对标思维,我发现他做的对的事情,是去了北京。17年的互联网行业依然如日中天,大城市的机会、薪资,都是二线城市无法比较的,彼时只要有一些热门技术的经验,哪怕是培训机构出来的,都很容易找到工作。
我不禁反思,为什么当时的我毕业时,为什么没有作出这个选择?
大学同学,大概都来源于五湖四海,而我的室友的家在东北,他对我们所在大学的城市,一定是不会有太多的感情的,所以对他而言,毕业后,自然是像北京这样的大城市,有着更大的吸引力。
而彼时的我,还处于一个舒适区里。我从上学开始就没离开过济南,外面的城市对我而言是陌生的,而在家不用租房,上下班有车开不需要坐地铁,周末有朋友一起吃饭喝酒,上班时间因为城市不大所以也不远,因此缺少了闯荡的勇气。
当我认识到这一点后,便暗暗下决心要出去闯闯看看,随后没过多久,我就在没有任何准备的情况下,裸辞去了北京。
当然因为缺少准备,我吃了不少苦头,不过这个都是后话了,有兴趣的也可以看看我之前的文章。
这件事已经过去了7年之久,我依然记忆犹新,从对标、思考、选择的路径,可以很顺畅的把这件事情完整的回忆出来,因为我在不经意间用了对标思维,做出了很大的改变,也让我认识了更多的朋友,去了更大规模的公司。
看到这里,我猜你可能心里会想,你这个思考路径,并不一定有太大的参考价值呀。你的对标不用你寻找,就在你身边,并且薪资对比如此明显,你自然很容易就会去思考如何去改变。
但是我想说,想主动应用对标思维,没有这么简单。
故事继续。
找到目标
从小带着我玩到大的哥哥,也是程序员,我选择程序员行业,也是受到了我哥哥的影响。
在那个我还没有进入社会,沉浸在大学的美好时光中的时候,哥哥就已经从腾讯跳槽到阿里,并且在工作的城市买房、定居了。
其实我哥哥也不只一次的和我讲过,提前准备好面试内容,参加校招,可能会比毕业后再进入大厂的难度要小上很多。
但我始终没有听进去,在学习了一些内容之后,便放弃了。
理论上,我在上学的时候,就已经有了这么好的一个榜样,我如果早早去对标哥哥的工作路径,了解大厂的面试标准,毕业就选择进入大厂,我几乎可以少走4年的弯路,但我还是在毕业后,选择留在了济南。
瑞·达利欧在《原则》书中提到,
个人进化过程(即我在上一条描述的循环)通过5个不同的步骤发生。如果你能把那5件事都做好,你几乎肯定可以成功。
这五步大概是:
1.有明确的目标。
2.找到阻碍你实现这些目标的问题,并且不容忍问题。
3.准确诊断问题,找到问题的根源。
4.规划可以解决问题的方案。
5.做一切必要的事来践行这些方案,实现成果。
我们就聊第一步,有明确的目标。
大四那年我在干什么?尝试着考了研,尝试着去了一家公司实习,但大部分时间还是在打游戏。错过了秋招春招,最后毕业才拿到一家offer,只能选择入职。
大学期间,我很明显是没有目标的。比如,毕业后想去什么样的公司,想拥有多少收入,去哪个城市发展。
因此即使有再好的榜样在我身边,我也无动于衷。
但当我工作之后,发现了技术上的差距,发现了工资上的差距,发现了工作环境上的差距,自己不想再浑浑噩噩下去,因此才有了改变的动力。
因此让心里埋下一个想法的种子,找到目标,才是运用思维工具的第一步。
具体如何找到目标,进行拆分,可以看看我之前的这篇文章。
说在最后
好了,文章到这里就要结束了,总结一下。
对标思维从概念上看其实不难,只需要三步即可找到对标,研究路径,复盘改进。
但是并非有了这个思维就可以立即应用,还是要有明确的目标,知道自己想要什么,才能更好的利用对标思维。
欢迎你在评论区和我分享,也希望你点赞、评论、收藏,让我知道对你有所收获,这对我来说很重要。也欢迎你加我的wx:Ldhrlhy10,一起交流~
本篇文章是第42篇原创文章,2024目标进度42/100,欢迎有趣的你,关注我。
来源:juejin.cn/post/7388488504055955492
我去,怎么http全变https了
项目场景:
在公司做的一个某地可视化项目。
部署采用的是前后端分离部署,图片等静态资源请求一台minio服务器。
项目平台用的是http
图片资源的服务器用的是https
问题描述
在以https请求图片资源时,图片请求成功报200。
【现象1】: 继图片后续的请求,后续此域名和子域名下的的url均由http变为https
【现象2】: 界面阻塞报错,无法交互
原因分析:
经过现象查阅,发现出现该现象与浏览器的HSTS有关。
什么是HSTS ?
HTTP的Strict-Transport-Security
(HSTS)请求头是一种网络安全机制,用于告诉浏览器仅通过HTTPS与服务器通信,而不是HTTP。它的作用主要有以下几点:
- 防止协议降级攻击:当浏览器接收到HSTS响应头后,它会将该网站添加到HSTS列表中,并在后续的访问中强制使用HTTPS,即使用户或攻击者尝试通过HTTP访问该网站,浏览器也会自动将其重定向到HTTPS。
- 减少中间人攻击的风险:通过确保所有通信都通过加密的HTTPS进行,可以降低中间人攻击(MITM)的风险,因为攻击者无法轻易地截获或篡改传输的数据。
- 提高网站的安全性:HSTS可以作为网站安全策略的一部分,帮助保护用户的敏感信息,如登录凭据、支付信息等。
- 简化安全配置:对于网站管理员来说,HSTS可以减少需要维护的安全配置,因为浏览器会自动处理HTTPS的重定向。
- 提高用户体验:由于浏览器会自动处理重定向,用户不需要担心访问的是HTTP还是HTTPS版本,可以更顺畅地浏览网站。
HSTS的配置可以通过max-age
指令来设置浏览器应该记住这个策略的时间长度,还可以使用includeSubDomains
指令来指示所有子域名也应该遵循这个策略。此外,还有一个preload
选项,允许网站所有者将他们的网站添加到浏览器的预加载HSTS列表中,这样用户在第一次访问时就可以立即应用HSTS策略。
于是在我发现该相关的响应头确有此物
解决方案:
那就取决于服务器是在哪里设置的该请求头。可能是在Nginx
,Lighttpd
,PHP
等等,将该响应头配置去除
来源:juejin.cn/post/7382386471272448035
再有人问你WebSocket为什么牛逼,就把这篇文章发给他!
点赞再看,Java进阶一大半
2008年6月诞生了一个影响计算机世界的通信协议,原先需要二十台计算机资源才能支撑的业务场景,现在只需要一台,这得帮"抠门"老板们省下多少钱,它就是大名鼎鼎的WebSocket协议。很快在下一年也就是2009年的12月,Google浏览器就宣布成为第一个支持WebSocket标准的浏览器。
WebSocket的推动者和设计者就是下面的Michael Carter,他设计的WebSocket协议技术现在每天在全地球有超过20亿的设备在使用。
逮嘎猴,我是南哥。
一个Java进阶的领路人,今天指南的是WebSocket,跟着南哥我们一起Java进阶。
本文收录在我开源的《Java进阶指南》中,一份帮助小伙伴们进阶Java、通关面试的Java学习面试指南,相信能帮助到你在Java进阶路上不迷茫。南哥希望收到大家的 ⭐ Star ⭐支持,这是我创作的最大动力。GitHub地址:github.com/hdgaadd/Jav…。
1. WebSocket概念
1.1 为什么会出现WebSocket
面试官:有了解过WebSocket吗?
一般的Http请求我们只有主动去请求接口,才能获取到服务器的数据。例如前后端分离的开发场景,自嘲为切图仔实际扮猪吃老虎的前端大佬找你要一个配置信息
的接口,我们后端开发三下两下开发出一个RESTful
架构风格的API接口,只有当前端主动请求,后端接口才会响应。
但上文这种基于HTTP的请求-响应模式并不能满足实时数据通信的场景,例如游戏、聊天室等实时业务场景。现在救世主来了,WebSocket作为一款主动推送技术,可以实现服务端主动推送数据给客户端。大家有没听说过全双工、半双工的概念。
全双工通信允许数据同时双向流动,而半双工通信则是数据交替在两个方向上传输,但在任一时刻只能一个方向上有数据流动
HTTP通信协议就是半双工,而数据实时传输需要的是全双工通信机制,WebSocket采用的便是全双工通信。举个微信聊天的例子,企业微信炸锅了,有成百条消息轰炸你手机,要实现这个场景,大家要怎么设计?用iframe、Ajax异步交互技术配合以客户端长轮询不断请求服务器数据也可以实现,但造成的问题是服务器资源的无端消耗,运维大佬直接找到你工位来。显然服务端主动推送数据的WebSocket技术更适合聊天业务场景。
1.2 WebSocket优点
面试官:为什么WebSocket可以减少资源消耗?
大家先看看传统的Ajax长轮询和WebSocket性能上掰手腕谁厉害。在websocket.org网站提供的Use Case C
的测试里,客户端轮询频率为10w/s,使用Poling长轮询每秒需要消耗高达665Mbps,而我们的新宠儿WebSocet仅仅只需要花费1.526Mbps,435倍的差距!!
为什么差距会这么大?南哥告诉你,WebSocket技术设计的目的就是要取代轮询技术和Comet技术。Http消息十分冗长和繁琐,一个Http消息就要包含了起始行、消息头、消息体、空行、换行符,其中请求头Header非常冗长,在大量Http请求的场景会占用过多的带宽和服务器资源。
大家看下百度翻译接口的Http请求,拷贝成curl命令是非常冗长的,可用的消息肉眼看过去没多少。
curl ^"https://fanyi.baidu.com/mtpe-individual/multimodal?query=^%^E6^%^B5^%^8B^%^E8^%^AF^%^95&lang=zh2en^" ^
-H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" ^
-H "Accept-Language: zh-CN,zh;q=0.9" ^
-H "Cache-Control: max-age=0" ^
-H "Connection: keep-alive" ^
-H ^"Cookie: BAIDUID=C8FA8569F446CB3F684CCD2C2B32721E:FG=1; BAIDUID_BFESS=C8FA8569F446CB3F684CCD2C2B32721E:FG=1; ab_sr=1.0.1_NDhjYWQyZmRjOWIwYjI3NTNjMGFiODExZWFiMWU4NTY4MjA2Y2UzNGQwZjJjZjI1OTdlY2JmOThlNzk1ZDAxMDljMTA2NTMxYmNlM1OTQ1MTE0ZTI3Y2M0NTIzMzdkMmU2MGMzMjc1OTRiM2EwNTJQ==; RT=^\^"z=1&dm=baidu.com&si=b9941642-0feb-4402-ac2b-a913a3eef1&ss=ly866fx&sl=4&tt=38d&bcn=https^%^3A^%^2F^%^2Ffclog.baidu.com^%^2Flog^%^2Fweirwood^%^3Ftype^%^3Dp&ld=ccy&ul=jes^\^"^" ^
-H "Sec-Fetch-Dest: document" ^
-H "Sec-Fetch-Mode: navigate" ^
-H "Sec-Fetch-Site: same-origin" ^
-H "Sec-Fetch-User: ?1" ^
-H "Upgrade-Insecure-Requests: 1" ^
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" ^
-H ^"sec-ch-ua: ^\^"Not/A)Brand^\^";v=^\^"8^\^", ^\^"Chromium^\^";v=^\^"126^\^", ^\^"Google Chrome^\^";v=^\^"126^\^"^" ^
-H "sec-ch-ua-mobile: ?0" ^
-H ^"sec-ch-ua-platform: ^\^"Windows^\^"^" &
而WebSocket是基于帧传输的,只需要做一次握手动作就可以让客户端和服务端形成一条通信通道,这仅仅只需要2个字节。我搭建了一个SpringBoot集成的WebSocket项目,浏览器拷贝WebSocket的Curl命令十分简洁明了,大家对比下。
curl "ws://localhost:8080/channel/echo" ^
-H "Pragma: no-cache" ^
-H "Origin: http://localhost:8080" ^
-H "Accept-Language: zh-CN,zh;q=0.9" ^
-H "Sec-WebSocket-Key: VoUk/1sA1lGGgMElV/5RPQ==" ^
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" ^
-H "Upgrade: websocket" ^
-H "Cache-Control: no-cache" ^
-H "Connection: Upgrade" ^
-H "Sec-WebSocket-Version: 13" ^
-H "Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits"
如果你要区分Http请求或是WebSocket请求很简单,WebSocket请求的请求行前缀都是固定是ws://
。
2. WebSocket实践
2.1 集成WebSocket服务器
面试官:有没动手实践过WebSocket?
大家要在SpringBoot使用WebSocket的话,可以集成spring-boot-starter-websocket
,引入南哥下面给的pom依赖。
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
dependencies>
感兴趣点开spring-boot-starter-websocket
依赖的话,你会发现依赖所引用包名为package jakarta.websocket
。这代表SpringBoot其实是集成了Java EE开源的websocket项目。这里有个小故事,Oracle当年决定将Java EE移交给Eclipse基金会后,Java EE就进行了改名,现在Java EE更名为Jakarta EE。Jakarta是雅加达的意思,有谁知道有什么寓意吗,评论区告诉我下?
我们的程序导入websocket依赖后,应用程序就可以看成是一台小型的WebSocket服务器。我们通过@ServerEndpoint可以定义WebSocket服务器对客户端暴露的接口。
@ServerEndpoint(value = "/channel/echo")
而WebSocket服务器要推送消息给到客户端,则使用package jakarta.websocket
下的Session对象,调用sendText
发送服务端消息。
private Session session;
@OnMessage
public void onMessage(String message) throws IOException{
LOGGER.info("[websocket] 服务端收到客户端{}消息:message={}", this.session.getId(), message);
this.session.getAsyncRemote().sendText("halo, 客户端" + this.session.getId());
}
看下getAsyncRemote
方法返回的对象,里面是一个远程端点实例。
RemoteEndpoint.Async getAsyncRemote();
2.2 客户端发送消息
面试官:那客户端怎么发送消息给服务器?
客户端发送消息要怎么操作?这点还和Http请求很不一样。后端开发出接口后,我们在Swagger填充参数,点击Try it out
,Http请求就发过去了。
但WebSocket需要我们在浏览器的控制台上操作,例如现在南哥要给我们的WebSocket服务器发送Halo,JavaGetOffer
,可以在浏览器的控制台手动执行以下命令。
websocket.send("Halo,JavaGetOffer");
实践的操作界面如下。
来源:juejin.cn/post/7388025457821810698
前端开发中过度封装的现象与思考
前言
作为公司内的一名高级前端码喽,大大小小也封装过了不少组件和功能,我逐渐意识到封装并非全是优点,也会存在一些不可忽视的潜在劣势。
在项目中,我们急切地对各种功能和 UI 进行封装,却在不经意间忽略了封装可能带来的额外成本与潜在问题。比如,在之前的一个项目中,为了实现一个看似简单的列表展示功能,我将数据获取、渲染逻辑以及交互处理都塞进了一个繁杂的组件中。后续当需要对列表的某一特定功能进行细微调整时,由于封装的过度复杂,修改工作变得极为棘手,耗费了大量时间去梳理内部的逻辑关系。
还有一次我在对一个表单验证功能的封装时,为追求过高的通用性,添加了过多的配置选项和繁杂的验证规则。这不但增加了代码量,还使得新加入团队的成员在使用时感到困惑,理解和运用这个封装的成本大幅提高。如果让我在写标准代码和学习过度封装的组件之间做选择,我绝对毫不犹豫的选择写标准代码。
一、前端功能封装的优势
- 可以提高代码复用性
在众多项目中,常碰到类似的数据请求、表单验证等功能需求。将这些功能封装成独立的函数或模块,能极大提升代码的复用程度。例如,我们成功封装了一个通用的数据获取函数,在不同页面中仅需传入各异的参数,就能顺利获取所需数据,无需反复编写请求逻辑。 - 有效增强代码的可维护性
封装后的功能代码相对独立,当需要对功能进行修改或优化时,只需在封装的模块内操作,不会对其他使用该功能的部分产生任何影响。如此一来,代码的维护工作变得更加清晰、易于掌控。 - 大幅提升代码的可读性
通过为封装的功能赋予清晰、有意义的函数名和详尽的参数说明,其他开发者能够迅速理解其功能和使用方式,这样也极大提高团队协作的效率。写到这里我突然想起曾经在一个屎山项目中看到过的aaaa、Areyouok、jiashizheng等变量和函数名,我花了好久的时间才把它们修改正常...
二、前端功能封装的劣势
- 事极必反
有时为追求极致的封装效果,可能会对一些简单且复用频率不高的功能进行封装,这反倒会增加代码的复杂程度和理解成本。例如,一个仅仅用于计算两个数之和的简单功能,若过度封装,可能会令后续的开发者感到迷茫。
代码示例:
function add(a, b) {
return a + b;
}
// 过度封装
function complexAdd(a, b) {
if (typeof a!== 'number' || typeof b!== 'number') {
throw new Error('输入必须为数字');
}
const result = a + b;
// 一些额外的复杂逻辑
return result;
}
在一个小型项目中,仅仅为了计算两个数字的和,使用了复杂的封装函数
complexAdd
,导致新同事在理解和使用时花费了过多时间,而原本简单的add
函数就能满足需求。 - 可能隐藏底层实现细节
过度封装或许会让使用功能的开发者对其内部实现一无所知。当问题出现时,可能需要耗费更多时间去理解封装内部的逻辑,进而影响问题的排查和解决效率。
三、UI 二次封装的优势
- 成功统一风格和交互
在大型项目中,保障 UI 的一致性至关重要。通过对基础 UI 组件进行二次封装,能够明确统一的样式、交互行为和响应式规则。例如,对按钮组件进行二次封装,设定不同状态下的颜色、尺寸和点击效果。
- 显著提高开发效率
开发人员能够直接运用封装好的 UI 组件,迅速搭建页面,无需在样式和交互的调整上耗费大量时间。
- 方便后期维护和更新
当需要对 UI 进行整体风格的调整或优化时,只需修改封装的组件,所有使用该组件的页面都会自动更新,大幅减少了维护的工作量。
四、UI 二次封装的劣势
- 过度封装的危害
- 增加不必要的代码量和复杂度,致使应用的加载性能降低。例如,一个简单的输入框组件,如果过度封装了很多复杂的逻辑和样式,可能会使代码体积过大。
- 可能引入过多的抽象层次,让代码变得难以理解和调试。复杂的封装结构可能让开发者在排查 UI 问题时感到无从下手。
- 过度复杂的封装在频繁的渲染和更新操作中,可能会导致性能瓶颈,影响用户体验。
代码示例:
// 过度封装的输入框组件
class OverlyComplexInput extends React.Component {
constructor(props) {
super(props);
this.state = { value: '' };
}
handleChange = (e) => {
// 复杂的处理逻辑
this.setState({ value: e.target.value });
// 更多的额外操作
}
render() {
return (
<input
value={this.state.value}
onChange={this.handleChange}
// 过多的样式和属性设置
/>
);
}
}
在一个性能要求较高的页面中,使用了过度封装的输入框组件,导致页面加载缓慢,用户输入时出现明显的卡顿。
- 灵活性受限
过于严格的封装可能限制了开发者在特定场景下对 UI 进行个性化定制的能力。有时候,某些页面可能需要独特的样式或交互效果,而过度封装的组件无法满足这些特殊需求。 - 版本兼容性问题
当对封装的 UI 组件进行更新时,可能会与之前使用该组件的页面产生兼容性问题。新的版本可能改变了组件的行为、样式或接口,导致使用旧版本组件的页面出现显示异常或功能失效。
所以在实际的开发过程中,我们需要权衡封装带来的好处和潜在的问题。封装应该是有针对性的,基于实际的复用需求和项目的规模。同时,要保持封装的适度性,避免过度封装带来的负面影响。只有这样,才能真正提高前端开发的效率和质量。
来源:juejin.cn/post/7387731346733121551
MySQL 9.0 创新版发布,大失所望。。
大家好,我是程序员鱼皮。2024 年 7 月 1 日,MySQL 发布了 9.0 创新版本。区别于我们大多数开发者常用的 LTS(Long-Term Support)长期支持版本,创新版本的发布会更频繁、会更快地推出新的特性和变更,可以理解为 “尝鲜版”,适合追求前沿技术的同学体验。
我通过阅读官方文档,完整了解了本次发布的新特性,结果怎么说呢,唉,接着往下看吧。。。
下面鱼皮带大家 “尝尝鲜”,来看看 MySQL 9.0 创新版本有哪些主要的变化。
新特性
1、Event 相关 SQL 语句可以被 Prepared
在 MySQL 中,事件(Events)是一种可以在预定时间执行的调度任务,比如定期清理数据之类的,就可以使用事件。
MySQL 9.0 对事件 SQL 提供了 Prepared 支持,包括:
- CREATE EVENT
- ALTER EVENT
- DROP EVENT
prepared 准备语句是一种预编译的 SQL 语句模板,可以在执行时动态地传入参数,从而提高查询的性能和安全性。
比如下面就是一个准备语句,插入的数据可以动态传入:
PREPARE stmt_insert_employee FROM 'INSERT INTO employees (name, salary) VALUES (?, ?)';
2、Performance Schema 新增 2 张表
MySQL 的 Performance Schema 是一个用于监视 MySQL 服务器性能的工具。它提供了一组动态视图和表,记录了 MySQL 服务器内部的活动和资源使用情况,帮助开发者进行性能分析、调优和故障排除。
本次新增的表:
- variables_metadata 表:提供关于系统变量的一般信息。包括 MySQL 服务器识别的每个系统变量的名称、作用域、类型、范围和描述。此表的 MIN_VALUE 和 MAX_VALUE 列旨在取代已弃用的 variables_info 表的 MIN_VALUE 和 MAX_VALUE 列。
- global_variable_attributes 表:提供有关服务器分配给全局系统变量的属性-值对的信息。
3、SQL 语句优化
现在可以使用以下语法将 EXPLAIN ANALYZE(分析查询执行计划和性能的工具)的 JSON 输出保存到用户变量中:
EXPLAIN ANALYZE FORMAT=JSON INTO @variable select_stmt
随后,可以将这个变量作为 MySQL 的任何 JSON 函数的 JSON 参数使用。
4、向量存储
AI 的发展带火了向量数据库,我们可以利用向量数据库存储喂给 AI 的知识库和文档。
虽然 MySQL 官方更新日志中并没有提到对于向量数据存储的支持,但是网上有博主在 MySQL 9.0 社区版中进行了测试,发现其实已经支持了向量存储,如图:
在此之前,MySQL 推出过一个专门用于分析处理和高性能查询的数据库变体 HeatWave,本来以为只会在 HeatWave 中支持向量存储,没想到社区版也能使用。如果是真的,那可太好了。
5、其他
此外,还优化了 Windows 系统上 MySQL 的安装和使用体验。
废弃和移除
1)在 MySQL 8.0 中,已移除了在 MySQL 8.0 中已废弃的 mysql_native_password 认证插件,并且服务器现在拒绝来自没有 CLIENT_PLUGIN_AUTH 能力的旧客户端程序的 mysql_native 认证请求。为了向后兼容性,mysql_native_password 仍然在客户端上可用;客户端内置的认证插件已转换为动态加载插件。
这些更改还涉及移除以下服务器选项和变量:
- --mysql-native-password 服务器选项
- --mysql-native-password-proxy-users 服务器选项
- default_authentication_plugin 服务器系统变量
2)Performance Schema 中 variables_info 表的 MIN_VALUE 和 MAX_VALUE 列现在已废弃,并可能在将来的 MySQL 版本中移除。开发者应该改为使用 variables_metadata 表的 MIN_VALUE 和 MAX_VALUE 列。
3)ER_SUBQUERY_NO_1_ROW 已从忽略包含 IGNORE 关键字的语句的错误列表中移除。这样做的原因如下:
- 忽略这类错误有时会导致将 NULL 插入非空列(对于未转换的子查询),或者根本不插入任何行(使用 subquery_to_derived 的子查询)。
- 当子查询转换为与派生表联接时,行为与未转换查询不同。
升级到 9.0 后,如果包含 SELECT 语句的 UPDATE、DELETE 或 INSERT 语句使用了包含多行结果的标量子查询,带有 IGNORE 关键字的语句可能会引发错误。
总结
看了本次 MySQL 9.0 创新版的更新,说实话,大失所望。在这之前,网上有很多关于 MySQL 9.0 版本新特性的猜测,结果基本上都没有出现。毕竟距离 MySQL 上一次发布的大版本 8.0 已经时隔 6 年,本来以为这次 MySQL 会有一些王炸的新特性,结果呢,本次除了修复了 100 多个 Bug 之外,几乎没啥对开发者有帮助的点。别说没帮助了,我估计很多同学在看这篇文章前都没接触过这些有变更的特性。
我们最关注的,无非就是使用难度、成本和性能提升对吧,最好是什么代码都不用改,直接升级个数据库的版本,性能提升个几倍,还能跟老板吹一波牛皮。
你看看隔壁的 PostgreSQL,这几年,都已经从 11 更新到 17 版本了,AI 时代人家也早就能通过插件支持存储向量数据了。MySQL 你这真的是创新么?
最后,MySQL 9.0 创新版本的下载地址我就不放了,咱还是老老实实用 5.7 和 8.0 版本,MySQL 的新版本,还有很长一条路要走呀!
来源:juejin.cn/post/7387999151411920931
初中都没念完的我,是怎么从IT这行坚持下去的...
大家好,我是一名二线(伪三线,毕竟连续两年二线城市了)的程序员。
现阶段状态在职,28岁,工作了10年左右,码农从事了5年左右,现薪资9k左右。如文章标题所说,初二辍学,第一学历中专,自己报的成人大专。
在掘金也看了不少经历性质的文章,大多都是很多大牛的文章,在大城市的焦虑,在大厂的烦恼,所以今天换换口味,看一看我这个没有学历的二线的程序员的经历。
1.辍学
我是在初二的时候辍学不上的,原因很简单,太二笔了。
现在想来当时的我非常的der,刚从村里的小学出来上中学之后(我还是年级第7名进中学,殊不知这就是我这辈子最好的成绩了),认为别人欺负我我就一定要还回来,完全不知道那是别人的地盘,嚣张的一批,不出意外就被锤了,但是当时个人武力还是很充沛的,按着一个往地上锤,1V7的战绩也算可以了。自此之后,我就开始走上了不良的道路,抽烟喝酒打架,直到中专毕业那天。
我清楚的记得我推着电车望着天,心里只想着一个问题,我毕业了,要工作了,我除了打游戏还会什么呢,我要拿什么生存呢...
这是当时我心里真实的想法,我好像就在这一刻、这一瞬间长大了。
2.深圳之旅
因为我特别喜欢玩游戏,而且家里电脑总是出问题,所以我就来到了我们这当地的一个电脑城打工,打了半年工左右想学习一下真正的维修技术,也就是芯片级维修,毅然决然踏上了深圳的路。
在深圳有一家机构叫做迅维的机构,还算是在业内比较出名的这么一个机构,学习主板显卡的维修,学习电路知识,学习手机维修的技术。现在的我想想当时也不太明白我怎么敢自己一个人就往深圳冲,家里人怎么拦着我都没用,当时我就好像着了魔一样必须要去...
不过在深圳的生活真的很不错,那一年的时光仍旧是我现在非常怀念的,早晨有便宜好吃的肠粉、米粉、甜包,中午有猪脚饭、汤饭、叉烧饭,晚上偶尔还会吃一顿火锅,来自五湖四海的朋友也是非常的友好,教会了我很多东西,生活非常的不错。
3.回家开店
为什么说我工作了10年左右呢,因为我清楚记得我18岁那年在本地开了一个小店,一个电脑手机维修的小店。现在想想我当时也是非常的二笔,以下列举几个事件:
- 修了一个显示器因为没接地线烧了,还跟人家顾客吵了一架。
- 修苹果手机翘芯片主板线都翘出来了,赔了一块。
- 自己说过要给人家上门保修,也忘了,人家一打电话还怼了一顿。
- 因为打游戏不接活儿。
以上这几种情况比比皆是,哪怕我当时这么二笔也是赚了一些钱,还是可以维持的,唯一让我毅然决然转行的就是店被偷了,大概损失了顾客机器、我的机器、图纸、二手电脑等一系列的商品,共计7万元左右,至今仍没找回!
4.迷茫
接下来这三年就是迷茫的几年了,第一件事就是报成人大专,主要从事的行业就杂乱无章了,跟我爸跑过车,当过网吧网管,超市里的理货员,但是这些都不是很满意,也是从这时候开始接触了C和C++开始正式踏入自学编程的路,直到有一次在招聘信息里看到java,于是在b站开始自学java,当时学的时候jdk还是1.6,学习资料也比较古老,但是好歹是入了门了。
5.入职
在入门以后自我感觉非常良好,去应聘了一个外包公司,当时那个经理就问了我一句话,会SSM吗,我说会,于是我就这么入职了,现在想想还是非常幸运的。
当时的我连SSM都用不明白,就懂一些java基础,会一些线程知识,前端更是一窍不通,在外包公司这两年也是感谢前辈带我做一些项目,当时自己也是非常争气,不懂就学,回去百度、b站、csdn各种网站开始学习,前端学习了H5、JS、CSS还有一个经典前端框架,贤心的Layui。
干的这两年我除了学习态度非常认真,工作还是非常不在意,工作两年从来没有任何一个月满勤过,拖延症严重,出现问题从来就是逃避问题,职场的知识是一点也不懂,当时的领导也很包容我,老板都主持了我的婚礼哈哈哈。但是后来我也为我的嚣张买了单,怀着侥幸心理喝了酒开车,这一次事情真真正正的打醒了我,我以后不能这样了...
6.第二家公司
在第二家公司我的态度就变了很多很多 当时已经25岁了,开始真真正正是一个大人了,遵纪守法,为了父母和家人考虑,生活方面也慢慢的好了起来(在刚结婚两年和老婆经常吵架,从这时候开始到现在没有吵过任何架了就),生活非常和睦。工作方面也是从来不迟到早退,听领导的安排,认真工作,认真学习,认识了很多同行,也得到了一些人的认可,从那开始才开始学习springboot、mq、redis、ES一些中间件,学习了很多知识,线程知识、堆栈、微服务等一系列的知识,也算是能独当一面了。但好景不长,当时我的薪资已经到13K左右了,也是因为我们部门的薪资成本、服务器成本太大,入不敷出,公司决定代理大厂的产品而不是自研了,所以当时一个部门就这么毕业了...
7.现阶段公司
再一次找工作就希望去一些自研的大公司去做事情了,但是也是碍于学历,一直没有合适的,可以说是人厌狗嫌,好的公司看不上我,小公司我又不想去,直到在面试现在公司的时候聊得非常的好,也是给我个机会,说走个特批,让我降薪入职,算上年终奖每个月到手大概10k(构成:9k月薪,扣除五险一金到手7.5k,年终奖27k,仨月全薪,所以每个月到手10k),我也是本着这个公司非常的大、非常的稳定、制度非常健全、工作也不是很忙也就来了,工作至今。
总结
- 任何时候想改变都不晚,改变不了别人改变自己。
- 面对问题绝对不能逃避,逃避没有任何用,只有面对才能更好的继续下去。
- 不要忘了自己为什么踏入这行,因为我想做游戏。
- 解决问题不要为了解决而解决,一定要从头学到尾,要不然以后出现并发问题无从下手。
- 任何事情都要合规合法。
- 工作了不要脱产做任何事情,我是因为家里非常支持,我妈至今都难以相信我能走到今天(我认为我大部分是运气好,加上赶上互联网浪潮的尾巴)。
- 最重要的,任何事情都没有家人重要,想回家就回家吧,挣钱多少放一边,IT行业找个副业还是非常简单的,多陪陪他们!
来源:juejin.cn/post/7309645869644480522
部署完了,样式不生效差点让我这个前端仔背锅
大家好,作为今年刚毕业的大聪明(小🐂🐎),知道今年不好就业,费心费力的面试终于进了个小公司,然后我的奇妙之旅开始了。
叮!闹钟响了!牙都不刷了,直接起床出门去上班,身为🐂🐎就要有早起吃草干活的觉悟,所以我是个很合格的食草动物。只不过,下面的信息,让我一天当动物的好心情都没了。
部署?因为该公司人数比较少,也没有自动化部署操作,只能让前端build一下dist包然后丢给后端,让后端部署,然后该项目也比较急,我刚来,也不好意思@后端,恰巧跟我对接的后端也是刚来不熟悉,所以我就跟负责人继续扯皮拖延时间了。(真是不好意思,假的)↓↓↓
终于经过不断努力(后端),部署好了,但是,我的样式乱了,为什么呢?看了一下网络和源代码,样式文件都请求到了,问了后端,一致说是我前端的问题,我懵了,然后快马加鞭查找问题
解决
ok,那我先看打包的dist文件有样式有问题吗,使用npm安装个 http-server 运行打包后的index.html,运行打开运行后的地址,欧克样式没乱,文件引用也没问题,那到底是什么鬼影响的呢?
在我百思不得其解的时候,我打开了神器--csdn,搜索打包dist部署后样式文件不生效,就遇到了这个神文t.csdnimg.cn/WbQyq
欧克,按照博主说的Nginx没有配置这两个东西而导致的,我有点不确定是不是,有点犹犹豫豫的找到后端说帮我在Nginx配置文件的http加下这两个配置,然后样式就好了,完美!后端由于疏忽,没加上就把问题推给我,结果被我扳回一城。
include mime.types;
default_type application/octet-stream;
- include mime.types;
- 这个指令告诉Nginx去包含一个文件,这个文件通常包含了定义了MIME类型(Multipurpose Internet Mail Extensions)的配置。MIME类型指定了文件的内容类型,例如文本文件、图像、视频等。通过包含
mime.types
文件,Nginx可以识别不同类型的文件并正确地处理它们。 - 示例:假设
mime.types
文件中定义了.html
文件为text/html
类型,Nginx在处理请求时会根据这个定义设置正确的HTTP响应头。
- 这个指令告诉Nginx去包含一个文件,这个文件通常包含了定义了MIME类型(Multipurpose Internet Mail Extensions)的配置。MIME类型指定了文件的内容类型,例如文本文件、图像、视频等。通过包含
- default_type application/octet-stream;
- 这个指令设置了默认的MIME类型。如果Nginx无法根据文件扩展名或其他方式确定响应的MIME类型时,就会使用这个默认类型。
application/octet-stream
是一个通用的MIME类型,表示未知的二进制数据。当服务器无法识别文件类型时,会默认使用这个类型。例如,如果请求的文件没有合适的MIME类型或没有被mime.types
文件中列出,Nginx就会返回application/octet-stream
类型。- 这种设置对于确保未知文件类型的安全传输很有用,因为浏览器通常会下载这些文件而不是尝试在浏览器中打开它们。
总之,添加 include mime.types;
和 default_type application/octet-stream;
配置后,Nginx能够正确地识别和处理CSS文件的MIME类型,从而确保浏览器能够正确加载和应用CSS样式。
所以,前端仔不能只当前端仔,还是要好好学点服务端的知识,不然锅给你,你还认为这是你该背的锅。
以上是开玩笑的描述,只是为了吸引增加阅读量
来源:juejin.cn/post/7388696625689051170
一份离职感想
一、和解
人的一生就是不断地与世界和解,以及与自己和解。
今天提离职了,相比于四年前的离职,更加的从容和淡定。所谓,心无外物,云淡风轻。
图:云卷云舒,心随境转
二、当下
今天晚上和一个刚来三周的应届生跑步,聊着聊着就说到了要是回到十八岁就好了的话题。
当然我也谈了我的一些想法, 如果是以前的我也会像你这样,要是回到以前就好了。 但是,现在的我更加从容、更加淡定、更加坚定,以及对事情的理解、看法、态度是那个时候没有的,同时懂得了筛选,也懂得舍弃,对社会的理解,对世界的理解也是那个时候没有的,甚至对自己的看法也更加立体,对自己也更宽容,现在反而会更加欣赏自己。
人生不必时时往过去看,"既往不恋,当下不杂,未来不迎"
图:操场跑步,有时候也会坐在最高的台阶上,享受这自然的风。
三、接纳
我们还谈到了失恋的话题,我说很多人是没有办法放下的,也没办法抹去,不管是事还是人都一样。
人的一生就像是石块不断垒起来的玛尼堆,有一些人会在特定时间段参与进来,帮你加一块,可能它的颜色、大小、形状、好看程度都不一样,但你不得不承认,那个时间点,那个高度就只有那么一块,无可替代。
与其不见,不如接纳!
图:某次户外徒步
四、选择
大学时的第一部课外读物 ,路遥的《人生》,开篇来自柳青的一段话,至今影响着我。
刚好昨天生日,有感而发,也时刻警醒着自己。
图:朋友圈,31岁的记录
五、感受
人这一生就是不断地给自己的感受积累素材
不知什么时候,我开始静下来,用心去感受这个世界。
在吃午餐的时候,变得专心,放下了手机,端起了饭碗,开始用舌头去感受食物原本的味道,就像《小森林》中的女主一样。静下来,都是收获。
图:午餐,三菜一汤
用心去感受,用心去体会,从容和淡然。享受生活,感恩生活。
你所看到的,就是你内心的一种投射,你看到了美好,那么你的内心就是美好的。你看到了恶,那么你的内心就是恶。 内心这块三分宝地,就不要装上一些乱七八糟的东西了。
六、经历
生命的厚度在于去经历一些不一样的事情。
人需要不断去经历一些不一样的事情,而不是将365天过成1天。
“人不是活一辈子,不是活几年几月几天,而是活那么几个瞬间。”——鲍利斯·帕斯捷尔纳克。
希望多一些这样怦然心动、与众不同的时刻, 看到黄山的日出、云海,觉得这个世界真的很酷,继续热爱。
图:黄山风景图
不要等到八十岁了才去做二十岁想做的事情。
七、自己
青年人要有青年人的样子,朝气、热血。
如罗曼罗兰说过:
“世界上只有一种真正的英雄主义,那就是在认清生活的真相后依然热爱生活”。
人不要给自己设限,勇敢地做自己,不要被这个世界迷失了自我。
在这个满是焦虑、满是浮躁的社会,更要做到正心正念。
就像我在 500px 网站上说过的一句话:“这个世界没有标准答案,活出自己,不受羁绊”。
来源:juejin.cn/post/7386497602193981481
丈母娘,你这是来真的啊?
今年春节在家里呆的时间比往年都要久一些,就算离家不远也会这样感觉,随着年龄的增加,越发思念故土以及父母。
以往回家都比较晚,待着待着就想着回工作的城市。因为回家总会伴随着和二老的争论,而且我那时也很戾气,合不到一块就气,或者进房间睡觉。
要说啥事能值得争论,我的确忘了,大概是思想观念以及家里生意的问题产生的。以至于身处异乡也要离开家里不愿意喋喋不休。
随着时间流逝,心态已经发生变化。我内心深处逐渐向着故土,和家里二老的关系也潜移默化,彼此心照不宣的拉近了很多。
二老也不过多问责一些,也不再以长者的心态和我对质。
事实也觉得普通家的孩子成年后就该放权干他自己的事,父母少些干预,有些做法和观念已经帮不上忙。父母与孩子之间往往都会产生很大分歧,尤其是思想观念的矛盾。
每一代都有可能推翻上一代的思想意识。父母与子女和谐,一般父母的思想观念足够前卫能够传承给子女,或者子女的思想观念没有很大差异,认可父母的思想体系。
我们会一起坐下来交谈家里的事情,想好对策处理外事,规划老家祠堂以及屋子的安排;参与布置家里,代替老爹出席各项习俗礼节活动。
从读大学开始到现在就没再给它们制造任何的麻烦和搞砸事情。只见他坐在店里抽着烟,沉默着望向我,迟迟不再发话,只在最后说了一句婚姻的问题。
这个话题别说他了,老家众多的亲戚都会抛出这个尖锐的问题,我说我回家只想休息,逢年过节和老朋友叙旧,去亲戚家串串门,一起乐呵乐呵聊聊家常,聊聊八卦。
95‘ 后教师彭老师
小学同学彭老师,现在是一名县城重点中学的高中老师。她是一名 90 后老师,我在她身上看到了很多老师的缩影。她毕业后第一份工作就是任高一班主任兼任课老师。
▲图/ 学生:???
彭老师很是焦虑很迷茫,害怕自己不能胜任;害怕自己教不好;害怕自己耽误学生的前途。
每一个担心都很沉重,责任肩负在身上,没日没夜的备课,改作业,演练怎么让学生听懂。
▲图/ 在最无能为力的学科遇到了最不想辜负的老师
小城市义务教育向来重视成绩不太注重心理建设,导致问题学生很多。彭老师经常 solo 学生和家长,试图说服他们把重心放在当前阶段要做的事。
尽管生病,喉咙发炎也坚持做着这些事情。
▲图/ 除夕夜,彭老师回学校,顺道提零食带红包给她
▲图/ 她赠了一罐饮料,握着只觉比当时天气还冷
▲图/ 彭老师收到开工红包请吃饭,还给了红包,呜呜呜感动
有时煎熬的对着同事和朋友说,看着自己的希望学生开始堕落真的很难受,说也说不通;面对自己的学生读着读着因家里或者各种的问题而辍学时更无语。
▲图/ 00 后学生在 95 后老师教案涂鸦
忍不住会直接联系家长 solo,开导家长有时候如同开山凿石一样,只有坚持不懈才使得那位学生继续读下去,尽管读大专,也改变了不少走向。
有一天她苦笑的跟我们说,她自己都没怎么谈过恋爱却还要开导失恋的学生,甚至被失恋学生反问:“老师,你谈过恋爱吗?”
???
彭老师所带的班级在 23 年第一批结业。上本率超额完成上面给的任务,这一路上面临学生堕落、早恋、辍学、逃课、网瘾...
彭老师有时候很想放弃摆烂,像那些老道的老师一样,风清云淡,看开一点,上课就上课,喝茶就喝茶,晒太阳就晒太阳。
后来她骑着小电瓶,向我重申了她那坚定的信念,绝不可能摆烂,我要对我的学生负责,要为他们的前途着想,就这么决定了。
我看着她感觉头上出现了一顶为人师表的光环。
实话说,老师的行为无论是怎样的,都会被学生刻在记忆深处,尽管有时不会联系,也会在某一刻回想起,念其良莠。
▲图/ 念大学的学生探望彭老师
她性格一直都很逗比很乐观,走路也喜欢蹦蹦跳跳,还被她的老师和学生嘲笑她走路蹦蹦跳跳,和她玩王者鲁班的走姿如出一辙。
在她学生毕业之后,彭老师回归万年鲁班,但技术依然停留在四五年前大学生时期,经常遭截杀一路“啊”,开疾跑徒走回水晶,也经常被我们和她的学生给护着。
现在见她时还是很逗比,嘴硬心软。脸上刻印出一副班主任的形象,坚定严肃而又亲和。
她说她带完一届之后不当班主任了,太辛苦太累了。后面她又被安排带复读班班主任。
阿姨,你来真的啊?
老家的天气很不错,逢年过节我们经常互串亲戚的门,晒晒太阳,欺负欺负小朋友,欺负过头了说送给他们一份《三年高考五年模拟卷》礼物,他们哭的更厉害。
▲图/帮人带孩子真好玩
春节假期充电的时间正在倒计时,最后赶着串堂弟的门,也就是大叔家,那时他们家里很忙,家族里的会做饭打点的都来帮忙了。
原来是堂弟未来丈母娘查家环节,家里上上下下忙活。
我等闲杂人在巷子里晒着太阳,准备迎接他们。堂弟我和同岁,月份比我小,在温州工作,这几年他老爹操够了心,费了不少钱。龙年又长了一岁,他父母更是着急。
▲图/ 回家路途中天色很震撼,大家都不想说话
闲聊之际,只见一列车齐刷刷地开到门口,不知道的还以为是迎亲来了。
二叔见状连忙赶上去打招呼并指挥停车泊位。只见那丈母娘下车后整理着装,望着周边的房屋装饰,一脸严肃对着二叔说位置有些偏远,绕来绕去的,二叔连忙解释可能走的道不一样,走国道会很顺。
亲友团齐刷刷招呼张罗着进去喝茶,握手,递烟,倒茶。摆了三桌才能坐下,我们自己人在旁边站着观望或帮忙。两方互相寒暄之后,不到十分钟对方便开始切入主题,商量儿女婚姻问题。
丈母娘吐露堂弟家位置偏僻,路道不好走,绕了很多弯才抵达,彩礼需要增加 3w 到 5w,作为她女儿的嫁衣钱。
阿姨,你这是来真的啊?
大叔一边忙着圆场有不同的道路可以走,镇与镇之间来往有很多路可走也方便,一边递烟倒茶使其思量再三。
丈母娘依然坚定不移,重申了一遍,对方亲戚应声附和。
大叔脸色像是喝酒上了头一样,随后陷入沉思,见其态度坚硬,且事已至此,作出退步可以增加 1w - 2w,给到对方女儿身上。
前些天,大叔带着堂弟相亲到女方家,据说对方开价 35.8w 彩礼,回礼是购买房之后支援 10w+,不知其是否商量后的价格。
只见那天大叔来我家喝茶水时带着儿子和未来儿媳去了县城买了“五金”, 4w 左右。
见此事既成,并不买二叔的帐,坚持需要增加 3w,并声称给女儿做嫁衣。
大叔又陷入了思考,心里计算着账面,上个月女儿刚嫁出去,彩礼还没捂热,就要付之东流,是为不甘且又无奈。
场面陷入了安静,对方只管握着茶杯吃着果子等待结果,势必做好了撤离的打算。
姑舅们遂即递烟倒茶聊家常。
堂弟陪同坐着喝茶望着对象低下了头,儿媳妇安静得陪在身边,挽着堂弟的手。
掂量之后大叔同意了对方的要求,双方态度方能缓和很多,继续喝茶,商量事宜,聊家常,聊孩子幸福。
饭后,互相道了别,每人随了红包礼。只觉得对方结婚习俗没有讲明白有点遗憾,但愿不会阻碍他两组建一个幸福的小家庭。
我和表弟坐在沙滩上,对着河扔石子打水漂,谁都不想再提,心里比谁都清楚。却和群里的伙伴嘲笑着自己家的那位是否也要几十个 w。
来源:juejin.cn/post/7336822951273824282
吾辈楷模!国人开源的Redis客户端被Redis官方收购了!
不久前开源圈子里的一则消息在网上引起了一阵关注和讨论。
一个由国人开发者所打造的开源项目被 Redis 公司官方给收购了,作者自己也发了动态,表示感谢项目9年以来的陪伴,同时也希望她未来一切都好。
这个开源项目的名字叫做:ioredis,相信不少小伙伴也用过。
目前在GitHub上我们可以看到,ioredis项目的开源地址已经被迁移至 Redis 官方旗下了。
iosredis是国人开发者所打造的一个Redis客户端,基于TypeScript所编写,以健壮性、高性能以及功能强大为特色,并且被很多大公司所使用。
截止到目前,该项目在GitHub上已累计获得超过 13000 个 Star标星和 1000+ Fork。
作者自己曾表示,自己创建这个开源项目的初衷也很简单,那就是当年在这方面并没有找到一个令自己满意的开源库,于是决定自己动手来打造一个,于是就利用闲暇时间,自己从零开发并开源了 ioredis 。
直到2022 年 8 月 30 日,历时整整7年,ioredis 成为了 Node.js 最流行的 Redis 客户端。
而直到如今,这个项目从个人的 side project 到被开源公司官方收购,作者9 年的坚持属实令人佩服,吾辈楷模啊!
而拜访了这位开发者的GitHub后我们会发现,作者非常热衷于创造工具,除了刚被收购的名作ioredis之外,主页还有非常多的开源项目,并且关注量都不低。
而且从作者发的一些动态来看,这也是一个热爱生活的有趣灵魂。
有一说一,个人开源作者真的挺不容易的,像上面这样的个人开源项目被官方收购的毕竟是个例,其实好多个人的开源项目到后期由于各种主客观原因,渐渐都停止更新和维护了。
大家都知道,伴随着这两年互联网行业的寒意,软件产业里的不少环节也受到了波动。行业不景气,连开源项目的主动维护也变得越来越少了。
毕竟连企业也要降本增效,而开源往往并不能带来快速直接的实际效益。付出了如果没有回报,便会很难坚持下去。
而对于一名学习者而言,参与开源项目的意义是不言而喻的,之前咱们这里也曾多次提及。
参与开源项目除了可以提升自身技术能力,收获项目开发经验之外,还可以让自己保持与开源社区其他优秀开发者之间的联系与沟通,并建立自己的技术影响力,另外参与优秀开源项目的经历也会成为自己求职简历上的一大亮点。
所以如果精力允许,利用业余时间来参与或维护一些开源项目,这对技术开发者来说,也是一段难得的经历!
来源:juejin.cn/post/7345746216150876198
我在国企当合同工的那段日子
@author: 郭瑞峰
@createTime: 2024/06/03
@updateTime: 2024/06/20
心血来潮
25号考完了,非常不理想,果然700页的东西不是一个月能搞完的。不对,我今儿写日志是为了纪念一下我的第一家公司,咋扯到别的了......言归正传,我在第一家公司待了仨年,可能是年纪到了(26岁咋还不退休啊),也可能是留了点感情在,离开前有些百感交集,思来想去还是写一个懒人日志吧,纪念一下我打工的三年光阴吧。
(:з」∠)
初说公司
先说一下俺的第一家公司,咱从学校出来就来这儿报道了,公司是国企控股,领导层全是国企员工,其他进公司的员工就是合同工,或者说是国企合同工,能吃公司东西,不是人力外包。
(:з」∠)
成都这边的开发都是围绕着云服务的,包括云操作系统、云桌面系统、云运维系统以及多云系统(我个人喜欢把他称为多个云集成系统),当然全是定制化项目。对,忘说了,公司主要业务是轨道交通行业,做云相关的产品是将轨道行业的运维放在云上面,算是相应国家的两化融合(信息化和工业化)。
对了,得说一下公司待遇,公司给的工资都在平均水平以下,尤其是对应届生而言,社保基数是工资八折(试用期)交的,公积金是12%,没有餐补但自带食堂以及饭卡补助,有些节假日有礼品,至少基础福利还好。
项目与业务
我所在的项目组就是多云系统,也算是我认为公司能拿得出手的项目。虽然是集成项目,但它只能集成。好像说了跟没说一样,那说具体点吧,比如说业主那边需要云,但怕私有云厂商垄断坐地起价,所以说一般配额划分为“7/3”、“4/3/3”、“6/4”,这样就有两套云系统,为了用起来顺心就需要一个集成系统,所以说我这个项目组的业务来源就是这样,至于你说的我们集成系统会不会垄断坐地起价,拜托,我们系统只会集成,没有底层设备控制权,坐地起价就直接禁用就行了,就不用这个系统呗,反正资源在另外的云操作系统中。
好了,话题回来,说说项目组开发相关的吧,项目开发受阻有三:与三方厂商沟通、项目代码老旧、随时随地变更的需求。
先说第一点吧,集成系统最大的麻烦就是跟三方厂商沟通,当然测试环境、测试数据获取这类的细节也算三方厂商沟通。因为地铁行业算是智能中国建设的一部分,所以说不光是我,连三方厂商的软件都必须是定制的。开发时候就要等着厂商环境稳定了,有数据了再联调,联调有bug了,再走一轮上面的流程,极大地增加了沟通成本以及开发成本。
在沟通,再沟通
其二就是和很多工业软件公司一样,软件项目时间跨度很大,里面东西不知道转手了好多次,缝缝补补式的开发,开发要考虑很多兼容性问题以及自己想办法写补丁。比如说node@6.x.x
不支持Object.entries
,你就要手动在webpack.base.conf.js
写的兼容,问我为啥不配置babel
呢,上次改babel
配置都是2016年的事儿了。代码要写兼容,久而久之就会忘记什么事封装、抽象,全部遗失在兼容的漩涡中。
我就改了一点点怎么崩了
其三就是随时随地变更的需求,这里我叠个甲,这个我不是甩锅给产品,虽然是产品改的需求,但产品不是想改就改,一定是业主/客户/上级/领导指示要改的。有需求变动谁都不会安逸,谁都烦,但请把炮火对准,不要误伤友军。频繁调整的需求会不断地消磨激情和热情,模糊项目方向,当然还有临时变卦导致的加班。
一直在变的需求
心态变化
三年工作时间虽然很短,但足以改变心态。原来有些迷茫到彻底迷茫;原来想要搞出一番事业到慢慢得过且过;原来想努力改变世界走到只想躺平加速世界毁灭。
公司的缝缝补补,工作的缝缝补补,项目的缝缝补补,这样的缝缝补补渐渐地缝补在人身伤,人心里。原来就算只有940的显卡也要努力熬夜玩游戏,现在用上3060ti后却只想打开直播看看,就只看看,重新上手玩太耗精力了。至于脱单嘛,自己都这么累了,为啥带着另一个一起累呢?
尾声
本来6月3号说写完的,忙着离职交接以及新公司入职,再加上拖延症又犯了,所以说一直到20号才写完,不过至少咱写完了,能发。
这篇算是自己里程记录,同时也是发牢骚,大家就当笑话看看吧。
来源:juejin.cn/post/7382121357608321059
.zip 结尾的域名很危险吗?有多危险?
Google 于 2023 年 5 月 10 日全面开放了以 .zip
结尾的域名,这一举动引起了安全研究人员和社区的警惕,他们担心该通用顶级域名(gTLD,Generic top-level domains)会被用于创建足以迷惑计算机高手的恶意 URL。
2023 年 5 月 3 日,Google 宣布了包括 .zip
和 .mov
在内的 8 个全新的通用顶级域名:
- .dad
- .phd
- .prof
- .esq
- .foo
- .zip
- .mov
- .nexus
并于 5 月 10 日通过 Google Domains 向公众开放注册。
Google Domains 是 Google 提供的一项域名注册和管理服务,支持用户搜索和注册域名
小心!含有 .zip 的恶意 URL
安全研究员 Bobby Rauch 指出(The Dangers of Google’s .zip TLD),要警惕含有 .zip
、Unicode 字符(特别是 U+2044、U+2215 等)以及 @
符号的恶意 URL。这类恶意 URL 迷惑性极强,甚至能欺骗十分有经验的用户。
若点击 https://google.com@bing.com
这个 URL,实际访问的是 https://bing.com
。这是因为根据 RFC 3986 Uniform Resource Identifier (URI): Generic Syntax 的规定,@
符号之前的 google.com
应识别为用户信息,其后的 bing.com
才是主机名(域名)。我们可以借助常用的编程语言来确认这一点,如利用 PHP 的 parse_url()
函数:
<?php
var_dump(parse_url("https://google.com@bing.com"));
array(3) {
["scheme"]=>
string(5) "https"
["host"]=>
string(8) "bing.com"
["user"]=>
string(10) "google.com"
}
然而,若 @
之前有正斜杠 /
,如 https://google.com/search@bing.com
,则浏览器会将 /search@bing.com
部分识别为路径,最终访问的是 https://google.com/
下的文件 search@bing.com
。由于没有这个文件,结果自然是 404。
<?php
var_dump(parse_url("https://google.com/search@bing.com"));
array(3) {
["scheme"]=>
string(5) "https"
["host"]=>
string(10) "google.com"
["path"]=>
string(16) "/search@bing.com"
}
Bobby Rauch 就是利用了上述规则,创建了一个恶意 URL,
https://github.com∕kubernetes∕kubernetes∕archive∕refs∕tags∕@v1271.zip
乍看之下,这个 URL 似乎是用于从 GitHub 上下载 v1271
这个特定版本的 Kubernetes 的链接。但实际上 parse_url()
函数的解析结果显示,真正要访问的域名却是 v1271.zip
而不是 github.com
:
<?php
var_dump(parse_url("https://github.com∕kubernetes∕kubernetes∕archive∕refs∕tags∕@v1271.zip"));
array(3) {
["scheme"]=>
string(5) "https"
["host"]=>
string(9) "v1271.zip"
["user"]=>
string(63) "github.com?__kubernetes?__kubernetes?__archive?__refs?__tags?__"
}
若你不小心点击了这类域名,那么恭喜你,很可能喜提一个 evil.exe
(请注意动画演示中的左下角)。
仅凭肉眼可能难以分辨以下两个 URL 的区别吧:
https://github.com∕kubernetes∕kubernetes∕archive∕refs∕tags∕
https://github.com/kubernetes/kubernetes/archive/refs/tags/
但若调整一下字体,则可以发现端倪,
恶意 URL 中的正斜杠 /
根本不是真正的 /
(U+002F),
而是下面这个看起来很像 /
的 Unicode 字符:
由于恶意 URL 中并没有使用真正的 /
,因此根据 RFC 3986 的规定,@
之前的部分github.com∕kubernetes∕kubernetes∕archive∕refs∕tags∕
尽管看似域名与路径,但实际上却是用户信息(真够长的)。
在刚刚的动画演示中,Bobby Rauch 其实还使用了另一个迷惑人的小伎俩——在电子邮件客户端上,将 @
的字号大小更改为 1,让这个特殊字符几乎看不到,从而更隐秘地伪装了恶意 URL。
对于由以 .zip
结尾的域名带来的安全隐患,Bobby Rauch 给出的建议是,在单击 URL 之前,先将鼠标悬停在该 URL 上并检查浏览器底部显示真正要访问的 URL。
来源:juejin.cn/post/7384244866875146290
希望你多出去看看,别活在短视频和文字里!
感谢你阅读本文!
这段时间在校友群里看到一些“混得比较好的”同学发言,类似于“5w的月薪很高吗?”,“我身边年薪六七十w的人不少”之类的话,加上偶尔看到一些“年薪百万很简单”的标题党文章或者视频,其实对于我来说,我根本懒得去考证这些是真是假!
但是我觉得有必要去聊一聊!
一、知识的贫乏
首先在说这个问题之前,我想引用罗翔老师的一句话。
一个知识越贫乏的人,就越有莫名的优越感!
一年多以前,我回老家,和以前的高中女同学出来聊天,彼此聊了聊自己现在的工作,然后他问我,“你现在一个月能赚三四万吧!”,我当时惊呆了,我回她:“瞧你说的,捡黄树叶也要赶上秋天呢”,我反问她你现在多少呢,她说两千八,我继续问,“你觉得工资多少才算高?”,她说最起码5万以上吧!我苦笑答:“我的妈呀,怎么都这么厉害呀!”。
事实是怎样的呢?
我们先不把事情说得太远,“脉脉上人均年薪百万”,“抖音上人手一台劳斯莱斯”这些不在叙述范围内,感情咱也不会那么不要脸去吹!
二、大众才是真相
像我们这种普通二本学校的学生现状应该最能接近真相了,往上不谈双一流,往下不谈专科,据我所知,我校2021毕业的学生,如果继续做软件工程的话,现在一个月能拿两万以上的人没几个,还得是一线城市,在一线城市的大多都是一万多,所以一万多就是一个中位数。
不过要注意,软件工程专业毕业后从事本专业的人是很少的,就拿我们班来说,班上50人,但是从事软件的不超过20个,20个还是比较理想的。
那么就有一部分从事其它职业,一部分待业,一部分考公考编。
软件行业在整个市场来说工资高一点,就业相对来说简单一点,虽然近几年来行情越来越差,但是相比于其他行业来说,还是稍微好一点!
从事其他行业的人来说,如果家里有点关系的人,条件好一点的人,可能去到一个单位里面暂时上班,条件不好的,那就出来随便找一个班上,对于销售型的,在广州深圳,大多都是六七千,小城市的话,五千基本上已经很高了。对于待业的,那基本上没收入,考公考编的一般都回到了小县城,随便找个单位临时上班,一个月也就两千来块!
我们就不去分析双一流,专科,中职这些了,所以整体算下来,我们现在的年轻人的收入是很低的。
三、时代特征
努力在这个社会貌似已经不是一个正能量的词了,仿佛已经是一个调侃的词了,就像现在大多女孩子,他现在不会选择一个很努力的男孩子作为伴侣,因为努力后得到回报是一个概率事件,大多会选择有“存货”的人!当然,并不是人人都这样!
社会的发展就是这样,就像森林里面的狼越来越多,那么捕获到猎物的概率就越来越小,这和努力没多大的关系,这是时代特征!
八九十年代别说考上大学,考上中专谋个职位都不难,而现在一砖头下去都能打中几个研究生已经不是什么稀奇事了。
还有现在的经济形势如此严峻,企业和单位的寒冬一直在降临,无数的人蜂拥而至,导致形势更加紧张,本来在夹缝中已经难以生存了,现在变成了针眼!
所以前段时间网红带货主播李佳琦在网上说:“找找自己的原因,工资涨了没涨,有没有认真工作”,是因为的认知出现了谬误,所以才说出了这种言论,而他的成功完全靠努力吗?你怎么看!
四、这和你有鸡毛关系!
浮躁来自于你的认知水平,在这个信息爆炸的时代,如果不能分辨真假是非,那么就很容易陷入浮躁的状态!
网络上和现实中总是充斥着一股“赚钱很容易”的妖风,他们去编造一些故事,制造一些假象来迷惑人的双眼,如果你的甄别能力不够,那么你就会觉得为啥别人那么厉害,自己为啥混成这样,从而陷入浮躁和迷茫之中,当你进入这个状态后,等待你的要么是镰刀,要么是内耗!
做人过程中的一大蠢事就是自己啥也不是的时候,总是去炫耀自己拥有的那些八竿子打不着的人脉和资源,被那些不知真假的事物去影响,去自我否定,当一个人不能独立去思考问题,不站在现实角度去看待问题的时候,那么是永远不可能获得成长的。
五、最后
现实中,很多人都是很窘迫的,赚到钱的人永远在少数,这是时代特征和个人运气所决定的,努力只占了很小一部分,所以别被互联网上的一些妖风所影响!
这个时代我们虽然能决定的东西很少,事物都充满不确定性,但是依然要如罗曼罗兰说的那样“世界上只有一种英雄主义,看清生活的真相依然热爱生活”,正因为充满不确定性,所以才有“赌”的意义!
来源:juejin.cn/post/7289692200161329210
axios中的那些天才代码!看完我实力大涨!
axios的两种调用方式
经常调接口的同学一定非常熟悉aixos下面的两种使用方式:
- axios(config)
// 配置式请求
axios({
method: 'post',
url: '/user/12345',
});
- axios.post(url, config)
// 简洁的写法
axios.post('/user/12345')
不知道各位大佬有没有思考过这样的问题:
axios到底是个什么东西?我们为什么可以使用这两种方式请求接口呢?axios是怎么设计的?
axios原理简析
为了搞明白上面的问题,我们先按照传统思路仿照axios源码实现一个简单的axios。
手写一个简单的axios
创建一个构造函数
function Axios(config){
this.defaults = config; // 配置对象
this.interceptors = { // 拦截器对象
request:{},
response:{}
}
}
上面的代码中,我们实现了一个基本的Axios类,但它还不具备任何功能。我们现在给它添加功能。
原型上添加方法
Axios.prototype.request = function(config){
console.log('发送Ajax 请求 type:' +config.method)
}
Axios.prototype.get = function(){
return this.request({method:'GET'})
}
Axios.prototype.post = function(){
return this.request({method: 'POST'})
}
上面的代码中,我们在request属性上创建了一个通用的接口请求方法,get和post实际都调用了request,但内部传递了不同的参数,这和axios(config)、axios.post()有异曲同工之妙。
参考aixos的用法, 现在,我们需要创建实例对象
let aixos = new Axios(config)
创建后的axios包含defaults
和interceptors
属性,其对象原型__proto__
上(指向Axios的prototype)包含request、get及post方法,因此,我们现在可以使用aixos.post()
的方法模拟调用接口了。
但注意,此时aixos只是一个实例对象,不是一个函数!我们似乎也没办法做到改造代码使用aixos(config)
的形式调用接口!
aixos是如何实现的呢?
aixos中的天才想法
为了即能使用axios(config)又能使用axios.get(),axios的核心伪逻辑如下
function Axios(config){
this.defaults = config; // 配置对象
this.interceptors = { // 拦截器对象
request:{},
response:{}
}
}
Axios.prototype.request = function(config){
console.log('发送Ajax 请求 type:' +config.method)
}
Axios.prototype.get = function(){
return this.request({method:'GET'})
}
Axios.prototype.post = function(){
return this.request({method: 'POST'})
}
function createInstance(config) {
//注意instance是函数
const instance = Axios.prototype.request;
instance.get = Axios.prototype.get
instance.post = Axios.prototype.post
return instance;
}
let axios = createInstance();
通过上述的伪代码,我们可以知道axios是createInstance()函数的返回值instance。
- instance 是一个函数,因此,axios也是一个函数,可以使用axios(config);
- instance也是一个对象(js万物皆对象),其原型上有get方法和post方法,因此,我们可以使用axios.post()。
我们看看aixos的源码
aixos的源码实现
function createInstance(config) {
//实例化一个对象
var context = new Axios(config); //但是不能直接当函数使用
var instance = Axios.prototype.request.bind(context);
//instance 是一个函数,并且可以 instance({}),
//将Axios.prototype 对象中的方法添加到instance函数中,让instance拥有get、post、request等方法属性
Object.keys(Axios.prototype).forEach(key => {
// console.log(key); //修改this指向context
instance[key] = Axios.prototype[key].bind(context);
})
//总结一下,到此instance自身即相当于Axios原型的request方法,
//然后又给instance的属性添加了上了Axios原型的request、get、post方法属性
//然后调用instance自身或instance的方法属性时,修改了this指向context这个Axios实例对象
//为instance函数对象添加属性 default 与 intercetors
Object.keys(context).forEach(key => {
instance[key] = context[key];
})
return instance;
}
可以说,上面的代码真的写的精妙绝伦啊!
注意这里,为什么要修改this的指向
var instance = Axios.prototype.request.bind(context);
首先,requset 是Axios原型对象上的方法,其方法内部的this指向的是其实例化对象context!
Axios.prototype.request = function request(config) {
/*eslint no-param-reassign:0*/
// Allow for axios('example/url'[, config]) a la fetch API
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
config = mergeConfig(this.defaults, config);
// Set config.method
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}
// Hook up interceptors middleware
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
因此,如果我们直接使用Axios.prototype.request()
就会出现问题,因为这事reques方法内部的this会指向错误,导致函数不能运行,因此,我们必须将this重新指向其实例化对象。
来源:juejin.cn/post/7387029190620184611
后端同事下班早,前端排序我来搞
写掘金博客有一小段时间了,我发现了一个秘密。文章阅读量小了吧,很心烦,阅读量大了吧,更心烦。很多特别懂特别会的大哥们就会来“指点”我。感谢这些大哥们的“指点”,使我进步。后来我和群里的小伙伴们讨论了一下,为什么掘金文章会有那么多大哥们来“开心指点”呢?大概是这么几种情况:
1 他们爱学习,爱分享,但是即将要被裁员了,所以心情不是太好,怎么办呢?去掘金上指点他们去,让他们知道自己很会很懂;
2 他们在团队中就是翘楚,指点完了团队的人,然后不放心,怕世界不足够完美,反正自己也要被裁员了,有时间,然后补偿没拿够,心情不太好,去给他们指点指点去;
3 他们一直觉得自己不太懂,但是看见文章又想指点指点,所以呢,在家找了3个月工作这段时间,一边学习,然后看大家的文章,学习过程中呢,看哪里觉得不太合适,指点指点,希望趁着阅读量高的文章,好希望有人能发现他们,直接邀约他们入职;
这种人呢,我们总结了一下,他们可以叫“黑哥会”,意思就是黑哥们,比较会,啥都会那种,嗯,希望黑哥会的成员们早日找到心仪的工作,在家闲着不好的。
好啦,在文章正式开始之前呢,告诉大家个好消息,本文点赞,友善评论,友善建议的大哥大姐们,2024年的后半年,一定能够心想事成,工作顺利,家庭和睦,一直到永久。
321... 文本正式开始。
1 未排序的数据
今天早上来了公司,我赶紧喊老张,问:新来的前端妹子这么快就被你搞定啦?听说昨晚你俩10点一起出的公司?是不是,快说。 老张,猛地抬头,问:你咋知道的?我保密工作做这么好。 我说:门口的李大爷说的。你快说说什么情况啊。
老张说:别瞎说,昨天后端下班早,把接口就给妹子了,妹子本来以为调一调接口,传几个参数完事,结果发现后端给的数据没有排序,但看了产品文档,发现,又要根据学生姓名按字母排序,又要根据分数排序,又要根据年龄排序,又要根据日期排序,直接把妹子气的快哭了,所以我就帮他弄了弄。然后就弄到10点了呗,一起出的公司而已,别瞎想。
但是妹子为了感谢我,告诉了我一个好消息,过会儿和你说。我说:你快点说。老张说:你先听我把功能说完,我再告诉你。
你看,后端就一个接口,给的数据大概是这样子:
const users = [
{"name": "小张伟", "age": 19, "score": 55, "dateTime": '2021-03-03 15:33:10'},
{"name": "张三", "age": 22, "score": 65, "dateTime": '2023-03-03 10:10:10'},
{"name": "李四", "age": 30, "score": 87, "dateTime": '2024-04-03 10:10:10'},
{"name": "阿斌", "age": 50, "score": 90, "dateTime": '2021-03-03 10:10:10'},
{"name": "曹小操", "age": 1300, "score": 23, "dateTime": '1021-05-08 10:10:10'},
{"name": "小张灰", "age": 31, "score": 15, "dateTime": '1994-03-04 08:33:10'},
];
2 根据属性排序
这是一个杂乱的json型数组,但是要根据属性进行排序。我们目前做了3种类型的实现
2.1 引入工具库
这里说一个高效便捷功能丰富的前端JS库,首先引入js-tool-big-box工具库。
执行安装命令:
npm install js-tool-big-box
引入dataBox对象,排序的这些公共方法被放到了这个对象下面:
import { dataBox } from 'js-tool-big-box';
2.2 数值型排序
数值型排序呢,就是,你看,age 和 score 都是数值型的,我们把这些归结为一类进行排序。
2.2.1 根据age从小到大的排序
代码如下:
const ageResult1 = dataBox.sortByNumber(users, 'age');
console.log('age是数值型,从小到大,排序后的值为:', ageResult1);
结果如下:
2.2.2 根据age从大到小的排序
代码如下:
const ageResult2 = dataBox.sortByNumber(users, 'age', 1);
console.log('age是数值型,从大到小,排序后的值为:', ageResult2);
结果如下:
2.2.3 根据score从低到高的排序
代码如下:
const ageResult3 = dataBox.sortByNumber(users, 'score');
console.log('score是数值型,从低到高,排序后的值为:', ageResult3);
结果如下:
2.2.4 根据score从高到低的排序
代码如下:
const ageResult4 = dataBox.sortByNumber(users, 'score', 1);
console.log('score是数值型,从大到小,排序后的值为:', ageResult4);
结果如下:
2.3 中文按字母排序
比如我们的姓名,很多时候需要按字母从A到Z来展示,这个时候就可以用下面这个方法来快速实现:
2.3.1 按字母从A到Z排序
代码如下:
const nameResult1 = dataBox.sortByletter(users, 'name');
console.log('比如name,我们按照字母顺序排序后为:', nameResult1);
结果如下:
2.3.2 按字母从Z到A排序
代码如下:
const nameResult2 = dataBox.sortByletter(users, 'name', 1);
console.log('比如name,我们按照字母顺序倒序排序后为:', nameResult2);
结果如下:
2.3.3 注意
需要注意的是,我们这里只是传入了name的属性,如果这个json中有其他中文属性,也是可以使用这个方法进行按字母排序的,很灵活。
2.4 按日期时间排序
比如我们例子中的时间,按时间排序也是非常实用且常见的需求,
2.4.1 按时间从早到晚排序
代码如下:
const timeResult1 = dataBox.sortByTime(users, 'dateTime');
console.log('以时间从早到晚排序后的值为:', timeResult1);
结果如下:
2.4.2 按时间从晚到早排序
代码如下:
const timeResult2 = dataBox.sortByTime(users, 'dateTime', 1);
console.log('以时间从晚到早排序后的值为:', timeResult2);
结果如下:
2.4.3 注意
需要注意的是,我们例子中只是传入了dateTime属性,如果json对象中有其他的是时间格式的属性值,也可以把属性传入,就可以进行字段的属性排序啦,很便捷。
3 最后
把效果展示完了,我赶紧催促老张说:你刚才跟我说的好消息呢?老张悄声说:妹子和门口老大爷,还有咱们公司老板都姓李,你品去吧。妹子跟我说了,她跟她爸爸说:这个季度的优秀就是我。我一听也跟着高兴起来,希望看到这篇文章的大哥大姐们,也都能像老张一样,升职加薪,变得越来越优秀。
最后告诉你个消息:
js-tool-big-box的npm地址是(js-tool-big-box 的npm地址)
js-tool-big-box的git仓库地址(js-tool-big-box的代码仓库地址)
来源:juejin.cn/post/7384419675073789991
因为打包太慢,我没吃上午饭
事情的起因是这样的:
鄙人呢,在公司负责一个小小的后台管理系统。
这天中午临近饭点的时候,测试小哥坐我工位旁边现场监督我改一个Bug。
Bug本身它倒是不复杂,甚至时隔几天之后我已不记清具体内容是什么。当时只见我是三下C两下V、提交、合并测试分支、登录Jekins点击deploy,一顿操作如行云流水一般丝滑。
说时迟,那时快。公司给 极品廉价劳动力们 我们安排的午饭送到了,一人一份。但你也知道,这两年经济下行,无人幸免;有的人食不果腹,有的人衣不蔽体, 保不齐谁饿的紧,拿了两份饭,那可就意味着有一个人要饿肚子,我可万万不希望那个倒霉蛋是我。
看着Jekins的deploy进度条,我对测试小哥说:
“你先回去,等会发完了你再看一下,应该没有问题,我先去干饭了”
说罢,我转头看向测试小哥:他面无表情盯着屏幕,似乎是默许了我的提议。于是我便准备起身——
只见他头也不回一手把我按住,缓缓吐出四个字:
“看完再吃”
...
...
...
大约半个小时后,KFC。
我:“我都告诉你了,不会有问题,先干饭,你非不听”
测试小哥:“......”
我:“这下好了吧,上个月的工资还没发,现在又来付费上班”
测试小哥:“我就问你,星期四的这个辣翅,它香不香”
我:“香”
罪魁祸首
所以,项目到底deploy了多久?
Jekins的记录中可以看到,编译打包环节耗时基本在五六分钟,最近几次成功构建的整体耗时平均在5分50秒。
这个项目本身呢,说大不大,说小也不算小。是个普通CRUD
页面居多的管理后台,没有太多其他乱七八糟的东西。如果以页面、组件数量的维度来看:
使用资源管理器在项目的/src
目录下通配*.vue
可以看到有561个文件
说实话,这样的体量打包5-6分钟,属实有点过分。
我又找来公司的一个巨石应用来对比:由于巨石应用历史比较悠久,横跨了多个技术栈(HandleBars模板引擎、使用jQuery的原生HTML、Vue2),不能只看SFC
的数量,所以这次就来比较一下,用来存放页面文件的文件夹的体积。
我的项目
巨石应用
先不算其他的资源,单就页面文件体积已经接近5倍,如果其他东西都算上,打包时间就算没有5倍,一两倍总是要有的吧?
结果呢,时间甚至更短
好好好,有活干了。为了避免下次面试官问我对webpack做过什么优化时复述那些网上千篇一律的答案,现在就来实际操作一下。
本文真实记录了一次对项目构建耗时、产物体积优化的过程。没有对知识点系统的梳理,主要突出的是思路:面对问题时的解决思路。
日志分析
曾经有位技术能力超强的架构师说过:遇到问题不要慌,先看日志。
既然这么慢的构建过程是发生在Jekins上,那就先来捋一捋Jekins的log,有没有什么值得注意的地方。
这个项目的构建脚本中,抛开(Jekins)工具的准备、从git上拉取代码以及最后的部署这些动作,只看跟前端的打包有关的部分,命令很简单,只有四行:
rm -rf node_modules
rm package-lock.json
npm i
npm run build
在日志中体现如下图:
开局就是一记暴击!
11:22:08 + npm i
11:25:59 + added 1863 packages from 1199 contributors balabala...
...
...
11:25:59 + npm run build
...
...
合着这五六分钟的打包时间,安装依赖就占了一大半,阿西巴!
在继续往下进行之前,请允许我先介绍些项目的其他背景:
deploy脚本拉取代码这一步简单来说就是:cd进项目目录 -> git pull -> 切换至要构建的分支
项目的开发人员较少,算我在内三个人
项目的依赖变动频率十分低,以月或数月为单位
背景铺垫完了,开始研究npm i
为什么这么耗时,相关的命令有三句:
rm -rf node_modules
rm package-lock.json
npm i
其中npm i
这句是必须的,没什么好说的;rm -rf node_modules
和rm package-lock.json
这两句是变量,挨个做耗时的对比测试。
首先,我在本地使用跟Jekins上相同的node版本(14.16.1),使用相同的npm源(官方源),新建一个目录clone项目代码:
- 完整执行三行命令,耗时与Jekins上相差无几
- (此时已经有了
package-lock.json
文件)执行rm -rf node_modules
+npm i
,耗时极短 - (此时已经有了
node_modules
目录)执行rm package-lock.json
+npm i
,耗时也极短
第三步其实没什么意义,npm文档中有提到,这种情况就相当于梳理了node_modules的结构并生成了package-lock.json
,并没有安装任何东西。
而步骤一和步骤二之间为何耗时相差比较大,可以参考这篇我觉得npm install流程写的比较好的文章:简单来说就是花费了大量时间去远端获取包信息。
结合项目背景一,我们的package-lock.json
会提交到仓库,每次肯定都是最新的,所以Jekins deploy脚本中rm package-lock.json
这一步属实是没有必要,本地计算完了到Jekins上又来一遍,纯纯的浪费时间。
联系运维哥把测试环境的deploy脚本修改一下,去掉了rm package-lock.json
这一句,测试下耗时:
如图中下边两次构建,【编译打包】环节时间都去到了2分30秒左右,直接缩短了一半多。部署成功后,在测试环境的页面咔咔一顿点,似乎也没有什么依赖包引起的报错。
效果是不是很显著,你以为这就完了?不不不,图中那条49秒的构建我可不是大意截进去的,伪装成不经意的失误,就是为了丝滑的承上启下
既然这个项目的开发人员很少,而且依赖的添加/更新频率又极低,也就意味着每次npm i
所安装的东西,基本都是一样的,既然都一样,为什么我还要rm -rf node_modules
再安装?
想象一下你日常本地开发时,如果某次需求要用到一个新的npm包,你一定是先npm install xxx
,除非碰到了依赖冲突,否则不会清除node_moduels
重新安装。
明明npm
提供了梳理依赖树、只做局部更新的逻辑,我们却偏偏每次清除node_modules
再install
,这种行为吧,我感觉就像明明是个Vue
项目,却在里边到处使用Document API
。
哎嘿,我就不用你的响应式,就是玩~
冒着被打的风险又私聊了运维哥,把rm -rf node_modules
去掉,再发布了一次看看效果
优秀!打包时间从5分多直接干到了50秒,优化率80%+!
本文结束!
在正式结束前,觉得还是有必要补充两点
- 各位读者在做打包优化时,部署脚本是否清除
package-lock.json
和node_modules
还是主要取决于项目实际情况和团队协作模式,不能因为一味的追求构建速度而导致频繁的构建失败/安装依赖失败[滑稽]
- 如果您经过深思熟虑后觉得还是有必要清除
package-lock.json
和node_modules
,山人还有一计可供大王优化构建速度:打包时离不开babel,但babel又是个老大难,好在它能缓存转换结果。一般情况下缓存会放在项目目录下的/node_modules/.cache/
,那我们把删除node_modules
的命令稍微改那么亿点:
find node_modules/ -mindepth 1 -maxdepth 1 ! -name '.cache' -exec rm -rf {} +
删除
node_modules
里面除了/.cache
目录以外的其他内容,这样在构建过程中babel还是能使用到之前的缓存。那速度,体验过的都说好!(看babel-loader的缓存文件有多大)
全面升级
如果是本着以后不影响吃午饭的目的,那现在确实可以结束了。但我自幼便深受中国四大名句之一来都来了的兄弟句式——干都干了的文化熏陶:既然已经开始了,那就干脆给项目做个全套大保健!
不过此时我和在座的各位都一样,对打包优化这块着实没什么经验,可以说是毫无头绪。
浅浅百度了下webpack打包优化,有两个工具基本每篇文章都有提到:打包耗时分析speed-measure-webpack-plugin
、打包体积分析webpack-bundle-analyzer
(vue-cli内置)
目前的痛点是慢,那就先来个耗时分析试试水。
使用方法还是老样子,自己去查,别人都写的我就不再重复写了。
效果如图:
此时因为babel
和eslint
还没有缓存,耗时多是意料之中的;其他的loader或多或少的三两组合,展示了一个module计数和耗时小计,我从中并没有办法获得什么有用的信息;并且多次构建对比发现loader组合的规律和耗时的排名也无迹可寻。在我的认知里:所有被命中的文件会按照loader
配置的顺序依次处理,所以面对这样的结果我实在是无计可施。(有会看的朋友可以补充一下)
翻看speed-measure-webpack-plugin
的文档,发现有可以打印耗时top N文件的配置项,但开启后再次构建得到的这些文件,同样令我摸不着头脑:一个寥寥数十行的SFC
小组件,css-loader
耗时竟然能用四到五秒!要知道里边只有一条scoped
的样式规则。
无奈只好放弃,看了下项目用的是vue-cli@4.x
创建的,对应的webpack@4.x
,那就去webpack
的文档里逛逛碰碰运气吧!
可惜,福无双至祸不单行。文档里翻了半天,耳熟能详、配置简单的路子,例如babel-loader、eslint-loader的编译缓存
、多线程打包
、chunk分割
、代码混淆压缩
、tree shaking
这些,要么是之前已经被配置过了,要么是webpack
内置了。而复杂、高级一些的优化方式,我的项目又用不到...
直到我看到了这里:
升级webpack
简单(呸),npm upgrade webpack
嘛,先来搞这个~~
回到项目的package.json
里,咦,好奇怪,没有webpack
,也没有vue-cli
。
vue-cli
是装在全局的,而webpack
是作为依赖的依赖安装的,没有体现在package.json
中,所以直接npm upgrade webpack
应该是不行的。vue-cli
的文档提供了一个升级的命令:vue upgrade
既然要升级,干脆全上新的!Node
也给他干到20!(我也不知道我当时为什么要这么做,但这为后来的事情埋下了伏笔。。。)
vue-cli
升级完,扫了一遍webpack升级指南,发现我项目里的配置文件也没什么好改的,Nice!
本地浅浅的run了一下server
、run了一下build
,发现也都OK!那就提交上去在Jekins上试试Node V14
o不ok
emmmm...
报错倒是没报错,只是...
本地build
的时候没注意,Jekins上跑才发现,怎么慢了这么多!说好的更新到最新版本均有助于提高性能呢?
再看看这构建物的体积
Hà的我赶紧又本地build
了一次,还真让我发现了些东西:似乎build
了两次
按理来说应该只有下边这个print,那上边的legacy bundle
又是什么东西?百度上随便那么一搜,应该是不少人都被这么坑过,很容易搜到:这是一种兼容性的构建产物,主要是为了兼容一些很古老版本的浏览器/客户端。想控制也很容易,改package.json
里的browserslist
字段即可。
这就好办了,这项目是我们公司的内部项目,考虑兼容性?不存在的。
{
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}
配置了之后又试了下,基本恢复到了升级webpack
之前的水平,但还是慢一点点...
构建速度的优化这块,实在是没头绪了,明明升级了webpack
版本,构建速度却变慢了。
不过刚才提到的两个工具,构建耗时分析的用过了,还有个vue-cli
内置的构建体积分析工具没体验;如果需要打包的东西变少了,那构建速度应该也能快一点吧!(吧?)
塑形瘦身
在正式瘦身前,有一个小插曲:
不知道在座的各位,项目里有没有这样的东西
console.log(123123)
// or
console.log('asdfasdf')
我是一个崇尚极简的人,我能接受的底线也就是
console.log('list data: ', data);
仅此而已
你要打印接口返回数据,Network
里能看
你要打印函数中某个变量的值,可以打断点
我实在是想不出什么必须console.log
的场景
如果你说为了方便线上调试
我能接受的最多也就是按规范打印有意义的log
更别提项目首屏就要翻好几页的无意义log,要知道,大量的console.log
也是会影响首屏加载性能的
在之前,我通过husky + lint-staged
进行过限制,但还是有人以我这个有用、这之前不是我写的等等诸多借口绕过了eslint
检测,提交了无意义的log。所以这次我最终还是决定,你不仁就休怪我不义,TerserPlugin
drop_console
走起,本地开发你随便log,只要发到线上我就删掉。
{
plugins: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
pure_funcs: ["console.log", "console.info"],
},
},
})
]
}
毕竟删掉几句console.log
,也算瘦身
接着就webpack-bundle-analyzer
走起,vue-cli
内置的使用方式是
vue-cli-service build --report
打包后会在你输出的目录里边生成一个report.html
,当时的截图找不到了,用语言描述一下就是:从node_moduels
里打进去的依赖包,面积直接占了整个屏幕的大概三分之一。那个图网上很容易找到,内容就是打包产物按照体积和来源绘制成一个个的矩形在页面里。
这其实是好事,打进去的依赖包多,我们的可操作空间就大,先拿Vue
开个刀。
// vue.config.js
module.exports = {
// ...
configureWebpack: {
externals: {
vue: "Vue",
},
},
// ...
}
也不要忘了把package.json
里的vue
依赖删除掉、在/public
的模版HTML
中,通过<script>
引入CDN文件。
再打个包看看效果:
可以看到vue
确实咩有了
但在调试的过程中,发现第三方CDN不稳定,时而获取超时
为了保险起见,只得把CDN文件copy到本地/public
里来(我司没有自己的CDN或者依赖私仓,正在筹备中)
暂时没什么问题了,下一个就是我们的UI组件库ant-design-vue@1.7.8
,按照相同的方式配置一下,不过这次运行后有报错了:
可以看到报错是和moment
有关系,在antd的文档也找到了原因:如果使用已经构建好的文件,要自行引入moment
为什么antdv不做按需引入?原因有二:
- 项目的入口
main.js
中全量导入了antdv
进行注册,页面中直接使用。如果要改成按需引入,要么每个页面里新增按需引入的语句,要么统计使用了哪些组件在main.js
里改为按需引入(似乎有plugin解决这个问题,记不太清了)
- 按需的这个需,基本等于全量了。。。粗略的扫了一下文档,除了像
Comment
和Mentions
这种带有互动性质的组件,其他的基本都用上了,所以改按需好像意义也不大
改moment
的时候国际化有一个小问题,CDN网站一般会提供以下几种文件:
- 无国际化的
moment
主体文件 - 带全部语言包的
moment
主体文件 - 单个语言包文件(无功能)
如果没有国际化的需求,那是万万没有必要引入全部语言包的moment
。但moment
默认是英语,至少需要引入一个中文语言包。碰巧antdv
也需要做国际化处理,是相同的问题。
moment
和antdv
的国际化方式很相似:
<a-config-provider :locale="antdLocale" />
moment.locale(momentLocale);
data() {
return {
antdLocale,
momentLocale
}
}
我们只需要知道这个locale
运行时的值,把它提取出来就行了。打印后发现其实就是个很简单的key-value
对象(不是JSON
),在node_modules
中的源码里找到 它们复制出来在/public
下新建zhCN
文件:
window.momentLocale = xxx /* 复制出来的对象 */
window.antdLocale = xxx /* 复制出来的对象 */
使用时:
<a-config-provider :locale="antdLocale" />
moment.locale(momentLocale);
data() {
return {
antdLocale: window.antdLocale,
momentLocale: window.momentLocale
}
}
以后如果有别的依赖也有类似的国际化需求,继续向zhCN.js
里添加就行。只需要新增一个http
请求,就解决了所有依赖的国际化问题。
剔除了antdv
和moment
之后的report.html
:
惊喜的发现,antdv
的icons
也被一起干掉了。
少了这么几个大家伙,此时必须要Jekins上build一波看看效果!
还记得之前把Node
给升到20了吗
于是就...报错了...Node
版本太低...
本地切回NodeV14
,发现连server
也起不来了。。
摸黑前行
预警:这将是一段枯燥且艰难的黑暗时光
搞过的都知道,处理Node
版本兼容问题时,如果是需要升级还好;如果是要降级,Node
内置的各种包会出现稀奇古怪的报错,而且这些报错还难以trace
...
由于这趴的问题实在过于稀奇,甚至在google上都搜不到有用的信息,所以基本都没有截图,但我会尽可能的描述出我对问题的看法。看文字也许你觉得云淡风轻解决起来很轻松,但实际上花费了我接近一整天的时间以及一撮撮掉落的头发...
1. npm run server
出现大量的.vue
单文件报错
具体的报错信息记不太清,但报错顺序与路由表注册的顺序相符(和动态路由懒加载是两码事,路由懒加载是在运行时访问到页面才会加载对应的chunk
,但编译打包时,只要是代码中webpack
能trace
到的文件,都会被处理)。目测是所有的.vue
都有报错,那问题就应该不是出在代码上,而是整体配置上。
翻看vue-loader
文档时看到了这个
升级vue-cli
时确实也升级了vue-loader
,按照指引配置了下,resolve
2. jsx语法报错
这个问题就有点奇怪了,在升级前,是没有给webpack
做过什么支持jsx
语法的配置的。升级后,却都报错了。
翻阅了一些资料和支持jsx
的解决方案,大部分都是说把SFC
的<script>
加上lang="jsx"
,里边的内容全部当作jsx
解析。这种方式对eslint
和babel
的配置改动比较大,曾数次尝试无法成功,最后都把所有配置还原重新开始。
后来灵光一闪,不如直接用刚更新的vue-cli
创建一个新项目,看是否支持,如果可行的话,直接把各个配置文件照搬即可。
结果还真可以。babel.config.js
、vue.config.js
以及package.json
里eslintConfig
字段,先全部按照官方脚手架的配置改掉,成功启动之后再挨个把我们自定义的配置添加回去。这过程当中没有出现什么问题,且按下不表,resolve
3. 启动之后,页面白屏报错:(0 , vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent) is not a function
其中resolveComponent
也有可能是其他一些Vue3
暴露出来的Api,通过打断点观察,推测是Vue
内部在初始化的时候出了问题。
不确定是哪里出了问题,但在把之前删除的Vue
依赖安装回来(只是开发环境会用到,打包不会打进去)以及把添加的VueLoaderPlugin
去掉以后,resolve
迎接黎明
以上这些问题解决以后,已经可以正常启动、打包项目了。但刚才的bundle analyzer
进行到一半还没结束,图中的第三方依赖库应该还有一些可以剔除掉的,比如隐藏在一个业务代码chunk
里的echarts
检索了代码后,发现有按需引入的:
import {xxx, xxx} from "echarts"
也有全量引入的:
import * as echarts from 'echarts';
在分析代码后整理了所使用到的echarts
Api和组件,把全量引入改为按需引入,重新打包后发现包体积没有变。我好奇难道echarts
只要有一个地方使用了按需引入,其他地方也能自动分析把全量引入改为按需引入?遂把按需引入的也反向改为全量引入重新打包,结果:
第二次的改动体积变化了,那就只能说明....
问了写那段代码的同事,果然,在需求迭代的过程当中技术方案变更了,所以那个文件废弃掉没有用了,改了个寂寞...
此时还剩下jquery
和lodash
计划剔除掉,其他的依赖包有一部分已经是比较规范的按需引入,剔除掉改为cdn引入带来的收益不大,当然主要的原因还是因为懒
jquery
:这个npm包有点意思
打进来的是非压缩版本,因为package.json
中设置的main
就确实是这个,但dist
包中明明提供了压缩后的版本。两个版本的体积差距在三倍多,不知包作者的意图是什么
但最后还是把jquery
这个依赖彻底放弃了:整个项目中只有一个远古时期添加的图片预览组件依赖了它,而我们现在开发了样式、功能更为强大的新组件,所以把所有使用到这个组件的地方都改为使用新组件,然后顺带把jquery uninstall
了。
lodash
:官网本身提供了可按需引入的版本lodash-es
,但项目中太多地方都是全量引入的方式在使用
import * as _ from "lodash"
暂且先改成CDN的方式全量引入
至此,bundle analyzer
的分析图变成了这样:
三方依赖的chunk
已经比包含了echarts
的那个业务代码chunk
体积还要小。瘦身瘦到这里感觉差不多了,那些更小的依赖包本身体积不大,换成一个http
请求也未必是一件划算的事。
然后就还是回到webpack
的配置上来,前边一直在琢磨怎么添加配置去做优化,但vue-cli
本身已经封装了一套久经考验的配置,不如从这个配置着手,看能否针对我们项目的实际情况做一些修改
获取配置命令(融合了自定义的配置)
vue inspect --mode=production > file-name.js
mode
不传的话默认是development
。下载下来打开,1400多行猛的一看似乎有点唬人,但实际上有1000行左右都是对样式文件的loader
配置。
粗略的看下vue-cli@5.0.8
中有哪些值得注意的配置
- 解析文件的优先级
// 导入模块时如果不提供文件后缀,同名文件 后缀名的优先级
extensions: [".mjs", ".js", ".jsx", ".vue", ".json", ".wasm"]
- Hash
optimization: {
realContentHash: false, // 使用非严格的hash计算,减少耗时
}
- 代码压缩:
css
使用的是CssMinimizerPlugin
,js
使用的是TerserPlugin
minimizer: [
// 已经内置了js压缩工具terser
new TerserPlugin({
terserOptions: {
compress: {
arrows: false,
collapse_vars: false,
comparisons: false,
computed_props: false,
hoist_funs: false,
hoist_props: false,
hoist_vars: false,
inline: false,
loops: false,
negate_iife: false,
properties: false,
reduce_funcs: false,
reduce_vars: false,
switches: false,
toplevel: false,
typeofs: false,
booleans: true,
if_return: true,
sequences: true,
unused: true,
conditionals: true,
dead_code: true,
evaluate: true,
},
mangle: {
safari10: true, // 代码混淆时兼容使用`let`关键字声明的循环迭代器变量可能会出现无法重复声明let变量的错误。
},
},
parallel: true, // 多进程打包
extractComments: false, // 不将注释单独提取到一个文件中
}),
new CssMinimizerPlugin({
parallel: true,
minimizerOptions: {
preset: [
"default",
{
mergeLonghand: false,
cssDeclarationSorter: false,
},
],
},
}),
]
- Loader
- 大量的篇幅编排不同样式文件相关的
Loader
,分别有css
、postcss
、scss
、sass
、less
、stylus
,按照css moduels in SFC
->SFC style
->normal css modules
->normal css
的顺序依次处理。 - 对于脚本文件,已经开启了多线程转译以及babel缓存功能
- 大量的篇幅编排不同样式文件相关的
{
test: /\.m?jsx?$/,
exclude: [
function () {
/* omitted long function */
},
],
use: [
{
loader:
"path-to-your-project/node_modules/thread-loader/dist/cjs.js",
},
{
loader:
"path-to-your-project/node_modules/babel-loader/lib/index.js",
options: {
cacheCompression: false,
cacheDirectory:
"path-to-your-project/node_modules/.cache/babel-loader",
cacheIdentifier: "1d489a9c",
},
},
],
}
- Plugin
VueLoaderPlugin
:已经内置了DefinePlugin
:注入编译时的全局配置CaseSensitivePathsPlugin
:路径的大小写严格匹配FriendlyErrorsWebpackPlugin
:优化报错信息MiniCssExtractPlugin
HtmlWebpackPlugin
CopyPlugin
:配置了info.minimized = true
,copy的同时也会压缩ESLintWebpackPlugin
:同样开启了缓存
得,不仅没找到有啥可优化的地方,甚至还污染了人自带的配置:
已经内置了TerserPlugin
,前边为了打包时去除console
在plugin
里边又配置了一次,通过speed-measure-webpack-plugin
分析时发现似乎是走了两遍TerserPlugin
。
只好通过webpack-chain
去注入一下,顺便把项目中其他修改webpack
配置的地方也改为注入的形式。(使用ConfigureWebpack
去改,无法改到已有的TerserPlugin
配置):
chainWebpack: (config) => {
config.when(process.env.NODE_ENV === "production", (config) => {
config.devtool(false);
config.optimization.minimizer("terser").tap((args) => {
const compress = args[0].terserOptions.compress;
args[0].terserOptions.compress = {
...compress,
drop_console: true,
pure_funcs: ["console.log", "console.info"],
};
return args;
});
});
config
.externals({
vue: "Vue",
moment: "moment",
"moment/locale/zh-cn": "moment.local",
"ant-design-vue": "antd",
lodash: "_",
})
.resolve.alias
.set("@", path.join(__dirname, "src"))
.set("@worker", path.resolve(__dirname, "public/worker.js"))
.end();
config.plugin("speed-measure").use(SpeedMeasurePlugin);
}
如果使用ConfigureWebpack
:
configureWebpack: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
pure_funcs: ["console.log", "console.info"],
},
},
}),
],
},
集成的配置最下方会出现一个新的minimizer
数组,不是我们想要的效果
截止到目前,构建速度变成这样(果然还是没有变更快)
从项目剔出去的第三方依赖,体积是这么多
不过虽然没打进chunk里去,但还是作为静态依赖在构建产物中,下一步就是搭一个公司内部的简易缓存服务器(有关缓存的内容可以看我上一篇文章)。届时这部分体积才算真正从项目里移除了,不过此时我们还是可以把它视作优化的成果,由于没有对别的类型的资源做什么优化处理,也压根就没什么别的资源,所以只看打包后的js体积的话:
优化前
优化后
数据也基本对的上,所以综合来看:
- 平均构建速度(average full time):从5分50秒减少到54秒,优化率84.48%(
1 - 54秒 / 3分50秒
) - 脚本构建体积(script size):从5.5M减少到3.6M,优化率35.55%(
1 - 3.6M / 5.5M
)
先这样吧,至少下次被问到webpack
,多少有点自己的东西能讲,近期可能也不会再更新文章了,原因嘛,你们懂的
欢迎真诚交流,但如果你来抬杠?阿,对对对~ 你说的都对~
来源:juejin.cn/post/7389044903940603945
2024 年中总结:务虚年代,逆风飞翔
大家好,我是双越老师,也是 wangEditor 作者
这是一篇散文,没啥结构性,比较随性,想到啥就写点啥吧。
不觉 2024 年过了 6 个月了,你有没有总结一下自己这半年做了啥?除了上班下班,有没有攒下一些钱?有没有收获一些美好的经历?自己的专业技能有没有得到提升?
如果你日常有积累、有主动去争取什么东西,哪怕每天一点点,积累半年就是一个大成就。如果你什么都没有,或很少,那时间也不会可怜你。
回顾我自己的工作日志,我这半年积累的还不错,比去年要好很多。是的,自由职业这么多年,我一直有记录工作日志的习惯,保持自律。
首先,我觉得我做的最好一件事儿,不是工作中的,是戒烟。我从 2023.8 开始戒烟,一直到现在 9 个月的时间了。戒烟也是我 2023 年做的最成功的一件事儿,2024 年继续。
戒烟是一个持久的事情,有些人戒 3 年还会复吸,我能继续保持到现在也是非常不容易。未来继续保持,这比工作成绩都重要。
戒烟,第一是保持身体的健康状态,例如现在嗓子清爽,跑步时呼吸通畅;第二是要让自己摆脱对某个东西的强依赖,而恢复自由的状态。因为烟瘾本质上就是尼古丁的戒断反应、就是生理du瘾,戒不掉就永远是它的奴隶。
今年春天开始,我也几乎不喝酒了。这半年聚餐吃饭无数次,但几乎没有喝过酒(偶尔一点点啤酒),我朋友戏称:你这戒烟戒酒,再往后就得戒色了...
我不懂葡萄酒,但那种纯麦啤酒,喝起来很香甜的那种,我还是比较喜欢的,当然得少喝。但日常聚餐的这些工业啤酒、白酒就算了,尤其白酒,本身就是一种反人类的饮料。爱喝白酒的人,都是为了快速获得酒精的刺激,跟吸烟是一个道理,所谓好酒就是既能让你快速获得酒精刺激、又不那么辣嗓子、第二天又不是很难受 —— 第一个目的最重要。
所以,我觉得和烟拜拜了,酒也该再见了,都是一类东西,都是瘾品(虽然我没酒瘾)。古人说的“酒色财气”都归属于“酒”类,因为古代没有烟。
同时,我一直坚持着每日跑步和 15 分钟的力量训练,规律生活早睡早起,所以整体感觉还不错。颈椎病依然还有(只要得了,就无法完全恢复)但不难受,也没啥影响。
再聊聊工作吧。
1月的时候,我实在是不知道干啥了。PS. 其实一整个 2023 年都不知道干啥,太闲了,所以戒烟找点成就感,没想到成功了。
当时的任务就是把我的两门 ke 程更新一下,一门是面试课(这个每年都更新一次),一门是《React仿问卷星 低代码》升级了服务端,使用 Nest.js 写的。
同时也有很多人找我 1v1 面试咨询,我记得春节假期还没结束,就开始预约了。集中忙了大概一个月,每天都和 2-3 个人聊,一边分析简历,一边绞尽脑汁的思考,一边写总结,一边聊,非常累。
但是后来到了 3、4 月发现,今年的行情真的太差了,之前的“金三银四”今年是一点都没体现出来,没体现出任何热度,倒是裁员不断。这和刚刚过去的 6.18 很像,也没体现出任何热度,就和 5 月一样。
大家就像一头一头的死猪,拿开水刺激已经不管用了,无论是公司还是个人。
在这种行情下,我能干啥呢?很多人说自己被裁员,找不到工作,其实我也有相似之处,你们就业环境不好,我也找不到突破口。怎么办呢?
要不我也像其他“讲师”(网红)一样,去深入搞一搞面试服务?开个训练营,多收一些钱,宣传一顿:保证就业率、涨薪多少……
其实能实打实的为学生服务的讲师也挺好的,为你备课,为你准备面试题,为你准备项目,价格公道,这已经很好了。还有一些机构跟骗子也差不多。
有专门的销售催着你,学费 1w 起步,承诺内推、承诺涨薪 xx —— 理性思考,当前这环境,哪儿这好事。
很惭愧,我虽然也是个自由职业,但对圈里的这些事儿,我还真不清楚。咨询了一个朋友才知道具体的事情。
后来我跟那位同学说:这样,你先让她给你安排内推机会,只要有了面试邀请,你就报名 —— 你看她还理不理你。
我当时想了一个月,我该干嘛呢,最终决定还是踏踏实实的做个项目吧,慢慢开发慢慢积累,做出一个扎实的产品才是长久之道。
而且也是一个真正的壁垒,自己长久积累做出来的项目,别人不可能一下子就做出来。但像面试服务这种东西,想搭建起来是很容易的,没啥壁垒,面试题、考点就那么多,找个技术讲师就齐了。最近有很多前端 A 哥、B 哥、C 哥的,都是这种机制,自己负责发广告招人,然后雇讲师上课。
但做项目做个啥呢?选题和方向也是需要思考的,主动思考是最难的事情。
第一,不能过于简单,技术和业务都得有一定复杂度,而且要是全栈的,前后端都有。也不能过于复杂,一个人搞个淘宝微信也搞不了。
第二,要是真实上线的项目,因为课程项目一直被吐槽为 demo ,所以这次我要做一个不一样的,这样才有差异化。
第三,还要带有一定的话题性和热度,和当前技术发展趋势要吻合。例如今年低代码不再热门了,就不要再用低代码这个话题了。所以我选择了 AI 方向。
第四,要考虑自己擅长的方向,发挥自己擅长的技术点。我擅长富文本编辑器领域,这个要充分利用起来。
于是这个项目就是,使用 Node 全栈 Next.js 开发 AIGC 知识库项目 划水AI ,参考 Notion AI ,基于 GPT 大模型,开发 AI 写作、AI 处理文本,这是当前的热门话题。
现在一期功能(文档管理、富文本编辑器、AI 写作)已经上线试用,二期(团队、分享、多人协同编辑)正在开发中,想加入学习的同学可以找我。
这个项目的研发过程,被我记录成了详细的 wiki 还有代码提交记录,我每做一步就记录一步,遇到一个 bug 我就记录一个 bug —— 这都是宝贵的项目经验。
现在是一个务虚的时代,务实的人太少了。务虚低成本高收益。务实,成本高,而且你还得能做出来呀!君子只动口,傻子才动手呢。 中国人自古以来就是:圣人坐而论道、舌战群儒,没有去动手做实验的。
我已 36 岁,还能有这种创造力和执行力其实挺不容易的。
30 岁之前大家都精力满满,对未来充满好奇和希望,对技术充满动力和热情,浑身都充满了创造力。但是慢慢熬到 35+ 就容易如老牛一般(王小波说的“被锤的公牛”)被工作生活磨的角都没了。
我最早开始写博客是 2014 年,最早开发 wangEditor 是 2015 年,至今已快 10 年了。即便强如周杰伦,他的创作黄金期也不到 10 年(随便举个例子,不是拿自己和周杰伦比,我也是周杰伦铁粉~)
总结一下。
半年,从一开始的迷茫、没有方向,到后来思考、分析、确定项目,再到后来开发、测试、一期上线。时间一晃而过,但回头一看确实积累了很多东西。
下半年继续,把二期、三期搞定并上线,用一年时间做出一款优秀的产品,教你亮瞎面试官的眼睛!
与君共勉~
来源:juejin.cn/post/7388891131037614089
听说你会架构设计?来,弄一个打车系统
目录
- 引言
- 网约车系统
- 需求设计
- 概要设计
- 详细设计
- 体验优化
- 小结
1.引言
1.1 台风来袭
深圳上周受台风“苏拉”影响,从 9 月 1 日 12 时起在全市启动防台风和防汛一级应急响应。
对深圳打工人的具体影响为,当日从下午 4 点起全市实行 “五停”:停工、停业、停市,当日已经停课、晚上 7 点后停运。
由于下午 4 点停市,于是大部分公司都早早下班。其中有赶点下班的,像这样:
有提前下班的,像这样:
还有像我们这样要居家远程办公的:
1.2 崩溃打车
下午 4 点左右,公交和地铁都人满为患。
于是快下班(居家办公)的时候就想着打个车回家,然而打开滴滴之后:
排队人数 142 位,这个排队人数和时长,让我的心一下就拔凉拔凉的。
根据历史经验,在雨天打上车的应答时间得往后推半个小时左右。更何况,这还是台风天气!
滴滴啊滴滴,你就不能提前准备一下嘛,这个等待时长,会让你损失很多订单分成的。
但反过来想,这种紧急预警,也不能完全怪打车平台,毕竟,车辆调度也是需要一定时间的。在这种大家争相逃命(bushi 的时候,周围的车辆估计也不太够用。
卷起来
等着也是等着,于是就回到公司继续看技术文章。这时我突然想到,经过这次车辆紧急调度之后,如果我是滴滴的开发工程师,需要怎么处理这种情况呢?
如果滴滴的面试官在我眼前,他又会怎么考量候选人的技术深度和产品思维呢?
2. 设计一个“网约车系统”
面试官:“滴滴打车用过是吧!看你简历里写道会架构设计是吧,如果让你设计一个网约车系统,你会从哪些方面考虑呢?”
2.1 需求分析
网约车系统(比如滴滴)的核心功能是把乘客的打车订单发送给附件的网约车司机,司机接单后,到上车点接送乘客,乘客下车后完成订单。
其中,司机通过平台约定的比例抽取分成(70%-80%不等),乘客可以根据第三方平台的信用值(比如支付宝)来开通免密支付,在下车后自动支付订单。用例图如下:
乘客和司机都有注册登录功能,分属于乘客用户模块和司机用户模块。网约车系统的另外核心功能是乘客打车,订单分配,以及司机送单。
2.2 概要设计
网约车系统是互联网+共享资源的一种模式,目的是要把车辆和乘客结合起来,节约已有资源的一种方式,通常是一辆网约车对多个用户。
所以对于乘客和司机来说,他们和系统的交互关系是不同的。比如一个人一天可能只打一次车,而一个司机一天得拉好几趟活。
故我们需要开发两个 APP 应用,分别给乘客和司机打车和接单,架构图如下:
1)乘客视角
如上所示:乘客在手机 App 注册成为用户后,可以选择出发地和目的地,进行打车。
打车请求通过负载均衡服务器,经过请求转发等一系列筛选,然后到达 HTTP 网关集群,再由网关集群进行业务校验,调用相应的微服务。
例如,乘客在手机上获取个人用户信息,收藏的地址信息等,可以将请求转发到用户系统。需要叫车时,将出发地、目的地、个人位置等信息发送至打车系统。
2)司机视角
如上图所示:司机在手机 App 注册成为用户并开始接单后,打开手机的位置信息,通过 TCP 长连接定时将自己的位置信息发送给平台,同时也接收平台发布的订单消息。
司机 App 采用 TCP 长连接是因为要定时发送和接收系统消息,若采用 HTTP 推送:
一方面对实时性有影响,另一方面每次通信都得重新建立一次连接会有失体面(耗费资源)。
司机 App:每 3~5 秒向平台发送一次当前的位置信息,包括车辆经纬度,车头朝向等。TCP 服务器集群相当于网关,只是以 TCP 长连接的方式向 App 提供接入服务,地理位置服务负责管理司机的位置信息。
3)订单接收
网关集群充当业务系统的注册中心,负责安全过滤,业务限流,请求转发等工作。
业务由一个个独立部署的网关服务器组成,当请求过多时,可以通过负载均衡服务器将流量压力分散到不同的网关服务器上。
当用户打车时,通过负载均衡服务器将请求打到某一个网关服务器上,网关首先会调用订单系统,为用户创建一个打车订单(订单状态为 “已创建”),并存库。
然后网关服务器调用打车系统,打车系统将用户信息、用户位置、出发地、目的地等数据封装到一个消息包中,发送到消息队列(比如 RabbitMQ),等待系统为用户订单分配司机。
4)订单分配
订单分配系统作为消息队列的消费者,会实时监听队列中的订单。当获取到新的订单消息时,订单分配系统会将订单状态修改为 “订单分配中”,并存库。
然后,订单分配系统将用户信息、用户位置、出发地、目的地等信息发送给订单推送 SDK。
接着,订单推送 SDK 调用地理位置系统,获取司机的实时位置,再结合用户的上车点,选择最合适的司机进行派单,然后把订单消息发送到消息告警系统。这时,订单分配系统将订单状态修改为 “司机已接单” 状态。
订单消息通过专门的消息告警系统进行推送,通过 TCP 长连接将订单推送到匹配上的司机手机 App。
5)拒单和抢单
订单推送 SDK 在分配司机时,会考虑司机当前的订单是否完成。当分配到最合适的司机时,司机也可以根据自身情况选择 “拒单”,但是平台会记录下来评估司机的接单效率。
打车平台里,司机如果拒单太多,就可能在后续的一段时间里将分配订单的权重分数降低,影响自身的业绩。
订单分派逻辑也可以修改为允许附加的司机抢单,具体实现为:
当订单创建后,由订单推送 SDK 将订单消息推送到一定的地理位置范围内的司机 App,在范围内的司机接收到订单消息后可以抢单,抢单完成后,订单状态变为“已派单”。
2.3 详细设计
打车平台的详细设计,我们会关注网约车系统的一些核心功能,如:长连接管理、地址算法、体验优化等。
1)长连接的优势
除了网页上常用的 HTTP 短连接请求,比如:百度搜索一下,输入关键词就发起一个 HTTP 请求,这就是最常用的短连接。
但是大型 APP,尤其是涉及到消息推送的应用(如 QQ、微信、美团等应用),几乎都会搭建一套完整的 TCP 长连接通道。
一张图看懂长连接的优势:
图片来源:《美团点评移动网络优化实践》
通过上图,我们得出结论。相比短连接,长连接优势有三:
- 连接成功率高
- 网络延时低
- 收发消息稳定,不易丢失
2)长连接管理
前面说到了长连接的优势是实时性高,收发消息稳定,而打车系统里司机需要定期发送自身的位置信息,并实时接收订单数据,所以司机 App 采用 TCP 长连接的方式来接入系统。
和 HTTP 无状态连接不同的是,TCP 长连接是有状态的连接。所谓无状态,是指每次用户请求可以随意发送到某一台服务器上,且每台服务器的返回相同,用户不关心是哪台服务器处理的请求。
当然,现在 HTTP2.0 也可以是有状态的长连接,我们此处默认是 HTTP1.x 的情况。
而 TCP 长连接为了保证传输效率和实时性,服务器和用户的手机 App 需要保持长连接的状态,即有状态的连接。
所以司机 App 每次信息上报或消息推送时,都会通过一个特定的连接通道,司机 App 接收消息和发送消息的连接通道是固定不变的。
因此,司机端的 TCP 长连接需要进行专门管理,处理司机 App 和服务器的连接信息,架构图如下:
为了保证每次消息的接收和推送都能找到对应通道,我们需要维护一个司机 App 到 TCP 服务器的映射关系,可以用 Redis 进行保存。
当司机 App 第一次登录,或者和服务器断开连接(比如服务器宕机、用户切换网络、后台关闭手机 App 等),需要重连时,司机 App 会通过用户长连接管理系统重新申请一个服务器连接(可用地址存储在 Zookeeper 中),TCP 连接服务器后再刷新 Redis 的缓存。
3)地址算法
当乘客打车后,订单推送 SDK 会结合司机所在地理位置,结合一个地址算法,计算出最适合的司机进行派单。
目前,手机收集地理位置一般是收集经纬度信息。经度范围是东经 180 到西经 180,纬度范围是南纬 90 到北纬 90。
我们设定西经为负,南纬为负,所以地球上的经度范围就是[-180, 180],纬度范围就是[-90,90]。如果以本初子午线、赤道为界,地球可以分成4个部分。
根据这个原理,我们可以先将二维的空间经纬度编码成一个字符串,来唯一标识司机和乘客的位置信息。再通过 Redis 的 GeoHash 算法,来获取乘客附加的所有司机信息。
GeoHash 算法的原理是将乘客的经纬度换算成地址编码字符串,表示在某个矩形区域,通过这个算法可以快速找到同一个区域的所有司机。
它的实现用到了跳表数据结构,具体实现为:
将某个市区的一块范围作为 GeoHash 的 key,这个市区范围内所有的司机存储到一个跳表中,当乘客的地理位置出现在这个市区范围时,获取该范围内所有的司机信息。然后进一步筛选出最近的司机信息,进行派单。
4)体验优化
1. 距离算法
作为线上派单,通过距离运算来分配订单效果一定会比较差,因为 Redis 计算的是两点之间的空间距离,但司机必须沿道路行驶过来,在复杂的城市路况下,也许几十米的空间距离行驶十几分钟也未可知。
所以,后续需综合行驶距离(而非空间距离)、司机车头朝向以及上车点进行路径规划,来计算区域内每个司机到达乘客的距离和时间。
更进一步,如果区域内有多个乘客和司机,就要考虑所有人的等待时间,以此来优化用户体验,节省派单时间,提升盈利额。
2. 订单优先级
如果打车订单频繁取消,可根据司机或乘客行为进行判责。判责后给乘客和司机计算信誉分,并告知用户信誉分会影响乘客和司机的使用体验,且关联到派单的优先级。
司机接单优先级
综合考虑司机的信誉分,投诉次数,司机的接单数等等,来给不同信誉分的司机分配不同的订单优先级。
乘客派单优先级
根据乘客的打车时间段,打车距离,上车点等信息,做成用户画像,以合理安排司机,或者适当杀熟(bushi。
PS:目前有些不良打车平台就是这么做的 🐶 甚至之前爆出某打车平台,会根据不同的手机系统,进行差异收费。
4. 小结
4.1 网约车平台发展
目前,全球网约车市场已经达到了数千亿美元的规模,主要竞争者包括滴滴、Uber、Grab 等公司。在中国,滴滴作为最大的网约车平台已经占据了绝大部分市场份额。
网约车的核心商业逻辑比较简单,利益关联方主要为平台、司机、车辆、消费者。
平台分别对接司机、车辆【非必选项,有很多司机是带车上岗】和乘客,通过有效供需匹配赚取整个共享经济链省下的钱。
具体表现为:乘客和司机分别通过网约平台打车和接单,平台提供技术支持。乘客为打车服务付费,平台从交易金额中抽成(10%-30%不等)。
据全国网约车监管信息交互平台统计,截至 2023 年 2 月底,全国共有 303 家网约车平台公司取得网约车平台经营许可。
这些平台一部分是依靠高德打车、百度地图、美团打车为代表的网约车聚合平台;另一部分则是以滴滴出行、花小猪、T3 为代表的出行平台。
4.2 网约车平台现状
随着出行的解封,网约车平台重现生机。
但由于部分网约车聚合平台的准入门槛太低,所以在过去一段时间里暴露出愈来愈多的问题。如车辆、司机合规率低,遇到安全事故,产生责任纠纷,乘客维权困难等等。
由于其特殊的模式,导致其与网约车运营商存在责任边界问题,一直游离在法律边缘。
但随着网约车聚合平台的监管不断落地,全国各地都出行了一定的监管条例。
比如某打车平台要求车辆将司机和乘客的沟通记录留档,除了司机与乘客的在线沟通记录必须保存以外,还需要一个语音电话或车载录音转换,留存一段时间备查。
有了这些人性化的监管条例和技术的不断创新,网约车平台或许会在未来的一段时间内,继续蓬勃发展。
后话
面试官:嗯,又专又红,全面发展!这小伙子不错,关注了~
来源:juejin.cn/post/7275211391102746684
何时使用Elasticsearch而不是MySql
MySQL 和 Elasticsearch 是两种不同的数据管理系统,它们各有优劣,适用于不同的场景。本文将从以下几个方面对它们进行比较和分析:
- 数据模型
- 查询语言
- 索引和搜索
- 分布式和高可用
- 性能和扩展性
- 使用场景
数据模型
MySQL 是一个关系型数据库管理系统(RDBMS),它使用表(table)来存储结构化的数据,每个表由多个行(row)和列(column)组成,每个列有一个预定义的数据类型,例如整数、字符串、日期等。MySQL 支持主键、外键、约束、触发器等关系型数据库的特性,以保证数据的完整性和一致性。
Elasticsearch 是一个基于 Lucene 的搜索引擎,它使用文档(document)来存储半结构化或非结构化的数据,每个文档由多个字段(field)组成,每个字段可以有不同的数据类型,例如文本、数字、布尔、数组等。Elasticsearch 支持动态映射(dynamic mapping),可以根据数据自动推断字段的类型和索引方式。
MySQL 和 Elasticsearch 的数据模型有以下几点区别:
- MySQL 的数据模型是严格的,需要事先定义好表的结构和约束,而 Elasticsearch 的数据模型是灵活的,可以随时添加或修改字段。
- MySQL 的数据模型是二维的,每个表只有行和列两个维度,而 Elasticsearch 的数据模型是多维的,每个文档可以有嵌套的对象或数组。
- MySQL 的数据模型是关系型的,可以通过连接(join)多个表来查询相关的数据,而 Elasticsearch 的数据模型是非关系型的,不支持连接操作,需要通过嵌套文档或父子文档来实现关联查询。
推荐博主开源的 H5 商城项目waynboot-mall,这是一套全部开源的微商城项目,包含三个项目:运营后台、H5 商城前台和服务端接口。实现了商城所需的首页展示、商品分类、商品详情、商品 sku、分词搜索、购物车、结算下单、支付宝/微信支付、收单评论以及完善的后台管理等一系列功能。 技术上基于最新得 Springboot3.0、jdk17,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中间件。分模块设计、简洁易维护,欢迎大家点个 star、关注博主。
github 地址:github.com/wayn111/way…
查询语言
MySQL 使用标准的 SQL 语言来查询和操作数据,SQL 语言是一种声明式的语言,可以通过简洁的语法来表达复杂的逻辑。SQL 语言支持多种查询类型,例如选择(select)、插入(insert)、更新(update)、删除(delete)、聚合(aggregate)、排序(order by)、分组(gr0up by)、过滤(where)、连接(join)等。
Elasticsearch 使用 JSON 格式的查询 DSL(Domain Specific Language)来查询和操作数据,查询 DSL 是一种基于 Lucene 查询语法的语言,可以通过嵌套的 JSON 对象来构建复杂的查询。查询 DSL 支持多种查询类型,例如全文检索(full-text search)、结构化检索(structured search)、地理位置检索(geo search)、度量检索(metric search)等。
MySQL 和 Elasticsearch 的查询语言有以下几点区别:
- MySQL 的查询语言是通用的,可以用于任何关系型数据库系统,而 Elasticsearch 的查询语言是专用的,只能用于 Elasticsearch 系统。
- MySQL 的查询语言是字符串形式的,需要拼接或转义特殊字符,而 Elasticsearch 的查询语言是 JSON 形式的,可以直接使用对象或数组表示。
- MySQL 的查询语言是基于集合论和代数运算的,可以进行集合操作和数学运算,而 Elasticsearch 的查询语言是基于倒排索引和相关度评分的,可以进行全文匹配和相似度计算。
索引和搜索
MySQL 使用 B+树作为主要的索引结构,B+树是一种平衡多路搜索树,它可以有效地存储和检索有序的数据。MySQL 支持主键索引、唯一索引、普通索引、全文索引等多种索引类型,以加速不同类型的查询。MySQL 也支持外部存储引擎,例如 InnoDB、MyISAM、Memory 等,不同的存储引擎有不同的索引和锁机制。
Elasticsearch 使用倒排索引作为主要的索引结构,倒排索引是一种将文档中的词和文档的映射关系存储的数据结构,它可以有效地支持全文检索。Elasticsearch 支持多种分词器(analyzer)和分词过滤器(token filter),以对不同语言和场景的文本进行分词和处理。Elasticsearch 也支持多种搜索类型,例如布尔搜索(boolean search)、短语搜索(phrase search)、模糊搜索(fuzzy search)、通配符搜索(wildcard search)等,以实现不同精度和召回率的检索。
MySQL 和 Elasticsearch 的索引和搜索有以下几点区别:
- MySQL 的索引是基于数据的值的,可以精确地定位数据的位置,而 Elasticsearch 的索引是基于数据的内容的,可以近似地匹配数据的含义。
- MySQL 的索引是辅助的,需要手动创建和维护,而 Elasticsearch 的索引是主要的,自动创建和更新。
- MySQL 的索引是局部的,只针对单个表或列,而 Elasticsearch 的索引是全局的,涵盖所有文档和字段。
分布式和高可用
MySQL 是一个单机数据库系统,它只能运行在一台服务器上,如果服务器出现故障或负载过高,就会影响数据库的可用性和性能。为了解决这个问题,MySQL 提供了多种复制(replication)和集群(cluster)方案,例如主从复制(master-slave replication)、双主复制(master-master replication)、MySQL Cluster、MySQL Fabric 等,以实现数据的冗余和负载均衡。
Elasticsearch 是一个分布式数据库系统,它可以运行在多台服务器上,形成一个集群(cluster)。每个集群由多个节点(node)组成,每个节点可以承担不同的角色,例如主节点(master node)、数据节点(data node)、协调节点(coordinating node)等。每个节点可以存储多个索引(index),每个索引可以划分为多个分片(shard),每个分片可以有多个副本(replica)。Elasticsearch 通过一致性哈希算法(consistent hashing algorithm)来分配分片到不同的节点上,并通过心跳检测(heartbeat check)来监控节点的状态。如果某个节点出现故障或加入集群,Elasticsearch 会自动进行分片的重新分配和平衡。
MySQL 和 Elasticsearch 的分布式和高可用有以下几点区别:
- MySQL 的分布式和高可用是可选的,需要额外配置和管理,而 Elasticsearch 的分布式和高可用是内置的,无需额外操作。
- MySQL 的分布式和高可用是基于复制或共享存储的,需要保证数据一致性或可用性之间的权衡,而 Elasticsearch 的分布式和高可用是基于分片和副本的,可以根据需求调整数据冗余度或容错能力。
- MySQL 的分布式和高可用是静态的,需要手动扩展或缩容集群规模,而 Elasticsearch 的分布式和高可用是动态的,可以自动适应集群变化。
性能和扩展性
MySQL 是一个面向事务(transaction)的数据库系统,它支持 ACID 特性(原子性、一致性、隔离性、持久性),以保证数据操作的正确性和完整性。MySQL 使用锁机制来实现事务隔离级别(isolation level),不同的隔离级别有不同的并发性能和一致性保证。MySQL 也使用缓冲池(buffer pool)来缓存数据和索引,以提高查询效率。MySQL 的性能主要取决于硬件资源、存储引擎、索引设计、查询优化等因素。
Elasticsearch 是一个面向搜索(search)的数据库系统,它支持近实时(near real-time)的索引和查询,以保证数据操作的及时性和灵活性。Elasticsearch 使用分片和副本来实现数据的分布式存储和并行处理,不同的分片数和副本数有不同的写入吞吐量和读取延迟。Elasticsearch 也使用缓存(cache)和内存映射文件(memory-mapped file)来加速数据和索引的访问,以提高搜索效率。Elasticsearch 的性能主要取决于集群规模、分片策略、文档结构、查询复杂度等因素。
MySQL 和 Elasticsearch 的性能和扩展性有以下几点区别:
- MySQL 的性能和扩展性是有限的,它受到单机资源、锁竞争、复制延迟等因素的限制,而 Elasticsearch 的性能和扩展性是无限的,它可以通过增加节点、分片、副本等方式来水平扩展集群。
- MySQL 的性能和扩展性是以牺牲搜索能力为代价的,它不能支持复杂的全文检索和相关度评分,而 Elasticsearch 的性能和扩展性是以牺牲事务能力为代价的,它不能保证数据操作的原子性和一致性。
- MySQL 的性能和扩展性是以提高写入速度为目标的,它优化了数据插入和更新的效率,而 Elasticsearch 的性能和扩展性是以提高读取速度为目标的,它优化了数据检索和分析的效率。
使用场景
MySQL 和 Elasticsearch 适用于不同的使用场景,根据不同的业务需求,可以选择合适的数据库系统或组合使用两者。以下是一些常见的使用场景:
- 如果需要存储结构化或半结构化的数据,并且需要保证数据操作的正确性和完整性,可以选择 MySQL 作为主要数据库系统。例如,电商网站、社交网络、博客平台等。
- 如果需要存储非结构化或多样化的数据,并且需要支持复杂的全文检索和相关度评分,可以选择 Elasticsearch 作为主要数据库系统。例如搜索引擎、日志分析、推荐系统等。
- 如果需要存储和分析大量的时序数据,并且需要支持实时的聚合和可视化,可以选择Elasticsearch作为主要数据库系统。例如,物联网、监控系统、金融市场等。
- 如果需要同时满足上述两种需求,并且可以容忍一定程度的数据不一致或延迟,可以将 MySQL 作为主数据库系统,并将部分数据同步到 Elasticsearch 作为辅助数据库系统。例如新闻网站、电影网站、招聘网站等。
自此本文讲解内容到此结束,感谢您的阅读,希望本文对您有所帮助。
来源:juejin.cn/post/7264528507932327948
无插件实现一个好看的甘特图
效果
预览地址 code.juejin.cn/pen/7272286…
前言
刚好看到这么一个东西,甘特图,然后又发现好像echarts 里面没有这个图形渲染,也去找了一下插件,都不符合我的要求,然后自己就想着看能不能实现一个,然后说干就干,过程还挺复杂的,不过一想清楚了,也就还行。
逻辑
刚开始看着这个图,想到肯定是用表格来实现的,后面开始渲染的时候,发现是我想简单了,这表格是渲染不出来的,也或者是我技术还没够,反正我找了很多相关的插件,都不是用表格去实现的,后面就改变了一些思路,用div去渲染,然后去定位,这么一想,发现事情就简单了许多。
为什么不用表格实现
每点击一个视图切换,表头就需要重新渲染,表头有索引,名称,负责人,如果是表格实现的话,就是如下代码,一看就知道了id=“tableYear”的不好渲染,因为上面有可能是年,也有可能是月,所以肯定是有循环的,但如果一循环,它们没有共同的父容器,怎么渲染呢,也想过表格里面套表格,但那样,样式又实现不了我要的效果。
<table>
<thead>
<tr>
<th rowspan="2">id</th>
<th rowspan="2">任务名称</th>
<th rowspan="2">负责人</th>
<th colspan="4" id="tableYear">2023-8</th>
</tr>
<tr id="tableDay">
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
</tr>
</thead>
</table>
第一个难点
日期渲染,因为我这表头是动态的,所以要复杂一些,有天数,月数,季度和年度显示的
点击日视图,就是按天去显示任务,月视图就是按月去显示任务,季视图和年视图同理,
比如说日视图,
先获取当前日期,年月日,然后是想渲染前后几年的
var currentDate=new Date;//当前日期
var currentYear = currentDate.getFullYear();//当前年份
var yearRange = 1; // 前后1年
var startDate = currentYear - yearRange;//前1年
var endDate = currentYear + yearRange;//后1年
var today = currentDate.getDate(); // 获取今天是几号
var currentYear = currentDate.getFullYear();//年
var currentMonth = currentDate.getMonth();//月
var displayedYears = {}; // 用于记录已显示的年份
开始渲染第一排是年月,一年的月份是固定的,所以都是可以写死的,这里有要注意一下的是,就是年月的宽度,因为要根据当月有多少天去计算宽度,所以我要知道,这一年这一月是多少天然后乘以40 下面是相关代码
for (var year = startDate; year <= endDate; year++) {
for (var month = 0; month < 12; month++) {
var lastDay = new Date(year, month + 1, 0).getDate();
var monthElement = $("<p>" + (month + 1) + "月: </p>"); // 创建表示月份的 <p> 元素
for (var day = 1; day <= lastDay; day++) {
dateRange.push(new Date(year, month, day));
}
// 在 .tableYear 中添加年份和月份信息
var yearMonthStr = year + "-" + (month + 1 < 10 ? "0" : "") + (month + 1);
var width = (lastDay * 40)-1 + "px"; // 计算宽度
$(".tableYear").append($("<p class='Gantt-table_header' style='width: " + width + "'>" + yearMonthStr + "</p>"));
}
}
渲染完成,年月以后就是日,我这里也做了一些小的显示,比如说周末深颜色表示,今天也深颜色表示,并且视图要显示在当前,而不是在2022-1-1号这里。
天数渲染
for (var i = 0; i < dateRange.length; i++) {
var currentDate = dateRange[i];
var dayNumber = currentDate.getDay(); // 获取星期几 (0 = 星期日, 1 = 星期一, ...)
var isWeekend = dayNumber === 0 || dayNumber === 6;
var dayText = currentDate.getDate();//获取日
var dayYear = currentDate.getFullYear(); // 获取年份
var dayMonth = currentDate.getMonth(); // 获取月份(注意:月份是从 0 到 11,0 表示一月,11 表示十二月)
var tableCell = $("<p>" + dayText + "</p>");
if (isWeekend) {
tableCell = $("<p class='Gantt-table_weekend'>" + dayText + "</p>");
}
//获取当前时间的年月日,与循环出来的年月日进行循环匹配,
if (dayText === today && dayYear === currentYear && dayMonth === currentMonth ) {
tableCell.addClass("today");
}
$(".tableDay").append(tableCell);
}
视口显示代码
// 将视口滚动到今天所在的位置
var todayElement = $(".today");
if (todayElement.length > 0) {
var viewportHeight = window.innerHeight || document.documentElement.clientHeight;
var elementHeight = todayElement[0].clientHeight;
var offset = (viewportHeight - elementHeight) / 2;
//平滑滚动参数 smooth auto不滚动
todayElement[0].scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
}
第二个难点
如果不用表格,怎么去实现这个网格呢,刚开始是想着去渲染,表格对应有多少天,就渲染有多少个p标签,但一想这怎么行,如果任务很多,一条任务就要渲染上千个p标签,太浪费资源了,任务一多,那不直接挂壁。
后面在css里面找到了解决办法
background: repeating-linear-gradient(to right, rgb(221, 221, 221), rgb(221, 221, 221)
1px, transparent 1px, transparent 40px) 0% 0% / 40px 100%;
ChatGPT是这样解释的
然后完美解决问题,不用渲染这么多,一个任务一个div就可以了。
第三个难点
甘特图的核心,那个柱状图的东西。
柱状图渲染,比如说我提了一个任务,是2023年8月20号开始的,然后到2023年8月25号要完成,那这样就只有五天时间,那渲染肯定是从2023年8月20号开始的,然后到8月25号结束。
我的思路是这样的,显示的宽度是五天,然后一天的宽度是40px,那么这个任务的总宽度就是200px,然后定位,获取到这个任务开始的时候,我这里显示是前后一年的也就是2022-1-1号开始的,然后相减,知道中间相差了多少天,然后再乘以40,得到left的距离,就实现了这个效果。后面的描述也是类似的效果,这样甘特图差不多就完成了。下面是日期相减代码
function getOffsetDays(startDate, endDate){
var startDateArr = startDate.split("-");
var checkStartDate = new Date();
checkStartDate.setFullYear(startDateArr[0], startDateArr[1], startDateArr[2]);
var endDateArr = endDate.split("-");
var checkEndDate = new Date();
checkEndDate.setFullYear(endDateArr[0], endDateArr[1], endDateArr[2]);
var days = (checkEndDate.getTime() - checkStartDate.getTime())/ 3600000 / 24;
if(startDateArr[0]!=endDateArr[0]){
flag = true;
}
return days;
}
结语
虽然三言两语就讲解完了,但其中还是有很多逻辑的问题,我只是讲解了一下我的一个实现的思路,居体代码请看这。
以上就是本篇文章的全部内容,希望对大家的学习有所帮助,以上就是关于无插件实现一个好看的甘特图的详细介绍,如果有什么问题,也希望各位大佬们能提出宝贵的意见。当然也有很多需要优化的地方,我这只是给了一个思路,你们可以去实现很多的功能。
来源:juejin.cn/post/7272174836336132132
2024年,为啥我不建议应届生再去互联网?
最近快过年了,和还留在成都的一些研究生同学吃了顿饭,其中博士姐姐因为今年刚刚毕业,所以在饭局里面还跟我们谈了一下今年的师弟师妹们的去向。
她说今年虽然就业挺难的,但是师弟师妹们的工作还都挺好的,有去成飞的,有去选调的还有去了一些大国企研究所的。然后我就问今年没有去互联网的吗?她说有哇,有学弟去了美团,钱还开得挺多的,有学弟去了个独角兽做算法,但是也就这两个人去了互联网相关的了。
其实听到这个我还是蛮感慨的,在我毕业的时候互联网还是如日中天,大多数计算机毕业的孩子首选的就是去互联网狠狠的大赚一笔。短短3年间,去互联网的应届生就屈指可数了,一方面是这两年互联网大厂缩招严重,进互联网没有我们当年那么容易。另一方面是,在大环境不容乐观的今天,以及互联网增长见顶的背景下,互联网的工作其实已经不是应届生的首选工作了。
实际上,即使在今年你能过千军万马杀出重围拿到互联网的offer,作为一个过来人我也不是很建议你再去趟互联网这趟浑水。因为,作为一个新人在一个注定下行的行业当中,你可能搭上的不是通往财富自由的快车道,很快你需要考虑的可能就是你还能不能保住你手头的这份工作的问题。
说一个老生常谈的事情,互联网的增长确实见底了,阿里、腾讯、网易的股票最近狂跌,阿里都跌回2014年了,只有抖音还依然坚挺一些但是依然看不到未来成长的空间。从2014年到2024年,正好十年的时间,互联网员工们加班加点996,熬夜爆肝的奋斗,最终的结果尽然是回到了原点。
其实,这个事情也并不奇怪,这些互联网大厂只是坐在电梯里面的人,他们都觉得自己能够取得成功是因为自己在电梯里面做俯卧撑。实际上,跟你在电梯上做啥没有关系,你之所以能够成功只是因为你恰好赶上了这班电梯而已,跟你在里面睡觉还是瞎折腾关系都不大。如今风停了,电梯开始往下走了,作为个体你非要去搭上这个末班车并且期待在踩在早就已经上电梯的这群人的头上的话,那么我只能跟你说,祝你好运了。
其实,作为一名应届生的时候我对职场也没有清醒的认识,以为职场上的同事和学校的同学一样大家和和气气不争不抢的。但是,正是抱着这样的心态我入职了互联网之后的短短一年时间内,才深刻感受到了社会的毒打和职场真实的样貌。所以,我不知道在学校的应届生们有没有做好准备在互联网面对全方位的竞争,这种竞争不仅仅是技术,不仅仅是加班,更是向上管理和领导处理好关系。和国企、外企、体制内不一样,互联网的大多数公司是有强制末尾淘汰的,有些公司甚至连新人保护期都没有,那么你觉得你作为一个活蹦乱跳的应届生,这个名额是老油条扛呢还是你呢?
另外,以前的人扎堆朝互联网冲是因为真的有财富自由的机会的,那时候啥app都没有,张小龙找几个应届生关小黑屋都能写出未来的国民级app微信。16年的字节也还是个小公司,那时候往互联网里面冲的话搞不好真的可以一年能够赚到别人一辈子赚不到的钱,所以去互联网真是一点儿问题都没有。你那时候不去互联网我都会拿着鞭子抽你,劝你上进一点儿!但是都2024年了,市场永远比个人知道一个方向的未来,还是那句话你想创业互联网都拉不到风投的年代,你还能奢望能够实现财富自由吗?
The End
其实作为一名程序员还是挺享受写有趣代码的过程的,也希望做一点儿东西能够被大家认可,所以我劝退互联网但是并不是劝退计算机。
即使是Chatgpt大行其道的今天,我也不认为未来某一天机器能够真正意义上取代程序员,要取代也是从另外一个维度上取代,比如说根据需求直接生成机器码而不是生成代码的这种形式。虽然互联网是一片红海,但是像新的技术VR、物联网、工业软件、芯片和智能机器人等行业,在我们国家还是蕴含着无限机会的。但是,我并不认为去到我上面所说的这些行业工资收入上能够超过现在的互联网大厂给出的工资,我的意思真的有想法的人可以尝试在这些领域去找到自己的一席之地,尤其是在校学生。
你去卷一个注定下山的行业无论它钱给多少都是毫无意义的,因为入职就可能就是你职业生涯的巅峰。相比起来,我觉得华子未来比这些靠着广告赚钱的公司都更有前景,因为是真的有一些核心技术在的。
所以,选择一个还没有走过巅峰的行业,提前布局才是更有未来的职业选择。
来源:juejin.cn/post/7327447632111419443
每天都很煎熬,领导派的活太难,真的想跑路了
人在江湖身不由己,无论是领导的亲信还是团队的边缘,都可能遇到这种情况———不得不干一件特别难以推进的事情,茫然无措,不知如何推进。每天陷入焦虑和自我怀疑中……
这种事情一般有一些共同特点。
- 结果和目标极其模糊。
- 需要协调其他团队干活但是对方很不配合。
- 领导也不知道怎么干
领导往往是拍脑袋提想法,他们也不知道具体如何执行。反过来说,如果领导明确知道怎么做,能亲自指导技术方案、亲自解决关键问题,那问题就好办了,只要跟着领导冲锋陷阵就好了,就不存在烦恼了。
遇到这种棘手的事情,如果自己被夹在中间,真的非常难受啊!
今天重点聊聊领导拍脑袋、心血来潮想做的那些大事 如果让你摊上了,你该怎么做!
1、提高警惕!逆风局翻盘难!
互联网行业目前处于稳定发展期,很少会出现突然迅猛增长的业务,也很少有公司能够迅速崛起。这是整个行业的大背景。因此,我们应该对任何不确定或模糊的目标表示怀疑,因为它们更有可能成为我们的绊脚石,而不是机遇。即使在王者荣耀这样的游戏里,要逆风翻盘也很困难,更何况在工作中呢。
当领导提出一个棘手的问题时,我们应立刻警惕,这可能不是一个好的机会,而是一个陷阱。我们不应该被领导画的饼所迷惑,而是要冷静客观地思考。哪些目标和结果是难以达到的,这些目标和结果就是领导给我们画的大饼!
领导给出任务后,我们就要努力完成。通常情况下,他们会给我们一大堆任务,需要我们确认各种事情。简而言之,他们只是有个想法,而调研报告和具体实施方案就需要我们去做。
如果领导是一位优秀而谦虚的人,通常在我们完成调研后,会根据调研结果来判断这个想法是否可行。如果不可行,他们会立即放弃,而我们也不会有什么损失。
但是,一旦领导有了一个想法,肯定是希望我们来完成的,即便我们在调研后认为不可行,大多数情况下,他们也不会接受我们的结论!因此,我们的调研工作必须极度认真,如果我们认为不可行,就要清楚地阐述不可行的理由,要非常充分。
这是我们第一次逃离的机会,我们必须重视这次机会,并抓住机会。
2、积极想办法退出
对于这种模糊不靠谱的事情,能避开就避开,不要犹豫。因为这种事情往往占用大量时间,但很难取得显著的成果。对于这种时间周期长、收益低、风险高的事情,最好保持距离。
你还需要忍受巨大的机会成本
在你长期投入这种事情的过程中,如果团队接到更好的项目和需求,那肯定不会考虑你。你只能羡慕别人的机会。
因此,如果可以撤退的话,最好离开这种费力不讨好的活远远的!
子曰:吾日三省吾身,这事能不能不干,这事能不能晚点干,这事能不能推给别人干。
如何摆脱这件事呢?
2.1 借助更高优事情插入,及时抽身
例如,突然出现了一件更为紧急的事情,这就是脱身的机会。与此同时,我们也可以为领导保留一些颜面,因为随着工作的进展,领导也会意识到这件事情的意义不大,很难取得实质成果。但是,如果我们一开始就表示不再继续做这件事,那么领导可能会觉得自己的判断出了问题,失去面子。所以,我们可以寻找一个时机,给领导下台阶。
或者,突然出现了一个需求,与我们目前的重构方案存在冲突。这是一个很好的借口。重构方案和未来产品规划产生了冲突,我们应优先满足产品规划和需求。重构方案需要后续再次评估,以找到更好的解决方案。
2.2 自己规划更重要的事情,并说服领导
当你对系统优化没有想法时,不要怪领导给你找事干。
如果领导有一个系统重构的计划和目标需要你执行,但是你不想干,或者你认为这件事不靠谱。那么你可以思考一个更可行、更有效、更能带来收益的重构方案,并与领导进行汇报。如果领导认为你的计划更加重要且更具可行性,那他可能会放弃自己的想法。
这就是主动转被动的策略。这时你的技术能力将接受考验,你能提出一个更优秀的系统重构方向吗?你能提出一个更佳的系统建设方向吗?
2.3 选择更好的时机做这件事
如果领导让你去做技术重构,而这件事的优先级不如产品需求高,上下游团队也不愿意配合你,而且领导给你的人力和时间资源也不够充裕,你应该怎么办呢?可以考虑与产品需求一起进行技术重构。也就是说,边开发需求,边进行技术重构。这样做有以下好处:可以借助于产品的力量,很自然地协调上下游团队与你一同进行重构。同时也能推动测试同事进行更全面的测试。在资源上遇到的问题,也可以让产品帮助解决。
所以,技术重构最好和产品需求结合起来进行。如果技术重构规模庞大,记得一定要分阶段进行,避免因技术重构导致产品需求延期哦。
2.4 坦诚自己能力不足,暂时无法完成这件事,以后再干行不行
可以考虑向领导坦然承认自己的能力还不足以立即执行这项任务,因此提出先缓一缓,先熟悉一下这个系统的建议。我可以多做一些需求,以此来熟悉系统,然后再进行重构。
我曾经接手一个系统,领导分配给我一个非常复杂的技术重构任务。当时我并没有足够聪明,没有拒绝,而是勉强去做,结果非常不理想,还导致了线上P0级别的事故发生!
新领导告诉我,"先想清楚如何实施,再去行动。盲目地勉强上阵只会带来糟糕的结果。当你对一个系统不熟悉的时候,绝对不能尝试对其进行重构。"
先熟悉系统至少三个月到半年。再谈重构系统!
2.5 拖字诀,拖到领导不想干这件事!
拖到领导不想干的时候,就万事大吉了。
注意这是最消极的策略,运气好,拖着拖着就不用干了。但如果运气不佳,拖延只会让任务在时间上更加紧迫,而且还会招致领导的不满。
使用拖延策略很可能得罪领导,给他们留下不良的印象。
因此,在使用此策略时应谨慎行事!
2.6 退出时毫不犹豫,不要惋惜沉默成本
如果有撤退的机会,一定不要犹豫,不要为自己付出的投入感到遗憾,不要勉强继续前进,也不必试图得到明确的结果。错误的决策只会带来错误的结果。一定要及时止损。
因为我曾经犯过类似的错误,本来有机会撤退,但是考虑到已经付出了很多,想要坚持下去。幸好有一位同事更加冷静,及时制止了我。事后我反思,庆幸及时撤退,否则后果真的不敢想象啊。
3、适当焦虑
每个人都喜欢做确定性的事情,面对不确定的事情每个人都会感到焦虑。为此可能你每天都很焦虑,甚至开始对工作和与领导见面感到厌恶。之所以这个事情让你感到不适,是因为它要求你跳出舒适区。
但是,请记住,适度的焦虑是正常的。告诉自己,这并没有什么大不了的。即使做得不好,顶多被领导责备一下而已。不值得让生活充满焦虑,最重要的是保持身心健康和快乐。
当你沉浸在焦虑中时,可能会对工作和领导感到厌烦。这样一来,你可能会对和领导沟通感到反感。这种情况是可怕的,因为你需要不断和领导沟通才能了解他真正的意图。如果失去了沟通,这个事情肯定不会有好的结果。
因此,一定要保持适度的焦虑。
3.1 沟通放在第一位
面对模糊的目标和结果,你需要反复和领导沟通,逐步确认他的意图。或者在沟通中,让领导他自己逐渐确定的自己的意图。在这方面有几个技巧~
3.2 直接去工位找他
如果在线上沟通,领导回复可能慢,可能沟通不通畅。单独约会议沟通,往往领导比较忙,没空参加。所以有问题可以直接去工位找他,随时找他沟通问题。提高效率
3.3 没听懂的话让领导说清楚
平常时候领导没说清楚,无所谓,影响不大。例如普通的产品需求,领导说的不清楚没关系,找产品问清楚就行。
面对目标不明确的项目,领导的意图就十分重要。因为你除了问领导,问其他人没用。领导就是需求的提出方,你不问领导你问谁。 在这种情况下,没听懂的事情必须要多问一嘴。把领导模糊的话问清楚。
不要怕啰嗦,也不要自己瞎揣摩领导的意图。每个人的想法都不同,瞎猜没用。
3.4 放低姿态
如果领导和你说这件事不用干了,你肯定拍手叫好。很多烦恼,领导一句话,就能帮你摆平!
放低姿态就是沟通时候,该叫苦叫苦,该求助就求助,别把自己当成超人,领导提啥要求都不打折扣的行为完全没必要。可以和领导叫叫苦,可以活跃气氛,让领导多给自己点资源,包括人和时间。
说白了,就是和 领导 “撒娇”。这方面女生比较有优势,男生可能拉不下脸。之前的公司,我真见识过,事情太多,干不完,希望领导给加人,但被领导拒绝。 然后她就哭了,最后还真管用!是个女同事。
男孩子想想其他办法撒娇吧。评论区留下你们的办法!
3.5 维护几个和领导的日常话题
平常如果有机会和领导闲聊天,一定不要社交恐惧啊! 闲聊天很能提升双方的信任关系,可以多想想几个话题。例如车、孩子、周末干啥、去哪旅游了等等。
提升了信任关系,容易在工作中和领导更加融洽。说白了就是等你需要帮忙的时候,领导会多卖你人情!
4 积极想替代方案————当领导提的想法不合理时
积极寻求替代方案,不要被领导的思路局限!引导众人朝着正确的方向前进!
不同领导的水平和对技术问题的认知不尽相同,他们注重整体大局,而员工更注重细节。这种差异导致了宏观和微观层面之间存在信息不对称,再加上个人经验、路径依赖导致的个人偏见,使得领导的想法不一定正确,也不一定能够顺利实施。
就我个人的经历来说,领导要求我进行一次技术重构。由于我对这个项目还不够熟悉,所以我完全按照领导的方案去操作,没有怀疑过。事后回顾,发现这个方案过于繁重,其实只需要调整前端接口就能解决问题,但最终我们却对底层数据库存储、业务代码和接口交互方式进行了全面改变。
最终收益并不高,反而导致了一个严重的故障。既没有获得功劳,也没有得到应有的认可。
事后反思,我意识到我不应该盲目按照领导的方案去执行,而是应该怀着质疑和批判的态度去思考他的方案。多寻求几个备选方案,进行横向比较,找到成本最低、实施最简单的方案。
4.1 汇报材料高大上,实现方案短平快
私底下,可以对老板坦诚这件事,就是没什么搞头。但是对外文章要写得高大上!
技术方案要高大上,实现方案要短平快。
面对不确定的目标、面对不好完成的任务,要适当吹牛逼和画饼。汇报文档可以和实现方案有出入。
模糊的目标,往往难以执行和完成,技术方案越复杂,越容易出问题。本来就没什么收益,还引出一堆线上问题,只能当项目失败的背锅侠,得不偿失。
一定要想办法,把实现方案做的简单。这样有3个好处;
- 降低实现难度,减少上线风险。
- 缩短开发周期,尽快摆脱这个项目。
- 把更多的时间放在汇报材料上。代码没人看!!!
程序员一般情况下习惯于实话实说,如果说假话,一定是被人逼得。
不会写文档?# 写文档不用发愁,1000个互联网常用词汇送给你
不会写技术方案?# 不会画图? 17 张图教你写好技术方案!
5、申请专门的团队攻克难关!
例如重构系统涉及到上下游系统,一个人搞不定的!要向领导寻求帮助,让上下游同事一起干这件事。
让熟悉系统的人跟自己一起做,拉更多的人入伙!多个人一起承担重任! 这种组织上的安排,只能由领导出面解决。
假如别的同事经常打扰你,总让你确认这件事,确认那件事,总让你帮忙梳理文档,你愿意配合吗? 每个人都很忙,没人愿意长期给你干活。
让领导帮忙成立重构小组!然后你可以给每个人都分派任务,比自己独自硬扛,成功概率大很多。
虽然重构的目标不明确,但你可以尝试明确每个人的责任,设置短期的里程碑。例如前三天梳理整理资料,每天开早会, push大家干活。(这样很招人恨!没办法,领导卷的)
5.1 寻求合作的最大公约数
重大项目往往需要多个团队同时配合,即便你申请了专门的小组跟进这件事,但是别人可能出工不出力!
他们不配合的原因在于:不光没有收益,付出还很多。成本和收益不对等,人家不愿意很正常。保持平常心!不要带着脾气看待这件事!
略微想一下就明白,既然你觉得这件事风险高、收益低,难道其他人看不出来吗?
作为项目的负责人推动事情更加困难。当别人不配合时,除了把矛盾上升到上层领导外,还有哪些更好的办法呢?
- 平时多和相关同学打好关系。平时奶茶咖啡多送点,吃别人嘴短,到时候求人时候很管事的。
- 调动对方的积极性!例如重构系统需要人家配合,但是这件事对他们又没有收益。可以和他们一起头脑风暴,想一下对方系统可以做哪些重构。当双方一拍即合,各取所需时,才能合作融洽。双赢的合作,才能顺利。
- 多作妥协。上下游系统的交互边界很难划分,如果交互存在争议,可以适当让步,换取对方的积极合作。完成胜于完美!
总之,涉及多个团队合作时,除了依靠上层领导的强硬干预之外,还要想一些合作共赢的方案!
6、争取更多的资源支持
没有完不成的事情,只要资源充裕,任何事情都是有希望的。当你面临棘手的问题时,除了打起12分的精气神,还要多想想和领导申请资源啊!
最重要的包括人力资源、时间资源。如果空口白牙就要人,可能比较困难。
这需要你在调研阶段深入思考,预想到系统的挑战点,把任务细分,越细越好,然后拿着排期表找领导,要人、要时间。
如果人和时间都不给!可以多试几次,软磨硬泡也是好办法!
此外还有别的办法,例如 ”偷工减料"。你可以和领导沟通,方案中哪些内容不重要,是否可以砍掉。”既然你不给人,砍掉不重要的部分,减少工作量,总可以吧"
除此之外,还可以考虑分期做。信用卡可以分期付款,技术重构当然也可以分期优化!
7、能分期就分期
对于技术重构类工作,一定要想办法分期重构,不要一次性只求大而全!
- 越复杂的技术方案越容易出问题!
- 越长的开发周期越容易出问题!
- 越想一次性完成,越容易忙中出错!
分期的好处自不必说,在设计方案时一定要想如何分期完成。
如果对一个系统不熟悉,建议分期方案 先易后难!先做简单的,逐渐地你对系统会有更深入的理解!
如果对一个系统很熟悉,可以考虑先难后易。先把最困难的完成!后面会轻松很多!
但是我还是建议庞大的重构工作,先易后难!先做简单的,拖着拖着,也许就不需要重构了呢!
8、即便没有功劳但是要收获苦劳
当一件事干成很难的时候,要想办法把损失降到最低。一定要想着先保护自己!别逞能!
工作几年的朋友应该知道,不是所有的项目都能成功!甚至大部分项目在商业上是失败的!做不成一件事很正常!
如果一件事很难办成,功劳就不要想了。但是可以赚一份苦劳。
这要求你能把自己的困难说给领导,例如其他团队不配合!你可以一直和领导反馈,并寻求领导的帮助。
日常工作的内容也要有文档留存。工作以周报形式单独和领导汇报!要让领导知道你每周的进展,向领导传递一个事实:“每一周你都努力地在做事,并且也都及时汇报了,日后干不成,可别只怪我一人啊!”
接到一个烫手山芋,处理起来很难~ 斗智斗勇,所以能躲开还是躲开啊!
9、转变观念:放弃责任心,领导关注的内容重点完成
出于责任心的角度,我们可能认为领导提出的方案并不正确,甚至认为领导给自己派的工作完全没有意义。
你可能认为领导的Idea 不切合实际!
出于责任心,你有你的想法,你有你的原则!你认为系统这样重构更适合!但那又怎样,除非你有足够的理由说服领导,否则改变不了什么。
站在更高的位置能看的更远,一般领导都会争取团队利益最大化。虽然看起来不切实际,但是努力拼一拼,也许能给团队带来更大的利益。这可能是领导的想法!说白了,就是领导想让团队多去冲锋陷阵,多把一些不可能变成可能!
和领导保持节奏,领导更关注哪件事,就尽力把这件事做好! 放弃自己所谓的“责任心”。
10、挑战、机遇、风险并存。
在互联网稳定期,各行各业都在内卷,公司内部更是在内卷!
在没有巨大增量的团队和公司里,靠内卷出成绩是很困难的事情。有时候真的很绝望,每一分钟都想躺平 。
像这种目标不明确、执行方案不明确、结果不明确、需要协调其他团队干活的难事越来越多!风险高、低收益的事情谁都不想干!
但是一旦能做成,对于个人也是极大地锻炼。所以大家不要一味地悲观,遇到这种棘手的事情,多和领导沟通,多想想更优的解决方案。也许能走出一条捷径,取得极大的成果~
来源:juejin.cn/post/7290469741867565092
Easy-Es:像mybatis-plus一样,轻松操作ES
0. 引言
es的java客户端不太友好的语法一直饱受诟病,书写一个查询语句可能需要书写一大串的代码,如果能像mybatis--plus一样,支持比较灵活方便的语句生成器那就好了。
于是为elasticsearch而生的ORM框架Easy-Es诞生了,使用及其方便快捷,今天我们就一起来学习easy-es,对比看看原生java-client方便之处在哪儿。
1. Easy-Es简介
Easy-Es是以elasticsearch官方提供的RestHighLevelClient
为基础,而开发的一款针对es的ORM框架,类似于es版的mybatis-plus,可以让开发者无需掌握es复杂的DSL语句,只要会mysql语法即可使用es,快速实现es客户端语法
2. Easy-Es使用
1、引入依赖
<!-- 引入easy-es最新版本的依赖-->
<dependency>
<groupId>org.dromara.easy-es</groupId>
<artifactId>easy-es-boot-starter</artifactId>
<version>2.0.0-beta3</version>
</dependency>
<!-- 排除springboot中内置的es依赖,以防和easy-es中的依赖冲突-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</exclusion>
<exclusion>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.14.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.14.0</version>
</dependency>
2、添加配置项,这里只配置了几个基本的配置项,更多配置可参考官网文档:easy-es 配置介绍
easy-es:
# es地址、账号密码
address: 192.168.244.11:9200
username: elastic
password: elastic
3、在启动类中添加es mapper文件的扫描路径
@EsMapperScan("com.example.easyesdemo.mapper")
4、创建实体类,通过@IndexName
注解申明索引名称及分片数, @IndexField
注解申明字段名、数据类型、分词器等,更多介绍参考官方文档:essy-es 注解介绍
@IndexName(value = "user_easy_es")
@Data
public class UserEasyEs {
@IndexId(type = IdType.CUSTOMIZE)
private Long id;
private String name;
private Integer age;
private Integer sex;
@IndexField(fieldType = FieldType.TEXT, analyzer = Analyzer.IK_SMART, searchAnalyzer = Analyzer.IK_SMART)
private String address;
@IndexField(fieldType = FieldType.DATE, dateFormat = "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis")
private Date createTime;
private String createUser;
}
5、创建mapper类,继承BaseEsMapper
类,注意这里的mapper一定要创建到第3步中设置的mapper扫描路径下com.example.easyesdemo.mapper
public interface UserEsMapper extends BaseEsMapper<UserEasyEs> {
}
6、创建controller,书写创建索引、新增、修改、查询的接口
@RestController
@RequestMapping("es")
@AllArgsConstructor
public class UserEsController {
private final UserEsMapper userEsMapper;
/**
* 创建索引
* @return
*/
@GetMapping("create")
public Boolean createIndex(){
return userEsMapper.createIndex();
}
@GetMapping("save")
public Integer save(Long id){
UserEasyEs user = new UserEasyEs();
user.setId(id);
user.setName("用户"+id);
user.setAddress("江苏省无锡市滨湖区");
user.setAge(30);
user.setSex(1);
user.setCreateUser("admin");
user.setCreateTime(new Date());
Long count = userEsMapper.selectCount(EsWrappers.lambdaQuery(UserEasyEs.class).eq(UserEasyEs::getId, id));
if(count > 0){
return userEsMapper.updateById(user);
}else{
return userEsMapper.insert(user);
}
}
@GetMapping("search")
public List<UserEasyEs> search(String name, String address){
List<UserEasyEs> userEasyEs = userEsMapper.selectList(
EsWrappers.lambdaQuery(UserEasyEs.class)
.eq(UserEasyEs::getName, name)
.match(UserEasyEs::getAddress, address)
);
return userEasyEs;
}
}
7、分别调用几个接口
- 创建索引
kibana中查询索引,发现创建成功
- 新增接口
这里新增了4笔
数据新增成功
- 数据查询
如上便是针对easy-es的简单使用,这里的用法都与mp类似,上手相当简单,不用再写那些复杂的DSL语句了
3. 拓展介绍
- 条件构造器
上述演示,我们构造查询条件时,使用了
EsWrappers
来构造条件,用法与mp及其类型,大家根据提示就可以推导出方法如何书写,更详细的使用说明可以查看官方文档:easy-es 条件构造器介绍
- 索引托管
如果想要自动根据创建的es实体类来创建对应的索引,那么只需要调整索引的托管模式为非手动模式即可,因为这里我不需要自动同步数据,所以选择
非平滑模式
easy-es:
global-config:
process_index_mode: not_smoothly
其中三种模式的区别为:
平滑模式:smoothly,索引的创建、数据更新迁移等都由easy-es自动完成
非平滑模式:not_smoothly,索引自动创建,但不会自动迁移数据
手动模式:manual,全部操作由用户手动完成,默认模式
- 数据同步
如果数据源是来自mysql, 那么建议使用canal来进行同步,canal的使用可在我主页搜索。
其次还有DataX, Logstash等同步工具,当然你也可以使用easy-es提供的CRUD接口,来手动同步数据
- 日志打印
通过开启日志,可以在控制台打印执行的DSL语句,更加方便我们在开发阶段进行问题排查
logging:
level:
tracer: trace # 开启trace级别日志,在开发时可以开启此配置,则控制台可以打印es全部请求信息及DSL语句,为了避免重复,开启此项配置后,可以将EE的print-dsl设置为false.
- 聚合查询
easy-es实现的聚合查询,只要是针对gr0up by这类聚合,也就是es中的Terms aggregation
,以及最大值、最小值、平均值、求和,而对于其他类型的聚合,还在不断更新中,但这里大家也需要了解,es的聚合和mysql的聚合完全是不一样的维度和复杂度,es支持非常多的聚合查询,所以其他类型的实现还需要借助RestHighLevelClient
来实现
我们利用easy-es来实现下之前书写的聚合案例
@RestController
@AllArgsConstructor
@RequestMapping("order")
public class OrderEsController {
private final OrderTestEsMapper orderEsMapper;
@GetMapping("search")
public String search(){
SearchResponse search = orderEsMapper.search(EsWrappers.lambdaQuery(OrderTest.class).groupBy(OrderTest::getStatus));
// 包装查询结果
Aggregations aggregations = search.getAggregations();
Terms terms = (Terms)aggregations.asList().get(0);
List<? extends Terms.Bucket> buckets = terms.getBuckets();
HashMap<String,Long> statusRes = new HashMap<>();
buckets.forEach(bucket -> {
statusRes.put(bucket.getKeyAsString(),bucket.getDocCount());
});
System.out.println("---聚合结果---");
System.out.println(statusRes);
return statusRes.toString();
}
}
可以看到实际上的查询语句就一行,而其他的都是对返回结果的封装,因为es本身返回的数据是封装到嵌套的对象中的,所以我们需要对其进行包装
对比原始的查询语句,其易用性上的提升还是很明显的
4. 总结
至此对easy-es的介绍就结束了,可以看到如果是针对es实现CRUD上,easy-es表现出非常好的便捷性,而在复杂的聚合查询中,仍然还有进步空间,目前还需要借助RestHighLevelClient
,但easy-es的出现,为未来提供更好用的ES ORM框架,提供了希望和方向
文中演示代码见:gitee.com/wuhanxue/wu…
来源:juejin.cn/post/7271896547594682428
一次低端机 WebView 白屏的兼容之路
问题
项目:Vite4 + Vue3,APP WebView 项目
页面在 OPPO A5 手机上打不开,页面空白。
最开始是客户端在看,然后发现一个警告,大概也因为没看出什么问题,给到 Web 前端。
相关背景
为了方便描述过程的行为,先做一些相关背景的介绍。知道这些背景才能更好的了解问题的复杂。这些在解决问题的过程中始终是干扰因素,在反复调试试错的过程中才梳理总结出来,这里把它们列出来。
使用测试 App,其中有两个入口,一个是本地调试,这个地址是写在 App 里的,也就是要修改这个地址需要客户端重新出包;一个是项目的测试地址,这个地址测试可以进行配置。
修改客户端,重新出包,是很麻烦的,所以尽量避免。
项目配置了 HTTPS 支持,所以开发地址是 https 开头。但是也能启动 http 的地址。
关于项目支持 HTTPS,可以参考之前写的这篇:juejin.cn/post/732783…。
之所以要支持 HTTPS 是因为 iOS WebView 只支持 HTTPS 的地址。
而安卓 App WebView 却需要 HTTP 打开,原因是安卓 WebView 反馈不支持本地地址 HTTPS 的方式。但是在本机上用 MuMu 模拟器打开 App,是能打开本地 HTTPS 地址的,之前也尝试过给安卓手机安装根证书,但是还是不行,得到以上反馈。
所以我本地开发安卓在电脑 MuMu 上调试,iOS 可以用手机调试,如果要安卓真机本地调试,需要去掉本地 HTTPS 的支持,使用 HTTP 的地址(自然,iOS 本地调试就不能同时进行了)
快速尝试
拿到问题之后,快速进行问题验证,在 OPPO A5 上,进入 APP 中,打开本地调试,用 HTTP 的方式。发现确实白屏,查看了客户端相关的日志,发现一个警告:
[INFO:CONSOLE(9)] "The key "viewport-fit" is not recognized and ignored.", source: xxx
于是修改 viewport-fit,发现并没有区别,这只是一个提示,应该没有影响。
于是采用最简代码法,排除法,用最简单的页面进行测试,看是否能正常打开,确定 WebView 没有问题。直到确定 script type="module"
引入的 main.ts
的代码没有起作用。
于是,基本上确定 Vite 的开发模式在 OPPO A5 WebView 中有问题。那不支持 ESM,就是兼容性问题?
快速查看解决方案,引入官方插件:@vitejs/plugin-legacy。但是怎么测试呢?要验证兼容性是否生效,只能验证打包构建后的代码,而不是通过本地调试进行测试。那只能发布到测试了,但这样岂不是要改一点就要发布一次,这是没办法进行的。但是第一次,还是发布一下看有没有生效。
不出意料,没那么容易解决!测试地址依然白屏。
如何调试
确定如何方便的调试是解决问题的必要条件。
几天后又开始看这个问题。
浏览器是否能打开页面?
首先在 App 中进行调试是比较麻烦的,需求启动 App,那么能否在浏览器中进行测试呢?很遗憾,期间用手机系统浏览器打开测试地址是正常的,后面打开本地地址也是正常的。所以浏览器和 App WebView 是有区别的。
启动本地服务查看构建后的页面
兼容插件只是解决打包后的构建产物,想要看打包后的效果,于是我想到将打包后的文件起一个 Web 服务,这样就可以打开打包后的页面 index.html,而且手机访问同一网络,扫码就可以打开这个页面。
找了 Chrome 插件 Web Server for Chrome,发现已经不能用了- 找了 VS code 插件 Live Server,服务启了,但是有个报错。
- 换用 http-server,启动服务 xxx:8080。正常打开页面,手机也能访问。
那么考虑我们的实际问题,如何在手机调试呢?将本地调试改成本地起的服务 xxx:8080,看 WebView 能否打开,这样每次修改、打包,生成新的打包后文件,刷新 WebView 就可以了。
但是前面说了,找 APP 出包很麻烦,改一个地址要出个包,费时。还有其他的办法吗?如果把本地启的服务端口改成 5173 不就不用改 App 了吗,可以直接用本地调试来进行测试,突然又想到本地调试的地址是 HTTPS,可是启动的本地服务好像没法改成 HTTPS。
通过测试地址增加本地调试入口
又想到 App 中的本地调试入口本应该做成一个公共页面,里面放上很多可能的入口。这样只需要 APP 改一次,之后想要什么入口,可以自己添加。改这个调试入口还是需要 App 改动,还是麻烦。我可以在项目中增加一个页面 debug.html(因为项目是多页面应用),这样我增加页面\增加调试入口,发布一下测试就生效了,在测试入口就能看到,这样更快。于是做了一个公共页面。
Vite preview
而且突然想到根本不需要自己起一个服务,Vite 项目,Vite preview 就是把打包后的页面启动服务。地址是:xxx:4173。
修改测试地址为本地预览
然而,OPPO A5 WebView 本来就是打不开我们的系统,那么 WebView 打不开测试地址自然也就没法打开我的本地预览了。但是,测试地址是可以配置的,所以为了快速调试,让测试配置了我的本地预览地址 xxx:4173。
这样,终于在 APP WebView 中打开了我本地预览的页面。
如何查看 App WebView 的日志
手机连接电脑,adb 日志:
看起来这几个报错是正常的,报错信息也说了:
vite: loading legacy chunks, syntax error above and the same error below should be ignored
但是页面没有加载,不知道 WebView 打开页面和页面加载之间发生了什么。
Vite 兼容插件的原理
这期间,反复详细理解原理,是否是插件的使用不对。
用一句话说就是,Vite 兼容插件让构建打包的产物多了传统版本的 chunk 和对应 ES 语言特性的 polyfill,来支持传统浏览器的运行。兼容版的 chunk 只会在传统浏览器中运行。它是如何做到的呢?
- 通过 script type="module" 中的代码,判断当前浏览器是否是现代浏览器。如果是,设置全局变量:window.__vite_is_modern_browser = true。判断的依据是:
- import.meta.url;
- import("_").catch(() => 1);
- async function* g() { }
- 通过 script type="module",如果是现代浏览器,直接退出;如果不是,加载兼容文件:
- 通过 script type="nomodule",加载兼容 polyfill 文件;
- 通过 script type="nomodule",加载兼容入口文件;
传统浏览器不执行 type="module" 的代码,执行 type="nomodule" 的代码。
现代浏览器执行 type="module" 的代码,不执行 type="nomodule" 的代码。
为什么需要 type="module" 的代码?这里是针对浏览器支持 ESM 却不支持以上 3 个语法的情况,仍然使用兼容模式。
详细可以看参考文章,以及查看打包构建产物。
除了知乎那篇文章,我几乎翻遍了搜到的 vite 兼容 空白 白屏 相关的文章,参考相关的配置。这个插件就是很常规的使用,几乎没有看到有任何特殊的配置或处理。就是生成了兼容的代码,低版本浏览器就能使用而已,似乎没人碰到过我的问题。
尝试解决
前面说了,用手机系统浏览器打开页面,竟然正常。怀疑是不是 WebView 的问题。
WebView 的内核版本
借了几个低端机型,几个安卓 5.x 6.x 的系统,结果手机浏览器都能正常打开。
打印 console.log(navigator.appVersion)
,WebView 中:
5.0 (Linux; Android 8.1.0; PBAT00 Build/OPM1.171019.026; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36 uniweb/ma75 uniweb-apk-version/0.6.0 uniweb-script-version/0.6.0 uniweb-channel/netease Unisdk/2.1 NetType/wifi os/android27 ngwebview/4.1 package_name/com.netease.sky udid/944046b939d510b1 webview_orbit/1.2(1)
而手机浏览器版本为 Chrome 90,其他手机有 Chrome70。总之,OPPO A5 WebView 的内核 Chrome 版本较低。
Vite 文档对于构建生产版本浏览器兼容性的介绍:
用于生产环境的构建包会假设目标浏览器支持现代 JavaScript 语法。默认情况下,Vite 的目标是能够 支持原生 ESM script 标签、支持原生 ESM 动态导入 和 import.meta 的浏览器
原生 ESM script 标签的支持:
原生 ESM 动态导入的支持:
import.meta 的支持:
所以,原来 Chrome 62 支持 ESM,但是不支持其他 2 个。通过日志,也可以知道不支持兼容插件尝试的 3 个语法,因为打印了那句警告,来自 module 的代码位置,window.__vite_is_modern_browser 不为 true。
从 Android 4.4 开始,系统 WebView 使用 Chrome 内核。
手机系统浏览器内核和系统 WebView 不一样,手机系统的 WebView 也可能不是安卓默认的。
兼容生效了吗?
但还是不知道低版本的浏览器兼容性是否生效,当前我们只能确定 Chrome 62 的 WebView 中兼容有问题,那是否在浏览器中就正常呢?或者更低的版本兼容是否生效?(毕竟不支持 ESM 的浏览器版本的代码执行又不一样)
target 配置不对?
target 配置的是目标浏览器,针对这些浏览器生成对应的兼容代码,期间我一直调整 target 的配置,使用 .broswerlistrc 文件配置,target 直接配置,参考不同的配置方案,确保包含了 Chrome 62。
又是如何调试?
想要下载安卓 Chrome 62 进行测试,但是搜了一圈也没找到。
后来想到不一定要手机浏览器进行测试,Chrome 也行。这里面有点思维上的转换,之前我测试只能通过 WebView 进行调试,因为浏览器上没有问题。现在确定了是浏览器 Chrome 版本的问题,那么我们还是可以通过 PC 浏览器进行测试。
于是范围更大一些,找到了 Chrome 的所有历史版本,不得不说,Chrome 提供的下载真是太有用了,对于测试兼容性非常有帮助! 而且我所担心的覆盖现有浏览器版本的问题完全不存在,下载之后直接运行。
安装 Chrome 62,打开页面,果然空白。终于在浏览器复现,确定就是兼容的问题,而不是 WebView 的问题。安装安卓版本的 Chrome 62,也同样复现。
下载 win 的 Chrome 62,虽然在 refs 里找到同版本的记录:xxx。但是没找到同版本的下载,不过也都是 62,应该没问题。下打开页面,打开控制台,和在 adb 中看到的报错一样,只是这里是红色的:
安装更低版本的 Chrome,同样复现,说明不支持 ESM 的兼容也出现问题。同时可以看到那句提示没有了:
过程当然也没那么顺利,下载 Chrome 的过程中,Chrome 62 直接可以运行,下载 Chrome 59 却没法打开。于是又下载了 Chrome 55,mini exe 文件,可以直接打开。
报错到底要不要处理?
通过 adb日志可以看到报错,也可以从打包后的代码看到:对于语法报错是可以忽略的,因为那是预期中的行为。可是之后的代码为什么没执行了呢?
回到现在的问题,这个报错不是不需要处理吗?但是加载了兼容的 js,页面却没有渲染元素。此时隐隐觉得报错可能还是要处理,至少可能最后一个报错有点问题?
但是这个报错实在难以查看,之前我把它当作和前两个报错一样的来源。现在只剩这个报错了,问题是这是打包压缩后的代码,完全不知道真正的问题是什么。
通过请教网友,做了一些尝试:
通过对插件配置:renderModernChunks: false,只生成兼容代码,依然报错。
通过修改 Vite 配置:build.minify: false 不压缩代码,尝试查看报错位置。新的报错:
升级 Vite。新的报错:
所以每次的报错都不一样,越来越奇怪。不过看起来似乎是同一个原因导致的。
在构建源码中调试
通过在构建后的源码中打印,其中 excute 函数中,有两个参数 exports module,但是在其中使用 module.meta 报错,说明其他文件在使用这个方法是并没有传参。看起来像是模块规范的问题(commonJS 和 ES Module)。
ChatGPT
在这期间,也在 ChatGPT 搜素方法:
就尝试了一下 format: 'es',顺便看到有个配置 compact: true
,好像也是压缩,就顺手改成 false,这样全部不要压缩,方便看报错。
结果竟然 OK 了,页面打开,没有报错!
是这个配置生效的吗?通过排除,发现竟然是 compact 的原因。这个配置不是 Vite 本身的,是 Vite 使用的 rollup 的配置:
果然是插件冲突的结果。
再搜素 execute
,已经没有带参数了:
再次感叹 Webpack 配置工程师
build.sourcemap
后来想到开启 sourcemap 来定位报错的原始文件位置,未开启:
开启 sourcemap:
如果在打包过程中对代码进行了混淆或压缩,可能会导致 Source Map 无法准确映射到原始代码位置。
这就完了?
中午去吃个饭,下午回来,本以为打包发布验证一下就完了,结果测试地址能打开页面了,却和我在电脑浏览器上看到的一样,没有正常加载页面。晴天霹雳!
说明一下:我们这个项目是 App Hybrid 应用,Web 前端和服务端的通信通过客户端中转,所以在非客户端环境是拿不到服务端的数据的,联调测试都是在 App 中进行的。
但为什么前面一直用浏览器测试呢?虽然数据获取不到,但是如果页面加载出来了,说明打包代码是没问题,所以在前面页面显示了背景图等元素,id="app"
中有了内容以后,就是兼容版本的 js 正常执行了。但是再说一次,真实的环境是 WebView,最终的结果还是要看 WebView。
目前在 App 中还是没有显示完整的网页加载过程,又陷入了困境。页面已经没有了报错(除了 Vite 那个可以被忽略的),难道生成的兼容 js 有问题? 如果是具体的兼容代码有问题,那整个项目的代码又如何排查呢?
虽然没有头绪,但是隐隐觉得应该再坚持一下。继续分析,如果已经显示了页面,兼容的 js 已经执行了,那么数据接口请求了吗?
于是联系服务端,查看服务器日志,确定页面加载没有请求接口,说明请求接口的代码没执行。我用高版本 Chrome 查看 Preview(之前没做这一步),果然除了看到页面元素,还进行了请求尝试。于是又运用排除法,一步步打印首页加载的过程。
在这之前,又是如何调试的问题,我们把测试入口改为我本地的 preview 地址。
加入打印日志,本地 build,Preview,打印了日志信息,果然接口请求的部分未执行。排查发现是之前调起 WebView 导航的代码出问题,可能是 jsBridge 还未加载。
但是为什么其他设备没有出现这个问题呢,可能是设备性能较差。于是把相关代码放到 jsBridge 加载完成后执行,修复了这样一个隐藏的 bug。
但是为什么不报错呢??这真的是很不好的体验,之前 Vue router 不报错的问题也类似。
总结
同样,我们再回头看那个最初的报错:
vite: loading legacy chunks, syntax error above and the same error below should be ignored
上面的报错、项目相同的报错可以被忽略。很容易让人忽略了报错,明确提到了下面的报错,但是 Vite 打包中下面没有出现同样的报错了(我目前的打包和其他文章中提到的不一样,比如知乎文章提到的代码);而且相同的报错到底是什么相同,同样是语法报错而已啊。
这句提示值得商榷。
function __vite_legacy_guard() {
import.meta.url;
import("_").catch(() => 1);
async function* g() {};
};
主要是因为出问题的恰恰就是中间的版本,Chrome 62,Vite 让它报错又能正常运行。遇到这种情况少之又少,从权衡上来说,好像也没有问题
本质上,只是在解决一个打包后的文件报错的问题,问题是一开始并没有定位到这个问题,其次是打包后的报错仍然难以定位具体错误位置。然后这其中还涉及到项目自身环境的各种干扰。
几点感悟:
- 坚持不懈,这是解决问题的唯一原因。
- 总结熟练调试很重要,要快速找到方便调试的方法。
- 没有报错是开发的一大痛点。
- 针对当前的问题更深入的分析原因,更广泛的尝试。
- 多用 ChatGPT,ChatGPT 的强大在于它没有弱点,没有缺项。
说明
通过这个案例,希望能给大家一点解决问题的启发。是遇到类似的问题时:
- 了解相关的问题
- 熟悉相关的概念
- 学习解决问题的方法
- 学习调试的方法
- 坚持的重要性
参考
【原理揭秘】Vite 是怎么兼容老旧浏览器的?你以为仅仅依靠 Babel?
来源:juejin.cn/post/7386493910820667418
研发都认为DBA很Low?我反手一个嘴巴子
前言
我用十多年的DBA经验告诉你,如果你作为研发觉得DBA很Low,你是会吃苦头的
“你以为DBA就是安装一下数据库,管理一下数据库?你丢个SQL给DBA优化下?你的日志爆满了DBA给你清理一下?DBA帮你安装下中间件?你以为的DBA只是做这些事?”
秉持着和平交流的学习态度,我这里精选了几位高赞粉丝的精彩回答
1.救火能力
1.1 调优
IT界并没有一个通行的 ”拳头“ 来判断谁low,谁更low。有时候,研发写的程序,新功能发布后,就出现磁盘IO出现瓶颈了、或者CPU飙高到100%了,但是这个时候,只是表象,只知道Linux机器的资源耗尽了,DBA得先找到资源消耗在哪了,才能进一步分析原因,用数据说话是应用的问题,才能责令程序员整改。
SQL调优是一个复杂的过程,涉及多个方面,包括但不限于SQL语句的编写、索引的使用、表的连接策略、数据库的统计信息、系统资源的利用等。调优的难度取决于多个因素,包括查询的复杂性、数据量、硬件资源、数据库的工作负载和现有的优化策略。
在这里给大家分享一个执行计划变,1个SQL把系统干崩的情景,由于业务用户检索数据范围过大,导致执行计划谓词越界,通过矫正执行计划及开启操作系统大页,服务器DB一直存在的CPU高负载从75%降低到25%!
生产问题,瞬息万变,DBA要同时熟悉业务,并对硬件、网络要精通,要在这样的复杂情况下作出正确的决策,这一点我想难度不小吧。
1.2 高可用
数据库高可用是指DB集群中任何一个节点的故障都不会影响用户的使用,连接到故障节点的用户会被自动转移到健康节点,从用户感受而言, 是感觉不到这种切换。
那么DBA在高可用的配置方面,下面就是某制造业大厂,应用层的链接方式
--jdbc应用端的连接
jdbc:oracle:thin:@(DESCRIPTION =
(ADDRESS_LIST =(ADDRESS = (PROTOCOL = TCP)(HOST = rac1-vip)
(PORT = 1521))(ADDRESS = (PROTOCOL = TCP)(HOST = rac2-vip)
(PORT = 1521))(LOAD_BALANCE = no)(FAILOVER = yes))
(CONNECT_DATA =(SERVER = DEDICATED)(SERVICE_NAME = dbserver)))
那么这种配置FAILOVER = yes,Net会从多个地址中按顺序选择一个地址进行连接,直到连接成功为止,那么就会保证数据库单节点故障,自动的切换,高可用是故障发生的第一个救命的稻草,系统上线前一定要测试好,才能确保数据库的高可用,这期间DBA功不可没!
还有客户要求选择的一套国产数据库支持核心业务,那么作为DBA在选型及业务适配上就发挥作用了,跟研发确认发现应用是兼容PG的,而且客户要求要同时兼容OLAT和OLTP业务,看下以下这套openGauss国产数据库的高可用架构。
1.openGauss高可用:CM
通过配置VIP故障转移,OLTP连接VIP,进行事物交易
同时支持动态配置CM集群故障切换策略和数据库集群脑裂故障恢复策略,
从而能够尽可能确保集群数据的完整性和一致性。
2.写重定向,报表分析业务连接,支持读写分离
主备节点开启控制参数 enable_remote_execute=on之后
通过备库发起的写操作,会重定向到主库执行
2.监控能力
这方面我是最有发言权了,SA一直是我的本职工作,从机房硬件部署、弱电以及数据库的安装实施,很多东西需要依赖于DBA来做,全力保障应用的稳定性,而且监控到的指标随时可以推送到邮件以及微信。这期间我也发现了很多天窗,原来还可以这么干?
2.1 服务器监控
首先监控Linux服务嘛,那肯定是要全方位系统的监控,网络、磁盘、CPU、内存等等,这才叫监控,那么其实给大家推荐一款免费的监控工作
Prometheus提供了从指标暴露,到指标抓取、存储和可视化,以及最后的监控告警等组件。
数据库监控
Zabbix聚焦于帮助用户通过性能优化和功能升级来快速响应业务需求,从而满足客户的高期望值,并提升IT运维人员的生产力。在可扩展性与性能、稳定性与高可用、可观测性几个领域获得持续提升。监控做不好,救火救到老!拿下Zabbix,现在!立刻!马上!!
1.监控Oracle
博客地址:
https://jeames.blog.csdn.net/article/details/126825934
2.监控PostgreSQL
博客地址:
https://jeames.blog.csdn.net/article/details/120300581
3.监控MySQL
博客地址:
https://jeames.blog.csdn.net/article/details/126825934
3 数据源赋能者
从AI、智能化到云迁移和安全性,业务和技术趋势不断重塑DBA在组织中的角色.DBA 群体站在时代的岔路口,国产数据库太多了应该怎么选?DBA 会被云上数据库抛弃吗?应该如何应对新时代挑战?职业终点在哪里?
1.云数据库解决方案
DBA要善于利用云原生保障数据安全和优化成本
2.数据安全与合规
随着数据保护法律的出台、日益严峻的网络攻击,
DBA必须掌握加密、访问控制和审计等技能
3.灾难恢复和业务连续性
随着企业愈加依赖数据的连续性,
快速恢复丢失数据并最大限度地减少停机时间至关重要
4.自动化和脚本编写
自动化和脚本编写对于DBA管理重复性任务和提高效率尤为关键
5.有效的沟通和协作
有效的沟通和协作仍然是DBA的重要技能。
能够向同事清楚地传达技术信息、与跨职能团队合作,
打破IT部门和业务部门之间的信息差,确保数据库的策略与组织目标保持一致。
4.总结
在一个公司写了屎山代码的研发,可以拍拍屁股走人,然后继续去下一个企业再写个屎山。反正不会追着代码跨省找你。而一个搞崩了系统的DBA,这个闯祸经历将成为他的黑历史,并影响到他未来的就业.因为需要专业DBA的好企业,基本都是几百台服务器起步的大项目,难免不会查背景,这就导致DBA如果想干得好,圈子会越来越小,请记住是干得好,不是混得好,混是会出事的。
好了,以上就是我对DBA的理解了,有不足之处还望指正。
来源:juejin.cn/post/7386505099848646710
谈谈前端如何防止数据泄漏
最近突然发现了一个好玩的事情,部分网站进去的时候几乎都是死的,那种死是区别于我们常见的网站的死:
- 不能选中文字
- 不能复制粘贴文字
- 不能鼠标右键显示选项
- 不能打开控制台
- ……
各种奇葩的操作应接不暇,像极了我最初接触的某库。shigen
的好奇心直接拉满,好家伙,这是咋做的呀。一顿操作之后,发现这种是为了防止网站的数据泄露(高大上)。在我看来,不是为了装X就是为了割韭菜。
咱废话也不多说,就手动来一个,部分代码参考文章:如何防止网站信息泄露(复制/水印/控制台)。
那shigen
实现的效果是这样的:
用魔法生成了一个页面,展示的是李白的《将进酒》。我需要的功能有尽可能的全面,禁止复制、选择、调试……
找了很多的方式,最后能自豪的展示出来的功能有:
- 禁止选择
- 禁止鼠标右键
- 禁止复制粘贴
- 禁止调试资源(刷新页面的方式)
- 常见的页面水印
那其实也没有特别的技术含量,我就在这里展示了,希望能作为工具类供大家使用。
页面部分
html5+css,没啥好讲的。
html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
line-height: 1.6;
padding: 20px;
text-align: center;
background-color: #f8f8f8;
}
.poem-container {
max-width: 600px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 1.5em;
margin-bottom: 20px;
}
p {
text-indent: 2em;
font-size: 1.2em;
}
style>
<title>李白《将进酒》title>
head>
<body>
<div class="poem-container">
<h1>将进酒h1>
<p>君不见,黄河之水天上来,奔流到海不复回。p>
<p>君不见,高堂明镜悲白发,朝如青丝暮成雪。p>
<p>人生得意须尽欢,莫使金樽空对月。p>
<p>天生我材必有用,千金散尽还复来。p>
<p>烹羊宰牛且为乐,会须一饮三百杯。p>
<p>岑夫子,丹丘生,将进酒,杯莫停。p>
<p>与君歌一曲,请君为我倾耳听。p>
<p>钟鼓馔玉不足贵,但愿长醉不复醒。p>
<p>古来圣贤皆寂寞,惟有饮者留其名。p>
<p>陈王昔时宴平乐,斗酒十千恣欢谑。p>
<p>主人何为言少钱,径须沽取对君酌。p>
<p>五花马,千金裘,呼儿将出换美酒,与尔同销万古愁。p>
div>
body>
js部分
禁止选中
// 防止用户选中
function disableSelect() {
// 方式:给body设置样式
document.body.style.userSelect = 'none';
// 禁用input的ctrl + a
document.keyDown = function(event) {
const { ctrlKey, metaKey, keyCode } = event;
if ((ctrlKey || metaKey) && keyCode === 65) {
return false;
}
}
};
禁止复制、粘贴、剪切
document.addEventListener('copy', function(e) {
e.preventDefault();
});
document.addEventListener('cut', function(e) {
e.preventDefault();
});
document.addEventListener('paste', function(e) {
e.preventDefault();
});
禁止鼠标右键
// 防止右键
window.oncontextmenu = function() {
event.preventDefault()
return false
}
禁止调试资源
这个我会重点分析。
let threshold = 160 // 打开控制台的宽或高阈值
window.setInterval(function() {
if (window.outerWidth - window.innerWidth > threshold ||
window.outerHeight - window.innerHeight > threshold) {
// 如果打开控制台,则刷新页面
window.location.reload()
}
}, 1000)
这个代码的意思很好理解,当我们F12的时候,页面的宽度肯定会变小的,我们这个时候和屏幕的宽度比较,大于我们设置的阈值,我们就算用户在调试页面了。这也是我目前找到的比较好的方式了。但是,但是,认真思考一下以下问题需要你考虑吗?
- 页面频繁加载,流量的损失大吗
- 页面刷新,后端接口频繁调用,接口压力、接口幂等性
所以,我觉得这种方式不优雅,极度的不优雅,但是有没有别的好的解决办法。
加水印
// 生成水印
function generateWatermark(keyword = 'shigen-demo') {
// 创建Canvas元素
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
// 设置Canvas尺寸和字体样式
canvas.width = 100;
canvas.height = 100;
context.font = '10px Arial';
context.fillStyle = 'rgba(0,0,0,0.1)';
// 绘制文字到Canvas上
context.fillText(keyword, 10, 50);
// 生成水印图像的URL
const watermarkUrl = canvas.toDataURL();
// 在页面上显示水印图像(或进行其他操作)
const divDom = document.createElement('div');
divDom.style.cssText = `
position: fixed;
z-index: 99999;
top: -10000px;
bottom: -10000px;
left: -10000px;
right: -10000px;
transform: rotate(-45deg);
pointer-events: none;
background-image: url(${watermarkUrl});
`;
document.body.appendChild(divDom);
}
代码不需要理解,部分的参数去调整一下,就可以拿来就用了。
我一想,我最初接触到这种页面水印的时候,是在很老的OA办公系统,到后来用到了某书,它的app页面充满了水印,包括浏览器端的页面。
所以,我也实现了这个。but,but,有一种技术叫做OCR,大白话讲就是文字识别。我把图片截个图,让某信、某书识别以下,速度和效果那叫一个nice,当然也可能把水印也识别出来了。聪敏的开发者会把水印的颜色和文字的颜色设置成一种,这个时候需要准确的文字那可得下一番功夫了。换句话说,不是定制化的OCR,准确的识别出信息,真的够呛。
还有的很多页面实现了js的数据加密、接口数据加密。但是道高一尺,魔高一丈,各种都是在一种相互进步的。就看实际的业务场景和系统的设计了。
来源:juejin.cn/post/7300102080903675915
从劝退 flutter_screenutil 聊到不同尺寸 UI 适配的最佳实践
先说优点
💡 先说优点叠个甲,毕竟库本身没有太大问题,往往都是使用的人有问题。
由于是基于设计稿进行屏幕适配的框架,在处理不同尺寸的屏幕时,都可以使用相同的 尺寸数值+单位
,实现对设计稿等比例的适配,同时保真程度一般很高。
在有设计稿的情况下,只使用 Container + GestureDetector 都可以做到快速的开发,可谓是十分的无脑梭哈。
在:只考虑移动端、可以接受使用大屏幕手机看小屏幕 ui、不考虑大字体的模式、被强烈要求还原设计稿、急着开发。的情况下,还是挺好用的。
为什么劝退?
来到我劝退师最喜欢的一个问题,为什么劝退。如果做得不好,瞎搞乱搞,那就是我劝退的对象。
在亲身使用了两个项目并结合群里的各种疑惑,我遇到常见的有如下问题:
如何实现对平板甚至是桌面设备的适配?
由于基于设计稿尺寸,平板、桌面等设备的适配基本上是没法做的,要做也是费力不讨好的事。
千万不要想着说,我通过屏幕宽度断点来使用不同的设计稿,当用户拉动边框来修改页面的宽度时,体验感是很崩溃的。而且三套设计稿要写三遍不同的代码,就更不提了。(这里说三遍代码的原因是,计算 .w
.h
的布局,数据会跟随设计稿变化)
如何适配大字体无障碍?
因为大字体缩放在满屏的 .w
.h
下,也就是写死了尺寸的情况下,字体由于随系统字体放大,布局是绝对会溢出的。很多项目开发到最后上线才意识到自己有大字体无障碍的用户,甚至某些博客上,使用了一句:
MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
来处理掉自己的用户,强制所有屏幕字体不可缩放。一时的勉强敷衍过去,最后只能等项目慢慢腐烂。
为什么在 1.w 的情况下会很糊?同样是 16.sp 为什么肉眼可见的不一样大?
库的原理很简单,提供了一堆的 api 相对于设计图的宽高去做等比例计算,所以必然存在一个问题,计算结果是浮点数。可是?浮点数有什么问题吗?
梳理一下原理:已知屏幕设计图宽度 sdw
、组件设计图宽度 dw
,根据屏幕实际宽度 sw
,去计算得出组件实际宽度 w
。
w = sw / sdw * dw
可是设计图的屏幕宽度 sdw
作为分母时,并不能保证总是可以被表示为有限小数。举个例子:库的文档中给的示例是 const Size(360, 690),
的尺寸,如果我需要一个 100.w
会得到多少?在屏幕宽度为 420 的情况下,得到组件宽度应该为 116.6666... 的无限小数。
这会导致最终在栅格化时会面临消除小数点像素的锯齿问题。一旦有像素点的偏差,就会导致边缘模糊。
字体对尺寸大小更为敏感,一些非矢量的字体甚至只有几个档位的大小,当使用 14.5、15、15.5 的字体大小时,可能会得到一样的视觉大小,再加上 .sp 去计算一道,误差更是放大。
具体是否会发生在栅格化阶段,哪怕文章有误也无所谓,小数点像素在物理意义上就是不存在的,总是会面临锯齿平滑的处理,导致无法像素级还原 UI。
为什么部分屏幕下会溢出?
我们知道了有小数点问题,那么不得不说起计算机编程常见的一个不等式:
0.1 + 0.2 != 0.3
由于底层表示浮点数本身就有的精度问题,现在让 Flutter 去做这个加法,一样会溢出。考虑以下代码:
Row(
children: [
SizedBox(width: 60.w),
SizedBox(width: 100.w),
SizedBox(width: 200.w),
],
);
在一个总共宽度 360.w 的设计图上,可能出现了溢出,如果不去使用多个屏幕来调试,根本不会觉得异常,毕竟设计图是这样做的,我也是这样写的,怎么可能有错呢?
然而恰恰是库本身的小数问题,加上编程届常见的底层浮点数精度问题,导致边缘溢出一点点像素。
我使用了 screenutil 为什么和真实的单位 1px 1rem 1dp 的大小不同呢?
哪怕是 .sp
都是基于设计图等比例缩放的,使用 screenutil 就从来不存在真实大小,计算的结果都是基于设计稿的相对大小。就连 .w
和 .h
都没法保证比例相同,导致所有布局优先使用 .w
来编写代码的库,还想保证和真实尺寸相等?
为什么需要响应式 UI?
说个题外话:在面试淘菜菜的时候真的会有点崩不住,他们问如何做好不同屏幕的适配,我说首先这是 UI 出图的问题,如果 UI 出的图是响应式的,那没问题,照着写,闭着眼都能适配。
但是如果设计图不是响应式的,使用 flutter_screenutil 可以做到和设计图高保真等比还原,但是如果做多平台就需要 UI 根据屏幕断点出不同平台的设计图。
面试官立即就打断我说他们的 UI 只会出一份图。我当场就沉默了,然后呢?也不说话了?是因为只有移动端用户,或者说贵公司 UI 太菜了,还是说都太菜了。菜就给我往下学 ⏬
首先 UI 的响应式设计是 UI 的责任
抛开国情不谈,因为国内的 UI 能做到设计的同时,UI 还是响应式的,这样的 UI 设计师很少很少,他们能把主题规范好,约定好,已经是不得了的了。
但即使如此,响应式 UI 设计也还是应该归于 UI 设计中,在设计图中去根据不同的尺寸,拖动验证不同的布局效果是很容易的。在不同的尺寸下,应该怎么调整元素个数,应该如何去布局元素,只有 UI 使用响应式的写法去实现了,UI 和开发之间的无效交流才会减少。
响应式的 UI 可以避免精度问题
早在 19 年我就有幸翻阅了一本 iOS 的 UI 设计规范,当时有个特别的点特别印象深刻:尺寸大小应该为 2 的整数次幂,或者 4 的倍数。因为这样做,在显示和计算上会较为友好。
💡 这其实是有点历史原因的,之前的 UI 在栅格化上做得并不是很好,锯齿化严重也是常态,所以使用可以被 2 整除的尺寸,一方面使用起来只有几个档位,方便调整;另一方面这样的尺寸可以在像素的栅格化上把小数除尽。
举个例子,在屏幕中间显示一个 300 宽度的卡片,和边距 16 的卡片,哪一个更响应式,无疑是后者,前者由于需要计算 300 相对与设计稿屏幕的宽度,后者只需要准确的执行 16 的边距就好,中间的卡片宽度随屏幕的宽度自动变化。
同样的例子,带有 Expanded 布局的 Row 组件,相比直接给定每个子组件尺寸导致精度问题的布局,更能适配不同的屏幕。因为 Row 会先放置固定大小的组件,剩余空间由 Expanded 去计算好传给子组件,原理和 Web 开发中的 flex 布局一样。
响应式布局是通用的规范
如果有 Web 开发经验的,应该会知道 Web 的屏幕是最多变的,但是设计起来也可以很规范,常见的 bootstrap 框架就提到了断点这个观点,指出了当我们去做 UI 适配的时候,需要根据不同的屏幕大小去做适配。同时 flex 布局也是 Web 布局中常用的响应式布局手段。
在设计工具中,响应式 UI 也没有那么遥远,去下载一份 Material Design 的 demo,对里面的组件自由的拉伸缩放,再对比一下自己通过输入尺寸大小拼凑在一起的 UI,找找参数里面哪里有差异。
怎么做响应式 UI
这里直接放一个谷歌大会的演讲,我相信下面的总结其实都可以不用看了,毕竟本实验室没有什么可补充的,但是我们还是通过从外到内、从整体到局部的顺序来梳理一下如何去做一个响应式的 UI,从而彻底告别使用 flutter_screenutil。
http://www.youtube.com/watch?v=LeK…
SafeArea
一个简单的组件,可以确保内部的 UI 不会因为愚蠢的设备圆角、前置挖孔摄像头、折叠屏链接脚、全面屏边框等原因而被意外的裁剪,将重要的内容,显示在“安全区”中。
屏幕断点
让 UI 根据不同的尺寸的窗口变化而变化,首先就要使用 MediaQuery.sizeOf(context);
和 LayoutBuilder()
来实现对窗口的宽度的获取,然后通过不同的屏幕断点,去构建不同情况下的 UI。
其中 LayoutBuilder
还能获取当前约束下的宽度,以实现页面中子区域的布局,比如 Drawer
的宽度,对话框的宽度,导航的宽度。
这里举了个例子,使用媒体查询获得窗口宽度之后,展示不同的 Dialog
:
写出如此优雅的断点代码只需要三步:
- 抽象:找到全屏对话框和普通对话框中共同的属性,并将功能页面提取出来。
- 测量:思考应该使用窗口级别的宽度(MediaQuery),还是某个约束下的宽度(LayoutBuilder)。
- 分支:编写如上图所示的带有断点逻辑的代码。
GridView
熟悉了移动端的 ListView 布局之后,切换到 GridView 布局并适配到平板、桌面端,是一件十分自然的事,只需要根据情况使用不同的 gridDelegate
属性来设置布局方式,就能简单的适配。
这里一般使用 SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: )
方法来适配,传入一个期望的最大宽度,使其在任何屏幕上看到的子组件都自然清晰,GridView 会根据宽度计算出合适的一行里合适的列数。
Flex 布局,但是 Flutter 版
前面说过了尽量不要去写固定尺寸的几个元素加起来等于屏幕宽度,没有那么巧合的事情。在 Row/Column 中,善用 Expanded 去展开子组件占用剩余空间,善用 Flexible 去缩紧子组件,最后善用 Spacer 去占用空白,结合 MainAxisAlignment 的属性,你会发现布局是那样的自然。
只有部分组件是固定尺寸的
例如 Icon 一般默认 24,AppBar 和 BottomNavigationBar 高度为 56,这些是写在 MD 设计中的固定尺寸,但是一般不去修改。图片或许是固定尺寸的,但是一般也使用 AspectRatio 来固定宽高比。
我曾经也说过一个普遍的公理,因为有太多初学者容易因为这个问题而出错了。
当你去动态计算宽高的时候,可能是布局思路有问题了。
在大多数情况下,你的布局都不应该计算宽高,交给响应式布局,让组件通过自己的能力去得出自己的位置、约束、尺寸。
举一个遇到过的群友问题,他使用了 stack 布局包裹了应用栏和一个滚动布局,由于SliverAppBar 拉伸后的高度会变化,他想去动态的计算下方的滚动布局的组件起始位置。这个问题就连描述出来都是不可思议的,然后他问我,我应该如何去获取这个 AppBar 的高度,因为我想计算下方组件的高度。(原问题记不清了,但是这样的需求是不成立的)
最后,多看文档
最后补上关于 MD3 设计中,关于布局的文档,仔细学习:
最后的最后,响应式布局其实是一个很宽的话题,这里没法三言两语说完,只能先暂时在某些领域劝退使用这个库。任何觉得可能布局困难的需求,都可以发到评论区讨论,下一篇文章我们将根据几个案例来谈谈具体的实践。
来源:juejin.cn/post/7386947074640298038
没用的东西,你连个内存泄漏都排查不出来!!
背景 (书接上回)
- ui妹子的无理要求,我通通满足了。但是不出意外的话,意外就出来了。
- 此功能在上线之后,我们的业务在客户app内使用刷脸的时候会因为内存过高导致app将webview杀死。
- 然后我们leader爆了,让我排查问题。可是可是,我哪里会排查内存泄漏呀。
- 我:我不会。你自己不会上吗?你tm天天端个茶,抽个烟,翘个二郎腿,色眯眯的看着ui妹妹。
- 领导:污蔑,你纯粹就是污蔑。我tm现在就可以让你滚蛋,你信吗?
- 我:我怕你个鸟哦,我还不知道你啥水平,你tm能写出来个防抖节流,我就给你磕头。
- 领导:hi~ui妹妹,今天过的好吗,来,哥哥这里有茶喝。(此时ui妹妹路过)。你赶快给我干活,以后ui妹妹留给你。
- 艹!你早这么说不就好了。
开始学习
Chrome devTools查看内存情况
- 打开
Chrome
的无痕模式,这样做的目的是为了屏蔽掉Chrome
插件对我们之后测试内存占用情况的影响
- 打开开发者工具,找到
Performance
这一栏,可以看到其内部带着一些功能按钮,例如:开始录制按钮;刷新页面按钮;清空记录按钮;记录并可视化js内存、节点、事件监听器按钮;触发垃圾回收机制按钮等
简单录制一下百度页面,看看我们能获得什么,如下动图所示:
从上图中我们可以看到,在页面从零到加载完成这个过程中
JS Heap
(js堆内存)、documents
(文档)、Nodes
(DOM节点)、Listeners
(监听器)、GPU memory
(GPU
内存)的最低值、最高值以及随时间的走势曲线,这也是我们主要关注的点
看看开发者工具中的Memory
一栏,其主要是用于记录页面堆内存的具体情况以及js堆内存随加载时间线动态的分配情况
堆快照就像照相机一样,能记录你当前页面的堆内存情况,每快照一次就会产生一条快照记录
如上图所示,刚开始执行了一次快照,记录了当时堆内存空间占用为
33.7MB
,然后我们点击了页面中某些按钮,又执行一次快照,记录了当时堆内存空间占用为32.5MB
。并且点击对应的快照记录,能看到当时所有内存中的变量情况(结构、占总占用内存的百分比...)
在开始记录后,我们可以看到图中右上角有起伏的蓝色与灰色的柱形图,其中
蓝色
表示当前时间线下占用着的内存;灰色
表示之前占用的内存空间已被清除释放
在得知有内存泄漏的情况存在时,我们可以改用Memory
来更明确得确认问题和定位问题
首先可以用Allocation instrumentation on timeline
来确认问题,如下图所示:
内存泄漏的场景
- 闭包使用不当引起内存泄漏
- 全局变量
- 分离的
DOM
节点 - 控制台的打印
- 遗忘的定时器
1. 闭包使用不当引起内存泄漏
使用Performance
和Memory
来查看一下闭包导致的内存泄漏问题
<button onclick="myClick()">执行fn1函数button>
<script>
function fn1 () {
let a = new Array(10000) // 这里设置了一个很大的数组对象
let b = 3
function fn2() {
let c = [1, 2, 3]
}
fn2()
return a
}
let res = []
function myClick() {
res.push(fn1())
}
script>
在退出
fn1
函数执行上下文后,该上下文中的变量a
本应被当作垃圾数据给回收掉,但因fn1
函数最终将变量a
返回并赋值给全局变量res
,其产生了对变量a
的引用,所以变量a
被标记为活动变量并一直占用着相应的内存,假设变量res
后续用不到,这就算是一种闭包使用不当的例子
设置了一个按钮,每次执行就会将fn1
函数的返回值添加到全局数组变量res
中,是为了能在performacne
的曲线图中看出效果,如图所示:
- 在每次录制开始时手动触发一次垃圾回收机制,这是为了确认一个初始的堆内存基准线,便于后面的对比,然后我们点击了几次按钮,即往全局数组变量
res
中添加了几个比较大的数组对象,最后再触发一次垃圾回收,发现录制结果的JS Heap曲线刚开始成阶梯式上升的,最后的曲线的高度比基准线要高,说明可能是存在内存泄漏的问题 - 在得知有内存泄漏的情况存在时,我们可以改用
Memory
来更明确得确认问题和定位问题 - 首先可以用
Allocation instrumentation on timeline
来确认问题,如下图所示:
- 在我们每次点击按钮后,动态内存分配情况图上都会出现一个
蓝色的柱形
,并且在我们触发垃圾回收后,蓝色柱形
都没变成灰色柱形,即之前分配的内存并未被清除 - 所以此时我们就可以更明确得确认内存泄漏的问题是存在的了,接下来就精准定位问题,可以利用
Heap snapshot
来定位问题,如图所示:
- 第一次先点击快照记录初始的内存情况,然后我们多次点击按钮后再次点击快照,记录此时的内存情况,发现从原来的
1.1M
内存空间变成了1.4M
内存空间,然后我们选中第二条快照记录,可以看到右上角有个All objects
的字段,其表示展示的是当前选中的快照记录所有对象的分配情况,而我们想要知道的是第二条快照与第一条快照的区别在哪,所以选择Object allocated between Snapshot1 and Snapshot2
即展示第一条快照和第二条快照存在差异的内存对象分配情况,此时可以看到Array的百分比很高,初步可以判断是该变量存在问题,点击查看详情后就能查看到该变量对应的具体数据了
以上就是一个判断闭包带来内存泄漏问题并简单定位的方法了
2. 全局变量
全局的变量一般是不会被垃圾回收掉的当然这并不是说变量都不能存在全局,只是有时候会因为疏忽而导致某些变量流失到全局,例如未声明变量,却直接对某变量进行赋值,就会导致该变量在全局创建,如下所示:
function fn1() {
// 此处变量name未被声明
name = new Array(99999999)
}
fn1()
- 此时这种情况就会在全局自动创建一个变量
name
,并将一个很大的数组赋值给name
,又因为是全局变量,所以该内存空间就一直不会被释放 - 解决办法的话,自己平时要多加注意,不要在变量未声明前赋值,或者也可以
开启严格模式
,这样就会在不知情犯错时,收到报错警告,例如
function fn1() {
'use strict';
name = new Array(99999999)
}
fn1()
3. 分离的DOM
节点
假设你手动移除了某个dom
节点,本应释放该dom节点所占用的内存,但却因为疏忽导致某处代码仍对该被移除节点有引用,最终导致该节点所占内存无法被释放,例如这种情况
<div id="root">
<div class="child">我是子元素div>
<button>移除button>
div>
<script>
let btn = document.querySelector('button')
let child = document.querySelector('.child')
let root = document.querySelector('#root')
btn.addEventListener('click', function() {
root.removeChild(child)
})
script>
该代码所做的操作就是点击按钮后移除
.child
的节点,虽然点击后,该节点确实从dom
被移除了,但全局变量child
仍对该节点有引用,所以导致该节点的内存一直无法被释放,可以尝试用Memory
的快照功能来检测一下,如图所示
同样的先记录一下初始状态的快照,然后点击移除按钮后,再点击一次快照,此时内存大小我们看不出什么变化,因为移除的节点占用的内存实在太小了可以忽略不计,但我们可以点击第二条快照记录,在筛选框里输入
detached
,于是就会展示所有脱离了却又未被清除的节点对象
解决办法如下图所示:
<div id="root">
<div class="child">我是子元素div>
<button>移除button>
div>
<script>
let btn = document.querySelector('button')
btn.addEventListener('click', function() {
let child = document.querySelector('.child')
let root = document.querySelector('#root')
root.removeChild(child)
})
script>
改动很简单,就是将对
.child
节点的引用移动到了click
事件的回调函数中,那么当移除节点并退出回调函数的执行上文后就会自动清除对该节点的引用,那么自然就不会存在内存泄漏的情况了,我们来验证一下,如下图所示:
结果很明显,这样处理过后就不存在内存泄漏的情况了
4. 控制台的打印
<button>按钮button>
<script>
document.querySelector('button').addEventListener('click', function() {
let obj = new Array(1000000)
console.log(obj);
})
script>
我们在按钮的点击回调事件中创建了一个很大的数组对象并打印,用performance
来验证一下
开始录制,先触发一次垃圾回收清除初始的内存,然后点击三次按钮,即执行了三次点击事件,最后再触发一次垃圾回收。查看录制结果发现
JS Heap
曲线成阶梯上升,并且最终保持的高度比初始基准线高很多,这说明每次执行点击事件创建的很大的数组对象obj
都因为console.log
被浏览器保存了下来并且无法被回收
接下来注释掉console.log
,再来看一下结果:
<button>按钮button>
<script>
document.querySelector('button').addEventListener('click', function() {
let obj = new Array(1000000)
// console.log(obj);
})
script>
可以看到没有打印以后,每次创建的obj
都立马被销毁了,并且最终触发垃圾回收机制后跟初始的基准线同样高,说明已经不存在内存泄漏的现象了
其实同理 console.log
也可以用Memory
来进一步验证
未注释 console.log
注释掉了console.log
最后简单总结一下:在开发环境下,可以使用控制台打印便于调试,但是在生产环境下,尽可能得不要在控制台打印数据。所以我们经常会在代码中看到类似如下的操作:
// 如果在开发环境下,打印变量obj
if(isDev) {
console.log(obj)
}
这样就避免了生产环境下无用的变量打印占用一定的内存空间,同样的除了
console.log
之外,console.error
、console.info
、console.dir
等等都不要在生产环境下使用
5. 遗忘的定时器
定时器也是平时很多人会忽略的一个问题,比如定义了定时器后就再也不去考虑清除定时器了,这样其实也会造成一定的内存泄漏。来看一个代码示例:
<button>开启定时器button>
<script>
function fn1() {
let largeObj = new Array(100000)
setInterval(() => {
let myObj = largeObj
}, 1000)
}
document.querySelector('button').addEventListener('click', function() {
fn1()
})
script>
这段代码是在点击按钮后执行fn1
函数,fn1
函数内创建了一个很大的数组对象largeObj
,同时创建了一个setInterval
定时器,定时器的回调函数只是简单的引用了一下变量largeObj
,我们来看看其整体的内存分配情况吧:
按道理来说点击按钮执行fn1
函数后会退出该函数的执行上下文,紧跟着函数体内的局部变量应该被清除,但图中performance
的录制结果显示似乎是存在内存泄漏问题的,即最终曲线高度比基准线高度要高,那么再用Memory
来确认一次:
- 在我们点击按钮后,从动态内存分配的图上看到出现一个蓝色柱形,说明浏览器为变量
largeObj
分配了一段内存,但是之后这段内存并没有被释放掉,说明的确存在内存泄漏的问题,原因其实就是因为setInterval
的回调函数内对变量largeObj
有一个引用关系,而定时器一直未被清除,所以变量largeObj
的内存也自然不会被释放 - 那么我们如何来解决这个问题呢,假设我们只需要让定时器执行三次就可以了,那么我们可以改动一下代码:
<button>开启定时器button>
<script>
function fn1() {
let largeObj = new Array(100000)
let index = 0
let timer = setInterval(() => {
if(index === 3) clearInterval(timer);
let myObj = largeObj
index ++
}, 1000)
}
document.querySelector('button').addEventListener('click', function() {
fn1()
})
script>
现在我们再通过performance
和memory
来看看还不会存在内存泄漏的问题
performance
这次的录制结果就能看出,最后的曲线高度和初始基准线的高度一样,说明并没有内存泄漏的情况
memory
这里做一个解释,图中刚开始出现的蓝色柱形是因为我在录制后刷新了页面,可以忽略;然后我们点击了按钮,看到又出现了一个蓝色柱形,此时就是为fn1
函数中的变量largeObj
分配了内存,3s
后该内存又被释放了,即变成了灰色柱形。所以我们可以得出结论,这段代码不存在内存泄漏的问题
简单总结一下: 大家在平时用到了定时器,如果在用不到定时器后一定要清除掉,否则就会出现本例中的情况。除了
setTimeout
和setInterval
,其实浏览器还提供了一个API
也可能就存在这样的问题,那就是requestAnimationFrame
- 好了好了,学完了,ui妹妹我来了
- ui妹妹:去你m的,滚远点
好了兄弟们,内存泄漏学会了吗?
来源:juejin.cn/post/7309040097936474175
面包会有的,玫瑰也会有的
前言
在杭州持续半个多月的阴雨中,迎来了“大火收汁”的7月🥵。转眼间这个2024年也过去了一半,我也从大学毕业做了2年的“来杭州讨饭的🐕”了。本来最近是有点忙的,7月底要疗休养,但是突然来了不少活,不过现在要等接口开发好,所以还是来做一下年中暨成为社畜两周年总结了。
减肥(膝盖要紧)
减不动了🤣,之前走路走太多了,把自己走出了滑膜炎,现在多走几千步膝盖就会疼(想想自己养成走路习惯还是因为买 huawei 手环下载的 APP 上面的成就奖牌,当时为了拿成就一天两万多步)。之前减肥是真的快啊,三四个月从 83kg 减到 68kg ,最近 4 个多月没怎么运动了,也就保持在 71kg 左右。家里人也不让我减了,说再减就难看了。
补牙(双连:一战成名)
去年体检被发现有了一颗蛀牙,今年才有时间去补,第一次体验补牙还挺新奇的(不过很快就不新奇了)。好在只蛀到一点点神经,上了点药阻断一下就补上了。原本以为这种事短时间内不会再体验了,结果 2 个月前在我品尝我的梅干菜肉饼时,我左上的一颗槽牙被神奇地磕掉了一小块😅......好嘛,再次喜提牙科医生的修补打磨。
工作学习(平平淡淡)
我这个小小前端每天也就是砌个div了,这半年没整太多新东西,就是基于公司的已有平台,加加feature、修修bug,把官网PC端和移动端换了一遍样式,现在官网到处是UI加的毛玻璃特效,在我这个 8G 内存小 thinkbook 机子的浏览器上肉眼可见的卡顿🫠(可能因为我的核显不行吧,我加了搜到的translateZ(0)
也没体会到加速),没办法可能用户电脑都很强能带的动吧。
不过说了好几个月的基于 umi + qiankun 的新平台在下半年终于是要交付了,又重新过了一遍代码和开发流程。有一说一,真的很麻烦,整个平台根本就不大,完全想不通为什么要上微前端,我们目前开发团队算上外包同学也就3个前端了,而且后续可能就我一个人负责这个平台的前端开发和维护😣。
工作之外重温了一下之前了解过的 SolidJS、Svelte 和 Tauri,写了几个小 Demo
练练手:
3月份打算今年11月考软考高项的,但是刚看了一个月的书,就通知只有上半年考了🫤。我寻思报名费这么贵,两三个月准备时间岂不是做慈善?!那就明年5月份再考吧,下半年该把书啊什么的再拿出来看了。
吃喝玩乐(还得是家乡的味道)
5月份,带女朋友回了老家徐州玩。感觉物价比起过去涨了好多,尤其是节假日的酒店(不敢想要是高铁也在节假日涨价,我过年还回不回得起家),苏宁广场的绿茶餐厅比杭州in77的还贵。宝莲寺在最近徐州旅游火起来之前我都没听说过😹。羊肉串、菜煎饼、米线、蒸菜、冷面味道还是好吃的👍,不过在家待的几天都是出去下馆子,没怎么吃到家里人做的菜,饭馆基本都是除了几道特色菜好吃,其他都一般般。
还得是过年时,家里做的好吃🫡。
再秀两张在宝莲寺的情侣照😎
展望
女朋友也毕业了,后面就是两个人在杭州打拼了。现在的工作其实挺好的,但是没有机会爬上去,爬不上去就没法在这个房价、物价如此离谱的城市留下。爸妈总是跟我说他们当初在一起的时候什么也没有,后面还是一起打拼出了这个家,面包总会有的。不过我想社会发展到现在,想要的也不一样了,面包要有,玫瑰也要有的吧。
hh,再写就要丧起来了。说不准哪天就中彩-票了,什么面包、玫瑰都不是事😇,全都做成鲜花饼😡,硌不坏牙的那种。
来源:juejin.cn/post/7386848746913366056
我是计算机专业研二的学生,我焦虑死了
最近两周,总有大三或研二的同学在微信上跟我说:
“学长,我想在明年上半年的时候找个中大厂的实习,以及在秋招的时候拿个好点儿的offer。
但我现在没有项目经验,八股文才刚刚开始看,算法只会刷几十道简单和中等难度的,现在时间一天天过去,我自己并不在学习状态,感觉非常焦虑,甚至焦虑得晚上睡不着觉。”
其实,我非常理解同学们有这种焦虑状态,而形成这种状态的原因,我的分析如下。
恐惧来源于未知,而焦虑来自于不可控,大部分又焦虑又没有学习状态的同学,往往都有类似困惑:
- 我是双非学历,学了还有用吗?会不会大厂的简历筛选都不通过?
- 我现在开始学,一天10个小时这种节奏,还来得及吗?
- 我到底应该学习哪些东西,先后顺序是什么,这些东西需要学到什么程度才行?
而正是这种努力了也不一定更好,自己的命运无法掌控的感觉,让他们产生了不安全感和焦虑。
恰恰,大部分处于这种状态的同学,都是学习成绩和技术能力处于中等水平的。
因为按照现在这市场行情,学习成绩和技术能力处于下游的同学,早就已经放飞自我、彻底躺平了。
他们的想法是:毕业后的工作爱找成啥样就找成啥样呗,反正最后有个工作干着就行。现在这经济环境,就算努力了也大概率没什么好的结果,反而希望越大,失望就越大,再给自己累出一身病来,得不偿失。
而在各方面都比较优秀的同学,感到焦虑的人也不是很多,毕竟那份骨子里的自信和从容还是有的。
下面,我就给这类焦虑的同学支支招,用我自己思考的“双标一力”策略来化解这种负面情绪。
“双标一力”,即:制定目标 + 拆解目标 + 执行力。
制定目标
有句话是这样说的,梦想还是要有的,万一实现了呢?
但如果真的把不切实际的梦想作为目标,容易让自己整体的执行落地计划和动作变形,导致自己越来越焦虑,最终彻底摆烂。
BTW:这里所说的目标,是以校招生的身份,拿到心仪公司的offer。
举个例子,如果你是双非二本学历,技术能力也没牛逼到傲视群雄的地步,我不建议你把目标定为互联网大厂。
你可以把目标更切实际地定为,找个福利待遇中上等的公司,保证先上牌桌,再图发展。
如果你不知道如何给自己制定目标,我建议你采用“参照对标”的方式。
比如:你目前的学习成绩和技术储备在学校中排名前 30%的话,可以把去年前20%的学长学姐所去的公司作为目标。毕竟,去年和今年的招聘市场环境相差不大。
为什么今年前30%对去年的前20%呢?很简单,就是留一些空间让你去追赶的。
拆解目标
制定完目标后,你的焦虑感依然是存在的,还是觉得心里没底。
接下来就要看,你想要拿到目标公司的offer,都需要做哪些技术储备,以及需要储备到什么程度了。
举个例子,现在你想拿到互联网大厂的Java后端岗的offer,那以下几个方面的储备是缺一不可的。其中包括:
- Java技术栈的相关八股文(一期:Java、Spring生态、MyBatis、MySQL、Redis、JVM、操作系统、计网;二期:ES、MQ、Netty等)。
- 500+的算法题。
- 两段以上的项目经历,以及熟悉项目中对应的技术点。
- 至少3个月的大厂实习经历。
下面我们继续对其进行拆解,以此得出不同时间节点需要做完哪些事情。
我们按照秋招九月份开始,那如果具备上述至少3个月的大厂实习经历的话,那意味着最迟六月初就要去大厂开始实习了。
我们继续往前推算,如果6月份入职实习,那预留一个半月找实习的时间,是比较充裕的,也就是4月中旬。
接下来,我们再看项目、八股文和算法应该如何进行安排。
其中,项目和八股文是有先后顺序的,如果你在没练手过任何项目的情况下,直接去背八股文,那这个过程会非常痛苦。
但反过来,你没有储备任何八股文,但只要掌握Java基本语法和SpringBoot、MyBatis的用法,跟着黑马、尚硅谷、慕课的视频敲两个新手小白项目,那应该是不难的。
而刷算法这件事情,只要你大学期间有些数据结构和算法的底子,那直接开始即可。另外,算法储备会比较耗时,所以越早开始越好。
那整体的拆解路径也就出来了,我们假设从2023年的12月中旬开始,进行如下安排:
- 项目 + 算法双管齐下,以二月中旬为期限,在这两个月中,动手敲两个小项目 + 初刷200个算法题。
- 在二月中旬到三月中旬期间,以一期八股文为主 + 再储备100个算法题为辅。
- 在三月中旬到四月中旬,继续以一期八股文为主 + 简历中项目的技术点为主 + 二刷300道算法题为辅。
- 四月中旬到六月初,一期八股文 + 简历中项目的技术点 + 300道算法(三刷、四刷)三管齐下。如果拿到了实习offer,那马上调转方向,在入职实习前的几天里,all in两三个月没碰过的项目,这样到了公司能快速上手。
- 六月初到九月初,在公司里好好工作实习,争取可以有亮点经历写在简历上,并巩固好。另外八股文和算法不要停,继续扩充广度,即:一期、二期八股文 + 500算法题。
执行力
这点没什么好说的,干就完了。真正的聪明人,都喜欢心无旁骛地下笨功夫。
我相信,只要你能用心坚持一个月,看着自己之前的学习计划正在如期逐步落地,你的焦虑感就会变为成就感。
结语
希望我写的这些,能够对正在焦虑中的大三、研二的计算机专业同学有所帮助。
来源:juejin.cn/post/7310147188252704787
前端超进化-小公司不用自研也能搞基建(全开源工具版)
蛮荒时代
快看,这个男人叫小帅,他进了一家只有4个人的信息高科技有限公司,还妄想改变世界。
前端只有一个人,所谓的发版,就是直接本地打包,然后代码通过ftp工具扔到服务器上,代码能跑就行。
农耕时代
这种不靠谱的开发模式进行了一段时间,直到某一天,ftp把服务器搞挂了的同时,本地的文件也丢了。所以他的leader小强意识到是时候需要改变了。
首先他们开始使用了git,代码不再是只存在本地,更是存在了云端。同时新建了dev/prod/maste分支来保证相对于各个环境的独立。
在某次把dev环境代码打包上传到生产后,为了区分环境和避免再次发生这种事故,小强也开始使用了在线打包工具jenkins,
jenkins的引入也有效的避免了服务器账号外泄的风险,现在的服务器账号相对开发者是黑盒的。
手工时代
慢慢的,公司的生意做的越来越好,老板已经开始看奔驰了。
前端也从1个人加到了3个人,成立前端技术部,小帅觉得自己快要走上人生巅峰了。人加了3个,问题也越来越多。
第一个重要的问题就是,git分支总冲突。
因为以前没有制定git规范,所以大家很随意的在dev和master上开发和提交代码。
所以约定了git的相关规范,每次用feature分支作为开发分支,feature通过merge合并到dev和上线release分支。
同时使用pre-commit来规范提交的commit信息。
第二个问题,是代码风格的问题。
人多了之后,大家的代码风格不一致了。比如有的人是2空格党,有个是4空格党,有人组件用驼峰命名,有人用大驼峰。为了统一风格,于是决定使用使用统一的风格检查。
同时了为了避免一些低级的语法错误和dirty code,引入了eslint进行代码检查。
通过Prettier对代码风格进行统一和格式化,通过Husky来在提交前进行link检查。在提交后,通过github Ci来进行二次检查。
至此,在手工时代,前端团队完成了代码风格和基础的lint检查。
工业时代
老板的爸爸原来是顾总,看到儿子创业很开心,给儿子调了5个亿的现金,于是大家开始快马加鞭,突击项目。
前端人数也直接爆炸,来到了20+,各种各样的项目如火如荼的进行,于是各种解决问题的前端方案应运而生。
微前端 single spa/qiankun
20个人的开发量,在一个项目中,代码量爆炸,每次启动打包,都巨慢无比。同时,之前一些零散的项目也需要并入到这个项目中,但是目前主技术栈用的react,零散项目有个用vue,有的用angular。
为了解决这些问题,所以要开始微前端的改造。
整体项目采用了single spa,通过统一的框架底层,将各个不同的项目聚合在一起。
微前端改造后,将整体的大项目拆分成了各自独立的项目。同时各自独立的项目有自己的仓库,各自独立打包。在运行时,各独立项目的停止也不影响其他项目。
物料库
现在各个项目独立开来,拥有各自的仓库,所以每次有新模块的加入,需要新建仓库,所以模版物料应运而生。
模版物料 degit
类似Vue-cli creat-react 都有做模版物料,里面包含了一个git地址,通过git拉取模版物料。
模版物料里包含了一个项目所需的相关配置,例如打包工具,框架cli,框架主模块,eslint,prettier,示例代码等。每个项目直接进行npm安装即可开始。
我们可以使用gedit来达成此目标
npx degit [git地址]
# 例如 npx degit https://github.com/tinlee/1000-project-demo
业务组件物料 VitePress/storybook
有一些物料我们需要结合业务场景,比如省市区选择器,这种物料通常都具有统一的api,ui,可以用于多个项目的使用。在基础的ui组件的基础上,还结合了业务属性。
这类物料也需要有展示的站点,我们可以使用VitePress/storybook来展示,也可以结合类似Dumi等。这些文档工具都是支持markdown和组件的混用插入。
npm私服 Sinopia
既然相关的业务组件库已经搭建完成,我们是希望不通过外网访问的,所以一个npm私服也必须要有。 这里通过Sinopia来进行私服的搭建。npm私服,在拉取的时候,检测当前服务器没有该包时会从npmjs获取,而上传的时候,只发到私服,而不会提交到npmjs。
文档工具 Outline
现在组件库很多,相关的技术文档很多,在以往的情况下,公司使用了腾讯/飞书等进行文档管理,但是很多内容不希望外部知道的内容。 所以使用Outline搭建了一个内部的文档工具。
Mock服务 YApi
以往前端和后端都口头约定,然后通过后端的swagger来生成文档。但是这存在一个问题,文档的生成滞后,前端没办法在正式接口之前进行mock。所以引入YApi,通过在Yapi上填写mock数据,进行数据的模拟。
jekins升级 docker/k8s
因为以往的构建,都是在一个项目中,现在微前端允许多框架,多环境构建。为了解决环境不统一的问题,引入了docker部署,对于多环境/多机器进行部署。
前端监控 sentry
Sentry 是一套开源的实时的异常收集、追踪、监控系统。这套解决方案由对应各种语言的 SDK 和一套庞大的数据后台服务组成,通过 Sentry SDK 的配置,还可以上报错误关联的版本信息、发布环境。同时 Sentry SDK 会自动捕捉异常发生前的相关操作,便于后续异常追踪。异常数据上报到数据服务之后,会通过过滤、关键信息提取、归纳展示在数据后台的 Web 界面中。
和平时代
基于上一次的工业大爆发,前端已经趋于稳定,公司业务也趋于稳定,老板的兰博已经停在了楼下。
在逐步的追求效率,追求速度的时期结束后,逐渐迎来了和平发展时期。这时候,团队开始关注自动化和创新。
灰度发布/一键回滚 k8s
各服务厂商都有提供灰度发布平台,结合服务商自己的服务可以对前端代码进行多环境,多场景,多条件的灰度部署及运维。
比如腾讯的服务网格,阿里的serverless等均有各种不同的服务。最基本的部署服务,就是根据k8s部署了多个不同的pod节点,然后根据规则匹配,对灰度环境进行的流量进行管理。
自动化测试 sonic
基于自动化测试的云平台,可以帮助每次执行真机的自动化测试,保证主流程的准确稳定。
性能监控/优化 Lighthouse/Sentry
chrome自带的Lighthouse可以进行本地性能的分析,同时搭配Sentry可以进行日志监控。
低代码/营销搭建 lowcode engine
公司一定时间之后一定需要一套做营销搭建和内部页面组织的工具,阿里开源的 lowcode engine可以在低代码领域进行快速的迭代。
AI客服 kimi/文心/通义
结合公司的文本资料库,将内容喂给Ai后,可以生成自己的Ai客服,解答一些常见的问题。
我是天元,立志做
1000个有趣的项目
的前端,公众号:前端cssandjs
如果你喜欢的话,请
点赞,收藏,转发
。
归寂
已经跟随公司奋斗了几年的小帅,拿着合同终止通知书,看着老板的兰博大牛,觉得是时候应该向社会输送自己了。
来源:juejin.cn/post/7364971296163414050
一个迷茫的25岁前端程序员的自述
一直听说程序员的危机在 35 岁,没想到我的危机从 25 岁就开始了。
我甚至不知道自己是不是 25 岁,也可能是 26 岁,或者 27 岁,1998 年的生日,按照 2023 - 1998 的算法就是 25,按照我老家那边的算法就是 26,也有人说是 27 ,无所谓了。
看着自己的掘金,上一次沉下心来好好的写一篇文章还是六个月前,那时候为了跳槽找工作,又写网站,又写文章的,过了那阵后来就不行了。
再往后稀稀拉拉的也在草稿箱里写了一些东西,东一榔头西一棒子,但没有一篇能看的,能发布出来的。
今年掘金社区的年终数据肯定不会好看,当初还想着要一年比一年强,结果一年快过去了了也没发布几篇文章,数据也都一般。
就连本文也是硬着头皮强迫自己写的,看看能不能借此调整调整心态。
也看过一些鸡汤文,像什么写给迷茫的xxx啊,写给年轻人啊,说实话没半点哔用,什么让你调整心态,自我提升,学好这个学好那个,我但凡要有那个劲,也不会迷茫了。
技术停滞不前
刚入行的时候,完成工作后剩下的时间,都会去看看技术视频啊,看看别人的博客啊,再或者看看一些三方库的文档和源码啊。
也可能是因为那会实在太菜,连工作都完成不了,就得被迫去提升基础能力。
再后来当了个小组长,虽然小组只有仨人,但是毕竟责任在,能给别人分配分配任务啦,给别人解决点小 bug 啦,再或者以组长的身份整两句啦,说实话稍微有那么一点点优越感,也就是靠着这一点点优越感,天天嚷嚷着要当一个架构师,这也学学那也学学,还看完了一个架构师教程,确实学到了不少东西,实际也用到了不少。
后来到北京,再次成为了一个底层的外包 coder, 每天除了听话和写业务代码也不用管别的。
后面公司又开始搞低代码,这回连代码也不用写了,就天天去系统上拖着玩,写配置,真心没意思。
说心里话,以我现在的能力吧,应付业务肯定是没有问题的,简单的页面基本不用思考随便写了,稍微复杂一点的动动脑筋,遇到困难就上网搜搜解决方案,问问 chatgpt,最不济实在搞不出来了,找个大佬问两句,最后也都能解决。
总结起来就是,能干活吗?能干。 能力强吗?不强。
技术文章收藏了不少,掘金小册也买了,视频也保存了一大堆,各种各样的。现在的状态就是看不下去,视频看一会就走神了,讲的什么完全记不得,还要后退回去再听一遍,文字更是看不进去。没有耐心去看文档,看不明白就开始烦了,觉得自己真傻逼的同时又不愿意去研究。
看视频的时候记不住,也不愿意记笔记。看文字的时候看不懂,也不愿意去实际操作一下。
现在把工作完成了,就摸鱼,看新闻,到处逛逛,刷短视频,真的不愿意学一点。
下班之后的时间就更不用说了,根本不愿意动一点脑子。
我有时以为是不是我生活的太好了,有吃有喝,没有压力。但转念一想现在如果给我大的压力,以我现在的心态能不能支撑的住。
但平时也经常想,自己就是一个破的被人看不起的外包,跟正式员工区别对待的外包,身边比我强的人比比皆是,那些月薪几万的正式员工,还有组长,组长的组长。前进的路很长,但就是不想走,不知道怎么走。
想学习吧,不知道从何学起,确定了要学习的内容吧,学不下去。
博客也不写了,开源仓库也不维护了,是躺平,亦或是摆烂,每天看着窗外发呆也占据了大部分时间。
生活百无聊赖
工作没有动力,生活亦是如此。
我现在工作的地方是北京顺义,住的地方就算是个村子,村子外面就是野外,没有一点城市的车水马龙和霓虹招展,一到黑天除了路灯照到的地方就是黑漆漆,没有广场,也没有广场舞。
每天下班回家就待在家里玩游戏,看电视剧。不知道会不会有人和我一样,玩游戏和看电视本应该是消遣娱乐的事情,但我感觉到的只有无聊,玩游戏的时候无聊,看电视剧的时候也一样。但你又不能什么都不干,你又得干点什么事情来让时间过度到需要睡觉的时候。
饭也不用自己做,公司的食堂一天三顿饭都有,早上要花钱买,一顿六七块钱左右,中午和晚上免费,不吃白不吃,每天六点下班,七点开饭,我都要在公司多摸一小时的鱼。也不用思考吃什么,在食堂供应的几种餐食里面选一个也不是什么困难的事情。
我这个人没有什么兴趣爱好,熬过上班的时间,就是玩游戏,看电视,看直播。也只是消磨时间,谈不上兴致勃勃。就没有上学时候宁可挨打也要偷摸去网吧通宵玩游戏的那个心境了。
其次,也没有什么社交,老家有几个朋友,平时在群里瞎聊两句,过年的时候一起吃个饭,也没别的了。之前在大连有几个朋友,现在也是极不频繁的闲聊两句。到北京以后呢,没朋友,没亲戚,自己一个人住单间,也没有室友。
也没有什么圈子,游戏基本上都是单排,就连游戏交流群都没加一个,有的时候发挥好了,别的人主动加我好友就一起玩两把。
也不爱运动,健身更是无稽之谈,每天最大的运动量就是从家骑自行车到公司,再从公司骑自行车回家,还是因为我不想早起挤公交。
平时极不愿意与陌生人打交道,路上碰见不太熟的都会装没看见。可以说是内向吧,也可以说社恐,甚至说我孤僻也没觉得有什么不妥。
但我一直都是这样吗,好像也不是,我大学时候是学院的辩论队教练,在数百人的场地里演讲,宣讲,打辩论赛,教学,做评委述票都迎刃有余,各个学院都有熟人,一块玩桌游,狼人杀,有很多朋友也爱交朋友。
似乎就是从毕业以后,又或许是分手以后,又或许是来到北京以后,短短的数月时间,感觉自己老了很多岁一样。
你问我是放不下她吗,好像也不是,平时也不怎么想,甚至连长什么样都记不清了。
那是一种什么感觉,大概就是放下她了,但没放下失去她吧。
假期回家,很多人说给我介绍对象,又期待又觉得无所谓,或许他们只是寒暄吧,也或许真的会介绍。不想,不愿意,又或者不耐烦去重新认识一个人。
假期在家的时候,以前不屑一顾的小夜市变成了我的一处乌托邦,太长时间没见过的市井气息,小吃摊前围绕的吃客,卖玩具的老板在尽力展示,还有踩高跷的表演,扭秧歌的队伍。还有一伙一伙的广场舞,我不跳,但我爱看,爱看大爷大妈们乐在其中的表情。
哪一瞬间开始觉得自己是个废物
我在朋友圈问过这个问题,得到的答案基本都是 “每一瞬间”,或许他们只是在说笑,也或许有的人真的这么想。但对我来说不是这样的。
我以前一直都自我感觉挺良好的。或者说自我感觉不错。
大学以前的成绩都是中等偏上水平,成绩还行,也拿奖状什么的,经常被夸奖。
大学也是一个普通本科,比上不足比下有余吧,虽然大学挂了很多科,但恰好赶上疫情,学校降低了毕业要求,顺利毕业了。
毕业以后找不到工作,交了 200 的定金准备去培训公司培训了,然后面试的最后一家告诉我通过了,我就没去培训。
边工作边学习,后来跳槽还当了个小组长,工资翻了接近一倍。
第一个对象在大一认识的,谈了一年。后来这个是疫情期间认识的,谈了三年。
家里条件一般,但是能吃饱饭,父母自给自足也不用我支援。
当时的我工作不愁,感情不愁,身体健康,生活没有压力。说自我感觉良好没有什么问题吧。
从小被灌输的概念都是近朱者赤近墨者黑,身边的人优秀才会激励你进步,但对我来说,身边优秀的人越多越容易摆烂。
以前在小公司当组长的时候更愿意去提升,现在当底层反而想躺平。
当你从最优秀的那些人中的一员成为最差的人中的一个,落差是很大的,体验感是十分差劲的。虽然工资翻了一倍,但生活变差了很多倍。
20 岁的时候单身,觉得无所谓,成天就是玩,30 岁的时候单身就会焦虑,担心自己能不能找到对象,或者说随着年龄越来越大,越担心能不能找到一个好女孩。
无论是分手,还是当外包,其实都还好,虽然说心情不太美妙,但一直也没对自己产生这么大的恶意。
直到国庆放假回家,我叔叔家有个小弟,比我小几岁。他的两个同学来接他同学聚会。俩孩子开着车,拿着伴手礼,奶,酒,还有水果。跟我叔叔我爸他们一起喝酒,言谈举止,人情世故活脱脱是个小大人,几杯白酒下肚啥事没有。来到陌生的村子跟几个陌生的人打麻将一点不紧张,输赢先不说,就这份勇气我就没有。明明我比他们要大几岁,可在他们面前我反而像个小孩子。
就在那一瞬间,我才意识到我是多么的没用,老话讲人比人得死,货比货得扔不是没有道理的,不需要别人来对比,自己都会自愧不如。
驾-照考了好几年了,却连车也不会开,要说买一个二手的便宜的车开,确实能买得起,但是不敢。
三十来岁的人了,别人都开着车到处跑,我还停留在父母去哪,我就跟着去哪的阶段。
我还给自己找借口,人家是租车公司的经理,每天跟各种人打交道,我就是个坐办公室的,没有什么历练。
怪性格也好,赖工作性质也罢。说白了就是没见过什么世面,没有什么阅历,三十来岁的年龄,十几岁孩子的见识。
说一句废物一点也不为过吧。
羡慕这两个字我已经说够了
大学玩的最好的朋友,现在在大连,经常跟我秀,自己有车,有房,有媳妇,媳妇有店,在要孩子。
再看看我自己,没车,没房,没媳妇,在村里租房子,在排位上分。
我虽然表面不屑一顾,说一句厉害坏了,但心里只有羡慕。
正式员工隔三差五发福利,半袖,外套,纪念品,外包什么都没有,只有羡慕。
国庆放假,没抢到高铁票,在公司拼了个车回去。
车是什么牌子,不知道,只知道看起来很好,应该不便宜。同事开车,闲聊中得知他比我小一岁,喊我哥,带着自己对象回家过节。
我坐在后排,闭着眼。坐着比我小一岁,领着对象回家过节,开着看起来不便宜的同事的车,心里只有羡慕。
回家吃婚宴,看着布置好的新房和现场的典礼,没有心思截门要红包,心里只有羡慕。
我从来不羡慕那些有钱人,富二代,也不羡慕那些天才,超能力,我觉得他们离我太远,跟我没有关系。但我真的羡慕明明跟我差不多,但是比我懂事的孩子。
羡慕这两个字,我已经说够了。
写在最后
现在的心态就是活着没意思,又不敢死。
说到底,我现在的生活已经比一部分人要好了,我也知道有很多人羡慕我这样的生活,也有很多不如我的人心态比我要阳光,要积极。
或许我是在无病呻吟,或许我是生在福中不知福,又或许我是闲的没事,但我只是把我内心的真实想法表达出来而已,我无意伤害任何人,包括我自己。
本来想在最后贴上自己的开源项目地址,要一些 star 的,想想还是算了。
就希望自己能快些振作起来,让生活走上正轨吧。
至于这要花费多长时间,我也不得而知,希望很快吧。
来源:juejin.cn/post/7288300174913159222
一种适合H5屏幕适配方案
一、动态rem适配方案:适合H5项目的适配方案
1. @media媒体查询适配
首先,我们需要设置一个根元素的基准值,这个基准值通常根据视口宽度进行计算。可以在项目的 CSS 文件中,通过媒体查询动态调整根元素的 font-size
。
html {
font-size: 16px; /* 默认基准值 */
}
...
@media (min-width: 1024px) {
html {
font-size: 14px; /* 适配较大屏幕 */
}
}
@media (min-width: 1440px) {
html {
font-size: 16px; /* 适配超大屏幕 */
}
}
2. PostCSS 插件(自动转换)实现 px2rem
手动转换 px
为 rem
可能很繁琐,因此可以使用 PostCSS
插件 postcss-pxtorem
来自动完成这一转换。
2.1 安装 postcss-pxtorem
首先,在项目中安装 postcss-pxtorem 插件:
npm install postcss-pxtorem --save-dev
2.2 配置 PostCSS
然后,在项目根目录创建或编辑 postcss.config.js 文件,添加 postcss-pxtorem 插件配置:
/* postcss.config.cjs */
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue: 16, // 基准值,对应于根元素的 font-size
unitPrecision: 5, // 保留小数点位数
propList: ['*', '!min-width', '!max-width'], // 排除 min-width 和 max-width 属性
selectorBlackList: [], // 忽略的选择器
replace: true, // 替换而不是添加备用属性
mediaQuery: false, // 允许在媒体查询中转换 px
minPixelValue: 0 // 最小的转换数值
}
}
};
/* vite */
export default defineConfig({
css: {
postcss: './postcss.config.cjs',
}
})
3. 在 CSS/SCSS 中使用 px
在编写样式时,依然可以使用 px
进行布局:
.container {
width: 320px;
padding: 16px;
}
.header {
height: 64px;
margin-bottom: 24px;
}
4. 构建项目
通过构建工具(如 webpack/vite
)运行项目时,PostCSS
插件会自动将 px
转换为 rem
。
5. 可以不用@media媒体查询,动态动态调整font-size
为了实现更动态的适配,可以通过 JavaScript
动态设置根元素的 font-size
:
/**utils/setRootFontSize**/
function setRootFontSize(): void {
const docEl = document.documentElement;
const clientWidth = docEl.clientWidth;
if (!clientWidth) return;
const baseFontSize = 16; // 基准字体大小
const designWidth = 1920; // 设计稿宽度
docEl.style.fontSize = (baseFontSize * (clientWidth / designWidth)) + 'px';
}
export default setRootFontSize;
/**utils/setRootFontSize**/
/**APP**/
import setRootFontSize from '../utils/setRootFontSize';
import { useEffect } from 'react';
export default function App() {
useEffect(() => {
// 设置根元素的字体大小
setRootFontSize();
// 窗口大小改变时重新设置
window.addEventListener('resize', setRootFontSize);
// 清除事件监听器
return () => {
window.removeEventListener('resize', setRootFontSize);
};
}, []);
return (
<>
<div>
<MyRoutes />
</div>
</>
)
}
/**APP**/
这样,无论视口宽度如何变化,页面元素都会根据基准值动态调整大小,确保良好的适配效果。
通过上述步骤,可以实现布局使用 px
,并动态转换为 rem
的适配方案。这个方案不仅使得样式编写更加简洁,还提高了适配的灵活性。
注:如果你使用了 setRootFontSize 动态调整根元素的 font-size
,就不再需要使用 @media 查询来调整根元素的字体大小了。这是因为 setRootFontSize
函数已经根据视口宽度动态调整了 font-size,从而实现了自适应。
- 动态调整根元素
font-size
的优势:
- 更加灵活:可以实现更加平滑的响应式调整,而不是依赖固定的断点。
- 统一管理:所有的样式都依赖根元素的 font-size,维护起来更加简单。
@media
媒体查询的优势:
- 尽管不再需要用
@media
查询来调整根元素的font-size
,但你可能仍然需要使用@media
查询来处理其他的响应式设计需求,比如调整布局、隐藏或显示元素等。
- 尽管不再需要用
这种方式简化了响应式设计,使得样式统一管理更加简单,同时保留了灵活性和适应性。
6. 效果对比(非H5界面)
图一为界面px
适配,效果为图片,文字等大小固定不变。
图二为动态rem
适配:整体随界面扩大而扩大,能够保持相对比例。
7. Tips
- 动态
rem
此方案比较适合H5屏幕适配 - 注意:
PostCSS
转换rem
应排除min-width
、max-width
、min-height
和max-height
,以免影响整体界面
二、其他适配
1. 弹性盒模型(Flexbox)
Flexbox
是一种布局模型,能够轻松地实现响应式布局。它允许元素根据容器的大小自动调整位置和大小。
.container {
display: flex;
flex-wrap: wrap;
}
.item {
flex: 1 1 100%; /* 默认情况下每个元素占满一行 */
}
@media (min-width: 600px) {
.item {
flex: 1 1 50%; /* 在较宽的屏幕上,每个元素占半行 */
}
}
@media (min-width: 1024px) {
.item {
flex: 1 1 33.33%; /* 在更宽的屏幕上,每个元素占三分之一行 */
}
}
2. 栅格系统(Grid System)
栅格系统是一种常见的响应式布局方案,广泛应用于各种框架(如 Bootstrap
)。通过定义行和列,可以轻松地创建复杂的布局。
.container {
display: grid;
grid-template-columns: 1fr; /* 默认情况下每行一个列 */
gap: 10px;
}
@media (min-width: 600px) {
.container {
grid-template-columns: 1fr 1fr; /* 在较宽的屏幕上,每行两个列 */
}
}
@media (min-width: 1024px) {
.container {
grid-template-columns: 1fr 1fr 1fr; /* 在更宽的屏幕上,每行三个列 */
}
}
3. 百分比和视口单位
使用百分比(%
)、视口宽度(vw
)、视口高度(vh
)等单位,可以根据视口尺寸调整元素大小。
/* 示例:百分比和视口单位 */
.container {
width: 100%;
height: 50vh; /* 高度为视口高度的一半 */
}
.element {
width: 50%; /* 宽度为容器的一半 */
height: 10vw; /* 高度为视口宽度的 10% */
}
4. 响应式图片
根据设备分辨率和尺寸加载不同版本的图片,以提高性能和视觉效果。可以使用 srcset 和 sizes 属性。
<!-- 示例:响应式图片 -->
<img
src="small.jpg"
srcset="medium.jpg 600w, large.jpg 1024w"
sizes="(max-width: 600px) 100vw, (max-width: 1024px) 50vw, 33.33vw"
alt="Responsive Image">
5. CSS Custom Properties(CSS变量)
使用 CSS 变量可以更灵活地定义和调整样式,同时通过 JavaScript
动态改变变量值实现响应式设计。
:root {
--main-padding: 20px;
}
.container {
padding: var(--main-padding);
}
@media (min-width: 600px) {
:root {
--main-padding: 40px;
}
}
来源:juejin.cn/post/7384265691162886178
利用高德地图API实现实时天气
前言
闲来无事,利用摸鱼时间实现实时天气的小功能
目录
效果图
这里样式我就不做处理了,地图可以不用做展示,只需要拿到获取到天气的结果,结合自己的样式展示就可以了,未来天气可以结合echarts进行展示,页面效果更佳
实现
- 登录高德开放平台控制台
- 创建 key
这里应用名称可以随便取(个人建议功能名称或者项目称)
3.获取 key 和密钥
4.获取当前城市定位
首先,先安装依赖
npm install @amap/amap-jsapi-loader --save
或者
pnpm add @amap/amap-jsapi-loader --save
页面使用时引入即可
import AMapLoader from "@amap/amap-jsapi-loader"
/**在index.html引入密钥,不添加会导致某些API调用不成功*/
<script type="text/javascript">window._AMapSecurityConfig =
{securityJsCode: "安全密钥"}</script>
/** 1. 调用AMapLoader.load方法,通过传入一个对象作为参数来指定加载地图时的配置信息。
* - key: 申请好的Web端开发者Key,是必填项,用于授权您的应用程序使用高德地图API。
* - version: 指定要加载的JSAPI版本,不指定时默认为1.4.15。
* - plugins: 需要使用的插件列表,如比例尺、缩放控件等。
*/
function initMap() {
AMapLoader.load({
key: "申请好的Web端开发者Key", // 申请好的Web端开发者Key,首次调用 load 时必填
version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
plugins: [
"AMap.ToolBar",
"AMap.Scale",
"AMap.HawkEye",
"AMap.MapType",
"AMap.Geolocation",
"AMap.Geocoder",
"AMap.Weather",
"AMap.CitySearch",
"AMap.InfoWindow",
"AMap.Marker",
"AMap.Pixel",
], // 需要使用的的插件列表,如比例尺'AMap.Scale'等
})
.then((AMap) => {
map.value = new AMap.Map("container", {
//设置地图容器id
resizeEnable: true,
viewMode: "3D", //是否为3D地图模式
zoom: 10, //初始化地图级别
center: locationArr.value, //初始化地图中心点位置
});
getGeolocation(AMap);
getCitySearch(AMap, map.value);
})
.catch((e) => {
console.log(e);
});
}
// 浏览器定位
const getGeolocation = (AMap: any) => {
const geolocation = new AMap.Geolocation({
enableHighAccuracy: true, //是否使用高精度定位,默认:true
timeout: 1000, //超过10秒后停止定位,默认:5s
position: "RB", //定位按钮的停靠位置
offset: [10, 20], //定位按钮与设置的停靠位置的偏移量,默认:[10, 20]
zoomToAccuracy: true, //定位成功后是否自动调整地图视野到定位点
});
map.value.addControl(geolocation);
geolocation.getCurrentPosition(function (status: string, result: any) {
if (status == "complete") {
onComplete(result);
} else {
onError(result);
}
});
};
// IP定位获取当前城市信息
const getCitySearch = (AMap: any, map: any) => {
const citySearch = new AMap.CitySearch();
citySearch.getLocalCity(function (
status: string,
result: {
city: string;
info: string;
}
) {
if (status === "complete" && result.info === "OK") {
console.log(
"🚀 ~ file: map-container.vue:88 ~ getCitySearch ~ result:",
result
);
// 查询成功,result即为当前所在城市信息
getWeather(AMap, map, result.city);
}
});
};
onMounted(() => {
initMap();
});
5.通过定位获取城市名称、区域编码,查询目标城市/区域的实时天气状况
const getWeather = (AMap: any, map: any, city: string) => {
const weather = new AMap.Weather();
weather.getLive(
city,
function (
err: any,
data: {
city: string;
weather: string;
temperature: string;
windDirection: string;
windPower: string;
humidity: string;
reportTime: string;
}
) {
if (!err) {
const str = [];
str.push("<h4 >实时天气" + "</h4><hr>");
str.push("<p>城市/区:" + data.city + "</p>");
str.push("<p>天气:" + data.weather + "</p>");
str.push("<p>温度:" + data.temperature + "℃</p>");
str.push("<p>风向:" + data.windDirection + "</p>");
str.push("<p>风力:" + data.windPower + " 级</p>");
str.push("<p>空气湿度:" + data.humidity + "</p>");
str.push("<p>发布时间:" + data.reportTime + "</p>");
const marker = new AMap.Marker({
map: map,
position: map.getCenter(),
});
const infoWin = new AMap.InfoWindow({
content:
'<div class="info" style="position:inherit;margin-bottom:0;background:#ffffff90;padding:10px">' +
str.join("") +
'</div><div class="sharp"></div>',
isCustom: true,
offset: new AMap.Pixel(0, -37),
});
infoWin.open(map, marker.getPosition());
marker.on("mouseover", function () {
infoWin.open(map, marker.getPosition());
});
}
}
);
}
完整代码
<template>
<div id="container"></div>
</template>
<script setup lang="ts">
import AMapLoader from "@amap/amap-jsapi-loader";
import { ref, onMounted, watch, reactive } from "vue";
const props = defineProps({
search: {
type: String,
default: "杭州市",
},
});
const isFalse = ref(false);
const map = ref<any>(null);
let locationArr = ref<any>();
watch(
() => props.search,
(newValue) => {
console.log("search", newValue);
initMap();
}
);
function initMap() {
AMapLoader.load({
key: "申请好的Web端开发者Key", // 申请好的Web端开发者Key,首次调用 load 时必填
version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
plugins: [
"AMap.ToolBar",
"AMap.Scale",
"AMap.HawkEye",
"AMap.MapType",
"AMap.Geolocation",
"AMap.Geocoder",
"AMap.Weather",
"AMap.CitySearch",
"AMap.InfoWindow",
"AMap.Marker",
"AMap.Pixel",
], // 需要使用的的插件列表,如比例尺'AMap.Scale'等
})
.then((AMap) => {
map.value = new AMap.Map("container", {
//设置地图容器id
resizeEnable: true,
viewMode: "3D", //是否为3D地图模式
zoom: 10, //初始化地图级别
center: locationArr.value, //初始化地图中心点位置
});
getGeolocation(AMap);
getCitySearch(AMap, map.value);
})
.catch((e) => {
console.log(e);
});
}
// 浏览器定位
const getGeolocation = (AMap: any) => {
const geolocation = new AMap.Geolocation({
enableHighAccuracy: true, //是否使用高精度定位,默认:true
timeout: 1000, //超过10秒后停止定位,默认:5s
position: "RB", //定位按钮的停靠位置
offset: [10, 20], //定位按钮与设置的停靠位置的偏移量,默认:[10, 20]
zoomToAccuracy: true, //定位成功后是否自动调整地图视野到定位点
});
map.value.addControl(geolocation);
geolocation.getCurrentPosition(function (status: string, result: any) {
if (status == "complete") {
onComplete(result);
} else {
onError(result);
}
});
};
// IP定位获取当前城市信息
const getCitySearch = (AMap: any, map: any) => {
const citySearch = new AMap.CitySearch();
citySearch.getLocalCity(function (
status: string,
result: {
city: string;
info: string;
}
) {
if (status === "complete" && result.info === "OK") {
console.log(
"🚀 ~ file: map-container.vue:88 ~ getCitySearch ~ result:",
result
);
// 查询成功,result即为当前所在城市信息
getWeather(AMap, map, result.city);
}
});
};
// 天气
const getWeather = (AMap: any, map: any, city: string) => {
const weather = new AMap.Weather();
weather.getLive(
city,
function (
err: any,
data: {
city: string;
weather: string;
temperature: string;
windDirection: string;
windPower: string;
humidity: string;
reportTime: string;
}
) {
console.log("🚀 ~ file: map-container.vue:96 ~ .then ~ data:", data);
if (!err) {
const str = [];
str.push("<h4 >实时天气" + "</h4><hr>");
str.push("<p>城市/区:" + data.city + "</p>");
str.push("<p>天气:" + data.weather + "</p>");
str.push("<p>温度:" + data.temperature + "℃</p>");
str.push("<p>风向:" + data.windDirection + "</p>");
str.push("<p>风力:" + data.windPower + " 级</p>");
str.push("<p>空气湿度:" + data.humidity + "</p>");
str.push("<p>发布时间:" + data.reportTime + "</p>");
const marker = new AMap.Marker({
map: map,
position: map.getCenter(),
});
const infoWin = new AMap.InfoWindow({
content:
'<div class="info" style="position:inherit;margin-bottom:0;background:#ffffff90;padding:10px">' +
str.join("") +
'</div><div class="sharp"></div>',
isCustom: true,
offset: new AMap.Pixel(0, -37),
});
infoWin.open(map, marker.getPosition());
marker.on("mouseover", function () {
infoWin.open(map, marker.getPosition());
});
}
}
);
// 未来4天天气预报
weather.getForecast(
city,
function (err: any, data: { forecasts: string | any[] }) {
console.log(
"🚀 ~ file: map-container.vue:186 ~ getWeather ~ data:",
data
);
if (err) {
return;
}
var strs = [];
for (var i = 0, dayWeather; i < data.forecasts.length; i++) {
dayWeather = data.forecasts[i];
strs.push(
`<p>${dayWeather.date}  ${dayWeather.dayWeather}  ${dayWeather.nightTemp}~${dayWeather.dayTemp}℃</p><br />`
);
}
}
);
};
function onComplete(data: any) {
console.log("🚀 ~ file: map-container.vue:107 ~ onComplete ~ data:", data);
const lngLat = [data.position.lng, data.position.lat];
locationArr.value = lngLat;
}
function onError(data: any) {
console.log("🚀 ~ file: map-container.vue:113 ~ onError ~ data:", data);
// 定位出错
}
onMounted(() => {
initMap();
});
</script>
<style scoped lang="less">
#container {
padding: 0px;
margin: 0px;
width: 100%;
height: 100%;
}
</style>
来源:juejin.cn/post/7316746866040619035
MySQL 高级(进阶)SQL 语句
MySQL 高级(进阶)SQL 语句
1. MySQL SQL 语句
1.1 常用查询
常用查询简单来说就是 增、删、改、查
对 MySQL 数据库的查询,除了基本的查询外,有时候需要对查询的结果集进行处理。 例如只取 10 条数据、对查询结果进行排序或分组等等
1、按关键字排序
PS:类比于windows 任务管理器
使用 SELECT 语句可以将需要的数据从 MySQL 数据库中查询出来,如果对查询的结果进行排序,可以使用 ORDER BY 语句来对语句实现排序,并最终将排序后的结果返回给用户。这个语句的排序不光可以针对某一个字段,也可以针对多个字段
(1)语法
SELECT column1, column2, … FROM table_name ORDER BY column1, column2, …
ASC
|DESC
ASC 是按照升序进行排序的,是默认的排序方式,即 ASC 可以省略。SELECT 语句中如果没有指定具体的排序方式,则默认按 ASC方式进行排序。
DESC 是按降序方式进 行排列。当然 ORDER BY 前面也可以使用 WHERE 子句对查询结果进一步过滤。
准备工作:
create database k1;
use k1;
create table location (Region char(20),Store_Name char(20));
insert int0 location values('East','Boston');
insert int0 location values('East','New York');
insert int0 location values('West','Los Angeles');
insert int0 location values('West','Houston');
create table store_info (Store_Name char(20),Sales int(10),Date char(10));
insert int0 store_info values('Los Angeles','1500','2020-12-05');
insert int0 store_info values('Houston','250','2020-12-07');
insert int0 store_info values('Los Angeles','300','2020-12-08');
insert int0 store_info values('Boston','700','2020-12-08');
1.2 SELECT
显示表格中一个或数个字段的所有数据记录 语法:SELECT "字段" FROM "表名";
SELECT Store_Name FROM location;
SELECT Store_Name FROM Store_Info;
1.3 DISTINCT
不显示重复的数据记录
语法:SELECT DISTINCT "字段" FROM "表名";
SELECT DISTINCT Store_Name FROM Store_Info;
1.4 AND OR
且 或
语法:SELECT "字段" FROM "表名" WHERE "条件1" {[AND|OR] "条件2"}+ ;
1.5 in
显示已知的值的数据记录
语法:SELECT "字段" FROM "表名" WHERE "字段" IN ('值1', '值2', ...);
SELECT * FROM store_info WHERE Store_Name IN ('Los Angeles', 'Houston');
1.6 BETWEEN
显示两个值范围内的数据记录
语法:SELECT "字段" FROM "表名" WHERE "字段" BETWEEN '值1' AND '值2';
2. 通配符 —— 通常与 LIKE 搭配 一起使用
% :百分号表示零个、一个或多个字符
_ :下划线表示单个字符
'A_Z':所有以 'A' 起头,另一个任何值的字符,且以 'Z' 为结尾的字符串。例如,'ABZ' 和 'A2Z' 都符合这一个模式,而 'AKKZ' 并不符合 (因为在 A 和 Z 之间有两个字符,而不是一个字符)。
'ABC%': 所有以 'ABC' 起头的字符串。例如,'ABCD' 和 'ABCABC' 都符合这个模式。 '%XYZ': 所有以 'XYZ' 结尾的字符串。例如,'WXYZ' 和 'ZZXYZ' 都符合这个模式。
'%AN%': 所有含有 'AN'这个模式的字符串。例如,'LOS ANGELES' 和 'SAN FRANCISCO' 都符合这个模式。
'_AN%':所有第二个字母为 'A' 和第三个字母为 'N' 的字符串。例如,'SAN FRANCISCO' 符合这个模式,而 'LOS ANGELES' 则不符合这个模式。
2.1 LIKE
匹配一个模式来找出我们要的数据记录
语法:SELECT "字段" FROM "表名" WHERE "字段" LIKE {模式};
SELECT * FROM store_info WHERE Store_Name like '%os%';
2.2 ORDER BY
按关键字排序
语法:SELECT "字段" FROM "表名" [WHERE "条件"] ORDER BY "字段" [ASC, DESC];
注意:ASC
是按照升序进行排序的,是默认的排序方式。 DESC
是按降序方式进行排序
SELECT Store_Name,Sales,Date FROM store_info ORDER BY Sales DESC;
3. 函数
3.1数学函数
abs(x) | 返回 x 的绝对值 |
---|---|
rand() | 返回 0 到 1 的随机数 |
mod(x,y) | 返回 x 除以 y 以后的余数 |
power(x,y) | 返回 x 的 y 次方 |
round(x) | 返回离 x 最近的整数 |
round(x,y) | 保留 x 的 y 位小数四舍五入后的值 |
sqrt(x) | 返回 x 的平方根 |
truncate(x,y) | 返回数字 x 截断为 y 位小数的值 |
ceil(x) | 返回大于或等于 x 的最小整数 |
floor(x) | 返回小于或等于 x 的最大整数 |
greatest(x1,x2...) | 返回集合中最大的值,也可以返回多个字段的最大的值 |
least(x1,x2...) | 返回集合中最小的值,也可以返回多个字段的最小的值 |
SELECT abs(-1), rand(), mod(5,3), power(2,3), round(1.89);
SELECT round(1.8937,3), truncate(1.235,2), ceil(5.2), floor(2.1), least(1.89,3,6.1,2.1);
3.2 聚合函数
avg() | 返回指定列的平均值 |
---|---|
count() | 返回指定列中非 NULL 值的个数 |
min() | 返回指定列的最小值 |
max() | 返回指定列的最大值 |
sum(x) | 返回指定列的所有值之和 |
SELECT avg(Sales) FROM store_info;
SELECT count(Store_Name) FROM store_info;
SELECT count(DISTINCT Store_Name) FROM store_info;
SELECT max(Sales) FROM store_info;
SELECT min(Sales) FROM store_info;
SELECT sum(Sales) FROM store_info;
3.3 字符串函数
trim() | 返回去除指定格式的值 |
---|---|
concat(x,y) | 将提供的参数 x 和 y 拼接成一个字符串 |
substr(x,y) | 获取从字符串 x 中的第 y 个位置开始的字符串,跟substring()函数作用相同 |
substr(x,y,z) | 获取从字符串 x 中的第 y 个位置开始长度为 z 的字符串 |
length(x) | 返回字符串 x 的长度 |
replace(x,y,z) | 将字符串 z 替代字符串 x 中的字符串 y |
upper(x) | 将字符串 x 的所有字母变成大写字母 |
lower(x) | 将字符串 x 的所有字母变成小写字母 |
left(x,y) | 返回字符串 x 的前 y 个字符 |
right(x,y) | 返回字符串 x 的后 y 个字符 |
repeat(x,y) | 将字符串 x 重复 y 次 |
space(x) | 返回 x 个空格 |
strcmp(x,y) | 比较 x 和 y,返回的值可以为-1,0,1 |
reverse(x) | 将字符串 x 反转 |
如 sql_mode 开启了 PIPES_AS_CONCAT,"||" 视为字符串的连接操作符而非或运算符,和字符串的拼接函数Concat相类似,这和Oracle数据库使用方法一样的
SELECT Region || ' ' || Store_Name FROM location WHERE Store_Name = 'Boston';
SELECT substr(Store_Name,3) FROM location WHERE Store_Name = 'Los Angeles';
SELECT substr(Store_Name,2,4) FROM location WHERE Store_Name = 'New York'
SELECT TRIM ([ [位置] [要移除的字符串] FROM ] 字符串);
**[位置]:的值可以为 LEADING (起头), TRAILING (结尾), BOTH (起头及结尾)。 **
[要移除的字符串]:从字串的起头、结尾,或起头及结尾移除的字符串。缺省时为空格。
SELECT TRIM(LEADING 'Ne' FROM 'New York');
SELECT Region,length(Store_Name) FROM location;
SELECT REPLACE(Region,'ast','astern')FROM location;
4. GR0UP BY
对GR0UP BY后面的字段的查询结果进行汇总分组,通常是结合聚合函数一起使用的
GR0UP BY 有一个原则
- 凡是在 GR0UP BY 后面出现的字段,必须在 SELECT 后面出现;
- 凡是在 SELECT 后面出现的、且未在聚合函数中出现的字段,必须出现在 GR0UP BY 后面
语法:SELECT "字段1", SUM("字段2") FROM "表名" GR0UP BY "字段1";
SELECT Store_Name, SUM(Sales) FROM store_info GR0UP BY Store_Name ORDER BY sales desc;
5. 别名
字段別名 表格別名
语法:SELECT "表格別名"."字段1" [AS] "字段別名" FROM "表格名" [AS] "表格別名";
SELECT A.Store_Name Store, SUM(A.Sales) "Total Sales" FROM store_info A GR0UP BY A.Store_Name;
6. 子查询
子查询也被称作内查询或者嵌套查询,是指在一个查询语句里面还嵌套着另一个查询语 句。子查询语句是先于主查询语句被执行的,其结果作为外层的条件返回给主查询进行下一 步的查询过滤
连接表格,在WHERE 子句或 HAVING 子句中插入另一个 SQL 语句
语法:SELECT "字段1" FROM "表格1" WHERE "字段2" [比较运算符] #外查询 (SELECT "字段1" FROM "表格2" WHERE "条件"); #内查询
[比较运算符]
可以是符号的运算符,例如 =、>、<、>=、<= ;也可以是文字的运算符,例如 LIKE、IN、BETWEEN
SELECT SUM(Sales) FROM store_info WHERE Store_Name IN
(SELECT Store_Name FROM location WHERE Region = 'West');
SELECT SUM(A.Sales) FROM store_info A WHERE A.Store_Name IN
(SELECT Store_Name FROM location B WHERE B.Store_Name = A.Store_Name);
7. EXISTS
用来测试内查询有没有产生任何结果,类似布尔值是否为真 #如果有的话,系统就会执行外查询中的SQL语句。若是没有的话,那整个 SQL 语句就不会产生任何结果。
语法:SELECT "字段1" FROM "表格1" WHERE EXISTS (SELECT \* FROM "表格2" WHERE "条件");
SELECT SUM(Sales) FROM store_info WHERE EXISTS (SELECT * FROM location WHERE Region = 'West');
8. 连接查询
准备工作
create database k1;
use k1;
create table location (Region char(20),Store_Name char(20));
insert int0 location values('East','Boston');
insert int0 location values('East','New York');
insert int0 location values('West','Los Angeles');
insert int0 location values('West','Houston');
create table store_info (Store_Name char(20),Sales int(10),Date char(10));
insert int0 store_info values('Los Angeles','1500','2020-12-05');
insert int0 store_info values('Houston','250','2020-12-07');
insert int0 store_info values('Los Angeles','300','2020-12-08');
insert int0 store_info values('Boston','700','2020-12-08');
UPDATE store_info SET store_name='Washington' WHERE sales=300;
inner join(内连接):只返回两个表中联结字段相等的行
left join(左连接):返回包括左表中的所有记录和右表中联结字段相等的记录
right join(右连接):返回包括右表中的所有记录和左表中联结字段相等的记录
8.1 内连接
MySQL 中的内连接就是两张或多张表中同时符合某种条件的数据记录的组合。通常在 FROM 子句中使用关键字 INNER JOIN 来连接多张表,并使用 ON 子句设置连接条件,内连接是系统默认的表连接,所以在 FROM 子句后可以省略 INNER 关键字,只使用 关键字 JOIN。同时有多个表时,也可以连续使用 INNER JOIN 来实现多表的内连接,不过为了更好的性能,建议最好不要超过三个表
(1) 语法 求交集
SELECT column_name(s)FROM table1 INNER JOIN table2 ON table1.column_name = table2.column_name;
SELECT * FROM location A INNER JOIN store_info B on A.Store_Name = B.Store_Name ;
内连查询:通过inner join 的方式将两张表指定的相同字段的记录行输出出来
8.2 左连接
左连接也可以被称为左外连接,在 FROM 子句中使用 LEFT JOIN 或者 LEFT OUTER JOIN 关键字来表示。左连接以左侧表为基础表,接收左表的所有行,并用这些行与右侧参 考表中的记录进行匹配,也就是说匹配左表中的所有行以及右表中符合条件的行。
SELECT * FROM location A LEFT JOIN store_info B on A.Store_Name = B.Store_Name ;
左连接中左表的记录将会全部表示出来,而右表只会显示符合搜索条件的记录,右表记录不足的地方均为 NULL
8.3 右连接
右连接也被称为右外连接,在 FROM 子句中使用 RIGHT JOIN 或者 RIGHT OUTER JOIN 关键字来表示。右连接跟左连接正好相反,它是以右表为基础表,用于接收右表中的所有行,并用这些记录与左表中的行进行匹配
SELECT * FROM location A RIGHT JOIN store_info B on A.Store_Name = B.Store_Name ;
9. UNION ----联集
将两个SQL语句的结果合并起来,两个SQL语句所产生的字段需要是同样的数据记录种类
UNION :生成结果的数据记录值将没有重复,且按照字段的顺序进行排序
语法:[SELECT 语句 1] UNION [SELECT 语句 2];
SELECT Store_Name FROM location UNION SELECT Store_Name FROM store_info;
UNION ALL :将生成结果的数据记录值都列出来,无论有无重复
语法:[SELECT 语句 1] UNION ALL [SELECT 语句 2];
SELECT Store_Name FROM location UNION ALL SELECT Store_Name FROM store_info;
9.1 交集值
取两个SQL语句结果的交集
SELECT A.Store_Name FROM location A INNER JOIN store_info B ON A.Store_Name = B.Store_Name;
SELECT A.Store_Name FROM location A INNER JOIN store_info B USING(Store_Name);
取两个SQL语句结果的交集,且没有重复
SELECT DISTINCT A.Store_Name FROM location A INNER JOIN store_info B USING(Store_Name);
SELECT DISTINCT Store_Name FROM location WHERE (Store_Name) IN (SELECT Store_Name FROM store_info);
SELECT DISTINCT A.Store_Name FROM location A LEFT JOIN store_info B USING(Store_Name) WHERE B.Store_Name IS NOT NULL;
SELECT A.Store_Name FROM (SELECT B.Store_Name FROM location B INNER JOIN store_info C ON B.Store_Name = C.Store_Name) A
GR0UP BY A.Store_Name;
SELECT A.Store_Name FROM
(SELECT DISTINCT Store_Name FROM location UNION ALL SELECT DISTINCT Store_Name FROM store_info) A
GR0UP BY A.Store_Name HAVING COUNT(*) > 1;
9.2 无交集值
显示第一个SQL语句的结果,且与第二个SQL语句没有交集的结果,且没有重复
SELECT DISTINCT Store_Name FROM location WHERE (Store_Name) NOT IN (SELECT Store_Name FROM store_info);
SELECT DISTINCT A.Store_Name FROM location A LEFT JOIN store_info B USING(Store_Name) WHERE B.Store_Name IS NULL;
SELECT A.Store_Name FROM
(SELECT DISTINCT Store_Name FROM location UNION ALL SELECT DISTINCT Store_Name FROM store_info) A
GR0UP BY A.Store_Name HAVING COUNT(*) = 1;
10. case
是 SQL 用来做为 IF-THEN-ELSE 之类逻辑的关键字
语法:
SELECT CASE ("字段名")
WHEN "条件1" THEN "结果1"
WHEN "条件2" THEN "结果2"
...
[ELSE "结果N"]
END
FROM "表名";
"条件" 可以是一个数值或是公式。 ELSE 子句则并不是必须的。
SELECT Store_Name, CASE Store_Name
WHEN 'Los Angeles' THEN Sales * 2
WHEN 'Boston' THEN 2000
ELSE Sales
END
"New Sales",Date
FROM store_info;
#"New Sales" 是用于 CASE 那个字段的字段名。
11. 正则表达式
匹配模式 | 描述 | 实例 |
---|---|---|
^ | 匹配文本的结束字符 | ‘^bd’ 匹配以 bd 开头的字符串 |
$ | 匹配文本的结束字符 | ‘qn$’ 匹配以 qn 结尾的字符串 |
. | 匹配任何单个字符 | ‘s.t’ 匹配任何 s 和 t 之间有一个字符的字符串 |
* | 匹配零个或多个在它前面的字符 | ‘fo*t’ 匹配 t 前面有任意个 o |
+ | 匹配前面的字符 1 次或多次 | ‘hom+’ 匹配以 ho 开头,后面至少一个m 的字符串 |
字符串 | 匹配包含指定的字符串 | ‘clo’ 匹配含有 clo 的字符串 |
p1|p2 | 匹配 p1 或 p2 | ‘bg|fg’ 匹配 bg 或者 fg |
[...] | 匹配字符集合中的任意一个字符 | ‘[abc]’ 匹配 a 或者 b 或者 c |
[^...] | 匹配不在括号中的任何字符 | ‘[^ab]’ 匹配不包含 a 或者 b 的字符串 |
{n} | 匹配前面的字符串 n 次 | ‘g{2}’ 匹配含有 2 个 g 的字符串 |
{n,m} | 匹配前面的字符串至少 n 次,至多m 次 | ‘f{1,3}’ 匹配 f 最少 1 次,最多 3 次 |
语法:SELECT "字段" FROM "表名" WHERE "字段" REGEXP {模式};
SELECT * FROM store_info WHERE Store_Name REGEXP 'os';
SELECT * FROM store_info WHERE Store_Name REGEXP '^[A-G]';
SELECT * FROM store_info WHERE Store_Name REGEXP 'Ho|Bo';
12. 存储过程
存储过程是一组为了完成特定功能的SQL语句集合。
存储过程在使用过程中是将常用或者复杂的工作预先使用SQL语句写好并用一个指定的名称存储起来,这个过程经编译和优化后存储在数据库服务器中。当需要使用该存储过程时,只需要调用它即可。存储过程在执行上比传统SQL速度更快、执行效率更高。
存储过程的优点:
1、执行一次后,会将生成的二进制代码驻留缓冲区,提高执行效率
2、SQL语句加上控制语句的集合,灵活性高
3、在服务器端存储,客户端调用时,降低网络负载
4、可多次重复被调用,可随时修改,不影响客户端调用
5、可完成所有的数据库操作,也可控制数据库的信息访问权限
12.1 创建存储过程
DELIMITER $$ #将语句的结束符号从分号;临时改为两个$$(可以是自定义)
CREATE PROCEDURE Proc() #创建存储过程,过程名为Proc,不带参数
-> BEGIN #过程体以关键字 BEGIN 开始
-> select * from Store_Info; #过程体语句
-> END $$ #过程体以关键字 END 结束
DELIMITER ; #将语句的结束符号恢复为分号
实例
DELIMITER $$ #将语句的结束符号从分号;临时改为两个$$(可以自定义)
CREATE PROCEDURE Proc5() #创建存储过程,过程名为Proc5,不带参数
-> BEGIN #过程体以关键字 BEGIN 开始
-> create table user (id int (10), name char(10),score int (10));
-> insert int0 user values (1, 'cyw',70);
-> select * from cyw; #过程体语句
-> END $$ #过程体以关键字 END 结束
DELIMITER ; #将语句的结束符号恢复为分号
12.2 调用存储过程
CALL Proc;
12.3 查看存储过程
SHOW CREATE PROCEDURE [数据库.]存储过程名; #查看某个存储过程的具体信息
SHOW CREATE PROCEDURE Proc;
SHOW PROCEDURE STATUS [LIKE '%Proc%'] \G
12.4 存储过程的参数
**IN 输入参数:**表示调用者向过程传入值(传入值可以是字面量或变量)
**OUT 输出参数:**表示过程向调用者传出值(可以返回多个值)(传出值只能是变量)
**INOUT 输入输出参数:**既表示调用者向过程传入值,又表示过程向调用者传出值(值只能是变量)
DELIMITER $$
CREATE PROCEDURE Proc6(IN inname CHAR(16))
-> BEGIN
-> SELECT * FROM store_info WHERE Store_Name = inname;
-> END $$
DELIMITER ;
CALL Proc6('Boston');
12.5 修改存储过程
ALTER PROCEDURE <过程名>[<特征>... ]
ALTER PROCEDURE GetRole MODIFIES SQL DATA SQL SECURITY INVOKER;
MODIFIES sQLDATA:表明子程序包含写数据的语句
SECURITY:安全等级
invoker:当定义为INVOKER时,只要执行者有执行权限,就可以成功执行。
12.6 删除存储过程
存储过程内容的修改方法是通过删除原有存储过程,之后再以相同的名称创建新的存储过程。如果要修改存储过程的名称,可以先删除原存储过程,再以不同的命名创建新的存储过程。
DROP PROCEDURE IF EXISTS Proc;
#仅当存在时删除,不添加 IF EXISTS 时,如果指定的过程不存在,则产生一个错误
13. 条件语句
if-then-else ···· end if
mysql> delimiter $$
mysql>
mysql> CREATE PROCEDURE proc8(IN pro int)
->
-> begin
->
-> declare var int;
-> set var=pro*2;
-> if var>=10 then
-> update t set id=id+1;
-> else
-> update t set id=id-1;
-> end if;
-> end $$
mysql> delimiter ;
14. 循环语句
while ···· end while
mysql> delimiter $$
mysql>
mysql> create procedure proc9()
-> begin
-> declare var int(10);
-> set var=0;
-> while var<6 do
-> insert int0 t values(var);
-> set var=var+1;
-> end while;
-> end $$
mysql> delimiter ;
15. 视图表 create view
15.1 视图表概述
视图,可以被当作是虚拟表或存储查询。
视图跟表格的不同是,表格中有实际储存数据记录,而视图是建立在表格之上的一个架构,它本身并不实际储存数据记录。
临时表在用户退出或同数据库的连接断开后就自动消失了,而视图不会消失。
视图不含有数据,只存储它的定义,它的用途一般可以简化复杂的查询。
比如你要对几个表进行连接查询,而且还要进行统计排序等操作,写sql语句会很麻烦的,用视图将几个表联结起来,然后对这个视图进行查询操作,就和对一个表查询一样,很方便。
15.2 视图表能否修改?
首先我们需要知道,视图表保存的是select语句的定义,所以视图表可不可以修改需要视情况而定。
- 如果 select 语句查询的字段是没有被处理过的源表字段,则可以通过视图表修改源表数据;
- 如果select 语句查询的字段是被 gr0up by语句或 函数 处理过的字段,则不可以直接修改视图表的数据。
create view v_store_info as select store_name,sales from store_info;
update v_store_info set sales=1000 where store_name='Houston';
create view v_sales as select store_name,sum(sales) from store_info gr0up by store_name having sum(sales)>1000;
update v_sales set store_name='xxxx' where store_name='Los Angeles';
15.3 基本语法
15.3.1 创建视图表
语法
create view "视图表名" as "select 语句";
create view v_region_sales as select a.region region,sum(b.sales) sales from location a
inner join store_info b on a.store_name = b.store_name gr0up by region;
15.4 查看视图表
语法
select * from 视图表名;
select * from v_region_sales;
15.5 删除视图表
语法
drop view 视图表名;
drop view v_region_sales;
15.6 通过视图表求无交集值
将两个表中某个字段的不重复值进行合并
只出现一次(count =1 ) ,即无交集
通过
create view 视图表名 as select distinct 字段 from 左表 union all select distinct 字段 from 右表;
select 字段 from 视图表名 gr0up by 字段 having count(字段)=1;
#先建立视图表
create viem v_union as select distinct store_name from location union all select distinct store_name from store_info;
#再通过视图表求无交集
select store_name from v_union gr0up by store_name having count(*)=1;
来源:juejin.cn/post/7291952951047929868
哇塞,新来个架构师,把Nacos注册中心讲得炉火纯青,佩服佩服~~
大家好,我是三友~~
今天就应某位小伙伴的要求,来讲一讲Nacos作为服务注册中心底层的实现原理
不知你是否跟我一样,在使用Nacos时有以下几点疑问:
- 临时实例和永久实例是什么?有什么区别?
- 服务实例是如何注册到服务端的?
- 服务实例和服务端之间是如何保活的?
- 服务订阅是如何实现的?
- 集群间数据是如何同步的?CP还是AP?
- Nacos的数据模型是什么样的?
- ...
本文就通过探讨上述问题来探秘Nacos服务注册中心核心的底层实现原理。
虽然Nacos最新版本已经到了2.x版本,但是为了照顾那些还在用1.x版本的同学,所以本文我会同时去讲1.x版本和2.x版本的实现
临时实例和永久实例
临时实例和永久实例在Nacos中是一个非常非常重要的概念
之所以说它重要,主要是因为我在读源码的时候发现,临时实例和永久实例在底层的许多实现机制是完全不同的
临时实例
临时实例在注册到注册中心之后仅仅只保存在服务端内部一个缓存中,不会持久化到磁盘
这个服务端内部的缓存在注册中心届一般被称为服务注册表
当服务实例出现异常或者下线之后,就会把这个服务实例从服务注册表中剔除
永久实例
永久服务实例不仅仅会存在服务注册表中,同时也会被持久化到磁盘文件中
当服务实例出现异常或者下线,Nacos只会将服务实例的健康状态设置为不健康,并不会对将其从服务注册表中剔除
所以这个服务实例的信息你还是可以从注册中心看到,只不过处于不健康状态
这是就是两者最最最基本的区别
当然除了上述最基本的区别之外,两者还有很多其它的区别,接下来本文还会提到
这里你可能会有一个疑问
为什么Nacos要将服务实例分为临时实例和永久实例?
主要还是因为应用场景不同
临时实例就比较适合于业务服务,服务下线之后可以不需要在注册中心中查看到
永久实例就比较适合需要运维的服务,这种服务几乎是永久存在的,比如说MySQL、Redis等等
MySQL、Redis等服务实例可以通过SDK手动注册
对于这些服务,我们需要一直看到服务实例的状态,即使出现异常,也需要能够查看时实的状态
所以从这可以看出Nacos跟你印象中的注册中心不太一样,他不仅仅可以注册平时业务中的实例,还可以注册像MySQL、Redis这个服务实例的信息到注册中心
在SpringCloud环境底下,一般其实都是业务服务,所以默认注册服务实例都是临时实例
当然如果你想改成永久实例,可以通过下面这个配置项来完成
spring
cloud:
nacos:
discovery:
#ephemeral单词是临时的意思,设置成false,就是永久实例了
ephemeral: false
这里还有一个小细节
在1.x版本中,一个服务中可以既有临时实例也有永久实例,服务实例是永久还是临时是由服务实例本身决定的
但是2.x版本中,一个服务中的所有实例要么都是临时的要么都是永久的,是由服务决定的,而不是具体的服务实例
所以在2.x可以说是临时服务和永久服务
为什么2.x把临时还是永久的属性由实例本身决定改成了由服务决定?
其实很简单,你想想,假设对一个MySQL服务来说,它的每个服务实例肯定都是永久的,不会出现一些是永久的,一些是临时的情况吧
所以临时还是永久的属性由服务本身决定其实就更加合理了
服务注册
作为一个服务注册中心,服务注册肯定是一个非常重要的功能
所谓的服务注册,就是通过注册中心提供的客户端SDK(或者是控制台)将服务本身的一些元信息,比如ip、端口等信息发送到注册中心服务端
服务端在接收到服务之后,会将服务的信息保存到前面提到的服务注册表中
1、1.x版本的实现
在Nacos在1.x版本的时候,服务注册是通过Http接口实现的
代码如下
整个逻辑比较简单,因为Nacos服务端本身就是用SpringBoot写的
但是在2.x版本的实现就比较复杂了
2、2.x版本的实现
2.1、通信协议的改变
2.x版本相比于1.x版本最主要的升级就是客户端和服务端通信协议的改变,由1.x版本的Http改成了2.x版本gRPC
gRPC是谷歌公司开发的一个高性能、开源和通用的RPC框架,Java版本的实现底层也是基于Netty来的
之所以改成了gRPC,主要是因为Http请求会频繁创建和销毁连接,白白浪费资源
所以在2.x版本之后,为了提升性能,就将通信协议改成了gRPC
根据官网显示,整体的效果还是很明显,相比于1.x版本,注册性能总体提升至少2倍
虽然通信方式改成了gRPC,但是2.x版本服务端依然保留了Http注册的接口,所以用1.x的Nacos SDK依然可以注册到2.x版本的服务端
2.2、具体的实现
Nacos客户端在启动的时候,会通过gRPC跟服务端建立长连接
这个连接会一直存在,之后客户端与服务端所有的通信都是基于这个长连接来的
当客户端发起注册的时候,就会通过这个长连接,将服务实例的信息发送给服务端
服务端拿到服务实例,跟1.x一样,也会存到服务注册表
除了注册之外,当注册的是临时实例时,2.x还会将服务实例信息存储到客户端中的一个缓存中,供Redo操作
所谓的Redo操作,其实就是一个补偿机制,本质是个定时任务,默认每3s执行一次
这个定时任务作用是,当客户端与服务端重新建立连接时(因为一些异常原因导致连接断开)
那么之前注册的服务实例肯定还要继续注册服务端(断开连接服务实例就会被剔除服务注册表)
所以这个Redo操作一个很重要的作用就是重连之后的重新注册的作用
除了注册之外,比如服务订阅之类的操作也需要Redo操作,当连接重新建立,之前客户端的操作都需要Redo一下
小总结
1.x版本是通过Http协议来进行服务注册的
2.x由于客户端与服务端的通信改成了gRPC长连接,所以改成通过gRPC长连接来注册
2.x比1.x多个Redo操作,当注册的服务实例是临时实例是,出现网络异常,连接重新建立之后,客户端需要将服务注册、服务订阅之类的操作进行重做
这里你可能会有个疑问
既然2.x有Redo机制保证客户端与服务端通信正常之后重新注册,那么1.x有类似的这种Redo机制么?
当然也会有,接下往下看。
心跳机制
心跳机制,也可以被称为保活机制,它的作用就是服务实例告诉注册中心我这个服务实例还活着
在正常情况下,服务关闭了,那么服务会主动向Nacos服务端发送一个服务下线的请求
Nacos服务端在接收到请求之后,会将这个服务实例从服务注册表中剔除
但是对于异常情况下,比如出现网络问题,可能导致这个注册的服务实例无法提供服务,处于不可用状态,也就是不健康
而此时在没有任何机制的情况下,服务端是无法知道这个服务处于不可用状态
所以为了避免这种情况,一些注册中心,就比如Nacos、Eureka,就会用心跳机制来判断这个服务实例是否能正常
在Nacos中,心跳机制仅仅是针对临时实例来说的,临时实例需要靠心跳机制来保活
心跳机制在1.x和2.x版本的实现也是不一样的
1.x心跳实现
在1.x中,心跳机制实现是通过客户端和服务端各存在的一个定时任务来完成的
在服务注册时,发现是临时实例,客户端会开启一个5s执行一次的定时任务
这个定时任务会构建一个Http请求,携带这个服务实例的信息,然后发送到服务端
在Nacos服务端也会开启一个定时任务,默认也是5s执行一次,去检查这些服务实例最后一次心跳的时间,也就是客户端最后一次发送Http请求的时间
- 当最后一次心跳时间超过15s,但没有超过30s,会把这服务实例标记成不健康
- 当最后一次心跳超过30s,直接把服务从服务注册表中剔除
这就是1.x版本的心跳机制,本质就是两个定时任务
其实1.x的这个心跳还有一个作用,就是跟上一节说的gRPC时Redo操作的作用是一样的
服务在处理心跳的时候,发现心跳携带这个服务实例的信息在注册表中没有,此时就会添加到服务注册表
所以心跳也有Redo的类似效果
2.x心跳实现
在2.x版本之后,由于通信协议改成了gRPC,客户端与服务端保持长连接,所以2.x版本之后它是利用这个gRPC长连接本身的心跳来保活
一旦这个连接断开,服务端就会认为这个连接注册的服务实例不可用,之后就会将这个服务实例从服务注册表中提出剔除
除了连接本身的心跳之外,Nacos还有服务端的一个主动检测机制
Nacos服务端也会启动一个定时任务,默认每隔3s执行一次
这个任务会去检查超过20s没有发送请求数据的连接
一旦发现有连接已经超过20s没发送请求,那么就会向这个连接对应的客户端发送一个请求
如果请求不通或者响应失败,此时服务端也会认为与客户端的这个连接异常,从而将这个客户端注册的服务实例从服务注册表中剔除
所以对于2.x版本,主要是两种机制来进行保活:
- 连接本身的心跳机制,断开就直接剔除服务实例
- Nacos主动检查机制,服务端会对20s没有发送数据的连接进行检查,出现异常时也会主动断开连接,剔除服务实例
小总结
心跳机制仅仅针对临时实例而言
1.x心跳机制是通过客户端和服务端两个定时任务来完成的,客户端定时上报心跳信息,服务端定时检查心跳时间,超过15s标记不健康,超过30s直接剔除
1.x心跳机制还有类似2.x的Redo作用,服务端发现心跳的服务信息不存在会,会将服务信息添加到注册表,相当于重新注册了
2.x是基于gRPC长连接本身的心跳机制和服务端的定时检查机制来的,出现异常直接剔除
健康检查
前面说了,心跳机制仅仅是临时实例用来保护的机制
而对于永久实例来说,一般来说无法主动上报心跳
就比如说MySQL实例,肯定是不会主动上报心跳到Nacos的,所以这就导致无法通过心跳机制来保活
所以针对永久实例的情况,Nacos通过一种叫健康检查的机制去判断服务实例是否活着
健康检查跟心跳机制刚好相反,心跳机制是服务实例向服务端发送请求
而所谓的健康检查就是服务端主动向服务实例发送请求,去探测服务实例是否活着
健康检查机制在1.x和2.x的实现机制是一样的
Nacos服务端在会去创建一个健康检查任务,这个任务每次执行时间间隔会在2000~7000毫秒之间
当任务触发的时候,会根据设置的健康检查的方式执行不同的逻辑,目前主要有以下三种方式:
- TCP
- HTTP
- MySQL
TCP的方式就是根据服务实例的ip和端口去判断是否能连接成功,如果连接成功,就认为健康,反之就任务不健康
HTTP的方式就是向服务实例的ip和端口发送一个Http请求,请求路径是需要设置的,如果能正常请求,说明实例健康,反之就不健康
MySQL的方式是一种特殊的检查方式,他可以执行下面这条Sql来判断数据库是不是主库
默认情况下,都是通过TCP的方式来探测服务实例是否还活着
服务发现
所谓的服务发现就是指当有服务实例注册成功之后,其它服务可以发现这些服务实例
Nacos提供了两种发现方式:
- 主动查询
- 服务订阅
主动查询就是指客户端主动向服务端查询需要关注的服务实例,也就是拉(pull)的模式
服务订阅就是指客户端向服务端发送一个订阅服务的请求,当被订阅的服务有信息变动就会主动将服务实例的信息推送给订阅的客户端,本质就是推(push)模式
在我们平时使用时,一般来说都是选择使用订阅的方式,这样一旦有服务实例数据的变动,客户端能够第一时间感知
并且Nacos在整合SpringCloud的时候,默认就是使用订阅的方式
对于这两种服务发现方式,1.x和2.x版本实现也是不一样
服务查询其实两者实现都很简单
1.x整体就是发送Http请求去查询服务实例,2.x只不过是将Http请求换成了gRPC的请求
服务端对于查询的处理过程都是一样的,从服务注册表中查出符合查询条件的服务实例进行返回
不过对于服务订阅,两者的机制就稍微复杂一点
在Nacos客户端,不论是1.x还是2.x都是通过SDK中的NamingService#subscribe
方法来发起订阅的
当有服务实例数据变动的时,客户端就会回调EventListener
,就可以拿到最新的服务实例数据了
虽然1.x还是2.x都是同样的方法,但是具体的实现逻辑是不一样的
1.x服务订阅实现
在1.x版本的时候,服务订阅的处理逻辑大致会有以下三步:
第一步,客户端在启动的时候,会去构建一个叫PushReceiver的类
这个类会去创建一个UDP Socket,端口是随机的
其实通过名字就可以知道这个类的作用,就是通过UDP的方式接收服务端推送的数据的
第二步,调用NamingService#subscribe
来发起订阅时,会先去服务端查询需要订阅服务的所有实例信息
之后会将所有服务实例数据存到客户端的一个内部缓存中
并且在查询的时候,会将这个UDP Socket的端口作为一个参数传到服务端
服务端接收到这个UDP端口后,后续就通过这个端口给客户端推送服务实例数据
第三步,会为这次订阅开启一个不定时执行的任务
之所以不定时,是因为这个当执行异常的时候,下次执行的时间间隔就会变长,但是最多不超过60s,正常是10s,这个10s是查询服务实例是服务端返回的
这个任务会去从服务端查询订阅的服务实例信息,然后更新内部缓存
这里你可能会有个疑问
既然有了服务变动推送的功能,为什么还要定时去查询更新服务实例信息呢?
其实很简单,那就是因为UDP通信不稳定导致的
虽然有Push,但是由于UDP通信自身的不确定性,有可能会导致客户端接收变动信息失败
所以这里就加了一个定时任务,弥补这种可能性,属于一个兜底的方案。
这就是1.x版本的服务订阅的实现
2.x服务订阅的实现
讲完1.x的版本实现,接下来就讲一讲2.x版本的实现
由于2.x版本换成了gRPC长连接的方式,所以2.x版本服务数据变更推送已经完全抛弃了1.x的UDP做法
当有服务实例变动的时候,服务端直接通过这个长连接将服务信息发送给客户端
客户端拿到最新服务实例数据之后的处理方式就跟1.x是一样了
除了处理方式一样,2.x也继承了1.x的其他的东西
比如客户端依然会有服务实例的缓存
定时对比机制也保留了,只不过这个定时对比的机制默认是关闭状态
之所以默认关闭,主要还是因为长连接还是比较稳定的原因
当客户端出现异常,接收不到请求,那么服务端会直接跟客户端断开连接
当恢复正常,由于有Redo操作,所以还是能拿到最新的实例信息的
所以2.x版本的服务订阅功能的实现大致如下图所示
这里还有个细节需要注意
在1.x版本的时候,任何服务都是可以被订阅的
但是在2.x版本中,只支持订阅临时服务,对于永久服务,已经不支持订阅了
小总结
服务查询1.x是通过Http请求;2.x通过gRPC请求
服务订阅1.x是通过UDP来推送的;2.x就基于gRPC长连接来实现的
1.x和2.x客户端都有服务实例的缓存,也有定时对比机制,只不过1.x会自动开启;2.x提供了一个开个,可以手动选择是否开启,默认不开启
数据一致性
由于Nacos是支持集群模式的,所以一定会涉及到分布式系统中不可避免的数据一致性问题
1、服务实例的责任机制
再说数据一致性问题之前,先来讨论一下服务实例的责任机制
什么是服务实例的责任机制?
比如上面提到的服务注册、心跳管理、监控检查机制,当只有一个Nacos服务时,那么自然而言这个服务会去检查所有的服务实例的心跳时间,执行所有服务实例的健康检查任务
但是当出现Nacos服务出现集群时,为了平衡各Nacos服务的压力,Nacos会根据一定的规则让每个Nacos服务只管理一部分服务实例的
当然每个Nacos服务的注册表还是全部的服务实例数据
这个管理机制我给他起了一个名字,就叫做责任机制,因为我在1.x和2.x都提到了responsible
这个单词
本质就是Nacos服务对哪些服务实例负有心跳监测,健康检查的责任。
2、CAP定理和BASE理论
谈到数据一致性问题,一定离不开两个著名分布式理论
- CAP定理
- BASE理论
CAP定理中,三个字母分别代表这些含义:
- C,Consistency单词的缩写,代表一致性,指分布式系统中各个节点的数据保持强一致,也就是每个时刻都必须一样,不一样整个系统就不能对外提供服务
- A,Availability单词的缩写,代表可用性,指整个分布式系统保持对外可用,即使从每个节点获取的数据可能都不一样,只要能获取到就行
- P,Partition tolerance单词的缩写,代表分区容错性。
所谓的CAP定理,就是指在一个分布式系统中,CAP这三个指标,最多同时只能满足其中的两个,不可能三个都同时满足
为什么三者不能同时满足?
对于一个分布式系统,网络分区是一定需要满足的
而所谓分区指的是系统中的服务部署在不同的网络区域中
比如,同一套系统可能同时在北京和上海都有部署,那么他们就处于不同的网络分区,就可能出现无法互相访问的情况
当然,你也可以把所有的服务都放在一个网络分区,但是当网络出现故障时,整个系统都无法对外提供服务,那这还有什么意义呢?
所以分布式系统一定需要满足分区容错性,把系统部署在不同的区域网络中
此时只剩下了一致性和可用性,它们为什么不能同时满足?
其实答案很简单,就因为可能出现网络分区导致的通信失败。
比如说,现在出现了网络分区的问题,上图中的A网络区域和B网络区域无法相互访问
此时假设往上图中的A网络区域发送请求,将服务中的一个值 i 属性设置成 1
如果保证可用性,此时由于A和B网络不通,此时只有A中的服务修改成功,B无法修改成功,此时数据AB区域数据就不一致性,也就没有保证数据一致性
如果保证一致性,此时由于A和B网络不通,所以此时A也不能修改成功,必须修改失败,否则就会导致AB数据不一致
虽然A没修改成功,保证了数据一致性,AB还是之前相同的数据,但是此时整个系统已经没有写可用性了,无法成功写数据了。
所以从上面分析可以看出,在有分区容错性的前提下,可用性和一致性是无法同时保证的。
虽然无法同时一致性和可用性,但是能不能换种思路来思考一下这个问题
首先我们可以先保证系统的可用性,也就是先让系统能够写数据,将A区域服务中的i修改成1
之后当AB区域之间网络恢复之后,将A区域的i值复制给B区域,这样就能够保证AB区域间的数据最终是一致的了
这不就皆大欢喜了么
这种思路其实就是BASE理论的核心要点,优先保证可用性,数据最终达成一致性。
BASE理论主要是包括以下三点:
- 基本可用(Basically Available):系统出现故障还是能够对外提供服务,不至于直接无法用了
- 软状态(Soft State):允许各个节点的数据不一致
- 最终一致性,(Eventually Consistent):虽然允许各个节点的数据不一致,但是在一定时间之后,各个节点的数据最终需要一致的
BASE理论其实就是妥协之后的产物。
3、Nacos的AP和CP
Nacos其实目前是同时支持AP和CP的
具体使用AP还是CP得取决于Nacos内部的具体功能,并不是有的文章说的可以通过一个配置自由切换。
就以服务注册举例来说,对于临时实例来说,Nacos会优先保证可用性,也就是AP
对于永久实例,Nacos会优先保证数据的一致性,也就是CP
接下来我们就来讲一讲Nacos的CP和AP的实现原理
3.1、Nacos的AP实现
对于AP来说,Nacos使用的是阿里自研的Distro协议
在这个协议中,每个服务端节点是一个平等的状态,每个服务端节点正常情况下数据是一样的,每个服务端节点都可以接收来自客户端的读写请求
当某个节点刚启动时,他会向集群中的某个节点发送请求,拉取所有的服务实例数据到自己的服务注册表中
这样其它客户端就可以从这个服务节点中获取到服务实例数据了
当某个服务端节点接收到注册临时服务实例的请求,不仅仅会将这个服务实例存到自身的服务注册表,同时也会向其它所有服务节点发送请求,将这个服务数据同步到其它所有节点
所以此时从任意一个节点都是可以获取到所有的服务实例数据的。
即使数据同步的过程发生异常,服务实例也成功注册到一个Nacos服务中,对外部而言,整个Nacos集群是可用的,也就达到了AP的效果
同时为了满足BASE理论,Nacos也有下面两种机制保证最终节点间数据最终是一致的:
- 失败重试机制
- 定时对比机制
失败重试机制是指当数据同步给其它节点失败时,会每隔3s重试一次,直到成功
定时对比机制就是指,每个Nacos服务节点会定时向所有的其它服务节点发送一些认证的请求
这个请求会告诉每个服务节点自己负责的服务实例的对应的版本号,这个版本号随着服务实例的变动就会变动
如果其它服务节点的数据的版本号跟自己的对不上,那就说明其它服务节点的数据不是最新的
此时这个Nacos服务节点就会将自己负责的服务实例数据发给不是最新数据的节点,这样就保证了每个节点的数据是一样的了。
3.2、Nacos的CP实现
Nacos的CP实现是基于Raft算法来实现的
在1.x版本早期,Nacos是自己手动实现Raft算法
在2.x版本,Nacos移除了手动实现Raft算法,转而拥抱基于蚂蚁开源的JRaft框架
在Raft算法,每个节点主要有三个状态
- Leader,负责所有的读写请求,一个集群只有一个
- Follower,从节点,主要是负责复制Leader的数据,保证数据的一致性
- Candidate,候选节点,最终会变成Leader或者Follower
集群启动时都是节点Follower,经过一段时间会转换成Candidate状态,再经过一系列复杂的选择算法,选出一个Leader
这个选举算法比较复杂,完全值得另写一篇文章,这里就不细说了。不过立个flag,如果本篇文章点赞量超过28个,我连夜爆肝,再来一篇。
当有写请求时,如果请求的节点不是Leader节点时,会将请求转给Leader节点,由Leader节点处理写请求
比如,有个客户端连到的上图中的Nacos服务2
节点,之后向Nacos服务2
注册服务
Nacos服务2
接收到请求之后,会判断自己是不是Leader节点,发现自己不是
此时Nacos服务2
就会向Leader节点发送请求,Leader节点接收到请求之后,会处理服务注册的过程
为什么说Raft是保证CP的呢?
主要是因为Raft在处理写的时候有一个判断过程
- 首先,Leader在处理写请求时,不会直接数据应用到自己的系统,而是先向所有的Follower发送请求,让他们先处理这个请求
- 当超过半数的Follower成功处理了这个写请求之后,Leader才会写数据,并返回给客户端请求处理成功
- 如果超过一定时间未收到超过半数处理成功Follower的信号,此时Leader认为这次写数据是失败的,就不会处理写请求,直接返回给客户端请求失败
所以,一旦发生故障,导致接收不到半数的Follower写成功的响应,整个集群就直接写失败,这就很符合CP的概念了。
不过这里还有一个小细节需要注意
Nacos在处理查询服务实例的请求直接时,并不会将请求转发给Leader节点处理,而是直接查当前Nacos服务实例的注册表
这其实就会引发一个问题
如果客户端查询的Follower节点没有及时处理Leader同步过来的写请求(过半响应的节点中不包括这个节点),此时在这个Follower其实是查不到最新的数据的,这就会导致数据的不一致
所以说,虽然Raft协议规定要求从Leader节点查最新的数据,但是Nacos至少在读服务实例数据时并没有遵守这个协议
当然对于其它的一些数据的读写请求有的还是遵守了这个协议。
JRaft对于读请求其实是做了很多优化的,其实从Follower节点通过一定的机制也是能够保证读到最新的数据
数据模型
在Nacos中,一个服务的确定是由三部分信息确定
- 命名空间(Namespace):多租户隔离用的,默认是
public
- 分组(Gr0up):这个其实可以用来做环境隔离,服务注册时可以指定服务的分组,比如是测试环境或者是开发环境,默认是
DEFAULT_GR0UP
- 服务名(ServiceName):这个就不用多说了
通过上面三者就可以确定同一个服务了
在服务注册和订阅的时候,必须要指定上述三部分信息,如果不指定,Nacos就会提供默认的信息
不过,在Nacos中,在服务里面其实还是有一个集群的概念
在服务注册的时候,可以指定这个服务实例在哪个集体的集群中,默认是在DEFAULT
集群下
在SpringCloud环境底下可以通过如下配置去设置
spring
cloud:
nacos:
discovery:
cluster-name: sanyoujavaCluster
在服务订阅的时候,可以指定订阅哪些集群下的服务实例
当然,也可以不指定,如果不指定话,默认就是订阅这个服务下的所有集群的服务实例
我们日常使用中可以将部署在相同区域的服务划分为同一个集群,比如杭州属于一个集群,上海属于一个集群
这样服务调用的时候,就可以优先使用同一个地区的服务了,比跨区域调用速度更快。
总结
到这,终终终于总算是讲完了Nacos作为注册中心核心的实现原理
来源:juejin.cn/post/7347325319198048283
你居然还去服务器上捞日志,搭个日志收集系统难道不香么!
1 ELK日志系统
经典的ELK架构或现被称为Elastic Stack。Elastic Stack架构为Elasticsearch + Logstash + Kibana + Beats的组合:
- Beats负责日志的采集
- Logstash负责做日志的聚合和处理
- ES作为日志的存储和搜索系统
- Kibana作为可视化前端展示
整体架构图:
2 EFK日志系统
容器化场景中,尤其k8s环境,用户经常使用EFK架构。F代表Fluent Bit,一个开源多平台的日志处理器和转发器。Fluent Bit可以:
- 让用户从不同来源收集数据/日志
- 统一并发到多个目的地
- 完全兼容Docker和k8s环境
3 PLG日志系统
3.1 Prometheus+k8s日志系统
PLG
Grafana Labs提供的另一个日志解决方案PLG逐渐流行。PLG架构即Promtail + Loki + Grafana的组合:
Grafana,开源的可视化和分析软件,允许用户查询、可视化、警告和探索监控指标。Grafana主要提供时间序列数据的仪表板解决方案,支持超过数十种数据源。
Grafana Loki是一组可以组成一个功能齐全的日志堆栈组件,与其它日志系统不同,Loki只建立日志标签的索引而不索引原始日志消息,而是为日志数据设置一组标签,即Loki运营成本更低,效率还提高几个数量级。
Loki设计理念
Prometheus启发,可实现可水平扩展、高可用的多租户日志系统。Loki整体架构由不同组件协同完成日志收集、索引、存储等。
各组件如下,Loki’s Architecture深入了解。Loki就是like Prometheus, but for logs。
Promtail是一个日志收集的代理,会将本地日志内容发到一个Loki实例,它通常部署到需要监视应用程序的每台机器/容器上。Promtail主要是用来发现目标、将标签附加到日志流以及将日志推送到Loki。截止到目前,Promtail可以跟踪两个来源的日志:本地日志文件和systemd日志(仅支持AMD64架构)。
4 PLG V.S ELK
4.1 ES V.S Loki
ELK/EFK架构确实强,经多年实际环境验证。存储在ES中的日志通常以非结构化JSON对象形式存储在磁盘,且ES为每个对象都建索引,以便全文搜索,然后用户可特定查询语言搜索这些日志数据。
而Loki数据存储解耦:
- 既可在磁盘存储
- 也可用如Amazon S3云存储系统
Loki日志带有一组标签名和值,只有标签对被索引,这种权衡使它比完整索引操作成本更低,但针对基于内容的查询,需通过LogQL再单独查询。
4.2 Fluentd V.S Promtail
相比Fluentd,Promtail专为Loki定制,它可为运行在同一节点的k8s Pods做服务发现,从指定文件夹读取日志。Loki类似Prometheus的标签方式。因此,当与Prometheus部署在同一环境,因为相同的服务发现机制,来自Promtail的日志通常具有与应用程序指标相同的标签,统一标签管理。
4.3 Grafana V.S Kibana
Kibana提供许多可视化工具来进行数据分析,高级功能如异常检测等机器学习功能。Grafana针对Prometheus和Loki等时间序列数据打造,可在同一仪表板上查看日志指标。
参考
来源:juejin.cn/post/7295623585364082739