面试题手册

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

服务端阅读 02026年5月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>
服务端阅读 02026年5月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>
服务端阅读 02026年5月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' });
服务端阅读 02026年5月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);
服务端阅读 02026年5月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) }))});
服务端阅读 02026年5月30日 00:37

React Query 如何处理错误和重试?哪些错误不该重试?

React Query 的错误处理要分两层:组件里展示用户能看懂的错误,全局里做日志和兜底通知。重试不要一刀切,网络抖动和 5xx 可以重试,401、403、404、表单校验这类 4xx 通常不该重试;否则只是把错误请求重复打到服务端。追问retry 默认是几次?浏览器端查询默认会重试 3 次,并使用退避延迟;服务端渲染场景通常不建议重试太多,否则会拖慢响应。retry 和 refetch 有什么区别?retry 是一次请求失败后的自动补救,refetch 是用户或程序主动重新拉取。错误页里的“再试一次”通常调用 refetch 或 reset error boundary。Mutation 失败要怎么处理?如果做了乐观更新,onMutate 里先保存旧数据,onError 里回滚,再提示用户。不要只弹 toast,却让缓存停在错误状态。全局 onError 适合做什么?适合上报 Sentry、统一 toast、处理登录过期。具体页面文案仍应在业务组件里判断。写段代码useQuery({ queryKey: ['todos'], queryFn: getTodos, retry: (count, error: any) => { if ([400, 401, 403, 404].includes(error.status)) return false; return count < 3; }, retryDelay: i => Math.min(1000 * 2 ** i, 30_000),});
服务端阅读 02026年5月30日 00:37

React Query 如何与 Suspense 集成?错误边界怎么处理?

React Query 接入 Suspense 后,加载状态交给 Suspense fallback,错误交给 Error Boundary。实际项目里优先用 useSuspenseQuery,而不是在每个组件里判断 isLoading。注意:开启 Suspense 不代表不用管错误,网络失败会抛给最近的错误边界;如果要让“重试”按钮生效,还要配 QueryErrorResetBoundary。追问useSuspenseQuery 和 useQuery 有什么区别?useSuspenseQuery 成功返回时 data 一定有值,加载中会挂起组件。你少写了 loading 分支,但必须准备好 Suspense 和错误边界。Error Boundary 应该放在哪里?放在页面块级别通常最合适。太外层会导致整页白掉,太内层又会让错误处理重复。有缓存时还会进入 fallback 吗?已有可用缓存时通常直接渲染;queryKey 改变触发新请求时,可能再次 fallback。交互更新可用 startTransition 降低界面闪烁。SSR 里能直接用吗?要谨慎。Next.js 等场景通常配合预取、dehydrate/hydrate,或使用框架推荐的 Suspense 数据方案。写段代码<QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <button onClick={resetErrorBoundary}>重试</button> )}> <Suspense fallback={<Spinner />}> <UserPanel /> </Suspense> </ErrorBoundary> )}</QueryErrorResetBoundary>
服务端阅读 02026年5月30日 00:37

React Query 项目中如何组织查询和命名 query key?

项目里不要把 useQuery 散落在组件里。更稳的做法是按业务域组织:API 函数只负责请求,自定义 Hook 负责暴露查询,query key 工厂负责命名和失效。query key 用数组,从宽到窄写,比如 ['users', 'detail', id];凡是 queryFn 依赖的参数都必须进 key,否则缓存可能串数据。追问为什么不建议直接写字符串 key?字符串很难表达层级,也不方便批量失效。数组 key 可以让你失效整个用户域、某个详情页,或某个筛选列表。API 函数和 Hook 为什么要分开?API 函数可独立测试,也能被预取、SSR、mutation 复用。Hook 里再放 staleTime、enabled、select 等 React Query 配置。项目结构按 api、hooks 分,还是按 feature 分?小项目都可以;中大型项目更推荐 feature 分组,例如 features/user/api.ts、queries.ts、keys.ts,修改用户模块时不用跨目录找文件。命名上最容易踩什么坑?列表和详情共用一个 key、筛选条件没放进 key、mutation 成功后失效范围太大。这些都会造成脏数据或无意义重请求。写段代码export const userKeys = { all: ['users'] as const, detail: (id: string) => [...userKeys.all, 'detail', id] as const,};export function useUser(id: string) { return useQuery({ queryKey: userKeys.detail(id), queryFn: () => getUser(id), staleTime: 60_000, });}
服务端阅读 02026年5月30日 00:37

