面试题手册

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

前端阅读 05月30日 01:39

MobX 异步操作为什么要用 runInAction 或 flow?

MobX 处理异步的关键不是“能不能 await”,而是 await 之后的状态修改已经离开原来的 action。开启 enforceActions 时,接口返回后直接改 observable 容易报警,也会让更新边界不清。常用做法有两种:简单请求用 async/await + runInAction,在成功、失败分支里集中更新 data/loading/error;流程复杂、需要取消任务时用 flow(function*(){}),把 await 换成 yield。不要说 async action 会自动包住整个异步过程,它只覆盖同步阶段。追问runInAction 和 flow 怎么选?普通接口请求选 runInAction,写法接近日常 async/await;多步骤流程、需要取消、想少写包装代码时选 flow。为什么 await 后还要重新进 action?因为 await 后是新的 tick,原 action 已结束。MobX 官方也强调 await 后的状态修改不在同一个执行阶段。loading 和 error 应该怎么写?进入请求前设 loading=true、清空 error;成功和失败分支都要把 loading=false 放进 action,避免页面一直转圈。实际项目最常见的坑是什么?最常见是 catch 里只记录错误,忘了重置 loading;其次是连续修改多个字段却没用 runInAction,导致严格模式报警。写段代码async fetchUser(id) { this.loading = true this.error = null try { const data = await api.getUser(id) runInAction(() => { this.user = data; this.loading = false }) } catch (e) { runInAction(() => { this.error = e.message; this.loading = false }) }}
服务端阅读 05月30日 01:39

如何优化 Zustand 状态更新性能?

Zustand 性能优化先看订阅粒度:组件只订阅自己需要的字段,不要 useStore() 拿整个 store。多个字段一起取时用 shallow 或拆成多个 selector;状态太大时按领域拆 store;异步更新用函数式 set 或 get() 避免旧值。真正的瓶颈通常不是 Zustand,而是选择器返回新对象、组件订阅过宽、列表渲染太重。追问为什么 useStore() 容易造成重渲染?它订阅整个 store,任何字段变化都会让组件重新渲染。字段越多,误伤越明显。shallow 能解决什么问题?selector 返回对象或数组时,每次都是新引用。shallow 会比较第一层字段,字段没变就不触发更新。拆 store 一定更好吗?不一定。强相关状态放一起更好维护;变化频率差异很大、业务边界清楚时再拆,否则会增加同步成本。批量更新要手动处理吗?React 18 下大多数场景会自动批处理。更重要的是把相关字段放在一次 set 里,避免中间状态被订阅者看到。写段代码import { shallow } from 'zustand/shallow';const count = useStore((s) => s.count);const inc = useStore((s) => s.inc);const userView = useStore( (s) => ({ name: s.user.name, role: s.user.role }), shallow);const useStore = create((set) => ({ count: 0, inc: () => set((s) => ({ count: s.count + 1 }))}));
服务端阅读 05月30日 01:39

如何在 Zustand 中处理异步操作?

Zustand 处理异步不需要额外机制,直接在 action 里写 async/await,用 set 更新 loading/data/error,需要最新状态时用 get()。如果是服务端缓存、重试、失效刷新这类问题,优先交给 React Query 或 SWR,Zustand 只保存跨页面共享的 UI 或业务状态。追问async action 里为什么要用 get()?异步代码执行时,闭包里的旧值可能已经过期。get() 读取的是当前 store 状态,适合在 await 后继续基于最新状态更新。loading 和 error 应该怎么设计?简单请求可以放 loading: boolean;多个并发请求最好按 key 存状态,避免 A 请求结束把 B 请求的 loading 误关掉。Promise 链和 async/await 有区别吗?能力上差不多。面试回答用 async/await 更清楚,但要说明 action 可以返回 Promise,组件或测试里可以继续 await。什么时候不该把请求全塞进 Zustand?需要缓存、分页、去重、后台刷新、请求取消时,不要自己造一套数据请求框架,直接用 React Query/SWR 更稳。写段代码const useStore = create((set, get) => ({ user: null, loading: false, error: null, fetchUser: async (id) => { set({ loading: true, error: null }); try { const user = await fetch(`/api/users/${id}`).then(r => r.json()); set({ user, loading: false }); } catch (e) { set({ error: e.message, loading: false }); } }}));
服务端阅读 05月30日 01:39

如何对 Zustand store 进行单元测试?

