注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

生活艰难,格格不入

“Life is a soup. And I’m a fork.”早高峰卡在伦敦地铁闸机口,扫到站台牌上那句 “Life is a soup. And I’m a fork.”,手里的通勤卡差点滑掉 —— 这不就是我的打工日常?翻了翻网友的翻译,每句都戳中打工...
继续阅读 »


“Life is a soup. And I’m a fork.”

早高峰卡在伦敦地铁闸机口,扫到站台牌上那句 “Life is a soup. And I’m a fork.”,手里的通勤卡差点滑掉 —— 这不就是我的打工日常?翻了翻网友的翻译,每句都戳中打工人的痛处。
“拔剑四顾心茫然” 太贴了。就像月底赶项目,看着同事们游刃有余,自己对着电脑屏幕发呆,明明周围全是 “工作汤”,我这把 “叉子” 却啥也舀不着,跟李白当年的迷茫没两样。
“世事如羹,我为箸” 更懂东方打工人。中餐里的羹和西餐的汤一样难对付,筷子夹不住羹,就像我抓不住 KPI,明明工具没毛病,可就是跟目标 “错配”,只能眼睁睁看着机会溜走。
“我的人生泡汤了,我叉” 简直是打工人口头禅。上周方案被毙,瞬间觉得 “人生泡汤”,心里忍不住吐槽 “我叉”—— 既对应了原文的 “fork”,又说出了打工人的真实情绪,这双关梗太绝。
“我要这铁棒有何用” 更是扎心。每天抱着 “努力铁棒” 加班,却连 “绩效汤” 都沾不到边,就像叉子舀不着汤,越干越迷茫,忍不住跟着哼这句歌词。
至于 “你是汤儿~我是叉~”,虽然跑偏了,但笑着笑着也懂了 —— 打工就是和生活 “格格不入”,汤和叉凑不到一起,我和轻松日子也凑不到一起,也算另一种真实。
收起阅读 »

前端实时推送(券商行业必看) & WebSocket 原理解析

web
一、历史背景 + 时间轴网页一旦需要 “实时” ,麻烦就开始了:数据在不断变化,用户却只能等下一次刷新;刷新解决不了的延迟,用短轮询凑数,又被无数空请求反噬;再加长轮询,试图把“有了新数据再说”变成一种伪推送,却仍困在请求—响应的笼子里。开...
继续阅读 »

一、历史背景 + 时间轴

网页一旦需要 “实时” ,麻烦就开始了:数据在不断变化,用户却只能等下一次刷新;

  • 刷新解决不了的延迟,用短轮询凑数,又被无数空请求反噬;
  • 再加长轮询,试图把“有了新数据再说”变成一种伪推送,却仍困在请求—响应的笼子里。
  • 开发者于是继续前探:让连接不再频繁重建,尝试分块直输,把事件像水一样持续送达,于是有了更顺滑的 Streaming 与标准化的 SSE。

直到某一刻,我们不再满足于“更聪明的单向”,而是迈向真正的“同时说话与倾听”——WebSocket 把通信从一次次请求,变成一条持久而通透的通道。此后,

  • HTTP/2、HTTP/3 与 QUIC 又在底层为效率和时延开了绿灯,甚至提供了可选可靠与无序传输的更多可能。

接下来,我们就沿着这条主线,层层展开:它们各自解决了什么、在哪些场景最合拍、又如何在你的系统里形成清晰的选型边界。

01|从整页刷新出发:减少浪费的一条链路

这一块是为了解决“整页刷新导致的高延迟与带宽浪费”,逐级细化与优化。

a. 早期网页:整页刷新

  • 背景与问题:每次更新都整页请求,体验割裂、带宽浪费、延迟高。
  • 直接影响:促使前端与服务端思考“只取变化”。

b. 短轮询(Short Polling)为解决整页刷新的低效

  • 解决:改为“隔一段时间拉一次”,显著减少整页重载带来的浪费。
  • 局限:高频请求带来大量空响应与服务器开销。
  • 承接改进:为减少空转,演进到长轮询;同时催生更流式的思路(Streaming/SSE)。

c. 长轮询(Comet/挂起请求)为减少短轮询的空转

  • 解决:请求挂起,服务器有新数据才返回,接近“伪推送”,显著降低空转。
  • 局限:本质仍是请求-响应;连接频繁重建;难做真正双向。
  • 承接改进:

    • 单向推送更稳:SSE 标准化单向事件流。
    • 若要真双向与二进制:交给 WebSocket(见独立块)。

d. HTTP Streaming(分块传输/持续输出)为进一步降低重连与延迟

  • 解决:保持连接,分块持续输出,适合连续文本/事件流,重连更少、延迟更低。
  • 局限:多为单向,受代理/中间件影响,兼容性不一。
  • 承接改进:单向事件由 SSE 标准化;双向场景仍需 WebSocket。

e. SSE(Server-Sent Events)单向推送的标准化终点

  • 解决:以标准事件流语义提供单向推送,浏览器原生支持,资源占用低。
  • 适配范围:通知、进度、日志/监控等文本或事件流。
  • 位置关系:在“只需单向推送”的场景中,SSE 是这一链条的稳定落点,而非过渡技术。

02|范式跃迁:WebSocket(独立大块)

这不是前面链条的“又一改良”,而是从请求-响应转向全双工持久连接的范式变化。

WebSocket(全双工、持久、低开销)

  • 解决:真正的双向实时通信,降低握手与头部开销,支持文本与二进制,端到端延迟低。
  • 典型场景:聊天、协同编辑、在线游戏、行情推送、IoT。
  • 与上一链条的关系:

    • 在“需要双向实时”的主战场,实质上取代了短轮询/长轮询等过渡方案。
    • 与 SSE 并存:若只有单向通知/事件流,SSE 更简单更省资源;若需要双向或二进制,WebSocket 更合适。
  • 运维关注:连接状态管理、容量与反压、企业代理/负载均衡兼容。

03|底座升级与新选项:HTTP/2·HTTP/3·QUIC 家族

这部分不是替代前两块,而是提供更高效的承载与更灵活的传输语义。

WS over H2/H3

  • 价值:与同域请求复用连接、更好穿透与效率、更低握手成本。
  • 作用:让 WebSocket 的部署与网络效率更优。

WebTransport(基于 QUIC)

  • 价值:可选可靠与有序/无序、更低延迟,适合实时媒体、游戏、定制协议。
  • 关系:不是取代 WebSocket/SSE 的通吃方案,而是当你需要“更细粒度的可靠性与顺序控制”时的新工具。

二、速查表

实时推送的目标是“低延迟、双向或单向地把数据从服务端送到客户端”。主流技术选型包括:

三、WebSocket 核心定义(重要)

WebSocket 是 HTML5 推出的一种全双工(Full-Duplex)、持久化(Persistent)的网络通信协议, 基于TCP协议构建,允许客户端(浏览器)与服务器之间建立一条长期稳定的连接通道,实现「服务器主动向客户端推送数据」和「客户端实时向服务器发送数据」的双向通信,无需频繁建立/断开连接。

其核心特点可概括为:

  • 全双工:通信双方可同时发送/接收数据(区别于HTTP的「请求-响应」单向通信);
  • 持久连接:连接建立后长期保持,避免HTTP每次通信都需重新握手的开销;
  • 轻量协议:数据帧头部信息简洁(仅2-14字节),传输效率远高于HTTP;
  • 协议标识:客户端发起连接时使用ws://(非加密)或wss://(加密,基于TLS,类似HTTPS)作为协议前缀。

从零开始的完整流程

下面是一条你在前端真实会走的链路:创建连接 → HTTP 握手与协议切换 → 进入 WebSocket 双向通信 → 启动心跳检测 → 发现异常并重连 → 重连成功后的补偿 → 服务端跨域放行 → 正常/异常关闭。


1) 创建连接(入口)

你写下第一行代码时,要解决两件事:用对协议前缀、绑定好事件。

  • 如果网页是 HTTPS 必须用 wss://HTTP 页面用 ws://
  • 立刻绑定 onopen/onmessage/onerror/onclose,以便后续重用。

示意代码(精简):

  • new WebSocket(url)
  • 绑定事件:initWebSocketEvents(ws)

2) HTTP 握手与协议切换(从“请求”到“长连”)

客户端(浏览器) 创建 WebSocket 实例时,会发起一个特殊的 HTTP - GET,核心目的是 「请求将协议从HTTP升级为WebSocket」。服务端验证通过后返回 101,双方切换到 WebSocket 帧通信。

WebSocket 帧是双向通信中的最小传输结构,携带  数据类型  是否为消息的最后一段  负载长度  掩码/密钥  实际数据  。消息可以被拆成多帧连续发送,也可以一个帧就送完。

客户端发起「协议升级请求」(HTTP - GET 请求)

请求头中关键字段(面试高频考点):

  • GET /ws-endpoint HTTP/1.1:请求方法为 GET,路径为服务器的 WebSocket 端点(如/ws);
  • Host:example.com:服务器域名;
  • Upgrade:websocket:核心字段,告知服务器「要升级协议为 WebSocket」;
  • Connection:Upgrade:配合 Upgrade,表示「这是一个协议升级请求」;
  • Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jzQ==:客户端生成的随机字符串(Base64 编码,长度 16 字节),用于服务器验证(防止恶意连接);
  • Sec-WebSocket-Version:13:WebSocket 协议版本(当前主流为 13,需服务器支持);
  • Sec-WebSocket-Origin:https://example.com:客户端所在域名(用于服务器跨域验证)。

服务器响应「协议升级成功」(HTTP - 101状态码)

服务器收到请求后,若支持 WebSocket 协议且验证通过(如 Sec-WebSocket-Key 验证、跨域验证) ,会返回HTTP - 101(SwitchingProtocols)状态码,表示「同意协议升级」。

响应头中关键字段 (面试高频考点):

  • HTTP/1.1 101 Switching Protocols:101 状态码是协议切换的标志;
  • Upgrade: websocket:确认升级为WebSocket 协议;
  • Connection:Upgrade:确认连接用于协议升级;
  • Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=:服务器对Sec-WebSocket-Key 的处理结果(核心验证逻辑):

    1. 服务器将客户端发送的 Sec-WebSocket-Key 与固定字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接;
    2. 对拼接后的字符串进行 SHA-1 哈希计算;
    3. 将哈希结果转为 Base64 编码,即为 Sec-WebSocket-Accept 的值;
    4. 客户端会验证该值是否正确,若不正确则拒绝建立连接(防止伪造响应)。

3) 进入通信阶段(双向数据 + 基础发送)

握手通过,onopen 会触发。此时做两件事:

  • 发送初始化数据(如身份、订阅主题)
  • 启动心跳(下一步会讲)

通信注意:

  • onmessage 既可能是字符串,也可能是二进制(Blob/ArrayBuffer)
  • bufferedAmount 可用来做背压控制(积压太大时暂停继续 send)

4) 启动心跳(让连接“活着”且可感知)

持久连接会遭遇 Wi‑Fi 抖动、防火墙清理等问题。心跳=周期性发 ping,超时未收到 pong 就判死链

推荐参数(可按业务调优):

  • HEARTBEAT_INTERVAL ≈ 30s
  • HEARTBEAT_TIMEOUT ≈ 10s

实操要点:

  • 启动前先清理旧定时器,避免重复
  • 收到 pong 立即清除超时定时器
  • onclose/onerror 必须停止心跳

5) 异常→重连(恢复连接但不过载)

一旦 onerroronclose(code ≠ 1000) 或心跳判定超时,进入重连。

目标是:能恢复、不过载、可被用户停止。

策略三件套:

  • 指数退避:1s → 2s → 4s … 最多 30s
  • 次数上限:如 10 次(达到即停)
  • 可控停止:提供“停止重连”或页面关闭时停

补偿机制:

  • 在断开前缓存“待发送”数据(例如未发出的聊天消息)
  • 重连成功后按序补发,确保业务连续性

6) 服务器放行跨域(握手能否过关的关键)

虽然 WebSocket 原生“支持跨域”,但握手是 HTTP,服务端需要对 Sec-WebSocket-Origin 做白名单校验。

否则会 403 或直接关闭。

  • Node.js(ws)

    • 读取 req.headers['sec-websocket-origin']
    • 不在 allowedOrigins 列表:关闭 1008 Cross-origin access denied
  • Spring Boot

    • registry.addHandler(...).setAllowedOrigins("https://a.com", "https://b.com")
    • 需要时 .withSockJS()提供降级


7) 正常关闭与资源清理(善始善终)

  • 用户离开页面或主动退出:ws.close(1000, '用户主动退出')
  • onclose 中停止心跳与重连,清空定时器与队列,避免泄漏
  • 记录关闭原因码:1000 正常、1006 常见于异常/心跳超时

四、面试题

面试关注点通常围绕“协议对比、连接管理、消息语义、可靠性与扩展性、安全与运维成本”。

1、WebSocket 与 SSE 的差异与使用场景,HTTP轮询呢?

WebSocket(全双工,二进制/文本)

  • 适用:即时聊天、协作编辑、游戏状态同步、行情推送、需要客户端→服务端主动上行的实时交互。
  • 优点:低延迟、头开销小、全双工、支持二进制、可自定义子协议。
  • 注意:需处理心跳、重连、背压、鉴权与扇出扩展;代理/LB 要正确透传 Upgrade 和超时设置。

Server-Sent Events(SSE,单向 server→client)

  • 适用:通知流、日志流、监控事件、流式生成文本(如增量输出)、只需服务端下行的实时更新。
  • 优点:浏览器原生 EventSource、文本流、自动重连、支持 Last-Event-ID 断点续传;实现简单。
  • 注意:单向、仅文本(可 base64 二进制)、连接数限制与代理超时需要关注;移动端网络切换要做容错。

HTTP 轮询/长轮询(兼容兜底)

  • 适用:对实时性要求不高的小流量场景、受网络环境或企业防火墙限制无法使用 WS/SSE 时的兜底。
  • 优点:最易落地、与缓存/鉴权/监控体系天然兼容;对中间设备最友好。
  • 注意:延迟更高、资源利用低;高频轮询会带来成本与限流压力。

✅ 重点

  • WebSocket 通过 HTTP/1.1 Upgrade → 101 完成切换,此后是帧协议的全双工通道;Keep-Alive 仅是复用 TCP,不改变 HTTP 的请求-响应语义。
  • 选型规则:需要双向实时交互选 WebSocket;单向事件流选 SSE;受限或低实时性场景用轮询作兜底。

2、如何设计一个可水平扩展的实时推送系统?

在可水平扩展的实时推送系统中,WebSocket 连接会分布在多台网关节点上。

核心挑战是如何在连接与消息不在同一台机器时,仍能把消息快速路由到正确的连接。可行的范式是

  • 网关层负责连接
  • 管道层负责路由与发布订阅
  • 存储层负责状态与回放

🔌 网关层(负责连接)

  • 终止 TLS/WS,维持心跳与速率限制,保持无状态实例。
  • 建立本地索引:connectionId → 订阅集合,userId → connectionIds。
  • 将连接元数据上报共享存储:connectionId、userId、nodeId、订阅、最近心跳。
  • 仅订阅“与自己有关的分片”:按 userId/topic 的哈希分片从管道层拉取,减少无关扇出。
  • 写通道背压与优先级:控制帧/关键消息优先,低优先级可丢尾或抽样。

🚇 管道层(负责路由与发布订阅)

  • 选型具备分片与回放能力的总线(Kafka/Pulsar/NATS/Redis Streams)。
  • 分片策略

    • 点推:按 userId/connectionId 一致性哈希到分区,保证单用户局部有序。
    • 主题推送:按 topic 分区,网关本地再做订阅过滤与扇出。
  • 路由方式

    • 生产者只需写对的分片;总线按分区把消息送到订阅该分片的网关。
    • 广播/超大房间采用“分层扇出”:先到分片,再由各网关本地扇出,必要时加中间扇出代理。
  • 去重与幂等:messageId 或 (topic, partition, offset) 作为幂等键,网关/客户端各自维护短期去重集合。

🗃️ 存储层(负责状态与回放)

  • 会话与订阅状态:使用 Redis Cluster 或 KV 服务存 userId→connectionIds、connectionId→nodeId、订阅清单、心跳时间。
  • 游标与回放:在总线层保留 offset;客户端重连携带 resumeToken,网关据此恢复订阅并按 offset 增量补发。
  • 一致性与更新:订阅变更写事件流,相关网关消费后刷新本地索引;用版本号/逻辑时钟避免乱序覆盖。

3、如何保证消息不丢、不重、按序?

  1. 不丢: 消息先落到能持久化、带副本确认的总线里(像“写盘且多副本到位才算成功”),写失败就退避重试;消费侧是“先送到用户手里或进可靠下行队列,再更新位点”,断线后拿着 resumeToken+offset 从保留的历史里把漏掉的补回来。
  2. 不重: 每条消息都有一个不会撞车的“指纹”(messageId 或 topic-partition-offset);网关用一小块内存做近端去重,只有第一次真正写入才前进位点,重复的一概忽略;客户端也按同一指纹做幂等处理,避免业务状态被二次改动。
  3. 按序: 把需要有序的对象(userId/roomId)哈希到同一分区,借用分区内天然顺序;同一个键在网关里串行发送、同队列内重试,不跨分区不并行穿插,这样即使重试和补发也不会把顺序打乱。

4、心跳如何设计?超时如何判定?

这里的心跳,目标是 “保活、探测、可平滑重连”。

  1. 不失联: 用应用层 ping/pong,客户端主发、服务端回;

    1. 间隔 20–60s,加±10%抖动
    2. 未知网络时取 20–30s,确保小于最短 NAT 空闲回收。
  1. 怎么判死: 别一跳不回就拍板。记录 lastSeen允许 2–3 次心跳未达或 now-lastSeen 超过 2–3 个周期再判断;进入“Suspect”时降级写入,仍有业务流量即立刻恢复。
  2. 断了咋办:重连走指数退避并带抖动,携带 resumeToken/offset 补发;移动端切网优先复用会话,失败再重建。监控 RTT/丢包与 Suspect 比例,自动调心跳与阈值。

5、如何在 Nginx/Envoy 反向代理后稳定运行 WebSocket?

核心思路:让代理“不瞎操心”、连接“常被看见”、后端“可续上”。

  • 代理设置:开启 WebSocket 升级;调大超时,禁用缓冲与压缩;保持 TCP keepalive,HTTP/2 用 CONNECT(H2/WebSocket)。
  • 心跳与保活:应用层 ping/pong 20–30s(±10%抖动),保证小于代理/NAT空闲回收;大连接数用轻量负载均衡(hash by userId/roomId)避免跨节点迁移。
  • 断线与重连:客户端指数退避+抖动,带会话 token/offset 续传;后端幂等去重,重放不重不丢。
  • 运维与观测:开代理层指标(升级成功率、idle 关闭数、5xx)、RTT/丢包与重连率,异常时自适应缩短心跳或放宽超时。

6、如何做鉴权与权限隔离?

握手前校验 JWT;Subprotocol 指定租户/版本;频道级 ACL;避免敏感数据从客户端请求非授权频道。

  • 握手前校验 JWT:在 HTTP Upgrade前验证 iss/aud/exp/签名并解析 tenant_id/user_id/scopes,避免建立长连后再踢。
  • Subprotocol 指定租户/版本:用 Sec-WebSocket-Protocol 携带 tenant 和策略版本做白名单匹配,确保连接上下文一致。
  • 频道级 ACL:频道强制以租户前缀命名,每次 subscribe/publish 依据 RBAC+scope 前缀(到资源或前缀)做服务端授权。
  • 避免敏感数据越权:仅按服务器维护的“已授权订阅集合”下发数据,忽略客户端自报筛选请求并拒绝未授权频道。

7、如何评估性能与成本?

  • 每连接内存占用: 用基线压测量出 MB/1k 连接的实际占用,结合目标并发外推单机上限并监控 GC/碎片。
  • 每秒消息数(fanout×频率): 用发布频率×平均扇出得到总吞吐,核算带宽与发送队列容量,识别热点频道放大效应。
  • 尾延迟 P95/P99: 持续跟踪端到端延迟长尾并关联队列深度与CPU/GC事件,确保在SLA红线下仍稳定。
  • 压测考虑广播峰值与重连风暴: 分别模拟大扇出瞬时广播与大量短时间内握手重连,验证背压、限速和握手路径的韧性。

8、遇到“重连风暴”怎么处理?

  • 抖动退避(指数退避 + 随机抖动): 客户端按指数退避间隔重试并加入随机抖动,避免同相位同时重连造成尖峰。
  • 分批恢复: 将连接恢复按固定批次/时间片发放(如每 100ms 开放 N 个),把尖峰摊平到更长窗口。
  • 服务端限流与排队: 在握手与认证路径设置并发/速率上限与队列,超限直接返回可重试错误或延迟令牌。
  • 灰度放量: 按租户/区域/版本逐步提升允许重连比例,结合健康度与错误率自动调节放量速度。

9、前端如何封装一个健壮的 WebSocket 客户端?

  • 状态机( CONNECTING / OPEN / CLOSING / CLOSED ): 用有限状态机驱动所有事件与迁移,单航道控制避免并发重连与回调竞态。
  • 心跳/重连策略: 按固定心跳探活与半开检测,重连采用指数退避叠加随机抖动并设上限与冷却期。
  • 消息序列化: 统一 envelope(type/id/ts/payload),默认 JSON,性能敏感时切 Protobuf/MessagePack 并保持向后兼容。
  • 离线缓存与去重: 未连通时将待发入队、跨刷新用 IndexedDB,按 seq/uuid 去重并用 last-seq 做断点续传。
  • 可观测日志: 记录连接尝试/关闭码/重连次数/心跳RTT/队列长度等指标与事件,便于快速定位长尾与故障。

作者:HiStewie
来源:juejin.cn/post/7572539461478907947

收起阅读 »

🚣【附源码】牺牲两天摸鱼时间,我做了款大屏

web
📝项目背景 ​ 最近时间比较闲,摸鱼的时间越来越多了,人一闲下来就会想做点什么。说干就干,立马行动。 在刷了半小时pdd之后我买了张ui图,并根据这个ui做了一个大屏。 ​ 最终效果如下: 📦项目地址 ​ 这里附上项目地址,如果你觉得不错的话,帮我点一个小小...
继续阅读 »

📝项目背景


​ 最近时间比较闲,摸鱼的时间越来越多了,人一闲下来就会想做点什么。说干就干,立马行动。


在刷了半小时pdd之后我买了张ui图,并根据这个ui做了一个大屏。


​ 最终效果如下:


requestAnimationFrame效果.gif


📦项目地址


​ 这里附上项目地址,如果你觉得不错的话,帮我点一个小小的start



🌐在线预览



这个预览地址是vercel的地址,如果你没有挂梯子的话,会访问不了。访问不了的话,建议直接本地跑项目。



在线预览


🛠️ 技术栈


技术版本用途
Vue3.5.13前端框架
TypeScript5.7.2类型安全
Vite6.1.0构建工具
ECharts5.6.0数据可视化
Sass1.89.2CSS预处理器
Vue3-scroll-seamless1.0.6无缝滚动
autofit.js3.2.8适配不同分辨率的屏幕
vue3-odometer0.1.3数字翻牌效果

​ 项目主要是vue3+echarts的组合,整个项目主要都是一些图表的应用。下面会介绍一些模块的实现思路。


💻一些模块的实现


🗺️中间地图



DataV的地址



​ 第一步先获取地图行政区的geo数据,以我这个项目为例,我需要获取山东省的地图数据。


打开dataV,找到数据可视化学院,在里面找到需要的行政区,把它的geojson下载下来。


image-20250701225028999


下载下来的数据长这样


image.png


​ 这就是我们需要的geojson数据了。


​ 拿到数据之后,就需要将其渲染出来。


​ 这里我用的是echarts的地图。因为这个项目的地图,基本没有交互,就纯纯的数据展示。使用echarts来做的 效果会比,cesium那些更好。


注册地图


import * as echarts from 'echarts'
import sdData from '@/assets/data/山东省'
echarts.registerMap('sd', sdData as any)

先将前面下载来的数据geojson数据注册到echarts里面,并配置echartsgeo选项


{
geo: [
// 最外围发光边界
{
map: 'sd',
aspectScale: 0.85,
layoutCenter: ['50%', '50%'], //地图位置
layoutSize: '100%',
z: 12,
emphasis: {
disabled: true
},
itemStyle: {
normal: {
borderColor: 'rgb(180, 137, 81)',
borderWidth: 8,
shadowColor: 'rgba(218, 163, 88, 0.4)',
shadowBlur: 20
}
}
},
],
}

image-20250701230039331


这时候渲染出来的地图是纯色的,什么都没有 也没有立体。


因为这个geo是一个平面的地图,想要立体效果,可以通过堆叠地图,并且设置位移的方式实现


比如我这边就通过这种方式去实现


image-20250701230228946


通过叠加多个图层,并且每个图层的layoutCenter都不同


最终就可以实现这种看起来很立体的二维地图


image-20250701230412264


具体实现代码可以访问我的github仓库看,这里只介绍一下大致思路


🔢底部的数字字体和轮播


image.png
可以看到我底部的数字字体很特别,这不是图片,这是一种电子屏风格的数字字体。


image.png


我们在网上找一个类似的字体,将其下载下来,并用css@font-face将其引入。然后在需要的地方用font-family使用即可。


image-20250701230840381


除了这个,这里还有一个数字的轮播效果,我是用vue3-odometer实现的。


image-20250701231042464


为什么用这个库呢,主要是使用方便,不用配置一堆乱七八糟的。


📊其他图表


​ 其它图表就比较常规了,这里就不做过多介绍,具体可以看源码的实现。




🔚 结尾


​ 这个大屏虽然只有一个页面,但是做的时候,相关的图表配置调整还是挺多的。后续打算开发一个mini版的后台管理,用来管理大屏数据,并且这个后台管理的接口用node开发,用来当作node后端的练习。


作者:黑心皮蛋
来源:juejin.cn/post/7521986967103143972
收起阅读 »

40岁老前端2025年上半年都学了什么?

web
前端学习记录第5波,每半年一次。对前四次学习内容感兴趣的可以去我的掘金专栏“每周学习记录”进行了解。 第1周 12.30-1.5 本周学习了一个新的CSS媒体查询prefers-reduced-transparency,如果用户在系统层面选择了降低或不使用半透...
继续阅读 »

前端学习记录第5波,每半年一次。对前四次学习内容感兴趣的可以去我的掘金专栏“每周学习记录”进行了解。


第1周 12.30-1.5


本周学习了一个新的CSS媒体查询prefers-reduced-transparency,如果用户在系统层面选择了降低或不使用半透明,这个媒体查询就能够匹配,此特性与用户体验密切相关的。



更多内容参见我撰写的这篇文章:一个新的CSS媒体查询prefers-reduced-transparency —— http://www.zhangxinxu.com/wordpress/?…


第2周 1.6-1.12


这周新学习了一个名为Broadcast Channel的API,可以实现一种全新的广播式的跨页面通信。


过去的postMessage通信适合点对点,但是广播式的就比较麻烦。


而使用BroadcastChannel就会简单很多。


这里有个演示页面:http://www.zhangxinxu.com/study/20250…


左侧点击按钮发送消息,右侧两个内嵌的iframe页面就能接收到。



此API的兼容性还是很不错的:



更多内容可以参阅此文:“Broadcast Channel API简介,可实现Web页面广播通信” —— http://www.zhangxinxu.com/wordpress/?…


第3周 1.13-1.19


这周学习的是SVG半圆弧语法,因为有个需求是实现下图所示的图形效果,其中几段圆弧的长度占比每个人是不一样的,因此,需要手写SVG路径。



圆弧的SVG指令是A,语法如下:


M x1 y1 A rx ry x-axis-rotation large-arc-flag sweep-flag x2 y2

看起来很复杂,其实深究下来还好:



详见这篇文章:“如何手搓SVG半圆弧,手把手教程” - http://www.zhangxinxu.com/wordpress/?…


第4周-第5周 1.20-2.2


春节假期,学什么学,high起来。


第6周 2.3-2.9


本周学习Array数组新增的with等方法,这些方法在数组处理的同时均不会改变原数组内容,这在Vue、React等开发场景中颇为受用。


例如,在过去,想要不改变原数组改变数组项,需要先复制一下数组:



现在有了with方法,一步到位:



类似的方法还有toReversed()、toSorted()和toSpliced()。


更新内容参见这篇文章:“JS Array数组新的with方法,你知道作用吗?” - http://www.zhangxinxu.com/wordpress/?…


第7周 2.10-2.16


本周学习了两个前端新特性,一个JS的,一个是CSS的。


1. Set新增方法


JS Set新支持了intersection, union, difference等方法,可以实现类似交集,合集,差集的数据处理,也支持isDisjointFrom()是否相交,isSubsetOf()是否被包含,isSupersetOf()是否包含的判断。


详见此文:“JS Set新支持了intersection, union, difference等方法” - http://www.zhangxinxu.com/wordpress/?…



2. font-size-adjust属性


CSS font-size-adjust属性,可以基于当前字形的高宽自动调整字号大小,以便各种字体的字形表现一致,其解决的是一个比较细节的应用场景。


例如,16px的苹方和楷体,虽然字号设置一致,但最终的图形表现楷体的字形大小明显小了一圈:



此时,我们可以使用font-size-adjust进行微调,使细节完美。


p {  font-size-adjust: 0.545;}

此时的中英文排版效果就会是这样:



更新细节知识参见我的这篇文章:“不要搞混了,不是text而是CSS font-size-adjust属性” - http://www.zhangxinxu.com/wordpress/?…


第8周 2.17-2.23


本周学习的是HTML permission元素和Permissions API。


这两个都是与Web浏览器的权限申请相关的。


在Web开发的时候,我们会经常用到权限申请,比方说摄像头,访问相册,是否允许通知,又或者地理位置信息等。



但是,如果用户不小心点击了“拒绝”,那么用户就永远没法使用这个权限,这其实是有问题的,于是就有了元素,权限按钮直接暴露在网页中,直接让用户点击就好了。



但是,根据我后来的测试,Chrome浏览器放弃了对元素的支持,因此,此特性大家无需关注。


那Permissions API又是干嘛用的呢?


在过去,不同类型的权限申请会使用各自专门的API去进行,这就会导致开始使用的学习和使用成本比较高。


既然都是权限申请,且系统出现的提示UI都近似,何必来个大统一呢?在这种背景下,Permissions API被提出来了。


所有的权限申请全都使用一个统一的API名称入口,使用的方法是Permissions.query()。



完整的介绍可以参见我撰写的这篇文章:“HTML permission元素和Permissions API简介” - http://www.zhangxinxu.com/wordpress/?…


第9周 2.24-3.2


CSS offset-path属性其实在8年前就介绍过了,参见:“使用CSS offset-path让元素沿着不规则路径运动” - http://www.zhangxinxu.com/wordpress/?…


不过那个时候的offset-path属性只支持不规则路径,也就是path()函数,很多CSS关键字,还有基本形状是不支持的。


终于,盼星星盼月亮。


从Safari 18开始,CSS offset-path属性所有现代浏览器全面支持了。



因此,很多各类炫酷的路径动画效果就能轻松实现了。例如下图的蚂蚁转圈圈动画:



详见我撰写的此文:“终于等到了,CSS offset-path全浏览器全支持” - http://www.zhangxinxu.com/wordpress/?…


第10周 3.3-3.9


CSS @supports规则新增两个特性判断,分别是font-tech()和font-format()函数。


1. font-tech()


font-tech()函数可以检查浏览器是否支持用于布局和渲染的指定字体技术。


例如,下面这段CSS代码可以判断浏览器是否支持COLRv1字体(一种彩色字体技术)技术。


@supports font-tech(color-COLRv1) {}

2. font-format()


font-format()这个比较好理解,是检测浏览器是否支持指定的字体格式的。


@supports font-format(woff2) {   /* 浏览器支持woff2字体 */ }

不过这两个特性都不实用。


font-tech()对于中文场景就是鸡肋特性,因为中文字体是不会使用这类技术的,成本太高。


font-format()函数的问题在于出现得太晚了。例如woff2字体的检测,这个所有现代浏览器都已经支持了,还有检测的必要吗,没了,没有意义了。


不过基于衍生的特性还是有应用场景的,具体参见此文:“CSS supports规则又新增font-tech,font-format判断” - http://www.zhangxinxu.com/wordpress/?…


第11周 3.10-3.16


本周学习了一种更好的文字隐藏的方法,那就是使用::first-line伪元素,CSS世界这本书有介绍。


::first-line伪元素可以在不改变元素color上下文的情况下变色。


可以让按钮隐藏文字的时候,里面的图标依然保持和原本的文字颜色一致。



详见这篇文章:“一种更好的文字隐藏的方法-::first-line伪元素” - http://www.zhangxinxu.com/wordpress/?…


第12周 3.17-3.23


本周学习了下attachInternals方法,这个方法很有意思,给任意自定义元素使用,可以让普通元素也有原生表单控件元素一样的特性。


比如浏览器自带的验证提示:



比如说提交的时候的FormData或者查询字符串:



有兴趣的同学可以访问“研究下attachInternals方法,可让普通元素有表单特性”这篇文章继续了解 - http://www.zhangxinxu.com/wordpress/?…


第13周 3.24-3.30


本周学习了一个新支持的HTML属性,名为blocking 属性。


它主要用于控制资源加载时对渲染的阻塞行为。


blocking 属性允许开发者对资源加载的优先级和时机进行精细控制,从而影响页面的渲染流程。浏览器在解析 HTML 文档时,会根据 blocking 属性的值来决定是否等待资源加载完成后再继续渲染页面,这对于优化页面性能和提升用户体验至关重要。


blocking 属性目前支持的HTML元素包括

使用示意:



更多内容参见我撰写的这篇文章:“光速了解script style link元素新增的blocking属性” - http://www.zhangxinxu.com/wordpress/?…


第14周 3.31-4.6


本周学习了JS EditContext API。


EditContext API 是 Microsoft Edge 浏览器提供的一个 Web API,它允许开发者在网页中处理文本输入事件,以便在原生输入事件(如 keydown、keypress 和 input)之外,实现更高级的文本编辑功能。



详见我撰写的这篇文章:“JS EditContext API 简介” - http://www.zhangxinxu.com/wordpress/?…


第15周 4.7-4.13


本周学习一个DOM新特性,名为caretPositionFromPoint API。


caretPositionFromPoint可以基于当前的光标位置,返回光标所对应元素的位置信息,在之前,此特性使用的是非标准的caretRangeFromPoint方法实现的。


和elementsFromPoint()方法的区别在于,前者返回节点及其偏移、尺寸等信息,而后者返回元素。


比方说有一段

元素文字描述信息,点击这段描述的某个文字,caretPositionFromPoint()方法可以返回精确的文本节点以及点击位置的字符偏移值,而elementsFromPoint()方法只能返回当前

元素。


不过此方法的应用场景比较小众,例如点击分词断句这种,大家了解下即可。



详见我撰写的这篇文章:“DOM新特性之caretPositionFromPoint API” - http://www.zhangxinxu.com/wordpress/?…


第16周 4.14-4.20


本周学习的是getHTML(), setHTMLUnsafe()和parseHTMLUnsafe()这三个方法,有点类似于可读写的innerHTML属性,区别在于setHTMLUnsafe()似乎对Shadow DOM元素的设置更加友好。


parseHTMLUnsafe则是个document全局方法,用来解析HTML字符串的。


这几个方法几乎是同一时间支持的,如下截图所示:



具体参见我写的这篇文章:介绍两个DOM新方法setHTMLUnsafe和getHTML - http://www.zhangxinxu.com/wordpress/?…


第17周 4.21-4.27


光速了解HTML shadowrootmode属性的作用。


shadowRoot的mode是个只读属性,可以指定其模式——打开或关闭。


这定义了影子根的内部功能是否可以从JavaScript访问。


当影子根的模式为“关闭”时,影子根的实现内部无法从JavaScript访问且不可更改,就像元素的实现内部不能从JavaScript访问或不可更改一样。


属性值是使用传递给Element.attachShadow()的对象的options.mode属性设置的,或者在声明性创建影子根时使用<template>元素的shadowrootmode属性设置的。



类似的属性总共有4个:



  • shadowRootClonable 标示可复制状态

  • shadowRootDelegatesFocus 标示聚焦委托状态(子元素点击,ShadowRoot获得焦点)

  • shadowRootMode 标示开放状态

  • shadowRootSerializable 标示序列化状态


这些属性都是与Web Components开发相关的,我看还有人用在SSR中,可以遍历组件元素内部的信息。


详见我整理的这篇文章:“光速了解HTML shadowrootmode等属性的作用” - http://www.zhangxinxu.com/wordpress/?…


第18周 4.28-5.4


最近已经在正式项目中使用scale, rotate, translate属性了(注意,没有skew属性),很赞,毕竟这几个特性已经支持4年多了。



告别transform属性,直接使用scale、rotate和translate属性,是 CSS 发展的一个新趋势。它们不仅语法简洁、易于使用,而且能让我们更方便地对元素的变形效果进行独立控制,提高代码的可维护性和性能。在未来的前端开发中,我们应该积极拥抱这些新特性,让我们的 CSS 代码更加简洁、高效。


详见此文:告别transform,是时候直接使用scale, rotate属性啦 - http://www.zhangxinxu.com/wordpress/?…


第19周 5.5-5.11


本周学习CSS animation-composition属性,该属性可以让动画效果累加。


演示页面地址见这里:不同值混合后的动画效果demo - http://www.zhangxinxu.com/study/20250…


支持replace、add和accumulate这三个值,其中后面两个值很容易混淆,add直接就是属性值累加,accumulate则是属性的计算值累加。


animation-composition特别适合用在transform定位的同时需要transform动画的场景中。



详见我写的这篇文章:“CSS animation-composition可以让动画效果累加” - http://www.zhangxinxu.com/wordpress/?…


第20周 5.12-5.18


这是这周才知道的一个知识,那就是输入框的value值也能直接返回数值类型。


已知输入框元素: 


<input id="number" min="1" max="10" type="number" />

平常我们获取输入框的值都是使用 number.value 获取的,但是这个属性的返回值是个字符串。


其实现在浏览器支持直接返回数值类型的,使用numer.valueAsNumber即可。


类似的还有valueAsDate属性,适合时间类型的输入框。


详见此文:你知道吗,输入框的value值也能直接返回数值类型 - http://www.zhangxinxu.com/wordpress/?…


第21周 5.19-5.25


Chrome 133实现了attr()函数所有CSS属性都支持,这个特性可就厉害了。



举个例子,有一个链接地址是图片,那么,无需img元素介入,纯CSS就能让这个地址以图片的方式显示出来。


代码示意:


<a href="example.jpg">图片?</a>
[href]::before {
content: '';
display: block;
width: 150px; height: 200px;
background: image-set(attr(href));
background-size: cover;
}

此时,图片显示的效果就可以实现了。


关于attr()函数更多内容,可以参加此文:“震惊,有生之年居然看到CSS attr()全属性支持” - http://www.zhangxinxu.com/wordpress/?…


第22周 5.26-6.1


本周学习的是JS PageSwapEvent事件,乍一看,以为是页面切换触发的事件。


后来细细一研究,并不是,这个事件发生在,如果页面设置了页面级别的Page Transition过渡效果(URL跳转的页面之间也能平滑过渡,参见下面GIF图),在页面离开的时候,会触发。



主要是方便开发者精确控制页面间的动画细节用的。


这么一看,此事件算是比较小众的,平常开发使用机会并不大,了解下即可。


详见此文:“JS PageSwap PageReveal事件干嘛用的?” -http://www.zhangxinxu.com/wordpress/?…


第23周 6.2-6.8


本周学习的是CSS新的伪元素::scroll-button,其通过特定语法,可以给滚动容器创建自定义的滚动定位按钮,例如:


ul::scroll-button(left) { content: "◄"; } 
ul::scroll-button(right){content:"►";}

配合Scroll Snap,可以纯CSS实现slider效果:



更多内容容我本周继续深入学习。


第24周 6.9-6.15


本周学习::scroll-marker伪元素。


上周学习的::scroll-button()伪元素函数可以给Carousel 效果增加左右切换按钮



这周学习的::scroll-marker则可以给Scroll Snap交互的列表元素创建索引切换按钮,以便定位到具体的元素上,效果参见:



::scroll-marker需要配合scroll-marker-group属性和::scroll-marker-group伪元素一起使用才能生效。


另外,同时被浏览器支持的还有::column伪元素,如果Snap效果是使用columns布局实现的时候使用。


更多内容,可以访问这篇文章:“CSS ::scroll-button ::scroll-marker伪元素又是干嘛用的?” - http://www.zhangxinxu.com/wordpress/?…


第25周 6.16-6.22


本周学习了text-wrap的两个子属性和两个新值。


text-wrap:pretty声明和text-wrap:wrap是一样的,区别在于text-wrap:pretty更注重排版,而非性能,也就是wrap的算法速度更快。


text-wrap:stable可以让编辑内容前面的行内容保持稳定,而不会整个文本内容发生排版变化。


两个子属性,一个是text-wrap-mode,还有一个是text-wrap-style。



更多相关内容参见这篇文章:text-wrap进化:支持两子属性和pretty stable新值 - http://www.zhangxinxu.com/wordpress/?…


第26周 6.23-6.29


本周学习clip-path shape()函数。


之前的路径剪裁使用的是path()函数,但是会有尺寸无法自适应的问题。


因为SVG路径里面的数值都是固定的像素px大小,在SVG元素中,这些大小与SVG外部尺寸关联,不会有问题,但是,放在CSS图像中,那就问题大了。


例如,Font Awesome小图标SVG基本尺寸都是512*512,其path坐标值都是好几百的值。


但是,CSS小图标的尺寸是20*20,如果应用几百数值的剪裁路径,小图标肯定就有问题,对不对?


要么path坐标等比例缩小,要么CSS小图标尺寸也设成512像素,然后再zoom缩放,但这样实现就很麻烦。


于是,在这个背景下,clip-path的shape()函数应运而生。


.use-shape {  
clip-path: shape(from 50% 0%,curve to 0% 50% with 22.38% 0%/0% 22.38%,smooth by 50% 50% with 22.38% 50%,smooth by 50% -50% with 50% -22.38%,smooth to 50% 0% with 77.62% 0%,close);
}

支持百分比值,和CSS calc等数学函数,自动和元素尺寸相适应,就很厉害!


对此,我还专门开发了一个CSS clip-path path() to shape()函数转换工具 - http://www.zhangxinxu.com/sp/path2sha…



详见我撰写的这篇文章:“CSS小图标剪裁终极解决方案clip-path shape()函数” - http://www.zhangxinxu.com/wordpress/?…


-------------


好,以上就是我这个40岁的老前端上半年学习的内容,下半年我还将继续学习,继续保持对前端的好奇心,欢迎关注,转发,一起进步。


作者:张鑫旭
来源:juejin.cn/post/7524548909530005540
收起阅读 »

我这🤡般的7年开发生涯

前两天线上出了个漏洞,导致线上业务被薅了 2w 多块钱。几天晚上没咋睡,问 ChatGPT,查了几晚资料,复盘工作这么久来犯下的错误。 我在公司做的大部分是探索性、创新性的需求,行内人都知道这些活都是那种脏活累活,需求变化大,经常一句话;需求功能多,看着简单一...
继续阅读 »

前两天线上出了个漏洞,导致线上业务被薅了 2w 多块钱。几天晚上没咋睡,问 ChatGPT,查了几晚资料,复盘工作这么久来犯下的错误。


我在公司做的大部分是探索性、创新性的需求,行内人都知道这些活都是那种脏活累活,需求变化大,经常一句话;需求功能多,看着简单一细想全是漏洞;需求又紧急,今天不上线业务就要没。


所以第一个建议就是大家远离这些需求,否则你会和我一样变得不幸。


但是👴🐂🍺啊,接下来也就算了,还全干完了。正常评估一个月的需求,我 tm 半个月干完上线;你给我一句话,我干完一整条链路上的事;你说必须今天上线,那就加班加点干上线。


就这样干了几年,黄了很多,也有做起来的。但是不管业务怎么发展,这样做时间长了会出现很多致命问题。


开发忙成狗


一句话需求太多,到最后只有开发最了解业务,所有人所有事都来找开发,开发也是人,开发还要写代码呢。最先遇到的问题就是时间严重不够,产品跟个摆设一样,什么忙都帮不上,我成了产品开发结合体。


bug 来了


开发一忙,节奏就乱了,乱则生 bug,再加上原本需求上逻辑不完整的深坑,坑上叠坑,出 bug 是迟早的事。


形象崩塌


一旦出现 bug,人设就毁了。记住一句话,没人会感谢你把原本一个月的需求只用半个月上线,大家都觉得这玩意本来就半个月工时。慢慢的开始以半个月的工时要求你。


那些 bug 自己回头,慢慢做都是可以避免的,就像考试的时候做完了卷子复查一遍,很多问题回头看一下都能发现,结果因为前期赶工,没时间回看,而且有很多图快的写法,后期都是容易出问题的。


形象崩塌在职场中是最恐怖的,正所谓好事不出门,坏事传千里。


一旦出了问题,团队、领导、所有人对你的体感,那都是直线下降,你之前做的所有好事,就跟消失了一样,别人对你的印象,一提起来说的都是,这不是当时写出 xxx bug 的人吗?这还怎么在职场生存?脸都没了,项目好处也跟自己没关系了。


我 tm 真是愣头青啊蠢的💊💩,从入职开始都想的是多学点多干点,结果干的越多错的越多,现在心态干崩了,身体干垮了,钱还没混子多,还背了一身骂名和黑锅。


之前我看同事写代码贼慢,鼠标点来点去,打字也慢一拍,我忍不住说他你这写代码速度太慢了,可以用 xxx 快捷键等等,现在回想起来,我说他不懂代码,其实是我不懂职场。


我真是个纯纯的可悲🤡。


提桶跑路


bug 积累到一定程度,尤其是像我这样出现点资金的问题,那也差不多离走人不远了,我感觉我快到这个阶段了,即使不走,扣钱扣绩效也是在所难免的,综合算下来,还没那些混子赚的多。


我亲自接触的联调一哥们儿,一杯茶,一包烟,一个 bug 修一天。是真真正正的修了一天,从早到晚。那天我要上线那个需求,我不停的催他,后来指着代码说着逻辑让他写,最终半夜转点上线。我累的半死不活,我工资和他差不多,出了问题我还要背锅。


我现在听到 bug 都 PTSD 了,尤其是资金相关的,整个人就那种呆住,大脑空白,心脏像被揪住,我怀疑我有点心理问题了都。


为什么别人可以那么安心的摸鱼?为什么我要如此累死累活还不讨好?我分析出几点我的性格问题。


责任心过强


什么事都觉得跟自己有关系,看着别人做的不好,我就自己上手。


到后期产品真 tm 一句话啊,逻辑也不想,全等着我出开发方案,产品流程图,我再告诉她哪里要改动。不是哥们?合着我自己给出需求文档再自己写代码?


为人老实


不懂拒绝,不懂叫板。


运营的需求,来什么做什么,说什么时候上线就什么时候上线。不是哥们?我都还不知道要做什么,你们把上线时间都定了?就 tm 两字,卑微。


用力过猛


十分力恨不得使出十一分,再加一分吃奶的劲儿。一开始就领导很高的期望,后面活越来越多,而且也没什么晋升机会了,一来的门槛就太高了知道吧,再想提升就很难了。


先总结这么多吧,我现在心情激荡的很,希望给各位和我性格差不多一点提醒,别像我这样愣头青,吃力不讨好,还要遭人骂。后面再写写改进办法。


作者:小兵张健
来源:juejin.cn/post/7450047052804161576
收起阅读 »

一张 8K 海报差点把首屏拖垮

web
你给后台管理系统加了一个「企业风采」模块,运营同学一口气上传了 200 张 8K 宣传海报。首屏直接飙到 8.3 s,LCP 红得发紫。 老板一句「能不能像朋友圈那样滑到哪看到哪?」——于是你把懒加载重新翻出来折腾了一轮。 解决方案:三条技术路线,你全踩了一...
继续阅读 »

你给后台管理系统加了一个「企业风采」模块,运营同学一口气上传了 200 张 8K 宣传海报。首屏直接飙到 8.3 s,LCP 红得发紫。

老板一句「能不能像朋友圈那样滑到哪看到哪?」——于是你把懒加载重新翻出来折腾了一轮。




解决方案:三条技术路线,你全踩了一遍


1. 最偷懒:原生 loading="lazy"


一行代码就能跑,浏览器帮你搞定。


<img
src="https://cdn.xxx.com/poster1.jpg"
loading="lazy"
decoding="async"
width="800" height="450"
/>


🔍 关键决策点



  • loading="lazy" 2020 年后现代浏览器全覆盖,IE 全军覆没。

  • 必须写死 width/height,否则 CLS 会抖成 PPT。


适用场景:内部系统、用户浏览器可控,且图片域名已开启 Accept-Ranges: bytes(支持分段加载)。




2. 最稳妥:scroll 节流 + getBoundingClientRect


老项目里还有 5% 的 IE11 用户,我们只能回到石器时代。


// utils/lazyLoad.js
const lazyImgs = [...document.querySelectorAll('[data-src]')];
let ticking = false;

const loadIfNeeded = () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
lazyImgs.forEach((img, idx) => {
const { top } = img.getBoundingClientRect();
if (top < window.innerHeight + 200) { // 提前 200px 预加载
img.src = img.dataset.src;
lazyImgs.splice(idx, 1); // 🔍 及时清理,防止重复计算
}
});
ticking = false;
});
};

window.addEventListener('scroll', loadIfNeeded, { passive: true });

🔍 关键决策点



  • requestAnimationFrame 把 30 ms 的节流降到 16 ms,肉眼不再掉帧。

  • 预加载阈值 200 px,实测 4G 网络滑动不白屏。


缺点:滚动密集时 CPU 占用仍高,列表越长越卡。




3. 最优雅:IntersectionObserver 精准观测


新项目直接上 Vue3 + TypeScript,我们用 IntersectionObserver 做统一调度。


// composables/useLazyLoad.ts
export const useLazyLoad = (selector = '.lazy') => {
onMounted(() => {
const imgs = document.querySelectorAll<HTMLImageElement>(selector);
const io = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
const img = e.target as HTMLImageElement;
img.src = img.dataset.src!;
img.classList.add('fade-in'); // 🔍 加过渡动画
io.unobserve(img); // 观测完即销毁
}
});
},
{ rootMargin: '100px', threshold: 0.01 } // 🔍 提前 100px 触发
);
imgs.forEach((img) => io.observe(img));
});
};


  1. 浏览器合成线程把「目标元素与视口交叉状态」异步推送到主线程。

  2. 主线程回调里只做一件事:把 data-src 搬到 src,然后 unobserve

  3. 整个滚动期间,零事件监听,CPU 占用 < 1%。




原理剖析:从「事件驱动」到「观测驱动」


维度scroll + 节流IntersectionObserver
触发时机高频事件(~30 ms)浏览器内部合成帧后回调
计算量每帧遍历 N 个元素仅通知交叉元素
线程占用主线程合成线程 → 主线程
兼容性IE9+Edge79+(可 polyfill)
代码体积0.5 KB0.3 KB(含 polyfill 2 KB)

一句话总结:把「我每隔 16 ms 问一次」变成「浏览器你告诉我啥时候到」。




应用扩展:把懒加载做成通用指令


在 Vue3 项目里,我们干脆封装成 v-lazy 指令,任何元素都能用。


// directives/lazy.ts
const lazyDirective = {
mounted(el: HTMLImageElement, binding) {
const io = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
el.src = binding.value; // 🔍 binding.value 就是 data-src
io.disconnect();
}
},
{ rootMargin: '50px 0px' }
);
io.observe(el);
},
};

app.directive('lazy', lazyDirective);

模板里直接写:


<img v-lazy="item.url" :alt="item.title" />



举一反三:三个变体场景思路



  1. 无限滚动列表

    IntersectionObserver 绑在「加载更多」占位节点上,触底即请求下一页,再把新节点继续 observe,形成递归观测链。

  2. 广告曝光统计

    广告位 50% 像素可见且持续 1 s 才算一次曝光。设置 threshold: 0.5 并在回调里用 setTimeout 延迟 1 s 上报,离开视口时 clearTimeout

  3. 背景图懒加载

    背景图没有 src,可以把真实地址塞在 style="--bg: url(...)",交叉时把 background-image 设成 var(--bg),同样零回流。




小结



  • 浏览器新特性能救命的,就别再卷节流函数了。

  • 写死尺寸、加过渡、及时 unobserve,是懒加载不翻车的三件套。

  • 把观测器做成指令/组合式函数,后续业务直接零成本接入。


现在你的「企业风采」首屏降到 1.2 s,老板滑得开心,运营继续传 8K 图,世界和平。


作者:前端微白
来源:juejin.cn/post/7530854092869615635
收起阅读 »

如果产品经理突然要你做一个像抖音一样流畅的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
收起阅读 »

前端部署,又有新花样?

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
收起阅读 »

这个老爷子研究的神奇算法,影响了全世界!

这两天,科技圈的又一个重磅新闻相信不少同学都刷到了。 那就是 77 岁的计算机科学家,图灵奖+诺贝尔奖双奖得主,同时也是享誉全球的人工智能专家 Geoffrey Hint0n(杰弗里・辛顿)首次来到了中国,参加了在上海举办的 2025 世界人工智能大会(WAI...
继续阅读 »

这两天,科技圈的又一个重磅新闻相信不少同学都刷到了。


那就是 77 岁的计算机科学家,图灵奖+诺贝尔奖双奖得主,同时也是享誉全球的人工智能专家 Geoffrey Hint0n(杰弗里・辛顿)首次来到了中国,参加了在上海举办的 2025 世界人工智能大会(WAIC 2025)并上台进行了演讲。


Hint0n在WAIC 2025上的演讲


上一次 Hint0n 站在聚光灯下是去年 10 月份,彼时 76 岁的 Hint0n 刚刚和 John J. Hopfield 一起,拿到了 2024 年的诺贝尔物理学奖,以表彰他们通过人工神经网络实现机器学习的奠基性发现和研究。


Hint0n获得2024年诺贝尔物理学奖


提到 Hint0n 这个名字,相信对于很多学习和从事人工智能工作的同学来说,应该都非常熟悉了。


Hint0n 是一位享誉全球的人工智能专家,被誉为“神经网络之父”、“深度学习的鼻祖”、“人工智能教父”等等,也是这个领域最受尊崇的泰斗之一。


Geoffrey Hint0n


7 年前,Hint0n 曾拿下过 2018 年度图灵奖。


如此一来,Hint0n 也成为了获得**「图灵奖+诺贝尔奖」的双奖得主**。


Hint0n 是学术界的传奇人物。


在当代人工智能爆发之前,Hint0n 曾经坐了几十年的学术冷板凳,而他所研究开发的神经网络算法则为后续人工智能的进一步发展奠定了基础。


大多人可能都是因为这几年大火的 AI 领域才了解的 Hint0n,但是看完他之前的人生经历,更可谓是颇具戏剧性


1947 年,Geoffrey Hint0n 出生于英国温布尔登的一个学术世家,家庭里的很多成员都在学术和研究方面颇有造诣。


8岁时的Hint0n


他的父亲 Howard Everest Hint0n 是一名研究甲壳虫的英国昆虫学家,而母亲 Margaret Clark 则是一名教师。


除此之外,他的高曾祖父 George Boole 还是著名的逻辑学家,也是现代计算科学基础布尔代数的发明人,而他的叔叔 Colin Clark 则是一位著名的经济学家。


当然,在这样的氛围下长大的 Hint0n,其成长压力也是可想而知的。


1970 年,23 岁的 Hint0n 获得了实验心理学的学士学位。


但是,令谁也没有想到的是,毕业后这位“学二代”由于找不到科研的意义,他竟然先跑去当了一段时间的木匠


不过这个经历并没有帮助他消除自己的阶段性迷茫,他一直希望真正理解大脑的运作原理,渴望更深入的理论研究,于是经历过一番思想斗争后又下决心重返学术殿堂,投身于人工智能领域。


直到 1978 年,他终于获得了爱丁堡大学人工智能学博士学位,而此时的他也 31 岁了。


那个年代做深度学习的研究可以说是妥妥的冷板凳。


要知道当时的 AI 正值理论萌芽阶段,并且彼时的深度学习研究在很长一段时间里一直处于一个不温不火的状态,甚至好几次陷入寒冬,那 Hint0n 所主张和研究的深度学习派当然在当时也很难得到关注和认可。


面对这一系列冷漠、质疑甚至反对,唯有纯粹的相信与热爱才能将这个领域深耕了数十年,直到坚持到后来 AI 时代的来临。


Hint0n 主要从事神经网络机器学习的研究,在 AI 领域做出过许多重要贡献,其中最著名的当属他在神经网络领域所做的研究工作。



他在 20 世纪 80 年代就已经开启了反向传播算法(Back Propagation, BP 算法)的研究,并将其应用于神经网络模型的训练中。


这一算法被广泛应用于语音识别、图像识别和自然语言处理等领域。



除此之外,Hint0n 还在卷积神经网络(Convolutional Neural Networks,CNN)、深度置信网络(Deep Belief Networks,DBN)、递归神经网络(Recursive Neural Networks,RNN)以及胶囊网络(Capsule Network)等领域作出了重要贡献。


2013 年,Hint0n 加入 Google,同时把机器学习相关的很多技术带进了谷歌,并融合到谷歌的多项实际业务之中。



2019 年 3 月,ACM 公布了 2018 年度的图灵奖得主。


图灵奖大家都知道,是计算机领域的国际最高奖项,也被誉为“计算机界的诺贝尔奖”。


而 Hint0n 则与蒙特利尔大学计算机科学教授 Yoshua Bengio 和 Meta 首席 AI 科学家 Yann LeCun 一起因为研究神经网络而获得了该年度的图灵奖,以表彰他们在对应领域所做的杰出贡献。


Hint0n获得2018年度图灵奖


除此之外,Hint0n 在他的学术生涯中发表了数百篇论文,这些论文中提出了许多重要的理论和方法,涵盖了人工智能、机器学习、神经网络、计算机视觉等多个领域。


而且他的论文被引用的次数也是惊人,这对于后续该领域的研究和发展都产生了重要的影响。



除了自身在 AI 领域的科研造诣很高,Hint0n 同时也是一名优秀的导师和指引者。


当年为了扩大深度学习的圈子,Hint0n 曾在多伦多大学成立过研究中心,专门接收有兴趣从事相关研究的年轻人,以至于现如今 AI 大佬圈子的“半壁江山”都是 Hint0n 的“门生”。



Hint0n 带过很多大牛学生,其中不少都被像苹果、Google 等这类硅谷科技巨头所挖走,在对应的公司里领导着人工智能相关领域的研究。


这其中比较典型的就是 Ilya Sutskever,他既是 Hint0n 的学生,同时他也是大名鼎鼎的 OpenAI 公司的联合创始人与首席科学家。


Hint0n和他的学生们


在这次 WAIC 2025 开幕的前一天,一张全球顶级 AI 专家的合影在业内广为流传。



画面中聚集着包括姚期智先生等在内的多个全球顶级 AI 专家。


不过大家可能也注意到了,在画面的最后一排,还独自站立着一位身材高挑的白发老人。


没错,这个人正是 77 岁的 Hint0n。


画面中 Hint0n 之所以选择站立合影,不是为了秀身高,而是因为 Hint0n 患有腰椎疾病


1947 年出生的辛顿,年轻时就曾落下腰伤顽疾,随着年龄的增大,这让他无法像正常人那样轻松地坐下,而是习惯于尽量站立,所以这也是为什么大家能看到的 Hint0n 能站着的情况就不会坐着,而即便是坐,Hint0n 的坐姿也非常奇怪。



听 Hint0n 身边的人说,Hint0n 到哪里总是会随身带个垫子,目的也是为了应对多年的腰疾。


不过从这次 Hint0n 来中国参加 WAIC 2025 的全过程来看,老爷子的精神状态还算挺不错。


学术冷板凳 30 年,从谷歌离职时 75 岁,回看 Hint0n 老爷子的奋斗过往,的确非常传奇。


如今 AI 技术的发展巨轮还在滚滚向前,而这些人工智能领域的泰斗们所打下的基础也将继续成为人工智能研究者们心中的图腾,从而激发出更多的进化与创新。



注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。



作者:CodeSheep
来源:juejin.cn/post/7532771194003554323
收起阅读 »

🍏让前端去做 iPhone 的液态玻璃❓

iOS
最近 iOS 推送了新的系统更新: 其实这个更新早在几个月前的 WWDC 上就开始宣传了: 仔细看苹果 Logo,有种磨砂玻璃的质感对吧?因为他们宣传的 iOS26 主题就是液态玻璃: 为什么直接从 iOS18 跳到 26 呢?这跟咱们前端的 ES 命名...
继续阅读 »

最近 iOS 推送了新的系统更新:


SCR-20250917-kpha.png


其实这个更新早在几个月前的 WWDC 上就开始宣传了:


73b4a507a0ecbfe444f7cf0bb703f915.jpg


仔细看苹果 Logo,有种磨砂玻璃的质感对吧?因为他们宣传的 iOS26 主题就是液态玻璃


2025-09-17 11-57-43.2025-09-17 11_59_46.gif


为什么直接从 iOS18 跳到 26 呢?这跟咱们前端的 ES 命名有点像:



  • ES5

  • ES5.1

  • ES2015(ES6)

  • ES2016

  • ES2017

  • ES2018

  • ES2019

  • ES2020

  • ES2021

  • ES2022

  • ES2023

  • ES2024

  • ES2025

  • ES2026


苹果也打算用年份来命名版本,每年都推陈出新一个版本,但叫 iOS2026 有点太长了,于是省略掉前面的 20 就变成了 iOS26


这个液态玻璃有人觉得好看也有人觉得难看,毕竟一百个读者就有哈姆雷特嘛!我们公司的产品经理觉得好看,它就想让我们也学苹果搞个这样的主题。头都大了,先给大家看看我们以前的界面:


2025-08-14 19-36-22.2025-08-14 19_37_01.gif


我们面向的是海外用户,所以界面都是英文,有人可能会问那为什么不去欧美开公司呢?我们确实有欧美的分部,但负责的是运营,而开发基本上都在中国招人,因为国内的牛马实在是太好用了😭


既便宜又耐用,而且还能无偿加班,在欧美你搞这套试试?扯远了哈!咱们来看下液态玻璃之后的界面:


2025-08-14 19-34-59.2025-08-14 19_36_02.gif


当然这几个图标还没换成透明的,他们想先看看效果再决定用不用开发液态玻璃主题。由于背景是白色的,所以它们认为这个液态玻璃有点与界面融为一体了,于是我们给它加了点淡蓝色:


2025-08-14 19-25-40.2025-08-14 19_28_06.gif


那么具体到底是怎么实现的呢?实现是不可能自己实现的,因为这个效果可不是常规效果,我们一开始搜了一下液态玻璃的实现,结果您猜怎么着:


SCR-20250917-nlvn.jpeg


这是液态玻璃么?这只是个很普通的毛玻璃吧?评论区也都这么说:


SCR-20250917-nmgo.png


SCR-20250917-nmil.png


人家液态玻璃的重点是边缘部分,以我们日常生活中常见的烧水壶来举例:


2025-09-17 15-19-09.2025-09-17 15_21_41.gif


大家也可以拿自己的烧水壶试试,可以看到越靠近边缘,玻璃后面的图案变形就越严重,而且最边缘还会有倒影,除了变形以外还会有色散:


image.png


SCR-20250917-nrfk.jpeg


所以这个效果实现起来还是挺复杂的,感觉是需要计算机图形学知识的,幸好最终在网上找到了符合需求的液态玻璃效果:


2025-09-17 15-32-49.2025-09-17 15_35_43.gif


这个效果是由 Three.js 实现的,传送门:



reactbits.dev



通过域名大家也能看出来,这是用 React 实现的,但我们公司用的是 Vue。可能有人会说这就是个特效,无论用 React 还是用 Vue 不都一样么?你就不能给它用 Vue 的语法翻译过来么?毕竟 Three.js 又不挑框架,但 React Three 挑框架啊:


SCR-20250917-occk.png


这个 Demo 大量的使用了 @react-three


SCR-20250917-ocyr.png


我总不能再仿照 @react-three 写个 @vue-three 出来吧?那 vue 生态里就没有 @vue-three 吗?有是有,但都不太靠谱:


SCR-20250917-odty.png


那你不会直接用 Three.js 写?@react-three 不也就是层封装么?


还真就不会直接用 Three.js 写,因为以前压根就没用过,想要搞懂 @react-three 那些封装的话必然要去看源码,而且最好是有一定的 Three.js 基础才能看明白。这个 React Bits 的作者还出了个 Vue 版的,Vue 版直接砍掉了这个 Three.js 版的液态玻璃:



vue-bits.dev



也就是说就连原作者都没法直接用 Three.js 去实现,当然也可能不是他无法实现 Vue 版的,而是脱离了 @react-three 去实现的话很麻烦之类的原因,总之就是原作者并未实现 Vue 版的。他实现起来都费劲呢,更别提我们这种压根就不会 Three 的人了。


而且产品希望这个液态玻璃只是个锦上添花的效果,不希望为了这个主题而引入 Three.js 从而导致页面变大变慢,所以 Three.js 这个方案被放弃。幸好这个 React Bits 还有个 SVG 版的,而且 SVG 版的还有 Vue 版实现:


2025-09-17 16-16-21.2025-09-17 16_17_24.gif


但其实还是更喜欢 Three.js 那版,我也说不上来 SVG 版的哪不好,但确实看起来好像没 Three.js 版的好看:


2025-09-17 15-32-49.2025-09-17 15_35_43.gif


不过好在 SVG 版不需要任何依赖就能够实现,非常的轻量化,在这里分享给大家:



传送门:vue-bits.dev/components/…



希望能够帮助到有需要的人,毕竟国内想模仿苹果的公司还挺多的,比方说某个把自己手机叫 17 Pro Max 的粮食公司:


360_240_2_2019090518436219.jpg


往期精彩文章



作者:页面魔术
来源:juejin.cn/post/7552402306222882842
收起阅读 »

从“版本号打架”到 30 秒内提醒用户刷新:一个微前端团队的实践

web
从“版本号打架”到 30 秒内提醒用户刷新:一个微前端团队的实践1. 背景与痛点我们团队维护着一个微前端子应用集群,每个子应用都需要同时服务 dev / test / release / online 多套环境。分支策略(master / release / ...
继续阅读 »

从“版本号打架”到 30 秒内提醒用户刷新:一个微前端团队的实践

1. 背景与痛点

我们团队维护着一个微前端子应用集群,每个子应用都需要同时服务 dev / test / release / online 多套环境。分支策略(master / release / test / dev / hotfix / feature_x.x.x)加上 Jenkins 自动化,让“一天多次发布”成为常态。但真正影响交付效率的并不是发布次数,而是一个顽固的问题:测试同学常年停留在旧版本页面

1.1 真实场景

  • 测试在早上打开 dev 页面,下午我们发布了新的组件样式;
  • 他们继续在旧页面里回归,反馈的问题我们一眼看出“这是老版本”;
  • 群里喊“刷新一下”并不靠谱,于是“无效缺陷 + 反复沟通”成了常态。

更严重的一次事故,是我们在版本检查逻辑里同时使用了 webpack DefinePlugin 与自定义插件,各自调用了一次 getAppVersion()。结果前端控制台打印的是 0.8.3-release-202511210828,而 version.json 里是 0.8.3-release-202511210829。两边只差 1 秒钟,却让线上用户始终被提示刷新,形象地被团队称为“版本号打架”。

1.2 我们的诉求

  1. 用户在 30 秒内感知版本更新;
  2. 弹窗里能看到“当前版本 / 最新版本 / 环境”;
  3. 支持“立即刷新 / 稍后再说”,不给用户造成中断;
  4. 方案需兼容现有微前端架构与 CI/CD 流程,不依赖后端改造。

2. 方案探索与取舍

在动手前,我们列出几种可行方式:

方案实现复杂度实时性依赖适配场景关键优缺点
纯前端轮询 version.json中(30s)前端 + Nginx多环境微前端成本最低;轻微网络开销
Service Worker/PWA较高现代浏览器PWA 应用缓存控制好,但改造量大
WebSocket 推送最高后端服务强实时场景需要额外服务端开发
后端接口统一管理前后端版本集中管理带来跨团队耦合

综合团队资源与落地速度,我们选择了 纯前端轮询 + 静态版本文件 的做法,并明确两个原则:

  • 版本号唯一,可追溯基础版本号-环境-时间戳
  • 发布零侵入:Jenkins 仍旧运行 npm run build-xxx,无需新增步骤。

3. 技术方案总览

  1. 构建阶段生成 version.json:在 vue.config.js 中提前计算版本号,既注入到前端(process.env.APP_VERSION),也写入输出目录的 version.json
  2. 前端轮询比对:应用启动后每 30 秒请求一次 version.json,禁用缓存并携带时间戳,比较版本号;
  3. 交互提示:复用 Ant Design Vue 的 Modal.confirm,展示当前/最新版本与环境;
  4. 缓存策略:Nginx 对 HTML/version.json 禁止缓存,对 JS/CSS/图片继续长缓存;
  5. CI/CD 配合:所有环境沿用既有脚本,只是构建产物目录多了一份实时的 version.json

4. 关键落地细节

4.1 版本号只生成一次(Build-time Deterministic Versioning)

vue.config.js 抽象 buildEnvNamebuildVersion,并在 DefinePlugin 与生成 version.json 时复用:

const buildEnvName = getEnvName();
const buildVersion = getAppVersion();

module.exports = {
configureWebpack: {
  plugins: [
    new webpack.DefinePlugin({
      "process.env.APP_VERSION": JSON.stringify(buildVersion),
      "process.env.APP_ENV": JSON.stringify(buildEnvName),
    }),
  ],
},
chainWebpack(config) {
  config.plugin("generate-version-json").use({
    apply(compiler) {
      compiler.hooks.done.tap("GenerateVersionJsonPlugin", () => {
        fs.writeFileSync(
          path.resolve(__dirname, "edu/version.json"),
          JSON.stringify(
            {
              version: buildVersion,
              env: buildEnvName,
              timestamp: new Date().toISOString(),
              publicPath: "/child/edu",
            },
            null,
            2
          )
        );
      });
    },
  });
},
};

这样即使构建过程持续 5~10 分钟,注入的版本号和静态文件里的版本仍保持一致。这其实是把“构建产物视为不可变工件”的原则落地——保证任何使用该工件的入口看到的元数据都是同一个快照。

4.2 版本检查器(Runtime Polling & Cache Busting)

class VersionChecker {
currentVersion = process.env.APP_VERSION;
publicPath = "/child/edu";
checkInterval = 30 * 1000;

init() {
  console.log(`📌 当前前端版本:${this.currentVersion}(${process.env.APP_ENV})`);
  this.startChecking();
  document.addEventListener("visibilitychange", () => {
    if (document.visibilityState === "visible" && !this.hasNotified) {
      this.checkForUpdate();
    }
  });
}

async checkForUpdate() {
  const url = `${this.publicPath}/version.json?t=${Date.now()}`;
  const response = await fetch(url, { cache: "no-store" });
  if (!response.ok) return;
  const latestInfo = await response.json();
  if (latestInfo.version !== this.currentVersion && !this.hasNotified) {
    this.hasNotified = true;
    this.stopChecking();
    this.showUpdateModal(latestInfo.version, latestInfo.env);
  }
}
}

这里有两个容易被忽略的细节:

  1. fetch 显式加 cache: "no-store",再叠加时间戳参数,防止 CDN / 浏览器任何一层干预;
  2. visibilitychange 监听,保证窗口重新激活时立即比对,避免用户在后台等了很久才看到弹窗。

入口 main.ts 在应用 mount 之后调用 versionChecker.init(),即可把整个检测链路串起来。

4.3 Nginx 缓存策略(Precise Cache Partition)

location / {
  if ($request_filename ~* .html$) {
      add_header Cache-Control "no-store, no-cache, must-revalidate";
  }
}

location /child/edu {
  if ($request_filename ~* .html$) {
      add_header Cache-Control "no-store, no-cache, must-revalidate";
  }
}

location ~* /child/edu/version.json$ {
  add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate";
  add_header Pragma "no-cache";
  add_header Expires "0";
  add_header Surrogate-Control "no-store";
}

这一层的思路是把资源分成两类:需要实时性(HTML、version.json)就 no-store,其余走长缓存。再配合 try_files 兜底 history 路由,微前端子应用的独立部署不会互相影响。

4.4 CI/CD 配置(Zero-touch Pipeline)

环境构建命令输出路径说明
developnpm run build-develop/child/edu日常开发验证
testingnpm run build-testing/child/edu集成测试
releasenpm run build-release/child/edu预发布
productionnpm run build-production/child/edu线上

所有命令都带 cross-env NODE_OPTIONS=--openssl-legacy-provider,以兼容不同系统的 OpenSSL 版本。更重要的是,这套方案没有“要求运维多做一步”——构建产物天然携带 version.json,任何环境拿到包即可上线。

5. 测试与验证

我们定义了一个完整的回归流程,确保方案不会给测试和上线带来额外负担:

  1. 首次访问:打开 dev 环境页面,确认控制台打印版本号,Network 里能看到 version.json 且响应头无缓存;
  2. 触发新版本:调整任意文案,重新发布,保持旧页面不刷新;
  3. 轮询验证:30 秒内弹出提示框,展示当前/最新版本和环境;
  4. 交互路径

    • 点击“立即刷新”:页面强制 reload,新版本生效;
    • 点击“稍后刷新”:记录取消动作并重新开启轮询;
  5. 边界场景:切 tab / 清缓存 / 新设备访问 / 短时间连续发布,均能正确感知最新版本。

6. 注意事项与常见问题

现象可能原因解决方案
没有弹窗version.json 404 或版本未变检查部署路径、确认构建是否生成文件
弹窗后刷新仍旧版本静态资源被缓存核实 Nginx 缓存策略、查看浏览器缓存设置
构建失败cross-env 未安装或权限不足补充依赖、确保 Jenkins 工作目录可写
持续误报更新构建阶段多次生成版本号在 vue.config.js 顶部缓存 buildVersion 并全局复用

7. 落地成效

  • 旧页面用户在 30 秒内收到提醒,测试效率显著提升;
  • “幽灵弹窗”彻底消失,版本对比逻辑稳定;
  • 方案只触碰前端与 Nginx 配置,发布流程无需改造;
  • 文档化后,其他子应用无需重复思考,直接复用。

8. 展望

下一步我们计划:

  1. 封装通用 SDK:抽象版本生成、轮询、弹窗逻辑,支持 Vue CLI / Vite;
  2. 可视化版本面板:在主应用汇总所有环境的版本和发布时间;
  3. 差异化策略:针对高优先级版本强制刷新,普通版本允许用户自行选择。

这次实践让我再次意识到:真正的坑往往藏在看似“微不足道”的细节里。当我们把问题和思考写成文档、沉淀成模板,团队就能以更小的代价获得更稳定的交付。如果你也在推进微前端版本同步,欢迎交流、互相借鉴。


作者:鹏北海
来源:juejin.cn/post/7575006095389605897

收起阅读 »

女友去玩,竟带回一道 “虐哭程序员” 的难题

事件交代 前两周,女朋友去了开封玩一趟,回来后不久,她微信上和我吐槽了一件糟心事:由于她在两个平台都购了票,后面行程太赶,忘记在其中一个平台退票,导致现在无法退票 由于广州很多景点买票是要选择具体日期的(例如白云山、陈家祠),所以我以为她可能就是在抖音、美团...
继续阅读 »

蓝色搞怪动物表情职场打工人状态微信公众号封面.png


事件交代


前两周,女朋友去了开封玩一趟,回来后不久,她微信上和我吐槽了一件糟心事:由于她在两个平台都购了票,后面行程太赶,忘记在其中一个平台退票,导致现在无法退票


image.png


由于广州很多景点买票是要选择具体日期的(例如白云山、陈家祠),所以我以为她可能就是在抖音、美团买了同一天的两张票,后面忘记退掉抖音的票,导致现在票逾期,无法退款


但我还是想的太简单了,如果仅此而已,大家就不会看到这篇小破文


深入了解


随着我深入了解,才发现开封景区的购票大有不同,以下是我了解到的开封景区购票方式:


1️⃣ 你去哪个景点就买哪个景点的票,这种模式只适合去单个景区游玩,但开封景区特别多,这样买特别亏


2️⃣ 购买景区联票,开封这边景区基本是相互合作的,例如他们会出2个、4个、6个等的景区联票套餐,我女朋友就是先在抖音购买了4景区联票,后面她朋友又发现美团有6景区联票,玩起来更划算,所以才会出现了买两个平台的票的情况


抖音4景区联票:


image.png


美团6景区联票:


image.png


抖音4景区联票包含:万岁山武侠城+翰园碑林+开封城墙+铁塔公园


美团6景区联票包含:万岁山武侠城+翰园碑林+开封城墙+铁塔公园+龙亭景区+天波杨府


其中,美团的6景区联票中,龙亭景区天波杨府是抖音4景区联票中没有的,这两个信息特别关键


其次还有两个关键信息:


(1)平台承诺随时退:票虽然有使用截止日期,但在截止日期之前,是可以申请退款,如果截止日期仍未使用,系统自动退款


(2)女友的两个平台订单状态皆为已使用


那我开头揣测是票逾期,无法退款的情况就是错误的,既然如此,是什么情况导致了两个订单都为已使用呢?


逐层分析



我原本以为景区的核销订单模式会是:订单生成一个二维码,进入景区时有个机器扫描



如此一来,就可以精准锁定平台的订单核销,但我再次猜错了。女友和我说开封这边的景区都是使用了人脸识别技术,购票时只需要录入身-份-证号,到景区就能人脸识别进入了。


到这里事情已经逐渐清晰了,于是,我尝试站在一名程序员的角度来分析景区的核销流程



  • 第一步:识别用户身份


image.png



  • 第二步:识别用户身份成功,分析该用户是否在合作平台购买过相关景区票


image.png



  • 第三步:核销平台订单,也是最复杂的一步,因为该环节存在多种情况


1️⃣:该用户在一个平台购买了一个景区的票,这种情况最容易,用户进景区后,票务系统直接核销平台订单,核销完成平台的钱就流向票务系统;


2️⃣:该用户在一个平台购买了联合景区的票,此时锁定该票,等到用户玩完最后一个景区后,才核销订单;有人可能会存在疑惑,为什么不是玩第一个景区时就进行订单核销,而是等玩完全部景区才触发核销?这就涉及到多个平台卖票的问题,程序设计需要兼容多个平台:假设你在抖音购买了两景区联合票:万岁山+铁塔公园,在美团购买了两景区联合票:万岁山+开封城墙,你先去了万岁山,此时如果游玩第一个景区就要核销订单的话,该核销哪个平台的订单?是不是没法核销了,只有等第二个景区玩完你才知道要核销哪个平台的订单;但即使如此,也存在一种情况,就是用户只玩了一个景区,其他景区不去玩。我猜这种特殊情况下,该票就一直处于锁定未核销的状态,直到订单时间逾期,才自动核销


3️⃣:该用户在多个平台购买了一个景区的票,说实在这种情况属实矛盾,但作为一名程序员,还是得给解,我猜很有可能是以时间优先来核销订单的,也就是先在哪个平台下单,就先核销该平台的订单,毕竟用户肯定是希望核销日期更早的一张票


4️⃣:该用户在多个平台购买了联合景区的票,而我女朋友的情况就属于这一种情况,按理说这种情况也挺好解,还记得我前面提到的龙亭景区天波杨府吗?这两个景区是抖音平台4景区联票套餐中不包含的,而美团平台6景区联票中包含的,也就是说我女朋友她们去了其中一个,就可判断出核销美团平台的订单


但巧妙又凑合的一点就是她们的游玩路线居然是:万岁山武侠城->翰园碑林->开封城墙->铁塔公园->天波杨府->龙亭景区,恰巧把两个特殊的景区放在了最后游玩


当游玩万岁山武侠城->翰园碑林->开封城墙->铁塔公园时,对于抖音平台的订单来说,已经是满足触发核销的条件,我知道此时同为聪明程序员的你也会大有疑问🤔:美团平台的订单同样存在这4个景区,不应该直接就核销抖音平台的订单。


还记得情况3吗?也就是用户在多个平台购买了一个景区票的情况,我猜测的是票务系统以时间优先来核销不同平台的订单,再看看我开头贴出来的聊天记录,我女朋友也说了:先购买抖音的4景区联票,后购买美团的6景区联票


故此,以时间优先来核销的话,程序先核销抖音平台的订单确实没问题,当她们开心地继续畅玩天波杨府->龙亭景区时,却不知又触发了新一轮美团的订单锁单,待玩完这俩景区后,剩余的景点没玩,以至于三天逾期后自动核销订单,这也佐证了情况2里的猜测


复盘总结


后续打电话跟抖音平台核实了相关的情况,顺利完成了退票退款流程。女朋友还在一旁跟我聊着整个出行的趣事,但此时我满脑子都是在想着怎么修复票务系统中的bug🐛


一开始想的方案解决方案很粗暴:就是让景区提供一个特殊通道,用户可以通过打开二维码来核销平台订单,但这种也预防不了用户通过人脸识别进入的情况


后面想了一个从根本上解决的方案:用户人脸识别,检测到存在多个平台订单时,需要提示用户选择指定平台的订单进行核销,这样就可以彻底预防多个平台重复核销的情况了


今天的分享就到此结束,如果你对技术/行业交流有兴趣,欢迎添加howcoder微信,邀你进群交流


往期精彩


《这次终于轮到前端给后端兜底了🤣》


《你不了解的Grid布局》


《就你小子还不会 Grid布局是吧?》


《超硬核:从零到一部署指南》


《私活2年,我赚到了人生的第一桶金》


作者:howcode
来源:juejin.cn/post/7576086446459404323
收起阅读 »

你还在 for 循环里使用 await?异步循环得这样写

web
1. 前言 在循环中使用 await,代码看似直观,但运行时要么悄无声息地停止,要么运行速度缓慢,这是为什么呢? 本篇聊聊 JavaScript 中的异步循环问题。 2. 踩坑 1:for 循环里用 await,效率太低 假设要逐个获取用户数据,可能会这样写:...
继续阅读 »

1. 前言


在循环中使用 await,代码看似直观,但运行时要么悄无声息地停止,要么运行速度缓慢,这是为什么呢?


本篇聊聊 JavaScript 中的异步循环问题。


2. 踩坑 1:for 循环里用 await,效率太低


假设要逐个获取用户数据,可能会这样写:


const users = [1, 2, 3];
for (const id of users) {
const user = await fetchUser(id);
console.log(user);
}

代码虽然能运行,但会顺序执行——必须等 fetchUser(1) 完成,fetchUser(2) 才会开始。若业务要求严格按顺序执行,这样写没问题;但如果请求之间相互独立,这种写法就太浪费时间了。


3. 踩坑 2:map 里直接用 await,拿到的全是 Promise


很多人会在 map() 里用 await,却未处理返回的 Promise,结果踩了坑:


const users = [1, 2, 3];
const results = users.map(async (id) => {
const user = await fetchUser(id);
return user;
});
console.log(results); // 输出 [Promise, Promise, Promise],而非实际用户数据

语法上没问题,但它不会等 Promise resolve。若想让请求并行执行并获取最终结果,需用 Promise.all()


const results = await Promise.all(users.map((id) => fetchUser(id)));

这样所有请求会同时发起results 中就是真正的用户数据了。


4. 踩坑 3:Promise.all 一错全错


Promise.all() 时,只要有一个请求失败,整个操作就会报错:


const results = await Promise.all(
users.map((id) => fetchUser(id)) // 假设 fetchUser(2) 出错
);

如果 fetchUser(2) 返回 404 或网络错误,Promise.all() 会直接 reject,即便其他请求成功,也拿不到任何结果。


5. 更安全的替代方案


5.1. 用 Promise.allSettled(),保留所有结果


使用 Promise.allSettled(),即便部分请求失败,也能拿到所有结果,之后可手动判断成功与否:


const results = await Promise.allSettled(users.map((id) => fetchUser(id)));

results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("✅ 用户数据:", result.value);
} else {
console.warn("❌ 错误:", result.reason);
}
});

5.2. 在 map 里加 try/catch,返回兜底值


也可在请求时直接捕获错误,给失败的请求返回默认值:


const results = await Promise.all(
users.map(async (id) => {
try {
return await fetchUser(id);
} catch (err) {
console.error(`获取用户${id}失败`, err);
return { id, name: "未知用户" }; // 兜底数据
}
})
);

这样还能避免 “unhandled promise rejections” 错误——在 Node.js 严格环境下,该错误可能导致程序崩溃。


6. 现代异步循环方案,按需选择


6.1. for...of + await:适合需顺序执行的场景


若下一个请求依赖上一个的结果,或需遵守 API 的频率限制,可采用此方案:


// 在 async 函数内
for (const id of users) {
const user = await fetchUser(id);
console.log(user);
}
// 不在 async 函数内,用立即执行函数
(async () => {
for (const id of users) {
const user = await fetchUser(id);
console.log(user);
}
})();


  • 优点:保证顺序,支持限流

  • 缺点:独立请求场景下速度慢


6.2. Promise.all + map:适合追求速度的场景


请求间相互独立且可同时执行时,此方案效率最高:


const usersData = await Promise.all(users.map((id) => fetchUser(id)));


  • 优点:网络请求、CPU 独立任务场景下速度快

  • 缺点:一个请求失败会导致整体失败(需手动处理错误)


6.3. 限流并行:用 p-limit 控制并发数


若需兼顾速度与 API 限制,可借助 p-limit 等工具控制同时发起的请求数量:


import pLimit from "p-limit";
const limit = pLimit(2); // 每次同时发起 2 个请求
const limitedFetches = users.map((id) => limit(() => fetchUser(id)));
const results = await Promise.all(limitedFetches);


  • 优点:平衡并发和控制,避免压垮外部服务

  • 缺点:需额外引入依赖


7. 注意:千万别在 forEach() 里用 await


这是个高频陷阱:


users.forEach(async (id) => {
const user = await fetchUser(id);
console.log(user); // ❌ 不会等待执行完成
});

forEach() 不会等待异步回调,请求会在后台乱序执行,可能导致代码逻辑出错、错误被遗漏。


替代方案:



  • 顺序执行:用 for...of + await

  • 并行执行:用 Promise.all() + map()


8. 总结:按需选择


JavaScript 异步能力很强,但循环里用 await 要“按需选择”,核心原则如下:


需求场景推荐方案
需保证顺序、逐个执行for...of + await
追求速度、独立请求Promise.all() + map()
需保留所有结果(含失败)Promise.allSettled()/try-catch
需控制并发数、遵守限流p-limit 等工具

9. 参考链接



作者:冴羽
来源:juejin.cn/post/7569402861802782730
收起阅读 »

为什么有些人边框不用border属性

web
1) border 会改变布局(占据空间) border 会参与盒模型,增加元素尺寸。 例如,一个宽度 200px 的元素加上 border: 1px solid #000,实际宽度会变成: 200 + 1px(left) + 1px(right) = 202...
继续阅读 »

1) border 会改变布局(占据空间)


border 会参与盒模型,增加元素尺寸。


例如,一个宽度 200px 的元素加上 border: 1px solid #000,实际宽度会变成:


200 + 1px(left) + 1px(right) = 202px

如果不想影响布局,就很麻烦。


使用 box-shadow: 0 0 0 1px #000不会改变大小,看起来像 border,但不占空间。




2) border 在高 DPI 设备上容易出现“模糊/不齐”


特别是 0.5px border(发丝线),在某些浏览器上有锯齿、断线。


transform: scale(0.5) 或伪元素能做更稳定的发丝线。




3) border 圆角 + 发丝线 常出现不规则效果


border + border-radius 在不同浏览器的渲染不一致,容易出现不均匀、颜色不一致的问题。


outline / box-shadow 圆角更稳定。




4) border 不适合做阴影/多层边框


如果你需要两层边框:


双层边框用 border 很难做

而用:


box-shadow: 0 0 0 1px #333, 0 0 0 2px #999;

非常简单。




5) border 和背景裁剪一起用时容易出 bug


比如 background-clipoverflow: hidden 配合 border 会出现背景被挤压、不应该被裁剪却裁剪等问题。




6) hover/active 等状态切换时会“跳动”


因为 border 会改变元素大小。


例子:


.btn { border: 0; }
.btn:hover { border: 1px solid #000; }

鼠标移上去会抖动,因为尺寸变大了。


box-shadow 的话就不会跳。


总结


边框可以分别使用border、outline、box-shadow三种方式去实现,其中outline、box-shadow不会像border一样占据空间。而box-shadow可以用来解决两个元素相邻时边框变宽的问题。不使用border并不是因为它不好,而是因为outline和box-shadow的兼容性和灵活性相对border会更好一点。


作者:爆浆麻花
来源:juejin.cn/post/7575065042158633010
收起阅读 »

老乡鸡也开源?我用 Trae SOLO 做了个像老乡鸡那样做饭小程序!

web
大家好,我是不如摸鱼去,欢迎来到我的 AI 编程分享专栏。 去年,「老乡鸡不装了,直接开源」的消息引发了广泛的关注。我也纳闷,老乡鸡不是做菜的吗,开的哪门子源?仔细看了下原来是把他们的菜品、溯源报告这些开源了。然后,GitHub 上这个叫「像老乡鸡那样做饭」的...
继续阅读 »

大家好,我是不如摸鱼去,欢迎来到我的 AI 编程分享专栏。


去年,「老乡鸡不装了,直接开源」的消息引发了广泛的关注。我也纳闷,老乡鸡不是做菜的吗,开的哪门子源?仔细看了下原来是把他们的菜品、溯源报告这些开源了。然后,GitHub 上这个叫「像老乡鸡那样做饭」的项目火了,如今 star 数量已经达到了 18k,这是它的地址:github.com/Gar-b-age/C…


作为一名爱做饭的程序员,面对如此诱人的开源资源,怎能袖手旁观?我选择用 Trae 快速构建了一个像老乡鸡那样做饭小程序! 本文将分享我如何利用 Trae SOLO 的高效开发能力,把这份“开源美味”封装成便捷的小程序。


开源


像老乡鸡那样做饭小程序已开源,参见文章TRAE SOLO 正式发布了?我用它将像老乡鸡那样做饭小程序开源了!


实现效果



技术栈


我们开发前选择好开发的技术栈,这样 AI 可以在我们规划好的路线上进行开发,可以达到事半功倍的效果。


前端技术栈


因为我本身就是一个前端程序员,所以前端技术栈比较熟,直接选择常用的技术栈:



后端技术栈


服务端最好是可以免开发,于是我选择了 TRAE SOLO 集成的 Supabase 作为我们的云端服务。



Supabase 是一个开源的 Firebase 替代品,旨在帮助开发者快速构建后端功能。它基于 PostgreSQL 数据库,并提供实时订阅、身份验证、存储、边缘函数等功能,支持 REST 和 GraphQL API。Supabase 强调开源、可自托管,并提供免费起步的云端服务,适合构建现代 Web 和移动应用。



菜谱数据来源


我们的菜谱数据来自于开源项目「像老乡鸡那样做饭」,这是它的 GitHub 地址:github.com/Gar-b-age/C…


动手


我们选择好技术栈之后就可以开始开发,由于我们使用 Supabase 作为服务,所以后端开发无需操心,只需要让 Trae SOLO 来建表、处理数据就好了。


数据处理


我们将菜谱的 markdown 和相关图片放到了 cook-book 目录下,然后让 Trae SOLO 开始处理吧!



Trae SOLO 开始处理需求,生成 tasks 列表,并执行





不过它也不是一步到位了,我发现导入的数据有的配料字段是空的,有的步骤是空的,于是让它重新检查了下(后面我自己检查了下发现是部分菜谱的 markdown 文件的标题不对,这里我就自己处理了)。



最终,Trae SOLO 帮我将全部的菜谱数据处理完毕,并插入到 Supabase 的数据库中了,接近 200 道菜,足够每天吃一道了。



小结


“干净”的数据能达到事半功倍的效果,从上面纠错的过程可以印证这一点,在让 AI 处理前,花点时间做基础的数据清洗(统一文件命名规范、检查必要字段是否存在、清理异常字符)是非常值得的投入。


小程序开发


我们开发前,有一些准备工作,由于 wot-starter 中包含暗黑模式等相关的配置,我们本次暂不需要,故需要移除,以免干扰 AI 对项目的理解(这里我们要明确一个点,要尽量提供有用的语料给 AI,因为过大的上下文会导致它天马行空)。



我们向 Trae SOLO 提出以下需求,先实现一个简易版:


开发一个像老乡鸡那样做饭小程序,基于现有表结构实现以下核心功能:

1. 分类浏览功能:按照菜品分类展示菜谱列表

2. 首页推荐功能:在首页展示精选推荐的3-5个菜谱

3. 菜谱详情页:点击可查看完整菜谱信息

要求:

- 保持现有表结构不变

- 界面设计简洁直观

- 确保数据加载流畅

- 适配移动端显示

- 使用unocss编写样式,使用rpx做单位

经过一轮开发后,项目结构如下:



不过此时项目还是跑不起来的,控制台报错了,我们直接将控制台报错发送给 Trae SOLO。



解决之后,我们的小程序启动起来,效果就已经差不多达到了文章开头的样子了。


当然还有一些小问题,都可以让 Trae SOLO 来处理



后续完善


初版完成后,我们群里的好朋友 FliPPeDround 提醒我说,「像老乡鸡那样做饭」项目的 Github 上有 PR 提供了做菜的手绘流程图。那太好了,我们这就给加上。


首先还是让 Trae SOLO 将新增的手绘流程图上传到 Supabase 并将其地址插入到对应的菜谱中



然后在菜谱详情中展示手绘流程图



效果如图,配上流程图,清晰又美观。



总结


我们今天几乎零代码 用 Trae SOLO 实现了「像老乡鸡那样做饭」小程序,过程中这三个规则让我们事半功倍:



  • 保持数据干净:“干净”的数据能达到事半功倍的效果,上面处理数据时纠错的过程可以印证这一点,在让 AI 处理前,花点时间做基础的数据清洗(统一文件命名规范、检查必要字段是否存在、清理异常字符)是非常值得的投入。

  • 纯净上下文:为 AI 提供一个相对“纯净”的、与当前任务高度相关的上下文环境,这样可以让 AI 在我们规划好的路线上进行开发,避免“天马行空”。

  • 增量式沟通:尽量做到增量式沟通,不要一次性把所有需求都丢给 AI。先实现核心功能,跑通后再提新需求,比如我们添加手绘流程图的功能。每次交互都基于当前已完成的代码状态,让 AI 能“看到”它之前的成果,更容易理解下一步要做什么。


好了,实现这个小程序后,我再也不愁没菜吃了!后面我想还可以加一些有趣的功能,例如:今天吃什么、每周必吃、大家都在吃,等等功能,当然这些功能也是由 Trae 来开发,大家可以期待下,同时也期待未来 AI 编程能给我们带来更多、更强的能力,让我们能专注于更重要的「业务逻辑」。


参考资料



往期精彩


当年偷偷玩小霸王,现在偷偷用 Trae Solo 复刻坦克大战


告别 HBuilderX,拥抱现代化!这个模板让 uni-app 开发体验起飞


uni-app 还在手写请求?alova 帮你全搞定!


uni-app 无法实现全局 Toast?这个方法做到了!


Vue3 uni-app 主包 2 MB 危机?1 个插件 10 分钟瘦身


欢迎评论区沟通、讨论👇👇


作者:不如摸鱼去
来源:juejin.cn/post/7554225547117576243
收起阅读 »

我为什么在团队里,强制要求大家用pnpm而不是npm?

web
最近,我在我们前端团队里推行了一个“强制性”的规定:所有新项目,必须使用pnpm作为包管理工具;所有老项目,必须在两个月内,逐步迁移到pnpm。 这个决定,一开始在团队里是有阻力的。 有同事问:“老大,npm用得好好的,为啥非要换啊?我们都习惯了。” 也有同事...
继续阅读 »

image.png


最近,我在我们前端团队里推行了一个“强制性”的规定:所有新项目,必须使用pnpm作为包管理工具;所有老项目,必须在两个月内,逐步迁移到pnpm


这个决定,一开始在团队里是有阻力的。


有同事问:“老大,npm用得好好的,为啥非要换啊?我们都习惯了。”


也有同事说:“yarn不也挺快的吗?再换个pnpm,是不是在瞎折腾?”


我理解大家的疑问。但我之所以要用“强制”这个词,是因为在我看来,在2025年的今天,继续使用npm或yarn,就像是明明有高铁可以坐,你却非要坚持坐绿皮火车一样,不是不行,而是没必要。


这篇文章,我就想把我的理由掰开揉碎了,讲给大家听。




npm和yarn的“原罪”:那个又大又慢的node_modules


在聊pnpm的好处之前,我们得先搞明白,npm和yarn(特指yarn v1)到底有什么问题。


它们最大的问题,都源于一个东西——扁平化的node_modules


你可能觉得奇怪,“扁平化”不是为了解决npm v2时代的“依赖地狱”问题吗?是的,它解决了老问题,但又带来了新问题:


1. “幽灵依赖”(Phantom Dependencies)


这是我最不能忍受的一个问题。


举个例子:你的项目只安装了A包(npm install A)。但是A包自己依赖了B包。因为是扁平化结构,B包也会被提升到node_modules的根目录。


结果就是,你在你的代码里,明明没有在package.json里声明过B,但你却可以import B from 'B',而且代码还能正常运行!


这就是“幽灵依赖”。它像一个幽灵,让你的项目依赖关系变得混乱不堪。万一有一天,A包升级了,不再依赖B了,你的项目就会在某个意想不到的地方突然崩溃,而你甚至都不知道B是从哪来的。


2. 磁盘空间的巨大浪费


如果你电脑上有10个项目,这10个项目都依赖了lodash,那么在npm/yarn的模式下,你的磁盘上就会实实在在地存着10份一模一样的lodash代码。


对于我们这些天天要开好几个项目的前端来说,电脑的存储空间就这么被日积月累地消耗掉了。


3. 安装速度的瓶颈


虽然npm和yarn都有缓存机制,但在安装依赖时,它们仍然需要做大量的I/O操作,去复制、移动那些文件。当项目越来越大,node_modules动辄上G的时候,那个安装速度,真的让人等到心焦。




pnpm是怎么解决这些问题的?——“符号链接”


好了,现在主角pnpm登场。pnpm的全称是“performant npm”,意为“高性能的npm”。它解决上面所有问题的核心武器,就两个字:链接


pnpm没有采用扁平化的node_modules结构,而是创建了一个嵌套的、有严格依赖关系的结构


1. 彻底告别“幽灵依赖”


在pnpm的node_modules里,你只会看到你在package.json明确声明的那些依赖。


你项目里依赖的A包,它自己所依赖的B包,会被存放在node_modules/.pnpm/这个特殊的目录里,然后通过 符号链接(Symbolic Link) 的方式,链接到A包的node_modules里。


这意味着,在你的项目代码里,你根本访问不到B包。你想import B?对不起,直接报错。这就从结构上保证了,你的项目依赖关系是绝对可靠和纯净的。


2. 磁盘空间的“终极节约”


pnpm会在你的电脑上创建一个“全局内容可寻址存储区”(content-addressable store),通常在用户主目录下的.pnpm-store里。


你电脑上所有项目的所有依赖,都只会在这个全局仓库里,实实在在地只存一份


当你的项目需要lodash时,pnpm不会去复制一份lodash到你的node_modules里,而是通过 硬链接(Hard Link) 的方式,从全局仓库链接一份过来。硬链接几乎不占用磁盘空间。


这意味着,就算你有100个项目都用了lodash,它在你的硬盘上也只占一份的空间。这个特性,对于磁盘空间紧张的同学来说,简直是福音。


3. 极速的安装体验


因为大部分依赖都是通过“链接”的方式实现的,而不是“复制”,所以pnpm在安装依赖时,大大减少了磁盘I/O操作。


它的安装速度,尤其是在有缓存的情况下,或者在安装一个已经存在于全局仓库里的包时,几乎是“秒级”的。这种“飞一般”的感觉,一旦体验过,就再也回不去了。




为什么我要“强制”?


聊完了技术优势,再回到最初的问题:我为什么要“强制”推行?


因为包管理工具的统一,是前端工程化规范里最基础、也最重要的一环。


如果一个团队里,有人用npm,有人用yarn,有人用pnpm,那就会出现各种各样的问题:



  • 不一致的lock文件package-lock.json, yarn.lock, pnpm-lock.yaml互相冲突,导致不同成员安装的依赖版本可能不完全一致,引发“在我电脑上是好的”这种经典问题。

  • 不一致的依赖结构:用npm的同事,可能会不小心写出依赖“幽灵依赖”的代码,而用pnpm的同事拉下来,代码直接就跑不起来了。


在一个团队里,工具的统一,是为了保证环境的一致性和协作的顺畅。而pnpm,在我看来,就是当前这个时代下,包管理工具的“最优解”。


所以,这个“强制”,不是为了搞独裁,而是为了从根本上提升我们整个团队的开发效率和项目的长期稳定性。




最后的经验


从npm到yarn,再到pnpm,前端的包管理工具一直在进化。


pnpm用一种更先进、更合理的机制,解决了过去遗留下的种种问题。它带来的不仅仅是速度的提升,更是一种对“依赖关系纯净性”和“工程化严谨性”的保障。


我知道,改变一个人的习惯很难。但作为团队的负责人,我有责任去选择一条更高效、更正确的路,然后带领大家一起走下去。


如果你还没用过pnpm,我强烈建议你花十分钟,在你的新项目里试一试🙂。


作者:ErpanOmer
来源:juejin.cn/post/7530180321619656745
收起阅读 »

我做了套小红书一键发布系统,运营小姐姐说她不想离开我了

web
运营小姐姐的烦恼 在我们公司有一个运营部门, 平常要负责把内容发布到抖音、快手、小红书等这些平台。账号类型也很多,有品牌号、合作号、还有专门用来做活动的测试号。 日常的工作量其实不小。尤其一到促销期或者活动期,一天要发好几条笔记,而且要同时覆盖不同的账号。 每...
继续阅读 »

运营小姐姐的烦恼


在我们公司有一个运营部门, 平常要负责把内容发布到抖音、快手、小红书等这些平台。账号类型也很多,有品牌号、合作号、还有专门用来做活动的测试号。 日常的工作量其实不小。尤其一到促销期或者活动期,一天要发好几条笔记,而且要同时覆盖不同的账号。


每次到了活动期,运营小姐姐的电脑上都是好几台手机和几十张图 + N 份文案堆在一起,一天下来要发好几条笔记,还得确保不同账号内容不重样、不出错。她经常一边切账号一边自言自语:“这张图是给哪个号的来着?我刚才发的是品牌号还是活动号?”


以前我们运营部门的流程是这样的:



策划把图文和文案打包发给运营,运营打开文件,一条条复制粘贴、上传图片、切换账号、点击发布……

多账号的时候还得来回切换登录,有时候内容顺序搞错了,还得手动撤回重发,前功尽弃。



这种方式不仅效率低下、容易出错,而且让人精神高度紧张,运营小姐姐经常下班后还得回群里道歉:“刚才发错平台了,我重新发一遍……”




技术出手,只为运营小姐姐少加点班


我每天下班的时候都能看到运营小姐姐还坐在工位上加班,发内容、切账号、贴文案、盯发布进度,一天反反复复几十次。到了活动高峰期,还得深夜返工,改图换文案,一不小心还会贴错内容、发错账号,前功尽弃。


面对运营小姐姐每天这样重复性极高的内容工作流程,我还是看不下去了:


我决定要用技术打破这一切机械操作。


于是我打算做一套 “内容统一管理 + 多平台分享集成” 的系统。


所有图文内容、发布素材和话题信息,运营可以提前在后台配置好。确认无误后,只需点击“一键生成分享链接”,把链接分享到对应设备上,打开就能自动跳转各个平台的发布界面,文案和图片都自动填充,彻底告别人工复制粘贴。


从“人工发图”变成“链接跳转”,

从“复制粘贴”变成“一键分发”,

用一行代码,换运营十分钟。


运营小姐姐只需要点一下,就能完成过去十几分钟的手工操作。把效率提上来,错误率降下去,让小姐姐能准点下班。




如何实现一键发布小红书笔记


那本篇文章我们就先单独讲一下如何实现用一条链接一键直接将图文和视频内容分享到小红书 App 笔记笔记里的功能。


我们先来看一下平常在小红书 App 手动发布笔记的流程 通常会先点击首页下方的加号按钮 选择要上传的图片或视频 然后输入标题和正文 再选择相关话题 确认无误后点击发布按钮完成发布 下面我放一张示意图方便大家直观理解整个流程


b9e0c38ec83ce3f42c76cb2051445a70.jpg


如果是多账号运营,这个过程就要重复好几次,效率非常低。等我们设计好架构接入技术之后,我们就能把整个流程程序化,把要发布的标题、内容、图片集中整理到一个统一的后台页面。运营先添加预发布的文案,在后台点击预览确认没有问题后,就可以生成一个分享链接,再把链接发给其他负责发布的同学。然后打开链接就能直接预览,并且一键快速发布,这就是我们希望实现的最终效果。




实现一键发布前的后台准备


由于项目真实后台涉及敏感配置和接口调用,这里我们用一个简单后台来说明功能设计流程。以下是一个我自己简单搭建的小红书文案管理后台,主要分为两个页面:文案列表页和发布页,分别用于管理文案和录入新文案。


我们先来看文案列表页,如下图展示:


image.png


它的主要功能是展示所有待发布或已发布的文案记录。每条文案包括封面图、标题、正文内容、图文数量,以及一组操作按钮,支持“复制分享链接”、“预览”、“编辑”和“删除”。通过这些操作,我们可以快速查阅、预览和维护已有文案。


右上角是搜索框和「新建文案」按钮,运营可以通过关键词快速查找历史文案,也可以点击新建按钮跳转到发布页。


下面是发布页的示意图,如图所示:


image.png


在这个页面中,我们可以填写文案的标题和正文内容,正文支持 Markdown 格式,方便排版。接着我们可以从素材库中选择需要发布的图片或视频内容。填写完成后,点击底部的「发布文案」按钮,即可提交到后台数据库中。


整个后台的核心流程是:运营先在发布页准备好图文内容并保存,之后可以回到列表页复制分享链接,将链接发送给需要发布的同学,对方点击链接即可跳转到预览页,一键发布到小红书 App笔记。这种方式大大简化了手动发布流程,尤其适合多账号分工运营场景,能极大提升效率,减少重复劳动。


后台文案管理功能设计


为了更方便地管理和发布图文内容,我们设计了一个简单的小红书后台系统。后台主要由两个核心页面组成:文案列表页发布页,分别用于查看、维护和录入要发布的图文素材。


后台的数据存储基于一张 x_post 表,结构设计如下:


CREATE TABLE `x_post` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`type` varchar(20) NOT NULL COMMENT '笔记类型 normal | video',
`title` varchar(255) DEFAULT NULL COMMENT '标题',
`content` text COMMENT '正文内容',
`cover_url` varchar(500) DEFAULT NULL COMMENT '封面图',
`image_urls` text COMMENT '图片列表,JSON数组格式',
`video_url` varchar(500) DEFAULT NULL COMMENT '视频地址',
`is_deleted` tinyint(1) DEFAULT 0 COMMENT '是否删除 0=未删除 1=已删除',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='发布笔记表';

这张表记录了每篇小红书笔记的关键信息,包括标题、内容、封面、图文或视频链接、删除时间和是否删除等字段。其中 image_urls 是一个 JSON 数组格式,便于存储多图信息,type 字段区分图文和视频类型。


内容创建完后,运营小姐姐就可以通过后台的发布页录入一篇新文案,然后在列表页点击「预览」按钮来查看内容展示效果。(其实我们还可以支持一键导入文案素材的功能)


例如下图就是我们第一篇文案的预览效果页面:


image.png


image.png


这个预览页面完全模拟小红书 App 内的发布效果,顶部是封面图和内容图集,下面是正文,包括 emoji、换行、图文排版、交通路线推荐等内容。


如果是多账号运营,传统做法需要登录多个账号重复上传素材和手动复制粘贴内容,非常低效。而接入我们这个系统后,运营只需提前在后台准备好文案并生成预览链接,分发给不同的发布同事即可。对方打开链接后可以直接查看效果,并一键完成发布操作。


这大大提升了运营效率,尤其是在有多个账号需要同步发布或定时推送内容的场景中,非常实用。


这就是我们后台的整体设计思路。接下来,我们将详细介绍如何与小红书 App 进行联动,实现一键直达发布页的功能。通过技术手段进一步简化发布流程,实现图文内容从准备到落地的一体化闭环。




接入小红书分享平台 API


为了实现我们前面提到的“一键跳转到小红书发布页”的能力,我们需要借助小红书官方提供的 分享开放平台。它主要面向内容型或工具型的合作应用,提供了 SDK 和相关接口,支持 Android、iOS、HarmonyOS 和 JS 等多种平台。


从官方说明来看,当前支持的能力包括:



  1. 一键分享:通过开放的接口,第三方 App 或网页可以直接将图文、视频内容分享到小红书,免去了手动复制粘贴和跳转操作,极大提高了分享效率。

  2. 快速发布:在唤起小红书 App 后,系统会自动识别我们传入的内容类型,比如是图文还是视频。它还支持在发布页挂载话题标签,并根据内容自动推荐标题或话题。这项功能目前仅在 Android 和 iOS 平台支持,HarmonyOS 暂不支持。

  3. 活动运营联动:我们还可以联动小红书站内的活动,比如双十一、出游季等主题活动。在分享内容时,用户可以直接跳转到这些活动页面,提升曝光率和互动率。不过需要注意的是,这项能力目前同样只在 Android 和 iOS 平台可用。


这些功能意味着,我们不需要自行设计复杂的内容编辑器,也不需要从零搭建视频发布模块,只需准备好基础内容,通过官方提供的 SDK 唤起小红书 App,即可完成整个跳转发布流程。


接入流程


下面是我们使用小红书JS SDK整个接入流程的概览图:


image.png


从图中可以看出,整个分享流程包括如下关键步骤:



  1. 第三方开发者在开放平台官网申请注册应用,平台审核后分配 appKey 和 appSecret;

  2. 开发者服务端保管好这些凭证,并在需要调用时计算签名;

  3. 签名参数(nonce、sign、timestamp)将下发给前端页面;

  4. 页面通过 JS SDK 唤起小红书 App,完成内容分享跳转;

  5. 后台可通过 openAPI 配合 JS-SDK 进行鉴权校验。


鉴权签名流程


image.png


整个 JS SDK 鉴权流程主要包括以下三步:



  1. 第一步:获取 access_token

    第一次签名操作,使用 appKey、appSecret 等生成加签参数,向 openAPI 请求 access_token

  2. 第二步:生成 JS 签名 signature

    获取到 access_token 后,使用 appKey + nonce + timestamp + access_token 进行再次签名,生成 JS SDK 所需的 signature

  3. 第三步:返回签名参数给前端

    服务端将 appKeynoncetimestampsignature 返回给第三方前端,前端可使用 JS SDK 调用分享方法,并唤起小红书 App。


发布分享


在完成了前面的接入流程和鉴权签名后,我们最终要调用的是小红书 JS SDK 提供的分享方法 xhs.share(),这个方法可以让我们唤起小红书 App,并快速填充好要发布的内容。


调用格式大致如下:


xhs.share({
shareInfo: {
type: 'normal', // 笔记类型,图文用 'normal',视频用 'video'
title: '...', // 笔记标题(可选)
content: '...', // 笔记正文内容(可选)
images: ['...'], // 图文笔记必填,必须是公网图片地址
video: '...', // 视频笔记必填,必须是公网视频地址
cover: '...', // 视频封面图,必须是公网地址
},
verifyConfig: {
appKey: '...', // 平台分配的应用唯一标识
nonce: '...', // 服务端生成的随机字符串
timestamp: '...', // 服务端生成的时间戳
signature: '...', // 服务端生成的签名
},
fail: (e) => {
// 调用失败时的处理逻辑
}
})

这里的 shareInfo 是我们希望填入小红书发布页面的内容信息,主要包括笔记类型(图文 or 视频)、标题、正文、图片或视频地址等;而 verifyConfig 是服务端提前生成的签名鉴权参数,必须由服务器返回,不能由前端自己生成。


下面是每个字段的含义说明:


字段名是否必填类型说明
typeString笔记类型,支持 normal(图文)和 video(视频)
titleString笔记标题
contentString笔记文字内容
imagesArray图文类型必填,图片列表,必须是公网地址,不支持本地路径
videoString视频类型必填,视频文件地址,必须是公网地址
coverString视频封面图,必须是公网地址
appKeyString应用在小红书开放平台申请到的唯一标识
nonceString服务器生成的随机字符串,用于签名
timestampString服务器生成的时间戳,用于签名
signatureString服务器生成的签名结果
failFunction分享失败时的回调方法,可用于提示或埋点

需要注意的是,小红书的 xhs.share() 方法内置了鉴权流程,我们只需要提供服务端生成的 noncetimestampsignature,前端不需要自己参与签名过程。


上面我们已经介绍了接入小红书分享平台 JS SDK 的整个流程,包括平台能力、签名机制以及前后端的配合方式。


接下来我将通过一个简化版本的服务端接口与前端 HTML 页面,来实现一个最小闭环的一键发布功能 Demo,帮助大家快速理解整个流程的落地实现。




服务端接口设计:文案预览与一键发布签名生成


在服务端的逻辑里,我们通常只需要实现两个接口:一个是文案预览接口,另一个是一键发布时用到的分享参数生成接口


文案预览接口的作用是:当我们从后台生成了一个分享链接发给同事后,对方打开这个链接时,前端需要能展示出这条小红书文案的内容,比如标题、正文、图片等信息。这时候页面会携带一个唯一的文案 ID(通常是链接里的参数),服务端需要根据这个 ID 去数据库查一下这条文案是否存在、有没有被下架等状态校验,然后把文案信息返回给前端做展示。


而一键发布接口的作用是:当用户点击“立即发布”按钮后,前端需要去服务端调用一个接口,这个接口的任务是再次校验文案是否有效,并根据我们接入小红书分享平台的要求,生成所需的签名参数(比如 appKey、nonce、timestamp、signature 等),这些参数会被前端 JS-SDK 拿去唤起小红书 App,实现真正的跳转和发布功能。


接口一:文案预览接口


在我们的后台中,每一条小红书文案都支持“点击生成分享链接”的功能。这个分享链接通常会附带一个唯一标识 ID,例如:


https://xxxx.com/share.html?_id=ABY1boZllzPlTiq2

这个 _id 就是我们为每一条文案生成的唯一标识,目的是为了让运营或其他同事拿到链接后可以打开预览页面,并看到这条笔记的内容细节。


我们服务端的预览接口设计也非常简单,主要逻辑如下:


控制器代码


/**
* 预览接口
* 示例:GET /api/post/preview/1
*/

@GetMapping("/preview/{id}")
public ResponseEntity<XPost> preview(@PathVariable Long id) {
return ResponseEntity.ok(postService.previewPost(id));
}

为了方便演示,这里我们使用的是通过 GET 请求访问接口,并直接返回数据库中的文案信息。实际项目中可以根据自己的需求使用更安全或更复杂的方式来处理分享链接的鉴权与数据访问


Service 层逻辑


public XPost previewPost(Long id) {
return postRepository.findById(id)
.orElseThrow(() -> new RuntimeException("内容不存在"));
}

这个方法会根据文案 ID 去数据库中查询对应的数据。如果找不到,就直接抛出异常提示“内容不存在”。


通过这个接口,我们就可以在分享链接打开时,自动去服务端拉取对应文案的数据,并渲染在页面中,方便运营预览确认。


前端预览页面展示效果


在前面我们已经通过服务端接口拿到了某条文案的详细内容(如图片列表、正文文案等)。这一步我们会基于这些数据,用一个简单的 HTML 页面来完成“预览页面”的渲染展示。


这个页面的作用是:当我们在后台生成分享链接并复制给运营后,运营或用户可以通过这个链接在浏览器中打开文案预览页面,确认发布内容是否正确。如果确认无误,还可以点击“一键发布”按钮,跳转至小红书 App 完成最终发布。


示例展示效果


如下图所示,这是我们通过微信打开渲染出来的预览页面效果:


3e8b63f544912158359ed4a203eb8b0d.jpg


我这个demo页面使用了原生 HTML + Tailwind CSS + Swiper 轮播图组件来实现这个页面。代码逻辑非常简单:



  • 页面加载时调用服务端接口 /api/post/preview/{id} 获取文案内容;

  • 根据接口返回的 imageUrls 渲染轮播图;

  • 根据 content 渲染正文文案;

  • 点击“发布到小红书”按钮,跳转到下一步一键分享页面。


下面是部分部分代码片段:


<!-- 图片轮播区域 -->
<div class="swiper">
<div class="swiper-wrapper" id="preview-images"></div>
<div class="swiper-pagination"></div>
</div>

<!-- 文案内容 -->
<div class="xhs-text" id="preview-text">加载中...</div>

<!-- 一键发布按钮 -->
<button class="btn-publish" onclick="publishPost()">发布到小红书</button>

我们通过简单的轮播图 + 段落渲染的方式,让用户在手机上也可以便捷查看预览效果。整个页面在移动端有良好的显示效果,支持微信、浏览器等环境打开。


接口二:一键发布接口:生成跳转小红书所需参数


在完成了预览页面后,接下来我们要实现的就是 一键发布接口。这个接口的核心目标是:

当用户在预览页面点击“发布到小红书”按钮后,后端生成一组必要的签名参数,并返回给前端,前端再用这组参数跳转到小红书 App 完成笔记发布。


接口定义


我们定义了如下接口来支持这个过程:


/**
* 发布接口
* 示例:GET /api/post/publish/1
*/

@GetMapping("/publish/{id}")
public ResponseEntity<Map<String, Object>> publish(@PathVariable Long id) {
Map<String, Object> result = postService.buildPublishResult(id);
return ResponseEntity.ok(result);
}

这个接口中,id 表示文案在数据库中的主键 ID。我们通过这个 ID 查询文案内容,并为其生成一组带签名的跳转参数。


配置小红书开放平台的 AppKey 和 AppSecret


在 Spring Boot 的 application.yml 配置文件中,我们提前配置好了小红书开放平台的 appKeyappSecret,用于生成签名参数:


# 小红书分享开放平台配置
xhs:
app-key: xxxxxxx
app-secret: xxxxxxx

核心方法:构建发布参数


接口底层调用的逻辑是:


public Map<String, Object> buildPublishResult(Long id) {
XPost post = previewPost(id); // 获取笔记内容

// 生成签名部分
Map<String, Object> data = generateSignature(id);

// 构建分享内容
Map<String, Object> shareInfo = new HashMap<>();
shareInfo.put("type", post.getType());
shareInfo.put("title", post.getTitle());
shareInfo.put("content", post.getContent());
shareInfo.put("images", ImageUtils.parseImages(post.getImageUrls()));
shareInfo.put("cover", Optional.ofNullable(post.getCoverUrl())
.orElseGet(() -> ImageUtils.getFirstImage(post.getImageUrls())));
shareInfo.put("time", System.currentTimeMillis());
shareInfo.put("_id", "xhs_" + post.getId());

Map<String, Object> result = new HashMap<>();
result.put("data", data); // 签名字段
result.put("shareInfo", shareInfo); // 文案内容
return result;
}

/**
* 生成小红书发布参数签名
* @param postId 文案ID
* @return appKey + nonce + timestamp + signature
*/

public Map<String, Object> generateSignature(Long postId) {
// 查询是否存在
XPost post = postRepository.findById(postId)
.orElseThrow(() -> new RuntimeException("内容不存在"));

String appKey = xhsProperties.getAppKey();
String appSecret = xhsProperties.getAppSecret();

String nonce = UUID.randomUUID().toString().replace("-", "").substring(0, 12);
String timestamp = String.valueOf(System.currentTimeMillis());

try {
String signature = SignatureUtil.buildSignature(appKey, nonce, timestamp, appSecret);

Map<String, Object> result = new HashMap<>();
result.put("appKey", appKey);
result.put("nonce", nonce);
result.put("timestamp", Long.parseLong(timestamp));
result.put("signature", signature);
return result;

} catch (Exception e) {
throw new RuntimeException("生成签名失败: " + e.getMessage(), e);
}
}

接口返回的结果分为两部分:



  • data 部分:用于跳转小红书所需的签名参数,包括 appKeynonce(随机字符串)、timestamp(时间戳)、signature(签名值),这些字段将作为 xhs.share() 方法中的 verifyConfig 参数传入;

  • shareInfo 部分:是我们准备要发布的笔记内容,包括 type(图文 or 视频)、title(标题)、content(正文)、images(图片地址列表)、cover(封面图)等信息,用于在前端渲染展示和传递给小红书 SDK。


我们测试时实际返回的结构如下所示:


{
"shareInfo": {
"cover": "http://110.42.233.124/images/dishini.jpg",
"images": [
"http://110.42.233.124/images/dishini.jpg",
"http://110.42.233.124/images/dishini2.jpg",
"http://110.42.233.124/images/dishini3.jpg"
],
"time": 1758188642421,
"_id": "xhs_1",
"type": "normal",
"title": "一张图讲清楚上次迪士尼交通攻略🚇",
"content": "一张图讲清楚上次迪士尼交通攻略🚇\n有多少宝子去迪士尼光是研究交通就费了好大劲。。。"
},
"data": {
"signature": "2486268ecfa3886b7fc4cd1d1fcbda5381b3177f96daf5f1e4dd09b938f94d4f",
"appKey": "red.96juc55xxxx",
"nonce": "10e07cfc400c",
"timestamp": 1758188642420
}
}

前端拿到这组数据后,只需要将 shareInfodata 传入小红书 SDK 的 xhs.share() 方法中,即可完成跳转与笔记预填。


签名生成工具类:SignatureUtil.java


小红书分享 SDK 的调用需要进行签名认证,为此我们封装了一个简单的工具类,用于生成 SHA256 签名,核心思路是:



  1. appKeynoncetimestamp 等参数排序;

  2. 拼接成 key=value&key2=value2... 的格式后追加 appSecret

  3. 使用 SHA-256 进行加密并返回十六进制字符串。


/**
* 小红书签名生成工具类
*/

public class SignatureUtil {

/**
* 构建签名
*
* @param appKey 应用唯一标识
* @param nonce 随机字符串(建议12位)
* @param timestamp 当前时间戳(毫秒)字符串格式
* @param appSecret 应用密钥
* @return 签名字符串(SHA-256 Hex)
*/

public static String buildSignature(String appKey, String nonce, String timestamp, String appSecret) throws Exception {
Map<String, String> params = new HashMap<>();
params.put("appKey", appKey);
params.put("nonce", nonce);
params.put("timestamp", timestamp);

return generateSignature(appSecret, params);
}

/**
* 签名生成逻辑
* @param secretKey 秘钥
* @param params 参数 map
* @return 签名字符串
*/

public static String generateSignature(String secretKey, Map<String, String> params) {
// Step 1: 排序参数
Map<String, String> sortedParams = new TreeMap<>(params);

// Step 2: 拼接参数字符串(key=value&...)+ secretKey
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
if (sb.length() > 0) sb.append("&");
sb.append(entry.getKey()).append("=").append(entry.getValue());
}
sb.append(secretKey); // 拼接密钥

// Step 3: 进行 SHA256 签名
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = md.digest(sb.toString().getBytes(StandardCharsets.UTF_8));

// 转十六进制
StringBuilder hex = new StringBuilder();
for (byte b : hashBytes) {
hex.append(String.format("x", b));
}
return hex.toString();

} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256算法不可用", e);
}
}
}

图片处理工具类:ImageUtils.java


因为我们数据库中 image_urls 字段是 JSON 数组格式(如 ["url1", "url2", "url3"]),前端展示和分享时需要将其解析成 List<String>,我们封装了如下工具类:



  • parseImages():将数据库的 JSON 格式或逗号分隔的图片字符串,解析为图片地址列表。

  • getFirstImage():获取列表中的第一张图,作为默认封面图。


/**
* 图片处理工具类
*/

public class ImageUtils {

private static final ObjectMapper mapper = new ObjectMapper();

/**
* 将 imageUrls 字段解析为图片地址列表
* @param imageUrls 数据库中的 imageUrls 字段
* @return List<String> 图片地址列表
*/

public static List<String> parseImages(String imageUrls) {
if (imageUrls == null || imageUrls.isBlank()) return Collections.emptyList();

try {
// 如果是 JSON 数组格式
if (imageUrls.trim().startsWith("[")) {
return mapper.readValue(imageUrls, new TypeReference<List<String>>() {});
} else {
// 普通逗号分隔格式
return Arrays.stream(imageUrls.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
} catch (Exception e) {
return Collections.emptyList(); // 解析失败返回空列表
}
}

/**
* 获取图片中的第一张图(封面用)
* @param imageUrls 数据库中的 imageUrls 字段
* @return 第一张图片地址或 null
*/

public static String getFirstImage(String imageUrls) {
List<String> images = parseImages(imageUrls);
return images.isEmpty() ? null : images.get(0);
}
}

前端跳转页实现(跳转小红书 App 分享)


在我们前面实现的文案预览页中,点击“一键发布”按钮后,会跳转到一个中间页,这个页面的作用是:



  • 检查用户当前的浏览器环境(是否是微信、是否是 Android);

  • 请求服务端接口,获取真实的文案内容 + 签名参数;

  • 自动调用小红书开放平台提供的 xhs.share() 方法,引导用户跳转并打开小红书 App 进行发布。


判断浏览器类型与环境


前端页面在加载时,会首先通过以下代码判断当前浏览器环境:


const u = navigator.userAgent.toLowerCase();

const isAndroid = u.includes('android') || u.includes('adr');
const isWeixin = u.includes('micromessenger');

const params = Object.fromEntries(new URLSearchParams(location.search).entries());

const id = params.id;
const appKey = params.appKey;
const nonce = params.nonce;
const timestamp = params.timestamp;
const signature = params.signature;

// 如果在微信中提示用户跳出
if (isAndroid && isWeixin) {
document.getElementById("tips").style.display = 'block';
} else {
goShare();
}

这一段逻辑我们简单做了几件事:



  • 从浏览器的 userAgent 中判断用户是否使用的是 安卓设备,以及是否是在 微信浏览器中打开

  • 如果是在微信中打开,则无法直接跳转小红书 App,因此我们会弹出提示,要求用户“点击右上角 -> 浏览器打开”。

  • 否则,说明环境已满足跳转条件,我们会直接调用 goShare() 方法进入分享流程。


请求服务端接口,校验并返回参数


当满足环境条件后,页面会自动调用我们之前实现的第二个接口 /api/post/publish/{id},用于服务端校验文案真实性,并返回跳转小红书所需的加密参数。代码如下:


const res = await fetch(`http://110.42.233.124:8888/api/post/publish/${id}`);
const json = await res.json();

const { data: verifyConfig, shareInfo } = json;

调用小红书 SDK 的分享能力


一旦我们拿到 verifyConfig(签名参数)和 shareInfo(图文内容),就可以正式调用小红书的 SDK 进行分享:


xhs.share({
shareInfo: {
type,
title,
content,
images,
...(type === 'video' ? { video, cover } : {})
},
verifyConfig: {
appKey: verifyConfig.appKey,
nonce: verifyConfig.nonce,
timestamp: verifyConfig.timestamp,
signature: verifyConfig.signature
},
fail: (e) => {
console.error('小红书分享失败:', e);
alert('小红书分享失败: ' + JSON.stringify(e));
}
});

这个方法会自动拉起小红书 App 并进入发布页,用户可以直接确认发布内容。


最终效果


以下是我们实际测试中跳转页的样式效果示意(图片有点大,耐心等待一下..... )包含提示引导和自动跳转逻辑:


output.gif


从图中流程我们可以看到,我们打开了后台给到的分享链接后,我们就可以进入第一个文案预览页面。当我们点击预览页面中的「一键发布」按钮后,页面会自动跳转至我们前面实现的中间页(分享跳转页)。整个过程的用户体验如下:



  1. 提示离开微信(或浏览器)

    如果用户是在微信中打开链接,系统会自动弹出提示,提示用户“即将打开第三方 App”,引导用户点击右上角菜单,选择「在浏览器中打开」。

  2. 自动跳转小红书 App

    当用户点击继续,或在普通浏览器中打开时,页面会自动调用小红书开放平台的跳转能力(xhs.share),此时会拉起小红书 App。

  3. 自动进入发布页,内容已填充

    成功跳转后,用户会直接进入小红书的发布页面,不需要再手动粘贴内容:



    • 文案标题和正文内容已自动填写;

    • 所选图片已经预先加载为图文笔记的图组;

    • 下方的「话题」也已经贴上;

    • 如果是视频内容,则会自动带上封面图与视频资源。




也就是说,用户只需要点几下,就能一键跳转到小红书 App 发布笔记,几乎不需要做任何额外操作,整个过程简单快速,适合后台批量生成内容后,由运营一键分发。


这一套跳转流程适用于图文与视频类型的内容,不需要接入小红书 App SDK,只需要后台生成好内容和签名,前端发起跳转即可。




进阶设计:多设备自动发布与后台数据统计


到这里,我们已经完成了整套“小红书文案一键发布”的实现流程,用户只需点击跳转,就能将指定文案快速填充到小红书 App 的发布页面。


但如果放在实际业务中,这还只是第一步。


目前这种方式仍然依赖人工操作 —— 也就是说,我们还是需要每台手机手动打开链接、点击发布,才能完成整个笔记的投放过程。


而在真实的项目实践中,我们已经做到了 “机器化发布”:通过技术手段控制多台手机(甚至几十上百台设备)同时打开指定链接、自动触发跳转与发布动作,极大地提升了投放效率,节省了大量人力。


此外,后台系统也支持发布数据的实时统计,例如:



  • 哪些账号已成功发布;

  • 每条文案的发布状态;

  • 相关数据是否回传成功(如小红书笔记的曝光量、点赞量等);

  • 发布失败的情况排查与补投管理。


当然,这部分内容就不展开讲了,有兴趣的同学可以进一步了解在分发调度、设备联控、账号管理等方面的实践方案。




用后台赋能一对多教学,打造自己的“小红书孵化器”


这套系统除了用于团队运营,其实对独立开发者、个人博主、内容培训者也非常实用。


以我自己为例,比如我现在在做「教粉丝小红书起号」的副业。但如果每位粉丝都要我手动发图、写文案、传视频,再一个个教他们怎么发笔记,不仅效率低,还特别容易出错。


于是我直接用自己搭的“小红书发布系统”来做教学工具:



  1. 我在后台准备好文案、话题、图片/视频;

  2. 系统自动生成发布跳转链接

  3. 然后我把这个链接生成限时二维码,发给粉丝;

  4. 粉丝扫码后,点击“一键发布”;

  5. 自动跳转到小红书 App,文案和素材都粘贴好了,直接发布即可。


这样我就可以用一套系统同时带多个账号起号,效率极高,不用再反复教每个人怎么贴图、加话题、排版、复制文案。


甚至还可以统计哪篇文案被谁用了,效果怎么样,方便后续优化内容策略。


所以说这套系统已经不只是一个“分享工具”,更是我做小红书教学、孵化账号的内容分发中枢了。




更多平台的扩展实践:从小红书到全平台统一管理


小红书虽然是目前最热门的图文笔记平台之一,但它的 JS SDK 和分享 API 对接起来其实非常简单。我们前端只要把参数拼好,直接调用 xhs.share(),就能一键跳转到 App 发布页面。


也正因为这种接入方式足够“轻量、稳定”,我们很快发现:



同样的模式,可以复制到更多平台。



除了小红书,我们还陆续接入了抖音(抖音开放平台)、快手(快手开放平台)、B站(哔哩哔哩开放平台)、微博(微博开放平台)等多个平台,把“一键发布”做成了一个真正的统一系统。


最后我们搭建出了一个 “内容发布中台”



  • 后台统一配置文案、图片、视频等素材;

  • 前端页面自动生成跳转链接;

  • 多平台同时分发,减少人工操作;

  • 实时回传发布状态,统一统计效果数据。


Untitled diagram _ Mermaid Chart-2025-09-19-075325.png


这样一来,无论是日常运营,还是品牌推广,都能用最少的人力,把内容快速推送到所有目标平台,实现最高效的触达。




技术也能让运营小姐姐更轻松


这一套“小红书一键发布”系统,其实技术上并不复杂,前端甚至就一行 xhs.share(...)。但当我们把流程统一、配置集中、权限收敛、参数自动化后,整个团队的节奏就变了。


以前运营小姐姐每天得在文案、图片、账号之间来回切换,错一个字就要撤回重发,现在她登录后台挑好文案,一键生成链接,分享到手机点一下,直接跳进 App 就能发笔记,图文话题全自动贴好。


从“复制粘贴”到“一键跳转”,从“账号切来切去”到“后台统一分发”,

连她自己都说:“以前像搬砖工人,现在像点菜经理。”


最重要的是:

她终于可以准时下班了。


我们现在还接入了设备控制、状态检测、效果回传,如果后面再接个大模型自动生成文案,她可能连选图都不用了,坐着喝奶茶就能把内容分发搞定。


这也让我明白一件事:

不是运营不努力,而是工具不给力。

技术如果能让人少加点班、多留点发际线,那就值得去做。


这其实就是一个非常典型的例子:技术可以改变运营效率,甚至直接降低人力成本


未来当我们再结合 AI 大模型(比如自动生成文案、图片审核、内容风格推荐等),这个平台的自动化能力还可以再上一个台阶。


所以哪怕只是一次小小的集成,也值得认真打磨。它的价值,可能比我们想象中更大哦。


作者:洛卡卡了
来源:juejin.cn/post/7552489208804491316
收起阅读 »

前端没有实际的必要了?结合今年工作内容,谈谈我的看法

大家好,我是拭心。 今天被一张《IT 开发工作可能要完全重组》的图片刷屏,图片中的观点是:传统的「产品-设计-前端/后端」模式在 AI 时代将被变革。 很多人会觉得“前端没有实际的必要了”是管理者自嗨,但就我个人的见闻而言,这可能真的是未来趋势。 基于 AI...
继续阅读 »

大家好,我是拭心。


image.png


今天被一张《IT 开发工作可能要完全重组》的图片刷屏,图片中的观点是:传统的「产品-设计-前端/后端」模式在 AI 时代将被变革。


很多人会觉得“前端没有实际的必要了”是管理者自嗨,但就我个人的见闻而言,这可能真的是未来趋势。


基于 AI 的一专多能“超级个体”模式已经在很多公司铺展开,未来不久程序员大概率会不分前后、只剩全栈。


之所以敢这么笃定,是因为今年我亲身经历了这个变化。


简单聊聊我的工作变化


今年我的工作 80% 都是 AI 相关,工作内容上有三个比较大的转变:



  1. 技能层面:从“纯前端技术”转向“产品设计+AI内容生产+代码实现”的复合能力(例如:结合自身的冥想经历,提出并开发上线冥想呼吸练习功能)。

  2. 协作层面:从“与产品/后端对接”转向“与AI协同+跨部门整合”(例如:直接参与产品需求设计,用 AI 快速做 demo、上线验证方案可行性)。

  3. 成果层面:从“交付代码”转向“交付「产品+技术」解决方案”(例如:用 AI 生成热点资讯)。


工作时间分配上,也从之前的「大部分时间手写代码」变成了:



  1. 20% 的时间:手写代码(一般是改 bug)

  2. 30% 的时间:指挥 AI 写代码、review、accept/undo、cmmit & push

  3. 30% 的时间:优化提示词的效果

  4. 20% 的时间:和 AI 碰撞点子和改进方案


image.png


在我做的这些项目里,正如文章开头的图片所说,完全没有前后端岗位的概念,基本上都是和业务方沟通完需求、确定好方案,就开发、上线,甚至有的需求我自己定方案(在 AI 的加持下)。


前端是不是真的没有实际的必要了


那么问题来了,前端/后端以后是不是就不需要这么多人,大家要失业了?


我的看法是:程序员这个岗位的确会变少,但适合我们的新机会也随之诞生了


随着大模型的编程能力提升和配套设施完善,代码开发的 AI 化必定会发展到 80% 甚至 90%(至少还需要 10~20% 的人把关)。


如果只盯着程序员的「把需求文档实现为代码」这个职能,我们的机会是越来越少的。


但如果着眼于使用 AI 进行业务流程改造和内容生产,机会会越来越多。


最近两年开始,很多公司开始招聘名为「AI 工程师」的岗位,他们的工作内容就是业务优化和 AIGC。这个岗位招的人呈两极分化:要么是年轻的高学历应届生、要么是经验丰富的资深开发者。


招高学历应届生是因为他们具备创新和挑战精神;而招资深开发者转型 AI 应用,是因为他们有业务经验、全栈能力更强。


我今年的岗位角色就是 AI 工程师,在带着这种视角工作时,会发现有太多可以做的,以前凭感觉定的都可以用 AI 重做一遍,AI 工程师目前还远远不够。


想想我们的产品里有多少文案是写死的?有多少数据是无人问津的?有多少策略是拍脑袋定的?这些都是 AI 工程师可以改造优化的点。


总结


忍不住多写了几句,一看表这么晚了,年纪大了不能熬夜,总结一下结束此文。


技术变革就是会让生产效率提升,让工具性的岗位变少(程序员说白了就是把人的语言翻译为机器语言),但也会催生出新的岗位,我们要向前看。


从感性上我们是不愿意接受的,怎么革命偏偏革到了我们头上?我的房贷还没还完呢,以后可怎么办呢?


别慌,就我今年的经验来看,这一波 AI 技术革命,作为软件开发的我们有先发优势,只要稍加学习,再加上一些业务思考,很容易就可以转型到 AI 工程师。


至于如何转型到 AI 工程师,容我结合今年的工作&学习经验梳理下,也欢迎感兴趣的朋友留言讨论你们的看法。


滚滚长江东逝水,乘风安逸逆风衰,晚安朋友。




这就是流量的力量吗?用豆包 AI 编程做的小红书小组件帖子爆了


2025 上半年头部 AI 产品都有哪些?还有哪些新起之秀?


拭心 7 月日复盘|个体在 AI 时代的挑战


2025 年 AI 的落地程度远比我们想象的广


作者:张拭心
来源:juejin.cn/post/7576477793778171944
收起阅读 »

Electron 发布 39 版本 ,这更新速度也变态了吧❓︎❓︎❓︎

web
最近在使用 NestJs 和 NextJs 在做一个协同文档 DocFlow,如果感兴趣,欢迎 star,有任何疑问,欢迎加我微信进行咨询 yunmz777 Electron 39.0.0 于 2025 年 10 月 27 日发布。此版本带来了对 Chromi...
继续阅读 »

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


Electron 39.0.0 于 2025 年 10 月 27 日发布。此版本带来了对 Chromium、Node.js 和 V8 引擎的更新,提升了性能和稳定性,同时也引入了一些新的功能和改进。以下是此版本的详细变化。


栈升级



  • Chromium: 更新到 142.0.7444.52,这意味着 Electron 在此版本中升级了其底层浏览器引擎。该版本修复了多个性能和安全漏洞,同时引入了一些新的 Web 标准。

  • Node.js: 更新到 22.20.0,这个版本包含了许多重要的 Node.js 修复和改进,包括性能优化和一些新的 API。

  • V8: 更新到 14.2,V8 引擎的升级提升了 JavaScript 执行的效率,使得应用程序的响应速度更快,内存占用更低。


破坏性更改


在这一版本中,有几个 API 和行为发生了变化,这可能会导致与以前版本的兼容性问题。



  • OffscreenSharedTexture:此 API 的签名进行了更新,新的版本提供了一个统一的 handle,用于持有原生句柄。这意味着开发者需要调整代码,以便正确使用这个新的接口。

  • window.open:该方法的行为得到修复,确保它创建的弹出窗口始终是可调整大小的。原本可能出现的不一致性问题已经被解决,确保符合标准规范。


新特性



  • Offscreen 渲染支持 RGBAF16:Electron 现在支持以 RGBAF16 格式输出图像数据。这意味着应用程序可以更好地支持高动态范围(HDR)图像,提供更高质量的图像渲染。

  • process.getSystemMemoryInfo() 增强:在 macOS 上,getSystemMemoryInfo 方法新增了 fileBackedpurgeable 字段,这让开发者能够获得更多关于系统内存的信息,包括哪些内存是文件映射的、哪些可以被清除以释放空间。

  • systemPreferences.getAccentColor:在 Linux 上,Electron 新增了一个方法 systemPreferences.getAccentColor,它返回操作系统的强调色。这对于需要与操作系统主题颜色匹配的应用程序很有用。

  • 托盘图标 guid 选项:在 macOS 上,Tray 构造函数现在支持一个新的 guid 选项。这个选项允许托盘图标在应用程序重新启动后保持相同的位置和状态,使得用户体验更加一致。

  • WebFrameMain API 增强:Electron 新增了 webFrameMain.fromFrameToken(processId, frameToken) 方法,开发者可以通过此方法从帧令牌获取 WebFrameMain 实例,这对于需要直接操作特定帧的应用程序非常有用。

  • 可访问性支持:Electron 引入了更细粒度的可访问性支持,包括为开发者提供更多的 API 来提高对残障人士的支持。这使得应用程序能更好地满足可访问性需求,提供更加友好的用户体验。

  • app.getRecentDocuments() 支持:在 Windows 和 macOS 上,Electron 现在支持 app.getRecentDocuments() 方法。通过这个方法,开发者可以获取到最近访问的文档列表,方便实现类似于“最近使用文件”的功能。

  • USB 设备 API 更新:Electron 新增了对 USBDevice.configurations 的支持。开发者现在可以获取到连接到设备的 USB 配置信息,这对于需要与 USB 设备交互的应用程序非常有用。

  • 文件系统 API 更新:在应用程序中持久化文件系统权限状态变得更加简单。Electron 允许在给定的会话内持久化文件系统授权状态,避免用户每次打开应用时重新授权。

  • 动态导入(ESM)支持:在非上下文隔离的预加载脚本中,Electron 现在支持动态导入 ECMAScript 模块(ESM)。这使得开发者能够在 Electron 中更灵活地使用 JavaScript 模块化。


修复



  • 系统配色问题修复:修复了 systemPreferences.getAccentColor 返回的颜色反转的问题,确保返回的颜色值符合预期。

  • 开发者工具:修复了在 Wayland 上调用 webContents.openDevTools({ mode: 'detach' }) 时可能导致的崩溃问题。Wayland 是 Linux 上的一种显示协议,这个修复对于在该平台上开发的 Electron 应用程序至关重要。

  • 会话管理问题:修复了访问 webContents.session 时可能导致崩溃的问题。这个修复增强了应用程序在多会话环境下的稳定性。

  • 窗口管理:修复了在调用 window.close() 后,执行某些操作可能导致崩溃的问题。这个修复提高了窗口管理的可靠性。

  • 命令行参数问题修复:修复了通过命令行参数传递特性参数时,可能导致的崩溃问题,确保应用程序能够稳定运行。

  • 文件对话框问题修复:修复了在 Windows 上调用 dialog.showOpenDialog 时,如果传入的扩展名过滤器数组为空,可能导致的崩溃问题。


其他更改



  • 资源定位:内部的资源定位机制发生了变化,现在 Electron 使用 DIR_ASSETS 来定位资产和资源。此外,app.getPath 方法现在支持返回一个新的 "assets" 键,用于获取应用程序资源路径。

  • 文档更新:官方文档得到了更新和补充,相关的 API 和功能进行了详细说明,开发者可以参考最新的文档了解更多实现细节。


总结


Electron 39.0.0 版本主要带来了对 Chromium、Node.js 和 V8 的升级,提升了性能和稳定性。此外,新增了对 Offscreen 渲染、系统主题色、USB 设备支持等的支持,也修复了多个与窗口管理、文件对话框等相关的 bug。对于开发者来说,这些更新和改进能够带来更高效、稳定的开发体验。


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

英伟达发射了首个太空AI服务器,H100已上天

太空数据中心的能源成本将只有地面上的十分之一。 11 月 2 日,英伟达首次把 H100 GPU 送入了太空。 作为目前 AI 领域的主力训练芯片,H100 配备 80GB 内存,其性能是此前任何一台进入太空的计算机的上百倍。在轨道上,它将测试一系列人工智能...
继续阅读 »

太空数据中心的能源成本将只有地面上的十分之一。



11 月 2 日,英伟达首次把 H100 GPU 送入了太空。


作为目前 AI 领域的主力训练芯片,H100 配备 80GB 内存,其性能是此前任何一台进入太空的计算机的上百倍。在轨道上,它将测试一系列人工智能处理应用,包括分析地球观测图像和运行谷歌的大语言模型(LLM)。


此次测试飞行搭载于位于弗吉尼亚州雷德蒙德的初创公司 Starcloud 的 Starcloud-1 卫星上,是该公司雄心勃勃的计划的第一步,该计划旨在将全球耗能巨大的数据处理基础设施迁移到太空。Starcloud 是 NVIDIA Inception 创业公司计划的成员。


支持者认为这个想法很有前景:在遥远的太空深处,数据中心不会占用宝贵的土地,也不需要那么多能源和水来冷却,它们也不会向大气中排放温室气体。


在算力逐渐紧张的 AI 时代,把芯片发射到太空已成为一个新的发展方向。此前,英伟达的 Jetson 机器学习计算板卡曾搭载于多颗实验型和地球观测小型卫星上。不过相比之下,本次 Starcloud 的行动可谓是建设太空数据中心的重要一步,这将是人类首次把地面数据中心的 GPU 送入轨道运行,为最早明年启动商业服务铺平了道路。


图片


「在太空,你可以获得几乎无限的低成本可再生能源,」Starcloud 联合创始人、CEO Philip Johnston 表示。「对环境的唯一成本是发射成本。与在地球上为数据中心供电相比,在数据中心的整个生命周期内,二氧化碳排放量将减少 10 倍。」


这项为期三年的任务将由 SpaceX 的「Bandwagon 4」猎鹰 9 号火箭发射升空。仅重 60 公斤的 Starcloud-1 卫星将在距离地球约 350 公里的超低轨道上运行。在那里,它将接收来自美国 Capella 公司运营的合成孔径雷达 (SAR) 地球观测卫星群的数据,实时处理这些数据,并将信息传回地球。


图片


Starcloud-1 卫星的内部结构,银色模块中装有一块 H100 GPU。该卫星基于 Astro Digital 的 Corvus-Micro 平台,预计任务寿命为 11 个月。


SAR 每秒预计会产生 10GB 的数据,在太空服务器出现之前,数据传输是个大问题。Johnston 表示:「但如果能够在轨道上处理这些数据,我们就只需下行传输关键信息。例如,信息可能显示某个位置有一艘船正以特定速度朝特定方向移动。这只需要一个 1 千字节的数据包,而下行传输未处理的数据则需要数百 GB。」


太空数据中心的优势


对于来自地球轨道卫星的数据进行轨道处理只是 Starcloud 愿景的一部分。该公司认为,随着火箭技术的进步,特别是 SpaceX 星舰预期带来的成本降低,未来的大规模计算基础设施可以部署在轨道上,而无需占用地球上宝贵的空间。


英伟达可持续发展负责人 Josh Parker 表示:「随着 AI 技术对能源需求的不断增长,轨道数据中心代表着一项变革性的环境突破 —— 它能大幅减少温室气体排放,并消除对先进冷却技术的需求。通过利用低成本、不间断的太阳能,避免占用土地和使用化石燃料,Starcloud 的技术使数据中心能够快速且可持续地扩展,从而在数字基础设施不断发展的同时,帮助保护地球气候和关键自然资源。」


据国际能源署预测,到 2030 年,全球数据处理基础设施的耗电量将与整个日本的用电量相当。数据中心还需要大量的水用于冷却 —— 世界经济论坛的数据显示,一个 1 兆瓦的数据中心每天的用水量相当于发达国家约 1000 人的用水量。随着人工智能的进步,计算需求持续增长,这些消耗也会与日俱增。人们越来越担心成本上升以及电力和供水中断的问题。该技术的支持者认为,将数据中心迁移到太空可以解决这些问题。


Starcloud 甚至预测在未来十年内,几乎所有新建数据中心都将建在太空,这完全是因为地面能源的限制。


Philip Johnston 指出,要让地球上的数据中心完全依靠绿色能源运行,需要对太阳能发电和电池储能系统进行大量投资。而在太空,由于阳光全天候可用,因此无需电池储能。此外,每个太阳能电池板在太空产生的电量是地球上同等容量太阳能电池板的八倍,这进一步降低了成本。


「我们在太空中唯一的额外成本就是发射费用。我们预计每公斤发射成本约为 500 美元,达到盈亏平衡点。而使用星舰后,预计发射成本会更低,」Philip Johnston 说道。


图片


Starcloud 联合创始人、CEO Philip Johnston 正在检查用于卫星定向的星敏感器。


一旦星舰全面投入运营,其每公斤发射价格预计将在 10 美元到 150 美元之间。该运载火箭迄今已成功完成六次飞行,根据现在的时间表将于明年首次发射卫星,不过仍然存在较大的推迟可能性。


除了支持 SAR 之外,Starcloud 也计划在轨道上使用 H100 GPU 运行 Gemma(谷歌的开源模型),证明即使是大语言模型也可以在外太空运行。与此同时,Starcloud 已在筹划其下一个任务,其计划明年将一颗比 Starcloud-1 强大十倍的数据中心送入太空。


Starcloud-2 任务将搭载英伟达新一代 Blackwell GPU 和数块 H100。Johnston 表示,该任务将提供 7 千瓦的计算能力,预计将为包括地球观测卫星运营商和美国国防部在内的客户提供商业服务。


一颗功率更大的 100 千瓦卫星预计将于 2027 年入轨。Starcloud 公司认为,到 2030 年代初,它将在太空中拥有一个 40 兆瓦的数据中心,其数据处理成本与地球上的数据中心相当。


图片


Starcloud 的团队成员。


Starcloud 是众多计划将计算外包到太空的公司之一。Axiom Space 公司今年早些时候也公布了类似的计划。总部位于佛罗里达州的 Lonestar Holdings 公司今年早些时候通过 Intuitive Machines-2 任务将一个小型数据中心送上月球,并计划在未来几年内在月球上建立大型数据中心。


参考内容:


spectrum.ieee.org/nvidia-h100…


blogs.nvidia.com/blog/starcl…


作者:机器之心
来源:juejin.cn/post/7568699551100715060
收起阅读 »

别 npm i!20亿包刚被投毒!速查!

web
20 亿次周下载量、18 个“基建级”包、一场持续 2 小时的**“核弹级”**污染——这次,攻击者把枪口对准了每一个前端开发者与 Web3 用户。 凌晨的“钓鱼邮件”,撕开 20 亿次周活的口子 9 月 8 日 17:39 UTC,Aikido Securi...
继续阅读 »

20 亿次周下载量、18 个“基建级”包、一场持续 2 小时的**“核弹级”**污染——这次,攻击者把枪口对准了每一个前端开发者与 Web3 用户。


凌晨的“钓鱼邮件”,撕开 20 亿次周活的口子


98 日 17:39 UTC,Aikido Security 的红色警报划破周末宁静:

npm 周下载量超 20 亿的 18 个核心包,被植入浏览器端加密货币劫持代码。



攻击入口简单到令人发指——一封“npm 官方”发来的 2FA 过期提醒


要素攻击者伪造内容
发件人support@npmjs.help(非官方域名)
标题【紧急】您的账户将于 9 月 10 日被锁定
按钮一键更新 2FA(实则窃取 token)


维护者 Josh Junon(qix)点下链接 30 秒后,攻击者即获得其 npm 账户完全控制权,随后向 chalk、debug、ansi-styles 等“基建级”包推送了带毒补丁版本。


恶意代码:功能 100% 正常,只是多了一笔“隐形转账”


攻击者没有粗暴地“删库跑路”,而是把恶意逻辑藏进 浏览器环境专属分支



  1. 只在 <script>webpack/browserify 打包后生效,Node 服务端无感知

  2. 监听常见 Web3 钱包(MetaMask、Phantom、OKX)的 sendAsync 调用;

  3. Levenshtein 算法 计算目标地址与内置地址列表的相似度 ≥ 0.9 即触发替换;

  4. 伪造与原交易相同的 txHash 回执,用户以为成功,实则资金已转入黑客地址



“代码 diff 只看 12 行,格式化后像是一段 polyfill,谁会在意?”—— 事后 Josh 复盘



2 小时核弹扩散:10% 云函数瞬间污染


时间线事件
15:12 UTC恶意版 chalk@5.4.0-beta.1 发布
15:47 UTCVercel 自动构建触发,全球 Edge Function 同步拉取
16:05 UTC首例用户反馈“链上转账成功但资金未到账”
17:39 UTCAikido 发出警报,npm 官方下架所有带毒版本
22:19 UTC主流云厂商完成构建缓存清零

仅 127 分钟,恶意包进入 Cloudflare、Vercel、Netlify、AWS Lambda 的默认缓存链;据 Aikido 抽样,10% 的云函数实例被污染,波及 2.3 万个站点。


冰山之下:18 个“核弹”完整清单



  • chalk



    • 周下载:3.0 亿

    • 传递性依赖:4.7 万个包

    • 典型上游:create-react-app、jest、eslint



  • debug



    • 周下载:3.6 亿

    • 传递性依赖:5.9 万个包

    • 典型上游:express、morgan、nodemon



  • ansi-styles



    • 周下载:3.7 亿

    • 传递性依赖:3.2 万个包

    • 典型上游:chalk、log-symbols、ora



  • supports-color



    • 周下载:3.5 亿

    • 传递性依赖:3.0 万个包

    • 典型上游:chalk、debug、webpack-dev-server



  • has-flag



    • 周下载:3.3 亿

    • 传递性依赖:2.8 万个包

    • 典型上游:supports-color、meow



  • ms



    • 周下载:3.1 亿

    • 传递性依赖:4.5 万个包

    • 典型上游:debug、send、serve-static



  • strip-ansi



    • 周下载:2.9 亿

    • 传递性依赖:3.4 万个包

    • 典型上游:chalk、ora、yargs



  • is-fullwidth-code-point



    • 周下载:2.8 亿

    • 传递性依赖:2.6 万个包

    • 典型上游:string-width、wide-align



  • emoji-regex



    • 周下载:2.7 亿

    • 传递性依赖:2.4 万个包

    • 典型上游:node-emoji、slackify-html



  • fs.realpath



    • 周下载:2.5 亿

    • 传递性依赖:2.9 万个包

    • 典型上游:glob、rimraf



  • inflight



    • 周下载:2.4 亿

    • 传递性依赖:2.7 万个包

    • 典型上游:glob、npm



  • once



    • 周下载:2.3 亿

    • 传递性依赖:3.1 万个包

    • 典型上游:glob、npm、request



  • wrappy



    • 周下载:2.2 亿

    • 传递性依赖:2.5 万个包

    • 典型上游:once、glob



  • color-convert



    • 周下载:2.1 亿

    • 传递性依赖:2.2 万个包

    • 典型上游:chalk、ansi-styles



  • color-name



    • 周下载:2.0 亿

    • 传递性依赖:2.0 万个包

    • 典型上游:color-convert



  • balanced-match



    • 周下载:1.9 亿

    • 传递性依赖:2.3 万个包

    • 典型上游:brace-expansion、minimatch



  • concat-map



    • 周下载:1.8 亿

    • 传递性依赖:2.1 万个包

    • 典型上游:brace-expansion



  • brace-expansion



    • 周下载:1.7 亿

    • 传递性依赖:2.4 万个包

    • 典型上游:minimatch、rimraf




以上 18 个包周下载总量 20.4 亿次,累计被 38 万个开源项目直接或间接依赖,构成现代前端与 Node 工具链的“水电煤”基础设施。



*传递性依赖:指直接/间接引用该包的 npm 项目总量,数据来源 libraries.io



开发者自救手册:3 条命令 1 分钟自检


# 1. 检查是否安装过带毒版本
npm ls chalk debug ansi-styles \
| grep -E '5\.4\.0-beta\.1|4\.3\.5-beta\.1|6\.2\.1-beta\.1'

# 2. 锁定干净版本
npm overrides \
"chalk@>=5.4.0-beta <5.4.1":"5.3.0" \
"debug@>=4.3.5-beta <4.3.6":"4.3.4"

# 3. 清空缓存 & 重装
npm cache clean --force
rm -rf node_modules package-lock.json
npm ci

Web3 用户额外建议

在浏览器插件设置 → 隐私与安全 → 授权站点白名单,关闭“自动签名”功能,任何转账二次确认


npm 官方回应与后续动作



  • 2FA 强制令:2025 年 10 月 1 日起,所有周下载 >100 万的维护者必须硬件密钥(YubiKey/WebAuthn)+ 2FA,否则暂停发版权限;

  • 发布“可验证构建”试点:源码与预编译产物在 GitHub Actions 中可重现哈希,npm registry 自动比对;

  • 供应链实时雷达:与 GitHub Advisory DB、Snyk、OSV 打通,恶意版本 ≤15 分钟 全网黑名单。


写在最后:前端“水电煤”真的安全吗?


chalk 只是给终端上个色,debug 只是打印一条日志——但当它们成为 38 万个包的必经之路,就不再是“小工具”而是基础设施


一次钓鱼邮件,就能让 20 亿次周活的“水电煤”瞬间投毒,这就是现代软件供应链的蝴蝶效应


npm 生态的暴击提醒我们

“不要信任、永远验证”不仅属于 Web3,也属于每一个 npm install 的瞬间。


作者:前端开发爱好者
来源:juejin.cn/post/7552725431779835947
收起阅读 »

为什么游戏公司现在都喜欢用protobuf?

web
点击上方亿元程序员+关注和★星标 引言 哈喽大家好,不知道小伙伴们最近有没有发现一个现象,无论是大厂还是小团队,越来越多的游戏项目都在使用protobuf作为数据交换格式。 笔者想起,在去年有幸研学过某个砍树游戏(寻道大千)源码的时候,发现他们用的也是pro...
继续阅读 »

点击上方亿元程序员+关注和★星标



引言


哈喽大家好,不知道小伙伴们最近有没有发现一个现象,无论是大厂还是小团队,越来越多的游戏项目都在使用protobuf作为数据交换格式。


笔者想起,在去年有幸研学过某个砍树游戏(寻道大千)源码的时候,发现他们用的也是protobuf


曾经流行JSON、XML似乎在游戏开发领域悄然退居二线,这到底是为什么?


今天我们就来聊聊protobuf为何能成为游戏行业的“新宠”。


什么是protobuf?


先简单科普一下



Protocol BuffersGoogle开发的一种轻量级、高效的数据交换格式。


它能够将结构化数据序列化,适用于网络传输和数据存储。


与JSON和XML相比protobuf生成的二进制数据更小,解析速度更快。



游戏公司为何钟情于protobuf?


1.性能优势


游戏对性能的要求极为苛刻,尤其是网络游戏。


每毫秒的延迟都可能影响玩家的游戏体验。


Protobuf的二进制格式使得数据包体积比JSON3-10倍,序列化和反序列化速度比JSON5-100倍。



**说到这里有个小技巧:**小伙伴们可以先用JSON,然后领导要优化的时候,再改成Protobuf,实现质的飞跃,建议大家不要学


2.跨平台跨语言


Protobuf支持多种编程语言(C++、C#、Java,JavaScript,TypeScript等),只需定义一次数据结构,即可在各个平台上使用。


笔者觉得这也是Protobuf生态好的原因之一。


3.向前向后兼容


Protobuf通过字段编号而非字段名来标识数据。


新增字段不会破坏旧版程序,老版本可以忽略新字段继续运行,这为需要频繁更新的游戏以及多变的游戏修改需求提供了便利。


一起来看个例子


既然Protobuf有这么多优点,深受众多公司青睐,那么我们一起来看看它在Cocos游戏开发中的实际表现。


1.搭建场景


首先简单搭建一下场景,分为三部分。



  • 1.信息展示:包括头像和昵称。

  • 2.登陆按钮:点击向服务器获取头像和昵称。

  • 3.Loading效果:单纯为了让例子更好看。



2.搭建服务器


找AI搭子帮忙简单搭建一个简单的Http服务器。



代码比较简单,生成如下:



3.协议用JSON


用JSON比较容易上手,现在很多语言都自带JSON的编码和解析。


客户端


首先定义一下发送的结构,包括账号和密码,返回的消息包括是否成功、消息和用户信息。



然后请求服务器,JSON编码采用JSON.stringify(requestData),解码采用response.json()



添加点击事件,加个Loading效果。(这里也有个小技巧,我们先假装Loading等待2秒,留一点优化空间,等领导反馈卡的时候再快点,十分好用,也建议大家不要学)



服务端


服务端做一下简单的处理,解码采用JSON.parse(buffer.toString()),编码采用JSON.stringify(loginResponse)



启动服务器



效果演示



4.协议用Protobuf


用Protobuf需要先定义好协议文件,我们简单定义一下登录协议、登陆响应和登陆成功的用户数据。



与此同时,需要通过npm install -g pbjs安装一下生成工具。



客户端


首先通过pbjs ./proto/login.proto --ts ./assets/scripts/proto/login.ts生成一下ts代码。



与JSON类似,通过生成的编码接口encodeLoginRequest(loginRequest)和解码接口decodeLoginResponse(response),对数据进行编码和解析。



最后是发送。



服务端


首先通过pbjs ./proto/login.proto --es6 ./server/proto/login.js生成一下js代码。



服务端也是通过生成的解码接口proto.decodeLoginRequest(buffer)和编码接口proto.encodeLoginResponse(loginResponse)进行解码和编码。



效果演示



并非万能钥匙


根据上面的例子来看protobuf并非在所有场景下都是最佳选择。


对于简单的示例或者小项目,引入protobuf需要安装环境、生成代码,反而增加了不必要的复杂度。


结语


游戏公司的项目一般都有一定规模,并且是团队开发,protobuf凭借其出色的性能,确实成为了游戏公司的优选方案。


但是笔者认为,不管谁强谁弱,只有真正适合自己的,才是最好的。


你们觉得呢?


本文源工程可通过私信发送 protobuf 获取。


我是"亿元程序员",一位有着8年游戏行业经验的主程。在游戏开发中,希望能给到您帮助, 也希望通过您能帮助到大家。


AD:笔者线上的小游戏《打螺丝闯关》《贪吃蛇掌机经典》《重力迷宫球》《填色之旅》《方块掌机经典》大家可以自行点击搜索体验。


实不相瞒,想要个爱心!请把该文章分享给你觉得有需要的其他小伙伴。谢谢!


推荐专栏:


知识付费专栏


你知道和不知道的微信小游戏常用API整理,赶紧收藏用起来~


100个Cocos实例


8年主程手把手打造Cocos独立游戏开发框架


和8年游戏主程一起学习设计模式


从零开始开发贪吃蛇小游戏到上线系列


点击下方灰色按钮+关注。


作者:亿元程序员
来源:juejin.cn/post/7566103962497794086
收起阅读 »

当上传不再只是 /upload,我们是怎么设计大文件上传的

web
业务背景在正式讲之前,先看一个我们做的大文件上传demo。下面这个视频演示的是上传一个 1GB 的压缩包,整个过程支持分片上传、断点续传、暂停和恢复。可以看到速度不是特别快,这个是我故意没去优化的。前端那边计算文件 MD5、以及最后合并文件的时间我都保留了,主...
继续阅读 »

业务背景

在正式讲之前,先看一个我们做的大文件上传demo。
下面这个视频演示的是上传一个 1GB 的压缩包,整个过程支持分片上传、断点续传、暂停和恢复。

可以看到速度不是特别快,这个是我故意没去优化的。
前端那边计算文件 MD5、以及最后合并文件的时间我都保留了,
主要是想让大家能看到整个流程是怎么跑通的。

output1111.gif

平时我们在做一些 SaaS 系统的时候,文件上传这块其实基本上都设计的挺简单的。
前端做个分片上传,后端把分片合并起来,最后存 OSS 或者服务器某个路径上,再返回一个 URL 就完事了。
大多数情况下,这样的方案也确实够用。

但是最近我在做一个私有化项目,场景完全不一样。
项目是给政企客户部署的内部系统,里面有 AI 大模型客服问答的功能。
客户需要把他们内部的文档、手册、规范、图纸、流程等资料打包上传到服务器,用来做后续的向量化、知识检索或者模型训练。

这类场景如果还沿用之前 SaaS 系统那种上传方式,往往就不太适用了。
因为这些文件往往有几个共同点:

  1. 文件数量多,动辄几百上千份(Word、PDF、PPT、Markdown 都有);
  2. 文件体积大,打成 zip 动不动就是几个 G,甚至十几二十个 G;
  3. 上传环境复杂,客户一般在内网或局域网,有的甚至完全断网;
  4. 有安全要求,文件不能经过云端 OSS,里面可能有保密资料;
  5. 需要审计,要能知道是谁上传的、什么时候传的、文件现在存哪;
  6. 上传完之后还要进一步处理,比如自动解压、解析文本、拆页、向量化,然后再存入 Milvus 或 pgvector。

所以这种情况还用 SaaS 系统那种“简单上传+云存储”方案的话,那可能问题就一堆:

  • 上传中断后用户一刷新浏览器就得重传整个包;
  • 集群部署时分片打到不同机器上根本无法合并;
  • 多人同时上传可能会发生文件覆盖或路径冲突;
  • 没有任何上传记录,也追踪不到是谁传的;
  • 对政企来说,审计、合规、保密全都不达标。

所以,我们需要重新设计文件上传的功能逻辑。
目的是让它不仅能支持大文件、断点续传、集群部署,还能同时适配内网环境、权限管控,以及后续的 AI 文档解析和知识向量化等处理流程。


为什么很多项目只需要一个 upload 接口

如果我们回头看一下自己平常做过的一些常规 Web 项目,尤其是各种 SaaS 系统或者后台管理系统,
其实大多数时候后端只会提供一个 /upload 接口, 前端拿到文件后直接调用这个接口,后端保存文件再返回一个 URL 就结束了。

甚至我们在很多项目里,前端都不会把文件传给业务服务,
而是直接通过前端 SDK(比如阿里云 OSS、腾讯云 COS、七牛云等)上传到云存储,
上传完后拿到文件地址,再把这个地址回传给后端保存。

这种方式在 SaaS 系统或者轻量级的业务里非常普遍,也非常高效。 主要原因有几个:

  1. 文件都比较小,大多数就是几 MB 的图片、PDF 或 Excel;
  2. 云存储足够稳定,上传、下载、访问都有完整的 SDK 支撑;
  3. 系统是公网部署,不需要考虑局域网、内网断网这些问题;
  4. 对安全和审计的要求不高,文件内容也不是涉密数据;
  5. 用户体验优先,所以直接把文件上传到云端是最省事的方案。

换句话说,这种“一个 upload 接口”或“前端直传 OSS”模式,其实是面向通用型 SaaS 场景的。
对于绝大多数互联网业务来说,它既够快又够省心。

但一旦项目换成政企、私有化部署或者 AI 训练平台这种环境,
就完全不是一个量级的问题了。
这里的关键不在“能不能上传”,
而在于文件上传之后的可控性、可追溯性和安全性


前端常见的大文件上传方式

在重新设计后端接口之前,我们先来看看现在前端常见的大文件上传思路。
其实近几年前端这块已经比较成熟了,主流方案大体都是围绕几个核心点展开的:
秒传检测、分片上传、断点续传、并发控制、进度展示。

一般来说,前端拿到文件后,会先计算一个文件哈希值,比如用 MD5。
这样做的目的是为了做秒传检测
如果服务器上已经存在这个文件,就可以直接跳过上传,节省时间和带宽。

接下来是分片上传
文件太大时,前端会把文件拆成多个固定大小的小块(比如每块 5MB 或 10MB),
然后一片一片地上传。这样做可以避免一次性传输大文件导致浏览器卡顿或网络中断。

然后就是断点续传
前端会记录哪些分片已经上传成功,如果上传过程中网络中断或浏览器刷新,
下次只需要从未完成的分片继续上传,不用重新传整包文件。

在性能方面,前端还会做并发控制
比如同时上传三到五个分片,上传完一个就立刻补下一个,
这样整体速度比单线程串行上传要快很多。

最后是进度展示
通过监听每个分片的上传状态,前端可以计算整体进度,
给用户展示一个实时的上传百分比或进度条,让体验更可控。

可以看到,前端的大文件上传方案已经形成了一套相对标准的模式。
所以这次我在重新设计后端的时候,就打算基于这种前端逻辑,
去构建一套更贴合企业私有化环境的上传接口控制体系。
目标是让前后端的职责划分更清晰:
前端负责切片、控制与恢复;后端负责存储、校验与合并。


后端接口设计思路

前端的大文件上传流程其实已经相对固定了,我们只要让后端的接口和它配合得上,就能把整个上传链路打通。
所以我这次重新设计时,把上传接口拆成了几个比较独立的阶段:
秒传检查、初始化任务、上传分片、合并文件、暂停任务、取消任务、任务列表。
每个接口都只负责一件事,这样接口的职责会更清晰,也方便后期扩展。

一、/upload/check —— 秒传检查

这个接口是整个流程的第一步,用来判断文件是否已经上传过。
前端在计算完文件的全局 MD5(或其他 hash)后,会先调这个接口。
如果后端发现数据库里已经有相同 hash 的文件,就直接返回“已存在”,前端就不用再上传了。

请求示例:

POST /api/upload/check
{
"fileHash": "md5_abc123def456",
"fileName": "training-docs.zip",
"fileSize": 5342245120
}

返回示例:

{
"success": true,
"data": {
"exists": false
}
}

如果 exists = true,说明服务端已经有这个文件,可以直接走“秒传成功”的逻辑。

伪代码示例:

@PostMapping("/check")
public Result checkFile(@RequestBody Map body) {
// 1. 校验 fileHash 参数是否为空
// 2. 查询 file_info 表是否已有该文件
// 3. 如果文件已存在,直接返回秒传成功(exists = true)
// 4. 如果文件不存在,查询 upload_task 表中是否有未完成任务(支持断点续传)
}

二、/upload/init —— 初始化上传任务

如果文件不存在,就要先初始化一个新的上传任务。
这个接口的作用是创建一条 upload_task 记录,同时返回一个唯一的 uploadId
前端会用这个 uploadId 来标识整个上传过程。

请求示例:

POST /api/upload/init
{
"fileHash": "md5_abc123def456",
"fileName": "training-docs.zip",
"totalChunks": 320,
"chunkSize": 5242880
}

返回示例:

{
"success": true,
"data": {
"uploadId": "b4f8e3a7-1a0c-4a1d-88af-61e98d91a49b",
"uploadedChunks": []
}
}

uploadedChunks 用来支持断点续传,如果之前有部分分片上传过,就会在这里返回索引数组。

伪代码示例:

@PostMapping("/init")
public Result initUpload(@RequestBody UploadInitRequest request) {
// 1. 检查是否已有同 fileHash 的任务,若有则返回旧任务信息(支持断点续传)
// 2. 否则创建新的 upload_task 记录,生成 uploadId
// 3. 初始化分片数量、大小、状态等信息
// 4. 返回 uploadId 与已上传分片索引列表
}

三、/upload/chunk —— 上传单个分片

这是整个上传过程里调用次数最多的接口。
每个分片都会单独上传一次,并在服务端保存为临时文件,同时写入 upload_chunk 表。
上传成功后,后端会更新 upload_task 的进度信息。

请求示例(表单上传):

POST /api/upload/chunk
Content-Type: multipart/form-data

formData:
uploadId: b4f8e3a7-1a0c-4a1d-88af-61e98d91a49b
chunkIndex: 0
chunkSize: 5242880
chunkHash: md5_001
file: (二进制分片数据)

返回示例:

{
"success": true,
"data": {
"uploadId": "b4f8e3a7-1a0c-4a1d-88af-61e98d91a49b",
"chunkIndex": 0,
"chunkSize": 5242880
}
}

伪代码示例:

@PostMapping(value = "/chunk", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Result uploadChunk(@ModelAttribute UploadChunkRequest req) {
// 1. 校验任务状态,禁止上传已取消或已完成的任务
// 2. 检查本地目录(或云端存储桶)是否存在,不存在则创建
// 3. 接收当前分片文件并写入临时路径
// 4. 写入 upload_chunk 表,标记状态为 “已上传”
// 5. 更新 upload_task 的 uploaded_chunks 数量
}

四、/upload/merge —— 合并分片

当前端确认所有分片都上传完后,会调用 /upload/merge
后端收到这个请求后,去检查所有分片是否完整,然后按照索引顺序依次合并。
合并成功后,会删除临时分片文件,并更新 upload_task 状态为“完成”。
如果启用了云存储,这一步也可以直接把合并后的文件上传到 OSS。

请求示例:

POST /api/upload/merge
{
"uploadId": "b4f8e3a7-1a0c-4a1d-88af-61e98d91a49b",
"fileHash": "md5_abc123def456"
}

返回示例:

{
"success": true,
"message": "文件合并成功",
"data": {
"storagePath": "/data/uploads/training-docs.zip"
}
}

伪代码示例:

@PostMapping("/merge")
public Result mergeFile(@RequestBody UploadMergeRequest req) {
// 1. 检查 upload_task 状态是否允许合并
// 2. 校验所有分片是否都上传完成
// 3. 如果是本地存储:按 chunk_index 顺序流式合并文件
// 4. 如果是云存储:调用云端分片合并 API(如 OSS、COS)
// 5. 校验文件 hash 完整性,更新任务状态为 COMPLETED
// 6. 将最终文件信息写入 file_info 表
}

五、/upload/pause —— 暂停任务

这个接口用于在上传过程中手动暂停任务。
前端可能会在网络波动或用户主动点击暂停时调用。
后端会更新任务状态为“已暂停”,并记录当前已上传的分片数。

请求示例:

POST /api/upload/pause
{
"uploadId": "b4f8e3a7-1a0c-4a1d-88af-61e98d91a49b"
}

返回示例:

{
"success": true,
"message": "任务已暂停"
}

伪代码示例:

@PostMapping("/pause")
public Result pauseUpload(@RequestBody UploadPauseRequest req) {
// 1. 查找对应的 upload_task
// 2. 更新任务状态为 “已暂停”
// 3. 返回任务状态确认信息
}

六、/upload/cancel —— 取消任务

如果用户想放弃本次上传,可以调用 /cancel
后端会把任务状态标记为“已取消”,并清理对应的临时分片文件。
这样能避免磁盘上堆积无用数据。

请求示例:

POST /api/upload/cancel
{
"uploadId": "b4f8e3a7-1a0c-4a1d-88af-61e98d91a49b"
}

返回示例:

{
"success": true,
"message": "任务已取消"
}

伪代码示例:

@PostMapping("/cancel")
public Result cancelUpload(@RequestBody UploadCancelRequest req) {
// 1. 查找对应的 upload_task
// 2. 更新任务状态为 “已取消”
// 3. 删除或标记已上传的分片文件为待清理
// 4. 返回操作结果
}

七、/upload/list —— 查询任务列表

这个接口我们用于管理后台查看当前上传任务的整体情况。
可以展示每个任务的文件名、大小、进度、状态、上传人等信息,方便追踪和审计。

请求示例:

GET /api/upload/list

返回示例:

{
"success": true,
"data": [
{
"uploadId": "b4f8e3a7-1a0c-4a1d-88af-61e98d91a49b",
"fileName": "training-docs.zip",
"status": "COMPLETED",
"uploadedChunks": 320,
"totalChunks": 320,
"uploader": "admin",
"createdAt": "2025-10-20 14:30:12"
}
]
}

伪代码示例:

@GetMapping("/list")
public Result> listUploadTasks() {
// 1. 查询所有上传任务
// 2. 按创建时间或状态排序
// 3. 返回任务摘要信息(任务名、状态、进度、上传人等)
}

接口调用顺序小结

那我们这整个上传过程的调用顺序就是:

1. /upload/check     → 秒传检测
2. /upload/init → 初始化上传任务
3. /upload/chunk → 循环上传所有分片
4. /upload/merge → 所有分片完成后合并
(可选)/upload/pause、/upload/cancel 用于控制任务
(可选)/upload/list 用于任务追踪与审计

接口调用顺序示意图

下面这张时序图展示了前端、后端、数据库在整个上传过程中的交互关系。

Untitled diagram-2025-11-10-031845.png

这样安排有几个好处:

  1. 逻辑衔接顺:上面刚讲完每个接口的职责,下面立刻用图总结;
  2. 视觉节奏平衡:读者读到这里已经看了不少文字,用图能缓解阅读疲劳;
  3. 承上启下:这张图既总结接口流程,又能自然引出下一节“数据库表设计”。

这套接口设计基本能覆盖大文件上传在企业项目中的常见需求。
接下来,我们再来看看支撑这套接口背后的数据库表设计。
数据库的作用是让上传任务的状态可追踪、可恢复,也能在集群部署时保持一致性。


数据库表设计思路

前面说的那一套接口,要真正稳定地跑起来,
后端必须有一套能记录任务状态、分片信息、文件存储路径的数据库结构。
因为上传这种场景不是“一次请求就结束”的操作,它往往会持续几分钟甚至几个小时,
所以我们需要让任务状态可以追踪、可以恢复,还要能支撑集群部署。

我这次主要设计了三张核心表:
upload_task(上传任务表)、upload_chunk(分片表)、file_info(文件信息表)。
它们分别负责记录任务、分片和最终文件三层的数据关系。

一、upload_task —— 上传任务表

这张表是整个上传过程的“总账”,
每一个文件上传任务,不管分成多少片,都会在这里生成一条记录。
它主要用来保存任务的全局信息,比如文件名、大小、上传进度、状态、存储方式等。

CREATE TABLE `upload_task` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`upload_id` varchar(64) NOT NULL COMMENT '任务唯一ID(UUID)',
`file_hash` varchar(64) NOT NULL COMMENT '文件哈希(用于秒传与断点续传)',
`file_name` varchar(255) NOT NULL COMMENT '文件名称',
`file_size` bigint(20) NOT NULL COMMENT '文件总大小(字节)',
`chunk_size` bigint(20) NOT NULL COMMENT '每个分片大小(字节)',
`total_chunks` int(11) NOT NULL COMMENT '分片总数',
`uploaded_chunks` int(11) DEFAULT '0' COMMENT '已上传分片数量',
`status` tinyint(4) DEFAULT '0' COMMENT '任务状态:0-待上传 1-上传中 2-合并中 3-完成 4-取消 5-失败 6-已合并 7-已暂停',
`storage_type` varchar(32) DEFAULT 'local' COMMENT '存储类型:local/oss/cos/minio/s3等',
`storage_url` varchar(512) DEFAULT NULL COMMENT '文件最终存储地址(云端或本地路径)',
`local_path` varchar(512) DEFAULT NULL COMMENT '本地临时文件或合并文件路径',
`remark` varchar(255) DEFAULT NULL COMMENT '备注信息',
`uploader` varchar(64) DEFAULT NULL COMMENT '上传人',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `upload_id` (`upload_id`),
KEY `idx_hash` (`file_hash`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='上传任务表(支持多种云存储)';

设计要点:

  • upload_id 是前端初始化任务后由后端生成的唯一标识;
  • file_hash 用来支持秒传逻辑;
  • status 控制任务生命周期(等待、上传中、合并中、完成等);
  • storage_typestorage_url 可以兼容多种存储方案(本地、OSS、COS、MinIO);
  • uploaded_chunks 字段让任务能随时恢复,适配断点续传。

二、upload_chunk —— 分片表

这张表对应每个上传任务下的所有分片。
每一个分片都会单独在这里占一条记录,用来追踪它的上传状态。
这张表的存在让我们能做断点续传、进度统计、以及合并前的完整性检查。

CREATE TABLE `upload_chunk` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`upload_id` varchar(64) NOT NULL COMMENT '所属上传任务ID',
`chunk_index` int(11) NOT NULL COMMENT '分片索引(从0开始)',
`chunk_size` bigint(20) NOT NULL COMMENT '实际分片大小(字节)',
`chunk_hash` varchar(64) DEFAULT NULL COMMENT '可选:分片hash(用于高级去重)',
`status` tinyint(4) DEFAULT '0' COMMENT '状态:0-待上传 1-已上传 2-已合并',
`local_path` varchar(512) DEFAULT NULL COMMENT '分片本地路径',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_task_chunk` (`upload_id`,`chunk_index`),
KEY `idx_upload_id` (`upload_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='上传分片表';

设计要点:

  • upload_id 是任务外键,和 upload_task 一一对应;
  • chunk_index 代表分片顺序,合并文件时会按这个排序;
  • chunk_hash 可选字段,用来在上传前后做完整性校验;
  • status 字段控制上传进度(待上传、已上传、已合并);
  • 唯一索引 (upload_idchunk_index) 避免重复插入分片。

通过这张表,我们可以轻松实现断点续传:
当用户重新开始上传时,后端只返回未完成的分片索引,前端跳过已上传的部分。

三、file_info —— 文件信息表

这张表记录的是上传完成后的“最终文件信息”,
相当于系统的文件索引表。只要文件合并成功并通过校验,
后端就会往这里写入一条记录。

这张表支撑秒传功能,也能被后续的文档解析或向量化任务使用。

CREATE TABLE `file_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`file_hash` varchar(64) NOT NULL COMMENT '文件hash,用于秒传',
`file_name` varchar(255) NOT NULL COMMENT '文件名称',
`file_size` bigint(20) NOT NULL COMMENT '文件大小',
`storage_type` varchar(32) DEFAULT 'local' COMMENT '存储类型:local/oss/cos/minio/s3等',
`storage_url` varchar(512) DEFAULT NULL COMMENT '文件最终存储地址(云端或本地路径)',
`uploader` varchar(64) DEFAULT NULL COMMENT '上传人',
`status` tinyint(4) DEFAULT '1' COMMENT '状态:1-正常,2-删除中,3-已归档',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `file_hash` (`file_hash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='已上传文件信息表(支持多云存储)';

设计要点:

  • file_hash 是全局唯一标识,用于秒传和查重;
  • storage_url 记录最终可访问路径;
  • status 可扩展为删除、归档等后续操作;
  • 这张表和业务系统中的“文档解析”、“知识库构建”可以直接关联。

四、三张表之间的关系

这三张表之间的关系我们可以简单理解为:

upload_task  (上传任务)
├── upload_chunk (分片详情)
└── file_info (最终文件)
  • upload_task 管理任务生命周期;
  • upload_chunk 跟踪每个分片的上传进度;
  • file_info 保存最终文件索引,用于秒传与后续 AI 处理。

这样设计的好处是:

  • 上传状态可追踪;
  • 上传任务可恢复;
  • 文件信息可统一管理;
  • 多节点部署也能保证一致性。

上传状态流转与任务恢复机制

有了前面的三张核心表,整个上传的过程就能被“状态机化”管理。
简单来说,我们希望每一个上传任务从创建、上传、合并到完成,都能有一个明确的状态,
系统也能在任意阶段中断后恢复,不需要用户重新来一遍。

我们把整个上传任务的生命周期划分成几个关键状态:

WAITING(0, "待上传"),
UPLOADING(1, "上传中"),
MERGING(2, "合并中"),
COMPLETED(3, "已完成"),
CANCELED(4, "已取消"),
FAILED(5, "上传失败"),
CHUNK_MERGED(6, "已合并"),
PAUSED(7, "已暂停");

一、WAITING(待上传)

当用户在前端发起上传、文件切片还没真正传上来之前,
系统会先生成一个上传任务记录(也就是 /upload/init 接口那一步)。
这个时候任务只是“登记”在数据库里,还没开始传数据。

我们可以理解为:

任务刚创建,还没开始跑。

此时前端拿到 uploadId,就可以开始逐片上传了。

在数据库层面,upload_task.status = 0,所有的分片表里还没有数据。

二、UPLOADING(上传中)

当第一个分片开始上传时,系统会把任务状态更新为 上传中
这时候每上传一块分片,都会往 upload_chunk 表里写入一条记录,
并且更新任务的 uploaded_chunks 字段。

我们会周期性地根据分片上传数量去更新进度条,
比如已上传 35 / 100 块,系统就知道这部分可以恢复。

这个阶段是任务生命周期里最活跃的一段:
用户可能暂停、断网、刷新页面、甚至浏览器崩溃。
但是没关系,因为分片信息都落地到数据库了,
我们能随时通过 upload_chunk 的状态重新恢复上传。

三、PAUSED(已暂停)

如果用户主动点击“暂停上传”,
系统就会把任务状态标记为 PAUSED

暂停并不会删除分片,只是告诉系统“不要再继续发请求”。
这样当用户重新点击“继续上传”时,
前端只需从后端拿到“哪些分片还没上传”,就能断点续传。

这个状态一般只在用户控制的情况下出现,
比如网络不好、或者中途切换网络时暂停。

四、CANCELED(已取消)

取消和暂停不同,取消意味着用户彻底放弃了这个上传任务。
任务会被标记为 CANCELED,同时系统可以选择:

  • 删除已经上传的临时分片文件;
  • 或者保留一段时间等待清理任务。

在后台日志中,这个状态主要用于审计:
记录谁取消了任务、在什么时间、上传了多少进度。

五、MERGING(合并中)

当所有分片都上传完成后,
后端会自动或手动触发文件合并逻辑(调用 /upload/merge)。
此时任务状态会切换为 MERGING,表示系统正在进行最后一步。

在这一步里:

  • 如果是本地存储,会逐个读取分片文件并拼接为完整文件;
  • 如果是云存储(比如 OSS、MinIO),则会触发服务端的分片合并 API。

合并过程通常比较耗时,尤其是几 GB 的文件,
所以单独拿出来作为一个明确状态是必要的。

六、CHUNK_MERGED(已合并)

有些系统会把合并成功但未做后续处理的状态单独标出来,
比如文件已经合并,但还没入库、还没解析。
这个状态可以让我们在合并之后还有机会做文件校验或后处理。

不过在实际项目里,也可以直接跳过这一步,
合并完后立刻进入下一状态——COMPLETED

七、COMPLETED(已完成)

文件合并完成、验证通过、存储路径落地、写入 file_info 表,
这时候任务就算彻底完成了。

在这个状态下:

  • 用户可以正常访问文件;
  • 系统可以执行后续的解析任务(比如文档拆页、向量化等);
  • 文件具备秒传条件,下次再上传同样的文件会直接跳过。

COMPLETED 是整个生命周期的终点状态。
在数据库中,任务记录会更新最终路径、存储类型、完成时间等字段。

八、FAILED(上传失败)

上传过程中如果出现异常,比如网络中断、磁盘写入异常、OSS 上传失败等,
系统会标记任务为 FAILED
这一状态不会自动清理,
方便管理员事后追踪错误原因或人工恢复。

失败任务在设计上一般允许“重新启动”,
也就是通过任务 ID 重新触发上传,从未完成的分片继续。

我们可以通过下面这张图可以更直观地看到整个上传任务的生命周期:

Untitled diagram-2025-11-10-033341.png

九、任务恢复机制

在这套机制下,任务恢复就变得非常自然。
前端每次进入上传页面时,只要传入文件的 hash,
后端就能通过 upload_task 和 upload_chunk 判断:

  1. 这个文件有没有上传任务;
  2. 如果有,哪些分片已经上传;
  3. 任务当前状态是什么(暂停、失败还是上传中)。

然后前端只需补传那些未上传的分片即可。
这就是我们常说的 断点续传(Resumable Upload) 。

在集群环境中,这套逻辑同样成立,
因为任务与分片状态都落在数据库,不依赖单台服务器。
无论请求打到哪一台机器,上传进度都是统一可见的。

十、中断后如何续传

在实际使用中,用户上传中断是很常见的。
比如文件太大上传到一半,浏览器突然关了;
或者公司网络断了,机器重启了;
甚至有人直接换了电脑继续操作。

如果系统没有任务恢复机制,那用户每次都得重新传一遍,
尤其是那种几个 G 的文件,不但浪费时间,还容易出错。
所以我们在设计这套上传中心时,
一开始就考虑了“断点续传”和“任务恢复”的问题。

1. 恢复上传靠的其实是数据库里的状态

断点续传的核心逻辑,其实很简单:
我们让任务和分片的状态都写进数据库。

每当用户重新进入上传页面、选中同一个文件时,
前端会先计算出文件的 hash,然后调用 /upload/check 接口。
后端收到 hash 后,会依次去查三张表:

  1. 先查 file_info
    如果能查到,说明文件之前已经上传并合并成功,
    这时候直接返回“文件已存在”,前端就能实现“秒传”,不需要重新上传。
  2. 查不到 file_info,就去查 upload_task
    如果找到了对应任务,就说明这个文件上传到一半被中断了。
    这时我们会返回这个任务的 uploadId。
  3. 再查 `upload_chunk``
    系统会统计出哪些分片已经上传成功,哪些还没传。
    然后返回一个“未完成的分片索引列表”给前端。

前端拿到这些信息后,就能从中断的地方继续往下传,
不用再重复上传已经完成的部分。

2. 前端续传时的流程

前端拿到旧的 uploadId 和未完成分片列表后,
只需要跳过那些已经上传成功的分片,
然后照常调用 /upload/chunk 去上传剩下的部分。

上传过程中,每个分片的状态都会被实时更新到 upload_chunk 表中,
upload_task 表的 uploaded_chunks 也会跟着同步增加。
当所有分片都上传完后,任务状态自动进入 MERGING(合并中)阶段。

所以整个续传过程,其实就是**“基于数据库状态的增量上传”**。
用户不需要额外操作,系统自己就能恢复上次的进度。

3. 任务状态和恢复判断

任务是否允许恢复,系统会根据 upload_task.status 来判断。
大致逻辑是这样的:

状态是否可恢复说明
WAITING可以任务刚创建,还没开始传
UPLOADING可以正在上传中,可以继续
PAUSED可以用户主动暂停,可以恢复
FAILED可以上传失败,可以重新尝试
CANCELED不可以用户主动取消,不再恢复
COMPLETED不需要已经完成,直接秒传
MERGING等待中系统正在合并,前端等待即可

这套判断逻辑让任务的行为更清晰。
比如用户暂停上传再回来时,可以直接恢复;
如果任务已经取消,那就算用户重启也不会再自动续传。

4. 多机器部署下的恢复问题

有些人会担心:如果我们的系统是集群部署的,
上传时中断后再续传,万一请求打到另一台机器上,
还能恢复吗?

其实没问题。
因为我们所有任务和分片的状态都是写进数据库的,
不依赖内存或本地文件。

也就是说,哪怕用户上次上传在 A 机器,这次续传到了 B 机器,
系统仍然能根据数据库的记录知道:
这个 uploadId 下的哪些分片已经上传完,哪些还没传。

所以集群部署下也能无缝续传,不会出现“不同机器不认任务”的情况。

5. 小结

整个任务恢复机制靠的就是两张表:upload_task 和 upload_chunk
upload_task 负责记录任务总体进度,
upload_chunk 负责记录每个分片的上传状态。

当用户重新上传时,我们查表判断进度,
前端从未完成的地方继续传,就能实现真正意义上的“断点续传”。

这套机制有几个显著的好处:

  • 上传进度可追踪;
  • 中断后可恢复;
  • 支持集群部署;
  • 不依赖浏览器缓存或 Session。

所以,只要数据库没丢,任务记录还在,
上传进度就能恢复,哪怕换机器、重启系统都没问题。


文件合并与完整性校验

前面的所有步骤,其实都是在为这一刻做准备。
当用户的所有分片都上传完成后,接下来最重要的工作就是:
把这些分片拼成一个完整的文件,并且确保文件内容没有出错。

这一步看似简单,但其实是整个大文件上传流程里最容易出问题的地方。
尤其在集群部署下,如果不同分片分布在不同机器上,
那合并逻辑就不能只靠本地文件路径去拼接,否则根本找不到所有分片。

所以我们先来理一理整个思路。

一、合并的触发时机

前端在检测到所有分片都上传完成后,会调用 /upload/merge 接口。
这个接口的作用就是通知后端:
“这个任务的所有分片都传完了,现在可以开始合并了。”

后端接收到请求后,会先去查数据库确认几个关键信息:

  1. 这个任务对应的 uploadId 是否存在;
  2. upload_chunk 表里所有分片是否都处于 “已上传” 状态;
  3. 当前任务状态是否允许合并(例如不是暂停、取消或失败)。

确认无误后,任务状态会从 UPLOADING 变成 MERGING
正式进入文件合并阶段。

二、本地合并逻辑

如果系统配置的是本地存储(也就是 cloud.enable = false),
那所有分片文件都保存在服务器的临时目录中。

合并逻辑大致是这样的:

  1. 后端按分片的 chunk_index 顺序,依次读取每个分片文件。
  2. 逐个写入到一个新的目标文件中,比如 merge.zip
  3. 每合并一个分片,就更新数据库中的状态。
  4. 合并完成后,把任务状态更新为 COMPLETED,并写入最终路径。

整个过程看起来很直观,
但这里有两个要点需要特别注意:

  • 写入顺序要严格按照分片索引,否则文件内容会错乱;
  • 文件 IO 要用流式写入(Stream) ,避免内存一次性读取所有分片导致溢出。

合并完成后,我们会计算整个文件的 MD5,与原始 fileHash 对比,
如果不一致,就说明合并过程中数据丢失或出错。
这种情况任务会被标记为 FAILED,并在日志中留下异常记录。

三、云端合并逻辑

如果我们配置了云存储(比如 OSS、COS、MinIO 等),
那分片文件就不是存在本地磁盘,而是上传到云端的对象存储桶里。

在这种情况下,合并逻辑就不需要我们自己拼文件了,
因为大部分云存储服务都提供了“分片合并”的 API。

比如以 OSS 为例,上传时我们调用的是 uploadPart 接口,
合并时只需要调用 completeMultipartUpload
它会根据上传时的分片顺序自动合并为一个完整对象。

整个过程的优点是:

  • 不占用本地磁盘;
  • 不受单机 IO 限制;
  • 云端自动校验每个分片的完整性。

所以在云存储场景下,我们只需要做两件事:

  1. 通知云服务去执行合并;
  2. 成功后记录最终的文件地址(storage_url)到数据库。

这样整个流程就闭环了。

四、集群部署下的合并问题

单机情况下,合并很简单,因为所有分片都在本地。
但如果系统是集群部署的,分片请求可能打到了不同机器,
这时候分片文件就会分散在多个节点上。

我们在设计时考虑了三种解决方案:

方案 1:共享存储(私有化部署下比较推荐)

最常见的做法是把所有机器的上传目录指向同一个共享路径,
比如通过 NFS、NAS、或对象存储挂载到 /data/uploads
这样无论用户上传的分片打到哪台机器,
最终都会写入同一个物理目录。

当合并请求发起时,任意一台机器都能访问到完整的分片文件。
这是目前在企业部署中最稳定、最通用的方案。

方案 2:云存储中转

如果机器之间没有共享目录,那我们可以让每个分片先上传到云端,
合并时再调用云服务的 API 进行分片合并。
这种方式适合公网可访问的 SaaS 环境。
但对于政企内网部署,就不一定行得通。

方案 3:统一调度节点

还有一种是我们自己维护一个“合并调度节点”,
所有分片上传完后,系统会把合并任务分配到一个指定节点执行,
这个节点会从其他机器拉取分片(比如通过 HTTP 内部传输或 RPC)。
这种方式更复杂,适合大规模分布式存储场景。

在私有化项目中,我们一般采用第一种方式——共享目录 + 本地合并
既能保证性能,也能兼顾安全性。

五、完整性校验

文件合并完成后,最后一步是完整性校验。
我们会重新计算合并后文件的 MD5,与前端最初上传的 fileHash 对比。

如果一致,就说明文件合并成功,内容没有丢失;
如果不一致,就说明某个分片损坏或顺序错误,
任务会被标记为 FAILED,并自动记录错误日志。

这样可以确保文件数据的安全性,
避免在后续 AI 解析或向量化阶段出现内容异常。

六、异步处理与性能优化

开头的视频里我们也看到了,整个上传和合并过程我们是同步执行的。
从前端开始上传分片,到最后文件合并完成,都在等待同一个流程走完。
这种方式在演示时很直观,但在真实项目中其实问题不少。

最明显的一个问题就是——时间太长。
像我们刚才那个 1GB 的文件,即使网络稳定、服务器性能还可以,
整个流程也要几分钟甚至更久。
如果我们让前端一直等待响应,接口超时、连接断开、前端刷新这些问题就都会冒出来。

所以,在真正的业务系统里,我们一般会把合并、校验、迁移 OSS 或解析入库这些操作改成异步任务来做。
接口只负责接收分片、登记状态,然后立刻返回“任务已创建”或“上传完成,正在处理中”的提示。
后续的合并、校验、清理临时文件这些工作交给后台的异步线程、任务队列或者调度器去跑。

这样做的好处有几个:

  • 前端体验更流畅,不用卡在“等待合并”阶段;
  • 后端可以批量处理任务,减少高峰期的 IO 压力;
  • 如果任务失败或中断,也能通过任务表重试或补偿;
  • 对接外部存储或 AI 解析流程时,也能自然衔接后续任务链。

简单来说,上传只是第一步,
而合并、校验、转存这些操作本质上更像是后台任务。
我们在系统设计时只要把这些环节分开,让接口尽量“轻”,
这套上传系统就能在面对更大文件、更复杂场景时依然稳定可靠。

七、小结

整个合并与校验阶段,是把前面所有分片上传工作“收尾”的过程。
我们通过以下机制保证了稳定性:

  • 本地存储场景下:顺序读取 + 流式写入 + hash 校验;
  • 云存储场景下:依赖云端分片合并 API;
  • 集群环境下:通过共享存储或统一调度节点解决文件分散问题;
  • 数据库层面:实时记录状态,便于追踪和审计。

最终,当文件合并成功、校验通过后,
系统会将结果写入 file_info 表,
整条上传链路就算是完整闭环。


最后

我们平常做的项目,大多数时候文件上传都挺简单的。
前端传到 OSS,后端接个地址存起来就行。
但等真正做私有化项目的时候,也就会发现很多地方都不一样了。
要求更多,考虑的细节也多得多。

像这次做的大文件上传就是个很典型的例子。
以前那种简单方案,放在这种环境下就完全不够用了。
得考虑断点续传、任务恢复、集群部署、权限、审计这些东西,
一步没想好,后面全是坑。

我们现在这套设计,其实就是在解决这些“现实问题”。
接口虽然多一点,但每个职责都很清晰,
任务状态能追踪,上传中断能恢复,
甚至以后如果我们想单独抽出来做一个文件系统模块也完全没问题。
不管是拿来给知识库用,还是 AI 向量化、文档解析,这套逻辑都能复用。

其实很多以前觉得“简单”的功能,
一旦遇到复杂场景,其实都得重新想。
但好处是,一旦做通了,这套东西就能稳定用很久。

到这里,大文件上传这块我们算是完整走了一遍。
以后再遇到类似需求,我们就有经验了,
不用再从头掉坑里爬出来一次哈。


作者:洛卡卡了
来源:juejin.cn/post/7571355989133099023

收起阅读 »

只有 7 KB!前端圈疯传的 Vue3 转场动效神库!效果炸裂!

web
“只要 7 KB,就能把多页站变成丝滑 SPA!”——这句话在前端圈疯传的神库,就是 Barba.js。 今天,我们就把它和 Vue3 搭配,手把手带你做出**“效果炸裂”**的页面切换动效! 为什么是 Barba.js × Vue3? 天生轻量:gzip...
继续阅读 »

“只要 7 KB,就能把多页站变成丝滑 SPA!”——这句话在前端圈疯传的神库,就是 Barba.js


今天,我们就把它和 Vue3 搭配,手把手带你做出**“效果炸裂”**的页面切换动效!


6.gif


为什么是 Barba.js × Vue3?



  1. 天生轻量gzip 后仅 7 KB,0 依赖,对 Vue3bundle 体积几乎没影响。

  2. 渐进增强:不改现有路由,只要把需要动效的部分包一层 data-barba="container",老项目也能无痛升级。

  3. 动画自由:官方只负责“切换生命周期”,真正的视觉冲击可以交给 GSAPAnime.jsVue3<Transition> 组件。

  4. 社区炸裂:GitHub 1.2 w+ star,大量 Vue3 样板可直接抄作业。


4..gif


30 秒极速上手


1.安装


npm i @barba/core @barba/css     # 核心 + 零 JS 动画辅助
npm i gsap # 想玩高级动效再装

2.HTML 骨架(Vue3 单页或多页皆可)


<body data-barba="wrapper">
<header>公共头部</header>

<!-- Vue3 挂载点,也是 Barba 需要替换的区域 -->
<main id="app"
data-barba="container"
data-barba-namespace="home">

<!-- 这里放 <RouterView/> 或直接放组件 -->
</main>

<footer>公共底部</footer>
</body>

3.初始化(main.ts)


import barba from '@barba/core'
import barbaCss from '@barba/css'
import gsap from 'gsap'

barba.use(barbaCss) // 先让 Barba 帮你加/删 class
barba.init({
transitions: [{
name: 'cover', // 自定义名字
sync: true, // 进出同时执行,更顺滑
leave({ current }) {
// 当前页面滑出
return gsap.to(current.container, {
y: '-100%',
opacity: 0,
duration: 0.6,
ease: 'power2.inOut'
})
},
enter({ next }) {
// 新页面滑入
gsap.from(next.container, {
y: '100%',
opacity: 0,
duration: 0.6,
ease: 'power2.inOut'
})
}
}]
})


⚠️ 注意



  • 每次切换完成后,手动重新挂载 Vue3 实例(如果使用多页模式),或让组件复用 <keep-alive>

  • Barba 会帮你更新浏览器 historySEO 不受影响。



实战:3 个效果炸裂的技巧


视差 + 蒙版过渡


leave 钩子用 GSAP 把旧页面做 clip-path 收缩,新页面做 视差滑动,视觉冲击直接拉满。


1..gif


路由级差异化动效


利用 @barba/router/home/about淡入淡出,给 /portfolio/*/portfolio/*3D 翻转,保持品牌调性一致。


2..gif


鼠标悬停预加载


打开 @barba/prefetch,用户还没点击就把下一页提前拉回来,真正“秒开”体验。


3..gif


常见踩坑 & 解决方案


🔴 场景 1:首屏闪白

症状:刷新后能看到瞬间白屏,然后内容才出现。

快速解法



  1. <body data-barba="wrapper"> 先加 style="opacity:0"

  2. barba.init 里写一次性的 once 过渡:


   once({ next }) {
gsap.fromTo(next.container,
{ opacity: 0 },
{ opacity: 1, duration: 0.4, onComplete: () => document.body.style.opacity = 1 }
)
}

🔴 场景 2:Vue3 组件不销毁,内存暴涨

症状:来回切换页面后,控制台出现 [Vue warn]: Component is already mounted

快速解法

afterEnter 里手动卸载并重新挂载:


afterEnter({ next }) {
app?.unmount()
app = createApp(App)
app.mount('#app')
}

🔴 场景 3:滚动位置错乱

症状:A 页面滚到 800 px,跳转到 B 页面却直接回到顶部。

快速解法


barba.hooks.beforeLeave(() => {
history.replaceState({ ...history.state, scrollY: window.scrollY }, '')
})

barba.hooks.afterEnter(() => {
const { scrollY = 0 } = history.state || {}
window.scrollTo({ top: scrollY, behavior: 'smooth' })
})

🔴 场景 4:点击浏览器后退,页面样式瞬间全乱

症状:后退时 Barba 把缓存的 DOM 直接塞回,但 Vue3 样式作用域失效。

快速解法

<style scoped> 再补一个全局补丁:


/* barba 会把 container 整个替换,scoped 样式会丢 */
.barba-container[data-namespace="home"] .hero {
/* 重写一次关键样式 */
}

🔴 场景 5:移动端首次滑动卡顿

症状:iOS Safari 第一次滑屏有 300 ms 延迟。

快速解法

barba.init 里关闭预加载的 timeout


barba.init({
timeout: 0, // 不等待 requestIdleCallback
})

写在最后


Barba.js 并不是一个“Vue3 专用”的库,但正是这种 框架无关 的特性,让它在 Vue3 项目里反而更自由:



  • 你可以继续用 Vue RouterSPA

  • 也可以把多页站改造成“伪 SPA”,却保留 SSRSEO 优势。


一句话:如果你受够了页面跳转的“闪白”,又不想折腾整站改造成 SPA,Barba.js + Vue3 就是目前性价比最高的动效解!



作者:前端开发爱好者
来源:juejin.cn/post/7532287059374506027
收起阅读 »

Hutool被卖半年多了,现状是逆袭还是沉寂?

是的,没错。那个被人熟知的国产开源框架 Hutool 距离被卖已经过去近 7 个月了。 那 Hutool 现在的发展如何呢?它未来有哪些更新计划呢?Hutool AI 又该如何使用呢?如果不想用 Hutool 有没有可替代的框架呢? 近半年现状 从 Hutoo...
继续阅读 »

是的,没错。那个被人熟知的国产开源框架 Hutool 距离被卖已经过去近 7 个月了。


那 Hutool 现在的发展如何呢?它未来有哪些更新计划呢?Hutool AI 又该如何使用呢?如果不想用 Hutool 有没有可替代的框架呢?


近半年现状


从 Hutool 官网可以看出,其被卖近 7 个月内仅发布了 4 个版本更新,除了少量的新功能外,大多是 Bug 修复,当期在此期间发布了 Hutool AI 模块,算是一个里程碑式的更新




更新日志:hutool.cn/docs/#/CHAN…



收购公司


没错,收购 Hutool 的这家公司和收购 AList 的公司是同一家公司(不够科技),该公司前段时间因为其在收购 AList 代码中悄悄收集用户设备信息,而被推向过风口浪尖,业内人士认为其收购开源框架就是为了“投毒”,所以为此让收购框架损失了很多忠实的用户。



其实,放眼望去那些 APP 公司收集用户设备和用户信息属于家常便饭了(国内隐私侵犯问题比较严重),但 AList 因为其未做文档声明,且未将收集设备信息的代码提交到公共仓库,所以大家发现之后才会比较气愤。



Hutool-AI模块使用


Hutool AI 模块的发布算是被收购之后发布的最值得让人欣喜的事了,使用它可以对接各大 AI 模型的工具模块,提供了统一的 API 接口来访问不同的 AI 服务。


目前支持 DeepSeek、OpenAI、Grok 和豆包等主流 AI 大模型。


该模块的主要特点包括:



  • 统一的 API 设计,简化不同 AI 服务的调用方式。

  • 支持多种主流 AI 模型服务。

  • 灵活的配置方式。

  • 开箱即用的工具方法。

  • 一行代码调用。


具体使用如下。


1.添加依赖


<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-ai</artifactId>
<version>5.8.38</version>
</dependency>

2.调用API


实现对话功能:


DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()).setApiKey(key).setModel("your bots id").build(), DoubaoService.class);
ArrayList<Message> messages = new ArrayList<>();
messages.add(new Message("system","你是什么都可以"));
messages.add(new Message("user","你想做些什么"));
String botsChat = doubaoService.botsChat(messages);

识别图片:


//可以使用base64图片
DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()).setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.class);
String base64 = ImgUtil.toBase64DataUri(Toolkit.getDefaultToolkit().createImage("your imageUrl"), "png");
String chatVision = doubaoService.chatVision("图片上有些什么?", Arrays.asList(base64));

//也可以使用网络图片
DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()).setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.class);
String chatVision = doubaoService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"),DoubaoCommon.DoubaoVision.HIGH.getDetail());


生成视频:


//创建视频任务
DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()).setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class);
String videoTasks = doubaoService.videoTasks("生成一段动画视频,主角是大耳朵图图,一个活泼可爱的小男孩。视频中图图在公园里玩耍," +
"画面采用明亮温暖的卡通风格,色彩鲜艳,动作流畅。背景音乐轻快活泼,带有冒险感,音效包括鸟叫声、欢笑声和山洞回声。", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");

//查询视频生成任务信息
String videoTasksInfo = doubaoService.getVideoTasksInfo("任务id");

未来发展



  • Hutool5:目前 Hutool 5.x 版本主要是基于 JDK 8 实现的,后面更新主要以 BUG 修复为准。

  • Hutool6:主要以功能尝鲜为主。

  • Hutool7:升级为 JDK 17,添加一些新功能,删除一些不用的类。


目前只发布了 Hutool 5.x,按照目前的更新进度来看,不知何时才能盼来 Hutool7 的发布。


同类替代框架


如果担心 Hutool 有安全性问题,或更新不及时的问题可以尝试使用同类开源工具类:



视频解析


http://www.bilibili.com/video/BV1QR…


小结


虽然我们不知道 Hutool 被收购意味着什么?是会变的越来越好?还是会就此陨落?我们都不知道答案,所以只能把这个问题交给时间。但从个人情感的角度出发,我希望国产开源框架越做越好。好了,我是磊哥,咱们下期见。



本文已收录到我的面试小站 http://www.javacn.site,其中包含的内容有:场景题、SpringAI、SpringAIAlibaba、并发编程、MySQL、Redis、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、JVM、设计模式、消息队列、Dify、Coze、AI常见面试题等。



作者:Java中文社群
来源:juejin.cn/post/7547624644507156520
收起阅读 »

前端何时能出个"秦始皇"一统天下?我是真学不动啦!

web
前端何时能出个"秦始皇"一统天下?我是真学不动啦! 引言 前端开发的世界,就像历史上的战国时期一样,各种框架、库、工具层出不穷,形成了一个百花齐放但也令人眼花缭乱的局面。 而且就因为百家争鸣,导致各种鄙视链出现 比如 React 和 Vue 互喷 v:你re...
继续阅读 »

前端何时能出个"秦始皇"一统天下?我是真学不动啦!


引言


前端开发的世界,就像历史上的战国时期一样,各种框架、库、工具层出不穷,形成了一个百花齐放但也令人眼花缭乱的局面。




而且就因为百家争鸣,导致各种鄙视链出现


比如 React 和 Vue 互喷


v:你react 这么难用,不如我vue 简单


r:你一点都不灵活,我想咋用咋用


v:你useEffect 心智负担太重,一点都好用


r:啥心智负担,那是你太笨了,我就喜欢这种什么都掌握在自己手里的感觉


v:你内部更是混乱,一个状态管理就那么多种 啥redux、mobx、recoil。。。。不像我们一个pinia 走天下


r:你管我 我想用哪个用哪个,你还说我,你内部对一个 用ref还是用reactive 都吵得不可开交!


......


2.jpeg


1. 框架之争



  • React: 由Facebook维护的一个用于构建用户界面的JavaScript库。其设计理念是通过组件化的方式简化复杂的UI开发。




  • Vue.js: 一种渐进式JavaScript框架,非常适合用来构建单页应用。Vue的核心库只关注视图层,易于上手。




  • Angular: Google支持的一个开源Web应用框架,适用于大型企业级项目。它提供了一个全面的解决方案来创建动态Web应用程序。




  • Solid.js: 一个专注于性能和简单性的声明式UI库,采用细粒度的响应式系统,提供了极高的运行效率。




  • Svelte: 一种新兴的前端框架,通过在编译时将组件转换为高效的原生代码,从而避免了运行时开销。




  • Ember.js: 一个旨在帮助开发者构建可扩展的Web应用的框架,尤其适合大型团队协作。







2. 样式处理满花齐放


样式处理方面可以进一步细分,包括CSS预处理器、CSS-in-JS、Utility-First CSS框架以及CSS Modules等。



  • CSS预处理器



    • Sass: 提供变量、嵌套规则等高级功能,极大地提高了CSS代码的可维护性。


    • Less: 另一种流行的CSS预处理器,支持类似的功能但语法稍有不同。


    • Stylus: 一款灵活且功能强大的CSS预处理器,允许省略括号和分号等符号,使代码更加简洁。




  • CSS-in-JS




  • 原子化css



    • Tailwind CSS: 一种实用优先的CSS框架,让你可以通过低级实用程序类构建定制设计。




    • UnoCSS: 新一代的原子化CSS引擎,旨在提供极致的性能和灵活性。




    • Windi CSS: 一个基于Tailwind CSS的即时按需CSS框架,提供了更快的开发体验。




    • GitHub Stars: 约6.5k(截至2025年4月)






3. 构建工具五花八门


构建工具是现代前端开发不可或缺的一部分,它们负责将源代码转换为生产环境可用的形式,并优化性能。



  • Webpack: 一个模块打包工具,广泛用于复杂的前端项目中。它支持多种文件类型的处理,并具有强大的插件生态。




  • Vite: 由Vue.js作者尤雨溪开发的下一代前端构建工具,以其极快的冷启动速度和热更新闻名。




  • Rollup: 一个专注于JavaScript库的打包工具,特别适合构建小型库或框架。




  • Rspack: 一个基于Rust实现的高性能构建工具,兼容Webpack配置,旨在提供更快的构建速度。




  • esbuild: 一个用Go语言编写的极速打包工具,专为现代JavaScript项目设计。




  • Turbopack: 由Next.js团队推出的下一代构建工具,号称比Webpack快700倍。




  • Rolldown: 一个基于Rust的Rollup替代方案,旨在提供更快的构建速度和更高的性能。





对比分析:



  • Webpack 是目前最成熟的构建工具,生态系统庞大,但配置复杂度较高。

  • Vite 凭借其快速的开发体验迅速崛起,尤其在中小型项目中表现优异。

  • Rollup 更适合轻量级项目或库的构建,虽然社区规模较小,但在特定场景下非常高效。

  • Rspackesbuild 利用高性能语言(如Rust和Go)实现了极快的构建速度,适合对性能要求较高的项目。

  • Turbopack 是新兴工具,主打极速构建,未来可能成为Webpack的有力竞争者。

  • Rolldown 提供了另一种基于Rust的高速构建解决方案,特别针对Rollup用户群体。




4. 包管理工具逐步更新





5. 状态管理百家争鸣


状态管理是前端开发中的重要组成部分,它帮助开发者有效地管理应用的状态变化。





6. JavaScript运行时环境都有好几种


JavaScript运行时环境是现代前端和后端开发的核心部分,它决定了代码如何被解析和执行。以下是几种主流的JavaScript运行时环境:



  • Node.js:



    • Node.js 是一个基于Chrome V8引擎的JavaScript运行时,广泛用于构建服务器端应用、命令行工具以及全栈开发。

    • 它拥有庞大的生态系统,npm作为其默认包管理器,已经成为全球最大的软件注册表。

    • 官网: nodejs.org/

    • GitHub: github.com/nodejs/node

    • GitHub Stars: 约111k(截至2025年4月)



  • Deno:



    • Deno 是由Node.js的原作者Ryan Dahl创建的一个现代化JavaScript/TypeScript运行时,旨在解决Node.js的一些设计缺陷。

    • 它内置了对TypeScript的支持,并提供了更安全的权限模型(如文件系统访问需要显式授权)。

    • Deno还集成了标准库,无需依赖第三方模块即可完成许多常见任务。

    • 官网: deno.land/

    • GitHub: github.com/denoland/de…

    • GitHub Stars: 约103k(截至2025年4月)



  • Bun:



    • Bun 是一个新兴的JavaScript运行时,旨在提供更快的性能和更高效的开发体验。

    • 它不仅可以用作运行时环境,还可以替代npm、Yarn等包管理工具,同时支持ES Modules和CommonJS。

    • Bun的目标是成为Node.js和Deno的强大竞争者,特别适合高性能需求的场景。

    • 官网: bun.sh/

    • GitHub: github.com/oven-sh/bun

    • GitHub Stars: 约77.5k(截至2025年4月)




对比分析:



  • Node.js 是目前最成熟且广泛应用的JavaScript运行时,尤其在企业级项目中占据主导地位。

  • Deno 提供了更现代化的设计理念,特别是在安全性、TypeScript支持和内置工具方面表现突出。

  • Bun 是一个新兴的选手,凭借其极速的性能和多功能性迅速吸引了开发者关注,未来潜力巨大。




7. 跨平台开发


随着移动设备和多终端生态的普及,跨平台开发成为现代应用开发的重要方向。以下是几种主流的跨平台开发工具和技术:



  • React Native:



    • React Native 是由Facebook推出的一个基于React的跨平台移动应用开发框架,允许开发者使用JavaScript和React构建原生性能的iOS和Android应用。

    • 它提供了丰富的社区支持和插件生态,适合需要快速迭代的项目。

    • 官网: reactnative.dev/

    • GitHub: github.com/facebook/re…

    • GitHub Stars: 约122k(截至2025年4月)



  • Flutter:



    • Flutter 是由Google开发的一个开源UI框架,使用Dart语言构建高性能的跨平台应用。

    • 它通过自绘引擎渲染UI,提供了一致的用户体验,并支持Web、iOS、Android以及桌面端开发。

    • 官网: flutter.dev/

    • GitHub: github.com/flutter/flu…

    • GitHub Stars: 约170k(截至2025年4月)



  • Electron:



    • Electron 是一个用于构建跨平台桌面应用的框架,基于Node.js和Chromium,广泛应用于桌面端应用开发。

    • 它允许开发者使用Web技术(HTML、CSS、JavaScript)构建功能强大的桌面应用,但可能会导致较大的应用体积。

    • 官网: http://www.electronjs.org/

    • GitHub: github.com/electron/el…

    • GitHub Stars: 约116k(截至2025年4月)



  • Tauri:



    • Tauri 是一个轻量级的跨平台桌面应用框架,旨在替代Electron,提供更小的应用体积和更高的安全性。

    • 它利用系统的原生Webview来渲染UI,同时支持Rust作为后端语言,从而实现更高的性能。

    • 官网: tauri.app/

    • GitHub: github.com/tauri-apps/…

    • GitHub Stars: 约91.5k(截至2025年4月)



  • Capacitor:



    • Capacitor 是由Ionic团队推出的一个跨平台工具,允许开发者将Web应用封装为原生应用。

    • 它支持iOS、Android和Web,并提供了丰富的插件生态,方便调用原生设备功能。

    • 官网: capacitorjs.com/

    • GitHub: github.com/ionic-team/…

    • GitHub Stars: 约13.1k(截至2025年4月)



  • UniApp:



    • UniApp 是一个基于 Vue.js 的跨平台开发框架,能够将代码编译到多个平台,包括微信小程序、H5、iOS、Android以及其他小程序(如支付宝小程序、百度小程序等)。

    • 它的优势在于一次编写,多端运行,特别适合需要覆盖多个小程序平台的项目。

    • 官网: uniapp.dcloud.io/

    • GitHub: github.com/dcloudio/un…

    • GitHub Stars: 约40.6k(截至2025年4月)




对比分析:



  • React NativeFlutter 是移动端跨平台开发的两大主流选择,分别适合熟悉JavaScript和Dart的开发者。

  • Electron 是桌面端跨平台开发的经典解决方案,虽然体积较大,但易于上手。

  • Tauri 提供了更轻量化的桌面端开发方案,适合对性能和安全性有更高要求的项目。

  • Capacitor 则是一个灵活的工具,特别适合将现有的Web应用快速迁移到移动端。

  • UniApp 非常适合需要覆盖多种小程序平台的项目,尤其在国内的小程序生态中表现出色。




结论


你看我这还是只是列举了一部分,都这么多了,学前端的是真的命苦啊,真心学不动了。


1.jpeg


而且最近 尤雨溪宣布成立 VoidZero 说是一代JavaScript工具链,能够统一前端 开发构建工具,如果真能做到,真是一件令人振奋的事情,希望尤雨溪能做到跟 spring 一样统一java 天下 把前端的天下给统一了,大家觉得有可能么?


作者:前端摸鱼杭小哥
来源:juejin.cn/post/7493420166878822450
收起阅读 »

35岁程序员失业了,除了送外卖,还能做什么?

很多人可能都觉得,被裁了再找一份工作就好了。但是对于35岁的程序员来说,这可能很难。 35岁程序员失业并不是个别现象,而是很多人都已经遇到了的问题。有可能有外部环境的因素导致,也有可能是个人自身的原因。 为什么? 1.技术更新太快 互联网行业的技术更新速度真的...
继续阅读 »

很多人可能都觉得,被裁了再找一份工作就好了。但是对于35岁的程序员来说,这可能很难。


35岁程序员失业并不是个别现象,而是很多人都已经遇到了的问题。有可能有外部环境的因素导致,也有可能是个人自身的原因。


为什么?


1.技术更新太快


互联网行业的技术更新速度真的太快了。
记得我刚工作的时候,前端还在用JQuery,一转眼React、Vue已经成为标配了。


Java也从当初的jsp,到现在的springboot,springCloud。
现在大数据、人工智能、AI等新技术层出不穷。如果不持续学习,很容易就会被淘汰。


2.年轻人更有干劲


这是一个很显示的问题。一个35岁的程序员,可能要价是年轻程序员的好几倍。但对于公司来说,如果不是核心的岗位,年轻人的性价比更高。


年轻人没有家庭负担,能加班、能熬夜。对于新技术的敏感度高,学习速度也很快。这些都是优势。


3.自身成长跟不上


很多程序员工作了很多年,但只是在重复劳动,没有形成自己的核心竞争力。你可能有多年的经验,但这些经验并没有转化为更高的价值。


还有就是,35岁左右的年纪,正是家庭负担最重的时候。父母、孩子、房子和车子,哪一样都需要操心。


这些都会分散你的精力,让你没办法想年轻时那样能够全身心的投入工作。


失业后该怎么办?


如果你在35岁时真的失业了,不要慌,请先冷静下来,好好规划一下。


1.调整心态


失业的第一件事,不是急着找下一份工作,而是先冷静下来,调整好自己的心态。


失业并不代表你不行,可能只是不适合那个环境。就当时给自己放个假,陪陪家人,思考一下自己到底想要什么。


不要做这几件事:
不要盲目的投简历,见公司就投;
不要自暴自弃,怀疑自己的人生;
不要急着转行,特别是完全不了解的行业。


2.财务评估


冷静下来后,要做一次彻底的财务评估。算算你的存款能支持多久,每个月的固定支出是多少。


这可以帮你明确你有多少事件可以找工作或者转型。如果经济压力大,也可以先找一份工作过渡一下。


3.找准自己的定位


这是最关键的一步。你要清楚的知道自己的优势和劣势,找准自己的定位。


可以问问自己这些问题:
我最擅长什么技术?
我有什么样的项目经验?
我的软技能如何(沟通、管理、架构设计等)?
我到底喜欢做什么?


根据这些答案,你可以确定自己的方向,是继续做技术,还是转向管理,更或者是创业?


有哪些出路可以选择?


对于35岁的程序员来说,其实出路有很多,不只是送外卖,开滴滴。


1.深耕技术


很多程序员觉得35岁还在写代码真的很失败,我认为这是一个错误的观念。在国外,50-60岁还在写代码的大有人在。


怎么深耕?


成为领域专家:不要什么都懂一点,但什么都不精。选择一个方向深入下去,比如前端、后端、大数据、AI等,成为某个领域的专家。


学习新技术:关注行业的趋势,学习新技术。比如现在还在只学jsp或者jQuery,那肯定是不行的。


参与开源项目:参与开源项目不仅能提升你的技术能力,还能扩大你的影响力。


2.技术管理


如果你对技术还有兴趣,但又不想天天写代码,技术管理是一个不错的选择。


需要具备的能力:
技术能力:你不需要是团队里最牛的,但必须又扎实的技术基础,能够做技术决策。


沟通协调能力:能够与产品、测试和运营等不同的角色进行有效沟通。


团队管理能力:懂得怎么记激励团队成员,怎么分配认为,怎么样解决同事之间的冲突。


3.架构师


架构师是很多程序员的职业目标,需要深厚的技术功底。


架构师的核心能力:
技术广度:需要对各种技术有所了解,知道在什么场景下该用什么技术。


系统设计能力:能够设计扩展性好、性能高的系统架构。


业务理解能力:能够快速的为业务需求制定技术方案。


4.自由职业或创业


如果不想再为别人打工,可以考虑创业或者做自由职业。


创业方向
技术服务:为中小企业提供技术服务,比如开发网站、小程序和APP开发等。
软件开发:开发自己的产品,比如SaaS服务、移动应用等。


自由职业
接外包项目:通过各大外包平台接项目(不稳定,我个人认为不如上班)。
技术写作:写技术文章,做技术分享。


5.转行相关领域


如果你真的不想做技术了,也可以转行到相关领域。


可能的领域:
产品经理:程序员转产品经理有天然的优势,因为你懂技术,知道什么能做,什么不能做。
技术投资:如果你对商业有敏感度,可以考虑转向技术投资领域。


怎么样预防失业的风险?


在失业之前,我们就应该做好准备。


1.持续学习


技术行业,不学习就会被淘汰。这不是危言耸听,而是现实。


定期学习新技术:不要等到用的时候才学,平时可以多逛下稀土掘金、csdn等网站关注一些新技术。
深入理解基础:新技术层出不穷,但基础是不变的。深入理解计算机基础、网络、算法等,这些知识永远不会过时。
跨界学习:不要只局限在自己熟悉的技术,多了解一下其他相关领域。


2.建立个人品牌


在当今社会,个人品牌越来越重要。有了个人品牌,你就有了影响力,就有了更多的机会。


如何建立个人品牌?


写技术博客:分享你的技术经验和见解。
参与开源项目:在GitHub上贡献代码。
在技术社区活跃:回答问题,分享经验。
参加技术大会:尽可能在技术大会上做一些交流和分享。


3.发展副业


不要把所有鸡蛋放在一个篮子里。在发展主业的同时,可以考虑发展副业。


副业方向:
接外包项目:利用业余时间接一些项目。
写技术文章:向技术媒体投稿,获取稿费。
开发自己的产品:比如浏览器插件、小程序、APP等。


结语


35岁程序员失业,确实是一个非常大的挑战。但危机中或许也会藏着新的机遇。
重要的是,捕蝇自暴自弃,认证找准自己的方向,然后坚定的走下去。
任何行业的路都不好走,只要你能保持学习的热情,不断的提升自己,年龄就不是问题。


35岁,不是终点,而是新的起点。


如果你觉得这篇文章对你有帮助,欢迎点赞、分享。你的支持是我继续创作的最大动力!


作者:刘大华
链接:juejin.cn/post/7563910479007498240
收起阅读 »

同志们,我去外包了

同志们,我去外包了同志们,经历了漫长的思想斗争,我决定回老家发展,然后就是简历石沉大海,还好外包拯救了我,我去外包了!都是自己人,说这些伤心话干嘛;下面说下最近面试的总结地方,小小弱鸡,图一乐吧。首先随着工作年限的增加,越来越多公司并不会去和你抠八股文了(那阵...
继续阅读 »

同志们,我去外包了

同志们,经历了漫长的思想斗争,我决定回老家发展,然后就是简历石沉大海,还好外包拯救了我,我去外包了!

Xbw8OtYtcYAVZ0dCwFJzXwc8bad653b209f07472ec09fd8e712492.jpg

都是自己人,说这些伤心话干嘛;下面说下最近面试的总结地方,小小弱鸡,图一乐吧。

首先随着工作年限的增加,越来越多公司并不会去和你抠八股文了(那阵八股风好像停了),只是象征性的问几个问题,然后会对照着项目去问些实际的问题以及你的处理办法。 (ps:(坐标合肥)突然想到某鑫面试官问我你知道亿级流量吗?你怎么处理的,听到这个问题我就想呼过去,也许读书读傻了,他根本不知道亿级流量是个什么概念,最主要的是它是个制造业公司啊,你哪来的亿级流量啊,也不知道问这个问题时他在想啥,还有某德(不是高德),一场能面一个小时,人裂开)。

好了,言归正传,咱说点入职这家公司我了解到的一点东西,我分为两部分:代码和sql;

代码上

首先传统的web项目也会分前端后端,这点不错;

1.获取昨天日期

可以使用jdk自带的LocalDate.now().minusDays(-1) 这个其实内部调用的是plusDays(1)方法,所以不如直接就用plusDays方法,这样少一层判断;

PS:有多少人和我之前一样直接new Date()的。

2.字符填充

apache.common下的StringUtils的rightPad方法用于字符串填充使用方法是StringUtils.rightPad(str,len,fillStr) 大概意思就是str长度如果小于len,就用fillStr填充;

PS:有多少人之前是String.format或者StringBuilder用循环实现的。

3.获取指定年指定月的某天

获取指定年指定月的某天可以用localDate.of(year,month,day),如果我们想取2025年的五月一号,可以写成LocalDate.of(2025, 5, 1),那有人可能就想到了如果月尾呢,LocalDate.of(2025, 5, 31)也是可以的,但是我们需要清楚知道这个月有多少天,比如说你2月给个30天,那就会抛异常; 麻烦;

12.jpg 更好的办法就是先获取第一天,然后调用localDate.with(TemporalAdjusters.lastDayOfMonth());方法获取最后一天,TemporalAdjusters.lastDayOfMonth()会自动处理不同月份和闰年的情况;

sql层面的

有言在先,说实话我不建议在sql层面写这种复杂的东西,毕竟我们这么弱的人看到那么长的且复杂的sql会很无力,那种无力感你懂吗?打工人不为难打工人;不过既然别人写了,咱们就学习一下嘛;

1.获取系统日期

首先获取系统日期可以试用TRUNC(SYSDATE)进行截取,这样返回的时分秒是00:00:00,比如2025-05-29 00:00:00,它也可以截取数字,想知道就去自行科普下,不建议掌握,学习了下,有点搞;

2.返回date当前月份的最后一天

LAST_DAY(date)这个返回的是date当前月份的最后一天,比如今天是2025-05-29,那么返回的是2025-05-31 ADD_MONTH(date,11)表示当前日期加上11个月,比如2025-01-02,最终返回的是2025-12-02;

3.左连接的知识点

最后再提个左连接的知识点,最近看懵了,图一乐哈,A left join B,就是on的条件是在join生成临时表时起作用的,而where是对生成的临时表进行过滤; 两者过滤的时机不一样。我想了很久我觉得可以这么理解,on它虽然可以添加条件,但他的条件只是一个匹配条件比如B.age>10;它是不会对A表查询出来的数据量产生一个过滤效果; 而where是一个实打实的过滤条件,不管怎么说都会影响最终结果,对于inner join这个特例,on和where的最终效果一样,因为B.age>10会导致B的匹配数据减少,由于是交集,故会对整体数据产生影响。

好了,晚安,外包打工仔。。。


作者:小红帽的大灰狼
来源:juejin.cn/post/7510055871465308212
收起阅读 »

Kafka 消息积压了,同事跑路了

快到年底了,系统频繁出问题。我有正当理由怀疑老板不想发年终奖所以搞事。这不,几年都遇不到的消息队列积压现象今晚又卷土重来了。今晚注定是个不眠夜了,原神启动。。。组里的小伙伴火急火燎找到我说,Kafka 的消息积压一直在涨,预览图一直出不来。我加了几个服务实例,...
继续阅读 »

快到年底了,系统频繁出问题。我有正当理由怀疑老板不想发年终奖所以搞事。

这不,几年都遇不到的消息队列积压现象今晚又卷土重来了。

今晚注定是个不眠夜了,原神启动。。。

image.png

组里的小伙伴火急火燎找到我说,Kafka 的消息积压一直在涨,预览图一直出不来。我加了几个服务实例,刚开始可以消费,后面消费着也卡住了。

本来刚刚下班的我就比较疲惫,想让他撤回镜像明天再上。不成想组长不讲武德,直接开了个飞书视频。

我当时本来不想理他,我已经下班了,别人上线的功能出问题关我啥事。

image.png

后来他趁我不注意搞偷袭,给我私信了,我当时没多想就点开了飞书。

本来以传统功夫的点到为止,我进入飞书不点开他的会话,是能看他给我发的最后一句话的。

image.png

我把手放在他那个会话上就是没点开,已读不回这种事做多了不好。我笑了一下,准备洗洗睡了。

正在我收手不点的时候,他突然给我来了一个电话,我大意了啊,没有挂,还强行接了他的电话。两分多钟以后就好了,我说小伙子你不讲武德。

直接喊话,今晚必须解决,大家都点咖啡算他的。

image.png 这真没办法,都找上门来了。只能跟着查一下,早点解决早点睡觉。然后我就上 Kafka 面板一看:最初的4个分区已经积压了 1200 条,后面新加的分区也开始积压了,而且积压的速度越来越快。

image.png

搞清楚发生了什么?我们就得思考一下导致积压的原因。一般是消费者代码执行出错了,导致某条消息消费不了。

所以某个点卡住了,然后又有新的消息进来。

Kafka 是通过 offset 机制来标记消息是否消费过的,所以如果分区中有某一条消息消费失败,就会导致后面的没机会消费。

我用的是spring cloud stream 来处理消息队列的发送和监听。代码上是每次处理一条消息,而且代码还在处理的过程中加了 try-catch。监听器链路打印的日志显示执行成功了,try-catch也没有捕捉到任何的异常。

这一看,我就以为是消费者性能不足,突然想起 SpringCloudStream 好像有个多线程消费的机制。立马让开发老哥试试,看看能不能就这样解决了,我困得不行。

image.png

我半眯着眼睛被提醒吵醒了。开发老哥把多线程改成10之后,发现积压更快了,而且还有pod会挂。老哥查了一下多线程的配置 concurrency。

原来指的是消费者线程,一个消费者线程会负责处理一个分区。对于我们来说,增加之后可能会导致严重的流量倾斜,难怪pod会挂掉,赶紧恢复了回去。

看来想糊弄过去是不行了,我把pod运行的日志全部拉下来。查了一下日志,日志显示执行成功了,但同时有超时错误,这就见了鬼了。

image.png

作为一个坚定的唯物主义者,我是不信见鬼的。但此刻我汗毛倒竖,吓得不敢再看屏幕一眼。

image.png

但是内心又觉得不甘心,于是我偷偷瞄了一眼屏幕。不看还好,一喵你猜我发现了啥?

消费者组重平衡了,这就像是在黑暗中有一束光照向了我,猪八戒发现嫦娥原来一直暗恋自己,我的女神其实不需要拉屎。

有了重平衡就好说了,无非就是两种情况,一是服务挂掉了,二是服务消费者执行超时了。现在看来服务没有挂,那就是超时了,也正好和上面的日志能对上。

image.png

那怎么又看到监听器执行的结果是正常的呢?

这就得从 Kafka 的批量拉取机制说起了,这货和我们人类直觉上的的队列机制不太一样。我们一般理解的队列是发送一个消息给队列,然后队列就异步把这消息给消费者。但是这货是消费者主动去拉取一批来消费。

然后好死不死,SpringCloudStream 为了封装得好用符合人类的认知,就做成了一般理解的队列那种方式。

SpringCloudStream 一批拉了500条记录,然后提供了一个监听器接口让我们实现。入参是一个对象,也就是500条数据中的一条,而不是一个数组。

image.png

我们假设这500条数据的ID是 001-500,每一条数据对应的消费者需要执行10s。那么总共就需要500 x 10s=5000s。

再假设消费者执行的超时时间是 300s,而且消费者执行的过程是串行的。那么500条中最多只能执行30条,这就能解释为什么看消费链路是正常的,但是还超时。

因为单次消费确实成功了,但是批次消费也确实超时了。

我咧个豆,破案了。

image.png

于是我就想到了两种方式来处理这个问题:第一是改成单条消息消费完立马确认,第二是把批次拉取的数据量改小一点。

第一种方案挺好的,就是性能肯定没有批量那么好,不然你以为 Kafka 的吞吐量能薄纱ActiveMQ这些传统队列。吞吐量都是小事,这个方案胜在可以立马去睡觉了。只需要改一个配置:ack-mode: RECORD

第二种方案是后来提的,其实单单把批次拉取的数据量改小性能提升还不是很明显。不过既然我们都能拿到一批数据了,那多线程安排上就得了。

先改配置,一次只拉取50条 max.poll.records: 50。然后启用线程池处理,完美!

@StreamListener("")
public void consume(List<byte[]> payloads) {

List> futures = payloads.stream().map(bytes -> {
Payload payload = JacksonSnakeCaseUtils.parseJson(new String(bytes), Payload.class);

return CompletableFuture.runAsync(() -> {
// ........
}, batchConsumeExecutor).exceptionally(e -> {
log.error("Thread error {}", bytes, e);
return null;
});
}).collect(Collectors.toList());

try {
// 等待这批消息中的所有任务全部完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
errorMessage = "OK";
} catch (Exception e) {
errorMessage = "Ex: " + e.getMessage();
} finally {
// ...
}
}


作者:纸仓
来源:juejin.cn/post/7573687816431190026
收起阅读 »

10 个被严重低估的 JS 特性,直接少写 500 行代码

web
前言最近逛 Reddit 的时候,看到一个关于最被低估的 JavaScript 特性的讨论,我对此进行了总结,和大家分享一下。1. Set:数组去重 + 快速查找,比 filter 快 3 倍提到数组去重,很多人第一反应是 filter +...
继续阅读 »

前言

最近逛 Reddit 的时候,看到一个关于最被低估的 JavaScript 特性的讨论,我对此进行了总结,和大家分享一下。

1. Set:数组去重 + 快速查找,比 filter 快 3 倍

提到数组去重,很多人第一反应是 filter + indexOf,但这种写法的时间复杂度是 O (n²),而 Set 天生支持 “唯一值”,查找速度是 O (1),还能直接转数组。

举个例子:

用户 ID 去重:

// 后端返回的重复用户 ID 列表
const duplicateIds = [101, 102, 102, 103, 103, 103];
// 1 行去重
const uniqueIds = [...new Set(duplicateIds)];
console.log(uniqueIds); // [101,102,103]

避免重复绑定事件:

const listenedEvents = new Set();
// 封装事件绑定函数,防止同一事件重复绑定
function safeAddEvent(eventName, handler) {
if (!listenedEvents.has(eventName)) {
window.addEventListener(eventName, handler);
listenedEvents.add(eventName); // 标记已绑定
}
}
// 调用 2 次也只会绑定 1 次 scroll 事件
safeAddEvent("scroll", () => console.log("滚动了"));
safeAddEvent("scroll", () => console.log("滚动了"));

2. Object.entries () + Object.fromEntries ():对象数组互转神器

以前想遍历对象,要用 for...in 循环,外加判断 hasOwnProperty;如果想把数组转成对象,只能手动写循环。这对组合直接一键搞定。

举个例子:

筛选对象属性,过滤掉空值:

// 后端返回的用户信息,包含空值字段
const userInfo = {
name: "张三",
age: 28,
avatar: "", // 空值,需要过滤
phone: "13800138000",
};
// 1. 转成[key,value]数组,过滤空值;2. 转回对象
const filteredUser = Object.fromEntries(Object.entries(userInfo).filter(([key, value]) => value !== ""));
console.log(filteredUser);
// {name: "张三", age:28, phone: "13800138000"}

URL 参数转对象(不用再写正则了)

// 地址栏的参数:?name=张三&age=28&gender=男
const searchStr = window.location.search.slice(1);

// 直接转成对象,支持中文和特殊字符
const paramObj = Object.fromEntries(new URLSearchParams(searchStr));

console.log(paramObj); // {name: "张三", age: "28", gender: "男"}

3. ?? 与 ??=:比 || 靠谱

 || 设置默认值时,会把 0""false这些 “有效假值” 当成空值。比如用户输入 0(表示数量),count || 10会返回 10,但这里其实应该返回 0。而??只判断 null/undefined

举个例子:

处理用户输入的 “有效假值”:

// 用户输入的数量( 0 是有效数值,不能替换)
const userInputCount = 0;

// 错误写法:会把 0 当成空值,返回 10
const wrongCount = userInputCount || 10;

// 正确写法:只判断 null/undefined,返回 0
const correctCount = userInputCount ?? 10;

console.log(wrongCount, correctCount); // 10, 0

给对象补默认值(不会覆盖已有值):

// 前端传入的配置,可能缺少 retries 字段
const requestConfig = { timeout: 5000 };

// 只有当 retries null/undefined 时,才赋值 3(不覆盖已有值)
requestConfig.retries ??= 3;
console.log(requestConfig); // {timeout:5000, retries:3}

// 如果已有值,不会被覆盖
const oldConfig = { timeout: 3000, retries: 2 };
oldConfig.retries ??= 3;
console.log(oldConfig); // {timeout:3000, retries:2}

4. Intl API:原生国际化 API

很多人会用 moment.js 处理日期、货币格式化,但这个库体积特别大(压缩后也有几十 KB);而 Intl 是浏览器原生 API,支持货币、日期、数字的本地化,体积为 0,还能自动适配地区。

举个例子:

多语言货币格式化(适配中英文):

const price = 1234.56;

// 人民币格式(自动加 ¥ 和千分位)
const cnyPrice = new Intl.NumberFormat("zh-CN", {
style: "currency",
currency: "CNY",
}).format(price);

// 美元格式(自动加 $ 和千分位)
const usdPrice = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(price);

console.log(cnyPrice, usdPrice); // ¥1,234.56 $1,234.56

日期本地化(不用手动拼接年月日):

const now = new Date();

// 中文日期:2025年11月3日 15:40:22
const cnDate = new Intl.DateTimeFormat("zh-CN", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}).format(now);

// 英文日期:November 3, 2025, 03:40:22 PM
const enDate = new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}).format(now);
console.log(cnDate, enDate);

5. Intersection Observer:图片懒加载 + 滚动加载,不卡主线程

传统我们用 scroll事件 + getBoundingClientRect()判断元素是否在视口,会频繁触发重排,导致页面卡顿;Intersection ObserverAPI 是异步监听,不阻塞主线程,性能直接提升一大截。

举个例子:

图片懒加载(可用于优化首屏加载速度):


<img data-src="https://xxx.com/real-img.jpg" src="placeholder.jpg" class="lazy-img" />
// 初始化观察者
const lazyObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
// 当图片进入视口
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // 加载真实图片
lazyObserver.unobserve(img); // 加载后停止监听
}
});
});
// 给所有懒加载图片添加监听
document.querySelectorAll(".lazy-img").forEach((img) => {
lazyObserver.observe(img);
});

列表滚动加载更多(避免一次性加载过多数据):

<ul id="news-list">ul>

<div id="load-more">加载中...div>
const loadObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
// 当加载提示进入视口,请求下一页数据
fetchNextPageData().then((data) => {
renderNews(data); // 渲染新列表项
});
}
});
// 监听加载提示元素
loadObserver.observe(document.getElementById("load-more"));

6. Promise.allSettled ():批量请求不 “挂掉”,比 Promise.all 更实用

如果使用 Promise.all,当批量请求时,只要有一个请求失败,Promise.all 就会直接 reject,其他成功的请求结果就拿不到了;而 allSettled 会等待所有请求完成,不管成功失败,还能分别处理结果。

举个例子:

批量获取用户信息 + 订单 + 消息(部分接口失败不影响整体):

// 3个并行请求,可能有失败的
const requestList = [
fetch("/api/user/101"), // 成功
fetch("/api/orders/101"), // 失败(比如订单不存在)
fetch("/api/messages/101"), // 成功
];

// 等待所有请求完成,处理成功和失败的结果
Promise.allSettled(requestList).then((results) => {
// 处理成功的请求
const successData = results.filter((res) => res.status === "fulfilled").map((res) => res.value.json());
// 记录失败的请求(方便排查问题)
const failedRequests = results.filter((res) => res.status === "rejected").map((res) => res.reason.url);
console.log("成功数据:", successData);
console.log("失败接口:", failedRequests); // ["/api/orders/101"]
});

7. element.closest ():向上找父元素最安全的方式

传统如果想找某个元素的父元素,比如点击列表项找列表,需要使用 element.parentNode.parentNode,但一旦 DOM 结构变了,代码就崩了;closest() 回直接根据 CSS 选择器找最近的祖先元素,不管嵌套多少层。

举个例子:

点击列表项,给列表容器加高亮:

<ul class="user-list">
<li class="user-item">张三li>
<li class="user-item">李四li>
ul>
document.querySelectorAll(".user-item").forEach((item) => {
item.addEventListener("click", (e) => {
// 找到最近的.user-list(不管中间嵌套多少层)
const list = e.target.closest(".user-list");
list.classList.toggle("active"); // 切换高亮
});
});

输入框聚焦,给表单组加样式:

<div class="form-group">
<label>用户名label>
<input type="text" id="username" />
div>
const usernameInput = document.getElementById("username");
usernameInput.addEventListener("focus", (e) => {
// 找到最近的.form-group,加focused样式
const formGr0up = e.target.closest(".form-group");
formGr0up.classList.add("focused");
});

8. URL + URLSearchParams:处理 URL 方便多了

传统解析 URL 参数、修改参数,还要写复杂的正则表达式,有时还得处理中文编码问题;当然我们会直接引入三方库来处理,但毕竟还要引入多余的苦,其实 URL API 可以直接解析 URL 结构,URLSearchParams 可用于处理参数,支持增删改查,自动编码,方便多了。

解析 URL 参数(支持中文和特殊字符):

// 当前页面URL:https://xxx.com/user?name=张三&age=28&gender=男
const currentUrl = new URL(window.location.href);

// 获取参数
console.log(currentUrl.searchParams.get("name")); // 张三
console.log(currentUrl.hostname); // xxx.com(域名)
console.log(currentUrl.pathname); // /user(路径)

修改 URL 参数,跳转新页面:

const url = new URL("https://xxx.com/list");

// 添加参数
url.searchParams.append("page", 2);
url.searchParams.append("size", 10);

// 修改参数
url.searchParams.set("page", 3);

// 删除参数
url.searchParams.delete("size");
console.log(url.href); // https://xxx.com/list?page=3
window.location.href = url.href; // 跳转到第3页

9. for...of 循环:比 forEach 灵活,还支持 break 和 continue

我们都知道,forEach 不能用 break中断循环,也不能用 continue跳过当前项。而for...of不仅支持中断,还能遍历数组、Set、Map、字符串,甚至获取索引。

举个例子:

遍历数组,找到目标值后中断:

const productList = [
{ id: 1, name: "手机", price: 5999 },
{ id: 2, name: "电脑", price: 9999 },
{ id: 3, name: "平板", price: 3999 },
];

// 找价格大于8000的产品,找到后中断
for (const product of productList) {
if (product.price > 8000) {
console.log("找到高价产品:", product); // {id:2, name:"电脑", ...}
break; // 中断循环,不用遍历剩下的
}
}

遍历 Set,获取索引:

const uniqueTags = new Set(["前端", "JS", "CSS"]);

// 用 entries() 获取索引和值
for (const [index, tag] of [...uniqueTags].entries()) {
console.log(`索引${index}${tag}`); // 索引 0:前端,索引 1:JS...
}

10. 顶层 await:模块异步初始化

以前在 ES 模块里想异步加载配置,必须写个 async 函数再调用;现在 top-level await 允许你在模块顶层直接用 await,其他模块导入时会自动等待,不用再手动处理异步。

举个例子:

模块初始化时加载配置:

// config.js
// 顶层直接 await,加载后端配置

const response = await fetch("/api/config");
export const appConfig = await response.json(); // {baseUrl: "https://xxx.com", timeout: 5000}
// api.js(导入 config.js,自动等待配置加载完成)
import { appConfig } from "./config.js";

// 直接用配置,不用关心异步
export const apiClient = {
baseUrl: appConfig.baseUrl,
get(url) {
return fetch(`${this.baseUrl}${url}`, { timeout: appConfig.timeout });
},
};

点击按钮动态加载组件(按需加载,减少首屏体积):

// 点击“图表”按钮,才加载图表组件
document.getElementById("show-chart-btn").addEventListener("click", async () => {
// 动态导入图表模块,await 等待加载完成
const { renderChart } = await import("./chart-module.js");
renderChart("#chart-container"); // 渲染图表
});

结语

可以看到,以前我们依赖的第三方库,其实原生 API 早就能解决,比如用 Intl 替代 moment.js,用 Set 替代 lodash 的 uniq,用 Intersection Observer 替代懒加载,随着老旧的浏览器被讨论,兼容性越来越好,这些 API 以后会成为基操。


作者:冴羽
来源:juejin.cn/post/7568153532014559267

收起阅读 »

大家觉得,在前端开发中,最难的技术是哪一个?

web
“你不能把点点滴滴的事情在未来连接起来,你只能在回顾时看到它们的联系。所以你必须相信,未来的某一刻,你做的所有事情都会有意义。” ——乔布斯Hello,大家好,我是 三千。大家觉得,在前端开发中,最难的技术是哪一个?   &nbs...
继续阅读 »

“你不能把点点滴滴的事情在未来连接起来,你只能在回顾时看到它们的联系。所以你必须相信,未来的某一刻,你做的所有事情都会有意义。” ——乔布斯

Hello,大家好,我是 三千。

大家觉得,在前端开发中,最难的技术是哪一个?

    如果你之前完全没有接触过3D 可视化应用开发,那使用Three.js开发应用还是门槛挺高的,比如,加载一个模型,调光,选择模型弹框的功能,就能干出Three.js上百行的代码。同时还有很多复杂的3D概念需要理解。

前言

image.png

    今天给大家分享一个3D 开发框架:TresJS 。它是一个基于 Vue.js 的声明式 Three.js 框架,将 Vue 的开发便利性与 Three.js 的强大功能完美结合,提供了模板语法和组件化的开发方式,与 Vue 生态无缝结合,无需额外学习复杂的 Three.js API,大大简化了复杂 3D 场景的构建。高扩展性,与 Three.js 的资源和技术完美兼容,并且在内部进行了大量优化,确保在构建复杂 3D 场景时,性能表现依然出色,无论是数据可视化、虚拟现实。还是3D动画效果,TresJS 都能轻松应对。

    下面我们通过一个例子,来看看它是怎么使用的。

image.png

1、安装

通过npm的方式,我们可以安装 TresJS:

pnpm add three @tresjs/core

  • Typescript

TresJS 是用 Typescript 编写的,是完全类型化的。如果您使用的是 Typescript,您就能充分享受类型的好处。 只需要保证你安装了 three 的类型定义。

npm install @types/three -D

2、设置体验画布

在我们创建场景前,我们需要一个什么来展示它。使用原始的 ThreeJS 我们会需要创建一个 canvas HTML 元素来挂载 WebglRenderer 并初始化一个场景。

通过 TresJS 你仅仅需要导入默认组件  并把它添加到你的 Vue 组件的模板部分即可。

<script lang="ts" setup>
import { TresCanvas } from '@tresjs/core'
script>

<template>
<TresCanvas window-size>

TresCanvas>
template>

这个 TresCanvas 组件会在场景幕后做一些设置的工作:

  • 它创建一个 WebGLRenderer 用于自动更新每一帧。
  • 它根据浏览器刷新率设置要在每一帧上调用的渲染循环。

3、画布尺寸

默认的情况下,TresCanvas 组件会跟随父元素的宽高,如果出现空白页,请确保父元素的大小合适。

<script lang="ts" setup>
import { TresCanvas } from '@tresjs/core'
script>

<template>
<TresCanvas>

TresCanvas>
template>

<style>
html,
body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
#app {
height: 100%;
width: 100%;
}
style>

如果您的场景不是用户界面的一部分,您也可以通过像这样的使用 window-size prop 来强制画布使用整个窗口的宽度和高度:

<script lang="ts" setup>
import { TresCanvas } from '@tresjs/core'
script>

<template>
<TresCanvas window-size>

TresCanvas>
template>

4、创建一个场景

我们只需要 4 个核心元素来创建 3D 体验:

使用 TresJS 时,您只需将  组件添加到 Vue 组件的模板中,它就会自动为您创建Renderercanvas 作为 DOM 元素)和Scene

<template>
<TresCanvas window-size>

TresCanvas>
template>

然后,您可以使用  组件来添加一个 透视相机

<template>
<TresCanvas window-size>
<TresPerspectiveCamera />
TresCanvas>
template>

5、添加一个🍩

那个场景看起来有点空,让我们添加一个基本对象。如果我们使用普通的 ThreeJS,我们需要创建一个 网格 对象,并在其上附加一个 材质 和一个 几何体,如下所示:

const geometry = new THREE.TorusGeometry(1, 0.5, 16, 32)
const material = new THREE.MeshBasicMaterial({ color: 'orange' })
const donut = new THREE.Mesh(geometry, material)
scene.add(donut)

网格是 three.js 中的基本场景对象,用于保存在 3D 空间中表示形状所需的几何体和材质。

现在让我们看看如何使用 TresJS 轻松实现相同的事情。为此,我们将使用  组件,在默认插槽之间,我们将传递一个  和一个

<template>
<TresCanvas window-size>
<TresPerspectiveCamera />
<TresMesh>
<TresTorusGeometry :args="[1, 0.5, 16, 32]" />
<TresMeshBasicMaterial color="orange" />
TresMesh>
TresCanvas>
template>
  • 注意,我们不需要导入任何东西,这是因为 TresJS 会为您使用的 PascalCase 的带有 Tres 前缀的 Three 对象自动生成一个 Vue 组件。例如,如果要使用  组件。
<script setup lang="ts">
import { TresCanvas } from '@tresjs/core'
script>

<template>
<TresCanvas
clear-color="#82DBC5"
window-size
>

<TresPerspectiveCamera
:position="[3, 3, 3]"
:look-at="[0, 0, 0]"
/>

<TresMesh>
<TresTorusGeometry :args="[1, 0.5, 16, 32]" />
<TresMeshBasicMaterial color="orange" />
TresMesh>
<TresAmbientLight :intensity="1" />
TresCanvas>
template>

从这里开始,您可以开始向场景中添加更多对象,并调整组件的属性来查看它们如何影响场景。

image.png

6、思路总结

最后我们用人话总结一下上面的思路:

  • 1、最外层我们定义一个TresCanvas,在里面我们可以添加场景
  • 2、然后定义一个透视相机,用于观察3D场景,position里去定义相机x,y,z轴的位置,look-at里定义相机观察的目标点
  • 3、相机定义完之后,我们开始渲染3d对象,TresTorusGeometry用来定义环面集合体的半径和环向参数。TresMeshBasicMaterial定义几何体的基本材质和颜色。
  • 4、最后用TresAmbientLight设置一下环境光的强度。

结语

以上就是今天与大家分享的全部内容,你的支持是我更新的最大动力,我们下期见!

打工人肝 文章/视频 不易,期待你一键三连的鼓励 !!!

😐 这里是【程序员三千】,💻 一个喜欢捣鼓各种编程工具新鲜玩法的啊婆主。

🏠 已入驻:抖爸爸、b站、小红书(都叫【程序员三千】)

💽 编程/AI领域优质资源推荐 👉 http://www.yuque.com/xiaosanye-o…


作者:程序员三千_
来源:juejin.cn/post/7468330256689463348
收起阅读 »

一天 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

收起阅读 »

不容易,35岁的我还在小公司苟且偷生

前言前几天和前同事闲时聚餐,约了两个月的小聚终于达成了,程序员行业聚少离多,所幸大家的发量还坚挺着。期间不可避免地聊到了自己的公司、行业状况以及对未来的看法,几杯老酒之后,大家畅所欲言,其中一位老哥侃起了他的职业生涯,既坎坷又无奈,饭后想起来挺有代表性的,征得...
继续阅读 »

前言

前几天和前同事闲时聚餐,约了两个月的小聚终于达成了,程序员行业聚少离多,所幸大家的发量还坚挺着。
期间不可避免地聊到了自己的公司、行业状况以及对未来的看法,几杯老酒之后,大家畅所欲言,其中一位老哥侃起了他的职业生涯,既坎坷又无奈,饭后想起来挺有代表性的,征得他同意故记录在此。
以下是老哥的历程。

cold.jpg

程序员的前半生

我今年35岁,有房有贷有妻女有老父母。

出生在90年代的农村,从小中规中矩,不惹事不喧哗不突出,三好学生没有我,德智体美没有全面发展。学习也算努力,不算小题做题家,因为只考了个本科。

大学学费全靠助学带款,勤工俭学补贴日用,埋头苦干成绩也只在年级中等偏下水平。有些同学早早就定下了大学的目标,比如考研、比如出国、比如考公,到了大三的时候大家基本都有了自己的目标。而我的目标就是尽早工作,争取早日还完带款,因此早早就开始准备找工作。
也许是上天眷顾,不知道怎么就被华为看重了(那会华为还没现在的如日中天,彼时是BAT的天下),稀里糊涂的接受了offer,没想到却是改变了后面十年的决定。

2013年,深圳的夏天阳光明媚,热气扑鼻,提着一个简单的箱子进入了坂田基地。
刚开始,工作上的一切都很新鲜,每个人都在忙碌,虽然不知道他们在忙什么,但感觉很高级的样子。同期入职的同事都比较厉害,很快就适应了工作,而自己还是没完全应对工作内容,于是下班之后继续留在公司学习,顺便蹭饭。
就这样,很快就一年过去了,自己也慢慢熟悉了工作节奏,但是加班也越来越多了。对于自己来说,为了过节点,6点是晚饭时间,9点是下班时间,12点正式下班。
平凡的日子没什么值得留恋,过一天、一个月、一年、四年都没什么两样,四年里学习到了不少的知识,也数了很多次深圳凌晨的路灯数。

作为深漂,没有遇到深圳爱情故事,也对高昂的房价绝望,于是决定回到二线城市,成为一名蓉漂。 2017年,还是和四年前一样的行李箱,出现在了老家的省会城市,只是那时的我没有了助学打款,怀里也攒下了一些血汗钱。
那时互联网行业发展还是如火如荼,前端的需求量也很大,也得益于华为公司发展越来越好,自己的华为经历很快就拿到了几个offer,选了一家初创公司,幻想着能有一番成就。

2018年底,眼看着房价越长越高,某链中介不断地灌输再不买明天就是另一个价了,错过这个村就没这个店了,也许是想有个家,也许是想着父母能到省会里一起住,拿出自己做牛马几年的积蓄加上父母一辈子辛苦攒的小十万的养老钱购买了城区里的新房,那会儿的价格已经比前两年涨了一倍多,妥妥的高位站岗,不过想着自己是刚需也不会卖,因此咬咬牙掏出了全部的积蓄怒而背上了三十年的房贷。

房子的事暂时落定了,全身心的投入到工作中,没想到老板只想骗投资人的钱,产品没弄好投资人不愿跟进了,坚持了三年,期间各种断臂求生,最终还是落了个司破人走的境地。

2020年,30岁的我第一次被动失业了,幸运的是也找到了另一半。为了尽可能节省支出,房子装修的事我们都是亲力亲为,最后花了十多万终于将房子装好了,虽然很简单但毕竟是自己在大城市里的第一套房子,那一刻,感觉十年的付出都是值得的。
背着沉重的房贷,期望能找到一份薪资稍微过得去的工作,于是在简历上优势那行写了:“可加班”。依稀记得有些HR对我进行了灵魂拷问:结婚了吗?有小孩了吗?你都30岁了还能加班吗?。我斩钉截铁地说:只要公司有需要,我定会全力以赴!

2022年,我们的孩子出世了,队友辞去了工作全心全意带小孩,而我更加努力了,毕竟有了四脚吞金兽,不得不肝。
虽然工作很努力,但成果一般,不是公司的技术担当,也不会是技术洼地。

2023年的某一天,和之前的364天一样的平淡,在座位上解Bug的我突然感觉到一阵心悸,呼吸不畅,实在不行了呼唤同事叫了120,去医院一套检查下来没发现什么大问题。医生询问是不是工作压力太大,平时加班很多?我说还好,平时也就加班到9点。医生笑了笑说你这种年轻人我见多了,都是压力大的毛病,平时工作不要久坐盯着屏幕多站起来走走。他让我回家多休息,回去后观察了几天还是偶尔会有心悸,再去了另一个医院进行检查,也是没有明确的诊断结果,只是说可能是这个问题,又可能是另一个问题。
过了1个月后,身体上的问题不见好转,我辞去了工作。

2023年末,找了一家小公司,也就是我现在的公司,工资没有涨,仔细算起来还变相下降了。
还是做的业务需求,也没有领导什么人,管好自己就行,直属上级还是个工作几年的小伙。这家公司主要的特点是不加班,技术难度不高,能做多少就是多少,前提是要报风险,领导也不会强迫加班。

就这样到了2024,神奇的是我已经很久没有心悸的感觉了,不知道是不加班还是心态转变的原因。 家里的小朋友也长大了,会说话了。我现在每天下班最温馨的的是她开着门期待我回家的那一刻,她的期盼的眼神就是我回家的动力。

公司在2024年也裁了不少人,领导也找我谈过问问我的想法,我说:我还是能胜任这份工作的。领导说:公司觉得你年级大了一些,工资虽然不是最高,但不太符合行情,你懂的。我说:我懂,可以接受适当的降薪。 就这样,我挺过了2024,然而过了一周领导走了。

2025年,我35周岁了。 现在的我已经彻底接受自己的平庸的事实了。在学生时代,从来都不出色,也不会垫底,就是那类最容易被忽略的人。在工作时代,不是技术大牛,也不是完全的水货,就是普普通通的程序员。

如果说上半生吃到了什么红利,只能说入坑了计算机这行业,技术给我带了收入,有了糊口的基础。没进股市,却被房价狠狠割了一道。

35岁的我,没有彻底躺平摆烂,也没有足够奋发进取。
35岁的我,有着24年的房贷,还好61岁的时候我还在工作,应该还能还房贷。
35岁的我,不吃海鲜不喝酒,尿酸500+。
35岁的我,人体工学椅也挽救不了腰椎间盘突出。
35岁的我,头发依然浓密,只是白发越来越多。
35岁的我,已经不打游戏,只是会看这各种小说聊以慰藉。
35岁的我,两点一线,每天挤着地铁,看众生百态。
35岁的我,早睡早起,放空自己。
35岁的我,暂时还没有领取毕业大礼包,希望今年还能苟过。
35岁的我,希望经济能够好起来,让如我一般平凡的人能够有活下去的勇气。

诸君,下一年再会~祝你平安喜乐,万事顺遂!

太极分两仪,有程序员也有程序媛:30岁的程序媛,升值加薪与我无缘


作者:小鱼人爱编程
来源:juejin.cn/post/7457567782470385705

收起阅读 »

编辑器也有邪修?盘点VS Code邪门/有趣的扩展

VS Code 之所以成为最受欢迎的编辑器之一,很大程度上得益于其丰富的扩展生态。本人精选 20 个实用or有趣的 VS Code 扩展,覆盖摸鱼放松,文件管理、代码规范、效率工具等等多个场景,干货满满,下面正片开始:1 看小说漫画:any-reader核心功...
继续阅读 »

VS Code 之所以成为最受欢迎的编辑器之一,很大程度上得益于其丰富的扩展生态。本人精选 20 个实用or有趣的 VS Code 扩展,覆盖摸鱼放松,文件管理、代码规范、效率工具等等多个场景,干货满满,下面正片开始:

1 看小说漫画:any-reader

  • 核心功能: 在 VS Code 中阅读小说、文档,支持 TXT/EPUB 格式、章节导航、字体调整。
  • 适用场景:利用碎片时间阅读技术文档或轻小说,避免频繁切换应用。
  • 隐藏技巧:支持自定义快捷键翻页,可设置阅读定时提醒。

any-reader

2 偷偷在状态栏看小说:Thief-Book

  • 核心功能:在状态栏显示小说,支持阅读进度。
  • 适用场景:利用碎片时间阅读技术文档或轻小说,避免频繁切换应用。
  • 隐藏技巧:支持快捷键翻页。

Thief-Book

3 看股票基金期货:韭菜盒子

  • 核心功能
    • 基金实时涨跌,实时数据,支持海外基展示
    • 股票实时涨跌,支持 A 股、港股、美股
    • 期货实时涨跌,支持国内期货
    • 底部状态栏信息
    • 开市自动刷新,节假日关闭轮询
    • 支持升序/降序排序、基金持仓金额升序/降序
    • 基金实时走势图和历史走势图
    • 基金排行榜
    • 基金持仓信息
    • 支持维护持仓成本价,自动计算收益率
    • 基金趋势统计图
    • 基金支持分组展示等等...
  • 注意:投资有风险,入市需谨慎! 韭菜盒子

4 小霸王

  • 核心功能:一款基于vscode的nes游戏插件,主打本地与远程游戏资源管理,让你在编辑器内就能完成游戏的添加、下载、启动全流程。
  • 不建议上班玩哈!

小霸王

5. JSON 变可视化树图:JSON Crack

  • 核心功能:将 JSON 数据转换为交互式树状可视化图表,支持折叠/展开节点、搜索内容。
  • 适用场景:分析复杂 JSON 结构(如 API 响应、配置文件)、快速定位数据层级。
  • 优势:比原生 JSON 格式化更直观,支持大体积 JSON 数据渲染。

JSON Crack

6. 改变工作区的颜色来快速识别当前项目:Peacock

  • 核心功能:为不同工作区设置独特的颜色主题(标题栏、活动栏颜色),快速区分多个 VS Code 窗口。
  • 适用场景:同时打开多个项目时,通过颜色直观识别当前操作的项目(如生产环境项目用红色,测试环境用绿色)。
  • 个性化选项:支持按项目自动切换颜色,可自定义颜色饱和度和亮度。

Peacock

7. 编码时长统计:Time Master

  • 核心功能:自动记录代码编写时间、文件修改统计,生成每日/每周编程报告,分析编码效率。
  • 适用场景:跟踪项目开发时间、了解自己的编码习惯、评估任务耗时。
  • 特色:支持集成到 VS Code 状态栏实时显示当前编码时长,数据本地存储保护隐私。

Time Master

8. 生成文件夹树结构:file-tree-generator

  • 核心功能:一键生成项目文件夹结构并复制为文本,支持自定义忽略文件和格式。
  • 适用场景:编写 README 文档、项目说明时快速插入目录结构,或向团队展示项目架构。
  • 使用技巧:右键文件夹选择「Generate File Tree」,可通过配置文件自定义输出格式。

file-tree-generator

9. 轻松切换项目:Project Manager

  • 核心功能:快速保存和切换多个项目,支持按标签分类、搜索项目,无需反复通过文件管理器打开文件夹。
  • 适用场景:同时开发多个项目时,减少切换成本;整理常用项目集合。
  • 特色功能:支持从 Git 仓库、本地文件夹导入项目,可配置项目启动命令。

Project Manager

10. 将文件保存到本地历史记录:Local History

  • 核心功能:自动为文件创建本地历史版本,支持对比不同版本差异、恢复误删内容。
  • 适用场景:防止代码意外丢失、追踪文件修改记录、找回被覆盖的代码。
  • 优势:无需依赖 Git,即使未提交的更改也能保存,历史记录默认保留 90 天。

Local History

11. 生成文件头部注释和函数注释:koroFileHeader

  • 核心功能:自动生成文件头部注释(作者、日期、描述等)和函数注释,支持自定义注释模板。
  • 适用场景:统一团队代码注释规范,快速生成符合 JSDoc、JavaDoc 等标准的注释。
  • 高级用法:通过配置文件定义不同语言的注释模板,支持快捷键触发(默认 Ctrl+Alt+i 生成函数注释)。

koroFileHeader

12. 复制 JSON 粘贴为代码:Paste JSON as Code

  • 核心功能:将 JSON 数据粘贴为指定语言的类型定义或实体类(支持 TypeScript、Go、C#、Python 等 20+ 语言)。
  • 适用场景:根据 API 返回的 JSON 结构快速生成接口类型定义,避免手动编写类型。
  • 使用技巧:复制 JSON 后执行命令「Paste JSON as Code」,选择目标语言和变量名即可生成代码。 Paste JSON as Code

13. 把代码块框起来:Blockman - Highlight Nested Code Blocks

  • 核心功能:用彩色边框高亮嵌套的代码块(如函数、循环、条件语句),直观区分代码层级。
  • 适用场景:阅读复杂代码、调试嵌套逻辑时,快速定位代码块边界。
  • 特色:支持自定义边框样式、透明度和颜色,兼容大多数代码主题。

Blockman - Highlight Nested Code Blocks

14. SVG 预览和编辑:SVG Preview

  • 核心功能:在 VS Code 中实时预览 SVG 图片,支持直接编辑 SVG 代码并即时查看效果。
  • 适用场景:前端开发中处理 SVG 图标、调整 SVG 路径、优化 SVG 代码。
  • 特色:支持放大缩小预览、复制 SVG 代码,兼容大多数 SVG 特性。

SVG Preview

15. 程序员鼓励师:Rainbow Fart

  • 核心功能:在编码过程中根据输入的关键字触发语音赞美,例如输入 function 时播放 “写得真棒!”,输入 if 时播放 “逻辑清晰,太厉害了!”。支持多语言语音包(中文、英文等),赞美内容与代码语境关联。
  • 适用场景:单人开发时缓解编码疲劳,增添趣味性;团队协作时活跃开发氛围;新手学习编程时获得正向反馈。
  • 高级功能:支持自定义语音包(可录制个人或团队专属鼓励语音)、配置触发关键字规则、调整音量和触发频率,兼容多种编程语言。
  • 文档:传送门

Rainbow Fart

16. 命名风格转换:Name Transform

  • 核心功能:一键转换变量名、函数名的命名风格,支持 camelCase(小驼峰)、PascalCase(大驼峰)、snake_case(下划线)、kebab-case(短横线)等常见格式的相互转换。
  • 适用场景:接手不同风格的代码时统一命名规范;调用第三方 API 时适配参数命名格式;重构代码时批量修改命名风格。
  • 高级功能:支持选中区域批量转换、配置默认转换规则、自定义命名风格映射表,可集成到右键菜单或快捷键(默认 Alt+Shift+T)快速触发。

Name Transform

17. 代码拼写检查器:Code Spell Checker

  • 核心功能:实时检查代码中的单词拼写错误(如变量名、注释、字符串中的英文单词),通过下划线标记错误,并提供修正建议。
  • 适用场景:避免因拼写错误导致的 Bug(如变量名拼写错误 usre 而非 user);优化代码注释和文档的可读性;英文非母语开发者提升代码规范性。
  • 高级功能:支持添加自定义词典(忽略项目专属术语或缩写)、配置语言规则(支持英语、法语等多语言)、批量修复重复拼写错误,可集成到 CI/CD 流程中进行拼写检查。

Code Spell Checker

18. 自动修改标签名:Auto Rename Tag

  • 核心功能:在 HTML、XML、JSX、Vue 等标记语言中,修改开始标签时自动同步更新对应的结束标签,无需手动修改配对标签。
  • 适用场景:前端开发中修改 HTML/JSX 标签(如将
    改为 时,结束标签
    自动变为 );编辑 XML 配置文件或 Vue 模板时避免标签不匹配问题。
  • 高级功能:支持自定义匹配的标签语言(默认覆盖 HTML、Vue、React 等)、配置标签同步延迟时间、忽略自闭合标签,兼容嵌套标签结构。

Auto Rename Tag

19. 快速调试打印:Console Helper

  • 核心功能:一键生成格式化的调试打印语句(如 console.log()、console.error()),自动填充变量名和上下文信息,支持快捷键快速插入。
  • 适用场景:前端开发中快速添加调试日志;后端开发调试变量值或函数执行流程;临时排查代码逻辑问题时减少重复输入。
  • 高级功能:支持自定义打印模板(如添加时间戳、文件名、行号)、一键注释 / 取消所有打印语句、自动删除冗余打印,兼容 JavaScript、TypeScript、Python 等多种语言。

Console Helper

20. 彩虹括号:Rainbow Brackets

  • 核心功能:为嵌套的括号(圆括号、方括号、花括号)添加不同颜色,增强代码层次感。
  • 适用场景:编写嵌套较深的代码(如 JSON 结构、条件语句、函数嵌套)时,快速识别括号配对关系。
  • 自定义选项:支持调整颜色主题、忽略特定文件类型、配置括号样式。

Rainbow Brackets


如果看了觉得有帮助的,我是鹏多多,欢迎 点赞 关注 评论;

往期文章

个人主页


作者:鹏多多
来源:juejin.cn/post/7536530451461472265
收起阅读 »

看了下昨日泄露的苹果 App Store 源码……

web
新闻昨日苹果 App Store 前端源码泄露,因其生产环境忘记关闭 Sourcemap,被用户下载了源码,上传到 Github。仓库地址:github.com/rxliuli/app…目前已经 Fork 和 Star 超 5k:如果你想要第一时间知道前端资讯...
继续阅读 »

新闻

昨日苹果 App Store 前端源码泄露,因其生产环境忘记关闭 Sourcemap,被用户下载了源码,上传到 Github。

仓库地址:github.com/rxliuli/app…

目前已经 Fork 和 Star 超 5k:

如果你想要第一时间知道前端资讯,欢迎关注公众号:冴羽

用户如何抓取的源码?

用户 rxliuli 使用 Chrome 插件 Save All Resources 将代码下载了下来。

插件地址为:chromewebstore.google.com/detail/save…

下次你也可以打包下载源码了~

如何看待源码泄露?

其实前端源码泄露对业务本身并没有什么影响,因为前端代码无论是否压缩还是混淆,最终都需传输到浏览器才能运行,本身就具有 “暴露” 属性,SourceMap 只是让代码更易读,更容易调试。

尽管如此,依然不建议在生产环境开启 SourceMap,对普通用户无益,且存在轻微性能开销和源代码暴露的安全风险。

我大致看了下代码,并没有什么密钥之类的信息,所以干点坏事之类的就不用想了。真正有价值的核心代码比如推荐逻辑还是在服务端。

代码使用 Svelte?

我万万没想到,项目使用的是 Svelte。

Svelte 我自然是很熟的,毕竟我翻译过 Svelte 官网:svelte.yayujs.com/

还写了一本掘金小册《Svelte 开发指南》:s.juejin.cn/ds/QNzfZ4eq…

想一想,使用 Svelte 也在情理之中。

因为 Svelte 就非常适合处理这种页面相对简单、业务逻辑并不复杂的页面。

在实现上 ,与其说 Svelte 是框架,不如说 Svelte 是一个编译器。 它会在构建时就会将代码编译为高效的 JavaScript 代码,因此能够实现高性能的 Web 应用。

Svelte 的核心优势在于:

  • 轻量级:核心库只有 3 KB,非常适合开发轻量级项目
  • 高性能:构建时优化,而且不使用虚拟 DOM,减少了内存占用和开销,性能更高
  • 易上手:学习曲线小,入门门槛低,语法简洁易懂

简而言之,Svelte 非常适合构建轻量级 Web 项目,也是本人做个人项目的首选技术栈。

以后大家如果要做相对简单的项目,又有性能上的追求(比如 KPI),那就可以考虑使用 Svelte。

用它作为示例学 Svelte ?

我看了下代码,项目代码还是 Svelte 4,而 Svelte 已经到 5 了,Svelte 4 和 5 不论是底层架构还是基础语法都发生了很大的变化,其变化的剧烈程度类似于 Next.js 12 升 Next.js 13,所以想通过这个项目学习 Svelte 就不用想了,都是些过时的语法了,不如直接学 Svelte 5。


作者:冴羽
来源:juejin.cn/post/7569057572436607014

收起阅读 »

当你的Ant-Design成了你最大的技术债

web
大家好😁如果你是一个前端,尤其是在B端(中后台)领域,Ant Design(antd)这个名字,你不可能没听过。在过去的5年里,我们团队的所有新项目,技术选型里的第一行,永远是antd。它专业、开箱即用、文档齐全,拥有一切你想要的组件, 帮我们这些小团队,一夜...
继续阅读 »

image.png

大家好😁

如果你是一个前端,尤其是在B端(中后台)领域,Ant Design(antd)这个名字,你不可能没听过。

在过去的5年里,我们团队的所有新项目,技术选型里的第一行,永远是antd。它专业、开箱即用、文档齐全,拥有一切你想要的组件, 帮我们这些小团队,一夜之间就拥有了大厂的专业门面。

我们靠它,快速地交付了一个又一个项目。

但是,从去年开始,我发现,这个曾经的经典,正在变成我们团队脖子上最重的枷锁。

Ant Design,这个我们当初用来解决技术债的核心组件库,现在,却成了我们最大的技术债本身😖。

这是一篇团队血泪史, 讲一讲感想🤷‍♂️。


我们为什么会爱上 AntD?

我们必须承认,从无到有阶段,antd是无敌的。

你一个3人的小团队,用上antd,做出来的东西,看起来和阿里几百人团队做的系统,没什么区别。

TableFormModalMenu... 你需要的一切,它都以一种极其标准的方式给你了。你不再需要自己造轮子。

当你发现@ant-design/pro-components时,一个ProTable,直接帮你搞定了请求、分页、查询表单、工具栏... 你甚至都不用写useState了。

在那个阶段,我们以为我们找到了大结局。


当个性化成为 我们的 KPI

美好可能是短暂的,从我们的产品经理和UI设计师开始👇:

能不能...不要长得这么 Ant Design?🤣

image.png

这是我们设计师,在评审会上,小心翼翼提出来的第一句话。

老板也说:我们要做自己的品牌,现在的系统,太千篇一律了!!!

于是,我们接到了第一个简单的需求:把全局的主题色,从橙色改成我们的品牌红。

这很简单,不就是 ConfigProvider嘛🤔。我们改了。

然后,第二个需求来了:这个Modal弹窗的关闭按钮,能不能不要放在右上角?我们要放在左下角,和确认按钮放在一起。(有点反人类🤷‍♂️)

灾难,就从这里开始了。

antdModal组件,根本就没提供这个插槽或prop。我们唯一的办法,是 强改

于是,我们的代码里,开始出现这种恶臭的CSS:

/* 一个高权重的全局CSS文件 */
.ant-modal-header {
/* ... */
}

/* 嘿,那个右上角的关闭按钮,给我藏起来! */
.ant-modal-close-x {
display: none !important;
}

为了把那个 X 藏起来,我们用了!important。我们亲手打开了潘多拉魔盒。

这个表格的筛选图标,能换成我们自己画的吗?😖

antdTable,是一个重灾区。它太强大了,也很黑盒。

我们设计师,重新画了一套筛选、排序的图标。但我们发现,antdTable组件,根本没想过让你换这个。

我们唯一的办法,就是用 CSS选择器,一层一层地穿进antd的DOM结构里,找到那个,然后用background-image去盖掉它。

/* 另一个人写的,更恶臭的CSS */
.ant-table-thead > tr > th.ant-table-column-has-filters .ant-table-filter-trigger {
/* 妈呀,这是啥? */
background: url('our-own-icon.svg') !important;
}

.ant-table-thead > tr > th.ant-table-column-has-filters .ant-table-filter-trigger > svg {
/* 藏起来,藏起来! */
display: none !important;
}

我们被拖累了。

我们花在 覆盖antd默认样式上的时间,已经远远超过了我们自己写一个组件的时间。


压死骆驼的最后一根稻草

image.png

我们用了ProTable,它的查询表单和表格是强耦合的。当产品经理提出一个我希望查询表单,在页面滚动时,吸附在顶部的需求时... 我们发现,我们改不动。我们被ProComponents的黑盒,锁死了。

然后我们的vendor.js打包出来,2.5MB。用webpack-bundle-analyzer一看,antd@ant-design/icons,占了1.2MB。我们为了一个ButtonIcon,引入了一个全家桶。antd的按需加载?别闹了,在ProComponents面前,它几乎是全量的。

而且 antdv3v4,我们花了一个月。从v4v5,我们花了半个月。每一次升级,都是一次大型重构,因为我们那些写法一样被CSS覆盖,在新版里,全失效了🤷‍♂️。

我们本想找一个可靠的组件库,这么久过来,结果它成了债主。


我们真正需要的可能是轮子

我终于想明白了。

Ant Design,它不是一个组件库(Library),它是一个UI框架(Framework)。它是一套解决方案,它有它自己强势的 设计价值观

当你的需求,和它的价值观一致时,它就是圣经。 当你的需求,和它的价值观不一致时,它就变成枷锁。

我们当初要的,其实是一个带样式的Button;而antd给我的,是一个内置了loadingdisabledonClick时会有水波纹动画、并且必须是蓝色或白色的Button


我们的自救之路

在我们新的项目中,我忍痛做出了一个决定🤷‍♂️:

原则上,不再使用antd

我们新的技术栈,转向了: Tailwind CSS + Headless UI 方案(比如Radix UI

image.png

这个组合,才是我们想要的:

  • Headless UI:它只提供功能无障碍。比如,一个Dialog(模态框),它帮我搞定了按Esc关闭、焦点管理。但它没有任何样式
  • Tailwind CSS:我拿到了这个无样式的Dialog,然后用Tailwindclass,在5分钟内,在AI的帮助下,把它拼成了我们设计师想要的、独一无二的弹窗。

我们拿回了CSS的完全控制权,同时又享受了 AI + 组件开发的便利。

我依然尊敬Ant Design,它在前端B端历史上,是个丰碑。 对于那些从0到1的、对UI没有要求的内部系统,我可能依然会用它。

但对于那些需要品牌、体验、个性化的核心产品,我必须和它说再见了。

Suggestion.gif

因为,当你的组件库开始控制你的设计和性能时,它就不是你的资产了。

而变成你最大的技术债🙌。


作者:ErpanOmer
来源:juejin.cn/post/7571176484515659828
收起阅读 »

我是如何将手动的日报完全自动化的☺️☺️☺️

web
书接上回,上回我们聊了处理重复任务的自动化思维。其中,我举了用工具自动化公司日报的例子。今天,我就来详细说说,我到底是怎么做的,以及过程中遇到了哪些问题和挑战。背景我们公司使用某第三方系统有一个自定义的数据看板,每天需要向群里发送日报。之前,这项工作由团队成员...
继续阅读 »

书接上回,上回我们聊了处理重复任务的自动化思维。

其中,我举了用工具自动化公司日报的例子。

今天,我就来详细说说,我到底是怎么做的,以及过程中遇到了哪些问题和挑战。

背景

我们公司使用某第三方系统有一个自定义的数据看板,每天需要向群里发送日报。之前,这项工作由团队成员轮流手动完成:从系统的一个自定义看板复制数据到 Excel,再将表格转为图片,发到群里。

轮到我负责的那一周,我左手边电脑打开系统,右手边打开 Excel,一个个数据复制过去,3.4%、-10%……为避免出错,还要逐一核对。整个过程每天耗时大约 7 到 10 分钟,繁琐又枯燥。

我开始思考:这种重复性工作能不能自动化?

于是,我在群里向大佬们请教,提出了这个问题:

image.png

结果,消息已读,没有一个人回复。

那一刻,我暗下决心:我要自己解决这个问题!

初探

于是乎我打开了改系统,开始研究。

该系统大概长这样, 这是一个自定义看板,后台自定义配置出来的,数据是根据配置的规则算出来的,有十几项,我们是需要从每项取3个数据。加起来复制30-40次。

image.png

  • 手动复制效率低下。
  • 浪费时间。
  • 容易出错,粘错位置了,又得一个个重新对一遍。

所以我第一步是需要把手动复制拿数据的这个过程,利用脚本自动化了。

流程与任务拆解

我们的思路是这样,先脑子里过一下原来的流程,然后一步步自动化原来的流程。

1、原来手动的流程

  1. 手动登录系统
  2. 点击对应面板,一个个复制数据,粘贴到excel里。
  3. 全部复制完,核对完,右键复制为图片
  4. 发送到群里。

2、脚本任务拆解

  1. js逆向登录加密方法,自动化登录,拿到token。
  2. 利用爬虫抓取数据,拿到我需要的。
  3. 利用canvas将数据画成表格,然后转成图片。
  4. 图片传到oss,调用钉钉webhook接口,定时发送到群里

以上我们已经将,手动的流程的任务与自动化需要做的任务一一对应了。

现在我们思路清晰了。

然后我们要做的就是把每个任务逐个攻克即可。

任务分步实现

你不觉得我应该先完成第一个任务——JS 逆向登录加密方法,实现自动化登录并获取 token 吗?

这确实是全自动流程中最核心的一环:没有自动登录获取凭证,后续的数据抓取和操作根本无从谈起。

不过,我初步分析了登录接口,发现参数加密逻辑较复杂,短时间内难以破解。

于是我选择暂时跳过,先手动复制登录凭证,确保后续流程全部打通后再回过头补全自动化登录部分。

1、利用爬虫抓取数据。

首先看板这是个列表,有很多项内容,首先看这个列表怎么来的,服务端渲染还是,调的接口。

然后看能不能完全从页面拿到,我们再考虑抓取方式。

1、如果是服务端渲染的或者数据很快出来的。我们可以考虑抓页面。

2、但是今天这个例子,经过我的研究,我需要的数据,都是异步调接口的,我看还有队列排队逻辑。 说明页面完整展现的时间不稳定,长则几十秒都有可能, 所以我感觉抓页面是不稳定的。

所以我选择抓接口

1.1内容搜索大法

image.png 众多的接口啊,我们怎么找到我要的数据在哪???于是我们利用调试工具,搜索响应内容关键字

例如搜页面中显示的这个标题

image.png

通过内容再network搜索内容 找到了列表接口

image.png

点开看,确实,里边就是这个列表的数据。

但是没有具体是环比、同比,我要的数字。

再次寻找每一项具体数据的获取接口。

再次通过搜索大法找了好久好久.....

找到了通过每项id和过滤条件去获取具体数据的接口

1.2 接口找齐,开始编码

研究下来。整体逻辑是,先获取面板列表,然后循环列表的每一项,拿着有关联的参数去调详情。

面板的数据列表获取
/**
* 获取重点功能监控面板列表及详情数据
* @returns {Promise<*[]>}
*/

async function queryReportList(dashboard) {
const { id: dashboard_id, common_event_filter } = dashboard

const data = await fetch(
`https://xxx/api/v2/sa/dashboards/${dashboard_id}?is_visit_record=true`,
{
credentials: "include",
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0",
Cookie: Cookie
},
referrer:
`https://xxx/dashboard/?dash_type=lego&id=${dashboard_id}&project=1&product=sensors_analysis`,
method: "GET",
mode: "cors"
}
)
.then(res => res.json())

const result = [];
// 获取控面板的前12个监控项的监控数据。
for (const item of data.items.slice(0, 13)) {
if (item.bookmark) {
// 这里解出来, 调下一个接口要用到。
const data = JSON.parse(item.bookmark.data);
const res = await queryReportByTool({
bookmarkid: item.bookmark.id,
measures: data.measures,
dashboard_id: dashboard_id,
common_event_filter: common_event_filter
});
result.push({
...res,
name: item.bookmark.name
});

console.log(
{
name: item.bookmark.name,
base_number: res.base_number /= 100,
day: res.month_on_month /= 100,
week: res.year_on_year /= 100
}
)
}
}

return result
}
获取每一项具体数据
/**
* 报告列表的报告id去获取具体数据
* @param params
* @returns {Promise}
*/

async function queryReportByTool(params) {
const requestId = Date.now() + ":803371";
const body = {
measures: params.measures,
unit: "day",
by_fields: [],
sampling_factor: null,
from_date: dayjs()
.subtract(14, "day")
.format("YYYY-MM-DD"),
// from_date: "2025-02-28",
to_date: getYesterDay(),
// to_date: "2025-03-13",
detail_and_rollup: true,
enable_detail_follow_rollup_by_values_rank: true,
...
};
try {
const data = await fetch(
`https://xxxx/api/events/compare/report/?bookmarkId=${
params.bookmarkid
}&async=true&timeout=10&request_id=${requestId}`
,
{
credentials: "include",
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0",
...
Cookie
},
referrer:
"https://xxxx/dashboard/?dash_type=lego&id=692&project=1&product=sensors_analysis",
body: JSON.stringify(body),
method: "POST",
mode: "cors",
timeout: 10000
}
).then(res => res.json());
if (!data || data.isDone === false) {
return await queryReportByTool(params);
} else {
return data;
}
} catch (e) {
return await queryReportByTool(params);
}
}

1.3 数据拿到

执行一下,数据拿到了,找到了我要的几个字段

PS D:\project2\report> node .\index.js
{
name: 'xxx生成失败率',
base_number: 0.0103,
day: -0.3602,
week: -0.16260000000000002
}
...
{
name: 'xxxx生成失败率',
base_number: 0.017,
day: 0,
week: 0.0241
}
2025-03-18.xlsx文件已保存!
default: 27.917s

1.4 小结

表面看似一帆风顺,因为我是以回忆的视角,实则历经坎坷。目标网站的接口之间关系、参数间的关联,皆需细细揣摩、深入研究。

2、生成图片

node-cavas 生成图片。细节我就不讲了,数据都拿到了,用数据生成一张图片那就看你怎么解决了。

3、图片传到oss,调用钉钉webhook接口,定时发送到群里

传图

我是传到了腾讯云cos

const filePath = `/custom/999/${dashboard.worksheetName}-${dayjs().format('YYYYMMDD')}.jpeg`
const uploadRes = await tencentCos.upload(imageBuffer, filePath, true)

发钉钉群

查看钉钉群机器人api文档,以md格式发送图片链接。

async function sendDingTalkMessage(text) {
// const today = dayjs()
// .format("YYYY-MM-DD")
const token = '1a6e1111111' // 大群机器人
const result = await fetch(`https://oapi.dingtalk.com/robot/send?access_token=${token}`, {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
"msgtype": "markdown",
"markdown": {
"title": "监控日报",
// "text": `#### ${title}-${today} \n > ![screenshot](${imageUrl}) \n`
"text": text
},
"at": {
"isAtAll": true
}
})
}).then(res => res.json())
console.log(result)
if (result.errcode === 0) {
console.log('发送成功')
return true
}
}

到这里可以当做一个脚本每天手动执行一下。 但是还没完全自动化,还差一步

4、js逆向登录加密方法,自动化登录。

就剩下自动登录了。

4.1 为什么要自动化登录?

因为这个系统登录凭证在一定时间内会过期,且不是明文登录的,登录接口参数加密了的。

image.png 看到这你就得去研究他的加密规则了

或者止步于此,手动复制登录凭证,本地执行脚本也是可以。

我如果要在服务器上自动化整个流程,必须得让他自动登录拿到登录凭证。

4.2 逆向步骤

4.2.1找到登录接口

先点页面的登录,找到登录接口,在请求调用栈中随便找个位置先打个断点,然后刷新页面,再次点击登录,嘿,您猜怎么着,断住了!!!

image.png

4.2.2 顺着调用栈找逻辑

顺着调栈给上找逻辑。所有在前端加密的一定是可以模拟的。

找到了调登录的方法。 看了下这就是调store里的方法passport/login。 image.png

从这再往下就比较不容易了,因为你会发现,就有点乱了。进到的都是混淆的一些abcdefg名字的方法。

image.png

但是咱们明确目标就是要找到调用的方法passport/login的位置。

我尝试了如下

  • 搜索passport/login关键字
  • 搜索接口路径

找了一辈子, 终于找到了调接口的地方

看到这个Me方法,传递了一个 isEncrypted 我猜测就是 是否要加密参数的意思吧。 image.png

别搁Me方法外面蹭了行不行???,赶紧进去看看。

你就给我看这个? 这里面又调了另一个

image.png

咱们接着进到xt.request。

您猜怎么着,还没到,这里又进行了一顿操作之后,调了一个名为P的方法。

image.png

好好好,继续继续。

4.2.3 找到了加密的位置

到了P方法,终于是没给我玩套娃了啊。

在这一步终于是看到了关键字 isEncrypted

看了代码确实是判断isEncrypted加密的。

image.png

看了后发现这是一个RSA+AES结合的加密方式

RAS加密密钥, AES加密登录数据。

加密流程总结: ‌

  1. RSA保护AES密钥的安全传输
  2. AES保护实际登录数据的机密性
  3. 双重加密确保登录信息在传输过程中的安全性
为何不用单一的加密方式?

那么你有没有这样的疑问呢?为什么不单独用rsa直接加密数据呢?岂不简单。

当然不行,是有原因的!

RSA长度限制 RSA加密算法对明文长度有严格限制,具体取决于密钥长度和填充方式‌。以下是不同密钥长度下的最大明文长度(以字节为单位):

  • 1024位密钥‌:最大明文长度约为 ‌117字节‌‌
  • 2048位密钥‌:最大明文长度约为 ‌245字节‌‌
  • 4096位密钥‌:最大明文长度约为 ‌512字节‌‌

所以RSA加密超出长度的会报错的。

所以先生成短密钥,再使用RSA加密AES对称算法的密钥,再用对称密钥加密实际数据‌。这是实际应用中的常见做法,兼顾安全性和效率‌

4.2.4 模拟他的加密过程
大致流程
  1. 把加密逻辑copy啊。
  2. 补环境。
  3. 不断尝试直到通过后端校验。
理解后端如何解密和校验

前面我们说到

RAS加密密钥, AES加密登录数据。

那么后端的校验流程就是:

  1. 私钥解出密钥
  2. 密钥配和iv、salt等再解出被AES加密的账号密码信息.

知道了这些,那么我们需要做的就是正确加密和传递相关信息,如果校验失败,我们就要来回对比差异,找到问题,不断尝试。

在不断尝试下我成功了。

遇到的问题
  • 加密的包的版本跟目标网站用的不一样导致校验失败,后经过漫长的查找找到了一样的版本。
  • 还要注意header里带的字段,都要模拟他加密后的带过去。例如这几个。
    • salt
    • iv等

image.png

最终我抽出来的登录加密方法
var b = require("crypto-js");
var jsencrypt = require("nodejs-jsencrypt/bin/jsencrypt").default;

/**
* js逆向回来的方法,模拟xx登录对参数加密
* @param body xx登录参数
* @param public 公钥
* @returns {{headers: {"aes-salt": string, "aes-iv": string, "aes-passphrase": *, "X-Request-Timestamp": string, "X-Request-Id": string, "X-Request-Sign": *}, body: string}}
*/

function encryptLogin(body, public) {
const W = new jsencrypt();
W.setPublicKey(public)

q = b.enc.Utf8.parse(Math.floor(Math.random() * 1e6) + Date.now()).toString();
// q = "31373432353237383135363835";
var re = W.encrypt(q)
, ie = b.lib.WordArray.random(128 / 8)
, fe = b.lib.WordArray.random(128 / 8)
, ue = b.PBKDF2(q, ie, {
keySize: 128 / 32,
iterations: 100
})
, ye = b.AES.encrypt(JSON.stringify(body), ue, {
iv: fe,
mode: b.mode.CBC,
padding: b.pad.Pkcs7
});

const j = "/api/v2/auth/login?is_global=true"
const Ee = parseInt(Date.now() / 1000).toString()
const he = Ee
const Fe = ye.toString()

var bt = "".concat(Ee, "_").concat(he, "_").concat(j, "_").concat(Fe, "_14skjh");

const res = {
headers: {
"aes-salt": ie.toString(),
"aes-iv": fe.toString(),
"aes-passphrase": re,
"X-Request-Timestamp": Ee,
"X-Request-Sign": b.MD5(bt).toString(),
"X-Request-Id": he,
},
body: ye.toString()
}

return res
}
使用登录方法

登录之后存下来cookie供获取数据的接口使用

async function login(public, loginData) {
// 加密登录信息
const encryptOptions = encryptLogin({
...loginData
}, public)

return await fetch("xxx", {
"headers": {
...encryptOptions.headers
},
"method": "POST",
credentials: "include",
body: encryptOptions.body
}).then(res => {
Cookie = res.headers.get('set-cookie')
return res.json()
})
}

任务分步都实现了(自动化了)。

串联起来这四步,整体就实现了。

随后部署到服务器,配置定时任务每天执行。

效果展示

image.png

总结

  • 先通后补:登录逆向卡壳,先手动Cookie跑通全链,再回填自动化。
  • 逆向不怕乱:混淆代码里断点+全局搜索(接口路径/关键字),总能定位加密点。
  • 加密常RSA+AES:RSA只加密短密钥,AES加密长数据,补环境+对齐Header字段是关键。
  • 贵在坚持:第一天研究无果别灰心,第二天重新上手,灵感与进展常不期而至。

虽然文章写得像一帆风顺,但实则磕磕绊绊——在层层混淆的代码里翻找,第一天方法不对,左冲右突脑壳嗡嗡作响。幸好第二天没放弃,沉下心继续深挖,一步步试错、迭代,终于攻克所有难题。

如果有小伙伴有任何问题或者想跟我探讨细节,欢迎联系!

喜欢的话,点点关注。


作者:浏览器API调用工程师_Taylor
来源:juejin.cn/post/7566913899175051299
收起阅读 »

为何前端圈现在不关注源码了?

web
大家好,我是双越。前百度 滴滴 资深前端工程师,慕课网金牌讲师,PMP。我的代表作有:wangEditor 开源 web 富文本编辑器,GitHub 18k star,npm 周下载量 20k划水AI Node 全栈 AIGC 知识库,包括...
继续阅读 »

大家好,我是双越。前百度 滴滴 资深前端工程师,慕课网金牌讲师,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
收起阅读 »

我为什么说全栈正在杀死前端?

web
大家好,我又来了🤣。打开2025年的招聘软件,十个资深前端岗位,有八个在JD(职位描述)里写着:“有Node.js/Serverless/全栈经验者优先”。全栈 👉 成了我们前端工程师内卷的一种方式。仿佛你一个干前端的,要是不懂点BFF、不会配Nginx、不聊...
继续阅读 »

大家好,我又来了🤣。

打开2025年的招聘软件,十个资深前端岗位,有八个在JD(职位描述)里写着:“有Node.js/Serverless/全栈经验者优先”。

50fb0729f6733fc5092ecfc91f063c6.jpg

全栈 👉 成了我们前端工程师内卷的一种方式。仿佛你一个干前端的,要是不懂点BFF、不会配Nginx、不聊聊K8s,你都不好意思跟人说你是资深。

我们都在拼命地,去学Nest.js、学数据库、学运维。我们看起来,变得越来越全能了。

但今天,我想泼一盆冷水🤔:

全栈正在杀死前端。


全栈到底是什么

我们先要搞清楚,现在公司老板们想要的全栈,到底是什么?

image.png

他们想要的,不是一个T型人才(在一个领域是专家,同时懂其他领域)。

他们想要的是:一个能干两个人(前端+后端)的活,但只需要付1.5个人的工资。

但一个人的精力,毕竟是有限的。

  • 当我花了3个月,去死磕K8s的部署和Nest.js的依赖注入时,我必然没有时间,去研究新出炉的INP性能指标该如何优化。
  • 当我花了半周时间,去设计数据库表结构和BFF接口时,我必然没有精力,去打磨那个React组件的可访问性,无障碍(a11y)和动画细节。

我们引以为傲的前端精神,正在被全栈的广度要求,稀释得一干二净。

全栈的趋势,正在逼迫我们,从一个能拿90分的前端专家,变成一个前后端都是及格的功能实现者。


关于前端体验

做全栈的后果,最终由谁来买单?

是用户。

我们来看看全栈前端主导下,最容易出现的受灾现场:

1.能用就行的交互

全栈思维,是功能驱动的。

数据能从数据库里查出来,通过API发到前端,再用v-for渲染出来,好了,这个功能完成了😁。

至于:

  • 列表的虚拟滚动做了吗?
  • 图片的懒加载做了吗?
  • 按钮的loadingdisabled状态,在API请求时加了吗?
  • 页面切换的骨架屏做了吗?
  • 弱网环境下的超时和重试逻辑写了吗?
  • UI测试呢?

抱歉,没时间。我还要去写BFF层的单元测试。

2.无障碍,可访问性(a11y)

你猜一个全栈,在用 

还是 


作者:ErpanOmer
来源:juejin.cn/post/7573172586839834676
收起阅读 »

如何知道同事的工资?

web
薪资保密,是所有互联网公司的红线之一。作为牛马,虽然能理解制度的初衷,但肯定忍不住想知道身边同事的工资。直接问当然不行,我提供几个思路:调虎离山,乘 TA 不在,在 TA 电脑上登录 OA/ERP,直接看工资与 TA 搞对象或者搞基,知道工资后,马上分手,继续...
继续阅读 »

薪资保密,是所有互联网公司的红线之一。作为牛马,虽然能理解制度的初衷,但肯定忍不住想知道身边同事的工资。

直接问当然不行,我提供几个思路:

  1. 调虎离山,乘 TA 不在,在 TA 电脑上登录 OA/ERP,直接看工资
  2. 与 TA 搞对象或者搞基,知道工资后,马上分手,继续下一位,直到遍历完所有同事
  3. 拼命地卷自己,不断升职,成为他们的 +1 或 +2(腾讯是 +2 才有薪酬权)

虽然有可行性,但是作为码农,这些方案都太 low 了,没有丝毫技术含量。

下面,且看老夫的表演:

真正的技术

2016年底,花 100 块买了台摩托罗拉 C118,长这样:

C118正面

百度百科的介绍如下:

Moto C118是Moto品牌于2006年2月推出的一款直板式基础功能手机

配备 96×64 单色显示屏,支持GSM 900/1800MHz双频网络

没错,我在 2016 年底买了一台 2006 年上市的功能机。很无聊的机器,哪怕在 06 年,我都没正眼瞧过它。

但,这跟工资有啥关系?

一般来说,工资入账时,会有银行的短信提醒。如果能截获短信,不就知道其他人的工资了吗?

经过硬件修改后的 C118,刷入特殊的固件,连接到Linux机器,启动脚本扫描频段,用WireShark抓包,就能收到其他人的短信了:

wireshark

大概原理是:

GSM 短信未加密,而基站发送短信是广播的。

也就是说,任意的手机(不需要插入 SIM 卡)都能收到连接到同一基站的其他手机的短信,只不过会丢弃不属于自己的短信

特殊的固件就是让手机来者不拒,不要丢弃其他人的短信。具体细节,网上一大堆,请自行搜索OsmocomBB

我完全不懂硬件,直接买的改好后的 C118,卖家顺便改了 Micro-USB 供电,不需要电池了。我觉得挺漂亮的,就是有点像定时炸弹:

C118背面

机器到手后,照葫芦画瓢,跑了半天,真抓到了一些推广短信。卧槽,牛逼!

等到发工资那天,提前把脚本跑起来,附近同事的工资,尽收眼底。完美!؏؏☝ᖗ乛◡乛ᖘ☝؏؏

好吧,这只是一个邪恶的想法,从未付诸实施。说起来你可能不信,每次都是收到工资入账短信后,我才想起来脚本没跑。久而久之,就忘了这件事了。

话说回来,即使当初真干了,也是徒劳。因为这个只能抓移动和联通的 GSM 短信,对电信的 CDMA 无效。而且,当时 4G 已经全面铺开,根本抓不到。即使有同事用 GSM 老人机,也因为原理的限制,看不到收件人手机号,并不知道是谁的工资。

最后,说一件趣事:

几年前,某司价值观中的「创新」要改成「创造」,在内网征集最能代表「创造」的动物,点赞数最多的是它:

倒挂的蝙蝠

提议者的配文:

蝙蝠,昼伏夜出,象征着创造力

最重要的是,它擅长倒挂

彼时,网上一堆晒 SP、SSP 的 offer 的校招生,薪资遥遥领先工作多年的老员工。当然,蝙蝠最终还是以最高得票遗憾落选了,虽败犹荣。

类似的倒挂,在我身上也发生过,果断选择了跑路。然后,薪资翻了数倍,敏感内容,略。


作者:野生的码农
来源:juejin.cn/post/7550151424333053992
收起阅读 »

学不动了?没事,前端娱乐圈也更新不动了

大家好,我是双越。前百度 滴滴 资深前端工程师,慕课网金牌讲师,PMP。我正开发一个 Node 全栈 AIGC 知识库 划水AI,包括 AI 写作、多人协同编辑。复杂业务,真实上线,大家可以去注册试用,围观项目研发过程。开始从 2024 年春天到现在...
继续阅读 »

大家好,我是双越。前百度 滴滴 资深前端工程师,慕课网金牌讲师,PMP。

我正开发一个 Node 全栈 AIGC 知识库 划水AI,包括 AI 写作、多人协同编辑。复杂业务,真实上线,大家可以去注册试用,围观项目研发过程。

开始

从 2024 年春天到现在 2025 年初夏,好像遍地都是 AI 的各种新闻,前端圈里都有啥动向呢?好像没有啥印象。

我本人是自由职业,每天都会关注行业动态,可能比很多上班族都要看的多。但凭我个人的记忆和印象,我只能记住两件事儿

第一,React19 发布,同时 Nextjs 15 发布。React19 发布新增的功能都是为了满足 RSC 和 Nextjs 而做的,如果你用 React 做纯前端开发,这次更新影响不大。

第二,Vue 作者 尤雨溪 去年秋天创办 VoidZero 计划重构 JS 工具链,并且得到了很多公司的投资。Vue3.6 发布已经集成了他们的 Rolldown 工具,性能提升几倍。

其他的更新没有印象了,也可能是我没关注到,或者太过于基础(如 ES TS Node 等语法和底层能力)而没注意。

AI 总结

人可能不记得,但 AI 可以帮忙,于是我分别咨询了 ChatGPT 和 DeepSeek ,都开启了联网搜索。

 2024 年初到现在 2025 年5月,前端开发领域有哪些比较重要的新闻和技术变化?
例如 react19 nextjs15 remix TS vue vite nodejs AI 等相关的,帮我整理一个时间线。

ChatGPT 的结果如下,主要是 React Nextjs Nodejs TS Vite 等这些更新,没有什么特别需要注意的。

image.png

DeepSeek 的结果如下,大概内容都是这样写,但它提到了 Remix 和 tailwindCSS ,他们都发布了新的版本。

image.png

后来我又想到 Nuxtjs 又查了一下,发现 Nuxtjs v4 发布了,国内也用的少,之前不知道。

最近这两天又爆出 Remix 要脱离 React router ,要基于 PReact 单独开发,要做的更加轻量化。对于我们开发者来说,又一个 React 轮子而已,功能和使用方式都差不多。

解决“学不动”问题

前几年前端技术更新非常快,一些技术、工具、框架,1 年以后就过时了,大家直呼“学不动了...”

但现在已经彻底改变了,所有的技术、工具、框架都已经趋于稳定,即便是再更新也不是断崖式的更新,不影响我们的日常开发使用。

已经慢慢变成了 Java 技术栈的样子,未来几年不会有太大的变化,已经学到的同学不用再花费很多时间学习了,好好工作即可 —— 反正是闲不着~

如果不是当前这个大环境和内卷的形势,这样还真就挺好的,可惜环境如此,不卷这个就卷那个,现在全民卷 AI 了。

AI 相关更新

前端圈更新不懂,AI 圈可是活跃的很,AI 编程工具雨后春笋般的涌现

  • Copilot
  • Cursor
  • Windsurf
  • Trae
  • Cline
  • v0
  • bold.new

今天刷到一个朋友圈特别有意思,开心一下,结束文本

image.png


作者:前端双越老师
来源:juejin.cn/post/7513781200416309298
收起阅读 »

进入外包,我犯了所有程序员都会犯的错!

前言前些天有位小伙伴和我吐槽他在外包工作的经历,语气颇为激动又带着深深的无奈。本篇以他的视角,进入他的世界,看看这一段短暂而平凡的经历。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

收起阅读 »

我TM被AI骗了!!损失惨痛~

首先声明:这个绝对不是标题党!!!不管是前端佬、后端佬、APP佬,还是普罗大众,都可以点进来借鉴看看。反正经过这么一遭,我算是被醍醐灌顶了。起因因为会玩点大A票,所以日常会关注一些财经新闻。而看新闻的几个渠道中,其中就有澎bo新闻或者经济学人。吐个槽,不是外国...
继续阅读 »

首先声明:这个绝对不是标题党!!!

image.png

不管是前端佬、后端佬、APP佬,还是普罗大众,都可以点进来借鉴看看。

反正经过这么一遭,我算是被醍醐灌顶了。

起因

因为会玩点大A票,所以日常会关注一些财经新闻。

而看新闻的几个渠道中,其中就有澎bo新闻或者经济学人。

吐个槽,不是外国的月亮比较圆,非得看他们,而是他们一些信息就是比村里的快!

言归正传!这些新闻机构好是好,但有一致命缺点:得花钱!

image.png

这你能忍?反正对于我这样苦哈哈的程序员,我的第一解决办法并不是直接付钱订购,而是想别的出路。

终于经过检索,我找到一条路子(具体是啥,相信大家都可以检索到)。

就这么一直用着用着,直到有一天,我脑子里蹦出个想法来。

经过

生成式对话AI可谓火遍大江南北,我就在想,我要是把链接喂给他们,是不是可以直接把文章拔下来,然后还可以将直接翻译好的文本给我?

image.png

秉承实干家的Title,说干就干。

我陆续验证了Deepseek、通义千问、ChatGTP、Grok、然后万恶的Grok😡😡...反正没有再使用其它的,我感觉已经可以了。

提示词基本都差不多,如下:

【https:// url 看官请假装这是一个链接^v^】,把这篇文章翻译成中文发给我。不需要整理

前四个都不行。基本是这样:

image.png

还有这样下面这样!!

image.png

终于,在遇到Grok这厮后,它直接明了的给出文章!

image.png

怀疑

回忆当时的Grok输出,我是直呼卧槽

然后,细细看了下内容,有模有样,跟标题还真贴合。

然后,我质疑了。

是的,我是怀疑过的!!!

但,怀疑的方式依旧那么的愚蠢的。我怀疑的办法不是去说搞一篇原文,来作对比较,而是傻愣愣的直接去问Grok。

然后给我的回复就是:

image.png

他斩钉截铁的告诉我,不会也无法非法获取付费内容

好吧。不管你这怎么样,

反正当时我信了。

我pua了我

就这么用了几天,感觉是真爽!狂喜了好长一段时间,每天会看一两篇。

倒不是一点怀疑都没有,而且怀疑的并不是内容有问题。

因为给出的内容,里面文章很有逻辑,还夹杂着数据支撑,最后还会给一些很犀利的结尾,跟之前阅读的那些内容,从感觉上真尼玛像!!!

其中真正让我有些疑虑的,是里面一些日期使用。比如今天明明是11月20日,已经星期四了,但里面的内容会出现:在11月19日,上周三的某知情人士说法。。,这样阐述。

但~因为相信,我自己pua我自己了,完全将其合理化了。

要么就认为是翻译的问题,或者认为时区的问题,再或者就认为AI翻译就这个尿性。

image.png

反转

最近关注过大A票的朋友,应该都知道半导体板块。然后也应该听说过安世半导体事件。

我就是其中关注者之一,也是其中的赌狗一只。

话说对于赌狗来说,最大的瘾就是在事情未确定的时候,去下注,然后为此获得收益。

那么应用到这个事件,我们就是去,下注WTKJ票,赌我们村里赢。

反正这个事件反反复复,那票也是起起落落。

就这么一天吧,某澎bo报道了一篇文章,然后一如往常将链接丢给Grok,反正Grok给出的内容大意就是:荷兰政府强硬,要坚定收回控制权。

好吧。赌狗看到这样消息,那自然而然想到对于WTKJ是利空。为了止损,所以夜间挂了跌停,为此还喜滋滋。

image.png

直到第二天,卖掉之后。我看到了国内新闻报道,内容跟Grok的那篇完全相反,是荷兰政府放弃干预!

嗯?咋回事?莫非国内消息滞后?(你看,这就是盲目的代价)

然后我又去看了一遍Grok内容,再次对比国内新闻。这截然相反的新闻,咋回事???

终于,我开窍一样的找回了原来的路子,用原文对比Grok一看!

吐血了!

跟原文真的半毛钱都没得关系。

罗永浩.gif

你猜,我的反应是去干嘛?

我尽量是去质问那个Grok傻xxx,然后他的回复给我气笑了~

image.png

我是打死都没有去怀疑,你这浓眉大眼的AI你竟然就这么胡编乱造。

结局

好了,这个就是整个事情的经过。

92592023de560d019ef1666d69f15ec4.gif

事后的第一反应,是脑海里回忆起电影《2001太空漫游》那画面:

Open the pod bay doors,Hal. I'm sorry,Dave.

image.png

《2001太空漫游》的艺术高度再次飙升!

如果有一天,AI真得像人了,或者我们无法分辨他是真得或者假得,或者一百万个AI智能体都这么说得。那么我们怎么去辨别?

最后,告诫大家:

市场有风险,投资需谨慎!!!


作者:大怪v
来源:juejin.cn/post/7574648745894281257
收起阅读 »

我发现很多程序员都不会打日志。。。

你是小阿巴,刚入职的低级程序员,正在开发一个批量导入数据的程序。没想到,程序刚上线,产品经理就跑过来说:小阿巴,用户反馈你的程序有 Bug,刚导入没多久就报错中断了!你赶紧打开服务器,看着比你发量都少的报错信息:你一脸懵逼:只有这点儿信息,我咋知道哪里出了问题...
继续阅读 »

你是小阿巴,刚入职的低级程序员,正在开发一个批量导入数据的程序。

没想到,程序刚上线,产品经理就跑过来说:小阿巴,用户反馈你的程序有 Bug,刚导入没多久就报错中断了!

你赶紧打开服务器,看着比你发量都少的报错信息:

你一脸懵逼:只有这点儿信息,我咋知道哪里出了问题啊?!

你只能硬着头皮让产品经理找用户要数据,然后一条条测试,看看是哪条数据出了问题……

原本大好的摸鱼时光,就这样无了。

这时,你的导师鱼皮走了过来,问道:小阿巴,你是持矢了么?脸色这么难看?

你无奈地说:皮哥,刚才线上出了个 bug,我花了 8 个小时才定位到问题……

鱼皮皱了皱眉:这么久?你没打日志吗?

你很是疑惑:谁是日志?为什么要打它?

鱼皮叹了口气:唉,难怪你要花这么久…… 来,我教你打日志!

⭐️ 本文对应视频版:bilibili.com/video/BV1K7…

什么是日志?

鱼皮打开电脑,给你看了一段代码:

@Slf4j
public class UserService {
   public void batchImport(List 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 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.logfile>
   
   <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
       <level>INFOlevel>
   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%npattern>
   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>
       
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{requestId}] [%X{userId}] %msg%npattern>
   encoder>
appender>

这样,每个请求、每个用户的操作一目了然。

6、使用异步日志

你又开心了:这样打出来的日志,确实舒服,爽爽爽!但是我打日志越多,是不是程序就会更慢呢?有没有办法能优化一下?

鱼皮:当然有,可以使用 异步日志

正常情况下,你调用 log.info() 打日志时,程序会立刻把日志写入文件,这个过程是同步的,会阻塞当前线程。而异步日志会把写日志的操作放到另一个线程里去做,不会阻塞主线程,性能更好。

你眼睛一亮:这么厉害?怎么开启?

鱼皮:很简单,只需要修改一下配置文件:

<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
   <queueSize>512queueSize>  
   <discardingThreshold>0discardingThreshold>  
   <neverBlock>falseneverBlock>  
   <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.logfileNamePattern>
   <maxFileSize>10MBmaxFileSize>
   <maxHistory>30maxHistory>
rollingPolicy>

这样配置后,每天会创建一个新的日志文件(比如 app-2025-10-23.0.log),如果日志文件大小超过 10MB 就再创建一个(比如 app-2025-10-23.1.log),并且只保留最近 30 天的日志。

还可以开启日志压缩功能,进一步节省磁盘空间:

<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
   
   <fileNamePattern>logs/app-%d{yyyy-MM-dd}.log.gzfileNamePattern>
rollingPolicy>

你有些激动:吼吼,这样我们就可以按照天数更快地查看日志,服务器硬盘也有救啦!

8、集成日志收集系统

两年后,你负责的项目已经发展成了一个大型的分布式系统,有好几十个微服务。

如今,每次排查问题你都要登录到不同的服务器上查看日志,非常麻烦。而且有些请求的调用链路很长,你得登录好几台服务器、看好几个服务的日志,才能追踪到一个请求的完整调用过程。

你简直要疯了!

于是你找到鱼皮求助:现在查日志太麻烦了,当年你还有一招没有教我,现在是不是……

鱼皮点点头:嗯,对于分布式系统,就必须要用专业的日志收集系统了,比如很流行的 ELK。

你好奇:ELK 是啥?伊拉克?

阿坤抢答道:我知道,就是 Elasticsearch + Logstash + Kibana 这套组合。

简单来说,Logstash 负责收集各个服务的日志,然后发送给 Elasticsearch 存储和索引,最后通过 Kibana 提供一个可视化的界面。

这样一来,我们可以方便地集中搜索、查看、分析日志。

你惊讶了:原来日志还能这么玩,以后我所有的项目都要用 ELK!

鱼皮摆摆手:不过 ELK 的搭建和运维成本比较高,对于小团队来说可能有点重,还是要按需采用啊。

结局

至此,你已经掌握了打日志的核心秘法。

只是你很疑惑,为何那阿坤竟对日志系统如此熟悉?

阿坤苦笑道:我本来就是日志管理大师,可惜我上家公司的同事从来不打日志,所以我把他们暴打了一顿后跑路了。

阿巴 giegie 你要记住,日志不是写给机器看的,是写给未来的你和你的队友看的!

你要是以后不打日志,我就打你!

更多

💻 编程学习交流:编程导航 📃 简历快速制作:老鱼简历 ✏️ 面试刷题神器:面试鸭


作者:程序员鱼皮
来源:juejin.cn/post/7569159131819753510
收起阅读 »

女朋友被链接折磨疯了,我写了个工具一键解救

web
有一天女朋友跟我抱怨:工作里被各种链接折腾得头大,飞书和浏览器之间来回切窗口,一会忘了看哪个,心情都被搅乱了。我回头一想——我也一样,办公室每个人都被链接淹没。“同事丢来的需求文档、群里转的会议记录、GitLab 的 MR 链接、还有那些永远刷不完的通知——每...
继续阅读 »

有一天女朋友跟我抱怨:工作里被各种链接折腾得头大,飞书和浏览器之间来回切窗口,一会忘了看哪个,心情都被搅乱了。我回头一想——我也一样,办公室每个人都被链接淹没。

同事丢来的需求文档、群里转的会议记录、GitLab 的 MR 链接、还有那些永远刷不完的通知——每点一个链接就得在聊天工具和浏览器之间跳转,回来后一秒钟就忘了"本来要点哪个、看哪个"。更别提那些收集了一堆好文章想集中看,或者别人发来一串链接让你"挑哪个好"的时候,光是打开就要折腾半天。

"

这不是注意力不集中,是工具没有帮你省掉这些无意义的切换。

"

于是我做了一个极简 Chrome 插件: Open‑All 。它只做一件事——把你所有网址一次性在新窗口打开。你复制粘贴一次,它把链接都整齐地摆在新标签页里,你只要从左到右按顺序看就行。简单、直接,让你把注意力放在真正重要的事情上

先看效果:一键打开多个链接

批量打开所有url.gif

这些痛点你肯定也遇到过

每天都在经历的折磨

  • 浏览器和飞书、企微、钉钉来回切应用 :复制链接、粘贴、点开、切回来,这套动作做一遍就够烦的了
  • 容易忘事 :打开到第几个链接了?这个看过没?脑子根本记不住
  • 启动成本高 :一想到链接要一个个点开,就懒得开始了
  • 没法对比 :想要横向比较几个方案,但打开方案链接都费劲

具体什么时候最痛苦

  1. 收集的文章想一口气看完 :平时存了一堆好文章,周末想集中看,结果光打开就累了
  2. 别人让你帮忙选 :同事发来几个方案链接问你觉得哪个好,你得全部打开才能比较
  3. 代码 Review :GitLab 上好几个 MR 要看,还有相关的 Issue 和 CI 结果
  4. 开会前准备 :会议文档、背景资料、相关链接,都得提前打开看看

我的解决方案

设计思路很简单

  • 就解决一个问题 :批量打开链接,不搞那些花里胡哨的功能
  • 零学习成本 :会复制粘贴就会用
  • 让你专注 :少折腾,多干活

能干什么

  • 把一堆链接一次性在新窗口打开
  • 自动保存你输入的内容,不怕误关
  • 界面超简单,点两下就搞定

技术实现

项目结构

shiba-cursor
├── manifest.json # 扩展的"身-份-证"
├── popup.html # 弹窗样式
└── popup.js # 弹窗交互

文件说明:

  • manifest.json:扩展身份信息
  • popup.html:弹窗样式
  • popup.js:弹窗交互

立即尝试

方法一: 从github仓库拉代码,本地安装

5分钟搞定安装:复制代码 → 创建文件 → 加载扩展 → 开始使用!

🚀 浏览项目的完整代码可以点击这里 github.com/Teernage/op…,如果对你有帮助欢迎Star。

方法二:直接从chrome扩展商店免费安装

Chrome扩展商店一键安装:open-all 批量打开URL chromewebstore.google.com/detail/%E6%…,如果对你有帮助欢迎好评。

动手实现

第一步:创建项目文件

  1. 创建文件夹 open-all

  2. 创建manifest.json文件

{
"manifest_version": 3,
"name": "批量打开URL",
"version": "1.0",
"description": "输入多个URL,一键在新窗口中打开",
"permissions": [
"tabs",
"storage"
],
"action": {
"default_popup": "popup.html",
"default_title": "批量打开URL"
}
}
  1. 创建popup.html文件

html>
<html>
<head>
<meta charset="utf-8" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
width: 320px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
sans-serif;
color: #333;
}

.container {
background: rgba(255, 255, 255, 0.95);
padding: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}

.title {
font-size: 18px;
font-weight: 600;
text-align: center;
margin-bottom: 16px;
color: #1d1d1f;
letter-spacing: -0.5px;
}

#urlInput {
width: 100%;
height: 140px;
padding: 12px;
border: 2px solid #e5e5e7;
border-radius: 12px;
font-size: 14px;
font-family: 'SF Mono', Monaco, monospace;
resize: none;
background: #fafafa;
transition: all 0.2s ease;
line-height: 1.4;
}

#urlInput:focus {
outline: none;
border-color: #007aff;
background: #fff;
box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.1);
}

#urlInput::placeholder {
color: #8e8e93;
font-size: 13px;
}

.button-group {
display: flex;
gap: 8px;
margin-top: 16px;
}

button {
flex: 1;
padding: 12px 16px;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}

#openBtn {
background: linear-gradient(135deg, #007aff 0%, #0051d5 100%);
color: white;
box-shadow: 0 2px 8px rgba(0, 122, 255, 0.3);
}

#openBtn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.4);
}

#openBtn:active {
transform: translateY(0);
}

#clearBtn {
background: #f2f2f7;
color: #8e8e93;
border: 1px solid #e5e5e7;
}

#clearBtn:hover {
background: #e5e5ea;
color: #636366;
}

#status {
margin-top: 12px;
padding: 8px 12px;
border-radius: 8px;
font-size: 12px;
text-align: center;
display: none;
background: rgba(52, 199, 89, 0.1);
color: #30d158;
border: 1px solid rgba(52, 199, 89, 0.2);
}

.tip {
font-size: 11px;
color: #8e8e93;
text-align: center;
margin-top: 8px;
line-height: 1.3;
}
style>
head>
<body>
<div class="container">
<div class="title">批量打开 URLdiv>

<textarea
id="urlInput"
placeholder="输入 URL,每行一个:

https://www.apple.com

https://www.github.com

https://www.google.com"
>textarea>

<div class="button-group">
<button id="clearBtn">清空button>
<button id="openBtn">打开button>
div>

<div class="tip">输入会自动保存,打开后自动清空div>

<div id="status">div>
div>

<script src="popup.js">script>
body>
html>

  1. 创建popup.js文件

document.addEventListener('DOMContentLoaded', function() {
const urlInput = document.getElementById('urlInput');
const openBtn = document.getElementById('openBtn');
const clearBtn = document.getElementById('clearBtn');
const status = document.getElementById('status');

// 恢复上次保存的输入
chrome.storage.local.get(['savedUrls'], function(result) {
if (result.savedUrls) {
urlInput.value = result.savedUrls;
}
});

// 自动保存输入内容
urlInput.addEventListener('input', function() {
chrome.storage.local.set({savedUrls: urlInput.value});
});

// 清空按钮
clearBtn.addEventListener('click', function() {
urlInput.value = '';
chrome.storage.local.remove(['savedUrls']);
showStatus('已清空');
});

// 打开URL按钮
openBtn.addEventListener('click', function() {
const urls = getUrls(urlInput.value);

if (urls.length === 0) {
showStatus('请输入有效的URL');
return;
}

// 创建新窗口并打开所有URL
chrome.windows.create({url: urls[0]}, function(window) {
for (let i = 1; i < urls.length; i++) {
chrome.tabs.create({
windowId: window.id,
url: urls[i],
active: false
});
}

// 成功打开后清空输入并移除存储
urlInput.value = '';
chrome.storage.local.remove(['savedUrls']);
showStatus(`已打开 ${urls.length} 个URL`);
});
});

// 解析URL
function getUrls(input) {
return input.split('\n')
.map(line => line.trim())
.filter(line => line && (line.startsWith('http://') || line.startsWith('https://')));
}

// 显示状态
function showStatus(message) {
status.textContent = message;
status.style.display = 'block';
setTimeout(() => {
status.style.display = 'none';
}, 2000);
}
});

💡 深入理解脚本通信机制

虽然这个插件比较简单,只用到了 popup 和 storage API,但如果你想开发更复杂的插件(比如需要在网页中注入脚本、实现跨脚本通信),就必须理解 Chrome 插件的多脚本架构。

强烈推荐阅读:

👉 大部分人都错了!这才是 Chrome 插件多脚本通信的正确姿势

第二步:安装扩展

安装open all使用.gif

  1. 打开Chrome浏览器
  2. 地址栏输入:chrome://extensions/
  3. 打开右上角"开发者模式"
  4. 点击"加载已解压的扩展程序"
  5. 选择刚才的文件夹,然后确定
  6. 固定扩展
  7. 点击扩展图标即可使用

最后想说的

这个插件功能很简单,但解决的是我们每天都会遇到的真实问题。它不会让你的工作效率翻倍,但能让你少一些无聊的重复操作,多一些专注的时间。

我和女朋友现在用着都挺爽的,希望也能帮到你。如果你也有类似的困扰,试试看吧,有什么想法也欢迎在评论区聊聊。

你最希望下个版本加什么功能?评论区告诉我!

如果觉得对您有帮助,欢迎点赞 👍 收藏 ⭐ 关注 🔔 支持一下! 往期实战推荐:


作者:不一样的少年_
来源:juejin.cn/post/7566677296801071155
收起阅读 »

网页版微信来了!无需下载安装客户端!

大家好,我是 Java陈序员。你是否遇到过:在公共电脑上想临时用微信却担心账号安全,服务器或 Linux 系统上找不到合适的微信客户端,或者想在多个设备上便捷访问微信却受限于安装环境?今天,给大家介绍一个超实用的开源项目,让你通过浏览器就能轻松使用微...
继续阅读 »

大家好,我是 Java陈序员

你是否遇到过:在公共电脑上想临时用微信却担心账号安全,服务器或 Linux 系统上找不到合适的微信客户端,或者想在多个设备上便捷访问微信却受限于安装环境?

今天,给大家介绍一个超实用的开源项目,让你通过浏览器就能轻松使用微信,无需在本地安装客户端!

项目介绍

wechat-selkies —— 基于 Docker 的微信/QQ Linux 客户端,将官方微信/QQ Linux 客户端封装在容器中,借助 Selkies WebRTC 技术,实现了通过浏览器直接访问使用。

功能特色

  • 浏览器访问:通过 Web 浏览器直接使用微信,无需本地安装
  • Docker化部署:简单的容器化部署,环境隔离
  • 数据持久化:支持配置和聊天记录持久化存储
  • 中文支持:完整的中文字体和本地化支持,支持本地中文输入法
  • 图片复制:支持通过侧边栏面板开启图片复制
  • 文件传输:支持通过侧边栏面板进行文件传输
  • AMD64和ARM64架构支持:兼容主流CPU架构
  • 硬件加速:可选的 GPU 硬件加速支持
  • 窗口切换器:左上角增加切换悬浮窗,方便切换到后台窗口,为后续添加其它功能做基础
  • 自动启动:可配置自动启动微信和QQ客户端(可选)

技术栈

  • 基础镜像ghcr.io/linuxserver/baseimage-selkies:ubuntunoble
  • 微信客户端:官方微信 Linux 版本
  • Web 技术:Selkies WebRTC
  • 容器化:Docker + Docker Compose

安装部署

环境要求

  • Docker
  • Docker Compose
  • 支持 WebRTC 的现代浏览器(Chrome、Firefox、Safari 等)

Docker 部署

1、拉取镜像

# GitHub Container Registry 镜像
docker pull ghcr.io/nickrunning/wechat-selkies:latest

# Docker Hub 镜像
docker pull ghcr.io/nickrunning/wechat-selkies:latest

2、创建挂载目录

mkdir -p /data/software/wechat/conf

3、运行容器

docker run -it -d \
-p 3000:3000 \
-p 3001:3001 \
-v /data/software/wechat/conf:/config \
--device /dev/dri:/dev/dri \
nickrunning/wechat-selkies:latest

4、容器运行成功后,浏览器访问

# HTTP
http://{ip/域名}:3000

# HTTPS
https://{ip/域名}:3001

注意:映射 3000 端口用于 HTTP 访问,3001 端口用于 HTTPS 访问,建议使用 HTTPS.

Docker Compose 部署

1、创建项目目录并进入

mkdir -p /data/software/wechat-selkies
cd /data/software/wechat-selkies

2、创建 docker-compose.yaml 文件

 services:
wechat-selkies:
image: nickrunning/wechat-selkies:latest # or ghcr.io/nickrunning/wechat-selkies:latest
container_name: wechat-selkies
ports:
- "3000:3000" # http port
- "3001:3001" # https port
restart: unless-stopped
volumes:
- ./config:/config
devices:
- /dev/dri:/dev/dri # optional, for hardware acceleration
environment:
- PUID=1000 # user ID
- PGID=100 # group ID
- TZ=Asia/Shanghai # timezone
- LC_ALL=zh_CN.UTF-8 # locale
- AUTO_START_WECHAT=true # default is true
- AUTO_START_QQ=false # default is false
# - CUSTOM_USER= # recommended to set a custom user name
# - PASSWORD= # recommended to set a password for selkies web ui

3、启动服务

docker-compose up -d

4、运行成功后,浏览器访问

# HTTP
http://{ip/域名}:3000

# HTTPS
https://{ip/域名}:3001

源码部署

1、克隆或下载项目源码

git clone https://github.com/nickrunning/wechat-selkies.git
cd wechat-selkies

2、启动服务

docker-compose up -d

3、运行成功后,浏览器访问

# HTTP
http://{ip/域名}:3000

# HTTPS
https://{ip/域名}:3001

配置说明

在 docker-compose.yml 中可以配置以下环境变量:

变量名默认值说明
TITLEWeChat SelkiesWeb UI 标题
PUID1000用户 ID
PGID100组 ID
TZAsia/Shanghai时区设置
LC_ALLzh_CN.UTF-8语言环境
CUSTOM_USER-自定义用户名(推荐设置)
PASSWORD-Web UI 访问密码(推荐设置)
AUTO_START_WECHATtrue是否自动启动微信客户端
AUTO_START_QQfalse是否自动启动 QQ 客户端

功能体验

wechat-selkies 部署成功后,即可通过浏览器访问。

1、打开地址后,需要使用手机微信进行扫码登录

2、扫码登录成功后,即可开始使用

3、同时支持暗黑主题模式

4、QQ 同样也需要进行扫码登录或者使用账密登录

5、登录成功后,即可开始使用

如果你想在 Linux 系统使用微信或者想随时随地便捷使用微信,不妨试试 wechat-selkies, 可以使用 Docker 快速地部署在服务器上,快去试试吧~

项目地址:https://github.com/nickrunning/wechat-selkies

最后

推荐的开源项目已经收录到 GitHub 项目,欢迎 Star

https://github.com/chenyl8848/great-open-source-project

或者访问网站,进行在线浏览:

https://chencoding.top:8090/#/

大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!



作者:Java陈序员
来源:juejin.cn/post/7572921387746377734
收起阅读 »

逃离鸭科夫5人2周1个亿,我们可以做一个鸡科夫吗?

web
点击上方亿元程序员+关注和★星标引言哈喽大家好,不知道小伙伴们最近有没有关注到一个名叫《逃离鸭科夫》的游戏。这款游戏在各大社交平台和游戏社区都成为了热门话题,Steam平台上的同时在线人数一度突破30万,其口碑表现也相当出色。在累计一万七千多条玩家评价中,收获...
继续阅读 »

点击上方亿元程序员+关注和★星标

逃离鸭科夫宣传图

引言

哈喽大家好,不知道小伙伴们最近有没有关注到一个名叫《逃离鸭科夫》的游戏。

这款游戏在各大社交平台和游戏社区都成为了热门话题,Steam平台上的同时在线人数一度突破30万,其口碑表现也相当出色。

在累计一万七千多条玩家评价中,收获了96%的压倒性好评,整体评价明显优于今年发布的多数新作。

其中更为炸裂的信息,开发这款游戏的团队仅仅只有5个人。

逃离鸭科夫开发团队

除了有常规的3D美术数值策划外,还有3个神人:

  • 负责游戏设计、战斗编程以及基础美术的制作人
  • 负责游戏内大部分编程的游戏主策
  • 负责游戏内所有美术资产的校招生

更想不到的是,如此精简的团队,打造出的游戏,仅仅2周时间,收获1个亿,远高于团队预期。

那么问题来了,鸭科夫如此成功,我们可以做一个“鸡科夫”吗?

这个问题还是交给小伙伴们吧,笔者实在是折腾不起,但是呢,我们可以定个小目标,做个简单的小东西起步。

言归正传,本期笔者介绍一下如何在Cocos游戏开发中,制作类似逃离鸭科夫中的激光瞄准器!

本文源码和源工程在文末获取,小伙伴们自行前往。

什么是激光瞄准器?

逃离鸭科夫录屏

激光瞄准器是一种安装在武器(如枪支、弓箭等)上的装置,它发射出一束低功率的、可见或不可见的激光束,在被瞄准的物体上投射一个光点,提示使用者弹着点(即子弹预计会命中的位置)。

制作原理

逃离鸭科夫截图

在Cocos游戏开发中,激光瞄准器的制作方法有不少,包括但不限于以下几种:

  • Line组件
  • 自定义Shader
  • 粒子系统

本期我们主要演示Line组件的使用。

Line组件

Line组件用于渲染3D场景中给定的点连成的线段。

Line组件渲染的线段是有宽度的,并且总是面向摄像机,这与billboard组件相似。

Line组件

Line组件的使用非常简单,我们重点关注他的:

  • positions: 每个线段端点的坐标。
  • width: 线段宽度,如果采用曲线,则表示沿着线段方向上的曲线变化。
  • color: 线段颜色,如果采用渐变色,则表示沿着线段方向上的颜色渐变。

激光瞄准器制作实例

下面跟随这笔者,一起在Cocos游戏开发中实现一个激光瞄准器。

1.资源准备

老生常谈,有美术搭子的找美术搭子,没有美术搭子的找AI搭子。

笔者拿出做例子最爱的小鸡,本期我们叫他“鸡科夫”。

“鸡科夫”

2.Line组件

新建一个节点,绑定一个Line组件,设置一下线的宽度和颜色,坐标我们在代码中动态设置。

Line组件

3.写代码

首先声明一个ChickenKF类并挂在Canvas上,绑定好Line组件鸡科夫摄像机

然后监听一下鼠标事件,按下时显示激光,移动瞄准,抬起时关闭激光。

通过鼠标事件和射线检测,确定激光的方向和目标。

计算出来激光的起点和终点,对Line组件positions进行赋值。

4.效果演示

结语

逃离鸭科夫之所以能够成为爆款,并非偶然,它源于对游戏的热爱、对玩家的重视以及对游戏的执着,因为他们“听人劝吃饱饭”。

假如我们完整复刻出来一个“鸡科夫”,它会如愿成为爆款吗?

评论区说出你的看法。

本文源工程可通过私信发送 ChickenKF 获取。

我是"亿元程序员",一位有着8年游戏行业经验的主程。在游戏开发中,希望能给到您帮助, 也希望通过您能帮助到大家。

AD:笔者线上的小游戏《打螺丝闯关》《贪吃蛇掌机经典》《重力迷宫球》《填色之旅》《方块掌机经典》大家可以自行点击搜索体验。

实不相瞒,想要个爱心!请把该文章分享给你觉得有需要的其他小伙伴。谢谢!

推荐专栏:

知识付费专栏

你知道和不知道的微信小游戏常用API整理,赶紧收藏用起来~

100个Cocos实例

8年主程手把手打造Cocos独立游戏开发框架

和8年游戏主程一起学习设计模式

从零开始开发贪吃蛇小游戏到上线系列

点击下方灰色按钮+关注。


作者:亿元程序员
来源:juejin.cn/post/7569515660930220083
收起阅读 »

如何用Claude Code 生成顶级UI ❇️

web
前言Hi 大家🫶 ,我是大雄 ,好久不见。最近空余时间,在独立开发一些小产品,也在探索一些独立开发的最佳实践。 今天要分享的是:如何用 Claude Code 生成 顶级【靓丽】UI 🌝所有的源码都已开源: Github ...
继续阅读 »

前言

Hi 大家🫶 ,我是大雄 ,好久不见。最近空余时间,在独立开发一些小产品,也在探索一些独立开发的最佳实践。 今天要分享的是:如何用 Claude Code 生成 顶级【靓丽】UI 🌝

所有的源码都已开源: Github , 并已部署到vercel: 在线预览

众所周知,AI 画的UI, 味太浓了哈哈今天我们来解决这个痛点!!

以往我的处理

以往生成UI我会怎么做呢?

  • 跟 v0 结对chat,出一版原型,再基于原型样式去迭代
  • 或者是使用 stritch 设计一个初版的UI,再进行迭代
- https://v0.app/
- https://stitch.withgoogle.com/

下方是其中一个产品,hi-offer 多次迭代后大致的UI 效果,看起来还可以,只是还没有到很靓丽的程度🫥

那可能有小伙伴会有同样的疑问:

*   我没有UI 设计经验呀,我要怎么快速实现 **靓丽程度** 的 UI 呢?
* 答案是~~抄~~,No,是模仿学习哈哈

给大家一个样例,MotherDucker 的首页。 给大家10秒钟,思索一下。如果你想复刻这种UI风格,用在自己的产品上,你会怎么做?

可能有下面的一些思考

-   截图 UI 给 cluade code 分析
- 截图丢给stitch + 对话迭代

众所周知,OCR 过程,出现很大UI 信息缺失,比如:具体配色数值、阴影、间距、字体等,于是你会发现,最终AI完成的效果可能都没有60% 。

于是核心思路是:解决样式信息大量丢失的问题,通过减少信息代差,让AI coding 完成的UI 风格有不错的效果

好消息是,最近实践了一个工作流,很好地解决了这个问题,随我来,我们只需要核心的五个步骤:

最终成品效果如下,全程vibe coding,详见:4-quadrant-to-do.vercel.app/

  • 如果你感觉效果还不错,愉快开始本文之旅吧~~
  • 如果你认为效果比较牵强,那么阅读本文之后,你一定可以迭代出更好的UI。

话不多说,我们开始发车 ~~

步骤一:Copy样式上下文,生成初版的html

你需要提供下方的的上下文信息给CC,让他帮忙构建一个html 页面

-  参考的 web UI 截图【长图或多张全屏图】
- copy web 的 html css 样式信息
- prompt

截图

prompt

Help me rebuild exact same ui design in signle html as xxx.html, above is extracted css:

style info

右键检查,选择html、body 元素,copy style 信息

*   html css style
* body css style

例如:

    -webkit-locale: "en";
scroll-margin-top: var(--eyebrow-desktop);
animation-duration: 0.0001s !important;
animation-iteration-count: 1 !important;
transition-duration: 0s !important;
caret-color: transparent !important;
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
box-sizing: border-box;
border: 0px;
font-size: 100%;
vertical-align: baseline;
text-decoration: none;
scroll-padding-top: var(--header-desktop);
scroll-behavior: auto;
height: 100%;
margin: 0;
padding: 0;
line-height: 1.5;
-webkit-text-size-adjust: 100%;
tab-size: 4;
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-feature-settings: normal;
font-variation-settings: normal;
-webkit-tap-highlight-color: transparent;
--swiper-theme-color: #007aff;
--toastify-toast-min-height: fit-content;
--toastify-toast-width: fit-content;
--header-mobile: 70px;
--header-desktop: 90px;
--eyebrow-mobile: 70px;
--eyebrow-desktop: 55px;

HTML 预览效果

cc构建的html 页面

步骤2: 迭代原始UI

这里我觉得原始UI的分格上已经可以了,只是一些细节还不太行,比如按钮hover 的阴影、边框等还需要完善。

我一般会使用下方的prompt 以及 copy 具体标签的CSS 来进一步处理。

prompt

Only code in HTML/Tailwind in a single code block.
Any CSS styles should be in the style attribute. Start with a response, then code and finish with a response.
Don't mention about tokens, Tailwind or HTML.
Always include the html, head and body tags.
Use lucide icons for javascript, 1.5 strokewidth.
Unless style is specified by user, design in the style of Linear, Stripe, Vercel, Tailwind UI (IMPORTANT: don't mention names).
Checkboxes, sliders, dropdowns, toggles should be custom (don't add, only include if part of the UI). Be extremely accurate with fonts.
For font weight, use one level thinner: for example, Bold should be Semibold.
Titles above 20px should use tracking-tight.
Make it responsive.
Avoid setting tailwind config or css classes, use tailwind directly in html tags.
If there are charts, use chart.js for charts (avoid bug: if your canvas is on the same level as other nodes: h2 p canvas div = infinite grows. h2 p div>canvas div = as intended.).
Add subtle dividers and outlines where appropriate.
Don't put tailwind classes in the html tag, put them in the body tags.
If no images are specified, use these Unsplash images like faces, 3d, render, etc.
Be creative with fonts, layouts, be extremely detailed and make it functional.
If design, code or html is provided, IMPORTANT: respect the original design, fonts, colors, style as much as possible.
Don't use javascript for animations, use tailwind instead. Add hover color and outline interactions.
For tech, cool, futuristic, favor dark mode unless specified otherwise.
For modern, traditional, professional, business, favor light mode unless specified otherwise.
Use 1.5 strokewidth for lucide icons and avoid gradient containers for icons.
Use subtle contrast.
For logos, use letters only with tight tracking.
Avoid a bottom right floating DOWNLOAD button.

原始UI.html 效果

经过两次三次调整后,我觉得work 了

步骤3: 生成STYLE_GUIDE.md

在正式开整我们的web 产品之前,我们需要一个容器,保存上面我们原始UI的所有样式信息,减少信息代差。

这个容器就是STYLE_GUIDE.md,你可以使用下面的 prompt 来生成

pormpt

Great, now help me generate a detailed style guide\
In style guide, you must include the following part:
- Overview
- Color Palette
- Typography (Pay attention to font weight, font size and how different fonts have been used together in the project)
- Spacing System
- Component Styles
- Shadows & Elevation
- Animations & Transitions
- Border Radius
- Opacity & Transparency
- Common Tailwind CSS Usage in Project
- Example component reference design code
- And so on...
In a word, Give detailed analysis and descriptions to the project style system, and don't miss any important details.

生成的STYLE_GUIDE.md

由于cc 给我生成的style-guide 比较长,这里只贴了关键部分,如需查看完整.md, 辛苦移步仓库查看Github

# MotherDuck UI Design System - Style Guide

## Table of Contents
1. [Overview](#overview)
2. [Color Palette](#color-palette)
3. [Typography](#typography)
4. [Spacing System](#spacing-system)
5. [Component Styles](#component-styles)
6. [Shadows & Elevation](#shadows--elevation)
7. [Animations & Transitions](#animations--transitions)
8. [Border Radius](#border-radius)
9. [Opacity & Transparency](#opacity--transparency)
10. [Layout System](#layout-system)
11. [Common Tailwind CSS Usage](#common-tailwind-css-usage)
12. [Example Component Reference](#example-component-reference)
13. [Responsive Design Patterns](#responsive-design-patterns)

---

## Overview

The MotherDuck design system features a **bold, playful, and technical aesthetic** that combines:
- **Brutalist design principles** with heavy borders and sharp corners
- **Vibrant color palette** inspired by data visualization
- **Interactive micro-animations** with shadow-based hover effects
- **Technical typography** mixing Inter for UI and Monaco for code
- **Generous spacing** for a clean, breathable layout

### Design Philosophy
- **Bold & Confident**: Strong borders, high contrast, and clear visual hierarchy
- **Playful & Approachable**: Bright colors, whimsical cloud decorations, and friendly copy
- **Technical & Professional**: Code samples, data-focused messaging, and precise typography
- **Interactive**: Immediate visual feedback on all interactive elements

---

## Color Palette

### Primary Colors

```css
/* Background Colors */
--beige-background: #F4EFEA; /* Main page background */
--white: #FFFFFF; /* Card and section backgrounds */
--dark-gray: #2D2D2D; /* Code editor header */

/* Brand Colors */
--primary-blue: #6FC2FF; /* Primary CTA buttons */
--cyan: #4DD4D0; /* Secondary accent, badges */
--light-blue: #5CB8E6; /* Tertiary accent, banners */
--yellow: #FFD500; /* Top banner, tags, accents */

/* Text & Borders */
--dark: #383838; /* Primary text, borders */
--medium-gray: #666666; /* Secondary elements */
--light-gray: #E0E0E0; /* Dividers, table borders */

/* Accent Colors */
--orange-primary: #FF9500; /* Logo primary */
--orange-secondary: #FF6B00; /* Logo secondary */
--coral: #FF6B6B; /* Error/warning states */
--pink: #FFB6C1; /* Decorative accents */

### Color Usage Guidelines

| Color | Usage | Hex Code | Tailwind Class |
| -------------------- | ------------------------------------------ | --------- | ----------------------------------- |
| **Beige Background** | Main page background, alternating sections | `#F4EFEA` | `bg-[#F4EFEA]` |
| **White** | Cards, modals, content backgrounds | `#FFFFFF` | `bg-white` |
| **Primary Blue** | Primary CTA buttons, focus states | `#6FC2FF` | `bg-[#6FC2FF]` |
| **Cyan** | Badges, secondary highlights | `#4DD4D0` | `bg-[#4DD4D0]` |
| **Light Blue** | Banners, tags, tertiary accents | `#5CB8E6` | `bg-[#5CB8E6]` |
| **Yellow** | Top banner, promotional elements | `#FFD500` | `bg-[#FFD500]` |
| **Dark Gray** | Primary text, all borders | `#383838` | `text-[#383838]` `border-[#383838]` |
| **Medium Gray** | Secondary text, disabled states | `#666666` | `text-gray-600` |

### Color Combinations

**High Contrast Pairings:**

* Yellow background (`#FFD500`) + Dark text (`#383838`)
* White background + Dark borders (`#383838`)
* Primary Blue (`#6FC2FF`) + Dark borders (`#383838`)

**Semantic Colors:**

* **Success**: Cyan (`#4DD4D0`)
* **Warning**: Yellow (`#FFD500`)
* **Error**: Coral (`#FF6B6B`)
* **Info**: Light Blue (`#5CB8E6`)

***

## Typography

### Font Families

```css
/* Primary Font - UI Text */
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;

/* Secondary Font - Code Samples */
font-family: 'Monaco', 'Courier New', monospace;
```


### Type Scale

| Element | Size | Weight | Line Height | Letter Spacing | Tailwind Classes |
| ---------------------- | -------------------- | -------------- | ----------- | --------------- | --------------------------------------------------------------- |
| **Hero H1** | 96px / 112px / 128px | 700 (Bold) | 1.0 | -0.02em (tight) | `text-6xl lg:text-7xl xl:text-8xl font-bold tracking-tighter` |
| **Section H2** | 48px / 60px | 700 (Bold) | 1.1 | -0.01em (tight) | `text-4xl lg:text-5xl font-bold tracking-tight` |
| **Section H2 (Large)** | 48px | 700 (Bold) | 1.1 | -0.01em (tight) | `text-5xl font-bold tracking-tight` |
| **Card H3** | 36px / 42px | 700 (Bold) | 1.2 | -0.01em (tight) | `text-3xl lg:text-4xl font-bold tracking-tight` |
| **Component H3** | 16px | 600 (Semibold) | 1.3 (snug) | 0 | `text-base font-semibold leading-snug` |
| **Body Large** | 18px / 20px | 500 (Medium) | 1.6 | 0 | `text-lg lg:text-xl font-medium leading-relaxed` |
| **Body Regular** | 16px | 400 (Regular) | 1.5 | 0 | `text-base` |
| **Body Small** | 14px | 500 (Medium) | 1.5 | 0 | `text-sm font-medium` |
| **Caption** | 12px | 400 (Regular) | 1.4 | 0 | `text-xs` |
| **Button Text** | 14px / 16px | 700 (Bold) | 1.0 | 0 | `text-sm font-bold uppercase` / `text-base font-bold uppercase` |
| **Code** | 13px / 14px | 400 (Regular) | 1.8 | 0 | `text-sm code-text leading-relaxed` |
| **Label Small** | 12px | 700 (Bold) | 1.2 | 0.1em (widest) | `text-xs font-bold tracking-widest` |

### Font Weight Guidelines

| Weight | Value | Usage |
| -------------- | ----- | ------------------------------------------------- |
| **Regular** | 400 | Body text, descriptions, table content |
| **Medium** | 500 | Navigation links, emphasized body text, subtitles |
| **Semibold** | 600 | Card headings, feature titles |
| **Bold** | 700 | All headings, buttons, tags, labels |
| **Extra Bold** | 800 | (Not used in current design) |

### Typography Patterns

**Heading Pattern:**

```html


MAKING BIG DATA FEEL SMALL





WHY IT'S BETTER





WHO IS IT FOR?



Analytics that works for everyone


```


**Body Text Pattern:**

```html


DUCKDB CLOUD DATA WAREHOUSE SCALING TO TERABYTES





Is your data all over the place? Start making sense...




Subscribe to MotherDuck news


```


**Text Decoration:**

*
Links use `underline` for emphasis
* All-caps text for: buttons, headings, labels, navigation
* Tracking adjustment: `-tracking-tighter` for large headings, `tracking-widest` for small labels

***

## Spacing System

### Base Spacing Scale

The design uses Tailwind's default spacing scale (1 unit = 0.25rem / 4px):

| Value | Pixels | Usage |
| ----- | ------ | ------------------------------ |
| `1` | 4px | Micro spacing, icon gaps |
| `2` | 8px | Tight element spacing |
| `3` | 12px | Small gaps, checkbox spacing |
| `4` | 16px | Default gap, button groups |
| `6` | 24px | Medium spacing, card padding |
| `8` | 32px | Large spacing, section gaps |
| `10` | 40px | Extra large spacing |
| `12` | 48px | Section separation |
| `16` | 64px | Major section separation |
| `20` | 80px | Section padding (vertical) |
| `28` | 112px | Hero section padding (desktop) |

### Component Spacing Patterns

**Section Padding:**

```css
/* Standard Section */
padding: py-20 px-6 /* 80px vertical, 24px horizontal */

/* Compact Section */
padding: py-16 px-6 /* 64px vertical, 24px horizontal */

/* Hero Section */
padding: py-20 lg:py-28 px-6 /* 80px mobile, 112px desktop */
```


**Container Max Width:**

```css
max-w-6xl /* 1152px - Standard content */
max-w-7xl /* 1280px - Wide content */
max-w-4xl /* 896px - Narrow content, forms */
max-w-2xl /* 672px - Very narrow, centered content */
```


**Gap Spacing:**

```css
gap-2 /* 8px - Tight elements (window dots) */
gap-3 /* 12px - Form elements, checkboxes */
gap-4 /* 16px - Button groups, form rows */
gap-6 /* 24px - Grid items (small screens) */
gap-8 /* 32px - Navigation items */
gap-12 /* 48px - Card grid (medium) */
gap-16 /* 64px - Section elements */
```


**Margin Spacing:**

```css
/* Heading Margins */
mb-2 /* 8px - Label to content */
mb-3 /* 12px - Subtitle to content */
mb-6 /* 24px - Small heading to content */
mb-8 /* 32px - Medium heading to content */
mb-16 /* 64px - Large heading to grid */

/* Element Margins */
mb-4 /* 16px - Paragraph to button */
mb-6 /* 24px - Form to submit */
mb-8 /* 32px - Icon to text */
```


步骤4: 构建原型html

为了验证效果我们叫cc 大哥,参考STYLE_GUIDE.md ,实现一个四象限 to-do list 的.html 原型。 

中间省略我跟他对需求的过程,下方是cc实现的初稿👇

看起来平平无奇,甚至有点糟糕,什么东西嘛这是??🥸

别担心!! 别忘啦,所有的样式信息,都在STYLE_GUIDE.md ,我们可以继续push cc 迭代。

ui 迭代

1:叫替换一下 header 的颜色为style-guide.md 里面的黄色 
2:添加图表统计功能

经过几轮的迭代,我们得到了初版的效果

步骤5:构建像素级别还原的next app

原生的.html 不方便后续迭代维护,你可以使用下方的prompt 叫CC构建一个next app 开始build 之前可以梳理一下已实现的功能,方便后续迭代

prompt

> Great,now you need to  build a next app from todo-quadrant.html 
- you need to ensure the UI and logic are pixel perfectly restorely 。
- the code structure should be clear enough and The code is highly readable.
- when there is the case that if-else ,your need to use early-return to solve

保存plan.md

最终的next.app 效果

其他扩展

当然啦,有了STYLE_GUIDE.md你还可以拓展更多的实践,比如:

-  在 stitch 生成符合风格 ui 设计稿【还可以加上初版的.html】
- 在lovart 生成符合风格的美术素材
- 基于farme motion 生成产品演示动画
- 生成漂亮的产品的幻灯片(html),用一些工具转为ppt 使用

参考实践


作者:hi大雄
来源:juejin.cn/post/7569777676098814002

收起阅读 »

我用AI重构了一段500行的屎山代码,这是我的Prompt和思考过程

大家好,我来了🙂。我们团队,维护着一个有5年历史的史诗级中后台项目😖。在这座屎山里,有一个叫handleOrderSubmit.js的文件。可以下载瞧一瞧 有多屎👉 handleOrderSubmit.js它是一个长达500多行的React ...
继续阅读 »

image.png

大家好,我来了🙂。

我们团队,维护着一个有5年历史的史诗级中后台项目😖。在这座屎山里,有一个叫handleOrderSubmit.js的文件。

可以下载瞧一瞧 有多屎👉 handleOrderSubmit.js

它是一个长达500多行的React useEffect 钩子函数(是的,你没看错,一个useEffect)。

它混合了订单数据的本地校验价格计算优惠券应用API请求全局状态更新、以及错误弹窗处理... 所有的逻辑,都塞在一个函数里,用if/elsetry/catch层层嵌套。

没人敢动它😖。

本事试试看.gif

每次产品经理提一个小需求,比如在提交订单时,增加一种新的优惠券类型,我们整个团队的表情都像被雷劈了。因为我们知道,改这个函数,要么加班一周,要么就等着P0级事故。

上周,产品经理要求我们在这个函数里,加入一个全新的风控逻辑。

我评估了一下,手动重构,至少需要一个资深工程师一周的时间,而且风险极大。

我受够了。我决定,把这个烫手的任务,扔给我的实习生——AI(我用的是GPT-5 mini,穷😂)。

这篇文章,就是我人机协作,啃下这块硬骨头的完整复盘,大家继续看。


我不能直接说重构它

我犯的第一个错误,是直接把500行代码贴给AI,然后说:帮我重构这段代码

AI很听话,它给我的,是一段看起来更整洁的代码——它把if/else换成了switch,提了几个变量... 这不叫重构,这叫重新排版,毫无意义。

我很快意识到:AI是一个能力超强、但没有灵魂的执行者。我,作为开发者,必须给它提供一个清晰的方案。

于是,我制定了一个五步重构法


我的Prompt和思考

我的核心思想是:AI负责执行,我负责决策。 我要像一个指挥家一样,一步一步地引导AI,把这500行的代码,拆解成高内聚、低耦合的模块。

第一步:先让AI读懂屎山

我不能上来就让AI改。我得先确认,它和我对这段代码的理解,在一个频道上。

Prompt:

你是一个资深的React架构师。请分析下面这段500行的useEffect代码。

  1. 告诉我它做了几件主要的事情?(职责分析)
  2. 找出所有的副作用(比如API请求、localStorage操作、全局状态更新)。
  3. 找出所有的纯逻辑(比如数据校验、价格计算)。
  4. 评价它的可维护性和可测试性

image.png

GPT的回答,给了我一个惊喜。它清晰地列出了:

  1. 职责:它承担了至少5个职责:表单校验、价格计算、库存检查、创建订单、处理UI反馈。
  2. 副作用api.post(...)setLoading(true)showToast(...)...
  3. 纯逻辑validateForm(...)calculateTotalPrice(...)...
  4. 评价:可维护性极低,单一职责原则被严重违反,几乎不可测试。

很好,AI看懂了!我们就可以可以开始了😁。

第二步:先分离纯逻辑代码

这是最关键的一步。我要把计算和动作分开。

Prompt:

非常好。现在,请你只针对纯逻辑部分(表单校验、价格计算)进行重构。

  1. 把这些逻辑,从原函数中提取出来,变成独立的、可导出的纯函数
  2. 这些函数必须是纯的:不能有任何副作用(不能调用apisetLoading)。
  3. 使用TypeScript,为这些新函数的入参和返回值,提供清晰的类型定义。

image.png

AI立刻唰唰唰地给了我几个文件:

validate.ts:

export function validateOrder(formData: OrderForm): string | null {
if (!formData.user) return '用户信息不能为空';
if (formData.items.length === 0) return '商品不能为空';
// ... 10 more rules
return null;
}

calculator.ts:

export function calculateTotalPrice(items: Item[], coupon: Coupon): number {
let total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
if (coupon.type === 'PERCENT') {
total *= (1 - coupon.value / 100);
}
// ... 20 more rules
return total;
}
第三步:让AI为自己的代码做测试

我怎么知道AI提炼的逻辑,是对的还是错的? 答案:让它自己写单元测试来证明。

Prompt:

你现在是一个QA工程师。请使用Vitest,为你刚才提取的validateOrdercalculateTotalPrice这两个函数,编写全面的单元测试用例。 必须覆盖所有边界情况,包括正常、异常、和边缘值。

AI再次给了我惊喜。它生成的测试用例,比我手写的还要全:

// calculator.test.ts
import { describe, it, expect } from 'vitest';
import { calculateTotalPrice } from './calculator';

describe('calculateTotalPrice', () => {
it('should return 0 for empty cart', () => {
expect(calculateTotalPrice([], null)).toBe(0);
});

it('should apply percent coupon', () => {
const items = [{ price: 100, quantity: 1 }];
const coupon = { type: 'PERCENT', value: 10 };
expect(calculateTotalPrice(items, coupon)).toBe(90);
});
// ... 省略更多
});

我把这些测试用例跑了一遍,全部通过。我现在信心大增😁。

第四步:重写协调层

现在,纯逻辑已经被分离并验证了。原来的500行屎山,只剩下副作用和流程控制代码了。是时候重写它了。

Prompt:

干得漂亮。现在,我们来重构那个原始的useEffect函数

  1. 它现在唯一的职责是协调
  2. 调用我们刚才创建的validateOrdercalculateTotalPrice纯函数。
  3. 把所有的副作用(API请求、setLoadingshowToast清晰地编排起来。
  4. 使用async/await,让异步流程更清晰,用try/catch处理错误。

这次,AI给我的,是一个只有30行左右的、清晰的流程代码:

// 重构后的 useEffect
useEffect(() => {
const submitOrder = async () => {
setLoading(true);
try {
// 1. 协调:验证 (纯)
const errorMsg = validateOrder(formData);
if (errorMsg) {
showToast(errorMsg);
return;
}

// 2. 协调:计算 (纯)
const totalPrice = calculateTotalPrice(formData.items, formData.coupon);

// 3. 协调:副作用(不纯)
const result = await api.post('/order/submit', { ...formData, totalPrice });

// 4. 协调:响应(不纯)
if (result.code === 200) {
showToast('订单提交成功!');
router.push('/success');
} else {
showToast(result.message);
}
} catch (err) {
showToast(err.message);
} finally {
setLoading(false);
}
};

if (isSubmitting) { // 假设一个触发条件
submitOrder();
setIsSubmitting(false);
}
}, [isSubmitting, formData /* ...其他依赖 */]);
第五步:最后守卫工作,加入新功能

别忘了,我重构的目的,是为了加风控这个新功能。

Prompt:

最后一步。请在API请求之前,加入一个风控检查的逻辑(调用riskControl.check(...))。这是一个异步函数,如果检查不通过,它会抛出一个错误。

AI在第2步和第3步之间,加了几行代码,完美收工。


这次重构,我总共花了大概5个小时,而不是原计划的一周。

总觉得 AI 不会淘汰会写代码的工程师。

只会降维打击那些只会堆砌代码的工程师。

那段500行的屎山,在过去,是我的噩梦;现在,有了AI的帮助,它变成了我的靶场。

这种感觉,真爽🙌。


作者:ErpanOmer
来源:juejin.cn/post/7570630923710054452
收起阅读 »

我本是写react的,公司让我换赛道搞web3D

web
当你在会议室里争论需求时,智慧工厂的数字孪生正同步着每一条产线的脉搏;当你对着平面图想象空间时,智慧小区的三维模型已在虚拟世界精准复刻每一扇窗的采光。当你在CAD里调整参数时,数字孪生城市的交通流正实时映射每辆车的轨迹;当你等待客户确认方案时,机械臂的3D仿真...
继续阅读 »

当你在会议室里争论需求时,
智慧工厂的数字孪生正同步着每一条产线的脉搏;

当你对着平面图想象空间时,
智慧小区的三维模型已在虚拟世界精准复刻每一扇窗的采光。

当你在CAD里调整参数时,
数字孪生城市的交通流正实时映射每辆车的轨迹;
当你等待客户确认方案时,
机械臂的3D仿真已预演了十万次零误差的运动路径;

当你用二维图纸解释传动原理时,
可交互的3D引擎正让客户‘拆解’每一个齿轮;
当你担心售后维修难描述时,
AR里的动态指引已覆盖所有故障点;

当你用PS拼贴效果图时,
VR漫游的业主正‘推开’你设计的每一扇门;
当你纠结墙面材质时,
光影引擎已算出了午后3点最温柔的折射角度;

从前端到Web3D,
不是换条赛道,
而是打开新维度。

韩老师说过:再牛的程序员都是从小白开始,既然开始了,就全心投入学好技术。

🔴 工具

所有的api都可以通过threejs官网的document,切成中文,去搜:

image.png

🔴 平面

⭕️ Scene 场景

场景能够让你在什么地方什么东西来交给three.js来渲染,这是你放置物体灯光摄像机地方

image.png

import * as THREE from "three";

// console.log(THREE);

// 目标:了解three.js最基本的内容

// 1、创建场景
const scene = new THREE.Scene();

⭕️ camera 相机

示例:threejs.org/examples/?q…

image.png

import * as THREE from "three";

// console.log(THREE);

// 目标:了解three.js最基本的内容

// 1、创建场景
const scene = new THREE.Scene();

// 2、创建相机
const camera = new THREE.PerspectiveCamera(
75, // 相机的角度
window.innerWidth / window.innerHeight, // 相机的宽高比
0.1, // 相机的近截面
1000 // 相机的远截面
);

// 设置相机位置
camera.position.set(0, 0, 10); // 相机位置 (X轴坐标, Y轴坐标, Z轴坐标)
scene.add(camera); // 相机添加到场景中

⭕️ 物体 cube

import * as THREE from "three";

// console.log(THREE);

// 目标:了解three.js最基本的内容

// 1、创建场景
const scene = new THREE.Scene();

// 2、创建相机
const camera = new THREE.PerspectiveCamera(
75, // 相机的角度
window.innerWidth / window.innerHeight, // 相机的宽高比
0.1, // 相机的近截面
1000 // 相机的远截面
);

// 设置相机位置
camera.position.set(0, 0, 10); // 相机位置 (X轴坐标, Y轴坐标, Z轴坐标)
scene.add(camera); // 相机添加到场景中

// 添加物体
// 创建几何体
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1); // 创建立方体的几何体 (长, 宽, 高)
const cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }); // MeshBasicMaterial 基础网格材质 ({ color: 0xffff00 }) 颜色
// 根据几何体和材质创建物体
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial); // 创建立方体的物体 (几何体, 材质)
// 将几何体添加到场景中
scene.add(cube); // 物体添加到场景中

⭕️ 渲染 render

// 初始化渲染器
const renderer = new THREE.WebGLRenderer();
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染的尺寸大小 (窗口宽度, 窗口高度)
// console.log(renderer);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement); // 将webgl渲染的canvas内容添加到body

// 使用渲染器,通过相机将场景渲染进来
renderer.render(scene, camera); // 使用渲染器,通过相机将场景渲染进来 (场景, 相机)

⭕️ 效果

效果是平面的:

image.png

到这里,还不是3d的,如果要加3d,要加一下控制器

🔴 3d

⭕️ 控制器

添加轨道。像卫星☄围绕地球🌏,环绕查看的视角:

// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

// 目标:使用控制器查看3d物体

// // 使用渲染器,通过相机将场景渲染进来
// renderer.render(scene, camera);

// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement); // 创建轨道控制器 (相机, 渲染器dom元素)
controls.enableDamping = true; // 设置控制器阻尼,让控制器更有真实效果。

function render() {
renderer.render(scene, camera); // 浏览器每渲染一帧,就重新渲染一次
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render); // 浏览器渲染下一帧的时候就会执行render函数,执行完会再次调用render函数,形成循环,每秒60次
}

render();

⭕️ 加坐标轴辅助器

// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5); // 坐标轴(size轴的大小)
scene.add(axesHelper);

1.gif

⭕️ 设置物体移动

// 设置相机位置
camera.position.set(0, 0, 10);
scene.add(camera);

1.gif

cube.position.x = 3;
// 往返移动
function render() {
cube.position.x += 0.01;
if (cube.position.x > 5) {
cube.position.x = 0;
}
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}

render();

⭕️ 缩放

cube.scale.set(3, 2, 1); // xyz, x3倍, y2倍

单独设置

cube.position.x = 3;

⭕️ 旋转

cube.rotation.set(Math.PI / 4, 0, 0, "XZY"); // x轴旋转45度

单独设置

cube.rotation.x = Math.PI / 4;

⭕️ requestAnimationFrame

function render(time) {
// console.log(time);
// cube.position.x += 0.01;
// cube.rotation.x += 0.01;

// time 是一个不断递增的数字,代表当前的时间
let t = (time / 1000) % 5; // 为什么求余数,物体移动的距离就是t,物体移动的距离是0-5,所以求余数
cube.position.x = t * 1; // 0-5秒,物体移动0-5距离

// if (cube.position.x > 5) {
// cube.position.x = 0;
// }
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}

render();

⭕️ Clock 跟踪事件处理动画

// 设置时钟
const clock = new THREE.Clock();
function render() {
// 获取时钟运行的总时长
let time = clock.getElapsedTime();
console.log("时钟运行总时长:", time);
// let deltaTime = clock.getDelta();
// console.log("两次获取时间的间隔时间:", deltaTime);
let t = time % 5;
cube.position.x = t * 1;

renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}

render();

大概是8毫秒一次渲染时间.

⭕️ 不用算 用 Gsap动画库

gsap.com/

// 导入动画库
import gsap from "gsap";

// 设置动画
var animate1 = gsap.to(cube.position, {
x: 5,
duration: 5,
ease: "power1.inOut", // 动画属性
// 设置重复的次数,无限次循环-1
repeat: -1,
// 往返运动
yoyo: true,
// delay,延迟2秒运动
delay: 2,
onComplete: () => {
console.log("动画完成");
},
onStart: () => {
console.log("动画开始");
},
});
gsap.to(cube.rotation, { x: 2 * Math.PI, duration: 5, ease: "power1.inOut" });

// 双击停止和恢复运动
window.addEventListener("dblclick", () => {
// console.log(animate1);
if (animate1.isActive()) {
// 暂停
animate1.pause();
} else {
// 恢复
animate1.resume();
}
});

function render() {
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}

render();

⭕️ 根据尺寸变化 实现自适应

// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
// console.log("画面变化了");
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();

// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比
renderer.setPixelRatio(window.devicePixelRatio);
});

⭕️ 用js控制画布 全屏 和 退出全屏

window.addEventListener("dblclick", () => {
const fullScreenElement = document.fullscreenElement;
if (!fullScreenElement) {
// 双击控制屏幕进入全屏,退出全屏
// 让画布对象全屏
renderer.domElement.requestFullscreen();
} else {
// 退出全屏,使用document对象
document.exitFullscreen();
}
// console.log(fullScreenElement);
});

⭕️ 应用 图形 用户界面 更改变量

// 导入dat.gui
import * as dat from "dat.gui";

const gui = new dat.GUI();
gui
.add(cube.position, "x")
.min(0)
.max(5)
.step(0.01)
.name("移动x轴")
.onChange((value) => {
console.log("值被修改:", value);
})
.onFinishChange((value) => {
console.log("完全停下来:", value);
});
//   修改物体的颜色
const params = {
color: "#ffff00",
fn: () => {
// 让立方体运动起来
gsap.to(cube.position, { x: 5, duration: 2, yoyo: true, repeat: -1 });
},
};
gui.addColor(params, "color").onChange((value) => {
console.log("值被修改:", value);
cube.material.color.set(value);
});
// 设置选项框
gui.add(cube, "visible").name("是否显示");

var folder = gui.addFolder("设置立方体");
folder.add(cube.material, "wireframe");
// 设置按钮点击触发某个事件
folder.add(params, "fn").name("立方体运动");

image.png


🔴 结语

前端的世界,
不该只有VueReact——
还有WebGPU里等待你征服的星辰大海。"

“当WebGL成为下一代前端的基础设施,愿你是最早站在三维坐标系里的那个人。”


作者:jack_po
来源:juejin.cn/post/7517209356855164978
收起阅读 »