RESTful、Git、SEO、HTTP 协议、缓存、Nginx
杂七杂八的一些知识点
REST(REpresentational State Transfer,表述性状态转移)是由 Roy Fielding 在其 2000 年的博士论文中提出的一种软件架构风格,RESTful 即"符合 REST 原则的" Web 服务设计方式。
一、核心概念
1. 资源(Resource)
REST 的核心思想是一切皆资源。每个资源用一个唯一的 URI(统一资源标识符)来标识。
/users → 用户集合
/users/123 → ID 为 123 的用户
/users/123/orders → 该用户的订单集合2. 表述(Representation)
资源本身是抽象的,客户端拿到的是资源的表述(如 JSON、XML、HTML 等):
{
"id": 123,
"name": "张三",
"email": "zhangsan@example.com"
}3. 状态转移(State Transfer)
客户端通过 HTTP 动词(GET、POST、PUT、DELETE 等)来操作资源的状态。
不是协议,不是标准,不是强制规范,只是一种建议的设计风格
二、六大约束
| 约束 | 说明 |
|---|---|
| 客户端-服务器 | 前后端分离,关注点分离 |
| 无状态 | 每次请求包含所有必要信息,服务端不保存会话状态 |
| 可缓存 | 响应可标记为可缓存或不可缓存 |
| 统一接口 | 核心约束,下面详细展开 |
| 分层系统 | 客户端无需关心是否直连服务器,中间可以有负载均衡、代理等 |
| 按需代码(可选) | 服务端可向客户端发送可执行代码(如 JS) |
三、统一接口(REST 最核心的特征)
统一接口包含四个子约束:
1. 资源标识:URI
每个资源用唯一且一致的 URI 标识,URI 只描述名词,不描述动作。
| ✅ 推荐 | ❌ 不推荐 |
|---|---|
GET /users/123 | GET /getUser?id=123 |
DELETE /users/123 | POST /deleteUser |
2. 资源表述:通过 HTTP 消息交换
请求和响应中使用标准的表述格式(JSON、XML 等),通过 Content-Type 和 Accept 头协商。
3. 自描述消息
每个请求/响应包含足够的信息来说明如何处理它:
Content-Type: application/jsonHTTP/1.1 200 OKCache-Control: no-cache
4. HATEOAS(超媒体即应用状态引擎)
客户端通过服务器返回的链接来发现可用操作:
{
"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" }
]
}所谓无状态就是每次都要带上所有信息,比如用户登录后,不是服务端记住状态,而是每次发请求客户端携带凭证,每个请求都是独立的,这样可以加多服务器,实现负载均衡
服务端:
四、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 设计规范
✅ /users → 用户集合
✅ /users/123 → 单个用户
✅ /users/123/orders → 用户的订单子资源
✅ /users/123/orders/456 → 某个具体订单
❌ /users/123/get → 不要用动词
❌ /GetUser → 不要用大驼峰
❌ /user_list → 不要用下划线连接版本控制
https://api.example.com/v1/users
https://api.example.com/v2/users过滤、分页、排序
GET /users?status=active&page=1&per_page=20&sort=-created_at请求/响应头
Content-Type: application/json
Accept: application/json
Authorization: Bearer <token>七、RESTful vs 其他风格
| 特性 | REST | GraphQL | gRPC |
|---|---|---|---|
| 协议 | HTTP/1.1 | HTTP | HTTP/2 |
| 数据格式 | JSON/XML | JSON | Protobuf |
| 接口定义 | 无强制 | Schema 强类型 | .proto 文件 |
| 适用场景 | 通用 Web API | 复杂查询/移动端 | 高性能微服务 |
总结
RESTful 的本质是:
用 URI 命名资源,用 HTTP 方法表达操作,用状态码描述结果,保持无状态通信。
它不是一种协议或标准,而是一种设计哲学——通过统一接口、无状态、资源导向等约束,构建可伸缩、可维护的 Web 服务架构。
实现这种风格restful
ai写,生成接口文档
Vibe coding
git操作
seo优化,搜索引擎优化
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),搜索引擎爬虫访问网站时首先读取它。
示例:
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 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 的价值:
帮助搜索引擎发现新页面(尤其是深层页面)
告诉搜索引擎哪些页面最重要
加速页面被收录的速度
对大型网站(数千页面)尤其重要
两者的关系
搜索引擎爬虫访问网站
│
▼
读取 robots.txt ──→ 确定能爬哪些目录/页面
│
▼
根据 Sitemap ──→ 知道有哪些重要页面需要爬取
│
▼
开始抓取页面内容并建立索引简单类比:
robots.txt = 门卫,告诉你"哪些房间能进,哪些不能进"
Sitemap = 建筑平面图,告诉你"这栋楼有哪些房间,分别在哪里"
两者配合使用,是技术 SEO 的基础配置。
offset,client,scroll
HTTP 协议基础详解
一、HTTP 的定义
HTTP(HyperText Transfer Protocol,**超文本传输协议**) 是一种用于分布式、协作式和超媒体信息系统的应用层协议。它基于请求-响应模型:
客户端 (Client) ---- HTTP 请求 (Request) ----> 服务器 (Server)
客户端 (Client) <--- HTTP 响应 (Response) ---- 服务器 (Server)客户端:通常是浏览器,也可以是任何发送 HTTP 请求的程序(如 curl、Postman、移动端 App)
服务器:接收请求并返回响应的 Web 服务器(如 Nginx、Apache、Node.js 服务)
一次完整的 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 中提取用户信息用于业务处理,整个过程无需查询数据库,实现无状态认证。
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方法
客户端 服务器
|---- TCP 连接 ---->| 建立连接
|---- GET /a ------>| 请求资源
|<--- 200 OK ------| 响应
|---- TCP 断开 ---->| 关闭连接
|---- TCP 连接 ---->| 重新建立连接
|---- GET /b ------>| 请求资源
|<--- 200 OK ------| 响应
|---- TCP 断开 ---->| 关闭连接问题:每个资源都需单独建立 TCP 连接,开销大、效率低。
HTTP/1.1(1997 年,使用最广泛的版本)
核心改进:
持久连接(Keep-Alive)默认开启
- 一个 TCP 连接可以发送多个请求和响应,不用每次都建立新连接
Plain客户端 服务器 |---- TCP 连接 ---->| 建立连接(一次) |---- GET /a ------>| |<--- 200 OK ------| |---- GET /b ------>| 同一连接复用 |<--- 200 OK ------| |---- GET /c ------>| |<--- 200 OK ------| |---- TCP 断开 ---->| 最终关闭管线化(Pipelining)
客户端可以在不等待响应的情况下连续发送多个请求
但服务器必须按序响应(队头阻塞问题)
Plain客户端发出: req1 -> req2 -> req3 服务器返回: res1 -> res2 -> res3 (必须按顺序)实际上由于实现复杂、队头阻塞等问题,很多浏览器默认不启用管线化。
Host 头部(虚拟主机)
同一 IP 的服务器可以托管多个域名
Host: www.example.com成为必传头
新增请求方法:
PUT、DELETE、OPTIONS、TRACE、CONNECT分块传输编码(Chunked Transfer Encoding)
- 服务器可以分块发送响应,不需要提前知道
Content-Length
- 服务器可以分块发送响应,不需要提前知道
更丰富的缓存机制
- 引入
Cache-Control、ETag、If-None-Match等头部
- 引入
HTTP/2(2015 年)
核心改进:
二进制分帧层(Binary Framing)
HTTP/1.x 是纯文本协议,HTTP/2 将数据分割为更小的二进制帧
更易于解析,更高效
PlainHTTP/1.1: "GET /index.html HTTP/1.1\r\nHost: ..." (纯文本) HTTP/2: [帧头][帧数据] [帧头][帧数据] [帧头][帧数据] (二进制帧)多路复用(Multiplexing)
最关键改进:一个 TCP 连接上可以并发传输多个请求和响应
彻底解决了 HTTP/1.1 的队头阻塞问题(应用层)
每个请求/响应通过 Stream ID 标识,互不干扰
Plain一个 TCP 连接: Stream 1: |--req--|----------|--res--| Stream 2: |---req---|-----|----res----| Stream 3: |--req--|----res----| 帧交错发送,不需要等待前一个完成头部压缩(HPACK)
使用 HPACK 算法压缩 HTTP 头部
维护一个头部字段索引表,避免重复传输相同头部
大幅减少头部开销(尤其是 Cookie 等重复信息)
服务端推送(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流优先级(Stream Priority)
- 客户端可以为不同的流设置优先级和依赖关系
注意:HTTP/2 在应用层解决了队头阻塞,但 TCP 层面的队头阻塞依然存在(一个 TCP 包丢失会阻塞所有流)。这正是 HTTP/3 要解决的问题。
HTTP/3(基于 QUIC,2022 年标准化)
核心改进:
底层传输协议从 TCP 改为 QUIC(基于 UDP)
PlainHTTP/1.1 & HTTP/2: HTTP → TCP → IP HTTP/3: HTTP → QUIC (UDP) → IP彻底解决队头阻塞
QUIC 在传输层也支持多路复用
单个流的数据包丢失不会影响其他流
更快的连接建立
TCP + TLS 需要 2-3 个 RTT(往返时间)
QUIC 将传输层握手和 TLS 握手合并,首次连接仅需 1 个 RTT
0-RTT 恢复:已连接过的客户端可以 0 延迟恢复连接
连接迁移
TCP 连接基于四元组(源IP、源端口、目标IP、目标端口)
网络切换(如 WiFi → 4G)会导致连接断开
QUIC 使用连接 ID 标识连接,网络切换时连接不中断
内置 TLS 1.3 加密
- 加密不再是可选项,所有 QUIC 连接都默认加密
版本对比总结
| 特性 | HTTP/1.0 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|---|
| 连接方式 | 短连接 | 持久连接 | 多路复用 | 多路复用 |
| 传输格式 | 文本 | 文本 | 二进制帧 | 二进制帧 |
| 队头阻塞 | 存在 | 存在(严重) | TCP层仍存在 | 彻底解决 |
| 头部压缩 | 无 | 无 | HPACK | QPACK |
| 服务端推送 | 不支持 | 不支持 | 支持 | 支持 |
| 底层协议 | TCP | TCP | TCP | QUIC(UDP) |
| 连接建立 | 0 RTT | 0 RTT | 1-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 请求另一个 URL | POST 后重定向到结果页 |
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 的关键区别
| 特性 | GET | POST |
|---|---|---|
| 参数位置 | URL 查询参数 ?key=value | 请求体 (Body) |
| 参数长度 | URL 上限由客户端、服务器与中间设施决定,没有通用 2KB/8KB 标准 | 请求体同样受各层配置限制,不是无限 |
| 可缓存 | 规范允许 | 满足显式缓存条件时允许,但实践中较少 |
| 可收藏为书签 | ✅ | ❌ |
| 浏览器历史 | 参数保留在 URL 中 | 参数不保留在 URL 中 |
| 安全性 | URL 更易进入历史、日志与 Referer | 请求体不等于加密;两者都应使用 HTTPS |
| 幂等性 | 规范语义幂等 | 规范语义不保证幂等 |
| 内容类型 | 查询串按 URL 规则编码;请求内容没有通用语义 | 可使用多种媒体类型 |
六、追问详解:HTTP 与 HTTPS 的区别
核心区别
HTTP: 客户端 ----[明文数据]----> 服务器 (不安全)
HTTPS: 客户端 ----[加密数据]----> 服务器 (安全)HTTPS = HTTP + SSL/TLS,在 HTTP 的基础上加了一层安全层:
| 特性 | HTTP | HTTPS |
|---|---|---|
| 默认端口 | 80 | 443 |
| 数据传输 | 明文 | 加密 |
| 证书 | 不需要 | 需要 CA 颁发的 SSL 证书 |
| 安全性 | 无 | 防窃听、防篡改、防冒充 |
| 性能 | 快 | 有额外加密开销(现在已可忽略) |
| SEO | 不利 | 搜索引擎优先收录 HTTPS |
HTTPS 提供的三大安全保障
机密性(Confidentiality) —— 加密传输
使用对称加密算法(如 AES)加密数据
即使被截获也无法读取内容
完整性(Integrity) —— 防篡改
- 使用 MAC(消息认证码)确保数据未被修改
身份认证(Authentication) —— 防冒充
通过 CA 证书验证服务器身份
确保连接的是真正的目标服务器
TLS 握手过程(简化)
客户端 服务器
| |
|---- 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-Origin | CORS 跨域允许的源 |
Transfer-Encoding | 传输编码(如 chunked) |
浏览器缓存机制详解
一、定义
浏览器缓存是浏览器将请求过的资源(如HTML、CSS、JS、图片等)保存在本地,当再次请求相同资源时,直接使用缓存而不向服务器发起请求的策略,目的是减少网络传输、提升页面加载速度、降低服务器压力。
二、缓存存储位置优先级
Service Worker → 内存缓存(Memory Cache) → 磁盘缓存(Disk Cache) → 推送缓存(Push Cache)| 存储位置 | 特点 |
|---|---|
| Service Worker | 可编程控制,需HTTPS,容量大 |
| Memory Cache | 读取快,生命周期短(关闭标签页即失效) |
| Disk Cache | 持久化存储,容量较大,读取较慢 |
| Push Cache | HTTP/2专属,会话级别,只在本次连接有效 |
三、强缓存(无需与服务器通信)
强缓存命中时,浏览器直接从本地读取资源,不会发送请求到服务器,状态码显示 200 (from cache) 或 200 (from disk cache)。
1. Expires(HTTP/1.0)
Expires: Thu, 01 Jun 2025 16:00:00 GMT原理:服务器返回资源的绝对过期时间,浏览器在该时间之前直接使用缓存
缺点:依赖客户端本地时间,如果用户修改了本地时间,缓存可能失效或不失效
2. Cache-Control(HTTP/1.1,优先级更高)
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
# 首次响应
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(优先级更高)
# 首次响应
ETag: "abc123def456"
# 后续请求携带
If-None-Match: "abc123def456"原理:服务器返回资源的唯一标识(通常是内容的哈希值),内容变化则标识变化
优点:比 Last-Modified 更精确,能感知内容变化
缺点:需要服务器计算哈希,有一定性能开销
两者同时存在时:
ETag优先级高于Last-Modified
五、完整缓存决策流程
请求资源
↓
检查 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-cache | no-store |
|---|---|---|
| 含义 | 可以缓存,但使用前必须向服务器验证 | 禁止任何缓存,每次都从服务器下载 |
| 是否存储 | 存储到本地缓存 | 不存储到本地缓存 |
| 网络请求 | 每次都发请求(协商缓存) | 每次都发请求(完整下载) |
| 使用场景 | 需要最新版本但允许缓存验证的资源 | 敏感数据(密码、银行卡信息) |
# no-cache:缓存但每次都验证
Cache-Control: no-cache
# no-store:完全不缓存
Cache-Control: no-store2. 如何控制缓存策略?
// 频繁变动的资源(如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) |
八、最佳实践总结
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** 来消费:
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 正在输出:
这是**加粗文此时只有 ** 的左标记,没有右标记,直接渲染会出错。
解决方案有几种策略:
策略一:容忍不完整语法的 Markdown 解析器
使用专门的「流式友好」Markdown 解析器(如 markdown-it 配合自定义处理),它能容忍不完整的语法:
遇到未闭合的
**,暂时当作普通文本渲染遇到未闭合的代码块 ```,保持代码块样式但不执行语法高亮
策略二:分段渲染 + 延迟解析
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 保护,防止不完整标签导致布局异常:
.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 辅助开发工作流:
明确需求 → 构建上下文 → AI 生成代码 → 人工审查 → 测试验证 → 提交上下文定义是核心,关键在于让 AI 准确理解「我在什么环境里,要做什么事」:
示例:我要给项目加一个「文章收藏」功能
我会这样构建 Prompt 上下文:
【项目背景】
这是一个 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 类型要完整
- 不要添加注释上下文的关键要素:
技术栈声明 — 让 AI 知道用什么框架、什么版本
约束声明 — 告诉它项目规范、代码风格、禁止事项
示例代码 — 给它参考样本,比纯文字描述高效 10 倍
明确的输出期望 — 说清楚要什么,不要什么
4. AI 生成代码与项目规范冲突时的约束
多层次约束策略:
第一层:Project Rules / Rules 文件
在项目中创建 .cursorrules、.github/copilot-instructions.md、CLAUDE.md 等规则文件,写明:
## 项目规范
- 使用 Vue 3 Composition API + `<script setup>`
- TypeScript 严格模式
- 组件命名使用 PascalCase
- CSS 使用 scoped + BEM 命名
- 禁止使用 any 类型
- API 请求必须通过封装的 request 实例
- 错误处理必须统一使用 errorHandler第二层:System Prompt 约束
在与 AI 对话时,一开始就在 System Prompt 中声明项目规范,作为全局约束。
第三层:Few-shot 示例
给 AI 展示「正确的代码长什么样」和「错误的代码长什么样」:
✅ 正确示例:
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,而是只给相关模块:
我正在处理用户模块,以下是相关的文件:
- src/stores/user.ts(用户状态管理)
- src/api/user.ts(用户 API)
- src/views/user/Profile.vue(个人资料页面)
请在这几个文件的基础上帮我实现...方法二:代码摘要代替完整代码
对于大的文件,只给关键部分:
以下是 router/index.ts 的路由结构概要:
- / → HomeView
- /login → LoginView
- /user/:id → UserDetailView(需要鉴权)
- /admin → AdminView(需要管理员权限)方法三:分层对话策略
| 层次 | 内容 | Token 消耗 |
|---|---|---|
| 第 1 轮 | 技术栈 + 目录结构 + 规范 | 低 |
| 第 2 轮 | 当前模块的关键文件 | 中 |
| 第 3 轮 | 具体需求 + 参考代码片段 | 低 |
先让 AI 理解项目结构,再逐步深入具体问题。
方法四:善用 **.cursorignore** 等工具
配置 AI IDE 的忽略文件,排除不需要索引的目录:
node_modules/
dist/
public/
*.test.ts
*.spec.ts方法五:RAG(检索增强生成)
对于超大项目,可以:
将项目文档、API 定义、类型定义等结构化存入知识库
AI 按需检索相关上下文,而不是全量加载
这在企业级项目中越来越常见
二、八股文
1. new 操作符做了什么
new 操作符执行时会经历以下 4 个步骤:
function Person(name) {
this.name = name
}
const p = new Person('Tom')等价于:
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
}四个步骤总结:
创建空对象:
let obj = {}链接原型:
obj.__proto__ = Constructor.prototype绑定 this 并执行:
Constructor.call(obj, ...args)判断返回值:构造函数返回的是对象就用返回的,否则用新创建的 obj
2. CommonJS 和 ES6 Module 的区别
| 对比维度 | CommonJS | ES6 Module |
|---|---|---|
| 语法 | require() / module.exports | import / export |
| 加载时机 | 运行时加载(动态) | 编译时静态分析(静态) |
| 值的拷贝 | 深拷贝(原始值复制,对象引用复制) | 活绑定(实时引用,读取的是最新值) |
| Tree Shaking | ❌ 不支持 | ✅ 支持(静态分析可确定哪些模块没用到) |
| 循环依赖 | 可能输出已经执行的部分结果 | 通过活绑定可以正确处理 |
| 环境 | Node.js 原生支持 | 浏览器原生支持,Node.js 需配置 |
| 异步/同步 | 同步加载 | 支持异步加载(import() 动态导入) |
关键区别举例 — 活绑定 vs 值拷贝:
// 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 在二进制中是无限循环小数,无法精确表示。
详细过程:
0.1 → 二进制: 0.000110011001100110011...(无限循环)
0.2 → 二进制: 0.00110011001100110011...(无限循环)由于 64 位浮点数的尾数只有 52 位,超出部分会被截断,产生精度丢失。
0.1 + 0.2 = 0.30000000000000004 ≠ 0.3解决方案:
// 方法一: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 防护
输入过滤和输出编码
使用
HttpOnlyCookie 防止 JS 读取CSP 限制内联脚本
⑤ CSRF 防护
SameSite Cookie 属性(
Strict/Lax)CSRF Token
验证 Referer / Origin 头
⑥ 其他安全机制
X-Frame-Options:防止点击劫持(Clickjacking),限制页面是否可以被 iframe 嵌入
X-Content-Type-Options:防止 MIME 嗅探,设置
nosniffHSTS(HTTP Strict Transport Security):强制使用 HTTPS
Sandbox:iframe 的沙箱属性,限制嵌入内容的能力
5. 浏览器的渲染机制
完整渲染流程(从 URL 到像素):
URL → 网络请求 → 解析HTML → 构建DOM树
↓
解析CSS → 构建CSSOM树
↓
DOM + CSSOM → 渲染树(Render Tree)
↓
布局(Layout/Reflow)→ 计算每个节点的位置和大小
↓
绘制(Paint)→ 生成绘制指令
↓
合成(Composite)→ GPU 合成图层并显示详细步骤:
解析 HTML → DOM 树
逐行解析 HTML,遇到
<script>会阻塞解析(除非 async/defer)生成树形 DOM 结构
解析 CSS → CSSOM 树
解析所有 CSS(包括外部样式表、内联样式、继承样式)
CSS 不会阻塞 DOM 解析,但会阻塞渲染
构建渲染树(Render Tree)
只包含可见节点(
display: none的节点不包含)visibility: hidden会包含在渲染树中(占据空间)为每个节点计算最终的样式值(层叠、继承、优先级)
布局(Layout / Reflow)
计算每个节点的几何信息(位置、宽高)
相对单位转换为绝对像素
绘制(Paint)
将布局信息转换为绘制指令(像素)
按层绘制背景、文字、图片、边框等
合成(Composite)
将多个图层合并
GPU 加速处理(transform、opacity 等属性触发合成层提升)
最终输出到屏幕
6. 怎么避免重排(Reflow)和重绘(Repaint)
先区分概念:
重排(Reflow):元素的几何属性(位置、大小)改变,需要重新计算布局。代价大。
重绘(Repaint):元素外观改变(颜色、背景),但不影响布局。代价相对小。
重排一定会引起重绘,重绘不一定引起重排。
触发重排的操作:
增删可见 DOM 元素
元素尺寸改变(宽高、边距、边框、内边距)
内容变化(文字、图片尺寸)
浏览器窗口 resize
读取布局属性(
offsetWidth、scrollTop、getComputedStyle、getBoundingClientRect)
避免重排重绘的策略:
① 批量修改 DOM
// ❌ 每次修改都触发重排
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
// 使用 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' // 只触发两次重排③ 避免频繁读取布局属性
// ❌ 读写交替导致强制同步布局(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
// ❌ 触发重排
el.style.top = '100px'
// ✅ 只触发合成,不重排不重绘
el.style.transform = 'translateY(100px)'⑤ 使用 **will-change** 提示浏览器
.animated-element {
will-change: transform, opacity;
}⑥ 使用 **requestAnimationFrame** 批量处理动画
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 + Atomics | Worker 间高性能数据共享 |
8. TCP 是怎么实现可靠传输的
TCP 通过以下机制实现可靠传输:
① 三次握手建立连接
客户端 → 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 的队头阻塞问题:
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 模式
// 原理:监听 hashchange 事件
window.addEventListener('hashchange', () => {
const hash = window.location.hash // #/about
// 根据 hash 匹配路由,渲染对应组件
})
// URL 形式:http://example.com/#/about不会向服务器发送请求(hash 变化不会触发页面刷新)
兼容性好,支持 IE
#/部分不会发送给服务器
② History 模式
// 原理:使用 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 的实现核心:
// 简化版实现思路
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 的异步流程
整体流程:
URL 变化 → 路由匹配 → 触发导航守卫(beforeEach → beforeEnter → beforeRouteEnter)
→ 组件解析(异步组件加载)→ 更新 <router-view> 渲染目标组件 → afterEach12. 正常开发项目时性能优化思路
我会按照 「度量 → 分析 → 优化 → 验证」 的闭环流程来做:
第一步:度量 — 用数据说话
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-ifvsv-showv-for必须加key组件级缓存(
<keep-alive>)大组件使用异步组件(
defineAsyncComponent)computed缓存计算结果,避免模板中做复杂计算shallowRef/shallowReactive减少深层响应式开销长列表使用虚拟滚动组件
第三步:验证
优化前后用同一工具测量,对比数据
确保优化有效且没有引入新问题
核心理念:性能优化不是一锤子买卖,而是持续的过程。先找到瓶颈(通常是网络请求和渲染),再有针对性地优化。不要过早优化,也不要盲目优化。
1. SSE (Server-Sent Events)
SSE 的原理:
SSE 基于 HTTP 协议,客户端通过
EventSourceAPI 建立一个持久的 HTTP 连接服务端响应头设置
Content-Type: text/event-stream,表示这是一个事件流服务端通过这个持久连接,以文本格式一段一段地发送数据,每段数据格式如下:
Plaindata: {"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
对话中的总结基本正确,补充一下:
| 特性 | SSE | WebSocket |
|---|---|---|
| 方向 | 单向(服务端→客户端) | 全双工(双向) |
| 协议 | 基于 HTTP | 独立的 ws:// 协议 |
| 数据格式 | 仅文本 | 文本 + 二进制 |
| 重连机制 | 内置自动重连 | 需要手动实现 |
| 适用场景 | 消息通知、AI 流式对话 | 即时通讯、实时游戏 |
3. 登录流程与 Token 存储
Speaker 1 的回答基本正确,补充完善:
客户端发送账号密码(HTTPS POST,密码在 body 中)
服务端验证后返回一个 JWT Token
将 Token 存储在 Cookie 中,并设置安全属性
后续请求自动携带 Cookie 中的 Token
关于密码加密:
前端可以用 RSA 公钥加密密码再传输(后端用私钥解密)
或者前端做一次 哈希(如 SHA-256) 再传输,但注意这不能替代 HTTPS
4. Cookie 安全属性
Speaker 1 提到了 HttpOnly 和 SameSite,但还有几个重要属性:
| 属性 | 作用 |
|---|---|
HttpOnly | 禁止 JavaScript 直接读取 Cookie,降低凭据被窃取风险;不能阻止 XSS 以用户身份发请求 |
SameSite=Strict/Lax/None | 控制跨站请求是否携带 Cookie,可降低部分 CSRF 风险但不能替代完整防护 |
Secure | 只在 HTTPS 连接下传输 Cookie |
Domain | 指定 Cookie 所属域名 |
Path | 指定 Cookie 的有效路径 |
Max-Age / Expires | Cookie 的过期时间 |
CSRF 攻击原理(Speaker 1 解释基本正确):
攻击者诱导用户访问恶意网站
恶意网站向目标网站发起请求(如
<img src="目标网站/api/transfer?to=attacker&amount=1000">)浏览器会自动携带目标网站的 Cookie,导致请求被当作合法请求处理
5. XSS 攻击与防御
Speaker 1 提到了三种类型,补充一下具体的防御手段:
防御措施:
输入过滤/输出转义:将
<转义为<,>转义为>,"转义为"等使用 **
textContent** 而非 **v-html**(Vue 中)或innerText而非innerHTML(原生 JS)CSP(Content Security Policy):通过 HTTP 头
Content-Security-Policy限制可执行脚本的来源白名单对用户输入进行严格校验和过滤
关于转义的具体做法:
不是简单删除
<script>标签(这样容易被绕过,如<img onerror="alert(1)">)而是将所有 HTML 特殊字符进行实体编码,这样浏览器会将其当作纯文本显示
6. 二叉树遍历
Speaker 1 的回答基本正确。层序遍历(BFS)的伪代码:
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等),是为了替代ExpiresETag比Last-Modified更精确,因为文件可能在 1 秒内多次修改,或者内容没变但时间变了(如 touch 命令)
缓存流程:
1. 强缓存检查 → 命中则直接使用缓存(200 from cache)
2. 强缓存未命中 → 发起协商缓存请求
3. 协商缓存命中 → 返回 304 Not Modified,使用缓存
4. 协商缓存未命中 → 返回 200 + 新资源1. Last-Modified vs ETag 的区别
Speaker 1 的回答基本正确,补充一下完整对比:
| 特性 | Last-Modified / If-Modified-Since | ETag / 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):
原理:
打包前:app.js
打包后:app.a1b2c3d4.js(文件内容的哈希值作为文件名的一部分)流程:
构建工具(Webpack/Vite)在打包时,会根据文件内容生成一个哈希值,拼接到文件名中
HTML 文件引用的是带哈希的文件名:
<script src="/js/app.a1b2c3d4.js"></script>当 CSS/JS 内容更新后,重新打包会生成新的哈希:
app.x9y8z7w6.jsHTML 文件也会更新引用:
<script src="/js/app.x9y8z7w6.js"></script>因为 HTML 走的是协商缓存,浏览器会发现 HTML 变了,进而加载新的 JS 文件
旧的 JS 文件(
app.a1b2c3d4.js)因为没人引用了,浏览器会自然淘汰
这就是为什么:
HTML 用协商缓存(需要及时发现更新)
带哈希的静态资源用强缓存(哈希变了就是新文件,哈希没变就是同一个文件,可以放心缓存)
总结一句话: 不是去"刷新"用户的缓存,而是通过文件名的变化来"绕过"缓存,让浏览器主动请求新资源。
问题1:Vue的虚拟DOM工作原理及diff算法优化
虚拟DOM的工作原理
虚拟DOM(Virtual DOM)是Vue渲染系统的核心,其本质是一个轻量级的JavaScript对象,是对真实DOM的抽象表示。
核心流程(渲染管线):
编译阶段:Vue模板被编译为渲染函数(render function),该函数返回虚拟DOM树
挂载阶段:运行时渲染器调用渲染函数,遍历返回的虚拟DOM树,创建对应的真实DOM节点。此过程作为响应式副作用执行,会追踪所有依赖
补丁阶段(Patch):当依赖的数据变化时,副作用重新运行,生成新的虚拟DOM树,渲染器对比新旧两棵树,计算差异并应用到真实DOM
// 虚拟节点的基本结构
const vnode = {
type: 'div',
props: { id: 'hello', class: 'container' },
children: [
{ type: 'span', props: {}, children: 'Hello World' }
]
}Vue的diff算法优化
Vue 2的双端比较算法
Vue 2采用双端指针法进行子节点比较:
头-头比较:旧头节点与新头节点对比
尾-尾比较:旧尾节点与新尾节点对比
头-尾比较:旧头节点与新尾节点对比
尾-头比较:旧尾节点与新头节点对比
如果以上四种都不匹配,则通过key在旧节点中查找可复用节点。
Vue 3的Block Tree架构优化
Vue 3引入了多项革命性优化:
1. 静态提升(Static Hoisting)
// 编译前
<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(补丁标志)
// 编译器标记节点的动态部分
createVNode("div", {
class: _normalizeClass({ active: isActive })
}, null, 2 /* CLASS */) // 只需检查class变化| 标志值 | 含义 | 优化效果 |
|---|---|---|
| 1 | TEXT | 只检查文本 |
| 2 | CLASS | 只检查class |
| 4 | STYLE | 只检查style |
| 8 | PROPS | 只检查特定props |
| 16 | FULL_PROPS | 完整对比 |
3. 树结构打平(Tree Flattening)
将模板中的动态节点收集到扁平数组中,遍历时跳过静态节点,减少95%的无效比较。
4. 最长递增子序列优化
在处理节点移动时,找出最长的不需要移动的节点序列,最小化DOM操作次数。
问题2:Vuex和Pinia的区别及选择场景
核心架构对比
| 特性 | Vuex | Pinia |
|---|---|---|
| API设计 | State + Mutations + Actions + Getters | State + Actions + Getters(无Mutations) |
| 状态更新 | 必须通过commit触发Mutation | 直接修改state或在Actions中修改 |
| 异步处理 | Actions中调用Mutations | Actions直接修改状态 |
| TypeScript | 需手动配置类型声明 | 原生支持,自动类型推导 |
| 模块化 | 通过modules嵌套定义,需namespaced | 多个独立store文件,天然模块化 |
| 体积 | 约10-12KB | 约1KB |
| Vue版本 | Vue 2/3 | 主要为Vue 3设计 |
代码对比
Vuex写法:
// 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写法:
// 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请求/响应拦截器设计
二次封装设计
// 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关键设计点
Token自动注入:请求拦截器统一添加Authorization头
Token无感刷新:使用并发锁+请求队列,避免多次刷新请求
统一错误处理:响应拦截器统一处理业务错误和HTTP错误
请求重试机制:可添加指数退避重试策略
问题7:微信小程序生命周期
应用生命周期(App级)
在app.js中定义,管控整个小程序的启动与销毁:
App({
onLaunch(options) {
// 小程序首次启动触发,全局仅执行1次
// 适合:初始化全局数据、获取用户授权、请求全局配置
},
onShow(options) {
// 小程序启动/从后台切到前台触发,可多次执行
// 适合:刷新全局状态、重启定时器
},
onHide() {
// 小程序从前台切到后台触发
// 适合:暂停定时器、保存临时数据
},
onError(msg) {
// 小程序发生脚本错误时触发
// 适合:捕获全局错误、上报错误日志
},
onPageNotFound() {
// 访问不存在的页面时触发
// 适合:页面跳转兜底
}
})页面生命周期(Page级)
在页面的.js文件中定义,控制单个页面的加载与卸载:
Page({
onLoad(options) {
// 页面首次加载触发,仅执行1次
// 适合:接收页面参数、初始化页面数据、请求核心接口
},
onShow() {
// 页面显示/从后台切回前台触发,可多次执行
// 适合:刷新页面数据、启动页面定时器
},
onReady() {
// 页面初次渲染完成触发,仅执行1次
// 适合:操作DOM、初始化第三方组件
},
onHide() {
// 页面被隐藏触发(跳转其他页面或切后台)
// 适合:暂停定时器、保存临时表单数据
},
onUnload() {
// 页面被卸载触发(redirectTo、navigateBack)
// 适合:清除定时器、取消未完成的请求
},
onPullDownRefresh() {
// 监听用户下拉刷新
},
onReachBottom() {
// 页面上拉触底触发
// 适合:加载下一页数据
},
onShareAppMessage() {
// 用户点击分享
}
})核心区别
| 维度 | 应用生命周期 | 页面生命周期 |
|---|---|---|
| 作用范围 | 整个小程序 | 单个页面 |
| 定义位置 | app.js | 页面.js文件 |
| 执行次数 | onLaunch仅1次 | onLoad仅1次,onShow多次 |
| 执行顺序 | onLaunch → onShow | onLoad → onShow → onReady |
| 数据传递 | 通过globalData | 通过options参数 |
问题8:微信支付完整流程
完整支付流程
用户选择商品 → 前端请求下单 → 后端调用微信统一下单API
→ 获取prepay_id → 前端调起支付 → 用户输入密码
→ 微信异步通知后端 → 后端验签更新订单 → 前端查询订单状态具体实现
后端下单接口(Java Spring Boot):
@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);
}支付回调处理:
@PostMapping("/api/pay/notify")
public String payNotify(HttpServletRequest request) {
// 1. 验证签名
// 2. 解密回调数据
// 3. 更新订单状态(幂等处理)
// 4. 返回成功响应
return "<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>";
}小程序前端调起支付:
// 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 })
}
}
})
}
})异常处理要点
签名验证:必须验证回调签名,确保来源真实
幂等处理:使用商户订单号做幂等键,防止重复处理
超时处理:设置合理的支付超时时间
并发处理:防止重复支付,对按钮做防抖
补单机制:定时查询未确认订单的状态
问题9:性能优化具体案例
案例:电商列表页性能优化
问题发现:
使用Lighthouse检测,FCP(首次内容渲染)达到4.5秒
使用Webpack Bundle Analyzer发现打包体积3.2MB
长列表滚动卡顿,用户反馈体验差
优化策略:
1. 路由懒加载(体积减少40%)
const routes = [
{
path: '/product-list',
component: () => import(/* webpackChunkName: "product" */ '@/views/ProductList.vue')
}
]2. 第三方库按需引入
// 优化前:全量引入
import ElementPlus from 'element-plus'
// 优化后:按需引入
import { ElButton, ElCard, ElPagination } from 'element-plus'3. 虚拟滚动优化长列表
<template>
<RecycleScroller
:items="productList"
:item-size="120"
key-field="id"
v-slot="{ item }"
>
<ProductCard :product="item" />
</RecycleScroller>
</template>4. 图片懒加载+WebP格式
<template>
<img v-lazy="product.imageWebp" loading="lazy" />
</template>5. CDN加速+Gzip压缩
# nginx配置
gzip on;
gzip_types text/plain application/javascript application/x-javascript text/css;优化效果:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| JS总体积 | 3.2MB | 680KB | ↓78% |
| FCP时间 | 4.5s | 0.8s | ↑462% |
| Lighthouse评分 | 58分 | 92分 | ↑58% |
问题10:Canvas图片压缩的质量与压缩率平衡
实现方案
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)
})
}质量与压缩率平衡策略
根据图片尺寸动态调整质量
大图(>2MB):quality = 0.6
中图(1-2MB):quality = 0.7-0.8
小图(<1MB):quality = 0.8-0.9
确保OCR清晰度
保持最小分辨率(如1000px宽度)
使用
image/png格式保留文字边缘清晰度对于文档类图片,quality不低于0.85
渐进式压缩
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问答功能实现
技术架构
用户提问 → 向量化查询 → 知识库检索 → 上下文拼接 → LLM生成回答实现方案
1. 知识库向量化存储
// 使用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. 语义检索
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问答实现
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:多端电商应用技术架构设计
技术选型
┌─────────────────────────────────────────────┐
│ 表现层 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Web │ │ H5 │ │ 小程序 │ │
│ │ Vue3 │ │ Vue3 │ │ uni-app │ │
│ └─────────┘ └─────────┘ └─────────┘ │
├─────────────────────────────────────────────┤
│ 跨端框架层 │
│ uni-app / Taro │
├─────────────────────────────────────────────┤
│ 状态管理 │
│ Pinia │
├─────────────────────────────────────────────┤
│ 网络请求 │
│ Axios / uni.request │
├─────────────────────────────────────────────┤
│ 后端服务 │
│ Node.js / Java / Go │
└─────────────────────────────────────────────┘处理不同端差异的策略
1. 条件编译
// #ifdef H5
console.log('H5端特有逻辑')
// #endif
// #ifdef MP-WEIXIN
console.log('微信小程序特有逻辑')
// #endif
// #ifdef APP-PLUS
console.log('App端特有逻辑')
// #endif2. 平台适配层
// 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. 组件差异化
<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. 样式适配
// 使用rpx实现响应式布局
.product-card {
width: 750rpx;
padding: 20rpx;
// H5端特殊样式
/* #ifdef H5 */
max-width: 750px;
margin: 0 auto;
/* #endif */
}