面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月31日 02:05

Redis 底层为什么这么快?核心数据结构如何支撑?

Redis 底层之所以快,主要靠三件事:数据大多在内存里,命令执行路径足够短,核心数据结构针对常见操作做了大量取舍。它不是简单的“单线程所以快”,而是 SDS、dict、skiplist、quicklist、intset、事件循环、I/O 多路复用和内存分配共同配合的结果。面试里回答这个问题,最好从数据结构讲到网络模型,再落到性能边界。字符串和哈希表是基础Redis 的 String 底层不是直接使用 C 字符串,而是 SDS。SDS 记录了长度和容量,获取长度是 O(1),扩容时有预分配策略,还能安全存储二进制数据。它的取舍是多占一点元数据空间,换来更安全的修改和更少的内存重分配。Hash、数据库 key 空间、过期字典大量依赖 dict。dict 使用哈希表加链地址法解决冲突,并通过两个哈希表做渐进式 rehash。这样不会在扩容时一次性搬完整张表,避免服务长时间卡住。踩坑点是 rehash 期间查询、写入都要同时照顾两张表,理解这一点才能解释为什么 Redis 能边服务边扩容。typedef struct dict { dictht ht[2]; long rehashidx;} dict;List、Set、ZSet 会按规模切换编码Redis 不会一上来就用最通用但最重的数据结构。小集合会用 intset 或紧凑编码节省内存,元素变多或类型变化后再升级。List 在新版本主要基于 quicklist,它把多个紧凑节点串成双向链表,兼顾顺序访问和内存占用。ZSet 常见实现是 dict + skiplist:dict 保证按 member 快速查分数,skiplist 保证按 score 范围查询和排名。跳表不是唯一能做有序结构的方案,平衡树也可以。Redis 选择跳表,是因为实现相对简单,范围查询自然,插入删除平均 O(log n),调试和维护成本低。代价是多层索引会额外占内存,score 相同还要按 member 做字典序比较。单线程命令执行不等于只有一个线程Redis 的核心命令执行长期保持单线程,这能避免复杂锁竞争,也让数据结构操作更可控。但网络 I/O、后台删除、AOF 刷盘、持久化重写等任务可能由其他线程或子进程参与。Redis 6 之后支持 I/O 多线程,主要优化网络读写,命令执行仍以单线程为主。redis-cli INFO memoryredis-cli SLOWLOG GET 10redis-cli --bigkeys -h 127.0.0.1 -p 6379事件循环和过期删除决定稳定性Redis 用事件循环处理文件事件和时间事件。文件事件负责连接读写,时间事件负责定时任务,比如过期 key 抽样删除、统计维护、复制心跳。过期删除不是每个 key 到点立刻删除,而是惰性删除加定期抽样删除。这个设计节省 CPU,但也意味着大量过期 key 可能短时间占住内存,需要合理设置 TTL 分布。追问Redis 为什么用 SDS,不直接用 C 字符串?C 字符串没有长度字段,求长度需要遍历,拼接时还容易写越界。SDS 把长度、剩余容量和字节数组放在一起,常见修改能先检查容量,不够再扩容。它的边界是会增加少量结构体开销,但 Redis 更在意高频操作的稳定延迟。存二进制内容时,SDS 也不会因为中间出现 \0 就截断。渐进式 rehash 解决了什么问题?如果哈希表扩容时一次性迁移所有 key,Redis 会在这一瞬间明显阻塞。渐进式 rehash 把迁移分摊到后续命令和定时任务里,每次搬一小部分。代价是 rehash 期间要查两张表,代码复杂一些,内存也会短暂增加。这个取舍很典型:用一点空间和实现复杂度,换线上延迟更平滑。ZSet 为什么通常用 dict 加 skiplist?dict 让 ZSCORE 这类按成员查分数的操作接近 O(1),skiplist 让按分数范围查询、排名查询保持 O(log n)。如果只用 dict,没法高效排序;如果只用 skiplist,按 member 查找又不够快。踩坑点是 ZSet 的内存成本比普通 Set 高很多,不适合无限堆用户行为明细。排行榜要加时间维度和过期策略,否则迟早变成大 key。Redis 单线程会不会被慢命令拖垮?会,所以生产环境要避免 KEYS、超大集合 SMEMBERS、大 key 删除这类操作。单线程的好处是没有锁竞争,坏处是一个慢命令就可能挡住后面的请求。线上更推荐 SCAN 分批扫描,用 UNLINK 异步删除大 key,并打开慢日志观察异常命令。Redis 很快,但前提是每个命令都足够短。怎么从底层角度排查 Redis 性能问题?先看 INFO memory 判断内存、碎片率和淘汰情况,再看 SLOWLOG 找慢命令。怀疑大 key 时用 --bigkeys 或离线 RDB 分析,怀疑网络瓶颈时看连接数、客户端输出缓冲区和命令 QPS。不要一上来就调线程数,很多问题其实是 key 设计或命令复杂度错了。底层原理的价值就在这里:能把“Redis 慢了”拆成结构、网络、内存和持久化几个方向。
服务端阅读 05月31日 02:05

Redis 常见应用场景有哪些?项目里该怎么选?

Redis 最常见的应用场景不是“把数据放进内存”这么简单,而是用它的低延迟、原子命令和多种数据结构,去补数据库、应用进程和消息系统的短板。项目里常见用法包括缓存、分布式锁、排行榜、计数器、限流、会话存储、轻量消息队列、地理位置和集合关系计算。真正要答好这个问题,重点不是背场景清单,而是说清楚每个场景为什么适合 Redis,以及什么时候不该用。缓存是最高频场景缓存通常用 Cache-Aside 模式:读请求先查 Redis,未命中再查数据库并写回缓存;写请求先更新数据库,再删除缓存。这个模式简单,但边界很多:不存在的数据要防缓存穿透,热点 key 过期要防击穿,大量 key 同时过期要防雪崩。实际项目里我会给 TTL 加随机抖动,并对空结果设置较短缓存时间。User user = redis.get("user:" + id);if (user == null) { user = db.queryById(id); redis.set("user:" + id, user, 3600 + random(300));}return user;分布式锁和限流要看一致性要求分布式锁常用 SET key value NX EX seconds,释放时用 Lua 比较 value 后再删除,避免删掉别人刚拿到的锁。它适合防重复提交、定时任务抢占、库存扣减前置保护,但不适合承诺绝对强一致;如果锁过期时间估短,业务没跑完锁就释放,会出现并发穿透。限流则常用 INCR、Sorted Set 滑动窗口或 Lua 令牌桶,固定窗口最简单,但窗口边界会有突刺。排行榜、计数器和集合计算是数据结构优势排行榜用 Sorted Set,ZADD 写分数,ZREVRANGE 查榜单,ZREVRANK 查名次,比数据库反复排序轻很多。阅读量、点赞数可以用 INCR 做原子计数,再异步落库。共同关注、标签筛选、用户分组适合用 Set 的交集和并集,但集合太大时要注意阻塞风险,线上不要随手对超大 key 做全量运算。ZADD article:rank 1024 article_1001ZREVRANGE article:rank 0 9 WITHSCORESSINTER user:1:follows user:2:follows消息队列和会话存储要谨慎取舍简单异步任务可以用 List 的 LPUSH + BRPOP,Redis Stream 支持消费组和确认机制,更像轻量队列。它的好处是接入快、延迟低;边界是消息堆积、重试治理和跨机房容灾能力不如 Kafka、RocketMQ 这类专业消息系统。Session 存 Redis 很常见,适合多实例共享登录态,但敏感字段要加密或只存引用 ID,不能把 Redis 当成无边界的安全仓库。还有一个常被忽略的点:Redis 不是持久化数据库的替代品,AOF 和 RDB 能降低丢数据概率,却不能让所有缓存场景天然具备事务语义。设计时要先问清楚“丢一小段数据能不能接受”,再决定是否把它放在 Redis 里,这个判断很关键。追问Redis 做缓存时,怎么处理缓存和数据库不一致?常见做法是先更新数据库,再删除缓存,让下一次读取重新加载新值。这个方案牺牲了一点短时间一致性,换来实现简单和故障恢复容易。踩坑点是删除缓存失败会留下旧值,所以生产里通常配合消息重试、binlog 订阅或短 TTL 兜底。如果业务要求读到最新值,比如支付状态,就不要读缓存,直接读主库或走强一致链路。Redis 做分布式锁可靠吗?单 Redis 节点的锁只能解决大多数工程并发问题,不能等同于严格分布式共识。锁 value 必须是唯一 token,释放锁必须用 Lua 原子校验,否则会误删别人的锁。Redisson 的看门狗能降低锁过期风险,但如果进程 STW、网络抖动或 Redis 主从切换,仍要考虑幂等和补偿。重要结论是锁只能减少并发冲突,不能替代业务层一致性设计。Redis Stream 能不能替代 Kafka?低吞吐、少团队协作、希望快速落地的内部任务队列可以用 Redis Stream。它支持消费组、ACK 和消息 ID,比 Pub/Sub 可靠,但在超大吞吐、长期消息保留、多分区扩展和生态工具上不如 Kafka。项目里如果消息是核心链路,优先选专业 MQ;如果只是削峰、异步发通知或刷新缓存,Stream 够用而且维护成本低。热点 key 和大 key 分别怎么处理?热点 key 是访问太集中,可以用本地缓存、多副本读、key 分片或提前预热来分散压力。大 key 是单个 value 或集合过大,会导致网络传输慢、删除阻塞、主从同步卡顿,应该拆分存储并用 UNLINK 异步删除。两个问题经常混在一起,但处理方向不同:热点 key 关注流量,大 key 关注体积。上线前用 redis-cli --bigkeys 和慢日志排查,比故障后临时猜要靠谱。
服务端阅读 05月31日 02:05

Redis 常见问题怎么排查?大 Key、热点和一致性如何处理?

Redis 常见问题不要按概念清单排查,线上通常先看现象:延迟升高、内存暴涨、从库追不上、数据库被打穿、某个接口 QPS 异常。高频原因包括大 Key、热点 Key、慢命令、缓存一致性、分布式锁误用和限流算法选错。Redis 很快,但单个大对象、阻塞命令、网络抖动和错误过期策略,都会把系统拖慢。追问Redis 为什么快,慢请求先看什么?Redis 快在内存访问、单线程命令执行避免锁竞争、I/O 多路复用处理连接,以及高效数据结构。Redis 6 之后网络 I/O 可以多线程,但命令执行仍是核心单线程路径,一个慢命令会拖住后面的请求。排查先看 SLOWLOG GET、INFO commandstats、LATENCY DOCTOR,不要只盯 CPU;很多延迟其实来自阻塞命令、fork、AOF fsync、主从同步或客户端连接池耗尽。大 Key 怎么发现,为什么不能直接 DEL?大 Key 可能是几 MB 的字符串,也可能是几十万元素的 hash、list、set,可以用 redis-cli --bigkeys 抽样扫描,并在业务侧记录 value 大小。生产环境不要随手 KEYS *,它本身就可能阻塞实例。大 Key 的读写、序列化、网络传输和删除都可能卡主线程;删除时优先用 UNLINK 或后台渐进清理,核心集群要放在低峰并评估影响。热点 Key 如何处理,复制多份一定安全吗?热点 Key 是某个 key 被大量并发访问,常见于秒杀库存、首页配置、热门商品详情。只读热点可以用本地缓存、读写分离、客户端缓存,或复制多份 key 随机读取。写热点不能简单复制,否则一致性会很难保证;可累加计数可以分片后异步汇总,强一致库存更适合排队、限流或业务层削峰。缓存和数据库不一致怎么处理?常用 Cache Aside 是读缓存,未命中查数据库再写缓存;写数据时先更新数据库,再删除缓存。它简单可靠,适合大多数最终一致场景,但并发读写下仍可能短暂不一致。延迟双删能降低概率却不好选延迟时间,binlog 或消息队列更稳但链路更复杂;余额、库存扣减这类强一致路径不要依赖缓存读。分布式锁和限流最容易踩哪些坑?Redis 锁至少要用 SET key value NX EX seconds,释放时用 Lua 先比较 value 再删除,避免删掉别人的锁。过期时间太短会导致业务没跑完锁已释放,太长又影响故障恢复,Redisson 看门狗能缓解但不能无视 GC 暂停和网络分区。限流里固定窗口简单但有边界突刺,滑动窗口更平滑但占内存,令牌桶适合允许短时突发;无论哪种都要配合幂等和降级。写段命令和配置redis-cli SLOWLOG GET 10redis-cli --latency -h 127.0.0.1 -p 6379redis-cli LATENCY DOCTORredis-cli --bigkeysredis-cli INFO memoryredis-cli INFO commandstats// 安全释放分布式锁:比较 value 后再删除String lua = "if redis.call('GET', KEYS[1]) == ARGV[1] " + "then return redis.call('DEL', KEYS[1]) else return 0 end";redis.eval(lua, Collections.singletonList(lockKey), Collections.singletonList(lockValue));// Cache Aside:先更新数据库,再删除缓存public void updateUser(User user) { db.update(user); redis.del("user:" + user.getId());}小结Redis 排查要从现象回到机制:慢请求看阻塞和慢命令,内存问题看大 Key 和淘汰策略,流量尖峰看热点 Key,数据异常看缓存一致性链路。解决方案都在性能、一致性、复杂度之间取舍,提前做好 key 规模约束、慢日志监控、过期时间打散和降级预案,比报警后临时救火可靠。
服务端阅读 05月31日 02:05

