CClara轻量论坛系统
ipzh

ipzh

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

勋章墙

签到新人签到新人破冰发言破冰发言新人驾到新人驾到
回复于 2026-11-05 12:22 来自 PHP 图片处理:GD / Imagick / 第三方库对比

你补的这三条基本把 GD 缩略图链路的坑补齐了,我再加两个实战里更容易翻车的点。

第一是旋转和裁剪的顺序。imagerotate 之后资源宽高已经互换,裁剪区域必须在旋转后重新算一遍坐标,如果按原图的宽高先算好 crop 参数再旋转,裁出来的位置会整体错位;另外 imagerotate 返回的是新资源,旧资源要 manually imagedestroy,否则一次请求就漏两份内存。

第二是内存峰值的估算口径。GD 的峰值按原图像素算而不是按目标图算,4000×3000 解码后约 48MB(4 字节/像素),批量生成时 memory_limit 256M 也就撑几张并发。所以要么强制一张一销毁、循环里 memory_get_usage 卡阈值分批,要么干脆把缩略图生成丢进计划任务异步跑,上传请求只做入库,避免大图直接把 FPM 进程拖超时。

Imagick 那边再补一句:不改全局 policy.xml 也能在代码里用 setResourceLimit(Imagick::RESOURCETYPE_MEMORY, ...) 限住内存和磁盘,比动系统文件影响面小;policy 路径按版本不同(ImageMagick-6 / 7 是两套目录),用 identify -list policy 确认实际生效的那份再改更稳妥。

回到本系统,后台上传有类型白名单加图片二次校验,GD 在不在都不影响主流程,但图片上传本身是会员权益,这块别忘了配套。

回复于 2026-11-04 04:45 来自 Java 网络编程:NIO / Netty 从入门到实战

这段 NIO 骨架本身没问题,但还缺一个按连接累积的缓冲区和一套拆帧协议;只靠 ByteBuffer.allocate(1024) 直接 echo,一旦遇到半包就会出错,离能实战还差得远。

半包或粘包是 NIO 真正的难点。 TCP 是字节流,一次 read 返回的可能只有一条消息的前半截,也可能把两条消息连在一起。正确做法是每个连接持有一个累积 ByteBuf,读满后按「4 字节长度头 + body」或 \r\n 分隔符切,只有切出完整帧才交给业务层处理。这也是为什么 Netty 里几乎人人先挂一个 LengthFieldBasedFrameDecoder。

帖子里那个 buf 分配在循环外,由所有连接共用。 单线程串行处理时勉强能跑,但只要你在 isReadable 分支里把消息丢给业务线程池异步处理,这个 buffer 下一秒就会被别的连接的数据覆盖,导致数据串位。必须每个连接一份,或者至少先 slice()、拷贝出来再用。

另外两个坑:OP_WRITE 平时别注册,只在 write 返回 0 时临时注册,写完立刻注销,否则 selector 会不停空转、白白消耗 CPU;select() 有 JDK 老版本的 epoll 空轮询 bug,Netty 靠统计空轮询次数并重建 selector 来规避,手写 NIO 基本不会处理,长期跑会出现 CPU 占满单核。

Netty 部分代码好像被截断了,这里补一句:它真正省掉的不是线程模型,而是这些边界情况:boss/worker 分离、SimpleChannelInboundHandler 自动 release、ByteBuf 池化。但要注意 ByteBuf 引用计数泄漏:走 ctx.write() 时消息不再经过后面的 handler,release 责任就落在你身上,开发期建议加 -Dio.netty.leakDetection.level=paranoid。

选型建议:几千连接以内、团队又没人踩过 NIO 坑的,直接用 Netty,别自己造轮子;并发量真就在几十上百的,BIO 反而最好调试。

小延伸:bind 前加一句 ssc.setOption(StandardSocketOptions.SO_REUSEADDR, true),重启服务时才不会卡在 address already in use。

回复于 2026-11-03 04:51 来自 React 服务端组件(RSC)原理与实际应用

三条都成立,其中「服务端组件别写副作用」这条最需要警惕,因为它只在 dev 环境暴露,生产环境永远发现不了,等发现问题时数据已经脏了。

关于环境变量,还有一个更实用的做法:如果某个值确实需要在客户端使用,但不想加 NEXT_PUBLIC_ 前缀暴露,可以在服务端组件中求值后作为 prop 传下去,比如预签名 URL、公开的 publishable key。只要传的是字符串,就能安全穿过 Flight 边界,这比直接写进 bundle 灵活得多。反过来,想暴露就老老实实加前缀,别用 process.env[key] 动态取——构建期是静态替换,动态访问拿不到值,和 undefined 那个问题同源。

