从 "组件外也能改 Zustand 状态" 聊到它的底层原理
这次对 Zustand 的理解升级,其实是从一个很简单的问题开始的: 为什么我可以在组件外面通过 修改状态,而且组件里的值还能自动更新? 一开始我一直以为 Zustand 是基于 React Context 实现的。既然是基于 Context,那它应该只能在组件树里用,离开组件就没法工作。但实际测试下来发现: 在组件外调用 完全没问题 组件内通过 订阅的值也会自动更新 根本不需要 Provider(在默认用法下) 这让我意识到,我对 Zustand 的理解是错的。 --- 一、Zustand 不是基于 React Context 很多人(包括我)会自然地把“全局状态管理”跟 React Context 绑定在一起。 但 Zustand 的默认实现,其实完全不依赖 React Context。 如果你这样创建一个 store: 这个 做的事情本质上是: 在内存里创建一个 store 对象 这个对象内部维护: 当前 state 一组 listeners(订阅者) getState setState subscribe
从 shadcn/ui Button 到可定制 loading 的业务按钮
在前端开发中,按钮是最常用的组件之一。shadcn/ui 提供了默认的 Button 组件,但在实际业务中,它存在一些不足: 默认按钮没有 loading 状态 无法阻止重复点击 loader 样式无法自定义,不适应不同主题 为了解决这些问题,我在 shadcn/ui Button 的基础上,封装了一个业务 Button 组件。 --- 组件设计 核心目标: 增加 loading 状态 当按钮正在处理请求时显示 loader 自动禁用按钮,防止重复提交 支持 loader 样式自定义 通过 可以修改 loader 样式,例如深色模式下反色 继承原生 Button 属性 保留所有原生属性,如 , , 安全点击逻辑 当按钮处于 loading 或 disabled 时,阻止点击事件 --- 核心代码 --- 使用示例 为 true 时显示 loader 按钮禁用,防止重复点击 loader 样式可通过 自定义 --- 总结 这个业务 Button 组件解决了 shadcn/ui 默认 Button 的不足: 增加 loading 状态 loader
Nextjs 16.1 文件系统缓存升级,冷启动速度提升明显
Nextjs 16.1 版本中 Turbopack 文件系统缓存(File System Caching)稳定并默认启用: 重启开发服务器(next dev) 时,不需要从零重新编译,而是直接复用缓存,导致启动时间大幅缩短,尤其在大项目中。 官方基准数据: react.dev 项目:冷启动 3.7s → 缓存后 380ms(约 10× 更快) 中型项目:冷启动 3.5s → 缓存后 700ms(约 5× 更快) Vercel 内部大型应用:冷启动 15s → 缓存后 1.1s(约 14× 更快)
时序攻击(Timing Attack):从一条 Twitter 推文说起
在 X(Twitter)刷到一条 推文,作者提出以下的代码存在 timing attack 漏洞 当时觉得不可思议,就和 Grok 讨论了下。下面是完整的技术梳理。漏洞机制字符串相等比较(JavaScript 的 ===,Python 的 ==,Java 的 equals 等)通常采用短路逐字节比较: 从索引 0 开始逐个比较 一旦任意位置不匹配,立即返回 false,后续字节不再检查 这直接导致执行时间泄露信息: 不匹配位置越靠前 → 比较结束越早(时间更短) 前缀匹配越长 → 需要比较的字节越多(时间更长) 以秘密值 "qwerty" 为例: "awerty" → 第 1 字节不同 → 最快返回 "qwertx" → 第 6 字节不同 → 明显更慢 "qwerty" → 完整匹配 → 最慢(全遍历) 攻击者可通过以下步骤利用: 向目标端点发送大量精心构造的探测字符串 使用高精度计时器记录每次请求的响应时间 统计分析:响应时间显著更长的前缀 → 很可能正确 逐字节固定正确前缀 + 尝试下一个字符,
lark-quote-converter:解决飞书文档复制出来的“弯引号”问题
如果你从飞书文档复制过内容到文本编辑器,有可能会遇到智能引号导致的问题。 什么是智能引号? 我们平时敲键盘输入的引号是这样的: 这叫 标准引号(ASCII 引号)。 而像下面这种: 叫 智能引号(Smart Quotes),也叫弯引号。 它们的特点是: 左右形态不同 更符合排版规范 更适合阅读 常见于书籍、新闻、正式文档 简单理解: 标准引号 = 给程序看的 智能引号 = 给人看的 --- 为什么飞书文档用智能引号? 因为飞书本质是一个文档工具,而不是代码编辑器。 在文档排版里,智能引号更美观,也更专业: “这是引用内容” 看起来会比: "这是引用内容" 更像正式出版物的排版。 所以飞书默认会把你输入的直引号自动替换成智能引号。 这在写文章时很好,但在写代码时就会出问题。 --- lark-quote-converter 为了解决这个问题,我做了一个很简单的 Chrome 插件: lark-quote-converter 它只做一件事: 把从飞书复制出来的智能引号,自动转换成标准引号。 不改别的内容,不加功能,只处理引号。
eslint-plugin-classname-arg-last:让外部 className 永远拥有最高优先级
在 React 组件封装中,有一个非常常见的模式: 是组件的对外扩展口。 它来自外部,通常代表使用者的“最终意图”。\ \ 因此在大多数设计约定中: 外部传入的 className 应该拥有最高优先级。 而在 Tailwind CSS 或 的场景下,参数越靠后,覆盖优先级越高。\ \ 这就意味着: 所以正确的组件写法应该是: 而不是: 否则组件内部样式可能反而把外部样式覆盖掉。 --- 为什么需要 ESLint 规则? 在团队协作中,这种顺序很容易被忽略: 代码不会报错,但语义已经错了。 当组件越来越多时,这种不一致会带来: 外部样式无法正确覆盖内部样式 调试样式时出现“为什么改不生效”的问题 Code Review 反复纠正参数顺序 为了解决这个问题,我写了一个 ESLint 插件: eslint-plugin-classname-arg-last --- 它做的事情非常简单 它会检查: 如果发现 不在最后一个参数位置,就报错。 错误示例: 正确示例: 其他函数不会被检查。
Tailwind CSS 动态拼接类名居然有效?记一次 Code Review 引发的思考
公司最近有个后端要来前端组,Review 了一下他的代码 width 是变量,比如 "20"、"12" 。我心想:这不应该崩吗?Tailwind 不是只认静态完整类名的吗?结果……它真的生效了。我懵了,烧烤了半天以后翻代码。翻到前面几百行,发现有人写过: 哦~ 原来是因为项目里已经静态写过 w-20 和 w-12,Tailwind JIT 扫描时把它们对应的 CSS 生成出来了。误打误撞生效了,但是这样封装是有问题的,动态属性最好写在内联样式里。
基于真实 DOM 还原 Skeleton 的工程思考
在前端开发中,Skeleton(骨架屏)是提升用户感知性能的重要手段。常见做法是单独写一套灰色占位组件,但这种方法存在一些明显的问题: 骨架屏和真实 UI 结构脱节,修改 UI 需要额外同步 skeleton; 文字行高和布局难以精准还原,容易出现错位; 页面渲染过程中容易出现布局抖动(CLS)。 本文分享我在组件库中对骨架屏的思考,以及如何通过与组件耦合的方式实现精准还原。 --- 传统 Skeleton 的问题 常见的实现方式是页面层写一套假的 Skeleton,例如: 这种方式存在几个问题: 结构脱节\ \ 骨架屏和真实组件是两个平行的版本,修改组件必须同步改 skeleton。 布局不精确\ \ 文字、行高、padding、响应式高度等都可能不一致,导致骨架屏和实际内容错位。 维护成本高\ \ 组件库或页面升级时,需要额外维护 skeleton 组件。 --- 我的方法:Skeleton 与组件结构耦合 我的核心思路是: 骨架屏应该是组件的一种状态,而不是独立组件。