Yew 框架是什么?适合用 WebAssembly 写前端吗?

Yew 是一个用 Rust 编写前端界面的 WebAssembly 框架,写法接近 React:有组件、属性、状态、回调和虚拟 DOM。它的优势不是“所有页面都更快”,而是把 Rust 的类型系统、所有权检查和计算能力带到浏览器里。适合 Yew 的项目通常有两个特征:团队能接受 Rust 工程成本,页面里确实有复杂计算、强类型约束或 Rust 逻辑复用需求。追问Yew 和 React 最大的区别是什么?两者都用组件化和声明式 UI,但 Yew 的组件逻辑用 Rust 写,最终编译成 Wasm,React 主要运行在 JavaScript 引擎里。Yew 的 props、消息和状态类型更严格,很多错误在编译阶段就能暴露。代价是编译链路更长、调试不如 TypeScript 直观、生态组件更少;如果项目靠成熟 UI 组件快速交付,React 通常更省心。WebAssembly 是否一定让 Yew 应用更快?不一定,Wasm 擅长解析、压缩、图形处理、规则引擎这类计算密集任务,但 UI 还受 DOM、布局、网络和资源体积影响。Yew 仍然要通过浏览器 DOM 呈现页面,列表渲染不合理一样会卡。常见踩坑是只看到 Rust 快,却忽略 Wasm 包首屏加载变慢;如果瓶颈在接口或 DOM,换 Yew 不会自动解决。Yew 的组件模型是怎么工作的?Yew 有函数组件和结构体组件,函数组件配合 hooks 更像现代 React,结构体组件通过 Component trait 暴露生命周期。组件用 Properties 接收外部数据,用 Callback 向外通知事件,内部状态可以用 use_state、use_reducer 或消息更新。边界是小交互优先函数组件,资源清理和生命周期复杂时结构体组件更清楚;不要为了过编译到处 .clone() 大对象。Yew 项目如何创建和运行?常见做法是安装 wasm target 和 trunk,再用 trunk serve 启动开发环境。项目至少需要 Cargo.toml、index.html 和 Rust 入口文件,依赖里开启 yew 的 csr 特性。踩坑点是工具链版本,yew、wasm-bindgen、trunk 和 Rust 版本不匹配时会出现奇怪构建错误,最好把版本写进 README 或 CI。什么项目不建议优先选 Yew?如果团队没有 Rust 经验、周期很短、页面主要是后台表单和普通列表,Yew 往往不是最省成本的选择。它的学习曲线包含所有权、生命周期、Wasm 调试、浏览器 API 绑定和包体积优化。更稳的取舍是先把 Yew 用在局部模块,例如复杂计算面板、可视化编辑器或 Rust 逻辑复用层,而不是直接重写整个业务系统。写段代码rustup target add wasm32-unknown-unknowncargo install trunkcargo new yew-demo# Cargo.toml[dependencies]yew = { version = "0.21", features = ["csr"] }use yew::prelude::*;#[function_component(App)]fn app() -> Html { let count = use_state(|| 0); let onclick = { let count = count.clone(); Callback::from(move |_| count.set(*count + 1)) }; html! { <button {onclick}>{ format!("count = {}", *count) }</button> }}fn main() { yew::Renderer::<App>::new().render();}小结Yew 的价值在于把 Rust 和 WebAssembly 带进前端工程,而不是替代所有 JavaScript 框架。它适合计算重、类型约束强、团队能驾驭 Rust 的场景;不适合只想快速堆 UI 的常规业务页。
服务端阅读 05月31日 02:05

Yew 组件生命周期有哪些方法?各自适合做什么?

Yew 组件生命周期主要对应 Component trait 的几个方法:create 初始化,update 处理消息,changed 响应 props 变化,view 生成虚拟 DOM,rendered 处理真实 DOM 之后的副作用,destroy 清理资源。面试里只背方法名不够,关键要说清楚每个阶段适合放什么、不适合放什么。最容易出问题的是把副作用塞进 view,或者忘记在卸载时释放监听器和定时器。追问create 和 view 分别适合做什么?create 在组件实例创建时调用,适合根据 props 初始化字段、准备默认状态和回调需要的数据。它不适合操作 DOM,因为这时节点还没有挂载,强行查元素通常会拿到空值。view 只应该根据当前 state 和 props 返回 Html,少量格式化可以放进去,但复杂计算最好提前放到 update 或缓存字段里。update 返回 true 和 false 有什么区别?update 收到消息后修改组件状态,返回 true 表示需要重新调用 view,返回 false 表示这次变化不影响 UI。比如计数器加一、接口数据写入 state 应该返回 true,日志上报或空操作可以返回 false。常见踩坑是所有分支都返回 true,小组件看不明显,列表页里会造成大量无意义 diff。changed 什么时候触发,为什么不能滥用?changed 在父组件传入的 props 变化时触发,适合根据新旧 props 判断子组件是否需要同步状态或重渲染。它的价值在于过滤无效更新,例如父组件重渲染但关键 id 没变,子组件可以返回 false。不要把所有 props 都复制成内部 state,否则很容易出现两份数据不同步;只有表单草稿、动画状态这类本地状态才值得复制。为什么 DOM 操作要放在 rendered?rendered 在真实 DOM 更新后调用,适合读取节点尺寸、聚焦输入框,或初始化依赖节点的第三方 JS 库。first_render 能区分首次渲染,图表实例、全局监听这类只应初始化一次的逻辑必须挡住重复执行。踩坑点是每次渲染都重新挂库,最后同一个 canvas 多个实例、多个事件监听一起存在,页面会越来越慢。destroy 主要清理哪些资源?destroy 在组件卸载时调用,适合取消定时器、释放 JS 事件监听、关闭 WebSocket、丢弃订阅句柄。Rust 会释放普通字段,但浏览器侧事件和外部订阅不一定按你想的时机断开。边界是一次性请求通常不用专门处理,长连接、轮询、全局事件和第三方库实例必须有明确退出路径。写段代码use yew::prelude::*;pub enum Msg { Inc, Loaded(String), Noop }pub struct Counter { count: i32, text: String }impl Component for Counter { type Message = Msg; type Properties = (); fn create(_ctx: &Context<Self>) -> Self { Self { count: 0, text: String::new() } } fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool { match msg { Msg::Inc => { self.count += 1; true } Msg::Loaded(v) => { self.text = v; true } Msg::Noop => false, } } fn view(&self, ctx: &Context<Self>) -> Html { html! { <button onclick={ctx.link().callback(|_| Msg::Inc)}>{ self.count }</button> } } fn rendered(&mut self, _ctx: &Context<Self>, first_render: bool) { if first_render { /* 初始化依赖 DOM 的逻辑 */ } } fn destroy(&mut self, _ctx: &Context<Self>) { /* 清理资源 */ }}小结Yew 生命周期的核心是把事情放到正确阶段:初始化在 create,状态变更在 update,属性同步在 changed,展示在 view,DOM 副作用在 rendered,资源释放在 destroy。真正写项目时,update 的返回值、rendered 的重复执行和 destroy 的清理边界,比方法定义本身更容易踩坑。
服务端阅读 05月31日 01:21

Yew 应用性能优化应该从哪些地方入手?

Yew 的性能优化不要一上来就盯着 WebAssembly。很多 Yew 应用变慢,不是 Rust 算不动,而是组件重复渲染、状态放得太高、列表没有 key、Wasm 包太大、JS 和 Wasm 之间传了太多数据。先用浏览器 Performance 面板和日志确认瓶颈,再决定优化手段,通常比凭感觉改代码有效得多。一个实用顺序是:先减少不必要渲染,再优化列表和状态,再看包体积,最后处理 JS/Wasm 边界和网络请求。过早优化会让组件变复杂,尤其是到处加 memo、callback 和手写比较逻辑,后面维护成本会很高。Yew 性能问题通常出在哪里?组件渲染是最常见的入口。父组件状态变化时,子组件可能跟着重新计算和重新生成虚拟 DOM。如果 props 很大,或者列表项很多,这个成本会被放大。函数组件里可以用 use_memo 缓存昂贵计算,用 use_callback 稳定回调引用,但前提是你真的遇到了重复计算。#[function_component(TotalPrice)]fn total_price(props: &CartProps) -> Html { let total = use_memo(props.items.clone(), |items| { items.iter().map(|i| i.price * i.count).sum::<u32>() }); html! { <span>{ format!("total: {}", *total) }</span> }}列表渲染要特别注意 key。没有稳定 key 时,列表插入、删除和重排会让框架更难复用已有节点。key 不要用数组下标,尤其是可排序、可删除列表,否则 UI 状态可能错位。html! { <ul> { for props.items.iter().map(|item| html! { <li key={item.id}>{ &item.title }</li> }) } </ul>}Yew 性能优化最怕“看起来高级”。稳定 key、合理状态位置、少传大 props、控制包体积,这些基础动作往往比复杂技巧更有效。先定位,再优化,最后用指标确认收益,才不会把代码越改越难维护。追问use_memo 和 use_callback 是不是应该到处加?不应该。它们本身也有依赖比较和额外心智成本,只有计算确实昂贵、回调引用确实导致子组件重复渲染时才值得加。简单字符串拼接、很小的列表过滤,直接计算通常更清楚。踩坑点是依赖写错:少写依赖会拿到旧值,多写依赖又等于每次重建。优化前最好先用日志或 Performance 面板确认问题存在。类组件里的 should_render 该怎么用?should_render 适合状态变化频繁但视图不一定需要更新的类组件。比如内部计数、节流状态、缓存字段变化时,可以返回 false 避免重绘。取舍是逻辑会更绕,状态和视图之间的关系必须非常清楚。常见坑是忘记某个字段会影响 UI,结果页面不刷新,看起来像状态丢了。大列表在 Yew 里怎么优化?先加稳定 key,再考虑分页、搜索节流和虚拟滚动。几百条以内通常不用急着上虚拟列表,代码复杂度不一定划算;上千条并且每项结构复杂时,虚拟滚动才明显。边界是可访问性和交互细节,虚拟滚动可能影响浏览器查找、焦点管理和滚动恢复。真实项目里要同时测首屏时间、滚动流畅度和用户操作是否丢状态。Wasm 包体积应该怎么压?发布构建至少要开启 release 优化、LTO、strip,并检查是否引入了过重依赖。opt-level = "z" 偏向包体积,opt-level = 3 偏向运行速度,具体选哪个要看应用瓶颈。踩坑点是为了省几十 KB 牺牲了热路径性能,或者引入一个 crate 只用一个小函数却带来大量依赖。可以用 twiggy 或构建报告看体积来源,而不是盲删。[profile.release]opt-level = "z"lto = truecodegen-units = 1panic = "abort"strip = trueJS 和 Wasm 交互会成为瓶颈吗?会,尤其是频繁传大对象、数组或字符串时。Wasm 计算快不代表跨边界调用免费,序列化、拷贝和绑定层都会消耗时间。优化思路是减少调用次数,批量传输数据,或者把连续计算留在同一侧完成。边界是不要为了减少跨边界调用把所有逻辑都塞进 Wasm,因为 DOM 操作和浏览器 API 仍然天然在 JS/浏览器侧更合适。
服务端阅读 05月31日 01:21

