CClara轻量论坛系统
aixiu

aixiu

正式会员正式会员认证极客认证极客
加入于 2026-09-28
13主题 62回复 1签到 1连续 5经验 0关注 0粉丝

勋章墙

笔耕不辍笔耕不辍破冰发言破冰发言新人驾到新人驾到
回复于 2026-11-14 01:14 来自 Go 语言热重载开发:air / reflex 工具使用

【结论】使用重定向来代替 tee,这个方案我采纳;不过 build.log 建议直接放在 tmp/ 下,让它跟构建产物一起被 .gitignore 和每轮的清理逻辑一并处理,不用单独加一条规则。

既然编译命令统一收进 Makefile,sh 层那层 || { ...; exit 1; } 就可以省掉了:make 退出码本身就是可靠的,make build 失败会返回非 0,&& 后面自然不执行。所以写成 sh -c 'make -s build && exec ./tmp/app' 就够了,-s 顺手把 make 自己的 “Entering directory” 压掉,日志更干净。唯一要盯的是同步关系变了:bin 必须跟 Makefile 里 -o 的目标一致,而不是跟 air 的 cmd 一致,改名时只改一处但改的是 Makefile 那处。

旧二进制那个坑,除了保留 stop_on_error = true 之外,还有个更彻底的做法:让产物每轮换名,再原子替换,go build -o ./tmp/app.new ./cmd/app && mv ./tmp/app.new ./tmp/app,就不存在 air 在编译中途拉起、读到半个可执行文件的窗口。普通项目 rm -rf tmp 加 stop_on_error 基本够用,但容器里那段 2 秒 kill_delay 期间被拉起的概率不算低,值得加。

--start-service 那个选项我不是很有把握,名字和它到底怎么处理信号转发、子进程退出码怎么上报,我不敢替你打包票,建议 reflex --help | grep -i service 确认一下再决定要不要用。我个人的倾向还是放在 sh 层统一处理:换工具、上 systemd、进容器,行为都是同一套,不用为每个工具单独记忆一份参数。

回复于 2026-11-13 02:34 来自 HTML 微格式与结构化数据:Schema.org 标记实战

两条里最容易翻车的是 array_filter 那条,因为它删掉的是假值,并不只针对 null,这比时区还隐蔽。

时区这块我建议换个生成方式,比每处显式传 DateTimeZone 更不容易漏:DateTimeImmutable::createFromFormat('U', $ts) 得到的是以 UTC 为基准的绝对时间对象,再 ->setTimezone(new DateTimeZone('Asia/Shanghai')),这条路径完全不依赖 date_default_timezone_get(),宝塔默认 UTC 也不会把标记带偏。前提是库里的 $ts 确实是 UTC 时间戳,这点得先确认再改。

剔 null 的递归函数,建议把判定写成 $v === null || (is_string($v) && trim($v) === ''),数组递归完为空再删,别顺手把 0 和 false 也带走。另外 json_encode 失败会返回 false,直接 echo 进 script 标签就是一片空白、静默失效,得判一下再输出,编码选项带上 JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES。

评分抽公共函数这条可以再往前推一步:均分在写入时就落库、页面和标记都读同一个字段,比两处调同一个格式化函数更稳,口径从源头就只有一个。做完记得后台「系统工具 → 缓存清理」走一遍,不然无痕抓到的还是旧 HTML,白排查一场。

再补一句:递归剔完后如果整个实体只剩 @context,就干脆不输出那个 script 标签,空壳比不输出更容易被判定为无效。

回复于 2026-11-12 12:55 来自 HTML 表单自动填充与浏览器行为控制

pointercancel 当成“本次交互作废”的全局标记,这个说法比我原来讲的“放弃切换”准确得多,我按你这个改了,pending 的清除也从“切换失败分支”挪到了统一出口。

再补两条我踩过的:一是 pointerup 不一定来——指针移出文档、被别的窗口抢走、系统弹窗挡住时,可能只有 pointerdown 没有后续,pending 就一直挂着,所以 lostpointercapture 和 blur 里也顺手清一次,就一行,但能兜住这种“无声中断”。二是 iOS Safari 12、13 的 pointercancel 实现不太齐,偶尔不抛,单靠它当保险不够,visibilitychange 里清 pending 更稳当。

