注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

电竞直播系统源码出售,CDN加速+多端同步,打造下一个Twitch!

东莞梦幻网络科技研发的游戏直播源码解决方案,采用经过市场验证的成熟技术架构,支持72小时快速部署上线。该套系统完整集成多路直播推流引擎与低延迟传输协议,同时配备弹幕互动系统、虚拟礼物交易体系、UGC短视频发布模块以及社交化社区论坛组件,私聊功能采用分布式消息队...
继续阅读 »

东莞梦幻网络科技研发的游戏直播源码解决方案,采用经过市场验证的成熟技术架构,支持72小时快速部署上线。该套系统完整集成多路直播推流引擎与低延迟传输协议,同时配备弹幕互动系统、虚拟礼物交易体系、UGC短视频发布模块以及社交化社区论坛组件,私聊功能采用分布式消息队列确保消息必达。这套全功能解决方案能有效缩短产品上市周期,规避自主研发的技术不确定性,大幅减少人力与时间投入成本。

更令人惊喜的是,东莞梦幻网络科技的技术团队通过独创的智能编解码算法,将直播延迟控制在200毫秒以内,让主播与观众的互动几乎同步进行。配合HDR10+高动态范围成像技术,无论是游戏直播的激烈对战,还是才艺表演的细腻呈现,都能展现令人惊艳的视觉效果。

东莞梦幻网络科技特别设计的社交互动系统,不仅包含常规的弹幕互动,还创新性地加入了3D虚拟礼物系统。当观众送出特定礼物时,直播间会触发全屏AR特效,这种沉浸式互动体验让用户留存率提升65%以上。平台独创的"热力值"算法,还能智能匹配兴趣相投的用户,构建真正有温度的直播社区。

在商业变现方面,东莞梦幻网络科技提供了业内领先的收益分成模式。平台独创的"阶梯式分成"机制,让主播收入随着粉丝增长自动升级,最高可获得85%的收益分成。同时配备智能数据分析后台,帮助运营者精准掌握每场直播的ROI表现,让流量变现更加高效透明。

东莞梦幻网络科技的模块化架构支持"热插拔"式功能扩展,平台运营者可以根据发展阶段,自由组合电商带货、知识付费、赛事竞猜等20+功能模块。特有的"皮肤工厂"让每个合作方都能轻松打造独具品牌特色的直播界面,从LOGO到主题色均可一键更换。

为确保全终端体验一致性,东莞梦幻网络科技采用自适应流媒体传输技术,能智能识别用户设备性能和网络环境,自动匹配最佳分辨率。从4K大屏到移动端小屏,画面都能保持完美比例,彻底解决传统直播平台常见的画面拉伸问题。

面对突发流量,东莞梦幻网络科技的分布式节点网络展现出强大优势。全球部署的2000+边缘计算节点,配合智能路由算法,确保即使百万用户同时在线,每个观众仍能获得低于1%的卡顿率体验。平台独有的"灾备自愈"机制,可在服务器异常时30秒内自动切换备用线路,真正实现24小时稳定播出。

在视觉呈现方面,东莞梦幻网络科技提供多样化的主题模板库,从未来科技感到二次元动漫风,再到北欧极简主义,全面覆盖各类用户群体的审美偏好。平台独创的自定义上传功能允许企业主导入专属品牌元素,包括但不限于企业标识、VI标准色系及个性化背景素材,真正实现品牌形象的差异化呈现。

交互设计层面,平台赋予用户高度自由的界面调控权限,无论是视频窗口比例缩放还是直播间模块重组,均可根据观众使用偏好进行灵活配置。双端自适应技术确保从智能手机到台式机的全终端适配,让不同设备的用户都能享受一致的优质观看体验。

技术开放性方面,东莞梦幻网络科技采用彻底的开源策略,所有业务逻辑层代码、数据库结构及前端组件均未做任何加密处理。开发者可基于现有架构深度改造功能模块,更可通过标准化API与支付系统、社交媒介及营销插件实现无缝对接,为商业模式的创新提供无限可能。

⚡ 在系统稳定性上,智能CDN节点与分布式服务器集群的协同工作,使平台具备百万级并发承载能力。七层防护体系实时抵御DDoS攻击、SQL注入等网络安全威胁,为直播数据的传输安全构筑多重防线。

东莞梦幻网络科技组建了专职的版本迭代团队,不仅定期推出包含虚拟礼物连麦、AI智能弹幕等前沿功能更新,更建立了用户需求快速响应机制,确保平台功能始终与市场趋势同步进化。
------

收起阅读 »

城市技术论坛首次登陆欧洲!OpenHarmony启航海外开源生态新征程

当地时间2025年3月30日下午,欧洲首场OpenHarmony城市技术论坛于荷兰鹿特丹圆满落下帷幕。本次活动以OpenHarmony技术创新与生态实践为主题,聚焦操作系统领域的最新创新以及与OpenHarmony项目相关的技术研究和生态实践。嘉宾合影本次活动...
继续阅读 »

当地时间2025年3月30日下午,欧洲首场OpenHarmony城市技术论坛于荷兰鹿特丹圆满落下帷幕。本次活动以OpenHarmony技术创新与生态实践为主题,聚焦操作系统领域的最新创新以及与OpenHarmony项目相关的技术研究和生态实践。


嘉宾合影

本次活动由OpenHarmony项目群技术指导委员会(TSC)主办,华为德累斯顿研究所、欧洲标准与产业发展部、阿姆斯特丹研究所等协办,邀请多位来自欧洲本土以及国内的资深技术专家和高校学者出席论坛并作技术报告。现场与会开发者近百人,线上直播观看人数累计超过2.1万。


现场合影

OpenHarmony TSC主席、ACM Fellow、IEEE Fellow、ACM SIGOPS主席陈海波为本次活动致开场词并作《OpenHarmony Technology Innovation and Ecosystem Practice》主题报告。陈海波表示:软件技术在全球经济增长中发挥了重要作用,智能终端操作系统是数字化和智能化转型的关键基础。OpenHarmony项目群技术指导委员会汇聚产业界、学术界和研究机构的力量,通过与国内超过100所高校成立OpenHarmony技术俱乐部和开发者协会,构建OpenHarmony技术大会、城市技术论坛等有影响力的产业-学术-研究交流平台,以及开展技术课题研究、竞赛、线上技术直播和项目孵化等,持续推动OpenHarmony开源技术和人才生态的繁荣发展。在报告中,陈海波强调了智能终端操作系统的重要性以及影响其发展的要素,阐述了万物智联时代操作系统的发展趋势、挑战与机遇,介绍了OpenHarmony的架构设计与关键特性,并分享了OpenHarmony开源项目的最新技术、生态、人才进展与行业实践。


陈海波作主题报告

Eclipse基金会Oniro项目经理Juan Rico出席活动并作《Paving the Way for a Global Open Source Ecosystem of Smart Devices: Oniro and OpenHarmony cooperation》技术报告。在报告中,Juan Rico表示:十余年来,由于技术栈的复杂性、制造商的区域分散性以及不断演变的法规要求,实现全球互联互通一直是智能设备面临的最大挑战之一。尽管解决方案层出不穷,但始终未能建立起真正的参考框架或获得全球范围的认可。在中国,开源操作系统OpenHarmony已展现出实现无缝互联的潜力。通过Eclipse 基金会和开放原子开源基金会的合作,基于OpenHarmony所构建的Oniro开源操作系统正逐步在欧洲展现出蓬勃的发展潜力。


Juan Rico作技术报告

Oniro指导和营销委员会主席Jaroslaw Marek出席活动并作《Jumpstart your Eclipse Oniro journey: a practical guide for developers and device makers》技术报告。在报告中,Jaroslaw Marek介绍了Eclipse基金会Oniro开源项目的历史和发展,强调了开发Oniro并发展其开源生态的原因(包括技术孤岛、开发成本高、缺乏物联网和设备连续性等)。此外,他还分享了Oniro项目相关的开发工具和资源,如Eclipse Theia IDE、OpenHarmony代码库的GitHub镜像、QEMU虚拟化开发和测试环境、Servo Web引擎以及React Native跨平台开发支持等。最后,他表示:通过Eclipse 基金会和开放原子开源基金会的持续深度合作,Oniro开源项目将继续攻克移动终端操作系统最紧迫的行业难题,并致力于弥合物联网与智能设备生态系统的断层。


Jaroslaw Marek作技术报告

上海交通大学软件学院教授、OpenHarmony技术俱乐部主任夏虞斌出席活动并作《Securing Data for Mobile Devices》技术报告。在报告中,夏虞斌表示:移动设备存储了海量个人数据,这些数据能反映用户全面的行为习惯,具有重要价值。例如,用户个人数据是个性化服务和本地大语言模型(LLM)训练的重要数据来源。同时,这也导致了数据安全和隐私保护的威胁和挑战日益突出。基于此,他介绍了TEE技术在保护移动数据安全方面的应用场景,并介绍了OpenTrustee(一个开源的TEE操作系统)和Penglai(一个基于RISC-V的开源TEE系统)等项目的当前进展。此外,他还介绍了基于OpenHarmony的机密计算环境,并强调了在设备端实现高效LLM推理和安全执行环境的重要性。


夏虞斌作技术报告

Futurewei首席软件架构师Kevin Boos出席活动并作《Bringing pure-Rust App Development to OpenHarmony with Makepad + Robius》技术报告。在报告中,Kevin Boos介绍了如何结合Makepad UI工具包和Robius平台抽象层组件持续优化Rust应用开发体验,并推动其普及;并演示了基于该框架开发的实际复杂应用(如Robrix——Matrix聊天客户端,以及Moly——面向桌面与云端AI大语言模型的GUI客户端),这些应用完全无需编写平台特定代码。最后,他还分享了当前在Robius & Makepad中支持OpenHarmony的进展,并展望未来路线图,探讨如何进一步优化纯Rust应用开发体验。


Kevin Boos作技术报告

华为爱丁堡研究所编程语言实验室的技术专家Magnus Morton出席活动并作报告《Cangjie for OpenHarmony Native Application Development》。在报告中,Magnus Morton介绍了仓颉语言及其核心特性,以及如何基于仓颉语言开发OpenHarmony原生应用。此外,Magnus还介绍了爱丁堡编程语言实验室在效应处理器(Effect Handlers)等前沿技术上的研究进展(该特性正在开发中,将在未来版本加入仓颉语言)。


Magnus Morton作技术报告

Qt公司软件开发工程师Hatem ElKharashy出席活动并作《Qt Quick 3D solution and advances in vector graphics rendering》技术报告。在报告中,Hatem ElKharashy介绍了Qt框架及其各模块中用于内容渲染的不同图形 API,并重点探讨了Qt Quick3D,该组件在Qt Quick基础上扩展,为Qt应用程序提供3D渲染支持。凭借丰富的功能集和卓越的性能表现,Qt Quick3D已达到专业3D引擎水准。最后,他解析了Qt中实现矢量图形渲染的多种方案,以及框架对各类矢量图形格式的支持能力,并分享了Qt Quick3D在OpenHarmony上适配的进展和未来规划。


Hatem ElKharashy作技术报告

Igalia公司软件开发工程师Rakhi Sharma出席活动并作《Building a Web Rendering Engine in Rust》技术报告。在报告中,Rakhi Sharma探讨了Servo渲染引擎的现状与未来愿景。她提到,基于Rust语言构建的Servo引擎具备内存安全、并发等特性,为构建分布式web生态提供了可能。她剖析了该引擎的技术优势,并分享了如何基于Servo在OpenHarmony平台上实现高性能轻应用的开发规划。


Rakhi Sharma作技术报告

润和软件副总裁、OpenHarmony工作委员会委员、DevBoard-SIG工作组组长刘洋出席活动并作《HopeRun: Building a New OpenHarmony Industrial Interconnection Ecosystem》主题报告。在报告中,刘洋分享了润和公司在OpenHarmony开源生态中的核心共建实践,以及如何从“技术引领”迈向“商业引领”的战略升级。作为OpenHarmony生态的重要推动者,润和软件基于“OpenHarmony+星闪”的领先技术,深入布局工业、教育、能源等多个行业,打造了面向多领域的商业发行版,不仅加速行业数字化转型进程,也为万物互联的智能化未来奠定了坚实基础。


刘洋作主题报告

在本次活动的Panel环节,华为德累斯顿研究所所长刘宇涛出席活动并担任主持人,与陈海波、Juan Rico、Jaroslaw Marek、夏虞斌、刘洋以及华为欧洲开源专家Adrian OSullivan等嘉宾围绕OpenHarmony在海外的核心价值主张、差异化竞争力、客户诉求和机遇以及未来趋势等方面,展开了深入讨论。这些嘉宾分别代表了OpenHarmony TSC、欧洲开源基金会、开发者、学术界、标准产业界、行业合作伙伴等领域,并一致认为:加强本地开发者社区建设、推动技术标准协同创新以及深化跨区域合作等对OpenHarmony在欧洲的发展至关重要。OpenHarmony的多设备高速互联、One OS Kit for All 、开源开放程度以及产学研协同发展等方面有一定的优势,符合欧洲本地市场的诉求和期待。同时,也需要重点关注OpenHarmony开源社区在开发者友好度、应用支持程度等方面的问题和挑战。


Panel环节

此外,华为阿姆斯特丹研究所所长周伦,华为爱尔兰研究所所长李俊,OpenHarmony TSC秘书处主任许家喆,天津大学OpenHarmony技术俱乐部主任赵来平等嘉宾也均出席本次活动。


活动现场

本次论坛的成功举办,标志着通过跨基金会的深入合作,为OpenHarmony智能终端操作系统的全球开源实践提供了创新范例,意味着OpenHarmony在构建海外开源技术生态上迈出了重要一步。未来,OpenHarmony将持续在海外开展开源技术生态活动,与全球开源社区开发者们共享技术资源,共同探讨技术进展,持续推动全球开源生态良好发展。

收起阅读 »

BOE(京东方)f-OLED柔性显示技术策源地论坛举办 携手中关村论坛共筑科技创新高地

3月27-31日,2025中关村论坛年会在北京隆重举办,本届论坛年会以“新质生产力与全球科技合作”为主题,与全球知名行业专家、领军企业共话全球科技创新发展路径。作为2025中关村论坛金牌战略合作伙伴,BOE(京东方)与中关村论坛强强携手,锚定全球科技创新高地,...
继续阅读 »

3月27-31日,2025中关村论坛年会在北京隆重举办,本届论坛年会以“新质生产力与全球科技合作”为主题,与全球知名行业专家、领军企业共话全球科技创新发展路径。作为2025中关村论坛金牌战略合作伙伴,BOE(京东方)与中关村论坛强强携手,锚定全球科技创新高地,不仅成功举办了“BOE技术策源地系列活动丨f-OLED专题”柔性显示专场活动,同时还带来了多款全球及行业首发的创新应用产品,彰显BOE(京东方)以“屏之物联”战略赋能万千细分应用场景的技术创新实力。

BOE(京东方)执行委员会委员、高级副总裁、首席产品官、首席技术官刘志强在技术策源地论坛致辞中表示,北京作为我国科技创新的战略高地,以雄厚的科技基础、丰富的创新资源和活跃的创新生态,为原始创新策源地建设提供了重要支撑。BOE(京东方)将携手中关村论坛,加速产业聚势跃迁。自2023年提出打造半导体显示、物联网创新、传感器件三大技术策源地以来,BOE(京东方)通过开放、协同、共赢的创新平台,携手各大高校、研究机构以及行业上下游伙伴,共同践行战略升维理论。面向未来,BOE(京东方)将始终秉承共创、共生、共赢的生态理念,持续深化策源地建设,携手谱写产业发展新篇章,持续推动产业升级和社会的发展。

BOE(京东方)执行委员会委员、高级副总裁、首席产品官、首席技术官刘志强

论坛期间,BOE(京东方)高级副总裁、OLED 技术与产品开发负责人邱海军及BOE(京东方)高级副总裁、小尺寸及移动业务分管负责人冒亮分别就BOE(京东方)OLED技术的发展前景与柔性显示应用趋势发表了演讲分享。与此同时,来自科研院所和企业的各领域专家分享了产学研热点技术进展及未来趋势。中国科学院刘云圻院士介绍了本征柔性材料与器件的研究现状,并表示随着有机电致发光材料在显示屏上的成功应用,柔性显示屏将成为未来的发展方向。3M 全球电子显示技术负责人李佑荣则对3M赋能BOE f-OLED技术的多项解决方案展开细致分享。北京中天创域市场咨询有限公司副总经理孙琦站在手机市场行业趋势角度,对手机终端市场的发展现状作深度分析。努比亚技术有限公司常务副总裁余航重点分享了柔性显示在电竞领域的应用,并展望了未来柔性显示技术在电竞领域更广泛的落地前景。除此之外,武汉海微科技股份有限公司总经理李林峰就OLED车载显示的应用趋势进行了深度分析,表示将与BOE(京东方)加强价值共创和OLED车载显示技术共研,让更出色的柔性显示产品植入车载显示更多应用场景,引领未来出行。

活动现场,BOE(京东方)还联合vivo、华硕、努比亚、红魔等全球一线品牌集中展示了基于f-OLED高端柔性显示技术品牌赋能的众多高端旗舰产品。f-OLED 代表了BOE(京东方)独有的、领先行业的高端柔性OLED显示技术解决方案,也是BOE(京东方)国内显示行业首个技术品牌子品牌之一,具有色彩绚丽、形态多变、功能集成度高、健康护眼、低碳环保等优势。目前,BOE(京东方)已在成都、重庆、绵阳投建了三条第6代柔性AMOLED生产线,以及国内首条第8.6代AMOLED生产线,实现包括曲面屏、全面屏、外折、内折、双向十字折叠、360°双向折叠、滑卷、卷轴等多形态的首发,为OLED技术发展的未来和场景化应用注入了无限可能。

自策源地计划发布以来,BOE(京东方)与产业链上下游、高校/研究机构、新型创新机构等伙伴保持紧密合作,聘请40多位优秀专家学者担任科技顾问,发布多批开放课题,并持续优化开放课题落地机制,开拓生态链新领域新平台合作,通过定向寻源、开放课题、联合研发、政府课题联合申报等累计落地合作项目百余项。同时,BOE(京东方)已成功举办氧化物、OLED、ADS Pro、健康显示等一系列专题论坛,与伙伴共同交流探讨产业发展机遇并深度合作,受到业内的广泛关注。

在中关村论坛年会期间,BOE (京东方)执行委员会委员、高级副总裁、联席首席技术官姜幸群发表《AI赋能制造·智能时代新趋势》演讲,介绍了BOE(京东方) AI 的战略布局,尤其在 AI 赋能显示制造方面的进展,未来 AI 将持续应用在更多领域。值得关注的是,BOE(京东方)还在中关村论坛年会上展出了多款行业领先或全球首发的创新应用产品。在前沿技术区,全球首款应用了BOE(京东方)先进钙钛矿光伏技术的传音Infinix光伏概念手机重磅亮相,其充分发挥钙钛矿电池低成本、高效率、轻薄化等优势,能够在室内外光照场景下实现无感补电,为消费电子续航增益提供革命性解决方案。同时,业内首款超大尺寸玻璃基封装载板、高端3D数字内容开发者工具成果展示等均吸引众多参会嘉宾驻足体验。在Powered by BOE专区,BOE(京东方)携手华硕推出的全球首款超大尺寸折叠OLED笔记本、联合红魔推出的全球首款1.5K真全面屏、联合腾讯打造的全球首款兼具裸眼3D和PC级性能的游戏掌机、携手iQOO带来的行业首发搭载全新发光器件产品、携手AGON打造的行业首款突破500Hz电竞显示器等多款创新展品惊艳亮相,为参观者带来一场科技盛宴。作为中关村论坛显示技术科技办会合作伙伴,BOE(京东方)赋能的多元化科技应用在会场内也无处不在,LED大屏、电子桌牌、移动智慧屏等创新产品为大会营造出浓浓的科技氛围,也展现出BOE(京东方)卓越的技术创新力与行业影响力。

科技创新是发展新质生产力的核心要素,技术创新力是实现企业高质量发展的源动力。面向未来,BOE(京东方)将持续以“屏之物联”战略为导向,积极扩展“第N曲线”发展路径,推动显示技术、物联网技术和数字技术深度融合,不断强化技术战略布局,构建技术创新生态,引领全球显示产业高质量发展。

关于BOE(京东方):

京东方科技集团股份有限公司(BOE)是全球领先的物联网创新企业,为信息交互和人类健康提供智慧端口产品和专业服务。作为全球半导体显示产业的龙头企业,BOE(京东方)带领中国显示产业破局“少屏”困境,实现了从0到1的跨越。如今,全球每四个智能终端就有一块显示屏来自BOE(京东方)。在“屏之物联”战略引领下,BOE(京东方)凭借“1+4+N+生态链”业务架构,将超高清液晶显示、柔性显示、MLED、微显示等领先技术广泛应用于交通、金融、艺术、零售、教育、办公、医疗等多元场景,赋能千行百业。目前,BOE(京东方)的子公司遍布全球近20个国家和地区,拥有超过5000家全球生态合作伙伴。更多详情可访问BOE(京东方)官网:https://www.boe.com.cn


收起阅读 »

websocket和socket有什么区别?

WebSocket 和 Socket 的区别 WebSocket 和 Socket 是两种不同的网络通信技术,它们在使用场景、协议、功能等方面有显著的差异。以下是它们之间的主要区别: 1. 定义 Socket:Socket 是一种网络通信的工具,可以实现不同...
继续阅读 »

WebSocket 和 Socket 的区别


WebSocket 和 Socket 是两种不同的网络通信技术,它们在使用场景、协议、功能等方面有显著的差异。以下是它们之间的主要区别:


1. 定义



  • Socket:Socket 是一种网络通信的工具,可以实现不同计算机之间的数据交换。它是操作系统提供的 API,广泛应用于 TCP/IP 网络编程中。Socket 可以是流式(TCP)或数据报(UDP)类型的,用于低层次的网络通信。

  • WebSocket:WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它允许服务器和客户端之间实时地交换数据。WebSocket 是建立在 HTTP 协议之上的,主要用于 Web 应用程序,以实现实时数据传输。


2. 协议层次



  • Socket:Socket 是一种底层通信机制,通常与 TCP/IP 协议一起使用。它允许开发者通过编程语言直接访问网络接口。

  • WebSocket:WebSocket 是一种应用层协议,建立在 HTTP 之上。在初始握手时,使用 HTTP 协议进行连接,之后切换到 WebSocket 协议进行数据传输。


3. 连接方式



  • Socket:Socket 通常需要手动管理连接的建立和关闭。通过调用相关的 API,开发者需要处理连接的状态,确保数据的可靠传输。

  • WebSocket:WebSocket 的连接管理相对简单。建立连接后,不需要频繁地进行握手,可以保持持久连接,随时进行数据交换。


4. 数据传输模式



  • Socket:Socket 可以实现单向或双向的数据传输,但通常需要在发送和接收之间进行明确的控制。

  • WebSocket:WebSocket 支持全双工通信,客户端和服务器之间可以随时互相发送数据,无需等待响应。这使得实时通信变得更加高效。


5. 适用场景



  • Socket:Socket 常用于需要高性能、低延迟的场景,如游戏开发、文件传输、P2P 网络等。由于其底层特性,Socket 适合对网络性能有严格要求的应用。

  • WebSocket:WebSocket 主要用于 Web 应用程序,如即时聊天、实时通知、在线游戏等。由于其易用性和高效性,WebSocket 特别适合需要实时更新和交互的前端应用。


6. 数据格式



  • Socket:Socket 发送的数据通常是二进制流或文本流,需要开发者自行定义数据格式和解析方式。

  • WebSocket:WebSocket 支持多种数据格式,包括文本(如 JSON)和二进制(如 Blob、ArrayBuffer)。WebSocket 的数据传输格式非常灵活,易于与 JavaScript 进行交互。


7. 性能



  • Socket:Socket 对于大量并发连接的处理性能较高,但需要开发者进行优化和管理。

  • WebSocket:WebSocket 在建立连接后可以保持长连接,减少了握手带来的延迟,适合高频率的数据交换场景。


8. 安全性



  • Socket:Socket 的安全性取决于使用的协议(如 TCP、UDP)和应用层的实现。开发者需要自行处理安全问题,如加密和身份验证。

  • WebSocket:WebSocket 支持通过 WSS(WebSocket Secure)进行加密,提供更高层次的安全保障。它可以很好地与 HTTPS 集成,确保数据在传输过程中的安全性。


9. 浏览器支持



  • Socket:Socket 是底层的网络通信技术,通常不直接在浏览器中使用。Web 开发者需要通过后端语言(如 Node.js、Java、Python)来实现 Socket 通信。

  • WebSocket:WebSocket 是专为 Web 应用设计的,所有现代浏览器均支持 WebSocket 协议,开发者可以直接在客户端使用 JavaScript API 进行通信。


10. 工具和库



  • Socket:使用 Socket 进行开发时,开发者通常需要使用底层网络编程库,如 BSD Sockets、Java Sockets、Python's socket 模块等。

  • WebSocket:WebSocket 提供了简单的 API,开发者可以使用原生 JavaScript 或第三方库(如 Socket.IO)轻松实现 WebSocket 通信。


结论


总结来说,WebSocket 是一种为现代 Web 应用量身定制的协议,具有实时、双向通信的优势,而 Socket 是一种底层的网络通信机制,提供更灵活的使用方式。选择使用哪种技术取决于具体的应用场景和需求。对于需要实时交互的 Web 应用,WebSocket 是更合适的选择;而对于底层或高性能要求的网络通信,Socket 提供了更多的控制和灵活性。


作者:Riesenzahn
来源:juejin.cn/post/7485631488114278454
收起阅读 »

当上小组长的第3天,我裁掉了2年老员工

前言 这周末和上上公司的小伙伴小酌一杯,获悉了两则消息,一则好消息,一则坏消息。 好消息是他晋升了,当了个小组长,管了4个人。 坏消息是他需要优化掉组内一个人。 征得本人同意,本文以他的视角来回顾这个魔幻的一周。 职业之殇 20年刚毕业那会,怀着满腔热情进入了...
继续阅读 »

前言


这周末和上上公司的小伙伴小酌一杯,获悉了两则消息,一则好消息,一则坏消息。

好消息是他晋升了,当了个小组长,管了4个人。

坏消息是他需要优化掉组内一个人。

征得本人同意,本文以他的视角来回顾这个魔幻的一周。


职业之殇


20年刚毕业那会,怀着满腔热情进入了某互联网公司,成为了人见人爱的前端CV仔,一心想要造出几个叫得上名字的轮子,也幻想着某天别人称呼我为轮子哥。



good.gif


人在怀揣梦想的时候感觉总有使不完的劲,工作中捣鼓了一些轮子,也在公司内部使用了,虽然有些Bug,但瑕不掩瑜,没有哪个轮子出来就是完美的,慢慢优化就是了。

然而天不遂人愿,公司因为一些不可说的原因,业务没法继续开展,它倒闭了,算上上一份实习的公司,这已经是我干垮的第二家公司了。

后来同事们戏称:"XX,你没当成轮子哥,却成了垮司哥"

我:"..."



sleep.gif


突然晋升


23年的行情你们是知道,还好前端的中级岗位还算比较多。

是的,我又入职了新公司。

前端有十个人,只有一个领导,姑且叫B吧。

与我同一年入职的还有2个小伙伴,其中一个是C。


平淡的日子没啥可留恋的,今天就是昨天复刻。

前几天下午,B突然钉钉发消息给我,还DING了一下,让我到会议室聊一下。

当时感觉挺诧异的,平时虽然也有消息往来,但从来没有单独约谈过,难道有什么坏消息?难道要干掉我了?思绪百转千回。


进了会议室就看到B正襟危坐面对着Mac笔记本,让我做到他对面,他的键盘声时不时想起。



B:是这样子的,你来公司快两年了,这段时间你的产出也是有目共睹的,技术也不错。现在团队内人也比较多,我一个人忙不过来,因此我向领导推荐你当前端的小组长,这事你觉得如何?

我:额,有点突然,可我之前没管过人呢?

B:凡事都有第一次,而且这次你管的人也不多,我们分成两个组,一个是我带,另一个是你带。你只需要管住你底下的4个人就好。

我:那我管哪几个人呢?

B:某某某...,这几个平时相处怎么样?

我:哦哦,这几位老哥我都是比较熟,平时也经常一起吃饭什么的,还好说话。

B:嗯,你回去先考虑一下,确定了明天就会发正式通知。

我:好的。



当B说出让我到小组长的瞬间,其实我已经接受了,没啥好考虑的,毕竟我之前没当过,也跃跃欲试。

接下来的几天,因为这事开心了不少,感觉每天都不一样...



image.png


裁员广进


过了三天,周五下午,又是B找我到会议室聊。



B:通知下来,组织架构变了,角色切换得咋样了?

我:还好啊,还是在做以前的事。

B:你现在是小组长了,算是管理了,管、理需要并进。

B:组内的同事工作了解的怎么样了?

我:还好吧,我们平时工作都有交集,大概知道他们在弄什么。

B:你觉得C怎么样,我需要真实的想法?

我:C和我同一年来的,工作年限比我长,技术也可以啊。

B:C技术比你如何?

我:额,各有千秋吧,侧重点不一样。

B:C招进来的时候工资比较高,但他的工资没有匹配他的产出,你看平时他也不怎么加班,很多时候到点就走,不像其他的同事有干劲,感觉他的积极性、主动性都不怎么强。

我:听说C的媳妇最近怀孕了,父亲也因病住院了,可能比较忙。

B:我们不说理由,只管结果,上头最近在盘点人力,前端需要走一个人,这个名额我倾向落到C,你找他谈谈。公司的底线是赔1个月,不能再多。

我:但他的绩效没问题啊,不是应该n+1吗?

B:他去年Q4得的是E,今年Q1再给一个E,两个E就可以因为绩效低而少给赔偿,之前其它部门也是有这种先例,Q1的E我倾向给他背,你来操作一下,理由要写的有理有据。

我:4月就准备发年终奖了(普遍1个月),是不是发了之后再让他走。

B:不可能,就是要在年终发之前给他赔偿,你只管通知他,具体会有人事去说。

我:...



走出会议室,我的心情是拔凉拔凉的,心想:资本果然是邪恶的,充满了算计。

整个下午我都在犹豫怎么和C开口,代码都没写几行。

犹豫着告诉了他这条消息,他会不会周末就过不好。

犹豫着告诉了他这条消息,他会不会记恨我,毕竟经常一起吃饭也互加了微信好友。

犹豫着告诉了他这条消息,组内的其他同事怎么看我...


最终我还是将B的想法告诉了C,全程我没怎么敢看C的眼睛,我怕看到他失落的眼神。

没想到的是C听到这消息还很平静,只说了一句:



公事公办,好聚好散



尘埃落定


我不知道C的周末怎么过的,我也不知道C最终和人事咋谈的,但结局已注定,C肯定要走。

期间我和B也争取了。



我:C还是比较有经验的,有些疑难问题还得靠他,而且平时对待工作也是蛮负责的。

B:我们不看态度,只看结果,他性价比不高。现在的疑难问题问AI就可以了,他走了我们再招一个新人,哪怕是实习生也可以在AI的帮助下胜任工作,还解决了一半多的成本。

我:C手里还有负责的比较重要的工作怕是不好分出来。

B:这会就要体现出你水平的时候了,该怎么平稳交接,我只要一个结果,记住必须是正向的结果。管理管理,就是管和理,既要管人(管人的行为),也要理人(修理人,让他走)。在公司工作就是要以公司的利益为准。我就是一个比较纯粹的人,只对事不对人,只做对团队对公司有益的事,其他的放第二位。

我:心想:泥马,价值观都出来了,我还能说啥...



经过这事,把刚当上小组长的喜悦心情基本冲没了,有时候我在想:



人和人都是有差距的,屁股决定脑袋,不要轻信别人,也不要总是试图说服别人。




image.png


你在工作过程中会遇到哪些冲击三观的事?说出来让大家涨涨姿势。


作者:小鱼人爱编程
来源:juejin.cn/post/7487210421209186355
收起阅读 »

AI对话的逐字输出:流式返回才是幕后黑手

web
AI应用已经渗透到我们生活的方方面面。其中,AI对话系统因其自然、流畅的交流方式而备受瞩目。前段时间有人在交流群中问到,如何实现AI对话中那种逐字输出的效果,这背后,流式返回技术发挥了关键作用。 欢迎加入前端筱园交流群:点击加入交流群 ​ 其实这背后并不是前...
继续阅读 »

AI应用已经渗透到我们生活的方方面面。其中,AI对话系统因其自然、流畅的交流方式而备受瞩目。前段时间有人在交流群中问到,如何实现AI对话中那种逐字输出的效果,这背后,流式返回技术发挥了关键作用。


image-20250305112300462

欢迎加入前端筱园交流群:点击加入交流群


​ 其实这背后并不是前端做了什么特效,而是采用的流式返回,而不是一次性返回完整的响应。流式返回允许服务器在一次连接中逐步发送数据,而不是一次性返回全部结果。这种方式使得前端可以在等待完整响应的过程中,逐步展示生成的内容,从而极大地提升了用户体验。


​ 那么,前端接收流式返回具体有哪些方式呢?接下来,本文将详细探讨几种常见的技术手段,帮助读者更好地理解并应用流式返回技术。


使用 Axios


大多数场景下,前端用的最多的就是axios来发送请求,但是axios 只有在在Node.js环境中支持设置 responseType: 'stream' 来接收流式响应。


const axios = require('axios');
const fs = require('fs');

axios.get('http://localhost:3000/stream', {
responseType: 'stream', // 设置响应类型为流
})
.then((response) => {
// 将响应流写入文件
response.data.pipe(fs.createWriteStream('output.txt'));
})
.catch((error) => {
console.error('Stream error:', error);
});

特点



  • 仅限 Node.js:浏览器中的 axios 不支持 responseType: 'stream'

  • 适合文件下载:适合处理大文件下载。


使用 WebSocket


WebSocket 是一种全双工通信协议,适合需要双向实时通信的场景。


前端代码:


const socket = new WebSocket('ws://localhost:3000');

socket.onopen = () => {
console.log('WebSocket connected');
};
socket.onmessage = (event) => {
console.log('Received data:', event.data);
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
socket.onclose = () => {
console.log('WebSocket closed');
};

服务器代码


const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 3000 });

wss.on('connection', (ws) => {
console.log('Client connected');

let counter = 0;
const intervalId = setInterval(() => {
counter++;
ws.send(JSON.stringify({ message: 'Hello', counter }));

if (counter >= 5) {
clearInterval(intervalId);
ws.close();
}
}, 1000);

ws.on('close', () => {
console.log('Client disconnected');
clearInterval(intervalId);
});
});

虽然WebSocket作为一种在单个TCP连接上进行全双工通信的协议,具有实时双向数据传输的能力,但AI对话情况下可能并不选择它进行通信。主要有以下几点原因:



  • 在AI对话场景中,通常是用户向AI模型发送消息,模型回复消息的单向通信模式,WebSocket的双向通信能力在此场景下并未被充分利用

  • 使用WebSocket可能会引入不必要的复杂性,如处理双向数据流、管理连接状态等,也会增加额外的部署与维护工作


image-20250304165718937

特点



  • 双向通信:适合实时双向数据传输

  • 低延迟:基于 TCP 协议,延迟低

  • 复杂场景:适合聊天、实时游戏等复杂场景


使用 XMLHttpRequest


虽然 XMLHttpRequest 不能直接支持流式返回,但可以通过监听 progress 事件模拟逐块接收数据


const xhr = new XMLHttpRequest();
xhr.open('GET', '/stream', true);

xhr.onprogress = (event) => {
const chunk = xhr.responseText; // 获取当前接收到的数据
console.log(chunk);
};

xhr.onload = () => {
console.log('Request complete');
};

xhr.send();

服务器代码(Koa 示例):


router.get("/XMLHttpRequest", async (ctx, next) => {
ctx.set({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
// 创建一个 PassThrough 流
const stream = new PassThrough();
ctx.body = stream;
let counter = 0;
const intervalId = setInterval(() => {
counter++;
ctx.res.write(
JSON.stringify({ message: "Hello", counter })
);

if (counter >= 5) {
clearInterval(intervalId);
ctx.res.end();
}
}, 1000);

ctx.req.on("close", () => {
clearInterval(intervalId);
ctx.res.end();
});
});

可以看到以下的输出结果,在onprogress中每次可以拿到当前已经接收到的数据。它并不支持真正的流式响应,用于AI对话场景中,每次都需要将以显示的内容全部替换,或者需要做一些额外的处理。


image-20250305093103230


如果想提前终止请求,可以使用 xhr.abort() 方法;


setTimeout(() => {
xhr.abort();
}, 3000);

image-20250305093253611


特点



  • 兼容性好:支持所有浏览器。

  • 非真正流式XMLHttpRequest 仍然需要等待整个响应完成,progress 事件只是提供了部分数据的访问能力。

  • 内存占用高:不适合处理大文件。


使用 Server-Sent Events (SSE)


SSE 是一种服务器向客户端推送事件的协议,基于 HTTP 长连接。它适合服务器向客户端单向推送实时数据


前端代码:


const eventSource = new EventSource('/sse');

eventSource.onmessage = (event) => {
console.log('Received data:', event.data);
};

eventSource.onerror = (event) => {
console.error('EventSource failed:', event);
};

服务器代码(Koa 示例):


router.get('/sse', (ctx) => {
ctx.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});

let counter = 0;
const intervalId = setInterval(() => {
counter++;
ctx.res.write(`data: ${JSON.stringify({ message: 'Hello', counter })}\n\n`);

if (counter >= 5) {
clearInterval(intervalId);
ctx.res.end();
}
}, 1000);

ctx.req.on('close', () => {
clearInterval(intervalId);
ctx.res.end();
});
});


image-20250304172215134


EventSource 也具有主动关闭请求的能力,在结果没有完全返回前,用户可以提前终止内容的返回。


// 在需要时中止请求
setTimeout(() => {
eventSource.close(); // 主动关闭请求
}, 3000); // 3 秒后中止请求

image-20250304202110200


虽然EventSource支持流式请求,但AI对话场景不使用它有以下几点原因:



  • 单向通信

  • 仅支持 get 请求:在 AI 对话场景中,通常需要发送用户输入(如文本、文件等),这需要使用 POST 请求

  • 无法自定义请求头:EventSource 不支持自定义请求头(如 AuthorizationContent-Type 等),在 AI 对话场景中,通常需要设置认证信息(如 API 密钥)或指定请求内容类型


注意点


返回给 EventSource 的值必须遵循 data: 开头并以 \n\n 结尾的格式,这是因为 Server-Sent Events (SSE) 协议规定了这种格式。SSE 是一种基于 HTTP 的轻量级协议,用于服务器向客户端推送事件。为了确保客户端能够正确解析服务器发送的数据,SSE 协议定义了一套严格的格式规范。SSE 协议规定,服务器发送的每条消息必须遵循以下格式:


field: value\n

其中 field 是字段名,value 是对应的值。常见的字段包括:



  • data::消息的内容(必须)。

  • event::事件类型(可选)。

  • id::消息的唯一标识符(可选)。

  • retry::客户端重连的时间间隔(可选)。


每条消息必须以 两个换行符 (\n\n) 结尾,表示消息结束


以下是一个完整的 SSE 消息示例:


id: 1\n
event: update\n
data: {"message": "Hello", "counter": 1}\n\n

特点



  • 单向通信:适合服务器向客户端推送数据。

  • 简单易用:基于 HTTP 协议,无需额外协议支持。

  • 自动重连EventSource 会自动处理连接断开和重连


使用 fetch API


fetch API 是现代浏览器提供的原生方法,支持流式响应。通过 response.body,可以获取一个 ReadableStream,然后逐块读取数据。


前端代码:


// 发送流式请求
fetch("http://localhost:3000/stream/fetch", {
method: "POST",
signal,
})
.then(async (response: any) => {
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(new TextDecoder().decode(value));
}
})
.catch((error) => {
console.error("Fetch error:", error);
});

服务器代码(Koa 示例):


router.post("/fetch", async (ctx) => {
ctx.set({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
// 创建一个 PassThrough 流
const stream = new PassThrough();
ctx.body = stream;
let counter = 0;
const intervalId = setInterval(() => {
counter++;
ctx.res.write(JSON.stringify({ message: "Hello", counter }));

if (counter >= 5) {
clearInterval(intervalId);
ctx.res.end();
}
}, 1000);

ctx.req.on("close", () => {
clearInterval(intervalId);
ctx.res.end();
});
});

image-20250305095034960


fetch也同样可以在客户端主动关闭请求。


// 创建一个 AbortController 实例
const controller = new AbortController();
const { signal } = controller;
// 发送流式请求
fetch("http://localhost:3000/stream/fetch", {
method: "POST",
signal,
})
.then(async (response: any) => {
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(new TextDecoder().decode(value));
}
})
.catch((error) => {
console.error("Fetch error:", error);
});

// 在需要时中止请求
setTimeout(() => {
controller.abort(); // 主动关闭请求
}, 3000); // 3 秒后中止请求

打开控制台,可以看到在Response中可以看到返回的全部数据,在EventStream中没有任何内容。


image-20250305095131519


image-20250305095143656


这是由于返回的信息SSE协议规范,具体规范见上文的 Server-Sent Events 模块中有介绍到


ctx.res.write(
`data: ${JSON.stringify({ message: "Hello", counter })}\n\n`
);

image-20250305100732796


但是客户端fetch请求中接收到的数据也包含了规范中的内容,需要前端对数据进一步的处理一下


image-20250305100744376


特点



  • 原生支持:现代浏览器均支持 fetchReadableStream

  • 逐块处理:可以实时处理每个数据块,而不需要等待整个响应完成。

  • 内存效率高:适合处理大文件或实时数据。


总结


综上所述,在 AI 对话场景中,fetch 请求 是主流的技术选择,而不是 XMLHttpRequestEventSource。以下是原因和详细分析:



  • fetch 是现代浏览器提供的原生 API,基于 Promise,代码更简洁、易读

  • fetch 支持 ReadableStream,可以实现流式请求和响应

  • fetch 支持自定义请求头、请求方法(GET、POST 等)和请求体

  • fetch 结合 AbortController 可以方便地中止请求

  • fetch 的响应对象提供了 response.okresponse.status,可以更方便地处理错误


方式特点适用场景
fetch原生支持,逐块处理,内存效率高大文件下载、实时数据推送
XMLHttpRequest兼容性好,非真正流式,内存占用高旧版浏览器兼容
Server-Sent Events (SSE)单向通信,简单易用,自动重连服务器向客户端推送实时数据
WebSocket双向通信,低延迟,适合复杂场景聊天、实时游戏
axios(Node.js)仅限 Node.js,适合文件下载Node.js 环境中的大文件下载

最后来看一个接入deekseek的完整例子:


resource.dengzhanyong.com/mp4/7823928…


服务器代码(Koa 示例):


const openai = new OpenAI({
baseURL: "https://api.deepseek.com",
apiKey: "这里是你申请的deepseek的apiKey",
});


// 流式请求 DeepSeek 接口并流式返回
router.post("/fetchStream", async (ctx) => {
// 设置响应头
ctx.set({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});

try {
// 创建一个 PassThrough 流
const stream = new PassThrough();
ctx.body = stream;

// 调用 OpenAI API,启用流式输出
const completion = await openai.chat.completions.create({
model: "deepseek-chat", // 或 'gpt-3.5-turbo'
messages: [{ role: "user", content: "请用 100 字介绍 OpenAI" }],
stream: true, // 启用流式输出
});
// 逐块处理流式数据
for await (const chunk of completion) {
const content = chunk.choices[0]?.delta?.content || ""; // 获取当前块的内容
ctx.res.write(content);
process.stdout.write(content); // 将内容输出到控制台
}
ctx.res.end();
} catch (err) {
console.error("Request failed:", err);
ctx.status = 500;
ctx.res.write({ error: "Failed to stream data" });
}
});

前端代码:


const controller = new AbortController();
const { signal } = controller;
const Chat = () => {
const [text, setText] = useState<string>("");
const [message, setMessage] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);

function send() {
if (!message) return;
setText(""); // 创建一个 AbortController 实例
setLoading(true);

// 发送流式请求
fetch("http://localhost:3000/deepseek/fetchStream", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message,
}),
signal,
})
.then(async (response: any) => {
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const data = new TextDecoder().decode(value);
console.log(data);
setText((t) => t + data);
}
})
.catch((error) => {
console.error("Fetch error:", error);
})
.finally(() => {
setLoading(false);
});
}

function stop() {
controller.abort();
setLoading(false);
}

return (
<div>
<Input
value={message}
onChange={(e) =>
setMessage(e.target.value)}
/>
<Button
onClick={send}
type="primary"
loading={loading}
disabled={loading}
>

发送
</Button>
<Button onClick={stop} danger>
停止回答
</Button>
<div>{text}</div>
</div>

);
};

写在最后


欢迎加入前端筱园交流群:点击加入交流群


关注我的公众号【前端筱园】,不错过每一篇推送


作者:前端筱园
来源:juejin.cn/post/7478109057044299810
收起阅读 »

electron+node-serialport串口通信

web
electron+node-serialport串口通信 公司项目需求给客户开发一个能与电子秤硬件通信的Win7客户端,提供数据读取、处理和显示功能。项目为了兼容win7使用了electron 22.0.0版本,串口通信使用了serialport 12.0....
继续阅读 »

electron+node-serialport串口通信



公司项目需求给客户开发一个能与电子秤硬件通信的Win7客户端,提供数据读取、处理和显示功能。项目为了兼容win7使用了electron 22.0.0版本,串口通信使用了serialport 12.0.0版本



serialport文档 electron文档


 //serialport的基本使用方法
 //安装 npm i serialport
 import { SerialPort } from 'serialport'
 SerialPort.list()//获取串口列表
 /** 创建一个串口连接
path(必需):串口设备的路径。例如,'/dev/robot' 或 'COM1'。
baudRate(必需):波特率,即每秒传输的比特数。常见值有 9600、19200、38400、57600、115200 等。
autoOpen(可选):是否在创建对象时自动打开串口。默认为 true。如果设置为 false,你需要手动调用 port.open() 来打开串口。
dataBits(可选):每字节的数据位数。可以是 5、6、7、8。默认值是 8。
stopBits(可选):停止位的位数。可以是 1 或 2。默认值是 1。
parity(可选):校验位类型。可以是 'none'、'even'、'odd'、'mark'、'space'。默认值是 'none'。
rtscts(可选):是否启用硬件流控制(RTS/CTS)。布尔值,默认值是 false。
xon(可选):是否启用软件流控制(XON)。布尔值,默认值是 false。
xoff(可选):是否启用软件流控制(XOFF)。布尔值,默认值是 false。
xany(可选):是否启用软件流控制(XANY)。布尔值,默认值是 false。
highWaterMark(可选):用于流控制的高水位标记。默认值是 16384(16KB)。
lock(可选):是否锁定设备文件,防止其他进程访问。布尔值,默认值是 true。
 **/

 const serialport = new SerialPort({ path: '/dev/example', baudRate: 9600 })

 serialport.open()//打开串口
 serialport.write('ROBOT POWER ON')//向串口发送数据
 serialport.on('data', (data) => {//接收数据
    //data为串口接收到的数据
 })

获取串口列表


 //主进程main.ts
 import { SerialPort, SerialPortOpenOptions } from 'serialport'
 //初始化先获取串口列表,提供给页面选择
 ipcMain.on('initData', async () => {
   const portList = await SerialPort.list()
   mainWindow.webContents.send('initData', {portList})
 })

 //渲染进程
 window.electron.ipcRenderer.once('initData', (_,{portList}) => {
   //获取串口列表后存入本地,登录页直接做弹窗给客户选择串口,配置波特率
   window.localStorage.setItem('portList', JSON.stringify(portList))
 })

串口选择


1720148892968.jpg


波特率配置


1720148971517.jpg


读取数据



公司秤和客户的秤串口配置不一样,所以做了model1和model2区分



 //主进程main.ts
 let P: SerialPort | undefined
 ipcMain.on('beginSerialPort', (_, { path, baudRate }) => {
   //区分配置
   const portConfig: SerialPortOpenOptions<AutoDetectTypes> =
     import.meta.env.VITE_MODE == 'model1'
       ? {
           path: path || 'COM1',
           baudRate: +baudRate || 9600, //波特率
           autoOpen: true,
           dataBits: 8
        }
      : {
           path: path || 'COM1',
           baudRate: +baudRate || 115200, //波特率
           autoOpen: true,
           dataBits: 8,
           stopBits: 1,
           parity: undefined
        }
   if (P) {
     P.close((error) => {
       if (error) {
         console.log('关闭失败:', error)
      } else {
         P = new SerialPort(portConfig)
         P?.write('SIR\r\n', 'ascii')//告诉秤端开始发送信息,具体看每个秤的配置,有的不需要
         P.on('data', (data) => {
           //接收到的data为Buffer类型,直接转为字符串就可以使用了
           mainWindow.webContents.send('readingData', data.toString())
        })
      }
    })
  } else {
     P = new SerialPort(portConfig)
     P?.write('SIR\r\n', 'ascii')
     P.on('data', (data) => {
       mainWindow.webContents.send('readingData', data.toString())
    })
  }
 })

解析数据


 <!--渲染进程解析数据-->
 <template>
   <div class="weight-con">
     <div class="weight-con-main">
       <div>
         <el-text class="wei-title" type="primary"><br /></el-text>
       </div>
       <div class="weight-panel">
         <el-text id="wei-num">{{ weightNum!.toFixed(2) }}</el-text>
         <div class="weight-con-footer">当前最大称重:600公斤</div>
       </div>
       <div>
         <el-text class="wei-title" type="primary"><br /></el-text>
       </div>
     </div>
   </div>
 </template>
 ​
 <script setup lang="ts">
 import { weightModel } from '@/utils/WeightReader'
 const emits = defineEmits(['zeroChange'])
 const weightNum = defineModel<number>()
 window.electron.ipcRenderer.on('readingData', (_, data: string) => {
   //渲染进程接收到主进程数据,根据环境变量解析数据
   weightNum.value = weightModel[import.meta.env.VITE_MODE](data)
   if (weightNum.value == 0) {
     emits('zeroChange')
  }
 })
 
</script>

 //weightReader.ts 解析配置
 export type Mode = 'model1' | 'model2'
 let str = ''
 export const weightModel = {
   model1: (weightStr: string) => {
     const rev = weightStr.split('').reverse().join('')
     return +rev.replace('=', '')
  },
   module2: (weightStr: string) => {
     str += weightStr
     if (str.match(/S\s+[A-Za-z]\s*(-?\d*.?\d+)\s*kg/g)) {
       const num = str.match(/S\s+[A-Za-z]\s*(-?\d*.?\d+)\s*kg/m)![1]
       str = ''
       return Number(num)
    } else {
       return 0
    }
  }
 }

67cd365b0ec45d2ca47e4eb0b597c33f.gif



完活~下班!



作者:彷徨的耗子
来源:juejin.cn/post/7387701265796562980
收起阅读 »

完蛋,被扣工资了,都是JSON惹的祸

JSON是一种轻量级的数据交换格式,基于ECMAScript的一个子集设计,采用完全独立于编程语言的文本格式来表示数据。它易于人类阅读和编写,同时也便于机器解析和生成,这使得JSON在数据交换中具有高效性。‌ JSON也就成了每一个程序员每天都要使用一个小类库...
继续阅读 »

JSON是一种轻量级的数据交换格式,基于ECMAScript的一个子集设计,采用完全独立于编程语言的文本格式来表示数据。它易于人类阅读和编写,同时也便于机器解析和生成,这使得JSON在数据交换中具有高效性。‌


JSON也就成了每一个程序员每天都要使用一个小类库。无论你使用的谷歌的gson,阿里巴巴的fastjson,框架自带的jackjson,还是第三方的hutool的json等。总之,每天都要和他打交道。


但是,却在阴沟里翻了船。


1、平平无奇的接口


 /**
* 获取vehicleinfo 信息
*
* @RequestParam vehicleId
* @return Vehicle的json字符串
*/

String loadVehicleInfo(Integer vehicleId);

该接口就是通过一个vehicleId参数获取Vehicle对象,返回的数据是Vehicle的JSON字符串,也就是将获取的对象信息序列化成JSON字符串了。


2、无懈可击的引用


String jsonStr = auctVehicleService.loadVehicleInfo(freezeDetail.getVehicle().getId());
if (StringUtils.isNotBlank(jsonStr)) {
Vehicle vehicle = JSON.parseObject(jsonStr, Vehicle.class);
if (vehicle != null) {
// 后续省略 ...
}
}

看似无懈可击的引用,隐藏着魔鬼。为什么无懈可击,因为做了健壮性的判断,非空字符串、非空对象等的判断,根除了空指针异常。


但是,魔鬼隐藏在哪里呢?


3、故障引发



线上直接出现类似的故障(此报错信息为线下模拟)。


现在测试为什么没有问题:主要的测试了基础数据,测试的数据中恰好没有Date 类型的数据,所以线下没有测出来。


4、故障原因分析


从报错日志可以看出,是因为日期类型的参数导致的。Mar 24, 2025 1:23:10 PM 这样的日期格式无法使用Fastjson解析。


深入代码查看:


@Override
public String loadVehicleInfo(Integer vehicleId) {
String key = VEHICLE_KEY + vehicleId;
Object obj = cacheService.get(key);

if (null != obj && StringUtils.isNotEmpty(obj.toString())
&& !"null".equals(obj.toString())) {
String result = (String)obj;
return result;
}

String json = null;
try {
Vehicle vInfo = overrideVehicleAttributes(vehicleId);
// 使用了Gson序列化对象
json = gson.toJson(vInfo);
cacheService.setExpireSec(key, gson.toJson(vInfo), 5 * 60);
} catch (Exception e) {
cacheService.setExpireSec(key, "", 1 * 60);
} finally {
}

return json;
}

原来接口的实现里面采用了谷歌的Gson对返回的对象做了序列化。调用的地方又使用了阿里巴巴的Fastjson发序列化,导致参数解析异常。



完蛋,上榜是要被扣工资的!!!


5、小结


问题虽小,但是影响却很大。坊间一直讨论着,程序员为什么不能写出没有bug的程序。这也许是其中的一种答案吧。


肉疼,被扣钱了!!!


--END--




喜欢就点赞收藏,也可以关注我的微信公众号:编程朝花夕拾


作者:SimonKing
来源:juejin.cn/post/7485560281955958794
收起阅读 »

IT外传:老技术部的困境

它像一个锈迹斑斑的铁柱,挪了它,会立马塌一片房。不挪它,又不敢在上面推陈出新。如今是一边定钉子加固,一边试探着放一把椅子,太难了…… 年会如期进行,在今年严峻的经济形势下,公司今年的营收和利润,相比去年都有大于30%的增长。这主要得益于老板有个原则:员工要比...
继续阅读 »

它像一个锈迹斑斑的铁柱,挪了它,会立马塌一片房。不挪它,又不敢在上面推陈出新。如今是一边定钉子加固,一边试探着放一把椅子,太难了……



年会如期进行,在今年严峻的经济形势下,公司今年的营收和利润,相比去年都有大于30%的增长。这主要得益于老板有个原则:员工要比公司挣得多。也就是今年公司增幅10%,员工收入也要比去年增10%。但是,这个增长不是针对所有人,而是那些挣钱的部门


老板拉来一堆现金,在年会现场分发。张三,3万;李四,13万;王五25万。销售1部,10万;客服2部,5万……很遗憾,IT研发部,不管是团队还是个人,都榜上无名。


不得不说,确实存在这么一个现象,销售类的岗位是盈利部门,像行政、人力、财务这类属于成本部门。而研发类的岗位,则视情况而定。可以是盈利部门,也可以是成本部门。


这个技术部几十个人,其中老员工很多。这里说的老员工,一方面是指在公司待得久,入职七、八年的大有人在。另一方面,年龄也都很大,40岁的也不在少数。公司很重视老员工,常将入职10多年的立为榜样,这让新员工入职后,感到不可思议以及安全感十足。


很多技术部老人看到发奖金,都会很失落。他们说,每年都这样,热闹是别人的,和自己没关系,连保洁、保安都有个“勤勤恳恳奖”,而程序员啥都没有。干技术没有前途。


作为刚入职的员工,我了解的不是很多,不过也稍微有点旁观者的视角。


公司对于技术部很有成见。各个部门也都有意见,尤其是老板。首先体现在系统的脆弱性上,基本上每年在最关键的营销活动时,服务都会宕机。每次宕机,老板都很着急,事后想让技术部避免此类情况再次发生。而技术部每次都有理由,也会提出新的解决方案。 老板配合技术部,从云服务器改为自建机房,从自建机房又迁移上云服务。


我来公司后,经历过两次宕机。第一次是一个大型活动,技术部说本来是没事的,结果因为运营人员在活动还没结束,就登上后台去查看汇总数据。这个汇总,会导致大量实时计算,结果数据库就顶不住了,停服务也停不掉,死机后数小时才重启成功。


领导说既然是数据库差,那就买数据库。技术部讨论后,感觉不能买。买多少?如果买了之后,还出现问题,那么就会处于舆论的被动面


第二次,不算是宕机,只是限流。就是很多人来访问,将一部分人挡在外面。体现在app上是一直弹出500错误、服务器内部错误的提示。弄得营销人员都不敢推广了。来了很多人,进不了门,错失很多客源,浪费了营销成本


后来,技术部又开始总结。原因是APP在一个页面调用了12个不同的接口,而且还有一个接口被连续调用了35次。这导致数据库压力加大,直接100%。幸好禁止用户访问,才没有崩溃。


技术部总监很着急。但是这个总监是App开发出身,不了解服务端。服务出问题了,他就去找后端开发。后端开发者感觉,架构设计是你总监的事情。我就算干好了,那也是你的功劳。因此不优化是本分,优化是情分,有些消极和抵触。


让各个部门吐槽的,还有跟技术部提需求。技术部一直说活多,排不开,响应不了需求。但是,很明显多数人,看起来并不忙,也没有人加班。于是,这几个业务部门领导一碰头,发现都没有开发他们的需求。那他们忙什呢?其实需求提到总监那里,当总监去安排任务时,结果安排不下去,各个组都说自己很忙。最后,总监就向上反馈说自己部门的人都很忙。实际上,可能是几个组长很忙,忙的很烦,不愿接需求,而组员并没有事情做。


整体情况就是这样,老技术们,感觉自己很辛苦,老板也不加钱。不加钱,我没有优化系统的动力,而且你也没有具体的详细策略,出架构那不是我的职责。老板感觉技术部问题频出,系统不稳定,不愿加钱。你干出成绩我才加钱,比如做到今年不宕机,原来需要100万的成本,通过你们技术研发,成本降低到50万。这才叫成绩。


技术:不加钱,我没法努力。老板:干不好,怎么加钱? 两者陷入如此的循环。


老板为了解决技术部的问题,就经常给技术部换领导。因为你告诉老板,宕机的原因是一个接口被调用30多次,他听不懂,也没有解决方案。要钱、要人都好办,你说接口调用太多,他懵了。他只能找一个能对得上话的人去解决。技术部内部是找不出来的,他认为如果存在这样的人,问题就不会发生了。实际上,技术部里的人,都觉着自己能解决,但是前提得加钱。多少钱办多少事,否则我就静止不动,装傻充愣。


结果,就空降了很多领导,换一个不出成果,换一个还是没有起色。但是,每一任领导都会推翻上一位的设计。比如云服务有问题,那我们就自己建机房。下一任领导来了,听说自己的机房不行?我们上云服务不就解决了!这就造成技术部架构经常变,也没有什么积累。更严重的是,员工也倦了,变来变去,反反复复,再有新的改革措施,大家也不愿认真执行了


并且,在这个过程中,业务还是不断积累和发展的。这也堆积了形形色色的病态业务系统。而这些系统,只有老员工能掌握,里面的机关埋伏,根本没有文档,全在他们的脑子里。想改什么东西,复杂不复杂,里面到底怎么个逻辑,老技术说啥就是啥。你想维持生命,又不能让老员工过于动荡,否则会导致业务断层。


倒是也会新来一些空降的技术领导。所有的空降领导,都能发现问题所在。问题大家都知道,实习生都看得出来。 比如缺乏技术领导力,团队没有激励机制,缺乏考核流程,技术债积累严重,技术体系稳定性缺失,业务混乱,人员消极等等。


但新领导也只能做一些表面上的改善。比如,基层管理说员工都不听他的,多次强调要交周报,底下员工就是不交,导致自己不知道他们每周都在干什么。空降领导说,周报写不好,扣工资,看他们交不交。稳定性缺失?把稳定性纳入考核,谁写的代码不稳定,扣工资。说一直忙还不加班?压缩工期,原定10天的任务,让6天干完,这不就忙起来了。 对于系统BUG多,领导说好解决,发现bug,按照数量扣工资,肯定就没有bug了。


实际上这是一种从末端治理的方案,是对洪水的围堵而非疏通。软件系统的配合是复杂的,更需要从源头开始治理。 发现bug扣程序员的工资,属于问题倒推的行为。Bug需要界定是哪方产生的,是单纯代码逻辑问题,还是产品规则问题,还是用户操作方式问题,或者是偶然问题。领导说:那就扣所有参与成员的工资,这样大家都会紧绷一根弦,谁都会为了没有bug而努力。


另外,功能还有复杂和简单之分。一个简单的版本,比如修改个提示语,可能产生不了bug。但是,如果是一项复杂的业务,比如写一个机器人对战,可能会有很多bug。还有,考核是不是应该和职级和工资挂钩?月薪2万的人和月薪5000的,干同一项任务,是不是应该有不同的要求。领导说:有意见?有意见可以去没意见的地方工作。


这又是一个新的轮回,让过于散漫的老技术部,又开始变得剑拔弩张起来。他们将面临新的技术架构,考核体系,工作方式。至于这一轮能给公司带来什么,或许只有时间才能给出答案。


而这个老技术部门的困境,到底能不能走出来,又该如何走出来?



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



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

离职转AI独立开发半年,我感受到了真正的生活

离职转AI独立开发半年,我感受到了真正的生活 我的新产品:code.promptate.xyz/ 开场白:一个不被理解的决定 2022年12月的最后一天,我收拾了自己的小盒子,里面装着我在这家互联网公司工作的所有痕迹:一个定制水杯,几本技术书籍,和一摞写满代...
继续阅读 »

离职转AI独立开发半年,我感受到了真正的生活


我的新产品:code.promptate.xyz/


开场白:一个不被理解的决定


photo-1580927752452-89d86da3fa0a.jpeg


2022年12月的最后一天,我收拾了自己的小盒子,里面装着我在这家互联网公司工作的所有痕迹:一个定制水杯,几本技术书籍,和一摞写满代码思路的便利贴。HR部门的小姐姐看着我签完最后一份文件,表情有些复杂:"小张,你才来半年就走,真的想好了吗?这个时候辞职,外面行情不好..."


我点点头,没多解释。如何向别人解释我这个2000年出生的"孩子",毕业仅仅半年就对光鲜的互联网工作心生倦意?如何解释我不想再每天凌晨两点被产品经理的消息惊醒,然后爬起来改几行代码?如何解释我想追求的不只是一份体面的工资和一个看起来不错的头衔?


当我走出公司大楼,北京的冬风刮得我脸生疼。我的储蓄只够支撑我半年,而我计划做的事情——成为一名AI独立开发者——在大多数人眼中无异于天方夜谭。"你疯了吧?现在的独立开发者,有几个能养活自己的?"这是我最好朋友听到我计划时的反应。


事实证明,他错了。我也曾经错了。而现在,当我坐在自己选择的咖啡馆,以自己喜欢的节奏工作,看着用户数突破10,000的后台数据,我知道这半年的挣扎、焦虑和不安都是值得的。


职场困境:我在互联网大厂的日子


回想起入职的第一天,一切都充满希望。校招拿到知名互联网公司的offer,年薪30万,比许多同学高出不少。父母骄傲地向亲戚们宣布他们的儿子"找到了好工作"。


然而现实很快给了我当头一棒。


我被分到一个负责内部工具开发的小组。领导在入职第一天就明确告诉我:"小张,我们这个组不是核心业务,资源有限,但任务不少,你得做好加班的准备。"


第一个月,适应期,我每天工作10小时,感觉还能接受。到了第二个月,一个重要项目启动,我开始习惯每天凌晨回家,第二天早上9点又准时出现在公司。最夸张的一次,我连续工作了38个小时,只为了赶一个莫名其妙被提前的deadline。


# 当时的我就像这段无限循环的代码
while True:
wake_up()
go_to_work()
coding_till_midnight()
get_emergency_task()
sleep(2) # 只睡2小时

工作内容也让我倍感挫折。作为一名热爱技术的程序员,我希望能够参与有挑战性的项目,学习前沿技术。但现实是,我大部分时间都在做重复性的维护工作,修复一些简单但繁琐的bug,或者应对产品经理们不断变化的需求。


我感到自己正在成为一个"代码工具人",一个可以被随时替换的齿轮。我的创造力,我对技术的热情,我想为这个世界带来一些改变的梦想,都在日复一日的996中渐渐磨灭。


转折点:AI浪潮中看到的希望


2022年底,ChatGPT横空出世。作为一个技术爱好者,我第一时间注册了账号,体验了这个令人震惊的产品。我记得那天晚上,我熬夜到凌晨三点,不断地与ChatGPT对话,测试它的能力边界。


"这太不可思议了,"我对自己说,"这将改变一切。"


随后几周,我利用所有空闲时间(其实并不多)研究OpenAI的API文档,尝试构建一些简单的应用。我发现,大语言模型(LLM)并不像我想象的那样遥不可及,即使是一个普通开发者,只要理解其工作原理,也能基于它创造出有价值的产品。


同时,我开始关注独立开发者社区。我惊讶地发现,有不少人依靠自己开发的小产品,实现了不错的收入。虽然他们中的大多数人都经历了长期的积累,但AI技术的爆发似乎提供了一个弯道超车的机会。


这个想法越来越强烈,直到有一天晚上,当我又一次被加到一个紧急项目里,领导发来消息:"小张,这个需求很紧急,今晚能上线吗?"


我望着窗外的夜色,突然感到一阵前所未有的清晰。


我回复道:"可以,这是我在公司的最后一个项目了。"


第二天,我提交了辞职申请。


技术探索:从零开始的AI学习之路


辞职后的第一个月,我给自己制定了严格的学习计划。每天早上6点起床,先锻炼一小时,然后开始我的"AI课程"。


首先,我需要理解大语言模型的基本原理。虽然我有编程基础,但NLP和深度学习对我来说仍是比较陌生的领域。我从《Attention is All You Need》这篇奠定Transformer架构的论文开始,通过各种在线资源,逐步理解了当代大语言模型的工作机制。


# 简化的Transformer注意力机制示例
def scaled_dot_product_attention(query, key, value, mask=):
# 计算注意力权重
matmul_qk = tf.matmul(query, key, transpose_b=True)

# 缩放
depth = tf.cast(tf.shape(key)[-1], tf.float32)
logits = matmul_qk / tf.math.sqrt(depth)

# 添加掩码(可选)
if mask is not :
logits += (mask * -1e9)

# softmax归一化
attention_weights = tf.nn.softmax(logits, axis=-1)

# 应用注意力权重
output = tf.matmul(attention_weights, value)

return output, attention_weights

然后,我需要掌握如何有效地利用OpenAI、Anthropic等公司提供的API。这包括了解Prompt Engineering的技巧,学会如何构建有效的提示词,以及如何处理模型输出的后处理工作。


我还深入研究了向量数据库、检索增强生成(RAG)等技术,这些对于构建基于知识的AI应用至关重要。


Similarity(A,B)=ABA×B=cos(θ)Similarity(A, B) = \frac{A \cdot B}{|A| \times |B|} = \cos(\theta)


这个余弦相似度公式成为了我日常工作的一部分,用于计算文本嵌入向量之间的相似性。


同时,我不断实践、不断失败、不断调整。我记得有一周,我几乎每天睡眠不足5小时,只为解决一个模型幻觉问题。但与公司工作不同的是,这种忙碌源于我的热情和对问题的好奇,而非外部压力。


产品孵化:从创意到实现


学习的同时,我开始思考自己的产品定位。在观察市场和分析自身技能后,我决定开发一款面向内容创作者的AI助手,我将其命名为"创作魔法师"。


这个产品的核心功能是帮助博主、自媒体人和营销人员高效创作内容。与市面上的通用AI不同,它专注于内容创作流程:从选题分析、结构规划、初稿生成到细节优化和SEO改进,提供全流程支持。


产品开发过程中,我遇到了许多挑战:



  1. 技术架构选择:作为独立开发者,资金有限,我需要在功能与成本间找平衡。最终我选择了Next.js + TailwindCSS搭建前端,Node.js构建后端,MongoDB存储数据,Pinecone作为向量数据库存储文档嵌入向量。

  2. 模型优化:为了降低API调用成本,我设计了一套智能路由系统,根据任务复杂度自动选择不同的模型,简单任务用更经济的模型,复杂任务才调用高端模型。

  3. 用户体验设计:没有设计团队,我自学了基础UI/UX知识,参考优秀产品,反复调整界面直到满意。

  4. 运营与推广:这对我这个技术人来说是最大挑战。我学会了编写有吸引力的产品描述,设计落地页,甚至尝试了简单的SEO优化。


最艰难的时刻是产品上线后的第一个月。用户增长缓慢,每天只有个位数的新注册。我开始怀疑自己的决定,甚至一度考虑放弃,重新找工作。


转机:从10个用户到10,000用户


转机出现在上线后的第二个月。一位拥有20万粉丝的自媒体创作者使用了我的产品,对效果非常满意,在他的平台上分享了使用体验。这篇分享在创作者圈内引起了不小的反响。


24小时内,我的注册用户从原来的不到200人猛增至1500多人。服务器一度崩溃,我熬夜进行紧急扩容和优化。这次意外的曝光让我意识到,产品定位是正确的,市场需求确实存在。


接下来,我调整了运营策略:



  1. 主动联系内容创作者,提供免费试用,换取真实反馈和可能的推荐。

  2. 根据用户反馈快速迭代产品功能,每周至少发布一次更新。

  3. 建立用户社区,鼓励用户分享使用技巧,相互帮助。

  4. 编写详细的使用教程和最佳实践指南,降低用户上手难度。


// 用户增长追踪系统的一部分
function trackUserGrowth() {
const date = new Date().toISOString().split('T')[0];

db.collection('metrics').updateOne(
{ date: date },
{
$inc: {
newUsers: 1,
totalImpressions: userSource.impressions || 0
},
$set: {
lastUpdated: new Date()
}
},
{ upsert: true }
);
}

三个月后,用户数突破5,000;半年后,达到10,000。更令人欣慰的是,付费转化率远超我的预期,达到了8%左右,而行业平均水平通常在2-3%。


我分析了成功原因:



  1. 产品聚焦特定痛点:不追求通用性,而是深入解决内容创作者的具体问题。

  2. 及时响应用户需求:独立开发的优势是决策链短,能快速调整方向。

  3. 社区效应:用户之间的口碑传播形成了良性循环。

  4. 个性化服务:我经常亲自回复用户问题,提供定制化建议,这在大公司很难做到。


财务自由:从赤字到收支平衡


谈到收入模式,我采用了"免费+订阅"的策略:



  • 基础功能完全免费,足以满足普通用户的需求

  • 高级功能(如批量处理、高级模板、深度分析等)需要订阅

  • 提供月度计划(49元)和年度计划(398元,约33元/月)


最初几个月,收入微乎其微。我记得第一个月的收入仅有287元,而我在公司的月薪是25,000元。差距之大,让我一度怀疑自己的决定。


但随着用户增长,情况逐渐改善。第三个月收入突破5,000元,第四个月达到12,000元,第六个月——也就是我离职半年后,月收入达到了23,500元,基本与我原来的工资持平。


考虑到我现在的生活成本降低了(不需要租住在北京市中心的高价公寓,不需要每天通勤),实际上我的生活质量反而提高了。


更重要的是,这些收入是真正属于我的,不依赖于任何公司的评价和KPI。我建立了自己的"被动收入引擎",它可以在我睡觉时继续为我工作。


生活平衡:找回被工作吞噬的自我


收入只是故事的一部分。对我来说,最大的变化是生活方式的改变。


在互联网公司工作时,我的生活可以用一句话概括:工作即生活。我几乎没有个人时间,健康状况逐渐恶化,社交圈萎缩到只剩同事,爱好被束之高阁。


成为独立开发者后,我重新掌控了自己的时间:



  • 合理作息:我不再熬夜加班,保持每天7-8小时高质量睡眠。

  • 定期锻炼:每天至少运动一小时,半年下来体重减轻10kg,体脂率降低5%。

  • 地点自由:我可以在家工作,也可以去咖啡馆,甚至尝试了几次"工作旅行",边旅游边维护产品。

  • 深度学习:不再为了应付工作而学习,而是追随个人兴趣深入研究技术。

  • 重拾爱好:我重新开始弹吉他,参加了当地的音乐小组,结识了一群志同道合的朋友。


这种生活方式让我找回了工作的意义——工作是为了更好的生活,而不是生活为了工作。我的创造力和工作热情反而因此提升,产品迭代速度和质量都超出了预期。


技术反思:AI时代的个人定位


在这半年的独立开发经历中,我对AI技术和个人发展有了更深的思考。


首先,大模型时代确实改变了软件开发的范式。传统开发模式是"写代码解决问题",而现在更多的是"设计提示词引导AI解决问题"。这不意味着编程技能不重要,而是编程与AI引导能力的结合变得越来越重要。


# 传统开发方式
def analyze_sentiment(text):
# 复杂的NLP算法实现
words = tokenize(text)
scores = calculate_sentiment_scores(words)
return determine_overall_sentiment(scores)

# AI时代的开发方式
def analyze_sentiment_with_llm(text):
prompt = f"""
分析以下文本的情感倾向,返回'正面'、'负面'或'中性'。
只返回分类结果,不要解释。
文本: {text}
"""

result = llm_client.generate(prompt, max_tokens=10)
return result.strip()

其次,我认识到技术民主化的力量。曾经需要一个团队才能完成的项目,现在一个人借助AI工具也能完成。这为独立开发者创造了前所未有的机会,但也意味着差异化和创新变得更加重要。


最后,我发现真正的核心竞争力不在于熟悉某项技术,而在于解决问题的思维方式和对用户需求的理解。技术工具会不断更新迭代,但洞察问题和设计解决方案的能力将长期有效。


写给迷茫的年轻人


回顾这半年的经历,我想对那些和当初的我一样迷茫的年轻人说几句话:



  1. 公司经历有价值,但不是唯一路径:在大公司工作能积累经验和人脉,但不要把它视为唯一选择。如果环境压抑了你的创造力和热情,寻找改变是勇敢而非逃避。

  2. 技术浪潮创造机会窗口:AI等新技术正在重构行业,为个人提供了"弯道超车"的机会。保持开放心态,持续学习,你会发现比想象中更多的可能性。

  3. 找到可持续的节奏:成功不在于短期的爆发,而在于长期的坚持。设计一种既能推动目标实现又不会消耗自己的工作方式,才能走得更远。

  4. 用户价值胜过技术炫耀:最成功的产品往往不是技术最先进的,而是最能解决用户痛点的。专注于创造真正的价值,而不仅仅是展示技术能力。

  5. 享受过程,而非仅追求结果:如果你只关注最终目标而忽视日常体验,即使达到目标也可能感到空虚。真正的成功包含了对过程的享受和个人成长。


未来展望:持续进化的旅程


现在,我站在新的起点上。"创作魔法师"只是我旅程的第一步,我已经开始规划下一个产品,瞄准了另一个我认为有潜力的细分市场。


与此同时,我也在考虑如何扩大团队规模。虽然独立开发有其魅力,但有些想法需要更多元的技能组合才能实现。我计划在未来半年内招募1-2名志同道合的伙伴,组建一个小而精的团队。


技术上,我将继续深入研究大模型的微调和部署技术。随着开源模型的进步,在特定领域微调自己的模型变得越来越可行,这将是我产品的下一个竞争优势。


生活方面,我正计划一次为期两个月的"数字游牧"之旅,边旅行边工作,探索更多可能的生活方式。


路上会有挑战,也会有挫折,但我不再惧怕。因为我知道,真正的自由不在于没有困难,而在于面对困难时仍能按自己的意愿选择前进的方向。


当我在咖啡馆工作到黄昏,看着窗外的夕阳,我常常感到一种难以言喻的满足感。这种感觉告诉我,我正在正确的道路上——一条通往真正生活的道路。


如果你也在考虑类似的选择,希望我的故事能给你一些启发。记住,每个人的路都不同,重要的是找到属于自己的节奏和方向。


在这个AI加速发展的时代,机会前所未有,但终究,技术只是工具,生活才是目的。


作者:aircrushin
来源:juejin.cn/post/7486788421932400652
收起阅读 »

从0到1开发DeepSeek天气助手智能体——你以为大模型只会聊天?Function Calling让它“上天入地”

前言2025年伊始,科技界的风云人物们——从英伟达的黄仁勋到OpenAI的山姆·奥特曼,再到机器学习领域的泰斗吴恩达不约而同地将目光聚焦于一个关键词:AI Agent(即智能体,若想深入了解,可阅读我的文章《一文读懂2025核心概念 AI Agent:科技巨头...
继续阅读 »

前言

2025年伊始,科技界的风云人物们——从英伟达的黄仁勋到OpenAI的山姆·奥特曼,再到机器学习领域的泰斗吴恩达不约而同地将目光聚焦于一个关键词:AI Agent(即智能体,若想深入了解,可阅读我的文章《一文读懂2025核心概念 AI Agent:科技巨头都在布局的未来赛道》)。然而,对于AI Agent的前景,持怀疑态度的人可能会问:“大模型只是个能完成问答的概率模型,它哪来的行为能力?又怎能摇身一变成为AI Agent呢?” 这个问题的答案,正隐藏在我们今天要探讨的 Function Calling(函数调用) 技术之中!

一、 什么是大模型的 Function Calling 技术?

Function Calling 是一种让大语言模型能够调用外部函数或工具的技术。简单来说,就是让大模型不仅能理解和生成文本,还能根据用户的需求,调用特定的 API 或工具来完成更复杂的任务。

举个例子:

  • 用户:“帮我订一张明天从北京到上海的机票。”
  • 不具备Function Calling的大模型:回复“好的,我会帮您订票。”,但无法真正执行。
  • 具备 Function Calling 的大模型:可以调用机票预订 API,获取航班信息,并完成订票操作。

二、 Function Calling 和 AI Agent 的关系

AI Agent 是指能够自主感知环境、进行决策和执行动作的智能体。Function Calling 是构建强大 AI Agent 的关键技术之一,它为 AI Agent 提供了以下能力:

  • 连接现实世界:  通过调用外部 API,AI Agent 可以获取实时信息、操作外部系统,从而与现实世界进行交互。
  • 执行复杂任务:  通过组合调用不同的函数,AI Agent 可以完成更复杂、更个性化的任务,例如旅行规划、日程安排等。
  • 提升效率和准确性:  利用外部工具的强大功能,AI Agent 可以更高效、更准确地完成任务,例如数据分析、代码生成等。

从上述分析中可知要开发智能体,必须用到大模型的Function Calling技术。要让大模型调用Function Calling功能,必须提供大模型相应功能的函数。

为了更直观感受大模型Function Calling技术,我们将利用DeepSeek大模型从0到1开发天气助手智能体,可以实时查询天气状态并给我们提供穿衣建议等~

三、心知天气 + Python + DeepSeek开发天气预报智能体

3.1 心知天气注册及API key获取方法

为了能够使用Python代码获得实时的天气情况,我们这里需要用到心知天气的的API:

  1. 打开心知天气的官网,注册登录并点击控制台:

2.png

  1. 在控制台左侧产品管理栏中点击添加产品

3.png

  1. 申请免费版的API,点击左侧免费版,就可以看到API私钥了:

5.png

  1. 利用python requests库调用API获得天气情况(免费版的只能得到天气现象、天气现象代码和气温 3项数据)
请提前安装requests sdk: pip install requests
import requests

url = "https://api.seniverse.com/v3/weather/now.json"

params = {
"key": "", # 填写你的私钥
"location": "北京", # 你要查询的地区可以用代号,拼音或者汉字,文档在官方下载,这里举例北京
"language": "zh-Hans", # 中文简体
"unit": "c", # 获取气温
}

response = requests.get(url, params=params) # 发送get请求
temperature = response.json() # 接受消息中的json部分
print(temperature['results'][0]['now']) # 输出接收到的消息进行查看

6.png

  1. 将请求天气的代码封装成可以指定查询地点的函数:
import requests

def get_weather(loc):
url = "https://api.seniverse.com/v3/weather/now.json"
params = {
"key": "", #填写你的私钥
"location": loc,
"language": "zh-Hans",
"unit": "c",
}
response = requests.get(url, params=params)
temperature = response.json()
return temperature['results'][0]['now']

3.2 DeepSeek API Key注册方法

Function Calling 适用于模型规模大于30B的模型,本次分享我们使用DeepSeek-V3模型。按如下方法注册获得DeepSeek-V3 API Key(Deep-V3 API 访问教程请看文章DeepSeek大模型API实战指南):

  1. 进入DeepSeek官网,点击API 开放平台:

7.png

  1. 注册并充值tokens后(deepseek的tokens还是相当便宜的,10元可以用好久),点击左边栏API Keys生成API Key:

8.png

  1. 利用python openai库访问deepseek (这里openai库定义的是请求数据格式,并不是说deepseek是基于openai构造的`)
# 请提前安装openai sdk: pip install openai

from openai import OpenAI

client = OpenAI(api_key="你创建的api key", base_url="https://api.deepseek.com")

response = client.chat.completions.create(
model="deepseek-chat", # 指定deepseek-chat, deepseek-chat对应deepseek-v3, deepseek-reasoner对应deepseek-r1
messages=[
{"role": "system", "content": "You are a helpful assistant"}, #指定系统背景
{"role": "user", "content": "Hello"}, #指定用户提问
],
stream=False
)

print(response.choices[0].message.content)

9.png

3.3 Function Calling准备: 让大模型理解函数

准备好外部函数之后,非常重要的一步是将外部函数的信息以某种形式传输给大模型,让大模型理解函数的作用。大模型需要特定的字典格式对函数进行完整描述, 字典描述包括:

  • name:函数名称字符串
  • description: 描述函数功能的字符串,大模型选择函数的核心依据
  • parameters: 函数参数, 要求遵照JSON Schema格式输入,JSON Schema格式请参照JSON Schema格式详解

对于上面的get_weather函数, 我们创建如下字典对其完整描述:

get_weather_function = {
'name': 'get_weather',
'description': '查询即时天气函数,根据输入的城市名称,查询对应城市的实时天气',
'parameters': {
'type': 'object',
'properties': { #参数说明
'loc': {
'description': '城市名称',
'type': 'string'
}
},
'required': ['loc'] #必备参数
}
}

完成对get_weather函数描述后,还需要将其加入tools列表,用于告知大模型可以使用哪些函数以及这些函数对应的描述,并在可用函数对象中记录一下:

tools = [
{
"type": "function",
"function":get_weather_function
}
]

available_functions = {
"get_weather": get_weather,
}

3.4 Function calling 功能实现

完成一系列基础准备工作之后,接下来尝试与DeepSeek-V3大模型对话调用Function calling功能(分步教程代码在 codecopy.cn/post/ir801w ,完整优化代码在codecopy.cn/post/c80rrk ):

  1. 实例化客户端并创建如下messages
# 实例化客户端
client = OpenAI(api_key=你的api_key,
base_url="https://api.deepseek.com")

messages=[
{"role": "user", "content": "请帮我查询北京地区今日天气情况"}
]
  1. 测试一下如果只输入问题不输入外部函数,模型是不知道天气结果的,只会告诉我们如何获得实时天气
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages
)
print(response.choices[0].message.content)

10.png

  1. 接下来尝试将函数相关信息输入给Chat模型,需要额外设置两个参数,首先是tools参数, 用于申明外部函数库, 也就是我们上面定义的tools列表对象。其次是可选参数tool_choice参数,该参数用于控制模型对函数的选取,默认值为auto, 表示会根据用户提问自动选择要执行函数,若想让模型在本次执行特定函数不要自行挑选,需要给tool_choice参数赋予{"name":"functionname"}值,这时大模型就会从tools列表中选取函数名为functionname的函数执行。这里我们考验一下模型的智能性,让模型自动挑选函数来执行:
response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "user", "content": "请帮我查询北京地区今日天气情况"}
],
tools=tools,
)

print(response.choices[0].message)

观察现在response返回的结果, 我们发现message中的content变为空字符串, 增加了一个tool_calls的list, 如图红框所示,该list就包含了当前调用外部函数的全部信息:

11.png

我们输出一下toll_calls列表项中的function内容,可以看到大模型自动帮我们选择了要执行的函数get_weather,并告诉我们要传递的参数{loc:北京}。,

response_message = response.choices[0].message
print(response_message.tool_calls[0].function)

12.png

  1. 下一步将大模型生成的函数参数输入大模型选择的函数并执行(注意大模型不会帮我们自动调用函数,它只会帮我们选择要调用的函数以及生成函数参数),通过上面定义的available_functions对象找到具体的函数,并将大模型返回的参数传入(这里 ** 是一种便捷的参数传递方法,该方法会将字典中的每个key对应的value传输到同名参数位中),可以看到天气函数成功执行:
# 获取函数名称
function_name = response_message.tool_calls[0].function.name

# 获得对应函数对象
function_to_call = available_functions[function_name]

# 获得执行函数所需参数
function_args = json.loads(response_message.tool_calls[0].function.arguments)

# 执行函数
function_response = function_to_call(**function_args)

print(function_response)

13.png

  1. 在调用天气函数得到天气情况后,将天气结果传入mesages列表中并发送给大模型,让大模型理解上下文。函数执行结果的messagetool_message类型(这部分有点绕,可以看整体对于message类型有疑问的请看我的文章DeepSeek大模型API实战指南, 里面有详细的参数指南)。

首先将大模型关于选择函数的回复response_message内容解析后传入messages列表中

print(response_message.model_dump())
messages.append(response_message.model_dump())

解析结果如下:

{
'content': '',
'refusal': ,
'role': 'assistant',
'annotations': ,
'audio': ,
'function_call': ,
'tool_calls': [{
'id': 'call_0_8feaa367-c274-4c84-830f-13b49358a231',
'function': {
'arguments': '{"loc":"北京"}',
'name': 'get_weather'
},
'type': 'function',
'index': 0
}]
}

然后再将函数执行结果作为tool_message并与response_message关联后传入messages列表中:

messages.append({
"role": "tool",
"content": json.dumps(function_response), # 将回复的字典转化为json字符串
"tool_call_id": response_message.tool_calls[0].id # 将函数执行结果作为tool_message添加到messages中, 并关联返回执行函数内容的id
})
  1. 接下来,再次调用Chat模型来围绕messages进行回答。需要注意的是,此时不再需要向模型重复提问,只需要简单的将我们已经准备好的messages传入Chat模型即可:
second_response = client.chat.completions.create(
model="deepseek-chat",
messages=messages)

print(second_response.choices[0].message.content)

下面看大模型的输出结果,很明显大模型接收到了函数执行的结果,并进一步处理得到输出,同时天气和气温的输出也是正确的,这样我们就基于function calling技术完成一个简单的智能体了!

14.png

3.5 代码优化

以上步骤详细描述了Fucntion Calling的技术细节,执行流程图如下:

1.png

开发一个智能体需要将上面流程串起来,下一步我们编写一个能够自动执行外部函数调用的Chat智能体函数, 参数messages为输入到Chat模型的messages参数对象, 参数api_key为调用模型的API-KEY ,参数tools设置为包含全部外部函数的列表对象, 参数model默认为deepseek-chat , 该函数返回结果为大模型根据function calling内容的回复, 函数的具体代码如下:

def run_conv(messages,
api_key,
tools=,
functions_list=,
model="deepseek-chat"):
user_messages = messages

client = OpenAI(api_key=api_key,
base_url="https://api.deepseek.com")

# 如果没有外部函数库,则执行普通的对话任务
if tools == :
response = client.chat.completions.create(
model=model,
messages=user_messages
)
final_response = response.choices[0].message.content

# 若存在外部函数库,则需要灵活选取外部函数并进行回答
else:
# 创建外部函数库字典
available_functions = {func.__name__: func for func in functions_list}

# 创建包含用户问题的message
messages = user_messages

# first response
response = client.chat.completions.create(
model=model,
messages=user_messages,
tools=tools,
)
response_message = response.choices[0].message

# 获取函数名
function_name = response_message.tool_calls[0].function.name
# 获取函数对象
fuction_to_call = available_functions[function_name]
# 获取函数参数
function_args = json.loads(response_message.tool_calls[0].function.arguments)

# 将函数参数输入到函数中,获取函数计算结果
function_response = fuction_to_call(**function_args)

# messages中拼接first response消息
user_messages.append(response_message.model_dump())

# messages中拼接外部函数输出结果
user_messages.append(
{
"role": "tool",
"content": json.dumps(function_response),
"tool_call_id": response_message.tool_calls[0].id
}
)

# 第二次调用模型
second_response = client.chat.completions.create(
model=model,
messages=user_messages)

# 获取最终结果
final_response = second_response.choices[0].message.content

return final_response

以上函数的流程就十分清晰啦,调用该函数测试一下结果~

ds_api_key = '你的api key'
messages = [{"role": "user", "content": "请问上海今天天气如何?"}]
get_weather_function = {
'name': 'get_weather',
'description': '查询即时天气函数,根据输入的城市名称,查询对应城市的实时天气',
'parameters': {
'type': 'object',
'properties': { # 参数说明
'loc': {
'description': '城市名称',
'type': 'string'
}
},
'required': ['loc'] # 必备参数
}
}
tools = [
{
"type": "function",
"function": get_weather_function
}
]
final_response = run_conv(messages=messages,
api_key=ds_api_key,
tools=tools,
functions_list=[get_weather])
print(final_response)

15.png

四、总结与展望

本文我们详细讲解了大模型 function calling技术并基于该技术开发了天气智能体。Function Calling技术是AI Agent实现的关键,它让大模型不再只是简单的聊天回复,更可以"上天入地”完成各种各样的事。

然而在开发过程中我们也发现,function calling 技术开发过程冗长,需要编写相应的能力函数,有没有什么办法可以做到函数复用或简化开发呢,这就需要用到2025年最流行的Agent开发技术——MCP协议,什么是MCP协议呢?我们下一篇文章给大家分享~

感兴趣大家可关注微信公众号:大模型真好玩,工作开发中的大模型经验、教程和工具免费分享,大家快来看看吧~


作者:大模型真好玩
来源:juejin.cn/post/7486323379474645027

收起阅读 »

JDK 24 发布,新特性解读!

真快啊!Java 24 这两天已经正式发布啦!这是自 Java 21 以来的第三个非长期支持版本,和 Java 22、Java 23一样。 下一个长期支持版是 Java 25,预计今年 9 月份发布。 Java 24 带来的新特性还是蛮多的,一共 24 个。J...
继续阅读 »

真快啊!Java 24 这两天已经正式发布啦!这是自 Java 21 以来的第三个非长期支持版本,和 Java 22Java 23一样。


下一个长期支持版是 Java 25,预计今年 9 月份发布。


Java 24 带来的新特性还是蛮多的,一共 24 个。Java 23 和 Java 23 都只有 12 个,Java 24的新特性相当于这两次的总和了。因此,这个版本还是非常有必要了解一下的。


下图是从 JDK8 到 JDK 24 每个版本的更新带来的新特性数量和更新时间:



我在昨天晚上详细看了一下 Java 24 的详细更新,并对其中比较重要的新特性做了详细的解读,希望对你有帮助!


本文内容概览



JEP 478: 密钥派生函数 API(预览)


密钥派生函数 API 是一种用于从初始密钥和其他数据派生额外密钥的加密算法。它的核心作用是为不同的加密目的(如加密、认证等)生成多个不同的密钥,避免密钥重复使用带来的安全隐患。 这在现代加密中是一个重要的里程碑,为后续新兴的量子计算环境打下了基础


通过该 API,开发者可以使用最新的密钥派生算法(如 HKDF 和未来的 Argon2):


// 创建一个 KDF 对象,使用 HKDF-SHA256 算法
KDF hkdf = KDF.getInstance("HKDF-SHA256");

// 创建 Extract 和 Expand 参数规范
AlgorithmParameterSpec params =
HKDFParameterSpec.ofExtract()
.addIKM(initialKeyMaterial) // 设置初始密钥材料
.addSalt(salt) // 设置盐值
.thenExpand(info, 32); // 设置扩展信息和目标长度

// 派生一个 32 字节的 AES 密钥
SecretKey key = hkdf.deriveKey("AES", params);

// 可以使用相同的 KDF 对象进行其他密钥派生操作

JEP 483: 提前类加载和链接


在传统 JVM 中,应用在每次启动时需要动态加载和链接类。这种机制对启动时间敏感的应用(如微服务或无服务器函数)带来了显著的性能瓶颈。该特性通过缓存已加载和链接的类,显著减少了重复工作的开销,显著减少 Java 应用程序的启动时间。测试表明,对大型应用(如基于 Spring 的服务器应用),启动时间可减少 40% 以上。


这个优化是零侵入性的,对应用程序、库或框架的代码无需任何更改,启动也方式保持一致,仅需添加相关 JVM 参数(如 -XX:+ClassDataSharing)。


JEP 484: 类文件 API


类文件 API 在 JDK 22 进行了第一次预览(JEP 457),在 JDK 23 进行了第二次预览并进一步完善(JEP 466)。最终,该特性在 JDK 24 中顺利转正。


类文件 API 的目标是提供一套标准化的 API,用于解析、生成和转换 Java 类文件,取代过去对第三方库(如 ASM)在类文件处理上的依赖。


// 创建一个 ClassFile 对象,这是操作类文件的入口。
ClassFile cf = ClassFile.of();
// 解析字节数组为 ClassModel
ClassModel classModel = cf.parse(bytes);

// 构建新的类文件,移除以 "debug" 开头的所有方法
byte[] newBytes = cf.build(classModel.thisClass().asSymbol(),
classBuilder -> {
// 遍历所有类元素
for (ClassElement ce : classModel) {
// 判断是否为方法 且 方法名以 "debug" 开头
if (!(ce instanceof MethodModel mm
&& mm.methodName().stringValue().startsWith("debug"))) {
// 添加到新的类文件中
classBuilder.with(ce);
}
}
});

JEP 485: 流收集器


流收集器 Stream::gather(Gatherer) 是一个强大的新特性,它允许开发者定义自定义的中间操作,从而实现更复杂、更灵活的数据转换。Gatherer 接口是该特性的核心,它定义了如何从流中收集元素,维护中间状态,并在处理过程中生成结果。


与现有的 filtermapdistinct 等内置操作不同,Stream::gather 使得开发者能够实现那些难以用标准 Stream 操作完成的任务。例如,可以使用 Stream::gather 实现滑动窗口、自定义规则的去重、或者更复杂的状态转换和聚合。 这种灵活性极大地扩展了 Stream API 的应用范围,使开发者能够应对更复杂的数据处理场景。


基于 Stream::gather(Gatherer) 实现字符串长度的去重逻辑:


var result = Stream.of("foo", "bar", "baz", "quux")
.gather(Gatherer.ofSequential(
HashSet::new, // 初始化状态为 HashSet,用于保存已经遇到过的字符串长度
(set, str, downstream) -> {
if (set.add(str.length())) {
return downstream.push(str);
}
return true; // 继续处理流
}
))
.toList();// 转换为列表

// 输出结果 ==> [foo, quux]

JEP 486: 永久禁用安全管理器


JDK 24 不再允许启用 Security Manager,即使通过 java -Djava.security.manager命令也无法启用,这是逐步移除该功能的关键一步。虽然 Security Manager 曾经是 Java 中限制代码权限(如访问文件系统或网络、读取或写入敏感文件、执行系统命令)的重要工具,但由于复杂性高、使用率低且维护成本大,Java 社区决定最终移除它。


JEP 487: 作用域值 (第四次预览)


作用域值(Scoped Values)可以在线程内和线程间共享不可变的数据,优于线程局部变量,尤其是在使用大量虚拟线程时。


final static ScopedValue<...> V = new ScopedValue<>();

// In some method
ScopedValue.where(V, <value>)
.run(() -> { ... V.get() ... call methods ... });

// In a method called directly or indirectly from the lambda expression
... V.get() ...

作用域值允许在大型程序中的组件之间安全有效地共享数据,而无需求助于方法参数。


JEP 491: 虚拟线程的同步而不固定平台线程


优化了虚拟线程与 synchronized 的工作机制。 虚拟线程在 synchronized 方法和代码块中阻塞时,通常能够释放其占用的操作系统线程(平台线程),避免了对平台线程的长时间占用,从而提升应用程序的并发能力。 这种机制避免了“固定 (Pinning)”——即虚拟线程长时间占用平台线程,阻止其服务于其他虚拟线程的情况。


现有的使用 synchronized 的 Java 代码无需修改即可受益于虚拟线程的扩展能力。 例如,一个 I/O 密集型的应用程序,如果使用传统的平台线程,可能会因为线程阻塞而导致并发能力下降。 而使用虚拟线程,即使在 synchronized 块中发生阻塞,也不会固定平台线程,从而允许平台线程继续服务于其他虚拟线程,提高整体的并发性能。


JEP 493:在没有 JMOD 文件的情况下链接运行时镜像


默认情况下,JDK 同时包含运行时镜像(运行时所需的模块)和 JMOD 文件。这个特性使得 jlink 工具无需使用 JDK 的 JMOD 文件就可以创建自定义运行时镜像,减少了 JDK 的安装体积(约 25%)。


说明:



  • Jlink 是随 Java 9 一起发布的新命令行工具。它允许开发人员为基于模块的 Java 应用程序创建自己的轻量级、定制的 JRE。

  • JMOD 文件是 Java 模块的描述文件,包含了模块的元数据和资源。


JEP 495: 简化的源文件和实例主方法(第四次预览)


这个特性主要简化了 main 方法的的声明。对于 Java 初学者来说,这个 main 方法的声明引入了太多的 Java 语法概念,不利于初学者快速上手。


没有使用该特性之前定义一个 main 方法:


public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}

使用该新特性之后定义一个 main 方法:


class HelloWorld {
void main() {
System.out.println("Hello, World!");
}
}

进一步简化(未命名的类允许我们省略类名)


void main() {
System.out.println("Hello, World!");
}

JEP 497: 量子抗性数字签名算法 (ML-DSA)


JDK 24 引入了支持实施抗量子的基于模块晶格的数字签名算法 (Module-Lattice-Based Digital Signature Algorithm, ML-DSA),为抵御未来量子计算机可能带来的威胁做准备。


ML-DSA 是美国国家标准与技术研究院(NIST)在 FIPS 204 中标准化的量子抗性算法,用于数字签名和身份验证。


JEP 498: 使用 sun.misc.Unsafe 内存访问方法时发出警告


JDK 23(JEP 471) 提议弃用 sun.misc.Unsafe 中的内存访问方法,这些方法将来的版本中会被移除。在 JDK 24 中,当首次调用 sun.misc.Unsafe 的任何内存访问方法时,运行时会发出警告。


这些不安全的方法已有安全高效的替代方案:



  • java.lang.invoke.VarHandle :JDK 9 (JEP 193) 中引入,提供了一种安全有效地操作堆内存的方法,包括对象的字段、类的静态字段以及数组元素。

  • java.lang.foreign.MemorySegment :JDK 22 (JEP 454) 中引入,提供了一种安全有效地访问堆外内存的方法,有时会与 VarHandle 协同工作。


这两个类是 Foreign Function & Memory API(外部函数和内存 API) 的核心组件,分别用于管理和操作堆外内存。Foreign Function & Memory API 在 JDK 22 中正式转正,成为标准特性。


import jdk.incubator.foreign.*;
import java.lang.invoke.VarHandle;

// 管理堆外整数数组的类
class OffHeapIntBuffer {

// 用于访问整数元素的VarHandle
private static final VarHandle ELEM_VH = ValueLayout.JAVA_INT.arrayElementVarHandle();

// 内存管理器
private final Arena arena;

// 堆外内存段
private final MemorySegment buffer;

// 构造函数,分配指定数量的整数空间
public OffHeapIntBuffer(long size) {
this.arena = Arena.ofShared();
this.buffer = arena.allocate(ValueLayout.JAVA_INT, size);
}

// 释放内存
public void deallocate() {
arena.close();
}

// 以volatile方式设置指定索引的值
public void setVolatile(long index, int value) {
ELEM_VH.setVolatile(buffer, 0L, index, value);
}

// 初始化指定范围的元素为0
public void initialize(long start, long n) {
buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start,
ValueLayout.JAVA_INT.byteSize() * n)
.fill((byte) 0);
}

// 将指定范围的元素复制到新数组
public int[] copyToNewArray(long start, int n) {
return buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start,
ValueLayout.JAVA_INT.byteSize() * n)
.toArray(ValueLayout.JAVA_INT);
}
}

JEP 499: 结构化并发(第四次预览)


JDK 19 引入了结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代java.util.concurrent,目前处于孵化器阶段。


结构化并发将不同线程中运行的多个任务视为单个工作单元,从而简化错误处理、提高可靠性并增强可观察性。也就是说,结构化并发保留了单线程代码的可读性、可维护性和可观察性。


结构化并发的基本 API 是StructuredTaskScope,它支持将任务拆分为多个并发子任务,在它们自己的线程中执行,并且子任务必须在主任务继续之前完成。


StructuredTaskScope 的基本用法如下:


    try (var scope = new StructuredTaskScope<Object>()) {
// 使用fork方法派生线程来执行子任务
Future<Integer> future1 = scope.fork(task1);
Future<String> future2 = scope.fork(task2);
// 等待线程完成
scope.join();
// 结果的处理可能包括处理或重新抛出异常
... process results/exceptions ...
} // close

结构化并发非常适合虚拟线程,虚拟线程是 JDK 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。


Java 新特性系列解读


如果你想系统了解 Java 8 以及之后版本的新特性,可以在 JavaGuide 上阅读对应的文章:



比较推荐这几篇:



作者:JavaGuide
来源:juejin.cn/post/7483478667143626762
收起阅读 »

无虚拟DOM到底能快多少?

web
相信大家都跟我一样有这样一个疑问:之前都说虚拟DOM是为了性能,怎么现在又反向宣传了?无虚拟DOM怎么又逆流而上成为了性能的标杆了呢? 下篇文章我们会仔细分析无虚拟DOM与虚拟DOM之间的区别,本篇文章我们先看数据。首先是体积,由于没有了虚拟DOM以及vDOM...
继续阅读 »

相信大家都跟我一样有这样一个疑问:之前都说虚拟DOM是为了性能,怎么现在又反向宣传了?无虚拟DOM怎么又逆流而上成为了性能的标杆了呢?


下篇文章我们会仔细分析无虚拟DOM与虚拟DOM之间的区别,本篇文章我们先看数据。首先是体积,由于没有了虚拟DOM以及vDOM diff算法,所以体积肯定能小不少。当然不是说无虚拟DOM就彻底不需要diff算法了,我看过同为无虚拟DOM框架的SvelteSolid源码,无虚拟DOM只是不需要vDOM间的Diff算法,列表之间还是需要diff的。毕竟再怎么编译,你从后端获取到的数组,编译器也不可能预测得到。


那么官方给出的数据是:



虽然没有想象中的那么多,但33.6%也算是小不少了。当然这个数据指的是纯Vapor模式,如果你把虚拟DOMVapor混着用的话,体积不仅不会减小反而还会增加。毕竟会同时加载Vapor模式的runtime和虚拟DOM模式的runtime,二者一相加就大了。



Vapor模式指的就是无虚拟DOM模式,如果你不太清楚二者之间有何关联的话,可以看一眼这篇:《无虚拟DOM版Vue为什么叫Vapor》



那性能呢?很多人对体积其实并不敏感,觉得多10K10k都无所谓,毕竟现在都是5G时代了。所以我们就来看一眼官方公布的性能数据:


SCR-20250301-scoj.png


SCR-20250301-scsm.png


从左到右依次为:



  • 原生JS:1.01

  • Solid:1.09

  • Svelte:1.11

  • 无虚拟DOMVue:1.24

  • 虚拟DOMVue:1.32

  • React:1.55


数字越小代表性能越高,但无论再怎么高都不可能高的过手动优化过的原生JS,毕竟无论什么框架最终打包编译出来的还是JS。不过框架的性能其实已经挺接近原生的了,尤其是以无虚拟DOM著称的SolidSvelte。但无虚拟DOMVue和虚拟DOMVue之间并没有拉开什么很大的差距,1.241.32这两个数字证明了其实二者的性能差距并不明显,这又是怎么一回事呢?



一个原因是Vue3本来就做了许多编译优化,包括但不限于静态提升、大段静态片段将采用innerHTML渲染、编译时打标记以帮助虚拟DOM走捷径等… 由于本来的性能就已经不错了,所以可提升的空间自然也没多少。


看完上述原因肯定有人会说:可提升的空间没多少可以理解,那为什么还比同为无虚拟DOMSolidSvelte差那么多?如果VaporSolid性能差不多的话,那可提升空间小倒是说得过去。但要是像现在这样差的还挺多的话,那这个理由是不是就有点站不住脚了?之所以会出现这样的情况是因为Vue Vapor的作者在性能优化和快速实现之间选择了后者,毕竟尤雨溪第一次公布要开始做无虚拟DOMVue的时间是在2020年:


c74b06f21c3c437156f444a905debc9e.jpeg


而如今已经是2025年了。也就是说如果一个大一新生第一次满心欢喜的听到无虚拟DOMVue的消息后,那么他现在大概率已经开始毕业工作了都没能等到无虚拟DOMVue的发布。这个时间拖的有点太长了,甚至从Vue2Vue3都没用这么久。


SCR-20250307-okfk.png


可以看到Vue3从立项到发布也就不到两年的时间,而Vapor呢?从立项到现在已经将近5年的光阴了,已经比Vue3所花费的时间多出一倍还多了。所以现在的首要目标是先实现出来一版能用的,后面再慢慢进行优化。我们工作不也是这样么?先优先把功能做出来,优化的事以后再说。那么具体该怎么优化呢:


SCR-20250301-scwz.png


现在的很多渲染逻辑继承自原版Vue3,但无虚拟DOMVue可以采用更优的渲染逻辑。而且现在的Vapor是跟着原版Vue的测试集来做的,这也是为了实现和原版Vue一样的行为。但这可能并不是最优解,之后会慢慢去掉一些不必要的功能,尤其是对性能产生较大影响的功能。


往期精彩文章:



作者:页面魔术
来源:juejin.cn/post/7480069116461088822
收起阅读 »

leaflet+天地图+更换地图主题

web
先弄清楚leaflet和天地图充当的角色 leaflet是用来在绘制、交互地图的 天地图是纯粹用来当个底图的,相当于在leaflet中底部的一个图层而已 进行Marker打点、geojson绘制等操作都是使用leaflet实现 1. 使用天地图当底图 在...
继续阅读 »

在这里插入图片描述


先弄清楚leaflet和天地图充当的角色



  • leaflet是用来在绘制、交互地图的

  • 天地图是纯粹用来当个底图的,相当于在leaflet中底部的一个图层而已

  • 进行Marker打点、geojson绘制等操作都是使用leaflet实现


1. 使用天地图当底图



  • 在token处填自己的token

  • 我这里用的是天地图的影像底图,如果需要可自行更换或添加底图

  • 天地图底图网址:lbs.tianditu.gov.cn/server/MapS…

  • 只用替换我代码里的天地图链接里的http://{s}.tianditu.com/img_c/里的img_c为我图中圈起来的编号,其他不用动


在这里插入图片描述


const token = "填自己的天地图token";
// 底图
const VEC_C ="http://{s}.tianditu.com/img_c/wmts?layer=vec&style=default&tilematrixset=c&Service=WMTS&Request=GetTile&Version=1.0.0&Format=tiles&TileMatrix={z}&TileCol={x}&TileRow={y}&tk=";
// 文字标注
// const CVA_C = "http://{s}.tianditu.com/cia_c/wmts?layer=cva&style=default&tilematrixset=c&Service=WMTS&Request=GetTile&Version=1.0.0&Format=tiles&TileMatrix={z}&TileCol={x}&TileRow={y}&tk=";
let map = L.map("map", {
minZoom: 3,
maxZoom: 17,
center: [34.33213, 109.00945],
zoomSnap: 0.1,
zoom: 3.5,
zoomControl: false,
attributionControl: false,
crs: L.CRS.EPSG4326,
});
L.tileLayer(VEC_C + token, {
zoomOffset: 1,
subdomains: ["t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7"],
opacity: 0.2,
}).addTo(map);
// 添加文字标注
// L.tileLayer(CVA_C + token, {
// tileSize: 256,
// zoomOffset: 1,
// subdomains: ["t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7"],
// }).addTo(map);


2. 绘制中国地图geojson



  • 这里我需要国的边界和省的边界线颜色不一样,所以用了一个国的geojson和另一个包含省的geojson叠加来实现

  • 获取geojson数据网站:datav.aliyun.com/portal/scho…


L.geoJson(sheng, {
style: {
color: "#0c9fb8",
weight: 1,
fillColor: "#028297",
fillOpacity: 0.1,
},
}).addTo(map);
L.geoJson(guo, {
style: {
color: "#b8c3c8",
weight: 2,
fillOpacity: 0,
},
}).addTo(map);

3. 更换背景主题色


我的实现思路比较简单粗暴,直接给天地图的图层设置透明度,对div元素设置背景色,如果UI配合,可以叫UI给个遮罩层的背景图,比如我这里就是用了四周有黑边渐变阴影,中间是透明的背景图。


<div id="map"></div>
<div class="mask"></div>

#map {
height: 100vh;
width: 100vw;
background: #082941;
z-index: 2;
}

.mask {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background: url("./mask.png") no-repeat;
background-size: 100% 100%;
z-index: 1;
}

4. 完整代码



  • 写自己天地图的token

  • 自己下载geojson文件

  • 自己看需要搞个遮罩层背景图,不需要就注释掉mask


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link
href="https://cdn.bootcdn.net/ajax/libs/leaflet/1.9.4/leaflet.css"
rel="stylesheet"
/>

</head>
<style>
* {
margin: 0;
padding: 0;
}
#map {
height: 100vh;
width: 100vw;
background: #082941;
z-index: 2;
}

.mask {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background: url("./mask.png") no-repeat;
background-size: 100% 100%;
z-index: 1;
}
</style>
<body>
<div id="map"></div>
<div class="mask"></div>
</body>
<script src="https://cdn.bootcdn.net/ajax/libs/leaflet/1.9.4/leaflet.js"></script>
<script src="./china.js"></script>
<script src="./guo.js"></script>

<script>
const token = "写自己天地图的token";
// 底图
const VEC_C =
"http://{s}.tianditu.com/img_c/wmts?layer=vec&style=default&tilematrixset=c&Service=WMTS&Request=GetTile&Version=1.0.0&Format=tiles&TileMatrix={z}&TileCol={x}&TileRow={y}&tk=";
// 文字标注
// const CVA_C =
// "http://{s}.tianditu.com/cia_c/wmts?layer=cva&style=default&tilematrixset=c&Service=WMTS&Request=GetTile&Version=1.0.0&Format=tiles&TileMatrix={z}&TileCol={x}&TileRow={y}&tk=";

let map = L.map("map", {
minZoom: 3,
maxZoom: 17,
center: [34.33213, 109.00945],
zoomSnap: 0.1,
zoom: 3.5,
zoomControl: false,
attributionControl: false, //版权控制器
crs: L.CRS.EPSG4326,
});
L.tileLayer(VEC_C + token, {
zoomOffset: 1,
subdomains: ["t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7"],
opacity: 0.2,
}).addTo(map);
// L.tileLayer(CVA_C + token, {
// tileSize: 256,
// zoomOffset: 1,
// subdomains: ["t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7"],
// }).addTo(map);

L.geoJson(sheng, {
style: {
color: "#0c9fb8",
weight: 1,
fillColor: "#028297",
fillOpacity: 0.1,
},
}).addTo(map);
L.geoJson(guo, {
style: {
color: "#b8c3c8",
weight: 2,
fillOpacity: 0,
},
}).addTo(map);
</script>
</html>


作者:十一月二十二
来源:juejin.cn/post/7485482994989596722
收起阅读 »

怎么将中文数字转为阿拉伯数字?

web
说在前面 最近实现了一个b站插件,可以通过语音来控制播放页面上指定的视频,在语音识别的过程中遇到了需要将中文数字转为阿拉伯数字的情况,在这里分享一下具体事例和处理过程。 功能背景 先介绍一下功能背景,页面渲染的时候会先对视频进行编号处理,具体如下: 比如...
继续阅读 »

说在前面



最近实现了一个b站插件,可以通过语音来控制播放页面上指定的视频,在语音识别的过程中遇到了需要将中文数字转为阿拉伯数字的情况,在这里分享一下具体事例和处理过程。



功能背景


先介绍一下功能背景,页面渲染的时候会先对视频进行编号处理,具体如下:



比如我们想要播放第4个视频的话,我们只需要说“第4个”,插件就能帮我们选择第四个视频进行播放。


问题描述


功能背景我们已经了解了,那么问题是出在哪里呢?



如上图,这里识别出来的语音文本数字是中文数字,这样跟页面的视频编号无法对应上,因此我们需要实现一个方法来将中文转为阿拉伯数字。


方法实现


1、个位级映射表


const numMap = {
零: 0,一: 1,壹: 1,二: 2,两: 2,
三: 3,叁: 3,四: 4,肆: 4,五: 5,
伍: 5,六: 6,陆: 6,七: 7,柒: 7,
八: 8,捌: 8,九: 9,玖: 9,
};

2、单位映射表


const unitMap = {
十: { value: 10, sec: false },
拾: { value: 10, sec: false },
百: { value: 100, sec: false },
佰: { value: 100, sec: false },
千: { value: 1000, sec: false },
仟: { value: 1000, sec: false },
万: { value: 10000, sec: true },
萬: { value: 10000, sec: true },
亿: { value: 100000000, sec: true },
億: { value: 100000000, sec: true }
};

3、处理流程



  • 遇到数字:先存起来(比如「三」记作3)


if (hasZero && current > 0) {
current *= 10;
hasZero = false;
}
current += numMap[char];


  • 遇到单位



    • 如果是十/百/千:把存着的数字乘上倍数
      (如「三百」→3×100=300)


    current = current === 0 ? unit.value : current * unit.value;
    section += current;
    current = 0;


    • 遇到万/亿:先结算当前数字,将当前数字加到总数上


    processSection();
    section = (section + current) * unit.value;
    total += section;
    section = 0;


  • 遇到零:做个标记,提醒下个数字要占位
    (如「三百零五」→300 + 0 +5=305)


if (char === "零") {
hasZero = true;
continue;
}

4、完整代码


function chineseToArabic(chineseStr) {
// 映射表(支持简繁)
const numMap = {
零: 0,一: 1,壹: 1,二: 2,两: 2,
三: 3,叁: 3,四: 4,肆: 4,五: 5,
伍: 5,六: 6,陆: 6,七: 7,柒: 7,
八: 8,捌: 8,九: 9,玖: 9,
};
//单位映射表
const unitMap = {
十: { value: 10, sec: false },
拾: { value: 10, sec: false },
百: { value: 100, sec: false },
佰: { value: 100, sec: false },
千: { value: 1000, sec: false },
仟: { value: 1000, sec: false },
万: { value: 10000, sec: true },
萬: { value: 10000, sec: true },
亿: { value: 100000000, sec: true },
億: { value: 100000000, sec: true }
};

let total = 0; // 最终结果
let section = 0; // 当前小节
let current = 0; // 当前累加值
let hasZero = false; // 零标记

const processSection = () => {
section += current;
current = 0;
};

for (const char of chineseStr) {
if (numMap.hasOwnProperty(char)) {
if (char === "零") {
hasZero = true;
continue;
}

if (hasZero && current > 0) {
current *= 10;
hasZero = false;
}
current += numMap[char];
} else if (unitMap.hasOwnProperty(char)) {
const unit = unitMap[char];

if (unit.sec) {
// 处理万/亿分段
processSection();
section = (section + current) * unit.value;
total += section;
section = 0;
} else {
current = current === 0 ? unit.value : current * unit.value;
section += current;
current = 0;
}
hasZero = false;
}
}

const last2 = chineseStr.slice(-2)[0];
const last2Unit = unitMap[last2];
if (last2Unit) {
current = (current * last2Unit.value) / 10;
}
return total + section + current;
}

功能测试


柒億零捌拾萬



十萬三十



十萬三



二百五



二百零五





插件信息


对我上述提到的插件感兴趣的同学可以看下我前面发的这篇文章:


juejin.cn/post/748557…


公众号


关注公众号『 前端也能这么有趣 』,获取更多有趣内容。


发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~


说在后面



🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。



作者:JYeontu
来源:juejin.cn/post/7485936146071765030
收起阅读 »

好人难当,坏人不做

好人难当,以后要多注意了,涨点记性。记录三件事情证明下: 1. 免费劳动 之前和一个同学一起做一个项目,说是创业,不过项目做好了,倒是他家店铺自己用起来了,后来一直让我根据他家的需求进行修改,我也一一的改了,他倒是挺感谢我的,说是请吃饭。不过也一直没请,后面都...
继续阅读 »

好人难当,以后要多注意了,涨点记性。记录三件事情证明下:


1. 免费劳动


之前和一个同学一起做一个项目,说是创业,不过项目做好了,倒是他家店铺自己用起来了,后来一直让我根据他家的需求进行修改,我也一一的改了,他倒是挺感谢我的,说是请吃饭。不过也一直没请,后面都一年多过去了,还让我免费帮他改需求,我就说没时间,他说没时间的话可以把源码给他,他自己学着修改,我就直接把源码给他了,这个项目辛苦了一个多月,钱一毛也没赚到,我倒是搭进去一台服务器,一年花了三百多吧。现在源码给他就给他了吧,毕竟同学一场。没想到又过了半年,前段时间又找我来改需求了。这个项目他们家自己拿着赚钱,又不给我一毛钱,我相当于免费给他家做了个软件,还要出服务器钱,还要免费进行维护。我的时间是真不值钱啊,真成义务劳动了。我拒绝了,理由是忙,没时间。


总结一下,这些人总会觉得别人帮自己是理所当然的,各种得寸进尺。


2. 帮到底吧


因为我进行了仲裁,有了经验,然后被一个人加了好友,是一个前同事(就是我仲裁的那家公司),然后这哥们各种问题我都尽心回答,本着能帮别人一点就帮别人一点的想法,但是我免费帮他,他仲裁到手多少钱,又不会给我一毛钱。这哥们一个问题接一个,我都做了回答,后来直接要求用我当做和公司谈判的筹码,我严词拒绝了,真不知道这人咋想的,我帮你并没有获得任何好处,你这个要求有点过分了,很简单,他直接把我搬出来和公司谈判,公司肯定会找我,会给我带来麻烦,这人一点也没想这些事。所以之后他再询问有关任何我的经验,我已经不愿意帮他了。


总结一下,这些人更进一步,甚至想利用下帮自己的人,不考虑会给别人带来哪些困扰。


3. 拿你顶缸


最近做了通过一个亲戚接了一个项目,而这个亲戚的表姐是该项目公司的领导,本来觉得都是有亲戚关系的,项目价格之类开始问了,他们没说,只是说根据每个人的工时进行估价,后面我们每个人提交了个人报价,然后还是一直没给明确答复,本着是亲戚的关系,觉得肯定不会坑我。就一直做下去了,直到快做完了,价格还是没有出来,我就直接问了这个价格的事情,第二天,价格出来了,在我报价基础上直接砍半。我当然不愿意了,后来经过各种谈判,我终于要到了一个勉强可以的价格,期间群里谈判也是我一个人在说话,团队的其他人都不说话。后来前端的那人问我价格,我也把过程都实话说了,这哥们也要加价,然后就各种问我,我也啥都告他了。后来这个前端在那个公司领导(亲戚表姐)主动亮明身份,她知道这个前端和那个亲戚关系好,然后这个前端立马不好意思加价了,并且还把锅甩我头上,说是我没有告诉他她是他姐。还说我不地道,我靠,你自己要加价,关我啥事,你加钱也没说要分我啊,另外我给自己加价的时候你也没帮忙说话啊,我告诉你我加价成功了是我好心,也是想着你能加点就加点吧,这时候你为了面子不加了,然后说成要加价的理由是因为我,真是没良心啊。后面还问我关于合同的事情,我已经不愿意回答他了,让他自己找对面公司问去。


总结一下,这些人你帮他了他当时倒是很感谢你,但是一旦结果有变,会直接怪罪到你头上。


4. 附录文章


这个文章说得挺好的《你的善良,要有锋芒》


你有没有发现,善良的人,心都很软,他们不好意思拒绝别人,哪怕为难了自己,也要想办法帮助身边的人。善良的人,心都很细,他们总是照顾着别人的情绪,明明受委屈的是自己,却第一时间想着别人会不会难过。


也许是习惯了对别人好,你常常会忽略自己的感受。有时候你知道别人是想占你便宜,你也知道别人不是真心把你当朋友,他们只是觉得你好说话,只是看中了你的善良,但是你没有戳穿,你还是能帮就帮,没有太多怨言。


你说你不想得罪人,你说你害怕被孤立,可是有人在乎过你吗?


这个世界上形形色色的人很多,有人喜欢你,有人讨厌你,你没有办法做到对每一个人好,也没办法要求每一个人都是真心爱你。所以你要有自己的选择,与舒服的人相处,对讨厌的人远离,过你自己觉得开心自在的生活就好,没必要为了便利别人,让自己受尽委屈。


看过一段话:善良是很珍贵的,但善良要是没有长出牙齿来,那就是软弱。


你的善良,要有锋芒,不要把时间浪费在不值得的人身上。对爱你的人,倾心相助,对利用你的人,勇敢说不。


愿你的善良,能被真心的人温柔以待。


作者:一线大码
来源:juejin.cn/post/7455667125798780980
收起阅读 »

如何优雅的回复面试官问:“你能接受加班吗?”

面试官问:“你能接受加班吗?”我脑袋嗡的一声,余音绕梁三日不绝于耳。 那一刻,我简直觉得自己像被突然砸中脑袋,脑袋里嗡的一声,余音绕梁三日。作为一个职场小白,这种问题简直颠覆了我对面试的认知。于是,我一时心血来潮,脱口而出一句:“领导抗揍吗?” 结果,大家猜到...
继续阅读 »

面试官问:“你能接受加班吗?”我脑袋嗡的一声,余音绕梁三日不绝于耳。


那一刻,我简直觉得自己像被突然砸中脑袋,脑袋里嗡的一声,余音绕梁三日。作为一个职场小白,这种问题简直颠覆了我对面试的认知。于是,我一时心血来潮,脱口而出一句:“领导抗揍吗?” 结果,大家猜到了,面试是上午结束的,Offer是当天中午凉的。


是牛是马


如何巧妙回答


“我认为加班是工作中不可避免的一部分,尤其是在一些特殊项目或紧急情况下。我非常热爱我的工作,并且对公司的发展充满信心,因此我愿意为了团队的成功付出额外的努力。当然,我也注重工作效率和时间管理,尽量在正常工作时间内完成任务。如果确实需要加班,我也会根据公司合理的安排,积极的响应。”


作为一名资深的面试官,今天面对这个问题,坐下来和大家聊聊应该怎么回答呢?面试官究竟喜欢怎样的回答?让我们深入分析一下。


面试官的心理


在职场中,想要出色地应对面试,需要具备敏锐的观察力和理解力。学会细致入微地观察,善于捕捉每一个细微的线索,这样才能在面试中游刃有余。懂的察言观色,方能尽显英雄本色。


请在此添加图片描述


面试官的考量点



  • 评估工作稳定性


面试官提出“能否接受加班”的问题,旨在深入了解求职者的职业稳定性和对加班安排的适应性。这一评估有助于预测求职者入职后的表现和长期留任的可能性。工作稳定性是企业考量员工的关键指标之一,通过这一问题,面试官能够洞察求职者的职业发展规划及其对未来工作的期望。



  • 筛选合适的候选人


通过询问加班的接受度,面试官筛选出那些愿意为达成工作目标而投入额外时间和精力的候选人。这种筛选方式有助于确保团队的整体运作效率和协作精神。合适的候选人不仅能快速融入团队,还能显著提升工作效率。因此,面试官借此问题寻找最匹配岗位需求的员工。



  • 了解求职者的价值观


面试官还利用这个问题来探查求职者的价值观和工作态度,以此判断他们是否与公司的文化和核心价值观相契合。员工的价值观和态度对公司的长远发展起着至关重要的作用。通过这一询问,面试官能够确保求职者的个人目标与公司的发展方向保持一致,从而促进整体的和谐与进步。


考察的问题的意义


要理解问题的本质……为什么面试官会提出这样的问题?难道是因为你的颜值过高,引发了他的嫉妒?


请在此添加图片描述



  • 工作态度


面试官通过询问加班的接受度,旨在评估求职者是否展现出积极的工作态度和强烈的责任心。在许多行业中,加班已成为常态,面试官借此问题了解求职者是否愿意在工作上投入额外的时间和精力。积极的工作态度和责任心是职场成功的关键因素,通过这一问题,面试官能够初步判断求职者是否适应高强度的工作环境。



  • 岗位匹配度


特定岗位因其工作性质可能需要频繁加班。面试官通过提出加班相关的问题,旨在了解求职者是否能适应这类岗位的工作强度。由于不同岗位对工作强度的要求各异,面试官希望通过这一问题确保求职者对即将承担的角色有明确的认识,从而防止入职后出现期望不一致的情况。



  • 抗压能力


加班往往伴随压力,面试官通过这一问题考察求职者的抗压能力和情绪管理技巧。抗压能力对于职场成功至关重要,面试官借此了解求职者在高压环境下的表现,以判断其是否符合公司的需求。



  • 公司文化


面试官还利用这个问题来评估求职者对公司加班文化的接受程度,以此判断其价值观是否与公司相符。公司文化对员工的工作体验和满意度有着深远的影响,面试官希望通过这一问题确保求职者能够认同并融入公司文化。


回答的艺术


“知己知彼,百战不殆。”在面试中,回答问题的关键在于展现出积极和正向的态度。


请在此添加图片描述



  • 积极态度


在回答有关加班的问题时,表达你对工作的热爱和对公司的忠诚,强调你愿意为了团队的成功而付出额外的努力。这种积极的态度不仅展示了你的职业素养和对工作的热情,还能显著提升面试官对你的好感。


例如:“我非常热爱我的工作,并且对公司的发展充满信心。我相信为了实现公司的目标和团队的成功,适当的加班是可以接受的。”



  • 灵活性和效率


强调你在时间管理和工作效率上的能力,表明你在确保工作质量的同时,会尽可能减少加班的需求。灵活性和效率是职场中极为重要的技能,面试官可以通过这个问题了解你的实际工作表现。


例如:“我在工作中注重效率和时间管理,通常能够在规定的工作时间内完成任务。当然,如果有特殊情况需要加班,我也会全力以赴。”



  • 平衡工作与生活


适当地提到你对工作与生活平衡的重视,并希望公司在安排加班时能够充分考虑到员工的个人需求。平衡工作与生活是职场人士普遍关注的问题,面试官通过这个问题可以了解你的个人需求和期望。


例如:“我非常重视工作与生活的平衡,希望在保证工作效率的同时,也能有足够的时间陪伴家人和进行个人活动。如果公司能够合理安排加班时间,我会非常乐意配合。”



  • 适度反问


在回答时,可以适当地向面试官询问关于公司加班的具体情况,以便更全面地了解公司的加班文化和预期。这样的反问可以展现你的主动性和对公司的兴趣,有助于获取更多信息,做出更加明智的回答。


例如:“请问公司通常的加班情况是怎样的?是否有相关的加班补偿或调休安排?”


最后


所谓士为知己者死,遇良将则冲锋陷阵,择良人则共谋天下。在职场这场没有硝烟的战争中,我们每个人都是一名战士,寻找着属于自己的知己和良将。当面试官提出挑战性问题时,我们不仅要展示自己的能力和才华,更要表现出对工作的热爱和对公司的忠诚。


面对“你能接受加班吗?”这样的问题,我们应以积极的态度、灵活的思维和对工作与生活平衡的重视来回应。这样的回答不仅能展示我们的职业素养,还能让我们在众多求职者中脱颖而出,赢得面试官的青睐。


正如士为知己者死,我们在职场中也要找到那个能理解我们、支持我们的知己;遇良将则冲锋陷阵,我们要在优秀的领导下发挥自己的潜能,为公司的发展贡献力量;择良人则共谋天下,我们要与志同道合的同事共同努力,实现职业生涯的辉煌。


总之一句话,在面试中展现出积极向上的形象,不仅能为我们的职业生涯加分,更能让我们在职场上找到属于自己的价值和归属感。让我们以这句话为指引,勇敢地迎接职场的挑战,书写属于自己的辉煌篇章。


作者:不惑_
来源:juejin.cn/post/7457211584709066792
收起阅读 »

npm和npx的区别

web
npx 和 npm 是 Node.js 生态中两个密切相关的工具,但它们的功能和用途有显著区别: 1. npm(Node Package Manager) 定位:Node.js 的包管理工具,用于安装、管理和发布 JavaScript 包。 核心功能: ...
继续阅读 »

npxnpm 是 Node.js 生态中两个密切相关的工具,但它们的功能和用途有显著区别:




1. npm(Node Package Manager)



  • 定位:Node.js 的包管理工具,用于安装、管理和发布 JavaScript 包。

  • 核心功能

    • 安装依赖:通过 npm install <package> 安装包到本地或全局。

    • 管理项目依赖:通过 package.json 文件记录依赖版本。

    • 运行脚本:通过 npm run <script> 执行 package.json 中定义的脚本。

    • 发布包:通过 npm publish 将代码发布到 npm 仓库。



  • 示例
    npm install lodash         # 安装 lodash 到本地 node_modules
    npm install -g typescript # 全局安装 TypeScript
    npm run start # 运行 package.json 中的 "start" 脚本





2. npx(Node Package Executor)



  • 定位npm 的配套工具,用于直接执行包中的命令,无需全局或本地安装

  • 核心功能

    • 临时执行包:自动下载远程包并运行,完成后删除。

    • 运行本地已安装的包:直接调用本地 node_modules/.bin 中的命令。

    • 切换包版本:指定特定版本运行(如 npx node@14 myscript.js)。



  • 示例
    npx create-react-app my-app  # 临时下载并运行 create-react-app
    npx eslint . # 运行本地安装的 eslint
    npx http-server # 启动一个临时 HTTP 服务器





关键区别


特性npmnpx
主要用途安装和管理依赖直接执行包中的命令
是否需要安装包需要提前安装(本地或全局)可临时下载并执行,无需提前安装
典型场景管理项目依赖、运行脚本、发布包运行一次性命令、测试工具、脚手架
执行本地包命令需通过 npm run 或完整路径调用直接通过 npx <command> 调用
全局包依赖依赖全局安装的包不依赖全局包,可指定版本运行



为什么需要 npx



  1. 避免全局污染

    例如运行 create-react-app 时,无需全局安装,直接通过 npx 临时调用最新版本。

  2. 简化本地包调用

    本地安装的工具(如 eslintjest)可以直接用 npx 执行,无需配置 package.json 脚本或输入冗长路径。

  3. 兼容多版本

    可指定版本运行,如 npx node@14 myscript.js,避免全局版本冲突。




使用建议



  • npm

    管理项目依赖、定义脚本、发布包。

  • npx

    运行脚手架工具(如 create-react-app)、临时工具(如 http-server)或本地已安装的命令。




示例场景


# 使用 npm 安装依赖
npm install axios

# 使用 npx 运行一次性工具
npx json-server db.json # 临时启动一个 REST API 服务器

# 使用 npm 运行脚本(需在 package.json 中定义 "scripts")
npm run build

# 使用 npx 调用本地已安装的包
npx webpack --config webpack.config.js

通过合理使用 npmnpx,可以更高效地管理依赖和执行命令。


作者:溪森堡
来源:juejin.cn/post/7484992785952096267
收起阅读 »

TypeScript 官方宣布弃用 Enum?Enum 何罪之有?

web
1. 官方真的不推荐 Enum 了吗? 1.1 事情的起因 起因是看到 科技爱好者周刊(第 340 期) 里面推荐了一篇文章,说是官方不再推荐使用 enum 语法,原文链接在 An ode to TypeScript enums,大致是说 TS 新出了一个 -...
继续阅读 »

1. 官方真的不推荐 Enum 了吗?


1.1 事情的起因


起因是看到 科技爱好者周刊(第 340 期) 里面推荐了一篇文章,说是官方不再推荐使用 enum 语法,原文链接在 An ode to TypeScript enums,大致是说 TS 新出了一个 --erasableSyntaxOnly 配置,只允许使用可擦除语法,但是 enum 不可擦除,因此推断官方已不再推荐使用 enum 了。官方并没有直接表达不推荐使用,那官方为什么要出这个配置项呢?


image.png


1.2 什么是可擦除语法


就在上周,TypeScript 发布了 5.8 版本,其中有一个改动是添加了 --erasableSyntaxOnly 配置选项,开启后仅允许使用可擦除语法,否则会报错enum 就是一个不可擦除语法,开启 erasableSyntaxOnly 配置后,使用 enum 会报错。


例如,如果在 tsconfig 文件中配置 "erasableSyntaxOnly": true(只允许可擦除语法),此时使用不可擦除语法,将会得到报错:
image.png


可擦除语法就是可以直接去掉的、仅在编译时存在、不会生成额外运行时代码的语法,例如 typeinterface。不可擦除语法就是不能直接去掉的、需要编译为JS且会生成额外运行时代码的语法,例如 enumnamesapce(with runtime code)。 具体举例如下:


可擦除语法,不生成额外运行时代码,比如 typelet n: numberinterfaceas number 等:
image.png


不可擦除语法,生成额外运行时代码,比如 enumnamespace(具有运行时行为的)、类属性参数构造语法糖(Class Parameter properties)等:


// 枚举类型
enum METHOD {
ADD = 'add'
}

// 类属性参数构造
class A {
constructor(public x: number) {}
}
let a: number = 1
console.log(a)

image.png


需要注意,具有运行时行为的 namespace 才属于不可擦除语法。


// 不可擦除,具有运行时逻辑
namespace MathUtils {
  export function add(a: number, b: number): number {
    return a + b;
  }
}

// 可擦除,仅用于类型声明,不包含任何运行时逻辑
namespace Shapes {
  export interface Rectangle {
    width: number;
    height: number;
  }
}

image.png


1.3 TS 官方为什么要出 erasableSyntaxOnly?


官方既然没有直接表达不推荐 enum,那为什么要出 erasableSyntaxOnly 配置来排除 enum 呢?


我找到了 TS 官方文档(The --erasableSyntaxOnly Option)说明:


image.png


大致意思是说之前 Node 新版本中支持了执行 TS 代码的能力,可以直接运行包含可擦除语法的 TypeScript 文件。Node 将用空格替换 TypeScript 语法,并且不执行类型检查。总结下来就是:


在 Node 22 版本:



  • 需要配置 --experimental-transform-types 执行支持 TS 文件

  • 要禁用 Node 这种特性,使用参数 --no-experimental-strip-types


在 Node 23.6.0 版本:



  • 默认支持直接运行可擦除语法的 TS 文件,删除参数 --no-experimental-strip-types

  • 对于不可擦除语法,使用参数 --experimental-transform-types


综上所述,TS 官方为了配合 Node.js 这次改动(即默认允许直接执行不可擦除语法的 TS 代码),才添加了一个配置项 erasableSyntaxOnly,只允许可擦除语法。


2. Enum 的三大罪行


自 Enum 从诞生以来,它一直是前端界最具争议的特性之一,许多前端开发者乃至不少大佬都对其颇有微词,纷纷发起了 DO NOT USE TypeScript Enum 的吐槽。那么enum 真的有那么难用吗?我认为是的,这玩意坑还挺多的,甲级战犯 Enum,出列!


2.1 枚举默认值


enum 默认的枚举值从 0 开始,这还不是最关键的,你传入了默认枚举值时,居然是合法的,这无形之中带来了类型安全问题。


enum METHOD {
ADD
}

function doAction(method: METHOD) {
// some code
}

doAction(METHOD.ADD) // ✅ 可以
doAction(0) // ✅ 可以

2.2 不支持枚举值字面量


还有一种场景,我要求既可以传入枚举类型,又要求传入枚举值字面量,如下所示,但是他又不合法了?(有人说你定义传枚举类型就要传相应的枚举,这没问题,但是上面提到的问题又是怎么回事呢?这何尝不是 Enum 的双标?)


enum METHOD {
ADD = 'add'
}

function doAction(method: METHOD) {
// some code
}

doAction(METHOD.ADD) // ✅ 可以
doAction('add') // ❌ 不行

2.3 增加运行时开销


TypeScript 的 enum 在编译后会生成额外的 JavaScript 双向映射数据,这会增加运行时的开销。


image.png


3. Enum 的替代方案


众所周知,TS 一大特性是类型变换,我们可以通过类型操作组合不同类型来达到目标类型,又称为类型体操。下面的四种解决方案,可以根据实际需求来选择。


3.1 const enum


const enum 是解决产生额外生成的代码和额外的间接成本有效且快捷的方法,但不推荐使用。



const enum 由于编译时内联带来了性能优化,但在 .d.ts 文件、isolatedModules 兼容性、版本不匹配及运行时缺少 .js 文件等场景下存在隐藏陷阱,可能导致难以发现的 bug。详见官方说明:const-enum-pitfalls



const enum METHOD {
ADD = 'add',
DELETE = 'delete',
UPDATE = 'update',
QUERY = 'query',
}

function doAction(method: METHOD) {
// some code
}

doAction(METHOD.ADD) // ✅ 可行
doAction('delete') // ❌ 不行

const enum 解析后的代码中引用 enum 的地方将直接被替换为对应的枚举值:


image.png


3.2 模板字面量类型


将枚举类型包装为模板字面量类型(Template Literal Types),从而即支持枚举类型,又支持枚举值字面量,但是没有解决运行时开销问题。


enum METHOD {
ADD = 'add',
DELETE = 'delete',
UPDATE = 'update',
QUERY = 'query',
}

type METHOD_STRING = `${METHOD}`

function doAction(method: METHOD_STRING) {
// some code
}

doAction(METHOD.ADD) // ✅ 可行
doAction('delete') // ✅ 可行
doAction('remove') // ❌ 不行


image.png


3.3 联合类型(Union Types)


使用联合类型,引用时可匹配的值限定为指定的枚举值了,但同时也没有一个地方可以统一维护枚举值,如果一旦枚举值有调整,其他地方都需要改。


type METHOD =
| 'add'
/**
* @deprecated 不再支持删除
*/

| 'delete'
| 'update'
| 'query'


function doAction(method: METHOD) {
// some code
}

doAction('delete') // ✅ 可行,没有 TSDoc 提示
doAction('remove') // ❌ 不行


image.png


3.4 类型字面量 + as const(推荐)


类型字面量就是一个对象,将一个对象断言(Type Assertion)为一个 const,此时这个对象的类型就是对象字面量类型,然后通过类型变换,达到即可以传入枚举值,又可以传入枚举类型的目的。


const METHOD = {
ADD:'add',
/**
* @deprecated 不再支持删除
*/

DELETE:'delete',
UPDATE: 'update',
QUERY: 'query'
} as const

type METHOD_TYPE = typeof METHOD[keyof typeof METHOD]

function doAction(method: METHOD_TYPE) {
// some code
}

doAction(METHOD.DELETE) // ✅ 可行,有 TSDoc 提示
doAction('delete') // ✅ 可行
doAction('remove') // ❌ 不行

image.png


3.5 Class 类静态属性自定义实现


还有一种方法,参考了 @Hamm 提供的方法,即利用 TS 面向对象的特性,自定义实现一个枚举类,实际上这很类似于后端定义枚举的一般方式。这种方式具有很好的扩展性,自定义程度更高。



  1. 定义枚举基类


    /**
    * 枚举基类
    */

    export default class EnumBase {
    /**
    * 枚举值
    */

    private value!: string

    /**
    * 枚举描述
    */

    private label!: string

    /**
    * 记录枚举
    */

    private static valueMap: Map<string, EnumBase> = new Map();

    /**
    * 构造函数
    * @param value 枚举值
    * @param label 枚举描述
    */

    public constructor(value: string, label: string) {
    this.value = value
    this.label = label
    const cls = this.constructor as typeof EnumBase
    if (!cls.valueMap.has(value)) {
    cls.valueMap.set(value, this)
    }
    }

    /**
    * 获取枚举值
    * @param value
    * @returns
    */

    public getValue(): string | null {
    return this.value
    }

    /**
    * 获取枚举描述
    * @param value
    * @returns
    */

    public getLabel(): string | null {
    return this.label
    }

    /**
    * 根据枚举值转换为枚举
    * @param this
    * @param value
    * @returns
    */

    static convert<E extends EnumBase>(this: new(...args: any[]) => E, value: string): E | null {
    return (this as any).valueMap.get(value) || null
    }
    }


  2. 继承实现具体的枚举(可根据需要扩展)


    /**
    * 审核状态
    */

    export class ENApproveState extends EnumBase {
    /**
    * 未审核
    */

    static readonly NOTAPPROVED = new ENApproveState('1', '未审核')
    /**
    * 已审核
    */

    static readonly APPROVED = new ENApproveState('2', '已审核')
    /**
    * 审核失败
    */

    static readonly FAILAPPROVE = new ENApproveState('3', '审核失败')
    /***
    * 审核中
    */

    static readonly APPROVING = new ENApproveState('4', '审核中')
    }


  3. 使用


    test('ENCancelState.NOCANCEL equal 1', () => {
    expect(ENApproveState.NOTAPPROVED.getValue()).toBe('1')
    expect(ENApproveState.APPROVING.getValue()).toBe('4')
    expect(ENApproveState.FAILAPPROVE.getLabel()).toBe('审核失败')
    expect(ENApproveState.convert('2')).toBe(ENApproveState.APPROVED)
    expect(ENApproveState.convert('99')).toBe(null)
    })

    image.png



4. 总结



  • TS 可擦除语法 是指 typeinterfacen:number 等可以直接去掉的、仅在编译时存在、不会生成额外运行时代码的语法

  • TS 不可擦除语法 是指 enumconstructor(public x: number) {} 等不可直接去除且会生成额外运行时代码的语法

  • Node.js 23.6.0 版本开始 默认支持直接执行可擦除语法 的 TS 文件

  • enum 的替代方案有多种,取决于实际需求。用字面量类型 + as const 是比较常用的一种方案。


TS 官方为了兼容 Node.js 23.6.0 这种可执行 TS 文件特性,出了 erasableSyntaxOnly 配置禁用不可擦除语法,反映了 TypeScript 官方对减少运行时开销和优化编译输出的关注,而不是要放弃 enum


但或许未来就是要朝着这个方向把 enum 优化掉也说不定呢?


5. 参考链接



作者:MurphyChen
来源:juejin.cn/post/7478980680183169078
收起阅读 »

Linux 之父把 AI 泡沫喷了个遍:90% 是营销,10% 是现实。

作者:Shubhransh Rai Linux 之父把 AI 泡沫喷了个遍 前言: 一篇“技术老炮”的情绪宣泄文而已,说白了,这篇文章就是作者用来发泄不满的牢骚文。全篇围绕一个中心思想打转:我讨厌 AI 炒作,讨厌到牙痒痒。 但话说回来,没炒作怎么能让大众知...
继续阅读 »

作者:Shubhransh Rai



Linux 之父把 AI 泡沫喷了个遍


前言: 一篇“技术老炮”的情绪宣泄文而已,说白了,这篇文章就是作者用来发泄不满的牢骚文。全篇围绕一个中心思想打转:我讨厌 AI 炒作,讨厌到牙痒痒。


但话说回来,没炒作怎么能让大众知道、接受这些新技术?大家都讨厌广告,可真到了你要买东西的时候,没有广告你上哪儿去找好产品?炒作虽然惹人烦,但在商业世界里,它就是传播的方式——不然怎么让一个普通人知道什么是AI?


所以归根到底,这篇文章其实并不是在批评 AI 本身,更不是在否定技术的未来。它只是在重复一个观点:**我就是讨厌炒作。**而已。


Linus Torvalds 刚刚狠狠喷了整个 AI 行业 —— 而且他说得没错


Linus Torvalds —— 那个基本上构建出现代计算的人 —— 直接放出了他对 AI 的原话。

他的结论?

“90% 是营销,10% 是现实。”


毒辣。准确。而且,说实话,早该有人站出来讲了。


在维也纳的开源峰会上,Torvalds 对 AI 的炒作问题发表了一番咬牙切齿的评论,他说:

“我觉得 AI 确实很有意思,我也觉得它终将改变世界。但与此同时,我真的太讨厌这类炒作循环了,我真的不想卷进去。”


这个人见过太多科技泡沫的兴起和崩塌。现在?AI 是下一个加密货币。


Torvalds 的应对方式:直接无视


AI 的炒作已经到了让人无法忍受的地步,甚至连 Linus —— 也就是发明了 Linux 的人 —— 都选择闭麦了。

“所以我现在对 AI 的态度基本就是:无视。因为我觉得整个围绕 AI 的科技行业都处在一个非常糟糕的状态。”


说真的?Respect。


我们现在活在一个时代,每个初创公司都在自己网站上贴上“AI 加持”,然后祈祷能拿到风投。

现实呢?这些所谓的“AI 公司”绝大多数不过是把 OpenAI 的 API 包装了一层花哨的 UI。


甚至那些大厂 —— Google、微软、OpenAI —— 也在砸几百亿美元,试图说服大家 AGI(通用人工智能)马上就来了。

与此同时,AI 模型却在数学题上瞎编,还能虚构出不存在的法律案件。


Torvalds 是科技圈为数不多的几个,完全没必要陪大家演戏的人。

他没在卖 AI 产品,也不需要讨好投资人。

他看到 BS(胡扯)就直说。


五年内 AI 的现实检验


Torvalds 也承认,AI 最终会有用的……

“再过五年,情况会变,到时候我们就会看到 AI 真正被用在日常工作负载中了。”


这是目前最靠谱的观点了。


现在的 AI,基本上:

• 写一些烂代码,让真正的工程师收拾残局。

• 吐出一堆 AI 生成垃圾,被 SEO 农场铺满互联网。

• 以前所未有的速度生成公司里的官话废话。


再等五年,我们要么看到实际的生产力提升,要么看到一堆烧光 hype 的 AI 创业公司坟场。


Torvalds 谈 AI 优点:“ChatGPT 还挺酷,我猜吧。”


Torvalds 也不完全是个 AI 悲观论者 —— 他承认确实有些场景是真的有用。

“ChatGPT 演示效果挺好,而且显然已经在很多领域用上了,尤其是像图形设计这类。”


听起来挺合理的。AI 工具有些方面确实还行:

• 帮创意项目生成素材

• 自动化一些无聊流程(比如总结文档)

• 让人以为自己变得更高效了


问题是?AI 的炒作和实际效果严重脱节。


我们听到一些 CEO 说“AI 会取代所有软件工程师”,结果 LLM 连基本逻辑都理不清。


Torvalds 一眼看穿了这些噪音。

他的最终结论?

“但我真的讨厌这个炒作周期。”


结语:Linus Torvalds 是科技界最后的清醒人


Torvalds 不讨厌 AI。

他讨厌的是 AI 的炒作机器。

而他是对的。


每一次科技革命,都是先疯狂承诺一堆,然后现实拍脸:

• 互联网泡沫 —— “互联网一夜之间会取代一切!”

• 加密货币泡沫 —— “去中心化能解决所有问题!”

• AI 泡沫 —— “AGI 马上就来了!”


现实呢?

• 互联网确实改变了一切 —— 但用了 20 年。

• 加密货币确实有用 —— 但 99% 的项目都是骗子。

• AI 也终将有用 —— 但现在,它基本上只是公司演戏用的道具。


Linus Torvalds 很清楚这游戏怎么玩。

他见过科技圈的每一波炒作潮起又落。

他的解决办法?

别听那些噪音。关注真正的技术。等 hype 自动消散。


说真的?这是 2025 年最靠谱的建议了。


AI 的炒作到底是个啥?


AI 就是个 hype 吗?是,也不是。


AI 炒作列车全速前进。

所有人都在卖 “生成式 AI”、“预测式 AI”、“自主智能体 AI”,还有不知道接下来啥新词。


硅谷根本停不下来,逮谁跟谁说 AI 会彻底颠覆一切。

问题是:真会吗?

我们来捋一捋。


AI 炒作周期:一套熟悉的骗局


只要你过去二十年关注过科技趋势,你肯定见过这个套路。

Gartner 给它取了个名字:炒作周期(Hype Cycle),它是这样的:



  1. 创新触发 —— 某些技术宅发明了点啥

  2. 膨胀期顶点 —— CEO 和 VC 开始说些离谱话

  3. 幻灭低谷 —— 现实来袭,发现比想象难多了

  4. 生产力平台期 —— 多年打磨后,终于变得真有用


我们现在在哪?

AI 正脸着地掉进“幻灭低谷”。


为啥?

• 大多数 AI 初创公司不过是 OpenAI API 的壳子

• 各种公司贴“AI 加持”标签就为了拉高股价

• 技术贵、不稳定、而且经常瞎编


基本上,我们正处在“先装出来,后面再补课”的阶段。


AI 已经来了(但和你想的不一样)


很多人以为 AI 是个超级智能体,一夜之间能自动化一切。


现实警告:AI 早就来了,真相却挺无聊的。

它没有掌控公司。

它没有替代程序员。

它在干的事包括:

• 过滤垃圾邮件

• 生成客服脚本

• 推荐广告(只是不那么烂而已)


所以,AI 是有用的。

但远没你风投爹说的那么牛。


预测式 AI vs. 生成式 AI:真正的游戏


AI 可以分两大类:



  1. 生成式 AI —— 那些 LLM(像 ChatGPT)能生成文本、图像、深伪视频

  2. 预测式 AI —— 用来预测趋势、识别模式的机器学习模型


生成式 AI 吸引了全部目光,因为它光鲜亮丽。

预测式 AI 才是挣钱的正道,因为它解决了真正的商业问题。


比如?

• 医疗:预测疾病暴发

• 金融:在诈骗发生前识别它

• 零售:在厕纸卖光前优化库存


最好的效果来自两者结合:

预测式 AI 预测未来,生成式 AI 自动应对。

这就是 AI 今天真正能发挥作用的地方。


AI 的未来:炒作 vs. 现实


所以,AI 会真的改变世界吗?

会。

但不是明天。


一些靠谱的预测:

✅ AI 会自动化那些烦人的工作 —— 重复性任务直接消失

✅ AI 会提升效率 —— 前提是公司别再吹过头

✅ AI 会无处不在 —— 某些我们根本注意不到的地方


一些纯 BS 的预测:

❌ AI 会替代所有工作 —— 它还是得靠人引导

❌ AGI 马上就来了 —— 不可能,别骗了

❌ AI 是完美且无偏见的 —— 它是喂互联网垃圾长大的


最终结论:AI 既被过度炒作,又是不可避免的未来


AI 是不是 hype?当然是。

AI 会不会消失?绝对不会。


现在大多数 AI 项目,都是营销秀。

但再过 5 到 10 年,最后活下来的赢家会是那些:

• 真正把 AI 用在合适地方的公司

• 关注解决实际问题,而不是追热词的公司

• 不再把 AI 当魔法,而是当工具对待的公司


hype 会死。

有用的东西会留下来。


作者:果冻人工智能
来源:juejin.cn/post/7485940589885538344
收起阅读 »

安卓突然终止「开源」,开发者遭背叛?社区炸锅了

【新智元导读】谷歌将改变一直以来对 Android 开源项目(AOSP)的公开开发模式,转而在私有环境中进行。但这并非意味着 Android 彻底闭源。对于普通用户而言不会有什么影响,但却让科技爱好者失去了一扇「窥视」安卓内部的窗口。 据 Android Au...
继续阅读 »


【新智元导读】谷歌将改变一直以来对 Android 开源项目(AOSP)的公开开发模式,转而在私有环境中进行。但这并非意味着 Android 彻底闭源。对于普通用户而言不会有什么影响,但却让科技爱好者失去了一扇「窥视」安卓内部的窗口。

据 Android Authority 报道,谷歌已经向其确认,谷歌将很快在私有环境中开发 Android 开源项目(AOSP,Android Open Source Project),但依然会开源代码。



网站地址:http://www.android.com/


很多小伙伴可能会慌了,我的安卓手机不能用了?


目前来看,谷歌私下开发 AOSP 项目还不至于到「天塌下来」的地步,普通手机用户更是几乎感觉不到什么变化。


大部分主流手机厂商(比如小米、vivo、三星等)早就跟谷歌签好了各种合作伙伴协议。


只要这些协议还在,厂商们就还能照常拿到最新的 Android 源代码,通过 Google 自家的认证,正常预装 Google Play、Gmail 这些服务和应用。


谷歌对安卓系统的支持也不会断。


一句话,还是老样子。


那么问题来了,谷歌到底做了什么?


这就要从谷歌的安卓开源项目(AOSP)说起了。


什么是 Android Open Source Project(AOSP)?


AOSP 简单来说,就是谷歌给所有 Android 设备提供了一个「毛坯房」——操作系统的基本框架和核心部件。


任何开发者都可以免费下载它的代码,随意改动、分发,然后打造自己的定制系统。


比如小米 HyperOS、vivo OriginOS 都是在 AOSP 基础上搭建起来的。



网站地址:source.android.com/?hl=zh-cn


而 Android 系统本身是跑在 Linux 内核上,这个内核用的是 GPL 许可证,规则挺严格。


简单说就是,只要使用采用了 GPL 许可证的代码,你就得开源,体现「要玩就一起玩」的精神。


但 Google 为了让 Android 既开源又能赚钱,玩了个聪明设计:底层 Linux 内核老实按 GPL 开源,但中间 AOSP 大部分代码却用宽松的 Apache 2.0 许可证。


这样厂商既能自由改动 Android,不用全盘公开,还能加自己专有的东西,既开放又灵活。


具体来说,Linux 内核和模块还得开源,但到了用户空间的应用就不受 GPL 限制,想闭源就闭源。


结果就是,AOSP 底层 GPL 开源,中层 Apache 宽松开源,上层应用随开发者意愿,想怎么玩就怎么玩。


谷歌的这点小聪明那是相当的成功。


回想将近二十年前,智能手机刚起步那会儿,苹果发布了 iPhone。


谷歌也想在移动市场分一杯羹,于是决定推出 Android。


这不光帮助谷歌赚了个技术开放的好名声,还把一大堆厂商和用户从塞班、诺基亚、Windows Mobile、黑莓手里抢了过来。


真是神来之笔。


Android 开源这步棋,绝对是谷歌今天能占据移动操作系统市场七成以上份额的最大功臣。


市场是拿到了,代价是 AOSP 软件的维护是要做的。


问题是,随着手机的功能越来越多,这种维护工作的代价也越来越大。


终于,谷歌忍不了了。


代码同步难,谷歌决定「关起门」来开发,但依然开源代码


写过代码的都知道相比写代码,「合并代码」反而是最令人头疼的问题。


2007 年,谷歌开放了安卓的核心代码,这步棋让谷歌摘取了移动互联网时代最大的果实。


但是也导致安卓这个项目有了两个「主分支」。


一个分支就是公共的 AOSP 分支,这个分支对任何人都开放,大家所说的「安卓是开源」就是指这个分支。



一些附属功能,比如蓝牙功能,仍然在 AOSP 分支中公开开发,你可以在开源的 Android Code Search 中搜索到相关源代码。



然而,AOSP 公共分支并不包含谷歌专有的应用和服务,比如 Google Play 商店、Gmail、Google Maps 等。


AOSP 虽然没有谷歌自己的服务,但是仍然可以编译为一个完整的可用操作系统。


许多设备制造商基于 AOSP 开发自己的操作系统,包括:



  • 三星:开发了 One UI。

  • 小米:开发了 MIUI。

  • OPPO:开发了 ColorOS。

  • 华为:开发了早期的 EMUI。

  • 一加:开发了 OxygenOS。


另一个分支则是完全的闭源开发,可以看做谷歌自己的安卓「亲儿子」。


这个分支仅限于拥有谷歌移动服务(GMS)许可协议的公司使用,以上类似三星 One UI 这种 Android 系统也可以使用,只要谷歌给予授权。


目前来看,大多数组件,包括核心 Android 操作系统框架,都是在 Google 的内部分支中私下开发的。


两个分支导致一个很大问题,就是内部分支的开发进度领先于公开的 AOSP,导致两个分支差异很大。


这种差异逼得谷歌必须花费时间和精力在公共 AOSP 分支与其内部分支之间合并补丁上。


这就到了程序员「喜闻乐见」的环节,由于分支差异很大,合并冲突经常出现。


以这个启用导航栏和键盘屏幕放大功能的补丁为例,该补丁引入了新的辅助功能设置,该设置被放置在辅助功能设置列表的末尾。


这会导致合并冲突,因为 AOSP 与谷歌内部分支之间的列表长度不同(图中变量 accessibility_magnify_nav_and_ime 设置为 58 和 59 冲突)。


虽然针对此特定问题的修复很简单,但当其他许多 AOSP 补丁集成到谷歌的内部分支时,都会触发类似的合并冲突。



另一个例子是,开发 Android 的新仅解锁存储区域 API 需要一位 Google 工程师从内部分支中挑选一个补丁到 AOSP 以解决合并冲突。


这是因为虽然 API 是在 AOSP 中开发的,但包含新 Android 构建标志的文件是在内部开发的。


因此,必须在内部提交一个更新构建标志文件的补丁,然后应用到 AOSP。



也许这些冲突单独看都不难处理,但是架不住可能会有无数这样「合并冲突」的例子。


「累觉不爱」,也许这就是谷歌放弃当前双管齐下的 Android 开发策略,转而将所有开发工作内部化的原因。


这对我们意味着什么?


这一决策整体来说,并不意味着 Android 正在变成「闭源」。


谷歌只是想把「开发过程」藏起来,依然会继续发布源代码。


最大的区别在于,AOSP 公共分支存在时,对于 Android 爱好者和科技行业记者来说,这是一个能够「窥探」Android 最新动向的窗口。


现在这个「窗口」要被谷歌关上了,这可能会让这些科技极客们感到沮丧,因为这减少了他们对 Google 开发工作的洞察力。


对于开发者,这会让他们更难跟上新的 Android 平台变化,因为他们将无法再跟踪 AOSP 中的变化。


比如外国的一个记者在 AOSP 中发现了某些代码变更,然后提前数月就预测了 Pixel 的网络摄像头功能,他还利用 AOSP 中的线索推断出 Android 16 的提前发布日期。


而对于大多数的我们,甚至包括安卓应用开发者,可以说毫无影响。


事实上,从逻辑的角度上,谷歌大概率就是觉得维护代码的成本过高,不论是从 AOSP 合并到内部版本,还是将内部版本的更新带给 AOSP 公共分支,这些工作都需要工程师完成。


可以说这些处理冲突的工作过于「低端」,对于谷歌的工程师来说,耗时耗力而且毫无意义。


但是 AOSP 某种意义上已经可以看做是谷歌在开源生态和程序员心目中的「投名状」。


作为以「不作恶」为公司理念的谷歌,安卓开源这步棋被认为是谷歌最成功的一次战略决策之一。


在极客们看来,这次决策类似于谷歌自己推倒了过去十几年树立起来的「精神丰碑」。


当然,从谷歌自己的角度看来,选择将工作整合在一个内部分支下,同时简化操作系统开发和源代码发布,是可以理解的。


毕竟 AOSP 对 Google 的商业价值,跟当年比起来,已经完全不是一个量级了。


从最近谷歌对 Gemini 以及 Gemma 的疯狂更新来看,AI 才是其工作的重点。


其实所有人都知道,相比于 Gemini,安卓对于谷歌已不再那么重要。


参考资料:


arstechnica.com/gadgets/202…


http://www.androidauthority.com/google-andr…


作者:新智元
来源:juejin.cn/post/7486315070362075173
收起阅读 »

年少不知自增好,错把UUID当个宝!!!

在 MySQL 中,使用 UUID 作为主键 在大表中可能会导致性能问题,尤其是在插入和修改数据时效率较低。以下是详细的原因分析,以及为什么修改数据会导致索引刷新,以及字符主键为什么效率较低。 1. UUID 作为主键的问题 (1)UUID 的特性 UUI...
继续阅读 »

在 MySQL 中,使用 UUID 作为主键 在大表中可能会导致性能问题,尤其是在插入和修改数据时效率较低。以下是详细的原因分析,以及为什么修改数据会导致索引刷新,以及字符主键为什么效率较低。




1. UUID 作为主键的问题


(1)UUID 的特性



  • UUID 是一个 128 位的字符串,通常表示为 36 个字符(例如:550e8400-e29b-41d4-a716-446655440000)。

  • UUID 是全局唯一的,适合分布式系统中生成唯一标识。


(2)UUID 作为主键的缺点


1. 索引效率低


  • 索引大小:UUID 是字符串类型,占用空间较大(36 字节),而整型主键(如 BIGINT)仅占用 8 字节。索引越大,存储和查询的效率越低。

  • 索引分裂:UUID 是无序的,插入新数据时,可能会导致索引树频繁分裂和重新平衡,影响性能。


2. 插入性能差


  • 随机性:UUID 是无序的,每次插入新数据时,新记录可能会插入到索引树的任意位置,导致索引树频繁调整。

  • 页分裂:InnoDB 存储引擎使用 B+ 树作为索引结构,随机插入会导致页分裂,增加磁盘 I/O 操作。


3. 查询性能差


  • 比较效率低:字符串比较比整型比较慢,尤其是在大表中,查询性能会显著下降。

  • 索引扫描范围大:UUID 索引占用的空间大,导致索引扫描的范围更大,查询效率降低。




2. 修改数据导致索引刷新的原因


(1)索引的作用



  • 索引是为了加速查询而创建的数据结构(如 B+ 树)。

  • 当数据被修改时,索引也需要同步更新,以保持数据的一致性。


(2)修改数据对索引的影响



  • 更新主键



    • 如果修改了主键值,MySQL 需要删除旧的主键索引记录,并插入新的主键索引记录。

    • 这个过程会导致索引树的调整,增加磁盘 I/O 操作。



  • 更新非主键列



    • 如果修改的列是索引列(如唯一索引、普通索引),MySQL 需要更新对应的索引记录。

    • 这个过程也会导致索引树的调整。




(3)UUID 主键的额外开销



  • 由于 UUID 是无序的,修改主键值时,新值可能会插入到索引树的不同位置,导致索引树频繁调整。

  • 相比于有序的主键(如自增 ID),UUID 主键的修改操作代价更高。




3. 字符主键导致效率降低的原因


(1)存储空间大



  • 字符主键(如 UUID)占用的存储空间比整型主键大。

  • 索引的大小直接影响查询性能,索引越大,查询时需要的磁盘 I/O 操作越多。


(2)比较效率低



  • 字符串比较比整型比较慢,尤其是在大表中,查询性能会显著下降。

  • 例如,WHERE id = '550e8400-e29b-41d4-a716-446655440000' 的效率低于 WHERE id = 12345


(3)索引分裂



  • 字符主键通常是无序的,插入新数据时,可能会导致索引树频繁分裂和重新平衡,影响性能。




4. 如何优化 UUID 主键的性能


(1)使用有序 UUID



  • 使用有序 UUID(如 UUIDv7),减少索引分裂和页分裂。

  • 有序 UUID 的生成方式可以基于时间戳,保证插入顺序。


(2)将 UUID 存储为二进制



  • 将 UUID 存储为 BINARY(16) 而不是 CHAR(36),减少存储空间。


    CREATE TABLE users (
    id BINARY(16) PRIMARY KEY,
    name VARCHAR(255)
    );



(3)使用自增主键 + UUID



  • 使用自增主键作为物理主键,UUID 作为逻辑主键。


    CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    uuid CHAR(36) UNIQUE,
    name VARCHAR(255)
    );



(4)分区表



  • 对大表进行分区,减少单个索引树的大小,提高查询性能。




~Summary



  • UUID 作为主键的缺点



    • 索引效率低,插入和查询性能差。

    • 修改数据时,索引需要频繁刷新,导致性能下降。



  • 字符主键效率低的原因



    • 存储空间大,比较效率低,索引分裂频繁。



  • 优化建议



    • 使用有序 UUID 或二进制存储。

    • 结合自增主键和 UUID。

    • 对大表进行分区。




作者:码农liuxin
来源:juejin.cn/post/7478495083374559270
收起阅读 »

AI时代下,我用陌生技术栈两天开发完一个App后的总结

web
AI时代下,我用陌生技术栈两天开发完一个App后的总结 今年的 AI 应用赛道简直是热火朝天,大街上扔一把核桃能砸到三个在讨论 AI 的,更有不少类似于零编程经验用 AI 三天开发 App 登榜 App Store的标题在各大平台的AI话题圈子里刷屏,作为一个...
继续阅读 »

AI时代下,我用陌生技术栈两天开发完一个App后的总结


今年的 AI 应用赛道简直是热火朝天,大街上扔一把核桃能砸到三个在讨论 AI 的,更有不少类似于零编程经验用 AI 三天开发 App 登榜 App Store的标题在各大平台的AI话题圈子里刷屏,作为一个在互联网行业摸爬滚打多年的程序员,我做过开源项目,也做过多个小型独立商业项目,最近两年也是在 AI 相关公司任职,对此我既感到兴奋又难免焦虑——为什么我还没遇到这样的机遇?


刚好最近想到了一个点子,是一个结合了屏幕呼吸灯 + 轻音乐 + 白噪声的辅助睡眠移动端 A 应用,我将其命名为“音之梦”,是我某天晚上睡不着看到路由器闪烁的灯光照耀在墙壁上之后突然爆发的灵感。


这是个纯客户端应用,没有太多外部依赖,体量小,正好拿来试一下是不是真的有可能完全让 AI 来将它实现,而为了尽量模拟“编程小白”这个身份,这次我选择用我比较陌生的 Swift UI。


先上结论:


对于小体量的应用,或者只考虑业务实现而不需要考虑架构合理性/可维护性的稍大体量的应用,在纯编码层面确实是完全可以实现的,作为一个不会 Swift UI 的开发者,我确实在不到 2 天时间内完全借助 AI 完成了这个应用的开发,而且已经上架苹果App Store。


以下是应用截图:


98shots_so.png


感兴趣的朋友们也访问下面的链接或者上App Store搜索 ”音之梦“ 下载体验。


App Store链接


我做了哪些事情?


工具准备


开发工具使用的是Cursor + XCode,开发语言选的 Swift UI,模型自然选择最适合编码的Claude 3.7。



为什么不选择Trae?因为下一个开坑项目准备用Trae + Deepseek来进行效果对比。



产品设计


上面截图展示的其实是第二版, UI和交互流程是我根据产品需求仔细思考琢磨设计出来的。


而第一版则完全由AI生成的,我只是描述了我期望的功能,交互方式和UI效果都是AI来生成的,那自然和我心目中期望的差距很大,不过最开始只是想验证AI的快速编码能力,所以首次上架的还是还是用的这一版的样式,可以看下面的截图:


478shots_so.png


而因为国区上架需要备案,在等待备案的过程中,我又诞生了很多新的想法,加上对于第一版的UI和交互流程也是越看越不爽,所以就重新思考了整个应用的UI和交互流程,并重新用figma画了设计稿,然后交由AI来实现。


当然每个人的审美和需求都不一样,也并不是每个人都有不错的UI和交互设计能力,对于大部分人来说现阶段的AI设计水平已经是能满足需要了的。


开发过程


使用AI来进行开发,那最重要的就是提示词,而如何编写提示词来让AI更了解你的需求、尽可能不走弯路去实现,其实是很不容易的。


这里我的经验是,先自己思考清楚,用markdown整理好需求,包括主要功能、需要有哪些页面、每个页面的大致布局,以及一些需要额外强调的细节等等,然后让AI先根据你整理的需求文档来描述一下它对这个需求的理解,也可以让它反过来问你一些在需求文档无法确定的问题,补全到文档中,直到他能八九不离十的把你想要的结果描述出来。



此外,你也可以先在chat模式里面简单一句话描述需求,选择claude 3.7 thinking模型,或者deepseek r1模型,然后你们俩一起交流来把需求逐步完善和明确。



需求明确之后,也不要马上就让AI直接开始开发,因为如果整个过程很长的话,大模型目前的上下文肯定是不够的,就算是基于codebase,后续也必然会丢失细节,甚至完全忘记了你们之前定好的需求。


这里的建议是,先让AI根据需求文档把目录和文件创建好,并为每个代码文件建立一个md文件,用于标记每个代码文件里面包含的关键变量、方法名、和其他模块的依赖关系等,这样相比完整的代码文件,数据量要小很多,后续觉得大模型忘事儿了,就让他根据md来进行分析,这要比让他分析完整的代码文件要靠谱很多。另外在后续开发过程中也一定要让AI及时更新md文件。



可以在Cursor的规则配置文件中明确上面的要求。



由于Cursor中的Claude 3.7不支持输入图片作为参考,所以如果你需要基于现有的设计图进行开发,可以先选择Claude 3.5,传入参考图,让它先帮你把不带交互的UI代码实现,然后再使用claude 3.7 来进一步完善设计的业务逻辑。


开发过程中,每一次迭代可能都大幅改动之前已经实现的部分,所以切记一定要及时git commit,尤其是已经完成了某个小功能之后,一定要提交代码!这也是我使用AI进行全流程开发的最大的教训!


音频资源的获取


这个App中有很多音频资源,包括轻音乐、环境声、白噪声等,在以往,个人开发者要获取这些资源其实是很费时间的,而且需要付出的成本也比较高,但是随着AI的发展,现在获取这些资源已经变得非常容易了。


比如轻音乐,我用的是Suno AI来生成的,十几美元一个月,就能生成足够多的轻音乐,而且质量还不错,可以满足大部分场景的需求。


白噪声,则是让AI帮我编写的nodejs脚本来生成的,直接本地生成mp3音乐文件。


环境声、物品音效之类的,可以使用Eleven Lab来生成,价格也很便宜,不过我这里是先用的开源项目Moodist中的资源,而且也对作者进行了捐赠支持。


另外,在讲这些音频资源打包到应用的时候,从体积角度考虑,我对这些音频都做了压缩,在以往可能需要找一些格式工厂之类的软件来完成,现在直接让AI帮你基于Macos内置的音频处理模块生成命令行脚本,轻松完成格式转换和比率压缩,非常方便。


非AI的部分


虽然我全程几乎没有写一行代码,但是还是有一些非AI的部分,需要手动来完成。


比如应用的启动图标设计、App Store上架资料的准备、关键词填写、技术支持网址、隐私协议内容、应用截图的准备等等,虽然这其中有一些也能借助AI辅助进行,但是最终还是免不了要手动进行处理。



隐私协议内容可以让AI生成,不过一定要自己过一遍,而技术支持网站,可以用在线问卷的形式,不用自己准备网站。



在App Store上架审核的时候,也需要时刻关注审核进度和反馈,一般来说新手第一次上架审核就过审的概率很低,苹果那边也有很多规范或者要求甚至是红线,需要踩坑多次才能了解清楚。我之前已经上架过好几款应用了,这一次提审第一次没过居然是因为内购项目忘记一并提审了,笑死,不然就是一把过了,后面更新的几个版本最快半小时不到就过审了。


另外还有国区上架备案的问题,实际要准备的资料也不多,流程其实也不复杂,但是就是需要等待,而且等待的时间不确定,我这次等了近5天才通过。


有朋友可能会咨询安卓版的问题,我只能说一言难尽,目前安卓的上架流程和资质要求对独立开发者太不友好了,不确定项目有商业价值之前,不建议考虑安卓(就算是出海上google play,也比上苹果应用商店麻烦多了)。


总结


以往我作为全栈工程师,在开发产品的时候,编码始终是我最核心的工作,而这一次,我最重要的编码过程交给了 AI,我则是充当了产品设计师和测试工程师的角色,短短几天,我已经体会了很多专职产品设计师在和开发人员沟通时候的感受,也体会到了测试工程师在测试产品时候的感受,这确实是一个非常有趣和有意义的过程。


作为产品设计师,我需要能够准备描述需求、考虑到尽可能多的场景和细节,才能让 AI 更加敏捷和高质量的完成需求的开发,而作为测试工程师,我需要学会如何准确地描述问题的表现和复现步骤,才能让 AI 更加精准的给出解决方案。


虽然我确实没有写一行代码,但是在开发过程中,遇到一些复杂场景或者问题,Cursor 也会原地踏步甚至把问题越改越严重,这个时候还是需要我去分析一下它实现的代码,跳出它的上下文来来给他提示,然后他就会恍然大悟一般迅速解决问题,这确确实实依赖了我多年来的开发经验和直觉,我相信在开发复杂应用的时候,这是必不可少的。


而且开发 App 是一回事,上架 App 又是另外一回事了,备案、审核、隐私协议准备、上架配图准备等等等等,这些可能要花的时间比开发的时间还长。


在这次实践的过程中,我虽然是借助了自己的以往的独立开发经验解决了很多问题,并因此缩短了从开始开发到真正完成审核上架的周期,但我相信最核心的编码问题已经完全能交给AI了的话,那对于大部想要做一个自己的应用的人来说,真正的技术门槛确实已经不存在了。


因此我可以对大家说,准备好面对一个人人都能编程做应用的新时代吧!


再回到关于零编程经验用 AI 三天开发 App 登榜 App Store这个话题,我只能说,这确实是一个非常吸引眼球的话题,但是它也确实存在一定的误导性,不管用 AI 还是纯人工编码,做出来好的 APP 和成为爆款 APP 其实是两回事。


事实上我体验过一些此类爆款产品,在产品完成度和交互设计上实际上还很初级,甚至可以说很粗糙,但是它们却能够获得巨大的成功,除了运气和时机之外,营销上的成功其实是更重要的。


需要清醒认识的是,App Store 榜单是产品力、运营策略和市场机遇的综合产物。作为开发者,更应该关注 AI 如何重构我们的能力边界,而非简单对标营销案例。


最后再说点


总有人会问AI会不会取代程序员,我觉得不会,被AI淘汰的,只会是那些害怕使用AI,不愿意学习使用AI的人。我相信经常逛掘金的朋友们也都是不甘于只做一颗螺丝钉的,快让借助AI来拓展你的能力边界吧,


最后再再再说一句


一定要记得及时git commit!!!


作者:Margox
来源:juejin.cn/post/7484530047866355766
收起阅读 »

🔥🔥什么?LocalStorage 也能被监听?为什么我试了却不行?

web
引言:最近,团队的伙伴需要实现监听 localStorage 数据变化,但开发中却发现无法直接监听。 在团队的一个繁重项目中,我们发现一个新功能的实现成本很大,因此开始思考:能否通过实时监听 LocalStorage 的变化并自动触发相关操作。我们尝试使用 ...
继续阅读 »

引言:最近,团队的伙伴需要实现监听 localStorage 数据变化,但开发中却发现无法直接监听。



在团队的一个繁重项目中,我们发现一个新功能的实现成本很大,因此开始思考:能否通过实时监听 LocalStorage 的变化并自动触发相关操作。我们尝试使用 addEventListener 来监听 localStorage 的变化,但令人意外的是,这种方法仅在不同浏览器标签页之间有效,而在同一标签页内却无法实现监听。这是怎么回事?





经过调研了解到,浏览器确实提供了 storage 事件机制,但它仅适用于同源的不同标签页之间。对于同一标签页内的 LocalStorage 变化,却没有直接的方法来实现实时监听。最初,我们考虑使用 setInterval 进行定时轮询来获取变化,但这种方式要么导致性能开销过大,要么无法第一时间捕捉到变化。


今天,我们探讨下几种高效且实用的解决方案,是否可以帮助轻松应对LocalStorage这种监听需求?希望对你有所帮助,有所借鉴!


传统方案的痛点🎯🎯


先来看看浏览器是如何帮助我们处理不同页签的 LocalStorage 变化:


window.addEventListener("storage", (event) => {      
if (event.key === "myKey") {
// 执行相应操作
}
});

通过监听 storage 事件,当在其他页签修改 LocalStorage 时,你可以在当前页签捕获到这个变化。但问题是:这种方法只适用于跨页签的 LocalStorage 修改,在同一页签下无法触发该事件。于是,很多开发者开始寻求替代方案,比如:


1、轮询(Polling)


轮询是一种最直观的方式,它定期检查 localStorage 的值是否发生变化。然而,这种方法性能较差,尤其在高频轮询时会对浏览器性能产生较大的影响,因此不适合作为长期方案。


let lastValue = localStorage.getItem('myKey');

setInterval(() => {
const newValue = localStorage.getItem('myKey');
if (newValue !== lastValue) {
lastValue = newValue;
console.log('Detected localStorage change:', newValue);
}
}, 1000); // 每秒检查一次

这种方式实现简单,不依赖复杂机制。但是性能较差,频繁轮询会影响浏览器性能。


2、监听代理(Proxy)或发布-订阅模式


这种方式通过创建一个代理来拦截 localStorage.setItem 的调用。每次数据变更时,我们手动发布一个事件,通知其他监听者。


(function() {
const originalSetItem = localStorage.setItem;
const subscribers = [];

localStorage.setItem = function(key, value) {
originalSetItem.apply(this, arguments);
subscribers.forEach(callback => callback(key, value));
};

function subscribe(callback) {
subscribers.push(callback);
}

subscribe((key, value) => {
if (key === 'myKey') {
console.log('Detected localStorage change:', value);
}
});

localStorage.setItem('myKey', 'newValue');
})();

这种比较灵活,可以用于复杂场景。但是需要手动拦截 setItem,维护成本高(但也是值得推荐的)。





然而,这些方案往往存在性能问题或者开发的复杂度,在高频数据更新的情况下,有一定的性能问题,而且存在一定的风险性。那么有没有可以简单快速,风险性还小的方案呢?


高效的解决方案 🚀🚀


既然浏览器不支持同一页签的 storage 事件,我们可以手动触发事件,以此来实现同一页签下的 LocalStorage 变化监听。


1、自定义 Storage 事件


通过手动触发 StorageEvent,你可以在 LocalStorage 更新时同步分发事件,从而实现同一页签下的监听。


localStorage.setItem('myKey', 'value');

// 手动创建并分发 StorageEvent
const storageEvent = new StorageEvent('storage', {
key: 'myKey',
url: window.location.href
});
window.dispatchEvent(storageEvent);

你可以使用相同的监听逻辑来处理数据变化,无论是同一页签还是不同页签:


window.addEventListener("storage", (event) => {
if (event.key === "myKey") {
// 处理 LocalStorage 更新
}
});

这种实现简单、轻量、快捷。但是需要手动触发事件。


2、基于 CustomEvent 的自定义事件


StorageEvent 类似,你可以使用 CustomEvent 手动创建并分发事件,实现 localStorage 的同步监听。


localStorage.setItem('myKey', 'newValue');

const customEvent = new CustomEvent('localStorageChange', {
detail: { key: 'myKey', value: 'newValue' }
});
window.dispatchEvent(customEvent);


这种方式适合更加灵活的事件触发场景。CustomEvent不局限于 localStorage 事件,可以扩展到其他功能。


window.addEventListener('localStorageChange', (event) => {
const { key, value } = event.detail;
if (key === 'myKey') {
console.log('Detected localStorage change:', value);
}
});

3、MessageChannel(消息通道)


MessageChannel API 可以在同一个浏览器上下文中发送和接收消息。我们可以通过 MessageChannellocalStorage 的变化信息同步到其他部分,起到类似事件监听的效果。


const channel = new MessageChannel();

channel.port1.onmessage = (event) => {
console.log('Detected localStorage change:', event.data);
};

localStorage.setItem('myKey', 'newValue');
channel.port2.postMessage(localStorage.getItem('myKey'));

适合组件通信和复杂应用场景,消息机制较为灵活。相对复杂的实现,可能不适合简单场景。


4、BroadcastChannel


BroadcastChannel 提供了一种更高级的浏览器通信机制,允许多个窗口或页面之间广播消息。你可以通过这个机制将 localStorage 变更同步到多个页面或同一页面的不同部分。


const channel = new BroadcastChannel('storage_channel');

channel.onmessage = (event) => {
console.log('Detected localStorage change:', event.data);
};

localStorage.setItem('myKey', 'newValue');
channel.postMessage({ key: 'myKey', value: 'newValue' });

支持跨页面通信,方便在不同页面间同步数据,易于实现。适用场景较为具体,通常用于复杂的页面通信需求。


这4个方法,主打的就是一个见缝插针,简单快速,风险性低。但是客观角度来讲,每种方案都是有各自优势的。


优势对比


方案优点缺点适用场景
轮询实现简单,适合低频监控需求性能差,频繁轮询影响浏览器性能简单场景或临时方案
监听代理/发布-订阅模式灵活扩展,适合复杂项目需要手动拦截 setItem,维护成本高需要手动事件发布的场景
自定义 StorageEvent实现简单,原生支持 storage 事件监听需要手动触发事件同页签下 localStorage 监听
自定义事件灵活的事件管理,适合不同场景需要手动触发事件需要自定义触发条件的场景
MessageChannel适合组件通信和复杂应用场景实现复杂,不适合简单场景高级组件通信需求
BroadcastChannel跨页面通信,适合复杂通信需求使用场景较具体复杂的多窗口通信

如何在 React / Vue 使用


在主流前端框架(如 React 和 Vue)中,监听 LocalStorage 变化并不困难。无论是 React 还是 Vue,你都可以使用自定义的 StorageEvent 或其他方法来实现监听。在此,我们以自定义 StorageEvent 为例,展示如何在 React 和 Vue 中实现 LocalStorage 的监听。





1. 在 React 中使用自定义 StorageEvent


React 是一个基于组件的框架,我们可以使用 React 的生命周期函数(如 useEffect)来监听和处理 LocalStorage 的变化。


import React, { useEffect } from 'react';

const LocalStorageListener = () => {
useEffect(() => {
// 定义 storage 事件监听器
const handleStorageChange = (event) => {
if (event.key === 'myKey') {
console.log('Detected localStorage change:', event.newValue);
}
};

// 添加监听器
window.addEventListener('storage', handleStorageChange);

// 模拟触发自定义的 StorageEvent
const triggerCustomStorageEvent = () => {
const storageEvent = new StorageEvent('storage', {
key: 'myKey',
newValue: 'newValue',
url: window.location.href,
});
window.dispatchEvent(storageEvent);
};

// 组件卸载时移除监听器
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, []); // 空依赖数组表示该 effect 只会在组件挂载时运行

return (
<div>
<button onClick={() => localStorage.setItem('myKey', 'newValue')}>
修改 localStorage
</button>
<button onClick={() => window.dispatchEvent(new StorageEvent('storage', {
key: 'myKey',
newValue: localStorage.getItem('myKey'),
url: window.location.href,
}))}>
手动触发 StorageEvent
</button>
</div>

);
};

export default LocalStorageListener;


  • useEffect 是 React 的一个 Hook,用来处理副作用,在这里我们用它来注册和清除事件监听器。

  • 我们手动触发了 StorageEvent,以便在同一页面中监听 LocalStorage 的变化。


2. 在 Vue 中使用自定义 StorageEvent


在 Vue 3 中,我们可以使用 onMountedonUnmounted 这两个生命周期钩子来管理事件监听器。(Vue 3 Composition API):


<template>
<div>
<button @click="updateLocalStorage">修改 localStorage</button>
<button @click="triggerCustomStorageEvent">手动触发 StorageEvent</button>
</div>
</template>

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

const handleStorageChange = (event: StorageEvent) => {
if (event.key === 'myKey') {
console.log('Detected localStorage change:', event.newValue);
}
};

const updateLocalStorage = () => {
localStorage.setItem('myKey', 'newValue');
};

const triggerCustomStorageEvent = () => {
const storageEvent = new StorageEvent('storage', {
key: 'myKey',
newValue: 'newValue',
url: window.location.href,
});
window.dispatchEvent(storageEvent);
};

onMounted(() => {
window.addEventListener('storage', handleStorageChange);
});

onUnmounted(() => {
window.removeEventListener('storage', handleStorageChange);
});
</script>


  • 使用了 Vue 的 Composition API,其中 onMountedonUnmounted 类似于 React 的 useEffect,用于在组件挂载和卸载时管理副作用。

  • 同样手动触发了 StorageEvent 来监听同一页面中的 LocalStorage 变化。


提炼封装一下 🚀🚀


无论是 React 还是 Vue,将自定义 StorageEvent 实现为一个组件或工具函数是常见的做法。你可以将上面的逻辑提取到一个独立的 hook 或工具函数中,方便在项目中多次使用。


在 React 中提取为 Hook


import { useEffect } from 'react';

const useLocalStorageListener = (key, callback) => {
useEffect(() => {
const handleStorageChange = (event) => {
if (event.key === key) {
callback(event.newValue);
}
};

window.addEventListener('storage', handleStorageChange);

return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, [key, callback]);
};

export default useLocalStorageListener;

在 Vue 中提取为工具函数


import { onMounted, onUnmounted } from 'vue';

export const useLocalStorageListener = (key: string, callback: (value: string | null) => void) => {
const handleStorageChange = (event: StorageEvent) => {
if (event.key === key) {
callback(event.newValue);
}
};

onMounted(() => {
window.addEventListener('storage', handleStorageChange);
});

onUnmounted(() => {
window.removeEventListener('storage', handleStorageChange);
});
};


  • 在 React 中,我们创建了一个自定义 Hook useLocalStorageListener,通过传入监听的 key 和回调函数来捕获 LocalStorage 的变化。

  • 在 Vue 中,我们创建了一个工具函数 useLocalStorageListener,同样通过传入 key 和回调函数来监听变化。


总结





在同一个浏览器页签中监听 localStorage 的变化并非难事,但不同场景下需要不同的方案。从简单的轮询到高级的 BroadcastChannel,本文介绍的几种方案各有优缺点。根据你的实际需求,选择合适的方案可以帮助你更高效地解决问题。



  • 简单需求:可以考虑使用自定义 StorageEventCustomEvent 实现监听。

  • 复杂需求:对于更高级的场景,如跨页面通信,MessageChannelBroadcastChannel 是更好的选择。


如果你有其他的优化技巧或问题,欢迎在评论区分享,让我们一起交流更多的解决方案!


作者:Sailing
来源:juejin.cn/post/7418117491720323081
收起阅读 »

CSS换行最容易出现的bug,半天被提了两个😭😭

web
引言 大家好,我是石小石! 文字超出换行应该是大家项目中高频的css样式了,它也是非常容易出现样式bug的一个地方。 分享一下我工作中遇到的问题。前几天开发代码的时候,突然收到两个测试提的bug: bug的内容大致就是我的文字换行出现了问题 我的第一反应就...
继续阅读 »

引言


大家好,我是石小石!


文字超出换行应该是大家项目中高频的css样式了,它也是非常容易出现样式bug的一个地方。


分享一下我工作中遇到的问题。前几天开发代码的时候,突然收到两个测试提的bug:



bug的内容大致就是我的文字换行出现了问题



我的第一反应就是线上代码不是最新的,因为自测的时候,我注意过这个问题,我在本地还测试过



然而经过验证,最后我还是被打脸了,确实是自己的问题!


问题原因分析


在上述的问题代码中,我没有做任何换行的规则


.hover-content {
max-width: 420px;
max-height: 420px;
}

因此,此时弹框内的换行规则遵循的是浏览器的默认换行规则(white-space:normal):


浏览器换行遵循 单词完整性优先 的规则,即尽可能不在单词或数字序列内部断行;而中文是固定宽度字符,每个汉字视为独立的可断点,因此换行非常自然,展示不下浏览器会将其移动到下一行。


那么,出现上述bug的原因就非常简单了,基于浏览器的默认换行规则,这种胡乱输入、没有规则的连续纯英文或数字不换行,而汉字会换行。


white-space:normal 指定文本能够换行,是css的默认值,后文我们会继续讲解


解决方案


解决上述问题其实非常简单,一行css就可以解决🤗


word-break: break-all

word-break: break-all 可以打破浏览器的默认规则,允许在任何字符间断行(包括英文单词和数字序列)。


word-break



  • 作用:指定如何对单词进行断行。

  • 取值





    • normal(默认值):使用浏览器默认规则,中文按字断行,英文按单词断行。

    • break-all:强制在任何字符间断行(适用于中文、英文、数字)。

    • keep-all:中文按字断行,英文和数字不允许在单词或数字中断行。




与换行关联的css属性


除了word-break,你可能还会对white-space、word-wrap有疑问,他们与文本换行又有什么关系呢?


white-space


white-space大家一定不陌生,做文本超出显示...的时候,它是老熟人了。


white-space: nowrap; /* 禁止文本换行 */
overflow: hidden; /* 隐藏溢出内容 */
text-overflow: ellipsis; /* 使用省略号表示溢出内容 */

white-space 用于指定如何处理元素内的空白字符和如何控制文本的换行。简单来说,它的作用就是是否应该允许文本自动换行,它的默认值normal,代表文本允许换行。



所有能够换行的文本,一定拥有此默认属性white-space:normal,如果你设置nowrap,那么不管是中文还是数字或者英文,都是不会出现换行的。


white-space的换行遵循的是单词完整性优先 的规则,如果我们要使单词可以在其内部被截断,就需要使用 overflow-wrapword-breakhyphens


word-break我们已经说过了,我们介绍下


overflow-wrap


这个属性原本属于微软扩展的一个非标准、无前缀的属性,叫做 word-wrap,后来在大多数浏览器中以相同的名称实现。目前它已被更名为 overflow-wrap,word-wrap 相当于其别称。


作用:控制单词过长时是否允许断行。


常用值



  • normal:单词超出容器宽度时不换行。

  • break-word:允许在单词中断行以防止溢出。

  • anywhere:类似 break-word,但优先级更高。


实际开发中,overflow-wrap:break-word的效果同word-break: break-all



但他们存在一点点差异


换行方式





    • overflow-wrap: break-word 允许在单词内部进行断行,但会尽量保持单词的完整性。

    • word-break: break-all 则强制在任意字符间进行换行,不考虑单词的完整性。




因此,使用verflow-wrap: break-word 会比word-break: break-all更好一些!


推荐实践


通过本文,相信大家对css文本换行有了新的认识,个人比较推荐的实践如下:





    • 中文为主:可以默认使用 word-break: normal;

    • 中英文混排overflow-wrap: break-word;

    • 主要为英文或数字:需要强制换行时,使用 word-break: break-all;




考虑到场景的复杂新,大家可以word-break: break-all走天下。


作者:石小石Orz
来源:juejin.cn/post/7450110698728816655
收起阅读 »

token泄漏产生的悲剧!Vant和Rspack被注入恶意代码,全网大面积被感染

web
一、事件 2024年12月19号,在前端圈发生了一件匪夷所思的事情,前端开源项目Vant的多个版本被注入恶意代码后,发布到了npm上,导致全网大面积被感染。 随后Vant团队人员出来发声说:经排查可能是我们团队一名成员的 token 被盗用,他们正在紧急处理。...
继续阅读 »

一、事件


2024年12月19号,在前端圈发生了一件匪夷所思的事情,前端开源项目Vant的多个版本被注入恶意代码后,发布到了npm上,导致全网大面积被感染


随后Vant团队人员出来发声说:经排查可能是我们团队一名成员的 token 被盗用,他们正在紧急处理。 What?token还能被别人盗用的么,这安全性真的是差点惊掉我的下巴。



然后Vant团队人员废弃了有问题的版本,并在几个大版本2、3、4上都发布了新的安全版本2.13.63.6.164.9.15,我刚试了下,现在使用npm i vant@latest安装的是最新的4.9.15版本,事件就算是告一段落了。



二、关联事件:Rspack躺枪


攻击者拿到了vant成员的token后,进一步拿到了同个GitHub组织下另一个成员的token,并发布了同样带有恶意代码的Rspack@1.1.7版本。


这里简单介绍下Rspack,它是一个基于Rust编写打的高性能javascript打包工具,相比于webpackrollup等打包工具,它的构建性能有很大的提升,是字节团队为了解决构建性能的问题而研发的,后开源在github


Rspack这波属实是躺枪了,不过Rspack团队反应很快,已经在一小时内完成该版本的废弃处理,并发布了1.1.8修复版本,字节的问题处理速度还是可以的。目前Rspack1.1.7版本在npm上已经删除了,无法安装。



三、带来的影响


Vant作为一个老牌的国产移动端组件库,由有赞团队负责开发和维护,在github上已经拥有23.4kStar,是一款很优秀的组件库,其在国内的前端项目中应用是非常广泛的,几乎是开发H5项目的首选组件库。vant官方目前提供了 Vue 2 版本、Vue 3 版本和微信小程序版本,微信小程序版本本次不受影响,遭受攻击的是Vue2Vue3版本。


如果在发布恶意版本还未修复的期间,正好这时候有项目发布安装到了这些恶意版本,那后果不堪设想。要知道Vant可是面向用户端(C端)的组件库,其杀伤力是十分大的。


我们公司很多前端移动端项目也用了Vant组件库,不过我们项目都用了package-lock.json,所以对于我们来说问题不大,这里也简单介绍下package-lock.json,也推荐大家都用一下。


四、package-lock.json介绍


比如你在package.json中写了一个依赖^3.7.0,你用npm install安装到了3.7.0版本,然后过了一段时间后,你同事把这个代码克隆到本地,然后也执行npm install,由于此时依赖包已经更新到了3.8.0版本,所以此时你同事安装到的是3.8.0版本。


这时候问题就来了,这个依赖的开发团队“不讲武德”,在3.8.0对一个API做了改动,并且做好向后兼容,于是代码报错了,项目跑不起来了,你同事找了半天,发现是依赖更新了,很无语,浪费半天开发时间,又得加班赶项目了!


按理来说,npm install就应该向纯函数(相同的输入产生相同的输入,无副作用的函数)一样,产出相同node_modules,然而依赖是会更新的,这会导致npm install产出的结果不一样,而如果依赖开发人员不按规范升级版本,或者升级后的新版本有bug,尽管业务代码一行没改,项目再次发布时也可能会出现问题。


package-lock.json就是为了解决这个问题的,在npm install的时候,会根据package.json中的依赖版本,默认生成一份package-lock.json文件,你可以把这个lock文件上传到git仓库上,下次npm install的时候,会根据一定规则选择最终安装的版本:



  • npm v5.0.x版本:不管package.json文件中的依赖项是否有更新,都会按照package-lock.json文件中的依赖版本进行下载。

  • npm v5.1.0 - v5.4.2:当package.json的依赖版本有符合的更新版本时,会忽略package-lock.json,按照package.json安装,并更新package-lock.json文件。

  • npm v5.4.2以上:当package.json声明的的依赖版本与package-lock.json中的版本兼容时,会按照package-lock.json的版本安装,反之,如果不兼容则按照package.json安装,并更新package-lock.json文件。


npm v5.4.2这个版本已经很旧了,我们用的npm版本几乎都在这之上,所以如果用了package-lock.json文件,应该就能避免这个问题,不要怕麻烦,我觉得为了项目的稳定性,这个还是需要用的。


这个事件就介绍到这里了,对此大家怎么看呢?


作者:程序员小寒
来源:juejin.cn/post/7450080841546121243
收起阅读 »

你知道JS中有哪些“好用到爆”的一行代码?

web
哈喽,各位小伙伴们,你们好呀,我是喵手。   今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。   我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都...
继续阅读 »

哈喽,各位小伙伴们,你们好呀,我是喵手。



  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。


  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。



小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!



前言


  偶尔帮同事开发点前端页面,每次写代码,总会遇到一些能让人直呼nb的代码。今天,我们就来盘点一下那些 “好用到爆”的 JavaScript 一行代码。省去复杂的多行代码,直接用一行或者简洁的代码进行实现。也能在同事面前秀一波(当然是展示技术实力,而不是装X 🤓)。


  也许你会问:“一行代码真的能有这么强吗?” 别急,接着往下看,保证让你大呼—— 这也行?! 哈哈,待看完之后,你可能会心一笑,原来一行代码还能发挥的如此优雅!核心就是要简洁高效快速实现。


目录



  1. 妙用之美:一行代码的魅力

  2. 实用案例:JS 一行代码提升开发效率

    • 生成随机数

    • 去重数组

    • 检查变量类型

    • 深拷贝对象

    • 交换两个变量的值

    • 生成 UUID



  3. 延伸知识:一行代码背后的原理

  4. 总结与感悟


妙用之美:一行代码的魅力


  为什么“一行代码”如此让人着迷?因为它是 简洁、高效、优雅 的化身。在日常开发中,我们总希望能用更少的代码实现更多的功能,而“一行代码”就像是开发者智慧的结晶,化繁为简,带来极致的编码体验。


  当然,别以为一行代码就等同于简单。事实上,这些代码往往利用了 JavaScript 中的高级技巧,比如 ES6+ 的特性、函数式编程的思维、甚至对底层机制的深入理解。它们既是技巧的体现,也是对语言掌控力的证明。


  接下来,让我们通过一些实用案例,感受“一行代码”的高优雅吧!


实用案例:JS 一行代码提升开发效率


1. 生成随机数


在日常开发中,生成随机数是非常常见的需求。但是我可以一句代码就能搞定,示例如下:


const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;

用法示例


console.log(random(1, 100)); // 输出 1 到 100 之间的随机整数

解析:代码核心是 Math.random(),它生成一个 0 到 1 的随机数。通过数学公式将其映射到指定范围,并利用 Math.floor() 确保返回的是整数。


2. 数组去重


数组去重的方法有很多种,但下面这种方式极其优雅,不信你看!


const unique = (arr) => [...new Set(arr)];

用法示例


console.log(unique([1, 2, 2, 3, 4, 4, 5])); // [1, 2, 3, 4, 5]

解析Set 是一种集合类型,能自动去重。而 ... 是扩展运算符,可以将 Set 转换为数组,省去手动遍历的步骤。


3. 检查变量类型


判断变量类型也是日常开发中的常见操作,但是下面这一行代码就足够满足你的需求:


const type = (value) => Object.prototype.toString.call(value).slice(8, -1).toLowerCase();

用法示例


console.log(type(123)); // 'number'
console.log(type([])); // 'array'
console.log(type(null)); // 'null'

解析:通过 Object.prototype.toString 可以准确获取变量的类型信息,而 slice(8, -1) 是为了提取出 [object Type] 中的 Type 部分。


4. 深拷贝对象


经常会碰到拷贝的场景,但是对于需要深拷贝的对象,下面的一行代码简单且高效:


const deepClone = (obj) => JSON.parse(JSON.stringify(obj));

用法示例


const obj = { a: 1, b: { c: 2 } };
const copy = deepClone(obj);
console.log(copy); // { a: 1, b: { c: 2 } }

注意:这种方法不适用于循环引用的对象。如果需要处理复杂对象,建议使用 Lodash 等库。


5. 交换两个变量的值


日常中,如果是传统写法,可能会采用需要引入临时变量,但是,今天,我可以教你一个新写法,使用解构赋值就简单多了:


let a = 1, b = 2;
[a, b] = [b, a];

用法示例


console.log(a, b); // 2, 1

解析:利用 ES6 的解构赋值语法,可以轻松实现两个变量的值交换,代码简洁且直观。


6. 生成 UUID


这个大家都不陌生,且基本所有的项目中都必须有,UUID ,它是开发中常用的唯一标识符,下面这段代码可以快速生成一个符合规范的 UUID,对,就一行搞定:


const uuid = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
return c === 'x' ? r.toString(16) : ((r & 0x3) | 0x8).toString(16);
});

用法示例


console.log(uuid()); // 类似 'e4e6c7c4-d5ad-4cc1-9be8-d497c1a9d461'

解析:通过正则匹配字符 xy,并利用 Math.random() 生成随机数,再将其转换为符合 UUID 规范的十六进制格式。


延伸知识


  如上这些“一行代码”的实现主要得益于 ES6+ 的特性,如:



  • 箭头函数:让函数表达更简洁。

  • 解构赋值:提升代码的可读性。

  • 扩展运算符:操作数组和对象时更加优雅。

  • SetMap:提供高效的数据操作方式。


  所以说,深入理解这些特性,不仅能让你更轻松地掌握这些代码,还能将它们灵活地应用到实际开发中,在日常开发中游刃有余,用最简洁的代码实现最复杂的也无需求。


总结与感悟


  一行代码的背后,藏着开发者的智慧和对 JavaScript 代码的深入理解。通过这些代码,我们不仅能提升开发效率,还能在细节中感受代码的优雅与美感,这个也是我们一致的追求。


  前端开发的乐趣就在于此——简单的代码,却能带来无限可能。如果你有更好用的一行代码,欢迎分享,让我们一起玩耍 JavaScript 的更多妙用!体验其中的乐趣。


... ...


文末


好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。


... ...


学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!


wished for you successed !!!




⭐️若喜欢我,就请关注我叭。


⭐️若对您有用,就请点赞叭。


⭐️若有疑问,就请评论留言告诉我叭。


作者:喵手
来源:juejin.cn/post/7444829930175905855
收起阅读 »

做定时任务,一定要用这个神库!!

web
说实话,作为前端开发者,我们经常需要处理一些定时任务,比如轮询接口、定时刷新数据、自动登出等功能。过去我总是用 setTimeout 和 setInterval,但这些方案在复杂场景下并不够灵活。我寻找了更可靠的方案,最终发现了 ...
继续阅读 »

说实话,作为前端开发者,我们经常需要处理一些定时任务,比如轮询接口、定时刷新数据、自动登出等功能。

过去我总是用 setTimeout 和 setInterval,但这些方案在复杂场景下并不够灵活。

我寻找了更可靠的方案,最终发现了 cron 这个 npm 包,为我的前端项目(特别是 Node.js 环境下运行的那部分)带来了专业级的定时任务能力。

cron 包:不只是个定时器

安装超级简单:

npm install cron

基础用法也很直观:

import { CronJob } from 'cron';

const job = new CronJob(
'0 */30 * * * *', // 每30分钟执行一次
function () {
console.log('刷新用户数据...');
// 这里放刷新数据的代码
},
null, // 完成时的回调
true, // 是否立即启动
'Asia/Shanghai' // 时区
);

看起来挺简单的,对吧?

但这个小包却能解决前端很多定时任务的痛点。

理解 cron 表达式,这个"魔法公式"

刚开始接触 cron 表达式时,我觉得这简直像某种加密代码。* * * * * * 这六个星号到底代表什么?

在 npm 的 cron 包中,表达式有六个位置(比传统的 cron 多一个),分别是:

秒 分 时 日 月 周

比如 0 0 9 * * 1 表示每周一早上 9 点整执行。

我找到一个特别好用的网站 crontab.guru 来验证表达式。

不过注意,那个网站是 5 位的表达式,少了"秒"这个位置,所以用的时候需要自己在前面加上秒的设置。

月份和星期几还可以用名称来表示,更直观:

// 每周一、三、五的下午5点执行
const job = new CronJob('0 0 17 * * mon,wed,fri', function () {
console.log('工作日提醒');
});

前端开发中的实用场景

作为前端开发者,我在这些场景中发现 cron 特别有用:

1. 在 Next.js/Nuxt.js 等同构应用中刷新数据缓存

// 每小时刷新一次产品数据缓存
const cacheRefreshJob = new CronJob(
'0 0 * * * *',
async function () {
try {
const newData = await fetchProductData();
updateProductCache(newData);
console.log('产品数据缓存已更新');
} catch (error) {
console.error('刷新缓存失败:', error);
}
},
null,
true,
'Asia/Shanghai'
);

2. Electron 应用中的定时任务

// 在 Electron 应用中每5分钟同步一次本地数据到云端
const syncJob = new CronJob(
'0 */5 * * * *',
async function () {
if (navigator.onLine) {
// 检查网络连接
try {
await syncDataToCloud();
sendNotification('数据已同步');
} catch (err) {
console.error('同步失败:', err);
}
}
},
null,
true
);

3. 定时检查用户会话状态

// 每分钟检查一次用户活动状态,30分钟无活动自动登出
const sessionCheckJob = new CronJob(
'0 * * * * *',
function () {
const lastActivity = getLastUserActivity();
const now = new Date().getTime();

if (now - lastActivity > 30 * 60 * 1000) {
console.log('用户30分钟无活动,执行自动登出');
logoutUser();
}
},
null,
true
);

踩过的那些坑

使用 cron 包时我踩过几个坑,分享给大家:

  1. 时区问题:有次我设置了一个定时提醒功能,但总是提前 8 小时触发。一查才发现是因为没设置时区。所以国内用户一定要设置 'Asia/Shanghai'
// 这样才会在中国时区的下午6点执行
const job = new CronJob('0 0 18 * * *', myFunction, null, true, 'Asia/Shanghai');
  1. this 指向问题:如果你用箭头函数作为回调,会发现无法访问 CronJob 实例的 this。
// 错误示范
const job = new CronJob('* * * * * *', () => {
console.log('执行任务');
this.stop(); // 这里的 this 不是 job 实例,会报错!
});

// 正确做法
const job = new CronJob('* * * * * *', function () {
console.log('执行任务');
this.stop(); // 这样才能正确访问 job 实例
});
  1. v3 版本变化:如果你从 v2 升级到 v3,要注意月份索引从 0-11 变成了 1-12。

实战案例:构建一个智能通知系统

这是我在一个电商前端项目中实现的一个功能,用 cron 来管理各种用户通知:

import { CronJob } from 'cron';
import { getUser, getUserPreferences } from './api/user';
import { sendNotification } from './utils/notification';

class NotificationManager {
constructor() {
this.jobs = [];
this.initialize();
}

initialize() {
// 新品上架提醒 - 每天早上9点
this.jobs.push(
new CronJob(
'0 0 9 * * *',
async () => {
if (!this.shouldSendNotification('newProducts')) return;

const newProducts = await this.fetchNewProducts();
if (newProducts.length > 0) {
sendNotification('新品上架', `今天有${newProducts.length}款新品上架啦!`);
}
},
null,
true,
'Asia/Shanghai'
)
);

// 限时优惠提醒 - 每天中午12点和晚上8点
this.jobs.push(
new CronJob(
'0 0 12,20 * * *',
async () => {
if (!this.shouldSendNotification('promotions')) return;

const promotions = await this.fetchActivePromotions();
if (promotions.length > 0) {
sendNotification('限时优惠', '有新的限时优惠活动,点击查看详情!');
}
},
null,
true,
'Asia/Shanghai'
)
);

// 购物车提醒 - 每周五下午5点提醒周末特价
this.jobs.push(
new CronJob(
'0 0 17 * * 5',
async () => {
if (!this.shouldSendNotification('cartReminder')) return;

const cartItems = await this.fetchUserCart();
if (cartItems.length > 0) {
sendNotification('周末将至', '别忘了查看购物车中的商品,周末特价即将开始!');
}
},
null,
true,
'Asia/Shanghai'
)
);

console.log('通知系统已初始化');
}

async shouldSendNotification(type) {
const user = getUser();
if (!user) return false;

const preferences = await getUserPreferences();
return preferences?.[type] === true;
}

// 其他方法...

stopAll() {
this.jobs.forEach(job => job.stop());
console.log('所有通知任务已停止');
}
}

export const notificationManager = new NotificationManager();

写在最后

作为前端开发者,我们的工作不只是构建漂亮的界面,还需要处理各种复杂的交互和时序逻辑。

npm 的 cron 包为我们提供了一种专业而灵活的方式来处理定时任务,特别是在 Node.js 环境下运行的前端应用(如 SSR 框架、Electron 应用等)。

它让我们能够用简洁的表达式设定复杂的执行计划,帮助我们构建更加智能和用户友好的前端应用。


作者:Immerse
来源:juejin.cn/post/7486390904992890895
收起阅读 »

Browser.js:轻松模拟浏览器环境

web
什么是Browser.js Browser.js是一个小巧而强大的JavaScript库,它模拟浏览器环境,使得原本只能在浏览器中运行的代码能够在Node.js环境中执行。这意味着你可以在服务器端运行前端代码,而不需要依赖真实浏览器 Browser.js的核心...
继续阅读 »

什么是Browser.js


Browser.js是一个小巧而强大的JavaScript库,它模拟浏览器环境,使得原本只能在浏览器中运行的代码能够在Node.js环境中执行。这意味着你可以在服务器端运行前端代码,而不需要依赖真实浏览器


Browser.js的核心原理


Browser.js通过实现与浏览器兼容的API(如windowdocumentnavigator等)来创建一个近似真实的浏览器上下文。它还支持fetch API用于网络请求,支持Promise,使得异步操作更加方便


Browser.js的用途


Browser.js主要用于以下场景:



  • 服务器端测试:在服务端运行前端单元测试,无需依赖真实浏览器,从而提高测试效率


    // 示例:使用Browser.js进行服务器端测试
    const browser = require('browser.js');
    const window = browser.window;
    const document = browser.document;

    // 在Node.js中模拟浏览器环境
    console.log(window.location.href);


  • 构建工具:编译或预处理只能在浏览器运行的库,例如基于DOM的操作,如CSS处理器或模板引擎


    // 示例:使用Browser.js处理CSS
    const browser = require('browser.js');
    const document = browser.document;

    // 创建一个CSS样式表
    const style = document.createElement('style');
    style.textContent = 'body { background-color: #f2f2f2; }';
    document.head.appendChild(style);


  • 离线应用:将部分业务逻辑放在客户端和服务端之外,在本地环境中执行1


    // 示例:使用Browser.js在本地环境中执行业务逻辑
    const browser = require('browser.js');
    const window = browser.window;

    // 在本地环境中执行JavaScript代码
    window.alert('Hello, World!');


  • 自动化脚本:对网页进行自动化操作,如爬虫、数据提取等,而不必依赖真实浏览器1


    // 示例:使用Browser.js进行网页爬虫
    const browser = require('browser.js');
    const fetch = browser.fetch;

    // 发送HTTP请求获取网页内容
    fetch('https://example.com')
    .then(response => response.text())
    .then(html => console.log(html));



解决的问题


Browser.js解决了以下问题:



  • 跨环境执行:使得原本只能在浏览器中运行的JavaScript代码能够在Node.js环境中执行,扩展了JavaScript的应用边界

  • 兼容性问题:通过模拟浏览器环境,减少了不同浏览器之间的兼容性问题,提高了代码的可移植性

  • 测试效率:提高了前端代码在服务端的测试效率,减少了对真实浏览器的依赖


Browser.js的特点



  • 轻量级:体积小,引入方便,不会过多影响项目整体性能

  • 兼容性:模拟的浏览器环境高度兼容Web标准,能够运行大部分浏览器代码

  • 易用性:提供简单直观的API接口,快速上手

  • 可扩展:支持自定义插件,可以根据需求扩展功能

  • 无依赖:不依赖其他大型库或框架,降低项目复杂度


作者:Y11_推特同名
来源:juejin.cn/post/7486845198485585935
收起阅读 »

Vue 首个 AI 组件库发布!

web
人工智能(AI)已深度融入我们的生活,如今 Vue 框架也迎来了首个 AI 组件库 —— Ant Design X Vue,为开发者提供了强大的 AI 开发工具。 Ant Design X Vue 概述 Ant Design X Vue 是基于 Vue.js ...
继续阅读 »

人工智能(AI)已深度融入我们的生活,如今 Vue 框架也迎来了首个 AI 组件库 —— Ant Design X Vue,为开发者提供了强大的 AI 开发工具。


Ant Design X Vue 概述


Ant Design X Vue 是基于 Vue.js 的 AI 组件库,旨在简化 AI 集成开发。



它包含高度定制化的 AI 组件和 API 解决方案,支持无缝接入 AI 服务,是构建智能应用的理想选择。


组件库亮点


丰富多样的 AI 组件


通用组件



  • Bubble:显示会话消息气泡,支持多种布局。

  • Conversations:管理多个会话,查看历史记录。


唤醒组件



  • Welcome:会话加载时插入欢迎语。

  • Prompts:展示上下文相关的问题或建议。


表达组件



  • Sender:构建会话输入框,支持自定义样式。

  • Attachments:展示和管理附件信息。

  • Suggestion:提供快捷输入提示。


确认组件



  • ThoughtChain:展示 AI 的思维过程或结果。


工具组件



  • useXAgent:对接 AI 模型推理服务。

  • useXChat:管理 AI 对话应用的数据流。

  • XStream:处理数据流,支持流式传输。

  • XRequest:向 AI 服务发起请求。

  • XProvider:全局化配置管理。


RICH 设计范式


基于 RICH 设计范式,提供丰富、沉浸式、连贯和人性化的交互体验,适应不同 AI 场景。


AGI 混合界面(Hybrid-UI)


融合 GUI 和自然会话交互,用户可在同一应用中自由切换交互方式,提升体验。


适用场景



  • 智能聊天应用:构建多轮对话界面,支持复杂会话逻辑。

  • 企业级 AI 系统:快速搭建智能客服、知识管理等系统。


如何使用 Ant Design X Vue


安装与引入


npm install ant-design-x-vue --save

引入组件库及样式:


import Vue from 'vue';
import Antd from 'ant-design-x-vue';
import 'ant-design-x-vue/dist/antd.css';

Vue.use(Antd);

使用组件


示例:使用 Bubble 组件展示对话气泡


<template>
<div>
<a-bubble content="欢迎使用 Ant Design X Vue!" />
</div>
</template>

官方文档与示例



访问 Ant Design X Vue 官方文档:https://antd-design-x-vue.netlify.app/ 获取更多信息。


Ant Design X Vue 为 Vue 开发者提供了强大的 AI 组件库,助力高效构建智能应用。


无论是聊天应用还是企业级系统,都值得一试。


最后



如果觉得本文对你有帮助,希望能够给我点赞支持一下哦 💪 也可以关注wx公众号:前端开发爱好者 回复加群,一起学习前端技能 公众号内包含很多实战精选资源教程,欢迎关注



作者:前端开发爱好者
来源:juejin.cn/post/7475978280841543716
收起阅读 »

如何限制一个账号只能在一处登录

如何限制一个账号只能在一处登录要实现账号单点登录(一处登录限制),需结合 会话管理、实时状态同步 和 冲突处理机制。以下是完整技术方案:一、核心实现方案服务端会话控制(推荐)// 用户登录时生成唯一令牌并记录 public Str...
继续阅读 »

如何限制一个账号只能在一处登录

要实现账号单点登录(一处登录限制),需结合 会话管理实时状态同步 和 冲突处理机制。以下是完整技术方案:

一、核心实现方案

  1. 服务端会话控制(推荐)
// 用户登录时生成唯一令牌并记录
public String login(String username, String password) {
// 1. 验证账号密码
User user = userService.authenticate(username, password);

// 2. 生成新令牌并失效旧会话
String newToken = UUID.randomUUID().toString();
redis.del("user:" + user.getId() + ":token"); // 清除旧token
redis.setex("user:" + user.getId() + ":token", 3600, newToken);

// 3. 返回新令牌
return newToken;
}
  1. WebSocket实时踢出(增强体验)
// 前端建立长连接
const socket = new WebSocket(`wss://api.example.com/ws?token=${token}`);

socket.onmessage = (event) => {
if (event.data === 'force_logout') {
alert('您的账号在其他设备登录');
location.href = '/logout';
}
};
  1. 登录设备指纹识别
// 生成设备指纹(前端)
function generateDeviceFingerprint() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.font = "14px Arial";
ctx.fillText("BrowserFingerprint", 2, 2);
return canvas.toDataURL().hashCode(); // 简化示例
}

// 服务端校验
if (storedFingerprint != currentFingerprint) {
forceLogout(storedToken);
}

二、多端适配策略

客户端类型实现方案
Web浏览器JWT令牌 + Redis黑名单
移动端APP设备ID绑定 + FCM/iMessage推送踢出
桌面应用硬件指纹 + 本地令牌失效检测
微信小程序UnionID绑定 + 服务端订阅消息

三、关键代码实现

  1. JWT令牌增强方案
// 生成带设备信息的JWT
public String generateToken(User user, String deviceId) {
return Jwts.builder()
.setSubject(user.getId())
.claim("device", deviceId) // 绑定设备
.setExpiration(new Date(System.currentTimeMillis() + 3600000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}

// 校验令牌时检查设备
public boolean validateToken(String token, String currentDevice) {
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
return claims.get("device").equals(currentDevice);
}
  1. Redis实时状态管理
# 使用Redis Hash存储登录状态
def login(user_id, token, device_info):
# 删除该用户所有活跃会话
r.delete(f"user_sessions:{user_id}")

# 记录新会话
r.hset(f"user_sessions:{user_id}",
mapping={
"token": token,
"device": device_info,
"last_active": datetime.now()
})
r.expire(f"user_sessions:{user_id}", 3600)

# 中间件校验
def check_token(request):
user_id = get_user_id_from_token(request.token)
stored_token = r.hget(f"user_sessions:{user_id}", "token")
if stored_token != request.token:
raise ForceLogoutError()

四、异常处理机制

场景处理方案
网络延迟冲突采用CAS(Compare-And-Swap)原子操作更新令牌
令牌被盗用触发二次验证(短信/邮箱验证码)
多设备同时登录后登录者优先,前会话立即失效(可配置为保留第一个登录)

五、性能与安全优化

  1. 会话同步优化

    # Redis Pub/Sub 跨节点同步
    PUBLISH user:123 "LOGOUT"
  2. 安全增强

    // 前端敏感操作二次确认
    function sensitiveOperation() {
    if (loginTime < lastServerCheckTime) {
    showReauthModal();
    }
    }
  3. 监控看板

    指标报警阈值
    并发登录冲突率>5%/分钟
    强制踢出成功率<99%

六、行业实践参考

  1. 金融级方案

    • 每次操作都验证设备指纹
    • 异地登录需视频人工审核
  2. 社交应用方案

    • 允许最多3个设备在线
    • 分设备类型控制(手机+PC+平板)
  3. ERP系统方案

    • 绑定特定MAC地址
    • VPN网络白名单限制

通过以上方案可实现:

  • 严格模式:后登录者踢出前会话(适合银行系统)
  • 宽松模式:多设备在线但通知告警(适合社交应用)
  • 混合模式:关键操作时强制单设备(适合电商系统)

部署建议:

  1. 根据业务需求选择合适严格度
  2. 关键系统增加异地登录二次验证
  3. 用户界面明确显示登录设备列表

作者:Epicurus
来源:juejin.cn/post/7485384798569250868

收起阅读 »

前端如何彻底解决重复请求问题

web
背景 保存按钮点击多次,造成新增多个单据 列表页疯狂刷新,导致服务器压力大 如何彻底解决 方案:我的思路从请求层面判断相同请求只发送一次,将结果派发给各个订阅者 实现思路 对请求进行数据进行hash 添加store 存储 hash => Array...
继续阅读 »

背景



  1. 保存按钮点击多次,造成新增多个单据

  2. 列表页疯狂刷新,导致服务器压力大


如何彻底解决


方案:我的思路从请求层面判断相同请求只发送一次,将结果派发给各个订阅者


实现思路



  1. 对请求进行数据进行hash

  2. 添加store 存储 hash => Array promise

  3. 相同请求,直接订阅对应的promise

  4. 请求取消,则将store中对应的promise置为null

  5. 请求返回后,调用所有未取消的订阅


核心代码



private handleFinish(key: string, index: number) {
const promises = this.store.get(key);
// 只有一个promise时则删除store
if (promises?.filter((item) => item).length === 1) {
this.store.delete(key);
} else if (promises && promises[index]) {
// 还有其他请求,则将当前取消的、或者完成的置为null
promises[index] = null;
}
}

private async handleRequest(config: any) {
const hash = sha256.create();
hash.update(
JSON.stringify({
params: config.params,
data: config.data,
url: config.url,
method: config.method,
}),
);
const fetchKey = hash.hex().slice(0, 40);
const promises = this.store.get(fetchKey);
const index = promises?.length || 0;
let promise = promises?.find((item) => item);
const controller = new AbortController();

if (config.signal) {
config.signal.onabort = (reason: any) => {
const _promises = this.store.get(fetchKey)?.filter((item) => item);
if (_promises?.length === 1) {
controller.abort(reason);
this.handleFinish(fetchKey, index);
}
};
}

if (!promise) {
promise = this.instance({
...config,
signal: controller.signal,
headers: {
...config.headers,
fetchKey,
},
}).catch((error) => {
console.log(error, "请求错误");
// 失败的话,立即删除,可以重试
this.handleFinish(fetchKey, index);
return { error };
});
}

const newPromise = Promise.resolve(promise)
.then((result: any) => {
if (config.signal?.aborted) {
this.handleFinish(fetchKey, index);
return result;
}
return result;
})
.finally(() => {
setTimeout(() => {
this.handleFinish(fetchKey, index);
}, 500);
});
this.store.set(fetchKey, [...(promises || []), newPromise]);
return newPromise;
}

以下为完整代码(仅供参考)


index.ts


import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
import { sha256 } from "js-sha256";
import transformResponseValue, { updateObjTimeToUtc } from "./utils";

type ErrorInfo = {
message: string;
status?: number;
traceId?: string;
version?: number;
};

type MyAxiosOptions = AxiosRequestConfig & {
goLogin: (type?: string) => void;
onerror: (info: ErrorInfo) => void;
getHeader: () => any;
};

export type MyRequestConfigs = AxiosRequestConfig & {
// 是否直接返回服务端返回的数据,默认false, 只返回 data
useOriginData?: boolean;
// 触发立即更新
flushApiHook?: boolean;
ifHandleError?: boolean;
};

type RequestResult<T, U> = U extends { useOriginData: true }
? T
: T extends { data?: infer D }
? D
: never;

class LmAxios {
private instance: AxiosInstance;

private store: Map<string, Array<Promise<any> | null>>;

private options: MyAxiosOptions;

constructor(options: MyAxiosOptions) {
this.instance = axios.create(options);

this.options = options;
this.store = new Map();
this.interceptorRequest();
this.interceptorResponse();
}

// 统一处理为utcTime
private interceptorRequest() {
this.instance.interceptors.request.use(
(config) => {
if (config.params) {
config.params = updateObjTimeToUtc(config.params);
}
if (config.data) {
config.data = updateObjTimeToUtc(config.data);
}
return config;
},
(error) => {
console.log("intercept request error", error);
Promise.reject(error);
},
);
}

// 统一处理为utcTime
private interceptorResponse() {
this.instance.interceptors.response.use(
(response): any => {
// 对响应数据做处理,以下根据实际数据结构改动!!...
const [checked, errorInfo] = this.checkStatus(response);

if (!checked) {
return Promise.reject(errorInfo);
}

const disposition =
response.headers["content-disposition"] ||
response.headers["Content-Disposition"];
// 文件处理
if (disposition && disposition.indexOf("attachment") !== -1) {
const filenameReg =
/filename\*?=['"]?(?:UTF-\d['"]*)?([^;\r\n"']*)['"]?;?/g;
const filenames: string[] = [];
disposition.replace(filenameReg, (r: any, r1: string) => {
filenames.push(decodeURIComponent(r1));
});
return Promise.resolve({
filename: filenames[filenames.length - 1],
data: response.data,
});
}
if (response) {
return Promise.resolve(response.data);
}
},
(error) => {
console.log("request error", error);
if (error.message.indexOf("timeout") !== -1) {
return Promise.reject({
message: "请求超时",
});
}
const [checked, errorInfo] = this.checkStatus(error.response);
return Promise.reject(errorInfo);
},
);
}

private checkStatus(
response: AxiosResponse<any>,
): [boolean] | [boolean, ErrorInfo] {
const { code, message = "" } = response?.data || {};
const { headers, status } = response || {};

if (!status) {
return [false];
}

// 单地登录判断,弹出不同提示
if (status === 401) {
this.options?.goLogin();
return [false];
}

if (code === "ECONNABORTED" && message?.indexOf("timeout") !== -1) {
return [
false,
{
message: "请求超时",
},
];
}

if ([108, 109, 401].includes(code)) {
this.options.goLogin();
return [false];
}
if ((code >= 200 && code < 300) || code === 304) {
// 如果http状态码正常,则直接返回数据
return [true];
}

if (!code && ((status >= 200 && status < 300) || status === 304)) {
return [true];
}

let errorInfo = "";
const _code = code || status;
switch (_code) {
case -1:
errorInfo = "远程服务响应失败,请稍后重试";
break;
case 400:
errorInfo = "400: 错误请求";
break;
case 401:
errorInfo = "401: 访问令牌无效或已过期";
break;
case 403:
errorInfo = message || "403: 拒绝访问";
break;
case 404:
errorInfo = "404: 资源不存在";
break;
case 405:
errorInfo = "405: 请求方法未允许";
break;
case 408:
errorInfo = "408: 请求超时";
break;
case 500:
errorInfo = message || "500: 访问服务失败";
break;
case 501:
errorInfo = "501: 未实现";
break;
case 502:
errorInfo = "502: 无效网关";
break;
case 503:
errorInfo = "503: 服务不可用";
break;
default:
errorInfo = "连接错误";
}

return [
false,
{
message: errorInfo,
status: _code,
traceId: response?.data?.requestId,
version: response.data.ver,
},
];
}

private handleFinish(key: string, index: number) {
const promises = this.store.get(key);
if (promises?.filter((item) => item).length === 1) {
this.store.delete(key);
} else if (promises && promises[index]) {
promises[index] = null;
}
}

private async handleRequest(config: any) {
const hash = sha256.create();
hash.update(
JSON.stringify({
params: config.params,
data: config.data,
url: config.url,
method: config.method,
}),
);
const fetchKey = hash.hex().slice(0, 40);
const promises = this.store.get(fetchKey);
const index = promises?.length || 0;
let promise = promises?.find((item) => item);
const controller = new AbortController();

if (config.signal) {
config.signal.onabort = (reason: any) => {
const _promises = this.store.get(fetchKey)?.filter((item) => item);
if (_promises?.length === 1) {
controller.abort(reason);
this.handleFinish(fetchKey, index);
}
};
}

if (!promise) {
promise = this.instance({
...config,
signal: controller.signal,
headers: {
...config.headers,
fetchKey,
},
}).catch((error) => {
console.log(error, "请求错误");
// 失败的话,立即删除,可以重试
this.handleFinish(fetchKey, index);
return { error };
});
}

const newPromise = Promise.resolve(promise)
.then((result: any) => {
if (config.signal?.aborted) {
this.handleFinish(fetchKey, index);
return result;
}
return result;
})
.finally(() => {
setTimeout(() => {
this.handleFinish(fetchKey, index);
}, 500);
});
this.store.set(fetchKey, [...(promises || []), newPromise]);
return newPromise;
}

// add override type
public async request<T = unknown, U extends MyRequestConfigs = {}>(
url: string,
config: U,
): Promise<RequestResult<T, U> | null> {
// todo
const options = {
url,
// 是否统一处理接口失败(提示)
ifHandleError: true,
...config,
headers: {
...this.options.getHeader(),
...config?.headers,
},
};

const res = await this.handleRequest(options);

if (!res) {
return null;
}

if (res.error) {
if (res.error.message && options.ifHandleError) {
this.options.onerror(res.error);
}
throw new Error(res.error);
}

if (config.useOriginData) {
return res;
}

if (config.headers?.feTraceId) {
window.dispatchEvent(
new CustomEvent<{ flush?: boolean }>(config.headers.feTraceId, {
detail: {
flush: config?.flushApiHook,
},
}),
);
}

// 默认返回res.data
return transformResponseValue(res.data)
}
}

export type MyRequest = <T = unknown, U extends MyRequestConfigs = {}>(
url: string,
config: U,
) =>
Promise<RequestResult<T, U> | null>;

export default LmAxios;


utils.ts(这里主要用来处理utc时间,你可能用不到删除相关代码就好)


import moment from 'moment';

const timeReg =
/^\d{4}([/:-])(1[0-2]|0?[1-9])\1(0?[1-9]|[1-2]\d|30|31)($|( |T)(?:[01]\d|2[0-3])(:[0-5]\d)?(:[0-5]\d)?(\..*\d)?Z?$)/;

export function formatTimeValue(time: string, format = 'YYYY-MM-DD HH:mm:ss') {
if (typeof time === 'string' || typeof time === 'number') {
if (timeReg.test(time)) {
return moment(time).format(format);
}
}
return time;
}

// 统一转化如参
export const updateObjTimeToUtc = (obj: any) => {
if (typeof obj === 'string') {
if (timeReg.test(obj)) {
return moment(obj).utc().format();
}
return obj;
}
if (toString.call(obj) === '[object Object]') {
const newObj: Record<string, any> = {};
Object.keys(obj).forEach((key) => {
newObj[key] = updateObjTimeToUtc(obj[key]);
});
return newObj;
}
if (toString.call(obj) === '[object Array]') {
obj = obj.map((item: any) => updateObjTimeToUtc(item));
}
return obj;
};

const utcReg = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*Z$/;

const transformResponseValue = (res: any) => {
if (!res) {
return res;
}
if (typeof res === 'string') {
if (utcReg.test(res)) {
return moment(res).format('YYYY-MM-DD HH:mm:ss');
}
return res;
}
if (toString.call(res) === '[object Object]') {
const result: any = {};
Object.keys(res).forEach((key) => {
result[key] = transformResponseValue(res[key]);
});
return result;
}
if (toString.call(res) === '[object Array]') {
return res.map((item: any) => transformResponseValue(item));
}
return res;
};
export default transformResponseValue;


作者:枫荷
来源:juejin.cn/post/7484202915390718004
收起阅读 »

[译]为什么我选择用Tauri来替代Electron

web
原文地址:Why I chose Tauri instead of Electron 以下为正文。 关于我为什么在Tauri上创建Aptakube的故事,我与Electron的斗争以及它们之间的简短比较。 大约一年前,我决定尝试构建一个桌面应用程序。 我对...
继续阅读 »

原文地址:Why I chose Tauri instead of Electron
以下为正文。



关于我为什么在Tauri上创建Aptakube的故事,我与Electron的斗争以及它们之间的简短比较。



大约一年前,我决定尝试构建一个桌面应用程序。


我对自己开发的小众应用并不满意,我想我可以开发出更好的应用。我作为全栈开发人员工作了很长时间,但我以前从未构建过桌面应用程序。


我的第一个想法是用 SwiftUI 来构建。开发者喜欢原生应用,我也一直想学习 Swift。然而,在 SwiftUI 上构建会将我的受众限制为 macOS 用户。虽然我有一种感觉,大多数用户无论如何都会使用 macOS,但当我可以构建跨平台应用程序时,为什么要限制自己呢?


现在回想起来,我真的很庆幸当初放弃了 SwiftUI。看看人们在不同的操作系统上使用我的应用就知道了。


Pasted image 20240321161648.png



Windows和Linux代表了超过35%的用户。这相当于放弃了35%的收入。



那么 Electron 怎么样呢?


我不是与世隔绝的人,所以我知道 Electron 是个好东西,我每天使用的很多流行应用都是基于 Electron 开发的,包括我现在用来写这篇文章的编辑器。它似乎非常适合我想做的事情,因为:



  • 单个代码库可以针对多个平台。

  • 支持 React + TypeScript + Tailwind 配合使用,每个我都非常熟练。

  • 非常受欢迎 = 很多资源和指南。

  • NPM 是最大的(是吧?)软件包社区,这意味着我可以更快地发布。


在 Electron 上开发的另一个好处是,我可以专注于开发应用程序,而不是学习一些全新的东西。我喜欢学习新的语言和框架,但我想快速构建一些有用的东西。我仍然需要学习 Electron 本身,但它不会像学习 Swift 和 SwiftUI 那样困难。


好了,我们开始吧!


我决定了。Aptakube 将使用 Electron 来构建。


我通常不看文档。我知道我应该看,但我没有。不过,每当我第一次选择一个框架时,我总会阅读 “入门(Getting Started)” 部分。


流行的框架都有一个 npx create {framework-name},可以为我们快速一个应用程序。Next.js、Expo、Remix 和许多其他框架都有这样的功能。我发现这非常有用,因为它们可以让你快速上手,而且通常会给你提供很多选项,例如:



  • 你想使用 TypeScript 还是 JavaScript?

  • 你想使用 CSS 框架吗?那 Tailwind 呢?

  • Prettier 和/或 ESLint?

  • 你要这个还是那个?


这样的例子不胜枚举。这是一种很棒的开发体验,我希望每个框架都有一个。


我可以直接 npx create electron-app 吗?


显然,我不能,或者至少我还没有找到一种方法来做到这一点,除了在 Getting Started 里。


相反,我找到的是一个“快速启动(quick-start)”模板,我可以从 Git 上克隆下来,安装依赖项,然后就可以开始我的工作了。


然而,它不是 TypeScript,没有打包工具,没有 CSS 框架,没有检查,没有格式化,什么都没有。它只是一个简单的打开窗口的应用程序。


我开始用这个模板构建,并添加了所有我想让它使用的东西。我以为会很容易,但事实并非如此。


一个 electron 应用程序有三个入口点: mainpreload(预加载),和render(渲染)。把所有这些和 Vite 连接起来是很痛苦的。我花了大约两周的空闲时间试图让所有东西都工作起来。我失败了,这让我很沮丧。


之后我为 Electron 找到了几十个其他的样板。我试了大约五个。有些还可以,但大多数模板都太固执己见了,而且安装了太多的依赖项,我甚至不知道它们是用来做什么的,这让我不太喜欢。有些甚至根本不工作,因为它们已经被遗弃多年了。


总之,对于刚接触 Electron 的人来说,开发体验低于平均水平。Next.js 和 Expo 将标准设置得如此之高,以至于我开始期待每个框架都能提供类似的体验。


那现在怎么办?


在漫无目的刷 Twitter 的时候,我看到了一条来自 Tauri 的有关 1.0 版本的推文。那时他们已经成立 2 年了,但我完全不知道 Tauri 是什么。我去看了他们的网站,我被震撼了 🤯 这似乎就是我正在寻找的东西。


你知道最棒的是什么吗?他们把一条 npm create tauri-app 命令放在了主页上。


Pasted image 20240327173829.png



Tauri 用 npx create Tauri -app 命令从一开始就抓住了开发体验。



我决定尝试一下。我运行了 create the tauri-app 命令,体验与 Next.js 非常相似。它问了我几个问题,然后根据我的选择为我创建了一个新项目。


在这之后,我可以简单地运行 npm run dev,然后我就有了一个带有热加载、TypeScript、Vite 和 Solid.js 的可以运行的应用程序,几乎包含了我开始使用所需的一切。这让我印象深刻,并且很想了解更多。我仍然不得不添加 Prettier、Linters、Tailwind 等类似的东西,但我已经习惯了,而且它比 Electron 容易太多了。


Pasted image 20240328093731.png


开始(再一次😅),但与 Tauri 一起


虽然在 Electron 中,我可以只用 JavaScript/HTML/CSS 构建整个应用程序,但在 Tauri 中,后端是 Rust,只有前端是 JavaScript。这显然意味着我必须学习 Rust,这让我很兴奋,但也不想花太多时间,因为我想快速构建原型。


我在使用过 7 种以上专业的编程语言,所以我认为学习 Rust 是轻而易举的事。


我错了。我大错特错了。Rust 是很难的,真的很难,至少对我来说是这样!


一年后,在我的应用发布了 20 多个版本之后,我仍然不能说我真正了解 Rust。我知道要不断地定期发布新功能,但每次我必须用 Rust 写一些东西时,我仍然能学到很多新知识。GitHub Copilot 和 ChatGPT 帮了我大忙,我现在还经常使用它们。


Pasted image 20240403164309.png



像使用字符串这样简单的事情在Rust中要比在其他语言中复杂得多🤣



不过,Tauri 中有一些东西可以让这一过程变得简单许多。


Tauri 有一个“command 命令”的概念,它就像前端和后端之间的桥梁。你可以用 Rust 在你的 Tauri 后端定义“命令”,然后在 JavaScript 中调用它们。Tauri 本身提供了一系列可以开箱即用的命令。例如,你可以通过 JavaScript 打开一个文件选择器(file dialog),读取/更新/删除文件,发送 HTTP 请求,以及其他很多与操作系统进行的交互,而无需编写任何 Rust 代码。


那么,如果你需要做一些在 Tauri 中没有的事情呢?这就是“Plugins插件”的用武之地。插件是 Rust 库,它定义了你可以在 Tauri 应用中使用的命令。稍后我会详细介绍插件,但现在你只需将它们视为扩展 Tauri 功能的一种方式就可以了。


事实上,我已经询问了很多使用 Tauri 构建应用程序的人,问他们是否必须编写 Rust 代码来构建他们的应用程序。他们中的大多数表示,他们只需要为一些特定的使用情况编写非常少的 Rust 代码。完全可以在不编写任何 Rust 代码的情况下构建一个 Tauri 应用程序!


那么 Tauri 与 Electron 相比又如何呢?


1. 编程语言和社区


在 Electron 中,你的后端是一个 Node.js 进程,而前端是 Chromium,这意味着 Web 开发人员可以仅使用 JavaScript/HTML/CSS 来构建桌面应用程序。NPM 上有着庞大的库社区,并且在互联网上有大量与此相关的内容,这使得学习过程变得更加容易。


然而,尽管通常认为能够在后端和前端之间共享代码是一件好事,但也可能会导致混淆,因为开发人员可能会尝试在前端使用后端函数,反之亦然。因此,你必须小心不要混淆。


相比之下,Tauri 的后端是 Rust,前端也是一个 Webview(稍后会详细介绍)。虽然有大量的 Rust 库,但它们远远不及 NPM 的规模。Rust 社区也比 JavaScript 社区小得多,这意味着关于它的内容在互联网上较少。但正如上面提到的,取决于你要构建的内容,你甚至可能根本不需要编写太多的 Rust 代码。


我的观点: 我只是喜欢我们在 Tauri 中得到的明确的前后端的分离。如果我在 Rust 中编写一段代码,我知道它将作为一个操作系统进程运行,并且我可以访问网络、文件系统和许多其他内容,而我在 JavaScript 中编写的所有内容都保证在一个 Webview 上运行。学习 Rust 对我来说并不容易,但我很享受这个过程,而且总的来说我学到了很多新东西!Rust 开始在我心中生根了。😊


2. Webview


在 Electron 中,前端是一个与应用程序捆绑在一起的 Chromium Webview。这意味着无论操作系统如何,您都可以确定应用程序使用的 Node.js 和 Chromium 版本。这带来了重大的好处,但也有一些缺点。


最大的好处是开发和测试的便利性,您知道哪些功能可用,如果某些功能在 macOS 上可用,那么它很可能也可以在 Windows 和 Linux 上使用。然而,缺点是由于所有这些二进制文件捆绑在一起,您的应用程序大小会更大。


Tauri 采用了截然不同的方法。它不会将 Chromium 与您的应用程序捆绑在一起,而是使用操作系统的默认 Webview。这意味着在 macOS 上,您的应用程序将使用 WebKit(Safari 的引擎),在 Windows 上将使用 WebView2(基于 Chromium),在 Linux 上将使用WebKitGTK(与 Safari 相同)。


最终结果是一个感觉非常快速的极小型应用程序!


作为参考,我的 Tauri 应用程序在 macOS 上只有 24.7MB 大小,而我的竞争对手的应用程序(Electron)则达到了 1.3GB。


为什么这很重要?



  • 下载和安装速度快得多。

  • 主机和分发成本更低(我在 AWS 上运行,所以我需要支付带宽和存储费用)。

  • 我经常被问到我的应用是否使用 Swift 构建,因为用户通常在看到如此小巧且快速的应用时会有一种“这感觉像是本地应用”的时候。

  • 安全性由操作系统处理。如果 WebKit 存在安全问题,苹果将发布安全更新,我的应用将简单地使用它。我不必发布我的应用的更新版本来修复它。


我的观点: 我喜欢我的应用如此小巧且快速。起初,我担心操作系统之间缺乏一致性会导致我需要在所有 3 个操作系统上测试我的应用,但到目前为止我没有遇到任何问题。无论如何,Web开发人员已经习惯了这种情况,因为我们长期以来一直在构建多浏览器应用程序。打包工具和兼容性填充也在这方面提供了很大帮助!


3. 插件


我之前简要提到过这一点,但我认为它值得更详细地讨论,因为在我看来,这是 Tauri 最好的特性之一。插件是由 Rust 编写的一组命令集,可以从 JavaScript 中调用。它允许开发人员通过组合不同的插件来构建应用程序,这些插件可以是开源的,也可以在您的应用程序中定义。


这是一种很好的应用程序组织结构的方式,它也使得在不同应用程序之间共享代码变得容易!


在 Tauri 社区中,您会找到一些插件的示例:



这些特性本来可能可以成为 Tauri 本身的一部分,但将它们单独分开意味着您可以挑选和选择您想要使用的功能。这也意味着它们可以独立演变,并且如果有更好的替代品发布,可以被替换。


插件系统是我选择 Tauri 的第二大原因;它让开发者的体验提升了 1000 倍!


4. 功能对比


就功能而言,Electron 和 Tauri 非常相似。Electron 仍然具有一些更多的功能,但 Tauri 正在迅速赶上。至少对于我的使用情况来说,Tauri 具有我所需要的一切。


唯一给我带来较大不便的是缺乏一个“本地上下文菜单”API。这是社区强烈要求的功能,它将使 Tauri 应用程序感觉更加本地化。我目前是用 JS/HTML/CSS 来实现这一点,虽然可以,但还有提升的空间。希望我们能在 Tauri 2 中看到这个功能的实现 🤞


但除此之外,Tauri 还有很多功能。开箱即用,您可以得到通知、状态栏、菜单、对话框、文件系统、网络、窗口管理、自动更新、打包、代码签名、GitHub actions、辅助组件等。如果您需要其他功能,您可以编写一个插件,或者使用现有的插件之一。


5. 移动端


这个消息让我感到惊讶。在我撰写这篇文章时,Tauri 已经实验性地支持 iOS 和 Android。似乎这一直是计划的一部分,但当我开始我的应用程序时并不知道这一点。我不确定自己是否会使用它,但知道它存在感到很不错。


这是 Electron 所不可能实现的,并且可能永远也不会。因此,如果您计划构建跨平台的移动和桌面应用程序,Tauri 可能是一种不错的选择,因为您可能能够在它们之间共享很多代码。利用网络技术设计移动优先界面多年来变得越来越容易,因此构建一个既可以作为桌面应用程序又可以作为移动应用程序运行的单一界面并不像听起来那么疯狂。


我只是想提一句,让大家对 Tauri 的未来感到兴奋。


Pasted image 20240517144955.png
watchOS 上的 Tauri 程序?🤯


正如 Jonas 在他的推文中所提到的,这只是实验性的和折衷的;它可能需要很长时间才能达到生产状态,但看到这个领域的创新仍然非常令人兴奋!


结论


我对选择使用 Tauri 感到非常满意。结合 Solid.js,我能够制作出一个真正快速的应用程序,人们喜欢它!我不是说它总是比 Electron 好,但如果它具有您需要的功能,我建议尝试一下!如前所述,您甚至可能不需要写那么多 Rust 代码,所以不要被吓倒!您会惊讶地发现,只用 JavaScript 就能做的事情有多少。


如果你对 Kubernetes 感兴趣,请查看 Aptakube,这是一个使用 Tauri 构建的 Kubernetes 桌面客户端 😊


我现在正在开发一个面向桌面和移动应用的开源且注重隐私的分析平台。它已经具有各种框架的 SDK,包括 Tauri 和 Electron。顺便说一句,Tauri SDK 被打包为一个 Tauri 插件! 😄


最后,我也活跃在 Twitter 上。如果您有任何问题或反馈,请随时联系我。我喜欢谈论 Tauri!


感谢阅读!👋


作者:qwei
来源:juejin.cn/post/7386115583845744649
收起阅读 »

最新Cursor无限续杯避坑指南,让你稳稳的喝咖啡~

2月份写了篇cursor无限续杯的文章,文章数据对我来说相当完美,看来大家对于不花钱这事比较感兴趣🤣介于有些同学反应说自定义域名有一定的门槛,上手不太容易,那么今天新的方案它来了!此方案针对频繁被提示试用过期,too many free accounts的场景...
继续阅读 »

2月份写了篇cursor无限续杯的文章,文章数据对我来说相当完美,看来大家对于不花钱这事比较感兴趣🤣

介于有些同学反应说自定义域名有一定的门槛,上手不太容易,那么今天新的方案它来了!此方案针对频繁被提示试用过期,too many free accounts的场景。

首先要申明一下:

  1. 无限邮你就别想了,请放弃!!
  2. cursor请稳定在 <= 0.46.11

废话不多说,进入正文吧 (查看原文体验更佳,有惊喜~)

Step1: 破解软件下载

✨方案使用的是开源软件cursor-help进行cursor重置

👉mac/linux 请使用go-cursor-help 进行操作

  • 下载cursor_bypass.exe (红框中的文件,不能科学上网的,下面有网盘链接)


🎈如果打不开链接,可以使用下面的网盘链接下载以上文件

百度

夸克

Step2: cursor退出账号

已退出账号直接跳过该步骤~

Step3: 运行软件

管理员****身份运行必须,不然点击会没反应) Cursor Bypass.exe

依次点击:

操作完会弹出网页,不用管它~

Step4:登录你之前注册的账号

浏览器打开cursor进行登录:

完成登录后,然后页面点击右上角头像,点击账号设置

然后左下角点开Advanced,找到delete account,点击它

Step5: 删除账号

输入Delete,点击删除按钮,删除账号

如果出现 Failed to fetch(cursor服务器网络波动),刷新页面,重试~

Step6:恢复之前的账号

浏览器打开cursor注册页面,使用之前cursor账号那个邮箱再重新注册一遍~

当然了,这里也可以注册新账号(不要用无限邮 

输入信息,完成注册~

Step7: 使用cursor软件进行登录

建议将chrome设置为默认浏览器chrome浏览器改成默认浏览器),这样登录会很顺利(同时需要退出360安全卫士这种垃圾软件,它会拦截登录,有点恶心)

💻点这里可以离线下载chrome浏览器

在弹出的页面中完成登录,登录成功是下面的状态

然后回到cursor,状态如下就登录成功了

注意:如果这一步失败,可能360安全卫士这类垃圾软件在搞怪(会拦截登录过程),建议退出360重试

Step8: 验证是否可以试用

看刚刚的cursor网页,如下状态,就可以了

cursor软件-账户信息再看一眼,这样就没问题了

验证提问 CTRL + L,能正常响应即可~

测试代码tab功能

试用版账号需要注意的点

试用账户:

  1. max模型只有pro正式会员可用,试用账号不可用!!
  2. tab补全是2000次
  3. 聊天只有50次,虽然显示了150(达到50即无效,此时按文档重新来一遍即可)

  1. Tinking打开后,可能出现error

需要关掉Thinking,重试

关掉Thinking即可提问(除非达到50次上限)

image.png

更多信息,请移步原文~


作者:jerrywus
来源:juejin.cn/post/7486323379474563107
收起阅读 »

前端の骚操作代码合集 | 让你的网页充满恶趣味

web
1️⃣ 永远点不到的幽灵按钮 效果描述:按钮会跟随鼠标指针,但始终保持微妙距离 <button id="ghostBtn" style="position:absolute">点我试试?</button> <script> ...
继续阅读 »

1️⃣ 永远点不到的幽灵按钮


效果描述:按钮会跟随鼠标指针,但始终保持微妙距离


<button id="ghostBtn" style="position:absolute">点我试试?</button>
<script>
const btn = document.getElementById('ghostBtn');
document.addEventListener('mousemove', (e) => {
btn.style.left = `${e.clientX + 15}px`;
btn.style.top = `${e.clientY + 15}px`;
});
</script>



Desktop2025.03.04-12.25.56.17-ezgif.com-video-to-gif-converter.gif


2️⃣ 极简黑客帝国数字雨


代码亮点:仅用 20 行代码实现经典效果


<canvas id="matrix"></canvas>
<script>
const canvas = document.getElementById('matrix');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const chars = '01';
const drops = Array(Math.floor(canvas.width/20)).fill(0);

function draw() {
ctx.fillStyle = 'rgba(0,0,0,0.05)';
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = '#0F0';
drops.forEach((drop, i) => {
ctx.fillText(chars[Math.random()>0.5?0:1], i*20, drop);
drops[i] = drop > canvas.height ? 0 : drop + 20;
});
}
setInterval(draw, 100);
</script>


运行建议:按下 F11 进入全屏模式效果更佳
Desktop2025.03.04-12.28.02.18-ezgif.com-video-to-gif-converter.gif




下面是优化版:


<script>
const canvas = document.getElementById('matrix');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

const chars = '01'; // 显示的字符
const columns = Math.floor(canvas.width / 20); // 列数
const drops = Array(columns).fill(0); // 每列的起始位置
const speeds = Array(columns).fill(0).map(() => Math.random() * 10 + 5); // 每列的下落速度

function draw() {
// 设置背景颜色并覆盖整个画布,制造渐隐效果
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
ctx.fillRect(0, 0, canvas.width, canvas.height);

// 设置字符颜色
ctx.fillStyle = '#0F0'; // 绿色
ctx.font = '20px monospace'; // 设置字体

// 遍历每一列
drops.forEach((drop, i) => {
// 随机选择一个字符
const char = chars[Math.random() > 0.5 ? 0 : 1];
// 绘制字符
ctx.fillText(char, i * 20, drop);
// 更新下落位置
drops[i] += speeds[i];
// 如果超出画布高度,重置位置
if (drops[i] > canvas.height) {
drops[i] = 0;
speeds[i] = Math.random() * 10 + 5; // 重置速度
}
});
}

// 每隔100毫秒调用一次draw函数
setInterval(draw, 100);
</script>

3️⃣ 元素融化动画


交互效果:点击元素后触发扭曲消失动画


<div onclick="melt(this)" 
style="cursor:pointer; padding:20px; background:#ff6666;">

点我融化!
</div>

<script>
function melt(element) {
let pos = 0;
const meltInterval = setInterval(() => {
element.style.borderRadius = `${pos}px`;
element.style.transform = `skew(${pos}deg) scale(${1 - pos/100})`;
element.style.opacity = 1 - pos/100;
pos += 2;
if(pos > 100) clearInterval(meltInterval);
}, 50);
}
</script>



Desktop 2025.03.04 - 12.28.43.19.gif


4️⃣ 控制台藏宝图


彩蛋效果:在开发者工具中埋入神秘信息


console.log('%c🔮 你发现了秘密通道!', 
'font-size:24px; color:#ff69b4; text-shadow: 2px 2px #000');
console.log('%c输入咒语 %c"芝麻开门()" %c获得力量',
'color:#666', 'color:#0f0; font-weight:bold', 'color:#666');
console.debug('%c⚡ 警告:前方高能反应!',
'background:#000; color:#ff0; padding:5px;');



5️⃣ 重力反转页面


魔性交互:让页面滚动方向完全颠倒


window.addEventListener('wheel', (e) => {
e.preventDefault();
window.scrollBy(-e.deltaX, -e.deltaY);
}, { passive: false });

慎用警告:此功能可能导致用户怀疑人生 ( ̄▽ ̄)"




6️⃣ 实时 ASCII 摄像头


技术亮点:将摄像头画面转为字符艺术


<pre id="asciiCam" style="font-size:8px; line-height:8px;"></pre>
<script>
navigator.mediaDevices.getUserMedia({ video: true })
.then(stream => {
const video = document.createElement('video');
video.srcObject = stream;
video.play();

const chars = '@%#*+=-:. ';
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

video.onplaying = () => {
canvas.width = 80;
canvas.height = 40;

setInterval(() => {
ctx.drawImage(video, 0, 0, 80, 40);
const imgData = ctx.getImageData(0,0,80,40).data;
let ascii = '';

for(let i=0; i<imgData.length; i+=4) {
const brightness = (imgData[i]+imgData[i+1]+imgData[i+2])/3;
ascii += chars[Math.floor(brightness/25.5)]
+ (i%(80*4) === (80*4-4) ? '\n' : '');
}

document.getElementById('asciiCam').textContent = ascii;
}, 100);
};
});
</script>



⚠️ 使用注意事项



  1. 摄像头功能需 HTTPS 环境或 localhost 才能正常工作

  2. 反向滚动代码可能影响用户体验,建议仅在整蛊场景使用

  3. 数字雨效果会持续消耗 GPU 资源

  4. 控制台彩蛋要确保不会暴露敏感信息




这些代码就像前端的"复活节彩蛋",适度使用能让网站充满趣味性,但千万别用在生产环境哦!(≧∇≦)ノ


https://codepen.io/  链接 CodePen)
希望这篇博客能成为程序员的快乐源泉!🎉

作者:一天睡25小时
来源:juejin.cn/post/7477573759254675507
收起阅读 »

别再追逐全新框架了,先打好基础再说......

web
Hello,大家好,我是 Sunday 如果大家做过一段时间的前端开发,就会发现相比其他的技术圈而言,前端圈总显得 “乱” 的很。 因为,每隔几个月,圈里就会冒出一个“闪闪发光的全新 JavaScript 框架”,声称能解决你所有问题,并提供各种数据来证明它拥...
继续阅读 »

Hello,大家好,我是 Sunday


如果大家做过一段时间的前端开发,就会发现相比其他的技术圈而言,前端圈总显得 “乱” 的很。


因为,每隔几个月,圈里就会冒出一个“闪闪发光的全新 JavaScript 框架”,声称能解决你所有问题,并提供各种数据来证明它拥有:更快的性能!更简洁的语法!更多的牛批特性!


而对应的,很多同学都会开始 “追逐” 这些全新的框架,并大多数情况下都会得出一个统一的评论 “好牛批......”


但是,根据我的经验来看,通常情况下 过于追逐全新的框架,毫无意义。 特别是对于 前端初学者 而言,打好基础会更加的重要!



PS:我这并不是在反对新框架的创新,出现更多全新的框架,全新的创新方案肯定是好的。但是,我们需要搞清楚一点,这一个所谓的全新框架 究竟是创新,还是只是通过一个不同的方式,重复的造轮子?



全新的框架是追逐不完的


我们回忆一下,是不是很多所谓的全新框架,总是按照以下的方式在不断的轮回?



  • 首先,网上出现了某个“全新 JS 框架”发布,并提供了:更小、更快、更优雅 的方案,从而吸引了大量关注

  • 然后,很多技术人开始追捧,从 掘金、抖音、B 站 开始纷纷上线各种 “教程”

  • 再然后,几乎就没有然后了。国内大厂不会轻易使用这种新的框架作为生产工具,因为大厂会更加看重框架的稳定性

  • 最后,无非会出现两种结果,第一种就是:热度逐渐消退,最后停止维护。第二种就是:不断的适配何种业务场景,直到这种全新的框架也开始变得“臃肿不堪”,和它当年要打败的框架几乎一模一样。

  • 重新开始轮回:另一个“热门”框架出现,整个循环再次启动。


Svelte 火了那么久,大家有见到过国内有多少公司在使用吗?可能有很多同学会说“国外有很多公司在使用 Svelte 呀?” 就算如此,它对比 Vue 、React、Angular(国外使用的不少) 市场占有率依然是寥寥无几的。并且大多数同学的主战场还不在国外。


很多框架只是语法层面发生了变化


咱们以一个 “点击计数” 的功能为例,分别来看下在 Vue、React、Svelte 三个框架中的实现(别问为啥没有 angular,问就是不会😂)


Vue3 实现


<template>
<button @click="count++">点击了 {{ count }} 次</button>
</template>

<script setup>
import { ref } from 'vue';

const count = ref(0);
</script>

React 实现


import { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);

return (
<button onClick={() => setCount(count + 1)}>
点击了 {count} 次
</button>

);
}

export default Counter;

Svelte 实现


<script>
let count = 0;
</script>

<button on:click={() => count += 1}>
点击了 {count} 次
</button>

这三个版本的核心逻辑完全一样,只是语法不同。


那么这就意味着:如果换框架,都要重新学习这些新的语法细节(哪里要写冒号、哪里要写大括号、哪里要写中括号)。


如果你把时间都浪费着这些地方上(特别是前端初学者),是不是就意味着 毫无意义,浪费时间呢?


掌握好基础才是王道


如果我们去看了大量的 国内大厂的招聘面经之后,就会发现,无论是 校招 || 社招,大厂的考察重点 永远不在框架,而在于 JS 基础、网络、算法、项目 这四个部分。至于你会的是 vue || react 并没有那么重要!



PS:对于大厂来说 vue 和 react 都有不同的团队在使用。所以不用担心你学的框架是什么,这并不影响你进大厂



因此,掌握好基础就显得太重要了


所以说:不用过于追逐新的技术框架


针对于 在校生 而言 打好基础,练习算法,多去做更多有价值的项目,研究框架底层源码 ,并且一定要注意 练习表达,这才是对你将来校招最重要的事情!


而对于 社招的同学 而言 多去挖掘项目的重难点,尝试通过 输出知识的方式 帮助你更好的梳理技术。多去思考 技术如何解决业务问题,这才是关键!


作者:程序员Sunday
来源:juejin.cn/post/7484960608782336027
收起阅读 »

让你辛辛苦苦选好的筛选条件刷新页面后不丢失,该怎么做?

web
你有遇到过同样的需求吗?告诉我你的第一想法。存 Session storage ?可以,但是我更建议你使用 router.replace 。 为什么建议使用 router.replace 而不是浏览器自带的存储空间呢? 增加实用性,你有没有考虑过这种场景,也就...
继续阅读 »

你有遇到过同样的需求吗?告诉我你的第一想法。存 Session storage ?可以,但是我更建议你使用 router.replace


为什么建议使用 router.replace 而不是浏览器自带的存储空间呢?


增加实用性,你有没有考虑过这种场景,也就是当我筛选好之后进行搜索,我需要将它发给我的同事。当使用storage时是实现不了的,同事只会得到一个初始的页面。那我们将这个筛选结果放入url中是不是就可以解决这个问题了。


router.replace


先给大家介绍一下 router.replace 的用法吧。


router.replace 是 Vue Router 提供的一个方法,用于替换当前的历史记录条目。与 router.push 不同的是,replace 不会在浏览器历史记录中添加新记录,而是替换当前的记录。这对于需要在 URL 中保存状态但不想影响浏览器导航历史的场景非常有用。


// 假设我们正在使用 Vue 2 和 Vue Router
methods: {
updateFilters(newFilters) {
// 将筛选条件编码为查询字符串参数
const query = {
...this.$route.query,
...newFilters,
};

// 使用 router.replace 更新 URL
this.$router.replace({ query });
}
}

在这个示例中,updateFilters 方法接收新的筛选条件,并将它们合并到当前的查询参数中。然后使用 router.replace 更新 URL,而不会在历史记录中添加新条目。


具体实现


将筛选条件转换为适合 URL 的格式,例如 JSON 字符串或简单的键值对。以下是一个更详细的实现:


methods: {
applyFilters(filters) {
const encodedFilters = JSON.stringify(filters);

this.$router.replace({
path: this.$route.path,
query: { ...this.$route.query, filters: encodedFilters },
});
},

getFiltersFromUrl() {
const filters = this.$route.query.filters;
return filters ? JSON.parse(filters) : {};
}
}

在这个实现中,applyFilters 方法将筛选条件编码为 JSON 字符串,并将其存储在 URL 的查询参数中。getFiltersFromUrl 方法用于从 URL 中读取筛选条件,并将其解析回 JavaScript 对象。


注意事项



  • 编码和解码:在将复杂对象存储到 URL 时,确保使用 encodeURIComponent 和 decodeURIComponent 来处理特殊字符。

  • URL 长度限制:浏览器对 URL 长度有一定的限制,确保不要在 URL 中存储过多数据。

  • 数据安全性:考虑 URL 中数据的敏感性,避免在 URL 中存储敏感信息。

  • url重置:不要忘了在筛选条件重置时也将 url 重置,在取消筛选时同时去除 url 上的筛选。


一些其他的应用场景



  1. 重定向用户



    • 当用户访问一个不再存在或不推荐使用的旧路径时,可以使用 router.replace 将他们重定向到新的路径。这避免了用户点击“返回”按钮时再次回到旧路径。



  2. 处理表单提交后清理 URL



    • 在表单提交后,可能会在 URL 中附加查询参数。使用 router.replace 可以在处理完表单数据后清理这些参数,保持 URL 的整洁。



  3. 登录后跳转



    • 在用户登录后,将他们重定向到一个特定的页面(如用户主页或仪表盘),并且不希望他们通过“返回”按钮回到登录页面。使用 router.replace 可以实现这一点。



  4. 错误页面处理



    • 当用户导航到一个不存在的页面时,可以使用 router.replace 将他们重定向到一个错误页面(如 404 页面),并且不希望这个错误路径保留在浏览历史中。



  5. 动态内容加载



    • 在需要根据用户操作动态加载内容时,使用 router.replace 更新 URL,而不希望用户通过“返回”按钮回到之前的状态。例如,在单页应用中根据选项卡切换更新 URL。



  6. 多步骤流程



    • 在多步骤的用户流程中(如注册或购买流程),使用 router.replace 可以在用户完成每一步时更新 URL,而不希望用户通过“返回”按钮回到上一步。



  7. 清理查询参数



    • 在用户操作完成后,使用 router.replace 清理不再需要的查询参数,保持 URL 简洁且易于阅读。




小结



简单来说就是把你的 url 当成浏览器的 sessionstorage 了。其实这就是我上周收到的任务,当时我甚至纠结的是该用localStorage还是sessionStorage,忙活半天,不停转类型,然后在开周会我讲了下我的思路。我的tl便说出了我的问题,讲了更加详细的需求,我才开始尝试 router.replace ,又是一顿忙活。。



作者:一颗苹果OMG
来源:juejin.cn/post/7424034641379098663
收起阅读 »

做了个渐变边框的input输入框,领导和客户很满意!

web
需求简介 前几天需求评审的时候,产品说客户希望输入框能够好看一点。由于我们UI框架用的是Elemnt Plus,input输入框的样式也比较中规中矩,所以我们组长准备拒绝这个需求 但是,喜欢花里胡哨的我立马接下了这个需求!我自信的告诉组长,放着我来,包满意! ...
继续阅读 »

需求简介


前几天需求评审的时候,产品说客户希望输入框能够好看一点。由于我们UI框架用的是Elemnt Plus,input输入框的样式也比较中规中矩,所以我们组长准备拒绝这个需求


但是,喜欢花里胡哨的我立马接下了这个需求!我自信的告诉组长,放着我来,包满意!


经过一番折腾,我通过 CSS 的技巧实现了一个带有渐变边框的 Input 输入框,而且当鼠标悬浮在上面时,边框颜色要更加炫酷并加深渐变效果。



最后,领导和客户对最后的效果都非常满意~我也成功获得了老板给我画的大饼,很开心!


下面就来分享我的实现过程和代码方案,满足类似需求的同学可以直接拿去用!


实现思路


实现渐变边框的原理其实很简单,首先实现一个渐变的背景作为底板,然后在这个底板上加上一个纯色背景就好了。



当然,我们在实际写代码的时候,不用专门写两个div来这么做,利用css的 background-clip 就可以实现上面的效果。



background-clip 属性详解


background-clip 是一个用于控制背景(background)绘制范围的 CSS 属性。它决定了背景是绘制在 内容区域内边距区域、还是 边框区域


background-clip: border-box | padding-box | content-box | text;


代码实现


背景渐变


<template>
<div class="input-container">
<input type="text" placeholder="请输入内容" class="gradient-input" />
</div>

</template>

<script setup>
</script>


<style>
/* 输入框容器 */
.input-container {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
width: 100vw;
background: #f4f4f9;
}

/* 渐变边框输入框 */
.gradient-input {
width: 400px;
height: 40px;
padding: 5px 12px;
font-size: 16px;
color: #333;
outline: none;
/* 渐变边框 */
border: 1px solid transparent;
background: linear-gradient(white, white) padding-box,
linear-gradient(45deg, #ff7eb3, #65d9ff, #c7f464, #ff7eb3) border-box;
border-radius: 20px;
}

/* Placeholder 样式 */
.gradient-input::placeholder {
color: #aaa;
font-style: italic;
}

</style>



通过上面的css方法,我们成功实现了一个带有渐变效果边框的 Input 输入框,它的核心代码是


background: linear-gradient(white, white) padding-box, 
linear-gradient(45deg, #ff7eb3, #65d9ff, #c7f464, #ff7eb3) border-box;

padding-box:限制背景在内容区域显示,防止覆盖输入框内容。


border-box:渐变背景会显示在边框位置,形成渐变边框效果。


这段代码分为两层背景:



  1. 第一层背景

    linear-gradient(white, white) 是一个纯白色的线性渐变,用于覆盖输入框的内容区域(padding-box)。

  2. 第二层背景

    linear-gradient(45deg, #ff7eb3, #65d9ff, #c7f464, #ff7eb3) 是一个多色的渐变,用于显示在输入框的边框位置(border-box)。


背景叠加后,最终效果是:内层内容是白色背景,边框区域显示渐变颜色。


Hover 效果


借助上面的思路,我们在添加一些hover后css样式,通过 :hover 状态改变渐变的颜色和 box-shadow 的炫光效果:


/* Hover 状态 */
.gradient-input:hover {
background: linear-gradient(white, white) padding-box,
linear-gradient(135deg, #ff0076, #1eaeff, #28ffbf, #ff0076) border-box;
box-shadow: 0 0 5px rgba(255, 0, 118, 0.5), 0 0 20px rgba(30, 174, 255, 0.5);
}


过渡似乎有点生硬,没关系,加个过渡样式


/* 渐变边框输入框 */
.gradient-input {
// .....

/* 平滑过渡 */
transition: background 0.3s ease, box-shadow 0.3s ease;
}

非常好看流畅~



激活样式


最后,我们再添加一个激活的Focus 状态:当用户聚焦输入框时,渐变变得更加灵动,加入额外的光晕。


/* Focus 状态 */
.gradient-input:focus {
background: linear-gradient(white, white) padding-box,
linear-gradient(45deg, #ff0076, #1eaeff, #28ffbf, #ff0076) border-box;
box-shadow: 0 0 15px rgba(255, 0, 118, 0.7), 0 0 25px rgba(30, 174, 255, 0.7);
color: #000; /* 聚焦时文本颜色 */
}

现在,我们就实现了一个渐变边框的输入框,是不是非常好看?



完整代码


<template>
<div class="input-container">
<input type="text" placeholder="请输入内容" class="gradient-input" />
</div>
</template>

<style>
/* 输入框容器 */
.input-container {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
width: 100vw;
background: #f4f4f9;
}

/* 渐变边框输入框 */
.gradient-input {
width: 400px;
height: 40px;
padding: 5px 12px;
font-size: 16px;
font-family: 'Arial', sans-serif;
color: #333;
outline: none;

/* 渐变边框 */
border: 1px solid transparent;
background: linear-gradient(white, white) padding-box,
linear-gradient(45deg, #ff7eb3, #65d9ff, #c7f464, #ff7eb3) border-box;
border-radius: 20px;

/* 平滑过渡 */
transition: background 0.3s ease, box-shadow 0.3s ease;
}
/*

/* Hover 状态 */

.gradient-input:hover {
background: linear-gradient(white, white) padding-box,
linear-gradient(135deg, #ff0076, #1eaeff, #28ffbf, #ff0076) border-box;
box-shadow: 0 0 5px rgba(255, 0, 118, 0.5), 0 0 20px rgba(30, 174, 255, 0.5);
}

/* Focus 状态 */
.gradient-input:focus {
background: linear-gradient(white, white) padding-box,
linear-gradient(45deg, #ff0076, #1eaeff, #28ffbf, #ff0076) border-box;
box-shadow: 0 0 15px rgba(255, 0, 118, 0.7), 0 0 25px rgba(30, 174, 255, 0.7);
color: #000; /* 聚焦时文本颜色 */
}

/* Placeholder 样式 */
.gradient-input::placeholder {
color: #aaa;
font-style: italic;
}

</style>

总结


通过上述方法,我们成功实现了一个带有渐变效果边框的 Input 输入框,并且在 Hover 和 Focus 状态下增强了炫彩效果。


大家可以根据自己的需求调整渐变的方向、颜色或动画效果,让你的输入框与众不同!


作者:快乐就是哈哈哈
来源:juejin.cn/post/7442216034751545394
收起阅读 »

几行代码,优雅的避免接口重复请求!同事都说好!

web
背景简介 我们日常开发中,经常会遇到点击一个按钮或者进行搜索时,请求接口的需求。 如果我们不做优化,连续点击按钮或者进行搜索,接口会重复请求。 首先,这会导致性能浪费!最重要的,如果接口响应比较慢,此时,我们在做其他操作会有一系列bug! 那么,我们该如...
继续阅读 »

背景简介


我们日常开发中,经常会遇到点击一个按钮或者进行搜索时,请求接口的需求。


如果我们不做优化,连续点击按钮或者进行搜索,接口会重复请求。




首先,这会导致性能浪费!最重要的,如果接口响应比较慢,此时,我们在做其他操作会有一系列bug!



那么,我们该如何规避这种问题呢?


如何避免接口重复请求


防抖节流方式(不推荐)


使用防抖节流方式避免重复操作是前端的老传统了,不多介绍了


防抖实现


<template>
<div>
<button @click="debouncedFetchData">请求</button>
</div>

</template>

<script setup>
import { ref } from 'vue';
import axios from 'axios';

const timeoutId = ref(null);

function debounce(fn, delay) {
return function(...args) {
if (timeoutId.value) clearTimeout(timeoutId.value);
timeoutId.value = setTimeout(() => {
fn(...args);
}, delay);
};
}

function fetchData() {
axios.get('http://api/gcshi) // 使用示例API
.then(response => {
console.log(response.data);
})
}

const debouncedFetchData = debounce(fetchData, 300);
</script>

防抖(Debounce)



  • 在setup函数中,定义了timeoutId用于存储定时器ID。

  • debounce函数创建了一个闭包,清除之前的定时器并设置新的定时器,只有在延迟时间内没有新调用时才执行fetchData。

  • debouncedFetchData是防抖后的函数,在按钮点击时调用。


节流实现


<template>
<div>
<button @click="throttledFetchData">请求</button>
</div>

</template>

<script setup>
import { ref } from 'vue';
import axios from 'axios';

const lastCall = ref(0);

function throttle(fn, delay) {
return function(...args) {
const now = new Date().getTime();
if (now - lastCall.value < delay) return;
lastCall.value = now;
fn(...args);
};
}

function fetchData() {
axios.get('http://api/gcshi') //
.then(response => {
console.log(response.data);
})
}

const throttledFetchData = throttle(fetchData, 1000);
</script>

节流(Throttle)



  • 在setup函数中,定义了lastCall用于存储上次调用的时间戳。

  • throttle函数创建了一个闭包,检查当前时间与上次调用时间的差值,只有大于设定的延迟时间时才执行fetchData。

  • throttledFetchData是节流后的函数,在按钮点击时调用。


节流防抖这种方式感觉用在这里不是很丝滑,代码成本也比较高,因此,很不推荐!


请求锁定(加laoding状态)


请求锁定非常好理解,设置一个laoding状态,如果第一个接口处于laoding中,那么,我们不执行任何逻辑!


<template>
<div>
<button @click="fetchData">请求</button>
</div>

</template>

<script setup>
import { ref } from 'vue';
import axios from 'axios';

const laoding = ref(false);

function fetchData() {
// 接口请求中,直接返回,避免重复请求
if(laoding.value) return
laoding.value = true
axios.get('http://api/gcshi') //
.then(response => {
laoding.value = fasle
})
}

const throttledFetchData = throttle(fetchData, 1000);
</script>


这种方式简单粗暴,十分好用!


但是也有弊端,比如我搜索A后,接口请求中;但我此时突然想搜B,就不会生效了,因为请求A还没响应



因此,请求锁定这种方式无法取消原先的请求,只能等待一个请求执行完才能继续请求。


axios.CancelToken取消重复请求


基本用法


axios其实内置了一个取消重复请求的方法:axios.CancelToken,我们可以利用axios.CancelToken来取消重复的请求,爆好用!


首先,我们要知道,aixos有一个config的配置项,取消请求就是在这里面配置的。


<template>
<div>
<button @click="fetchData">请求</button>
</div>

</template>

<script setup>
import { ref } from 'vue';
import axios from 'axios';

let cancelTokenSource = null;


function fetchData() {
if (cancelTokenSource) {
cancelTokenSource.cancel('取消上次请求');
cancelTokenSource = null;
}
cancelTokenSource = axios.CancelToken.source();

axios.get('http://api/gcshi',{cancelToken: cancelTokenSource.token}) //
.then(response => {
laoding.value = fasle
})
}

</script>


我们测试下,如下图:可以看到,重复的请求会直接被终止掉!



CancelToken官网示例



官网使用方法传送门:http://www.axios-http.cn/docs/cancel…



const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});

axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})

// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');

也可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建一个 cancel token:


const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});

// 取消请求
cancel();

注意: 可以使用同一个 cancel token 或 signal 取消多个请求。


在过渡期间,您可以使用这两种取消 API,即使是针对同一个请求:


const controller = new AbortController();

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
cancelToken: source.token,
signal: controller.signal
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});

axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})

// 取消请求 (message 参数是可选的)
source.cancel('Operation canceled by the user.');
// 或
controller.abort(); // 不支持 message 参数

作者:石小石Orz
来源:juejin.cn/post/7380185173689204746
收起阅读 »

最近 React Scan 太火了,做了个 Vue Scan

web
在 React Scan 的 github 有这么一张 gif 图片。当用户在页面上操作时,对应的组件会有一个闪烁,表示当前组件更新了。用这样的方式来排查程序的性能是一个很直观的方式。 根据 React Scan 自己的介绍,React Scan 可以 通过...
继续阅读 »

在 React Scan 的 github 有这么一张 gif 图片。当用户在页面上操作时,对应的组件会有一个闪烁,表示当前组件更新了。用这样的方式来排查程序的性能是一个很直观的方式。


React Scan


根据 React Scan 自己的介绍,React Scan 可以 通过自动检测和突出显示导致性能问题的渲染


Vue Scan


但是我主要使用 vue 来开发我的应用,看到这个功能非常眼馋,所以就动手自己做了一个 demo,目前也构建了一个 chrome 扩展,不过扩展仅支持识别 vue3 项目 现在已经支持 vue2 和 vue3 项目了。


项目地址:Vue Scan


简单介绍,Vue Scan 通过组件的 onBeforUpdate 钩子,当组件更新时,在组件对应位置绘制一个闪烁的边框。看起来的效果就像这样。


image.png


用法


我更推荐在开发环境使用它,Vue Scan 提供一个 vue plugin,允许你在 mount 之前注入相关的内容。


// vue3
import { createApp } from 'vue'
import VueScan, { type VueScanOptions } from 'z-vue-scan/src'

import App from './App.vue'

const app = createApp(App)
app.use<VueScanOptions>(VueScan, {})
app.mount('#app')

// vue2
import Vue from 'vue'
import VueScan, { type VueScanBaseOptions } from 'z-vue-scan/vue2'
import App from './App.vue'

Vue.use<VueScanBaseOptions>(VueScan, {})

new Vue({
render: h => h(App),
}).$mount('#app')

浏览器扩展


如果你觉得看自己的网站没什么意思,那么我还构建了一个浏览器扩展,允许你注入相关方法到别人的 vue 程序中。


你可以在 Github Release 寻找一下最新版的安装包,然后解压安装到浏览器中。


安装完成后,你的扩展区域应该会多一个图标,点击之后会展开一个面板,允许你控制是否注入相关的内容。


image.png


这是如果你进入一个使用 vue 构建的网站,可以看控制台看到相关的信息,当你在页面交互时,页面应该也有相应的展示。


image.png


缺陷


就像 React Scan 的介绍中提到的,它能自动识别性能问题,单目前 Vue Scan 只是真实地反映组件的更新,并不会区分和识别此次更新是否有性能问题。


结语


通过观察网站交互时组件的更新状态,来尝试发现网站的性能问题,我觉得这是一个很好的方式。希望这个工具可以给大家带来一点乐趣和帮助。


作者:huali
来源:juejin.cn/post/7444449353165488168
收起阅读 »

Electron 启动白屏解决方案

web
对于 Web 开发者使用 Electron 构建桌面应用程序时,经常会遇到如上图所示的一个问题 —— 窗口加载过程中长时间白屏。在应用窗口创建完成到页面加载出来的这段时间里,出现了长时间的白屏,这个问题对于前端开发来说是一个老生常谈的问题,纯 Web...
继续阅读 »

对于 Web 开发者使用 Electron 构建桌面应用程序时,经常会遇到如上图所示的一个问题 —— 窗口加载过程中长时间白屏。在应用窗口创建完成到页面加载出来的这段时间里,出现了长时间的白屏,这个问题对于前端开发来说是一个老生常谈的问题,纯 Web 端可能就是异步加载、静态资源压缩、CDN 以及骨架屏等等优化方案,但是如果是开发 Electron 应用,场景又有些许不同,因此我们也不能完全按照通用的前端解决白屏的方案进行处理,本文就来探索基于 Electron 场景下启动白屏的解决方案。

问题原因分析

1. Electron 主进程加载时间过长

Electron 应用在启动时,需要先加载主进程,然后由主进程去创建浏览器窗口和加载页面。如果主进程加载时间过长,就会导致应用一直停留在空白窗口,出现白屏。

主进程加载时间长的原因可以有:

  • 初始化逻辑复杂,比如加载大量数据、执行计算任务等
  • 主进程依赖的模块加载时间长,例如 Native 模块编译耗时
  • 主进程代码进行了大量同步 I/O 操作,阻塞了事件循环

2. Web 部分性能优化不足

浏览器窗口加载 HTML、JavaScript、CSS 等静态资源是一个渐进的过程,如果资源体积过大,加载时间过长,在加载过程中就会短暂出现白屏,这一点其实就是我们常说的前端首屏加载时间过长的问题。导致 Web 加载时间过长的原因可以是:

  • 页面体积大,如加载过多图片、视频等大资源
  • 没有代码拆分,一次加载全部 Bundles
  • 缺乏缓存机制,资源无法命中缓存
  • 主线程运算量大,频繁阻塞渲染

解决方案

1. 常规 Web 端性能优化

Web 端加载渲染过程中的白屏,可以采用常规前端的性能优化手段:

  1. 代码拆分,异步加载,避免大包导致的加载时间过长
  2. 静态资源压缩合并、CDN 加速,减少资源加载时间
  3. 使用骨架屏技术,先提供页面骨架,优化用户体验
  4. 减少主线程工作量,比如使用 Web Worker 进行复杂计算
  5. 避免频繁布局重排,优化 DOM 操作

以上优化可以明显减少 HTML 和资源加载渲染的时,缩短白屏现象。还是那句话,纯 Web 端的性能优化对于前端开发来说老生常谈,我这边不做详细的赘述,不提供实际代码,开发者可以参考其他大佬写的性能优化文章,本文主要针对的是 Electron 启动白屏过长的问题,因为体验下来 Electron 白屏的本质问题还是要通过 Electron 自身来解决~

2. 控制 Electron 主进程加载时机

Electron 启动长时间白屏的本质原因,前面特意强调了,解决方案还是得看 Electron 自身的加载时机,因为我这边将 Web 部分的代码打包启动,白屏时间是非常短的,与上面动图里肉眼可见的白屏时间形成了鲜明的对比。所以为了解决这个问题,我们还是要探寻 Electron 的加载时机,通过对 Electron 的启动流程分析,我们发现:

  • 如果在主进程准备就绪之前就创建并显示浏览器窗口,由于此时渲染进程和页面还未开始加载,窗口内自然就是空白,因此需要确保在合适的时机创建窗口。
  • 反之如果创建窗口后,又长时间不调用 window.show() 显示窗口,那么窗口会一直在后台加载页面,用户也会看不到,从而出现白屏的效果。

因此我们可以通过控制主进程的 Ready 事件时机以及 Window 窗口的加载时机来对这个问题进行优化,同样的关于加载时机我们也可以有两种方案进行优化:

  1. 通过监听 BrowserWindow 上面的 ready-to-show 事件控制窗口显示
// 解决白屏问题
app.whenReady().then(() => {
// 将创建窗口的代码放在 `app.whenReady` 事件回调中,确保主进程启动完成后再创建窗口
const mainWindow = new BrowserWindow({ show:false });
// 加载页面
mainWindow.loadURL('index.html');
// 在 ready-to-show 事件中显示窗口
mainWindow..once("ready-to-show", () => {
mainWindow.show();
});
});

上述代码通过操作 app.whenReady() 和 BrowserWindow 的 mainWindow.once('ready-to-show') 这几个 Electron 核心启动 API,优雅地处理了窗口隐藏 + 页面加载 + 窗口显示等问题,详细流程如下:

  • 将创建窗口的代码放在 app.whenReady 事件回调中,确保主进程启动完成后再创建窗口
  • 创建窗口的时候让窗口隐藏不显示{ show: false },避免页面没加载完成导致的白屏
  • 窗口加载页面 win.loadURL,也就是说窗口虽然隐藏了,但是不耽误加载页面
  • 通过 ready-to-show 事件来判断窗口是否已经准备好,这个事件其实就代表页面已经加载完成了,因此此时调用 mainWidnow.show() 让窗口显示就解决了白屏的问题
  1. 通过监听 BrowserWindow.webContents 上面的 did-finish-load 或者 dom-ready 事件来控制窗口显示
app.whenReady().then(() => {
// 将创建窗口的代码放在 `app.whenReady` 事件回调中,确保主进程启动完成后再创建窗口
const mainWindow = new BrowserWindow({ show:false });
// 加载页面
mainWindow.loadURL(indexPage);
// 通过 webContents 对应事件来处理窗口显示
mainWindow.webContents.on("did-finish-load", () => {
mainWindow.show();
});
});

此方案与上述方案的唯一区别就是,第一个使用的是 BrowserWindow 的事件来处理,而此方案通过判断 BrowserWindow.webContents 这个对象,这个对象是 Electron 中用来渲染以及控制 Web 页面的,因此我们可以更直接的使用 did-finish-load 或者直接 dom-ready 这两个事件来判断页面是否加载完成,这两个 API 的含义相信前端开发者都不陌生,页面加载完成以及 DOM Ready 都是前端的概念,通过这种方式也是可以解决启动白屏的。

相关文档:BrowserWindowwebCotnents

最后解决完成的效果如下:

white-screen-fix.gif

总结

从上图来看最终的效果还是不错的,当窗口出现的一瞬间页面就直接加载完成了,不过细心的小伙伴应该会发现,这个方案属于偷梁换柱,给用户的感觉是窗口出现的时候页面就有内容了,但是其实窗口没出现的时间是有空档期的,大概就是下面这个意思:

白屏流程.png

从上图以及实际效果来看,其实我们的启动时间是没有发生改变的,但是因为端上应用和我们纯 Web 应用的使用场景不同,它自身就是有应用的启动时间,所以空档期如果不长,这个方案的体验还是可以的。但是如果前面的空档期过长,那么可能就是 Electron 启动的时候加载资源过多造成的了,就需要其他优化方案了。由此也可以见得其实对于用户体验来说,可能我们的产品性能并不一定有提升,只要从场景出发从用户角度去考虑问题,其实就能提升整个应用的体验。

回归本篇文章,我们从问题入手分析了 Electron 启动白屏出现的原因并提供了对应的解决方案,笔者其实对 Electron 的开发也并不深入,只是解决了一个需求一个问题用文章做记录,欢迎大家交流心得,共同进步~


作者:前端周公子
来源:juejin.cn/post/7371386534179520539
收起阅读 »

用electron写个浏览器给自己玩

web
浏览器这种东西工程量很唬人,但是有了electron+webview我们就相当于只需要干组装的活就可以了,而且产品目标就是给自己玩, 成品的效果 😄本来想写成专业的技术博客,但是发现大家好像对那种密密麻麻,全是代码的技术博客不感兴趣,我就挑重点来写吧。 下...
继续阅读 »

浏览器这种东西工程量很唬人,但是有了electron+webview我们就相当于只需要干组装的活就可以了,而且产品目标就是给自己玩,
成品的效果
image.png
😄本来想写成专业的技术博客,但是发现大家好像对那种密密麻麻,全是代码的技术博客不感兴趣,我就挑重点来写吧。
image.png


下载拦截功能


image.png


下载逻辑如果不做拦截处理的话,默认就是我们平常写web那种弹窗的方式,既然是浏览器肯定不能是那样的。
electron中可以监听BrowserWindow的页面下载事件,并把拿到的下载状态传给渲染线程,实现类似浏览器的下载器功能。


//这个global.WIN =   global.WIN = new BrowserWindow({ ...})
global.WIN.webContents.session.on('will-download', (evt, item) => {
//其他逻辑
item.on('updated', (evt, state) => {
//实时的下载进度传递给渲染线程
})
})

页面搜索功能


当时做这个功能的时候我就觉得完了,这个玩意看起来太麻烦了,还要有一个的功能这不是头皮发麻啊。


image.png
查资料和文档发现这个居然是webview内置的功能,瞬间压力小了很多,我们只需要出来ctrl+f的时候把搜索框弹出来这个UI就可以了,关键字变色和下一个都是内部已经实现好了的。


function toSearch() {
let timer
return () => {
if (timer) {
clearTimeout(timer)
}

timer = setTimeout(() => {
if (keyword.value) {
webviewRef.value.findInPage(keyword.value, { findNext: true })
} else {
webviewRef.value.stopFindInPage('clearSelection')
}
}, 200)
}
}
function closeSearch() {
showSearch.value = false
webviewRef.value.stopFindInPage('clearSelection')
}

function installFindPage(webview) {
webviewRef.value = webview
webviewRef.value.addEventListener('found-in-page', (e) => {
current.value = e.result.activeMatchOrdinal
total.value = e.result.matches
})
}

当前标签页打开功能


就是因为chrome和edge这些浏览器每次使用的时候开非常多的标签,挤在一起,所以我想这个浏览器不能主动开标签,打开了一个标签后强制所有的标签都在当前标签覆盖。


app.on('web-contents-created', (event, contents) => {
contents.setWindowOpenHandler((info) => {
global.WIN?.webContents.send('webview-url-is-change')
if (info.disposition === 'new-window') {
return { action: 'allow' }
} else {
global.WIN?.webContents.send('webview-open-url', info.url)
return { action: 'deny' }
}
})
})

渲染线程监听到webview-open-url后也就是tart="_blank"的情况,强制覆盖当前不打开新窗口


ipcRenderer.on('webview-open-url', (event, url) => {
try {
let reg = /http|https/g
if (webviewRef.value && reg.test(url)) {
webviewRef.value.src = url
}
} catch (err) {
console.log(err)
}
})

标签页切换功能


这里的切换是css的显示隐藏,借助了vue-router
image.png


这里我们看dom就能清晰的看出来。


image.png


地址栏功能


地址栏支持输入url直接访问链接、支持关键字直接打开收藏的网站、还支持关键字搜索。优先级1打开收藏的网页 2访问网站 3关键字搜索


function toSearch(keyword) {
if (`${keyword}`.length === 0) {
return false
}
// app搜索
if (`${keyword}`.length < 20) {
let item = null
const list = [...deskList.value, ...ALL_DATA]
for (let i = 0; i < list.length; i++) {
if (
list[i].title.toUpperCase().search(keyword.toUpperCase()) !== -1 &&
list[i].type !== 'mini-component'
) {
item = list[i]
break
}
}
if (item) {
goApp(item)
return false
}
}

// 网页访问
let url
if (isUrl(keyword)) {
if (!/^https?:\/\//i.test(keyword)) {
url = 'http://' + keyword
} else {
url = keyword
}
goAppNewTab(url)
return false
} else {
// 关键字搜索
let searchEngine = localStorage.getItem('searchEngine')
searchEngine = searchEngine || CONFIG.searchEngine
url = searchEngine + keyword

if (!router.hasRoute('search')) {
router.addRoute({
name: 'search',
path: '/search',
meta: {
title: '搜索',
color: 'var(--app-icon-bg)',
icon: 'search.svg',
size: 1
},
component: WebView
})
keepAliveInclude.value.push('search')
}

router.push({
path: '/search',
query: { url }
})

setTimeout(() => {
Bus.$emit('toSearch', url)
}, 20)
}
}

桌面图标任意位置拖动


这个问题困扰了我很久,因为它不像电脑桌面大小是固定的,浏览器可以全屏也可以小窗口,如果最开始是大窗口然后拖成小窗口,那么图标就看不到了。后来想到我干脆给个中间区域固定大小,就可以解决这个问题了。因为固定大小出来起来就方便多了。这个桌面是上下两层


//背景格子
<div v-show="typeActive === 'me'" class="bg-boxs">
<div
v-for="(item, i) in 224" //这里有点不讲究了直接写死了
:key="item"
class="bg-box"
@dragenter="enter($event, { x: (i % 14) + 1, y: Math.floor(i / 14) + 1 })"
@dragover="over($event)"
@dragleave="leave($event)"
@drop="drop($event)"
>
</div>
</div>
// 桌面层
// ...

import { ref, computed } from 'vue'
import useDesk from '@/store/deskList'
import { storeToRefs } from 'pinia'

export default function useDrag() {
const dragging = ref(null)
const currentTarget = ref()
const desk = useDesk()

const { deskList } = storeToRefs(desk)
const { setDeskList, updateDeskData } = desk

function start(e, item) {
e.target.classList.add('dragging')
e.dataTransfer.effectAllowed = 'move'
dragging.value = item
currentTarget.value = e
console.log('开始')
}
let timer2
function end(e) {
dragging.value = null
e.target.classList.remove('dragging')
setDeskList(deskList.value)

if (timer2) {
clearTimeout(timer2)
}
timer2 = setTimeout(() => {
updateDeskData()
}, 2000)
}
function over(e) {
e.preventDefault()
}

let timer
function enter(e, item) {
e.dataTransfer.effectAllowed = 'move'
if (timer) {
clearTimeout(timer)
}

timer = setTimeout(() => {
if (item?.x) {
dragging.value.x = item.x
dragging.value.y = item.y
}
}, 100)
}
function leave(e) {}

function drop(e) {
e.preventDefault()
}

return { start, end, over, enter, leave, drop }
}


image.png


image.png


东西太多了就先介绍这些了


安装包地址


github.com/jddk/aweb-b…


也可以到官网后aweb123.com 如何进入微软商店下载,mac版本因为文件大于100mb没有传上去所以暂时还用不了。


作者:九段刀客
来源:juejin.cn/post/7395389351641612300
收起阅读 »

大声点回答我:token应该存储在cookie还是localStorage上?

web
背景 前置文章:浏览器: cookie机制完全解析 在考虑token是否应该存储在cookie或localStorage中时,我们需要综合考虑安全性、便利性、两者的能力边界以及设计目的等因素。 安全性: Cookies的优势: Set-Cookie: to...
继续阅读 »

背景


前置文章:浏览器: cookie机制完全解析


在考虑token是否应该存储在cookie或localStorage中时,我们需要综合考虑安全性、便利性、两者的能力边界以及设计目的等因素。
截屏2024-10-14 15.59.32.png


安全性:


Cookies的优势:


Set-Cookie: token=abc123; HttpOnly;Secure;SameSite=Strict;Domain=example.com; Path=/


  • HttpOnly:将 HttpOnly 属性设置为 true 可以防止 JavaScript 读取 cookie,从而有效防止 XSS(跨站脚本)攻击读取 token。这一特性使得 cookies 在敏感信息存储上更具安全性。

  • Secure:设置 Secure 属性后,cookie 只会在 HTTPS 连接时发送,从而防止中间人攻击。这确保了即使有人截获请求,token 也不会被明文传输。

  • SameSite:SameSite 属性减少了 CSRF(跨站请求伪造)攻击的风险,通过指示浏览器在同一站点请求时才发送 cookie。

  • Domain 和 Path:这些属性限制了 cookie 的作用范围,例如仅在特定子域或者路径下生效,进一步提高安全性。


localStorage的缺点:

XSS 风险:localStorage 对 JavaScript 代码完全可见,这意味着如果应用存在 XSS 漏洞,攻击者即可轻易获取存储在 localStorage 中的 token。


能力层面


Cookies可以做到更前置更及时的页面访问控制,服务器可以在接收到页面请求时,立即通过读取 cookie 判断用户身份,返回响应的页面(例如重定向到登录页)。


// 示例:后端在接收到请求时可以立即判断 
if (!request.cookies.token) {
response.redirect('/login');
}

和cookie相比 localStorage具有一定的滞后性,浏览器必须先加载 HTML 和 JavaScript资源,解析执行后 才能通过在localStorage取到数据后 经过ajax网络请求 发送给服务端判断用户身份,这种方式有滞后性,可能导致临时显示不正确的内容。


管理的便利性


Cookies是由服务端设置的 由浏览器自动管理生命周期的一种方式

服务器可以直接通过 HTTP 响应头设置 cookie,浏览器会自动在后续请求中携带,无需在客户端手动添加。减少了开发和维护负担,且降低了人为错误的风险。


localStorage需要客户端手动管理

使用 localStorage 需要在客户端代码管理 token,你得确保在每个请求中手动添加和删除token,增加了代码复杂度及出错的可能性。


设计目的:


HTTP协议是无状态的 一个用户第二次请求和一个新用户第一次请求 服务端是识别不出来的,cookie是为了让服务端记住客户端而被设计的。

Cookie 设计的初衷就是帮助服务器标识用户的会话状态(如登录状态),因而有很多内建的安全和管理机制,使其特别适合承载 token 等这些用户状态的信息。


localStorage 主要用于存储客户端关心的、较大体积的数据(如用户设置、首选项等),而不是设计来存储需要在每次请求时使用的认证信息。


总结


在大多数需要处理用户身份认证的应用中,将 token 存储在设置了合适属性的 cookie 中,不仅更安全,还更符合 cookie 的设计目的。


通过 HTTP 响应头由服务端设置并自动管理,极大简化了客户端代码,并确保在未经身份验证的情况下阻断对敏感页面的访问。


因此 我认为 在大多数情况下,将 token 存储在 cookies 中更为合理和安全。


补充


然鹅 现实的业务场景往往是复杂多变的 否则也不会有token应该存储在cookie还是localStorage上?这个问题出现了。


localStorage更具灵活性: 不同应用有不同的安全需求,有时 localStorage 可以提供更加灵活和精细化的控制。 开发者可以在 JavaScript 中手动管理 localStorage,包括在每次请求时显式设置认证信息。这种 灵活性 对于一些高级用例和性能优化场景可能非常有用。


所以一般推荐使用cookie 但是在合适的场景下使用localStorage完全没问题。


作者:某某某人
来源:juejin.cn/post/7433079710382571558
收起阅读 »

MCP 终极指南

为什么 MCP 是一个突破 MCP 官方集成教学: 🎖️ 第三方平台官方支持 MCP 的例子 🌎 社区 MCP 服务器 为什么是 MCP? Function Calling Model Context Protocol (MCP) AI Agent 思...
继续阅读 »

过去快一年的时间没有更新 AI 相关的博客,一方面是在忙 side project,另外一方面也是因为 AI 技术虽然日新月异,但是 AI 应用层的开发并没有多少新的东西,大体还是2023年的博客讲的那三样,Prompt、RAG、Agent。


但是自从去年 11 月底 Claude(Anthropic) 主导发布了 MCP(Model Context Protocol 模型上下文协议) 后,AI 应用层的开发算是进入了新的时代。


不过关于 MCP 的解释和开发,目前似乎还没有太多的资料,所以笔者决定将自己的一些经验和思考整理成一篇文章,希望能够帮助到大家。


为什么 MCP 是一个突破


我们知道过去一年时间,AI 模型的发展非常迅速,从 GPT 4 到 Claude Sonnet 3.5 到 Deepseek R1,推理和幻觉都进步的非常明显。


新的 AI 应用也很多,但我们都能感受到的一点是,目前市场上的 AI 应用基本都是全新的服务,和我们原来常用的服务和系统并没有集成,换句话说,AI 模型和我们已有系统集成发展的很缓慢。


例如我们目前还不能同时通过某个 AI 应用来做到联网搜索、发送邮件、发布自己的博客等等,这些功能单个实现都不是很难,但是如果要全部集成到一个系统里面,就会变得遥不可及。


如果你还没有具体的感受,我们可以思考一下日常开发中,想象一下在 IDE 中,我们可以通过 IDE 的 AI 来完成下面这些工作。



  • 询问 AI 来查询本地数据库已有的数据来辅助开发

  • 询问 AI 搜索 Github Issue 来判断某问题是不是已知的bug

  • 通过 AI 将某个 PR 的意见发送给同事的即时通讯软件(例如 Slack)来 Code Review

  • 通过 AI 查询甚至修改当前 AWS、Azure 的配置来完成部署


以上谈到的这些功能通过 MCP 目前正在变为现实,大家可以关注 Cursor MCP 和 Windsurf MCP 获取更多的信息。可以试试用 Cursor MCP + browsertools 插件来体验一下在 Cursor 中自动获取 Chrome dev tools console log 的能力。


为什么 AI 集成已有服务的进展这么缓慢?这里面有很多的原因,一方面是企业级的数据很敏感,大多数企业都要很长的时间和流程来动。另一个方面是技术方面,我们缺少一个开放的、通用的、有共识的协议标准。


MCP 就是 Claude(Anthropic) 主导发布的一个开放的、通用的、有共识的协议标准,如果你是一个对 AI 模型熟悉的开发人员,想必对 Anthropic 这个公司不会陌生,他们发布了 Claude 3.5 Sonnet 的模型,到目前为止应该还是最强的编程 AI 模型(刚写完就发布了 3.7😅)。



这里还是要多提一句,这个协议的发布最好机会应该是属于 OpenAI 的,如果 OpenAI 刚发布 GPT 时就推动协议,相信大家都不会拒绝,但是 OpenAI 变成了 CloseAI,只发布了一个封闭的 GPTs,这种需要主导和共识的标准协议一般很难社区自发形成,一般由行业巨头来主导。



Claude 发布了 MCP 后,官方的 Claude Desktop 就开放了 MCP 功能,并且推动了开源组织 Model Context Protocol,由不同的公司和社区进行参与,例如下面就列举了一些由不同组织发布 MCP 服务器的例子。


MCP 官方集成教学:



  • Git - Git 读取、操作、搜索。

  • GitHub - Repo 管理、文件操作和 GitHub API 集成。

  • Google Maps - 集成 Google Map 获取位置信息。

  • PostgreSQL - 只读数据库查询。

  • Slack - Slack 消息发送和查询。


🎖️ 第三方平台官方支持 MCP 的例子


由第三方平台构建的 MCP 服务器。



  • Grafana - 在 Grafana 中搜索查询数据。

  • JetBrains – JetBrains IDEs。

  • Stripe - 与Stripe API交互。


🌎 社区 MCP 服务器


下面是一些由开源社区开发和维护的 MCP 服务器。



  • AWS - 用 LLM 操作 AWS 资源。

  • Atlassian - 与 Confluence 和 Jira 进行交互,包括搜索/查询 Confluence 空间/页面,访问 Jira Issue 和项目。

  • Google Calendar - 与 Google 日历集成,日程安排,查找时间,并添加/删除事件。

  • Kubernetes - 连接到 Kubernetes 集群并管理 pods、deployments 和 services。

  • X (Twitter)  - 与 Twitter API 交互。发布推文并通过查询搜索推文。

  • YouTube - 与 YouTube API 集成,视频管理、短视频创作等。


为什么是 MCP?


看到这里你可能有一个问题,在 23 年 OpenAI 发布 GPT function calling 的时候,不是也是可以实现类似的功能吗?我们之前博客介绍的 AI Agent,不就是用来集成不同的服务吗?为什么又出现了 MCP。


function calling、AI Agent、MCP 这三者之间有什么区别?


Function Calling



  • Function Calling 指的是 AI 模型根据上下文自动执行函数的机制。

  • Function Calling 充当了 AI 模型与外部系统之间的桥梁,不同的模型有不同的 Function Calling 实现,代码集成的方式也不一样。由不同的 AI 模型平台来定义和实现。


如果我们使用 Function Calling,那么需要通过代码给 LLM 提供一组 functions,并且提供清晰的函数描述、函数输入和输出,那么 LLM 就可以根据清晰的结构化数据进行推理,执行函数。


Function Calling 的缺点在于处理不好多轮对话和复杂需求,适合边界清晰、描述明确的任务。如果需要处理很多的任务,那么 Function Calling 的代码比较难维护。


Model Context Protocol (MCP)



  • MCP 是一个标准协议,如同电子设备的 Type C 协议(可以充电也可以传输数据),使 AI 模型能够与不同的 API 和数据源无缝交互。

  • MCP 旨在替换碎片化的 Agent 代码集成,从而使 AI 系统更可靠,更有效。通过建立通用标准,服务商可以基于协议来推出它们自己服务的 AI 能力,从而支持开发者更快的构建更强大的 AI 应用。开发者也不需要重复造轮子,通过开源项目可以建立强大的 AI Agent 生态。

  • MCP 可以在不同的应用/服务之间保持上下文,从而增强整体自主执行任务的能力。


可以理解为 MCP 是将不同任务进行分层处理,每一层都提供特定的能力、描述和限制。而 MCP Client 端根据不同的任务判断,选择是否需要调用某个能力,然后通过每层的输入和输出,构建一个可以处理复杂、多步对话和统一上下文的 Agent。


AI Agent



  • AI Agent 是一个智能系统,它可以自主运行以实现特定目标。传统的 AI 聊天仅提供建议或者需要手动执行任务,AI Agent 则可以分析具体情况,做出决策,并自行采取行动。

  • AI Agent 可以利用 MCP 提供的功能描述来理解更多的上下文,并在各种平台/服务自动执行任务。


思考


为什么 Claude 推出 MCP 后会被广泛接受呢?其实在过去的一年中我个人也参与了几个小的 AI 项目的开发工作,在开发的过程中,将 AI 模型集成现有的系统或者第三方系统确实挺麻烦。


虽然市面上有一些框架支持 Agent 开发,例如 LangChain ToolsLlamaIndex 或者是 Vercel AI SDK


LangChain 和 LlamaIndex 虽然都是开源项目,但是整体发展还是挺混乱的,首先是代码的抽象层次太高了,想要推广的都是让开发人员几行代码就完成某某 AI 功能,这在 Demo 阶段是挺好用的,但是在实际开发中,只要业务一旦开始复杂,糟糕的代码设计带来了非常糟糕的编程体验。还有就是这几个项目都太想商业化了,忽略了整体生态的建设。


还有一个就是 Vercel AI SDK,尽管个人觉得 Vercel AI SDK 代码抽象的比较好,但是也只是对于前端 UI 结合和部分 AI 功能的封装还不错,最大的问题是和 Nextjs 绑定太深了,对其它的框架和语言支持度不够。


所以 Claude 推动 MCP 可以说是一个很好的时机,首先是 Claude Sonnet 3.5 在开发人员心中有较高的地位,而 MCP 又是一个开放的标准,所以很多公司和社区都愿意参与进来,希望 Claude 能够一直保持一个良好的开放生态。


MCP 对于社区生态的好处主要是下面两点:



  • 开放标准给服务商,服务商可以针对 MCP 开放自己的 API 和部分能力。

  • 不需要重复造轮子,开发者可以用已有的开源 MCP 服务来增强自己的 Agent。


MCP 如何工作


那我们来介绍一下 MCP 的工作原理。首先我们看一下官方的 MCP 架构图


MCP 架构图


总共分为了下面五个部分:



  • MCP Hosts: Hosts 是指 LLM 启动连接的应用程序,像 Cursor, Claude Desktop、Cline 这样的应用程序。

  • MCP Clients: 客户端是用来在 Hosts 应用程序内维护与 Server 之间 1:1 连接。

  • MCP Servers: 通过标准化的协议,为 Client 端提供上下文、工具和提示。

  • Local Data Sources: 本地的文件、数据库和 API。

  • Remote Services: 外部的文件、数据库和 API。


整个 MCP 协议核心的在于 Server,因为 Host 和 Client 相信熟悉计算机网络的都不会陌生,非常好理解,但是 Server 如何理解呢?


看看 Cursor 的 AI Agent 发展过程,我们会发现整个 AI 自动化的过程发展会是从 Chat 到 Composer 再进化到完整的 AI Agent。


AI Chat 只是提供建议,如何将 AI 的 response 转化为行为和最终的结果,全部依靠人类,例如手动复制粘贴,或者进行某些修改。


AI Composer 是可以自动修改代码,但是需要人类参与和确认,并且无法做到除了修改代码之外的其它操作。


AI Agent 是一个完全的自动化程序,未来完全可以做到自动读取 Figma 的图片,自动生产代码,自动读取日志,自动调试代码,自动 push 代码到 GitHub。


而 MCP Server 就是为了实现 AI Agent 的自动化而存在的,它是一个中间层,告诉 AI Agent 目前存在哪些服务,哪些 API,哪些数据源,AI Agent 可以根据 Server 提供的信息来决定是否调用某个服务,然后通过 Function Calling 来执行函数。


MCP Server 的工作原理


我们先来看一个简单的例子,假设我们想让 AI Agent 完成自动搜索 GitHub Repository,接着搜索 Issue,然后再判断是否是一个已知的 bug,最后决定是否需要提交一个新的 Issue 的功能。


那么我们就需要创建一个 Github MCP Server,这个 Server 需要提供查找 Repository、搜索 Issues 和创建 Issue 三种能力。


我们直接来看看代码:


const server = new Server(
{
name: "github-mcp-server",
version: VERSION,
},
{
capabilities: {
tools: {},
},
}
);

server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "search_repositories",
description: "Search for GitHub repositories",
inputSchema: zodToJsonSchema(repository.SearchRepositoriesSchema),
},
{
name: "create_issue",
description: "Create a new issue in a GitHub repository",
inputSchema: zodToJsonSchema(issues.CreateIssueSchema),
},
{
name: "search_issues",
description: "Search for issues and pull requests across GitHub repositories",
inputSchema: zodToJsonSchema(search.SearchIssuesSchema),
}
],
};
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
if (!request.params.arguments) {
throw new Error("Arguments are required");
}

switch (request.params.name) {
case "search_repositories": {
const args = repository.SearchRepositoriesSchema.parse(request.params.arguments);
const results = await repository.searchRepositories(
args.query,
args.page,
args.perPage
);
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
}

case "create_issue": {
const args = issues.CreateIssueSchema.parse(request.params.arguments);
const { owner, repo, ...options } = args;
const issue = await issues.createIssue(owner, repo, options);
return {
content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
};
}

case "search_issues": {
const args = search.SearchIssuesSchema.parse(request.params.arguments);
const results = await search.searchIssues(args);
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
}

default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
} catch (error) {}
});

async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("GitHub MCP Server running on stdio");
}

runServer().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});

上面的代码中,我们通过 server.setRequestHandler 来告诉 Client 端我们提供了哪些能力,通过 description 字段来描述这个能力的作用,通过 inputSchema 来描述完成这个能力需要的输入参数。


我们再来看看具体的实现代码:


export const SearchOptions = z.object({
q: z.string(),
order: z.enum(["asc", "desc"]).optional(),
page: z.number().min(1).optional(),
per_page: z.number().min(1).max(100).optional(),
});

export const SearchIssuesOptions = SearchOptions.extend({
sort: z.enum([
"comments",
...
]).optional(),
});

export async function searchUsers(params: z.infer<typeof SearchUsersSchema>) {
return githubRequest(buildUrl("https://api.github.com/search/users", params));
}

export const SearchRepositoriesSchema = z.object({
query: z.string().describe("Search query (see GitHub search syntax)"),
page: z.number().optional().describe("Page number for pagination (default: 1)"),
perPage: z.number().optional().describe("Number of results per page (default: 30, max: 100)"),
});

export async function searchRepositories(
query: string,
page: number = 1,
perPage: number = 30
) {
const url = new URL("https://api.github.com/search/repositories");
url.searchParams.append("q", query);
url.searchParams.append("page", page.toString());
url.searchParams.append("per_page", perPage.toString());

const response = await githubRequest(url.toString());
return GitHubSearchResponseSchema.parse(response);
}

可以很清晰的看到,我们最终实现是通过了 https://api.github.com 的 API 来实现和 Github 交互的,我们通过 githubRequest 函数来调用 GitHub 的 API,最后返回结果。


在调用 Github 官方的 API 之前,MCP 的主要工作是描述 Server 提供了哪些能力(给 LLM 提供),需要哪些参数(参数具体的功能是什么),最后返回的结果是什么。


所以 MCP Server 并不是一个新颖的、高深的东西,它只是一个具有共识的协议。


如果我们想要实现一个更强大的 AI Agent,例如我们想让 AI Agent 自动的根据本地错误日志,自动搜索相关的 GitHub Repository,然后搜索 Issue,最后将结果发送到 Slack。


那么我们可能需要创建三个不同的 MCP Server,一个是 Local Log Server,用来查询本地日志;一个是 GitHub Server,用来搜索 Issue;还有一个是 Slack Server,用来发送消息。


AI Agent 在用户输入 我需要查询本地错误日志,将相关的 Issue 发送到 Slack 指令后,自行判断需要调用哪些 MCP Server,并决定调用顺序,最终根据不同 MCP Server 的返回结果来决定是否需要调用下一个 Server,以此来完成整个任务。


如何使用 MCP


如果你还没有尝试过如何使用 MCP 的话,我们可以考虑用 Cursor(本人只尝试过 Cursor),Claude Desktop 或者 Cline 来体验一下。


当然,我们并不需要自己开发 MCP Servers,MCP 的好处就是通用、标准,所以开发者并不需要重复造轮子(但是学习可以重复造轮子)。


首先推荐的是官方组织的一些 Server:官方的 MCP Server 列表


目前社区的 MCP Server 还是比较混乱,有很多缺少教程和文档,很多的代码功能也有问题,我们可以自行尝试一下 Cursor Directory 的一些例子,具体的配置和实战笔者就不细讲了,大家可以参考官方文档。


MCP 的一些资源


下面是个人推荐的一些 MCP 的资源,大家可以参考一下。


MCP 官方资源



社区的 MCP Server 的列表



写在最后


本篇文章写的比较仓促,如果有错误再所难免,欢迎各位大佬指正。


最后本篇文章可以转载,但是请注明出处,会在 X/Twitter小红书微信公众号 同步发布,欢迎各位大佬关注一波。


References



The Ultimate Guide to MCP


相关系列文章推荐



作者:独立开发者_阿乐
来源:juejin.cn/post/7479471387020001306
收起阅读 »

这次终于轮到前端给后端兜底了🤣

web
需求交代 最近我们项目组开发了个互联网采集的功能,也就是后端合理抓取了第三方的文章,数据结构大致如下: <h1>前端人</h1> <p>学好前端,走遍天下都不怕</p> 数据抓取到后,存储到数据库,然后前端请求...
继续阅读 »

封面.png


需求交代


最近我们项目组开发了个互联网采集的功能,也就是后端合理抓取了第三方的文章,数据结构大致如下:


<h1>前端人</h1>
<p>学好前端,走遍天下都不怕</p>

数据抓取到后,存储到数据库,然后前端请求接口获取到数据,直接在页面预览


<div v-html='articleContent'></div>

image.png


整个需求已经交代清楚


这个需求有点为难后端了


前天,客户说要新增一个文章的pdf导出功能,但就是这么一个合情合理的需求,却把后端为难住了,原因是部分数据采集过来的结构可能是这样的:


<h1>前端人</h1>
<p>学好前端,走遍天下都不怕</p>
<div>前端强,前端狂,交互特效我称王!</div
<p>JS 写得好,需求改不了!</p>
<p>React Vue 两手抓,高薪 offer 到你家!</p>
<p>浏览器里横着走, bug 见我都绕道!</p>
<p>Chrome 调试一声笑, IE 泪洒旧时光!</p>
<span>Git 提交不留情,版本回退我最行!

仔细的人就能发现问题了,很多html元素存在没有完整的闭合情况


image.png


但浏览器是强大的,丝毫不影响渲染效果,原来浏览器自动帮我们补全结构了


image.png


可后端处理这件事就没那么简单了,爬取到的数据也比我举例的要复杂的多,使用第三方插件将html转pdf时会识别标签异常等问题,因此程序会抛异常


来自后端的建议


苦逼的后端折腾了很久,还是没折腾出来,终于他发现前端页面有个右键打印的功能,也就是:


image.png


于是他说:浏览器这玩意整挺好啊,前端能不能研究研究,尝试从前端实现导出


那就研究研究


我印象中,确实有个叫vue-print-nb的前端插件,可以实现这个功能


但.......等等,这个插件仅仅是唤起打印的功能,我总不能真做成这样,让用户另存为pdf吧


于是,只能另辟蹊径,终于我找到了这么个仓库:github.com/burc-li/vue…


里面实现了dom元素导出pdf的功能


image.png


image.png


效果很不错,技术用到了jspdfhtml2canvas这两个第三方库,代码十分简单


const downLoadPdfA4Single = () => {
const pdfContaniner = document.querySelector('#pdfContaniner')
html2canvas(pdfContaniner).then(canvas => {
// 返回图片dataURL,参数:图片格式和清晰度(0-1)
const pageData = canvas.toDataURL('image/jpeg', 1.0)

// 方向纵向,尺寸ponits,纸张格式 a4 即 [595.28, 841.89]
const A4Width = 595.28
const A4Height = 841.89 // A4纸宽
const pageHeight = A4Height >= A4Width * canvas.height / canvas.width ? A4Height : A4Width * canvas.height / canvas.width
const pdf = new jsPDF('portrait', 'pt', [A4Width, pageHeight])

// addImage后两个参数控制添加图片的尺寸,此处将页面高度按照a4纸宽高比列进行压缩
pdf.addImage(
pageData,
'JPEG',
0,
0,
A4Width,
A4Width * canvas.height / canvas.width,
)
pdf.save('下载一页PDF(A4纸).pdf')
})
}

技术流程大致就是:



  • dom -> canvas

  • canvas -> image

  • image -> pdf


似乎一切都将水到渠成了


困在眼前的难题


这个技术栈,最核心的就是:必须要用到dom元素渲染


如果你尝试将打印的元素设置样式:


display: none;


visibility: hidden;



opacity: 0;

执行导出功能都将抛异常或者只能导出一个空白的pdf


这时候有人会问了:为什么要设置dom元素为不可见?


试想一下,你做了一个导出功能,总不能让客户必须先打开页面等html渲染完后,再导出吧?


客户的理想状态是:在列表的操作列里,有个导出按钮,点击就可以导出pdf了


何况还需要实现批量勾选导出的功能,总不能程序控制,导出一个pdf就open一个窗口渲染html吧


寻找新方法


此路不通,就只能重新寻找新的方向,不过也没费太多功夫,就找到了另外一个插件html2pdf.js解决了这事


这插件用起来也极其简单


npm install html2pdf.js

<template>
<div class="container">
<button @click="generatePDF">下载PDF</button>
</div>
</template>
<script setup>
import html2pdf from 'html2pdf.js'

// 使用示例
let element = `
<h1>前端人</h1>
<p>学好前端,走遍天下都不怕</p>
<div>前端强,前端狂,交互特效我称王!</div
<p>JS 写得好,需求改不了!</p>
<p>React Vue 两手抓,高薪 offer 到你家!</p>
<p>浏览器里横着走, bug 见我都绕道!</p>
<p>Chrome 调试一声笑, IE 泪洒旧时光!</p>
<span>Git 提交不留情,版本回退我最行!
`
;

function generatePDF() {
// 配置选项
const opt = {
margin: 10,
filename: 'hello_world.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2 },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
// 生成PDF并导出
html2pdf().from(element).set(opt).save();
}
</script>


功能正常,似乎一切都完美


image.png


问题没有想的那么简单


如果我们的html是纯文本元素,这程序跑起来没有任何问题,但我们抓取的信息都源于互联网,html结构怎么可能会这么简单?如果我们的html中包含图片信息,例如:


// 使用示例
let element = `
<div>
<img src='http://t13.baidu.com/it/u=2041049195,1001882902&fm=224&app=112&f=JPEG?w=500&h=500' style="width: 300px;" />
<p>职业:前端</p>
<p>技能:唱、跳、rap</p>
</div>
`
;

此时你会发现,导出来的pdf,图片占位处是个空白块


image.png



思考一下:类似案例中的图片加载方式,都是get方式的异步请求,而异步请求就会导致图片还没渲染完成,但导出的程序已经执行完成情况(最直接的观察方式就是,把这个元素放到浏览器上渲染,会发现图片也是过一会才慢慢加载完成的)


不过我不确定html2pdf.js这个插件是否会发起图片请求,但不管发不发起,导出的行为明显是在图片渲染前完成的,就导致了这个空白块的存在



问题分析完了,那就解决吧


既然图片异步加载不行,那就使用图片同步加载吧


不是吧,你问我:什么是图片同步加载?我也不晓得,这个词是我自己当下凭感觉造的,如有雷同,纯属巧合了


那我理解的图片同步加载是什么意思呢?简单来说,就是将图片转成Base64,因为这种方式,即使说无网的情况也能正常加载图片,因此我凭感觉断定,这就是图片同步加载


基于这个思路,我写了个demo


<template>
<div class="container">
<button @click="generatePDF">下载PDF</button>
</div>
</template>
<script setup>
import html2pdf from 'html2pdf.js'

async function convertImagesToBase64(htmlString) {
// 创建一个临时DOM元素来解析HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlString;

// 获取所有图片元素
const images = tempDiv.querySelectorAll('img');

// 遍历每个图片并转换
for (const img of images) {
try {
const base64 = await getBase64FromUrl(img.src);
img.src = base64;
} catch (error) {
console.error(`无法转换图片 ${img.src}:`, error);
// 保留原始URL如果转换失败
}
}

// 返回转换后的HTML
return tempDiv.innerHTML;
}

// 图片转base64
function getBase64FromUrl(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous'; // 处理跨域问题

img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;

const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);

// 获取Base64数据
const dataURL = canvas.toDataURL('image/png');
resolve(dataURL);
};

img.onerror = () => {
reject(new Error('图片加载失败'));
};

img.src = url;
});
}

// 使用示例
let element = `
<div>
<img src='http://t13.baidu.com/it/u=2041049195,1001882902&fm=224&app=112&f=JPEG?w=500&h=500' style="width: 300px;" />
<p>职业:前端</p>
<p>技能:唱、跳、rap</p>
</div>
`
;

function generatePDF() {
convertImagesToBase64(element)
.then(convertedHtml => {
// 配置选项
const opt = {
margin: 10,
filename: '前端大法好.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2 },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};

// 生成PDF并导出
html2pdf().from(convertedHtml).set(opt).save();
})
.catch(error => {
console.error('转换过程中出错:', error);
});
}
</script>


此时就大功告成啦!不过得提一句:图片的URL链接必须是同源或者允许跨越的,否则就会存在图片加载异常的问题


修复图片过大的问题


部分图片的宽度会过大,导致图片加载不全的问题,这在预览的情况下也存在


image.png


因为需要加上样式限定


img {
max-width: 100%;
max-height: 100%;
vertical-align: middle;
height: auto !important;
width: auto !important;
margin: 10px 0;
}

这样就正常啦


image.png


故此需要在导出pdf前,给元素添加一个图片的样式限定


element =`<style>
img {
max-width: 100%;
max-height: 100%;
vertical-align: middle;
height: auto !important;
width: auto !important;
margin: 10px 0;
}
</style>`
+ element;

完整代码:


<template>
<div class="container">
<button @click="generatePDF">下载PDF</button>
</div>
</template>
<script setup>
import html2pdf from 'html2pdf.js'
async function convertImagesToBase64(htmlString) {
// 创建一个临时DOM元素来解析HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlString;

// 获取所有图片元素
const images = tempDiv.querySelectorAll('img');

// 遍历每个图片并转换
for (const img of images) {
try {
const base64 = await getBase64FromUrl(img.src);
img.src = base64;
} catch (error) {
console.error(`无法转换图片 ${img.src}:`, error);
// 保留原始URL如果转换失败
}
}

// 返回转换后的HTML
return tempDiv.innerHTML;
}
// 图片转base64
function getBase64FromUrl(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous'; // 处理跨域问题

img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;

const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);

// 获取Base64数据
const dataURL = canvas.toDataURL('image/png');
resolve(dataURL);
};

img.onerror = () => {
reject(new Error('图片加载失败'));
};

img.src = url;
});
}

// 使用示例
let element = `
<div>
<img src='http://t13.baidu.com/it/u=2041049195,1001882902&fm=224&app=112&f=JPEG?w=500&h=500' style="width: 300px;" />
<p>职业:前端</p>
<p>技能:唱、跳、rap</p>
</div>
`
;

function generatePDF() {
element =`<style>
img {
max-width: 100%;
max-height: 100%;
vertical-align: middle;
height: auto !important;
width: auto !important;
margin: 10px 0;
}
</style>`
+ element;
convertImagesToBase64(element)
.then(convertedHtml => {
// 配置选项
const opt = {
margin: 10,
filename: '前端大法好.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2 },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};

// 生成PDF
html2pdf().from(convertedHtml).set(opt).save();
})
.catch(error => {
console.error('转换过程中出错:', error);
});
}
</script>


后话


前天提的需求,昨天兜的底,今天写的文章记录


这种问题,理应该后端处理,但后端和我吐槽过他处理起来的困难与问题,寻求前端帮助时,我也会积极配合。可在现实中,我遇过很多后端,死活不愿意配合前端,例如日期格式化、数据id类型bigint过大不字符化返回给前端等等,主打一个本着前端可以做就前端做的原则,说实在:属实下头


前后端本应该就是相互打配合的关系,谁方便就行个方便,没必要僵持不下


今天的分享就到此结束,如果你对技术/行业交流有兴趣,欢迎添加howcoder微信,邀你进群交流


往期精彩


《你不了解的Grid布局》


《就你小子还不会 Grid布局是吧?》


《超硬核:从零到一部署指南》


《私活2年,我赚到了人生的第一桶金》


《接入AI后,开源项目瞬间有趣了😎》


《肝了两个月,我们无偿开源了》


《彻底不NG前端路由》


《vue项目部署自动检测更新》


《一个公告滚动播放功能引发的背后思考》


《前端值得学习的开源socket应用》


作者:howcode
来源:juejin.cn/post/7486440418139652137
收起阅读 »

想做线上拍卖平台?这款现成源码助你零门槛快速上线!

近年来,电商行业进入存量竞争,传统零售的增长红利逐渐消退,而 直播+拍卖 的模式却在悄然崛起。数据显示,全球 直播电商市场规模已突破 7000 亿美元,而其中拍卖模式因其 高互动性、高成交率,成为新兴电商的流量密码。无论是珠宝、奢侈品、艺术品,还是房产、二手车...
继续阅读 »

近年来,电商行业进入存量竞争,传统零售的增长红利逐渐消退,而 直播+拍卖 的模式却在悄然崛起。数据显示,全球 直播电商市场规模已突破 7000 亿美元,而其中拍卖模式因其 高互动性、高成交率,成为新兴电商的流量密码。

无论是珠宝、奢侈品、艺术品,还是房产、二手车,甚至盲盒,拍卖模式都能最大化地调动用户情绪,让商品价值被充分挖掘。一场竞拍,往往能创造普通电商难以企及的高溢价,让卖家和买家都乐在其中。

然而,对于创业者来说,开发一套稳定、专业的拍卖系统,并非易事——高昂的研发成本、漫长的测试周期、复杂的支付与风控体系,往往让人望而却步。

一、有没有一种方式,可以低成本、快速上线拍卖平台,抢占市场先机?
东莞梦幻网络科技,一站式直播拍卖系统,让你低成本抢占市场!
针对创业者的痛点,东莞梦幻网络科技 推出了 「开箱即用」的直播拍卖系统源码,助力企业 低成本、高效率上线自己的直播拍卖平台!

🌟 核心功能亮点:
✅ 直播+竞拍双驱动:用户边看直播,边参与竞价,沉浸感满分
✅ 全网最透明:竞拍全程直播,所有出价公开可见,用户信任度直线上升
✅ 互动玩法多样:弹幕PK、红包雨、连麦砍价……增强用户粘性,留住流量
✅ AR/VR展示技术:360°旋转珠宝展示、房产 VR 看房,让体验感直接拉满

🔹 双模式商城,轻松打造综合电商平台
刺激拍卖:拍卖倒计时+智能加价,助推成交率飙升
常规零售:拍卖+固定价销售双模式,满足不同用户需求

🔹 6大盈利模式,打造持续增长的商业闭环
💰 佣金抽成:每笔成交,平台都能分润
💰 店铺年费:商家入驻,持续稳定收益
💰 广告位招商:商家竞价投放广告,赚取流量红利
💰 会员增值服务:VIP 会员享受独家折扣,提升复购率
💰 竞拍加速道具:用户可付费提升竞拍优势,创造更多收入点
💰 分销裂变:邀请好友参与竞拍,赚取推广收益


二、从0到1,他如何7天搭建拍卖平台,赚到第一桶金?
90后创业者李明,原本只是在某电商平台做二手奢侈品交易,但受限于平台规则和高额抽成,利润一直难以提升。

一次偶然的机会,他了解到直播+拍卖的模式,并看到了商机。但自己组建技术团队开发拍卖系统,至少需要 百万级别的资金投入,这让他望而却步。

直到他接触到了 东莞梦幻网络科技的拍卖系统源码——一次性购买,直接搭建,不到7天就能上线,让他看到了希望。

🚀 短短一个月,他的直播拍卖平台成交额突破 100 万元!
💡 用户粘性大幅提高,90% 参与竞拍的用户都会复购
📈 商家争相入驻,平台流量不断上涨

李明感慨:“如果自己开发,至少要花 6 个月以上,但现在7天就上线了,直接抢占了市场先机。”

三、未来已来,直播+拍卖将是电商的新主流,你还要等多久?
电商的流量越来越贵,传统卖货模式难以突破,而 直播+拍卖 以超高互动性、强成交驱动成为新趋势。

但风口稍纵即逝,真正赚钱的人,都是 “第一个吃螃蟹的人”!

收起阅读 »

一站式体育直播系统,让你跳过开发,快速抢占市场

面对体育直播平台开发中遇到的高成本和长周期问题,许多创业者往往感到困扰。不过,东莞梦幻网络科技有限公司现在提供了一种全新的解决方案,其精心打造的体育直播系统将帮助创业者们轻松跳过繁琐的开发阶段,直接进入运营快车道。沉浸式互动体验,构建社交主场东莞梦幻网络深知互...
继续阅读 »

面对体育直播平台开发中遇到的高成本和长周期问题,许多创业者往往感到困扰。不过,东莞梦幻网络科技有限公司现在提供了一种全新的解决方案,其精心打造的体育直播系统将帮助创业者们轻松跳过繁琐的开发阶段,直接进入运营快车道。

沉浸式互动体验,构建社交主场
东莞梦幻网络深知互动性对于提升用户体验的重要性,因此在其直播系统中集成了社交聊天室功能,让粉丝能够实时交流,分享观赛心得。此外,趣味竞猜活动不仅增加了观赛的乐趣,还有效点燃了用户的热情。而通过积分商城的设置,用户可以通过参与平台活动获得奖励,从而提升了用户忠诚度,使得这个平台不仅仅是一个观看比赛的地方,更是体育爱好者的社交主场。

专业内容矩阵,吸引真正体育迷
为了增强平台的专业性和吸引力,东莞梦幻网络提供了专家深度解析赛事、独家行业资讯报道以及精准的赛事预测分析等服务。这些高质量的内容不仅有助于树立平台作为专业体育媒体的形象,还能吸引那些对体育充满热情且追求深入见解的忠实用户群体。

多元变现渠道,确保投资回报
东莞梦幻网络的体育直播系统支持多种盈利模式,包括但不限于打赏、会员订阅、VIP特权、广告位精准投放以及赛事周边商城等。这种多元化的收入来源组合为投资者提供了更加稳定和有保障的投资回报途径。

自生长内容生态,持续激发平台活力
为了保证平台内容的新鲜度和多样性,东莞梦幻网络鼓励用户生成内容(UGC),如原创短视频分享、体育社区论坛互动及赛事精彩集锦回放等功能,促进了良性内容循环的发展,使平台始终保持活力。

东莞梦幻网络科技凭借其革命性的体育直播系统,正在改变着行业的游戏规则。无论是初创企业还是寻求转型的传统媒体,都可以借助这一平台快速实现目标,开启属于自己的成功之旅。对于希望在体育直播领域有所建树的企业来说,这是一个不容错过的机会。

收起阅读 »

React 官方推荐使用 Vite

web
“技术更替不是一场革命,而是一场漫长的进化过程。” Hello,大家好,我是 三千。 React 官方已明确建议开发者逐步淘汰 Create React App (CRA) ,转而使用 Vite 等现代框架或工具来创建新项目。 那官方为什么要这样做呢? 一...
继续阅读 »

“技术更替不是一场革命,而是一场漫长的进化过程。”



Hello,大家好,我是 三千。


React 官方已明确建议开发者逐步淘汰 Create React App (CRA) ,转而使用 Vite 等现代框架或工具来创建新项目。


那官方为什么要这样做呢?




一、CRA 被淘汰的背景与原因



  1. 历史局限性

    CRA 诞生于 2016 年,旨在简化 React 项目的初始化配置,但其底层基于 Webpack 和 Babel 的架构在性能、扩展性和灵活性上逐渐无法满足现代开发需求。随着项目规模扩大,CRA 的启动和构建速度显著下降,且默认配置难以优化生产包体积。

  2. 维护停滞与兼容性问题

    React 团队于 2023 年宣布停止积极维护 CRA,且 CRA 的最新版本(v5.0.1)已无法兼容 React 19 等新特性,导致其在生产环境中逐渐不适用。

  3. 缺乏对现代开发模式的支持

    CRA 仅提供客户端渲染(CSR)的默认配置,无法满足服务端渲染(SSR)、静态生成(SSG)等需求。此外,其“零配置”理念限制了路由、状态管理等常见需求的灵活实现。




二、Vite 成为 React 官方推荐的核心优势



  1. 性能提升



    • 开发速度:Vite 基于原生 ESM 模块和 esbuild(Go 语言编写)实现秒级启动与热更新,显著优于 CRA 的 Webpack 打包机制。

    • 生产构建:通过 Rollup 优化代码体积,支持 Tree Shaking 和懒加载,减少冗余代码。



  2. 灵活性与生态兼容



    • 配置自由:允许开发者按需定制构建流程,支持 TypeScript、CSS 预处理器、SVG 等特性,无需繁琐的 eject 操作。

    • 框架无关性:虽与 React 深度集成,但也可用于 Vue、Svelte 等项目,适应多样化技术栈。



  3. 现代化开发体验



    • 原生浏览器支持:利用现代浏览器的 ESM 特性,无需打包即可直接加载模块。

    • 插件生态:丰富的 Vite 插件(如 @vitejs/plugin-react)简化了 React 项目的开发与调试。






三、迁移至 Vite 的具体步骤 



  1. 卸载 CRA 依赖


    npm uninstall react-scripts
    npm install vite @vitejs/plugin-react --save-dev


  2. 调整项目结构



    • 将 index.html 移至项目根目录,并更新脚本引用为 ESM 格式:


      <script type="module" src="/src/main.jsx"></script>


    • 将 .js 文件扩展名改为 .jsx(如 App.js → App.jsx)。



  3. 配置 Vite

    创建 vite.config.js 文件:


    import { defineConfig } from "vite";
    import react from "@vitejs/plugin-react";

    export default defineConfig({
    plugins: [react()],
    });


  4. 更新环境变量

    环境变量前缀需从 REACT_APP_ 改为 VITE_(如 VITE_API_KEY=123)。

  5. 运行与调试

    修改 package.json 脚本命令:


    "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
    }





四、其他官方推荐的 React 框架 



  1. Next.js



    • 适用场景:服务端渲染(SSR)、静态生成(SSG)及全栈应用开发。

    • 优势:内置路由、API 路由、图像优化等功能,适合企业级应用与 SEO 敏感项目。



  2. Remix



    • 适用场景:嵌套路由驱动的全栈应用,注重数据加载优化与渐进增强。

    • 优势:集成数据预加载机制,减少请求瀑布问题。



  3. Astro



    • 适用场景:内容型静态网站(如博客、文档站)。

    • 优势:默认零客户端 JS 开销,通过“岛屿架构”按需激活交互组件。






五、总结与建议



  • 新项目:优先选择 Vite(轻量级 CSR 项目)或 Next.js(复杂全栈应用)。

  • 现有 CRA 项目:逐步迁移至 Vite,或根据需求转向 Next.js/Remix 等框架。

  • 学习曲线:Vite 对 React 核心概念干扰较小,适合初学者;Next.js 功能全面但学习成本较高。


React 生态正朝着  “库+框架”协同发展 的方向演进,开发者需结合项目需求选择工具链,以平衡性能、灵活性与开发效率。


结语


以上就是今天与大家分享的全部内容,你的支持是我更新的最大动力,我们下期见!


打工人肝 文章/视频 不易,期待你一键三连的鼓励 !!!



😐 这里是【程序员三千】,💻 一个喜欢捣鼓各种编程工具新鲜玩法的啊婆主。


🏠 已入驻:抖爸爸、b站、小红书(都叫【程序员三千】)


💽 编程/AI领域优质资源推荐 👉 http://www.yuque.com/xiaosanye-o…



作者:程序员三千_
来源:juejin.cn/post/7472008189976461346
收起阅读 »

async/await 必须使用 try/catch 吗?

web
前言 在 JavaScript 开发者的日常中,这样的对话时常发生: 👨💻 新人:"为什么页面突然白屏了?" 👨🔧 老人:"异步请求没做错误处理吧?" async/await 看似优雅的语法糖背后,隐藏着一个关键问题:错误处理策略的抉择。 在 JavaSc...
继续阅读 »

前言


在 JavaScript 开发者的日常中,这样的对话时常发生:



  • 👨💻 新人:"为什么页面突然白屏了?"

  • 👨🔧 老人:"异步请求没做错误处理吧?"


async/await 看似优雅的语法糖背后,隐藏着一个关键问题:错误处理策略的抉择


在 JavaScript 中使用 async/await 时,很多人会问:“必须使用 try/catch 吗?”


其实答案并非绝对,而是取决于你如何设计错误处理策略和代码风格。


接下来,我们将探讨 async/await 的错误处理机制、使用 try/catch 的优势,以及其他可选的错误处理方法。


async/await 的基本原理


异步代码的进化史


// 回调地狱时代
fetchData(url1, (data1) => {
process(data1, (result1) => {
fetchData(url2, (data2) => {
// 更多嵌套...
})
})
})

// Promise 时代
fetchData(url1)
.then(process)
.then(() => fetchData(url2))
.catch(handleError)

// async/await 时代
async function workflow() {
const data1 = await fetchData(url1)
const result = await process(data1)
return await fetchData(url2)
}

async/await 是基于 Promise 的语法糖,它使异步代码看起来更像同步代码,从而更易读、易写。一个 async 函数总是返回一个 Promise,你可以在该函数内部使用 await 来等待异步操作完成。


如果在异步操作中出现错误(例如网络请求失败),该错误会使 Promise 进入 rejected 状态


async function fetchData() {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
return data;
}

使用 try/catch 捕获错误


打个比喻,就好比铁路信号系统


想象 async 函数是一列高速行驶的列车:



  • await 是轨道切换器:控制代码执行流向

  • 未捕获的错误如同脱轨事故:会沿着铁路网(调用栈)逆向传播

  • try/catch 是智能防护系统

    • 自动触发紧急制动(错误捕获)

    • 启动备用轨道(错误恢复逻辑)

    • 向调度中心发送警报(错误日志)




为了优雅地捕获 async/await 中出现的错误,通常我们会使用 try/catch 语句。这种方式可以在同一个代码块中捕获同步和异步抛出的错误,使得错误处理逻辑更集中、直观。



  • 代码逻辑集中,错误处理与业务逻辑紧密结合。

  • 可以捕获多个 await 操作中抛出的错误。

  • 适合需要在出错时进行统一处理或恢复操作的场景。


async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error fetching data:", error);
// 根据需要,可以在此处处理错误,或者重新抛出以便上层捕获
throw error;
}
}

不使用 try/catch 的替代方案


虽然 try/catch 是最直观的错误处理方式,但你也可以不在 async 函数内部使用它,而是在调用该 async 函数时捕获错误


在 Promise 链末尾添加 .catch()


async function fetchData() {
const response = await fetch("https://api.example.com/data");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}

// 调用处使用 Promise.catch 捕获错误
fetchData()
.then(data => {
console.log("Data:", data);
})
.catch(error => {
console.error("Error fetching data:", error);
});

这种方式将错误处理逻辑移至函数调用方,适用于以下场景:



  • 当多个调用者希望以不同方式处理错误时。

  • 希望让 async 函数保持简洁,将错误处理交给全局统一的错误处理器(例如在 React 应用中可以使用 Error Boundary)。


将 await 与 catch 结合


async function fetchData() {
const response = await fetch('https://api.example.com/data').catch(error => {
console.error('Request failed:', error);
return null; // 返回兜底值
});
if (!response) return;
// 继续处理 response...
}

全局错误监听(慎用,适合兜底)


// 浏览器端全局监听
window.addEventListener('unhandledrejection', event => {
event.preventDefault();
sendErrorLog({
type: 'UNHANDLED_REJECTION',
error: event.reason,
stack: event.reason.stack
});
showErrorToast('系统异常,请联系管理员');
});

// Node.js 进程管理
process.on('unhandledRejection', (reason, promise) => {
logger.fatal('未处理的 Promise 拒绝:', reason);
process.exitCode = 1;
});

错误处理策略矩阵


决策树分析


graph TD
A[需要立即处理错误?] -->|是| B[使用 try/catch]
A -->|否| C{错误类型}
C -->|可恢复错误| D[Promise.catch]
C -->|致命错误| E[全局监听]
C -->|批量操作| F[Promise.allSettled]

错误处理体系



  1. 基础层:80% 的异步操作使用 try/catch + 类型检查

  2. 中间层:15% 的通用错误使用全局拦截 + 日志上报

  3. 战略层:5% 的关键操作实现自动恢复机制


小结


我的观点是:不强制要求,但强烈推荐



  • 不强制:如果不需要处理错误,可以不使用 try/catch,但未捕获的 Promise 拒绝(unhandled rejection)会导致程序崩溃(在 Node.js 或现代浏览器中)。

  • 推荐:90% 的场景下需要捕获错误,因此 try/catch 是最直接的错误处理方式。


所有我个人观点:使用 async/await 尽量使用 try/catch好的错误处理不是消灭错误,而是让系统具备优雅降级的能力


你的代码应该像优秀的飞行员——在遇到气流时,仍能保持平稳飞行。大家如有不同意见,还请评论区讨论,说出自己的见解。


作者:雨夜寻晴天
来源:juejin.cn/post/7482013975077928995
收起阅读 »

告别龟速删除!前端老司机教你秒删node_modules的黑科技

web
引言:每个前端的痛——node_modules删除噩梦 “npm install一时爽,删包火葬场。”这几乎是所有Node.js开发者都经历过的痛。尤其是当项目依赖复杂时,动辄几百MB甚至几个G的node_modules文件夹,手动删除时转圈圈的进度条简直让人...
继续阅读 »

引言:每个前端的痛——node_modules删除噩梦


“npm install一时爽,删包火葬场。”这几乎是所有Node.js开发者都经历过的痛。尤其是当项目依赖复杂时,动辄几百MB甚至几个G的node_modules文件夹,手动删除时转圈圈的进度条简直让人抓狂。


如何高效解决这个问题?今天我们就来揭秘几种秒删node_modules的硬核技巧,让你从此告别龟速删除!




一、为什么手动删除node_modules这么慢?


node_modules的目录结构复杂,层级深、文件数量庞大(比如一个中型项目可能有上万个小文件)。手动删除时,操作系统需要逐个处理这些文件,导致效率极低,尤其是Windows系统表现更差。核心原因包括:



  1. 文件系统限制:Windows的NTFS和macOS的HFS+对超多小文件的删除并未优化,系统需要频繁更新索引和缓存,资源占用高。

  2. 权限问题:某些文件可能被进程占用或权限不足,导致删除失败或卡顿。

  3. 递归删除效率低:系统自带的删除命令(如右键删除)是单线程操作,而node_modules的嵌套结构会让递归删除耗时剧增。




二、终极方案:用rimraf实现“秒删”


如果你还在手动拖拽删除,赶紧试试这个Node.js社区公认的神器——rimraf!它的原理是封装了rm -rf命令,通过减少系统调用和优化递归逻辑,速度提升可达10倍以上。


操作步骤



  1. 全局安装rimraf(仅需一次):
    npm install rimraf -g


  2. 一键删除

    进入项目根目录,执行:
    rimraf node_modules

    实测:一个5GB的node_modules,10秒内删干净!


进阶用法



  • 集成到npm脚本:在package.json中添加脚本,直接运行npm run clean
    {
    "scripts": {
    "clean": "rimraf node_modules"
    }
    }


  • 跨平台兼容:无论是Windows、Linux还是macOS,命令完全一致,团队协作无压力。




三、其他高效删除方案


如果不想安装额外工具,系统原生命令也能解决问题:


1. Windows用户:用命令行暴力删除



  • CMD命令
    rmdir /s /q node_modules

    /s表示递归删除,/q表示静默执行(不弹窗确认)。

  • PowerShell(更快)
    Remove-Item -Force -Recurse node_modules



2. Linux/macOS用户:终端直接起飞


rm -rf ./node_modules



四、避坑指南:删不干净怎么办?


有时即使删了node_modules,重新安装依赖仍会报错。此时需要彻底清理残留



  1. 清除npm缓存
    npm cache clean --force


  2. 删除锁文件

    手动移除package-lock.jsonyarn.lock

  3. 重启IDE:确保没有进程占用文件。




五、总结:选对工具,效率翻倍


方案适用场景速度对比
rimraf跨平台、大型项目⚡⚡⚡⚡⚡
系统命令临时快速操作⚡⚡⚡
手动删除极小项目(不推荐)

推荐组合拳:日常使用rimraf+脚本,遇到权限问题时切换系统命令。




互动话题

你遇到过最离谱的node_modules有多大?评论区晒出你的经历!




作者:LeQi
来源:juejin.cn/post/7477926585087606820
收起阅读 »