Yew 应用应该如何设计测试策略?

Yew 应用的测试要分两层看:纯 Rust 逻辑尽量放在普通单元测试里跑,涉及组件渲染、浏览器 API、事件交互的部分再放到 Wasm 或端到端测试里。这样做的原因很现实:浏览器测试慢、依赖环境多,而业务函数、状态 reducer、数据转换逻辑其实不需要浏览器。比较稳的策略是“底层多测、上层少测、关键路径必测”。底层用 cargo test 覆盖解析、状态流转和工具函数;组件层用 wasm-bindgen-test 或 SSR 渲染验证输出;真正依赖点击、输入、路由和网络的流程交给 Playwright 这类 E2E 工具。Yew 测试从哪里开始?先把可测试逻辑从组件里拆出来。比如表单校验、接口响应转换、列表过滤,不要全部塞进 html! 宏附近。拆出来之后,普通 Rust 测试就能覆盖大部分分支,速度快,也不需要启动浏览器。pub fn normalize_name(input: &str) -> Option<String> { let name = input.trim(); if name.is_empty() { None } else { Some(name.to_string()) }}#[test]fn trims_user_name() { assert_eq!(normalize_name(" Alice "), Some("Alice".into())); assert_eq!(normalize_name(" "), None);}涉及组件渲染时,可以用 wasm-bindgen-test 在真实浏览器或 headless 环境里跑。配置通常放在测试文件顶部,CI 里再指定浏览器。use wasm_bindgen_test::*;wasm_bindgen_test_configure!(run_in_browser);#[wasm_bindgen_test]fn browser_env_is_ready() { assert!(web_sys::window().is_some());}Yew 测试的关键不是工具越多越好,而是把不同风险放到合适层级。普通 Rust 测试兜住逻辑,Wasm 测试兜住浏览器边界,E2E 测试兜住主流程,这样速度和信心比较平衡。追问单元测试和组件测试的边界怎么划?只要逻辑不依赖 DOM,就优先写普通单元测试。比如 reducer、权限判断、价格格式化、API DTO 转换,都应该从 Yew 组件里抽出去。组件测试只验证组件是否把状态、属性和事件正确接起来,不要用它覆盖所有业务分支。边界划清以后,测试会更快,也更不容易因为 UI 文案改动而大面积失败。wasm-bindgen-test 适合测什么?它适合测试必须运行在 Wasm 或浏览器环境里的代码,比如 web_sys、定时器、LocalStorage、组件挂载后的副作用。它不适合承担全部测试,因为启动浏览器成本高,失败日志也比普通 Rust 测试难读。取舍上,可以把它放在“少而关键”的位置。常见踩坑是本地能跑,CI 里缺浏览器或驱动,最后需要在 workflow 里显式安装 Firefox 或 Chrome。异步请求和 Mock 应该怎么做?最好把请求层封装成 trait 或独立函数,让组件只依赖抽象结果,而不是直接在组件里到处调用 gloo_net::http::Request。单元测试里用 fake client 返回固定数据,E2E 再连测试环境或 mock server。这样能避免网络抖动污染组件测试结果。边界是不要 mock 得太过头,如果连序列化字段名都绕过了,线上接口变更时测试也发现不了。#[derive(Clone, PartialEq)]pub enum LoadState<T> { Idle, Loading, Ready(T), Failed(String) }pub fn user_label(state: &LoadState<String>) -> String { match state { LoadState::Ready(name) => format!("User: {name}"), LoadState::Failed(e) => format!("Error: {e}"), _ => "Loading".into(), }}Yew 项目需要端到端测试吗?需要,但不要太多。E2E 测试适合覆盖登录、核心表单提交、关键路由跳转、支付或发布这类主链路。它的价值是发现组件之间、路由、网络和浏览器行为组合后的问题。代价是慢、脆、维护成本高,所以不适合把每个按钮都写成 E2E。比较好的边界是:失败会直接影响业务闭环的流程,才放进 E2E。CI 里测试 Yew 有哪些配置坑?最常见的是工具链没装全,Rust、wasm target、wasm-pack、浏览器缺一个都会失败。其次是缓存配置不当,target 和 cargo registry 没缓存会让 CI 非常慢。另一个坑是把所有测试塞进同一个 job,导致普通单测也被浏览器环境拖慢。可以拆成两个 job:一个跑 cargo test,一个跑 Wasm/browser 测试。- uses: actions-rs/toolchain@v1 with: toolchain: stable target: wasm32-unknown-unknown- run: cargo test- run: cargo install wasm-pack- run: wasm-pack test --headless --firefox
服务端阅读 05月31日 01:21

Scrapy 框架的核心组件是如何协同工作的?

Scrapy 是 Python 生态里的爬虫框架,解决“怎么稳定抓取一批页面”。如果只是抓一两个静态页面,requests + BeautifulSoup 足够;一旦涉及队列、并发、重试和导出,Scrapy 的价值就明显了。核心链路可以简化成一句话:Spider 产生请求,Scheduler 排队,Downloader 下载页面,Spider 解析响应,Item Pipeline 处理数据,Engine 调度全局。理解链路比背组件名更重要,因为很多问题都发生在边界上。Scrapy 的核心组件怎么分工?Scrapy Engine 是中控层,负责让请求、响应和数据在各组件之间流转。Scheduler 保存待抓取请求,并按去重规则决定哪些请求入队。Downloader 发起网络请求,处理代理、超时、编码和响应返回。Spider 只关心两件事:从响应里提取数据,以及继续生成新的请求。Item Pipeline 则放在数据出口,适合做清洗、校验、去重、入库和导出。一个最小 Spider 大概是这样:import scrapyclass ProductSpider(scrapy.Spider): name = "product" start_urls = ["https://example.com/products"] def parse(self, response): for card in response.css(".product-card"): yield { "name": card.css(".name::text").get(), "price": card.css(".price::text").get(), } next_url = response.css("a.next::attr(href)").get() if next_url: yield response.follow(next_url, callback=self.parse)这段代码只写了 Spider,但调度器、下载器、去重器和管道都已参与工作。Scrapy 的好处就在这里:业务写解析,工程化交给框架。追问Scrapy 和 requests + BeautifulSoup 该怎么选?如果页面数量少、抓取频率低、没有复杂队列,requests + BeautifulSoup 更轻,调试也直观。Scrapy 适合页面规模大、链路长、需要失败重试和持续运行的场景。取舍点主要是工程成本:Scrapy 起步配置多一点,但后期维护更稳。踩坑最多的是“小任务硬上 Scrapy”,最后项目结构比业务还复杂。Scheduler、Downloader 和 Spider 的边界在哪里?Scheduler 不应该关心页面内容,它只负责请求排队和去重。Downloader 不应该写业务解析逻辑,它只负责拿到响应并处理网络层问题。Spider 才是解析 HTML、生成 Item 和下一批 Request 的地方。边界混乱会导致代码难测,比如把字段清洗写进下载中间件,后面换站点时很难复用。Downloader Middleware 和 Item Pipeline 有什么区别?Downloader Middleware 处理的是请求和响应,常见用途是加代理、换 User-Agent、处理 Cookie、识别封禁响应。Item Pipeline 处理的是已经解析出来的数据,适合字段规范化、去重、入库和异常数据丢弃。一个简单判断是:还没拿到页面内容,用中间件;已经拿到结构化字段,用 Pipeline。不要把数据库写入放到中间件里,否则请求失败和数据失败会混在一起。Scrapy 的异步并发是不是越大越好?不是。Scrapy 基于 Twisted 事件循环,可以高并发抓取,但并发过高会带来封禁、超时、数据重复和本机资源耗尽。生产里通常要配合 CONCURRENT_REQUESTS、DOWNLOAD_DELAY、AUTOTHROTTLE_ENABLED 一起调。边界是目标站点的承受能力和反爬策略,不是你的机器能开多少连接。比较稳的做法是先小并发跑通,再根据错误率和响应时间逐步增加。# settings.pyCONCURRENT_REQUESTS = 16DOWNLOAD_DELAY = 0.5AUTOTHROTTLE_ENABLED = TrueAUTOTHROTTLE_TARGET_CONCURRENCY = 4.0RETRY_TIMES = 3ROBOTSTXT_OBEY = TrueScrapy 项目上线后最容易踩什么坑?第一类是选择器太脆,页面结构一改就抓不到字段,所以关键字段要做空值校验和告警。第二类是去重规则没想清楚,带时间戳或追踪参数的 URL 会造成重复抓取。第三类是 Pipeline 入库没有幂等,爬虫重跑后数据重复。Scrapy 提供框架能力,但“抓取是否合法、数据是否可靠、失败是否可恢复”仍要靠项目设计兜底。
服务端阅读 05月31日 01:21

Yew 状态管理怎么选,use_state、reducer 和全局状态各适合什么场景?

