注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

马斯克招人策略曝光:9 轮面试,底薪低于同行,只招 “铁杆特斯拉人”

事情是这样的。 Business Insider 最近获得了特斯拉内部薪酬数据库(截至 2021 年 12 月)的访问权限,里面有 10 万名员工的薪酬数据。 然后他们发现了有关特斯拉薪酬的一系列猛料: 面试 9 轮只为招聘特斯拉铁粉; 采用低底薪 + 股票...
继续阅读 »

事情是这样的。


Business Insider 最近获得了特斯拉内部薪酬数据库(截至 2021 年 12 月)的访问权限,里面有 10 万名员工的薪酬数据。


然后他们发现了有关特斯拉薪酬的一系列猛料



  • 面试 9 轮只为招聘特斯拉铁粉;

  • 采用低底薪 + 股票奖励策略,打出 “高风险、高回报” 口号;

  • 特斯拉底薪低于同行,不及苹果、谷歌、英伟达、Meta、福特等科技公司和传统汽车制造商;

  • 工程师更有可能获得股票奖励;

  • 仅有 4% 的员工通过激励股票期权(ISOs) 获得股票,且通常授予高管;

  • ……



更多爆料细节如下——


马斯克招人策略:低底薪 + 股票奖励


透过这份曝光的内部薪酬文件,我们看到特斯拉向员工喊出的是 “高风险、高回报” 这一口号。


why??


一切的一切,还是归于特斯拉想要招聘自身铁粉。据一位了解招聘的内部员工透露:



他们可能在别处得到更好的报酬,但我们想要的是铁杆的特斯拉人。



而为了实现这一目标,特斯拉主要靠 “低底薪 + 股票奖励” 这一策略以及配套的招聘系统


对于前者,一位特斯拉前销售经理将其比喻为 “金手铐”:



股票是主要的钩子…… 我要低下头再等几个月,直到我获得股权。



至于后者,一位特斯拉前招聘人员表示,前司的招聘流程极为严格,通常需要几个月时间来考察面试候选人。


比如面试一位工程师,通常至少包括九次面试,可能需要数月时间



这是一件文化上的事情,一切都是为了排除掉只想 “打卡上下班” 的员工。




那么,特斯拉到底给员工们开了多少薪酬呢?


这里需要补充一个员工人数数据。在这份文件里,我们可以看到 10 万名员工的薪酬情况,而据 CNBC 报道,截至今年 6 月,特斯拉雇佣了大约 12 万名员工(包括正式员工和临时工)。


下面具体来看。


第一,先从公司内部来看。


首先,Business Insider 分析了大约 13,000 名全职、有薪、美国本土员工的平均基本工资(年薪),这些员工分属特斯拉的各个业务部门(如工程、制造或数据管理),而且排除了无法准确计算平均年薪的小时工。



可以看出,这些员工的基本工资中位数(年薪)大多在 10 万美元和 15 万美元之间。


接下来,Business Insider 进一步将数据细分,并查看特斯拉管理岗(全职、美国本土员工、有薪且手下至少有五名员工)的基本工资情况。


结果显示,包括工程总监和在特斯拉服务中心维修车辆的经理在内,这些人的基本工资中位数(年薪)从大约 35,000 美元到 324,000 美元不等。



而且据 9 位现任和前员工透露,自 2021 年 12 月以来,特斯拉的薪酬结构基本保持不变


换句话说,虽然上述数据看起来老旧,但特斯拉目前仍在延续这些薪酬方案。


不过,只看内部情况,我们可能无法直观感受特斯拉的 “低底薪”。


别急,Business Insider 还另外使用了来自证券交易委员会的数据,将特斯拉的基本工资与传统汽车制造商以及市值最大的六家科技公司进行了比较。


可以看出,除了亚马逊,特斯拉均处于落后地位。


而且我们知道,像亚马逊和苹果这样的公司,它们还拥有庞大的仓库劳动力和零售劳动力,这些因素也会影响公司的平均工资。


因此,一个基本情况浮出水面:



特斯拉的基本工资通常低于竞争对手




那么,接下来的问题是:员工为什么愿意接受低底薪呢?


最大原因还是在于股票


9 位现任和前工程师及销售人员表示,特斯拉的股票授予计划使得他们更容易接受较低的底薪。


据悉,过去 5 年,特斯拉的股价飙升超过 1000%;而今年,虽然特斯拉股价经历了显著波动(4 月中旬跌至年初价格的 44%),但在川普成功竞选后,特斯拉收获重大利好,其股价至今累计上涨近 30%。


so,又有多少员工能享受到特斯拉的股票奖励呢?


据内部文件显示,2020 年和 2021 年,有 44 名美国本土员工获得了价值超过 100 万美元的股票。


为了了解哪些员工更有可能获得股票奖励,Business Insider 根据职位类别对股票奖励进行了拆分。


结果显示,大多数工程师收到的股票奖励超过 25,000 美元。(股票的价值基于授予时的股价,但会根据特斯拉的股价变动而变化)



不过需要注意的是,特斯拉将限制性股票单位(RSUs)作为薪酬结构中的主要组成部分,约占薪资发放的 75%。


解释一下,RSUs 指授予时并不立即转化为实际的股票,而是在一定时间锁定期后,以公司股票的形式提供给员工。


换句话说,员工在满足特定条件(如服务年限或公司业绩目标)后才能获得 RSUs 股票。


同时,特斯拉将非合格股票期权(NQSOs) 作为基于业绩的薪酬的一部分,占薪资发放的 21%。


最终,仅有 4% 的员工通过激励股票期权(ISOs) 获得股票,且通常授予高管和其他高级员工。


而对于这一部分,内部文件显示,特斯拉高管中,除一名未列出持股数量的员工外,其余人收到的股票价值在 95 万美元至 2000 万美元之间。



除了股票,另一大原因在于特斯拉的公司形象


按照招聘公司 Stanton Chase 一位总监的说法:



它包含一个以使命为导向的元素…… 这些人正在努力实现地球的脱碳。



更不必说,还有 CEO 马斯克这位顶流的卖力宣传(doge):



我们给每个人股票期权,我们让许多只是在工作一线的人——甚至不知道股票是什么的人——变成了百万富翁。



马斯克 560 亿美元天价薪酬案将于年底见分晓


那么,老马本人在特斯拉的薪酬水平如何呢?


事实上,“马斯克 560 亿美元天价薪酬案” 一直引人关注:


2018 年,特斯拉为马斯克制定了一项为期 10 年的激励计划,方案核心是通过股票期权的方式,将马斯克的个人利益与公司的市值和业绩紧密绑定。


简单说,一旦老马能完成 KPI,他将累计获得特斯拉 12% 的股票期权作为奖励,总价值约为 560 亿美元,这一方案也被外媒认为是美国有史以来规模最大的高管薪酬方案。


当然了,当时来看公司定的 KPI 非常难,结果没想到后来特斯拉一路起飞,市值大涨(目前已来到 1.12 万亿美元)。


这下就有股东跳出来,觉得不公平了。


2022 年,特斯拉的部分股东将马斯克告上法庭,称他将大部分精力花在 SpaceX 等其他公司上,同时利用其对公司及董事会的控制敲定了长期薪酬计划,因此希望废除该方案。


紧接着,特拉华州的法官便以 “对股东不公平” 为由,宣布马斯克的长期薪酬方案无效。


面对这一判决,老马一怒之下宣称将特斯拉的注册地从特拉华州迁至得克萨斯州。(后来确实迁了)


并进行了上诉。


而最终结果,将在今年年底前得到裁决。


就在上周四,负责审理此案的特拉华州衡平法院大法官凯瑟琳・麦考密克(Kathaleen McCormick)表示,她将在 2024 年年底前作出最终决定。


不过,尽管之前的法院判决不利于老马,但特斯拉股东在 2024 年 6 月 13 日的年度股东大会上,已经以较大优势批准了这一薪酬方案。



参考链接:

x.com/BusinessIns…



作者:量子位
来源:juejin.cn/post/7436286873523421238
收起阅读 »

百亿补贴为什么用 H5?H5 未来会如何发展?

web
23 年 11 月末,拼多多市值超过了阿里。我想写一篇《百亿补贴为什么用 H5》,没有动笔;24 年新年,我想写一篇《新的一年,H5 会如何发展》,也没有动笔。 眼看着灵感烂在手里,我决定把两篇文章合为一篇,与你分享。提前说明,我并非百亿补贴的开发人员,本文的...
继续阅读 »

23 年 11 月末,拼多多市值超过了阿里。我想写一篇《百亿补贴为什么用 H5》,没有动笔;24 年新年,我想写一篇《新的一年,H5 会如何发展》,也没有动笔。


眼看着灵感烂在手里,我决定把两篇文章合为一篇,与你分享。提前说明,我并非百亿补贴的开发人员,本文的内容是我的推理。


拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。


我的证据


在 Android 开发者模式下,开启显示布局边界,你可以看到「百亿补贴」是一个完整大框,这说明「百亿补贴」在 App 内是 H5。拷贝分享链接,在浏览器打开,可以看到资源中有 React,说明「百亿补贴」技术栈是 React。


pdd-stack.png


不只是拼多多,利用同样的方法,你可以发现京东、淘宝的「百亿补贴」技术栈也是 H5。


pdd-jd-taobao.png


那么,为什么电商巨头会选择做「百亿补贴」时会选择 H5 呢?


我的推理逻辑


解答问题前,我先说明下推理逻辑。巨头可能选择 H5 的原因千千万万,但最有说服力的原因,肯定具有排他性


什么是排他性?


举个例子,成功人物为什么成功,如果我回答「成功人士会喝水」,你肯定不满意。如果我回答「成功人士坚持不懈」,你会更满意一些。喝水分明是成功人士成功的原因,不喝水人会渴死,没办法成功。你为什么对这个答案不满意呢?


因为「喝水」不具备排他性,普通人也会喝水;而「坚持不懈」比「喝水」更具排他性,大部分普通人没有这个特质。


按照排他性,我需要说明百亿补贴只有 H5 能干,其他技术栈不能干,这样才有说服力。


百亿补贴为什么用 H5?


现在进入正题。粗略来看,大前端的技术栈分为 Native 和跨平台两大类。前者包括 3 小类,分别是 Android、iOS、纯血鸿蒙;后者也包括 3 小类,分别是基于 Web 的方案、基于系统 UI 框架的方案(比如 React Native)、自己绘制 UI 的方案(比如 Flutter)。


其中,基于 Web 的方案,又可以细分为纯 H5 和 DSL 转 H5(比如 Taro)。


graph TB;
大前端 --> Native;
Native --> Android;
Native --> iOS;
Native --> 纯血鸿蒙;
大前端 --> 跨平台;
跨平台 --> 基于Web的方案;
跨平台 --> 基于系统UI框架的方案;
跨平台 --> 自己绘制UI的方案;
基于Web的方案 --> H5;
基于Web的方案 --> DSL转H5;

我们需要排除 H5 外的其他方案。


原因一:百亿补贴迭代频繁


百亿补贴的业务形式,是一个常住 H5,搭配上多个流动 H5。(「常住」和「流动」是我借鉴「常住人口」和「流动人口」造的词)



  • 常住 H5 链接保持不变,方便用户二次访问。

  • 流动 H5 链接位于常住 H5 的不同位置,方便分发用户流量。


具体到拼多多,它至少有 3 个流量的分发点,可点击的头图、列表上方的活动模块和侧边栏,3 者可以投放不同链接。下图分别投放了 3.8 女神节链接、新人链接和品牌链接:


pdd-activity.png


可以想到,几乎每到一个节日、每换一个品牌,「百亿补贴」就需要更新一次。


这样频繁的迭代,框架必须满足快速开发、快速部署、一次开发多端复用条件。因此可以排除掉 Native 技术栈,留下动态化技术栈。


原因二:百亿补贴需要投放小程序和其他 App


如图所示,你可以在微信上搜索拼多多,可以看到百亿补贴不仅在 App 上投放,还在微信小程序里投放。


pdd-wx.png


此时我们几乎可以排除掉 React Native 和 Flutter 技术栈。因为社区虽然有方案让 React Native、Flutter 适配小程序,但并不成熟,不适合用到生产项目中。


此外,如果你在抖音、B 站和小红书搜索百亿补贴,你可以看到百亿补贴在这些 App 上都有投放广告。


pdd-advertisement.png


这点可以完全排除 React Native 和 Flutter 技术栈。据我所知,目前没有主流 App,会愿意让第三方在自己的 App 里运行 React Native 和 Flutter。


原因三:百亿补贴核心流量在 APP


现在只剩下了基于 Web 的 2 种技术方案,也就是 H5 和 DSL 转出来的 H5(比如 Taro)。


百亿补贴的 HTML 结果,更符合原生 H5 的组织结构,而不是 Taro 这种 DSL 转出来的结构。


我对此的解释是,百亿补贴的核心流量在 App。核心流量在 APP 时。投放小程序是锦上添花,把 H5 嵌入到小程序 Webview 就能满足要求,不需要卷性能。


如果百亿补贴的核心流量在小程序,那么大概率就会使用 DSL 框架,转出来小程序代码和 H5 代码。


综上所述,迭代频繁、需要投放小程序和其他 App,核心流量在 App,是百亿补贴选择 H5 的 3 个主要原因。


H5 未来会如何发展


知道百亿补贴选择 H5 的 3 个原因后,我们可以得到结论,如果 3 个前提不变,未来很长一段时间内,H5 依然是电商活动的主流方案。


不过,主流方案并不意味着一成不变,我认为未来 H5 会有 2 个发展趋势:


趋势一:离线包、SSR 比例增加


H5 有诸多优势的同时,也有着先天缺陷,那就是下载成功率低、容易白屏。


解决这个问题,社区主流的两个方案是离线包和 SSR。


离线包可以将网页的静态资源(如 HTML、CSS、JS、图片等)缓存到本地,用户访问 H5 页面时,可以直接读取本地的离线资源,从而提升页面的加载速度。阿里云腾讯云等云服务商都有自己的离线包方案。


SSR 即服务器端渲染,它可以减少白屏时间,让用户更快看到页面。传统的 CSR(客户端渲染)初始时只渲染空白的 HTML 框架,然后再去获取数据并渲染内容。而在 SSR 中,服务器在接收到客户端请求时,会在服务器端利用数据和模板生成完整的 HTML 页面,再把页面发送给客户端浏览器。


不难想到,业务陷入瓶颈后,企业开始看中性能,大部分前端开发者都会来卷一卷离线包、 SSR,它们的比例会进一步增加。


趋势二:定制化要求苛刻


近年 C 端市场增长缓慢,企业重点从扩张新客,变成留存老客。


这个背景下,定制化要求变得越来越苛刻,目的是让用户区分各种活动。用互联网黑话来说,就是「建立用户心智」。


下面是拼多多、京东、淘宝、12306、中国移动和招商银行的活动 H5,尽管它们结构都差不多,但长得是千奇百怪。


fluid.png


12306-yidong-zhaoshang.png


我估计未来,电商活动 H5 的外观将变得极具个性,各位前端同学可以在卷卷 CSS、动效方向。


总结


本文介绍了我认为「百亿补贴」会选用 H5 的 3 大原因:



  • 百亿补贴迭代频繁

  • 百亿补贴需要投放小程序、其他 App

  • 百亿补贴核心流量是自己的 App


以及我 H5 未来发展趋势的 2 个预测:



  • 离线包、SSR 比例增加

  • 定制化要求苛刻


拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。


作者:小霖家的混江龙
来源:juejin.cn/post/7344325496983732250
收起阅读 »

2024年总结: 迷茫

12月今年最后一个月了,相逢的人已走散, Q4的OKR已经定型了, 很平淡无味, 闲的无聊 提前写个年终总结吧。00年, 再过一个月就25岁了,一个人来杭州也已经3年多了 每天有时间写一点 周六了 写到凌晨1点了 看直播/打麻将到凌晨5点才睡。 去年也写了一篇...
继续阅读 »

12月今年最后一个月了,相逢的人已走散, Q4的OKR已经定型了, 很平淡无味, 闲的无聊 提前写个年终总结吧。00年, 再过一个月就25岁了,一个人来杭州也已经3年多了 每天有时间写一点 周六了 写到凌晨1点了 看直播/打麻将到凌晨5点才睡。 去年也写了一篇 2023年总结:日渐清醒,得失随意 //TODO DDL 应该是在月中完结吧。



工作



我大概回忆一下 我今年在工作上应该干了这些事情吧




  • 自己申请换项目组,日常维护新品App版本迭代 2周一个版本 多个app同时进行

  • 完成所有App 苹果服务器接口Storekit2 升级上线

  • Google 支付/订阅 SDK 重构原生API调用代码

  • RocketMQ优化多数据中心用户数据同步/webhook/推送

  • RocketMQ-Exporter 搭建 监控相关性能指标

  • SSE+服务器GRPC流式 推送消息

  • us机器 内存优化 切换到jemalloc内存分配器

  • RocketMQ 单机 升级为Dledger 集群模式 Q4 任务



从22年3月毕业到现在 再度过一个季度 在这家公司呆三年了(3年之约),整个过程就是升级打怪,看着人来人往 合作的人 离职一个又来一个, 每个季度干着重复的工作,技术框架还是那一套 SpringBoot+GRPC 经过一年熟悉后 就觉得没有新鲜感了, C端产品App 在用户基数不大情况下 基本的重心在客户端的ui/操作体验上 后端嘛就是一个数据存储的地方 存取能有什么难度 大家都会,每个季度任务就是 基本的版本迭代+一些服务器内部的优化,如果你想要拿到高的绩效 那干这点是远远不够的, 基本规则在无告警的版本迭代下 做一些对团队贡献大 有价值的事情 拿A/季度优秀员工,三年期间升了两次小级别 p5-1->p5-2->p5-3 还是一个初级开发 今年估计也悬了 没有两次A的绩效 跨段位升没机会,没有owner过项目, 和旁边人朋友/同学工作/升职对比下 只能说自己像个废物 躺的太平了 每天965的生活 除了偶尔上线 需要加班留下来 大家都不加班 也没有那么多活需要加班来干的活。




生活



去年立的flag 也是一腔鸡血




  • 软考 系统架构师 +软著 拿到杭州E类人才

  • 健身/减肥

  • 骑车 vlog (杭州景点全部骑完 影石360 ace pro)

  • 摄影佳能rp/视频剪辑学习

  • 日语/英语学习

  • leetcode 上Knight

  • 考D驾-照 骑防赛

  • 日麻线下雀庄体验 参加各种线上日麻比赛

  • ClickHouse hangzhou 线下沙龙

  • 掘金bolg 更新 技术日常

  • 千岛湖

  • 抽烟+喝酒

  • B站 直播 日麻


软考 系统架构师+软著 拿到杭州E类人才



img


骑车 vlog (杭州景点全部骑完 影石360 ace pro)



  • 一个人走走停停 骑过很多地方, 最多的还是钱塘江到彭浦大桥->复兴大桥路线 不知道骑了多少遍,西湖/湘湖/九溪这些地方都去过了,车子是青春款只在线上售卖 后期毛病很多 链条蹭盘/刹车无效 自己不知道维修了多少次,最近这两个月很少骑了,放在地下室发霉 后续准备卖掉了这车 还买了那么多骑行装备,买的insta 360 ace pro 3000多降价到2k左右 当时在大疆和insta中选择了好久 最后还是踩坑了 实体店体验了大疆action 画质比insta360好太多了,有必要考虑再买一台大疆action5了, 一个人的骑行之路也该结束了 开始新的玩具 仿赛摩托车 芜湖起飞。


img
img
img
img
image.png

健身/减肥



  • 怎么说呢 三天打鱼两天晒网的行动 体脂没什么变化,饮食更不会控制 每天外卖外卖外卖, Q1/Q2两个季度挺积极的 基本工作日晚上有时间就去健身房 周末白天也去, 在健身房的时间也能让自身感受到轻松, 这个小区有个百姓健身房在地下室 24小时 刷脸进去 设备齐全 没什么人 每个季度的话300块RMB, 我主要后面可能没有看到短期效果+活着很累 有一段时间没有去了, 偶尔下楼抽烟去逛逛, 最后得到的只是自己的一个心里安慰,没有合理的计划和坚持下去的心 我现在已经懒连手表都不戴了。


img
img
img
img

摄影佳能rp/视频剪辑学习



  • 怎么说呢 周末放假就是宅 已经吃灰了 除了9月1号 拿到免费的门票 杭州植物园专门去了一趟拍彼岸花,其他时间不出手。视频剪辑也是一坨屎 目前就用剪映弄一些雀魂麻将抽角色的视频,后续还是想学一下专业的剪辑工具 这个也看需求吧。


img

考D驾-照 骑防赛



  • 为什么要去骑摩托车,主要是中秋节回家一趟,隔壁邻居已经买了一辆机车, 当时他让我试试 我没试 后面就一直关注摩托车这个事 抖音一直给我推视频。才有了考D驾-照驾-照,周末练半天,工作日考试半天就好了,4科联考 比C1驾-照周期短 速度快,驾-照到手后面周末直接找附近最近的租车平台试试水,萧山那边的之江路/美女坝路险,本田500 手震麻了 1个小时 干了100公里, 最后一个半小时就还车了 跑了150km,整体的体验感是非常好的,无论是去骑车的路上 还是过程中 都能够忘记生活/工作上的烦心事,最尴尬的是红绿灯起步熄火了,后续周末继续出行租车,找个有缘人一起。


img
img
img

日麻线上比赛/线下雀庄体验



  • 每天下班就是点外卖 开始打麻将,水各种日麻群 打友人赛/团队赛,每天晚上达到2 3点 菜就爱多玩 和群友打比赛 对个人的实力也是有了认知 学习别人的打法 从野猪冲击 也慢慢在意铳率了,前一天打完线上比赛, 这个月周日也是马上跟着一个大哥去杭州线下的湖滨牌浪屋体验完线下日麻,今年干的最多的事情就是打麻将,下班除了打麻将还是打麻将。


img
img
img
img

leetcode 上Knight



  • 基本上是原地没动,比赛一场没打,为什么要刷?为了什么?能带给自己什么收益?呆在舒适圈里久了 不想出去,算法也是提不上一点兴趣了 估计只有到时候找工作之前才会接触到了 其实也制定了计划 刷灵神的清单 还是自己懒吧 动不起来,最后一个月 要不开始发力?算了 打麻将吧。


img

其他



  • 今年参加了ClickHouse hangzhou 线下沙龙, 虽然没有使用过clickhouse这款db,去听听别人公司的落地方案,去阿里园区转了转。

  • 掘金bolg 更新 技术日常 主要是参加创作者训练营吧 锻炼一点自己的文本输出能力,总结的过程中也能知道问题的本质是什么,解决的过程/方式以及别人是怎么解决的,收获还是有的。

  • 和同学五一去了千岛湖一趟 结局不是很好 过程体验不错。

  • 在日语/英语学习上面投入的时间 ,无论是日常工作上英语的使用 还是各种文档阅读能力,在逛各种项目/看论文的时候 就能体现出来, 日语兴趣的话 纯粹是打日麻和旁边的日麻群友影响/看番剧而来的 每天用多邻国完成任务,买了4本书《标准日语》+《大家的日语》,在B站上看圆圆姐的视频教程【京大博士带你学日语】新标日初级上册全新课程!必能学会!超详细讲解!轻松搞定日语学习!(课本内容完结!)哔哩哔哩bilibili

  • 抽烟+喝酒已经是家常便饭一样的事情了 上半年是沉迷于喝酒消愁 下半年就抽烟打发时间,每天下班又不知道干什么 找点打发时间的乐趣,天天熬夜看直播 打麻将 2点3点睡觉已经是常态了,每天晚上看陈伯/刘刘江直播 带来的乐趣, 工作日每天基本8点50的闹钟吵醒,拖着尸体去上班,周末基本睡到自然醒中午/下午 除了楼上楼下装修 直接被震醒了。

  • 这样一回想2024年还是干了很多无意义的事。



虽然只有15篇文章 文章的阅读数也有3w 其实数据对我来说也是无所谓的,主要还是方便以后回忆吧,分享出去 可能有人和你遇到相同问题,带给解决思路 明年要不要继续写?还是把时间投入在别的地方?都是未知



img
img
img

个人技术学习



  • AI 知识点拓展学习

  • 部门分享

  • 推荐系统&&RAG

  • 前端

  • 第三方支付订阅

  • 分布式论文学习总结

  • 《计算机网络-自顶向下方法第七版》

  • 《CSAPP 深入理解计算机系统(第3版)》

  • 《设计数据密集型应用》

  • 技术拓展/深挖(RocketMq源码/go-redis源码/Netty源码/Mycat2源码)



看个锤子 没心思学习 下半年天天打麻将




  • AI的话主要是身边环境影响,自己的项目组一直在利用AI做业务,从2023年开始 公司一直对接的是openai 提供的chat 能力,公司内部举行了ai相关的比赛,业务想要搭建自己的知识库和RAG搜索 主要是用AWS上的Redrock封装好的知识库 ,项目组一些APP一直在使用微软的TTS进行语言转音频的操作,部门,组内和项目组 大家一直在内部分享和ai相关的知识点,产品会使用cursor提前将需求写完 自己进部署上线。

  • 跟着项目组业务走,最近在支付方面的功能进行了改动,对于web网页上的购买消耗型商品/续费型商品的购买,主要对接的平台是Stripe信用卡visa支付 和paypal支付。appstore 支付的话 最近负责组内storekit2 服务器接口升级重构代码 用的官方开源库 github.com/apple/app-s…。Alipay 支付宝和Wechat 对于中国环境的用户提供的一种支付方式, 代码很粗糙 很久没有相关需求迭代了。Google 支付 来的两年时间接触的业务还是比较少,整个支付逻辑和appstore是一致的,有时间把代码逻辑和官方文档进行学习总结一下。

  • 现在只会个后端远远不够了,替代性太强了,除非是中大厂那样细分工作岗位/业务内容,如果你有自己的想法 后续做一些自己的产品/独立开发者 一人一公司 全栈只能是无敌路。我这边对前端也是零零散散的学习 没有整个的大项目使用,github.com/lobehub/lob… 前端React开源项目学习 TSX+TS 认知冲击 原来前端已经进化到这个地步了,没有html+css全部被封装了,我们内部的数据平台还是原生html+django搭建的,每次加新功能ai生成的代码 能跑就行。



在下半年 觉得基础知识很重要 技术跟着业务走 没必要太追求新技术 就往计算机基础知识+算法+基础论文投入时间




  • 中间一段也是将《计算机网络-自顶向下方法第七版》 计算机网络-自顶向下方法第七版 · 语雀和《CSAPP 深入理解计算机系统(第3版)》CSAPP 深入理解计算机系统(第3版) · 语雀 细看了一遍 , 书籍买了很多 都是吃灰的 没有去年那个干劲了。

  • 看论文 可以学习技术理论的基础 还有重要一点是学英语, 主要是看一个up在学习这方面的知识点 就跟着看了一段时间,谷歌三大剑客 GFS/MapReduce/BigTable 看论文不看分布式论文 就像四大名著不读红楼梦,唐诗不读李杜,吃泡面不加调料包 Raft/Paxos 那一块真的一看就是几天 深陷进去 每次看硬核课堂的文章 Raft -论文导读 与 ETCD源码解读

  • 参与开源项目任务也没有达成 往里面投入的时间太少了,最后下班/上班有时间 也没深入去学习业务上用到的组件源码, 最近的话 负责RocketMQ集群Dledger搭建/MQ优化业务 ,RocketMQ-exporter+herzbeat+prometheus监控指标, 遇到的异常信息太多 每次都是网上找案例解决,上班利用ai 深入在看看RocketMQ源码吧github.com/apache/rock… 边看边总结RocketMQ 源码 5.2.0 - 生产者 Producer



杂学杂记




  • 中间又去了解了下机器学习/深度学习相关的内容,又看了大数据开发Spark/Flink等等组件,前端看了React+TS相关知识点/demo/Flutter开源项目, 背单词的时候发现墨墨背单词是node+ts写的,有个软考刷题的app是Flutter写的 作者是独立开发者 最近公司客户端也在用flutter开发新品app 代码我这边也是有权限的, 也去了解了一下技术栈,中间又有一段时间去了解了下亚马逊运营的工作,也看了AIGC/agent/图像/音频/向量数据库milvus等相关的方向 RAG 知识库增强式搜索,在推荐系统领域 推荐 广告 搜索 也花了一段时间去了解/学习 因为我们这边没有算法工程师 推荐功能很粗糙 没有用户画像的概念,有一段时间被cloudwego社区的kitex/hertz吸引 当时想去上海站的线下沙龙 可惜正好那周软考考试, 因为有个麻将牌谱工具是rust写的 github.com/Equim-chan/… 所以又去了解了一下rust,我记得有段时间投入在系统设计/业务场景思考方向(IM/Feed流/本地消息表/分布式限流等等), 都是一点毛皮,Q2服务器服务的内存问题 每天上班想下班想 把互联网的文章都翻烂了 从堆内到堆外到Glibc 询问各种技术大佬场景异常 组内成员给不了你任何帮助 全靠自己,所有的东西没有实质性的收获 也没有在项目中使用到 过了一段时间基本全忘了(不做笔记/总结) 。



现在往回看 在技术学习的时长投入的太少了 对技术没有追求 什么都知道一点 什么都不精通 啥也没学会 离开了Java/Spring 我还能干什么,我能干的 找个会使用AI的人来都能干,天天熬夜 不知道熬的是什么 碌碌无为。最近一年 代码写的很少了 基本靠ai生成 微改/设计一下 写的自己也看不懂了。生活/工作迷茫 现在都是活一天是一天, 想回家。



后续规划 待定



  • 英语/日语

  • 独立开发者



打麻将去咯 一起玩雀魂的可以加我



20241212-151216.png

作者:呆呆蛇
来源:juejin.cn/post/7445511025702764555
收起阅读 »

pnpm 的崛起:如何降维打击 npm 和 yarn🫡

web
今天研究了一下 pnpm 的机制,发现它确实很强大,甚至可以说对 yarn 和 npm 形成了降维打击 我们从包管理工具的发展历史,一起看下到底好在哪里? npm2 在 npm 3.0 版本之前,项目的 node_modules 会呈现出嵌套结构,也就是说,我...
继续阅读 »

今天研究了一下 pnpm 的机制,发现它确实很强大,甚至可以说对 yarnnpm 形成了降维打击


我们从包管理工具的发展历史,一起看下到底好在哪里?


npm2


在 npm 3.0 版本之前,项目的 node_modules 会呈现出嵌套结构,也就是说,我安装的依赖、依赖的依赖、依赖的依赖的依赖...,都是递归嵌套的


node_modules
├─ express
│ ├─ index.js
│ ├─ package.json
│ └─ node_modules
│ ├─ accepts
│ │ ├─ index.js
│ │ ├─ package.json
│ │ └─ node_modules
│ │ ├─ mime-types
| | | └─ node_modules
| | | └─ mime-db
| │ └─ negotiator
│ ├─ array-flatten
│ ├─ ...
│ └─ ...
└─ A
├─ index.js
├─ package.json
└─ node_modules
└─ accepts
├─ index.js
├─ package.json
└─ node_modules
├─ mime-types
| └─ node_modules
| └─ mime-db
└─ negotiator

设计缺陷


这种嵌套依赖树的设计确实存在几个严重的问题



  1. 路径过长问题: 由于包的嵌套结构 , node_modules 的目录结构可能会变得非常深,甚至可能会超出系统路径长度上限 ,毕竟 windows 系统的文件路径默认最多支持 256 个字符

  2. 磁盘空间浪费: 多个包之间难免会有公共的依赖,公共依赖会被多次安装在不同的包目录下,导致磁盘空间被大量浪费 。比如上面 express 和 A 都依赖了 accepts,它就被安装了两次

  3. 安装速度慢:由于依赖包之间的嵌套结构,npm 在安装包时需要多次处理和下载相同的包,导致安装速度变慢,尤其是在依赖关系复杂的项目中


当时 npm 还没解决这些问题, 社区便推出了新的解决方案 ,就是 yarn。 它引入了一种新的依赖管理方式——扁平化依赖。


看到 yarn 的成功,npm 在 3.0 版本中也引入了类似的扁平化依赖结构


yarn


yarn 的主要改进之一就是通过扁平化依赖结构来解决嵌套依赖树的问题


具体来说铺平,yarn 尽量将所有依赖包安装在项目的顶层 node_modules 目录下,而不是嵌套在各自的 node_modules 目录中。


这样一来,减少了目录的深度,避免了路径过长的问题 ,也尽可能避免了依赖被多次重复安装的问题


|350


我们可以在 yarn-example 看到整个目录,全部铺平在了顶层 node_modules 目录下,展开下面的包大部分是没有二层 node_modules


然而,有些依赖包还是会在自己的目录下有一个 node_modules 文件夹,出现嵌套的情况,例如 yarn-example 下的http-errors 依赖包就有自己的 node_modules,原因是:


当一个项目的多个依赖包需要同一个库的不同版本时,yarn 只能将一个版本的库提升到顶层 node_modules 目录中。 对于需要这个库其他版本的依赖,yarn 仍然需要在这些依赖包的目录下创建一个嵌套的 node_modules 来存放不同版本的包


比如,包 A 依赖于 lodash@4.0.0,而包 B 依赖于 lodash@3.0.0。由于这两个版本的 lodash 不能合并,yarn 会将 lodash@4.0.0 提升到顶层 node_modules,而 lodash@3.0.0 则被嵌套在包 B 的 node_modules 目录下。


幽灵依赖


虽然 yarn 和 npm 都采用了扁平化的方案来解决依赖嵌套的问题,但这种方案本身也有一些缺陷,其中幽灵依赖是一个主要问题。


幽灵依赖,也就是你明明没有在 package.json 文件中声明的依赖项,但在项目代码里却可以 require 进来
这个也很容易理解,因为依赖的依赖被扁平化安装在顶层 node_modules 中,所以我们能访问到依赖的依赖


但是这样是有隐患的,因为没有显式依赖,未来某个时候这些包可能会因为某些原因消失(例如新版本库不再引用这个包了,然后我们更新了库),就会引发代码运行错误


浪费磁盘空间


而且还有一个问题,就是上面提到的依赖包有多个版本的时候,只会提升一个,那其余版本的包不还是复制了很多次么,依然有浪费磁盘空间的问题


那社区有没有解决这俩问题的思路呢? pnpm 就是其中最成功的一个


pnpm


pnpm 通过全局存储和符号链接机制从根源上解决了依赖重复安装和路径长度问题,同时也避免了扁平化依赖结构带来的幽灵依赖问题
pnpm 的优势概括来说就是“快、准、狠”:



  • 快:安装速度快

  • 准:安装过的依赖会准确复用缓存,甚至包版本升级带来的变化都只 diff,绝不浪费一点空间

  • 狠:直接废掉了幽灵依赖


执行 npm add express,我们可以在 pnpm-example 看到整个目录,由于只安装了 express,那 node_modules 下就只有 express


|400


那么所有的(次级)依赖去哪了呢? binggo,在node_modules/.pnpm/目录下,.pnpm/ 以平铺的形式储存着所有的包


|400


三层寻址



  1. 所有 npm 包都安装在全局目录 ~/.pnpm-store/v3/files 下,同一版本的包仅存储一份内容,甚至不同版本的包也仅存储 diff 内容。

  2. 顶层 node_modules 下有 .pnpm 目录以打平结构管理每个版本包的源码内容,以硬链接方式指向 pnpm-store 中的文件地址。

  3. 每个项目 node_modules 下安装的包以软链接方式将内容指向 node_modules/.pnpm 中的包。
    所以每个包的寻找都要经过三层结构:node_modules/package-a > 软链接 node_modules/.pnpm/package-a@1.0.0/node_modules/package-a > 硬链接 ~/.pnpm-store/v3/files/00/xxxxxx


    这就是 pnpm 的实现原理。官方给了一张原理图,可以搭配食用


    |600


    前面说过,npm 包都被安装在全局 pnpm store ,默认情况下,会创建多个存储(每个驱动器(盘符)一个),并在项目所在盘符的根目录


    所以,同一个盘符下的不同项目,都可以共用同一个全局 pnpm store,绝绝子啊👏,大大节省了磁盘空间,提高了安装速度


    |600



软硬链接


也就是说,所有的依赖都是从全局 store 硬连接到了 node_modules/.pnpm 下,然后之间通过软链接来相互依赖。


那么,这里的软连接、硬链接到底是什么东西?


硬链接是指向磁盘上原始文件所在的同一位置 (直接指向相同的数据块)


软连接可以理解为新建一个文件,它包含一个指向另一个文件或目录的路径 (指向目标路径)



总结


npm2 的嵌套结构: 每个依赖项都会有自己的 node_modules 目录,导致了依赖被重复安装,严重浪费了磁盘空间💣;在依赖层级比较深的项目中,甚至会超出 windows 系统的文件路径长度💣


npm3+ 和 Yarn 的扁平化策略: 尽量将所有依赖包安装在项目的顶层 node_modules 目录下,解决了 npm2 嵌套依赖的问题。但是该方案有一个重大缺陷就是“幽灵依赖”💣;而且依赖包有多个版本时,只会提升一个,那其余版本依然会被重复安装,还是有浪费磁盘空间的问题💣


pnpm全局存储和符号链接机制: 结合软硬链和三层寻址,解决了依赖被重复安装的问题,更加变态的是,同一盘符下的不同项目都可以共用一个全局 pnpm store。节省了磁盘空间,并且根本不存在“幽灵依赖”,安装速度还贼快💪💪💪


作者:柏成
来源:juejin.cn/post/7410923898647461938
收起阅读 »

写点掏心窝子的话

唯有读书和赚钱,才是一个人最好的修行,前者使人不惑,后者使人不屈! 世界上任何一种能力,只要你迫切想学,真心想学,无论身边有没有人教你,你都可以想办法找人,找资源,学会自我学习,自我教育才是最好的教育。 事实上,不管学历怎么样,你一生中绝大部分的东西都是自学的...
继续阅读 »

唯有读书和赚钱,才是一个人最好的修行,前者使人不惑,后者使人不屈!


世界上任何一种能力,只要你迫切想学,真心想学,无论身边有没有人教你,你都可以想办法找人,找资源,学会自我学习,自我教育才是最好的教育。


事实上,不管学历怎么样,你一生中绝大部分的东西都是自学的。


一定要聚焦到自己想做的事情上,并且在头脑里想象自己正在做,而且做得越来越好。


当你的思想,注意力,意识集中在某一个领域,某一件事,某一个行业,某些人身上时,与此相关的大量人,事,物全会被你吸引过来。


对于成年人来说,每个人的时间都是有限的,2025,千万不要再给自己树立太多的目标。


百门通不如一门精,一个人终其一生不如专注一件事,做到极致,朝着一个既定的方向不懈努力的人,几乎都成为社会各界的成功人士。


如果你能只深挖一个点,把事情做到极致,只要肯下功夫,在六个月内你能掌握任何一门学问。


但这不是让你去挑战别人的天赋,深思读书4个月能涨粉10万,只要肯下功夫,你也可以。


因为每门学问所包含的信息量大约是五万个信息块,一个人一分半钟可以记一个信息块。那么五万个大约需要一千小时,以每星期学习40小时计算。


要掌握一门学问,大约需要六个月。


反过来,如果不能专注聚焦于一件事,经不住其他事和人的干扰和诱惑,于是什么都做不精,做不透。


知识是无限的,专业能力有无数种细分领域,行业的经验无穷无尽。


每个都了解一点表面信息,一辈子也不会有成就,每一个专业能力,每一个细分领域赛道,每一个行业都有挖不尽的信息、知识、理论、经验,要用专注和时间去钻研一件事,最容易成为专家有结果的人。


很多人忙碌一生,却不明白真正的优先级是什么!


余认为:


家庭的核心是经济,而不是感情。


职场的准则是价值,而不是努力。


社交的关键是利益互换,而不是单纯的友谊。


教育的目的是培养能力,而不是追求分数。


健康的要素是自律,而不是医疗。


爱情的基础是理解,而不是激情。


创业的要点是市场需求,而不是个人喜好。


投资的原则是风险控制,而不是高回报。


人生的追求是幸福,而不是成功。


养老的保障是提前规划,而不是依赖子女。


成长的动力是反思,而不是经历。


学习的意义是应用,而不是积累知识。


旅行的收获是见识,而不是拍照打卡。


婚姻的支撑是责任,而不是浪漫。


生活的智慧是知足,而不是无尽的欲望。 


与君共勉!愿2025活成理想的自己!


作者:河北小田
来源:juejin.cn/post/7451223580595273778
收起阅读 »

微信公众平台:天下之事合久必分分久必合

web
微信公众平台的故事,还得从 “再小的个体,也有自己的品牌” 的 Slogan 开始说起。 时间线开始 2012年8月17日 这一天,微信公众平台正式向普通用户开放,也就从这一天开始,普通人可以在微信上注册属于自己个人或组织的公众号。 2012年8月23日 这...
继续阅读 »

QQ_1734967828741.png


微信公众平台的故事,还得从 “再小的个体,也有自己的品牌” 的 Slogan 开始说起。


QQ_1734972139162.png


时间线开始


2012年8月17日


这一天,微信公众平台正式向普通用户开放,也就从这一天开始,普通人可以在微信上注册属于自己个人或组织的公众号。


2012年8月23日


这一天,微信公众平台正式上线。各大博主、媒体纷纷注册加入了这个平台,开始在微信公众平台上创作,建立自己的读者圈子,打造自己的IP。


2012年11月29日


从这天起,微信图文群发系统升级发布,图文并茂的文章可以通过微信公众平台发送给关注的粉丝了。


这时候,很多企业嗅到了春天来临的味道,招聘互联网编辑的岗位越来越多。


2013年2月6日


这一天,微信的公众号支持开发者模式了,开发者们的春天(噩梦)开始了。


很多公众号开始提供更多的功能了,比如微信公众平台文档里十年没变的那些什么话费查询、机票航班查询等:



于是公众平台开始对外提供了 “火警请按119,急救请按120” 的鸡肋开发能力——关键词回复、关注消息等。



虽然十多年过去了,我依然碰到了很多人不太理解微信公众号的这玩意的交互流程……


2013年3月19日


2013年3月20日,公众平台灰度了“自定义菜单”,当然,还只是内测。


此时的微信公众号,除了可以推送消息之外,也支持在后台编辑公众号菜单,指定菜单可以回复不同的内容或者打开一个 URL。


2013年8月5日


这天,微信发布了 v5.0 大版本,同时也带来了很多好玩的东西。


为了区分平台内公众号的各种主体,微信公众号在这一天分了家:订阅号 + 服务号。


区别在哪呢?



嗯,内测的自定义菜单给服务号开放了。但是阉割了服务号群发的频率:每月4条。



同时,对可申请的主体也做了限制:



个人只能申请订阅号了。组织类不限制。



然后当年很糟心,但现在很开心的事情发生了:订阅号从消息列表折叠到了 “订阅号” 栏目里。


好,很直接。



不过直至今日,服务号依然还可以在消息列表中直接显示。



2013年10月29日


这天,微信公众平台推出了认证的功能,认证之后有一些特权:



  • 语音识别、客服接口、获取用户地理位置、获取用户基本信息、获取关注列表和用户分组接口的权限

  • 仅认证服务号支持的 OAuth2.0网页授权、生成带参数二维码 等

  • 认证了可以送你一个



此时,微信公众号支持通过 腾讯微博新浪微博 等第三方平台的认证来同步认证服务号。此时不管是个人还是组织的号,都可以认证。但是还没有充值即认证的功能。



我猜是运营开始往赚钱上靠了,毕竟 不充钱的腾讯产品不是好产品。


2013年12月24日


说时迟那是快,这不就来了。


从今天起,你可以花 300 块钱来认证你的号了,前提是,你得是 组织 号,个人的不支持。(当然,部分类型的主体认证是不收费的,比如 政务 媒体 等)


2014年3月


今天,微信公众平台支持接入微信支付了。不过,无论你是订阅号还是服务号,都需要通过企业认证之后,再申请开通微信支付。


这一年,开发者们忙起来了。


创建订单、创建支付请求参数、签名、回调处理、支付结果查询 等等事情接踵而至。


微信开发者的圈子和生态慢慢的繁荣了起来。


2014年9月18日



哎,到哪都逃不掉 ToBCURD 业务。



随着微信开发者生态的繁荣,微信意识到了很多开发者在微信的服务号上做 ToB 的业务,要不要独立一个出来呢?


那就叫 企业号 吧,于是微信公众号的第三个兄弟也来了。


在2014年-2017年这段时间,有一个网站很火,叫 很*微信开发者社区(weixin.com)请记住这个名字,一会要考。


2016年1月11日


2016微信公开课PRO版在广州举行,那个男人(张小龙,微信之父) 首次公开演讲。


这天,张小龙说,微信要做 应用号,要让用户 用完即走


2016年5月


这段时间,上面的社区使用的 weixin.com 最终被南山必胜客拿下。手动狗头:)


2016年9月22日


微信开始内测 小程序。又一次噩梦开始了。


2016年11月3日


微信开始公测 小程序


2017年1月9日


微信小程序 正式上线。



小应用?应用号?



2017年6月29日


随着企业号的发展,微信意识到这与微信的个人社交出现了很多的冲突,于是,微信在2017年6月29日,抽离出了企业微信,牛马们开始使用这个工具来为老板创收了。


2017年12月28日


微信小游戏上线,大家一起来 打飞机


2020年1月22日


微信视频号开始内测,本文讲的微信公众平台系列故事本以为到此会结束了。然而:


2024年11月


这个月,微信把 订阅号 改名为 公众号 了。



服务号:那我呢???



我怎么总觉得有大事要发生?


最近


个人可以注册服务号了,而且注册的服务号依然是在消息列表里,还没有被折叠。



企业:??? 当年费劲巴力注册了服务号,一个月还只有四条,我的特权呢???



当然,目前注册的服务号都是没有认证的,我试了试,目前个人主体的服务号不支持认证。也就是所有的高级开发接口权限一个都没有。


我还是觉得要有大事发生了。



完全没看懂微信公众平台这个骚操作。



总结


今天简单聊了聊微信公众平台的一些小故事,如有错误,欢迎评论区指正和讨论。


只是作为曾经风风火火的微信公众平台开发者,心里感慨颇多。


Bye.


作者:Hamm
来源:juejin.cn/post/7451561994799890483
收起阅读 »

手把手教你做个煎饼小程序,摊子开起来

web
前言 周饼伦在街头摊煎饼,摊后人群熙熙攘攘。他忙得不可开交,既要记住面前小哥要加的培根,又要记住身后奶奶要加的生菜,这时又来了个小妹妹,点的煎饼既要培根又要腊肠。他把鸡蛋打进煎饼后,竟突然忘了前面光头大叔要加的料,是火腿还是鸡柳?一时记不清,好像是火腿,对。然...
继续阅读 »

前言


周饼伦在街头摊煎饼,摊后人群熙熙攘攘。他忙得不可开交,既要记住面前小哥要加的培根,又要记住身后奶奶要加的生菜,这时又来了个小妹妹,点的煎饼既要培根又要腊肠。他把鸡蛋打进煎饼后,竟突然忘了前面光头大叔要加的料,是火腿还是鸡柳?一时记不清,好像是火腿,对。然而当把煎饼交给大叔,大叔却怒了,说要的是鸡柳。😡


这可咋办?周饼伦赶忙道歉,大叔却语重心长地说:“试试用小程序云开发吧!最近的数据模型新功能好用得很!” 周饼伦亮出祖传手艺,边摊煎饼边开发小程序,把新开发的小程序点餐页面二维码贴在摊前。从此再没出过错,终于能安心摊煎饼啦!


设计思路


图片


客户扫摊子上面贴的二维码后,会进入点餐页面,在选好要加的配料之后,点击确定就可以点餐,随后,即可在云后台上看到食客提交的数据


图片


实现过程


周饼伦就把当前摊位的主食、配菜,以及各自相应的价格贴在了摊位上,也要把食客的点餐内容记在脑里或者用笔写在纸上。


点餐页要实现两个功能:1.展示当前摊位有的主食、配菜、口味 2.提交订单到周饼伦的订单页面。


煎饼摊子主食(staple food)目前只有摊饼、青菜饼,主食下面有的配菜(side dish),有鸡柳、生菜、鸡蛋、火腿、腊肠。


同理,数据库里面也需要呈现相应的结构。


数据表的实现


数据模型现在提供了一种便捷的能力来,可以快速创建一套可用的数据表来记录摊煎饼的相关数据。


图片


在云后台中新增了一个基于 MySQL 的数据模型,数据模型相当于一张纸,可以在上面记录任何想要记录的数据,比如周饼伦摊位的提供的菜品


图片


创建了基于云开发MySQL数据库的主食表,主食表中包含主食名称,主食价格


图片


图片


字段的详细设置如下


图片


图片


加了主食、配菜两个表之后,将当前的主食和配菜一起加进数据表中

图片


图片


现在就实现了记录当前摊子的主食和配菜。还需要一个订单表,来记录用户的点餐数据


图片


配菜的类型是一个数组文本,用来记录配菜的类型,结构如下


图片


接着需要分别设置每个数据模型的权限。在使用小程序查看订单时,也是以用户的身份来读取的,所以,需要配置用户权限,通过页面访问来控制用户能够访问到哪些页面


图片


图片


图片


至此,数据表就已经大功告成!现在完全可以使用三个表来记录当前摊子的菜品、营业情况。


但是,别忘了周饼伦的目的不止于此,为了周饼伦实现早日暴富,当上CEO,所以,还要利用小程序实现一个界面,来给”上帝“们点餐,并且提供各位CEO查看订单


小程序实现过程


一. 初始化 SDK


在云后台的数据管理中的右侧中,可以方便的查询到使用的文档


图片


新建一个基于云开发的小程序,删除不必要的页面,并且按照文档的步骤进行初始化👇


1.按照指引在 miniprogram 目录下初始化 npm 环境并安装 npm 包


请注意,这里需要在 miniprogram 目录下初始化 npm ,不然需要编辑 project.config.json 手动指定 npm 包的位置


在 miniprogram 目录下打开终端


图片


2.初始化当前 npm 并且安装 @cloudbase/wx-cloud-client-sdk npm 包


npm init -y & npm install @cloudbase/wx-cloud-client-sdk --save

图片


3.在小程序中构建 npm


图片


4.在小程序 app.js 中初始化环境


// app.js
App({
globalData: {
// 在这里提供全局变量 models 数据模型方法,方便给页面使用
models: null
},
onLaunch: async function () {
const {
init
} = require('@cloudbase/wx-cloud-client-sdk')
// 指定云开发环境 ID
wx.cloud.init({
env: "ju-9g1guvph88886b02",
});
const client = init(wx.cloud);
const models = client.models;
// 可以取消注释查看效果
// const { data } = await models.stapleFood.list({
// filter: {
// where: {}
// },
// pageSize: 10,
// pageNumber: 1,
// getCount: true,
// });
// console.log('当前的主食数据:');
// console.log(data.records);
}
});

二. 下单页面的实现


首先创建一个页面 goods-list 页面作为首页


顾客如果浏览下单页面,那么就需要看到当前可以选择的主食、配菜,还有他们分别的价格。所以首先我们需要把主食、配菜加载进来


// 加载主食
const stapleFood = (await models.stapleFood.list({
filter: {
where: {}
},
pageSize: 100, // 一次性加载完,
pageNumber: 1,
getCount: true,
})).data.records;

// 加载配菜
const sideDish = (await models.sideDish.list({
filter: {
where: {}
},
pageSize: 100, // 一次性加载完,
pageNumber: 1,
getCount: true,
})).data.records;

// pages/goods-list/index.js
Page({
data: {
// 总价格
totalPrize: 0,
// 选中的主食
selectedStapleFoodName: '',
// 选中的配菜
selectedSideDishName: [],
// 所有的主食
stapleFood: [],
// 所有的配菜
sideDish: [],

以下是全部的js代码


// pages/goods-list/index.js
Page({
data: {
// 总价格
totalPrize: 0,
// 选中的主食
selectedStapleFoodName: '',
// 选中的配菜
selectedSideDishName: [],
// 所有的主食
stapleFood: [],
// 所有的配菜
sideDish: [],
},

async onLoad(options) {
const models = getApp().globalData.models;
console.log('models', models)

// 加载主食
const stapleFood = (await models.stapleFood.list({
filter: {
where: {}
},
pageSize: 100, // 一次性加载完,
pageNumber: 1,
getCount: true,
})).data.records;

// 加载配菜
const sideDish = (await models.sideDish.list({
filter: {
where: {}
},
pageSize: 100, // 一次性加载完,
pageNumber: 1,
getCount: true,
})).data.records;

console.log({
stapleFood,
sideDish
});

this.setData({
stapleFood: stapleFood,
sideDish: sideDish
})
},

// 选中主食
onSelectStapleFood(event) {
this.setData({
selectedStapleFoodName: event.currentTarget.dataset.data.name
});

this.computeTotalPrize();
},

// 选中配菜
onSelectedSideDish(event) {
console.log(event);
// 选中配菜名字
const sideDishName = event.currentTarget.dataset.data.name;

// 如果已经选中,则取消选中
if (this.data.selectedSideDishName.includes(sideDishName)) {
this.setData({
selectedSideDishName: this.data.selectedSideDishName.filter((name) => (name !== sideDishName))
});
} else {
// 未选中,则选中
this.setData({
selectedSideDishName: this.data.selectedSideDishName.concat(sideDishName)
});
}

this.computeTotalPrize();
},

// 重新计算价格
computeTotalPrize() {
// 主食价格
let staplePrize = 0;
if (this.data.selectedStapleFoodName) {
staplePrize = this.data.stapleFood.find((staple) => staple.name === this.data.selectedStapleFoodName).prize;
}

// 配菜价格
let sideDish = 0;
this.data.selectedSideDishName.forEach((sideDishName) => {
sideDish += this.data.sideDish.find((sideDishItem) => (
sideDishItem.name === sideDishName
)).prize;
});

// 总价格
this.setData({
totalPrize: staplePrize + sideDish
})
},

// 提交
async onSubmit() {
// 提示正在加载中
wx.showLoading({
title: '正在提交订单',
});

const models = getApp().globalData.models;
const { data } = await models.order.create({
data: {
served: false, // 是否已出餐
sideDish: this.data.selectedSideDishName, // 配菜
stapleFoodName: this.data.selectedStapleFoodName, // 主食名称
prize: this.data.totalPrize, // 订单总价格
}
});

console.log(data);
wx.hideLoading();
}
});

接着来实现页面


<!--pages/goods-list/index.wxml-->
<view>
<view class="title">
<image src='/asset/pancake.png'></image>
<text class="title">请选择主食</text>
</view>

<!-- 主食展示 -->
<view class="staple-food">
<view wx:for="{{stapleFood}}" wx:key="_id">
<view bindtap="onSelectStapleFood" data-data="{{item}}" class="staple-food-item {{selectedStapleFoodName === item.name ? 'selected' : ''}}">
<image src="{{item.imageUrl}}"></image>
<view class="prize">{{item.prize}}¥</view>
</view>
</view>
</view>

<!-- 选择配菜 -->
<view class="title">
<image src='/asset/sideDish.png'></image>
请选择配菜
</view>

<!-- 配菜展示 -->
<view class="side-dish">
<view wx:for="{{sideDish}}" wx:key="_id">
<!-- 使得class动态绑定支持 includes 语法 -->
<wxs module="tool">
var includes = function (array, text) {
return array.indexOf(text) !== -1
}
module.exports.includes = includes;
</wxs>
<view class="side-dish-item {{tool.includes(selectedSideDishName, item.name) ? 'selected' : ''}}" bindtap="onSelectedSideDish" data-data="{{item}}">
<image src="{{item.imageUrl}}"></image>
<view class="prize">{{item.prize}}¥</view>
</view>
</view>
</view>

<!-- 底部菜单 -->
<view class="bottom-content">
<view class='bottom-info'>
<view wx:if="{{!!selectedStapleFoodName}}">主食:{{selectedStapleFoodName}}</view>
<view wx:if="{{selectedSideDishName.length !== 0}}">配菜:{{selectedSideDishName}}</view>
</view>

<view class="bottom-operate">
<view class="total-prize">当前价格<text class="prize">{{totalPrize}}¥</text></view>
<view class="submit-button {{!selectedStapleFoodName ? 'disabled' : ''}}" bind:tap="onSubmit">下单</view>
</view>
</view>
</view>

再添加一点点的样式


/* pages/goods-list/index.wxss */
.title {
display: flex;
align-items: center;
gap: 16rpx;
padding: 0 20rpx;
}

.title image {
height: 46rpx;
width: 46rpx;
}

.staple-food {
display: flex;
margin-bottom: 60rpx;
overflow: auto;
}

.staple-food-item {
margin: 20rpx 10rpx;
display: flex;
flex-direction: column;
border: 1px solid #f3f0ee;
box-shadow: 6rpx 6rpx 6rpx #dfdfdf, -6rpx -6rpx 6rpx #dfdfdf;
border-radius: 6rpx;
padding: 8rpx;
}

.staple-food-item.selected, .side-dish-item.selected {
box-shadow: 6rpx 6rpx 6rpx #58b566, -6rpx -6rpx 6rpx #58b566, 6rpx -6rpx 6rpx #58b566, -6rpx 6rpx 6rpx #58b566;
}

.staple-food-item image {
border-radius: 6rpx;
width: 300rpx;
height: 300rpx;
}

.prize {
padding: 6rpx 6rpx 0;
text-align: right;
color: orangered
}

.side-dish {
padding: 20rpx 12rpx;
display: flex;
gap: 12rpx;
overflow: auto;
}

.side-dish image {
height: 200rpx;
width: 200rpx;
}

.side-dish-item {
border-radius: 8px;
padding: 16rpx;
box-shadow: 6rpx 6rpx 6rpx #dfdfdf, -6rpx -6rpx 6rpx #dfdfdf;
}

.bottom-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
}

.bottom-info {
padding: 30rpx;
display: flex;
flex-direction: column;
color: grey;
font-size: 0.5em;
}

.bottom-content .total-prize {
padding: 0 30rpx;
}

.bottom-operate {
border-top: 1px solid #dfdfdf;
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
height: 100rpx;
}

.submit-button {
width: 350rpx;
color: white;
background: #22b85c;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}

.submit-button.disabled {
background: grey;
/* 注意,这里设置了当按钮置灰的时候,不可点击 */
pointer-events: none;
}

于是,煎饼摊的小程序就大功告成了!


接着就可以在云后台管理订单了,在将订单完成之后,即可在云后台将订单的状态修改成已完成。


图片


我们还可以做的更多…


是否可以在订单中新增一个点餐号,这样就知道是哪个顾客点的餐?是否可以使用数据模型的关联关系将配菜、主食和订单关联起来?


是否可以在小程序中创建一个管理订单的页面?是否可以添加优惠券数据表,来给客户一些限时优惠?


期待大家的体验反馈!


代码地址:github.com/vancece/qiL…


点击体验:tcb.cloud.tencent.com/cloud-admin…


作者:腾讯云云开发
来源:juejin.cn/post/7413376270518042651
收起阅读 »

这两年,我把28年以来欠的亏都吃完了...

前言 很长一段时间没有总结一下过去几个月的状态了,今天思绪万千,脑海中浮现了很多经历和生活片段,我把它记录下来。顺便今天聊一聊认知突破,分享我在买房这段时间吃过的亏,也希望作为你的前车之鉴。 买房 21年底的时候,那时刚好毕业三年,也正是互联网公司996最流行...
继续阅读 »

前言


很长一段时间没有总结一下过去几个月的状态了,今天思绪万千,脑海中浮现了很多经历和生活片段,我把它记录下来。顺便今天聊一聊认知突破,分享我在买房这段时间吃过的亏,也希望作为你的前车之鉴。


买房


21年底的时候,那时刚好毕业三年,也正是互联网公司996最流行的阶段,由于平时我不怎么花钱,也很少买衣服,上网买东西是个矛盾体,需要花很多时间对比,经常看了一件东西很久,最后又不买。加上比较高强度的工作状态,两点一线,可以说是没时间花钱,再加上自己把钱都拿去理财了,也赚了几万块,最后一共攒了几十万下来。我从小就立志要走出农村,而且认为以后有女朋友结婚也要房子,加上当时花比较多时间在理财上面,那时候其实行情已经不好了,工作上没什么突破,比较迷茫,于是想着干脆就把钱花出去了,自己也就有动力去搞各种路子尝试赚钱。在没有经过任何对比之后就在佛山买了一套房子,房价正是高峰的时候,于是我成功站岗!因为这个契机,躲过了持续了2年多的低迷股市,却没躲过低迷的房地产。


while(true) { 坑++ }


我买的是期房,当时不知道期房会有这么多坑,比如期间不确定开发商会不会破产,我这个开发商(龙光)就差点破产了,房产证无着落,相当于花了200w买了一个无证的房子,这辈子就算是搭进去了。


对于整个购房过程也是很懵逼,对流程完全不熟悉,当时去翻了政府规划文件,看那个地段后续有没有涨价空间,然后跟着亲戚介绍的销售转圈圈,当时说给我免3年物业费,合计也有几万块。在签合同之前销售都有说可以给到,但由于第一次没有录音,导致在签合同的时候销售反口,不承认,我们也没有证据,最后吃了哑巴亏。


开始的时候谈好了一个价格167w,然后销售私下打电话给我洗脑说我给点辛苦费1.5w,他可以向领导申请多几万块优惠。我知道这是他们的销售套路,但是架不住给我优惠5w啊,中间反复拉扯最后说给他8k,采用线下现金交易的方式。这一次我有录音了,因为私底下交易没有任何痕迹,也不合法,所以留了一手,也成为我后面维权时争取话语权的基础。


中介佣金是很乐观的,当时由于我亲戚推荐我去,销售承诺税前有4w,当时看中这个返佣也促使我火急火燎的交了定金。现在3年过去了,这个佣金依旧没有到账,我一度怀疑是中介搞ABC套路把我这个钱💰吃了,其他邻居的推荐佣金都到了账,加上现在地产商没钱了,同时跟那个亲戚有些过节,这个返佣更是遥遥无期。最后通过上面的录音获得了一丝话语权,知道了这个钱还在开发商手上,一直没有拨款下来到中介公司。下面是部分聊天记录:


image.png


不接受微信语音沟通,文字可以留给自己思考的时间,同时也更好收集证据。


image.png


然后去找相关人员把信息拉出来给我看,显示开发商未付款状态,这个状态维持2年了,目前看来只能再等下去。


image.png


签合同的时候,有个律师所说是协助我们签合同、备案、办房产证等各种边缘工作,糊里糊涂交了700元律师费,不交不行,甚至律师所连发票都没有给,而我都没有意识到这个最基本的法律法规问题。现在交房了可以办理房产证了,拿证下来也就80块登记费,居然收我700,其他业主有些是600多,400多,顿时觉得智商受到了侮辱,看了网上铁头各种打假的视频,我觉得自己也应该勇敢发声。现在也在收集商家各种违规证据,提交给相关部门解决。


image.png


image.png


image.png


后面市场监督管理局收到投诉,应该是有协商,意识到没有给我们发票,过来几天之后才把发票补过来,开票日期不是付款时候的2022年,而是2024年,明显属于偷税了。目前跟他要发票的应该只有我,估算2300多户业主都没有开发票的。


当时我首付需要50w,自己手上不够,我爸干建筑一辈子,辛苦供我们两个孩子上了大学,山上建了两层楼,手里没钱。我妈是一辈子没打过工,消极派,说出来没几句好话,家里不和睦的始作俑者,更不可能有钱支持。所以我还有20w是首付贷,也就是跟开发商借的,利率10%,这个利息很高了。销售当时说可以优惠到5%,但是优惠金额是补贴到总房价里面去,其实这也是他们的一种销售套路,这亏我也吃了,2年之后我连本带息还24w。当时认为自己应该一年左右能还完,但是实际远远高估自己的能力,买完房子接着我爸又生病在医院待了几个月,前后花了十几万,人生一下子跌入了谷底。


从头再来


后面2023一年,夫妻出去创业,很多人不赞同,期间遇到了不少小人诋毁我们两夫妻,当时我老婆还在怀孕,但我们最后都熬过来了,还生了一个儿子,6斤多。期间一年赚了十几万,但是开支也大,加上父母要养,我爸还要吃药,房子要供,最后还是选择了先稳定下来,我重新回到了职场,空窗一年后在这个环境下拿了一个还不错的offer,同时也想自己沉淀一下。


自从有了宝宝之后,生活似乎都往好的方面发展,出版社找我出书,为了契合自己的职业发展,我选择了写书《NestJS全栈开发秘籍》,从2023年11月份开始,迄今快半年了,在收尾阶段,希望尽快与各位读者们见面。同时,等了3年的房子也收房了,由于是高层,质量相对其他邻居好,没有出现成片天花掉下来或者漏水的情况。我们经常都说他是天使宝宝,是来报恩的。


由于我们公司技术部门是属于后勤支持性质的,技术变化不大,Vue2+微前端和React管理系统那一套,没有太多的新技术扩展,意味着不确定也大。业务发展不好考虑的是减少这些部门的开支,所以不出意外最近也迎来了降薪。这不是最可怕的,对于我们技术人来讲,最可怕的是我认为在业务中成长停滞了,或者没有业务来锻炼技术,所以在业余时间也选择了参与一些开源项目,如hello-alog开源算法书的代码贡献,并且这也是选择写书的原因。很简单地说,当下一个面试官问到我的时候,我不可能什么都讲不出来,最经典的问题就是:在这个公司期间你做过最有成就感的事情是什么?现在,我有了答案!


哲学


我的人生哲学是不断改变,拥抱不确定性!这么看来,我的确在这些年上了不少当,吃了不少亏,把自己搞的很累,甚至连累到家里人。但,用我老婆经常说的一句话:人生这么长,总是要经历点什么,再说现在也没有很差。的确,不断将自己处于变化之中,当不确定性降临到普罗大众时,我们唯一的优势,就是更加从容


总结


人们还在行走,我们的故事还在继续~


WechatIMG154.jpg


作者:元兮
来源:juejin.cn/post/7349136892333981711
收起阅读 »

运维打工人,周末兼职送外卖的一天

运维打工人,周末兼职送外卖的一天 在那个不经意的周末,我决定尝试一份新的工作——为美团外卖做兼职配送员。这份工作对于一向规律生活的我来说,既是突破也是挑战。 早晨,城市的喧嚣还未完全苏醒,空气中带着几分凉意和宁静。准备好出发时,线上生产环境出现问题,协助处理。...
继续阅读 »

运维打工人,周末兼职送外卖的一天


在那个不经意的周末,我决定尝试一份新的工作——为美团外卖做兼职配送员。这份工作对于一向规律生活的我来说,既是突破也是挑战。


早晨,城市的喧嚣还未完全苏醒,空气中带着几分凉意和宁静。准备好出发时,线上生产环境出现问题,协助处理。


收拾好后,戴上头盔,骑上踏板车,开始了自己的第一次外卖配送之旅。


刚开始,我的心情既紧张又兴奋。手机里的订单提示声是今日的任务号角。第一份订单来自一公里外的一家外卖便利店。我快速地在地图上规划路线,开启高德导航,发动踏板车,朝着目的地出发。


123.jpg


由于便利店在园区里面,转了两圈没找到,这是就慌张了,这找不到店咋办了,没办法赶紧问下旁边的老手骑手,也就顺利找到了,便利店,进门问老板,美团104号好了嘛?老板手一指,在架子上自己看。核对没问题,点击已达到店,然后在点击已取货。


然后在导航去收获目的地,找到C栋,找到107门牌号,紧接敲门,说您好,美团外卖到了,并顺利的送达,然后点击已送达,第一单顺利完成,4.8元顺利到手。


其中的小插曲,送给一个顾客时,手机导航提示目的地,结果一看,周围都拆了。没办法给顾客打电话,加微信确认位置具体在哪里,送达时,还差三分钟,这单就要超时了。


1.jpg


配送过程中,我遇到了第一个难题:找不到店家在哪里,我的内心不禁生出些许焦虑。但很快,我调整心态,不懂不知道的地方,需要多多问人。


紧接着,第二份、第三份订单接踵而至。每一次出发和到达,每一条街道和巷弄,我开始逐渐熟悉。


7.jpg


6.jpg


日落时分,我结束了一天的工作。虽然身体有些疲惫,但内心充满了前所未有的充实感。这份工作让我体验到了不一样的人生角色,感受到了城市节奏背后的种种辛劳与甘甜


周末的兼职跑美团外卖,对我来说不仅是一份简单的工作,更是一段特别的人生经历。它教会了我坚持与责任,让我在忙碌中找到了属于自己的节奏,在逆风中学会了更加珍惜每一次到达。


最后实际周六跑了4个小时,周天跑了7个小时,一共跑了71公里,合计收获了137.80,已提现到账。


5.jpg


2.png


作者:平凡的运维之路
来源:juejin.cn/post/7341669201010425893
收起阅读 »

代码与蓝湖ui颜色值一致!但页面效果出现色差问题?

web
前言 最近在开发新需求,按照蓝湖的ui图进行开发,但是在开发完部署后发现做出来的页面部分元素的颜色和设计图有出入,有色差!经过一步步的排查最终破案,解决。仅以此篇记录自己踩坑、学习的过程,也希望可以帮助到其他同学。 发现问题 事情是这样的,那是一个愉快的周五的...
继续阅读 »

前言


最近在开发新需求,按照蓝湖的ui图进行开发,但是在开发完部署后发现做出来的页面部分元素的颜色和设计图有出入,有色差!经过一步步的排查最终破案,解决。仅以此篇记录自己踩坑、学习的过程,也希望可以帮助到其他同学。


发现问题


事情是这样的,那是一个愉快的周五的下午,和往常一样我开心的提交了代码后进行打包发版,然后通知负责人查看我的工作成果。


但是,过了不久后,负责人找到了我,说我做出来的效果和ui有点出入,有的颜色有点不一样。我一脸懵逼,心想怎么可能呢,我是根据ui图来的,ui的颜色可是手把手从蓝湖复制到代码中的啊。


随后他就把页面和ui的对比效果图发了出来:


image.png


上图中左侧是蓝湖ui图,右侧是页面效果图。我定睛一看,哇趣!!!好像是有点不一样啊。 感觉右侧的比左侧的更亮一些。于是我赶紧本地查看我的页面和ui,果然也是同样问题! 开发时真的没注意,没发现这个问题!!!


排查问题


于是,我迅速开始进行问题排查,看看到底是什么问题,是值写错了?还是那里的问题。


ui、页面、代码对比


下图中:最上面部分是蓝湖ui图、下面左侧是我的页面、右侧是我的页面代码样式


image.png


仔细检查后发现颜色的值没错啊,我的代码中背景颜色、边框颜色的值都和ui的颜色值是一致的! 但这是什么问题呢??? 值都一样为什么渲染到页面会出现色差?


起初,我想到的是屏幕的问题,因为不同分辨率下展示出来的页面效果是会有差距的。但是经过查看发现同事的win10笔记本、我的mac笔记本、外接显示器上都存在颜色有色差这个问题!!!


ui、页面、源文件对比


通过对比ui、页面、颜色值,不同设备展示效果可以初步确认:和显示器关系不大。当我在百思不解的时候,我突然想到了ui设计师!ui提供的ui图是蓝湖上切出来的,那么她的源文件颜色是什么呢?


于是我火急火燎的联系到了公司ui小姐姐,让她发我源文件该元素的颜色值,结果值确实是一样的,但是!!! 源文件展示出来的效果好像和蓝湖上的不太一样!


然后我进行了对比(左侧蓝湖、右上页面、右下源文件):


image.png


可以看到源文件和我页面的效果基本一致!到这一步基本可以确定我的代码是没问题的!


尝试解决


首先去网上找了半天没有找到想要的答案,于是我灵光一现,想到了蓝湖客服!然后就询问了客服,为什么上传后的ui图内容和源文件有色差?


image.png


image.png


沟通了很久,期间我又和ui小姐姐在询问她的软件版本、电脑版本、源文件效果、设置等内容就不贴了,最终得到如下解答:


image.png


解决方式


下载最新版蓝湖插件,由于我们的ui小姐姐用的 sketch 切图工具,然后操作如下:


1.下载安装最新版蓝湖插件: lanhuapp.com/mac?formHea…


2.安装新版插件后--插件重置


3.后台程序退出 sketch,重新启动再次尝试打开蓝湖插件.


4.插件设置打开高清导出上传(重要!)


5.重新切图上传蓝湖


最终效果


左侧ui源文件、右侧蓝湖ui:
image.png


页面效果:


image.png


可以看到我的页面元素的border好像比ui粗一些,感觉设置0.5px就可以了,字体效果的话是因为我还没来得及下载ui对应的字体文件。


但是走到这一步发现整体效果已经和ui图到达了95%以上相似了,不至于和开始有那么明显的色差。


总结


至此,问题已经基本是解决。遇到问题不能怕,多想一想,然后有思路后就一步一步排查、尝试解决问题。当解决完问题后会发现心情舒畅!整个人都好起来了,也会增加自信心!


作者:尖椒土豆sss
来源:juejin.cn/post/7410712345226035200
收起阅读 »

基于Vue.js和高德地图API来实现一个简易的天气预报

web
今天就让我们来使用 Vue.js 和高德地图开放平台提供的 API,实现一个关于天气预报的小demo,实时查询当前城市的天气以及未来三天的天气预测,且实现切换城市查询。实现效果如下; 准备工作 既然要使用真实的数据,那么就需要用到高德地图开放平台提供的天气查...
继续阅读 »

今天就让我们来使用 Vue.js 和高德地图开放平台提供的 API,实现一个关于天气预报的小demo,实时查询当前城市的天气以及未来三天的天气预测,且实现切换城市查询。实现效果如下;


PixPin_2024-12-15_00-13-38.gif


准备工作


既然要使用真实的数据,那么就需要用到高德地图开放平台提供的天气查询 API,先高德地图api注册为开发者。然后点击文档与支持,选择JS API。


image.png


然后登录到控制台创建一个应用并且添加一个key,服务平台为Web端(JS API)。
16b5ba85e6c5f128b699fe8d521bb67.jpg


终端npm create vite@latest使用vite创建项目,npm install下载该项目需要用的包,npm run dev运行项目。


image.png


将天气预报的功能全部开发在weather.vue里面,再将这个组件import weather from "./components/weather.vue"引入到app.vue中。


image.png


js代码概览


image.png


具体代码步骤实现


开始weather.vue里面的代码了。


html 部分


<div>
// 头部
<div class="head">
<div class="city-name">
<i class="iconfont icon-dingwei"></i>
{{ state.city }}
</div>
<div @click="toggle" class="city-change">
<i class="iconfont icon-24gf-city3"></i>
切换城市
</div>
</div>


// 中间部分实时温度
<div class="main">
<div class="weather-info">
<p class="temp">{{ state.weather.temperature }}℃</p>
<div class="info">{{ state.weather.weather }}</div>
<div class="detail">
<div class="item">
<i class="iconfont icon-shuidi"></i>
<span>湿度</span>
<span>{{ state.weather.humidity }}</span>
</div>
<div class="item">
<i class="iconfont icon-feng"></i>
<span>风向</span>
<span>{{ state.weather.windDirection }}</span>
</div>
<div class="item">
<i class="iconfont icon-fengli"></i>
<span>风力</span>
<span>{{ state.weather.windPower }}</span>
</div>
</div>
</div>

// 未来三日的天气预报
<div class="future">
<div class="future-title">三日天气预报</div>
<div class="future-content">
<div v-for="(item,i) in state.future" class="forecast">
<p class="week">周{{ chinese[Number(item.week)-1] }}</p>
<i :class="getWeatherIcon(item.dayWeather)"></i>
<p><span class="left">{{ item.dayTemp }}℃</span> <span class="right"> / {{ item.nightTemp }}℃</span></p>
</div>
</div>
</div>
</div>


// 切换城市input框
<div v-show="state.isVisible" >
<input id="newCity" @keydown.enter="handle" type="text" v-model="state.newCity" placeholder="请输入你要查询的城市">
</div>
</div>


然后使用css样式美化成如下界面


image.png


js部分


接下来就是渲染其中的数据了,首先使用高德 api 来获取定位数据,查看官方文档,JS API结合 Vue 使用,首先安装Loader,如下所示,复制到当前文件终端安装。
image.png


然后复制代码粘贴;
image.png


AMapLoader 是高德地图 js API 的加载器,它可以在前端项目中加载和初始化高德地图的 js API。


import AMapLoader from '@amap/amap-jsapi-loader';
import { onMounted, reactive } from 'vue'

onMounted(() => {   // 在浏览器上出现内容时候触发
// 加载官方提供的方法
window._AMapSecurityConfig = {
securityJsCode: "", // 密钥
};
AMapLoader.load({
key: "", // 申请好的Web端开发者Key,首次调用 load 时必填
version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
plugins: ["AMap.Scale"], //需要使用的的插件列表,如比例尺'AMap.Scale',支持添加多个如:['...','...']
})

// 加载完上面代码高德提供的服务后,执行then后面的操作
.then((AMap) => {
// 获取定位
getLocalCity(AMap) // 使用一个函数,将获取地址信息放到这个函数中
})
})

获取城市信息


官方文档:
image.png


const getLocalCity = (AMap) => {
AMap.plugin('AMap.CitySearch', function () {
var citySearch = new AMap.CitySearch()
citySearch.getLocalCity(function (status, result) {
if (status === 'complete' && result.info === 'OK') {
// 查询成功,result即为当前所在城市信息
console.log(result.city); // 会打印当前城市
state.city = result.city //将城市改为定位获取到的城市
getWeather(AMap) // 获取天气
}
})
})
}

image.png


利用该地址获取实时天气数据
image.png


const getWeather = (AMap) => {
//加载天气查询插件
AMap.plugin("AMap.Weather", function () {
//创建天气查询实例
var weather = new AMap.Weather();
//执行实时天气信息查询
weather.getLive(state.city, function (err, data) { // 将城市替换成state.city
console.log(err, data); // 获取天气数据,详情见下表
state.weather = data // 将数据赋值给 state.weather
getForecast(AMap) // 后面用来获取未来三天的天气
});
});
}

image.png
将这一整个对象赋值给state.weather然后再state.weather.渲染到页面上。


获取未来三天天气


const getForecast = (AMap) => {
AMap.plugin("AMap.Weather", function () {
//创建天气查询实例
var weather = new AMap.Weather();
//执行实时天气信息查询
weather.getForecast(state.city, function (err, data) {
console.log(err, data);
state.future = data.forecasts // 获取天气预报数据

//err 正确时返回 null
//data 返回天气预报数据,返回数据见下表
});
});
}

image.png


最后就是切换城市中的input框的实现;


<div v-show="state.isVisible" >
<input id="newCity" @keydown.enter="handle" type="text" v-model="state.newCity" placeholder="请输入你要查询的城市">
</div>

添加以一个v-show方法,然后绑定一个键盘敲击事件触发handle,并用v-model获取输入的数据并将其存储到state.newCity


const handle = () => {
state.isVisible =!state.isVisible // 回车键将框不显示
state.city = state.newCity // 城市变为输入的城市
getWeather(AMap) // 重新获取该城市天气以及该城市未来天气
}

const toggle = () => {
state.isVisible =!state.isVisible // 使得点击切换城市框会显示和消失
}

以上就是实现获取定位城市,该城市的实时天气,以及未来三天的天气预测,切换查询其它城市的功能具体代码了。


总结


以上使用了Vue.js 组件化的方式来构建界面,利用高德地图 API 获取定位和天气数据,利用 Vue 的响应式机制来实时更新页面数据,通过使用官方文档中 AMapLoader 加载高德地图的JS API,使得我们能高效处理地图相关功能,希望这个小 demo 能够对你的前端开发有所帮助,同时记得给文章点点赞哦🤗。


image.png


作者:六个点
来源:juejin.cn/post/7448246468471521307
收起阅读 »

一个普通人的27岁

致工作三年即将27岁的自己 这是一篇自己的碎碎念、即回顾自己以前的成长经历、也小小的持有一下对未来的期待。 我是一个双非本科从事于Java开发的一名普普通通的码农、不同于大多数人的27岁、大部分人在这个年龄都已经工作了4/5年、而我也恰恰刚刚满三年而已。 读书...
继续阅读 »

致工作三年即将27岁的自己


这是一篇自己的碎碎念、即回顾自己以前的成长经历、也小小的持有一下对未来的期待。


我是一个双非本科从事于Java开发的一名普普通通的码农、不同于大多数人的27岁、大部分人在这个年龄都已经工作了4/5年、而我也恰恰刚刚满三年而已。


读书


小时候的记忆很模糊、很少关于有父母的记忆、从小的印象就是他们在很远的地方打工、那边还有一个从未谋面的哥哥、小时候的记忆更多是和爷爷奶奶在一起,爷爷在我记事起、他就很忙、很少在家里也或许是我不记事或者缺少了这部分的记忆。


在小时候的记忆里、住在茅草屋里面、那个时候家里还没有完全通电、印象里经常点煤油灯、这个时间段应该是02/03年的时候、记忆里这个时候家里养了一头牛、是一头老黄牛。家里在需要耕田播种的时候、不管风吹日晒、都能看见爷爷在田里一边驾驭着黄牛、嘴边一直在说什么、应该是教导牛牛该怎么走以便使的犁田犁的更好。记不清了、只知道每次遇到下雨的时候、爷爷披着蓑衣带着一个草帽、颇有一些武林大侠的气息。


那个时候家里有一条很凶很凶的狗、幺爷爷家里还是一条白猫、年龄比我那个时候都大。这条很凶的狗已经不记得长啥样了、甚至什么时候去世的都没有印象。


关于这条狗不多的记忆就是、它很凶、但凡看见我们在地坝(四川话:门前小院的意思、通常用于晒一些农作物的地方、或者夜晚乘凉的地方)里面打闹。它都会狂吠不止、这是对它的一个记忆。


还有一个记忆就是,记得是在某一个夏天、在屋后发现了一只野兔、这个时候不记得是不是爸妈在家了、全家都在追这个野兔、追了好久、这条狗也在追、有一个画面就是在我小时候的眼里那个农田的岸边很高、这个直接就从岸边往下面的田里跳下去、连续跳了好几个这样的梯田、那个姿势在我眼里好帅好帅、现在都记得很清楚。最后这个野兔是被抓住了、炸了酥肉、那个味道真的很香、现在都记忆深刻。毕竟小时候家里都是吃猪油、用很小很小的一块、煎出油炒菜。


幺爷爷在的猫是条白猫,印象里是一条抓老鼠的好手、但是不知道它什么死的、只记得大概有十三岁左右。


奶奶有风湿心脏病、那个时候总会吃一些药和一些偏方、记忆里有这么一幕、爷爷把刚出生的小狗狗杀掉、给奶奶治病、嘴馋的我也要吃、结果吃了一口就闷住了。


奶奶在我的记忆里有个画面就是我不想去读书、在上学的路上跑到了一个斜坡上、就如同那个时候黑白电视机上播放的游击战争片一样、以为自己躲在这里他们指定找不到、当然了最后肯定少不了一顿打。印象里只被奶奶打了这一次。


奶奶是在06年走的、不到六十岁。记忆特别深,当时哥哥从东北回老家也快一年了、那是在一个夜晚、哥哥先去睡了、我和其他堂哥堂姐在家里看电视、电视里播放的是洪金宝主演的、是一部战争片、大概就是他们去越南救什么人、有一个人在飞机上跳伞的时候说要倒数十个数、然后打开伞、结果这个嘴吃的人没数到就摔死了。里面有个画面用草杀人、后面还依葫芦画瓢学过这个东西。


奶奶走的时候、爷爷是第一个发现的、我记得爷爷发现之后、我去把哥哥喊醒了、然后我就一直在哭。虽然当时不知道死亡意味着什么、就是在哭、那个时候我上三年级了。奶奶走的那天的天气很好、我还记得我捡了一个螺母回家、后来我把这个螺母扔掉了、当时就想如果不捡这个螺母就好了、奶奶就不会走了。


第一次见哥哥的时候是在一个夏天、爸妈把他从东北送了回来、打算让他家里面读书、当然读书的地方现在已经垮掉了。那个时候家里的公路还是泥巴路、泥巴路大概还是前一年挖机采挖的、挖坏了几个秧田。他们在回来的前一年、写了一封信寄回来、内容是什么记不住了、只记得有一封信、分别向爷爷奶奶以及我都写了点东西。初次见面的时候很陌生、眼前这个和我差不多高有点黑的就是我哥、我的关注并没有在他的身上、更多的是他们提的袋子里面、因为有一袋子糖。


当然了小时候的记忆还有几个画面、就埋藏在心里吧、为什么说上面的狗狗很凶、因为他在我堂姐的脸上留下了印子、现在都能看见。


奶奶走掉之后、我和我哥就去了东北、因为家里没人会做饭了。就去东北读书、东北的记忆说不上多好、校园霸凌是一个很常见的事情。


在东北这三年、父母总是在吵架打架、住在平房里面、附近都是和父母一样的体力劳动者、他们一闹附近的人都会知道。我们的右边住了一个也是一个外出务工者、他们的有个女儿、比我和我哥都大、长得很白。在我们的左边也是一户外地务工者、不过是东三省的、不是四川的、这家的女主人好像很贤惠很好看、长得也很白。


在这期间、四川发生了很大的一件事、汶川地震、当时我记得我和附近的小孩偷偷跑去上网、结果附近的网吧都没网、然后回家就看到电视上到处都在播放新闻、去上学的时候、学校组织了捐款、我捐了五块钱。


小学结束之后、过完了六年级的暑假我就被送回到了老家、走的时候是和爸爸在工地上的工友一路的、正好他要回家。他和我们是一个地方的,记得大概是午饭后、叫了一辆出租车、我就和这个工友上了车、爸爸的这个工友坐在了副驾、我坐在了后排、送行的人有几个、车窗升起、行驶了一段路后、眼泪就落下了、大概知道了以后又不会在爸妈身边了、也不知道为什么没有哭出声、就和电视里面一样。这就是小学的记忆。


大概走了三四天、回到家了、就开始上初中了。


报名的时候见到了很多小学同学、他们很容易就认出我来了、然而我并没有很快的认出他们、他们说我五官没什么变化、很好认。


初中是在一所民办初中读的、我们这边的公立学校很水、很乱、上课打牌抽烟都存在、老师也不会管。而且离我们也很远。民办学校离我家很近、这里的校长和附近的家长都很熟悉、自然而然的就去读了、自然而然的也会听到这样的交代、娃儿不听话不好好读书就整哦。初中的算是目前为止的小高光、因为那个时候自己还算聪明、成绩也还算可以、被当着升学的苗子重点关注。当然最后也还算争气、以A+1的成绩考进县一中、我们这一届也还算争气、有一个去了同济大学、算是历史最好的一届了,当然这个学校现在也垮了。


高中的时候流传出了一个梗、你的数学不会是体育老师教的吧、那个时候会自嘲、我初中的时候、不止是数学是体育老师教的、历史和物理也是体育老师教的、这个老师还没上过高中。


高中是lol很火的时候、那个时候脱离了棍棒教育的我、理所当然的沉迷了进去、高一上学期还好、棍棒教育的习惯还在、期末考试全年级2000多人我考了200名左右、班上第五名好像。


学习态度的变化不止是因为lol、还记得当时班上有个人说我很努力、所以成绩这样、当时不知道是脑子抽了还是咋了就认为别人在说我笨、然后就慢慢放弃了之前的学习方式、再加上联盟的影响、自然而然的成绩一落千丈、后来也就去复读了。


复读这一年没什么特别的记忆、涨了几十分、去了一所双非学校。还是没有做到高一班主任说的那样、你好好读上个一本不成问题。当时学校的升学率是前60名可以上川大的程度、200名左右上一个一本好像确实不是什么问题。但也确实没做到。


上了大学就和大部分人一样、加部门、当班干部、实际上就是混吃等死。不同的是大二那年、由于初中埋下的病因、做了双侧股骨头置换手术、这一下就把家里面掏空了、手术是在北京做的、花了20+、是在18年、三月一号做的左腿三月14号下午14:17做的右腿、刚检测出来的时候很崩溃、出了诊室就哭了、因为知道这么大笔钱家里出不起、当时借住在北京的姐姐家,在十五楼窗口处、恐惧战胜了勇气、没有跳下去。


查出来的时候就告知了父母、父母当时在深圳上班、我一个人去的北京找的姐姐、父亲先赶过来、看见父亲憔悴的面庞、自己也彻底取消了跳下去的想法、太憔悴了、没见过这个样子的父亲、也无法去想象如果跳了父亲会咋样、只知道那个时候父亲的头发白了很多、然后开始秃头了。


做手术的那几天恰逢过年期间、医院的人很多、见识了人生百态、有的人痛苦呻吟着想活下去,有的人沉默不语想离开人世,坐在轮椅上的时候、被推出去透透风、看见了一个和我一般大的人、少了一条腿,那个时候心里想着都是苦命人。不同于大一暑假工被晒的黢黑的我,在学校看到一个老外、老外的黑衬托出我的白,那个时候由于被晒的黢黑心情很糟糕,见到这个交换生之后得到了极大的安慰。


因为这个手术需要人照顾、学校是上下铺、因此休学一年、手术很顺利、在我们眼里是一个天大的事情、在医生眼里如果一个小手术一般、就和普通的感冒差不多。术后也会恢复的很好、有一段时间是长短腿、走路一瘸一拐的、过了两个多月吧就彻底正常了。到目前为止至少没什么问题。唱跳rap不打篮球。


后面的大学时光就很平平无奇、本以为就和之前的师兄师姐一样正常大学然后毕业、后面就遇到了口罩事件、在学校都没有好好学习、在家里怎么可能会好好学习、真的是在混吃等死、大学期间没有什么特别的记忆、唯一的印象就是大一老校区是一群室友、大二搬到新校区、又换了一批室友、寝室从原来和其他专业的混寝、变成了同专业的混寝、但是由于休学一年、复学的时候又被安排到新的寝室、又换了一批室友、读了一年这一批室友毕业了、我大四的时候又换了一批室友。也就是一年一批室友。也算是独一份了。不过后面的都没怎么联系了。


这就是整个读书生涯了。还有很多画面就埋藏在心底吧。


工作


毕业之后、第一年认识了一个老师、养鱼达人、第一次约她出来玩、就问我用什么去接他、给了刚毕业的我一个暴击。于是呼在工作上加把力,从刚毕业的几千块不到一年的时间就破万了。也就是在22年左右吧。这个时候总觉得自己谈恋爱应该有点底气了。可在24年又给了我一个暴击。也就是今年。


在整个22年里面、由于工作还行、有大量的自由时间、在b上学习了尚硅谷的mysql和jvm课程、在慕课网上学习Java高并发课程、还算充实、虽然工作上用到的不多。


在22年、养了一只猫取名壹贰、是只三花、很粘人,也很喜欢、但我把它放在老家了。我的头像就是它很可爱吧。


在23年里、由于之前的学习累积、总觉得要记录一下、避免用的时候又到处找、就开始了写博客这个过程、博客更新的速度很稳定、生活节奏也很稳定、每天下班之后、买菜回家做晚饭和第二天中午的午餐、厨艺和刀工得到了大涨,每天晚上还能学习两小时、从周末开始选题、工作日开始编码、验证写博客、一切都有条不紊的进行着,生活节奏很稳定、窗外的阳光也很温暖。


23年发生了一件事、就是爷爷走了、遗憾的是没有带个孙媳妇回去让他看一眼、爷爷是五月份走的、守灵的那个晚上、睡在爷爷旁边、没有丝毫的害怕、下葬的那一天、没有哭但全是遗憾。至此带我长大的两个人都离开了人世。


在23年11月份的时候、认识了一个菇凉、她的名字很好听、长得也很好看、她的生活多姿多彩、现在都觉得她活得很多姿多彩。就和大家想的那样、慢慢的喜欢上了这个人、好巧不巧的是她对我也有点点意思吧、然后就约着出来玩、一起看电影、一起跨年等等、初期总是美好的、回忆也是。她不会做饭、总是吃外卖、我会让她点菜然后我做好了带给她吃、无论什么时候会送她回家然后自己再回家、每次见面都会给她准备一点零食或者小惊喜、理所当然的我们在一起了、直到过完年之后的某一个周末、我朋友约我们出去玩、在晚上回来的时候、我朋友买了房子(和女朋友一起买的)、刚好又说到这个问题、我就说了一句以后我们也一起买、用公积金带款、然后就因为这个问题讨论了一周、直到最后的分手。


具体的细节问题就不说了。我工作三年攒了一些钱、家里修房子我出了一点钱。一时间我家肯定是拿不出来的、我想让他给我点时间、结果不愿意、她之前有过很长一段时间的恋情被分手、大概是害怕再浪费时间、也能理解。


刚分手那段时间、感觉像是丢了半条命。心态很崩溃、觉得自己很差劲、一眼望到了头、好像也成不了家。掘金的更新速度就能看出来影响,虽然在一起的时间不长、三月份分的手、到现在为止有些时候都会因为这件事emo。


分手之前很喜欢做饭、分手之后再也没做过饭、看着那些为了给她做菜买的调味品以及打包盒、总是别有一番滋味、有时候总觉得自己要是当时做的再好一点就好了。在这期间看了一些心理学相关的书、也学会了一些调整自己的方法。


分手这段时间里、激情消费买了辆车、自驾去了一趟若尔盖大草原、草原很好看、自此身上的积蓄被自己花得差不多了。不止如此、由于工作上没有任何发展、总是干一些和Java无关的事情、甚至打算让我做嵌入式开发和大模型这一类工作,职业发展也看到了头。


整理生活这段时间丢了很多东西、总感觉自己也把自己丢了、好在慢慢的把自己拼好重新捡起来了。


下一个月也就马上27岁了、看着身边同龄的人要么成家、要么即将成家、要么事业有成、自己还是孤家寡人,多多少少也很羡慕。


站在生活这条十字路口、迷茫、彷徨、不安、焦虑每隔一段时间都会出现在自己身边、好在自己的调整能力得到了极大的提升、看书总归是有用的。


古人云:三十而立、至少现在看来、在这有限的时间里很难立起来了、但总要去试试、说不定就成了呢。


未来会是什么样子的呢?不知道,能把自己的生活过好就已经很不错了。感知幸福是一种能力、感知焦虑也是。


对生活的感悟如同总有千言万语、却有一种如鲠在喉的感觉。不知道命运会给我带来什么样的生活?不管怎么样都坦然接受吧。期待吗?期待吧。


写到这里、感受万千、内心细腻的人总是容易伤春悲秋。


回顾过往、就如同这篇文字一样、普普通通平平无奇、都无法用鸡肋来形容。但相信生活不会辜负每一个好好生活的人、始终对未来抱有期待与憧憬。不管最终如何、终将相信我们都会过上自己想要的生活。


最后给自己定一个目标吧:



  • 坚持写博客、写到35岁,我相信自己会一直从事计算机行业的!

  • 健健康康的活到退休。


窗外的天空很蓝、阳光很温暖、最近的心情也很好、希望您也是。


谢谢您能看到这里,祝君心想事成、万事顺遂。


作者:晚_风
来源:juejin.cn/post/7396609176744886310
收起阅读 »

独立开发上班后:我的故事,你的酒,一腔沉默往前走

有时候,我会断言世间奇妙的事情都让我赶上了。后来又觉得是自作多情,因为上天压根就没正眼瞧过我,何谈特意针对,我经历的只是业界常态而已。 我,一名老程序员,失业,本以为能凭借一人可抵一支小团队的技术能力,混口饭吃,实现小富。但是,漂浮半年,收入不及原来工资的二...
继续阅读 »

有时候,我会断言世间奇妙的事情都让我赶上了。后来又觉得是自作多情,因为上天压根就没正眼瞧过我,何谈特意针对,我经历的只是业界常态而已。



我,一名老程序员,失业,本以为能凭借一人可抵一支小团队的技术能力,混口饭吃,实现小富。但是,漂浮半年,收入不及原来工资的二分之一。


最终无奈重新找工作,已上班一个月有余。


找工作时,我特意选择了一家不是很忙的公司。选这类公司,我也是费了一番功夫。首先是凭借自己的判断:一定是传统大型企业,IT人数占比不超过5%。这样的配置说明公司主业务不以IT为主,IT部门仅仅是给传统业务打辅助。其次,从内部的IT员工验证是否加班多。小城市的圈子不大,好打听。我了解到这个公司最近一年内基本不加班。面试时,HR和部门领导也以不加班为谈判优势。


随后,面对一众offer(包含涨薪到120%的巨忙公司)我选择了这家降薪至80%的不忙公司。因为之前的经历,太多无意义的忙碌,会导致生活失去意义。入职后,签了很多协议,包括薪资构成。谈好的固定工资,变成50%基本工资+50%绩效工资。也就是说即便选择降薪,这钱我可能也拿不全。HR说你不用担心,如果你不犯错就能拿全。


好吧。


我开始了我的工作。


和我同时入职的,还有一个空降而来的IT高管。公司历来对IT部门不满意。新来的IT高管对公司进行了诊断,发现一个问题:IT人员的工作不饱和啊,居然不加班!干这行哪有不加班的?!


其实,这个问题,我也发现了。我常环顾四周,发现80%的人60%的时间都在玩手机。


随后,我似乎开始了单休生涯。第一次周六加班,说是要专门验证我AI算法的正确率。然而,我监控了一天,我写的接口一次都没有被调用。晚上,领导问我怎么样?我说,没有收到任何反馈。领导说摇摇头说,唉,正确率真的很差。我又去问测试,测试说模板样例都生成不了,还没到算法检测那一步。


第二次周六加班,是上线后验证整个业务流程。但是,这个线一直没有上去。Java发版一直有问题,提示版本号不对,找不到JDK。而且,生产发版不是运维操作,也不是测试操作,是开发人员自己操作。


我一开始,非常热心。前端写的小程序拍照不清晰,总是出现糊了一片的情况,前端说受限于平台,解决不了。安卓和测试讨论说,一连串音频很难实现依次播放,更无法实现打断一个音频播另一个音频。我则转身就把Demo写好了给他们。后端说不用考虑并发,我说现在不考虑后面肯定会出问题。


表面上看是你帮团队处理卡点问题,实则是你抬高自己贬低别人,对立面包括你的同事和领导。这导致有我不在场的会议,张三说那个新来的算法能力不行,我给他发了一个基础算法问题,他看不懂。而此时,我根本不认识张三。这件事,是李四告诉我的。


先做人再做事,是我们千百年以来的文化精髓。我以为选择了计算机,可以有效地避免这个问题,实际上这并不可能。


其实,我应当学会和环境共存。翻看我以前写的文章,好像也充满了类似的抱怨和愤懑。这说明现状大抵就是这样,是我自己的问题。


如果一个人长期在不变的圈子里混,说明你也不是什么高人。否则,你早就跳出圈外了。既然没有跳到圈外,那么你还有需要突破的东西。就比如清高、严谨或者太过于纯粹。


一个纯粹的人,可能会推崇一种非常耐用的电灯泡,甚至可以亮一百年不会坏。但是,这会导致产品只能卖一次,根本没有替换的需求,最终结局是工厂倒闭。它可能是个精品,甚至是一个真理,但它并不符合市场规则,无法生存。这就好比你写出非常完美,不需要任何修改和维护的项目一样。


很好。你有这种能力,能让一个技术团队,人缩减到原来的50%, 活承担到原来200%,稳定率还能提高到原来的120%。但是,被砍掉的那部分人乐意吗?被削减了势力的总监乐意吗?你凭什么要这样?他们凭什么会那样?


你可能会觉得老板肯定乐意吧。不一定。你觉得,老板是更信任你呢,还是更信任跟随他创业多年的兄弟。他甚至认为你不是来降本增效的,而是来搞破坏的。


这很形象。在你不是权威教育专家的情况下,你有好的教育方式,你要用在你孩子身上,别拿别人的孩子下手啊。


打工和创业,完全不一样。打工,你做好自己分内的事情就好。没有人来找你,或者你对形势不是非常了解,不要乱动。


所以,并不是能力越大,责任越大。而是权利越大,责任越大。


慢慢熬着吧,熬到和同事们关系融洽,熬到老板信任了你,或许那时更容易施展一些策略。


看着眼前的一幕幕,一个个坑,该参与就参与,该跳就跳。别谈故事别谈酒,低头沉默往前走。



本故事纯属虚构,如有雷同,纯属巧合。



作者:TF男孩
来源:juejin.cn/post/7435175934170529830
收起阅读 »

普通人能否彻底告别代码学习?

小明:“嘿,AI,你说我还需要学编程吗?” 机器人:“理论上,当我达到‘终极智能’时,你可能就不需要了。但现在嘛,还是得学一点。” 小程:“那程序员呢?他们会被你取代吗?” 机器人:“哈哈,别担心!虽然我能帮你写代码,但现阶段我更像是个助手,而不是替代者。” ...
继续阅读 »

小明:“嘿,AI,你说我还需要学编程吗?”


机器人:“理论上,当我达到‘终极智能’时,你可能就不需要了。但现在嘛,还是得学一点。”


小程:“那程序员呢?他们会被你取代吗?”


机器人:“哈哈,别担心!虽然我能帮你写代码,但现阶段我更像是个助手,而不是替代者。”


普通人能否彻底告别代码学习,直接使用AI编程?


就这个问题我们先来看看几位大佬们的观点:


百度-李彦宏


2024《对话·开年说》系列中,百度公司创始人、董事长李彦宏在节目中表示“以后不会存在程序员这种职业了”



360-周鸿祎


在《对话》现场,360创始人兼董事长周鸿祎对李彦宏的观点提出反对意见“我不同意这个观点”。



英伟达-黄仁勋


黄仁勋认为,即便是在人工智能(AI)革命刚刚起步的今天,编程已不再是一项关键技能。



可以看到大佬们对AI与编程的影响这个问题的回答,虽然有差异,但我们不难看出AI在影响着编程这个职业或者行业。或许随着技术的发展与成熟,最终AI可以完全的代替人类。当然这个时间可能是一万年或许会更长或者更短。



之前也有读过阮一峰大佬《未来世界的幸存者》, 2018年7月发表的“技术的边界”中有写到:


“人工智能领域有一个概念,叫做“终极智能”。意思是,当机器的智能达到这种程度时,就不需要人类再做发明创造了,因为机器自己就会发明创造。”


我们来看看现阶段AI能给我们编程带来些什么?


一、专业性AI编程插件的能力


自从GPT带动全球AI热潮,AI席卷着各行各业。而在编程界也发生了巨大的变化,最出名的莫过于OpenAI与GitHub联合开发的Github Copilot。Github Copilot带动了一大堆AI编程工具的出现。


当然除了Github Copilot之外还有很多优秀的AI编程插件,我们来具体看一看:



p.s.以上的下载量与评分均只是plugins.jetbrains的marketplace数据,发布的时长也不相同,数据仅供参考。


基本AI编程工具的功能都差不多:



  • 代码补全:根据当前代码上下文自动补全代码。

  • 根据注释生成代码:根据注释描述生成相应的代码。

  • 方法和函数生成:根据方法名或函数名自动生成该方法或函数的代码。

  • 生成测试代码:生成测试代码。

  • ....


这里选择豆包MarsCode来展示AI编程插件的功能:



MarsCode 是豆包旗下的智能编程助手,提供以智能代码补全为代表的核心能力,支持主流编程语言及 IDE,能在编码过程中提供单行或整个函数的建议,同时支持在用户编码过程中提供代码解释、单测生成、问题修复、技术问答等辅助功能,提升编码效率与质量。


安装方式


JetBrains与 Visual Studio Code都可以安装,比如下面就是Visual Studio Code中编程助手的安装,在市场搜索后进行安装。



安装好后就可以看到AI功能界面



主要功能



  • 行级/函数级实时补全、注释生成代码


在编码过程中提供单行或多行的代码推荐,并支持通过注释生成代码片段,提升代码编写速度。



我只写了注释,回车后代码就自动会生成



  • 代码解释


精确解释项目代码,帮助开发人员快速熟悉项目。



生成代码注释




  • 单元测试生成


为选中函数生成单测,提升单测覆盖率,提升代码质量。




  • 智能修复


一键修改代码bug,提升代码修复效率。


当运行程序出现bug后,“AI Fix”图标会自动出现,点击后会可以通过AI生成相应的解决方案,解决方案里也会有相应的按钮半自动化的处理,非常的方便。




  • AI 智能问答


针对研发领域定向优化问答质量,提供更精准的问答结果。



1 通用性AI产品的编程能力


目前市面上能实现编程的AI产品非常多,基本上AI产品都会带编程的能力,比如chatgpt、文心一言、通义千问、豆包等


下面我们用chatgpt4o与kimi的对比,来了解通用性AI产品的编程能力


2 生成手机正则代码



  • KIMI


这里是KIMI生成的代码,



把代码贴到IDE中是可以直接执行的




  • chatgpt4o


和KIMI类似生成相应的正则代码,不过chatgpt默认会生成不同国家的手机号格式的正则



当然这只是比较简单的正则表达式。不过这种情况下就不需要我们去学习复杂的正则表达试的语法了,直接拿过来用就可以了。


再如core表达式也是类似的,比如:每周一晚上10执行一次的core表达式


就需要我们专门去学习core表达式的语法了



3 不同语言代码转换


我们现在让AI把上面的python代码转换成javascript代码



  • KIMI




  • chatgpt



这次两者是完全相同的,在IDE里也是可以执行的



4 生成PDF电子签名


让AI生成PDF电子签名的代码



  • kimi


给chatgpt以下需求:请写出itextpdf5实现pdf电子签名的代码


生成的代码直接放到IDE里还是不能直接使用的



可以看到是缺少import,kimi生成的代码中import并不完整,先把缺少的import先引入



引入后还是有多处错误


1)函数参数类型不正确


2)变量没定义


3)无对象枚举



  • chatgpt


给chatgpt相同的需求



把生成后的代码拷贝到IDE中,可以看到依赖已经下载好了,程序还是会报错



发现是import引用缺失,增加相应的import。


然后还是会发现PdfSignatureAppearance是没有WINCER_SIGNED枚举。一般来说就是引用的版本不对,说明itext生成的代码依赖与代码是不对称的。



虽然chatgpt4o生成的效果好一点,但还是不能直接使用。但大体上还是能知道实现PDF电子签名的技术实现,微调后还是可以使用。还是得完全懂代码的人才能正直使用起来。


总结


AI辅助编程给我们带来了一次变革,但目前或者很长一段时间内它的作用还是辅助的。并没有达到能代替程序员的能力。


像生成代码、代码注释、单元测试、bug自动修复等功能对编程的助力是非常大的。


我是栈江湖,如果你喜欢此文章,不要忘记点赞+关注


作者:栈江湖
来源:juejin.cn/post/7452197545588146214
收起阅读 »

作为一名程序员,你是如何看待外包的

大家好,我是凌览 。 同样是程序员靠手艺吃饭,为啥外包却是过街老鼠人人喊打,这里我精选了几位网友的回答让我们一起来看看。 第一位网友 其实我觉得,国家应当立法禁止外包驻场。应当规定只有在外包公司所在办公场所工作才能算外包,驻场外包一律必须与目标公司签订劳务合...
继续阅读 »

大家好,我是凌览


同样是程序员靠手艺吃饭,为啥外包却是过街老鼠人人喊打,这里我精选了几位网友的回答让我们一起来看看。


第一位网友


其实我觉得,国家应当立法禁止外包驻场。应当规定只有在外包公司所在办公场所工作才能算外包,驻场外包一律必须与目标公司签订劳务合同。否则,驻场外包本质上相当于公司钻劳动法漏洞雇人。


你请外包公司开发软件,给需求给预算给时间给报酬,对方开发了给你验收,这叫外包。没毛病。


你给外包公司钱,人家直接把人派到你办公室,这叫什么玩意的外包?这不就是逃避责任,规避劳动法么?——你是个公司,又不是个人。


当然了,其实确实有些情况,需要不同公司去同一个办公地点合作做项目的,大家觉得说不清楚这与外包的区别。但其实区别还是很明显,区别在于,这些员工遵守谁家的工作制度。


我是A公司人,去客户B那里出差,帮客户B解决问题,这段时间虽然在客户B公司上班,但我不用打他们的卡,不用给他们汇报工作,不用交他们周报,我只对自己公司负责,这是出差,因为我还是A公司员工。


如果我在A公司签订合同然后去B公司工作,由他们(B公司)给分配任务,由他们考核计划完成情况,由他们收我周报,由他们定我KPI,我向他们汇报工作,这性质就完全变了。


这除了劳务关系以外,难道不是实质上B公司员工么?如果允许这样的形态存在,那不就等于是B公司的金蝉脱壳方式规避劳动法么?


所以我的看法是这样:外包可以,B公司写好需求人力时间,签合同,包项目,A公司直接交付最终成果,A公司的员工不受B公司管理,这是外包,这样的外包我觉得很合理。——A公司直接把人派出去给B公司,让B公司管理A公司的人,这不叫外包,这叫买卖人口,这叫A公司帮B公司规避劳动法,这是对外包的侮辱。


第二位网友


我其实一直本着给钱做事的风格,所以外包我并不歧视,直到有一天。


今年我面试了一个外包,行情不好,所以不怎么敢开薪资,比离职前低了一丢丢的样子,喊了12K


甲方面试,问的那一个细,从日常工作到项目数据流,到接口全问了一个遍,还好大差不多,聊了半个多小时。


面试结果是过的。


但是但是面试官和我介绍项目时候就说了,上半个月加班会少一点,可能到8点9点,下半个月可能会到1点2点,偶尔周六还要加班,是一个新项目。


我懂了。


我就问外包公司有加班费吗?他们说没有,只能调休,我算了一下按照面试官介绍,这加班一个月得加班120个小时打底,这没加班费,还只能调休,我直接裂开。


五险一金有,最低的


试用期全薪,这个除了小公司基本上是全的。


剩下啥福利没有,没餐补,没车补,啥也没有。


我直接就拒了。。


第三位网友


外包的活尽量别干,比如培训班入行,或换城市发展着急找工作,或者刚毕业想积累经验,这些情况下可能不得不找外包积累经验,但外包的活尽量别超过2年,干3年都嫌多,原因如下:



  1. 外包员工的工资会被“折上折”,甲方公司会根据自己同等条件员工的薪资打个折给外包公司,而外包公司会在此基础上再打个折,所以外包的薪资一般是甲方同类员工的6折甚至更低。

  2. 技术上得不到提升。甲方公司明着可能不说,但在分配重要活的时候,一定是一个正式员工带若干个外包员工,外包员工顶多就调用下api,打打下手,这样干2,3年,接触不到核心技术,而且在组里干久了业务都熟,可能还自我感觉良好。但此时如果出去找工作,真就很难找了。

  3. 出了问题,会让外包公司顶包。比如在一个项目中,只要外包员工参与的活出了大问题,一般甲方员工顶多就内部批评,外包员工一般就会被“退回原外包公司”。

  4. 工作环境不好,传说中的“不能吃甲方公司提供的零食”,这真不是空悬来风。

  5. 丧失信心。外包干久了,逆来顺受惯了,真会认为自己无法去挑战更高级的职位。

  6. 有一定的风险。比如甲方公司项目组砍预算,优先考虑的是,裁剪外包员工。


总之,甲方人员对外包员工可能真是客客气气的,但在各种工作中,总不免会想,我是甲方,他是外包,也就是说,甲方和外包之间的鸿沟是天然存在的。


作者:程序员凌览
来源:juejin.cn/post/7453817457912938505
收起阅读 »

一个网页打造自己的天气预报

web
概念解释 通过数据接口,简化功能开发。天气数据从哪里来?如果是自己采集,不光要写后端代码,最难的是维护啊,所以必须《天气预报》此类APP特别适合 前后端分离的,以下用一个简单的例子快速打通前后端的调用 前端使用HTML页面,通过JS的Fetch发起请求,向天气...
继续阅读 »

b64dacfad036df7512a0dbcd6a7ceb12.png


概念解释


通过数据接口,简化功能开发。天气数据从哪里来?如果是自己采集,不光要写后端代码,最难的是维护啊,所以必须《天气预报》此类APP特别适合 前后端分离的,以下用一个简单的例子快速打通前后端的调用

前端使用HTML页面,通过JS的Fetch发起请求,向天气API拿到JSON数据回显到页面上。

比较麻烦的是找到免费易用天气API接口。


前后端分离


前端负责用户界面展示和交互体验,

后端负责业务逻辑和数据处理。

这里后端直接使用免费的天气API,所以后端可以视为云服务。图上的左半部分。



  1. 前端:HTML+JS+Fetch请求

  2. 后端:云服务API(天气数据接口网站)

  3. 数据:JSON格式传输


数据接口


简化理解为一个返回JSON数据的网页。


项目《天气预报》


一、后端 云服务API(天气数据接口网站)


1. 注册激活帐号(目标得到APPID和APPSecret即可)


找到免费方便的天气API数据接口网页,这里使用 http://www.yiketianqi.com/
(非广告 只是顺手找到,如果有更方便的欢迎评论区留言),每天有1000次免费调用

注册记下自己 APPIDAPPSecret ,前端请求时要用
图片.png


2. 数据接口文档


一定要注册帐号,才能看到自己的 APPIDAPPSecret
文档中 http://www.yiketianqi.com/index/doc
直接复制下图(1) 就是前端用到的 目标数据接口


图片.png


3.测试天气数据API


以下URL供 前端请求时替换成自己的 APPIDAPPSecret



gfeljm.tianqiapi.com/api?unescap…



使用浏览器打开即可,可以通过浏览器观察,其实前端有这个URL就开业啦
图片.png


二、前端 HTML+JS+Fetch请求


1. 基础fetch请求页面


使用fetch方法发起请求,特别注意每一步返回的数据是否为Promise,需要使用async和await消除回调


const appid = 68621484
const appsecret = `XXXXX`
let URL = `http://gfeljm.tianqiapi.com/api?unescape=1&version=v63&appid=${appid}&appsecret=${appsecret}`

获取(URL)

async function 获取(URL){
let 响应 = await fetch(URL)
let 数据 = await 响应.json()

return 数据
}

注意这个页面通过浏览器 查看网络请求XHR


图片.png


2. 完整静态HTML页面


制作一个简易的HTML页面,显示出关键数据。更多数据需要参考接口文档。


图片.png


使用Fetch发起请求,获得数据后,使用innerHTML属性替换掉元素内容。

同时使用模版字符串,没有使用任何CSS样式。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>fetch请求</title>
</head>
<body>

<div id="A">
</div>

<script>

const appid = 68621484
const appsecret = `fZnW1ikK`
let URL = `http://gfeljm.tianqiapi.com/api?unescape=1&version=v63&appid=${appid}&appsecret=${appsecret}`


main()

async function main(){
let data = await 获取(URL)


const listItems = data.hours.map(hour => `
<li>
时间:${hour.hours}<br>
天气状况:${hour.wea}<br>
天气图标:<img src="images/weather_icons/${hour.wea_img}.png" alt="${hour.wea}"><br>
温度:${hour.tem}°C<br>
风向:${hour.win}<br>
风速:${hour.win_speed}<br>
能见度:${hour.vis} km<br>
空气质量:${hour.aqi}<br>
</li>
`
).join('');

A.innerHTML = `

<h2>城市:${data.city} (${data.cityEn})</h2>
国家:${data.country} (${data.countryEn})<br>
日期:${data.date} ${data.week}<br>
更新时间:${data.update_time}<br>
天气状况:${data.wea}<br>
天气图标:<img src="images/weather_icons/${data.wea_img}.png" alt="${data.wea}"><br>
当前温度:${data.tem}°C<br>
最高温度:${data.tem1}°C<br>
最低温度:${data.tem2}°C<br>
风向:${data.win}<br>
风速:${data.win_speed} (${data.win_meter})<br>
湿度:${data.humidity}<br>
能见度:${data.visibility}<br>
气压:${data.pressure} hPa<br>
降雨量:${data.rain_pcpn} mm<br>
空气质量指数:${data.air}<br>
PM2.5:${data.air_pm25}<br>
空气质量等级:${data.air_level}<br>
空气质量提示:${data.air_tips}
<ul>
${listItems}
</ul>
`
;


console.log(data)
}

async function 获取(URL){
let 响应 = await fetch(URL)
let 数据 = await 响应.json()

return 数据
}


</script>
</body>
</html>

3. 补充CSS样式


图片.png
ul{
display: flex;
flex-wrap: wrap;
list-style: none;
}

li{
width: 300px;
background-color: palegreen;
margin: 10px;
padding: 15px;
border-radius: 50%;
text-align: center;
}

span{
padding: 15px;
background-color: orange;
cursor: pointer;
}

4. 补充JS多城市查询


天气.gif


4.1 增加对应的HTML代码

图片.png


4.2 增加对应的JS代码

图片.png


三、项目图示总结


使用Fetch和async/await极大的简化了前端代码。后端数据接口就是一个URL地址。
整个后端具备云服务的特征,可以视作云服务数据接口,如图所示


图片.png


四、天气接口


1.易客云天气API 推荐:⭐⭐⭐⭐⭐


对新手比较友好。
tianqiapi.com/


2.高德地图 需要注册开发者(推荐:⭐⭐⭐)


lbs.amap.com/api/webserv…


3.心知天气(推荐:⭐)


免费的API数据只有一行,且文档藏得太深难用
http://www.seniverse.com/


欢迎大家提供更多更好的天气API。


作者:百万蹄蹄向前冲
来源:juejin.cn/post/7441560184010014735
收起阅读 »

“有办法让流程图动起来吗?”“当然有!”:一起用LogicFlow实现动画边

web
引言 在流程图中,边(Edge) 的主要作用是连接两个节点,表示从一个节点到另一个节点的关系或流程。在业务系统中,边通常代表某种逻辑连接,比如状态转移、事件触发、任务流动等。对于复杂的流程图,边不仅仅是两点之间的连接,它还可以传递信息、约束流程的顺序,并通过不...
继续阅读 »

引言


在流程图中,边(Edge) 的主要作用是连接两个节点,表示从一个节点到另一个节点的关系或流程。在业务系统中,边通常代表某种逻辑连接,比如状态转移、事件触发、任务流动等。对于复杂的流程图,边不仅仅是两点之间的连接,它还可以传递信息、约束流程的顺序,并通过不同的样式或标记来表达不同的含义。


不同的场景下,边可能需要具备丰富的样式或交互,比如箭头表示方向、虚线表示条件判断、动画表示动态效果等。因此,灵活定义和实现自定义边对于流程图的可视化设计尤为重要。


LogicFlow的边


为了灵活适配不同场景下的需求,LogicFlow的边模型是由 线条、箭头、文本、调整点五个模块组成。用户可以继承基础边类,对边的线条、箭头、文本和调整点进行自定义。


edge-struct.png
在技术实现上,LogicFlow设计了一个基础边模型BaseEdge,它定义了LogicFlow边的基本属性,如起点、终点、路径、样式等,并提供了操作这些属性的基本方法,提供逻辑处理和渲染的基础,通过继承基础边的数据类BaseEdgeModel和视图类BaseEdge,可以实现自定义边的逻辑和交互。


基础边:BaseEdge


属性方法简介

BaseEdgeModel中定义了一些核心属性,用于描述边的几何结构和样式。


属性释义
sourceNodeId起始节点Id
targetNodeId目标节点Id
startPoint起点信息,默认存储的是起始节点上连接该边锚点的坐标信息
endPoint终点信息,默认存储的是目标节点上连接该边锚点的坐标信息
text边文本信息,存储边上文本的内容和位置
properties自定义属性,用于存储不同业务场景下的定制属性
pointsList路径顶点坐标列表

围绕着这些核心属性,LogicFlow设计了支撑边运转的核心方法


方法用途
initEdgeData初始化边的数据和状态
setAnchors设置边的端点,startPoint和endPoint会在这个被赋值
initPoints设置边路径,pointsList会在这个阶段被赋值
formatText将外部传入的文本格式化成统一的文本对象

还有一些渲染使用的样式方法


方法用途
getEdgeStyle设置边样式
getEdgeAnimationStyle设置边动画
getAdjustPointStyle设置调整点样式
getTextStyle设置文本样式
getArrowStyle设置箭头样式
getOutlineStyle设置边外框样式
getTextPosition设置文本位置

运转过程

边实例化时,数据层Model类内部会先调用initeEdgeData方法,将无需处理的属性直接存储下来,设置为监听属性然后触发setAnchors、initPoints和formatText方法,生成边起终点、路径和文本信息存储并监听。


model-run.png


视图层渲染时,Model中存储的数据会以外部参数的形式传给组件,由不同渲染方法消费。每个渲染方法都是从Model存储的核心数据中获取图形信息、从样式方法中获取图形渲染样式,组装到svg图形上。最终由render函数将不同模块方法返回的内容呈现出来。


view-run.png


内置衍生边


LogicFlow内部基于基础边衍生提供三种边:直线边、折线边和曲线边。


直线边

在基础边的之上做简单的定制:



  1. 支持样式快速设置

  2. 限制文本位置在线段中间

  3. 使用svg的line元素实现线条的绘制


ViewModel
LogicFlow-packages-core-src-view-edge-LineEdge-tsx-at-master-·-didi-LogicFlow-10-29-2024_09_07_PM.pngimage.png

直线边数据层和视图层源码逻辑


折线边

折线边在Model类的实现上针对边路径计算做了比较多的处理,会根据两个节点的位置、重叠情况,使用 A*查找 结合 曼哈顿距离 计算路径,实时自动生成pointsList数据。在View类中则重写了getEdge方法,使用svg polyline元素渲染路径。


录屏2024-10-30 10.52.14.gif


曲线边

曲线边和折线边类似,Model类针对边路径计算做了较多处理,不一样的是,为了调整曲线边的弧度,曲线边额外还提供了两个调整点,边路径也是根据边起终点和两个调整点的位置和距离计算得出,View类里使用svg的path元素渲染路径。


录屏2024-10-30 10.54.48.gif


一起实现一条自定义动画边


自定义边的实现思路和内置边的实现类似:继承基础边 → 重写Model类/View类的方法 → 按需增加自定义方法 → 命名并导出成模块


今天就带大家一起实现一条复杂动画边,话不多说,先看效果:


animate-line-high-quality.gif


要实现这样效果的边,我们核心只需要做一件事:重新定义边的渲染内容。


在实际写代码时,主要需要继承视图类,重写getEdge方法。


实现基础边

那我们先声明自定义边,并向getEdge方法中增加逻辑,让它返回基础的折线边。


为了方便预览效果,我们在画布上增加节点和边数据。


自定义边实现

import { h, PolylineEdge, PolylineEdgeModel } from '@logicflow/core'

class CustomAnimateEdge extends PolylineEdge {
// 重写 getEdge 方法,定义边的渲染
getEdge() {
const { model } = this.props
const { points, arrowConfig } = model
const style = model.getEdgeStyle()
return h('g', {}, [
h('polyline', {
points,
...style,
...arrowConfig,
fill: 'none',
strokeLinecap: 'round',
}),
])
}
}

class CustomAnimateEdgeModel extends PolylineEdgeModel {}

export default {
type: 'customAnimatePolyline',
model: CustomAnimateEdgeModel,
view: CustomAnimateEdge,
}


定义画布渲染内容

lf.render({
nodes: [
{
id: '1',
type: 'rect',
x: 150,
y: 320,
properties: {},
},
{
id: '2',
type: 'rect',
x: 630,
y: 320,
properties: {},
},
],
edges: [
{
id: '1-2-1',
type: 'customPolyline',
sourceNodeId: '1',
targetNodeId: '2',
startPoint: { x: 200, y: 320 },
endPoint: { x: 580, y: 320 },
properties: {
textPosition: 'center',
style: {
strokeWidth: 10,
},
},
text: { x: 390, y: 320, value: '边文本3' },
pointsList: [
{ x: 200, y: 320 },
{ x: 580, y: 320 },
],
},
{
id: '1-2-2',
type: 'customPolyline',
sourceNodeId: '1',
targetNodeId: '2',
startPoint: { x: 150, y: 280 },
endPoint: { x: 630, y: 280 },
properties: {
textPosition: 'center',
style: {
strokeWidth: 10,
},
},
text: { x: 390, y: 197, value: '边文本2' },
pointsList: [
{ x: 150, y: 280 },
{ x: 150, y: 197 },
{ x: 630, y: 197 },
{ x: 630, y: 280 },
],
},
{
id: '1-2-3',
type: 'customPolyline',
sourceNodeId: '2',
targetNodeId: '1',
startPoint: { x: 630, y: 360 },
endPoint: { x: 150, y: 360 },
properties: {
textPosition: 'center',
style: {
strokeWidth: 10,
},
},
text: { x: 390, y: 458, value: '边文本4' },
pointsList: [
{ x: 630, y: 360 },
{ x: 630, y: 458 },
{ x: 150, y: 458 },
{ x: 150, y: 360 },
],
},
{
id: '1-2-4',
type: 'customPolyline',
sourceNodeId: '1',
targetNodeId: '2',
startPoint: { x: 100, y: 320 },
endPoint: { x: 680, y: 320 },
properties: {
textPosition: 'center',
style: {
strokeWidth: 10,
},
},
text: { x: 390, y: 114, value: '边文本1' },
pointsList: [
{ x: 100, y: 320 },
{ x: 70, y: 320 },
{ x: 70, y: 114 },
{ x: 760, y: 114 },
{ x: 760, y: 320 },
{ x: 680, y: 320 },
],
},
],
})

然后我们就能获得一个这样内容的画布:


绚丽动画折线-LogicFlow-Examples-10-30-2024_11_08_AM.png


添加动画

LogicFlow提供的边动画能力其实是svg 属性和css属性的集合,目前主要支持了下述这些属性。


type EdgeAnimation = {
stroke?: Color; // 边颜色, 本质是svg stroke属性
strokeDasharray?: string; // 虚线长度与间隔设置, 本质是svg strokeDasharray属性
strokeDashoffset?: NumberOrPercent; // 虚线偏移量, 本质是svg strokeDashoffset属性
animationName?: string; // 动画名称,能力等同于css animation-name
animationDuration?: `${number}s` | `${number}ms`; // 动画周期时间,能力等同于css animation-duration
animationIterationCount?: 'infinite' | number; // 动画播放次数,能力等同于css animation-iteration-count
animationTimingFunction?: string; // 动画在周期内的执行方式,能力等同于css animation-timing-function
animationDirection?: string; // 动画播放顺序,能力等同于css animation-direction
};

接下来我们就使用这些属性实现虚线滚动效果。


边的动画样式是取的 model.getEdgeAnimationStyle() 方法的返回值,在内部这个方法是取全局主题的edgeAnimation属性的值作为返回的,默认情况下默认的动画是这样的效果:


default-edge-animation.gif


开发者可以通过修改全局样式来设置边动画样式;但如果是只是指定类型边需要设置动画部分,则需要重写getEdgeAnimationStyle方法做自定义,就像下面这样:


class ConveyorBeltEdgeModel extends PolylineEdgeModel {
// 自定义动画
getEdgeAnimationStyle() {
const style = super.getEdgeAnimationStyle()
style.strokeDasharray = '40 160' // 虚线长度和间隔
style.animationDuration = '10s' // 动画时长
style.stroke = 'rgb(130, 179, 102)' // 边颜色
return style
}
}

然后在getEdge方法中加上各个动画属性


// 改写getEdge方法内容
const animationStyle = model.getEdgeAnimationStyle()
const {
stroke,
strokeDasharray,
strokeDashoffset,
animationName,
animationDuration,
animationIterationCount,
animationTimingFunction,
animationDirection,
} = animationStyle

return h('g', {}, [
h('polyline', {
// ...
strokeDasharray,
stroke,
style: {
strokeDashoffset: strokeDashoffset,
animationName,
animationDuration,
animationIterationCount,
animationTimingFunction,
animationDirection,
},
}),
])

我们就得到了定制样式的动画边:


base-edge-animation.gif


添加渐变颜色和阴影

最后来增加样式效果,我们需要给这些边增加渐变颜色和阴影。
SVG提供了元素linearGradient定义线性渐变,我们只需要在getEdge返回的内容里增加linearGradient元素,就能实现边颜色线性变化的效果。
实现阴影则是使用了SVG的滤镜能力实现。


// 继续改写getEdge方法内容
return h('g', {}, [
h('linearGradient', { // svg 线性渐变元素
id: 'linearGradient-1',
x1: '0%',
y1: '0%',
x2: '100%',
y2: '100%',
spreadMethod: 'repeat',
}, [
h('stop', { // 坡度1,0%颜色为#36bbce
offset: '0%',
stopColor: '#36bbce'
}),
h('stop', { // 坡度2,100%颜色为#e6399b
offset: '100%',
stopColor: '#e6399b'
})
]),
h('defs', {}, [
h('filter', { // 定义滤镜
id: 'filter-1',
x: '-0.2',
y: '-0.2',
width: '200%',
height: '200%',
}, [
h('feOffset', { // 定义输入图像和偏移量
result: 'offOut',
in: 'SourceGraphic',
dx: 0,
dy: 10,
}),
h('feGaussianBlur', { // 设置高斯模糊
result: 'blurOut',
in: 'offOut',
stdDeviation: 10,
}),
h('feBlend', { // 设置图像和阴影的混合模式
mode: 'normal',
in: 'SourceGraphic',
in2: 'blurOut',
}),
]),
]),
h('polyline', {
points,
...style,
...arrowConfig,
strokeDasharray,
stroke: 'url(#linearGradient-1)', // 边颜色指向渐变元素
filter: 'url(#filter-1)', // 滤镜指向前面定义的滤镜内容
fill: 'none',
strokeLinecap: 'round',
style: {
strokeDashoffset: strokeDashoffset,
animationName,
animationDuration,
animationIterationCount,
animationTimingFunction,
animationDirection,
},
}),
])

就得到了我们的自定义动画边


录屏2024-10-29 19.57.02.gif


结尾


在流程图中,边不仅仅是节点之间的连接,更是传递信息、表达逻辑关系的重要工具。通过 LogicFlow,开发者可以轻松地创建和自定义边,以满足不同的业务场景需求。从基础的直线边到复杂的曲线边,甚至动画边,LogicFlow 都为开发者提供了高度的灵活性和定制能力。


希望能通过这篇文章抛砖引玉,帮助你了解在 LogicFlow 中创建和定制边的核心技巧,打造出符合你业务需求的流程图效果。


如果这篇文章对你有帮助,请为我们的项目点上star,非常感谢ღ( ´・ᴗ・` )


项目传送门:github.com/didi/LogicF…


作者:LogicFlow
来源:juejin.cn/post/7431379490969010212
收起阅读 »

程序员加班很晚应该怎么锻炼身体?

作为程序员,肯定都深受加班的痛苦。 ❝那每天加班很晚的情况下,该通过怎样的锻炼来保持身体健康呢? 我觉得还是得先把觉睡够,然后才是锻炼。 ❝睡眠不足情况下高强度锻炼,容易猝死。 如果睡觉的时间都不够,建议辞,换个不太卷的地方。 把特别卷的岗位,留给那些更...
继续阅读 »

作为程序员,肯定都深受加班的痛苦。



❝那每天加班很晚的情况下,该通过怎样的锻炼来保持身体健康呢?



我觉得还是得先把觉睡够,然后才是锻炼。



❝睡眠不足情况下高强度锻炼,容易猝死。


如果睡觉的时间都不够,建议辞,换个不太卷的地方。


把特别卷的岗位,留给那些更年轻的,特别需要钱买房结婚的,拼几年,把生存问题解决掉之后,就不要再用命赚钱了。


人生几十年,钱是赚不完的,基本生活需求之外,多赚到的钱,对生活质量提升作用有限。



图片


睡眠的优先级,不但高于锻炼,甚至高于洗脸洗澡。



❝而且睡前三小时不要吃太多东西。


对于经常晚上加班很晚的人来说,戒掉睡觉前玩手机的不良习惯,尽量减少晚上的一切活动,争分夺秒地保证睡眠。



健身,足够的营养和休息,都比身体锻炼本身更重要。



❝所以如果长期生活不规律,饮食习惯不好,休息睡眠不能保证。


如果已经很累了,就不要考虑上高强度的训练了,夸张一点有可能做个俯卧撑都有可能把人送进医院。



有位网友总结得好:



❝去健身,你会得到强壮的身体,过度劳累,你会得到猝死的尸体,过度劳累还去健身,你会得到强壮的尸体。



所以:下班晚,好好休息就是你最好的健身!


程序员在工作空闲之余也可以通过以下方式来锻炼身体:



❝通过走路或骑自行车的方式出门活动,可以锻炼身体的同时享受户外的新鲜空气。


在家里可以做一些简单的,如俯卧撑、仰卧起坐等,这些操作都可以锻炼身体的同时不需要太多的器材。





每日一题


题目描述




给你一个二叉树的根节点 root , 检查它是否轴对称。



解题思路



递归实现


递归结束条件:



  • 都为空指针则返回 true

  • 只有一个为空则返回 false


递归过程:



  • 判断两个指针当前节点值是否相等

  • 判断 A 的右子树与 B 的左子树是否对称

  • 判断 A 的左子树与 B 的右子树是否对称



代码实现


Java代码:


 class Solution {
     public boolean isSymmetric(TreeNode root) {
         if(root == null) {
           return true;
         }
         return dfs(root.left,root.right);
     }
     public boolean dfs(TreeNode p,TreeNode q){
         if (p == null && q == null) {
           return true;
         } else if (p == null||q == null) {
           return false//只有一个为空
         }
         if(p.val != q.val) {
           return false;
         }
         //第一棵子树的左子树和第二棵子树的右子树对称,且第一棵子树的右子树和第二棵子树的左子树对称
         return dfs(p.left,q.right) && dfs(p.right,q.left);
     }
 }

Python代码:


class Solution(object):
 def isSymmetric(self, root):
  """
  :type root: TreeNode
  :rtype: bool
  """

  if not root:
   return True
  def dfs(left,right):
   # 递归的终止条件是两个节点都为空
   # 或者两个节点中有一个为空
   # 或者两个节点的值不相等
   if not (left or right):
    return True
   if not (left and right):
    return False
   if left.val!=right.val:
    return False
   return dfs(left.left,right.right) and dfs(left.right,right.left)
  # 用递归函数,比较左节点,右节点
  return dfs(root.left,root.right)

Go代码:


func isSymmetric(root *TreeNode) bool {
 // 递归-对称二叉树
 var dfs func(leftright *TreeNode) bool
 dfs = func(leftright *TreeNode) bool {
  if left == nil && right == nil {
   return true
  }
  if left ==nil || right == nil || left.Val != right.Val {
   return false
  }
  // 左右子节点都存在且val等,递归其子树
  return dfs(left.Leftright.Right) && dfs(left.Rightright.Left)
 }
 return dfs(root.Left, root.Right)
}

复杂度分析



❝假设树上一共 n 个节点。


时间复杂度:



  • 这里遍历了这棵树,时间复杂度为 O(n)


空间复杂度:



  • 这里的空间复杂度和递归使用的栈空间有关,这里递归层数不超过 n,故空间复杂度为 O(n)



作者:程序员飞鱼
来源:juejin.cn/post/7453489707109531702
收起阅读 »

svg实现地铁线路图

web
简介最近学习了svg,想着使用svg实现地铁线路图 其中黄色是1号线,蓝色是2号线,橙色是3号线实现:react+svg+数据结构-图。考虑范围包括了每站时间,但未包括了换站时间。考虑到换站时间可以加到每2个交换站的路程里功能功能:选择2个地铁站,标...
继续阅读 »

简介

最近学习了svg,想着使用svg实现地铁线路图

insta.gif 其中黄色是1号线,蓝色是2号线,橙色是3号线

实现:react+svg+数据结构-图。

考虑范围包括了每站时间,但未包括了换站时间。考虑到换站时间可以加到每2个交换站的路程里

功能

功能:选择2个地铁站,标出最短路程。

求最少换站路线,暂未做

实现思路

  1. 简化问题,先将所有地铁站分2类,交换站和非交换站。那么交换站可以充当图中的。那么从a=>b, 变成a=>交换站=>交换站=>b的问题,需要写死的是非交换站(a,b)能到达的交换站(下面的adjcent数组), 其中a=>交换站 和b=>交换站 相对静止,但是我这里也考虑到了非交换站到交换站需要的时间(time)

地铁线路图

image.png

image.png

  1. 首先根据每条地铁图数据绘制出地铁线路图,并添加上点击事件,这里要处理好地铁线路图的数据,数据需要相对准确,因为后面需要计算出最短路径。

image.png

image.png

  1. 求最短距离,使用的是Floyd最短路算法(全局/多源最短路)。 其中原理:计算a->b的最短路径,遍历所有,查找是否有最捷径路径 a->x x->b
for(k=1;k<=n;k++) 
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(e[i][j]>e[i][k]+e[k][j]) // i->j i->k k->j
e[i][j]=e[i][k]+e[k][j];

然而拿到最短路程后,但是并未拿到路程,拿到的是比如,a点到所有点的最短路程。你们可以思考一下如果获取最短路径。

大概长这样

image.png

  1. 求最短路径 使用一个对象,存储每次找到较短路径。 changeRodePath[${is}to${js}] = [ [is, ks], [ks, js], ]
  function getAllPointShortest(n, e) {
let changeRodePath = {};
for (let k = 0; k < n; k++) {
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (e[i][j] > e[i][k] + e[k][j]) {
e[i][j] = e[i][k] + e[k][j];
console.log("-------------------------");
const is = changeStation[i];
const ks = changeStation[k];
const js = changeStation[j];
changeRodePath[`${is}to${js}`] = [
[is, ks],
[ks, js],
];
console.log(changeStation[i], changeStation[j]);
console.log(changeStation[i], changeStation[k]);
console.log(changeStation[k], changeStation[j]);
// 2_2 2_5
//2_2 1_2
//1_2 2_5
}
}
}
}
setChangeRodePath(changeRodePath);
return e;
}

当选中2个站时,先取出adjacent,然后求出最短路程,

         let path = {};
adjacent0.forEach((p0,i1) => {
adjacent1.forEach((p1,i2) => {
const index0 = changeStation.indexOf(p0);
const index1 = changeStation.indexOf(p1);
let t=time0[i1]+time1[i2]
if ((rodePath[index0][index1]+t) < minPath) {
minPath = rodePath[index0][index1];
path = { p0, p1};
}
});
});

具体多少不重要,重要的是通过 let pathm = changeRodePath[${path.p0}to${path.p1}],递归查找是否有更短的捷径,因为,2_1 =>3_9 的路径是:2_1 =>1_3=>1_5=>1_8,所以不一定有捷径a->c c—b, 可能是 a->c c->b, 然后发现有更短路径,c->d d->b,那么a-b 路程就变成了a->c->d->b。回到正题,递归之后就能取到最短路径了,然后通过2个交换点取得路径。

没有就更简单了

5.取对应的line,去渲染,这里分2类,交换站之间的路径(最短路径),头和尾。然后分别渲染polyline(使用对应line 的颜色)

function getPl(item, attr, listen) {
return (
<g {...attr} {...listen}>
<polyline //绘制line
{...item}
fill="none"
color={item.colorH}
strokeWidth="5"
strokeLinecap="round"
strokeLinejoin="round"
/>

{item.usePointn.map((point) => { // line 上的站
return (
<use
x={point.x}
onClick={() =>
choosePoint(point)}
y={point.y}
fill={point.color}
href="#point"
>use>
);
})}
g>
);
}

代码准备

// 上图所示,数据随便造,需要合理时间,不然得到的路程奇奇怪怪的

代码部分

html

  
width: "80vw", height: "100vh" }}>
<svg
id="passWay"
viewBox="0 0 800 600"
xmlns="http://www.w3.org/2000/svg"
>

<defs>
<g id="point">
<circle r="4">circle>
<circle r="3" fill="#fff">circle>
g>
defs>
// 所有地铁线路图
{polyline.map((item) => {
return getPl(
item,
{},
{
onMouseEnter: (e) => onMouseEnterShow(e, item),
onMouseOut: () => {
clearTimeout(t1.current);
t1.current = null;
},
}
);
})}
// mask
{ choosePoints.length==2 && (
<rect
x="0"
y="0"
width={"100%"}
height={"100%"}
fillOpacity={0.9}
fill="white"
>
rect>
)}
// 最短路程
{choosePoints && choosePoints.length==2 && showReduLine.map(line=>{
return getPl(line, {}, {})
})
}
svg>

通过line 获取 polyline

  function getLineP(line) {
const usePointn = [];
let path = "";
line.points.forEach((item, index) => {
const { x, y, isStart, isChange, isEnd } = item;

usePointn.push({ ...item, color: line.color });
if (index == 0) {
path = `${x},${y} `;
} else {
path += `${x},${y} `;
}
});
const polylinen = {
usePointn,
stroke: line.color,
...line,
pointStation: line.points,
points: path,
};
return polylinen;
}

选出2站绘制路程

  function comfirPath(point0, point1, p0, p1, pathm) {

let pShow0= getLines(point0,p0)
let pShow1= getLines(point1,p1)
let pathsCenter=[]
if (pathm) {
function recursion(pathm){
pathm.map(([p0,p1])=>{
let pathn = changeRodePath[`${p0}to${p1}`];
if(pathn){
recursion(pathn)
}else{
// 中间的line 不用按顺序
pathsCenter.push(getChangeStationLine(p0,p1))
}
})
}
recursion(pathm)
}else{
pathsCenter=[getChangeStationLine(p0,p1)]
}
const pyAll= [pShow0,pShow1,...pathsCenter].map(line=>{
const py= getLineP({
points:line,
})
py.stroke=line.color
return py
})
setShowReduLine(pyAll); // 绘制
}

参考: 1.# [数据结构拾遗]图的最短路径算法


作者:无名小兵
来源:juejin.cn/post/7445208959151767604

收起阅读 »

极越“猝死“,对于打工人来说可能是好事儿~

大家好,我是日拱一卒的攻城师不浪,致力于技术与艺术的融合,梦想开一家小而美的工作室。这是2024年输出的第49/100篇文章。 人生不止有打工! 前言 今天暂且先不聊技术了,换换口味儿~ 说说最近两天极越汽车暴雷的事情吧,想必大家应该也都有所耳闻了。这件事的...
继续阅读 »

大家好,我是日拱一卒的攻城师不浪,致力于技术与艺术的融合,梦想开一家小而美的工作室。这是2024年输出的第49/100篇文章。



人生不止有打工!


前言


今天暂且先不聊技术了,换换口味儿~


说说最近两天极越汽车暴雷的事情吧,想必大家应该也都有所耳闻了。这件事的发生真是让我越想越气!


其实企业倒闭,在当前这个时间点并不是什么新鲜事儿了,而极越也不是什么大公司,但为什么它暴雷这个事件会闹的这么大呢?



这是极越总部办公室墙上贴的标语,现在回头看看,是不是很讽刺呢?


极越暴雷的很合理


我为什么这么说,并不是我墙倒众人推,而是它倒闭,早就有人先兆预见了。


当时就有之情人士透露,极越公司很快将解散,但是这时作为极越一把手的夏一平先生还仍在努力“辟谣”:公司没有倒闭,只是暂时遇到了困难;


自己刚辟谣完,接着就啪啪打脸,宣布公司所有员工请立即原地解散!要不说谣言从来不会是空穴来风呢。



公司不仅发不出工资,也没钱赔偿,然而更可气的是说,公司无力继续给员工缴纳11月份的五险一金,请员工自行解决。


但凡在一二线工作过的牛马们就能明白,当自己在自己梦想的奋斗了多年的城市,如果断了社保,会带来怎样的一种影响?基本上宣布自己与这个城市无缘了!


这也是这次事件为何如此严重的原因之一!这完全是不给我们这些牛马留活路!


而一个公司能够走到今天这种地步,并且以这种方式结束,无疑不与它的掌门人有着莫大的关系。


公司为何会突然就没钱了?据极越内部的知情人士透露:根源在于百度单方面宣布对极越撤资,终止之后的一切合作


那为什么会突然撤资:因为极越内部高层贪腐严重,百度在财账审计的时候,发现自己一直被蒙在鼓里,原来极越使用的财务供应商是他们“自己人”,并且一直在偷偷的把百度的钱转走!


夏一平经常一手独裁,擅自指定高于十几倍市场价格的供应商,目前极越的财务欠账多达70多亿


甚至还有更炸裂的消息爆出,极越前高管曾经在个人朋友圈爆料:夏一平婚内出轨,嫖娼等,这样的垃圾,在今天选择跑路,一点不奇怪



总结来说,极越的倒闭,原因很明显:高层贪腐,封建官僚,内斗严重,一人掌权,任人唯亲。


其实极越倒闭,对夏老板来说,一点影响都没有,该装进口袋的钱早已装的盆满钵满,事件过去后,该怎么逍遥快活一点都不耽误。


只是苦了极越的底层打工人啊,现在面临的是工资没有,补偿没有,连基本的五险一金都没有(不过最新消息:百度与吉利已经在商讨方案,为极越员工补上欠缺的社保缴纳)。


所以,资本家根本就不值得同情,最苦命的还是底层打工人。


你能说我们不努力吗?现在这个时代,努力真的有用吗?




牛马们该醒醒了


是的,真的该做出改变了,因为时代在改变,你不去适应环境,那就会被环境所淘汰,物竞天择,更古不变的真理。


更可怕的是,这个时代发展之迅速甚至有点让我们招架不住,AI的迅速崛起,已经肉眼可见的在重塑各行各业。


所以,打工人,不要一门心思想着为公司卖力了,当公司不需要你的时候,是一点情面不会跟你讲的!当被淘汰之后,你是否还能够拿出养活自身的技能呢?


当然我不是说不让你去打工,毕竟中国这么多人,也不能每个人都去创业是吗?


我只是想让大家能够转变一下打工的思维,比如:能不能慢慢的变成给自己打工?


几点建议



  1. 选公司就是选领导:领导决定你的职业发展和走势,公司再牛,没有领导提拔重用你,你一样展露不了头角,因此,去找伯乐。

  2. 把公司当成自己的公司:这时候有人会说了:你这不是自相矛盾吗?刚刚还说不要一心只为公司。是的,我的意思其实是:把公司当成自己的创业路上的铺路石,把能在公司里学到的都积极的去学,把公司的能力慢慢的演变成自己的能力,观察下公司日常是怎么运作的,有哪些可取之处与缺点,一步步积累。而不是只把自己当一个工具

  3. 做一个副业:慢慢的挑选到自己的一个副业赛道,最好是跟自己的兴趣相关,不然很难做的下去(经验所谈)。而且经常有人会问:怎么平衡工作与副业?其实坦白来讲:它们两个并不冲突,当你把上班当作自己创业路上的学习平台,那上班的价值意义就体现出来了,你甚至会爱上上班。如果不这么做的话,那你就会感觉很累,就是个纯种牛马



建了一个副业&AI社群,可联系我:brown_7778,我拉你进群,群里会经常分享一些我做副业的经验,认知等,还有一些我掏腰包在其他社群拿到的资料,副业信息等。群里还会讨论与AI相关的信息与知识,欢迎入群。



作者:攻城师不浪
来源:juejin.cn/post/7448806505255813147
收起阅读 »

插件系统为什么在前端开发中如此重要?

web
插件系统是一种软件架构模式,允许开发者通过添加外部模块或组件来扩展和定制软件应用的功能,而无需修改其核心代码。这种方式为软件提供了高度的可扩展性、灵活性和可定制性。 用过构建工具的同学都知道,grunt, webpack, gulp 都支持插件开发。后端框架比...
继续阅读 »

插件系统是一种软件架构模式,允许开发者通过添加外部模块或组件来扩展和定制软件应用的功能,而无需修改其核心代码。这种方式为软件提供了高度的可扩展性、灵活性和可定制性。


用过构建工具的同学都知道,grunt, webpack, gulp 都支持插件开发。后端框架比如 egg koa 都支持插件机制拓展,前端页面也有许多可拓展性的要求。插件化无处不在,所有的框架都希望自身拥有最强大的可拓展能力,可维护性,而且都选择了插件化的方式达到目标。


什么是插件系统


插件系统主要由三个关键部分组成:



  1. 核心系统(Host Application):这是主软件应用,提供了插件可以扩展或修改的基础功能。

  2. 插件接口(Plugin Interface):定义了插件和核心系统之间的交互协议。插件接口规定了插件必须遵循的规则和标准,以便它们能够被核心系统识别和使用。

  3. 插件(Plugins):根据插件接口规范开发的外部模块或组件,用于扩展核心系统的功能。插件可以被添加或移除,而不影响核心系统的运行。


20240316121736


插件的执行流程和实现方式


插件的执行流程是指从插件被加载到执行其功能直至卸载的一系列步骤。



  1. 设计核心系统:首先,我们需要一个核心系统。这个系统负责维护基础功能,并提供插件可以扩展或修改的接口。



    • 核心系统的生命周期:定义核心系统的关键阶段,例如启动、运行中、关闭等。每个阶段可能会触发特定的事件。

    • 暴露的 API:确定哪些内部功能是可以被插件访问的。这包括数据访问、系统服务调用等接口。



  2. 插件的结构设计:插件需要有一个清晰的结构,使其能够容易地集成到核心系统中。一个典型的插件结构可能包含:



    • 初始化代码:插件加载时执行的代码,用于设置插件的运行环境。

    • 处理函数:实现插件功能的核心代码,根据插件的目的可以有多个。

    • 资源清理:插件卸载时需要执行的清理代码,以确保资源被适当释放。



  3. 插件的注册和加载:开发者通过配置文件、命令或图形界面在核心系统中注册插件,系统随后根据注册信息安装并加载插件,这个过程涉及读取插件元数据、执行初始化代码,以及将插件绑定到特定的生命周期事件或 API 上。

  4. 插件的实现:插件的实现依赖于核心系统提供的生命周期钩子和 API。



    • 利用生命周期钩子:插件可以注册函数来响应核心系统的生命周期事件,例如在系统启动完成后执行初始化操作,或在系统关闭前进行资源清理。

    • 调用暴露的 API:插件通过调用核心系统暴露的 API 来实现其功能。这些 API 可以提供系统信息、修改数据、触发事件等功能。



  5. 代码执行流程:插件通过注册自身到核心系统,绑定处理函数至特定事件或 API,以响应系统生命周期变化或 API 调用执行特定任务。在适当时机,如系统关闭或更新时,插件被卸载,其资源得以清理并从系统中移除。


通过这个流程,插件系统提供了一个灵活、可扩展的方式来增强和定制核心系统的功能。插件的开发者可以专注于插件逻辑的实现,而无需修改核心系统的代码。同时,核心系统能够保持稳定性和安全性,因为插件的执行是在明确定义的接口和约束条件下进行的。


插件的几种形式


插件的主要形式主要分为以下几种形式:



  1. 约定式插件

  2. 注入式插件

  3. 事件式插件

  4. 插槽式插件


约定式插件


约定式插件通常在那些采用“约定优于配置”理念的框架或工具中很常见。以 Webpack 为例,它过各种加载器(Loaders)和插件(Plugins)提供强大的扩展性,而这些扩展往往遵循一定的约定,以简化配置的复杂性。


在 Webpack 配置中使用插件时,通常不需要指定插件工作的具体点,只需要将插件加入到配置的 plugins 数组中。Webpack 根据内部的运行机制和生命周期事件,自动调用这些插件,执行相关的任务。


例如,使用 HtmlWebpackPlugin 可以自动生成一个 HTML 文件,并自动将打包后的 JS 文件注入到这个 HTML 文件中。开发者只需要按照约定将 HtmlWebpackPlugin 加入到 plugins 数组中,无需指定具体的注入点或方式,Webpack 就会自动完成这些任务。


const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
// 其他配置...
plugins: [
new HtmlWebpackPlugin({
template: "./src/template.html",
}),
],
};

通过这种约定式的插件机制,Webpack 极大地简化了开发者的配置工作,同时保持了强大的灵活性和扩展性。用户只需遵循简单的约定,如将插件实例添加到 plugins 数组,Webpack 便能自动完成复杂的集成工作,如资源打包、文件处理等,从而提高了开发效率和项目的可维护性。这正体现了约定式插件的主要优势:通过遵循一套预定义的规则,减少配置的需求,同时提供强大的功能扩展能力。


注入式插件


注入式插件通过在应用程序的运行时或编译时将插件的功能注入到应用程序中,从而扩展应用程序的功能。这种方式往往依赖于一种中间件或框架来实现插件的动态加载和执行。一个典型的例子就是 NestJs 世界中广泛使用的依赖注入(DI)功能。


除此之外,尽管 Webpack 更常被人们提及其约定式插件机制,但我们可以从一个角度将 Loaders 视为一种注入式插件,在 Webpack 配置中,Loaders 允许你在模块被添加到依赖图中时,预处理文件。可以看作是在编译过程中“注入”了额外的处理步骤。这些处理步骤可以包括将 TypeScript 转换为 JavaScript、将 SASS 转换为 CSS,或者将图片和字体文件转换为 Webpack 可以处理的格式。


module.exports = {
// ...其他配置
module: {
rules: [
{
test: /\.js$/, // 使用正则表达式匹配文件路径,处理.js文件
exclude: /node_modules/, // 排除node_modules目录
use: {
loader: "babel-loader", // 指定使用babel-loader
options: {
presets: ["@babel/preset-env"], // 使用预设配置转换ES6+代码
},
},
},
],
},
// ...其他配置
};

通过 loader 的配置,Webpack 实现了一种灵活的“注入式”扩展机制,允许开发者根据需要为构建过程注入各种预处理步骤。


事件插件化


事件插件化是一种基于事件驱动编程模式的插件化机制,其中插件通过监听和响应系统中发生的特定事件来工作。这种机制允许插件在不直接修改主程序代码的情况下增加或改变程序的行为。


Node.js 的 EventEmitter 类是实现事件插件化的一个很好的例子。假设我们正在开发一个应用程序,该程序需要在完成某个任务后执行一系列的操作,这些操作由不同的插件来实现。


首先,创建一个基于 EventEmitter 的任务执行器,它在完成任务时会发出一个事件:


const EventEmitter = require("events");

class TaskExecutor extends EventEmitter {
execute(taskFunc) {
console.log("Executing task...");
taskFunc();
this.emit("taskCompleted", "Task execution finished");
}
}

接着,我们可以开发插件来监听 taskCompleted 事件。每个插件都可以注册自己的监听器来响应事件:


// Plugin A
executor.on("taskCompleted", (message) => {
console.log(`Plugin A responding to event: ${message}`);
});

// Plugin B
executor.on("taskCompleted", (message) => {
console.log(`Plugin B responding to event: ${message}`);
});

最后,创建 TaskExecutor 的实例,并执行一个任务,看看插件如何响应:


const executor = new TaskExecutor();

// 注册插件
// ...此处省略插件注册代码...

executor.execute(() => {
console.log("Task is done.");
});

运行上述代码时,TaskExecutor 执行一个任务,并在任务完成后发出 taskCompleted 事件。注册监听该事件的所有插件(在这个例子中是插件 A 和插件 B)都会接到通知,并执行相应的响应操作。这种模式使得开发者可以很容易地通过添加更多的事件监听器来扩展应用程序的功能,而无需修改 TaskExecutor 或其他插件的代码,实现了高度的解耦和可扩展性。


插槽插件化


在 React 中,插槽插件化的概念可以通过组件的 children 属性或使用特定的插槽来实现。这种模式允许开发者定义一个组件框架,其中一些部分可以通过传入的子组件来填充,从而实现自定义内容的注入。这类似于 Vue 中的插槽(slots)功能,但在 React 中,它通过 props.children 或通过特定的 props 来传递组件来实现。


function Card({ children }) {
return <div className="card">{children}</div>;
}

function App() {
return (
<Card>
<h2>标题</h2>
<p>这是一段文本</p>
</Card>

);
}

通过这种方式,React 支持了组件的插槽化,使组件的复用和自定义变得更加容易。这种模式在构建可扩展和可复用的 UI 组件库时尤其有用。


代码实现


接下来我们通过插件来实现一个计算器,可以实现加减乘除


插件核心实现


class Calculator {
constructor(options = {}) {
const { initialValue = 0 } = options;
this.currentValue = initialValue;
}
getCurrentValue() {
return this.currentValue;
}
setValue(value) {
this.currentValue = value;
}
plus(addend) {
this.setValue(this.currentValue + addend);
}
minus(subtrahend) {
this.setValue(this.currentValue - subtrahend);
}
multiply(multiplicand) {
this.setValue(this.currentValue * multiplicand);
}
division(divisor) {
if (divisor === 0) {
console.error("不允许除零。");
return;
}
this.setValue(this.currentValue / divisor);
}
}

// test
const calculator = new Calculator();
calculator.plus(10);
console.log(calculator.getCurrentValue()); // 10
calculator.minus(5);
console.log(calculator.getCurrentValue()); // 5
calculator.multiply(2);
console.log(calculator.getCurrentValue()); // 10
calculator.division(2);
console.log(calculator.getCurrentValue()); // 5

实现 hooks


核心系统想要对外提供生命周期钩子,就需要一个事件机制。


class Hooks {
constructor() {
this.listeners = {};
}
on(eventName, handler) {
let listeners = this.listeners[eventName];
if (!listeners) {
this.listeners[eventName] = listeners = [];
}
listeners.push(handler);
}
off(eventName, handler) {
const listeners = this.listeners[eventName];
if (listeners) {
this.listeners[eventName] = listeners.filter((l) => l !== handler);
}
}
trigger(eventName, ...args) {
const listeners = this.listeners[eventName];
const results = [];
if (listeners) {
for (const listener of listeners) {
const result = listener.call(null, ...args);
results.push(result);
}
}
return results;
}
destroy() {
this.listeners = {};
}
}

这个 Hooks 类是一个事件监听器或事件钩子的简单实现,它允许你在应用程序的不同部分之间传递消息或事件,而不必直接引用那些部分。


暴露生命周期(通过 Hooks)


然后将 hooks 运用在核心系统中 -- JavaScript 计算器。


每个钩子对应的事件:



  • pressedPlus 做加法操作

  • pressedMinus 做减法操作

  • pressedMultiply 做乘法操作

  • pressedDivision 做乘法操作

  • valueWillChanged 即将赋值 currentValue,如果执行此钩子后返回值为 false,则中断赋值。

  • valueChanged 已经赋值 currentValue


class Calculator {
constructor(options = {}) {
this.hooks = new Hooks();
const { initialValue = 0, plugins = [] } = options;
this.currentValue = initialValue;
plugins.forEach((plugin) => plugin.apply(this.hooks));
}

getCurrentValue() {
return this.currentValue;
}

setValue(value) {
const result = this.hooks.trigger("valueWillChanged", value);
if (result.length !== 0 && result.some((_) => !_)) {
} else {
this.currentValue = value;
this.hooks.trigger("valueChanged", this.currentValue);
}
}

plus(addend) {
this.hooks.trigger("pressedPlus", this.currentValue, addend);
this.setValue(this.currentValue + addend);
}

minus(subtrahend) {
this.hooks.trigger("pressedMinus", this.currentValue, subtrahend);
this.setValue(this.currentValue - subtrahend);
}

multiply(factor) {
this.hooks.trigger("pressedMultiply", this.currentValue, factor);
this.setValue(this.currentValue * factor);
}

division(divisor) {
if (divisor === 0) {
console.error("Division by zero is not allowed.");
return;
}
this.hooks.trigger("pressedDivision", this.currentValue, divisor);
this.setValue(this.currentValue / divisor);
}
}

插件实现


插件要实现 apply 方法,在 Calculator 的 constructor 调用时,才能确保插件 apply 执行后会绑定(插件内的)处理函数到生命周期。


apply 的入参是 this.hooks,通过 this.hooks 来监听生命周期并添加处理器。


class LogPlugins {
apply(hooks) {
hooks.on("pressedPlus", (currentVal, addend) =>
console.log(`${currentVal} + ${addend}`)
);
hooks.on("pressedMinus", (currentVal, subtrahend) =>
console.log(`${currentVal} - ${subtrahend}`)
);
hooks.on("pressedMultiply", (currentVal, factor) =>
console.log(`${currentVal} * ${factor}`)
);
hooks.on("pressedDivision", (currentVal, divisor) =>
console.log(`${currentVal} / ${divisor}`)
);
hooks.on("valueChanged", (currentVal) =>
console.log(`结果: ${currentVal}`)
);
}
}

class LimitPlugins {
apply(hooks) {
hooks.on("valueWillChanged", (newVal) => {
if (100 < newVal) {
console.log("result is too large");
return false;
}
return true;
});
}
}

LogPlugins 的目的是记录计算器操作的详细日志。通过监听 Calculator 类中定义的事件(如加、减、乘、除操作和值变化时的事件),这个插件在这些操作执行时打印出相应的操作和结果。


LimitPlugins 的目的是在值变更前进行检查,以确保计算器的结果不会超出预设的限制(在这个例子中是 100)。如果预计的新值超出了限制,这个插件会阻止值的更改并打印一条警告消息。


通过这两个插件,Calculator 类获得了额外的功能,而无需直接在其代码中加入日志记录和值限制检查的逻辑。


完整代码


最后我们应该贴上全部代码:


class Hooks {
constructor() {
this.listener = {};
}

on(eventName, handler) {
if (!this.listener[eventName]) {
this.listener[eventName] = [];
}
this.listener[eventName].push(handler);
}

trigger(eventName, ...args) {
const handlers = this.listener[eventName];
const results = [];
if (handlers) {
for (const handler of handlers) {
const result = handler(...args);
results.push(result);
}
}
return results;
}

off(eventName, handler) {
const handlers = this.listener[eventName];
if (handlers) {
this.listener[eventName] = handlers.filter((cb) => cb !== handler);
}
}

destroy() {
this.listener = {};
}
}

class Calculator {
constructor(options = {}) {
this.hooks = new Hooks();
const { initialValue = 0, plugins = [] } = options;
this.currentValue = initialValue;
plugins.forEach((plugin) => plugin.apply(this.hooks));
}

getCurrentValue() {
return this.currentValue;
}

setValue(value) {
const result = this.hooks.trigger("valueWillChanged", value);
if (result.length !== 0 && result.some((_) => !_)) {
} else {
this.currentValue = value;
this.hooks.trigger("valueChanged", this.currentValue);
}
}

plus(addend) {
this.hooks.trigger("pressedPlus", this.currentValue, addend);
this.setValue(this.currentValue + addend);
}

minus(subtrahend) {
this.hooks.trigger("pressedMinus", this.currentValue, subtrahend);
this.setValue(this.currentValue - subtrahend);
}

multiply(factor) {
this.hooks.trigger("pressedMultiply", this.currentValue, factor);
this.setValue(this.currentValue * factor);
}

division(divisor) {
if (divisor === 0) {
console.error("Division by zero is not allowed.");
return;
}
this.hooks.trigger("pressedDivision", this.currentValue, divisor);
this.setValue(this.currentValue / divisor);
}
}

class LogPlugins {
apply(hooks) {
hooks.on("pressedPlus", (currentVal, addend) =>
console.log(`${currentVal} + ${addend}`)
);
hooks.on("pressedMinus", (currentVal, subtrahend) =>
console.log(`${currentVal} - ${subtrahend}`)
);
hooks.on("pressedMultiply", (currentVal, factor) =>
console.log(`${currentVal} * ${factor}`)
);
hooks.on("pressedDivision", (currentVal, divisor) =>
console.log(`${currentVal} / ${divisor}`)
);
hooks.on("valueChanged", (currentVal) =>
console.log(`结果: ${currentVal}`)
);
}
}

class LimitPlugins {
apply(hooks) {
hooks.on("valueWillChanged", (newVal) => {
if (100 < newVal) {
console.log("result is too large");
return false;
}
return true;
});
}
}

// 运行测试
const calculator = new Calculator({
initialValue: 0,
plugins: [new LogPlugins(), new LimitPlugins()],
});
calculator.plus(10);
calculator.minus(5);
calculator.multiply(2);
calculator.division(5);
calculator.plus(1000); // 尝试加到超过限制的值

最终输出结果如下图所示:


20240316211128


参考资料


-简单实现一个插件系统(不引入任何库),学会插件化思维


-当我们说插件系统的时候,我们在说什么


总结


通过这两个插件的例子,我们可以看到插件化设计模式在软件开发中的强大之处。它允许开发者在不修改原有代码基础上扩展功能、增加新的处理逻辑,使得应用更加模块化和易于维护。这种模式特别适用于那些需要高度可扩展性和可定制性的应用程序。


最后分享两个我的两个开源项目,它们分别是:



这两个项目都会一直维护的,如果你也喜欢,欢迎 star 🚗🚗🚗


如果你对开源项目感兴趣的,可以加我微信 yunmz777


作者:Moment
来源:juejin.cn/post/7347220605609410595
收起阅读 »

没想到,axios下载文件竟然比fetch好用

web
前言 还是和上篇一样,是关于导出excel的问题。好像是生产上导出的excel有问题,具体是啥没和我说,当然和我上篇写的没有什么关系,这是另一个模块历史遗留的问题。反正到我手里的任务就是更改导出的接口让后端去做表格。 原来的写法 原来的写法很粗暴,直接用win...
继续阅读 »

前言


还是和上篇一样,是关于导出excel的问题。好像是生产上导出的excel有问题,具体是啥没和我说,当然和我上篇写的没有什么关系,这是另一个模块历史遗留的问题。反正到我手里的任务就是更改导出的接口让后端去做表格。


原来的写法


原来的写法很粗暴,直接用window.location去跳转下载链接就把excel下载了,后端具体怎么做我的不清楚,前端的逻辑就是有一个固定的地址,然后通过query去传参让后端知道该导出什么样的excel表格。


function exportExcel(params){
const url = 'xxxxx/exportExcel?id=params.id&type=params.type'
   window.location = url
}

content-disposition


基础没学好应该也是会这样的一个疑问,为什么我在浏览器中输入一个地址就会下载文件,是的我也是,所以我去查了一下,主要是由于Content-Disposition 这个响应头字段。它告诉浏览器该文件是作为附件下载,还是在浏览器中直接打开。如果该字段的值为 attachment,则浏览器会将文件下载到本地;如果该字段的值为inline,则浏览器会尝试在浏览器中直接打开文件。


image-20241220101529424.png


语法格式




  • 其基本语法格式为:Content-Disposition: attachment; filename="filename.ext"Content-Disposition: inline; filename="filename.ext"

  • 其中,attachment表示将内容作为附件下载,这是最常见的用于文件下载的设置;而inline则表示在浏览器中内联显示内容,即直接在浏览器窗口中展示,而不是下载。

  • filename参数用于指定下载文件的名称,若不指定,浏览器可能会根据服务器返回的其他信息或自身的默认规则来确定文件名。



标题党?


才不是啊,因为我要对接的接口变成post请求,用原来这种方式肯定是不行的,这个时候我就想到了我之前写过的类似需求,就是用fetch。但是一直请求不成功,后端一直报请求参数异常。


fetch


function exportExcel(data){
fetch(`xxxxxxx/ExportExcel`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Authorization': 'xxxxxxxxxxxxxxxxxxxxxxxxxx'
}
}).then(res => {
const readableStream = res.body
if (readableStream) {
return new Response(readableStream).blob()
} else {
console.error('No readable stream available.')
}
}).then(blob => {
// 创建一个下载链接
const downloadLink = document.createElement('a')
downloadLink.href = URL.createObjectURL(blob)
// 设置下载属性,指定文件名
downloadLink.download = '测试.xlsx'
// 模拟点击下载链接
downloadLink.click()
// 释放 URL 对象
URL.revokeObjectURL(downloadLink.href)
})
}

我感觉我写的没有什么毛病啊,fetch第一个then回调转成blob数据类型,第二个then模拟a标签点击下载。但是后端老给报参数类型异常。


image-20241220105642273.png


我本来想让后端给我看看什么原因的,是什么参数没传对,还是什么请求头不对,但是他就老给甩一张swagger的请求成功的截图,根本不会帮你去看日志是因为什么原因。当然,swagger能调成功,说明接口肯定是没问题的,肯定是我没有传对东西,但是就挺烦的,都没有沟通欲望了,想着自己去换种方式去解决,然后我就想着用axios去试一下,没想到成功了


axios


function exportExcel(data) {
 axios({
   method: 'post',
   url: `xxxxx/ExportExcel`,
   data,
   responseType: 'blob'// 这里就是转化为blob文件流
}).then(res => {
   console.log(res, 'res')
     // 创建一个下载链接
   const downloadLink = document.createElement('a')
   downloadLink.href = URL.createObjectURL(res.data)
   // 设置下载属性,指定文件名
   downloadLink.download = '测试.xlsx'
   // 模拟点击下载链接
   downloadLink.click()
   // 释放 URL 对象
   URL.revokeObjectURL(downloadLink.href)
})
}

这里通过responseType设置blob值,就会自动将响应的东西转成blob二进制的格式内容,然后还是通过模拟a标签下载。相比于fetch,我们要在第二个then中对数据进行转换,而axios配置一个参数就行了。


总结


现在大部分的项目中,基本都是使用axios封装的交互方法,所以我们其实用axios是最好的,只需要配置一个参数就可以下载excel,相较于fetch来说,代码是比较简洁一点。虽然我这里fetch是没有成功的,但是放心,肯定是没有问题,是可以这样下载excel的,我估摸着应该是请求头的原因吧,可能是后端做了什么对请求头的处理,我也不知道,但是我之前做这个需求都是用fetch肯定没问题。


作者:落课
来源:juejin.cn/post/7450310230536208418
收起阅读 »

🌿一个vue3指令让el-table自动轮播

web
前言 本文开发的工具,是vue3 element-plus ui库专用的,需要对vue3指令概念有一定的了解 ​ 最近开发的项目中,需要对项目中大量的列表实现轮播效果,经过一番折腾.最终决定不使用第三方插件,手搓一个滚动指令. 效果展示 实现思路 第一步...
继续阅读 »

img


前言



本文开发的工具,是vue3 element-plus ui库专用的,需要对vue3指令概念有一定的了解



​ 最近开发的项目中,需要对项目中大量的列表实现轮播效果,经过一番折腾.最终决定不使用第三方插件,手搓一个滚动指令.


效果展示


列表滚动.webp


实现思路


第一步先确定功能



  • 列表自动滚动

  • 鼠标移入停止滚动

  • 鼠标移出继续滚动

  • 滚轮滚动完成,还可以继续在当前位置滚动

  • 元素少于一定条数时,不滚动


滚动思路


image-20241226223121217.png


image-20241226223310536.png


通过观察el-table的结构可以发现el-scrollbar__view里面放着所有的元素,而el-scrollbar__wrap是一个固定高度的容器,那么只需要获取到el-scrollbar__wrap这个DOM,并且再给一个定时器,不断的改变它的scrollTop值,就可以实现自动滚动的效果,这个值必须要用一个变量来存储,不然会失效


停止和继续滚动思路


设置一个boolean类型变量,每次执行定时器的时候判断一下,true就滚动,否则就不滚动


滚轮事件思路


为了每次鼠标在列表中滚动之后,我们的轮播还可以在当前滚动的位置,继续轮播,只需要在鼠标移出的时候,将当前el-scrollbar__wrapscrollTop赋给前面存储的变量,这样执行定时器的时候,就可以继续在当前位置滚动


不滚动的思路


​ 只需要判断el-scrollbar__view这个容器的高度,是否大于el-scrollbar__wrap的高度,是就可以滚动,不是就不滚动。


大致的思路是这样的,下面上源码


实现代码


文件名:tableAutoScroll.ts


interface ElType extends HTMLElement {
timer: number | null
isScroll: boolean
curTableTopValue: number
}
export default {
created(el: ElType) {
el.timer = null
el.isScroll = true
el.curTableTopValue = 0
},
mounted(el: ElType, binding: { value?: { delay?: number } }) {
const { delay = 15 } = binding.value || {}
const tableDom = el.getElementsByClassName(
'el-scrollbar__wrap'
)[0] as HTMLElement
const viewDom = el.getElementsByClassName(
'el-scrollbar__view'
)[0] as HTMLElement

const onMouseOver = () => (el.isScroll = false)
const onMouseOut = () => {
el.curTableTopValue = tableDom.scrollTop
el.isScroll = true
}

tableDom.addEventListener('mouseover', onMouseOver)
tableDom.addEventListener('mouseout', onMouseOut)

el.timer = window.setInterval(() => {
const viewDomClientHeight = viewDom.scrollHeight
const tableDomClientHeight = el.clientHeight

if (el.isScroll && viewDomClientHeight > tableDomClientHeight) {
const curScrollPosition = tableDom.clientHeight + el.curTableTopValue
el.curTableTopValue =
curScrollPosition === tableDom.scrollHeight
? 0
: el.curTableTopValue + 1
tableDom.scrollTop = el.curTableTopValue
}
}, delay)
},
unmounted(el: ElType) {
if (el.timer !== null) {
clearInterval(el.timer)
}
el.timer = null

const tableDom = el.getElementsByClassName(
'el-scrollbar__wrap'
)[0] as HTMLElement
tableDom.removeEventListener('mouseover', () => (el.isScroll = false))
tableDom.removeEventListener('mouseout', () => {
el.curTableTopValue = tableDom.scrollTop
el.isScroll = true
})
},
}

上面代码中,我在 created中初始化了三个变量,分别用于存储,定时器对象 、是否滚动判断、滚动当前位置。


mounted中我还获取了一个options,主要是为了可以定制滚动速度


用法



  1. 将这段代码放在你的文件夹中

  2. main.ts中注册这个指令


    import tableAutoScroll from './modules/tableAutoScroll.ts'
    const directives: any = {
    tableAutoScroll,
    }
    /**
    * @function 批量注册指令
    * @param app vue 实例对象
    */

    export const install = (app: any) => {
    Object.keys(directives).forEach((key) => {
    app.directive(key, directives[key]) // 将每个directive注册到app中
    })
    }



image-20241226224940418.png
image-20241226225027524.png


我这边是将自己的弄了一个批量注册,正常使用就像官网里面注册指令就可以了


在需要滚动的el-table上使用这个指令就可以


image-20241226225257264.png


<!-- element 列表滚动指令插件 -->
<template>
<div class="container">
<el-table v-tableAutoScroll :data="tableData" height="300">
<el-table-column prop="date" label="时间" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="address" label="Address" />
</el-table>
<!-- delay:多少毫秒滚动一次 -->
<el-table
v-tableAutoScroll="{
delay: 50,
}"
:data="tableData"
height="300"
>
<el-table-column prop="date" label="时间" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="address" label="Address" />
</el-table>
</div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
const tableData = ref<any>([])
onMounted(() => {
tableData.value = Array.from(Array(100), (item, index) => ({
date: '时间' + index,
name: '名称' + index,
address: '地点' + index,
}))
console.log('👉 ~ tableData.value=Array.from ~ tableData:', tableData)
})
</script>

<style lang="scss" scoped>
.container {
height: 100%;
display: flex;
align-items: flex-start;
justify-content: center;
gap: 100px;
.el-table {
width: 500px;
}
}
</style>

上面这个例子,分别演示两种调用方法,带参数和不带参数


最后


做了这个工具之后,突然有很多思路,打算后面再做几个,做成一个开源项目,一个开源的vue3指令集


作者:BAO_OA
来源:juejin.cn/post/7452667228006678540
收起阅读 »

马上2025年了,你还在用组件式弹窗? 来看看这个吧~

web
闲言少叙,直切正题。因为我喜欢命令式弹窗,所以就封装了它做为了业务代码的插件!如今在实际项目中跑了大半年,挺方便也挺灵活的! 如何使用 // vue2 npm install @e-dialog/v2 // main.js 入口文件 import Vue f...
继续阅读 »

闲言少叙,直切正题。因为我喜欢命令式弹窗,所以就封装了它做为了业务代码的插件!如今在实际项目中跑了大半年,挺方便也挺灵活的!


如何使用


// vue2
npm install @e-dialog/v2

// main.js 入口文件
import Vue from 'vue'
import App from './App'

//导包
import eDialog from '@e-dialog/v2'
//注册插件
Vue.use(eDialog, {
width:'50%',//全局配置
top:'15vh',
//...省略
})

new Vue({
el: '#app',
render: h => h(App)
})


// vue3
npm install @e-dialog/v3

// main.js 入口文件
import { createApp } from 'vue'
import App from './App.vue'
//导包
import eDialog from '@e-dialog/v3'

// 创建实例
const setupAll = async () => {
const app = createApp(App)
app.use(eDialog,{
width:'50%',//全局配置
top:'15vh',
//...省略
})
app.mount('#app')
}

setupAll()

插件简介


vue2是基于element ui elDialog组件做的二次封装,vue3则是基于element-plus elDialog组件做的二次封装,属性配置这一块可以全部参考element UI文档!



微信截图_20241215192735.png


扩展的属性配置

参数说明类型默认值
isBtn是否显示底部操作按钮booleantrue
draggable是否开启拖拽,vue3版本element-plus内置了该属性booleantrue
floorBtnSize底部操作按钮的尺寸medium、small、minismall
sureBtnText确定按钮的文案string确定
closeBtnText关闭按钮的文案string关闭
footer底部按钮的插槽,是一个函数返回值必须是JSXfunction-

底部插槽用法

// index.vue
<template>
<!-- vue2示例 -->
<div>
<el-button @click="handleDialog">弹窗</el-button>
</div>

</template>

<script>
//弹窗内容
import Edit form './edit.vue'
export default {
methods: {
handleDialog() {
this.$Dialog(Edit, props,function(vm,next){
//vm可以通过vm.formData拿到数据
},{
isBtn:false //如果定义了插槽,建议关闭底部操作按钮,不然会出现布局问题
footer:function(h,next){
return (
<el-button onClick={()=>{this.handleCheck(next)}}>按钮</el-button>
)
}
})
},

//按钮点击触发
handleCheck(next){
//next是一个手动关闭函数
console.log('业务逻辑')
}
}
}
</script>



页面使用:vue2

// index.vue
<template>
<!-- vue2示例 -->
<div>
<el-button @click="handleDialog">弹窗</el-button>
</div>

</template>

<script>
//弹窗内容
import Edit form './edit.vue'
export default {
methods: {
handleDialog() {
/**
* @function $Dialog是一个全局方法,自动挂载到了vue的原型
* @description 它总共接收4个参数
* @param 组件实例 | html字符串
* @param props要传递到组件的参数对象。
* @param 点击确定按钮的回调,回调里面的第一个参数是弹窗内容的组件实例,第二个参数是关闭弹窗的执行函数
* @param 配置对象,可以覆盖全局的配置
* @return void
*/


this.$Dialog(Edit, props,function(vm,next){
//vm可以通过vm.formData拿到数据
},{
//配置对象
})
}

}
}
</script>


//edit.vue
<template>
<div>弹窗内容!</div>
</template>


<script>
export default {
props:[/*这里可以接收$Dialog第二个参数props的数据*/]
data() {
return {
formData:{
a:'',
b:'',
c:''
}
}
},

}
</script>



页面使用:vue3

// index.vue
<template>
<!-- vue2示例 -->
<div>
<el-button @click="handleDialog">弹窗</el-button>
</div>

</template>

<script setup>
//弹窗内容
import Edit form './edit.vue'
const { proxy } = getCurrentInstance();
const $Dialog = proxy.useDialog()

function handleDialog() {
/**
* @function $Dialog是一个全局方法,自动挂载到了vue的原型
* @description 它总共接收4个参数
* @param 组件实例 | html字符串
* @param props要传递到组件的参数对象。
* @param 点击确定按钮的回调,回调里面的第一个参数是弹窗内容的组件实例,第二个参数是关闭弹窗的执行函数
* @param 配置对象,可以覆盖全局的配置
* @return void
*/


$Dialog(Edit, props,function(vm,next){
//vm可以通过vm.formData拿到数据
},{
//配置对象
})
}
</script>


//edit.vue
<template>
<div>弹窗内容!</div>
</template>

<script setup>
const formData = reactive({
a:'',
b:'',
c:''
})
defineExpose({ formData }) //这里注意一点要把外部要用的抛出去,如果不抛,则$Dialog回调将拿不到任何数据
</script>



函数参数设计理念


  1. 如果你弹窗内容比较复杂,例如涉及一些表单操作。最好建议抽离成一个组件,导入到Dialog第一个入参里面,如果只是简单的静态文本,则直接可以传HTML。

  2. 如果你Dialog导入的是组件,那么你有可能需要给组件传参。所以Dialog第二个入参就是给你开放的入口。

  3. 如果你点击确认按钮可能需要执行一些逻辑,例如调用API接口。所以你可能在Dialog第三个回调函数里面写入逻辑。回调函数会把第一个入参组件的实例给你传递回来,你拿到实例就可以干任何事情咯!

  4. Dialog第四个参数考虑到不同页面的配置不同。可以灵活设置。


vue2源码地址(github.com/zy1992829/e…)


vue3源码地址(github.com/zy1992829/e…)


喜欢的朋友可以去看一看,顺便帮忙点个星星。这个就不贴源码了。。


作者:阳火锅
来源:juejin.cn/post/7448661024440401957
收起阅读 »

Vue3.5正式上线,父传子props用法更丝滑简洁

web
前言 Vue3.5在2024-09-03正式上线,目前在Vue官网显最新版本已经是Vue3.5,其中主要包含了几个小改动,我留意到日常最常用的改动就是props了,肯定是用Vue3的人必用的,所以针对性说一下props的两个小改动使我们日常使用更加灵活。 一...
继续阅读 »

前言


Vue3.52024-09-03正式上线,目前在Vue官网显最新版本已经是Vue3.5,其中主要包含了几个小改动,我留意到日常最常用的改动就是props了,肯定是用Vue3的人必用的,所以针对性说一下props两个小改动使我们日常使用更加灵活。


image.png


一、带响应式Props解构赋值


简述: 以前我们对Props直接进行解构赋值是会失去响应式的,需要配合使用toRefs或者toRef解构才会有响应式,那么就多了toRefs或者toRef这工序,而最新Vue3.5版本已经不需要了。



这样直接解构,testCount能直接渲染显示,但会失去响应式,当我们修改testCount时页面不更新。



<template>
<div>
{{ testCount }}
</div>
</template>

<script setup>
import { defineProps } from 'vue';
const props = defineProps({
testCount: {
type: Number,
default: 0,
},
});

const { testCount } = props;
</script>


保留响应式的老写法,使用toRefs或者toRef解构



<template>
<div>
{{ testCount }}
</div>
</template>

<script setup>
import { defineProps, toRef, toRefs } from 'vue';
const props = defineProps({
testCount: {
type: Number,
default: 0,
},
});

const { testCount } = toRefs(props);
// 或者
const testCount = toRef(props, 'testCount');
</script>


最新Vue3.5写法,不借助”外力“直接解构,依然保持响应式



<template>
<div>
{{ testCount }}
</div>
</template>

<script setup>
import { defineProps } from 'vue';
const { testCount } = defineProps({
testCount: {
type: Number,
},
});

</script>

相比以前简洁了真的太多,直接解构使用省去了toRefs或者toRef


二、Props默认值新写法


简述: 以前默认值都是用default: ***去设置,现在不用了,现在只需要解构的时候直接设置默认值,不需要额外处理。



先看看旧的default: ***默认值写法



如下第12就是旧写法,其它以前Vue2也是这样设置默认值


<template>
<div>
{{ props.testCount }}
</div>
</template>

<script setup>
import { defineProps } from 'vue';
const props = defineProps({
testCount: {
type: Number,
default: 1
},
});
</script>

最新优化的写法
如下第9行,解构的时候直接一步到位设置默认值,更接近js语法的写法。


<template>
<div>
{{ testCount }}
</div>
</template>

<script setup>
import { defineProps } from 'vue';
const { testCount=18 } = defineProps({
testCount: {
type: Number,
},
});
</script>

小结


这次更新其实props的本质功能并没有改变,但写法确实变的更加丝滑好用了,props使用非常高频感觉还是有必要跟进这种更简洁的写法。如果那里写的不对或者有更好建议欢迎大佬指点啊。


作者:天天鸭
来源:juejin.cn/post/7410333135118090279
收起阅读 »

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

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

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


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


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


但是这有用吗?


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


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


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


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


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


那么人情世故重要吗?


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


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


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


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


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


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


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


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


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


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


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


想啥呢!


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


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


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


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


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


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


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

最强开源模型,DeepSeek V3,它来了!

2024年12月26日,DeepSeek正式发布了其最新一代大型语言模型:DeepSeek-V3。 这一模型的发布不仅标志着DeepSeek在 AGI(人工通用智能) 探索道路上的又一里程碑,也再次证明了其在开源AI领域的领先地位。 从V2.5到V3,Deep...
继续阅读 »

2024年12月26日,DeepSeek正式发布了其最新一代大型语言模型:DeepSeek-V3


这一模型的发布不仅标志着DeepSeek在 AGI(人工通用智能) 探索道路上的又一里程碑,也再次证明了其在开源AI领域的领先地位。


从V2.5到V3,DeepSeek仅用了短短几个月的时间,便完成了从通用与代码能力融合到全面性能突破的跨越。


DeepSeek里程碑


DeepSeek的初心:探索AGI的本质


DeepSeek始终秉持"投身于探索AGI的本质,不做中庸的事,带着好奇心,用最长期的眼光去回答最大的问题"的理念。这种长期主义的追求,使得DeepSeek在技术研发上不断突破,从V2.5的通用与代码能力融合,到V3的全面性能提升,每一步都彰显了其对技术创新的执着。


从V2.5到V3:性能的全面飞跃


DeepSeek-V3是一款拥有6710亿参数的专家混合(MoE)模型,激活370亿参数,基于14.8T token的预训练数据。


生成速度方面相比V2.5提升了3倍,从 20TPS 提升至惊人的 60TPS。实测回复速度极快


回复速度


在性能上,DeepSeek-V3在多项基准测试中超越了Qwen2.5-72B和Llama-3.1-405B等开源模型,并与GPT-4和Claude-3.5-Sonnet等顶尖闭源模型不相上下。尤其在数学、代码和中文任务上,V3表现尤为突出,成为当前最强的开源模型。


模型基准测试


技术创新:高效训练与推理


DeepSeek-V3采用了多项创新技术,包括多头潜在注意力(MLA)架构无辅助损失的负载均衡策略以及多token预测(MTP)目标。这些技术不仅提升了模型的推理效率,还大幅降低了训练成本。V3的整个训练过程仅耗费了278.8万H800 GPU小时,总成本约为557.6万美元,远低于其他前沿大模型。


API服务:价格调整与优惠


随着V3的发布,DeepSeek调整了API服务价格。优惠期内(即日起至2025年2月8日),API价格为每百万输入tokens 0.1元(缓存命中)/1元(缓存未命中),每百万输出tokens 2元。优惠期结束后,价格将恢复至每百万输入tokens 0.5元(缓存命中)/2元(缓存未命中),每百万输出tokens 8元。


时期Token类型缓存命中缓存未命中
优惠期内
(至2025年2月8日)
输入tokens(每百万)¥0.1¥1
输出tokens(每百万)¥2¥2
优惠期后输入tokens(每百万)¥0.5¥2
输出tokens(每百万)¥8¥8

开源与社区支持


DeepSeek-V3不仅开源了原生FP8权重,还提供了BF16转换脚本,方便社区适配和应用。SGLang、LMDeploy、TensorRT-LLM等工具已支持V3模型推理,进一步降低了用户的使用门槛。


DeepSeek-V3的实际应用


1. 官方对话平台体验


DeepSeek-V3对话已在官网上线,用户可以通过chat.deepseek.com直接体验。


在线免费使用


2. API能力与开发接入


DeepSeek API 接口,支持以下功能:



  • 多轮对话能力

  • 对话前缀续写(Beta)

  • FIM(Fill In Middle)补全

  • 结构化输出 JSON output

  • 多语言支持


开发者可以通过API文档了解详细的接入方式和示例代码:api-docs.deepseek.com


结语:开源AI的新标杆



DeepSeek-V3的发布不仅是技术的一次飞跃,更是开源精神的体现。


它不仅在性能上与世界顶尖的闭源模型媲美,更以开源的方式推动了人工智能技术的普惠发展,是当之无愧的国产之光!


未来,相信DeepSeek将会继续在AGI探索的道路上砥砺前行,为AI领域带来更多创新与突破。


哦对了,关于使用开源类ChatGPT应用 EsChatPro 接入DeepSeek 大模型的教程,可参考如下文章:


juejin.cn/post/745189…


作者:极客密码
来源:juejin.cn/post/7452914615678713856
收起阅读 »

2024 年了! CSS 终于加入了 light-dark 函数!

web
一. 前言 随着 Web 技术的不断发展,用户体验成为了设计和开发过程中越来越重要的因素之一。为了更好地适应用户的视觉偏好,CSS 在 2024 年正式引入了一项新的功能 —— light-dark() 函数。 这项功能的加入主要在于简化网页对于浅色模式(Li...
继续阅读 »



一. 前言


随着 Web 技术的不断发展,用户体验成为了设计和开发过程中越来越重要的因素之一。为了更好地适应用户的视觉偏好,CSS 在 2024 年正式引入了一项新的功能 —— light-dark() 函数。


这项功能的加入主要在于简化网页对于浅色模式(Light Mode)与深色模式(Dark Mode)的支持,使得我们能够更快更轻松轻松地实现不同的主题切换。


接下来,我们就来详细了解一下我们在开发网页是如何实现主题切换的!


以下 Demo 示例,支持跟随系统模式和自定义切换主题,先一睹为快吧!


juejin6.gif


二. 传统方式


light-dark() 函数出现之前,开发者通常需要通过 JavaScript 或者 CSS 变量配合媒体查询来实现主题切换。例如:


使用 CSS 变量 + 媒体查询


开发者会定义一套 CSS 变量,然后基于用户的偏好设置(如:prefers-color-scheme: darkprefers-color-schema: light)来改变这些变量的值。


/* 默认模式 */
:root {
--background-color: white;
--text-color: black;
}

/* dark模式 */
@media (prefers-color-scheme: dark) {
:root {
--background-color: #333;
--text-color: #fff;
}
}

也可以使用 JavaScript 监听主题切换


JavaScript 可以监听用户更改其操作系统级别的主题设置,并相应地更新网页中的类名或样式表链接。


// 检测是否启用了dark模式
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark-mode')
} else {
document.body.classList.remove('dark-mode')
}

以上这种方法虽然有效,但增加了代码复杂度,特别是当需要处理多个元素的颜色变化时,我们可能需要更多的代码来支持主题。


接下来我们看一下 light-dark 是如何实现的?


三. 什么是 light-dark?


image.png


light-dark() 是在 2024 年新加入的一种新的 CSS 函数,它允许我们根据用户的系统颜色方案(浅色或深色模式)来自动选择合适的颜色值。这个函数的引入简化了创建响应用户偏好主题的应用程序和网站的过程,而无需使用媒体查询或其他复杂的逻辑。


1. 基本用法


具体的说,light-dark() 函数接受两个参数,分别对应于浅色模式下的颜色值和深色模式下的颜色值。



  • 第一个参数是在浅色模式下使用的颜色。

  • 第二个参数是在深色模式下使用的颜色。


当用户的设备设置为浅色模式时,light-dark() 会返回第一个参数的颜色;当用户的设备设置为深色模式时,则返回第二个参数的颜色。


基本语法如下:


color: light-dark(浅色模式颜色, 深色模式颜色);

因此,light-dark() 提供了一种更简洁的方式来直接在 CSS 中指定两种模式下的颜色,而不需要额外的脚本或复杂的 CSS 结构。例如:


body {
background-color: light-dark(white, #333);
color: light-dark(black, #fff);
}

这里的 light-dark(白色, 深灰色) 表示如果用户处于浅色模式下,则背景色为白色;如果是深色模式,则背景色为深灰色。同样适用于文本颜色等其他属性。


2. 结合其他 CSS 特性


light-dark() 可以很好地与其他 CSS 特性结合使用,如变量、渐变等,以创造更加丰富多样的效果。当结合其他 CSS 特性使用 light-dark() 将更加灵活的创造页面的效果。


(1) 结合 CSS 变量


你可以利用 CSS 变量来存储颜色值,然后在 light-dark() 内引用这些变量,这样就能够在一处更改颜色方案并影响整个站点。


CSS 变量(也称为自定义属性)允许你存储可重复使用的值,这使得在不同的主题之间切换变得非常方便。你可以设置基础颜色变量,然后利用 light-dark() 来决定这些变量的具体值。


:root {
--primary-color: light-dark(#007bff, #6c757d);
--background-color: light-dark(white, #212529);
--text-color: light-dark(black, white);
}

body {
background-color: var(--background-color);
color: var(--text-color);
}

(2) 结合媒体查询


虽然 light-dark() 本身就可以根据系统偏好自动调整颜色,但有时候你可能还需要针对特定的屏幕尺寸或分辨率进行额外的样式调整。这时可以将 light-dark() 与媒体查询结合使用。


@media (max-width: 600px) {
body {
--button-bg: light-dark(#f8f9fa, #343a40); /* 更小的屏幕上按钮背景色 */
--button-text: light-dark(black, white);
}
button {
background-color: var(--button-bg);
color: var(--button-text);
}
}

(3) 结合伪类


light-dark() 也可以与伪类一起工作,比如 :hover, :focus 等,以实现不同状态下的颜色变化。


button {
background-color: light-dark(#007bff, #6c757d);
color: light-dark(white, black);
}

button:hover,
button:focus {
background-color: light-dark(#0056b3, #5a6268);
}

(4) 结合渐变


如果你希望在浅色模式和深色模式下使用不同的渐变效果,同样可以通过 light-dark() 来实现。


.header {
background: linear-gradient(light-dark(#e9ecef, #343a40), light-dark(#dee2e6, #495057));
}

(5) 结合阴影


对于元素的阴影效果,你也可以根据不同主题设置不同的阴影颜色和强度。


.box-shadow {
box-shadow: 0 4px 8px rgba(light-dark(0, 255), light-dark(0, 255), light-dark(0, 255), 0.1);
}

通过上述方法,你可以充分利用 light-dark() 函数的优势,并与其他 CSS 特性结合,创造出既美观又具有高度适应性的网页设计。这样不仅提高了用户体验,还简化了开发过程中的复杂度。


四. 兼容性


在 2024 年初时,light-dark() 函数作为 CSS 的一个新特性被加入到规范中,并且开始得到一些现代浏览器的支持。


image.png


其实,通过上图我们可以看到,light-dark() 在主流浏览器在大部分版本下都是支持了,所以我们可以放心的使用它。


但是同时我们也要注意,在一些较低的浏览器版本上仍然不被支持,比如 IE。因此,为了确保兼容性,在生产环境中使用该功能前需要检查目标浏览器是否支持这一特性。


如果浏览器不支持 light-dark(),可能需要提供回退方案,比如使用传统的媒体查询 @media (prefers-color-scheme: dark) 或者通过 JavaScript 来动态设置颜色。


五. 总结


通过本文,我们了解到,light-dark() 函数是 CSS 中的一个新特性,它允许开发者根据用户的系统偏好(浅色或深色模式)来自动切换颜色。


通过与传统模式开发深浅主题的比较,我们可以总结出 light-dark() 的优势应该包括:



  • 使用简洁:不需要编写额外的媒体查询,简洁高效。

  • 自动响应:能够随着系统的颜色方案改变而自动切换颜色。

  • 易于维护:所有与颜色相关的样式可以在同一处定义。

  • 减少代码量:相比使用多个媒体查询,可以显著减少 CSS 代码量。


light-dark() 函数是 CSS 领域的一项进步,它不仅简化了响应式设计的过程,也体现了对终端用户个性化体验的重视。随着越来越多的现代浏览器开始支持这一特性,我们未来可以在更多的应用场景中使用这一特性!


文档链接


light-dark


码上掘金演示


可以点击按钮切换主题,也可以切换系统的暗黑模式跟随:






🔥 我正在参加2024年度人气创作者评选,每投2票可以抽奖! 点击链接投票



作者:前端梦工厂
来源:juejin.cn/post/7443828372775764006
收起阅读 »

程序员为何会出纰漏?

这两天我们开发团队不知道咋的,跟包饺子下锅似的接连出了不少纰漏,有的大有的小,其实开发能力都可以,不是那种能力差导致的问题,我从外部观察,总结了一些出纰漏的原因和解决方案。 先说一下有啥纰漏。 小程序代码分包的时候,影响到线上正在使用的业务,损失了大概 1 ...
继续阅读 »

这两天我们开发团队不知道咋的,跟包饺子下锅似的接连出了不少纰漏,有的大有的小,其实开发能力都可以,不是那种能力差导致的问题,我从外部观察,总结了一些出纰漏的原因和解决方案。


先说一下有啥纰漏。



  1. 小程序代码分包的时候,影响到线上正在使用的业务,损失了大概 1 晚上的流量。

  2. 上了身-份-证、人脸认证功能,测试回归的时候,测了不需要实名和人脸的场景,没测只需要身-份-证的场景,结果线上跑的时候用这个场景,导致功能也出了问题,用户反馈过来才发现。

  3. 错把代码提交到了 dev 分支。


看起来研发该死,但恐怕不全是研发的锅,当然我不是故意找理由,这些纰漏也是研发扛下来了,我只是尝试分析从更具体的原因分析,而不是简单的说一句能力太差、或者水平不够这样没法定位也没法改进的原因。


这些出问题的场景,无一例外都是很紧急的需求,开发加班加点做出来的,代码写的时候很匆忙,测试也是加班加点测的。


常在河边走,哪有不湿鞋?我觉得快和稳之间,对开发来说很难平衡,有些需求强行要那个时间点,最后只能牺牲稳定性求效率。


那怎么避免这种事情发生?


需求可以 delay,代码不能出问题


如果工作量实在大,那就先花点时间列举工作量大的原因。大部分领导其实讲道理的,你能像他说明工作量的确大,事项的确做不完,领导会额外给时间。


我觉得这是比只闷头写代码更有难度的事,也是一种能力的体现,这需要调研充分、思维清晰、表达有条理、和领导沟通的心理等等各项挑战。


只知道埋头苦干,但干不多不一定就是好。


万一要是真的只知道埋头苦干,那也要掌控好自己的节奏,一定要保证代码的质量,平时加加班,周末也来加班,通过拉长时间线的方式多写点代码,而不是通过偷懒、减少代码逻辑的方式。


加班的时候冒冒泡,留点记录,这样即使需求 delay 了,至少自己的态度表达到位了,一般领导也不会责怪。


需求这东西,delay 两天没那么恐怖,反倒是着急出了纰漏,那才是更恐怖的。


慢慢写


写代码很费脑子,要考虑到所有可能的异常场景,还要从业务上闭环,一着急,就容易漏场景,出纰漏,不要着急,细水长流。


想好再写


尤其是后端,新业务的架构设计,一定是要多花时间思考的,要充分考虑到业务的扩展性、未来的维护性、和其他业务对接的兼容性。


比如我最近写的京东商户订单支付,我们已有一套支付中心的系统,而在对接京东的时候,他们的支付其实是通过京东的订单状态回调来做的,我们一开始准备写在支付中心,后来随着三方接口的对接,对京东业务有更深的理解,我们决定做一套新的商户订单支付系统,和原本的支付中心(支付宝、微信支付)做区分。


如果我们当时匆忙的直接嵌入到支付中心,整个系统架构就会很混乱,订单和支付裹在一起,后续既不好维护,也不好扩展。


这样虽然需求有 delay 风险,但整体技术侧的方案,是绝对没问题的。大不了我周末来加班,加班都干不完,那就得赶紧汇报领导了。


包括最开始的人脸也是的,没有调研清楚,光阿里就两套不同的人脸接口,结果先用的贵的一套,后面发现有便宜的,又强行接入便宜的一套。如果一开始能多想想,先调研清楚,可能最后的工时反而更短一些。


专一写代码不要跳


写代码的时候,最好不要来回跳需求写,看起来很牛逼感觉也很吊,实际上很容易出问题,精力消耗太快了,有些场景思考不深入,就有可能埋雷。


决策和执行分开


如果开发过程中又做决策又做执行,尤其是干需求的时候,有的决策问起来吧很耗时间,需求到期上不了线了,就自己做个决策,没有告知其他人。这种场景的雷我也踩了几个。


开发对业务的理解不如运营产品深入,有时候开发觉得的最优决策不是运营想要的,最好不要为了图省隐蔽这些问题。


甩锅技巧


这部分是语言的艺术,就是当纰漏下来了,判责归自己,怎么表达,才是比较得体的。


一直说自己的责任吧,领导会觉得我很菜,一直推脱责任吧,领导又会觉得我不负责。


最好是那种和自己有点关系,但是关系不是那么直接的描述。


或是用于日常沟通,为了避免别人误把锅扣到自己头上。


我总结了同事们常用的有如下技巧:



  1. 首先主体对象不要说自己,比如分包的问题、锁的问题、分支的问题、没有这样的场景等等,避免说成我打的包有问题、我代码写的有问题、我分支切的不对等等。

  2. 先说一些撇开自己责任的话术,比如这里的代码没动过;之前还是好好的;这里用的外部接口的数据/逻辑。

  3. 接到莫名其妙的锅第一时间弹回去,怎么弹看 2 中的话术。我以前懒得弹,结果头上的锅越来越多。


挺瞧不上这些东西的,也不想花心思想,但有时候职场、工作、社会就是这么贱,人越是老实,就越容易被欺负;越能干活的人,最后会有越来越多的活;希望大家不要重蹈我的覆辙。


每天抽点时间学习和反思,加油,共勉。


作者:小兵张健
来源:juejin.cn/post/7453023172226334739
收起阅读 »

⚡聊天框 - 微信加载历史数据的效果原来这样实现的

web
前言 我记得2021年的时候做过聊天功能,那时业务也只限微信小程序 那时候的心路历程是: 卧槽,让我写一个聊天功能这么高大上?? 嗯?这么简单,不就画画页面来个轮询吗,加个websocket也还行吧 然后,卧槽?这查看历史聊天记录什么鬼,页面闪一下不太好啊,...
继续阅读 »

前言


我记得2021年的时候做过聊天功能,那时业务也只限微信小程序


那时候的心路历程是:



卧槽,让我写一个聊天功能这么高大上??


嗯?这么简单,不就画画页面来个轮询吗,加个websocket也还行吧


然后,卧槽?这查看历史聊天记录什么鬼,页面闪一下不太好啊,真的能做到微信的那种效果吗



然后一堆调研加测试,总算在小程序中查看历史记录没那么鬼畜了,但是总是感觉不是最佳解决方案。



那时打出的子弹,一直等到现在击中了我



最近又回想到了这个痛点,于是网上想看看有没有大佬发解决方案,结果还真被我找到了。


image.png


正文开始


1,效果展示


上才艺~~~


222.gif


2,聊天页面


2.1,查看历史聊天记录的坑


常规写法加载历史记录拼接到聊天主体的顶部后,滚动条会回到顶部、不在原聊天页面


直接上图


111.gif


而我们以往的解决方案也只是各种利用缓存scroll的滚动定位把回到顶部的滚动条重新拉回加载历史记录前的位置,好让我们可以继续在原聊天页面。


但即使我们做了很多优化,也会有安卓和苹果部分机型适配问题,还是不自然,可能会出现页面闪动


其实吧,解决方案只有两行css代码~~~


2.2,解决方案:flex神功


想优雅顺滑的在聊天框里查看历史记录,这两行css代码就是flex的这个翻转属性


dispaly:flex;
flex-direction: column-reverse

灵感来源~~~


333.gif


小伙伴可以看到,在加载更多数据时



滚动条位置没变、加载数据后还是原聊天页面的位置



这不就是我们之前的痛点吗~~~


所以,我们只需要翻转位置,用这个就可以优雅流畅的实现微信的加载历史记录啦


flex-direction: column-reverse


官方的意思:指定Flex容器中子元素的排列方向为列(从上到下),并且将其顺序反转(从底部到顶部)


如果感觉还是抽象,不好理解的话,那就直接上图,不加column-reverse的样子


image.png


加了column-reverse的样子


image.png


至此,我们用column-reverse再搭配data数据的位置处理就完美解决加载历史记录的历史性问题啦


代码放最后啦~~~


2.3,其他问题


2.3.1,数据过少时第一屏展示


因为用了翻转,数据少的时候会出现上图的问题


只需要.mainArea加上height:100%


然后额外写个适配盒子就行


flex-grow: 1; 
flex-shrink: 1;

image.png


2.3.2,用了scroll-view导致的问题


这一part是因为我用了uniappscroll-view组件导致的坑以及解决方案,小伙伴们没用这个组件的可忽略~~~


如下图,.mainArea使用了height:100%后,继承了父级高度后scroll-view滚动条消失了。


image.png


.mainArea去掉height:100%后scroll-view滚动条出现,但是第一屏数据过多时不会滚动到底部展示最新信息


image.png


解决方案:第一屏手动进行滚动条置顶


scrollBottom() {
if (this.firstLoad) return;
// 第一屏后不触发
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this);
query
.select("#mainArea")
.boundingClientRect((data) => {
console.log(data);
if (data.height > +this.chatHeight) {
this.scrollTop = data.height; // 填写个较大的数
this.firstLoad = true;
}
})
.exec();
});
},

3,服务端


使用koa自己搭一个websocket服务端


3.1 服务端项目目录


image.png


package.json


{
"name": "websocketapi",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"koa": "^2.14.2",
"koa-router": "^12.0.1",
"koa-websocket": "^7.0.0"
}
}


koa-tcp.js


const koa = require('koa')
const Router = require('koa-router')
const ws = require('koa-websocket')

const app = ws(new koa())
const router = new Router()

/**
* 服务端给客户端的聊天信息格式
* {
id: lastid,
showTime: 是否展示时间,
time: nowDate,
type: type,
userinfo: {
uid: this.myuid,
username: this.username,
face: this.avatar,
},
content: {
url:'',
text:'',
w:'',
h:''
},
}
消息数据队列的队头为最新消息,以次往下为老消息
客户端展示需要reverse(): 客户端聊天窗口最下面需要为最新消息,所以队列尾部为最新消息,以此往上为老消息
*/



router.all('/websocket/:id', async (ctx) => {
// const query = ctx.query
console.log(JSON.stringify(ctx.params))
ctx.websocket.send('我是小服,告诉你连接成功啦')
ctx.websocket.on('message', (res) => {
console.log(`服务端收到消息, ${res}`)
let data = JSON.parse(res)
if (data.type === 'chat') {
ctx.websocket.send(`我也会说${data.text}`)
}
})
ctx.websocket.on('close', () => {
console.log('服务端关闭')
})
})

// 将路由中间件添加到Koa应用中
app.ws.use(router.routes()).use(router.allowedMethods())

app.listen(9001, () => {
console.log('socket is connect')
})



切到server目录yarn


然后执行nodemon koa-tcp.js


没有nodemon的小伙伴要装一下


image.png


代码区


完整项目Github传送门


聊天页面的核心代码如下(包含data数据的位置处理和与服务端联动)



完结


这篇文章我尽力把我的笔记和想法放到这了,希望对小伙伴有帮助。


到这里,想给小伙伴分享两句话



现在搞不清楚的事,不妨可以先慢下来,不要让自己钻到牛角尖了


一些你现在觉得解决不了的事,可能需要换个角度



欢迎转载,但请注明来源。


最后,希望小伙伴们给我个免费的点赞,祝大家心想事成,平安喜乐。


image.png


作者:尘落笔记
来源:juejin.cn/post/7337114587123335180
收起阅读 »

在高德地图上实现建筑模型动态单体化

web
前言 前段时间系统性地学习了国内知名GIS平台的三维应用解决方案,受益匪浅,准备分几期对所学内容进行整理归纳和分享,今天来讲讲城市3D模型的处理方案。 城市3D模型图层在Web GIS平台中能够对地面物体进行单独选中、查询、空间分析等操作,是属于比较普遍的功能...
继续阅读 »

前言


前段时间系统性地学习了国内知名GIS平台的三维应用解决方案,受益匪浅,准备分几期对所学内容进行整理归纳和分享,今天来讲讲城市3D模型的处理方案。


城市3D模型图层在Web GIS平台中能够对地面物体进行单独选中、查询、空间分析等操作,是属于比较普遍的功能需求。这样的图层一般不会只有单一的数据来源,而是个集成了倾斜投影、BIM建模、动态参数建模的混合型图层,并且需要对模型进行适当的处理优化以提升其性能,因此在GIS平台不会选择支持单独地选中地图,我们需要对模型进行拆分——即单体化处理。


对建筑模型单体化处理通常有三种方案,各有其优点和适用场景,对比如下:


方案实现原理优势缺陷
切割单体化将三维模型与二维面进行切割操作成为单独可操作对象能够实现非常精细的单体化效果,适用于精细度要求高的场景数据处理量大,对计算机性能要求较高
ID 单体化预先为每个三维模型对象和二维矢量数据分配一个可关联的 ID数据处理相对简单,不需要进行复杂的几何运算,适用于数据量相对较小、模型结构相对简单的场景可能会出现 ID 管理困难的情况,且对于模型的几何形状变化适应性较差
动态单体化实时判断鼠标点击或选择操作的位置与三维模型的关系,动态地将选中的部分从整体模型中分离出来进行单独操作无需预先处理数据,可根据用户的交互实时进行单体化操作,适用于需要快速响应用户交互的场景对计算机的图形处理能力和性能要求较高

在这些方案中,动态单体化无需对模型进行预处理,依数据而变化使用较为灵活,接下来我们通过一个简单的例子来演示该方案的整体实现过程,源代码在这里供下载交流。


Honeycam_2024-08-18_16-31-38.gif


需求分析


假设我们拿到一个城市行政区域内的三维建筑数据,对里面该区域里面的几栋关键型建筑进行操作管理,我们需要把建筑模型放置到对应的地图位置上,可宏观地浏览模型;支持通过鼠标选中建筑的楼层,查看当前楼层的相关数据;楼层数据支持动态变更,且可以进行结构性存储。因此我们得到以下功能需求:



  1. 在Web地图上建立3D区域模型图层

  2. 根据当前光标位置动态高亮楼层,并展示楼层基本信息

  3. 建筑单体化数据为通用geoJSON格式,方便直接转换为csv或导入数据库


技术栈说明


工具名称版本用途
高德地图 JSAPI2.0为GIS平台提供基础底图和服务
three.js0.157主流webGL引擎之一,负责实现展示层面的功能
QGIS3.32.3GIS数据处理工具,用于处理本文的矢量化数据
cesiumlab3.1.11三维数据处理工具集,用于将模型转换为互联网可用的3DTiles
blender3.6模型处理工具,用于对BIM模型进行最简单的预处理

实现步骤


制作3DTiles


城市级的三维模型通常以无人机倾斜投影获取到的数据最为快捷,数据格式为OSGB且体积巨大不利于分享,由于手头没有合适的倾斜投影数据,我们以一个小型的BIM模型为例进行三维瓦片化处理也是一样的。



  1. 模型预处理。从sketchfab寻找到一个合适的建筑模型,下载其FBX格式并导入到模型处理工具(C4D、blender等)进行简单的预处理,调整模型的大小、重置坐标轴原点的位置到模型的几何中心,然后导出带材质的FBX模型备用,这里blender如何带材质地导出模型有一些技巧。


    image.png


  2. 启动cesiumlab,点击“通用模型切片”选项,选择预处理好的模型,指定它的地理位置(ENU: 维度,经度),点击确认


    image 1.png


  3. 在最后的“数据存储”设置原始坐标为打开、存储类型为散列(最终输出多个文件)、输出路径,提交处理等待3DTiles生成


    image 2.png


  4. 生成过程结束后我们来到分发服务选项,点击chrome的图标就能够进入3DTiles的预览了,注意看路径这一列,这里面包含了入口文件tileset.json的两个路径(文件存储目录和静态资源服务地址),后面开发中我们会用到它。


    image 3.png


  5. 至此模型准备完毕,我们可以把输出出的3Tiles目录放到开发i工程中,也可以单独部署为静态资源服务,保证tileset.json可访问即可。


    image 4.png


  6. 开发3DTiles图层,详细的教程之前已经分享过了,这里直接上代码。



    // 默认地图状态
    const mapConf = {
    name: '虚拟小区',
    tilesURL: '../static/tiles/small-town/tileset.json',
    //...
    }

    // 添加3DTiles图层
    async function initTilesLayer() {

    const layer = new TilesLayer({
    container,
    id: 'tilesLayer',
    map: getMap(),
    center: [113.536206, 22.799285],
    zooms: [4, 22],
    zoom: mapConf.zoom,
    tilesURL: mapConf.tilesURL,
    alone: false,
    interact: false
    })

    layer.on('complete', ({ scene }) => {
    // 调整模型的亮度
    const aLight = new THREE.AmbientLight(0xffffff, 3.0)
    scene.add(aLight)
    })

    layerManger.add(layer)
    }


  7. 这一阶段实现的效果如下


    Honeycam_2024-08-18_17-45-29.gif



创建单体化数据



  1. 使用QGIS处理矢量数据,绘制建筑模型轮廓的矢量面。由于本示例是虚拟的,我们需要自己创建矢量数据,把上一个步骤完成的内容高空垂直截图导入到QGIS上配准位置,作为描绘的参考图。


    image 5.png


  2. 创建形状文件图层,进入编辑模式绘制建筑轮廓


    image 6.png


  3. 选择图层右键打开属性表,开始编辑每个建筑的基础数据,导出为monobuildingexample1.geojson


    image 7.png


  4. 对关键建筑“商业办公楼A”和“商业办公楼B”的楼层数据进行进一步编辑(bottomAltitude为每层楼的离地高度,extendAltitude为楼层高度),这块数据与GIS无关,我直接用wps图表去做了,完成后将csv文件转换成为json格式,然后与monobuildingexample1.geojson做一个组合,得到最终的geoJSON数据。


    image 8.png



开发动态单体化图层


底座和数据准备好,终于可以进行动态单体化图层开发了,实现原理其实非常简单,根据上一步获得的建筑矢量和楼层高度数据,我们就可以在于模型匹配的地理位置上创建若干个“罩住”楼栋模型的盒状网格体,并监听网格体的鼠标拾取状态,即可实现楼层单体化交互。


image 9.png



  1. 我们的数据来自monobuildingexample1.geojson,生成每个楼层侧面包围盒的核心代码如下,通过path数据和bottomAltitued、extendAltitude就能得到网格体的所有顶点。



    /**
    * 根据路线创建侧面几何面
    * @param {Array} path [[x,y],[x,y],[x,y]...] 路线数据
    * @param {Number} height 几何面高度,默认为0
    * @returns {THREE.BufferGeometry}
    */

    createSideGeometry (path, region) {
    if (path instanceof Array === false) {
    throw 'createSideGeometry: path must be array'
    }
    const { id, bottomAltitude, extendAltitude } = region

    // 保持path的路线是闭合的
    if (path[0].toString() !== path[path.length - 1].toString()) {
    path.push(path[0])
    }

    const vec3List = [] // 顶点数组
    let faceList = [] // 三角面数组
    let faceVertexUvs = [] // 面的UV层队列,用于纹理和几何信息映射

    const t0 = [0, 0]
    const t1 = [1, 0]
    const t2 = [1, 1]
    const t3 = [0, 1]

    for (let i = 0; i < path.length; i++) {
    const [x1, y1] = path[i]
    vec3List.push([x1, y1, bottomAltitude])
    vec3List.push([x1, y1, bottomAltitude + extendAltitude])
    }

    for (let i = 0; i < vec3List.length - 2; i++) {
    if (i % 2 === 0) {
    // 下三角
    faceList = [
    ...faceList,
    ...vec3List[i],
    ...vec3List[i + 2],
    ...vec3List[i + 1]
    ]
    // UV
    faceVertexUvs = [...faceVertexUvs, ...t0, ...t1, ...t3]
    } else {
    // 上三角
    faceList = [
    ...faceList,
    ...vec3List[i],
    ...vec3List[i + 1],
    ...vec3List[i + 2]
    ]
    // UV
    faceVertexUvs = [...faceVertexUvs, ...t3, ...t1, ...t2]
    }
    }

    const geometry = new THREE.BufferGeometry()
    // 顶点三角面
    geometry.setAttribute(
    'position',
    new THREE.BufferAttribute(new Float32Array(faceList), 3)
    )
    // UV面
    geometry.setAttribute(
    'uv',
    new THREE.BufferAttribute(new Float32Array(faceVertexUvs), 2)
    )

    return geometry
    }


  2. 经过前面步骤,得到网格体如下


    Honeycam_2024-08-18_18-24-35.gif


  3. 添加默认状态和选中状态下材质


    initMaterial () {
    const { initial, hover } = this._conf.style
    // 顶部材质
    this._mt = {}
    this._mt.initial = new THREE.MeshBasicMaterial({
    color: initial.color,
    transparent: true,
    opacity: initial.opacity,
    side: THREE.DoubleSide,
    wireframe: true
    })
    this._mt.hover = new THREE.MeshBasicMaterial({
    color: hover.color,
    transparent: true,
    opacity: hover.opacity,
    side: THREE.DoubleSide
    })
    }


  4. 添加拾取事件,对选中的网格体Mesh设置选中材质,并对外派发事件


    // 处理拾取事件
    onPicked ({ targets, event }) {
    let attrs = null
    if (targets.length > 0) {
    const cMesh = targets[0]?.object
    if (cMesh?.type == 'Mesh') {
    // 设置选中状态
    this.setLastPick(cMesh)
    attrs = cMesh._attrs
    } else {
    // 移除选中状态
    this.removeLastPick()
    }
    } else {
    this.removeLastPick()
    }
    /**
    * 外派模型拾取事件
    * @event ModelLayer#pick
    * @type {object}
    * @property {Number} screenX 图层场景
    * @property {Number} screenY 图层相机
    * @property {Object} attrs 模型属性
    */

    this.handleEvent('pick', {
    screenX: event?.pixel?.x,
    screenY: event?.pixel?.y,
    attrs
    })
    }


  5. 外部监听到拾取事件,调动浮层展示详情


    /**
    * 建筑单体化图层
    * @return {Promise<void>}
    */

    async function initMonoBuilding() {
    const data = await fetchData('../static/mock/monobuildingexample1.geojson')
    const layer = new MonoBuildingLayer({
    //...
    data
    })
    layerManger.add(layer)

    layer.on('pick', (event) => {
    updateMarker(event)
    })
    }
    // 更新浮标
    function updateMarker(event) {
    const { screenX, screenY, attrs } = event

    if (attrs) {
    // 更新信息浮层
    const { id, name, belong, bottomAltitude, extendAltitude } = attrs
    tip.style.left = screenX + 20 + 'px'
    tip.style.top = screenY + 10 + 'px'
    tip.innerHTML = `
    <ul>
    <li>id: ${id}</li>
    <li>楼层: ${name}</li>
    <li>离地高度: ${bottomAltitude}米</li>
    <li>楼层高度: ${extendAltitude}米</li>
    <li>所属: ${belong}</li>
    </ul>
    `

    tip.style.display = 'block'
    // 更新鼠标手势
    container.classList.add('mouse_hover')
    } else {
    tip.style.display = 'none'
    container.classList.remove('mouse_hover')
    }
    }


  6. 最终得到的交互效果如下


    Honeycam_2024-08-18_18-56-30.gif


  7. 把3DTiles图层和点标记图层加上叠加显示,得到本示例最终效果


    Honeycam_2024-08-18_19-03-00.gif



待拓展功能



  1. 对建筑模型单体的进一步细化


    楼层功能还可以细化到每个楼层中各个户型,也许每个楼层都有独特的户型分布图,这个应该结合内部的墙体轮廓一起展示,选个弹窗在子内容页进行下一步操作,还是直在当前场景下钻到楼层内部?具体交互流程我还没想好。


  2. 如何处理异体模型


    目前的方案仅针对规规矩矩的立方体建筑楼栋,而对于鸟巢、大裤衩、小蛮腰之类的异形地标性建筑,每个楼层的轮廓可能都是不一样的,因此在数据和代码方面仍需再做改进。


    image 10.png



本示例使用到的高德JSAPI


3D自定义图层AMap.GLCustomLayer


AMap.Map地图对象类


点标记: 用于在地图上添加点状地图要素


空间数据计算的函数库 GeometryUtil


相关工具链接


Sketchfab上免费下载的小区模型


使用blender导出带材质的FBX文件


在线将cvs文件转换为JSON


作者:Gyrate
来源:juejin.cn/post/7404007685643501595
收起阅读 »

一封写给离职小伙伴的信

前言 亲爱的小伙伴,当你看到这封信的时候,相信你大概率也是离职浪潮中的一员了,不管是被裁还是主动离职,相信在接下来的日子里,求职路上必定不会一帆风顺,势必要经历一番波折与挑战。 也许你才刚开始,此刻意气风发信心满满;也许你正在经历,此时彻底怀疑自我,将要放弃底...
继续阅读 »

前言


亲爱的小伙伴,当你看到这封信的时候,相信你大概率也是离职浪潮中的一员了,不管是被裁还是主动离职,相信在接下来的日子里,求职路上必定不会一帆风顺,势必要经历一番波折与挑战。


也许你才刚开始,此刻意气风发信心满满;也许你正在经历,此时彻底怀疑自我,将要放弃底线;也许你已经历过了,此时已是遍体鳞伤体无完肤,彻底摆烂……


不管屏幕前的你是哪一种,但请记住,这是每个人入世时社会老师要给我们上的第一节课。


在这里,我先分享一段平凡普通又心酸的求职历程,也希望通过这篇文章能给你一些启发和帮助,也衷心地希望你能够重拾信心,一路披荆斩棘!




关于我


我是双非一本毕业,计算机专业,目前毕业已有九年了,一直在远离家乡的北漂之地工作。由于母校没啥名气且名字中带有的小众地域性,非本省的人很少知道它,所以在外省找工作经常会被问你是不是本科毕业(尴尬😅),学历这一块姑且算是杂牌吧,起码在筛选简历时不具备任何优势。


其次,简历也没有太多的亮眼经历,没有大厂背景,基本都是中小厂的工作经历,普通到不能再普通了。


其三,关于年龄这一块,本人目前很接近35岁的“退休”年龄,属于被重点劝退互联网的年纪,触碰到了互联网年龄红线。


最后,这还不算最糟糕,还有更糟糕的面试门槛。


什么是糟糕的面试门槛?接着往下看。


由于去年家里出事了,辞职回了老家,之后也没着急出来找工作,期间闲来无事,顺便考了个公(原本辞职也不是为了考公),结果可想而知,也没考上,所以也没当回事,该玩玩,该旅游就旅游,彻底放飞自我~(没有房贷车贷毫无顾忌)


等我再次出来找工作的时候,已经离上一份工作的间隔有一年多了(准确的说是一年零两个月),也就是说我已经 Gap 一年多了,我还丝毫没意识到这将成为我求职路上的一个障碍。




面试的准备


7月初,我正式开始准备面试。首先更新了简历,回顾之前的项目并总结,梳理项目架构、流程图、负责的模块以及技术难点。同时看了看八股文复习基础知识,刷刷leetcode,时间过得很快,大概三周后开始投简历。




面试的门槛


Gap一年多的经历,让我在求职中非常被动,一线大厂全都因为这段 Gap 经历被 HR 前期的电话沟通中直接否掉,连一面的面试机会都没有。


只有一家公司例外,那就是字节,也是我面试的第一家公司。首先不得不表扬一下这家公司的用人态度,只是我准备不足仓促应付,以为刷了算法题就没啥大问题,结果人家来了一个手撕题(非算法),当场把我给整懵了,结果可想而知……其实本质上还是综合能力不够,但起码人家给了你面试的机会。


不管最终结果如何,单凭这种不问出身来者不拒,招人包容的胸襟与态度就值得点赞👍


因此,后面我能接到面试的机会只有中小厂,一间大厂都没有,这也许就是 Gap 太久要付出的代价之一吧。




面试的过程


求职的过程比较曲折,毕竟离开岗位已经一年多了,很多零零碎碎的知识要整理起来也不是一蹴而就的,因此求职期间,一边面试,一边不断总结经验,把之前做过的东西以及面试的空白知识慢慢梳理出来,并整理成博客。通过系统化的梳理与表达,自己的思路也开始有了更清晰的脉络和方向。


经过三个多月的面试,期间共投了三十多家公司的简历,排除一线大厂以及被其他公司pass掉的简历,其实真正接到面试机会的一共只有二十家左右。


期间有一段是没有面试的空白期,那段时间真的怀疑自己,很彷徨,是不是真的该投外包和od,这里没有贬低的意思,只是目前自己还没有养老的打算,同时也有自己的职业规划和方向,外包暂时还不在考虑范围之内。


从刚开始投的高薪大中厂,到后来的中小厂,虽然姿态一直在放低,但终归守住了自己的底线——那就是行业方向和薪资待遇。




面试中的奇葩


面试过程中也会遇到各种各样的面试官,结合自己曾经也做过面试官的经验,一些常规的套路基本是熟悉的,幸运的是遇到的绝大多数面试官都非常的nice,当然也遇到个别的奇葩。


比如这次遇到了一个思路清奇的二面面试官,一面聊得还挺好的,本以为二面面缘应该也不会差,没想到上来他就开始板着脸,似乎人人都欠他八百万似的,之所以如此,直到后来我才知道,原来他在怀疑我简历造假。等我介绍完项目,没问技术实现细节,而是开始扣字眼,这个项目公司内部的名字叫什么?为什么简历中没提这个项目的内部名字?xxx公司有海外方向吗?我全程耐心地解释,一度怀疑他才是我前司的员工,而我不是。


最后问了我那个项目的域名地址,那个项目我也只跟进了一期,没有太深的记忆,当时没有找出来,后来就草草结束了。


这里也怪我,由于离开前司一年多,项目域名早变了,而自己在整理项目时没有及时跟进,才导致如此尴尬局面,后来我才知道那个项目的域名早已换成了官网域名,把它整合进海外官网了。


其实我也非常理解他的这种行为,换成是我,遇到这种情况,我也会对面试者产生怀疑。然而从逆向思维的角度分析,像我这种非大厂背景,学历看着又像渣本的简历,怎么造,简历都不会好到哪里去吧,何况还有背调,我又何必费那个心思。想想都觉得有点滑稽~


这里也给自己总结两点经验:



  • 凡是可能涉及到的点,都要一一回顾,有备无患,但说实话经历过那么多年的面试,不管是我面别人还是别人面我,问项目域名的我还是头一次遇到。

  • 怀疑一旦开始,罪名就已经成立。不管你后面如何辩白,结局其实早已注定,还不如趁早结束。即使你有幸通过面试,将来在一位不信任你的领导下干活,也是一件非常心累的事。


面试的结果


经过将近20家公司的轮番摩擦,终于在10月底的时候,陆陆续续有了口头offer,又经过银行流水、背调、体检,最终拿到了3家公司的正式offer,两家小公司和一家独角兽。


那家独角兽公司我很早就知道,其实也一直是我想进的一家公司,因此毫无悬念,我最终选择了那家中厂独角兽,总包降了一点点,但总算还是属于平跳,这个结果在当下的环境,对我来说已经很难得了。


面试经验分享


这里不提供什么面经,因为每面的问题几乎都不一样,几乎没有碰到任何相同的一个问题(自我介绍、项目介绍和HR面除外)。


面了这么多公司,印象中只有一道题是重复的,所以即使给出各家公司的面经,真正轮到你面时,出的题也会因人而异。面经并不是圣经,只是可以作为一个难度级别的参考罢了,所以面经意义其实并不大。


这里我想分享更多的是个人职业的规划与成长。


其实面试的过程本身就是一个提升自我认知的过程。


面对如此困境,我想分享一下我是如何破局的。


作为一个普通、大龄、又gap一年多的普通程序猿,我首先做的便是潜下心来扪心自问,在这么激烈的竞争环境中,与其他人相比,我的优势在哪?





  • 首先是职业稳定性,我虽然没有大厂背景,但还算稳定,总共经历3家公司,只有中间一家是因疫情原因而不满一年,八年时间里没有频繁跳槽还算稳定。

  • 其次是职业方向,个人之前从事的行业一直与电商领域相关,前几段工作经历一直是从事海外电商方向。因此,在海外电商这个领域中我有天然的行业经验优势,从最终拿到的3家offer公司的结果来看,也反向证明了这一点。

  • 其三,投简历时我只会挑选特定的领域方向,我不会进行海投,更不会盲投,因为那没有什么意义,因为人家没看上你的,即便投了也不会有什么结果,最多只会礼貌地给你回访,并不会真正进入面试流程。


    这是我在脉脉上得出的结论,因为对你发起的沟通是系统推荐,是系统发起的并不是他们本人,因此他们并不一定对你感兴趣(当然你若是那种天之骄子又有大厂背书,就请忽略我们这种平凡人的经历)。


    因此,投简历时我一般只在boss上,并且是被动投简历,也就是别人先发起的简历请求。底层逻辑是因为只有人家对你感兴趣,你才会有更大的可能获得面试机会


  • 其四,珍惜每一次面试的机会,看清楚JD里的要求和公司从事的方向,对JD中描述的职位要求和职责,有针对性地准备面试,面试时遇到自己知识盲区要诚实表示不会,不要试图与面试官产生争论,因为即便你是对的,也会对你产生不利。坦然接受任何结果,放松心态。

  • 其五,放低姿态,降低期望,期望越高,失望越高。由于我已经离职并 Gap 太久,没有任何骑驴找马的依仗,谈薪资时显得非常被动,这便是 Gap 太久的代价之二。


    之前也面过几个到终面的公司,谈薪时过高而被Pass掉,我的诉求也很简单,就是期望能平薪而跳,也没指望能涨薪,也愿意接受适度降薪。


    期间,有遇到过很爽快的,也有遇到拼命压你薪的,面了很多小公司都有这样一个相同的经历,到谈薪阶段,他们会故意冷淡你不跟你谈,而是经过多轮对比候选人,看看哪个性价比更高。其实也非常理解,毕竟当前的环境下,哪个老板不希望用低薪招到一个综合能力更强的人呢?


    而有的会比较爽快,他们急着大量招人,流程会很快,也不会过分压薪,碰到这种公司那么恭喜你,你中奖了,这种基本在经济行情好(公司盈利大好)的时候才会出现。


    放低姿态不意味着放弃原则反而要有更清晰的底线,它的底层逻辑是降低期望值,期望值是心理学上一个很巧妙东西,期望锚点值越低,才有更大的可能获得惊喜。


  • 最后,主动沉淀和总结经验,对面试中经常问到的同一类问题进行总结和思考,进而纳入自己的脑海中,逐渐形成自己的知识体系。


    比如,你为什么会从上一家公司离职?问这个问题的背后动机是什么?HR为什么会压你薪资?这些问题的背后原理一定要思考清楚,只有理解底层逻辑,才能应对自如。


    当然,搞清楚这些问题的背后逻辑不是为了让你去说谎,面试时可以结合自身的实际,美化用词,但不要试图说谎。





总结


以上就是一个平凡普通又Gap一年多的打工人自我总结与分享,也希望它能给你带来一些启发和帮助,哪怕只有一点点,那也是我莫大的荣幸!


眼下的就业形势确实不容乐观,招聘越来越挑剔,要找到心仪的工作实属不易,但请不要因此而对生活失去信心,要相信好好总结与反思,总有一个位置属于你。


愿屏幕前的你能渡过难关、顺利上岸!加油!


一封来自远方陌生人的信


作者:九幽归墟
来源:juejin.cn/post/7444773769242116111
收起阅读 »

震惊,开源项目vant 2.13.5 被投毒,挖矿!

web
2024年12月19日,vant仓库新增一条issue,vant 2.13.5 被投毒,挖矿。 具体原因 可能是团队一名成员的 token 被盗用 与本次事件关联的攻击 攻击者在利用 @landluck 的 token 进行攻击后,进一步拿到了同个 GitHu...
继续阅读 »

2024年12月19日,vant仓库新增一条issue,vant 2.13.5 被投毒,挖矿


具体原因


可能是团队一名成员的 token 被盗用


与本次事件关联的攻击


攻击者在利用 @landluck 的 token 进行攻击后,进一步拿到了同个 GitHub 组织下的维护者 @chenjiahan 的 token,并发布了带有相同恶意代码的 Rspack 1.1.7 版本。


Rspack 团队已经在一小时内完成该版本的废弃处理,并发布了 1.1.8 修复版本,参考 web-infra-dev/rspack#8767 (comment)


目前相关 token 已经全部清理。


相关版本


以下异常版本被盗号者注入了脚本,已经全部标记为废弃,请勿使用!


image.png


有使用的大家可以升级版本,降低影响。


作者:一诺滚雪球
来源:juejin.cn/post/7450001084067627058
收起阅读 »

腾讯打工8年:我为什么选择离开?

"你真的要离开腾讯吗? " 在提完离职的那一刻,总监脸上难掩惊讶。我知道,对于很多人来说,这个决定可能显得不够理性——毕竟,腾讯仍是众多程序员心中的梦想企业,而我在这里已经连续五年获得五星好评,而且还有很多股票,似乎一切都在正轨上。 但此时的我,内心异常平静。...
继续阅读 »

"你真的要离开腾讯吗? "


在提完离职的那一刻,总监脸上难掩惊讶。我知道,对于很多人来说,这个决定可能显得不够理性——毕竟,腾讯仍是众多程序员心中的梦想企业,而我在这里已经连续五年获得五星好评,而且还有很多股票,似乎一切都在正轨上。


但此时的我,内心异常平静。


记得第一次走进腾讯大厦,仰望着那座象征着互联网巅峰的建筑,内心既激动又忐忑。彼时的我,怎么也想不到,八年后,我会主动选择告别。


人们常说,选择大于努力。但在我看来,每一个选择背后,都是一段难以言说的心路历程。


今天,我想和大家分享一下我的故事,不为别的,只是希望正在经历职业迷茫的你,能从我的经历中获得一些启发。


山沟沟出来的孩子


"柯...什么?"


每次报出自己的家乡,对方脸上都会露出这种尴尬的表情。我来自克孜勒苏柯尔克孜自治州,这个拗口的名字,是祖国最西北的一隅。那里四面环山,与戈壁相伴,远离喧嚣。后来我索性只说自己是喀什人,倒也省去了许多解释的麻烦。


在那个被群山环绕的小县城里,我的父母都是普通的公职人员。没有精英家庭的财商教育,没有琴棋书画的素质培养,但他们给了我最宝贵的礼物——选择的自由。现在想来,这份"不懂"反而成就了今天的我。


2000年的某一天,我第一次见证了互联网的魔力。那是在拨号上网的年代,Windows 95系统的笔记本电脑屏幕上,搜狐的首页缓缓展开。 蓝色的网页在我眼前闪烁,像打开了一扇通向新世界的窗。就是那一刻,一颗小小的种子,悄然埋入了我的心底。


从Frontpage搭建简陋的网站,到用VC编写第一个"Hello World",再到沉迷于QQ带来的社交乐趣。每当看到腾讯的Logo,我就会想:如果有一天能在这样的公司工作,该有多好。 当时的我,把这个想法深深藏在心里,像是一个遥不可及的梦。


高中时期的一次全国模考,我考了数学满分。校长把我叫去谈话,说我有希望冲击北大。虽然最后因为理综的失误与北大失之交臂,但我还是如愿考入了西安交通大学——这所充满历史底蕴的顶尖学府。


然而,真正的挑战才刚刚开始。在这个人才济济的地方,我才发现自己有多渺小。 没有选择计算机专业成了我最大的遗憾,一个简单的递归算法都能让我纠结好几天。但正是这种"较真"的性格,让我在挫折中不断成长。死磕,成了我的标签。


在校期间,我像一块海绵,疯狂地吸收着各种知识。从设计美工到前端开发,从后台架构到算法优化,每一个技术社团都留下了我的身影。终于,在不懈的努力下,我拿到了一张通往互联网大厂的船票。


那一刻,我知道,山沟沟里的孩子,也能走出属于自己的路。


第一次为技术跳槽


"你的代码刚刚上线了,已经有超过一百万人在使用了。"


收到这条消息的那一刻,我的心跳漏了一拍。 这是在腾讯的第一个项目——QQ厘米秀。每天打开后台,看着用户们的反馈如潮水般涌来:有赞美、有吐槽、有建议。我常常在深夜里一条条查看这些留言,时不时还会收到用户的好友申请,他们热切地称呼我为"腾讯大佬"。那种感觉,就像是漫漫长夜里终于看到了星光。


然而,技术圈的浪潮从不等人。


某天刷技术社区时,我突然发现自己像是站在了时代的孤岛上。React、Vue、Webpack、Gulp......这些响彻业界的技术名词,对我而言却如同天书。而我们的项目还在用着jQuery这个"老古董",连基本的构建工具都没有,每天都在"刀耕火种"般地手动部署。


那种焦虑感,就像是站在原地看着别人坐上了飞驰的列车。


凭借着还算不错的工作表现,我争取到了一次技术重构的机会。导师和leader都给予了支持,我像个不知天高地厚的愣头青,义无反顾地扎进了新技术的海洋。然而现实给了我一记重拳——性能出现了劣化,工具链的改造也让团队措手不及。那时我才明白,单打独斗的技术进化是多么艰难。


"我想去IMWeb团队。"


这个决定让我的总监足足和我谈了两个小时。"我从来没有对其他人挽留这么长时间, "他说。当时我将信将疑,直到多年后自己成为leader,才明白这份真诚。临别前,他留下了一句话:"都是围城。 "这句话如同一颗种子,在之后的每一个选择前都会在我心里发芽。


就这样,我选择了内部转岗到IMWeb。这是一个我仰慕已久的技术团队,他们举办的每一场技术大会都让我热血沸腾。虽然这个选择意外地把我带入了教育行业,但在当时的我看来,技术才是唯一的追求。业务?那是以后才需要考虑的事情。


现在想来,那时的我就像个执着的技术苦行僧,只顾着追逐技术的浪潮,却还没有领悟到技术与业务相辅相成的真谛。


连续五星,快速成长


如果说技术是一场马拉松,那么我大概是那种不懂得"匀速"的选手。


刚加入IMWeb团队时,我像个饥渴的海绵,疯狂地汲取着每一滴技术养分。从底层构建到视图框架,从脚手架到部署平台,我把每一个技术细节都当作一道必解的谜题。常常是凌晨三点,办公室里只剩下我和显示器的蓝光。 那种投入的状态,连清洁阿姨都会打趣说:"小伙子,你是不是把家搬到公司了? "


在那段"技术狂热"的日子里,我给自己定下了一个原则:不设边界。整个开发链路上,但凡觉得哪个环节"不够优雅",我就一定要想办法优化它。那时候DevOps还没有成为主流,我就开始研究效能工具,开发团队机器人。书架上摞满了技术书籍,从 设计模式 到工程实践,每一本都写满了笔记和标签。


技术成长就像滚雪球,一旦开始就会越滚越大。从前端基建到监控平台,再到低代码转型,每一步都让我对技术有了更深的理解。记得有一次要同时处理性能优化项目和晋升答辩,我在办公室里连续工作到天明,端着咖啡顶着两个黑眼圈走进答辩室。 那种"拼命"的状态,现在想来还是会被自己感动。


"凡事有着落,进展有反馈。"这是我总结出的工作法则。每个项目我都设法做出亮点:重构后的代码性能提升80%,新开发的工具链将团队效率提高50%,监控平台覆盖了整个业务线...这些数据背后,是无数个加班的夜晚,无数次的推倒重来。


慢慢地,我开始跨界探索。后台架构、消息队列、数据仓库,这些"后端"的概念逐渐成为我的工作日常。还记得第一次用Go语言重写监控系统时的忐忑,以及系统成功上线时的兴奋。 那种跨越技术边界的快感,让人上瘾。


三年时间,从T9到T11,两次走绿色通道提名。五次五星绩效,每一次都是对付出的肯定。但最让我感动的,不是这些头衔和数字,而是那种全情投入后的成就感。 就像跑步一样,你永远记得突破自己最好成绩时的那一刻。


现在回想那段岁月,就像一场酣畅淋漓的技术马拉松。也许我跑得太快了些,但那种纯粹的技术热情,那种为理想拼搏的激情,至今想来依然热血沸腾。因为在那个年纪,我们都相信,所有的汗水终将变成成长的养分。


这大概就是技术人的青春吧 —— 用代码书写理想,用汗水浇灌梦想。 即便现在我已经学会了"匀速",但那段全力奔跑的日子,永远是最珍贵的财富。


职场情商,如此重要


人生的至暗时刻,往往来得猝不及防。


那是一次跨部门的季度汇报会,两位GM和各部门主管齐聚一堂。作为技术骨干,我信心满满地展示着我们团队最新的技术方案。在Q&A环节,我敏锐地发现了其他部门方案中的几个技术漏洞。仗着自己过硬的技术实力,我开始逐条分析对方方案的不足——逻辑严密,有理有据


会议室里的气氛逐渐凝固。


对方 GM 的脸色越来越难看,我的直属领导在桌下不断地用脚轻踢我的小腿,但那一刻的我,依然沉浸在"伸张技术正义"的快感中。


散会后,办公室里弥漫着一种诡异的沉默。直到总监火急火燎地把我叫进会议室,我才如梦初醒。


"你知道你今天得罪了多少人吗? "总监的声音里带着无奈,"对方那个项目是集团重点项目,GM为此准备了整整三个月..."


我的心猛地沉了下去。


接下来的一周,我的提案被一再打回,跨部门协作遇到前所未有的阻力。曾经指尖飞舞写代码的自信,在一次次的"委婉拒绝"中土崩瓦解。


那段时间,我开始失眠。 每天凌晨在公司楼下的便利店坐到天明,看着来来往往的清洁工人,看着渐渐亮起的写字楼,像极了我那个支离破碎的职场梦。


直到有一天,我的+2 leader把我叫到咖啡厅。


"你知道为什么同样的话,有的人说出来会赢得掌声,有的人说出来却会树敌无数吗?"他说,"因为职场最重要的,从来都不是你说了什么,而是你怎么说。"


恍然间,我想起了自己在会议上咄咄逼人的样子,想起了那些被我用"技术正确"包裹的尖锐言词。原来在职场中,正确未必就是恰当,真理未必就要以最锋利的方式表达。


就像代码重构,有时候最优解未必是最适合的解。技术追求极致,但人心需要温度。所谓职场情商,不是要你放弃原则,而是学会用更柔和的方式坚持原则。


现在的我依然对技术保持着极致的追求,但学会了在表达时多一分善意,少一分锋芒。毕竟,职场如水,柔则善流。那些曾经的挫折与迷茫,终究化作了成长的养分。


从未经历过的大裁员


2021年的时候,谁能想到互联网行业的至暗时刻会来得如此突然?


一纸政策文件,让整个互联网教育行业轰然倒塌。那时的我,正写下《写在腾讯的第六年》,试图在这场巨变中寻找一些答案。回想起来,这是我职业生涯中第一次如此深刻地感受到商业世界的无常。


其实警钟早已敲响,只是我们这些"技术至上"的工程师选择性失聪。我就是最典型的例子:没有系统的财商教育,错过了大学时期的财经双学位,一心扑在代码世界里。对于经济周期、商业规律,我们往往像个睁眼瞎,直到风暴来临,才惊觉自己的无知与脆弱。


那个下午,我坐在显示器前,盯着那份需要填写的Excel表格。 光标在一个个熟悉的名字上跳动,每一次点击都像是一记重锤,击打在我的良知上。


这不是一份普通的表格,这是一份裁员名单。


表格里的每一行都是一个鲜活的生命,是一段难以割舍的情谊:



  • 小W刚在深圳买了房,月供还没开始第一期

  • 老Z的二胎前不久才满月

  • 小L昨天还在跟我讨论新项目的技术方案...


鼠标悬停在"保存"按钮上方,我的手指却迟迟无法按下。


最煎熬的是裁员沟通。那些平日里在代码世界里运筹帷幄的工程师们,此刻却像迷失的孩子。会议室里,有人失声痛哭,有人愤怒质问,更多的是令人窒息的沉默。


记得小M听到消息后,只是呆呆地坐在工位上,手中的星巴克早已凉透。 我机械地重复着准备好的话术,每一个字却都像是砂纸摩擦着喉咙,干涩得说不出口。


教育业务接连经历了数轮调整,每一次的裁员比例都令人心惊。曾经热闹的团队群渐渐沉寂,会议室里的笑声消失了,午餐时间的餐厅变得空荡。我常常站在工位的落地窗前,看着又一批同事抱着纸箱离开,心里五味杂陈。


记得副总裁在最后一次全员大会上说:"等春天来了,我们一定把这些优秀的同学接回来。"然而风暴太猛,连这位给我们希望的副总裁也离开了。那一刻,我才真正明白,互联网的黄金时代,或许真的已经落幕了。


在第二轮裁员后,现实给了我当头一棒。原本热闹的团队只剩下零星几人,但繁重的业务需求丝毫未减。 我一边准备简历面试,一边要维持团队运转,那段日子就像在钢丝上跳舞。


最终,我选择了加入QQ团队。这个决定既是无奈,也是机遇:他们正在进行的Electron重构升级项目,让我看到了继续追逐技术理想的可能;而团队里都是熟悉的战友,少了空降带来的适应期。这或许不是最完美的选择,但在当时的环境下,是最温暖的归宿。


七年之痒,职业倦怠


重返QQ团队的那一刻,我以为自己终于找到了职业生涯的新高地。


面对QQ桌面版这个历史包袱最重的项目,我们要在两年内完成看似不可能的Electron 重构 那段日子,我废寝忘食地研究VSCode 源码,在性能优化的迷宫中探索。每一个技术难关被攻克时,都让我热血沸腾。


2023年,ChatGPT的横空出世让整个科技圈地震。我敏锐地嗅到了时代的气息,立即投身这股AI浪潮。当我写下《花了大半个月,我终于逆向分析了Github Copilot》引发技术圈热议时,我觉得自己终于站在了技术浪潮的前沿。


同期主导的鸿蒙项目,更是让我在技术深度上有了质的飞跃。每天与华为工程师们的头脑风暴,从C++底层到系统架构的探讨,都在拓展着我的技术边界。


然而,就在我以为一切都在正轨上时,一种莫名的倦怠感悄然而至。


每天的日程表被无休止的会议填满:四个产运团队的例会、层层叠叠的排期评审、永远写不完的汇报PPT...时间碎片化得让人窒息。前端AI技术专项、D2C、OTeam的建设压在肩上,跨团队协作的压力像滚雪球一样越积越大。


更令人疲惫的是,在这个熟人遍布的环境里,期待越高,压力越大。 庞大的团队规模加上晋升通道的受限,让我看不到职业发展的新可能。一些管理上的内耗更是雪上加霜,让我身心俱疲。


负面情绪就像病毒一样开始扩散:



"这么拼命到底为了什么?" "不过是份工作而已,何必这么认真?" "要不要找个965的公司,给自己一个喘息的机会?"



公司降本增效的寒潮中,各种福利的缩减成了最后一根稻草。我看到周围的同事们渐渐被"躺平文化"感染,我也不能免俗。


但就在我几乎要放弃时,一个声音在内心响起:



"真的要这样认输吗?"



我开始反思:这些年的技术积累,难道就这样轻易放下? 我发现自己在商业认知上的短板,于是报名了产品课程,同时拜读《沸腾十五年》。


这本书像一记警钟,让我清醒地认识到:商业世界从不讲情面,机遇转瞬即逝。 程序员的高薪,不过是搭上了互联网红利的顺风车。而现在,ChatGPT的出现预示着新一轮技术革命的曙光。


站在这个时代的十字路口,我决定不再自怨自艾。下一个风口正在形成,而我,还有机会成为浪潮中的弄潮儿。


2023,前端已死


那天,我站在深圳的地铁站,看着来来往往的人群,第一次对自己的选择产生了动摇。 作为一名有着腾讯多年经验的前端技术leader,我本以为换工作不过是一个轻松的选择题。然而现实远比我想象的更为残酷。


"抱歉,你的期望薪资太高了。" 这已经是本月第三次听到这样的回复。我苦笑着放下手机,回想起三个月前信心满满地场景,那时的我,还沉浸在大厂光环带来的优越感中,以为凭借自己的技术积累和管理经验,找一份理想的工作不过是水到渠成的事。


现实给了我当头三棒:



  1. 金字塔效应:技术leader的位置,永远是一个萝卜一个坑。我引以为傲的经历,在这个市场里竟显得如此普通。就像一场无声的军备竞赛,每个候选人都带着光鲜的履历,而职位却少得可怜。

  2. 寒冬突袭:大环境不好,大量公司在收缩。中小公司直接要面到CTO,大公司也至少要到CTO-1。每一轮面试都像被放在显微镜下审视:业务、技术、管理,稍有不慎就会被淘汰。

  3. 残酷现实:最让人心寒的是,有CTO直言不讳: "招你就是来当'刽子手'的,帮我把现有团队改造成996。 "这样的话,让我第一次认真思考:技术人的价值,究竟在哪里?


而我回想了一下我自己的副业:连赚100块都难如登天。 这个经历让我明白,离开大厂的光环,也许我们都不过是普通人。那些曾经习以为常的高薪,或许只是平台赋予我们的虚幻泡沫。


在这个迷雾重重的职业十字路口,我最终选择了加入字节跳动,深耕AI领域。与其在前端寒冬中随波逐流,不如搭上AI这列可能改变未来的列车。


也许, "前端已死"并非终点,而是一个新起点。在技术的浪潮中,唯有不断进化,才能立于不败之地。


开悟之坡,终身学习


最近看到一张耐人寻味的图,据说出自王慧文之手。这张图生动描绘了人生的三个关键阶段:



在"巨婴阶段",我们不知道自己不知道。恃才傲物,以自我为中心,对世界的不公愤愤不平。 站在"愚昧之峰"上,以为自己已经看透一切。


然后是社会的毒打,让我们跌入"绝望之谷"。在这里,我们终于知道了自己的无知。自信崩塌,万念俱灰,有人选择躺平,有人则在黑暗中摸索前行。


而能够攀上"开悟之坡"的人,都经历过重重考验。就像马化腾在3Q大战后的蜕变,任正非在遭遇背叛后对经营之道的顿悟。这些挫折,最终都化作了通向智慧之路的基石。


经历过风雨,我的人生信条也从"天行健,君子以自强不息"的进取,沉淀为"宠辱不惊,看庭前花开花落"的从容。就像赵英俊生前所言:"除了生死,再无大事。"这份豁达,是经历过生命起落后的珍贵礼物。


在这条修心之路上,我学会了:



  • 在逆境中保持乐观,在顺境时保持警醒

  • 身处黑暗,心向光明

  • 对生活怀抱感恩,珍惜当下的一切

  • 学会利他,不再斤斤计较

  • 追求共赢,理解组织与个人的共生关系


站在人生的这个节点,我深知未来依然充满不确定。但我不再惶恐,因为我已经习惯了在学习中寻找答案,在成长中遇见更好的自己。 「认知」在人生中极为关键,这也是我起名公众号的原因。


这条开悟之路上,我还在前行。或许前方还有更多未知的挑战,但我已经学会了在风雨中保持内心的平静。


人生的每一步,都是通向成长的必经之路。与君共勉!




作者:孟健的AI编程认知
来源:juejin.cn/post/7451168270877704218
收起阅读 »

禁止调试,阻止浏览器F12开发者工具

web
写在前面 这两天突然想看看文心一言的http通信请求接口,于是想着用F12看看。 谁知道刚打开开发者工具,居然被动debugger了。 直接被JS写死的debugger关键字下了断点。行吧,不让调试就不让调试吧,关闭开发者工具之后,直接跳到了空白页。 其...
继续阅读 »

写在前面


这两天突然想看看文心一言的http通信请求接口,于是想着用F12看看。



谁知道刚打开开发者工具,居然被动debugger了。



直接被JS写死的debugger关键字下了断点。行吧,不让调试就不让调试吧,关闭开发者工具之后,直接跳到了空白页。



其实几年之前就碰到过类似的情况,不过当时才学疏浅,也没当回事,就没研究过。这次又碰到了,毕竟已经不是当年的我了,于是便来研究研究。


分析


大家都知道浏览器的开发者工具能干啥,正经的用法:开发时调试代码逻辑,修改布局样式;不正经的用法:改改元素骗骗人,找找网站接口写爬虫,逆向js破解加密等等,所以说前端不安全,永远不要相信用户的输入。


而这次碰到的这个情况确实可以在用户端做一些防御操作,但是也可以绕过。 (PS:感谢评论区大佬指教:开发者工具Ctrl+F8可以禁用断点调试,学到了)


先做一波分析。


首先,防止你用F12调试,先用debugger关键字阻止你进行任何操作。随后,在你关闭之后,又直接跳转到空白页,不让你接着操作。


这就需要一个开发者工具检测的机制了,发现你打开了开发者工具,就给你跳走到空白页。


所以,关键就是要实现开发者工具的检测。


实现


经过查阅一番,发现原来这个debugger可能并不仅仅是阻止你进行调试的功能,同时还兼具判断开发者工具是否打开的作用。怎么实现?


debugger本身只是调试,阻止你继续对前端进行调试,但是代码中并不知道用户是否打开了开发者工具,所以就无法进行更进一步的操作,例如文心一言的跳转到空白页。


但是,有一点,你打开开发者工具之后,debugger下了断点,程序就停到那里了,如果你不打开开发者工具,程序是不会停止到断点的。没错,这就是我们可以判断的方式,时间间隔。正常情况下debugger前后的时间间隔可以忽略不计。但是,当你打开开发者工具之后,这个时间间隔就产生了,判断这个时间间隔,就可以知道是否打开了开发者工具。


直接上示例代码


<!DOCTYPE html>
<html>
<header>
<title>test</title>
</header>
<body>
<h1>test</h1>
</body>
<script>
setInterval(function() {

var startTime = performance.now();
// 设置断点
debugger;
var endTime = performance.now();
// 设置一个阈值,例如100毫秒
if (endTime - startTime > 100) {
window.location.href = 'about:blank';
}

}, 100);

</script>

</html>

通过设置一个定时循环任务来进行检测。


在不打开发者工具的情况下,debugger是不会执行将页面卡住,而恰恰是利用debugger的这一点,如果你打开开发者工具一定会被debugger卡住,那么上下文时间间隔就会增加,在对时间间隔进行判断,就能巧妙的知道绝对开了开发者工具,随后直接跳转到空白页,一气呵成。


测试



现在来进行测试,打开F12


关闭开发者工具。



完美!


写在后面


这样确实可以阻挡住通过在开发者工具上获取信息,但是仅仅是浏览器场景。我想要拿到对话的api接口也不是只有这一种方法。


感谢评论区大佬指教:开发者工具Ctrl+F8可以禁用断点调试


或者说,开个代理抓包不好吗?hhh



作者:银空飞羽
来源:juejin.cn/post/7337188759055663119
收起阅读 »

Cesium从入门到入坟

web
大扎好,我系渣渣辉,斯一扔,介四里没有挽过的船新版本,挤需体验三番钟,里造会干我一样,爱象节款js裤 Cesium 概述 Cesium是国外一个基于JavaScript编写的使用WebGL的地图引擎,支持3D,2D,2.5D形式的地图展示,也是目前最流行的三...
继续阅读 »

大扎好,我系渣渣辉,斯一扔,介四里没有挽过的船新版本,挤需体验三番钟,里造会干我一样,爱象节款js裤



Cesium 概述


Cesium是国外一个基于JavaScript编写的使用WebGL的地图引擎,支持3D,2D,2.5D形式的地图展示,也是目前最流行的三维数字地球渲染引擎。


Cesium 基础介绍


首先我们需要登录上Cesium的官网,网址是 cesium.com/ ,获取源代码可以在Platform菜单项的Downloads中下载 。
接下来,第一个比较重要的事情就是我们需要注册一个免费账户以获取Cesium世界地形资产所需的访问令牌,而这个账户的token决定了哪些资产咱们可以使用;而第二个比较重要的事情就是Cesium的文档中心( cesium.com/learn/cesiu… ),我们在实际使用的过程中会经常来查阅这些API。


Cesium 的使用


由于我是使用的vue-cli生成的项目,所以直接安装vite-plugin-cesium依赖项,当然你也可以使用直接下载源码,在HTML中引入的方式。如果使用的是vite-plugin-cesium,你还需要在vite.config.ts中添加一下Cesium的引用。


import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import VueDevTools from 'vite-plugin-vue-devtools'
// add this line
import cesium from 'vite-plugin-cesium';

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
VueDevTools(),
// add this line
cesium()
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

初始化地球


<script setup lang="ts">
import { onMounted } from 'vue'
import * as Cesium from 'cesium'
onMounted(() => {
const defaultToken = 'your access token'
Cesium.Ion.defaultAccessToken = defaultToken
const viewer = new Cesium.Viewer('cesiumContainer', {
//这里是配置项
})
})
</script>

<template>
<div id="cesiumContainer" class="cesium-container"></div>
</template>


<style scoped>
#cesiumContainer {
width: 100vw;
height: 100vh;
}
</style>


效果如下:


1.gif


现在我们就可以看到Cesium生成的地球了,可以对其进行二维和三维状态的切换,也可以用其自带的播放器,对时间轴进行一个播放,支持正放和倒放,Cesium还自带了搜索地理位置组件,并且兼容了中文。


Cesium 常用的类


1. Viewer


它是Cesium展示三维要素内容的主要窗口,不仅仅包含了三维地球的视窗,还包含了一些基础控件,在定义Viewer对象的时候需要设定基础部件、图层等的初始化状态,下面演示一下部分属性的使用。


  const viewer = new Cesium.Viewer('cesiumContainer', {
// 这里是配置项
// 动画播放控件
animation: false,
// 时间轴控件
timeline: false,
// 全屏按钮
fullscreenButton: true,
// 搜索位置按钮
geocoder: true,
// 帮助按钮
navigationHelpButton: false,
// VR按钮
vrButton: true
})

除了上述的控件属性之外,还有entities这种实体合集属性,主要用于加载实体模型,几何图形并对其进行样式设置,动效修改等,我们可以通过下述代码生成一个绿色的圆点。


const entity = viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 400),
point: {
pixelSize: 100,
color: new Cesium.Color(0, 1, 0, 1)
}
})
viewer.trackedEntity = entity

效果如下:
image.png


当然,我们也可以用entities来加载模型文件,下面我们用飞机模型试试


  /** 通过entities加载一个飞机模型 */
const orientation = Cesium.Transforms.headingPitchRollQuaternion(
position,
new Cesium.HeadingPitchRoll(-90, 0, 0)
)
const entity = viewer.entities.add({
position: position,
orientation: orientation,
model: {
uri: '/Cesium_Air.glb',
minimumPixelSize: 100,
maximumScale: 10000,
show: true
}
})
viewer.trackedEntity = entity

效果如下:


3.jpg


2. Camera


Cesium中可以通过相机来描述和操作场景的视角,而通过相机Camera操作场景的视角还有下面的几种方法



  • 飞行fly,比如flyTo,flyHome,flyToBoundingSphere

  • 缩放zoom,比如zoomIn,zoomOut

  • 移动move,比如moveBackward,moveDown,moveForward,moveLeft,moveRight,moveUp

  • 视角look,比如lookDown,lookLeft,lookRight,lookUp

  • 扭转twist,比如twistLeft,twistRight

  • 旋转rotate,比如rotateDown,rotateLeft,rotateRight,rotateUp

  • 其他方法,比如setView,lookAt


viewer.scene.camera.setView({
// 设定相机的目的地
destination: position,
// 设定相机视口的方向
orientation: {
// 控制视口方向的水平旋转,即沿着Y轴旋转
heading: Cesium.Math.toRadians(0),
// 控制视口方向的上下旋转,即沿着X轴旋转
pitch: Cesium.Math.toRadians(-20),
// 控制视口的翻转角度,即沿着Z轴旋转
roll: 0
}
})

我们尝试使用setView后可以发现,相机视角直接被定位到了下图的位置


1.jpg


3. DataSourceCollection


DataSourceCollection是Cesium中加载矢量数据的主要方式之一,它最大的特点是支持加载矢量数据集和外部文件的调用,主要有三种调用方法,分别为 CzmlDataSourceKmlDataSourceGeoJsonDataSource,分别对应加载Czml,Kml,GeoJSON格式的数据,在使用过程中我们只需要将矢量数据转换为以上任意一种格式就可以在Cesium中实现矢量数据的加载和存取。


  viewer.dataSources.add(Cesium.GeoJsonDataSource.load('/ne_10m_us_states.topojson'))

效果如下:
2.jpg
这时候我们看到图层已经被加载上去了~


Cesium的坐标体系


通过上面的示例我们可以得知Cesium具有真实地理坐标的三维球体,但是用户是通过二维屏幕与Cesium进行操作的,假设我们需要将一个三维模型绘制到三维球体上,我们就需要再地理坐标和屏幕坐标之间做转换,而这就需要涉及到Cesium的坐标体系。


Cesium主要有5种坐标系:



  • WGS84经纬度坐标系

  • WGS84弧度坐标系

  • 笛卡尔空间直角坐标系

  • 平面坐标系

  • 4D笛卡尔坐标系


他们的基础概念大家感兴趣的可以百度查阅一下,我也说不太清楚,问我他们的区别我也只能用 恰特鸡屁踢 敷衍你,下面我们演示一下怎么将WGS84左边西转换为笛卡尔空间直角坐标系:


const cartesian3 = Cesium.Cartesian3.fromDegrees(longitude, latitude, height)

我们可以通过经纬度进行转换,当然我们还有其他的方式,比如Cesium.Cartesian3.fromDegreesArray(coordinates),这里的coordinates格式为不带高度的数组。


Cesium加载地图和地形


加载地图


我们使用ArcGis地图服务来加载新地图,Cesium也给其提供了相关的加载方法:


  const esri = await Cesium.ArcGisMapServerImageryProvider.fromUrl(
'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer'
)
/** 这里是配置项 */
const viewer = new Cesium.Viewer('cesiumContainer', {
baseLayerPicker: false,
})
// 加载ArcGis地图
viewer.imageryLayers.addImageryProvider(esri)

效果如下:


4.jpg


我们再来看一下之前的地球效果来对比对比:


5.jpg


可以明显看出来ArcGisMapServer提供的地图更加的清晰和立体。



注:加载ArcGis地图服务请使用我上述提供的代码,从Cesium中文网看到的示例代码可能很久没更新了,使用会报错~



当然我们还可以加载一些特定场景的地图,比如夜晚的地球,官网上直接给出了示例代码:


// addImageryProvider方法用于添加一个新的图层
viewer.imageryLayers.addImageryProvider(await Cesium.IonImageryProvider.fromAssetId(3812))

效果如下:


6.jpg


加载地形


我们回到刚刚的ArcGis地图,我们进入到地球内部查看一些山脉,会发现从俯视角度来看山脉是有轮廓的,但是当我们旋转相机后会发现,实际上地球表面是平的,并没有显示出地形,效果如下:


7.jpg


这时候我们就需要加载出地形数据了


const esri = await Cesium.ArcGisMapServerImageryProvider.fromUrl(
'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer'
)
/** 这里是配置项 */
const viewer = new Cesium.Viewer('cesiumContainer', {
baseLayerPicker: false,
terrainProvider: await Cesium.CesiumTerrainProvider.fromIonAssetId(1, {
// 可以增加法线,用于提高光照效果
requestVertexNormals: true,
// 可以增加水面特效
requestWaterMask: true
})
})
// 加载ArcGis地图
viewer.imageryLayers.addImageryProvider(esri)

效果如下:


2.gif


8.jpg


可以看到原先的平面通过加载了地形数据,已经有了山势起伏,河流湖泊~


Cesium加载建筑体


我们在实际开发中,比如搭建一个智慧城市,光有地图和地形是远远不够的,还需要加载城市中的建筑模型信息,这时候我们就需要用到Cesium中建筑体的添加和使用的相关功能了,我们以官网的纽约市的模型数据为例:


  /** 添加建筑物 */
const tileset = viewer.scene.primitives.add(await Cesium.Cesium3DTileset.fromIonAssetId(75343))
/** 添加相机信息 */
const position = Cesium.Cartesian3.fromDegrees(-74.006, 40.7128, 100)
viewer.camera.setView({
destination: position,
orientation: {
heading: 0,
pitch: 0,
roll: 0.0
}
})

效果如下:


9.jpg


我们看到纽约市建筑物的数据已经加载出来了,但是看起来都是白白的过于单调,我们还可以通过style来修改建筑物的样式


tileset.style = new Cesium.Cesium3DTileStyle({
color: {
conditions: [
['${Height} >= 300', 'rgba(45,0,75,0.5)'],
['${Height} >= 100', 'rgb(170,162,204)'],
['${Height} >= 50', 'rgb(102,71,151)'],
['true', 'rgb(127,59,8)']
]
},
show: '${Height} > 0',
meta: {
description: '"Building id ${id} has height ${Height}."'
}
})

现在我们再来看一下效果:


10.jpg


可以看出我们根据建筑物的不同高度,设定了不同的颜色,比如超过300米的建筑就带有透明效果了,比较上图的效果更有层次感。


最后


关于Cesium我也是初窥门径,具体的学习和使用大家还是要以 英文官网 为准,中文网上很多都过时了,使用的时候可能会报错,我已经帮大家踩好坑了😭,也欢迎大家在评论区里多沟通交流,互相学习~


WechatIMG24.jpg


作者:魔术师Grace
来源:juejin.cn/post/7392558409742925874
收起阅读 »

全栈工程师的自媒体&副业之路:从零到月入破千的经验分享

引言 大家好,我叫立东,一名专注于Java和Vue3技术栈的全栈工程师。在过去的几年里,我不仅在本职工作中积累了丰富的经验,还通过自媒体平台进行知识分享,并成功实现了副业变现。今天,我想分享一下我的这段经历,希望能给同样对自媒体感兴趣的朋友们一些启发和帮助。 ...
继续阅读 »

引言


大家好,我叫立东,一名专注于Java和Vue3技术栈的全栈工程师。在过去的几年里,我不仅在本职工作中积累了丰富的经验,还通过自媒体平台进行知识分享,并成功实现了副业变现。今天,我想分享一下我的这段经历,希望能给同样对自媒体感兴趣的朋友们一些启发和帮助。


1. 起步阶段


第一次尝试


2020年5月2日,我在掘金平台上发布了我的第一篇文章。当时的初衷很简单,就是想记录自己的学习过程和技术心得。刚开始时,确实遇到了不少挑战,比如如何找到合适的选题、如何写出有吸引力的文章等。为了克服这些困难,我采取了以下几个步骤:



  • 选题策略:我倾向于选择自己熟悉且受众感兴趣的话题,例如前后端分离、快速开发框架、Java、Vue、Docker、Kubernetes(K8s)以及工作流引擎等。这样不仅能够确保内容的专业性和准确性,还能更好地吸引和满足读者的需求。

  • 写作技巧:其实我并没有特别参考他人的写作习惯,更多的是随心所欲地记录自己的想法和心得。因此,我的文笔可能并不完美,但我会尽量用最真诚和直接的方式表达自己。

  • 互动反馈:积极回复读者的评论,收集反馈并不断改进。


坚持与成长


坚持了一年左右,我逐渐积累了一些宝贵的经验和粉丝。截至目前,我在掘金平台上的粉丝数虽然不算多,但也达到了700多人,发布了70多篇文章,总阅读量超过了27.2万次。尽管初期增长较为缓慢,但每一次点赞和评论都给了我极大的鼓励和支持。


2. 视频录制


转战B站


2021年8月1日,我决定将内容扩展到视频领域,并在B站上传了我的第一个视频。选择视频形式的原因主要是因为它能够更直观地展示技术细节,同时也能更好地与观众互动。从那时起,我基本上保持每天录制更新一个视频,坚持了一年半的时间。


我的视频主要从架构、提效以及研发管理者的角度进行录制。这类视频在市场上相对较少,因为大多数讲师录制的内容通常集中在实战项目或某个开发框架的使用上。然而,每家公司中能够并且需要搭建这类开发框架和开发脚手架的人并不多。因此,我的视频填补了这一空白,为那些希望提升整体开发效率和管理水平的技术人员提供了宝贵的知识资源。


最初,我的视频以合集的形式连更,每个课时大约四五十分钟。随着时间的推移,我逐渐将视频长度控制在十分钟左右,这样不仅更符合观众的观看习惯,也让我越来越适应这个录制节奏。录制的内容涵盖了前端、后端以及持续集成等多个方面,其中比较有代表性的系列是《SpringBoot+Vue3前后端分离工程化最佳实践(后端篇)》和《SpringBoot+Vue3前后端分离工程化最佳实践(前端篇)》,这些视频与我在掘金上发布的《打造一款适合自己的快速开发框架》系列文章相呼应。


截至目前,这两个合集的总课时数达到了407个,总播放量接近10万次。通过这些视频,我不仅能够更全面地分享我的技术知识,还获得了许多宝贵的反馈和支持。


粉丝增长


2023年3月8日,我的B站粉丝终于突破了1000人。这对我来说是一个重要的里程碑,也给了我继续前进的动力。事实上,我并未采取任何刻意的推广策略来迅速增加粉丝数量或提升他们的活跃度,也未曾深入思索过如何将这份影响力转化为经济收益。我始终秉持着一份纯粹的热爱与坚持,默默耕耘。当然可能是整个过程并未占用我过多的时间,录制视频于我而言,更像是一种轻松愉悦的表达方式,而非沉重的负担,所以才能坚持那么久的用爱发电。


3. 知识付费平台


我知道,一直用爱发电是很难坚持的,所以我开始尝试做突破……


知识星球


对我而言已略显陌生,因为它是我在很久以前就使用知识付费平台的,但遗憾的是,后续并未给予太多的运营关注。主要的原因在于,其配备的文章编辑器功能相对简陋,许多文章的编辑工作变得相当不便,这大大影响了我的使用体验与效率。因此,随着时间的推移,我逐渐减少了对它的管理与投入。


纷传


2023年5月,一个偶然的机会,看到了纷传这个知识付费平台,经过初步试用,我发现它基本上能够满足我对于文章创作与付费阅读功能的需求。然后在纷传创建了一个《立东和他的朋友们》的圈子,在这个圈子上,我不仅分享了一系列关于工作流引擎的专业文章,还提供了与我当前录制的视频课程紧密相关的文档链接及访问密钥。这一方式不仅丰富了我的内容创作生态,还意外地成为了我的一项副业收入来源,为我的月收入增添了亮丽的一笔。


4. 开源项目与爆发


开源项目


2023年12月16日,我上传了一个开源项目《快速开发框架+自研工作流管理系统》。这个项目旨在帮助开发者快速构建企业级应用。为了让更多人了解这个项目,我在掘金上发布了一篇《开源啦!!!轻量级工作流引擎管理系统》的文章,详细介绍了它的功能和使用方法。没想到,这篇文章被【稀土掘金技术社区】官方公众号和几个大V的公众号相继转载。也正因为如此,我开源项目关注量也稳定增长着,现在已经有8.3k的关注量。


5. 付费课程


入驻B站课堂


2023年11月15日,我正式入驻B站课堂,并于12月16日推出了我的第一门付费课程《全栈工程师带你从0~1设计实现轻量级工作流引擎》。这门课程受到了很多学员的好评,也为我带来了不错的收益。


后续课程


随后,我又陆续推出了几门付费课程,涵盖了多个主题:



  • 2023年12月29日,《AI神器!阿里通义灵码从零带你开发前端代码生成器》。

  • 2024年1月10日,《毕业设计!家庭记账管理系统!快速开发框架!》。

  • 2024年5月4日,《AI助手开源引流VuePress站点动态权限控制》。

  • 2024年6月25日,《工作流设计器开发最佳实践》。

  • 2024年8月3日,《从0~1搭建前端快速开发框架VbenAdmin篇》(未完结)。


我的付费课程远未止步,未来还有更多课程等待我去录制与打磨。在明确的变现路径上,我将秉持初心,持续输出高质量的教学内容,为每一位求知者点亮前行的灯塔。


6. 变现成果


随着课程数量的增加,我的收益也在稳步增长。2024年8月份,我的课程收益首次突破了千元。这不仅是对我努力的认可,也证明了通过自媒体变现是完全可行的。


7. 经验总结与建议


坚持的重要性


持续输出内容,逐步积累粉丝。无论是文章还是视频,都需要持之以恒的努力。只有不断地提供有价值的内容,才能赢得用户的信任和支持。


内容质量


高质量的内容是吸引粉丝和变现的关键。无论是文章还是视频,都要确保内容的专业性和实用性。此外,清晰的表达和良好的视觉效果也是提升用户体验的重要因素。


社区互动


积极与粉丝互动,建立良好的社区氛围。通过回复评论、举办问答活动等方式,增强粉丝的参与感和归属感。这样不仅能提高粉丝的黏性,还能获得宝贵的反馈和建议。


多样化变现


通过多种方式变现,如付费课程、付费圈子等。不同的变现方式可以覆盖不同类型的用户需求,从而最大化收益。例如,付费圈子可以为用户提供更深入的技术支持和交流机会,而付费课程则可以系统地传授知识和技能。


兴趣很重要


兴趣真的很重要,因为兴趣大概率是处在自己的舒适圈,而舒适圈内,你可以坚持得更久,才有机会等到变现的到来。


多平台布局的反思


虽然我在B站上取得了一些成绩,但我也有些后悔没有同时在其他平台(如抖音)上进行内容发布。现在,我的抖音粉丝还没有达到1000人,导致一些功能无法使用。未来,我会考虑在多个平台上同步发布内容,以扩大影响力。同时,也会更加注重多平台的运营策略,以便更好地触达不同的用户群体。


结语


感谢大家一直以来的支持和陪伴。未来,我会继续努力,为大家带来更多有价值的内容。如果你有任何问题或建议,欢迎随时联系我。


附录



作者:mldong
来源:juejin.cn/post/7423066953039085606
收起阅读 »

2024年总结: 迷茫

12月今年最后一个月了,相逢的人已走散, Q4的OKR已经定型了, 很平淡无味, 闲的无聊 提前写个年终总结吧。00年, 再过一个月就25岁了,一个人来杭州也已经3年多了 每天有时间写一点 周六了 写到凌晨1点了 看直播/打麻将到凌晨5点才睡。 去年也写了一篇...
继续阅读 »

12月今年最后一个月了,相逢的人已走散, Q4的OKR已经定型了, 很平淡无味, 闲的无聊 提前写个年终总结吧。00年, 再过一个月就25岁了,一个人来杭州也已经3年多了 每天有时间写一点 周六了 写到凌晨1点了 看直播/打麻将到凌晨5点才睡。 去年也写了一篇 2023年总结:日渐清醒,得失随意 //TODO DDL 应该是在月中完结吧。



工作



我大概回忆一下 我今年在工作上应该干了这些事情吧




  • 自己申请换项目组,日常维护新品App版本迭代 2周一个版本 多个app同时进行

  • 完成所有App 苹果服务器接口Storekit2 升级上线

  • Google 支付/订阅 SDK 重构原生API调用代码

  • RocketMQ优化多数据中心用户数据同步/webhook/推送

  • RocketMQ-Exporter 搭建 监控相关性能指标

  • SSE+服务器GRPC流式 推送消息

  • us机器 内存优化 切换到jemalloc内存分配器

  • RocketMQ 单机 升级为Dledger 集群模式 Q4 任务



从22年3月毕业到现在 再度过一个季度 在这家公司呆三年了(3年之约),整个过程就是升级打怪,看着人来人往 合作的人 离职一个又来一个, 每个季度干着重复的工作,技术框架还是那一套 SpringBoot+GRPC 经过一年熟悉后 就觉得没有新鲜感了, C端产品App 在用户基数不大情况下 基本的重心在客户端的ui/操作体验上 后端嘛就是一个数据存储的地方 存取能有什么难度 大家都会,每个季度任务就是 基本的版本迭代+一些服务器内部的优化,如果你想要拿到高的绩效 那干这点是远远不够的, 基本规则在无告警的版本迭代下 做一些对团队贡献大 有价值的事情 拿A/季度优秀员工,三年期间升了两次小级别 p5-1->p5-2->p5-3 还是一个初级开发 今年估计也悬了 没有两次A的绩效 跨段位升没机会,没有owner过项目, 和旁边人朋友/同学工作/升职对比下 只能说自己像个废物 躺的太平了 每天965的生活 除了偶尔上线 需要加班留下来 大家都不加班 也没有那么多活需要加班来干的活。




生活



去年立的flag 也是一腔鸡血




  • 软考 系统架构师 +软著 拿到杭州E类人才

  • 健身/减肥

  • 骑车 vlog (杭州景点全部骑完 影石360 ace pro)

  • 摄影佳能rp/视频剪辑学习

  • 日语/英语学习

  • leetcode 上Knight

  • 考D驾-照 骑防赛

  • 日麻线下雀庄体验 参加各种线上日麻比赛

  • ClickHouse hangzhou 线下沙龙

  • 掘金bolg 更新 技术日常

  • 千岛湖

  • 抽烟+喝酒

  • B站 直播 日麻


软考 系统架构师+软著 拿到杭州E类人才



img


骑车 vlog (杭州景点全部骑完 影石360 ace pro)



  • 一个人走走停停 骑过很多地方, 最多的还是钱塘江到彭浦大桥->复兴大桥路线 不知道骑了多少遍,西湖/湘湖/九溪这些地方都去过了,车子是青春款只在线上售卖 后期毛病很多 链条蹭盘/刹车无效 自己不知道维修了多少次,最近这两个月很少骑了,放在地下室发霉 后续准备卖掉了这车 还买了那么多骑行装备,买的insta 360 ace pro 3000多降价到2k左右 当时在大疆和insta中选择了好久 最后还是踩坑了 实体店体验了大疆action 画质比insta360好太多了,有必要考虑再买一台大疆action5了, 一个人的骑行之路也该结束了 开始新的玩具 仿赛摩托车 芜湖起飞。


img
img
img
img
image.png

健身/减肥



  • 怎么说呢 三天打鱼两天晒网的行动 体脂没什么变化,饮食更不会控制 每天外卖外卖外卖, Q1/Q2两个季度挺积极的 基本工作日晚上有时间就去健身房 周末白天也去, 在健身房的时间也能让自身感受到轻松, 这个小区有个百姓健身房在地下室 24小时 刷脸进去 设备齐全 没什么人 每个季度的话300块RMB, 我主要后面可能没有看到短期效果+活着很累 有一段时间没有去了, 偶尔下楼抽烟去逛逛, 最后得到的只是自己的一个心里安慰,没有合理的计划和坚持下去的心 我现在已经懒连手表都不戴了。


img
img
img
img

摄影佳能rp/视频剪辑学习



  • 怎么说呢 周末放假就是宅 已经吃灰了 除了9月1号 拿到免费的门票 杭州植物园专门去了一趟拍彼岸花,其他时间不出手。视频剪辑也是一坨屎 目前就用剪映弄一些雀魂麻将抽角色的视频,后续还是想学一下专业的剪辑工具 这个也看需求吧。


img

考D驾-照 骑防赛



  • 为什么要去骑摩托车,主要是中秋节回家一趟,隔壁邻居已经买了一辆机车, 当时他让我试试 我没试 后面就一直关注摩托车这个事 抖音一直给我推视频。才有了考D驾-照驾-照,周末练半天,工作日考试半天就好了,4科联考 比C1驾-照周期短 速度快,驾-照到手后面周末直接找附近最近的租车平台试试水,萧山那边的之江路/美女坝路险,本田500 手震麻了 1个小时 干了100公里, 最后一个半小时就还车了 跑了150km,整体的体验感是非常好的,无论是去骑车的路上 还是过程中 都能够忘记生活/工作上的烦心事,最尴尬的是红绿灯起步熄火了,后续周末继续出行租车,找个有缘人一起。


img
img
img

日麻线上比赛/线下雀庄体验



  • 每天下班就是点外卖 开始打麻将,水各种日麻群 打友人赛/团队赛,每天晚上达到2 3点 菜就爱多玩 和群友打比赛 对个人的实力也是有了认知 学习别人的打法 从野猪冲击 也慢慢在意铳率了,前一天打完线上比赛, 这个月周日也是马上跟着一个大哥去杭州线下的湖滨牌浪屋体验完线下日麻,今年干的最多的事情就是打麻将,下班除了打麻将还是打麻将。


img
img
img
img

leetcode 上Knight



  • 基本上是原地没动,比赛一场没打,为什么要刷?为了什么?能带给自己什么收益?呆在舒适圈里久了 不想出去,算法也是提不上一点兴趣了 估计只有到时候找工作之前才会接触到了 其实也制定了计划 刷灵神的清单 还是自己懒吧 动不起来,最后一个月 要不开始发力?算了 打麻将吧。


img

其他



  • 今年参加了ClickHouse hangzhou 线下沙龙, 虽然没有使用过clickhouse这款db,去听听别人公司的落地方案,去阿里园区转了转。

  • 掘金bolg 更新 技术日常 主要是参加创作者训练营吧 锻炼一点自己的文本输出能力,总结的过程中也能知道问题的本质是什么,解决的过程/方式以及别人是怎么解决的,收获还是有的。

  • 和同学五一去了千岛湖一趟 结局不是很好 过程体验不错。

  • 在日语/英语学习上面投入的时间 ,无论是日常工作上英语的使用 还是各种文档阅读能力,在逛各种项目/看论文的时候 就能体现出来, 日语兴趣的话 纯粹是打日麻和旁边的日麻群友影响/看番剧而来的 每天用多邻国完成任务,买了4本书《标准日语》+《大家的日语》,在B站上看圆圆姐的视频教程【京大博士带你学日语】新标日初级上册全新课程!必能学会!超详细讲解!轻松搞定日语学习!(课本内容完结!)哔哩哔哩bilibili

  • 抽烟+喝酒已经是家常便饭一样的事情了 上半年是沉迷于喝酒消愁 下半年就抽烟打发时间,每天下班又不知道干什么 找点打发时间的乐趣,天天熬夜看直播 打麻将 2点3点睡觉已经是常态了,每天晚上看陈伯/刘刘江直播 带来的乐趣, 工作日每天基本8点50的闹钟吵醒,拖着尸体去上班,周末基本睡到自然醒中午/下午 除了楼上楼下装修 直接被震醒了。

  • 这样一回想2024年还是干了很多无意义的事。



虽然只有15篇文章 文章的阅读数也有3w 其实数据对我来说也是无所谓的,主要还是方便以后回忆吧,分享出去 可能有人和你遇到相同问题,带给解决思路 明年要不要继续写?还是把时间投入在别的地方?都是未知



img
img
img

个人技术学习



  • AI 知识点拓展学习

  • 部门分享

  • 推荐系统&&RAG

  • 前端

  • 第三方支付订阅

  • 分布式论文学习总结

  • 《计算机网络-自顶向下方法第七版》

  • 《CSAPP 深入理解计算机系统(第3版)》

  • 《设计数据密集型应用》

  • 技术拓展/深挖(RocketMq源码/go-redis源码/Netty源码/Mycat2源码)



看个锤子 没心思学习 下半年天天打麻将




  • AI的话主要是身边环境影响,自己的项目组一直在利用AI做业务,从2023年开始 公司一直对接的是openai 提供的chat 能力,公司内部举行了ai相关的比赛,业务想要搭建自己的知识库和RAG搜索 主要是用AWS上的Redrock封装好的知识库 ,项目组一些APP一直在使用微软的TTS进行语言转音频的操作,部门,组内和项目组 大家一直在内部分享和ai相关的知识点,产品会使用cursor提前将需求写完 自己进部署上线。

  • 跟着项目组业务走,最近在支付方面的功能进行了改动,对于web网页上的购买消耗型商品/续费型商品的购买,主要对接的平台是Stripe信用卡visa支付 和paypal支付。appstore 支付的话 最近负责组内storekit2 服务器接口升级重构代码 用的官方开源库 github.com/apple/app-s…。Alipay 支付宝和Wechat 对于中国环境的用户提供的一种支付方式, 代码很粗糙 很久没有相关需求迭代了。Google 支付 来的两年时间接触的业务还是比较少,整个支付逻辑和appstore是一致的,有时间把代码逻辑和官方文档进行学习总结一下。

  • 现在只会个后端远远不够了,替代性太强了,除非是中大厂那样细分工作岗位/业务内容,如果你有自己的想法 后续做一些自己的产品/独立开发者 一人一公司 全栈只能是无敌路。我这边对前端也是零零散散的学习 没有整个的大项目使用,github.com/lobehub/lob… 前端React开源项目学习 TSX+TS 认知冲击 原来前端已经进化到这个地步了,没有html+css全部被封装了,我们内部的数据平台还是原生html+django搭建的,每次加新功能ai生成的代码 能跑就行。



在下半年 觉得基础知识很重要 技术跟着业务走 没必要太追求新技术 就往计算机基础知识+算法+基础论文投入时间




  • 中间一段也是将《计算机网络-自顶向下方法第七版》 计算机网络-自顶向下方法第七版 · 语雀和《CSAPP 深入理解计算机系统(第3版)》CSAPP 深入理解计算机系统(第3版) · 语雀 细看了一遍 , 书籍买了很多 都是吃灰的 没有去年那个干劲了。

  • 看论文 可以学习技术理论的基础 还有重要一点是学英语, 主要是看一个up在学习这方面的知识点 就跟着看了一段时间,谷歌三大剑客 GFS/MapReduce/BigTable 看论文不看分布式论文 就像四大名著不读红楼梦,唐诗不读李杜,吃泡面不加调料包 Raft/Paxos 那一块真的一看就是几天 深陷进去 每次看硬核课堂的文章 Raft -论文导读 与 ETCD源码解读

  • 参与开源项目任务也没有达成 往里面投入的时间太少了,最后下班/上班有时间 也没深入去学习业务上用到的组件源码, 最近的话 负责RocketMQ集群Dledger搭建/MQ优化业务 ,RocketMQ-exporter+herzbeat+prometheus监控指标, 遇到的异常信息太多 每次都是网上找案例解决,上班利用ai 深入在看看RocketMQ源码吧github.com/apache/rock… 边看边总结RocketMQ 源码 5.2.0 - 生产者 Producer



杂学杂记




  • 中间又去了解了下机器学习/深度学习相关的内容,又看了大数据开发Spark/Flink等等组件,前端看了React+TS相关知识点/demo/Flutter开源项目, 背单词的时候发现墨墨背单词是node+ts写的,有个软考刷题的app是Flutter写的 作者是独立开发者 最近公司客户端也在用flutter开发新品app 代码我这边也是有权限的, 也去了解了一下技术栈,中间又有一段时间去了解了下亚马逊运营的工作,也看了AIGC/agent/图像/音频/向量数据库milvus等相关的方向 RAG 知识库增强式搜索,在推荐系统领域 推荐 广告 搜索 也花了一段时间去了解/学习 因为我们这边没有算法工程师 推荐功能很粗糙 没有用户画像的概念,有一段时间被cloudwego社区的kitex/hertz吸引 当时想去上海站的线下沙龙 可惜正好那周软考考试, 因为有个麻将牌谱工具是rust写的 github.com/Equim-chan/… 所以又去了解了一下rust,我记得有段时间投入在系统设计/业务场景思考方向(IM/Feed流/本地消息表/分布式限流等等), 都是一点毛皮,Q2服务器服务的内存问题 每天上班想下班想 把互联网的文章都翻烂了 从堆内到堆外到Glibc 询问各种技术大佬场景异常 组内成员给不了你任何帮助 全靠自己,所有的东西没有实质性的收获 也没有在项目中使用到 过了一段时间基本全忘了(不做笔记/总结) 。



现在往回看 在技术学习的时长投入的太少了 对技术没有追求 什么都知道一点 什么都不精通 啥也没学会 离开了Java/Spring 我还能干什么,我能干的 找个会使用AI的人来都能干,天天熬夜 不知道熬的是什么 碌碌无为。最近一年 代码写的很少了 基本靠ai生成 微改/设计一下 写的自己也看不懂了。生活/工作迷茫 现在都是活一天是一天, 想回家。



后续规划 待定



  • 英语/日语

  • 独立开发者



打麻将去咯 一起玩雀魂的可以加我



20241212-151216.png

作者:呆呆蛇
来源:juejin.cn/post/7445511025702764555
收起阅读 »

攀一座山,看一场雪,追一个梦

序 我是个很念旧的人,今年也发生了很多很多事,本文就写点心里话叭,想到啥就写啥,认识我的也别跟我说哈,反正我也不会承认是我 去年在朋友圈写下: 今年再次看到这篇文章的最大感受就是我今年变化好大啊,在所有方面(或许也会受写此文时心境影响),我可能已经有一点认不...
继续阅读 »


我是个很念旧的人,今年也发生了很多很多事,本文就写点心里话叭,想到啥就写啥,认识我的也别跟我说哈,反正我也不会承认是我


去年在朋友圈写下:


c8319449d07eb2032db9ade53b72980.jpg


今年再次看到这篇文章的最大感受就是我今年变化好大啊,在所有方面(或许也会受写此文时心境影响),我可能已经有一点认不出去年的自己了,然后就是想把它删了,好尬啊,想了想还是要坦然的接受过去的自己😅,尬就尬叭,反正我脸皮厚


先看看去年目标:



  • 体重控制到65并且不增长

  • 独立完成项目

  • 写五篇技术博客

  • 看一场周杰伦演唱会

  • 拿到软考架构证


总的来说去年任务完成的不错,今年体重最轻到了60kg,后面被朋友说太轻了增重到了65kg


对技术有了全新的理解,博客也记录了很多以前没听过的新技术(语雀)


可惜周杰伦门票太难抢了,一直没能抢到😭,许嵩邓紫棋啥的也没抢到,太可惜了,架构师太难了,今年年中也太忙了没能考过,明年再接再厉叭


工作


今年3月到11月太忙了,大概4月有俩到三周早8晚11周末加班,凌晨也熬了几次第二天还继续上班,那段时间天天熬夜,晚上3点睡觉,7点又起床,感觉生活都没希望了,一度觉得活着没意思,有一次2点多回家第二天早上请假了没去,有人说我熬一晚请半天假真好,我也没有争辩,其实我挺委屈的,我真的很累啊我前面天天在熬,除了睡觉就在上班,也不敢跟人抱怨,每天晚上就算回家很晚很累了也不想睡觉,因为我知道我一旦睡觉了,第二天的世界又不属于我了...


还有一个多月三级响应周末不休一直加班💦💦💦,后面又参与俩个着急的项目,都是月底验收还麻烦,到处都是压力,白天干这边,晚上远程另一边,真的很累,不管是精神还是身体,不知道怎么抗过来的


不过最近早八晚五正常双休挺开心的,工作量也不大,属于自己的时间很多,唯一可惜的是办公室人越来越少了,波哥,药王,鹏哥,丰哥,超哥都相继离开了,今年过完可能最后的jjj和qs也要走了,能拌嘴的人越来越少了,虽然微信能联系,但是不见面的感情,始终觉得差了点


再也不能和波哥瞎扯了😟,药王偷拍我的照片记得删掉☹️,鹏哥还欠我一份文档记得给我😤,丰哥下次再约滑雪爬山, 我现在感觉我巨强😍,中午午睡再也没有超哥和qs打lol的鼠标声音(真有点菜叭🤣),以后吃完饭回来也闻不到jjj的超级香的饭了,qs以后应该也救不了我了,还有jjj给业主打电话的外放声音再也听不到了😄😄😄😄😄


希望你们过得越来越好~~~


生活


今年生活总体来说还是很开心的,发展了很多兴趣爱好,做了很多以前想做的事情


看着自己的存款越来越多真开心,今年也给爸妈爷爷奶奶买了很多东西,更开心的是侄女终于让我抱了哈哈哈,花了几箱旺仔牛奶和一些玩具成功收买!哈哈哈哈


希望武汉房价再降低一点,想不依靠爸妈在武汉买房子~🤠🤠他们把我养大已经很累了,我希望他们能一直过的好


不知道我以后看到下面这一段会不会笑出来哈哈哈哈


以下是YY时刻


想有属于自己的房子,将每一块地方都布置成我喜欢的样子


房子一定要能晒到太阳,我喜欢太阳晒在床上的感觉


我喜欢躺在阳台上晒太阳,看刚洗的床单轻盈地舞动,听窗外风吹过树枝发出莎莎的声音,鸟儿在树枝间穿梭飞翔(目前租的房子完美满足这点,所以一直没换)


附近要有公园(能让我运动,走路散心),超市(每周会去买菜采购,菜市场的部分阿姨真的很烦😑),附近要有很多好吃的店(偶尔会懒得做饭)


洗碗机一定要有,并且有烘干功能(我喜欢盘子拿在手上热热得温度,很安心),每次做完饭收拾厨房得花很长时间,我喜欢手干燥燥的,最近每洗一个碗都会用纸巾把水擦干净,太费厨房纸巾了


暂时想不起来了,想起来再写叭...


最后,我希望有一个我爱并且爱我的人,一起上下班,逛超市,做饭,吃饭,玩游戏,看电影,一起做去做很多很多事情,留下记忆点


以上就是我为之奋斗的目标(想笑就笑叭反正这里没人认识我🤣🤣)


兴趣&&娱乐


add:跑步,羽毛球,做饭,爬山,滑雪,音乐(最爱的还是杰伦)


夏天的时候每天去江滩跑步,然后公共健身器材区域拉伸,每天浑身湿漉漉的回家洗澡,有一点小小的成就感,有一次在按摩小腿的时候,发现有阿姨在那里按脚🤮🤮,吓得我回家把小腿搓了半个多小时🤢🤢🤢🤢


最近羽毛球打的比较多,打球时间总是很短暂,希望明年能认识更多的朋友一起打球


今年给自己做了好多饭吃,贴一张朋友来我家聚餐,我做的饭照片,希望明年能收录更多菜谱,今年过年做给家里人吃😋😋


2a841c4aa70217a542b3378e8a6e687.jpg


端午节和wsy去反穿了武功山,挺开心的,成功到达金顶,路上还遇到了几个人一起组队的,其中有个'公主'路上和我互相拍照哈哈哈哈哈,还说我是湖南王子,其实我是湖北的哈哈哈哈笑死我了


音乐不必多说,听十多年了杰伦了,永远爱JAY💖💖💖


del:游戏(大部分)


去年回家就玩游戏,今年增加了很多活动,游戏玩的很少了,其他大部分游戏基本都戒掉了,年初还在玩王者,打了个省牌云中君之后也没玩了


现在只有lol有时候朋友叫我还在玩,偶尔自己玩一下地平线,开开车,让自己静下心来


黑悟空第一天的时候,11点到的虎先锋,结果被砍到凌晨2点😭😭😭


9edf54c734998ce43edce42c8cff4bb.jpg


感情



在大家都不再纯粹去爱的年纪,我才笨拙的打开爱情的大门。



学习


软考架构师


今年年中太忙了,没花太多精力在这上面,希望明年能过


前端


从亮哥代码中学了很多,从代码质量到工程化,现在自己搭建了一个monorepo的项目,在往里面添加一些自己的项目


后续有机会的话想在公司推行,因为太多项目都是衍生至一个项目,修改一个项目其他的好多需要同步代码,太麻烦了


服务端


学习了一点java(还未深入),一点python,一点linux等,多少都带点但是没深入,今年主要还是在前端发展,明年向服务端发展,之前还不小心把fhy的云服务器服务全给删了哈哈哈哈


代码质量


今年代码质量有了很大提升,现在看到自己以前的代码犯恶心😤😤,从几个项目里面学习了很多风格,慢慢变成自己的东西


眼界


今年认识了很多新朋友,跟他们聊天也开阔了很多眼界,听到了很多以前闻所未闻东西,今年很多新学习的东西来自他们,还有个朋友送了我一个包包哈哈哈🥳


关于可惜的事儿


没抢到周杰伦演唱会门票


希望明年能抢到叭🥺,真的很想去一次啊,还有许嵩,邓紫棋等等好多~


生日那天在武功山没看到日出


去返穿武功山,结果是大雾,凌晨四点起床也没能看到生日当天的日出,说不失望是假的,不过好歹登上了金顶


没抽到武汉马拉松


今年第一年抽,可惜没抽到,希望明年有好运!


展望


明年我要做到:



  • 每周至少运动8-10小时

  • 写一个自己的网站

  • 继续学习服务端,自己给自己写接口

  • 运维能达到廖哥一半水平

  • 前端代码质量能达到亮哥水平

  • 回家给爸爸妈妈爷爷奶奶做饭吃


明年努力做到:



  • 拿到架构师

  • 找到对象(认真且真诚)


明年希望做到:



  • 抢到周杰伦演唱会门票


今年目标写的很多,但是passion!


写在末尾的话(全是废话)


今天打完球回来写本文,本来想随便写一点,但是写着写着情绪涌了上来,慢慢写了接近五六个小时


中途回忆起了今年很多的事情,有开心,有难忘的时刻,有做错事的尴尬,也有想到讨厌的人的难受,有做的好的事情,也有让别人难堪之后自己后悔莫及的事情


我在这里上不停地打字,想要把这些情绪都宣泄出来,让它们从心里转移到这里,每一个字都是我内心的倾诉,每一个段落都是对过去的一次梳理


我敲下的或许不仅仅是文字,更是自己这些年的成长轨迹,也是我内心最真实的自己


当我敲下这段,明天又是新的一天,马上又是新的一年。




我常觉时间本不存在,是一件件事与一个个人,标记了时间的刻度,赋予了记忆的还原点,我能做的,就是创造更多记忆的闪光点。



作者:介个凑是爱情
来源:juejin.cn/post/7452652740541972490
收起阅读 »

MyBatis里面写模糊查询,like怎么用才对呢?

深入浅出:MyBatis中的模糊查询技巧 在数据库操作的世界里,模糊查询堪称是一项既基本又极其强大的功能。特别是在处理大量数据,需要根据某些不完全匹配的条件进行搜索时,模糊查询的价值就显得尤为重要。🔍 MyBatis作为一个广泛使用的持久层框架,为实现这一功能...
继续阅读 »

深入浅出:MyBatis中的模糊查询技巧


在数据库操作的世界里,模糊查询堪称是一项既基本又极其强大的功能。特别是在处理大量数据,需要根据某些不完全匹配的条件进行搜索时,模糊查询的价值就显得尤为重要。🔍 MyBatis作为一个广泛使用的持久层框架,为实现这一功能提供了便捷的途径。但不少开发者对其模糊查询的实现方式仍然感到困惑。本文将试图消除这种困惑,通过一步步的解析,带领大家正确使用MyBatis进行模糊查询。


引言


简述模糊查询在数据处理中的重要性


模糊查询是数据库操作中不可或缺的一部分,尤其在处理文本数据时,它能够根据不完全或模糊的条件,帮助开发者快速定位并检索出所需的数据行。例如,在一个拥有成千上万用户信息的系统中,通过模糊查询姓名或地址,能够高效地筛选出符合条件的信息。🚀


为什么要掌握MyBatis中的模犹如查询技术


掌握MyBatis中的模糊查询,可以使数据库操作更加灵活高效。对于已经选择MyBatis作为数据层框架的项目,能准确运用模糊查询,意味着能在保持代码的可维护性和清晰结构的同时,实现强大的数据检索功能。


模犹如查询基础


SQL中的LIKE语句


在SQL中,LIKE语句是实现模糊查询的关键。它通常与%(表示任意多个字符)和_(表示一个任意字符)这两个通配符一起使用。例如:



  • %apple%:匹配任何包含"apple"的字符串。

  • _apple%:匹配以任意字符开头,后面跟着"apple"的字符串。


LIKE语句的常见使用模式


基于LIKE语句的模糊查询可以有多种不同的用法,选择合适的模式可以大幅提升查询的效率和准确度。


MyBatis简介


MyBatis的核心功能


MyBatis是一种半ORM(对象关系映射)框架,它桥接了Java对象和数据库之间的映射,通过XML或注解的方式,将SQL语句与Java方法关联起来,从而简化了数据操作层的代码。


如何在MyBatis中配置和使用Mapper


在MyBatis中,Mapper的配置主要通过Mapper.xml文件进行。每一个Mapper.xml文件都对应一个Mapper接口,文件中定义了与接口方法相对应的SQL语句。使用Mapper非常简单,只需在相关的Service层中引入Mapper接口,MyBatis框架会自动代理这些接口,使得调用数据库操作像调用Java方法一样简单。


MyBatis中的模犹如查询实现


MyBatis中LIKE语句的基本用法


在Mapper.xml中使用#{variable}占位符


<select id="findUserByName" resultType="com.example.User">
SELECT * FROM users WHERE name LIKE CONCAT('%', #{name}, '%')
</select>

此处使用了CONCAT函数和#{variable}占位符,动态地将输入的变量与%通配符结合起来,实现了基本的模犹如查询功能。


使用${variable}拼接SQL


虽然使用${variable}进行SQL拼接能提供更灵活的查询方法,但需要谨慎使用,以避免SQL注入风险。


<select id="findUserByName" resultType="com.example.User">
SELECT * FROM users WHERE name LIKE '%${name}%'
</select>

动态SQL与模犹如查询


<if>标签的使用


<select id="findUserByCondition" parameterType="map" resultType="com.example.User">
SELECT * FROM users
<where>
<if test="name != null">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
... // 更多条件
</where>
</select>

<choose><when><otherwise>的结合使用


<select id="findUserByDynamicCondition" parameterType="map" resultType="com.example.User">
SELECT * FROM users
<where>
<choose>
<when test="name != null">
AND name LIKE CONCAT('%', #{name}, '%')
</when>
<when test="email != null">
AND email LIKE CONCAT('%', #{email}, '%')
</when>
<otherwise>
AND id > 0 // 默认条件
</otherwise>
</choose>
</where>
</select>

实践案例


假设我们有一个用户管理系统,需要根据用户的姓名进行模糊查询。


场景描述


在用户管理系统中,后台需要根据前端传来的姓名关键字,模糊匹配数据库中的用户姓名,返回匹配的用户列表。


代码实现


Mapper接口定义


public interface UserMapper {
List<User> findUserByName(String name);
}

Mapper.xml配合LIKE的具体写法


<select id="findUserByName" resultType="com.example.User">
SELECT * FROM users WHERE name LIKE CONCAT('%', #{name}, '%')
</select>

结果验证


调用findUserByName方法,传入关键字,即可得到所有姓名中包含该关键字的用户数据。


高级技巧与最佳实践


使用trim标签优化LIKE查询


<select id="findUserByName" parameterType="string" resultType="com.example.User">
SELECT * FROM users
WHERE name LIKE
<trim prefix="%" suffix="%" prefixOverrides="%" suffixOverrides="%">
#{name}
</trim>
</select>

小技巧:避免模糊查询带来的性能问题


尽量避免以%开头的模糊查询,因为这会使数据库全表扫描,极大地影响查询性能。


安全性考虑:防止SQL注入


在使用${}进行SQL拼接时,一定要确保变量来源可控或已做过适当校验,防止SQL注入攻击。


总结与展望


虽然模糊查询在数据库操作中极其有用,但它也不是万能的。在使用MyBatis实现模糊查询时,既要考虑到其便捷性和灵活性,也不能忽视潜在的性能和安全风险。我们希望通过本文,你能更准确、更高效地使用MyBatis进行模糊查询。


未来随着技术的发展,MyBatis和相关的数据库技术仍将不断进化,但基本的原则和最佳实践应该是不变的。掌握这些,将能使你在使用MyBatis进行数据库操作时更加得心应手。


附录


欲了解更多MyBatis的高级功能和最佳实践,可以参考:



  • MyBatis官方文档

  • 相关技术社区和论坛


Q&A环节:如果你有任何关于MyBatis模糊查询的问题,欢迎在评论区留言交流。📢




希望本文能帮助你更好地理解和使用MyBatis进行模糊查询,欢迎分享和交流你的经验!🚀


作者:lsoxvxe
来源:juejin.cn/post/7343225969237671972
收起阅读 »

面试官:limit 100w,10为什么慢?如何优化?

在 MySQL 中,limit X,Y 的查询中,X 值越大,那么查询速度也就越慢,例如以下示例: limit 0,10:查询时间大概在 20 毫秒左右。 limit 1000000,10:查询时间可能是 15 秒左右(1秒等于 1000 毫秒),甚至更长时...
继续阅读 »


在 MySQL 中,limit X,Y 的查询中,X 值越大,那么查询速度也就越慢,例如以下示例:



  • limit 0,10:查询时间大概在 20 毫秒左右。

  • limit 1000000,10:查询时间可能是 15 秒左右(1秒等于 1000 毫秒),甚至更长时间。


所以,可以看出,limit 中 X 值越大,那么查询速度都越慢。


这个问题呢其实就是 MySQL 中典型的深度分页问题。那问题来了,为什么 limit 越往后查询越慢?如何优化查询速度呢?


为什么limit越来越慢?


在数据库查询中,当使用 LIMIT x, y 分页查询时,如果 x 值越大,查询速度可能会变慢。这主要是因为数据库需要扫描和跳过 x 条记录才能返回 y 条结果。随着 x 的增加,需要扫描和跳过的记录数也增加,从而导致性能下降。



例如 limit 1000000,10 需要扫描 1000010 行数据,然后丢掉前面的 1000000 行记录,所以查询速度就会很慢。



优化手段


对于 MySQL 深度分页比较典型的优化手段有以下两种:



  1. 起始 ID 定位法:使用最后查询的 ID 作为起始查询的 ID。

  2. 索引覆盖+子查询


1.起始ID定位法


起始 ID 定位法指的是 limit 查询时,指定起始 ID。而这个起始 ID 是上一次查询的最后一条 ID。例如上一次查询的最后一条数据的 ID 为 6800000,那我们就从 6800001 开始扫描表,直接跳过前面的 6800000 条数据,这样查询的效率就高了,具体实现 SQL 如下:


select name, age, gender
from person
where id > 6800000 -- 核心实现 SQL
order by id limit 10;


其中 id 字段为表的主键字段。



为什么起始ID查询效率高呢?


因此这种查询是以上一次查询的最后 ID 作为起始 ID 进行查询的,而上次的 ID 已经定位到具体的位置了,所以只需要遍历 B+ 树叶子节点的双向链表(主键索引的底层数据结构)就可以查询到后面的数据了,所以查询效率就比较高,如下图所示:



如果上次查询结果为 9,之后再查询时,只需要从 9 之后再遍历 N 条数据就能查询出结果了,所以效率就很高。


优缺点分析


这种查询方式,只适合一页一页的数据查询,例如手机 APP 中刷新闻时那种瀑布流方式。


但如果用户是跳着分页的,例如查询完第 1 页之后,直接查询第 250 页,那么这种实现方式就不行了。


2.索引覆盖+子查询


此时我们为了查询效率,可以使用索引覆盖加子查询的方式,具体实现如下。


假设,我们未优化前的 SQL 如下:


select name, age, gender
from person
order by createtime desc
limit 1000000,10;


在以上 SQL 中,createtime 字段创建了索引,但查询效率依然很慢,因为它要取出 100w 完整的数据,并需要读取大量的索引页,和进行频繁的回表查询,所以执行效率会很低。



此时,我们可以做以下优化:


SELECT p1.name, p1.age, p1.gender
FROM person p1
JOIN (
SELECT id FROM person ORDER BY createtime desc LIMIT 1000000, 10
)
AS p2 ON p1.id = p2.id;

相比于优化前的 SQL,优化后的 SQL 将不需要频繁回表查询了,因为子查询中只查询主键 ID,这时可以使用索引覆盖来实现。那么子查询就可以先查询出一小部分主键 ID,再进行查询,这样就可以大大提升查询的效率了。



索引覆盖(Index Coverage)是一种数据库查询优化技术,它指的是在执行查询时,数据库引擎可以直接从索引中获取所有需要的数据,而不需要再回表(访问主键索引或者表中的实际数据行)来获取额外的信息。这种方式可以减少磁盘 I/O 操作,从而提高查询性能。



课后思考


你还知道哪些深度分页的优化手段呢?欢迎评论区留下你的答案。



本文已收录到我的面试小站 http://www.javacn.site,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。



作者:Java中文社群
来源:juejin.cn/post/7410987368343765046
收起阅读 »

SpringBoot引入Flyway

1 缘起与目的 最近遇到一个项目要部署到很多不同的地方,在每个地方升级时如何管理数据库升级脚本就成了一个叩待解决的问题。本文引入flyway工具来解决这个问题。 2 依赖 <dependency> <groupId>org.fl...
继续阅读 »

1 缘起与目的


最近遇到一个项目要部署到很多不同的地方,在每个地方升级时如何管理数据库升级脚本就成了一个叩待解决的问题。本文引入flyway工具来解决这个问题。


2 依赖


<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>7.15.0</version>
</dependency>

此处笔者MySQL版本为5.7,上述版本依赖可生效。此处踩坑过程见踩坑记录。


3 yml


spring:
# flyway 配置
flyway:
# 启用或禁用 flyway
enabled: false
# flyway 的 clean 命令会删除指定 schema 下的所有 table, 生产务必禁掉。这个默认值是 false 理论上作为默认配置是不科学的。
clean-disabled: true
# SQL 脚本的目录,多个路径使用逗号分隔 默认值 classpath:db/migration {vendor}对应数据库类型,可选值 https://github.com/spring-projects/spring-boot/blob/v2.3.3.RELEASE/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/DatabaseDriver.java
locations: classpath:sql/{vendor}
# metadata 版本控制信息表 默认 flyway_schema_history
table: flyway_schema_history
# 如果没有 flyway_schema_history 这个 metadata 表, 在执行 flyway migrate 命令之前, 必须先执行 flyway baseline 命令
# 设置为 true 后 flyway 将在需要 baseline 的时候, 自动执行一次 baseline。
baseline-on-migrate: false
# 指定 baseline 的版本号,默认值为 1, 低于该版本号的 SQL 文件, migrate 时会被忽略
baseline-version: 1
# 字符编码 默认 UTF-8
encoding: UTF-8
# 是否允许不按顺序迁移 开发建议 true 生产建议 false
out-of-order: false
# 执行迁移时是否自动调用验证 当你的 版本不符合逻辑 比如 你先执行了 DML 而没有 对应的DDL 会抛出异常
validate-on-migrate: true

4 表结构


配好后依赖和yml直接启动项目会自动创建表结构。


image.png
值得一说的是checksum。可以理解为校验字符串,每次执行完sql脚本后会针对脚本生成checknum,后续如果之前执行过的脚本出现改动与前面的checknum不一致会直接报错。


4 脚本命名


命名规则如下:


V版本号__版本名.sql


例如: V2.1.5__create_user_ddl.sqlV4.1_2__add_user_dml.sql


image.png
因为配置的baseline-version=1,所以只有1以上版本才会被执行,上图V0.0.1__base.sql是不会被执行的。上图只是为了展示命名规则。


5 踩坑记录


5.1 Unsupported Database: MySQL 5.7


笔者最开始的依赖如下:


<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>

报错如下:


org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'asyncBeanPriorityLoadPostProcessor' defined in class path resource [io/github/linyimin0812/async/AsyncBeanAutoConfiguration.class]: Initialization of bean failed; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'efpxInitQuartzJob': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'sysJobServiceImpl' defined in file [E:\java\project\pm2\pm_modularity\efp-plugins\target\classes\com\sdecloud\modules\quartz\service\impl\SysJobServiceImpl.class]: Unsatisfied dependency expressed through constructor parameter 1; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'flywayInitializer' defined in class path resource [org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayConfiguration.class]: Invocation of init method failed; nested exception is org.flywaydb.core.api.FlywayException: Unsupported Database: MySQL 5.7
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:628)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)



此处笔者检索到了互联网文章 【原创】Flyway 8.2.1及以后版本不再支持MySQL?!_unsupported database: mysql 8.0-CSDN博客,阅读后笔者表示???还是去官网一探究竟吧。




  1. 通过官网(documentation.red-gate.com/flyway/flyw… 对MySQL支持的说明,修改依赖如下:


    <dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
    </dependency>
    <dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-mysql</artifactId>
    </dependency>

    结果依然报错如下


    MySQL 5.7 is no longer supported by Flyway Community Edition, but still supported by Flyway Teams Edition.


  2. 通过stack overflow(stackoverflow.com/questions/7… )查询发现:


    (1)Flyway Community Edition 8.0.0-beta1放弃了对5年以上数据库的支持,包括MySQL 5.7
    在这次提交中,MySQL的最低支持版本从5.7增加到8.0,这是在Flyway 8.0.0-beta1中引入的。目前,支持MySQL 5.7的最新社区版本是Flyway 7.15.0。


    (2)从Flyway第10版(2023年10月)起,此限制不再有效。我们已经更新了Flyway,使其适用于所有支持的数据库版本,因此如果您升级到版本10,您可以访问所有支持的MySQL版本。


    笔者回退到7.15.0后再无报错。即最终依赖为标题1所示。



作者:roc98
来源:juejin.cn/post/7330463614954209334
收起阅读 »

比Spring参数校验更优雅!使用函数式编程把参数检验玩出花来!

比Spring参数校验更优雅!使用函数式编程把参数检验玩出花来! 未经允许禁止转载! 使用 Vavr 验证库来替代标准的 Java Bean Validation(如 @NotBlank, @Size 等注解)可以通过函数式的方式来处理验证逻辑。Vavr 是一...
继续阅读 »

比Spring参数校验更优雅!使用函数式编程把参数检验玩出花来!


未经允许禁止转载!


使用 Vavr 验证库来替代标准的 Java Bean Validation(如 @NotBlank, @Size 等注解)可以通过函数式的方式来处理验证逻辑。Vavr 是一个支持不可变数据结构和函数式编程的库,可以让代码更加简洁和函数式。


要使用 Vavr 的验证器,我们可以利用 Vavr 下Validation 类,它提供了一种函数式的方式来处理验证,允许收集多个错误,而不仅仅是遇到第一个错误就终止。


1. BeanValidator 实现的问题


以下是使用BeanValidator实现参数校验的代码


@Data
public class User {
// bean validator 使用注解实现参数校验
@NotBlank(message = "用户姓名不能为空")
private String name;

@NotBlank(message = "密码不能为空")
@Size(min = 6, message = "密码长度不能少于6位")
private String password;

@Min(value = 0, message = "年龄不能小于0岁")
@Max(value = 150, message = "年龄不应超过150岁")
private Integer age;

@Pattern(regexp = "^((13[0-9])|(15[^4])|(18[0-9])|(17[0-9])|(147))\d{8}$", message = "手机号格式不正确")
private String phone;
}

Spring 提供了对 BeanValidator 的支持,可以在不同的层级(controller、service、repository)使用。


缺点:



  1. 要求被验证的对象是可变的 JavaBean(具有getter,setter方法),JavaBean是一种常见的反模式。

  2. 校验逻辑的复杂应用有很大的学习成本,比如自定义验证注解、分组校验等。

  3. 异常处理逻辑一般需要配合Spring全局异常处理。


最佳实践:


PlanA: 实践中建议仅在 controller 层面校验前端传入的 json 参数,不使用自定义注解,分组校验等复杂功能。


PlanB: 直接使用函数式验证。


2. 使用 Vavr 重新设计 User 类的验证逻辑


2.1 使用到的函数式思想:



  1. 校验结果视为值,返回结果为和类型,即异常结果或正常结果。这里的异常结果指的是校验失败的参数列表,正常结果指的是新创建的对象。

  2. 复用函数,这里具体指校验逻辑和构造器方法(或者静态方法创建对象)

  3. Applicative functor,本文不想讨论难以理解的函数式概念。这里可以简单理解成封装函数、同时支持 apply(map)的容器。

  4. 收集所有校验异常结果,此处的处理和提前返回(卫模式、短路操作)不同。


以下是使用 Vavr 中参数校验的代码:


PersonValidator personValidator = new PersonValidator();

// Valid(Person(John Doe, 30))
Validation<Seq<String>, Person> valid = personValidator.validatePerson("John Doe", 30);

// Invalid(List(Name contains invalid characters: '!4?', Age must be greater than 0))
Validation<Seq<String>, Person> invalid = personValidator.validatePerson("John? Doe!4", -1);

首先,需要定义一个验证器类,而不是直接在 User 类上使用注解。这个验证器类会对 User 的字段进行验证,并返回一个 Validation 对象。


2.2 验证器实现


// 使用实体类,这个类是无状态的
public class UserValidator {

// 验证用户
public Validation<Seq<String>, User> validateUser(String name, String password, Integer age, String phone) {
return Validation.combine(
validateName(name),
validatePassword(password),
validateAge(age),
validatePhone(phone))
.ap(User::new);
}

// 验证用户名
private Validation<String, String> validateName(String name) {
return (name == null || name.trim().isEmpty())
? Invalid("用户姓名不能为空")
: Valid(name);
}

// 验证密码
private Validation<String, String> validatePassword(String password) {
if (password == null || password.isEmpty()) {
return Invalid("密码不能为空");
}
if (password.length() < 6) {
return Invalid("密码长度不能少于6位");
}
return Valid(password);
}

// 验证年龄
private Validation<String, Integer> validateAge(Integer age) {
if (age == null) {
return Invalid("年龄不能为空");
}
if (age < 0) {
return Invalid("年龄不能小于0岁");
}
if (age > 150) {
return Invalid("年龄不应超过150岁");
}
return Valid(age);
}

// 验证手机号
private Validation<String, String> validatePhone(String phone) {
String phoneRegex = "^((13[0-9])|(15[^4])|(18[0-9])|(17[0-9])|(147))\\d{8}$";
if (phone == null || !phone.matches(phoneRegex)) {
return Invalid("手机号格式不正确");
}
return Valid(phone);
}
}

2.3 使用


public class UserValidationExample {

public static void main(String[] args) {
UserValidator validator = new UserValidator();

// 示例:测试一个有效用户
Validation<Seq<String>, User> validUser = validator.validateUser("Alice", "password123", 25, "13912345678");

if (validUser.isValid()) {
System.out.println("Valid user: " + validUser.get());
} else {
System.out.println("Validation errors: " + validUser.getError());
}

// 示例:测试一个无效用户
Validation<Seq<String>, User> invalidUser = validator.validateUser("", "123", -5, "12345");

if (invalidUser.isValid()) {
System.out.println("Valid user: " + invalidUser.get());
} else {
System.out.println("Validation errors: " + invalidUser.getError());
}
}
}


  1. Validation.combine():将多个验证结果组合起来。每个验证返回的是 Validation<String, T>,其中 String 是错误消息,T 是验证成功时的值。

  2. User::new:这是一个方法引用,表示如果所有的字段都验证成功,就调用 User 的构造函数创建一个新的 User 对象。

  3. 验证错误的收集:Vavr 的验证机制允许收集多个错误,而不是像传统 Java Bean Validation 那样一旦遇到错误就停止。这样,你可以返回所有的验证错误,让用户一次性修复。


2.4 结果示例



  1. 对于一个有效的用户:


    Valid user: User(name=Alice, password=password123, age=25, phone=13912345678)


  2. 对于一个无效的用户:


    Validation errors: List(用户姓名不能为空, 密码长度不能少于6位, 年龄不能小于0岁, 手机号格式不正确)



3. 源码解析


如果你仅关注使用的话,此段内容可以跳过。


此处仅分析其核心代码:


// Validation#combine 返回 Builder 类型
final class Builder<E, T1, T2> {
private Validation<E, T1> v1;
private Validation<E, T2> v2;

public <R> Validation<Seq<E>, R> ap(Function2<T1, T2, R> f) {
// 注意这里的执行顺序: v1#ap -> v2#ap
return v2.ap(v1.ap(Validation.valid(f.curried())));
}
}

f.curried 返回结果为 T1 => T2 => R,valid 方法使用 Validation 容器封装了函数:


// validation 为和类型,有且仅有两种实现
public interface Validation<E, T> extends Value<T>, Serializable {
static <E, T> Validation<E, T> valid(T value) {
return new Valid<>(value);
}

static <E, T> Validation<E, T> invalid(E error) {
Objects.requireNonNull(error, "error is null");
return new Invalid<>(error);
}
}

最关键的代码为 ap(apply的缩写):


default <U> Validation<Seq<E>, U> ap(Validation<Seq<E>, ? extends Function<? super T, ? extends U>> validation) {
Objects.requireNonNull(validation, "validation is null");
if (isValid()) {
if (validation.isValid()) {
// 正常处理逻辑
final Function<? super T, ? extends U> f = validation.get();
final U u = f.apply(this.get());
return valid(u);
} else {
// 保留原有的失败结果
final Seq<E> errors = validation.getError();
return invalid(errors);
}
} else {
if (validation.isValid()) {
// 初始化失败结果
final E error = this.getError();
return invalid(List.of(error));
} else {
// 校验失败,收集失败结果
final Seq<E> errors = validation.getError();
final E error = this.getError();
return invalid(errors.append(error));
}
}
}

这里的实现非常巧妙,柯里化的函数在正常处理逻辑中不断执行,最后调用成功,返回正确的函数结果。执行流程中有异常结果后,分成三中情况进行处理,分别是初始化,保留结果,进一步收集结果。


4. 总结与最佳实践



  1. 这种方式使用 Vavr 提供的函数式验证工具,使得验证逻辑更加简洁、灵活,并且可以收集多个错误进行统一处理,避免散弹枪问题。

  2. 对于需要返回单一错误的情况(实际上不多),也可以使用这种方法,然后取用任意一条结果。

  3. Validation支持多条无关参数的校验。当涉及到多参数的校验时,建议进行手动编码。


record Person(name, age) {}
static final String ADULT_CONTENT = "adult";
static final int ADULT_AGE = 18;

public Validation<Seq<String>, Person> validatePerson2(String name, int age) {
return Validation.combine(validateName(name), validateAge(age)).ap(Person::new)
.flatMap(this::validateAdult);
}

private Validation<Seq<String>, Person> validateAdult(Person p) {
return p.age < ADULT_AGE && p.name.contains(ADULT_CONTENT)
? Validation.invalid(API.List("Illegal name"))
: Validation.valid(p);
}

此外,对于某些参数传参,建议使用对象组合,比如range参数有两种做法,第一种可以传入 from, to, 校验条件为 from < to, 校验后对象包含属性Range,之后在额外校验中校验 Range;第二种可以限制传入参数为 Range。


作者:桦说编程
来源:juejin.cn/post/7416605082688962610
收起阅读 »

shardingjdbc有点坑,数据库优化别再无脑回答分库分表了

故事背景 在八股文中,说到如何进行数据库的优化,除了基本的索引优化,经常会提到分库分表,说是如果业务量剧增,数据库性能会到达瓶颈,如果单表数据超过两千万,数据查询效率就会变低,就要引入分库分表巴拉巴拉。我同事也问我,我们数据表有些是上亿数据的,为什么不用分库分...
继续阅读 »

故事背景


在八股文中,说到如何进行数据库的优化,除了基本的索引优化,经常会提到分库分表,说是如果业务量剧增,数据库性能会到达瓶颈,如果单表数据超过两千万,数据查询效率就会变低,就要引入分库分表巴拉巴拉。我同事也问我,我们数据表有些是上亿数据的,为什么不用分库分表,如果我没接触过分库分表我也会觉得大数据表就要分库分表呀,这是八股文一直以来教导的东西。但是我就跟他说,分库分表很坑爹,最近才让我遇到一个BUG......


系统复杂度upup


业务中有个设备表数据量很大,到现在为止已经有5、6亿数据了。在4年前,前人们已经尝试了分库分表技术,分了4个库,5个表,我只是负责维护这个业务发现他们用了分库分表。但是在查询表数据的时候看到是查询ES的,我就问为什么要用ES?同事回答查询分库分表一定要带分片才能走到路由,否则会查询全部库和全部表,意思是不查分片字段,单表只用一个SQL,但是分库分表要用20个SQL.....所以引入了ES进行数据查询。但是引入ES之后又引入一个新的问题,就是ES和数据库的数据同步问题。他们使用了logstash做数据同步,但不是实时的,在logstash设置了每20秒同步一次。


image.png

因为要使用分库分表,引入了shardingjdbc,因为查询方便引入了es,因为要处理数据同步问题引入了logstash......所以系统复杂度不是高了一点半点,之前发现有个字段长度设置小了,还要改20张表。


分页问题


最近遇到一个奇怪的bug,在一个设备的单表查询翻页失败,怎么翻都只显示第一页的数据,一开始我以为是分页代码有问题,看了半天跟其他表是一样的,其他表分页没问题,见鬼了。后面再细看发现这个单表的数据源是设备数据源,用的是shardingjdbc的配置。


image.png

之前就看过shardingjdbc有一些sql是不支持的,怀疑就是这个原因,百度了一下果然是有bug。


image.png

想了一下有两个解决办法,第一个是升级shardingjdbc的版本,据说是4.1之后修复了该问题,但是还没有尝试。


第二个办法是把分库分表业务的数据源跟单表区分开,单表业务使用普通的数据源后分页数据正常显示。


关于数据库优化


一般来说数据库优化,可以从几个角度进行优化:


1、硬件优化


(1) 提升存储性能



  • 使用SSD:替换传统机械硬盘(HDD),SSD能提供更快的随机读写速度。

  • 增加存储带宽:采用RAID(推荐RAID 10)提高数据存储的读写速度和冗余。

  • 内存扩展:尽量让数据库缓存更多的数据,减少IO操作。


(2) 增强CPU性能



  • 使用多核高频率CPU,支持更高并发。

  • 分析数据库对CPU的利用情况,确保不被CPU性能瓶颈限制。


(3) 提高网络带宽



  • 优化服务器与客户端之间的网络延迟和带宽,尤其是分布式数据库的场景中。

  • 使用高速网络接口(如10GbE网卡)。


2、软件层面优化


(1) 数据库配置



  • 调整数据库缓冲池(Buffer Pool)的大小,确保能缓存大部分热数据。

  • 优化日志文件的写入(如MySQL中调整innodb_log_buffer_size)。

  • 使用内存数据库或缓存技术(如Redis、Memcached)加速访问速度。


(2) 分布式架构



  • 对于高并发需求,采用分布式数据库(如TiDB、MongoDB)进行读写分离或数据分片。


(3) 数据库索引



  • 选择合适的索引类型:如B+树索引、哈希索引等,根据查询特点选择适配的索引。

  • 避免冗余索引,定期清理无用索引。


(4) 数据库版本升级



  • 保持数据库版本为最新的稳定版本,利用最新的优化特性和Bug修复。


3. SQL层面优化


(1) 查询优化



  • 减少不必要的字段:只查询需要的列,避免使用SELECT *

  • 加速排序和分组:在ORDER BYGR0UP BY字段上建立索引。

  • 拆分复杂查询:将复杂的SQL分解为多个简单查询或视图。

  • 分页查询优化:如避免大OFFSET分页,可以使用索引条件替代(如WHERE id > last_seen_id)。


(2) 合理使用索引



  • 对频繁用于WHERE、JOIN、GR0UP BY等的字段建立索引。

  • 避免在索引列上使用函数或隐式转换。


(3) 减少锁定



  • 尽量使用小事务,减少锁定范围。

  • 使用合适的事务隔离级别,避免不必要的资源等待。


(4) SQL调优工具



  • 使用数据库自带的分析工具(如MySQL的EXPLAIN、SQL Server的性能监控工具)来分析查询计划并优化执行路径。


4. 综合优化



  • 定期进行性能分析:定期查看慢查询日志,优化慢查询。

  • 清理历史数据:对于不再使用的历史数据,可存储到冷数据仓库,减少主数据库的负载。

  • 使用连接池:通过数据库连接池(如HikariCP)管理和复用连接,降低创建和销毁连接的开销。


tips:


现网的数据库是64核128G内存,测试环境是32核64G,加上现网数据库配置的优化,现网数据库查询大表的速度是测试环境的3倍!所以服务器硬件配置和数据库配置都很重要。下面是数据库的配置文件,仅供参考


[universe]
bakupdir = /data/mysql/backup/7360
iops = 0
mem_limit_mb = 0
cpu_quota_percentage = 0
quota_limit_mb = 0
scsi_pr_level = 0
mycnf = /opt/mysql/etc/7360/my.cnf
run_user = actiontech-mysql
umask_dir = 0750
umask = 0640
id = mysql-mt1cbg
group_id = mysql-test

[mysql]
no-auto-rehash
prompt = '\\u@\\h:\\p\\R:\\m:\\s[\\d]> '
#default-character-set = utf8mb4
#tee = /data/mysql_tmp/mysql_operation.log

[mysqld]
super_read_only = 1
# DO NOT MODIFY, Universe will generate this part
port = 7360
server_id = 123
basedir = /opt/mysql/base/5.7.40
datadir = /data/mysql/data/7360
log_bin = /opt/mysql/log/binlog/7360/mysql-bin
tmpdir = /opt/mysql/tmp/7360
relay_log = /opt/mysql/log/relaylog/7360/mysql-relay
innodb_log_group_home_dir = /opt/mysql/log/redolog/7360
log_error = /data/mysql/data/7360/mysql-error.log
# 数据库ip
report_host = xxx

#
BINLOG
binlog_error_action = ABORT_SERVER
binlog_format = row
binlog_rows_query_log_events = 1
log_slave_updates = 1
master_info_repository = TABLE
max_binlog_size = 250M
relay_log_info_repository = TABLE
relay_log_recovery = 1
sync_binlog = 1

#
GTID #
gtid_mode = ON
enforce_gtid_consistency = 1
binlog_gtid_simple_recovery = 1

#
ENGINE
default_storage_engine = InnoDB
innodb_buffer_pool_size = 64G
innodb_data_file_path = ibdata1:1G:autoextend
innodb_file_per_table = 1
innodb_flush_log_at_trx_commit = 1
innodb_flush_method = O_DIRECT
innodb_io_capacity = 1000
innodb_log_buffer_size = 64M
innodb_log_file_size = 2G
innodb_log_files_in_group = 2
innodb_max_dirty_pages_pct = 60
innodb_print_all_deadlocks = 1
#innodb_stats_on_metadata = 0
innodb_strict_mode = 1
#innodb_undo_logs = 128 #Deprecated In 5.7.19
#innodb_undo_tablespaces=3 #Deprecated In 5.7.21
innodb_max_undo_log_size = 4G
innodb_undo_log_truncate = 1
innodb_read_io_threads = 8
innodb_write_io_threads = 8
innodb_purge_threads = 4
innodb_buffer_pool_load_at_startup = 1
innodb_buffer_pool_dump_at_shutdown = 1
innodb_buffer_pool_dump_pct = 25
innodb_sort_buffer_size = 8M
#innodb_page_cleaners = 8
innodb_buffer_pool_instances = 8
innodb_lock_wait_timeout = 10
innodb_io_capacity_max = 2000
innodb_flush_neighbors = 1
#innodb_large_prefix = 1
innodb_thread_concurrency = 64
innodb_stats_persistent_sample_pages = 64
innodb_autoinc_lock_mode = 2
innodb_online_alter_log_max_size = 1G
innodb_open_files = 4096
innodb_temp_data_file_path = ibtmp1:12M:autoextend:max:50G
innodb_rollback_segments = 128
#innodb_numa_interleave = 1

#
CACHE
key_buffer_size = 16M
tmp_table_size = 64M
max_heap_table_size = 64M
table_open_cache = 2000
query_cache_type = 0
query_cache_size = 0
max_connections = 3000
thread_cache_size = 200
open_files_limit = 65535
binlog_cache_size = 1M
join_buffer_size = 8M
sort_buffer_size = 2M
read_buffer_size = 8M
read_rnd_buffer_size = 8M
table_definition_cache = 2000
table_open_cache_instances = 8


#
SLOW LOG
slow_query_log = 1
slow_query_log_file = /data/mysql/data/7360/mysql-slow.log
log_slow_admin_statements = 1
log_slow_slave_statements = 1
long_query_time = 1

#
SEMISYNC #
plugin_load = "rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so"
rpl_semi_sync_master_enabled = 1
rpl_semi_sync_slave_enabled = 0
rpl_semi_sync_master_wait_for_slave_count = 1
rpl_semi_sync_master_wait_no_slave = 0
rpl_semi_sync_master_timeout = 30000

#
CLIENT_DEPRECATE_EOF
session_track_schema = 1
session_track_state_change = 1
session_track_system_variables = '*'

#
MISC
log_timestamps = SYSTEM
lower_case_table_names = 1
max_allowed_packet = 64M
read_only = 1
skip_external_locking = 1
skip_name_resolve = 1
skip_slave_start = 1
socket = /data/mysql/data/7360/mysqld.sock
pid_file = /data/mysql/data/7360/mysqld.pid
disabled_storage_engines = ARCHIVE,BLACKHOLE,EXAMPLE,FEDERATED,MEMORY,MERGE,NDB
log-output = TABLE,FILE
character_set_server = utf8mb4
secure_file_priv = ""
performance-schema-instrument = 'wait/lock/metadata/sql/mdl=ON'
performance-schema-instrument = 'memory/% = COUNTED'
expire_logs_days = 7
max_connect_errors = 1000000
interactive_timeout = 1800
wait_timeout = 1800
log_bin_trust_function_creators = 1

#
MTS
slave-parallel-type = LOGICAL_CLOCK
slave_parallel_workers = 16
slave_preserve_commit_order = ON
slave_rows_search_algorithms = 'INDEX_SCAN,HASH_SCAN'

#
#BaseConfig
collation_server = utf8mb4_bin
explicit_defaults_for_timestamp = 1
transaction_isolation = READ-COMMITTED

#
#Unused
#plugin-load-add = validate_password.so
#validate_password_policy = MEDIUM

总结


如果我没用过分库分表,面试官问我数据库优化,我可能也会回答分库分表。但是踩过几个坑之后可能会推荐其他的方式。


1、按业务分表,比如用户表放在用户库,订单表放在订单库,用微服务的思想切割数据库减少数据库压力。


2、如果数据量超过10E,可以考虑上分布式数据库,融合了OLAP和OLTP的优点,毕竟mysql其实不适合做大数据量的查询统计。评论区也可以推荐一下有哪些好的数据库。


3、按时间归档数据表,每天或者每个月把历史数据存入历史数据表,适用于大数据量且历史数据查询较少的业务。


每个技术都有它的利弊,比如微服务、分库分表、分布式数据库等。按需选择技术类型,切勿过度设计


作者:玛奇玛丶
来源:juejin.cn/post/7444014749321461811
收起阅读 »

Mybatis-Plus的insert执行之后,id是怎么获取的?

在日常开发中,会经常使用Mybatis-Plus 当简单的插入一条记录时,使用mapper的insert是比较简洁的写法 @Data public class NoEo { Long id; String no; } NoEo noEo = ...
继续阅读 »

在日常开发中,会经常使用Mybatis-Plus


当简单的插入一条记录时,使用mapper的insert是比较简洁的写法


@Data
public class NoEo {
Long id;
String no;
}

NoEo noEo = new NoEo();
noEo.setNo("321");
noMapper.insert(noEo);
System.out.println(noEo);

这里可以注意到一个细节,就是不管我们使用的是什么类型的id,好像都不需要去setId,也能执行insert语句


不仅不需要setId,在insert语句执行完毕之后,我们还能通过实体类获取到这条insert的记录的id是什么


image.png


image.png


这背后的原理是什么呢?


自增类型ID


刚学Java的时候,插入了一条记录还要再select一次来获取这条记录的id,比较青涩


后面误打误撞才发现可以直接从insert的实体类中拿到这个id


难道框架是自己帮我查了一次嘛


先来看看自增id的情况


首先要先把yml中的mp的id类型设置为auto


mybatis-plus:
global-config:
db-config:
id-type: auto

然后从insert语句开始一直往下跟进


noMapper.insert(noEo);

后面会来到这个方法


// com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor#doUpdate
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
stmt = prepareStatement(handler, ms.getStatementLog(), false);
return stmt == null ? 0 : handler.update(stmt);
} finally {
closeStatement(stmt);
}
}

在执行了下面这个方法之后


handler.update(stmt)

实体类的id就赋值上了


继续往下跟


// org.apache.ibatis.executor.statement.PreparedStatementHandler#update
@Override
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
int rows = ps.getUpdateCount();
Object parameterObject = boundSql.getParameterObject();
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows;
}

image.png


最后的赋值在这一行


keyGenerator.processAfter

可以看到会有一个KeyGenerator做一个后置增强,它具体的实现类是Jdbc3KeyGenerator


// org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator#processAfter
@Override
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
processBatch(ms, stmt, parameter);
}

// org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator#processBatch
public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
final String[] keyProperties = ms.getKeyProperties();
if (keyProperties == null || keyProperties.length == 0) {
return;
}
try (ResultSet rs = stmt.getGeneratedKeys()) {
final ResultSetMetaData rsmd = rs.getMetaData();
final Configuration configuration = ms.getConfiguration();
if (rsmd.getColumnCount() < keyProperties.length) {
// Error?
} else {
assignKeys(configuration, rs, rsmd, keyProperties, parameter);
}
} catch (Exception e) {
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
}
}

// org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator#assignKeys
private void assignKeys(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties,
Object parameter)
throws SQLException {
if (parameter instanceof ParamMap || parameter instanceof StrictMap) {
// Multi-param or single param with @Param
assignKeysToParamMap(configuration, rs, rsmd, keyProperties, (Map<String, ?>) parameter);
} else if (parameter instanceof ArrayList && !((ArrayList<?>) parameter).isEmpty()
&& ((ArrayList<?>) parameter).get(0) instanceof ParamMap) {
// Multi-param or single param with @Param in batch operation
assignKeysToParamMapList(configuration, rs, rsmd, keyProperties, (ArrayList<ParamMap<?>>) parameter);
} else {
// Single param without @Param
// 当前case会走这里
assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter);
}
}

// org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator#assignKeysToParam
private void assignKeysToParam(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
String[] keyProperties, Object parameter)
throws SQLException {
Collection<?> params = collectionize(parameter);
if (params.isEmpty()) {
return;
}
List<KeyAssigner> assignerList = new ArrayList<>();
for (int i = 0; i < keyProperties.length; i++) {
assignerList.add(new KeyAssigner(configuration, rsmd, i + 1, null, keyProperties[i]));
}
Iterator<?> iterator = params.iterator();
while (rs.next()) {
if (!iterator.hasNext()) {
throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, params.size()));
}
Object param = iterator.next();
assignerList.forEach(x -> x.assign(rs, param));
}
}

// org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator.KeyAssigner#assign
protected void assign(ResultSet rs, Object param) {
if (paramName != null) {
// If paramName is set, param is ParamMap
param = ((ParamMap<?>) param).get(paramName);
}
MetaObject metaParam = configuration.newMetaObject(param);
try {
if (typeHandler == null) {
if (metaParam.hasSetter(propertyName)) {
// 获取主键的类型
Class<?> propertyType = metaParam.getSetterType(propertyName);
// 获取主键类型处理器
typeHandler = typeHandlerRegistry.getTypeHandler(propertyType,
JdbcType.forCode(rsmd.getColumnType(columnPosition)));
} else {
throw new ExecutorException("No setter found for the keyProperty '" + propertyName + "' in '"
+ metaParam.getOriginalObject().getClass().getName() + "'.");
}
}
if (typeHandler == null) {
// Error?
} else {
// 获取主键的值
Object value = typeHandler.getResult(rs, columnPosition);
// 设置主键值
metaParam.setValue(propertyName, value);
}
} catch (SQLException e) {
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e,
e);
}
}

// com.mysql.cj.jdbc.result.ResultSetImpl#getObject(int, java.lang.Class<T>)
@Override
public <T> T getObject(int columnIndex, Class<T> type) throws SQLException {
// ...
else if (type.equals(Long.class) || type.equals(Long.TYPE)) {
checkRowPos();
checkColumnBounds(columnIndex);
return (T) this.thisRow.getValue(columnIndex - 1, this.longValueFactory);

}
// ...
}

image.png


最后可以看到这个自增id是在ResultSet的thisRow里面


然后后面的流程就是去解析这个字节数据获取这个long的id


就不往下赘述了


雪花算法ID


yml切换回雪花算法


mybatis-plus:
global-config:
db-config:
id-type: assign_id

在使用雪花算法的时候,也是会走到这个方法


// com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor#doUpdate
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
stmt = prepareStatement(handler, ms.getStatementLog(), false);
return stmt == null ? 0 : handler.update(stmt);
} finally {
closeStatement(stmt);
}
}

但是不同的是,执行完这一行之后,实体类的id字段就已经赋值上了


StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);

image.png


继续往下跟进


// org.apache.ibatis.session.Configuration#newStatementHandler
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}

// org.apache.ibatis.executor.statement.RoutingStatementHandler#RoutingStatementHandler
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

switch (ms.getStatementType()) {
// ...
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
// ...
}

}

最后跟进到一个构造器,会有一个processParameter的方法


// com.baomidou.mybatisplus.core.MybatisParameterHandler#MybatisParameterHandler
public MybatisParameterHandler(MappedStatement mappedStatement, Object parameter, BoundSql boundSql) {
this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
this.mappedStatement = mappedStatement;
this.boundSql = boundSql;
this.configuration = mappedStatement.getConfiguration();
this.sqlCommandType = mappedStatement.getSqlCommandType();
this.parameterObject = processParameter(parameter);
}

在这个方法里面会去增强参数


// com.baomidou.mybatisplus.core.MybatisParameterHandler#processParameter
public Object processParameter(Object parameter) {
/* 只处理插入或更新操作 */
if (parameter != null
&& (SqlCommandType.INSERT == this.sqlCommandType || SqlCommandType.UPDATE == this.sqlCommandType)) {
//检查 parameterObject
if (ReflectionKit.isPrimitiveOrWrapper(parameter.getClass())
|| parameter.getClass() == String.class) {
return parameter;
}
Collection<Object> parameters = getParameters(parameter);
if (null != parameters) {
parameters.forEach(this::process);
} else {
process(parameter);
}
}
return parameter;
}

// com.baomidou.mybatisplus.core.MybatisParameterHandler#process
private void process(Object parameter) {
if (parameter != null) {
TableInfo tableInfo = null;
Object entity = parameter;
if (parameter instanceof Map) {
Map<?, ?> map = (Map<?, ?>) parameter;
if (map.containsKey(Constants.ENTITY)) {
Object et = map.get(Constants.ENTITY);
if (et != null) {
entity = et;
tableInfo = TableInfoHelper.getTableInfo(entity.getClass());
}
}
} else {
tableInfo = TableInfoHelper.getTableInfo(parameter.getClass());
}
if (tableInfo != null) {
//到这里就应该转换到实体参数对象了,因为填充和ID处理都是争对实体对象处理的,不用传递原参数对象下去.
MetaObject metaObject = this.configuration.newMetaObject(entity);
if (SqlCommandType.INSERT == this.sqlCommandType) {
populateKeys(tableInfo, metaObject, entity);
insertFill(metaObject, tableInfo);
} else {
updateFill(metaObject, tableInfo);
}
}
}
}

最终生成id并赋值的操作是在populateKeys中


// com.baomidou.mybatisplus.core.MybatisParameterHandler#populateKeys
protected void populateKeys(TableInfo tableInfo, MetaObject metaObject, Object entity) {
final IdType idType = tableInfo.getIdType();
final String keyProperty = tableInfo.getKeyProperty();
if (StringUtils.isNotBlank(keyProperty) && null != idType && idType.getKey() >= 3) {
final IdentifierGenerator identifierGenerator = GlobalConfigUtils.getGlobalConfig(this.configuration).getIdentifierGenerator();
Object idValue = metaObject.getValue(keyProperty);
if (StringUtils.checkValNull(idValue)) {
if (idType.getKey() == IdType.ASSIGN_ID.getKey()) {
if (Number.class.isAssignableFrom(tableInfo.getKeyType())) {
metaObject.setValue(keyProperty, identifierGenerator.nextId(entity));
} else {
metaObject.setValue(keyProperty, identifierGenerator.nextId(entity).toString());
}
} else if (idType.getKey() == IdType.ASSIGN_UUID.getKey()) {
metaObject.setValue(keyProperty, identifierGenerator.nextUUID(entity));
}
}
}
}

在tableInfo中可以得知Id的类型


如果是雪花算法类型,那么生成雪花id;UUID同理


image.png


总结


insert之后,id被赋值到实体类的时机要根据具体情况具体讨论:


如果是自增类型的id,那么要在插入数据库完成之后,在ResultSet的ByteArrayRow中获取到这个id


如果是雪花算法id,那么在在插入数据库之前,会通过参数增强的方式,提前生成一个雪花id,然后赋值给实体类


作者:我爱果汁
来源:juejin.cn/post/7319541656399102002
收起阅读 »

第一次排查 Java 内存泄漏,别人觉得惊险为什么我觉得脸红害羞呢

今天前端一直在群里说,服务是不是又挂了?一直返回 503。我一听这不对劲,赶紧看了一眼 K8S 的 pod 状态,居然重启了4次。测试环境只有一个副本,所以赶紧把副本数给上调到了3个。 堵住前端的嘴,免得破坏我在老板心目中的形象,我害怕下次加薪名单没有我,而优...
继续阅读 »

今天前端一直在群里说,服务是不是又挂了?一直返回 503。我一听这不对劲,赶紧看了一眼 K8S 的 pod 状态,居然重启了4次。测试环境只有一个副本,所以赶紧把副本数给上调到了3个。


堵住前端的嘴,免得破坏我在老板心目中的形象,我害怕下次加薪名单没有我,而优化名单有我。


image.png


暂时安抚好前端之后我得立马看看哪里出问题了,先看看 K8S 为什么让这个容器领盒饭了。


Last State: Terminated 
Reason: OOMKilled

看起来是 JVM 胃口太大,被 K8S 嫌弃从而被赶走了。看看最近谁提交部署了,把人拉过来拷问一番。


代码摆出来分析,发现这小子每次使用http调用都会 new 一个连接池对象。一次业务请求使用了 6 次 http 调用,也就是会 new 6 个连接池对象。有可能是这里的问题,抓紧改了发上去测试看看。


image.png


不出意外的话又出意外了,上去之后也没缓解,那就不是这个问题了。要找到具体的原因还是不能瞎猜,得有专业的工具来进行分析才行。之前为了省点镜像空间,所以使用了 jre 的基础镜像。


image.png


总所周知,jre 只有一个运行环境,是没有开发工具的。所以我们得使用 jdk。你说我为省那点空间干什么?都想抽自己了。我们应该以 "让打靶老板花钱"为荣,以 "为打靶老板省钱"为耻。


image.png


把JDK准备好之后,就要开始我的第一次了。开始之前总是需要洗白白的,把一些影响心情的东西全部处理掉,就像这个 Skywalking,之前一直跟着我。但现在影响到我了,我得暂时把它放一边。不然他会在进行的过程中一直蹦出来烦人。


使用 Skywalking 需要设置此环境变量,每一次执行Java相关的命令都会执行 Skywalking 的一些操作,可以使用 unset 命令把环境变量临时置空。因为等我做完还是需要他来继续给我工作的。


unset JAVA_TOOL_OPTIONS

image.png


琐碎事处理完了之后,就得挑个技师才行。这行命令一把梭就会打印出所有 java 进程信息,这主要是为了获取到 vmid,也就是技师的编号。


jps -lv

root@xxx-ext-6464577d8-vvz2n:/app# jps -lv
608 sun.tools.jps.Jps -Denv.class.path=.:/usr/local/java/lib/rt.jar:/usr/local/java/lib/dt.jar:/usr/local/java/lib/tools.jar -Dapplication.home=/usr/local/openjdk-8 -Xms8m
7 /root/app/xxx-ext.jar -javaagent:/skywalking/agent/skywalking-agent.jar -Dfile.encoding=UTF-8 -Xms1024m -Xmx2048m
568 sun.tools.jstat.Jstat -javaagent:/skywalking/agent/skywalking-agent.jar -Denv.class.path=.:/usr/local/java/lib/rt.jar:/usr/local/java/lib/dt.jar:/usr/local/java/lib/tools.jar -Dapplication.home=/usr/local/openjdk-8 -Xms8m

这里总共查到3个Java进程,608 jps7 xxx-ext568 jstat。中间这个 7 号技师 xxx-ext 就是我相中的,我将会把第一次交给他。


image.png


选完技师就正式开始了,过程中要时刻关心对方的身体状态。隔几秒钟就问一下状态怎么样?为了方便时刻了解对方的身体状态,可以用这个命令每隔5s就问一下。如果你对自己的能力有信心可以把间隔设置短一些。


# jstat -gcutil {vmid} {间隔毫秒}
jstat -gcutil 7 5000

root@xxx-ext-6464577d8-vvz2n:/app# jstat -gcutil 7 5000
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
99.96 0.00 100.00 100.00 95.85 94.74 178 8.047 8 3.966 12.012
99.97 0.00 100.00 100.00 95.50 94.33 178 8.047 11 8.072 16.118
99.99 0.00 100.00 100.00 95.51 94.33 178 8.047 14 12.408 20.455
100.00 0.00 100.00 100.00 95.48 94.30 178 8.047 18 17.140 25.187
100.00 0.00 100.00 100.00 95.48 94.30 178 8.047 23 22.730 30.776
100.00 0.00 100.00 100.00 95.48 94.30 178 8.047 27 27.035 35.082
100.00 0.00 100.00 100.00 95.49 94.30 178 8.047 32 32.614 40.661

虽然是第一次,但对方给回来的信息务必要了然于胸。知己知彼胜券在握,所以要把下面的心法记住。这会影响我们下一步的动作。


S0/S1 是Survivor区空间使用率
E 是新生代空间使用率
O 是老年代空间使用率
YGC 是 Young GC 次数
YGCT 是 Young GC 总耗时
FGC 是 Full GC 次数
FGCT 是 Full GC 总耗时

当对方的状态到达一个关键点的时候,一般是老年代满,或者是新生代满,这就表示对方快溢出来了。像我提供的这个示例,E 和 O 的使用率都是100,就说明对方不仅满了,还快噶了。我们得赶紧把这个关键时刻详细探究一下,看看是哪个对象让对方感觉到满的。


image.png


用这个命令查询对方体内对象占用排名,不用贪多,前10个就绰绰有余了。你能把前10个全部弄清楚就够牛了。


jmap -histo:live 7 | head -n 10

root@xxx-ext-6464577d8-vvz2n:/app# jmap -histo:live 7 | head -n 10

num #instances #bytes class name
----------------------------------------------
1: 454962 1852234368 [C
2: 1773671 56757472 java.util.HashMap$Node
3: 881987 30188352 [B
4: 55036 19781352 [Ljava.util.HashMap$Node;
5: 857235 13715760 java.lang.Integer
6: 852094 13633504 com.knuddels.jtokkit.ByteArrayWrapper
7: 454195 10900680 java.lang.String
8: 104386 6436624 [Ljava.lang.Object;
9: 191593 6130976 java.util.concurrent.ConcurrentHashMap$Node
10: 63278 5568464 java.lang.reflect.Method

可以看到对方已经在边缘了,我们要抓紧分析了。我提供的这个示例,排名前三分别是 [Cjava.util.HashMap$Node[B[C 表示字符数组,[B 表示字节数组。看来对方偏爱 [C,占用差不多1.7G,需要重点分析它。


这一步就到了十字路口,关键点在于我们能不能从这里分析得到对方偏爱的对象,从而定位到代码中的问题点。一旦我们定位到代码中的问题点,那就证明对方已经被我们拿捏了,流程结束。


那就开始分析吧,先看看最近哪个瘪犊子提交了代码,把他拉过来。然后看最近改动的代码哪里和 [C 相关,一般是 List<String>StringBuffer 这类对象。


image.png


我没想到小丑竟是我自己🤡,有一个接口入参是一个 List<ID>,当这个 list 传了空的时候,就会把库里的所有数据都查出来。


破案了,这次把对方完全拿捏了,流程结束。


image.png


如果上一步无法拿捏,那就不要讲武德了。把对方的一举一动dump下来,最终导出成堆快照来分析。



dump 时间取决于数据量



jmap -dump:live,format=b,file=heap.hprof 7

root@xxx-ext-6464577d8-vvz2n:/app# jmap -dump:live,format=b,file=heap.hprof 7
Dumping heap to /app/heap.hprof ...
Heap dump file created

将dump文件从pod中复制出来


kubectl cp <ns>/<pod>:/app/heap.hprof ./heap.hprof

kubectl cp test/xxx-ext-6464577d8-vvz2n:/app/heap.hprof ./heap.hprof

我摊牌了,这一步我压根没做。


image.png


当我想从pod中把对快照复制出来的时候磁盘空间不够,然后pod就被 K8S 这个暴脾气干了,只剩下我颤抖的手无力地放在键盘上。


Ref


juejin.cn/post/700622…


作者:纸仓
来源:juejin.cn/post/7426189830562906149
收起阅读 »

震惊!🐿浏览器居然下毒!

web
发生什么事了 某天,我正在愉快的摸鱼,然后我看到测试给我发了条消息,说我们这个系统在UC浏览器中有问题,没办法操作,点了经常没反应(测试用得iPhone14,是一个h5的项目)。我直接懵了,这不是都测了好久了吗,虽然不是在uc上测得,chrome、safari...
继续阅读 »

发生什么事了


某天,我正在愉快的摸鱼,然后我看到测试给我发了条消息,说我们这个系统在UC浏览器中有问题,没办法操作,点了经常没反应(测试用得iPhone14,是一个h5的项目)。我直接懵了,这不是都测了好久了吗,虽然不是在uc上测得,chrome、safari、自带浏览器等,都没这个问题,代码应该是没问题的,uc上为啥会没有反应呢?难道是有什么隐藏的bug,需要一定的操作顺序才能触发?我就去找了测试,让他重新操作一下,看看是啥样的没反应。结果就是,正常进入列表页(首页),正常点某一项,正常进入详情页,然后点左上角返回,没反应。我上手试了下,确实,打开vconsole看了下,也没有报错。在uc上看起来还是必现的,我都麻了,这能是啥引起的啊。


找问题


在其他浏览器上都是好好的,uc上也不报错,完全看不出来代码有啥bug,完全没有头绪啊!那怎么办,刷新看看:遇事不决,先刷新,还不行就清空缓存刷新。刷新了之后,哎,好了!虽然不知道是什么问题,但现在已经好了,就当作遇到了灵异事件,我就去做其他事了。


过了一会,测试来找我了,说又出现了,不止是详情页,进其他页面也返回不了。这就难受住了呀,说明肯定是有问题的,只是还没找到原因。我就只好打开vconsole,一遍一遍的进入详情页点返回;刷新再进;清掉缓存,再进。


然后,我就发现,network中,出现了一个没有见过的请求


20240906-222447.png


20240906-222456.png
根据track、collect这些单词来判断,这应该是uc在跟踪、记录某些操作,requestType还是个ping;我就在想,难道是这个请求的问题?但是请求为啥会导致,我页面跳转产生问题?然后我又看到了intercept(拦截)、pushState(history添加记录)拦截了pushState?

这个项目确实使用的是history路由,问题也确实出在路由跳转的时候;而且出现问题的时候,路由跳转,浏览器地址栏中的地址是没有变化的,返回就g了(看起来是后退没有反应,实际是前进时G了)。这样看,uc确实拦截了pushState的操作。那它是咋做到的?


原来如此


然后,我想起来,前段时间在掘金上看到了一篇,讲某些第三方cdn投毒的事情,那么uc是不是在我们不知情的情况下,改了我们的代码。然后我切到了vconsole的element,展开head,发现了一个不属于我们项目的script,外链引入了一段js,就挂在head的最顶上。通过阅读,发现它在window的history上加了点料覆写了forward和pushState(forward和pushState是继承来的方法)

正常的history应该是这样:


image.png
复写的类似这样:


image.png
当然,有些系统或框架,为了实现某些功能,比如实现触发popstate的效果,也会复写

但uc是纯纯的为了记录你的操作,它这玩意主要还有bug,会导致路由跳转出问题,真是闹麻了


如何做


删掉就好了,只要删掉uc添加的,当我们调用相关方法时,history就会去继承里找


// 判断是否是uc浏览器
if (navigator.userAgent.indexOf('UCBrowser') > -1) {
if (history.hasOwnProperty('pushState')) {
delete window.history.forward
delete window.history.pushState
}
// 找到注入的script
const ucScript = document.querySelector('script[src*="ucbrowser_script"]')
if (ucScript) {
document.head.removeChild(ucScript)}
}
}

吐槽


你说你一个搞浏览器的,就不能在底层去记录用户行为吗,还不容易被发现。主要是你这玩意它有bug呀,这不是更容易被发现吗。(这是23年11月份遇到的问题,当时产品要求在qq/百度/uc这些浏览器上也都测一下才发现的,现在记录一下,希望能帮助到其他同学)


作者:忍者扔飞镖
来源:juejin.cn/post/7411358506048766006
收起阅读 »

SpringBoot 实战:文件上传之秒传、断点续传、分片上传

文件上传功能几乎是每个 Web 应用不可或缺的一部分。无论是个人博客中的图片上传,还是企业级应用中的文档管理,文件上传都扮演着至关重要的角色。今天,松哥和大家来聊聊文件上传中的几个高级玩法——秒传、断点续传和分片上传。 一 文件上传的常见场景 在日常开发中,文...
继续阅读 »

文件上传功能几乎是每个 Web 应用不可或缺的一部分。无论是个人博客中的图片上传,还是企业级应用中的文档管理,文件上传都扮演着至关重要的角色。今天,松哥和大家来聊聊文件上传中的几个高级玩法——秒传、断点续传和分片上传。


一 文件上传的常见场景


在日常开发中,文件上传的场景多种多样。比如,在线教育平台上的视频资源上传,社交平台上的图片分享,以及企业内部的知识文档管理等。这些场景对文件上传的要求也各不相同,有的追求速度,有的注重稳定性,还有的需要考虑文件大小和安全性。因此,针对不同需求,我们有了秒传、断点续传和分片上传等解决方案。


二 秒传、断点上传与分片上传


秒传


秒传,顾名思义,就是几乎瞬间完成文件上传的过程。其实现原理是通过计算文件的哈希值(如 MD5 或 SHA-1),然后将这个唯一的标识符发送给服务器。如果服务器上已经存在相同的文件,则直接返回成功信息,避免了重复上传。这种方式不仅节省了带宽,也大大提高了用户体验。


断点续传


断点续传是指在网络不稳定或者用户主动中断上传后,能够从上次中断的地方继续上传,而不需要重新开始整个过程。这对于大文件上传尤为重要,因为它可以有效防止因网络问题导致的上传失败,同时也能节约用户的流量和时间。


分片上传


分片上传则是将一个大文件分割成多个小块分别上传,最后再由服务器合并成完整的文件。这种做法的好处是可以并行处理多个小文件,提高上传效率;同时,如果某一部分上传失败,只需要重传这一部分,不影响其他部分。


三 秒传实战


后端实现


在 SpringBoot 项目中,我们可以使用 MessageDigest 类来计算文件的 MD5 值,然后检查数据库中是否存在该文件。


@RestController
@RequestMapping("/file")
public class FileController {
@Autowired
FileService fileService;

@PostMapping("/upload1")
public ResponseEntity<String> secondUpload(@RequestParam(value = "file",required = false) MultipartFile file,@RequestParam(required = false,value = "md5") String md5) {
try {
// 检查数据库中是否已存在该文件
if (fileService.existsByMd5(md5)) {
return ResponseEntity.ok("文件已存在");
}
// 保存文件到服务器
file.transferTo(new File("/path/to/save/" + file.getOriginalFilename()));
// 保存文件信息到数据库
fileService.save(new FileInfo(file.getOriginalFilename(), DigestUtils.md5DigestAsHex(file.getInputStream())));
return ResponseEntity.ok("上传成功");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("上传失败");
}
}
}

前端调用


前端可以通过 JavaScript 的 FileReader API 读取文件内容,通过 spark-md5 计算 MD5 值,然后发送给后端进行校验。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>秒传</title>
<script src="spark-md5.js"></script>
</head>
<body>
<input type="file" id="fileInput" />
<button onclick="startUpload()">开始上传</button>
<hr>
<script>
async function startUpload() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert("请选择文件");
return;
}

const md5 = await calculateMd5(file);
const formData = new FormData();
formData.append('md5', md5);

const response = await fetch('/file/upload1', {
method: 'POST',
body: formData
});

const result = await response.text();
if (response.ok) {
if (result != "文件已存在") {
// 开始上传文件
}
} else {
console.error("上传失败: " + result);
}
}

function calculateMd5(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const spark = new SparkMD5.ArrayBuffer();
spark.append(reader.result);
resolve(spark.end());
};
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(file);
});
}
</script>
</body>
</html>

前端分为两个步骤:



  1. 计算文件的 MD5 值,计算之后发送给服务端确定文件是否存在。

  2. 如果文件已经存在,则不需要继续上传文件;如果文件不存在,则开始上传文件,上传文件和 MD5 校验请求类似,上面的案例代码中我就没有重复演示了,松哥在书里和之前的课程里都多次讲过文件上传,这里不再啰嗦。


四 分片上传实战


分片上传关键是在前端对文件切片,比如一个 10MB 的文件切为 10 份,每份 1MB。每次上传的时候,需要多一个参数记录当前上传的文件切片的起始位置。


比如一个 10MB 的文件,切为 10 份,每份 1MB,那么:



  • 第 0 片,从 0 开始,一共是 1024*1024 个字节。

  • 第 1 片,从 1024*1024 开始,一共是 1024*1024 个字节。

  • 第 2 片...


把这个搞懂,后面的代码就好理解了。


后端实现


private static final String UPLOAD_DIR = System.getProperty("user.home") + "/uploads/";
/**
* 上传文件到指定位置
*
* @param file 上传的文件
* @param start 文件开始上传的位置
* @return ResponseEntity<String> 上传结果
*/

@PostMapping("/upload2")
public ResponseEntity<String> resumeUpload(@RequestParam("file") MultipartFile file, @RequestParam("start") long start,@RequestParam("fileName") String fileName) {
try {
File directory = new File(UPLOAD_DIR);
if (!directory.exists()) {
directory.mkdirs();
}
File targetFile = new File(UPLOAD_DIR + fileName);
RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "rw");
FileChannel channel = randomAccessFile.getChannel();
channel.position(start);
channel.transferFrom(file.getResource().readableChannel(), start, file.getSize());
channel.close();
randomAccessFile.close();
return ResponseEntity.ok("上传成功");
} catch (Exception e) {
System.out.println("上传失败: "+e.getMessage());
return ResponseEntity.status(500).body("上传失败");
}
}

后端每次处理的时候,需要先设置文件的起始位置。


前端调用


前端需要将文件切分成多个小块,然后依次上传。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>分片示例</title>
</head>
<body>
<input type="file" id="fileInput" />
<button onclick="startUpload()">开始上传</button>

<script>
async function startUpload() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert("请选择文件");
return;
}

const filename = file.name;
let start = 0;

uploadFile(file, start);
}

async function uploadFile(file, start) {
const chunkSize = 1024 * 1024; // 每个分片1MB
const total = Math.ceil(file.size / chunkSize);

for (let i = 0; i < total; i++) {
const chunkStart = start + i * chunkSize;
const chunkEnd = Math.min(chunkStart + chunkSize, file.size);
const chunk = file.slice(chunkStart, chunkEnd);

const formData = new FormData();
formData.append('file', chunk);
formData.append('start', chunkStart);
formData.append('fileName', file.name);

const response = await fetch('/file/upload2', {
method: 'POST',
body: formData
});

const result = await response.text();
if (response.ok) {
console.log(`分片 ${i + 1}/${total} 上传成功`);
} else {
console.error(`分片 ${i + 1}/${total} 上传失败: ${result}`);
break;
}
}
}
</script>
</body>
</html>

五 断点续传实战


断点续传的技术原理类似于分片上传。


当文件已经上传了一部分之后,断了需要重新开始上传。


那么我们的思路是这样的:



  1. 前端先发送一个请求,检查要上传的文件在服务端是否已经存在,如果存在,目前大小是多少。

  2. 前端根据已经存在的大小,继续上传文件即可。


后端案例


先来看后端检查的接口,如下:


@GetMapping("/check")
public ResponseEntity<Long> checkFile(@RequestParam("filename") String filename) {
File file = new File(UPLOAD_DIR + filename);
if (file.exists()) {
return ResponseEntity.ok(file.length());
} else {
return ResponseEntity.ok(0L);
}
}

如果文件存在,则返回已经存在的文件大小。


如果文件不存在,则返回 0,表示前端从头开始上传该文件。


前端调用


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>断点续传示例</title>
</head>
<body>
<input type="file" id="fileInput"/>
<button onclick="startUpload()">开始上传</button>

<script>
async function startUpload() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert("请选择文件");
return;
}

const filename = file.name;
let start = await checkFile(filename);

uploadFile(file, start);
}

async function checkFile(filename) {
const response = await fetch(`/file/check?filename=${filename}`);
const start = await response.json();
return start;
}

async function uploadFile(file, start) {
const chunkSize = 1024 * 1024; // 每个分片1MB
const total = Math.ceil((file.size - start) / chunkSize);

for (let i = 0; i < total; i++) {
const chunkStart = start + i * chunkSize;
const chunkEnd = Math.min(chunkStart + chunkSize, file.size);
const chunk = file.slice(chunkStart, chunkEnd);

const formData = new FormData();
formData.append('file', chunk);
formData.append('start', chunkStart);
formData.append('fileName', file.name);

const response = await fetch('/file/upload2', {
method: 'POST',
body: formData
});

const result = await response.text();
if (response.ok) {
console.log(`分片 ${i + 1}/${total} 上传成功`);
} else {
console.error(`分片 ${i + 1}/${total} 上传失败: ${result}`);
break;
}
}
}
</script>
</body>
</html>

这个案例实际上是一个断点续传+分片上传的案例,相关知识点并不难,小伙伴们可以自行体会下。


六 总结


好了,以上就是关于文件上传中秒传、断点续传和分片上传的实战分享。通过这些技术的应用,我们可以极大地提升文件上传的效率和稳定性,改善用户体验。希望各位小伙伴在自己的项目中也能灵活运用这些技巧,解决实际问题。


本文完整案例:github.com/lenve/sprin…


作者:江南一点雨
来源:juejin.cn/post/7436026758438453274
收起阅读 »

MyBatis-Plus 效能提升秘籍:掌握这些注解,事半功倍!

MyBatis-Plus是一个功能强大的MyBatis扩展插件,它提供了许多便捷的注解,让我们在开发过程中能够更加高效地完成数据库操作,本文将带你一一了解这些注解,并通过实例来展示它们的魅力。 一、@Tablename注解 这个注解用于指定实体类对应的数据库表...
继续阅读 »

MyBatis-Plus是一个功能强大的MyBatis扩展插件,它提供了许多便捷的注解,让我们在开发过程中能够更加高效地完成数据库操作,本文将带你一一了解这些注解,并通过实例来展示它们的魅力。


一、@Tablename注解


这个注解用于指定实体类对应的数据库表名。如果你的表名和实体类名不一致,就需要用到它:


@TableName("user_info")
public class UserInfo {
// 类的属性和方法
}

在上述代码中,即使实体类名为UserInfo,但通过@TableName注解,我们知道它对应数据库中的"user_info"表。


二、@Tableld注解


每个数据库表都有主键,@TableId注解用于标识实体类中的主键属性。通常与@TableName配合使用,确保主键映射正确。


AUTO(0),
NONE(1),
INPUT(2),
ASSIGN_ID(3),
ASSIGN_UUID(4),
/** @deprecated */
@Deprecated
ID_WORKER(3),
/** @deprecated */
@Deprecated
ID_WORKER_STR(3),
/** @deprecated */
@Deprecated
UUID(4);

Description



  • INPUT 如果开发者没有手动赋值,则数据库通过自增的方式给主键赋值,如果开发者手动赋值,则存入该值。

  • AUTO 默认就是数据库自增,开发者无需赋值。

  • ASSIGN_ID MP 自动赋值,雪花算法。

  • ASSIGN_UUID 主键的数据类型必须是 String,自动生成 UUID 进行赋值。


// 自己赋值
//@TableId(type = IdType.INPUT)
// 默认使用的雪花算法,长度比较长,所以使用Long类型,不用自己赋值
@TableId
private Long id;

测试


@Test
void save(){
// 由于id加的有注解,这里就不用赋值了
Student student = new Student();
student.setName("天明");
student.setAge(18);
mapper.insert(student);
}

Description


雪花算法


雪花算法是由Twitter公布的分布式主键生成算法,它能够保证不同表的主键的不重复性,以及相同表的主键的有序性。


核心思想:



  • 长度共64bit(一个long型)。

  • 首先是一个符号位,1bit标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0。

  • 41bit时间截(毫秒级),存储的是时间截的差值(当前时间截 - 开始时间截),结果约等于69.73年。

  • 10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID,可以部署在1024个节点)。

  • 12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID)。


Description


优点:  整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞,并且效率较高。


三、@TableField注解


当你的实体类属性名与数据库字段名不一致时,@TableField注解可以帮助你建立二者之间的映射关系。



  • 映射非主键字段,value 映射字段名;

  • exist 表示是否为数据库字段 false,如果实体类中的成员变量在数据库中没有对应的字段,则可以使用 exist,VO、DTO;

  • select 表示是否查询该字段;

  • fill 表示是否自动填充,将对象存入数据库的时候,由 MyBatis Plus 自动给某些字段赋值,create_time、update_time。


Description


自动填充


1)给表添加 create_time、update_time 字段。


Description


2)实体类中添加成员变量。


package com.md.entity;

import com.baomidou.mybatisplus.annotation.*;
import com.md.enums.StatusEnum;
import lombok.Data;
import java.util.Date;

@Data
@TableName(value = "student")
public class Student {
@TableId
private Long id;

// 当该字段名称与数据库名字不一致
@TableField(value = "name")
private String name;

// 不查询该字段
@TableField(select = false)
private Integer age;

// 当数据库中没有该字段,就忽略
@TableField(exist = false)
private String gender;

// 第一次添加填充
@TableField(fill = FieldFill.INSERT)
private Date createTime;

// 第一次添加的时候填充,但之后每次更新也会进行填充
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;

}

3)创建自动填充处理器。


注意:不要忘记添加 @Component 注解。


package com.md.handler;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
* @author md
* @Desc 对实体类中使用的自动填充注解进行编写
* @date 2020/10/26 17:29
*/

// 加入注解才能生效
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("createTime", new Date(), metaObject);
this.setFieldValByName("updateTime", new Date(), metaObject);
}

@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", new Date(), metaObject);
}
}

4)测试


@Test
void save(){
// 由于id加的有注解,这里就不用赋值了
Student student = new Student();
student.setName("韩立");
student.setAge(11);
// 时间自动填充
mapper.insert(student);
}

Description


5)更新


当该字段发生变化的时候时间会自动更新。


@Test
void update(){
Student student = mapper.selectById(1001);
student.setName("韩信");
mapper.updateById(student);
}

Description


四、@TableLogic注解


在很多应用中,数据并不是真的被删除,而是标记为已删除状态。@TableLogic注解用于标识逻辑删除字段,通常配合逻辑删除功能使用。


1、逻辑删除


物理删除:  真实删除,将对应数据从数据库中删除,之后查询不到此条被删除的数据。


逻辑删除:  假删除,将对应数据中代表是否被删除字段的状态修改为“被删除状态”,之后在数据库中仍旧能看到此条数据记录。


使用场景:  可以进行数据恢复。


2、实现逻辑删除


step1:  数据库中创建逻辑删除状态列。

Description


step2:  实体类中添加逻辑删除属性。


@TableLogic
@TableField(value = "is_deleted")
private Integer deleted;

3、测试


测试删除:  删除功能被转变为更新功能。


-- 实际执行的SQL
update user set is_deleted=1 where id = 1 and is_deleted=0

测试查询:  被逻辑删除的数据默认不会被查询。


-- 实际执行的SQL
select id,name,is_deleted from user where is_deleted=0


你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是现在还是免费的!点这里即可查看!



五、@Version注解


乐观锁是一种并发控制策略,@Version注解用于标识版本号字段,确保数据的一致性。


乐观锁


Description

标记乐观锁,通过 version 字段来保证数据的安全性,当修改数据的时候,会以 version 作为条件,当条件成立的时候才会修改成功。


version = 2



  • 线程1:update … set version = 2 where version = 1

  • 线程2:update … set version = 2 where version = 1


1.数据库表添加 version 字段,默认值为 1。


2.实体类添加 version 成员变量,并且添加 @Version。


package com.md.entity;

import com.baomidou.mybatisplus.annotation.*;
import com.md.enums.StatusEnum;
import lombok.Data;
import java.util.Date;

@Data
@TableName(value = "student")
public class Student {
@TableId
private Long id;
@TableField(value = "name")
private String name;
@TableField(select = false)
private Integer age;
@TableField(exist = false)
private String gender;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;

@Version
private Integer version; //版本号

}

3.注册配置类


在 MybatisPlusConfig 中注册 Bean。


package com.md.config;

import com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @author md
* @Desc
* @date 2020/10/26 20:42
*/

@Configuration
public class MyBatisPlusConfig {
/**
* 乐观锁
*/

@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor(){
return new OptimisticLockerInterceptor();
}
}

六、@EnumValue注解


mp框架对枚举进行处理的一个注解。


使用场景:  创建枚举类,在需要存储数据库的属性上添加@EnumValue注解。


public enum SexEnum {

MAN(1, "男"),
WOMAN(2, "女");

@EnumValue
private Integer key;
}

MyBatis-Plus的注解是开发者的好帮手,它们简化了映射配置,提高了开发效率。希望以上的介绍能帮助新手朋友们快速理解和运用这些常用注解,让你们在MyBatis-Plus的世界里游刃有余!记得实践是最好的学习方式,快去动手试试吧!


作者:云端源想
来源:juejin.cn/post/7340471458949169215
收起阅读 »

Java 语法糖,你用过几个?

你好,我是猿java。 这篇文章,我们来聊聊 Java 语法糖。 什么是语法糖? 语法糖(Syntactic Sugar)是编程语言中的一种设计概念,它指的是在语法层面上对某些操作提供更简洁、更易读的表示方式。这种表示方式并不会新增语言的功能,而只是使代码更简...
继续阅读 »

你好,我是猿java。


这篇文章,我们来聊聊 Java 语法糖。


什么是语法糖?


语法糖(Syntactic Sugar)是编程语言中的一种设计概念,它指的是在语法层面上对某些操作提供更简洁、更易读的表示方式。这种表示方式并不会新增语言的功能,而只是使代码更简洁、更直观,便于开发者理解和维护。


语法糖的作用:



  • 提高代码可读性:语法糖可以使代码更加贴近自然语言或开发者的思维方式,从而更容易理解。

  • 减少样板代码:语法糖可以减少重复的样板代码,使得开发者可以更专注于业务逻辑。

  • 降低出错率:简化的语法可以减少代码量,从而降低出错的概率。


因此,语法糖不是 Java 语言特有的,它是很多编程语言设计中的一些语法特性,这些特性使代码更加简洁易读,但并不会引入新的功能或能力。


那么,Java中有哪些语法糖呢?


Java 语法糖


1. 自动装箱与拆箱


自动装箱和拆箱 (Autoboxing and Unboxing)是 Java 5 引入的特性,用于在基本数据类型和它们对应的包装类之间自动转换。


// 自动装箱
Integer num = 10; // 实际上是 Integer.valueOf(10)

// 自动拆箱
int n = num; // 实际上是 num.intValue()

2. 增强型 for 循环


增强型 for 循环(也称为 for-each 循环)用于遍历数组或集合。


int[] numbers = {1, 2, 3, 4, 5};
for (int number : numbers) {
System.out.println(number);
}

3. 泛型


泛型(Generics)使得类、接口和方法可以操作指定类型的对象,提供了类型安全的检查和消除了类型转换的需要。


List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0); // 不需要类型转换

4. 可变参数


可变参数(Varargs)允许在方法中传递任意数量的参数。


public void printNumbers(int... numbers) {
for (int number : numbers) {
System.out.println(number);
}
}

printNumbers(1, 2, 3, 4, 5);

5. try-with-resources


try-with-resources 语句用于自动关闭资源,实现了 AutoCloseable 接口的资源会在语句结束时自动关闭。


try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
System.out.println(br.readLine());
} catch (IOException e) {
e.printStackTrace();
}

6. Lambda 表达式


Lambda 表达式是 Java 8 引入的特性,使得可以使用更简洁的语法来实现函数式接口(只有一个抽象方法的接口)。


List<String> list = Arrays.asList("a", "b", "c");
list.forEach(s -> System.out.println(s));

7. 方法引用


方法引用(Method References)是 Lambda 表达式的一种简写形式,用于直接引用已有的方法。


list.forEach(System.out::println);

8. 字符串连接


从 Java 5 开始,Java 编译器会将字符串的连接优化为 StringBuilder 操作。


String message = "Hello, " + "world!"; // 实际上是 new StringBuilder().append("Hello, ").append("world!").toString();

9. Switch 表达式


Java 12 引入的 Switch 表达式使得 Switch 语句更加简洁和灵活。


int day = 5;
String dayName = switch (day) {
case 1 -> "Sunday";
case 2 -> "Monday";
case 3 -> "Tuesday";
case 4 -> "Wednesday";
case 5 -> "Thursday";
case 6 -> "Friday";
case 7 -> "Saturday";
default -> "Invalid day";
};

10. 类型推断 (Type Inference)


Java 10 引入了局部变量类型推断,通过 var 关键字来声明变量,编译器会自动推断变量的类型。


var list = new ArrayList<String>();
list.add("Hello");

这些语法糖使得 Java 代码更加简洁和易读,但需要注意的是,它们并不会增加语言本身的功能,只是对已有功能的一种简化和封装。


总结


本文,我们介绍了 Java 语言中的一些语法糖,从上面的例子可以看出,Java 语法糖只是一些简化的语法,可以使代码更简洁易读,而本身并不增加新的功能。


学习交流


如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。


作者:猿java
来源:juejin.cn/post/7412672643633791039
收起阅读 »