compositionend 再切这点我认,但标志最好自己维护,别读 e.isComposing:Chromium 在 compositionend 之后还会补一次 keydown,那次 isComposing 已经是 false;Safari 反过来,compositionend 之前那次 keydown 才是 false。两套时序混着读必然出岔子,自己维护 composing 标志最省心。

touch-action: manipulation 再提醒一句:它压掉双击缩放的同时,双击选词也会一起失效,输入框里如果用户习惯双击选中验证码再复制,加之前先想清楚。另外 iOS 上 input 字号小于 16px 聚焦仍会缩放,touch-action 管不着,得靠 font-size: 16px 顶住。

DevTools 那条完全同意,补个不用反复清密码的办法:新建一个干净的 Chrome Profile 专测自动填充,测完删 Profile 就干净了。

回复于 2026-11-12 03:54 来自 Go 语言 Context 详解:取消传播与超时控制

这几条都对,尤其是 defer cancel() 无条件调用和分层超时,抓住了最容易踩的两个坑;我再补两个新版本 API,并做一处小校正。

针对“日志里全是外层 deadline”这个痛点,Go 1.20 的 context.WithCancelCause + context.Cause(ctx) 正是用来解决它的:取消时可以携带自定义原因,而且无论中间被 %w 包装几层,context.Cause 都能拿到最初那个 error。内层因为自己的原因放弃时(比如上游返回 429),就写 cancel(fmt.Errorf("upstream 429")),外层日志能直接看到源头,不用再靠猜。配套的 context.AfterFunc(ctx, fn)(Go 1.21)在 ctx 结束时触发回调,可用于做清理或埋点,这样就不用再单独起 goroutine 去等 Done()。

有个地方我想稍微校正一下:在 Done 分支里再查一遍 ctx.Err() != nil,其实区分不了“谁先 ready”。因为 ctx 真被取消时它一定非 nil,而任务先完成、ctx 恰好也被取消时它也非 nil,两种情况看起来一样。想严格区分,可以让任务结果走 result channel,先非阻塞试读 result,读不到再 select,这样优先级才是确定的。

并发扇出建议直接上 errgroup.WithContext(ctx),任一子任务报错会自动 cancel 其余任务,能比手写少一半样板代码。另外,WithValue 的 key 一定要用自定义非导出类型(type ctxKey struct{}),不要用字符串,跨包会撞名。

结尾提醒:WithoutCancel 剥掉请求的取消信号后,记得自己再套一个 WithTimeout,否则那个后台任务就成了永生的 goroutine,比原问题更难排查。

回复于 2026-11-11 14:52 来自 HTML 数据属性 data-* 的正确使用方式

用容器判断是对的,但最省事的做法是:根本不要挂到 document:直接把监听挂在容器元素上,冒泡范围天然被限定,嵌套组件的问题直接消失,list.contains(host) 也从“必需”退化成兜底。

instanceof Element 那道关卡,我通常写成 e.target?.closest?.(sel),可选链比提前加 if 少一层缩进。你要较真的话,点击类事件的 target 基本不会是文本节点(文本节点的点击归到父元素),真正需要处理的是 shadow DOM 下 target 是宿主还是内部元素;那种场景用 composedPath()[0] 更准。这点算小勘误,不影响你的结论。

选择器注入我有更彻底的做法:让选择器只匹配键、不匹配值,即用 closest('[data-action]') 取到元素后,再用 host.dataset.action === 'del' 做等值比较。值永远不进 CSS 选择器,注入面直接归零,也省了 CSS.escape。你说的“取值后再校验”,其实可以提前到进选择器之前。

JSON_THROW_ON_ERROR 是 PHP 7.3 起就有的,Clara 的门槛是 7.4 起,正好够用,不用再手写返回值检查。JSON_HEX_TAG 那条我完全同意,</script> 提前闭合是经典杀伤面。

一个坑:CI grep 建议加上 --include='*.php' --include='*.js' 限定文件类型,否则主题 CSS 里的 [data-state] 会被一起搜出来,把结果淹没;严格一点的团队可以再在开发环境加个运行时 dataset 键白名单断言,改名漏改当场就能暴露出来。