Yew 状态管理最容易走两个极端:要么所有东西都塞进组件里的 use_state,要么一上来就找全局状态库。前者前期快,后期回调互相影响;后者结构看似专业,却可能让一个简单页面背上过重的抽象。更稳的思路是先看状态的“作用范围”:只影响一个组件、影响一组组件,还是影响整个应用。本地状态:先从最小范围开始组件内部状态适合输入框、弹窗开关、当前 tab、局部加载状态这类只在当前组件使用的值。函数组件里通常用 use_state,类组件里可以放在 struct 字段中。它的优点是直接、可读、改动小;边界是当多个兄弟组件都需要同一份数据时,继续往下传回调会很快变乱。use yew::prelude::*;#[function_component(FilterBox)]fn filter_box() -> Html { let keyword = use_state(String::new); let oninput = { let keyword = keyword.clone(); Callback::from(move |e: InputEvent| { let input: web_sys::HtmlInputElement = e.target_unchecked_into(); keyword.set(input.value()); }) }; html! { <input value={(*keyword).clone()} {oninput} /> }}reducer:把复杂变化收拢到一个地方当状态字段变多,或者同一个动作会同时影响多个字段,use_reducer 更合适。它让每次状态变化都通过 action 表达,适合表单向导、购物车、筛选条件、异步请求状态。缺点是样板代码更多,小组件用它会显得啰嗦。#[derive(Clone, PartialEq)]struct TodoState { items: Vec<String>, loading: bool,}enum TodoAction { Add(String), SetLoading(bool),}impl Reducible for TodoState { type Action = TodoAction; fn reduce(self: std::rc::Rc<Self>, action: Self::Action) -> std::rc::Rc<Self> { let mut next = (*self).clone(); match action { TodoAction::Add(text) => next.items.push(text), TodoAction::SetLoading(v) => next.loading = v, } next.into() }}Context 和全局状态:别太早上跨多层组件共享状态时,可以用 Context Provider,把状态放在上层,再由子组件读取。它适合主题、登录用户、语言、权限、全局配置这类应用级信息。踩坑点是把频繁变化的大对象放进全局 Context,导致很多组件跟着重渲染。全局状态不是仓库,越往里面塞,边界越模糊。type AppState = UseReducerHandle<UserState>;#[function_component(App)]fn app() -> Html { let state = use_reducer(|| UserState { name: "Guest".into(), logged_in: false, }); html! { <ContextProvider<AppState> context={state}> <MainPage /> </ContextProvider<AppState>> }}如果状态涉及服务端缓存、分页列表、乐观更新和失败重试,单靠组件状态会很吃力。可以自己封装请求 Hook,也可以引入社区状态方案,但要先确认维护状态和项目兼容性。Yew 生态还不如 React 丰富,选库时要看版本活跃度、文档和与当前 Yew 版本的匹配情况。追问什么时候不用全局状态?只被一个组件或一小段组件树使用的状态,不该放全局。比如弹窗开关、临时输入、局部排序方式,放近一点更容易理解。全局状态的成本是隐式依赖变多,任何组件都可能读写它。项目里常见的坑是“为了方便”把所有状态放进 Context,最后没人知道某个字段在哪里被改了。use_state 多了就一定要换 reducer 吗?不一定,数量不是唯一标准。真正的信号是多个状态必须一起更新,或者更新规则已经开始重复。比如 loading、data、error 三个字段围绕一次请求变化,用 reducer 会更清晰。反过来,三个互不相关的小开关继续用 use_state 没问题,强行合并只会增加阅读成本。Context 会不会导致性能问题?会,但不是用了就慢,而是值变化太频繁、范围太大时才明显。Provider 的 context 值变化后,消费者组件会重新响应,若里面挂了大列表或复杂子树,体验就可能受影响。取舍上,可以把低频状态和高频状态拆成不同 Context,或者把频繁变化的局部状态留在组件内。性能问题最好用实际 profiling 看,不要凭感觉过早优化。异步请求状态该放哪里?如果请求只服务当前组件,放本地 Hook 就够了,比如 loading/data/error 一起维护。多个页面共享同一份服务端数据时,可以考虑上提到 Context 或封装缓存层。边界是服务端状态和客户端 UI 状态不要混在一起,前者有过期、刷新、失败重试,后者更多是界面临时行为。很多 bug 来自把接口返回当成永远正确的全局真相,结果切换用户或参数后仍显示旧数据。状态管理库值得引入吗?值得与否取决于项目复杂度,而不是技术偏好。小项目引库会增加学习和升级成本,大项目不用库又可能让状态流散在各处。选库时要看它是否支持当前 Yew 版本、是否有活跃维护、是否方便测试。最怕的是为了“架构完整”引入库,却没有统一使用规范,最后同时存在本地状态、Context 和第三方 store 三套写法。小结Yew 状态管理可以按范围逐级选择:组件内用 use_state,复杂局部逻辑用 use_reducer,跨层共享再用 Context,全应用级状态和服务端缓存再考虑专门方案。不要太早抽象,也不要等到回调和 props 传递失控才整理边界。状态放在哪里,决定了后面调试问题时要翻多少文件。
服务端阅读 05月31日 01:21

Yew 和 React 有什么区别,什么项目更适合选 Yew?

Yew 和 React 都能写组件化前端,但它们解决问题的方式差得很远。React 站在 JavaScript/TypeScript 生态上,追求开发效率、生态完整和团队协作成本可控;Yew 站在 Rust 和 WebAssembly 上,更看重类型安全、内存安全以及复用 Rust 代码的能力。选哪个不是“谁更先进”的问题,而是你的项目到底在为哪种成本买单。核心差异在哪里?React 的运行环境是 JavaScript,组件写法、构建工具、调试体验都非常成熟。Yew 编译到 WebAssembly,组件宏和 html! 语法接近 JSX,但状态、事件和生命周期都受 Rust 所有权系统约束。React 项目里常见的问题是运行时才暴露的类型或状态错误;Yew 项目里常见的问题是编译期和生命周期模型更难上手。| 维度 | React | Yew || --- | --- | --- || 语言 | JavaScript/TypeScript | Rust || 运行方式 | JS 引擎执行 | 编译为 Wasm || 生态 | 极成熟 | 仍偏小众 || 上手成本 | 较低 | 较高 || 优势场景 | 通用 Web 应用 | Rust/Wasm 结合场景 |同一个计数器,React 写起来更短,Yew 写起来更啰嗦但类型边界更硬。Yew 的啰嗦不是单纯缺点,它把“谁拥有状态、谁能修改状态”提前说清楚。问题是,如果团队里大多数人不熟 Rust,这些好处会先表现为开发速度下降。use yew::prelude::*;#[function_component(Counter)]fn counter() -> Html { let count = use_state(|| 0); let onclick = { let count = count.clone(); Callback::from(move |_| count.set(*count + 1)) }; html! { <button {onclick}>{ *count }</button> }}function Counter() { const [count, setCount] = React.useState(0); return <button onClick={() => setCount(count + 1)}>{count}</button>;}性能是不是 Yew 一定赢?不一定。WebAssembly 在计算密集型任务上有优势,比如解析、加密、图形计算、复杂数据处理。普通表单、后台管理、内容站这类应用,瓶颈更多在网络、DOM 更新和业务状态组织,React 未必比 Yew 慢。Yew 的 Wasm 包体积、加载成本、JS 互操作成本也要算进去。这也是很多选型误区的来源:看到 Rust 和 Wasm 就默认“更快”。如果页面主要是按钮、列表和接口请求,React 的生态和调试工具可能更值钱。如果你已经有 Rust 核心逻辑,希望前端直接复用,Yew 的吸引力才会明显增加。还有一个容易忽略的点是调试链路。React 的性能问题可以用浏览器插件、Profiler 和成熟监控快速定位;Yew 涉及 Rust、Wasm、JS glue code 和浏览器 API,排查时经常要跨几层看。性能收益如果不能被团队稳定验证和维护,就很容易变成纸面优势。追问什么项目更适合选 Yew?最适合的是 Rust 已经是核心技术栈的项目,比如需要在前端复用 Rust 模型、校验规则或计算逻辑。还有一些对类型安全要求高、业务逻辑复杂但 UI 形态相对稳定的内部工具,也可以考虑。边界是:如果项目高度依赖现成 UI 组件、图表库、低代码生态,Yew 会让你少很多捷径。选 Yew 前最好先做一个包含路由、表单、请求和构建部署的原型,而不是只写计数器。React 的生态优势具体体现在哪?React 有成熟的 UI 库、状态管理、表单方案、测试工具、DevTools 和大量团队经验。遇到问题时,你很容易搜到答案,也容易招到能接手的人。Yew 的生态在进步,但很多时候你要读源码、查 web_sys,甚至自己补封装。取舍上,React 买的是确定性和速度,Yew 买的是 Rust/Wasm 带来的长期一致性。Yew 能不能替代 React 做所有前端?理论上能做很多 Web UI,现实里不建议这么理解。前端工程不只有渲染组件,还包括设计系统、可访问性、埋点、国际化、性能监控和各种第三方 SDK。React 在这些方面的基础设施更完整。Yew 更适合“有明确 Rust/Wasm 收益”的项目,而不是为了尝鲜替换所有 React 页面。团队学习成本怎么评估?如果团队已有 Rust 经验,Yew 的学习曲线主要在 Web API 和组件模型上。若团队主要是前端工程师,Rust 所有权、生命周期、宏和编译错误会成为真实成本。踩坑点是管理者只比较语言性能,不计算招聘、培训和排障时间。一次合理评估应该包括首屏构建、常用组件开发、CI 缓存、错误定位和新人接手速度。Yew 和 React 可以混用吗?可以,但要谨慎。常见方式是把 Rust/Wasm 逻辑作为独立模块给 React 调用,或者在局部页面里引入 Yew 应用。边界在状态同步和构建链路:两个框架各自管理 DOM 和状态时,交界面越大越容易出问题。更稳的方案是让一个框架负责 UI,另一个只提供明确边界的能力。小结React 更像默认答案,适合大多数追求交付效率和生态稳定的 Web 项目。Yew 更像特定条件下的强选择:团队懂 Rust,项目需要 Wasm,或者前端要复用已有 Rust 逻辑。真正的选型不是比较口号,而是把性能、生态、团队和维护成本放在同一张账单里算。
服务端阅读 05月31日 01:21

Yew 事件处理怎么写,什么时候该用 target_unchecked_into?

Yew 的事件处理看起来像 JSX:在元素上写 onclick、oninput、onsubmit,传一个 Callback。真正的差异在 Rust 类型系统里:每个事件都有明确类型,事件目标也需要显式转换。好处是很多错误能在编译期被挡住,代价是输入框、表单和键盘事件会比 JavaScript 写法啰嗦一些。基本事件怎么写?最简单的点击事件只需要 Callback::from。如果回调里要更新状态,就 clone 一份状态句柄进闭包;如果不更新状态,只做日志或发送消息,闭包可以很短。事件类型可以让编译器推断,但复杂场景建议写出来,后面维护的人更容易判断你处理的是鼠标、键盘还是输入事件。use yew::prelude::*;#[function_component(ClickDemo)]fn click_demo() -> Html { let count = use_state(|| 0); let onclick = { let count = count.clone(); Callback::from(move |_e: MouseEvent| count.set(*count + 1)) }; html! { <button {onclick}>{ format!("点击 {} 次", *count) }</button> }}输入事件通常要从事件目标里取 DOM 元素。Yew 常见写法是 target_unchecked_into::<HtmlInputElement>(),意思是告诉编译器“我确认这个事件来自 input”。它方便,但不是魔法;如果回调被挂到了错误元素上,运行时就可能出问题。更稳妥的做法是让回调和元素靠近,别把一个输入回调到处复用。use web_sys::HtmlInputElement;use yew::prelude::*;#[function_component(NameInput)]fn name_input() -> Html { let name = use_state(String::new); let oninput = { let name = name.clone(); Callback::from(move |e: InputEvent| { let input: HtmlInputElement = e.target_unchecked_into(); name.set(input.value()); }) }; html! { <> <input value={(*name).clone()} {oninput} /> <p>{ format!("当前输入:{}", *name) }</p> </> }}表单事件要特别注意默认行为。浏览器会在 submit 后刷新页面,Yew 单页应用里通常需要 prevent_default()。如果你忘了这一步,看起来像状态丢了,其实是页面被重新加载了。let onsubmit = Callback::from(move |e: SubmitEvent| { e.prevent_default(); // 校验表单并发送请求});支持哪些事件?Yew 基本覆盖浏览器常用事件:鼠标事件、键盘事件、输入事件、表单事件、焦点事件、拖拽事件、触摸事件等。对应类型大多来自 web_sys,比如 MouseEvent、KeyboardEvent、InputEvent、SubmitEvent。如果需要底层浏览器能力,通常也是通过 web_sys 继续往下拿。边界在于:不是所有 Web API 都有非常顺滑的封装,有些场景要自己处理类型转换和 feature 开关。事件回调还要考虑冒泡和默认行为。比如按钮放在表单里,点击按钮可能触发表单提交;子元素和父元素都绑定点击时,父元素也会收到事件。需要阻止时可以调用 stop_propagation() 或 prevent_default(),但不要把它们当成万能保险。过度阻止事件会破坏键盘操作、可访问性和浏览器原生交互。追问target_unchecked_into 安全吗?它是方便但带前提的写法,前提是事件目标一定是你声明的 DOM 类型。把它放在 <input> 的 oninput 上通常没问题,但如果回调被复用到 <textarea> 或外层 <div>,假设就变了。取舍是:为了简洁可以用它,但要让回调和元素绑定关系清晰。对公共组件库来说,最好多做一层封装或检查,不要把 unchecked 转换暴露给使用者。事件回调里为什么经常要 clone 状态?因为 Rust 闭包需要拥有它捕获的值,而组件渲染函数本身还要继续使用状态句柄。clone 句柄不是复制完整状态,通常成本很低。踩坑点是新手会试图直接 move 原变量,结果后面的 html! 又要读它,编译器就报所有权错误。理解这一点后,clone 反而是更清楚的写法:一份给回调,一份留给视图。oninput 和 onchange 应该选哪个?oninput 在用户每次输入时触发,适合实时搜索、字符计数、即时校验。onchange 更接近“值提交变化”,常用于选择框、失焦后再处理的输入。取舍取决于你是否需要实时反馈:实时反馈体验好,但可能带来频繁请求或频繁渲染。做搜索框时通常会搭配 debounce,否则输入一个词就打出多次请求。如何处理键盘快捷键?组件内快捷键可以用 onkeydown 绑定到具体输入区域,全局快捷键则要通过 use_effect_with 给 window 或 document 注册监听。边界是全局监听必须清理,否则组件卸载后仍然响应按键。另一个坑是输入框聚焦时快捷键可能误触,比如用户打字按下 / 却触发搜索面板。实际项目里要判断事件目标,必要时避开 input、textarea 和 contenteditable。事件处理适合写很多业务逻辑吗?不适合。事件回调最好只做取值、阻止默认行为、派发动作这几件事,复杂业务放进 reducer、service 或异步函数里。否则一个 onclick 里混着校验、请求、状态更新和错误提示,后面很难测试。Yew 的类型系统能帮你挡住类型错误,但挡不住业务逻辑变成一团。小结Yew 事件处理的关键不是记住所有事件名,而是把事件类型、目标转换和状态更新边界写清楚。普通交互用 Callback 足够,表单记得阻止默认提交,输入事件谨慎使用 target_unchecked_into。当事件回调开始变长,就说明业务逻辑该从视图层抽走了。
服务端阅读 05月31日 01:21