什么是 WebRTC?它的核心组成部分有哪些?

WebRTC 是浏览器里的实时音视频和数据通信能力,核心价值是不用插件就能让两个端建立低延迟连接。它主要由三块组成:媒体采集用 getUserMedia 拿摄像头、麦克风;连接与协商用 RTCPeerConnection 处理 SDP、ICE、加密和媒体传输;任意数据传输用 RTCDataChannel。另外要记住:WebRTC 不自带信令服务,房间、呼叫、offer/answer 和 candidate 交换通常由业务用 WebSocket 或 HTTP 自己实现。追问WebRTC 是完全点对点吗?不一定。能直连时媒体可以 P2P;直连失败会走 TURN 中继;多人会议通常还会用 SFU 转发媒体流,所以“WebRTC 等于 P2P”这个说法不准确。RTCPeerConnection 具体负责什么?它负责创建 offer/answer、管理 ICE 候选、建立 DTLS/SRTP 安全传输,并把本地媒体轨道发送给对端。简单说,它是 WebRTC 连接的核心对象。信令为什么不算 WebRTC 标准的一部分?因为不同业务的房间模型、鉴权、重连和消息格式差异很大。标准只规定浏览器如何生成 SDP 和 candidate,至于怎么传给对端,由业务决定。getUserMedia 和 RTCDataChannel 有什么区别?getUserMedia 采集音视频流,适合通话、录制、屏幕共享。RTCDataChannel 传任意数据,适合聊天、白板同步、文件分片或游戏状态同步。写段代码const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })const pc = new RTCPeerConnection({ iceServers })stream.getTracks().forEach(track => pc.addTrack(track, stream))const channel = pc.createDataChannel('chat')
服务端阅读 02026年5月30日 00:37

WebRTC 如何实现 NAT 穿透?STUN、TURN 和 ICE 分别做什么?

WebRTC 的 NAT 穿透不是靠某一个协议硬打洞,而是由 ICE 统一调度:先收集本地地址,再用 STUN 拿到公网映射地址,最后在直连失败时走 TURN 中继。面试里可以这样答:STUN 负责“我在公网看起来是谁”,TURN 负责“直连不通时替我转发”,ICE 负责“把所有候选路径试一遍,选能通且质量最好的”。生产环境只配 STUN 不够,遇到对称 NAT、企业防火墙或 UDP 被限制时,必须有 TURN 兜底。追问STUN 和 TURN 有什么区别?STUN 只参与建连阶段,帮助发现公网 IP 和端口,媒体流通常仍然点对点传输。TURN 会一直在媒体路径上转发数据,成功率高,但延迟、带宽成本和服务器压力都更大。ICE 候选者有哪些?常见有 host、srflx、relay 三类。host 是本机局域网地址,srflx 是 STUN 得到的公网映射地址,relay 是 TURN 分配的中继地址。为什么有 STUN 还需要 TURN?因为 STUN 依赖 NAT 映射可预测。对称 NAT 或严格防火墙下,对端无法直接打到这个映射端口,TURN 中继就成了最后的可用路径。实际项目里怎么配置?至少配置一个 STUN 和一个带鉴权的 TURN,TURN 建议靠近用户部署。监控里要看 relay 占比、ICE 失败率、连接耗时和 RTT,relay 占比突然升高通常说明网络或 STUN 可用性出问题。写段代码const pc = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'turn:turn.example.com:3478', username: 'u', credential: 'p' } ]})pc.onicecandidate = e => sendToPeer(e.candidate)