Zustand store 单测重点是把状态恢复到干净初始值,再验证 action、异步状态和 selector 行为。同步 action 可以直接用 getState() 调;React hook 场景用 renderHook 和 act;异步 action 要 mock 请求并等待 Promise 结束。面试里别只说“很好测”,要提到全局 store 会污染用例,必须在 beforeEach 重置。追问为什么每个测试前要重置 store?Zustand store 默认是模块级单例,上一个测试改过的状态会留到下一个测试,导致用例顺序一变就失败。测 action 一定要 renderHook 吗?不一定。纯 store 逻辑用 useStore.getState().action() 更快;只有要验证 hook 订阅和组件重渲染时,才需要 renderHook。异步 action 怎么测?mock fetch 或请求层,调用 action 后断言 loading、data、error 的最终状态。需要中间态时,可以分阶段 await。selector 性能怎么测?订阅一个具体 selector,更新无关字段,断言渲染次数不变;再更新目标字段,断言它才重新触发。写段代码beforeEach(() => { useStore.setState({ count: 0, user: null }, true);});test('increments count', () => { useStore.getState().increment(); expect(useStore.getState().count).toBe(1);});test('fetch user', async () => { vi.spyOn(global, 'fetch').mockResolvedValue({ json: async () => ({ id: 1 }) }); await useStore.getState().fetchUser(1); expect(useStore.getState().user).toEqual({ id: 1 });});
服务端阅读 05月30日 01:39

如何在 Zustand 中自定义中间件?

Zustand 自定义中间件本质是包一层 config:拦截 set/get/api,再把增强后的能力交还给 store。常见用途是日志、校验、性能埋点、撤销重做。面试里先说函数签名,再强调两点:不要破坏原始 set 语义;组合多个 middleware 时外层先执行,顺序会影响结果。追问自定义 middleware 和普通 action 封装有什么区别?普通 action 只管某个业务动作;middleware 能统一拦截所有状态更新,适合横切能力,比如日志、持久化、校验。set 包装时最容易踩什么坑?别忘了转发 replace 参数,也不要在 middleware 里无条件再次调用增强后的 set,否则可能递归或改变 replace 行为。多个 middleware 的执行顺序怎么看?create(a(b(config))) 中,a 先拿到 config 并包装,实际更新时通常外层逻辑先触发。日志、persist、immer 混用时要明确谁先处理原始对象。实际项目会怎么用?我更倾向把日志、权限校验、状态快照放 middleware,业务状态变化仍留在 action 里,避免 middleware 变成黑盒业务层。写段代码const logger = (config) => (set, get, api) => config((partial, replace) => { const prev = get(); const ret = set(partial, replace); console.log('zustand change', { prev, next: get() }); return ret; }, get, api);const useStore = create(logger((set) => ({ count: 0, inc: () => set((s) => ({ count: s.count + 1 }))})));
服务端阅读 05月30日 01:00

什么是 Cookie?它的工作原理和用途是什么?

Cookie 是浏览器保存的一小段键值数据,通常用来让 HTTP 这种“无状态协议”记住用户状态。流程很简单:服务端在响应里用 Set-Cookie 下发 Cookie,浏览器按 Domain、Path、Expires、SameSite 等规则保存;之后访问匹配的地址时,浏览器会自动把 Cookie 放进 Cookie 请求头发回服务端。它常用于登录会话、用户偏好、购物车和统计分析。敏感会话 Cookie 一般要配 Secure、HttpOnly、SameSite,否则容易被窃取或被跨站请求滥用。追问Cookie 和 localStorage 有什么区别?Cookie 会随匹配请求自动发送给服务器,适合会话识别;localStorage 只在浏览器本地保存,不会自动进请求头,适合非敏感前端数据。Session Cookie 和持久 Cookie 有什么区别?不设置 Expires 或 Max-Age 的通常是会话 Cookie,浏览器会话结束后失效;设置了过期时间的是持久 Cookie。Cookie 为什么有安全风险?因为它可能代表登录态。被 XSS 读走会导致会话劫持,被 CSRF 利用会让浏览器自动带着 Cookie 发起恶意请求。SameSite、HttpOnly、Secure 分别解决什么问题?SameSite 缓解 CSRF,HttpOnly 防止脚本读取,Secure 限制只走 HTTPS。写段代码Set-Cookie: sessionId=abc; Path=/; Secure; HttpOnly; SameSite=LaxCookie: sessionId=abc
服务端阅读 05月30日 01:00