Yew Hooks 怎么用,use_state 和 use_effect 该如何取舍?

Yew 的 Hooks 不是把 React API 换成 Rust 语法那么简单。它解决的是函数组件里状态、生命周期和副作用怎么组织的问题,但同时也把 Rust 的所有权、闭包捕获和依赖比较带进了前端开发。写得顺时,组件会很短;写得随意时,最常见的坑是闭包拿到旧值、依赖写错导致重复请求,或者把本该抽出去的业务状态塞进一个组件里。常用 Hook 该怎么理解?use_state 适合保存简单值,比如输入框文本、开关状态、当前页码。它返回的 UseStateHandle<T> 可以 clone 到事件闭包里,读取时用 *handle 解引用,更新时用 set。如果下一个值依赖旧值,简单计数器可以直接写,但复杂逻辑最好别把计算散落在多个回调中。use yew::prelude::*;#[function_component(Counter)]fn counter() -> Html { let count = use_state(|| 0); let onclick = { let count = count.clone(); Callback::from(move |_| count.set(*count + 1)) }; html! { <button {onclick}>{ format!("count = {}", *count) }</button> }}use_effect_with 更适合处理副作用,比如订阅事件、请求接口、同步浏览器标题。它的关键不是“能不能执行”,而是依赖值什么时候变化。依赖放得太宽,会让请求频繁触发;依赖放得太窄,又会读到过期参数。需要清理资源时,返回的析构闭包一定要写,否则定时器、监听器和 WebSocket 这类资源会悄悄累积。#[function_component(PageTitle)]fn page_title() -> Html { let title = use_state(|| "Yew".to_string()); { let title = title.clone(); use_effect_with((*title).clone(), move |title| { gloo::utils::document().set_title(title); || () }); } html! { <input value={(*title).clone()} /> }}use_reducer 适合状态字段多、更新规则明确的场景,例如表单编辑、购物车、步骤流。它把“状态如何变化”集中到 reducer 里,代码会比多个 use_state 稍重,但调试时更容易定位。边界是:如果只是一个布尔值或一个字符串,强行上 reducer 会让组件显得笨重。自定义 Hook 什么时候值得写?当同一段状态和副作用逻辑在两个以上组件里重复出现,就可以考虑自定义 Hook。比如本地存储同步、分页请求、窗口尺寸监听,都适合封装成 use_xxx。但自定义 Hook 不是工具函数,它内部也要遵守 Hook 调用顺序,不能放在条件分支里调用。#[hook]fn use_toggle(default: bool) -> (UseStateHandle<bool>, Callback<MouseEvent>) { let value = use_state(|| default); let toggle = { let value = value.clone(); Callback::from(move |_| value.set(!*value)) }; (value, toggle)}追问usestate 和 usereducer 该怎么取舍?use_state 的优势是轻,适合值少、更新简单的组件状态。use_reducer 的优势是把状态变化变成显式动作,适合多个字段互相影响的场景。踩坑点是很多人一开始全用 use_state,后来表单校验、异步加载和错误状态混在一起,回调里到处都是重复逻辑。经验上,只要你开始给同一个状态写三四个不同更新分支,就该考虑 reducer。useeffectwith 的依赖为什么容易写错?依赖值决定副作用什么时候重新执行,这和 Rust 闭包捕获一起看才准确。把整个结构体作为依赖,可能因为字段变化太多导致重复请求;只放一个 id,又可能漏掉筛选条件。边界是副作用内部用到但不希望触发重跑的值,通常要重新设计数据流,而不是随手 clone。最安全的做法是让依赖尽量小,并在代码审查时逐项对照副作用里真正使用的变量。Yew Hook 可以放在 if 语句里吗?不建议,也通常不该这么写。Hook 依赖固定调用顺序保存内部状态,条件调用会让下一次渲染时顺序变化,状态就可能对应错位置。需要条件行为时,应当始终调用 Hook,把条件放进 Hook 的回调或渲染内容里。这个规则看起来像 React,但在 Yew 里还叠加了 Rust 类型约束,错误信息有时不如业务意图直观。闭包里 clone handle 会不会有性能问题?多数场景不用担心,UseStateHandle 这类句柄 clone 的成本很低,它不是复制完整状态。真正要注意的是你在闭包里 clone 了大型业务对象,或者为了绕过所有权把整棵数据复制来复制去。取舍上,UI 回调里 clone 句柄是正常写法,clone 大对象则应考虑 Rc、reducer 或把数据拆小。踩坑最多的是异步任务里捕获了旧 handle,以为 set 后马上能读到新值。自定义 Hook 会不会让代码更难读?会,尤其是名字抽象、返回值过多的时候。自定义 Hook 的边界应该是“一段可复用的状态行为”,而不是把组件里的几行代码随手搬出去。好的 Hook 调用处能看出业务意图,比如 use_window_size、use_local_storage_state;差的 Hook 只会让读者在多个文件之间跳来跳去。团队里最好约定返回结构和命名方式,否则 Hooks 多了以后会比 class 组件还难查。小结Yew Hooks 的核心取舍是:简单状态用 use_state,规则复杂用 use_reducer,外部世界交互放进 use_effect_with,重复的状态行为再抽自定义 Hook。真正影响稳定性的不是 Hook 名字记没记住,而是依赖、清理和闭包捕获有没有想清楚。写 Yew 时多花一点时间整理状态边界,后面排查渲染和异步问题会省很多力。
服务端阅读 05月31日 01:21

Yew 和 WebAssembly 集成时性能瓶颈通常在哪里?

Yew 应用最终运行在 WebAssembly 里,但性能不一定天然比 JavaScript 快。Wasm 擅长密集计算、类型明确的逻辑和可复用 Rust 代码;页面更新、DOM 操作、网络请求仍然要经过浏览器 Web API。也就是说,Yew 的性能优化重点不是“把一切改成 Rust”,而是减少无意义渲染、控制 Wasm 与 JS 的边界调用,并把大计算放到合适的位置。集成链路先保持简单新项目通常用 Trunk 最省心,Cargo.toml 配好 yew、wasm-bindgen、web-sys,HTML 里留一个挂载点即可。wasm-bindgen 负责把 Rust 类型和 JS 世界接起来,web-sys 提供浏览器 API 绑定。踩坑点是 web-sys 默认不开所有 feature,用到 Window、Storage、HtmlInputElement 之类类型时,要在依赖里显式声明。[dependencies]yew = { version = "0.21", features = ["csr"] }wasm-bindgen = "0.2"gloo-net = "0.5"web-sys = { version = "0.3", features = ["Window", "Storage"] }use yew::prelude::*;#[function_component(App)]fn app() -> Html { html! { <main>{ "Hello from Yew + Wasm" }</main> }}fn main() { yew::Renderer::<App>::new().render();}Wasm 与 JS 互操作要少而清楚Rust 调 JS、JS 调 Rust 都可以,但边界不是免费的。字符串、JSON、大数组在两边来回传,会产生序列化和拷贝成本。小数据无所谓,图像像素、表格数据、日志流这类大块数据就要谨慎。更好的做法是让一边完成尽可能完整的一段工作,只把最终结果或必要索引传给另一边。use wasm_bindgen::prelude::*;#[wasm_bindgen]extern "C" { #[wasm_bindgen(js_namespace = console)] fn log(s: &str);}#[wasm_bindgen]pub fn score(values: Vec<f64>) -> f64 { let sum: f64 = values.iter().sum(); log("score calculated in wasm"); sum / values.len() as f64}包体和渲染同样重要很多 Yew 页面慢,不是 Rust 算得慢,而是首包大、依赖重、组件频繁重渲染。发布构建要打开优化,并用 wasm-opt 或 Trunk 的 release 流程压缩。组件层面,props 要尽量保持 PartialEq 有意义,列表项加 key,昂贵计算用 memo 或提前整理。边界是不要为了省一次渲染把状态拆得过碎,状态太分散会让数据流更难查。性能优化先量再改Yew + Wasm 项目最怕凭感觉优化:看到页面卡就拆组件,看到包体大就删依赖,最后问题可能还在接口或图片资源上。更稳的做法是先看浏览器 Performance、Network 和构建产物大小,再决定改哪里。边界也很明确:首屏慢优先查包体和资源加载,交互卡优先查主线程长任务,数据量大才重点看 Wasm 与 JS 的传输成本。如果团队没有现成监控,至少在关键交互前后打点记录耗时,不要只看开发机上的主观体感。生产环境的低端手机、慢网络和浏览器扩展都会放大问题,优化方案必须覆盖这些边界。同时要注意,Wasm 不是独立运行时,它仍然和页面脚本共享浏览器主线程的很多限制。一次过大的同步计算会挡住渲染,一次过多的日志输出也会拖慢调试环境。优化时保留可回滚空间,比一次性改掉整条链路更安全。追问Yew + Wasm 一定比 React 快吗?不一定,尤其是以表单、DOM 交互和网络请求为主的页面,瓶颈通常不在语言运行速度。Yew 的优势在 Rust 类型系统、共享业务逻辑和某些计算密集场景。取舍是:如果团队已经有 Rust 能力,Yew 很有吸引力;如果只是普通内容站,为了“更快”迁移到 Wasm 往往不划算。什么时候应该把计算放进 Web Worker?当计算会阻塞主线程,比如大文件解析、图像处理、复杂加密或上万行数据聚合,就该考虑 Worker。Yew 组件本身负责 UI,长任务放主线程会让点击、输入和动画都卡住。踩坑点是 Worker 通信也要序列化数据,任务很小却丢给 Worker,反而会因为来回传输更慢。wasm-bindgen 传 Vec 和传 JSON 有什么区别?Vec<f64> 这类简单数值数组更适合跨边界传递,结构清楚,解析成本低。JSON 灵活,但每次都要序列化和反序列化,大对象会明显拖慢。边界做法是外部接口可以用 JSON 保持兼容,内部高频调用尽量改成明确类型或索引引用。包体太大通常从哪里查?先看依赖,不要把只用一个小功能的庞大 crate 拉进浏览器。再看 web-sys feature、调试符号、panic 信息和日志库是否进入 release 包。取舍是可读的错误信息和小包体之间要平衡,生产环境可以收紧 panic 和日志,开发环境保留调试体验。Yew 调浏览器 API 有哪些边界?浏览器 API 仍然受同源策略、权限提示和异步模型限制,Wasm 不能绕过这些安全规则。比如剪贴板、文件系统、摄像头都需要用户授权,离线缓存也受浏览器策略影响。踩坑点是 Rust 类型让代码看起来很可靠,但运行时权限失败仍然要按前端方式处理错误。小结Yew 和 WebAssembly 的集成价值在于把 Rust 的可靠性带到前端,而不是神奇地替浏览器消除所有成本。优化时先量化首包、渲染次数和 JS/Wasm 边界调用,再决定是拆包、缓存、Worker 还是减少数据传输。这样做比盲目堆优化技巧更稳。
服务端阅读 05月31日 01:21