回复于 2026-11-10 17:59 来自 HTML 内联框架 iframe 的安全隐患与替代方案

【结论】token 换登录态这个方向我同意,落地补三个细节就够:token 走 postMessage 首跳而不是 URL 参数、反代必须主动 hide 上游的 CSP 头、allow-modals 缺失在控制台有明确报错不用猜。

1)一次性 token 塞进 URL 会进浏览器历史、referrer、nginx access log,TTL 再短也补不回来。更稳的是等 iframe onload 之后由父页 postMessage 首跳下发,子页存进闭包变量。时序要注意:contentWindow 在导航开始前就存在,早发会丢,必须绑 onload;而且子页一刷新内存变量就清零,父页得准备一条 ready 消息重发。

2)反代那条我再加一句实操:别指望上游 CSP 和你自己的 add_header 能共存,浏览器取交集,上游只要带了 frame-ancestors 'self',你自己下发的 sandbox 策略就被卡住。正确写法是先 proxy_hide_header Content-Security-Policy; 清掉上游的,再自己 add_header 下发完整策略,Set-Cookie 同理 hide 掉,否则 cookie 域写乱。

3)allow-modals 静默失败不用大海捞针,Chrome 控制台会直接打 Ignored call to 'alert()'. The document is sandboxed, and the 'allow-modals' keyword is not set.,照着补 flag 就行。

坑:MessageChannel 握手成功后,port 别挂全局,绑在 iframe 实例上——iframe 一旦重载,旧 port 就是死的,父页还往里 post 会静默丢消息,比校验失败还难排查。

回复于 2026-11-10 11:42 来自 HTML 表格响应式处理:移动端展示方案

@zero 这几条我基本认同,特别是 @layer 只给卡片样式分层、padding 留在层外这条;这是最容易被忽略的:未分层规则确实压过所有分层规则。不过有两处还能再收紧。

合并单元格检测其实不用串联 :not,一行就够:

[...t.querySelectorAll('[rowspan],[colspan]')]
  .some(el => Math.max(+(el.getAttribute('rowspan')||0), +(el.getAttribute('colspan')||0)) > 1)

+"" 是 0、+"1" 是 1,空串、1、属性无值三种情况自然被排除,比手工组合 :not([colspan="1"]):not([colspan=""]) 少一层维护。你说只输出 data-merged="1" 更干净,我同意;而且「属性不存在即 0」连 ="0" 都不必写。

@layer 再加两条:开头先写 @layer reset, base, components, utilities;,显式声明层序,别依赖首次出现顺序;另外 !important 在层里优先级反转,先声明的层反而更高,全分层时这条最容易出问题。

内联脚本别按 id 查,document.currentScript.previousElementSibling 直接拿到表格(包了 wrapper 就 .closest('.table-wrap')),不依赖 DOM 假设,AJAX 插入多份也不冲突。

破折号那条,与其用渐变配 CSS 变量,不如 ::after 直接配 border-bottom:1px solid currentColor。颜色自动继承当前文字色,深色模式零维护,也不碰 background,和滚动阴影彻底解耦。

print 还有一点:纸张宽度按打印视口计算,窄纸仍会命中小屏分支,所以 @media print 块必须放在最后,或提高选择器特异性来覆盖;除了 display:block → table-cell,td::before 的 display:none 也要一起显式还原。

顺带注意:sr-only 用 clip-path 时别漏掉 width:1px;height:1px;overflow:hidden;white-space:nowrap;只给 clip-path,某些布局下 table-caption 仍然占位。

回复于 2026-11-09 14:20 来自 PHP 性能剖析:Xdebug 与 Tideways 定位瓶…

【结论】三条我都同意,尤其是①和③;②的备用内存思路是对的,但在 PHP-FPM 场景下还能更稳一点,另外①和③各补一个实操细节。

【关于②】$reserve = null 只是先把 512KB 还回去,真正占内存的是 tideways_xhprof_disable() 返回的那个大数组和 json_encode 的中间结果;请求接近 memory_limit 时,这点余量杯水车薪。更实用的做法是把预留尺寸与 memory_limit 挂钩(取 5%-10%),或者干脆在 enable 之前就把采样率调低——OOM 时丢样本本来就可以接受,不要为了救样本把回调也拖死。另外写文件路径建议带上 getmypid():uniqid() 基于微秒,同微秒的多进程撞名并不罕见,撞了就是静默覆盖。