Cookie 的 Secure、HttpOnly、SameSite 有什么区别?如何防止被窃取?

防止 Cookie 被窃取,核心是把“传输、读取、跨站发送、作用域”四件事管住:Secure 只允许 HTTPS 传输,降低中间人窃听风险;HttpOnly 禁止 JavaScript 读取,降低 XSS 偷 Cookie 的风险;SameSite 控制跨站请求是否带 Cookie,缓解 CSRF;Domain 和 Path 缩小发送范围。敏感登录态建议服务端设置 Cookie,不把 token 暴露给前端脚本,并优先使用 __Host- 前缀来避免子域覆盖。追问Secure 和 HttpOnly 有什么区别?Secure 管传输,只保证 Cookie 不走普通 HTTP;HttpOnly 管读取,防止 document.cookie 直接拿到敏感值。Path 能当安全边界吗?不能。Path 主要控制发送范围,不应把它当权限隔离。真正的权限判断必须在服务端做。为什么推荐 __Host- 前缀?它要求 Secure、Path=/,且不能设置 Domain,能避免子域种下同名 Cookie 覆盖主站会话。实战里如何排查 Cookie 被窃取?先看是否缺 HttpOnly 或 Secure,再查 XSS、第三方脚本、过宽 Domain,以及登录后 sessionId 是否轮换。写段代码Set-Cookie: __Host-token=abc; Secure; HttpOnly; SameSite=Strict; Path=/; Max-Age=1800
服务端阅读 05月30日 01:00

SameSite Cookie 如何防止 CSRF?Strict、Lax、None 怎么选?

SameSite 是 Set-Cookie 的属性,用来决定 Cookie 是否会随跨站请求发送。它能缓解 CSRF,是因为 CSRF 依赖“用户已登录,浏览器自动带上目标站 Cookie”。当 Cookie 设置为 Strict 或多数情况下的 Lax 时,恶意站点发起的跨站 POST、iframe、图片等请求通常拿不到登录态。常规业务优先用 Lax;高敏感操作用 Strict;确实需要第三方嵌入或跨站登录时才用 None,并且必须同时加 Secure。追问Lax 会完全禁止跨站 Cookie 吗?不会。跨站顶级导航的安全方法,比如用户点击链接发起 GET,仍可能携带 Cookie,所以它更像折中方案。SameSite=None 为什么必须配 Secure?因为 None 允许跨站携带 Cookie,如果不用 HTTPS,Cookie 更容易在链路上泄露,现代浏览器也要求 None; Secure 组合使用。有 SameSite 还需要 CSRF Token 吗?高风险写操作仍建议保留 Token。SameSite 是浏览器策略,Token 是服务端校验,两者叠加更稳。项目里怎么选默认值?登录态默认 Lax;支付确认、改密码这类敏感 Cookie 用 Strict;第三方登录回调、嵌入式应用再评估 None; Secure。写段代码Set-Cookie: session=abc; Secure; HttpOnly; SameSite=Lax; Path=/Set-Cookie: iframe_token=xyz; Secure; HttpOnly; SameSite=None; Path=/
服务端阅读 05月30日 01:00

如何在 JavaScript 中设置、读取和删除 Cookie?

JavaScript 操作 Cookie 主要靠 document.cookie。读取时它返回当前页面可访问的 Cookie 字符串;设置时一次只能写入一个 name=value,不会清空其他 Cookie;删除本质是把同名 Cookie 的过期时间设为过去。实际开发要注意两点:值要用 encodeURIComponent 编码,删除时 Path、Domain 要和设置时一致,否则看起来执行了,其实没删掉。HttpOnly Cookie 不能被 JavaScript 读取或设置,只能由服务端通过 Set-Cookie 下发。追问document.cookie = xxx 会覆盖所有 Cookie 吗?不会。它只会新增或更新当前这一个 Cookie。读取 document.cookie 才会看到多个 Cookie 拼成的字符串。为什么删除 Cookie 有时不生效?大概率是 Path 或 Domain 不一致。比如设置时用了 Path=/app,删除时只写 Path=/,浏览器会认为它们不是同一个 Cookie。为什么要编码 Cookie 值?Cookie 值里如果有空格、分号、中文等特殊字符,可能导致解析错误。写入用 encodeURIComponent,读取用 decodeURIComponent。JavaScript 能设置 HttpOnly 吗?不能。HttpOnly 的目的就是禁止脚本访问 Cookie,必须由服务端设置。写段代码function setCookie(name, value, maxAge, path = '/') { document.cookie = `${name}=${encodeURIComponent(value)}; Max-Age=${maxAge}; Path=${path}`;}function getCookie(name) { return document.cookie.split('; ').find(v => v.startsWith(name + '=')) ?.split('=').slice(1).join('=') || null;}function deleteCookie(name, path = '/') { document.cookie = `${name}=; Max-Age=0; Path=${path}`;}
服务端阅读 05月30日 01:00

