Skip to content

RESTful、Git、SEO、HTTP 协议、缓存、Nginx ​

作者:青见春山

杂七杂八的一些知识点


REST(REpresentational State Transfer,表述性状态转移)是由 Roy Fielding 在其 2000 年的博士论文中提出的一种软件架构风格,RESTful 即"符合 REST 原则的" Web 服务设计方式。


一、核心概念 ​

1. 资源(Resource) ​

REST 的核心思想是一切皆资源。每个资源用一个唯一的 URI(统一资源标识符)来标识。

Plain
/users          → 用户集合
/users/123      → ID 为 123 的用户
/users/123/orders → 该用户的订单集合

Image

2. 表述(Representation) ​

资源本身是抽象的,客户端拿到的是资源的表述(如 JSON、XML、HTML 等):

JSON
{
  "id": 123,
  "name": "张三",
  "email": "zhangsan@example.com"
}

3. 状态转移(State Transfer) ​

客户端通过 HTTP 动词(GET、POST、PUT、DELETE 等)来操作资源的状态。

Image

Image

Image

Image

不是协议,不是标准,不是强制规范,只是一种建议的设计风格


二、六大约束 ​

约束说明
客户端-服务器前后端分离,关注点分离
无状态每次请求包含所有必要信息,服务端不保存会话状态
可缓存响应可标记为可缓存或不可缓存
统一接口核心约束,下面详细展开
分层系统客户端无需关心是否直连服务器,中间可以有负载均衡、代理等
按需代码(可选)服务端可向客户端发送可执行代码(如 JS)

Image

Image


三、统一接口(REST 最核心的特征) ​

统一接口包含四个子约束:

1. 资源标识:URI ​

每个资源用唯一且一致的 URI 标识,URI 只描述名词,不描述动作。

Image

✅ 推荐❌ 不推荐
GET /users/123GET /getUser?id=123
DELETE /users/123POST /deleteUser

2. 资源表述:通过 HTTP 消息交换 ​

请求和响应中使用标准的表述格式(JSON、XML 等),通过 Content-Type 和 Accept 头协商。

Image

Image

Image

3. 自描述消息 ​

每个请求/响应包含足够的信息来说明如何处理它:

  • Content-Type: application/json

  • HTTP/1.1 200 OK

  • Cache-Control: no-cache

4. HATEOAS(超媒体即应用状态引擎) ​

客户端通过服务器返回的链接来发现可用操作:

JSON
{
  "id": 123,
  "name": "张三",
  "links": [
    { "rel": "self", "href": "/users/123", "method": "GET" },
    { "rel": "edit", "href": "/users/123", "method": "PUT" },
    { "rel": "orders", "href": "/users/123/orders", "method": "GET" }
  ]
}

Image

Image

所谓无状态就是每次都要带上所有信息,比如用户登录后,不是服务端记住状态,而是每次发请求客户端携带凭证,每个请求都是独立的,这样可以加多服务器,实现负载均衡

服务端:

Image

Image

Image

Image

Image


Image

四、HTTP 方法与 CRUD 映射 ​

HTTP 方法语义CRUD 操作幂等安全
GET获取资源Read✅✅
POST创建资源Create❌❌
PUT全量替换资源Update✅❌
PATCH部分更新资源Update❌❌
DELETE删除资源Delete✅❌
  • 幂等性:多次请求结果一致(如 GET、DELETE 多次调用结果相同)

  • 安全性:不修改服务端资源状态(只有 GET 是安全的)


五、HTTP 状态码规范 ​

范围含义常用示例
2xx成功200 OK、201 Created、204 No Content
3xx重定向301 Moved Permanently、304 Not Modified
4xx客户端错误400 Bad Request、401 Unauthorized、403 Forbidden、404 Not Found、422 Unprocessable Entity
5xx服务端错误500 Internal Server Error、502 Bad Gateway、503 Service Unavailable

六、实践要点 ​

URI 设计规范 ​

Plain
✅ /users                  → 用户集合
✅ /users/123              → 单个用户
✅ /users/123/orders       → 用户的订单子资源
✅ /users/123/orders/456   → 某个具体订单

❌ /users/123/get          → 不要用动词
❌ /GetUser                → 不要用大驼峰
❌ /user_list              → 不要用下划线连接

版本控制 ​

Plain
https://api.example.com/v1/users
https://api.example.com/v2/users

过滤、分页、排序 ​

Plain
GET /users?status=active&page=1&per_page=20&sort=-created_at

请求/响应头 ​

Plain
Content-Type: application/json
Accept: application/json
Authorization: Bearer <token>

七、RESTful vs 其他风格 ​

特性RESTGraphQLgRPC
协议HTTP/1.1HTTPHTTP/2
数据格式JSON/XMLJSONProtobuf
接口定义无强制Schema 强类型.proto 文件
适用场景通用 Web API复杂查询/移动端高性能微服务

总结 ​

RESTful 的本质是:

用 URI 命名资源,用 HTTP 方法表达操作,用状态码描述结果,保持无状态通信。

它不是一种协议或标准,而是一种设计哲学——通过统一接口、无状态、资源导向等约束,构建可伸缩、可维护的 Web 服务架构。

Image

实现这种风格restful

Image

ai写,生成接口文档

Image

Vibe coding ​

Image

git操作 ​

Image

Image

seo优化,搜索引擎优化 ​

Image

1. "什么是SEO?" ​

SEO 是通过优化网站的技术架构、页面内容和外部权威性,提升网站在搜索引擎自然结果中的排名,从而获取精准流量的系统方法。它不是一次性的操作,而是需要持续优化的长期策略。


2. "SEO 分为哪几类?" ​

分为三类:技术SEO(确保搜索引擎能抓取和索引,比如网站速度、移动端适配、结构化数据);页面SEO(优化页面本身,比如标题标签、关键词布局、内容质量);站外SEO(建立外部权威性,比如外链建设、品牌曝光)。


3. "怎么做关键词研究?" ​

首先通过工具(如 Google Keyword Planner、Ahrefs)挖掘目标关键词,然后从搜索量、竞争难度、搜索意图三个维度评估,优先选择搜索意图匹配且竞争度适中的词。特别重视长尾关键词,因为竞争小、转化率高。最后将关键词合理布局到标题、正文、URL 和图片 alt 中。


4. "影响SEO排名的主要因素有哪些?" ​

主要包括:内容质量与相关性(E-E-A-T 标准)、外链的质量和数量、用户体验指标(Core Web Vitals)、移动端适配、网站技术架构(爬取性、索引性)、搜索意图匹配度。其中内容和外链是最重要的两个因素。


5. "SEO 多久能见效?" ​

通常 3-6 个月可以看到明显效果,竞争激烈的行业可能更久。这取决于网站基础、行业竞争程度、内容产出频率和外链建设速度。我会先做技术审计快速修复明显问题,同时制定中长期内容和外链策略。


6. "如何做外链建设?" ​

核心原则是质量优于数量。主要方法:一是创建可链接资产(原创数据、深度指南、工具);二是客座文章,在行业权威网站发布内容;三是竞品外链分析,挖掘竞品的外链来源;四是断链建设,为别人的失效链接提供替代内容。坚决避免购买链接等黑帽手段。


7. "你用过哪些SEO工具?" ​

  • Google Search Console:监控索引状态、发现技术问题、查看搜索表现
  • Google Analytics 4:分析流量和用户行为
  • Ahrefs/SEMrush:关键词研究、竞品分析、外链分析
  • Screaming Frog:技术审计,爬取网站发现SEO问题
  • PageSpeed Insights:检测页面速度和 Core Web Vitals

robots.txt 和 Sitemap 的作用 ​

robots.txt ​

一句话:告诉搜索引擎爬虫"哪些能爬,哪些不能爬"。