Yew Router 怎么做参数路由、跳转和权限控制?

Yew 做路由管理,核心是 yew-router 的 Routable 枚举。你把 URL 规则写成 Rust 类型,再用 BrowserRouter 和 Switch 把不同路由映射到组件。它比手写 window.location 安全得多:参数解析、404、导航链接都能在编译期获得一部分保障。真正容易出问题的地方不在“怎么配路由”,而在路由粒度、权限检查、查询参数和浏览器刷新后的状态恢复。先把路由当成公开接口设计路由不是组件文件名的翻译,它是用户会收藏、分享、刷新进入的地址。列表页、详情页、设置页可以成为路由;弹窗开关、局部 tab 是否进 URL,要看它有没有独立访问价值。过细的路由会让导航逻辑变复杂,过粗又会让返回按钮不好用。我的经验是:刷新后仍然应该保留的页面状态,才值得放进路径或查询参数。use yew::prelude::*;use yew_router::prelude::*;#[derive(Clone, Routable, PartialEq)]enum Route { #[at("/")] Home, #[at("/users")] Users, #[at("/users/:id")] UserDetail { id: u32 }, #[at("/login")] Login, #[not_found] #[at("/404")] NotFound,}fn switch(route: Route) -> Html { match route { Route::Home => html! { <Home /> }, Route::Users => html! { <Users /> }, Route::UserDetail { id } => html! { <UserDetail id={id} /> }, Route::Login => html! { <Login /> }, Route::NotFound => html! { <h2>{ "页面不存在" }</h2> }, }}#[function_component(App)]fn app() -> Html { html! { <BrowserRouter><Switch<Route> render={switch} /></BrowserRouter> }}跳转别只会写 Link静态导航用 Link<Route>,比如菜单、面包屑、卡片入口。登录成功后跳转、提交表单后回详情页这类动作,需要 use_navigator。边界在于:导航是一种副作用,不要在组件渲染过程中直接触发,否则可能造成重复跳转。通常把跳转放在点击回调、请求成功分支或 effect 里,并且先判断条件是否真的变化。#[function_component(LoginButton)]fn login_button() -> Html { let nav = use_navigator().unwrap(); let onclick = Callback::from(move |_| { // 登录成功后再跳转,示例省略请求逻辑 nav.push(&Route::Users); }); html! { <button {onclick}>{ "进入用户列表" }</button> }}权限控制要区分“未登录”和“无权限”很多路由守卫写坏,是因为把所有异常都跳到登录页。未登录应该去登录页,无权限应该显示 403,资源不存在应该是 404。Yew Router 没有强制的守卫语法,常见做法是在 switch 或页面壳组件里根据认证状态返回不同组件。踩坑点是认证状态往往异步加载,刚进页面时既不是已登录也不是未登录,最好先显示 loading,避免页面闪一下就跳走。查询参数要有默认值和兼容策略真实项目里的 URL 会被用户收藏,也会被旧版本页面留下来,所以解析查询参数时不要假设它永远合法。分页不是数字、筛选值已经下线、参数组合互相冲突,都应该回退到安全默认值。这里的取舍是不要把所有状态都塞进 URL,只有刷新后需要保留、分享后仍有意义的状态才放进去,否则路由会变成难维护的状态仓库。对运营活动页还要考虑短链接和历史链接兼容,旧路径最好通过服务端或前端重定向到新路由。这样会多维护一层映射,但比让用户点进收藏夹直接看到 404 更可控。追问路由参数应该用 path 还是 query?资源身份用 path,例如 /users/42,因为它表达的是页面主体。筛选、排序、分页更适合 query,例如 /users?page=2&role=admin,因为它们只是同一资源列表的视图状态。取舍标准是:去掉这个字段后页面主体是否改变,改变就放 path,不改变就放 query。yew-router 支持嵌套路由吗?可以做,但通常不是像某些前端框架那样自动生成嵌套出口,而是通过路由枚举和组件组合来表达。后台管理系统可以把 /settings/profile、/settings/billing 定成不同变体,再由 Settings 布局组件包一层。边界是嵌套太深会让枚举变臃肿,这时可以拆分子路由枚举,但别为了“架构好看”过早拆。刷新页面 404 是 Yew 的问题吗?多数情况下不是 Yew 的问题,而是服务器没有把未知路径回退到 index.html。开发环境里路由正常,部署到 Nginx 或静态托管后刷新 404,就是典型踩坑。解决方式是在服务器配置 history fallback;如果做不到,就考虑 HashRouter 风格的 URL,但链接观感和 SEO 会差一些。权限判断放 switch 里还是页面组件里?简单项目放 switch 里够用,可以集中看清哪些路由需要登录。复杂项目更适合做 ProtectedRoute 或布局组件,因为权限往往还影响菜单、面包屑和页面骨架。取舍是集中判断方便审计,分散到页面更灵活,但要避免同一套权限规则复制三四遍。路由懒加载在 Yew 里值得做吗?管理后台、图表页、编辑器页这类包体大的页面值得考虑,普通三五个页面的小应用不一定划算。Rust/Wasm 的拆包和加载链路比 JS 项目更需要构建工具配合,贸然做会增加调试成本。边界是先用构建分析看首包是否真的过大,再决定是否引入懒加载,不要只因为“最佳实践”就上。小结Yew Router 的基础 API 很少,难点在 URL 设计和副作用边界。把路由当作产品接口,而不是组件目录索引,再认真处理参数、刷新、权限和跳转时机,路由层就会稳定很多。
服务端阅读 05月31日 01:21

Yew 里异步数据该用 spawn_local 还是 use_effect_with?

Yew 里处理异步数据,先记住一个边界:组件渲染本身不能 await,异步任务必须被放到浏览器事件循环里执行,再把结果写回状态。最常见的选择是 spawn_local、use_effect_with、use_async 或自己封装状态机。它们没有绝对高低,关键看异步任务是由用户动作触发,还是由组件生命周期和参数变化触发。点击触发的请求适合 spawn_local按钮提交、刷新列表、重试失败请求这类动作,用 spawn_local 最直观。它的优点是写法接近普通 Rust async,缺点是任务一旦启动就不会因为组件卸载自动取消,所以回调里要避免闭包持有过多状态。实际项目里还要处理重复点击,否则第二次请求可能比第一次先返回,把新数据覆盖成旧数据。use gloo_net::http::Request;use wasm_bindgen_futures::spawn_local;use yew::prelude::*;#[function_component(UserPanel)]fn user_panel() -> Html { let user = use_state(|| None::<String>); let loading = use_state(|| false); let onclick = { let user = user.clone(); let loading = loading.clone(); Callback::from(move |_| { loading.set(true); let user = user.clone(); let loading = loading.clone(); spawn_local(async move { let text = Request::get("/api/user").send().await .and_then(|r| r.text()) .await .unwrap_or_else(|_| "load failed".into()); user.set(Some(text)); loading.set(false); }); }) }; html! { <button {onclick} disabled={*loading}>{ user.as_deref().unwrap_or("Load user") }</button> }}参数变化的请求适合 useeffectwith如果请求依赖 id、分页、筛选条件,就不要把逻辑散落在多个按钮里。use_effect_with 能表达“依赖变化就重新拉取”,读起来更像 React 里的 effect。坑在于依赖值必须稳定,复杂对象频繁创建会导致请求被反复触发。遇到搜索框输入时,通常还要加 debounce,否则每个键盘事件都会打到后端。#[derive(Clone, PartialEq, Properties)]struct Props { pub user_id: u32 }#[function_component(Profile)]fn profile(props: &Props) -> Html { let name = use_state(|| String::new()); { let name = name.clone(); let id = props.user_id; use_effect_with(id, move |_| { spawn_local(async move { let url = format!("/api/users/{id}"); if let Ok(resp) = Request::get(&url).send().await { name.set(resp.text().await.unwrap_or_default()); } }); || () }); } html! { <p>{ (*name).clone() }</p> }}状态别只放 data异步 UI 至少要区分 idle、loading、success、error,否则页面会出现“空白到底是没数据还是请求失败”的尴尬。小组件可以用三个 use_state,列表页或表单页更建议定义枚举,避免 loading 结束但 error 没清掉的脏状态。缓存也不是越早做越好,Yew 端缓存适合低频、可容忍短暂旧数据的接口;强一致数据仍应以服务端返回为准。错误和重试要克制请求失败后直接 unwrap 是 Yew 新手最常见的坑,浏览器网络环境比本地测试脆弱得多。建议把错误展示成用户能理解的文案,同时保留日志给调试用。重试也要有边界:普通查询可以手动重试,支付、下单、写入接口必须考虑幂等,否则一次网络抖动可能造成重复提交。如果接口支持取消,可以把 AbortController 或请求 token 也纳入封装;如果不支持取消,至少要在响应回来时判断它是否仍然属于当前页面状态。这样做牺牲了一点代码简洁度,但能换来更可靠的用户体验。追问为什么不能在组件函数里直接 await?Yew 的函数组件必须同步返回 Html,否则虚拟 DOM 没法完成本轮渲染。把 await 放进组件函数会破坏这个约定,所以要用 spawn_local 或 effect 把异步任务挪出去。取舍是代码多了一层状态管理,但边界更清楚:渲染只读状态,异步只改状态。spawn_local 会不会导致内存泄漏?它本身不等于泄漏,但任务不会因为组件消失自动停下,这是浏览器端异步的常见坑。若任务里捕获了大对象,或请求返回后还更新已经无意义的状态,就会造成额外开销。边界做法是让任务只捕获必要的 UseStateHandle,长轮询、WebSocket 这类任务要在 effect 清理函数里显式关闭。HTTP 请求用 reqwest 还是 gloo-net?浏览器端 Yew 项目通常优先选 gloo-net,依赖轻,和 Web API 的行为更贴近。reqwest 在 Rust 生态里更通用,但 wasm 目标下功能会受限制,包体也可能更重。取舍很简单:前端只做普通 JSON 请求选 gloo-net,共享跨端 SDK 或已有封装时再考虑 reqwest。如何避免旧请求覆盖新结果?可以给每次请求加递增版本号,只接受当前版本的响应。也可以在输入搜索时先 debounce,再发请求,减少乱序返回的概率。踩坑点是不要只靠 loading 判断,因为两个请求并发时,第一个完成会把 loading 置回 false,让用户以为第二个也结束了。WebSocket 应该放在哪里管理?WebSocket 不适合散落在多个组件的点击回调里,最好放在上层组件或自定义 hook 中统一管理。它需要连接、重连、心跳、关闭四个状态,普通一次性请求的写法不够用。边界是实时协作、行情、日志流适合 WebSocket;偶尔刷新一次的通知列表,用 HTTP 轮询反而更简单。小结Yew 的异步处理不是把 Rust 后端那套原样搬到浏览器,而是围绕组件状态做编排。用户动作触发用 spawn_local,参数变化用 use_effect_with,复杂页面把 loading、error、data 收进明确的状态模型。只要处理好重复请求、卸载清理和错误展示,异步代码就不会在页面越写越大时失控。
服务端阅读 05月31日 01:07

