注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Arco Design 停摆!字节跳动 UI 库凉了?

web
1. 引言:设计系统的“寒武纪大爆发”与 Arco 的陨落 在 2019 年至 2021 年间,中国前端开发领域经历了一场前所未有的“设计系统”爆发期。伴随着企业级 SaaS 市场的崛起和中后台业务的复杂度攀升,各大互联网巨头纷纷推出了自研的 UI 组件库。这...
继续阅读 »

1. 引言:设计系统的“寒武纪大爆发”与 Arco 的陨落


在 2019 年至 2021 年间,中国前端开发领域经历了一场前所未有的“设计系统”爆发期。伴随着企业级 SaaS 市场的崛起和中后台业务的复杂度攀升,各大互联网巨头纷纷推出了自研的 UI 组件库。这不仅是技术实力的展示,更是企业工程化标准的话语权争夺。在这一背景下,字节跳动推出了 Arco Design,这是一套旨在挑战 Ant Design 霸主地位的“双栈”(React & Vue)企业级设计系统。


Arco Design 在发布之初,凭借其现代化的视觉语言、对 TypeScript 的原生支持以及极具创新性的“Design Lab”设计令牌(Design Token)管理系统,迅速吸引了大量开发者的关注。它被定位为不仅仅是一个组件库,而是一套涵盖设计、开发、工具链的完整解决方案。然而,就在其社区声量达到顶峰后的短短两两年内,这一曾被视为“下一代标准”的项目却陷入了令人费解的沉寂。


截至 2025 年末,GitHub 上的 Issues 堆积如山,关键的基础设施服务(如 IconBox 图标平台)频繁宕机,官方团队的维护活动几乎归零。对于数以万计采用了 Arco Design 的企业和独立开发者而言,这无疑是一场技术选型的灾难。


本文将深入剖析 Arco Design 从辉煌到停摆的全过程。我们将剥开代码的表层,深入字节跳动的组织架构变革、内部团队的博弈(赛马机制)、以及中国互联网大厂特有的“KPI 开源”文化,为您还原整件事情的全貌。


2. 溯源:Arco Design 的诞生背景与技术野心


要理解 Arco Design 为何走向衰败,首先必须理解它诞生时的宏大野心及其背后的组织推手。Arco 并不仅仅是一个简单的 UI 库,它是字节跳动在高速扩张期,为了解决内部极其复杂的国际化与商业化业务需求而孵化的产物。


1.png


2.1 “务实的浪漫主义”:差异化的产品定位


Arco Design 在推出时,鲜明地提出了“务实的浪漫主义”这一设计哲学。这一口号的提出,实际上是为了在市场上与阿里巴巴的 Ant Design 进行差异化竞争。



  • Ant Design 的困境:作为行业标准,Ant Design 以“确定性”著称,其风格克制、理性,甚至略显单调。虽然极其适合金融和后台管理系统,但在需要更强品牌表达力和 C 端体验感的场景下显得力不从心。

  • Arco 的切入点:字节跳动的产品基因(如抖音、TikTok)强调视觉冲击力和用户体验的流畅性。Arco 试图在中后台系统中注入这种基因,主张在解决业务问题(务实)的同时,允许设计师发挥更多的想象力(浪漫)。


这种定位在技术层面体现为对 主题定制(Theming) 的极致追求。Arco Design 并没有像传统库那样仅仅提供几个 Less 变量,而是构建了一个庞大的“Design Lab”平台,允许用户在网页端通过可视化界面细粒度地调整成千上万个 Design Token,并一键生成代码。这种“设计即代码”的早期尝试,是 Arco 最核心的竞争力之一。


2.2 组织架构:GIP UED 与架构前端的联姻


Arco Design 的官方介绍中明确指出,该系统是由 字节跳动 GIP UED 团队架构前端团队(Infrastructure FrontEnd Team) 联合推出的。这一血统注定了它的命运与“GIP”这个业务单元的兴衰紧密绑定。


2.2.1 GIP 的含义与地位


“GIP” 通常指代 Global Internet Products(全球互联网产品)或与之相关的国际化/商业化业务部门。在字节跳动 2019-2021 年的扩张期,这是一个充满活力的部门,负责探索除了核心 App(抖音/TikTok)之外的各种创新业务,包括海外新闻应用(BuzzVideo)、办公套件、以及各种尝试性的出海产品。



  • UED 的话语权:在这一时期,GIP 部门拥有庞大的设计师团队(UED)。为了统一各条分散业务线的设计语言,UED 团队急需一套属于自己的设计系统,而不是直接沿用外部的 Ant Design。

  • 技术基建的配合:架构前端团队的加入,为 Arco Design 提供了工程化落地的保障。这种“设计+技术”的双驱动模式,使得 Arco 在初期展现出了极高的完成度,不仅有 React 版本,还同步推出了 Vue 版本,甚至包括移动端组件库。


2.3 黄金时代的技术堆栈


在 2021 年左右,Arco Design 的技术选型是极具前瞻性的,这也是它能迅速获得 5.5k Star 的原因之一:



  • 全链路 TypeScript:所有组件均采用 TypeScript 编写,提供了优秀的类型推导体验,解决了当时 Ant Design v4 在某些复杂场景下类型定义不友好的痛点。

  • 双框架并进:@arco-design/web-react 和 @arco-design/web-vue 保持了高度统一的 API 设计和视觉风格。这对于那些技术栈不统一的大型公司极具吸引力,意味着设计规范可以跨框架复用。

  • 生态闭环:除了组件库,Arco 还发布了 arco-cli(脚手架)、Arco Pro(中后台模板)、IconBox(图标管理平台)以及 Material Market(物料市场)。这表明团队不仅是在做一个库,而是在构建一个类似 Salesforce Lightning 或 SAP Fiori 的企业级生态。


然而,正是这种庞大的生态铺设,为日后的维护埋下了巨大的隐患。当背后的组织架构发生震荡时,维持如此庞大的产品矩阵所需的资源将变得不可持续。


3. 停摆的证据:基于数据与现象的法医式分析


尽管字节跳动从未发布过一份正式的“Arco Design 停止维护声明”,但通过对代码仓库、社区反馈以及基础设施状态的深入分析,我们可以断定该项目已进入实质性的“脑死亡”状态。


3.1 代码仓库的“心跳停止”


对 GitHub 仓库 arco-design/arco-design (React) 和 arco-design/arco-design-vue (Vue) 的提交记录分析显示,活跃度在 2023 年底至 2024 年初出现了断崖式下跌。


3.png


3.1.1 提交频率分析


虽然 React 版本的最新 Release 版本号为 2.66.8(截至文章撰写时),但这更多是惯性维护。



  • 核心贡献者的离场:早期的高频贡献者(如 sHow8e、jadelike-wine 等)在 2024 年后的活跃度显著降低。许多提交变成了依赖项升级(Dependabot)或极其微小的文档修复,缺乏实质性的功能迭代。

  • Vue 版本的停滞:Vue 版本的状态更为糟糕。最近的提交多集中在构建工具迁移(如迁移到 pnpm)或很久以前的 Bug 修复。核心组件的 Feature Request 长期无人响应。


3.1.2 积重难返的 Issue 列表


Issue 面板是衡量开源项目生命力的体温计。目前,Arco Design 仓库中积累了超过 330 个 Open Issue。



  • 严重的 Bug 无人修复:例如 Issue #3091 “tree-select 组件在虚拟列表状态下搜索无法选中最后一个” 和 Issue #3089 “table 组件的 default-expand-all-rows 属性设置不生效”。这些都是影响生产环境使用的核心组件 Bug,却长期处于 Open 状态。

  • 社区的绝望呐喊:Issue #3090 直接以 “又一个没人维护的 UI 库” 为题,表达了社区用户的愤怒与失望。更有用户在 Discussion 中直言 “这个是不是 KPI 项目啊,现在维护更新好像都越来越少了”。这种负面情绪的蔓延,通常是一个项目走向终结的社会学信号。


3.2 基础设施的崩塌:IconBox 事件


如果说代码更新变慢还可以解释为“功能稳定”,那么基础设施的故障则是项目被放弃的直接证据。



  • IconBox 无法发布:Issue #3092 指出 “IconBox 无法发布包了”。IconBox 是 Arco 生态中用于管理和分发自定义图标的 SaaS 服务。这类服务需要后端服务器、数据库以及运维支持。

  • 含义解读:当一个大厂开源项目的配套 SaaS 服务出现故障且无人修复时,这不仅仅是开发人员没时间的问题,而是意味着服务器的预算可能已经被切断,或者负责运维该服务的团队(GIP 相关的基建团队)已经被解散。这是项目“断供”的最强物理证据。


3.3 文档站点的维护降级


Arco Design 的文档站点虽然目前仍可访问,但其内容更新已经明显滞后。例如,关于 React 18/19 的并发特性支持、最新的 SSR 实践指南等现代前端话题,在文档中鲜有提及。与竞争对手 Ant Design 紧跟 React 官方版本发布的节奏相比,Arco 的文档显得停留在 2022 年的时光胶囊中。


4. 深层归因:组织架构变革下的牺牲品


Arco Design 的陨落,本质上不是技术失败,而是组织架构变革的牺牲品。要理解这一点,我们需要将视线从 GitHub 移向字节跳动的办公大楼,审视这家巨头在过去三年中发生的剧烈动荡。


2.png


4.1 “去肥增瘦”战略与 GIP 的解体


2022 年至 2024 年,字节跳动 CEO 梁汝波多次强调“去肥增瘦”战略,旨在削减低效业务,聚焦核心增长点。这一战略直接冲击了 Arco Design 的母体——GIP 部门。


4.1.1 战略投资部的解散与业务收缩


2022 年初,字节跳动解散了战略投资部,并将原有的投资业务线员工分流。这一动作标志着公司从无边界扩张转向防御性收缩。紧接着,教育(大力教育)、游戏(朝夕光年)以及各类边缘化的国际化尝试业务(GIP 的核心腹地)遭遇了毁灭性的裁员。


4.1.2 GIP 团队的消失


在多轮裁员中,GIP 及其相关的商业化技术团队是重灾区。



  • 人员流失:Arco Design 的核心维护者作为 GIP UED 和架构前端的一员,极有可能在这些轮次的“组织优化”中离职,或者被转岗到核心业务(如抖音电商、AI 模型 Doubao)以保住职位。

  • 业务目标转移:留下来的人员也面临着 KPI 的重置。当业务线都在为生存而战,或者全力以赴投入 AI 军备竞赛时,维护一个无法直接带来营收的开源 UI 库,显然不再是绩效考核中的加分项,甚至是负担。


4.2 内部赛马机制:Arco Design vs. Semi Design


字节跳动素以“APP 工厂”和“内部赛马”文化著称。这种文化不仅存在于 C 端产品中,也渗透到了技术基建领域。Arco Design 的停摆,很大程度上是因为它在与内部竞争对手 Semi Design 的博弈中败下阵来。


4.2.1 Semi Design 的崛起


Semi Design 是由 抖音前端团队MED 产品设计团队 联合推出的设计系统。



  • 出身显赫:与 GIP 这个边缘化的“探索型”部门不同,Semi Design 背靠的是字节跳动的“现金牛”——抖音。抖音前端团队拥有极其充裕的资源和稳固的业务地位。

  • 业务渗透率:Semi Design 官方宣称支持了公司内部“近千个平台产品”,服务 10 万+ 用户。它深度嵌入在抖音的内容生产、审核、运营后台中。这些业务是字节跳动的生命线,因此 Semi Design 被视为“核心资产”。


4.2.2 为什么 Arco 输了?


在资源收缩期,公司高层显然不需要维护两套功能高度重叠的企业级 UI 库。选择保留哪一个,不仅看技术优劣,更看业务绑定深度。



  • 技术路线之争:Semi Design 在 D2C(Design-to-Code)领域走得更远,提供了强大的 Figma 插件,能直接将设计稿转为 React 代码。这种极其强调效率的工具链,更符合字节跳动“大力出奇迹”的工程文化。

  • 归属权:Arco 属于 GIP,GIP 被裁撤或缩编;Semi 属于抖音,抖音如日中天。这几乎是一场没有悬念的战役。当 GIP 团队分崩离析,Arco 自然就成了没人认领的“孤儿”。


4.3 中国大厂的“KPI 开源”陷阱


Arco Design 的命运也折射出中国互联网大厂普遍存在的“KPI 开源”现象。



  • 晋升阶梯:在阿里的 P7/P8 或字节的 2-2/3-1 晋升答辩中,主导一个“行业领先”的开源项目是极具说服力的业绩。因此,很多工程师或团队 Leader 会发起此类项目,投入巨大资源进行推广(刷 Star、做精美官网)。

  • 晋升后的遗弃:一旦发起人成功晋升、转岗或离职,该项目的“剩余价值”就被榨干了。接手的新人往往不愿意维护“前人的功劳簿”,更愿意另起炉灶做一个新的项目来证明自己。

  • Arco 的轨迹:Arco 的高调发布(2021年)恰逢互联网泡沫顶峰。随着 2022-2024 年行业进入寒冬,晋升通道收窄,维护开源项目的 ROI(投入产出比)变得极低,导致项目被遗弃。


5. 社区自救的幻象:为何没有强有力的 Fork?


面对官方的停摆,用户自然会问:既然代码是开源的(MIT 协议),为什么没有人 Fork 出来继续维护?调查显示,虽然存在一些零星的 Fork,但并未形成气候。


5.png


5.1 Fork 的现状调查


通过对 GitHub 和 Gitee 的检索,我们发现了一些 Fork 版本,但并未找到具备生产力的社区继任者。



  • vrx-arco:这是一个名为 vrx-arco/arco-design-pro 的仓库,声称是 "aro-design-vue 的部分功能扩展"。然而,这更像是一个补丁集,而不是一个完整的 Fork。它主要解决特定开发者的个人需求,缺乏长期维护的路线图。

  • imoty_studio/arco-design-designer:这是一个基于 Arco 的表单设计器,并非组件库本身的 Fork。

  • 被动 Fork:GitHub 显示 Arco Design 有 713 个 Fork。经抽样检查,绝大多数是开发者为了阅读源码或修复单一 Bug 而进行的“快照式 Fork”,并没有持续的代码提交。


5.2 为什么难以 Fork?


维护一个像 Arco Design 这样的大型组件库,其门槛远超普通开发者的想象。



  1. Monorepo 构建复杂度:Arco 采用了 Lerna + pnpm 的 Monorepo 架构,包含 React 库、Vue 库、CLI 工具、图标库等多个 Package。其构建脚本极其复杂,往往依赖于字节内部的某些环境配置或私有源。外部开发者即使拉下来代码,要跑通完整的 Build、Test、Doc 生成流程都非常困难。

  2. 生态维护成本:Arco 的核心优势在于 Design Lab 和 IconBox 等配套 SaaS 服务。Fork 代码容易,但 Fork 整个后端服务是不可能的。失去了 Design Lab 的 Arco,就像失去了灵魂的空壳,吸引力大减。

  3. 技术栈锁定:Arco 的一些底层实现可能为了适配字节内部的微前端框架或构建工具(如 Modern.js)做了特定优化,这增加了通用化的难度。


因此,社区更倾向于迁移,而不是接盘


6. 用户生存指南:现状评估与迁移策略


对于目前仍在使用 Arco Design 的团队,局势十分严峻。随着 React 19 的临近和 Vue 3 生态的演进,Arco 将面临越来越多的兼容性问题。


6.1 风险评估表


风险维度风险等级具体表现
安全性🔴 高危依赖的第三方包(如 lodash, async-validator 等)若爆出漏洞,Arco 不会发版修复,需用户手动通过 resolutions 强行覆盖。
框架兼容性🔴 高危React 19 可能会废弃某些 Arco 内部使用的旧生命周期或模式;Vue 3.5+ 的新特性无法享受。
浏览器兼容性🟠 中等新版 Chrome/Safari 的样式渲染变更可能导致 UI 错位,无人修复。
基础设施⚫ 已崩溃IconBox 无法上传新图标,Design Lab 可能随时下线,导致主题无法更新。

6.png


6.2 迁移路径推荐


方案 A:迁移至 Semi Design(推荐指数:⭐⭐⭐⭐)


如果你是因为喜欢字节系的设计风格而选择 Arco,那么 Semi Design 是最自然的替代者。



  • 优势:同为字节出品,设计语言的命名规范和逻辑有相似之处。Semi 目前维护活跃,背靠抖音,拥有强大的 D2C 工具链。

  • 劣势:API 并非 100% 兼容,仍需重构大量代码。且 Semi 主要是 React 优先,Vue 生态支持相对较弱(主要靠社区适配)。


7.png


方案 B:迁移至 Ant Design v5/v6(推荐指数:⭐⭐⭐⭐⭐)


如果你追求极致的稳定和长期的维护保障,Ant Design 是不二之选。



  • 优势:行业标准,庞大的社区,Ant Gr0up 背书。v5 版本引入了 CSS-in-JS,在定制能力上已经大幅追赶 Arco 的 Design Lab。

  • 劣势:设计风格偏保守,需要设计师重新调整 UI 规范。


方案 C:本地魔改(推荐指数:⭐)


如果项目庞大无法迁移,唯一的出路是将 @arco-design/web-react 源码下载到本地 packages 目录,作为私有组件库维护。



  • 策略:放弃官方更新,仅修复阻塞性 Bug。这需要团队内有资深的前端架构师能够理解 Arco 的源码。


4.png


7. 结语与启示


Arco Design 的故事是现代软件工程史上的一个典型悲剧。它证明了在企业级开源领域,康威定律(Conway's Law) 依然是铁律——软件的架构和命运取决于开发它的组织架构。


当 GIP 部门意气风发时,Arco 是那颗最耀眼的星,承载着“务实浪漫主义”的理想;当组织收缩、业务调整时,它便成了由于缺乏商业造血能力而被迅速遗弃的资产。对于技术决策者而言,Arco Design 的教训是惨痛的:在进行技术选型时,不能仅看 README 上的 Star 数或官网的精美程度,更要审视项目背后的组织生命力维护动机


8.png


目前来看,Arco Design 并没有复活的迹象,社区也没有出现强有力的接棒者。这套组件库正在数字化浪潮的沙滩上,慢慢风化成一座无人问津的丰碑。


作者:HexCIer
来源:juejin.cn/post/7582879379441745963
收起阅读 »

桌面应用开发,Flutter 与 Electron如何选

web
前言:这一年来我基本处于断更的状态,我知道在AI时代,编码的成本已经变得越来越低,技术分享的流量必然会下降。但这依然是一个艰难的过程,日常斥责自己没有成长,没有作品。 除了流量问题、巨量的工作,更多的原因是由于技术栈的变化。我开始使用Electron编写一个重...
继续阅读 »

前言:这一年来我基本处于断更的状态,我知道在AI时代,编码的成本已经变得越来越低,技术分享的流量必然会下降。但这依然是一个艰难的过程,日常斥责自己没有成长,没有作品。

除了流量问题、巨量的工作,更多的原因是由于技术栈的变化。我开始使用Electron编写一个重要的AI产品,并且在 Flutter 与 Electron 之间来回拉扯......



背景


我们对 Flutter 技术的应用,不仅是在移动端APP,在我们的终端设备也用来做 OS 应用,跨Android、Windows、Linux系统。

在 Flutter 上,我们是有所沉淀的,但是当我们决定研发一款重要的PC应用时,依然产生了疑问:Flutter 这门技术,真的能满足我们在核心桌面应用的研发需求吗?

最终,基于官方能力、技术生态、roadmap等一系列原因,我们放弃在核心应用上使用 Flutter,转而代之选择了 Electron


这篇文章将从这几个月使用 Electron 的切实体验,从不同角度,对 FlutterElectron 这两款支持跨端桌面应用开发技术,做一个详细的对比。


Flutter VS Electron


维度FlutterElectron
发布时间2021 年 3 月 宣布支持桌面端2013 年 4 月发布,发布即支持
核心场景移动APP跨端桌面应用跨端
官方网站flutter.devhttp://www.electronjs.org
开发文档docs.flutter.devhttp://www.electronjs.org/docs
插件包管理Pub(pub.dev),提供大量 UI 组件、工具类库npm(http://www.npmjs.com),依赖前端生态,插件丰富(如 electron-builder 打包工具)
研发组织GoogleGithub

方案成熟度


毫无疑问,在方案成熟度上 Electron 是碾压 Flutter 的存在。


1. 多进程能力



  • Flutter 目前还是单进程的能力,只能通过创建 isolate 来实现部分耗时任务,但是内存也是不共享的。

  • Electron 集成了 Nodejs 服务,自带多进程的能力,且提供了完整的跨进程机制IPC「Inter-Process Communication」)。


2. 多窗口支持



  • Flutter 目前不支持多窗口。由于其是自绘引擎,本身还是依赖原生进程提供的桌面窗口,所以需要原生与 Flutter 引擎不断的进行沟通对接,才能很好的使用多窗口能力。

    目前官方只是提供了 demo 来验证多窗口的可行性,但截止发文还没有办法在公版试用。

  • Electron 将 Chromium 的核心模块打包到发行包中,借助浏览器的能力,可以随意开辟新的窗口(如: BrowserWindow


3. 开发语言



  • Flutter 使用dart语言开发,采用声明式UI进行布局,插件管理使用官方的 pub 社区,学习和使用成本不算高。

  • Electron 使用JavaScript/TypeScript + HTML/CSS 的前端技术栈进行开发,社区也完全跟前端一致,非常丰富但鱼龙混杂


4. 原生能力的支持



  • Flutter 本质是一个 UI 框架,原生能力需要通过编写插件去调用,或者通过 FFI 调用,成本是很高的,你很难找到一个懂多端原生技术的开发。

  • Electron 有 node 环境,node.js 很多原生模块,可以直接调用到系统的能力,非常的高效。


开发体验和技术生态


1. 调试工具



  • Flutter 的调试工具,主要是依赖 IDE 本身的断点调试能力,以及自研的Flutter Inspector、devTools。

    在UI定位、性能监控方面,基本可以满足。但由于是个 UI 框架,对于原生容器是无法进行调试的,这在混合开发过程中是个比较大的痛点。

  • Electron 就是个浏览器,对于主进程和node子进程,有 Inspect 的机制; UI 层就更方便了,就是浏览器的调试器一模一样。生产环境下调试成本也低。


2. 打包编译


Flutter 是通过自绘引擎生成原生应用包,而 Electron 是将网页技术(HTML/CSS/JS)包裹在 Chromium 内核中。


底层技术架构的区别,直接决定了 Electron 的打包相对 Flutter 有些困难,且包体积很大。


对比维度FlutterElectron
打包原理编译成目标平台的原生二进制代码,搭配自绘引擎(Skia)封装 Chromium 内核 + Node.js 环境,运行网页资源
最终产物与原生应用格式一致(如 .apk/.ipa/.exe)包含浏览器内核的独立应用包
跨平台方式一份代码编译成多平台原生包,需分别打包一份代码打包成多平台包,内核随应用分发
应用体积较小(基础包约 10-20MB)较大(基础包约 50-100MB,内核占主要体积)

3. 官方和社区的活跃性



  • Flutter 官方在桌面端的推进很慢,很多基础能力都没有太多的推进。同时在 roadmap 中,重心都偏向移动端和 web 端。

  • Electron 由于产品的体量和成熟度,稳定的在更新,每个版本都会带来一些新的特性。
    image.png


4. 研发团队


技能维度FlutterElectron
核心语言Dart,需理解其异步逻辑、Widget 组件化思想JavaScript/TypeScript,前端开发者可无缝衔接
UI 技术Flutter 内置 Widget 体系,需学习其布局(Row/Column)、状态管理(Provider/Bloc)HTML/CSS,可复用前端生态(Vue/React/Element UI 等)
原生交互需了解 Android(Kotlin/Java)、iOS(Swift/OC)基础,复杂功能需写原生插件依赖 Node.js 模块或现成插件,无需深入原生开发
工程化工具依赖 Flutter CLI、Android Studio/Xcode(打包配置)依赖 npm/yarn、webpack/vite(前端构建工具)

可以看出,Flutter至少需要 1-2 名熟悉 Dart 的开发者,还需要有原生开发能力,技术门槛是比较高的;而 Electron 以前端开发者为主,熟悉 Node.js 即可完成所有开发,是可以快速上手的


同时前端开发也比 Flutter 开发要更容易招聘


结语


笔者本身是 Flutter 的忠实维护者,我认为 Flutter 的 Impeller 图形渲染引擎将不断完善,能在各个端达到更好的渲染速度和效果;同时 Flutter 目前的多窗口方案,让我们可以充分的相信可以多个窗口共用一份内存,而不需要通过进程间通信机制


但是,在 Flutter 暂未成熟的阶段,桌面核心产品还是用 Electron 进行开发会更加合适。我们 也期待未来 Electron 可以多集成WebAssembly来提升计算密集型任务的性能,减少 Chromium 内核的高内存占用。


作者:Karl_wei
来源:juejin.cn/post/7578719771589066762
收起阅读 »

BOE(京东方)“焕新2026”年终媒体智享会落地深圳 绘就显示产业生态新蓝图

12月11日,BOE(京东方)“焕新2026”年终媒体智享会系列活动在深圳成功举办。作为粤港澳大湾区的科技创新与产业协同高地,此次深圳活动以“焕新·向融而行”为主题,聚焦BOE(京东方)生态合作与融合发展,BOE(京东方)高级副总裁、显示器件及物联网创新业务前...
继续阅读 »

12月11日,BOE(京东方)“焕新2026”年终媒体智享会系列活动在深圳成功举办。作为粤港澳大湾区的科技创新与产业协同高地,此次深圳活动以“焕新·向融而行”为主题,聚焦BOE(京东方)生态合作与融合发展,BOE(京东方)高级副总裁、显示器件及物联网创新业务前台负责人刘竞出席,系统阐释了BOE(京东方)如何通过赋能产业链实现生态协同与价值共创。此次活动的成功举办,彰显了BOE(京东方)作为全球领先的物联网创新企业,持续以技术创新赋能生态合作伙伴,引领行业迈向融合共生的新发展阶段。

四大维度协同 绘就“焕新2026”产业蓝图

面对全球市场化发展新趋势,BOE(京东方)正积极将技术实力转化为更具感知度的品牌价值主张。此次“焕新2026”年终媒体智享会,BOE(京东方)从AI、技术、生态、可持续四大维度全面勾勒“焕新2026”的完整蓝图,为产业高质发展注入强劲动力。在“AI焕新”方面,BOE(京东方)AI+战略,为“屏之物联”战略注入核心动能;在“技术焕新”方面,BOE(京东方)通过ADS Pro、f-OLED、α-MLED三大技术品牌,不断推动技术升级;在“生态焕新”方面,BOE(京东方)携手全球合作伙伴共拓新技术、新应用和新场景,实现从技术赋能到生态共赢的跃迁;在“可持续焕新”方面,BOE(京东方)将可持续发展升级为企业发展的核心之一,发布了中国显示行业首个可持续发展品牌“ONE”以及与之相呼应的六大支柱的可持续发展战略。

深化生态开放合作 构建产业创新生态

多年来,BOE(京东方)通过深化产业生态协同、赋能全球合作伙伴以及前瞻布局市场趋势,携手推动显示产业从技术竞争迈向价值共创新阶段。活动现场,BOE(京东方)高级副总裁、显示器件及物联网创新业务前台负责人刘竞围绕《焕新生态 融合共生》发表主题演讲,系统阐述了BOE(京东方)通过技术品牌赋能、产业链协同与行业标准制定,与生态伙伴共同构建高价值增长新格局。他表示,市场升维,源于与伙伴的同频共振。从电视、显示器到手机、笔记本电脑,客户的每一次认可与选择,不仅是对一块屏幕的肯定,更是让“Powered by BOE”成为BOE(京东方)携手合作伙伴定义价值、共创健康产业生态的坚实印记。

作为全球物联网创新企业,BOE(京东方)始终坚持市场化、国际化、专业化发展道路,提供极具价值力的合作伙伴赋能“屏台”,深度联动全球超过5000家生态伙伴,从联合研发、场景共创到解决方案落地,实现从“交付产品”到“交付价值”的跨越,构建协同创新的伙伴生态。在创新引领领域,BOE(京东方)以客户需求为导向,不断进行显示技术的升级。例如,BOE(京东方)基于独有的ADS Pro技术与创维推出了全新极黑广角类纸屏,并全球首发于创维A5F Pro系列新品,在高环境光对比度、全视角无色偏等优势的基础上,实现更真实、更清晰、更护眼的画质表现。在f-OLED方面,当前BOE(京东方)柔性OLED屏幕出货量已超过5亿片,近两年几乎所有一线品牌的高端旗舰折叠机型,均搭载BOE(京东方)折叠屏。同时BOE(京东方)积极布局中国首条6代柔性OLED生产线及中国首条8.6代OLED生产线,持续引领产业升维发展。而在α-MLED方面,BOE(京东方)通过室内平台、户外平台、创新应用平台三大产品平台,打造COB、COG、SMD、Micro LED高竞争力产品线,全面满足多元化的市场需求。今年3月,珠海京东方晶芯COB成功量产,推动了BOE(京东方)向COB产业领先企业快速迈进。在开放包容领域,BOE(京东方)依托“技术+品牌”双价值驱动,推动品牌客户高端合作,并联合合作伙伴积极发掘创新合作模式,共拓市场边界。例如BOE(京东方)携手一加手机联合打造东方屏3.0,该屏幕同时具备全球首款原生165Hz超高刷新率,将手机屏幕正式带入超高刷时代;携手联想发布高端商用显示器联想ThinkVision P系列新品,凭借BOE(京东方)独供Oxide氧化物技术,打造商用显示器领域新标杆。而在永续生态领域,BOE(京东方)着力搭建绿色发展平台,例如试点打造“零碳产品”,通过推动上游供应链原材减碳、厂内生产减碳、剩余碳量抵消等方式,实现NB显示面板综合碳排放为零,并获得了绿交所颁发的碳中和证书,共创绿色可持续发展的生态链。

与此同时,BOE(京东方)坚持通过技术创新和服务优化满足市场和客户期待,通过产品性能、高端品质、用户体验、生态合作与绿色责任五个维度的坚实支撑,持续巩固“好屏认准BOE”的市场共识。产品性能上,BOE(京东方)依托ADS Pro、f-OLED及α-MLED三大显示技术品牌持续为伙伴提供高端化突破的核心引擎;高端品质上,以近乎严苛的可靠性与耐用性标准赢得信赖;用户体验上,围绕健康护眼、沉浸画质等用户需求,与合作伙伴共同定义终端感受;生态合作上,BOE(京东方)深度融入全球一线品牌创新链,成为客户信赖的战略伙伴;绿色责任上,通过绿色技术、低碳材料与全生命周期碳管理,为客户提供高性能、低功耗的可持续解决方案。

在产业协作持续深化的趋势下,BOE(京东方)联合合作伙伴共同发布“百吋大屏三真承诺”,致力于为消费者提供真品质、真体验和真服务的极致大屏体验。同时,BOE(京东方)还携手京东等合作伙伴联合发起“高价值生态产业联盟”,推动行业从价格竞争向价值共创跃迁,以开放协作终结零和博弈,以可持续动能重塑增长根基,引领显示产业步入融合共生的良性发展新阶段。

面向未来,BOE(京东方)将始终与全球生态伙伴紧密携手,积极推动先进显示技术的标准制定,携手合作伙伴加速物联网应用场景的深度融合与落地,共同构建“Powered by BOE”的产业价值创新生态,持续带动产业价值升维,引领全球显示产业迈向更高质量、更可持续的新未来。

收起阅读 »

偷看浏览器后台,发现它比我忙多了

web
为啥以前 IE 老“崩溃”,而现在开一堆标签页的 Chrome 还能比较稳?答案都藏在浏览器的「进程模型」里。 作为一个还在学习前端的同学,我经常听到几个关键词: 进程、线程、多进程浏览器、渲染进程、V8、事件循环…… 下面就是我目前的理解,算是一篇学习笔记...
继续阅读 »

为啥以前 IE 老“崩溃”,而现在开一堆标签页的 Chrome 还能比较稳?答案都藏在浏览器的「进程模型」里。



作为一个还在学习前端的同学,我经常听到几个关键词:

进程、线程、多进程浏览器、渲染进程、V8、事件循环……


下面就是我目前的理解,算是一篇学习笔记式的分享


一、先把概念捋清:进程 vs 线程



  • 进程(Process)



    • 操作系统分配资源的最小单位。

    • 拥有独立的内存空间、句柄、文件等资源。

    • 不同进程间默认互相隔离,通信要通过 IPC。



  • 线程(Thread)



    • CPU 调度、执行代码的最小单位。

    • 共享所属进程的资源(内存、文件句柄等)。

    • 一个进程里可以有多个线程并发执行。




简单理解:



  • 进程 = 一个“应用实例” (开一个浏览器窗口就是一个进程)。

  • 线程 = 应用里的很多“小工人” (一个负责渲染页面,一个负责网络请求……)。


二、单进程浏览器:旧时代的 IE 模型


早期的浏览器(如旧版 IE)基本都是单进程架构:整个浏览器只有一个进程,里面开多个线程来干活。


可以想象成这样一张图(对应你给的“单进程浏览器”那张图):



  • 顶部是「代码 / 数据 / 文件」等资源。

  • 下面有多个线程:



    • 页面线程:负责页面渲染、布局、绘制。

    • 网络线程:负责网络请求。

    • 其他线程:例如插件、定时任务等。



  • 所有线程共享同一份进程资源。


在这个模型下:



  • 页面渲染、JavaScript 执行、插件运行,都挤在同一个进程里。

  • 某个插件或脚本一旦崩溃、死循环、内存泄漏,整个浏览器都会被拖垮

  • 多开几个标签页,本质上依旧是同一个进程里的不同页面线程, “一荣俱荣,一损俱损”


这也是很多人对 IE 的经典印象:



“多开几个页面就卡死,崩一次,所有标签页一起消失”。



三、多进程浏览器:Chrome 的现代架构


Chrome 采用的是多进程、多线程混合架构。打开浏览器时,大致会涉及这些进程:



  • 浏览器主进程(Browser Process)



    • 负责浏览器 UI、地址栏、书签、前进后退等。

    • 管理和调度其他子进程(类似一个大管家)。

    • 负责部分存储、权限管理等。



  • 渲染进程(Render Process)



    • 核心任务:把 HTML / CSS / JavaScript 变成用户可以交互的页面。

    • 布局引擎(如 Blink)和 JS 引擎(如 V8)都在这里。

    • 默认情况下,每个标签页会对应一个独立的渲染进程



  • GPU 进程(GPU Process)



    • 负责 2D / 3D 绘制和加速(动画、3D 变换等)。

    • 统一为浏览器和各渲染进程提供 GPU 服务。



  • 网络进程(Network Process)



    • 负责资源下载、网络请求、缓存等。



  • 插件进程(Plugin Process)



    • 负责运行如 Flash、扩展等插件代码,通常放在更严格的沙箱里。




你给的第一张图,其实就是这么一个多进程架构示意图:中间是主进程,两侧是渲染、网络、GPU、插件等子进程,有的被沙箱保护。


download.png


四、单进程 vs 多进程:核心差异一览(表格对比)


下面这张表,把旧式单进程浏览器和现代多进程浏览器的差异总结出来,适合在文章中重点展示:


对比维度单进程浏览器(典型:旧版 IE)多进程浏览器(典型:Chrome)
进程模型整个浏览器基本只有一个进程,多标签页只是不同线程浏览器主进程 + 多个子进程(渲染、网络、GPU、插件…),标签页通常独立渲染进程
稳定性任意线程(脚本、插件)崩溃,可能拖垮整个进程,浏览器整体崩溃某个标签页崩溃只影响对应渲染进程,其他页面基本不受影响
安全性代码都在同一进程运行,权限边界模糊,攻击面大利用多进程 + 沙箱:渲染进程、插件进程被限制访问系统资源,需要通过主进程/IPC
性能体验多标签共享资源,某个页面卡顿,容易拖慢整体;UI 和页面渲染耦合严重不同进程之间可以更好地利用多核 CPU,重页面操作不会轻易阻塞整个浏览器 UI
内存占用单进程内存相对集中,但一旦泄漏难以回收;崩溃时损失全部状态多进程会有一定内存冗余,但某个进程关闭/崩溃后,其内存可被系统直接回收
插件影响插件崩溃 = 浏览器崩溃,体验极差插件独立进程 + 沙箱,崩溃影响有限,可以单独重启
维护与扩展所有模块耦合在一起,改动风险大进程边界天然分层,更利于模块化演进和大规模工程化

download.png


五、别被“多进程”骗了:JS 依然是单线程


聊到这里,很多同学容易混淆一个点:



浏览器是多进程的,那 JavaScript 是不是也多线程并行执行了?



答案是否定的:主线程上的 JavaScript 依然是单线程模型。区别在于:



  • 渲染进程内部,有一个主线程负责:



    • 执行 JavaScript。

    • 页面布局、绘制。

    • 处理用户交互(点击、输入等)。



  • JS 代码仍然遵循:同步任务立即执行,异步任务丢进任务队列,由事件循环(Event Loop)调度


为什么要坚持单线程?



  • DOM 是单线程模型,多个线程同时改 DOM,锁会非常复杂。

  • 前端开发心智成本可控;不必像多线程语言那样到处考虑锁和竞态条件。


多进程架构只是把:



  • “这个页面的主线程 + 渲染 + JS 引擎”

    放在一个单独的进程里(渲染进程)。


这也是为什么:



  • 一个页面 JS 写了死循环,会卡死那一个标签页。

  • 但其他标签页通常还能正常使用,因为它们在完全不同的渲染进程内。


六、从架构看体验:为什么我们更喜欢现在的浏览器?


站在前端开发者角度,多进程架构带来的直接收益有:



  • 更好的容错性

  • 更高的安全等级

  • 更顺滑的交互体验

  • 更容易工程化演进


当然,代价也很现实:多进程 = 更高的内存占用。这也是为什么:



  • 多开几十个标签,任务管理器里能看到很多浏览器相关进程。

  • 但换来的是更好的稳定性、安全性和扩展性——在现代硬件下,这是可以接受的 trade-off。


七、总结



  • 早期浏览器采用单进程 + 多线程模式,所有页面、脚本、插件都在同一个进程里,一旦出问题就“全军覆没”。

  • 现代浏览器(代表是 Chrome)使用多进程架构:主进程负责调度,各个渲染、网络、GPU、插件进程各司其职,并通过沙箱强化隔离。

  • 尽管浏览器整体是多进程的,但单个页面里的 JavaScript 依然是单线程 + 事件循环模型,这点没有变。

  • 从用户体验、前端开发、安全性、稳定性来看,多进程架构几乎是全面碾压旧时代单进程浏览器的一次升级。


作者:T___T
来源:juejin.cn/post/7580263284338311209
收起阅读 »

数组判断?我早不用instanceof了,现在一行代码搞定!

web
传统方案 1. Object.prototype.toString.call 方法 原理:通过调用 Object.prototype.toString.call(obj) ,判断返回值是否为  [object Array] 。 function isArray...
继续阅读 »

传统方案


1. Object.prototype.toString.call 方法


原理:通过调用 Object.prototype.toString.call(obj) ,判断返回值是否为  [object Array] 。


function isArray(obj){
return Object.prototype.toString.call(obj) === '[object Array]';
}

 
缺陷



  • ES6 引入 Symbol.toStringTag 后,可被人为篡改。例如:


    const obj = {
    [Symbol.toStringTag]: 'Array'
    };
    console.log(Object.prototype.toString.call(obj)); // 输出 [object Array]


  • 若开发通用型代码(如框架、库),该漏洞会导致判断失效。


2. instanceof 方法


原理:判断对象原型链上是否存在 Array 构造函数。


function isArray(obj){
return obj instanceof Array;
}

缺陷



  • 可通过 Object.setPrototypeOf 篡改原型链,导致误判。例如:


    const obj = {};
    Object.setPrototypeOf(obj, Array.prototype);
    console.log(obj instanceof Array); // 输出 true,但 obj 并非真正数组


  • 跨 iframe 场景失效。不同 iframe 中的 Array 构造函数不共享,导致真数组被误判为非数组。例如:


    const frame = document.querySelector('iframe');
    const Array2 = frame.contentWindow.Array;
    const arr = new Array2();
    console.log(arr instanceof Array); // 输出 false,但 arr 是真正数组



ES6 原生方法


方法:使用 Array.isArray 静态方法。


console.log(Array.isArray(arr));

 
优势



  • 该方法由JavaScript引擎内部实现,直接判断对象是否由 Array 构造函数创建,不受原型链、 Symbol.toStringTag  或跨 iframe 影响;

  • 完美解决所有边界场景。


总结


判断数组的方法中 Array.isArray 是唯一准确且无缺陷的方案。其他方法(如Object.prototype.toString.callinstanceof)均存在局限性,仅在特定场景下可用。


作者:南游
来源:juejin.cn/post/7579849892614094884
收起阅读 »

HTML5 自定义属性 data-*:别再把数据塞进 class 里了!

web
前言:由于“无处安放”而引发的混乱 在 HTML5 普及之前,前端开发者为了在 DOM 元素上绑定一些数据(比如用户 ID、商品价格、状态码),可谓是八仙过海,各显神通: 隐藏域流派:到处塞 <input type="hidden" value="12...
继续阅读 »

前言:由于“无处安放”而引发的混乱


在 HTML5 普及之前,前端开发者为了在 DOM 元素上绑定一些数据(比如用户 ID、商品价格、状态码),可谓是八仙过海,各显神通:



  1. 隐藏域流派:到处塞 <input type="hidden" value="123">,导致 HTML 结构像个堆满杂物的仓库。

  2. Class 拼接流派<div class="btn item-id-8848">,然后用正则去解析 class 字符串提取 ID。这简直是在用 CSS 类名当数据库用,类名听了都想离家出走。

  3. 自定义非标属性流派:直接写 <div my_id="123">。虽然浏览器大多能容忍,但这就好比在公共泳池里裸泳——虽然没人抓你,但不合规矩且看着尴尬。


直到 HTML5 引入了 data-*  自定义数据属性,这一切终于有了“官方标准”。




第一阶段:基础——它长什么样?


data-* 属性允许我们在标准 HTML 元素中存储额外的页面私有信息。


1. HTML 写法


语法非常简单:必须以 data- 开头,后面接上你自定义的名称。


codeHtml


<!-- ❌ 错误示范:不要大写,不要乱用特殊符号 -->
<div data-User-Id="1001"></div>

<!-- ✅ 正确示范:全小写,连字符连接 -->
<div
id="user-card"
data-id="1001"
data-user-name="juejin_expert"
data-value="99.9"
data-is-vip="true"
>

用户信息卡片
</div>

2. CSS 中的妙用


很多人以为 data-* 只是给 JS 用的,其实 CSS 也能完美利用它。


场景一:通过属性选择器控制样式


/* 当 data-is-vip 为 "true" 时,背景变金 */
div[data-is-vip="true"] {
background: gold;
border: 2px solid orange;
}

场景二:利用 attr() 显示数据

这是一个非常酷的技巧,可以用来做 Tooltip 或者计数器显示。


div::after {
/* 直接把 data-value 的值显示在页面上 */
content: "当前分值: " attr(data-value);
font-size: 12px;
color: #666;
}



第二阶段:进阶——JavaScript 如何读写?


这才是重头戏。在 JS 中操作 data-* 有两种方式:传统派 和 现代派


1. 传统派:getAttribute / setAttribute


这是最稳妥的方法,兼容性最好(虽然现在也没人要兼容 IE6 了)。


const el = document.getElementById('user-card');

// 读取
const userId = el.getAttribute('data-id'); // "1001"

// 修改
el.setAttribute('data-value', '100');

特点:读出来永远是字符串。哪怕你存的是 100,取出来也是 "100"。


2. 现代派:dataset API (推荐 ✨)


HTML5 为每个元素提供了一个 dataset 对象(DOMStringMap),它将所有的 data-* 属性映射成了对象的属性。


这里有个大坑(或者说是规范),请务必注意:

HTML 中的 连字符命名 (kebab-case)  会自动转换为 JS 中的 小驼峰命名 (camelCase)


const el = document.getElementById('user-card');

// 1. 访问 data-id
console.log(el.dataset.id); // "1001"

// 2. 访问 data-user-name (注意变身了!)
console.log(el.dataset.userName); // "juejin_expert"
// ❌ el.dataset.user-name 是语法错误
// ❌ el.dataset['user-name'] 是 undefined

// 3. 修改数据
el.dataset.value = "200";
// HTML 会自动变成 data-value="200"

// 4. 删除数据
delete el.dataset.isVip;
// HTML 中的 data-is-vip 属性会被移除


💡 敲黑板:dataset 里的属性名不支持大写字母。如果你在 HTML 里写 data-MyValue="1", 浏览器会强制转为小写 data-myvalue,JS 里就得用 dataset.myvalue 访问。所以,HTML 里老老实实全小写吧。





第三阶段:深入——类型陷阱与性能权衡


1. 一切皆字符串


不管你赋给 dataset 什么类型的值,最终都会被转为字符串。


el.dataset.count = 100;        // HTML: data-count="100"
el.dataset.active = true; // HTML: data-active="true"
el.dataset.config = {a: 1}; // HTML: data-config="[object Object]" -> 灾难!

避坑指南



  • 如果你要存数字,取出来时记得 Number(el.dataset.count)。

  • 如果你要存布尔值,判断时不能简单用 if (el.dataset.active),因为 "false" 字符串也是真值!要用 el.dataset.active === 'true'。

  • 千万不要试图在 data-* 里存复杂的 JSON 对象。如果非要存,请使用 JSON.stringify(),但在 DOM 上挂载大量字符串数据会影响性能。


2. 性能考量



  • 读写速度:dataset 的访问速度在现代浏览器中非常快,但在极高频操作下(比如每秒几千次),直接操作 JS 变量肯定比操作 DOM 快。

  • 重排与重绘:修改 data-* 属性会触发 DOM 变更。如果你的 CSS 依赖属性选择器(如 div[data-status="active"]),修改属性可能会触发页面的重排(Reflow)或重绘(Repaint)。




第四阶段:实战——优雅的事件委托


data-value 最经典的用法之一就是在列表项的事件委托中。


需求:点击列表中的“删除”按钮,删除对应项。


<ul id="todo-list">
<li>
<span>学习 HTML5</span>
<!-- 把 ID 藏在这里 -->
<button class="btn-delete" data-id="101" data-action="delete">删除</button>
</li>
<li>
<span>写掘金文章</span>
<button class="btn-delete" data-id="102" data-action="delete">删除</button>
</li>
</ul>

const list = document.getElementById('todo-list');

list.addEventListener('click', (e) => {
// 利用 dataset 判断点击的是不是删除按钮
const { action, id } = e.target.dataset;

if (action === 'delete') {
console.log(`准备删除 ID 为 ${id} 的条目`);
// 这里发送请求或操作 DOM
// deleteItem(id);
}
});

为什么这么做优雅?

你不需要给每个按钮都绑定事件,也不需要去分析 DOM 结构(比如 e.target.parentNode...)来找数据。数据就在元素身上,唾手可得。




总结与“禁忌”


HTML5 的 data-* 属性是连接 DOM 和数据的一座轻量级桥梁。


什么时候用?



  • 当需要把少量数据绑定到特定 UI 元素上时。

  • 当 CSS 需要根据数据状态改变样式时。

  • 做事件委托需要传递参数时。


什么时候别用?(禁忌)



  1. 不要存储敏感数据:用户可以直接在浏览器控制台修改 DOM,千万别把 data-password 或 data-user-token 放在这。

  2. 不要当数据库用:别把几 KB 的 JSON 数据塞进去,那是 JS 变量或者是 IndexDB 该干的事。

  3. SEO 无用:搜索引擎爬虫通常不关心 data-* 里的内容,重要的文本内容还是要写在标签里。




最后一句

代码整洁之道,始于不再乱用 Class。下次再想存个 ID,记得想起那个以 data- 开头的帅气属性。


Happy Coding! 🚀


作者:南山安
来源:juejin.cn/post/7575119254314401818
收起阅读 »

前端发版总被用户说“没更新”?一文搞懂浏览器缓存,彻底解决!

web
有时候我们发了新版,结果用户看到的还是老界面。 你:“我更新了啊!” 用户:“我这儿没变啊!” 然后你俩开始互相怀疑人生。 那咋办?总不能让用户都清缓存吧? 当然不能。 我们得让浏览器自己知道“该换新的了”。 核心思路就一条:让静态资源的文件名变一变。 浏览器...
继续阅读 »

有时候我们发了新版,结果用户看到的还是老界面。
你:“我更新了啊!”
用户:“我这儿没变啊!”
然后你俩开始互相怀疑人生。
那咋办?总不能让用户都清缓存吧?
当然不能。
我们得让浏览器自己知道“该换新的了”。
核心思路就一条:让静态资源的文件名变一变。
浏览器靠文件名判断是不是同一个文件。
文件名变了,它就会重新下载。


方法1:加时间戳(简单粗暴)


以前:


<script src="/js/app.js"></script>

现在:


<script src="/js/app.js?v=20250901"></script>

或者用时间戳:


<script src="/js/app.js?t=1725153600"></script>

发版的时候,改一下vt的值,浏览器看到后发现文件名不一样,就会重新下载。


优点:简单,立马见效

缺点:每次发版都得手动改,容易忘记。


方法2:用构建工具加hash(推荐!)


这是现在最主流的做法。
你用WebpackViteRollup这些工具打包时,它会自动给文件名加一串hash


<script src="/js/app.a1b2c3d.js"></script>

你代码一改,hash就变。

比如下次变成:


<script src="/js/app.e4f5g6h.js"></script>

浏览器看到后发现文件名不一样,会自动拉新文件。


使用时需要检查你的打包配置,确保输出文件带hash。


Vite配置(vite.config.js):


export default defineConfig({
build: {
rollupOptions: {
output: {
entryFileNames: 'assets/[name].[hash].js',
chunkFileNames: 'assets/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]'
}
}
}
})

Vue CLI(vue.config.js):


module.exports = {
filenameHashing: true, // 默认就是 true,别关掉!
}

只要这个开着,JS/CSS 文件名就会变,浏览器就会更新。


优点:全自动,不用操心

优点:用户无感知,体验好

优点:还能利用缓存(没改的文件hash不变,继续用旧的)


来看看Vue的项目


只要你用的是Vue CLI、Vite或 Webpack打包,发版时默认就解决了缓存问题。
因为它们会自动给文件名加hash
比如你打包后:


dist/
├── assets/app.8a2b1f3.js
├── assets/chunk-vendors.a1b2c3d.js
└── index.html

你改了代码,再打包,hash就变了:


assets/app.x9y8z7w.js  # 新文件

虽说是这样,但为啥还有人卡在旧版本?


文件名带hash,但index.html这个入口文件本身可能被缓存了


流程:



  • index.html里引用了app.8a2b1f3.js

  • 用户第一次访问,加载了index.html和对应的JS

  • 你发新版,index.html指向app.x9y8z7w.js

  • 但用户浏览器缓存了旧的index.html,还在引用app.8a2b1f3.js

  • 结果:页面还是旧的


这是入口文件缓存导致的发版无效。


解决方案


方法1:让index.html不被缓存


这是最简单有效的办法。
配置Nginx,让index.html不缓存:


location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}

这样每次用户访问,都会重新下载最新的 index.html,自然就拿到新的 JS 文件名。



注意:其他静态资源(js/css)可以长期缓存,只有 index.html 要禁缓存。



方法2:根据版本号来控制


每一次更新都新建一个文件夹



然后修改Nginx配置


location / {
root /home/server/html/yudao-vue3/version_1_2_5;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}

最后
缓存这事看着小,真出问题能让我们忙半天。
提前设好机制,发版才能睡得香。
搞定!



我是大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!



📌往期精彩


《Elasticsearch 太重?来看看这个轻量级的替代品 Manticore Search》


《只会写 Mapper 就敢说会 MyBatis?面试官:原理都没懂》


《别学23种了!Java项目中最常用的6个设计模式,附案例》


《写给小公司前端的 UI 规范:别让页面丑得自己都看不下去》


《Vue3+TS设计模式:5个真实场景让你代码更优雅》


作者:刘大华
来源:juejin.cn/post/7545252678936100918
收起阅读 »

前端部署,又有新花样?

web
大多数前端开发者在公司里,很少需要直接操心“部署”这件事——那通常是运维或 DevOps 的工作。 但一旦回到个人项目,情况就完全不一样了。写个小博客、搭个文档站,或者搞个 demo 想给朋友看,部署往往成了最大的拦路虎。 常见的选择无非是 Vercel、Ne...
继续阅读 »

大多数前端开发者在公司里,很少需要直接操心“部署”这件事——那通常是运维或 DevOps 的工作。


但一旦回到个人项目,情况就完全不一样了。写个小博客、搭个文档站,或者搞个 demo 想给朋友看,部署往往成了最大的拦路虎。


常见的选择无非是 Vercel、Netlify 或 GitHub Pages。它们表面上“一键部署”,但细节其实并不轻松:要注册平台账号、要配置域名,还得接受平台的各种限制。国内的一些云服务商(比如阿里云、腾讯云)管控更严格,操作门槛也更高。更让人担心的是,一旦平台宕机,或者因为地区网络问题导致访问不稳定,你的项目可能随时“消失”在用户面前。虽然这种情况不常见,但始终让人心里不踏实。


很多时候,我们只是想快速上线一个小页面,不想被部署流程拖累,有没有更好的方法?


一个更轻的办法


前段时间我发现了一个开源工具 PinMe,主打的就是“极简部署”。



它的使用体验非常直接:



  • 不需要服务器

  • 不用注册账号

  • 在项目目录敲一条命令,就能把项目打包上传到 IPFS 网络

  • 很快,你就能拿到一个可访问的地址


实际用起来的感受就是一个字:


整个过程几乎没有繁琐配置,不需要绑定平台账号,也不用担心流量限制或收费。


这让很多场景变得顺手:



  • 临时展示一个 demo,不必折腾服务器

  • 写了个静态博客,不想搞 CI/CD 流程

  • 做了个活动页或 landing page,随时上线就好


以前这些需求可能要纠结“用 GitHub Pages 还是 Vercel”,现在有了 PinMe,直接一键上链就行。


体验一把


接下来看看它在真实场景下的表现:部署流程有多简化?访问速度如何?和传统方案相比有没有优势?


测试项目


为了覆盖不同体量的场景,这次我选了俩类项目来测试:



  • 小型项目:fuwari(开源的个人博客项目),打包体积约 4 MB。

  • 中大型项目:Soybean Admin(开源的后台管理系统),打包体积约 15 MB。


部署项目


PinMe 提供了两种方式:命令行可视化界面



这两种方式我们都来试一下。


命令行部署


先全局安装:


npm install -g pinme

然后一条命令上传:


pinme upload <folder/file-path>

比如上传 Soybean Admin,文件大小 15MB:



输入命令之后,等着就可以了:



只用了两分钟,终端返回的 URL 就能直接访问项目的控制页面。还能绑定自己的域名:



点击网站链接就可以看到已经部署好的项目,访问速度还是挺快的:



同样地,上传个人博客也是一样的流程。



部署完成:



可视化部署


不习惯命令行?PinMe 也提供了网页上传,进度条实时显示:



部署完成后会自动进入管理页面:



经过测试,部署速度和命令行几乎一致。


其他功能


历时记录


部署过的网站都能在主页的 History 查看:



历史部署记录:



也可以用命令行:


pinme list

历史部署记录:



删除网站


如果不再需要某个项目,执行以下命令即可:


pinme rm

PinMe 背后的“硬核支撑”


如果只看表层,PinMe 就像一个极简的托管工具。但要理解它为什么能做到“不依赖平台”,还得看看它背后的底层逻辑。


PinMe 的底层依赖 IPFS,这是一个去中心化的分布式文件系统。


要理解它的意义,得先聊聊“去中心化”这个概念。


传统互联网是中心化的:你访问一个网站时,浏览器会通过 DNS 找到某台服务器,然后从这台服务器获取内容。这条链路依赖强烈,一旦 DNS 被劫持、服务器宕机、服务商下线,网站就无法访问。



去中心化的思路完全不同:



  • 数据不是放在单一服务器,而是分布在全球节点中

  • 访问不依赖“位置”,而是通过内容哈希来检索

  • 只要有节点存储这份内容,就能访问到,不怕单点故障


这意味着:



  • 更稳定:即使部分节点宕机,内容依然能从其他节点获取。

  • 防篡改:文件哪怕改动一个字节,对应的 CID 也会完全不同,从机制上保障了前端资源的完整性和安全性。

  • 更自由:不再受制于中心化平台,文件真正由用户自己掌控。


当然,IPFS 地址(哈希)太长,不适合直接记忆和分享。这时候就需要 ENS(Ethereum Name Service)。它和 DNS 类似,但记录存储在以太坊区块链上,不可能被篡改。比如你可以把 myblog.eth 指向某个 IPFS 哈希,别人只要输入 ENS 域名就能访问,不依赖传统 DNS,自然也不会被劫持。



换句话说:



ENS + IPFS = 内容去中心化 + 域名去中心化




前端个人项目瞬间就有了更高的自由度和安全性。


一点初步感受


PinMe 并不是要取代 Vercel 这类成熟平台,但它带来了一种新的选择:更简单、更自由、更去中心化


如果你只是想快速上线一个小项目,或者对去中心化部署感兴趣,PinMe 值得一试。





这是一个完全开源的项目,开发团队也会持续更新。如果你在测试过程中有想法或需求,不妨去 GitHub 提个 Issue —— 这不仅能帮助项目成长,也能让它更贴近前端开发的实际使用场景!


作者:CUGGZ
来源:juejin.cn/post/7547515500453380136
收起阅读 »

快到  2026  年了:为什么我们还在争论  CSS 和 Tailwind?

web
最近在使用 NestJs 和 NextJs 在做一个协同文档 DocFlow,如果感兴趣,欢迎 star,有任何疑问,欢迎加我微信进行咨询 yunmz777 老实说,我对 Tailwind CSS 的看法有些复杂。它像是一个总是表现完美的朋友,让我觉得自己有些...
继续阅读 »

最近在使用 NestJs 和 NextJs 在做一个协同文档 DocFlow,如果感兴趣,欢迎 star,有任何疑问,欢迎加我微信进行咨询 yunmz777


老实说,我对 Tailwind CSS 的看法有些复杂。它像是一个总是表现完美的朋友,让我觉得自己有些不够好。
我一直是纯 CSS 的拥护者,直到最近,我才意识到,Tailwind 也有其独特的优点。
然而,虽然我不喜欢 Tailwind 的一些方面,但它无疑为开发带来了更多的选择,让我反思自己做决定的方式。


问题


大家争论的,不是 "哪个更好",而是“哪个让你觉得更少痛苦”。
对我来说,Tailwind 有时带来的是压力。比如:


<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg shadow-md hover:shadow-lg transition duration-300 ease-in-out transform hover:-translate-y-1"
>

Click me
</button>

它让我想:“这已经不再是简单的 HTML,而是样式类的拼凑。”
而纯 CSS 则让我感到平静、整洁:


.button {
background-color: #3b82f6;
color: white;
font-weight: bold;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease-in-out;
}
.button:hover {
background-color: #2563eb;
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
transform: translateY(-4px);
}

纯 CSS 让我觉得自己在“写代码”,而不是“编排类名”。


背景说明


为什么要写这篇文章呢?因为到了 2026 年,CSS 和 Tailwind 的争论已经不再那么重要。



  • Tailwind 发布了  v4,速度和性能都大大提升。

  • 纯 CSS 也在复兴,容器查询(container queries)、CSS 嵌套(nesting)和 Cascade Layers 这些新特性令人振奋。

  • 还有像 Panda CSS、UnoCSS 等新兴工具在不断尝试解决同样的问题。
    这让选择变得更加复杂,也让开发变得更加“累”。


Tailwind 的优缺点


优点:



  1. 减少命名烦恼:你不再需要为类命名。只需使用 Tailwind 提供的类名,省去了命名的麻烦。

  2. 设计一致性:使用 Tailwind,你的设计系统自然一致,避免了颜色和间距不统一的麻烦。

  3. 编辑器自动补全:Tailwind 的 IntelliSense 使得开发更加高效,输入类名时有智能提示。

  4. 响应式设计更简单:通过简单的类名就能实现响应式设计,比传统的媒体查询更简洁。


缺点:



  1. HTML 看起来乱七八糟:多个类名叠加在一起,让 HTML 看起来复杂且难以维护。

  2. 构建步骤繁琐:你需要一个构建工具链来处理 Tailwind,这对某些项目来说可能显得过于复杂。

  3. 调试困难:开发者工具中显示的类名多而杂,调试时很难快速找到问题所在。

  4. 不够可重用:Tailwind 的类名并不具备良好的可重用性,你可能会不断复制粘贴类,而不是通过自定义组件来实现复用。


纯 CSS 的优缺点


优点:



  1. 更干净的代码结构:HTML 和 CSS 分离,代码简洁易懂。

  2. 无构建步骤:只需简单的 <link> 标签引入样式表,轻松部署。

  3. 现代特性强大:2025 年的 CSS 已经非常强大,容器查询和 CSS 嵌套让你可以更加灵活地进行响应式设计。

  4. 自定义属性:通过 CSS 变量,你可以轻松实现全站的样式管理,改一个变量,所有样式立即生效。


缺点:



  1. 命名仍然困难:即使有 BEM 或 SMACSS 等方法,命名仍然是一项挑战。

  2. 保持一致性需要更多约束:没有像 Tailwind 那样的规则,纯 CSS 需要更多的自律来保持一致性。

  3. 生态碎片化:不同团队和开发者采用不同的方式来组织 CSS,缺少统一标准。

  4. 没有编辑器自动补全:不像 Tailwind,纯 CSS 需要手动编写所有的类名和样式。


到 2026 年,你该用哪个?


Tailwind 适合:



  • 使用 React、Vue 或 Svelte 等组件化框架的开发者

  • 需要快速开发并保证一致性的团队

  • 不介意添加构建步骤并依赖工具链的人


纯 CSS 适合:



  • 小型项目或静态页面

  • 喜欢简洁代码、分离 HTML 和 CSS 的开发者

  • 想要完全掌控样式并避免复杂构建步骤的人


两者结合:



  • 你可以在简单的页面中使用纯 CSS,在复杂的项目中使用 Tailwind 或两者结合,以此来平衡灵活性与效率。


真正值得关注的 2026 年趋势



  • 容器查询:响应式设计不再依赖视口尺寸,而是根据容器的尺寸进行调整。

  • CSS 嵌套原生支持:你可以直接在 CSS 中使用嵌套,避免了依赖预处理器。

  • Cascade Layers:这让你能更好地管理 CSS 优先级,避免使用 !important 来解决冲突。

  • View Transitions API:它让页面过渡更平滑,无需依赖 JavaScript。


这些新特性将极大改善我们的开发体验,无论是使用纯 CSS 还是借助 Tailwind。


结尾


不管是 Tailwind 还是纯 CSS,都有它们的优缺点。关键是要根据项目需求和个人偏好做出选择。
至于我:我喜欢纯 CSS,因为它更干净,HTML 更直观。但是如果项目需求更适合 Tailwind,那我也会使用它。
2026 年的开发趋势,将让我们有更多选择,让我们能够用最适合的工具解决问题,而不是纠结于某种工具是否“最好”。


作者:Moment
来源:juejin.cn/post/7568674364042330166
收起阅读 »

老板,我真干活了...

辛苦写了3天的代码,突然一下,全部消失了 我说我每次都git add .了,但是他就是消失了 你会相信我吗 这次真的跳进黄河都洗不清,六月都要飞雪了 一个安静的夜晚,主包正在和往常一样敲着代码,最后一个优化完成以后执行了git add . 看着长长的暂存...
继续阅读 »

辛苦写了3天的代码,突然一下,全部消失了


我说我每次都git add .了,但是他就是消失了


你会相信我吗


image.png





这次真的跳进黄河都洗不清,六月都要飞雪了


一个安静的夜晚,主包正在和往常一样敲着代码,最后一个优化完成以后执行了git add .


看着长长的暂存区,主包想是时候git commit -m


变故就发生在一瞬间,commit校验返回失败,伴随着电脑的终端闪烁了一下,主包的 所 有 改 动都消失了,文件内容停留在上次的提交...


老板,你相信我吗?我真的干活了


image.png


不过幸好,我的Gemini在我的指导下一步一步帮我把3天的劳动成果还原了,下面记录一下整个事件过程




首先。我已经执行了 git add .,这意味着 Git 已经将这些文件的内容以「blob 对象」的形式保存到本地 .git/objects 目录中,这些数据就不会凭空消失。


解决步骤

第一步:不要乱动

立即停止在仓库中执行任何覆写的操作!避免覆盖磁盘上的 blob 对象



  • ❌ 不要执行 git reset --hard/git clean -fd/git gc/git prune

  • ❌ 不要往仓库目录写入新文件


第二步:检查 Git 状态

git status查看当前暂存区的状态,如果暂存区有文件的话可以通过get checkout -- .进行恢复,可是我没有了,我的暂存区和我的脑袋一样空空


image.png


第三步:拉取悬空文件

如果 git status 显示「Working Tree Clean」,且 git checkout -- . 没效果,说明暂存区被清空,但 Git 仍保存了 git add 时的 blob 对象
需通过 git fsck --lost-fond 找回


该指令会扫描 .git/objects/ 目录,找出所有没有被任何分支/标签引用的对象,列出 悬空的 commits、blobs、trees(优先找最新的commit)


image.png


第四步:验证每个commit

执行 git show --stat <commit ID> 可以验证该悬空 commit 内容


image.png



  • 如果「提交信息 / 时间 / 作者」匹配你刚才中断的 commit → 这就是包含你所有改动的快照;

  • 如果显示 initial commit 或旧提交信息 → 这是历史悬空 commit,换列表里下一个时间最新的 commit ID 重试。


image.png



  • 这里会列出该 commit 中所有改动的文件路径 + 行数变化 → 如果能看到你丢失的文件(如 src/App.js),说明找对了;

  • 如果显示 0 files changed → 这个 commit 是空的,换其他 commit ID 重试。


第五步:恢复提交

方式 1:仅恢复文件到工作区(推荐,不修改 HEAD)


git checkout commitId -- .

方式 2:#### 直接将 HEAD 指向该 commit(完成提交)


    git reset --hard commitId // 等同于完成当时的 commit 操作



到这里基本就已经恢复了,可以check一下更改的文件,如果不全可以继续执行checkout进行恢复,如果已经完成了就尽快commit以防发生别的变故啦~


最后,还是简单讲解一下为什么优先恢复悬空commit,commit、tree、Blob的区别


核心结论先摆清楚


对象类型中文名称核心作用类比(便于理解)能否直接恢复你的文件?
Blob数据对象存储单个文件的内容(无路径)一本书里的某一页内容能,但需匹配原文件路径
Tree树对象存储目录结构(文件 / 子目录映射)一本书的目录(章节→页码)不能直接恢复,仅辅助找路径
Commit提交对象存储完整的提交快照(关联 tree + 作者 / 时间 / 信息)一本书的版本记录(含目录 + 修改说明)最优选择,一键恢复所有文件

一、逐个拆解:悬空 blob/commit/tree 到底是什么?


Git 仓库的所有内容(文件、目录、提交记录)最终都会被存储为「对象」,存放在 .git/objects 目录下;「悬空」意味着这些对象没有被任何分支 / 标签 / HEAD 引用(比如 commit 中断、reset 后、删除分支等),但只要没执行 git gc(垃圾回收),就不会消失。


1. 悬空 Blob(数据对象)—— 「只存内容,不管路径」



  • 本质:Git 中最小的存储单元,仅保存「单个文件的原始内容」,不包含文件名、路径、修改时间等信息;

  • 举例:你修改了 src/App.js 并执行 git add .,Git 会把 App.js 的内容打包成一个 blob 对象(比如你看到的 ec0529e46516594593b1befb48740956c8758884),存到 .git/objects 里;

  • 悬空原因:执行 git add 后生成了 blob,但 commit 中断 / 执行 git reset 清空暂存区,导致这个 blob 没有被 tree/commit 引用;

  • 恢复特点:能拿到文件内容,但不知道原文件路径(比如你只知道 blob 是一段 JS 代码,却不知道它原本是 src/App.js 还是 src/Page.js)。


2. 悬空 Tree(树对象)—— 「只存目录结构,不存内容」



  • 本质:描述「目录层级 + 文件映射关系」,相当于「文件路径 ↔ blob ID」的对照表,也能包含子 tree(对应子目录);

  • 举例:一个 tree 对象可能记录:


    src/ (子tree)  tree ID: bb0065eb...
    package.json blob ID: e90a82fe...
    src/App.js blob ID: ec0529e4...


  • 悬空原因:Tree 是 commit 的「子对象」,如果 commit 变成悬空(比如 reset 后),对应的 tree 也会悬空;

  • 恢复特点:仅能看到「哪些 blob 对应哪些路径」,但本身不存储文件内容,需结合 blob 才能恢复完整文件。


3. 悬空 Commit(提交对象)—— 「完整的提交快照」



  • 本质:Git 中最高级的对象,是「一次提交的完整记录」,包含:



    • 指向一个 root tree(根目录的 tree 对象)→ 能拿到整个项目的目录结构 + 所有 blob;

    • 作者、提交时间、提交信息;

    • 父 commit ID(如果是后续提交);



  • 举例:你执行 git commit -m "修改App.js" 时,Git 会生成一个 commit 对象,关联 root tree(包含所有文件路径 + blob),记录你的操作信息;

  • 悬空原因:commit 执行中断、git reset --hard 后原 HEAD 指向的 commit 无引用、删除分支后分支上的 commit 无引用;

  • 恢复特点:✅ 最优选择!通过一个 commit 对象,能一键恢复「该提交时刻的所有文件(路径 + 内容)」,不用手动匹配 blob 和路径。


二、为什么你该优先恢复「悬空 Commit」?


你之前执行了 git add . + 尝试 git commit,大概率 Git 已经生成了 commit 对象(只是没被 HEAD 引用,变成悬空)—— 恢复 commit 有 2 个核心优势:



  1. 一键恢复所有文件:commit 关联了 root tree,能直接拿到「所有文件的路径 + 对应的 blob 内容」,执行 git checkout <commit ID> -- . 就能把所有文件恢复到工作区,不用逐个处理 blob;

  2. 不用手动匹配路径:如果只恢复 blob,你需要逐个查看 blob 内容,再手动命名 / 放到原路径;而 commit 直接包含路径信息,恢复后文件路径和名称完全和丢失前一致。


三、实操场景:不同悬空对象该怎么用?


场景 1:有可用的悬空 Commit(优先选)


# 1. 找时间最新的悬空 commit
git fsck --lost-found | grep 'dangling commit' | awk '{print $3}' | while read c; do
echo "$c | $(git log -1 --format='%ai' $c)"
done | sort -k2 -r

# 2. 验证该 commit 包含你的文件
git show --stat <最新的commit ID>

# 3. 一键恢复所有文件到工作区
git checkout <commit ID> -- .

场景 2:只有悬空 Blob/Tree(无可用 Commit)


# 1. 先通过 tree 找「blob ID ↔ 文件路径」的映射
git ls-tree -r <tree ID> # 列出该 tree 下的所有文件路径+blob ID

# 2. 按路径恢复 blob 内容
git cat-file -p <blob ID> > <原文件路径> # 比如 git cat-file -p ec0529e4 > src/App.js

场景 3:只有悬空 Blob(无 Tree/Commit)


只能批量导出 blob,通过内容匹配原文件:


mkdir -p recover
for blob in $(git fsck --lost-found | grep 'dangling blob' | awk '{print $3}'); do
git cat-file -p $blob > recover/$blob
# 自动补文件后缀(如 .js/.json)
file_type=$(file -b --mime-type recover/$blob | awk -F '/' '{print $2}')
[ "$file_type" != "octet-stream" ] && mv recover/$blob recover/$blob.$file_type
done

四、关键提醒:避免悬空对象被清理


Git 的 git gc(垃圾回收)默认会清理「超过 14 天的悬空对象」,所以恢复前务必:



  1. 不要执行 git gc/git prune

  2. 恢复完成后,尽快执行 git commit 让对象被 HEAD 引用,避免后续被清理;

  3. 如果暂时没恢复完,可执行 git fsck --full 检查所有悬空对象,确认未被清理。


总结来说:优先找悬空 Commit(一键恢复)→ 其次用 Tree 匹配 Blob 路径 → 最后批量导出 Blob 手动匹配,这是最高效的恢复路径


作者:呵阿咯咯
来源:juejin.cn/post/7581678032336519210
收起阅读 »

2025博客框架选择指南:Hugo、Astro、Hexo该选哪个?

web
引言 2025年了,想搭个博客,在Hugo、Hexo、Astro之间纠结了一周,还是不知道选哪个? 我完全理解这种感受。去年我也在这几个博客框架之间来回折腾,看了无数对比文章,最后发现大部分都是2022-2023年的过时内容。性能数据变了、框架更新了、生态也不...
继续阅读 »

Gemini_Generated_Image_c4yo85c4yo85c4yo.jpg


引言


2025年了,想搭个博客,在Hugo、Hexo、Astro之间纠结了一周,还是不知道选哪个?


我完全理解这种感受。去年我也在这几个博客框架之间来回折腾,看了无数对比文章,最后发现大部分都是2022-2023年的过时内容。性能数据变了、框架更新了、生态也不一样了。每次看完一篇文章就更纠结,到底该听谁的?


更让人崩溃的是,深夜折腾博客配置,第二天还要上班。花了三个月研究框架,结果一篇文章都没写出来。这种感觉,相信你也经历过。


这篇文章基于2024-2025年最新数据,用真实的构建时间测试、实际的使用体验,帮你在5分钟内做出最适合的选择。不是告诉你"哪个最好",而是"哪个最适合你"。


我会用9大框架的最新性能对比、3分钟决策矩阵,帮你避免90%新手会踩的坑。说实话,早点看到这篇文章,我能省好几周时间。


为什么2025年还在聊博客框架?


静态博客真的还有必要吗?


老实讲,我一开始也觉得搭博客是个过时的想法。但用了一年多,真香。


静态博客和WordPress这类动态博客的本质区别,就是"提前做好"和"现场制作"的区别。静态网站生成器(SSG)会在你写完文章后,就把所有页面生成好,像做批量打印一样。访客来了直接看成品,速度飞快。WordPress这类动态博客呢,每次有人访问就现场从数据库拉数据、拼装页面,就像现场手工做菜。


为什么静态博客成为主流趋势?


说白了就是:快、便宜、不用操心服务器。


WordPress需要租服务器,一个月怎么也得几十块钱起步。静态博客呢?GitHub Pages、Vercel、Cloudflare Pages全都免费托管。我现在博客一年花费0元,连域名都是之前买的。


性能上更没得比。静态页面的Lighthouse评分能轻松拿到95+,WordPress想到90分都费劲。用户打开页面,一眨眼就加载完了,这种体验真的会让人爱上写博客。


那什么时候该选动态博客?


也不是说静态博客就天下无敌。如果你要做复杂的功能,比如:



  • 多人协作发布(需要后台管理)

  • 电商集成(要处理支付、订单)

  • 复杂的用户系统(评论、权限管理)


这些场景,WordPress或者Ghost确实更合适。但老实说,大部分个人博客和技术博客,真用不到这些。


一句话总结:个人博客、技术文档、作品展示,选静态博客框架准没错。需要复杂功能、多人协作,才考虑动态博客。


性能对决 - 谁是速度之王?


性能这块,说实话是我最关心的。刚开始用Gatsby的时候,每次改一点内容,重新构建等十几分钟,真的想砸电脑。


构建速度:差距大到惊人


先说结论:Hugo是速度之王,没有之一


看看这组2024年的实测数据:


| 框架 | 构建10000页用时 | 平均每页速度 |


|------|----------------|-------------|


| Hugo | 2.95秒 | <1ms |


| Hexo | 45秒(1000页) | ~45ms |


| Jekyll | 187.15秒 | ~18ms |


| Gatsby | 30分钟+ | ~180ms |


第一次看到Hugo的构建速度,真的惊到我了。10000篇文章,不到3秒!这意味着啥?你改个标题、修个错别字,按个保存,页面刷新,博客就更新完了。这种即时反馈,爽到飞起。


Hexo呢,中型博客(100-1000文章)表现也挺不错。我之前300篇文章的博客,15秒就构建完成,完全够用。


Gatsby...嗯,别提了。我用它做过一个项目,200多篇文章,每次构建5分钟起步。后来文章多了,直接放弃了。


页面加载速度:Astro异军突起


2024年Astro成了最大黑马。根据HTTP Archive的数据:



  • Astro:中位传输大小889KB(最轻)

  • Hugo:1,174KB(平衡不错)

  • Next.js:1,659KB(功能多,体积大)


Astro的"零JavaScript默认"策略真的厉害。页面只加载必要的JS,不像其他框架,把整个React/Vue库都塞给用户。结果就是,页面打开速度飞快,用户体验特别好。


实际项目表现:别只看数字


老实讲,如果你博客不到100篇文章,选啥框架都差不多。几秒和十几秒的区别,你感受不出来。


但如果你计划长期写作,文章会越来越多,提前选个性能好的框架,能省很多麻烦。我见过太多人,开始用了Gatsby,写到500篇文章,构建慢到受不了,迁移框架,那个痛苦...


推荐组合



  • 小型博客(<100文章):随便选,都够用

  • 中型博客(100-1000文章):Hugo、Hexo、Astro

  • 大型站点(1000+文章):闭眼选Hugo


开发体验 - 哪个最好用?


性能再好,用起来糟心,也是白搭。


学习曲线:新手别踩坑


说实话,Hugo的Go模板语法,我当初学了好久才上手。那些{{ range }}{{ with }}的语法,刚开始真的看懵了。虽然性能无敌,但新手上来就选Hugo,可能会被劝退。


新手友好度排名



  1. Hexo(最友好):Node.js生态,中文文档多到看不过来。配置就是一个_config.yml文件,改几个参数就能跑起来。我当时半小时就搭好了第一个博客。

  2. Jekyll(友好):Ruby生态,官方文档写得特别清楚,按着步骤来不会错。GitHub Pages原生支持,push一下就自动部署。

  3. VuePress(需要前端基础):如果你会Vue,上手很快。不会的话,还得先学Vue,成本就高了。

  4. Gatsby/Next.js(陡峭):要懂React、GraphQL,配置复杂。我看到配置文件就头大。


我的建议:如果你和我一样是Node.js开发者,Hexo真的是顺手。装个npm包,改改配置,半小时搞定。前端技术栈是React?那Astro或Next.js更合适。别纠结了,新手就选Hexo,错不了。


配置和自定制:平衡艺术


Hexo和Jekyll的配置简单直接,一个YAML文件搞定。但也有缺点:想做复杂定制,就得深入源码改,不太灵活。


Gatsby的灵活性确实强,GraphQL数据层可以接各种数据源。但坦白说,个人博客真用不到那么复杂的功能。就像买了辆跑车在市区开,性能过剩了。


Astro走了个中间路线,既灵活又不复杂。支持多框架(React、Vue、Svelte随便混),但配置没那么吓人。这种平衡感,我挺喜欢的。


开发工具和调试


现代框架在开发体验上真的吊打老框架。


Vite驱动的Astro和VuePress,热重载快到飞起。改个内容,不到1秒页面就更新了。Webpack那套老架构,等待时间能让人发呆。


Hugo虽然是传统框架,但速度快,改完刷新也很即时。这点体验还不错。


生态系统 - 主题与插件谁更丰富?


框架再好,没主题也白搭。谁想从零开始写CSS啊。


主题生态:Hexo中文世界称王


Hexo真的是中文博客的天选之子。200+中文主题,风格各异,总有一款适合你。我用的那个主题,中文文档详细到连怎么改字体颜色都写得清清楚楚。


Hugo主题虽然有300+,但质量参差不齐。有些特别精美,有些就是demo级别。很多主题文档是英文的,踩坑全靠自己摸索。


Jekyll作为老牌框架,主题数量最多,但很多都是好几年前的设计风格了,看着有点过时。


Gatsby和Astro的主题,走的是现代化路线,设计感很强。如果你追求视觉效果,这两个不错。


插件和扩展能力


Jekyll的插件生态最庞大,毕竟历史最久。想要啥功能,基本都能找到插件。


Gatsby依托npm生态,插件也超级丰富。但很多插件其实是为商业项目设计的,个人博客可能用不上。


Hexo插件生态在Node.js圈很成熟,常用的评论、搜索、SEO优化,都有现成插件。我装了七八个插件,没遇到过兼容问题。


社区活跃度:看数据说话


2024-2025年的数据挺有意思:



  • Astro:增长最快,npm下载量2024年9月达到300万。Netlify调查显示,它是2024年开发者关注度最高的框架。

  • Hugo:GitHub星标6万+,稳定增长,老牌强者。

  • Hexo:中文社区最活跃,知乎、掘金、CSDN到处都是教程。


说实话,社区活跃度对新手很重要。遇到问题,能搜到中文解决方案,省太多时间了。


部署与SEO - 上线才是王道


博客搭好了,不上线有啥用?


部署平台:全都免费真香


现在部署静态博客,真的太简单了。


GitHub Pages:Jekyll原生支持,push代码就自动部署。其他框架需要配个GitHub Actions,也就多写几行配置,5分钟搞定。


Vercel/Netlify:这俩是我最推荐的。拖个仓库进去,自动识别框架,自动构建部署。都有免费额度,个人博客完全够用。我现在用的Vercel,一年没花过一分钱。


Cloudflare Pages:性能特别好,CDN全球分布。免费额度也很大,速度比GitHub Pages快不少。


老实讲,2025年部署静态博客,已经没有技术门槛了。真的,比你想象的简单。


SEO:静态博客天生优势


所有静态博客框架,SEO都友好。为啥?生成的都是纯HTML,搜索引擎最爱这个。


区别在于细节:



  • Hugo和Jekyll:成熟稳定,sitemap、RSS自动生成,SEO基础功能齐全。

  • Astro和Next.js:在现代SEO实践上更领先,支持更细致的元数据管理,结构化数据也更方便。

  • Hexo:通过插件实现SEO功能,也挺完善,中文SEO教程多。


说白了,只要你写好内容、优化好关键词、页面结构合理,用哪个框架SEO都不会差。别太纠结这个。


长期维护成本:别选冷门框架


这个坑我踩过。之前用过一个小众框架,开始挺好,半年后发现作者不更新了。后来依赖库升级,博客直接跑不起来,迁移框架花了整整一周。


低维护框架:Hugo、Jekyll。成熟稳定,基本不会出幺蛾子。我Hugo博客跑了一年多,一次问题都没遇到。


需要关注更新:Gatsby、Next.js。依赖多,更新频繁,偶尔会遇到breaking changes。如果你不想经常折腾,慎选。


平衡选手:Astro、Hexo。更新有节制,兼容性做得不错。


决策框架 - 如何选择最适合你的?


好了,前面说了这么多数据和对比,到底该怎么选?


3分钟快速决策矩阵


别想太多,回答三个问题:


问题1:你的技术栈是什么?



  • 熟悉Node.js → Hexo(新手)/ Astro(追求现代化)

  • React开发者 → Gatsby(重型)/ Astro(轻量)/ Next.js(全栈)

  • Vue开发者 → VuePress(博客+文档)/ Gridsome

  • 技术栈不限 → Hugo(性能第一)/ Jekyll(求稳)


如果你和我一样是Node.js开发者,Hexo真的顺手。装个npm包,改改配置,半小时搞定。


问题2:你的项目规模是多大?



  • <100文章 → 随便选,性能差异你感受不出来

  • 100-1000文章 → Hugo(快)/ Hexo(够用)/ Astro(现代)

  • 1000+文章或大型文档站 → Hugo(一骑绝尘,没得比)


我现在500多篇文章,用的Hexo,45秒构建完成,完全够用。如果文章继续增长到1000+,可能会换Hugo。


问题3:你的经验水平如何?



  • 新手 → Hexo(中文资源多)/ Jekyll(文档友好)

  • 前端开发者 → Astro(现代化体验)/ VuePress(Vue技术栈)

  • 性能极客 → Hugo(速度无敌,值得学Go模板)

  • 求稳用户 → Jekyll(GitHub原生支持,最省心)


典型场景具体推荐


个人技术博客



  • 中文用户:Hexo(生态强,主题多)

  • 国际化:Hugo(性能好,英文资源丰富)

  • 追求现代化:Astro(体验好,性能也不错)


技术文档站点



  • Docusaurus(Facebook出品,专为文档设计)

  • VuePress(Vue生态,中文支持好)


大型内容站点



  • Hugo(1000+页面,只有它能扛住)


现代化项目网站



  • Astro(灵活性+性能的最佳平衡)

  • Next.js(需要动态功能时选它)


我的真实建议


说实话,别再纠结了。我见过太多人花三个月研究框架,一篇文章没写。框架真的只是工具,内容才是核心。


选择建议



  • 90%的人:选Hexo或Hugo,够用了

  • 前端开发者:Astro值得尝试,体验很现代

  • 新手怕选错:Hexo,中文教程多到看不完,遇到问题都能搜到答案

  • 性能焦虑症患者:闭眼选Hugo,速度真的无敌


记住:框架可以迁移(内容都是Markdown,搬家成本不高),但荒废的时间回不来。先选一个动起来,边用边优化,这才是正道。


避坑指南与最佳实践


最后说说那些大坑,我替你踩过了。


5个常见错误


错误1:过度追求完美框架,迟迟不开始


这个坑我踩得最深。当年对比了两个月框架,看了几十篇文章,结果还是不确定。后来一个前辈跟我说:"先选一个动起来,框架不满意可以换,但浪费的时间回不来。"


说白了,内容才是博客的核心,框架只是工具。没有完美的框架,只有最适合当下的选择。


错误2:只看主题外观,忽略框架本质


看到某个Hugo主题特别炫酷,就选了Hugo。结果发现Go模板语法学不会,自定义主题难如登天。最后用了半年,还是换回Hexo。


主题可以定制、可以换,但框架的性能、生态、维护性,这些本质特性才是长期影响你的因素。


错误3:新手直接上Gatsby/Next.js被劝退


我一朋友,刚学前端,听说Gatsby牛逼,直接上手。结果GraphQL不会、React不熟、配置看不懂,折腾两周直接放弃了。


老实说,Gatsby和Next.js真的不适合新手。它们是给有经验的开发者准备的工具。新手想快速上线博客,Hexo或Jekyll才是正确选择。


错误4:忽略长期维护成本


选了个冷门框架,一开始挺好,半年后作者不更新了。依赖库升级,博客跑不起来。迁移框架,痛苦得要死。


看框架选择的三个指标



  • GitHub更新频率(至少每月有commit)

  • 社区规模(遇到问题能找到人问)

  • 中文资源(新手必看,能省80%时间)


错误5:花80%时间折腾框架,20%写内容


我见过太多人,陷入"完美主义陷阱"。CSS改来改去、插件装了卸卸了装,就是不写文章。


记住80/20法则:80%精力写内容,20%折腾框架。够用就好,别追求极致完美。


框架迁移建议


万一真选错了,想换框架怎么办?


其实没那么可怕。所有静态博客框架,内容都是Markdown,迁移成本不高。我从Hexo迁移到Hugo,内容迁移只花了1小时。主要是配置和主题要重新搞,但也就半天时间。


迁移原则



  • 先有后优:快速上线 > 完美配置

  • 内容优先:写够50篇文章再考虑迁移,不然没必要

  • 不影响SEO:做好301重定向,URL结构尽量保持一致


2025年趋势展望


根据2024-2025的数据和社区动向,我预测:



  • Astro会继续增长:岛屿架构是未来趋势,零JavaScript默认太香了

  • Hugo保持性能王者地位:大型站点没得选

  • Hexo中文生态持续稳定:中文博客的首选不会变

  • 传统框架逐步被取代:Jekyll虽然稳定,但新项目会越来越少


但说实话,趋势只是参考。选框架还是要看自己的需求和技术栈。


结论


回到最开始的问题:2025年博客框架该怎么选?


一句话总结



  • 新手首选:Hexo(中文资源丰富,主题多,上手快)

  • 前端开发者:Astro(性能+灵活性的最佳平衡,现代化体验)

  • 性能极客:Hugo(速度无敌,适合大型站点)

  • 文档站点:Docusaurus/VuePress(专为文档设计)

  • 求稳:Jekyll(GitHub原生支持,最省心)


但老实讲,选择框架只需要5分钟,写好内容需要一辈子


别再纠结了。选一个顺手的,开始行动吧。写第一篇文章,比研究框架重要一百倍。


我当初纠结了两个月,现在回头看,那段时间完全是浪费。早点开始写,现在可能已经有200篇文章了。


行动建议



  1. 根据上面的决策矩阵,花5分钟选一个框架

  2. 找个主题,1小时搭好环境

  3. 写第一篇文章,哪怕只有500字

  4. 发布上线,享受成就感


记住:框架不重要,内容才重要。够用就好,专注写作。


评论区说说你的选择和理由?我很好奇大家最后都选了什么。如果有问题,我会尽量回复的。



本文首发自个人博客



作者:技术更好说
来源:juejin.cn/post/7578714735307849754
收起阅读 »

为什么说低代码谎言的破灭,是AI原生开发的起点?

几年前,公司老板、产品经理,甚至隔壁行政的同事,都拿着一份花里胡哨的低代码方案,眼睛放光地跟你说:“小张啊,你来看看,未来!拖拉拽就能上线,咱们再也不用养那么多程序员了!” 我当时啥心情?表面上“嗯嗯嗯,是是是,很有前景”,心里一万头羊驼在奔腾。你懂个锤子啊,...
继续阅读 »

几年前,公司老板、产品经理,甚至隔壁行政的同事,都拿着一份花里胡哨的低代码方案,眼睛放光地跟你说:“小张啊,你来看看,未来!拖拉拽就能上线,咱们再也不用养那么多程序员了!”


我当时啥心情?表面上“嗯嗯嗯,是是是,很有前景”,心里一万头羊驼在奔腾。你懂个锤子啊,我一直认为它是解决了一类问题,引入了一大堆的复杂。


这玩意儿的核心是啥?说白了,就是想用一套“万能模板”去解决所有问题。听着是挺美,但咱都是写代码的,你心里清楚,软件开发最难的,从来不是那几行CRUD,而是那些“该死”的个性化业务逻辑。认同?!


低代码,本质上就是把这些逻辑,从代码文件里,挪到了一堆图形化的配置框里。你以为是解放了生产力?扯淡。你只是换了个地方“坐牢”。





从“卧槽,牛逼”到“卧槽,什么玩意儿”


AI写代码一段时间,比如Cursor、Claude Code、codex这些,我当时的第一反应是:“卧-槽,牛-逼!”


你跟它说:“帮我写个用户登录接口,用JWT,密码要bcrypt加密,加上参数校验。”


唰一下,代码出来了。连注释都给你写得明明白白。那一瞬间,真有种未来已来的感觉。


这不就是低代码想干但没干成的事儿吗?


低代码是给你一堆乐高积木,说:“来,你自己拼个宇宙飞船。” 你吭哧吭哧拼了半天,发现给你的零件根本就不够,而且拼出来的玩意儿四不像,还飞不起来。


AI原生开发是啥?你直接跟管家说:“多啦 A 梦,给我造个能飞的玩意儿,要快,要帅。” 贾维斯直接给你把图纸、零件、引擎全整出来了。你只需要当那个总设计师,告诉它你的“意图”,然后去检查、微调。


你品,你细品。一个让你当“拼装工”,一个让你当“设计师”。格局一样吗?


我为啥突然这么大感触?老黄-全栈工程师,前阵子接了个项目,有个模块特别急。按老路子,前端画页面、后端写接口、联调... 怎么也得一周吧?


他说他用Claude Code,把需求拆成几个点,啪啪啪跟AI对话。
“给我个Vue3+TS的表格页,能分页,能搜索,数据接口是/api/users
“后端用Go写这个接口,连PostgreSQL,把分页逻辑做了。”
“加个逻辑,角色是admin才能看到删除按钮。”


一下午,真的,就一个下午,他说,一个带前后端的完整功能雏形就出来了。代码质量还不低,结构清晰,拿过来改改就能用。


而用低代码呢?他说他可能还在研究那个破平台的“数据源”到底该怎么连,或者某个组件的某个奇葩属性到底藏在哪个配置项里。那种抓狂,谁用谁知道。


我想,老黄的这种感觉你肯定有过,如果可以选择,你会选择用地代码去拖拉拽吗?我想大概率你是不会了,回不去了。




别跟我扯什么“AI搞不定复杂逻辑”


我知道,肯定有人会跳出来说:“哥们,你这是偷换概念!简单的CRUD当然AI快,你让AI搞个复杂的风控引擎试试?”


每次听到这话,我就想笑。


兄弟,你是不是忘了我们现代软件架构的核心思想是啥了?高内聚、低耦合!


一个复杂的系统,不就是一堆简单的、正交的子系统组合起来的吗?你告诉我,哪个“复杂业务逻辑”是不能拆解的?如果不能,那不是AI的问题,是你的架构设计有问题。


领域驱动设计(DDD)讲了这么多年,不就是为了把业务模型理清楚,把边界划分明白吗?


在AI原生时代,架构师的核心能力,不再是堆砌代码,而是精准地拆解问题,然后把拆解后的子问题清晰地描述给AI。


低代码平台最大的死穴就在这儿。它试图用一个大而全的“黑箱”包办一切,结果就是耦合度极高。你想改其中一根“毛细血管”,可能得把整个“心脏”都停了。这种系统,业务稍微一变,就得推倒重来,维护成本高到让你怀疑人生。我亲眼见过一个团队,被他们选的低代码平台折磨得死去活-来,最后整个项目烂尾。


AI生成的代码呢?那是“白箱”。清清楚楚的源代码,你想怎么改就怎么改,想怎么扩展就怎么扩展。这才是咱们工程师该有的掌控感!




所以,有人焦虑了?


聊到这,估计有些年轻的哥们儿开始焦虑了:“老哥,照你这么说,以后是不是不用写代码了?我们都要被AI干掉了?”


我说:慌啥。


AI不是来取代你的,是来升级你的工具箱的。,取带你的是那些用 AI 比你用得更深,更好的人。人家都开车了,你还走路,你搁那怪谁呢?


低代码,是想把你变成一个“组件配置工程师”,说实话,这玩意儿真没啥技术含量,他无非就是把一些固化的规则和业务组件放在了一个系统中,让你去组合,去连接,也确实容易被替代。


但AI原生开发,对人的要求,其实是更高了。


你需要有更强的架构能力,去拆解复杂问题。
你需要有更强的业务理解能力,去把需求转化成清晰的指令。
你需要有更强的代码审美,去判断AI生成的代码是“精品”还是“垃圾”。


说白了,AI帮你干了那些重复、枯燥的体力活,让你能把全部精力,都放在 “思考” 这个最有价值的事情上。




写在最后,一点不成熟的牢骚


我也不知道这股风会刮多久,技术这玩意儿,日新月异。今天你觉得牛逼的东西,明天可能就是一堆垃圾。


AI 原生开发,它不是一个工具的改变,更像是一种“思维范式”的革命。它在逼着我们从“如何实现”的工匠思维,转向“做什么、为什么做”的创造者思维。


低代码的时代,可能还没开始,就已经结束了。它只是一个过渡期的妥协品,一个想走捷径但最终掉进坑里的解决方案。


至于未来会怎么样?我也不知道。可能我们以后写的不是代码,而是“提示词”;面试考的不是算法,而是你跟AI“对话”的能力。


谁说得准呢?


反正我是已经把主力开发工具换成cursor、Claude Code、codex 这些了。一边写,一边让AI帮我优化,或者干脆让它写大段的模板代码,这感觉,真挺爽的。


当然,这都是我个人的一点看法,不一定对,欢迎来杠。评论区聊聊?


作者:老码小张
来源:juejin.cn/post/7551214653553066019
收起阅读 »

“全栈模式”必然导致“质量雪崩”!和个人水平关系不大

在经济下行的大背景下,越来越多的中小型企业开始放弃“前后端分离”的人员配置,开始采用“全栈式开发”的模式来进行研发费用的节省。 这方法真那么好吗? 作为一名从“全栈开发”自我阉割成“前端开发”的逆行研发,我有很多话想说。 先从一个活生生的真实案例开始吧。 我...
继续阅读 »

在经济下行的大背景下,越来越多的中小型企业开始放弃“前后端分离”的人员配置,开始采用“全栈式开发”的模式来进行研发费用的节省。



这方法真那么好吗?


作为一名从“全栈开发”自我阉割成“前端开发”的逆行研发,我有很多话想说。


先从一个活生生的真实案例开始吧。


我认识一个非常优秀的全栈开发,因为名字最后一个字是阳,所以被大家称为“阳神”。


1. “阳神”的“神狗二相性”


阳神当然是牛逼的。


他不仅精通后端开发,更是对前端了解的非常深。这样来说吧:



当他作为后端开发时,他可以是那群后端同事里库表设计最清晰,代码最规范,效率最高的后端。




当他作为前端开发时,他除了比几位高级别前端稍逊一点外,效率和UI还原性都非常高,还会主动封装组件减少耦合。



但是非常奇怪的事情总是会发生,因为一旦阳神不是全职的“后端”或者“前端”时,一旦让他同时操刀“后端+前端”开发任务,作为一名“全栈”来进行业务推进时,他的表现会让人感到惊讶:



他会写出设计糟糕,不规范,职责混乱的代码。



这个现象我把他戏称为“阳神”的“神狗二相性”,作为单一职责时他是“阳神”,同时兼任多职时,他就有非常大的可能降格为“阳狗”。



为什么呢?这是阳神主观上让自己写更糟糕的代码吗?


不是的兄弟,不是的。


这是系统性的崩塌,几乎不以人的意志为转移。换我去也是一样,换你去也是一样。


2. 分工粗化必然导致技术细节的差异


从前,在软件开发的古老行会里,一个学徒需要花很多年才能出师,专门做一把椅子,或者专门雕一朵花。现在,你被要求从伐木到抛光,从结构力学到表面美学,全部一手包办。


生产力在发展,对人的技能要求也在发展。


因此“分工细化”成为了工业革命之后完全不可逆的趋势。


IT 产业上也是如此。


“软件开发”经过多年被细化出了前端开发后端开发客户端开发大数据开发 等等多种不同的细分职业。


但是现在有人想通过 粗化 职业分功来达到 “提效” 的目的,在我眼中这就是和客观规律对着干。


人的精力是守恒的。当你需要同时关心useEffect的依赖数组会不会导致无限渲染,和kubectl的配置能不能正确拉起Pod时,你的注意力就被稀释了。你不再有那种“针对一个领域,往深里钻,钻到冒油”的奢侈。


当你脑袋里冒出了一个关于前端工程化优化的问题时,身为全栈的你会本能地冒出另一个念头:



在整个全栈体系内,前端工程化优化是多么边角料且无关痛痒的问题啊,我去深入研究和解决它的性价比实在太低了,算了不想了。



如此一来,无论是后端的性能问题还是前端的性能问题都会变得无关紧要。




结果是,只有业务问题是全栈开发要关心的问题。



2. “岗位对立”与“自我妥协”


在日常开发中,前端开发和后端开发之间互相吐槽争论是再正常不过的话题,而且争论的核心非常简单易懂:



前端:这事儿不能在后端做吗?




后端:这事儿前端不能做吗?



可以的,兄弟,最后你会发现都是可以的,代码里大部分的事情无论是在浏览器端完成还是在服务器里完成都是可行的。


但是,总有一个“哪方更适合做”吧?



  • 一个大屏页面的几万几十万条的数据统计,是应该后端做还是前端做?

  • 业务数据到Echarts展示数据的格式转换应该后端做还是前端做?

  • 用户数据权限的过滤应该后端做还是前端做?

  • 一个列表到底要做真分页还是假分页?

  • 列表已经返回了全量实体信息,为什么还要再增加一个详情接口?


这都是日常开发时前端和后端都会去争论思考的问题,身处不同的职位,就会引入不同的立场和思考。



  • 前端需要去思考页面刷新后状态的留存,js单线程下大量数据处理的卡顿,页面dom树爆表的困境。

  • 后端也需要思考并发下服务器资源和内存的分配,可能的死锁问题,以及用户的无状态token如何处理等。


前后端的“争吵”和观点输出是不可避免的。


真理总是越辩越清晰的,后续讨论出的结果多半是最有利于当前现状的。


但如果“前后端”都是同一个人呢?


全栈模式,完美地消灭了这种“有益的摩擦”。当你自己和自己联调时,你不会给自己提挑战灵魂的问题。你不会问:“这个API设计是否RESTful?”因为你赶时间。你也不会纠结:“这个组件的可访问性够好吗?”因为你还得去部署服务器。



这两种思想在你的大脑里打架,最终往往不是最优解胜出,而是最省事的那个方案活了下来。



于是,你的代码里充满了“差不多就行”的妥协。这种妥协,一两个无所谓,当成百上千个“差不多”堆积起来时,质量的基础就酥了。


内部摩擦的消失,使得代码在诞生之初就缺少了一道质量校验的工序。它顺滑地流向生产环境,然后,在某个深夜,轰然引爆。


3. 工程的“不可能三角”


软件开发领域有一个著名的“不可能三角”:



快、好、省,你只能选两样。




全栈模式,在管理者眼中,完美地实现了“省”(一个人干两个人的活)和“快”(省去沟通成本)。那么,被牺牲掉的是谁?


雪崩时,没有一片雪花是无辜的。但更重要的是,当结构性雪崩发生时,问责任何一片雪花,都意义不大。


至于“快、好、省”这三兄弟怎么选?


那主要看老板的认知和他的钱包了。


作者:摸鱼的春哥
来源:juejin.cn/post/7555387521451606068
收起阅读 »

面试官:CDN是怎么加速网站访问的?

web
做前端项目的时候,经常听到"静态资源要放CDN"这种说法: CDN到底是什么,为什么能加速? 用户访问CDN的时候发生了什么? 前端项目怎么配置CDN? 先说结论 CDN(Content Delivery Network)的核心原理:把内容缓存到离用户最近...
继续阅读 »

做前端项目的时候,经常听到"静态资源要放CDN"这种说法:



  • CDN到底是什么,为什么能加速?

  • 用户访问CDN的时候发生了什么?

  • 前端项目怎么配置CDN?


先说结论


CDN(Content Delivery Network)的核心原理:把内容缓存到离用户最近的服务器上


没有CDN时,北京用户访问美国服务器,数据要跨越太平洋。有了CDN,数据从北京的CDN节点返回,快得多。


flowchart LR
subgraph 没有CDN
U1[北京用户] -->|跨越太平洋| S1[美国服务器]
end

subgraph 有CDN
U2[北京用户] -->|就近访问| C2[北京CDN节点]
C2 -.->|首次回源| S2[美国服务器]
end

CDN的工作流程


当用户访问一个CDN加速的资源时,会经过这几步:


1. DNS解析


用户访问 cdn.example.com,DNS会返回离用户最近的CDN节点IP。


# 在北京查询
$ dig cdn.example.com
;; ANSWER SECTION:
cdn.example.com. 60 IN A 101.37.27.xxx # 北京节点

# 在上海查询
$ dig cdn.example.com
;; ANSWER SECTION:
cdn.example.com. 60 IN A 47.100.99.xxx # 上海节点

同一个域名,不同地区解析出不同IP,这就是CDN的智能调度。


2. 缓存判断


请求到达CDN节点后,节点检查本地是否有缓存:



  • 缓存命中:直接返回,速度最快

  • 缓存未命中:向源服务器获取,然后缓存起来


flowchart TD
A[用户请求] --> B[CDN边缘节点]
B --> C{本地有缓存?}
C -->|有| D[直接返回]
C -->|没有| E[回源获取]
E --> F[源服务器]
F --> G[返回内容]
G --> H[缓存到边缘节点]
H --> D

classDef hit fill:#d4edda,stroke:#28a745,color:#155724
classDef miss fill:#fff3cd,stroke:#ffc107,color:#856404
class D hit
class E,F,G,H miss

3. 缓存策略


CDN根据HTTP头决定怎么缓存:


# 典型的静态资源响应头
Cache-Control: public, max-age=31536000
ETag: "abc123"
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT


  • max-age=31536000:缓存1年

  • public:允许CDN缓存

  • ETag:文件指纹,用于验证缓存是否过期


为什么CDN能加速


1. 物理距离更近


光在光纤中的传播速度约为 20 万公里/秒。北京到美国往返 2 万公里,光传输就要 100ms。


CDN把内容放到离用户近的地方,这个延迟几乎可以忽略。


2. 分散服务器压力


没有CDN,所有请求都打到源服务器。有了CDN,只有第一次请求(缓存未命中)才需要回源。


假设一张图片被访问 100 万次:



  • 没有CDN:源服务器处理 100 万次请求

  • 有CDN:源服务器只处理几十次(各节点首次回源)


3. 边缘节点优化


CDN服务商的边缘节点通常有这些优化:



  • 自动压缩:Gzip/Brotli压缩

  • 协议优化:HTTP/2、HTTP/3

  • 连接复用:Keep-Alive连接池

  • 智能路由:选择最优网络路径


前端项目配置CDN


1. 构建配置


以Vite为例,配置静态资源的CDN地址:


// vite.config.js
export default defineConfig({
base: process.env.NODE_ENV === 'production'
? 'https://cdn.example.com/'
: '/',
build: {
rollupOptions: {
output: {
// 文件名带hash,便于长期缓存
entryFileNames: 'js/[name].[hash].js',
chunkFileNames: 'js/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]'
}
}
}
})

Webpack配置类似:


// webpack.config.js
module.exports = {
output: {
publicPath: process.env.NODE_ENV === 'production'
? 'https://cdn.example.com/'
: '/',
filename: 'js/[name].[contenthash].js',
}
}

2. 上传到CDN


构建完成后,把dist目录的文件上传到CDN。大多数CDN服务商都提供CLI工具或API:


# 阿里云OSS + CDN
aliyun oss cp -r ./dist oss://your-bucket/

# AWS S3 + CloudFront
aws s3 sync ./dist s3://your-bucket/

# 七牛云
qshell qupload2 --src-dir=./dist --bucket=your-bucket

3. 缓存策略配置


关键是区分两类文件:


带hash的静态资源(JS、CSS、图片):长期缓存


location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}

HTML文件:不缓存或短期缓存


location ~* \.html$ {
expires 0;
add_header Cache-Control "no-cache, must-revalidate";
}

为什么这样设计?因为HTML是入口文件,它引用的JS/CSS带有hash。更新代码时:



  1. 新的JS文件会有新的hash(app.abc123.jsapp.def456.js

  2. HTML文件引用新的JS文件

  3. 用户获取新HTML后,会加载新的JS文件

  4. 旧的JS文件在CDN上继续存在,不影响正在访问的用户


4. DNS配置


把CDN域名配置成CNAME指向CDN服务商:


# 阿里云CDN
cdn.example.com. IN CNAME cdn.example.com.w.kunlunsl.com.

# Cloudflare
cdn.example.com. IN CNAME example.com.cdn.cloudflare.net.

常见问题


跨域问题


CDN域名和主域名不同,字体文件、AJAX请求可能遇到跨域。


解决方案是在CDN配置CORS头:


# CDN服务器配置
add_header Access-Control-Allow-Origin "https://example.com";
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS";

或者在源服务器的响应里加上CORS头,CDN会透传。


缓存更新问题


发布新版本后,用户还是看到旧内容?


方案一:文件名带hash(推荐)


前面已经提到,文件名带hash,新版本就是新文件,不存在缓存问题。


方案二:主动刷新缓存


// 调用CDN服务商的刷新API
await cdnClient.refreshObjectCaches({
ObjectPath: 'https://cdn.example.com/app.js\nhttps://cdn.example.com/app.css',
ObjectType: 'File'
});

方案三:URL加版本号


<script src="https://cdn.example.com/app.js?v=1.2.3"></script>

不太推荐,因为有些CDN会忽略查询参数。


HTTPS证书


CDN域名也需要HTTPS证书。大多数CDN服务商提供免费证书或支持上传自有证书。


配置方式:



  1. 在CDN控制台申请免费证书(通常是DV证书)

  2. 或上传自己的证书(用Let's Encrypt申请)


# 用certbot申请泛域名证书
certbot certonly --manual --preferred-challenges dns \
-d "*.example.com" -d "example.com"

回源优化


如果源服务器压力大,可以启用"回源加速"或"中间源":


用户 → 边缘节点 → 中间源 → 源服务器

中间源作为二级缓存,减少对源服务器的请求。多数CDN服务商默认开启这个功能。


CDN选型


CDN服务商优势适用场景
阿里云CDN国内节点多,生态完整国内业务为主
腾讯云CDN游戏加速好,直播支持强游戏、直播
Cloudflare全球节点,有免费套餐出海业务、个人项目
AWS CloudFront与AWS生态集成已用AWS的项目
Vercel/Netlify前端项目一站式部署JAMStack项目

个人项目推荐Cloudflare,免费套餐够用,全球节点覆盖好。


企业项目根据用户分布选择。面向国内用户,阿里云/腾讯云更合适;面向全球用户,Cloudflare或CloudFront。


验证CDN效果


浏览器开发者工具


打开Network面板,关注这几个指标:



  • TTFB(Time To First Byte):首字节时间,越小越好

  • 响应头中的缓存信息X-Cache: HIT 表示命中CDN缓存


命令行测试


# 查看响应头
curl -I https://cdn.example.com/app.js

# 输出示例
HTTP/2 200
cache-control: public, max-age=31536000
x-cache: HIT from CN-Beijing

在线工具



小结


CDN加速的核心原理:



  1. 就近访问:DNS智能解析,把用户引导到最近的节点

  2. 缓存机制:边缘节点缓存内容,减少回源

  3. 协议优化:HTTP/2、压缩、连接复用


前端配置CDN的关键:



  1. 构建时配置publicPath:指向CDN域名

  2. 文件名带hash:便于长期缓存

  3. HTML不缓存:确保用户能获取到最新入口

  4. 处理跨域:配置CORS头




如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:


Claude Code Skills(按需加载,意图自动识别,不浪费 token,介绍文章):



全栈项目(适合学习现代技术栈):



  • prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑

  • chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB


作者:也无风雨也雾晴
来源:juejin.cn/post/7582438310103613486
收起阅读 »

为什么大部分程序员成不了架构师?

很多程序员初学编程那会,几乎都有一个成为架构师的梦想。 ❝ 毕竟不想当架构师的程序员不是一个好程序员。 这里有几个架构师需要具备的能力模型: ❝ 技术深度和广度: 具备深厚的技术功底,同时对相关领域非常熟悉与了解。 经验积累: 具备在某一领域,有...
继续阅读 »

很多程序员初学编程那会,几乎都有一个成为架构师的梦想。




毕竟不想当架构师的程序员不是一个好程序员。



图片


这里有几个架构师需要具备的能力模型:




技术深度和广度:



  • 具备深厚的技术功底,同时对相关领域非常熟悉与了解。


经验积累:



  • 具备在某一领域,有非常丰富的行业经验

  • 具体涉及到系统设计、性能优化、风险管理等方面。


业务理解和沟通能力:



  • 需要理解业务需求,将业务目标转化为系统设计。

  • 需要与不同角色进行高效的沟通,包括与非技术人员的沟通。


领导和管理能力:



  • 在一些情况下,架构师可能需要领导团队、制定技术方向。


学习和适应能力:



  • 需要不断学习新的技术和趋势,并将其应用到实际项目中。



其实有些程序员可能更喜欢专注于编码本身。




对于涉及更广泛系统设计和管理方面的工作不感兴趣。


他们可能更倾向于深入技术领域而非走向管理和架构方向。



不过能成为架构师还有几个点很关键:




想成为架构师至少要有一个好平台,还要有毅力钻研技术并付诸实践。



  • 而且要经历各种各样的场景。


最好还要有一个好团队一起努力,毕竟一个人的精力是有限的。



不过并非每个程序员都适合成为架构师,不同人有不同的兴趣和职业目标。


有啥其他看法,欢迎在评论区留言讨论。




想看技术文章的,可以去我的个人网站:hardyfish.top/



  • 目前网站的内容足够应付基础面试(P6)了!



每日一题


题目描述




给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。


找出那个只出现了一次的元素。



示例 1 :


ini
体验AI代码助手
代码解读
复制代码
输入:nums = [2,2,1]
输出:1

示例 2 :


ini
体验AI代码助手
代码解读
复制代码
输入:nums = [4,1,2,1,2]
输出:4

示例 3 :


ini
体验AI代码助手
代码解读
复制代码
输入:nums = [1]
输出:1

解题思路




位运算


数组中的全部元素的异或运算结果即为数组中只出现一次的数字。



代码实现


Java代码:


Java
体验AI代码助手
代码解读
复制代码
class Solution {
    public int singleNumber(int[] nums) {
        int single = 0;
        for (int num : nums) {
            single ^= num;
        }
        return single;
    }
}

Python代码:


Python
体验AI代码助手
代码解读
复制代码
class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        return reduce(lambda x, y: x ^ y, nums)

Go代码:


Go
体验AI代码助手
代码解读
复制代码
func singleNumber(nums []int) int {
    single := 0
    for _, num := range nums {
        single ^= num
    }
    return single
}

复杂度分析




时间复杂度:O(n),其中 n 是数组长度。



  • 只需要对数组遍历一次。


空间复杂度:O(1)


作者:程序员飞鱼
链接:https://juejin.cn/post/7459671967306940431
收起阅读 »

数据可视化神器Heat.js:让你的数据热起来

web
😱 我发现了一个「零依赖」的数据可视化宝藏! Hey,前端小伙伴们!今天必须给你们安利一个「让数据说话」的神器——Heat.js!这可不是一个普通的JavaScript库,而是一个能让你的数据「热」起来的魔法工具! 想象一下,当你有一堆枯燥的日期数据,想要以...
继续阅读 »

😱 我发现了一个「零依赖」的数据可视化宝藏!


Hey,前端小伙伴们!今天必须给你们安利一个「让数据说话」的神器——Heat.js!这可不是一个普通的JavaScript库,而是一个能让你的数据「热」起来的魔法工具!


image.png


想象一下,当你有一堆枯燥的日期数据,想要以直观、炫酷的方式展示出来时,Heat.js就像一个魔法师,「唰」的一下就能把它们变成色彩斑斓的热图、清晰明了的图表,甚至还有详细的统计分析!


🤩 这个库到底有什么「超能力」?


1. 「零依赖」轻量级选手,绝不拖你后腿 🦵


在这个「依赖地狱」的时代,Heat.js简直就是一股清流!它零依赖,体积小得惊人,加载速度快得飞起!再也不用担心引入一个库就拖慢整个页面加载速度了~


2. 「四种视图」任你选,总有一款适合你 🔄


Heat.js提供了四种不同的视图模式:



  • Map视图:就像GitHub贡献图一样炫酷,用颜色深浅展示日期活跃度

  • Chart视图:把数据变成专业的图表,让趋势一目了然

  • Days视图:专注于展示每一天的详细数据

  • Statistics视图:直接给你算出各种统计数据,懒人福音!


想换个姿势看数据?只需轻轻一点,瞬间切换~


3. 「51种语言」支持,真正的「世界公民」🌍


担心你的国际用户看不懂?不存在的!Heat.js支持51种语言,从中文、英文到阿拉伯语、冰岛语,应有尽有!你的应用可以轻松走向全球,再也不用为语言本地化发愁了~


4. 「数据导入导出」无所不能,数据来去自由 📤📥


想导出数据做进一步分析?没问题!Heat.js支持导出为CSV、JSON、XML、TXT、HTML、MD和TSV等多种格式,任你选择!


想导入已有数据快速生成热图?同样简单!支持从JSON、TXT、CSV和TSV导入,甚至还支持拖拽上传,简直不要太方便!


5. 「12种主题」随意切换,颜值与实用并存 💅


担心热图不好看?Heat.js提供了12种精心设计的主题,包括暗黑模式和明亮模式,让你的数据可视化既专业又美观!无论你的网站是什么风格,都能找到匹配的主题~


💡 这个神奇的库可以用来做什么?


1. 「活动追踪」,让你的用户活跃起来 📊


想展示用户的登录活跃度?想用热图展示文章的发布频率?Heat.js帮你轻松实现!就像GitHub的贡献图一样,让你的用户看到自己的「努力成果」,成就感满满!


2. 「数据分析」,让你的决策更明智 🧠


通过Heat.js的Statistics视图,你可以快速获取数据的各种统计信息,比如最活跃的月份、平均活动频率等。这些数据可以帮助你做出更明智的产品决策,优化用户体验!


3. 「趋势展示」,让你的报告更有说服力 📈


想在报告中展示某个指标的变化趋势?Heat.js的Chart视图可以将枯燥的数据变成直观的图表,让你的报告更有说服力,老板看了都说好!


🛠️ 如何用最简单的方式用上这个神器?


第一步:「把宝贝抱回家」📦


npm install jheat.js

或者直接使用CDN:


<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/williamtroup/Heat.js@4.5.1/dist/heat.js.min.css">
<script src="https://cdn.jsdelivr.net/gh/williamtroup/Heat.js@4.5.1/dist/heat.min.js"></script>

第二步:「给它找个家」🏠


<div id="heat-map" data-heat-js="{ 'views': { 'map': { 'showDayNames': true } } }">
<!-- 这里将显示你的热图 -->
</div>

第三步:「喂它数据」🍽️


// 添加日期数据
let newDateObject = new Date();
$heat.addDate("heat-map", newDateObject, "Trend Type 1", true);

// 移除日期数据(如果需要)
// $heat.removeDate("heat-map", newDateObject, "Trend Type 1", true);

三步搞定!就是这么简单!


🎯 为什么Heat.js值得你拥有?


1. 「简单易用」,小白也能轻松上手 👶


Heat.js的API设计非常友好,文档也很详细,即使是JavaScript初学者也能快速上手。几个简单的步骤,就能实现专业级的数据可视化效果!


2. 「高度定制」,满足你的各种需求 ⚙️


无论是颜色、样式,还是功能配置,Heat.js都提供了丰富的选项。你可以根据自己的需求,定制出独一无二的数据可视化效果!


3. 「响应式设计」,在任何设备上都完美展示 📱💻


Heat.js完全支持响应式设计,无论是在手机、平板还是电脑上,都能完美展示。你的数据可视化效果将在任何设备上都一样出色!


4. 「TypeScript支持」,框架党福利 🎉


如果你使用React、Angular等现代前端框架,Heat.js的TypeScript支持会让你用得更爽!类型定义清晰,代码提示完善,开发体验一流!


🚀 最后想说的话...


在这个「数据为王」的时代,如何让数据更直观、更有说服力,是每个开发者都需要面对的挑战。而Heat.js,就是帮助你征服这个挑战的绝佳工具!


它轻量级、零依赖、功能强大、易于使用,无论是个人项目还是企业应用,都能轻松胜任。最重要的是,它让数据可视化不再是一件复杂的事情,而是一种乐趣!


所以,还等什么呢?赶紧去GitHub上给Heat.js点个Star⭐,然后在你的项目中用起来吧!相信我,它一定会给你带来惊喜!


✨ 祝大家的数据可视化之路一帆风顺,让我们一起用Heat.js让数据「热」起来!✨


作者:Yanni4Night
来源:juejin.cn/post/7578161740467421235
收起阅读 »

解决网页前端中文字体包过大的几种方案

web
最近想给我的博客的网页换个字体,在修复了历史遗留的一些bug之后,去google fonts上找了自己喜欢的字体,本地测试和自己的设备发现没问题后,便以为OK了。但是当我接朋友的设备打开时,发现网页依然是默认字体。这时候我才发现,我的设备能够翻墙,所以能够使用...
继续阅读 »

最近想给我的博客的网页换个字体,在修复了历史遗留的一些bug之后,去google fonts上找了自己喜欢的字体,本地测试和自己的设备发现没问题后,便以为OK了。

但是当我接朋友的设备打开时,发现网页依然是默认字体。这时候我才发现,我的设备能够翻墙,所以能够使用Google CDN服务,但是对于我的其他读者们,在大陆内是访问不了Google的,便也无法渲染字体了。

于是为了解决这个问题,我尝试了各种办法比如格式压缩,子集化(Subset),分包等等,最后考虑到本站的实际情况选用了一种比较邪门的方法,让字体压缩率达到了惊人的98.5%!于是,这篇文章就是对这个过程的总结。也希望这篇文章能够帮助到你。😊


想要自定义网站的字体,最重要的其实就是字体包的获取。大体上可以分为两种办法:在线获取网站本地部署

在线获取──利用 CDN 加速服务

CDN(Content Delivery Network) 意为内容配送网络。你可以简单理解为是一种“就近给你东西”的互联网加速服务。

传统不使用 CDN 服务的是这样的: User ←→ Server,如果相聚遥远,效果显然很差。

使用了 CDN 服务是这样的: User ←→ CDN Nodes ←→ Server,CDN 会提前把你的网站静态资源缓存到各个节点,但你需要时可以直接从最近的节点获取。

全球有多家CDN服务提供商,Google Fonts使用的CDN服务速度很快。所以如果在网络畅通的情况下,使用Google Fonts API是最简单省事的!

你可以直接在文件中导入Google fonts API:

@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Merriweather:ital,opsz,wght@0,18..144,733;1,18..144,733&family=Noto+Serif+SC:wght@500&display=swap');

这样网站它便会自动向最近的Google CDN节点请求资源。

当然,这些都是建立在网络状态畅通无阻的情况下。大陆用户一般使用不了Google服务,但并不意味着无法使用CDN服务。国内的腾讯云,阿里云同样提供高效的服务,但具体的规则我并不了解,请自行阅读研究。

本地部署

既然用不了在线的,那就只能将字体包文件一并上传到服务器上了。

这种做法不需要依赖外部服务,但缺点是字体包的文件往往很大,从进入网站到彻底加载完成的时间会及其漫长!而且这种问题尤其在中日韩(CJK)字体上体现的十分明显。

以本站为例,我主要采用了三种字体:Merriweather, Inter, Noto Serif SC. 其中每种字体都包含了Bold和Regular两种格式。前面两种都属于西文字体,每种格式原始文件大小都在200kb-300kb,但是到了思源宋体这里,仅仅一种格式的字体包大小就达到了足足14M多。如果全部加载完,恐怕从进入网站到完全渲染成功,需要耽误个2分钟。所以将原始字体包文件上传是极不可取的做法!

为了解决这个问题,我在网上查阅资料,找到了三种做法。

字体格式转换(WOFF2)

WOFF2 (Web Open Font Format 2.0) 是一种专为 Web 设计的字体文件格式,旨在提供更高的压缩率和更快的加载速度,也是是目前在 Web 上部署自定义字体的推荐标准。它本质上是一种将 TTF 或 OTF 字体数据进行高度压缩后的格式,目前已经获得了所有主流浏览器的广泛支持。

我们可以找一个在线的字体格式转化网站来实现格式的转化。本文我们以NotoSerifSC-Bold.ttf为例,转换后的NotoSerifSC-Bold.woff2文件只有5.8M左右,压缩率达到了60%!

但是,这仍旧是不够的,仅两个中文字体包加起来也已经快12M,还没有算上其他字体。这对于一个网页来说依然是灾难性的。我们必须寻找另一种方法。

子集化处理(Subset)

中国人都知道,虽然中文的字符加起来有2万多个,但是我们平常交流基本只会用到3000多个,范围再大一点,6000多个字符已经可以覆盖99%的使用场景。这意味着:

我们根本不需要保留所有字符,而只需要保留常用的几千个汉字即可。

于是这就给了我们解决问题的思路了。

首先我们可以去寻找中文常用汉字字符表,这里我获取的资源是 All-Chinese-Character-Set。我们将文件下载解压后,可以在里面找到各种各样按照字频统计的官方文件。这里我们就以《通用规范汉字表》(2013年)一级字和二级字为例。我们创建一个文档char_set.txt并将一级字和二级字的内容全部复制进去。这份文档就是我们子集化的对照表。

接着我们需要下载一个字体子集化工具,这里使用的是Python中的fonttools库,它提供了许多工具(比如我们需要的pyftsubset)可以在命令行中执行子集化、字体转化字体操作。

我们安装一下这个库和对应的依赖(在这之前确保你的电脑上安装了Pythonpip,后者一般官方安装会自带)

pip install fonttools brotli zopfli

然后找到我们字体包对应的文件夹,将原来的char_set.txt复制到该文件夹内,在该文件下打开终端,然后以NotoSerifSC-Bold.ttf为例,输入以下命令:

pyftsubset NotoSerifSC-Bold.ttf --output-file=NotoSerifSC-Bold.subset.woff2 --flavor=woff2 --text-file=char_set.txt --no-hinting --with-zopfli

过一会就能看到会输出一个NotoSerifSC-Bold.subset.woff2的文件。

font-pic-1.png 我们欣喜的发现这个文件的大小竟然只有980KB。至此,我们已经已经将压缩率达到了93%!到这一步,其实直接部署也并没有十分大问题,不过从加载进去到完全渲染,可能依然需要近十秒左右,我们依然还有优化空间。

分包处理实现动态加载

这个方法是我阅读这篇文章了解到的,但是遗憾的是我并没有在自己的网站上实现,不过失败的尝试也让我去寻找其它的方法,最终找到适用本站的一种极限字体渲染的方法,比这三种的效果还要好。下面我依然简单介绍一下这个方法的原理,想更了解可以通过看到最后通过参考资料去进一步了解。

在2017年,Google Fonts团队提出切片字体,因为他们发现:绝大部分网站只需要加载CJK字体包的小部分内容即可覆盖大部分场景。基于适用频率统计,他们将字符分成多个切片,再按 Unicode 编码对剩余字符进行分类。

怎么理解呢?他其实就是把所有的字符分成许多个小集合,每个集合里面都包含一定数量的字符,在靠前的一些集合中,都是我们常用的汉字,越到后,字形越复杂,使用频率也越低。当网页需要加载字体文件时,它是以切片为单位加载的。这意味,只有当你需要用到某个片区的字符时,这个片区才会被加载。

这种方式的好处时,能够大大加快网站加载速率。我们不用每次都一次性把全部字符加载,而是按需加载。这项技术如今已经被Noto Sans字体全面采用。

但是我们需要本地部署的话,需要多费一点功夫。这里我们利用中文网字计划的在线分包网站来实现。

我们将需要的字体上传进行分包,可以观察到输出结果是一系列以哈希值命名的woff2文件。分包其实就是做切分,把每个切分后的区域都转化为一份体积极小的woff2文件。

font-pic-2.png 下载压缩包,然后可以将里面的文件夹导入你的项目,并引用文件夹下的result.css即可。理论上,当网站需要加载渲染某个字体时,它会根据css里面的规则去寻找到对应的分包再下载。每个包的体积极小,网站加载的速度应该提升的很明显。

font-pic-3.png

我的实践──将字符压缩到极限

我的方法可以理解为子集化的一种,只不过我的做法更加的极端一些──只保留文章出现的字符

根据统计结果,截止到这篇post发布,我的文章总共出现的所有字符数不到1200个(数据来源见下文),所以我们可以做的更激进一些,只需将文章出现的中文字符全部记录下来,制成一张专属于自己网站的字符表,然后在每次发布文章时动态更新,这样我们能够保证字体完整渲染,并且处于边界极限状态!

实现这个个性化字符表char_set.txt的核心是一个提取文章中文字符的算法。这部分我是通过Gemini生成了一个update_lists.cpp文件,他能够识别_posts/下面所有文章,并输出到根目录的char_set.txt中,你可以根据代码内容进行自定义的修改:

/**
* @file update_lists.cpp
* @brief Scans Markdown files in /_posts/ and updates char_set.txt in root.
* @author Gemini
* @date 2025-11-28
*/

#include
#include
#include
#include
#include
#include
namespace fs = std::filesystem;
namespace char_collector {
const std::string kRegistryFilename = "char_set.txt";
const std::string kMarkdownExt = ".md";

const uint32_t kCJKStart = 0x4E00;
const uint32_t kCJKEnd = 0x9FFF;

bool NextUtf8Char(std::string::const_iterator& it,
const std::string::const_iterator& end,
uint32_t& out_codepoint,
std::string& out_bytes)
{
if (it == end) return false;
unsigned char c1 = static_cast<unsigned char>(*it);
out_bytes.clear();
out_bytes += c1;
if (c1 < 0x80) { out_codepoint = c1; it++; return true; }
if ((c1 & 0xE0) == 0xC0) {
if (std::distance(it, end) < 2) return false;
unsigned char c2 = static_cast<unsigned char>(*(it + 1));
out_codepoint = ((c1 & 0x1F) << 6) | (c2 & 0x3F);
out_bytes += *(it + 1); it += 2; return true;
}
if ((c1 & 0xF0) == 0xE0) {
if (std::distance(it, end) < 3) return false;
unsigned char c2 = static_cast<unsigned char>(*(it + 1));
unsigned char c3 = static_cast<unsigned char>(*(it + 2));
out_codepoint = ((c1 & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
out_bytes += *(it + 1); out_bytes += *(it + 2); it += 3; return true;
}
if ((c1 & 0xF8) == 0xF0) {
if (std::distance(it, end) < 4) return false;
unsigned char c2 = static_cast<unsigned char>(*(it + 1));
unsigned char c3 = static_cast<unsigned char>(*(it + 2));
unsigned char c4 = static_cast<unsigned char>(*(it + 3));
out_codepoint = ((c1 & 0x07) << 18) | ((c2 & 0x3F) << 12) |
((c3 & 0x3F) << 6) | (c4 & 0x3F);
out_bytes += *(it + 1); out_bytes += *(it + 2); out_bytes += *(it + 3); it += 4; return true;
}
it++; return false;
}

bool IsChineseChar(uint32_t codepoint) {
return (codepoint >= kCJKStart && codepoint <= kCJKEnd);
}

class CharManager {
public:
CharManager() = default;

void LoadExistingChars(const std::string& filepath) {
std::ifstream infile(filepath);
if (!infile.is_open()) {

std::cout << "Info: " << filepath << " not found or empty. Starting fresh." << std::endl;
return;
}
std::string line;
while (std::getline(infile, line)) {
ProcessString(line, false);
}
std::cout << "Loaded " << existing_chars_.size()
<< " unique characters from " << filepath << "." << std::endl;
}

void ScanDirectory(const std::string& directory_path) {

if (!fs::exists(directory_path)) {
std::cerr << "Error: Directory '" << directory_path << "' does not exist." << std::endl;
return;
}

for (const auto& entry : fs::directory_iterator(directory_path)) {
if (entry.is_regular_file() &&
entry.path().extension() == kMarkdownExt) {
ProcessFile(entry.path().string());
}
}
}

void SaveNewChars(const std::string& filepath) {
if (new_chars_list_.empty()) {
std::cout << "No new Chinese characters found." << std::endl;
return;
}
std::ofstream outfile(filepath, std::ios::app);
if (!outfile.is_open()) {
std::cerr << "Error: Could not open " << filepath << " for writing." << std::endl;
return;
}
for (const auto& ch : new_chars_list_) {
outfile << ch;
}
std::cout << "Successfully added " << new_chars_list_.size()
<< " new characters to " << filepath << std::endl;
}

private:
std::unordered_set existing_chars_;
std::vector new_chars_list_;

void ProcessFile(const std::string& filepath) {
std::ifstream file(filepath);
if (!file.is_open()) return;

std::cout << "Scanning: " << fs::path(filepath).filename().string() << std::endl;
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>())
;
ProcessString(content, true);
}

void ProcessString(const std::string& content, bool track_new) {
auto it = content.begin();
auto end = content.end();
uint32_t codepoint;
std::string bytes;

while (NextUtf8Char(it, end, codepoint, bytes)) {
if (IsChineseChar(codepoint)) {
if (existing_chars_.find(bytes) == existing_chars_.end()) {
existing_chars_.insert(bytes);
if (track_new) {
new_chars_list_.push_back(bytes);
}
}
}
}
}
};

}

int main() {
char_collector::CharManager manager;
manager.LoadExistingChars(char_collector::kRegistryFilename);
manager.ScanDirectory("_posts");
manager.SaveNewChars(char_collector::kRegistryFilename);
return 0;
}

然后我们在终端编译一下再运行即可:

clang++ update_lists.cpp -o update_lists  && ./update_lists

然后我们就会发现这张独属于本站的字符表生成了!🥳 font-pic-6.png 为了方便操作,我们把原始的ttf文件放入仓库的/FontRepo/下(最后记得在.gitignore添加这个文件夹!),然后稍微修改一下之前子集化的命令就可以了:

pyftsubset /FontRepo/NotoSerifSC-Bold.ttf --output-file=/assets/fonts/noto-serif-sc/NotoSerifSC-Bold.subset.woff2 --flavor=woff2 --text-file=char_set.txt --no-hinting --with-zopfli

可以看到,最终输出的文件只有200K!压缩率达到了98.5%!

font-pic-4.png 但是这个方法就像前面说的,处于字体渲染的边界。但凡多出一个字符表中的符号,那么这个字符就无法渲染,会回退到系统字体,看起来格外别扭。所以,在每次更新文章前,我们都需要运行一下./update_lists。此外,还存在一个问题,每次更新产生新的子集化文件时,都需要把旧的子集化文件删除,防止旧文件堆积。

这些过程十分繁琐而且耗费时间,所以我们可以写一个bash脚本来实现这个过程的自动化。我这里同样是求助了Gemini,写了一个build_fonts.sh

#!/bin/bash
set -e # 遇到错误立即停止执行

# ================= 配置区域 =================
# 字体源文件目录
SRC_DIR="FontRepo"
# 字体输出目录
OUT_DIR="assets/fonts/noto-serif-sc"
# 字符列表文件
CHAR_LIST="char_set.txt"
# C++ 更新工具
UPDATE_TOOL="./updateLists"

# 确保输出目录存在
if [ ! -d "$OUT_DIR" ]; then
echo "创建输出目录: $OUT_DIR"
mkdir -p "$OUT_DIR"
fi

# ================= 第一步:更新字符表 =================
echo "========================================"
echo ">> [1/3] 正在更新字符列表..."
if [ -x "$UPDATE_TOOL" ]; then
$UPDATE_TOOL
else
echo "错误: 找不到可执行文件 $UPDATE_TOOL 或者没有执行权限。"
echo "请尝试运行: chmod +x updateLists"
exit 1
fi
# 检查 char_set.txt 是否成功生成
if [ ! -f "$CHAR_LIST" ]; then
echo "错误: $CHAR_LIST 未找到,字符表更新可能失败。"
exit 1
fi
echo "字符列表更新完成。"
# ================= 定义子集化处理函数 =================
process_font() {
local font_name="$1" # 例如: NotoSerifSC-Regular
local input_ttf="$SRC_DIR/${font_name}.ttf"
local final_woff2="$OUT_DIR/${font_name}.woff2"
local temp_woff2="$OUT_DIR/${font_name}.temp.woff2"

echo "----------------------------------------"
echo "正在处理字体: $font_name"
# 检查源文件是否存在
if [ ! -f "$input_ttf" ]; then
echo "错误: 源文件 $input_ttf 不存在!"
exit 1
fi

# 2. 调用 fonttools (pyftsubset) 生成临时子集文件
# 使用 --obfuscate-names 可以进一步减小体积,但这里只用基础参数以保证稳定性
echo "正在生成子集 (TTF -> WOFF2)..."
pyftsubset "$input_ttf" \
--flavor=woff2 \
--text-file="$CHAR_LIST" \
--output-file="$temp_woff2"
# 3. & 4. 删除旧文件并重命名 (更新逻辑)
if [ -f "$temp_woff2" ]; then
if [ -f "$final_woff2" ]; then
echo "删除旧文件: $final_woff2"
rm "$final_woff2"
fi

echo "重命名新文件: $temp_woff2 -> $final_woff2"
mv "$temp_woff2" "$final_woff2"
echo ">>> $font_name 更新成功!"
else
echo "错误: 子集化失败,未生成目标文件。"
exit 1
fi
}
# ================= 第二步 & 第三步:执行转换 =================
echo "========================================"
echo ">> [2/3] 开始字体子集化处理..."
# 处理 Regular 字体
process_font "NotoSerifSC-Regular"
# 处理 Bold 字体
process_font "NotoSerifSC-Bold"
echo "========================================"
echo ">> [3/3] 所有任务圆满完成!"

如此一来,以后每次更新完文章,都只需要在终端输入./build_fonts.sh就可以完成字符提取、字体包子集化、清除旧字体包文件的过程了。

font-pic-5.png

一点感想

在这之前另外讲个小故事,我尝试更换字体之前发现自定义的字体样式根本没有用,后来检查了很久,发现竟然是2个月前AI在我代码里加的一句font-family:'Noto Serif SC',而刚好他修改的又是优先级最高的文件,所以后面怎么修改字体都没有用。所以有时候让AI写代码前最好先搞清除代码的地位i,并且做好为AI代码后果负全责的准备。

更改网站字体其实很多时候属于锦上添花的事情,因为很多读者其实并不会太在意网站的字体。但不幸的是我对细节比较在意,或者说有种敝帚自珍的感觉吧,想慢慢地把网站装饰得舒适一些,所以才总是花力气在一些细枝末节的事情上。更何况,我是懂一点点设计的,有时候看见一些非常丑的Interface心里是很难受的。尽管就像绝大部分人理解不了设计师在细节上的别有用心一样,绝大部分人也不会在意一个网站的字体如何,但是我自己的家,我想装饰地好看些,对我来说就满足了。

更不要说,如果不去折腾这些东西,怎么可能会有这篇文章呢?如果能够帮助到一些人,也算是在世界留下一点价值了。

参考资料及附录

  1. 参考资料

    a. 网页中文字体加载速度优化

    b. 缩减网页字体大小

    c. All-Chinese-Character-Set

  2. 让Gemini生成代码时的Prompt:
---Prompt 1---
# 任务名称:创建脚本实现对字符的收集
请利用C++来完成一下任务要求:
1. 该脚本能够读取项目目录下的markdown文件,并且能够识别当中所有的中文字符,将该中文字符与`/char_test/GeneralUsedChars.txt`的字符表进行查重比较:
若该字在表中存在,则跳过,处理下一个字;
若不存在,则将该字添加到表中,然后继续处理下一个字符
2. 请设计一个高效的算法,尤其是在字符查重的过程中,你需要设计一个高效且准确率高的算法
3. 请注意脚本的通用性,你需要考虑到这个项目以后可能会继续增加更多的markdown文件,所以你不应该仅仅只是处理现有的markdown文件,还需要考虑到以后的拓展性
4. 如果可以的话,尽可能使用C++来实现,因为效率更高

---Prompt 2---
可以了,现在我要求你编写一个脚本以实现自动化,要求如下:
1. 脚本运行时,首先会调用项目根目录下的updateLists可执行文件,更新char_set.txt
2. 接着,脚本会调用fonttools工具,对路径在`/FontRepo/`下的两个文件进行ttf到woff2的子集化转化,其中这两个字体文件的名字分别为`NotoSerifSC-Regular.ttf`和`NotoSerifSC-Bold.ttf`。
3. 转化好的子集文件应该输出到 `/assets/fonts/noto-serif-sc/`文件夹下。
4. 将`/assets/fonts/noto-serif-sc/`文件夹下原本已经存在的两个字体文件`NotoSerifSC-Bold.woff2`和`NotoSerifSC-Regular.woff2`删除,然后将新得到子集化文件重新命名为这两个删除了的文件的名字。这一步相当于完成了字体文件的更新

请注意文件的命名,尤其是不要搞错字号,新子集文件和旧子集文件。
请注意在子集化步骤的bash命令,环境已经安装好fonttools及其对应依赖,你可以参考下面这个命令来使用,或者使用更好更稳定的用法:
pyftsubset --flavor=woff2 --text-file= --output-file=
(再次注意输出路径)
  1. 最终实践效果(以NotoSerifSC-Bold为例)
    处理方式字体包体积压缩率
    无处理14.462M0%
    格式转化5.776M60.06%
    子集化处理981K93.21%
    分包处理依据动态加载量而定
    我的实践216K98.5%


作者:ChangYo
来源:juejin.cn/post/7578699866181238822
收起阅读 »

一个AI都无法提供的html转PDF方案

web
这也许就是AI无法代替人的原因,只需一行代码就可以实现纯前端 html 转矢量 pdf 的功能 // 引入 dompdf.js库 import dompdf from "dompdf.js"; dompdf(document.querySelector("#...
继续阅读 »

这也许就是AI无法代替人的原因,只需一行代码就可以实现纯前端 html 转矢量 pdf 的功能


// 引入 dompdf.js库
import dompdf from "dompdf.js";

dompdf(document.querySelector("#capture")).then(function (blob) {
//文件操作
});

实现效果(复杂表格)


企业微信截图_20251011152750.png


1. 在线体验



dompdfjs.lisky.com.cn



2. Git 仓库地址 (欢迎 Star⭐⭐⭐)



github.com/lmn1919/dom…



3. 生成 PDF


在前端生态里,把网页内容生成 PDF 一直是一个常见但不简单的需求。从报表导出、小票生成、合同下载到打印排版,很多项目或多或少都会遇到。市面上常见的方案大致有以下几类:



  • 服务端渲染 PDF(后端库如 wkhtmltopdf、PrinceXML 等)

  • 客户端将 HTML 渲染为图片(如 html2canvas + jsPDF)然后再封装为 PDF

  • 前端调用相关 pdf 生成库来生成 PDF(如 pdfmake,jspdf,pdfkit)


但是这些方案都有各自的局限性,



  • 比如服务端渲染 PDF 对服务器资源要求高,需要后端参与。

  • html2canvas + jsPDF 需要将 html 内容渲染为图片,再将图片封装为 PDF,速度会比较慢,而且生成体积会比较大,内容会模糊,限制于 canvas 生成高度,不能生成超过 canvas 高度的内容。

  • 而前端调用相关 pdf 生成库来生成 PDF 则需要对相关库有一定的了解,api 比较复杂,学习使用成本很高。


使用 jspdf 生成如图简单的 pdf


企业微信截图_20251011173729.png


就需要如此复杂的代码,如果要生成复杂的 pdf, 比如包含表格、图片、图表等内容,那使用成本就更高了。


function generateChinesePDF() {
// Check if jsPDF is loaded
if (typeof window.jspdf === "undefined") {
alert("jsPDF library has not finished loading, please try again later");
return;
}

const { jsPDF } = window.jspdf;
const doc = new jsPDF();

// Note: Default jsPDF does not support Chinese, this is just a demo
// In real projects you need to add Chinese font support

doc.setFontSize(16);
doc.text("Chinese Text Support Demo", 20, 30);

doc.setFontSize(12);
doc.text("Note: Default jsPDF does not support Chinese characters.", 20, 50);
doc.text("You need to add Chinese font support for proper display.", 20, 70);

// Draw some graphics for demonstration
doc.setFillColor(255, 182, 193);
doc.rect(20, 90, 60, 30, "F");
doc.setTextColor(0, 0, 0);
doc.text("Pink Rectangle", 25, 108);

doc.setFillColor(173, 216, 230);
doc.rect(100, 90, 60, 30, "F");
doc.text("Light Blue Rectangle", 105, 108);

doc.save("chinese-example.pdf");
}

但是现在,有了 dompdf.js,你只需要一行代码,就可以完成比这个复杂 10 倍的 PDF 生成任务,html页面所见即所得,可以将复杂的css样式转化成pdf


dompdf(document.querySelector("#capture")).then(function (blob) {
//文件操作
});

而且,dompdf.js 生成的 PDF 是矢量的,非图片式的,高清晰度的,文字可以选中、复制、搜索等操作(在支持的 PDF 阅读器环境下),区别于客户端将 HTML 渲染为图片(如 html2canvas + jsPDF)然后再封装为 PDF。



具体可以去体验 立即体验 https://dompdfjs.lisky.com.cn



4. dompdf.js 是如何实现的?


其实 dompdf.js 也是基于 html2canvas+jspdf 实现的,但是为什么 dompdf.js 生成的 pdf 文件可以二次编辑,更清晰,体积小呢?


不同于普通的 html2canvas + jsPDF 方案,将 dom 内容生成为图片,再将图片内容用 jspdf 绘制到 pdf 上,这就导致了生成的 pdf 文件体积大,无法编辑,放大后会模糊。


html2canvas 原理简介


1. DOM 树遍历
html2canvas 从指定的 DOM 节点开始,递归遍历所有子节点,构建一个描述页面结构的内部渲染队列。


2. 样式计算
对每个节点调用 window.getComputedStyle() 获取最终的 CSS 属性值。这一步至关重要,因为它包含了所有 CSS 规则(内联、内部、外部样式表)层叠计算后的最终结果。


3. 渲染模型构建
将每个 DOM 节点和其计算样式封装成渲染对象,包含绘制所需的完整信息:位置(top, left)、尺寸(width, height)、背景、边框、文本内容、字体属性、层级关系(z-index)等。


4. Canvas 上下文创建
在内存中创建 canvas 元素,获取其 2D 渲染上下文(CanvasRenderingContext2D)。


5. 浏览器绘制模拟
按照 DOM 的堆叠顺序和布局规则,遍历渲染队列,将每个元素绘制到 Canvas 上。这个过程实质上是将 CSS 属性"翻译"成对应的绘制 API 调用:


CSS 属性传统 Canvas APIdompdf.js 中的 jsPDF API
background-colorctx.fillStyle + ctx.fillRect()doc.setFillColor() + doc.rect(x, y, w, h, 'F')
borderctx.strokeStyle + ctx.strokeRect()doc.setDrawColor() + doc.rect(x, y, w, h, 'S')
color, font-family, font-sizectx.fillStyle, ctx.font + ctx.fillText()doc.setTextColor() + doc.setFont() + doc.text()
border-radiusarcTo()bezierCurveTo() 创建剪切路径doc.roundedRect()doc.lines() 绘制圆角
imagectx.drawImage()doc.addImage()

核心创新:API 替换,底层是封装了 jsPDF 的 API
dompdf.js 的关键突破在于改造了 html2canvas 的 canvas-renderer.ts 文件,将原本输出到 Canvas 的绘制 API 替换为 jsPDF 的 API 调用。这样就实现了从 DOM 直接到 PDF 的转换,生成真正可编辑、可搜索的 PDF 文件,而不是传统的图片格式。


目前实现的功能


1. 文字绘制 (颜色,大小)
2. 图片绘制 (支持 jpeg, png 等格式)
3. 背景,背景颜色 (支持合并单元格)
4. 边框,复杂表格绘制 (支持合并单元格)
5. canvas (支持多种图表类型)
6. svg (支持 svg 元素绘制)
7. 阴影渲染 (使用 foreignObjectRendering,支持边框阴影渲染)
8. 渐变渲染 (使用 foreignObjectRendering,支持背景渐变渲染)


7.使用


安装


        npm install dompdf.js --save

CDN 引入


<script src="https://cdn.jsdelivr.net/npm/dompdf.js@latest/dist/dompdf.js"></script>

基础用法


import dompdf from "dompdf.js";
dompdf(document.querySelector("#capture"), {
useCORS: true, //是否允许跨域
})
.then(function (blob) {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "example.pdf";
document.body.appendChild(a);
a.click();
})
.catch(function (err) {
console.log(err, "err");
});

写在最后


dompdf.js 让前端 PDF 生成变得前所未有的简单:无需后端、无需繁琐配置、一行代码即可输出矢量、可检索、可复制的专业文档。无论是简历、报告还是发票,它都能轻松胜任。 欢迎在你的项目中使用它 。


如果它帮到了你,欢迎去 github.com/lmn1919/dom… 点个 Star,提优化,共建项目。


作者:刘发财
来源:juejin.cn/post/7559886023661649958
收起阅读 »

从前端的角度出发,目前最具性价比的全栈路线是啥❓❓❓

web
我正在筹备一套前端工程化体系的实战课程。如果你在学习前端的过程中感到方向模糊、技术杂乱无章,那么前端工程化将是你实现系统进阶的最佳路径。它不仅能帮你建立起对现代前端开发的整体认知,还能提升你在项目开发、协作规范、性能优化等方面的工程能力。 ✅ 本课程覆盖构建工...
继续阅读 »

我正在筹备一套前端工程化体系的实战课程。如果你在学习前端的过程中感到方向模糊、技术杂乱无章,那么前端工程化将是你实现系统进阶的最佳路径。它不仅能帮你建立起对现代前端开发的整体认知,还能提升你在项目开发、协作规范、性能优化等方面的工程能力。


✅ 本课程覆盖构建工具测试体系脚手架CI/CDDockerNginx 等核心模块,内容体系完整,贯穿从开发到上线的全流程。每一章节都配有贴近真实场景的企业级实战案例,帮助你边学边用,真正掌握现代团队所需的工程化能力,实现从 CRUD 开发者到工程型前端的跃迁。


详情请看前端工程化实战课程


学完本课程,对你的简历和具体的工作能力都会有非常大的提升。如果你对此项目感兴趣,或者课程感兴趣,可以私聊我微信 yunmz777


今年大部分时间都是在编码上和写文章上,但是也不知道自己都学到了啥,那就写篇文章来盘点一下目前的技术栈吧,也作为下一年的参考目标,方便知道每一年都学了些啥。


20241223154451


我的技术栈


首先我先来对整体的技术做一个简单的介绍吧,然后后面再对当前的一些技术进行细分吧。


React、Typescript、React Native、mysql、prisma、NestJs、Redis、前端工程化。


React


React 这个框架我花的时间应该是比较多的了,在校期间已经读了一遍源码了,对这些原理已经基本了解了。在随着技术的继续深入,今年毕业后又重新开始阅读了一遍源码,对之前的认知有了更深一步的了解。


也写了比较多跟 React 相关的文章,包括设计模式,原理,配套生态的使用等等都有一些涉及。


在状态管理方面,redux,zustand 我都用过,尤其在 Zustand 的使用上,我特别喜欢 Zustand,它使得我能够快速实现全局状态管理,同时避免了传统 Redux 中繁琐的样板代码,且性能更优。也对 Zustand 有比较深入的了解,也对其源码有过研究。


NextJs


Next.js 是一个基于 React 的现代 Web 开发框架,它为开发者提供了一系列强大的功能和工具,旨在优化应用的性能、提高开发效率,并简化部署流程。Next.js 支持多种渲染模式,包括服务器端渲染(SSR)、静态生成(SSG)和增量静态生成(ISR),使得开发者可以根据不同的需求选择合适的渲染方式,从而在提升页面加载速度的同时优化 SEO。


在路由管理方面,Next.js 采用了基于文件系统的路由机制,这意味着开发者只需通过创建文件和文件夹来自动生成页面路由,无需手动配置。这种约定优于配置的方式让路由管理变得直观且高效。此外,Next.js 提供了动态路由支持,使得开发者可以轻松实现复杂的 URL 结构和参数化路径。


Next.js 还内置了 API 路由,允许开发者在同一个项目中编写后端 API,而无需独立配置服务器。通过这种方式,前后端开发可以在同一个代码库中协作,大大简化了全栈开发流程。同时,Next.js 对 TypeScript 提供了原生支持,帮助开发者提高代码的可维护性和可靠性。


Typescript


今年所有的项目都是在用 ts 写了,真的要频繁修改的项目就知道用 ts 好处了,有时候用 js 写的函数修改了都不知道怎么回事,而用了 ts 之后,哪里引用到的都报红了,修改真的非常方便。


今年花了一点时间深入学习了一下 Ts 类型,对一些高级类型以及其实现原理也基本知道了,明年还是多花点时间在类型体操上,除了算法之外,感觉类型体操也可以算得上是前端程序员的内功心法了。


React Native



不得不说,React Native 不愧是接活神器啊,刚学完之后就来了个安卓和 ios 的私活,虽然没有谈成。



React Native 和 Expo 是构建跨平台移动应用的两大热门工具,它们都基于 React,但在功能、开发体验和配置方式上存在一些差异。React Native 是一个开放源代码的框架,允许开发者使用 JavaScript 和 React 来构建 iOS 和 Android 原生应用。Expo 则是一个构建在 React Native 之上的开发平台,它提供了一套工具和服务,旨在简化 React Native 开发过程。


React Native 的核心优势在于其高效的跨平台开发能力。通过使用 React 语法和组件,开发者能够一次编写应用的 UI 和逻辑,然后部署到 iOS 和 Android 平台。React Native 提供了对原生模块的访问,使开发者能够使用原生 API 来扩展应用的功能,确保性能和用户体验能够接近原生应用。


Expo 在此基础上进一步简化了开发流程。作为一个开发工具,Expo 提供了许多内置的 API 和组件,使得开发者无需在项目中进行繁琐的原生模块配置,就能够快速实现设备的硬件访问功能(如摄像头、位置、推送通知等)。Expo 还内置了一个开发客户端,使得开发者可以实时预览应用,无需每次都进行完整的构建和部署。


另外,Expo 提供了一个完全托管的构建服务,开发者只需将应用推送到 Expo 服务器,Expo 就会自动处理 iOS 和 Android 应用的构建和发布。这大大简化了应用的构建和发布流程,尤其适合不想处理复杂原生配置的开发者。


然而,React Native 和 Expo 也有各自的局限性。React Native 提供更大的灵活性和自由度,开发者可以更自由地集成原生代码或使用第三方原生库,但这也意味着需要更多的配置和维护。Expo 则封装了很多功能,简化了开发,但在需要使用某些特定原生功能时,开发者可能需要“弹出”Expo 的托管环境,进行额外的原生开发。


样式方案的话我使用的是 twrnc,大部分组件都是手撸,因为有 cursor 和 chatgpt 的加持,开发效果还是杠杠的。


rn 原理也争取明年能多花点时间去研究研究,不然对着盲盒开发还是不好玩。


Nestjs


NestJs 的话没啥好说的,之前也都写过很多篇文章了,感兴趣的可以直接观看:



对 Nodejs 的底层也有了比较深的理解了:



Prisma & mysql


Prisma 是一个现代化的 ORM(对象关系映射)工具,旨在简化数据库操作并提高开发效率。它支持 MySQL 等关系型数据库,并为 Node.js 提供了类型安全的数据库客户端。在 NestJS 中使用 Prisma,可以让开发者轻松定义数据库模型,并通过自动生成的 Prisma Client 执行类型安全的查询操作。与 MySQL 配合时,Prisma 提供了一种简单、直观的方式来操作数据库,而无需手动编写复杂的 SQL 查询。


Prisma 的核心优势在于其强大的类型安全功能,所有的数据库操作都能通过 Prisma Client 提供的自动生成的类型来进行,这大大减少了代码中的错误,提升了开发的效率。它还包含数据库迁移工具 Prisma Migrate,能够帮助开发者方便地管理数据库结构的变化。此外,Prisma Client 的查询 API 具有很好的性能,能够高效地执行复杂的数据库查询,支持包括关系查询、聚合查询等高级功能。


与传统的 ORM 相比,Prisma 使得数据库交互更加简洁且高效,减少了配置和手动操作的复杂性,特别适合在 NestJS 项目中使用,能够与 NestJS 提供的依赖注入和模块化架构很好地结合,提升整体开发体验。


Redis


Redis 和 mysql 都仅仅是会用的阶段,目前都是直接在 NestJs 项目中使用,都是已经封装好了的,直接传参调用就好了:


import { Injectable, Inject, OnModuleDestroy, Logger } from "@nestjs/common";
import Redis, { ClientContext, Result } from "ioredis";

import { ObjectType } from "../types";

import { isObject } from "@/utils";

@Injectable()
export class RedisService implements OnModuleDestroy {
private readonly logger = new Logger(RedisService.name);

constructor(@Inject("REDIS_CLIENT") private readonly redisClient: Redis) {}

onModuleDestroy(): void {
this.redisClient.disconnect();
}

/**
* @Description: 设置值到redis中
* @param {string} key
* @param {any} value
* @return {*}
*/

public async set(
key: string,
value: unknown,
second?: number
): Promise<Result<"OK", ClientContext> | null> {
try {
const formattedValue = isObject(value)
? JSON.stringify(value)
: String(value);

if (!second) {
return await this.redisClient.set(key, formattedValue);
} else {
return await this.redisClient.set(key, formattedValue, "EX", second);
}
} catch (error) {
this.logger.error(`Error setting key ${key} in Redis`, error);

return null;
}
}

/**
* @Description: 获取redis缓存中的值
* @param key {String}
*/

public async get(key: string): Promise<string | null> {
try {
const data = await this.redisClient.get(key);

return data ? data : null;
} catch (error) {
this.logger.error(`Error getting key ${key} from Redis`, error);

return null;
}
}

/**
* @Description: 设置自动 +1
* @param {string} key
* @return {*}
*/

public async incr(
key: string
): Promise<Result<number, ClientContext> | null> {
try {
return await this.redisClient.incr(key);
} catch (error) {
this.logger.error(`Error incrementing key ${key} in Redis`, error);

return null;
}
}

/**
* @Description: 删除redis缓存数据
* @param {string} key
* @return {*}
*/

public async del(key: string): Promise<Result<number, ClientContext> | null> {
try {
return await this.redisClient.del(key);
} catch (error) {
this.logger.error(`Error deleting key ${key} from Redis`, error);

return null;
}
}

/**
* @Description: 设置hash结构
* @param {string} key
* @param {ObjectType} field
* @return {*}
*/

public async hset(
key: string,
field: ObjectType
): Promise<Result<number, ClientContext> | null> {
try {
return await this.redisClient.hset(key, field);
} catch (error) {
this.logger.error(`Error setting hash for key ${key} in Redis`, error);

return null;
}
}

/**
* @Description: 获取单个hash值
* @param {string} key
* @param {string} field
* @return {*}
*/

public async hget(key: string, field: string): Promise<string | null> {
try {
return await this.redisClient.hget(key, field);
} catch (error) {
this.logger.error(
`Error getting hash field ${field} from key ${key} in Redis`,
error
);

return null;
}
}

/**
* @Description: 获取所有hash值
* @param {string} key
* @return {*}
*/

public async hgetall(key: string): Promise<Record<string, string> | null> {
try {
return await this.redisClient.hgetall(key);
} catch (error) {
this.logger.error(
`Error getting all hash fields from key ${key} in Redis`,
error
);

return null;
}
}

/**
* @Description: 清空redis缓存
* @return {*}
*/

public async flushall(): Promise<Result<"OK", ClientContext> | null> {
try {
return await this.redisClient.flushall();
} catch (error) {
this.logger.error("Error flushing all Redis data", error);

return null;
}
}

/**
* @Description: 保存离线通知
* @param {string} userId
* @param {any} notification
*/

public async saveOfflineNotification(
userId: string,
notification: any
): Promise<void> {
try {
await this.redisClient.lpush(
`offline_notifications:${userId}`,
JSON.stringify(notification)
);
} catch (error) {
this.logger.error(
`Error saving offline notification for user ${userId}`,
error
);
}
}

/**
* @Description: 获取离线通知
* @param {string} userId
* @return {*}
*/

public async getOfflineNotifications(userId: string): Promise<any[]> {
try {
const notifications = await this.redisClient.lrange(
`offline_notifications:${userId}`,
0,
-1
);
await this.redisClient.del(`offline_notifications:${userId}`);

return notifications.map((notification) => JSON.parse(notification));
} catch (error) {
this.logger.error(
`Error getting offline notifications for user ${userId}`,
error
);

return [];
}
}

/**
* 获取指定 key 的剩余生存时间
* @param key Redis key
* @returns 剩余生存时间(秒)
*/

public async getTTL(key: string): Promise<number> {
return await this.redisClient.ttl(key);
}
}

前端工程化


前端工程化这块花了很多信息在 eslint、prettier、husky、commitlint、github action 上,现在很多项目都是直接复制之前写好的过来就直接用。


后续应该是投入更多的时间在性能优化、埋点、自动化部署上了,如果有机会的也去研究一下 k8s 了。


全栈性价比最高的一套技术


最近刷到一个帖子,讲到了


20241223165138


我目前也算是一个小全栈了吧,我也来分享一下我的技术吧:



  1. NextJs

  2. React Native

  3. prisma

  4. NestJs

  5. taro (目前还不会,如果有需求就会去学)


剩下的描述也是和他下面那句话一样了(毕业后对技术态度的转变就是什么能让我投入最小,让我最快赚到钱的就是好技术)


总结


学无止境,任重道远。


最后再来提一下这两个开源项目,它们都是我们目前正在维护的开源项目:



如果你想参与进来开发或者想进群学习,可以添加我微信 yunmz777,后面还会有很多需求,等这个项目完成之后还会有很多新的并且很有趣的开源项目等着你。


作者:Moment
来源:juejin.cn/post/7451483063568154639
收起阅读 »

还在用html2canvas?介绍一个比它快100倍的截图神器!

web
在日常业务开发里,DOM 截图 几乎是刚需场景。 无论是生成分享卡片、导出报表,还是保存一段精美排版内容,前端同学都绕不开它。 但问题来了——市面上的截图工具,比如 html2canvas,虽然用得多,却有一个致命缺陷:慢! 普通截图动辄 1 秒以上,大一点的...
继续阅读 »

在日常业务开发里,DOM 截图 几乎是刚需场景。


无论是生成分享卡片、导出报表,还是保存一段精美排版内容,前端同学都绕不开它。


但问题来了——市面上的截图工具,比如 html2canvas,虽然用得多,却有一个致命缺陷:慢!
普通截图动辄 1 秒以上,大一点的 DOM,甚至能直接卡到怀疑人生,用户体验一言难尽。


大家好,我是芝士,欢迎点此扫码加我微信 Hunyi32 交流,最近创建了一个低代码/前端工程化交流群,欢迎加我微信 Hunyi32 进群一起交流学习,也可关注我的公众号[ 前端界 ] 持续更新优质技术文章


最近发现一个保存速度惊艳到我的库snapDOM


这货在性能上的表现,完全可以用“碾压”来形容:



  • 👉 相比 html2canvas,快 32 ~ 133 倍

  • 👉 相比 modern-screenshot,也快 2 ~ 93 倍



以下是官方基本测试数据:


场景snapDOM vs html2canvassnapDOM vs dom-to-image
小元素 (200×100)32 倍6 倍
模态框 (400×300)33 倍7 倍
整页截图 (1200×800)35 倍13 倍
大滚动区域 (2000×1500)69 倍38 倍
超大元素 (4000×2000)93 倍 🔥133 倍

📊 数据来源:snapDOM 官方 benchmark(基于 headless Chromium 实测)。


⚡ 为什么它这么快?


二者的实现原理不同


html2canvas 的实现方式



  • 原理:
    通过遍历 DOM,把每个节点的样式(宽高、字体、背景、阴影、图片等)计算出来,然后在 <canvas> 上用 Canvas API 重绘一遍。

  • 特点:



    • 需要完整计算 CSS 样式 → 排版 → 绘制。

    • 复杂 DOM 时计算量极大,比如渐变、阴影、字体渲染都会消耗 CPU。

    • 整个过程基本是 模拟浏览器的渲染引擎,属于“重造轮子”。




所以一旦 DOM 大、样式复杂,html2canvas 很容易出现 1s+ 延迟甚至卡死。


snapDOM 的实现方式


原理:利用浏览器 原生渲染能力,而不是自己模拟


snapDOM 的 captureDOM 并不是自己用 Canvas API 去一笔一笔绘制 DOM(像 html2canvas 那样),而是:



  1. 复制 DOM 节点(prepareClone)



    • → 生成一个“克隆版”的 DOM,里面包含了样式、结构。



  2. 把图片、背景、字体都转成 inline(base64 / dataURL)



    • → 确保克隆 DOM 是完全自包含的。



  3. <foreignObject> 包在 SVG 里面



    • → 浏览器原生支持直接渲染 HTML 片段到 SVG → 再转成 dataURL。




所以核心就是:
👉 利用浏览器自己的渲染引擎(SVG foreignObject)来排版和绘制,而不是 JS 重造渲染过程


如何使用 snapDOM


snapDOM 上手非常简单, 学习成本比较低。


1. 安装


通过npm 安装:


npm install @zumer/snapdom

或者直接用 CDN 引入:


<script src="https://cdn.jsdelivr.net/npm/@zumer/snapdom/dist/snapdom.min.js"></script>

2. 基础用法


只需要一行代码,就能把 DOM 节点“变”成图片:


// 选择你要截图的 DOM 元素
const target = document.querySelector('.card');

// 导出为 PNG 图片
const image = await snapdom.toPng(target);

// 直接添加到页面
document.body.appendChild(image);


3. 更多导出方式


除了 PNG,snapDOM 还支持多种输出格式:


// 导出为 JPEG
const jpeg = await snapdom.toJpeg(target);

// 导出为 SVG
const svg = await snapdom.toSvg(target);

// 直接保存为文件
await snapdom.download(target, { format: 'png', filename: 'screenshot.png' });

4. 导出一个这样的海报图



开发中生成海报并保存到, 是非常常见的需求,以前使用html2canvas,也要写不少代码, 还要处理图片失真等问题, 使用snapDOM,真的一行代码能搞定。


 <div ref="posterRef" class="poster">
....
</div>

<script setup lang="ts">
const downloadPoster = async () => {
if (!posterRef.value) {
alert("海报元素未找到");
return;
}

try {
// snapdom 是 UMD 格式,通过全局 window.snapdom 访问
const snap = (window as any).snapdom;
if (!snap) {
alert("snapdom 库未加载,请刷新页面重试");
return;
}
await snap.download(posterRef.value, {
format: "png",
filename: `tech-poster-${Date.now()}`
});


} catch (error) {
console.error("海报生成失败:", error);
alert("海报生成失败,请重试");
}
};
</script>

相比传统方案需要大量配置和兼容性处理,snapDOM 真正做到了 一行代码,极速生成。无论是分享卡片、营销海报还是报表导出,都能轻松搞定。


await snap.download(posterRef.value, {
format: "png",
filename: `tech-poster-${Date.now()}`
});

大家好,我是芝士,欢迎点此扫码加我微信 Hunyi32 交流,最近创建了一个低代码/前端工程化交流群,欢迎加我微信 Hunyi32 进群一起交流学习,也可关注我的公众号[ 前端界 ] 持续更新优质技术文章


总结


在前端开发里,DOM 截图是一个常见但“让人头疼”的需求。



  • html2canvas 代表的传统方案,虽然功能强大,但性能和体验常常拖后腿;

  • 而 snapDOM 借助浏览器原生渲染能力,让截图变得又快又稳。


一句话:
👉 如果你还在为截图慢、卡顿、模糊烦恼,不妨试试 snapDOM —— 可能会刷新你对前端截图的认知。 🚀


作者:芝士加
来源:juejin.cn/post/7542379658522116123
收起阅读 »

10年老前端吐槽Tailwind CSS:是神器还是“神坑”?

web
作为一个老前端人比大家虚长几岁,前端技术的飞速发展,从早期的 jQuery 到现代的 React、Vue,再到 CSS 框架的演变。最近几年,Tailwind CSS 成为了前端圈的热门话题,很多人称它为“神器”,但也有不少人认为它是“神坑”。今天,我就从实际...
继续阅读 »

作为一个老前端人比大家虚长几岁,前端技术的飞速发展,从早期的 jQuery 到现代的 React、Vue,再到 CSS 框架的演变。最近几年,Tailwind CSS 成为了前端圈的热门话题,很多人称它为“神器”,但也有不少人认为它是“神坑”。今天,我就从实际项目经验出发,吐槽一下 Tailwind CSS 的弊端。


HTML 代码臃肿,可读性差


例如:


<div class="p-4 bg-white rounded-lg shadow-md text-gray-800 hover:bg-gray-100">
Content
</div>

以至于前端同学前来吐槽,这和写style有个毛线区别。
虽然tailwind提供了@apply方法,将常用的实用类代码提取到css中,来减少html的代码量


.btn {
@apply p-4 bg-blue-500 text-white rounded-lg hover:bg-blue-600;
}

可以一个大型项目样式复杂,常常看到这样的场景:


.btn1 {
@apply p-4 bg-blue-500 text-white rounded-lg hover:bg-blue-600;
}
.btn2 {
@apply p-6 bg-blue-500 text-blue rounded-lg hover:bg-blue-600;
}
.btn3 {
@apply p-8 bg-blue-500 text-red rounded-lg hover:bg-blue-600;
}
.btn4 {
@apply p-10 bg-blue-500 rounded-lg hover:bg-green-600;
}

读起来都费劲。


增加了前端同学的学习成本


开发是必须学习大量的tailwind的实用类,并且要花时间学习这些命名规则,例如:



  • p-4 是 padding,m-4 是 margin。

  • text-sm 是小字体,text-lg 是大字体。

  • bg-blue-500 是背景色,text-blue-500 是文字颜色。


国内的项目大家都知道,不是在赶工期,就是在赶工期的路上,好多小伙伴开发的时候直接就上style了。


当然这样也有好处,能使用JIT模式,按需生成css,减少文件的大小。避免了以前项目中好多无用的css。


动态样式支持有限


有时候需要动态生成的类会导致错误,例如:


<div class="text-{{ color }}-500 bg-{{ bgColor }}-100">
Content
</div>

如果color被复制为green,但是系统并没有定义 text-green-500 这个类。让项目变得难以调试和维护。


总结


Tailwind CSS 是一把双刃剑,它既能为开发带来极大的便利,也可能成为项目的“神坑”。作为开发者,我们需要根据项目需求,合理使用 Tailwind,并通过一些最佳实践规避它的弊端。希望这篇文章能帮助你在项目中更好地使用 Tailwind CSS,享受它带来的便利,同时避免踩坑!


如果你有更多关于 Tailwind CSS 的问题或经验分享,欢迎在评论区留言讨论! 😊


作者:yaoganjili
来源:juejin.cn/post/7484638486994681890
收起阅读 »

🔄一张图,让你再也忘不了浏览器的事件循环(Event Loop)了

web
一、前言下面纯手工画了一张在浏览器执行JavaScript代码的Event Loop(事件循环) 流程图。后文会演示几个例子,把示例代码放到这个流程图演示其执行流程。当然,这只是简单的事件循环流程,不过,却能让我们快速掌握其原理。二、概念事件循环是J...
继续阅读 »

一、前言

下面纯手工画了一张在浏览器执行JavaScript代码的Event Loop(事件循环) 流程图。

后文会演示几个例子,把示例代码放到这个流程图演示其执行流程。

当然,这只是简单的事件循环流程,不过,却能让我们快速掌握其原理。

Event Loop.png

二、概念

事件循环JavaScript为了处理单线程执行代码时,能异步地处理用户交互、网络请求等任务 (异步Web API),而设计的一套任务调度机制。它就像一个永不停止的循环,不断地检查(结合上图就是不断检查Task QueueMicrotask Queue这两个队列)并需要运行的代码。


三、为什么需要事件循环

JavaScript是单线程的,这意味着它只有一个主线程来执行代码。如果所有任务(比如一个耗时的计算、一个网络请求)都同步执行,那么浏览器就会被卡住,无法响应用户的点击、输入,直到这个任务完成。这会造成极差的用户体验。

事件循环就是为了解决这个问题而生的:它让耗时的操作(如网络请求、文件读取)在后台异步执行,等这些操作完成后,再通过回调的方式来执行相应的代码,从而不阻塞主线程

四、事件循环流程图用法演示

演示一:小菜一碟

先来一个都是同步代码的小菜,先了解一下前面画的流程图是怎样在调用栈当中执行JavaScript代码的。

console.log(1)

function funcOne() {
console.log(2)
}

function funcTwo() {
funcOne()
console.log(3)
}

funcTwo()

console.log(4)

控制台输出:

1 2 3 4

下图为调用栈执行流程

演示01.png

每执行完一个同步任务会把该任务进行出栈。在这个例子当中每次在控制台输出一次,则进行一次出栈处理,直至全部代码执行完成。

演示二:小试牛刀

setTimeout+Promise组合拳,了解异步代码是如何进入任务队列等待执行的。

console.log(1)

setTimeout(() => {
console.log('setTimeout', 2)
}, 0)

const promise = new Promise((resolve, reject) => {
console.log('promise', 3)
resolve(4)
})

setTimeout(() => {
console.log('setTimeout', 5)
}, 10)

promise.then(res => {
console.log('then', res)
})

console.log(6)

控制台输出:

1 promise 3 6 then 4 setTimeout 2 setTimeout 5

流程图执行-步骤一:

先执行同步代码,如遇到异步代码,则把异步回调事件放到后台监听对应的任务队列

image.png

  1. 执行console.log(1),控制台输出1
  2. 执行定时器,遇到异步代码,后台注册定时器回调事件,时间到了,把回调函数() => {console.log('setTimeout', 2)},放到宏任务队列等待。
  3. 执行创建Promise实例,并执行其中同步代码:执行console.log('promise', 3),控制台输出promise 3;执行resolve(4),此时Promise已经确定为完成fulfilled状态,把promise.then()的回调函数响应值设为4
  4. 执行定时器,遇到异步代码,后台注册定时器回调事件,时间未到,把回调函数() => { console.log('setTimeout', 5) }放到后台监听。
  5. 执行promise.then(res => { console.log('then', res) }),出栈走异步代码,把回调函数4 => { console.log('then', 4) }放入微任务队列等待。

流程图执行-步骤二:

上面已经把同步代码执行完成,并且把对应异步回调事件放到了指定任务队列,接下来开始事件循环

image.png

  1. 扫描微任务队列,执行4 => { console.log('then', 4) }回调函数,控制台输出then 4
  2. 微任务队列为空,扫描宏任务队列,执行() => {console.log('setTimeout', 2)}回调函数,控制台输出setTimeout 2
  3. 每执行完一个宏任务,需要再次扫描微任务队列是否存在可执行任务(假设此时后台定时到了,则会把() => { console.log('setTimeout', 5) }加入到了宏任务队列末尾)。
  4. 微任务队列为空,扫描宏任务队列,执行() => { console.log('setTimeout', 5) },控制台输出setTimeout 5

演示三:稍有难度

setTimeout+Promise组合拳+多层嵌套Promise

console.log(1)

setTimeout(() => {
console.log('setTimeout', 10)
}, 0)

new Promise((resolve, reject) => {
console.log(2)
resolve(7)

new Promise((resolve, reject) => {
resolve(5)
}).then(res => {
console.log(res)

new Promise((resolve, reject) => {
resolve('嵌套第三层 Promise')
}).then(res => {
console.log(res)
})
})

Promise.resolve(6).then(res => {
console.log(res)
})

}).then(res => {
console.log(res)
})

new Promise((resolve, reject) => {
console.log(3)

Promise.resolve(8).then(res => {
console.log(res)
})

resolve(9)
}).then(res => {
console.log(res)
})

console.log(4)

上一个演示说明了流程图执行的详细步骤,下面就不多加赘叙了,直接看图!

talk is cheap, show me the chart

image.png

上图,调用栈同步代码执行完成,开始事件循环,先看微任务队列,发现不为空,按顺序执行微任务事件:

嵌套02.png

上图,已经把刚才排队的微任务队列全部清空了。但是在执行第一个微任务时,发现还有嵌套微任务,则把该任务放到微任务队列末尾,然后接着一起执行完所有新增任务

嵌套03.png

最后微任务清空后,接着执行宏任务。到此全部事件已执行完毕!

控制台完整输出顺序:

1 2 3 4 5 6 7 8 9 10

演示四:setTimeout伪定时

setTimeout并不是设置的定时到了就马上执行,而是把定时回调放在task queue任务队列当中进行等待,待主线程调用栈中的同步任务执行完成后空闲时才会执行。

const startTime = Date.now()
setTimeout(() => {
const endTime = Date.now()
console.log('setTimeout cost time', endTime - startTime)
// setTimeout cost time 2314
}, 100)

for (let i = 0; i < 300000; i++) {
// 模拟执行耗时同步任务
console.log(i)
}

控制台输出:

1 2 3 ··· 300000 setTimeout cost time 2314

下图演示了其执行流程:

setTimeout假延时.png

演示五:fetch网络请求和setTimeout

获取网络数据,fetch回调函数属于微任务,优于setTimeout先执行。

setTimeout(() => {
console.log('setTimeout', 2)
}, 510)

const startTime = Date.now()
fetch('http://localhost:3000/test').then(res => {
const endTime = Date.now()
console.log('fetch cost time', endTime - startTime)
return res.json()
}).then(data => {
console.log('data', data)
})

下图当前Call Stack执行栈执行完同步代码后,由于fetchsetTimeout都是宏任务,所以走宏任务Web API流程后注册这两个事件回调,等待定时到后了,由于定时回调是个普通的同步函数,所以放到宏任务队列;等待fetch拿到服务器响应数据后,由于fetch回调为一个Promise对象,所以放到微任务队列。

fetch.png

经过多番刷新网页测试,下图控制台打印展示了setTimeout延时为510msfetch请求响应同样是510ms的情况下,.then(data => { console.log('data', data) })先执行了,也是由于fetch基于Promise实现,所以其回调为微任务。

b475cbb38b0161d3e7f5f97b45824b31.png

五、结语

这可能只是简单的JavaScript代码执行事件循环流程,目的也是让大家更直观理解其中原理。实际执行过程可能还会读取堆内存获取引用类型数据、操作dom的方法,可能还会触发页面的重排、重绘等过程、异步文件读取和写入操作、fetch发起网络请求,与服务器建立连接获取网络数据等情况。

但是,它们异步执行的回调函数都会经过图中的这个事件循环过程,从而构成完整的浏览器事件循环。


作者:vilan_微澜
来源:juejin.cn/post/7577395040592756746

收起阅读 »

单点登录:一次登录,全网通行

大家好,我是小悟。 想象一下你去游乐园,买了一张通票(登录),然后就可以玩所有项目(访问各个系统),不用每个项目都重新买票(重新登录)。这就是单点登录(SSO)的精髓! SSO的日常比喻 普通登录:像去不同商场,每个都要查会员卡 单点登录:像微信扫码登录,...
继续阅读 »

大家好,我是小悟。



  • 想象一下你去游乐园,买了一张通票(登录),然后就可以玩所有项目(访问各个系统),不用每个项目都重新买票(重新登录)。这就是单点登录(SSO)的精髓!


    SSO的日常比喻



    • 普通登录:像去不同商场,每个都要查会员卡

    • 单点登录:像微信扫码登录,一扫全搞定

    • 令牌:像游乐园手环,戴着就能证明你买过票


    下面用代码来实现这个"游乐园通票系统":


    代码实现:简易SSO系统


    import java.util.*;

    // 用户类 - 就是我们这些想玩项目的游客
    class User {
    private String username;
    private String password;

    public User(String username, String password) {
    this.username = username;
    this.password = password;
    }

    // getters 省略...
    }

    // 令牌类 - 游乐园手环
    class Token {
    private String tokenId;
    private String username;
    private Date expireTime;

    public Token(String username) {
    this.tokenId = UUID.randomUUID().toString();
    this.username = username;
    // 令牌1小时后过期 - 游乐园晚上要关门的!
    this.expireTime = new Date(System.currentTimeMillis() + 3600 * 1000);
    }

    public boolean isValid() {
    return new Date().before(expireTime);
    }

    // getters 省略...
    }

    // SSO认证中心 - 游乐园售票处
    class SSOAuthCenter {
    private Map<String, Token> validTokens = new HashMap<>();
    private Map<String, User> users = new HashMap<>();

    public SSOAuthCenter() {
    // 预先注册几个用户 - 办了年卡的游客
    users.put("zhangsan", new User("zhangsan", "123456"));
    users.put("lisi", new User("lisi", "abcdef"));
    }

    // 登录 - 买票入场
    public String login(String username, String password) {
    User user = users.get(username);
    if (user != null && user.getPassword().equals(password)) {
    Token token = new Token(username);
    validTokens.put(token.getTokenId(), token);
    System.out.println(username + " 登录成功!拿到游乐园手环:" + token.getTokenId());
    return token.getTokenId();
    }
    System.out.println("用户名或密码错误!请重新买票!");
    return null;
    }

    // 验证令牌 - 检查手环是否有效
    public boolean validateToken(String tokenId) {
    Token token = validTokens.get(tokenId);
    if (token != null && token.isValid()) {
    System.out.println("手环有效,欢迎继续玩耍!");
    return true;
    }
    System.out.println("手环无效或已过期,请重新登录!");
    validTokens.remove(tokenId); // 清理过期令牌
    return false;
    }

    // 登出 - 离开游乐园
    public void logout(String tokenId) {
    validTokens.remove(tokenId);
    System.out.println("已登出,欢迎下次再来玩!");
    }
    }

    // 业务系统A - 过山车
    class SystemA {
    private SSOAuthCenter authCenter;

    public SystemA(SSOAuthCenter authCenter) {
    this.authCenter = authCenter;
    }

    public void accessSystem(String tokenId) {
    System.out.println("=== 欢迎来到过山车 ===");
    if (authCenter.validateToken(tokenId)) {
    System.out.println("过山车启动!尖叫声在哪里!");
    } else {
    System.out.println("请先登录再玩过山车!");
    }
    }
    }

    // 业务系统B - 旋转木马
    class SystemB {
    private SSOAuthCenter authCenter;

    public SystemB(SSOAuthCenter authCenter) {
    this.authCenter = authCenter;
    }

    public void accessSystem(String tokenId) {
    System.out.println("=== 欢迎来到旋转木马 ===");
    if (authCenter.validateToken(tokenId)) {
    System.out.println("木马转起来啦!找回童年记忆!");
    } else {
    System.out.println("请先登录再玩旋转木马!");
    }
    }
    }

    // 测试我们的SSO系统
    public class SSODemo {
    public static void main(String[] args) {
    // 创建认证中心 - 游乐园大门
    SSOAuthCenter authCenter = new SSOAuthCenter();

    // 张三登录
    String token = authCenter.login("zhangsan", "123456");

    if (token != null) {
    // 拿着同一个令牌玩不同项目
    SystemA systemA = new SystemA(authCenter);
    SystemB systemB = new SystemB(authCenter);

    systemA.accessSystem(token); // 玩过山车
    systemB.accessSystem(token); // 玩旋转木马

    // 登出
    authCenter.logout(token);

    // 再尝试访问 - 应该被拒绝
    systemA.accessSystem(token);
    }

    // 测试错误密码
    authCenter.login("lisi", "wrongpassword");
    }
    }

    运行结果示例:


    zhangsan 登录成功!拿到游乐园手环:a1b2c3d4-e5f6-7890-abcd-ef1234567890
    === 欢迎来到过山车 ===
    手环有效,欢迎继续玩耍!
    过山车启动!尖叫声在哪里!
    === 欢迎来到旋转木马 ===
    手环有效,欢迎继续玩耍!
    木马转起来啦!找回童年记忆!
    已登出,欢迎下次再来玩!
    === 欢迎来到过山车 ===
    手环无效或已过期,请重新登录!
    请先登录再玩过山车!
    用户名或密码错误!请重新买票!

    总结一下:


    单点登录就像:



    • 一次认证,处处通行 🎫

    • 不用重复输入密码 🔑

    • 安全又方便 👍


    好的SSO系统就像好的游乐园管理,既要让游客玩得开心,又要确保安全!



单点登录:一次登录,全网通行.png


谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。


您的一键三连,是我更新的最大动力,谢谢


山水有相逢,来日皆可期,谢谢阅读,我们再会


我手中的金箍棒,上能通天,下能探海


作者:悟空码字
来源:juejin.cn/post/7577599015426228259
收起阅读 »

别让认知天花板,变成你的职业终点——技术人如何走出信息茧房

我们都活在自己编织的真相里吗? 最近参加了两位前端同事的转正答辩。他们的总结都很真诚,也很努力,但两人之间那种微妙的认知落差,却让我久久不能平静。 第一位同事踏实勤恳,技术中规中矩,对自己的评价也相对保守。他清楚自己的短板,也知道自己需要提升,但他把改变的希...
继续阅读 »

我们都活在自己编织的真相里吗?



最近参加了两位前端同事的转正答辩。他们的总结都很真诚,也很努力,但两人之间那种微妙的认知落差,却让我久久不能平静。


第一位同事踏实勤恳,技术中规中矩,对自己的评价也相对保守。他清楚自己的短板,也知道自己需要提升,但他把改变的希望寄托在别人身上——希望有人来指导他、推动他。可实际上,他并没有拿出具体的行动或方案。没有外力,他似乎就只能停在原地。看着他,我忽然想起几年前的自己:是不是也曾这样,一边焦虑,一边等待别人来拯救?


第二位同事则完全不同。他自信满满,言谈间流露出对自身能力的高度认可,甚至认为自己已经达到了“高级工程师”的水平。他在团队协作、沟通表达上也自认表现优异,列举了不少“高光时刻”。但当我站在更高的视角去看这些“成果”时,却感到一种强烈的错位——他的“高级”,建立在一个极其有限的认知框架之上。


他对跨团队协作的理解,停留在“配合顺畅”;对技术深度的衡量,止步于“功能能跑”。他看不到系统设计中的耦合隐患,意识不到架构演进背后的权衡逻辑,更缺乏对业务本质的追问。


那一刻我突然明白:一个人最深的局限,往往不是能力不足,而是根本不知道自己不知道


而更令人警觉的是,这种认知盲区,并非个例。它像一层透明的茧,包裹着我们每一个人。我们常称之为“信息茧房”,但或许更准确的说法是:认知茧房




被这个时代悄悄做局了



我们生活在一个信息爆炸的时代,但获取信息的方式,却被前所未有地“驯化”了。


算法精准推送你喜欢的内容,社交圈层不断强化你已有的观点,搜索引擎只呈现你愿意相信的答案。你读的每一篇文章、看的每一段视频、加入的每一个群聊,都在悄悄加固这层茧。


久而久之,我们开始相信:



  • 我看到的就是真实的世界;

  • 我认同的就是正确的道理;

  • 我周围人的共识就是普世的价值。


于是,偏执悄然滋生。我们变得难以倾听异见,习惯性地把不同声音归为“愚蠢”或“别有用心”。沟通不再是交换思想,而成了立场的对抗。我们活在由自己偏好构建的“回音室”里,每一次回响都让我们更加确信:我没错,错的是世界


那位自认“高级”的同事,问题不在技术本身,而在于他无法感知更高维度的存在。他没见过真正的系统复杂性,没经历过技术债务的反噬,也没体会过从0到1推动变革的艰难。他的“高级”,只是井底之蛙眼中的天空。




偏听则暗,兼听则明



信息茧房最危险的地方,在于它让人变得偏执。偏执的人很难沟通,再加上“傻子共振效应”(相似认知的人互相强化),整个世界的理解就越来越狭隘。


在这个被算法投喂的时代,你看到的,都是你想看的;你喜欢的,也被不断推给你。这不断强化你的认知,形成你的价值观,同时也压缩了你的视野、削弱了你的思辨能力,最终让你陷入一个自我闭环的逻辑牢笼


你开始以为自己了解世界的运作原理,甚至以为自己看清了本质。但最致命的是——你不觉得自己被局限了


我不禁反思:

我知道的,是不是错的?

我是不是也在自我麻痹?

我是不是一只井底之蛙,而我身边的人,也都是井底之蛙?我们彼此认同,形成联盟,却浑然不觉自己被困在同一个井里。


更可怕的是,这种“共识”会让我们坚信:我们不在茧房里。




睁眼看世界,看到的未必是真实


你看到的世界,可能是别人想让你看到的,也可能是你自己想让自己看到的。你可能并不知道真实的世界是什么样,但你以为你知道


如今的大数据和智能推荐,正不断按照你的喜好,一层层加固你的信息茧房。人类的惰性,又被海量信息投喂成“奶头乐”——不加思考,被动消费,逐渐沦为信息时代的消费品。


当然,我写下这些,也可能带着某种激进甚至偏激。但正因如此,才更要停下来问一问:

我是否也陷在这样的焦油坑里?


我是不是也自以为是、固步自封?

是不是只听自己想听的,只信自己愿意信的?

是不是被网络言论洗脑,被算法圈套套牢?


我常常做一些自以为不错的事,但别人可能完全不会那样做。这提醒我:我的“正确”,未必是世界的尺度


不要被无意义的信息重塑价值观。真正的出路,必须从内在出发。




认知决定你能活出怎样的人生



我们常说:“人赚不到认知以外的钱。”其实这句话可以更广义地理解为:人活不出认知以外的人生


你的决策、判断、人际关系、职业发展,甚至对幸福的定义,都被你当下的认知边界框定。如果你的认知是扁平的、片面的、情绪化的,那么无论多努力,你也只能在同一个维度里打转。


要破局,第一步是承认自己被困住了

这不是自我否定,而是一种清醒的自知。真正的成长,始于一个简单的念头:

“这样是不是太受限了?”


这个念头,就是打破茧房的第一道裂痕。




如何让裂痕扩大,直至破茧?



主动“反向输入”:让异见成为养分

不要只读你认同的书,只听你支持的声音。刻意去接触那些让你不适的观点:



  • 读一本政治立场与你对立的作者写的书;

  • 看一部挑战你价值观的纪录片;

  • 和一个你平时不会交往的人深入聊一次天。


重点不是说服对方,而是理解:他为什么这么想?


拓展“认知半径”:走出同类人的圈子

你周围的人,往往和你有相似的背景、价值观和信息来源。这种“同温层”会不断强化你的既有认知。试着去接触:



  • 不同行业的人(比如医生、教师、手艺人);

  • 不同年龄段的人(年轻人的焦虑,老人的智慧);

  • 不同文化背景的人(你会发现,很多你视为“理所当然”的事,在别处根本不存在)。


成年后,我们的圈子越来越小,思维越来越固化。这时候,更需要主动打破圈层的束缚。


3保持“空杯心态”:允许自己被推翻

最可怕的不是无知,而是以为自己知道。定期问自己:



  • 我三年前相信什么?现在还信吗?

  • 如果我现在的观点是错的,世界会是什么样子?

  • 有没有可能,我引以为傲的“成就”,其实只是低水平的重复?


这种自我质疑,不是自我贬低,而是一种精神上的“排毒”。




站在更高的维度看问题


当我开始带团队后,才发现下属的问题会暴露无遗。这也反过来提醒我:站得更高,才能看得更清


或许我们永远无法抵达“全知”的境界,但至少可以仰望星空。


写到这里,我也在警惕:

这会不会是另一种“认知优越感”?

我是否也在用“破茧”的叙事,构建一个新的茧?


很可能。

但关键不在于是否彻底摆脱茧房——那几乎不可能——而在于是否保有觉察和挣扎的意愿


我们无法看到全貌,但可以努力多转几个角度;

我们无法摆脱偏见,但可以学会与之共处并保持警惕;

我们可能永远都是井底之蛙,但至少可以抬头,看看那圈之外,是否还有星光。


或许,我们永远无法完全摆脱茧房



这个世界越来越复杂,而我们的认知工具却未必同步进化。

算法在固化我们,信息在淹没我们,社交在同化我们。


但人之所以为人,正是因为我们有反思的能力,有超越当下的渴望


不必追求绝对的“正确”,也不必幻想彻底的“觉醒”。

只需在每一个自以为是的瞬间,轻轻问一句:


“我是不是,又忘了抬头?”


认知的破局,不在远方,就在此刻的怀疑与开放之中


作者:uzong
来源:juejin.cn/post/7580592190020517922
收起阅读 »

老板:能不能别手动复制路由了?我:写个脚本自动扫描

web
起因 周五快下班,老板过来看权限配置页面。 "这个每次都要手动输路径?" "对,现在是这样。"我打开给他看: 角色:运营专员 路由路径:[手动输入] /user/list 组件路径:[手动输入] @/views/user/List.vue "上次运营配错了,...
继续阅读 »

起因


周五快下班,老板过来看权限配置页面。


"这个每次都要手动输路径?"


"对,现在是这样。"我打开给他看:


角色:运营专员
路由路径:[手动输入] /user/list
组件路径:[手动输入] @/views/user/List.vue

"上次运营配错了,/user/list 写成 /user/lists,页面打不开找了半天。能不能做个下拉框,直接选?"


我想了想:"可以,但得先有个页面列表。"


"那就搞一个,现在页面这么多,手动输容易出错。"


确实,项目现在几十个页面,每次配置权限都要翻代码找路径,复制粘贴,还担心复制错。


解决办法


周末琢磨了一下,其实就是缺个"页面清单"。views 目录下都是页面文件,扫描一遍不就有了?


写了个 Node 脚本,自动扫描 views 目录,生成路由映射表。配置权限的时候下拉框选,还能搜索。


实现


两步:扫描文件 + 生成映射。


扫描 .vue 文件


function getAllVueFiles(dir, filesList = []) {
const files = fs.readdirSync(dir);

files.forEach((file) => {
const filePath = path.join(dir, file);

if (fs.statSync(filePath).isDirectory()) {
getAllVueFiles(filePath, filesList); // 递归子目录
} else if (file.endsWith(".vue")) {
filesList.push(filePath); // 收集 .vue 文件
}
});

return filesList;
}

生成映射文件


function start() {
const viewsDir = path.resolve(__dirname, "../views");
let files = getAllVueFiles(viewsDir);

// 兼容 Windows 路径
files = files.map(item => item.replace(/\\/g, "/"));

// 拼接成 import 映射
let str = "";
files.forEach(item => {
let n = item.replace(/.*src\//, "@/");
str += `"${n}":()=>import("${n}"),\r\n`;
});

// 写入文件
fs.writeFileSync(
path.resolve(__dirname, "../router/all.router.js"),
`export const ROUTERSDATA = {\n${str}}`
);
}

最后生成的文件大概是这样:


// src/router/all.router.js
export const ROUTERSDATA = {
"@/views/Home.vue": () => import("@/views/Home.vue"),
"@/views/About.vue": () => import("@/views/About.vue"),
"@/views/user/List.vue": () => import("@/views/user/List.vue"),
}

怎么用


权限配置页面


<template>
<el-select
v-model="selectedRoute"
filterable
placeholder="搜索并选择页面">
<el-option
v-for="(component, path) in ROUTERSDATA"
:key="path"
:label="path"
:value="path">
{{ path }}
</el-option>
</el-select>
</template>

<script setup>
import { ROUTERSDATA } from '@/router/all.router.js'

// 后台返回的权限路由配置
const permissionRoutes = [
{ path: '/user/list', component: '@/views/user/List.vue' },
{ path: '/order/list', component: '@/views/order/List.vue' }
]

// 直接从映射表取组件
const routes = permissionRoutes.map(route => ({
path: route.path,
component: ROUTERSDATA[route.component] // 这里直接用
}))
</script>

好处:



  • 下拉框自动包含所有页面

  • 支持搜索,输入 "user" 就能找到所有用户相关页面

  • 新加页面自动出现在列表里


动态路由


后台返回权限配置,前端从映射表取组件:


function generateRoutes(backendConfig) {
return backendConfig.map(item => ({
path: item.path,
component: ROUTERSDATA[item.component] // 直接用
}))
}

效果


周一把代码提上去,改了权限配置页面:


image.png


<!-- 配置页面 -->
<el-select v-model="route" filterable placeholder="搜索页面">
<el-option
v-for="(component, path) in ROUTERSDATA"
:key="path"
:label="path"
:value="path" />
</el-select>

老板过来试了一下,在下拉框输入 "user" 就搜到所有用户相关页面。


"嗯,这个好用。新加页面也会自动出现在这里吧?"


"对,每次启动项目会自动扫描。"


"行,那就这样。"


后来发现还有些意外收获:



  • 新人看这个映射表就知道项目有哪些页面

  • 后台只存路径字符串,数据库干净

  • 顺带解决了手动 import 几十个路由的问题


package.json 加个脚本:


{
"scripts": {
"dev": "node src/start/index.js && vite"
}
}

每次 npm run dev 会先扫描 views 目录,生成最新的映射表。


完整代码


// src/start/index.js
const fs = require("fs");
const path = require("path");

function getAllVueFiles(dir, filesList = []) {
const files = fs.readdirSync(dir);

files.forEach((file) => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);

if (stat.isDirectory()) {
getAllVueFiles(filePath, filesList);
} else if (file.endsWith(".vue")) {
filesList.push(filePath);
}
});

return filesList;
}

function start() {
console.log("[自动获取全部可显示页面]");

const viewsDir = path.resolve(__dirname, "../views");
let files = getAllVueFiles(viewsDir);

// 统一路径分隔符,兼容 Windows 反斜杠
files = files.map((item) => item.replace(/\\/g, "/"));

let str = "";
// 构造 import 映射:"@/views/xxx.vue": ()=>import("@/views/xxx.vue")
files.forEach((item) => {
let n = item.replace(/.*src\//, "@/");
str += `"${n}":()=>import("${n}"),\r\n`;
});

const routerFilePath = path.resolve(__dirname, "../router/all.router.js");
// 将映射写入路由聚合文件,供路由动态引用
fs.writeFileSync(
routerFilePath,
`
export const ROUTERSDATA = {
${str}
}`
,
);
console.log("[./src/router/all.router.js 写入]");
}

start();

注意事项


记得把生成的 src/router/all.router.js 加到 .gitignore,毕竟是自动生成的文件,没必要提交。


# .gitignore
src/router/all.router.js

后来


用了一个多月,运营配置权限再也没出过错。上周老板说:"这个功能不错,其他项目也加上。"


代码其实挺简单的,但确实解决了问题。


作者:码是生活
来源:juejin.cn/post/7582808491583504420
收起阅读 »

进入外包,我犯了所有程序员都会犯的错!

前言 前些天有位小伙伴和我吐槽他在外包工作的经历,语气颇为激动又带着深深的无奈。 本篇以他的视角,进入他的世界,看看这一段短暂而平凡的经历。 1. 上岸折戟尘沙 本人男,安徽马鞍山人士,21年毕业于江苏某末流211,在校期间转码。 上网课期间就向往大城市,...
继续阅读 »

前言


前些天有位小伙伴和我吐槽他在外包工作的经历,语气颇为激动又带着深深的无奈。



image.png


本篇以他的视角,进入他的世界,看看这一段短暂而平凡的经历。


1. 上岸折戟尘沙


本人男,安徽马鞍山人士,21年毕业于江苏某末流211,在校期间转码。

上网课期间就向往大城市,于是毕业后去了深圳,找到了一家中等IT公司(人数500+)搬砖,住着宝安城中村,来往繁华南山区。

待了三年多,自知买房变深户无望,没有归属感,感觉自己也没那么热爱技术,于是乎想回老家考公务员,希望待在宇宙的尽头。

24年末,匆忙备考,平时工作忙里偷闲刷题,不出所料,笔试卒,梦碎。


2. 误入外包


复盘了备考过程,觉得工作占用时间过多,想要找一份轻松点且离家近的工作,刚好公司也有大礼包的指标,于是主动申请,辞别深圳,前往徽京。

Boss上南京的软件大部分是外包(果然是外包之都),前几年外包还很活跃,这些年外包都沉寂了不少,找了好几个月,断断续续有几个邀约,最后实在没得选了,想着反正就过渡一下挣点钱不寒碜,接受了外包,作为WX服务某为。薪资比在深圳降了一些,在接受的范围内。


想着至少苟着等待下一次考公,因此前期做项目比较认真,遇到问题追根究底,为解决问题也主动加班加点,同为WX的同事都笑话我说比自有员工还卷,我却付之一笑。


直到我经历了几件事,正所谓人教人教不会,事教人一教就会。


3. 我在外包的二三事


有一次,我提出了自有员工设计方案的衍生出的一个问题,并提出拉个会讨论一下,他并没有当场答应,而是回复说:我们内部看看。

而后某天我突然被邀请进入会议,聊了几句,意犹未尽之际,突然就被踢出会议...开始还以为是某位同事误触按钮,然后再申请入会也没响应。

后来我才知道,他们内部商量核心方案,因为权限管控问题,我不能参会。

这是我第一次体会到WX和自有员工身份上的隔阂。


还有一次和自有员工一起吃饭的时候,他不小心说漏嘴了他的公积金,我默默推算了一下他的工资至少比我高了50%,而他的毕业院校、工作经验和我差不多,瞬间不平衡了。


还有诸如其它的团建、夜宵、办公权限、工牌等无一不是明示着你是外包员工,要在外包的规则内行事。
至于转正的事,头上还有OD呢,OD转正的几率都很低,好几座大山要爬呢,别想了。


3. 反求诸己


以前网上看到很多吐槽外包的帖子,还总觉得言过其实,亲身经历了才刻骨铭心。

我现在已经摆正了心态,既来之则安之。正视自己WX的身份,给多少钱干多少活,给多少权利就承担多少义务。

不攀比,不讨好,不较真,不内耗,不加班。

另外每次当面讨论的时候,我都会把工牌给露出来,潜台词就是:快看,我就是个外包,别为难我😔~


另外我现在比较担心的是:



万一我考公还是失败,继续找工作的话,这段外包经历会不会是我简历的污点😢



当然这可能是我个人感受,其它外包的体验我不知道,也不想再去体验了。

对,这辈子和下辈子都不想了。
附南京外包之光,想去或者不想去的伙伴可以留意一下:



image.png


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

为什么 SVG 能在现代前端中胜出?

web
如果你关注前端图标的发展,会发现一个现象: 过去前端图标主要有三种方案: PNG 小图(配合雪碧图) Iconfont SVG 到了今天,大部分中大型项目都把图标系统全面迁移到 SVG。 无论 React/Vue 项目、新框架(Next/Remix/Nux...
继续阅读 »

如果你关注前端图标的发展,会发现一个现象:


过去前端图标主要有三种方案:



  • PNG 小图(配合雪碧图)

  • Iconfont

  • SVG


到了今天,大部分中大型项目都把图标系统全面迁移到 SVG。

无论 React/Vue 项目、新框架(Next/Remix/Nuxt),还是大厂的设计规范(Ant Design、Material、Carbon),基本都默认 SVG。


为什么是 SVG 胜出?

为什么不是 Iconfont、不是独立 PNG、不是雪碧图?

答案不是一句“清晰不失真”这么简单。


下面从前端实际开发的角度,把 SVG 胜出的原因讲透。




一、SVG 为什么比位图(PNG/JPG)更强?


矢量图永不失真(核心优势)


PNG/JPG 是位图,只能按像素存图。

移动端倍率屏越来越高(2x、3x、4x……),一张 24px 的 PNG 在 iPhone 高分屏里可能看起来糊成一团。


SVG 是矢量图,数学计算绘制:



  • 任意缩放不糊

  • 任意清晰度场景都不怕

  • 深色模式也不会变形


这点直接解决了前端图标领域长期存在的一个痛点:适配成本太高




体积小、多级复用不浪费


同样一个图标:



  • PNG 做 1x/2x/3x 需要三份资源

  • SVG 只要一份


而且:



  • SVG 本质是文本

  • gzip 压缩非常有效


在 CDN 下,通常能压到个位数 KB,轻松复用。




图标换色非常容易


PNG 改颜色很麻烦:



  • 设计师改

  • 重新导出

  • 重新上传/构建


Iconfont 的颜色只能统一,只能覆盖轮廓颜色,多色很麻烦。


SVG 则非常灵活:


.icon {
fill: currentColor;
}

可以跟随字体颜色变化,支持 hover、active、主题色。


深浅模式切换不需要任何额外资源。




支持 CSS 动画、交互效果


SVG 不只是图标文件,它是 DOM,可以直接加动画:



  • stroke 动画

  • 路径绘制动画

  • 颜色渐变

  • hover 发光

  • 多段路径动态控制


PNG 和 Iconfont 都做不到这种级别的交互。


很多现代 UI 的微动效(Loading、赞、收藏),都是基于 SVG 完成。




二、SVG 为什么比 iconfont 更强?


Iconfont 在 2015~2019 年非常火,但明显已经退潮了。

原因有以下几个:




① 字体图标本质是“字符”而不是图形


这带来大量问题:


● 不能多色


只能 monochrome,彩色图标很难实现。


● 渲染脆弱


在 Windows 某些字体渲染环境下会出现:



  • 发虚

  • 锯齿

  • baseline 不一致


● 字符冲突


不同项目的字体图标可能互相覆盖。


相比之下,SVG 是独立图形文件,没有这些问题。




② iconfont 需要加载字体文件,失败会出现“乱码方块”


如果字体文件没加载成功,你会看到:



☐ ☐ ☐ ☐



这在弱网、支付类页面、海外环境都非常常见。


SVG 就没有这个风险。




③ iconfont 不利于按需加载


字体文件通常包含几十甚至几百个图标:

一次加载很重,不够精细。


SVG 可以做到按需加载:



  • 一个组件一个 SVG

  • 一个页面只引入用到的部分

  • 可组合、可动态切换


对于现代构建体系非常友好。




三、SVG 为什么比“新版雪碧图”更强?


即便抛开 iconfont,PNG 雪碧图也完全被淘汰。


原因很简单:



  • 雪碧图文件大

  • 缓存粒度差

  • 不可按需加载

  • 维护复杂

  • retina 适配麻烦

  • 颜色不可动态变更


而 SVG 天生具备现代开发所需的一切特性:



  • 轻量化

  • 组件化

  • 可变色

  • 可动画

  • 可 inline

  • 可自动 tree-shaking


雪碧图本质上是为了“减少请求数”而生的产物,

但在 HTTP/2/3 中已经没有价值。


而 SVG 不是 hack,而是自然适配现代 Web 的技术方案




四、SVG 为什么能在工程体系里更好地落地?


现代构建工具(Vite / Webpack / Rollup)原生支持 SVG:



  • 转组件

  • 优化路径

  • 压缩

  • 自动雪碧(symbol sprite)

  • Tree-shaking

  • 资源分包


这让 SVG 完全融入工程体系,而不是外挂方案。


例如:


import Logo from './logo.svg'

你可以:



  • 当组件使用

  • 当资源下载

  • 当背景图

  • 动态注入


工程化友好度是它胜出的关键原因之一。




五、SVG 胜出的根本原因总结


不是 SVG “长得好看”,也不是趋势,是整个现代前端生态把它推到了最合适的位置。


1)协议升级:HTTP/2/3 让雪碧图和 Iconfont 的优势全部消失

2)设备升级:高分屏让位图模糊问题暴露得更明显

3)工程升级:组件化开发需要精细化图标

4)体验升级:动画、主题、交互都离不开 SVG


一句话总结:



SVG 不只是“更清晰”,而是从工程到体验全面适配现代前端的图标方案,因此胜出。



作者:吹水一流
来源:juejin.cn/post/7577691061034172462
收起阅读 »

软件工程师必须要掌握的泳道图

作者:面汤放盐 / uzong 在软件开发的世界里,我们习惯用代码表达逻辑,但当系统涉及多个角色、多个服务、甚至跨团队协作时,光靠代码注释或口头沟通,往往不够。这时候,一张清晰的流程图,胜过千行文档。 泳道图 :它可能不像 UML 那样“高大上”,也不如架构...
继续阅读 »

作者:面汤放盐 / uzong



在软件开发的世界里,我们习惯用代码表达逻辑,但当系统涉及多个角色、多个服务、甚至跨团队协作时,光靠代码注释或口头沟通,往往不够。这时候,一张清晰的流程图,胜过千行文档。


泳道图 :它可能不像 UML 那样“高大上”,也不如架构图那样宏观,但在梳理业务流程、厘清责任边界、排查系统瓶颈时,它真的非常实用。


1. 什么是泳道图


泳道图的核心思想很简单:把流程中的每个步骤,按执行者(人、系统、模块)分组排列,就像游泳池里的泳道一样,各走各道,互不干扰又彼此关联。



每一列就是一个“泳道”,代表一个责任主体。流程从左到右、从上到下流动,谁在什么时候做了什么,一目了然




一眼就能看出:谁干了什么,谁依赖谁,边界是什么。


与流程的差异点:



  • 流程图聚焦:“步骤顺序”,侧重 “先做什么、再做什么”,适合梳理线性业务流程;

  • 泳道图聚焦: “流转通道”,侧重 “什么东西在什么约束下通过什么路径流转”,适合拆解复杂、多路径、有规则约束的流转场景(如分布式系统数据同步、供应链物料流转、微服务请求链路等)


2. 泳道图分类


2.1. 垂直泳道图


垂直泳道图采取上下布局结构,‌主要强调职能群体。这种布局方式更适合于展示跨职能任务和流程中,‌各职能部门或角色之间的垂直关系和职能分工。



2.2. 水平泳道图


水平泳道图则采用左右布局结构,‌重点在于事件进程的展示。这种布局方式更适合于强调事件或过程的水平流动,‌以及不同阶段或部门在流程中的水平参与



3. 泳道图组成元素


泳池: 泳池是泳道图的外部框架,泳道、流程都包含于泳池内。


泳道: 泳池里可以创建多个泳道。


流程: 实际的业务流程。


部门: 通过部门或者责任来区分,明确每个部门/人/信息系统负责完成的任务环节。


阶段: 通过任务阶段来区分,明确每个阶段需要处理的任务环节。


4. 泳道图应用场景


4.1. 项目管理


展示项目从启动到完成的各个阶段,明确每个团队或成员在项目中的角色和职责,便于进行项目管理和监控,同时促进团队协作和沟通



4.2. 业务流程分析


展示业务流程的各个环节和涉及的不同部门或职能。通过分析泳道图,可以发现业务流程中的瓶颈、冗余环节或不合理之处,进而进行流程优化和改进。



4.3. 系统设计


展示系统的整体架构和各个组件之间的关系,描述系统的工作流程,包括数据的输入、处理、输出等各个环节,有助于系统开发人员更好地理解系统的功能和需求



5. 更多参考模板


故障处理多维泳道图



资源扩容泳道图



6. 最后


我刚工作时,看到导师抛出一份精致的泳道图,把一团乱麻的问题讲得透亮,心里特别佩服。直到自己多年后用上才真正体会:在面对跨部门协作、复杂故障排查、关键流程设计时,掏出这么一张图,往往就是高效沟通的开始。


作为开发者,我们常陷入“只要代码跑得通就行”的思维惯性。但软件不仅是机器执行的指令,更是人与人协作的媒介。泳道图这样的工具,本质上是在降低认知成本——让复杂的事情变得可沟通、可验证、可迭代。 其实泳道图的核心不是“画图”,而是“梳理流程、明确权责”。


在跨部门、协同需求、故障分析等关键场景使用泳道图是非常合适,并且也能把问题讲清楚。技术世界充满了抽象和复杂性,而优秀工程师的能力之一,就是创建合适的可视化工具,让复杂问题变得简单可见



本文中的大部分图片来源于 ProcessOn,ProcessOn 是一个非常不错的画图软件,功能强大,界面优美。



作者:uzong
来源:juejin.cn/post/7580423629164068916
收起阅读 »

我发现很多程序员都不会打日志。。。

你是小阿巴,刚入职的低级程序员,正在开发一个批量导入数据的程序。 没想到,程序刚上线,产品经理就跑过来说:小阿巴,用户反馈你的程序有 Bug,刚导入没多久就报错中断了! 你赶紧打开服务器,看着比你发量都少的报错信息: 你一脸懵逼:只有这点儿信息,我咋知道哪里...
继续阅读 »

你是小阿巴,刚入职的低级程序员,正在开发一个批量导入数据的程序。


没想到,程序刚上线,产品经理就跑过来说:小阿巴,用户反馈你的程序有 Bug,刚导入没多久就报错中断了!


你赶紧打开服务器,看着比你发量都少的报错信息:



你一脸懵逼:只有这点儿信息,我咋知道哪里出了问题啊?!


你只能硬着头皮让产品经理找用户要数据,然后一条条测试,看看是哪条数据出了问题……


原本大好的摸鱼时光,就这样无了。


这时,你的导师鱼皮走了过来,问道:小阿巴,你是持矢了么?脸色这么难看?



你无奈地说:皮哥,刚才线上出了个 bug,我花了 8 个小时才定位到问题……


鱼皮皱了皱眉:这么久?你没打日志吗?


你很是疑惑:谁是日志?为什么要打它?



鱼皮叹了口气:唉,难怪你要花这么久…… 来,我教你打日志!


⭐️ 本文对应视频版:bilibili.com/video/BV1K7…


什么是日志?


鱼皮打开电脑,给你看了一段代码:


@Slf4j
public class UserService {
   public void batchImport(List<UserDTO> userList) {
       log.info("开始批量导入用户,总数:{}", userList.size());
       
       int successCount = 0;
       int failCount = 0;
       
       for (UserDTO userDTO : userList) {
           try {
               log.info("正在导入用户:{}", userDTO.getUsername());
               validateUser(userDTO);
               saveUser(userDTO);
               successCount++;
               log.info("用户 {} 导入成功", userDTO.getUsername());
          } catch (Exception e) {
               failCount++;
               log.error("用户 {} 导入失败,原因:{}", userDTO.getUsername(), e.getMessage(), e);
          }
      }
       
       log.info("批量导入完成,成功:{},失败:{}", successCount, failCount);
  }
}

你看着代码里的 log.infolog.error,疑惑地问:这些 log 是干什么的?


鱼皮:这就是打日志。日志用来记录程序运行时的状态和信息,这样当系统出现问题时,我们可以通过日志快速定位问题。



你若有所思:哦?还可以这样!如果当初我的代码里有这些日志,一眼就定位到问题了…… 那我应该怎么打日志?用什么技术呢?


怎么打日志?


鱼皮:每种编程语言都有很多日志框架和工具库,比如 Java 可以选用 Log4j 2、Logback 等等。咱们公司用的是 Spring Boot,它默认集成了 Logback 日志框架,你直接用就行,不用再引入额外的库了~



日志框架的使用非常简单,先获取到 Logger 日志对象。


1)方法 1:通过 LoggerFactory 手动获取 Logger 日志对象:


public class MyService {
   private static final Logger logger = LoggerFactory.getLogger(MyService.class);
}

2)方法 2:使用 this.getClass 获取当前类的类型,来创建 Logger 对象:


public class MyService {
   private final Logger logger = LoggerFactory.getLogger(this.getClass());
}

然后调用 logger.xxx(比如 logger.info)就能输出日志了。


public class MyService {
   private final Logger logger = LoggerFactory.getLogger(this.getClass());

   public void doSomething() {
       logger.info("执行了一些操作");
  }
}

效果如图:



小阿巴:啊,每个需要打日志的类都要加上这行代码么?


鱼皮:还有更简单的方式,使用 Lombok 工具库提供的 @Slf4j 注解,可以自动为当前类生成日志对象,不用手动定义啦。


@Slf4j
public class MyService {
   public void doSomething() {
       log.info("执行了一些操作");
  }
}

上面的代码等同于 “自动为当前类生成日志对象”:


private static final org.slf4j.Logger log = 
   org.slf4j.LoggerFactory.getLogger(MyService.class);

你咧嘴一笑:这个好,爽爽爽!



等等,不对,我直接用 Java 自带的 System.out.println 不也能输出信息么?何必多此一举?


System.out.println("开始导入用户" + user.getUsername());

鱼皮摇了摇头:千万别这么干!


首先,System.out.println 是一个同步方法,每次调用都会导致耗时的 I/O 操作,频繁调用会影响程序的性能。



而且它只能输出信息到控制台,不能灵活控制输出位置、输出格式、输出时机等等。比如你现在想看三天前的日志,System.out.println 的输出早就被刷没了,你还得浪费时间找半天。



你恍然大悟:原来如此!那使用日志框架就能解决这些问题吗?


鱼皮点点头:没错,日志框架提供了丰富的打日志方法,还可以通过修改日志配置文件来随心所欲地调教日志,比如把日志同时输出到控制台和文件中、设置日志格式、控制日志级别等等。



在下苦心研究日志多年,沉淀了打日志的 8 大邪修秘法,先传授你 2 招最基础的吧。


打日志的 8 大最佳实践


1、合理选择日志级别


第一招,日志分级。


你好奇道:日志还有级别?苹果日志、安卓日志?


鱼皮给了你一巴掌:可不要乱说,日志的级别是按照重要程度进行划分的。



其中 DEBUG、INFO、WARN 和 ERROR 用的最多。



  • 调试用的详细信息用 DEBUG

  • 正常的业务流程用 INFO

  • 可能有问题但不影响主流程的用 WARN

  • 出现异常或错误的用 ERROR


log.debug("用户对象的详细信息:{}", userDTO);  // 调试信息
log.info("用户 {} 开始导入", username);  // 正常流程信息
log.warn("用户 {} 的邮箱格式可疑,但仍然导入", username);  // 警告信息
log.error("用户 {} 导入失败", username, e);  // 错误信息

你挠了挠头:俺直接全用 DEBUG 不行么?


鱼皮摇了摇头:如果所有信息都用同一级别,那出了问题时,你怎么快速找到错误信息?



在生产环境,我们通常会把日志级别调高(比如 INFO 或 WARN),这样 DEBUG 级别的日志就不会输出了,防止重要信息被无用日志淹没。



你点点头:俺明白了,不同的场景用不同的级别!


2、正确记录日志信息


鱼皮:没错,下面教你第二招。你注意到我刚才写的日志里有一对大括号 {} 吗?


log.info("用户 {} 开始导入", username);

你回忆了一下:对哦,那是啥啊?


鱼皮:这叫参数化日志。{} 是一个占位符,日志框架会在运行时自动把后面的参数值替换进去。


你挠了挠头:我直接用字符串拼接不行吗?


log.info("用户 " + username + " 开始导入");

鱼皮摇摇头:不推荐。因为字符串拼接是在调用 log 方法之前就执行的,即使这条日志最终不被输出,字符串拼接操作还是会执行,白白浪费性能。



你点点头:确实,而且参数化日志比字符串拼接看起来舒服~



鱼皮:没错。而且当你要输出异常信息时,也可以使用参数化日志:


try {
   // 业务逻辑
catch (Exception e) {
   log.error("用户 {} 导入失败", username, e);  // 注意这个 e
}

这样日志框架会同时记录上下文信息和完整的异常堆栈信息,便于排查问题。



你抱拳:学会了,我这就去打日志!


3、把控时机和内容


很快,你给批量导入程序的代码加上了日志:


@Slf4j
public class UserService {
   public BatchImportResult batchImport(List<UserDTO> userList) {
       log.info("开始批量导入用户,总数:{}", userList.size());
       int successCount = 0;
       int failCount = 0;
       for (UserDTO userDTO : userList) {
           try {
               log.info("正在导入用户:{}", userDTO.getUsername());   
               // 校验用户名
               if (StringUtils.isBlank(userDTO.getUsername())) {
                   throw new BusinessException("用户名不能为空");
              }
               // 保存用户
               saveUser(userDTO);
               successCount++;
               log.info("用户 {} 导入成功", userDTO.getUsername());
          } catch (Exception e) {
               failCount++;
               log.error("用户 {} 导入失败,原因:{}", userDTO.getUsername(), e.getMessage(), e);
          }
      }
       log.info("批量导入完成,成功:{},失败:{}", successCount, failCount);
       return new BatchImportResult(successCount, failCount);
  }
}

光做这点还不够,你还翻出了之前的屎山代码,想给每个文件都打打日志。



但打着打着,你就不耐烦了:每段代码都要打日志,好累啊!但是不打日志又怕出问题,怎么办才好?


鱼皮笑道:好问题,这就是我要教你的第三招 —— 把握打日志的时机。


对于重要的业务功能,我建议采用防御性编程,先多多打日志。比如在方法代码的入口和出口记录参数和返回值、在每个关键步骤记录执行状态,而不是等出了问题无法排查的时候才追悔莫及。之后可以再慢慢移除掉不需要的日志。



你叹了口气:这我知道,但每个方法都打日志,工作量太大,都影响我摸鱼了!


鱼皮:别担心,你可以利用 AOP 切面编程,自动给每个业务方法的执行前后添加日志,这样就不会错过任何一次调用信息了。



你双眼放光:这个好,爽爽爽!



鱼皮:不过这样做也有一个缺点,注意不要在日志中记录了敏感信息,比如用户密码。万一你的日志不小心泄露出去,就相当于泄露了大量用户的信息。



你拍拍胸脯:必须的!


4、控制日志输出量


一个星期后,产品经理又来找你了:小阿巴,你的批量导入功能又报错啦!而且怎么感觉程序变慢了?


你完全不慌,淡定地打开服务器的日志文件。结果瞬间呆住了……


好家伙,满屏都是密密麻麻的日志,这可怎么看啊?!



鱼皮看了看你的代码,摇了摇头:你现在每导入一条数据都要打一些日志,如果用户导入 10 万条数据,那就是几十万条日志!不仅刷屏,还会影响性能。


你有点委屈:不是你让我多打日志的么?那我应该怎么办?


鱼皮:你需要控制日志的输出量。


1)可以添加条件来控制,比如每处理 100 条数据时才记录一次:


if ((i + 1% 100 == 0) {
   log.info("批量导入进度:{}/{}"i + 1, userList.size());
}

2)或者在循环中利用 StringBuilder 进行字符串拼接,循环结束后统一输出:


StringBuilder logBuilder = new StringBuilder("处理结果:");
for (UserDTO userDTO : userList) {
   processUser(userDTO);
   logBuilder.append(String.format("成功[ID=%s], ", userDTO.getId()));
}
log.info(logBuilder.toString());

3)还可以通过修改日志配置文件,过滤掉特定级别的日志,防止日志刷屏:


<appender name="FILE" class="ch.qos.logback.core.FileAppender">
   <file>logs/app.log</file>
   <!-- 只允许 INFO 级别及以上的日志通过 -->
   <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
       <level>INFO</level>
   </filter>
</appender>

5、统一日志格式


你开心了:好耶,这样就不会刷屏了!但是感觉有时候日志很杂很乱,尤其是我想看某一个请求相关的日志时,总是被其他的日志干扰,怎么办?


鱼皮:好问题,可以在日志配置文件中定义统一的日志格式,包含时间戳、线程名称、日志级别、类名、方法名、具体内容等关键信息。


<!-- 控制台日志输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
   <encoder>
       <!-- 日志格式 -->
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
   </encoder>
</appender>

这样输出的日志更整齐易读:



此外,你还可以通过 MDC(Mapped Diagnostic Context)给日志添加额外的上下文信息,比如请求 ID、用户 ID 等,方便追踪。



在 Java 代码中,可以为 MDC 设置属性值:


@PostMapping("/user/import")
public Result importUsers(@RequestBody UserImportRequest request) {
   // 1. 设置 MDC 上下文信息
   MDC.put("requestId"generateRequestId());
   MDC.put("userId", String.valueOf(request.getUserId()));
   try {
       log.info("用户请求处理完成");      
       // 执行具体业务逻辑
       userService.batchImport(request.getUserList());     
       return Result.success();
  } finally {
       // 2. 及时清理MDC(重要!)
       MDC.clear();
  }
}

然后在日志配置文件中就可以使用这些值了:


<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
   <encoder>
       <!-- 包含 MDC 信息 -->
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{requestId}] [%X{userId}] %msg%n</pattern>
   </encoder>
</appender>

这样,每个请求、每个用户的操作一目了然。



6、使用异步日志


你又开心了:这样打出来的日志,确实舒服,爽爽爽!但是我打日志越多,是不是程序就会更慢呢?有没有办法能优化一下?


鱼皮:当然有,可以使用 异步日志


正常情况下,你调用 log.info() 打日志时,程序会立刻把日志写入文件,这个过程是同步的,会阻塞当前线程。而异步日志会把写日志的操作放到另一个线程里去做,不会阻塞主线程,性能更好。


你眼睛一亮:这么厉害?怎么开启?


鱼皮:很简单,只需要修改一下配置文件:


<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
   <queueSize>512</queueSize>  <!-- 队列大小 -->
   <discardingThreshold>0</discardingThreshold>  <!-- 丢弃阈值,0 表示不丢弃 -->
   <neverBlock>false</neverBlock>  <!-- 队列满时是否阻塞,false 表示会阻塞 -->
   <appender-ref ref="FILE" />  <!-- 引用实际的日志输出目标 -->
</appender>
<root level="INFO">
   <appender-ref ref="ASYNC" />
</root>

不过异步日志也有缺点,如果程序突然崩溃,缓冲区中还没来得及写入文件的日志可能会丢失。



所以要权衡一下,看你的系统更注重性能还是日志的完整性。


你想了想:我们的程序对性能要求比较高,偶尔丢几条日志问题不大,那我就用异步日志吧。


7、日志管理


接下来的很长一段时间,你混的很舒服,有 Bug 都能很快发现。


你甚至觉得 Bug 太少、工作没什么激情,所以没事儿就跟新来的实习生阿坤吹吹牛皮:你知道日志么?我可会打它了!



直到有一天,运维小哥突然跑过来:阿巴阿巴,服务器挂了!你快去看看!


你连忙登录服务器,发现服务器的硬盘爆满了,没法写入新数据。


你查了一下,发现日志文件竟然占了 200GB 的空间!



你汗流浃背了,正在考虑怎么甩锅,结果阿坤突然鸡叫起来:阿巴 giegie,你的日志文件是不是从来没清理过?


你尴尬地倒了个立,这样眼泪就不会留下来。



鱼皮叹了口气:这就是我要教你的下一招 —— 日志管理。


你好奇道:怎么管理?我每天登服务器删掉一些历史文件?


鱼皮:人工操作也太麻烦了,我们可以通过修改日志配置文件,让框架帮忙管理日志。


首先设置日志的滚动策略,可以根据文件大小和日期,自动对日志文件进行切分。


<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
   <fileNamePattern>logs/app-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
   <maxFileSize>10MB</maxFileSize>
   <maxHistory>30</maxHistory>
</rollingPolicy>

这样配置后,每天会创建一个新的日志文件(比如 app-2025-10-23.0.log),如果日志文件大小超过 10MB 就再创建一个(比如 app-2025-10-23.1.log),并且只保留最近 30 天的日志。



还可以开启日志压缩功能,进一步节省磁盘空间:


<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
   <!-- .gz 后缀会自动压缩 -->
   <fileNamePattern>logs/app-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
</rollingPolicy>


你有些激动:吼吼,这样我们就可以按照天数更快地查看日志,服务器硬盘也有救啦!


8、集成日志收集系统


两年后,你负责的项目已经发展成了一个大型的分布式系统,有好几十个微服务。


如今,每次排查问题你都要登录到不同的服务器上查看日志,非常麻烦。而且有些请求的调用链路很长,你得登录好几台服务器、看好几个服务的日志,才能追踪到一个请求的完整调用过程。



你简直要疯了!


于是你找到鱼皮求助:现在查日志太麻烦了,当年你还有一招没有教我,现在是不是……


鱼皮点点头:嗯,对于分布式系统,就必须要用专业的日志收集系统了,比如很流行的 ELK。


你好奇:ELK 是啥?伊拉克?


阿坤抢答道:我知道,就是 Elasticsearch + Logstash + Kibana 这套组合。


简单来说,Logstash 负责收集各个服务的日志,然后发送给 Elasticsearch 存储和索引,最后通过 Kibana 提供一个可视化的界面。



这样一来,我们可以方便地集中搜索、查看、分析日志。



你惊讶了:原来日志还能这么玩,以后我所有的项目都要用 ELK!


鱼皮摆摆手:不过 ELK 的搭建和运维成本比较高,对于小团队来说可能有点重,还是要按需采用啊。


结局


至此,你已经掌握了打日志的核心秘法。



只是你很疑惑,为何那阿坤竟对日志系统如此熟悉?


阿坤苦笑道:我本来就是日志管理大师,可惜我上家公司的同事从来不打日志,所以我把他们暴打了一顿后跑路了。


阿巴 giegie 你要记住,日志不是写给机器看的,是写给未来的你和你的队友看的!


你要是以后不打日志,我就打你!


更多


💻 编程学习交流:编程导航 📃 简历快速制作:老鱼简历 ✏️ 面试刷题神器:面试鸭


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

分库分表正在被淘汰

前言 “分库分表这种架构模式会逐步的被淘汰!” 不知道在哪儿看到的观点 如果我们现在在搭建新的业务架构,如果说你们未来的业务数据量会达到千万 或者上亿的级别 还在一股脑的使用分库分表的架构,那么你们的技术负责人真的就应该提前退休了🙈 如果对未来的业务非常有...
继续阅读 »

前言



“分库分表这种架构模式会逐步的被淘汰!” 不知道在哪儿看到的观点



如果我们现在在搭建新的业务架构,如果说你们未来的业务数据量会达到千万 或者上亿的级别 还在一股脑的使用分库分表的架构,那么你们的技术负责人真的就应该提前退休了🙈


如果对未来的业务非常有信心,单表的数据量能达到千万上亿的级别,请使用NewSQL 数据库,那么NewSQL 这么牛,分布库分表还有意义吗?



今天虽然写的是一篇博客,但是更多的是抱着和大家讨论的心态来的,所以大家目前有深度参与分库分表,或者NewSQL 的都可以在评论区讨论!



什么是NewSQL


NewSQL 是21世纪10年代初出现的一个术语,用来描述一类新型的关系型数据库管理系统(RDBMS)。它们的共同目标是:在保持传统关系型数据库(如Oracle、MySQL)的ACID事务和SQL模型优势的同时,获得与NoSQL系统类似的、弹性的水平扩展能力


NewSQL 的核心理念就是 将“分库分表”的复杂性从应用层下沉到数据库内核层,对上层应用呈现为一个单一的数据库入口,解决现在 分库分表的问题;


分库分表的问题


分库分表之后,会带来非常多的问题;比如需要跨库联查、跨库更新数据如何保证事务一致性等问题,下面就来详细看看分库分表都有那些问题



  1. 数据库的操作变得复杂



    • 跨库 JOIN 几乎不可行:原本简单的多表关联查询,因为表被分散到不同库甚至不同机器上,变得异常困难。通常需要拆成多次查询,在应用层进行数据组装,代码复杂且性能低下。

    • 聚合查询效率低下COUNT()SUM()GR0UP BYORDER BY 等操作无法在数据库层面直接完成。需要在每个分片上执行,然后再进行合并。

    • 分页问题LIMIT 20, 10 这样的分页查询会变得非常诡异。你需要从所有分片中获取前30条数据,然后在应用层排序后取第20-30条。页码越大,性能越差。



  2. 设计上需要注意的问题



    • 分片键(Sharding Key)的选择:如果前期没有设计好,后期数据倾斜比较严重

    • 全局唯一ID需要提前统一设计,规范下来

    • 分布式事务问题,需要考虑使用哪种方式去实现(XA协议,柔性事务)




选择TiDB还是采用mysql 分库分表的设计


数据量非常大,需要满足OLTP (Online Transactional Processing)OLAP (Online Analytical Processing)HTAP预算充足(分布式数据库的成本也是非常高的这一点非常的重要),并且是新业务新架构落地 优先推荐使用TiDB
当然实际上选择肯定是需要多方面考虑的,大家有什么观点都可以在评论区讨论。


可以看看一个资深开发,深度参与TiDB项目,他对TiDB的一些看法:


3150c08d-9372-41aa-9cf4-7aafbea0c149.png


efe93ca3-12ef-47fe-aab4-16e191894a01.png


f3e3a4a7-c0f1-47bf-b524-e9863458cff0.png


1 什么是TiDB?


TiDB是PingCAP公司研发的开源分布式关系型数据库,采用存储计算分离架构,支持混合事务分析处理(HTAP) 。它与MySQL 5.7协议兼容,并支持MySQL生态,这意味着使用MySQL的应用程序可以几乎无需修改代码就能迁移到TiDB。


🚀目标是为用户提供一站式 OLTP (Online Transactional Processing)、OLAP (Online Analytical Processing)、HTAP 解决方案。TiDB 适合高可用、强一致要求较高、数据规模较大等各种应用场景。



官方文档:docs.pingcap.com/zh/tidb/dev…



TiDB五大核心特性


TiDB之所以在分布式数据库领域脱颖而出,得益于其五大核心特性



  • 一键水平扩容或缩容:得益于存储计算分离的架构设计,可按需对计算、存储分别进行在线扩容或缩容,整个过程对应用透明。

  • 金融级高可用:数据采用多副本存储,通过Multi-Raft协议同步事务日志,只有多数派写入成功事务才能提交,确保数据强一致性。

  • 实时HTAP:提供行存储引擎TiKV和列存储引擎TiFlash,两者之间的数据保持强一致,解决了HTAP资源隔离问题。

  • 云原生分布式数据库:通过TiDB Operator可在公有云、私有云、混合云中实现部署工具化、自动化。

  • 兼容MySQL 5.7协议和生态:从MySQL迁移到TiDB无需或只需少量代码修改,极大降低了迁移成本。


2 TiDB与MySQL的核心差异


虽然TiDB兼容MySQL协议,但它们在架构设计和适用场景上存在根本差异。以下是它们的详细对比:


2.1 架构差异


表1:TiDB与MySQL架构对比


特性MySQLTiDB
架构模式集中式架构分布式架构
扩展性垂直扩展,主从复制水平扩展,存储计算分离
数据分片需要分库分表自动分片,无需sharding key
高可用机制主从复制、MGRMulti-Raft协议,多副本
存储引擎InnoDB、MyISAM等TiKV(行存)、TiFlash(列存)

2.2 性能表现对比


性能方面,TiDB与MySQL各有优势,主要取决于数据量和查询类型:



  • 小数据量简单查询:在数据量百万级以下的情况下,MySQL的写入性能和点查点写通常优于TiDB。因为TiDB的分布式架构在少量数据时无法充分发挥优势,却要承担分布式事务的开销。

  • 大数据量复杂查询:当数据量达到千万级以上,TiDB的性能优势开始显现。一张千万级别表关联查询,MySQL可能需要20秒,而TiDB+TiKV只需约5.57秒,使用TiFlash甚至可缩短到0.5秒。

  • 高并发场景:MySQL性能随着并发增加会达到瓶颈然后下降,而TiDB性能基本随并发增加呈线性提升,节点资源不足时还可通过动态扩容提升性能。


2.3 扩展性与高可用对比


MySQL的主要扩展方式是一主多从架构,主节点无法横向扩展(除非接受分库分表),从节点扩容需要应用支持读写分离。而TiDB的存储和计算节点都可以独立扩容,支持最大512节点,集群容量可达PB级别。


高可用方面,MySQL使用增强半同步和MGR方案,但复制效率较低,主节点故障会影响业务处理[]。TiDB则通过Raft协议将数据打散分布,单机故障对集群影响小,能保证RTO(恢复时间目标)不超过30秒且RPO(恢复点目标)为0,真正实现金融级高可用。


2.4 SQL功能及兼容性


虽然TiDB高度兼容MySQL 5.7协议和生态,但仍有一些重要差异需要注意:


不支持的功能包括:



  • 存储过程与函数

  • 触发器

  • 事件

  • 自定义函数

  • 全文索引(计划中)

  • 空间类型函数和索引


有差异的功能包括:



  • 自增ID的行为(TiDB推荐使用AUTO_RANDOM避免热点问题)

  • 查询计划的解释结果

  • 在线DDL能力(TiDB更强,不锁表支持DML并行操作)


3 如何选择:TiDB还是MySQL?


选择数据库时,应基于实际业务需求和技术要求做出决策。以下是具体的选型建议:


3.1 选择TiDB的场景


TiDB在以下场景中表现卓越:



  1. 数据量大且增长迅速的OLTP场景:当单机MySQL容量或性能遇到瓶颈,且数据量达到TB级别时,TiDB的水平扩展能力能有效解决问题。

    例如,当业务数据量预计将超过TB级别,或并发连接数超过MySQL合理处理范围时。

  2. 实时HTAP需求:需要同时进行在线事务处理和实时数据分析的场景。

    传统方案需要OLTP数据库+OLAP数据库+ETL工具,TiDB的HTAP能力可简化架构,降低成本和维护复杂度。

  3. 金融级高可用要求:对系统可用性和数据一致性要求极高的金融行业场景。

    TiDB的多副本和自动故障转移机制能确保业务连续性和数据安全。

  4. 多业务融合平台:需要将多个业务数据库整合的统一平台场景。

    TiDB的资源管控能力可以按照RU(Request Unit)大小控制资源总量,实现多业务资源隔离和错峰利用。

  5. 频繁的DDL操作需求:需要频繁进行表结构变更的业务。

    TiDB的在线DDL能力在业务高峰期也能平稳执行,对大表结构变更尤其有效。


3.2 选择MySQL的场景


MySQL在以下情况下仍是更合适的选择:



  1. 中小规模数据量:数据量在百万级以下,且未来增长可预测。

    在这种情况下,MySQL的性能可能更优,且总拥有成本更低。

  2. 简单读写操作为主:业务以点查点写为主,没有复杂的联表查询或分析需求。

  3. 需要特定MySQL功能:业务依赖存储过程、触发器、全文索引等TiDB不支持的功能。

  4. 资源受限环境:硬件资源有限且没有分布式数据库管理经验的团队。

    MySQL的运维管理相对简单,学习曲线较平缓。


3.3 决策参考框架


为了更直观地帮助决策,可以参考以下决策表:


考虑因素倾向TiDB倾向MySQL
数据规模TB级别或预计快速增长GB级别,增长稳定
并发需求高并发(数千连接以上)低至中等并发
查询类型复杂SQL,多表关联简单点查点写
可用性要求金融级(RTO<30s,RPO=0)常规可用性要求
架构演进微服务、云原生、HTAP传统单体应用
运维能力有分布式系统管理经验传统DBA团队

4 迁移注意事项


如果决定从MySQL迁移到TiDB,需要注意以下关键点:



  1. 功能兼容性验证:检查应用中是否使用了TiDB不支持的MySQL功能,如存储过程、触发器等。

  2. 自增ID处理:将AUTO_INCREMENT改为AUTO_RANDOM以避免写热点问题。

  3. 事务大小控制:注意TiDB对单个事务的大小限制(早期版本限制较严,4.0版本已提升到10GB)。

  4. 迁移工具选择:使用TiDB官方工具如DM(Data Migration)进行数据迁移和同步。

  5. 性能测试:迁移前务必进行充分的性能测试,特别是针对业务关键查询的测试。


5 总结


TiDB和MySQL是适用于不同场景的数据库解决方案,没有绝对的优劣之分。MySQL是优秀的单机数据库,适用于数据量小、架构简单的场景;数据量大了之后需要做分库分表。而TiDB作为分布式数据库,专注于解决大数据量、高并发、高可用性需求下的数据库瓶颈问题,但是成本也是非常的高



本人没有使用过NewSQL ,还望各位大佬批评指正



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

告别终端低效,10个让同事直呼卧槽的小技巧

在 IDE 横行的今天,我们这些程序员依然需要跟终端打交道,三五年下来,谁还没踩过一些坑,又或者自己琢磨出一些能让效率起飞的小窍门呢? 今天不聊那些 ls -la 比 ls 好用之类的基础知识,只分享那些真正改变我工作流、甚至让旁边同事忍不住探过头来问“哥们,...
继续阅读 »

在 IDE 横行的今天,我们这些程序员依然需要跟终端打交道,三五年下来,谁还没踩过一些坑,又或者自己琢磨出一些能让效率起飞的小窍门呢?


今天不聊那些 ls -lals 好用之类的基础知识,只分享那些真正改变我工作流、甚至让旁边同事忍不住探过头来问“哥们,你这手速没单身30年练不下来吧”的实战技巧。



快速定位系统性能瓶颈


服务器或者自己电脑突然变卡,得快速知道是谁在捣鬼。


# 查看哪个目录最占硬盘空间(只看当前目录下一级)
du -ah --max-depth=1 | sort -rh | head -n 10

# 按 CPU 使用率列出排名前 10 的进程
ps aux --sort=-%cpu | head -n 11

# 按内存使用率列出排名前 10 的进程
ps aux --sort=-%mem | head -n 11

环境和配置管理?交给专业的来


以前,管理本地开发环境简直是一场灾难。一会儿要配 PHP,一会儿又要弄 Node.js,还得装个 Python。各种环境变量、数据库配置写在 .bashrc.zshrc 里,像这样:


# 老办法:用函数切换环境
switch_env() {
if [ "$1" = "proj_a" ]; then
export DB_HOST="localhost"
export DB_PORT="3306"
echo "切换到项目 A 环境"
elif [ "$1" = "proj_b" ]; then
export DB_HOST="127.0.0.1"
export DB_PORT="5432"
echo "切换到项目 B 环境"
fi
}

这种方式手动维护起来很麻烦,项目一多,配置文件就变得特别臃肿,切换起来也容易出错。


但是,时代变了,朋友们。现在我处理本地开发环境,都用 ServBay。


请注意,ServBay不是命令行工具,而是一个集成的本地开发环境平台。它把程序员常用的语言,比如 PHP、Node.js、Python、Go、Rust 都打包好了,需要哪个版本点一下就行,完全不用自己去折腾编译和环境变量。


CleanShot 2025-11-18 at <a href=18.00.15@2x.png" loading="lazy" src="https://www.imgeek.net/uploads/article/20251214/459b28b5fdb4e57a34956d28d1655e18.jpg"/>


数据库也一样,无论是 SQL(MySQL, PostgreSQL)还是 NoSQL(Redis, MongoDB),都给你准备得妥妥的。而且它支持一键部署本地 AI,适合vibe coder。


用了 ServBay 之后,上面那些复杂的环境切换脚本我早就删了。所有环境和配置管理都通过一个清爽的图形界面搞定,我变强了,也变快了。


一行搞定网络调试


简单测试一下端口通不通,或者 API 能不能访问,完全没必要打开 Postman 那么重的工具。


# 检查本地 3306 端口是否开放
nc -zv 127.0.0.1 3306

# 快速给 API 发送一个 POST 请求
curl -X POST http://localhost:8080/api/v1/users \
-H "Content-Type: application/json" \
-d '{"username":"test","role":"admin"}'

目录间的闪转腾挪:pushdpopd


还在用一连串的 cd ../../.. 来返回之前的目录吗?那也太“复古”了。试试目录栈吧。


# 你当前在 /Users/me/workspace/project-a/src
pushd /etc/nginx/conf.d
# 这时你瞬间移动到了 Nginx 配置目录,并且终端会记住你来的地方

# 在这里查看和修改配置...
vim default.conf

# 搞定之后,想回去了?
popd
# “嗖”的一下,你又回到了 /Users/me/workspace/project-a/src

pushd 可以多次使用,它会把目录一个个地压入一个“栈”里。可以用 dirs -v 查看这个栈,然后用 pushd +N 跳到指定的目录。对于需要在多个不相关的目录之间反复横跳的场景,这就是大杀器。


文件操作的骚操作


cp 复制大文件时,看着光标一动不动,你是不是也曾怀疑过电脑是不是死机了?


# 安装 rsync (macOS 自带,Linux 大部分也自带)
# 复制文件并显示进度条
rsync -avh --progress source-large-file.zip /path/to/destination/

查找文件,find 命令固然强大,但参数复杂得像咒语。我更推荐用 fd,一个更快、更友好的替代品。


# 安装 fd (brew install fd / apt install fd-find)
# 查找所有 tsx 文件
fd ".tsx$"

# 查找并删除所有 .log 文件
fd ".log$" --exec rm {}

批量重命名文件,也不用再写复杂的脚本了。


# 比如把所有的 .jpeg 后缀改成 .jpg
for img in *.jpeg; do
mv "$img" "${img%.jpeg}.jpg"
done

历史命令的魔法:!!!$


这个绝对是手残党和健忘症患者的良药。最常见的场景就是,刚敲了一个需要管理员权限的命令,然后……


# 信心满满地创建目录
mkdir /usr/local/my-app
# 得到一个冷冰冰的 "Permission denied"

# 这时候别傻乎乎地重敲一遍,优雅地输入:
sudo !!
# 这行命令会自动展开成:sudo mkdir /usr/local/my-app

!! 代表上一条完整的命令。而 !$ 则更精妙,它代表上一条命令的最后一个参数。


# 创建一个藏得很深的项目目录
mkdir -p projects/a-very-long/and-nested/project-name

# 紧接着,想进入这个目录
cd !$
# 是的,它会自动展开成:cd projects/a-very-long/and-nested/project-name

# 或者,想在那个目录下创建一个文件
touch !$/index.js

自从熟练掌握了这两个符号,我的键盘方向上键和 Ctrl+C 的使用频率都降低了不少。


进程管理不用抓狂


以前杀个进程,得先 ps aux | grep xxx,然后复制 PID,再 kill -9 PID,一套操作下来黄花菜都凉了。现在,我们可以更直接一点。


# 按名字干掉某个进程
pkill -f "gunicorn"

# 优雅地请所有 Python 脚本进程“离开”
pkill -f "python.*.py"

对于我们这些经常和端口打交道的开发者来说,端口被占用的问题更是家常便饭。下面这个函数,我把它写进了我的 .zshrc 里,谁用谁知道。


# 定义一个函数,专门用来释放被占用的端口
free_port() {
lsof -i tcp:$1 | grep LISTEN | awk '{print $2}' | xargs kill -9
echo "端口 $1 已释放"
}

# 比如,干掉占用 8000 端口的那个“钉子户”
free_port 8000

给 Git 整个外挂


把这些别名(alias)加到 ~/.gitconfig 文件里,每天能省下无数次敲击键盘的力气。


[alias]
co = checkout
br = branch
ci = commit -m
st = status -sb
lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset'

# 把暂存区的修改撤销回来
unstage = reset HEAD --

# 彻底丢弃上一次提交,但保留代码改动
undo = reset --soft HEAD~1

# 一键推送当前分支到远程
pushup = "!git push --set-upstream origin $(git rev-parse --abbrev-ref HEAD)"

还有一个我特别喜欢的,一键清理已经合并到主干的本地分支,分支列表干净清爽。


git branch --merged main | grep -v "*|main|develop" | xargs -n 1 git branch -d

文本处理,快准狠


从日志里捞个邮箱,或者快速格式化一坨 JSON,都是日常操作。


# 从文件里提取所有 URL
grep -oE 'https?://[a-zA-Z0-9./-]+' access.log

# 格式化粘贴板里的 JSON (macOS)
pbpaste | jq .

# 从 API 响应中只提取需要的字段
curl -s 'https://api.github.com/users/torvalds' | jq '.name, .followers'

把重复劳动变成自动化脚本


真正拉开效率差距的,是把那些每天都在重复的操作,变成一个可以随时呼叫的函数或脚本。


# 比如,创建一个新前端项目的完整流程
new_react_project() {
npx create-react-app "$1" && cd "$1"
git init && git add .
git commit -m "🎉 Initial commit"
# 自动在 VS Code 中打开
code .
}

# 比如,在执行危险操作前,快速打包备份当前目录
backup() {
local fname="backup-$(date +%Y%m%d-%H%M).tar.gz"
tar -czvf "$fname" . --exclude-from=.gitignore
echo "备份完成: $fname"
}

把这些函数写进 .bashrc.zshrc,下次再做同样的事情时,只需要敲一个命令就搞定了。


写在最后


这些技巧本身并不复杂,但它们就像肌肉记忆,一旦养成习惯,就能在日常工作中节省大量时间。对程序员来说,时间就是头发。


作者:该用户已不存在
来源:juejin.cn/post/7573988811916017714
收起阅读 »

做中国人自己的视频编辑UI框架,WebCut正式开源

web
项目地址:github.com/tangshuang/… 朋友们晚上好啊,这段时间我在忙着完成最新的开源项目WebCut。这个项目是我这小半年来唯一的开源新项目,这对比我过去几年的一些开源事迹来说,真的是一段低产荒。不过这是正常的,没有任何人可以长时间的一直...
继续阅读 »



项目地址:github.com/tangshuang/…



朋友们晚上好啊,这段时间我在忙着完成最新的开源项目WebCut。这个项目是我这小半年来唯一的开源新项目,这对比我过去几年的一些开源事迹来说,真的是一段低产荒。不过这是正常的,没有任何人可以长时间的一直发布新项目,这种沉寂,正是因为我把时间和精力都投入在其他事情上,所以其实是好事。之所以要发起和开源这个项目,说起来还是有些背景,下面我会来聊一聊关于这个项目的一些背景,以及过程中在技术上的一些探索。


没有合适的视频编辑UI框架😭


过去半年,我连续发布了多款与视频相关的产品,这些产品或多或少都需要用户参与视频编辑操作,比如给视频添加字幕、对视频进行裁剪、给视频配音等等,这些在视频编辑器中常见的视频处理工具,在Web端其实需求非常巨大,特别是现在AI领域,制作各种个样的视频的需求非常多。而这些需求,可能并不需要在产品中载入一整个视频编辑器,而是只需要几个简单的部件来实现编辑功能。然而回望开源市场,能够支持这种编辑能力的项目少之又少,虽然有一些项目呈现了视频编辑的能力,然而,要么项目太大,一个完整的视频编辑器甩在开发者脸上,要么过于底层,没有UI界面。如果我只是想有一个视频预览,再有轨道和几个配置界面,就没法直接用这些项目。包括我自己在内,每次有与视频相关的功能,都要把之前在另外一个产品中实现的编辑能力移植到新产品中,而且要调的细节也很多。正是这种求而不得的现状,促使我打算自己写一个视频编辑器项目。


初始想法💡:拼积木


我可不是从0开始的,因为我已经开发过很多次视频编辑相关的功能了。我还在Videa项目中完整实现了一个视频编辑器。因此,我的想法是把我之前做过的功能,整理一遍,就可以得到想要的组件或逻辑。有了这个工具包之后,我只需要在将来的新产品中复用这些代码即可。于是我建立了一个独立的npm包,来把所有功能集中放在一起。随着持续的迭代,我发现其实这里面是有规律的。


视频编辑器我们都用过,像剪映一样,有各种功能,细节也很多。但是,当我们把视频编辑的功能放到某个具体的产品中时,我们是不可能直接把整个编辑器给用户的。实际上,我们最终呈现的产品形态,基本上都是剪映的子集,而且是很小很小的子集,可能只是整个剪映1%的功能,最终给到用户操作可能只是非常简单的一次性操作,而页面也很轻量,属于用户即用即走,用完就关再也不会来第二次的那种。正是这种看似功能点很小,但实际上需要为它单独定制,技术上的成本可以用巨大来描述的场景,让我觉得这是一个需要认真对待的点。


我的计划是采用组件化的思想,把一个视频编辑器拆成一个一个的组件,把一个完整的剪映编辑器,拆成一个按钮一个按钮的积木。当我们面对产品需求时,就从这些积木中挑选,然后组合成产品经理所描述的功能,同时,具体这些积木怎么布局,则根据设计稿调整位置,还可以用CSS来覆盖组件内部的样式,达到与设计稿媲美的效果。



上面是我用AI做的一张示意图,大概就是这个意思,把一个编辑器拆的细碎,然后要什么功能就把对应的组件拿来拼凑一下。比如要对视频进行静音裁剪,就只要把预览区和轨道区拿出来,然后自己再增加一些能力上去。这样,开发者在面对各种各样的需求时,就能快速搭建起界面效果,而不需要从头实现界面和视频处理能力。



通过不同的组合方式,配合开发者自己的布局,就可以创建符合需求的视频编辑界面。


没那么容易😥:外简内繁的接口艺术


虽然想法很容易有,但是要做成成品,并且发布为可用的库可没那么轻松。要知道视频编辑器的功能非常多,多到我们无法用简单的文字描述它们。那么,如何才能站在开发者的角度,让这件事变得简单呢?


开发库永远面临着一个矛盾:灵活性和规范性之间的矛盾。灵活性就是暴露又多又细的接口,让开发者可以用不同参数,玩出花活,例如知名项目echarts早期就是这种。这种对于刚上手的开发者而言,无言是一场灾难,他们甚至不知道从哪里开始,但是一旦完全掌握,就相当于拥有了一个武器库,可以为所欲为。而规范性则是强制性规则比较多,只能暴露少量接口,避免开发者破坏这种规范,例如很多前端UI组件库就是这样。这两者之间的矛盾,是每一个库或框架开发者最难平衡的。


在视频编辑器上,我认为有些东西是固定的,几乎每一个需求都会用到,例如视频预览播放器、控制按钮等,但是有些功能是低频功能,例如媒体库、字幕编辑等。因此,对于高频和低频的功能,我用两种态度去处理。


高频的功能,拆的很细,拆到一个按钮为止,例如视频播放器,看上去已经很单一了,但是,我还要把预览屏幕和播放按钮拆开,屏幕和按钮如何布局就可以随意去处理。类似的还有导出按钮、分割片段按钮等等。把这些工具拆分为一个一个最小的单元,由开发者自由布局,这样就可以很好的去适配产品需求。


对于低频功能,则直接导出为大组件,这样就可以在一个组件内完成复杂的逻辑,减少开发这些组件时需要跨组件控制状态的心智成本。比如媒体库,里面包含了媒体的管理、上传、本地化等等,这些逻辑还是很复杂的,如果还是按照细拆思路,那么实现起来就烦得要死。因此,这类工具我都是只暴露为一个大的组件。


同时,为了方便开发者在某些情况下快捷接入,我会设计一些自己看到的不错的编辑器UI,然后用上诉的工具把它们搭出来,这样,开发者如果在产品需求中发现仓库里已经有功能一致的组件,就不需要自己去组合,直接使用对应组件即可。


以模仿剪映为例,开发者只需要接入WebCutEditor这个组件即可:


<script setup>
import { WebCutEditor } from 'webcut';
import 'webcut/esm/style.css';
</script>

<template>
<WebCutEditor />
</template>

这样就可以得到一个界面接近于剪映的视频编辑器。


数据驱动,视频编辑的DSL


经过我的研究,发现对于单纯的视频编辑而言,编辑器其实只需要两份数据,就可以解决大部分场景下的需求。一份是编辑的素材数据,一份是视频的配置数据。


素材数据


它包含素材文件本身的信息、素材的组织信息、素材的属性信息。


文件信息


我通过opfs将文件存在本地,并且在indexedDB中存储每一个文件的关联信息。包含文件的类型、名称、大小等。在一个域名下,每一个文件的实体(也就是File对象)只需要一份,通过file-md5值作为索引存在opfs中。而一个文件可能会在多处被使用,indexedDB中则是记录这些信息,多个关联信息可同时链接到同一个File。另外,基于indexedDB的特性,还可以实现筛选等能力。


素材组织信息


主要是指当把素材放在视频时间轨道中时,所需要的数据结构。包含轨道列表、素材所对应的文件、素材对应时间点、播放时的一些属性等等信息。这些信息综合起来,我们就知道,在视频的某一个时刻,应该播放什么内容。


素材属性信息


在播放中,素材以什么方式呈现,如文本的样式、视频音频的播放速度、动画、转场等。


配置数据


主要指视频本身的信息,在导出时这些配置可以直接体现出来,例如视频的分辨率、比例、速率等,视频是否要添加某些特殊的内容,例如水印等。


基于素材数据和配置数据,我们基本上可以完整的知道当前这个视频的编辑状态。通过数据来恢复当前的编辑状态,变得可行,这可以抵消用户在浏览器中经常执行“刷新”操作带来的状态丢失。同时,这份数据也可以备份到云端,实现多端的同步(不过需要同时同步File,速度肯定会受影响)。而且由于数据本身是纯序列化的,因此,可以交给AI来进行处理,例如让AI调整一些时间、样式等可基于纯序列化数据完成的功能。这就让我们的编辑器变得有更多的玩法。


发布上线🌏


经过几天的工作,我终于把代码整理完整,经过调试之后,基本可用了,便迫不及待的准备与大家分享。现在,你可以使用这个项目了。



由于底层是由Vue3作为驱动的,因此,在Vue中使用有非常大的优势,具体如下:


npm i webcut

先安装,安装之后,你就可以在Vue中如下使用。


<script setup>
import { WebCutEditor } from 'webcut';
import 'webcut/esm/style.css';
</script>

<template>
<WebCutEditor />
</template>

或者如果你的项目支持typescript,你可以直接从源码进行引入,这样就不必主动引入css:


<script setup lang="ts">
import { WebCutEditor } from 'webcut/src';
</script>

如果是非Vue的项目,则需要引用webcomponents的构建产物:


import 'webcut/webcomponents';
import 'webcut/webcomponents/style.css';

export default function Some() {
return <webcut-editor></webcut-editor>;
}

如果是直接在HTML中使用,可以直接引入webcomponents/bundle,这样包含了Vue等依赖,就不需要另外构建。


<script src="https://unpkg.com/webcut/webcomponents/bundle/index.js"></script>
<link rel="stylesheet" href="https://unpkg.com/webcut/webcomponents/bundle/style.css" />

<webcut-editor></webcut-editor>

如果是想自己布局,则需要引入各个很小的组件来自己布局。在这种情况下,你必须引入WebCutProvider组件,将所有的子组件包含在内。


<webcut-provider>
<webcut-player></webcut-player>
<webcut-export-button></webcut-export-button>
</webcut-provider>

未来展望


当前,WebCut还是处于很初级的阶段,实现了最核心的能力,我的目标是能够为开发者们提供一切需要的组件,并且不需要复杂的脚本处理就可以获得视频编辑的全部功能。还有很多功能没有实现,在计划中:



  • 历史记录功能,包含撤销和重做功能

  • 内置样式的字体

  • 花字,比内置样式更高级的文本

  • 轨道里的素材吸附能力

  • 视频的轨道分离(音频分离)

  • 音视频的音量调节

  • 单段素材的下载导出

  • 整个视频导出时可以进行分辨率、码率、速率、编码、格式的选择,支持只导出音频


以上这些都是编辑的基本功能。还有一些是从视频编辑定制化的角度思考的能力:



  • 动画(帧)支持

  • 转场过渡效果支持

  • 扩展功能模块,这部分可能会做成收费的,下载模块后,通过一个接口安装一下,就可以支持某些扩展功能

  • AI Agent能力,通过对话来执行视频编辑,降低视频编辑的门槛

  • 视频模板,把一些流行的视频效果片段做成模板,在视频中直接插入后,与现有视频融合为模板中的效果

  • 基于AI来进行短剧创作的能力


要实现这些能力,需要大量的投入,虽然现在AI编程非常火热,但是真正能够完美实现的,其实还比较少,因此,这些工作都需要小伙伴们的支持,如果你对这个项目感兴趣,可以通过DeepWiki来了解项目代码的底层结构,并fork项目后,向我们提PR,让我们一起共建一个属于我们自己的视频编辑UI框架。


最后,你的支持是我前进的动力,动动你的小手,到我们的github上给个start,让我们知道你对此感兴趣。


作者:否子戈
来源:juejin.cn/post/7579819594270900262
收起阅读 »

程序员越想创业,越不要急着动手

昨天晚上,我和老婆聊了一个创业点子。 一个能源方面的前辈找到我,希望通过我把一些人工的工作 AI 自动化。 能源方面我不懂,找 Gemini 聊完发现这个是可以复制的,非常兴奋。我跟老婆说,这个项目做好以后可以做成平台,推广到其他公司,你就等着做总裁夫人吧! ...
继续阅读 »

昨天晚上,我和老婆聊了一个创业点子。


一个能源方面的前辈找到我,希望通过我把一些人工的工作 AI 自动化。


能源方面我不懂,找 Gemini 聊完发现这个是可以复制的,非常兴奋。我跟老婆说,这个项目做好以后可以做成平台,推广到其他公司,你就等着做总裁夫人吧!


她听完以后跟我说,这个项目还是太定制化,和我之前做的一个项目很像。


那个项目一开始,我和朋友设想的是可以做完一个以后再推到不同的学校,但最后没有达到期望。不同的甲方想要的和我们做的差异很大,没办法推广,都得定制。


但我觉得这次不一样,她说了好几遍,发现我一直“冥顽不灵”,就不再说了。


今天休息,在写完专栏《转型 AI 工程师》第二篇以后,我想起之前老婆分享给我的一些抖音视频还没看,打开看了以后,发现她分享的都是一些赚钱妙招,有些真的很打破我的认知,让我大受震撼。


好些项目是我头一次见到,在这之前脑子里完全没有概念。这时我才明白她昨天晚上跟我说的话。对比别人讲的这些,我想做的的确是太小众、太定制了。


为什么我听不进去?


冯新(原真格基金投资合伙人)说过:创业企业的成长本质是创始人认知边界的突破,而『不知道自己不知道』的认知茧房,正是创始人成长的最大障碍


过去八九年,我两耳不闻窗外事、一心只想搞技术,对商业机会的认知非常少。如今想要参与进 AI 这波浪潮,却不知道从何做起。


在前辈跟我说了诉求后,我像落水的人抓住绳子一样,脑子里都是那个新项目的画面:问题都有哪些、怎么解决、做完怎么推广到更多公司等等。这些画面在我脑子里不断循环,占据了全部的注意力。


而老婆说的"大规模复制的模式",在我脑子里没有画面,所以就完全听不进去。就像戴着VR眼镜,你只能看到眼镜里的世界,别人跟你说外面的世界是什么样,你根本想象不出来。


我想这也是为什么很多孩子你跟他讲话他不听,很多年轻人长辈跟他讲话他也不听。不是他们不想听,而是他们脑子里只能看到当前自己见到的、听到的、想到的一些事。


我不是第一个犯这种错的人


后来我查了一下,发现我这种情况不是个例。


哈佛商学院有个研究发现,90% 的创业者倒在头 18 个月。“最大的敌人不是市场或竞争对手,而是创业者自己"。


具体来说,就是对「快速试错」和「尽早动手」等观点的误解,导致在错误道路上浪费了太多资源。这种情况被叫做「错误的起步」——省略初期的全面审慎思考,直接进入执行阶段。


现在这个能源项目,如果我按照昨晚的想法继续推进,很可能又会掉进一样的坑。


在写下这篇文章的时候,我想明白了动手之前要调研的情况:



  1. 这个需求是不是真的普遍存在?

  2. 不同公司的差异有多大?

  3. 推广的成本和难度是什么?

  4. 有没有更标准化的切入点?


如何拓宽认知边界?


老婆分享的那几个抖音视频,讲的赚钱方式,有些我从来没想到过,也没接触过这些行业,脑子里完全没有画面。但看完以后,我突然明白了一件事:原来赚钱的方式有这么多种,而我一直在自己的一亩三分地里打转。


对于像我这样想要创业的人来说,今天最大的感悟是:一定要多看,一定要先知道「猪是怎么跑的」,哪怕吃不到猪肉,至少知道猪是怎么跑的,心里有了一个概念


具体怎么做?我目前想到这些,欢迎你评论区留言:


1、主动搜索不同行业的赚钱案例


不是为了照搬,而是为了拓宽认知边界。


我在日历里加上了日程---每周花30分钟,专门看别人有什么小众赚钱模式,他们是怎么发现机会的,怎么切入市场的,怎么实现标准化的。抖音、小红书、知乎、即刻,都是很好的信息源。


关键是要看那些你从来没想到过的行业。比如我今天看到的几个案例:直播切片、广告媒介采买。


2、用 AI 筛选出适合自己的机会


看得多了,就会发现很多机会。接下来需要进行筛选。


我的优势是 AI 技术,所以我会重点关注那些「传统行业+AI」的机会。不是去做通用大模型,而是找那些可以用 AI 提升效率的垂直领域。


3、经常问自己三个问题


为了避免再次陷入「执行陷阱」,我给自己设了一个自我检查清单,每周问自己三个问题:



  1. 我现在做的事,是在拓宽认知边界,还是重复的经验复用?

  2. 我看到的机会,受众有多少,其中有多少人愿意付费?

  3. 这个项目如果一年内没有成果,我会坚持吗?坚持的原因是什么


4、搭建一个「商机捕获系统」


这几天我一直在想,能不能用 AI 搭建一个系统,自动帮我捕获各种赚钱商机?


我试了几个方案,发现真的可行。


这个系统的核心不是找到一个具体的项目,而是持续拓宽认知边界,让自己能看到更多的可能性。具体怎么搭建,我准备放到 转型 AI 工程师专栏 的最后大作业部分,这个点子比之前想的「深度研究助手」更有价值。


最后想说的


今天突然有这个感想,没想到越写越多,差不多了收个尾吧。


像我这样两耳不闻窗外事、一心只想搞技术的老程序员,想要创业不要急着动手。


不是说不要行动,而是说在行动之前,先拓宽自己的认知边界。


如果你脑子里只有一种赚钱方式,那你只能在这一种方式里打转。如果你脑子里有十种、一百种赚钱方式,你才能找到最适合自己的那一种。


我自己的经历就是教训。那个学校项目失败了,现在这个项目如果不调整思路,很可能又会失败。


但好在,我现在意识到了这个问题。


希望这篇文章对你也有启发。


以上。


作者:张拭心
来源:juejin.cn/post/7582854603061379114
收起阅读 »

我为什么放弃了XMind和亿图,投向了这款开源绘图工具的怀抱?

web
关注我的公众号:【编程朝花夕拾】,可获取首发内容。 01 引言 思维导图、流程图应该是每个程序员都会用到的绘图工具。Xmind和亿图曾是我的首选工具,但是免费版功能受限,高级功能需付费,用起来总是差点意思。虽然通过其他方式正常使用(大家都懂得),但是软件的更...
继续阅读 »

关注我的公众号:【编程朝花夕拾】,可获取首发内容。



01 引言


思维导图、流程图应该是每个程序员都会用到的绘图工具。Xmind和亿图曾是我的首选工具,但是免费版功能受限,高级功能需付费,用起来总是差点意思。虽然通过其他方式正常使用(大家都懂得),但是软件的更新根本不敢动,一旦更新就会前功尽弃......而且两款工具总会来回切换,虽已习惯,但稍显麻烦!


直到不久前逛GitHub发现了一款开源的且可以在线使用的工具:Drawnix。界面简约,满足日常基本绘图需求,且支持 mermaid 语法转流程图等,用起来非常丝滑。整理一下分享给大家!


02 简介



2.1 名称的由来


Drawnix ,源于绘画( Draw)与凤凰( Phoenix )的灵感交织。凤凰象征着生生不息的创造力,而 Draw代表着人类最原始的表达方式。在这里,每一次创作都是一次艺术的涅槃,每一笔绘画都是灵感的重生。


创意如同凤凰,浴火方能重生,而 Drawnix 要做技术与创意之火的守护者。


2.2 框架


Drawnix 的定位是一个开箱即用、开源、免费的工具产品,它的底层是 Plait框架,Plait 是作者公司开源的一款画图框架,代表着公司在知识库产品上的重要技术沉淀。


Drawnix 是插件架构,与前面说到开源工具比技术架构更复杂一些,但是插件架构也有优势,比如能够支持多种 UI 框架(AngularReact),能够集成不同富文本框架(当前仅支持 Slate框架),在开发上可以很好的实现业务的分层,开发各种细粒度的可复用插件,可以扩展更多的画板的应用场景。


GitHub地址:


2.3 特性



  • 💯 免费 + 开源

  • ⚒️ 思维导图、流程图

  • 🖌 画笔

  • 😀 插入图片

  • 🚀 基于插件机制

  • 🖼️ 📃 导出为 PNG, JSON(.drawnix)

  • 💾 自动保存(浏览器缓存)

  • ⚡ 编辑特性:撤销、重做、复制、粘贴等

  • 🌌 无限画布:缩放、滚动

  • 🎨 主题模式

  • 📱 移动设备适配

  • 📈 支持 mermaid 语法转流程图

  • ✨ 支持 markdown 文本转思维导图(新支持 🔥🔥🔥)


2.4 安装


提供Docker部署:


docker pull pubuzhixing/drawnix:latest

也可以在线使用:


地址:


03 最佳实践


实践我们采用在线方式,如果注重安全或者本地使用可以使用Docker部署。


3.1 界面说明


通过https://drawnix.com/进入之后,所有功能如图:



中间工具栏分别表示:



  • 拖拽

  • 选中

  • 思维导图

  • 文字

  • 手绘

  • 箭头

  • 流程图

  • 插入图片

  • 格式转换


功能都比较简单,思维导图、流程图以及格式转化是常用的工具。



3.2 思维导图


思维导图的使用方式和Xmind的用法极为相似。选中节点,然后Tab键就可以添加同级子节点,也可以利用+号添加。-号并不是删除,而是合并。删除也非常简单,选中直接Delete即可。



3.3 流程图


流程图的图形相对来说比较简单,只有七项。



选中之后直接绘制即可,图形之间使用箭头链接即可。


3.4 格式转化


目前支持两种格式转化成流程图:



Mermaid



仅支持流程图、序列图和类图


Markdown



仅支持思维导图。


04 小结


Drawnix的极简设计满足日常绘图的需要,可能在很多功能上不如Xmind或者亿图,但是它确实两者的结合。是一款不错的绘图软件。


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

作为前端你必须要会的CICD

web
前言 这是一篇属于面向前端的关于CICD的入门文章,其旨在: 入门掌握CI CD的用法 学习CI和CD的含义及其实现细节 基于GitLab展示如何给自己手上的项目添加CICD的流程 学习本文你需要注意的事情 你的项目必须是支持Node版本 16.20.0...
继续阅读 »

前言


这是一篇属于面向前端的关于CICD的入门文章,其旨在:



  1. 入门掌握CI CD的用法

  2. 学习CICD的含义及其实现细节

  3. 基于GitLab展示如何给自己手上的项目添加CICD的流程


学习本文你需要注意的事情



  1. 你的项目必须是支持Node版本 16.20.0

  2. 笔者的CentOS安装Node18以上的版本底层库不支持,如果你想安装高版本的Node请先解决CentOS版本低的问题

  3. 本文采用的是CentOSLinux操作系统

  4. 本文的操作系统版本截图在下方


image.png


OK 如果你已经明白了我上面说的注意事项 那我们事不宜迟,直接开始本文的内容吧。


安装Node


(第一种)安装 NVM


请注意 如果使用这种安装方式在后面执行runner的时候在gitlab-runner账户会报错找不到Node 这是因为Linux的系统的用户环境隔离问题



curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash


更新环境变量

source ~/.bashrc


验证NVM是否安装成功

command -v nvm


使用NVM安装Node.js:

nvm install node


安装指定版

nvm install 16.20.0


使用 NVM 切换到安装的 Node.js 版本

nvm use node


nvm use node 16.20.0


验证安装:

node -v
npm -v
npx -v


(第二种)使用系统级 Node.js 安装

wget https://nodejs.org/dist/v16.20.0/node-v16.20.0-linux-x64.tar.xz


解压

tar xf node-v12.9.0-linux-x64.tar.xz


复制

cp -rf /root/node-v16.20.0-linux-x64 /usr/local/node


打开编辑配置文件

vim /etc/profile


在文件的最后一行加上这句话

export PATH=$PATH:/usr/local/node/bin


重载系统配置文件

source /etc/profile


这次在切换用户到gitlab-runner就可以查看到你安装的版本了

安装 Nginx


安装相关依赖


  • zlib 开启 gzip 需要

  • openssl 开启 SSL 需要

  • pcre rewrite模块需要

  • gcc-c++ C/C++ 编译器


yum -y install gcc-c++ zlib zlib-devel openssl openssl-devel pcre pcre-devel


下载压缩包

wget https://nginx.org/download/nginx-1.18.0.tar.gz


解压

tar -zxvf nginx-1.18.0.tar.gz
cd ./nginx-1.18.0
./configure
make
make install

查看安装路径

whereis nginx


编辑环境变量

vim /etc/profile


在文件最下面添加这两行

export NGINX_HOME=/usr/local/nginx


export PATH=$NGINX_HOME/sbin:$PATH


更新配置文件

source /etc/profile


查看nginx是否安装完成

nginx -v


开放 80 端口 如果不想一次性一个一个的放行端口,可以关闭防火墙

firewall-cmd --permanent --zone=public --add-port=80/tcp


查看防火墙的状态

systemctl status firewalld.service


关闭防火墙的状态

systemctl stop firewalld.service


查看已经放行的端口

firewall-cmd --zone=public --list-ports


重载防火墙

firewall-cmd --reload


启动

nginx


安装 GitLab


安装 GitLab,需要的时间比较长

yum -y install https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/gitlab-ce-14.3.0-ce.0.el7.x86_64.rpm


编辑配置文件

vim /etc/gitlab/gitlab.rb


修改配置文件

image.png


重载配置文件,

gitlab-ctl reconfigure


开放 1874 端口

firewall-cmd --permanent --zone=public --add-port=1874/tcp


查看已经放行的端口

firewall-cmd --zone=public --list-ports


重载防火墙

firewall-cmd --reload


修改gitlab的root用户密码

1.进入到gitlab控制面板中 gitlab-rails console -e production
2.执行命令: user = User.where(id: 1).first,此 user 则表示 root 用户

3、修改密码
执行命令:user.password = '12345678'修改密码
再次执行 user.password_confirmation = '12345678' 确认密码

4、保存密码
执行命令: user.save!

5、退出控制台
执行命令: exit

image.png


验证是否修改成功


http://192.168.80.130:1874/users/sign_in 把Ip换成自己的Ip 输入root的用户名和密码尝试进行登录,正常创建项目进行测试



配置 CI/CD


下载

wget -O /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64


分配运行权限

chmod +x /usr/local/bin/gitlab-runner


创建用户

useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash


安装 在 /usr/local/bin/gitlab-runner 这个目录下

gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner


运行

gitlab-runner start


新建 runner


注册 runner

gitlab-runner register


输入 gitlab 的访问地址

http://192.168.80.130:1874


输入 runner token

打开 http://192.168.80.130:1874/admin/runners 页面查看


image.png


runner 描述,随便填

测试vue项目部署


runner tag

ceshi


Enter optional maintenance note for the runner:

直接回车走过


输入(选择) shell 最后一步选择执行的脚本

shell


image.png


注册完成后,就可以在 http://192.168.80.130/admin/runners 里面看到创建的 runner。


nginx 配置项目访问地址


创建目录

mkdir -pv /www/wwwroot/dist


分配权限 如果后面执行脚本命令提示没有权限那就是这个地方有问题

chown gitlab-runner /www/wwwroot/dist/


(备用)如果权限有问题可以使用这个命令单独给这个目录设置上gitlab-runner用户权限

sudo chown -R gitlab-runner:gitlab-runner /www/wwwroot/


开放 3001 端口

firewall-cmd --permanent --zone=public --add-port=3001/tcp


重载防火墙 .

firewall-cmd --reload


打开 nginx 配置文件

vim /usr/local/nginx/conf/nginx.conf


在第一个 server 下方 (nginx 默认的,端口为80),加上下面的内容

server {
listen 80;
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root html;
index index.html index.htm;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
#新增开始
server {
listen 3001;
server_name localhost;
location / {
root /www/wwwroot/dist;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
#新增结束

重新加载配置文件

nginx -s reload


编写 .gitlab-ci.yml 文件


# 阶段
stages:
- build
- deploy

# 缓存 node_modules 减少打包时间,默认会清除 node_modules 和 dist
cache:
paths:
- node_modules/

# 拉取项目,打包
build:
stage: build
tags:
- ceshi
before_script:
# - export PATH=/usr/local/bin:$PATH
- node --version
- npm --version
- echo "开始构建"
script:
- cd ${CI_PROJECT_DIR}
- npm install
- npm run build
only:
- main
artifacts:
paths:
- dist/

# 部署
deploy:
stage: deploy
tags:
- ceshi
script:
- rm -rf /www/wwwroot/dist/*
- cp -rf ${CI_PROJECT_DIR}/dist/* /www/wwwroot/dist/
only:
- main

浏览器打开3001端口

提交代码到main分支上,如果想改为提交到其他分支上也可以进行自动部署就需要改only参数为分支名称


作者:青晚舟
来源:juejin.cn/post/7546420270421999642
收起阅读 »

一年多三次考试,总算过了系统架构师

前言 2024/12/27更新:实体证书也出来啦,如下: 2024/12/20更新:电子证书出来啦,如下: 算上这次,我其实已经参加了三次考试,先贴上这三次的成绩,相信大家也能感受到我的心情: 虽然这次总算通过了考试,但看到综合知识的成绩还是心有余悸,由于...
继续阅读 »

前言


2024/12/27更新:实体证书也出来啦,如下:


image.png


2024/12/20更新:电子证书出来啦,如下:


image.png


算上这次,我其实已经参加了三次考试,先贴上这三次的成绩,相信大家也能感受到我的心情:


20241214160256.png虽然这次总算通过了考试,但看到综合知识的成绩还是心有余悸,由于前两次考试的打击(都是差一门案例没有通过,上半年只差了两分),这次考试前只写了两套半的综合知识真题和在考前一天准备了大半天(背论文模板和知识点集锦的 pdf),而综合知识也是自己信心最足的一门,结果这次压线通过,所以还是建议大家只要报名了考试,还是要认真准备,至少把往年综合知识的真题刷一刷,避免最后发现只有综合知识未通过而追悔莫及。


我也在Github上分享了几个我备考用到的文档资料,大家自行取用。


PS:这次考试通过还要感谢我女友的祝福,我们在今年5.23相识(上半年考试前两天,然后考试也是差两分),再加上这次的压线通过,感受到了冥冥之中自有天意(❁´◡`❁)。


考试注意点


从去年下半年开始,软考统一由笔试改为机考,虽然不用再担心写字速度太慢或者不美观导致论文扣分,但要注意的是键盘只能使用考场提供的,因此很多人可能不太习惯。就我这几次的考试经验来说,两个小时写论文还是比较紧凑的,剩余时间都不超过10分钟,还要用这点时间去通读检查一遍论文有没有什么错别字,因此在考前准备一个论文模板还是十分必要的,这样就可以在写模板内容的同时去构思正文,对于时间充分的小伙伴来说,也可以计时去练习写几篇论文。另外需要注意从2024年开始,系统架构师改为一年两考(不通过也可以趁热打铁立刻准备下一次的考试了),上午考综合知识和案例(总共四小时,分别两小时,综合知识写完可提前半小时交卷去写案例),下午考论文(两个小时),考试时间安排如下:


考试时间考试科目
8:30—12:30综合知识、案例分析
14:30—16:30论文

备考经历


第一次(2 ~ 3个月):看完某赛视频全集(无大数据相关)+ 某赛知识点集锦 + 写完历年综合知识真题 + 案例论文对着答案看一遍(写了几道质量属性和数据库相关的案例题)+ 准备并背诵一个论文模板。


第二次(0.5 ~ 1个月):这次将上次的看视频改为了看教材(把考试重点的几个章节内容都混了个眼熟),然后其他准备都差不多,只是准备时间有相应减少。


第三次(1 ~ 2天):两套半综合知识真题 + 大致浏览一遍知识点集锦 + 背诵论文模板。


备考主要有以下注意点:



  1. 视频课不管是哪一家都无所谓,但需要注意架构师考试在22年12月更新了考试大纲,所以需要留意视频的版本不可太老,然后就是不管是在B站、闲鱼还是原价购买都不会有什么差别,只需保证视频内容完整即可。

  2. 各个机构的模拟题不要过多在意,尤其是考纲之外的题目,可作为对个人学习情况的测试。

  3. 近三次的考试由于是机考,只能在网上找到部分回忆版,不再有完整版真题,这个可自行搜索了解。

  4. 如果是第一次备考,建议还是至少 2 ~ 3 个月,除非基础特别好,不然还是建议将视频课看完(至少看完核心内容,计算机基础部分的优先级最低),这样至少可以保证综合知识问问拿下,还有就是真题特别特别特别重要


备考方式


综合知识


就我的经验来讲,我觉得综合知识是最可控的部分,只需将视频课 + 重要知识点集锦 + 历年综合知识真题过一遍,综合知识是完全不需要担心的。还有就是遇到考纲之外的真题,比如今年有一道题是:一项外观设计专利里面相似设计最多有几个,像这种基本无再考可能的题,只需要看到答案后混个眼熟就可以。除此之外还有一部分反复考的知识点:构件、4 + 1视图、ABSD、DSSA、架构评估(质量属性)、系统架构风格、项目时间和成本计算以及软件测试,这些内容需要格外留意,有时间的话,可以把教材上相关知识的内容过一遍。除此之外,一定要记得考试时相信自己的第一感觉,不确定的题目不要修改答案


案例分析


案例分析的题型变化比较大,更考验平时的技术积累,不过第一道必选题近几年都是和质量属性相关(除了23年下半年是大数据),然后就是 Redis 的考频也比较高,近三次考试有两次涉及(以往也有涉及),在24年上半年甚至精确到了命令的考察。此外,近几次案例也都考到了关于技术架构图的填空题,所以建议练习一下往年的相关题型,再到 ProcessOn 之类的平台找几个技术架构图看看。



案例考察的范围比较广,因此建议在高频考点上多加复习和准备。然后遇到不熟悉的知识点也不要慌,更不要空着不写,可以分点试着写一些或者硬凑一些相关的内容,能得一分是一分。如果时间充足,还是建议把往年的案例真题按照时间由近到远认真看一看,即使是一些视频中说的考试概率很低的知识点(Hibernate和设计模式)在前两次的考试和论文中也都有涉及,尤其是项目和技术经验不是那么丰富的小伙伴(比如我自己)需要注意这点。



论文


虽然看到很多小伙伴都说论文难写还会卡分,但因为我三次考试也都只有案例未过,论文虽然分数不高,但也都过了合格线,这里也分享一下我的写作经验。


我觉得写论文只需要记住真实项目 + 技术点讨论 + 论点点题并结合项目分析 + 项目中遇到的问题点这几点即可,即使内容有点流水账也无伤大雅,最重要的是写的让项目看起来真实,是自己做的,除了摘要和开头结尾可以找模板进行参考,正文部分还是需要自己结合论点去写,不能全是理论而没有一点技术点(使用到的各种工具和服务也都可以说,例如代码评审使用和项目管理相关的)的讨论。就以我这次的论文结构为例,首先是摘要部分(250字以内):



​ 2022年12月,我所在公司承接了某区xxx的开发项目。我在该项目中担任系统架构设计师的职务,负责需求分析和系统的架构设计等工作。该项目主要提供xxx、xxx和xxx功能。本文将结合作者的实践,以xxx项目为例,论述xxx在系统开发中的具体应用。在xxx模块使用了xxx,解决了xxx问题。在xxx模块使用了xxx,解决了xxx问题。在xxx模块使用了xxx,解决了xxx问题。实践证明,采用xxx,提升了软件的开发效率和质量。整个项目历时一年多,于今年6月正式上线运行,整个系统运行稳定,达到了预期的目标的要求。



然后是开头和结尾(800字左右):



​ ......。(项目背景,150字左右)


​ 正是在这一背景下,2022年12月,我们公司承接了xxx项目,在本项目中,我担任系统架构师的职务,负责需求分析和系统的架构设计等工作。经过对项目的调研和对用户需求的分析,我们确认了系统应当具有以下功能:xxx,xxx,xxx。基于以上的需求,我们采用xxx解决了xxx问题。(300字左右,这部分介绍功能的部分可以和摘要内容有重合)


​ 经过团队的共同努力,本项目按时交付,于今年6月顺利交付并上线,到目前运行稳定,不管是xxx使用xxx,还是xxx使用xxx都反馈良好。但在实施的过程中也遇到了一些问题,xxx。而如何让xxx更xxx是一项长期的工作,还有很多问题需要在实践中不断探索,在理论中深入研究并加以解决。只有这样,xxx才能不断地优化和发展,xxx。(350字左右)



最后是正文,由于我写的是软件维护(具体包含完善性维护、预防性维护、改正性维护、适应性维护),所以我首先用200 ~ 300字描述了这四种维护的具体含义(可以用自己的语言去描述,不需要和书上完全一致)。然后针对每种维护,再分四段用250 ~ 300字去结合项目和技术点具体去讨论我在每种维护中所做的工作。


当然上面只是我的一些论文写作经验,至少最近三次都是按照这个模板和套路去写,也都通过了。不过大家还是要结合自己的项目去做一些修改,建议多找几个论文综合一下,然后结合自己的语言去写一个属于自己的模板( •̀ ω •́ )✧。


感想


经过这三次的备考和考试经历,我觉得除了一些实力外,运气也占了一部分。就像这次的案例考了我熟悉的也简单的质量属性和 Cache Aside 缓存策略,前两次都有涉及到大数据这个我不熟悉的相关知识,也是我挂在案例的原因之一,所以大家如果考试遇到不熟悉的题或者分数还差一点,不妨再试一两次,相信自己可以的(●'◡'●)。如果大家有什么问题,也可以留言交流讨论。


作者:庄周de蝴蝶
来源:juejin.cn/post/7449570539884265524
收起阅读 »

当一个前端学了很久的神经网络...👈🤣

web
前言最近在学习神经网络相关的知识,并做了一个简单的猫狗识别的神经网络,结果如图。虽然有点绷不住,但这其实是少数情况,整体的猫狗分类正确率已经来到 90% 了。本篇文章是给大家介绍一下我是如何利用前端如何做神经网络-猫狗训练的。如果觉得这篇文章有些复杂,那么也可...
继续阅读 »

前言

最近在学习神经网络相关的知识,并做了一个简单的猫狗识别的神经网络,结果如图。

虽然有点绷不住,但这其实是少数情况,整体的猫狗分类正确率已经来到 90% 了。

本篇文章是给大家介绍一下我是如何利用前端如何做神经网络-猫狗训练的。如果觉得这篇文章有些复杂,那么也可以看看我的上一篇更简单的鸢尾花分类

步骤概览

还是掏出之前那个步骤流程,我们只需要按照这个步骤就可以训练出自己的神经网络

  1. 处理数据集
  2. 定义模型

    1. 神经网络层数
    2. 每层节点数
    3. 每层的激活函数
  3. 编译模型
  4. 训练模型
  5. 使用模型

最终的页面是这样的

处理数据集

  1. 首先得找到数据集,本次使用的是这个 http://www.kaggle.com/datasets/li… 2000 个猫图,2000 个狗图,足够我们使用(其实我只用了其中 500 个,电脑跑太慢了)
  2. 由于这些图片大小不一致,首先我们需要将其处理为大小一致。这一步可以使用 canvas 来做,我统一处理成了 128 * 128 像素大小。
const preprocessImage = (img: HTMLImageElement): HTMLCanvasElement => {
const canvas = document.createElement("canvas");
canvas.width = 128;
canvas.height = 128;

const ctx = canvas.getContext("2d");
if (!ctx) return canvas;

// 保持比例缩放并居中裁剪
const ratio = Math.min(128 / img.width, 128 / img.height);
const newWidth = img.width * ratio;
const newHeight = img.height * ratio;

ctx.drawImage(
img,
(128 - newWidth) / 2,
(128 - newHeight) / 2,
newWidth,
newHeight
);
return canvas;
};

这里可能就有同学要问了:imooimoo,你怎么返回了 canvas,不应该返回它 getImageData 的数据点吗。我一开始也是这样想的,结果 ai 告诉我,tfjs 是可以直接读取 canvas 的,牛。

tf.browser.fromPixels() // 可以接受 canvas 作为参数

  1. 将其处理为 tfjs 可用的对象
  // 加载单个图片并处理为 tfjs 对应格式
const loadImage = async (category: "cat" | "dog", index: number): Promise<ImageData> => {
const imgPath = `src/pages/cat-dog/image/${category}/${category}.${index}.jpg`;
const img = new Image();
img.src = imgPath;

await new Promise((resolve, reject) => {
img.onload = () => resolve(img);
img.onerror = reject;
});

return {
path: imgPath,
element: img,
tensor: tf.browser.fromPixels(preprocessImage(img)).div(255), // 归一化
label: category === "cat" ? 0 : 1,
};
};

// 加载全部图片
const loadDataset = async () => {
const images: ImageData[] = [];

for (const category of ["cat", "dog"]) {
for (let i = 1000; i < 1500; i++) { // 这里只使用了后 500 张,电脑跑不动
try {
const imgData = await loadImage(category, i);
images.push(imgData);
} catch (error) {
console.error(`加载${category === "cat" ? "猫" : "狗"}图片失败: ${category}.${i}.jpg`, error);
}
}
}
return images;
};

定义模型 & 编译模型

由于我们的主题是图片识别,图片识别一般会需要用到几个常用的层

  1. 最大池化层:用于缩小图片,节约算力。但也不能太小,否则很糊会提取不出东西。
  2. 卷积层:用于提取图片特征
  3. 展平层:将多维的结果转为一维

有同学可能想问为什么会有多维。首先是三维的颜色,输入就有三维;卷积层的每一个卷积核,都会使结果增加维度,所以后续的维度会很高。这张图比较形象,最后就只会剩下一维,方便机器进行计算。

  // 创建卷积神经网络模型
const createCNNModel = () => {
const model = tf.sequential({
layers: [
// 最大池化层:降低特征图尺寸,增强特征鲁棒性
tf.layers.maxPooling2d({
inputShape: [128, 128, 3], // 输入形状 [高度, 宽度, 通道数]
poolSize: 2, // 池化窗口尺寸 2x2
strides: 2, // 滑动步长:每次移动 n 像素,使输出尺寸减小到原先的 1/n
}),

// 卷积层:用于提取图像局部特征
tf.layers.conv2d({
filters: 32, // 卷积核数量,决定输出特征图的深度
kernelSize: 3, // 卷积核尺寸 3x3
activation: "relu", // 激活函数:修正线性单元,解决梯度消失问题
padding: "same", // 边缘填充方式:保持输出尺寸与输入相同
}),

// 展平层:将多维特征图转换为一维向量
tf.layers.flatten(),

// 全连接层(输出层):进行最终分类
tf.layers.dense({
units: 2, // 输出单元数:对应猫/狗两个类别
activation: "softmax", // 激活函数:将输出转换为概率分布
}),
],
});

// 编译模型,参数基本写死这几个就对了
model.compile({
optimizer: "adam",
loss: "categoricalCrossentropy",
metrics: ["accuracy"],
});

console.log("模型架构:");
model.summary();

return model;
};

这里实际上只需要额外注意两点:

  1. 卷积层的激活函数activation: "relu",这里理论上是个非线性激活函数就行。但是我个人更喜欢 relu,函数好记,速度和效果又不错。
  2. 输出层的激活函数activation: "softmax",由于我们做的是分类,最后必须是这个。

训练模型

训练模型可以说的就不多了,也就是提供一下你的模型、训练集就可以开始了。这里有俩参数可以注意下

  • epochs: 训练轮次
  • validationSplit: 验证集比例,用于测算训练好的模型准确程度并优化下一轮的模型
  // 训练模型
const trainModel = async (
model: tf.Sequential,
xData: tf.Tensor4D,
yData: tf.Tensor2D
) => {
setTrainingLogs([]); // 清空之前的训练日志

await model.fit(xData, yData, {
epochs: 10, // 训练轮数
batchSize: 4,
validationSplit: 0.4,
callbacks: {
onEpochEnd: (epoch, logs) => {
if (!logs) return;
setTrainingLogs((prev) => [
...prev,
{
epoch: epoch + 1,
loss: Number(logs.loss.toFixed(4)),
accuracy: Number(logs.acc.toFixed(4)),
},
]);
},
},
});
};

整体页面

基本就是这样了,稍微写一下页面,基本就完工了

总结

别慌,神经网络没那么可怕,核心步骤就那几步,冲冲冲。

源码:github.com/imoo666/neu…


作者:imoo
来源:juejin.cn/post/7477540557787938852
收起阅读 »

一天 AI 搓出痛风伴侣 H5 程序,前后端+部署通吃,还接入了大模型接口(万字总结)

web
自我介绍 大家好,我是志辉,10 年大数据架构,目前专注 AI 编程 1、背景 这个很早我就想写了 App 了,我也是痛风患者,好多年,深知这里面的痛呀,所以我想给大家带来一个好的通风管家的体验,但宏伟目标还是从小点着手,那么就有了今天的主角,痛风伴侣 H5。...
继续阅读 »

自我介绍


大家好,我是志辉,10 年大数据架构,目前专注 AI 编程


1、背景


这个很早我就想写了 App 了,我也是痛风患者,好多年,深知这里面的痛呀,所以我想给大家带来一个好的通风管家的体验,但宏伟目标还是从小点着手,那么就有了今天的主角,痛风伴侣 H5。


目录大纲


前面都是些开胃小菜,看官们现在我们就正式开始正文,那么整体目前是分的 6 个阶段。


第零阶段:介绍


第一阶段:需求


第二阶段:数据准备


第三阶段:开发+联调+部署


第四阶段:部署+上线


第五阶段:运营维护+推广


第六阶段:成本计算


前四个阶段可以分为一个大家的阶段,就完成了你的产品工作


最后就是收尾工作,以及后续的维护。


废话少说,就正式开始吧。


第零阶段:介绍


产品开发流程图


img


这是一个传统的软件开发流程,从需求的讨论开始到最后的产品上线,总共需要的六大步骤,包括后续的迭代升级维护。


成本测算


这里面其实最大的就是投入的人力成本,还不算使用的电脑、软件这些,还包括最大的就是时间成本。


我们按按照基本公司业务项目的项目来迭代看



  • 人力成本



    • 产品:1~2 人,有些大项目合作的会更多,跨大部门合作的。

    • UI :1 人

    • 研发:



      • 前端:1~2人

      • 后端:2~3人



    • 测试:1~2 人

    • 合计:这里面最少都是 6 人



  • 时间成本



    • 这里不用多少,大家如果有经验的,基本公司项目一般的需求都是至少一个月才能上线一个版本,小需求快的也就是半个月上线。



  • 沟通成本



    • 这个就用说了,大家都是合作项目,产品和 UI,产品和研发,研发和测试,这就是为啥会有那么多会的缘故,不同的工种,面对的是不同




img


时间成本感受


个人创业感想


那这里你就可能要较真了,你这个功能简单,哪能跟公司的项目比了。


那我就想起我之前跟我同学一起创业搞 app 的时候,那个时候我不会 app、也不会前端,我是主战大数据的,其实对后端有些框架不也太熟。


那会儿我们四个人,1 个 app、1 个后端+前端、1 个产品,也是足足搞了 1 个多月才勉强上了第一个小版本。


但是我们花的时间很多,虽然一个月,但那一个月我没睡过觉,不会就得学呀,哪像现在不会你找个 AI 帮手帮你搞,你就盯着就行,那会一边学前端,一遍写代码,遇到问题只能搜索引擎查,要么就是硬看源码去找思路解决。


想想就是很痛苦。


公司工作感想


本职工作是大数据架构,设计的都是后端复杂的项目通信,整体底层架构设计,但是也需要去做一些产品的事情。


但是大数据产品就不像业务系统配比那么豪华,一整个公司就两三个人,那么有时候就的去做后端服务、前端界面,就为了把我们的产品体验做好。


每天下班疯狂学习前端框架,从最基本的 html、css、js 学起,不然问题解决不了,花了大量的时间,并且做项目还要学习各种框架,不然报错了你都不知道咋去搜索。


这样能做大功能的事情很少,也就是修修补补做些小功能。


产品感想


这也是我最近用了 AI 编程后的感想,最近公司的数据产品项目,我基本都是 AI 编程搞定。


以前复杂的画布拖拉拽,我基本搞不定到上线的质量,现在咔咔的一下午就搞定开发,再结合 AI 的部署模板,一天就基本完成功能。效率太快。


也是这样,我在现在的公司的产出一周顶的上以前的一个月(这真不是吹牛,开会的半个小时,大数据的首页的 landing page 我就做好了🤦♂️) ,但是时间完全不用一周(偷偷的在这了讲,老板知道了,就。。。所以我要多做一些,让老板留下我)。


我现在感想的就是现在更加需要的就是你的创意、你的想法,现在的 AI 能力让更多的人提效的同时,也降低了普通人实现自己产品的可能性。这在以前是无法想象的,毕竟很多门槛是无法跨越,是需要时间磨练的。


效果展示


然后再多来几张美美的截图(偷偷告诉你,这就是我的背景图片工具做出来的。)


img


第一阶段:需求


1、需求思考


做产品最开的就是需求了,如果你是产品经理,那么我理解这一阶段是不需要 AI 来帮你忙的。


虽然大家基本对产品或多或少都有一些理解,那么专业性肯定比不了,那么我们就需要找专业的帮忙了。


所以我这里找的是 ChatGPT,大家找 DeepSeek,或者是 Gemin,或者是 Claude 都可以的。


我目前准备为痛风患者开发一个拍照识别食物嘌呤的h5应用,我的需求如下:

1. 这个h5底部有3个tab: 识别、食物、我的
2. 在【识别】页面,用户可以选择拍照或者选择相册上传,然后AI识别食物,并且给到对应的嘌呤的识别结果和建议。
3. 在【食物】页面,用于展示不同升糖指数的常见食物,顶部有一个筛选,用户可以筛选按嘌呤含量高低的食物,下方显示食物照片/名称/嘌呤/描述
4. 【我的】页面顶部有一个折线图,用户记录用户的尿酸历史;下方显示近3次的尿酸数据:包括平均、最低、最高的数据;还有记录尿酸和历史记录的列表。

在技术上,【我的】页面尿酸历史记录保存在本地localStorage中,【食物】的筛选也是通过本地筛选,拍照识别食物嘌呤的功能,采用通义千问的vl模型。

请你参考我的需求,帮我编写一份对应的需求文档。

发给 ChatGPT


img


这样就给我们回复了。


2、思考


你说我不会像你写那么多好的提示词,一个我也是借鉴别人的,一个就是继续找 AI 帮你搞定,比如你不知道 localstoreage 是什么,没关系,这个都是可以找 AI 问出来的。


img


或者是说你只有一个想法,而不知道这个产品要做成什么,也可以问 AI。


GPT 会告诉你每个阶段该做哪些功能,这样看看哪些对你合适,然后通过不断的多轮对话,来让他输出最后的需求文档。


img


3、创建需求工作空间


我们在电脑新建个目录,用来存放暂时的需求文档和一些前置工作的文件


Step 1: 在电脑的某个目录下创建前期我们需要的工作项目的目录,这里我叫 h5-food-piaoling


Step 2: Cursor 打开这个目录


Step 3: 创建 docs 目录


Step 4: docs 目录下创建 prd.md 文件,把刚刚 GPT 生成的需求文档拷贝过来。



我这里是后截图的,所以文件很多,不要受干扰了



img


4、重要的一步


到这里需求文档就创建好了,那么我们是不是马上就可以开发了,哦,NO,这里还有很重要的一步。


那么就是需要仔细看这个 GPT 给我们生成的需求文档,还是需要人工审核下的,避免一些小细节的词语、或者影响的需要修改的。


比如这里,我已经恢复不出来了,这里原来有些 “什么一页的文章,适合老年人的这些文字”,这些其实不符合我那会儿想的需求的,所以我就删除了。


img


比如这里用到的一些技术,如果你懂的话,就可以换成你懂的技术,也是需要考虑到后面迭代升级的一些事情。


img


总结:其实这里就是需要人工审查下,避免一些很不符合你想的那些,是需要修改/删除的,这个会影响后面生成 UI/交互的逻辑。



不过这个步骤不做问题也不大,这一步也是需要长久锻炼出来,后面等真实的页面出来后,你再去修改也行。



第二阶段:数据准备


这里的一步也是我认为比较特别的点,这个步骤的点可以借鉴到其他场景里面。


1、哪里找数据


你的产品里的数据的可信度在哪里?特别是关乎于健康的,网上的信息纷繁复杂,大家很难分清哪些是真的,哪些是假的。


我之前查食物的嘌呤的时候,就遇见了,同样一个食物,app 上看到的,网上看到的都不一样,我就黑人问号了???


所以,这里就涉及到数据的权威性、真实性了。那么权威机构发布的可信度会更强。


所以我找到了卫健委颁发的数据。


地址:http://www.nhc.gov.cn/sps/c100088…


img



另外还可以看到不止痛风的资料有,还有青少年、肥胖、肾病的食养指南。


这些病其实都是慢性病,不是吃药就能马上好起来,需要长期靠饮食、运动来恢复的。


可以把这些数据用起来,后面挖掘更多需求。



2、下载数据


这一步周就是把数据下载下来,直接点击上面的


img


下载来后是个pdf 的文件,那么这一步我们就准备好了。


这里我附带一份,大家可以作为参考


暂时无法在飞书文档外展示此内容


3、处理数据


这一步是为什么了,是因为目前在所有的 AI 编程工具里面,pdf 是读取不了的,特别是 Cursor 里面。


目前能够读取的是 markdown 格式的数据



markdown 格式的数据很简单,就是纯文本,加上一些符号,就可以做成标题显示


不懂的可以直接问题 AI 工具就行了。



这里就可以看到大模型给我们的解释了。


插曲

我不懂 markdown 是什么,帮我解释下,我一点都不懂这个

在 Cursor 里面使用 ask 模式来提问


img


下面就是一个回答的截图,如果你对里面的文字不清楚的,那么就继续问 AI 就可以了。多轮对话。


img


处理数据

这里就是需要把 pdf 转为 markdown 的数据


这里推荐使用:mineru.net/


重点在于免费,直接登录注册进来后,点击上传我们刚下载的 pdf。


img


等待上传转换完成,下一步就是在文件里面,看到转换的文件了。


点击右侧下载,就是 markdown 格式。


img


把下载好的 markdown 文件放入到项目里面的 data 目录,待会儿会需要数据处理。


img


4、修正需求文档


那么让 Cursor 给我们重新生成需求文档,这样食物的分类,还有统计,会更准确,因为现在是基于权威数据来的。


食物数据库目前是存储在 json 文件里,请根据 @成人高尿酸血症与痛风食养指南2024 年版).md 的食物嘌呤数据,再根据 @prd.md 里面的食物数据结构,生成一份数据,并获取对应的 image 图片,保存在 imgs 目录下

img


5、生成数据文件


前面我们不是讲到了。食物列表的数据需要存储在本地,也就是客户端,形式我们就采用 json 的形式



同样你不知道 json 是个啥的话,找 AI 问,或者直接 Cursor 里面提问就行了。



左边是提示词,右侧就是创建的 json 文件


食物数据库目前是存储在 json 文件里,请根据 @成人高尿酸血症与痛风食养指南2024 年版).md 的食物嘌呤数据,再根据 @prd.md 里面的食物数据结构,生成一份数据,并获取对应的 image 图片,保存在 imgs 目录下

imgimg


结果:


img


6、继续调整文件


上一步骤发现,其实只给我们列觉了 53 种食物,并不全


我需要全部的数据,那么继续


总结的有 53 种食物,但是我看 @成人高尿酸血症与痛风食养指南2024 年版).md 下的“表1-2 常见食物嘌呤含量表” 应该不止这么多,请再次阅读然后补全数据到 @foods.json 文件里

img


最后发现,总文档里总结了 180 种的食物


img


最后生成的数据文件如下:


img


6、图片问题


不过这里有个问题就是,食物对应的图片是没有办法在这里一次性完成的


我也尝试了在 Cursor 里让他帮我完成,结果些了一大堆的代码,下来的图片还对应不上。


尝试了很多方案,都不太理想。


那你说了,去搜索引擎下载了,我也想到了,不过想起来你要去搜索,然后找图片,下载,有 180 多种了,还要命名好图片名字,最后保存到目录。


想到这里,我就头大,索性干脆自己写一个,其他流程系统都帮我搞定,暂时目前只需要我人工确认图片,保证准确性。


Claude Code + 爬虫碰撞什么样的火花,3 小时搞定我的数据需求



这个小系统也还是有很大的挖掘潜力,后面也还可以做很多事情



到这里基本需求阶段就完成了,数据也准备的差不多了,下面就是进入开发阶段了。


不要看前面的文字多,那都是前戏,下面就是正戏,坐稳扶好,开奔。


第三阶段:开发+联调+测试


这里是主要的开发、联调、测试阶段,也就是在传统开发流程中会占据大部分的时间,基本一个软件/系统的开发大部分的时间都在这个里面,所以我们看看结合 AI 它的速度将会达到什么样。


== 1、前端 ==


步骤一:bolt 开发



说下为什么采用 bolt 工具来做第一步工作。


其实线下 v0、bolt、lovable 很多这种前端设计工具,那么他与 Cursor 的区别在哪里了?


1、首先通过简单的提示词,它生成的功能和 UI 基本都是很完善的,UI 很美、交互也很舒服。这种你在 Curosr 里面从零开始些是很难的。


2、这种工具一般都可以选择界面的上的元素(比如 div、button,这个就比较难),然后进行你的提示词修改,很精准,这个你在 Cursor 里面比较难做。


3、还有一个点就是前端开发的界面的定位这些大模型很难听得懂你在说啥的,所以我感觉也是这块的难度采用了上面那么多的类似的工具的诞生。



当然,如果不用这些工具,直接让 Cursor 给你出 ui 设计,然后使用 UI 设计出前端代码也可以的。


这个我看看后面用其他例子来讲解。


把上面的需求步骤的 prd.md 的需求直接粘贴到提示词框里。没问题,就可以直接点击提交了。



小技巧:看左下角有个五角星的图标,是可以美化提示词的,这个目前倒是 bolt 都有的功能。


另外还可以通过 Github 或者 Figma 来生成项目图片。



img


下面就是嘎嘎开始干活了。


img


等他写完,就可以在界面的右侧看到写完的H5程序。


界面很简单,左侧就是对话区域,右侧就是产品的展示区域


小细节:在使用移动端展示的时候,还可以选择对应的手机型号


img


步骤二:调整


1、错误修复

这个交互我觉得做的特别好,不用粘贴错误,直接就在界面上点击“Attempt fix”就可以了,这真的是纯 vibe coding ,粘贴复制都不用了。🤦♂️


如果有错误,继续就可以了。


img


2、UI 调整:主题

刚开始其实 UI 并不是太好看,我的主题色是绿色的,所以我也不知道让它弄什么样的好看。


再帮我美化下 UI 界面

就输入了上面一句话,刚开始的 UI 如下图


img


最后看下对比效果


左边是最开始生成的,右边是我让他优化后的样子。还是有很多细节优化的。


img


3、UI 修复方式一:截图

另外如果样式有问题,可以截图粘贴到对话框,然后输入提示词修改。


img


4、UI 修复方式二:选择元素

这里就是我要说的可以选择界面上的元素,然后针对某些元素进行改写



bolt 的方式这几输入提示词


v0 比较高级,选择后,可以直接修改 div 的一些样式参数,比如:宽高、字体、布局、背景、阴影。精准调节。(低代码+vibe coding)



img


经过多轮修复,觉得功能差不多了,就可以转战本地 Cursor 就继续下一步了。


步骤三:本地 Cursor 修改


1、同步代码到 Github

点击右上角的「Integrations」里的 Github。


img


下面就会提示你链登录 Github


img


接着授权就可以


img


然欧输入你需要创建的项目名称


img


2、本地下载代码

使用 git 工具把代码下载到本地



git 就类似游戏的存档工具,每一个步骤都可以存档,当有问题的时候,就可以从某个存档恢复了。


当然:这里需要提前安装好 Git,如果有不懂的可以联系我,我来帮你解决。这你就不多说了



打开你的 Github 仓库页面,复制 HTTPS 的地址


img


然后使用下面的命令,就可以下载到本地了。


git clone 你的代码仓库地址

img


下一步就是安装代码的依赖包



这里需要 nodejs 环境,同样就不多说了,不懂的可以私聊



img


下一步就是启动


img


接着就是浏览器打开上面的地址:http://localhsot:3000,就可以看见上面写好的页面。


默认打开是按照 pc 的全屏显示的,可能看着有些别扭


img


我们打开 F12,打开调试窗口,如下图


点击右侧类似电脑手机的按钮,就可以调到移动端模式显示了,还可以选择对应的机型。


img


xx 小插曲 xx


原本不想放这里的,结果还是放一下吧,刚好是解决了一个很大的问题


刚开始在 bolt 上面修改的时候,修改后一直报个错误,结果修复了很多次,还是没有解决。


img


没办法,我就在本地 Cursor 上仔细看了下代码,发现是个引号的问题。


img


我就在本地 Cursor 中快速修复了下


img


但是后面惊悚的事情来了,我去 bolt 上调整了下界面样式,结果又给我写成了引号的问题


最后我就发现,可能 bolt 目前对这类的错误还是没有意识。并且看它界面的代码,每次都是从头开始写(难怪要等好一段时间才弄完,究竟是什么设计了?)


最后索性,我仔细看了下代码,删除掉了,没啥大的影响。


目前来看 bolt 这种工具还是有点门槛,解决错误的能力还是没有 Cursor 强大,一不小心页面上的错误就在一起存在,你也不知道它改了啥。


这就需要你对代码还是有基本的认识。


步骤四:使用本地数据


首先就是把前面下载准备好的图片放到 imgs 目录下


img


在 Cursor 中让从 imgs 目录中显示图片。


img


不过这里 Cursor 还是很智能的,访问后都是 404


img


那么就直接告诉 Cursor 让他解决这个问题。


结果他一下子就找到问题所在了,需要放在 public 目录下,这个放以前你需要去搜索引擎里面找问题,并且有时候你拿让 AI 解决的问题,去搜索引擎找,基本都是牛头不对马嘴的回答。


最后还要去找官方文档看资料,不断的尝试。


imgimg


** 前端小结 **


到这里,基本前端的事情就搞完了


1、识别:识别流程,现在都是走前端模拟的流程


2、食物:这里目前应该是很全的功能了,读取本地的 json 数据,有分类标识,还有图片的展示


3、我的:个人中心有尿酸的记录,有曲线图,还有基本的体重指数记录。


img


== 2、后端 ==


步骤零:阿里云大模型准备


背景:需要使用大模型来识别图片,然后返回嘌呤的含量,所以我们需要选择一个大模型的 API 来对接。


这里选择阿里的 qwen 来对接。


登录百炼平台:bailian.console.aliyun.com/


访问API-kEY 的地址:bailian.console.aliyun.com/?tab=model#…


创建一个 API-kEY,并保存好你的 key 信息。


img


步骤一:创建必要的配置


先访问找到通义千问 API 的文档的地方


img


这里我们采用直接复制上面页面的内容,保存到项目下的 docs 目录在的 qwen.md 里面


img



这里顺便把之前的 prd.md 文档从之前的项目目录拷贝过来了



步骤二:创建后端服务模板代码


直接使用下面的提示词,就可以创建一个后端的服务



这里要想为什么要创建后端服务,


一方面主要是需要调用大模型的 API,用到一些KEY 信息,这些是需要保密的,不能在前端被人看到了。


另外一方面,后面如果需要一些登录注册服务,还有食物数据都是需要后端来存储,提供给前端。



请在项目的根目录下创建 backend 目录,在这个 backend 目录下创建一个基于fastify框架的server,保证服务没有问题


同样的不知道什么fastify技术的,找大模型聊就行。



imgimgimg


步骤三:API 文档+后端业务服务开发


重点来了,这里我就写到一个提示词里面,让他完成的


帮我接入图像理解能力,参考 @qwen.md  :
1. 现在在 @/backend 的后端服务器环境中调用ai能力,
2. 使用 .env 文件保存API_KEY,并使用环境变量中的DASHSCOPE_API_KEY.并且.env文件不能提交到git上,提交到git的可以用.env.example文件作为举例供供用户参考
3. 要求使用openai的sdk,并且前端上传base64的图片
4. 后端返回值要求返回json格式,返回的数据能够渲染识别结果中的字段,包括:食物/嘌呤值/是否适合高尿酸患者/食用建议/营养成分估算
5. 在 @/backend 目录下创建 api.md 文件,记录后端接口文档

这里我把 api.md 高亮了,这个是关键,是后面前后端联调的关键,不然 Cursor 是不知道请求字段和响应字段该怎么对接的,到时候数据不对,再来调试就比较麻烦。


所以接口文档务必保证 100% 准确,后面的调试就会很容易。


截图如下:


imgimgimg


很贴心的完成功能后,最后帮我们些了 api.md 接口文档,还进行了一些列测试,保证功能是完整的。


这里放出来,Cursor 看是怎么帮我们写这个代码的



  • 帮我们组装好了提示词

  • 根据 qwen.md 的接口文档,组装请求数据和返回数据,字段都我们的项目符合


img


== 3、联调 ==


其实这里的联调很简单了。就是一句话的事情。


因为之前的前端的拍照图片都是走的模拟的接口,没有真正的调用后端的接口,所以需要换成真正的后端接口。


刚好前面的后端服务写好了 api.md 接口文档


前端修改点,前端目录是当前根目录
1. 也需要加入请求后端的 url 的环境变量,本地调试就默认使用 localhost,线上发布的时候设置环境变量后,前端服务从环境变量获取 url 然后请求到对应的后端服务
2. 食物识别的接口参考 @api.md 文档,请修改需要适配的地方,食物识别的代码在 @identify-page.tsx代码中。

imgimgimg


这里要说的是:前面的 api.md 接口文档些的非常准备,这一步的前端请求后端接口,基本都是一遍过,所以后端提供的接口文档一定要准确,这样前端就可以很准确的调用接口传参和取返回值了。


== 4、测试 ==


其实到这里,基本测试的工作也就完成了。


基本的流程到现在都是跑通的。


不过还是需要多实际测试,这里下面的例子就是,我上传了「黄瓜」的照片,结果没识别,按理说不应该呀。


这里上了点专业的技巧,通过 F12 的调试窗口,看下接口返回的数据。



按照以往经验来说,估计是字段对应不上



img


所以我就直接和 Cursor 说,可能是字段对应不上。请帮我修复。


测试黄光的食物的时候,后台接口返回的数据是 "purine_level": "low(低嘌呤<50mg)",但是 @getPurineLevel() @getPurineLevelText 没有识别到,请帮我修复

最后从前后端都给我做了修复,字段的匹配对应上了。


img


最后的总结如下:


img


== 4、总结 ==


其实到这里基本功能就完成了。



  1. 前端使用 bolt 工具等生成,快速生成漂亮的 UI 界面和基本完整的前端功能



    1. bolt工具调整样式、UI 等细节(擅长的)

    2. Cursor 精修前端小细节



  2. Cursor 开发完整后端功能



    1. 写清楚需求,如果知道具体技术栈是最好的

    2. 写好接口文档,最好人工校验下



  3. 前后端联调



    1. @使用后端的接口文档,最好写改动的接口的地方,前后精准对接

    2. 学会使用浏览器的 F12 调试窗口,特备是接口的请求参数和响应值的学习。




就目前来看,如果你是零基础,那么基本的术语不明白的话,有些问题可能会不好解决



  1. 寻求 AI 的帮助,遇事不决问 AI,它可以帮你搞定

  2. 寻求懂行的人来帮助你,比如环境的事情、按照的事情有时候一句话就可以给你讲明白的。


第四阶段:部署+上线


部署这一块其实对普通人门槛还比较高的,问题比较多。



  • 域名问题

  • 服务器问题

  • 如何部署,如何配置


这里我们采用云厂商的部署服务,简化配置文件和部署的流程


但是域名申请还是需要提前准备好的,不过现在我们用的这个云服务暂时现在没有的域名,也有临时域名可以先用。


到这里,其实如果你只是本地看的话,就已经可以了,那么这里我们教一个上线部署的步骤,傻瓜式的,不需要各种配置环境。


我相信大家如果搞独立开发的 Vercel 肯定都熟悉了。这里也介绍下类似的工具,railway.com/,他不仅可以部署前端静态页面,还有后端服务,PostgreSQL、Redis 等数据库也支持一键部署。


img


1、项目的配置文件


railway 部署是需要一些配置文件的,当然我们可以让 Cursor 帮我们搞定。


直接告诉 Cursor 我们需要部署到 railway 上,看还需要什么工作可以做的。


后端


@/backend 这个后端项目现在需要在railway上去部署,请帮我看看需要哪些部署配置

imgimgimg


前端


也是一样,让 Cursor 给我们生成部署的配置文件


当前目录是前端目录,也需要添加railway的部署相关配置

imgimgimg


Cursor 会帮我们创建需要的配置文件,那么就可以进入下一步部署了。


2、提交代码


记得要提交代码,在 Cursor 的页面添加提交代码,推送代码到 Github 上,这样 railway 才可以拉取到代码。


提交代码的时候,可以使用 AI 生成提交信息,也可以自己填写信息


imgimg


记得还要同步更改


imgimg


3、railway 页面操作


现在会有赠送的额度,并且免费就用也有 512M 的内存机器使用。对于当前下的足够了。


注册登录后,选择 Dashboard 后,点击添加,就可以看到如下的页面,


添加 Github 项目,后续就会授权等操作,继续完成就可以。


img


下一步就一个你的项目


然后就会跳转到工作区间,会自动部署。


img


记得不要忘记环境变量


img


就是在「Variables」标签下,直接添加变量就行。


添加完记得需要重新部署下。


后端环境变量


img


前端环境变量


img


当然不过你有错误,可以把 log 里面的错误复制,粘贴到 Cursor 里面,让他解决,我之前部署的项目有个就有问题,通过这个方式,帮我解决了。


4、大功告成


部署完成后怎么访问了,切换到 settings 页面,有个 Networking 部分,可以生成一个 railway 自带的域名,用这个域名就可以访问了,如果你有自己的域名还可以添加一个自己的域名,添加完以后就可以自己访问了。


img


5、总结


很开心,跟我走到了这里,基本到这里,算是完成一大步,也就是我们的 MVP 完成了。


现在我们再来总结下前面整体的步骤


1、前端我们通过 bolt 来生成代码,加速前端的设计,让 bolt 这种工具提供我们更多的能力,发挥他的有点


2、后端使用 Cursor 来开发,纯业务逻辑通过提示词还是很好的达到效果。


3、前后端联调,写好接口文档,让 Cursor 必须阅读接口文档,前端再写接口


4、部署配置文件也可以通过 Cursor 来搞定,无所不能


5、中间有任何问题,有任何不懂的都可以找 Cursor 使用 ask 模式搞定。


第五阶段:运营维护+推广


分了「优化」「安全」「推广」三个部分来说这个事情。



  • 其实到这里是后续的常态,你不要不断的推广你的产品,去增加访问量。

  • 另外就是不断的迭代优化你的功能,提升用户体验,加强本身产品的竞争力。

  • 最后、最后、最后就是安全,这个不要忘记了,后面我也会加强后,然后去推广下我的产品,安全很重要,提前做好可以更保护你的服务器和大模型的 API-KEY。


优化


这个是上线了后发现的,就是使用手机拍的照片,一般都比较大,这张图片请求后端的时候,数据量比较大,接口超时了。


那么解决办法:


1、增加后端的请求体的大小


2、压缩图片,然后再请求后端接口


安全


其实这里还是蛮重要的,因为你的服务,还有你的大模型的 KEY,如果服务器被攻击是要付出代价的,最重要的是花掉你的钱呀。


所以这块我还在做,目前想的就是让 Cursor 正题 revivew 代码,看下有什么安全隐患,给我一些解决方案。


推广


如果你的产品上线后,需要写文章、发小红书去推广,首先从你的种子用户开始,你的微信群,你的朋友圈都是可以的。


后面积极听取用户心声,持续解决痛点需求,满足用户的痛点,产品就会越来越好。


第六阶段:成本计算


时间成本


从开始到结束上线,手机使用正式的域名访问,大概就是整一天的时间,从早上开始,忙到晚上我就开启了测试,晚上搞完还去外面遛弯了一大圈回来的。


我们就算:10 小时


人力成本


哈哈哈哈,很清楚,就我一个人


软件成本


bolt:20 元优惠包月(海鲜市场),就算正式渠道,20 刀一个月,当然有免费额度,调整不多,基本够用


Cursor:150教育优惠(海鲜市场),就算正式渠道,20 刀一个月,足足够用


域名:32首年


我们就算满的,折算成人民币,也就是 300 块


想想 300 块一天你就做出来一个系统(前后端+部署),何况软件都是包月的,一个月你可以产出很多东西,不止这个一个系统。


对比公司开发,一个月的成本前后端两个人,毕业生也的上万了吧,何况还是 5 年经验开发的(市面上的抢手货)。


总结


能走到这里的,我希望你给自己一个掌声,确实不容易。


我希望你也有可以通过编程来实现自己的想法和创意。


虽然目前编程对于零基础的人来说确实可能会有些吃劲,但是你我差距也不大,我现在遇到了很多在搞 AI 编程的都是程序员,有房地产行业的、也有产品的。


遇事不决,问 AI


我希望你可以记住这句话,自己的创意+基本问题找 AI,你基本就可以解决 99% 的问题,剩下的 1% 你基本遇不到,遇到了,也不要慌,身边这么多牛人总会有人知道。


作者:志辉AI编程
来源:juejin.cn/post/7517496354244067339
收起阅读 »

一行生成绝对唯一 ID:别再依赖 Date.now() 了!

web
在前端开发中,“生成唯一 ID” 是高频需求 —— 从列表项标识、表单临时存储,到数据缓存键值,都需要一个 “绝对不重复” 的标识符。但看似简单的需求下,藏着很多容易踩坑的实现方式,稍有不慎就会引发数据冲突、逻辑异常等问题。 今天我们就来拆解常见误区,带你掌握...
继续阅读 »

在前端开发中,“生成唯一 ID” 是高频需求 —— 从列表项标识、表单临时存储,到数据缓存键值,都需要一个 “绝对不重复” 的标识符。但看似简单的需求下,藏着很多容易踩坑的实现方式,稍有不慎就会引发数据冲突、逻辑异常等问题。


今天我们就来拆解常见误区,带你掌握真正可靠的唯一 ID 生成方案。


一、为什么 “唯一 ID” 比想象中难?


唯一 ID 的核心要求是 “全局不重复”,但前端环境的特殊性(无状态、多标签页、高并发操作),让很多看似合理的方案在实际场景中失效。


下面两种常见实现,其实都是 “伪唯一” 陷阱。


❌ 误区 1:时间戳 + 随机数(Date.now() + Math.random())


很多开发者会直觉性地将 “时间唯一性” 和 “随机唯一性” 结合,写出这样的代码:


// 错误示例:看似合理的“伪唯一”方案
function generateNaiveId() {
// 时间戳转36进制(缩短长度)+ 随机数截取
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
// 示例输出:l6n7f4v2am50k9m7o4

这种方案的缺陷在高并发场景下会暴露无遗:



  • 时间戳精度不足Date.now() 的精度是毫秒级(1ms),如果同一毫秒内调用多次(比如循环生成、高频接口回调),ID 的 “时间部分” 会完全重复;

  • 伪随机性风险Math.random() 生成的是 “非加密级随机数”,其算法可预测,在短时间内可能生成重复的序列,进一步增加冲突概率。


结论:仅适用于低频次、非核心场景(如临时展示用 ID),绝对不能用于生产环境的核心数据标识


❌ 误区 2:全局自增计数器


另一种思路是维护一个全局变量自增,看似能保证 “有序唯一”:


// 错误示例:自增计数器方案
let counter = 0;
function generateIncrementId() {
return `id-${counter++}`;
}
// 示例输出:id-0、id-1、id-2...

但在浏览器环境中,这个方案的缺陷更致命:



  • 无状态丢失:页面刷新、路由跳转后,counter 会重置为 0,之前的 ID 序列会重复;

  • 多标签页冲突:用户打开多个相同页面时,每个页面的 counter 都是独立的,会生成完全相同的 ID(比如两个页面同时生成 id-0)。


结论:浏览器环境中几乎毫无实用价值,仅能用于单次会话、单页面的临时标识。


二、王者方案:一行代码实现绝对唯一 —— crypto.randomUUID()


既然简单方案不可靠,我们需要借助浏览器原生提供的 “加密级” 能力。crypto.randomUUID() 就是 W3C 标准推荐的官方解决方案,彻底解决 “唯一 ID” 难题。


1. 用法:一行代码搞定


crypto 是浏览器内置的全局对象(无需引入任何库),专门提供加密相关能力,randomUUID() 方法可直接生成符合 RFC 4122 v4 规范 的 UUID(通用唯一标识符):


// 正确示例:生成绝对唯一ID
const uniqueId = crypto.randomUUID();
// 示例输出:3a6c4b2a-4c26-4d0f-a4b7-3b1a2b3c4d5e

2. 为什么它是 “绝对唯一” 的?


crypto.randomUUID() 的可靠性源于三个核心优势:



  • 极低碰撞概率:v4 UUID 由 122 位随机数构成,组合数量高达 2^122(约 5.3×10^36),相当于 “在地球所有沙滩的沙粒中,选中某一颗特定沙粒” 的概率,实际场景中碰撞概率趋近于 0;

  • 加密级随机性:基于 “密码学安全伪随机数生成器(CSPRNG)”,随机性远优于 Math.random(),无法被预测或破解,避免恶意伪造重复 ID;

  • 跨环境兼容:生成的 UUID 是全球通用标准格式(8-4-4-4-12 位字符),前端、后端(Node.js、Java 等)、数据库(MySQL、MongoDB)都能直接识别,无需格式转换。


3. 兼容性:覆盖所有现代环境


crypto.randomUUID() 的支持范围已经非常广泛,完全满足绝大多数新项目需求:



  • 浏览器:Chrome 92+、Firefox 90+、Safari 15.4+(2022 年及以后发布的版本);

  • 服务器:Node.js 14.17+(LTS 版本均支持);

  • 框架:Vue 3、React 18、Svelte 等现代框架无任何兼容性问题。


三、兼容性兜底方案(针对旧环境)


如果需要兼容旧浏览器(如 IE11)或低版本 Node.js,可以使用第三方库 uuid(轻量、无依赖),其底层逻辑与 crypto.randomUUID() 一致:


安装依赖:


npm install uuid
# 或 yarn add uuid

使用方式:


// 旧环境兜底方案
import { v4 as uuidv4 } from 'uuid';
const uniqueId = uuidv4();
// 示例输出:同标准UUID格式

四、总结:唯一 ID 生成的 “最佳实践”



对于 2023 年后的新项目,直接使用 crypto.randomUUID() 即可 —— 一行代码、零依赖、绝对可靠,彻底告别 “ID 重复” 的烦恼!


作者:大知闲闲i
来源:juejin.cn/post/7561781514922688522
收起阅读 »

前端终于不用再写html,可以js一把梭了,我的ovs(不写html,兼容vue)的语法插件终于上线了

web
OVSJS 语法预览 语法是这样的: 项目资源 GitHub 地址: ovsjs 示例代码 (hello.ovs) VS Code 语法提示插件: ovs-vscode-client 下载 模仿的kotlin的语法 欢迎大家体验交流! 卫星 (We...
继续阅读 »

OVSJS 语法预览


语法是这样的:


语法示例图


项目资源





模仿的kotlin的语法


image.png


欢迎大家体验交流!



  • 卫星 (WeChat): alamhubb


如果你对这个语法感兴趣欢迎和我交流,谢谢


dc35feef2cb4dae6c8678cd89571378e.png


作者:alamhubb
来源:juejin.cn/post/7579871631096266778
收起阅读 »

为何前端圈现在不关注源码了?

web
大家好,我是双越。前百度 滴滴 资深前端工程师,慕课网金牌讲师,PMP。我的代表作有: wangEditor 开源 web 富文本编辑器,GitHub 18k star,npm 周下载量 20k 划水AI Node 全栈 AIGC 知识库,包括 AI 写作、...
继续阅读 »

大家好,我是双越。前百度 滴滴 资深前端工程师,慕课网金牌讲师,PMP。我的代表作有:



  • wangEditor 开源 web 富文本编辑器,GitHub 18k star,npm 周下载量 20k

  • 划水AI Node 全栈 AIGC 知识库,包括 AI 写作、多人协同编辑。复杂业务,真实上线。

  • 前端面试派 系统专业的面试导航,刷题,写简历,看面试技巧,内推工作。开源免费。


开始


大家有没有发现一个现象:最近 1-2 年,前端圈不再关注源码了。


最近 Vue3.6 即将发布,alien-signal 不再依赖 Proxy 可更细粒度的实现响应式,vapor-model 可以不用 vdom 。


Vue 如此大的内部实现的改动,我没发现多少人研究它的源码,我日常关注的那些博客、公众号也没有发布源码相关的内容。


这要是在 3 年之前,早就开始有人研究这方面的源码了,博客一篇接一篇,跟前段时间的 MCP 话题一样。


还有前端工具链几乎快让 Rust 重构一遍了,rolldown turbopack 等产品使得构建效率大大提升。这要是按照 3 年之前对 webpack 那个研究态度,你不会 rust 就不好意思说自己是前端了。


不光是这些新东西,就是传统的 Vue React 等框架源码现在也没啥热度了,我关注每日的热门博客,几乎很少有关于源码的文章了。


这是为什么呢?


泡沫


看源码,其实是一种泡沫,现在破灭了。所谓泡沫,就是它的真实价值之前一直被夸大,就像房地产泡沫。


前几年是互联网发展的红利期,到处招聘开发人员,大家都拿着高工资,随便跳槽就能涨薪 20% ,大家就会误以为真的是自己的能力值这么多钱。


而且,当年面试时,尤其是大公司,为了筛选出优秀的候选人(因为培训涌入的人实在太多),除了看学历以外,最喜欢考的就是算法和源码。


确实,如果一个技术人员能把算法和源码看明白,那他肯定算是一个合格的程序员,上限不好说,但下限是能保证的。就像一个人名牌大学毕业的,他的能力下限应该是没问题的。


大公司如此面试,其他公司也就跟风,面试题在网络上传播,各位程序员也就跟风学习,很快普及到整个社区。


所以,如果不经思考,表面看来:就是因为我会算法、会源码,有这些技能,才拿到一个月几万甚至年薪百万的工资。


即,源码和算法价值百万。


现状


现在泡沫破灭了。业务没有增长了,之前是红利期,现在是内卷期,之前大量招聘,现在大量裁员。


你看这段时间淘宝和美团掐架多严重,你补贴我补贴,你广告我也广告。如果有新业务增长,他们早就忙着去开疆拓土了,没公司在这掐架。


面试少了,算法和源码也就没有发挥空间了。关键是大家现在才发现:原来自己会算法会源码,也会被裁员,也拿不到高工资了。


哦,原来之前自己的价值并不是算法和源码决定的,最主要是因为市场需求决定的。哪怕我现在看再多的源码,也少有面试机会,那还看个锤子!


现在企业预算缩减,对于开发人员的要求更加返璞归真:降低工资,甚至大量使用外包人员代替。


所以开发人员的价值,就是开发一些增删改查的日常 web 或 app 的功能,什么算法和框架源码,真实的使用场景太少。


看源码有用吗?


答案当然是肯定的。学习源码对于提升个人技术能力是至关重要的,尤其是对于初学者,学习前辈经验是个捷径。


但我觉得看 Vue react 这些源码对于开发提升并不会很直接,它也许会潜移默化的提升你的“内功”,但无法直接体现在工作上,除非你的工作就是开发 Vue react 类的框架。


我更建议大家去看一些应用类的源码,例如 UI 组件库的源码看如何封装复杂组件,例如 vue-admin 看如何封装一个 B 端管理后台。


再例如我之前学习 AI Agent 开发,就看了 langChain 提供的 agent-chat-ui 和 Vercel 提供的 ai-chatbot 这两个项目的源码,我并没有直接看 langChain 的源码。


找一些和你实际开发工作相关的一些优秀开源项目,学习他们的设计,阅读他们的源码,这是最直接有效的。


最后


前端人员想学习全栈 + AI 项目和源码,可关注我开发的 划水AI,包括 AI 写作、多人协同编辑。复杂业务,真实上线。


作者:前端双越老师
来源:juejin.cn/post/7531888067218800640
收起阅读 »

我们需要前端架构师这个职位吗?

前端架构师,这个岗位,在我的技术体系的认知中,是不需要这样的一个岗位的。但是市面上,我发现,在招聘的需求中,很多企业,依然在招聘前端架构师这个岗位。 为什么市场中,依然会有这个岗位呢? 真实的场景中,这个岗位的设立,是否真的是合理的呢? 作为前端出身的同学,这...
继续阅读 »

前端架构师,这个岗位,在我的技术体系的认知中,是不需要这样的一个岗位的。但是市面上,我发现,在招聘的需求中,很多企业,依然在招聘前端架构师这个岗位。


为什么市场中,依然会有这个岗位呢?


真实的场景中,这个岗位的设立,是否真的是合理的呢?


作为前端出身的同学,这是我一直在思考的一个问题,因为这个问题,关乎一个人的职业发展,代表了我们前端的同学,职业的道路的天花板,到底在哪里。


如果前端架构师,这个岗位,本身是不应该出现的,那么我们前端的职业道路,就不应该走纯技术路线。真正应该走的是,技术兼管理的路线,也就是应该走前端leader的角色,再往上就是技术总监/CTO的职业角色。


如果前端架构师,这个岗位,是应该出现,那么前端的职业道路,其实完全可以走纯技术路线,从开发到前端架构师,然后持续深耕。


不同的方向,对人的要求是不一样的。


走纯技术路线,对一个人的更大要求是专注,持续的技术学习力,持续的自我技术进步,其他方面,作为辅助,协助你的技术能力在职场中进行发挥,你就能在这个行业和岗位上,有一定的立足之地。


走leader的路线,对一个人的要求是全面,关注点在技术,人,事务,项目,管理等等一系列比较杂乱的事情上,它注定了不能过于专注,而是站在高点,俯瞰整个大盘,才能真正的把事情做好。


作为自己,一直以来,努力的方向,也是走技术leader的路线。


真实的市场场景是什么样?


有一句很经典的话,世界是由一帮草台班子组成的。


这句话反馈到真实的工作场景中,就变成了这样的现状。


大多数的人的技术水准,真的让人一言难尽,很多人,真的也就是把技术,当作一份吃饭的差事,大多数人对技术,并没有追逐的热情。


我们这个行业,有太多的小公司了,小公司招聘人的标准,和大公司比,也是真的一言难尽,我见过有些公司,用四五千的薪资,招聘了一些人做事,真的是啥也不会。


大量的中小型企业,招聘技术人员,薪资大概给1万左右,这类研发人员,大多数的水准,大概就是能把项目做出来,会一些框架,仅此而已。


但是我们日常中,遇到的业务问题,往往是复杂的,比如前端的复杂表单问题,不同环境的运行容器差异问题,各种各样的兼容问题,复杂的数据处理和渲染问题等等。这类问题,其实在日常的开发中,很常见,但是往往很多人对此,难以处理。


这个时候,很多企业,潜意识就觉得,招聘一个技术更厉害的人,这个时候,前端架构师这个岗位,其实就出现了,而这也是市场中,需要这个岗位的现实情况。


不同规模企业的前端组织架构到底有哪些差异?


在大公司,我看到的更多的是,前端技术leader的岗位,一般而言,是由一个技术leader,带领团队,完成业务。


比如阿里,比较有钱,一般一个团队中,p8是前端leader,p6/p7是做事的主力,配备部分p4/p5的同学,一起完成业务。整体大概是,一个leader负责统筹全局,团队中真正做事的同学,完全由能力驾驭自己的业务。


再中小型公司,我看到的是,因为成本的原因,招聘的工程师偏于初中级,然后团队中,有那么一两位高级工程师作为主力成员。有些时候,这些高级工程师还会担任leader的角色。


但是这类团队有一些问题,就是因为技术能力问题,无法真正的做到,对企业的业务负责。


1、针对业务场景,无法给出合理的方案/方法。经常性的在日常工作中,这类团队,会提到这个需求改动太大,这个方案没法实现,这个东西做不了等等。但是其实正常的场景中,业务方提出一个需求,本身是有一定的运营目标/目的,这个时候,应该从业务的角度出发,再结合我们互联网技术,针对这个运营目标,提出我们的产品/技术方案,然后执行。


2、无法做出合理的判断。在大多数的场景中,我们对代码的要求是,高内聚和低耦合,代码的结构要清晰,可维护要高。但是还应该有技术之外的一些判断,我们完成一项业务,应该团队配备什么样的成员,市场中哪类程序员好招聘,技术的选型和业务是不是最优解,在人员、技术,业务、成本,这类问题中,如何达到更高的效率最优。


在中小型公司,大家习惯性的把问题简单化,做不了,判断不了,以为招聘一个技术更厉害的人,就能解决当下问题。


那到底需要前端架构师吗?


这个答案很显然,其实当下的市场中,市场有这样的职位诉求,原因就是一些复杂问题,很多企业搞不定。(但是这个问题的解决之道,不在于招聘前端架构师这么一个岗位,而是在于团队内在的一些问题,这些问题恰恰是需要一个前端leader来解决的)


从职业发展的角度来说,其实是不需要前端架构师的。


我们从技术的角度,来分析一下为什么不需要前端架构师。


前端的职责,是对UI负责,我们的工作,主要是针对,不同的容器环境(浏览器、手机app、桌面端内嵌h5等等)、不同的技术(RN、react、小程序等等),实现不同的端上的产品(App、小程序、网页、桌面端应用),我们通过接口协议,同后端进行数据通信。


端上的页面,主体是运行在用户端的设备上,最大的障碍是加载/渲染性能。接口方面,是和用户的网络环境相关。


这些东西,其实从技术的层面上,属于开发的职责。我们不得不承认,前端开发的层面,入手会比后端简单一些,但是做到一定程度,其实要求是要比后端更高的。


所以前端架构师,到底在架构什么东西?它不过就是,针对每一种端上的技术,使用的比普通开发,更好一些。


相比而言,后端有太多的策略性的东西了,哪些数据是业务数据,哪些数据是缓存数据,哪些数据要支持实时查询,哪些数据支持统计查询,后端的基础组件也多,不同的组件,擅长的事情也不一样,适用的场景其实也有差异,kafka、redis这些东西,数据分库和分表,按什么维度分,机器的运行性能,支持的量到底有多大等等。


这些就是后端架构师的存在责任,同一块代码,在不同的场景下是完全不同的。在某些场景中可能是最优解,换一个场景,可能就是最差的代码。


这就是我一直推崇的价值观,前端和后端,不一样,前端不需要架构师。


合理的团队架构到底长什么样?


如我个人所评判的那样,一个团队中,是不需要前端架构师这个角色的,那么对于那些中小型公司的人来说,到底什么样的结构,符合自身最优价值的团队结构呢?


答案是这样的,一位专家级别的前端,带领一两个干活的主力,再加上多个初中级的工程师。专家级别的工程师-也就是leader,保障我们的技术和方案,是行业内标准方案,方向是最优的,一两个高级工程师,辅助这个leader把事情能落地下去,其他所有初中级工程师,就是干活的苦力,也就是团队内的技术体力劳动者。


这也就是,我认可的职业发展中,前端需要leader的原因,但是不需要架构师。


作者:Mapbarfront
来源:juejin.cn/post/7559053482831052846
收起阅读 »

别再死磕框架了!你的技术路线图该更新了

先说结论: 前端不会凉,但“只会几个框架 API”的前端,确实越来越难混 这两年“前端要凉了”“全栈替代前端”的声音此起彼伏,本质是门槛重新洗牌: 简单 CRUD、纯样式开发被低代码、模板代码和 AI 模型快速蚕食; 复杂业务、工程体系、跨端体验、AI 能力...
继续阅读 »

先说结论:


前端不会凉,但“只会几个框架 API”的前端,确实越来越难混

这两年“前端要凉了”“全栈替代前端”的声音此起彼伏,本质是门槛重新洗牌:



  • 简单 CRUD、纯样式开发被低代码、模板代码和 AI 模型快速蚕食;

  • 复杂业务、工程体系、跨端体验、AI 能力集成,反而需要更强的前端工程师去撑住。


如果你对“前端的尽头是跑路转管理”已经开始迷茫,那这篇就是给你看的:别再死磕框架版本号,该更新的是你的技术路线图。




一、先搞清楚:2025 的前端到底在变什么?


框架红海:从“会用”到“用得值”


React、Vue、Svelte、Solid、Qwik、Next、Nuxt……Meta Framework 一大堆,远远超过岗位需求。

现在企业选型更关注:



  • 生态成熟度(如 Next.js 的 SSR/SSG 能力)

  • 框架在应用生命周期中的角色(渲染策略、数据流转、SEO、部署)


趋势:



  • 框架 Meta 化(Next.js、Nuxt)将路由、数据获取、缓存策略整体纳入规范;

  • 约定优于配置,不再是“一个前端库”,而是“一套完整解决方案”。



以前是“你会 Vue/React 就能干活”,现在是“你要理解框架在整个应用中的角色”。





工具有 AI,开发方式也在变


AI 工具(如 Cursor、GitHub Copilot X)可以显著提速,甚至替代重复劳动。

真正拉开差距的变成了:



  • 你能给 AI 写出清晰、可实现的需求描述(Prompt);

  • 你能判断 AI 生成代码的质量、潜在风险、性能问题;

  • 你能基于生成结果做出合理抽象和重构。



AI 不是来抢饭碗,而是逼你从“码农”进化成“架构和决策的人”。





业务侧:前端不再是“画界面”,而是“做体验 + 做增长”



  • B 端产品:交互工程师 + 低代码拼装师 + 复杂表单处理专家;

  • C 端产品:与产品运营深度捆绑,懂 A/B 测试、埋点、Funnel 分析、广告投放链路;

  • 跨平台:Web + 小程序 + App(RN/Flutter/WebView)混合形态成为常态。



那些还在喊“切图仔优化 padding”的岗位确实在消失,但对“懂业务、有数据意识、能搭全链路体验”的前端需求更高。





二、别再死磕框架 API:2025 的前端核心能力长什么样?


基石能力:Web 原生三件套,得真的吃透


重点不是“会用”,而是理解底层原理:



  • JS:事件循环、原型链、Promise 执行模型、ESM 模块化;

  • 浏览器:渲染流程(DOM/CSSOM/布局/绘制/合成)、HTTP/2/3、安全防护(XSS/CSRF)。



这块扎实了,你在任何框架下都不会慌,也更能看懂“框架为什么这么设计”。





工程能力:从“会用脚手架”到“能看懂和调整工程栈”


Vite、Rspack、Turbopack 等工具让工程构建从“黑魔法”变成“可组合拼装件”。

你需要:



  • 看懂项目的构建配置(Vite/Webpack/Rspack 任意一种);

  • 理解打包拆分、动态加载、CI/CD 流程;

  • 能排查构建问题(路径解析、依赖冲突)。



如果你在团队里能主动做这些事,别人对你的“级别判断”会明显不一样。





跨端和运行时:不只会“写 Web 页”


2025 年前端视角的关键方向:



  • 小程序/多端框架(Taro、Uni-app);

  • 混合方案(RN/Flutter/WebView 通信机制);

  • 桌面端(Electron、Tauri)。


建议:



  • 至少深耕一个“跨端主战场”(如 Web + 小程序 或 Web + Flutter)。




数据和状态:从“会用 Vuex/Redux”到“能设计状态模型”


现代前端复杂度 70% 在“数据和状态管理”。

进阶点在于:



  • 设计合理的数据模型(本地 UI 状态 vs 服务端真相);

  • 学会用 Query 库、State Machine 解耦状态与视图。



当你能把“状态设计清楚”,你在复杂业务团队里会非常吃香。





性能、稳定性、可观测性:高级前端的硬指标


你需要系统性回答问题,而不是“瞎猜”:



  • 性能优化:首屏加载(资源拆分、CDN)、运行时优化(减少重排、虚拟列表);

  • 稳定性:错误采集、日志上报、灰度发布;

  • 工具:Lighthouse、Web Vitals、Session Replay。



这块做得好的人往往是技术骨干,且很难被低代码或 AI 直接替代。





AI 时代的前端:不是“写 AI”,而是“让 AI 真正跑进产品”


你需要驾驭:



  • 基础能力:调用 AI 平台 API(流式返回处理、增量渲染);

  • 产品思维:哪些场景适合 AI(智能搜索、文档问答);如何做权限控制、错误兜底。




三、路线图别再按“框架学习顺序”排了,按角色来选


初中级:从“会用”到“能独立负责一个功能”


目标:



  • 独立完成中等复杂度模块(登录、权限、表单、列表分页)。


建议路线:



  • 夯实 JS + 浏览器基础;

  • 选择 React/Vue + Next/Nuxt 做完整项目;

  • 搭建 eslint + prettier + git hooks 的开发习惯。




进阶:从“功能前端”到“工程前端 + 业务前端”


目标:



  • 优化项目、推进基础设施、给后端/产品提技术方案。


建议路线:



  • 深入构建工具(Webpack/Vite);

  • 主导一次性能优化或埋点方案;

  • 引入 AI 能力(如智能搜索、工单回复建议)。




高级/资深:从“高级前端”到“前端技术负责人”


目标:



  • 设计技术体系、推动长期价值。


建议路线:



  • 明确团队技术栈(框架、状态管理、打包策略);

  • 主导跨部门项目、建立知识分享机制;

  • 评估 AI/低代码/新框架的引入价值。




四、2025 年不要再犯的几个错误



  1. 只跟着热点学框架,不做项目和抽象



    • 选一个主战场 + 一个备胎(React+Next.js,Vue+Nuxt.js),用它们做 2~3 个完整项目。



  2. 完全忽略业务,沉迷写“优雅代码”



    • 把重构和业务迭代绑一起,而不是搞“纯技术重构”。



  3. 对 AI 持敌视和逃避态度



    • 把重复劳动交给 AI,把时间投到架构设计、业务抽象上。



  4. 把“管理”当成唯一出路



    • 做前端架构、性能优化平台、低代码平台的技术专家,薪资和自由度不输管理岗。






五、一个现实点的建议:给自己的 2025 做个“年度规划”


Q1:



  • 选定主技术栈(React+Next 或 Vue+Nuxt);

  • 做一个完整小项目(登录、权限、列表/详情、SSR、部署)。


Q2:



  • 深入工程化方向(优化打包体积、搭建监控埋点系统)。


Q3:



  • 选一个业务场景引入 AI 或配置化能力(如智能搜索、低代码表单)。


Q4:



  • 输出和沉淀(写 3~5 篇技术文章、踩坑复盘)。




最后:别问前端凉没凉,先问问自己“是不是还停在 2018 年的玩法”



  • 如果你还把“熟练掌握 Vue/React”当成简历亮点,那确实会焦虑;

  • 但如果你能说清楚:

    • 在复杂项目里主导过哪些工程优化;

    • 如何把业务抽象成可复用的组件/平台;

    • 如何在产品里融入 AI/多端/数据驱动;

      那么,在 2025 年的前端市场,你不仅不会“凉”,反而会成为别人眼中的“稀缺”。




别再死磕框架了,更新你的技术路线图,从“写页面的人”变成“打造体验和平台的人”。这才是 2025 年前端真正的进化方向。


作者:纯爱掌门人
来源:juejin.cn/post/7573694361474629659
收起阅读 »

别再吹性能优化了:你的应用卡顿,纯粹是因为产品设计烂🤷‍♂️

web
大家好! 最近面试,我发现一个很有意思的事情。几乎每个高级前端的简历上,都专门开辟了一栏,叫性能优化。 里面写满了各种高大上的名词😖: 使用Virtual List(虚拟列表)优化长列表渲染... 使用Web Worker把复杂计算移出主线程... 使用WA...
继续阅读 »

image.png


大家好!


最近面试,我发现一个很有意思的事情。几乎每个高级前端的简历上,都专门开辟了一栏,叫性能优化


里面写满了各种高大上的名词😖:



使用Virtual List(虚拟列表)优化长列表渲染...


使用Web Worker把复杂计算移出主线程...


使用WASM重写核心算法...



看着这些,我通常会问一个问题:


你为什么要渲染一个有一万条数据的列表?用户真的看得过来吗?


候选人通常会愣住,然后支支吾吾地说:“呃...这是我们产品经理要求的🤷‍♂️。”


这就是今天我想聊的话题:


在2025年的今天,前端领域90%的所谓性能瓶颈,根本不是技术问题,而是产品问题。


我们这群工程师,拿着最先进的前端技术(Vite, Rust, WASM),却在日复一日地给一坨屎💩(糟糕的产品设计)雕花。




我们正在解决错误的问题


让我们还原一个经典的性能优化现场吧👇。


场景:一个中后台的超级表格(默认大家应该比较熟悉🤔)。


产品经理说需求:这个表格要展示所有订单,大概有50列,每页要展示500条,而且要支持实时搜索,还要支持列拖拽,每个单元格里可能还有下拉菜单...


image.png


开发者的第一反应(技术视角)



  • 50列 x 500行 = 25000个DOM节点,浏览器肯定卡死。

  • 快!上虚拟滚动(Virtual Scroll)!

  • 快!上防抖(Debounce)!

  • 快!上Memoization(缓存)!


我们为了这个需求,引入了复杂的 第三方库,写了晦涩难懂的优化代码,甚至为了解决虚拟滚动带来的样式问题(比如高度坍塌、定位异常),又打了一堆补丁。


最后,页面终于不卡了。我们觉得自己很牛逼,技术很强。


但我们从来没问过那个最核心的问题:



人类的视网膜和大脑,真的能同时处理50列 x 500行的数据吗?



答案是:不能。


当屏幕上密密麻麻挤满了数据时,用户的认知负荷已经爆表了。他根本找不到他要看的东西。他需要的不是高性能的渲染,他需要的是筛选搜索


我们用顶级的技术,去实现了一个反人类的设计。 这不是优化,这是叫作恶😠。




真正的优化,是从砍需求开始


我曾经接手过一个类似的项目,页面卡顿到FPS只有10。前任开发留下了几千行用来优化渲染的复杂代码,维护起来生不如死。


我接手后,没有改一行渲染代码。


我直接去找了产品总监,把那个页面投在大屏幕上,问了他三个问题:


1.你看这一列 订单原始JSON日志,平均长度3000字符,你把它全展示在表格里,谁会看?


砍掉!改成一个查看详情的按钮,点开再加载。DOM节点减少20%。


2.这50列数据,用户高频关注的真的有这么多吗?


默认只展示核心的8列。剩下的放在自定义列里,用户想看自己勾选。DOM节点减少80%。


3.我就不知道为什么🤷‍♂️ 要一次性加载500条?用户翻到第400条的时候,他还记得第1条是什么吗?


赶紧砍掉!改成标准的分页,每页20条。DOM节点减少96%。


做完这三件事,我甚至把之前的虚拟滚动代码全删了,回退到了最朴素的<table>标签。


结果呢?



  • 页面飞一样快(因为DOM只有原来的1%)。

  • 代码极其简单(维护就更简单了🤔)。

  • 用户反而更开心了(因为界面清爽了,信息层级清晰了)。


这才是最高级的性能优化:不仅优化了机器的性能,更优化了人的体验。




技术自负的陷阱


为什么我们总是陷在技术优化的泥潭里出不来呢?😒


因为我们有技术自负


作为工程师,我们潜意识里觉得:承认这个需求做不了(或者做不好),是因为我技术不行。


产品经理要五彩斑斓的黑,我就得给他做出来!


产品经理要在这个页面跑3D地球,我就得去学Three.js!


我们试图用技术去弥补产品逻辑上的懒惰!(非常有触感😖)


因为产品经理懒得思考信息的层级 ,所以他把所有信息一股脑扔给前端,让你去搞懒加载。


技术不是万能的。


浏览器的渲染能力是有上限的,JS的主线程是单核的,移动端的电量是有限的。更重要的是,用户的注意力是极其有限的。


当你发现你需要用极其复杂的新技术才能勉强让一个页面跑起来的时候


请停下来!


stOpStopstopstOpstOp.gif


这时候,问题的根源通常不在代码里,而可能是在 PRD(需求文档) 里。




说了那么多,该怎么做呢?


下次,当你再面对一个导致卡顿的需求时,别急着打开Profiler分析性能。


请试着做以下几步:


我们真的需要在前端处理10万条数据吗?能不能在后端聚合好,只给我返回结果?


这个图表真的需要实时刷新吗?用户真的能看清1毫秒的变化吗?改成5秒刷新一次行不行?


在这个弹窗里塞个完整地图太卡了。能不能改成:点击缩略图,跳转到专门的地图页面?


你要告诉产品经理: 性能本身,也是一个产品功能。


如果为了塞下更多的功能,牺牲了流畅度这个最核心的功能,那是丢了西瓜捡芝麻。




最好的代码,是 没有代码(No Code)


同理,最好的性能优化,是没有需求


作为高级工程师,你的价值不仅仅体现在你会写Virtual List,更体现在你敢不敢在需求评审会上,拍着桌子说:


这个设计怎么这么反人类😠!我们能不能换个更好的方式?🤷‍♂️


别再给屎山💩雕花了。把那座山推了,才是真正的优化。


关于这个观点你们怎么看?


作者:ErpanOmer
来源:juejin.cn/post/7573950036897038376
收起阅读 »

别再滥用 Base64 了——Blob 才是前端减负的正确姿势

web
一、什么是 Blob? Blob(Binary Large Object,二进制大对象)是浏览器提供的一种不可变、类文件的原始数据容器。它可以存储任意类型的二进制或文本数据,例如图片、音频、PDF、甚至一段纯文本。与 File 对象相比,Blob 更底层,Fi...
继续阅读 »

一、什么是 Blob?


Blob(Binary Large Object,二进制大对象)是浏览器提供的一种不可变、类文件的原始数据容器。它可以存储任意类型的二进制或文本数据,例如图片、音频、PDF、甚至一段纯文本。与 File 对象相比,Blob 更底层,File 实际上继承自 Blob,并额外携带了 namelastModified 等元信息 。


Blob 最大的特点是纯客户端、零网络:数据一旦进入 Blob,就活在内存里,无需上传服务器即可预览、下载或进一步加工。




二、构造一个 Blob:一行代码搞定


const blob = new Blob(parts, options);

参数说明
parts数组,元素可以是 StringArrayBufferTypedArrayBlob 等。
options可选对象,常用字段:
type MIME 类型,默认 application/octet-stream
endings 是否转换换行符,几乎不用。

示例:动态生成一个 Markdown 文件并让用户下载


const content = '# Hello Blob\n> 由浏览器动态生成';
const blob = new Blob([content], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);

const a = document.createElement('a');
a.href = url;
a.download = 'hello.md';
a.click();

// 内存用完即弃
URL.revokeObjectURL(url);



三、Blob URL:给内存中的数据一个“临时地址”


1. 生成方式


const url = URL.createObjectURL(blob);
// 返回值样例
// blob:https://localhost:3000/550e8400-e29b-41d4-a716-446655440000

2. 生命周期



  • 作用域:仅在当前文档、当前会话有效;页面刷新、close()、手动调用 revokeObjectURL() 都会使其失效 。

  • 性能陷阱:不主动释放会造成内存泄漏,尤其在单页应用或大量图片预览场景 。


最佳实践封装:


function createTempURL(blob) {
const url = URL.createObjectURL(blob);
// 自动 revoke,避免忘记
requestIdleCallback(() => URL.revokeObjectURL(url));
return url;
}



四、Blob vs. Base64 vs. ArrayBuffer:如何选型?


场景推荐格式理由
图片回显、<img>/<video>Blob URL浏览器可直接解析,无需解码;内存占用低。
小图标内嵌在 CSS/JSONBase64减少一次 HTTP 请求,但体积增大约 33%。
纯计算、WebAssembly 传递ArrayBuffer可写、可索引,适合高效运算。
上传大文件、断点续传Blob.slice流式分片,配合 File.prototype.slice 做断点续传 。



五、高频实战场景


1. 本地图片/视频预览(零上传)


<input type="file" accept="image/*" id="uploader">
<img id="preview" style="max-width: 100%">

<script>
uploader.onchange = e => {
const file = e.target.files[0];
if (!file) return;
const url = URL.createObjectURL(file);
preview.src = url;
preview.onload = () => URL.revokeObjectURL(url); // 加载完即释放
};
</script>

2. 将 Canvas 绘图导出为 PNG 并下载


canvas.toBlob(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'snapshot.png';
a.click();
URL.revokeObjectURL(url);
}, 'image/png');

3. 抓取远程图片→Blob→本地预览(跨域需 CORS)


fetch('https://i.imgur.com/xxx.png', { mode: 'cors' })
.then(r => r.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
document.querySelector('img').src = url;
});

若出现图片不显示,99% 是因为服务端未返回 Access-Control-Allow-Origin 头 。




六、踩坑指南与性能锦囊


坑点解决方案
内存暴涨每次 createObjectURL 后,务必在合适的时机 revokeObjectURL
跨域失败确认服务端开启 CORS;fetch 时加 {credentials: 'include'} 如需 Cookie。
移动端大视频卡顿避免一次性读完整文件,使用 blob.slice(start, end) 分段读取。
旧浏览器兼容IE10+ 才原生支持 Blob;如需更低版本,请引入 Blob.js 兼容库。



七、延伸:Blob 与 Stream 的梦幻联动


当文件超大(GB 级)时,全部读进内存并不现实。可以借助 ReadableStream 把 Blob 转为流,实现渐进式上传:


const stream = blob.stream(); // 返回 ReadableStream
await fetch('/upload', {
method: 'POST',
body: stream,
headers: { 'Content-Type': blob.type }
});

Chrome 85+、Edge 85+、Firefox 已经支持 blob.stream(),能以流式形式边读边传,内存占用极低。




八、总结:记住“三句话”



  1. Blob = 浏览器端的二进制数据仓库,File 只是它的超集。

  2. Blob URL = 指向内存的临时指针,用完后必须手动或自动释放。

  3. 凡是“本地预览、零上传、动态生成下载”的需求,优先考虑 Blob + Blob URL 组合。


用好 Blob,既能提升用户体验(秒开预览),又能降低服务端压力(无需中转),是每一位前端工程师的必备技能。


作者:404星球的猫
来源:juejin.cn/post/7573521516324896795
收起阅读 »

亲历外企裁员:上午还在写代码,下午工位就空了

引子:不再是“主动的选择” 记得 2018 年的时候,年轻与互联网的兴起,只要简历挂在求职 APP 上,立马就会有猎头或者 HR 来联系。在那时,离职通常伴随着的是薪资增长和职级的跃升。 那时候的我们,仿佛是在草原上逐水草而居的游牧民族,哪里水草丰美就去哪里,...
继续阅读 »

jimeng-2025-12-09-9499-现代办公室内部,冷色调,蓝色和灰色为主,空荡荡的工位,几台亮着的显示器,窗外是阴..._cleanup.png


引子:不再是“主动的选择”


记得 2018 年的时候,年轻与互联网的兴起,只要简历挂在求职 APP 上,立马就会有猎头或者 HR 来联系。在那时,离职通常伴随着的是薪资增长和职级的跃升。


那时候的我们,仿佛是在草原上逐水草而居的游牧民族,哪里水草丰美就去哪里,从未想过草原也会有枯黄的一天。


然而今天,我第一次以旁观者的身份,见证了一场并非出于自愿的离别。当大环境不再安定、当时代的红利退潮,裸泳的不仅是企业,还有每一个身处其中的个体。


现场:两小时的“消失术”


早在一个月前,空气中就已经弥漫着不安的味道。或者说早在 HC(Headcount)冻结时,就已经埋下了伏笔。


11月的时候,一些原本该续签的合同被搁置,那时候我们就知道,暴风雨要来了。但我没想到,它来得如此迅猛且安静。


早上 9 点,无意间瞥见 Leader 们聚在一间会议室中开会。原以为是他们的例会,没曾想会是裁员的开始键:


Leader 谈话 -> 确认赔偿 -> 签字 -> 交还电脑 -> 离开。


这场“斩首行动”持续了不到两个小时,迅速且安静。整个过程持续到了上午 11 点,尘埃落定。整个部门合计少了四分之一的人。


外企的体面在这一刻展现得淋漓尽致,同时也冷酷得令人心惊。没有多余的废话,只有流程和结果。前一分钟还在和我说要修 CI 错误的同事,后一分钟工位就已经回来和我们告别了。


jimeng-2025-12-09-6938-咖啡厅的一角,桌上放着两杯咖啡,模糊的背景是繁华的城市CBD,透过玻璃窗看外面,..._cleanup.png


午餐:焦虑的泡沫


中午,“幸存”下来的同事不约而同地一起去吃饭。


话题不再是往日的“最近哪个 AI 技术栈很火”、“哪个 Node.js 的库可以用到项目里”,而是变成了“现在出去了能做什么”、“还会有下一波吗”。


谈到赔偿,由于不方便透露,只能算是“中规中矩”。即不会像佳能那样上新闻,也不会闹到仲裁。


在六七年前,这笔钱可能是一笔快乐的旅游基金;而在当下的环境中,它更像是一笔“过冬费”,是面对未知的漫长寒冬时,手里仅有的一点余粮。


吃完饭后,大家也是心照不宣地散着步。专门挑了太阳最大的地方走了好久,仿佛可以驱散身上的寒气。


但我们都清楚,我们这些留下来的人,也没有太多庆幸。更多的是一种“兔死狐悲”的无力感。谁也不知道,下一次名单上会不会有自己的名字。


下午:沉默的办公室


回到工位,工作还得继续。代码还在那里,Bug 还没修完,PR 还等着 Merge。


但当看到 PR 中那些点了 Approve 或者留下 Comments 的“前”同事的头像,竟有一丝不想 Merge 的冲动。


发生了这么大的事情后,办公室的氛围自然变了。往常到了下班点,大家可能还会为了赶进度再多待一会儿或者一起聊会天。但今天,一到时间,Leader 们就示意大家早点回去,不要再加班了。


这不仅是一种体恤,更像是一种无声的宣告:当“努力”已经无法对抗“趋势”时,大家默契地选择了“节能模式”。


jimeng-2025-12-09-2436-一个人孤独地站在高楼的落地窗前,背影,俯瞰着下面繁忙但渺小的城市车流,夕阳西下或..._cleanup.png


结语


古人云:“山雨欲来风满楼”。今天的这场裁员,或许只是时代大潮中的一朵浪花。


外企曾经是我们心中的避风港,意味着高薪、体面、Work-Life Balance。但今天的一切告诉我们,在这个充满不确定性的时代,没有绝对的安全岛。


对于我们每一个技术人来说,或许是时候重新审视自己的核心竞争力了。当大潮退去,我们手里握着的,究竟是可以随时变现的技能,还是一张随时可能失效的工牌?


最后的最后,我想说我们组本来人也不多,大家关系也都很好,平时说说笑笑也很开心。衷心祝愿他们能在后面的日子顺利。


作者:Konata_9
来源:juejin.cn/post/7582048028393144370
收起阅读 »

让网页在 PC 缩放时“纹丝不动”的 4 个技巧

web
记录一次把「标题、描述、背景图」全部做成“流体响应式”的踩坑与经验 背景 最近给 LUCI OS 官网做首屏改版,需求只有一句话: “PC 端浏览器随意缩放,首屏内容要像海报一样,几乎看不出形变。” 听起来简单,但「缩放不变形」+「多端自适应」本质上是...
继续阅读 »

记录一次把「标题、描述、背景图」全部做成“流体响应式”的踩坑与经验





背景


最近给 LUCI OS 官网做首屏改版,需求只有一句话:



“PC 端浏览器随意缩放,首屏内容要像海报一样,几乎看不出形变。”



听起来简单,但「缩放不变形」+「多端自适应」本质上是矛盾的。

经过 3 轮迭代,我们把问题拆成了 4 个小目标,并给出了最简洁的解法。




1. 文本:用 clamp() 一把梭


传统写法给 3~4 个断点写死字号,窗口稍微拉一下就会跳变。

CSS 4 级函数 clamp(MIN, VAL, MAX) 天生就是解决“跳变”的:



  • 标题:text-[clamp(28px,6vw,48px)]

  • 描述:text-[clamp(14px,1.2vw,18px)]


一行代码实现「最小值保底、最大值封顶、中间平滑变化」。

浏览器缩放时,字号随 vw 线性变化,肉眼几乎察觉不到阶梯感。




2. 容器:限宽 + 居中 = “锁死”水平形变


再漂亮的字号,如果容器宽度跟着窗口无限拉伸,一样会崩。

做法简单粗暴:


css


复制


max-w-6xl mx-auto


  • max-w-6xl 把最大内容宽度锁死在 1152px;

  • mx-auto 保证左右留白始终对称。


窗口继续拉大,两侧只是等比留空,内容区不再变形。




3. 图片(或背景):固定尺寸 + 背景定位


背景图不能跟着 100% 拉伸,否则人物/产品会被拉长。

我们把背景拆成两层:



  • 外层:全屏 div,只做黑色渐变遮罩;

  • 内层:真正的背景图用


    css


    复制


    background: url(...) 50% / cover no-repeat;
    max-width: 1280px;
    max-height: 800px;

    只要窗口没超过 1280×800,背景图始终保持原始比例,居中裁剪。





4. 布局:断点内“锁死”,断点外才变化


Tailwind 的 md:flex-row 之类前缀只在跨断点时生效。

同一断点内 我们故意:



  • 用固定 gap-32px 而非百分比;

  • 用固定图片宽 md:w-75md:h-47

  • items-center 保证垂直居中。


=> 浏览器宽一点点、窄一点点,所有尺寸都不变,自然看不出变化。

直到窗口拉到下一个断点阈值,布局一次切换,干净利落。




最终代码(最简可读版)


tsx


复制


<section className="relative flex items-center justify-center min-h-[400px] md:h-[800px]">
{/* 1. 背景层:固定尺寸 + 居中 */}
<div
className="absolute inset-0 mx-auto"
style={{
maxWidth: 1280,
maxHeight: 800,
background:
'linear-gradient(180deg,rgba(2,2,2,0) 60%,#020202 99%), url(/unlocking_vast_data_potential.png) 50%/cover no-repeat',
}}
/>

{/* 2. 内容层:限宽 + 居中 + clamp */}
<div className="relative z-10 w-full max-w-6xl px-4 text-center">
<h1 className="font-bold text-white text-[clamp(28px,6vw,48px)]">
Unlocking Vast Data Potential
</h1>
<p className="mt-4 mx-auto max-w-5xl text-[clamp(14px,1.2vw,18px)] text-[#8C8B95]">
LUCI OS is powered by Mavi's video understanding engine
</p>
</div>
</section>



效果



  • 1440px 与 1920px 两档分辨率下,标题、描述、背景图的视觉差异 < 2%

  • 字号、行宽、图片比例在鼠标拖拽窗口时线性变化,无跳变

  • 移动端仍保持完美自适应,无需额外代码。




写在最后


把「响应式」做细,核心就是 “在需要的范围内平滑,在不需要的范围内锁死”。

希望这 4 个小技巧也能帮你把“缩放不变形”真正落地。


作者:CrabXin
来源:juejin.cn/post/7540939051195056143
收起阅读 »

如果产品经理突然要你做一个像抖音一样流畅的H5

web
从前端到爆点!抖音级 H5 如何炼成? 在万物互联的时代,H5 页面已成为产品推广的利器。当产品经理丢给你一个“像抖音一样流畅的 H5”任务时,是挑战还是机遇?别慌,今天就带你走进抖音 H5 的前端魔法世界。 一、先看清本质:抖音 H5 为何丝滑? 抖音 H5...
继续阅读 »

从前端到爆点!抖音级 H5 如何炼成?


在万物互联的时代,H5 页面已成为产品推广的利器。当产品经理丢给你一个“像抖音一样流畅的 H5”任务时,是挑战还是机遇?别慌,今天就带你走进抖音 H5 的前端魔法世界。


一、先看清本质:抖音 H5 为何丝滑?


抖音 H5 之所以让人欲罢不能,核心在于两点:极低的卡顿率和极致的交互反馈。前者靠性能优化,后者靠精心设计的交互逻辑。比如,你刷视频时的流畅下拉、点赞时的爱心飞舞,背后都藏着前端开发的“小心机”。


二、性能优化:让页面飞起来


(一)懒加载与预加载协同作战


懒加载是 H5 性能优化的经典招式,只在用户即将看到某个元素时才加载它。但光靠懒加载还不够,聪明的抖音 H5 还会预加载下一个可能进入视野的元素。以下是一个基于 IntersectionObserver 的懒加载示例:


document.addEventListener('DOMContentLoaded', () => {
const lazyImages = [].slice.call(document.querySelectorAll('img.lazy'));
if ('IntersectionObserver' in window) {
let lazyImageObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
let lazyImage = entry.target;
lazyImage.src = lazyImage.dataset.src;
lazyImageObserver.unobserve(lazyImage);
}
});
});
lazy Images.forEach((lazyImage) => {
lazyImageObserver.observe(lazyImage);
});
}
});

(二)图片压缩技术大显神威


图片是 H5 的“体重”大户。抖音 H5 常用 WebP 格式,它在保证画质的同时,能将图片体积压缩到 JPEG 的一半。你可以用以下代码轻松实现图片格式转换:


function compressImage(inputImage, quality) {
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = inputImage.naturalWidth;
canvas.height = inputImage.naturalHeight;
ctx.drawImage(inputImage, 0, 0, canvas.width, canvas.height);
const compressedImage = new Image();
compressedImage.src = canvas.toDataURL('image/webp', quality);
compressedImage.onload = () => {
resolve(compressedImage);
};
});
}

三、交互设计:让用户欲罢不能


(一)微动画营造沉浸感


在点赞、评论等关键操作上,抖音 H5 会加入精巧的微动画。比如点赞时的爱心从手指位置飞出,这其实是一个 CSS 动画加 JavaScript 事件监听的组合拳。以下是一个简易版的点赞动画代码:


@keyframes flyHeart {
0% {
transform: scale(0) translateY(0);
opacity: 0;
}
50% {
transform: scale(1.5) translateY(-10px);
opacity: 1;
}
100% {
transform: scale(1) translateY(-20px);
opacity: 0;
}
}
.heart {
position: fixed;
width: 30px;
height: 30px;
background-image: url('../assets/heart.png');
background-size: contain;
background-repeat: no-repeat;
animation: flyHeart 1s ease-out;
}

document.querySelector('.like-btn').addEventListener('click', function(e) {
const heart = document.createElement('div');
heart.className = 'heart';
heart.style.left = e.clientX + 'px';
heart.style.top = e.clientY + 'px';
document.body.appendChild(heart);
setTimeout(() => {
heart.remove();
}, 1000);
});

(二)触摸事件优化


在移动设备上,触摸事件的响应速度直接影响用户体验。抖音 H5 通过精准控制触摸事件的捕获和冒泡阶段,减少了延迟。以下是一个优化触摸事件的示例:


const touchStartHandler = (e) => {
e.preventDefault(); // 防止页面滚动干扰
// 处理触摸开始逻辑
};

const touchMoveHandler = (e) => {
// 处理触摸移动逻辑
};

const touchEndHandler = (e) => {
// 处理触摸结束逻辑
};

const element = document.querySelector('.scrollable-container');
element.addEventListener('touchstart', touchStartHandler, { passive: false });
element.addEventListener('touchmove', touchMoveHandler, { passive: false });
element.addEventListener('touchend', touchEndHandler);

四、音频处理:让声音为 H5 增色


抖音 H5 的音频体验也很讲究。它会根据用户的操作实时调整音量,甚至在不同视频切换时平滑过渡音频。以下是一个简单的声音控制示例:


const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const audioElement = document.querySelector('audio');
const audioSource = audioContext.createMediaElementSource(audioElement);
const gainNode = audioContext.createGain();
audioSource.connect(gainNode);
gainNode.connect(audioContext.destination);

// 调节音量
function setVolume(level) {
gainNode.gain.value = level;
}

// 音频淡入效果
function fadeInAudio() {
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(1, audioContext.currentTime + 1);
}

// 音频淡出效果
function fadeOutAudio() {
gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 1);
}

五、跨浏览器兼容:让 H5 无处不在


抖音 H5 能在各种浏览器上保持一致的体验,这离不开前端开发者的兼容性优化。常用的手段包括使用 Autoprefixer 自动生成浏览器前缀、为老浏览器提供 Polyfill 等。以下是一个为 CSS 动画添加前缀的示例:


const autoprefixer = require('autoprefixer');
const postcss = require('postcss');

const css = '.example { animation: slidein 2s; } @keyframes slidein { from { transform: translateX(0); } to { transform: translateX(100px); } }';

postcss([autoprefixer]).process(css).then(result => {
console.log(result.css);
/*
输出:
.example {
animation: slidein 2s;
}
@keyframes slidein {
from {
-webkit-transform: translateX(0);
transform: translateX(0);
}
to {
-webkit-transform: translateX(100px);
transform: translateX(100px);
}
}
*/

});

打造一个像抖音一样的流畅 H5,需要前端开发者在性能优化、交互设计、音频处理和跨浏览器兼容等方面全方位发力。希望这些技术点能为你的 H5 开发之旅提供助力,让你的产品在激烈的市场竞争中脱颖而出!


作者:前端的日常
来源:juejin.cn/post/7522090635908251686
收起阅读 »

前端开发人员:以下是如何充分利用 Cursor😍😍😍

web
前言 我相信作为程序员Cuesor大家一定都很熟悉了,它是一款代码编辑工具,位于 Claude AI、o3、Gemini-3.5-Pro 和 GPT-4.1 等其他顶级 AI 模型之上,帮助我们提高工作效率以及体验。 Cursor AI 入门 导航到 curs...
继续阅读 »

前言


我相信作为程序员Cuesor大家一定都很熟悉了,它是一款代码编辑工具,位于 Claude AI、o3、Gemini-3.5-Pro 和 GPT-4.1 等其他顶级 AI 模型之上,帮助我们提高工作效率以及体验。


Cursor AI 入门


导航到 cursor.com 时,可以下载 IDE:


image.png
下载并打开代码编辑器后,你将看到以下内容:


image.png
在上面的界面中,我们观察到一些事情:



  • 代理模式 每当编码时,您都需要使用代理模式。它有助于处理端到端任务

  • Ask 如果单击代理模式下拉列表:


image.png
你会看到“提问”选项——当你想提出问题时使用此选项,就像使用 ChatGPT 一样。



  • Model selection( 模型选择 ) 
    此下拉列表显示要从中选择的最喜欢的模型列表

  • Context integration(上下文集成) “添加上下文”按钮,用于使用 @ 符号(@docs、@web 等)

  • Chat interface(聊天界面) 
    “新聊天”,用于开始新的或后续的对话。


如何利用这些功能


传统的 Web 开发项目遵循以下工作流程:


image.png
但是在进行 vibe 编码时,您的工作流程将看起来更像这样:


image.png
而cursor可以帮你完成设计这一步骤,使用 Cursor AI 生成界面 。首先创建一个新项目;我们将在本例中使用 Next.js。


image.png
使用快捷键 Command+K,这非常适合您忘记基本命令


image.png
现在我们已经安装了项目,是时候进行提示了。


构建示例项目


使用聊天进行设计提示


导航到新建聊天
选择光标代理 ,然后选择您喜欢的模型。对我来说,我更喜欢 Claude 3.5


image.png


现在你可以提示它了。这是我的提示的样子:“在我的 page.tsx 中,重新创建 Logrocket 的登录页面。”


这是我们得到的:


image.png
接受更改,并使用 npm run dev 运行应用程序
如果生成的和我们所设想的效果不太一样,我们可以通过多种方式使用图像上下文使其更接近。


Attaching an image  附加图像


导航到聊天,单击图像图标,然后附加图像。这应该是您想要复制的设计图像。来不断迭代来达到我们想要的效果。


在迭代时,可能会有时ai误解了我们的意思,或者Cursor 发现这个问题有点难以修复。在这种情况下,我们可以通过单击恢复检查点轻松恢复到以前的设计。这可以在之前的聊天下找到:


image.png


模型上下文协议 (MCP) 服务器


MCP 是一种开放标准,使开发人员能够在其数据源和 AI 驱动的工具之间构建安全的双向连接。


Framelink.ai 为 Figma 构建了一个 MCP 服务器,它允许您直接访问和处理 Cursor 中的设计文件。按照本指南轻松设置。


充分利用 Cursor 的更多策略


利用 AI 代理模式


使用光标时,您最常使用 AI 代理模式。这是一个强大的功能,可以:



  • 自动安装依赖项(不再需要手动包管理)

  • 在整个项目中创建和修改文件

  • 在确认后运行终端命令

  • 处理复杂的多文件重构

  • 端到端实现完整功能


使用 @ 符号进行上下文管理


了解上下文管理可以提升您使用 Cursor 的编码体验。 @ 符号是您在帮助您时告诉 Cursor 要查看什么或优先考虑什么的方式。主要的  @  类型是:



  • @code – 您的整个项目

  • @web – 搜索互联网

  • @docs – 该工具或库的文档甚至可以是一个框架

  • @files 和文件夹 – 特定文件

  • 图像 – 拖放屏幕截图/设计

  • @cursor 规则 – 甚至您过去的聊天记录


“新聊天”策略


根据经验,我建议您始终为新功能或项目开始新的聊天。当您在旧对话之上提示某些内容时,可能会分散 AI 的注意力并降低响应质量。


Cursor 添加了一项有用的功能,允许您使用旧聊天的摘要开始新的聊天,为您提供两全其美的体验 - 新鲜的上下文而不会丢失重要信息。


使用上面的  “添加 ”按钮创建新聊天,单击上下文选项的  @ ,滚动以选择  “最近的更改”,  单击它,然后继续提示:


image.png


总结


Cursor AI 的定价似乎偏高。但其功能确实还行,不过我们也可以选择trae来替代,毕竟Cursor也不比trae强多少


作者:狂炫一碗大米饭
来源:juejin.cn/post/7545725771315478554
收起阅读 »

脱裤子放屁 - 你们讨厌这样的页面吗?

web
前言 平时在逛掘金和少数派等网站的时候,经常有跳转外链的场景,此时基本都会被中转到一个官方提供的提示页面。 掘金: 知乎: 少数派: 这种官方脱裤子放屁的行为实在令人恼火。是、是、是、我当然知道这么做有很多冠冕堂皇的理由,比如: 防止钓鱼攻击 增强用户...
继续阅读 »

前言


平时在逛掘金和少数派等网站的时候,经常有跳转外链的场景,此时基本都会被中转到一个官方提供的提示页面。


掘金:


site-juejin.png


知乎:


site-zhihu.png


少数派:


site-sspai.png


这种官方脱裤子放屁的行为实在令人恼火。是、是、是、我当然知道这么做有很多冠冕堂皇的理由,比如:



  • 防止钓鱼攻击

  • 增强用户意识

  • 品牌保护

  • 遵守法律法规

  • 控制流量去向


(以上5点是 AI 告诉我的理由)


但是作为混迹多年的互联网用户,什么链接可以点,什么最好不要点(悄悄的点) 我还是具备判断能力的。


互联网的本质就是自由穿梭,一个 A 标签就可以让你在整个互联网翱翔,现在你每次起飞的时候都被摁住强迫你阅读一次免责声明,多少是有点恼火的。


解决方案


这些中转站的实现逻辑基本都是将目标地址挂在中转地址的target 参数后面,在中转站做免责声明,然后点击继续跳转才跳到目标网站。


掘金:


https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.apple.com%2Fcn%2Fdesign%2Fhuman-interface-guidelines%2Fapp-icons%23macOS/


少数派:
https://sspai.com/link?target=https%3A%2F%2Fgeoguess.games%2F


知乎:
https://link.zhihu.com/?target=https%3A//asciidoctor.org/


所以我们就可以写一个浏览器插件,在这些网站中,找出命中外链的 A 标签,替换掉它的 href 属性(只保留 target 后面的真实目标地址)。


核心函数:



function findByTarget() {
if (!hostnames.includes(location.hostname)) return;
const linkKeyword = "?target=";
const aLinks = document.querySelectorAll(
`a[href*="${linkKeyword}"]:not([data-redirect-skipper])`
);
if (!aLinks) return;
aLinks.forEach((a) => {
const href = a.href;
const targetIndex = href.indexOf(linkKeyword);
if (targetIndex !== -1) {
const newHref = href.substring(targetIndex + linkKeyword.length);
a.href = decodeURIComponent(newHref);
a.setAttribute("data-redirect-skipper", "true");
}
});
}

为此我创建了一个项目仓库 redirect-skipper ,并且将该浏览器插件发布在谷歌商店了 安装地址


安装并启用这个浏览器插件之后,在这些网站中点击外链就不会看到中转页面了,而是直接跳转到目标网站。


因为我目前明确需要修改的就是这几个网站,如果大家愿意使用这个插件,且有其他网站需要添加到替换列表的,可以给 redirect-skipper 仓库 提PR。


如果需要添加的网站的转换规则是和 findByTarget 一致的,那么仅需更新 sites.json 文件即可。


如果需要添加的网站的转换规则是独立的,那么需要更新插件代码,合并之后,由我向谷歌商店发起更新。


为了后期可以灵活更新配置(谷歌商店审核太慢了),我默认将插件应用于所有网站,然后在代码里通过 hostname 来判断是否真的需要执行。


{
"$schema": "https://json.schemastore.org/chrome-manifest.json",
"name": "redirect-skipper",
"manifest_version": 3,
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["./scripts/redirect-skipper.js"],
"run_at": "document_end"
}
],
}

在当前仓库里维护一份 sites.json 的配置表,格式如下:


{
"description": "远程配置可以开启 Redirect-Skipper 插件的网站 (因为谷歌商店审核太慢了,否则无需通过远程配置,增加复杂性)",
"sites": [
{
"hostname": "juejin.cn",
"title": "掘金"
},
{
"hostname": "sspai.com",
"title": "少数派"
},
{
"hostname": "www.zhihu.com",
"title": "知乎"
}
]
}

这样插件在拉取到这份数据的时候,就可以根据这边描述的网站配置,决定是否执行具体代码。


插件完整代码:


function replaceALinks() {
findByTarget();
}

function observerDocument() {
const mb = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === "childList") {
if (mutation.addedNodes.length) {
replaceALinks();
}
}
}
});
mb.observe(document, { childList: true, subtree: true });
}

// 监听路由等事件
["hashchange", "popstate", "load"].forEach((event) => {
window.addEventListener(event, async () => {
replaceALinks();
if (event === "load") {
observerDocument();
await updateHostnames();
replaceALinks(); // 更新完数据后再执行一次
}
});
});

let hostnames = ["juejin.cn", "sspai.com", "www.zhihu.com"];

function updateHostnames() {
return fetch(
"https://raw.githubusercontent.com/dogodo-cc/redirect-skipper/master/sites.json"
)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok");
})
.then((data) => {
// 如果拉到了远程数据,就用远程的
hostnames = data.sites.map((site) => {
return site.hostname;
});
})
.catch((error) => {
console.error(error);
});
}

// 符合 '?target=' 格式的链接
// https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.apple.com%2Fcn%2Fdesign%2Fhuman-interface-guidelines%2Fapp-icons%23macOS/
// https://sspai.com/link?target=https%3A%2F%2Fgeoguess.games%2F
// https://link.zhihu.com/?target=https%3A//asciidoctor.org/

function findByTarget() {
if (!hostnames.includes(location.hostname)) return;
const linkKeyword = "?target=";
const aLinks = document.querySelectorAll(
`a[href*="${linkKeyword}"]:not([data-redirect-skipper])`
);
if (!aLinks) return;
aLinks.forEach((a) => {
const href = a.href;
const targetIndex = href.indexOf(linkKeyword);
if (targetIndex !== -1) {
const newHref = href.substring(targetIndex + linkKeyword.length);
a.href = decodeURIComponent(newHref);
a.setAttribute("data-redirect-skipper", "true");
}
});
}


更详细的流程可以查看 redirect-skipper 仓库地址


夹带私货



标题历史



  • 浏览器插件之《跳过第三方链接的提示中转页》


作者:甜甜的泥土
来源:juejin.cn/post/7495977411273490447
收起阅读 »