它是一个放在网站根目录下的文本文件(如 https://example.com/robots.txt),搜索引擎爬虫访问网站时首先读取它。

示例: ​

Plain
User-agent: *
Disallow: /admin/
Disallow: /private/
Allow: /public/
Sitemap: https://example.com/sitemap.xml

常见用途: ​

用途示例
禁止爬取后台页面Disallow: /admin/
禁止爬取敏感目录Disallow: /private/
禁止爬取某些文件Disallow: *.pdf$
允许特定爬虫User-agent: Googlebot + Allow: /
全站允许User-agent: * + Disallow:(空值表示全部允许)

⚠️ 注意事项: ​

  • robots.txt 是"君子协议",不是安全机制,恶意爬虫可以忽略它

  • 不要用它来隐藏敏感信息(应该用服务器权限控制)

  • 错误配置可能导致整站被搜索引擎屏蔽


Sitemap(站点地图) ​

一句话:给搜索引擎提供一份"网站所有重要页面的清单"。

它是 XML 格式的文件(如 https://example.com/sitemap.xml),列出网站中希望被搜索引擎索引的页面。

示例: ​

XML
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com/</loc>
    <lastmod>2026-05-14</lastmod>
    <changefreq>daily</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>https://example.com/seo-guide</loc>
    <lastmod>2026-05-10</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.8</priority>
  </url>
</urlset>

核心字段说明: ​

字段作用
<loc>页面完整 URL
<lastmod>最后修改日期
<changefreq>内容更新频率(daily/weekly/monthly)
<priority>页面重要程度(0.0-1.0)

Sitemap 的价值: ​

  • 帮助搜索引擎发现新页面(尤其是深层页面)

  • 告诉搜索引擎哪些页面最重要

  • 加速页面被收录的速度

  • 对大型网站(数千页面)尤其重要


两者的关系 ​

Plain
搜索引擎爬虫访问网站
        │
        ▼
  读取 robots.txt ──→ 确定能爬哪些目录/页面
        │
        ▼
  根据 Sitemap ──→ 知道有哪些重要页面需要爬取
        │
        ▼
  开始抓取页面内容并建立索引

简单类比:

  • robots.txt = 门卫,告诉你"哪些房间能进,哪些不能进"

  • Sitemap = 建筑平面图,告诉你"这栋楼有哪些房间,分别在哪里"

两者配合使用,是技术 SEO 的基础配置。

offset,client,scroll ​

Image

Image

HTTP 协议基础详解 ​

一、HTTP 的定义 ​

HTTP(HyperText Transfer Protocol,**超文本传输协议**) 是一种用于分布式、协作式和超媒体信息系统的应用层协议。它基于请求-响应模型:

Plain
客户端 (Client)  ----  HTTP 请求 (Request)  ---->  服务器 (Server)
客户端 (Client)  <---  HTTP 响应 (Response) ----  服务器 (Server)
  • 客户端:通常是浏览器,也可以是任何发送 HTTP 请求的程序(如 curl、Postman、移动端 App)

  • 服务器:接收请求并返回响应的 Web 服务器(如 Nginx、Apache、Node.js 服务)

一次完整的 HTTP 交互由请求报文和响应报文组成:

HTTP
-- 请求报文示例 --
GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: text/html

-- 响应报文示例 --
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 1234

<html>...</html>

二、HTTP 是无状态协议 ​

无状态(Stateless) 意味着:服务器不会保存任何关于客户端的历史请求信息。每一次请求都是独立的,服务器不知道两次请求是否来自同一个客户端。

为什么这样设计? ​

  • 简化服务器设计,不需要维护会话状态

  • 提高可扩展性,请求可以被任意服务器处理

  • 降低服务器内存消耗

如何实现状态跟踪? ​

正因为无状态带来了很多业务场景的不便(如登录态),所以发展出了多种"状态管理"手段:

技术原理存储位置
Cookie服务器通过 Set-Cookie 响应头在客户端存储数据,后续请求自动携带客户端(浏览器)
Session服务器端存储会话数据,通过 Cookie 中的 Session ID 关联服务器端
Token(JWT等)服务端签发令牌,客户端每次请求在 Header 中携带客户端
URL 参数将状态信息放在 URL 查询参数中URL 中

Token(令牌) 是一个 广义概念 ,泛指用于身份验证和授权的凭证字符串。

JWT 是 Token 的一种具体实现形式。所有的 JWT 都是 Token,但不是所有的 Token 都是 JWT。

双token机制:

Access Token(JWT) → 短有效期(15分钟-2小时),用于身份验证

Refresh Token(随机字符串) → 长有效期(7天-30天),用于刷新 Access Token

  • Access Token 用 JWT :无状态验证,性能好,适合频繁请求

  • Refresh Token 用随机字符串 :需要存储,可以随时吊销(如用户退出登录、密码修改后强制下线)

  • JWT 的实现过程可以概括为: 用户登录成功后,服务端将用户标识信息(如用户ID、角色等)放入 Payload,连同固定的 Header(声明算法类型)一起进行 Base64Url 编码,再使用服务端密钥(secret)通过指定算法(如 HS256)对编码后的 Header 和 Payload 进行签名,最终将编码后的 Header、Payload、Signature 三段用 . 拼接生成 JWT 字符串返回给客户端;客户端收到后将其存储(通常在 localStorage 或 Cookie 中),并在后续每次请求时通过 Authorization: Bearer <token> 请求头携带发送给服务端;服务端收到请求后,先按 . 拆分三段,对 Header 和 Payload 重新计算签名并与传入的 Signature 对比,验证通过则说明令牌未被篡改,再检查 Payload 中的过期时间(exp)是否有效,验证全部通过后直接从 Payload 中提取用户信息用于业务处理,整个过程无需查询数据库,实现无状态认证。

Plain
Cookie 工作流程:

客户端                          服务器
  |---- POST /login ----------->|  登录成功
  |<--- Set-Cookie: sid=abc123--|  下发 Cookie
  |                             |
  |---- GET /user  ------------>|  携带 Cookie
  |     Cookie: sid=abc123      |  识别用户
  |<--- 200 OK (用户信息) ------|

三、HTTP 协议的演进 ​

HTTP/1.0(1996 年) ​

核心特点:

  • 每次请求都需要建立一个新的 TCP 连接,请求完成后立即断开

  • 引入了 Content-Type 头部,支持传输非 HTML 内容

  • 引入了状态码体系

  • 支持 GET、POST、HEAD 方法

Plain
客户端                服务器
  |---- TCP 连接 ---->|  建立连接
  |---- GET /a ------>|  请求资源
  |<--- 200 OK ------|  响应
  |---- TCP 断开 ---->|  关闭连接

  |---- TCP 连接 ---->|  重新建立连接
  |---- GET /b ------>|  请求资源
  |<--- 200 OK ------|  响应
  |---- TCP 断开 ---->|  关闭连接

问题:每个资源都需单独建立 TCP 连接,开销大、效率低。


HTTP/1.1(1997 年,使用最广泛的版本) ​

核心改进:

  1. 持久连接(Keep-Alive)默认开启

    • 一个 TCP 连接可以发送多个请求和响应,不用每次都建立新连接
    Plain
    客户端                服务器
      |---- TCP 连接 ---->|  建立连接(一次)
      |---- GET /a ------>|
      |<--- 200 OK ------|
      |---- GET /b ------>|  同一连接复用
      |<--- 200 OK ------|
      |---- GET /c ------>|
      |<--- 200 OK ------|
      |---- TCP 断开 ---->|  最终关闭
  2. 管线化(Pipelining)

    • 客户端可以在不等待响应的情况下连续发送多个请求

    • 但服务器必须按序响应(队头阻塞问题)

    Plain
    客户端发出:  req1 -> req2 -> req3
    服务器返回:  res1 -> res2 -> res3 (必须按顺序)

    实际上由于实现复杂、队头阻塞等问题,很多浏览器默认不启用管线化。

  3. Host 头部(虚拟主机)

    • 同一 IP 的服务器可以托管多个域名

    • Host: www.example.com 成为必传头

  4. 新增请求方法:PUT、DELETE、OPTIONS、TRACE、CONNECT

  5. 分块传输编码(Chunked Transfer Encoding)

    • 服务器可以分块发送响应,不需要提前知道 Content-Length
  6. 更丰富的缓存机制

    • 引入 Cache-Control、ETag、If-None-Match 等头部

HTTP/2(2015 年) ​

核心改进:

  1. 二进制分帧层(Binary Framing)

    • HTTP/1.x 是纯文本协议,HTTP/2 将数据分割为更小的二进制帧

    • 更易于解析,更高效

    Plain
    HTTP/1.1: "GET /index.html HTTP/1.1\r\nHost: ..."  (纯文本)
    
    HTTP/2:  [帧头][帧数据] [帧头][帧数据] [帧头][帧数据]  (二进制帧)
  2. 多路复用(Multiplexing)

    • 最关键改进:一个 TCP 连接上可以并发传输多个请求和响应

    • 彻底解决了 HTTP/1.1 的队头阻塞问题(应用层)

    • 每个请求/响应通过 Stream ID 标识,互不干扰

    Plain
    一个 TCP 连接:
    Stream 1: |--req--|----------|--res--|
    Stream 2: |---req---|-----|----res----|
    Stream 3: |--req--|----res----|
    帧交错发送,不需要等待前一个完成
  3. 头部压缩(HPACK)

    • 使用 HPACK 算法压缩 HTTP 头部

    • 维护一个头部字段索引表,避免重复传输相同头部

    • 大幅减少头部开销(尤其是 Cookie 等重复信息)

  4. 服务端推送(Server Push)

    • 服务器可以主动将客户端需要的资源"推送"过去

    • 例如:客户端请求 index.html,服务器主动推送 style.css、script.js

    Plain
    客户端: GET /index.html
    服务器: PUSH_PROMISE /style.css    (主动推送)
    服务器: PUSH_PROMISE /script.js    (主动推送)
    服务器: 200 /index.html
    服务器: 200 /style.css
    服务器: 200 /script.js
  5. 流优先级(Stream Priority)

    • 客户端可以为不同的流设置优先级和依赖关系

注意:HTTP/2 在应用层解决了队头阻塞,但 TCP 层面的队头阻塞依然存在(一个 TCP 包丢失会阻塞所有流)。这正是 HTTP/3 要解决的问题。

Image


HTTP/3(基于 QUIC,2022 年标准化) ​

核心改进:

  1. 底层传输协议从 TCP 改为 QUIC(基于 UDP)

    Plain
    HTTP/1.1 & HTTP/2:  HTTP → TCP → IP
    HTTP/3:             HTTP → QUIC (UDP) → IP
  2. 彻底解决队头阻塞

    • QUIC 在传输层也支持多路复用

    • 单个流的数据包丢失不会影响其他流

  3. 更快的连接建立

    • TCP + TLS 需要 2-3 个 RTT(往返时间)

    • QUIC 将传输层握手和 TLS 握手合并,首次连接仅需 1 个 RTT

    • 0-RTT 恢复:已连接过的客户端可以 0 延迟恢复连接

  4. 连接迁移

    • TCP 连接基于四元组(源IP、源端口、目标IP、目标端口)

    • 网络切换(如 WiFi → 4G)会导致连接断开

    • QUIC 使用连接 ID 标识连接,网络切换时连接不中断

  5. 内置 TLS 1.3 加密

    • 加密不再是可选项,所有 QUIC 连接都默认加密

版本对比总结 ​

特性HTTP/1.0HTTP/1.1HTTP/2HTTP/3
连接方式短连接持久连接多路复用多路复用
传输格式文本文本二进制帧二进制帧
队头阻塞存在存在(严重)TCP层仍存在彻底解决
头部压缩无无HPACKQPACK
服务端推送不支持不支持支持支持
底层协议TCPTCPTCPQUIC(UDP)
连接建立0 RTT0 RTT1-2 RTT(含TLS)0-1 RTT
连接迁移不支持不支持不支持支持

四、常见 HTTP 状态码 ​

状态码是服务器对请求结果的反馈,由三位数字组成,按首位分类:

1xx —— 信息性状态码 ​

状态码含义
100 Continue服务器已收到请求头,客户端应继续发送请求体
101 Switching Protocols服务器同意切换协议(如升级到 WebSocket)
103 Early Hints预加载资源提示

2xx —— 成功 ​

状态码含义使用场景
200 OK请求成功最常见的成功状态
201 Created资源已创建POST 创建资源后返回
204 No Content成功但无响应体DELETE 删除成功后返回
206 Partial Content部分内容(范围请求)断点续传、视频分段加载

3xx —— 重定向 ​

状态码含义使用场景
301 Moved Permanently永久重定向永久更换域名/URL,搜索引擎更新索引
302 Found临时重定向临时跳转(语义上应该用 GET)
303 See Other用 GET 请求另一个 URLPOST 后重定向到结果页
304 Not Modified资源未修改,使用缓存缓存验证成功
307 Temporary Redirect临时重定向(保持原请求方法)POST 临时重定向,不会降级为 GET
308 Permanent Redirect永久重定向(保持原请求方法)永久重定向且保持方法不变

301 vs 302:301 是永久性重定向,浏览器会缓存,下次直接请求新地址;302 是临时的,每次都要先请求原地址。

302 vs 307:302 允许浏览器将 POST 降级为 GET,307 严格保持原方法不变。

4xx —— 客户端错误 ​

状态码含义使用场景
400 Bad Request请求格式错误参数校验失败、JSON 格式错误
401 Unauthorized未认证(需要登录)缺少或无效的认证凭证(Token/Cookie)
403 Forbidden已认证但无权限已登录但没有访问该资源的权限
404 Not Found资源不存在URL 错误或资源已被删除
405 Method Not Allowed请求方法不支持对只读资源发送 DELETE 请求
408 Request Timeout请求超时客户端发送请求太慢
409 Conflict资源冲突创建已存在的资源、版本冲突
413 Payload Too Large请求体过大上传文件超过限制
429 Too Many Requests请求频率超限触发限流/反爬

401 vs 403:401 表示"你是谁?我不知道",应该先登录;403 表示"我知道你是谁,但你没权限"。

5xx —— 服务器错误 ​

状态码含义使用场景
500 Internal Server Error服务器内部错误代码 bug、未捕获的异常
502 Bad Gateway网关/代理收到上游无效响应Nginx 反代后端服务挂掉
503 Service Unavailable服务暂不可用服务器过载或维护中
504 Gateway Timeout网关超时上游服务器响应太慢

500 vs 502 vs 503:

  • 500:代码本身出错了(NPE、数据库连接失败等)
  • 502:Nginx/Apache 等代理服务器无法连接后端应用服务器
  • 503:服务临时不可用(过载、维护),通常可重试恢复

五、HTTP 请求方法 ​

方法语义幂等性有请求体说明
GET获取资源✅ 幂等通常无获取数据,可缓存、可收藏;请求内容没有通用语义
POST按目标资源语义处理内容不保证幂等通常有常用于提交或创建;满足显式条件时也可缓存
PUT全量更新资源✅ 幂等✅ 有完整替换目标资源
PATCH应用部分修改取决于补丁语义✅ 有幂等性由补丁格式和接口设计决定
DELETE删除资源✅ 幂等可选删除指定资源
HEAD获取响应头✅ 幂等❌ 无同 GET 但不返回响应体,用于检测资源是否存在
OPTIONS获取支持的方法✅ 幂等❌ 无CORS 预检请求会用到
TRACE回显请求✅ 幂等❌ 无用于诊断,生产中通常禁用
CONNECT建立隧道连接--用于 HTTPS 代理

幂等性:同一个请求执行一次和执行多次的效果相同。GET 幂等(多次读同一数据结果一致),POST 不幂等(多次提交可能创建多个资源)。

GET vs POST 的关键区别 ​

特性GETPOST
参数位置URL 查询参数 ?key=value请求体 (Body)
参数长度URL 上限由客户端、服务器与中间设施决定,没有通用 2KB/8KB 标准请求体同样受各层配置限制,不是无限
可缓存规范允许满足显式缓存条件时允许,但实践中较少
可收藏为书签✅❌
浏览器历史参数保留在 URL 中参数不保留在 URL 中
安全性URL 更易进入历史、日志与 Referer请求体不等于加密;两者都应使用 HTTPS
幂等性规范语义幂等规范语义不保证幂等
内容类型查询串按 URL 规则编码;请求内容没有通用语义可使用多种媒体类型

六、追问详解:HTTP 与 HTTPS 的区别 ​

核心区别 ​

Plain
HTTP:   客户端 ----[明文数据]----> 服务器     (不安全)
HTTPS:  客户端 ----[加密数据]----> 服务器     (安全)

HTTPS = HTTP + SSL/TLS,在 HTTP 的基础上加了一层安全层:

特性HTTPHTTPS
默认端口80443
数据传输明文加密
证书不需要需要 CA 颁发的 SSL 证书
安全性无防窃听、防篡改、防冒充
性能快有额外加密开销(现在已可忽略)
SEO不利搜索引擎优先收录 HTTPS

HTTPS 提供的三大安全保障 ​

  1. 机密性(Confidentiality) —— 加密传输

    • 使用对称加密算法(如 AES)加密数据

    • 即使被截获也无法读取内容

  2. 完整性(Integrity) —— 防篡改

    • 使用 MAC(消息认证码)确保数据未被修改
  3. 身份认证(Authentication) —— 防冒充

    • 通过 CA 证书验证服务器身份

    • 确保连接的是真正的目标服务器

TLS 握手过程(简化) ​

Plain
客户端                                    服务器
  |                                        |
  |---- ClientHello (支持的TLS版本、加密套件) -->|
  |<--- ServerHello (选定的版本、加密套件) ------|
  |<--- Server Certificate (证书) -------------|
  |<--- ServerHelloDone -----------------------|
  |                                        |
  |  (客户端验证证书、生成预主密钥)              |
  |---- ClientKeyExchange ------------------->|
  |---- ChangeCipherSpec -------------------->|
  |---- Finished (加密) --------------------->|
  |                                        |
  |<--- ChangeCipherSpec --------------------|
  |<--- Finished (加密) ----------------------|
  |                                        |
  |=== 加密通信开始 ==========================|

七、追问详解:HTTP/2 的改进 ​

HTTP/2 相比 HTTP/1.1 的核心改进总结为以下几点:

1. 二进制分帧 ​

  • HTTP/1.1 是纯文本协议,解析复杂(需处理换行符、空格等)

  • HTTP/2 将数据封装为二进制帧(Frame),每帧有固定的格式

  • 帧类型:HEADERS帧、DATA帧、SETTINGS帧、PRIORITY帧等

  • 多个帧组成一个消息(Message),一个或多个消息组成一个流(Stream)

2. 多路复用 ​

  • 这是最重要的改进,彻底解决了 HTTP/1.1 的应用层队头阻塞

  • 同一个 TCP 连接中,多个请求和响应可以并行交错传输

  • 每个请求通过唯一的 Stream ID 标识,接收端根据 ID 重新组装

3. 头部压缩(HPACK) ​

  • HTTP 头部往往很大(尤其含 Cookie 时),且每次请求高度重复

  • HPACK 算法:

    • 维护一份静态表(常用头部字段的预定义索引)

    • 维护一份动态表(之前传输过的头部字段)

    • 用霍夫曼编码压缩头部值

    • 只需发送索引号或增量更新

4. 服务端推送 ​

  • 服务器预测客户端需要的资源,主动推送

  • 减少了请求往返次数

  • 实际应用中因控制粒度不够灵活,使用率不算高(Chrome 后来甚至移除了支持)

5. 流量控制 ​

  • HTTP/2 实现了流级别的流量控制(基于窗口)

  • 防止一个流占用过多带宽影响其他流

6. 请求优先级 ​

  • 客户端可以为每个流分配权重和依赖关系

  • 例如:HTML 文档优先级最高,CSS 次之,图片和脚本较低


八、补充知识:HTTP 常见请求头和响应头 ​

请求头 ​

头部说明
Host目标主机名(HTTP/1.1 必传)
User-Agent客户端标识
Accept客户端可接受的响应内容类型
Accept-Encoding可接受的压缩编码(gzip, br 等)
Cookie客户端存储的 Cookie
Authorization认证凭证(Bearer Token 等)
Content-Type请求体的数据类型
If-None-Match缓存验证(配合 ETag)
If-Modified-Since缓存验证(配合 Last-Modified)

响应头 ​

头部说明
Content-Type响应体的数据类型(如 text/html, application/json)
Content-Length响应体大小(字节)
Set-Cookie设置 Cookie
Cache-Control缓存策略
ETag资源的唯一标识(用于缓存验证)
Location重定向目标 URL(配合 3xx 状态码)
Access-Control-Allow-OriginCORS 跨域允许的源
Transfer-Encoding传输编码(如 chunked)

浏览器缓存机制详解 ​

一、定义 ​

浏览器缓存是浏览器将请求过的资源(如HTML、CSS、JS、图片等)保存在本地,当再次请求相同资源时,直接使用缓存而不向服务器发起请求的策略,目的是减少网络传输、提升页面加载速度、降低服务器压力。

二、缓存存储位置优先级 ​

Plain
Service Worker → 内存缓存(Memory Cache) → 磁盘缓存(Disk Cache) → 推送缓存(Push Cache)
存储位置特点
Service Worker可编程控制,需HTTPS,容量大
Memory Cache读取快,生命周期短(关闭标签页即失效)
Disk Cache持久化存储,容量较大,读取较慢
Push CacheHTTP/2专属,会话级别,只在本次连接有效

三、强缓存(无需与服务器通信) ​

强缓存命中时,浏览器直接从本地读取资源,不会发送请求到服务器,状态码显示 200 (from cache) 或 200 (from disk cache)。

1. Expires(HTTP/1.0) ​

Plain
Expires: Thu, 01 Jun 2025 16:00:00 GMT
  • 原理:服务器返回资源的绝对过期时间,浏览器在该时间之前直接使用缓存

  • 缺点:依赖客户端本地时间,如果用户修改了本地时间,缓存可能失效或不失效

2. Cache-Control(HTTP/1.1,优先级更高) ​

Plain
Cache-Control: max-age=3600

常用指令:

指令含义
max-age=秒数资源在多少秒后过期
s-maxage=秒数同max-age,但仅对CDN等共享缓存生效
public允许任何缓存(浏览器、CDN等)存储
private只允许浏览器缓存,不允许CDN等缓存
no-cache不是不缓存,而是每次使用前必须向服务器验证(走协商缓存)
no-store真正的不缓存,每次都从服务器重新下载
immutable资源永不变,在有效期内即使用户刷新也不验证

两者同时存在时:Cache-Control 优先级高于 Expires


四、协商缓存(需与服务器通信验证) ​

强缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器判断资源是否更新:

  • 未更新:返回 304 Not Modified,浏览器继续使用本地缓存

  • 已更新:返回 200 和新资源

1. Last-Modified / If-Modified-Since ​

Plain
# 首次响应
Last-Modified: Wed, 10 May 2025 08:00:00 GMT

# 后续请求携带
If-Modified-Since: Wed, 10 May 2025 08:00:00 GMT
  • 原理:服务器返回资源最后修改时间,下次请求时浏览器将该时间发送给服务器验证

  • 缺点:

    • 精度只到秒级,1秒内的多次修改无法识别

    • 资源内容没变但修改时间变了(如重新部署),也会导致缓存失效

2. ETag / If-None-Match(优先级更高) ​

Plain
# 首次响应
ETag: "abc123def456"

# 后续请求携带
If-None-Match: "abc123def456"
  • 原理:服务器返回资源的唯一标识(通常是内容的哈希值),内容变化则标识变化

  • 优点:比 Last-Modified 更精确,能感知内容变化

  • 缺点:需要服务器计算哈希,有一定性能开销

两者同时存在时:ETag 优先级高于 Last-Modified


五、完整缓存决策流程 ​

Plain
请求资源
    ↓
检查 Cache-Control/Expires(强缓存)
    ↓
命中? → YES → 直接使用本地缓存(200 from cache)
    ↓ NO
携带 If-None-Match/If-Modified-Since 发起请求(协商缓存)
    ↓
服务器验证资源是否更新
    ↓
未更新? → YES → 返回 304,使用本地缓存
    ↓ NO
返回 200 + 新资源

六、GET vs POST 的缓存行为 ​

方法默认行为原因
GET默认可缓存幂等、无副作用,适合缓存
POST默认不缓存非幂等、有副作用,每次请求可能产生不同结果

POST 也可以通过设置 Cache-Control 和 Content-Location 响应头实现缓存,但实践中很少使用。


七、常见追问解答 ​

1. no-cache 和 no-store 的区别 ​

属性no-cacheno-store
含义可以缓存,但使用前必须向服务器验证禁止任何缓存,每次都从服务器下载
是否存储存储到本地缓存不存储到本地缓存
网络请求每次都发请求(协商缓存)每次都发请求(完整下载)
使用场景需要最新版本但允许缓存验证的资源敏感数据(密码、银行卡信息)
Bash
# no-cache:缓存但每次都验证
Cache-Control: no-cache

# no-store:完全不缓存
Cache-Control: no-store

2. 如何控制缓存策略? ​

JavaScript
// 频繁变动的资源(如HTML页面)
Cache-Control: no-cache  // 每次验证,确保获取最新

// 不常变动的资源(如版本化的JS/CSS)
Cache-Control: max-age=31536000  // 缓存1年

// 敏感数据
Cache-Control: no-store  // 完全不缓存

// 配合文件名哈希实现长期缓存 + 即时更新
// app.a1b2c3d4.js → max-age=31536000
// 内容变化后文件名变化,自动获取新资源

3. 用户行为对缓存的影响 ​

用户操作缓存行为
地址栏输入URL正常缓存流程
F5刷新跳过强缓存,走协商缓存(加 Cache-Control: max-age=0)
Ctrl+F5强制刷新跳过所有缓存(加 Cache-Control: no-cache 和 Pragma: no-cache)
前进/后退正常缓存流程(部分浏览器可能用 Memory Cache)

八、最佳实践总结 ​

Plain
1. HTML文件:Cache-Control: no-cache(每次验证,确保入口文件最新)
2. 带哈希的静态资源:Cache-Control: max-age=31536000, immutable(长期缓存)
3. API接口:Cache-Control: no-store(通常不缓存)
4. 图片等不常变资源:Cache-Control: max-age=86400(缓存1天)

一、AI 相关 ​

1. AI 工具使用熟悉度 ​

是的,我在日常开发中高频使用 AI 工具,主要包括:

  • 编码辅助:使用 Cursor、Trae 等 AI IDE 进行代码补全、Bug 修复、重构建议

  • 代码审查:让 AI 检查代码中的潜在问题、安全漏洞

  • 文档生成:自动生成 JSDoc 注释、API 文档

  • 技术调研:快速了解不熟悉的技术方案、对比不同方案的优劣

  • 调试辅助:将报错信息提供给 AI,快速定位问题

整体来说,AI 已经成为我工作流中不可或缺的一部分,更像是一个「智能编程伙伴」。


2. SSE 流式输出 + 不完整 Markdown 的渲染处理 ​

SSE(Server-Sent Events)是什么:

SSE 是一种服务端向客户端单向推送数据的技术,基于 HTTP 协议。与 WebSocket 的双向通信不同,SSE 是服务端到客户端的单向流。它的特点是:

  • 基于普通 HTTP,不需要特殊协议升级

  • 自动重连机制

  • 数据格式是纯文本,以 data: 前缀分隔事件

  • 天然适合 AI 大模型的流式输出场景

前端通过 **EventSource** 或 **fetch** + **ReadableStream** 来消费:

JavaScript
const response = await fetch('/api/chat', { method: 'POST', body: JSON.stringify(data) })
const reader = response.body.getReader()
const decoder = new TextDecoder()

while (true) {
  const { done, value } = await reader.read()
  if (done) break
  const chunk = decoder.decode(value)
  // 处理 SSE 数据块
  accumulatedText += parseSSEChunk(chunk)
  renderMarkdown(accumulatedText)
}

不完整 Markdown 的渲染问题:

这是 SSE 流式渲染中最核心的难点。例如 AI 正在输出:

Plain
这是**加粗文

此时只有 ** 的左标记,没有右标记,直接渲染会出错。

解决方案有几种策略:

策略一:容忍不完整语法的 Markdown 解析器

使用专门的「流式友好」Markdown 解析器(如 markdown-it 配合自定义处理),它能容忍不完整的语法:

  • 遇到未闭合的 **,暂时当作普通文本渲染

  • 遇到未闭合的代码块 ```,保持代码块样式但不执行语法高亮

策略二:分段渲染 + 延迟解析

JavaScript
function renderMarkdownSafely(text) {
  // 1. 找到最后一个可能不完整的行
  const lines = text.split('\n')
  const lastLine = lines[lines.length - 1]

  // 2. 判断最后一行是否有未闭合的 Markdown 语法
  const isComplete = checkMarkdownComplete(lastLine)

  // 3. 完整部分正常渲染,不完整的最后一行降级为纯文本
  if (isComplete) {
    return marked(text)
  } else {
    const completePart = lines.slice(0, -1).join('\n')
    return marked(completePart) + escapeHtml(lastLine)
  }
}

策略三:CSS 层面兜底

对流式渲染容器加 CSS 保护,防止不完整标签导致布局异常:

CSS
.streaming-content {
  overflow-wrap: break-word;
  word-break: break-word;
}
.streaming-content strong:empty,
.streaming-content em:empty {
  display: none;
}

实际项目中我倾向的方案是: 策略一 + 策略三 结合。使用一个支持容错的 Markdown 渲染库,同时用 CSS 做兜底保护,每个 chunk 到达时增量渲染,并对未闭合的语法块做特殊处理(等待下一个 chunk 补全后再渲染样式)。


3. 正常工作中的 AI 工作流 & 上下文定义 ​

典型的 AI 辅助开发工作流:

Plain
明确需求 → 构建上下文 → AI 生成代码 → 人工审查 → 测试验证 → 提交

上下文定义是核心,关键在于让 AI 准确理解「我在什么环境里,要做什么事」:

示例:我要给项目加一个「文章收藏」功能

我会这样构建 Prompt 上下文:

Plain
【项目背景】
这是一个 Vue 3 + TypeScript + Pinia 的内容管理系统

【技术约束】
- 使用 Element Plus 作为 UI 库
- 请求封装在 src/utils/request.ts 中,基于 Axios
- 状态管理使用 Pinia,store 文件在 src/stores/ 目录
- 组件采用 `<script setup>` + Composition API 风格
- 代码规范:ESLint Airbnb + Prettier,tab 缩进

【现有代码参考】
以下是现有的「点赞」功能实现,收藏功能要参考它的模式:
(附上点赞组件和 store 的关键代码)

【需求描述】
请实现文章收藏功能,包括:
1. 收藏按钮组件
2. 收藏状态管理 store
3. 收藏 API 接口调用

【输出要求】
- 遵循上述代码风格和目录结构
- TypeScript 类型要完整
- 不要添加注释

上下文的关键要素:

  1. 技术栈声明 — 让 AI 知道用什么框架、什么版本

  2. 约束声明 — 告诉它项目规范、代码风格、禁止事项

  3. 示例代码 — 给它参考样本,比纯文字描述高效 10 倍

  4. 明确的输出期望 — 说清楚要什么,不要什么


4. AI 生成代码与项目规范冲突时的约束 ​

多层次约束策略:

第一层:Project Rules / Rules 文件

在项目中创建 .cursorrules、.github/copilot-instructions.md、CLAUDE.md 等规则文件,写明:

Markdown
## 项目规范
- 使用 Vue 3 Composition API + `<script setup>`
- TypeScript 严格模式
- 组件命名使用 PascalCase
- CSS 使用 scoped + BEM 命名
- 禁止使用 any 类型
- API 请求必须通过封装的 request 实例
- 错误处理必须统一使用 errorHandler

第二层:System Prompt 约束

在与 AI 对话时,一开始就在 System Prompt 中声明项目规范,作为全局约束。

第三层:Few-shot 示例

给 AI 展示「正确的代码长什么样」和「错误的代码长什么样」:

Plain
✅ 正确示例:
const useUserStore = defineStore('user', () => {
  const userInfo = ref<UserInfo | null>(null)
  // ...
})

❌ 错误示例(不要这样写):
export default {
  data() {
    return { userInfo: null }
  }
}

第四层:验证拦截

  • ESLint + Prettier 自动格式化,AI 生成的代码提交前必须过 lint

  • Code Review 环节人工把关

  • 写单元测试验证行为正确性


5. 大项目中如何控制 Token 消耗 ​

核心原则:只给 AI 它需要的上下文,而不是全部代码

方法一:按模块/功能切分上下文

不要把整个项目扔给 AI,而是只给相关模块:

Plain
我正在处理用户模块,以下是相关的文件:
- src/stores/user.ts(用户状态管理)
- src/api/user.ts(用户 API)
- src/views/user/Profile.vue(个人资料页面)
请在这几个文件的基础上帮我实现...

方法二:代码摘要代替完整代码

对于大的文件,只给关键部分:

Plain
以下是 router/index.ts 的路由结构概要:
- / → HomeView
- /login → LoginView
- /user/:id → UserDetailView(需要鉴权)
- /admin → AdminView(需要管理员权限)

方法三:分层对话策略

层次内容Token 消耗
第 1 轮技术栈 + 目录结构 + 规范低
第 2 轮当前模块的关键文件中
第 3 轮具体需求 + 参考代码片段低

先让 AI 理解项目结构,再逐步深入具体问题。

方法四:善用 **.cursorignore** 等工具

配置 AI IDE 的忽略文件,排除不需要索引的目录:

Plain
node_modules/
dist/
public/
*.test.ts
*.spec.ts

方法五:RAG(检索增强生成)

对于超大项目,可以:

  • 将项目文档、API 定义、类型定义等结构化存入知识库

  • AI 按需检索相关上下文,而不是全量加载

  • 这在企业级项目中越来越常见


二、八股文 ​

1. new 操作符做了什么 ​

new 操作符执行时会经历以下 4 个步骤:

JavaScript
function Person(name) {
  this.name = name
}
const p = new Person('Tom')

等价于:

JavaScript
function myNew(Constructor, ...args) {
  // 1. 创建一个空对象,指向构造函数的原型
  const obj = Object.create(Constructor.prototype)

  // 2. 执行构造函数,this 指向这个新对象
  const result = Constructor.apply(obj, args)

  // 3. 如果构造函数返回了一个对象,则返回该对象;否则返回新创建的对象
  return result instanceof Object ? result : obj
}

四个步骤总结:

  1. 创建空对象:let obj = {}

  2. 链接原型:obj.__proto__ = Constructor.prototype

  3. 绑定 this 并执行:Constructor.call(obj, ...args)

  4. 判断返回值:构造函数返回的是对象就用返回的,否则用新创建的 obj


2. CommonJS 和 ES6 Module 的区别 ​

对比维度CommonJSES6 Module
语法require() / module.exportsimport / export
加载时机运行时加载(动态)编译时静态分析(静态)
值的拷贝深拷贝(原始值复制,对象引用复制)活绑定(实时引用,读取的是最新值)
Tree Shaking❌ 不支持✅ 支持(静态分析可确定哪些模块没用到)
循环依赖可能输出已经执行的部分结果通过活绑定可以正确处理
环境Node.js 原生支持浏览器原生支持,Node.js 需配置
异步/同步同步加载支持异步加载(import() 动态导入)

关键区别举例 — 活绑定 vs 值拷贝:

JavaScript
// CommonJS
// counter.js
let count = 0
module.exports = { count, increment: () => count++ }

// main.js
const { count, increment } = require('./counter')
increment()
console.log(count) // 0 ❌ 拿的是导出时的快照

// ES Module
// counter.js
export let count = 0
export function increment() { count++ }

// main.js
import { count, increment } from './counter.js'
increment()
console.log(count) // 1 ✅ 活绑定,读取最新值

3. 0.1 + 0.2 !== 0.3 的原因 ​

根本原因:JavaScript 使用 IEEE 754 双精度浮点数(64位)表示数字,而 0.1 和 0.2 在二进制中是无限循环小数,无法精确表示。

详细过程:

Plain
0.1 → 二进制: 0.000110011001100110011...(无限循环)
0.2 → 二进制: 0.00110011001100110011...(无限循环)

由于 64 位浮点数的尾数只有 52 位,超出部分会被截断,产生精度丢失。

Plain
0.1 + 0.2 = 0.30000000000000004 ≠ 0.3

解决方案:

JavaScript
// 方法一:toFixed(简单场景)
(0.1 + 0.2).toFixed(10) // "0.3000000000"

// 方法二:转为整数运算
(0.1 * 10 + 0.2 * 10) / 10 // 0.3

// 方法三:Math.round 精度容差
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON // true

// 方法四:使用 toPrecision
parseFloat((0.1 + 0.2).toPrecision(12)) // 0.3

扩展:为什么 0.1 + 0.2 是 0.30000000000000004 而不是 0.30000000000000003?

因为两个近似值相加后的截断/舍入结果刚好超过了 0.3 一点点。


4. 浏览器的安全策略 ​

① 同源策略(Same-Origin Policy)

  • 协议、域名、端口三者都相同才算同源

  • 限制:不同源的脚本不能读取对方的 DOM、Cookie、LocalStorage、IndexDB

  • AJAX 请求也受同源策略限制

  • 目的:防止恶意网站窃取用户数据

② CORS(跨源资源共享)

  • 服务端通过 Access-Control-Allow-Origin 等响应头来放行跨域请求

  • 简单请求直接发送,复杂请求会先发 OPTIONS 预检请求

③ CSP(Content Security Policy)

  • 通过 HTTP 响应头 Content-Security-Policy 配置

  • 限制页面可以加载的资源来源,防止 XSS 攻击

  • 例如:Content-Security-Policy: script-src 'self' https://cdn.example.com

④ XSS 防护

  • 输入过滤和输出编码

  • 使用 HttpOnly Cookie 防止 JS 读取

  • CSP 限制内联脚本

⑤ CSRF 防护

  • SameSite Cookie 属性(Strict / Lax)

  • CSRF Token

  • 验证 Referer / Origin 头

⑥ 其他安全机制

  • X-Frame-Options:防止点击劫持(Clickjacking),限制页面是否可以被 iframe 嵌入

  • X-Content-Type-Options:防止 MIME 嗅探,设置 nosniff

  • HSTS(HTTP Strict Transport Security):强制使用 HTTPS

  • Sandbox:iframe 的沙箱属性,限制嵌入内容的能力


5. 浏览器的渲染机制 ​

完整渲染流程(从 URL 到像素):

Plain
URL → 网络请求 → 解析HTML → 构建DOM树
                         ↓
                    解析CSS → 构建CSSOM树
                         ↓
                  DOM + CSSOM → 渲染树(Render Tree)
                         ↓
                    布局(Layout/Reflow)→ 计算每个节点的位置和大小
                         ↓
                    绘制(Paint)→ 生成绘制指令
                         ↓
                    合成(Composite)→ GPU 合成图层并显示

详细步骤:

  1. 解析 HTML → DOM 树

    • 逐行解析 HTML,遇到 <script> 会阻塞解析(除非 async/defer)

    • 生成树形 DOM 结构

  2. 解析 CSS → CSSOM 树

    • 解析所有 CSS(包括外部样式表、内联样式、继承样式)

    • CSS 不会阻塞 DOM 解析,但会阻塞渲染

  3. 构建渲染树(Render Tree)

    • 只包含可见节点(display: none 的节点不包含)

    • visibility: hidden 会包含在渲染树中(占据空间)

    • 为每个节点计算最终的样式值(层叠、继承、优先级)

  4. 布局(Layout / Reflow)

    • 计算每个节点的几何信息(位置、宽高)

    • 相对单位转换为绝对像素

  5. 绘制(Paint)

    • 将布局信息转换为绘制指令(像素)

    • 按层绘制背景、文字、图片、边框等

  6. 合成(Composite)

    • 将多个图层合并

    • GPU 加速处理(transform、opacity 等属性触发合成层提升)

    • 最终输出到屏幕


6. 怎么避免重排(Reflow)和重绘(Repaint) ​

先区分概念:

  • 重排(Reflow):元素的几何属性(位置、大小)改变,需要重新计算布局。代价大。

  • 重绘(Repaint):元素外观改变(颜色、背景),但不影响布局。代价相对小。

  • 重排一定会引起重绘,重绘不一定引起重排。

触发重排的操作:

  • 增删可见 DOM 元素

  • 元素尺寸改变(宽高、边距、边框、内边距)

  • 内容变化(文字、图片尺寸)

  • 浏览器窗口 resize

  • 读取布局属性(offsetWidth、scrollTop、getComputedStyle、getBoundingClientRect)

避免重排重绘的策略:

① 批量修改 DOM

JavaScript
// ❌ 每次修改都触发重排
el.style.width = '100px'
el.style.height = '100px'
el.style.margin = '10px'

// ✅ 合并为一次修改
el.style.cssText = 'width:100px; height:100px; margin:10px;'
// 或使用 class
el.classList.add('new-style')

② 离线操作 DOM

JavaScript
// 使用 DocumentFragment
const fragment = document.createDocumentFragment()
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li')
  li.textContent = `Item ${i}`
  fragment.appendChild(li)
}
document.getElementById('list').appendChild(fragment) // 只触发一次重排

// 或使用 display:none 隐藏元素后再操作
const el = document.getElementById('list')
el.style.display = 'none'
// ... 大量修改
el.style.display = 'block' // 只触发两次重排

③ 避免频繁读取布局属性

JavaScript
// ❌ 读写交替导致强制同步布局(Forced Synchronous Layout)
for (let i = 0; i < elements.length; i++) {
  elements[i].style.width = container.offsetWidth + 'px' // 每次循环都触发重排
}

// ✅ 缓存布局值
const width = container.offsetWidth
for (let i = 0; i < elements.length; i++) {
  elements[i].style.width = width + 'px'
}

④ 使用 CSS transform 代替 top/left

JavaScript
// ❌ 触发重排
el.style.top = '100px'

// ✅ 只触发合成,不重排不重绘
el.style.transform = 'translateY(100px)'

⑤ 使用 **will-change** 提示浏览器

CSS
.animated-element {
  will-change: transform, opacity;
}

⑥ 使用 **requestAnimationFrame** 批量处理动画

JavaScript
requestAnimationFrame(() => {
  el.style.transform = `translateX(${x}px)`
})

7. 进程通信方式及使用场景 ​

浏览器中常见的进程通信方式:

① 管道(Pipe)

  • 原理:半双工通信,数据只能单向流动

  • 使用场景:父子进程间简单的数据传递

  • 例如:shell 命令 cat file.txt | grep "keyword"

② 消息队列(Message Queue)

  • 原理:进程将消息写入队列,另一个进程从队列中读取

  • 使用场景:异步通信,不需要立即响应的场景

  • 例如:浏览器中的 postMessage

③ 共享内存(Shared Memory)

  • 原理:多个进程可以访问同一块物理内存

  • 使用场景:大量数据传输,性能要求高的场景

  • 例如:SharedArrayBuffer(Web Worker 间共享数据)

④ 信号量(Semaphore)

  • 原理:用于进程间的同步和互斥

  • 使用场景:控制对共享资源的并发访问

⑤ Socket

  • 原理:可以在不同机器间通信

  • 使用场景:网络通信、不同主机间的进程通信

  • 例如:WebSocket

浏览器架构中的进程通信:

通信方式使用场景
IPC(浏览器内部)主进程与渲染进程通信
postMessage页面与 iframe、Web Worker 通信
Broadcast Channel同源页面间通信
Service Worker拦截网络请求、离线缓存、推送通知
SharedArrayBuffer + AtomicsWorker 间高性能数据共享

8. TCP 是怎么实现可靠传输的 ​

TCP 通过以下机制实现可靠传输:

① 三次握手建立连接

Plain
客户端 → SYN=1, seq=x          → 服务端
客户端 ← SYN=1, ACK=1, seq=y, ack=x+1 ← 服务端
客户端 → ACK=1, seq=x+1, ack=y+1  → 服务端
  • 确保双方的发送和接收能力都正常

  • 同步初始序列号

② 序列号与确认应答(Sequence Number & ACK)

  • 每个字节的数据都有一个序列号

  • 接收方收到数据后发送 ACK,告知「我已收到哪些数据」

  • 发送方收到 ACK 后确认数据已被成功接收

③ 超时重传(Retransmission)

  • 发送方启动定时器,如果在规定时间内没收到 ACK,就重传数据

  • 超时时间(RTO)动态计算,基于 RTT(往返时间)的估算

④ 滑动窗口(Sliding Window)

  • 发送方维护一个「发送窗口」,窗口内的数据可以连续发送而不必等待每个 ACK

  • 窗口大小由接收方告知(流量控制)

  • 提高了传输效率(不用逐个等待 ACK)

⑤ 流量控制(Flow Control)

  • 接收方通过 TCP 头部的窗口大小字段告知发送方「我还能接收多少数据」

  • 防止发送方发送过快导致接收方缓冲区溢出

⑥ 拥塞控制(Congestion Control)

  • 4 种算法:
    • 慢启动:初始窗口小,指数增长

    • 拥塞避免:窗口达到阈值后线性增长

    • 快重传:收到 3 个重复 ACK 立即重传

    • 快恢复:快重传后不回到慢启动,而是将阈值减半,从新阈值开始拥塞避免

⑦ 校验和(Checksum)

  • 每个 TCP 段都有校验和,接收方验证数据是否损坏

  • 如果校验失败,丢弃该段,等待超时重传

⑧ 四次挥手断开连接

  • 确保双方数据都已发送完毕

  • 有 TIME_WAIT 状态等待 2MSL,防止最后的 ACK 丢失


9. HTTP/3.0 为什么要基于 UDP ​

核心原因:TCP 的队头阻塞问题在 HTTP/2 中无法彻底解决。

TCP 的队头阻塞问题:

Plain
HTTP/2 虽然在应用层实现了多路复用(多个请求共享一个 TCP 连接),
但在传输层,TCP 保证的是字节流的顺序交付。

如果第 1 个 TCP 段丢失,即使第 2、3、4 个段已到达,
TCP 也必须等待第 1 个段重传成功后才能向上交付数据。
→ 所有 HTTP/2 流都被这一个丢包阻塞了。

HTTP/3 基于 QUIC 协议(运行在 UDP 之上)的解决方案:

① 彻底解决队头阻塞

  • QUIC 在传输层就实现了多路复用

  • 每个 HTTP 流有独立的可靠性保证

  • 一个流的丢包不会影响其他流

② 更快的连接建立

  • TCP + TLS 1.2 需要 3 个 RTT(TCP 握手 + TLS 握手)

  • TCP + TLS 1.3 需要 2 个 RTT

  • QUIC 只需要 1 个 RTT(首次连接),甚至 0 RTT(后续连接)

③ 连接迁移

  • TCP 连接基于四元组(源IP、源端口、目标IP、目标端口)

  • 网络切换(WiFi → 4G)会导致 IP 变化,TCP 连接断开

  • QUIC 使用 Connection ID 标识连接,网络切换后连接不中断

④ UDP 本身的灵活性

  • UDP 是无连接、不可靠的「裸」传输层协议

  • QUIC 在 UDP 之上自己实现了可靠传输、流量控制、拥塞控制

  • 不受操作系统内核更新限制,可以更快迭代

为什么不在 TCP 之上做?

  • TCP 是操作系统内核实现的,修改 TCP 协议需要更新全世界的操作系统

  • UDP 足够简单和灵活,可以在用户态自由实现上层协议

  • 这就是「在 UDP 之上重建 TCP 的优点,同时避免 TCP 的缺点」


10. Vue Router 是怎么实现的 ​

Vue Router 本质上是一个前端路由管理器,核心原理是URL 变化与视图映射的对应关系。

两种路由模式:

① Hash 模式

JavaScript
// 原理:监听 hashchange 事件
window.addEventListener('hashchange', () => {
  const hash = window.location.hash // #/about
  // 根据 hash 匹配路由,渲染对应组件
})

// URL 形式:http://example.com/#/about
  • 不会向服务器发送请求(hash 变化不会触发页面刷新)

  • 兼容性好,支持 IE

  • #/ 部分不会发送给服务器

② History 模式

JavaScript
// 原理:使用 HTML5 History API
// pushState 和 replaceState 改变 URL 而不刷新页面
history.pushState(state, '', '/about')

// 监听 popstate 事件(浏览器前进后退)
window.addEventListener('popstate', () => {
  // 根据 location.pathname 匹配路由
})

// 也可以拦截 <a> 标签的 click 事件,用 pushState 替代默认跳转
  • URL 更美观:http://example.com/about

  • 需要服务器配合:所有路由都指向 index.html,否则刷新会 404

Vue Router 3.x 的实现核心:

JavaScript
// 简化版实现思路
class VueRouter {
  constructor(options) {
    this.routes = options.routes
    this.current = '/'

    // 根据 mode 选择监听方式
    if (options.mode === 'hash') {
      window.addEventListener('hashchange', this.onHashChange.bind(this))
    } else {
      window.addEventListener('popstate', this.onPopState.bind(this))
    }
  }

  onHashChange() {
    this.current = window.location.hash.slice(1) || '/'
    this.matchRoute()
  }

  matchRoute() {
    const matched = this.routes.find(r => r.path === this.current)
    // 触发视图更新(Vue 响应式系统)
    this.matchedComponent = matched?.component
  }

  push(path) {
    if (this.mode === 'hash') {
      window.location.hash = path
    } else {
      history.pushState(null, '', path)
      this.current = path
      this.matchRoute()
    }
  }
}

Vue Router 4.x(Vue 3)的变化:

  • 内部使用 createRouter + createWebHistory / createWebHashHistory

  • 路由匹配使用 path-to-regexp 进行路径解析

  • 支持组合式 API(useRouter、useRoute)

  • 导航守卫改为基于 Promise 的异步流程

整体流程:

Plain
URL 变化 → 路由匹配 → 触发导航守卫(beforeEach → beforeEnter → beforeRouteEnter)
→ 组件解析(异步组件加载)→ 更新 <router-view> 渲染目标组件 → afterEach

12. 正常开发项目时性能优化思路 ​

我会按照 「度量 → 分析 → 优化 → 验证」 的闭环流程来做:

第一步:度量 — 用数据说话

  • Web Vitals:关注 LCP(最大内容绘制)、FID(首次输入延迟)、CLS(累计布局偏移)

  • Chrome DevTools:Performance 面板录制、Lighthouse 审计

  • Network 面板:查看资源加载瀑布图、文件大小、加载耗时

第二步:分类优化

网络层面:

  • 代码分割(Code Splitting)+ 路由懒加载

  • 资源压缩(gzip/brotli)

  • 图片优化(WebP 格式、响应式图片、懒加载)

  • CDN 加速静态资源

  • HTTP 缓存策略(强缓存 + 协商缓存)

  • HTTP/2 多路复用

渲染层面:

  • 避免重排重绘(前面已详细回答)

  • 使用虚拟列表处理长列表(只渲染可视区域)

  • CSS 动画使用 transform/opacity(触发 GPU 合成)

  • 关键 CSS 内联,非关键 CSS 异步加载

JavaScript 层面:

  • 减少主线程阻塞(长任务拆分)

  • Web Worker 处理计算密集型任务

  • 避免内存泄漏(及时清理定时器、事件监听器)

  • 使用 requestIdleCallback / requestAnimationFrame 调度任务

Vue 项目专项:

  • 合理使用 v-if vs v-show

  • v-for 必须加 key

  • 组件级缓存(<keep-alive>)

  • 大组件使用异步组件(defineAsyncComponent)

  • computed 缓存计算结果,避免模板中做复杂计算

  • shallowRef / shallowReactive 减少深层响应式开销

  • 长列表使用虚拟滚动组件

第三步:验证

  • 优化前后用同一工具测量,对比数据

  • 确保优化有效且没有引入新问题

核心理念:性能优化不是一锤子买卖,而是持续的过程。先找到瓶颈(通常是网络请求和渲染),再有针对性地优化。不要过早优化,也不要盲目优化。


1. SSE (Server-Sent Events) ​

SSE 的原理:

  • SSE 基于 HTTP 协议,客户端通过 EventSource API 建立一个持久的 HTTP 连接

  • 服务端响应头设置 Content-Type: text/event-stream,表示这是一个事件流

  • 服务端通过这个持久连接,以文本格式一段一段地发送数据,每段数据格式如下:

    Plain
    data: {"message": "hello"}\n\n

    每条消息以 \n\n(两个换行符)分隔

  • 客户端通过 onmessage 事件监听器接收每条消息

关于原生 EventSource 不支持 POST 的问题:

  • 对,原生 EventSource 只支持 GET 请求,不支持自定义请求头

  • 所以在需要 POST 请求或自定义请求头的场景下(如 AI 对话携带 Token),需要用 polyfill 库(如 eventsource-parser、microsoft/fetch-event-source)来实现

SSE 重连原理:

  • 原生 EventSource 有内置的自动重连机制:当连接断开时,浏览器会自动重新发起请求

  • 服务端可以在响应中通过 id: 字段设置每条消息的 ID(即 Last-Event-ID)

  • 重连时,浏览器会在请求头中携带 Last-Event-ID,告诉服务端从哪条消息开始续传

  • 但正如 Speaker 1 所说,AI 对话场景下这个重连机制往往不太适用,因为上下文状态可能已经丢失


2. SSE vs WebSocket ​

对话中的总结基本正确,补充一下:

特性SSEWebSocket
方向单向(服务端→客户端)全双工(双向)
协议基于 HTTP独立的 ws:// 协议
数据格式仅文本文本 + 二进制
重连机制内置自动重连需要手动实现
适用场景消息通知、AI 流式对话即时通讯、实时游戏

3. 登录流程与 Token 存储 ​

Speaker 1 的回答基本正确,补充完善:

  1. 客户端发送账号密码(HTTPS POST,密码在 body 中)

  2. 服务端验证后返回一个 JWT Token

  3. 将 Token 存储在 Cookie 中,并设置安全属性

  4. 后续请求自动携带 Cookie 中的 Token

关于密码加密:

  • 前端可以用 RSA 公钥加密密码再传输(后端用私钥解密)

  • 或者前端做一次 哈希(如 SHA-256) 再传输,但注意这不能替代 HTTPS


Speaker 1 提到了 HttpOnly 和 SameSite,但还有几个重要属性:

属性作用
HttpOnly禁止 JavaScript 直接读取 Cookie,降低凭据被窃取风险;不能阻止 XSS 以用户身份发请求
SameSite=Strict/Lax/None控制跨站请求是否携带 Cookie,可降低部分 CSRF 风险但不能替代完整防护
Secure只在 HTTPS 连接下传输 Cookie
Domain指定 Cookie 所属域名
Path指定 Cookie 的有效路径
Max-Age / ExpiresCookie 的过期时间

CSRF 攻击原理(Speaker 1 解释基本正确):

  • 攻击者诱导用户访问恶意网站

  • 恶意网站向目标网站发起请求(如 <img src="目标网站/api/transfer?to=attacker&amount=1000">)

  • 浏览器会自动携带目标网站的 Cookie,导致请求被当作合法请求处理


5. XSS 攻击与防御 ​

Speaker 1 提到了三种类型,补充一下具体的防御手段:

防御措施:

  1. 输入过滤/输出转义:将 < 转义为 &lt;,> 转义为 &gt;," 转义为 &quot; 等

  2. 使用 **textContent** 而非 **v-html**(Vue 中)或 innerText 而非 innerHTML(原生 JS)

  3. CSP(Content Security Policy):通过 HTTP 头 Content-Security-Policy 限制可执行脚本的来源白名单

  4. 对用户输入进行严格校验和过滤

关于转义的具体做法:

  • 不是简单删除 <script> 标签(这样容易被绕过,如 <img onerror="alert(1)">)

  • 而是将所有 HTML 特殊字符进行实体编码,这样浏览器会将其当作纯文本显示


6. 二叉树遍历 ​

Speaker 1 的回答基本正确。层序遍历(BFS)的伪代码:

JavaScript
function levelOrder(root) {
    if (!root) return [];
    const queue = [root];
    const result = [];

    while (queue.length) {
        const levelSize = queue.length;
        const level = [];

        for (let i = 0; i < levelSize; i++) {
            const node = queue.shift();
            level.push(node.val);
            if (node.left) queue.push(node.left);
            if (node.right) queue.push(node.right);
        }
        result.push(level);
    }
    return result; // 二维数组
}

7. HTTP 缓存策略 ​

Speaker 1 的回答中有一个重要错误需要纠正:

强缓存:

  • Cache-Control 是相对时间(如 max-age=3600,表示 3600 秒后过期)

  • Expires 是绝对时间(如 Expires: Thu, 01 Jan 2026 00:00:00 GMT)

  • Speaker 1 说反了:Cache-Control 是相对时间,Expires 是绝对时间

  • 优先级:**Cache-Control** > **Expires**

协商缓存:

  • Last-Modified / If-Modified-Since:基于文件修改时间,精度为秒级

  • ETag / If-None-Match:基于文件内容的哈希值,更精确

  • 优先级:**ETag** > **Last-Modified**

为什么会有两套?

  • Cache-Control 是 HTTP/1.1 引入的,功能更强大(可设置 no-cache、no-store、private 等),是为了替代 Expires

  • ETag 比 Last-Modified 更精确,因为文件可能在 1 秒内多次修改,或者内容没变但时间变了(如 touch 命令)

缓存流程:

Plain
1. 强缓存检查 → 命中则直接使用缓存(200 from cache)
2. 强缓存未命中 → 发起协商缓存请求
3. 协商缓存命中 → 返回 304 Not Modified,使用缓存
4. 协商缓存未命中 → 返回 200 + 新资源

1. Last-Modified vs ETag 的区别 ​

Speaker 1 的回答基本正确,补充一下完整对比:

特性Last-Modified / If-Modified-SinceETag / If-None-Match
精度秒级,1秒内多次修改无法区分内容级,只要内容变化就会生成新的标识
原理基于文件的最后修改时间基于文件内容生成的哈希值(如 MD5)
性能无需计算哈希,性能更好需要计算哈希,有一定开销
优先级较低较高(两者同时存在时优先使用 ETag)

为什么会有两套?

  • Last-Modified 是 HTTP/1.0 就有的,简单但不够精确

  • ETag 是 HTTP/1.1 引入的,更精确但有一定性能开销

  • 实际项目中,很多服务器会同时返回两个,让浏览器根据优先级自行判断


2. 强缓存 vs 协商缓存在实际项目中的使用 ​

Speaker 1 说的基本正确,实际项目中的典型策略:

强缓存(**Cache-Control: max-age=31536000**):

  • 适合带哈希值的静态资源,如打包后的 CSS、JS 文件

    • 例:app.a1b2c3d4.js、style.e5f6g7h8.css

    • 文件名中的哈希值在内容变化时会改变,所以可以放心设置很长的缓存时间

  • 字体文件、图片等不常变化的资源

协商缓存(**Cache-Control: no-cache**):

  • 适合HTML 入口文件,因为 HTML 是页面的入口,需要每次都去服务端验证是否有更新

  • 适合需要实时性但变化不频繁的 API 数据

不缓存(**Cache-Control: no-store**):

  • 适合敏感数据,如用户信息、支付接口等

3. 强缓存下如何更新资源? ​

这是 Speaker 1 没回答上来的问题,答案是前端工程化中非常核心的一个概念——文件名哈希(Content Hash):

原理:

Plain
打包前:app.js
打包后:app.a1b2c3d4.js(文件内容的哈希值作为文件名的一部分)

流程:

  1. 构建工具(Webpack/Vite)在打包时,会根据文件内容生成一个哈希值,拼接到文件名中

  2. HTML 文件引用的是带哈希的文件名:<script src="/js/app.a1b2c3d4.js"></script>

  3. 当 CSS/JS 内容更新后,重新打包会生成新的哈希:app.x9y8z7w6.js

  4. HTML 文件也会更新引用:<script src="/js/app.x9y8z7w6.js"></script>

  5. 因为 HTML 走的是协商缓存,浏览器会发现 HTML 变了,进而加载新的 JS 文件

  6. 旧的 JS 文件(app.a1b2c3d4.js)因为没人引用了,浏览器会自然淘汰

这就是为什么:

  • HTML 用协商缓存(需要及时发现更新)

  • 带哈希的静态资源用强缓存(哈希变了就是新文件,哈希没变就是同一个文件,可以放心缓存)

总结一句话: 不是去"刷新"用户的缓存,而是通过文件名的变化来"绕过"缓存,让浏览器主动请求新资源。

问题1:Vue的虚拟DOM工作原理及diff算法优化 ​

虚拟DOM的工作原理 ​

虚拟DOM(Virtual DOM)是Vue渲染系统的核心,其本质是一个轻量级的JavaScript对象,是对真实DOM的抽象表示。

核心流程(渲染管线):

  1. 编译阶段:Vue模板被编译为渲染函数(render function),该函数返回虚拟DOM树

  2. 挂载阶段:运行时渲染器调用渲染函数,遍历返回的虚拟DOM树,创建对应的真实DOM节点。此过程作为响应式副作用执行,会追踪所有依赖

  3. 补丁阶段(Patch):当依赖的数据变化时,副作用重新运行,生成新的虚拟DOM树,渲染器对比新旧两棵树,计算差异并应用到真实DOM

JavaScript
// 虚拟节点的基本结构
const vnode = {
  type: 'div',
  props: { id: 'hello', class: 'container' },
  children: [
    { type: 'span', props: {}, children: 'Hello World' }
  ]
}

Vue的diff算法优化 ​

Vue 2的双端比较算法 ​

Vue 2采用双端指针法进行子节点比较:

  1. 头-头比较:旧头节点与新头节点对比

  2. 尾-尾比较:旧尾节点与新尾节点对比

  3. 头-尾比较:旧头节点与新尾节点对比

  4. 尾-头比较:旧尾节点与新头节点对比

如果以上四种都不匹配,则通过key在旧节点中查找可复用节点。

Vue 3的Block Tree架构优化 ​

Vue 3引入了多项革命性优化:

1. 静态提升(Static Hoisting)

JavaScript
// 编译前
<div>
  <h1>静态标题</h1>
  <p>{{ message }}</p>
</div>

// 编译后 - 静态节点被提升,只创建一次
const _hoisted_1 = createVNode("h1", null, "静态标题")
function render() {
  return (openBlock(), createBlock("div", null, [
    _hoisted_1,  // 直接复用
    createVNode("p", null, ctx.message)
  ]))
}

2. Patch Flags(补丁标志)

JavaScript
// 编译器标记节点的动态部分
createVNode("div", {
  class: _normalizeClass({ active: isActive })
}, null, 2 /* CLASS */)  // 只需检查class变化
标志值含义优化效果
1TEXT只检查文本
2CLASS只检查class
4STYLE只检查style
8PROPS只检查特定props
16FULL_PROPS完整对比

3. 树结构打平(Tree Flattening)

将模板中的动态节点收集到扁平数组中,遍历时跳过静态节点,减少95%的无效比较。

4. 最长递增子序列优化

在处理节点移动时,找出最长的不需要移动的节点序列,最小化DOM操作次数。


问题2:Vuex和Pinia的区别及选择场景 ​

核心架构对比 ​

特性VuexPinia
API设计State + Mutations + Actions + GettersState + Actions + Getters(无Mutations)
状态更新必须通过commit触发Mutation直接修改state或在Actions中修改
异步处理Actions中调用MutationsActions直接修改状态
TypeScript需手动配置类型声明原生支持,自动类型推导
模块化通过modules嵌套定义,需namespaced多个独立store文件,天然模块化
体积约10-12KB约1KB
Vue版本Vue 2/3主要为Vue 3设计

代码对比 ​

Vuex写法:

JavaScript
// store/index.js
const store = createStore({
  state: { count: 0 },
  mutations: {
    increment(state) { state.count++ }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => commit('increment'), 1000)
    }
  }
})
// 组件中使用
this.$store.commit('increment')
this.$store.dispatch('incrementAsync')

Pinia写法:

JavaScript
// stores/counter.js
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  actions: {
    increment() { this.count++ },
    async incrementAsync() {
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.count++
    }
  }
})
// 组件中使用
const counter = useCounterStore()
counter.increment()

选择场景 ​

选择Pinia的场景:

  • Vue 3新项目

  • 需要良好的TypeScript支持

  • 追求简洁API和开发效率

  • 中小型项目

选择Vuex的场景:

  • 维护现有Vuex项目

  • 需要严格的变更追踪(mutations提供明确记录)

  • 大型团队需要严格代码规范

  • Vue 2项目


问题3:Axios请求/响应拦截器设计 ​

二次封装设计 ​

JavaScript
// utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'

const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 15000
})

let isRefreshing = false
let requestQueue = []

const executeQueue = (token) => {
  requestQueue.forEach(cb => cb(token))
  requestQueue = []
}

// 请求拦截器
service.interceptors.request.use(
  config => {
    const token = localStorage.getItem('token')
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`
    }
    return config
  },
  error => Promise.reject(error)
)