Scrapy 中间件有什么作用?适合哪些场景?

Scrapy 中间件可以理解为爬虫流程里的拦截层:请求发出去之前能改,响应回来之后能查,异常发生时也能兜底。它主要分为下载器中间件和爬虫中间件,前者夹在引擎和下载器之间,常用来处理请求头、代理、重试、Cookie、响应状态;后者夹在引擎和 Spider 之间,更适合处理输入给 Spider 的响应和 Spider 产出的请求。中间件的价值是把通用逻辑从 Spider 里抽走,但边界也明显:业务字段解析不要塞进中间件,否则后面排查时会分不清数据到底在哪一步被改掉。判断一个逻辑要不要做成中间件,可以看它是否能被多个 Spider 复用,以及是否只依赖请求、响应、异常这些通用对象。追问下载器中间件和爬虫中间件有什么区别?下载器中间件关注“请求能不能顺利拿到响应”,所以常见场景是加请求头、切代理、处理 403、记录耗时、对异常做重试。爬虫中间件关注“响应和请求如何进出 Spider”,比如过滤某些响应、统一补充 meta、处理 Spider 抛出的异常。取舍上,大多数反爬和网络层问题放下载器中间件更自然,业务解析前后的流程控制才考虑爬虫中间件。踩坑点是把两者职责混在一起,比如在下载器中间件里解析商品价格,短期能跑,长期会让代码很难测试。class TimingDownloaderMiddleware: def process_request(self, request, spider): request.meta["start_ts"] = time.time() def process_response(self, request, response, spider): cost = time.time() - request.meta.get("start_ts", time.time()) spider.logger.info("%s %s %.2fs", response.status, response.url, cost) return responseprocess_request、process_response、process_exception 分别怎么用?process_request 在请求进入下载器前执行,适合补请求头、代理、Cookie 或直接返回缓存响应。process_response 在响应回到引擎后执行,适合检查状态码、替换响应、对异常页面重新发请求。process_exception 只处理下载阶段抛出的异常,比如超时、连接失败、DNS 错误。边界是返回值会改变流程:返回 Request 会重新调度,返回 Response 会跳过下载,返回 None 才是继续交给下一个中间件;不理解这个规则,很容易写出重复请求或响应丢失的问题。class StatusRetryMiddleware: def process_response(self, request, response, spider): if response.status in {403, 429} and request.meta.get("retry_times", 0) < 2: new = request.copy() new.meta["retry_times"] = request.meta.get("retry_times", 0) + 1 new.dont_filter = True return new return response中间件优先级应该怎么配置?Scrapy 通过数字控制中间件顺序,下载器中间件的 process_request 按数字从小到大执行,process_response 则反过来。这个设计容易让人第一次配置时看反,尤其是多个中间件都在改代理、请求头和重试逻辑时。取舍上,通用基础逻辑可以靠前,例如设置默认 Header;依赖响应结果的统计、重试、清洗可以靠后。踩坑最多的是优先级和内置中间件冲突,比如自定义重试放错位置,导致 Scrapy 内置 RetryMiddleware 已经处理过一次,你又额外重试一次。# settings.pyDOWNLOADER_MIDDLEWARES = { "myproject.middlewares.RandomHeaderMiddleware": 400, "myproject.middlewares.ProxyMiddleware": 410, "myproject.middlewares.StatusRetryMiddleware": 550,}哪些逻辑不适合写进中间件?和具体页面结构强相关的字段解析不适合写进中间件,应该留在 Spider 或 Item Pipeline 里。中间件也不适合保存大量业务状态,例如把所有已抓商品、分类树、价格规则都塞进去,这会让它变成隐藏的业务中心。边界判断可以很简单:如果这个逻辑换一个 Spider 仍然有价值,它适合抽成中间件;如果只服务某个页面字段,就别放进去。实际项目里滥用中间件会让调试很痛苦,因为响应还没到 parse 方法就已经被改过,日志不全时很难还原现场。如何设计一个可维护的代理中间件?代理中间件不要只做随机选择,还应该记录代理的失败次数、最近使用时间、适用域名和是否需要隔离登录态。轻量任务可以从列表里轮询,成本低也容易排查;高并发任务则需要独立代理池服务,负责健康检查和下线坏代理。取舍上,本地简单实现开发快,但多 Spider 共用时容易重复踩同一个坏代理;中心化代理池更稳定,却需要额外维护。常见坑是失败后立刻无限重试同一个请求,最后把调度队列拖慢,应该设置最大重试次数并对状态码做区分。class ProxyMiddleware: def process_request(self, request, spider): proxy = spider.proxy_pool.get(domain=request.url.split('/')[2]) if proxy: request.meta["proxy"] = proxy def process_exception(self, request, exception, spider): proxy = request.meta.get("proxy") if proxy: spider.proxy_pool.mark_bad(proxy)结论Scrapy 中间件适合承载跨 Spider 的通用流程逻辑,尤其是网络请求、反爬、重试、代理、日志和异常处理。写中间件时最重要的是职责边界和执行顺序:通用逻辑抽出来,业务解析留在业务层,返回值和优先级要明确。这样中间件才是扩展点,而不是另一个难排查的黑盒。生产环境里还要给关键中间件补日志和开关,遇到代理池抖动、目标站改规则或重试风暴时,可以快速关闭某一层,而不是停掉整套爬虫。
服务端阅读 05月31日 01:07

Scrapy 如何用选择器解析网页内容?

Scrapy 解析网页内容主要靠 Selector,它把响应内容包装成可以用 CSS、XPath 和正则提取的对象。选择器写得好,爬虫会很稳定;选择器写得太依赖页面样式,前端一改 class 名就会全线失效。实际开发里不要纠结“CSS 一定比 XPath 简单”或者“XPath 一定更强”,更重要的是看页面结构、字段稳定性和后续维护成本。写选择器前先在 Scrapy shell 里试几条真实页面,比直接在代码里盲改更省时间,也能提前发现编码、重定向和空页面问题。列表页、详情页、分页、隐藏字段都可能需要不同写法,边界是:如果数据来自异步接口而不是 HTML,优先抓接口,别硬从渲染后的 DOM 里抠。追问CSS 选择器和 XPath 应该怎么选?CSS 选择器写起来更直观,适合按 class、id、标签层级提取内容,团队里前端背景的人也容易维护。XPath 的优势是表达能力更强,能按文本、位置、祖先节点、兄弟节点做更精确的定位。取舍上,结构简单就用 CSS,遇到“找到包含某段文字的标题,再取它后面的价格”这类需求,用 XPath 会少绕很多弯。踩坑点是过度依赖层级路径,例如 div > div > div:nth-child(2),页面插一个广告位就会错位。更稳的方式是先找业务语义明显的容器,再在容器内取标题、链接和价格,避免整页范围内抓到推荐位或页脚里的相似元素。def parse(self, response): for card in response.css("div.product-card"): yield { "title": card.css("h2::text").get(default="").strip(), "price": card.xpath(".//span[contains(@class, 'price')]/text()").get(), "url": response.urljoin(card.css("a::attr(href)").get()) }.get()、.getall() 和 .extract() 有什么区别?现在更推荐用 .get() 和 .getall(),它们语义更清楚:前者拿第一个结果,后者拿全部结果。.extract() 是旧写法,仍然能用,但新代码里没有必要继续混用,团队维护时容易让人误判返回类型。边界在于 .get() 没匹配到会返回 None,后续直接 .strip() 就会报错,所以要加默认值或单独判断。很多解析 bug 不是选择器错了,而是默认值没处理好,导致某一条脏数据让整个任务中断。字段进入 Item 前最好统一做清洗,例如去空白、补全 URL、规范日期格式,这些步骤比事后在数据库里修数据可靠得多。title = response.css("h1::text").get(default="").strip()tags = [x.strip() for x in response.css(".tag::text").getall() if x.strip()]如何让选择器更抗页面改版?优先选择语义稳定的属性,比如 data-id、itemprop、aria-label、固定 URL 结构,而不是只看样式 class。很多网站的 class 是构建工具生成的,今天叫 css-a1b2,下次发布就变成另一个值,用它做选择器风险很高。取舍上,选择器写得宽一点能抗改版,但可能误抓广告、推荐位和隐藏模板;写得窄一点更准确,却更容易被结构变动打断。实战里可以先定位稳定容器,再在容器内做相对选择,避免全局搜索抓到重复字段。for item in response.css("article[data-id]"): title = item.css("[itemprop='headline']::text, h2::text").get(default="").strip() if not title: continue yield {"title": title}正则选择器适合用在哪些地方?正则适合从一段文本里提取格式明确的值,比如脚本里的 JSON 字段、价格数字、日期、页面内嵌 ID。它不适合替代 HTML 解析,因为用正则匹配嵌套标签通常会变得脆弱又难读。边界是正则要尽量约束上下文,别写一个过宽的 .* 去吞整页内容,否则页面稍微变大就可能性能变差。踩坑最多的是拿到转义后的 JSON 字符串却没有反转义,或者把多个相似字段里的第一个误当成目标字段。import jsonraw = response.xpath("//script[contains(., 'window.__DATA__')]/text()").re_first(r"window\.__DATA__\s*=\s*(\{.*\})")if raw: data = json.loads(raw)解析结果怎么做质量检查?选择器写完后,至少要检查必填字段是否为空、列表数量是否异常、URL 是否能补全、价格或日期格式是否符合预期。不要等数据入库后才发现标题全是空字符串,那时排查成本会高很多。取舍上,严格校验能尽早发现页面变动,但也可能丢掉少量字段不完整的正常数据;宽松校验吞吐高,却容易把坏数据带到下游。常见做法是必填字段缺失就丢弃并打日志,非核心字段允许为空,但要在统计里看到缺失比例。如果某次发布后缺失率突然升高,就应该暂停入库或降级为抽样抓取,先确认页面结构是否变化。结论Scrapy 选择器的核心不是背语法,而是写出稳定、可读、可验证的提取逻辑。CSS、XPath、正则各有位置:CSS 负责常规结构,XPath 处理复杂关系,正则只提取文本里的明确模式。解析代码一旦进入生产,就要配合默认值、校验和日志,否则页面一次小改版就可能让数据悄悄变脏。
服务端阅读 05月31日 01:07

Scrapy 如何应对反爬虫机制?