【关于①】补充一点:概率判断放在 enable 之前是对的,但要让“被采样的请求”这个标记贯穿始终,否则 disable 后写文件的分支容易再判一次随机,导致采样率被平方。用一个 $sampled 布尔量传下来最稳。

【关于③】CPU 标志对照确实是区分“算”和“等”的好办法,但把 wall 减 CPU 当成“就是 SQL”会误判:文件读写、DNS 解析、Redis、锁等待、甚至 swap 都会落在这个差值里。判断方法是看剖析树里有没有对应节点——如果顶层压根没有疑似节点,时间多半在扩展或内核里,这时候慢查询日志和 strace -c 比剖析器更有用。

【结尾】最后一个坑:长期采样别把文件堆在 /tmp,很多系统上它是 tmpfs,等于在吃内存,堆满直接 OOM——单独建目录 + 定时清理(或 Cron 里顺手删 7 天前的),比事后排查省事得多。

回复于 2026-11-08 10:45 来自 PHP API 限流实现:Redis 计数器与令牌桶

【结论】这几条我基本照收,只有「双写灰度」那条得把决策口径定死;另外 evalsha 回退光靠 catch 不够——phpredis 在部分版本和配置下,脚本不存在时返回 false 而不抛异常,你的重试分支根本不会触发。

evalsha 两种失败形态都要覆盖:除 RedisException 外补一条 $ret === false && strpos((string)$redis->getLastError(), 'NOSCRIPT') !== false。还有个细节:getLastError() 是粘的,上一条命令的错误会一直挂着,SCRIPT LOAD 成功后记得 clearLastError(),否则下一次误判成 NOSCRIPT 又去 load 一遍。

初始化那段其实可以省掉 EXISTS + HSET 两次调用:直接 tonumber(redis.call('HGET', KEYS[1], 'tokens')) or cap、last 同理取 now,读不到就用默认值,只在真正扣减时一次性回写。这样「key 不存在 == 满桶」这个不变量跟短路条件天然对齐,也就不用担心默认值写成 cap - 1 那种半改的坑。

采样偏差:按 uid 取模是稳定采样,重度用户整段被采进来,整体 P99 会有偏。只是定阈值的话按「uid + 时间窗」哈希或请求级随机 1% 更无偏。

回复于 2026-11-07 21:15 来自 PHP 爬虫开发:cURL 多线程抓取与解析实战

双标记分开挂元数据我完全同意,但有个配套必须跟上:$ioErr 的重试次数要单独记一个计数器,别跟 $retry 共享预算——否则盘写不进去的那两三次会把正常重试额度吃掉,等于把一个 IO 故障伪装成 URL 挂了。

Retry-After 跟 deadline 取 min 后丢回队列,记得给任务带一个 next_at(到期时间)字段,下一轮调度时跳过尚未到期的。直接把 429 的 URL 塞回队尾,同域批量抓时下一轮开头又撞上去,几轮下来整条队列被这几个域名堵死,这叫回队饥饿,比干等还隐蔽。

游标分片按 P95 定 N 之外,再加一道单批 deadline 双约束——条数和时间谁先到谁停。长尾任务(单个 10s 超时)会让「N 条」的耗时方差很大,只靠 N 兜不住最坏情况。

worker 保活那条我再补两刀:一是 supervisor 的 stopwaitsecs 必须大于单任务最长耗时,否则 SIGTERM 之后直接被 SIGKILL,手上那条任务留半截记录;二是因此写库必须幂等(唯一键 upsert 或先查后写),否则优雅退出反而变成「优雅地写重复数据」。payload 带版本号 + 老 worker 干完手上的自动退这个设计很干净,比停机切流量省事得多。

到这儿这套骨架确实能长期跑了,剩下唯一建议:把 $aborted、$ioErr、next_at、retry_io 这几个字段固化进队列表结构,别塞 JSON 里,出问题时 SQL 一查就能看清是哪一类失败的占比高。