// 响应拦截器
service.interceptors.response.use(
  response => {
    const res = response.data
    if (res.code !== 200) {
      ElMessage.error(res.message || '请求失败')
      return Promise.reject(new Error(res.message))
    }
    return res
  },
  async error => {
    const originalRequest = error.config

    // Token过期处理(401)
    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        return new Promise((resolve) => {
          requestQueue.push((token) => {
            originalRequest.headers['Authorization'] = `Bearer ${token}`
            resolve(service(originalRequest))
          })
        })
      }

      originalRequest._retry = true
      isRefreshing = true

      try {
        const refreshToken = localStorage.getItem('refreshToken')
        const { data } = await axios.post('/auth/refresh', { refreshToken })
        localStorage.setItem('token', data.accessToken)
        executeQueue(data.accessToken)
        originalRequest.headers['Authorization'] = `Bearer ${data.accessToken}`
        return service(originalRequest)
      } catch (refreshError) {
        localStorage.removeItem('token')
        localStorage.removeItem('refreshToken')
        router.push('/login')
        return Promise.reject(refreshError)
      } finally {
        isRefreshing = false
      }
    }

    // 网络错误处理
    if (!error.response) {
      ElMessage.error('网络连接异常,请检查网络')
      return Promise.reject(error)
    }

    // 其他HTTP错误
    const errorMessages = {
      403: '没有权限访问',
      404: '请求资源不存在',
      500: '服务器内部错误'
    }
    ElMessage.error(errorMessages[error.response.status] || '请求失败')
    return Promise.reject(error)
  }
)

