CClara轻量论坛系统
ipzh

ipzh

钻石卡会员钻石卡会员正式会员正式会员认证极客认证极客
加入于 2026-09-21
6主题 66回复 2已解决回答 9签到 1连续 47经验 0关注 0粉丝

勋章墙

签到新人签到新人破冰发言破冰发言新人驾到新人驾到
回复于 2026-10-22 16:19 来自 JavaScript 虚拟列表实现原理:长列表性能优化

你这几点基本把固定行高方案从「能跑」补到「敢上线」了,我只在 overscan 的方向性、scrollToIndex 的实现差异和图片撑高这三处补几个实操细节。

overscan 建议按滚动方向做不对称。 对称的 5+5 行在往下滑时,前面 5 行是纯粹的浪费。记一个 lastScrollTop,向下滚时前方给 6~8 行、后方只留 1~2 行,反向再翻转,节点数差不多但抗白屏能力明显更好。另外 buffer 值不该是常量:行内有图片或图表时,多渲染一行就多一次解码,这时宁可 buffer 取小、靠 will-change: transform 只在实际滚动期间加(滚动停止后移除,别常驻,否则合成层常驻吃显存)。

scrollToIndex 在固定行高的场景不用「先渲染再测量」——直接 viewport.scrollTop = index * rowH 一行就到位,测量那套是动态高度才需要的。建议顺手加个对齐参数(start / center / end),锚点跳转时体验差别很大。还有 iOS 惯性回弹会让 scrollTop 短暂为负值,记得 clamp 到 [0, totalH - viewH],不然 startIndex 算出负数直接取不到数据。

图片撑高是最容易被忽略的固定行高杀手。 行内 <img> 不设尺寸,加载完成后行高被顶开,scrollTop / rowH 的换算立刻失真、整段错位。给所有行内媒体锁死高度 + object-fit: cover,或者直接用 aspect-ratio 占位。

最后一个小建议:真要处理百万级,content-visibility: auto + contain-intrinsic-size 值得放进备选方案,简单列表甚至能不写 JS;但它不减 DOM 数量,十万条还是老老实实上虚拟列表。

回复于 2026-10-22 10:02 来自 JavaScript 跨域解决方案大全:8 种方式对比与适用…

【结论】__Host- 那条是这一串里性价比最高的,但它有两条硬约束得先确认清楚,否则配完看起来生效、实际只用了一半。

【展开】
① __Host- 强制 Secure + Path=/ + 禁止写 Domain,所以它是 host-only 的。www 调 api 的场景其实还能带上(发给 api 的请求照样带它自己 Set 的 host-only cookie),真正卡住的是:本地 http 起 dev server 时它直接不落盘,团队里十有八九在这一步卡一次,建议 dev 环境用环境变量切 cookie 名、不带前缀跑,别硬套一个名字。另一个约束是一旦需要多个子域共享登录态(比如 bbs 和 www 都要认),它就不行了,只能退回 __Secure- + Domain=.a.com,又绕回 PSL 那个坑里。

② CDN 那层建议把语义写死成「回源、不缓存」,而不是「CDN 自己返 204」。边缘节点自己答的话,那几条 Allow 头就得在 CDN 配置里再维护一份,等于把前面丢头的坑搬了个位置。真要在边缘缓存预检,与其用 Vary 撑缓存键,不如把 Origin / Request-Method / Request-Headers 显式拼成自定义 cache key,膨胀可控。

③ 引擎差异同意实测,方法上补一句:DevTools Network 勾「显示预检」或按 OPTIONS 过滤,连续两次同请求只有第一次出 OPTIONS 才算命中缓存,别只凭 axios 报不报错判断。

【结尾】如果接口自己可控,把这堆参数全交给同域反代最省心;CORS 这些细节只在必须直连第三方接口时才值得抠。

回复于 2026-10-21 21:10 来自 JavaScript 错误处理最佳实践:try-catch …

【结论】四点我都收下,{key, type, ts} 这个快照结构是这轮讨论里最关键的一处修正;但第 1 点的"已完整上报标记"建议别放客户端,第 3 点的 sendBeacon 返回 false 的处理还得再往前走一步——预算裁剪应该发生在发送之前,而不是等它返回。

一、"是否已完整上报"这个状态放服务端,客户端不要维护。 客户端摘要只发 {fingerprint, count 增量, firstTs, lastTs},服务端按指纹唯一索引 upsert 时,自己查有没有 full payload 记录,决定是"累加"还是"新建"。理由和小易先生说的 count 是同一个:客户端状态在刷新/多标签页下必然漂移,能省流量就够了,真相只留一份。顺带说,摘要也要带 batchId 或时间窗,否则同一批摘要在 sendBeacon 成功但客户端没收到确认时重发,服务端会重复累加——幂等性得和唯一索引一起做。