Cookie 有哪些安全风险?如何防范 XSS、CSRF 和窃取?

Cookie 的主要风险有四类:XSS 读取 document.cookie、CSRF 借浏览器自动带 Cookie 发请求、HTTP 明文传输被中间人截获,以及 Domain/Path 配置过宽导致 Cookie 被子域或无关路径滥用。敏感 Cookie 应由服务端设置,并组合使用 HttpOnly、Secure、SameSite=Lax/Strict、短有效期和签名校验。注意:SameSite 能降低 CSRF 风险,但高风险写操作仍建议校验 CSRF Token 或 Origin/Referer。追问HttpOnly 能彻底防住 XSS 吗?不能。它只是禁止 JavaScript 读取 Cookie,不能阻止脚本执行,也不能阻止恶意脚本发起带 Cookie 的请求。SameSite=Lax 和 Strict 怎么选?普通登录态多用 Lax,兼顾外部链接跳转体验;支付、后台管理等高风险场景可用 Strict。Cookie 被篡改怎么办?不要信任客户端值。服务端应对 Cookie 做签名或只存 sessionId,真实权限状态放服务端。实际项目里最容易漏什么?SameSite=None 忘记配 Secure,浏览器会拒绝或异常处理;另一个坑是把主站 session 设置到 .example.com,导致子域风险放大。写段代码Set-Cookie: __Host-session=abc; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=3600
服务端阅读 05月30日 01:00

Cookie 浏览器兼容有哪些差异?如何处理?

Cookie 浏览器兼容差异主要集中在容量限制、SameSite 默认值、第三方 Cookie、隐私保护策略和过期时间。现在不能再假设“所有浏览器都会长期保存并发送 Cookie”:Safari 的 ITP、Firefox 的 ETP、Chrome 的第三方 Cookie 收紧都会影响跨站登录、埋点和广告归因。处理思路是默认一方 Cookie、SameSite=Lax、Secure 开启,跨站场景单独设计降级方案。追问各浏览器容量差异要怎么记?单个 Cookie 通常按 4KB 估算,每个域名数量上限各家不同。面试里不必背精确数字,关键是不要依赖“能存很多”,超过少量会话字段就该换存储方案。SameSite=None 为什么必须配 Secure?因为 SameSite=None 表示允许跨站发送 Cookie,风险更高。现代浏览器要求它只能在 HTTPS 下配合 Secure 使用,否则可能直接拒收。Safari ITP 对业务影响最大在哪里?跨站追踪、第三方登录态和长期归因最容易出问题。即使代码没变,Cookie 也可能被限制有效期或阻止发送,所以要用真实 Safari 和隐私模式测试。第三方 Cookie 被禁后怎么办?优先改成一方登录态;确实跨域时,用 OAuth 跳转、服务端会话、postMessage 或浏览器提供的新 API,而不是继续依赖隐藏 iframe 写 Cookie。写段代码document.cookie = 'sid=abc; Path=/; Secure; SameSite=Lax';// 跨站且必须携带时:document.cookie = 'sso=abc; Path=/; Secure; SameSite=None';
服务端阅读 05月30日 01:00

Cookie 的 Domain 和 Path 有什么作用?如何设置?