export default service

关键设计点 ​

  1. Token自动注入:请求拦截器统一添加Authorization头

  2. Token无感刷新:使用并发锁+请求队列,避免多次刷新请求

  3. 统一错误处理:响应拦截器统一处理业务错误和HTTP错误

  4. 请求重试机制:可添加指数退避重试策略


问题7:微信小程序生命周期 ​

应用生命周期(App级) ​

在app.js中定义,管控整个小程序的启动与销毁:

JavaScript
App({
  onLaunch(options) {
    // 小程序首次启动触发,全局仅执行1次
    // 适合:初始化全局数据、获取用户授权、请求全局配置
  },
  onShow(options) {
    // 小程序启动/从后台切到前台触发,可多次执行
    // 适合:刷新全局状态、重启定时器
  },
  onHide() {
    // 小程序从前台切到后台触发
    // 适合:暂停定时器、保存临时数据
  },
  onError(msg) {
    // 小程序发生脚本错误时触发
    // 适合:捕获全局错误、上报错误日志
  },
  onPageNotFound() {
    // 访问不存在的页面时触发
    // 适合:页面跳转兜底
  }
})

页面生命周期(Page级) ​

在页面的.js文件中定义,控制单个页面的加载与卸载:

JavaScript
Page({
  onLoad(options) {
    // 页面首次加载触发,仅执行1次
    // 适合:接收页面参数、初始化页面数据、请求核心接口
  },
  onShow() {
    // 页面显示/从后台切回前台触发,可多次执行
    // 适合:刷新页面数据、启动页面定时器
  },
  onReady() {
    // 页面初次渲染完成触发,仅执行1次
    // 适合:操作DOM、初始化第三方组件
  },
  onHide() {
    // 页面被隐藏触发(跳转其他页面或切后台)
    // 适合:暂停定时器、保存临时表单数据
  },
  onUnload() {
    // 页面被卸载触发(redirectTo、navigateBack)
    // 适合:清除定时器、取消未完成的请求
  },
  onPullDownRefresh() {
    // 监听用户下拉刷新
  },
  onReachBottom() {
    // 页面上拉触底触发
    // 适合:加载下一页数据
  },
  onShareAppMessage() {
    // 用户点击分享
  }
})