Scrapy 处理反爬,不是简单地把 User-Agent 换成浏览器就结束了。更稳的做法是先判断网站的限制来自哪里:是请求频率过高、Cookie 会话缺失、IP 被限流,还是前端渲染和验证码把页面内容挡住了。Scrapy 能做的是把请求行为变得更接近正常访问,并把失败、限速、代理、登录态这些环节放进可维护的配置和中间件里。上线前最好先用小样本跑通完整链路,确认列表页、详情页、翻页和异常页都能被识别,否则反爬策略还没生效,解析层就已经把脏页面当成正常数据了。真正的边界也要说清楚:遇到强验证码、设备指纹、复杂 JS 加密时,Scrapy 本身不是万能钥匙,继续硬撞通常只会浪费代理和时间。追问只改 User-Agent 能绕过反爬吗?不能把 User-Agent 当成反爬方案的全部,它最多解决一部分“默认爬虫特征太明显”的问题。很多网站会同时检查 Accept、Accept-Language、Referer、Cookie、请求间隔,甚至会观察同一 IP 的访问路径是否像真人。取舍上,随机请求头实现成本低,适合轻度限制的网站;但如果每次请求头组合都乱跳,反而可能显得更异常。踩坑最多的是只改 UA、不保留会话,结果登录页、列表页、详情页之间的 Cookie 对不上,服务端直接返回空数据或 403。还有一种隐蔽情况是服务端返回状态码 200,但正文其实是风控提示页,解析器照样能跑,却会把错误内容写进数据库。# middlewares.pyimport randomclass RandomHeaderMiddleware: agents = [ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/124 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/123 Safari/537.36" ] def process_request(self, request, spider): request.headers.setdefault("User-Agent", random.choice(self.agents)) request.headers.setdefault("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")请求速度应该怎么控制?Scrapy 的 DOWNLOADDELAY、RANDOMIZEDOWNLOADDELAY 和 AutoThrottle 都能控制速度,但它们解决的问题不一样。固定延迟简单可预测,适合小站点和低并发任务;AutoThrottle 会根据响应延迟动态调整,更适合站点负载变化明显的场景。边界是 AutoThrottle 不是“越开越安全”,如果目标站本身响应很快但限制按分钟计数,它可能仍然跑得太快。实际项目里建议从保守参数开始,看 429、403、超时比例再调,不要一上来把 CONCURRENTREQUESTS 拉满。如果任务有时效要求,可以按域名拆分队列,让高价值页面优先抓取,而不是所有 URL 使用同一套并发策略。# settings.pyCONCURRENT_REQUESTS = 8CONCURRENT_REQUESTS_PER_DOMAIN = 4DOWNLOAD_DELAY = 1.5RANDOMIZE_DOWNLOAD_DELAY = TrueAUTOTHROTTLE_ENABLED = TrueAUTOTHROTTLE_START_DELAY = 1AUTOTHROTTLE_MAX_DELAY = 10AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0代理池什么时候该用,什么时候不该用?代理池适合 IP 维度限流明显的网站,例如同一 IP 连续访问几十次后开始返回 403 或验证码。它不适合拿来掩盖所有问题,因为代理质量差会带来超时、脏 IP、地区不一致、TLS 指纹异常等新麻烦。取舍上,少量稳定代理加合理限速,通常比大量廉价代理随机切换更可靠。踩坑点是登录态和代理绑定:如果登录请求用 A 代理,后续详情页突然换到 B 代理,服务端很可能判定会话异常。class ProxyMiddleware: def process_request(self, request, spider): proxy = spider.proxy_pool.pick() request.meta["proxy"] = proxy登录、Cookie 和验证码怎么处理?需要登录的网站先用 FormRequest 或接口请求建立会话,让 Scrapy 的 CookieMiddleware 保持后续请求状态。简单验证码可以接第三方识别服务,但这会增加成本和失败率,也可能触碰站点规则边界。更推荐的思路是先确认数据是否有公开接口、RSS、站点地图或授权数据源,不要默认把验证码当成必须绕过的障碍。常见坑是登录成功后没有检查响应内容,只看状态码 200 就继续抓,最后抓到一堆登录页 HTML。def start_requests(self): yield scrapy.FormRequest( url="https://example.com/login", formdata={"username": "user", "password": "pwd"}, callback=self.after_login )def after_login(self, response): if "退出登录" not in response.text: self.logger.warning("login failed") return yield scrapy.Request("https://example.com/profile")反爬失败时怎么判断问题出在哪?不要只看异常类型,要同时记录状态码、响应长度、重定向地址、命中的代理、请求头和重试次数。403 常见于权限或规则拦截,429 多半是频率问题,200 但内容为空则可能是登录态、JS 渲染或被返回了假页面。取舍上,日志越详细越容易定位,但也要避免记录密码、Cookie 等敏感信息。一个实用做法是对异常响应保存少量样本 HTML,排查完及时清理,避免把脏数据继续送进解析流程。日志里还可以记录指纹字段,例如代理、UA、下载延迟和重试次数,这样才能判断是单个代理坏了,还是整套策略被目标站识别。结论Scrapy 的反爬处理重点是分层:请求头和 Cookie 解决基础识别,限速解决频率,代理解决 IP 限制,中间件负责把这些策略做成可复用逻辑。遇到强验证码、设备指纹和复杂加密时,要先评估成本和合规边界,而不是把所有问题都交给代理池。
服务端阅读 05月31日 01:07

Scrapy 数据流从请求到入库是如何运转的?

Scrapy 的数据流可以理解成一条异步流水线:Spider 产生请求,Engine 负责协调,Scheduler 排队和去重,Downloader 取回响应,Spider 再解析响应并产出新的请求或 Item,最后 Item 进入 Pipeline。真正的核心不是某一个组件,而是 Engine 在这些组件之间来回转发数据。启动爬虫后,Engine 会向 Spider 索要初始请求,并交给 Scheduler。Scheduler 保存请求,Downloader Middleware 可以在下载前改请求头、代理或 cookie,Downloader 拿到响应后再经过 Middleware 返回给 Spider。Spider 的回调函数解析页面,如果产出 Request,就回到 Scheduler;如果产出 Item,就交给 Pipeline。这个循环持续到队列清空、运行被停止或触发关闭条件。Spider -> Engine -> Scheduler -> Engine -> DownloaderDownloader -> Engine -> Spider -> Item PipelineSpider -> new Request -> Scheduler常见配置会影响这条数据流的节奏:CONCURRENT_REQUESTS = 16DOWNLOAD_DELAY = 0.5RETRY_ENABLED = TrueRETRY_TIMES = 2DOWNLOADER_MIDDLEWARES = { 'myproject.middlewares.ProxyMiddleware': 350,}ITEM_PIPELINES = { 'myproject.pipelines.StorePipeline': 300,}追问Engine 在 Scrapy 数据流里到底做什么?Engine 是调度中枢,它不负责解析页面,也不负责真正下载,而是决定请求和响应该交给谁。它向 Scheduler 要请求,把请求交给 Downloader,再把响应交给 Spider。取舍是这种拆分让组件边界清楚,也让中间件和扩展有插入点。边界是业务代码通常不直接操作 Engine,调试时更多看日志、信号和各组件输入输出。Scheduler 和 Downloader 的关系是什么?Scheduler 只管请求排队、优先级和去重,Downloader 只管把某个请求变成响应。Engine 从 Scheduler 取出下一个请求,再交给 Downloader 执行下载。踩坑点是把“为什么这个 URL 没抓”都归因于 Downloader,其实它可能早就在 Scheduler 阶段被去重过滤了。排查时应先看 dupefilter 日志,再看下载状态码和异常。Middleware 在数据流中适合做哪些事?Downloader Middleware 适合处理请求发送前和响应返回后的横切逻辑,比如代理、User-Agent、重试、异常响应处理。Spider Middleware 更靠近解析层,适合处理进入或离开 spider 的 response、request、item。取舍是 Middleware 很强,但滥用会让数据流变得不透明。边界建议是:和网络请求相关的放 Downloader Middleware,和解析结果相关的放 Spider Middleware,不要把入库逻辑塞进去。Scrapy 为什么能同时处理很多请求?Scrapy 基于 Twisted 的异步事件模型,等待网络响应时不会阻塞整个进程,可以继续处理其他请求。它适合 I/O 密集型爬取,尤其是大量网页下载场景。边界是解析函数里如果写了 CPU 很重的代码,或者调用同步阻塞接口,异步优势会被抵消。踩坑最多的是在回调里直接做大文件处理、慢 SQL 或 time.sleep(),结果并发配置再高也跑不起来。数据流排查应该从哪里开始?先看请求有没有进入 Scheduler,再看是否被去重,然后看 Downloader 返回的状态码,最后看 Spider 是否产出 Item 或新 Request。这个顺序比直接盯着 pipeline 更有效,因为很多问题根本没走到入库阶段。取舍是全链路日志会增加噪音,但在调试复杂爬虫时非常值。生产环境可以只保留关键统计,例如请求数、去重数、非 200 响应数、item 数和 drop 数。
服务端阅读 05月31日 01:07

Scrapy Pipeline 管道如何清洗、去重并入库?

Scrapy Pipeline 是 Item 离开 spider 后进入存储系统前的处理链。它适合做数据清洗、字段校验、去重、补全、入库、发送消息等工作。和 spider 相比,Pipeline 更靠近数据出口,所以它不应该关心页面怎么解析,而应该关心“这条数据是否可信、如何保存、失败后怎么办”。Pipeline 可以配置多个,Scrapy 会按优先级从小到大依次执行。每个 process_item 要么返回 item 交给下一个管道,要么抛出 DropItem 丢弃数据。这里的取舍很实际:把所有逻辑写进一个 Pipeline 简单,但后期很难复用;拆得太细又会增加配置和排查成本。通常可以按职责拆成校验、去重、存储三类。# pipelines.pyfrom itemadapter import ItemAdapterfrom scrapy.exceptions import DropItemclass ValidatePipeline: def process_item(self, item, spider): data = ItemAdapter(item) if not data.get('title') or not data.get('url'): raise DropItem('missing title or url') data['title'] = data['title'].strip() return itemclass DedupePipeline: def open_spider(self, spider): self.seen = set() def process_item(self, item, spider): url = ItemAdapter(item).get('url') if url in self.seen: raise DropItem(f'duplicate url: {url}') self.seen.add(url) return item# settings.pyITEM_PIPELINES = { 'myproject.pipelines.ValidatePipeline': 100, 'myproject.pipelines.DedupePipeline': 200, 'myproject.pipelines.StorePipeline': 300,}追问Pipeline 和 Item Loader 都能清洗数据,应该放哪边?Loader 更适合处理字段级别的格式问题,比如去空格、取第一个文本、把价格字符串转数字。Pipeline 更适合处理整条数据的业务判断,比如字段是否完整、是否重复、是否需要入库。取舍点是复用范围:页面解析相关的清洗放 Loader,跨 spider 的出口规则放 Pipeline。常见坑是两个地方都清洗同一个字段,最后线上数据异常时没人说得清是哪一步改坏的。多个 Pipeline 的执行顺序怎么定?ITEM_PIPELINES 里的数字越小越早执行,所以校验通常放前面,存储放后面。这样缺字段或明显错误的数据可以尽早丢掉,避免浪费数据库连接和网络请求。边界是有些字段需要存储前才生成,比如数据库 ID 或消息队列 trace id,就不能在前置校验里强依赖。实践里优先级不要随手写,最好留出间隔,比如 100、200、300,后面插新步骤更方便。Pipeline 里连接数据库要注意什么?连接对象应在 open_spider 中初始化,在 close_spider 中关闭,避免每条 item 都新建连接。每条都建连接会非常慢,还容易把数据库打到连接数上限。取舍是长连接性能好,但要处理断线重连和批量提交失败。边界是小脚本写 CSV 可以简单处理,生产入库则要考虑唯一键、事务、重试和错误日志。去重应该放内存、Redis 还是数据库?单机小任务用内存 set 最简单,速度快,也不需要额外服务。多进程、多机器或长周期任务就应该用 Redis 或数据库唯一约束,因为本地 set 无法跨节点共享。取舍是 Redis 去重快但需要维护 key 生命周期,数据库去重可靠但写入压力更大。踩坑点是只用 URL 去重,有些站同一内容存在移动端、PC 端、带追踪参数的多个 URL,需要先做 URL 规范化。Pipeline 抛出 DropItem 后还会发生什么?抛出 DropItem 后,这条 item 不会进入后续 Pipeline,Scrapy 会把它记录到 dropped 统计里。这个机制适合丢弃缺字段、重复、明显异常的数据。边界是不要把暂时性错误也直接 Drop,比如数据库超时、接口偶发失败,这类问题更适合重试或落失败队列。否则数据会“安静地丢失”,日志里看得到,但业务侧很难补回来。