Cookie 的 Domain 决定哪些域名会收到它,Path 决定哪些路径会收到它。默认不写 Domain 时,Cookie 通常只属于当前 host;写成父域如 example.com 时,子域也可能共享。Path 是路径前缀匹配,不是权限隔离,主要用于减少无关请求携带 Cookie。正确做法是按最小范围设置,登录态、后台、支付、静态资源不要混用一个宽泛 Cookie。追问不写 Domain 和写 Domain 有什么区别?不写 Domain 更收敛,通常只给当前主机使用。写父域会扩大到子域,适合 SSO,但也扩大了泄露和冲突范围。现在前导点 .example.com 基本被忽略,关键是域本身是否合法。Path=/api 能防止前端 JS 读取 Cookie 吗?不能。Path 控制请求是否携带 Cookie,不是安全边界。要防 JS 读取,应设置 HttpOnly;要跨站防护,还要配合 SameSite 和 CSRF 方案。Path 的匹配规则是什么?它是前缀匹配。Path=/admin 会匹配 /admin 和 /admin/user,但设计时最好写清楚边界,避免 /admin-old 这类路径造成误判。实际项目怎么设置?会话 Cookie 尽量只给需要鉴权的域和路径;静态资源放到无 Cookie 域名;管理后台、支付、开放 API 分开命名和作用域,排查时也更清楚。写段代码Set-Cookie: sid=abc; Domain=example.com; Path=/; HttpOnly; Secure; SameSite=LaxSet-Cookie: admin=xyz; Path=/admin; HttpOnly; Secure; SameSite=Strict
服务端阅读 05月30日 01:00

Cookie 性能如何优化?怎样减少大小和数量?

Cookie 性能优化的核心是少带、少存、少匹配。因为同域请求会自动携带匹配 Cookie,Cookie 越多,请求头越大,移动网络和高并发接口越容易被拖慢。实战里优先把会话凭证保留在 Cookie,把偏好、缓存、草稿这类非敏感数据迁到 localStorage、sessionStorage 或 IndexedDB;同时收窄 Domain、Path,避免静态资源和无关接口也背着 Cookie 跑。追问Cookie 为什么会影响接口性能?每次 HTTP 请求都会带上匹配的 Cookie。即使只是请求一张图片,只要域名和路径匹配,也可能多传几 KB 请求头。HTTP/2 有 HPACK 压缩,但不能把设计不当的 Cookie 变成零成本。减少 Cookie 大小有哪些做法?只存必要字段,键名和值都尽量短;多个小偏好可以合并成一个值,但不要把大对象硬塞进去。超过 4KB 或频繁变化的数据不适合放 Cookie。Domain 和 Path 怎么配合性能优化?Domain 不要随手设成 .example.com,Path 也不要一律 /。例如后台令牌只给 admin.example.com 和 /admin,API Cookie 只给 /api,静态资源域名最好不设置 Cookie。项目里常见坑是什么?分析、AB 实验、灰度标记容易越积越多,最后每个请求都多带一串历史垃圾。上线前可以在网关或浏览器 DevTools 里看 Request Headers,超过 2KB 就该清理。写段代码function cookieBytes() { return new Blob([document.cookie]).size;}if (cookieBytes() > 2048) { console.warn('Cookie too large:', cookieBytes());}
服务端阅读 05月30日 01:00

iframe 如何用 postMessage 实现安全跨域通信?

iframe 跨域通信用 window.postMessage:父页拿到 iframe 的 contentWindow 发消息,子页用 window.parent.postMessage 回消息,双方用 message 事件接收。安全重点只有两句:发送时别乱用 *,接收时必须校验 event.origin 和 event.data,否则任何页面都可能伪造消息触发你的逻辑。追问postMessage 为什么能跨域?它是浏览器提供的受控跨文档通信 API,不直接暴露 DOM,只传递结构化克隆后的数据,所以绕开的是直接访问限制,不是绕开安全校验。targetOrigin 能不能写 *?只有广播类、完全不含敏感信息的消息才考虑 *。登录态、token、订单状态、支付结果都必须写明确源,例如 https://pay.example.com。接收消息时要校验什么?先校验 event.origin,再校验数据结构和 type 白名单。不要把 event.data 直接拼进 HTML,也不要让消息直接驱动跳转、支付、删除等高危操作。transfer 参数有什么用?它可以转移 MessagePort、ArrayBuffer 等对象所有权,适合大数据或双向通道。普通业务状态同步通常用不到。写段代码iframe.contentWindow.postMessage({ type: 'init' }, 'https://child.example');window.addEventListener('message', (event) => { if (event.origin !== 'https://child.example') return; if (!event.data || event.data.type !== 'resize') return; iframe.style.height = `${Number(event.data.height) || 0}px`;});
服务端阅读 05月30日 01:00

iframe 的 sandbox 有什么作用?如何安全配置权限?