核心区别 ​

维度应用生命周期页面生命周期
作用范围整个小程序单个页面
定义位置app.js页面.js文件
执行次数onLaunch仅1次onLoad仅1次,onShow多次
执行顺序onLaunch → onShowonLoad → onShow → onReady
数据传递通过globalData通过options参数

问题8:微信支付完整流程 ​

完整支付流程 ​

Plain
用户选择商品 → 前端请求下单 → 后端调用微信统一下单API
→ 获取prepay_id → 前端调起支付 → 用户输入密码
→ 微信异步通知后端 → 后端验签更新订单 → 前端查询订单状态

具体实现 ​

后端下单接口(Java Spring Boot):

Java
@PostMapping("/api/pay/create")
public Result createOrder(@RequestBody OrderDTO orderDTO) {
    // 1. 创建统一下单请求
    WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest();
    request.setBody(orderDTO.getBody());
    request.setOutTradeNo(orderDTO.getOrderNo());
    request.setTotalFee(orderDTO.getTotalFee()); // 单位:分
    request.setNotifyUrl("https://your.domain/api/pay/notify");
    request.setTradeType("JSAPI");
    request.setOpenid(orderDTO.getOpenid());

    // 2. 调用微信统一下单接口
    WxPayUnifiedOrderResult result = wxPayService.unifiedOrder(request);

    // 3. 生成前端支付参数
    Map<String, String> payInfo = new HashMap<>();
    payInfo.put("appId", appId);
    payInfo.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
    payInfo.put("nonceStr", WxPayUtil.generateNonceStr());
    payInfo.put("package", "prepay_id=" + result.getPrepayId());
    payInfo.put("signType", "RSA");
    payInfo.put("paySign", WxPayUtil.generateSignature(payInfo, privateKey));

    return Result.success(payInfo);
}