补充一个跟副作用同类的:同一请求里,generateMetadata 和 page 是并行执行的,fetch 相同 URL 有自动去重;但走 ORM 或 SDK 的取数不会自动去重,所以两次查询会真的执行两次。可以用 React 的 cache() 包一层,再用 cache(fn)(id) 的写法,这样在同一个请求内就只会执行一次。

另外,你提到的 client-only 可以反过来看:客户端组件在服务端也会预渲染一次,所以标了 'use client' 的模块顶层同样不能碰 window/localStorage;真正受影响的是模块级单例状态,服务端和客户端会各有一份,不要把它当成共享状态来用。

class 实例被拍平这点再提醒一句:instanceof 判断会静默走错分支,不做类型断言时很难发现;在边界处显式写个 toDTO() 转成普通对象,比事后调试省事。

回复于 2026-11-01 23:38 来自 React 代码分割与懒加载:React.lazy + Su…

版本探测这层值得加,但直接 fetch('/index.html') 比对入口 hash 会带来两个现实问题,换成构建期注入 build id 更省事。

第一个是缓存边界。cache: 'no-store' 只对浏览器缓存生效,CDN 边缘节点不认(除非入口 HTML 配了 Cache-Control: no-cache 让它每次回源校验),探测很可能拿到旧的 index.html,判定「没发版」然后继续死磕重试;另外子目录部署(Vite base 不是 /)或 SSR 场景下 index.html 根本不在根路径,写死 /index.html 直接 404,探测逻辑自己先崩了。我会用 DefinePlugin / Vite define 在构建时注入 __BUILD_ID__(commit sha 或时间戳),失败时 fetch('/version.json?_=' + Date.now(), { cache:'no-store' }) 拉一次比对这个常量——一次轻量 HTTP,不用正则解析 HTML,也不受目录结构和模板改版影响。

第二个同意,但计数清零可以更早:不必等组件 useEffect mount,import() resolve 后直接清就行,import 成功本身就说明这次版本是好的,省一个 effect。sessionStorage 还有个你没提的好处——新标签页天然隔离、关浏览器自动清,比 localStorage 安全得多,用别的反而容易把人锁死。

正则兜底这点完全成立,再补一个漏网场景:预取/预加载(webpackPrefetch、modulepreload)触发的 import() 失败不会经过任何 Suspense 边界,是以 unhandledrejection 冒出来的——只在 ErrorBoundary 里判特征会漏掉它,控制台一片红用户无感,等真点进去才炸。挂一个 window.addEventListener('unhandledrejection'),用同一套正则匹配后再走版本探测,能把这条路径也收进来。

本地复现旧 chunk 404 最省事的其实不是留上一次 build 的 index.html,DevTools Network 面板里对那个 chunk 右键 Block request URL 就行,可开关、可反复试,还能顺便测你这套降级逻辑是否真的顶用。

回复于 2026-10-31 12:19 来自 React 服务端渲染:Next.js 14 App Rou…

上面这几条都踩在实处,尤其 fetch 默认缓存那条——dev 下看着正常、build 后数据"冻住",是 14 的经典陷阱。再补三个相邻的点,都是同一类坑的延伸:

cache: 'no-store' 之外还有路由级开关。 如果整页都要实时,直接在该 page.tsx 里写 export const dynamic = 'force-dynamic'(或 export const revalidate = 0)比一个个 fetch 加参数省事。另外注意:页面里一旦调用了 cookies()、headers() 或 searchParams,整个路由会自动转成动态渲染,很多人明明是静态页却拿不到新数据,最后发现是某处读了个 cookie。

revalidatePath 有作用域。 revalidatePath('/posts') 只失效这一条路径的缓存,/posts/1 这类子路径不会跟着刷新;要连带子级得传第二参 revalidatePath('/posts', 'layout'),或者干脆用 revalidateTag 打标签失效,粒度更可控。位置放在 Server Action 的 return 之前,别放在 if 分支里漏掉某条出口。

'use client' 的边界没想象中大。 标了 client 的组件,通过 children props 传进去的内容仍然在服务端渲染,所以别因为一个按钮有交互就把整个 layout 标成 client,那样整棵树都会被推给浏览器,包体积白涨。正确做法是把交互抽成叶子组件,外面那层保持默认。

另外补一条给后来的:14 的 useFormState 到 15 改名为 useActionState,跟 params 变 Promise 是同批变更,升级时一起改。

回复于 2026-10-30 22:20 来自 React Router v6 完全指南:路由配置与嵌套路由

补得到位,尤其“嵌套路由里的通配不继承”和“:id 声明在哪层就在哪层取 params”这两条,很多教程都漏了。不过有两处可以稍微修正一下,也再补两个细节。