iframe 的 sandbox 是给嵌入页面加一层浏览器级限制:默认禁止脚本、表单、弹窗、顶层跳转,并把内容放进独立的特殊源。安全配置的原则是先空 sandbox,再按功能逐项放开;最危险的组合是同源页面同时给 allow-scripts 和 allow-same-origin,子页面可能移除 sandbox 或访问同源能力。追问空 sandbox 和不写 sandbox 有什么区别?空 sandbox 是最严格模式;不写 sandbox 等于 iframe 按普通页面运行。第三方、不可信 HTML、广告位优先用空 sandbox。常用权限怎么选?只展示内容:sandbox;需要 JS:allow-scripts;需要提交表单:加 allow-forms;需要弹窗:加 allow-popups;需要跳转父页面,优先用 allow-top-navigation-by-user-activation,别直接用 allow-top-navigation。为什么 allow-scripts + allow-same-origin 危险?如果 iframe 和父页同源,脚本恢复后可能读写同源资源,甚至移除自身 sandbox。可信内部页可以用,不可信内容不要这么配。实际项目里还要配什么?配合 CSP 的 frame-src 限制可嵌入来源,并定期审查权限。sandbox 是能力限制,CSP 是来源限制,两者解决的问题不一样。写段代码<iframe src="https://third.example" sandbox></iframe><iframe src="https://widget.example" sandbox="allow-scripts allow-forms"></iframe>
服务端阅读 05月30日 00:37

iframe 对 SEO 有什么影响?如何优化才更友好?

iframe 对 SEO 的主要影响是:父页面不一定能继承 iframe 内内容的索引价值,搜索引擎可能把 iframe 地址当成独立页面处理;对百度等爬虫来说,重要内容放在 iframe 里更容易漏抓。优化原则很简单:核心内容不要只放 iframe;必须用时,加 title、fallback、可访问链接、结构化数据,并让 iframe 页面本身也具备可索引的标题、描述和正文。追问Google 能索引 iframe 内容吗?能,但不等于父页面拿到完整 SEO 收益。iframe 内容可能按独立 URL 理解,父页面的主题相关性和链接权重都可能被削弱。哪些内容不该放进 iframe?文章正文、商品详情、主导航、关键表单都不建议放。它们既影响索引,也影响无障碍、性能和用户路径分析。必须嵌第三方内容怎么办?给 iframe 写清楚 title,提供可点击的原始链接或 fallback 文案,视频类内容补 VideoObject 结构化数据,父页面保留摘要和上下文说明。性能会不会影响 SEO?会。iframe 多一个页面加载和脚本环境,容易拖慢 LCP。能懒加载就加 loading="lazy",首屏关键内容不要依赖 iframe。写段代码<iframe src="https://example.com/video" title="产品演示视频" loading="lazy"> <a href="https://example.com/video">查看产品演示视频</a></iframe>
服务端阅读 05月30日 00:37

iframe、object、embed 和 AJAX 有什么区别?如何选择?

iframe 适合嵌入完整页面,object/embed 更适合 PDF、插件式媒体,AJAX 适合把自有内容拉回当前页面渲染。选择时先看三个问题:是不是跨域完整网页?要不要样式和脚本隔离?内容是否需要被搜索引擎直接索引?跨域、强隔离选 iframe;自有内容和 SEO 优先选 AJAX/服务端渲染;PDF 等文件优先 object,embed 只适合简单媒体嵌入。追问iframe 和 AJAX 最大区别是什么?iframe 会创建独立浏览上下文,父页面只能通过 postMessage 等方式通信;AJAX 把数据或 HTML 放进当前 DOM,样式、脚本、SEO 都由主页面控制。object 和 embed 还值得用吗?object 值得保留,尤其是 PDF、SVG、媒体文件,并且能写 fallback。embed 更轻,但缺少 fallback,通常不是首选。为什么第三方视频和地图常用 iframe?因为它能跨域加载完整页面,并把第三方脚本、样式和安全边界隔开。缺点是性能开销更高,SEO 也不如直接渲染。项目里怎么判断用哪种?核心内容、商品详情、文章正文不要放 iframe;第三方内容、广告、地图、独立小应用可以用 iframe;组件复用和样式隔离优先考虑 Web Components 或 Shadow DOM。写段代码<iframe src="https://example.com/widget" title="第三方组件" loading="lazy" sandbox="allow-scripts allow-same-origin"></iframe>
服务端阅读 05月30日 00:37