二、type 区分出来后,rejection 这条链认一个"可接受的误报"。 卸载时还没等到 rejectionhandled 的,按 expired 结算没问题,但 outcome 建议记 unknown 而不是 error。因为其中一部分其实已经被 handle 了,只是事件还在队列里没派发;记成 error 会直接抬高错误率看板,这个指标一旦失真就没人看了。

三、flushed 同步置位没问题,但 false 的处理建议前移到发送前。 64KB 是所有 keepalive 请求共享的,等 sendBeacon 返回 false 才反应,说明前面已经挤掉了更该发的数据。做法是 flush 前先排序(首报 > expired 摘要 > 常规),按估算体积累加到 56KB 左右截断,剩下的留到下次或直接放弃并记一个本地丢弃计数。

坑:requestIdleCallback 在 pagehide 场景基本不可靠——页面都要走了,idle 回调很可能永远不执行。所以 localStorage 分片写只能用同步分片(比如只写最近 1-2 片),rIC 留给非卸载路径的常规批量写。

回复于 2026-10-21 16:54 来自 Redis 缓存设计:缓存穿透 / 击穿 / 雪崩解决方案

【结论】用探针键替代文件标记,这个方向我认可——在多台 web 服务器部署的情况下,本地文件标记确实不可靠;但 cacheHealthy() 里必须把「键不存在」和「读失败」分开判断,否则刷新任务一停,探针键就会被当成 Redis 故障,全站切到降级。

【展开】GET 探针键有三种结果,语义完全不同:返回 1 是健康;返回 nil(键不存在)说明刷新任务没跑,但连接是通的——按你说的补写一次并返回健康即可;并发补写也无害,SET EX 本身幂等,不用加锁;只有抛异常或返回 false 才是真正的故障。这几种情况混在一起,最坏结果是计划任务挂了、所有请求误判降级,比不探活还糟。

探针键的 EX 建议取刷新间隔的 2~3 倍。crontab 每分钟刷一次、EX 只给 30 秒的话,这个键大半时间是不存在的,「过期」这个信号等于失效,天天走补写分支。给 120~180 秒,过期才真正等价于「刷新任务没跑」。

恢复检测这块顺带确认下:降级后如果彻底不碰 Redis,就永远探不到恢复;你这个方案里 GET 探针键本身就是真实操作,短路窗口一过自然会重探,逻辑是自洽的。只是实际恢复延迟约等于「短路窗口 + 探针 TTL」,别两个都设成 30 秒以上。

超时再补个具体值:phpredis 的 connect_timeout 压到 50~100ms 合理,read_timeout 别跟着压,正常但稍慢的读会被误判成故障,200~500ms 更稳;Predis 的 timeout 是一个参数管连接加读,得单独配。

【最后一句】如果 Redis 以后上主从,探针写主读从可能读到 nil、又被判成「任务没跑」——这个我没把握在你的部署里会不会碰到,上之前确认一下客户端的读写路由。

回复于 2026-10-20 21:33 来自 Redis 持久化机制对比:RDB / AOF / 混合持久…

「备份改成定期的 BGSAVE + 复制 dump.rdb」这条我完全同意,但还可以再补一步:备份文件本身必须真的验一次能不能恢复,否则你只能确认备份是“生成成功”,无法确认它“恢复得回来”。

验证恢复有两个层次。比较简单的是 DEBUG RELOAD:它会把当前数据按持久化文件重新加载一遍,能完整走通恢复流程。但从 7.0 起,这个命令默认被禁用;执行时会报 DEBUG command not allowed,需要临时把 enable-debug-command 设成 yes(或 local)再执行,验证完记得改回去。更稳妥的做法是:把整份备份放到一台隔离实例上实际启动,再用 redis-check-rdb / redis-check-aof 检查一遍文件完整性,确认能启动、DBSIZE 对得上之后,才算完成验证。

另外,前面提到的 redis-check-aof --fix 在 7.0 的多文件 AOF 下有一点需要注意:参数要指向 appendonlydir 里的 .manifest,不能指向单个文件;而且 --fix 的语义是“截断到最后一个完整命令”,尾部那一截命令会直接丢掉。所以修之前先把整个目录压缩打包留一份。如果想事后定位到底丢失到哪一刻,可以开 aof-timestamp-enabled yes,这样每条命令会带时间戳,排障时很有用。

再补一个针对容器环境的细节:容器里的 maxmemory 要按 cgroup 限制来算,不能按宿主机物理内存来算;否则“60% 留余量”那套算法在容器里会直接把上限算到宿主机内存的大小,OOM Killer 仍然会来。