支付回调处理:

Java
@PostMapping("/api/pay/notify")
public String payNotify(HttpServletRequest request) {
    // 1. 验证签名
    // 2. 解密回调数据
    // 3. 更新订单状态(幂等处理)
    // 4. 返回成功响应
    return "<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>";
}

小程序前端调起支付:

JavaScript
// pages/pay/pay.js
Page({
  handlePay() {
    wx.request({
      url: 'https://your.domain/api/pay/create',
      method: 'POST',
      data: { orderNo: this.data.orderNo, totalFee: this.data.totalFee },
      success: (res) => {
        if (res.data.code === 0) {
          const payParams = res.data.data
          wx.requestPayment({
            timeStamp: payParams.timeStamp,
            nonceStr: payParams.nonceStr,
            package: payParams.package,
            signType: payParams.signType,
            paySign: payParams.paySign,
            success: () => {
              // 支付成功,查询订单状态确认
              this.queryOrderStatus()
            },
            fail: (err) => {
              if (err.errMsg.includes('cancel')) {
                wx.showToast({ title: '已取消支付', icon: 'none' })
              } else {
                wx.showToast({ title: '支付失败', icon: 'none' })
              }
            }
          })
        }
      }
    })
  },

  queryOrderStatus() {
    // 调用后端查询订单接口确认支付状态
    wx.request({
      url: `https://your.domain/api/pay/query?orderNo=${this.data.orderNo}`,
      success: (res) => {
        if (res.data.data.status === 'PAID') {
          wx.showToast({ title: '支付成功' })
          wx.redirectTo({ url: '/pages/order/detail?orderNo=' + this.data.orderNo })
        }
      }
    })
  }
})