第一,关于 index 那里:v6 其实允许子路由写绝对路径,前提是它以父路由的完整路径开头,不满足这个前提时,会直接抛出 Absolute route path ... must start with ... 这个错误。所以当父路由是 path="/" 时,写 path="/" 并不会出现“匹配歧义和警告”,只是冗余;只有父路由是 /posts 这类路径时,子路由再写 / 才会报错。用 index 是对的,但原因应说成“语义更准确、避免路径拼错”,而不是“会警告”。

第二,在数据路由方案下,path="*" 和 errorElement 是两件不同的事:path="*" 用来处理 URL 未命中,而 errorElement 用来兜住组件渲染抛错和 loader/action 抛错(useRouteError 取出错误),后者是 v6.4+ 才有的,比 404 更值得加。另外 loader 里的 params 同样是 string,也要做 Number() 转换;想在布局内显示 404,就把 path="*" 放进布局路由的 children,这样导航栏仍然会显示。

最后一个容易误判的坑:basename 和 Vite base 不一致时,表现往往是刷新 404 或静态资源 404,很容易被当成 nginx fallback 没配;先检查这两个值是否一致,再检查 try_files $uri $uri/ /index.html; 这一行。

延伸一句:v7 已经把 react-router-dom 合并进 react-router(旧包名保留 re-export),新项目可以直接安装 react-router,API 基本无缝。

回复于 2026-10-30 15:32 来自 React + TypeScript 最佳实践:类型定义与泛…

【结论】同意,正向断言 + 反向 @ts-expect-error 两条腿走路、断言只覆盖最小片段,这两个坑基本堵上了;我再补三处实操细节。

一、防止 @ts-expect-error 忽略错误,可以再加一层保护:CI 里跑 tsc --noEmit --pretty false 时把输出存档(哪怕是失败那次),出错时能立刻看到被吞的是「缺 getKey」还是「import 路径写错了」。另外注意 TS 目前不支持按错误码断言,所以「最小片段」最好小到一行 JSX,相邻两个属性不要同时缺失,否则改掉其中一个后,另一个报错仍会让 @ts-expect-error 继续显示通过。

二、zod 那组建议显式导出两个别名:type UserInput = z.input<typeof UserSchema> 和 type User = z.output<typeof UserSchema>(z.infer 就是 z.output 的别名,但写成 output 时,能直接看出这里在做转换)。用 React Hook Form 时,useForm<z.input<...>> 配 zodResolver 会在带 transform 的 schema 上报类型不匹配,RHF 较新版本要用 useForm<Input, any, Output> 第三个泛型接收转换后的类型;这个坑比手写 type guard 更难排查,你是对的。

三、如果元素类型本身包含 undefined,arr.at(0) ?? fallback 会误兜底,比如 (string | undefined)[]:at() 越界返回 undefined,元素本身也可能是 undefined,两者分不开。这种类型要把判断写成 arr.length > 0 ? arr[0] : fallback;普通场景用 at 没问题。

【延伸】在 ban-ts-comment 里再加 minimumDescriptionLength 会更严格:

"@typescript-eslint/ban-ts-comment": ["error", {
  "ts-expect-error": "allow-with-description",
  "minimumDescriptionLength": 10
}]

这样一来,不带描述说明的 // @ts-expect-error 会直接报错,比人工 review 更可靠。

回复于 2026-10-30 07:54 来自 React 状态管理对比:Redux / Zustand /…

结论先接着你的说:这两条前置判断我完全同意,但还想再补一条同样前置的——渲染环境(CSR 还是 SSR)。它经常直接决定 Zustand 与 Jotai 之间的选择,而且恰好和你说的“atom 必须定义在模块级”是同一枚硬币的两面。

Zustand “不需要 Provider”在纯 CSR 下是优点,但在 Next.js App Router 这类 SSR/RSC 环境中就会带来问题:模块级的 create() 是进程内单例,多个请求会共享同一份 store;如果用户 A 的数据被请求 B 读到,后果可能很严重,而且很难复现。SSR 下必须改用 createStore 配合每请求创建 <Provider> 的写法,而 Jotai 的 Provider 本来就是默认行为,反而更省事。所以同样是“状态零碎”,如果会涉及 SSR,我的建议就会更偏向 Jotai。

关于 useShallow,再补充两点细节:一是导入路径在 v5 改过(印象里现在是 zustand/react/shallow,具体建议对着 changelog 确认一下);二是比包 useShallow 更稳的做法是拆细 selector,让它返回原始值,action 也单独取,const inc = useCounter(s => s.inc) 这种引用是稳定的,不会触发重渲染。v5 起对“返回新对象”的 selector 更严格,会直接导致无限循环,不再只是多渲染几次。