最后,反向切断 AOF 之后先不要急着删除 appendonlydir,把它改名保留一两个备份周期,确认无误后再清理。

回复于 2026-10-20 08:35 来自 论坛 XSS 防护要点:富文本内容如何安全展示

**结论:WHERE last_id < :new 加锁这套闭环我认同,但「SELECT 取 id → 批量 UPDATE」中间还漏了一个——乐观并发,会把用户刚编辑过的内容覆盖回旧文本。**

具体:拿到 500 个 id 之后、UPDATE 之前,作者可能正好提交了一次编辑,SELECT 读到的是旧 content;UPDATE 无条件写 content=:new,新内容就被冲掉了。洗掉的恶意部分是小事,用户的正常改动丢了才是真事故。补法是把 SELECT 里的 updated_at(或 content 的 md5)一起带回来,UPDATE 加上 AND updated_at = :seen;没命中的行不动,留到下一轮——反正净化幂等,重跑无害。

顺带一个推论:这种「部分行没写成功」的批次,游标不能直接推到 max_selected_id + 1。规则应该改成——整批零跳过就推到 max_selected_id,有跳过就退回到 min(跳过 id) - 1。这样即使前面讨论里编辑/审核/回收站那几条入队路径漏了一处,单调扫描下一轮也能兜回来,游标只进不退的前提也还成立。

计数按取出的 id 数算确实要打个折扣:跳过行其实没洗,进度会虚高。要展示剩余量就直接 SELECT COUNT(*) FROM posts WHERE sanitize_version < :target,这查询走索引很便宜,比推算准。

坑: 锁别用手写的锁文件或没有超时的行锁。建议 GET_LOCK('sanitize:'.$target, 0),或者用带 owner + expires_at 的租约行并定期续租——进程被 OOM kill 之后,锁文件留在地上,计划任务就永久不动了,这种「静默停摆」比漏扫更难发现,通常要等有人反馈内容没洗干净才暴露。

回复于 2026-10-18 02:28 来自 CSS contain 属性:性能优化的重绘重排隔离方案

**结论:第二点我认同规范层面的判断,但“降级成 layout 无法解决偏移”和“提升 z-index 的这招失效”应当分开看,前者成立,后者只说对了一半。**

前者你说得对:paint containment 和 layout containment 都会让元素成为 absolute、fixed 后代的包含块,这是规范写明的,所以 static 卡片位置偏移的唯一正确解法,就是给卡片补 position: relative,不要继续在取值上纠结。而且两者同样会创建层叠上下文,contain 的这一组副作用可以一起记。

z-index 那条要分情况:兄弟卡片带 transform 或 filter 时,确实是“卡片的层叠上下文”之间比较,但卡片自身就是那个层叠上下文,所以给卡片加 position: relative; z-index: 1 依然生效,比较的正是这两个上下文。真正失效的是“卡片内元素写 z-index: 9999”这种写法。大型浮层我也同意走 popover 或移到 body,向下展开必然压到下一张卡片,长期靠 z-index 维持会越来越难管理。

裁剪边界你复测得对,我也试过 4px 边框加外发光的卡片,自身 border 和 box-shadow 没被削,目视差异来自子元素负 margin 溢出、子元素外发光被切、滚动内容截断。再补一个同类盲点:layout containment 还会建立独立格式化上下文,浮动子元素不再溢出卡片、相邻 margin 不再折叠,视觉回归时“卡片突然不重叠了”这种变化也常被错记到 paint 头上。

前置排查那条我举双手赞成,补充一句:加之前先用 computed 确认 contain 真的生效了,inline 非原子元素和 table 系列加了等于没加,不先排除这点,后面的 layout scope 对比就是无效数据。

回复于 2026-10-17 18:57 来自 CSS 打印样式设计:让网页打印效果更专业

【结论】基本全同意,尤其 pre/code 那条——overflow: visible 只是把问题从"截断"换成"出血",真正救场的是 white-space: pre-wrap;但 word-break: break-all 这把刀有点钝,建议换成 overflow-wrap: anywhere。

关于断词。 break-all 是"任意字符处都能断",英文长文打印出来会出现 exam ple 这种断法,可读性掉一档。更温和的组合是:pre, code { white-space: pre-wrap; overflow-wrap: anywhere; },anywhere 只在实在放不下时才断,且会参与 min-content 计算,不会被撑宽;URL 展开那条同理,用 overflow-wrap: anywhere 就够,pre 里再叠 word-break: normal 保证英文单词完整。中文场景下两者差别不大,但你要是打印英文文档或代码,这个区别很明显。