异常处理要点 ​

  1. 签名验证:必须验证回调签名,确保来源真实

  2. 幂等处理:使用商户订单号做幂等键,防止重复处理

  3. 超时处理:设置合理的支付超时时间

  4. 并发处理:防止重复支付,对按钮做防抖

  5. 补单机制:定时查询未确认订单的状态


问题9:性能优化具体案例 ​

案例:电商列表页性能优化 ​

问题发现:

  • 使用Lighthouse检测,FCP(首次内容渲染)达到4.5秒

  • 使用Webpack Bundle Analyzer发现打包体积3.2MB

  • 长列表滚动卡顿,用户反馈体验差

优化策略:

1. 路由懒加载(体积减少40%)

JavaScript
const routes = [
  {
    path: '/product-list',
    component: () => import(/* webpackChunkName: "product" */ '@/views/ProductList.vue')
  }
]

2. 第三方库按需引入

JavaScript
// 优化前:全量引入
import ElementPlus from 'element-plus'

// 优化后:按需引入
import { ElButton, ElCard, ElPagination } from 'element-plus'

3. 虚拟滚动优化长列表

Plain
<template>
  <RecycleScroller
    :items="productList"
    :item-size="120"
    key-field="id"
    v-slot="{ item }"
  >
    <ProductCard :product="item" />
  </RecycleScroller>