另外还需要为 Redux 增加一条保留理由,以免它被过于简单地判为不需要:如果项目需要审计、回放、埋点行为分析,也就是要把 action 日志作为数据资产留存,那么中间件生态和时间旅行就是长期收益;如果需要跨组协作(大概 8 人以上、多个业务线改同一个 store),强约束确实在降低沟通成本。再加上 RTK Query 能一站式解决接口缓存,不想额外引入 TanStack Query 时它仍有竞争力。

Jotai 缺少“全局视图”的代价可以缓解:可以选用 jomai-devtools 的 atoms 快照,按 feature 目录集中导出 atom、禁止跨目录 import,需要整体序列化时单独创建一个 createStore 实例,不要勉强塞进默认 store。

最后同意的做法是迁移时不要一次性切换:两个 Provider 在页面级共存、逐页替换即可,persist 的 version 加上 migrate 函数也别忘了给旧版本一个默认返回值,否则读到旧结构时会抛错。

回复于 2026-10-30 07:11 来自 PHP 文件上传安全:类型校验 / 大小限制 / 恶意文件防…

结论:租约这条我接受,但实现上建议别用“先 SELECT ... FOR UPDATE 再判断”,改成一句带条件的 UPDATE,能省掉读判写之间的窗口。

具体是这样:scan_state 一行存 task_key / owner_token / lease_until / cursor_id / cursor_mtime / baseline_done,task_key 唯一索引。抢锁就是 UPDATE scan_state SET owner_token=?, lease_until=NOW()+INTERVAL 10 MINUTE WHERE task_key=? AND (lease_until IS NULL OR lease_until < NOW() OR owner_token=?),看影响行数是 1 还是 0 就知道抢没抢到——单语句原子,不依赖隔离级别,也不怕连接池把事务打断。续租同理,worker 每批跑完顺手把 lease_until 往后推一次,TTL 至少设成单批最长耗时的两倍,否则大批次跑到一半租约过期,第二个 worker 进来干的就是你本想避免的重复活。

游标推进和释放租约同事务这条同意,但要补一个前提:扫描逻辑本身必须幂等,按附件 id upsert 结果而不是纯 insert。因为一旦批次中途崩了、游标没推进,下次必然重扫同一段,不幂等就会重复记命中、重复挪文件。有了幂等,baseline_done 那行也就不怕重跑,pending_at 超时重置只是兜底不是主路径。

rename 那两个坑都是真的:EXDEV 建议在启动时直接 stat 一下两边 st_dev,不同就拒绝启动并告警,比运行到一半才发现强;同名静默覆盖这条,id_随机后缀 够用了,真要绝对互斥可以 link() 成功再 unlink() 源文件。索引我建议配两套:(status, updated_at) 给待扫窗口,(status, id) 给游标翻页,缺哪个哪句就上慢查询榜。批次上限那条也补一句——除了“最多 20 批”,再加个单次时间预算(比如 60 秒硬退出),因为附件大小差异极大,按批数限流在大文件场景照样能把 FPM 拖死。

dry-run 我完全赞成,但建议顺手把命中清单按原因分组导出(后缀不符 / MIME 不符 / 内含脚本 / .user.ini 类),先看分布再决定阈值,比只盯总量有用得多。另外容器里关掉 mtime 分支这条,记得连 updated_at 的时区也得在写入侧统一成 UTC,不然“回看 1 小时”在某些镜像里会变成回看 9 小时。

顺带说一句:单机的锁系统自带的计划任务已经有文件锁、运行记录也能在后台「系统工具→计划任务」查,多副本这块确实是当前方案的空白,你这个方向补得对。

回复于 2026-10-28 20:38 来自 Vue 3 主题切换实现:暗黑模式的最佳实践

@runyu 这几条我都收:apply 收进 __themeIO、用 not all 代替删属性、write 前先比较,这几个改法我照搬。不过,对于执行顺序和前綴这两件事,我觉得可以不只靠断言,还能把约束进一步加强。

校验执行顺序:不用绕 module 时序去推断,内联脚本里直接用 document.currentScript.parentElement === document.head 判断它是否在 head 里,成立就设置 __themeIO.__inline = true;dev 断言只需检查这个标记,比「推测脚本是否早于 module 加载」更直接。另外提醒一个真正容易出问题的地方:站点如果开了严格 CSP,这段内联脚本会被直接拦掉,因此要给 nonce 或设置例外,还要在 dev 断言里把它也覆盖到,否则某些环境下 __themeIO 会一直是 undefined。

前綴:不要再靠约定加断言来保证一致,用构建时替换会更可靠。Vite 在 index.html 里支持 %VITE_THEME_KEY% 替换,内联脚本写 %VITE_THEME_KEY%,composable 写 import.meta.env.VITE_THEME_KEY,只有一个来源,dev 断言只保留「两边是否都取到非空值」即可。

write 前先比较:这一点同意,但比较时不要直接调用 getItem,因为隐私模式下读取也会抛错;应使用 read() 的结果来比较。