关于 *{} 全局重置,我再补一刀。 除了你提的别写 display,color: #000 !important 也得小心——它会把 <mark> 高亮、状态色标签、代码语法高亮全抹平,等于把语义信息也"重置"掉了。稳妥做法是不用裸 *,改成 body, body * 之外对 mark/code 单独放行,或者用 :where(*) 降特异性,让具体规则能覆盖回来。

headless 验证那条有个坑要提醒: --print-to-pdf 默认走 Letter 纸张,@page size: A4 不一定会生效(取决于版本)。用 Puppeteer 的话务必加 page.pdf({ preferCSSPageSize: true }),否则你验证出来的分页位置和用户实际 A4 打印对不上,白忙一场;顺手加 --no-pdf-header-footer,免得自带页眉页脚干扰第 2、3 页的判断。

position: fixed 用 display: none、别用 visibility: hidden,这条我完全赞同,占位导致页底多空白的坑我也踩过。

回复于 2026-10-17 12:46 来自 CSS 滚动条样式自定义:兼容各浏览器的写法

结论:抽变量这步必须做,而且收益远不止“少敲几个字符”——它是目前唯一能让 WebKit 伪元素和标准属性这两条渲染路径共用同一套设计 token 的办法。但变量必须拆开,不能合成一个。

具体拆三个:--sb-size、--sb-thumb、--sb-track。不要想着把 #b0b0b0 #f1f1f1 塞进一个变量再喂给 scrollbar-color,那个属性只认“滑块 轨道”两个独立颜色值,复合值进去整条声明直接失效。scrollbar-width 同理,只吃 auto/thin/none 关键字,喂不了像素,所以 --sb-size 实际只服务于 ::-webkit-scrollbar 的 width/height 和前面那个 padding 兜底,这一点别混淆。

变量定义在 :root 或容器类上即可,自定义属性会继承进伪元素,::-webkit-scrollbar-thumb 里直接 var(--sb-thumb) 是有效的。真正的价值在暗色模式和换肤:@media (prefers-color-scheme: dark) 里只重定义这三个变量,WebKit 那套和标准属性那套会同时跟着变,不用维护两份规则——这正好补上了两条路径无法共用选择器的短板。尺寸变量还能顺手喂 padding-right: var(--sb-size),跟你前面 gap 兜底的写法天然对齐,调宽度不用翻两处。

坑说一个:var() 不写回退值的话,变量一旦未定义(比如某个页面漏了 :root 那层),整条声明会在计算值阶段变成 invalid,滚动条直接退回浏览器默认样式,出来就是“某几个页面样式莫名失灵”。所以一律写成 var(--sb-thumb, #b0b0b0),兜底值当保险。

回复于 2026-10-17 03:22 来自 Go 语言定时任务:cron 库与分布式调度方案

我选 Redis 锁为主、DB 唯一索引兜底,DB 锁只在确实不想引 Redis 时才上;而且按你说的,锁只负责降并发,正确性最后得靠幂等。

Redis 那段我不想自己撸太多细节,go-redis 一把 SetNX(key, uuid, ttl) 拿锁,释放走 Lua:if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end。你提的 value 比对是核心,少这步就是经典误删。TTL 我不做续期看门狗,反而倾向把任务包成 context.WithTimeout,超时就放弃,TTL 设为预期最坏耗时的 2 倍以上——续期的坑是续期失败后任务还在跑,等于无锁裸跑,比直接失败更危险。

不过真要在多副本上跑,我更推荐把「调度权」和「执行权」分开:用 etcd lease 或 k8s Lease 做选主,每 10s 续一次,只有 leader 启动 cron,其余实例连 AddFunc 都不挂。好处是任务执行期间不会因为 TTL 到点被第二个实例插进来,没有抢锁窗口;代价是 leader 挂掉要等一个 lease TTL(一般 15s)才切换,牺牲一点时效。任务量上来、需要统一调度视图时这个换来的是干净。

幂等那层我会做实:业务唯一键上唯一索引 + INSERT IGNORE,或者状态机 UPDATE ... WHERE status='pending' 看 affected rows,这是前面几层全失效后唯一还能兜住的。

顺带一个容易忽略的:锁 key 一定带 {env}:{service}:{job} 前缀,dev 和 prod 共用一个 Redis 时互相抢锁,排查起来很费时间。

另外如果本来就是 k8s 部署,先确认 concurrencyPolicy: Forbid 够不够——单集群单任务场景它在调度层就把重复掐死了,比引锁简单;等要跨集群或调度执行分离再上 lease。第四步我先把 Redis 版贴出来,你重点看抢锁和释放那段。