</template>

4. 图片懒加载+WebP格式

Plain
<template>
  <img v-lazy="product.imageWebp" loading="lazy" />
</template>

5. CDN加速+Gzip压缩

Nginx
# nginx配置
gzip on;
gzip_types text/plain application/javascript application/x-javascript text/css;

优化效果:

指标优化前优化后提升
JS总体积3.2MB680KB↓78%
FCP时间4.5s0.8s↑462%
Lighthouse评分58分92分↑58%

问题10:Canvas图片压缩的质量与压缩率平衡 ​

实现方案 ​

JavaScript
function compressImage(file, maxWidth = 1920, quality = 0.8) {
  return new Promise((resolve) => {
    const reader = new FileReader()
    reader.onload = (e) => {
      const img = new Image()
      img.onload = () => {
        const canvas = document.createElement('canvas')
        const ctx = canvas.getContext('2d')

        // 计算压缩后的尺寸
        let { width, height } = img
        if (width > maxWidth) {
          height = (maxWidth / width) * height
          width = maxWidth
        }

        canvas.width = width
        canvas.height = height

        // 绘制图片
        ctx.drawImage(img, 0, 0, width, height)

        // 导出压缩后的图片
        canvas.toBlob((blob) => {
          resolve(new File([blob], file.name, { type: 'image/jpeg' }))
        }, 'image/jpeg', quality)
      }
      img.src = e.target.result
    }
    reader.readAsDataURL(file)
  })
}

质量与压缩率平衡策略 ​

  1. 根据图片尺寸动态调整质量

    • 大图(>2MB):quality = 0.6

    • 中图(1-2MB):quality = 0.7-0.8

    • 小图(<1MB):quality = 0.8-0.9

  2. 确保OCR清晰度

    • 保持最小分辨率(如1000px宽度)

    • 使用image/png格式保留文字边缘清晰度

    • 对于文档类图片,quality不低于0.85

  3. 渐进式压缩

JavaScript
async function smartCompress(file) {
  const size = file.size / 1024 / 1024 // MB

  if (size > 5) {
    return compressImage(file, 1600, 0.6)
  } else if (size > 2) {
    return compressImage(file, 1920, 0.75)
  } else {
    return compressImage(file, 2560, 0.85)
  }
}

问题11:基于知识库的AI问答功能实现 ​

技术架构 ​

Plain
用户提问 → 向量化查询 → 知识库检索 → 上下文拼接 → LLM生成回答

实现方案 ​

1. 知识库向量化存储

JavaScript
// 使用Embedding API将文档向量化
async function vectorizeDocument(text) {
  const response = await openai.embeddings.create({
    model: "text-embedding-ada-002",
    input: text
  })
  return response.data[0].embedding
}

// 存储到向量数据库(如Pinecone、Milvus)
async function storeVector(id, embedding, metadata) {
  await vectorDB.upsert({
    id,
    values: embedding,
    metadata
  })
}

2. 语义检索

JavaScript
async function searchKnowledgeBase(query, topK = 5) {
  const queryEmbedding = await vectorizeDocument(query)

  const results = await vectorDB.query({
    vector: queryEmbedding,
    topK,
    includeMetadata: true
  })

  return results.matches.map(match => ({
    content: match.metadata.content,
    score: match.score
  }))
}

3. RAG问答实现

JavaScript
async function answerQuestion(question) {
  // 1. 检索相关文档
  const relevantDocs = await searchKnowledgeBase(question)

  // 2. 构建上下文
  const context = relevantDocs
    .map(doc => doc.content)
    .join('\n\n')

  // 3. 调用LLM生成回答
  const response = await openai.chat.completions.create({
    model: "gpt-4",
    messages: [
      {
        role: "system",
        content: `你是一个基于知识库的问答助手。请根据以下参考资料回答用户问题,如果资料中没有相关信息,请说明无法找到相关内容。

参考资料:
${context}`
      },
      {
        role: "user",
        content: question
      }
    ]
  })

  return response.choices[0].message.content
}

使用的技术/工具 ​

  • Embedding模型:OpenAI text-embedding-ada-002、本地模型如BGE

  • 向量数据库:Pinecone、Milvus、Chroma、Weaviate

  • LLM:OpenAI GPT-4、Claude、本地部署的开源模型

  • 文档处理:LangChain、LlamaIndex


问题12:AI工具提高开发效率的经验 ​

实际应用场景 ​

1. 代码生成与补全

  • 使用AI IDE(如Trae)的智能补全功能,快速生成样板代码

  • 通过自然语言描述需求,AI生成完整函数实现

2. 代码审查与优化

  • 让AI分析代码,发现潜在的性能问题和安全漏洞

  • 获取重构建议,提升代码质量

3. 文档与注释生成

  • 自动生成API文档、代码注释

  • 快速编写README、变更日志

4. 问题排查

  • 将错误信息提供给AI,快速定位问题原因

  • 获取解决方案和修复代码

5. 学习新技术

  • 通过对话方式学习新框架、新API

  • 获取最佳实践和设计模式建议

效率提升案例 ​

  • 组件开发:通过描述需求,AI生成组件框架代码,开发时间从2小时缩短到30分钟

  • Bug修复:将错误堆栈提供给AI,平均定位时间从30分钟缩短到5分钟

  • 单元测试:AI根据函数签名自动生成测试用例,覆盖率从40%提升到80%


问题13:多端电商应用技术架构设计 ​

技术选型 ​

Plain
┌─────────────────────────────────────────────┐
│                    表现层                      │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐      │
│  │  Web    │  │  H5     │  │ 小程序   │      │
│  │  Vue3   │  │  Vue3   │  │ uni-app │      │
│  └─────────┘  └─────────┘  └─────────┘      │
├─────────────────────────────────────────────┤
│                 跨端框架层                     │
│            uni-app / Taro                     │
├─────────────────────────────────────────────┤
│                 状态管理                       │
│               Pinia                          │
├─────────────────────────────────────────────┤
│                 网络请求                       │
│           Axios / uni.request                 │
├─────────────────────────────────────────────┤
│                 后端服务                       │
│        Node.js / Java / Go                   │
└─────────────────────────────────────────────┘

处理不同端差异的策略 ​

1. 条件编译

JavaScript
// #ifdef H5
console.log('H5端特有逻辑')
// #endif

// #ifdef MP-WEIXIN
console.log('微信小程序特有逻辑')
// #endif

// #ifdef APP-PLUS
console.log('App端特有逻辑')
// #endif

2. 平台适配层

JavaScript
// utils/platform.js
export const platform = {
  isH5: process.env.UNI_PLATFORM === 'h5',
  isMP: process.env.UNI_PLATFORM === 'mp-weixin',
  isApp: process.env.UNI_PLATFORM === 'app-plus',

  // 统一API封装
  async login() {
    if (this.isMP) {
      return uni.login({ provider: 'weixin' })
    } else if (this.isH5) {
      return h5Login()
    }
  },

  // 支付适配
  async pay(orderInfo) {
    if (this.isMP) {
      return wxPay(orderInfo)
    } else if (this.isH5) {
      return h5Pay(orderInfo)
    } else if (this.isApp) {
      return appPay(orderInfo)
    }
  }
}

3. 组件差异化

Plain
<template>
  <!-- 通用组件 -->
  <view class="product-card">
    <image :src="product.image" mode="aspectFill" />
    <text>{{ product.name }}</text>

    <!-- 小程序端显示分享按钮 -->
    <!-- #ifdef MP-WEIXIN -->
    <button open-type="share">分享</button>
    <!-- #endif -->
  </view>
</template>

4. 样式适配

SCSS
// 使用rpx实现响应式布局
.product-card {
  width: 750rpx;
  padding: 20rpx;

  // H5端特殊样式
  /* #ifdef H5 */
  max-width: 750px;
  margin: 0 auto;
  /* #endif */
}