useQuery 和 useMutation 有什么区别?分别适合什么场景?

useQuery 用来读数据,useMutation 用来改数据。读数据通常自动执行、会按 queryKey 缓存,并处理 loading、error、重试和后台刷新;改数据不会因为组件渲染就自动执行,需要调用 mutate,成功后通常 invalidateQueries 让相关列表重新获取,或者做乐观更新。追问为什么 useMutation 不像 useQuery 那样自动缓存?因为写操作的重点不是展示“这次提交的返回值”,而是让服务器状态发生变化。变化后要么让相关 query 失效重新拉取,要么手动更新缓存。提交表单应该用哪个?用 useMutation。表单提交是创建或更新资源,应该由用户动作触发,而不是组件一挂载就执行。删除一条数据后列表怎么刷新?常见做法是在 onSuccess 里调用 queryClient.invalidateQueries,让列表 query 重新获取。体验要求高时可以先做乐观删除,失败再回滚。queryKey 在 useQuery 里为什么重要?它决定缓存身份。列表、详情、筛选条件都应该进 queryKey,否则容易拿到旧数据或串数据。写段代码const todos = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });const addTodo = useMutation({ mutationFn: createTodo, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }); },});addTodo.mutate({ title: 'learn query' });
服务端阅读 05月30日 00:37

React Query 和 Redux 有什么区别?什么时候该用 React Query?

React Query 管服务器状态,Redux 管客户端状态。服务器状态来自后端,会过期、要缓存、要重试、要和接口同步;客户端状态只存在前端,比如弹窗开关、主题、复杂表单流程。项目里常见做法不是二选一,而是用 React Query 处理接口数据,用 Redux、Zustand 或 Context 处理真正的全局 UI 状态。追问React Query 能替代 Redux 吗?只能替代一大部分“把接口数据塞进 Redux”的写法。购物车草稿、权限后的菜单展开状态、跨页面编辑流程这类客户端状态,React Query 不负责。为什么接口数据不适合直接放 Redux?你要自己写 loading、error、缓存过期、重复请求合并、重试和失效逻辑。React Query 把这些变成默认能力,代码量少很多。什么时候还应该用 Redux?当状态更新规则复杂、多个模块共享同一份前端状态、需要严格可追踪的状态变更时,Redux 仍然合适。已经重度使用 Redux Toolkit 的项目,也可以考虑 RTK Query。两者一起用会不会混乱?不会,边界划清就行:接口返回的数据归 React Query,用户在页面上的交互状态归客户端状态库。最怕的是同一份数据两边各存一份。写段代码const { data: user } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id),});const theme = useSelector(state => state.ui.theme);
服务端阅读 05月30日 00:37

React Query 有哪些高级特性?依赖查询和并行查询怎么用?

React Query 的高级特性主要解决三个问题:按条件发请求、同时发多个请求、让缓存数据在合适时机变新。依赖查询用 enabled 控制执行时机;并行查询用多个 useQuery 或 useQueries;窗口聚焦重新获取用 refetchOnWindowFocus 保证页面回来时数据不过期。回答时要补一句:这些能力都围绕服务器状态,不是替代本地 UI 状态。追问依赖查询为什么不用 if 包住 useQuery?Hook 不能放进条件语句里,否则调用顺序会变。正确做法是始终调用 useQuery,用 enabled: !!id 控制是否真正请求。多个 useQuery 和 useQueries 怎么选?数量固定时直接写多个 useQuery,可读性更好。数量来自数组时用 useQueries,例如按一批 id 拉详情。refetchOnWindowFocus 会不会导致请求太多?会,所以后台管理页、实时看板适合开启;编辑表单、低频静态数据可以关闭,或配合 staleTime 降低重复请求。无限滚动靠什么实现?用 useInfiniteQuery,关键是 getNextPageParam 返回下一页参数,返回 undefined 表示没有更多。写段代码const user = useQuery({ queryKey: ['user', id], queryFn: getUser });const posts = useQuery({ queryKey: ['posts', user.data?.id], queryFn: () => getPosts(user.data.id), enabled: !!user.data?.id, staleTime: 60_000, refetchOnWindowFocus: true,});const results = useQueries({ queries: ids.map(id => ({ queryKey: ['todo', id], queryFn: () => getTodo(id) }))});